From cf2ebe9597447dd72e84053a82b0c9bf1ec4e0e7 Mon Sep 17 00:00:00 2001 From: "a.bouhuolia" Date: Sat, 24 Jul 2021 03:10:32 +0200 Subject: [PATCH] WIP: Allocate landed cost. --- server/src/api/controllers/Expenses.ts | 77 +++++++- server/src/api/controllers/Items.ts | 2 +- server/src/api/controllers/Purchases/Bills.ts | 38 +++- .../api/controllers/Purchases/LandedCost.ts | 11 +- server/src/api/index.ts | 1 - ...200722173423_create_items_entries_table.js | 3 +- server/src/interfaces/Entry.ts | 18 ++ server/src/interfaces/Expenses.ts | 5 + server/src/interfaces/ItemEntry.ts | 33 ++-- server/src/interfaces/LandedCost.ts | 5 +- server/src/interfaces/index.ts | 1 + server/src/loaders/events.ts | 4 +- server/src/models/BillLandedCost.js | 20 ++ server/src/models/BillLandedCostEntry.js | 22 +++ server/src/models/Expense.js | 8 +- server/src/models/ExpenseCategory.js | 4 +- server/src/models/ItemEntry.js | 13 +- .../services/Accounting/JournalCommands.ts | 54 ++---- server/src/services/Entries/index.ts | 78 ++++++++ .../src/services/Expenses/ExpensesService.ts | 37 +++- server/src/services/Purchases/Bills.ts | 73 +++++++- .../Purchases/LandedCost/BillLandedCost.ts | 7 +- .../Purchases/LandedCost/ExpenseLandedCost.ts | 3 + .../LandedCost/TransctionLandedCost.ts | 29 ++- .../Purchases/LandedCost/constants.ts | 15 -- .../services/Purchases/LandedCost/index.ts | 175 +++++++----------- .../services/Purchases/LandedCost/utils.ts | 34 ++++ server/src/services/Purchases/constants.ts | 5 +- .../subscribers/Bills/WriteJournalEntries.ts | 8 +- server/src/subscribers/LandedCost/index.ts | 37 ++++ 30 files changed, 602 insertions(+), 218 deletions(-) create mode 100644 server/src/interfaces/Entry.ts create mode 100644 server/src/services/Entries/index.ts delete mode 100644 server/src/services/Purchases/LandedCost/constants.ts create mode 100644 server/src/services/Purchases/LandedCost/utils.ts create mode 100644 server/src/subscribers/LandedCost/index.ts diff --git a/server/src/api/controllers/Expenses.ts b/server/src/api/controllers/Expenses.ts index a207b6029..b6010d706 100644 --- a/server/src/api/controllers/Expenses.ts +++ b/server/src/api/controllers/Expenses.ts @@ -39,7 +39,7 @@ export default class ExpensesController extends BaseController { ); router.post( '/:id', - [...this.expenseDTOSchema, ...this.expenseParamSchema], + [...this.editExpenseDTOSchema, ...this.expenseParamSchema], this.validationResult, asyncMiddleware(this.editExpense.bind(this)), this.catchServiceErrors @@ -116,12 +116,62 @@ export default class ExpensesController extends BaseController { } /** - * Expense param schema. + * Edit expense validation schema. + */ + get editExpenseDTOSchema() { + return [ + check('reference_no') + .optional({ nullable: true }) + .trim() + .escape() + .isLength({ max: DATATYPES_LENGTH.STRING }), + check('payment_date').exists().isISO8601(), + check('payment_account_id') + .exists() + .isInt({ max: DATATYPES_LENGTH.INT_10 }) + .toInt(), + check('description') + .optional({ nullable: true }) + .isString() + .isLength({ max: DATATYPES_LENGTH.TEXT }), + check('currency_code').optional().isString().isLength({ max: 3 }), + check('exchange_rate').optional({ nullable: true }).isNumeric().toFloat(), + check('publish').optional().isBoolean().toBoolean(), + check('payee_id').optional({ nullable: true }).isNumeric().toInt(), + + check('categories').exists().isArray({ min: 1 }), + check('categories.*.id').optional().isNumeric().toInt(), + check('categories.*.index') + .exists() + .isInt({ max: DATATYPES_LENGTH.INT_10 }) + .toInt(), + check('categories.*.expense_account_id') + .exists() + .isInt({ max: DATATYPES_LENGTH.INT_10 }) + .toInt(), + check('categories.*.amount') + .optional({ nullable: true }) + .isFloat({ max: DATATYPES_LENGTH.DECIMAL_13_3 }) // 13, 3 + .toFloat(), + check('categories.*.description') + .optional() + .trim() + .escape() + .isLength({ max: DATATYPES_LENGTH.STRING }), + check('categories.*.landed_cost').optional().isBoolean().toBoolean(), + ]; + } + + /** + * Expense param validation schema. */ get expenseParamSchema() { return [param('id').exists().isNumeric().toInt()]; } - + + /** + * Expenses list validation schema. + */ get expensesListSchema() { return [ query('custom_view_id').optional().isNumeric().toInt(), @@ -291,7 +341,7 @@ export default class ExpensesController extends BaseController { * @param {Response} res * @param {ServiceError} error */ - catchServiceErrors( + private catchServiceErrors( error: Error, req: Request, res: Response, @@ -348,6 +398,25 @@ export default class ExpensesController extends BaseController { errors: [{ type: 'EXPENSE_HAS_ASSOCIATED_LANDED_COST', code: 900 }], }); } + if (error.errorType === 'ENTRIES_ALLOCATED_COST_COULD_NOT_DELETED') { + return res.status(400).send({ + errors: [ + { type: 'ENTRIES_ALLOCATED_COST_COULD_NOT_DELETED', code: 1000 }, + ], + }); + } + if ( + error.errorType === 'LOCATED_COST_ENTRIES_SHOULD_BIGGE_THAN_NEW_ENTRIES' + ) { + return res.status(400).send({ + errors: [ + { + type: 'LOCATED_COST_ENTRIES_SHOULD_BIGGE_THAN_NEW_ENTRIES', + code: 1100, + }, + ], + }); + } } next(error); } diff --git a/server/src/api/controllers/Items.ts b/server/src/api/controllers/Items.ts index 44f782ce7..ff30657e3 100644 --- a/server/src/api/controllers/Items.ts +++ b/server/src/api/controllers/Items.ts @@ -406,7 +406,7 @@ export default class ItemsController extends BaseController { * @param {Response} res * @param {NextFunction} next */ - handlerServiceErrors( + private handlerServiceErrors( error: Error, req: Request, res: Response, diff --git a/server/src/api/controllers/Purchases/Bills.ts b/server/src/api/controllers/Purchases/Bills.ts index 7100bd3ad..e190d7d84 100644 --- a/server/src/api/controllers/Purchases/Bills.ts +++ b/server/src/api/controllers/Purchases/Bills.ts @@ -145,7 +145,7 @@ export default class BillsController extends BaseController { .optional({ nullable: true }) .trim() .escape(), - check('entries.*.landedCost') + check('entries.*.landed_cost') .optional({ nullable: true }) .isBoolean() .toBoolean(), @@ -347,7 +347,7 @@ export default class BillsController extends BaseController { * @param {Response} res * @param {NextFunction} next */ - handleServiceError( + private handleServiceError( error: Error, req: Request, res: Response, @@ -422,6 +422,40 @@ export default class BillsController extends BaseController { ], }); } + if (error.errorType === 'BILL_HAS_ASSOCIATED_LANDED_COSTS') { + return res.status(400).send({ + errors: [ + { + type: 'BILL_HAS_ASSOCIATED_LANDED_COSTS', + message: + 'Cannot delete bill that has associated landed cost transactions.', + code: 1300, + }, + ], + }); + } + if (error.errorType === 'ENTRIES_ALLOCATED_COST_COULD_NOT_DELETED') { + return res.status(400).send({ + errors: [ + { + type: 'ENTRIES_ALLOCATED_COST_COULD_NOT_DELETED', + code: 1400, + message: + 'Bill entries that have landed cost type can not be deleted.', + }, + ], + }); + } + if (error.errorType === 'LOCATED_COST_ENTRIES_SHOULD_BIGGE_THAN_NEW_ENTRIES') { + return res.status(400).send({ + errors: [ + { + type: 'LOCATED_COST_ENTRIES_SHOULD_BIGGE_THAN_NEW_ENTRIES', + code: 1500, + }, + ], + }); + } } next(error); } diff --git a/server/src/api/controllers/Purchases/LandedCost.ts b/server/src/api/controllers/Purchases/LandedCost.ts index b1f403c3c..5471be78c 100644 --- a/server/src/api/controllers/Purchases/LandedCost.ts +++ b/server/src/api/controllers/Purchases/LandedCost.ts @@ -64,9 +64,9 @@ export default class BillAllocateLandedCost extends BaseController { /** * Retrieve the landed cost transactions of the given query. - * @param {Request} req - * @param {Response} res - * @param {NextFunction} next + * @param {Request} req - Request + * @param {Response} res - Response. + * @param {NextFunction} next - Next function. */ private async getLandedCostTransactions( req: Request, @@ -192,10 +192,7 @@ export default class BillAllocateLandedCost extends BaseController { billId ); - return res.status(200).send({ - billId, - transactions, - }); + return res.status(200).send({ billId, transactions }); } catch (error) { next(error); } diff --git a/server/src/api/index.ts b/server/src/api/index.ts index 649e67f0a..1e95d7a05 100644 --- a/server/src/api/index.ts +++ b/server/src/api/index.ts @@ -40,7 +40,6 @@ import Ping from 'api/controllers/Ping'; import Subscription from 'api/controllers/Subscription'; import Licenses from 'api/controllers/Subscription/Licenses'; import InventoryAdjustments from 'api/controllers/Inventory/InventoryAdjustments'; - import Setup from 'api/controllers/Setup'; export default () => { 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 616234ac9..b480540de 100644 --- a/server/src/database/migrations/20200722173423_create_items_entries_table.js +++ b/server/src/database/migrations/20200722173423_create_items_entries_table.js @@ -28,7 +28,8 @@ exports.up = function (knex) { .inTable('accounts'); table.boolean('landed_cost').defaultTo(false); - table.decimal('allocated_cost_amount', 13, 3); + table.decimal('allocated_cost_amount', 13, 3).defaultTo(0); + table.timestamps(); }); }; diff --git a/server/src/interfaces/Entry.ts b/server/src/interfaces/Entry.ts new file mode 100644 index 000000000..b55bb0aa1 --- /dev/null +++ b/server/src/interfaces/Entry.ts @@ -0,0 +1,18 @@ +export interface ICommonEntry { + id: number; + amount: number; +} + +export interface ICommonLandedCostEntry extends ICommonEntry { + landedCost: boolean; + allocatedCostAmount: number; +} + +export interface ICommonEntryDTO { + id?: number; + amount: number; +} + +export interface ICommonLandedCostEntryDTO extends ICommonEntryDTO { + landedCost?: boolean; +} diff --git a/server/src/interfaces/Expenses.ts b/server/src/interfaces/Expenses.ts index a1bea49ac..78dba8946 100644 --- a/server/src/interfaces/Expenses.ts +++ b/server/src/interfaces/Expenses.ts @@ -40,6 +40,9 @@ export interface IExpenseCategory { description: string; expenseId: number; amount: number; + + allocatedCostAmount: number; + unallocatedCostAmount: number; landedCost: boolean; } @@ -57,8 +60,10 @@ export interface IExpenseDTO { } export interface IExpenseCategoryDTO { + id?: number; expenseAccountId: number; index: number; + amount: number; description?: string; expenseId: number; landedCost?: boolean; diff --git a/server/src/interfaces/ItemEntry.ts b/server/src/interfaces/ItemEntry.ts index f9df14934..3cb07e4a7 100644 --- a/server/src/interfaces/ItemEntry.ts +++ b/server/src/interfaces/ItemEntry.ts @@ -1,26 +1,29 @@ - export type IItemEntryTransactionType = 'SaleInvoice' | 'Bill' | 'SaleReceipt'; export interface IItemEntry { - id?: number, + id?: number; - referenceType: string, - referenceId: number, + referenceType: string; + referenceId: number; - index: number, + index: number; - itemId: number, - description: string, - discount: number, - quantity: number, - rate: number, + itemId: number; + description: string; + discount: number; + quantity: number; + rate: number; + amount: number; - sellAccountId: number, - costAccountId: number, + landedCost: number; + allocatedCostAmount: number; + unallocatedCostAmount: number; - landedCost?: boolean, + sellAccountId: number; + costAccountId: number; } export interface IItemEntryDTO { - landedCost?: boolean -} \ No newline at end of file + id?: number, + landedCost?: boolean; +} diff --git a/server/src/interfaces/LandedCost.ts b/server/src/interfaces/LandedCost.ts index 141e0f721..8e9ce2e77 100644 --- a/server/src/interfaces/LandedCost.ts +++ b/server/src/interfaces/LandedCost.ts @@ -64,7 +64,10 @@ export interface ILandedCostTransactionEntry { name: string; code: string; amount: number; + unallocatedCostAmount: number; + allocatedCostAmount: number; description: string; + costAccountId: number; } interface ILandedCostEntry { @@ -83,7 +86,7 @@ export interface IBillLandedCostTransaction { costAccountId: number, description: string; - allocatedEntries?: IBillLandedCostTransactionEntry[], + allocateEntries?: IBillLandedCostTransactionEntry[], }; export interface IBillLandedCostTransactionEntry { diff --git a/server/src/interfaces/index.ts b/server/src/interfaces/index.ts index 0ce5f414c..4e8dc9e78 100644 --- a/server/src/interfaces/index.ts +++ b/server/src/interfaces/index.ts @@ -54,6 +54,7 @@ export * from './Ledger'; export * from './CashFlow'; export * from './InventoryDetails'; export * from './LandedCost'; +export * from './Entry'; export interface I18nService { __: (input: string) => string; diff --git a/server/src/loaders/events.ts b/server/src/loaders/events.ts index e0478145f..92a8427a4 100644 --- a/server/src/loaders/events.ts +++ b/server/src/loaders/events.ts @@ -26,4 +26,6 @@ import 'subscribers/vendors'; import 'subscribers/paymentMades'; import 'subscribers/paymentReceives'; import 'subscribers/saleEstimates'; -import 'subscribers/items'; \ No newline at end of file +import 'subscribers/items'; + +import 'subscribers/LandedCost'; \ No newline at end of file diff --git a/server/src/models/BillLandedCost.js b/server/src/models/BillLandedCost.js index f517dbbaf..ffa69eaeb 100644 --- a/server/src/models/BillLandedCost.js +++ b/server/src/models/BillLandedCost.js @@ -1,4 +1,5 @@ import { Model } from 'objection'; +import { lowerCase } from 'lodash'; import TenantModel from 'models/TenantModel'; export default class BillLandedCost extends TenantModel { @@ -16,6 +17,25 @@ export default class BillLandedCost extends TenantModel { return ['createdAt', 'updatedAt']; } + /** + * Virtual attributes. + */ + static get virtualAttributes() { + return ['allocationMethodFormatted']; + } + + /** + * Allocation method formatted. + */ + get allocationMethodFormatted() { + const allocationMethod = lowerCase(this.allocationMethod); + const keyLabelsPairs = { + value: 'Value', + quantity: 'Quantity', + }; + return keyLabelsPairs[allocationMethod] || ''; + } + /** * Relationship mapping. */ diff --git a/server/src/models/BillLandedCostEntry.js b/server/src/models/BillLandedCostEntry.js index aca0a87b7..d4f3fc833 100644 --- a/server/src/models/BillLandedCostEntry.js +++ b/server/src/models/BillLandedCostEntry.js @@ -1,3 +1,4 @@ +import { Model } from 'objection'; import TenantModel from 'models/TenantModel'; export default class BillLandedCostEntry extends TenantModel { @@ -7,4 +8,25 @@ export default class BillLandedCostEntry extends TenantModel { static get tableName() { return 'bill_located_cost_entries'; } + + /** + * Relationship mapping. + */ + static get relationMappings() { + const ItemEntry = require('models/ItemEntry'); + + return { + itemEntry: { + relation: Model.BelongsToOneRelation, + modelClass: ItemEntry.default, + join: { + from: 'bill_located_cost_entries.entryId', + to: 'items_entries.referenceId', + }, + filter(builder) { + builder.where('reference_type', 'Bill'); + }, + }, + }; + } } diff --git a/server/src/models/Expense.js b/server/src/models/Expense.js index e8bc02545..efadc2bbb 100644 --- a/server/src/models/Expense.js +++ b/server/src/models/Expense.js @@ -39,14 +39,18 @@ export default class Expense extends TenantModel { } static get virtualAttributes() { - return ['isPublished', 'unallocatedLandedCost']; + return ['isPublished', 'unallocatedCostAmount']; } isPublished() { return Boolean(this.publishedAt); } - unallocatedLandedCost() { + /** + * Retrieve the unallocated cost amount. + * @return {number} + */ + get unallocatedCostAmount() { return Math.max(this.amount - this.allocatedCostAmount, 0); } diff --git a/server/src/models/ExpenseCategory.js b/server/src/models/ExpenseCategory.js index a761acb23..50416805e 100644 --- a/server/src/models/ExpenseCategory.js +++ b/server/src/models/ExpenseCategory.js @@ -13,14 +13,14 @@ export default class ExpenseCategory extends TenantModel { * Virtual attributes. */ static get virtualAttributes() { - return ['unallocatedLandedCost']; + return ['unallocatedCostAmount']; } /** * Remain unallocated landed cost. * @return {number} */ - get unallocatedLandedCost() { + get unallocatedCostAmount() { return Math.max(this.amount - this.allocatedCostAmount, 0); } diff --git a/server/src/models/ItemEntry.js b/server/src/models/ItemEntry.js index 3434b01af..a8156fc01 100644 --- a/server/src/models/ItemEntry.js +++ b/server/src/models/ItemEntry.js @@ -21,8 +21,8 @@ export default class ItemEntry extends TenantModel { return ['amount']; } - static amount() { - return this.calcAmount(this); + get amount() { + return ItemEntry.calcAmount(this); } static calcAmount(itemEntry) { @@ -34,6 +34,7 @@ export default class ItemEntry extends TenantModel { static get relationMappings() { const Item = require('models/Item'); + const BillLandedCostEntry = require('models/BillLandedCostEntry'); return { item: { @@ -44,6 +45,14 @@ export default class ItemEntry extends TenantModel { to: 'items.id', }, }, + allocatedCostEntries: { + relation: Model.HasManyRelation, + modelClass: BillLandedCostEntry.default, + join: { + from: 'items_entries.referenceId', + to: 'bill_located_cost_entries.entryId', + }, + }, }; } } diff --git a/server/src/services/Accounting/JournalCommands.ts b/server/src/services/Accounting/JournalCommands.ts index 15f731f3c..4a5df9bf0 100644 --- a/server/src/services/Accounting/JournalCommands.ts +++ b/server/src/services/Accounting/JournalCommands.ts @@ -1,9 +1,11 @@ import moment from 'moment'; +import { sumBy } from 'lodash'; import { IBill, IManualJournalEntry, ISaleReceipt, ISystemUser, + IAccount, } from 'interfaces'; import JournalPoster from './JournalPoster'; import JournalEntry from './JournalEntry'; @@ -17,7 +19,6 @@ import { IItemEntry, } from 'interfaces'; import { increment } from 'utils'; - export default class JournalCommands { journal: JournalPoster; models: any; @@ -37,45 +38,20 @@ export default class JournalCommands { /** * Records the bill journal entries. * @param {IBill} bill - * @param {boolean} override - Override the old bill entries. + * @param {IAccount} payableAccount - */ - async bill(bill: IBill, override: boolean = false): Promise { - const { transactionsRepository, accountRepository } = this.repositories; - const { Item, ItemEntry } = this.models; - - const entriesItemsIds = bill.entries.map((entry) => entry.itemId); - - // Retrieve the bill transaction items. - const storedItems = await Item.query().whereIn('id', entriesItemsIds); - - const storedItemsMap = new Map(storedItems.map((item) => [item.id, item])); - const payableAccount = await accountRepository.findOne({ - slug: 'accounts-payable', - }); - const formattedDate = moment(bill.billDate).format('YYYY-MM-DD'); - + bill(bill: IBill, payableAccount: IAccount): void { const commonJournalMeta = { debit: 0, credit: 0, referenceId: bill.id, referenceType: 'Bill', - date: formattedDate, + date: moment(bill.billDate).format('YYYY-MM-DD'), userId: bill.userId, - referenceNumber: bill.referenceNo, transactionNumber: bill.billNumber, - createdAt: bill.createdAt, }; - // Overrides the old bill entries. - if (override) { - const entries = await transactionsRepository.journal({ - referenceType: ['Bill'], - referenceId: [bill.id], - }); - this.journal.fromTransactions(entries); - this.journal.removeEntries(); - } const payableEntry = new JournalEntry({ ...commonJournalMeta, credit: bill.amount, @@ -86,15 +62,15 @@ export default class JournalCommands { this.journal.credit(payableEntry); bill.entries.forEach((entry, index) => { - const item: IItem = storedItemsMap.get(entry.itemId); - const amount = ItemEntry.calcAmount(entry); + const landedCostAmount = sumBy(entry.allocatedCostEntries, 'cost'); + // Inventory or cost entry. const debitEntry = new JournalEntry({ ...commonJournalMeta, - debit: amount, + debit: entry.amount + landedCostAmount, account: - ['inventory'].indexOf(item.type) !== -1 - ? item.inventoryAccountId + ['inventory'].indexOf(entry.item.type) !== -1 + ? entry.item.inventoryAccountId : entry.costAccountId, index: index + 2, itemId: entry.itemId, @@ -102,6 +78,16 @@ export default class JournalCommands { }); this.journal.debit(debitEntry); }); + + // Allocate cost entries journal entries. + bill.locatedLandedCosts.forEach((landedCost) => { + const creditEntry = new JournalEntry({ + ...commonJournalMeta, + credit: landedCost.amount, + account: landedCost.costAccountId, + }); + this.journal.credit(creditEntry); + }); } /** diff --git a/server/src/services/Entries/index.ts b/server/src/services/Entries/index.ts new file mode 100644 index 000000000..b4277dabb --- /dev/null +++ b/server/src/services/Entries/index.ts @@ -0,0 +1,78 @@ +import { Service } from 'typedi'; +import { ServiceError } from 'exceptions'; +import { transformToMap } from 'utils'; +import { + ICommonLandedCostEntry, + ICommonLandedCostEntryDTO +} from 'interfaces'; + +const ERRORS = { + ENTRIES_ALLOCATED_COST_COULD_NOT_DELETED: + 'ENTRIES_ALLOCATED_COST_COULD_NOT_DELETED', + LOCATED_COST_ENTRIES_SHOULD_BIGGE_THAN_NEW_ENTRIES: + 'LOCATED_COST_ENTRIES_SHOULD_BIGGE_THAN_NEW_ENTRIES', +}; + +@Service() +export default class EntriesService { + /** + * Validates bill entries that has allocated landed cost amount not deleted. + * @param {IItemEntry[]} oldBillEntries - + * @param {IItemEntry[]} newBillEntries - + */ + public getLandedCostEntriesDeleted( + oldBillEntries: ICommonLandedCostEntry[], + newBillEntriesDTO: ICommonLandedCostEntryDTO[] + ): ICommonLandedCostEntry[] { + const newBillEntriesById = transformToMap(newBillEntriesDTO, 'id'); + + return oldBillEntries.filter((entry) => { + const newEntry = newBillEntriesById.get(entry.id); + + if (entry.allocatedCostAmount > 0 && typeof newEntry === 'undefined') { + return true; + } + return false; + }); + } + + /** + * Validates the bill entries that have located cost amount should not be deleted. + * @param {IItemEntry[]} oldBillEntries - Old bill entries. + * @param {IItemEntryDTO[]} newBillEntries - New DTO bill entries. + */ + public validateLandedCostEntriesNotDeleted( + oldBillEntries: ICommonLandedCostEntry[], + newBillEntriesDTO: ICommonLandedCostEntryDTO[] + ): void { + const entriesDeleted = this.getLandedCostEntriesDeleted( + oldBillEntries, + newBillEntriesDTO + ); + if (entriesDeleted.length > 0) { + throw new ServiceError(ERRORS.ENTRIES_ALLOCATED_COST_COULD_NOT_DELETED); + } + } + + /** + * Validate allocated cost amount entries should be smaller than new entries amount. + * @param {IItemEntry[]} oldBillEntries - Old bill entries. + * @param {IItemEntryDTO[]} newBillEntries - New DTO bill entries. + */ + public validateLocatedCostEntriesSmallerThanNewEntries( + oldBillEntries: ICommonLandedCostEntry[], + newBillEntriesDTO: ICommonLandedCostEntryDTO[] + ): void { + const oldBillEntriesById = transformToMap(oldBillEntries, 'id'); + + newBillEntriesDTO.forEach((entry) => { + const oldEntry = oldBillEntriesById.get(entry.id); + + if (oldEntry && oldEntry.allocatedCostAmount > entry.amount) { + throw new ServiceError( + ERRORS.LOCATED_COST_ENTRIES_SHOULD_BIGGE_THAN_NEW_ENTRIES + ); + } + }); + } +} diff --git a/server/src/services/Expenses/ExpensesService.ts b/server/src/services/Expenses/ExpensesService.ts index 1ac1623b0..39a8e8fdd 100644 --- a/server/src/services/Expenses/ExpensesService.ts +++ b/server/src/services/Expenses/ExpensesService.ts @@ -23,6 +23,7 @@ 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'; +import EntriesService from 'services/Entries'; const ERRORS = { EXPENSE_NOT_FOUND: 'expense_not_found', @@ -53,6 +54,9 @@ export default class ExpensesService implements IExpensesService { @Inject() contactsService: ContactsService; + @Inject() + entriesService: EntriesService; + /** * Retrieve the payment account details or returns not found server error in case the * given account not found on the storage. @@ -251,14 +255,16 @@ export default class ExpensesService implements IExpensesService { * @returns {IExpense|ServiceError} */ private async getExpenseOrThrowError(tenantId: number, expenseId: number) { - const { expenseRepository } = this.tenancy.repositories(tenantId); + const { Expense } = this.tenancy.models(tenantId); this.logger.info('[expense] trying to get the given expense.', { tenantId, expenseId, }); // Retrieve the given expense by id. - const expense = await expenseRepository.findOneById(expenseId); + const expense = await Expense.query() + .findById(expenseId) + .withGraphFetched('categories'); if (!expense) { this.logger.info('[expense] the given expense not found.', { @@ -459,36 +465,47 @@ export default class ExpensesService implements IExpensesService { const { expenseRepository } = this.tenancy.repositories(tenantId); const oldExpense = await this.getExpenseOrThrowError(tenantId, expenseId); - // - Validate payment account existance on the storage. + // Validate payment account existance on the storage. const paymentAccount = await this.getPaymentAccountOrThrowError( tenantId, expenseDTO.paymentAccountId ); - // - Validate expense accounts exist on the storage. + // Validate expense accounts exist on the storage. const expensesAccounts = await this.getExpensesAccountsOrThrowError( tenantId, this.mapExpensesAccountsIdsFromDTO(expenseDTO) ); - // - Validate payment account type. + // Validate payment account type. await this.validatePaymentAccountType(tenantId, paymentAccount); - // - Validate expenses accounts type. + // Validate expenses accounts type. await this.validateExpensesAccountsType(tenantId, expensesAccounts); - // - Validate the expense payee contact id existance on storage. + // Validate the expense payee contact id existance on storage. if (expenseDTO.payeeId) { await this.contactsService.getContactByIdOrThrowError( tenantId, expenseDTO.payeeId ); } - // - Validate the given expense categories not equal zero. + // Validate the given expense categories not equal zero. this.validateCategoriesNotEqualZero(expenseDTO); - // - Update the expense on the storage. + // Update the expense on the storage. const expenseObj = this.expenseDTOToModel(expenseDTO); - // - Upsert the expense object with expense entries. + // Validate expense entries that have allocated landed cost cannot be deleted. + this.entriesService.validateLandedCostEntriesNotDeleted( + oldExpense.categories, + expenseDTO.categories, + ); + // Validate expense entries that have allocated cost amount should be bigger than amount. + this.entriesService.validateLocatedCostEntriesSmallerThanNewEntries( + oldExpense.categories, + expenseDTO.categories, + ); + + // Upsert the expense object with expense entries. const expense = await expenseRepository.upsertGraph({ id: expenseId, ...expenseObj, diff --git a/server/src/services/Purchases/Bills.ts b/server/src/services/Purchases/Bills.ts index 12a63ad67..7cd4dfe34 100644 --- a/server/src/services/Purchases/Bills.ts +++ b/server/src/services/Purchases/Bills.ts @@ -1,4 +1,4 @@ -import { omit, sumBy } from 'lodash'; +import { omit, runInContext, sumBy } from 'lodash'; import moment from 'moment'; import { Inject, Service } from 'typedi'; import composeAsync from 'async/compose'; @@ -24,6 +24,7 @@ import { IBillsFilter, IBillsService, IItemEntry, + IItemEntryDTO, } from 'interfaces'; import { ServiceError } from 'exceptions'; import ItemsService from 'services/Items/ItemsService'; @@ -32,6 +33,7 @@ import JournalCommands from 'services/Accounting/JournalCommands'; import JournalPosterService from 'services/Sales/JournalPosterService'; import VendorsService from 'services/Contacts/VendorsService'; import { ERRORS } from './constants'; +import EntriesService from 'services/Entries'; /** * Vendor bills services. @@ -72,6 +74,9 @@ export default class BillsService @Inject() vendorsService: VendorsService; + @Inject() + entriesService: EntriesService; + /** * Validates whether the vendor is exist. * @async @@ -166,16 +171,33 @@ export default class BillsService * Validate the bill number require. * @param {string} billNo - */ - validateBillNoRequire(billNo: string) { + private validateBillNoRequire(billNo: string) { if (!billNo) { throw new ServiceError(ERRORS.BILL_NO_IS_REQUIRED); } } + /** + * Validate bill transaction has no associated allocated landed cost transactions. + * @param {number} tenantId + * @param {number} billId + */ + private async validateBillHasNoLandedCost(tenantId: number, billId: number) { + const { BillLandedCost } = this.tenancy.models(tenantId); + + const billLandedCosts = await BillLandedCost.query().where( + 'billId', + billId + ); + if (billLandedCosts.length > 0) { + throw new ServiceError(ERRORS.BILL_HAS_ASSOCIATED_LANDED_COSTS); + } + } + /** * Sets the default cost account to the bill entries. */ - setBillEntriesDefaultAccounts(tenantId: number) { + private setBillEntriesDefaultAccounts(tenantId: number) { return async (entries: IItemEntry[]) => { const { Item } = this.tenancy.models(tenantId); @@ -246,6 +268,7 @@ export default class BillsService billDTO.vendorId ); const initialEntries = billDTO.entries.map((entry) => ({ + amount: ItemEntry.calcAmount(entry), reference_type: 'Bill', ...omit(entry, ['amount']), })); @@ -397,6 +420,16 @@ export default class BillsService authorizedUser, oldBill ); + // Validate landed cost entries that have allocated cost could not be deleted. + await this.entriesService.validateLandedCostEntriesNotDeleted( + oldBill.entries, + billObj.entries, + ); + // Validate new landed cost entries should be bigger than new entries. + await this.entriesService.validateLocatedCostEntriesSmallerThanNewEntries( + oldBill.entries, + billObj.entries + ); // Update the bill transaction. const bill = await billRepository.upsertGraph({ id: billId, @@ -429,6 +462,9 @@ export default class BillsService // Retrieve the given bill or throw not found error. const oldBill = await this.getBillOrThrowError(tenantId, billId); + // Validate the givne bill has no associated landed cost transactions. + await this.validateBillHasNoLandedCost(tenantId, billId); + // Validate the purchase bill has no assocaited payments transactions. await this.validateBillHasNoEntries(tenantId, billId); @@ -561,9 +597,16 @@ export default class BillsService */ public async recordInventoryTransactions( tenantId: number, - bill: IBill, + billId: number, override?: boolean ): Promise { + const { Bill } = this.tenancy.models(tenantId); + + // Retireve bill with assocaited entries and allocated cost entries. + const bill = await Bill.query() + .findById(billId) + .withGraphFetched('entries.allocatedCostEntries'); + // Loads the inventory items entries of the given sale invoice. const inventoryEntries = await this.itemsEntriesService.filterInventoryEntries( @@ -573,7 +616,6 @@ export default class BillsService const transaction = { transactionId: bill.id, transactionType: 'Bill', - date: bill.billDate, direction: 'IN', entries: inventoryEntries, @@ -609,13 +651,30 @@ export default class BillsService */ public async recordJournalTransactions( tenantId: number, - bill: IBill, + billId: number, override: boolean = false ) { + const { Bill, Account } = this.tenancy.models(tenantId); + const journal = new JournalPoster(tenantId); const journalCommands = new JournalCommands(journal); - await journalCommands.bill(bill, override); + const bill = await Bill.query() + .findById(billId) + .withGraphFetched('entries.item') + .withGraphFetched('entries.allocatedCostEntries') + .withGraphFetched('locatedLandedCosts.allocateEntries'); + + const payableAccount = await Account.query().findOne({ + slug: 'accounts-payable', + }); + + // Overrides the bill journal entries. + if (override) { + await journalCommands.revertJournalEntries(billId, 'Bill'); + } + // Writes the bill journal entries. + journalCommands.bill(bill, payableAccount); return Promise.all([ journal.deleteEntries(), diff --git a/server/src/services/Purchases/LandedCost/BillLandedCost.ts b/server/src/services/Purchases/LandedCost/BillLandedCost.ts index 0aacc5ecf..92fcb89ee 100644 --- a/server/src/services/Purchases/LandedCost/BillLandedCost.ts +++ b/server/src/services/Purchases/LandedCost/BillLandedCost.ts @@ -12,8 +12,8 @@ import { export default class BillLandedCost { /** * Retrieve the landed cost transaction from the given bill transaction. - * @param {IBill} bill - * @returns {ILandedCostTransaction} + * @param {IBill} bill - Bill transaction. + * @returns {ILandedCostTransaction} - Landed cost transaction. */ public transformToLandedCost = (bill: IBill): ILandedCostTransaction => { const number = bill.billNumber || bill.referenceNo; @@ -49,7 +49,10 @@ export default class BillLandedCost { name: billEntry.item.name, code: billEntry.item.code, amount: billEntry.amount, + unallocatedCostAmount: billEntry.unallocatedCostAmount, + allocatedCostAmount: billEntry.allocatedCostAmount, description: billEntry.description, + costAccountId: billEntry.costAccountId || billEntry.item.costAccountId, }; } } diff --git a/server/src/services/Purchases/LandedCost/ExpenseLandedCost.ts b/server/src/services/Purchases/LandedCost/ExpenseLandedCost.ts index 8931e500c..078ae6627 100644 --- a/server/src/services/Purchases/LandedCost/ExpenseLandedCost.ts +++ b/server/src/services/Purchases/LandedCost/ExpenseLandedCost.ts @@ -48,6 +48,9 @@ export default class ExpenseLandedCost { code: expenseEntry.expenseAccount.code, amount: expenseEntry.amount, description: expenseEntry.description, + allocatedCostAmount: expenseEntry.allocatedCostAmount, + unallocatedCostAmount: expenseEntry.unallocatedCostAmount, + costAccountId: expenseEntry.expenseAccount.id, }; }; } diff --git a/server/src/services/Purchases/LandedCost/TransctionLandedCost.ts b/server/src/services/Purchases/LandedCost/TransctionLandedCost.ts index 57c8e13c6..9362aae1d 100644 --- a/server/src/services/Purchases/LandedCost/TransctionLandedCost.ts +++ b/server/src/services/Purchases/LandedCost/TransctionLandedCost.ts @@ -1,11 +1,12 @@ import { Inject, Service } from 'typedi'; import * as R from 'ramda'; -import { IBill, IExpense, ILandedCostTransaction } from 'interfaces'; +import { Model } from 'objection'; +import { IBill, IExpense, ILandedCostTransaction, ILandedCostTransactionEntry } from 'interfaces'; import { ServiceError } from 'exceptions'; import BillLandedCost from './BillLandedCost'; import ExpenseLandedCost from './ExpenseLandedCost'; import HasTenancyService from 'services/Tenancy/TenancyService'; -import { ERRORS } from './constants'; +import { ERRORS } from './utils'; @Service() export default class TransactionLandedCost { @@ -27,7 +28,7 @@ export default class TransactionLandedCost { public getModel = ( tenantId: number, transactionType: string - ): IBill | IExpense => { + ): Model => { const Models = this.tenancy.models(tenantId); const Model = Models[transactionType]; @@ -58,4 +59,26 @@ export default class TransactionLandedCost { ), )(transaction); } + + /** + * Transformes the given expense or bill entry to landed cost transaction entry. + * @param {string} transactionType + * @param {} transactionEntry + * @returns {ILandedCostTransactionEntry} + */ + public transformToLandedCostEntry = ( + transactionType: 'Bill' | 'Expense', + transactionEntry, + ): ILandedCostTransactionEntry => { + return R.compose( + R.when( + R.always(transactionType === 'Bill'), + this.billLandedCost.transformToLandedCostEntry, + ), + R.when( + R.always(transactionType === 'Expense'), + this.expenseLandedCost.transformToLandedCostEntry, + ), + )(transactionEntry); + } } diff --git a/server/src/services/Purchases/LandedCost/constants.ts b/server/src/services/Purchases/LandedCost/constants.ts deleted file mode 100644 index be247f943..000000000 --- a/server/src/services/Purchases/LandedCost/constants.ts +++ /dev/null @@ -1,15 +0,0 @@ - - - -export const ERRORS = { - COST_TYPE_UNDEFINED: 'COST_TYPE_UNDEFINED', - LANDED_COST_ITEMS_IDS_NOT_FOUND: 'LANDED_COST_ITEMS_IDS_NOT_FOUND', - COST_TRANSACTION_HAS_NO_ENOUGH_TO_LOCATE: - 'COST_TRANSACTION_HAS_NO_ENOUGH_TO_LOCATE', - BILL_LANDED_COST_NOT_FOUND: 'BILL_LANDED_COST_NOT_FOUND', - COST_ENTRY_ID_NOT_FOUND: 'COST_ENTRY_ID_NOT_FOUND', - LANDED_COST_TRANSACTION_NOT_FOUND: 'LANDED_COST_TRANSACTION_NOT_FOUND', - LANDED_COST_ENTRY_NOT_FOUND: 'LANDED_COST_ENTRY_NOT_FOUND', - COST_AMOUNT_BIGGER_THAN_UNALLOCATED_AMOUNT: 'COST_AMOUNT_BIGGER_THAN_UNALLOCATED_AMOUNT', - ALLOCATE_COST_SHOULD_NOT_BE_BILL: 'ALLOCATE_COST_SHOULD_NOT_BE_BILL' -}; diff --git a/server/src/services/Purchases/LandedCost/index.ts b/server/src/services/Purchases/LandedCost/index.ts index 9b4c1ffb7..b189610ee 100644 --- a/server/src/services/Purchases/LandedCost/index.ts +++ b/server/src/services/Purchases/LandedCost/index.ts @@ -1,5 +1,9 @@ import { Inject, Service } from 'typedi'; import { difference, sumBy } from 'lodash'; +import { + EventDispatcher, + EventDispatcherInterface, +} from 'decorators/eventDispatcher'; import BillsService from '../Bills'; import { ServiceError } from 'exceptions'; import { @@ -9,15 +13,14 @@ import { ILandedCostItemDTO, ILandedCostDTO, IBillLandedCostTransaction, - IBillLandedCostTransactionEntry, + ILandedCostTransaction, + ILandedCostTransactionEntry, } from 'interfaces'; +import events from 'subscribers/events'; import InventoryService from 'services/Inventory/Inventory'; import HasTenancyService from 'services/Tenancy/TenancyService'; -import { ERRORS } from './constants'; -import { transformToMap } from 'utils'; -import JournalPoster from 'services/Accounting/JournalPoster'; -import JournalEntry from 'services/Accounting/JournalEntry'; import TransactionLandedCost from './TransctionLandedCost'; +import { ERRORS, mergeLocatedWithBillEntries } from './utils'; const CONFIG = { COST_TYPES: { @@ -47,6 +50,9 @@ export default class AllocateLandedCostService { @Inject() public transactionLandedCost: TransactionLandedCost; + @EventDispatcher() + eventDispatcher: EventDispatcherInterface; + /** * Validates allocate cost items association with the purchase invoice entries. * @param {IItemEntry[]} purchaseInvoiceEntries @@ -72,23 +78,23 @@ export default class AllocateLandedCostService { }; /** - * Saves the bill landed cost model. - * @param {number} tenantId - * @param {ILandedCostDTO} landedCostDTO - * @param {number} purchaseInvoiceId - * @returns {Promise} + * Transformes DTO to bill landed cost model object. + * @param landedCostDTO + * @param bill + * @param costTransaction + * @param costTransactionEntry + * @returns */ - private saveBillLandedCostModel = ( - tenantId: number, + private transformToBillLandedCost( landedCostDTO: ILandedCostDTO, - purchaseInvoiceId: number - ): Promise => { - const { BillLandedCost } = this.tenancy.models(tenantId); + bill: IBill, + costTransaction: ILandedCostTransaction, + costTransactionEntry: ILandedCostTransactionEntry + ) { const amount = sumBy(landedCostDTO.items, 'cost'); - // Inserts the bill landed cost to the storage. - return BillLandedCost.query().insertGraph({ - billId: purchaseInvoiceId, + return { + billId: bill.id, fromTransactionType: landedCostDTO.transactionType, fromTransactionId: landedCostDTO.transactionId, fromTransactionEntryId: landedCostDTO.transactionEntryId, @@ -96,8 +102,9 @@ export default class AllocateLandedCostService { allocationMethod: landedCostDTO.allocationMethod, description: landedCostDTO.description, allocateEntries: landedCostDTO.items, - }); - }; + costAccountId: costTransactionEntry.costAccountId, + }; + } /** * Allocate the landed cost amount to cost transactions. @@ -147,7 +154,6 @@ export default class AllocateLandedCostService { tenantId, transactionType ); - // Decrement the allocate cost amount of cost transaction. return Model.query() .where('id', transactionId) @@ -202,12 +208,22 @@ export default class AllocateLandedCostService { const entry = await Model.relatedQuery(relation) .for(transactionId) .findOne('id', transactionEntryId) - .where('landedCost', true); + .where('landedCost', true) + .onBuild((q) => { + if (transactionType === 'Bill') { + q.withGraphFetched('item'); + } else if (transactionType === 'Expense') { + q.withGraphFetched('expenseAccount'); + } + }); if (!entry) { throw new ServiceError(ERRORS.LANDED_COST_ENTRY_NOT_FOUND); } - return entry; + return this.transactionLandedCost.transformToLandedCostEntry( + transactionType, + entry + ); }; /** @@ -230,31 +246,11 @@ export default class AllocateLandedCostService { unallocatedCost: number, amount: number ): void => { - console.log(unallocatedCost, amount, '123'); - if (unallocatedCost < amount) { throw new ServiceError(ERRORS.COST_AMOUNT_BIGGER_THAN_UNALLOCATED_AMOUNT); } }; - /** - * Merges item entry to bill located landed cost entry. - * @param {IBillLandedCostTransactionEntry[]} locatedEntries - - * @param {IItemEntry[]} billEntries - - * @returns {(IBillLandedCostTransactionEntry & { entry: IItemEntry })[]} - */ - private mergeLocatedWithBillEntries = ( - locatedEntries: IBillLandedCostTransactionEntry[], - billEntries: IItemEntry[] - ): (IBillLandedCostTransactionEntry & { entry: IItemEntry })[] => { - const billEntriesByEntryId = transformToMap(billEntries, 'id'); - - return locatedEntries.map((entry) => ({ - ...entry, - entry: billEntriesByEntryId.get(entry.entryId), - })); - }; - /** * Records inventory transactions. * @param {number} tenantId @@ -266,7 +262,7 @@ export default class AllocateLandedCostService { bill: IBill ) => { // Retrieve the merged allocated entries with bill entries. - const allocateEntries = this.mergeLocatedWithBillEntries( + const allocateEntries = mergeLocatedWithBillEntries( billLandedCost.allocateEntries, bill.entries ); @@ -304,22 +300,24 @@ export default class AllocateLandedCostService { * * @param {ILandedCostDTO} landedCostDTO - Landed cost DTO. * @param {number} tenantId - Tenant id. - * @param {number} purchaseInvoiceId - Purchase invoice id. + * @param {number} billId - Purchase invoice id. */ public allocateLandedCost = async ( tenantId: number, allocateCostDTO: ILandedCostDTO, - purchaseInvoiceId: number + billId: number ): Promise<{ billLandedCost: IBillLandedCost; }> => { + const { BillLandedCost } = this.tenancy.models(tenantId); + // Retrieve total cost of allocated items. const amount = this.getAllocateItemsCostTotal(allocateCostDTO); // Retrieve the purchase invoice or throw not found error. - const purchaseInvoice = await this.billsService.getBillOrThrowError( + const bill = await this.billsService.getBillOrThrowError( tenantId, - purchaseInvoiceId + billId ); // Retrieve landed cost transaction or throw not found service error. const landedCostTransaction = await this.getLandedCostOrThrowError( @@ -336,25 +334,36 @@ export default class AllocateLandedCostService { ); // Validates allocate cost items association with the purchase invoice entries. this.validateAllocateCostItems( - purchaseInvoice.entries, + bill.entries, allocateCostDTO.items ); // Validate the amount of cost with unallocated landed cost. this.validateLandedCostEntryAmount( - landedCostEntry.unallocatedLandedCost, + landedCostEntry.unallocatedCostAmount, amount ); - // Save the bill landed cost model. - const billLandedCost = await this.saveBillLandedCostModel( - tenantId, + // Transformes DTO to bill landed cost model object. + const billLandedCostObj = this.transformToBillLandedCost( allocateCostDTO, - purchaseInvoiceId + bill, + landedCostTransaction, + landedCostEntry ); + // Save the bill landed cost model. + const billLandedCost = await BillLandedCost.query().insertGraph( + billLandedCostObj + ); + // Triggers the event `onBillLandedCostCreated`. + await this.eventDispatcher.dispatch(events.billLandedCost.onCreated, { + tenantId, + billId, + billLandedCostId: billLandedCost.id, + }); // Records the inventory transactions. await this.recordInventoryTransactions( tenantId, billLandedCost, - purchaseInvoice + bill ); // Increment landed cost amount on transaction and entry. await this.incrementLandedCostAmount( @@ -364,55 +373,9 @@ export default class AllocateLandedCostService { allocateCostDTO.transactionEntryId, amount ); - // Write the landed cost journal entries. - // await this.writeJournalEntry(tenantId, billLandedCost, purchaseInvoice); - return { billLandedCost }; }; - /** - * Write journal entries of the given purchase invoice landed cost. - * @param tenantId - * @param purchaseInvoice - * @param landedCost - */ - private writeJournalEntry = async ( - tenantId: number, - landedCostEntry: any, - purchaseInvoice: IBill, - landedCost: IBillLandedCost - ) => { - const journal = new JournalPoster(tenantId); - const billEntriesById = purchaseInvoice.entries; - - const commonEntry = { - referenceType: 'Bill', - referenceId: purchaseInvoice.id, - date: purchaseInvoice.billDate, - indexGroup: 300, - }; - const costEntry = new JournalEntry({ - ...commonEntry, - credit: landedCost.amount, - account: landedCost.costAccountId, - index: 1, - }); - journal.credit(costEntry); - - landedCost.allocateEntries.forEach((entry, index) => { - const billEntry = billEntriesById[entry.entryId]; - - const inventoryEntry = new JournalEntry({ - ...commonEntry, - debit: entry.cost, - account: billEntry.item.inventoryAccountId, - index: 1 + index, - }); - journal.debit(inventoryEntry); - }); - return journal; - }; - /** * Retrieve the give bill landed cost or throw not found service error. * @param {number} tenantId - Tenant id. @@ -422,7 +385,7 @@ export default class AllocateLandedCostService { public getBillLandedCostOrThrowError = async ( tenantId: number, landedCostId: number - ): Promise => { + ): Promise => { const { BillLandedCost } = this.tenancy.models(tenantId); // Retrieve the bill landed cost model. @@ -462,7 +425,7 @@ export default class AllocateLandedCostService { * - Delete the associated inventory transactions. * - Decrement allocated amount of landed cost transaction and entry. * - Revert journal entries. - * + * ---------------------------------- * @param {number} tenantId - Tenant id. * @param {number} landedCostId - Landed cost id. * @return {Promise} @@ -481,6 +444,12 @@ export default class AllocateLandedCostService { // Delete landed cost transaction with assocaited locate entries. await this.deleteLandedCost(tenantId, landedCostId); + // Triggers the event `onBillLandedCostCreated`. + await this.eventDispatcher.dispatch(events.billLandedCost.onDeleted, { + tenantId, + billLandedCostId: oldBillLandedCost.id, + billId: oldBillLandedCost.billId, + }); // Removes the inventory transactions. await this.removeInventoryTransactions(tenantId, landedCostId); diff --git a/server/src/services/Purchases/LandedCost/utils.ts b/server/src/services/Purchases/LandedCost/utils.ts new file mode 100644 index 000000000..48d8cb683 --- /dev/null +++ b/server/src/services/Purchases/LandedCost/utils.ts @@ -0,0 +1,34 @@ +import { IItemEntry, IBillLandedCostTransactionEntry } from 'interfaces'; +import { transformToMap } from 'utils'; + +export const ERRORS = { + COST_TYPE_UNDEFINED: 'COST_TYPE_UNDEFINED', + LANDED_COST_ITEMS_IDS_NOT_FOUND: 'LANDED_COST_ITEMS_IDS_NOT_FOUND', + COST_TRANSACTION_HAS_NO_ENOUGH_TO_LOCATE: + 'COST_TRANSACTION_HAS_NO_ENOUGH_TO_LOCATE', + BILL_LANDED_COST_NOT_FOUND: 'BILL_LANDED_COST_NOT_FOUND', + COST_ENTRY_ID_NOT_FOUND: 'COST_ENTRY_ID_NOT_FOUND', + LANDED_COST_TRANSACTION_NOT_FOUND: 'LANDED_COST_TRANSACTION_NOT_FOUND', + LANDED_COST_ENTRY_NOT_FOUND: 'LANDED_COST_ENTRY_NOT_FOUND', + COST_AMOUNT_BIGGER_THAN_UNALLOCATED_AMOUNT: + 'COST_AMOUNT_BIGGER_THAN_UNALLOCATED_AMOUNT', + ALLOCATE_COST_SHOULD_NOT_BE_BILL: 'ALLOCATE_COST_SHOULD_NOT_BE_BILL', +}; + +/** + * Merges item entry to bill located landed cost entry. + * @param {IBillLandedCostTransactionEntry[]} locatedEntries - + * @param {IItemEntry[]} billEntries - + * @returns {(IBillLandedCostTransactionEntry & { entry: IItemEntry })[]} + */ +export const mergeLocatedWithBillEntries = ( + locatedEntries: IBillLandedCostTransactionEntry[], + billEntries: IItemEntry[] +): (IBillLandedCostTransactionEntry & { entry: IItemEntry })[] => { + const billEntriesByEntryId = transformToMap(billEntries, 'id'); + + return locatedEntries.map((entry) => ({ + ...entry, + entry: billEntriesByEntryId.get(entry.entryId), + })); +}; diff --git a/server/src/services/Purchases/constants.ts b/server/src/services/Purchases/constants.ts index 09b31979d..251598385 100644 --- a/server/src/services/Purchases/constants.ts +++ b/server/src/services/Purchases/constants.ts @@ -9,5 +9,8 @@ export const ERRORS = { BILL_ALREADY_OPEN: 'BILL_ALREADY_OPEN', BILL_NO_IS_REQUIRED: 'BILL_NO_IS_REQUIRED', BILL_HAS_ASSOCIATED_PAYMENT_ENTRIES: 'BILL_HAS_ASSOCIATED_PAYMENT_ENTRIES', - VENDOR_HAS_BILLS: 'VENDOR_HAS_BILLS' + VENDOR_HAS_BILLS: 'VENDOR_HAS_BILLS', + BILL_HAS_ASSOCIATED_LANDED_COSTS: 'BILL_HAS_ASSOCIATED_LANDED_COSTS', + BILL_ENTRIES_ALLOCATED_COST_COULD_DELETED: 'BILL_ENTRIES_ALLOCATED_COST_COULD_DELETED', + LOCATED_COST_ENTRIES_SHOULD_BIGGE_THAN_NEW_ENTRIES: 'LOCATED_COST_ENTRIES_SHOULD_BIGGE_THAN_NEW_ENTRIES' }; diff --git a/server/src/subscribers/Bills/WriteJournalEntries.ts b/server/src/subscribers/Bills/WriteJournalEntries.ts index 3a94f2c71..0741f6255 100644 --- a/server/src/subscribers/Bills/WriteJournalEntries.ts +++ b/server/src/subscribers/Bills/WriteJournalEntries.ts @@ -23,20 +23,20 @@ export default class BillSubscriber { * Handles writing journal entries once bill created. */ @On(events.bill.onCreated) - async handlerWriteJournalEntriesOnCreate({ tenantId, bill }) { + async handlerWriteJournalEntriesOnCreate({ tenantId, billId }) { // Writes the journal entries for the given bill transaction. this.logger.info('[bill] writing bill journal entries.', { tenantId }); - await this.billsService.recordJournalTransactions(tenantId, bill); + await this.billsService.recordJournalTransactions(tenantId, billId); } /** * Handles the overwriting journal entries once bill edited. */ @On(events.bill.onEdited) - async handleOverwriteJournalEntriesOnEdit({ tenantId, bill }) { + async handleOverwriteJournalEntriesOnEdit({ tenantId, billId }) { // Overwrite the journal entries for the given bill transaction. this.logger.info('[bill] overwriting bill journal entries.', { tenantId }); - await this.billsService.recordJournalTransactions(tenantId, bill, true); + await this.billsService.recordJournalTransactions(tenantId, billId, true); } /** diff --git a/server/src/subscribers/LandedCost/index.ts b/server/src/subscribers/LandedCost/index.ts new file mode 100644 index 000000000..ec31b83d9 --- /dev/null +++ b/server/src/subscribers/LandedCost/index.ts @@ -0,0 +1,37 @@ +import { Container } from 'typedi'; +import { On, EventSubscriber } from 'event-dispatch'; +import events from 'subscribers/events'; +import TenancyService from 'services/Tenancy/TenancyService'; +import BillsService from 'services/Purchases/Bills'; + +@EventSubscriber() +export default class BillLandedCostSubscriber { + logger: any; + tenancy: TenancyService; + billsService: BillsService; + + /** + * Constructor method. + */ + constructor() { + this.logger = Container.get('logger'); + this.tenancy = Container.get(TenancyService); + this.billsService = Container.get(BillsService); + } + + /** + * Marks the rewrite bill journal entries once the landed cost transaction + * be deleted or created. + */ + @On(events.billLandedCost.onCreated) + @On(events.billLandedCost.onDeleted) + public async handleRewriteBillJournalEntries({ + tenantId, + billId, + bilLandedCostId, + }) { + // Overwrite the journal entries for the given bill transaction. + this.logger.info('[bill] overwriting bill journal entries.', { tenantId }); + await this.billsService.recordJournalTransactions(tenantId, billId, true); + } +}