feat(contacts): auto-complete contacts.

feat(items): auto-complete items.
feat(resources): resource columns feat.
feat(contacts): retrieve specific contact details.
This commit is contained in:
a.bouhuolia
2021-03-03 11:35:42 +02:00
parent d51d9a5038
commit ce875ccf4e
37 changed files with 693 additions and 219 deletions

View File

@@ -170,7 +170,10 @@ export default class AccountsController extends BaseController {
accountDTO accountDTO
); );
return res.status(200).send({ id: account.id }); return res.status(200).send({
id: account.id,
message: 'The account has been created successfully.',
});
} catch (error) { } catch (error) {
next(error); next(error);
} }
@@ -258,7 +261,11 @@ export default class AccountsController extends BaseController {
try { try {
await this.accountsService.activateAccount(tenantId, accountId, true); await this.accountsService.activateAccount(tenantId, accountId, true);
return res.status(200).send({ id: accountId });
return res.status(200).send({
id: accountId,
message: 'The account has been activated successfully.'
});
} catch (error) { } catch (error) {
next(error); next(error);
} }
@@ -276,7 +283,11 @@ export default class AccountsController extends BaseController {
try { try {
await this.accountsService.activateAccount(tenantId, accountId, false); await this.accountsService.activateAccount(tenantId, accountId, false);
return res.status(200).send({ id: accountId });
return res.status(200).send({
id: accountId,
message: 'The account has been inactivated successfully.',
});
} catch (error) { } catch (error) {
next(error); next(error);
} }

View File

@@ -1,8 +1,100 @@
import { check, param, query, body, ValidationChain } from 'express-validator'; import { check, param, query, body, ValidationChain } from 'express-validator';
import { Router, Request, Response, NextFunction } from 'express';
import { Inject } from 'typedi';
import BaseController from 'api/controllers/BaseController'; import BaseController from 'api/controllers/BaseController';
import ContactsService from 'services/Contacts/ContactsService';
import { DATATYPES_LENGTH } from 'data/DataTypes'; import { DATATYPES_LENGTH } from 'data/DataTypes';
import { Service } from 'typedi';
@Service()
export default class ContactsController extends BaseController { export default class ContactsController extends BaseController {
@Inject()
contactsService: ContactsService;
/**
* Express router.
*/
router() {
const router = Router();
router.get(
'/:id',
[param('id').exists().isNumeric().toInt()],
this.validationResult,
this.asyncMiddleware(this.getContact.bind(this))
);
router.get(
'/auto-complete',
[...this.autocompleteQuerySchema],
this.validationResult,
this.asyncMiddleware(this.autocompleteContacts.bind(this))
);
return router;
}
/**
* Auto-complete list query validation schema.
*/
get autocompleteQuerySchema() {
return [
query('column_sort_by').optional().trim().escape(),
query('sort_order').optional().isIn(['desc', 'asc']),
query('stringified_filter_roles').optional().isJSON(),
query('limit').optional().isNumeric().toInt(),
];
}
/**
* Retrieve details of the given contact.
* @param {Request} req -
* @param {Response} res -
* @param {NextFunction} next -
*/
async getContact(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const { id: contactId } = req.params;
try {
const contact = await this.contactsService.getContact(
tenantId,
contactId,
);
return res.status(200).send({
customer: this.transfromToResponse(contact),
});
} catch (error) {
next(error);
}
}
/**
* Retrieve auto-complete contacts list.
* @param {Request} req - Request object.
* @param {Response} res - Response object.
* @param {NextFunction} next
*/
async autocompleteContacts(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const filter = {
filterRoles: [],
sortOrder: 'asc',
columnSortBy: 'created_at',
limit: 10,
...this.matchedQueryData(req),
};
try {
const contacts = await this.contactsService.autocompleteContacts(
tenantId,
filter
);
return res.status(200).send({ contacts });
} catch (error) {
next(error);
}
}
/** /**
* @returns {ValidationChain[]} * @returns {ValidationChain[]}
*/ */

View File

@@ -4,12 +4,16 @@ import { check, query, param } from 'express-validator';
import { ServiceError } from 'exceptions'; import { ServiceError } from 'exceptions';
import BaseController from '../BaseController'; import BaseController from '../BaseController';
import InventoryAdjustmentService from 'services/Inventory/InventoryAdjustmentService'; import InventoryAdjustmentService from 'services/Inventory/InventoryAdjustmentService';
import DynamicListingService from 'services/DynamicListing/DynamicListService';
@Service() @Service()
export default class InventoryAdjustmentsController extends BaseController { export default class InventoryAdjustmentsController extends BaseController {
@Inject() @Inject()
inventoryAdjustmentService: InventoryAdjustmentService; inventoryAdjustmentService: InventoryAdjustmentService;
@Inject()
dynamicListService: DynamicListingService;
/** /**
* Router constructor. * Router constructor.
*/ */
@@ -42,6 +46,7 @@ export default class InventoryAdjustmentsController extends BaseController {
[...this.validateListQuerySchema], [...this.validateListQuerySchema],
this.validationResult, this.validationResult,
this.asyncMiddleware(this.getInventoryAdjustments.bind(this)), this.asyncMiddleware(this.getInventoryAdjustments.bind(this)),
this.dynamicListService.handlerErrorsToResponse,
this.handleServiceErrors this.handleServiceErrors
); );
return router; return router;
@@ -191,6 +196,9 @@ export default class InventoryAdjustmentsController extends BaseController {
const filter = { const filter = {
page: 1, page: 1,
pageSize: 12, pageSize: 12,
columnSortBy: 'created_at',
sortOrder: 'desc',
filterRoles: [],
...this.matchedQueryData(req), ...this.matchedQueryData(req),
}; };

View File

@@ -65,6 +65,11 @@ export default class ItemsController extends BaseController {
asyncMiddleware(this.deleteItem.bind(this)), asyncMiddleware(this.deleteItem.bind(this)),
this.handlerServiceErrors this.handlerServiceErrors
); );
router.get(
'/auto-complete',
this.autocompleteQuerySchema,
this.asyncMiddleware(this.autocompleteList.bind(this)),
);
router.get( router.get(
'/:id', '/:id',
[...this.validateSpecificItemSchema], [...this.validateSpecificItemSchema],
@@ -201,6 +206,48 @@ export default class ItemsController extends BaseController {
]; ];
} }
/**
* Validate autocomplete list query schema.
*/
get autocompleteQuerySchema() {
return [
query('column_sort_by').optional().trim().escape(),
query('sort_order').optional().isIn(['desc', 'asc']),
query('stringified_filter_roles').optional().isJSON(),
query('limit').optional().isNumeric().toInt(),
];
}
/**
* Auto-complete list.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
async autocompleteList(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const filter = {
filterRoles: [],
sortOrder: 'asc',
columnSortBy: 'created_at',
limit: 10,
...this.matchedQueryData(req),
};
try {
const items = await this.itemsService.autocompleteItems(
tenantId,
filter
);
return res.status(200).send({
items,
});
} catch (error) {
next(error);
}
}
/** /**
* Stores the given item details to the storage. * Stores the given item details to the storage.
* @param {Request} req * @param {Request} req
@@ -237,7 +284,7 @@ export default class ItemsController extends BaseController {
return res.status(200).send({ return res.status(200).send({
id: itemId, id: itemId,
message: 'The item has been edited successfully.' message: 'The item has been edited successfully.',
}); });
} catch (error) { } catch (error) {
next(error); next(error);
@@ -302,7 +349,7 @@ export default class ItemsController extends BaseController {
return res.status(200).send({ return res.status(200).send({
id: itemId, id: itemId,
message: 'The item has been deleted successfully.' message: 'The item has been deleted successfully.',
}); });
} catch (error) { } catch (error) {
next(error); next(error);
@@ -481,7 +528,9 @@ export default class ItemsController extends BaseController {
} }
if (error.errorType === 'ITEM_HAS_ASSOCIATED_INVENTORY_ADJUSTMENT') { if (error.errorType === 'ITEM_HAS_ASSOCIATED_INVENTORY_ADJUSTMENT') {
return res.status(400).send({ return res.status(400).send({
errors: [{ type: 'ITEM_HAS_ASSOCIATED_INVENTORY_ADJUSTMENT', code: 330 }], errors: [
{ type: 'ITEM_HAS_ASSOCIATED_INVENTORY_ADJUSTMENT', code: 330 },
],
}); });
} }
} }

View File

@@ -27,6 +27,7 @@ import FinancialStatements from 'api/controllers/FinancialStatements';
import Expenses from 'api/controllers/Expenses'; import Expenses from 'api/controllers/Expenses';
import Settings from 'api/controllers/Settings'; import Settings from 'api/controllers/Settings';
import Currencies from 'api/controllers/Currencies'; import Currencies from 'api/controllers/Currencies';
import Contacts from 'api/controllers/Contacts/Contacts';
import Customers from 'api/controllers/Contacts/Customers'; import Customers from 'api/controllers/Contacts/Customers';
import Vendors from 'api/controllers/Contacts/Vendors'; import Vendors from 'api/controllers/Contacts/Vendors';
import Sales from 'api/controllers/Sales' import Sales from 'api/controllers/Sales'
@@ -93,6 +94,7 @@ export default () => {
dashboard.use('/item_categories', Container.get(ItemCategories).router()); dashboard.use('/item_categories', Container.get(ItemCategories).router());
dashboard.use('/expenses', Container.get(Expenses).router()); dashboard.use('/expenses', Container.get(Expenses).router());
dashboard.use('/financial_statements', Container.get(FinancialStatements).router()); dashboard.use('/financial_statements', Container.get(FinancialStatements).router());
dashboard.use('/contacts', Container.get(Contacts).router());
dashboard.use('/customers', Container.get(Customers).router()); dashboard.use('/customers', Container.get(Customers).router());
dashboard.use('/vendors', Container.get(Vendors).router()); dashboard.use('/vendors', Container.get(Vendors).router());
dashboard.use('/sales', Container.get(Sales).router()); dashboard.use('/sales', Container.get(Sales).router());

View File

@@ -106,6 +106,10 @@ export default {
key: "number_prefix", key: "number_prefix",
type: "string", type: "string",
}, },
{
key: 'increment_mode',
type: 'string'
}
], ],
payment_receives: [ payment_receives: [
{ {

View File

@@ -205,3 +205,12 @@ export interface ICustomersFilter extends IDynamicListFilter {
pageSize?: number, pageSize?: number,
}; };
export interface IContactsAutoCompleteFilter {
limit: number,
keyword: string,
}
export interface IContactAutoCompleteItem {
displayName: string,
contactService: string,
}

View File

@@ -1,25 +1,28 @@
export interface IDynamicFilter { export interface IDynamicFilter {
setTableName(tableName: string): void; setTableName(tableName: string): void;
buildQuery(): void; buildQuery(): void;
} }
export interface IFilterRole { export interface IFilterRole {
fieldKey: string, fieldKey: string;
value: string, value: string;
condition?: string, condition?: string;
index?: number, index?: number;
comparator?: string, comparator?: string;
}; }
export interface IDynamicListFilterDTO { export interface IDynamicListFilterDTO {
customViewId?: number, customViewId?: number;
filterRoles?: IFilterRole[], filterRoles?: IFilterRole[];
columnSortBy: string, columnSortBy: string;
sortOrder: string, sortOrder: string;
} }
export interface IDynamicListService { export interface IDynamicListService {
dynamicList(tenantId: number, model: any, filter: IDynamicListFilterDTO): Promise<any>; dynamicList(
tenantId: number,
model: any,
filter: IDynamicListFilterDTO
): Promise<any>;
handlerErrorsToResponse(error, req, res, next): void; handlerErrorsToResponse(error, req, res, next): void;
} }

View File

@@ -77,3 +77,9 @@ export interface IItemsFilter extends IDynamicListFilter {
page: number, page: number,
pageSize: number, pageSize: number,
}; };
export interface IItemsAutoCompleteFilter {
limit: number,
keyword: string,
}

View File

@@ -1,3 +1,4 @@
import { IDynamicListFilter } from 'interfaces/DynamicFilter';
import { IItemEntry, IItemEntryDTO } from "./ItemEntry"; import { IItemEntry, IItemEntryDTO } from "./ItemEntry";
export interface ISaleInvoice { export interface ISaleInvoice {
@@ -36,7 +37,7 @@ export interface ISaleInvoiceEditDTO extends ISaleInvoiceDTO {
}; };
export interface ISalesInvoicesFilter{ export interface ISalesInvoicesFilter extends IDynamicListFilter{
page: number, page: number,
pageSize: number, pageSize: number,
}; };

View File

@@ -1,7 +1,5 @@
import { forEach, uniqBy } from 'lodash'; import { forEach, uniqBy } from 'lodash';
import { import { buildFilterRolesJoins } from 'lib/ViewRolesBuilder';
buildFilterRolesJoins,
} from 'lib/ViewRolesBuilder';
import { IModel } from 'interfaces'; import { IModel } from 'interfaces';
export default class DynamicFilter { export default class DynamicFilter {
@@ -20,7 +18,7 @@ export default class DynamicFilter {
/** /**
* Set filter. * Set filter.
* @param {*} filterRole - * @param {*} filterRole - Filter role.
*/ */
setFilter(filterRole) { setFilter(filterRole) {
filterRole.setModel(this.model); filterRole.setModel(this.model);
@@ -36,14 +34,22 @@ export default class DynamicFilter {
this.filters.forEach((filter) => { this.filters.forEach((filter) => {
const { filterRoles } = filter; const { filterRoles } = filter;
buildersCallbacks.push(filter.buildQuery()); buildersCallbacks.push(filter.buildQuery());
tableColumns.push(...(Array.isArray(filterRoles)) ? filterRoles : [filterRoles]); tableColumns.push(
...(Array.isArray(filterRoles) ? filterRoles : [filterRoles])
);
}); });
return (builder) => { return (builder) => {
buildersCallbacks.forEach((builderCallback) => { buildersCallbacks.forEach((builderCallback) => {
builderCallback(builder); builderCallback(builder);
}); });
buildFilterRolesJoins(this.model, uniqBy(tableColumns, 'columnKey'))(builder);
buildFilterRolesJoins(
this.model,
uniqBy(tableColumns, 'columnKey')
)(builder);
}; };
} }

View File

@@ -1,11 +1,11 @@
import { difference } from 'lodash'; import { difference } from 'lodash';
import DynamicFilterRoleAbstructor from 'lib/DynamicFilter/DynamicFilterRoleAbstructor'; import DynamicFilterRoleAbstructor from 'lib/DynamicFilter/DynamicFilterRoleAbstructor';
import { import { buildFilterQuery } from 'lib/ViewRolesBuilder';
buildFilterQuery,
} from 'lib/ViewRolesBuilder';
import { IFilterRole } from 'interfaces'; import { IFilterRole } from 'interfaces';
export default class FilterRoles extends DynamicFilterRoleAbstructor { export default class FilterRoles extends DynamicFilterRoleAbstructor {
filterRoles: IFilterRole[];
/** /**
* Constructor method. * Constructor method.
* @param {Array} filterRoles - * @param {Array} filterRoles -
@@ -13,6 +13,7 @@ export default class FilterRoles extends DynamicFilterRoleAbstructor {
*/ */
constructor(filterRoles: IFilterRole[]) { constructor(filterRoles: IFilterRole[]) {
super(); super();
this.filterRoles = filterRoles; this.filterRoles = filterRoles;
this.setResponseMeta(); this.setResponseMeta();
} }
@@ -23,9 +24,10 @@ export default class FilterRoles extends DynamicFilterRoleAbstructor {
*/ */
private buildLogicExpression(): string { private buildLogicExpression(): string {
let expression = ''; let expression = '';
this.filterRoles.forEach((role, index) => { this.filterRoles.forEach((role, index) => {
expression += (index === 0) ? expression +=
`${role.index} ` : `${role.condition} ${role.index} `; index === 0 ? `${role.index} ` : `${role.condition} ${role.index} `;
}); });
return expression.trim(); return expression.trim();
} }
@@ -45,7 +47,7 @@ export default class FilterRoles extends DynamicFilterRoleAbstructor {
*/ */
setResponseMeta() { setResponseMeta() {
this.responseMeta = { this.responseMeta = {
filterRoles: this.filterRoles filterRoles: this.filterRoles,
}; };
} }
} }

View File

@@ -1,8 +1,12 @@
import DynamicFilterRoleAbstructor from 'lib/DynamicFilter/DynamicFilterRoleAbstructor'; import DynamicFilterRoleAbstructor from 'lib/DynamicFilter/DynamicFilterRoleAbstructor';
import { getRoleFieldColumn, validateFieldKeyExistance } from 'lib/ViewRolesBuilder'; import {
getRoleFieldColumn,
validateFieldKeyExistance,
getTableFromRelationColumn,
} from 'lib/ViewRolesBuilder';
export default class DynamicFilterSortBy extends DynamicFilterRoleAbstructor { export default class DynamicFilterSortBy extends DynamicFilterRoleAbstructor {
sortRole: { fieldKey: string, order: string } = {}; sortRole: { fieldKey: string; order: string } = {};
/** /**
* Constructor method. * Constructor method.
@@ -19,6 +23,9 @@ export default class DynamicFilterSortBy extends DynamicFilterRoleAbstructor {
this.setResponseMeta(); this.setResponseMeta();
} }
/**
* Validate the given field key with the model.
*/
validate() { validate() {
validateFieldKeyExistance(this.model, this.sortRole.fieldKey); validateFieldKeyExistance(this.model, this.sortRole.fieldKey);
} }
@@ -27,15 +34,41 @@ export default class DynamicFilterSortBy extends DynamicFilterRoleAbstructor {
* Builds database query of sort by column on the given direction. * Builds database query of sort by column on the given direction.
*/ */
buildQuery() { buildQuery() {
return (builder) => { const fieldRelation = getRoleFieldColumn(
const fieldRelation = getRoleFieldColumn(this.model, this.sortRole.fieldKey); this.model,
this.sortRole.fieldKey
);
const comparatorColumn = const comparatorColumn =
fieldRelation.relationColumn || fieldRelation.relationColumn ||
`${this.tableName}.${fieldRelation.column}`; `${this.tableName}.${fieldRelation.column}`;
if (typeof fieldRelation.sortQuery !== 'undefined') {
return (builder) => {
fieldRelation.sortQuery(builder, this.sortRole);
};
}
return (builder) => {
if (this.sortRole.fieldKey) { if (this.sortRole.fieldKey) {
builder.orderBy(`${comparatorColumn}`, this.sortRole.order); builder.orderBy(`${comparatorColumn}`, this.sortRole.order);
} }
this.joinBuildQuery()(builder);
};
}
joinBuildQuery() {
const fieldColumn = getRoleFieldColumn(this.model, this.sortRole.fieldKey);
return (builder) => {
if (fieldColumn.relation) {
const joinTable = getTableFromRelationColumn(fieldColumn.relation);
builder.join(
joinTable,
`${this.model.tableName}.${fieldColumn.column}`,
'=',
fieldColumn.relation
);
}
}; };
} }

View File

@@ -1,9 +1,7 @@
import { omit } from 'lodash'; import { omit } from 'lodash';
import { IView, IViewRole } from 'interfaces'; import { IView, IViewRole } from 'interfaces';
import DynamicFilterRoleAbstructor from 'lib/DynamicFilter/DynamicFilterRoleAbstructor'; import DynamicFilterRoleAbstructor from 'lib/DynamicFilter/DynamicFilterRoleAbstructor';
import { import { buildFilterQuery } from 'lib/ViewRolesBuilder';
buildFilterQuery,
} from 'lib/ViewRolesBuilder';
export default class DynamicFilterViews extends DynamicFilterRoleAbstructor { export default class DynamicFilterViews extends DynamicFilterRoleAbstructor {
viewId: number; viewId: number;
@@ -38,7 +36,11 @@ export default class DynamicFilterViews extends DynamicFilterRoleAbstructor {
*/ */
buildQuery() { buildQuery() {
return (builder) => { return (builder) => {
buildFilterQuery(this.model, this.filterRoles, this.logicExpression)(builder); buildFilterQuery(
this.model,
this.filterRoles,
this.logicExpression
)(builder);
}; };
} }
@@ -49,11 +51,11 @@ export default class DynamicFilterViews extends DynamicFilterRoleAbstructor {
this.responseMeta = { this.responseMeta = {
view: { view: {
logicExpression: this.logicExpression, logicExpression: this.logicExpression,
filterRoles: this.filterRoles.map((filterRole) => filterRoles: this.filterRoles.map((filterRole) => ({
({ ...omit(filterRole, ['id', 'viewId']) }) ...omit(filterRole, ['id', 'viewId']),
), })),
customViewId: this.viewId, customViewId: this.viewId,
} },
}; };
} }
} }

View File

@@ -5,7 +5,10 @@ import Parser from 'lib/LogicEvaluation/Parser';
import QueryParser from 'lib/LogicEvaluation/QueryParser'; import QueryParser from 'lib/LogicEvaluation/QueryParser';
import { IFilterRole, IModel } from 'interfaces'; import { IFilterRole, IModel } from 'interfaces';
const numberRoleQueryBuilder = (role: IFilterRole, comparatorColumn: string) => { const numberRoleQueryBuilder = (
role: IFilterRole,
comparatorColumn: string
) => {
switch (role.comparator) { switch (role.comparator) {
case 'equals': case 'equals':
case 'equal': case 'equal':
@@ -72,7 +75,11 @@ const dateQueryBuilder = (role: IFilterRole, comparatorColumn: string) => {
case 'before': case 'before':
return (builder) => { return (builder) => {
const comparator = role.comparator === 'before' ? '<' : '>'; const comparator = role.comparator === 'before' ? '<' : '>';
const hasTimeFormat = moment(role.value, 'YYYY-MM-DD HH:MM', true).isValid(); const hasTimeFormat = moment(
role.value,
'YYYY-MM-DD HH:MM',
true
).isValid();
const targetDate = moment(role.value); const targetDate = moment(role.value);
const dateFormat = 'YYYY-MM-DD HH:MM:SS'; const dateFormat = 'YYYY-MM-DD HH:MM:SS';
@@ -88,7 +95,11 @@ const dateQueryBuilder = (role: IFilterRole, comparatorColumn: string) => {
}; };
case 'in': case 'in':
return (builder) => { return (builder) => {
const hasTimeFormat = moment(role.value, 'YYYY-MM-DD HH:MM', true).isValid(); const hasTimeFormat = moment(
role.value,
'YYYY-MM-DD HH:MM',
true
).isValid();
const dateFormat = 'YYYY-MM-DD HH:MM:SS'; const dateFormat = 'YYYY-MM-DD HH:MM:SS';
if (hasTimeFormat) { if (hasTimeFormat) {
@@ -112,7 +123,7 @@ const dateQueryBuilder = (role: IFilterRole, comparatorColumn: string) => {
*/ */
export function getRoleFieldColumn(model: IModel, fieldKey: string) { export function getRoleFieldColumn(model: IModel, fieldKey: string) {
const tableFields = model.fields; const tableFields = model.fields;
return (tableFields[fieldKey]) ? tableFields[fieldKey] : null; return tableFields[fieldKey] ? tableFields[fieldKey] : null;
} }
/** /**
@@ -122,9 +133,10 @@ export function getRoleFieldColumn(model: IModel, fieldKey: string) {
*/ */
export function buildRoleQuery(model: IModel, role: IFilterRole) { export function buildRoleQuery(model: IModel, role: IFilterRole) {
const fieldRelation = getRoleFieldColumn(model, role.fieldKey); const fieldRelation = getRoleFieldColumn(model, role.fieldKey);
const comparatorColumn = fieldRelation.relationColumn || `${model.tableName}.${fieldRelation.column}`; const comparatorColumn =
fieldRelation.relationColumn ||
`${model.tableName}.${fieldRelation.column}`;
//
if (typeof fieldRelation.query !== 'undefined') { if (typeof fieldRelation.query !== 'undefined') {
return (builder) => { return (builder) => {
fieldRelation.query(builder, role); fieldRelation.query(builder, role);
@@ -149,13 +161,13 @@ export function buildRoleQuery(model: IModel, role: IFilterRole) {
*/ */
export const getTableFromRelationColumn = (column: string) => { export const getTableFromRelationColumn = (column: string) => {
const splitedColumn = column.split('.'); const splitedColumn = column.split('.');
return (splitedColumn.length > 0) ? splitedColumn[0] : ''; return splitedColumn.length > 0 ? splitedColumn[0] : '';
}; };
/** /**
* Builds view roles join queries. * Builds view roles join queries.
* @param {String} tableName - * @param {String} tableName - Table name.
* @param {Array} roles - * @param {Array} roles - Roles.
*/ */
export function buildFilterRolesJoins(model: IModel, roles: IFilterRole[]) { export function buildFilterRolesJoins(model: IModel, roles: IFilterRole[]) {
return (builder) => { return (builder) => {
@@ -164,7 +176,13 @@ export function buildFilterRolesJoins(model: IModel, roles: IFilterRole[]) {
if (fieldColumn.relation) { if (fieldColumn.relation) {
const joinTable = getTableFromRelationColumn(fieldColumn.relation); const joinTable = getTableFromRelationColumn(fieldColumn.relation);
builder.join(joinTable, `${model.tableName}.${fieldColumn.column}`, '=', fieldColumn.relation);
builder.join(
joinTable,
`${model.tableName}.${fieldColumn.column}`,
'=',
fieldColumn.relation
);
} }
}); });
}; };
@@ -176,7 +194,12 @@ export function buildSortColumnJoin(model: IModel, sortColumnKey: string) {
if (fieldColumn.relation) { if (fieldColumn.relation) {
const joinTable = getTableFromRelationColumn(fieldColumn.relation); const joinTable = getTableFromRelationColumn(fieldColumn.relation);
builder.join(joinTable, `${model.tableName}.${fieldColumn.column}`, '=', fieldColumn.relation); builder.join(
joinTable,
`${model.tableName}.${fieldColumn.column}`,
'=',
fieldColumn.relation
);
} }
}; };
} }
@@ -187,7 +210,11 @@ export function buildSortColumnJoin(model: IModel, sortColumnKey: string) {
* @param {Array} roles - * @param {Array} roles -
* @return {Function} * @return {Function}
*/ */
export function buildFilterRolesQuery(model: IModel, roles: IFilterRole[], logicExpression: string = '') { export function buildFilterRolesQuery(
model: IModel,
roles: IFilterRole[],
logicExpression: string = ''
) {
const rolesIndexSet = {}; const rolesIndexSet = {};
roles.forEach((role) => { roles.forEach((role) => {
@@ -211,7 +238,11 @@ export function buildFilterRolesQuery(model: IModel, roles: IFilterRole[], logic
* @param {Array} roles - * @param {Array} roles -
* @param {String} logicExpression - * @param {String} logicExpression -
*/ */
export const buildFilterQuery = (model: IModel, roles: IFilterRole[], logicExpression: string) => { export const buildFilterQuery = (
model: IModel,
roles: IFilterRole[],
logicExpression: string
) => {
return (builder) => { return (builder) => {
buildFilterRolesQuery(model, roles, logicExpression)(builder); buildFilterRolesQuery(model, roles, logicExpression)(builder);
}; };
@@ -233,7 +264,6 @@ export function mapViewRolesToConditionals(viewRoles) {
})); }));
} }
export function mapFilterRolesToDynamicFilter(roles) { export function mapFilterRolesToDynamicFilter(roles) {
return roles.map((role) => ({ return roles.map((role) => ({
...role, ...role,
@@ -247,9 +277,14 @@ export function mapFilterRolesToDynamicFilter(roles) {
* @param {String} columnKey - * @param {String} columnKey -
* @param {String} sortDirection - * @param {String} sortDirection -
*/ */
export function buildSortColumnQuery(model: IModel, columnKey: string, sortDirection: string) { export function buildSortColumnQuery(
model: IModel,
columnKey: string,
sortDirection: string
) {
const fieldRelation = getRoleFieldColumn(model, columnKey); const fieldRelation = getRoleFieldColumn(model, columnKey);
const sortColumn = fieldRelation.relation || `${model.tableName}.${fieldRelation.column}`; const sortColumn =
fieldRelation.relation || `${model.tableName}.${fieldRelation.column}`;
return (builder) => { return (builder) => {
builder.orderBy(sortColumn, sortDirection); builder.orderBy(sortColumn, sortDirection);
@@ -257,22 +292,34 @@ export function buildSortColumnQuery(model: IModel, columnKey: string, sortDirec
}; };
} }
export function validateFilterLogicExpression(logicExpression: string, indexes) { export function validateFilterLogicExpression(
logicExpression: string,
indexes
) {
const logicExpIndexes = logicExpression.match(/\d+/g) || []; const logicExpIndexes = logicExpression.match(/\d+/g) || [];
const diff = difference(logicExpIndexes.map(Number), indexes); const diff = difference(logicExpIndexes.map(Number), indexes);
return (diff.length > 0) ? false : true; return diff.length > 0 ? false : true;
} }
export function validateRolesLogicExpression(logicExpression: string, roles: IFilterRole[]) { export function validateRolesLogicExpression(
return validateFilterLogicExpression(logicExpression, roles.map((r) => r.index)); logicExpression: string,
roles: IFilterRole[]
) {
return validateFilterLogicExpression(
logicExpression,
roles.map((r) => r.index)
);
} }
export function validateFieldKeyExistance(model: any, fieldKey: string) { export function validateFieldKeyExistance(model: any, fieldKey: string) {
return model?.fields?.[fieldKey] || false; return model?.fields?.[fieldKey] || false;
} }
export function validateFilterRolesFieldsExistance(model, filterRoles: IFilterRole[]) { export function validateFilterRolesFieldsExistance(
model,
filterRoles: IFilterRole[]
) {
return filterRoles.filter((filterRole: IFilterRole) => { return filterRoles.filter((filterRole: IFilterRole) => {
return !validateFieldKeyExistance(model, filterRole.fieldKey); return !validateFieldKeyExistance(model, filterRole.fieldKey);
}); });
@@ -287,8 +334,12 @@ export function getModelFieldsKeys(Model: IModel) {
const fields = Object.keys(Model.fields); const fields = Object.keys(Model.fields);
return fields.sort((a, b) => { return fields.sort((a, b) => {
if (a < b) { return -1; } if (a < b) {
if (a > b) { return 1; } return -1;
}
if (a > b) {
return 1;
}
return 0; return 0;
}); });
} }
@@ -302,5 +353,5 @@ export function getModelFields(Model: IModel) {
...field, ...field,
key: fieldKey, key: fieldKey,
}; };
}) });
} }

View File

@@ -215,19 +215,11 @@ export default class Account extends TenantModel {
label: 'Account name', label: 'Account name',
column: 'name', column: 'name',
columnType: 'string', columnType: 'string',
fieldType: 'text', fieldType: 'text',
}, },
type: { type: {
label: 'Account type', label: 'Account type',
column: 'account_type_id', column: 'account_type',
relation: 'account_types.id',
relationColumn: 'account_types.key',
fieldType: 'options',
optionsResource: 'AccountType',
optionsKey: 'key',
optionsLabel: 'label',
}, },
description: { description: {
label: 'Description', label: 'Description',

View File

@@ -74,13 +74,11 @@ export default class BillPayment extends TenantModel {
}; };
} }
/**
* Resource fields.
*/
static get fields() { static get fields() {
return { return {
created_at: {
label: 'Created at',
column: 'created_at',
columnType: 'date',
},
vendor: { vendor: {
lable: "Vendor name", lable: "Vendor name",
column: 'vendor_id', column: 'vendor_id',
@@ -96,7 +94,7 @@ export default class BillPayment extends TenantModel {
payment_account: { payment_account: {
label: "Payment account", label: "Payment account",
column: "payment_account_id", column: "payment_account_id",
relation: "accounts", relation: "accounts.id",
relationColumn: "accounts.name", relationColumn: "accounts.name",
fieldType: 'options', fieldType: 'options',
@@ -116,7 +114,7 @@ export default class BillPayment extends TenantModel {
columnType: 'date', columnType: 'date',
fieldType: 'date', fieldType: 'date',
}, },
reference: { reference_no: {
label: "Reference No.", label: "Reference No.",
column: "reference", column: "reference",
columnType: 'string', columnType: 'string',
@@ -127,7 +125,12 @@ export default class BillPayment extends TenantModel {
column: "description", column: "description",
columnType: 'string', columnType: 'string',
fieldType: 'text', fieldType: 'text',
} },
created_at: {
label: 'Created at',
column: 'created_at',
columnType: 'date',
},
} }
} }
} }

View File

@@ -165,12 +165,6 @@ export default class Expense extends TenantModel {
label: "Published", label: "Published",
column: "published", column: "published",
}, },
user: {
label: "User",
column: "user_id",
relation: "users.id",
relationColumn: "users.id",
},
created_at: { created_at: {
label: "Created at", label: "Created at",
column: "created_at", column: "created_at",

View File

@@ -79,4 +79,54 @@ export default class InventoryAdjustment extends TenantModel {
}, },
}; };
} }
/**
* Model defined fields.
*/
static get fields() {
return {
date: {
label: 'Date',
column: 'date',
columnType: 'date',
},
type: {
label: 'Adjustment type',
column: 'type',
options: [
{ key: 'increment', label: 'Increment', },
{ key: 'decrement', label: 'Decrement' },
],
},
adjustment_account: {
column: 'adjustment_account_id',
},
reason: {
label: 'Reason',
column: 'reason',
},
reference_no: {
label: 'Reference No.',
column: 'reference_no',
},
description: {
label: 'Description',
column: 'description',
},
user: {
label: 'User',
column: 'user_id',
},
published_at: {
label: 'Published at',
column: 'published_at'
},
created_at: {
label: 'Created at',
column: 'created_at',
columnType: 'date',
fieldType: 'date',
},
};
}
} }

View File

@@ -140,16 +140,19 @@ export default class Item extends TenantModel {
label: 'Cost account', label: 'Cost account',
column: 'cost_account_id', column: 'cost_account_id',
relation: 'accounts.id', relation: 'accounts.id',
relationColumn: 'accounts.name',
}, },
sell_account: { sell_account: {
label: 'Sell account', label: 'Sell account',
column: 'sell_account_id', column: 'sell_account_id',
relation: 'accounts.id', relation: 'accounts.id',
relationColumn: 'accounts.name',
}, },
inventory_account: { inventory_account: {
label: "Inventory account", label: "Inventory account",
column: 'inventory_account_id', column: 'inventory_account_id',
relation: 'accounts.id', relation: 'accounts.id',
relationColumn: 'accounts.name',
}, },
sell_description: { sell_description: {
label: "Sell description", label: "Sell description",
@@ -170,18 +173,21 @@ export default class Item extends TenantModel {
category: { category: {
label: "Category", label: "Category",
column: 'category_id', column: 'category_id',
relation: 'categories.id', relation: 'items_categories.id',
}, relationColumn: 'items_categories.name',
user: {
label: 'User',
column: 'user_id',
relation: 'users.id',
relationColumn: 'users.id',
}, },
// user: {
// label: 'User',
// column: 'user_id',
// relation: 'users.id',
// relationColumn: 'users.',
// },
created_at: { created_at: {
label: 'Created at', label: 'Created at',
column: 'created_at', column: 'created_at',
} columnType: 'date',
fieldType: 'date',
},
}; };
} }
} }

View File

@@ -91,6 +91,11 @@ export default class ItemCategory extends TenantModel {
}], }],
columnType: 'string', columnType: 'string',
}, },
count: {
label: 'Count',
column: 'count',
sortQuery: this.sortCountQuery
},
created_at: { created_at: {
label: 'Created at', label: 'Created at',
column: 'created_at', column: 'created_at',
@@ -98,4 +103,8 @@ export default class ItemCategory extends TenantModel {
}, },
}; };
} }
static sortCountQuery(query, role) {
query.orderBy('count', role.order);
}
} }

View File

@@ -1,5 +1,6 @@
import { Model } from 'objection'; import { Model } from 'objection';
import TenantModel from 'models/TenantModel'; import TenantModel from 'models/TenantModel';
import { query } from 'winston';
export default class ManualJournal extends TenantModel { export default class ManualJournal extends TenantModel {
/** /**
@@ -20,9 +21,7 @@ export default class ManualJournal extends TenantModel {
* Virtual attributes. * Virtual attributes.
*/ */
static get virtualAttributes() { static get virtualAttributes() {
return [ return ['isPublished'];
'isPublished',
];
} }
/** /**
@@ -33,6 +32,17 @@ export default class ManualJournal extends TenantModel {
return !!this.publishedAt; return !!this.publishedAt;
} }
/**
* Model modifiers.
*/
static get modifiers() {
return {
sortByStatus(query, order) {
query.orderByRaw(`PUBLISHED_AT IS NULL ${order}`);
},
};
}
/** /**
* Relationship mapping. * Relationship mapping.
*/ */
@@ -51,7 +61,7 @@ export default class ManualJournal extends TenantModel {
}, },
filter(query) { filter(query) {
query.orderBy('index', 'ASC'); query.orderBy('index', 'ASC');
} },
}, },
transactions: { transactions: {
relation: Model.HasManyRelation, relation: Model.HasManyRelation,
@@ -77,8 +87,8 @@ export default class ManualJournal extends TenantModel {
}, },
filter(query) { filter(query) {
query.where('model_name', 'ManualJournal'); query.where('model_name', 'ManualJournal');
} },
} },
}; };
} }
@@ -102,9 +112,10 @@ export default class ManualJournal extends TenantModel {
column: 'reference', column: 'reference',
columnType: 'string', columnType: 'string',
}, },
status: { journal_type: {
label: 'Status', label: 'Journal type',
column: 'status', column: 'journal_type',
columnType: 'string',
}, },
amount: { amount: {
label: 'Amount', label: 'Amount',
@@ -116,15 +127,12 @@ export default class ManualJournal extends TenantModel {
column: 'description', column: 'description',
columnType: 'string', columnType: 'string',
}, },
user: { status: {
label: 'User', label: 'Status',
column: 'user_id', column: 'status',
relation: 'users.id', sortQuery(query, role) {
relationColumn: 'users.id', query.modify('sortByStatus', role.order);
}, },
journal_type: {
label: 'Journal type',
column: 'journal_type',
}, },
created_at: { created_at: {
label: 'Created at', label: 'Created at',

View File

@@ -3,7 +3,7 @@ import TenantModel from 'models/TenantModel';
export default class PaymentReceive extends TenantModel { export default class PaymentReceive extends TenantModel {
/** /**
* Table name * Table name.
*/ */
static get tableName() { static get tableName() {
return 'payment_receives'; return 'payment_receives';
@@ -16,11 +16,14 @@ export default class PaymentReceive extends TenantModel {
return ['created_at', 'updated_at']; return ['created_at', 'updated_at'];
} }
/**
* Resourcable model.
*/
static get resourceable() { static get resourceable() {
return true; return true;
} }
/** /*
* Relationship mapping. * Relationship mapping.
*/ */
static get relationMappings() { static get relationMappings() {
@@ -79,6 +82,9 @@ export default class PaymentReceive extends TenantModel {
customer: { customer: {
label: 'Customer', label: 'Customer',
column: 'customer_id', column: 'customer_id',
relation: 'contacts.id',
relationColumn: 'contacts.displayName',
fieldType: 'options', fieldType: 'options',
optionsResource: 'customers', optionsResource: 'customers',
optionsKey: 'id', optionsKey: 'id',
@@ -102,10 +108,11 @@ export default class PaymentReceive extends TenantModel {
columnType: 'string', columnType: 'string',
fieldType: 'text', fieldType: 'text',
}, },
deposit_acount: { deposit_account: {
column: 'deposit_account_id', column: 'deposit_account_id',
lable: 'Deposit account', lable: 'Deposit account',
relation: "accounts.id", relation: "accounts.id",
relationColumn: 'accounts.name',
optionsResource: "account", optionsResource: "account",
}, },
payment_receive_no: { payment_receive_no: {
@@ -125,9 +132,6 @@ export default class PaymentReceive extends TenantModel {
column: 'created_at', column: 'created_at',
columnType: 'date', columnType: 'date',
}, },
user: {
},
}; };
} }
} }

View File

@@ -175,6 +175,9 @@ export default class SaleEstimate extends TenantModel {
customer: { customer: {
label: 'Customer', label: 'Customer',
column: 'customer_id', column: 'customer_id',
relation: 'contacts.id',
relationColumn: 'contacts.displayName',
fieldType: 'options', fieldType: 'options',
optionsResource: 'customers', optionsResource: 'customers',
optionsKey: 'id', optionsKey: 'id',

View File

@@ -3,6 +3,7 @@ import moment from 'moment';
import TenantModel from 'models/TenantModel'; import TenantModel from 'models/TenantModel';
import { defaultToTransform } from 'utils'; import { defaultToTransform } from 'utils';
import { QueryBuilder } from 'knex'; import { QueryBuilder } from 'knex';
import { query } from 'winston';
export default class SaleInvoice extends TenantModel { export default class SaleInvoice extends TenantModel {
/** /**
@@ -198,6 +199,18 @@ export default class SaleInvoice extends TenantModel {
*/ */
fromDate(query, fromDate) { fromDate(query, fromDate) {
query.where('invoice_date', '<=', fromDate) query.where('invoice_date', '<=', fromDate)
},
/**
* Sort the sale invoices by full-payment invoices.
*/
sortByStatus(query, order) {
query.orderByRaw(`PAYMENT_AMOUNT = BALANCE ${order}`);
},
/**
* Sort the sale invoices by the due amount.
*/
sortByDueAmount(query, order) {
query.orderByRaw(`BALANCE - PAYMENT_AMOUNT ${order}`)
} }
}; };
} }
@@ -293,6 +306,9 @@ export default class SaleInvoice extends TenantModel {
customer: { customer: {
label: 'Customer', label: 'Customer',
column: 'customer_id', column: 'customer_id',
relation: 'contacts.id',
relationColumn: 'contacts.displayName',
fieldType: 'options', fieldType: 'options',
optionsResource: 'customers', optionsResource: 'customers',
optionsKey: 'id', optionsKey: 'id',
@@ -351,6 +367,9 @@ export default class SaleInvoice extends TenantModel {
column: 'due_amount', column: 'due_amount',
columnType: 'number', columnType: 'number',
fieldType: 'number', fieldType: 'number',
sortQuery(query, role) {
query.modify('sortByDueAmount', role.order);
}
}, },
created_at: { created_at: {
label: 'Created at', label: 'Created at',
@@ -389,6 +408,9 @@ export default class SaleInvoice extends TenantModel {
break; break;
} }
}, },
sortQuery(query, role) {
query.modify('sortByStatus', role.order);
}
} }
}; };
} }

View File

@@ -16,22 +16,7 @@ import {
import DynamicListingService from 'services/DynamicListing/DynamicListService'; import DynamicListingService from 'services/DynamicListing/DynamicListService';
import events from 'subscribers/events'; import events from 'subscribers/events';
import AccountTypesUtils from 'lib/AccountTypes'; import AccountTypesUtils from 'lib/AccountTypes';
import { ERRORS } from './constants';
const ERRORS = {
ACCOUNT_NOT_FOUND: 'account_not_found',
ACCOUNT_TYPE_NOT_FOUND: 'account_type_not_found',
PARENT_ACCOUNT_NOT_FOUND: 'parent_account_not_found',
ACCOUNT_CODE_NOT_UNIQUE: 'account_code_not_unique',
ACCOUNT_NAME_NOT_UNIQUE: 'account_name_not_unqiue',
PARENT_ACCOUNT_HAS_DIFFERENT_TYPE: 'parent_has_different_type',
ACCOUNT_TYPE_NOT_ALLOWED_TO_CHANGE: 'account_type_not_allowed_to_changed',
ACCOUNT_PREDEFINED: 'account_predefined',
ACCOUNT_HAS_ASSOCIATED_TRANSACTIONS: 'account_has_associated_transactions',
PREDEFINED_ACCOUNTS: 'predefined_accounts',
ACCOUNTS_HAVE_TRANSACTIONS: 'accounts_have_transactions',
CLOSE_ACCOUNT_AND_TO_ACCOUNT_NOT_SAME_TYPE: 'close_account_and_to_account_not_same_type',
ACCOUNTS_NOT_FOUND: 'accounts_not_found',
}
@Service() @Service()
export default class AccountsService { export default class AccountsService {

View File

@@ -0,0 +1,16 @@
export const ERRORS = {
ACCOUNT_NOT_FOUND: 'account_not_found',
ACCOUNT_TYPE_NOT_FOUND: 'account_type_not_found',
PARENT_ACCOUNT_NOT_FOUND: 'parent_account_not_found',
ACCOUNT_CODE_NOT_UNIQUE: 'account_code_not_unique',
ACCOUNT_NAME_NOT_UNIQUE: 'account_name_not_unqiue',
PARENT_ACCOUNT_HAS_DIFFERENT_TYPE: 'parent_has_different_type',
ACCOUNT_TYPE_NOT_ALLOWED_TO_CHANGE: 'account_type_not_allowed_to_changed',
ACCOUNT_PREDEFINED: 'account_predefined',
ACCOUNT_HAS_ASSOCIATED_TRANSACTIONS: 'account_has_associated_transactions',
PREDEFINED_ACCOUNTS: 'predefined_accounts',
ACCOUNTS_HAVE_TRANSACTIONS: 'accounts_have_transactions',
CLOSE_ACCOUNT_AND_TO_ACCOUNT_NOT_SAME_TYPE:
'close_account_and_to_account_not_same_type',
ACCOUNTS_NOT_FOUND: 'accounts_not_found',
};

View File

@@ -3,7 +3,13 @@ import { difference, upperFirst, omit } from 'lodash';
import moment from 'moment'; import moment from 'moment';
import { ServiceError } from 'exceptions'; import { ServiceError } from 'exceptions';
import TenancyService from 'services/Tenancy/TenancyService'; import TenancyService from 'services/Tenancy/TenancyService';
import { IContact, IContactNewDTO, IContactEditDTO } from 'interfaces'; import DynamicListingService from 'services/DynamicListing/DynamicListService';
import {
IContact,
IContactNewDTO,
IContactEditDTO,
IContactsAutoCompleteFilter,
} from 'interfaces';
import JournalPoster from '../Accounting/JournalPoster'; import JournalPoster from '../Accounting/JournalPoster';
type TContactService = 'customer' | 'vendor'; type TContactService = 'customer' | 'vendor';
@@ -17,6 +23,9 @@ export default class ContactsService {
@Inject() @Inject()
tenancy: TenancyService; tenancy: TenancyService;
@Inject()
dynamicListService: DynamicListingService;
@Inject('logger') @Inject('logger')
logger: any; logger: any;
@@ -166,11 +175,40 @@ export default class ContactsService {
async getContact( async getContact(
tenantId: number, tenantId: number,
contactId: number, contactId: number,
contactService: TContactService contactService?: TContactService
) { ) {
return this.getContactByIdOrThrowError(tenantId, contactId, contactService); return this.getContactByIdOrThrowError(tenantId, contactId, contactService);
} }
/**
* Retrieve auto-complete contacts list.
* @param {number} tenantId -
* @param {IContactsAutoCompleteFilter} contactsFilter -
* @return {IContactAutoCompleteItem}
*/
async autocompleteContacts(
tenantId: number,
contactsFilter: IContactsAutoCompleteFilter
) {
const { Contact } = this.tenancy.models(tenantId);
// Dynamic list.
const dynamicList = await this.dynamicListService.dynamicList(
tenantId,
Contact,
contactsFilter,
);
// Retrieve contacts list by the given query.
const contacts = await Contact.query().onBuild((builder) => {
if (contactsFilter.keyword) {
builder.where('display_name', 'LIKE', contactsFilter.keyword);
}
dynamicList.buildQuery()(builder);
builder.limit(contactsFilter.limit);
});
return contacts;
}
/** /**
* Retrieve contacts or throw not found error if one of ids were not found * Retrieve contacts or throw not found error if one of ids were not found
* on the storage. * on the storage.
@@ -182,7 +220,7 @@ export default class ContactsService {
async getContactsOrThrowErrorNotFound( async getContactsOrThrowErrorNotFound(
tenantId: number, tenantId: number,
contactsIds: number[], contactsIds: number[],
contactService: TContactService, contactService: TContactService
) { ) {
const { Contact } = this.tenancy.models(tenantId); const { Contact } = this.tenancy.models(tenantId);
const contacts = await Contact.query() const contacts = await Contact.query()
@@ -240,10 +278,7 @@ export default class ContactsService {
journal.fromTransactions(contactsTransactions); journal.fromTransactions(contactsTransactions);
journal.removeEntries(); journal.removeEntries();
await Promise.all([ await Promise.all([journal.saveBalance(), journal.deleteEntries()]);
journal.saveBalance(),
journal.deleteEntries(),
]);
} }
/** /**
@@ -268,7 +303,6 @@ export default class ContactsService {
contactId, contactId,
contactService contactService
); );
// Should the opening balance date be required. // Should the opening balance date be required.
if (!contact.openingBalanceAt && !openingBalanceAt) { if (!contact.openingBalanceAt && !openingBalanceAt) {
throw new ServiceError(ERRORS.OPENING_BALANCE_DATE_REQUIRED); throw new ServiceError(ERRORS.OPENING_BALANCE_DATE_REQUIRED);

View File

@@ -173,7 +173,6 @@ export default class CustomersService {
tenantId, tenantId,
customerId, customerId,
}); });
// Retrieve the customer of throw not found service error. // Retrieve the customer of throw not found service error.
await this.getCustomerByIdOrThrowError(tenantId, customerId); await this.getCustomerByIdOrThrowError(tenantId, customerId);
@@ -375,7 +374,6 @@ export default class CustomersService {
const salesInvoice = await saleInvoiceRepository.find({ const salesInvoice = await saleInvoiceRepository.find({
customer_id: customerId, customer_id: customerId,
}); });
if (salesInvoice.length > 0) { if (salesInvoice.length > 0) {
throw new ServiceError('customer_has_invoices'); throw new ServiceError('customer_has_invoices');
} }

View File

@@ -1,4 +1,4 @@
import { Service, Inject } from "typedi"; import { Service, Inject } from 'typedi';
import validator from 'is-my-json-valid'; import validator from 'is-my-json-valid';
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
import { ServiceError } from 'exceptions'; import { ServiceError } from 'exceptions';
@@ -37,7 +37,11 @@ export default class DynamicListService implements IDynamicListService {
* @param {number} viewId * @param {number} viewId
* @return {Promise<IView>} * @return {Promise<IView>}
*/ */
private async getCustomViewOrThrowError(tenantId: number, viewId: number, model: IModel) { private async getCustomViewOrThrowError(
tenantId: number,
viewId: number,
model: IModel
) {
const { viewRepository } = this.tenancy.repositories(tenantId); const { viewRepository } = this.tenancy.repositories(tenantId);
const view = await viewRepository.findOneById(viewId, 'roles'); const view = await viewRepository.findOneById(viewId, 'roles');
@@ -67,8 +71,14 @@ export default class DynamicListService implements IDynamicListService {
* @param {IFilterRole[]} filterRoles * @param {IFilterRole[]} filterRoles
* @throws {ServiceError} * @throws {ServiceError}
*/ */
private validateRolesFieldsExistance(model: IModel, filterRoles: IFilterRole[]) { private validateRolesFieldsExistance(
const invalidFieldsKeys = validateFilterRolesFieldsExistance(model, filterRoles); model: IModel,
filterRoles: IFilterRole[]
) {
const invalidFieldsKeys = validateFilterRolesFieldsExistance(
model,
filterRoles
);
if (invalidFieldsKeys.length > 0) { if (invalidFieldsKeys.length > 0) {
throw new ServiceError(ERRORS.FILTER_ROLES_FIELDS_NOT_FOUND); throw new ServiceError(ERRORS.FILTER_ROLES_FIELDS_NOT_FOUND);
@@ -100,17 +110,24 @@ export default class DynamicListService implements IDynamicListService {
/** /**
* Dynamic listing. * Dynamic listing.
* @param {number} tenantId * @param {number} tenantId - Tenant id.
* @param {IModel} model * @param {IModel} model - Model.
* @param {IDynamicListFilterDTO} filter * @param {IDynamicListFilterDTO} filter - Dynamic filter DTO.
*/ */
public async dynamicList(tenantId: number, model: IModel, filter: IDynamicListFilterDTO) { public async dynamicList(
tenantId: number,
model: IModel,
filter: IDynamicListFilterDTO
) {
const dynamicFilter = new DynamicFilter(model); const dynamicFilter = new DynamicFilter(model);
// Custom view filter roles. // Custom view filter roles.
if (filter.customViewId) { if (filter.customViewId) {
const view = await this.getCustomViewOrThrowError(tenantId, filter.customViewId, model); const view = await this.getCustomViewOrThrowError(
tenantId,
filter.customViewId,
model
);
const viewFilter = new DynamicFilterViews(view); const viewFilter = new DynamicFilterViews(view);
dynamicFilter.setFilter(viewFilter); dynamicFilter.setFilter(viewFilter);
} }
@@ -119,7 +136,8 @@ export default class DynamicListService implements IDynamicListService {
this.validateSortColumnExistance(model, filter.columnSortBy); this.validateSortColumnExistance(model, filter.columnSortBy);
const sortByFilter = new DynamicFilterSortBy( const sortByFilter = new DynamicFilterSortBy(
filter.columnSortBy, filter.sortOrder filter.columnSortBy,
filter.sortOrder
); );
dynamicFilter.setFilter(sortByFilter); dynamicFilter.setFilter(sortByFilter);
} }
@@ -146,7 +164,12 @@ export default class DynamicListService implements IDynamicListService {
* @param {Response} res * @param {Response} res
* @param {NextFunction} next * @param {NextFunction} next
*/ */
public handlerErrorsToResponse(error: Error, req: Request, res: Response, next: NextFunction) { public handlerErrorsToResponse(
error: Error,
req: Request,
res: Response,
next: NextFunction
) {
if (error instanceof ServiceError) { if (error instanceof ServiceError) {
if (error.errorType === 'sort_column_not_found') { if (error.errorType === 'sort_column_not_found') {
return res.boom.badRequest(null, { return res.boom.badRequest(null, {

View File

@@ -17,6 +17,7 @@ import {
import events from 'subscribers/events'; import events from 'subscribers/events';
import AccountsService from 'services/Accounts/AccountsService'; import AccountsService from 'services/Accounts/AccountsService';
import ItemsService from 'services/Items/ItemsService'; import ItemsService from 'services/Items/ItemsService';
import DynamicListingService from 'services/DynamicListing/DynamicListService';
import HasTenancyService from 'services/Tenancy/TenancyService'; import HasTenancyService from 'services/Tenancy/TenancyService';
import InventoryService from './Inventory'; import InventoryService from './Inventory';
@@ -45,6 +46,9 @@ export default class InventoryAdjustmentService {
@Inject() @Inject()
inventoryService: InventoryService; inventoryService: InventoryService;
@Inject()
dynamicListService: DynamicListingService;
/** /**
* Transformes the quick inventory adjustment DTO to model object. * Transformes the quick inventory adjustment DTO to model object.
* @param {IQuickInventoryAdjustmentDTO} adjustmentDTO - * @param {IQuickInventoryAdjustmentDTO} adjustmentDTO -
@@ -208,7 +212,7 @@ export default class InventoryAdjustmentService {
await this.eventDispatcher.dispatch(events.inventoryAdjustment.onDeleted, { await this.eventDispatcher.dispatch(events.inventoryAdjustment.onDeleted, {
tenantId, tenantId,
inventoryAdjustmentId, inventoryAdjustmentId,
oldInventoryAdjustment oldInventoryAdjustment,
}); });
this.logger.info( this.logger.info(
'[inventory_adjustment] the adjustment deleted successfully.', '[inventory_adjustment] the adjustment deleted successfully.',
@@ -275,9 +279,18 @@ export default class InventoryAdjustmentService {
}> { }> {
const { InventoryAdjustment } = this.tenancy.models(tenantId); const { InventoryAdjustment } = this.tenancy.models(tenantId);
const dynamicFilter = await this.dynamicListService.dynamicList(
tenantId,
InventoryAdjustment,
adjustmentsFilter
);
const { results, pagination } = await InventoryAdjustment.query() const { results, pagination } = await InventoryAdjustment.query()
.withGraphFetched('entries.item') .onBuild((query) => {
.withGraphFetched('adjustmentAccount') query.withGraphFetched('entries.item');
query.withGraphFetched('adjustmentAccount');
dynamicFilter.buildQuery()(query);
})
.pagination(adjustmentsFilter.page - 1, adjustmentsFilter.pageSize); .pagination(adjustmentsFilter.page - 1, adjustmentsFilter.pageSize);
return { return {

View File

@@ -5,7 +5,13 @@ import {
EventDispatcherInterface, EventDispatcherInterface,
} from 'decorators/eventDispatcher'; } from 'decorators/eventDispatcher';
import events from 'subscribers/events'; import events from 'subscribers/events';
import { IItemsFilter, IItemsService, IItemDTO, IItem } from 'interfaces'; import {
IItemsFilter,
IItemsService,
IItemDTO,
IItem,
IItemsAutoCompleteFilter,
} from 'interfaces';
import DynamicListingService from 'services/DynamicListing/DynamicListService'; import DynamicListingService from 'services/DynamicListing/DynamicListService';
import TenancyService from 'services/Tenancy/TenancyService'; import TenancyService from 'services/Tenancy/TenancyService';
import { ServiceError } from 'exceptions'; import { ServiceError } from 'exceptions';
@@ -16,6 +22,7 @@ import {
ACCOUNT_TYPE, ACCOUNT_TYPE,
} from 'data/AccountTypes'; } from 'data/AccountTypes';
import { ERRORS } from './constants'; import { ERRORS } from './constants';
@Service() @Service()
export default class ItemsService implements IItemsService { export default class ItemsService implements IItemsService {
@Inject() @Inject()
@@ -496,6 +503,34 @@ export default class ItemsService implements IItemsService {
}; };
} }
/**
* Retrieve auto-complete items list.
* @param {number} tenantId -
* @param {IItemsAutoCompleteFilter} itemsFilter -
*/
public async autocompleteItems(
tenantId: number,
itemsFilter: IItemsAutoCompleteFilter
) {
const { Item } = this.tenancy.models(tenantId);
const dynamicFilter = await this.dynamicListService.dynamicList(
tenantId,
Item,
itemsFilter
);
const items = await Item.query().onBuild((builder) => {
builder.withGraphFetched('category');
dynamicFilter.buildQuery()(builder);
builder.limit(itemsFilter.limit);
});
// const autocompleteItems = this.transformAutoCompleteItems(items);
return items;
}
// transformAutoCompleteItems(item)
/** /**
* Validates the given item or items have no associated invoices or bills. * Validates the given item or items have no associated invoices or bills.
* @param {number} tenantId - Tenant id. * @param {number} tenantId - Tenant id.

View File

@@ -591,6 +591,7 @@ export default class BillPaymentsService {
.onBuild((builder) => { .onBuild((builder) => {
builder.withGraphFetched('vendor'); builder.withGraphFetched('vendor');
builder.withGraphFetched('paymentAccount'); builder.withGraphFetched('paymentAccount');
dynamicFilter.buildQuery()(builder); dynamicFilter.buildQuery()(builder);
}) })
.pagination(billPaymentsFilter.page - 1, billPaymentsFilter.pageSize); .pagination(billPaymentsFilter.page - 1, billPaymentsFilter.pageSize);

View File

@@ -27,17 +27,7 @@ import ItemsService from 'services/Items/ItemsService';
import ItemsEntriesService from 'services/Items/ItemsEntriesService'; import ItemsEntriesService from 'services/Items/ItemsEntriesService';
import JournalCommands from 'services/Accounting/JournalCommands'; import JournalCommands from 'services/Accounting/JournalCommands';
import JournalPosterService from 'services/Sales/JournalPosterService'; import JournalPosterService from 'services/Sales/JournalPosterService';
import { ERRORS } from './constants';
const ERRORS = {
BILL_NOT_FOUND: 'BILL_NOT_FOUND',
BILL_VENDOR_NOT_FOUND: 'BILL_VENDOR_NOT_FOUND',
BILL_ITEMS_NOT_PURCHASABLE: 'BILL_ITEMS_NOT_PURCHASABLE',
BILL_NUMBER_EXISTS: 'BILL_NUMBER_EXISTS',
BILL_ITEMS_NOT_FOUND: 'BILL_ITEMS_NOT_FOUND',
BILL_ENTRIES_IDS_NOT_FOUND: 'BILL_ENTRIES_IDS_NOT_FOUND',
NOT_PURCHASE_ABLE_ITEMS: 'NOT_PURCHASE_ABLE_ITEMS',
BILL_ALREADY_OPEN: 'BILL_ALREADY_OPEN',
};
/** /**
* Vendor bills services. * Vendor bills services.

View File

@@ -0,0 +1,10 @@
export const ERRORS = {
BILL_NOT_FOUND: 'BILL_NOT_FOUND',
BILL_VENDOR_NOT_FOUND: 'BILL_VENDOR_NOT_FOUND',
BILL_ITEMS_NOT_PURCHASABLE: 'BILL_ITEMS_NOT_PURCHASABLE',
BILL_NUMBER_EXISTS: 'BILL_NUMBER_EXISTS',
BILL_ITEMS_NOT_FOUND: 'BILL_ITEMS_NOT_FOUND',
BILL_ENTRIES_IDS_NOT_FOUND: 'BILL_ENTRIES_IDS_NOT_FOUND',
NOT_PURCHASE_ABLE_ITEMS: 'NOT_PURCHASE_ABLE_ITEMS',
BILL_ALREADY_OPEN: 'BILL_ALREADY_OPEN',
};

View File

@@ -29,20 +29,7 @@ import ItemsEntriesService from 'services/Items/ItemsEntriesService';
import CustomersService from 'services/Contacts/CustomersService'; import CustomersService from 'services/Contacts/CustomersService';
import SaleEstimateService from 'services/Sales/SalesEstimate'; import SaleEstimateService from 'services/Sales/SalesEstimate';
import JournalPosterService from './JournalPosterService'; import JournalPosterService from './JournalPosterService';
import SaleInvoiceRepository from 'repositories/SaleInvoiceRepository'; import { ERRORS } from './constants';
const ERRORS = {
INVOICE_NUMBER_NOT_UNIQUE: 'INVOICE_NUMBER_NOT_UNIQUE',
SALE_INVOICE_NOT_FOUND: 'SALE_INVOICE_NOT_FOUND',
SALE_INVOICE_ALREADY_DELIVERED: 'SALE_INVOICE_ALREADY_DELIVERED',
ENTRIES_ITEMS_IDS_NOT_EXISTS: 'ENTRIES_ITEMS_IDS_NOT_EXISTS',
NOT_SELLABLE_ITEMS: 'NOT_SELLABLE_ITEMS',
SALE_INVOICE_NO_NOT_UNIQUE: 'SALE_INVOICE_NO_NOT_UNIQUE',
INVOICE_AMOUNT_SMALLER_THAN_PAYMENT_AMOUNT:
'INVOICE_AMOUNT_SMALLER_THAN_PAYMENT_AMOUNT',
INVOICE_HAS_ASSOCIATED_PAYMENT_ENTRIES:
'INVOICE_HAS_ASSOCIATED_PAYMENT_ENTRIES',
};
/** /**
* Sales invoices service * Sales invoices service

View File

@@ -0,0 +1,12 @@
export const ERRORS = {
INVOICE_NUMBER_NOT_UNIQUE: 'INVOICE_NUMBER_NOT_UNIQUE',
SALE_INVOICE_NOT_FOUND: 'SALE_INVOICE_NOT_FOUND',
SALE_INVOICE_ALREADY_DELIVERED: 'SALE_INVOICE_ALREADY_DELIVERED',
ENTRIES_ITEMS_IDS_NOT_EXISTS: 'ENTRIES_ITEMS_IDS_NOT_EXISTS',
NOT_SELLABLE_ITEMS: 'NOT_SELLABLE_ITEMS',
SALE_INVOICE_NO_NOT_UNIQUE: 'SALE_INVOICE_NO_NOT_UNIQUE',
INVOICE_AMOUNT_SMALLER_THAN_PAYMENT_AMOUNT:
'INVOICE_AMOUNT_SMALLER_THAN_PAYMENT_AMOUNT',
INVOICE_HAS_ASSOCIATED_PAYMENT_ENTRIES:
'INVOICE_HAS_ASSOCIATED_PAYMENT_ENTRIES',
};