From d919b56e78efc1343cb6b3c02f79a35972df5b5a Mon Sep 17 00:00:00 2001 From: "a.bouhuolia" Date: Sat, 23 Jan 2021 11:39:29 +0200 Subject: [PATCH] feat: redesign accounts types. --- .../containers/Accounts/AccountsDataTable.js | 2 +- server/package.json | 5 - server/src/api/controllers/AccountTypes.ts | 23 +- server/src/api/controllers/Accounts.ts | 10 +- server/src/api/controllers/BaseController.ts | 16 +- .../api/controllers/Sales/PaymentReceives.ts | 4 +- server/src/data/AccountTypes.ts | 226 +++++++++++ ...190822214302_create_account_types_table.js | 14 - .../20190822214303_create_accounts_table.js | 2 +- .../core/20190423085242_seed_accounts.js | 22 +- server/src/database/seeds/data/accounts.js | 26 +- .../src/database/seeds/data/accounts_types.js | 48 +-- server/src/interfaces/Account.ts | 6 +- server/src/lib/AccountTypes/index.ts | 101 +++++ server/src/loaders/tenantModels.ts | 2 - server/src/loaders/tenantRepositories.ts | 2 - server/src/locales/en.json | 13 +- server/src/models/Account.js | 116 +++++- server/src/models/AccountType.js | 80 ---- server/src/models/index.js | 2 - .../src/repositories/AccountTypeRepository.ts | 48 --- .../src/services/Accounts/AccountsService.ts | 31 +- .../Accounts/AccountsTypesServices.ts | 16 +- .../src/services/Expenses/ExpensesService.ts | 22 +- .../ItemCategories/ItemCategoriesService.ts | 25 +- server/src/services/Items/ItemsService.ts | 43 +-- server/src/services/Purchases/BillPayments.ts | 360 +++++++++++------- server/src/services/Sales/PaymentsReceives.ts | 30 +- server/src/services/Sales/SalesReceipts.ts | 19 +- 29 files changed, 786 insertions(+), 528 deletions(-) create mode 100644 server/src/data/AccountTypes.ts delete mode 100644 server/src/database/migrations/20190822214302_create_account_types_table.js create mode 100644 server/src/lib/AccountTypes/index.ts delete mode 100644 server/src/models/AccountType.js delete mode 100644 server/src/repositories/AccountTypeRepository.ts diff --git a/client/src/containers/Accounts/AccountsDataTable.js b/client/src/containers/Accounts/AccountsDataTable.js index 14ab2c163..99df961a2 100644 --- a/client/src/containers/Accounts/AccountsDataTable.js +++ b/client/src/containers/Accounts/AccountsDataTable.js @@ -136,7 +136,7 @@ function AccountsDataTable({ Header: formatMessage({ id: 'account_name' }), accessor: 'name', className: 'account_name', - width: 220, + width: 200, }, { id: 'code', diff --git a/server/package.json b/server/package.json index 4fba092e0..e68ae1703 100644 --- a/server/package.json +++ b/server/package.json @@ -22,11 +22,6 @@ "app-root-path": "^3.0.0", "axios": "^0.20.0", "bcryptjs": "^2.4.3", - "bookshelf": "^0.15.1", - "bookshelf-cascade-delete": "^2.0.1", - "bookshelf-json-columns": "^2.1.1", - "bookshelf-modelbase": "^2.10.4", - "bookshelf-paranoia": "^0.13.1", "compression": "^1.7.4", "country-codes-list": "^1.6.8", "crypto-random-string": "^3.2.0", diff --git a/server/src/api/controllers/AccountTypes.ts b/server/src/api/controllers/AccountTypes.ts index 0be025ed3..9e832162d 100644 --- a/server/src/api/controllers/AccountTypes.ts +++ b/server/src/api/controllers/AccountTypes.ts @@ -5,7 +5,7 @@ import BaseController from 'api/controllers/BaseController'; import AccountsTypesService from 'services/Accounts/AccountsTypesServices'; @Service() -export default class AccountsTypesController extends BaseController{ +export default class AccountsTypesController extends BaseController { @Inject() accountsTypesService: AccountsTypesService; @@ -15,23 +15,26 @@ export default class AccountsTypesController extends BaseController{ router() { const router = Router(); - router.get('/', - asyncMiddleware(this.getAccountTypesList.bind(this)) - ); + router.get('/', asyncMiddleware(this.getAccountTypesList.bind(this))); + return router; } /** * Retrieve accounts types list. + * @param {Request} req - Request. + * @param {Response} res - Response. + * @return {Response} */ - async getAccountTypesList(req: Request, res: Response, next: NextFunction) { - const { tenantId, user } = req; - + getAccountTypesList(req: Request, res: Response, next: NextFunction) { try { - const accountTypes = await this.accountsTypesService.getAccountsTypes(tenantId); - return res.status(200).send({ account_types: accountTypes }); + const accountTypes = this.accountsTypesService.getAccountsTypes(); + + return res.status(200).send({ + account_types: this.transfromToResponse(accountTypes, ['label'], req), + }); } catch (error) { next(error); } } -}; +} diff --git a/server/src/api/controllers/Accounts.ts b/server/src/api/controllers/Accounts.ts index 01a83189a..9c26daa88 100644 --- a/server/src/api/controllers/Accounts.ts +++ b/server/src/api/controllers/Accounts.ts @@ -109,10 +109,11 @@ export default class AccountsController extends BaseController { .isLength({ min: 3, max: 6 }) .trim() .escape(), - check('account_type_id') + check('account_type') .exists() - .isInt({ min: 0, max: DATATYPES_LENGTH.INT_10 }) - .toInt(), + .isLength({ min: 3, max: DATATYPES_LENGTH.STRING }) + .trim() + .escape(), check('description') .optional({ nullable: true }) .isLength({ max: DATATYPES_LENGTH.TEXT }) @@ -341,6 +342,7 @@ export default class AccountsController extends BaseController { * Retrieve accounts datatable list. * @param {Request} req * @param {Response} res + * @param {Response} */ async getAccountsList(req: Request, res: Response, next: NextFunction) { const { tenantId } = req; @@ -360,7 +362,7 @@ export default class AccountsController extends BaseController { } = await this.accountsService.getAccountsList(tenantId, filter); return res.status(200).send({ - accounts, + accounts: this.transfromToResponse(accounts, 'accountTypeLabel', req), filter_meta: this.transfromToResponse(filterMeta), }); } catch (error) { diff --git a/server/src/api/controllers/BaseController.ts b/server/src/api/controllers/BaseController.ts index 6e974fc5e..4d3bb8b3e 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, snakeCase, omit } from "lodash"; +import { camelCase, snakeCase, omit, set, get } from "lodash"; import { mapKeysDeep } from 'utils' import asyncMiddleware from 'api/middleware/asyncMiddleware'; @@ -61,8 +61,18 @@ export default class BaseController { * Transform the given data to response. * @param {any} data */ - transfromToResponse(data: any) { - return mapKeysDeep(data, (v, k) => snakeCase(k)); + transfromToResponse(data: any, translatable?: string | string[], req?: Request) { + const response = mapKeysDeep(data, (v, k) => snakeCase(k)); + + if (translatable) { + const translatables = Array.isArray(translatable) ? translatable : [translatable]; + + translatables.forEach((path) => { + const value = get(response, path); + set(response, path, req.__(value)); + }); + } + return response; } asyncMiddleware(callback) { diff --git a/server/src/api/controllers/Sales/PaymentReceives.ts b/server/src/api/controllers/Sales/PaymentReceives.ts index 9c4070a85..143c765d5 100644 --- a/server/src/api/controllers/Sales/PaymentReceives.ts +++ b/server/src/api/controllers/Sales/PaymentReceives.ts @@ -334,10 +334,10 @@ export default class PaymentReceivesController extends BaseController { errors: [{ type: 'PAYMENT_RECEIVE_NOT_EXISTS', code: 300 }], }); } - if (error.errorType === 'DEPOSIT_ACCOUNT_NOT_CURRENT_ASSET_TYPE') { + if (error.errorType === 'DEPOSIT_ACCOUNT_INVALID_TYPE') { return res.boom.badRequest(null, { errors: [ - { type: 'DEPOSIT_ACCOUNT_NOT_CURRENT_ASSET_TYPE', code: 300 }, + { type: 'DEPOSIT_ACCOUNT_INVALID_TYPE', code: 300 }, ], }); } diff --git a/server/src/data/AccountTypes.ts b/server/src/data/AccountTypes.ts new file mode 100644 index 000000000..649db460b --- /dev/null +++ b/server/src/data/AccountTypes.ts @@ -0,0 +1,226 @@ + + +export const ACCOUNT_TYPE = { + CASH: 'cash', + BANK: 'bank', + ACCOUNTS_RECEIVABLE: 'accounts-receivable', + INVENTORY: 'inventory', + OTHER_CURRENT_ASSET: 'other-ACCOUNT_PARENT_TYPE.CURRENT_ASSET', + FIXED_ASSET: 'fixed-asset', + NON_CURRENT_ASSET: 'non-ACCOUNT_PARENT_TYPE.CURRENT_ASSET', + + ACCOUNTS_PAYABLE: 'accounts-payable', + CREDIT_CARD: 'credit-card', + TAX_PAYABLE: 'tax-payable', + OTHER_CURRENT_LIABILITY: 'other-current-liability', + LOGN_TERM_LIABILITY: 'long-term-liability', + NON_CURRENT_LIABILITY: 'non-current-liability', + + EQUITY: 'equity', + INCOME: 'income', + OTHER_INCOME: 'other-income', + COST_OF_GOODS_SOLD: 'cost-of-goods-sold', + EXPENSE: 'expense', + OTHER_EXPENSE: 'other-expense', +}; + +export const ACCOUNT_PARENT_TYPE = { + CURRENT_ASSET: 'current-asset', + FIXED_ASSET: 'fixed-asset', + NON_CURRENT_ASSET: 'non-ACCOUNT_PARENT_TYPE.CURRENT_ASSET', + + CURRENT_LIABILITY: 'current-liability', + LOGN_TERM_LIABILITY: 'long-term-liability', + NON_CURRENT_LIABILITY: 'non-current-liability', + + EQUITY: 'equity', + EXPENSE: 'expense', + INCOME: 'income', +}; + +export const ACCOUNT_ROOT_TYPE = { + ASSET: 'asset', + LIABILITY: 'liability', + EQUITY: 'equity', + EXPENSE: 'expene', + INCOME: 'income', +}; + + +export const ACCOUNT_NORMAL = { + CREDIT: 'credit', + DEBIT: 'debit', +}; +export const ACCOUNT_TYPES = [ + { + label: 'Cash', + key: ACCOUNT_TYPE.CASH, + normal: ACCOUNT_NORMAL.DEBIT, + parentType: ACCOUNT_PARENT_TYPE.CURRENT_ASSET, + rootType: ACCOUNT_ROOT_TYPE.ASSET, + balanceSheet: true, + incomeSheet: false, + }, + { + label: 'Bank', + key: ACCOUNT_TYPE.BANK, + normal: ACCOUNT_NORMAL.DEBIT, + parentType: ACCOUNT_PARENT_TYPE.CURRENT_ASSET, + rootType: ACCOUNT_ROOT_TYPE.ASSET, + balanceSheet: true, + incomeSheet: false, + }, + { + label: 'Accounts Receivable', + key: ACCOUNT_TYPE.ACCOUNTS_RECEIVABLE, + normal: ACCOUNT_NORMAL.DEBIT, + rootType: ACCOUNT_ROOT_TYPE.ASSET, + parentType: ACCOUNT_PARENT_TYPE.CURRENT_ASSET, + balanceSheet: true, + incomeSheet: false, + }, + { + label: 'Inventory', + key: ACCOUNT_TYPE.INVENTORY, + normal: ACCOUNT_NORMAL.DEBIT, + rootType: ACCOUNT_ROOT_TYPE.ASSET, + parentType: ACCOUNT_PARENT_TYPE.CURRENT_ASSET, + balanceSheet: true, + incomeSheet: false, + }, + { + label: 'Other Current Asset', + key: ACCOUNT_TYPE.OTHER_CURRENT_ASSET, + normal: ACCOUNT_NORMAL.DEBIT, + rootType: ACCOUNT_ROOT_TYPE.ASSET, + parentType: ACCOUNT_PARENT_TYPE.CURRENT_ASSET, + balanceSheet: true, + incomeSheet: false, + }, + { + label: 'Fixed Asset', + key: ACCOUNT_TYPE.FIXED_ASSET, + normal: ACCOUNT_NORMAL.DEBIT, + rootType: ACCOUNT_ROOT_TYPE.ASSET, + parentType: ACCOUNT_PARENT_TYPE.FIXED_ASSET, + balanceSheet: true, + incomeSheet: false, + }, + { + label: 'Non-Current Asset', + key: ACCOUNT_TYPE.NON_CURRENT_ASSET, + normal: ACCOUNT_NORMAL.DEBIT, + rootType: ACCOUNT_ROOT_TYPE.ASSET, + parentType: ACCOUNT_PARENT_TYPE.FIXED_ASSET, + balanceSheet: true, + incomeSheet: false, + }, + { + label: 'Accounts Payable', + key: ACCOUNT_TYPE.ACCOUNTS_PAYABLE, + normal: ACCOUNT_NORMAL.CREDIT, + rootType: ACCOUNT_ROOT_TYPE.LIABILITY, + parentType: ACCOUNT_PARENT_TYPE.CURRENT_LIABILITY, + balanceSheet: true, + incomeSheet: false, + }, + { + label: 'Credit Card', + key: ACCOUNT_TYPE.CREDIT_CARD, + normal: ACCOUNT_NORMAL.CREDIT, + rootType: ACCOUNT_ROOT_TYPE.LIABILITY, + parentType: ACCOUNT_PARENT_TYPE.CURRENT_LIABILITY, + balanceSheet: true, + incomeSheet: false, + }, + { + label: 'Tax Payable', + key: ACCOUNT_TYPE.TAX_PAYABLE, + normal: ACCOUNT_NORMAL.CREDIT, + rootType: ACCOUNT_ROOT_TYPE.LIABILITY, + parentType: ACCOUNT_PARENT_TYPE.CURRENT_LIABILITY, + balanceSheet: true, + incomeSheet: false, + }, + { + label: 'Other Current Liability', + key: ACCOUNT_TYPE.OTHER_CURRENT_LIABILITY, + normal: ACCOUNT_NORMAL.CREDIT, + rootType: ACCOUNT_ROOT_TYPE.LIABILITY, + parentType: ACCOUNT_PARENT_TYPE.CURRENT_LIABILITY, + balanceSheet: false, + incomeSheet: true, + }, + { + label: 'Long Term Liability', + key: ACCOUNT_TYPE.LOGN_TERM_LIABILITY, + normal: ACCOUNT_NORMAL.CREDIT, + rootType: ACCOUNT_ROOT_TYPE.LIABILITY, + parentType: ACCOUNT_PARENT_TYPE.LOGN_TERM_LIABILITY, + balanceSheet: false, + incomeSheet: true, + }, + { + label: 'Non-Current Liability', + key: ACCOUNT_TYPE.NON_CURRENT_LIABILITY, + normal: ACCOUNT_NORMAL.CREDIT, + rootType: ACCOUNT_ROOT_TYPE.LIABILITY, + parentType: ACCOUNT_PARENT_TYPE.NON_CURRENT_LIABILITY, + balanceSheet: false, + incomeSheet: true, + }, + { + label: 'Equity', + key: ACCOUNT_TYPE.EQUITY, + normal: ACCOUNT_NORMAL.CREDIT, + rootType: ACCOUNT_ROOT_TYPE.EQUITY, + parentType: ACCOUNT_PARENT_TYPE.EQUITY, + balanceSheet: true, + incomeSheet: false, + }, + { + label: 'Income', + key: ACCOUNT_TYPE.INCOME, + normal: ACCOUNT_NORMAL.CREDIT, + rootType: ACCOUNT_ROOT_TYPE.INCOME, + parentType: ACCOUNT_PARENT_TYPE.INCOME, + balanceSheet: false, + incomeSheet: true, + }, + { + label: 'Other Income', + key: ACCOUNT_TYPE.OTHER_INCOME, + normal: ACCOUNT_NORMAL.CREDIT, + rootType: ACCOUNT_ROOT_TYPE.INCOME, + parentType: ACCOUNT_PARENT_TYPE.INCOME, + balanceSheet: false, + incomeSheet: true, + }, + { + label: 'Cost of Goods Sold', + key: ACCOUNT_TYPE.COST_OF_GOODS_SOLD, + normal: ACCOUNT_NORMAL.DEBIT, + rootType: ACCOUNT_ROOT_TYPE.EXPENSE, + parentType: ACCOUNT_PARENT_TYPE.EXPENSE, + balanceSheet: false, + incomeSheet: true, + }, + { + label: 'Expense', + key: ACCOUNT_TYPE.EXPENSE, + normal: ACCOUNT_NORMAL.DEBIT, + rootType: ACCOUNT_ROOT_TYPE.EXPENSE, + parentType: ACCOUNT_PARENT_TYPE.EXPENSE, + balanceSheet: false, + incomeSheet: true, + }, + { + label: 'Other Expense', + key: ACCOUNT_TYPE.OTHER_EXPENSE, + normal: ACCOUNT_NORMAL.DEBIT, + rootType: ACCOUNT_ROOT_TYPE.EXPENSE, + parentType: ACCOUNT_PARENT_TYPE.EXPENSE, + balanceSheet: false, + incomeSheet: true, + }, +]; \ No newline at end of file diff --git a/server/src/database/migrations/20190822214302_create_account_types_table.js b/server/src/database/migrations/20190822214302_create_account_types_table.js deleted file mode 100644 index 4515e3338..000000000 --- a/server/src/database/migrations/20190822214302_create_account_types_table.js +++ /dev/null @@ -1,14 +0,0 @@ - -exports.up = (knex) => { - return knex.schema.createTable('account_types', (table) => { - table.increments(); - 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'); - }).raw('ALTER TABLE `ACCOUNT_TYPES` AUTO_INCREMENT = 1000'); -}; - -exports.down = (knex) => knex.schema.dropTableIfExists('account_types'); diff --git a/server/src/database/migrations/20190822214303_create_accounts_table.js b/server/src/database/migrations/20190822214303_create_accounts_table.js index 3116f8f1b..81abcaf26 100644 --- a/server/src/database/migrations/20190822214303_create_accounts_table.js +++ b/server/src/database/migrations/20190822214303_create_accounts_table.js @@ -3,7 +3,7 @@ exports.up = function (knex) { 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.string('account_type').index(); table.integer('parent_account_id').unsigned().references('id').inTable('accounts'); table.string('code', 10).index(); table.text('description'); diff --git a/server/src/database/seeds/core/20190423085242_seed_accounts.js b/server/src/database/seeds/core/20190423085242_seed_accounts.js index 6d0f86b95..d33868ec9 100644 --- a/server/src/database/seeds/core/20190423085242_seed_accounts.js +++ b/server/src/database/seeds/core/20190423085242_seed_accounts.js @@ -3,30 +3,10 @@ import { get } from 'lodash'; import TenancyService from 'services/Tenancy/TenancyService' import AccountsData from '../data/accounts'; - exports.up = function (knex) { - const tenancyService = Container.get(TenancyService); - const i18n = tenancyService.i18n(knex.userParams.tenantId); - - const accountMapper = (account) => { - return knex('account_types').where('key', account.account_type).first() - .then((accountType) => ({ - name: i18n.__(account.name), - slug: account.slug, - account_type_id: get(accountType, 'id', null), - code: account.code, - description: i18n.__(account.description), - active: 1, - index: 1, - predefined: account.predefined, - })); - }; return knex('accounts').then(async () => { - const accountsPromises = AccountsData.map(accountMapper); - const accounts = await Promise.all(accountsPromises); - // Inserts seed entries. - return knex('accounts').insert([ ...accounts ]); + return knex('accounts').insert([ ...AccountsData ]); }); }; diff --git a/server/src/database/seeds/data/accounts.js b/server/src/database/seeds/data/accounts.js index 2f61ad79a..a0c8382a9 100644 --- a/server/src/database/seeds/data/accounts.js +++ b/server/src/database/seeds/data/accounts.js @@ -44,7 +44,7 @@ export default [ name:'Computer Equipment', slug: 'computer-equipment', code: '10005', - account_type: 'fixed_asset', + account_type: 'fixed-asset', predefined: 0, parent_account_id: null, index: 1, @@ -55,7 +55,7 @@ export default [ name:'Office Equipment', slug: 'office-equipment', code: '10006', - account_type: 'fixed_asset', + account_type: 'fixed-asset', predefined: 0, parent_account_id: null, index: 1, @@ -65,7 +65,7 @@ export default [ { name:'Accounts Receivable (A/R)', slug: 'accounts-receivable', - account_type: 'accounts_receivable', + account_type: 'accounts-receivable', code: '10007', description: '', active: 1, @@ -88,7 +88,7 @@ export default [ { name:'Accounts Payable (A/P)', slug: 'accounts-payable', - account_type: 'accounts_payable', + account_type: 'accounts-payable', parent_account_id: null, code: '20001', description: '', @@ -99,7 +99,7 @@ export default [ { name:'Owner A Drawings', slug: 'owner-drawings', - account_type: 'other_current_liability', + account_type: 'other-current-liability', parent_account_id: null, code: '20002', description:'Withdrawals by the owners.', @@ -110,7 +110,7 @@ export default [ { name:'Loan', slug: 'owner-drawings', - account_type: 'other_current_liability', + account_type: 'other-current-liability', code: '20003', description:'Money that has been borrowed from a creditor.', active: 1, @@ -120,7 +120,7 @@ export default [ { name:'Opening Balance Adjustments', slug: 'opening-balance-adjustments', - account_type: 'other_current_liability', + account_type: 'other-current-liability', code: '20004', description:'This account will hold the difference in the debits and credits entered during the opening balance..', active: 1, @@ -129,8 +129,8 @@ export default [ }, { name:'Revenue Received in Advance', - slug: 'Revenue-received-in-advance', - account_type: 'other_current_liability', + slug: 'revenue-received-in-advance', + account_type: 'other-current-liability', parent_account_id: null, code: '20005', description: 'When customers pay in advance for products/services.', @@ -141,7 +141,7 @@ export default [ { name:'Sales Tax Payable', slug: 'owner-drawings', - account_type: 'tax_payable', + account_type: 'other-current-liability', code: '20006', description: '', active: 1, @@ -206,7 +206,7 @@ export default [ { name:'Cost of Goods Sold', slug: 'cost-of-goods-sold', - account_type: 'cost_of_goods_sold', + account_type: 'cost-of-goods-sold', parent_account_id: null, code: '40002', description:'Tracks the direct cost of the goods sold.', @@ -239,7 +239,7 @@ export default [ { name:'Exchange Gain or Loss', slug: 'exchange-grain-loss', - account_type: 'other_expense', + account_type: 'other-expense', parent_account_id: null, code: '40005', description:'Tracks the gain and losses of the exchange differences.', @@ -307,7 +307,7 @@ export default [ { name:'Other Income', slug: 'other-income', - account_type: 'other_income', + account_type: 'other-income', parent_account_id: null, code: '50004', description:'The income activities are not associated to the core business.', diff --git a/server/src/database/seeds/data/accounts_types.js b/server/src/database/seeds/data/accounts_types.js index b38da463a..bab8ac7f3 100644 --- a/server/src/database/seeds/data/accounts_types.js +++ b/server/src/database/seeds/data/accounts_types.js @@ -105,51 +105,5 @@ export default [ balance_sheet: false, income_sheet: true, }, - { - key: 'equity', - normal: 'credit', - root_type: 'equity', - child_type: 'equity', - balance_sheet: true, - income_sheet: false, - }, - { - key: 'income', - normal: 'credit', - root_type: 'income', - child_type: 'income', - balance_sheet: false, - income_sheet: true, - }, - { - key: 'other_income', - normal: 'credit', - root_type: 'income', - child_type: 'other_income', - balance_sheet: false, - income_sheet: true, - }, - { - key: 'cost_of_goods_sold', - normal: 'debit', - root_type: 'expenses', - child_type: 'expenses', - balance_sheet: false, - income_sheet: true, - }, - { - key: 'expense', - normal: 'debit', - root_type: 'expense', - child_type: 'expense', - balance_sheet: false, - income_sheet: true, - }, - { - key: 'other_expense', - normal: 'debit', - root_type: 'expense', - balance_sheet: false, - income_sheet: true, - }, + ]; \ No newline at end of file diff --git a/server/src/interfaces/Account.ts b/server/src/interfaces/Account.ts index c62fedb03..28e22617e 100644 --- a/server/src/interfaces/Account.ts +++ b/server/src/interfaces/Account.ts @@ -4,7 +4,7 @@ export interface IAccountDTO { name: string, code: string, description: string, - accountTypeId: number, + accountType: string, parentAccountId: number, active: boolean, }; @@ -16,7 +16,7 @@ export interface IAccount { code: string, index: number, description: string, - accountTypeId: number, + accountType: string, parentAccountId: number, active: boolean, predefined: boolean, @@ -31,7 +31,7 @@ export interface IAccountsFilter extends IDynamicListFilterDTO { }; export interface IAccountType { - id: number, + label: string, key: string, label: string, normal: string, diff --git a/server/src/lib/AccountTypes/index.ts b/server/src/lib/AccountTypes/index.ts new file mode 100644 index 000000000..04bbdaf93 --- /dev/null +++ b/server/src/lib/AccountTypes/index.ts @@ -0,0 +1,101 @@ +import { get } from 'lodash'; +import { ACCOUNT_TYPES } from 'data/AccountTypes'; + +export default class AccountTypesUtils { + + /** + * Retrieve account types list. + */ + static getList() { + return ACCOUNT_TYPES; + } + + /** + * + * @param {string} rootType + */ + static getTypesByRootType(rootType: string) { + return ACCOUNT_TYPES.filter((type) => type.rootType === rootType); + } + + /** + * + * @param {string} key + * @param {string} accessor + */ + static getType(key: string, accessor?: string) { + const type = ACCOUNT_TYPES.find((type) => type.key === key); + + if (accessor) { + return get(type, accessor); + } + return type; + } + + /** + * + * @param {string} parentType + */ + static getTypesByParentType(parentType: string) { + return ACCOUNT_TYPES.filter((type) => type.parentType === parentType); + } + + /** + * + * @param {string} normal + */ + static getTypesByNormal(normal: string) { + return ACCOUNT_TYPES.filter((type) => type.normal === normal); + } + + + /** + * + * @param {string} key + * @param {string} rootType + */ + static isRootTypeEqualsKey(key: string, rootType: string) { + return ACCOUNT_TYPES.some((type) => { + const isType = type.key === key; + const isRootType = type.rootType === rootType; + + return isType && isRootType; + }); + } + + /** + * + * @param {string} key + * @param {string} parentType + */ + static isParentTypeEqualsKey(key: string, parentType: string) { + return ACCOUNT_TYPES.some((type) => { + const isType = type.key === key; + const isParentType = type.parentType === parentType; + + return isType && isParentType; + }); + } + + /** + * + * @param {string} key + */ + static isTypeBalanceSheet(key: string) { + return ACCOUNT_TYPES.some((type) => { + const isType = type.key === key; + return isType && type.balanceSheet; + }); + } + + /** + * + * @param {string} key + */ + static isTypePLSheet(key: string) { + return ACCOUNT_TYPES.some((type) => { + const isType = type.key === key; + return isType && type.incomeSheet; + }); + } +} \ No newline at end of file diff --git a/server/src/loaders/tenantModels.ts b/server/src/loaders/tenantModels.ts index 5aa72131f..86adacdab 100644 --- a/server/src/loaders/tenantModels.ts +++ b/server/src/loaders/tenantModels.ts @@ -2,7 +2,6 @@ import { mapValues } from 'lodash'; import Account from 'models/Account'; import AccountTransaction from 'models/AccountTransaction'; -import AccountType from 'models/AccountType'; import Item from 'models/Item'; import ItemEntry from 'models/ItemEntry'; import ItemCategory from 'models/ItemCategory'; @@ -43,7 +42,6 @@ export default (knex) => { Option, Account, AccountTransaction, - AccountType, Item, ItemCategory, ItemEntry, diff --git a/server/src/loaders/tenantRepositories.ts b/server/src/loaders/tenantRepositories.ts index d0bf9e551..58bf49b73 100644 --- a/server/src/loaders/tenantRepositories.ts +++ b/server/src/loaders/tenantRepositories.ts @@ -1,5 +1,4 @@ import AccountRepository from 'repositories/AccountRepository'; -import AccountTypeRepository from 'repositories/AccountTypeRepository'; import VendorRepository from 'repositories/VendorRepository'; import CustomerRepository from 'repositories/CustomerRepository'; import ExpenseRepository from 'repositories/ExpenseRepository'; @@ -18,7 +17,6 @@ export default (knex, cache) => { return { accountRepository: new AccountRepository(knex, cache), transactionsRepository: new AccountTransactionsRepository(knex, cache), - accountTypeRepository: new AccountTypeRepository(knex, cache), customerRepository: new CustomerRepository(knex, cache), vendorRepository: new VendorRepository(knex, cache), contactRepository: new ContactRepository(knex, cache), diff --git a/server/src/locales/en.json b/server/src/locales/en.json index e06d2625a..fc9fb381a 100644 --- a/server/src/locales/en.json +++ b/server/src/locales/en.json @@ -1,5 +1,6 @@ { "Petty Cash": "Petty Cash", + "Cash": "Cash", "Bank": "Bank", "Other Income": "Other Income", "Interest Income": "Interest Income", @@ -9,10 +10,13 @@ "Sales of Product Income": "Sales of Product Income", "Inventory Asset": "Inventory Asset", "Cost of Goods Sold (COGS)": "Cost of Goods Sold (COGS)", + "Cost of Goods Sold": "Cost of Goods Sold", "Accounts Payable": "Accounts Payable", - "Other Expenses": "Other Expenses", + "Other Expense": "Other Expense", "Payroll Expenses": "Payroll Expenses", "Fixed Asset": "Fixed Asset", + "Credit Card": "Credit Card", + "Non-Current Asset": "Non-Current Asset", "Current Asset": "Current Asset", "Other Asset": "Other Asset", "Long Term Liability": "Long Term Liability", @@ -20,14 +24,17 @@ "Other Liability": "Other Liability", "Equity": "Equity", "Expense": "Expense", - "Other Expense": "Other Expense", "Income": "Income", "Accounts Receivable (A/R)": "Accounts Receivable (A/R)", + "Accounts Receivable": "Accounts Receivable", "Accounts Payable (A/P)": "Accounts Payable (A/P)", "Inactive": "Inactive", + "Other Current Asset": "Other Current Asset", + "Tax Payable": "Tax Payable", + "Other Current Liability": "Other Current Liability", + "Non-Current Liability": "Non-Current Liability", "Assets": "Assets", "Liabilities": "Liabilities", - "Expenses": "Expenses", "Account name": "Account name", "Account type": "Account type", "Account normal": "Account normal", diff --git a/server/src/models/Account.js b/server/src/models/Account.js index 721e2dedf..2c5761384 100644 --- a/server/src/models/Account.js +++ b/server/src/models/Account.js @@ -1,6 +1,6 @@ /* eslint-disable global-require */ import { Model } from 'objection'; -import { flatten } from 'lodash'; +import { flatten, castArray } from 'lodash'; import TenantModel from 'models/TenantModel'; import { buildFilterQuery, @@ -8,6 +8,7 @@ import { } from 'lib/ViewRolesBuilder'; import { flatToNestedArray } from 'utils'; import DependencyGraph from 'lib/DependencyGraph'; +import AccountTypesUtils from 'lib/AccountTypes' export default class Account extends TenantModel { /** @@ -24,6 +25,54 @@ export default class Account extends TenantModel { return ['createdAt', 'updatedAt']; } + /** + * Virtual attributes. + */ + static get virtualAttributes() { + return [ + 'accountTypeLabel', + 'accountParentTypeLabel', + 'accountNormal', + 'isBalanceSheetAccount', + 'isPLSheet' + ]; + } + + /** + * Account normal. + */ + get accountNormal() { + return AccountTypesUtils.getType(this.accountType, 'normal'); + } + + /** + * Retrieve account type label. + */ + get accountTypeLabel() { + return AccountTypesUtils.getType(this.accountType, 'label'); + } + + /** + * Retrieve account parent type. + */ + get accountParentType() { + return AccountTypesUtils.getType(this.accountType, 'parentType'); + } + + /** + * Retrieve whether the account is balance sheet account. + */ + get isBalanceSheetAccount() { + return this.isBalanceSheet(); + } + + /** + * Retrieve whether the account is profit/loss sheet account. + */ + get isPLSheet() { + return this.isProfitLossSheet(); + } + /** * Allows to mark model as resourceable to viewable and filterable. */ @@ -61,22 +110,9 @@ export default class Account extends TenantModel { * Relationship mapping. */ static get relationMappings() { - const AccountType = require('models/AccountType'); const AccountTransaction = require('models/AccountTransaction'); return { - /** - * Account model may belongs to account type. - */ - type: { - relation: Model.BelongsToOneRelation, - modelClass: AccountType.default, - join: { - from: 'accounts.accountTypeId', - to: 'account_types.id', - }, - }, - /** * Account model may has many transactions. */ @@ -90,6 +126,58 @@ export default class Account extends TenantModel { }, }; } + + /** + * Detarmines whether the given type equals the account type. + * @param {string} accountType + * @return {boolean} + */ + isAccountType(accountType) { + const types = castArray(accountType); + return types.indexOf(this.accountType) !== -1; + } + + /** + * Detarmines whether the given root type equals the account type. + * @param {string} rootType + * @return {boolean} + */ + isRootType(rootType) { + return AccountTypesUtils.isRootTypeEqualsKey(this.accountType, rootType); + } + + /** + * Detarmine whether the given parent type equals the account type. + * @param {string} parentType + * @return {boolean} + */ + isParentType(parentType) { + return AccountTypesUtils.isParentTypeEqualsKey(this.accountType, parentType); + } + + /** + * Detarmines whether the account is balance sheet account. + * @return {boolean} + */ + isBalanceSheet() { + return AccountTypesUtils.isTypeBalanceSheet(this.accountType); + } + + /** + * Detarmines whether the account is profit/loss account. + * @return {boolean} + */ + isProfitLossSheet() { + return AccountTypesUtils.isTypePLSheet(this.accountType); + } + + /** + * Detarmines whether the account is income statement account + * @return {boolean} + */ + isIncomeSheet() { + return this.isProfitLossSheet(); + } static collectJournalEntries(accounts) { return flatten(accounts.map((account) => account.transactions.map((transaction) => ({ diff --git a/server/src/models/AccountType.js b/server/src/models/AccountType.js deleted file mode 100644 index 8d16b1dee..000000000 --- a/server/src/models/AccountType.js +++ /dev/null @@ -1,80 +0,0 @@ -// import path from 'path'; -import { Model, mixin } from 'objection'; -import TenantModel from 'models/TenantModel'; - -export default class AccountType extends TenantModel { - /** - * Table name. - */ - static get tableName() { - return 'account_types'; - } - - /** - * Virtaul attributes. - */ - static get virtualAttributes() { - return ['label']; - } - - /** - * Allows to mark model as resourceable to viewable and filterable. - */ - static get resourceable() { - return true; - } - - /** - * Translatable lable. - */ - label() { - return AccountType.labels[this.key] || ''; - } - - /** - * Relationship mapping. - */ - static get relationMappings() { - const Account = require('models/Account'); - - return { - /** - * Account type may has many associated accounts. - */ - accounts: { - relation: Model.HasManyRelation, - modelClass: Account.default, - join: { - from: 'account_types.id', - to: 'accounts.accountTypeId', - }, - }, - }; - } - - /** - * Accounts types labels. - */ - static get labels() { - return { - inventory: 'Inventory', - other_current_asset: 'Other Current Asset', - bank: 'Bank Account', - cash: 'Cash', - fixed_asset: 'Fixed Asset', - non_current_asset: 'Non-Current Asset', - accounts_payable: 'Accounts Payable (A/P)', - accounts_receivable: 'Accounts Receivable (A/R)', - credit_card: 'Credit Card', - long_term_liability: 'Long Term Liability', - other_current_liability: 'Other Current Liability', - other_liability: 'Other Liability', - equity: "Equity", - expense: "Expense", - income: "Income", - other_income: "Other Income", - other_expense: "Other Expense", - cost_of_goods_sold: "Cost of Goods Sold (COGS)", - }; - } -} diff --git a/server/src/models/index.js b/server/src/models/index.js index 77cfc23af..5196acd92 100644 --- a/server/src/models/index.js +++ b/server/src/models/index.js @@ -17,7 +17,6 @@ import BillPaymentEntry from './BillPaymentEntry'; import View from './View'; import ItemEntry from './ItemEntry'; import InventoryTransaction from './InventoryTransaction'; -import AccountType from './AccountType'; import InventoryLotCostTracker from './InventoryCostLotTracker'; import Customer from './Customer'; import Contact from './Contact'; @@ -45,7 +44,6 @@ export { ItemEntry, InventoryTransaction, InventoryLotCostTracker, - AccountType, Option, Contact, ExpenseCategory, diff --git a/server/src/repositories/AccountTypeRepository.ts b/server/src/repositories/AccountTypeRepository.ts deleted file mode 100644 index 1b7f4eade..000000000 --- a/server/src/repositories/AccountTypeRepository.ts +++ /dev/null @@ -1,48 +0,0 @@ -import TenantRepository from 'repositories/TenantRepository'; -import { IAccountType } from 'interfaces'; -import { AccountType } from 'models'; - -export default class AccountTypeRepository extends TenantRepository { - /** - * Gets the repository's model. - */ - get model() { - return AccountType.bindKnex(this.knex); - } - - /** - * Retrieve accounts types of the given keys. - * @param {string[]} keys - * @return {Promise} - */ - getByKeys(keys: string[]): Promise { - return super.findWhereIn('key', keys); - } - - /** - * Retrieve account tpy eof the given key. - * @param {string} key - * @return {Promise} - */ - getByKey(key: string): Promise { - return super.findOne({ key }); - } - - /** - * Retrieve accounts types of the given root type. - * @param {string} rootType - * @return {Promise} - */ - getByRootType(rootType: string): Promise { - return super.find({ root_type: rootType }); - } - - /** - * Retrieve accounts types of the given child type. - * @param {string} childType - * @return {Promise} - */ - getByChildType(childType: string): Promise { - return super.find({ child_type: childType }); - } -} \ No newline at end of file diff --git a/server/src/services/Accounts/AccountsService.ts b/server/src/services/Accounts/AccountsService.ts index 2b3be14a3..bebd6ae2a 100644 --- a/server/src/services/Accounts/AccountsService.ts +++ b/server/src/services/Accounts/AccountsService.ts @@ -15,6 +15,7 @@ import { } from 'decorators/eventDispatcher'; import DynamicListingService from 'services/DynamicListing/DynamicListService'; import events from 'subscribers/events'; +import AccountTypesUtils from 'lib/AccountTypes'; const ERRORS = { ACCOUNT_NOT_FOUND: 'account_not_found', @@ -52,17 +53,13 @@ export default class AccountsService { * @param {number} accountTypeId - * @return {IAccountType} */ - private async getAccountTypeOrThrowError( - tenantId: number, - accountTypeId: number + private getAccountTypeOrThrowError( + accountTypeKey: string ) { - const { accountTypeRepository } = this.tenancy.repositories(tenantId); - this.logger.info('[accounts] validating account type existance.', { - tenantId, - accountTypeId, + accountTypeKey }); - const accountType = await accountTypeRepository.findOneById(accountTypeId); + const accountType = AccountTypesUtils.getType(accountTypeKey); if (!accountType) { this.logger.info('[accounts] account type not found.'); @@ -153,7 +150,7 @@ export default class AccountsService { accountDTO: IAccountDTO, parentAccount: IAccount ) { - if (accountDTO.accountTypeId !== parentAccount.accountTypeId) { + if (accountDTO.accountType !== parentAccount.accountType) { throw new ServiceError(ERRORS.PARENT_ACCOUNT_HAS_DIFFERENT_TYPE); } } @@ -193,7 +190,7 @@ export default class AccountsService { oldAccount: IAccount | IAccountDTO, newAccount: IAccount | IAccountDTO ) { - if (oldAccount.accountTypeId !== newAccount.accountTypeId) { + if (oldAccount.accountType !== newAccount.accountType) { throw new ServiceError(ERRORS.ACCOUNT_TYPE_NOT_ALLOWED_TO_CHANGE); } } @@ -244,7 +241,7 @@ export default class AccountsService { if (accountDTO.code) { await this.isAccountCodeUniqueOrThrowError(tenantId, accountDTO.code); } - await this.getAccountTypeOrThrowError(tenantId, accountDTO.accountTypeId); + this.getAccountTypeOrThrowError(accountDTO.accountType); if (accountDTO.parentAccountId) { const parentAccount = await this.getParentAccountOrThrowError( @@ -638,7 +635,6 @@ export default class AccountsService { filter, }); const accounts = await Account.query().onBuild((builder) => { - builder.withGraphFetched('type'); dynamicList.buildQuery()(builder); }); @@ -673,10 +669,8 @@ export default class AccountsService { toAccountId, deleteAfterClosing, }); - const { AccountTransaction } = this.tenancy.models(tenantId); const { - accountTypeRepository, accountRepository, } = this.tenancy.repositories(tenantId); @@ -685,14 +679,7 @@ export default class AccountsService { this.throwErrorIfAccountPredefined(account); - const accountType = await accountTypeRepository.findOneById( - account.accountTypeId - ); - const toAccountType = await accountTypeRepository.findOneById( - toAccount.accountTypeId - ); - - if (accountType.rootType !== toAccountType.rootType) { + if (account.accountType !== toAccount.accountType) { throw new ServiceError(ERRORS.CLOSE_ACCOUNT_AND_TO_ACCOUNT_NOT_SAME_TYPE); } const updateAccountBalanceOper = await accountRepository.balanceChange( diff --git a/server/src/services/Accounts/AccountsTypesServices.ts b/server/src/services/Accounts/AccountsTypesServices.ts index 4acb11b17..3545d819a 100644 --- a/server/src/services/Accounts/AccountsTypesServices.ts +++ b/server/src/services/Accounts/AccountsTypesServices.ts @@ -1,20 +1,14 @@ -import { Inject, Service } from 'typedi'; -import { omit } from 'lodash'; -import TenancyService from 'services/Tenancy/TenancyService'; +import { Service } from 'typedi'; import { IAccountsTypesService, IAccountType } from 'interfaces'; +import AccountTypesUtils from 'lib/AccountTypes'; @Service() export default class AccountsTypesService implements IAccountsTypesService { - @Inject() - tenancy: TenancyService; - /** * Retrieve all accounts types. - * @param {number} tenantId - - * @return {Promise} + * @return {IAccountType} */ - async getAccountsTypes(tenantId: number): Promise { - const { accountTypeRepository } = this.tenancy.repositories(tenantId); - return accountTypeRepository.all(); + getAccountsTypes(): IAccountType[] { + return AccountTypesUtils.getList(); } } diff --git a/server/src/services/Expenses/ExpensesService.ts b/server/src/services/Expenses/ExpensesService.ts index bf064a960..ba5ac0324 100644 --- a/server/src/services/Expenses/ExpensesService.ts +++ b/server/src/services/Expenses/ExpensesService.ts @@ -21,6 +21,7 @@ import { import DynamicListingService from 'services/DynamicListing/DynamicListService'; import events from 'subscribers/events'; import ContactsService from 'services/Contacts/ContactsService'; +import { ACCOUNT_PARENT_TYPE, ACCOUNT_ROOT_TYPE } from 'data/AccountTypes' const ERRORS = { EXPENSE_NOT_FOUND: 'expense_not_found', @@ -153,17 +154,10 @@ export default class ExpensesService implements IExpensesService { tenantId, expensesAccounts, }); - - const { accountTypeRepository } = this.tenancy.repositories(tenantId); - - // Retrieve accounts types of the given root type. - const expensesTypes = await accountTypeRepository.getByRootType('expense'); - - const expensesTypesIds = expensesTypes.map((t) => t.id); const invalidExpenseAccounts: number[] = []; expensesAccounts.forEach((expenseAccount) => { - if (expensesTypesIds.indexOf(expenseAccount.accountTypeId) === -1) { + if (expenseAccount.isRootType(ACCOUNT_ROOT_TYPE.EXPENSE)) { invalidExpenseAccounts.push(expenseAccount.id); } }); @@ -187,16 +181,7 @@ export default class ExpensesService implements IExpensesService { paymentAccount, }); - const { accountTypeRepository } = this.tenancy.repositories(tenantId); - - // Retrieve account tpy eof the given key. - const validAccountsType = await accountTypeRepository.getByKeys([ - 'current_asset', - 'fixed_asset', - ]); - const validAccountsTypeIds = validAccountsType.map((t) => t.id); - - if (validAccountsTypeIds.indexOf(paymentAccount.accountTypeId) === -1) { + if (paymentAccount.isParentType(ACCOUNT_PARENT_TYPE.CURRENT_ASSET)) { this.logger.info( '[expenses] the given payment account has invalid type', { tenantId, paymentAccount } @@ -270,7 +255,6 @@ export default class ExpensesService implements IExpensesService { tenantId, expenseId, }); - // Retrieve the given expense by id. const expense = await expenseRepository.findOneById(expenseId); diff --git a/server/src/services/ItemCategories/ItemCategoriesService.ts b/server/src/services/ItemCategories/ItemCategoriesService.ts index cd721ccfd..96a3c615c 100644 --- a/server/src/services/ItemCategories/ItemCategoriesService.ts +++ b/server/src/services/ItemCategories/ItemCategoriesService.ts @@ -16,6 +16,7 @@ import { import DynamicListingService from 'services/DynamicListing/DynamicListService'; import TenancyService from 'services/Tenancy/TenancyService'; import events from 'subscribers/events'; +import { ACCOUNT_ROOT_TYPE, ACCOUNT_TYPE } from 'data/AccountTypes'; const ERRORS = { ITEM_CATEGORIES_NOT_FOUND: 'ITEM_CATEGORIES_NOT_FOUND', @@ -172,16 +173,12 @@ export default class ItemCategoriesService implements IItemCategoriesService { * @return {Promise} */ private async validateSellAccount(tenantId: number, sellAccountId: number) { - const { - accountRepository, - accountTypeRepository, - } = this.tenancy.repositories(tenantId); + const { accountRepository } = this.tenancy.repositories(tenantId); this.logger.info('[items] validate sell account existance.', { tenantId, sellAccountId, }); - const incomeType = await accountTypeRepository.getByKey('income'); const foundAccount = await accountRepository.findOneById(sellAccountId); if (!foundAccount) { @@ -190,7 +187,7 @@ export default class ItemCategoriesService implements IItemCategoriesService { sellAccountId, }); throw new ServiceError(ERRORS.SELL_ACCOUNT_NOT_FOUND); - } else if (foundAccount.accountTypeId !== incomeType.id) { + } else if (!foundAccount.isRootType(ACCOUNT_ROOT_TYPE.INCOME)) { this.logger.info('[items] sell account not income type.', { tenantId, sellAccountId, @@ -206,16 +203,12 @@ export default class ItemCategoriesService implements IItemCategoriesService { * @return {Promise} */ private async validateCostAccount(tenantId: number, costAccountId: number) { - const { - accountRepository, - accountTypeRepository, - } = this.tenancy.repositories(tenantId); + const { accountRepository } = this.tenancy.repositories(tenantId); this.logger.info('[items] validate cost account existance.', { tenantId, costAccountId, }); - const COGSType = await accountTypeRepository.getByKey('cost_of_goods_sold'); const foundAccount = await accountRepository.findOneById(costAccountId); if (!foundAccount) { @@ -224,7 +217,7 @@ export default class ItemCategoriesService implements IItemCategoriesService { costAccountId, }); throw new ServiceError(ERRORS.COST_ACCOUNT_NOT_FOUMD); - } else if (foundAccount.accountTypeId !== COGSType.id) { + } else if (!foundAccount.isRootType(ACCOUNT_ROOT_TYPE.EXPENSE)) { this.logger.info('[items] validate cost account not COGS type.', { tenantId, costAccountId, @@ -243,16 +236,12 @@ export default class ItemCategoriesService implements IItemCategoriesService { tenantId: number, inventoryAccountId: number ) { - const { - accountTypeRepository, - accountRepository, - } = this.tenancy.repositories(tenantId); + const { accountRepository } = this.tenancy.repositories(tenantId); this.logger.info('[items] validate inventory account existance.', { tenantId, inventoryAccountId, }); - const otherAsset = await accountTypeRepository.getByKey('other_asset'); const foundAccount = await accountRepository.findOneById( inventoryAccountId ); @@ -263,7 +252,7 @@ export default class ItemCategoriesService implements IItemCategoriesService { inventoryAccountId, }); throw new ServiceError(ERRORS.INVENTORY_ACCOUNT_NOT_FOUND); - } else if (otherAsset.id !== foundAccount.accountTypeId) { + } else if (!foundAccount.isAccountType(ACCOUNT_TYPE.INVENTORY)) { this.logger.info('[items] inventory account not inventory type.', { tenantId, inventoryAccountId, diff --git a/server/src/services/Items/ItemsService.ts b/server/src/services/Items/ItemsService.ts index fe25f5c1f..2508a2594 100644 --- a/server/src/services/Items/ItemsService.ts +++ b/server/src/services/Items/ItemsService.ts @@ -1,4 +1,4 @@ -import { defaultTo, difference, omit, pick } from 'lodash'; +import { defaultTo, difference } from 'lodash'; import { Service, Inject } from 'typedi'; import { EventDispatcher, @@ -10,7 +10,7 @@ import DynamicListingService from 'services/DynamicListing/DynamicListService'; import TenancyService from 'services/Tenancy/TenancyService'; import { ServiceError } from 'exceptions'; import InventoryService from 'services/Inventory/Inventory'; -import InventoryAdjustmentEntry from 'models/InventoryAdjustmentEntry'; +import { ACCOUNT_ROOT_TYPE, ACCOUNT_TYPE } from 'data/AccountTypes' const ERRORS = { NOT_FOUND: 'NOT_FOUND', @@ -29,7 +29,8 @@ const ERRORS = { ITEMS_HAVE_ASSOCIATED_TRANSACTIONS: 'ITEMS_HAVE_ASSOCIATED_TRANSACTIONS', ITEM_HAS_ASSOCIATED_TRANSACTINS: 'ITEM_HAS_ASSOCIATED_TRANSACTINS', - ITEM_HAS_ASSOCIATED_INVENTORY_ADJUSTMENT: 'ITEM_HAS_ASSOCIATED_INVENTORY_ADJUSTMENT', + ITEM_HAS_ASSOCIATED_INVENTORY_ADJUSTMENT: + 'ITEM_HAS_ASSOCIATED_INVENTORY_ADJUSTMENT', }; @Service() @@ -114,16 +115,12 @@ export default class ItemsService implements IItemsService { tenantId: number, costAccountId: number ): Promise { - const { - accountRepository, - accountTypeRepository, - } = this.tenancy.repositories(tenantId); + const { accountRepository } = this.tenancy.repositories(tenantId); this.logger.info('[items] validate cost account existance.', { tenantId, costAccountId, }); - const COGSType = await accountTypeRepository.getByKey('cost_of_goods_sold'); const foundAccount = await accountRepository.findOneById(costAccountId); if (!foundAccount) { @@ -132,7 +129,9 @@ export default class ItemsService implements IItemsService { costAccountId, }); throw new ServiceError(ERRORS.COST_ACCOUNT_NOT_FOUMD); - } else if (foundAccount.accountTypeId !== COGSType.id) { + + // Detarmines the cost of goods sold account. + } else if (foundAccount.isRootType(ACCOUNT_ROOT_TYPE.EXPENSE)) { this.logger.info('[items] validate cost account not COGS type.', { tenantId, costAccountId, @@ -152,14 +151,13 @@ export default class ItemsService implements IItemsService { ) { const { accountRepository, - accountTypeRepository, } = this.tenancy.repositories(tenantId); this.logger.info('[items] validate sell account existance.', { tenantId, sellAccountId, }); - const incomeType = await accountTypeRepository.getByKey('income'); + const foundAccount = await accountRepository.findOneById(sellAccountId); if (!foundAccount) { @@ -168,7 +166,8 @@ export default class ItemsService implements IItemsService { sellAccountId, }); throw new ServiceError(ERRORS.SELL_ACCOUNT_NOT_FOUND); - } else if (foundAccount.accountTypeId !== incomeType.id) { + + } else if (!foundAccount.isRootType(ACCOUNT_ROOT_TYPE.INCOME)) { this.logger.info('[items] sell account not income type.', { tenantId, sellAccountId, @@ -187,7 +186,6 @@ export default class ItemsService implements IItemsService { inventoryAccountId: number ) { const { - accountTypeRepository, accountRepository, } = this.tenancy.repositories(tenantId); @@ -195,7 +193,6 @@ export default class ItemsService implements IItemsService { tenantId, inventoryAccountId, }); - const otherAsset = await accountTypeRepository.getByKey('other_asset'); const foundAccount = await accountRepository.findOneById( inventoryAccountId ); @@ -206,7 +203,8 @@ export default class ItemsService implements IItemsService { inventoryAccountId, }); throw new ServiceError(ERRORS.INVENTORY_ACCOUNT_NOT_FOUND); - } else if (otherAsset.id !== foundAccount.accountTypeId) { + + } else if (foundAccount.isAccountType(ACCOUNT_TYPE.INVENTORY)) { this.logger.info('[items] inventory account not inventory type.', { tenantId, inventoryAccountId, @@ -361,11 +359,11 @@ export default class ItemsService implements IItemsService { await this.getItemOrThrowError(tenantId, itemId); // Validate the item has no associated inventory transactions. - await this.validateHasNoInventoryAdjustments(tenantId, itemId); + await this.validateHasNoInventoryAdjustments(tenantId, itemId); // Validate the item has no associated invoices or bills. await this.validateHasNoInvoicesOrBills(tenantId, itemId); - + await Item.query().findById(itemId).delete(); this.logger.info('[items] deleted successfully.', { tenantId, itemId }); @@ -479,7 +477,7 @@ export default class ItemsService implements IItemsService { await this.validateItemsIdsExists(tenantId, itemsIds); // Validate the item has no associated inventory transactions. - await this.validateHasNoInventoryAdjustments(tenantId, itemsIds); + await this.validateHasNoInventoryAdjustments(tenantId, itemsIds); // Validate the items have no associated invoices or bills. await this.validateHasNoInvoicesOrBills(tenantId, itemsIds); @@ -554,14 +552,15 @@ export default class ItemsService implements IItemsService { */ private async validateHasNoInventoryAdjustments( tenantId: number, - itemId: number[] | number, + itemId: number[] | number ): Promise { const { InventoryAdjustmentEntry } = this.tenancy.models(tenantId); const itemsIds = Array.isArray(itemId) ? itemId : [itemId]; - const inventoryAdjEntries = await InventoryAdjustmentEntry.query() - .whereIn('item_id', itemsIds); - + const inventoryAdjEntries = await InventoryAdjustmentEntry.query().whereIn( + 'item_id', + itemsIds + ); if (inventoryAdjEntries.length > 0) { throw new ServiceError(ERRORS.ITEM_HAS_ASSOCIATED_INVENTORY_ADJUSTMENT); } diff --git a/server/src/services/Purchases/BillPayments.ts b/server/src/services/Purchases/BillPayments.ts index 8a3a52aed..91c36155b 100644 --- a/server/src/services/Purchases/BillPayments.ts +++ b/server/src/services/Purchases/BillPayments.ts @@ -25,13 +25,15 @@ import TenancyService from 'services/Tenancy/TenancyService'; import DynamicListingService from 'services/DynamicListing/DynamicListService'; import { entriesAmountDiff, formatDateFields } from 'utils'; import { ServiceError } from 'exceptions'; +import { ACCOUNT_PARENT_TYPE } from 'data/AccountTypes'; const ERRORS = { BILL_VENDOR_NOT_FOUND: 'VENDOR_NOT_FOUND', PAYMENT_MADE_NOT_FOUND: 'PAYMENT_MADE_NOT_FOUND', BILL_PAYMENT_NUMBER_NOT_UNQIUE: 'BILL_PAYMENT_NUMBER_NOT_UNQIUE', PAYMENT_ACCOUNT_NOT_FOUND: 'PAYMENT_ACCOUNT_NOT_FOUND', - PAYMENT_ACCOUNT_NOT_CURRENT_ASSET_TYPE: 'PAYMENT_ACCOUNT_NOT_CURRENT_ASSET_TYPE', + PAYMENT_ACCOUNT_NOT_CURRENT_ASSET_TYPE: + 'PAYMENT_ACCOUNT_NOT_CURRENT_ASSET_TYPE', BILL_ENTRIES_IDS_NOT_FOUND: 'BILL_ENTRIES_IDS_NOT_FOUND', BILL_PAYMENT_ENTRIES_NOT_FOUND: 'BILL_PAYMENT_ENTRIES_NOT_FOUND', INVALID_BILL_PAYMENT_AMOUNT: 'INVALID_BILL_PAYMENT_AMOUNT', @@ -63,9 +65,9 @@ export default class BillPaymentsService { /** * Validate whether the bill payment vendor exists on the storage. - * @param {Request} req - * @param {Response} res - * @param {Function} next + * @param {Request} req + * @param {Response} res + * @param {Function} next */ private async getVendorOrThrowError(tenantId: number, vendorId: number) { const { vendorRepository } = this.tenancy.repositories(tenantId); @@ -74,18 +76,21 @@ export default class BillPaymentsService { const vendor = await vendorRepository.findOneById(vendorId); if (!vendor) { - throw new ServiceError(ERRORS.BILL_VENDOR_NOT_FOUND) + throw new ServiceError(ERRORS.BILL_VENDOR_NOT_FOUND); } return vendor; } /** - * Validates the bill payment existance. - * @param {Request} req - * @param {Response} res - * @param {Function} next + * Validates the bill payment existance. + * @param {Request} req + * @param {Response} res + * @param {Function} next */ - private async getPaymentMadeOrThrowError(tenantid: number, paymentMadeId: number) { + private async getPaymentMadeOrThrowError( + tenantid: number, + paymentMadeId: number + ) { const { BillPayment } = this.tenancy.models(tenantid); const billPayment = await BillPayment.query() .withGraphFetched('entries') @@ -98,23 +103,25 @@ export default class BillPaymentsService { } /** - * Validates the payment account. - * @param {number} tenantId - + * Validates the payment account. + * @param {number} tenantId - * @param {number} paymentAccountId * @return {Promise} */ - private async getPaymentAccountOrThrowError(tenantId: number, paymentAccountId: number) { - const { accountTypeRepository, accountRepository } = this.tenancy.repositories(tenantId); + private async getPaymentAccountOrThrowError( + tenantId: number, + paymentAccountId: number + ) { + const { accountRepository } = this.tenancy.repositories(tenantId); - const currentAssetTypes = await accountTypeRepository.getByChildType('current_asset'); - const paymentAccount = await accountRepository.findOneById(paymentAccountId); - - const currentAssetTypesIds = currentAssetTypes.map(type => type.id); + const paymentAccount = await accountRepository.findOneById( + paymentAccountId + ); if (!paymentAccount) { throw new ServiceError(ERRORS.PAYMENT_ACCOUNT_NOT_FOUND); } - if (currentAssetTypesIds.indexOf(paymentAccount.accountTypeId) === -1) { + if (!paymentAccount.isParentType(ACCOUNT_PARENT_TYPE.CURRENT_ASSET)) { throw new ServiceError(ERRORS.PAYMENT_ACCOUNT_NOT_CURRENT_ASSET_TYPE); } return paymentAccount; @@ -122,35 +129,44 @@ export default class BillPaymentsService { /** * Validates the payment number uniqness. - * @param {number} tenantId - - * @param {string} paymentMadeNumber - + * @param {number} tenantId - + * @param {string} paymentMadeNumber - * @return {Promise} */ - private async validatePaymentNumber(tenantId: number, paymentMadeNumber: string, notPaymentMadeId?: number) { + private async validatePaymentNumber( + tenantId: number, + paymentMadeNumber: string, + notPaymentMadeId?: number + ) { const { BillPayment } = this.tenancy.models(tenantId); - - const foundBillPayment = await BillPayment.query() - .onBuild((builder: any) => { + + const foundBillPayment = await BillPayment.query().onBuild( + (builder: any) => { builder.findOne('payment_number', paymentMadeNumber); if (notPaymentMadeId) { builder.whereNot('id', notPaymentMadeId); } - }); - + } + ); + if (foundBillPayment) { - throw new ServiceError(ERRORS.BILL_PAYMENT_NUMBER_NOT_UNQIUE) + throw new ServiceError(ERRORS.BILL_PAYMENT_NUMBER_NOT_UNQIUE); } return foundBillPayment; } /** * Validate whether the entries bills ids exist on the storage. - * @param {Request} req - * @param {Response} res - * @param {NextFunction} next + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next */ - private async validateBillsExistance(tenantId: number, billPaymentEntries: IBillPaymentEntry[], vendorId: number) { + private async validateBillsExistance( + tenantId: number, + billPaymentEntries: IBillPaymentEntry[], + vendorId: number + ) { const { Bill } = this.tenancy.models(tenantId); const entriesBillsIds = billPaymentEntries.map((e: any) => e.billId); @@ -162,15 +178,15 @@ export default class BillPaymentsService { const notFoundBillsIds = difference(entriesBillsIds, storedBillsIds); if (notFoundBillsIds.length > 0) { - throw new ServiceError(ERRORS.BILL_ENTRIES_IDS_NOT_FOUND) + throw new ServiceError(ERRORS.BILL_ENTRIES_IDS_NOT_FOUND); } } /** * Validate wether the payment amount bigger than the payable amount. - * @param {Request} req - * @param {Response} res - * @param {NextFunction} next + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next * @return {void} */ private async validateBillsDueAmount( @@ -179,22 +195,28 @@ export default class BillPaymentsService { oldPaymentEntries: IBillPaymentEntry[] = [] ) { const { Bill } = this.tenancy.models(tenantId); - const billsIds = billPaymentEntries.map((entry: IBillPaymentEntryDTO) => entry.billId); + const billsIds = billPaymentEntries.map( + (entry: IBillPaymentEntryDTO) => entry.billId + ); const storedBills = await Bill.query().whereIn('id', billsIds); const storedBillsMap = new Map( - storedBills - .map((bill) => { - const oldEntries = oldPaymentEntries.filter(entry => entry.billId === bill.id); - const oldPaymentAmount = sumBy(oldEntries, 'paymentAmount') || 0; + storedBills.map((bill) => { + const oldEntries = oldPaymentEntries.filter( + (entry) => entry.billId === bill.id + ); + const oldPaymentAmount = sumBy(oldEntries, 'paymentAmount') || 0; - return [bill.id, { ...bill, dueAmount: bill.dueAmount + oldPaymentAmount }]; - }), + return [ + bill.id, + { ...bill, dueAmount: bill.dueAmount + oldPaymentAmount }, + ]; + }) ); - interface invalidPaymentAmountError{ - index: number, - due_amount: number - }; + interface invalidPaymentAmountError { + index: number; + due_amount: number; + } const hasWrongPaymentAmount: invalidPaymentAmountError[] = []; billPaymentEntries.forEach((entry: IBillPaymentEntryDTO, index: number) => { @@ -203,7 +225,7 @@ export default class BillPaymentsService { if (dueAmount < entry.paymentAmount) { hasWrongPaymentAmount.push({ index, due_amount: dueAmount }); - } + } }); if (hasWrongPaymentAmount.length > 0) { throw new ServiceError(ERRORS.INVALID_BILL_PAYMENT_AMOUNT); @@ -211,9 +233,9 @@ export default class BillPaymentsService { } /** - * Validate the payment receive entries IDs existance. - * @param {Request} req - * @param {Response} res + * Validate the payment receive entries IDs existance. + * @param {Request} req + * @param {Response} res * @return {Response} */ private async validateEntriesIdsExistance( @@ -227,9 +249,12 @@ export default class BillPaymentsService { .filter((entry: any) => entry.id) .map((entry: any) => entry.id); - const storedEntries = await BillPaymentEntry.query().where('bill_payment_id', billPaymentId); + const storedEntries = await BillPaymentEntry.query().where( + 'bill_payment_id', + billPaymentId + ); - const storedEntriesIds = storedEntries.map((entry: any) => entry.id); + const storedEntriesIds = storedEntries.map((entry: any) => entry.id); const notFoundEntriesIds = difference(entriesIds, storedEntriesIds); if (notFoundEntriesIds.length > 0) { @@ -256,7 +281,10 @@ export default class BillPaymentsService { tenantId: number, billPaymentDTO: IBillPaymentDTO ): Promise { - this.logger.info('[paymentDate] trying to save payment made.', { tenantId, billPaymentDTO }); + this.logger.info('[paymentDate] trying to save payment made.', { + tenantId, + billPaymentDTO, + }); const { BillPayment } = this.tenancy.models(tenantId); const billPaymentObj = { @@ -268,28 +296,39 @@ export default class BillPaymentsService { await this.getVendorOrThrowError(tenantId, billPaymentObj.vendorId); // Validate the payment account existance and type. - await this.getPaymentAccountOrThrowError(tenantId, billPaymentObj.paymentAccountId); + await this.getPaymentAccountOrThrowError( + tenantId, + billPaymentObj.paymentAccountId + ); // Validate the payment number uniquiness. if (billPaymentObj.paymentNumber) { await this.validatePaymentNumber(tenantId, billPaymentObj.paymentNumber); } // Validates the bills existance and associated to the given vendor. - await this.validateBillsExistance(tenantId, billPaymentObj.entries, billPaymentDTO.vendorId); + await this.validateBillsExistance( + tenantId, + billPaymentObj.entries, + billPaymentDTO.vendorId + ); // Validates the bills due payment amount. await this.validateBillsDueAmount(tenantId, billPaymentObj.entries); - const billPayment = await BillPayment.query() - .insertGraphAndFetch({ - ...omit(billPaymentObj, ['entries']), - entries: billPaymentDTO.entries, - }); + const billPayment = await BillPayment.query().insertGraphAndFetch({ + ...omit(billPaymentObj, ['entries']), + entries: billPaymentDTO.entries, + }); await this.eventDispatcher.dispatch(events.billPayment.onCreated, { - tenantId, billPayment, billPaymentId: billPayment.id, + tenantId, + billPayment, + billPaymentId: billPayment.id, + }); + this.logger.info('[payment_made] inserted successfully.', { + tenantId, + billPaymentId: billPayment.id, }); - this.logger.info('[payment_made] inserted successfully.', { tenantId, billPaymentId: billPayment.id, }); return billPayment; } @@ -315,11 +354,14 @@ export default class BillPaymentsService { public async editBillPayment( tenantId: number, billPaymentId: number, - billPaymentDTO, + billPaymentDTO ): Promise { const { BillPayment } = this.tenancy.models(tenantId); - const oldBillPayment = await this.getPaymentMadeOrThrowError(tenantId, billPaymentId); + const oldBillPayment = await this.getPaymentMadeOrThrowError( + tenantId, + billPaymentId + ); const billPaymentObj = { amount: sumBy(billPaymentDTO.entries, 'paymentAmount'), @@ -330,31 +372,57 @@ export default class BillPaymentsService { await this.getVendorOrThrowError(tenantId, billPaymentObj.vendorId); // Validate the payment account existance and type. - await this.getPaymentAccountOrThrowError(tenantId, billPaymentObj.paymentAccountId); + await this.getPaymentAccountOrThrowError( + tenantId, + billPaymentObj.paymentAccountId + ); // Validate the items entries IDs existance on the storage. - await this.validateEntriesIdsExistance(tenantId, billPaymentId, billPaymentObj.entries); - + await this.validateEntriesIdsExistance( + tenantId, + billPaymentId, + billPaymentObj.entries + ); + // Validate the bills existance and associated to the given vendor. - await this.validateBillsExistance(tenantId, billPaymentObj.entries, billPaymentDTO.vendorId); + await this.validateBillsExistance( + tenantId, + billPaymentObj.entries, + billPaymentDTO.vendorId + ); // Validates the bills due payment amount. - await this.validateBillsDueAmount(tenantId, billPaymentObj.entries, oldBillPayment.entries); + await this.validateBillsDueAmount( + tenantId, + billPaymentObj.entries, + oldBillPayment.entries + ); // Validate the payment number uniquiness. if (billPaymentObj.paymentNumber) { - await this.validatePaymentNumber(tenantId, billPaymentObj.paymentNumber, billPaymentId); + await this.validatePaymentNumber( + tenantId, + billPaymentObj.paymentNumber, + billPaymentId + ); } - const billPayment = await BillPayment.query() - .upsertGraphAndFetch({ - id: billPaymentId, - ...omit(billPaymentObj, ['entries']), - entries: billPaymentDTO.entries, - }); - await this.eventDispatcher.dispatch(events.billPayment.onEdited, { - tenantId, billPaymentId, billPayment, oldBillPayment, + const billPayment = await BillPayment.query().upsertGraphAndFetch({ + id: billPaymentId, + ...omit(billPaymentObj, ['entries']), + entries: billPaymentDTO.entries, + }); + await this.eventDispatcher.dispatch(events.billPayment.onEdited, { + tenantId, + billPaymentId, + billPayment, + oldBillPayment, + }); + this.logger.info('[bill_payment] edited successfully.', { + tenantId, + billPaymentId, + billPayment, + oldBillPayment, }); - this.logger.info('[bill_payment] edited successfully.', { tenantId, billPaymentId, billPayment, oldBillPayment }); return billPayment; } @@ -367,15 +435,30 @@ export default class BillPaymentsService { */ public async deleteBillPayment(tenantId: number, billPaymentId: number) { const { BillPayment, BillPaymentEntry } = this.tenancy.models(tenantId); - - this.logger.info('[bill_payment] trying to delete.', { tenantId, billPaymentId }); - const oldBillPayment = await this.getPaymentMadeOrThrowError(tenantId, billPaymentId); - await BillPaymentEntry.query().where('bill_payment_id', billPaymentId).delete(); + this.logger.info('[bill_payment] trying to delete.', { + tenantId, + billPaymentId, + }); + const oldBillPayment = await this.getPaymentMadeOrThrowError( + tenantId, + billPaymentId + ); + + await BillPaymentEntry.query() + .where('bill_payment_id', billPaymentId) + .delete(); await BillPayment.query().where('id', billPaymentId).delete(); - await this.eventDispatcher.dispatch(events.billPayment.onDeleted, { tenantId, billPaymentId, oldBillPayment }); - this.logger.info('[bill_payment] deleted successfully.', { tenantId, billPaymentId }); + await this.eventDispatcher.dispatch(events.billPayment.onDeleted, { + tenantId, + billPaymentId, + oldBillPayment, + }); + this.logger.info('[bill_payment] deleted successfully.', { + tenantId, + billPaymentId, + }); } /** @@ -386,7 +469,10 @@ export default class BillPaymentsService { public async getPaymentBills(tenantId: number, billPaymentId: number) { const { Bill } = this.tenancy.models(tenantId); - const billPayment = await this.getPaymentMadeOrThrowError(tenantId, billPaymentId); + const billPayment = await this.getPaymentMadeOrThrowError( + tenantId, + billPaymentId + ); const paymentBillsIds = billPayment.entries.map((entry) => entry.id); const bills = await Bill.query().whereIn('id', paymentBillsIds); @@ -396,11 +482,14 @@ export default class BillPaymentsService { /** * Records bill payment receive journal transactions. - * @param {number} tenantId - + * @param {number} tenantId - * @param {BillPayment} billPayment * @param {Integer} billPaymentId */ - public async recordJournalEntries(tenantId: number, billPayment: IBillPayment) { + public async recordJournalEntries( + tenantId: number, + billPayment: IBillPayment + ) { const { AccountTransaction } = this.tenancy.models(tenantId); const { accountRepository } = this.tenancy.repositories(tenantId); @@ -408,7 +497,9 @@ export default class BillPaymentsService { const formattedDate = moment(billPayment.paymentDate).format('YYYY-MM-DD'); // Retrieve AP account from the storage. - const payableAccount = await accountRepository.findOne({ slug: 'accounts-payable' }); + const payableAccount = await accountRepository.findOne({ + slug: 'accounts-payable', + }); const journal = new JournalPoster(tenantId); const commonJournal = { @@ -451,44 +542,50 @@ export default class BillPaymentsService { /** * Reverts bill payment journal entries. - * @param {number} tenantId - * @param {number} billPaymentId + * @param {number} tenantId + * @param {number} billPaymentId * @return {Promise} */ public async revertJournalEntries(tenantId: number, billPaymentId: number) { const journal = new JournalPoster(tenantId); const journalCommands = new JournalCommands(journal); - + await journalCommands.revertJournalEntries(billPaymentId, 'BillPayment'); - - return Promise.all([ - journal.saveBalance(), - journal.deleteEntries(), - ]); + + return Promise.all([journal.saveBalance(), journal.deleteEntries()]); } /** * Retrieve bill payment paginted and filterable list. - * @param {number} tenantId - * @param {IBillPaymentsFilter} billPaymentsFilter + * @param {number} tenantId + * @param {IBillPaymentsFilter} billPaymentsFilter */ public async listBillPayments( tenantId: number, - billPaymentsFilter: IBillPaymentsFilter, - ): Promise<{ billPayments: IBillPayment, pagination: IPaginationMeta, filterMeta: IFilterMeta }> { + 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, + 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, @@ -501,10 +598,13 @@ export default class BillPaymentsService { * @param {number} billPaymentId - The bill payment id. * @return {object} */ - public async getBillPayment(tenantId: number, billPaymentId: number): Promise<{ - billPayment: IBillPayment, - payableBills: IBill[], - paymentMadeBills: IBill[], + public async getBillPayment( + tenantId: number, + billPaymentId: number + ): Promise<{ + billPayment: IBillPayment; + payableBills: IBill[]; + paymentMadeBills: IBill[]; }> { const { BillPayment, Bill } = this.tenancy.models(tenantId); const billPayment = await BillPayment.query() @@ -527,8 +627,8 @@ export default class BillPaymentsService { // Retrieve all payment made assocaited bills. const paymentMadeBills = billPayment.entries.map((entry) => ({ - ...(entry.bill), - dueAmount: (entry.bill.dueAmount + entry.paymentAmount), + ...entry.bill, + dueAmount: entry.bill.dueAmount + entry.paymentAmount, })); return { @@ -552,7 +652,7 @@ export default class BillPaymentsService { public async saveChangeBillsPaymentAmount( tenantId: number, paymentMadeEntries: IBillPaymentEntryDTO[], - oldPaymentMadeEntries?: IBillPaymentEntryDTO[], + oldPaymentMadeEntries?: IBillPaymentEntryDTO[] ): Promise { const { Bill } = this.tenancy.models(tenantId); const opers: Promise[] = []; @@ -561,17 +661,21 @@ export default class BillPaymentsService { paymentMadeEntries, oldPaymentMadeEntries, 'paymentAmount', - 'billId', + 'billId' ); - diffEntries.forEach((diffEntry: { paymentAmount: number, billId: number }) => { - if (diffEntry.paymentAmount === 0) { return; } + diffEntries.forEach( + (diffEntry: { paymentAmount: number; billId: number }) => { + if (diffEntry.paymentAmount === 0) { + return; + } - const oper = Bill.changePaymentAmount( - diffEntry.billId, - diffEntry.paymentAmount, - ); - opers.push(oper); - }); + const oper = Bill.changePaymentAmount( + diffEntry.billId, + diffEntry.paymentAmount + ); + opers.push(oper); + } + ); await Promise.all(opers); } } diff --git a/server/src/services/Sales/PaymentsReceives.ts b/server/src/services/Sales/PaymentsReceives.ts index 8cab34e9a..bf000906d 100644 --- a/server/src/services/Sales/PaymentsReceives.ts +++ b/server/src/services/Sales/PaymentsReceives.ts @@ -31,13 +31,13 @@ import { ServiceError } from 'exceptions'; import CustomersService from 'services/Contacts/CustomersService'; import ItemsEntriesService from 'services/Items/ItemsEntriesService'; import JournalCommands from 'services/Accounting/JournalCommands'; +import { ACCOUNT_PARENT_TYPE } from 'data/AccountTypes'; const ERRORS = { PAYMENT_RECEIVE_NO_EXISTS: 'PAYMENT_RECEIVE_NO_EXISTS', PAYMENT_RECEIVE_NOT_EXISTS: 'PAYMENT_RECEIVE_NOT_EXISTS', DEPOSIT_ACCOUNT_NOT_FOUND: 'DEPOSIT_ACCOUNT_NOT_FOUND', - DEPOSIT_ACCOUNT_NOT_CURRENT_ASSET_TYPE: - 'DEPOSIT_ACCOUNT_NOT_CURRENT_ASSET_TYPE', + DEPOSIT_ACCOUNT_INVALID_TYPE: 'DEPOSIT_ACCOUNT_INVALID_TYPE', INVALID_PAYMENT_AMOUNT: 'INVALID_PAYMENT_AMOUNT', INVOICES_IDS_NOT_FOUND: 'INVOICES_IDS_NOT_FOUND', ENTRIES_IDS_NOT_EXISTS: 'ENTRIES_IDS_NOT_EXISTS', @@ -99,8 +99,8 @@ export default class PaymentReceiveService { /** * Validates the payment receive existance. - * @param {number} tenantId - - * @param {number} paymentReceiveId - + * @param {number} tenantId - Tenant id. + * @param {number} paymentReceiveId - Payment receive id. */ async getPaymentReceiveOrThrowError( tenantId: number, @@ -119,32 +119,25 @@ export default class PaymentReceiveService { /** * Validate the deposit account id existance. - * @param {number} tenantId - - * @param {number} depositAccountId - + * @param {number} tenantId - Tenant id. + * @param {number} depositAccountId - Deposit account id. + * @return {Promise} */ async getDepositAccountOrThrowError( tenantId: number, depositAccountId: number ): Promise { - const { - accountTypeRepository, - accountRepository, - } = this.tenancy.repositories(tenantId); + const { accountRepository } = this.tenancy.repositories(tenantId); - const currentAssetTypes = await accountTypeRepository.getByChildType( - 'current_asset' - ); const depositAccount = await accountRepository.findOneById( depositAccountId ); - - const currentAssetTypesIds = currentAssetTypes.map((type) => type.id); - if (!depositAccount) { throw new ServiceError(ERRORS.DEPOSIT_ACCOUNT_NOT_FOUND); } - if (currentAssetTypesIds.indexOf(depositAccount.accountTypeId) === -1) { - throw new ServiceError(ERRORS.DEPOSIT_ACCOUNT_NOT_CURRENT_ASSET_TYPE); + // Detarmines whether the account is cash equivalents. + if (!depositAccount.isParentType(ACCOUNT_PARENT_TYPE.CURRENT_ASSET)) { + throw new ServiceError(ERRORS.DEPOSIT_ACCOUNT_INVALID_TYPE); } return depositAccount; } @@ -316,7 +309,6 @@ export default class PaymentReceiveService { ...formatDateFields(omit(paymentReceiveDTO, ['entries']), [ 'paymentDate', ]), - entries: paymentReceiveDTO.entries.map((entry) => ({ ...omit(entry, ['id']), })), diff --git a/server/src/services/Sales/SalesReceipts.ts b/server/src/services/Sales/SalesReceipts.ts index 43dd81ef2..27bc61f9f 100644 --- a/server/src/services/Sales/SalesReceipts.ts +++ b/server/src/services/Sales/SalesReceipts.ts @@ -18,6 +18,7 @@ import { ServiceError } from 'exceptions'; import ItemsEntriesService from 'services/Items/ItemsEntriesService'; import { ItemEntry } from 'models'; import InventoryService from 'services/Inventory/Inventory'; +import { ACCOUNT_PARENT_TYPE } from 'data/AccountTypes'; const ERRORS = { SALE_RECEIPT_NOT_FOUND: 'SALE_RECEIPT_NOT_FOUND', @@ -78,30 +79,20 @@ export default class SalesReceiptService { /** * Validate whether sale receipt deposit account exists on the storage. - * @param {number} tenantId - - * @param {number} accountId - + * @param {number} tenantId - Tenant id. + * @param {number} accountId - Account id. */ async validateReceiptDepositAccountExistance( tenantId: number, accountId: number ) { - const { - accountRepository, - accountTypeRepository, - } = this.tenancy.repositories(tenantId); + const { accountRepository } = this.tenancy.repositories(tenantId); const depositAccount = await accountRepository.findOneById(accountId); if (!depositAccount) { throw new ServiceError(ERRORS.DEPOSIT_ACCOUNT_NOT_FOUND); } - const depositAccountType = await accountTypeRepository.findOneById( - depositAccount.accountTypeId - ); - - if ( - !depositAccountType || - depositAccountType.childRoot === 'current_asset' - ) { + if (!depositAccount.isParentType(ACCOUNT_PARENT_TYPE.CURRENT_ASSET)) { throw new ServiceError(ERRORS.DEPOSIT_ACCOUNT_NOT_CURRENT_ASSET); } }