Merge branch 'master' of https://github.com/abouolia/Bigcapital into feature/viewDetail

This commit is contained in:
elforjani3
2021-08-01 13:37:04 +02:00
88 changed files with 3360 additions and 1854 deletions

View File

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

View File

@@ -100,6 +100,8 @@ export default class VendorsController extends ContactsController {
query('page').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) {
const { tenantId } = req;
const vendorsFilter: IVendorsFilter = {
filterRoles: [],
inactiveMode: false,
sortOrder: 'asc',
columnSortBy: 'created_at',
page: 1,
pageSize: 12,
...this.matchedQueryData(req),
};

View File

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

View File

@@ -17,6 +17,7 @@ import TransactionsByCustomers from './FinancialStatements/TransactionsByCustome
import TransactionsByVendors from './FinancialStatements/TransactionsByVendors';
import CashFlowStatementController from './FinancialStatements/CashFlow/CashFlow';
import InventoryDetailsController from './FinancialStatements/InventoryDetails';
import TransactionsByReferenceController from './FinancialStatements/TransactionsByReference';
@Service()
export default class FinancialStatementsService {
@@ -87,6 +88,10 @@ export default class FinancialStatementsService {
'/inventory-item-details',
Container.get(InventoryDetailsController).router(),
);
router.use(
'/transactions-by-reference',
Container.get(TransactionsByReferenceController).router(),
)
return router;
}
}

View File

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

View File

@@ -0,0 +1,87 @@
import { Inject, Service } from 'typedi';
import { Router, Request, Response, NextFunction } from 'express';
import { query, ValidationChain } from 'express-validator';
import BaseController from 'api/controllers/BaseController';
import TransactionsByReferenceService from 'services/FinancialStatements/TransactionsByReference';
@Service()
export default class TransactionsByReferenceController extends BaseController {
@Inject()
private transactionsByReferenceService: TransactionsByReferenceService;
/**
* Router constructor.
*/
router() {
const router = Router();
router.get(
'/',
this.validationSchema,
this.validationResult,
this.asyncMiddleware(this.transactionsByReference.bind(this))
);
return router;
}
/**
* Validation schema.
*/
get validationSchema(): ValidationChain[] {
return [
query('reference_id').exists().isInt(),
query('reference_type').exists().isString(),
query('number_format.precision')
.optional()
.isInt({ min: 0, max: 5 })
.toInt(),
query('number_format.divide_on_1000').optional().isBoolean().toBoolean(),
query('number_format.negative_format')
.optional()
.isIn(['parentheses', 'mines'])
.trim()
.escape(),
];
}
/**
* Retrieve transactions by the given reference type and id.
* @param {Request} req - Request object.
* @param {Response} res - Response.
* @param {NextFunction} next
* @returns
*/
public async transactionsByReference(
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId } = req;
const filter = this.matchedQueryData(req);
try {
const transactions =
await this.transactionsByReferenceService.getTransactionsByReference(
tenantId,
filter
);
const accept = this.accepts(req);
const acceptType = accept.types(['json']);
switch (acceptType) {
case 'json':
default:
return res
.status(200)
.send(this.transformToJsonResponse(transactions));
}
} catch (error) {
next(error);
}
}
private transformToJsonResponse(transactions) {
return transactions;
}
}

View File

@@ -62,6 +62,8 @@ export default class InventoryAdjustmentsController extends BaseController {
query('page').optional().isNumeric().toInt(),
query('page_size').optional().isNumeric().toInt(),
query('stringified_filter_roles').optional().isJSON(),
];
}

View File

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

View File

