refactoring: custom views service.

fix: constraints of delete item from storage.
fix: constraints of delete item category from storage.
fix: localize database seeds files.
fix: view meta data in accounts list response.
This commit is contained in:
Ahmed Bouhuolia
2020-10-05 19:09:56 +02:00
parent 0114ed9f8b
commit 99e6fe273f
64 changed files with 1593 additions and 1103 deletions

View File

@@ -127,18 +127,12 @@ export default class AccountsController extends BaseController{
]; ];
} }
/**
* Account param schema validation.
*/
get accountParamSchema() { get accountParamSchema() {
return [ return [
param('id').exists().isNumeric().toInt() param('id').exists().isNumeric().toInt()
]; ];
} }
/**
* Accounts list schema validation.
*/
get accountsListSchema() { get accountsListSchema() {
return [ return [
query('custom_view_id').optional().isNumeric().toInt(), query('custom_view_id').optional().isNumeric().toInt(),
@@ -149,9 +143,6 @@ export default class AccountsController extends BaseController{
]; ];
} }
/**
*
*/
get bulkSelectIdsQuerySchema() { get bulkSelectIdsQuerySchema() {
return [ return [
query('ids').isArray({ min: 2 }), query('ids').isArray({ min: 2 }),
@@ -328,8 +319,12 @@ export default class AccountsController extends BaseController{
filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles); filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles);
} }
try { try {
const accounts = await this.accountsService.getAccountsList(tenantId, filter); const { accounts, filterMeta } = await this.accountsService.getAccountsList(tenantId, filter);
return res.status(200).send({ accounts });
return res.status(200).send({
accounts,
filter_meta: this.transfromToResponse(filterMeta)
});
} catch (error) { } catch (error) {
next(error); next(error);
} }
@@ -358,9 +353,8 @@ export default class AccountsController extends BaseController{
} }
if (error.errorType === 'account_type_not_found') { if (error.errorType === 'account_type_not_found') {
return res.boom.badRequest( return res.boom.badRequest(
'The given account type not found.', { 'The given account type not found.',
errors: [{ type: 'ACCOUNT_TYPE_NOT_FOUND', code: 200 }] { errors: [{ type: 'ACCOUNT_TYPE_NOT_FOUND', code: 200 }] }
}
); );
} }
if (error.errorType === 'account_type_not_allowed_to_changed') { if (error.errorType === 'account_type_not_allowed_to_changed') {

View File

@@ -1,6 +1,6 @@
import { Response, Request, NextFunction } from 'express'; import { Response, Request, NextFunction } from 'express';
import { matchedData, validationResult } from "express-validator"; import { matchedData, validationResult } from "express-validator";
import { camelCase, omit } from "lodash"; import { camelCase, snakeCase, omit } from "lodash";
import { mapKeysDeep } from 'utils' import { mapKeysDeep } from 'utils'
export default class BaseController { export default class BaseController {
@@ -55,4 +55,12 @@ export default class BaseController {
} }
next(); next();
} }
/**
* Transform the given data to response.
* @param {any} data
*/
transfromToResponse(data: any) {
return mapKeysDeep(data, (v, k) => snakeCase(k));
}
} }

View File

@@ -46,6 +46,12 @@ export default class CustomersController extends ContactsController {
this.validationResult, this.validationResult,
asyncMiddleware(this.deleteBulkCustomers.bind(this)) asyncMiddleware(this.deleteBulkCustomers.bind(this))
); );
router.get('/', [
],
this.validationResult,
asyncMiddleware(this.getCustomersList.bind(this))
);
router.get('/:id', [ router.get('/:id', [
...this.specificContactSchema, ...this.specificContactSchema,
], ],
@@ -193,4 +199,15 @@ export default class CustomersController extends ContactsController {
next(error); next(error);
} }
} }
async getCustomersList(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
try {
await this.customersService.getCustomersList(tenantId)
} catch (error) {
next(error);
}
}
} }

View File

@@ -30,7 +30,8 @@ export default class ExpensesController extends BaseController {
asyncMiddleware(this.newExpense.bind(this)), asyncMiddleware(this.newExpense.bind(this)),
this.catchServiceErrors, this.catchServiceErrors,
); );
router.post('/publish', [ router.post(
'/publish', [
...this.bulkSelectSchema, ...this.bulkSelectSchema,
], ],
this.bulkPublishExpenses.bind(this), this.bulkPublishExpenses.bind(this),
@@ -69,7 +70,9 @@ export default class ExpensesController extends BaseController {
this.catchServiceErrors, this.catchServiceErrors,
); );
router.get( router.get(
'/', '/', [
...this.expensesListSchema,
],
asyncMiddleware(this.getExpensesList.bind(this)), asyncMiddleware(this.getExpensesList.bind(this)),
this.dynamicListService.handlerErrorsToResponse, this.dynamicListService.handlerErrorsToResponse,
this.catchServiceErrors, this.catchServiceErrors,
@@ -89,6 +92,7 @@ export default class ExpensesController extends BaseController {
check('currency_code').optional(), check('currency_code').optional(),
check('exchange_rate').optional().isNumeric().toFloat(), check('exchange_rate').optional().isNumeric().toFloat(),
check('publish').optional().isBoolean().toBoolean(), check('publish').optional().isBoolean().toBoolean(),
check('categories').exists().isArray({ min: 1 }), check('categories').exists().isArray({ min: 1 }),
check('categories.*.index').exists().isNumeric().toInt(), check('categories.*.index').exists().isNumeric().toInt(),
check('categories.*.expense_account_id').exists().isNumeric().toInt(), check('categories.*.expense_account_id').exists().isNumeric().toInt(),
@@ -120,6 +124,20 @@ export default class ExpensesController extends BaseController {
]; ];
} }
get expensesListSchema() {
return [
query('custom_view_id').optional().isNumeric().toInt(),
query('stringified_filter_roles').optional().isJSON(),
query('column_sort_by').optional(),
query('sort_order').optional().isIn(['desc', 'asc']),
query('page').optional().isNumeric().toInt(),
query('page_size').optional().isNumeric().toInt(),
];
}
/** /**
* Creates a new expense on * Creates a new expense on
* @param {Request} req * @param {Request} req
@@ -240,12 +258,23 @@ export default class ExpensesController extends BaseController {
const filter = { const filter = {
filterRoles: [], filterRoles: [],
sortOrder: 'asc', sortOrder: 'asc',
columnSortBy: 'created_at',
page: 1,
pageSize: 12,
...this.matchedQueryData(req), ...this.matchedQueryData(req),
}; };
if (filter.stringifiedFilterRoles) {
filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles);
}
try { try {
const expenses = await this.expensesService.getExpensesList(tenantId, filter); const { expenses, pagination, filterMeta } = await this.expensesService.getExpensesList(tenantId, filter);
return res.status(200).send({ expenses });
return res.status(200).send({
expenses,
pagination: this.transfromToResponse(pagination),
filter_meta: this.transfromToResponse(filterMeta),
});
} catch (error) { } catch (error) {
next(error); next(error);
} }
@@ -259,29 +288,34 @@ export default class ExpensesController extends BaseController {
catchServiceErrors(error: Error, req: Request, res: Response, next: NextFunction) { catchServiceErrors(error: Error, req: Request, res: Response, next: NextFunction) {
if (error instanceof ServiceError) { if (error instanceof ServiceError) {
if (error.errorType === 'expense_not_found') { if (error.errorType === 'expense_not_found') {
return res.boom.badRequest(null, { return res.boom.badRequest(
errors: [{ type: 'EXPENSE_NOT_FOUND', code: 100 }], 'Expense not found.',
}); { errors: [{ type: 'EXPENSE_NOT_FOUND', code: 100 }] }
);
} }
if (error.errorType === 'total_amount_equals_zero') { if (error.errorType === 'total_amount_equals_zero') {
return res.boom.badRequest(null, { return res.boom.badRequest(
errors: [{ type: 'TOTAL.AMOUNT.EQUALS.ZERO', code: 200 }], 'Expense total should not equal zero.',
}); { errors: [{ type: 'TOTAL.AMOUNT.EQUALS.ZERO', code: 200 }] },
);
} }
if (error.errorType === 'payment_account_not_found') { if (error.errorType === 'payment_account_not_found') {
return res.boom.badRequest(null, { return res.boom.badRequest(
errors: [{ type: 'PAYMENT.ACCOUNT.NOT.FOUND', code: 300 }], 'Payment account not found.',
}); { errors: [{ type: 'PAYMENT.ACCOUNT.NOT.FOUND', code: 300 }] },
);
} }
if (error.errorType === 'some_expenses_not_found') { if (error.errorType === 'some_expenses_not_found') {
return res.boom.badRequest(null, { return res.boom.badRequest(
errors: [{ type: 'SOME.EXPENSE.ACCOUNTS.NOT.FOUND', code: 400 }] 'Some expense accounts not found.',
}) { errors: [{ type: 'SOME.EXPENSE.ACCOUNTS.NOT.FOUND', code: 400 }] },
);
} }
if (error.errorType === 'payment_account_has_invalid_type') { if (error.errorType === 'payment_account_has_invalid_type') {
return res.boom.badRequest(null, { return res.boom.badRequest(
errors: [{ type: 'PAYMENT.ACCOUNT.HAS.INVALID.TYPE', code: 500 }], 'Payment account has invalid type.',
}); { errors: [{ type: 'PAYMENT.ACCOUNT.HAS.INVALID.TYPE', code: 500 }], },
);
} }
if (error.errorType === 'expenses_account_has_invalid_type') { if (error.errorType === 'expenses_account_has_invalid_type') {
return res.boom.badRequest(null, { return res.boom.badRequest(null, {

View File

@@ -10,12 +10,16 @@ import asyncMiddleware from 'api/middleware/asyncMiddleware';
import { IItemCategoryOTD } from 'interfaces'; import { IItemCategoryOTD } from 'interfaces';
import { ServiceError } from 'exceptions'; import { ServiceError } from 'exceptions';
import BaseController from 'api/controllers/BaseController'; import BaseController from 'api/controllers/BaseController';
import DynamicListingService from 'services/DynamicListing/DynamicListService';
@Service() @Service()
export default class ItemsCategoriesController extends BaseController { export default class ItemsCategoriesController extends BaseController {
@Inject() @Inject()
itemCategoriesService: ItemCategoriesService; itemCategoriesService: ItemCategoriesService;
@Inject()
dynamicListService: DynamicListingService;
/** /**
* Router constructor method. * Router constructor method.
*/ */
@@ -64,6 +68,7 @@ export default class ItemsCategoriesController extends BaseController {
this.validationResult, this.validationResult,
asyncMiddleware(this.getList.bind(this)), asyncMiddleware(this.getList.bind(this)),
this.handlerServiceError, this.handlerServiceError,
this.dynamicListService.handlerErrorsToResponse,
); );
return router; return router;
} }
@@ -112,8 +117,9 @@ export default class ItemsCategoriesController extends BaseController {
*/ */
get categoriesListValidationSchema() { get categoriesListValidationSchema() {
return [ return [
query('column_sort_order').optional().trim().escape(), query('column_sort_by').optional().trim().escape(),
query('sort_order').optional().trim().escape().isIn(['desc', 'asc']), query('sort_order').optional().trim().escape().isIn(['desc', 'asc']),
query('stringified_filter_roles').optional().isJSON(), query('stringified_filter_roles').optional().isJSON(),
]; ];
} }
@@ -189,13 +195,21 @@ export default class ItemsCategoriesController extends BaseController {
*/ */
async getList(req: Request, res: Response, next: NextFunction) { async getList(req: Request, res: Response, next: NextFunction) {
const { tenantId, user } = req; const { tenantId, user } = req;
const itemCategoriesFilter = this.matchedQueryData(req); const itemCategoriesFilter = {
filterRoles: [],
sortOrder: 'asc',
columnSortBy: 'created_at',
...this.matchedQueryData(req),
};
try { try {
const itemCategories = await this.itemCategoriesService.getItemCategoriesList( const { itemCategories, filterMeta } = await this.itemCategoriesService.getItemCategoriesList(
tenantId, itemCategoriesFilter, user, tenantId, itemCategoriesFilter, user,
); );
return res.status(200).send({ item_categories: itemCategories }); return res.status(200).send({
item_categories: itemCategories,
filter_meta: this.transfromToResponse(filterMeta),
});
} catch (error) { } catch (error) {
next(error); next(error);
} }

View File

@@ -149,10 +149,12 @@ export default class ItemsController extends BaseController {
*/ */
get validateListQuerySchema() { get validateListQuerySchema() {
return [ return [
query('column_sort_order').optional().trim().escape(), query('column_sort_by').optional().trim().escape(),
query('sort_order').optional().isIn(['desc', 'asc']), query('sort_order').optional().isIn(['desc', 'asc']),
query('page').optional().isNumeric().toInt(), query('page').optional().isNumeric().toInt(),
query('page_size').optional().isNumeric().toInt(), query('page_size').optional().isNumeric().toInt(),
query('custom_view_id').optional().isNumeric().toInt(), query('custom_view_id').optional().isNumeric().toInt(),
query('stringified_filter_roles').optional().isJSON(), query('stringified_filter_roles').optional().isJSON(),
] ]
@@ -239,14 +241,22 @@ export default class ItemsController extends BaseController {
const filter = { const filter = {
filterRoles: [], filterRoles: [],
sortOrder: 'asc', sortOrder: 'asc',
columnSortBy: 'created_at',
page: 1,
pageSize: 12,
...this.matchedQueryData(req), ...this.matchedQueryData(req),
}; };
if (filter.stringifiedFilterRoles) { if (filter.stringifiedFilterRoles) {
filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles); filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles);
} }
try { try {
const items = await this.itemsService.itemsList(tenantId, filter); const { items, pagination, filterMeta } = await this.itemsService.itemsList(tenantId, filter);
return res.status(200).send({ items });
return res.status(200).send({
items,
pagination: this.transfromToResponse(pagination),
filter_meta: this.transfromToResponse(filterMeta),
});
} catch (error) { } catch (error) {
next(error); next(error);
} }
@@ -344,6 +354,16 @@ export default class ItemsController extends BaseController {
errors: [{ type: 'INVENTORY.ACCOUNT.NOT.CURRENT.ASSET', code: 300 }], errors: [{ type: 'INVENTORY.ACCOUNT.NOT.CURRENT.ASSET', code: 300 }],
}); });
} }
if (error.errorType === 'ITEMS_HAVE_ASSOCIATED_TRANSACTIONS') {
return res.status(400).send({
errors: [{ type: 'ITEMS_HAVE_ASSOCIATED_TRANSACTIONS', code: 310 }],
});
}
if (error.errorType === 'ITEM_HAS_ASSOCIATED_TRANSACTINS') {
return res.status(400).send({
errors: [{ type: 'ITEM_HAS_ASSOCIATED_TRANSACTINS', code: 320 }],
})
}
} }
} }
} }

View File

@@ -299,17 +299,22 @@ export default class ManualJournalsController extends BaseController {
sortOrder: 'asc', sortOrder: 'asc',
columnSortBy: 'created_at', columnSortBy: 'created_at',
filterRoles: [], filterRoles: [],
page: 1,
pageSize: 12,
...this.matchedQueryData(req), ...this.matchedQueryData(req),
} }
if (filter.stringifiedFilterRoles) { if (filter.stringifiedFilterRoles) {
filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles); filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles);
} }
try { try {
const manualJournals = await this.manualJournalsService.getManualJournals(tenantId, filter); const { manualJournals, pagination, filterMeta } = await this.manualJournalsService.getManualJournals(tenantId, filter);
return res.status(200).send({ manualJournals });
} catch (error) {
console.log(error);
return res.status(200).send({
manual_journals: manualJournals,
pagination: this.transfromToResponse(pagination),
filter_meta: this.transfromToResponse(filterMeta),
});
} catch (error) {
next(error); next(error);
} }
} }

View File

@@ -381,6 +381,20 @@ export default class BillsPayments extends BaseController {
* @return {Response} * @return {Response}
*/ */
async getBillsPayments(req: Request, res: Response) { async getBillsPayments(req: Request, res: Response) {
const { tenantId } = req.params;
const billPaymentsFilter = this.matchedQueryData(req);
try {
const { billPayments, pagination, filterMeta } = await this.billPaymentService
.listBillPayments(tenantId, billPaymentsFilter);
return res.status(200).send({
bill_payments: billPayments,
pagination: this.transfromToResponse(pagination),
filter_meta: this.transfromToResponse(filterMeta)
});
} catch (error) {
next(error);
}
} }
} }

View File

@@ -1,115 +0,0 @@
import express from 'express';
import {
param,
query,
} from 'express-validator';
import asyncMiddleware from 'api/middleware/asyncMiddleware';
export default {
/**
* Router constructor.
*/
router() {
const router = express.Router();
router.get('/:resource_slug/data',
this.resourceData.validation,
asyncMiddleware(this.resourceData.handler));
router.get('/:resource_slug/columns',
this.resourceColumns.validation,
asyncMiddleware(this.resourceColumns.handler));
router.get('/:resource_slug/fields',
this.resourceFields.validation,
asyncMiddleware(this.resourceFields.handler));
return router;
},
/**
* Retrieve resource data of the given resource key/slug.
*/
resourceData: {
validation: [
param('resource_slug').trim().escape().exists(),
],
async handler(req, res) {
const { AccountType } = req.models;
const { resource_slug: resourceSlug } = req.params;
const data = await AccountType.query();
return res.status(200).send({
data,
resource_slug: resourceSlug,
});
},
},
/**
* Retrieve resource columns of the given resource.
*/
resourceColumns: {
validation: [
param('resource_slug').trim().escape().exists(),
],
async handler(req, res) {
const { resource_slug: resourceSlug } = req.params;
const { Resource } = req.models;
const resource = await Resource.query()
.where('name', resourceSlug)
.withGraphFetched('fields')
.first();
if (!resource) {
return res.status(400).send({
errors: [{ type: 'RESOURCE.SLUG.NOT.FOUND', code: 200 }],
});
}
const resourceFields = resource.fields
.filter((field) => field.columnable)
.map((field) => ({
id: field.id,
label: field.labelName,
key: field.key,
}));
return res.status(200).send({
resource_columns: resourceFields,
resource_slug: resourceSlug,
});
},
},
/**
* Retrieve resource fields of the given resource.
*/
resourceFields: {
validation: [
param('resource_slug').trim().escape().exists(),
query('predefined').optional().isBoolean().toBoolean(),
query('builtin').optional().isBoolean().toBoolean(),
],
async handler(req, res) {
const { resource_slug: resourceSlug } = req.params;
const { Resource } = req.models;
const resource = await Resource.query()
.where('name', resourceSlug)
.withGraphFetched('fields')
.first();
if (!resource) {
return res.status(400).send({
errors: [{ type: 'RESOURCE.SLUG.NOT.FOUND', code: 200 }],
});
}
return res.status(200).send({
resource_fields: resource.fields,
resource_slug: resourceSlug,
});
},
},
};

View File

@@ -0,0 +1,44 @@
import { Router, Request, Response, NextFunction } from 'express';
import {
param,
query,
} from 'express-validator';
import asyncMiddleware from 'api/middleware/asyncMiddleware';
import BaseController from './BaseController';
import { Service } from 'typedi';
import ResourceFieldsKeys from 'data/ResourceFieldsKeys';
@Service()
export default class ResourceController extends BaseController{
/**
* Router constructor.
*/
router() {
const router = Router();
router.get('/:resource_model/fields',
this.resourceModelParamSchema,
asyncMiddleware(this.resourceFields.bind(this))
);
return router;
}
get resourceModelParamSchema() {
return [
param('resource_model').exists().trim().escape(),
];
}
/**
* Retrieve resource fields of the given resource.
*/
resourceFields(req: Request, res: Response, next: NextFunction) {
const { resource_model: resourceModel } = req.params;
try {
} catch (error) {
next(error);
}
}
};

View File

@@ -297,7 +297,21 @@ export default class SalesEstimatesController extends BaseController {
* @param {Request} req * @param {Request} req
* @param {Response} res * @param {Response} res
*/ */
async getEstimates(req: Request, res: Response) { async getEstimates(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const estimatesFilter: ISalesEstimatesFilter = this.matchedQueryData(req);
try {
const { salesEstimates, pagination, filterMeta } = await this.saleEstimateService
.estimatesList(tenantId, estimatesFilter);
return res.status(200).send({
sales_estimates: this.transfromToResponse(salesEstimates),
pagination,
filter_meta: this.transfromToResponse(filterMeta),
})
} catch (error) {
next(error);
}
} }
}; };

View File

