mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-17 13:20:31 +00:00
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:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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[]}
|
||||
*/
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
|
||||
|
||||
@@ -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 },
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -106,6 +106,10 @@ export default {
|
||||
key: "number_prefix",
|
||||
type: "string",
|
||||
},
|
||||
{
|
||||
key: 'increment_mode',
|
||||
type: 'string'
|
||||
}
|
||||
],
|
||||
payment_receives: [
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,4 +76,10 @@ export interface IItemsFilter extends IDynamicListFilter {
|
||||
stringifiedFilterRoles?: string,
|
||||
page: number,
|
||||
pageSize: number,
|
||||
};
|
||||
};
|
||||
|
||||
export interface IItemsAutoCompleteFilter {
|
||||
limit: number,
|
||||
keyword: string,
|
||||
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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: {
|
||||
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
16
server/src/services/Accounts/constants.ts
Normal file
16
server/src/services/Accounts/constants.ts
Normal 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',
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
10
server/src/services/Purchases/constants.ts
Normal file
10
server/src/services/Purchases/constants.ts
Normal 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',
|
||||
};
|
||||
@@ -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
|
||||
|
||||
12
server/src/services/Sales/constants.ts
Normal file
12
server/src/services/Sales/constants.ts
Normal 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',
|
||||
};
|
||||
Reference in New Issue
Block a user