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
);
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);
}

View File

@@ -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[]}
*/

View File

@@ -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),
};

View File

@@ -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 },
],
});
}
}

View File

@@ -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());

View File

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

View File

@@ -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,
}

View File

@@ -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<any>;
dynamicList(
tenantId: number,
model: any,
filter: IDynamicListFilterDTO
): Promise<any>;
handlerErrorsToResponse(error, req, res, next): void;
}
}

View File

@@ -76,4 +76,10 @@ export interface IItemsFilter extends IDynamicListFilter {
stringifiedFilterRoles?: string,
page: 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";
export interface ISaleInvoice {
@@ -36,7 +37,7 @@ export interface ISaleInvoiceEditDTO extends ISaleInvoiceDTO {
};
export interface ISalesInvoicesFilter{
export interface ISalesInvoicesFilter extends IDynamicListFilter{
page: number,
pageSize: number,
};

View File

@@ -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;
}
}
}

View File

@@ -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,
};
}
}
}

View File

@@ -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
);
}
};
}

View File

@@ -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,
}
},
};
}
}
}

View File

@@ -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,
};
})
});
}

View File

@@ -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',

View File

@@ -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',
},
}
}
}

View File

@@ -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",

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',
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',
},
};
}
}

View File

@@ -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);
}
}

View File

@@ -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',

View File

@@ -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: {
},
};
}
}

View File

@@ -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',

View File

@@ -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);
}
}
};
}

View File

@@ -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 {

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 { 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);

View File

@@ -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');
}

View File

@@ -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<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 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);
}
}
}

View File

@@ -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 {

View File

@@ -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.

View File

@@ -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);

View File

@@ -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.

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 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

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',
};