diff --git a/server/src/api/controllers/Purchases/Bills.ts b/server/src/api/controllers/Purchases/Bills.ts index dbea7503e..89c14e317 100644 --- a/server/src/api/controllers/Purchases/Bills.ts +++ b/server/src/api/controllers/Purchases/Bills.ts @@ -197,8 +197,8 @@ export default class BillsController extends BaseController { * @param {Response} res */ async editBill(req: Request, res: Response, next: NextFunction) { - const { id: billId, user } = req.params; - const { tenantId } = req; + const { id: billId } = req.params; + const { tenantId, user } = req; const billDTO: IBillEditDTO = this.matchedBodyData(req); try { diff --git a/server/src/interfaces/Bill.ts b/server/src/interfaces/Bill.ts index df111a888..dfe2b3acd 100644 --- a/server/src/interfaces/Bill.ts +++ b/server/src/interfaces/Bill.ts @@ -46,6 +46,7 @@ export interface IBill { openedAt: Date | string, entries: IItemEntry[], + userId: number, }; export interface IBillsFilter extends IDynamicListFilterDTO { diff --git a/server/src/repositories/AccountTransactionRepository.ts b/server/src/repositories/AccountTransactionRepository.ts index 69fe9076e..e0bbdfeba 100644 --- a/server/src/repositories/AccountTransactionRepository.ts +++ b/server/src/repositories/AccountTransactionRepository.ts @@ -11,6 +11,8 @@ interface IJournalTransactionsFilter { toAmount: number, contactsIds?: number[], contactType?: string, + referenceType?: string[], + referenceId?: number[], }; export default class AccountTransactionsRepository extends TenantRepository { @@ -42,6 +44,12 @@ export default class AccountTransactionsRepository extends TenantRepository { if (filter.contactType) { query.where('contact_type', filter.contactType); } + if (filter.referenceType && filter.referenceType.length > 0) { + query.whereIn('reference_type', filter.referenceType); + } + if (filter.referenceId && filter.referenceId.length > 0) { + query.whereIn('reference_id', filter.referenceId); + } }); }); } diff --git a/server/src/repositories/CachableRepository.ts b/server/src/repositories/CachableRepository.ts index 8bbd25874..7c0401e48 100644 --- a/server/src/repositories/CachableRepository.ts +++ b/server/src/repositories/CachableRepository.ts @@ -177,7 +177,20 @@ export default class CachableRepository extends EntityRepository{ * * @param {string|number[]} values - */ - async deleteWhereIn(values: string | number[]) { + async deleteWhereIn(field: string, values: string | number[]) { + const result = await super.deleteWhereIn(field, values); + + // Flushes the repository cache after delete operation. + this.flushCache(); + + return result; + } + + /** + * + * @param {string|number[]} values + */ + async deleteWhereIdIn(values: string | number[]) { const result = await super.deleteWhereIdIn(values); // Flushes the repository cache after delete operation. diff --git a/server/src/repositories/EntityRepository.ts b/server/src/repositories/EntityRepository.ts index a015e6081..e948d6068 100644 --- a/server/src/repositories/EntityRepository.ts +++ b/server/src/repositories/EntityRepository.ts @@ -175,7 +175,7 @@ export default class EntityRepository { } /** - * + * Deletes the given entries in the array on the specific field. * @param {string} field - * @param {number|string} values - */ diff --git a/server/src/services/Accounting/JournalCommands.ts b/server/src/services/Accounting/JournalCommands.ts index 0a5e15b13..e1d999bf3 100644 --- a/server/src/services/Accounting/JournalCommands.ts +++ b/server/src/services/Accounting/JournalCommands.ts @@ -1,4 +1,6 @@ import { sumBy, chain } from 'lodash'; +import moment from 'moment'; +import { IBill } from 'interfaces'; import JournalPoster from "./JournalPoster"; import JournalEntry from "./JournalEntry"; import { AccountTransaction } from 'models'; @@ -7,6 +9,7 @@ import { IManualJournal, IExpense, IExpenseCategory, + IItem, } from 'interfaces'; interface IInventoryCostEntity { @@ -57,6 +60,68 @@ export default class JournalCommands{ this.models = this.journal.models; } + /** + * Records the bill journal entries. + * @param {IBill} bill + * @param {boolean} override - Override the old bill entries. + */ + 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'); + + const commonJournalMeta = { + debit: 0, + credit: 0, + referenceId: bill.id, + referenceType: 'Bill', + date: formattedDate, + userId: bill.userId, + }; + // 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, + account: payableAccount.id, + contactId: bill.vendorId, + contactType: 'Vendor', + index: 1, + }); + this.journal.credit(payableEntry); + + bill.entries.forEach((entry, index) => { + const item: IItem = storedItemsMap.get(entry.itemId); + const amount = ItemEntry.calcAmount(entry); + + const debitEntry = new JournalEntry({ + ...commonJournalMeta, + debit: amount, + account: + ['inventory'].indexOf(item.type) !== -1 + ? item.inventoryAccountId + : item.costAccountId, + index: index + 2, + }); + this.journal.debit(debitEntry); + }); + } + /** * Customer opening balance journals. * @param {number} customerId diff --git a/server/src/services/Accounting/JournalPoster.ts b/server/src/services/Accounting/JournalPoster.ts index 3aa3c2572..f7569607b 100644 --- a/server/src/services/Accounting/JournalPoster.ts +++ b/server/src/services/Accounting/JournalPoster.ts @@ -21,7 +21,7 @@ export default class JournalPoster implements IJournalPoster { deletedEntriesIds: number[] = []; entries: IJournalEntry[] = []; balancesChange: IAccountsChange = {}; - accountsDepGraph: IAccountsChange = {}; + accountsDepGraph: IAccountsChange; accountsBalanceTable: { [key: number]: number; } = {}; @@ -250,12 +250,12 @@ export default class JournalPoster implements IJournalPoster { * @returns {Promise} */ public async saveEntries() { - const { AccountTransaction } = this.models; + const { transactionsRepository } = this.repositories; const saveOperations: Promise[] = []; this.entries.forEach((entry) => { - const oper = AccountTransaction.query() - .insert({ + const oper = transactionsRepository + .create({ accountId: entry.account, ...omit(entry, ['account']), }); @@ -309,12 +309,10 @@ export default class JournalPoster implements IJournalPoster { * @return {Promise} */ public async deleteEntries() { - const { AccountTransaction } = this.models; + const { transactionsRepository } = this.repositories; if (this.deletedEntriesIds.length > 0) { - await AccountTransaction.query() - .whereIn('id', this.deletedEntriesIds) - .delete(); + await transactionsRepository.deleteWhereIdIn(this.deletedEntriesIds); } } @@ -332,7 +330,6 @@ export default class JournalPoster implements IJournalPoster { }); } - /** * Calculates the entries balance change. * @public diff --git a/server/src/services/Purchases/Bills.ts b/server/src/services/Purchases/Bills.ts index fab143c62..ecfbfd734 100644 --- a/server/src/services/Purchases/Bills.ts +++ b/server/src/services/Purchases/Bills.ts @@ -27,6 +27,7 @@ import { import { ServiceError } from 'exceptions'; import ItemsService from 'services/Items/ItemsService'; import ItemsEntriesService from 'services/Items/ItemsEntriesService'; +import JournalCommands from 'services/Accounting/JournalCommands'; const ERRORS = { BILL_NOT_FOUND: 'BILL_NOT_FOUND', @@ -139,8 +140,8 @@ export default class BillsService extends SalesInvoicesCost { private async billDTOToModel( tenantId: number, billDTO: IBillDTO | IBillEditDTO, - oldBill?: IBill, authorizedUser: ISystemUser, + oldBill?: IBill, ) { const { ItemEntry } = this.tenancy.models(tenantId); let invLotNumber = oldBill?.invLotNumber; @@ -156,7 +157,7 @@ export default class BillsService extends SalesInvoicesCost { return { ...formatDateFields( - omit(billDTO, ['open']), + omit(billDTO, ['open', 'entries']), ['billDate', 'dueDate'] ), amount, @@ -165,7 +166,6 @@ export default class BillsService extends SalesInvoicesCost { reference_type: 'Bill', ...omit(entry, ['amount', 'id']), })), - // Avoid rewrite the open date in edit mode when already opened. ...(billDTO.open && (!oldBill?.openedAt)) && ({ openedAt: moment().toMySqlDateTime(), @@ -197,7 +197,7 @@ export default class BillsService extends SalesInvoicesCost { const { Bill } = this.tenancy.models(tenantId); this.logger.info('[bill] trying to create a new bill', { tenantId, billDTO }); - const billObj = await this.billDTOToModel(tenantId, billDTO, null, authorizedUser); + const billObj = await this.billDTOToModel(tenantId, billDTO, authorizedUser, null); // Retrieve vendor or throw not found service error. await this.getVendorOrThrowError(tenantId, billDTO.vendorId); @@ -253,7 +253,7 @@ export default class BillsService extends SalesInvoicesCost { const oldBill = await this.getBillOrThrowError(tenantId, billId); // Transforms the bill DTO object to model object. - const billObj = await this.billDTOToModel(tenantId, billDTO, oldBill, authorizedUser); + const billObj = await this.billDTOToModel(tenantId, billDTO, authorizedUser, oldBill); // Retrieve vendor details or throw not found service error. await this.getVendorOrThrowError(tenantId, billDTO.vendorId); @@ -342,62 +342,16 @@ export default class BillsService extends SalesInvoicesCost { * @param {IBill} bill * @param {Integer} billId */ - public async recordJournalTransactions(tenantId: number, bill: IBill, billId?: number) { - const { AccountTransaction, Item, ItemEntry } = this.tenancy.models(tenantId); - const { accountRepository } = this.tenancy.repositories(tenantId); - - const entriesItemsIds = bill.entries.map((entry) => entry.itemId); - const formattedDate = moment(bill.billDate).format('YYYY-MM-DD'); - - const storedItems = await Item.query().whereIn('id', entriesItemsIds); - - const storedItemsMap = new Map(storedItems.map((item) => [item.id, item])); - const payableAccount = await accountRepository.find({ slug: 'accounts-payable' }); - + public async recordJournalTransactions( + tenantId: number, + bill: IBill, + override: boolean = false, + ) { const journal = new JournalPoster(tenantId); + const journalCommands = new JournalCommands(journal); - const commonJournalMeta = { - debit: 0, - credit: 0, - referenceId: bill.id, - referenceType: 'Bill', - date: formattedDate, - userId: bill.userId, - }; - if (billId) { - const transactions = await AccountTransaction.query() - .whereIn('reference_type', ['Bill']) - .whereIn('reference_id', [billId]) - .withGraphFetched('account.type'); + await journalCommands.bill(bill, override) - journal.loadEntries(transactions); - journal.removeEntries(); - } - const payableEntry = new JournalEntry({ - ...commonJournalMeta, - credit: bill.amount, - account: payableAccount.id, - contactId: bill.vendorId, - contactType: 'Vendor', - index: 1, - }); - journal.credit(payableEntry); - - bill.entries.forEach((entry, index) => { - const item: IItem = storedItemsMap.get(entry.itemId); - const amount = ItemEntry.calcAmount(entry); - - const debitEntry = new JournalEntry({ - ...commonJournalMeta, - debit: amount, - account: - ['inventory'].indexOf(item.type) !== -1 - ? item.inventoryAccountId - : item.costAccountId, - index: index + 2, - }); - journal.debit(debitEntry); - }); return Promise.all([ journal.deleteEntries(), journal.saveEntries(), diff --git a/server/src/subscribers/bills.ts b/server/src/subscribers/bills.ts index 080b7588d..d4ab5f1a8 100644 --- a/server/src/subscribers/bills.ts +++ b/server/src/subscribers/bills.ts @@ -37,11 +37,20 @@ export default class BillSubscriber { * Handles writing journal entries once bill created. */ @On(events.bill.onCreated) - @On(events.bill.onEdited) - async handlerWriteJournalEntries({ tenantId, billId, bill }) { + async handlerWriteJournalEntriesOnCreate({ tenantId, bill }) { // Writes the journal entries for the given bill transaction. this.logger.info('[bill] writing bill journal entries.', { tenantId }); - await this.billsService.recordJournalTransactions(tenantId, bill, billId); + await this.billsService.recordJournalTransactions(tenantId, bill); + } + + /** + * Handles the overwriting journal entries once bill edited. + */ + @On(events.bill.onEdited) + async handleOverwriteJournalEntriesOnEdit({ tenantId, bill }) { + // 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); } /** diff --git a/server/src/subscribers/saleInvoices.ts b/server/src/subscribers/saleInvoices.ts index 5a7226cc8..6fc279471 100644 --- a/server/src/subscribers/saleInvoices.ts +++ b/server/src/subscribers/saleInvoices.ts @@ -1,9 +1,10 @@ import { Container } from 'typedi'; -import { On, EventSubscriber } from "event-dispatch"; +import { On, EventSubscriber } from 'event-dispatch'; import events from 'subscribers/events'; import TenancyService from 'services/Tenancy/TenancyService'; import SettingsService from 'services/Settings/SettingsService'; import SaleEstimateService from 'services/Sales/SalesEstimate'; + @EventSubscriber() export default class SaleInvoiceSubscriber { logger: any; @@ -22,23 +23,36 @@ export default class SaleInvoiceSubscriber { * Handles customer balance increment once sale invoice created. */ @On(events.saleInvoice.onCreated) - public async handleCustomerBalanceIncrement({ tenantId, saleInvoice, saleInvoiceId }) { + public async handleCustomerBalanceIncrement({ + tenantId, + saleInvoice, + saleInvoiceId, + }) { const { customerRepository } = this.tenancy.repositories(tenantId); - this.logger.info('[sale_invoice] trying to increment customer balance.', { tenantId }); - await customerRepository.changeBalance(saleInvoice.customerId, saleInvoice.balance); + this.logger.info('[sale_invoice] trying to increment customer balance.', { + tenantId, + }); + await customerRepository.changeBalance( + saleInvoice.customerId, + saleInvoice.balance + ); } /** - * + * Marks the sale estimate as converted from the sale invoice once created. */ @On(events.saleInvoice.onCreated) - public async handleMarkEstimateConvert({ tenantId, saleInvoice, saleInvoiceId }) { - if (saleInvoice.fromEstiamteId) { + public async handleMarkEstimateConvert({ + tenantId, + saleInvoice, + saleInvoiceId, + }) { + if (saleInvoice.fromEstimateId) { this.saleEstimatesService.convertEstimateToInvoice( tenantId, saleInvoice.fromEstiamteId, - saleInvoiceId, + saleInvoiceId ); } } @@ -47,29 +61,42 @@ export default class SaleInvoiceSubscriber { * Handles customer balance diff balnace change once sale invoice edited. */ @On(events.saleInvoice.onEdited) - public async onSaleInvoiceEdited({ tenantId, saleInvoice, oldSaleInvoice, saleInvoiceId }) { + public async onSaleInvoiceEdited({ + tenantId, + saleInvoice, + oldSaleInvoice, + saleInvoiceId, + }) { const { customerRepository } = this.tenancy.repositories(tenantId); - this.logger.info('[sale_invoice] trying to change diff customer balance.', { tenantId }); + this.logger.info('[sale_invoice] trying to change diff customer balance.', { + tenantId, + }); await customerRepository.changeDiffBalance( saleInvoice.customerId, saleInvoice.balance, oldSaleInvoice.balance, - oldSaleInvoice.customerId, - ) + oldSaleInvoice.customerId + ); } /** * Handles customer balance decrement once sale invoice deleted. */ @On(events.saleInvoice.onDeleted) - public async handleCustomerBalanceDecrement({ tenantId, saleInvoiceId, oldSaleInvoice }) { + public async handleCustomerBalanceDecrement({ + tenantId, + saleInvoiceId, + oldSaleInvoice, + }) { const { customerRepository } = this.tenancy.repositories(tenantId); - this.logger.info('[sale_invoice] trying to decrement customer balance.', { tenantId }); - await customerRepository.changeBalance( + this.logger.info('[sale_invoice] trying to decrement customer balance.', { + tenantId, + }); + await customerRepository.changeBalance( oldSaleInvoice.customerId, - oldSaleInvoice.balance * -1, + oldSaleInvoice.balance * -1 ); } @@ -77,10 +104,14 @@ export default class SaleInvoiceSubscriber { * Handles sale invoice next number increment once invoice created. */ @On(events.saleInvoice.onCreated) - public async handleInvoiceNextNumberIncrement({ tenantId, saleInvoiceId, saleInvoice }) { + public async handleInvoiceNextNumberIncrement({ + tenantId, + saleInvoiceId, + saleInvoice, + }) { await this.settingsService.incrementNextNumber(tenantId, { key: 'next_number', group: 'sales_invoices', }); } -} \ No newline at end of file +}