diff --git a/server/src/api/controllers/Accounts.ts b/server/src/api/controllers/Accounts.ts index 9c26daa88..82601b7b4 100644 --- a/server/src/api/controllers/Accounts.ts +++ b/server/src/api/controllers/Accounts.ts @@ -170,7 +170,10 @@ export default class AccountsController extends BaseController { 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) { next(error); } @@ -258,7 +261,11 @@ export default class AccountsController extends BaseController { try { 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) { next(error); } @@ -276,7 +283,11 @@ export default class AccountsController extends BaseController { try { 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) { next(error); } diff --git a/server/src/api/controllers/Contacts/Contacts.ts b/server/src/api/controllers/Contacts/Contacts.ts index 8666d19c1..7ff3ccade 100644 --- a/server/src/api/controllers/Contacts/Contacts.ts +++ b/server/src/api/controllers/Contacts/Contacts.ts @@ -1,8 +1,100 @@ 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 ContactsService from 'services/Contacts/ContactsService'; import { DATATYPES_LENGTH } from 'data/DataTypes'; +import { Service } from 'typedi'; +@Service() 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[]} */ diff --git a/server/src/api/controllers/Inventory/InventoryAdjustments.ts b/server/src/api/controllers/Inventory/InventoryAdjustments.ts index 99147ceb2..aee0077bc 100644 --- a/server/src/api/controllers/Inventory/InventoryAdjustments.ts +++ b/server/src/api/controllers/Inventory/InventoryAdjustments.ts @@ -4,12 +4,16 @@ import { check, query, param } from 'express-validator'; import { ServiceError } from 'exceptions'; import BaseController from '../BaseController'; import InventoryAdjustmentService from 'services/Inventory/InventoryAdjustmentService'; +import DynamicListingService from 'services/DynamicListing/DynamicListService'; @Service() export default class InventoryAdjustmentsController extends BaseController { @Inject() inventoryAdjustmentService: InventoryAdjustmentService; + @Inject() + dynamicListService: DynamicListingService; + /** * Router constructor. */ @@ -42,6 +46,7 @@ export default class InventoryAdjustmentsController extends BaseController { [...this.validateListQuerySchema], this.validationResult, this.asyncMiddleware(this.getInventoryAdjustments.bind(this)), + this.dynamicListService.handlerErrorsToResponse, this.handleServiceErrors ); return router; @@ -191,6 +196,9 @@ export default class InventoryAdjustmentsController extends BaseController { const filter = { page: 1, pageSize: 12, + columnSortBy: 'created_at', + sortOrder: 'desc', + filterRoles: [], ...this.matchedQueryData(req), }; diff --git a/server/src/api/controllers/Items.ts b/server/src/api/controllers/Items.ts index 3f205a1ea..a18f55c41 100644 --- a/server/src/api/controllers/Items.ts +++ b/server/src/api/controllers/Items.ts @@ -65,6 +65,11 @@ export default class ItemsController extends BaseController { asyncMiddleware(this.deleteItem.bind(this)), this.handlerServiceErrors ); + router.get( + '/auto-complete', + this.autocompleteQuerySchema, + this.asyncMiddleware(this.autocompleteList.bind(this)), + ); router.get( '/:id', [...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. * @param {Request} req @@ -237,7 +284,7 @@ export default class ItemsController extends BaseController { return res.status(200).send({ id: itemId, - message: 'The item has been edited successfully.' + message: 'The item has been edited successfully.', }); } catch (error) { next(error); @@ -302,7 +349,7 @@ export default class ItemsController extends BaseController { return res.status(200).send({ id: itemId, - message: 'The item has been deleted successfully.' + message: 'The item has been deleted successfully.', }); } catch (error) { next(error); @@ -481,7 +528,9 @@ export default class ItemsController extends BaseController { } if (error.errorType === 'ITEM_HAS_ASSOCIATED_INVENTORY_ADJUSTMENT') { return res.status(400).send({ - errors: [{ type: 'ITEM_HAS_ASSOCIATED_INVENTORY_ADJUSTMENT', code: 330 }], + errors: [ + { type: 'ITEM_HAS_ASSOCIATED_INVENTORY_ADJUSTMENT', code: 330 }, + ], }); } } diff --git a/server/src/api/index.ts b/server/src/api/index.ts index 0143b5200..49e773abb 100644 --- a/server/src/api/index.ts +++ b/server/src/api/index.ts @@ -27,6 +27,7 @@ import FinancialStatements from 'api/controllers/FinancialStatements'; import Expenses from 'api/controllers/Expenses'; import Settings from 'api/controllers/Settings'; import Currencies from 'api/controllers/Currencies'; +import Contacts from 'api/controllers/Contacts/Contacts'; import Customers from 'api/controllers/Contacts/Customers'; import Vendors from 'api/controllers/Contacts/Vendors'; import Sales from 'api/controllers/Sales' @@ -93,6 +94,7 @@ export default () => { dashboard.use('/item_categories', Container.get(ItemCategories).router()); dashboard.use('/expenses', Container.get(Expenses).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('/vendors', Container.get(Vendors).router()); dashboard.use('/sales', Container.get(Sales).router()); diff --git a/server/src/data/options.js b/server/src/data/options.js index ff9149027..5303dae27 100644 --- a/server/src/data/options.js +++ b/server/src/data/options.js @@ -106,6 +106,10 @@ export default { key: "number_prefix", type: "string", }, + { + key: 'increment_mode', + type: 'string' + } ], payment_receives: [ { diff --git a/server/src/interfaces/Contact.ts b/server/src/interfaces/Contact.ts index 862741d3c..a8519819c 100644 --- a/server/src/interfaces/Contact.ts +++ b/server/src/interfaces/Contact.ts @@ -205,3 +205,12 @@ export interface ICustomersFilter extends IDynamicListFilter { pageSize?: number, }; +export interface IContactsAutoCompleteFilter { + limit: number, + keyword: string, +} + +export interface IContactAutoCompleteItem { + displayName: string, + contactService: string, +} \ No newline at end of file diff --git a/server/src/interfaces/DynamicFilter.ts b/server/src/interfaces/DynamicFilter.ts index 800b20d18..b72971289 100644 --- a/server/src/interfaces/DynamicFilter.ts +++ b/server/src/interfaces/DynamicFilter.ts @@ -1,25 +1,28 @@ - export interface IDynamicFilter { setTableName(tableName: string): void; buildQuery(): void; } export interface IFilterRole { - fieldKey: string, - value: string, - condition?: string, - index?: number, - comparator?: string, -}; + fieldKey: string; + value: string; + condition?: string; + index?: number; + comparator?: string; +} export interface IDynamicListFilterDTO { - customViewId?: number, - filterRoles?: IFilterRole[], - columnSortBy: string, - sortOrder: string, + customViewId?: number; + filterRoles?: IFilterRole[]; + columnSortBy: string; + sortOrder: string; } export interface IDynamicListService { - dynamicList(tenantId: number, model: any, filter: IDynamicListFilterDTO): Promise; + dynamicList( + tenantId: number, + model: any, + filter: IDynamicListFilterDTO + ): Promise; handlerErrorsToResponse(error, req, res, next): void; -} \ No newline at end of file +} diff --git a/server/src/interfaces/Item.ts b/server/src/interfaces/Item.ts index 031ad1201..27b26b743 100644 --- a/server/src/interfaces/Item.ts +++ b/server/src/interfaces/Item.ts @@ -76,4 +76,10 @@ export interface IItemsFilter extends IDynamicListFilter { stringifiedFilterRoles?: string, page: number, pageSize: number, -}; \ No newline at end of file +}; + +export interface IItemsAutoCompleteFilter { + limit: number, + keyword: string, + +} \ No newline at end of file diff --git a/server/src/interfaces/SaleInvoice.ts b/server/src/interfaces/SaleInvoice.ts index 9e7770bef..ff0447109 100644 --- a/server/src/interfaces/SaleInvoice.ts +++ b/server/src/interfaces/SaleInvoice.ts @@ -1,3 +1,4 @@ +import { IDynamicListFilter } from 'interfaces/DynamicFilter'; import { IItemEntry, IItemEntryDTO } from "./ItemEntry"; export interface ISaleInvoice { @@ -36,7 +37,7 @@ export interface ISaleInvoiceEditDTO extends ISaleInvoiceDTO { }; -export interface ISalesInvoicesFilter{ +export interface ISalesInvoicesFilter extends IDynamicListFilter{ page: number, pageSize: number, }; \ No newline at end of file diff --git a/server/src/lib/DynamicFilter/DynamicFilter.ts b/server/src/lib/DynamicFilter/DynamicFilter.ts index cb795ed88..8e42e325e 100644 --- a/server/src/lib/DynamicFilter/DynamicFilter.ts +++ b/server/src/lib/DynamicFilter/DynamicFilter.ts @@ -1,7 +1,5 @@ import { forEach, uniqBy } from 'lodash'; -import { - buildFilterRolesJoins, -} from 'lib/ViewRolesBuilder'; +import { buildFilterRolesJoins } from 'lib/ViewRolesBuilder'; import { IModel } from 'interfaces'; export default class DynamicFilter { @@ -20,7 +18,7 @@ export default class DynamicFilter { /** * Set filter. - * @param {*} filterRole - + * @param {*} filterRole - Filter role. */ setFilter(filterRole) { filterRole.setModel(this.model); @@ -36,14 +34,22 @@ export default class DynamicFilter { this.filters.forEach((filter) => { const { filterRoles } = filter; + buildersCallbacks.push(filter.buildQuery()); - tableColumns.push(...(Array.isArray(filterRoles)) ? filterRoles : [filterRoles]); + tableColumns.push( + ...(Array.isArray(filterRoles) ? filterRoles : [filterRoles]) + ); }); + return (builder) => { buildersCallbacks.forEach((builderCallback) => { builderCallback(builder); }); - buildFilterRolesJoins(this.model, uniqBy(tableColumns, 'columnKey'))(builder); + + buildFilterRolesJoins( + this.model, + uniqBy(tableColumns, 'columnKey') + )(builder); }; } @@ -62,4 +68,4 @@ export default class DynamicFilter { }); return responseMeta; } -} \ No newline at end of file +} diff --git a/server/src/lib/DynamicFilter/DynamicFilterFilterRoles.ts b/server/src/lib/DynamicFilter/DynamicFilterFilterRoles.ts index afe4bd6ef..34374cb0f 100644 --- a/server/src/lib/DynamicFilter/DynamicFilterFilterRoles.ts +++ b/server/src/lib/DynamicFilter/DynamicFilterFilterRoles.ts @@ -1,11 +1,11 @@ import { difference } from 'lodash'; import DynamicFilterRoleAbstructor from 'lib/DynamicFilter/DynamicFilterRoleAbstructor'; -import { - buildFilterQuery, -} from 'lib/ViewRolesBuilder'; +import { buildFilterQuery } from 'lib/ViewRolesBuilder'; import { IFilterRole } from 'interfaces'; export default class FilterRoles extends DynamicFilterRoleAbstructor { + filterRoles: IFilterRole[]; + /** * Constructor method. * @param {Array} filterRoles - @@ -13,8 +13,9 @@ export default class FilterRoles extends DynamicFilterRoleAbstructor { */ constructor(filterRoles: IFilterRole[]) { super(); + this.filterRoles = filterRoles; - this.setResponseMeta(); + this.setResponseMeta(); } /** @@ -23,9 +24,10 @@ export default class FilterRoles extends DynamicFilterRoleAbstructor { */ private buildLogicExpression(): string { let expression = ''; + this.filterRoles.forEach((role, index) => { - expression += (index === 0) ? - `${role.index} ` : `${role.condition} ${role.index} `; + expression += + index === 0 ? `${role.index} ` : `${role.condition} ${role.index} `; }); return expression.trim(); } @@ -45,7 +47,7 @@ export default class FilterRoles extends DynamicFilterRoleAbstructor { */ setResponseMeta() { this.responseMeta = { - filterRoles: this.filterRoles + filterRoles: this.filterRoles, }; } -} \ No newline at end of file +} diff --git a/server/src/lib/DynamicFilter/DynamicFilterSortBy.ts b/server/src/lib/DynamicFilter/DynamicFilterSortBy.ts index 3d413aa5e..d019be118 100644 --- a/server/src/lib/DynamicFilter/DynamicFilterSortBy.ts +++ b/server/src/lib/DynamicFilter/DynamicFilterSortBy.ts @@ -1,8 +1,12 @@ 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 { - sortRole: { fieldKey: string, order: string } = {}; + sortRole: { fieldKey: string; order: string } = {}; /** * Constructor method. @@ -19,6 +23,9 @@ export default class DynamicFilterSortBy extends DynamicFilterRoleAbstructor { this.setResponseMeta(); } + /** + * Validate the given field key with the model. + */ validate() { 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. */ buildQuery() { - return (builder) => { - const fieldRelation = getRoleFieldColumn(this.model, this.sortRole.fieldKey); - const comparatorColumn = - fieldRelation.relationColumn || - `${this.tableName}.${fieldRelation.column}`; + const fieldRelation = getRoleFieldColumn( + this.model, + this.sortRole.fieldKey + ); + const comparatorColumn = + fieldRelation.relationColumn || + `${this.tableName}.${fieldRelation.column}`; + if (typeof fieldRelation.sortQuery !== 'undefined') { + return (builder) => { + fieldRelation.sortQuery(builder, this.sortRole); + }; + } + return (builder) => { if (this.sortRole.fieldKey) { 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 + ); + } }; } diff --git a/server/src/lib/DynamicFilter/DynamicFilterViews.ts b/server/src/lib/DynamicFilter/DynamicFilterViews.ts index 392fdad61..060efa5f5 100644 --- a/server/src/lib/DynamicFilter/DynamicFilterViews.ts +++ b/server/src/lib/DynamicFilter/DynamicFilterViews.ts @@ -1,9 +1,7 @@ import { omit } from 'lodash'; import { IView, IViewRole } from 'interfaces'; import DynamicFilterRoleAbstructor from 'lib/DynamicFilter/DynamicFilterRoleAbstructor'; -import { - buildFilterQuery, -} from 'lib/ViewRolesBuilder'; +import { buildFilterQuery } from 'lib/ViewRolesBuilder'; export default class DynamicFilterViews extends DynamicFilterRoleAbstructor { viewId: number; @@ -12,7 +10,7 @@ export default class DynamicFilterViews extends DynamicFilterRoleAbstructor { /** * Constructor method. - * @param {IView} view - + * @param {IView} view - */ constructor(view: IView) { super(); @@ -23,7 +21,7 @@ export default class DynamicFilterViews extends DynamicFilterRoleAbstructor { .replace('AND', '&&') .replace('OR', '||'); - this.setResponseMeta(); + this.setResponseMeta(); } /** @@ -32,13 +30,17 @@ export default class DynamicFilterViews extends DynamicFilterRoleAbstructor { buildLogicExpression() { return this.logicExpression; } - + /** * Builds database query of view roles. */ buildQuery() { 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 = { view: { logicExpression: this.logicExpression, - filterRoles: this.filterRoles.map((filterRole) => - ({ ...omit(filterRole, ['id', 'viewId']) }) - ), + filterRoles: this.filterRoles.map((filterRole) => ({ + ...omit(filterRole, ['id', 'viewId']), + })), customViewId: this.viewId, - } + }, }; } -} \ No newline at end of file +} diff --git a/server/src/lib/ViewRolesBuilder/index.ts b/server/src/lib/ViewRolesBuilder/index.ts index dfd997b3c..128d4729b 100644 --- a/server/src/lib/ViewRolesBuilder/index.ts +++ b/server/src/lib/ViewRolesBuilder/index.ts @@ -5,7 +5,10 @@ import Parser from 'lib/LogicEvaluation/Parser'; import QueryParser from 'lib/LogicEvaluation/QueryParser'; import { IFilterRole, IModel } from 'interfaces'; -const numberRoleQueryBuilder = (role: IFilterRole, comparatorColumn: string) => { +const numberRoleQueryBuilder = ( + role: IFilterRole, + comparatorColumn: string +) => { switch (role.comparator) { case 'equals': case 'equal': @@ -67,28 +70,36 @@ const textRoleQueryBuilder = (role: IFilterRole, comparatorColumn: string) => { }; const dateQueryBuilder = (role: IFilterRole, comparatorColumn: string) => { - switch(role.comparator) { + switch (role.comparator) { case 'after': case 'before': return (builder) => { 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 dateFormat = 'YYYY-MM-DD HH:MM:SS'; if (!hasTimeFormat) { if (role.comparator === 'before') { - targetDate.startOf('day'); + targetDate.startOf('day'); } else { - targetDate.endOf('day'); + targetDate.endOf('day'); } } const comparatorValue = targetDate.format(dateFormat); builder.where(comparatorColumn, comparator, comparatorValue); }; - case 'in': + case 'in': 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'; if (hasTimeFormat) { @@ -112,7 +123,7 @@ const dateQueryBuilder = (role: IFilterRole, comparatorColumn: string) => { */ export function getRoleFieldColumn(model: IModel, fieldKey: string) { 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) { 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') { return (builder) => { fieldRelation.query(builder, role); @@ -139,7 +151,7 @@ export function buildRoleQuery(model: IModel, role: IFilterRole) { case 'varchar': default: return textRoleQueryBuilder(role, comparatorColumn); - } + } } /** @@ -149,13 +161,13 @@ export function buildRoleQuery(model: IModel, role: IFilterRole) { */ export const getTableFromRelationColumn = (column: string) => { const splitedColumn = column.split('.'); - return (splitedColumn.length > 0) ? splitedColumn[0] : ''; + return splitedColumn.length > 0 ? splitedColumn[0] : ''; }; /** * Builds view roles join queries. - * @param {String} tableName - - * @param {Array} roles - + * @param {String} tableName - Table name. + * @param {Array} roles - Roles. */ export function buildFilterRolesJoins(model: IModel, roles: IFilterRole[]) { return (builder) => { @@ -164,7 +176,13 @@ export function buildFilterRolesJoins(model: IModel, roles: IFilterRole[]) { if (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) { 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 - * @return {Function} */ -export function buildFilterRolesQuery(model: IModel, roles: IFilterRole[], logicExpression: string = '') { +export function buildFilterRolesQuery( + model: IModel, + roles: IFilterRole[], + logicExpression: string = '' +) { const rolesIndexSet = {}; roles.forEach((role) => { @@ -211,7 +238,11 @@ export function buildFilterRolesQuery(model: IModel, roles: IFilterRole[], logic * @param {Array} roles - * @param {String} logicExpression - */ -export const buildFilterQuery = (model: IModel, roles: IFilterRole[], logicExpression: string) => { +export const buildFilterQuery = ( + model: IModel, + roles: IFilterRole[], + logicExpression: string +) => { return (builder) => { buildFilterRolesQuery(model, roles, logicExpression)(builder); }; @@ -233,7 +264,6 @@ export function mapViewRolesToConditionals(viewRoles) { })); } - export function mapFilterRolesToDynamicFilter(roles) { return roles.map((role) => ({ ...role, @@ -247,32 +277,49 @@ export function mapFilterRolesToDynamicFilter(roles) { * @param {String} columnKey - * @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 sortColumn = fieldRelation.relation || `${model.tableName}.${fieldRelation.column}`; + const sortColumn = + fieldRelation.relation || `${model.tableName}.${fieldRelation.column}`; return (builder) => { builder.orderBy(sortColumn, sortDirection); buildSortColumnJoin(model, columnKey)(builder); }; } - -export function validateFilterLogicExpression(logicExpression: string, indexes) { + +export function validateFilterLogicExpression( + logicExpression: string, + indexes +) { const logicExpIndexes = logicExpression.match(/\d+/g) || []; 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[]) { - return validateFilterLogicExpression(logicExpression, roles.map((r) => r.index)); +export function validateRolesLogicExpression( + logicExpression: string, + roles: IFilterRole[] +) { + return validateFilterLogicExpression( + logicExpression, + roles.map((r) => r.index) + ); } export function validateFieldKeyExistance(model: any, fieldKey: string) { return model?.fields?.[fieldKey] || false; } -export function validateFilterRolesFieldsExistance(model, filterRoles: IFilterRole[]) { +export function validateFilterRolesFieldsExistance( + model, + filterRoles: IFilterRole[] +) { return filterRoles.filter((filterRole: IFilterRole) => { return !validateFieldKeyExistance(model, filterRole.fieldKey); }); @@ -280,15 +327,19 @@ export function validateFilterRolesFieldsExistance(model, filterRoles: IFilterRo /** * Retrieve model fields keys. - * @param {IModel} Model + * @param {IModel} Model * @return {string[]} */ export function getModelFieldsKeys(Model: IModel) { const fields = Object.keys(Model.fields); return fields.sort((a, b) => { - if (a < b) { return -1; } - if (a > b) { return 1; } + if (a < b) { + return -1; + } + if (a > b) { + return 1; + } return 0; }); } @@ -302,5 +353,5 @@ export function getModelFields(Model: IModel) { ...field, key: fieldKey, }; - }) + }); } diff --git a/server/src/models/Account.js b/server/src/models/Account.js index 0ea21e42c..9f73c4ae6 100644 --- a/server/src/models/Account.js +++ b/server/src/models/Account.js @@ -215,19 +215,11 @@ export default class Account extends TenantModel { label: 'Account name', column: 'name', columnType: 'string', - fieldType: 'text', }, type: { label: 'Account type', - column: 'account_type_id', - relation: 'account_types.id', - relationColumn: 'account_types.key', - - fieldType: 'options', - optionsResource: 'AccountType', - optionsKey: 'key', - optionsLabel: 'label', + column: 'account_type', }, description: { label: 'Description', diff --git a/server/src/models/BillPayment.js b/server/src/models/BillPayment.js index 2ac029278..5b823f219 100644 --- a/server/src/models/BillPayment.js +++ b/server/src/models/BillPayment.js @@ -74,13 +74,11 @@ export default class BillPayment extends TenantModel { }; } + /** + * Resource fields. + */ static get fields() { return { - created_at: { - label: 'Created at', - column: 'created_at', - columnType: 'date', - }, vendor: { lable: "Vendor name", column: 'vendor_id', @@ -96,7 +94,7 @@ export default class BillPayment extends TenantModel { payment_account: { label: "Payment account", column: "payment_account_id", - relation: "accounts", + relation: "accounts.id", relationColumn: "accounts.name", fieldType: 'options', @@ -116,7 +114,7 @@ export default class BillPayment extends TenantModel { columnType: 'date', fieldType: 'date', }, - reference: { + reference_no: { label: "Reference No.", column: "reference", columnType: 'string', @@ -127,7 +125,12 @@ export default class BillPayment extends TenantModel { column: "description", columnType: 'string', fieldType: 'text', - } + }, + created_at: { + label: 'Created at', + column: 'created_at', + columnType: 'date', + }, } } } diff --git a/server/src/models/Expense.js b/server/src/models/Expense.js index 2335ebbc7..a514fbf83 100644 --- a/server/src/models/Expense.js +++ b/server/src/models/Expense.js @@ -165,12 +165,6 @@ export default class Expense extends TenantModel { label: "Published", column: "published", }, - user: { - label: "User", - column: "user_id", - relation: "users.id", - relationColumn: "users.id", - }, created_at: { label: "Created at", column: "created_at", diff --git a/server/src/models/InventoryAdjustment.js b/server/src/models/InventoryAdjustment.js index 9f2a7d779..a79bdcdab 100644 --- a/server/src/models/InventoryAdjustment.js +++ b/server/src/models/InventoryAdjustment.js @@ -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', + }, + }; + } } diff --git a/server/src/models/Item.js b/server/src/models/Item.js index 7e23db72f..83e622ea8 100644 --- a/server/src/models/Item.js +++ b/server/src/models/Item.js @@ -140,16 +140,19 @@ export default class Item extends TenantModel { label: 'Cost account', column: 'cost_account_id', relation: 'accounts.id', + relationColumn: 'accounts.name', }, sell_account: { label: 'Sell account', column: 'sell_account_id', relation: 'accounts.id', + relationColumn: 'accounts.name', }, inventory_account: { label: "Inventory account", column: 'inventory_account_id', relation: 'accounts.id', + relationColumn: 'accounts.name', }, sell_description: { label: "Sell description", @@ -170,18 +173,21 @@ export default class Item extends TenantModel { category: { label: "Category", column: 'category_id', - relation: 'categories.id', - }, - user: { - label: 'User', - column: 'user_id', - relation: 'users.id', - relationColumn: 'users.id', + relation: 'items_categories.id', + relationColumn: 'items_categories.name', }, + // user: { + // label: 'User', + // column: 'user_id', + // relation: 'users.id', + // relationColumn: 'users.', + // }, created_at: { label: 'Created at', column: 'created_at', - } + columnType: 'date', + fieldType: 'date', + }, }; } } diff --git a/server/src/models/ItemCategory.js b/server/src/models/ItemCategory.js index 2741c5174..808bc30b7 100644 --- a/server/src/models/ItemCategory.js +++ b/server/src/models/ItemCategory.js @@ -91,6 +91,11 @@ export default class ItemCategory extends TenantModel { }], columnType: 'string', }, + count: { + label: 'Count', + column: 'count', + sortQuery: this.sortCountQuery + }, created_at: { label: 'Created at', column: 'created_at', @@ -98,4 +103,8 @@ export default class ItemCategory extends TenantModel { }, }; } + + static sortCountQuery(query, role) { + query.orderBy('count', role.order); + } } diff --git a/server/src/models/ManualJournal.js b/server/src/models/ManualJournal.js index 996549226..a3330ef5f 100644 --- a/server/src/models/ManualJournal.js +++ b/server/src/models/ManualJournal.js @@ -1,5 +1,6 @@ import { Model } from 'objection'; import TenantModel from 'models/TenantModel'; +import { query } from 'winston'; export default class ManualJournal extends TenantModel { /** @@ -20,9 +21,7 @@ export default class ManualJournal extends TenantModel { * Virtual attributes. */ static get virtualAttributes() { - return [ - 'isPublished', - ]; + return ['isPublished']; } /** @@ -33,6 +32,17 @@ export default class ManualJournal extends TenantModel { return !!this.publishedAt; } + /** + * Model modifiers. + */ + static get modifiers() { + return { + sortByStatus(query, order) { + query.orderByRaw(`PUBLISHED_AT IS NULL ${order}`); + }, + }; + } + /** * Relationship mapping. */ @@ -51,7 +61,7 @@ export default class ManualJournal extends TenantModel { }, filter(query) { query.orderBy('index', 'ASC'); - } + }, }, transactions: { relation: Model.HasManyRelation, @@ -77,8 +87,8 @@ export default class ManualJournal extends TenantModel { }, filter(query) { query.where('model_name', 'ManualJournal'); - } - } + }, + }, }; } @@ -102,9 +112,10 @@ export default class ManualJournal extends TenantModel { column: 'reference', columnType: 'string', }, - status: { - label: 'Status', - column: 'status', + journal_type: { + label: 'Journal type', + column: 'journal_type', + columnType: 'string', }, amount: { label: 'Amount', @@ -116,15 +127,12 @@ export default class ManualJournal extends TenantModel { column: 'description', columnType: 'string', }, - user: { - label: 'User', - column: 'user_id', - relation: 'users.id', - relationColumn: 'users.id', - }, - journal_type: { - label: 'Journal type', - column: 'journal_type', + status: { + label: 'Status', + column: 'status', + sortQuery(query, role) { + query.modify('sortByStatus', role.order); + }, }, created_at: { label: 'Created at', diff --git a/server/src/models/PaymentReceive.js b/server/src/models/PaymentReceive.js index 24f7d7ded..5db76e5d1 100644 --- a/server/src/models/PaymentReceive.js +++ b/server/src/models/PaymentReceive.js @@ -3,7 +3,7 @@ import TenantModel from 'models/TenantModel'; export default class PaymentReceive extends TenantModel { /** - * Table name + * Table name. */ static get tableName() { return 'payment_receives'; @@ -16,11 +16,14 @@ export default class PaymentReceive extends TenantModel { return ['created_at', 'updated_at']; } + /** + * Resourcable model. + */ static get resourceable() { return true; } - /** + /* * Relationship mapping. */ static get relationMappings() { @@ -79,6 +82,9 @@ export default class PaymentReceive extends TenantModel { customer: { label: 'Customer', column: 'customer_id', + relation: 'contacts.id', + relationColumn: 'contacts.displayName', + fieldType: 'options', optionsResource: 'customers', optionsKey: 'id', @@ -102,10 +108,11 @@ export default class PaymentReceive extends TenantModel { columnType: 'string', fieldType: 'text', }, - deposit_acount: { + deposit_account: { column: 'deposit_account_id', lable: 'Deposit account', relation: "accounts.id", + relationColumn: 'accounts.name', optionsResource: "account", }, payment_receive_no: { @@ -125,9 +132,6 @@ export default class PaymentReceive extends TenantModel { column: 'created_at', columnType: 'date', }, - user: { - - }, }; } } diff --git a/server/src/models/SaleEstimate.js b/server/src/models/SaleEstimate.js index f83e016bd..20d6d5e20 100644 --- a/server/src/models/SaleEstimate.js +++ b/server/src/models/SaleEstimate.js @@ -175,6 +175,9 @@ export default class SaleEstimate extends TenantModel { customer: { label: 'Customer', column: 'customer_id', + relation: 'contacts.id', + relationColumn: 'contacts.displayName', + fieldType: 'options', optionsResource: 'customers', optionsKey: 'id', diff --git a/server/src/models/SaleInvoice.js b/server/src/models/SaleInvoice.js index 1cbea60e2..8dd272335 100644 --- a/server/src/models/SaleInvoice.js +++ b/server/src/models/SaleInvoice.js @@ -3,6 +3,7 @@ import moment from 'moment'; import TenantModel from 'models/TenantModel'; import { defaultToTransform } from 'utils'; import { QueryBuilder } from 'knex'; +import { query } from 'winston'; export default class SaleInvoice extends TenantModel { /** @@ -198,6 +199,18 @@ export default class SaleInvoice extends TenantModel { */ fromDate(query, 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: { label: 'Customer', column: 'customer_id', + relation: 'contacts.id', + relationColumn: 'contacts.displayName', + fieldType: 'options', optionsResource: 'customers', optionsKey: 'id', @@ -351,6 +367,9 @@ export default class SaleInvoice extends TenantModel { column: 'due_amount', columnType: 'number', fieldType: 'number', + sortQuery(query, role) { + query.modify('sortByDueAmount', role.order); + } }, created_at: { label: 'Created at', @@ -389,6 +408,9 @@ export default class SaleInvoice extends TenantModel { break; } }, + sortQuery(query, role) { + query.modify('sortByStatus', role.order); + } } }; } diff --git a/server/src/services/Accounts/AccountsService.ts b/server/src/services/Accounts/AccountsService.ts index bebd6ae2a..6d3647dfe 100644 --- a/server/src/services/Accounts/AccountsService.ts +++ b/server/src/services/Accounts/AccountsService.ts @@ -16,22 +16,7 @@ import { import DynamicListingService from 'services/DynamicListing/DynamicListService'; import events from 'subscribers/events'; import AccountTypesUtils from 'lib/AccountTypes'; - -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', -} +import { ERRORS } from './constants'; @Service() export default class AccountsService { diff --git a/server/src/services/Accounts/constants.ts b/server/src/services/Accounts/constants.ts new file mode 100644 index 000000000..13ff9cc34 --- /dev/null +++ b/server/src/services/Accounts/constants.ts @@ -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', +}; diff --git a/server/src/services/Contacts/ContactsService.ts b/server/src/services/Contacts/ContactsService.ts index 3b962bd0b..e152fa137 100644 --- a/server/src/services/Contacts/ContactsService.ts +++ b/server/src/services/Contacts/ContactsService.ts @@ -3,7 +3,13 @@ import { difference, upperFirst, omit } from 'lodash'; import moment from 'moment'; import { ServiceError } from 'exceptions'; 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'; type TContactService = 'customer' | 'vendor'; @@ -17,6 +23,9 @@ export default class ContactsService { @Inject() tenancy: TenancyService; + @Inject() + dynamicListService: DynamicListingService; + @Inject('logger') logger: any; @@ -166,11 +175,40 @@ export default class ContactsService { async getContact( tenantId: number, contactId: number, - contactService: TContactService + contactService?: TContactService ) { 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 * on the storage. @@ -182,7 +220,7 @@ export default class ContactsService { async getContactsOrThrowErrorNotFound( tenantId: number, contactsIds: number[], - contactService: TContactService, + contactService: TContactService ) { const { Contact } = this.tenancy.models(tenantId); const contacts = await Contact.query() @@ -240,10 +278,7 @@ export default class ContactsService { journal.fromTransactions(contactsTransactions); journal.removeEntries(); - await Promise.all([ - journal.saveBalance(), - journal.deleteEntries(), - ]); + await Promise.all([journal.saveBalance(), journal.deleteEntries()]); } /** @@ -268,7 +303,6 @@ export default class ContactsService { contactId, contactService ); - // Should the opening balance date be required. if (!contact.openingBalanceAt && !openingBalanceAt) { throw new ServiceError(ERRORS.OPENING_BALANCE_DATE_REQUIRED); diff --git a/server/src/services/Contacts/CustomersService.ts b/server/src/services/Contacts/CustomersService.ts index 5157582e2..f7fe7e0dc 100644 --- a/server/src/services/Contacts/CustomersService.ts +++ b/server/src/services/Contacts/CustomersService.ts @@ -173,7 +173,6 @@ export default class CustomersService { tenantId, customerId, }); - // Retrieve the customer of throw not found service error. await this.getCustomerByIdOrThrowError(tenantId, customerId); @@ -375,7 +374,6 @@ export default class CustomersService { const salesInvoice = await saleInvoiceRepository.find({ customer_id: customerId, }); - if (salesInvoice.length > 0) { throw new ServiceError('customer_has_invoices'); } diff --git a/server/src/services/DynamicListing/DynamicListService.ts b/server/src/services/DynamicListing/DynamicListService.ts index 27c21a4fb..d7abb5e94 100644 --- a/server/src/services/DynamicListing/DynamicListService.ts +++ b/server/src/services/DynamicListing/DynamicListService.ts @@ -1,4 +1,4 @@ -import { Service, Inject } from "typedi"; +import { Service, Inject } from 'typedi'; import validator from 'is-my-json-valid'; import { Request, Response, NextFunction } from 'express'; import { ServiceError } from 'exceptions'; @@ -33,11 +33,15 @@ export default class DynamicListService implements IDynamicListService { /** * Retreive custom view or throws error not found. - * @param {number} tenantId - * @param {number} viewId + * @param {number} tenantId + * @param {number} viewId * @return {Promise} */ - 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 view = await viewRepository.findOneById(viewId, 'roles'); @@ -49,7 +53,7 @@ export default class DynamicListService implements IDynamicListService { /** * Validates the sort column whether exists. - * @param {IModel} model + * @param {IModel} model * @param {string} columnSortBy - Sort column * @throws {ServiceError} */ @@ -63,12 +67,18 @@ export default class DynamicListService implements IDynamicListService { /** * Validates existance the fields of filter roles. - * @param {IModel} model - * @param {IFilterRole[]} filterRoles + * @param {IModel} model + * @param {IFilterRole[]} filterRoles * @throws {ServiceError} */ - private validateRolesFieldsExistance(model: IModel, filterRoles: IFilterRole[]) { - const invalidFieldsKeys = validateFilterRolesFieldsExistance(model, filterRoles); + private validateRolesFieldsExistance( + model: IModel, + filterRoles: IFilterRole[] + ) { + const invalidFieldsKeys = validateFilterRolesFieldsExistance( + model, + filterRoles + ); if (invalidFieldsKeys.length > 0) { throw new ServiceError(ERRORS.FILTER_ROLES_FIELDS_NOT_FOUND); @@ -77,7 +87,7 @@ export default class DynamicListService implements IDynamicListService { /** * Validates filter roles schema. - * @param {IFilterRole[]} filterRoles + * @param {IFilterRole[]} filterRoles */ private validateFilterRolesSchema(filterRoles: IFilterRole[]) { const validate = validator({ @@ -100,17 +110,24 @@ export default class DynamicListService implements IDynamicListService { /** * Dynamic listing. - * @param {number} tenantId - * @param {IModel} model - * @param {IDynamicListFilterDTO} filter + * @param {number} tenantId - Tenant id. + * @param {IModel} model - Model. + * @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); // Custom view filter roles. 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); dynamicFilter.setFilter(viewFilter); } @@ -119,7 +136,8 @@ export default class DynamicListService implements IDynamicListService { this.validateSortColumnExistance(model, filter.columnSortBy); const sortByFilter = new DynamicFilterSortBy( - filter.columnSortBy, filter.sortOrder + filter.columnSortBy, + filter.sortOrder ); dynamicFilter.setFilter(sortByFilter); } @@ -141,12 +159,17 @@ export default class DynamicListService implements IDynamicListService { /** * Middleware to catch services errors - * @param {Error} error - * @param {Request} req - * @param {Response} res - * @param {NextFunction} next + * @param {Error} error + * @param {Request} req + * @param {Response} res + * @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.errorType === 'sort_column_not_found') { return res.boom.badRequest(null, { @@ -171,4 +194,4 @@ export default class DynamicListService implements IDynamicListService { } next(error); } -} \ No newline at end of file +} diff --git a/server/src/services/Inventory/InventoryAdjustmentService.ts b/server/src/services/Inventory/InventoryAdjustmentService.ts index 2f1507354..b3fd6492f 100644 --- a/server/src/services/Inventory/InventoryAdjustmentService.ts +++ b/server/src/services/Inventory/InventoryAdjustmentService.ts @@ -17,6 +17,7 @@ import { import events from 'subscribers/events'; import AccountsService from 'services/Accounts/AccountsService'; import ItemsService from 'services/Items/ItemsService'; +import DynamicListingService from 'services/DynamicListing/DynamicListService'; import HasTenancyService from 'services/Tenancy/TenancyService'; import InventoryService from './Inventory'; @@ -45,6 +46,9 @@ export default class InventoryAdjustmentService { @Inject() inventoryService: InventoryService; + @Inject() + dynamicListService: DynamicListingService; + /** * Transformes the quick inventory adjustment DTO to model object. * @param {IQuickInventoryAdjustmentDTO} adjustmentDTO - @@ -208,7 +212,7 @@ export default class InventoryAdjustmentService { await this.eventDispatcher.dispatch(events.inventoryAdjustment.onDeleted, { tenantId, inventoryAdjustmentId, - oldInventoryAdjustment + oldInventoryAdjustment, }); this.logger.info( '[inventory_adjustment] the adjustment deleted successfully.', @@ -275,9 +279,18 @@ export default class InventoryAdjustmentService { }> { const { InventoryAdjustment } = this.tenancy.models(tenantId); + const dynamicFilter = await this.dynamicListService.dynamicList( + tenantId, + InventoryAdjustment, + adjustmentsFilter + ); const { results, pagination } = await InventoryAdjustment.query() - .withGraphFetched('entries.item') - .withGraphFetched('adjustmentAccount') + .onBuild((query) => { + query.withGraphFetched('entries.item'); + query.withGraphFetched('adjustmentAccount'); + + dynamicFilter.buildQuery()(query); + }) .pagination(adjustmentsFilter.page - 1, adjustmentsFilter.pageSize); return { diff --git a/server/src/services/Items/ItemsService.ts b/server/src/services/Items/ItemsService.ts index f6d91557a..8b9b76044 100644 --- a/server/src/services/Items/ItemsService.ts +++ b/server/src/services/Items/ItemsService.ts @@ -5,7 +5,13 @@ import { EventDispatcherInterface, } from 'decorators/eventDispatcher'; 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 TenancyService from 'services/Tenancy/TenancyService'; import { ServiceError } from 'exceptions'; @@ -16,6 +22,7 @@ import { ACCOUNT_TYPE, } from 'data/AccountTypes'; import { ERRORS } from './constants'; + @Service() export default class ItemsService implements IItemsService { @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. * @param {number} tenantId - Tenant id. diff --git a/server/src/services/Purchases/BillPayments.ts b/server/src/services/Purchases/BillPayments.ts index d229bcddc..c4a42a636 100644 --- a/server/src/services/Purchases/BillPayments.ts +++ b/server/src/services/Purchases/BillPayments.ts @@ -591,6 +591,7 @@ export default class BillPaymentsService { .onBuild((builder) => { builder.withGraphFetched('vendor'); builder.withGraphFetched('paymentAccount'); + dynamicFilter.buildQuery()(builder); }) .pagination(billPaymentsFilter.page - 1, billPaymentsFilter.pageSize); diff --git a/server/src/services/Purchases/Bills.ts b/server/src/services/Purchases/Bills.ts index 1e44b3cdc..bf3ab1137 100644 --- a/server/src/services/Purchases/Bills.ts +++ b/server/src/services/Purchases/Bills.ts @@ -27,17 +27,7 @@ import ItemsService from 'services/Items/ItemsService'; import ItemsEntriesService from 'services/Items/ItemsEntriesService'; import JournalCommands from 'services/Accounting/JournalCommands'; import JournalPosterService from 'services/Sales/JournalPosterService'; - -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', -}; +import { ERRORS } from './constants'; /** * Vendor bills services. diff --git a/server/src/services/Purchases/constants.ts b/server/src/services/Purchases/constants.ts new file mode 100644 index 000000000..e73fbb71b --- /dev/null +++ b/server/src/services/Purchases/constants.ts @@ -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', +}; diff --git a/server/src/services/Sales/SalesInvoices.ts b/server/src/services/Sales/SalesInvoices.ts index bbfa3a863..b647966cd 100644 --- a/server/src/services/Sales/SalesInvoices.ts +++ b/server/src/services/Sales/SalesInvoices.ts @@ -29,20 +29,7 @@ import ItemsEntriesService from 'services/Items/ItemsEntriesService'; import CustomersService from 'services/Contacts/CustomersService'; import SaleEstimateService from 'services/Sales/SalesEstimate'; import JournalPosterService from './JournalPosterService'; -import SaleInvoiceRepository from 'repositories/SaleInvoiceRepository'; - -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', -}; +import { ERRORS } from './constants'; /** * Sales invoices service diff --git a/server/src/services/Sales/constants.ts b/server/src/services/Sales/constants.ts new file mode 100644 index 000000000..27d0a3737 --- /dev/null +++ b/server/src/services/Sales/constants.ts @@ -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', +};