@@ -1,4 +1,4 @@
import { Router, Request, Response } from 'express'; import { Router, Request, Response, NextFunction } from 'express';
import { check, param, query, matchedData } from 'express-validator'; import { check, param, query, matchedData } from 'express-validator';
import { difference } from 'lodash'; import { difference } from 'lodash';
import { raw } from 'objection'; import { raw } from 'objection';
@@ -7,7 +7,8 @@ import BaseController from '../BaseController';
import asyncMiddleware from 'api/middleware/asyncMiddleware'; import asyncMiddleware from 'api/middleware/asyncMiddleware';
import SaleInvoiceService from 'services/Sales/SalesInvoices'; import SaleInvoiceService from 'services/Sales/SalesInvoices';
import ItemsService from 'services/Items/ItemsService'; import ItemsService from 'services/Items/ItemsService';
import { ISaleInvoiceOTD } from 'interfaces'; import DynamicListingService from 'services/DynamicListing/DynamicListService';
import { ISaleInvoiceOTD, ISalesInvoicesFilter } from 'interfaces';
@Service() @Service()
export default class SaleInvoicesController extends BaseController{ export default class SaleInvoicesController extends BaseController{
@@ -17,6 +18,9 @@ export default class SaleInvoicesController extends BaseController{
@Inject() @Inject()
saleInvoiceService: SaleInvoiceService; saleInvoiceService: SaleInvoiceService;
@Inject()
dynamicListService: DynamicListingService;
/** /**
* Router constructor. * Router constructor.
*/ */
@@ -412,7 +416,21 @@ export default class SaleInvoicesController extends BaseController{
* @param {Response} res * @param {Response} res
* @param {Function} next * @param {Function} next
*/ */
async getSalesInvoices(req, res) { public async getSalesInvoices(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req.params;
const salesInvoicesFilter: ISalesInvoicesFilter = req.query;
try {
const { salesInvoices, filterMeta, pagination } = await this.saleInvoiceService.salesInvoicesList(
tenantId, salesInvoicesFilter,
);
return res.status(200).send({
sales_invoices: salesInvoices,
pagination: this.transfromToResponse(pagination),
filter_meta: this.transfromToResponse(filterMeta),
});
} catch (error) {
next(error);
}
} }
} }

View File

@@ -274,7 +274,6 @@ export default class SalesReceiptsController extends BaseController{
const { id: saleReceiptId } = req.params; const { id: saleReceiptId } = req.params;
const saleReceipt = { ...req.body }; const saleReceipt = { ...req.body };
const errorReasons = []; const errorReasons = [];
// Handle all errors with reasons messages. // Handle all errors with reasons messages.
@@ -296,7 +295,19 @@ export default class SalesReceiptsController extends BaseController{
* @param {Request} req * @param {Request} req
* @param {Response} res * @param {Response} res
*/ */
async listingSalesReceipts(req: Request, res: Response) { async getSalesReceipts(req: Request, res: Response) {
const { tenantId } = req;
const filter = {
sortOrder: 'asc',
page: 1,
pageSize: 12,
...this.matchedBodyData(req),
};
try {
} catch (error) {
next(error);
}
} }
}; };

View File

@@ -222,19 +222,22 @@ export default class UsersController extends BaseController{
} }
if (error instanceof ServiceError) { if (error instanceof ServiceError) {
if (error.errorType === 'user_not_found') { if (error.errorType === 'user_not_found') {
return res.status(404).send({ return res.boom.badRequest(
errors: [{ type: 'USER.NOT.FOUND', code: 100 }], 'User not found.',
}); { errors: [{ type: 'USER.NOT.FOUND', code: 100 }] }
);
} }
if (error.errorType === 'user_already_active') { if (error.errorType === 'user_already_active') {
return res.status(404).send({ return res.boom.badRequest(
errors: [{ type: 'USER.ALREADY.ACTIVE', code: 200 }], 'User is already active.',
}); { errors: [{ type: 'USER.ALREADY.ACTIVE', code: 200 }] },
);
} }
if (error.errorType === 'user_already_inactive') { if (error.errorType === 'user_already_inactive') {
return res.status(404).send({ return res.boom.badRequest(
errors: [{ type: 'USER.ALREADY.INACTIVE', code: 200 }], 'User is already inactive.',
}); { errors: [{ type: 'USER.ALREADY.INACTIVE', code: 200 }] },
);
} }
if (error.errorType === 'user_same_the_authorized_user') { if (error.errorType === 'user_same_the_authorized_user') {
return res.boom.badRequest( return res.boom.badRequest(

View File

@@ -1,473 +0,0 @@
import { difference, pick } from 'lodash';
import express from 'express';
import {
check,
query,
param,
oneOf,
validationResult,
} from 'express-validator';
import asyncMiddleware from 'api/middleware/asyncMiddleware';
import {
validateViewRoles,
} from 'lib/ViewRolesBuilder';
export default {
resource: 'items',
/**
* Router constructor.
*/
router() {
const router = express.Router();
router.get('/',
this.listViews.validation,
asyncMiddleware(this.listViews.handler));
router.post('/',
this.createView.validation,
asyncMiddleware(this.createView.handler));
router.post('/:view_id',
this.editView.validation,
asyncMiddleware(this.editView.handler));
router.delete('/:view_id',
this.deleteView.validation,
asyncMiddleware(this.deleteView.handler));
router.get('/:view_id',
asyncMiddleware(this.getView.handler));
router.get('/:view_id/resource',
this.getViewResource.validation,
asyncMiddleware(this.getViewResource.handler));
return router;
},
/**
* List all views that associated with the given resource.
*/
listViews: {
validation: [
oneOf([
query('resource_name').exists().trim().escape(),
], [
query('resource_id').exists().isNumeric().toInt(),
]),
],
async handler(req, res) {
const { Resource, View } = req.models;
const filter = { ...req.query };
const resource = await Resource.query().onBuild((builder) => {
if (filter.resource_id) {
builder.where('id', filter.resource_id);
}
if (filter.resource_name) {
builder.where('name', filter.resource_name);
}
builder.first();
});
const views = await View.query().where('resource_id', resource.id);
return res.status(200).send({ views });
},
},
/**
* Retrieve view details of the given view id.
*/
getView: {
validation: [
param('view_id').exists().isNumeric().toInt(),
],
async handler(req, res) {
const { view_id: viewId } = req.params;
const { View } = req.models;
const view = await View.query()
.where('id', viewId)
.withGraphFetched('resource')
.withGraphFetched('columns')
.withGraphFetched('roles.field')
.first();
if (!view) {
return res.boom.notFound(null, {
errors: [{ type: 'VIEW_NOT_FOUND', code: 100 }],
});
}
return res.status(200).send({ view: view.toJSON() });
},
},
/**
* Delete the given view of the resource.
*/
deleteView: {
validation: [
param('view_id').exists().isNumeric().toInt(),
],
async handler(req, res) {
const { View } = req.models;
const { view_id: viewId } = req.params;
const view = await View.query().findById(viewId);
if (!view) {
return res.boom.notFound(null, {
errors: [{ type: 'VIEW_NOT_FOUND', code: 100 }],
});
}
if (view.predefined) {
return res.boom.badRequest(null, {
errors: [{ type: 'PREDEFINED_VIEW', code: 200 }],
});
}
await Promise.all([
view.$relatedQuery('roles').delete(),
view.$relatedQuery('columns').delete(),
]);
await View.query().where('id', view.id).delete();
return res.status(200).send({ id: view.id });
},
},
/**
* Creates a new view.
*/
createView: {
validation: [
check('resource_name').exists().escape().trim(),
check('name').exists().escape().trim(),
check('logic_expression').exists().trim().escape(),
check('roles').isArray({ min: 1 }),
check('roles.*.field_key').exists().escape().trim(),
check('roles.*.comparator').exists(),
check('roles.*.value').exists(),
check('roles.*.index').exists().isNumeric().toInt(),
check('columns').exists().isArray({ min: 1 }),
check('columns.*.key').exists().escape().trim(),
check('columns.*.index').exists().isNumeric().toInt(),
],
async handler(req, res) {
const validationErrors = validationResult(req);
if (!validationErrors.isEmpty()) {
return res.boom.badData(null, {
code: 'validation_error', ...validationErrors,
});
}
const {
Resource,
View,
ViewColumn,
ViewRole,
} = req.models;
const form = { roles: [], ...req.body };
const resource = await Resource.query().where('name', form.resource_name).first();
if (!resource) {
return res.boom.notFound(null, {
errors: [{ type: 'RESOURCE_NOT_FOUND', code: 100 }],
});
}
const errorReasons = [];
const fieldsSlugs = form.roles.map((role) => role.field_key);
const resourceFields = await resource.$relatedQuery('fields');
const resourceFieldsKeys = resourceFields.map((f) => f.key);
const resourceFieldsKeysMap = new Map(resourceFields.map((field) => [field.key, field]));
const columnsKeys = form.columns.map((c) => c.key);
// The difference between the stored resource fields and submit fields keys.
const notFoundFields = difference(fieldsSlugs, resourceFieldsKeys);
if (notFoundFields.length > 0) {
errorReasons.push({ type: 'RESOURCE_FIELDS_NOT_EXIST', code: 100, fields: notFoundFields });
}
// The difference between the stored resource fields and the submit columns keys.
const notFoundColumns = difference(columnsKeys, resourceFieldsKeys);
if (notFoundColumns.length > 0) {
errorReasons.push({ type: 'COLUMNS_NOT_EXIST', code: 200, columns: notFoundColumns });
}
// Validates the view conditional logic expression.
if (!validateViewRoles(form.roles, form.logic_expression)) {
errorReasons.push({ type: 'VIEW.ROLES.LOGIC.EXPRESSION.INVALID', code: 400 });
}
if (errorReasons.length > 0) {
return res.boom.badRequest(null, { errors: errorReasons });
}
// Save view details.
const view = await View.query().insert({
name: form.name,
predefined: false,
resource_id: resource.id,
roles_logic_expression: form.logic_expression,
});
// Save view roles async operations.
const saveViewRolesOpers = [];
form.roles.forEach((role) => {
const fieldModel = resourceFieldsKeysMap.get(role.field_key);
const saveViewRoleOper = ViewRole.query().insert({
...pick(role, ['comparator', 'value', 'index']),
field_id: fieldModel.id,
view_id: view.id,
});
saveViewRolesOpers.push(saveViewRoleOper);
});
form.columns.forEach((column) => {
const fieldModel = resourceFieldsKeysMap.get(column.key);
const saveViewColumnOper = ViewColumn.query().insert({
field_id: fieldModel.id,
view_id: view.id,
index: column.index,
});
saveViewRolesOpers.push(saveViewColumnOper);
});
await Promise.all(saveViewRolesOpers);
return res.status(200).send({ id: view.id });
},
},
/**
* Edit the given custom view metadata.
*/
editView: {
validation: [
param('view_id').exists().isNumeric().toInt(),
check('name').exists().escape().trim(),
check('logic_expression').exists().trim().escape(),
check('columns').exists().isArray({ min: 1 }),
check('columns.*.id').optional().isNumeric().toInt(),
check('columns.*.key').exists().escape().trim(),
check('columns.*.index').exists().isNumeric().toInt(),
check('roles').isArray(),
check('roles.*.id').optional().isNumeric().toInt(),
check('roles.*.field_key').exists().escape().trim(),
check('roles.*.comparator').exists(),
check('roles.*.value').exists(),
check('roles.*.index').exists().isNumeric().toInt(),
],
async handler(req, res) {
const { view_id: viewId } = req.params;
const validationErrors = validationResult(req);
if (!validationErrors.isEmpty()) {
return res.boom.badData(null, {
code: 'validation_error', ...validationErrors,
});
}
const {
View, ViewRole, ViewColumn, Resource,
} = req.models;
const view = await View.query().where('id', viewId)
.withGraphFetched('roles.field')
.withGraphFetched('columns')
.first();
if (!view) {
return res.boom.notFound(null, {
errors: [{ type: 'ROLE_NOT_FOUND', code: 100 }],
});
}
const form = { ...req.body };
const resource = await Resource.query()
.where('id', view.resourceId)
.withGraphFetched('fields')
.withGraphFetched('views')
.first();
const errorReasons = [];
const fieldsSlugs = form.roles.map((role) => role.field_key);
const resourceFieldsKeys = resource.fields.map((f) => f.key);
const resourceFieldsKeysMap = new Map(resource.fields.map((field) => [field.key, field]));
const columnsKeys = form.columns.map((c) => c.key);
// The difference between the stored resource fields and submit fields keys.
const notFoundFields = difference(fieldsSlugs, resourceFieldsKeys);
// Validate not found resource fields keys.
if (notFoundFields.length > 0) {
errorReasons.push({
type: 'RESOURCE_FIELDS_NOT_EXIST', code: 100, fields: notFoundFields,
});
}
// The difference between the stored resource fields and the submit columns keys.
const notFoundColumns = difference(columnsKeys, resourceFieldsKeys);
// Validate not found view columns.
if (notFoundColumns.length > 0) {
errorReasons.push({ type: 'RESOURCE_COLUMNS_NOT_EXIST', code: 200, columns: notFoundColumns });
}
// Validates the view conditional logic expression.
if (!validateViewRoles(form.roles, form.logic_expression)) {
errorReasons.push({ type: 'VIEW.ROLES.LOGIC.EXPRESSION.INVALID', code: 400 });
}
const viewRolesIds = view.roles.map((r) => r.id);
const viewColumnsIds = view.columns.map((c) => c.id);
const formUpdatedRoles = form.roles.filter((r) => r.id);
const formInsertRoles = form.roles.filter((r) => !r.id);
const formRolesIds = formUpdatedRoles.map((r) => r.id);
const formUpdatedColumns = form.columns.filter((r) => r.id);
const formInsertedColumns = form.columns.filter((r) => !r.id);
const formColumnsIds = formUpdatedColumns.map((r) => r.id);
const rolesIdsShouldDeleted = difference(viewRolesIds, formRolesIds);
const columnsIdsShouldDelete = difference(viewColumnsIds, formColumnsIds);
const notFoundViewRolesIds = difference(formRolesIds, viewRolesIds);
const notFoundViewColumnsIds = difference(viewColumnsIds, viewColumnsIds);
// Validate the not found view roles ids.
if (notFoundViewRolesIds.length) {
errorReasons.push({ type: 'VIEW.ROLES.IDS.NOT.FOUND', code: 500, ids: notFoundViewRolesIds });
}
// Validate the not found view columns ids.
if (notFoundViewColumnsIds.length) {
errorReasons.push({ type: 'VIEW.COLUMNS.IDS.NOT.FOUND', code: 600, ids: notFoundViewColumnsIds });
}
if (errorReasons.length > 0) {
return res.status(400).send({ errors: errorReasons });
}
const asyncOpers = [];
// Save view details.
await View.query()
.where('id', view.id)
.patch({
name: form.name,
roles_logic_expression: form.logic_expression,
});
// Update view roles.
if (formUpdatedRoles.length > 0) {
formUpdatedRoles.forEach((role) => {
const fieldModel = resourceFieldsKeysMap.get(role.field_key);
const updateOper = ViewRole.query()
.where('id', role.id)
.update({
...pick(role, ['comparator', 'value', 'index']),
field_id: fieldModel.id,
});
asyncOpers.push(updateOper);
});
}
// Insert a new view roles.
if (formInsertRoles.length > 0) {
formInsertRoles.forEach((role) => {
const fieldModel = resourceFieldsKeysMap.get(role.field_key);
const insertOper = ViewRole.query()
.insert({
...pick(role, ['comparator', 'value', 'index']),
field_id: fieldModel.id,
view_id: view.id,
});
asyncOpers.push(insertOper);
});
}
// Delete view roles.
if (rolesIdsShouldDeleted.length > 0) {
const deleteOper = ViewRole.query()
.whereIn('id', rolesIdsShouldDeleted)
.delete();
asyncOpers.push(deleteOper);
}
// Insert a new view columns to the storage.
if (formInsertedColumns.length > 0) {
formInsertedColumns.forEach((column) => {
const fieldModel = resourceFieldsKeysMap.get(column.key);
const insertOper = ViewColumn.query()
.insert({
field_id: fieldModel.id,
index: column.index,
view_id: view.id,
});
asyncOpers.push(insertOper);
});
}
// Update the view columns on the storage.
if (formUpdatedColumns.length > 0) {
formUpdatedColumns.forEach((column) => {
const updateOper = ViewColumn.query()
.where('id', column.id)
.update({
index: column.index,
});
asyncOpers.push(updateOper);
});
}
// Delete the view columns from the storage.
if (columnsIdsShouldDelete.length > 0) {
const deleteOper = ViewColumn.query()
.whereIn('id', columnsIdsShouldDelete)
.delete();
asyncOpers.push(deleteOper);
}
await Promise.all(asyncOpers);
return res.status(200).send();
},
},
/**
* Retrieve resource columns that associated to the given custom view.
*/
getViewResource: {
validation: [
param('view_id').exists().isNumeric().toInt(),
],
async handler(req, res) {
const { view_id: viewId } = req.params;
const { View } = req.models;
const view = await View.query()
.where('id', viewId)
.withGraphFetched('resource.fields')
.first();
if (!view) {
return res.boom.notFound(null, {
errors: [{ type: 'VIEW.NOT.FOUND', code: 100 }],
});
}
if (!view.resource) {
return res.boom.badData(null, {
errors: [{ type: 'VIEW.HAS.NOT.ASSOCIATED.RESOURCE', code: 200 }],
});
}
const resourceColumns = view.resource.fields
.filter((field) => field.columnable)
.map((field) => ({
id: field.id,
label: field.labelName,
key: field.key,
}));
return res.status(200).send({
resource_slug: view.resource.name,
resource_columns: resourceColumns,
resource_fields: view.resource.fields,
});
}
},
};

View File

@@ -1,19 +1,10 @@
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import { Router, Request, NextFunction, Response } from 'express'; import { Router, Request, NextFunction, Response } from 'express';
import { import { check, param } from 'express-validator';
check,
query,
param,
oneOf,
validationResult,
} from 'express-validator';
import asyncMiddleware from 'api/middleware/asyncMiddleware'; import asyncMiddleware from 'api/middleware/asyncMiddleware';
import {
validateViewRoles,
} from 'lib/ViewRolesBuilder';
import ViewsService from 'services/Views/ViewsService'; import ViewsService from 'services/Views/ViewsService';
import BaseController from './BaseController'; import BaseController from 'api/controllers/BaseController';
import { IViewDTO } from 'interfaces'; import { IViewDTO, IViewEditDTO } from 'interfaces';
import { ServiceError } from 'exceptions'; import { ServiceError } from 'exceptions';
@Service() @Service()
@@ -27,66 +18,96 @@ export default class ViewsController extends BaseController{
router() { router() {
const router = Router(); const router = Router();
router.get('/', [ router.get('/resource/:resource_model', [
...this.viewDTOSchemaValidation, ...this.viewsListSchemaValidation,
], ],
asyncMiddleware(this.listViews) this.validationResult,
asyncMiddleware(this.listResourceViews.bind(this)),
this.handlerServiceErrors,
); );
router.post('/', [ router.post('/', [
...this.viewDTOSchemaValidation, ...this.viewDTOSchemaValidation,
], ],
asyncMiddleware(this.createView) this.validationResult,
asyncMiddleware(this.createView.bind(this)),
this.handlerServiceErrors
); );
router.post('/:id', [
router.post('/:view_id', [ ...this.viewParamSchemaValidation,
...this.viewDTOSchemaValidation, ...this.viewEditDTOSchemaValidation,
], ],
asyncMiddleware(this.editView) this.validationResult,
asyncMiddleware(this.editView.bind(this)),
this.handlerServiceErrors,
); );
router.delete('/:id', [
router.delete('/:view_id', [
...this.viewParamSchemaValidation ...this.viewParamSchemaValidation
], ],
asyncMiddleware(this.deleteView)); this.validationResult,
asyncMiddleware(this.deleteView.bind(this)),
router.get('/:view_id', [ this.handlerServiceErrors,
...this.viewParamSchemaValidation
]
asyncMiddleware(this.getView)
); );
router.get('/:id', [
router.get('/:view_id/resource', [
...this.viewParamSchemaValidation ...this.viewParamSchemaValidation
], ],
asyncMiddleware(this.getViewResource) this.validationResult,
asyncMiddleware(this.getView.bind(this)),
); );
return router; return router;
} }
/**
* New view DTO schema validation.
*/
get viewDTOSchemaValidation() { get viewDTOSchemaValidation() {
return [ return [
check('resource_name').exists().escape().trim(), check('resource_model').exists().escape().trim(),
check('name').exists().escape().trim(), check('name').exists().escape().trim(),
check('logic_expression').exists().trim().escape(), check('logic_expression').exists().trim().escape(),
check('roles').isArray({ min: 1 }), check('roles').isArray({ min: 1 }),
check('roles.*.field_key').exists().escape().trim(), check('roles.*.field_key').exists().escape().trim(),
check('roles.*.comparator').exists(), check('roles.*.comparator').exists(),
check('roles.*.value').exists(), check('roles.*.value').exists(),
check('roles.*.index').exists().isNumeric().toInt(), check('roles.*.index').exists().isNumeric().toInt(),
check('columns').exists().isArray({ min: 1 }), check('columns').exists().isArray({ min: 1 }),
check('columns.*.key').exists().escape().trim(), check('columns.*.field_key').exists().escape().trim(),
check('columns.*.index').exists().isNumeric().toInt(),
];
}
/**
* Edit view DTO schema validation.
*/
get viewEditDTOSchemaValidation() {
return [
check('name').exists().escape().trim(),
check('logic_expression').exists().trim().escape(),
check('roles').isArray({ min: 1 }),
check('roles.*.field_key').exists().escape().trim(),
check('roles.*.comparator').exists(),
check('roles.*.value').exists(),
check('roles.*.index').exists().isNumeric().toInt(),
check('columns').exists().isArray({ min: 1 }),
check('columns.*.field_key').exists().escape().trim(),
check('columns.*.index').exists().isNumeric().toInt(), check('columns.*.index').exists().isNumeric().toInt(),
]; ];
} }
get viewParamSchemaValidation() { get viewParamSchemaValidation() {
return [ return [
param('id').exists().isNumeric().toInt(),
] ];
} }
get viewsListSchemaValidation() {
return [
param('resource_model').exists().trim().escape(),
]
}
/** /**
* List all views that associated with the given resource. * List all views that associated with the given resource.
@@ -94,12 +115,12 @@ export default class ViewsController extends BaseController{
* @param {Response} res - * @param {Response} res -
* @param {NextFunction} next - * @param {NextFunction} next -
*/ */
listViews(req: Request, res: Response, next: NextFunction) { async listResourceViews(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req; const { tenantId } = req;
const filter = req.query; const { resource_model: resourceModel } = req.params;
try { try {
const views = this.viewsService.listViews(tenantId, filter); const views = await this.viewsService.listResourceViews(tenantId, resourceModel);
return res.status(200).send({ views }); return res.status(200).send({ views });
} catch (error) { } catch (error) {
next(error); next(error);
@@ -107,16 +128,17 @@ export default class ViewsController extends BaseController{
} }
/** /**
* * Retrieve view details with assocaited roles and columns.
* @param {Request} req * @param {Request} req
* @param {Response} res * @param {Response} res
* @param {NextFunction} next * @param {NextFunction} next
*/ */
getView(req: Request, res: Response, next: NextFunction) { async getView(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req; const { tenantId } = req;
const { id: viewId } = req.params;
try { try {
const view = this.viewsService.getView(tenantId, viewId); const view = await this.viewsService.getView(tenantId, viewId);
return res.status(200).send({ view }); return res.status(200).send({ view });
} catch (error) { } catch (error) {
next(error); next(error);
@@ -129,13 +151,13 @@ export default class ViewsController extends BaseController{
* @param {Response} res - * @param {Response} res -
* @param {NextFunction} next - * @param {NextFunction} next -
*/ */
createView(req: Request, res: Response, next: NextFunction) { async createView(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req; const { tenantId } = req;
const viewDTO: IViewDTO = this.matchedBodyData(req); const viewDTO: IViewDTO = this.matchedBodyData(req);
try { try {
await this.viewsService.newView(tenantId, viewDTO); const view = await this.viewsService.newView(tenantId, viewDTO);
return res.status(200).send({ id: 1 }); return res.status(200).send({ id: view.id });
} catch (error) { } catch (error) {
next(error); next(error);
} }
@@ -147,10 +169,10 @@ export default class ViewsController extends BaseController{
* @param {Response} res - * @param {Response} res -
* @param {NextFunction} next - * @param {NextFunction} next -
*/ */
editView(req: Request, res: Response, next: NextFunction) { async editView(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req; const { tenantId } = req;
const { id: viewId } = req.params; const { id: viewId } = req.params;
const { body: viewEditDTO } = req; const viewEditDTO: IViewEditDTO = this.matchedBodyData(req);
try { try {
await this.viewsService.editView(tenantId, viewId, viewEditDTO); await this.viewsService.editView(tenantId, viewId, viewEditDTO);
@@ -166,7 +188,7 @@ export default class ViewsController extends BaseController{
* @param {Response} res - * @param {Response} res -
* @param {NextFunction} next - * @param {NextFunction} next -
*/ */
deleteView(req: Request, res: Response, next: NextFunction) { async deleteView(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req; const { tenantId } = req;
const { id: viewId } = req.params; const { id: viewId } = req.params;
@@ -187,6 +209,16 @@ export default class ViewsController extends BaseController{
*/ */
handlerServiceErrors(error: Error, req: Request, res: Response, next: NextFunction) { handlerServiceErrors(error: Error, req: Request, res: Response, next: NextFunction) {
if (error instanceof ServiceError) { if (error instanceof ServiceError) {
if (error.errorType === 'VIEW_NAME_NOT_UNIQUE') {
return res.boom.badRequest(null, {
errors: [{ type: 'VIEW_NAME_NOT_UNIQUE', code: 110 }],
});
}
if (error.errorType === 'RESOURCE_MODEL_NOT_FOUND') {
return res.boom.badRequest(null, {
errors: [{ type: 'RESOURCE_MODEL_NOT_FOUND', code: 150, }],
});
}
if (error.errorType === 'INVALID_LOGIC_EXPRESSION') { if (error.errorType === 'INVALID_LOGIC_EXPRESSION') {
return res.boom.badRequest(null, { return res.boom.badRequest(null, {
errors: [{ type: 'VIEW.ROLES.LOGIC.EXPRESSION.INVALID', code: 400 }], errors: [{ type: 'VIEW.ROLES.LOGIC.EXPRESSION.INVALID', code: 400 }],
@@ -212,6 +244,17 @@ export default class ViewsController extends BaseController{
errors: [{ type: 'PREDEFINED_VIEW', code: 200 }], errors: [{ type: 'PREDEFINED_VIEW', code: 200 }],
}); });
} }
if (error.errorType === 'RESOURCE_FIELDS_KEYS_NOT_FOUND') {
return res.boom.badRequest(null, {
errors: [{ type: 'RESOURCE_FIELDS_KEYS_NOT_FOUND', code: 300 }],
})
} }
if (error.errorType === 'RESOURCE_COLUMNS_KEYS_NOT_FOUND') {
return res.boom.badRequest(null, {
errors: [{ type: 'RESOURCE_COLUMNS_KEYS_NOT_FOUND', code: 310 }],
})
}
}
next(error);
} }
}; };

View File

@@ -87,16 +87,16 @@ export default () => {
dashboard.use('/accounts', Container.get(Accounts).router()); dashboard.use('/accounts', Container.get(Accounts).router());
dashboard.use('/account_types', Container.get(AccountTypes).router()); dashboard.use('/account_types', Container.get(AccountTypes).router());
dashboard.use('/manual-journals', Container.get(ManualJournals).router()); dashboard.use('/manual-journals', Container.get(ManualJournals).router());
dashboard.use('/views', Views.router()); dashboard.use('/views', Container.get(Views).router());
dashboard.use('/items', Container.get(Items).router()); dashboard.use('/items', Container.get(Items).router());
dashboard.use('/item_categories', Container.get(ItemCategories).router()); dashboard.use('/item_categories', Container.get(ItemCategories).router());
dashboard.use('/expenses', Container.get(Expenses).router()); dashboard.use('/expenses', Container.get(Expenses).router());
dashboard.use('/financial_statements', FinancialStatements.router()); dashboard.use('/financial_statements', FinancialStatements.router());
dashboard.use('/sales', Container.get(Sales).router());
dashboard.use('/customers', Container.get(Customers).router()); dashboard.use('/customers', Container.get(Customers).router());
dashboard.use('/vendors', Container.get(Vendors).router()); dashboard.use('/vendors', Container.get(Vendors).router());
dashboard.use('/purchases', Container.get(Purchases).router()); // dashboard.use('/sales', Container.get(Sales).router());
dashboard.use('/resources', Resources.router()); // dashboard.use('/purchases', Container.get(Purchases).router());
dashboard.use('/resources', Container.get(Resources).router());
dashboard.use('/exchange_rates', Container.get(ExchangeRates).router()); dashboard.use('/exchange_rates', Container.get(ExchangeRates).router());
dashboard.use('/media', Media.router()); dashboard.use('/media', Media.router());

View File

@@ -17,6 +17,8 @@ export default (req: Request, tenant: ITenant) => {
const repositories = tenantServices.repositories(tenantId) const repositories = tenantServices.repositories(tenantId)
const cacheInstance = tenantServices.cache(tenantId); const cacheInstance = tenantServices.cache(tenantId);
tenantServices.setI18nLocals(tenantId, { __: req.__ });
req.knex = knexInstance; req.knex = knexInstance;
req.organizationId = organizationId; req.organizationId = organizationId;
req.tenant = tenant; req.tenant = tenant;

View File

@@ -24,6 +24,7 @@ export default {
db_user: process.env.SYSTEM_DB_USER, db_user: process.env.SYSTEM_DB_USER,
db_password: process.env.SYSTEM_DB_PASSWORD, db_password: process.env.SYSTEM_DB_PASSWORD,
db_name: process.env.SYSTEM_DB_NAME, db_name: process.env.SYSTEM_DB_NAME,
charset: process.env.SYSTEM_DB_CHARSET,
migrations_dir: process.env.SYSTEM_MIGRATIONS_DIR, migrations_dir: process.env.SYSTEM_MIGRATIONS_DIR,
seeds_dir: process.env.SYSTEM_SEEDS_DIR, seeds_dir: process.env.SYSTEM_SEEDS_DIR,
}, },

View File

@@ -2,7 +2,7 @@
export default { export default {
// Expenses. // Expenses.
'expenses': { expense: {
payment_date: { payment_date: {
column: 'payment_date', column: 'payment_date',
}, },
@@ -10,9 +10,12 @@ export default {
column: 'payment_account_id', column: 'payment_account_id',
relation: 'accounts.id', relation: 'accounts.id',
}, },
total_amount: { amount: {
column: 'total_amount', column: 'total_amount',
}, },
currency_code: {
column: 'currency_code',
},
reference_no: { reference_no: {
column: 'reference_no' column: 'reference_no'
}, },
@@ -30,7 +33,7 @@ export default {
}, },
// Accounts // Accounts
'accounts': { Account: {
name: { name: {
column: 'name', column: 'name',
}, },
@@ -72,23 +75,106 @@ export default {
}, },
// Items // Items
'items': { item: {
'type': { type: {
column: 'type', column: 'type',
}, },
'name': { name: {
column: 'name', column: 'name',
}, },
sellable: {
column: 'sellable',
},
purchasable: {
column: 'purchasable',
},
sell_price: {
column: 'sell_price'
},
cost_price: {
column: 'cost_price',
},
currency_code: {
column: 'currency_code',
},
cost_account: {
column: 'cost_account_id',
relation: 'accounts.id',
},
sell_account: {
column: 'sell_account_id',
relation: 'accounts.id',
},
inventory_account: {
column: 'inventory_account_id',
relation: 'accounts.id',
},
sell_description: {
column: 'sell_description',
},
purchase_description: {
column: 'purchase_description',
},
quantity_on_hand: {
column: 'quantity_on_hand',
},
note: {
column: 'note',
},
category: {
column: 'category_id',
relation: 'categories.id',
},
user: {
column: 'user_id',
relation: 'users.id',
relationColumn: 'users.id',
},
created_at: {
column: 'created_at',
}
},
// Item category.
item_category: {
name: {
column: 'name',
},
description: {
column: 'description',
},
parent_category_id: {
column: 'parent_category_id',
relation: 'items_categories.id',
relationColumn: 'items_categories.id',
},
user: {
column: 'user_id',
relation: 'users.id',
relationColumn: 'users.id',
},
cost_account: {
column: 'cost_account_id',
relation: 'accounts.id',
},
sell_account: {
column: 'sell_account_id',
relation: 'accounts.id',
},
inventory_account: {
column: 'inventory_account_id',
relation: 'accounts.id',
},
cost_method: {
column: 'cost_method',
},
}, },
// Manual Journals // Manual Journals
manual_journals: { manual_journal: {
date: { date: {
column: 'date', column: 'date',
}, },
created_at: {
column: 'created_at',
},
journal_number: { journal_number: {
column: 'journal_number', column: 'journal_number',
}, },
@@ -112,5 +198,8 @@ export default {
journal_type: { journal_type: {
column: 'journal_type', column: 'journal_type',
}, },
created_at: {
column: 'created_at',
},
} }
}; };

View File

@@ -2,7 +2,6 @@
exports.up = (knex) => { exports.up = (knex) => {
return knex.schema.createTable('account_types', (table) => { return knex.schema.createTable('account_types', (table) => {
table.increments(); table.increments();
table.string('name');
table.string('key').index(); table.string('key').index();
table.string('normal').index(); table.string('normal').index();
table.string('root_type').index(); table.string('root_type').index();

View File

@@ -1,8 +1,14 @@
import Container from 'typedi';
import TenancyService from 'services/Tenancy/TenancyService'
import I18nMiddleware from 'api/middleware/I18nMiddleware';
exports.up = function (knex) { exports.up = function (knex) {
const tenancyService = Container.get(TenancyService);
const i18n = tenancyService.i18n(knex.userParams.tenantId);
return knex('account_types').insert([ return knex('account_types').insert([
{ {
id: 1, id: 1,
name: 'Fixed Asset',
key: 'fixed_asset', key: 'fixed_asset',
normal: 'debit', normal: 'debit',
root_type: 'asset', root_type: 'asset',
@@ -12,7 +18,6 @@ exports.up = function (knex) {
}, },
{ {
id: 2, id: 2,
name: 'Current Asset',
key: 'current_asset', key: 'current_asset',
normal: 'debit', normal: 'debit',
root_type: 'asset', root_type: 'asset',
@@ -22,7 +27,6 @@ exports.up = function (knex) {
}, },
{ {
id: 14, id: 14,
name: 'Other Asset',
key: 'other_asset', key: 'other_asset',
normal: 'debit', normal: 'debit',
root_type: 'asset', root_type: 'asset',
@@ -32,7 +36,6 @@ exports.up = function (knex) {
}, },
{ {
id: 3, id: 3,
name: 'Long Term Liability',
key: 'long_term_liability', key: 'long_term_liability',
normal: 'credit', normal: 'credit',
root_type: 'liability', root_type: 'liability',
@@ -42,7 +45,6 @@ exports.up = function (knex) {
}, },
{ {
id: 4, id: 4,
name: 'Current Liability',
key: 'current_liability', key: 'current_liability',
normal: 'credit', normal: 'credit',
root_type: 'liability', root_type: 'liability',
@@ -52,7 +54,6 @@ exports.up = function (knex) {
}, },
{ {
id: 13, id: 13,
name: 'Other Liability',
key: 'other_liability', key: 'other_liability',
normal: 'credit', normal: 'credit',
root_type: 'liability', root_type: 'liability',
@@ -62,7 +63,6 @@ exports.up = function (knex) {
}, },
{ {
id: 5, id: 5,
name: 'Equity',
key: 'equity', key: 'equity',
normal: 'credit', normal: 'credit',
root_type: 'equity', root_type: 'equity',
@@ -72,7 +72,6 @@ exports.up = function (knex) {
}, },
{ {
id: 6, id: 6,
name: 'Expense',
key: 'expense', key: 'expense',
normal: 'debit', normal: 'debit',
root_type: 'expense', root_type: 'expense',
@@ -82,7 +81,6 @@ exports.up = function (knex) {
}, },
{ {
id: 10, id: 10,
name: 'Other Expense',
key: 'other_expense', key: 'other_expense',
normal: 'debit', normal: 'debit',
root_type: 'expense', root_type: 'expense',
@@ -91,7 +89,6 @@ exports.up = function (knex) {
}, },
{ {
id: 7, id: 7,
name: 'Income',
key: 'income', key: 'income',
normal: 'credit', normal: 'credit',
root_type: 'income', root_type: 'income',
@@ -101,7 +98,6 @@ exports.up = function (knex) {
}, },
{ {
id: 11, id: 11,
name: 'Other Income',
key: 'other_income', key: 'other_income',
normal: 'credit', normal: 'credit',
root_type: 'income', root_type: 'income',
@@ -111,7 +107,6 @@ exports.up = function (knex) {
}, },
{ {
id: 12, id: 12,
name: 'Cost of Goods Sold (COGS)',
key: 'cost_of_goods_sold', key: 'cost_of_goods_sold',
normal: 'debit', normal: 'debit',
root_type: 'expenses', root_type: 'expenses',
@@ -121,7 +116,6 @@ exports.up = function (knex) {
}, },
{ {
id: 8, id: 8,
name: 'Accounts Receivable (A/R)',
key: 'accounts_receivable', key: 'accounts_receivable',
normal: 'debit', normal: 'debit',
root_type: 'asset', root_type: 'asset',
@@ -131,7 +125,6 @@ exports.up = function (knex) {
}, },
{ {
id: 9, id: 9,
name: 'Accounts Payable (A/P)',
key: 'accounts_payable', key: 'accounts_payable',
normal: 'credit', normal: 'credit',
root_type: 'liability', root_type: 'liability',

View File

@@ -1,16 +1,18 @@
import TenancyService from 'services/Tenancy/TenancyService'
import Container from 'typedi'; import Container from 'typedi';
import TenancyService from 'services/Tenancy/TenancyService'
exports.up = function (knex) { exports.up = function (knex) {
const tenancyService = Container.get(TenancyService); const tenancyService = Container.get(TenancyService);
const i18n = tenancyService.i18n(knex.userParams.tenantId); const i18n = tenancyService.i18n(knex.userParams.tenantId);
console.log(i18n);
return knex('accounts').then(() => { return knex('accounts').then(() => {
// Inserts seed entries // Inserts seed entries
return knex('accounts').insert([ return knex('accounts').insert([
{ {
id: 1, id: 1,
name: 'Petty Cash', name: i18n.__('Petty Cash'),
slug: 'petty-cash', slug: 'petty-cash',
account_type_id: 2, account_type_id: 2,
parent_account_id: null, parent_account_id: null,
@@ -22,7 +24,7 @@ exports.up = function (knex) {
}, },
{ {
id: 2, id: 2,
name: 'Bank', name: i18n.__('Bank'),
slug: 'bank', slug: 'bank',
account_type_id: 2, account_type_id: 2,
parent_account_id: null, parent_account_id: null,
@@ -34,7 +36,7 @@ exports.up = function (knex) {
}, },
{ {
id: 3, id: 3,
name: 'Other Income', name: i18n.__('Other Income'),
slug: 'other-income', slug: 'other-income',
account_type_id: 7, account_type_id: 7,
parent_account_id: null, parent_account_id: null,
@@ -46,7 +48,7 @@ exports.up = function (knex) {
}, },
{ {
id: 4, id: 4,
name: 'Interest Income', name: i18n.__('Interest Income'),
slug: 'interest-income', slug: 'interest-income',
account_type_id: 7, account_type_id: 7,
parent_account_id: null, parent_account_id: null,
@@ -58,7 +60,7 @@ exports.up = function (knex) {
}, },
{ {
id: 5, id: 5,
name: 'Opening Balance', name: i18n.__('Opening Balance'),
slug: 'opening-balance', slug: 'opening-balance',
account_type_id: 5, account_type_id: 5,
parent_account_id: null, parent_account_id: null,
@@ -70,7 +72,7 @@ exports.up = function (knex) {
}, },
{ {
id: 6, id: 6,
name: 'Depreciation Expense', name: i18n.__('Depreciation Expense'),
slug: 'depreciation-expense', slug: 'depreciation-expense',
account_type_id: 6, account_type_id: 6,
parent_account_id: null, parent_account_id: null,
@@ -82,7 +84,7 @@ exports.up = function (knex) {
}, },
{ {
id: 7, id: 7,
name: 'Interest Expense', name: i18n.__('Interest Expense'),
slug: 'interest-expense', slug: 'interest-expense',
account_type_id: 6, account_type_id: 6,
parent_account_id: null, parent_account_id: null,
@@ -94,7 +96,7 @@ exports.up = function (knex) {
}, },
{ {
id: 8, id: 8,
name: 'Payroll Expenses', name: i18n.__('Payroll Expenses'),
slug: 'payroll-expenses', slug: 'payroll-expenses',
account_type_id: 6, account_type_id: 6,
parent_account_id: null, parent_account_id: null,
@@ -106,7 +108,7 @@ exports.up = function (knex) {
}, },
{ {
id: 9, id: 9,
name: 'Other Expenses', name: i18n.__('Other Expenses'),
slug: 'other-expenses', slug: 'other-expenses',
account_type_id: 6, account_type_id: 6,
parent_account_id: null, parent_account_id: null,
@@ -118,7 +120,7 @@ exports.up = function (knex) {
}, },
{ {
id: 10, id: 10,
name: 'Accounts Receivable', name: i18n.__('Accounts Receivable'),
slug: 'accounts-receivable', slug: 'accounts-receivable',
account_type_id: 8, account_type_id: 8,
parent_account_id: null, parent_account_id: null,
@@ -130,7 +132,7 @@ exports.up = function (knex) {
}, },
{ {
id: 11, id: 11,
name: 'Accounts Payable', name: i18n.__('Accounts Payable'),
slug: 'accounts-payable', slug: 'accounts-payable',
account_type_id: 9, account_type_id: 9,
parent_account_id: null, parent_account_id: null,
@@ -142,7 +144,7 @@ exports.up = function (knex) {
}, },
{ {
id: 12, id: 12,
name: 'Cost of Goods Sold (COGS)', name: i18n.__('Cost of Goods Sold (COGS)'),
slug: 'cost-of-goods-sold', slug: 'cost-of-goods-sold',
account_type_id: 12, account_type_id: 12,
predefined: 1, predefined: 1,
@@ -153,7 +155,7 @@ exports.up = function (knex) {
}, },
{ {
id: 13, id: 13,
name: 'Inventory Asset', name: i18n.__('Inventory Asset'),
slug: 'inventory-asset', slug: 'inventory-asset',
account_type_id: 14, account_type_id: 14,
predefined: 1, predefined: 1,
@@ -164,7 +166,7 @@ exports.up = function (knex) {
}, },
{ {
id: 14, id: 14,
name: 'Sales of Product Income', name: i18n.__('Sales of Product Income'),
slug: 'sales-of-product-income', slug: 'sales-of-product-income',
account_type_id: 7, account_type_id: 7,
predefined: 1, predefined: 1,

View File

@@ -1,17 +1,20 @@
exports.up = (knex) => { exports.up = (knex) => {
const tenancyService = Container.get(TenancyService);
const i18n = tenancyService.i18n(knex.userParams.tenantId);
// Deletes ALL existing entries // Deletes ALL existing entries
return knex('views').del() return knex('views').del()
.then(() => { .then(() => {
// Inserts seed entries // Inserts seed entries
return knex('views').insert([ return knex('views').insert([
// Accounts // Accounts
{ id: 15, name: 'Inactive', roles_logic_expression: '1', resource_model: 'Account', predefined: true }, { id: 15, name: i18n.__('Inactive'), roles_logic_expression: '1', resource_model: 'Account', predefined: true },
{ id: 1, name: 'Assets', roles_logic_expression: '1', resource_model: 'Account', predefined: true }, { id: 1, name: i18n.__('Assets'), roles_logic_expression: '1', resource_model: 'Account', predefined: true },
{ id: 2, name: 'Liabilities', roles_logic_expression: '1', resource_model: 'Account', predefined: true }, { id: 2, name: i18n.__('Liabilities'), roles_logic_expression: '1', resource_model: 'Account', predefined: true },
{ id: 3, name: 'Equity', roles_logic_expression: '1', resource_model: 'Account', predefined: true }, { id: 3, name: i18n.__('Equity'), roles_logic_expression: '1', resource_model: 'Account', predefined: true },
{ id: 4, name: 'Income', roles_logic_expression: '1', resource_model: 'Account', predefined: true }, { id: 4, name: i18n.__('Income'), roles_logic_expression: '1', resource_model: 'Account', predefined: true },
{ id: 5, name: 'Expenses', roles_logic_expression: '1', resource_model: 'Account', predefined: true }, { id: 5, name: i18n.__('Expenses'), roles_logic_expression: '1', resource_model: 'Account', predefined: true },
// Items // Items
// { id: 6, name: 'Services', roles_logic_expression: '1', resource_id: 2, predefined: true }, // { id: 6, name: 'Services', roles_logic_expression: '1', resource_id: 2, predefined: true },

View File

@@ -178,8 +178,9 @@ export interface IVendorsFilter extends IDynamicListFilter {
pageSize?: number, pageSize?: number,
}; };
export interface ICustomerFilter extends IDynamicListFilter { export interface ICustomersFilter extends IDynamicListFilter {
stringifiedFilterRoles?: string, stringifiedFilterRoles?: string,
page?: number, page?: number,
pageSize?: number, pageSize?: number,
}; };

View File

@@ -1,5 +1,16 @@
import { ISystemUser } from "./User"; import { ISystemUser } from "./User";
export interface IPaginationMeta {
total: number,
page: number,
pageSize: number,
};
export interface IExpensesFilter{
page: number,
pageSize: number,
};
export interface IExpense { export interface IExpense {
id: number, id: number,
totalAmount: number, totalAmount: number,
@@ -53,4 +64,6 @@ export interface IExpensesService {
deleteBulkExpenses(tenantId: number, expensesIds: number[], authorizedUser: ISystemUser): Promise<void>; deleteBulkExpenses(tenantId: number, expensesIds: number[], authorizedUser: ISystemUser): Promise<void>;
publishBulkExpenses(tenantId: number, expensesIds: number[], authorizedUser: ISystemUser): Promise<void>; publishBulkExpenses(tenantId: number, expensesIds: number[], authorizedUser: ISystemUser): Promise<void>;
getExpensesList(tenantId: number, expensesFilter: IExpensesFilter): Promise<{ expenses: IExpense[], pagination: IPaginationMeta, filterMeta: IFilterMeta }>;
} }

View File

@@ -70,6 +70,6 @@ export interface IItemsService {
export interface IItemsFilter extends IDynamicListFilter { export interface IItemsFilter extends IDynamicListFilter {
stringifiedFilterRoles?: string, stringifiedFilterRoles?: string,
page?: number, page: number,
pageSize?: number, pageSize: number,
}; };

View File

@@ -37,8 +37,8 @@ export interface IManualJournalDTO {
export interface IManualJournalsFilter extends IDynamicListFilterDTO { export interface IManualJournalsFilter extends IDynamicListFilterDTO {
stringifiedFilterRoles?: string, stringifiedFilterRoles?: string,
page?: number, page: number,
pageSize?: number, pageSize: number,
} }
export interface IManuaLJournalsService { export interface IManuaLJournalsService {
@@ -48,5 +48,6 @@ export interface IManuaLJournalsService {
deleteManualJournals(tenantId: number, manualJournalsIds: number[]): Promise<void>; deleteManualJournals(tenantId: number, manualJournalsIds: number[]): Promise<void>;
publishManualJournals(tenantId: number, manualJournalsIds: number[]): Promise<void>; publishManualJournals(tenantId: number, manualJournalsIds: number[]): Promise<void>;
publishManualJournal(tenantId: number, manualJournalId: number): Promise<void>; publishManualJournal(tenantId: number, manualJournalId: number): Promise<void>;
getManualJournals(tenantId: number, filter: IManualJournalsFilter): Promise<void>;
getManualJournals(tenantId: number, filter: IManualJournalsFilter): Promise<{ manualJournals: IManualJournal, pagination: IPaginationMeta, filterMeta: IFilterMeta }>;
} }

View File

@@ -0,0 +1,17 @@
export interface IModel {
name: string,
tableName: string,
fields: { [key: string]: any, },
};
export interface IFilterMeta {
sortOrder: string,
sortBy: string,
};
export interface IPaginationMeta {
pageSize: number,
page: number,
};

View File

@@ -16,3 +16,8 @@ export interface ISaleInvoiceOTD {
termsConditions: string, termsConditions: string,
entries: any[], entries: any[],
} }
export interface ISalesInvoicesFilter{
page: number,
pageSize: number,
};

View File

@@ -0,0 +1,37 @@
import { ISalesInvoicesFilter } from "./SaleInvoice";
export interface ISaleReceipt {
id?: number,
customerId: number,
depositAccountId: number,
receiptDate: Date,
sendToEmail: string,
referenceNo: string,
receiptMessage: string,
statement: string,
entries: any[],
};
export interface ISalesReceiptsFilter {
};
export interface ISaleReceiptDTO {
customerId: number,
depositAccountId: number,
receiptDate: Date,
sendToEmail: string,
referenceNo: string,
receiptMessage: string,
statement: string,
entries: any[],
};
export interface ISalesReceiptService {
createSaleReceipt(tenantId: number, saleReceiptDTO: ISaleReceiptDTO): Promise<void>;
editSaleReceipt(tenantId: number, saleReceiptId: number): Promise<void>;
deleteSaleReceipt(tenantId: number, saleReceiptId: number): Promise<void>;
salesReceiptsList(tennatid: number, salesReceiptsFilter: ISalesReceiptsFilter): Promise<{ salesReceipts: ISaleReceipt[], pagination: IPaginationMeta, filterMeta: IFilterMeta }>;
};

View File

@@ -5,7 +5,10 @@ export interface IView {
predefined: boolean, predefined: boolean,
resourceModel: string, resourceModel: string,
favourite: boolean, favourite: boolean,
rolesLogicRxpression: string, rolesLogicExpression: string,
roles: IViewRole[],
columns: IViewHasColumn[],
}; };
export interface IViewRole { export interface IViewRole {
@@ -42,6 +45,8 @@ export interface IViewColumnDTO {
export interface IViewDTO { export interface IViewDTO {
name: string, name: string,
logicExpression: string, logicExpression: string,
resourceModel: string,
roles: IViewRoleDTO[], roles: IViewRoleDTO[],
columns: IViewColumnDTO[], columns: IViewColumnDTO[],
}; };
@@ -49,12 +54,13 @@ export interface IViewDTO {
export interface IViewEditDTO { export interface IViewEditDTO {
name: string, name: string,
logicExpression: string, logicExpression: string,
roles: IViewRoleDTO[], roles: IViewRoleDTO[],
columns: IViewColumnDTO[], columns: IViewColumnDTO[],
}; };
export interface IViewsService { export interface IViewsService {
listViews(tenantId: number, resourceModel: string): Promise<void>; listResourceViews(tenantId: number, resourceModel: string): Promise<IView[]>;
newView(tenantId: number, viewDTO: IViewDTO): Promise<void>; newView(tenantId: number, viewDTO: IViewDTO): Promise<void>;
editView(tenantId: number, viewId: number, viewEditDTO: IViewEditDTO): Promise<void>; editView(tenantId: number, viewId: number, viewEditDTO: IViewEditDTO): Promise<void>;
deleteView(tenantId: number, viewId: number): Promise<void>; deleteView(tenantId: number, viewId: number): Promise<void>;

View File

@@ -1,3 +1,5 @@
export * from './Model';
export * from './InventoryTransaction'; export * from './InventoryTransaction';
export * from './BillPayment'; export * from './BillPayment';
export * from './InventoryCostMethod'; export * from './InventoryCostMethod';

View File

@@ -1,15 +1,20 @@
import { uniqBy } from 'lodash'; import { forEach, uniqBy } from 'lodash';
import { import {
buildFilterRolesJoins, buildFilterRolesJoins,
} from 'lib/ViewRolesBuilder'; } from 'lib/ViewRolesBuilder';
import { IModel } from 'interfaces';
export default class DynamicFilter { export default class DynamicFilter {
model: IModel;
tableName: string;
/** /**
* Constructor. * Constructor.
* @param {String} tableName - * @param {String} tableName -
*/ */
constructor(tableName) { constructor(model) {
this.tableName = tableName; this.model = model;
this.tableName = model.tableName;
this.filters = []; this.filters = [];
} }
@@ -18,7 +23,7 @@ export default class DynamicFilter {
* @param {*} filterRole - * @param {*} filterRole -
*/ */
setFilter(filterRole) { setFilter(filterRole) {
filterRole.setTableName(this.tableName); filterRole.setModel(this.model);
this.filters.push(filterRole); this.filters.push(filterRole);
} }
@@ -38,7 +43,23 @@ export default class DynamicFilter {
buildersCallbacks.forEach((builderCallback) => { buildersCallbacks.forEach((builderCallback) => {
builderCallback(builder); builderCallback(builder);
}); });
buildFilterRolesJoins(this.tableName, uniqBy(tableColumns, 'columnKey'))(builder); buildFilterRolesJoins(this.model, uniqBy(tableColumns, 'columnKey'))(builder);
}; };
} }
/**
* Retrieve response metadata from all filters adapters.
*/
getResponseMeta() {
const responseMeta = {};
this.filters.forEach((filter) => {
const { responseMeta: filterMeta } = filter;
forEach(filterMeta, (value, key) => {
responseMeta[key] = value;
});
});
return responseMeta;
}
} }

View File

@@ -14,14 +14,14 @@ export default class FilterRoles extends DynamicFilterRoleAbstructor {
constructor(filterRoles: IFilterRole[]) { constructor(filterRoles: IFilterRole[]) {
super(); super();
this.filterRoles = filterRoles; this.filterRoles = filterRoles;
this.setResponseMeta();
} }
private buildLogicExpression(): string { private buildLogicExpression(): string {
let expression = ''; let expression = '';
this.filterRoles.forEach((role, index) => { this.filterRoles.forEach((role, index) => {
expression += (index === 0) ? expression += (index === 0) ?
`${role.index} ` : `${role.index} ` : `${role.condition} ${role.index} `;
`${role.condition} ${role.index} `;
}); });
return expression.trim(); return expression.trim();
} }
@@ -32,7 +32,16 @@ export default class FilterRoles extends DynamicFilterRoleAbstructor {
buildQuery() { buildQuery() {
return (builder) => { return (builder) => {
const logicExpression = this.buildLogicExpression(); const logicExpression = this.buildLogicExpression();
buildFilterQuery(this.tableName, this.filterRoles, logicExpression)(builder); buildFilterQuery(this.model, this.filterRoles, logicExpression)(builder);
};
}
/**
* Sets response meta.
*/
setResponseMeta() {
this.responseMeta = {
filterRoles: this.filterRoles
}; };
} }
} }

View File

@@ -1,10 +1,13 @@
import { IFilterRole, IDynamicFilter } from "interfaces"; import { IFilterRole, IDynamicFilter, IModel } from "interfaces";
export default class DynamicFilterAbstructor implements IDynamicFilter { export default class DynamicFilterAbstructor implements IDynamicFilter {
filterRoles: IFilterRole[] = []; filterRoles: IFilterRole[] = [];
tableName: string; tableName: string;
model: IModel;
responseMeta: { [key: string]: any } = {};
setTableName(tableName) { setModel(model: IModel) {
this.tableName = tableName; this.model = model;
this.tableName = model.tableName;
} }
} }

View File

@@ -16,10 +16,11 @@ export default class DynamicFilterSortBy extends DynamicFilterRoleAbstructor {
fieldKey: sortByFieldKey, fieldKey: sortByFieldKey,
order: sortDirection, order: sortDirection,
}; };
this.setResponseMeta();
} }
validate() { validate() {
validateFieldKeyExistance(this.tableName, this.sortRole.fieldKey); validateFieldKeyExistance(this.model, this.sortRole.fieldKey);
} }
/** /**
@@ -27,7 +28,7 @@ export default class DynamicFilterSortBy extends DynamicFilterRoleAbstructor {
*/ */
buildQuery() { buildQuery() {
return (builder) => { return (builder) => {
const fieldRelation = getRoleFieldColumn(this.tableName, this.sortRole.fieldKey); const fieldRelation = getRoleFieldColumn(this.model, this.sortRole.fieldKey);
const comparatorColumn = const comparatorColumn =
fieldRelation.relationColumn || fieldRelation.relationColumn ||
`${this.tableName}.${fieldRelation.column}`; `${this.tableName}.${fieldRelation.column}`;
@@ -37,4 +38,14 @@ export default class DynamicFilterSortBy extends DynamicFilterRoleAbstructor {
} }
}; };
} }
/**
* Sets response meta.
*/
setResponseMeta() {
this.responseMeta = {
sortOrder: this.sortRole.fieldKey,
sortBy: this.sortRole.order,
};
}
} }

View File

@@ -1,25 +1,29 @@
import { IFilterRole } from 'interfaces'; import { omit } from 'lodash';
import { IView, IViewRole } from 'interfaces';
import DynamicFilterRoleAbstructor from 'lib/DynamicFilter/DynamicFilterRoleAbstructor'; import DynamicFilterRoleAbstructor from 'lib/DynamicFilter/DynamicFilterRoleAbstructor';
import { import {
validateViewRoles,
buildFilterQuery, buildFilterQuery,
} from 'lib/ViewRolesBuilder'; } from 'lib/ViewRolesBuilder';
export default class DynamicFilterViews extends DynamicFilterRoleAbstructor { export default class DynamicFilterViews extends DynamicFilterRoleAbstructor {
viewId: number;
logicExpression: string; logicExpression: string;
filterRoles: IViewRole[];
/** /**
* Constructor method. * Constructor method.
* @param {*} filterRoles - Filter roles. * @param {IView} view -
* @param {*} logicExpression - Logic expression.
*/ */
constructor(filterRoles: IFilterRole[], logicExpression: string) { constructor(view: IView) {
super(); super();
this.filterRoles = filterRoles; this.viewId = view.id;
this.logicExpression = logicExpression this.filterRoles = view.roles;
this.logicExpression = view.rolesLogicExpression
.replace('AND', '&&') .replace('AND', '&&')
.replace('OR', '||'); .replace('OR', '||');
this.setResponseMeta();
} }
/** /**
@@ -29,19 +33,26 @@ export default class DynamicFilterViews extends DynamicFilterRoleAbstructor {
return this.logicExpression; return this.logicExpression;
} }
/**
* Validates filter roles.
*/
validate() {
return validateViewRoles(this.filterRoles, this.logicExpression);
}
/** /**
* Builds database query of view roles. * Builds database query of view roles.
*/ */
buildQuery() { buildQuery() {
return (builder) => { return (builder) => {
buildFilterQuery(this.tableName, this.filterRoles, this.logicExpression)(builder); buildFilterQuery(this.model, this.filterRoles, this.logicExpression)(builder);
};
}
/**
* Sets response meta.
*/
setResponseMeta() {
this.responseMeta = {
view: {
logicExpression: this.logicExpression,
filterRoles: this.filterRoles
.map((filterRole) => ({ ...omit(filterRole, ['id', 'viewId']) })),
customViewId: this.viewId,
}
}; };
} }
} }

View File

@@ -1,10 +1,9 @@
import { difference, filter } from 'lodash'; import { difference } from 'lodash';
import moment from 'moment'; import moment from 'moment';
import { Lexer } from 'lib/LogicEvaluation/Lexer'; import { Lexer } from 'lib/LogicEvaluation/Lexer';
import Parser from 'lib/LogicEvaluation/Parser'; import Parser from 'lib/LogicEvaluation/Parser';
import QueryParser from 'lib/LogicEvaluation/QueryParser'; import QueryParser from 'lib/LogicEvaluation/QueryParser';
import resourceFieldsKeys from 'data/ResourceFieldsKeys'; import { IFilterRole, IModel } from 'interfaces';
import { IFilterRole } from 'interfaces';
const numberRoleQueryBuilder = (role: IFilterRole, comparatorColumn: string) => { const numberRoleQueryBuilder = (role: IFilterRole, comparatorColumn: string) => {
switch (role.comparator) { switch (role.comparator) {
@@ -109,19 +108,19 @@ const dateQueryBuilder = (role: IFilterRole, comparatorColumn: string) => {
* @param {String} tableName - Table name of target column. * @param {String} tableName - Table name of target column.
* @param {String} fieldKey - Target column key that stored in resource field. * @param {String} fieldKey - Target column key that stored in resource field.
*/ */
export function getRoleFieldColumn(tableName: string, fieldKey: string) { export function getRoleFieldColumn(model: IModel, fieldKey: string) {
const tableFields = resourceFieldsKeys[tableName]; const tableFields = model.fields;
return (tableFields[fieldKey]) ? tableFields[fieldKey] : null; return (tableFields[fieldKey]) ? tableFields[fieldKey] : null;
} }
/** /**
* Builds roles queries. * Builds roles queries.
* @param {String} tableName - * @param {IModel} model -
* @param {Object} role - * @param {Object} role -
*/ */
export function buildRoleQuery(tableName: string, role: IFilterRole) { export function buildRoleQuery(model: IModel, role: IFilterRole) {
const fieldRelation = getRoleFieldColumn(tableName, role.fieldKey); const fieldRelation = getRoleFieldColumn(model, role.fieldKey);
const comparatorColumn = fieldRelation.relationColumn || `${tableName}.${fieldRelation.column}`; const comparatorColumn = fieldRelation.relationColumn || `${model.tableName}.${fieldRelation.column}`;
switch (fieldRelation.columnType) { switch (fieldRelation.columnType) {
case 'number': case 'number':
@@ -150,26 +149,26 @@ export const getTableFromRelationColumn = (column: string) => {
* @param {String} tableName - * @param {String} tableName -
* @param {Array} roles - * @param {Array} roles -
*/ */
export function buildFilterRolesJoins(tableName: string, roles: IFilterRole[]) { export function buildFilterRolesJoins(model: IModel, roles: IFilterRole[]) {
return (builder) => { return (builder) => {
roles.forEach((role) => { roles.forEach((role) => {
const fieldColumn = getRoleFieldColumn(tableName, role.fieldKey); const fieldColumn = getRoleFieldColumn(model, role.fieldKey);
if (fieldColumn.relation) { if (fieldColumn.relation) {
const joinTable = getTableFromRelationColumn(fieldColumn.relation); const joinTable = getTableFromRelationColumn(fieldColumn.relation);
builder.join(joinTable, `${tableName}.${fieldColumn.column}`, '=', fieldColumn.relation); builder.join(joinTable, `${model.tableName}.${fieldColumn.column}`, '=', fieldColumn.relation);
} }
}); });
}; };
} }
export function buildSortColumnJoin(tableName: string, sortColumnKey: string) { export function buildSortColumnJoin(model: IModel, sortColumnKey: string) {
return (builder) => { return (builder) => {
const fieldColumn = getRoleFieldColumn(tableName, sortColumnKey); const fieldColumn = getRoleFieldColumn(model, sortColumnKey);
if (fieldColumn.relation) { if (fieldColumn.relation) {
const joinTable = getTableFromRelationColumn(fieldColumn.relation); const joinTable = getTableFromRelationColumn(fieldColumn.relation);
builder.join(joinTable, `${tableName}.${fieldColumn.column}`, '=', fieldColumn.relation); builder.join(joinTable, `${model.tableName}.${fieldColumn.column}`, '=', fieldColumn.relation);
} }
}; };
} }
@@ -180,11 +179,11 @@ export function buildSortColumnJoin(tableName: string, sortColumnKey: string) {
* @param {Array} roles - * @param {Array} roles -
* @return {Function} * @return {Function}
*/ */
export function buildFilterRolesQuery(tableName: string, roles: IFilterRole[], logicExpression: string = '') { export function buildFilterRolesQuery(model: IModel, roles: IFilterRole[], logicExpression: string = '') {
const rolesIndexSet = {}; const rolesIndexSet = {};
roles.forEach((role) => { roles.forEach((role) => {
rolesIndexSet[role.index] = buildRoleQuery(tableName, role); rolesIndexSet[role.index] = buildRoleQuery(model, role);
}); });
// Lexer for logic expression. // Lexer for logic expression.
const lexer = new Lexer(logicExpression); const lexer = new Lexer(logicExpression);
@@ -204,9 +203,9 @@ export function buildFilterRolesQuery(tableName: string, roles: IFilterRole[], l
* @param {Array} roles - * @param {Array} roles -
* @param {String} logicExpression - * @param {String} logicExpression -
*/ */
export const buildFilterQuery = (tableName: string, roles, logicExpression: string) => { export const buildFilterQuery = (model: IModel, roles: IFilterRole[], logicExpression: string) => {
return (builder) => { return (builder) => {
buildFilterRolesQuery(tableName, roles, logicExpression)(builder); buildFilterRolesQuery(model, roles, logicExpression)(builder);
}; };
}; };
@@ -240,35 +239,33 @@ export function mapFilterRolesToDynamicFilter(roles) {
* @param {String} columnKey - * @param {String} columnKey -
* @param {String} sortDirection - * @param {String} sortDirection -
*/ */
export function buildSortColumnQuery(tableName: string, columnKey: string, sortDirection: string) { export function buildSortColumnQuery(model: IModel, columnKey: string, sortDirection: string) {
const fieldRelation = getRoleFieldColumn(tableName, columnKey); const fieldRelation = getRoleFieldColumn(model, columnKey);
const sortColumn = fieldRelation.relation || `${tableName}.${fieldRelation.column}`; const sortColumn = fieldRelation.relation || `${model.tableName}.${fieldRelation.column}`;
return (builder) => { return (builder) => {
builder.orderBy(sortColumn, sortDirection); builder.orderBy(sortColumn, sortDirection);
buildSortColumnJoin(tableName, columnKey)(builder); buildSortColumnJoin(model, columnKey)(builder);
}; };
} }
export function validateFilterLogicExpression(logicExpression: string, indexes) { export function validateFilterLogicExpression(logicExpression: string, indexes) {
const logicExpIndexes = logicExpression.match(/\d+/g) || []; const logicExpIndexes = logicExpression.match(/\d+/g) || [];
const diff = !difference(logicExpIndexes.map(Number), indexes).length; const diff = difference(logicExpIndexes.map(Number), indexes);
return (diff.length > 0) ? false : true;
} }
export function validateRolesLogicExpression(logicExpression: string, roles: IFilterRole[]) { export function validateRolesLogicExpression(logicExpression: string, roles: IFilterRole[]) {
return validateFilterLogicExpression(logicExpression, roles.map((r) => r.index)); return validateFilterLogicExpression(logicExpression, roles.map((r) => r.index));
} }
export function validateFieldKeyExistance(tableName: string, fieldKey: string) { export function validateFieldKeyExistance(model: any, fieldKey: string) {
if (!resourceFieldsKeys?.[tableName]?.[fieldKey]) return model?.fields?.[fieldKey] || false;
return fieldKey;
else
return false;
} }
export function validateFilterRolesFieldsExistance(tableName, filterRoles: IFilterRole[]) { export function validateFilterRolesFieldsExistance(model, filterRoles: IFilterRole[]) {
return filterRoles.filter((filterRole: IFilterRole) => { return filterRoles.filter((filterRole: IFilterRole) => {
return validateFieldKeyExistance(tableName, filterRole.fieldKey); return !validateFieldKeyExistance(model, filterRole.fieldKey);
}); });
} }

View File

@@ -4,6 +4,6 @@ import path from 'path';
export default () => i18n.configure({ export default () => i18n.configure({
locales: ['en', 'ar'], locales: ['en', 'ar'],
register: global, register: global,
directory: path.join(global.__root, 'src/locales'), directory: path.join(global.__root, 'locales'),
updateFiles: false updateFiles: false
}) })

View File

@@ -1,4 +1,33 @@
{ {
"Empty": "", "Empty": "",
"Hello": "Hello" "Hello": "Hello",
"Petty Cash": "Petty Cash 2",
"Bank": "Bank",
"Other Income": "Other Income",
"Interest Income": "Interest Income",
"Opening Balance": "Opening Balance",
"Depreciation Expense": "Depreciation Expense",
"Interest Expense": "Interest Expense",
"Sales of Product Income": "Sales of Product Income",
"Inventory Asset": "Inventory Asset",
"Cost of Goods Sold (COGS)": "Cost of Goods Sold (COGS)",
"Accounts Payable": "Accounts Payable",
"Other Expenses": "Other Expenses",
"Payroll Expenses": "Payroll Expenses",
"Fixed Asset": "Fixed Asset",
"Current Asset": "Current Asset",
"Other Asset": "Other Asset",
"Long Term Liability": "Long Term Liability",
"Current Liability": "Current Liability",
"Other Liability": "Other Liability",
"Equity": "Equity",
"Expense": "Expense",
"Other Expense": "Other Expense",
"Income": "Income",
"Accounts Receivable (A/R)": "Accounts Receivable (A/R)",
"Accounts Payable (A/P)": "Accounts Payable (A/P)",
"Inactive": "Inactive",
"Assets": "Assets",
"Liabilities": "Liabilities",
"Expenses": "Expenses",
} }

View File

@@ -8,6 +8,7 @@ import {
} from 'lib/ViewRolesBuilder'; } from 'lib/ViewRolesBuilder';
import { flatToNestedArray } from 'utils'; import { flatToNestedArray } from 'utils';
import DependencyGraph from 'lib/DependencyGraph'; import DependencyGraph from 'lib/DependencyGraph';
import TenantManagerSubscriber from 'subscribers/tenantManager';
export default class Account extends TenantModel { export default class Account extends TenantModel {
/** /**
@@ -24,6 +25,13 @@ export default class Account extends TenantModel {
return ['createdAt', 'updatedAt']; return ['createdAt', 'updatedAt'];
} }
/**
*
*/
static get resourceable() {
return true;
}
/** /**
* Model modifiers. * Model modifiers.
*/ */
@@ -106,4 +114,55 @@ export default class Account extends TenantModel {
accounts, { itemId: 'id', parentItemId: 'parentAccountId' } accounts, { itemId: 'id', parentItemId: 'parentAccountId' }
); );
} }
/**
* Model defined fields.
*/
static get fields() {
return {
name: {
label: 'Name',
column: 'name',
},
type: {
label: 'Account type',
column: 'account_type_id',
relation: 'account_types.id',
relationColumn: 'account_types.key',
},
description: {
label: 'Description',
column: 'description',
},
code: {
label: 'Account code',
column: 'code',
},
root_type: {
label: 'Type',
column: 'account_type_id',
relation: 'account_types.id',
relationColumn: 'account_types.root_type',
},
created_at: {
column: 'created_at',
columnType: 'date',
},
active: {
column: 'active',
},
balance: {
column: 'amount',
columnType: 'number'
},
currency: {
column: 'currency_code',
},
normal: {
column: 'account_type_id',
relation: 'account_types.id',
relationColumn: 'account_types.normal'
},
};
}
} }

View File

@@ -59,10 +59,6 @@ export default class Expense extends TenantModel {
viewRolesBuilder(query, conditionals, expression) { viewRolesBuilder(query, conditionals, expression) {
viewRolesBuilder(conditionals, expression)(query); viewRolesBuilder(conditionals, expression)(query);
}, },
orderBy(query) {
}
}; };
} }
@@ -72,7 +68,6 @@ export default class Expense extends TenantModel {
static get relationMappings() { static get relationMappings() {
const Account = require('models/Account'); const Account = require('models/Account');
const ExpenseCategory = require('models/ExpenseCategory'); const ExpenseCategory = require('models/ExpenseCategory');
const SystemUser = require('system/models/SystemUser');
return { return {
paymentAccount: { paymentAccount: {
@@ -91,14 +86,44 @@ export default class Expense extends TenantModel {
to: 'expense_transaction_categories.expenseId', to: 'expense_transaction_categories.expenseId',
}, },
}, },
};
}
/**
* Model defined fields.
*/
static get fields() {
return {
payment_date: {
column: 'payment_date',
},
payment_account: {
column: 'payment_account_id',
relation: 'accounts.id',
},
amount: {
column: 'total_amount',
},
currency_code: {
column: 'currency_code',
},
reference_no: {
column: 'reference_no'
},
description: {
column: 'description',
},
published: {
column: 'published',
},
user: { user: {
relation: Model.BelongsToOneRelation, column: 'user_id',
modelClass: SystemUser.default, relation: 'users.id',
join: { relationColumn: 'users.id',
from: 'expenses_transactions.userId', },
to: 'users.id', created_at: {
} column: 'created_at',
} },
}; };
} }
} }

View File

@@ -95,4 +95,67 @@ export default class Item extends TenantModel {
}, },
}; };
} }
static get fields() {
return {
type: {
column: 'type',
},
name: {
column: 'name',
},
sellable: {
column: 'sellable',
},
purchasable: {
column: 'purchasable',
},
sell_price: {
column: 'sell_price'
},
cost_price: {
column: 'cost_price',
},
currency_code: {
column: 'currency_code',
},
cost_account: {
column: 'cost_account_id',
relation: 'accounts.id',
},
sell_account: {
column: 'sell_account_id',
relation: 'accounts.id',
},
inventory_account: {
column: 'inventory_account_id',
relation: 'accounts.id',
},
sell_description: {
column: 'sell_description',
},
purchase_description: {
column: 'purchase_description',
},
quantity_on_hand: {
column: 'quantity_on_hand',
},
note: {
column: 'note',
},
category: {
column: 'category_id',
relation: 'categories.id',
},
user: {
column: 'user_id',
relation: 'users.id',
relationColumn: 'users.id',
},
created_at: {
column: 'created_at',
}
};
}
} }

View File

@@ -10,6 +10,10 @@ export default class ItemCategory extends TenantModel {
return 'items_categories'; return 'items_categories';
} }
static get resourceable() {
return true;
}
/** /**
* Timestamps columns. * Timestamps columns.
*/ */
@@ -37,4 +41,43 @@ export default class ItemCategory extends TenantModel {
}, },
}; };
} }
static get fields() {
return {
name: {
column: 'name',
},
description: {
column: 'description',
},
parent_category_id: {
column: 'parent_category_id',
relation: 'items_categories.id',
relationColumn: 'items_categories.id',
},
user: {
column: 'user_id',
relation: 'users.id',
relationColumn: 'users.id',
},
cost_account: {
column: 'cost_account_id',
relation: 'accounts.id',
},
sell_account: {
column: 'sell_account_id',
relation: 'accounts.id',
},
inventory_account: {
column: 'inventory_account_id',
relation: 'accounts.id',
},
cost_method: {
column: 'cost_method',
},
created_at: {
column: 'created_at',
},
};
}
} }

View File

@@ -50,4 +50,41 @@ export default class ManualJournal extends TenantModel {
} }
}; };
} }
/**
* Model defined fields.
*/
static get fields() {
return {
date: {
column: 'date',
},
journal_number: {
column: 'journal_number',
},
reference: {
column: 'reference',
},
status: {
column: 'status',
},
amount: {
column: 'amount',
},
description: {
column: 'description',
},
user: {
column: 'user_id',
relation: 'users.id',
relationColumn: 'users.id',
},
journal_type: {
column: 'journal_type',
},
created_at: {
column: 'created_at',
},
};
}
} }

View File

@@ -34,11 +34,12 @@ export default class ExpenseRepository extends TenantRepository {
* Inserts a new expense object. * Inserts a new expense object.
* @param {IExpense} expense - * @param {IExpense} expense -
*/ */
async create(expense: IExpense): Promise<void> { async create(expenseInput: IExpense): Promise<void> {
const { Expense } = this.models; const { Expense } = this.models;
await Expense.query().insert({ ...expense }); const expense = await Expense.query().insert({ ...expenseInput });
this.flushCache(); this.flushCache();
return expense;
} }
/** /**

View File

@@ -1,3 +1,4 @@
import { IView } from 'interfaces';
import { View } from 'models'; import { View } from 'models';
import TenantRepository from 'repositories/TenantRepository'; import TenantRepository from 'repositories/TenantRepository';
@@ -17,7 +18,6 @@ export default class ViewRepository extends TenantRepository {
this.models = this.tenancy.models(tenantId); this.models = this.tenancy.models(tenantId);
this.cache = this.tenancy.cache(tenantId); this.cache = this.tenancy.cache(tenantId);
this.repositories = this.tenancy.cache(tenantId);
} }
/** /**
@@ -27,17 +27,41 @@ export default class ViewRepository extends TenantRepository {
getById(id: number) { getById(id: number) {
const { View } = this.models; const { View } = this.models;
return this.cache.get(`customView.id.${id}`, () => { return this.cache.get(`customView.id.${id}`, () => {
return View.query().findById(id); return View.query().findById(id)
.withGraphFetched('columns')
.withGraphFetched('roles');
}); });
} }
/** /**
* Retrieve all views of the given resource id. * Retrieve all views of the given resource id.
*/ */
allByResource() { allByResource(resourceModel: string) {
const resourceId = 1; const { View } = this.models;
return this.cache.get(`customView.resource.id.${resourceId}`, () => { return this.cache.get(`customView.resourceModel.${resourceModel}`, () => {
return View.query().where('resource_id', resourceId); return View.query().where('resource_model', resourceModel)
.withGraphFetched('columns')
.withGraphFetched('roles');
}); });
} }
/**
* Inserts a new view to the storage.
* @param {IView} view
*/
async insert(view: IView): Promise<IView> {
const insertedView = await View.query().insertGraph({ ...view });
this.flushCache();
return insertedView;
}
/**
* Flushes repository cache.
*/
flushCache() {
this.cache.delStartWith('customView');
}
} }

View File

@@ -3,7 +3,7 @@ import { difference } from 'lodash';
import { kebabCase } from 'lodash' import { kebabCase } from 'lodash'
import TenancyService from 'services/Tenancy/TenancyService'; import TenancyService from 'services/Tenancy/TenancyService';
import { ServiceError } from 'exceptions'; import { ServiceError } from 'exceptions';
import { IAccountDTO, IAccount, IAccountsFilter } from 'interfaces'; import { IAccountDTO, IAccount, IAccountsFilter, IFilterMeta } from 'interfaces';
import { import {
EventDispatcher, EventDispatcher,
EventDispatcherInterface, EventDispatcherInterface,
@@ -260,17 +260,6 @@ export default class AccountsService {
return foundAccounts.length > 0; return foundAccounts.length > 0;
} }
public async getAccountByType(tenantId: number, accountTypeKey: string) {
const { AccountType, Account } = this.tenancy.models(tenantId);
const accountType = await AccountType.query()
.findOne('key', accountTypeKey);
const account = await Account.query()
.findOne('account_type_id', accountType.id);
return account;
}
/** /**
* Throws error if the account was prefined. * Throws error if the account was prefined.
* @param {IAccount} account * @param {IAccount} account
@@ -468,9 +457,11 @@ export default class AccountsService {
* @param {number} tenantId * @param {number} tenantId
* @param {IAccountsFilter} accountsFilter * @param {IAccountsFilter} accountsFilter
*/ */
public async getAccountsList(tenantId: number, filter: IAccountsFilter) { public async getAccountsList(
tenantId: number,
filter: IAccountsFilter,
): Promise<{ accounts: IAccount[], filterMeta: IFilterMeta }> {
const { Account } = this.tenancy.models(tenantId); const { Account } = this.tenancy.models(tenantId);
const dynamicList = await this.dynamicListService.dynamicList(tenantId, Account, filter); const dynamicList = await this.dynamicListService.dynamicList(tenantId, Account, filter);
this.logger.info('[accounts] trying to get accounts datatable list.', { tenantId, filter }); this.logger.info('[accounts] trying to get accounts datatable list.', { tenantId, filter });
@@ -478,6 +469,10 @@ export default class AccountsService {
builder.withGraphFetched('type'); builder.withGraphFetched('type');
dynamicList.buildQuery()(builder); dynamicList.buildQuery()(builder);
}); });
return accounts;
return {
accounts,
filterMeta: dynamicList.getResponseMeta(),
};
} }
} }

View File

@@ -7,9 +7,12 @@ import {
ICustomerNewDTO, ICustomerNewDTO,
ICustomerEditDTO, ICustomerEditDTO,
ICustomer, ICustomer,
IPaginationMeta,
ICustomersFilter
} from 'interfaces'; } from 'interfaces';
import { ServiceError } from 'exceptions'; import { ServiceError } from 'exceptions';
import TenancyService from 'services/Tenancy/TenancyService'; import TenancyService from 'services/Tenancy/TenancyService';
import DynamicListingService from 'services/DynamicListing/DynamicListService';
@Service() @Service()
export default class CustomersService { export default class CustomersService {
@@ -19,12 +22,15 @@ export default class CustomersService {
@Inject() @Inject()
tenancy: TenancyService; tenancy: TenancyService;
@Inject()
dynamicListService: DynamicListingService;
/** /**
* Converts customer to contact DTO. * Converts customer to contact DTO.
* @param {ICustomerNewDTO|ICustomerEditDTO} customerDTO * @param {ICustomerNewDTO|ICustomerEditDTO} customerDTO
* @returns {IContactDTO} * @returns {IContactDTO}
*/ */
customerToContactDTO(customerDTO: ICustomerNewDTO|ICustomerEditDTO) { private customerToContactDTO(customerDTO: ICustomerNewDTO | ICustomerEditDTO) {
return { return {
...omit(customerDTO, ['customerType']), ...omit(customerDTO, ['customerType']),
contactType: customerDTO.customerType, contactType: customerDTO.customerType,
@@ -39,7 +45,7 @@ export default class CustomersService {
* @param {ICustomerNewDTO} customerDTO * @param {ICustomerNewDTO} customerDTO
* @return {Promise<void>} * @return {Promise<void>}
*/ */
async newCustomer(tenantId: number, customerDTO: ICustomerNewDTO) { public async newCustomer(tenantId: number, customerDTO: ICustomerNewDTO) {
const contactDTO = this.customerToContactDTO(customerDTO) const contactDTO = this.customerToContactDTO(customerDTO)
const customer = await this.contactService.newContact(tenantId, contactDTO, 'customer'); const customer = await this.contactService.newContact(tenantId, contactDTO, 'customer');
@@ -59,7 +65,7 @@ export default class CustomersService {
* @param {number} tenantId * @param {number} tenantId
* @param {ICustomerEditDTO} customerDTO * @param {ICustomerEditDTO} customerDTO
*/ */
async editCustomer(tenantId: number, customerId: number, customerDTO: ICustomerEditDTO) { public async editCustomer(tenantId: number, customerId: number, customerDTO: ICustomerEditDTO) {
const contactDTO = this.customerToContactDTO(customerDTO); const contactDTO = this.customerToContactDTO(customerDTO);
return this.contactService.editContact(tenantId, customerId, contactDTO, 'customer'); return this.contactService.editContact(tenantId, customerId, contactDTO, 'customer');
} }
@@ -70,7 +76,7 @@ export default class CustomersService {
* @param {number} customerId * @param {number} customerId
* @return {Promise<void>} * @return {Promise<void>}
*/ */
async deleteCustomer(tenantId: number, customerId: number) { public async deleteCustomer(tenantId: number, customerId: number) {
const { Contact } = this.tenancy.models(tenantId); const { Contact } = this.tenancy.models(tenantId);
await this.getCustomerByIdOrThrowError(tenantId, customerId); await this.getCustomerByIdOrThrowError(tenantId, customerId);
@@ -88,10 +94,34 @@ export default class CustomersService {
* @param {number} tenantId * @param {number} tenantId
* @param {number} customerId * @param {number} customerId
*/ */
async getCustomer(tenantId: number, customerId: number) { public async getCustomer(tenantId: number, customerId: number) {
return this.contactService.getContact(tenantId, customerId, 'customer'); return this.contactService.getContact(tenantId, customerId, 'customer');
} }
/**
* Retrieve customers paginated list.
* @param {number} tenantId - Tenant id.
* @param {ICustomersFilter} filter - Cusotmers filter.
*/
public async getCustomersList(
tenantId: number,
filter: ICustomersFilter
): Promise<{ customers: ICustomer[], pagination: IPaginationMeta, filterMeta: IFilterMeta }> {
const { Contact } = this.tenancy.models(tenantId);
const dynamicList = await this.dynamicListService.dynamicList(tenantId, Contact, filter);
const { results, pagination } = await Contact.query().onBuild((query) => {
query.modify('customer');
dynamicList.buildQuery()(query);
});
return {
customers: results,
pagination,
filterMeta: dynamicList.getResponseMeta(),
};
}
/** /**
* Writes customer opening balance journal entries. * Writes customer opening balance journal entries.
* @param {number} tenantId * @param {number} tenantId

View File

@@ -1,6 +1,6 @@
import { Service, Inject } from "typedi"; import { Service, Inject } from "typedi";
import validator from 'is-my-json-valid'; import validator from 'is-my-json-valid';
import { Router, Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
import { ServiceError } from 'exceptions'; import { ServiceError } from 'exceptions';
import { import {
DynamicFilter, DynamicFilter,
@@ -12,8 +12,13 @@ import {
validateFieldKeyExistance, validateFieldKeyExistance,
validateFilterRolesFieldsExistance, validateFilterRolesFieldsExistance,
} from 'lib/ViewRolesBuilder'; } from 'lib/ViewRolesBuilder';
import {
IDynamicListFilterDTO,
IFilterRole,
IDynamicListService,
IModel,
} from 'interfaces';
import TenancyService from 'services/Tenancy/TenancyService'; import TenancyService from 'services/Tenancy/TenancyService';
import { IDynamicListFilterDTO, IFilterRole, IDynamicListService } from 'interfaces';
const ERRORS = { const ERRORS = {
VIEW_NOT_FOUND: 'view_not_found', VIEW_NOT_FOUND: 'view_not_found',
@@ -32,11 +37,11 @@ export default class DynamicListService implements IDynamicListService {
* @param {number} viewId * @param {number} viewId
* @return {Promise<IView>} * @return {Promise<IView>}
*/ */
private async getCustomViewOrThrowError(tenantId: number, viewId: number) { private async getCustomViewOrThrowError(tenantId: number, viewId: number, model: IModel) {
const { viewRepository } = this.tenancy.repositories(tenantId); const { viewRepository } = this.tenancy.repositories(tenantId);
const view = await viewRepository.getById(viewId); const view = await viewRepository.getById(viewId);
if (!view || view.resourceModel !== 'Account') { if (!view || view.resourceModel !== model.name) {
throw new ServiceError(ERRORS.VIEW_NOT_FOUND); throw new ServiceError(ERRORS.VIEW_NOT_FOUND);
} }
return view; return view;
@@ -49,9 +54,9 @@ export default class DynamicListService implements IDynamicListService {
* @throws {ServiceError} * @throws {ServiceError}
*/ */
private validateSortColumnExistance(model: any, columnSortBy: string) { private validateSortColumnExistance(model: any, columnSortBy: string) {
const notExistsField = validateFieldKeyExistance(model.tableName, columnSortBy); const notExistsField = validateFieldKeyExistance(model, columnSortBy);
if (notExistsField) { if (!notExistsField) {
throw new ServiceError(ERRORS.SORT_COLUMN_NOT_FOUND); throw new ServiceError(ERRORS.SORT_COLUMN_NOT_FOUND);
} }
} }
@@ -62,8 +67,10 @@ export default class DynamicListService implements IDynamicListService {
* @param {IFilterRole[]} filterRoles * @param {IFilterRole[]} filterRoles
* @throws {ServiceError} * @throws {ServiceError}
*/ */
private validateRolesFieldsExistance(model: any, filterRoles: IFilterRole[]) { private validateRolesFieldsExistance(model: IModel, filterRoles: IFilterRole[]) {
const invalidFieldsKeys = validateFilterRolesFieldsExistance(model.tableName, filterRoles); const invalidFieldsKeys = validateFilterRolesFieldsExistance(model, filterRoles);
console.log(invalidFieldsKeys);
if (invalidFieldsKeys.length > 0) { if (invalidFieldsKeys.length > 0) {
throw new ServiceError(ERRORS.FILTER_ROLES_FIELDS_NOT_FOUND); throw new ServiceError(ERRORS.FILTER_ROLES_FIELDS_NOT_FOUND);
@@ -96,23 +103,21 @@ export default class DynamicListService implements IDynamicListService {
* Dynamic listing. * Dynamic listing.
* @param {number} tenantId * @param {number} tenantId
* @param {IModel} model * @param {IModel} model
* @param {IAccountsFilter} filter * @param {IDynamicListFilterDTO} filter
*/ */
async dynamicList(tenantId: number, model: any, filter: IDynamicListFilterDTO) { public async dynamicList(tenantId: number, model: IModel, filter: IDynamicListFilterDTO) {
const { viewRoleRepository } = this.tenancy.repositories(tenantId); const dynamicFilter = new DynamicFilter(model);
const dynamicFilter = new DynamicFilter(model.tableName);
// Custom view filter roles. // Custom view filter roles.
if (filter.customViewId) { if (filter.customViewId) {
const view = await this.getCustomViewOrThrowError(tenantId, filter.customViewId); const view = await this.getCustomViewOrThrowError(tenantId, filter.customViewId, model);
const viewRoles = await viewRoleRepository.allByView(view.id);
const viewFilter = new DynamicFilterViews(viewRoles, view.rolesLogicExpression); const viewFilter = new DynamicFilterViews(view);
dynamicFilter.setFilter(viewFilter); dynamicFilter.setFilter(viewFilter);
} }
// Sort by the given column. // Sort by the given column.
if (filter.columnSortBy) { if (filter.columnSortBy) {
this.validateSortColumnExistance(model, filter.columnSortBy);; this.validateSortColumnExistance(model, filter.columnSortBy);
const sortByFilter = new DynamicFilterSortBy( const sortByFilter = new DynamicFilterSortBy(
filter.columnSortBy, filter.sortOrder filter.columnSortBy, filter.sortOrder
@@ -138,7 +143,7 @@ export default class DynamicListService implements IDynamicListService {
* @param {Response} res * @param {Response} res
* @param {NextFunction} next * @param {NextFunction} next
*/ */
handlerErrorsToResponse(error, req: Request, res: Response, next: NextFunction) { public handlerErrorsToResponse(error: Error, req: Request, res: Response, next: NextFunction) {
if (error instanceof ServiceError) { if (error instanceof ServiceError) {
if (error.errorType === 'sort_column_not_found') { if (error.errorType === 'sort_column_not_found') {
return res.boom.badRequest(null, { return res.boom.badRequest(null, {
@@ -147,8 +152,8 @@ export default class DynamicListService implements IDynamicListService {
} }
if (error.errorType === 'view_not_found') { if (error.errorType === 'view_not_found') {
return res.boom.badRequest(null, { return res.boom.badRequest(null, {
errors: [{ type: 'CUSTOM.VIEW.NOT.FOUND', code: 100 }] errors: [{ type: 'CUSTOM.VIEW.NOT.FOUND', code: 100 }],
}) });
} }
if (error.errorType === 'filter_roles_fields_not_found') { if (error.errorType === 'filter_roles_fields_not_found') {
return res.boom.badRequest(null, { return res.boom.badRequest(null, {

View File

@@ -9,7 +9,7 @@ import { ServiceError } from "exceptions";
import TenancyService from 'services/Tenancy/TenancyService'; import TenancyService from 'services/Tenancy/TenancyService';
import JournalPoster from 'services/Accounting/JournalPoster'; import JournalPoster from 'services/Accounting/JournalPoster';
import JournalCommands from 'services/Accounting/JournalCommands'; import JournalCommands from 'services/Accounting/JournalCommands';
import { IExpense, IAccount, IExpenseDTO, IExpensesService, ISystemUser } from 'interfaces'; import { IExpense, IExpensesFilter, IAccount, IExpenseDTO, IExpensesService, ISystemUser, IPaginationMeta } from 'interfaces';
import DynamicListingService from 'services/DynamicListing/DynamicListService'; import DynamicListingService from 'services/DynamicListing/DynamicListService';
import events from 'subscribers/events'; import events from 'subscribers/events';
@@ -442,17 +442,23 @@ export default class ExpensesService implements IExpensesService {
* @param {IExpensesFilter} expensesFilter * @param {IExpensesFilter} expensesFilter
* @return {IExpense[]} * @return {IExpense[]}
*/ */
public async getExpensesList(tenantId: number, expensesFilter: IExpensesFilter) { public async getExpensesList(
tenantId: number,
expensesFilter: IExpensesFilter
): Promise<{ expenses: IExpense[], pagination: IPaginationMeta, filterMeta: IFilterMeta }> {
const { Expense } = this.tenancy.models(tenantId); const { Expense } = this.tenancy.models(tenantId);
const dynamicFilter = await this.dynamicListService.dynamicList(tenantId, Expense, expensesFilter); const dynamicFilter = await this.dynamicListService.dynamicList(tenantId, Expense, expensesFilter);
this.logger.info('[expense] trying to get expenses datatable list.', { tenantId, expensesFilter }); this.logger.info('[expense] trying to get expenses datatable list.', { tenantId, expensesFilter });
const expenses = await Expense.query().onBuild((builder) => { const { results, pagination } = await Expense.query().onBuild((builder) => {
builder.withGraphFetched('paymentAccount'); builder.withGraphFetched('paymentAccount');
builder.withGraphFetched('user');
dynamicFilter.buildQuery()(builder); dynamicFilter.buildQuery()(builder);
}); }).pagination(expensesFilter.page - 1, expensesFilter.pageSize);
return expenses;
return {
expenses: results,
pagination, filterMeta:
dynamicFilter.getResponseMeta(),
};
} }
} }

View File

@@ -8,7 +8,6 @@ import {
IItemCategoriesFilter, IItemCategoriesFilter,
ISystemUser, ISystemUser,
} from "interfaces"; } from "interfaces";
import ItemCategory from "models/ItemCategory";
import DynamicListingService from 'services/DynamicListing/DynamicListService'; import DynamicListingService from 'services/DynamicListing/DynamicListService';
import TenancyService from 'services/Tenancy/TenancyService'; import TenancyService from 'services/Tenancy/TenancyService';
@@ -21,6 +20,7 @@ const ERRORS = {
SELL_ACCOUNT_NOT_FOUND: 'SELL_ACCOUNT_NOT_FOUND', SELL_ACCOUNT_NOT_FOUND: 'SELL_ACCOUNT_NOT_FOUND',
INVENTORY_ACCOUNT_NOT_FOUND: 'INVENTORY_ACCOUNT_NOT_FOUND', INVENTORY_ACCOUNT_NOT_FOUND: 'INVENTORY_ACCOUNT_NOT_FOUND',
INVENTORY_ACCOUNT_NOT_INVENTORY: 'INVENTORY_ACCOUNT_NOT_INVENTORY', INVENTORY_ACCOUNT_NOT_INVENTORY: 'INVENTORY_ACCOUNT_NOT_INVENTORY',
CATEGORY_HAVE_ITEMS: 'CATEGORY_HAVE_ITEMS'
}; };
export default class ItemCategoriesService implements IItemCategoriesService { export default class ItemCategoriesService implements IItemCategoriesService {
@@ -202,6 +202,7 @@ export default class ItemCategoriesService implements IItemCategoriesService {
public async deleteItemCategory(tenantId: number, itemCategoryId: number, authorizedUser: ISystemUser) { public async deleteItemCategory(tenantId: number, itemCategoryId: number, authorizedUser: ISystemUser) {
this.logger.info('[item_category] trying to delete item category.', { tenantId, itemCategoryId }); this.logger.info('[item_category] trying to delete item category.', { tenantId, itemCategoryId });
await this.getItemCategoryOrThrowError(tenantId, itemCategoryId); await this.getItemCategoryOrThrowError(tenantId, itemCategoryId);
await this.unassociateItemsWithCategories(tenantId, itemCategoryId);
const { ItemCategory } = this.tenancy.models(tenantId); const { ItemCategory } = this.tenancy.models(tenantId);
await ItemCategory.query().findById(itemCategoryId).delete(); await ItemCategory.query().findById(itemCategoryId).delete();
@@ -233,10 +234,16 @@ export default class ItemCategoriesService implements IItemCategoriesService {
const dynamicList = await this.dynamicListService.dynamicList(tenantId, ItemCategory, filter); const dynamicList = await this.dynamicListService.dynamicList(tenantId, ItemCategory, filter);
const itemCategories = await ItemCategory.query().onBuild((query) => { const itemCategories = await ItemCategory.query().onBuild((query) => {
query.orderBy('createdAt', 'ASC');
dynamicList.buildQuery()(query); dynamicList.buildQuery()(query);
}); });
return itemCategories; return { itemCategories, filterMeta: dynamicList.getResponseMeta() };
}
private async unassociateItemsWithCategories(tenantId: number, itemCategoryId: number|number[]) {
const { Item } = this.tenancy.models(tenantId);
const ids = Array.isArray(itemCategoryId) ? itemCategoryId : [itemCategoryId];
await Item.query().whereIn('id', ids).patch({ category_id: null });
} }
/** /**
@@ -247,6 +254,7 @@ export default class ItemCategoriesService implements IItemCategoriesService {
public async deleteItemCategories(tenantId: number, itemCategoriesIds: number[], authorizedUser: ISystemUser) { public async deleteItemCategories(tenantId: number, itemCategoriesIds: number[], authorizedUser: ISystemUser) {
this.logger.info('[item_category] trying to delete item categories.', { tenantId, itemCategoriesIds }); this.logger.info('[item_category] trying to delete item categories.', { tenantId, itemCategoriesIds });
await this.getItemCategoriesOrThrowError(tenantId, itemCategoriesIds); await this.getItemCategoriesOrThrowError(tenantId, itemCategoriesIds);
await this.unassociateItemsWithCategories(tenantId, itemCategoriesIds);
await ItemCategory.query().whereIn('id', itemCategoriesIds).delete(); await ItemCategory.query().whereIn('id', itemCategoriesIds).delete();
this.logger.info('[item_category] item categories deleted successfully.', { tenantId, itemCategoriesIds }); this.logger.info('[item_category] item categories deleted successfully.', { tenantId, itemCategoriesIds });

View File

@@ -4,7 +4,6 @@ import { IItemsFilter, IItemsService, IItemDTO, IItem } from 'interfaces';
import DynamicListingService from 'services/DynamicListing/DynamicListService'; import DynamicListingService from 'services/DynamicListing/DynamicListService';
import TenancyService from 'services/Tenancy/TenancyService'; import TenancyService from 'services/Tenancy/TenancyService';
import { ServiceError } from "exceptions"; import { ServiceError } from "exceptions";
import { Item } from "models";
const ERRORS = { const ERRORS = {
NOT_FOUND: 'NOT_FOUND', NOT_FOUND: 'NOT_FOUND',
@@ -17,6 +16,9 @@ const ERRORS = {
INVENTORY_ACCOUNT_NOT_FOUND: 'INVENTORY_ACCOUNT_NOT_FOUND', INVENTORY_ACCOUNT_NOT_FOUND: 'INVENTORY_ACCOUNT_NOT_FOUND',
INVENTORY_ACCOUNT_NOT_INVENTORY: 'INVENTORY_ACCOUNT_NOT_INVENTORY', INVENTORY_ACCOUNT_NOT_INVENTORY: 'INVENTORY_ACCOUNT_NOT_INVENTORY',
ITEMS_HAVE_ASSOCIATED_TRANSACTIONS: 'ITEMS_HAVE_ASSOCIATED_TRANSACTIONS',
ITEM_HAS_ASSOCIATED_TRANSACTINS: 'ITEM_HAS_ASSOCIATED_TRANSACTINS'
} }
@Service() @Service()
@@ -222,6 +224,7 @@ export default class ItemsService implements IItemsService {
this.logger.info('[items] trying to delete item.', { tenantId, itemId }); this.logger.info('[items] trying to delete item.', { tenantId, itemId });
await this.getItemOrThrowError(tenantId, itemId); await this.getItemOrThrowError(tenantId, itemId);
await this.validateHasNoInvoicesOrBills(tenantId, itemId);
await Item.query().findById(itemId).delete(); await Item.query().findById(itemId).delete();
this.logger.info('[items] deleted successfully.', { tenantId, itemId }); this.logger.info('[items] deleted successfully.', { tenantId, itemId });
@@ -269,6 +272,7 @@ export default class ItemsService implements IItemsService {
this.logger.info('[items] trying to delete items in bulk.', { tenantId, itemsIds }); this.logger.info('[items] trying to delete items in bulk.', { tenantId, itemsIds });
await this.validateItemsIdsExists(tenantId, itemsIds); await this.validateItemsIdsExists(tenantId, itemsIds);
await this.validateHasNoInvoicesOrBills(tenantId, itemsIds);
await Item.query().whereIn('id', itemsIds).delete(); await Item.query().whereIn('id', itemsIds).delete();
this.logger.info('[items] deleted successfully in bulk.', { tenantId, itemsIds }); this.logger.info('[items] deleted successfully in bulk.', { tenantId, itemsIds });
@@ -283,14 +287,39 @@ export default class ItemsService implements IItemsService {
const { Item } = this.tenancy.models(tenantId); const { Item } = this.tenancy.models(tenantId);
const dynamicFilter = await this.dynamicListService.dynamicList(tenantId, Item, itemsFilter); const dynamicFilter = await this.dynamicListService.dynamicList(tenantId, Item, itemsFilter);
const items = await Item.query().onBuild((builder) => { const { results, pagination } = await Item.query().onBuild((builder) => {
builder.withGraphFetched('inventoryAccount'); builder.withGraphFetched('inventoryAccount');
builder.withGraphFetched('sellAccount'); builder.withGraphFetched('sellAccount');
builder.withGraphFetched('costAccount'); builder.withGraphFetched('costAccount');
builder.withGraphFetched('category'); builder.withGraphFetched('category');
dynamicFilter.buildQuery()(builder); dynamicFilter.buildQuery()(builder);
}); }).pagination(
return items; itemsFilter.page - 1,
itemsFilter.pageSize,
);
return { items: results, pagination, filterMeta: dynamicFilter.getResponseMeta() };
}
/**
* Validates the given item or items have no associated invoices or bills.
* @param {number} tenantId - Tenant id.
* @param {number|number[]} itemId - Item id.
* @throws {ServiceError}
*/
private async validateHasNoInvoicesOrBills(tenantId: number, itemId: number[]|number) {
const { ItemEntry } = this.tenancy.models(tenantId);
const ids = Array.isArray(itemId) ? itemId : [itemId];
const foundItemEntries = await ItemEntry.query()
.whereIn('item_id', ids)
.whereIn('reference_type', ['SaleInvoice', 'Bill']);
if (foundItemEntries.length > 0) {
throw new ServiceError(ids.length > 1 ?
ERRORS.ITEMS_HAVE_ASSOCIATED_TRANSACTIONS :
ERRORS.ITEM_HAS_ASSOCIATED_TRANSACTINS
);
}
} }
} }

View File

@@ -9,6 +9,7 @@ import {
ISystemUser, ISystemUser,
IManualJournal, IManualJournal,
IManualJournalEntryDTO, IManualJournalEntryDTO,
IPaginationMeta,
} from 'interfaces'; } from 'interfaces';
import TenancyService from 'services/Tenancy/TenancyService'; import TenancyService from 'services/Tenancy/TenancyService';
import DynamicListingService from 'services/DynamicListing/DynamicListService'; import DynamicListingService from 'services/DynamicListing/DynamicListService';
@@ -227,7 +228,7 @@ export default class ManualJournalsService implements IManuaLJournalsService {
} }
/** /**
* * Transform DTO to model.
* @param {IManualJournalEntryDTO[]} entries * @param {IManualJournalEntryDTO[]} entries
*/ */
private transformDTOToEntriesModel(entries: IManualJournalEntryDTO[]) { private transformDTOToEntriesModel(entries: IManualJournalEntryDTO[]) {
@@ -396,16 +397,23 @@ export default class ManualJournalsService implements IManuaLJournalsService {
* @param {number} tenantId * @param {number} tenantId
* @param {IManualJournalsFilter} filter * @param {IManualJournalsFilter} filter
*/ */
public async getManualJournals(tenantId: number, filter: IManualJournalsFilter) { public async getManualJournals(
tenantId: number,
filter: IManualJournalsFilter
): Promise<{ manualJournals: IManualJournal, pagination: IPaginationMeta, filterMeta: IFilterMeta }> {
const { ManualJournal } = this.tenancy.models(tenantId); const { ManualJournal } = this.tenancy.models(tenantId);
const dynamicList = await this.dynamicListService.dynamicList(tenantId, ManualJournal, filter); const dynamicList = await this.dynamicListService.dynamicList(tenantId, ManualJournal, filter);
this.logger.info('[manual_journals] trying to get manual journals list.', { tenantId, filter }); this.logger.info('[manual_journals] trying to get manual journals list.', { tenantId, filter });
const manualJournal = await ManualJournal.query().onBuild((builder) => { const { results, pagination } = await ManualJournal.query().onBuild((builder) => {
dynamicList.buildQuery()(builder); dynamicList.buildQuery()(builder);
}); }).pagination(filter.page - 1, filter.pageSize);
return manualJournal;
return {
manualJournals: results,
pagination,
filterMeta: dynamicList.getResponseMeta(),
};
} }
/** /**

View File

@@ -1,13 +1,14 @@
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import { omit, sumBy } from 'lodash'; import { omit, sumBy } from 'lodash';
import moment from 'moment'; import moment from 'moment';
import { IBillPaymentOTD, IBillPayment } from 'interfaces'; import { IBillPaymentOTD, IBillPayment, IBillPaymentsFilter, IPaginationMeta, IFilterMeta } from 'interfaces';
import ServiceItemsEntries from 'services/Sales/ServiceItemsEntries'; import ServiceItemsEntries from 'services/Sales/ServiceItemsEntries';
import AccountsService from 'services/Accounts/AccountsService'; import AccountsService from 'services/Accounts/AccountsService';
import JournalPoster from 'services/Accounting/JournalPoster'; import JournalPoster from 'services/Accounting/JournalPoster';
import JournalEntry from 'services/Accounting/JournalEntry'; import JournalEntry from 'services/Accounting/JournalEntry';
import JournalPosterService from 'services/Sales/JournalPosterService'; import JournalPosterService from 'services/Sales/JournalPosterService';
import TenancyService from 'services/Tenancy/TenancyService'; import TenancyService from 'services/Tenancy/TenancyService';
import DynamicListingService from 'services/DynamicListing/DynamicListService';
import { formatDateFields } from 'utils'; import { formatDateFields } from 'utils';
/** /**
@@ -25,6 +26,9 @@ export default class BillPaymentsService {
@Inject() @Inject()
journalService: JournalPosterService; journalService: JournalPosterService;
@Inject()
dynamicListService: DynamicListingService;
/** /**
* Creates a new bill payment transcations and store it to the storage * Creates a new bill payment transcations and store it to the storage
* with associated bills entries and journal transactions. * with associated bills entries and journal transactions.
@@ -39,7 +43,7 @@ export default class BillPaymentsService {
* @param {number} tenantId - Tenant id. * @param {number} tenantId - Tenant id.
* @param {BillPaymentDTO} billPayment - Bill payment object. * @param {BillPaymentDTO} billPayment - Bill payment object.
*/ */
async createBillPayment(tenantId: number, billPaymentDTO: IBillPaymentOTD) { public async createBillPayment(tenantId: number, billPaymentDTO: IBillPaymentOTD) {
const { Bill, BillPayment, BillPaymentEntry, Vendor } = this.tenancy.models(tenantId); const { Bill, BillPayment, BillPaymentEntry, Vendor } = this.tenancy.models(tenantId);
const billPayment = { const billPayment = {
@@ -102,7 +106,7 @@ export default class BillPaymentsService {
* @param {BillPaymentDTO} billPayment * @param {BillPaymentDTO} billPayment
* @param {IBillPayment} oldBillPayment * @param {IBillPayment} oldBillPayment
*/ */
async editBillPayment( public async editBillPayment(
tenantId: number, tenantId: number,
billPaymentId: number, billPaymentId: number,
billPaymentDTO, billPaymentDTO,
@@ -171,7 +175,7 @@ export default class BillPaymentsService {
* @param {Integer} billPaymentId - The given bill payment id. * @param {Integer} billPaymentId - The given bill payment id.
* @return {Promise} * @return {Promise}
*/ */
async deleteBillPayment(tenantId: number, billPaymentId: number) { public async deleteBillPayment(tenantId: number, billPaymentId: number) {
const { BillPayment, BillPaymentEntry, Vendor } = this.tenancy.models(tenantId); const { BillPayment, BillPaymentEntry, Vendor } = this.tenancy.models(tenantId);
const billPayment = await BillPayment.query().where('id', billPaymentId).first(); const billPayment = await BillPayment.query().where('id', billPaymentId).first();
@@ -203,7 +207,7 @@ export default class BillPaymentsService {
* @param {BillPayment} billPayment * @param {BillPayment} billPayment
* @param {Integer} billPaymentId * @param {Integer} billPaymentId
*/ */
async recordPaymentReceiveJournalEntries(tenantId: number, billPayment) { private async recordPaymentReceiveJournalEntries(tenantId: number, billPayment) {
const { AccountTransaction, Account } = this.tenancy.models(tenantId); const { AccountTransaction, Account } = this.tenancy.models(tenantId);
const paymentAmount = sumBy(billPayment.entries, 'payment_amount'); const paymentAmount = sumBy(billPayment.entries, 'payment_amount');
@@ -252,6 +256,35 @@ export default class BillPaymentsService {
]); ]);
} }
/**
* Retrieve bill payment paginted and filterable list.
* @param {number} tenantId
* @param {IBillPaymentsFilter} billPaymentsFilter
*/
public async listBillPayments(
tenantId: number,
billPaymentsFilter: IBillPaymentsFilter,
): Promise<{ billPayments: IBillPayment, pagination: IPaginationMeta, filterMeta: IFilterMeta }> {
const { BillPayment } = this.tenancy.models(tenantId);
const dynamicFilter = await this.dynamicListService.dynamicList(tenantId, BillPayment, billPaymentsFilter);
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);
}).pagination(
billPaymentsFilter.page - 1,
billPaymentsFilter.pageSize,
);
return {
billPayments: results,
pagination,
filterMeta: dynamicFilter.getResponseMeta(),
};
}
/** /**
* Retrieve bill payment with associated metadata. * Retrieve bill payment with associated metadata.
* @param {number} billPaymentId - The bill payment id. * @param {number} billPaymentId - The bill payment id.

View File

@@ -1,5 +0,0 @@
export default class ResourceService {
}

View File

@@ -0,0 +1,78 @@
import { Service, Inject } from 'typedi';
import { camelCase, upperFirst } from 'lodash'
import { IModel } from 'interfaces';
import resourceFieldsKeys from 'data/ResourceFieldsKeys';
import TenancyService from 'services/Tenancy/TenancyService';
@Service()
export default class ResourceService {
@Inject()
tenancy: TenancyService;
/**
*
* @param {string} resourceName
*/
getResourceFieldsRelations(modelName: string) {
const fieldsRelations = resourceFieldsKeys[modelName];
if (!fieldsRelations) {
throw new Error('Fields relation not found in thte given resource model.');
}
return fieldsRelations;
}
/**
* Transform resource to model name.
* @param {string} resourceName
*/
private resourceToModelName(resourceName: string): string {
return upperFirst(camelCase(resourceName));
}
/**
* Retrieve model from resource name in specific tenant.
* @param {number} tenantId
* @param {string} resourceName
*/
public getModel(tenantId: number, resourceName: string) {
const models = this.tenancy.models(tenantId);
const modelName = this.resourceToModelName(resourceName);
return models[modelName];
}
getModelFields(Model: IModel) {
const fields = Object.keys(Model.fields);
return fields.sort((a, b) => {
if (a < b) { return -1; }
if (a > b) { return 1; }
return 0;
});
}
/**
*
* @param {string} resourceName
*/
getResourceFields(Model: IModel) {
console.log(Model);
if (Model.resourceable) {
return this.getModelFields(Model);
}
return [];
}
/**
*
* @param {string} resourceName
*/
getResourceColumns(Model: IModel) {
if (Model.resourceable) {
return this.getModelFields(Model);
}
return [];
}
}

View File

@@ -10,6 +10,7 @@ import ServiceItemsEntries from 'services/Sales/ServiceItemsEntries';
import PaymentReceiveEntryRepository from 'repositories/PaymentReceiveEntryRepository'; import PaymentReceiveEntryRepository from 'repositories/PaymentReceiveEntryRepository';
import CustomerRepository from 'repositories/CustomerRepository'; import CustomerRepository from 'repositories/CustomerRepository';
import TenancyService from 'services/Tenancy/TenancyService'; import TenancyService from 'services/Tenancy/TenancyService';
import DynamicListingService from 'services/DynamicListing/DynamicListService';
import { formatDateFields } from 'utils'; import { formatDateFields } from 'utils';
/** /**
@@ -27,6 +28,9 @@ export default class PaymentReceiveService {
@Inject() @Inject()
journalService: JournalPosterService; journalService: JournalPosterService;
@Inject()
dynamicListService: DynamicListingService;
@Inject('logger') @Inject('logger')
logger: any; logger: any;
@@ -37,7 +41,7 @@ export default class PaymentReceiveService {
* @param {number} tenantId - Tenant id. * @param {number} tenantId - Tenant id.
* @param {IPaymentReceive} paymentReceive * @param {IPaymentReceive} paymentReceive
*/ */
async createPaymentReceive(tenantId: number, paymentReceive: IPaymentReceiveOTD) { public async createPaymentReceive(tenantId: number, paymentReceive: IPaymentReceiveOTD) {
const { const {
PaymentReceive, PaymentReceive,
PaymentReceiveEntry, PaymentReceiveEntry,
@@ -107,7 +111,7 @@ export default class PaymentReceiveService {
* @param {IPaymentReceive} paymentReceive - * @param {IPaymentReceive} paymentReceive -
* @param {IPaymentReceive} oldPaymentReceive - * @param {IPaymentReceive} oldPaymentReceive -
*/ */
async editPaymentReceive( public async editPaymentReceive(
tenantId: number, tenantId: number,
paymentReceiveId: number, paymentReceiveId: number,
paymentReceive: any, paymentReceive: any,
@@ -242,7 +246,7 @@ export default class PaymentReceiveService {
* @param {number} tenantId - Tenant id. * @param {number} tenantId - Tenant id.
* @param {Integer} paymentReceiveId - Payment receive id. * @param {Integer} paymentReceiveId - Payment receive id.
*/ */
async getPaymentReceive(tenantId: number, paymentReceiveId: number) { public async getPaymentReceive(tenantId: number, paymentReceiveId: number) {
const { PaymentReceive } = this.tenancy.models(tenantId); const { PaymentReceive } = this.tenancy.models(tenantId);
const paymentReceive = await PaymentReceive.query() const paymentReceive = await PaymentReceive.query()
.where('id', paymentReceiveId) .where('id', paymentReceiveId)
@@ -251,6 +255,30 @@ export default class PaymentReceiveService {
return paymentReceive; return paymentReceive;
} }
/**
* Retrieve payment receives paginated and filterable list.
* @param {number} tenantId
* @param {IPaymentReceivesFilter} paymentReceivesFilter
*/
public async listPaymentReceives(tenantId: number, paymentReceivesFilter: IPaymentReceivesFilter) {
const { PaymentReceive } = this.tenancy.models(tenantId);
const dynamicFilter = await this.dynamicListService.dynamicList(tenantId, PaymentReceive, paymentReceivesFilter);
const { results, pagination } = await PaymentReceive.query().onBuild((builder) => {
builder.withGraphFetched('customer');
builder.withGraphFetched('depositAccount');
dynamicFilter.buildQuery()(builder);
}).pagination(
paymentReceivesFilter.page - 1,
paymentReceivesFilter.pageSize,
);
return {
paymentReceives: results,
pagination,
filterMeta: dynamicFilter.getResponseMeta(),
};
}
/** /**
* Retrieve the payment receive details with associated invoices. * Retrieve the payment receive details with associated invoices.
* @param {Integer} paymentReceiveId * @param {Integer} paymentReceiveId
@@ -310,7 +338,7 @@ export default class PaymentReceiveService {
* @param {IPaymentReceive} paymentReceive * @param {IPaymentReceive} paymentReceive
* @param {Number} paymentReceiveId * @param {Number} paymentReceiveId
*/ */
async recordPaymentReceiveJournalEntries( private async recordPaymentReceiveJournalEntries(
tenantId: number, tenantId: number,
paymentReceive: any, paymentReceive: any,
paymentReceiveId?: number paymentReceiveId?: number
@@ -370,7 +398,7 @@ export default class PaymentReceiveService {
* @param {Array} revertInvoices * @param {Array} revertInvoices
* @return {Promise} * @return {Promise}
*/ */
async revertInvoicePaymentAmount(tenantId: number, revertInvoices: any[]) { private async revertInvoicePaymentAmount(tenantId: number, revertInvoices: any[]) {
const { SaleInvoice } = this.tenancy.models(tenantId); const { SaleInvoice } = this.tenancy.models(tenantId);
const opers: Promise<T>[] = []; const opers: Promise<T>[] = [];
@@ -392,7 +420,7 @@ export default class PaymentReceiveService {
* @param {Array} newPaymentReceiveEntries * @param {Array} newPaymentReceiveEntries
* @return * @return
*/ */
async saveChangeInvoicePaymentAmount( private async saveChangeInvoicePaymentAmount(
tenantId: number, tenantId: number,
paymentReceiveEntries: [], paymentReceiveEntries: [],
newPaymentReceiveEntries: [], newPaymentReceiveEntries: [],

View File

@@ -1,8 +1,10 @@
import { omit, difference, sumBy, mixin } from 'lodash'; import { omit, difference, sumBy, mixin } from 'lodash';
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import { IEstimatesFilter, IFilterMeta, IPaginationMeta } from 'interfaces';
import HasItemsEntries from 'services/Sales/HasItemsEntries'; import HasItemsEntries from 'services/Sales/HasItemsEntries';
import { formatDateFields } from 'utils'; import { formatDateFields } from 'utils';
import TenancyService from 'services/Tenancy/TenancyService'; import TenancyService from 'services/Tenancy/TenancyService';
import DynamicListingService from 'services/DynamicListing/DynamicListService';
/** /**
* Sale estimate service. * Sale estimate service.
@@ -19,6 +21,9 @@ export default class SaleEstimateService {
@Inject('logger') @Inject('logger')
logger: any; logger: any;
@Inject()
dynamicListService: DynamicListingService;
/** /**
* Creates a new estimate with associated entries. * Creates a new estimate with associated entries.
* @async * @async
@@ -208,4 +213,32 @@ export default class SaleEstimateService {
}); });
return foundEstimates.length > 0; return foundEstimates.length > 0;
} }
/**
* Retrieves estimates filterable and paginated list.
* @param {number} tenantId
* @param {IEstimatesFilter} estimatesFilter
*/
public async estimatesList(
tenantId: number,
estimatesFilter: IEstimatesFilter
): Promise<{ salesEstimates: ISaleEstimate[], pagination: IPaginationMeta, filterMeta: IFilterMeta }> {
const { SaleEstimate } = this.tenancy.models(tenantId);
const dynamicFilter = await this.dynamicListService.dynamicList(tenantId, SaleEstimate, estimatesFilter);
const { results, pagination } = await SaleEstimate.query().onBuild(builder => {
builder.withGraphFetched('customer');
builder.withGraphFetched('entries');
dynamicFilter.buildQuery()(builder);
}).pagination(
estimatesFilter.page - 1,
estimatesFilter.pageSize,
);
return {
salesEstimates: results,
pagination,
filterMeta: dynamicFilter.getResponseMeta(),
};
}
} }

View File

@@ -1,11 +1,16 @@
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import { omit, sumBy, difference, pick, chain } from 'lodash'; import { omit, sumBy, difference, pick, chain } from 'lodash';
import { ISaleInvoice, ISaleInvoiceOTD, IItemEntry } from 'interfaces'; import {
EventDispatcher,
EventDispatcherInterface,
} from 'decorators/eventDispatcher';
import { ISaleInvoice, ISaleInvoiceOTD, IItemEntry, ISalesInvoicesFilter, IPaginationMeta, IFilterMeta } from 'interfaces';
import JournalPoster from 'services/Accounting/JournalPoster'; import JournalPoster from 'services/Accounting/JournalPoster';
import HasItemsEntries from 'services/Sales/HasItemsEntries'; import HasItemsEntries from 'services/Sales/HasItemsEntries';
import InventoryService from 'services/Inventory/Inventory'; import InventoryService from 'services/Inventory/Inventory';
import SalesInvoicesCost from 'services/Sales/SalesInvoicesCost'; import SalesInvoicesCost from 'services/Sales/SalesInvoicesCost';
import TenancyService from 'services/Tenancy/TenancyService'; import TenancyService from 'services/Tenancy/TenancyService';
import DynamicListingService from 'services/DynamicListing/DynamicListService';
import { formatDateFields } from 'utils'; import { formatDateFields } from 'utils';
/** /**
@@ -26,6 +31,12 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
@Inject('logger') @Inject('logger')
logger: any; logger: any;
@Inject()
dynamicListService: DynamicListingService;
@EventDispatcher()
eventDispatcher: EventDispatcherInterface;
/** /**
* Creates a new sale invoices and store it to the storage * Creates a new sale invoices and store it to the storage
* with associated to entries and journal transactions. * with associated to entries and journal transactions.
@@ -34,7 +45,7 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
* @param {ISaleInvoice} saleInvoiceDTO - * @param {ISaleInvoice} saleInvoiceDTO -
* @return {ISaleInvoice} * @return {ISaleInvoice}
*/ */
async createSaleInvoice(tenantId: number, saleInvoiceDTO: ISaleInvoiceOTD) { public async createSaleInvoice(tenantId: number, saleInvoiceDTO: ISaleInvoiceOTD) {
const { SaleInvoice, Customer, ItemEntry } = this.tenancy.models(tenantId); const { SaleInvoice, Customer, ItemEntry } = this.tenancy.models(tenantId);
const balance = sumBy(saleInvoiceDTO.entries, e => ItemEntry.calcAmount(e)); const balance = sumBy(saleInvoiceDTO.entries, e => ItemEntry.calcAmount(e));
@@ -94,7 +105,7 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
* @param {Number} saleInvoiceId - * @param {Number} saleInvoiceId -
* @param {ISaleInvoice} saleInvoice - * @param {ISaleInvoice} saleInvoice -
*/ */
async editSaleInvoice(tenantId: number, saleInvoiceId: number, saleInvoiceDTO: any) { public async editSaleInvoice(tenantId: number, saleInvoiceId: number, saleInvoiceDTO: any) {
const { SaleInvoice, ItemEntry, Customer } = this.tenancy.models(tenantId); const { SaleInvoice, ItemEntry, Customer } = this.tenancy.models(tenantId);
const balance = sumBy(saleInvoiceDTO.entries, e => ItemEntry.calcAmount(e)); const balance = sumBy(saleInvoiceDTO.entries, e => ItemEntry.calcAmount(e));
@@ -152,7 +163,7 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
* @async * @async
* @param {Number} saleInvoiceId - The given sale invoice id. * @param {Number} saleInvoiceId - The given sale invoice id.
*/ */
async deleteSaleInvoice(tenantId: number, saleInvoiceId: number) { public async deleteSaleInvoice(tenantId: number, saleInvoiceId: number) {
const { const {
SaleInvoice, SaleInvoice,
ItemEntry, ItemEntry,
@@ -215,7 +226,7 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
* @param {number} saleInvoiceId - * @param {number} saleInvoiceId -
* @param {boolean} override - * @param {boolean} override -
*/ */
recordInventoryTranscactions( private recordInventoryTranscactions(
tenantId: number, tenantId: number,
saleInvoice, saleInvoice,
saleInvoiceId: number, saleInvoiceId: number,
@@ -243,7 +254,7 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
* @param {string} transactionType * @param {string} transactionType
* @param {number} transactionId * @param {number} transactionId
*/ */
async revertInventoryTransactions(tenantId: number, inventoryTransactions: array) { private async revertInventoryTransactions(tenantId: number, inventoryTransactions: array) {
const { InventoryTransaction } = this.tenancy.models(tenantId); const { InventoryTransaction } = this.tenancy.models(tenantId);
const opers: Promise<[]>[] = []; const opers: Promise<[]>[] = [];
@@ -280,7 +291,7 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
* @async * @async
* @param {Number} saleInvoiceId * @param {Number} saleInvoiceId
*/ */
async getSaleInvoiceWithEntries(tenantId: number, saleInvoiceId: number) { public async getSaleInvoiceWithEntries(tenantId: number, saleInvoiceId: number) {
const { SaleInvoice } = this.tenancy.models(tenantId); const { SaleInvoice } = this.tenancy.models(tenantId);
return SaleInvoice.query() return SaleInvoice.query()
.where('id', saleInvoiceId) .where('id', saleInvoiceId)
@@ -405,4 +416,27 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
journal.saveBalance(), journal.saveBalance(),
]); ]);
} }
/**
* Retrieve sales invoices filterable and paginated list.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
public async salesInvoicesList(tenantId: number, salesInvoicesFilter: ISalesInvoicesFilter):
Promise<{ salesInvoices: ISaleInvoice[], pagination: IPaginationMeta, filterMeta: IFilterMeta }> {
const { SaleInvoice } = this.tenancy.models(tenantId);
const dynamicFilter = await this.dynamicListService.dynamicList(tenantId, SaleInvoice, salesInvoicesFilter);
this.logger.info('[sale_invoice] try to get sales invoices list.', { tenantId, salesInvoicesFilter });
const { results, pagination } = await SaleInvoice.query().onBuild((builder) => {
builder.withGraphFetched('entries');
builder.withGraphFetched('customer');
dynamicFilter.buildQuery()(builder);
}).pagination(
salesInvoicesFilter.page - 1,
salesInvoicesFilter.pageSize,
);
return { salesInvoices: results, pagination, filterMeta: dynamicFilter.getResponseMeta() };
}
} }

View File

@@ -4,12 +4,17 @@ import JournalPosterService from 'services/Sales/JournalPosterService';
import HasItemEntries from 'services/Sales/HasItemsEntries'; import HasItemEntries from 'services/Sales/HasItemsEntries';
import TenancyService from 'services/Tenancy/TenancyService'; import TenancyService from 'services/Tenancy/TenancyService';
import { formatDateFields } from 'utils'; import { formatDateFields } from 'utils';
import { IFilterMeta, IPaginationMeta } from 'interfaces';
import DynamicListingService from 'services/DynamicListing/DynamicListService';
@Service() @Service()
export default class SalesReceiptService { export default class SalesReceiptService {
@Inject() @Inject()
tenancy: TenancyService; tenancy: TenancyService;
@Inject()
dynamicListService: DynamicListingService;
@Inject() @Inject()
journalService: JournalPosterService; journalService: JournalPosterService;
@@ -22,7 +27,7 @@ export default class SalesReceiptService {
* @param {ISaleReceipt} saleReceipt * @param {ISaleReceipt} saleReceipt
* @return {Object} * @return {Object}
*/ */
async createSaleReceipt(tenantId: number, saleReceiptDTO: any) { public async createSaleReceipt(tenantId: number, saleReceiptDTO: any) {
const { SaleReceipt, ItemEntry } = this.tenancy.models(tenantId); const { SaleReceipt, ItemEntry } = this.tenancy.models(tenantId);
const amount = sumBy(saleReceiptDTO.entries, e => ItemEntry.calcAmount(e)); const amount = sumBy(saleReceiptDTO.entries, e => ItemEntry.calcAmount(e));
@@ -55,7 +60,7 @@ export default class SalesReceiptService {
* @param {ISaleReceipt} saleReceipt * @param {ISaleReceipt} saleReceipt
* @return {void} * @return {void}
*/ */
async editSaleReceipt(tenantId: number, saleReceiptId: number, saleReceiptDTO: any) { public async editSaleReceipt(tenantId: number, saleReceiptId: number, saleReceiptDTO: any) {
const { SaleReceipt, ItemEntry } = this.tenancy.models(tenantId); const { SaleReceipt, ItemEntry } = this.tenancy.models(tenantId);
const amount = sumBy(saleReceiptDTO.entries, e => ItemEntry.calcAmount(e)); const amount = sumBy(saleReceiptDTO.entries, e => ItemEntry.calcAmount(e));
@@ -88,7 +93,7 @@ export default class SalesReceiptService {
* @param {Integer} saleReceiptId * @param {Integer} saleReceiptId
* @return {void} * @return {void}
*/ */
async deleteSaleReceipt(tenantId: number, saleReceiptId: number) { public async deleteSaleReceipt(tenantId: number, saleReceiptId: number) {
const { SaleReceipt, ItemEntry } = this.tenancy.models(tenantId); const { SaleReceipt, ItemEntry } = this.tenancy.models(tenantId);
const deleteSaleReceiptOper = SaleReceipt.query() const deleteSaleReceiptOper = SaleReceipt.query()
.where('id', saleReceiptId) .where('id', saleReceiptId)
@@ -160,4 +165,35 @@ export default class SalesReceiptService {
return saleReceipt; return saleReceipt;
} }
/**
* Retrieve sales receipts paginated and filterable list.
* @param {number} tenantId
* @param {ISaleReceiptFilter} salesReceiptsFilter
*/
public async salesReceiptsList(
tenantId: number,
salesReceiptsFilter: ISaleReceiptFilter,
): Promise<{ salesReceipts: ISaleReceipt[], pagination: IPaginationMeta, filterMeta: IFilterMeta }> {
const { SaleReceipt } = this.tenancy.models(tenantId);
const dynamicFilter = await this.dynamicListService.dynamicList(tenantId, SaleReceipt, salesReceiptsFilter);
this.logger.info('[sale_receipt] try to get sales receipts list.', { tenantId });
const { results, pagination } = await SaleReceipt.query().onBuild((builder) => {
builder.withGraphFetched('depositAccount');
builder.withGraphFetched('customer');
builder.withGraphFetched('entries');
dynamicFilter.buildQuery()(builder);
}).pagination(
salesReceiptsFilter.page - 1,
salesReceiptsFilter.pageSize,
);
return {
salesReceipts: results,
pagination,
filterMeta: dynamicFilter.getResponseMeta(),
};
}
} }

View File

@@ -27,7 +27,6 @@ export default class HasTenancyService {
singletonService(tenantId: number, key: string, callback: Function) { singletonService(tenantId: number, key: string, callback: Function) {
const container = this.tenantContainer(tenantId); const container = this.tenantContainer(tenantId);
const Logger = Container.get('logger'); const Logger = Container.get('logger');
const hasServiceInstnace = container.has(key); const hasServiceInstnace = container.has(key);
if (!hasServiceInstnace) { if (!hasServiceInstnace) {
@@ -74,12 +73,24 @@ export default class HasTenancyService {
}); });
} }
/**
* Sets i18n locals function.
* @param {number} tenantId
* @param locals
*/
setI18nLocals(tenantId: number, locals: any) {
return this.singletonService(tenantId, 'i18n', () => {
return locals;
})
}
/** /**
* Retrieve i18n locales methods. * Retrieve i18n locales methods.
* @param {number} tenantId - Tenant id. * @param {number} tenantId - Tenant id.
*/ */
i18n(tenantId: number) { i18n(tenantId: number) {
return this.singletonService(tenantId, 'i18n', () => { return this.singletonService(tenantId, 'i18n', () => {
throw new Error('I18n locals is not set yet.');
}); });
} }

View File

@@ -1,20 +1,24 @@
import { Service, Inject } from "typedi"; import { Service, Inject } from "typedi";
import { pick, difference } from 'lodash'; import { difference } from 'lodash';
import { ServiceError } from 'exceptions'; import { ServiceError } from 'exceptions';
import { import {
IViewsService, IViewsService,
IViewDTO, IViewDTO,
IView, IView,
IViewRole, IViewEditDTO,
IViewHasColumn,
} from 'interfaces'; } from 'interfaces';
import TenancyService from 'services/Tenancy/TenancyService'; import TenancyService from 'services/Tenancy/TenancyService';
import ResourceService from "services/Resource/ResourceService";
import { validateRolesLogicExpression } from 'lib/ViewRolesBuilder'; import { validateRolesLogicExpression } from 'lib/ViewRolesBuilder';
const ERRORS = { const ERRORS = {
VIEW_NOT_FOUND: 'VIEW_NOT_FOUND', VIEW_NOT_FOUND: 'VIEW_NOT_FOUND',
VIEW_PREDEFINED: 'VIEW_PREDEFINED', VIEW_PREDEFINED: 'VIEW_PREDEFINED',
INVALID_LOGIC_EXPRESSION: 'INVALID_LOGIC_EXPRESSION', VIEW_NAME_NOT_UNIQUE: 'VIEW_NAME_NOT_UNIQUE',
LOGIC_EXPRESSION_INVALID: 'INVALID_LOGIC_EXPRESSION',
RESOURCE_FIELDS_KEYS_NOT_FOUND: 'RESOURCE_FIELDS_KEYS_NOT_FOUND',
RESOURCE_COLUMNS_KEYS_NOT_FOUND: 'RESOURCE_COLUMNS_KEYS_NOT_FOUND',
RESOURCE_MODEL_NOT_FOUND: 'RESOURCE_MODEL_NOT_FOUND'
}; };
@Service() @Service()
@@ -25,29 +29,131 @@ export default class ViewsService implements IViewsService {
@Inject('logger') @Inject('logger')
logger: any; logger: any;
@Inject()
resourceService: ResourceService;
/** /**
* Listing resource views. * Listing resource views.
* @param {number} tenantId * @param {number} tenantId -
* @param {string} resourceModel * @param {string} resourceModel -
*/ */
public async listViews(tenantId: number, resourceModel: string) { public async listResourceViews(tenantId: number, resourceModel: string): Promise<IView[]> {
const { View } = this.tenancy.models(tenantId); this.logger.info('[views] trying to retrieve resource views.', { tenantId, resourceModel });
return View.query().where('resource_model', resourceModel);
}
validateResourceFieldsExistance() { // Validate the resource model name is valid.
this.getResourceModelOrThrowError(tenantId, resourceModel);
}
validateResourceColumnsExistance() {
}
getView(tenantId: number, viewId: number) {
const { viewRepository } = this.tenancy.repositories(tenantId);
return viewRepository.allByResource(resourceModel);
} }
/** /**
* Validate model resource conditions fields existance.
* @param {string} resourceName
* @param {IViewRoleDTO[]} viewRoles
*/
private validateResourceRolesFieldsExistance(ResourceModel: IModel, viewRoles: IViewRoleDTO[]) {
const resourceFieldsKeys = this.resourceService.getResourceFields(ResourceModel);
const fieldsKeys = viewRoles.map(viewRole => viewRole.fieldKey);
const notFoundFieldsKeys = difference(fieldsKeys, resourceFieldsKeys);
if (notFoundFieldsKeys.length > 0) {
throw new ServiceError(ERRORS.RESOURCE_FIELDS_KEYS_NOT_FOUND);
}
return notFoundFieldsKeys;
}
/**
* Validates model resource columns existance.
* @param {string} resourceName
* @param {IViewColumnDTO[]} viewColumns
*/
private validateResourceColumnsExistance(ResourceModel: IModel, viewColumns: IViewColumnDTO[]) {
const resourceFieldsKeys = this.resourceService.getResourceColumns(ResourceModel);
const fieldsKeys = viewColumns.map((viewColumn: IViewColumnDTO) => viewColumn.fieldKey);
const notFoundFieldsKeys = difference(fieldsKeys, resourceFieldsKeys);
if (notFoundFieldsKeys.length > 0) {
throw new ServiceError(ERRORS.RESOURCE_COLUMNS_KEYS_NOT_FOUND);
}
return notFoundFieldsKeys;
}
/**
* Retrieve the given view details with associated conditions and columns.
* @param {number} tenantId - Tenant id.
* @param {number} viewId - View id.
*/
public getView(tenantId: number, viewId: number): Promise<IView> {
this.logger.info('[view] trying to get view from storage.', { tenantId, viewId });
return this.getViewOrThrowError(tenantId, viewId);
}
/**
* Retrieve view or throw not found error.
* @param {number} tenantId - Tenant id.
* @param {number} viewId - View id.
*/
private async getViewOrThrowError(tenantId: number, viewId: number): Promise<IView> {
const { viewRepository } = this.tenancy.repositories(tenantId);
this.logger.info('[view] trying to get view from storage.', { tenantId, viewId });
const view = await viewRepository.getById(viewId);
if (!view) {
this.logger.info('[view] view not found.', { tenantId, viewId });
throw new ServiceError(ERRORS.VIEW_NOT_FOUND);
}
return view;
}
/**
* Retrieve resource model from resource name or throw not found error.
* @param {number} tenantId
* @param {number} resourceModel
*/
private getResourceModelOrThrowError(tenantId: number, resourceModel: string): IModel {
const ResourceModel = this.resourceService.getModel(tenantId, resourceModel);
if (!ResourceModel || !ResourceModel.resourceable) {
throw new ServiceError(ERRORS.RESOURCE_MODEL_NOT_FOUND);
}
return ResourceModel;
}
/**
* Validates view name uniqiness in the given resource.
* @param {number} tenantId
* @param {stirng} resourceModel
* @param {string} viewName
* @param {number} notViewId
*/
private async validateViewNameUniquiness(
tenantId: number,
resourceModel: string,
viewName: string,
notViewId?: number
): void {
const { View } = this.tenancy.models(tenantId);
const foundViews = await View.query()
.where('resource_model', resourceModel)
.where('name', viewName)
.onBuild((builder) => {
if (notViewId) {
builder.whereNot('id', notViewId);
}
});
if (foundViews.length > 0) {
throw new ServiceError(ERRORS.VIEW_NAME_NOT_UNIQUE);
}
}
/**
* Creates a new custom view to specific resource.
* ----
* Precedures. * Precedures.
* ---- * ----
* - Validate resource fields existance. * - Validate resource fields existance.
@@ -60,116 +166,78 @@ export default class ViewsService implements IViewsService {
* @param {number} tenantId - Tenant id. * @param {number} tenantId - Tenant id.
* @param {IViewDTO} viewDTO - View DTO. * @param {IViewDTO} viewDTO - View DTO.
*/ */
async newView(tenantId: number, viewDTO: IViewDTO): Promise<void> { public async newView(tenantId: number, viewDTO: IViewDTO): Promise<IView> {
const { View, ViewColumn, ViewRole } = this.tenancy.models(tenantId); const { viewRepository } = this.tenancy.repositories(tenantId);
this.logger.info('[views] trying to create a new view.', { tenantId, viewDTO }); this.logger.info('[views] trying to create a new view.', { tenantId, viewDTO });
// Validate the resource name is exists and resourcable.
const ResourceModel = this.getResourceModelOrThrowError(tenantId, viewDTO.resourceModel);
// Validate view name uniquiness.
await this.validateViewNameUniquiness(tenantId, viewDTO.resourceModel, viewDTO.name);
// Validate the given fields keys exist on the storage.
this.validateResourceRolesFieldsExistance(ResourceModel, viewDTO.roles);
// Validate the given columnable fields keys exists on the storage.
this.validateResourceColumnsExistance(ResourceModel, viewDTO.columns);
// Validates the view conditional logic expression. // Validates the view conditional logic expression.
if (!validateRolesLogicExpression(viewDTO.logicExpression, viewDTO.roles)) { if (!validateRolesLogicExpression(viewDTO.logicExpression, viewDTO.roles)) {
throw new ServiceError(ERRORS.INVALID_LOGIC_EXPRESSION); throw new ServiceError(ERRORS.LOGIC_EXPRESSION_INVALID);
} }
// Save view details. // Save view details.
const view = await View.query().insert({ const view = await viewRepository.insert({
name: viewDTO.name,
predefined: false, predefined: false,
name: viewDTO.name,
rolesLogicExpression: viewDTO.logicExpression, rolesLogicExpression: viewDTO.logicExpression,
resourceModel: viewDTO.resourceModel,
roles: viewDTO.roles,
columns: viewDTO.columns,
}); });
this.logger.info('[views] inserted to the storage.', { tenantId, viewDTO }); this.logger.info('[views] inserted to the storage successfully.', { tenantId, viewDTO });
return view;
// Save view roles async operations.
const saveViewRolesOpers = [];
viewDTO.roles.forEach((role) => {
const saveViewRoleOper = ViewRole.query().insert({
...pick(role, ['fieldKey', 'comparator', 'value', 'index']),
viewId: view.id,
});
saveViewRolesOpers.push(saveViewRoleOper);
});
viewDTO.columns.forEach((column) => {
const saveViewColumnOper = ViewColumn.query().insert({
viewId: view.id,
index: column.index,
});
saveViewRolesOpers.push(saveViewColumnOper);
});
this.logger.info('[views] roles and columns inserted to the storage.', { tenantId, viewDTO });
await Promise.all(saveViewRolesOpers);
} }
/** /**
* Edits view details, roles and columns on the storage.
* --------
* Precedures.
* --------
* - Validate view existance.
* - Validate view resource fields existance.
* - Validate view resource columns existance.
* - Validate view logic expression.
* - Delete old view columns and roles.
* - Re-save view columns and roles.
* *
* @param {number} tenantId * @param {number} tenantId
* @param {number} viewId * @param {number} viewId
* @param {IViewEditDTO} * @param {IViewEditDTO}
*/ */
async editView(tenantId: number, viewId: number, viewEditDTO: IViewEditDTO) { public async editView(tenantId: number, viewId: number, viewEditDTO: IViewEditDTO): Promise<void> {
const { View, ViewRole, ViewColumn } = req.models; const { View } = this.tenancy.models(tenantId);
const view = await View.query().where('id', viewId) this.logger.info('[view] trying to edit custom view.', { tenantId, viewId });
.withGraphFetched('roles.field')
.withGraphFetched('columns')
.first();
const errorReasons = []; // Retrieve view details or throw not found error.
const fieldsSlugs = viewEditDTO.roles.map((role) => role.field_key); const view = await this.getViewOrThrowError(tenantId, viewId);
const resourceFieldsKeys = resource.fields.map((f) => f.key);
const resourceFieldsKeysMap = new Map(resource.fields.map((field) => [field.key, field]));
const columnsKeys = viewEditDTO.columns.map((c) => c.key);
// The difference between the stored resource fields and submit fields keys. // Validate the resource name is exists and resourcable.
const notFoundFields = difference(fieldsSlugs, resourceFieldsKeys); const ResourceModel = this.getResourceModelOrThrowError(tenantId, view.resourceModel);
// Validate not found resource fields keys. // Validate view name uniquiness.
if (notFoundFields.length > 0) { await this.validateViewNameUniquiness(tenantId, view.resourceModel, viewEditDTO.name, viewId);
errorReasons.push({
type: 'RESOURCE_FIELDS_NOT_EXIST', code: 100, fields: notFoundFields, // Validate the given fields keys exist on the storage.
}); this.validateResourceRolesFieldsExistance(ResourceModel, view.roles);
}
// The difference between the stored resource fields and the submit columns keys. // Validate the given columnable fields keys exists on the storage.
const notFoundColumns = difference(columnsKeys, resourceFieldsKeys); this.validateResourceColumnsExistance(ResourceModel, view.columns);
// Validate not found view columns.
if (notFoundColumns.length > 0) {
errorReasons.push({ type: 'RESOURCE_COLUMNS_NOT_EXIST', code: 200, columns: notFoundColumns });
}
// Validates the view conditional logic expression. // Validates the view conditional logic expression.
if (!validateViewRoles(viewEditDTO.roles, viewEditDTO.logicExpression)) { if (!validateRolesLogicExpression(viewEditDTO.logicExpression, viewEditDTO.roles)) {
errorReasons.push({ type: 'VIEW.ROLES.LOGIC.EXPRESSION.INVALID', code: 400 }); throw new ServiceError(ERRORS.LOGIC_EXPRESSION_INVALID);
} }
const viewRolesIds = view.roles.map((r) => r.id);
const viewColumnsIds = view.columns.map((c) => c.id);
const formUpdatedRoles = viewEditDTO.roles.filter((r) => r.id);
const formInsertRoles = viewEditDTO.roles.filter((r) => !r.id);
const formRolesIds = formUpdatedRoles.map((r) => r.id);
const formUpdatedColumns = viewEditDTO.columns.filter((r) => r.id);
const formInsertedColumns = viewEditDTO.columns.filter((r) => !r.id);
const formColumnsIds = formUpdatedColumns.map((r) => r.id);
const rolesIdsShouldDeleted = difference(viewRolesIds, formRolesIds);
const columnsIdsShouldDelete = difference(viewColumnsIds, formColumnsIds);
const notFoundViewRolesIds = difference(formRolesIds, viewRolesIds);
const notFoundViewColumnsIds = difference(viewColumnsIds, viewColumnsIds);
// Validate the not found view roles ids.
if (notFoundViewRolesIds.length) {
errorReasons.push({ type: 'VIEW.ROLES.IDS.NOT.FOUND', code: 500, ids: notFoundViewRolesIds });
}
// Validate the not found view columns ids.
if (notFoundViewColumnsIds.length) {
errorReasons.push({ type: 'VIEW.COLUMNS.IDS.NOT.FOUND', code: 600, ids: notFoundViewColumnsIds });
}
if (errorReasons.length > 0) {
return res.status(400).send({ errors: errorReasons });
}
const asyncOpers = [];
// Save view details. // Save view details.
await View.query() await View.query()
.where('id', view.id) .where('id', view.id)
@@ -177,78 +245,15 @@ export default class ViewsService implements IViewsService {
name: viewEditDTO.name, name: viewEditDTO.name,
roles_logic_expression: viewEditDTO.logicExpression, roles_logic_expression: viewEditDTO.logicExpression,
}); });
this.logger.info('[view] edited successfully.', { tenantId, viewId });
// Update view roles.
if (formUpdatedRoles.length > 0) {
formUpdatedRoles.forEach((role) => {
const fieldModel = resourceFieldsKeysMap.get(role.field_key);
const updateOper = ViewRole.query()
.where('id', role.id)
.update({
...pick(role, ['comparator', 'value', 'index']),
field_id: fieldModel.id,
});
asyncOpers.push(updateOper);
});
}
// Insert a new view roles.
if (formInsertRoles.length > 0) {
formInsertRoles.forEach((role) => {
const fieldModel = resourceFieldsKeysMap.get(role.field_key);
const insertOper = ViewRole.query()
.insert({
...pick(role, ['comparator', 'value', 'index']),
field_id: fieldModel.id,
view_id: view.id,
});
asyncOpers.push(insertOper);
});
}
// Delete view roles.
if (rolesIdsShouldDeleted.length > 0) {
const deleteOper = ViewRole.query()
.whereIn('id', rolesIdsShouldDeleted)
.delete();
asyncOpers.push(deleteOper);
}
// Insert a new view columns to the storage.
if (formInsertedColumns.length > 0) {
formInsertedColumns.forEach((column) => {
const fieldModel = resourceFieldsKeysMap.get(column.key);
const insertOper = ViewColumn.query()
.insert({
field_id: fieldModel.id,
index: column.index,
view_id: view.id,
});
asyncOpers.push(insertOper);
});
}
// Update the view columns on the storage.
if (formUpdatedColumns.length > 0) {
formUpdatedColumns.forEach((column) => {
const updateOper = ViewColumn.query()
.where('id', column.id)
.update({
index: column.index,
});
asyncOpers.push(updateOper);
});
}
// Delete the view columns from the storage.
if (columnsIdsShouldDelete.length > 0) {
const deleteOper = ViewColumn.query()
.whereIn('id', columnsIdsShouldDelete)
.delete();
asyncOpers.push(deleteOper);
}
await Promise.all(asyncOpers);
} }
/** /**
* Retrieve views details of the given id or throw not found error. * Retrieve views details of the given id or throw not found error.
* @private
* @param {number} tenantId * @param {number} tenantId
* @param {number} viewId * @param {number} viewId
* @return {Promise<IView>}
*/ */
private async getViewByIdOrThrowError(tenantId: number, viewId: number): Promise<IView> { private async getViewByIdOrThrowError(tenantId: number, viewId: number): Promise<IView> {
const { View } = this.tenancy.models(tenantId); const { View } = this.tenancy.models(tenantId);
@@ -267,6 +272,7 @@ export default class ViewsService implements IViewsService {
* Deletes the given view with associated roles and columns. * Deletes the given view with associated roles and columns.
* @param {number} tenantId - Tenant id. * @param {number} tenantId - Tenant id.
* @param {number} viewId - View id. * @param {number} viewId - View id.
* @return {Promise<void>}
*/ */
public async deleteView(tenantId: number, viewId: number): Promise<void> { public async deleteView(tenantId: number, viewId: number): Promise<void> {
const { View } = this.tenancy.models(tenantId); const { View } = this.tenancy.models(tenantId);