@@ -185,6 +185,8 @@ export default class ItemsController extends BaseController {
query('custom_view_id').optional().isNumeric().toInt(),
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) {
const { tenantId } = req;
const filter = {
filterRoles: [],
sortOrder: 'asc',
columnSortBy: 'created_at',
page: 1,
pageSize: 12,
inactiveMode: false,
...this.matchedQueryData(req),
};
if (filter.stringifiedFilterRoles) {
filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles);
}
try {
const {
items,

View File

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

View File

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

View File

@@ -1,16 +1,13 @@
import { Service, Inject } from 'typedi';
import { Router, Request, Response, NextFunction } from 'express';
import {
param,
query,
} from 'express-validator';
import { param, query } from 'express-validator';
import asyncMiddleware from 'api/middleware/asyncMiddleware';
import BaseController from './BaseController';
import { ServiceError } from 'exceptions';
import ResourceService from 'services/Resource/ResourceService';
@Service()
export default class ResourceController extends BaseController{
export default class ResourceController extends BaseController {
@Inject()
resourcesService: ResourceService;
@@ -21,42 +18,76 @@ export default class ResourceController extends BaseController{
const router = Router();
router.get(
'/:resource_model/fields', [
...this.resourceModelParamSchema,
],
'/:resource_model/meta',
[...this.resourceModelParamSchema],
this.asyncMiddleware(this.resourceMeta.bind(this)),
this.handleServiceErrors
);
router.get(
'/:resource_model/fields',
[...this.resourceModelParamSchema],
this.validationResult,
asyncMiddleware(this.resourceFields.bind(this)),
this.handleServiceErrors
);
router.get(
'/:resource_model/data', [
...this.resourceModelParamSchema,
],
'/:resource_model/data',
[...this.resourceModelParamSchema],
this.validationResult,
asyncMiddleware(this.resourceData.bind(this)),
this.handleServiceErrors,
)
this.handleServiceErrors
);
return router;
}
get resourceModelParamSchema() {
return [
param('resource_model').exists().trim().escape(),
];
return [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.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
resourceFields(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const { resource_model: resourceModel } = req.params;
try {
const resourceFields = this.resourcesService.getResourceFields(tenantId, resourceModel);
const resourceFields = this.resourcesService.getResourceFields(
tenantId,
resourceModel
);
return res.status(200).send({
resource_fields: this.transfromToResponse(resourceFields),
@@ -68,9 +99,9 @@ export default class ResourceController extends BaseController{
/**
* Retrieve resource data of the give resource based on the given query.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
async resourceData(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
@@ -78,7 +109,11 @@ export default class ResourceController extends BaseController{
const filter = req.query;
try {
const resourceData = await this.resourcesService.getResourceData(tenantId, resourceModel, filter);
const resourceData = await this.resourcesService.getResourceData(
tenantId,
resourceModel,
filter
);
return res.status(200).send({
resource_data: this.transfromToResponse(resourceData),
@@ -90,12 +125,17 @@ export default class ResourceController extends BaseController{
/**
* Handles service errors.
* @param {Error} error
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @param {Error} error
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
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.errorType === 'RESOURCE_MODEL_NOT_FOUND') {
return res.status(400).send({
@@ -105,4 +145,4 @@ export default class ResourceController extends BaseController{
}
next(error);
}
};
}

View File

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

View File

@@ -2,11 +2,11 @@ import { Router, Request, Response, NextFunction } from 'express';
import { check, param, query, matchedData } from 'express-validator';
import { Inject, Service } from 'typedi';
import { ISaleEstimateDTO } from 'interfaces';
import BaseController from 'api/controllers/BaseController'
import BaseController from 'api/controllers/BaseController';
import asyncMiddleware from 'api/middleware/asyncMiddleware';
import SaleEstimateService from 'services/Sales/SalesEstimate';
import DynamicListingService from 'services/DynamicListing/DynamicListService';
import { ServiceError } from "exceptions";
import { ServiceError } from 'exceptions';
@Service()
export default class SalesEstimatesController extends BaseController {
@@ -23,63 +23,56 @@ export default class SalesEstimatesController extends BaseController {
const router = Router();
router.post(
'/', [
...this.estimateValidationSchema,
],
'/',
[...this.estimateValidationSchema],
this.validationResult,
asyncMiddleware(this.newEstimate.bind(this)),
this.handleServiceErrors,
this.handleServiceErrors
);
router.post(
'/:id/deliver',
[
...this.validateSpecificEstimateSchema,
],
[...this.validateSpecificEstimateSchema],
this.validationResult,
asyncMiddleware(this.deliverSaleEstimate.bind(this)),
this.handleServiceErrors,
this.handleServiceErrors
);
router.post(
'/:id/approve',
[
this.validateSpecificEstimateSchema,
],
[this.validateSpecificEstimateSchema],
this.validationResult,
asyncMiddleware(this.approveSaleEstimate.bind(this)),
this.handleServiceErrors
);
router.post(
'/:id/reject',
[
this.validateSpecificEstimateSchema,
],
[this.validateSpecificEstimateSchema],
this.validationResult,
asyncMiddleware(this.rejectSaleEstimate.bind(this)),
this.handleServiceErrors,
)
this.handleServiceErrors
);
router.post(
'/:id', [
'/:id',
[
...this.validateSpecificEstimateSchema,
...this.estimateValidationSchema,
],
this.validationResult,
asyncMiddleware(this.editEstimate.bind(this)),
this.handleServiceErrors,
this.handleServiceErrors
);
router.delete(
'/:id', [
this.validateSpecificEstimateSchema,
],
'/:id',
[this.validateSpecificEstimateSchema],
this.validationResult,
asyncMiddleware(this.deleteEstimate.bind(this)),
this.handleServiceErrors,
this.handleServiceErrors
);
router.get(
'/:id',
this.validateSpecificEstimateSchema,
this.validationResult,
asyncMiddleware(this.getEstimate.bind(this)),
this.handleServiceErrors,
this.handleServiceErrors
);
router.get(
'/',
@@ -87,7 +80,7 @@ export default class SalesEstimatesController extends BaseController {
this.validationResult,
asyncMiddleware(this.getEstimates.bind(this)),
this.handleServiceErrors,
this.dynamicListService.handlerErrorsToResponse,
this.dynamicListService.handlerErrorsToResponse
);
return router;
}
@@ -109,8 +102,14 @@ export default class SalesEstimatesController extends BaseController {
check('entries.*.item_id').exists().isNumeric().toInt(),
check('entries.*.quantity').exists().isNumeric().toInt(),
check('entries.*.rate').exists().isNumeric().toFloat(),
check('entries.*.description').optional({ nullable: true }).trim().escape(),
check('entries.*.discount').optional({ nullable: true }).isNumeric().toFloat(),
check('entries.*.description')
.optional({ nullable: true })
.trim()
.escape(),
check('entries.*.discount')
.optional({ nullable: true })
.isNumeric()
.toFloat(),
check('note').optional().trim().escape(),
check('terms_conditions').optional().trim().escape(),
@@ -122,9 +121,7 @@ export default class SalesEstimatesController extends BaseController {
* Specific sale estimate validation schema.
*/
get validateSpecificEstimateSchema() {
return [
param('id').exists().isNumeric().toInt(),
];
return [param('id').exists().isNumeric().toInt()];
}
/**
@@ -137,8 +134,8 @@ export default class SalesEstimatesController extends BaseController {
query('column_sort_by').optional(),
query('sort_order').optional().isIn(['desc', 'asc']),
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);
try {
const storedEstimate = await this.saleEstimateService.createEstimate(tenantId, estimateDTO);
const storedEstimate = await this.saleEstimateService.createEstimate(
tenantId,
estimateDTO
);
return res.status(200).send({
id: storedEstimate.id,
@@ -165,8 +165,8 @@ export default class SalesEstimatesController extends BaseController {
/**
* Handle update estimate details with associated entries.
* @param {Request} req
* @param {Response} res
* @param {Request} req
* @param {Response} res
*/
async editEstimate(req: Request, res: Response, next: NextFunction) {
const { id: estimateId } = req.params;
@@ -175,12 +175,16 @@ export default class SalesEstimatesController extends BaseController {
try {
// 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({
id: estimateId,
message: 'The sale estimate has been created successfully.',
});
});
} catch (error) {
next(error);
}
@@ -188,8 +192,8 @@ export default class SalesEstimatesController extends BaseController {
/**
* Deletes the given estimate with associated entries.
* @param {Request} req
* @param {Response} res
* @param {Request} req
* @param {Response} res
*/
async deleteEstimate(req: Request, res: Response, next: NextFunction) {
const { id: estimateId } = req.params;
@@ -200,7 +204,7 @@ export default class SalesEstimatesController extends BaseController {
return res.status(200).send({
id: estimateId,
message: 'The sale estimate has been deleted successfully.'
message: 'The sale estimate has been deleted successfully.',
});
} catch (error) {
next(error);
@@ -209,8 +213,8 @@ export default class SalesEstimatesController extends BaseController {
/**
* Deliver the given sale estimate.
* @param {Request} req
* @param {Response} res
* @param {Request} req
* @param {Response} res
*/
async deliverSaleEstimate(req: Request, res: Response, next: NextFunction) {
const { id: estimateId } = req.params;
@@ -230,9 +234,9 @@ export default class SalesEstimatesController extends BaseController {
/**
* Marks the sale estimate as approved.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
async approveSaleEstimate(req: Request, res: Response, next: NextFunction) {
const { id: estimateId } = req.params;
@@ -252,9 +256,9 @@ export default class SalesEstimatesController extends BaseController {
/**
* Marks the sale estimate as rejected.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
async rejectSaleEstimate(req: Request, res: Response, next: NextFunction) {
const { id: estimateId } = req.params;
@@ -274,16 +278,19 @@ export default class SalesEstimatesController extends BaseController {
/**
* Retrieve the given estimate with associated entries.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
async getEstimate(req: Request, res: Response, next: NextFunction) {
const { id: estimateId } = req.params;
const { tenantId } = req;
try {
const estimate = await this.saleEstimateService.getEstimate(tenantId, estimateId);
const estimate = await this.saleEstimateService.getEstimate(
tenantId,
estimateId
);
return res.status(200).send({ estimate });
} catch (error) {
@@ -293,35 +300,28 @@ export default class SalesEstimatesController extends BaseController {
/**
* Retrieve estimates with pagination metadata.
* @param {Request} req
* @param {Response} res
* @param {Request} req
* @param {Response} res
*/
async getEstimates(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const filter = {
filterRoles: [],
sortOrder: 'asc',
columnSortBy: 'created_at',
page: 1,
pageSize: 12,
...this.matchedQueryData(req),
};
if (filter.stringifiedFilterRoles) {
filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles);
}
try {
const {
salesEstimates,
pagination,
filterMeta
} = await this.saleEstimateService.estimatesList(tenantId, filter);
const { salesEstimates, pagination, filterMeta } =
await this.saleEstimateService.estimatesList(tenantId, filter);
return res.status(200).send({
sales_estimates: this.transfromToResponse(salesEstimates),
pagination,
filter_meta: this.transfromToResponse(filterMeta),
})
});
} catch (error) {
next(error);
}
@@ -329,12 +329,17 @@ export default class SalesEstimatesController extends BaseController {
/**
* Handles service errors.
* @param {Error} error
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @param {Error} error
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
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.errorType === 'ITEMS_NOT_FOUND') {
return res.boom.badRequest(null, {
@@ -409,4 +414,4 @@ export default class SalesEstimatesController extends BaseController {
}
next(error);
}
};
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,17 +1,81 @@
export interface IModel {
name: string,
tableName: string,
fields: { [key: string]: any, },
};
name: string;
tableName: string;
fields: { [key: string]: any };
}
export interface IFilterMeta {
sortOrder: string,
sortBy: string,
};
sortOrder: string;
sortBy: string;
}
export interface IPaginationMeta {
pageSize: number,
page: number,
};
pageSize: 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 IModelMetaRelationFieldCommon {
fieldType: 'relation';
}
export interface IModelMetaRelationEnumerationField {
relationType: 'enumeration';
relationKey: string;
relationEntityLabel: string;
relationEntityKey: string;
}
export type IModelMetaRelationField = IModelMetaRelationFieldCommon & (
IModelMetaRelationEnumerationField
);
export interface IModelMeta {
defaultFilterField: string;
defaultSort: IModelMetaDefaultSort;
fields: { [key: string]: IModelMetaField };
}

View File

@@ -0,0 +1,31 @@
export interface ITransactionsByReferenceQuery {
referenceType: string;
referenceId: string;
}
export interface ITransactionsByReferenceAmount {
amount: number;
formattedAmount: string;
currencyCode: string;
}
export interface ITransactionsByReferenceTransaction{
credit: ITransactionsByReferenceAmount;
debit: ITransactionsByReferenceAmount;
contactType: string;
formattedContactType: string;
contactId: number;
referenceType: string;
formattedReferenceType: string;
referenceId: number;
accountName: string;
accountCode: string;
accountId: number;
}

View File

@@ -55,6 +55,7 @@ export * from './CashFlow';
export * from './InventoryDetails';
export * from './LandedCost';
export * from './Entry';
export * from './TransactionsByReference';
export interface I18nService {
__: (input: string) => string;

View File

@@ -1,65 +1,85 @@
import { forEach, uniqBy } from 'lodash';
import { buildFilterRolesJoins } from 'lib/ViewRolesBuilder';
import { IModel } from 'interfaces';
import DynamicFilterAbstructor from './DynamicFilterAbstructor';
import { IDynamicFilter, IFilterRole, IModel } from 'interfaces';
export default class DynamicFilter {
model: IModel;
tableName: string;
export default class DynamicFilter extends DynamicFilterAbstructor{
private model: IModel;
private tableName: string;
private dynamicFilters: IDynamicFilter[];
/**
* Constructor.
* @param {String} tableName -
*/
constructor(model) {
super();
this.model = model;
this.tableName = model.tableName;
this.filters = [];
this.dynamicFilters = [];
}
/**
* Set filter.
* @param {*} filterRole - Filter role.
* Registers the given dynamic filter.
* @param {IDynamicFilter} filterRole - Filter role.
*/
setFilter(filterRole) {
filterRole.setModel(this.model);
this.filters.push(filterRole);
public setFilter = (dynamicFilter: IDynamicFilter) => {
dynamicFilter.setModel(this.model);
dynamicFilter.onInitialize();
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.
*/
buildQuery() {
const buildersCallbacks = [];
const tableColumns = [];
this.filters.forEach((filter) => {
const { filterRoles } = filter;
buildersCallbacks.push(filter.buildQuery());
tableColumns.push(
...(Array.isArray(filterRoles) ? filterRoles : [filterRoles])
);
});
public buildQuery = () => {
const buildersCallbacks = this.dynamicFiltersBuildQuery();
const tableColumns = this.dynamicFilterTableColumns();
return (builder) => {
buildersCallbacks.forEach((builderCallback) => {
builderCallback(builder);
});
buildFilterRolesJoins(
this.model,
uniqBy(tableColumns, 'columnKey')
)(builder);
this.buildFilterRolesJoins(builder);
};
}
/**
* Retrieve response metadata from all filters adapters.
*/
getResponseMeta() {
public getResponseMeta = () => {
const responseMeta = {};
this.filters.forEach((filter) => {
this.dynamicFilters.forEach((filter) => {
const { responseMeta: filterMeta } = filter;
forEach(filterMeta, (value, key) => {

View File

@@ -0,0 +1,40 @@
import { IModel, IFilterRole } from 'interfaces';
import { FIELD_TYPE } from './constants';
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 = (builder) => {
this.dynamicFilters.forEach((dynamicFilter) => {
const relationsFields = dynamicFilter.relationFields;
this.buildFieldsJoinQueries(builder, relationsFields);
});
};
private buildFieldsJoinQueries = (builder, fieldsRelations: string[]) => {
fieldsRelations.forEach((fieldRelation) => {
const relation = this.model.relationMappings[fieldRelation];
if (relation) {
const splitToRelation = relation.join.to.split('.');
const relationTable = splitToRelation[0] || '';
builder.join(relationTable, relation.join.from, '=', relation.join.to);
}
});
};
}

View File

@@ -1,10 +1,8 @@
import { difference } from 'lodash';
import DynamicFilterRoleAbstructor from 'lib/DynamicFilter/DynamicFilterRoleAbstructor';
import { buildFilterQuery } from 'lib/ViewRolesBuilder';
import DynamicFilterRoleAbstructor from './DynamicFilterRoleAbstructor';
import { IFilterRole } from 'interfaces';
export default class FilterRoles extends DynamicFilterRoleAbstructor {
filterRoles: IFilterRole[];
private filterRoles: IFilterRole[];
/**
* Constructor method.
@@ -13,18 +11,23 @@ export default class FilterRoles extends DynamicFilterRoleAbstructor {
*/
constructor(filterRoles: IFilterRole[]) {
super();
this.filterRoles = filterRoles;
this.setResponseMeta();
}
public onInitialize() {
this.setFilterRolesRelations();
}
/**
* Builds filter roles logic expression.
* @return {string}
*/
private buildLogicExpression(): string {
let expression = '';
this.filterRoles.forEach((role, index) => {
expression +=
index === 0 ? `${role.index} ` : `${role.condition} ${role.index} `;
@@ -35,19 +38,33 @@ export default class FilterRoles extends DynamicFilterRoleAbstructor {
/**
* Builds database query of view roles.
*/
buildQuery() {
protected buildQuery() {
const logicExpression = this.buildLogicExpression();
return (builder) => {
const logicExpression = this.buildLogicExpression();
buildFilterQuery(this.model, this.filterRoles, logicExpression)(builder);
this.buildFilterQuery(
this.model,
this.filterRoles,
logicExpression
)(builder);
};
}
/**
* Sets response meta.
*/
setResponseMeta() {
private setResponseMeta() {
this.responseMeta = {
filterRoles: this.filterRoles,
};
}
/**
* Sets filter roles relations if field was relation type.
*/
private setFilterRolesRelations() {
this.filterRoles.forEach((relationRole) => {
this.setRelationIfRelationField(relationRole.fieldKey);
});
}
}

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,349 @@
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 {
filterRoles: IFilterRole[] = [];
tableName: string;
model: IModel;
responseMeta: { [key: string]: any } = {};
export default abstract class DynamicFilterAbstructor
implements IDynamicFilter
{
protected filterRoles: IFilterRole[] = [];
protected tableName: string;
protected model: IModel;
protected responseMeta: { [key: string]: any } = {};
public relationFields = [];
setModel(model: IModel) {
/**
* Sets model the dynamic filter service.
* @param {IModel} model
*/
public setModel(model: IModel) {
this.model = model;
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);
};
};
/**
* Retrieve relation column of comparator fieldز
*/
private getFieldComparatorRelationColumn(field) {
const relation = this.model.relationMappings[field.relationKey];
if (relation) {
const relationModel = relation.modelClass;
const relationColumn =
field.relationEntityKey === 'id'
? 'id'
: relationModel.getField(field.relationEntityKey, 'column');
return `${relationModel.tableName}.${relationColumn}`;
}
}
/**
* Retrieve the comparator field column.
* @param {IModel} model -
* @param {} -
*/
private getFieldComparatorColumn = (field) => {
return field.fieldType === FIELD_TYPE.RELATION
? this.getFieldComparatorRelationColumn(field)
: `${this.tableName}.${field.column}`;
};
/**
* Builds roles queries.
* @param {IModel} model -
* @param {Object} role -
*/
protected buildRoleQuery = (model: IModel, role: IFilterRole) => {
const field = model.getField(role.fieldKey);
const comparatorColumn = this.getFieldComparatorColumn(field);
// Field relation custom query.
if (typeof field.filterCustomQuery !== 'undefined') {
return (builder) => {
field.filterCustomQuery(builder, role);
};
}
switch (field.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);
};
/**
* Registers relation field if the given field was relation type
* and not registered.
* @param {string} fieldKey - Field key.
*/
protected setRelationIfRelationField = (fieldKey: string): void => {
const field = this.model.getField(fieldKey);
const isAlreadyRegistered = this.relationFields.some(
(field) => field === fieldKey
);
if (
!isAlreadyRegistered &&
field &&
field.fieldType === FIELD_TYPE.RELATION
) {
this.relationFields.push(field.relationKey);
}
};
}

View File

@@ -1,12 +1,13 @@
import DynamicFilterRoleAbstructor from 'lib/DynamicFilter/DynamicFilterRoleAbstructor';
import {
getRoleFieldColumn,
validateFieldKeyExistance,
getTableFromRelationColumn,
} from 'lib/ViewRolesBuilder';
import { FIELD_TYPE } from './constants';
interface ISortRole {
fieldKey: string;
order: string;
}
export default class DynamicFilterSortBy extends DynamicFilterRoleAbstructor {
sortRole: { fieldKey: string; order: string } = {};
private sortRole: ISortRole = {};
/**
* Constructor method.
@@ -24,58 +25,65 @@ export default class DynamicFilterSortBy extends DynamicFilterRoleAbstructor {
}
/**
* Validate the given field key with the model.
* On initialize the dyanmic sort by.
*/
validate() {
validateFieldKeyExistance(this.model, this.sortRole.fieldKey);
public onInitialize() {
this.setRelationIfRelationField(this.sortRole.fieldKey);
}
/**
* Retrieve field comparator relatin column.
* @param field
* @returns {string}
*/
private getFieldComparatorRelationColumn = (field): string => {
const relation = this.model.relationMappings[field.relationKey];
if (relation) {
const relationModel = relation.modelClass;
const relationField = relationModel.getField(field.relationEntityLabel);
return `${relationModel.tableName}.${relationField.column}`;
}
return '';
};
/**
* Retrieve the comparator field column.
* @param {IModel} field
* @returns {string}
*/
private getFieldComparatorColumn = (field): string => {
return field.fieldType === FIELD_TYPE.RELATION
? this.getFieldComparatorRelationColumn(field)
: `${this.tableName}.${field.column}`;
};
/**
* Builds database query of sort by column on the given direction.
*/
buildQuery() {
const fieldRelation = getRoleFieldColumn(
this.model,
this.sortRole.fieldKey
);
const comparatorColumn =
fieldRelation.relationColumn ||
`${this.tableName}.${fieldRelation.column}`;
public buildQuery = () => {
const field = this.model.getField(this.sortRole.fieldKey);
const comparatorColumn = this.getFieldComparatorColumn(field);
if (typeof fieldRelation.sortQuery !== 'undefined') {
// Sort custom query.
if (typeof field.sortCustomQuery !== 'undefined') {
return (builder) => {
fieldRelation.sortQuery(builder, this.sortRole);
field.sortCustomQuery(builder, this.sortRole);
};
}
return (builder) => {
if (this.sortRole.fieldKey) {
builder.orderBy(`${comparatorColumn}`, this.sortRole.order);
}
this.joinBuildQuery()(builder);
};
}
joinBuildQuery() {
const fieldColumn = getRoleFieldColumn(this.model, this.sortRole.fieldKey);
return (builder) => {
if (fieldColumn.relation) {
const joinTable = getTableFromRelationColumn(fieldColumn.relation);
builder.join(
joinTable,
`${this.model.tableName}.${fieldColumn.column}`,
'=',
fieldColumn.relation
);
}
};
}
};
/**
* Sets response meta.
*/
setResponseMeta() {
public setResponseMeta() {
this.responseMeta = {
sortOrder: this.sortRole.fieldKey,
sortBy: this.sortRole.order,

View File

@@ -0,0 +1,37 @@
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',
COMPUTED: 'computed'
};

View File

@@ -1,121 +1,7 @@
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';
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.
* @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;
}
/**
* 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) {
return (builder) => {
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.
* @param {Array} viewRoles -
@@ -316,14 +96,6 @@ export function validateFieldKeyExistance(model: any, fieldKey: string) {
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.

View File

@@ -0,0 +1,100 @@
import { IModelMeta } from 'interfaces';
import { ACCOUNT_TYPES } from 'data/AccountTypes';
export default {
defaultFilterField: 'name',
defaultSort: {
sortOrder: 'DESC',
sortField: 'name',
},
fields: {
name: {
name: 'Account name',
column: 'name',
fieldType: 'text',
},
description: {
name: 'Description',
column: 'description',
fieldType: 'text',
},
slug: {
name: 'Account slug',
column: 'slug',
fieldType: 'text',
columnable: false,
},
code: {
name: 'Account code',
column: 'code',
fieldType: 'text',
},
root_type: {
name: 'Root type',
fieldType: 'enumeration',
options: [
{ key: 'asset', label: 'Asset' },
{ key: 'liability', label: 'Liability' },
{ key: 'equity', label: 'Equity' },
{ key: 'Income', label: 'Income' },
{ key: 'expense', label: 'Expense' },
],
filterCustomQuery: RootTypeFieldFilterQuery,
sortable: false,
},
normal: {
name: 'Account normal',
fieldType: 'enumeration',
options: [
{ key: 'debit', label: 'Debit' },
{ key: 'credit', label: 'Credit' },
],
filterCustomQuery: NormalTypeFieldFilterQuery,
sortable: false,
},
type: {
name: 'Type',
column: 'account_type',
fieldType: 'enumeration',
options: ACCOUNT_TYPES.map((accountType) => ({
label: accountType.label,
key: accountType.key
})),
},
active: {
name: 'Active',
column: 'active',
fieldType: 'boolean',
filterable: false,
},
balance: {
name: 'Account balance',
column: 'amount',
fieldType: 'number',
},
currency: {
name: 'Currency',
column: 'currency_code',
fieldType: 'text',
},
created_at: {
name: 'Created at',
column: 'created_at',
fieldType: 'date',
},
},
};
/**
* Filter query of root type field .
*/
function RootTypeFieldFilterQuery(query, role) {
query.modify('filterByRootType', role.value);
}
/**
* Filter query of normal field .
*/
function NormalTypeFieldFilterQuery(query, role) {
query.modify('filterByAccountNormal', role.value);
}

View File

@@ -1,16 +1,16 @@
/* eslint-disable global-require */
import { Model } from 'objection';
import { flatten, castArray } from 'lodash';
import { mixin, Model } from 'objection';
import { castArray } from 'lodash';
import TenantModel from 'models/TenantModel';
import {
buildFilterQuery,
buildSortColumnQuery,
} from 'lib/ViewRolesBuilder';
import { buildFilterQuery, buildSortColumnQuery } from 'lib/ViewRolesBuilder';
import { flatToNestedArray } from 'utils';
import DependencyGraph from 'lib/DependencyGraph';
import AccountTypesUtils from 'lib/AccountTypes'
import AccountTypesUtils from 'lib/AccountTypes';
import AccountSettings from './Account.Settings';
import ModelSettings from './ModelSetting';
import { ACCOUNT_TYPES } from 'data/AccountTypes';
export default class Account extends TenantModel {
export default class Account extends mixin(TenantModel, [ModelSettings]) {
/**
* Table name.
*/
@@ -21,7 +21,7 @@ export default class Account extends TenantModel {
/**
* Timestamps columns.
*/
get timestamps() {
static get timestamps() {
return ['createdAt', 'updatedAt'];
}
@@ -35,7 +35,7 @@ export default class Account extends TenantModel {
'accountRootType',
'accountNormal',
'isBalanceSheetAccount',
'isPLSheet'
'isPLSheet',
];
}
@@ -95,6 +95,13 @@ export default class Account extends TenantModel {
const TABLE_NAME = Account.tableName;
return {
/**
* Inactive/Active mode.
*/
inactiveMode(query, active = false) {
query.where('accounts.active', !active);
},
filterAccounts(query, accountIds) {
if (accountIds.length > 0) {
query.whereIn(`${TABLE_NAME}.id`, accountIds);
@@ -111,6 +118,28 @@ export default class Account extends TenantModel {
sortColumnBuilder(query, columnKey, direction) {
buildSortColumnQuery(Account.tableName, columnKey, direction)(query);
},
/**
* Filter by root type.
*/
filterByRootType(query, rootType) {
const filterTypes = ACCOUNT_TYPES.filter(
(accountType) => accountType.rootType === rootType
).map((accountType) => accountType.key);
query.whereIn('account_type', filterTypes);
},
/**
* Filter by account normal
*/
filterByAccountNormal(query, accountNormal) {
const filterTypes = ACCOUNT_TYPES.filter(
(accountType) => accountType.normal === accountNormal,
).map((accountType) => accountType.key);
query.whereIn('account_type', filterTypes);
},
};
}
@@ -134,10 +163,10 @@ export default class Account extends TenantModel {
},
};
}
/**
* Detarmines whether the given type equals the account type.
* @param {string} accountType
* @param {string} accountType
* @return {boolean}
*/
isAccountType(accountType) {
@@ -147,7 +176,7 @@ export default class Account extends TenantModel {
/**
* Detarmines whether the given root type equals the account type.
* @param {string} rootType
* @param {string} rootType
* @return {boolean}
*/
isRootType(rootType) {
@@ -156,11 +185,14 @@ export default class Account extends TenantModel {
/**
* Detarmine whether the given parent type equals the account type.
* @param {string} parentType
* @param {string} parentType
* @return {boolean}
*/
isParentType(parentType) {
return AccountTypesUtils.isParentTypeEqualsKey(this.accountType, parentType);
return AccountTypesUtils.isParentTypeEqualsKey(
this.accountType,
parentType
);
}
/**
@@ -188,105 +220,32 @@ export default class Account extends TenantModel {
}
/**
* Converts flatten accounts list to nested array.
* @param {Array} accounts
* @param {Object} options
* Converts flatten accounts list to nested array.
* @param {Array} accounts
* @param {Object} options
*/
static toNestedArray(accounts, options = { children: 'children' }) {
return flatToNestedArray(accounts, { id: 'id', parentId: 'parentAccountId' })
return flatToNestedArray(accounts, {
id: 'id',
parentId: 'parentAccountId',
});
}
/**
* Transformes the accounts list to depenedency graph structure.
* @param {IAccount[]} accounts
*/
* @param {IAccount[]} accounts
*/
static toDependencyGraph(accounts) {
return DependencyGraph.fromArray(
accounts, { itemId: 'id', parentItemId: 'parentAccountId' }
);
return DependencyGraph.fromArray(accounts, {
itemId: 'id',
parentItemId: 'parentAccountId',
});
}
/**
* Model defined fields.
* Model settings.
*/
static get fields() {
return {
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' },
],
},
};
static get meta() {
return AccountSettings;
}
}

View File

@@ -0,0 +1,96 @@
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',
fieldType: 'relation',
relationType: 'enumeration',
relationKey: 'vendor',
relationEntityLabel: 'name',
relationEntityKey: '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' },
],
filterCustomQuery: StatusFieldFilterQuery,
sortCustomQuery: StatusFieldSortQuery,
},
amount: {
name: 'Amount',
column: 'amount',
fieldType: 'number',
},
payment_amount: {
name: 'Payment amount',
column: 'payment_amount',
fieldType: 'number',
},
note: {
name: 'Note',
column: 'note',
fieldType: 'text',
},
created_at: {
name: 'Created at',
column: 'created_at',
fieldType: 'date',
},
},
};
/**
* Status field filter custom query.
*/
function StatusFieldFilterQuery(query, role) {
query.modify('statusFilter', role.value);
}
/**
* Status field sort custom query.
*/
function StatusFieldSortQuery(query, role) {
query.modify('sortByStatus', role.order);
}

View File

@@ -1,10 +1,11 @@
import { Model, raw } from 'objection';
import { Model, raw, mixin } from 'objection';
import moment from 'moment';
import { difference } from 'lodash';
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
*/
@@ -12,10 +13,9 @@ export default class Bill extends TenantModel {
return 'bills';
}
static get resourceable() {
return true;
}
/**
* Model modifiers.
*/
static get modifiers() {
return {
/**
@@ -71,15 +71,42 @@ export default class Bill extends TenantModel {
* Filters the bills from the given date.
*/
fromDate(query, fromDate) {
query.where('bill_date', '<=', fromDate)
query.where('bill_date', '<=', fromDate);
},
/**
* Sort the bills by full-payment bills.
*/
sortByStatus(query, order) {
sortByStatus(query, order) {
query.orderByRaw(`PAYMENT_AMOUNT = AMOUNT ${order}`);
},
/**
* Status filter.
*/
statusFilter(query, filterType) {
switch (filterType) {
case 'draft':
query.modify('draft');
break;
case 'delivered':
query.modify('delivered');
break;
case 'unpaid':
query.modify('unpaid');
break;
case 'overdue':
default:
query.modify('overdue');
break;
case 'partially-paid':
query.modify('partiallyPaid');
break;
case 'paid':
query.modify('paid');
break;
}
},
};
}
@@ -103,7 +130,7 @@ export default class Bill extends TenantModel {
'remainingDays',
'overdueDays',
'isOverdue',
'unallocatedCostAmount'
'unallocatedCostAmount',
];
}
@@ -198,6 +225,13 @@ export default class Bill extends TenantModel {
return Math.max(date.diff(dueDate, 'days'), 0);
}
/**
* Bill model settings.
*/
static get meta() {
return BillSettings;
}
/**
* Relationship mapping.
*/
@@ -269,88 +303,4 @@ export default class Bill extends TenantModel {
.where('id', billId)
[changeMethod]('payment_amount', Math.abs(amount));
}
static get fields() {
return {
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) {
case 'draft':
query.modify('draft');
break;
case 'opened':
query.modify('opened');
break;
case 'unpaid':
query.modify('unpaid');
break;
case 'overdue':
query.modify('overdue');
break;
case 'partially-paid':
query.modify('partiallyPaid');
break;
case 'paid':
query.modify('paid');
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',
},
};
}
}

View File

@@ -0,0 +1,67 @@
export default {
defaultFilterField: 'vendor',
defaultSort: {
sortOrder: 'DESC',
sortField: 'bill_date',
},
fields: {
'vendor': {
name: 'Vendor name',
column: 'vendor_id',
fieldType: 'relation',
relationType: 'enumeration',
relationKey: 'vendor',
relationEntityLabel: 'display_name',
relationEntityKey: 'id',
},
'amount': {
name: 'Amount',
column: 'amount',
fieldType: 'number',
},
'due_amount': {
name: 'Due amount',
column: 'due_amount',
fieldType: 'number',
},
'payment_account': {
name: 'Payment account',
column: 'payment_account_id',
fieldType: 'relation',
fieldRelation: 'paymentAccount',
fieldRelationType: 'enumeration',
relationLabelField: 'name',
relationKeyField: 'slug',
},
'payment_number': {
name: 'Payment number',
column: 'payment_number',
fieldType: 'number',
},
'payment_date': {
name: 'Payment date',
column: 'payment_date',
fieldType: 'date',
},
'reference_no': {
name: 'Reference No.',
column: 'reference',
fieldType: 'text',
},
'description': {
name: 'Description',
column: 'description',
fieldType: 'text',
},
'created_at': {
name: 'Created at',
column: 'created_at',
fieldType: 'date',
},
},
};

View File

@@ -1,7 +1,9 @@
import { Model } from "objection";
import { Model, mixin } from "objection";
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
*/
@@ -16,8 +18,11 @@ export default class BillPayment extends TenantModel {
return ["createdAt", "updatedAt"];
}
static get resourceable() {
return true;
/**
* Model settings.
*/
static get meta() {
return BillPaymentSettings;
}
/**
@@ -26,7 +31,7 @@ export default class BillPayment extends TenantModel {
static get relationMappings() {
const BillPaymentEntry = require("models/BillPaymentEntry");
const AccountTransaction = require("models/AccountTransaction");
const Contact = require("models/Contact");
const Vendor = require("models/Vendor");
const Account = require("models/Account");
return {
@@ -41,7 +46,7 @@ export default class BillPayment extends TenantModel {
vendor: {
relation: Model.BelongsToOneRelation,
modelClass: Contact.default,
modelClass: Vendor.default,
join: {
from: "bills_payments.vendorId",
to: "contacts.id",
@@ -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 PaginationQueryBuilder from './Pagination';
import QueryParser from 'lib/LogicEvaluation/QueryParser';
import ModelSetting from './ModelSetting';
import CustomerSettings from './Customer.Settings';
class CustomerQueryBuilder extends PaginationQueryBuilder {
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.
*/
@@ -63,6 +65,13 @@ export default class Customer extends TenantModel {
*/
static get modifiers() {
return {
/**
* Inactive/Active mode.
*/
inactiveMode(query, active = false) {
query.where('active', !active);
},
/**
* Filters the active customers.
*/
@@ -81,10 +90,9 @@ export default class Customer extends TenantModel {
overdue(query) {
query.select(
'*',
Customer
.relatedQuery('overDueInvoices', query.knex())
Customer.relatedQuery('overDueInvoices', query.knex())
.count()
.as('countOverdue'),
.as('countOverdue')
);
query.having('countOverdue', '>', 0);
},
@@ -93,7 +101,7 @@ export default class Customer extends TenantModel {
*/
unpaid(query) {
query.whereRaw('`BALANCE` + `OPENING_BALANCE` <> 0');
}
},
};
}
@@ -122,77 +130,12 @@ export default class Customer extends TenantModel {
},
filter: (query) => {
query.modify('overdue');
}
}
},
},
};
}
static get fields() {
return {
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',
}
};
static get meta() {
return CustomerSettings;
}
}

View File

@@ -0,0 +1,71 @@
/**
* 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',
relationType: 'enumeration',
relationKey: 'paymentAccount',
relationEntityLabel: 'name',
relationEntityKey: 'slug',
},
'amount': {
name: 'Amount',
column: 'total_amount',
fieldType: 'number',
},
'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' },
],
filterCustomQuery: StatusFieldFilterQuery,
sortCustomQuery: StatusFieldSortQuery,
},
'created_at': {
name: 'Created at',
column: 'created_at',
fieldType: 'date',
},
},
};
function StatusFieldFilterQuery(query, role) {
query.modify('filterByStatus', role.value);
}
function StatusFieldSortQuery(query, role) {
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 { 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
*/
@@ -25,19 +27,8 @@ export default class Expense extends TenantModel {
}
/**
* Allows to mark model as resourceable to viewable and filterable.
* Virtual attributes.
*/
static get resourceable() {
return true;
}
/**
*
*/
static get media() {
return true;
}
static get virtualAttributes() {
return ['isPublished', 'unallocatedCostAmount'];
}
@@ -96,6 +87,18 @@ export default class Expense extends TenantModel {
filterByPublished(query) {
query.whereNot('published_at', null);
},
filterByStatus(query, status) {
switch (status) {
case 'draft':
query.modify('filterByDraft');
break;
case 'published':
default:
query.modify('filterByPublished');
break;
}
},
};
}
@@ -142,71 +145,7 @@ export default class Expense extends TenantModel {
};
}
/**
* Model defined fields.
*/
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',
},
};
static get meta() {
return ExpenseSettings;
}
}

View File

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

View File

@@ -1,7 +1,9 @@
import { Model } from 'objection';
import { Model, mixin } from 'objection';
import TenantModel from 'models/TenantModel';
import InventoryAdjustmentSettings from './InventoryAdjustment.Settings';
import ModelSetting from './ModelSetting';
export default class InventoryAdjustment extends TenantModel {
export default class InventoryAdjustment extends mixin(TenantModel, [ModelSetting]) {
/**
* Table name
*/
@@ -40,8 +42,8 @@ export default class InventoryAdjustment extends TenantModel {
static getInventoryDirection(type) {
const directions = {
'increment': 'IN',
'decrement': 'OUT',
increment: 'IN',
decrement: 'OUT',
};
return directions[type] || '';
}
@@ -81,52 +83,9 @@ export default class InventoryAdjustment extends TenantModel {
}
/**
* Model defined fields.
* Model settings.
*/
static get fields() {
return {
date: {
label: 'Date',
column: 'date',
columnType: 'date',
},
type: {
label: 'Adjustment type',
column: 'type',
options: [
{ key: 'increment', label: 'Increment', },
{ key: 'decrement', label: 'Decrement' },
],
},
adjustment_account: {
column: 'adjustment_account_id',
},
reason: {
label: 'Reason',
column: 'reason',
},
reference_no: {
label: 'Reference No.',
column: 'reference_no',
},
description: {
label: 'Description',
column: 'description',
},
user: {
label: 'User',
column: 'user_id',
},
published_at: {
label: 'Published at',
column: 'published_at'
},
created_at: {
label: 'Created at',
column: 'created_at',
columnType: 'date',
fieldType: 'date',
},
};
static get meta() {
return InventoryAdjustmentSettings;
}
}

View File

@@ -0,0 +1,123 @@
export default {
defaultFilterField: 'name',
defaultSort: {
sortField: 'name',
sortOrder: 'DESC',
},
fields: {
'type': {
name: 'Item type',
column: 'type',
fieldType: 'enumeration',
options: [
{ key: 'inventory', label: 'Inventory', },
{ key: 'service', label: 'Service' },
{ key: 'non-inventory', label: 'Non Inventory', },
],
},
'name': {
name: 'Name',
column: 'name',
fieldType: 'text',
},
'code': {
name: 'Code',
column: 'code',
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',
fieldType: 'relation',
relationType: 'enumeration',
relationKey: 'costAccount',
relationEntityLabel: 'name',
relationEntityKey: 'slug',
},
'sell_account': {
name: 'Sell account',
column: 'sell_account_id',
fieldType: 'relation',
relationType: 'enumeration',
relationKey: 'sellAccount',
relationEntityLabel: 'name',
relationEntityKey: 'slug',
},
'inventory_account': {
name: 'Inventory account',
column: 'inventory_account_id',
relationType: 'enumeration',
relationKey: 'inventoryAccount',
relationEntityLabel: 'name',
relationEntityKey: 'slug',
},
'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',
},
'category': {
name: 'Category',
column: 'category_id',
relationType: 'enumeration',
relationKey: 'category',
relationEntityLabel: 'name',
relationEntityKey: 'id',
},
'active': {
name: 'Active',
column: 'active',
fieldType: 'boolean',
filterable: false,
},
'created_at': {
name: 'Created at',
column: 'created_at',
columnType: 'date',
fieldType: 'date',
},
},
};

View File

@@ -1,20 +1,22 @@
import { Model } from "objection";
import TenantModel from "models/TenantModel";
import { buildFilterQuery } from "lib/ViewRolesBuilder";
import { Model, mixin } from 'objection';
import TenantModel from 'models/TenantModel';
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
*/
static get tableName() {
return "items";
return 'items';
}
/**
* Model timestamps.
*/
get timestamps() {
return ["createdAt", "updatedAt"];
return ['createdAt', 'updatedAt'];
}
/**
@@ -35,6 +37,13 @@ export default class Item extends TenantModel {
viewRolesBuilder(query, conditions, logicExpression) {
buildFilterQuery(Item.tableName, conditions, logicExpression)(query);
},
/**
* Inactive/Active mode.
*/
inactiveMode(query, active = false) {
query.where('items.active', !active);
},
};
}
@@ -42,9 +51,9 @@ export default class Item extends TenantModel {
* Relationship mapping.
*/
static get relationMappings() {
const Media = require("models/Media");
const Account = require("models/Account");
const ItemCategory = require("models/ItemCategory");
const Media = require('models/Media');
const Account = require('models/Account');
const ItemCategory = require('models/ItemCategory');
return {
/**
@@ -54,8 +63,8 @@ export default class Item extends TenantModel {
relation: Model.BelongsToOneRelation,
modelClass: ItemCategory.default,
join: {
from: "items.categoryId",
to: "items_categories.id",
from: 'items.categoryId',
to: 'items_categories.id',
},
},
@@ -63,8 +72,8 @@ export default class Item extends TenantModel {
relation: Model.BelongsToOneRelation,
modelClass: Account.default,
join: {
from: "items.costAccountId",
to: "accounts.id",
from: 'items.costAccountId',
to: 'accounts.id',
},
},
@@ -72,8 +81,8 @@ export default class Item extends TenantModel {
relation: Model.BelongsToOneRelation,
modelClass: Account.default,
join: {
from: "items.sellAccountId",
to: "accounts.id",
from: 'items.sellAccountId',
to: 'accounts.id',
},
},
@@ -81,8 +90,8 @@ export default class Item extends TenantModel {
relation: Model.BelongsToOneRelation,
modelClass: Account.default,
join: {
from: "items.inventoryAccountId",
to: "accounts.id",
from: 'items.inventoryAccountId',
to: 'accounts.id',
},
},
@@ -90,110 +99,21 @@ export default class Item extends TenantModel {
relation: Model.ManyToManyRelation,
modelClass: Media.default,
join: {
from: "items.id",
from: 'items.id',
through: {
from: "media_links.model_id",
to: "media_links.media_id",
from: 'media_links.model_id',
to: 'media_links.media_id',
},
to: "media.id",
to: 'media.id',
},
},
};
}
/**
* Item fields.
* Model settings.
*/
static get fields() {
return {
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",
},
};
static get meta() {
return ItemSettings;
}
}

View File

@@ -0,0 +1,30 @@
export default {
defaultFilterField: 'name',
defaultSort: {
sortField: 'name',
sortOrder: 'DESC',
},
fields: {
name: {
label: 'Name',
column: 'name',
fieldType: 'text',
},
description: {
label: 'Description',
column: 'description',
fieldType: 'text',
},
count: {
label: 'Count',
column: 'count',
fieldType: 'number',
virtualColumn: true,
},
created_at: {
label: 'Created at',
column: 'created_at',
columnType: 'date',
},
},
};

View File

@@ -1,8 +1,9 @@
import path from 'path';
import { Model } from 'objection';
import { Model, mixin } from 'objection';
import TenantModel from 'models/TenantModel';
import ModelSetting from './ModelSetting';
import ItemCategorySettings from './ItemCategory.Settings';
export default class ItemCategory extends TenantModel {
export default class ItemCategory extends mixin(TenantModel, [ModelSetting]) {
/**
* Table name.
*/
@@ -10,10 +11,6 @@ export default class ItemCategory extends TenantModel {
return 'items_categories';
}
static get resourceable() {
return true;
}
/**
* Timestamps columns.
*/
@@ -43,68 +40,23 @@ export default class ItemCategory extends TenantModel {
}
/**
* Item category fields.
* Model modifiers.
*/
static get fields() {
static get modifiers() {
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',
/**
* Inactive/Active mode.
*/
sortByCount(query, order = 'asc') {
query.orderBy('count', order);
},
};
}
static sortCountQuery(query, role) {
query.orderBy('count', role.order);
}
/**
* Model meta.
*/
static get meta() {
return ItemCategorySettings;
}
}

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',
sortCustomQuery: 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 { 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.
*/
@@ -99,52 +101,7 @@ export default class ManualJournal extends TenantModel {
};
}
/**
* Model defined fields.
*/
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',
},
};
static get meta() {
return ManualJournalSettings;
}
}

View File

@@ -0,0 +1,56 @@
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, attribute?:string): IModelMetaField {
const field = get(this.meta.fields, key);
return attribute ? get(field, attribute) : field;
}
/**
* 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,57 @@
export default {
fields: {
customer: {
name: 'Customer',
column: 'customer_id',
fieldType: 'relation',
relationType: 'enumeration',
relationKey: 'customer',
relationEntityLabel: 'name',
relationEntityKey: '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',
fieldType: 'relation',
relationType: 'enumeration',
relationKey: 'depositAccount',
relationEntityLabel: 'name',
relationEntityKey: 'slug',
},
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 ModelSetting from './ModelSetting';
import PaymentReceiveSettings from './PaymentReceive.Settings';
export default class PaymentReceive extends TenantModel {
export default class PaymentReceive extends mixin(TenantModel, [ModelSetting]) {
/**
* Table name.
*/
@@ -75,63 +77,9 @@ export default class PaymentReceive extends TenantModel {
}
/**
* Model defined fields.
*
*/
static get fields() {
return {
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',
},
};
static get meta() {
return PaymentReceiveSettings;
}
}

View File

@@ -0,0 +1,81 @@
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',
fieldType: 'relation',
relationType: 'enumeration',
relationKey: 'customer',
relationEntityLabel: 'display_name',
relationEntityKey: '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',
fieldType: 'enumeration',
options: [
{ name: 'Delivered', key: 'delivered' },
{ name: 'Rejected', key: 'rejected' },
{ name: 'Approved', key: 'approved' },
{ name: 'Delivered', key: 'delivered' },
{ name: 'Draft', key: 'draft' },
],
filterCustomQuery: StatusFieldFilterQuery,
sortCustomQuery: StatusFieldSortQuery,
},
'created_at': {
name: 'Created at',
column: 'created_at',
columnType: 'date',
},
},
};
function StatusFieldSortQuery(query, role) {
query.modify('orderByStatus', role.order);
}
function StatusFieldFilterQuery(query, role) {
query.modify('filterByStatus', role.value);
}

View File

@@ -1,11 +1,11 @@
import moment from 'moment';
import { Model } from 'objection';
import { Model, mixin } from 'objection';
import TenantModel from 'models/TenantModel';
import { defaultToTransform } from 'utils';
import HasItemEntries from 'services/Sales/HasItemsEntries';
import { query } from 'winston';
import SaleEstimateSettings from './SaleEstimate.Settings';
import ModelSetting from './ModelSetting';
export default class SaleEstimate extends TenantModel {
export default class SaleEstimate extends mixin(TenantModel, [ModelSetting]) {
/**
* Table name
*/
@@ -29,9 +29,9 @@ export default class SaleEstimate extends TenantModel {
'isExpired',
'isConvertedToInvoice',
'isApproved',
'isRejected'
'isRejected',
];
}
}
/**
* Detarmines whether the sale estimate converted to sale invoice.
@@ -57,7 +57,7 @@ export default class SaleEstimate extends TenantModel {
return defaultToTransform(
this.expirationDate,
moment().isAfter(this.expirationDate, 'day'),
false,
false
);
}
@@ -123,13 +123,38 @@ export default class SaleEstimate extends TenantModel {
* Filters the approved estimates transactions.
*/
approved(query) {
query.whereNot('approved_at', null)
query.whereNot('approved_at', null);
},
/**
* Sorting the estimates orders by delivery status.
*/
orderByDraft(query, order) {
query.orderByRaw(`delivered_at is null ${order}`)
orderByStatus(query, order) {
query.orderByRaw(`delivered_at is null ${order}`);
},
/**
* Filtering the estimates oreders by status field.
*/
filterByStatus(query, filterType) {
switch (filterType) {
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;
}
}
};
}
@@ -151,7 +176,7 @@ export default class SaleEstimate extends TenantModel {
},
filter(query) {
query.where('contact_service', 'customer');
}
},
},
entries: {
relation: Model.HasManyRelation,
@@ -168,91 +193,9 @@ export default class SaleEstimate extends TenantModel {
}
/**
* Model defined fields.
* Model settings.
*/
static get fields() {
return {
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',
},
};
static get meta() {
return SaleEstimateSettings;
}
}

View File

@@ -0,0 +1,100 @@
export default {
defaultFilterField: 'customer',
defaultSort: {
sortOrder: 'DESC',
sortField: 'created_at',
},
fields: {
customer: {
name: 'Customer',
column: 'customer_id',
fieldType: 'relation',
relationType: 'enumeration',
relationKey: 'customer',
relationEntityLabel: 'display_name',
relationEntityKey: 'id',
},
invoice_date: {
name: 'Invoice date',
column: 'invoice_date',
fieldType: 'date',
},
due_date: {
name: 'Due date',
column: 'due_date',
fieldType: 'date',
},
invoice_no: {
name: 'Invoice No.',
column: 'invoice_no',
fieldType: 'text',
},
reference_no: {
name: 'Reference No.',
column: 'reference_no',
fieldType: 'text',
},
invoice_message: {
name: 'Invoice message',
column: 'invoice_message',
fieldType: 'text',
},
terms_conditions: {
name: 'Terms & conditions',
column: 'terms_conditions',
fieldType: 'text',
},
amount: {
name: 'Invoice amount',
column: 'balance',
fieldType: 'number',
},
payment_amount: {
name: 'Payment amount',
column: 'payment_amount',
fieldType: 'number',
},
due_amount: { // calculated.
name: 'Due amount',
column: 'due_amount',
fieldType: 'number',
virtualColumn: true,
},
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' },
],
filterCustomQuery: StatusFieldFilterQuery,
sortCustomQuery: StatusFieldSortQuery,
},
created_at: {
name: 'Created at',
column: 'created_at',
fieldType: 'date',
},
},
};
/**
* Status field filter custom query.
*/
function StatusFieldFilterQuery(query, role) {
query.modify('statusFilter', role.value);
}
/**
* Status field sort custom query.
*/
function StatusFieldSortQuery(query, role) {
query.modify('sortByStatus', role.order);
}

View File

@@ -1,8 +1,10 @@
import { Model, raw } from 'objection';
import { mixin, Model, raw } from 'objection';
import moment from 'moment';
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
*/
@@ -106,10 +108,6 @@ export default class SaleInvoice extends TenantModel {
return this.getOverdueDays();
}
static get resourceable() {
return true;
}
/**
*
* @param {*} asDate
@@ -229,6 +227,33 @@ export default class SaleInvoice extends TenantModel {
byPrefixAndNumber(query, prefix, number) {
query.where('invoice_no', `${prefix}${number}`);
},
/**
* Status filter.
*/
statusFilter(query, filterType) {
switch (filterType) {
case 'draft':
query.modify('draft');
break;
case 'delivered':
query.modify('delivered');
break;
case 'unpaid':
query.modify('unpaid');
break;
case 'overdue':
default:
query.modify('overdue');
break;
case 'partially-paid':
query.modify('partiallyPaid');
break;
case 'paid':
query.modify('paid');
break;
}
},
};
}
@@ -238,11 +263,14 @@ export default class SaleInvoice extends TenantModel {
static get relationMappings() {
const AccountTransaction = require('models/AccountTransaction');
const ItemEntry = require('models/ItemEntry');
const Contact = require('models/Contact');
const Customer = require('models/Customer');
const InventoryCostLotTracker = require('models/InventoryCostLotTracker');
const PaymentReceiveEntry = require('models/PaymentReceiveEntry');
return {
/**
* Sale invoice associated entries.
*/
entries: {
relation: Model.HasManyRelation,
modelClass: ItemEntry.default,
@@ -255,9 +283,12 @@ export default class SaleInvoice extends TenantModel {
},
},
/**
* Belongs to customer model.
*/
customer: {
relation: Model.BelongsToOneRelation,
modelClass: Contact.default,
modelClass: Customer.default,
join: {
from: 'sales_invoices.customerId',
to: 'contacts.id',
@@ -267,6 +298,9 @@ export default class SaleInvoice extends TenantModel {
},
},
/**
* Invoice has associated account transactions.
*/
transactions: {
relation: Model.HasManyRelation,
modelClass: AccountTransaction.default,
@@ -316,125 +350,13 @@ export default class SaleInvoice extends TenantModel {
}
/**
* Model defined fields.
* Sale invoice meta.
*/
static get fields() {
return {
customer: {
label: 'Customer',
column: 'customer_id',
relation: 'contacts.id',
relationColumn: 'contacts.displayName',
static get meta() {
return SaleInvoiceMeta;
}
fieldType: 'options',
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);
},
},
created_at: {
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) {
case 'draft':
query.modify('draft');
break;
case 'delivered':
query.modify('delivered');
break;
case 'unpaid':
query.modify('unpaid');
break;
case 'overdue':
query.modify('overdue');
break;
case 'partially-paid':
query.modify('partiallyPaid');
break;
case 'paid':
query.modify('paid');
break;
}
},
sortQuery(query, role) {
query.modify('sortByStatus', role.order);
},
},
};
static dueAmountFieldSortQuery(query, role) {
query.modify('sortByDueAmount', role.order);
}
}

View File

@@ -0,0 +1,85 @@
export default {
defaultFilterField: 'receipt_date',
defaultSort: {
sortOrder: 'DESC',
sortField: 'created_at',
},
fields: {
'amount': {
name: 'Amount',
column: 'amount',
fieldType: 'number',
},
'deposit_account': {
column: 'deposit_account_id',
name: 'Deposit account',
fieldType: 'relation',
relationType: 'enumeration',
relationKey: 'depositAccount',
relationEntityLabel: 'name',
relationEntityKey: 'slug',
},
'customer': {
name: 'Customer',
column: 'customer_id',
fieldType: 'relation',
relationType: 'enumeration',
relationKey: 'customer',
relationEntityLabel: 'display_name',
relationEntityKey: 'id',
},
'receipt_date': {
name: 'Receipt date',
column: 'receipt_date',
fieldType: 'date',
},
'receipt_number': {
name: 'Receipt No.',
column: 'receipt_number',
fieldType: 'text',
},
'reference_no': {
name: 'Reference No.',
column: 'reference_no',
fieldType: 'text',
},
'receipt_message': {
name: 'Receipt message',
column: 'receipt_message',
fieldType: 'text',
},
'statement': {
name: 'Statement',
column: 'statement',
fieldType: 'text',
},
'created_at': {
name: 'Created at',
column: 'created_at',
fieldType: 'date',
},
'status': {
name: 'Status',
fieldType: 'enumeration',
options: [
{ key: 'draft', name: 'Draft' },
{ key: 'closed', name: 'Closed' },
],
filterCustomQuery: StatusFieldFilterQuery,
sortCustomQuery: StatusFieldSortQuery,
},
},
};
function StatusFieldFilterQuery(query, role) {
query.modify('filterByStatus', role.value);
}
function StatusFieldSortQuery(query, role) {
query.modify('sortByStatus', role.order);
}

View File

@@ -1,7 +1,9 @@
import { Model, mixin } from 'objection';
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
*/
@@ -16,14 +18,11 @@ export default class SaleReceipt extends TenantModel {
return ['created_at', 'updated_at'];
}
/**
/**
* Virtual attributes.
*/
static get virtualAttributes() {
return [
'isClosed',
'isDraft',
];
return ['isClosed', 'isDraft'];
}
/**
@@ -66,6 +65,21 @@ export default class SaleReceipt extends TenantModel {
*/
sortByStatus(query, order) {
query.orderByRaw(`CLOSED_AT IS NULL ${order}`);
},
/**
* Filtering the receipts orders by status.
*/
filterByStatus(query, status) {
switch (status) {
case 'draft':
query.modify('draft');
break;
case 'closed':
default:
query.modify('closed');
break;
}
}
};
}
@@ -74,7 +88,7 @@ export default class SaleReceipt extends TenantModel {
* Relationship mapping.
*/
static get relationMappings() {
const Contact = require('models/Contact');
const Customer = require('models/Customer');
const Account = require('models/Account');
const AccountTransaction = require('models/AccountTransaction');
const ItemEntry = require('models/ItemEntry');
@@ -82,14 +96,14 @@ export default class SaleReceipt extends TenantModel {
return {
customer: {
relation: Model.BelongsToOneRelation,
modelClass: Contact.default,
modelClass: Customer.default,
join: {
from: 'sales_receipts.customerId',
to: 'contacts.id',
},
filter(query) {
query.where('contact_service', 'customer');
}
},
},
depositAccount: {
@@ -118,95 +132,19 @@ export default class SaleReceipt extends TenantModel {
modelClass: AccountTransaction.default,
join: {
from: 'sales_receipts.id',
to: 'accounts_transactions.referenceId'
to: 'accounts_transactions.referenceId',
},
filter(builder) {
builder.where('reference_type', 'SaleReceipt');
},
}
},
};
}
/**
* Model defined fields.
* Sale invoice meta.
*/
static get fields() {
return {
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);
}
}
};
static get meta() {
return SaleReceiptSettings;
}
}

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 PaginationQueryBuilder from './Pagination';
import ModelSetting from './ModelSetting';
import VendorSettings from './Vendor.Settings';
class VendorQueryBuilder extends PaginationQueryBuilder {
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.
*/
@@ -62,6 +64,13 @@ export default class Vendor extends TenantModel {
*/
static get modifiers() {
return {
/**
* Inactive/Active mode.
*/
inactiveMode(query, active = false) {
query.where('active', !active);
},
/**
* Filters the active customers.
*/
@@ -125,72 +134,7 @@ export default class Vendor extends TenantModel {
};
}
static get fields() {
return {
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',
}
};
static get meta() {
return VendorSettings;
}
}

View File

@@ -1,6 +1,7 @@
import { Inject, Service } from 'typedi';
import { difference, chain, uniq } from 'lodash';
import { kebabCase } from 'lodash';
import R from 'ramda';
import TenancyService from 'services/Tenancy/TenancyService';
import { ServiceError } from 'exceptions';
import {
@@ -606,6 +607,17 @@ export default class AccountsService {
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.
* @param {number} tenantId
@@ -613,21 +625,26 @@ export default class AccountsService {
*/
public async getAccountsList(
tenantId: number,
filter: IAccountsFilter
filterDTO: IAccountsFilter
): Promise<{ accounts: IAccountResponse[]; filterMeta: IFilterMeta }> {
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(
tenantId,
Account,
filter
);
this.logger.info('[accounts] trying to get accounts datatable list.', {
tenantId,
filter,
});
const accounts = await Account.query().onBuild((builder) => {
dynamicList.buildQuery()(builder);
builder.modify('inactiveMode', filter.inactiveMode);
});
return {
@@ -727,10 +744,11 @@ export default class AccountsService {
}));
return flatToNestedArray(
this.i18nService.i18nMapper(_accounts, ['account_type_label'], tenantId),
{
id: 'id',
parentId: 'parent_account_id',
});
{
id: 'id',
parentId: 'parent_account_id',
}
);
}
/**

View File

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

View File

@@ -1,5 +1,6 @@
import { Inject, Service } from 'typedi';
import { intersection, defaultTo } from 'lodash';
import { defaultTo } from 'lodash';
import * as R from 'ramda';
import {
EventDispatcher,
EventDispatcherInterface,
@@ -255,6 +256,12 @@ export default class VendorsService {
);
}
private parseVendorsListFilterDTO(filterDTO) {
return R.compose(
this.dynamicListService.parseStringifiedFilter
)(filterDTO);
}
/**
* Retrieve vendors datatable list.
* @param {number} tenantId - Tenant id.
@@ -262,7 +269,7 @@ export default class VendorsService {
*/
public async getVendorsList(
tenantId: number,
vendorsFilter: IVendorsFilter
filterDTO: IVendorsFilter
): Promise<{
vendors: IVendor[];
pagination: IPaginationMeta;
@@ -270,21 +277,28 @@ export default class VendorsService {
}> {
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,
Vendor,
vendorsFilter
filter
);
// Vendors list.
const { results, pagination } = await Vendor.query()
.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 {
vendors: results,
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,90 @@
import { Service, Inject } from 'typedi';
import validator from 'is-my-json-valid';
import { Request, Response, NextFunction } from 'express';
import { castArray, isEmpty } from 'lodash';
import { ServiceError } from 'exceptions';
import { DynamicFilter } from 'lib/DynamicFilter';
import {
DynamicFilter,
DynamicFilterSortBy,
DynamicFilterViews,
DynamicFilterFilterRoles,
} from 'lib/DynamicFilter';
import {
validateFieldKeyExistance,
validateFilterRolesFieldsExistance,
} from 'lib/ViewRolesBuilder';
import {
IDynamicListFilterDTO,
IFilterRole,
IDynamicListFilter,
IDynamicListService,
IFilterRole,
IModel,
} from 'interfaces';
import TenancyService from 'services/Tenancy/TenancyService';
const ERRORS = {
VIEW_NOT_FOUND: 'view_not_found',
SORT_COLUMN_NOT_FOUND: 'sort_column_not_found',
FILTER_ROLES_FIELDS_NOT_FOUND: 'filter_roles_fields_not_found',
};
import DynamicListFilterRoles from './DynamicListFilterRoles';
import DynamicListSortBy from './DynamicListSortBy';
import DynamicListCustomView from './DynamicListCustomView';
@Service()
export default class DynamicListService implements IDynamicListService {
@Inject()
tenancy: TenancyService;
/**
* Retreive custom view or throws error not found.
* @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');
@Inject()
dynamicListFilterRoles: DynamicListFilterRoles;
if (!view || view.resourceModel !== model.name) {
throw new ServiceError(ERRORS.VIEW_NOT_FOUND);
}
return view;
}
@Inject()
dynamicListSortBy: DynamicListSortBy;
@Inject()
dynamicListView: DynamicListCustomView;
/**
* Validates the sort column whether exists.
* @param {IModel} model
* @param {string} columnSortBy - Sort column
* @throws {ServiceError}
* Parses filter DTO.
* @param {IMode} model -
* @param {} filterDTO -
*/
private validateSortColumnExistance(model: any, columnSortBy: string) {
const notExistsField = validateFieldKeyExistance(model, columnSortBy);
if (!notExistsField) {
throw new ServiceError(ERRORS.SORT_COLUMN_NOT_FOUND);
}
}
/**
* 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');
}
}
private parseFilterObject = (model, filterDTO) => {
return {
// Merges the default properties with filter object.
...model.defaultSort ? {
sortOrder: model.defaultSort.sortOrder,
columnSortBy: model.defaultSort.sortOrder,
} : {},
...filterDTO,
};
};
/**
* Dynamic listing.
* @param {number} tenantId - Tenant id.
* @param {IModel} model - Model.
* @param {IDynamicListFilterDTO} filter - Dynamic filter DTO.
* @param {IDynamicListFilter} filter - Dynamic filter DTO.
*/
public async dynamicList(
public dynamicList = async (
tenantId: number,
model: IModel,
filter: IDynamicListFilterDTO
) {
filter: IDynamicListFilter
) => {
const dynamicFilter = new DynamicFilter(model);
// Custom view filter roles.
if (filter.customViewId) {
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);
// Parses the filter object.
const parsedFilter = this.parseFilterObject(model, filter);
const sortByFilter = new DynamicFilterSortBy(
filter.columnSortBy,
filter.sortOrder
// Custom view filter roles.
// if (filter.customViewId) {
// 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.
if (filter.filterRoles.length > 0) {
const filterRoles = filter.filterRoles.map((filterRole, index) => ({
...filterRole,
index: index + 1,
}));
this.validateFilterRolesSchema(filterRoles);
this.validateRolesFieldsExistance(model, filterRoles);
// Validate the model resource fields.
const dynamicFilterRoles = new DynamicFilterFilterRoles(filterRoles);
if (!isEmpty(parsedFilter.filterRoles)) {
const dynamicFilterRoles = this.dynamicListFilterRoles.dynamicList(
model,
parsedFilter.filterRoles
);
dynamicFilter.setFilter(dynamicFilterRoles);
}
return dynamicFilter;
}
};
/**
* Middleware to catch services errors
@@ -173,25 +102,62 @@ export default class DynamicListService implements IDynamicListService {
if (error instanceof ServiceError) {
if (error.errorType === 'sort_column_not_found') {
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') {
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') {
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') {
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);
}
/**
* 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 { difference, sumBy, omit, map } from 'lodash';
import { difference, sumBy, omit } from 'lodash';
import moment from 'moment';
import * as R from 'ramda';
import {
EventDispatcher,
EventDispatcherInterface,
@@ -630,6 +631,16 @@ export default class ExpensesService implements IExpensesService {
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.
* @param {number} tenantId
@@ -638,35 +649,41 @@ export default class ExpensesService implements IExpensesService {
*/
public async getExpensesList(
tenantId: number,
expensesFilter: IExpensesFilter
filterDTO: IExpensesFilter
): Promise<{
expenses: IExpense[];
pagination: IPaginationMeta;
filterMeta: IFilterMeta;
}> {
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,
Expense,
expensesFilter
filter,
);
this.logger.info('[expense] trying to get expenses datatable list.', {
tenantId,
expensesFilter,
filter,
});
const { results, pagination } = await Expense.query()
.onBuild((builder) => {
builder.withGraphFetched('paymentAccount');
builder.withGraphFetched('categories.expenseAccount');
dynamicFilter.buildQuery()(builder);
dynamicList.buildQuery()(builder);
})
.pagination(expensesFilter.page - 1, expensesFilter.pageSize);
.pagination(filter.page - 1, filter.pageSize);
return {
expenses: results,
pagination,
filterMeta: dynamicFilter.getResponseMeta(),
filterMeta: dynamicList.getResponseMeta(),
};
}

View File

@@ -29,7 +29,6 @@ export default class JournalSheetService {
fromRange: null,
toRange: null,
accountsIds: [],
transactionTypes: [],
numberFormat: {
noCents: false,
divideOn1000: false,
@@ -107,6 +106,13 @@ export default class JournalSheetService {
}
query.modify('filterDateRange', filter.fromDate, filter.toDate);
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.
const transactionsJournal = Journal.fromTransactions(

View File

@@ -0,0 +1,79 @@
import {
IAccount,
IAccountTransaction,
INumberFormatQuery,
ITransactionsByReferenceQuery,
ITransactionsByReferenceTransaction,
} from 'interfaces';
import FinancialSheet from '../FinancialSheet';
export default class TransactionsByReference extends FinancialSheet {
readonly transactions: IAccountTransaction[];
readonly query: ITransactionsByReferenceQuery;
readonly baseCurrency: string;
readonly numberFormat: INumberFormatQuery;
/**
* Constructor method.
* @param {IAccountTransaction[]} transactions
* @param {ITransactionsByReferenceQuery} query
* @param {string} baseCurrency
*/
constructor(
transactions: (IAccountTransaction & { account: IAccount }) [],
query: ITransactionsByReferenceQuery,
baseCurrency: string
) {
super();
this.transactions = transactions;
this.query = query;
this.baseCurrency = baseCurrency;
this.numberFormat = this.query.numberFormat;
}
/**
* Mappes the given account transaction to report transaction.
* @param {IAccountTransaction} transaction
* @returns {ITransactionsByReferenceTransaction}
*/
private transactionMapper = (
transaction: IAccountTransaction
): ITransactionsByReferenceTransaction => {
return {
credit: this.getAmountMeta(transaction.credit, { money: true }),
debit: this.getAmountMeta(transaction.debit, { money: true }),
referenceTypeFormatted: transaction.referenceTypeFormatted,
referenceType: transaction.referenceType,
referenceId: transaction.referenceId,
contactId: transaction.contactId,
contactType: transaction.contactType,
contactTypeFormatted: transaction.contactType,
accountName: transaction.account.name,
accountCode: transaction.account.code,
accountId: transaction.accountId,
};
};
/**
* Mappes the given accounts transactions to report transactions.
* @param {IAccountTransaction} transaction
* @returns {ITransactionsByReferenceTransaction}
*/
private transactionsMapper = (
transactions: IAccountTransaction[]
): ITransactionsByReferenceTransaction[] => {
return transactions.map(this.transactionMapper);
};
/**
* Retrieve the report data.
* @returns {ITransactionsByReferenceTransaction}
*/
public reportData(): ITransactionsByReferenceTransaction[] {
return this.transactionsMapper(this.transactions);
}
}

View File

@@ -0,0 +1,29 @@
import HasTenancyService from 'services/Tenancy/TenancyService';
import { Service, Inject } from 'typedi';
import { IAccount, IAccountTransaction, ITransactionsByReferenceQuery } from 'interfaces';
@Service()
export default class TransactionsByReferenceRepository {
@Inject()
tenancy: HasTenancyService;
/**
* Retrieve the accounts transactions of the givne reference id and type.
* @param {number} tenantId -
* @param {number} referenceId - Reference id.
* @param {string} referenceType - Reference type.
* @return {Promise<IAccountTransaction[]>}
*/
public getTransactions(
tenantId: number,
referenceId: number,
referenceType: string,
): Promise<(IAccountTransaction & { account: IAccount }) []> {
const { AccountTransaction } = this.tenancy.models(tenantId);
return AccountTransaction.query()
.where('reference_id', referenceId)
.where('reference_type', referenceType)
.withGraphFetched('account');
}
}

View File

@@ -0,0 +1,77 @@
import { Service, Inject } from 'typedi';
import HasTenancyService from 'services/Tenancy/TenancyService';
import {
ITransactionsByReferenceQuery,
ITransactionsByReferenceTransaction,
} from 'interfaces';
import TransactionsByReferenceRepository from './TransactionsByReferenceRepository';
import TransactionsByReferenceReport from './TransactionsByReferenceReport';
@Service()
export default class TransactionsByReferenceService {
@Inject()
tenancy: HasTenancyService;
@Inject('logger')
logger: any;
@Inject()
reportRepository: TransactionsByReferenceRepository;
/**
* Default query of transactions by reference report.
*/
get defaultQuery(): ITransactionsByReferenceQuery {
return {
numberFormat: {
precision: 2,
divideOn1000: false,
showZero: false,
formatMoney: 'total',
negativeFormat: 'mines',
},
};
}
/**
* Retrieve accounts transactions by given reference id and type.
* @param {number} tenantId
* @param {ITransactionsByReferenceQuery} filter
*/
public async getTransactionsByReference(
tenantId: number,
query: ITransactionsByReferenceQuery
): Promise<{
data: ITransactionsByReferenceTransaction[];
}> {
const filter = {
...this.defaultQuery,
...query,
};
// Retrieve the accounts transactions of the given reference.
const transactions = await this.reportRepository.getTransactions(
tenantId,
filter.referenceId,
filter.referenceType
);
// Settings tenant service.
const settings = this.tenancy.settings(tenantId);
const baseCurrency = settings.get({
group: 'organization',
key: 'base_currency',
});
// Transactions by reference report.
const report = new TransactionsByReferenceReport(
transactions,
filter,
baseCurrency
);
return {
data: report.reportData(),
};
}
}

View File

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

View File

@@ -1,5 +1,6 @@
import { Inject } from 'typedi';
import { difference } from 'lodash';
import * as R from 'ramda';
import {
EventDispatcher,
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.
* @param {number} tenantId
@@ -384,16 +397,22 @@ export default class ItemCategoriesService implements IItemCategoriesService {
*/
public async getItemCategoriesList(
tenantId: number,
filter: IItemCategoriesFilter,
filterDTO: IItemCategoriesFilter,
authorizedUser: ISystemUser
): Promise<{ itemCategories: IItemCategory[]; filterMeta: IFilterMeta }> {
const { ItemCategory } = this.tenancy.models(tenantId);
// Parses list filter DTO.
const filter = this.parsesListFilterDTO(filterDTO);
// Dynamic list service.
const dynamicList = await this.dynamicListService.dynamicList(
tenantId,
ItemCategory,
filter
);
// Items categories.
const itemCategories = await ItemCategory.query().onBuild((query) => {
// Subquery to calculate sumation of assocaited items to the item category.
query.select('*', ItemCategory.relatedQuery('items').count().as('count'));

View File

@@ -1,5 +1,6 @@
import { defaultTo, difference } from 'lodash';
import { defaultTo } from 'lodash';
import { Service, Inject } from 'typedi';
import * as R from 'ramda';
import {
EventDispatcher,
EventDispatcherInterface,
@@ -525,20 +526,37 @@ export default class ItemsService implements IItemsService {
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.
* @param {number} tenantId
* @param {IItemsFilter} itemsFilter
*/
public async itemsList(tenantId: number, itemsFilter: IItemsFilter) {
public async itemsList(tenantId: number, filterDTO: IItemsFilter) {
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(
tenantId,
Item,
itemsFilter
filter
);
const { results: items, pagination } = await Item.query()
.onBuild((builder) => {
builder.modify('inactiveMode', filter.inactiveMode);
builder.withGraphFetched('inventoryAccount');
builder.withGraphFetched('sellAccount');
builder.withGraphFetched('costAccount');
@@ -546,7 +564,7 @@ export default class ItemsService implements IItemsService {
dynamicFilter.buildQuery()(builder);
})
.pagination(itemsFilter.page - 1, itemsFilter.pageSize);
.pagination(filter.page - 1, filter.pageSize);
const results = items.map((item) =>
this.transformItemToResponse(tenantId, item)

View File

@@ -1,7 +1,8 @@
import { difference, sumBy, omit, map } from 'lodash';
import { Service, Inject } from 'typedi';
import moment from 'moment';
import { ServiceError, ServiceErrors } from 'exceptions';
import * as R from 'ramda';
import { ServiceError } from 'exceptions';
import {
IManualJournalDTO,
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.
* @param {number} tenantId
* @param {IManualJournalsFilter} filter
* @param {number} tenantId -
* @param {IManualJournalsFilter} filter -
*/
public async getManualJournals(
tenantId: number,
filter: IManualJournalsFilter
filterDTO: IManualJournalsFilter
): Promise<{
manualJournals: IManualJournal;
pagination: IPaginationMeta;
filterMeta: IFilterMeta;
}> {
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,
ManualJournal,
filter
);
this.logger.info('[manual_journals] trying to get manual journals list.', {
tenantId,
filter,
});
const { results, pagination } = await ManualJournal.query()
.onBuild((builder) => {
dynamicList.buildQuery()(builder);
dynamicService.buildQuery()(builder);
builder.withGraphFetched('entries.account');
})
.pagination(filter.page - 1, filter.pageSize);
@@ -802,7 +817,7 @@ export default class ManualJournalsService implements IManualJournalsService {
return {
manualJournals: results,
pagination,
filterMeta: dynamicList.getResponseMeta(),
filterMeta: dynamicService.getResponseMeta(),
};
}

View File

@@ -1,5 +1,6 @@
import { Inject, Service } from 'typedi';
import { omit, sumBy, difference } from 'lodash';
import { sumBy, difference } from 'lodash';
import * as R from 'ramda';
import {
EventDispatcher,
EventDispatcherInterface,
@@ -25,7 +26,7 @@ import TenancyService from 'services/Tenancy/TenancyService';
import DynamicListingService from 'services/DynamicListing/DynamicListService';
import { entriesAmountDiff, formatDateFields } from 'utils';
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 { 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.
* @param {number} tenantId
@@ -642,34 +649,40 @@ export default class BillPaymentsService implements IBillPaymentsService {
*/
public async listBillPayments(
tenantId: number,
billPaymentsFilter: IBillPaymentsFilter
filterDTO: IBillPaymentsFilter
): Promise<{
billPayments: IBillPayment;
pagination: IPaginationMeta;
filterMeta: IFilterMeta;
}> {
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,
BillPayment,
billPaymentsFilter
filter
);
this.logger.info('[bill_payment] try to get bill payments list.', {
tenantId,
});
const { results, pagination } = await BillPayment.query()
.onBuild((builder) => {
builder.withGraphFetched('vendor');
builder.withGraphFetched('paymentAccount');
dynamicFilter.buildQuery()(builder);
dynamicList.buildQuery()(builder);
})
.pagination(billPaymentsFilter.page - 1, billPaymentsFilter.pageSize);
.pagination(filter.page - 1, filter.pageSize);
return {
billPayments: results,
pagination,
filterMeta: dynamicFilter.getResponseMeta(),
filterMeta: dynamicList.getResponseMeta(),
};
}

View File

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

View File

@@ -2,7 +2,7 @@ import { Service, Inject } from 'typedi';
import { camelCase, upperFirst } from 'lodash';
import pluralize from 'pluralize';
import { buildFilter } from 'objection-filter';
import { IModel } from 'interfaces';
import { IModel, IModelMeta } from 'interfaces';
import {
getModelFields,
} from 'lib/ViewRolesBuilder'
@@ -102,4 +102,18 @@ export default class ResourceService {
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 { Service, Inject } from 'typedi';
import * as R from 'ramda';
import {
EventDispatcher,
EventDispatcherInterface,
@@ -613,6 +614,16 @@ export default class PaymentReceiveService implements IPaymentsReceiveService {
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.
* @param {number} tenantId
@@ -620,33 +631,39 @@ export default class PaymentReceiveService implements IPaymentsReceiveService {
*/
public async listPaymentReceives(
tenantId: number,
paymentReceivesFilter: IPaymentReceivesFilter
filterDTO: IPaymentReceivesFilter
): Promise<{
paymentReceives: IPaymentReceive[];
pagination: IPaginationMeta;
filterMeta: IFilterMeta;
}> {
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,
PaymentReceive,
paymentReceivesFilter
filter
);
const { results, pagination } = await PaymentReceive.query()
.onBuild((builder) => {
builder.withGraphFetched('customer');
builder.withGraphFetched('depositAccount');
dynamicFilter.buildQuery()(builder);
dynamicList.buildQuery()(builder);
})
.pagination(
paymentReceivesFilter.page - 1,
paymentReceivesFilter.pageSize
filter.page - 1,
filter.pageSize
);
return {
paymentReceives: results,
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 * as R from 'ramda';
import {
IEstimatesFilter,
IFilterMeta,
@@ -412,6 +413,16 @@ export default class SaleEstimateService implements ISalesEstimatesService{
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.
* @param {number} tenantId -
@@ -419,17 +430,22 @@ export default class SaleEstimateService implements ISalesEstimatesService{
*/
public async estimatesList(
tenantId: number,
estimatesFilter: IEstimatesFilter
filterDTO: IEstimatesFilter
): Promise<{
salesEstimates: ISaleEstimate[];
pagination: IPaginationMeta;
filterMeta: IFilterMeta;
}> {
const { SaleEstimate } = this.tenancy.models(tenantId);
// Parses filter DTO.
const filter = this.parseListFilterDTO(filterDTO);
// Dynamic list service.
const dynamicFilter = await this.dynamicListService.dynamicList(
tenantId,
SaleEstimate,
estimatesFilter
filter,
);
const { results, pagination } = await SaleEstimate.query()
@@ -438,7 +454,7 @@ export default class SaleEstimateService implements ISalesEstimatesService{
builder.withGraphFetched('entries');
dynamicFilter.buildQuery()(builder);
})
.pagination(estimatesFilter.page - 1, estimatesFilter.pageSize);
.pagination(filter.page - 1, filter.pageSize);
return {
salesEstimates: results,

View File

@@ -1,5 +1,6 @@
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 composeAsync from 'async/compose';
import {
@@ -647,6 +648,17 @@ export default class SaleInvoicesService implements ISalesInvoicesService {
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.
* @param {Request} req
@@ -655,22 +667,27 @@ export default class SaleInvoicesService implements ISalesInvoicesService {
*/
public async salesInvoicesList(
tenantId: number,
salesInvoicesFilter: ISalesInvoicesFilter
filterDTO: ISalesInvoicesFilter
): Promise<{
salesInvoices: ISaleInvoice[];
pagination: IPaginationMeta;
filterMeta: IFilterMeta;
}> {
const { SaleInvoice } = this.tenancy.models(tenantId);
// Parses stringified filter roles.
const filter = this.parseListFilterDTO(filterDTO);
// Dynamic list service.
const dynamicFilter = await this.dynamicListService.dynamicList(
tenantId,
SaleInvoice,
salesInvoicesFilter
filter
);
this.logger.info('[sale_invoice] try to get sales invoices list.', {
tenantId,
salesInvoicesFilter,
filter,
});
const { results, pagination } = await SaleInvoice.query()
.onBuild((builder) => {
@@ -678,7 +695,7 @@ export default class SaleInvoicesService implements ISalesInvoicesService {
builder.withGraphFetched('customer');
dynamicFilter.buildQuery()(builder);
})
.pagination(salesInvoicesFilter.page - 1, salesInvoicesFilter.pageSize);
.pagination(filter.page - 1, filter.pageSize);
return {
salesInvoices: results,

View File

@@ -1,6 +1,7 @@
import { omit, sumBy } from 'lodash';
import { Service, Inject } from 'typedi';
import moment from 'moment';
import * as R from 'ramda';
import {
EventDispatcher,
EventDispatcherInterface,
@@ -406,6 +407,16 @@ export default class SalesReceiptService implements ISalesReceiptsService {
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.
* @param {number} tenantId
@@ -413,17 +424,22 @@ export default class SalesReceiptService implements ISalesReceiptsService {
*/
public async salesReceiptsList(
tenantId: number,
salesReceiptsFilter: ISaleReceiptFilter
filterDTO: ISaleReceiptFilter
): Promise<{
salesReceipts: ISaleReceipt[];
pagination: IPaginationMeta;
filterMeta: IFilterMeta;
}> {
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(
tenantId,
SaleReceipt,
salesReceiptsFilter
filter,
);
this.logger.info('[sale_receipt] try to get sales receipts list.', {
@@ -437,7 +453,7 @@ export default class SalesReceiptService implements ISalesReceiptsService {
dynamicFilter.buildQuery()(builder);
})
.pagination(salesReceiptsFilter.page - 1, salesReceiptsFilter.pageSize);
.pagination(filter.page - 1, filter.pageSize);
return {
salesReceipts: results,