diff --git a/client/src/containers/Setup/WizardSetupSteps.js b/client/src/containers/Setup/WizardSetupSteps.js
index aaa39d6b7..d29cb4abe 100644
--- a/client/src/containers/Setup/WizardSetupSteps.js
+++ b/client/src/containers/Setup/WizardSetupSteps.js
@@ -24,7 +24,7 @@ function WizardSetupSteps({
{registerWizardSteps.map((step, index) => (
))}
diff --git a/client/src/style/pages/authentication.scss b/client/src/style/pages/authentication.scss
index 05379fe82..81e1c4e75 100644
--- a/client/src/style/pages/authentication.scss
+++ b/client/src/style/pages/authentication.scss
@@ -145,32 +145,33 @@
}
}
- // Register Form
- .register-form {
- width: 690px;
- margin: 0px auto;
- padding: 85px 50px;
+ // // Register Form
+ // .register-form {
+ // // width: 690px;
+ // // margin: 0px auto;
+ // // padding: 85px 50px;
+
- &__agreement-section {
- margin-top: -10px;
+ // &__agreement-section {
+ // margin-top: -10px;
- p {
- font-size: 13px;
- margin-top: -10px;
- margin-bottom: 24px;
- line-height: 1.65;
- }
- }
+ // p {
+ // font-size: 13px;
+ // margin-top: -10px;
+ // margin-bottom: 24px;
+ // line-height: 1.65;
+ // }
+ // }
- &__submit-button-wrap {
- margin: 25px 0px 25px 0px;
+ // &__submit-button-wrap {
+ // margin: 25px 0px 25px 0px;
- .bp3-button {
- min-height: 45px;
- background-color: #0052cc;
- }
- }
- }
+ // .bp3-button {
+ // min-height: 45px;
+ // background-color: #0052cc;
+ // }
+ // }
+ // }
.send-reset-password {
.form-group--crediential {
diff --git a/client/src/style/pages/register-wizard-page.scss b/client/src/style/pages/register-wizard-page.scss
index a53e1add5..67ebe31fa 100644
--- a/client/src/style/pages/register-wizard-page.scss
+++ b/client/src/style/pages/register-wizard-page.scss
@@ -1,11 +1,136 @@
+.register-page {
+ .bp3-input {
+ min-height: 40px;
+ border: 1px solid #ced4da;
+ }
+ .bp3-form-group {
+ margin-bottom: 23px;
+
+ &.bp3-intent-danger {
+ .bp3-input {
+ border-color: #eea9a9;
+ }
+ }
+ }
+ .bp3-form-group.has-password-revealer {
+ .bp3-label {
+ display: flex;
+ justify-content: space-between;
+ }
+
+ .password-revealer {
+ .text {
+ font-size: 12px;
+ }
+ }
+ }
+
+ .bp3-button.bp3-fill.bp3-intent-primary {
+ font-size: 16px;
+ }
+
+ &__label-section {
+ margin-bottom: 29px;
+ color: #555;
+
+ h3 {
+ // font-weight: 500;
+ font-weight: 400;
+ // font-size: 28px;
+ font-size: 22px;
+ // color: #444;
+ color: #555555;
+ margin: 0 0 12px;
+ }
+
+ a {
+ text-decoration: underline;
+ color: #0040bd;
+ }
+ }
+
+ &__form-wrapper {
+ width: 100%;
+ // max-width: 415px;
+ // padding: 15px;
+ margin: 0 auto;
+ }
+
+ &__footer-links {
+ padding: 9px;
+ border-top: 1px solid #ddd;
+ border-bottom: 1px solid #ddd;
+ text-align: center;
+ margin-bottom: 1.2rem;
+
+ a {
+ color: #0052cc;
+ }
+ }
+
+ &__loading-overlay {
+ position: absolute;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ right: 0;
+ background: rgba(252, 253, 255, 0.5);
+ display: flex;
+ justify-content: center;
+ }
+
+ &__submit-button-wrap {
+ margin: 0px 0px 24px 0px;
+
+ .bp3-button {
+ background-color: #0052cc;
+ min-height: 45px;
+ }
+ }
+
+ &-form {
+ width: 800px;
+ margin: 0 auto;
+
+ // width: 690px;
+ // padding: 85px 60px;
+ // padding: 85px 105px;
+
+ // Register Form
+ .register-form {
+ padding: 85px 105px;
+
+ &__agreement-section {
+ margin-top: -10px;
+
+ p {
+ font-size: 13px;
+ margin-top: -10px;
+ margin-bottom: 24px;
+ line-height: 1.65;
+ }
+ }
+
+ &__submit-button-wrap {
+ margin: 25px 0px 25px 0px;
+
+ .bp3-button {
+ min-height: 45px;
+ background-color: #0052cc;
+ }
+ }
+ }
+ }
+}
+
.setup-page {
-
+
&__right-section {
padding-left: 25%;
}
- &__left-section{
+ &__left-section {
position: fixed;
background: #778cab;
overflow: auto;
@@ -59,6 +184,8 @@
}
}
}
+
+
.setup-page-steps {
@@ -122,9 +249,6 @@
}
}
-
-// @import './billing.scss';
-
//Register Subscription form
.register-subscription-form {
padding-top: 50px;
diff --git a/server/.env.example b/server/.env.example
index ea99a35cc..437019d76 100644
--- a/server/.env.example
+++ b/server/.env.example
@@ -20,7 +20,7 @@ TENANT_DB_NAME_PERFIX=bigcapital_tenant_
TENANT_DB_HOST=127.0.0.1
TENANT_DB_PASSWORD=root
TEANNT_DB_USER=root
-TENANT_DB_CHARSET=charset
+TENANT_DB_CHARSET=utf8
TENANT_MIGRATIONS_DIR=src/database/migrations
TENANT_SEEDS_DIR=src/database/seeds/core
diff --git a/server/src/api/controllers/Accounts.ts b/server/src/api/controllers/Accounts.ts
index fb3670580..a3126f202 100644
--- a/server/src/api/controllers/Accounts.ts
+++ b/server/src/api/controllers/Accounts.ts
@@ -43,6 +43,15 @@ export default class AccountsController extends BaseController{
asyncMiddleware(this.inactivateAccount.bind(this)),
this.catchServiceErrors,
);
+ router.post(
+ '/:id/close', [
+ ...this.accountParamSchema,
+ ...this.closingAccountSchema,
+ ],
+ this.validationResult,
+ asyncMiddleware(this.closeAccount.bind(this)),
+ this.catchServiceErrors,
+ )
router.post(
'/:id', [
...this.accountDTOSchema,
@@ -127,18 +136,12 @@ export default class AccountsController extends BaseController{
];
}
- /**
- * Account param schema validation.
- */
get accountParamSchema() {
return [
param('id').exists().isNumeric().toInt()
];
}
- /**
- * Accounts list schema validation.
- */
get accountsListSchema() {
return [
query('custom_view_id').optional().isNumeric().toInt(),
@@ -149,9 +152,6 @@ export default class AccountsController extends BaseController{
];
}
- /**
- *
- */
get bulkSelectIdsQuerySchema() {
return [
query('ids').isArray({ min: 2 }),
@@ -159,6 +159,13 @@ export default class AccountsController extends BaseController{
];
}
+ get closingAccountSchema() {
+ return [
+ check('to_account_id').exists().isNumeric().toInt(),
+ check('delete_after_closing').exists().isBoolean(),
+ ]
+ }
+
/**
* Creates a new account.
* @param {Request} req -
@@ -328,8 +335,36 @@ export default class AccountsController extends BaseController{
filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles);
}
try {
- const accounts = await this.accountsService.getAccountsList(tenantId, filter);
- return res.status(200).send({ accounts });
+ const { accounts, filterMeta } = await this.accountsService.getAccountsList(tenantId, filter);
+
+ return res.status(200).send({
+ accounts,
+ filter_meta: this.transfromToResponse(filterMeta)
+ });
+ } catch (error) {
+ next(error);
+ }
+ }
+
+ /**
+ * Closes the given account.
+ * @param {Request} req
+ * @param {Response} res
+ * @param next
+ */
+ async closeAccount(req: Request, res: Response, next: NextFunction) {
+ const { tenantId } = req;
+ const { id: accountId } = req.params;
+ const closeAccountQuery = this.matchedBodyData(req);
+
+ try {
+ await this.accountsService.closeAccount(
+ tenantId,
+ accountId,
+ closeAccountQuery.toAccountId,
+ closeAccountQuery.deleteAfterClosing
+ );
+ return res.status(200).send({ id: accountId });
} catch (error) {
next(error);
}
@@ -358,9 +393,8 @@ export default class AccountsController extends BaseController{
}
if (error.errorType === 'account_type_not_found') {
return res.boom.badRequest(
- 'The given account type not found.', {
- errors: [{ type: 'ACCOUNT_TYPE_NOT_FOUND', code: 200 }]
- }
+ 'The given account type not found.',
+ { errors: [{ type: 'ACCOUNT_TYPE_NOT_FOUND', code: 200 }] }
);
}
if (error.errorType === 'account_type_not_allowed_to_changed') {
@@ -417,6 +451,12 @@ export default class AccountsController extends BaseController{
{ errors: [{ type: 'ACCOUNTS_PREDEFINED', code: 1100 }] }
);
}
+ if (error.errorType === 'close_account_and_to_account_not_same_type') {
+ return res.boom.badRequest(
+ 'The close account has different root type with to account.',
+ { errors: [{ type: 'CLOSE_ACCOUNT_AND_TO_ACCOUNT_NOT_SAME_TYPE', code: 1200 }] },
+ );
+ }
}
next(error)
}
diff --git a/server/src/api/controllers/Authentication.ts b/server/src/api/controllers/Authentication.ts
index e5d162333..e219e5a81 100644
--- a/server/src/api/controllers/Authentication.ts
+++ b/server/src/api/controllers/Authentication.ts
@@ -2,10 +2,9 @@ import { Request, Response, Router } from 'express';
import { check, ValidationChain } from 'express-validator';
import { Service, Inject } from 'typedi';
import BaseController from 'api/controllers/BaseController';
-import validateMiddleware from 'api/middleware/validateMiddleware';
import asyncMiddleware from 'api/middleware/asyncMiddleware';
import AuthenticationService from 'services/Authentication';
-import { IUserOTD, ISystemUser, IRegisterOTD } from 'interfaces';
+import { ILoginDTO, ISystemUser, IRegisterOTD } from 'interfaces';
import { ServiceError, ServiceErrors } from "exceptions";
@Service()
@@ -61,7 +60,6 @@ export default class AuthenticationController extends BaseController{
*/
get registerSchema(): ValidationChain[] {
return [
- check('organization_name').exists().trim().escape(),
check('first_name').exists().trim().escape(),
check('last_name').exists().trim().escape(),
check('email').exists().isEmail().trim().escape(),
@@ -102,7 +100,7 @@ export default class AuthenticationController extends BaseController{
* @param {Response} res
*/
async login(req: Request, res: Response, next: Function): Response {
- const userDTO: IUserOTD = this.matchedBodyData(req);
+ const userDTO: ILoginDTO = this.matchedBodyData(req);
try {
const { token, user, tenant } = await this.authService.signIn(
diff --git a/server/src/api/controllers/BaseController.ts b/server/src/api/controllers/BaseController.ts
index 1ff423c00..6299a9ac0 100644
--- a/server/src/api/controllers/BaseController.ts
+++ b/server/src/api/controllers/BaseController.ts
@@ -1,6 +1,6 @@
import { Response, Request, NextFunction } from 'express';
import { matchedData, validationResult } from "express-validator";
-import { camelCase, omit } from "lodash";
+import { camelCase, snakeCase, omit } from "lodash";
import { mapKeysDeep } from 'utils'
export default class BaseController {
@@ -55,4 +55,12 @@ export default class BaseController {
}
next();
}
+
+ /**
+ * Transform the given data to response.
+ * @param {any} data
+ */
+ transfromToResponse(data: any) {
+ return mapKeysDeep(data, (v, k) => snakeCase(k));
+ }
}
\ No newline at end of file
diff --git a/server/src/api/controllers/Contacts/Customers.ts b/server/src/api/controllers/Contacts/Customers.ts
index b271cf7f9..0ee03b2bc 100644
--- a/server/src/api/controllers/Contacts/Customers.ts
+++ b/server/src/api/controllers/Contacts/Customers.ts
@@ -46,6 +46,12 @@ export default class CustomersController extends ContactsController {
this.validationResult,
asyncMiddleware(this.deleteBulkCustomers.bind(this))
);
+ router.get('/', [
+
+ ],
+ this.validationResult,
+ asyncMiddleware(this.getCustomersList.bind(this))
+ );
router.get('/:id', [
...this.specificContactSchema,
],
@@ -193,4 +199,15 @@ export default class CustomersController extends ContactsController {
next(error);
}
}
+
+
+ async getCustomersList(req: Request, res: Response, next: NextFunction) {
+ const { tenantId } = req;
+
+ try {
+ await this.customersService.getCustomersList(tenantId)
+ } catch (error) {
+ next(error);
+ }
+ }
}
\ No newline at end of file
diff --git a/server/src/api/controllers/Currencies.ts b/server/src/api/controllers/Currencies.ts
index 06f0fbb5e..357083e34 100644
--- a/server/src/api/controllers/Currencies.ts
+++ b/server/src/api/controllers/Currencies.ts
@@ -162,7 +162,7 @@ export default class CurrenciesController extends BaseController {
* @param {Response} res
* @param {NextFunction} next
*/
- handlerServiceError(error, req, res, next) {
+ handlerServiceError(error: Error, req: Request, res: Response, next: NextFunction) {
if (error instanceof ServiceError) {
if (error.errorType === 'currency_not_found') {
return res.boom.badRequest(null, {
diff --git a/server/src/api/controllers/Expenses.ts b/server/src/api/controllers/Expenses.ts
index 668dbba45..5d51cc68a 100644
--- a/server/src/api/controllers/Expenses.ts
+++ b/server/src/api/controllers/Expenses.ts
@@ -7,6 +7,7 @@ import ExpensesService from "services/Expenses/ExpensesService";
import { IExpenseDTO } from 'interfaces';
import { ServiceError } from "exceptions";
import DynamicListingService from 'services/DynamicListing/DynamicListService';
+import { takeWhile } from "lodash";
@Service()
export default class ExpensesController extends BaseController {
@@ -30,7 +31,8 @@ export default class ExpensesController extends BaseController {
asyncMiddleware(this.newExpense.bind(this)),
this.catchServiceErrors,
);
- router.post('/publish', [
+ router.post(
+ '/publish', [
...this.bulkSelectSchema,
],
this.bulkPublishExpenses.bind(this),
@@ -69,11 +71,22 @@ export default class ExpensesController extends BaseController {
this.catchServiceErrors,
);
router.get(
- '/',
+ '/', [
+ ...this.expensesListSchema,
+ ],
+ this.validationResult,
asyncMiddleware(this.getExpensesList.bind(this)),
this.dynamicListService.handlerErrorsToResponse,
this.catchServiceErrors,
);
+ router.get(
+ '/:id', [
+ this.expenseParamSchema,
+ ],
+ this.validationResult,
+ asyncMiddleware(this.getExpense.bind(this)),
+ this.catchServiceErrors,
+ );
return router;
}
@@ -89,6 +102,7 @@ export default class ExpensesController extends BaseController {
check('currency_code').optional(),
check('exchange_rate').optional().isNumeric().toFloat(),
check('publish').optional().isBoolean().toBoolean(),
+
check('categories').exists().isArray({ min: 1 }),
check('categories.*.index').exists().isNumeric().toInt(),
check('categories.*.expense_account_id').exists().isNumeric().toInt(),
@@ -120,6 +134,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
* @param {Request} req
@@ -240,12 +268,41 @@ export default class ExpensesController extends BaseController {
const filter = {
filterRoles: [],
sortOrder: 'asc',
+ columnSortBy: 'created_at',
+ page: 1,
+ pageSize: 12,
...this.matchedQueryData(req),
};
+ if (filter.stringifiedFilterRoles) {
+ filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles);
+ }
try {
- const expenses = await this.expensesService.getExpensesList(tenantId, filter);
- return res.status(200).send({ expenses });
+ const { expenses, pagination, filterMeta } = await this.expensesService.getExpensesList(tenantId, filter);
+
+ return res.status(200).send({
+ expenses,
+ pagination: this.transfromToResponse(pagination),
+ filter_meta: this.transfromToResponse(filterMeta),
+ });
+ } catch (error) {
+ next(error);
+ }
+ }
+
+ /**
+ * Retrieve expense details.
+ * @param {Request} req
+ * @param {Response} res
+ * @param {NextFunction} next
+ */
+ async getExpense(req: Request, res: Response, next: NextFunction) {
+ const { tenantId } = req;
+ const { id: expenseId } = req.params;
+
+ try {
+ const expense = await this.expensesService.getExpense(tenantId, expenseId);
+ return res.status(200).send({ expense });
} catch (error) {
next(error);
}
@@ -259,29 +316,34 @@ export default class ExpensesController extends BaseController {
catchServiceErrors(error: Error, req: Request, res: Response, next: NextFunction) {
if (error instanceof ServiceError) {
if (error.errorType === 'expense_not_found') {
- return res.boom.badRequest(null, {
- errors: [{ type: 'EXPENSE_NOT_FOUND', code: 100 }],
- });
+ return res.boom.badRequest(
+ 'Expense not found.',
+ { errors: [{ type: 'EXPENSE_NOT_FOUND', code: 100 }] }
+ );
}
if (error.errorType === 'total_amount_equals_zero') {
- return res.boom.badRequest(null, {
- errors: [{ type: 'TOTAL.AMOUNT.EQUALS.ZERO', code: 200 }],
- });
+ return res.boom.badRequest(
+ 'Expense total should not equal zero.',
+ { errors: [{ type: 'TOTAL.AMOUNT.EQUALS.ZERO', code: 200 }] },
+ );
}
if (error.errorType === 'payment_account_not_found') {
- return res.boom.badRequest(null, {
- errors: [{ type: 'PAYMENT.ACCOUNT.NOT.FOUND', code: 300 }],
- });
+ return res.boom.badRequest(
+ 'Payment account not found.',
+ { errors: [{ type: 'PAYMENT.ACCOUNT.NOT.FOUND', code: 300 }] },
+ );
}
if (error.errorType === 'some_expenses_not_found') {
- return res.boom.badRequest(null, {
- errors: [{ type: 'SOME.EXPENSE.ACCOUNTS.NOT.FOUND', code: 400 }]
- })
+ return res.boom.badRequest(
+ 'Some expense accounts not found.',
+ { errors: [{ type: 'SOME.EXPENSE.ACCOUNTS.NOT.FOUND', code: 400 }] },
+ );
}
if (error.errorType === 'payment_account_has_invalid_type') {
- return res.boom.badRequest(null, {
- errors: [{ type: 'PAYMENT.ACCOUNT.HAS.INVALID.TYPE', code: 500 }],
- });
+ return res.boom.badRequest(
+ 'Payment account has invalid type.',
+ { errors: [{ type: 'PAYMENT.ACCOUNT.HAS.INVALID.TYPE', code: 500 }], },
+ );
}
if (error.errorType === 'expenses_account_has_invalid_type') {
return res.boom.badRequest(null, {
diff --git a/server/src/api/controllers/ItemCategories.ts b/server/src/api/controllers/ItemCategories.ts
index 0d54a3c0c..8eb5d28e8 100644
--- a/server/src/api/controllers/ItemCategories.ts
+++ b/server/src/api/controllers/ItemCategories.ts
@@ -7,16 +7,19 @@ import {
import ItemCategoriesService from 'services/ItemCategories/ItemCategoriesService';
import { Inject, Service } from 'typedi';
import asyncMiddleware from 'api/middleware/asyncMiddleware';
-import validateMiddleware from 'api/middleware/validateMiddleware';
import { IItemCategoryOTD } from 'interfaces';
import { ServiceError } from 'exceptions';
import BaseController from 'api/controllers/BaseController';
+import DynamicListingService from 'services/DynamicListing/DynamicListService';
@Service()
export default class ItemsCategoriesController extends BaseController {
@Inject()
itemCategoriesService: ItemCategoriesService;
+ @Inject()
+ dynamicListService: DynamicListingService;
+
/**
* Router constructor method.
*/
@@ -27,44 +30,45 @@ export default class ItemsCategoriesController extends BaseController {
...this.categoryValidationSchema,
...this.specificCategoryValidationSchema,
],
- validateMiddleware,
+ this.validationResult,
asyncMiddleware(this.editCategory.bind(this)),
this.handlerServiceError,
);
router.post('/', [
...this.categoryValidationSchema,
],
- validateMiddleware,
+ this.validationResult,
asyncMiddleware(this.newCategory.bind(this)),
this.handlerServiceError,
);
router.delete('/', [
...this.categoriesBulkValidationSchema,
],
- validateMiddleware,
+ this.validationResult,
asyncMiddleware(this.bulkDeleteCategories.bind(this)),
this.handlerServiceError,
);
router.delete('/:id', [
...this.specificCategoryValidationSchema
],
- validateMiddleware,
+ this.validationResult,
asyncMiddleware(this.deleteItem.bind(this)),
this.handlerServiceError,
);
router.get('/:id', [
...this.specificCategoryValidationSchema,
],
- validateMiddleware,
+ this.validationResult,
asyncMiddleware(this.getCategory.bind(this)),
this.handlerServiceError,
);
router.get('/', [
...this.categoriesListValidationSchema
],
- validateMiddleware,
+ this.validationResult,
asyncMiddleware(this.getList.bind(this)),
this.handlerServiceError,
+ this.dynamicListService.handlerErrorsToResponse,
);
return router;
}
@@ -113,8 +117,9 @@ export default class ItemsCategoriesController extends BaseController {
*/
get categoriesListValidationSchema() {
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('stringified_filter_roles').optional().isJSON(),
];
}
@@ -190,13 +195,21 @@ export default class ItemsCategoriesController extends BaseController {
*/
async getList(req: Request, res: Response, next: NextFunction) {
const { tenantId, user } = req;
- const itemCategoriesFilter = this.matchedQueryData(req);
+ const itemCategoriesFilter = {
+ filterRoles: [],
+ sortOrder: 'asc',
+ columnSortBy: 'created_at',
+ ...this.matchedQueryData(req),
+ };
try {
- const itemCategories = await this.itemCategoriesService.getItemCategoriesList(
+ const { itemCategories, filterMeta } = await this.itemCategoriesService.getItemCategoriesList(
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) {
next(error);
}
diff --git a/server/src/api/controllers/Items.ts b/server/src/api/controllers/Items.ts
index 672c09bb5..a063f01ac 100644
--- a/server/src/api/controllers/Items.ts
+++ b/server/src/api/controllers/Items.ts
@@ -23,8 +23,7 @@ export default class ItemsController extends BaseController {
router() {
const router = Router();
- router.post(
- '/', [
+ router.post('/', [
...this.validateItemSchema,
],
this.validationResult,
@@ -150,10 +149,12 @@ export default class ItemsController extends BaseController {
*/
get validateListQuerySchema() {
return [
- query('column_sort_order').optional().trim().escape(),
+ query('column_sort_by').optional().trim().escape(),
query('sort_order').optional().isIn(['desc', 'asc']),
+
query('page').optional().isNumeric().toInt(),
query('page_size').optional().isNumeric().toInt(),
+
query('custom_view_id').optional().isNumeric().toInt(),
query('stringified_filter_roles').optional().isJSON(),
]
@@ -240,14 +241,22 @@ export default class ItemsController extends BaseController {
const filter = {
filterRoles: [],
sortOrder: 'asc',
+ columnSortBy: 'created_at',
+ page: 1,
+ pageSize: 12,
...this.matchedQueryData(req),
};
if (filter.stringifiedFilterRoles) {
filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles);
}
try {
- const items = await this.itemsService.itemsList(tenantId, filter);
- return res.status(200).send({ items });
+ const { items, pagination, filterMeta } = await this.itemsService.itemsList(tenantId, filter);
+
+ return res.status(200).send({
+ items,
+ pagination: this.transfromToResponse(pagination),
+ filter_meta: this.transfromToResponse(filterMeta),
+ });
} catch (error) {
next(error);
}
@@ -345,6 +354,16 @@ export default class ItemsController extends BaseController {
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 }],
+ })
+ }
}
}
}
\ No newline at end of file
diff --git a/server/src/api/controllers/ManualJournals.ts b/server/src/api/controllers/ManualJournals.ts
index 486d681f8..b4ad00dc8 100644
--- a/server/src/api/controllers/ManualJournals.ts
+++ b/server/src/api/controllers/ManualJournals.ts
@@ -299,17 +299,22 @@ export default class ManualJournalsController extends BaseController {
sortOrder: 'asc',
columnSortBy: 'created_at',
filterRoles: [],
+ page: 1,
+ pageSize: 12,
...this.matchedQueryData(req),
}
if (filter.stringifiedFilterRoles) {
filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles);
}
try {
- const manualJournals = await this.manualJournalsService.getManualJournals(tenantId, filter);
- return res.status(200).send({ manualJournals });
- } catch (error) {
- console.log(error);
+ const { manualJournals, pagination, filterMeta } = await this.manualJournalsService.getManualJournals(tenantId, filter);
+ return res.status(200).send({
+ manual_journals: manualJournals,
+ pagination: this.transfromToResponse(pagination),
+ filter_meta: this.transfromToResponse(filterMeta),
+ });
+ } catch (error) {
next(error);
}
}
diff --git a/server/src/api/controllers/Media.js b/server/src/api/controllers/Media.js
deleted file mode 100644
index 94631e804..000000000
--- a/server/src/api/controllers/Media.js
+++ /dev/null
@@ -1,163 +0,0 @@
-
-import express from 'express';
-import {
- param,
- query,
- validationResult,
-} from 'express-validator';
-import Container from 'typedi';
-import fs from 'fs';
-import { difference } from 'lodash';
-import asyncMiddleware from 'api/middleware/asyncMiddleware';
-
-const fsPromises = fs.promises;
-
-export default {
- /**
- * Router constructor.
- */
- router() {
- const router = express.Router();
-
- router.post('/upload',
- this.upload.validation,
- asyncMiddleware(this.upload.handler));
-
- router.delete('/',
- this.delete.validation,
- asyncMiddleware(this.delete.handler));
-
- router.get('/',
- this.get.validation,
- asyncMiddleware(this.get.handler));
-
- return router;
- },
-
- /**
- * Retrieve all or the given attachment ids.
- */
- get: {
- validation: [
- query('ids'),
- ],
- async handler(req, res) {
- const validationErrors = validationResult(req);
-
- if (!validationErrors.isEmpty()) {
- return res.boom.badData(null, {
- code: 'validation_error', ...validationErrors,
- });
- }
- const { Media } = req.models;
- const media = await Media.query().onBuild((builder) => {
-
- if (req.query.ids) {
- const ids = Array.isArray(req.query.ids) ? req.query.ids : [req.query.ids];
- builder.whereIn('id', ids);
- }
- });
-
- return res.status(200).send({ media });
- },
- },
-
- /**
- * Uploads the given attachment file.
- */
- upload: {
- validation: [
- // check('attachment').exists(),
- ],
- async handler(req, res) {
- const Logger = Container.get('logger');
-
- if (!req.files.attachment) {
- return res.status(400).send({
- errors: [{ type: 'ATTACHMENT.NOT.FOUND', code: 200 }],
- });
- }
- const publicPath = 'storage/app/public/';
- const attachmentsMimes = ['image/png', 'image/jpeg'];
- const { attachment } = req.files;
- const { Media } = req.models;
-
- const errorReasons = [];
-
- // Validate the attachment.
- if (attachment && attachmentsMimes.indexOf(attachment.mimetype) === -1) {
- errorReasons.push({ type: 'ATTACHMENT.MINETYPE.NOT.SUPPORTED', code: 160 });
- }
- // Catch all error reasons to response 400.
- if (errorReasons.length > 0) {
- return res.status(400).send({ errors: errorReasons });
- }
-
- try {
- await attachment.mv(`${publicPath}${req.organizationId}/${attachment.md5}.png`);
- Logger.info('[attachment] uploaded successfully');
- } catch (error) {
- Logger.info('[attachment] uploading failed.', { error });
- }
-
- const media = await Media.query().insert({
- attachment_file: `${attachment.md5}.png`,
- });
- return res.status(200).send({ media });
- },
- },
-
- /**
- * Deletes the given attachment ids from file system and database.
- */
- delete: {
- validation: [
- query('ids').exists().isArray(),
- query('ids.*').exists().isNumeric().toInt(),
- ],
- async handler(req, res) {
- const Logger = Container.get('logger');
- const validationErrors = validationResult(req);
-
- if (!validationErrors.isEmpty()) {
- return res.boom.badData(null, {
- code: 'validation_error', ...validationErrors,
- });
- }
- const { Media, MediaLink } = req.models;
- const ids = Array.isArray(req.query.ids) ? req.query.ids : [req.query.ids];
- const media = await Media.query().whereIn('id', ids);
- const mediaIds = media.map((m) => m.id);
- const notFoundMedia = difference(ids, mediaIds);
-
- if (notFoundMedia.length) {
- return res.status(400).send({
- errors: [{ type: 'MEDIA.IDS.NOT.FOUND', code: 200, ids: notFoundMedia }],
- });
- }
- const publicPath = 'storage/app/public/';
- const tenantPath = `${publicPath}${req.organizationId}`;
- const unlinkOpers = [];
-
- media.forEach((mediaModel) => {
- const oper = fsPromises.unlink(`${tenantPath}/${mediaModel.attachmentFile}`);
- unlinkOpers.push(oper);
- });
- await Promise.all(unlinkOpers).then((resolved) => {
- resolved.forEach(() => {
- Logger.info('[attachment] file has been deleted.');
- });
- })
- .catch((errors) => {
- errors.forEach((error) => {
- Logger.info('[attachment] Delete item attachment file delete failed.', { error });
- })
- });
-
- await MediaLink.query().whereIn('media_id', mediaIds).delete();
- await Media.query().whereIn('id', mediaIds).delete();
-
- return res.status(200).send();
- },
- },
-};
diff --git a/server/src/api/controllers/Media.ts b/server/src/api/controllers/Media.ts
new file mode 100644
index 000000000..469d6564b
--- /dev/null
+++ b/server/src/api/controllers/Media.ts
@@ -0,0 +1,212 @@
+
+import { Router, Request, Response, NextFunction } from 'express';
+import {
+ param,
+ query,
+ check,
+} from 'express-validator';
+import { camelCase, upperFirst } from 'lodash';
+import { Inject, Service } from 'typedi';
+import { IMediaLinkDTO } from 'interfaces';
+import fs from 'fs';
+import asyncMiddleware from 'api/middleware/asyncMiddleware';
+import BaseController from './BaseController';
+import MediaService from 'services/Media/MediaService';
+import { ServiceError } from 'exceptions';
+
+const fsPromises = fs.promises;
+
+@Service()
+export default class MediaController extends BaseController {
+ @Inject()
+ mediaService: MediaService;
+
+ /**
+ * Router constructor.
+ */
+ router() {
+ const router = Router();
+
+ router.post('/upload', [
+ ...this.uploadValidationSchema,
+ ],
+ this.validationResult,
+ asyncMiddleware(this.uploadMedia.bind(this)),
+ this.handlerServiceErrors,
+ );
+ router.post('/:id/link', [
+ ...this.mediaIdParamSchema,
+ ...this.linkValidationSchema,
+ ],
+ this.validationResult,
+ asyncMiddleware(this.linkMedia.bind(this)),
+ this.handlerServiceErrors,
+ );
+ router.delete('/', [
+ ...this.deleteValidationSchema,
+ ],
+ this.validationResult,
+ asyncMiddleware(this.deleteMedia.bind(this)),
+ this.handlerServiceErrors,
+ );
+ router.get('/:id', [
+ ...this.mediaIdParamSchema,
+ ],
+ this.validationResult,
+ asyncMiddleware(this.getMedia.bind(this)),
+ this.handlerServiceErrors,
+ );
+ return router;
+ }
+
+ get uploadValidationSchema() {
+ return [
+ // check('attachment'),
+ check('model_name').optional().trim().escape(),
+ check('model_id').optional().isNumeric().toInt(),
+ ];
+ }
+
+ get linkValidationSchema() {
+ return [
+ check('model_name').exists().trim().escape(),
+ check('model_id').exists().isNumeric().toInt(),
+ ]
+ }
+
+ get deleteValidationSchema() {
+ return [
+ query('ids').exists().isArray(),
+ query('ids.*').exists().isNumeric().toInt(),
+ ];
+ }
+
+ get mediaIdParamSchema() {
+ return [
+ param('id').exists().isNumeric().toInt(),
+ ];
+ }
+
+ /**
+ * Retrieve all or the given attachment ids.
+ * @param {Request} req -
+ * @param {Response} req -
+ * @param {NextFunction} req -
+ */
+ async getMedia(req: Request, res: Response, next: NextFunction) {
+ const { tenantId } = req;
+ const { id: mediaId } = req.params;
+
+ try {
+ const media = await this.mediaService.getMedia(tenantId, mediaId);
+ return res.status(200).send({ media });
+ } catch (error) {
+ next(error);
+ }
+ }
+
+ /**
+ * Uploads media.
+ * @param {Request} req -
+ * @param {Response} req -
+ * @param {NextFunction} req -
+ */
+ async uploadMedia(req: Request, res: Response, next: NextFunction) {
+ const { tenantId } = req;
+ const { attachment } = req.files
+
+ const linkMediaDTO: IMediaLinkDTO = this.matchedBodyData(req);
+ const modelName = linkMediaDTO.modelName
+ ? upperFirst(camelCase(linkMediaDTO.modelName)) : '';
+
+ try {
+ const media = await this.mediaService.upload(tenantId, attachment, modelName, linkMediaDTO.modelId);
+ return res.status(200).send({ media_id: media.id });
+ } catch (error) {
+ next(error);
+ }
+ }
+
+ /**
+ * Deletes the given attachment ids from file system and database.
+ * @param {Request} req -
+ * @param {Response} req -
+ * @param {NextFunction} req -
+ */
+ async deleteMedia(req: Request, res: Response, next: NextFunction) {
+ const { tenantId } = req;
+ const { ids: mediaIds } = req.query;
+
+ try {
+ await this.mediaService.deleteMedia(tenantId, mediaIds);
+ return res.status(200).send({
+ media_ids: mediaIds
+ });
+ } catch (error) {
+ next(error);
+ }
+ }
+
+ /**
+ * Links the given media to the specific resource model.
+ * @param {Request} req
+ * @param {Response} res
+ * @param {NextFunction} next
+ */
+ async linkMedia(req: Request, res: Response, next: NextFunction) {
+ const { tenantId } = req;
+ const { id: mediaId } = req.params;
+ const linkMediaDTO: IMediaLinkDTO = this.matchedBodyData(req);
+ const modelName = upperFirst(camelCase(linkMediaDTO.modelName));
+
+ try {
+ await this.mediaService.linkMedia(tenantId, mediaId, linkMediaDTO.modelId, modelName);
+ return res.status(200).send({ media_id: mediaId });
+ } catch (error) {
+ next(error);
+ }
+ }
+
+ /**
+ * Handler service errors.
+ * @param {Error} error
+ * @param {Request} req
+ * @param {Response} res
+ * @param {NextFunction} next
+ */
+ handlerServiceErrors(error, req: Request, res: Response, next: NextFunction) {
+ if (error instanceof ServiceError) {
+ if (error.errorType === 'MINETYPE_NOT_SUPPORTED') {
+ return res.boom.badRequest(null, {
+ errors: [{ type: 'MINETYPE_NOT_SUPPORTED', code: 100, }]
+ });
+ }
+ if (error.errorType === 'MEDIA_NOT_FOUND') {
+ return res.boom.badRequest(null, {
+ errors: [{ type: 'MEDIA_NOT_FOUND', code: 200 }]
+ });
+ }
+ if (error.errorType === 'MODEL_NAME_HAS_NO_MEDIA') {
+ return res.boom.badRequest(null, {
+ errors: [{ type: 'MODEL_NAME_HAS_NO_MEDIA', code: 300 }]
+ });
+ }
+ if (error.errorType === 'MODEL_ID_NOT_FOUND') {
+ return res.boom.badRequest(null, {
+ errors: [{ type: 'MODEL_ID_NOT_FOUND', code: 400 }]
+ });
+ }
+ if (error.errorType === 'MEDIA_IDS_NOT_FOUND') {
+ return res.boom.badRequest(null, {
+ errors: [{ type: 'MEDIA_IDS_NOT_FOUND', code: 500 }],
+ });
+ }
+ if (error.errorType === 'MEDIA_LINK_EXISTS') {
+ return res.boom.badRequest(null, {
+ errors: [{ type: 'MEDIA_LINK_EXISTS', code: 600 }],
+ });
+ }
+ }
+ next(error);
+ }
+};
diff --git a/server/src/api/controllers/Purchases/Bills.ts b/server/src/api/controllers/Purchases/Bills.ts
index 9f7618323..83168506e 100644
--- a/server/src/api/controllers/Purchases/Bills.ts
+++ b/server/src/api/controllers/Purchases/Bills.ts
@@ -3,7 +3,6 @@ import { check, param, query, matchedData } from 'express-validator';
import { Service, Inject } from 'typedi';
import { difference } from 'lodash';
import { BillOTD } from 'interfaces';
-import validateMiddleware from 'api/middleware/validateMiddleware';
import asyncMiddleware from 'api/middleware/asyncMiddleware';
import BillsService from 'services/Purchases/Bills';
import BaseController from 'api/controllers/BaseController';
@@ -30,7 +29,7 @@ export default class BillsController extends BaseController {
router.post(
'/',
[...this.billValidationSchema],
- validateMiddleware,
+ this.validationResult,
asyncMiddleware(this.validateVendorExistance.bind(this)),
asyncMiddleware(this.validateItemsIds.bind(this)),
asyncMiddleware(this.validateBillNumberExists.bind(this)),
@@ -40,7 +39,7 @@ export default class BillsController extends BaseController {
router.post(
'/:id',
[...this.billValidationSchema, ...this.specificBillValidationSchema],
- validateMiddleware,
+ this.validationResult,
asyncMiddleware(this.validateBillExistance.bind(this)),
asyncMiddleware(this.validateVendorExistance.bind(this)),
asyncMiddleware(this.validateItemsIds.bind(this)),
@@ -51,20 +50,20 @@ export default class BillsController extends BaseController {
router.get(
'/:id',
[...this.specificBillValidationSchema],
- validateMiddleware,
+ this.validationResult,
asyncMiddleware(this.validateBillExistance.bind(this)),
asyncMiddleware(this.getBill.bind(this))
);
router.get(
'/',
[...this.billsListingValidationSchema],
- validateMiddleware,
+ this.validationResult,
asyncMiddleware(this.listingBills.bind(this))
);
router.delete(
'/:id',
[...this.specificBillValidationSchema],
- validateMiddleware,
+ this.validationResult,
asyncMiddleware(this.validateBillExistance.bind(this)),
asyncMiddleware(this.deleteBill.bind(this))
);
diff --git a/server/src/api/controllers/Purchases/BillsPayments.ts b/server/src/api/controllers/Purchases/BillsPayments.ts
index bd80c6903..1b72a99d6 100644
--- a/server/src/api/controllers/Purchases/BillsPayments.ts
+++ b/server/src/api/controllers/Purchases/BillsPayments.ts
@@ -30,7 +30,7 @@ export default class BillsPayments extends BaseController {
router.post('/', [
...this.billPaymentSchemaValidation,
],
- validateMiddleware,
+ this.validationResult,
asyncMiddleware(this.validateBillPaymentVendorExistance.bind(this)),
asyncMiddleware(this.validatePaymentAccount.bind(this)),
asyncMiddleware(this.validatePaymentNumber.bind(this)),
@@ -42,7 +42,7 @@ export default class BillsPayments extends BaseController {
...this.billPaymentSchemaValidation,
...this.specificBillPaymentValidateSchema,
],
- validateMiddleware,
+ this.validationResult,
asyncMiddleware(this.validateBillPaymentVendorExistance.bind(this)),
asyncMiddleware(this.validatePaymentAccount.bind(this)),
asyncMiddleware(this.validatePaymentNumber.bind(this)),
@@ -53,19 +53,19 @@ export default class BillsPayments extends BaseController {
)
router.delete('/:id',
this.specificBillPaymentValidateSchema,
- validateMiddleware,
+ this.validationResult,
asyncMiddleware(this.validateBillPaymentExistance.bind(this)),
asyncMiddleware(this.deleteBillPayment.bind(this)),
);
router.get('/:id',
this.specificBillPaymentValidateSchema,
- validateMiddleware,
+ this.validationResult,
asyncMiddleware(this.validateBillPaymentExistance.bind(this)),
asyncMiddleware(this.getBillPayment.bind(this)),
);
router.get('/',
this.listingValidationSchema,
- validateMiddleware,
+ this.validationResult,
asyncMiddleware(this.getBillsPayments.bind(this))
);
return router;
@@ -381,6 +381,20 @@ export default class BillsPayments extends BaseController {
* @return {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);
+ }
}
}
\ No newline at end of file
diff --git a/server/src/api/controllers/Resources.js b/server/src/api/controllers/Resources.js
deleted file mode 100644
index bf1063a41..000000000
--- a/server/src/api/controllers/Resources.js
+++ /dev/null
@@ -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,
- });
- },
- },
-};
diff --git a/server/src/api/controllers/Resources.ts b/server/src/api/controllers/Resources.ts
new file mode 100644
index 000000000..91f0d8955
--- /dev/null
+++ b/server/src/api/controllers/Resources.ts
@@ -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);
+ }
+ }
+};
diff --git a/server/src/api/controllers/Sales/PaymentReceives.ts b/server/src/api/controllers/Sales/PaymentReceives.ts
index d2693a174..0d2d1fac7 100644
--- a/server/src/api/controllers/Sales/PaymentReceives.ts
+++ b/server/src/api/controllers/Sales/PaymentReceives.ts
@@ -4,7 +4,6 @@ import { difference } from 'lodash';
import { Inject, Service } from 'typedi';
import { IPaymentReceive, IPaymentReceiveOTD } from 'interfaces';
import BaseController from 'api/controllers/BaseController';
-import validateMiddleware from 'api/middleware/validateMiddleware';
import asyncMiddleware from 'api/middleware/asyncMiddleware';
import PaymentReceiveService from 'services/Sales/PaymentsReceives';
import SaleInvoiceService from 'services/Sales/SalesInvoices';
@@ -34,7 +33,7 @@ export default class PaymentReceivesController extends BaseController {
router.post(
'/:id',
this.editPaymentReceiveValidation,
- validateMiddleware,
+ this.validationResult,
asyncMiddleware(this.validatePaymentReceiveExistance.bind(this)),
asyncMiddleware(this.validatePaymentReceiveNoExistance.bind(this)),
asyncMiddleware(this.validateCustomerExistance.bind(this)),
@@ -47,7 +46,7 @@ export default class PaymentReceivesController extends BaseController {
router.post(
'/',
this.newPaymentReceiveValidation,
- validateMiddleware,
+ this.validationResult,
asyncMiddleware(this.validatePaymentReceiveNoExistance.bind(this)),
asyncMiddleware(this.validateCustomerExistance.bind(this)),
asyncMiddleware(this.validateDepositAccount.bind(this)),
@@ -58,20 +57,20 @@ export default class PaymentReceivesController extends BaseController {
router.get(
'/:id',
this.paymentReceiveValidation,
- validateMiddleware,
+ this.validationResult,
asyncMiddleware(this.validatePaymentReceiveExistance.bind(this)),
asyncMiddleware(this.getPaymentReceive.bind(this))
);
router.get(
'/',
this.validatePaymentReceiveList,
- validateMiddleware,
+ this.validationResult,
asyncMiddleware(this.getPaymentReceiveList.bind(this)),
);
router.delete(
'/:id',
this.paymentReceiveValidation,
- validateMiddleware,
+ this.validationResult,
asyncMiddleware(this.validatePaymentReceiveExistance.bind(this)),
asyncMiddleware(this.deletePaymentReceive.bind(this)),
);
diff --git a/server/src/api/controllers/Sales/SalesEstimates.ts b/server/src/api/controllers/Sales/SalesEstimates.ts
index 9b397071c..dd5f589ec 100644
--- a/server/src/api/controllers/Sales/SalesEstimates.ts
+++ b/server/src/api/controllers/Sales/SalesEstimates.ts
@@ -3,7 +3,6 @@ import { check, param, query, matchedData } from 'express-validator';
import { Inject, Service } from 'typedi';
import { ISaleEstimate, ISaleEstimateOTD } from 'interfaces';
import BaseController from 'api/controllers/BaseController'
-import validateMiddleware from 'api/middleware/validateMiddleware';
import asyncMiddleware from 'api/middleware/asyncMiddleware';
import SaleEstimateService from 'services/Sales/SalesEstimate';
import ItemsService from 'services/Items/ItemsService';
@@ -25,7 +24,7 @@ export default class SalesEstimatesController extends BaseController {
router.post(
'/',
this.estimateValidationSchema,
- validateMiddleware,
+ this.validationResult,
asyncMiddleware(this.validateEstimateCustomerExistance.bind(this)),
asyncMiddleware(this.validateEstimateNumberExistance.bind(this)),
asyncMiddleware(this.validateEstimateEntriesItemsExistance.bind(this)),
@@ -36,7 +35,7 @@ export default class SalesEstimatesController extends BaseController {
...this.validateSpecificEstimateSchema,
...this.estimateValidationSchema,
],
- validateMiddleware,
+ this.validationResult,
asyncMiddleware(this.validateEstimateIdExistance.bind(this)),
asyncMiddleware(this.validateEstimateCustomerExistance.bind(this)),
asyncMiddleware(this.validateEstimateNumberExistance.bind(this)),
@@ -48,21 +47,21 @@ export default class SalesEstimatesController extends BaseController {
'/:id', [
this.validateSpecificEstimateSchema,
],
- validateMiddleware,
+ this.validationResult,
asyncMiddleware(this.validateEstimateIdExistance.bind(this)),
asyncMiddleware(this.deleteEstimate.bind(this))
);
router.get(
'/:id',
this.validateSpecificEstimateSchema,
- validateMiddleware,
+ this.validationResult,
asyncMiddleware(this.validateEstimateIdExistance.bind(this)),
asyncMiddleware(this.getEstimate.bind(this))
);
router.get(
'/',
this.validateEstimateListSchema,
- validateMiddleware,
+ this.validationResult,
asyncMiddleware(this.getEstimates.bind(this))
);
return router;
@@ -298,7 +297,21 @@ export default class SalesEstimatesController extends BaseController {
* @param {Request} req
* @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);
+ }
}
};
diff --git a/server/src/api/controllers/Sales/SalesInvoices.ts b/server/src/api/controllers/Sales/SalesInvoices.ts
index 96c0ef719..db4ce0a16 100644
--- a/server/src/api/controllers/Sales/SalesInvoices.ts
+++ b/server/src/api/controllers/Sales/SalesInvoices.ts
@@ -1,22 +1,26 @@
-import { Router, Request, Response } from 'express';
+import { Router, Request, Response, NextFunction } from 'express';
import { check, param, query, matchedData } from 'express-validator';
import { difference } from 'lodash';
import { raw } from 'objection';
import { Service, Inject } from 'typedi';
-import validateMiddleware from 'api/middleware/validateMiddleware';
+import BaseController from '../BaseController';
import asyncMiddleware from 'api/middleware/asyncMiddleware';
import SaleInvoiceService from 'services/Sales/SalesInvoices';
import ItemsService from 'services/Items/ItemsService';
-import { ISaleInvoiceOTD } from 'interfaces';
+import DynamicListingService from 'services/DynamicListing/DynamicListService';
+import { ISaleInvoiceOTD, ISalesInvoicesFilter } from 'interfaces';
@Service()
-export default class SaleInvoicesController {
+export default class SaleInvoicesController extends BaseController{
@Inject()
itemsService: ItemsService;
@Inject()
saleInvoiceService: SaleInvoiceService;
+ @Inject()
+ dynamicListService: DynamicListingService;
+
/**
* Router constructor.
*/
@@ -26,7 +30,7 @@ export default class SaleInvoicesController {
router.post(
'/',
this.saleInvoiceValidationSchema,
- validateMiddleware,
+ this.validationResult,
asyncMiddleware(this.validateInvoiceCustomerExistance.bind(this)),
asyncMiddleware(this.validateInvoiceNumberUnique.bind(this)),
asyncMiddleware(this.validateInvoiceItemsIdsExistance.bind(this)),
@@ -39,7 +43,7 @@ export default class SaleInvoicesController {
...this.saleInvoiceValidationSchema,
...this.specificSaleInvoiceValidation,
],
- validateMiddleware,
+ this.validationResult,
asyncMiddleware(this.validateInvoiceExistance.bind(this)),
asyncMiddleware(this.validateInvoiceCustomerExistance.bind(this)),
asyncMiddleware(this.validateInvoiceNumberUnique.bind(this)),
@@ -52,7 +56,7 @@ export default class SaleInvoicesController {
router.delete(
'/:id',
this.specificSaleInvoiceValidation,
- validateMiddleware,
+ this.validationResult,
asyncMiddleware(this.validateInvoiceExistance.bind(this)),
asyncMiddleware(this.deleteSaleInvoice.bind(this))
);
@@ -64,13 +68,14 @@ export default class SaleInvoicesController {
router.get(
'/:id',
this.specificSaleInvoiceValidation,
- validateMiddleware,
+ this.validationResult,
asyncMiddleware(this.validateInvoiceExistance.bind(this)),
asyncMiddleware(this.getSaleInvoice.bind(this))
);
router.get(
'/',
this.saleInvoiceListValidationSchema,
+ this.validationResult,
asyncMiddleware(this.getSalesInvoices.bind(this))
)
return router;
@@ -411,7 +416,21 @@ export default class SaleInvoicesController {
* @param {Response} res
* @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);
+ }
}
}
diff --git a/server/src/api/controllers/Sales/SalesReceipts.ts b/server/src/api/controllers/Sales/SalesReceipts.ts
index 7d5e7c8e8..53823def1 100644
--- a/server/src/api/controllers/Sales/SalesReceipts.ts
+++ b/server/src/api/controllers/Sales/SalesReceipts.ts
@@ -1,14 +1,14 @@
import { Router, Request, Response } from 'express';
import { check, param, query, matchedData } from 'express-validator';
import { Inject, Service } from 'typedi';
-import validateMiddleware from 'api/middleware/validateMiddleware';
import asyncMiddleware from 'api/middleware/asyncMiddleware';
import AccountsService from 'services/Accounts/AccountsService';
import ItemsService from 'services/Items/ItemsService';
import SaleReceiptService from 'services/Sales/SalesReceipts';
+import BaseController from '../BaseController';
@Service()
-export default class SalesReceiptsController {
+export default class SalesReceiptsController extends BaseController{
@Inject()
saleReceiptService: SaleReceiptService;
@@ -29,7 +29,7 @@ export default class SalesReceiptsController {
...this.specificReceiptValidationSchema,
...this.salesReceiptsValidationSchema,
],
- validateMiddleware,
+ this.validationResult,
asyncMiddleware(this.validateSaleReceiptExistance.bind(this)),
asyncMiddleware(this.validateReceiptCustomerExistance.bind(this)),
asyncMiddleware(this.validateReceiptDepositAccountExistance.bind(this)),
@@ -40,7 +40,7 @@ export default class SalesReceiptsController {
router.post(
'/',
this.salesReceiptsValidationSchema,
- validateMiddleware,
+ this.validationResult,
asyncMiddleware(this.validateReceiptCustomerExistance.bind(this)),
asyncMiddleware(this.validateReceiptDepositAccountExistance.bind(this)),
asyncMiddleware(this.validateReceiptItemsIdsExistance.bind(this)),
@@ -49,14 +49,14 @@ export default class SalesReceiptsController {
router.delete(
'/:id',
this.specificReceiptValidationSchema,
- validateMiddleware,
+ this.validationResult,
asyncMiddleware(this.validateSaleReceiptExistance.bind(this)),
asyncMiddleware(this.deleteSaleReceipt.bind(this))
);
router.get(
'/',
this.listSalesReceiptsValidationSchema,
- validateMiddleware,
+ this.validationResult,
asyncMiddleware(this.listingSalesReceipts.bind(this))
);
return router;
@@ -274,7 +274,6 @@ export default class SalesReceiptsController {
const { id: saleReceiptId } = req.params;
const saleReceipt = { ...req.body };
-
const errorReasons = [];
// Handle all errors with reasons messages.
@@ -296,7 +295,19 @@ export default class SalesReceiptsController {
* @param {Request} req
* @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);
+ }
}
};
diff --git a/server/src/api/controllers/Settings.ts b/server/src/api/controllers/Settings.ts
index eddd3eae9..23b5b1136 100644
--- a/server/src/api/controllers/Settings.ts
+++ b/server/src/api/controllers/Settings.ts
@@ -1,3 +1,4 @@
+import { Service } from 'typedi';
import { Router, Request, Response } from 'express';
import { body, query } from 'express-validator';
import { pick } from 'lodash';
@@ -9,6 +10,7 @@ import {
isDefinedOptionConfigurable,
} from 'utils';
+@Service()
export default class SettingsController extends BaseController{
/**
* Router constructor.
diff --git a/server/src/api/controllers/Subscription/Licenses.ts b/server/src/api/controllers/Subscription/Licenses.ts
index 781c8710f..5639d9b7c 100644
--- a/server/src/api/controllers/Subscription/Licenses.ts
+++ b/server/src/api/controllers/Subscription/Licenses.ts
@@ -6,7 +6,6 @@ import config from 'config';
import { License, Plan } from 'system/models';
import BaseController from 'api/controllers/BaseController';
import LicenseService from 'services/Payment/License';
-import validateMiddleware from 'api/middleware/validateMiddleware';
import asyncMiddleware from 'api/middleware/asyncMiddleware';
import { ILicensesFilter } from 'interfaces';
@@ -31,13 +30,13 @@ export default class LicensesController extends BaseController {
router.post(
'/generate',
this.generateLicenseSchema,
- validateMiddleware,
+ this.validationResult,
asyncMiddleware(this.validatePlanExistance.bind(this)),
asyncMiddleware(this.generateLicense.bind(this)),
);
router.post(
'/disable/:licenseId',
- validateMiddleware,
+ this.validationResult,
asyncMiddleware(this.validateLicenseExistance.bind(this)),
asyncMiddleware(this.validateNotDisabledLicense.bind(this)),
asyncMiddleware(this.disableLicense.bind(this)),
@@ -45,7 +44,7 @@ export default class LicensesController extends BaseController {
router.post(
'/send',
this.sendLicenseSchemaValidation,
- validateMiddleware,
+ this.validationResult,
asyncMiddleware(this.sendLicense.bind(this)),
);
router.delete(
diff --git a/server/src/api/controllers/Subscription/PaymentViaLicense.ts b/server/src/api/controllers/Subscription/PaymentViaLicense.ts
index 56ceb1345..45b219c53 100644
--- a/server/src/api/controllers/Subscription/PaymentViaLicense.ts
+++ b/server/src/api/controllers/Subscription/PaymentViaLicense.ts
@@ -1,7 +1,6 @@
import { Inject, Service } from 'typedi';
import { Router, Request, Response } from 'express';
import { check } from 'express-validator';
-import validateMiddleware from 'api/middleware/validateMiddleware';
import asyncMiddleware from 'api/middleware/asyncMiddleware';
import PaymentMethodController from 'api/controllers/Subscription/PaymentMethod';
import {
@@ -26,7 +25,7 @@ export default class PaymentViaLicenseController extends PaymentMethodController
router.post(
'/payment',
this.paymentViaLicenseSchema,
- validateMiddleware,
+ this.validationResult,
asyncMiddleware(this.validatePlanSlugExistance.bind(this)),
asyncMiddleware(this.paymentViaLicense.bind(this)),
);
diff --git a/server/src/api/controllers/Users.ts b/server/src/api/controllers/Users.ts
index 0dc47c8ae..1c2bae9b8 100644
--- a/server/src/api/controllers/Users.ts
+++ b/server/src/api/controllers/Users.ts
@@ -222,19 +222,22 @@ export default class UsersController extends BaseController{
}
if (error instanceof ServiceError) {
if (error.errorType === 'user_not_found') {
- return res.status(404).send({
- errors: [{ type: 'USER.NOT.FOUND', code: 100 }],
- });
+ return res.boom.badRequest(
+ 'User not found.',
+ { errors: [{ type: 'USER.NOT.FOUND', code: 100 }] }
+ );
}
if (error.errorType === 'user_already_active') {
- return res.status(404).send({
- errors: [{ type: 'USER.ALREADY.ACTIVE', code: 200 }],
- });
+ return res.boom.badRequest(
+ 'User is already active.',
+ { errors: [{ type: 'USER.ALREADY.ACTIVE', code: 200 }] },
+ );
}
if (error.errorType === 'user_already_inactive') {
- return res.status(404).send({
- errors: [{ type: 'USER.ALREADY.INACTIVE', code: 200 }],
- });
+ return res.boom.badRequest(
+ 'User is already inactive.',
+ { errors: [{ type: 'USER.ALREADY.INACTIVE', code: 200 }] },
+ );
}
if (error.errorType === 'user_same_the_authorized_user') {
return res.boom.badRequest(
diff --git a/server/src/api/controllers/Views.js b/server/src/api/controllers/Views.js
deleted file mode 100644
index 68063e77f..000000000
--- a/server/src/api/controllers/Views.js
+++ /dev/null
@@ -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,
- });
- }
- },
-};
diff --git a/server/src/api/controllers/Views.ts b/server/src/api/controllers/Views.ts
index e82a8ae64..238d01b6b 100644
--- a/server/src/api/controllers/Views.ts
+++ b/server/src/api/controllers/Views.ts
@@ -1,19 +1,10 @@
import { Inject, Service } from 'typedi';
import { Router, Request, NextFunction, Response } from 'express';
-import {
- check,
- query,
- param,
- oneOf,
- validationResult,
-} from 'express-validator';
+import { check, param } from 'express-validator';
import asyncMiddleware from 'api/middleware/asyncMiddleware';
-import {
- validateViewRoles,
-} from 'lib/ViewRolesBuilder';
import ViewsService from 'services/Views/ViewsService';
-import BaseController from './BaseController';
-import { IViewDTO } from 'interfaces';
+import BaseController from 'api/controllers/BaseController';
+import { IViewDTO, IViewEditDTO } from 'interfaces';
import { ServiceError } from 'exceptions';
@Service()
@@ -27,66 +18,96 @@ export default class ViewsController extends BaseController{
router() {
const router = Router();
- router.get('/', [
- ...this.viewDTOSchemaValidation,
+ router.get('/resource/:resource_model', [
+ ...this.viewsListSchemaValidation,
],
- asyncMiddleware(this.listViews)
+ this.validationResult,
+ asyncMiddleware(this.listResourceViews.bind(this)),
+ this.handlerServiceErrors,
);
router.post('/', [
...this.viewDTOSchemaValidation,
],
- asyncMiddleware(this.createView)
+ this.validationResult,
+ asyncMiddleware(this.createView.bind(this)),
+ this.handlerServiceErrors
);
-
- router.post('/:view_id', [
- ...this.viewDTOSchemaValidation,
+ router.post('/:id', [
+ ...this.viewParamSchemaValidation,
+ ...this.viewEditDTOSchemaValidation,
],
- asyncMiddleware(this.editView)
+ this.validationResult,
+ asyncMiddleware(this.editView.bind(this)),
+ this.handlerServiceErrors,
);
-
- router.delete('/:view_id', [
+ router.delete('/:id', [
...this.viewParamSchemaValidation
],
- asyncMiddleware(this.deleteView));
-
- router.get('/:view_id', [
- ...this.viewParamSchemaValidation
- ]
- asyncMiddleware(this.getView)
+ this.validationResult,
+ asyncMiddleware(this.deleteView.bind(this)),
+ this.handlerServiceErrors,
);
-
- router.get('/:view_id/resource', [
+ router.get('/:id', [
...this.viewParamSchemaValidation
],
- asyncMiddleware(this.getViewResource)
+ this.validationResult,
+ asyncMiddleware(this.getView.bind(this)),
);
-
return router;
}
+ /**
+ * New view DTO schema validation.
+ */
get viewDTOSchemaValidation() {
return [
- check('resource_name').exists().escape().trim(),
+ check('resource_model').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.*.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(),
];
}
get viewParamSchemaValidation() {
return [
-
- ]
+ param('id').exists().isNumeric().toInt(),
+ ];
}
-
+ get viewsListSchemaValidation() {
+ return [
+ param('resource_model').exists().trim().escape(),
+ ]
+ }
/**
* List all views that associated with the given resource.
@@ -94,12 +115,12 @@ export default class ViewsController extends BaseController{
* @param {Response} res -
* @param {NextFunction} next -
*/
- listViews(req: Request, res: Response, next: NextFunction) {
+ async listResourceViews(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
- const filter = req.query;
+ const { resource_model: resourceModel } = req.params;
try {
- const views = this.viewsService.listViews(tenantId, filter);
+ const views = await this.viewsService.listResourceViews(tenantId, resourceModel);
return res.status(200).send({ views });
} catch (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 {Response} res
* @param {NextFunction} next
*/
- getView(req: Request, res: Response, next: NextFunction) {
+ async getView(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
+ const { id: viewId } = req.params;
try {
- const view = this.viewsService.getView(tenantId, viewId);
+ const view = await this.viewsService.getView(tenantId, viewId);
return res.status(200).send({ view });
} catch (error) {
next(error);
@@ -129,13 +151,13 @@ export default class ViewsController extends BaseController{
* @param {Response} res -
* @param {NextFunction} next -
*/
- createView(req: Request, res: Response, next: NextFunction) {
+ async createView(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const viewDTO: IViewDTO = this.matchedBodyData(req);
try {
- await this.viewsService.newView(tenantId, viewDTO);
- return res.status(200).send({ id: 1 });
+ const view = await this.viewsService.newView(tenantId, viewDTO);
+ return res.status(200).send({ id: view.id });
} catch (error) {
next(error);
}
@@ -147,10 +169,10 @@ export default class ViewsController extends BaseController{
* @param {Response} res -
* @param {NextFunction} next -
*/
- editView(req: Request, res: Response, next: NextFunction) {
+ async editView(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const { id: viewId } = req.params;
- const { body: viewEditDTO } = req;
+ const viewEditDTO: IViewEditDTO = this.matchedBodyData(req);
try {
await this.viewsService.editView(tenantId, viewId, viewEditDTO);
@@ -166,7 +188,7 @@ export default class ViewsController extends BaseController{
* @param {Response} res -
* @param {NextFunction} next -
*/
- deleteView(req: Request, res: Response, next: NextFunction) {
+ async deleteView(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const { id: viewId } = req.params;
@@ -187,6 +209,16 @@ export default class ViewsController extends BaseController{
*/
handlerServiceErrors(error: Error, req: Request, res: Response, next: NextFunction) {
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') {
return res.boom.badRequest(null, {
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 }],
});
}
+ 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);
}
};
diff --git a/server/src/api/index.ts b/server/src/api/index.ts
index 3cfa75f3e..12dc5138b 100644
--- a/server/src/api/index.ts
+++ b/server/src/api/index.ts
@@ -87,18 +87,18 @@ export default () => {
dashboard.use('/accounts', Container.get(Accounts).router());
dashboard.use('/account_types', Container.get(AccountTypes).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('/item_categories', Container.get(ItemCategories).router());
dashboard.use('/expenses', Container.get(Expenses).router());
dashboard.use('/financial_statements', FinancialStatements.router());
- dashboard.use('/sales', Container.get(Sales).router());
dashboard.use('/customers', Container.get(Customers).router());
dashboard.use('/vendors', Container.get(Vendors).router());
- dashboard.use('/purchases', Container.get(Purchases).router());
- dashboard.use('/resources', Resources.router());
+ // dashboard.use('/sales', Container.get(Sales).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('/media', Media.router());
+ dashboard.use('/media', Container.get(Media).router());
app.use('/', dashboard);
diff --git a/server/src/api/middleware/TenantDependencyInjection.ts b/server/src/api/middleware/TenantDependencyInjection.ts
index fed2b4219..c8c447bca 100644
--- a/server/src/api/middleware/TenantDependencyInjection.ts
+++ b/server/src/api/middleware/TenantDependencyInjection.ts
@@ -17,6 +17,8 @@ export default (req: Request, tenant: ITenant) => {
const repositories = tenantServices.repositories(tenantId)
const cacheInstance = tenantServices.cache(tenantId);
+ tenantServices.setI18nLocals(tenantId, { __: req.__ });
+
req.knex = knexInstance;
req.organizationId = organizationId;
req.tenant = tenant;
diff --git a/server/src/api/middleware/validateMiddleware.js b/server/src/api/middleware/validateMiddleware.js
deleted file mode 100644
index 266680e9a..000000000
--- a/server/src/api/middleware/validateMiddleware.js
+++ /dev/null
@@ -1,13 +0,0 @@
-import { validationResult } from 'express-validator';
-
-export default (req, res, next) => {
- const validationErrors = validationResult(req);
-
- if (!validationErrors.isEmpty()) {
- return res.boom.badData(null, {
- code: 'validation_error',
- ...validationErrors,
- });
- }
- next();
-}
\ No newline at end of file
diff --git a/server/src/config/index.js b/server/src/config/index.js
index 1258c04ce..7ce065268 100644
--- a/server/src/config/index.js
+++ b/server/src/config/index.js
@@ -24,6 +24,7 @@ export default {
db_user: process.env.SYSTEM_DB_USER,
db_password: process.env.SYSTEM_DB_PASSWORD,
db_name: process.env.SYSTEM_DB_NAME,
+ charset: process.env.SYSTEM_DB_CHARSET,
migrations_dir: process.env.SYSTEM_MIGRATIONS_DIR,
seeds_dir: process.env.SYSTEM_SEEDS_DIR,
},
diff --git a/server/src/data/ResourceFieldsKeys.js b/server/src/data/ResourceFieldsKeys.js
index 462142dfd..8504d3fdf 100644
--- a/server/src/data/ResourceFieldsKeys.js
+++ b/server/src/data/ResourceFieldsKeys.js
@@ -2,7 +2,7 @@
export default {
// Expenses.
- 'expenses': {
+ expense: {
payment_date: {
column: 'payment_date',
},
@@ -10,9 +10,12 @@ export default {
column: 'payment_account_id',
relation: 'accounts.id',
},
- total_amount: {
+ amount: {
column: 'total_amount',
},
+ currency_code: {
+ column: 'currency_code',
+ },
reference_no: {
column: 'reference_no'
},
@@ -30,7 +33,7 @@ export default {
},
// Accounts
- 'accounts': {
+ Account: {
name: {
column: 'name',
},
@@ -72,23 +75,106 @@ export default {
},
// Items
- 'items': {
- 'type': {
+ item: {
+ type: {
column: 'type',
},
- 'name': {
+ 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_journal: {
date: {
column: 'date',
},
- created_at: {
- column: 'created_at',
- },
journal_number: {
column: 'journal_number',
},
@@ -112,5 +198,8 @@ export default {
journal_type: {
column: 'journal_type',
},
+ created_at: {
+ column: 'created_at',
+ },
}
};
diff --git a/server/src/database/migrations/20190822214242_create_users_table.js b/server/src/database/migrations/20190822214242_create_users_table.js
deleted file mode 100644
index fb9ee35f6..000000000
--- a/server/src/database/migrations/20190822214242_create_users_table.js
+++ /dev/null
@@ -1,21 +0,0 @@
-
-exports.up = function (knex) {
- return knex.schema.createTable('users', (table) => {
- table.increments();
- table.string('first_name');
- table.string('last_name');
- table.string('email').unique();
- table.string('phone_number').unique();
- table.boolean('active');
- table.integer('role_id').unique();
- table.string('language');
- table.date('last_login_at');
-
- table.date('invite_accepted_at');
- table.timestamps();
- }).raw('ALTER TABLE `USERS` AUTO_INCREMENT = 1000');;
-};
-
-exports.down = function (knex) {
- return knex.schema.dropTableIfExists('users');
-};
diff --git a/server/src/database/migrations/20190822214904_create_account_types_table.js b/server/src/database/migrations/20190822214302_create_account_types_table.js
similarity index 76%
rename from server/src/database/migrations/20190822214904_create_account_types_table.js
rename to server/src/database/migrations/20190822214302_create_account_types_table.js
index c580efe83..4515e3338 100644
--- a/server/src/database/migrations/20190822214904_create_account_types_table.js
+++ b/server/src/database/migrations/20190822214302_create_account_types_table.js
@@ -2,10 +2,9 @@
exports.up = (knex) => {
return knex.schema.createTable('account_types', (table) => {
table.increments();
- table.string('name');
- table.string('key');
- table.string('normal');
- table.string('root_type');
+ table.string('key').index();
+ table.string('normal').index();
+ table.string('root_type').index();
table.string('child_type');
table.boolean('balance_sheet');
table.boolean('income_sheet');
diff --git a/server/src/database/migrations/20190822214303_create_accounts_table.js b/server/src/database/migrations/20190822214303_create_accounts_table.js
new file mode 100644
index 000000000..d2dd03580
--- /dev/null
+++ b/server/src/database/migrations/20190822214303_create_accounts_table.js
@@ -0,0 +1,20 @@
+
+exports.up = function (knex) {
+ return knex.schema.createTable('accounts', (table) => {
+ table.increments('id').comment('Auto-generated id');;
+ table.string('name').index();
+ table.string('slug');
+ table.integer('account_type_id').unsigned().references('id').inTable('account_types');
+ table.integer('parent_account_id').unsigned().references('id').inTable('accounts');
+ table.string('code', 10).index();
+ table.text('description');
+ table.boolean('active').defaultTo(true).index();
+ table.integer('index').unsigned();
+ table.boolean('predefined').defaultTo(false).index();
+ table.decimal('amount', 15, 5);
+ table.string('currency_code', 3).index();
+ table.timestamps();
+ }).raw('ALTER TABLE `ACCOUNTS` AUTO_INCREMENT = 1000');
+};
+
+exports.down = (knex) => knex.schema.dropTableIfExists('accounts');
diff --git a/server/src/database/migrations/20190822214303_create_items_table.js b/server/src/database/migrations/20190822214303_create_items_table.js
deleted file mode 100644
index 09516bfc1..000000000
--- a/server/src/database/migrations/20190822214303_create_items_table.js
+++ /dev/null
@@ -1,27 +0,0 @@
-
-exports.up = function (knex) {
- return knex.schema.createTable('items', (table) => {
- table.increments();
- table.string('name');
- table.string('type');
- table.string('sku');
- table.boolean('sellable');
- table.boolean('purchasable');
- table.decimal('sell_price', 13, 3).unsigned();
- table.decimal('cost_price', 13, 3).unsigned();
- table.string('currency_code', 3);
- table.string('picture_uri');
- table.integer('cost_account_id').unsigned();
- table.integer('sell_account_id').unsigned();
- table.integer('inventory_account_id').unsigned();
- table.text('sell_description').nullable();
- table.text('purchase_description').nullable();
- table.integer('quantity_on_hand');
- table.text('note').nullable();
- table.integer('category_id').unsigned();
- table.integer('user_id').unsigned();
- table.timestamps();
- }).raw('ALTER TABLE `ITEMS` AUTO_INCREMENT = 1000');;
-};
-
-exports.down = (knex) => knex.schema.dropTableIfExists('items');
diff --git a/server/src/database/migrations/20190822214304_create_accounts_table.js b/server/src/database/migrations/20190822214304_create_accounts_table.js
deleted file mode 100644
index 038116275..000000000
--- a/server/src/database/migrations/20190822214304_create_accounts_table.js
+++ /dev/null
@@ -1,20 +0,0 @@
-
-exports.up = function (knex) {
- return knex.schema.createTable('accounts', (table) => {
- table.bigIncrements('id').comment('Auto-generated id');;
- table.string('name');
- table.string('slug');
- table.integer('account_type_id').unsigned();
- table.integer('parent_account_id').unsigned();
- table.string('code', 10);
- table.text('description');
- table.boolean('active').defaultTo(true);
- table.integer('index').unsigned();
- table.boolean('predefined').defaultTo(false);
- table.decimal('amount', 15, 5);
- table.string('currency_code', 3);
- table.timestamps();
- }).raw('ALTER TABLE `ACCOUNTS` AUTO_INCREMENT = 1000');
-};
-
-exports.down = (knex) => knex.schema.dropTableIfExists('accounts');
diff --git a/server/src/database/migrations/20190822214304_create_items_categories_table.js b/server/src/database/migrations/20190822214304_create_items_categories_table.js
new file mode 100644
index 000000000..c807352f5
--- /dev/null
+++ b/server/src/database/migrations/20190822214304_create_items_categories_table.js
@@ -0,0 +1,19 @@
+
+exports.up = function (knex) {
+ return knex.schema.createTable('items_categories', (table) => {
+ table.increments();
+ table.string('name').index();
+ table.integer('parent_category_id').unsigned().references('id').inTable('items_categories');
+ table.text('description');
+ table.integer('user_id').unsigned().index();
+
+ table.integer('cost_account_id').unsigned().references('id').inTable('accounts');
+ table.integer('sell_account_id').unsigned().references('id').inTable('accounts');
+ table.integer('inventory_account_id').unsigned().references('id').inTable('accounts');
+
+ table.string('cost_method');
+ table.timestamps();
+ });
+};
+
+exports.down = (knex) => knex.schema.dropTableIfExists('items_categories');
diff --git a/server/src/database/migrations/20190822214306_create_items_categories_table.js b/server/src/database/migrations/20190822214306_create_items_categories_table.js
deleted file mode 100644
index a8b189137..000000000
--- a/server/src/database/migrations/20190822214306_create_items_categories_table.js
+++ /dev/null
@@ -1,19 +0,0 @@
-
-exports.up = function (knex) {
- return knex.schema.createTable('items_categories', (table) => {
- table.increments();
- table.string('name');
- table.integer('parent_category_id').unsigned();
- table.text('description');
- table.integer('user_id').unsigned();
-
- table.integer('cost_account_id').unsigned();
- table.integer('sell_account_id').unsigned();
- table.integer('inventory_account_id').unsigned();
-
- table.string('cost_method');
- table.timestamps();
- });
-};
-
-exports.down = (knex) => knex.schema.dropTableIfExists('items_categories');
diff --git a/server/src/database/migrations/20190822214306_create_items_table.js b/server/src/database/migrations/20190822214306_create_items_table.js
new file mode 100644
index 000000000..ff79afd56
--- /dev/null
+++ b/server/src/database/migrations/20190822214306_create_items_table.js
@@ -0,0 +1,27 @@
+
+exports.up = function (knex) {
+ return knex.schema.createTable('items', (table) => {
+ table.increments();
+ table.string('name').index();
+ table.string('type').index();
+ table.string('sku');
+ table.boolean('sellable').index();
+ table.boolean('purchasable').index();
+ table.decimal('sell_price', 13, 3).unsigned();
+ table.decimal('cost_price', 13, 3).unsigned();
+ table.string('currency_code', 3);
+ table.string('picture_uri');
+ table.integer('cost_account_id').nullable().unsigned().references('id').inTable('accounts');
+ table.integer('sell_account_id').nullable().unsigned().references('id').inTable('accounts');
+ table.integer('inventory_account_id').unsigned().references('id').inTable('accounts');
+ table.text('sell_description').nullable();
+ table.text('purchase_description').nullable();
+ table.integer('quantity_on_hand');
+ table.text('note').nullable();
+ table.integer('category_id').unsigned().index().references('id').inTable('items_categories');
+ table.integer('user_id').unsigned().index();
+ table.timestamps();
+ }).raw('ALTER TABLE `ITEMS` AUTO_INCREMENT = 1000');
+};
+
+exports.down = (knex) => knex.schema.dropTableIfExists('items');
diff --git a/server/src/database/migrations/20190822214905_create_views_table.js b/server/src/database/migrations/20190822214903_create_views_table.js
similarity index 82%
rename from server/src/database/migrations/20190822214905_create_views_table.js
rename to server/src/database/migrations/20190822214903_create_views_table.js
index e238d7ccb..53099e854 100644
--- a/server/src/database/migrations/20190822214905_create_views_table.js
+++ b/server/src/database/migrations/20190822214903_create_views_table.js
@@ -2,9 +2,9 @@
exports.up = function (knex) {
return knex.schema.createTable('views', (table) => {
table.increments();
- table.string('name');
+ table.string('name').index();
table.boolean('predefined');
- table.string('resource_model');
+ table.string('resource_model').index();
table.boolean('favourite');
table.string('roles_logic_expression');
table.timestamps();
diff --git a/server/src/database/migrations/20190822214302_create_settings_table.js b/server/src/database/migrations/20190822214904_create_settings_table.js
similarity index 71%
rename from server/src/database/migrations/20190822214302_create_settings_table.js
rename to server/src/database/migrations/20190822214904_create_settings_table.js
index b6aba4bef..65f3f4fdc 100644
--- a/server/src/database/migrations/20190822214302_create_settings_table.js
+++ b/server/src/database/migrations/20190822214904_create_settings_table.js
@@ -2,10 +2,10 @@
exports.up = function (knex) {
return knex.schema.createTable('settings', (table) => {
table.increments();
- table.integer('user_id').unsigned();
- table.string('group');
+ table.integer('user_id').unsigned().index();
+ table.string('group').index();
table.string('type');
- table.string('key');
+ table.string('key').index();
table.string('value');
}).raw('ALTER TABLE `SETTINGS` AUTO_INCREMENT = 2000');
};
diff --git a/server/src/database/migrations/20190822214905_create_views_columns.js b/server/src/database/migrations/20190822214905_create_views_columns.js
index 5b902fbf3..4fc76e399 100644
--- a/server/src/database/migrations/20190822214905_create_views_columns.js
+++ b/server/src/database/migrations/20190822214905_create_views_columns.js
@@ -2,7 +2,7 @@
exports.up = function (knex) {
return knex.schema.createTable('view_has_columns', (table) => {
table.increments();
- table.integer('view_id').unsigned();
+ table.integer('view_id').unsigned().index().references('id').inTable('views');
table.string('field_key');
table.integer('index').unsigned();
}).raw('ALTER TABLE `ITEMS_CATEGORIES` AUTO_INCREMENT = 1000');
diff --git a/server/src/database/migrations/20190822214905_create_views_roles_table.js b/server/src/database/migrations/20190822214905_create_views_roles_table.js
index f96a15cbe..e9add11eb 100644
--- a/server/src/database/migrations/20190822214905_create_views_roles_table.js
+++ b/server/src/database/migrations/20190822214905_create_views_roles_table.js
@@ -3,10 +3,10 @@ exports.up = function (knex) {
return knex.schema.createTable('view_roles', (table) => {
table.increments();
table.integer('index');
- table.string('field_key');
+ table.string('field_key').index();
table.string('comparator');
table.string('value');
- table.integer('view_id').unsigned();
+ table.integer('view_id').unsigned().index().references('id').inTable('views');
}).raw('ALTER TABLE `VIEW_ROLES` AUTO_INCREMENT = 1000');
};
diff --git a/server/src/database/migrations/20200104232644_create_contacts_table.js b/server/src/database/migrations/20200104232644_create_contacts_table.js
new file mode 100644
index 000000000..ccc7e13f0
--- /dev/null
+++ b/server/src/database/migrations/20200104232644_create_contacts_table.js
@@ -0,0 +1,49 @@
+
+exports.up = function(knex) {
+ return knex.schema.createTable('contacts', table => {
+ table.increments();
+
+ table.string('contact_service');
+ table.string('contact_type');
+
+ table.decimal('balance', 13, 3).defaultTo(0);
+ table.decimal('opening_balance', 13, 3).defaultTo(0);
+
+ table.string('first_name').nullable();
+ table.string('last_name').nullable();
+ table.string('company_name').nullable();
+
+ table.string('display_name');
+
+ table.string('email').nullable();
+ table.string('work_phone').nullable();
+ table.string('personal_phone').nullable();
+
+ table.string('billing_address_1').nullable();
+ table.string('billing_address_2').nullable();
+ table.string('billing_address_city').nullable();
+ table.string('billing_address_country').nullable();
+ table.string('billing_address_email').nullable();
+ table.string('billing_address_zipcode').nullable();
+ table.string('billing_address_phone').nullable();
+ table.string('billing_address_state').nullable(),
+
+ table.string('shipping_address_1').nullable();
+ table.string('shipping_address_2').nullable();
+ table.string('shipping_address_city').nullable();
+ table.string('shipping_address_country').nullable();
+ table.string('shipping_address_email').nullable();
+ table.string('shipping_address_zipcode').nullable();
+ table.string('shipping_address_phone').nullable();
+ table.string('shipping_address_state').nullable();
+
+ table.text('note');
+ table.boolean('active').defaultTo(true);
+
+ table.timestamps();
+ });
+};
+
+exports.down = function(knex) {
+ return knex.schema.dropTableIfExists('contacts');
+};
diff --git a/server/src/database/migrations/20200104232647_create_accounts_transactions_table.js b/server/src/database/migrations/20200104232647_create_accounts_transactions_table.js
index bc02d6b4a..42adc70fb 100644
--- a/server/src/database/migrations/20200104232647_create_accounts_transactions_table.js
+++ b/server/src/database/migrations/20200104232647_create_accounts_transactions_table.js
@@ -4,17 +4,17 @@ exports.up = function(knex) {
table.increments();
table.decimal('credit', 13, 3);
table.decimal('debit', 13, 3);
- table.string('transaction_type');
- table.string('reference_type');
- table.integer('reference_id');
- table.integer('account_id').unsigned();
- table.string('contact_type').nullable();
- table.integer('contact_id').unsigned().nullable();
+ table.string('transaction_type').index();
+ table.string('reference_type').index();
+ table.integer('reference_id').index();
+ table.integer('account_id').unsigned().index().references('id').inTable('accounts');
+ table.string('contact_type').nullable().index();
+ table.integer('contact_id').unsigned().nullable().index().references('id').inTable('contacts');
table.string('note');
table.boolean('draft').defaultTo(false);
- table.integer('user_id').unsigned();
+ table.integer('user_id').unsigned().index();
table.integer('index').unsigned();
- table.date('date');
+ table.date('date').index();
table.timestamps();
}).raw('ALTER TABLE `ACCOUNTS_TRANSACTIONS` AUTO_INCREMENT = 1000');
};
diff --git a/server/src/database/migrations/20200105013334_create_options_table.js b/server/src/database/migrations/20200105013334_create_options_table.js
deleted file mode 100644
index 2003c6c9b..000000000
--- a/server/src/database/migrations/20200105013334_create_options_table.js
+++ /dev/null
@@ -1,14 +0,0 @@
-
-exports.up = function(knex) {
- return knex.schema.createTable('options', (table) => {
- table.increments();
- table.string('key');
- table.string('value');
- table.string('group');
- table.string('type');
- });
-};
-
-exports.down = function(knex) {
- return knex.schema.dropTableIfExists('options');
-};
diff --git a/server/src/database/migrations/20200105014405_create_expenses_table.js b/server/src/database/migrations/20200105014405_create_expenses_table.js
index 40030dce3..5f4382e35 100644
--- a/server/src/database/migrations/20200105014405_create_expenses_table.js
+++ b/server/src/database/migrations/20200105014405_create_expenses_table.js
@@ -5,12 +5,12 @@ exports.up = function(knex) {
table.decimal('total_amount', 13, 3);
table.string('currency_code', 3);
table.text('description');
- table.integer('payment_account_id').unsigned();
- table.integer('payee_id').unsigned();
+ table.integer('payment_account_id').unsigned().references('id').inTable('accounts');
+ table.integer('payee_id').unsigned().references('id').inTable('contacts');;
table.string('reference_no');
- table.date('published_at');
- table.integer('user_id').unsigned();
- table.date('payment_date');
+ table.date('published_at').index();
+ table.integer('user_id').unsigned().index();
+ table.date('payment_date').index();
table.timestamps();
}).raw('ALTER TABLE `EXPENSES_TRANSACTIONS` AUTO_INCREMENT = 1000');
};
diff --git a/server/src/database/migrations/20200105195823_create_manual_journals_table.js b/server/src/database/migrations/20200105195823_create_manual_journals_table.js
index 69ba8a965..b4200c29a 100644
--- a/server/src/database/migrations/20200105195823_create_manual_journals_table.js
+++ b/server/src/database/migrations/20200105195823_create_manual_journals_table.js
@@ -2,15 +2,15 @@
exports.up = function(knex) {
return knex.schema.createTable('manual_journals', (table) => {
table.increments();
- table.string('journal_number');
- table.string('reference');
- table.string('journal_type');
+ table.string('journal_number').index();
+ table.string('reference').index();
+ table.string('journal_type').index();
table.decimal('amount', 13, 3);
- table.date('date');
- table.boolean('status').defaultTo(false);
+ table.date('date').index();
+ table.boolean('status').defaultTo(false).index();
table.string('description');
table.string('attachment_file');
- table.integer('user_id').unsigned();
+ table.integer('user_id').unsigned().index();
table.timestamps();
}).raw('ALTER TABLE `MANUAL_JOURNALS` AUTO_INCREMENT = 1000');
};
diff --git a/server/src/database/migrations/20200419171451_create_currencies_table.js b/server/src/database/migrations/20200419171451_create_currencies_table.js
index de6201bb7..c2d847140 100644
--- a/server/src/database/migrations/20200419171451_create_currencies_table.js
+++ b/server/src/database/migrations/20200419171451_create_currencies_table.js
@@ -2,8 +2,8 @@
exports.up = function(knex) {
return knex.schema.createTable('currencies', table => {
table.increments();
- table.string('currency_name');
- table.string('currency_code', 4);
+ table.string('currency_name').index();
+ table.string('currency_code', 4).index();
table.timestamps();
}).raw('ALTER TABLE `CURRENCIES` AUTO_INCREMENT = 1000');
};
diff --git a/server/src/database/migrations/20200419191832_create_exchange_rates_table.js b/server/src/database/migrations/20200419191832_create_exchange_rates_table.js
index e347c42f7..99db76530 100644
--- a/server/src/database/migrations/20200419191832_create_exchange_rates_table.js
+++ b/server/src/database/migrations/20200419191832_create_exchange_rates_table.js
@@ -2,9 +2,9 @@
exports.up = function(knex) {
return knex.schema.createTable('exchange_rates', table => {
table.increments();
- table.string('currency_code', 4);
+ table.string('currency_code', 4).index();
table.decimal('exchange_rate');
- table.date('date');
+ table.date('date').index();
table.timestamps();
}).raw('ALTER TABLE `EXCHANGE_RATES` AUTO_INCREMENT = 1000');
};
diff --git a/server/src/database/migrations/20200503032011_create_media_links_table.js b/server/src/database/migrations/20200503032011_create_media_links_table.js
index 52d5b4ca7..31d26be4b 100644
--- a/server/src/database/migrations/20200503032011_create_media_links_table.js
+++ b/server/src/database/migrations/20200503032011_create_media_links_table.js
@@ -2,9 +2,9 @@
exports.up = function(knex) {
return knex.schema.createTable('media_links', table => {
table.increments();
- table.string('model_name');
- table.integer('media_id').unsigned();
- table.integer('model_id').unsigned();
+ table.string('model_name').index();
+ table.integer('media_id').unsigned().references('id').inTable('media');
+ table.integer('model_id').unsigned().index();
})
};
diff --git a/server/src/database/migrations/20200606113848_create_expense_transactions_categories_table.js b/server/src/database/migrations/20200606113848_create_expense_transactions_categories_table.js
index 58568503b..b383bd668 100644
--- a/server/src/database/migrations/20200606113848_create_expense_transactions_categories_table.js
+++ b/server/src/database/migrations/20200606113848_create_expense_transactions_categories_table.js
@@ -2,11 +2,11 @@
exports.up = function(knex) {
return knex.schema.createTable('expense_transaction_categories', table => {
table.increments();
- table.integer('expense_account_id').unsigned();
+ table.integer('expense_account_id').unsigned().index().references('id').inTable('accounts');
table.integer('index').unsigned();
table.text('description');
table.decimal('amount', 13, 3);
- table.integer('expense_id').unsigned();
+ table.integer('expense_id').unsigned().index().references('id').inTable('expenses_transactions');
table.timestamps();
}).raw('ALTER TABLE `EXPENSE_TRANSACTION_CATEGORIES` AUTO_INCREMENT = 1000');;
};
diff --git a/server/src/database/migrations/20200713192127_create_sales_estimates_table.js b/server/src/database/migrations/20200713192127_create_sales_estimates_table.js
index 2dbce39ec..82d2d3ebe 100644
--- a/server/src/database/migrations/20200713192127_create_sales_estimates_table.js
+++ b/server/src/database/migrations/20200713192127_create_sales_estimates_table.js
@@ -3,15 +3,15 @@ exports.up = function(knex) {
return knex.schema.createTable('sales_estimates', (table) => {
table.increments();
table.decimal('amount', 13, 3);
- table.integer('customer_id').unsigned();
- table.date('estimate_date');
- table.date('expiration_date');
+ table.integer('customer_id').unsigned().index().references('id').inTable('contacts');
+ table.date('estimate_date').index();
+ table.date('expiration_date').index();
table.string('reference');
- table.string('estimate_number');
+ table.string('estimate_number').index();
table.text('note');
table.text('terms_conditions');
- table.integer('user_id').unsigned();
+ table.integer('user_id').unsigned().index();
table.timestamps();
});
};
diff --git a/server/src/database/migrations/20200713213303_create_sales_receipt_table.js b/server/src/database/migrations/20200713213303_create_sales_receipt_table.js
index 3adb04ba1..6b53bdd4d 100644
--- a/server/src/database/migrations/20200713213303_create_sales_receipt_table.js
+++ b/server/src/database/migrations/20200713213303_create_sales_receipt_table.js
@@ -3,9 +3,9 @@ exports.up = function(knex) {
return knex.schema.createTable('sales_receipts', table => {
table.increments();
table.decimal('amount', 13, 3);
- table.integer('deposit_account_id').unsigned();
- table.integer('customer_id').unsigned();
- table.date('receipt_date');
+ table.integer('deposit_account_id').unsigned().index().references('id').inTable('accounts');
+ table.integer('customer_id').unsigned().index().references('id').inTable('contacts');
+ table.date('receipt_date').index();
table.string('reference_no');
table.string('email_send_to');
table.text('receipt_message');
diff --git a/server/src/database/migrations/20200715193633_create_sale_invoices_table.js b/server/src/database/migrations/20200715193633_create_sale_invoices_table.js
index 7f5b394b8..9fac2e9f2 100644
--- a/server/src/database/migrations/20200715193633_create_sale_invoices_table.js
+++ b/server/src/database/migrations/20200715193633_create_sale_invoices_table.js
@@ -2,12 +2,12 @@
exports.up = function(knex) {
return knex.schema.createTable('sales_invoices', table => {
table.increments();
- table.integer('customer_id');
- table.date('invoice_date');
+ table.integer('customer_id').unsigned().index().references('id').inTable('contacts')
+ table.date('invoice_date').index();
table.date('due_date');
- table.string('invoice_no');
+ table.string('invoice_no').index();
table.string('reference_no');
- table.string('status');
+ table.string('status').index();
table.text('invoice_message');
table.text('terms_conditions');
@@ -15,7 +15,7 @@ exports.up = function(knex) {
table.decimal('balance', 13, 3);
table.decimal('payment_amount', 13, 3);
- table.string('inv_lot_number');
+ table.string('inv_lot_number').index();
table.timestamps();
});
};
diff --git a/server/src/database/migrations/20200715194514_create_payment_receives_table.js b/server/src/database/migrations/20200715194514_create_payment_receives_table.js
index bf299c4ee..758b32869 100644
--- a/server/src/database/migrations/20200715194514_create_payment_receives_table.js
+++ b/server/src/database/migrations/20200715194514_create_payment_receives_table.js
@@ -3,14 +3,14 @@ const { knexSnakeCaseMappers } = require("objection");
exports.up = function(knex) {
return knex.schema.createTable('payment_receives', (table) => {
table.increments();
- table.integer('customer_id').unsigned();
- table.date('payment_date');
+ table.integer('customer_id').unsigned().index().references('id').inTable('contacts');
+ table.date('payment_date').index();
table.decimal('amount', 13, 3).defaultTo(0);
- table.string('reference_no');
- table.integer('deposit_account_id').unsigned();
+ table.string('reference_no').index();
+ table.integer('deposit_account_id').unsigned().references('id').inTable('accounts');
table.string('payment_receive_no');
table.text('description');
- table.integer('user_id').unsigned();
+ table.integer('user_id').unsigned().index();
table.timestamps();
});
};
diff --git a/server/src/database/migrations/20200718161031_create_payment_receives_entries_table.js b/server/src/database/migrations/20200718161031_create_payment_receives_entries_table.js
index 627e46cbe..ffc9c665e 100644
--- a/server/src/database/migrations/20200718161031_create_payment_receives_entries_table.js
+++ b/server/src/database/migrations/20200718161031_create_payment_receives_entries_table.js
@@ -2,8 +2,8 @@
exports.up = function(knex) {
return knex.schema.createTable('payment_receives_entries', table => {
table.increments();
- table.integer('payment_receive_id').unsigned();
- table.integer('invoice_id').unsigned();
+ table.integer('payment_receive_id').unsigned().index().references('id').inTable('payment_receives');
+ table.integer('invoice_id').unsigned().index().references('id').inTable('sales_invoices');
table.decimal('payment_amount').unsigned();
})
};
diff --git a/server/src/database/migrations/20200719152005_create_bills_table.js b/server/src/database/migrations/20200719152005_create_bills_table.js
index d6f3c2f7a..efc81c7d6 100644
--- a/server/src/database/migrations/20200719152005_create_bills_table.js
+++ b/server/src/database/migrations/20200719152005_create_bills_table.js
@@ -2,18 +2,18 @@
exports.up = function(knex) {
return knex.schema.createTable('bills', (table) => {
table.increments();
- table.integer('vendor_id').unsigned();
+ table.integer('vendor_id').unsigned().index().references('id').inTable('contacts');
table.string('bill_number');
- table.date('bill_date');
- table.date('due_date');
+ table.date('bill_date').index();
+ table.date('due_date').index();
table.string('reference_no');
- table.string('status');
+ table.string('status').index();
table.text('note');
table.decimal('amount', 13, 3).defaultTo(0);
table.decimal('payment_amount', 13, 3).defaultTo(0);
- table.string('inv_lot_number');
+ table.string('inv_lot_number').index();
table.timestamps();
});
};
diff --git a/server/src/database/migrations/20200719153909_create_bills_payments_table.js b/server/src/database/migrations/20200719153909_create_bills_payments_table.js
index e069d0472..065f2bae6 100644
--- a/server/src/database/migrations/20200719153909_create_bills_payments_table.js
+++ b/server/src/database/migrations/20200719153909_create_bills_payments_table.js
@@ -2,14 +2,14 @@
exports.up = function(knex) {
return knex.schema.createTable('bills_payments', table => {
table.increments();
- table.integer('vendor_id').unsigned();
+ table.integer('vendor_id').unsigned().index().references('id').inTable('contacts');
table.decimal('amount', 13, 3).defaultTo(0);
- table.integer('payment_account_id');
- table.string('payment_number');
- table.date('payment_date');
+ table.integer('payment_account_id').unsigned().references('id').inTable('accounts');
+ table.string('payment_number').index();
+ table.date('payment_date').index();
table.string('payment_method');
table.string('reference');
- table.integer('user_id').unsigned();
+ table.integer('user_id').unsigned().index();
table.text('description');
table.timestamps();
});
diff --git a/server/src/database/migrations/20200722164251_create_inventory_transactions_table.js b/server/src/database/migrations/20200722164251_create_inventory_transactions_table.js
index ede2040c4..a62b4d905 100644
--- a/server/src/database/migrations/20200722164251_create_inventory_transactions_table.js
+++ b/server/src/database/migrations/20200722164251_create_inventory_transactions_table.js
@@ -2,20 +2,20 @@
exports.up = function(knex) {
return knex.schema.createTable('inventory_transactions', table => {
table.increments('id');
- table.date('date');
+ table.date('date').index();
- table.string('direction');
+ table.string('direction').index();
- table.integer('item_id').unsigned();
+ table.integer('item_id').unsigned().index().references('id').inTable('items');
table.integer('quantity').unsigned();
table.decimal('rate', 13, 3).unsigned();
- table.integer('lot_number');
+ table.integer('lot_number').index();
- table.string('transaction_type');
- table.integer('transaction_id').unsigned();
+ table.string('transaction_type').index();
+ table.integer('transaction_id').unsigned().index();
- table.integer('entry_id').unsigned();
+ table.integer('entry_id').unsigned().index();
table.timestamps();
});
};
diff --git a/server/src/database/migrations/20200722173423_create_items_entries_table.js b/server/src/database/migrations/20200722173423_create_items_entries_table.js
index 902ef253d..dc36d5b72 100644
--- a/server/src/database/migrations/20200722173423_create_items_entries_table.js
+++ b/server/src/database/migrations/20200722173423_create_items_entries_table.js
@@ -2,11 +2,11 @@
exports.up = function(knex) {
return knex.schema.createTable('items_entries', (table) => {
table.increments();
- table.string('reference_type');
- table.string('reference_id');
+ table.string('reference_type').index();
+ table.string('reference_id').index();
table.integer('index').unsigned();
- table.integer('item_id');
+ table.integer('item_id').unsigned().index().references('id').inTable('items');
table.text('description');
table.integer('discount').unsigned();
table.integer('quantity').unsigned();
diff --git a/server/src/database/migrations/20200728161617_create_bill_payments_entries.js b/server/src/database/migrations/20200728161617_create_bill_payments_entries.js
index 7dac8c241..30af9d50f 100644
--- a/server/src/database/migrations/20200728161617_create_bill_payments_entries.js
+++ b/server/src/database/migrations/20200728161617_create_bill_payments_entries.js
@@ -3,8 +3,8 @@ exports.up = function(knex) {
return knex.schema.createTable('bills_payments_entries', table => {
table.increments();
- table.integer('bill_payment_id').unsigned();
- table.integer('bill_id').unsigned();
+ table.integer('bill_payment_id').unsigned().index().references('id').inTable('bills_payments');
+ table.integer('bill_id').unsigned().index();
table.decimal('payment_amount', 13, 3).unsigned();
})
};
diff --git a/server/src/database/migrations/20200810121807_create_inventory_cost_lot_tracker_table.js b/server/src/database/migrations/20200810121807_create_inventory_cost_lot_tracker_table.js
index 51b31cf06..344bf30e4 100644
--- a/server/src/database/migrations/20200810121807_create_inventory_cost_lot_tracker_table.js
+++ b/server/src/database/migrations/20200810121807_create_inventory_cost_lot_tracker_table.js
@@ -2,19 +2,19 @@
exports.up = function(knex) {
return knex.schema.createTable('inventory_cost_lot_tracker', table => {
table.increments();
- table.date('date');
- table.string('direction');
+ table.date('date').index();
+ table.string('direction').index();
- table.integer('item_id').unsigned();
- table.integer('quantity').unsigned();
+ table.integer('item_id').unsigned().index();
+ table.integer('quantity').unsigned().index();
table.decimal('rate', 13, 3);
table.integer('remaining');
table.integer('cost');
- table.integer('lot_number');
+ table.integer('lot_number').index();
- table.string('transaction_type');
- table.integer('transaction_id').unsigned();
- table.integer('entry_id').unsigned();
+ table.string('transaction_type').index();
+ table.integer('transaction_id').unsigned().index();
+ table.integer('entry_id').unsigned().index();
});
};
diff --git a/server/src/database/seeds/core/20190423085241_seed_accounts_types.js b/server/src/database/seeds/core/20190423085241_seed_accounts_types.js
index fd03d2f7d..ffe6b9b77 100644
--- a/server/src/database/seeds/core/20190423085241_seed_accounts_types.js
+++ b/server/src/database/seeds/core/20190423085241_seed_accounts_types.js
@@ -1,8 +1,14 @@
+import Container from 'typedi';
+import TenancyService from 'services/Tenancy/TenancyService'
+import I18nMiddleware from 'api/middleware/I18nMiddleware';
+
exports.up = function (knex) {
+ const tenancyService = Container.get(TenancyService);
+ const i18n = tenancyService.i18n(knex.userParams.tenantId);
+
return knex('account_types').insert([
{
id: 1,
- name: 'Fixed Asset',
key: 'fixed_asset',
normal: 'debit',
root_type: 'asset',
@@ -12,7 +18,6 @@ exports.up = function (knex) {
},
{
id: 2,
- name: 'Current Asset',
key: 'current_asset',
normal: 'debit',
root_type: 'asset',
@@ -22,7 +27,6 @@ exports.up = function (knex) {
},
{
id: 14,
- name: 'Other Asset',
key: 'other_asset',
normal: 'debit',
root_type: 'asset',
@@ -32,7 +36,6 @@ exports.up = function (knex) {
},
{
id: 3,
- name: 'Long Term Liability',
key: 'long_term_liability',
normal: 'credit',
root_type: 'liability',
@@ -42,7 +45,6 @@ exports.up = function (knex) {
},
{
id: 4,
- name: 'Current Liability',
key: 'current_liability',
normal: 'credit',
root_type: 'liability',
@@ -52,7 +54,6 @@ exports.up = function (knex) {
},
{
id: 13,
- name: 'Other Liability',
key: 'other_liability',
normal: 'credit',
root_type: 'liability',
@@ -62,7 +63,6 @@ exports.up = function (knex) {
},
{
id: 5,
- name: 'Equity',
key: 'equity',
normal: 'credit',
root_type: 'equity',
@@ -72,7 +72,6 @@ exports.up = function (knex) {
},
{
id: 6,
- name: 'Expense',
key: 'expense',
normal: 'debit',
root_type: 'expense',
@@ -82,7 +81,6 @@ exports.up = function (knex) {
},
{
id: 10,
- name: 'Other Expense',
key: 'other_expense',
normal: 'debit',
root_type: 'expense',
@@ -91,7 +89,6 @@ exports.up = function (knex) {
},
{
id: 7,
- name: 'Income',
key: 'income',
normal: 'credit',
root_type: 'income',
@@ -101,7 +98,6 @@ exports.up = function (knex) {
},
{
id: 11,
- name: 'Other Income',
key: 'other_income',
normal: 'credit',
root_type: 'income',
@@ -111,7 +107,6 @@ exports.up = function (knex) {
},
{
id: 12,
- name: 'Cost of Goods Sold (COGS)',
key: 'cost_of_goods_sold',
normal: 'debit',
root_type: 'expenses',
@@ -121,7 +116,6 @@ exports.up = function (knex) {
},
{
id: 8,
- name: 'Accounts Receivable (A/R)',
key: 'accounts_receivable',
normal: 'debit',
root_type: 'asset',
@@ -131,7 +125,6 @@ exports.up = function (knex) {
},
{
id: 9,
- name: 'Accounts Payable (A/P)',
key: 'accounts_payable',
normal: 'credit',
root_type: 'liability',
diff --git a/server/src/database/seeds/core/20190423085240_seed_accounts.js b/server/src/database/seeds/core/20190423085242_seed_accounts.js
similarity index 85%
rename from server/src/database/seeds/core/20190423085240_seed_accounts.js
rename to server/src/database/seeds/core/20190423085242_seed_accounts.js
index cce8e6d27..58c29f3cc 100644
--- a/server/src/database/seeds/core/20190423085240_seed_accounts.js
+++ b/server/src/database/seeds/core/20190423085242_seed_accounts.js
@@ -1,16 +1,18 @@
-import TenancyService from 'services/Tenancy/TenancyService'
import Container from 'typedi';
+import TenancyService from 'services/Tenancy/TenancyService'
exports.up = function (knex) {
const tenancyService = Container.get(TenancyService);
const i18n = tenancyService.i18n(knex.userParams.tenantId);
+ console.log(i18n);
+
return knex('accounts').then(() => {
// Inserts seed entries
return knex('accounts').insert([
{
id: 1,
- name: 'Petty Cash',
+ name: i18n.__('Petty Cash'),
slug: 'petty-cash',
account_type_id: 2,
parent_account_id: null,
@@ -22,7 +24,7 @@ exports.up = function (knex) {
},
{
id: 2,
- name: 'Bank',
+ name: i18n.__('Bank'),
slug: 'bank',
account_type_id: 2,
parent_account_id: null,
@@ -34,7 +36,7 @@ exports.up = function (knex) {
},
{
id: 3,
- name: 'Other Income',
+ name: i18n.__('Other Income'),
slug: 'other-income',
account_type_id: 7,
parent_account_id: null,
@@ -46,7 +48,7 @@ exports.up = function (knex) {
},
{
id: 4,
- name: 'Interest Income',
+ name: i18n.__('Interest Income'),
slug: 'interest-income',
account_type_id: 7,
parent_account_id: null,
@@ -58,7 +60,7 @@ exports.up = function (knex) {
},
{
id: 5,
- name: 'Opening Balance',
+ name: i18n.__('Opening Balance'),
slug: 'opening-balance',
account_type_id: 5,
parent_account_id: null,
@@ -70,7 +72,7 @@ exports.up = function (knex) {
},
{
id: 6,
- name: 'Depreciation Expense',
+ name: i18n.__('Depreciation Expense'),
slug: 'depreciation-expense',
account_type_id: 6,
parent_account_id: null,
@@ -82,7 +84,7 @@ exports.up = function (knex) {
},
{
id: 7,
- name: 'Interest Expense',
+ name: i18n.__('Interest Expense'),
slug: 'interest-expense',
account_type_id: 6,
parent_account_id: null,
@@ -94,7 +96,7 @@ exports.up = function (knex) {
},
{
id: 8,
- name: 'Payroll Expenses',
+ name: i18n.__('Payroll Expenses'),
slug: 'payroll-expenses',
account_type_id: 6,
parent_account_id: null,
@@ -106,7 +108,7 @@ exports.up = function (knex) {
},
{
id: 9,
- name: 'Other Expenses',
+ name: i18n.__('Other Expenses'),
slug: 'other-expenses',
account_type_id: 6,
parent_account_id: null,
@@ -118,7 +120,7 @@ exports.up = function (knex) {
},
{
id: 10,
- name: 'Accounts Receivable',
+ name: i18n.__('Accounts Receivable'),
slug: 'accounts-receivable',
account_type_id: 8,
parent_account_id: null,
@@ -130,7 +132,7 @@ exports.up = function (knex) {
},
{
id: 11,
- name: 'Accounts Payable',
+ name: i18n.__('Accounts Payable'),
slug: 'accounts-payable',
account_type_id: 9,
parent_account_id: null,
@@ -142,7 +144,7 @@ exports.up = function (knex) {
},
{
id: 12,
- name: 'Cost of Goods Sold (COGS)',
+ name: i18n.__('Cost of Goods Sold (COGS)'),
slug: 'cost-of-goods-sold',
account_type_id: 12,
predefined: 1,
@@ -153,7 +155,7 @@ exports.up = function (knex) {
},
{
id: 13,
- name: 'Inventory Asset',
+ name: i18n.__('Inventory Asset'),
slug: 'inventory-asset',
account_type_id: 14,
predefined: 1,
@@ -164,7 +166,7 @@ exports.up = function (knex) {
},
{
id: 14,
- name: 'Sales of Product Income',
+ name: i18n.__('Sales of Product Income'),
slug: 'sales-of-product-income',
account_type_id: 7,
predefined: 1,
diff --git a/server/src/database/seeds/core/20200810121807_seed_views.js b/server/src/database/seeds/core/20200810121807_seed_views.js
index 87d383888..5e945978c 100644
--- a/server/src/database/seeds/core/20200810121807_seed_views.js
+++ b/server/src/database/seeds/core/20200810121807_seed_views.js
@@ -1,17 +1,20 @@
exports.up = (knex) => {
+ const tenancyService = Container.get(TenancyService);
+ const i18n = tenancyService.i18n(knex.userParams.tenantId);
+
// Deletes ALL existing entries
return knex('views').del()
.then(() => {
// Inserts seed entries
return knex('views').insert([
// Accounts
- { id: 15, name: 'Inactive', roles_logic_expression: '1', resource_model: 'Account', predefined: true },
- { id: 1, name: 'Assets', roles_logic_expression: '1', resource_model: 'Account', predefined: true },
- { id: 2, name: 'Liabilities', roles_logic_expression: '1', resource_model: 'Account', predefined: true },
- { id: 3, name: 'Equity', roles_logic_expression: '1', resource_model: 'Account', predefined: true },
- { id: 4, name: 'Income', roles_logic_expression: '1', resource_model: 'Account', predefined: true },
- { id: 5, name: 'Expenses', 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: i18n.__('Assets'), 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: i18n.__('Equity'), 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: i18n.__('Expenses'), roles_logic_expression: '1', resource_model: 'Account', predefined: true },
// Items
// { id: 6, name: 'Services', roles_logic_expression: '1', resource_id: 2, predefined: true },
diff --git a/server/src/interfaces/Register.ts b/server/src/interfaces/Authentication.ts
similarity index 89%
rename from server/src/interfaces/Register.ts
rename to server/src/interfaces/Authentication.ts
index 375c800ac..be86dfdd3 100644
--- a/server/src/interfaces/Register.ts
+++ b/server/src/interfaces/Authentication.ts
@@ -9,6 +9,11 @@ export interface IRegisterDTO {
organizationName: string,
};
+export interface ILoginDTO {
+ crediential: string,
+ password: string,
+};
+
export interface IPasswordReset {
id: number,
email: string,
diff --git a/server/src/interfaces/Contact.ts b/server/src/interfaces/Contact.ts
index e0183cd8e..c2be84a73 100644
--- a/server/src/interfaces/Contact.ts
+++ b/server/src/interfaces/Contact.ts
@@ -178,8 +178,9 @@ export interface IVendorsFilter extends IDynamicListFilter {
pageSize?: number,
};
-export interface ICustomerFilter extends IDynamicListFilter {
+export interface ICustomersFilter extends IDynamicListFilter {
stringifiedFilterRoles?: string,
page?: number,
pageSize?: number,
-};
\ No newline at end of file
+};
+
diff --git a/server/src/interfaces/Expenses.ts b/server/src/interfaces/Expenses.ts
index 8ed67b362..4a3263f0f 100644
--- a/server/src/interfaces/Expenses.ts
+++ b/server/src/interfaces/Expenses.ts
@@ -1,5 +1,16 @@
import { ISystemUser } from "./User";
+export interface IPaginationMeta {
+ total: number,
+ page: number,
+ pageSize: number,
+};
+
+export interface IExpensesFilter{
+ page: number,
+ pageSize: number,
+};
+
export interface IExpense {
id: number,
totalAmount: number,
@@ -53,4 +64,7 @@ export interface IExpensesService {
deleteBulkExpenses(tenantId: number, expensesIds: number[], authorizedUser: ISystemUser): Promise;
publishBulkExpenses(tenantId: number, expensesIds: number[], authorizedUser: ISystemUser): Promise;
+
+ getExpensesList(tenantId: number, expensesFilter: IExpensesFilter): Promise<{ expenses: IExpense[], pagination: IPaginationMeta, filterMeta: IFilterMeta }>;
+ getExpense(tenantId: number, expenseId: number): Promise;
}
\ No newline at end of file
diff --git a/server/src/interfaces/Item.ts b/server/src/interfaces/Item.ts
index d0824056c..2887481ec 100644
--- a/server/src/interfaces/Item.ts
+++ b/server/src/interfaces/Item.ts
@@ -70,6 +70,6 @@ export interface IItemsService {
export interface IItemsFilter extends IDynamicListFilter {
stringifiedFilterRoles?: string,
- page?: number,
- pageSize?: number,
+ page: number,
+ pageSize: number,
};
\ No newline at end of file
diff --git a/server/src/interfaces/ManualJournal.ts b/server/src/interfaces/ManualJournal.ts
index cc40c3611..2a1e0fc05 100644
--- a/server/src/interfaces/ManualJournal.ts
+++ b/server/src/interfaces/ManualJournal.ts
@@ -37,8 +37,8 @@ export interface IManualJournalDTO {
export interface IManualJournalsFilter extends IDynamicListFilterDTO {
stringifiedFilterRoles?: string,
- page?: number,
- pageSize?: number,
+ page: number,
+ pageSize: number,
}
export interface IManuaLJournalsService {
@@ -48,5 +48,6 @@ export interface IManuaLJournalsService {
deleteManualJournals(tenantId: number, manualJournalsIds: number[]): Promise;
publishManualJournals(tenantId: number, manualJournalsIds: number[]): Promise;
publishManualJournal(tenantId: number, manualJournalId: number): Promise;
- getManualJournals(tenantId: number, filter: IManualJournalsFilter): Promise;
+
+ getManualJournals(tenantId: number, filter: IManualJournalsFilter): Promise<{ manualJournals: IManualJournal, pagination: IPaginationMeta, filterMeta: IFilterMeta }>;
}
\ No newline at end of file
diff --git a/server/src/interfaces/Media.ts b/server/src/interfaces/Media.ts
new file mode 100644
index 000000000..6cd338583
--- /dev/null
+++ b/server/src/interfaces/Media.ts
@@ -0,0 +1,25 @@
+
+
+export interface IMedia {
+ id?: number,
+ attachmentFile: string,
+ createdAt?: Date,
+};
+
+export interface IMediaLink {
+ mediaId: number,
+ modelName: string,
+ modelId: number,
+};
+
+export interface IMediaLinkDTO {
+ modelName: string,
+ modelId: number,
+};
+
+export interface IMediaService {
+ linkMedia(tenantId: number, mediaId: number, modelId?: number, modelName?: string): Promise;
+ getMedia(tenantId: number, mediaId: number): Promise;
+ deleteMedia(tenantId: number, mediaId: number | number[]): Promise;
+ upload(tenantId: number, attachment: any, modelName?: string, modelId?: number): Promise;
+}
\ No newline at end of file
diff --git a/server/src/interfaces/Model.ts b/server/src/interfaces/Model.ts
new file mode 100644
index 000000000..91f20c15f
--- /dev/null
+++ b/server/src/interfaces/Model.ts
@@ -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,
+};
\ No newline at end of file
diff --git a/server/src/interfaces/SaleInvoice.ts b/server/src/interfaces/SaleInvoice.ts
index 1628c9da0..ac8850243 100644
--- a/server/src/interfaces/SaleInvoice.ts
+++ b/server/src/interfaces/SaleInvoice.ts
@@ -15,4 +15,9 @@ export interface ISaleInvoiceOTD {
invoiceMessage: string,
termsConditions: string,
entries: any[],
-}
\ No newline at end of file
+}
+
+export interface ISalesInvoicesFilter{
+ page: number,
+ pageSize: number,
+};
\ No newline at end of file
diff --git a/server/src/interfaces/SaleReceipt.ts b/server/src/interfaces/SaleReceipt.ts
new file mode 100644
index 000000000..1866a6cfd
--- /dev/null
+++ b/server/src/interfaces/SaleReceipt.ts
@@ -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;
+ editSaleReceipt(tenantId: number, saleReceiptId: number): Promise;
+
+ deleteSaleReceipt(tenantId: number, saleReceiptId: number): Promise;
+ salesReceiptsList(tennatid: number, salesReceiptsFilter: ISalesReceiptsFilter): Promise<{ salesReceipts: ISaleReceipt[], pagination: IPaginationMeta, filterMeta: IFilterMeta }>;
+};
\ No newline at end of file
diff --git a/server/src/interfaces/View.ts b/server/src/interfaces/View.ts
index 5e7ccbcf3..06197cae8 100644
--- a/server/src/interfaces/View.ts
+++ b/server/src/interfaces/View.ts
@@ -5,7 +5,10 @@ export interface IView {
predefined: boolean,
resourceModel: string,
favourite: boolean,
- rolesLogicRxpression: string,
+ rolesLogicExpression: string,
+
+ roles: IViewRole[],
+ columns: IViewHasColumn[],
};
export interface IViewRole {
@@ -42,6 +45,8 @@ export interface IViewColumnDTO {
export interface IViewDTO {
name: string,
logicExpression: string,
+ resourceModel: string,
+
roles: IViewRoleDTO[],
columns: IViewColumnDTO[],
};
@@ -49,12 +54,13 @@ export interface IViewDTO {
export interface IViewEditDTO {
name: string,
logicExpression: string,
+
roles: IViewRoleDTO[],
columns: IViewColumnDTO[],
};
export interface IViewsService {
- listViews(tenantId: number, resourceModel: string): Promise;
+ listResourceViews(tenantId: number, resourceModel: string): Promise;
newView(tenantId: number, viewDTO: IViewDTO): Promise;
editView(tenantId: number, viewId: number, viewEditDTO: IViewEditDTO): Promise;
deleteView(tenantId: number, viewId: number): Promise;
diff --git a/server/src/interfaces/index.ts b/server/src/interfaces/index.ts
index 17852dea8..61d02b690 100644
--- a/server/src/interfaces/index.ts
+++ b/server/src/interfaces/index.ts
@@ -1,3 +1,5 @@
+
+export * from './Model';
export * from './InventoryTransaction';
export * from './BillPayment';
export * from './InventoryCostMethod';
@@ -9,7 +11,7 @@ export * from './Payment';
export * from './SaleInvoice';
export * from './PaymentReceive';
export * from './SaleEstimate';
-export * from './Register';
+export * from './Authentication';
export * from './User';
export * from './Metable';
export * from './Options';
@@ -22,4 +24,5 @@ export * from './Tenancy';
export * from './View';
export * from './ManualJournal';
export * from './Currency';
-export * from './ExchangeRate';
\ No newline at end of file
+export * from './ExchangeRate';
+export * from './Media';
\ No newline at end of file
diff --git a/server/src/lib/DynamicFilter/DynamicFilter.js b/server/src/lib/DynamicFilter/DynamicFilter.ts
similarity index 54%
rename from server/src/lib/DynamicFilter/DynamicFilter.js
rename to server/src/lib/DynamicFilter/DynamicFilter.ts
index 62000f11f..cb795ed88 100644
--- a/server/src/lib/DynamicFilter/DynamicFilter.js
+++ b/server/src/lib/DynamicFilter/DynamicFilter.ts
@@ -1,15 +1,20 @@
-import { uniqBy } from 'lodash';
+import { forEach, uniqBy } from 'lodash';
import {
buildFilterRolesJoins,
} from 'lib/ViewRolesBuilder';
+import { IModel } from 'interfaces';
export default class DynamicFilter {
+ model: IModel;
+ tableName: string;
+
/**
* Constructor.
* @param {String} tableName -
*/
- constructor(tableName) {
- this.tableName = tableName;
+ constructor(model) {
+ this.model = model;
+ this.tableName = model.tableName;
this.filters = [];
}
@@ -18,7 +23,7 @@ export default class DynamicFilter {
* @param {*} filterRole -
*/
setFilter(filterRole) {
- filterRole.setTableName(this.tableName);
+ filterRole.setModel(this.model);
this.filters.push(filterRole);
}
@@ -38,7 +43,23 @@ export default class DynamicFilter {
buildersCallbacks.forEach((builderCallback) => {
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;
+ }
}
\ No newline at end of file
diff --git a/server/src/lib/DynamicFilter/DynamicFilterFilterRoles.ts b/server/src/lib/DynamicFilter/DynamicFilterFilterRoles.ts
index 4a8366a8d..0a64d5a43 100644
--- a/server/src/lib/DynamicFilter/DynamicFilterFilterRoles.ts
+++ b/server/src/lib/DynamicFilter/DynamicFilterFilterRoles.ts
@@ -14,14 +14,14 @@ export default class FilterRoles extends DynamicFilterRoleAbstructor {
constructor(filterRoles: IFilterRole[]) {
super();
this.filterRoles = filterRoles;
+ this.setResponseMeta();
}
private buildLogicExpression(): string {
let expression = '';
this.filterRoles.forEach((role, index) => {
expression += (index === 0) ?
- `${role.index} ` :
- `${role.condition} ${role.index} `;
+ `${role.index} ` : `${role.condition} ${role.index} `;
});
return expression.trim();
}
@@ -32,7 +32,16 @@ export default class FilterRoles extends DynamicFilterRoleAbstructor {
buildQuery() {
return (builder) => {
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
};
}
}
\ No newline at end of file
diff --git a/server/src/lib/DynamicFilter/DynamicFilterRoleAbstructor.ts b/server/src/lib/DynamicFilter/DynamicFilterRoleAbstructor.ts
index 5ccfe111b..006008ddd 100644
--- a/server/src/lib/DynamicFilter/DynamicFilterRoleAbstructor.ts
+++ b/server/src/lib/DynamicFilter/DynamicFilterRoleAbstructor.ts
@@ -1,10 +1,13 @@
-import { IFilterRole, IDynamicFilter } from "interfaces";
+import { IFilterRole, IDynamicFilter, IModel } from "interfaces";
export default class DynamicFilterAbstructor implements IDynamicFilter {
filterRoles: IFilterRole[] = [];
tableName: string;
+ model: IModel;
+ responseMeta: { [key: string]: any } = {};
- setTableName(tableName) {
- this.tableName = tableName;
+ setModel(model: IModel) {
+ this.model = model;
+ this.tableName = model.tableName;
}
}
\ No newline at end of file
diff --git a/server/src/lib/DynamicFilter/DynamicFilterSortBy.ts b/server/src/lib/DynamicFilter/DynamicFilterSortBy.ts
index 32b35f555..3d413aa5e 100644
--- a/server/src/lib/DynamicFilter/DynamicFilterSortBy.ts
+++ b/server/src/lib/DynamicFilter/DynamicFilterSortBy.ts
@@ -16,10 +16,11 @@ export default class DynamicFilterSortBy extends DynamicFilterRoleAbstructor {
fieldKey: sortByFieldKey,
order: sortDirection,
};
+ this.setResponseMeta();
}
validate() {
- validateFieldKeyExistance(this.tableName, this.sortRole.fieldKey);
+ validateFieldKeyExistance(this.model, this.sortRole.fieldKey);
}
/**
@@ -27,7 +28,7 @@ export default class DynamicFilterSortBy extends DynamicFilterRoleAbstructor {
*/
buildQuery() {
return (builder) => {
- const fieldRelation = getRoleFieldColumn(this.tableName, this.sortRole.fieldKey);
+ const fieldRelation = getRoleFieldColumn(this.model, this.sortRole.fieldKey);
const comparatorColumn =
fieldRelation.relationColumn ||
`${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,
+ };
+ }
}
diff --git a/server/src/lib/DynamicFilter/DynamicFilterViews.ts b/server/src/lib/DynamicFilter/DynamicFilterViews.ts
index b02a86aa1..d8f2bfc84 100644
--- a/server/src/lib/DynamicFilter/DynamicFilterViews.ts
+++ b/server/src/lib/DynamicFilter/DynamicFilterViews.ts
@@ -1,25 +1,29 @@
-import { IFilterRole } from 'interfaces';
+import { omit } from 'lodash';
+import { IView, IViewRole } from 'interfaces';
import DynamicFilterRoleAbstructor from 'lib/DynamicFilter/DynamicFilterRoleAbstructor';
import {
- validateViewRoles,
buildFilterQuery,
} from 'lib/ViewRolesBuilder';
export default class DynamicFilterViews extends DynamicFilterRoleAbstructor {
+ viewId: number;
logicExpression: string;
+ filterRoles: IViewRole[];
/**
* Constructor method.
- * @param {*} filterRoles - Filter roles.
- * @param {*} logicExpression - Logic expression.
+ * @param {IView} view -
*/
- constructor(filterRoles: IFilterRole[], logicExpression: string) {
+ constructor(view: IView) {
super();
- this.filterRoles = filterRoles;
- this.logicExpression = logicExpression
+ this.viewId = view.id;
+ this.filterRoles = view.roles;
+ this.logicExpression = view.rolesLogicExpression
.replace('AND', '&&')
.replace('OR', '||');
+
+ this.setResponseMeta();
}
/**
@@ -28,20 +32,27 @@ export default class DynamicFilterViews extends DynamicFilterRoleAbstructor {
buildLogicExpression() {
return this.logicExpression;
}
-
- /**
- * Validates filter roles.
- */
- validate() {
- return validateViewRoles(this.filterRoles, this.logicExpression);
- }
-
+
/**
* Builds database query of view roles.
*/
buildQuery() {
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,
+ }
};
}
}
\ No newline at end of file
diff --git a/server/src/lib/ViewRolesBuilder/index.ts b/server/src/lib/ViewRolesBuilder/index.ts
index 39af35931..f8eeff373 100644
--- a/server/src/lib/ViewRolesBuilder/index.ts
+++ b/server/src/lib/ViewRolesBuilder/index.ts
@@ -1,10 +1,9 @@
-import { difference, filter } from 'lodash';
+import { difference } from 'lodash';
import moment from 'moment';
import { Lexer } from 'lib/LogicEvaluation/Lexer';
import Parser from 'lib/LogicEvaluation/Parser';
import QueryParser from 'lib/LogicEvaluation/QueryParser';
-import resourceFieldsKeys from 'data/ResourceFieldsKeys';
-import { IFilterRole } from 'interfaces';
+import { IFilterRole, IModel } from 'interfaces';
const numberRoleQueryBuilder = (role: IFilterRole, comparatorColumn: string) => {
switch (role.comparator) {
@@ -93,7 +92,7 @@ const dateQueryBuilder = (role: IFilterRole, comparatorColumn: string) => {
if (hasTimeFormat) {
const targetDateTime = moment(role.value).format(dateFormat);
builder.where(comparatorColumn, '=', targetDateTime);
- } else {
+ } else {
const startDate = moment(role.value).startOf('day');
const endDate = moment(role.value).endOf('day');
@@ -109,19 +108,19 @@ const dateQueryBuilder = (role: IFilterRole, comparatorColumn: string) => {
* @param {String} tableName - Table name of target column.
* @param {String} fieldKey - Target column key that stored in resource field.
*/
-export function getRoleFieldColumn(tableName: string, fieldKey: string) {
- const tableFields = resourceFieldsKeys[tableName];
+export function getRoleFieldColumn(model: IModel, fieldKey: string) {
+ const tableFields = model.fields;
return (tableFields[fieldKey]) ? tableFields[fieldKey] : null;
}
/**
* Builds roles queries.
- * @param {String} tableName -
+ * @param {IModel} model -
* @param {Object} role -
*/
-export function buildRoleQuery(tableName: string, role: IFilterRole) {
- const fieldRelation = getRoleFieldColumn(tableName, role.fieldKey);
- const comparatorColumn = fieldRelation.relationColumn || `${tableName}.${fieldRelation.column}`;
+export function buildRoleQuery(model: IModel, role: IFilterRole) {
+ const fieldRelation = getRoleFieldColumn(model, role.fieldKey);
+ const comparatorColumn = fieldRelation.relationColumn || `${model.tableName}.${fieldRelation.column}`;
switch (fieldRelation.columnType) {
case 'number':
@@ -150,26 +149,26 @@ export const getTableFromRelationColumn = (column: string) => {
* @param {String} tableName -
* @param {Array} roles -
*/
-export function buildFilterRolesJoins(tableName: string, roles: IFilterRole[]) {
+export function buildFilterRolesJoins(model: IModel, roles: IFilterRole[]) {
return (builder) => {
roles.forEach((role) => {
- const fieldColumn = getRoleFieldColumn(tableName, role.fieldKey);
+ const fieldColumn = getRoleFieldColumn(model, role.fieldKey);
if (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) => {
- const fieldColumn = getRoleFieldColumn(tableName, sortColumnKey);
+ const fieldColumn = getRoleFieldColumn(model, sortColumnKey);
if (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 -
* @return {Function}
*/
-export function buildFilterRolesQuery(tableName: string, roles: IFilterRole[], logicExpression: string = '') {
+export function buildFilterRolesQuery(model: IModel, roles: IFilterRole[], logicExpression: string = '') {
const rolesIndexSet = {};
roles.forEach((role) => {
- rolesIndexSet[role.index] = buildRoleQuery(tableName, role);
+ rolesIndexSet[role.index] = buildRoleQuery(model, role);
});
// Lexer for logic expression.
const lexer = new Lexer(logicExpression);
@@ -204,9 +203,9 @@ export function buildFilterRolesQuery(tableName: string, roles: IFilterRole[], l
* @param {Array} roles -
* @param {String} logicExpression -
*/
-export const buildFilterQuery = (tableName: string, roles, logicExpression: string) => {
+export const buildFilterQuery = (model: IModel, roles: IFilterRole[], logicExpression: string) => {
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} sortDirection -
*/
-export function buildSortColumnQuery(tableName: string, columnKey: string, sortDirection: string) {
- const fieldRelation = getRoleFieldColumn(tableName, columnKey);
- const sortColumn = fieldRelation.relation || `${tableName}.${fieldRelation.column}`;
+export function buildSortColumnQuery(model: IModel, columnKey: string, sortDirection: string) {
+ const fieldRelation = getRoleFieldColumn(model, columnKey);
+ const sortColumn = fieldRelation.relation || `${model.tableName}.${fieldRelation.column}`;
return (builder) => {
builder.orderBy(sortColumn, sortDirection);
- buildSortColumnJoin(tableName, columnKey)(builder);
+ buildSortColumnJoin(model, columnKey)(builder);
};
}
export function validateFilterLogicExpression(logicExpression: string, indexes) {
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[]) {
return validateFilterLogicExpression(logicExpression, roles.map((r) => r.index));
}
-export function validateFieldKeyExistance(tableName: string, fieldKey: string) {
- if (!resourceFieldsKeys?.[tableName]?.[fieldKey])
- return fieldKey;
- else
- return false;
+export function validateFieldKeyExistance(model: any, fieldKey: string) {
+ return model?.fields?.[fieldKey] || false;
}
-export function validateFilterRolesFieldsExistance(tableName, filterRoles: IFilterRole[]) {
+export function validateFilterRolesFieldsExistance(model, filterRoles: IFilterRole[]) {
return filterRoles.filter((filterRole: IFilterRole) => {
- return validateFieldKeyExistance(tableName, filterRole.fieldKey);
+ return !validateFieldKeyExistance(model, filterRole.fieldKey);
});
}
\ No newline at end of file
diff --git a/server/src/loaders/events.ts b/server/src/loaders/events.ts
index fed18f4b3..e81cf6659 100644
--- a/server/src/loaders/events.ts
+++ b/server/src/loaders/events.ts
@@ -1,4 +1,7 @@
+import { Container } from 'typedi';
+
// Here we import all events.
import 'subscribers/authentication';
import 'subscribers/organization';
import 'subscribers/manualJournals';
+import 'subscribers/expenses';
\ No newline at end of file
diff --git a/server/src/loaders/i18n.ts b/server/src/loaders/i18n.ts
index b87823dd2..1c27f8238 100644
--- a/server/src/loaders/i18n.ts
+++ b/server/src/loaders/i18n.ts
@@ -4,6 +4,6 @@ import path from 'path';
export default () => i18n.configure({
locales: ['en', 'ar'],
register: global,
- directory: path.join(global.__root, 'src/locales'),
+ directory: path.join(global.__root, 'locales'),
updateFiles: false
})
\ No newline at end of file
diff --git a/server/src/loaders/tenantModels.ts b/server/src/loaders/tenantModels.ts
index d5c124dce..e5f32d033 100644
--- a/server/src/loaders/tenantModels.ts
+++ b/server/src/loaders/tenantModels.ts
@@ -10,9 +10,7 @@ import Bill from 'models/Bill';
import BillPayment from 'models/BillPayment';
import BillPaymentEntry from 'models/BillPaymentEntry';
import Currency from 'models/Currency';
-import Customer from 'models/Customer';
import Contact from 'models/Contact';
-import Vendor from 'models/Vendor';
import ExchangeRate from 'models/ExchangeRate';
import Expense from 'models/Expense';
import ExpenseCategory from 'models/ExpenseCategory';
@@ -49,8 +47,6 @@ export default (knex) => {
BillPayment,
BillPaymentEntry,
Currency,
- Customer,
- Vendor,
ExchangeRate,
Expense,
ExpenseCategory,
diff --git a/server/src/locales/en.json b/server/src/locales/en.json
index 48c4c5fdc..13a8df5e2 100644
--- a/server/src/locales/en.json
+++ b/server/src/locales/en.json
@@ -1,4 +1,33 @@
{
"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",
}
\ No newline at end of file
diff --git a/server/src/models/Account.js b/server/src/models/Account.js
index a70b0623a..6fa7625ac 100644
--- a/server/src/models/Account.js
+++ b/server/src/models/Account.js
@@ -8,6 +8,7 @@ import {
} from 'lib/ViewRolesBuilder';
import { flatToNestedArray } from 'utils';
import DependencyGraph from 'lib/DependencyGraph';
+import TenantManagerSubscriber from 'subscribers/tenantManager';
export default class Account extends TenantModel {
/**
@@ -24,6 +25,13 @@ export default class Account extends TenantModel {
return ['createdAt', 'updatedAt'];
}
+ /**
+ *
+ */
+ static get resourceable() {
+ return true;
+ }
+
/**
* Model modifiers.
*/
@@ -106,4 +114,55 @@ export default class Account extends TenantModel {
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'
+ },
+ };
+ }
}
diff --git a/server/src/models/Bill.js b/server/src/models/Bill.js
index bace2321a..2e7429492 100644
--- a/server/src/models/Bill.js
+++ b/server/src/models/Bill.js
@@ -36,17 +36,20 @@ export default class Bill extends TenantModel {
* Relationship mapping.
*/
static get relationMappings() {
- const Vendor = require('models/Vendor');
+ const Contact = require('models/Contact');
const ItemEntry = require('models/ItemEntry');
return {
vendor: {
relation: Model.BelongsToOneRelation,
- modelClass: Vendor.default,
+ modelClass: Contact.default,
join: {
from: 'bills.vendorId',
- to: 'vendors.id',
+ to: 'contacts.id',
},
+ filter(query) {
+ query.where('contact_type', 'Vendor');
+ }
},
entries: {
diff --git a/server/src/models/BillPayment.js b/server/src/models/BillPayment.js
index 71f598615..282722a35 100644
--- a/server/src/models/BillPayment.js
+++ b/server/src/models/BillPayment.js
@@ -22,7 +22,7 @@ export default class BillPayment extends TenantModel {
static get relationMappings() {
const BillPaymentEntry = require('models/BillPaymentEntry');
const AccountTransaction = require('models/AccountTransaction');
- const Vendor = require('models/Vendor');
+ const Contact = require('models/Contact');
const Account = require('models/Account');
return {
@@ -37,11 +37,14 @@ export default class BillPayment extends TenantModel {
vendor: {
relation: Model.BelongsToOneRelation,
- modelClass: Vendor.default,
+ modelClass: Contact.default,
join: {
from: 'bills_payments.vendorId',
- to: 'vendors.id',
+ to: 'contacts.id',
},
+ filter(query) {
+ query.where('contact_type', 'Vendor');
+ }
},
paymentAccount: {
diff --git a/server/src/models/Customer.js b/server/src/models/Customer.js
deleted file mode 100644
index 5d0590667..000000000
--- a/server/src/models/Customer.js
+++ /dev/null
@@ -1,81 +0,0 @@
-import { Model } from 'objection';
-import TenantModel from 'models/TenantModel';
-
-export default class Customer extends TenantModel {
- /**
- * Table name
- */
- static get tableName() {
- return 'customers';
- }
-
- /**
- * Model timestamps.
- */
- get timestamps() {
- return ['createdAt', 'updatedAt'];
- }
-
- /**
- * Model modifiers.
- */
- static get modifiers() {
- return {
- filterCustomerIds(query, customerIds) {
- query.whereIn('id', customerIds);
- },
- };
- }
-
- /**
- * Change vendor balance.
- * @param {Integer} customerId
- * @param {Numeric} amount
- */
- static async changeBalance(customerId, amount) {
- const changeMethod = (amount > 0) ? 'increment' : 'decrement';
-
- return this.query()
- .where('id', customerId)
- [changeMethod]('balance', Math.abs(amount));
- }
-
- /**
- * Increment the given customer balance.
- * @param {Integer} customerId
- * @param {Integer} amount
- */
- static async incrementBalance(customerId, amount) {
- return this.query()
- .where('id', customerId)
- .increment('balance', amount);
- }
-
- /**
- * Decrement the given customer balance.
- * @param {integer} customerId -
- * @param {integer} amount -
- */
- static async decrementBalance(customerId, amount) {
- await this.query()
- .where('id', customerId)
- .decrement('balance', amount);
- }
-
- static changeDiffBalance(customerId, oldCustomerId, amount, oldAmount) {
- const diffAmount = amount - oldAmount;
- const asyncOpers = [];
-
- if (customerId != oldCustomerId) {
- const oldCustomerOper = this.changeBalance(oldCustomerId, (oldAmount * -1));
- const customerOper = this.changeBalance(customerId, amount);
-
- asyncOpers.push(customerOper);
- asyncOpers.push(oldCustomerOper);
- } else {
- const balanceChangeOper = this.changeBalance(customerId, diffAmount);
- asyncOpers.push(balanceChangeOper);
- }
- return Promise.all(asyncOpers);
- }
-}
diff --git a/server/src/models/Expense.js b/server/src/models/Expense.js
index c422a6065..eb1ae2c01 100644
--- a/server/src/models/Expense.js
+++ b/server/src/models/Expense.js
@@ -1,6 +1,7 @@
import { Model } from 'objection';
import TenantModel from 'models/TenantModel';
import { viewRolesBuilder } from 'lib/ViewRolesBuilder';
+import Media from './Media';
export default class Expense extends TenantModel {
/**
@@ -24,6 +25,11 @@ export default class Expense extends TenantModel {
return ['createdAt', 'updatedAt'];
}
+
+ static get media () {
+ return true;
+ }
+
/**
* Model modifiers.
*/
@@ -55,14 +61,9 @@ export default class Expense extends TenantModel {
query.where('payment_account_id', accountId);
}
},
-
viewRolesBuilder(query, conditionals, expression) {
viewRolesBuilder(conditionals, expression)(query);
},
-
- orderBy(query) {
-
- }
};
}
@@ -72,7 +73,7 @@ export default class Expense extends TenantModel {
static get relationMappings() {
const Account = require('models/Account');
const ExpenseCategory = require('models/ExpenseCategory');
- const SystemUser = require('system/models/SystemUser');
+ const Media = require('models/Media');
return {
paymentAccount: {
@@ -91,14 +92,59 @@ export default class Expense extends TenantModel {
to: 'expense_transaction_categories.expenseId',
},
},
- user: {
- relation: Model.BelongsToOneRelation,
- modelClass: SystemUser.default,
+ media: {
+ relation: Model.ManyToManyRelation,
+ modelClass: Media.default,
join: {
- from: 'expenses_transactions.userId',
- to: 'users.id',
+ from: 'expenses_transactions.id',
+ through: {
+ from: 'media_links.model_id',
+ to: 'media_links.media_id',
+ },
+ to: 'media.id',
+ },
+ filter(query) {
+ query.where('model_name', 'Expense');
}
- }
+ },
+ };
+ }
+
+ /**
+ * 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: {
+ column: 'user_id',
+ relation: 'users.id',
+ relationColumn: 'users.id',
+ },
+ created_at: {
+ column: 'created_at',
+ },
};
}
}
diff --git a/server/src/models/Item.js b/server/src/models/Item.js
index ea500c8cc..739342c66 100644
--- a/server/src/models/Item.js
+++ b/server/src/models/Item.js
@@ -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',
+ }
+ };
+ }
}
diff --git a/server/src/models/ItemCategory.js b/server/src/models/ItemCategory.js
index 26e7f2684..48b763efa 100644
--- a/server/src/models/ItemCategory.js
+++ b/server/src/models/ItemCategory.js
@@ -10,6 +10,10 @@ export default class ItemCategory extends TenantModel {
return 'items_categories';
}
+ static get resourceable() {
+ return true;
+ }
+
/**
* 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',
+ },
+ };
+ }
}
diff --git a/server/src/models/ManualJournal.js b/server/src/models/ManualJournal.js
index 71c285825..d3f0bf408 100644
--- a/server/src/models/ManualJournal.js
+++ b/server/src/models/ManualJournal.js
@@ -46,8 +46,48 @@ export default class ManualJournal extends TenantModel {
to: 'media_links.media_id',
},
to: 'media.id',
+ },
+ filter(query) {
+ query.where('model_name', 'ManualJournal');
}
}
};
}
+
+ /**
+ * 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',
+ },
+ };
+ }
}
diff --git a/server/src/models/Media.js b/server/src/models/Media.js
index acf421451..aab3aa227 100644
--- a/server/src/models/Media.js
+++ b/server/src/models/Media.js
@@ -1,3 +1,4 @@
+import { Model } from 'objection';
import TenantModel from 'models/TenantModel';
export default class Media extends TenantModel {
@@ -7,4 +8,29 @@ export default class Media extends TenantModel {
static get tableName() {
return 'media';
}
+
+ /**
+ * Model timestamps.
+ */
+ get timestamps() {
+ return ['createdAt', 'updatedAt'];
+ }
+
+ /**
+ * Relationship mapping.
+ */
+ static get relationMappings() {
+ const MediaLink = require('models/MediaLink');
+
+ return {
+ links: {
+ relation: Model.HasManyRelation,
+ modelClass: MediaLink.default,
+ join: {
+ from: 'media.id',
+ to: 'media_links.media_id',
+ },
+ },
+ };
+ }
}
diff --git a/server/src/models/PaymentReceive.js b/server/src/models/PaymentReceive.js
index d992978a9..db1df4b0a 100644
--- a/server/src/models/PaymentReceive.js
+++ b/server/src/models/PaymentReceive.js
@@ -22,17 +22,20 @@ export default class PaymentReceive extends TenantModel {
static get relationMappings() {
const PaymentReceiveEntry = require('models/PaymentReceiveEntry');
const AccountTransaction = require('models/AccountTransaction');
- const Customer = require('models/Customer');
+ const Contact = require('models/Contact');
const Account = require('models/Account');
return {
customer: {
relation: Model.BelongsToOneRelation,
- modelClass: Customer.default,
+ modelClass: Contact.default,
join: {
from: 'payment_receives.customerId',
- to: 'customers.id',
+ to: 'contacts.id',
},
+ filter(query) {
+ query.where('contact_type', 'Customer');
+ }
},
depositAccount: {
diff --git a/server/src/models/ResourcableModel.js b/server/src/models/ResourcableModel.js
new file mode 100644
index 000000000..289c2dfa3
--- /dev/null
+++ b/server/src/models/ResourcableModel.js
@@ -0,0 +1,8 @@
+
+
+export default class ResourceableModel {
+
+ static get resourceable() {
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/server/src/models/SaleEstimate.js b/server/src/models/SaleEstimate.js
index fcbe46cb0..375682193 100644
--- a/server/src/models/SaleEstimate.js
+++ b/server/src/models/SaleEstimate.js
@@ -21,16 +21,19 @@ export default class SaleEstimate extends TenantModel {
*/
static get relationMappings() {
const ItemEntry = require('models/ItemEntry');
- const Customer = require('models/Customer');
+ const Contact = require('models/Contact');
return {
customer: {
relation: Model.BelongsToOneRelation,
- modelClass: Customer.default,
+ modelClass: Contact.default,
join: {
from: 'sales_estimates.customerId',
- to: 'customers.id',
+ to: 'contacts.id',
},
+ filter(query) {
+ query.where('contact_type', 'Customer');
+ }
},
entries: {
diff --git a/server/src/models/SaleInvoice.js b/server/src/models/SaleInvoice.js
index 0fae868fa..4070dbcf9 100644
--- a/server/src/models/SaleInvoice.js
+++ b/server/src/models/SaleInvoice.js
@@ -57,7 +57,7 @@ export default class SaleInvoice extends TenantModel {
static get relationMappings() {
const AccountTransaction = require('models/AccountTransaction');
const ItemEntry = require('models/ItemEntry');
- const Customer = require('models/Customer');
+ const Contact = require('models/Contact');
const InventoryCostLotTracker = require('models/InventoryCostLotTracker');
return {
@@ -75,11 +75,14 @@ export default class SaleInvoice extends TenantModel {
customer: {
relation: Model.BelongsToOneRelation,
- modelClass: Customer.default,
+ modelClass: Contact.default,
join: {
from: 'sales_invoices.customerId',
- to: 'customers.id',
+ to: 'contacts.id',
},
+ filter(query) {
+ query.where('contact_type', 'Customer');
+ }
},
transactions: {
diff --git a/server/src/models/SaleReceipt.js b/server/src/models/SaleReceipt.js
index 9b05cda29..4fe84b9f0 100644
--- a/server/src/models/SaleReceipt.js
+++ b/server/src/models/SaleReceipt.js
@@ -20,7 +20,7 @@ export default class SaleReceipt extends TenantModel {
* Relationship mapping.
*/
static get relationMappings() {
- const Customer = require('models/Customer');
+ const Contact = require('models/Contact');
const Account = require('models/Account');
const AccountTransaction = require('models/AccountTransaction');
const ItemEntry = require('models/ItemEntry');
@@ -28,11 +28,14 @@ export default class SaleReceipt extends TenantModel {
return {
customer: {
relation: Model.BelongsToOneRelation,
- modelClass: Customer.default,
+ modelClass: Contact.default,
join: {
from: 'sales_receipts.customerId',
- to: 'customers.id',
+ to: 'contacts.id',
},
+ filter(query) {
+ query.where('contact_type', 'Customer');
+ }
},
depositAccount: {
diff --git a/server/src/models/Vendor.js b/server/src/models/Vendor.js
deleted file mode 100644
index 636121624..000000000
--- a/server/src/models/Vendor.js
+++ /dev/null
@@ -1,61 +0,0 @@
-import { Model } from 'objection';
-import TenantModel from 'models/TenantModel';
-
-export default class Vendor extends TenantModel {
- /**
- * Table name
- */
- static get tableName() {
- return 'vendors';
- }
-
- /**
- * Model timestamps.
- */
- get timestamps() {
- return ['createdAt', 'updatedAt'];
- }
-
- /**
- * Changes the vendor balance.
- * @param {Integer} customerId
- * @param {Number} amount
- * @return {Promise}
- */
- static async changeBalance(vendorId, amount) {
- const changeMethod = amount > 0 ? 'increment' : 'decrement';
-
- return this.query()
- .where('id', vendorId)
- [changeMethod]('balance', Math.abs(amount));
- }
-
- /**
- *
- * @param {number} vendorId - Specific vendor id.
- * @param {number} oldVendorId - The given old vendor id.
- * @param {number} amount - The new change amount.
- * @param {number} oldAmount - The old stored amount.
- */
- static changeDiffBalance(vendorId, oldVendorId, amount, oldAmount) {
- const diffAmount = (amount - oldAmount);
- const asyncOpers = [];
-
- if (vendorId != oldVendorId) {
- const oldVendorOper = Vendor.changeBalance(
- oldVendorId,
- (oldAmount * -1)
- );
- const vendorOper = Vendor.changeBalance(
- vendorId,
- amount,
- );
- asyncOpers.push(vendorOper);
- asyncOpers.push(oldVendorOper);
- } else {
- const balanceChangeOper = Vendor.changeBalance(vendorId, diffAmount);
- asyncOpers.push(balanceChangeOper);
- }
- return Promise.all(asyncOpers);
- }
-}
diff --git a/server/src/models/index.js b/server/src/models/index.js
index bd91c577d..a980b2156 100644
--- a/server/src/models/index.js
+++ b/server/src/models/index.js
@@ -1,5 +1,3 @@
-import Customer from './Customer';
-import Vendor from './Vendor';
import Option from './Option';
import SaleEstimate from './SaleEstimate';
import SaleEstimateEntry from './SaleEstimateEntry';
@@ -22,8 +20,6 @@ import AccountType from './AccountType';
import InventoryLotCostTracker from './InventoryCostLotTracker';
export {
- Customer,
- Vendor,
SaleEstimate,
SaleEstimateEntry,
SaleReceipt,
diff --git a/server/src/repositories/AccountRepository.ts b/server/src/repositories/AccountRepository.ts
index 2452cffe4..d8b0ba3c6 100644
--- a/server/src/repositories/AccountRepository.ts
+++ b/server/src/repositories/AccountRepository.ts
@@ -1,5 +1,6 @@
import TenantRepository from 'repositories/TenantRepository';
import { IAccount } from 'interfaces';
+import { Account } from 'models';
export default class AccountRepository extends TenantRepository {
models: any;
@@ -57,14 +58,89 @@ export default class AccountRepository extends TenantRepository {
/**
* Retrieve the account by the given id.
- * @param {number} id - Account id.
+ * @param {number} id - Account id.
* @return {IAccount}
*/
- getById(id: number): IAccount {
+ findById(id: number): IAccount {
const { Account } = this.models;
return this.cache.get(`accounts.id.${id}`, () => {
return Account.query().findById(id);
});
}
+ /**
+ * Retrieve accounts by the given ids.
+ * @param {number[]} ids -
+ * @return {IAccount[]}
+ */
+ findByIds(accountsIds: number[]) {
+ const { Account } = this.models;
+ return Account.query().whereIn('id', accountsIds);
+ }
+
+ /**
+ * Activate the given account.
+ * @param {number} accountId -
+ * @return {void}
+ */
+ async activate(accountId: number): Promise {
+ const { Account } = this.models;
+ await Account.query().findById(accountId).patch({ active: 1 })
+ this.flushCache();
+ }
+
+ /**
+ * Inserts a new accounts to the storage.
+ * @param {IAccount} account
+ */
+ async insert(accountInput: IAccount): Promise {
+ const { Account } = this.models;
+ const account = await Account.query().insertAndFetch({ ...accountInput });
+ this.flushCache();
+
+ return account;
+ }
+
+ /**
+ * Updates account of the given account.
+ * @param {number} accountId - Account id.
+ * @param {IAccount} account
+ * @return {void}
+ */
+ async edit(accountId: number, account: IAccount): Promise {
+ const { Account } = this.models;
+ await Account.query().findById(accountId).patch({ ...account });
+ this.flushCache();
+ }
+
+ /**
+ * Deletes the given account by id.
+ * @param {number} accountId - Account id.
+ */
+ async deleteById(accountId: number): Promise {
+ const { Account } = this.models;
+ await Account.query().deleteById(accountId);
+ this.flushCache();
+ }
+
+ /**
+ * Changes account balance.
+ * @param {number} accountId
+ * @param {number} amount
+ * @return {Promise}
+ */
+ async balanceChange(accountId: number, amount: number): Promise {
+ const { Account } = this.models;
+ const method: string = (amount < 0) ? 'decrement' : 'increment';
+
+ await Account.query().where('id', accountId)[method]('amount', amount);
+ this.flushCache();
+ }
+
+ /**
+ * Flush repository cache.
+ */
+ flushCache(): void {
+ this.cache.delStartWith('accounts');
+ }
}
\ No newline at end of file
diff --git a/server/src/repositories/AccountTypeRepository.ts b/server/src/repositories/AccountTypeRepository.ts
index 7b273076e..21c93ead5 100644
--- a/server/src/repositories/AccountTypeRepository.ts
+++ b/server/src/repositories/AccountTypeRepository.ts
@@ -76,4 +76,11 @@ export default class AccountTypeRepository extends TenantRepository {
return AccountType.query().where('root_type', rootType);
});
}
+
+ /**
+ * Flush repository cache.
+ */
+ flushCache() {
+ this.cache.delStartWith('accountType');
+ }
}
\ No newline at end of file
diff --git a/server/src/repositories/ContactRepository.ts b/server/src/repositories/ContactRepository.ts
index b25942ec4..712e357e1 100644
--- a/server/src/repositories/ContactRepository.ts
+++ b/server/src/repositories/ContactRepository.ts
@@ -1,4 +1,6 @@
import TenantRepository from 'repositories/TenantRepository';
+import { IContact } from 'interfaces';
+import Contact from 'models/Contact';
export default class ContactRepository extends TenantRepository {
cache: any;
@@ -17,21 +19,70 @@ export default class ContactRepository extends TenantRepository {
this.cache = this.tenancy.cache(tenantId);
}
- findById(contactId: number) {
+ /**
+ * Retrieve the given contact model.
+ * @param {number} contactId
+ */
+ findById(contactId: number): IContact {
const { Contact } = this.models;
- return this.cache.get(`contact.id.${contactId}`, () => {
+ return this.cache.get(`contacts.id.${contactId}`, () => {
return Contact.query().findById(contactId);
})
}
- findByIds(contactIds: number[]) {
+ /**
+ * Retrieve the given contacts model.
+ * @param {number[]} contactIds - Contacts ids.
+ */
+ findByIds(contactIds: number[]): IContact[] {
const { Contact } = this.models;
- return this.cache.get(`contact.ids.${contactIds.join(',')}`, () => {
+ return this.cache.get(`contacts.ids.${contactIds.join(',')}`, () => {
return Contact.query().whereIn('id', contactIds);
});
}
- insert(contact) {
+ /**
+ * Inserts a new contact model.
+ * @param contact
+ */
+ async insert(contact) {
+ await Contact.query().insert({ ...contact })
+ this.flushCache();
+ }
+ /**
+ * Updates the contact details.
+ * @param {number} contactId - Contact id.
+ * @param {IContact} contact - Contact input.
+ */
+ async update(contactId: number, contact: IContact) {
+ await Contact.query().findById(contactId).patch({ ...contact });
+ this.flushCache();
+ }
+
+ /**
+ * Deletes contact of the given id.
+ * @param {number} contactId -
+ * @return {Promise}
+ */
+ async deleteById(contactId: number): Promise {
+ await Contact.query().where('id', contactId).delete();
+ this.flushCache();
+ }
+
+ /**
+ * Deletes contacts in bulk.
+ * @param {number[]} contactsIds
+ */
+ async bulkDelete(contactsIds: number[]) {
+ await Contact.query().whereIn('id', contactsIds);
+ this.flushCache();
+ }
+
+ /**
+ * Flush contact repository cache.
+ */
+ flushCache() {
+ this.cache.delStartWith(`contacts`);
}
}
\ No newline at end of file
diff --git a/server/src/repositories/CustomerRepository.js b/server/src/repositories/CustomerRepository.js
deleted file mode 100644
index 166476a45..000000000
--- a/server/src/repositories/CustomerRepository.js
+++ /dev/null
@@ -1,26 +0,0 @@
-import { Customer } from 'models';
-
-export default class CustomerRepository {
-
- static changeDiffBalance(customerId, oldCustomerId, amount, oldAmount) {
- const diffAmount = amount - oldAmount;
- const asyncOpers = [];
-
- if (customerId != oldCustomerId) {
- const oldCustomerOper = Customer.changeBalance(
- oldCustomerId,
- (oldAmount * -1)
- );
- const customerOper = Customer.changeBalance(
- customerId,
- amount,
- );
- asyncOpers.push(customerOper);
- asyncOpers.push(oldCustomerOper);
- } else {
- const balanceChangeOper = Customer.changeBalance(customerId, diffAmount);
- asyncOpers.push(balanceChangeOper);
- }
- return Promise.all(asyncOpers);
- }
-}
diff --git a/server/src/repositories/CustomerRepository.ts b/server/src/repositories/CustomerRepository.ts
index ce019acc6..12c41cd93 100644
--- a/server/src/repositories/CustomerRepository.ts
+++ b/server/src/repositories/CustomerRepository.ts
@@ -17,7 +17,7 @@ export default class CustomerRepository extends TenantRepository {
/**
* Retrieve customer details of the given id.
- * @param {number} customerId -
+ * @param {number} customerId - Customer id.
*/
getById(customerId: number) {
const { Contact } = this.models;
diff --git a/server/src/repositories/ExpenseRepository.ts b/server/src/repositories/ExpenseRepository.ts
index e83836ee0..a25c308e0 100644
--- a/server/src/repositories/ExpenseRepository.ts
+++ b/server/src/repositories/ExpenseRepository.ts
@@ -7,6 +7,10 @@ export default class ExpenseRepository extends TenantRepository {
repositories: any;
cache: any;
+ /**
+ * Constructor method.
+ * @param {number} tenantId
+ */
constructor(tenantId: number) {
super(tenantId);
@@ -14,38 +18,98 @@ export default class ExpenseRepository extends TenantRepository {
this.cache = this.tenancy.cache(tenantId);
}
+ /**
+ * Retrieve the given expense by id.
+ * @param {number} expenseId
+ * @return {Promise}
+ */
getById(expenseId: number) {
const { Expense } = this.models;
return this.cache.get(`expense.id.${expenseId}`, () => {
- return Expense.query().findById(expenseId);
- })
- }
-
- create(expense: IExpense) {
- const { Expense } = this.models;
- return Expense.query().insert({ ...expense });
- }
-
- update(expenseId: number, expense: IExpense) {
- const { Expense } = this.models;
- return Expense.query().patchAndFetchById(expenseId, { ...expense });
- }
-
- publish(expenseId: number) {
- const { Expense } = this.models;
-
- return Expense.query().findById(expenseId).patch({
- publishedAt: moment().toMySqlDateTime(),
+ return Expense.query().findById(expenseId).withGraphFetched('categories');
});
}
- delete(expenseId: number) {
+ /**
+ * Inserts a new expense object.
+ * @param {IExpense} expense -
+ */
+ async create(expenseInput: IExpense): Promise {
const { Expense } = this.models;
- return Expense.query().findById(expenseId).delete();
+ const expense = await Expense.query().insertGraph({ ...expenseInput });
+ this.flushCache();
+
+ return expense;
}
- bulkDelete(expensesIds: number[]) {
+ /**
+ * Updates the given expense details.
+ * @param {number} expenseId
+ * @param {IExpense} expense
+ */
+ async update(expenseId: number, expense: IExpense) {
const { Expense } = this.models;
- return Expense.query().whereIn('id', expensesIds).delete();
+
+ await Expense.query().findById(expenseId).patch({ ...expense });
+ this.flushCache();
+ }
+
+ /**
+ * Publish the given expense.
+ * @param {number} expenseId
+ */
+ async publish(expenseId: number): Promise {
+ const { Expense } = this.models;
+
+ await Expense.query().findById(expenseId).patch({
+ publishedAt: moment().toMySqlDateTime(),
+ });
+ this.flushCache();
+ }
+
+ /**
+ * Deletes the given expense.
+ * @param {number} expenseId
+ */
+ async delete(expenseId: number): Promise {
+ const { Expense, ExpenseCategory } = this.models;
+
+ await ExpenseCategory.query().where('expense_id', expenseId).delete();
+ await Expense.query().where('id', expenseId).delete();
+
+ this.flushCache();
+ }
+
+ /**
+ * Deletes expenses in bulk.
+ * @param {number[]} expensesIds
+ */
+ async bulkDelete(expensesIds: number[]): Promise {
+ const { Expense } = this.models;
+
+ await Expense.query().whereIn('expense_id', expensesIds).delete();
+ await Expense.query().whereIn('id', expensesIds).delete();
+
+ this.flushCache();
+ }
+
+ /**
+ * Publishes the given expenses in bulk.
+ * @param {number[]} expensesIds
+ * @return {Promise}
+ */
+ async bulkPublish(expensesIds: number): Promise {
+ const { Expense } = this.models;
+ await Expense.query().whereIn('id', expensesIds).patch({
+ publishedAt: moment().toMySqlDateTime(),
+ });
+ this.flushCache();
+ }
+
+ /**
+ * Flushes repository cache.
+ */
+ flushCache() {
+ this.cache.delStartWith(`expense`);
}
}
\ No newline at end of file
diff --git a/server/src/repositories/VendorRepository.ts b/server/src/repositories/VendorRepository.ts
index a02443a0b..264c158c8 100644
--- a/server/src/repositories/VendorRepository.ts
+++ b/server/src/repositories/VendorRepository.ts
@@ -1,3 +1,4 @@
+import { IVendor } from "interfaces";
import TenantRepository from "./TenantRepository";
@@ -18,7 +19,7 @@ export default class VendorRepository extends TenantRepository {
/**
* Retrieve the bill that associated to the given vendor id.
- * @param {number} vendorId
+ * @param {number} vendorId - Vendor id.
*/
getBills(vendorId: number) {
const { Bill } = this.models;
@@ -29,16 +30,17 @@ export default class VendorRepository extends TenantRepository {
}
/**
- *
+ * Retrieve all the given vendors.
* @param {numner[]} vendorsIds
+ * @return {IVendor}
*/
- vendors(vendorsIds: number[]) {
+ vendors(vendorsIds: number[]): IVendor[] {
const { Contact } = this.models;
return Contact.query().modifier('vendor').whereIn('id', vendorsIds);
}
/**
- *
+ * Retrieve vendors with associated bills.
* @param {number[]} vendorIds
*/
vendorsWithBills(vendorIds: number[]) {
diff --git a/server/src/repositories/ViewRepository.ts b/server/src/repositories/ViewRepository.ts
index ab4b0b8d9..0d124d24b 100644
--- a/server/src/repositories/ViewRepository.ts
+++ b/server/src/repositories/ViewRepository.ts
@@ -1,3 +1,4 @@
+import { IView } from 'interfaces';
import { View } from 'models';
import TenantRepository from 'repositories/TenantRepository';
@@ -17,7 +18,6 @@ export default class ViewRepository extends TenantRepository {
this.models = this.tenancy.models(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) {
const { View } = this.models;
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.
*/
- allByResource() {
- const resourceId = 1;
- return this.cache.get(`customView.resource.id.${resourceId}`, () => {
- return View.query().where('resource_id', resourceId);
+ allByResource(resourceModel: string) {
+ const { View } = this.models;
+ return this.cache.get(`customView.resourceModel.${resourceModel}`, () => {
+ 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 {
+ const insertedView = await View.query().insertGraph({ ...view });
+ this.flushCache();
+
+ return insertedView;
+ }
+
+
+
+ /**
+ * Flushes repository cache.
+ */
+ flushCache() {
+ this.cache.delStartWith('customView');
+ }
}
\ No newline at end of file
diff --git a/server/src/services/Accounting/JournalCommands.ts b/server/src/services/Accounting/JournalCommands.ts
index 9e07d306b..322180adb 100644
--- a/server/src/services/Accounting/JournalCommands.ts
+++ b/server/src/services/Accounting/JournalCommands.ts
@@ -2,9 +2,12 @@ import { sumBy, chain } from 'lodash';
import JournalPoster from "./JournalPoster";
import JournalEntry from "./JournalEntry";
import { AccountTransaction } from 'models';
-import { IInventoryTransaction, IManualJournal } from 'interfaces';
-import AccountsService from '../Accounts/AccountsService';
-import { IInventoryTransaction, IInventoryTransaction } from '../../interfaces';
+import {
+ IInventoryTransaction,
+ IManualJournal,
+ IExpense,
+ IExpenseCategory,
+} from 'interfaces';
interface IInventoryCostEntity {
date: Date,
@@ -120,6 +123,38 @@ export default class JournalCommands{
this.journal.credit(creditEntry);
}
+ /**
+ * Writes journal entries of expense model object.
+ * @param {IExpense} expense
+ */
+ expense(expense: IExpense) {
+ const mixinEntry = {
+ referenceType: 'Expense',
+ referenceId: expense.id,
+ date: expense.paymentDate,
+ userId: expense.userId,
+ draft: !expense.publishedAt,
+ };
+ const paymentJournalEntry = new JournalEntry({
+ credit: expense.totalAmount,
+ account: expense.paymentAccountId,
+ index: 1,
+ ...mixinEntry,
+ });
+ this.journal.credit(paymentJournalEntry);
+
+ expense.categories.forEach((category: IExpenseCategory, index) => {
+ const expenseJournalEntry = new JournalEntry({
+ account: category.expenseAccountId,
+ debit: category.amount,
+ note: category.description,
+ ...mixinEntry,
+ index: index + 2,
+ });
+ this.journal.debit(expenseJournalEntry);
+ });
+ }
+
/**
*
* @param {number|number[]} referenceId
diff --git a/server/src/services/Accounts/AccountsService.ts b/server/src/services/Accounts/AccountsService.ts
index 825835b7e..2c5635073 100644
--- a/server/src/services/Accounts/AccountsService.ts
+++ b/server/src/services/Accounts/AccountsService.ts
@@ -1,10 +1,18 @@
import { Inject, Service } from 'typedi';
+import { difference } from 'lodash';
import { kebabCase } from 'lodash'
import TenancyService from 'services/Tenancy/TenancyService';
import { ServiceError } from 'exceptions';
-import { IAccountDTO, IAccount, IAccountsFilter } from 'interfaces';
-import { difference } from 'lodash';
+import { IAccountDTO, IAccount, IAccountsFilter, IFilterMeta } from 'interfaces';
+import {
+ EventDispatcher,
+ EventDispatcherInterface,
+} from 'decorators/eventDispatcher';
import DynamicListingService from 'services/DynamicListing/DynamicListService';
+import events from 'subscribers/events';
+import JournalPoster from 'services/Accounting/JournalPoster';
+import { Account } from 'models';
+import AccountRepository from 'repositories/AccountRepository';
@Service()
export default class AccountsService {
@@ -17,6 +25,9 @@ export default class AccountsService {
@Inject('logger')
logger: any;
+ @EventDispatcher()
+ eventDispatcher: EventDispatcherInterface;
+
/**
* Retrieve account type or throws service error.
* @param {number} tenantId -
@@ -104,10 +115,10 @@ export default class AccountsService {
* @return {IAccount}
*/
private async getAccountOrThrowError(tenantId: number, accountId: number) {
- const { Account } = this.tenancy.models(tenantId);
+ const { accountRepository } = this.tenancy.repositories(tenantId);
this.logger.info('[accounts] validating the account existance.', { tenantId, accountId });
- const account = await Account.query().findById(accountId);
+ const account = await accountRepository.findById(accountId);
if (!account) {
this.logger.info('[accounts] the given account not found.', { accountId });
@@ -159,8 +170,8 @@ export default class AccountsService {
* @returns {IAccount}
*/
public async newAccount(tenantId: number, accountDTO: IAccountDTO) {
- const { Account } = this.tenancy.models(tenantId);
-
+ const { accountRepository } = this.tenancy.repositories(tenantId);
+
// Validate account name uniquiness.
await this.validateAccountNameUniquiness(tenantId, accountDTO.name);
@@ -176,11 +187,15 @@ export default class AccountsService {
);
this.throwErrorIfParentHasDiffType(accountDTO, parentAccount);
}
- const account = await Account.query().insertAndFetch({
+ const account = await accountRepository.insert({
...accountDTO,
slug: kebabCase(accountDTO.name),
});
this.logger.info('[account] account created successfully.', { account, accountDTO });
+
+ // Triggers `onAccountCreated` event.
+ this.eventDispatcher.dispatch(events.accounts.onCreated);
+
return account;
}
@@ -191,7 +206,7 @@ export default class AccountsService {
* @param {IAccountDTO} accountDTO
*/
public async editAccount(tenantId: number, accountId: number, accountDTO: IAccountDTO) {
- const { Account } = this.tenancy.models(tenantId);
+ const { accountRepository } = this.tenancy.repositories(tenantId);
const oldAccount = await this.getAccountOrThrowError(tenantId, accountId);
// Validate account name uniquiness.
@@ -214,12 +229,13 @@ export default class AccountsService {
this.throwErrorIfParentHasDiffType(accountDTO, parentAccount);
}
// Update the account on the storage.
- const account = await Account.query().patchAndFetchById(
- oldAccount.id, { ...accountDTO }
- );
+ const account = await accountRepository.edit(oldAccount.id, accountDTO);
this.logger.info('[account] account edited successfully.', {
account, accountDTO, tenantId
});
+ // Triggers `onAccountEdited` event.
+ this.eventDispatcher.dispatch(events.accounts.onEdited);
+
return account;
}
@@ -247,17 +263,6 @@ export default class AccountsService {
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.
* @param {IAccount} account
@@ -309,7 +314,7 @@ export default class AccountsService {
* @param {number} accountId
*/
public async deleteAccount(tenantId: number, accountId: number) {
- const { Account } = this.tenancy.models(tenantId);
+ const { accountRepository } = this.tenancy.repositories(tenantId);
const account = await this.getAccountOrThrowError(tenantId, accountId);
this.throwErrorIfAccountPredefined(account);
@@ -317,10 +322,13 @@ export default class AccountsService {
await this.throwErrorIfAccountHasChildren(tenantId, accountId);
await this.throwErrorIfAccountHasTransactions(tenantId, accountId);
- await Account.query().deleteById(account.id);
+ await accountRepository.deleteById(account.id);
this.logger.info('[account] account has been deleted successfully.', {
tenantId, accountId,
- })
+ });
+
+ // Triggers `onAccountDeleted` event.
+ this.eventDispatcher.dispatch(events.accounts.onDeleted);
}
/**
@@ -400,6 +408,9 @@ export default class AccountsService {
this.logger.info('[account] given accounts deleted in bulk successfully.', {
tenantId, accountsIds
});
+
+ // Triggers `onBulkDeleted` event.
+ this.eventDispatcher.dispatch(events.accounts.onBulkDeleted);
}
/**
@@ -418,6 +429,9 @@ export default class AccountsService {
active: activate ? 1 : 0,
});
this.logger.info('[account] accounts have been activated successfully.', { tenantId, accountsIds });
+
+ // Triggers `onAccountBulkActivated` event.
+ this.eventDispatcher.dispatch(events.accounts.onActivated);
}
/**
@@ -436,6 +450,9 @@ export default class AccountsService {
active: activate ? 1 : 0,
})
this.logger.info('[account] account have been activated successfully.', { tenantId, accountId });
+
+ // Triggers `onAccountActivated` event.
+ this.eventDispatcher.dispatch(events.accounts.onActivated);
}
/**
@@ -443,9 +460,11 @@ export default class AccountsService {
* @param {number} tenantId
* @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 dynamicList = await this.dynamicListService.dynamicList(tenantId, Account, filter);
this.logger.info('[accounts] trying to get accounts datatable list.', { tenantId, filter });
@@ -453,6 +472,60 @@ export default class AccountsService {
builder.withGraphFetched('type');
dynamicList.buildQuery()(builder);
});
- return accounts;
+
+ return {
+ accounts,
+ filterMeta: dynamicList.getResponseMeta(),
+ };
+ }
+
+ /**
+ * Closes the given account.
+ * -----------
+ * Precedures.
+ * -----------
+ * - Transfer the given account transactions to another account
+ * with the same root type.
+ * - Delete the given account.
+ * -------
+ * @param {number} tenantId -
+ * @param {number} accountId -
+ * @param {number} toAccountId -
+ * @param {boolean} deleteAfterClosing -
+ */
+ public async closeAccount(
+ tenantId: number,
+ accountId: number,
+ toAccountId: number,
+ deleteAfterClosing: boolean,
+ ) {
+ this.logger.info('[account] trying to close account.', { tenantId, accountId, toAccountId, deleteAfterClosing });
+
+ const { AccountTransaction } = this.tenancy.models(tenantId);
+ const { accountTypeRepository, accountRepository } = this.tenancy.repositories(tenantId);
+
+ const account = await this.getAccountOrThrowError(tenantId, accountId);
+ const toAccount = await this.getAccountOrThrowError(tenantId, toAccountId);
+
+ this.throwErrorIfAccountPredefined(account);
+
+ const accountType = await accountTypeRepository.getTypeMeta(account.accountTypeId);
+ const toAccountType = await accountTypeRepository.getTypeMeta(toAccount.accountTypeId);
+
+ if (accountType.rootType !== toAccountType.rootType) {
+ throw new ServiceError('close_account_and_to_account_not_same_type');
+ }
+ const updateAccountBalanceOper = await accountRepository.balanceChange(accountId, account.balance || 0);
+
+ // Move transactiosn operation.
+ const moveTransactionsOper = await AccountTransaction.query()
+ .where('account_id', accountId)
+ .patch({ accountId: toAccountId });
+
+ await Promise.all([ moveTransactionsOper, updateAccountBalanceOper ]);
+
+ if (deleteAfterClosing) {
+ await accountRepository.deleteById(accountId);
+ }
}
}
diff --git a/server/src/services/Authentication/index.ts b/server/src/services/Authentication/index.ts
index 90344d9cf..12ccab4ec 100644
--- a/server/src/services/Authentication/index.ts
+++ b/server/src/services/Authentication/index.ts
@@ -7,12 +7,13 @@ import {
EventDispatcher,
EventDispatcherInterface,
} from 'decorators/eventDispatcher';
-import { SystemUser, PasswordReset } from 'system/models';
+import { PasswordReset } from 'system/models';
import {
IRegisterDTO,
ITenant,
ISystemUser,
IPasswordReset,
+ IAuthenticationService,
} from 'interfaces';
import { hashPassword } from 'utils';
import { ServiceError, ServiceErrors } from 'exceptions';
@@ -134,7 +135,7 @@ export default class AuthenticationService implements IAuthenticationService {
const { systemUserRepository } = this.sysRepositories;
const registeredUser = await systemUserRepository.create({
- ...omit(registerDTO, 'country', 'organizationName'),
+ ...omit(registerDTO, 'country'),
active: true,
password: hashedPassword,
tenant_id: tenant.id,
diff --git a/server/src/services/Contacts/ContactsService.ts b/server/src/services/Contacts/ContactsService.ts
index 0577dd74f..6c98474ec 100644
--- a/server/src/services/Contacts/ContactsService.ts
+++ b/server/src/services/Contacts/ContactsService.ts
@@ -45,10 +45,10 @@ export default class ContactsService {
* @param {IContactDTO} contactDTO
*/
async newContact(tenantId: number, contactDTO: IContactNewDTO, contactService: TContactService) {
- const { Contact } = this.tenancy.models(tenantId);
+ const { contactRepository } = this.tenancy.repositories(tenantId);
this.logger.info('[contacts] trying to insert contact to the storage.', { tenantId, contactDTO });
- const contact = await Contact.query().insert({ contactService, ...contactDTO });
+ const contact = await contactRepository.insert({ contactService, ...contactDTO });
this.logger.info('[contacts] contact inserted successfully.', { tenantId, contact });
return contact;
@@ -77,11 +77,11 @@ export default class ContactsService {
* @return {Promise}
*/
async deleteContact(tenantId: number, contactId: number, contactService: TContactService) {
- const { Contact } = this.tenancy.models(tenantId);
+ const { contactRepository } = this.tenancy.repositories(tenantId);
const contact = await this.getContactByIdOrThrowError(tenantId, contactId, contactService);
this.logger.info('[contacts] trying to delete the given contact.', { tenantId, contactId });
- await Contact.query().findById(contactId).delete();
+ await contactRepository.deleteById(contactId);
}
/**
@@ -124,10 +124,10 @@ export default class ContactsService {
* @return {Promise}
*/
async deleteBulkContacts(tenantId: number, contactsIds: number[], contactService: TContactService) {
- const { Contact } = this.tenancy.models(tenantId);
+ const { contactRepository } = this.tenancy.repositories(tenantId);
this.getContactsOrThrowErrorNotFound(tenantId, contactsIds, contactService);
- await Contact.query().whereIn('id', contactsIds).delete();
+ await contactRepository.bulkDelete(contactsIds);
}
/**
diff --git a/server/src/services/Contacts/CustomersService.ts b/server/src/services/Contacts/CustomersService.ts
index 97f85c927..be63d6f93 100644
--- a/server/src/services/Contacts/CustomersService.ts
+++ b/server/src/services/Contacts/CustomersService.ts
@@ -6,10 +6,13 @@ import ContactsService from 'services/Contacts/ContactsService';
import {
ICustomerNewDTO,
ICustomerEditDTO,
+ ICustomer,
+ IPaginationMeta,
+ ICustomersFilter
} from 'interfaces';
import { ServiceError } from 'exceptions';
import TenancyService from 'services/Tenancy/TenancyService';
-import { ICustomer } from 'src/interfaces';
+import DynamicListingService from 'services/DynamicListing/DynamicListService';
@Service()
export default class CustomersService {
@@ -19,12 +22,15 @@ export default class CustomersService {
@Inject()
tenancy: TenancyService;
+ @Inject()
+ dynamicListService: DynamicListingService;
+
/**
* Converts customer to contact DTO.
* @param {ICustomerNewDTO|ICustomerEditDTO} customerDTO
* @returns {IContactDTO}
*/
- customerToContactDTO(customerDTO: ICustomerNewDTO|ICustomerEditDTO) {
+ private customerToContactDTO(customerDTO: ICustomerNewDTO | ICustomerEditDTO) {
return {
...omit(customerDTO, ['customerType']),
contactType: customerDTO.customerType,
@@ -39,7 +45,7 @@ export default class CustomersService {
* @param {ICustomerNewDTO} customerDTO
* @return {Promise}
*/
- async newCustomer(tenantId: number, customerDTO: ICustomerNewDTO) {
+ public async newCustomer(tenantId: number, customerDTO: ICustomerNewDTO) {
const contactDTO = this.customerToContactDTO(customerDTO)
const customer = await this.contactService.newContact(tenantId, contactDTO, 'customer');
@@ -59,7 +65,7 @@ export default class CustomersService {
* @param {number} tenantId
* @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);
return this.contactService.editContact(tenantId, customerId, contactDTO, 'customer');
}
@@ -70,7 +76,7 @@ export default class CustomersService {
* @param {number} customerId
* @return {Promise}
*/
- async deleteCustomer(tenantId: number, customerId: number) {
+ public async deleteCustomer(tenantId: number, customerId: number) {
const { Contact } = this.tenancy.models(tenantId);
await this.getCustomerByIdOrThrowError(tenantId, customerId);
@@ -88,10 +94,34 @@ export default class CustomersService {
* @param {number} tenantId
* @param {number} customerId
*/
- async getCustomer(tenantId: number, customerId: number) {
+ public async getCustomer(tenantId: number, customerId: number) {
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.
* @param {number} tenantId
diff --git a/server/src/services/CustomFields/ResourceCustomFieldRepository.js b/server/src/services/CustomFields/ResourceCustomFieldRepository.js
deleted file mode 100644
index 9c99dd48c..000000000
--- a/server/src/services/CustomFields/ResourceCustomFieldRepository.js
+++ /dev/null
@@ -1,154 +0,0 @@
-import Resource from 'models/Resource';
-import ResourceField from 'models/ResourceField';
-import ResourceFieldMetadata from 'models/ResourceFieldMetadata';
-import ResourceFieldMetadataCollection from 'collection/ResourceFieldMetadataCollection';
-
-export default class ResourceCustomFieldRepository {
- /**
- * Class constructor.
- */
- constructor(model) {
- if (typeof model === 'function') {
- this.resourceName = model.name;
- } else if (typeof model === 'string') {
- this.resourceName = model;
- }
- // Custom fields of the given resource.
- this.customFields = [];
- this.filledCustomFields = {};
-
- // metadata of custom fields of the given resource.
- this.fieldsMetadata = {};
- this.resource = {};
- }
-
- /**
- * Fetches metadata of custom fields of the given resource.
- * @param {Integer} id - Resource item id.
- */
- async fetchCustomFieldsMetadata(id) {
- if (typeof id === 'undefined') {
- throw new Error('Please define the resource item id.');
- }
- if (!this.resource) {
- throw new Error('Target resource model is not found.');
- }
- const metadata = await ResourceFieldMetadata.query()
- .where('resource_id', this.resource.id)
- .where('resource_item_id', id);
-
- this.fieldsMetadata[id] = metadata;
- }
-
- /**
- * Load resource.
- */
- async loadResource() {
- const resource = await Resource.query().where('name', this.resourceName).first();
-
- if (!resource) {
- throw new Error('There is no stored resource in the storage with the given model name.');
- }
- this.setResource(resource);
- }
-
- /**
- * Load metadata of the resource.
- */
- async loadResourceCustomFields() {
- if (typeof this.resource.id === 'undefined') {
- throw new Error('Please fetch resource details before fetch custom fields of the resource.');
- }
- const customFields = await ResourceField.query()
- .where('resource_id', this.resource.id)
- .modify('whereNotPredefined');
-
- this.setResourceCustomFields(customFields);
- }
-
- /**
- * Sets resource model.
- * @param {Resource} resource -
- */
- setResource(resource) {
- this.resource = resource;
- }
-
- /**
- * Sets resource custom fields collection.
- * @param {Array} customFields -
- */
- setResourceCustomFields(customFields) {
- this.customFields = customFields;
- }
-
- /**
- * Retrieve metadata of the resource custom fields.
- * @param {Integer} itemId -
- */
- getMetadata(itemId) {
- return this.fieldsMetadata[itemId] || this.fieldsMetadata;
- }
-
- /**
- * Fill metadata of the custom fields that associated to the resource.
- * @param {Inter} id - Resource item id.
- * @param {Array} attributes -
- */
- fillCustomFields(id, attributes) {
- if (typeof this.filledCustomFields[id] === 'undefined') {
- this.filledCustomFields[id] = [];
- }
- attributes.forEach((attr) => {
- this.filledCustomFields[id].push(attr);
-
- if (!this.fieldsMetadata[id]) {
- this.fieldsMetadata[id] = new ResourceFieldMetadataCollection();
- }
- this.fieldsMetadata[id].setMeta(attr.key, attr.value, {
- resource_id: this.resource.id,
- resource_item_id: id,
- });
- });
- }
-
- /**
- * Saves the instered, updated and deleted custom fields metadata.
- * @param {Integer} id - Optional resource item id.
- */
- async saveCustomFields(id) {
- if (id) {
- if (typeof this.fieldsMetadata[id] === 'undefined') {
- throw new Error('There is no resource item with the given id.');
- }
- await this.fieldsMetadata[id].saveMeta();
- } else {
- const opers = [];
- this.fieldsMetadata.forEach((metadata) => {
- const oper = metadata.saveMeta();
- opers.push(oper);
- });
- await Promise.all(opers);
- }
- }
-
- /**
- * Validates the exist custom fields.
- */
- validateExistCustomFields() {
-
- }
-
- toArray() {
- return this.fieldsMetadata.toArray();
- }
-
- async load() {
- await this.loadResource();
- await this.loadResourceCustomFields();
- }
-
- static forgeMetadataCollection() {
-
- }
-}
diff --git a/server/src/services/DynamicListing/DynamicListService.ts b/server/src/services/DynamicListing/DynamicListService.ts
index 54ee80db5..60fe3af38 100644
--- a/server/src/services/DynamicListing/DynamicListService.ts
+++ b/server/src/services/DynamicListing/DynamicListService.ts
@@ -1,6 +1,6 @@
import { Service, Inject } from "typedi";
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 {
DynamicFilter,
@@ -12,8 +12,13 @@ import {
validateFieldKeyExistance,
validateFilterRolesFieldsExistance,
} from 'lib/ViewRolesBuilder';
+import {
+ IDynamicListFilterDTO,
+ IFilterRole,
+ IDynamicListService,
+ IModel,
+} from 'interfaces';
import TenancyService from 'services/Tenancy/TenancyService';
-import { IDynamicListFilterDTO, IFilterRole, IDynamicListService } from 'interfaces';
const ERRORS = {
VIEW_NOT_FOUND: 'view_not_found',
@@ -32,11 +37,11 @@ export default class DynamicListService implements IDynamicListService {
* @param {number} viewId
* @return {Promise}
*/
- private async getCustomViewOrThrowError(tenantId: number, viewId: number) {
+ private async getCustomViewOrThrowError(tenantId: number, viewId: number, model: IModel) {
const { viewRepository } = this.tenancy.repositories(tenantId);
const view = await viewRepository.getById(viewId);
- if (!view || view.resourceModel !== 'Account') {
+ if (!view || view.resourceModel !== model.name) {
throw new ServiceError(ERRORS.VIEW_NOT_FOUND);
}
return view;
@@ -49,9 +54,9 @@ export default class DynamicListService implements IDynamicListService {
* @throws {ServiceError}
*/
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);
}
}
@@ -62,8 +67,10 @@ export default class DynamicListService implements IDynamicListService {
* @param {IFilterRole[]} filterRoles
* @throws {ServiceError}
*/
- private validateRolesFieldsExistance(model: any, filterRoles: IFilterRole[]) {
- const invalidFieldsKeys = validateFilterRolesFieldsExistance(model.tableName, filterRoles);
+ private validateRolesFieldsExistance(model: IModel, filterRoles: IFilterRole[]) {
+ const invalidFieldsKeys = validateFilterRolesFieldsExistance(model, filterRoles);
+
+ console.log(invalidFieldsKeys);
if (invalidFieldsKeys.length > 0) {
throw new ServiceError(ERRORS.FILTER_ROLES_FIELDS_NOT_FOUND);
@@ -96,23 +103,21 @@ export default class DynamicListService implements IDynamicListService {
* Dynamic listing.
* @param {number} tenantId
* @param {IModel} model
- * @param {IAccountsFilter} filter
+ * @param {IDynamicListFilterDTO} filter
*/
- async dynamicList(tenantId: number, model: any, filter: IDynamicListFilterDTO) {
- const { viewRoleRepository } = this.tenancy.repositories(tenantId);
- const dynamicFilter = new DynamicFilter(model.tableName);
+ public async dynamicList(tenantId: number, model: IModel, filter: IDynamicListFilterDTO) {
+ const dynamicFilter = new DynamicFilter(model);
// Custom view filter roles.
if (filter.customViewId) {
- const view = await this.getCustomViewOrThrowError(tenantId, filter.customViewId);
- const viewRoles = await viewRoleRepository.allByView(view.id);
+ const view = await this.getCustomViewOrThrowError(tenantId, filter.customViewId, model);
- const viewFilter = new DynamicFilterViews(viewRoles, view.rolesLogicExpression);
+ const viewFilter = new DynamicFilterViews(view);
dynamicFilter.setFilter(viewFilter);
}
// Sort by the given column.
if (filter.columnSortBy) {
- this.validateSortColumnExistance(model, filter.columnSortBy);;
+ this.validateSortColumnExistance(model, filter.columnSortBy);
const sortByFilter = new DynamicFilterSortBy(
filter.columnSortBy, filter.sortOrder
@@ -124,7 +129,7 @@ export default class DynamicListService implements IDynamicListService {
this.validateFilterRolesSchema(filter.filterRoles);
this.validateRolesFieldsExistance(model, filter.filterRoles);
- // Validate the accounts resource fields.
+ // Validate the model resource fields.
const filterRoles = new DynamicFilterFilterRoles(filter.filterRoles);
dynamicFilter.setFilter(filterRoles);
}
@@ -138,7 +143,7 @@ export default class DynamicListService implements IDynamicListService {
* @param {Response} res
* @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.errorType === 'sort_column_not_found') {
return res.boom.badRequest(null, {
@@ -147,8 +152,8 @@ export default class DynamicListService implements IDynamicListService {
}
if (error.errorType === 'view_not_found') {
return res.boom.badRequest(null, {
- errors: [{ type: 'CUSTOM.VIEW.NOT.FOUND', code: 100 }]
- })
+ errors: [{ type: 'CUSTOM.VIEW.NOT.FOUND', code: 100 }],
+ });
}
if (error.errorType === 'filter_roles_fields_not_found') {
return res.boom.badRequest(null, {
diff --git a/server/src/services/Expenses/ExpensesService.ts b/server/src/services/Expenses/ExpensesService.ts
index 617e88d48..cc9b083a2 100644
--- a/server/src/services/Expenses/ExpensesService.ts
+++ b/server/src/services/Expenses/ExpensesService.ts
@@ -1,13 +1,17 @@
import { Service, Inject } from "typedi";
import { difference, sumBy, omit } from 'lodash';
import moment from "moment";
+import {
+ EventDispatcher,
+ EventDispatcherInterface,
+} from 'decorators/eventDispatcher';
import { ServiceError } from "exceptions";
import TenancyService from 'services/Tenancy/TenancyService';
import JournalPoster from 'services/Accounting/JournalPoster';
-import JournalEntry from 'services/Accounting/JournalEntry';
import JournalCommands from 'services/Accounting/JournalCommands';
-import { IExpense, IAccount, IExpenseDTO, IExpenseCategory, IExpensesService, ISystemUser } from 'interfaces';
+import { IExpense, IExpensesFilter, IAccount, IExpenseDTO, IExpensesService, ISystemUser, IPaginationMeta } from 'interfaces';
import DynamicListingService from 'services/DynamicListing/DynamicListService';
+import events from 'subscribers/events';
const ERRORS = {
EXPENSE_NOT_FOUND: 'expense_not_found',
@@ -30,6 +34,9 @@ export default class ExpensesService implements IExpensesService {
@Inject('logger')
logger: any;
+ @EventDispatcher()
+ eventDispatcher: EventDispatcherInterface;
+
/**
* Retrieve the payment account details or returns not found server error in case the
* given account not found on the storage.
@@ -41,7 +48,7 @@ export default class ExpensesService implements IExpensesService {
this.logger.info('[expenses] trying to get the given payment account.', { tenantId, paymentAccountId });
const { accountRepository } = this.tenancy.repositories(tenantId);
- const paymentAccount = await accountRepository.getById(paymentAccountId)
+ const paymentAccount = await accountRepository.findById(paymentAccountId)
if (!paymentAccount) {
this.logger.info('[expenses] the given payment account not found.', { tenantId, paymentAccountId });
@@ -136,16 +143,15 @@ export default class ExpensesService implements IExpensesService {
}
}
- private async revertJournalEntries(
+ public async revertJournalEntries(
tenantId: number,
expenseId: number|number[],
) {
const journal = new JournalPoster(tenantId);
const journalCommands = new JournalCommands(journal);
-
- if (revertOld) {
- await journalCommands.revertJournalEntries(expenseId, 'Expense');
- }
+
+ await journalCommands.revertJournalEntries(expenseId, 'Expense');
+
return Promise.all([
journal.saveBalance(),
journal.deleteEntries(),
@@ -158,11 +164,10 @@ export default class ExpensesService implements IExpensesService {
* @param {IExpense} expense
* @param {IUser} authorizedUser
*/
- private async writeJournalEntries(
+ public async writeJournalEntries(
tenantId: number,
expense: IExpense,
revertOld: boolean,
- authorizedUser: ISystemUser
) {
this.logger.info('[expense[ trying to write expense journal entries.', { tenantId, expense });
const journal = new JournalPoster(tenantId);
@@ -171,29 +176,8 @@ export default class ExpensesService implements IExpensesService {
if (revertOld) {
await journalCommands.revertJournalEntries(expense.id, 'Expense');
}
- const mixinEntry = {
- referenceType: 'Expense',
- referenceId: expense.id,
- date: expense.paymentDate,
- userId: authorizedUser.id,
- draft: !expense.publish,
- };
- const paymentJournalEntry = new JournalEntry({
- credit: expense.totalAmount,
- account: expense.paymentAccountId,
- ...mixinEntry,
- });
- journal.credit(paymentJournalEntry);
-
- expense.categories.forEach((category: IExpenseCategory) => {
- const expenseJournalEntry = new JournalEntry({
- account: category.expenseAccountId,
- debit: category.amount,
- note: category.description,
- ...mixinEntry,
- });
- journal.debit(expenseJournalEntry);
- });
+ journalCommands.expense(expense);
+
return Promise.all([
journal.saveBalance(),
journal.saveEntries(),
@@ -229,7 +213,7 @@ export default class ExpensesService implements IExpensesService {
* @param {IExpense} expense
*/
private validateExpenseIsNotPublished(expense: IExpense) {
- if (expense.published) {
+ if (expense.publishedAt) {
throw new ServiceError(ERRORS.EXPENSE_ACCOUNT_ALREADY_PUBLISED);
}
}
@@ -291,33 +275,29 @@ export default class ExpensesService implements IExpensesService {
const { expenseRepository } = this.tenancy.repositories(tenantId);
const expense = await this.getExpenseOrThrowError(tenantId, expenseId);
- // 1. Validate payment account existance on the storage.
+ // - Validate payment account existance on the storage.
const paymentAccount = await this.getPaymentAccountOrThrowError(
tenantId,
expenseDTO.paymentAccountId,
);
- // 2. Validate expense accounts exist on the storage.
+ // - Validate expense accounts exist on the storage.
const expensesAccounts = await this.getExpensesAccountsOrThrowError(
tenantId,
this.mapExpensesAccountsIdsFromDTO(expenseDTO),
);
- // 3. Validate payment account type.
+ // - Validate payment account type.
await this.validatePaymentAccountType(tenantId, paymentAccount);
- // 4. Validate expenses accounts type.
+ // - Validate expenses accounts type.
await this.validateExpensesAccountsType(tenantId, expensesAccounts);
- // 5. Validate the given expense categories not equal zero.
+ // - Validate the given expense categories not equal zero.
this.validateCategoriesNotEqualZero(expenseDTO);
- // 6. Update the expense on the storage.
+ // - Update the expense on the storage.
const expenseObj = this.expenseDTOToModel(expenseDTO);
const expenseModel = await expenseRepository.update(expenseId, expenseObj, null);
- // 7. In case expense published, write journal entries.
- if (expenseObj.published) {
- await this.writeJournalEntries(tenantId, expenseModel, true, authorizedUser);
- }
this.logger.info('[expense] the expense updated on the storage successfully.', { tenantId, expenseDTO });
return expenseModel;
}
@@ -364,13 +344,12 @@ export default class ExpensesService implements IExpensesService {
// 6. Save the expense to the storage.
const expenseObj = this.expenseDTOToModel(expenseDTO, authorizedUser);
const expenseModel = await expenseRepository.create(expenseObj);
-
- // 7. In case expense published, write journal entries.
- if (expenseObj.published) {
- await this.writeJournalEntries(tenantId, expenseModel, false, authorizedUser);
- }
+
this.logger.info('[expense] the expense stored to the storage successfully.', { tenantId, expenseDTO });
+ // Triggers `onExpenseCreated` event.
+ this.eventDispatcher.dispatch(events.expenses.onCreated, { tenantId, expenseId: expenseModel.id });
+
return expenseModel;
}
@@ -394,6 +373,9 @@ export default class ExpensesService implements IExpensesService {
await expenseRepository.publish(expenseId);
this.logger.info('[expense] the expense published successfully.', { tenantId, expenseId });
+
+ // Triggers `onExpensePublished` event.
+ this.eventDispatcher.dispatch(events.expenses.onPublished, { tenantId, expenseId });
}
/**
@@ -409,10 +391,10 @@ export default class ExpensesService implements IExpensesService {
this.logger.info('[expense] trying to delete the expense.', { tenantId, expenseId });
await expenseRepository.delete(expenseId);
- if (expense.published) {
- await this.revertJournalEntries(tenantId, expenseId);
- }
this.logger.info('[expense] the expense deleted successfully.', { tenantId, expenseId });
+
+ // Triggers `onExpenseDeleted` event.
+ this.eventDispatcher.dispatch(events.expenses.onDeleted, { tenantId, expenseId });
}
/**
@@ -427,9 +409,11 @@ export default class ExpensesService implements IExpensesService {
this.logger.info('[expense] trying to delete the given expenses.', { tenantId, expensesIds });
await expenseRepository.bulkDelete(expensesIds);
- await this.revertJournalEntries(tenantId, expensesIds);
this.logger.info('[expense] the given expenses deleted successfully.', { tenantId, expensesIds });
+
+ // Triggers `onExpenseBulkDeleted` event.
+ this.eventDispatcher.dispatch(events.expenses.onBulkDeleted, { tenantId, expensesIds });
}
/**
@@ -443,9 +427,12 @@ export default class ExpensesService implements IExpensesService {
const { expenseRepository } = this.tenancy.repositories(tenantId);
this.logger.info('[expense] trying to publish the given expenses.', { tenantId, expensesIds });
- await expenseRepository.publishBulk(expensesIds);
+ await expenseRepository.bulkPublish(expensesIds);
this.logger.info('[expense] the given expenses ids published successfully.', { tenantId, expensesIds });
+
+ // Triggers `onExpenseBulkDeleted` event.
+ this.eventDispatcher.dispatch(events.expenses.onBulkPublished, { tenantId, expensesIds });
}
/**
@@ -454,17 +441,43 @@ export default class ExpensesService implements IExpensesService {
* @param {IExpensesFilter} expensesFilter
* @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 dynamicFilter = await this.dynamicListService.dynamicList(tenantId, Expense, 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('user');
-
dynamicFilter.buildQuery()(builder);
- });
- return expenses;
+ }).pagination(expensesFilter.page - 1, expensesFilter.pageSize);
+
+ return {
+ expenses: results,
+ pagination, filterMeta:
+ dynamicFilter.getResponseMeta(),
+ };
+ }
+
+ /**
+ * Retrieve expense details.
+ * @param {number} tenantId
+ * @param {number} expenseId
+ * @return {Promise}
+ */
+ public async getExpense(tenantId: number, expenseId: number): Promise {
+ const { Expense } = this.tenancy.models(tenantId);
+
+ const expense = await Expense.query().findById(expenseId)
+ .withGraphFetched('paymentAccount')
+ .withGraphFetched('media')
+ .withGraphFetched('categories');
+
+ if (!expense) {
+ throw new ServiceError(ERRORS.EXPENSE_NOT_FOUND);
+ }
+ return expense;
}
}
\ No newline at end of file
diff --git a/server/src/services/ItemCategories/ItemCategoriesService.ts b/server/src/services/ItemCategories/ItemCategoriesService.ts
index cbc8a0ff2..cacd7a52e 100644
--- a/server/src/services/ItemCategories/ItemCategoriesService.ts
+++ b/server/src/services/ItemCategories/ItemCategoriesService.ts
@@ -8,7 +8,6 @@ import {
IItemCategoriesFilter,
ISystemUser,
} from "interfaces";
-import ItemCategory from "models/ItemCategory";
import DynamicListingService from 'services/DynamicListing/DynamicListService';
import TenancyService from 'services/Tenancy/TenancyService';
@@ -21,6 +20,7 @@ const ERRORS = {
SELL_ACCOUNT_NOT_FOUND: 'SELL_ACCOUNT_NOT_FOUND',
INVENTORY_ACCOUNT_NOT_FOUND: 'INVENTORY_ACCOUNT_NOT_FOUND',
INVENTORY_ACCOUNT_NOT_INVENTORY: 'INVENTORY_ACCOUNT_NOT_INVENTORY',
+ CATEGORY_HAVE_ITEMS: 'CATEGORY_HAVE_ITEMS'
};
export default class ItemCategoriesService implements IItemCategoriesService {
@@ -108,7 +108,7 @@ export default class ItemCategoriesService implements IItemCategoriesService {
this.logger.info('[items] validate sell account existance.', { tenantId, sellAccountId });
const incomeType = await accountTypeRepository.getByKey('income');
- const foundAccount = await accountRepository.getById(sellAccountId);
+ const foundAccount = await accountRepository.findById(sellAccountId);
if (!foundAccount) {
this.logger.info('[items] sell account not found.', { tenantId, sellAccountId });
@@ -130,7 +130,7 @@ export default class ItemCategoriesService implements IItemCategoriesService {
this.logger.info('[items] validate cost account existance.', { tenantId, costAccountId });
const COGSType = await accountTypeRepository.getByKey('cost_of_goods_sold');
- const foundAccount = await accountRepository.getById(costAccountId)
+ const foundAccount = await accountRepository.findById(costAccountId)
if (!foundAccount) {
this.logger.info('[items] cost account not found.', { tenantId, costAccountId });
@@ -152,7 +152,7 @@ export default class ItemCategoriesService implements IItemCategoriesService {
this.logger.info('[items] validate inventory account existance.', { tenantId, inventoryAccountId });
const otherAsset = await accountTypeRepository.getByKey('other_asset');
- const foundAccount = await accountRepository.getById(inventoryAccountId);
+ const foundAccount = await accountRepository.findById(inventoryAccountId);
if (!foundAccount) {
this.logger.info('[items] inventory account not found.', { tenantId, inventoryAccountId });
@@ -202,6 +202,7 @@ export default class ItemCategoriesService implements IItemCategoriesService {
public async deleteItemCategory(tenantId: number, itemCategoryId: number, authorizedUser: ISystemUser) {
this.logger.info('[item_category] trying to delete item category.', { tenantId, itemCategoryId });
await this.getItemCategoryOrThrowError(tenantId, itemCategoryId);
+ await this.unassociateItemsWithCategories(tenantId, itemCategoryId);
const { ItemCategory } = this.tenancy.models(tenantId);
await ItemCategory.query().findById(itemCategoryId).delete();
@@ -214,7 +215,9 @@ export default class ItemCategoriesService implements IItemCategoriesService {
* @param {number[]} itemCategoriesIds
*/
private async getItemCategoriesOrThrowError(tenantId: number, itemCategoriesIds: number[]) {
- const itemCategories = await ItemCategory.query().whereIn('id', ids);
+ const { ItemCategory } = this.tenancy.models(tenantId);
+ const itemCategories = await ItemCategory.query().whereIn('id', itemCategoriesIds);
+
const storedItemCategoriesIds = itemCategories.map((category: IItemCategory) => category.id);
const notFoundCategories = difference(itemCategoriesIds, storedItemCategoriesIds);
@@ -233,10 +236,22 @@ export default class ItemCategoriesService implements IItemCategoriesService {
const dynamicList = await this.dynamicListService.dynamicList(tenantId, ItemCategory, filter);
const itemCategories = await ItemCategory.query().onBuild((query) => {
- query.orderBy('createdAt', 'ASC');
dynamicList.buildQuery()(query);
});
- return itemCategories;
+ return { itemCategories, filterMeta: dynamicList.getResponseMeta() };
+ }
+
+ /**
+ * Unlink items relations with item categories.
+ * @param {number} tenantId
+ * @param {number|number[]} itemCategoryId -
+ * @return {Promise}
+ */
+ private async unassociateItemsWithCategories(tenantId: number, itemCategoryId: number|number[]): Promise {
+ const { Item } = this.tenancy.models(tenantId);
+ const ids = Array.isArray(itemCategoryId) ? itemCategoryId : [itemCategoryId];
+
+ await Item.query().whereIn('id', ids).patch({ category_id: null });
}
/**
@@ -246,8 +261,11 @@ export default class ItemCategoriesService implements IItemCategoriesService {
*/
public async deleteItemCategories(tenantId: number, itemCategoriesIds: number[], authorizedUser: ISystemUser) {
this.logger.info('[item_category] trying to delete item categories.', { tenantId, itemCategoriesIds });
- await this.getItemCategoriesOrThrowError(tenantId, itemCategoriesIds);
+ const { ItemCategory } = this.tenancy.models(tenantId);
+ await this.getItemCategoriesOrThrowError(tenantId, itemCategoriesIds);
+ await this.unassociateItemsWithCategories(tenantId, itemCategoriesIds);
+
await ItemCategory.query().whereIn('id', itemCategoriesIds).delete();
this.logger.info('[item_category] item categories deleted successfully.', { tenantId, itemCategoriesIds });
}
diff --git a/server/src/services/Items/ItemsService.ts b/server/src/services/Items/ItemsService.ts
index 8428826e2..3597219c4 100644
--- a/server/src/services/Items/ItemsService.ts
+++ b/server/src/services/Items/ItemsService.ts
@@ -4,7 +4,6 @@ import { IItemsFilter, IItemsService, IItemDTO, IItem } from 'interfaces';
import DynamicListingService from 'services/DynamicListing/DynamicListService';
import TenancyService from 'services/Tenancy/TenancyService';
import { ServiceError } from "exceptions";
-import { Item } from "models";
const ERRORS = {
NOT_FOUND: 'NOT_FOUND',
@@ -17,6 +16,9 @@ const ERRORS = {
INVENTORY_ACCOUNT_NOT_FOUND: 'INVENTORY_ACCOUNT_NOT_FOUND',
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()
@@ -83,7 +85,7 @@ export default class ItemsService implements IItemsService {
this.logger.info('[items] validate cost account existance.', { tenantId, costAccountId });
const COGSType = await accountTypeRepository.getByKey('cost_of_goods_sold');
- const foundAccount = await accountRepository.getById(costAccountId)
+ const foundAccount = await accountRepository.findById(costAccountId)
if (!foundAccount) {
this.logger.info('[items] cost account not found.', { tenantId, costAccountId });
@@ -104,7 +106,7 @@ export default class ItemsService implements IItemsService {
this.logger.info('[items] validate sell account existance.', { tenantId, sellAccountId });
const incomeType = await accountTypeRepository.getByKey('income');
- const foundAccount = await accountRepository.getById(sellAccountId);
+ const foundAccount = await accountRepository.findById(sellAccountId);
if (!foundAccount) {
this.logger.info('[items] sell account not found.', { tenantId, sellAccountId });
@@ -125,7 +127,7 @@ export default class ItemsService implements IItemsService {
this.logger.info('[items] validate inventory account existance.', { tenantId, inventoryAccountId });
const otherAsset = await accountTypeRepository.getByKey('other_asset');
- const foundAccount = await accountRepository.getById(inventoryAccountId);
+ const foundAccount = await accountRepository.findById(inventoryAccountId);
if (!foundAccount) {
this.logger.info('[items] inventory account not found.', { tenantId, inventoryAccountId });
@@ -222,6 +224,7 @@ export default class ItemsService implements IItemsService {
this.logger.info('[items] trying to delete item.', { tenantId, itemId });
await this.getItemOrThrowError(tenantId, itemId);
+ await this.validateHasNoInvoicesOrBills(tenantId, itemId);
await Item.query().findById(itemId).delete();
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 });
await this.validateItemsIdsExists(tenantId, itemsIds);
+ await this.validateHasNoInvoicesOrBills(tenantId, itemsIds);
await Item.query().whereIn('id', itemsIds).delete();
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 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('sellAccount');
builder.withGraphFetched('costAccount');
builder.withGraphFetched('category');
dynamicFilter.buildQuery()(builder);
- });
- return items;
+ }).pagination(
+ 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
+ );
+ }
}
}
\ No newline at end of file
diff --git a/server/src/services/ManualJournals/ManualJournalsService.ts b/server/src/services/ManualJournals/ManualJournalsService.ts
index 614107497..91864a82f 100644
--- a/server/src/services/ManualJournals/ManualJournalsService.ts
+++ b/server/src/services/ManualJournals/ManualJournalsService.ts
@@ -9,6 +9,7 @@ import {
ISystemUser,
IManualJournal,
IManualJournalEntryDTO,
+ IPaginationMeta,
} from 'interfaces';
import TenancyService from 'services/Tenancy/TenancyService';
import DynamicListingService from 'services/DynamicListing/DynamicListService';
@@ -227,7 +228,7 @@ export default class ManualJournalsService implements IManuaLJournalsService {
}
/**
- *
+ * Transform DTO to model.
* @param {IManualJournalEntryDTO[]} entries
*/
private transformDTOToEntriesModel(entries: IManualJournalEntryDTO[]) {
@@ -396,16 +397,23 @@ export default class ManualJournalsService implements IManuaLJournalsService {
* @param {number} tenantId
* @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 dynamicList = await this.dynamicListService.dynamicList(tenantId, ManualJournal, 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);
- });
- return manualJournal;
+ }).pagination(filter.page - 1, filter.pageSize);
+
+ return {
+ manualJournals: results,
+ pagination,
+ filterMeta: dynamicList.getResponseMeta(),
+ };
}
/**
@@ -421,7 +429,8 @@ export default class ManualJournalsService implements IManuaLJournalsService {
this.logger.info('[manual_journals] trying to get specific manual journal.', { tenantId, manualJournalId });
const manualJournal = await ManualJournal.query()
.findById(manualJournalId)
- .withGraphFetched('entries');
+ .withGraphFetched('entries')
+ .withGraphFetched('media');
return manualJournal;
}
diff --git a/server/src/services/Media/MediaService.ts b/server/src/services/Media/MediaService.ts
new file mode 100644
index 000000000..eb8da6f70
--- /dev/null
+++ b/server/src/services/Media/MediaService.ts
@@ -0,0 +1,223 @@
+import fs from 'fs';
+import { Service, Inject } from 'typedi';
+import TenancyService from 'services/Tenancy/TenancyService';
+import { ServiceError } from "exceptions";
+import { IMedia, IMediaService } from 'interfaces';
+import { difference } from 'lodash';
+
+const fsPromises = fs.promises;
+
+const ERRORS = {
+ MINETYPE_NOT_SUPPORTED: 'MINETYPE_NOT_SUPPORTED',
+ MEDIA_NOT_FOUND: 'MEDIA_NOT_FOUND',
+ MODEL_NAME_HAS_NO_MEDIA: 'MODEL_NAME_HAS_NO_MEDIA',
+ MODEL_ID_NOT_FOUND: 'MODEL_ID_NOT_FOUND',
+ MEDIA_IDS_NOT_FOUND: 'MEDIA_IDS_NOT_FOUND',
+ MEDIA_LINK_EXISTS: 'MEDIA_LINK_EXISTS'
+}
+const publicPath = 'storage/app/public/';
+const attachmentsMimes = ['image/png', 'image/jpeg'];
+
+@Service()
+export default class MediaService implements IMediaService {
+ @Inject('logger')
+ logger: any;
+
+ @Inject()
+ tenancy: TenancyService;
+
+ @Inject('repositories')
+ sysRepositories: any;
+
+ /**
+ * Retrieve media model or throw not found error
+ * @param tenantId
+ * @param mediaId
+ */
+ async getMediaOrThrowError(tenantId: number, mediaId: number) {
+ const { Media } = this.tenancy.models(tenantId);
+ const foundMedia = await Media.query().findById(mediaId);
+
+ if (!foundMedia) {
+ throw new ServiceError(ERRORS.MEDIA_NOT_FOUND);
+ }
+ return foundMedia;
+ }
+
+ /**
+ * Retreive media models by the given ids or throw not found error.
+ * @param {number} tenantId
+ * @param {number[]} mediaIds
+ */
+ async getMediaByIdsOrThrowError(tenantId: number, mediaIds: number[]) {
+ const { Media } = this.tenancy.models(tenantId);
+ const foundMedia = await Media.query().whereIn('id', mediaIds);
+
+ const storedMediaIds = foundMedia.map((m) => m.id);
+ const notFoundMedia = difference(mediaIds, storedMediaIds);
+
+ if (notFoundMedia.length > 0) {
+ throw new ServiceError(ERRORS.MEDIA_IDS_NOT_FOUND);
+ }
+ return foundMedia;
+ }
+
+ /**
+ * Validates the model name and id.
+ * @param {number} tenantId
+ * @param {string} modelName
+ * @param {number} modelId
+ */
+ async validateModelNameAndIdExistance(tenantId: number, modelName: string, modelId: number) {
+ const models = this.tenancy.models(tenantId);
+ this.logger.info('[media] trying to validate model name and id.', { tenantId, modelName, modelId });
+
+ if (!models[modelName]) {
+ this.logger.info('[media] model name not found.', { tenantId, modelName, modelId });
+ throw new ServiceError(ERRORS.MODEL_NAME_HAS_NO_MEDIA);
+ }
+ if (!models[modelName].media) {
+ this.logger.info('[media] model is not media-able.', { tenantId, modelName, modelId });
+ throw new ServiceError(ERRORS.MODEL_NAME_HAS_NO_MEDIA);
+ }
+
+ const foundModel = await models[modelName].query().findById(modelId);
+
+ if (!foundModel) {
+ this.logger.info('[media] model is not found.', { tenantId, modelName, modelId });
+ throw new ServiceError(ERRORS.MODEL_ID_NOT_FOUND);
+ }
+ }
+
+ /**
+ * Validates the media existance.
+ * @param {number} tenantId
+ * @param {number} mediaId
+ * @param {number} modelId
+ * @param {string} modelName
+ */
+ async validateMediaLinkExistance(
+ tenantId: number,
+ mediaId: number,
+ modelId: number,
+ modelName: string
+ ) {
+ const { MediaLink } = this.tenancy.models(tenantId);
+
+ const foundMediaLinks = await MediaLink.query()
+ .where('media_id', mediaId)
+ .where('model_id', modelId)
+ .where('model_name', modelName);
+
+ if (foundMediaLinks.length > 0) {
+ throw new ServiceError(ERRORS.MEDIA_LINK_EXISTS);
+ }
+ }
+
+ /**
+ * Links the given media to the specific media-able model resource.
+ * @param {number} tenantId
+ * @param {number} mediaId
+ * @param {number} modelId
+ * @param {string} modelType
+ */
+ async linkMedia(tenantId: number, mediaId: number, modelId: number, modelName: string) {
+ this.logger.info('[media] trying to link media.', { tenantId, mediaId, modelId, modelName });
+ const { MediaLink } = this.tenancy.models(tenantId);
+ await this.validateMediaLinkExistance(tenantId, mediaId, modelId, modelName);
+
+ const media = await this.getMediaOrThrowError(tenantId, mediaId);
+ await this.validateModelNameAndIdExistance(tenantId, modelName, modelId);
+
+ await MediaLink.query().insert({ mediaId, modelId, modelName });
+ }
+
+ /**
+ * Retrieve media metadata.
+ * @param {number} tenantId - Tenant id.
+ * @param {number} mediaId - Media id.
+ * @return {Promise}
+ */
+ public async getMedia(tenantId: number, mediaId: number): Promise {
+ this.logger.info('[media] try to get media.', { tenantId, mediaId });
+ return this.getMediaOrThrowError(tenantId, mediaId);
+ }
+
+ /**
+ * Deletes the given media.
+ * @param {number} tenantId
+ * @param {number} mediaId
+ * @return {Promise}
+ */
+ public async deleteMedia(tenantId: number, mediaId: number|number[]): Promise {
+ const { Media, MediaLink } = this.tenancy.models(tenantId);
+ const { tenantRepository } = this.sysRepositories;
+
+ this.logger.info('[media] trying to delete media.', { tenantId, mediaId });
+
+ const mediaIds = Array.isArray(mediaId) ? mediaId : [mediaId];
+
+ const tenant = await tenantRepository.getById(tenantId);
+ const media = await this.getMediaByIdsOrThrowError(tenantId, mediaIds);
+
+ const tenantPath = `${publicPath}${tenant.organizationId}`;
+ const unlinkOpers = [];
+
+ media.forEach((mediaModel) => {
+ const oper = fsPromises.unlink(`${tenantPath}/${mediaModel.attachmentFile}`);
+ unlinkOpers.push(oper);
+ });
+ await Promise.all(unlinkOpers)
+ .then((resolved) => {
+ resolved.forEach(() => {
+ this.logger.info('[attachment] file has been deleted.');
+ });
+ })
+ .catch((errors) => {
+ this.logger.info('[attachment] Delete item attachment file delete failed.', { errors });
+ });
+ await MediaLink.query().whereIn('media_id', mediaIds).delete();
+ await Media.query().whereIn('id', mediaIds).delete();
+ }
+
+ /**
+ * Uploads the given attachment.
+ * @param {number} tenantId -
+ * @param {any} attachment -
+ * @return {Promise}
+ */
+ public async upload(tenantId: number, attachment: any, modelName?: string, modelId?: number): Promise {
+ const { tenantRepository } = this.sysRepositories;
+ const { Media } = this.tenancy.models(tenantId);
+
+ this.logger.info('[media] trying to upload media.', { tenantId });
+
+ const tenant = await tenantRepository.getById(tenantId);
+ const fileName = `${attachment.md5}.png`;
+
+ // Validate the attachment.
+ if (attachment && attachmentsMimes.indexOf(attachment.mimetype) === -1) {
+ throw new ServiceError(ERRORS.MINETYPE_NOT_SUPPORTED);
+ }
+ if (modelName && modelId) {
+ await this.validateModelNameAndIdExistance(tenantId, modelName, modelId);
+ }
+ try {
+ await attachment.mv(`${publicPath}${tenant.organizationId}/${fileName}`);
+ this.logger.info('[attachment] uploaded successfully');
+ } catch (error) {
+ this.logger.info('[attachment] uploading failed.', { error });
+ }
+ const media = await Media.query().insertGraph({
+ attachmentFile: `${fileName}`,
+ ...(modelName && modelId) ? {
+ links: [{
+ modelName,
+ modelId,
+ }]
+ } : {},
+ });
+ this.logger.info('[media] uploaded successfully.', { tenantId, fileName, modelName, modelId });
+ return media;
+ }
+}
\ No newline at end of file
diff --git a/server/src/services/Purchases/BillPayments.ts b/server/src/services/Purchases/BillPayments.ts
index 4389f2267..f3825488c 100644
--- a/server/src/services/Purchases/BillPayments.ts
+++ b/server/src/services/Purchases/BillPayments.ts
@@ -1,13 +1,14 @@
import { Inject, Service } from 'typedi';
import { omit, sumBy } from 'lodash';
import moment from 'moment';
-import { IBillPaymentOTD, IBillPayment } from 'interfaces';
+import { IBillPaymentOTD, IBillPayment, IBillPaymentsFilter, IPaginationMeta, IFilterMeta } from 'interfaces';
import ServiceItemsEntries from 'services/Sales/ServiceItemsEntries';
import AccountsService from 'services/Accounts/AccountsService';
import JournalPoster from 'services/Accounting/JournalPoster';
import JournalEntry from 'services/Accounting/JournalEntry';
import JournalPosterService from 'services/Sales/JournalPosterService';
import TenancyService from 'services/Tenancy/TenancyService';
+import DynamicListingService from 'services/DynamicListing/DynamicListService';
import { formatDateFields } from 'utils';
/**
@@ -25,6 +26,9 @@ export default class BillPaymentsService {
@Inject()
journalService: JournalPosterService;
+ @Inject()
+ dynamicListService: DynamicListingService;
+
/**
* Creates a new bill payment transcations and store it to the storage
* with associated bills entries and journal transactions.
@@ -39,7 +43,7 @@ export default class BillPaymentsService {
* @param {number} tenantId - Tenant id.
* @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 billPayment = {
@@ -102,7 +106,7 @@ export default class BillPaymentsService {
* @param {BillPaymentDTO} billPayment
* @param {IBillPayment} oldBillPayment
*/
- async editBillPayment(
+ public async editBillPayment(
tenantId: number,
billPaymentId: number,
billPaymentDTO,
@@ -171,7 +175,7 @@ export default class BillPaymentsService {
* @param {Integer} billPaymentId - The given bill payment id.
* @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 = await BillPayment.query().where('id', billPaymentId).first();
@@ -203,7 +207,7 @@ export default class BillPaymentsService {
* @param {BillPayment} billPayment
* @param {Integer} billPaymentId
*/
- async recordPaymentReceiveJournalEntries(tenantId: number, billPayment) {
+ private async recordPaymentReceiveJournalEntries(tenantId: number, billPayment) {
const { AccountTransaction, Account } = this.tenancy.models(tenantId);
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.
* @param {number} billPaymentId - The bill payment id.
diff --git a/server/src/services/Resource/ResourceService.js b/server/src/services/Resource/ResourceService.js
deleted file mode 100644
index 21f15beff..000000000
--- a/server/src/services/Resource/ResourceService.js
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-export default class ResourceService {
-
-}
\ No newline at end of file
diff --git a/server/src/services/Resource/ResourceService.ts b/server/src/services/Resource/ResourceService.ts
new file mode 100644
index 000000000..cd2364b46
--- /dev/null
+++ b/server/src/services/Resource/ResourceService.ts
@@ -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 [];
+ }
+}
\ No newline at end of file
diff --git a/server/src/services/Sales/JournalPosterService.ts b/server/src/services/Sales/JournalPosterService.ts
index 000bb17a4..d0e4d95b8 100644
--- a/server/src/services/Sales/JournalPosterService.ts
+++ b/server/src/services/Sales/JournalPosterService.ts
@@ -14,7 +14,7 @@ export default class JournalPosterService {
* @param {string} referenceType - The transaction reference type.
* @return {Promise}
*/
- async deleteJournalTransactions(
+ async revertJournalTransactions(
tenantId: number,
referenceId: number,
referenceType: string
diff --git a/server/src/services/Sales/PaymentsReceives.ts b/server/src/services/Sales/PaymentsReceives.ts
index 57d85c3b7..f0f27a262 100644
--- a/server/src/services/Sales/PaymentsReceives.ts
+++ b/server/src/services/Sales/PaymentsReceives.ts
@@ -10,6 +10,7 @@ import ServiceItemsEntries from 'services/Sales/ServiceItemsEntries';
import PaymentReceiveEntryRepository from 'repositories/PaymentReceiveEntryRepository';
import CustomerRepository from 'repositories/CustomerRepository';
import TenancyService from 'services/Tenancy/TenancyService';
+import DynamicListingService from 'services/DynamicListing/DynamicListService';
import { formatDateFields } from 'utils';
/**
@@ -27,6 +28,9 @@ export default class PaymentReceiveService {
@Inject()
journalService: JournalPosterService;
+ @Inject()
+ dynamicListService: DynamicListingService;
+
@Inject('logger')
logger: any;
@@ -37,7 +41,7 @@ export default class PaymentReceiveService {
* @param {number} tenantId - Tenant id.
* @param {IPaymentReceive} paymentReceive
*/
- async createPaymentReceive(tenantId: number, paymentReceive: IPaymentReceiveOTD) {
+ public async createPaymentReceive(tenantId: number, paymentReceive: IPaymentReceiveOTD) {
const {
PaymentReceive,
PaymentReceiveEntry,
@@ -107,7 +111,7 @@ export default class PaymentReceiveService {
* @param {IPaymentReceive} paymentReceive -
* @param {IPaymentReceive} oldPaymentReceive -
*/
- async editPaymentReceive(
+ public async editPaymentReceive(
tenantId: number,
paymentReceiveId: number,
paymentReceive: any,
@@ -242,7 +246,7 @@ export default class PaymentReceiveService {
* @param {number} tenantId - Tenant 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 = await PaymentReceive.query()
.where('id', paymentReceiveId)
@@ -250,6 +254,30 @@ export default class PaymentReceiveService {
.first();
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.
@@ -310,7 +338,7 @@ export default class PaymentReceiveService {
* @param {IPaymentReceive} paymentReceive
* @param {Number} paymentReceiveId
*/
- async recordPaymentReceiveJournalEntries(
+ private async recordPaymentReceiveJournalEntries(
tenantId: number,
paymentReceive: any,
paymentReceiveId?: number
@@ -370,7 +398,7 @@ export default class PaymentReceiveService {
* @param {Array} revertInvoices
* @return {Promise}
*/
- async revertInvoicePaymentAmount(tenantId: number, revertInvoices: any[]) {
+ private async revertInvoicePaymentAmount(tenantId: number, revertInvoices: any[]) {
const { SaleInvoice } = this.tenancy.models(tenantId);
const opers: Promise[] = [];
@@ -392,7 +420,7 @@ export default class PaymentReceiveService {
* @param {Array} newPaymentReceiveEntries
* @return
*/
- async saveChangeInvoicePaymentAmount(
+ private async saveChangeInvoicePaymentAmount(
tenantId: number,
paymentReceiveEntries: [],
newPaymentReceiveEntries: [],
diff --git a/server/src/services/Sales/SalesEstimate.ts b/server/src/services/Sales/SalesEstimate.ts
index ac00708c1..8ec782d8f 100644
--- a/server/src/services/Sales/SalesEstimate.ts
+++ b/server/src/services/Sales/SalesEstimate.ts
@@ -1,8 +1,10 @@
import { omit, difference, sumBy, mixin } from 'lodash';
import { Service, Inject } from 'typedi';
+import { IEstimatesFilter, IFilterMeta, IPaginationMeta } from 'interfaces';
import HasItemsEntries from 'services/Sales/HasItemsEntries';
import { formatDateFields } from 'utils';
import TenancyService from 'services/Tenancy/TenancyService';
+import DynamicListingService from 'services/DynamicListing/DynamicListService';
/**
* Sale estimate service.
@@ -19,6 +21,9 @@ export default class SaleEstimateService {
@Inject('logger')
logger: any;
+ @Inject()
+ dynamicListService: DynamicListingService;
+
/**
* Creates a new estimate with associated entries.
* @async
@@ -208,4 +213,32 @@ export default class SaleEstimateService {
});
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(),
+ };
+ }
}
\ No newline at end of file
diff --git a/server/src/services/Sales/SalesInvoices.ts b/server/src/services/Sales/SalesInvoices.ts
index 877b0bf01..1dc960e5b 100644
--- a/server/src/services/Sales/SalesInvoices.ts
+++ b/server/src/services/Sales/SalesInvoices.ts
@@ -1,11 +1,16 @@
import { Service, Inject } from 'typedi';
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 HasItemsEntries from 'services/Sales/HasItemsEntries';
import InventoryService from 'services/Inventory/Inventory';
import SalesInvoicesCost from 'services/Sales/SalesInvoicesCost';
import TenancyService from 'services/Tenancy/TenancyService';
+import DynamicListingService from 'services/DynamicListing/DynamicListService';
import { formatDateFields } from 'utils';
/**
@@ -26,6 +31,12 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
@Inject('logger')
logger: any;
+ @Inject()
+ dynamicListService: DynamicListingService;
+
+ @EventDispatcher()
+ eventDispatcher: EventDispatcherInterface;
+
/**
* Creates a new sale invoices and store it to the storage
* with associated to entries and journal transactions.
@@ -34,7 +45,7 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
* @param {ISaleInvoice} saleInvoiceDTO -
* @return {ISaleInvoice}
*/
- async createSaleInvoice(tenantId: number, saleInvoiceDTO: ISaleInvoiceOTD) {
+ public async createSaleInvoice(tenantId: number, saleInvoiceDTO: ISaleInvoiceOTD) {
const { SaleInvoice, Customer, ItemEntry } = this.tenancy.models(tenantId);
const balance = sumBy(saleInvoiceDTO.entries, e => ItemEntry.calcAmount(e));
@@ -94,7 +105,7 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
* @param {Number} saleInvoiceId -
* @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 balance = sumBy(saleInvoiceDTO.entries, e => ItemEntry.calcAmount(e));
@@ -152,7 +163,7 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
* @async
* @param {Number} saleInvoiceId - The given sale invoice id.
*/
- async deleteSaleInvoice(tenantId: number, saleInvoiceId: number) {
+ public async deleteSaleInvoice(tenantId: number, saleInvoiceId: number) {
const {
SaleInvoice,
ItemEntry,
@@ -215,7 +226,7 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
* @param {number} saleInvoiceId -
* @param {boolean} override -
*/
- recordInventoryTranscactions(
+ private recordInventoryTranscactions(
tenantId: number,
saleInvoice,
saleInvoiceId: number,
@@ -243,7 +254,7 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
* @param {string} transactionType
* @param {number} transactionId
*/
- async revertInventoryTransactions(tenantId: number, inventoryTransactions: array) {
+ private async revertInventoryTransactions(tenantId: number, inventoryTransactions: array) {
const { InventoryTransaction } = this.tenancy.models(tenantId);
const opers: Promise<[]>[] = [];
@@ -280,7 +291,7 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
* @async
* @param {Number} saleInvoiceId
*/
- async getSaleInvoiceWithEntries(tenantId: number, saleInvoiceId: number) {
+ public async getSaleInvoiceWithEntries(tenantId: number, saleInvoiceId: number) {
const { SaleInvoice } = this.tenancy.models(tenantId);
return SaleInvoice.query()
.where('id', saleInvoiceId)
@@ -405,4 +416,27 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
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() };
+ }
}
diff --git a/server/src/services/Sales/SalesReceipts.ts b/server/src/services/Sales/SalesReceipts.ts
index df726b1d2..96a481136 100644
--- a/server/src/services/Sales/SalesReceipts.ts
+++ b/server/src/services/Sales/SalesReceipts.ts
@@ -4,12 +4,17 @@ import JournalPosterService from 'services/Sales/JournalPosterService';
import HasItemEntries from 'services/Sales/HasItemsEntries';
import TenancyService from 'services/Tenancy/TenancyService';
import { formatDateFields } from 'utils';
+import { IFilterMeta, IPaginationMeta } from 'interfaces';
+import DynamicListingService from 'services/DynamicListing/DynamicListService';
@Service()
export default class SalesReceiptService {
@Inject()
tenancy: TenancyService;
+ @Inject()
+ dynamicListService: DynamicListingService;
+
@Inject()
journalService: JournalPosterService;
@@ -22,7 +27,7 @@ export default class SalesReceiptService {
* @param {ISaleReceipt} saleReceipt
* @return {Object}
*/
- async createSaleReceipt(tenantId: number, saleReceiptDTO: any) {
+ public async createSaleReceipt(tenantId: number, saleReceiptDTO: any) {
const { SaleReceipt, ItemEntry } = this.tenancy.models(tenantId);
const amount = sumBy(saleReceiptDTO.entries, e => ItemEntry.calcAmount(e));
@@ -55,7 +60,7 @@ export default class SalesReceiptService {
* @param {ISaleReceipt} saleReceipt
* @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 amount = sumBy(saleReceiptDTO.entries, e => ItemEntry.calcAmount(e));
@@ -88,7 +93,7 @@ export default class SalesReceiptService {
* @param {Integer} saleReceiptId
* @return {void}
*/
- async deleteSaleReceipt(tenantId: number, saleReceiptId: number) {
+ public async deleteSaleReceipt(tenantId: number, saleReceiptId: number) {
const { SaleReceipt, ItemEntry } = this.tenancy.models(tenantId);
const deleteSaleReceiptOper = SaleReceipt.query()
.where('id', saleReceiptId)
@@ -160,4 +165,35 @@ export default class SalesReceiptService {
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(),
+ };
+ }
}
diff --git a/server/src/services/Tenancy/TenancyService.ts b/server/src/services/Tenancy/TenancyService.ts
index 8c3b2479d..2d4dfe160 100644
--- a/server/src/services/Tenancy/TenancyService.ts
+++ b/server/src/services/Tenancy/TenancyService.ts
@@ -27,7 +27,6 @@ export default class HasTenancyService {
singletonService(tenantId: number, key: string, callback: Function) {
const container = this.tenantContainer(tenantId);
const Logger = Container.get('logger');
-
const hasServiceInstnace = container.has(key);
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.
* @param {number} tenantId - Tenant id.
*/
i18n(tenantId: number) {
return this.singletonService(tenantId, 'i18n', () => {
+ throw new Error('I18n locals is not set yet.');
});
}
diff --git a/server/src/services/Views/ViewsService.ts b/server/src/services/Views/ViewsService.ts
index 6af82e033..cec769496 100644
--- a/server/src/services/Views/ViewsService.ts
+++ b/server/src/services/Views/ViewsService.ts
@@ -1,20 +1,24 @@
import { Service, Inject } from "typedi";
-import { pick, difference } from 'lodash';
+import { difference } from 'lodash';
import { ServiceError } from 'exceptions';
import {
IViewsService,
IViewDTO,
IView,
- IViewRole,
- IViewHasColumn,
+ IViewEditDTO,
} from 'interfaces';
import TenancyService from 'services/Tenancy/TenancyService';
+import ResourceService from "services/Resource/ResourceService";
import { validateRolesLogicExpression } from 'lib/ViewRolesBuilder';
const ERRORS = {
VIEW_NOT_FOUND: 'VIEW_NOT_FOUND',
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()
@@ -25,29 +29,131 @@ export default class ViewsService implements IViewsService {
@Inject('logger')
logger: any;
+ @Inject()
+ resourceService: ResourceService;
+
/**
* Listing resource views.
- * @param {number} tenantId
- * @param {string} resourceModel
+ * @param {number} tenantId -
+ * @param {string} resourceModel -
*/
- public async listViews(tenantId: number, resourceModel: string) {
- const { View } = this.tenancy.models(tenantId);
- return View.query().where('resource_model', resourceModel);
- }
+ public async listResourceViews(tenantId: number, resourceModel: string): Promise {
+ this.logger.info('[views] trying to retrieve resource views.', { tenantId, resourceModel });
- validateResourceFieldsExistance() {
-
- }
-
- validateResourceColumnsExistance() {
-
- }
-
- getView(tenantId: number, viewId: number) {
+ // Validate the resource model name is valid.
+ this.getResourceModelOrThrowError(tenantId, resourceModel);
+ 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 {
+ 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 {
+ 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.
* ----––––––
* - Validate resource fields existance.
@@ -60,116 +166,78 @@ export default class ViewsService implements IViewsService {
* @param {number} tenantId - Tenant id.
* @param {IViewDTO} viewDTO - View DTO.
*/
- async newView(tenantId: number, viewDTO: IViewDTO): Promise {
- const { View, ViewColumn, ViewRole } = this.tenancy.models(tenantId);
-
+ public async newView(tenantId: number, viewDTO: IViewDTO): Promise {
+ const { viewRepository } = this.tenancy.repositories(tenantId);
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.
if (!validateRolesLogicExpression(viewDTO.logicExpression, viewDTO.roles)) {
- throw new ServiceError(ERRORS.INVALID_LOGIC_EXPRESSION);
+ throw new ServiceError(ERRORS.LOGIC_EXPRESSION_INVALID);
}
// Save view details.
- const view = await View.query().insert({
- name: viewDTO.name,
+ const view = await viewRepository.insert({
predefined: false,
+ name: viewDTO.name,
rolesLogicExpression: viewDTO.logicExpression,
+ resourceModel: viewDTO.resourceModel,
+ roles: viewDTO.roles,
+ columns: viewDTO.columns,
});
- this.logger.info('[views] inserted to the storage.', { tenantId, viewDTO });
-
- // 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);
+ this.logger.info('[views] inserted to the storage successfully.', { tenantId, viewDTO });
+ return view;
}
/**
+ * 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} viewId
* @param {IViewEditDTO}
*/
- async editView(tenantId: number, viewId: number, viewEditDTO: IViewEditDTO) {
- const { View, ViewRole, ViewColumn } = req.models;
- const view = await View.query().where('id', viewId)
- .withGraphFetched('roles.field')
- .withGraphFetched('columns')
- .first();
+ public async editView(tenantId: number, viewId: number, viewEditDTO: IViewEditDTO): Promise {
+ const { View } = this.tenancy.models(tenantId);
+ this.logger.info('[view] trying to edit custom view.', { tenantId, viewId });
- const errorReasons = [];
- const fieldsSlugs = viewEditDTO.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 = viewEditDTO.columns.map((c) => c.key);
+ // Retrieve view details or throw not found error.
+ const view = await this.getViewOrThrowError(tenantId, viewId);
- // The difference between the stored resource fields and submit fields keys.
- const notFoundFields = difference(fieldsSlugs, resourceFieldsKeys);
+ // Validate the resource name is exists and resourcable.
+ const ResourceModel = this.getResourceModelOrThrowError(tenantId, view.resourceModel);
- // 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 view name uniquiness.
+ await this.validateViewNameUniquiness(tenantId, view.resourceModel, viewEditDTO.name, viewId);
+
+ // Validate the given fields keys exist on the storage.
+ this.validateResourceRolesFieldsExistance(ResourceModel, view.roles);
+
+ // Validate the given columnable fields keys exists on the storage.
+ 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.
- if (!validateViewRoles(viewEditDTO.roles, viewEditDTO.logicExpression)) {
- errorReasons.push({ type: 'VIEW.ROLES.LOGIC.EXPRESSION.INVALID', code: 400 });
+ if (!validateRolesLogicExpression(viewEditDTO.logicExpression, viewEditDTO.roles)) {
+ 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.
await View.query()
.where('id', view.id)
@@ -177,78 +245,15 @@ export default class ViewsService implements IViewsService {
name: viewEditDTO.name,
roles_logic_expression: viewEditDTO.logicExpression,
});
-
- // 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);
+ this.logger.info('[view] edited successfully.', { tenantId, viewId });
}
/**
* Retrieve views details of the given id or throw not found error.
+ * @private
* @param {number} tenantId
* @param {number} viewId
+ * @return {Promise}
*/
private async getViewByIdOrThrowError(tenantId: number, viewId: number): Promise {
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.
* @param {number} tenantId - Tenant id.
* @param {number} viewId - View id.
+ * @return {Promise}
*/
public async deleteView(tenantId: number, viewId: number): Promise {
const { View } = this.tenancy.models(tenantId);
diff --git a/server/src/subscribers/events.ts b/server/src/subscribers/events.ts
index 975036766..13a990f23 100644
--- a/server/src/subscribers/events.ts
+++ b/server/src/subscribers/events.ts
@@ -37,6 +37,18 @@ export default {
tenantSeeded: 'onTenantSeeded',
},
+ /**
+ * Accounts service.
+ */
+ accounts: {
+ onCreated: 'onAccountCreated',
+ onEdited: 'onAccountEdited',
+ onDeleted: 'onAccountDeleted',
+ onBulkDeleted: 'onBulkDeleted',
+ onBulkActivated: 'onAccountBulkActivated',
+ onActivated: 'onAccountActivated'
+ },
+
/**
* Manual journals service.
*/
@@ -47,5 +59,18 @@ export default {
onDeletedBulk: 'onManualJournalCreatedBulk',
onPublished: 'onManualJournalPublished',
onPublishedBulk: 'onManualJournalPublishedBulk',
+ },
+
+ /**
+ * Expenses service.
+ */
+ expenses: {
+ onCreated: 'onExpenseCreated',
+ onEdited: 'onExpenseEdited',
+ onDeleted: 'onExpenseDelted',
+ onPublished: 'onExpensePublished',
+
+ onBulkDeleted: 'onExpenseBulkDeleted',
+ onBulkPublished: 'onBulkPublished',
}
}
diff --git a/server/src/subscribers/expenses.ts b/server/src/subscribers/expenses.ts
new file mode 100644
index 000000000..3425509bf
--- /dev/null
+++ b/server/src/subscribers/expenses.ts
@@ -0,0 +1,87 @@
+import { Container, Inject, Service } from 'typedi';
+import { EventSubscriber, On } from 'event-dispatch';
+import events from 'subscribers/events';
+import ExpensesService from 'services/Expenses/ExpensesService';
+import TenancyService from 'services/Tenancy/TenancyService';
+import ExpenseRepository from 'repositories/ExpenseRepository';
+
+@EventSubscriber()
+export default class ExpensesSubscriber {
+ tenancy: TenancyService;
+ expensesService: ExpensesService;
+
+ constructor() {
+ this.tenancy = Container.get(TenancyService);
+ this.expensesService = Container.get(ExpensesService);
+ }
+
+ /**
+ * On expense created.
+ */
+ @On(events.expenses.onCreated)
+ public async onExpenseCreated({ expenseId, tenantId }) {
+ const { expenseRepository } = this.tenancy.repositories(tenantId);
+ const expense = await expenseRepository.getById(expenseId);
+
+ // In case expense published, write journal entries.
+ if (expense.publishedAt) {
+ await this.expensesService.writeJournalEntries(tenantId, expense, false);
+ }
+ }
+
+ /**
+ * On expense edited.
+ */
+ @On(events.expenses.onEdited)
+ public async onExpenseEdited({ expenseId, tenantId }) {
+ const { expenseRepository } = this.tenancy.repositories(tenantId);
+ const expense = await expenseRepository.getById(expenseId);
+
+ // In case expense published, write journal entries.
+ if (expense.publishedAt) {
+ await this.expensesService.writeJournalEntries(tenantId, expense, true);
+ }
+ }
+
+ /**
+ *
+ * @param param0
+ */
+ @On(events.expenses.onDeleted)
+ public async onExpenseDeleted({ expenseId, tenantId }) {
+ await this.expensesService.revertJournalEntries(tenantId, expenseId);
+ }
+
+ /**
+ *
+ * @param param0
+ */
+ @On(events.expenses.onPublished)
+ public async onExpensePublished({ expenseId, tenantId }) {
+ const { expenseRepository } = this.tenancy.repositories(tenantId);
+ const expense = await expenseRepository.getById(expenseId);
+
+ // In case expense published, write journal entries.
+ if (expense.publishedAt) {
+ await this.expensesService.writeJournalEntries(tenantId, expense, false);
+ }
+ }
+
+ /**
+ *
+ * @param param0
+ */
+ @On(events.expenses.onBulkDeleted)
+ public onExpenseBulkDeleted({ expensesIds, tenantId }) {
+
+ }
+
+ /**
+ *
+ * @param param0
+ */
+ @On(events.expenses.onBulkPublished)
+ public onExpenseBulkPublished({ expensesIds, tenantId }) {
+
+ }
+}
\ No newline at end of file
diff --git a/server/src/system/migrations/20190104195900_create_password_resets_table.js b/server/src/system/migrations/20190104195900_create_password_resets_table.js
index bd274950e..9337949c7 100644
--- a/server/src/system/migrations/20190104195900_create_password_resets_table.js
+++ b/server/src/system/migrations/20190104195900_create_password_resets_table.js
@@ -1,8 +1,8 @@
exports.up = (knex) => knex.schema.createTable('password_resets', (table) => {
table.increments();
- table.string('email');
- table.string('token');
+ table.string('email').index();
+ table.string('token').index();
table.timestamp('created_at');
});
diff --git a/server/src/system/migrations/20200420134631_create_tenants_table.js b/server/src/system/migrations/20200420134631_create_tenants_table.js
index 1d73550fb..ef968abf9 100644
--- a/server/src/system/migrations/20200420134631_create_tenants_table.js
+++ b/server/src/system/migrations/20200420134631_create_tenants_table.js
@@ -2,7 +2,7 @@
exports.up = function(knex) {
return knex.schema.createTable('tenants', (table) => {
table.bigIncrements();
- table.string('organization_id');
+ table.string('organization_id').index();
table.dateTime('under_maintenance_since').nullable();
table.dateTime('initialized_at').nullable();
diff --git a/server/src/system/migrations/20190822214242_create_users_table.js b/server/src/system/migrations/20200420134633_create_users_table.js
similarity index 50%
rename from server/src/system/migrations/20190822214242_create_users_table.js
rename to server/src/system/migrations/20200420134633_create_users_table.js
index 50fa5e42c..d4a08a226 100644
--- a/server/src/system/migrations/20190822214242_create_users_table.js
+++ b/server/src/system/migrations/20200420134633_create_users_table.js
@@ -4,18 +4,15 @@ exports.up = function (knex) {
table.increments();
table.string('first_name');
table.string('last_name');
- table.string('email').unique();
- table.string('phone_number').unique();
+ table.string('email').unique().index();
+ table.string('phone_number').unique().index();
table.string('password');
- table.boolean('active');
+ table.boolean('active').index();
table.string('language');
-
- table.integer('tenant_id').unsigned();
-
- table.date('invite_accepted_at');
- table.date('last_login_at');
-
- table.dateTime('deleted_at');
+ table.bigInteger('tenant_id').unsigned().index().references('id').inTable('tenants');
+ table.date('invite_accepted_at').index();
+ table.date('last_login_at').index();
+ table.dateTime('deleted_at').index();
table.timestamps();
});
};
diff --git a/server/src/system/migrations/20200422225247_create_user_invites_table.js b/server/src/system/migrations/20200422225247_create_user_invites_table.js
index 21f1e7fa6..abb723b20 100644
--- a/server/src/system/migrations/20200422225247_create_user_invites_table.js
+++ b/server/src/system/migrations/20200422225247_create_user_invites_table.js
@@ -2,9 +2,9 @@
exports.up = function(knex) {
return knex.schema.createTable('user_invites', (table) => {
table.increments();
- table.string('email');
- table.string('token').unique();
- table.integer('tenant_id').unsigned();
+ table.string('email').index();
+ table.string('token').unique().index();
+ table.bigInteger('tenant_id').unsigned().index().references('id').inTable('tenants');
table.datetime('created_at');
});
};
diff --git a/server/src/system/migrations/20200527091649_create_subscriptions_usage_table.js b/server/src/system/migrations/20200527091649_create_subscriptions_usage_table.js
deleted file mode 100644
index 5ab810315..000000000
--- a/server/src/system/migrations/20200527091649_create_subscriptions_usage_table.js
+++ /dev/null
@@ -1,18 +0,0 @@
-exports.up = function(knex) {
- return knex.schema.createTable('subscriptions_usage', table => {
- table.increments();
- table.integer('user_id');
- table.integer('plan_id');
-
- table.dateTime('trial_ends_at');
-
- table.dateTime('subscription_starts_at');
- table.dateTime('subscription_ends_at');
-
- table.timestamps();
- });
-};
-
-exports.down = function(knex) {
- return knex.schema.dropTableIfExists('subscriptions_usage');
-};
diff --git a/server/src/system/migrations/20200823234134_create_plans_table.js b/server/src/system/migrations/20200823234134_create_plans_table.js
index 80392db62..2fc61a43a 100644
--- a/server/src/system/migrations/20200823234134_create_plans_table.js
+++ b/server/src/system/migrations/20200823234134_create_plans_table.js
@@ -17,7 +17,6 @@ exports.up = function(knex) {
table.string('invoice_interval').nullable();
table.integer('index').unsigned();
-
table.timestamps();
}).then(() => {
return knex.seed.run({
diff --git a/server/src/system/migrations/20200823234434_create_subscription_plan_feature.js b/server/src/system/migrations/20200823234434_create_subscription_plan_feature.js
index 78aaf3356..43fea2798 100644
--- a/server/src/system/migrations/20200823234434_create_subscription_plan_feature.js
+++ b/server/src/system/migrations/20200823234434_create_subscription_plan_feature.js
@@ -2,12 +2,10 @@
exports.up = function(knex) {
return knex.schema.createTable('subscription_plan_features', table => {
table.increments();
-
- table.integer('plan_id').unsigned();
+ table.integer('plan_id').unsigned().index().references('id').inTable('subscription_plans');
table.string('slug');
table.string('name');
table.string('description');
-
table.timestamps();
});
};
diff --git a/server/src/system/migrations/20200823234636_create_subscription_plan_subscription.js b/server/src/system/migrations/20200823234636_create_subscription_plan_subscription.js
index a8b7e2621..acdc9a0e6 100644
--- a/server/src/system/migrations/20200823234636_create_subscription_plan_subscription.js
+++ b/server/src/system/migrations/20200823234636_create_subscription_plan_subscription.js
@@ -4,8 +4,8 @@ exports.up = function(knex) {
table.increments('id');
table.string('slug');
- table.integer('plan_id').unsigned();
- table.integer('tenant_id').unsigned();
+ table.integer('plan_id').unsigned().index().references('id').inTable('subscription_plans');
+ table.bigInteger('tenant_id').unsigned().index().references('id').inTable('tenants');
table.dateTime('trial_started_at').nullable();
table.dateTime('trial_ends_at').nullable();
diff --git a/server/src/system/migrations/20200823235339_create_subscription_licenses_table.js b/server/src/system/migrations/20200823235339_create_subscription_licenses_table.js
index 5b79cd4a2..206721cb2 100644
--- a/server/src/system/migrations/20200823235339_create_subscription_licenses_table.js
+++ b/server/src/system/migrations/20200823235339_create_subscription_licenses_table.js
@@ -3,15 +3,15 @@ exports.up = function(knex) {
return knex.schema.createTable('subscription_licenses', table => {
table.increments();
- table.string('license_code').unique();
- table.integer('plan_id').unsigned();
+ table.string('license_code').unique().index();
+ table.integer('plan_id').unsigned().index().references('id').inTable('subscription_plans');
table.integer('license_period').unsigned();
table.string('period_interval');
- table.dateTime('sent_at');
- table.dateTime('disabled_at');
- table.dateTime('used_at');
+ table.dateTime('sent_at').index();
+ table.dateTime('disabled_at').index();
+ table.dateTime('used_at').index();
table.timestamps();
})
diff --git a/server/src/system/repositories/SystemUserRepository.ts b/server/src/system/repositories/SystemUserRepository.ts
index ccfa76fa4..7ab28b3ba 100644
--- a/server/src/system/repositories/SystemUserRepository.ts
+++ b/server/src/system/repositories/SystemUserRepository.ts
@@ -12,21 +12,22 @@ export default class SystemUserRepository extends SystemRepository {
/**
* Patches the last login date to the given system user.
* @param {number} userId
+ * @return {Promise}
*/
- async patchLastLoginAt(userId: number) {
- const user = await SystemUser.query().patchAndFetchById(userId, {
+ async patchLastLoginAt(userId: number): Promise {
+ await SystemUser.query().patchAndFetchById(userId, {
last_login_at: moment().toMySqlDateTime()
});
- this.flushUserCache(user);
- return user;
+ this.flushCache();
}
/**
* Finds system user by crediential.
* @param {string} crediential - Phone number or email.
* @return {ISystemUser}
+ * @return {Promise}
*/
- findByCrediential(crediential: string) {
+ findByCrediential(crediential: string): Promise {
return SystemUser.query().whereNotDeleted()
.findOne('email', crediential)
.orWhere('phone_number', crediential);
@@ -34,9 +35,10 @@ export default class SystemUserRepository extends SystemRepository {
/**
* Retrieve system user details of the given id.
- * @param {number} userId
+ * @param {number} userId - User id.
+ * @return {Promise}
*/
- getById(userId: number) {
+ getById(userId: number): Promise {
return this.cache.get(`systemUser.id.${userId}`, () => {
return SystemUser.query().whereNotDeleted().findById(userId);
});
@@ -44,10 +46,11 @@ export default class SystemUserRepository extends SystemRepository {
/**
* Retrieve user by id and tenant id.
- * @param {number} userId
- * @param {number} tenantId
+ * @param {number} userId - User id.
+ * @param {number} tenantId - Tenant id.
+ * @return {Promise}
*/
- getByIdAndTenant(userId: number, tenantId: number) {
+ getByIdAndTenant(userId: number, tenantId: number): Promise {
return this.cache.get(`systemUser.id.${userId}.tenant.${tenantId}`, () => {
return SystemUser.query().whereNotDeleted()
.findOne({ id: userId, tenant_id: tenantId });
@@ -56,9 +59,10 @@ export default class SystemUserRepository extends SystemRepository {
/**
* Retrieve system user details by the given email.
- * @param {string} email
+ * @param {string} email - Email
+ * @return {Promise}
*/
- getByEmail(email: string) {
+ getByEmail(email: string): Promise {
return this.cache.get(`systemUser.email.${email}`, () => {
return SystemUser.query().whereNotDeleted().findOne('email', email);
});
@@ -66,9 +70,10 @@ export default class SystemUserRepository extends SystemRepository {
/**
* Retrieve user by phone number.
- * @param {string} phoneNumber
+ * @param {string} phoneNumber - Phone number
+ * @return {Promise}
*/
- getByPhoneNumber(phoneNumber: string) {
+ getByPhoneNumber(phoneNumber: string): Promise {
return this.cache.get(`systemUser.phoneNumber.${phoneNumber}`, () => {
return SystemUser.query().whereNotDeleted().findOne('phoneNumber', phoneNumber);
});
@@ -76,62 +81,61 @@ export default class SystemUserRepository extends SystemRepository {
/**
* Edits details.
- * @param {number} userId
- * @param {number} user
+ * @param {number} userId - User id.
+ * @param {number} user - User input.
+ * @return {Promise}
*/
- edit(userId: number, userInput: ISystemUser) {
- const user = SystemUser.query().patchAndFetchById(userId, { ...userInput });
- this.flushUserCache(user);
- return user;
+ async edit(userId: number, userInput: ISystemUser): Promise {
+ await SystemUser.query().patchAndFetchById(userId, { ...userInput });
+ this.flushCache();
}
/**
* Creates a new user.
- * @param {IUser} userInput
+ * @param {IUser} userInput - User input.
+ * @return {Promise}
*/
- create(userInput: ISystemUser) {
- return SystemUser.query().insert({ ...userInput });
+ async create(userInput: ISystemUser): Promise {
+ const systemUser = await SystemUser.query().insert({ ...userInput });
+ this.flushCache();
+
+ return systemUser;
}
/**
* Deletes user by the given id.
- * @param {number} userId
+ * @param {number} userId - User id.
+ * @return {Promise}
*/
- async deleteById(userId: number) {
- const user = await this.getById(userId);
+ async deleteById(userId: number): Promise {
await SystemUser.query().where('id', userId).delete();
- this.flushUserCache(user);
+ this.flushCache();
}
/**
* Activate user by the given id.
- * @param {number} userId
+ * @param {number} userId - User id.
+ * @return {Promise}
*/
- async activateById(userId: number) {
- const user = await SystemUser.query().patchAndFetchById(userId, { active: 1 });
- this.flushUserCache(user);
- return user;
+ async activateById(userId: number): Promise {
+ await SystemUser.query().patchAndFetchById(userId, { active: 1 });
+ this.flushCache();
}
/**
* Inactivate user by the given id.
- * @param {number} userId
+ * @param {number} userId - User id.
+ * @return {Promise}
*/
- async inactivateById(userId: number) {
- const user = await SystemUser.query().patchAndFetchById(userId, { active: 0 });
- this.flushUserCache(user);
- return user;
+ async inactivateById(userId: number): Promise {
+ await SystemUser.query().patchAndFetchById(userId, { active: 0 });
+ this.flushCache();
}
/**
- * Flush user cache.
- * @param {IUser} user
+ * Flushes user repository cache.
*/
- flushUserCache(user: ISystemUser) {
- this.cache.del(`systemUser.phoneNumber.${user.phoneNumber}`);
- this.cache.del(`systemUser.email.${user.email}`);
-
- this.cache.del(`systemUser.id.${user.id}`);
- this.cache.del(`systemUser.id.${user.id}.tenant.${user.tenantId}`);
+ flushCache() {
+ this.cache.delStartWith('systemUser');
}
}
\ No newline at end of file