feat: optimize dynamic list service.

feat: inactive mode for accounts, items, customers and vendors services.
This commit is contained in:
a.bouhuolia
2021-07-29 08:46:41 +02:00
parent 720dc5b7d7
commit 9186076676
80 changed files with 2748 additions and 1806 deletions

View File

@@ -29,11 +29,9 @@ export default class AccountsController extends BaseController {
router.get( router.get(
'/transactions', '/transactions',
[ [query('account_id').optional().isInt().toInt()],
query('account_id').optional().isInt().toInt(),
],
this.asyncMiddleware(this.accountTransactions.bind(this)), this.asyncMiddleware(this.accountTransactions.bind(this)),
this.catchServiceErrors, this.catchServiceErrors
); );
router.post( router.post(
'/:id/activate', '/:id/activate',
@@ -136,6 +134,8 @@ export default class AccountsController extends BaseController {
query('column_sort_by').optional(), query('column_sort_by').optional(),
query('sort_order').optional().isIn(['desc', 'asc']), query('sort_order').optional().isIn(['desc', 'asc']),
query('inactive_mode').optional().isBoolean().toBoolean(),
]; ];
} }
@@ -213,7 +213,9 @@ export default class AccountsController extends BaseController {
tenantId, tenantId,
accountId accountId
); );
return res.status(200).send({ account: this.transfromToResponse(account) }); return res
.status(200)
.send({ account: this.transfromToResponse(account) });
} catch (error) { } catch (error) {
next(error); next(error);
} }
@@ -256,7 +258,7 @@ export default class AccountsController extends BaseController {
return res.status(200).send({ return res.status(200).send({
id: accountId, id: accountId,
message: 'The account has been activated successfully.' message: 'The account has been activated successfully.',
}); });
} catch (error) { } catch (error) {
next(error); next(error);
@@ -291,22 +293,24 @@ export default class AccountsController extends BaseController {
* @param {Response} res * @param {Response} res
* @param {Response} * @param {Response}
*/ */
async getAccountsList(req: Request, res: Response, next: NextFunction) { public async getAccountsList(
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId } = req; const { tenantId } = req;
const filter: IAccountsFilter = {
filterRoles: [], // Filter query.
const filter = {
sortOrder: 'asc', sortOrder: 'asc',
columnSortBy: 'name', columnSortBy: 'name',
inactiveMode: false,
...this.matchedQueryData(req), ...this.matchedQueryData(req),
}; };
if (filter.stringifiedFilterRoles) {
filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles);
}
try { try {
const { const { accounts, filterMeta } =
accounts, await this.accountsService.getAccountsList(tenantId, filter);
filterMeta,
} = await this.accountsService.getAccountsList(tenantId, filter);
return res.status(200).send({ return res.status(200).send({
accounts: this.transfromToResponse(accounts, 'accountTypeLabel', req), accounts: this.transfromToResponse(accounts, 'accountTypeLabel', req),
@@ -353,7 +357,8 @@ export default class AccountsController extends BaseController {
const transactionsFilter = this.matchedQueryData(req); const transactionsFilter = this.matchedQueryData(req);
try { try {
const { transactions } = await this.accountsService.getAccountsTransactions( const { transactions } =
await this.accountsService.getAccountsTransactions(
tenantId, tenantId,
transactionsFilter transactionsFilter
); );
@@ -372,7 +377,12 @@ export default class AccountsController extends BaseController {
* @param {Response} res * @param {Response} res
* @param {ServiceError} error * @param {ServiceError} error
*/ */
catchServiceErrors(error, req: Request, res: Response, next: NextFunction) { private catchServiceErrors(
error,
req: Request,
res: Response,
next: NextFunction
) {
if (error instanceof ServiceError) { if (error instanceof ServiceError) {
if (error.errorType === 'account_not_found') { if (error.errorType === 'account_not_found') {
return res.boom.notFound('The given account not found.', { return res.boom.notFound('The given account not found.', {

View File

@@ -120,6 +120,8 @@ export default class CustomersController extends ContactsController {
query('custom_view_id').optional().isNumeric().toInt(), query('custom_view_id').optional().isNumeric().toInt(),
query('stringified_filter_roles').optional().isJSON(), query('stringified_filter_roles').optional().isJSON(),
query('inactive_mode').optional().isBoolean().toBoolean(),
]; ];
} }
@@ -264,17 +266,15 @@ export default class CustomersController extends ContactsController {
*/ */
async getCustomersList(req: Request, res: Response, next: NextFunction) { async getCustomersList(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req; const { tenantId } = req;
const filter = { const filter = {
filterRoles: [], inactiveMode: false,
sortOrder: 'asc', sortOrder: 'asc',
columnSortBy: 'created_at', columnSortBy: 'created_at',
page: 1, page: 1,
pageSize: 12, pageSize: 12,
...this.matchedQueryData(req), ...this.matchedQueryData(req),
}; };
if (filter.stringifiedFilterRoles) {
filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles);
}
try { try {
const { const {

View File

@@ -100,6 +100,8 @@ export default class VendorsController extends ContactsController {
query('page').optional().isNumeric().toInt(), query('page').optional().isNumeric().toInt(),
query('page_size').optional().isNumeric().toInt(), query('page_size').optional().isNumeric().toInt(),
query('inactive_mode').optional().isBoolean().toBoolean(),
]; ];
} }
@@ -227,8 +229,13 @@ export default class VendorsController extends ContactsController {
*/ */
async getVendorsList(req: Request, res: Response, next: NextFunction) { async getVendorsList(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req; const { tenantId } = req;
const vendorsFilter: IVendorsFilter = { const vendorsFilter: IVendorsFilter = {
filterRoles: [], inactiveMode: false,
sortOrder: 'asc',
columnSortBy: 'created_at',
page: 1,
pageSize: 12,
...this.matchedQueryData(req), ...this.matchedQueryData(req),
}; };

View File

@@ -290,16 +290,12 @@ export default class ExpensesController extends BaseController {
async getExpensesList(req: Request, res: Response, next: NextFunction) { async getExpensesList(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req; const { tenantId } = req;
const filter = { const filter = {
filterRoles: [],
sortOrder: 'asc', sortOrder: 'asc',
columnSortBy: 'created_at', columnSortBy: 'created_at',
page: 1, page: 1,
pageSize: 12, pageSize: 12,
...this.matchedQueryData(req), ...this.matchedQueryData(req),
}; };
if (filter.stringifiedFilterRoles) {
filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles);
}
try { try {
const { expenses, pagination, filterMeta } = const { expenses, pagination, filterMeta } =

View File

@@ -32,13 +32,8 @@ export default class JournalSheetController extends BaseFinancialReportControlle
return [ return [
query('from_date').optional().isISO8601(), query('from_date').optional().isISO8601(),
query('to_date').optional().isISO8601(), query('to_date').optional().isISO8601(),
oneOf( query('transaction_type').optional().trim().escape(),
[ query('transaction_id').optional().isInt().toInt(),
query('transaction_types').optional().isArray({ min: 1 }),
query('transaction_types.*').optional().isNumeric().toInt(),
],
[query('transaction_types').optional().trim().escape()]
),
oneOf( oneOf(
[ [
query('account_ids').optional().isArray({ min: 1 }), query('account_ids').optional().isArray({ min: 1 }),

View File

@@ -199,8 +199,8 @@ export default class ItemsCategoriesController extends BaseController {
*/ */
async getList(req: Request, res: Response, next: NextFunction) { async getList(req: Request, res: Response, next: NextFunction) {
const { tenantId, user } = req; const { tenantId, user } = req;
const itemCategoriesFilter = { const itemCategoriesFilter = {
filterRoles: [],
sortOrder: 'asc', sortOrder: 'asc',
columnSortBy: 'created_at', columnSortBy: 'created_at',
...this.matchedQueryData(req), ...this.matchedQueryData(req),

View File

@@ -185,6 +185,8 @@ export default class ItemsController extends BaseController {
query('custom_view_id').optional().isNumeric().toInt(), query('custom_view_id').optional().isNumeric().toInt(),
query('stringified_filter_roles').optional().isJSON(), query('stringified_filter_roles').optional().isJSON(),
query('inactive_mode').optional().isBoolean().toBoolean(),
]; ];
} }
@@ -339,17 +341,16 @@ export default class ItemsController extends BaseController {
*/ */
async getItemsList(req: Request, res: Response, next: NextFunction) { async getItemsList(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req; const { tenantId } = req;
const filter = { const filter = {
filterRoles: [],
sortOrder: 'asc', sortOrder: 'asc',
columnSortBy: 'created_at', columnSortBy: 'created_at',
page: 1, page: 1,
pageSize: 12, pageSize: 12,
inactiveMode: false,
...this.matchedQueryData(req), ...this.matchedQueryData(req),
}; };
if (filter.stringifiedFilterRoles) {
filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles);
}
try { try {
const { const {
items, items,

View File

@@ -288,14 +288,10 @@ export default class ManualJournalsController extends BaseController {
const filter = { const filter = {
sortOrder: 'asc', sortOrder: 'asc',
columnSortBy: 'created_at', columnSortBy: 'created_at',
filterRoles: [],
page: 1, page: 1,
pageSize: 12, pageSize: 12,
...this.matchedQueryData(req), ...this.matchedQueryData(req),
}; };
if (filter.stringifiedFilterRoles) {
filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles);
}
try { try {
const { const {
manualJournals, manualJournals,

View File

@@ -1,5 +1,6 @@
import { Router, Request, Response, NextFunction } from 'express'; import { Router, Request, Response, NextFunction } from 'express';
import { check, param, query } from 'express-validator'; import { check, param, query } from 'express-validator';
import * as R from 'ramda';
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import { IBillDTO, IBillEditDTO } from 'interfaces'; import { IBillDTO, IBillEditDTO } from 'interfaces';
import asyncMiddleware from 'api/middleware/asyncMiddleware'; import asyncMiddleware from 'api/middleware/asyncMiddleware';
@@ -300,14 +301,11 @@ export default class BillsController extends BaseController {
const filter = { const filter = {
page: 1, page: 1,
pageSize: 12, pageSize: 12,
filterRoles: [],
sortOrder: 'asc', sortOrder: 'asc',
columnSortBy: 'created_at', columnSortBy: 'created_at',
...this.matchedQueryData(req), ...this.matchedQueryData(req),
}; };
if (filter.stringifiedFilterRoles) {
filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles);
}
try { try {
const { bills, pagination, filterMeta } = const { bills, pagination, filterMeta } =
await this.billsService.getBills(tenantId, filter); await this.billsService.getBills(tenantId, filter);

View File

@@ -1,16 +1,13 @@
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import { Router, Request, Response, NextFunction } from 'express'; import { Router, Request, Response, NextFunction } from 'express';
import { import { param, query } from 'express-validator';
param,
query,
} from 'express-validator';
import asyncMiddleware from 'api/middleware/asyncMiddleware'; import asyncMiddleware from 'api/middleware/asyncMiddleware';
import BaseController from './BaseController'; import BaseController from './BaseController';
import { ServiceError } from 'exceptions'; import { ServiceError } from 'exceptions';
import ResourceService from 'services/Resource/ResourceService'; import ResourceService from 'services/Resource/ResourceService';
@Service() @Service()
export default class ResourceController extends BaseController{ export default class ResourceController extends BaseController {
@Inject() @Inject()
resourcesService: ResourceService; resourcesService: ResourceService;
@@ -21,30 +18,61 @@ export default class ResourceController extends BaseController{
const router = Router(); const router = Router();
router.get( router.get(
'/:resource_model/fields', [ '/:resource_model/meta',
...this.resourceModelParamSchema, [...this.resourceModelParamSchema],
], this.asyncMiddleware(this.resourceMeta.bind(this)),
this.handleServiceErrors
);
router.get(
'/:resource_model/fields',
[...this.resourceModelParamSchema],
this.validationResult, this.validationResult,
asyncMiddleware(this.resourceFields.bind(this)), asyncMiddleware(this.resourceFields.bind(this)),
this.handleServiceErrors this.handleServiceErrors
); );
router.get( router.get(
'/:resource_model/data', [ '/:resource_model/data',
...this.resourceModelParamSchema, [...this.resourceModelParamSchema],
],
this.validationResult, this.validationResult,
asyncMiddleware(this.resourceData.bind(this)), asyncMiddleware(this.resourceData.bind(this)),
this.handleServiceErrors, this.handleServiceErrors
) );
return router; return router;
} }
get resourceModelParamSchema() { get resourceModelParamSchema() {
return [ return [param('resource_model').exists().trim().escape()];
param('resource_model').exists().trim().escape(),
];
} }
/**
* Retrieve resource model meta.
* @param {Request} req -
* @param {Response} res -
* @param {NextFunction} next -
* @returns {Response}
*/
private resourceMeta = (
req: Request,
res: Response,
next: NextFunction
): Response => {
const { tenantId } = req;
const { resource_model: resourceModel } = req.params;
try {
const resourceMeta = this.resourcesService.getResourceMeta(
tenantId,
resourceModel
);
return res
.status(200)
.send({ resource_meta: this.transfromToResponse(resourceMeta) });
} catch (error) {
next(error);
}
};
/** /**
* Retrieve resource fields of the given resource. * Retrieve resource fields of the given resource.
* @param {Request} req * @param {Request} req
@@ -56,7 +84,10 @@ export default class ResourceController extends BaseController{
const { resource_model: resourceModel } = req.params; const { resource_model: resourceModel } = req.params;
try { try {
const resourceFields = this.resourcesService.getResourceFields(tenantId, resourceModel); const resourceFields = this.resourcesService.getResourceFields(
tenantId,
resourceModel
);
return res.status(200).send({ return res.status(200).send({
resource_fields: this.transfromToResponse(resourceFields), resource_fields: this.transfromToResponse(resourceFields),
@@ -78,7 +109,11 @@ export default class ResourceController extends BaseController{
const filter = req.query; const filter = req.query;
try { try {
const resourceData = await this.resourcesService.getResourceData(tenantId, resourceModel, filter); const resourceData = await this.resourcesService.getResourceData(
tenantId,
resourceModel,
filter
);
return res.status(200).send({ return res.status(200).send({
resource_data: this.transfromToResponse(resourceData), resource_data: this.transfromToResponse(resourceData),
@@ -95,7 +130,12 @@ export default class ResourceController extends BaseController{
* @param {Response} res * @param {Response} res
* @param {NextFunction} next * @param {NextFunction} next
*/ */
handleServiceErrors(error: Error, req: Request, res: Response, next: NextFunction) { private handleServiceErrors(
error: Error,
req: Request,
res: Response,
next: NextFunction
) {
if (error instanceof ServiceError) { if (error instanceof ServiceError) {
if (error.errorType === 'RESOURCE_MODEL_NOT_FOUND') { if (error.errorType === 'RESOURCE_MODEL_NOT_FOUND') {
return res.status(400).send({ return res.status(400).send({
@@ -105,4 +145,4 @@ export default class ResourceController extends BaseController{
} }
next(error); next(error);
} }
}; }

View File

@@ -261,16 +261,12 @@ export default class PaymentReceivesController extends BaseController {
async getPaymentReceiveList(req: Request, res: Response, next: NextFunction) { async getPaymentReceiveList(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req; const { tenantId } = req;
const filter = { const filter = {
filterRoles: [],
sortOrder: 'asc', sortOrder: 'asc',
columnSortBy: 'created_at', columnSortBy: 'created_at',
page: 1, page: 1,
pageSize: 12, pageSize: 12,
...this.matchedQueryData(req), ...this.matchedQueryData(req),
}; };
if (filter.stringifiedFilterRoles) {
filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles);
}
try { try {
const { const {

View File

@@ -2,11 +2,11 @@ import { Router, Request, Response, NextFunction } from 'express';
import { check, param, query, matchedData } from 'express-validator'; import { check, param, query, matchedData } from 'express-validator';
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import { ISaleEstimateDTO } from 'interfaces'; import { ISaleEstimateDTO } from 'interfaces';
import BaseController from 'api/controllers/BaseController' import BaseController from 'api/controllers/BaseController';
import asyncMiddleware from 'api/middleware/asyncMiddleware'; import asyncMiddleware from 'api/middleware/asyncMiddleware';
import SaleEstimateService from 'services/Sales/SalesEstimate'; import SaleEstimateService from 'services/Sales/SalesEstimate';
import DynamicListingService from 'services/DynamicListing/DynamicListService'; import DynamicListingService from 'services/DynamicListing/DynamicListService';
import { ServiceError } from "exceptions"; import { ServiceError } from 'exceptions';
@Service() @Service()
export default class SalesEstimatesController extends BaseController { export default class SalesEstimatesController extends BaseController {
@@ -23,63 +23,56 @@ export default class SalesEstimatesController extends BaseController {
const router = Router(); const router = Router();
router.post( router.post(
'/', [ '/',
...this.estimateValidationSchema, [...this.estimateValidationSchema],
],
this.validationResult, this.validationResult,
asyncMiddleware(this.newEstimate.bind(this)), asyncMiddleware(this.newEstimate.bind(this)),
this.handleServiceErrors, this.handleServiceErrors
); );
router.post( router.post(
'/:id/deliver', '/:id/deliver',
[ [...this.validateSpecificEstimateSchema],
...this.validateSpecificEstimateSchema,
],
this.validationResult, this.validationResult,
asyncMiddleware(this.deliverSaleEstimate.bind(this)), asyncMiddleware(this.deliverSaleEstimate.bind(this)),
this.handleServiceErrors, this.handleServiceErrors
); );
router.post( router.post(
'/:id/approve', '/:id/approve',
[ [this.validateSpecificEstimateSchema],
this.validateSpecificEstimateSchema,
],
this.validationResult, this.validationResult,
asyncMiddleware(this.approveSaleEstimate.bind(this)), asyncMiddleware(this.approveSaleEstimate.bind(this)),
this.handleServiceErrors this.handleServiceErrors
); );
router.post( router.post(
'/:id/reject', '/:id/reject',
[ [this.validateSpecificEstimateSchema],
this.validateSpecificEstimateSchema,
],
this.validationResult, this.validationResult,
asyncMiddleware(this.rejectSaleEstimate.bind(this)), asyncMiddleware(this.rejectSaleEstimate.bind(this)),
this.handleServiceErrors, this.handleServiceErrors
) );
router.post( router.post(
'/:id', [ '/:id',
[
...this.validateSpecificEstimateSchema, ...this.validateSpecificEstimateSchema,
...this.estimateValidationSchema, ...this.estimateValidationSchema,
], ],
this.validationResult, this.validationResult,
asyncMiddleware(this.editEstimate.bind(this)), asyncMiddleware(this.editEstimate.bind(this)),
this.handleServiceErrors, this.handleServiceErrors
); );
router.delete( router.delete(
'/:id', [ '/:id',
this.validateSpecificEstimateSchema, [this.validateSpecificEstimateSchema],
],
this.validationResult, this.validationResult,
asyncMiddleware(this.deleteEstimate.bind(this)), asyncMiddleware(this.deleteEstimate.bind(this)),
this.handleServiceErrors, this.handleServiceErrors
); );
router.get( router.get(
'/:id', '/:id',
this.validateSpecificEstimateSchema, this.validateSpecificEstimateSchema,
this.validationResult, this.validationResult,
asyncMiddleware(this.getEstimate.bind(this)), asyncMiddleware(this.getEstimate.bind(this)),
this.handleServiceErrors, this.handleServiceErrors
); );
router.get( router.get(
'/', '/',
@@ -87,7 +80,7 @@ export default class SalesEstimatesController extends BaseController {
this.validationResult, this.validationResult,
asyncMiddleware(this.getEstimates.bind(this)), asyncMiddleware(this.getEstimates.bind(this)),
this.handleServiceErrors, this.handleServiceErrors,
this.dynamicListService.handlerErrorsToResponse, this.dynamicListService.handlerErrorsToResponse
); );
return router; return router;
} }
@@ -109,8 +102,14 @@ export default class SalesEstimatesController extends BaseController {
check('entries.*.item_id').exists().isNumeric().toInt(), check('entries.*.item_id').exists().isNumeric().toInt(),
check('entries.*.quantity').exists().isNumeric().toInt(), check('entries.*.quantity').exists().isNumeric().toInt(),
check('entries.*.rate').exists().isNumeric().toFloat(), check('entries.*.rate').exists().isNumeric().toFloat(),
check('entries.*.description').optional({ nullable: true }).trim().escape(), check('entries.*.description')
check('entries.*.discount').optional({ nullable: true }).isNumeric().toFloat(), .optional({ nullable: true })
.trim()
.escape(),
check('entries.*.discount')
.optional({ nullable: true })
.isNumeric()
.toFloat(),
check('note').optional().trim().escape(), check('note').optional().trim().escape(),
check('terms_conditions').optional().trim().escape(), check('terms_conditions').optional().trim().escape(),
@@ -122,9 +121,7 @@ export default class SalesEstimatesController extends BaseController {
* Specific sale estimate validation schema. * Specific sale estimate validation schema.
*/ */
get validateSpecificEstimateSchema() { get validateSpecificEstimateSchema() {
return [ return [param('id').exists().isNumeric().toInt()];
param('id').exists().isNumeric().toInt(),
];
} }
/** /**
@@ -138,7 +135,7 @@ export default class SalesEstimatesController extends BaseController {
query('sort_order').optional().isIn(['desc', 'asc']), query('sort_order').optional().isIn(['desc', 'asc']),
query('page').optional().isNumeric().toInt(), query('page').optional().isNumeric().toInt(),
query('page_size').optional().isNumeric().toInt(), query('page_size').optional().isNumeric().toInt(),
] ];
} }
/** /**
@@ -152,7 +149,10 @@ export default class SalesEstimatesController extends BaseController {
const estimateDTO: ISaleEstimateDTO = this.matchedBodyData(req); const estimateDTO: ISaleEstimateDTO = this.matchedBodyData(req);
try { try {
const storedEstimate = await this.saleEstimateService.createEstimate(tenantId, estimateDTO); const storedEstimate = await this.saleEstimateService.createEstimate(
tenantId,
estimateDTO
);
return res.status(200).send({ return res.status(200).send({
id: storedEstimate.id, id: storedEstimate.id,
@@ -175,7 +175,11 @@ export default class SalesEstimatesController extends BaseController {
try { try {
// Update estimate with associated estimate entries. // Update estimate with associated estimate entries.
await this.saleEstimateService.editEstimate(tenantId, estimateId, estimateDTO); await this.saleEstimateService.editEstimate(
tenantId,
estimateId,
estimateDTO
);
return res.status(200).send({ return res.status(200).send({
id: estimateId, id: estimateId,
@@ -200,7 +204,7 @@ export default class SalesEstimatesController extends BaseController {
return res.status(200).send({ return res.status(200).send({
id: estimateId, id: estimateId,
message: 'The sale estimate has been deleted successfully.' message: 'The sale estimate has been deleted successfully.',
}); });
} catch (error) { } catch (error) {
next(error); next(error);
@@ -283,7 +287,10 @@ export default class SalesEstimatesController extends BaseController {
const { tenantId } = req; const { tenantId } = req;
try { try {
const estimate = await this.saleEstimateService.getEstimate(tenantId, estimateId); const estimate = await this.saleEstimateService.getEstimate(
tenantId,
estimateId
);
return res.status(200).send({ estimate }); return res.status(200).send({ estimate });
} catch (error) { } catch (error) {
@@ -299,29 +306,22 @@ export default class SalesEstimatesController extends BaseController {
async getEstimates(req: Request, res: Response, next: NextFunction) { async getEstimates(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req; const { tenantId } = req;
const filter = { const filter = {
filterRoles: [],
sortOrder: 'asc', sortOrder: 'asc',
columnSortBy: 'created_at', columnSortBy: 'created_at',
page: 1, page: 1,
pageSize: 12, pageSize: 12,
...this.matchedQueryData(req), ...this.matchedQueryData(req),
}; };
if (filter.stringifiedFilterRoles) {
filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles);
}
try { try {
const { const { salesEstimates, pagination, filterMeta } =
salesEstimates, await this.saleEstimateService.estimatesList(tenantId, filter);
pagination,
filterMeta
} = await this.saleEstimateService.estimatesList(tenantId, filter);
return res.status(200).send({ return res.status(200).send({
sales_estimates: this.transfromToResponse(salesEstimates), sales_estimates: this.transfromToResponse(salesEstimates),
pagination, pagination,
filter_meta: this.transfromToResponse(filterMeta), filter_meta: this.transfromToResponse(filterMeta),
}) });
} catch (error) { } catch (error) {
next(error); next(error);
} }
@@ -334,7 +334,12 @@ export default class SalesEstimatesController extends BaseController {
* @param {Response} res * @param {Response} res
* @param {NextFunction} next * @param {NextFunction} next
*/ */
handleServiceErrors(error: Error, req: Request, res: Response, next: NextFunction) { private handleServiceErrors(
error: Error,
req: Request,
res: Response,
next: NextFunction
) {
if (error instanceof ServiceError) { if (error instanceof ServiceError) {
if (error.errorType === 'ITEMS_NOT_FOUND') { if (error.errorType === 'ITEMS_NOT_FOUND') {
return res.boom.badRequest(null, { return res.boom.badRequest(null, {
@@ -409,4 +414,4 @@ export default class SalesEstimatesController extends BaseController {
} }
next(error); next(error);
} }
}; }

View File

@@ -284,23 +284,15 @@ export default class SaleInvoicesController extends BaseController {
) { ) {
const { tenantId } = req; const { tenantId } = req;
const filter = { const filter = {
filterRoles: [],
sortOrder: 'asc', sortOrder: 'asc',
columnSortBy: 'created_at', columnSortBy: 'created_at',
page: 1, page: 1,
pageSize: 12, pageSize: 12,
...this.matchedQueryData(req), ...this.matchedQueryData(req),
}; };
if (filter.stringifiedFilterRoles) {
filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles);
}
try { try {
const { const { salesInvoices, filterMeta, pagination } =
salesInvoices, await this.saleInvoiceService.salesInvoicesList(tenantId, filter);
filterMeta,
pagination,
} = await this.saleInvoiceService.salesInvoicesList(tenantId, filter);
return res.status(200).send({ return res.status(200).send({
sales_invoices: salesInvoices, sales_invoices: salesInvoices,

View File

@@ -230,16 +230,12 @@ export default class SalesReceiptsController extends BaseController {
async getSalesReceipts(req: Request, res: Response, next: NextFunction) { async getSalesReceipts(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req; const { tenantId } = req;
const filter = { const filter = {
filterRoles: [],
sortOrder: 'asc', sortOrder: 'asc',
columnSortBy: 'created_at', columnSortBy: 'created_at',
page: 1, page: 1,
pageSize: 12, pageSize: 12,
...this.matchedQueryData(req), ...this.matchedQueryData(req),
}; };
if (filter.stringifiedFilterRoles) {
filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles);
}
try { try {
const { const {

View File

@@ -47,6 +47,7 @@ export interface IAccountResponse extends IAccount {
export interface IAccountsFilter extends IDynamicListFilterDTO { export interface IAccountsFilter extends IDynamicListFilterDTO {
stringifiedFilterRoles?: string, stringifiedFilterRoles?: string,
onlyInactive: boolean;
}; };
export interface IAccountType { export interface IAccountType {

View File

@@ -62,6 +62,8 @@ export interface IBill {
export interface IBillsFilter extends IDynamicListFilterDTO { export interface IBillsFilter extends IDynamicListFilterDTO {
stringifiedFilterRoles?: string; stringifiedFilterRoles?: string;
page: number;
pageSize: number;
} }
export interface IBillsService { export interface IBillsService {

View File

@@ -1,6 +1,9 @@
import { IModel, ISortOrder } from "./Model";
export interface IDynamicFilter { export interface IDynamicFilter {
setTableName(tableName: string): void; setModel(model: IModel): void;
buildQuery(): void; buildQuery(): void;
getResponseMeta();
} }
export interface IFilterRole { export interface IFilterRole {
@@ -10,19 +13,19 @@ export interface IFilterRole {
index?: number; index?: number;
comparator?: string; comparator?: string;
} }
export interface IDynamicListFilter {
export interface IDynamicListFilterDTO {
customViewId?: number; customViewId?: number;
filterRoles?: IFilterRole[]; filterRoles?: IFilterRole[];
columnSortBy: string; columnSortBy: ISortOrder;
sortOrder: string; sortOrder: string;
stringifiedFilterRoles: string;
} }
export interface IDynamicListService { export interface IDynamicListService {
dynamicList( dynamicList(
tenantId: number, tenantId: number,
model: any, model: any,
filter: IDynamicListFilterDTO filter: IDynamicListFilter
): Promise<any>; ): Promise<any>;
handlerErrorsToResponse(error, req, res, next): void; handlerErrorsToResponse(error, req, res, next): void;
} }

View File

@@ -72,6 +72,7 @@ export interface IItemsFilter extends IDynamicListFilterDTO {
stringifiedFilterRoles?: string, stringifiedFilterRoles?: string,
page: number, page: number,
pageSize: number, pageSize: number,
inactiveMode: boolean,
}; };
export interface IItemsAutoCompleteFilter { export interface IItemsAutoCompleteFilter {

View File

@@ -7,7 +7,9 @@ export interface IJournalReportQuery {
noCents: boolean, noCents: boolean,
divideOn1000: boolean, divideOn1000: boolean,
}, },
transactionTypes: string | string[], transactionType: string,
transactionId: string,
accountsIds: number | number[], accountsIds: number | number[],
fromRange: number, fromRange: number,
toRange: number, toRange: number,

View File

@@ -1,17 +1,72 @@
export interface IModel { export interface IModel {
name: string, name: string;
tableName: string, tableName: string;
fields: { [key: string]: any, }, fields: { [key: string]: any };
}; }
export interface IFilterMeta { export interface IFilterMeta {
sortOrder: string, sortOrder: string;
sortBy: string, sortBy: string;
}; }
export interface IPaginationMeta { export interface IPaginationMeta {
pageSize: number, pageSize: number;
page: number, page: number;
}; }
export interface IModelMetaDefaultSort {
sortOrder: ISortOrder;
sortField: string;
}
export type IModelColumnType =
| 'text'
| 'number'
| 'enumeration'
| 'boolean'
| 'relation';
export type ISortOrder = 'DESC' | 'ASC';
export interface IModelMetaFieldCommon {
name: string;
column: string;
columnable?: boolean;
fieldType: IModelColumnType;
customQuery?: Function;
}
export interface IModelMetaFieldNumber {
fieldType: 'number';
minLength?: number;
maxLength?: number;
}
export interface IModelMetaFieldOther {
fieldType: 'text' | 'boolean';
}
export type IModelMetaField = IModelMetaFieldCommon &
(IModelMetaFieldOther | IModelMetaEnumerationField | IModelMetaRelationField);
export interface IModelMetaEnumerationOption {
key: string;
label: string;
}
export interface IModelMetaEnumerationField {
fieldType: 'enumeration';
options: IModelMetaEnumerationOption[];
}
export interface IModelMetaRelationField {
fieldType: 'relation';
relationToModel: IModel;
relationToField: string;
}
export interface IModelMeta {
defaultFilterField: string;
defaultSort: IModelMetaDefaultSort;
fields: { [key: string]: IModelMetaField };
}

View File

@@ -1,52 +1,73 @@
import { forEach, uniqBy } from 'lodash'; import { forEach, uniqBy } from 'lodash';
import { buildFilterRolesJoins } from 'lib/ViewRolesBuilder'; import DynamicFilterAbstructor from './DynamicFilterAbstructor';
import { IModel } from 'interfaces'; import { IDynamicFilter, IFilterRole, IModel } from 'interfaces';
export default class DynamicFilter { export default class DynamicFilter extends DynamicFilterAbstructor{
model: IModel; private model: IModel;
tableName: string; private tableName: string;
private dynamicFilters: IDynamicFilter[];
/** /**
* Constructor. * Constructor.
* @param {String} tableName - * @param {String} tableName -
*/ */
constructor(model) { constructor(model) {
super();
this.model = model; this.model = model;
this.tableName = model.tableName; this.tableName = model.tableName;
this.filters = []; this.dynamicFilters = [];
} }
/** /**
* Set filter. * Registers the given dynamic filter.
* @param {*} filterRole - Filter role. * @param {IDynamicFilter} filterRole - Filter role.
*/ */
setFilter(filterRole) { public setFilter = (dynamicFilter: IDynamicFilter) => {
filterRole.setModel(this.model); dynamicFilter.setModel(this.model);
this.filters.push(filterRole); this.dynamicFilters.push(dynamicFilter);
}
/**
* Retrieve dynamic filter build queries.
* @returns
*/
private dynamicFiltersBuildQuery = () => {
return this.dynamicFilters.map((filter) => {
return filter.buildQuery()
});
}
/**
* Retrieve dynamic filter roles.
* @returns {IFilterRole[]}
*/
private dynamicFilterTableColumns = (): IFilterRole[] => {
const localFilterRoles = [];
this.dynamicFilters.forEach((dynamicFilter) => {
const { filterRoles } = dynamicFilter;
localFilterRoles.push(
...(Array.isArray(filterRoles) ? filterRoles : [filterRoles])
);
});
return localFilterRoles;
} }
/** /**
* Builds queries of filter roles. * Builds queries of filter roles.
*/ */
buildQuery() { public buildQuery = () => {
const buildersCallbacks = []; const buildersCallbacks = this.dynamicFiltersBuildQuery();
const tableColumns = []; const tableColumns = this.dynamicFilterTableColumns();
this.filters.forEach((filter) => {
const { filterRoles } = filter;
buildersCallbacks.push(filter.buildQuery());
tableColumns.push(
...(Array.isArray(filterRoles) ? filterRoles : [filterRoles])
);
});
return (builder) => { return (builder) => {
buildersCallbacks.forEach((builderCallback) => { buildersCallbacks.forEach((builderCallback) => {
builderCallback(builder); builderCallback(builder);
}); });
buildFilterRolesJoins( this.buildFilterRolesJoins(
this.model, this.model,
uniqBy(tableColumns, 'columnKey') uniqBy(tableColumns, 'columnKey')
)(builder); )(builder);
@@ -56,10 +77,10 @@ export default class DynamicFilter {
/** /**
* Retrieve response metadata from all filters adapters. * Retrieve response metadata from all filters adapters.
*/ */
getResponseMeta() { public getResponseMeta = () => {
const responseMeta = {}; const responseMeta = {};
this.filters.forEach((filter) => { this.dynamicFilters.forEach((filter) => {
const { responseMeta: filterMeta } = filter; const { responseMeta: filterMeta } = filter;
forEach(filterMeta, (value, key) => { forEach(filterMeta, (value, key) => {

View File

@@ -0,0 +1,37 @@
import { IModel, IFilterRole } from 'interfaces';
export default class DynamicFilterAbstructor {
/**
* Extract relation table name from relation.
* @param {String} column -
* @return {String} - join relation table.
*/
protected getTableFromRelationColumn = (column: string) => {
const splitedColumn = column.split('.');
return splitedColumn.length > 0 ? splitedColumn[0] : '';
};
/**
* Builds view roles join queries.
* @param {String} tableName - Table name.
* @param {Array} roles - Roles.
*/
protected buildFilterRolesJoins = (model: IModel, roles: IFilterRole[]) => {
return (builder) => {
roles.forEach((role) => {
const field = model.getField(role.fieldKey);
if (field.relation) {
const joinTable = this.getTableFromRelationColumn(field.relation);
builder.join(
joinTable,
`${model.tableName}.${field.column}`,
'=',
field.relation
);
}
});
};
};
}

View File

@@ -1,10 +1,8 @@
import { difference } from 'lodash'; import DynamicFilterRoleAbstructor from './DynamicFilterRoleAbstructor';
import DynamicFilterRoleAbstructor from 'lib/DynamicFilter/DynamicFilterRoleAbstructor';
import { buildFilterQuery } from 'lib/ViewRolesBuilder';
import { IFilterRole } from 'interfaces'; import { IFilterRole } from 'interfaces';
export default class FilterRoles extends DynamicFilterRoleAbstructor { export default class FilterRoles extends DynamicFilterRoleAbstructor {
filterRoles: IFilterRole[]; private filterRoles: IFilterRole[];
/** /**
* Constructor method. * Constructor method.
@@ -35,17 +33,22 @@ export default class FilterRoles extends DynamicFilterRoleAbstructor {
/** /**
* Builds database query of view roles. * Builds database query of view roles.
*/ */
buildQuery() { protected buildQuery() {
return (builder) => {
const logicExpression = this.buildLogicExpression(); const logicExpression = this.buildLogicExpression();
buildFilterQuery(this.model, this.filterRoles, logicExpression)(builder);
return (builder) => {
this.buildFilterQuery(
this.model,
this.filterRoles,
logicExpression
)(builder);
}; };
} }
/** /**
* Sets response meta. * Sets response meta.
*/ */
setResponseMeta() { private setResponseMeta() {
this.responseMeta = { this.responseMeta = {
filterRoles: this.filterRoles, filterRoles: this.filterRoles,
}; };

View File

@@ -0,0 +1,61 @@
import { OPERATION } from 'lib/LogicEvaluation/Parser';
export default class QueryParser {
constructor(tree, queries) {
this.tree = tree;
this.queries = queries;
this.query = null;
}
setQuery(query) {
this.query = query.clone();
}
parse() {
return this.parseNode(this.tree);
}
parseNode(node) {
if (typeof node === 'string') {
const nodeQuery = this.getQuery(node);
return (query) => { nodeQuery(query); };
}
if (OPERATION[node.operation] === undefined) {
throw new Error(`unknow expression ${node.operation}`);
}
const leftQuery = this.getQuery(node.left);
const rightQuery = this.getQuery(node.right);
switch (node.operation) {
case '&&':
case 'AND':
default:
return (nodeQuery) => nodeQuery.where((query) => {
query.where((q) => { leftQuery(q); });
query.andWhere((q) => { rightQuery(q); });
});
case '||':
case 'OR':
return (nodeQuery) => nodeQuery.where((query) => {
query.where((q) => { leftQuery(q); });
query.orWhere((q) => { rightQuery(q); });
});
}
}
getQuery(node) {
if (typeof node !== 'string' && node !== null) {
return this.parseNode(node);
}
const value = parseFloat(node);
if (!isNaN(value)) {
if (typeof this.queries[node] === 'undefined') {
throw new Error(`unknow query under index ${node}`);
}
return this.queries[node];
}
return null;
}
}

View File

@@ -1,13 +1,300 @@
import { IFilterRole, IDynamicFilter, IModel } from "interfaces"; import moment from 'moment';
import { IFilterRole, IDynamicFilter, IModel } from 'interfaces';
import { Lexer } from 'lib/LogicEvaluation/Lexer';
import Parser from 'lib/LogicEvaluation/Parser';
import DynamicFilterQueryParser from './DynamicFilterQueryParser';
import { COMPARATOR_TYPE, FIELD_TYPE } from './constants';
export default class DynamicFilterAbstructor implements IDynamicFilter { export default abstract class DynamicFilterAbstructor
filterRoles: IFilterRole[] = []; implements IDynamicFilter
tableName: string; {
model: IModel; protected filterRoles: IFilterRole[] = [];
responseMeta: { [key: string]: any } = {}; protected tableName: string;
protected model: IModel;
protected responseMeta: { [key: string]: any } = {};
setModel(model: IModel) { /**
* Sets model the dynamic filter service.
* @param {IModel} model
*/
public setModel(model: IModel) {
this.model = model; this.model = model;
this.tableName = model.tableName; this.tableName = model.tableName;
} }
/**
* Transformes filter roles to map by index.
* @param {IModel} model
* @param {IFilterRole[]} roles
* @returns
*/
protected convertRolesMapByIndex = (model, roles) => {
const rolesIndexSet = {};
roles.forEach((role) => {
rolesIndexSet[role.index] = this.buildRoleQuery(model, role);
});
return rolesIndexSet;
};
/**
* Builds database query from stored view roles.
* @param {Array} roles -
* @return {Function}
*/
protected buildFilterRolesQuery = (
model: IModel,
roles: IFilterRole[],
logicExpression: string = ''
) => {
const rolesIndexSet = this.convertRolesMapByIndex(model, roles);
// Lexer for logic expression.
const lexer = new Lexer(logicExpression);
const tokens = lexer.getTokens();
// Parse the logic expression.
const parser = new Parser(tokens);
const parsedTree = parser.parse();
const queryParser = new DynamicFilterQueryParser(parsedTree, rolesIndexSet);
return queryParser.parse();
};
/**
* Builds filter query for query builder.
* @param {String} tableName - Table name.
* @param {Array} roles - Filter roles.
* @param {String} logicExpression - Logic expression.
*/
protected buildFilterQuery = (
model: IModel,
roles: IFilterRole[],
logicExpression: string
) => {
return (builder) => {
this.buildFilterRolesQuery(model, roles, logicExpression)(builder);
};
};
/**
* Builds roles queries.
* @param {IModel} model -
* @param {Object} role -
*/
protected buildRoleQuery = (model: IModel, role: IFilterRole) => {
const fieldRelation = model.getField(role.fieldKey);
const comparatorColumn = `${model.tableName}.${fieldRelation.column}`;
// Field relation custom query.
if (typeof fieldRelation.customQuery !== 'undefined') {
return (builder) => {
fieldRelation.customQuery(builder, role);
};
}
switch (fieldRelation.fieldType) {
case FIELD_TYPE.BOOLEAN:
case FIELD_TYPE.ENUMERATION:
return this.booleanRoleQueryBuilder(role, comparatorColumn);
case FIELD_TYPE.NUMBER:
return this.numberRoleQueryBuilder(role, comparatorColumn);
case FIELD_TYPE.DATE:
return this.dateQueryBuilder(role, comparatorColumn);
case FIELD_TYPE.TEXT:
default:
return this.textRoleQueryBuilder(role, comparatorColumn);
}
};
/**
* Boolean column query builder.
* @param {IFilterRole} role
* @param {string} comparatorColumn
* @returns
*/
protected booleanRoleQueryBuilder = (
role: IFilterRole,
comparatorColumn: string
) => {
switch (role.comparator) {
case COMPARATOR_TYPE.EQUALS:
case COMPARATOR_TYPE.EQUAL:
case COMPARATOR_TYPE.IS:
default:
return (builder) => {
builder.where(comparatorColumn, '=', role.value);
};
case COMPARATOR_TYPE.NOT_EQUAL:
case COMPARATOR_TYPE.NOT_EQUALS:
case COMPARATOR_TYPE.IS_NOT:
return (builder) => {
builder.where(comparatorColumn, '<>', role.value);
};
}
};
/**
* Numeric column query builder.
* @param {IFilterRole} role
* @param {string} comparatorColumn
* @returns
*/
protected numberRoleQueryBuilder = (
role: IFilterRole,
comparatorColumn: string
) => {
switch (role.comparator) {
case COMPARATOR_TYPE.EQUALS:
case COMPARATOR_TYPE.EQUAL:
default:
return (builder) => {
builder.where(comparatorColumn, '=', role.value);
};
case COMPARATOR_TYPE.NOT_EQUAL:
case COMPARATOR_TYPE.NOT_EQUALS:
return (builder) => {
builder.whereNot(comparatorColumn, role.value);
};
case COMPARATOR_TYPE.BIGGER_THAN:
case COMPARATOR_TYPE.BIGGER:
return (builder) => {
builder.where(comparatorColumn, '>', role.value);
};
case COMPARATOR_TYPE.BIGGER_OR_EQUALS:
return (builder) => {
builder.where(comparatorColumn, '>=', role.value);
};
case COMPARATOR_TYPE.SMALLER_THAN:
case COMPARATOR_TYPE.SMALLER:
return (builder) => {
builder.where(comparatorColumn, '<', role.value);
};
case COMPARATOR_TYPE.SMALLER_OR_EQUALS:
return (builder) => {
builder.where(comparatorColumn, '<=', role.value);
};
}
};
/**
* Text column query builder.
* @param {IFilterRole} role
* @param {string} comparatorColumn
* @returns {Function}
*/
protected textRoleQueryBuilder = (
role: IFilterRole,
comparatorColumn: string
) => {
switch (role.comparator) {
case COMPARATOR_TYPE.EQUAL:
case COMPARATOR_TYPE.EQUALS:
case COMPARATOR_TYPE.IS:
default:
return (builder) => {
builder.where(comparatorColumn, role.value);
};
case COMPARATOR_TYPE.NOT_EQUALS:
case COMPARATOR_TYPE.NOT_EQUAL:
case COMPARATOR_TYPE.IS_NOT:
return (builder) => {
builder.whereNot(comparatorColumn, role.value);
};
case COMPARATOR_TYPE.CONTAIN:
case COMPARATOR_TYPE.CONTAINS:
return (builder) => {
builder.where(comparatorColumn, 'LIKE', `%${role.value}%`);
};
case COMPARATOR_TYPE.NOT_CONTAIN:
case COMPARATOR_TYPE.NOT_CONTAINS:
return (builder) => {
builder.whereNot(comparatorColumn, 'LIKE', `%${role.value}%`);
};
}
};
/**
* Date column query builder.
* @param {IFilterRole} role
* @param {string} comparatorColumn
* @returns {Function}
*/
protected dateQueryBuilder = (
role: IFilterRole,
comparatorColumn: string
) => {
switch (role.comparator) {
case COMPARATOR_TYPE.AFTER:
case COMPARATOR_TYPE.BEFORE:
return (builder) => {
this.dateQueryAfterBeforeComparator(role, comparatorColumn, builder);
};
case COMPARATOR_TYPE.IN:
return (builder) => {
this.dateQueryInComparator(role, comparatorColumn, builder);
};
}
};
/**
* Date query 'IN' comparator type.
* @param {IFilterRole} role
* @param {string} comparatorColumn
* @param builder
*/
protected dateQueryInComparator = (
role: IFilterRole,
comparatorColumn: string,
builder
) => {
const hasTimeFormat = moment(
role.value,
'YYYY-MM-DD HH:MM',
true
).isValid();
const dateFormat = 'YYYY-MM-DD HH:MM:SS';
if (hasTimeFormat) {
const targetDateTime = moment(role.value).format(dateFormat);
builder.where(comparatorColumn, '=', targetDateTime);
} else {
const startDate = moment(role.value).startOf('day');
const endDate = moment(role.value).endOf('day');
builder.where(comparatorColumn, '>=', startDate.format(dateFormat));
builder.where(comparatorColumn, '<=', endDate.format(dateFormat));
}
}
/**
* Date query after/before comparator type.
* @param {IFilterRole} role
* @param {string} comparatorColumn - Column.
* @param builder
*/
protected dateQueryAfterBeforeComparator = (
role: IFilterRole,
comparatorColumn: string,
builder
) => {
const comparator = role.comparator === COMPARATOR_TYPE.BEFORE ? '<' : '>';
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 === COMPARATOR_TYPE.BEFORE) {
targetDate.startOf('day');
} else {
targetDate.endOf('day');
}
}
const comparatorValue = targetDate.format(dateFormat);
builder.where(comparatorColumn, comparator, comparatorValue);
}
} }

View File

@@ -5,8 +5,13 @@ import {
getTableFromRelationColumn, getTableFromRelationColumn,
} from 'lib/ViewRolesBuilder'; } from 'lib/ViewRolesBuilder';
interface ISortRole {
fieldKey: string;
order: string;
}
export default class DynamicFilterSortBy extends DynamicFilterRoleAbstructor { export default class DynamicFilterSortBy extends DynamicFilterRoleAbstructor {
sortRole: { fieldKey: string; order: string } = {}; private sortRole: ISortRole = {};
/** /**
* Constructor method. * Constructor method.
@@ -23,39 +28,28 @@ export default class DynamicFilterSortBy extends DynamicFilterRoleAbstructor {
this.setResponseMeta(); this.setResponseMeta();
} }
/**
* Validate the given field key with the model.
*/
validate() {
validateFieldKeyExistance(this.model, this.sortRole.fieldKey);
}
/** /**
* Builds database query of sort by column on the given direction. * Builds database query of sort by column on the given direction.
*/ */
buildQuery() { public buildQuery() {
const fieldRelation = getRoleFieldColumn( const field = this.model.getField(this.sortRole.fieldKey);
this.model, const comparatorColumn = `${this.tableName}.${field.column}`;
this.sortRole.fieldKey
);
const comparatorColumn =
fieldRelation.relationColumn ||
`${this.tableName}.${fieldRelation.column}`;
if (typeof fieldRelation.sortQuery !== 'undefined') { if (typeof field.customSortQuery !== 'undefined') {
return (builder) => { return (builder) => {
fieldRelation.sortQuery(builder, this.sortRole); field.customSortQuery(builder, this.sortRole);
}; };
} }
return (builder) => { return (builder) => {
if (this.sortRole.fieldKey) { if (this.sortRole.fieldKey) {
builder.orderBy(`${comparatorColumn}`, this.sortRole.order); builder.orderBy(`${comparatorColumn}`, this.sortRole.order);
} }
this.joinBuildQuery()(builder);
}; };
} }
joinBuildQuery() { private joinBuildQuery() {
const fieldColumn = getRoleFieldColumn(this.model, this.sortRole.fieldKey); const fieldColumn = getRoleFieldColumn(this.model, this.sortRole.fieldKey);
return (builder) => { return (builder) => {
@@ -75,7 +69,7 @@ export default class DynamicFilterSortBy extends DynamicFilterRoleAbstructor {
/** /**
* Sets response meta. * Sets response meta.
*/ */
setResponseMeta() { public setResponseMeta() {
this.responseMeta = { this.responseMeta = {
sortOrder: this.sortRole.fieldKey, sortOrder: this.sortRole.fieldKey,
sortBy: this.sortRole.order, sortBy: this.sortRole.order,

View File

@@ -0,0 +1,36 @@
export const COMPARATOR_TYPE = {
EQUAL: 'equal',
EQUALS: 'equals',
NOT_EQUAL: 'not_equal',
NOT_EQUALS: 'not_equals',
BIGGER_THAN: 'bigger_than',
BIGGER: 'bigger',
BIGGER_OR_EQUALS: 'bigger_or_equals',
SMALLER_THAN: 'smaller_than',
SMALLER: 'smaller',
SMALLER_OR_EQUALS: 'smaller_or_equals',
IS: 'is',
IS_NOT: 'is_not',
CONTAINS: 'contains',
CONTAIN: 'contain',
NOT_CONTAINS: 'contains',
NOT_CONTAIN: 'contain',
AFTER: 'after',
BEFORE: 'before',
IN: 'in',
};
export const FIELD_TYPE = {
TEXT: 'text',
NUMBER: 'number',
ENUMERATION: 'enumeration',
BOOLEAN: 'boolean',
RELATION: 'relation',
DATE: 'date',
};

View File

@@ -1,121 +1,7 @@
import { difference } from 'lodash'; import { difference } from 'lodash';
import moment from 'moment';
import { Lexer } from 'lib/LogicEvaluation/Lexer';
import Parser from 'lib/LogicEvaluation/Parser';
import QueryParser from 'lib/LogicEvaluation/QueryParser';
import { IFilterRole, IModel } from 'interfaces'; import { IFilterRole, IModel } from 'interfaces';
const numberRoleQueryBuilder = (
role: IFilterRole,
comparatorColumn: string
) => {
switch (role.comparator) {
case 'equals':
case 'equal':
default:
return (builder) => {
builder.where(comparatorColumn, '=', role.value);
};
case 'not_equals':
case 'not_equal':
return (builder) => {
builder.whereNot(comparatorColumn, role.value);
};
case 'bigger_than':
case 'bigger':
return (builder) => {
builder.where(comparatorColumn, '>', role.value);
};
case 'bigger_or_equals':
return (builder) => {
builder.where(comparatorColumn, '>=', role.value);
};
case 'smaller_than':
case 'smaller':
return (builder) => {
builder.where(comparatorColumn, '<', role.value);
};
case 'smaller_or_equals':
return (builder) => {
builder.where(comparatorColumn, '<=', role.value);
};
}
};
const textRoleQueryBuilder = (role: IFilterRole, comparatorColumn: string) => {
switch (role.comparator) {
case 'equals':
case 'is':
default:
return (builder) => {
builder.where(comparatorColumn, role.value);
};
case 'not_equal':
case 'not_equals':
case 'is_not':
return (builder) => {
builder.whereNot(comparatorColumn, role.value);
};
case 'contain':
case 'contains':
return (builder) => {
builder.where(comparatorColumn, 'LIKE', `%${role.value}%`);
};
case 'not_contain':
case 'not_contains':
return (builder) => {
builder.whereNot(comparatorColumn, 'LIKE', `%${role.value}%`);
};
}
};
const dateQueryBuilder = (role: IFilterRole, comparatorColumn: string) => {
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 targetDate = moment(role.value);
const dateFormat = 'YYYY-MM-DD HH:MM:SS';
if (!hasTimeFormat) {
if (role.comparator === 'before') {
targetDate.startOf('day');
} else {
targetDate.endOf('day');
}
}
const comparatorValue = targetDate.format(dateFormat);
builder.where(comparatorColumn, comparator, comparatorValue);
};
case 'in':
return (builder) => {
const hasTimeFormat = moment(
role.value,
'YYYY-MM-DD HH:MM',
true
).isValid();
const dateFormat = 'YYYY-MM-DD HH:MM:SS';
if (hasTimeFormat) {
const targetDateTime = moment(role.value).format(dateFormat);
builder.where(comparatorColumn, '=', targetDateTime);
} else {
const startDate = moment(role.value).startOf('day');
const endDate = moment(role.value).endOf('day');
builder.where(comparatorColumn, '>=', startDate.format(dateFormat));
builder.where(comparatorColumn, '<=', endDate.format(dateFormat));
}
};
}
};
/** /**
* Get field column metadata and its relation with other tables. * Get field column metadata and its relation with other tables.
* @param {String} tableName - Table name of target column. * @param {String} tableName - Table name of target column.
@@ -126,68 +12,6 @@ export function getRoleFieldColumn(model: IModel, fieldKey: string) {
return tableFields[fieldKey] ? tableFields[fieldKey] : null; return tableFields[fieldKey] ? tableFields[fieldKey] : null;
} }
/**
* Builds roles queries.
* @param {IModel} model -
* @param {Object} role -
*/
export function buildRoleQuery(model: IModel, role: IFilterRole) {
const fieldRelation = getRoleFieldColumn(model, role.fieldKey);
const comparatorColumn =
fieldRelation.relationColumn ||
`${model.tableName}.${fieldRelation.column}`;
if (typeof fieldRelation.query !== 'undefined') {
return (builder) => {
fieldRelation.query(builder, role);
};
}
switch (fieldRelation.columnType) {
case 'number':
return numberRoleQueryBuilder(role, comparatorColumn);
case 'date':
return dateQueryBuilder(role, comparatorColumn);
case 'text':
case 'varchar':
default:
return textRoleQueryBuilder(role, comparatorColumn);
}
}
/**
* Extract relation table name from relation.
* @param {String} column -
* @return {String} - join relation table.
*/
export const getTableFromRelationColumn = (column: string) => {
const splitedColumn = column.split('.');
return splitedColumn.length > 0 ? splitedColumn[0] : '';
};
/**
* Builds view roles join queries.
* @param {String} tableName - Table name.
* @param {Array} roles - Roles.
*/
export function buildFilterRolesJoins(model: IModel, roles: IFilterRole[]) {
return (builder) => {
roles.forEach((role) => {
const fieldColumn = getRoleFieldColumn(model, role.fieldKey);
if (fieldColumn.relation) {
const joinTable = getTableFromRelationColumn(fieldColumn.relation);
builder.join(
joinTable,
`${model.tableName}.${fieldColumn.column}`,
'=',
fieldColumn.relation
);
}
});
};
}
export function buildSortColumnJoin(model: IModel, sortColumnKey: string) { export function buildSortColumnJoin(model: IModel, sortColumnKey: string) {
return (builder) => { return (builder) => {
const fieldColumn = getRoleFieldColumn(model, sortColumnKey); const fieldColumn = getRoleFieldColumn(model, sortColumnKey);
@@ -204,50 +28,6 @@ export function buildSortColumnJoin(model: IModel, sortColumnKey: string) {
}; };
} }
/**
* Builds database query from stored view roles.
*
* @param {Array} roles -
* @return {Function}
*/
export function buildFilterRolesQuery(
model: IModel,
roles: IFilterRole[],
logicExpression: string = ''
) {
const rolesIndexSet = {};
roles.forEach((role) => {
rolesIndexSet[role.index] = buildRoleQuery(model, role);
});
// Lexer for logic expression.
const lexer = new Lexer(logicExpression);
const tokens = lexer.getTokens();
// Parse the logic expression.
const parser = new Parser(tokens);
const parsedTree = parser.parse();
const queryParser = new QueryParser(parsedTree, rolesIndexSet);
return queryParser.parse();
}
/**
* Builds filter query for query builder.
* @param {String} tableName -
* @param {Array} roles -
* @param {String} logicExpression -
*/
export const buildFilterQuery = (
model: IModel,
roles: IFilterRole[],
logicExpression: string
) => {
return (builder) => {
buildFilterRolesQuery(model, roles, logicExpression)(builder);
};
};
/** /**
* Mapes the view roles to view conditionals. * Mapes the view roles to view conditionals.
* @param {Array} viewRoles - * @param {Array} viewRoles -
@@ -316,14 +96,6 @@ export function validateFieldKeyExistance(model: any, fieldKey: string) {
return model?.fields?.[fieldKey] || false; return model?.fields?.[fieldKey] || false;
} }
export function validateFilterRolesFieldsExistance(
model,
filterRoles: IFilterRole[]
) {
return filterRoles.filter((filterRole: IFilterRole) => {
return !validateFieldKeyExistance(model, filterRole.fieldKey);
});
}
/** /**
* Retrieve model fields keys. * Retrieve model fields keys.

View File

@@ -0,0 +1,63 @@
import { IModelMeta } from 'interfaces';
export default {
defaultFilterField: 'name',
defaultSort: {
sortOrder: 'DESC',
sortField: 'name',
},
fields: {
name: {
name: 'Account name',
column: 'name',
columnable: true,
fieldType: 'text',
},
description: {
name: 'Description',
column: 'description',
columnable: true,
fieldType: 'text',
},
code: {
name: 'Account code',
column: 'code',
columnable: true,
fieldType: 'text',
},
root_type: {
name: 'Root type',
column: 'root_type',
columnable: true,
fieldType: 'enumeration',
options: [
{ key: 'asset', label: 'Asset' },
{ key: 'liability', label: 'Liability' },
{ key: 'equity', label: 'Equity' },
{ key: 'Income', label: 'Income' },
{ key: 'expense', label: 'Expense' },
],
},
active: {
name: 'Active',
column: 'active',
fieldType: 'boolean',
},
amount: {
name: 'Account balance',
column: 'amount',
columnable: true,
fieldType: 'number',
},
currency: {
name: 'Currency',
column: 'currency_code',
fieldType: 'text',
},
created_at: {
name: 'Created at',
column: 'created_at',
fieldType: 'date',
},
},
} as IModelMeta;

View File

@@ -1,16 +1,15 @@
/* eslint-disable global-require */ /* eslint-disable global-require */
import { Model } from 'objection'; import { mixin, Model } from 'objection';
import { flatten, castArray } from 'lodash'; import { castArray } from 'lodash';
import TenantModel from 'models/TenantModel'; import TenantModel from 'models/TenantModel';
import { import { buildFilterQuery, buildSortColumnQuery } from 'lib/ViewRolesBuilder';
buildFilterQuery,
buildSortColumnQuery,
} from 'lib/ViewRolesBuilder';
import { flatToNestedArray } from 'utils'; import { flatToNestedArray } from 'utils';
import DependencyGraph from 'lib/DependencyGraph'; import DependencyGraph from 'lib/DependencyGraph';
import AccountTypesUtils from 'lib/AccountTypes' import AccountTypesUtils from 'lib/AccountTypes';
import AccountSettings from './Account.Settings';
import ModelSettings from './ModelSetting';
export default class Account extends TenantModel { export default class Account extends mixin(TenantModel, [ModelSettings]) {
/** /**
* Table name. * Table name.
*/ */
@@ -21,7 +20,7 @@ export default class Account extends TenantModel {
/** /**
* Timestamps columns. * Timestamps columns.
*/ */
get timestamps() { static get timestamps() {
return ['createdAt', 'updatedAt']; return ['createdAt', 'updatedAt'];
} }
@@ -35,7 +34,7 @@ export default class Account extends TenantModel {
'accountRootType', 'accountRootType',
'accountNormal', 'accountNormal',
'isBalanceSheetAccount', 'isBalanceSheetAccount',
'isPLSheet' 'isPLSheet',
]; ];
} }
@@ -95,6 +94,13 @@ export default class Account extends TenantModel {
const TABLE_NAME = Account.tableName; const TABLE_NAME = Account.tableName;
return { return {
/**
* Inactive/Active mode.
*/
inactiveMode(query, active = false) {
query.where('active', !active);
},
filterAccounts(query, accountIds) { filterAccounts(query, accountIds) {
if (accountIds.length > 0) { if (accountIds.length > 0) {
query.whereIn(`${TABLE_NAME}.id`, accountIds); query.whereIn(`${TABLE_NAME}.id`, accountIds);
@@ -160,7 +166,10 @@ export default class Account extends TenantModel {
* @return {boolean} * @return {boolean}
*/ */
isParentType(parentType) { isParentType(parentType) {
return AccountTypesUtils.isParentTypeEqualsKey(this.accountType, parentType); return AccountTypesUtils.isParentTypeEqualsKey(
this.accountType,
parentType
);
} }
/** /**
@@ -193,7 +202,10 @@ export default class Account extends TenantModel {
* @param {Object} options * @param {Object} options
*/ */
static toNestedArray(accounts, options = { children: 'children' }) { static toNestedArray(accounts, options = { children: 'children' }) {
return flatToNestedArray(accounts, { id: 'id', parentId: 'parentAccountId' }) return flatToNestedArray(accounts, {
id: 'id',
parentId: 'parentAccountId',
});
} }
/** /**
@@ -201,92 +213,16 @@ export default class Account extends TenantModel {
* @param {IAccount[]} accounts * @param {IAccount[]} accounts
*/ */
static toDependencyGraph(accounts) { static toDependencyGraph(accounts) {
return DependencyGraph.fromArray( return DependencyGraph.fromArray(accounts, {
accounts, { itemId: 'id', parentItemId: 'parentAccountId' } itemId: 'id',
); parentItemId: 'parentAccountId',
});
} }
/** /**
* Model defined fields. * Model settings.
*/ */
static get fields() { static get meta() {
return { return AccountSettings;
name: {
label: 'Account name',
column: 'name',
columnType: 'string',
fieldType: 'text',
},
type: {
label: 'Account type',
column: 'account_type',
},
description: {
label: 'Description',
column: 'description',
columnType: 'string',
fieldType: 'text',
},
code: {
label: 'Account code',
column: 'code',
columnType: 'string',
fieldType: 'text',
},
root_type: {
label: 'Root type',
options: [
{ key: 'asset', label: 'Asset', },
{ key: 'liability', label: 'Liability' },
{ key: 'equity', label: 'Equity' },
{ key: 'Income', label: 'Income' },
{ key: 'expense', label: 'Expense' },
],
query: (query, role) => {
const accountsTypes = AccountTypesUtils.getTypesByRootType(role.value);
const accountsTypesKeys = accountsTypes.map(type => type.key);
query.whereIn('account_type', accountsTypesKeys);
},
},
created_at: {
label: 'Created at',
column: 'created_at',
columnType: 'date',
fieldType: 'date',
},
active: {
label: 'Active',
column: 'active',
columnType: 'boolean',
fieldType: 'checkbox',
},
balance: {
label: 'Balance',
column: 'amount',
columnType: 'number',
fieldType: 'number',
},
currency: {
label: 'Currency',
column: 'currency_code',
fieldType: 'options',
optionsResource: 'currency',
optionsKey: 'currency_code',
optionsLabel: 'currency_name',
},
normal: {
label: 'Account normal',
column: 'account_type_id',
fieldType: 'options',
relation: 'account_types.id',
relationColumn: 'account_types.normal',
options: [
{ key: 'credit', label: 'Credit' },
{ key: 'debit', label: 'Debit' },
],
},
};
} }
} }

View File

@@ -0,0 +1,79 @@
import { IModelMeta } from 'interfaces';
import Bill from './Bill';
export default {
defaultFilterField: 'vendor',
defaultSort: {
sortOrder: 'DESC',
sortField: 'bill_date',
},
fields: {
// vendor: {
// name: 'Vendor',
// column: 'vendor_id',
// },
'bill_number': {
name: 'Bill number',
column: 'bill_number',
columnable: true,
fieldType: 'text',
},
'bill_date': {
name: 'Bill date',
column: 'bill_date',
columnable: true,
fieldType: 'date',
},
'due_date': {
name: 'Due date',
column: 'due_date',
columnable: true,
fieldType: 'date',
},
'reference_no': {
name: 'Reference No.',
column: 'reference_no',
columnable: true,
fieldType: 'text',
},
'status': {
name: 'Status',
fieldType: 'enumeration',
columnable: true,
options: [
{ name: 'Paid', key: 'paid' },
{ name: 'Partially paid', key: 'partially-paid' },
{ name: 'Overdue', key: 'overdue' },
{ name: 'Unpaid', key: 'unpaid' },
{ name: 'Opened', key: 'opened' },
{ name: 'Draft', key: 'draft' },
],
// filterQuery: Bill.statusFieldFilterQuery,
// sortQuery: Bill.statusFieldSortQuery,
},
'amount': {
name: 'Amount',
column: 'amount',
columnable: true,
fieldType: 'number',
},
'payment_amount': {
name: 'Payment amount',
column: 'payment_amount',
columnable: true,
fieldType: 'number',
},
'note': {
name: 'Note',
column: 'note',
columnable: true,
fieldType: 'text',
},
'created_at': {
name: 'Created at',
column: 'created_at',
columnable: true,
fieldType: 'date',
},
},
} as IModelMeta;

View File

@@ -1,10 +1,11 @@
import { Model, raw } from 'objection'; import { Model, raw, mixin } from 'objection';
import moment from 'moment'; import moment from 'moment';
import { difference } from 'lodash'; import { difference } from 'lodash';
import TenantModel from 'models/TenantModel'; import TenantModel from 'models/TenantModel';
import { query } from 'winston'; import BillSettings from './Bill.Settings';
import ModelSetting from './ModelSetting';
export default class Bill extends TenantModel { export default class Bill extends mixin(TenantModel, [ModelSetting]) {
/** /**
* Table name * Table name
*/ */
@@ -12,10 +13,9 @@ export default class Bill extends TenantModel {
return 'bills'; return 'bills';
} }
static get resourceable() { /**
return true; * Model modifiers.
} */
static get modifiers() { static get modifiers() {
return { return {
/** /**
@@ -198,6 +198,13 @@ export default class Bill extends TenantModel {
return Math.max(date.diff(dueDate, 'days'), 0); return Math.max(date.diff(dueDate, 'days'), 0);
} }
/**
* Bill model settings.
*/
static get meta() {
return BillSettings;
}
/** /**
* Relationship mapping. * Relationship mapping.
*/ */
@@ -270,40 +277,8 @@ export default class Bill extends TenantModel {
[changeMethod]('payment_amount', Math.abs(amount)); [changeMethod]('payment_amount', Math.abs(amount));
} }
static get fields() {
return { static statusFieldFilterQuery(query, role) {
vendor: {
label: 'Vendor',
column: 'vendor_id',
relation: 'contacts.id',
relationColumn: 'contacts.display_name',
},
bill_number: {
label: 'Bill number',
column: 'bill_number',
columnType: 'string',
fieldType: 'text',
},
bill_date: {
label: 'Bill date',
column: 'bill_date',
columnType: 'date',
fieldType: 'date',
},
due_date: {
label: 'Due date',
column: 'due_date',
},
reference_no: {
label: 'Reference No.',
column: 'reference_no',
columnType: 'string',
fieldType: 'text',
},
status: {
label: 'Status',
options: [],
query: (query, role) => {
switch (role.value) { switch (role.value) {
case 'draft': case 'draft':
query.modify('draft'); query.modify('draft');
@@ -324,33 +299,9 @@ export default class Bill extends TenantModel {
query.modify('paid'); query.modify('paid');
break; break;
} }
},
sortQuery(query, role) {
query.modify('sortByStatus', role.order);
},
},
amount: {
label: 'Amount',
column: 'amount',
columnType: 'number',
fieldType: 'number',
},
payment_amount: {
label: 'Payment amount',
column: 'payment_amount',
columnType: 'number',
fieldType: 'number',
},
note: {
label: 'Note',
column: 'note',
},
user: {},
created_at: {
label: 'Created at',
column: 'created_at',
columnType: 'date',
},
}; };
}
static statusFieldSortQuery(query, role) {
return query.modify('sortByStatus', role.order);
};
} }

View File

@@ -0,0 +1,61 @@
import { IModelMeta } from 'interfaces';
export default {
defaultFilterField: 'vendor',
defaultSort: {
sortOrder: 'DESC',
sortField: 'bill_date',
},
fields: {
'vendor': {
name: 'Vendor name',
column: 'vendor_id',
},
'amount': {
name: 'Amount',
column: 'amount',
columnable: true,
fieldType: 'number',
},
'due_amount': {
name: 'Due amount',
column: 'due_amount',
columnable: true,
fieldType: 'number',
},
'payment_account': {
name: 'Payment account',
column: 'payment_account_id',
},
'payment_number': {
name: 'Payment number',
column: 'payment_number',
columnable: true,
fieldType: 'number',
},
'payment_date': {
name: 'Payment date',
column: 'payment_date',
columnable: true,
fieldType: 'date',
},
'reference_no': {
name: 'Reference No.',
column: 'reference',
columnable: true,
fieldType: 'text',
},
'description': {
name: 'Description',
column: 'description',
columnable: true,
fieldType: 'text',
},
'created_at': {
name: 'Created at',
column: 'created_at',
columnable: true,
fieldType: 'date',
},
},
};

View File

@@ -1,7 +1,9 @@
import { Model } from "objection"; import { Model, mixin } from "objection";
import TenantModel from "models/TenantModel"; import TenantModel from "models/TenantModel";
import ModelSetting from "./ModelSetting";
import BillPaymentSettings from "./BillPayment.Settings";
export default class BillPayment extends TenantModel { export default class BillPayment extends mixin(TenantModel, [ModelSetting]) {
/** /**
* Table name * Table name
*/ */
@@ -16,8 +18,11 @@ export default class BillPayment extends TenantModel {
return ["createdAt", "updatedAt"]; return ["createdAt", "updatedAt"];
} }
static get resourceable() { /**
return true; * Model settings.
*/
static get meta() {
return BillPaymentSettings;
} }
/** /**
@@ -73,70 +78,4 @@ export default class BillPayment extends TenantModel {
}, },
}; };
} }
/**
* Resource fields.
*/
static get fields() {
return {
vendor: {
label: "Vendor name",
column: "vendor_id",
relation: "contacts.id",
relationColumn: "contacts.display_name",
},
amount: {
label: "Amount",
column: "amount",
columnType: "number",
fieldType: "number",
},
due_amount: {
label: "Due amount",
column: "due_amount",
columnType: "number",
fieldType: "number",
},
payment_account: {
label: "Payment account",
column: "payment_account_id",
relation: "accounts.id",
relationColumn: "accounts.name",
fieldType: "options",
optionsResource: "Account",
optionsKey: "id",
optionsLabel: "name",
},
payment_number: {
label: "Payment number",
column: "payment_number",
columnType: "string",
fieldType: "text",
},
payment_date: {
label: "Payment date",
column: "payment_date",
columnType: "date",
fieldType: "date",
},
reference_no: {
label: "Reference No.",
column: "reference",
columnType: "string",
fieldType: "text",
},
description: {
label: "Description",
column: "description",
columnType: "string",
fieldType: "text",
},
created_at: {
label: "Created at",
column: "created_at",
columnType: "date",
},
};
}
} }

View File

@@ -0,0 +1,98 @@
export default {
fields: {
display_name: {
name: 'Display name',
column: 'display_name',
fieldType: 'text',
columnable: true,
},
email: {
name: 'Email',
column: 'email',
fieldType: 'text',
columnable: true,
},
work_phone: {
name: 'Work phone',
column: 'work_phone',
fieldType: 'text',
columnable: true,
},
personal_phone: {
name: 'Personal phone',
column: 'personal_phone',
fieldType: 'text',
columnable: true,
},
company_name: {
name: 'Company name',
column: 'company_name',
fieldType: 'text',
columnable: true,
},
website: {
name: 'Website',
column: 'website',
fieldType: 'text',
columnable: true,
},
created_at: {
name: 'Created at',
column: 'created_at',
fieldType: 'date',
columnable: true,
},
balance: {
name: 'Balance',
column: 'balance',
fieldType: 'number',
columnable: true,
},
opening_balance: {
name: 'Opening balance',
column: 'opening_balance',
fieldType: 'number',
columnable: true,
},
opening_balance_at: {
name: 'Opening balance at',
column: 'opening_balance_at',
filterable: false,
fieldType: 'date',
columnable: true,
},
currency_code: {
column: 'currency_code',
columnable: true,
fieldType: 'text',
},
status: {
label: 'Status',
options: [
{ key: 'active', label: 'Active' },
{ key: 'inactive', label: 'Inactive' },
{ key: 'overdue', label: 'Overdue' },
{ key: 'unpaid', label: 'Unpaid' },
],
columnable: true,
filterQuery: statusFieldFilterQuery,
},
},
};
function statusFieldFilterQuery(query, role) {
switch (role.value) {
case 'active':
query.modify('active');
break;
case 'inactive':
query.modify('inactive');
break;
case 'overdue':
query.modify('overdue');
break;
case 'unpaid':
query.modify('unpaid');
break;
}
}

View File

@@ -1,7 +1,9 @@
import { Model } from 'objection'; import { Model, mixin } from 'objection';
import TenantModel from 'models/TenantModel'; import TenantModel from 'models/TenantModel';
import PaginationQueryBuilder from './Pagination'; import PaginationQueryBuilder from './Pagination';
import QueryParser from 'lib/LogicEvaluation/QueryParser'; import QueryParser from 'lib/LogicEvaluation/QueryParser';
import ModelSetting from './ModelSetting';
import CustomerSettings from './Customer.Settings';
class CustomerQueryBuilder extends PaginationQueryBuilder { class CustomerQueryBuilder extends PaginationQueryBuilder {
constructor(...args) { constructor(...args) {
@@ -15,7 +17,7 @@ class CustomerQueryBuilder extends PaginationQueryBuilder {
} }
} }
export default class Customer extends TenantModel { export default class Customer extends mixin(TenantModel, [ModelSetting]) {
/** /**
* Query builder. * Query builder.
*/ */
@@ -63,6 +65,13 @@ export default class Customer extends TenantModel {
*/ */
static get modifiers() { static get modifiers() {
return { return {
/**
* Inactive/Active mode.
*/
inactiveMode(query, active = false) {
query.where('active', !active);
},
/** /**
* Filters the active customers. * Filters the active customers.
*/ */
@@ -81,10 +90,9 @@ export default class Customer extends TenantModel {
overdue(query) { overdue(query) {
query.select( query.select(
'*', '*',
Customer Customer.relatedQuery('overDueInvoices', query.knex())
.relatedQuery('overDueInvoices', query.knex())
.count() .count()
.as('countOverdue'), .as('countOverdue')
); );
query.having('countOverdue', '>', 0); query.having('countOverdue', '>', 0);
}, },
@@ -93,7 +101,7 @@ export default class Customer extends TenantModel {
*/ */
unpaid(query) { unpaid(query) {
query.whereRaw('`BALANCE` + `OPENING_BALANCE` <> 0'); query.whereRaw('`BALANCE` + `OPENING_BALANCE` <> 0');
} },
}; };
} }
@@ -122,77 +130,12 @@ export default class Customer extends TenantModel {
}, },
filter: (query) => { filter: (query) => {
query.modify('overdue'); query.modify('overdue');
} },
} },
}; };
} }
static get fields() { static get meta() {
return { return CustomerSettings;
contact_service: {
column: 'contact_service',
},
display_name: {
column: 'display_name',
},
email: {
column: 'email',
},
work_phone: {
column: 'work_phone',
},
personal_phone: {
column: 'personal_phone',
},
company_name: {
column: 'company_name',
},
website: {
column: 'website'
},
created_at: {
column: 'created_at',
},
balance: {
column: 'balance',
},
opening_balance: {
column: 'opening_balance',
},
opening_balance_at: {
column: 'opening_balance_at',
},
currency_code: {
column: 'currency_code',
},
status: {
label: 'Status',
options: [
{ key: 'active', label: 'Active' },
{ key: 'inactive', label: 'Inactive' },
{ key: 'overdue', label: 'Overdue' },
{ key: 'unpaid', label: 'Unpaid' },
],
query: (query, role) => {
switch(role.value) {
case 'active':
query.modify('active');
break;
case 'inactive':
query.modify('inactive');
break;
case 'overdue':
query.modify('overdue');
break;
case 'unpaid':
query.modify('unpaid');
break;
}
},
},
created_at: {
column: 'created_at',
}
};
} }
} }

View File

@@ -0,0 +1,82 @@
/**
* Expense - Settings.
*/
export default {
defaultFilterField: 'description',
defaultSort: {
sortOrder: 'DESC',
sortField: 'name',
},
fields: {
'payment_date': {
name: 'Payment date',
column: 'payment_date',
fieldType: 'date',
},
'payment_account': {
name: 'Payment account',
column: 'payment_account_id',
fieldType: 'relation',
fieldRelation: 'paymentAccount',
fieldRelationType: 'enumeration',
relationLabelField: 'name',
relationKeyField: 'slug',
},
'amount': {
name: 'Amount',
column: 'total_amount',
fieldType: 'number',
},
// currency_code: {
// name: 'Currency',
// column: 'currency_code',
// },
'reference_no': {
name: 'Reference No.',
column: 'reference_no',
fieldType: 'text',
},
'description': {
name: 'Description',
column: 'description',
fieldType: 'text',
},
'published': {
name: 'Published',
column: 'published_at',
fieldType: 'date',
},
'status': {
name: 'Status',
fieldType: 'enumeration',
options: [
{ key: 'draft', name: 'Draft' },
{ key: 'published', name: 'Published' },
],
filterQuery: statusFieldFilterQuery,
sortQuery: statusFieldSortQuery,
},
'created_at': {
name: 'Created at',
column: 'created_at',
fieldType: 'date',
},
},
};
function statusFieldFilterQuery(query, role) {
switch (role.value) {
case 'draft':
query.modify('filterByDraft');
break;
case 'published':
query.modify('filterByPublished');
break;
}
}
function statusFieldSortQuery(query, role) {
return query.modify('sortByStatus', role.order);
}

View File

@@ -1,8 +1,10 @@
import { Model } from 'objection'; import { Model, mixin } from 'objection';
import TenantModel from 'models/TenantModel'; import TenantModel from 'models/TenantModel';
import { viewRolesBuilder } from 'lib/ViewRolesBuilder'; import { viewRolesBuilder } from 'lib/ViewRolesBuilder';
import ModelSetting from './ModelSetting';
import ExpenseSettings from './Expense.Settings';
export default class Expense extends TenantModel { export default class Expense extends mixin(TenantModel, [ModelSetting]) {
/** /**
* Table name * Table name
*/ */
@@ -31,12 +33,6 @@ export default class Expense extends TenantModel {
return true; return true;
} }
/**
*
*/
static get media() {
return true;
}
static get virtualAttributes() { static get virtualAttributes() {
return ['isPublished', 'unallocatedCostAmount']; return ['isPublished', 'unallocatedCostAmount'];
@@ -142,71 +138,7 @@ export default class Expense extends TenantModel {
}; };
} }
/** static get meta() {
* Model defined fields. return ExpenseSettings;
*/
static get fields() {
return {
payment_date: {
label: 'Payment date',
column: 'payment_date',
columnType: 'date',
},
payment_account: {
label: 'Payment account',
column: 'payment_account_id',
relation: 'accounts.id',
optionsResource: 'account',
},
amount: {
label: 'Amount',
column: 'total_amount',
columnType: 'number',
},
currency_code: {
label: 'Currency',
column: 'currency_code',
optionsResource: 'currency',
},
reference_no: {
label: 'Reference No.',
column: 'reference_no',
columnType: 'string',
},
description: {
label: 'Description',
column: 'description',
columnType: 'string',
},
published: {
label: 'Published',
column: 'published_at',
},
status: {
label: 'Status',
options: [
{ key: 'draft', label: 'Draft' },
{ key: 'published', label: 'Published' },
],
query: (query, role) => {
switch (role.value) {
case 'draft':
query.modify('filterByDraft');
break;
case 'published':
query.modify('filterByPublished');
break;
}
},
sortQuery(query, role) {
query.modify('sortByStatus', role.order);
},
},
created_at: {
label: 'Created at',
column: 'created_at',
columnType: 'date',
},
};
} }
} }

View File

@@ -0,0 +1,60 @@
export default {
defaultFilterField: 'date',
defaultSort: {
sortOrder: 'DESC',
sortField: 'date',
},
fields: {
'date': {
name: 'Date',
column: 'date',
fieldType: 'date',
columnable: true,
},
'type': {
name: 'Adjustment type',
column: 'type',
fieldType: 'enumeration',
options: [
{ key: 'increment', name: 'Increment' },
{ key: 'decrement', name: 'Decrement' },
],
columnable: true,
},
'adjustment_account': {
name: 'Adjustment account',
column: 'adjustment_account_id',
columnable: true,
},
'reason': {
name: 'Reason',
column: 'reason',
fieldType: 'text',
columnable: true,
},
'reference_no': {
name: 'Reference No.',
column: 'reference_no',
fieldType: 'text',
columnable: true,
},
'description': {
name: 'Description',
column: 'description',
fieldType: 'text',
columnable: true,
},
'published_at': {
name: 'Published at',
column: 'published_at',
fieldType: 'date',
columnable: true,
},
'created_at': {
name: 'Created at',
column: 'created_at',
fieldType: 'date',
columnable: true,
},
},
};

View File

@@ -1,5 +1,6 @@
import { Model } from 'objection'; import { Model } from 'objection';
import TenantModel from 'models/TenantModel'; import TenantModel from 'models/TenantModel';
import InventoryAdjustmentSettings from './InventoryAdjustment.Settings';
export default class InventoryAdjustment extends TenantModel { export default class InventoryAdjustment extends TenantModel {
/** /**
@@ -40,8 +41,8 @@ export default class InventoryAdjustment extends TenantModel {
static getInventoryDirection(type) { static getInventoryDirection(type) {
const directions = { const directions = {
'increment': 'IN', increment: 'IN',
'decrement': 'OUT', decrement: 'OUT',
}; };
return directions[type] || ''; return directions[type] || '';
} }
@@ -81,52 +82,9 @@ export default class InventoryAdjustment extends TenantModel {
} }
/** /**
* Model defined fields. * Model settings.
*/ */
static get fields() { static get meta() {
return { return InventoryAdjustmentSettings;
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

@@ -0,0 +1,102 @@
export default {
defaultFilterField: 'name',
defaultSort: {
sortField: 'name',
sortOrder: 'DESC',
},
fields: {
'type': {
name: 'Item type',
column: 'type',
columnable: true,
fieldType: 'enumeration',
options: [
{ key: 'inventory', label: 'Inventory', },
{ key: 'service', label: 'Service' },
{ key: 'non-inventory', label: 'Non Inventory', },
],
},
'name': {
name: 'Name',
column: 'name',
columnable: true,
fieldType: 'text',
},
'code': {
name: 'Code',
column: 'code',
columnable: true,
fieldType: 'text',
},
'sellable': {
name: 'Sellable',
column: 'sellable',
fieldType: 'boolean',
},
'purchasable': {
name: 'Purchasable',
column: 'purchasable',
fieldType: 'boolean',
},
'sell_price': {
name: 'Sell price',
column: 'sell_price',
fieldType: 'number',
},
'cost_price': {
name: 'Cost price',
column: 'cost_price',
fieldType: 'number',
},
'cost_account': {
name: 'Cost account',
column: 'cost_account_id',
columnable: true,
},
'sell_account': {
name: 'Sell account',
column: 'sell_account_id',
},
'inventory_account': {
name: 'Inventory account',
column: 'inventory_account_id',
},
'sell_description': {
name: 'Sell description',
column: 'sell_description',
fieldType: 'text',
},
'purchase_description': {
name: 'Purchase description',
column: 'purchase_description',
fieldType: 'text',
},
'quantity_on_hand': {
name: 'Quantity on hand',
column: 'quantity_on_hand',
fieldType: 'number',
},
'note': {
name: 'Note',
column: 'note',
fieldType: 'text',
columnable: true,
},
'category': {
name: 'Category',
column: 'category_id',
columnable: true,
},
'active': {
name: 'Active',
column: 'active',
fieldType: 'boolean',
},
'created_at': {
name: 'Created at',
column: 'created_at',
columnType: 'date',
fieldType: 'date',
},
},
};

View File

@@ -1,20 +1,22 @@
import { Model } from "objection"; import { Model, mixin } from 'objection';
import TenantModel from "models/TenantModel"; import TenantModel from 'models/TenantModel';
import { buildFilterQuery } from "lib/ViewRolesBuilder"; import { buildFilterQuery } from 'lib/ViewRolesBuilder';
import ItemSettings from './Item.Settings';
import ModelSetting from './ModelSetting';
export default class Item extends TenantModel { export default class Item extends mixin(TenantModel, [ModelSetting]) {
/** /**
* Table name * Table name
*/ */
static get tableName() { static get tableName() {
return "items"; return 'items';
} }
/** /**
* Model timestamps. * Model timestamps.
*/ */
get timestamps() { get timestamps() {
return ["createdAt", "updatedAt"]; return ['createdAt', 'updatedAt'];
} }
/** /**
@@ -35,6 +37,13 @@ export default class Item extends TenantModel {
viewRolesBuilder(query, conditions, logicExpression) { viewRolesBuilder(query, conditions, logicExpression) {
buildFilterQuery(Item.tableName, conditions, logicExpression)(query); buildFilterQuery(Item.tableName, conditions, logicExpression)(query);
}, },
/**
* Inactive/Active mode.
*/
inactiveMode(query, active = false) {
query.where('active', !active);
},
}; };
} }
@@ -42,9 +51,9 @@ export default class Item extends TenantModel {
* Relationship mapping. * Relationship mapping.
*/ */
static get relationMappings() { static get relationMappings() {
const Media = require("models/Media"); const Media = require('models/Media');
const Account = require("models/Account"); const Account = require('models/Account');
const ItemCategory = require("models/ItemCategory"); const ItemCategory = require('models/ItemCategory');
return { return {
/** /**
@@ -54,8 +63,8 @@ export default class Item extends TenantModel {
relation: Model.BelongsToOneRelation, relation: Model.BelongsToOneRelation,
modelClass: ItemCategory.default, modelClass: ItemCategory.default,
join: { join: {
from: "items.categoryId", from: 'items.categoryId',
to: "items_categories.id", to: 'items_categories.id',
}, },
}, },
@@ -63,8 +72,8 @@ export default class Item extends TenantModel {
relation: Model.BelongsToOneRelation, relation: Model.BelongsToOneRelation,
modelClass: Account.default, modelClass: Account.default,
join: { join: {
from: "items.costAccountId", from: 'items.costAccountId',
to: "accounts.id", to: 'accounts.id',
}, },
}, },
@@ -72,8 +81,8 @@ export default class Item extends TenantModel {
relation: Model.BelongsToOneRelation, relation: Model.BelongsToOneRelation,
modelClass: Account.default, modelClass: Account.default,
join: { join: {
from: "items.sellAccountId", from: 'items.sellAccountId',
to: "accounts.id", to: 'accounts.id',
}, },
}, },
@@ -81,8 +90,8 @@ export default class Item extends TenantModel {
relation: Model.BelongsToOneRelation, relation: Model.BelongsToOneRelation,
modelClass: Account.default, modelClass: Account.default,
join: { join: {
from: "items.inventoryAccountId", from: 'items.inventoryAccountId',
to: "accounts.id", to: 'accounts.id',
}, },
}, },
@@ -90,110 +99,21 @@ export default class Item extends TenantModel {
relation: Model.ManyToManyRelation, relation: Model.ManyToManyRelation,
modelClass: Media.default, modelClass: Media.default,
join: { join: {
from: "items.id", from: 'items.id',
through: { through: {
from: "media_links.model_id", from: 'media_links.model_id',
to: "media_links.media_id", to: 'media_links.media_id',
}, },
to: "media.id", to: 'media.id',
}, },
}, },
}; };
} }
/** /**
* Item fields. * Model settings.
*/ */
static get fields() { static get meta() {
return { return ItemSettings;
type: {
label: "Type",
column: "type",
},
name: {
label: "Name",
column: "name",
},
code: {
label: "Code",
column: "code",
},
sellable: {
label: "Sellable",
column: "sellable",
},
purchasable: {
label: "Purchasable",
column: "purchasable",
},
sell_price: {
label: "Sell price",
column: "sell_price",
},
cost_price: {
label: "Cost price",
column: "cost_price",
},
currency_code: {
label: "Currency",
column: "currency_code",
},
cost_account: {
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",
column: "sell_description",
},
purchase_description: {
label: "Purchase description",
column: "purchase_description",
},
quantity_on_hand: {
label: "Quantity on hand",
column: "quantity_on_hand",
},
note: {
label: "Note",
column: "note",
},
category: {
label: "Category",
column: "category_id",
relation: "items_categories.id",
relationColumn: "items_categories.name",
},
active: {
label: "Active",
column: "active",
},
// 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

@@ -0,0 +1,41 @@
export default {
defaultFilterField: 'name',
defaultSort: {
sortField: 'name',
sortOrder: 'DESC',
},
fields: {
name: {
label: 'Name',
column: 'name',
fieldType: 'text',
},
description: {
label: 'Description',
column: 'description',
fieldType: 'text',
},
cost_account: {
label: 'Cost account',
column: 'cost_account_id',
},
sell_account: {
label: 'Sell account',
column: 'sell_account_id',
},
inventory_account: {
label: 'Inventory account',
column: 'inventory_account_id',
},
count: {
label: 'Count',
column: 'count',
sortQuery: this.sortCountQuery,
},
created_at: {
label: 'Created at',
column: 'created_at',
columnType: 'date',
},
},
};

View File

@@ -42,68 +42,6 @@ export default class ItemCategory extends TenantModel {
}; };
} }
/**
* Item category fields.
*/
static get fields() {
return {
name: {
label: 'Name',
column: 'name',
columnType: 'string'
},
description: {
label: 'Description',
column: 'description',
columnType: 'string'
},
user: {
label: 'User',
column: 'user_id',
relation: 'users.id',
relationColumn: 'users.id',
},
cost_account: {
label: 'Cost account',
column: 'cost_account_id',
relation: 'accounts.id',
optionsResource: 'account'
},
sell_account: {
label: 'Sell account',
column: 'sell_account_id',
relation: 'accounts.id',
optionsResource: 'account'
},
inventory_account: {
label: 'Inventory account',
column: 'inventory_account_id',
relation: 'accounts.id',
optionsResource: 'account'
},
cost_method: {
label: 'Cost method',
column: 'cost_method',
options: [{
key: 'FIFO', label: 'First-in first-out (FIFO)',
key: 'LIFO', label: 'Last-in first-out (LIFO)',
key: 'average', label: 'Average rate',
}],
columnType: 'string',
},
count: {
label: 'Count',
column: 'count',
sortQuery: this.sortCountQuery
},
created_at: {
label: 'Created at',
column: 'created_at',
columnType: 'date',
},
};
}
static sortCountQuery(query, role) { static sortCountQuery(query, role) {
query.orderBy('count', role.order); query.orderBy('count', role.order);
} }

View File

@@ -0,0 +1,54 @@
export default {
defaultFilterField: 'date',
defaultSort: {
sortOrder: 'DESC',
sortField: 'name',
},
fields: {
'date': {
label: 'Date',
column: 'date',
fieldType: 'date',
},
'journal_number': {
label: 'Journal number',
column: 'journal_number',
fieldType: 'text',
},
'reference': {
label: 'Reference No.',
column: 'reference',
fieldType: 'text',
},
'journal_type': {
label: 'Journal type',
column: 'journal_type',
fieldType: 'text',
},
'amount': {
label: 'Amount',
column: 'amount',
columnType: 'number',
},
'description': {
label: 'Description',
column: 'description',
fieldType: 'text',
},
'status': {
label: 'Status',
column: 'status',
fieldType: 'enumeration',
sortQuery: statusFieldSortQuery,
},
'created_at': {
label: 'Created at',
column: 'created_at',
fieldType: 'date',
},
},
};
function statusFieldSortQuery(query, role) {
return query.modify('sortByStatus', role.order);
}

View File

@@ -1,8 +1,10 @@
import { Model } from 'objection'; import { Model, mixin } from 'objection';
import TenantModel from 'models/TenantModel'; import TenantModel from 'models/TenantModel';
import { formatNumber } from 'utils'; import { formatNumber } from 'utils';
import ModelSetting from './ModelSetting';
import ManualJournalSettings from './ManualJournal.Settings';
export default class ManualJournal extends TenantModel { export default class ManualJournal extends mixin(TenantModel, [ModelSetting]) {
/** /**
* Table name. * Table name.
*/ */
@@ -99,52 +101,7 @@ export default class ManualJournal extends TenantModel {
}; };
} }
/** static get meta() {
* Model defined fields. return ManualJournalSettings;
*/
static get fields() {
return {
date: {
label: 'Date',
column: 'date',
columnType: 'date',
},
journal_number: {
label: 'Journal number',
column: 'journal_number',
columnType: 'string',
},
reference: {
label: 'Reference No.',
column: 'reference',
columnType: 'string',
},
journal_type: {
label: 'Journal type',
column: 'journal_type',
columnType: 'string',
},
amount: {
label: 'Amount',
column: 'amount',
columnType: 'number',
},
description: {
label: 'Description',
column: 'description',
columnType: 'string',
},
status: {
label: 'Status',
column: 'status',
sortQuery(query, role) {
query.modify('sortByStatus', role.order);
},
},
created_at: {
label: 'Created at',
column: 'created_at',
},
};
} }
} }

View File

@@ -0,0 +1,54 @@
import { get } from 'lodash';
import { IModelMeta, IModelMetaField, IModelMetaDefaultSort } from 'interfaces';
export default (Model) =>
class ModelSettings extends Model {
/**
*
*/
static get meta(): IModelMeta {
throw new Error('');
}
/**
* Retrieve specific model field meta of the given field key.
* @param {string} key
* @returns {IModelMetaField}
*/
public static getField(key: string): IModelMetaField {
return get(this.meta.fields, key);
}
/**
* Retrieve the specific model meta.
* @param {string} key
* @returns
*/
public static getMeta(key: string) {
return get(this.meta, key);
}
/**
* Retrieve the model meta fields.
* @return {{ [key: string]: IModelMetaField }}
*/
public static get fields(): { [key: string]: IModelMetaField } {
return this.getMeta('fields');
}
/**
* Retrieve the model default sort settings.
* @return {IModelMetaDefaultSort}
*/
public static get defaultSort(): IModelMetaDefaultSort {
return this.getMeta('defaultSort');
}
/**
* Retrieve the default filter field key.
* @return {string}
*/
public static get defaultFilterField(): string {
return this.getMeta('defaultFilterField');
}
};

View File

@@ -0,0 +1,43 @@
export default {
fields: {
customer: {
name: 'Customer',
column: 'customer_id',
},
payment_date: {
name: 'Payment date',
column: 'payment_date',
fieldType: 'date',
},
amount: {
name: 'Amount',
column: 'amount',
fieldType: 'number',
},
reference_no: {
name: 'Reference No.',
column: 'reference_no',
fieldType: 'text',
},
deposit_account: {
name: 'Deposit account',
column: 'deposit_account_id',
},
payment_receive_no: {
name: 'Payment receive No.',
column: 'payment_receive_no',
fieldType: 'text',
},
statement: {
name: 'Statement',
column: 'statement',
fieldType: 'text',
},
created_at: {
name: 'Created at',
column: 'created_at',
fieldDate: 'date',
},
},
};

View File

@@ -1,7 +1,9 @@
import { Model } from 'objection'; import { Model, mixin } from 'objection';
import TenantModel from 'models/TenantModel'; import TenantModel from 'models/TenantModel';
import ModelSetting from './ModelSetting';
import PaymentReceiveSettings from './PaymentReceive.Settings';
export default class PaymentReceive extends TenantModel { export default class PaymentReceive extends mixin(TenantModel, [ModelSetting]) {
/** /**
* Table name. * Table name.
*/ */
@@ -75,63 +77,9 @@ export default class PaymentReceive extends TenantModel {
} }
/** /**
* Model defined fields. *
*/ */
static get fields() { static get meta() {
return { return PaymentReceiveSettings;
customer: {
label: 'Customer',
column: 'customer_id',
relation: 'contacts.id',
relationColumn: 'contacts.displayName',
fieldType: 'options',
optionsResource: 'customers',
optionsKey: 'id',
optionsLable: 'displayName',
},
payment_date: {
label: 'Payment date',
column: 'payment_date',
columnType: 'date',
fieldType: 'date',
},
amount: {
label: 'Amount',
column: 'amount',
columnType: 'number',
fieldType: 'number',
},
reference_no: {
label: 'Reference No.',
column: 'reference_no',
columnType: 'string',
fieldType: 'text',
},
deposit_account: {
column: 'deposit_account_id',
lable: 'Deposit account',
relation: "accounts.id",
relationColumn: 'accounts.name',
optionsResource: "account",
},
payment_receive_no: {
label: 'Payment receive No.',
column: 'payment_receive_no',
columnType: 'string',
fieldType: 'text',
},
description: {
label: 'description',
column: 'description',
columnType: 'string',
fieldType: 'text',
},
created_at: {
label: 'Created at',
column: 'created_at',
columnType: 'date',
},
};
} }
} }

View File

@@ -0,0 +1,85 @@
export default {
defaultFilterField: 'estimate_date',
defaultSort: {
sortOrder: 'DESC',
sortField: 'estimate_date',
},
fields: {
'amount': {
name: 'Amount',
column: 'amount',
fieldType: 'number',
},
'estimate_number': {
name: 'Estimate number',
column: 'estimate_number',
fieldType: 'text',
},
'customer': {
name: 'Customer',
column: 'customer_id',
},
'estimate_date': {
name: 'Estimate date',
column: 'estimate_date',
fieldType: 'date',
},
'expiration_date': {
name: 'Expiration date',
column: 'expiration_date',
fieldType: 'date',
},
'reference_no': {
name: 'Reference No.',
column: 'reference',
fieldType: 'text',
},
'note': {
name: 'Note',
column: 'note',
fieldType: 'text',
},
'terms_conditions': {
name: 'Terms & conditions',
column: 'terms_conditions',
fieldType: 'text',
},
'status': {
name: 'Status',
filterQuery: statusFieldFilterQuery,
sortQuery: statusFieldSortQuery,
},
'created_at': {
name: 'Created at',
column: 'created_at',
columnType: 'date',
},
},
};
function statusFieldSortQuery(query, role) {
return query.modify('orderByDraft', role.order);
}
function statusFieldFilterQuery(query, role) {
switch (role.value) {
case 'draft':
query.modify('draft');
break;
case 'delivered':
query.modify('delivered');
break;
case 'approved':
query.modify('approved');
break;
case 'rejected':
query.modify('rejected');
break;
case 'invoiced':
query.modify('invoiced');
break;
case 'expired':
query.modify('expired');
break;
}
}

View File

@@ -1,11 +1,11 @@
import moment from 'moment'; import moment from 'moment';
import { Model } from 'objection'; import { Model, mixin } from 'objection';
import TenantModel from 'models/TenantModel'; import TenantModel from 'models/TenantModel';
import { defaultToTransform } from 'utils'; import { defaultToTransform } from 'utils';
import HasItemEntries from 'services/Sales/HasItemsEntries'; import SaleEstimateSettings from './SaleEstimate.Settings';
import { query } from 'winston'; import ModelSetting from './ModelSetting';
export default class SaleEstimate extends TenantModel { export default class SaleEstimate extends mixin(TenantModel, [ModelSetting]) {
/** /**
* Table name * Table name
*/ */
@@ -29,7 +29,7 @@ export default class SaleEstimate extends TenantModel {
'isExpired', 'isExpired',
'isConvertedToInvoice', 'isConvertedToInvoice',
'isApproved', 'isApproved',
'isRejected' 'isRejected',
]; ];
} }
@@ -57,7 +57,7 @@ export default class SaleEstimate extends TenantModel {
return defaultToTransform( return defaultToTransform(
this.expirationDate, this.expirationDate,
moment().isAfter(this.expirationDate, 'day'), moment().isAfter(this.expirationDate, 'day'),
false, false
); );
} }
@@ -123,14 +123,14 @@ export default class SaleEstimate extends TenantModel {
* Filters the approved estimates transactions. * Filters the approved estimates transactions.
*/ */
approved(query) { approved(query) {
query.whereNot('approved_at', null) query.whereNot('approved_at', null);
}, },
/** /**
* Sorting the estimates orders by delivery status. * Sorting the estimates orders by delivery status.
*/ */
orderByDraft(query, order) { orderByDraft(query, order) {
query.orderByRaw(`delivered_at is null ${order}`) query.orderByRaw(`delivered_at is null ${order}`);
} },
}; };
} }
@@ -151,7 +151,7 @@ export default class SaleEstimate extends TenantModel {
}, },
filter(query) { filter(query) {
query.where('contact_service', 'customer'); query.where('contact_service', 'customer');
} },
}, },
entries: { entries: {
relation: Model.HasManyRelation, relation: Model.HasManyRelation,
@@ -168,91 +168,9 @@ export default class SaleEstimate extends TenantModel {
} }
/** /**
* Model defined fields. * Model settings.
*/ */
static get fields() { static get meta() {
return { return SaleEstimateSettings;
amount: {
label: 'Amount',
column: 'amount',
columnType: 'number',
fieldType: 'number',
},
estimate_number: {
label: 'Estimate number',
column: 'estimate_number',
columnType: 'text',
fieldType: 'text',
},
customer: {
label: 'Customer',
column: 'customer_id',
relation: 'contacts.id',
relationColumn: 'contacts.displayName',
fieldType: 'options',
optionsResource: 'customers',
optionsKey: 'id',
optionsLable: 'displayName',
},
estimate_date: {
label: 'Estimate date',
column: 'estimate_date',
columnType: 'date',
fieldType: 'date',
},
expiration_date: {
label: 'Expiration date',
column: 'expiration_date',
columnType: 'date',
fieldType: 'date',
},
reference_no: {
label: "Reference No.",
column: "reference",
columnType: "number",
fieldType: "number",
},
note: {
label: 'Note',
column: 'note',
columnType: 'text',
fieldType: 'text',
},
terms_conditions: {
label: 'Terms & conditions',
column: 'terms_conditions',
columnType: 'text',
fieldType: 'text',
},
status: {
label: 'Status',
query: (query, role) => {
switch(role.value) {
case 'draft':
query.modify('draft'); break;
case 'delivered':
query.modify('delivered'); break;
case 'approved':
query.modify('approved'); break;
case 'rejected':
query.modify('rejected'); break;
case 'invoiced':
query.modify('invoiced');
break;
case 'expired':
query.modify('expired'); break;
}
},
sortQuery: (query, role) => {
query.modify('orderByDraft', role.order);
}
},
created_at: {
label: 'Created at',
column: 'created_at',
columnType: 'date',
},
};
} }
} }

View File

@@ -0,0 +1,86 @@
export default {
defaultFilterField: 'customer',
defaultSort: {
sortOrder: 'DESC',
sortField: 'created_at',
},
fields: {
// customer: {
// name: 'Customer',
// column: 'customer_id',
// },
invoice_date: {
name: 'Invoice date',
column: 'invoice_date',
fieldType: 'date',
columnable: true,
},
due_date: {
name: 'Due date',
column: 'due_date',
fieldType: 'date',
columnable: true,
},
invoice_no: {
name: 'Invoice No.',
column: 'invoice_no',
fieldType: 'text',
columnable: true,
},
reference_no: {
name: 'Reference No.',
column: 'reference_no',
fieldType: 'text',
columnable: true,
},
invoice_message: {
name: 'Invoice message',
column: 'invoice_message',
fieldType: 'text',
columnable: true,
},
terms_conditions: {
name: 'Terms & conditions',
column: 'terms_conditions',
fieldType: 'text',
columnable: true,
},
amount: {
name: 'Invoice amount',
column: 'balance',
columnable: true,
fieldType: 'number',
},
payment_amount: {
name: 'Payment amount',
column: 'payment_amount',
fieldType: 'number',
},
due_amount: {
name: 'Due amount',
column: 'due_amount',
fieldType: 'number',
// sortQuery: SaleInvoice.dueAmountFieldSortQuery,
},
created_at: {
name: 'Created at',
column: 'created_at',
fieldType: 'date',
},
// status: {
// name: 'Status',
// columnable: true,
// fieldType: 'enumeration',
// options: [
// { key: 'draft', name: 'Draft' },
// { key: 'delivered', name: 'Delivered' },
// { key: 'unpaid', name: 'Unpaid' },
// { key: 'overdue', name: 'Overdue' },
// { key: 'partially-paid', name: 'Partially paid' },
// { key: 'paid', name: 'Paid' },
// ],
// // filterQuery: SaleInvoice.statusFieldFilterQuery,
// // sortQuery: SaleInvoice.statusFieldSortQuery,
// },
},
};

View File

@@ -1,8 +1,10 @@
import { Model, raw } from 'objection'; import { mixin, Model, raw } from 'objection';
import moment from 'moment'; import moment from 'moment';
import TenantModel from 'models/TenantModel'; import TenantModel from 'models/TenantModel';
import ModelSetting from './ModelSetting';
import SaleInvoiceMeta from './SaleInvoice.Settings';
export default class SaleInvoice extends TenantModel { export default class SaleInvoice extends mixin(TenantModel, [ModelSetting]) {
/** /**
* Table name * Table name
*/ */
@@ -243,6 +245,9 @@ export default class SaleInvoice extends TenantModel {
const PaymentReceiveEntry = require('models/PaymentReceiveEntry'); const PaymentReceiveEntry = require('models/PaymentReceiveEntry');
return { return {
/**
* Sale invoice associated entries.
*/
entries: { entries: {
relation: Model.HasManyRelation, relation: Model.HasManyRelation,
modelClass: ItemEntry.default, modelClass: ItemEntry.default,
@@ -255,6 +260,9 @@ export default class SaleInvoice extends TenantModel {
}, },
}, },
/**
* Belongs to customer model.
*/
customer: { customer: {
relation: Model.BelongsToOneRelation, relation: Model.BelongsToOneRelation,
modelClass: Contact.default, modelClass: Contact.default,
@@ -267,6 +275,9 @@ export default class SaleInvoice extends TenantModel {
}, },
}, },
/**
* Invoice has associated account transactions.
*/
transactions: { transactions: {
relation: Model.HasManyRelation, relation: Model.HasManyRelation,
modelClass: AccountTransaction.default, modelClass: AccountTransaction.default,
@@ -316,100 +327,17 @@ export default class SaleInvoice extends TenantModel {
} }
/** /**
* Model defined fields. * Sale invoice meta.
*/ */
static get fields() { static get meta() {
return { return SaleInvoiceMeta;
customer: { }
label: 'Customer',
column: 'customer_id',
relation: 'contacts.id',
relationColumn: 'contacts.displayName',
fieldType: 'options', static dueAmountFieldSortQuery(query, role) {
optionsResource: 'customers',
optionsKey: 'id',
optionsLable: 'displayName',
},
invoice_date: {
label: 'Invoice date',
column: 'invoice_date',
columnType: 'date',
fieldType: 'date',
},
due_date: {
label: 'Due date',
column: 'due_date',
columnType: 'date',
fieldType: 'date',
},
invoice_no: {
label: 'Invoice No.',
column: 'invoice_no',
columnType: 'number',
fieldType: 'number',
},
reference_no: {
label: 'Reference No.',
column: 'reference_no',
columnType: 'number',
fieldType: 'number',
},
invoice_message: {
label: 'Invoice message',
column: 'invoice_message',
columnType: 'text',
fieldType: 'text',
},
terms_conditions: {
label: 'Terms & conditions',
column: 'terms_conditions',
columnType: 'text',
fieldType: 'text',
},
invoice_amount: {
label: 'Invoice amount',
column: 'invoice_amount',
columnType: 'number',
fieldType: 'number',
},
payment_amount: {
label: 'Payment amount',
column: 'payment_amount',
columnType: 'number',
fieldType: 'number',
},
balance: {
label: 'Balance',
column: 'balance',
columnType: 'number',
fieldType: 'number',
},
due_amount: {
label: 'Due amount',
column: 'due_amount',
columnType: 'number',
fieldType: 'number',
sortQuery(query, role) {
query.modify('sortByDueAmount', role.order); query.modify('sortByDueAmount', role.order);
}, }
},
created_at: { static statusFieldFilterQuery(query, role) {
label: 'Created at',
column: 'created_at',
columnType: 'date',
},
status: {
label: 'Status',
options: [
{ key: 'draft', label: 'Draft' },
{ key: 'delivered', label: 'Delivered' },
{ key: 'unpaid', label: 'Unpaid' },
{ key: 'overdue', label: 'Overdue' },
{ key: 'partially-paid', label: 'Partially paid' },
{ key: 'paid', label: 'Paid' },
],
query: (query, role) => {
switch (role.value) { switch (role.value) {
case 'draft': case 'draft':
query.modify('draft'); query.modify('draft');
@@ -430,11 +358,9 @@ export default class SaleInvoice extends TenantModel {
query.modify('paid'); query.modify('paid');
break; break;
} }
}, }
sortQuery(query, role) {
static statusFieldSortQuery(query, role) {
query.modify('sortByStatus', role.order); query.modify('sortByStatus', role.order);
},
},
};
} }
} }

View File

@@ -0,0 +1,87 @@
export default {
defaultFilterField: 'receipt_date',
defaultSort: {
sortOrder: 'DESC',
sortField: 'created_at',
},
fields: {
'amount': {
name: 'Amount',
column: 'amount',
fieldType: 'number',
columnable: true
},
'deposit_account': {
column: 'deposit_account_id',
name: 'Deposit account',
columnable: true
},
'customer': {
name: 'Customer',
column: 'customer_id',
columnable: true
},
'receipt_date': {
name: 'Receipt date',
column: 'receipt_date',
fieldType: 'date',
columnable: true
},
'receipt_number': {
name: 'Receipt No.',
column: 'receipt_number',
fieldType: 'text',
columnable: true
},
'reference_no': {
name: 'Reference No.',
column: 'reference_no',
fieldType: 'text',
columnable: true
},
'receipt_message': {
name: 'Receipt message',
column: 'receipt_message',
fieldType: 'text',
columnable: true
},
'statement': {
name: 'Statement',
column: 'statement',
fieldType: 'text',
columnable: true
},
'created_at': {
name: 'Created at',
column: 'created_at',
fieldType: 'date',
columnable: true
},
'status': {
name: 'Status',
fieldType: 'enumeration',
options: [
{ key: 'draft', name: 'Draft' },
{ key: 'closed', name: 'Closed' },
],
query: statusFieldFilterQuery,
sortQuery: statusFieldSortQuery,
columnable: true
},
},
};
function statusFieldFilterQuery(query, role) {
switch (role.value) {
case 'draft':
query.modify('draft');
break;
case 'closed':
query.modify('closed');
break;
}
}
function statusFieldSortQuery(query, role) {
query.modify('sortByStatus', role.order);
}

View File

@@ -1,7 +1,9 @@
import { Model, mixin } from 'objection'; import { Model, mixin } from 'objection';
import TenantModel from 'models/TenantModel'; import TenantModel from 'models/TenantModel';
import ModelSetting from './ModelSetting';
import SaleReceiptSettings from './SaleReceipt.Settings';
export default class SaleReceipt extends TenantModel { export default class SaleReceipt extends mixin(TenantModel, [ModelSetting]) {
/** /**
* Table name * Table name
*/ */
@@ -20,10 +22,7 @@ export default class SaleReceipt extends TenantModel {
* Virtual attributes. * Virtual attributes.
*/ */
static get virtualAttributes() { static get virtualAttributes() {
return [ return ['isClosed', 'isDraft'];
'isClosed',
'isDraft',
];
} }
/** /**
@@ -66,7 +65,7 @@ export default class SaleReceipt extends TenantModel {
*/ */
sortByStatus(query, order) { sortByStatus(query, order) {
query.orderByRaw(`CLOSED_AT IS NULL ${order}`); query.orderByRaw(`CLOSED_AT IS NULL ${order}`);
} },
}; };
} }
@@ -89,7 +88,7 @@ export default class SaleReceipt extends TenantModel {
}, },
filter(query) { filter(query) {
query.where('contact_service', 'customer'); query.where('contact_service', 'customer');
} },
}, },
depositAccount: { depositAccount: {
@@ -118,95 +117,19 @@ export default class SaleReceipt extends TenantModel {
modelClass: AccountTransaction.default, modelClass: AccountTransaction.default,
join: { join: {
from: 'sales_receipts.id', from: 'sales_receipts.id',
to: 'accounts_transactions.referenceId' to: 'accounts_transactions.referenceId',
}, },
filter(builder) { filter(builder) {
builder.where('reference_type', 'SaleReceipt'); builder.where('reference_type', 'SaleReceipt');
}, },
} },
}; };
} }
/** /**
* Model defined fields. * Sale invoice meta.
*/ */
static get fields() { static get meta() {
return { return SaleReceiptSettings;
amount: {
label: 'Amount',
column: 'amount',
columnType: 'number',
fieldType: 'number',
},
deposit_account: {
column: 'deposit_account_id',
label: 'Deposit account',
relation: "accounts.id",
optionsResource: "account",
},
customer: {
label: 'Customer',
column: 'customer_id',
fieldType: 'options',
optionsResource: 'customers',
optionsKey: 'id',
optionsLable: 'displayName',
},
receipt_date: {
label: 'Receipt date',
column: 'receipt_date',
columnType: 'date',
fieldType: 'date',
},
receipt_number: {
label: 'Receipt No.',
column: 'receipt_number',
columnType: 'string',
fieldType: 'text',
},
reference_no: {
label: 'Reference No.',
column: 'reference_no',
columnType: 'text',
fieldType: 'text',
},
receipt_message: {
label: 'Receipt message',
column: 'receipt_message',
columnType: 'text',
fieldType: 'text',
},
statement: {
label: 'Statement',
column: 'statement',
columnType: 'text',
fieldType: 'text',
},
created_at: {
label: 'Created at',
column: 'created_at',
columnType: 'date',
},
status: {
label: 'Status',
options: [
{ key: 'draft', label: 'Draft', },
{ key: 'closed', label: 'Closed' },
],
query: (query, role) => {
switch(role.value) {
case 'draft':
query.modify('draft');
break;
case 'closed':
query.modify('closed');
break;
}
},
sortQuery(query, role) {
query.modify('sortByStatus', role.order);
}
}
};
} }
} }

View File

@@ -0,0 +1,89 @@
export default {
defaultFilterField: 'display_name',
defaultSort: {
sortOrder: 'DESC',
sortField: 'created_at',
},
fields: {
'display_name': {
name: 'Display name',
column: 'display_name',
fieldType: 'text',
},
'email': {
name: 'Email',
column: 'email',
fieldType: 'text',
},
'work_phone': {
name: 'Work phone',
column: 'work_phone',
fieldType: 'text',
},
'personal_phone': {
name: 'Personal phone',
column: 'personal_phone',
fieldType: 'text',
},
'company_name': {
name: 'Company name',
column: 'company_name',
fieldType: 'text',
},
'website': {
name: 'Website',
column: 'website',
fieldType: 'text',
},
'created_at': {
name: 'Created at',
column: 'created_at',
fieldType: 'date',
},
'balance': {
name: 'Balance',
column: 'balance',
fieldType: 'number',
},
'opening_balance': {
name: 'Opening balance',
column: 'opening_balance',
fieldType: 'number',
},
'opening_balance_at': {
name: 'Opening balance at',
column: 'opening_balance_at',
fieldType: 'date',
},
'currency_code': {
name: 'Currency code',
column: 'currency_code',
fieldType: 'text',
},
'status': {
label: 'Status',
options: [
{ key: 'active', label: 'Active' },
{ key: 'inactive', label: 'Inactive' },
{ key: 'overdue', label: 'Overdue' },
{ key: 'unpaid', label: 'Unpaid' },
],
query: (query, role) => {
switch (role.value) {
case 'active':
query.modify('active');
break;
case 'inactive':
query.modify('inactive');
break;
case 'overdue':
query.modify('overdue');
break;
case 'unpaid':
query.modify('unpaid');
break;
}
},
},
},
};

View File

@@ -1,6 +1,8 @@
import { Model, QueryBuilder } from 'objection'; import { Model, mixin } from 'objection';
import TenantModel from 'models/TenantModel'; import TenantModel from 'models/TenantModel';
import PaginationQueryBuilder from './Pagination'; import PaginationQueryBuilder from './Pagination';
import ModelSetting from './ModelSetting';
import VendorSettings from './Vendor.Settings';
class VendorQueryBuilder extends PaginationQueryBuilder { class VendorQueryBuilder extends PaginationQueryBuilder {
constructor(...args) { constructor(...args) {
@@ -14,7 +16,7 @@ class VendorQueryBuilder extends PaginationQueryBuilder {
} }
} }
export default class Vendor extends TenantModel { export default class Vendor extends mixin(TenantModel, [ModelSetting]) {
/** /**
* Query builder. * Query builder.
*/ */
@@ -62,6 +64,13 @@ export default class Vendor extends TenantModel {
*/ */
static get modifiers() { static get modifiers() {
return { return {
/**
* Inactive/Active mode.
*/
inactiveMode(query, active = false) {
query.where('active', !active);
},
/** /**
* Filters the active customers. * Filters the active customers.
*/ */
@@ -125,72 +134,7 @@ export default class Vendor extends TenantModel {
}; };
} }
static get fields() { static get meta() {
return { return VendorSettings;
contact_service: {
column: 'contact_service',
},
display_name: {
column: 'display_name',
},
email: {
column: 'email',
},
work_phone: {
column: 'work_phone',
},
personal_phone: {
column: 'personal_phone',
},
company_name: {
column: 'company_name',
},
website: {
column: 'website'
},
created_at: {
column: 'created_at',
},
balance: {
column: 'balance',
},
opening_balance: {
column: 'opening_balance',
},
opening_balance_at: {
column: 'opening_balance_at',
},
currency_code: {
column: 'currency_code',
},
status: {
label: 'Status',
options: [
{ key: 'active', label: 'Active' },
{ key: 'inactive', label: 'Inactive' },
{ key: 'overdue', label: 'Overdue' },
{ key: 'unpaid', label: 'Unpaid' },
],
query: (query, role) => {
switch(role.value) {
case 'active':
query.modify('active');
break;
case 'inactive':
query.modify('inactive');
break;
case 'overdue':
query.modify('overdue');
break;
case 'unpaid':
query.modify('unpaid');
break;
}
},
},
created_at: {
column: 'created_at',
}
};
} }
} }

View File

@@ -1,6 +1,7 @@
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import { difference, chain, uniq } from 'lodash'; import { difference, chain, uniq } from 'lodash';
import { kebabCase } from 'lodash'; import { kebabCase } from 'lodash';
import R from 'ramda';
import TenancyService from 'services/Tenancy/TenancyService'; import TenancyService from 'services/Tenancy/TenancyService';
import { ServiceError } from 'exceptions'; import { ServiceError } from 'exceptions';
import { import {
@@ -606,6 +607,17 @@ export default class AccountsService {
this.eventDispatcher.dispatch(events.accounts.onActivated); this.eventDispatcher.dispatch(events.accounts.onActivated);
} }
/**
* Parsees accounts list filter DTO.
* @param filterDTO
* @returns
*/
private parseListFilterDTO(filterDTO) {
return R.compose(
this.dynamicListService.parseStringifiedFilter
)(filterDTO);
}
/** /**
* Retrieve accounts datatable list. * Retrieve accounts datatable list.
* @param {number} tenantId * @param {number} tenantId
@@ -613,21 +625,26 @@ export default class AccountsService {
*/ */
public async getAccountsList( public async getAccountsList(
tenantId: number, tenantId: number,
filter: IAccountsFilter filterDTO: IAccountsFilter
): Promise<{ accounts: IAccountResponse[]; filterMeta: IFilterMeta }> { ): Promise<{ accounts: IAccountResponse[]; filterMeta: IFilterMeta }> {
const { Account } = this.tenancy.models(tenantId); const { Account } = this.tenancy.models(tenantId);
// Parses the stringified filter roles.
const filter = this.parseListFilterDTO(filterDTO);
// Dynamic list service.
const dynamicList = await this.dynamicListService.dynamicList( const dynamicList = await this.dynamicListService.dynamicList(
tenantId, tenantId,
Account, Account,
filter filter
); );
this.logger.info('[accounts] trying to get accounts datatable list.', { this.logger.info('[accounts] trying to get accounts datatable list.', {
tenantId, tenantId,
filter, filter,
}); });
const accounts = await Account.query().onBuild((builder) => { const accounts = await Account.query().onBuild((builder) => {
dynamicList.buildQuery()(builder); dynamicList.buildQuery()(builder);
builder.modify('inactiveMode', filter.inactiveMode);
}); });
return { return {
@@ -730,7 +747,8 @@ export default class AccountsService {
{ {
id: 'id', id: 'id',
parentId: 'parent_account_id', parentId: 'parent_account_id',
}); }
);
} }
/** /**

View File

@@ -1,6 +1,6 @@
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import { omit, defaultTo } from 'lodash'; import { omit, defaultTo } from 'lodash';
import async from 'async'; import * as R from 'ramda';
import { import {
EventDispatcher, EventDispatcher,
EventDispatcherInterface, EventDispatcherInterface,
@@ -265,6 +265,16 @@ export default class CustomersService {
return this.transformContactToCustomer(contact); return this.transformContactToCustomer(contact);
} }
/**
* Parses customers list filter DTO.
* @param filterDTO -
*/
private parseCustomersListFilterDTO(filterDTO) {
return R.compose(
this.dynamicListService.parseStringifiedFilter
)(filterDTO);
}
/** /**
* Retrieve customers paginated list. * Retrieve customers paginated list.
* @param {number} tenantId - Tenant id. * @param {number} tenantId - Tenant id.
@@ -272,7 +282,7 @@ export default class CustomersService {
*/ */
public async getCustomersList( public async getCustomersList(
tenantId: number, tenantId: number,
customersFilter: ICustomersFilter filterDTO: ICustomersFilter
): Promise<{ ): Promise<{
customers: ICustomer[]; customers: ICustomer[];
pagination: IPaginationMeta; pagination: IPaginationMeta;
@@ -280,17 +290,23 @@ export default class CustomersService {
}> { }> {
const { Customer } = this.tenancy.models(tenantId); const { Customer } = this.tenancy.models(tenantId);
// Parses customers list filter DTO.
const filter = this.parseCustomersListFilterDTO(filterDTO);
// Dynamic list. // Dynamic list.
const dynamicList = await this.dynamicListService.dynamicList( const dynamicList = await this.dynamicListService.dynamicList(
tenantId, tenantId,
Customer, Customer,
customersFilter filter
); );
// Customers.
const { results, pagination } = await Customer.query() const { results, pagination } = await Customer.query()
.onBuild((query) => { .onBuild((builder) => {
dynamicList.buildQuery()(query); dynamicList.buildQuery()(builder);
builder.modify('inactiveMode', filter.inactiveMode);
}) })
.pagination(customersFilter.page - 1, customersFilter.pageSize); .pagination(filter.page - 1, filter.pageSize);
return { return {
customers: results.map(this.transformContactToCustomer), customers: results.map(this.transformContactToCustomer),

View File

@@ -1,5 +1,6 @@
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import { intersection, defaultTo } from 'lodash'; import { defaultTo } from 'lodash';
import * as R from 'ramda';
import { import {
EventDispatcher, EventDispatcher,
EventDispatcherInterface, EventDispatcherInterface,
@@ -255,6 +256,12 @@ export default class VendorsService {
); );
} }
private parseVendorsListFilterDTO(filterDTO) {
return R.compose(
this.dynamicListService.parseStringifiedFilter
)(filterDTO);
}
/** /**
* Retrieve vendors datatable list. * Retrieve vendors datatable list.
* @param {number} tenantId - Tenant id. * @param {number} tenantId - Tenant id.
@@ -262,7 +269,7 @@ export default class VendorsService {
*/ */
public async getVendorsList( public async getVendorsList(
tenantId: number, tenantId: number,
vendorsFilter: IVendorsFilter filterDTO: IVendorsFilter
): Promise<{ ): Promise<{
vendors: IVendor[]; vendors: IVendor[];
pagination: IPaginationMeta; pagination: IPaginationMeta;
@@ -270,21 +277,28 @@ export default class VendorsService {
}> { }> {
const { Vendor } = this.tenancy.models(tenantId); const { Vendor } = this.tenancy.models(tenantId);
const dynamicFilter = await this.dynamicListService.dynamicList( // Parses vendors list filter DTO.
const filter = this.parseVendorsListFilterDTO(filterDTO);
// Dynamic list service.
const dynamicList = await this.dynamicListService.dynamicList(
tenantId, tenantId,
Vendor, Vendor,
vendorsFilter filter
); );
// Vendors list.
const { results, pagination } = await Vendor.query() const { results, pagination } = await Vendor.query()
.onBuild((builder) => { .onBuild((builder) => {
dynamicFilter.buildQuery()(builder); dynamicList.buildQuery()(builder);
builder.modify('inactiveMode', filter.inactiveMode);
}) })
.pagination(vendorsFilter.page - 1, vendorsFilter.pageSize); .pagination(filter.page - 1, filter.pageSize);
return { return {
vendors: results, vendors: results,
pagination, pagination,
filterMeta: dynamicFilter.getResponseMeta(), filterMeta: dynamicList.getResponseMeta(),
}; };
} }

View File

@@ -0,0 +1,6 @@
export default class DynamicListAbstruct {
}

View File

@@ -0,0 +1,52 @@
import { Inject, Service } from 'typedi';
import DynamicListAbstruct from './DynamicListAbstruct';
import DynamicFilterViews from 'lib/DynamicFilter/DynamicFilterViews';
import { ServiceError } from 'exceptions';
import HasTenancyService from 'services/Tenancy/TenancyService';
import {ERRORS } from './constants';
import { IModel }from 'interfaces';
@Service()
export default class DynamicListCustomView extends DynamicListAbstruct {
@Inject()
tenancy: HasTenancyService;
/**
* Retreive custom view or throws error not found.
* @param {number} tenantId
* @param {number} viewId
* @return {Promise<IView>}
*/
private getCustomViewOrThrowError = async (
tenantId: number,
viewId: number,
model: IModel
) => {
const { viewRepository } = this.tenancy.repositories(tenantId);
const view = await viewRepository.findOneById(viewId, 'roles');
if (!view || view.resourceModel !== model.name) {
throw new ServiceError(ERRORS.VIEW_NOT_FOUND);
}
return view;
};
/**
* Dynamic list custom view.
* @param {IModel} model
* @param {number} customViewId
* @returns
*/
public dynamicListCustomView = async (
tenantId: number,
model,
customViewId: number
) => {
const view = await this.getCustomViewOrThrowError(
tenantId,
customViewId,
model
);
return new DynamicFilterViews(view);
};
}

View File

@@ -0,0 +1,103 @@
import { Service } from 'typedi';
import * as R from 'ramda';
import validator from 'is-my-json-valid';
import { IFilterRole, IModel } from 'interfaces';
import DynamicListAbstruct from './DynamicListAbstruct';
import DynamicFilterFilterRoles from 'lib/DynamicFilter/DynamicFilterFilterRoles';
import { ERRORS } from './constants';
import { ServiceError } from 'exceptions';
@Service()
export default class DynamicListFilterRoles extends DynamicListAbstruct {
/**
* Validates filter roles schema.
* @param {IFilterRole[]} filterRoles - Filter roles.
*/
private validateFilterRolesSchema = (filterRoles: IFilterRole[]) => {
const validate = validator({
required: true,
type: 'object',
properties: {
condition: { type: 'string' },
fieldKey: { required: true, type: 'string' },
value: { required: true },
},
});
const invalidFields = filterRoles.filter((filterRole) => {
return !validate(filterRole);
});
if (invalidFields.length > 0) {
throw new ServiceError(ERRORS.STRINGIFIED_FILTER_ROLES_INVALID);
}
};
/**
* Retrieve filter roles fields key that not exists on the given model.
* @param {IModel} model
* @param {IFilterRole} filterRoles
* @returns {string[]}
*/
private getFilterRolesFieldsNotExist = (
model,
filterRoles: IFilterRole[]
): string[] => {
return filterRoles
.filter((filterRole) => !model.getField(filterRole.fieldKey))
.map((filterRole) => filterRole.fieldKey);
};
/**
* Validates existance the fields of filter roles.
* @param {IModel} model
* @param {IFilterRole[]} filterRoles
* @throws {ServiceError}
*/
private validateFilterRolesFieldsExistance = (
model: IModel,
filterRoles: IFilterRole[]
) => {
const invalidFieldsKeys = this.getFilterRolesFieldsNotExist(
model,
filterRoles
);
if (invalidFieldsKeys.length > 0) {
throw new ServiceError(ERRORS.FILTER_ROLES_FIELDS_NOT_FOUND);
}
};
/**
* Associate index to filter roles.
* @param {IFilterRole[]} filterRoles
* @returns {IFilterRole[]}
*/
private incrementFilterRolesIndex = (
filterRoles: IFilterRole[]
): IFilterRole[] => {
return filterRoles.map((filterRole, index) => ({
...filterRole,
index: index + 1,
}));
};
/**
* Dynamic list filter roles.
* @param {IModel} model
* @param {IFilterRole[]} filterRoles
* @returns {DynamicFilterFilterRoles}
*/
public dynamicList = (
model: IModel,
filterRoles: IFilterRole[]
): DynamicFilterFilterRoles => {
const filterRolesParsed = R.compose(this.incrementFilterRolesIndex)(
filterRoles
);
// Validate filter roles json schema.
this.validateFilterRolesSchema(filterRolesParsed);
// Validate the model resource fields.
this.validateFilterRolesFieldsExistance(model, filterRoles);
return new DynamicFilterFilterRoles(filterRolesParsed);
};
}

View File

@@ -1,161 +1,88 @@
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import validator from 'is-my-json-valid';
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
import { castArray, isEmpty } from 'lodash';
import { ServiceError } from 'exceptions'; import { ServiceError } from 'exceptions';
import { DynamicFilter } from 'lib/DynamicFilter';
import { import {
DynamicFilter, IDynamicListFilter,
DynamicFilterSortBy,
DynamicFilterViews,
DynamicFilterFilterRoles,
} from 'lib/DynamicFilter';
import {
validateFieldKeyExistance,
validateFilterRolesFieldsExistance,
} from 'lib/ViewRolesBuilder';
import {
IDynamicListFilterDTO,
IFilterRole,
IDynamicListService, IDynamicListService,
IFilterRole,
IModel, IModel,
} from 'interfaces'; } from 'interfaces';
import TenancyService from 'services/Tenancy/TenancyService'; import TenancyService from 'services/Tenancy/TenancyService';
import DynamicListFilterRoles from './DynamicListFilterRoles';
const ERRORS = { import DynamicListSortBy from './DynamicListSortBy';
VIEW_NOT_FOUND: 'view_not_found', import DynamicListCustomView from './DynamicListCustomView';
SORT_COLUMN_NOT_FOUND: 'sort_column_not_found',
FILTER_ROLES_FIELDS_NOT_FOUND: 'filter_roles_fields_not_found',
};
@Service() @Service()
export default class DynamicListService implements IDynamicListService { export default class DynamicListService implements IDynamicListService {
@Inject() @Inject()
tenancy: TenancyService; tenancy: TenancyService;
/** @Inject()
* Retreive custom view or throws error not found. dynamicListFilterRoles: DynamicListFilterRoles;
* @param {number} tenantId
* @param {number} viewId
* @return {Promise<IView>}
*/
private async getCustomViewOrThrowError(
tenantId: number,
viewId: number,
model: IModel
) {
const { viewRepository } = this.tenancy.repositories(tenantId);
const view = await viewRepository.findOneById(viewId, 'roles');
if (!view || view.resourceModel !== model.name) { @Inject()
throw new ServiceError(ERRORS.VIEW_NOT_FOUND); dynamicListSortBy: DynamicListSortBy;
}
return view; @Inject()
} dynamicListView: DynamicListCustomView;
/** /**
* Validates the sort column whether exists. * Parses filter DTO.
* @param {IModel} model
* @param {string} columnSortBy - Sort column
* @throws {ServiceError}
*/ */
private validateSortColumnExistance(model: any, columnSortBy: string) { private parseFilterObject = (model, filterDTO) => {
const notExistsField = validateFieldKeyExistance(model, columnSortBy); return {
// Merges the default properties with filter object.
if (!notExistsField) { ...model.defaultSort ? {
throw new ServiceError(ERRORS.SORT_COLUMN_NOT_FOUND); sortOrder: model.defaultSort.sortOrder,
} columnSortBy: model.defaultSort.sortOrder,
} } : {},
...filterDTO,
/** };
* Validates existance the fields of filter roles. };
* @param {IModel} model
* @param {IFilterRole[]} filterRoles
* @throws {ServiceError}
*/
private validateRolesFieldsExistance(
model: IModel,
filterRoles: IFilterRole[]
) {
const invalidFieldsKeys = validateFilterRolesFieldsExistance(
model,
filterRoles
);
if (invalidFieldsKeys.length > 0) {
throw new ServiceError(ERRORS.FILTER_ROLES_FIELDS_NOT_FOUND);
}
}
/**
* Validates filter roles schema.
* @param {IFilterRole[]} filterRoles
*/
private validateFilterRolesSchema(filterRoles: IFilterRole[]) {
const validate = validator({
required: true,
type: 'object',
properties: {
condition: { type: 'string' },
fieldKey: { required: true, type: 'string' },
value: { required: true },
},
});
const invalidFields = filterRoles.filter((filterRole) => {
const isValid = validate(filterRole);
return isValid ? false : true;
});
if (invalidFields.length > 0) {
throw new ServiceError('stringified_filter_roles_invalid');
}
}
/** /**
* Dynamic listing. * Dynamic listing.
* @param {number} tenantId - Tenant id. * @param {number} tenantId - Tenant id.
* @param {IModel} model - Model. * @param {IModel} model - Model.
* @param {IDynamicListFilterDTO} filter - Dynamic filter DTO. * @param {IDynamicListFilter} filter - Dynamic filter DTO.
*/ */
public async dynamicList( public dynamicList = async (
tenantId: number, tenantId: number,
model: IModel, model: IModel,
filter: IDynamicListFilterDTO filter: IDynamicListFilter
) { ) => {
const dynamicFilter = new DynamicFilter(model); const dynamicFilter = new DynamicFilter(model);
// Custom view filter roles. // Parses the filter object.
if (filter.customViewId) { const parsedFilter = this.parseFilterObject(model, filter);
const view = await this.getCustomViewOrThrowError(
tenantId,
filter.customViewId,
model
);
const viewFilter = new DynamicFilterViews(view);
dynamicFilter.setFilter(viewFilter);
}
// Sort by the given column.
if (filter.columnSortBy) {
this.validateSortColumnExistance(model, filter.columnSortBy);
const sortByFilter = new DynamicFilterSortBy( // Custom view filter roles.
filter.columnSortBy, // if (filter.customViewId) {
filter.sortOrder // const dynamicListCustomView = this.dynamicListView.dynamicListCustomView();
// dynamicFilter.setFilter(dynamicListCustomView);
// }
// Sort by the given column.
if (parsedFilter.columnSortBy) {
const dynmaicListSortBy = this.dynamicListSortBy.dynamicSortBy(
model,
parsedFilter.columnSortBy,
parsedFilter.sortOrder,
); );
dynamicFilter.setFilter(sortByFilter); dynamicFilter.setFilter(dynmaicListSortBy);
} }
// Filter roles. // Filter roles.
if (filter.filterRoles.length > 0) { if (!isEmpty(parsedFilter.filterRoles)) {
const filterRoles = filter.filterRoles.map((filterRole, index) => ({ const dynamicFilterRoles = this.dynamicListFilterRoles.dynamicList(
...filterRole, model,
index: index + 1, parsedFilter.filterRoles
})); );
this.validateFilterRolesSchema(filterRoles);
this.validateRolesFieldsExistance(model, filterRoles);
// Validate the model resource fields.
const dynamicFilterRoles = new DynamicFilterFilterRoles(filterRoles);
dynamicFilter.setFilter(dynamicFilterRoles); dynamicFilter.setFilter(dynamicFilterRoles);
} }
return dynamicFilter; return dynamicFilter;
} };
/** /**
* Middleware to catch services errors * Middleware to catch services errors
@@ -173,25 +100,62 @@ export default class DynamicListService implements IDynamicListService {
if (error instanceof ServiceError) { if (error instanceof ServiceError) {
if (error.errorType === 'sort_column_not_found') { if (error.errorType === 'sort_column_not_found') {
return res.boom.badRequest(null, { return res.boom.badRequest(null, {
errors: [{ type: 'SORT.COLUMN.NOT.FOUND', code: 200 }], errors: [
{
type: 'SORT.COLUMN.NOT.FOUND',
message: 'Sort column not found.',
code: 200,
},
],
}); });
} }
if (error.errorType === 'view_not_found') { if (error.errorType === 'view_not_found') {
return res.boom.badRequest(null, { return res.boom.badRequest(null, {
errors: [{ type: 'CUSTOM.VIEW.NOT.FOUND', code: 100 }], errors: [
{
type: 'CUSTOM.VIEW.NOT.FOUND',
message: 'Custom view not found.',
code: 100,
},
],
}); });
} }
if (error.errorType === 'filter_roles_fields_not_found') { if (error.errorType === 'filter_roles_fields_not_found') {
return res.boom.badRequest(null, { return res.boom.badRequest(null, {
errors: [{ type: 'FILTER.ROLES.FIELDS.NOT.FOUND', code: 300 }], errors: [
{
type: 'FILTER.ROLES.FIELDS.NOT.FOUND',
message: 'Filter roles fields not found.',
code: 300,
},
],
}); });
} }
if (error.errorType === 'stringified_filter_roles_invalid') { if (error.errorType === 'stringified_filter_roles_invalid') {
return res.boom.badRequest(null, { return res.boom.badRequest(null, {
errors: [{ type: 'STRINGIFIED_FILTER_ROLES_INVALID', code: 400 }], errors: [
{
type: 'STRINGIFIED_FILTER_ROLES_INVALID',
message: 'Stringified filter roles json invalid.',
code: 400,
},
],
}); });
} }
} }
next(error); next(error);
} }
/**
* Parses stringified filter roles.
* @param {string} stringifiedFilterRoles - Stringified filter roles.
*/
public parseStringifiedFilter = (filterRoles: IDynamicListFilter) => {
return {
...filterRoles,
filterRoles: filterRoles.stringifiedFilterRoles
? castArray(JSON.parse(filterRoles.stringifiedFilterRoles))
: [],
};
};
} }

View File

@@ -0,0 +1,40 @@
import { Service } from 'typedi';
import DynamicListAbstruct from './DynamicListAbstruct';
import DynamicFilterSortBy from 'lib/DynamicFilter/DynamicFilterSortBy';
import { IModel, ISortOrder } from 'interfaces';
import { ServiceError } from 'exceptions';
import { ERRORS } from './constants';
@Service()
export default class DynamicListSortBy extends DynamicListAbstruct {
/**
* Dynamic list sort by.
* @param {IModel} model
* @param {string} columnSortBy
* @param {ISortOrder} sortOrder
* @returns {DynamicFilterSortBy}
*/
public dynamicSortBy(
model: IModel,
columnSortBy: string,
sortOrder: ISortOrder
) {
this.validateSortColumnExistance(model, columnSortBy);
return new DynamicFilterSortBy(columnSortBy, sortOrder);
}
/**
* Validates the sort column whether exists.
* @param {IModel} model - Model.
* @param {string} columnSortBy - Sort column
* @throws {ServiceError}
*/
private validateSortColumnExistance(model: any, columnSortBy: string) {
const field = model.getField(columnSortBy);
if (!field) {
throw new ServiceError(ERRORS.SORT_COLUMN_NOT_FOUND);
}
}
}

View File

@@ -0,0 +1,6 @@
export const ERRORS = {
STRINGIFIED_FILTER_ROLES_INVALID: 'stringified_filter_roles_invalid',
VIEW_NOT_FOUND: 'view_not_found',
SORT_COLUMN_NOT_FOUND: 'sort_column_not_found',
FILTER_ROLES_FIELDS_NOT_FOUND: 'filter_roles_fields_not_found',
};

View File

@@ -1,6 +1,7 @@
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import { difference, sumBy, omit, map } from 'lodash'; import { difference, sumBy, omit } from 'lodash';
import moment from 'moment'; import moment from 'moment';
import * as R from 'ramda';
import { import {
EventDispatcher, EventDispatcher,
EventDispatcherInterface, EventDispatcherInterface,
@@ -630,6 +631,16 @@ export default class ExpensesService implements IExpensesService {
return expenses.filter((expense) => expense.publishedAt); return expenses.filter((expense) => expense.publishedAt);
} }
/**
* Parses filter DTO of expenses list.
* @param filterDTO -
*/
private parseListFilterDTO(filterDTO) {
return R.compose(
this.dynamicListService.parseStringifiedFilter,
)(filterDTO);
}
/** /**
* Retrieve expenses datatable lsit. * Retrieve expenses datatable lsit.
* @param {number} tenantId * @param {number} tenantId
@@ -638,35 +649,41 @@ export default class ExpensesService implements IExpensesService {
*/ */
public async getExpensesList( public async getExpensesList(
tenantId: number, tenantId: number,
expensesFilter: IExpensesFilter filterDTO: IExpensesFilter
): Promise<{ ): Promise<{
expenses: IExpense[]; expenses: IExpense[];
pagination: IPaginationMeta; pagination: IPaginationMeta;
filterMeta: IFilterMeta; filterMeta: IFilterMeta;
}> { }> {
const { Expense } = this.tenancy.models(tenantId); const { Expense } = this.tenancy.models(tenantId);
const dynamicFilter = await this.dynamicListService.dynamicList(
// Parses list filter DTO.
const filter = this.parseListFilterDTO(filterDTO);
// Dynamic list service.
const dynamicList = await this.dynamicListService.dynamicList(
tenantId, tenantId,
Expense, Expense,
expensesFilter filter,
); );
this.logger.info('[expense] trying to get expenses datatable list.', { this.logger.info('[expense] trying to get expenses datatable list.', {
tenantId, tenantId,
expensesFilter, filter,
}); });
const { results, pagination } = await Expense.query() const { results, pagination } = await Expense.query()
.onBuild((builder) => { .onBuild((builder) => {
builder.withGraphFetched('paymentAccount'); builder.withGraphFetched('paymentAccount');
builder.withGraphFetched('categories.expenseAccount'); builder.withGraphFetched('categories.expenseAccount');
dynamicFilter.buildQuery()(builder);
dynamicList.buildQuery()(builder);
}) })
.pagination(expensesFilter.page - 1, expensesFilter.pageSize); .pagination(filter.page - 1, filter.pageSize);
return { return {
expenses: results, expenses: results,
pagination, pagination,
filterMeta: dynamicFilter.getResponseMeta(), filterMeta: dynamicList.getResponseMeta(),
}; };
} }

View File

@@ -29,7 +29,6 @@ export default class JournalSheetService {
fromRange: null, fromRange: null,
toRange: null, toRange: null,
accountsIds: [], accountsIds: [],
transactionTypes: [],
numberFormat: { numberFormat: {
noCents: false, noCents: false,
divideOn1000: false, divideOn1000: false,
@@ -107,6 +106,13 @@ export default class JournalSheetService {
} }
query.modify('filterDateRange', filter.fromDate, filter.toDate); query.modify('filterDateRange', filter.fromDate, filter.toDate);
query.orderBy(['date', 'createdAt', 'indexGroup', 'index']); query.orderBy(['date', 'createdAt', 'indexGroup', 'index']);
if (filter.transactionType) {
return query.where('reference_type', filter.transactionType);
}
if (filter.transactionType && filter.transactionId) {
return query.where('reference_id', filter.transactionId);
}
}); });
// Transform the transactions array to journal collection. // Transform the transactions array to journal collection.
const transactionsJournal = Journal.fromTransactions( const transactionsJournal = Journal.fromTransactions(

View File

@@ -1,6 +1,7 @@
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import { omit } from 'lodash'; import { omit } from 'lodash';
import moment from 'moment'; import moment from 'moment';
import * as R from 'ramda';
import { import {
EventDispatcher, EventDispatcher,
EventDispatcherInterface, EventDispatcherInterface,
@@ -225,8 +226,8 @@ export default class InventoryAdjustmentService {
/** /**
* Publish the inventory adjustment transaction. * Publish the inventory adjustment transaction.
* @param tenantId * @param {number} tenantId
* @param inventoryAdjustmentId * @param {number} inventoryAdjustmentId
*/ */
async publishInventoryAdjustment( async publishInventoryAdjustment(
tenantId: number, tenantId: number,
@@ -265,6 +266,17 @@ export default class InventoryAdjustmentService {
); );
} }
/**
* Parses inventory adjustments list filter DTO.
* @param filterDTO
* @returns
*/
private parseListFilterDTO(filterDTO) {
return R.compose(
this.dynamicListService.parseStringifiedFilter,
)(filterDTO);
}
/** /**
* Retrieve the inventory adjustments paginated list. * Retrieve the inventory adjustments paginated list.
* @param {number} tenantId * @param {number} tenantId
@@ -272,17 +284,21 @@ export default class InventoryAdjustmentService {
*/ */
async getInventoryAdjustments( async getInventoryAdjustments(
tenantId: number, tenantId: number,
adjustmentsFilter: IInventoryAdjustmentsFilter filterDTO: IInventoryAdjustmentsFilter
): Promise<{ ): Promise<{
inventoryAdjustments: IInventoryAdjustment[]; inventoryAdjustments: IInventoryAdjustment[];
pagination: IPaginationMeta; pagination: IPaginationMeta;
}> { }> {
const { InventoryAdjustment } = this.tenancy.models(tenantId); const { InventoryAdjustment } = this.tenancy.models(tenantId);
// Parses inventory adjustments list filter DTO.
const filter = this.parseListFilterDTO(filterDTO);
// Dynamic list service.
const dynamicFilter = await this.dynamicListService.dynamicList( const dynamicFilter = await this.dynamicListService.dynamicList(
tenantId, tenantId,
InventoryAdjustment, InventoryAdjustment,
adjustmentsFilter filter,
); );
const { results, pagination } = await InventoryAdjustment.query() const { results, pagination } = await InventoryAdjustment.query()
.onBuild((query) => { .onBuild((query) => {
@@ -291,7 +307,7 @@ export default class InventoryAdjustmentService {
dynamicFilter.buildQuery()(query); dynamicFilter.buildQuery()(query);
}) })
.pagination(adjustmentsFilter.page - 1, adjustmentsFilter.pageSize); .pagination(filter.page - 1, filter.pageSize);
return { return {
inventoryAdjustments: results, inventoryAdjustments: results,

View File

@@ -1,5 +1,6 @@
import { Inject } from 'typedi'; import { Inject } from 'typedi';
import { difference } from 'lodash'; import { difference } from 'lodash';
import * as R from 'ramda';
import { import {
EventDispatcher, EventDispatcher,
EventDispatcherInterface, EventDispatcherInterface,
@@ -377,6 +378,18 @@ export default class ItemCategoriesService implements IItemCategoriesService {
} }
} }
/**
* Parses items categories filter DTO.
* @param {} filterDTO
* @returns
*/
private parsesListFilterDTO(filterDTO) {
return R.compose(
// Parses stringified filter roles.
this.dynamicListService.parseStringifiedFilter,
)(filterDTO);
}
/** /**
* Retrieve item categories list. * Retrieve item categories list.
* @param {number} tenantId * @param {number} tenantId
@@ -384,10 +397,15 @@ export default class ItemCategoriesService implements IItemCategoriesService {
*/ */
public async getItemCategoriesList( public async getItemCategoriesList(
tenantId: number, tenantId: number,
filter: IItemCategoriesFilter, filterDTO: IItemCategoriesFilter,
authorizedUser: ISystemUser authorizedUser: ISystemUser
): Promise<{ itemCategories: IItemCategory[]; filterMeta: IFilterMeta }> { ): Promise<{ itemCategories: IItemCategory[]; filterMeta: IFilterMeta }> {
const { ItemCategory } = this.tenancy.models(tenantId); const { ItemCategory } = this.tenancy.models(tenantId);
// Parses list filter DTO.
const filter = this.parsesListFilterDTO(filterDTO);
// Dynamic list service.
const dynamicList = await this.dynamicListService.dynamicList( const dynamicList = await this.dynamicListService.dynamicList(
tenantId, tenantId,
ItemCategory, ItemCategory,

View File

@@ -1,5 +1,6 @@
import { defaultTo, difference } from 'lodash'; import { defaultTo } from 'lodash';
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import * as R from 'ramda';
import { import {
EventDispatcher, EventDispatcher,
EventDispatcherInterface, EventDispatcherInterface,
@@ -525,20 +526,37 @@ export default class ItemsService implements IItemsService {
return this.transformItemToResponse(tenantId, item); return this.transformItemToResponse(tenantId, item);
} }
/**
* Parses items list filter DTO.
* @param {} filterDTO - Filter DTO.
*/
private parseItemsListFilterDTO(filterDTO) {
return R.compose(
this.dynamicListService.parseStringifiedFilter,
)(filterDTO);
}
/** /**
* Retrieve items datatable list. * Retrieve items datatable list.
* @param {number} tenantId * @param {number} tenantId
* @param {IItemsFilter} itemsFilter * @param {IItemsFilter} itemsFilter
*/ */
public async itemsList(tenantId: number, itemsFilter: IItemsFilter) { public async itemsList(tenantId: number, filterDTO: IItemsFilter) {
const { Item } = this.tenancy.models(tenantId); const { Item } = this.tenancy.models(tenantId);
// Parses items list filter DTO.
const filter = this.parseItemsListFilterDTO(filterDTO);
// Dynamic list service.
const dynamicFilter = await this.dynamicListService.dynamicList( const dynamicFilter = await this.dynamicListService.dynamicList(
tenantId, tenantId,
Item, Item,
itemsFilter filter
); );
const { results: items, pagination } = await Item.query() const { results: items, pagination } = await Item.query()
.onBuild((builder) => { .onBuild((builder) => {
builder.modify('inactiveMode', filter.inactiveMode);
builder.withGraphFetched('inventoryAccount'); builder.withGraphFetched('inventoryAccount');
builder.withGraphFetched('sellAccount'); builder.withGraphFetched('sellAccount');
builder.withGraphFetched('costAccount'); builder.withGraphFetched('costAccount');
@@ -546,7 +564,7 @@ export default class ItemsService implements IItemsService {
dynamicFilter.buildQuery()(builder); dynamicFilter.buildQuery()(builder);
}) })
.pagination(itemsFilter.page - 1, itemsFilter.pageSize); .pagination(filter.page - 1, filter.pageSize);
const results = items.map((item) => const results = items.map((item) =>
this.transformItemToResponse(tenantId, item) this.transformItemToResponse(tenantId, item)

View File

@@ -1,7 +1,8 @@
import { difference, sumBy, omit, map } from 'lodash'; import { difference, sumBy, omit, map } from 'lodash';
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import moment from 'moment'; import moment from 'moment';
import { ServiceError, ServiceErrors } from 'exceptions'; import * as R from 'ramda';
import { ServiceError } from 'exceptions';
import { import {
IManualJournalDTO, IManualJournalDTO,
IManualJournalsService, IManualJournalsService,
@@ -768,33 +769,47 @@ export default class ManualJournalsService implements IManualJournalsService {
); );
} }
/**
* Parses filter DTO of the manual journals list.
* @param filterDTO
*/
private parseListFilterDTO(filterDTO) {
return R.compose(
this.dynamicListService.parseStringifiedFilter
)(filterDTO);
}
/** /**
* Retrieve manual journals datatable list. * Retrieve manual journals datatable list.
* @param {number} tenantId * @param {number} tenantId -
* @param {IManualJournalsFilter} filter * @param {IManualJournalsFilter} filter -
*/ */
public async getManualJournals( public async getManualJournals(
tenantId: number, tenantId: number,
filter: IManualJournalsFilter filterDTO: IManualJournalsFilter
): Promise<{ ): Promise<{
manualJournals: IManualJournal; manualJournals: IManualJournal;
pagination: IPaginationMeta; pagination: IPaginationMeta;
filterMeta: IFilterMeta; filterMeta: IFilterMeta;
}> { }> {
const { ManualJournal } = this.tenancy.models(tenantId); const { ManualJournal } = this.tenancy.models(tenantId);
const dynamicList = await this.dynamicListService.dynamicList(
// Parses filter DTO.
const filter = this.parseListFilterDTO(filterDTO);
// Dynamic service.
const dynamicService = await this.dynamicListService.dynamicList(
tenantId, tenantId,
ManualJournal, ManualJournal,
filter filter
); );
this.logger.info('[manual_journals] trying to get manual journals list.', { this.logger.info('[manual_journals] trying to get manual journals list.', {
tenantId, tenantId,
filter, filter,
}); });
const { results, pagination } = await ManualJournal.query() const { results, pagination } = await ManualJournal.query()
.onBuild((builder) => { .onBuild((builder) => {
dynamicList.buildQuery()(builder); dynamicService.buildQuery()(builder);
builder.withGraphFetched('entries.account'); builder.withGraphFetched('entries.account');
}) })
.pagination(filter.page - 1, filter.pageSize); .pagination(filter.page - 1, filter.pageSize);
@@ -802,7 +817,7 @@ export default class ManualJournalsService implements IManualJournalsService {
return { return {
manualJournals: results, manualJournals: results,
pagination, pagination,
filterMeta: dynamicList.getResponseMeta(), filterMeta: dynamicService.getResponseMeta(),
}; };
} }

View File

@@ -1,5 +1,6 @@
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import { omit, sumBy, difference } from 'lodash'; import { sumBy, difference } from 'lodash';
import * as R from 'ramda';
import { import {
EventDispatcher, EventDispatcher,
EventDispatcherInterface, EventDispatcherInterface,
@@ -25,7 +26,7 @@ import TenancyService from 'services/Tenancy/TenancyService';
import DynamicListingService from 'services/DynamicListing/DynamicListService'; import DynamicListingService from 'services/DynamicListing/DynamicListService';
import { entriesAmountDiff, formatDateFields } from 'utils'; import { entriesAmountDiff, formatDateFields } from 'utils';
import { ServiceError } from 'exceptions'; import { ServiceError } from 'exceptions';
import { ACCOUNT_PARENT_TYPE, ACCOUNT_TYPE } from 'data/AccountTypes'; import { ACCOUNT_TYPE } from 'data/AccountTypes';
import VendorsService from 'services/Contacts/VendorsService'; import VendorsService from 'services/Contacts/VendorsService';
import { ERRORS } from './constants'; import { ERRORS } from './constants';
@@ -635,6 +636,12 @@ export default class BillPaymentsService implements IBillPaymentsService {
]); ]);
} }
private parseListFilterDTO(filterDTO) {
return R.compose(
this.dynamicListService.parseStringifiedFilter
)(filterDTO);
}
/** /**
* Retrieve bill payment paginted and filterable list. * Retrieve bill payment paginted and filterable list.
* @param {number} tenantId * @param {number} tenantId
@@ -642,34 +649,40 @@ export default class BillPaymentsService implements IBillPaymentsService {
*/ */
public async listBillPayments( public async listBillPayments(
tenantId: number, tenantId: number,
billPaymentsFilter: IBillPaymentsFilter filterDTO: IBillPaymentsFilter
): Promise<{ ): Promise<{
billPayments: IBillPayment; billPayments: IBillPayment;
pagination: IPaginationMeta; pagination: IPaginationMeta;
filterMeta: IFilterMeta; filterMeta: IFilterMeta;
}> { }> {
const { BillPayment } = this.tenancy.models(tenantId); const { BillPayment } = this.tenancy.models(tenantId);
const dynamicFilter = await this.dynamicListService.dynamicList(
// Parses filter DTO.
const filter = this.parseListFilterDTO(filterDTO);
// Dynamic list service.
const dynamicList = await this.dynamicListService.dynamicList(
tenantId, tenantId,
BillPayment, BillPayment,
billPaymentsFilter filter
); );
this.logger.info('[bill_payment] try to get bill payments list.', { this.logger.info('[bill_payment] try to get bill payments list.', {
tenantId, tenantId,
}); });
const { results, pagination } = await BillPayment.query() const { results, pagination } = await BillPayment.query()
.onBuild((builder) => { .onBuild((builder) => {
builder.withGraphFetched('vendor'); builder.withGraphFetched('vendor');
builder.withGraphFetched('paymentAccount'); builder.withGraphFetched('paymentAccount');
dynamicFilter.buildQuery()(builder); dynamicList.buildQuery()(builder);
}) })
.pagination(billPaymentsFilter.page - 1, billPaymentsFilter.pageSize); .pagination(filter.page - 1, filter.pageSize);
return { return {
billPayments: results, billPayments: results,
pagination, pagination,
filterMeta: dynamicFilter.getResponseMeta(), filterMeta: dynamicList.getResponseMeta(),
}; };
} }

View File

@@ -1,6 +1,7 @@
import { omit, runInContext, sumBy } from 'lodash'; import { omit, runInContext, sumBy } from 'lodash';
import moment from 'moment'; import moment from 'moment';
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import * as R from 'ramda';
import composeAsync from 'async/compose'; import composeAsync from 'async/compose';
import { import {
EventDispatcher, EventDispatcher,
@@ -521,6 +522,16 @@ export default class BillsService
}); });
} }
/**
* Parses bills list filter DTO.
* @param filterDTO -
*/
private parseListFilterDTO(filterDTO) {
return R.compose(
this.dynamicListService.parseStringifiedFilter,
)(filterDTO);
}
/** /**
* Retrieve bills data table list. * Retrieve bills data table list.
* @param {number} tenantId - * @param {number} tenantId -
@@ -528,28 +539,33 @@ export default class BillsService
*/ */
public async getBills( public async getBills(
tenantId: number, tenantId: number,
billsFilter: IBillsFilter filterDTO: IBillsFilter
): Promise<{ ): Promise<{
bills: IBill; bills: IBill;
pagination: IPaginationMeta; pagination: IPaginationMeta;
filterMeta: IFilterMeta; filterMeta: IFilterMeta;
}> { }> {
const { Bill } = this.tenancy.models(tenantId); const { Bill } = this.tenancy.models(tenantId);
// Parses bills list filter DTO.
const filter = this.parseListFilterDTO(filterDTO);
// Dynamic list service.
const dynamicFilter = await this.dynamicListService.dynamicList( const dynamicFilter = await this.dynamicListService.dynamicList(
tenantId, tenantId,
Bill, Bill,
billsFilter filter,
); );
this.logger.info('[bills] trying to get bills data table.', { this.logger.info('[bills] trying to get bills data table.', {
tenantId, tenantId,
billsFilter, filter,
}); });
const { results, pagination } = await Bill.query() const { results, pagination } = await Bill.query()
.onBuild((builder) => { .onBuild((builder) => {
builder.withGraphFetched('vendor'); builder.withGraphFetched('vendor');
dynamicFilter.buildQuery()(builder); dynamicFilter.buildQuery()(builder);
}) })
.pagination(billsFilter.page - 1, billsFilter.pageSize); .pagination(filter.page - 1, filter.pageSize);
return { return {
bills: results, bills: results,

View File

@@ -2,7 +2,7 @@ import { Service, Inject } from 'typedi';
import { camelCase, upperFirst } from 'lodash'; import { camelCase, upperFirst } from 'lodash';
import pluralize from 'pluralize'; import pluralize from 'pluralize';
import { buildFilter } from 'objection-filter'; import { buildFilter } from 'objection-filter';
import { IModel } from 'interfaces'; import { IModel, IModelMeta } from 'interfaces';
import { import {
getModelFields, getModelFields,
} from 'lib/ViewRolesBuilder' } from 'lib/ViewRolesBuilder'
@@ -102,4 +102,18 @@ export default class ResourceService {
return buildFilter(resourceModel).build(filter); return buildFilter(resourceModel).build(filter);
} }
/**
* Retrieve the resource meta.
* @param {number} tenantId
* @param {string} modelName
* @returns {IModelMeta}
*/
public getResourceMeta(tenantId: number, modelName: string): IModelMeta {
const resourceModel = this.getResourceModel(tenantId, modelName);
const settings = resourceModel.meta();
return settings;
}
} }

View File

@@ -1,5 +1,6 @@
import { omit, sumBy, difference } from 'lodash'; import { omit, sumBy, difference } from 'lodash';
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import * as R from 'ramda';
import { import {
EventDispatcher, EventDispatcher,
EventDispatcherInterface, EventDispatcherInterface,
@@ -613,6 +614,16 @@ export default class PaymentReceiveService implements IPaymentsReceiveService {
return saleInvoices; return saleInvoices;
} }
/**
* Parses payments receive list filter DTO.
* @param filterDTO
*/
private parseListFilterDTO(filterDTO) {
return R.compose(
this.dynamicListService.parseStringifiedFilter
)(filterDTO);
}
/** /**
* Retrieve payment receives paginated and filterable list. * Retrieve payment receives paginated and filterable list.
* @param {number} tenantId * @param {number} tenantId
@@ -620,33 +631,39 @@ export default class PaymentReceiveService implements IPaymentsReceiveService {
*/ */
public async listPaymentReceives( public async listPaymentReceives(
tenantId: number, tenantId: number,
paymentReceivesFilter: IPaymentReceivesFilter filterDTO: IPaymentReceivesFilter
): Promise<{ ): Promise<{
paymentReceives: IPaymentReceive[]; paymentReceives: IPaymentReceive[];
pagination: IPaginationMeta; pagination: IPaginationMeta;
filterMeta: IFilterMeta; filterMeta: IFilterMeta;
}> { }> {
const { PaymentReceive } = this.tenancy.models(tenantId); const { PaymentReceive } = this.tenancy.models(tenantId);
const dynamicFilter = await this.dynamicListService.dynamicList(
// Parses filter DTO.
const filter = this.parseListFilterDTO(filterDTO);
// Dynamic list service.
const dynamicList = await this.dynamicListService.dynamicList(
tenantId, tenantId,
PaymentReceive, PaymentReceive,
paymentReceivesFilter filter
); );
const { results, pagination } = await PaymentReceive.query() const { results, pagination } = await PaymentReceive.query()
.onBuild((builder) => { .onBuild((builder) => {
builder.withGraphFetched('customer'); builder.withGraphFetched('customer');
builder.withGraphFetched('depositAccount'); builder.withGraphFetched('depositAccount');
dynamicFilter.buildQuery()(builder); dynamicList.buildQuery()(builder);
}) })
.pagination( .pagination(
paymentReceivesFilter.page - 1, filter.page - 1,
paymentReceivesFilter.pageSize filter.pageSize
); );
return { return {
paymentReceives: results, paymentReceives: results,
pagination, pagination,
filterMeta: dynamicFilter.getResponseMeta(), filterMeta: dynamicList.getResponseMeta(),
}; };
} }

View File

@@ -1,5 +1,6 @@
import { omit, sumBy } from 'lodash'; import { filter, omit, sumBy } from 'lodash';
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import * as R from 'ramda';
import { import {
IEstimatesFilter, IEstimatesFilter,
IFilterMeta, IFilterMeta,
@@ -412,6 +413,16 @@ export default class SaleEstimateService implements ISalesEstimatesService{
return estimate; return estimate;
} }
/**
* Parses estimates list filter DTO.
* @param filterDTO
*/
private parseListFilterDTO(filterDTO) {
return R.compose(
this.dynamicListService.parseStringifiedFilter
)(filterDTO);
}
/** /**
* Retrieves estimates filterable and paginated list. * Retrieves estimates filterable and paginated list.
* @param {number} tenantId - * @param {number} tenantId -
@@ -419,17 +430,22 @@ export default class SaleEstimateService implements ISalesEstimatesService{
*/ */
public async estimatesList( public async estimatesList(
tenantId: number, tenantId: number,
estimatesFilter: IEstimatesFilter filterDTO: IEstimatesFilter
): Promise<{ ): Promise<{
salesEstimates: ISaleEstimate[]; salesEstimates: ISaleEstimate[];
pagination: IPaginationMeta; pagination: IPaginationMeta;
filterMeta: IFilterMeta; filterMeta: IFilterMeta;
}> { }> {
const { SaleEstimate } = this.tenancy.models(tenantId); const { SaleEstimate } = this.tenancy.models(tenantId);
// Parses filter DTO.
const filter = this.parseListFilterDTO(filterDTO);
// Dynamic list service.
const dynamicFilter = await this.dynamicListService.dynamicList( const dynamicFilter = await this.dynamicListService.dynamicList(
tenantId, tenantId,
SaleEstimate, SaleEstimate,
estimatesFilter filter,
); );
const { results, pagination } = await SaleEstimate.query() const { results, pagination } = await SaleEstimate.query()
@@ -438,7 +454,7 @@ export default class SaleEstimateService implements ISalesEstimatesService{
builder.withGraphFetched('entries'); builder.withGraphFetched('entries');
dynamicFilter.buildQuery()(builder); dynamicFilter.buildQuery()(builder);
}) })
.pagination(estimatesFilter.page - 1, estimatesFilter.pageSize); .pagination(filter.page - 1, filter.pageSize);
return { return {
salesEstimates: results, salesEstimates: results,

View File

@@ -1,5 +1,6 @@
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import { omit, sumBy, join, entries } from 'lodash'; import { omit, sumBy } from 'lodash';
import * as R from 'ramda';
import moment from 'moment'; import moment from 'moment';
import composeAsync from 'async/compose'; import composeAsync from 'async/compose';
import { import {
@@ -647,6 +648,17 @@ export default class SaleInvoicesService implements ISalesInvoicesService {
return saleInvoice; return saleInvoice;
} }
/**
* Parses the sale invoice list filter DTO.
* @param filterDTO
* @returns
*/
private parseListFilterDTO(filterDTO) {
return R.compose(
this.dynamicListService.parseStringifiedFilter,
)(filterDTO);
}
/** /**
* Retrieve sales invoices filterable and paginated list. * Retrieve sales invoices filterable and paginated list.
* @param {Request} req * @param {Request} req
@@ -655,22 +667,27 @@ export default class SaleInvoicesService implements ISalesInvoicesService {
*/ */
public async salesInvoicesList( public async salesInvoicesList(
tenantId: number, tenantId: number,
salesInvoicesFilter: ISalesInvoicesFilter filterDTO: ISalesInvoicesFilter
): Promise<{ ): Promise<{
salesInvoices: ISaleInvoice[]; salesInvoices: ISaleInvoice[];
pagination: IPaginationMeta; pagination: IPaginationMeta;
filterMeta: IFilterMeta; filterMeta: IFilterMeta;
}> { }> {
const { SaleInvoice } = this.tenancy.models(tenantId); const { SaleInvoice } = this.tenancy.models(tenantId);
// Parses stringified filter roles.
const filter = this.parseListFilterDTO(filterDTO);
// Dynamic list service.
const dynamicFilter = await this.dynamicListService.dynamicList( const dynamicFilter = await this.dynamicListService.dynamicList(
tenantId, tenantId,
SaleInvoice, SaleInvoice,
salesInvoicesFilter filter
); );
this.logger.info('[sale_invoice] try to get sales invoices list.', { this.logger.info('[sale_invoice] try to get sales invoices list.', {
tenantId, tenantId,
salesInvoicesFilter, filter,
}); });
const { results, pagination } = await SaleInvoice.query() const { results, pagination } = await SaleInvoice.query()
.onBuild((builder) => { .onBuild((builder) => {
@@ -678,7 +695,7 @@ export default class SaleInvoicesService implements ISalesInvoicesService {
builder.withGraphFetched('customer'); builder.withGraphFetched('customer');
dynamicFilter.buildQuery()(builder); dynamicFilter.buildQuery()(builder);
}) })
.pagination(salesInvoicesFilter.page - 1, salesInvoicesFilter.pageSize); .pagination(filter.page - 1, filter.pageSize);
return { return {
salesInvoices: results, salesInvoices: results,

View File

@@ -1,6 +1,7 @@
import { omit, sumBy } from 'lodash'; import { omit, sumBy } from 'lodash';
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import moment from 'moment'; import moment from 'moment';
import * as R from 'ramda';
import { import {
EventDispatcher, EventDispatcher,
EventDispatcherInterface, EventDispatcherInterface,
@@ -406,6 +407,16 @@ export default class SalesReceiptService implements ISalesReceiptsService {
return saleReceipt; return saleReceipt;
} }
/**
* Parses the sale receipts list filter DTO.
* @param filterDTO
*/
private parseListFilterDTO(filterDTO) {
return R.compose(
this.dynamicListService.parseStringifiedFilter,
)(filterDTO);
}
/** /**
* Retrieve sales receipts paginated and filterable list. * Retrieve sales receipts paginated and filterable list.
* @param {number} tenantId * @param {number} tenantId
@@ -413,17 +424,22 @@ export default class SalesReceiptService implements ISalesReceiptsService {
*/ */
public async salesReceiptsList( public async salesReceiptsList(
tenantId: number, tenantId: number,
salesReceiptsFilter: ISaleReceiptFilter filterDTO: ISaleReceiptFilter
): Promise<{ ): Promise<{
salesReceipts: ISaleReceipt[]; salesReceipts: ISaleReceipt[];
pagination: IPaginationMeta; pagination: IPaginationMeta;
filterMeta: IFilterMeta; filterMeta: IFilterMeta;
}> { }> {
const { SaleReceipt } = this.tenancy.models(tenantId); const { SaleReceipt } = this.tenancy.models(tenantId);
// Parses the stringified filter roles.
const filter = this.parseListFilterDTO(filterDTO);
// Dynamic list service.
const dynamicFilter = await this.dynamicListService.dynamicList( const dynamicFilter = await this.dynamicListService.dynamicList(
tenantId, tenantId,
SaleReceipt, SaleReceipt,
salesReceiptsFilter filter,
); );
this.logger.info('[sale_receipt] try to get sales receipts list.', { this.logger.info('[sale_receipt] try to get sales receipts list.', {
@@ -437,7 +453,7 @@ export default class SalesReceiptService implements ISalesReceiptsService {
dynamicFilter.buildQuery()(builder); dynamicFilter.buildQuery()(builder);
}) })
.pagination(salesReceiptsFilter.page - 1, salesReceiptsFilter.pageSize); .pagination(filter.page - 1, filter.pageSize);
return { return {
salesReceipts: results, salesReceipts: results,