diff --git a/server/src/api/controllers/Sales/SalesInvoices.ts b/server/src/api/controllers/Sales/SalesInvoices.ts index 84e543664..cb8ae6fc6 100644 --- a/server/src/api/controllers/Sales/SalesInvoices.ts +++ b/server/src/api/controllers/Sales/SalesInvoices.ts @@ -272,10 +272,12 @@ export default class SaleInvoicesController extends BaseController { next: NextFunction ) { const { tenantId } = req; - const filter: ISalesInvoicesFilter = { + const filter = { filterRoles: [], sortOrder: 'asc', columnSortBy: 'created_at', + page: 1, + pageSize: 12, ...this.matchedQueryData(req), }; if (filter.stringifiedFilterRoles) { diff --git a/server/src/config/index.js b/server/src/config/index.js index 2d5df41d7..68c26ce34 100644 --- a/server/src/config/index.js +++ b/server/src/config/index.js @@ -164,4 +164,5 @@ export default { protocol: '', hostname: '', + scheduleComputeItemCost: 'in 5 seconds' }; \ No newline at end of file diff --git a/server/src/interfaces/SaleInvoice.ts b/server/src/interfaces/SaleInvoice.ts index 63a40bf96..554f2cb34 100644 --- a/server/src/interfaces/SaleInvoice.ts +++ b/server/src/interfaces/SaleInvoice.ts @@ -9,7 +9,8 @@ export interface ISaleInvoice { dueAmount: number, customerId: number, entries: IItemEntry[], - deliveredAt: string|Date, + deliveredAt: string | Date, + userId: number, } export interface ISaleInvoiceDTO { diff --git a/server/src/jobs/writeInvoicesJEntries.ts b/server/src/jobs/writeInvoicesJEntries.ts index f48725c60..4523dcb62 100644 --- a/server/src/jobs/writeInvoicesJEntries.ts +++ b/server/src/jobs/writeInvoicesJEntries.ts @@ -2,12 +2,11 @@ import { Container } from 'typedi'; import SalesInvoicesCost from 'services/Sales/SalesInvoicesCost'; export default class WriteInvoicesJournalEntries { - constructor(agenda) { agenda.define( 'rewrite-invoices-journal-entries', - { priority: 'normal', concurrency: 1, }, - this.handler.bind(this), + { priority: 'normal', concurrency: 1 }, + this.handler.bind(this) ); } @@ -17,15 +16,24 @@ export default class WriteInvoicesJournalEntries { const salesInvoicesCost = Container.get(SalesInvoicesCost); - Logger.info(`Write sales invoices journal entries - started: ${job.attrs.data}`); - + Logger.info( + `Write sales invoices journal entries - started: ${job.attrs.data}` + ); try { - await salesInvoicesCost.writeJournalEntries(tenantId, startingDate, true); - Logger.info(`Write sales invoices journal entries - completed: ${job.attrs.data}`); + await salesInvoicesCost.writeInventoryCostJournalEntries( + tenantId, + startingDate, + true + ); + Logger.info( + `Write sales invoices journal entries - completed: ${job.attrs.data}` + ); done(); - } catch(e) { - Logger.info(`Write sales invoices journal entries: ${job.attrs.data}, error: ${e}`); - done(e); + } catch (e) { + Logger.info( + `Write sales invoices journal entries: ${job.attrs.data}, error: ${e}` + ); + done(e); } } -} \ No newline at end of file +} diff --git a/server/src/loaders/dependencyInjector.ts b/server/src/loaders/dependencyInjector.ts index 97ed5722e..c0e66572c 100644 --- a/server/src/loaders/dependencyInjector.ts +++ b/server/src/loaders/dependencyInjector.ts @@ -7,7 +7,6 @@ import dbManagerFactory from 'loaders/dbManager'; import i18n from 'loaders/i18n'; import repositoriesLoader from 'loaders/systemRepositories'; import Cache from 'services/Cache'; -import redisLoader from './redisLoader'; import rateLimiterLoaders from './rateLimiterLoader'; export default ({ mongoConnection, knex }) => { diff --git a/server/src/models/InventoryCostLotTracker.js b/server/src/models/InventoryCostLotTracker.js index 351e01a7b..83df8f0dd 100644 --- a/server/src/models/InventoryCostLotTracker.js +++ b/server/src/models/InventoryCostLotTracker.js @@ -23,14 +23,13 @@ export default class InventoryCostLotTracker extends TenantModel { static get modifiers() { return { groupedEntriesCost(query) { - query.select(['entry_id', 'transaction_id', 'transaction_type']); + query.select(['date', 'item_id', 'transaction_id', 'transaction_type']); + query.sum('cost as cost'); - query.groupBy('item_id'); - query.groupBy('entry_id'); query.groupBy('transaction_id'); query.groupBy('transaction_type'); - - query.sum('cost as cost'); + query.groupBy('date'); + query.groupBy('item_id'); }, filterDateRange(query, startDate, endDate, type = 'day') { const dateFormat = 'YYYY-MM-DD HH:mm:ss'; diff --git a/server/src/repositories/AccountTransactionRepository.ts b/server/src/repositories/AccountTransactionRepository.ts index e0bbdfeba..25e160a1d 100644 --- a/server/src/repositories/AccountTransactionRepository.ts +++ b/server/src/repositories/AccountTransactionRepository.ts @@ -13,6 +13,7 @@ interface IJournalTransactionsFilter { contactType?: string, referenceType?: string[], referenceId?: number[], + index: number|number[] }; export default class AccountTransactionsRepository extends TenantRepository { @@ -50,6 +51,13 @@ export default class AccountTransactionsRepository extends TenantRepository { if (filter.referenceId && filter.referenceId.length > 0) { query.whereIn('reference_id', filter.referenceId); } + if (filter.index) { + if (Array.isArray(filter.index)) { + query.whereIn('index', filter.index); + } else { + query.where('index', filter.index); + } + } }); }); } diff --git a/server/src/services/Accounting/JournalCommands.ts b/server/src/services/Accounting/JournalCommands.ts index cfdc96bbc..f205e6444 100644 --- a/server/src/services/Accounting/JournalCommands.ts +++ b/server/src/services/Accounting/JournalCommands.ts @@ -1,5 +1,5 @@ import { sumBy, chain } from 'lodash'; -import moment from 'moment'; +import moment, { LongDateFormatKey } from 'moment'; import { IBill, IManualJournalEntry, ISystemUser } from 'interfaces'; import JournalPoster from './JournalPoster'; import JournalEntry from './JournalEntry'; @@ -183,7 +183,7 @@ export default class JournalCommands { async vendorOpeningBalance( vendorId: number, openingBalance: number, - openingBalanceAt: Date|string, + openingBalanceAt: Date | string, authorizedUserId: ISystemUser ) { const { accountRepository } = this.repositories; @@ -225,10 +225,7 @@ export default class JournalCommands { * Writes journal entries of expense model object. * @param {IExpense} expense */ - expense( - expense: IExpense, - userId: number, - ) { + expense(expense: IExpense, userId: number) { const mixinEntry = { referenceType: 'Expense', referenceId: expense.id, @@ -279,14 +276,50 @@ export default class JournalCommands { this.journal.removeEntries(); } + /** + * Reverts the sale invoice cost journal entries. + * @param {Date|string} startingDate + * @return {Promise} + */ + async revertInventoryCostJournalEntries( + startingDate: Date | string + ): Promise { + const { transactionsRepository } = this.repositories; + + const transactions = await transactionsRepository.journal({ + fromDate: startingDate, + referenceType: ['SaleInvoice'], + index: [3, 4], + }); + console.log(transactions); + + this.journal.fromTransactions(transactions); + this.journal.removeEntries(); + } + + /** + * Reverts sale invoice the income journal entries. + * @param {number} saleInvoiceId + */ + async revertInvoiceIncomeEntries( + saleInvoiceId: number, + ) { + const { transactionsRepository } = this.repositories; + + const transactions = await transactionsRepository.journal({ + referenceType: ['SaleInvoice'], + referenceId: [saleInvoiceId], + }); + this.journal.fromTransactions(transactions); + this.journal.removeEntries(); + } + /** * Writes journal entries from manual journal model object. * @param {IManualJournal} manualJournalObj * @param {number} manualJournalId */ - async manualJournal( - manualJournalObj: IManualJournal, - ) { + async manualJournal(manualJournalObj: IManualJournal) { manualJournalObj.entries.forEach((entry: IManualJournalEntry) => { const jouranlEntry = new JournalEntry({ debit: entry.debit, @@ -310,261 +343,68 @@ export default class JournalCommands { }); } - /** - * Removes and revert accounts balance journal entries that associated - * to the given inventory transactions. - * @param {IInventoryTransaction[]} inventoryTransactions - * @param {Journal} journal - */ - revertEntriesFromInventoryTransactions( - inventoryTransactions: IInventoryTransaction[] - ) { - const groupedInvTransactions = chain(inventoryTransactions) - .groupBy( - (invTransaction: IInventoryTransaction) => - invTransaction.transactionType - ) - .map((groupedTrans: IInventoryTransaction[], transType: string) => [ - groupedTrans, - transType, - ]) - .value(); - - return Promise.all( - groupedInvTransactions.map( - async (grouped: [IInventoryTransaction[], string]) => { - const [invTransGroup, referenceType] = grouped; - const referencesIds = invTransGroup.map( - (trans: IInventoryTransaction) => trans.transactionId - ); - - const _transactions = await AccountTransaction.tenant() - .query() - .where('reference_type', referenceType) - .whereIn('reference_id', referencesIds) - .withGraphFetched('account.type'); - - if (_transactions.length > 0) { - this.journal.loadEntries(_transactions); - this.journal.removeEntries(_transactions.map((t: any) => t.id)); - } - } - ) - ); - } - - public async nonInventoryEntries(transactions: NonInventoryJEntries[]) { - const receivableAccount = { id: 10 }; - const payableAccount = { id: 11 }; - - transactions.forEach((trans: NonInventoryJEntries) => { - const commonEntry = { - date: trans.date, - referenceId: trans.referenceId, - referenceType: trans.referenceType, - }; - - switch (trans.referenceType) { - case 'Bill': - const payableEntry: JournalEntry = new JournalEntry({ - ...commonEntry, - credit: trans.payable, - account: payableAccount.id, - }); - const costEntry: JournalEntry = new JournalEntry({ - ...commonEntry, - }); - this.journal.credit(payableEntry); - this.journal.debit(costEntry); - break; - case 'SaleInvoice': - const receivableEntry: JournalEntry = new JournalEntry({ - ...commonEntry, - debit: trans.receivable, - account: receivableAccount.id, - }); - const saleIncomeEntry: JournalEntry = new JournalEntry({ - ...commonEntry, - credit: trans.income, - account: trans.incomeAccountId, - }); - this.journal.debit(receivableEntry); - this.journal.credit(saleIncomeEntry); - break; - } - }); - } - - /** - * - * @param {string} referenceType - - * @param {number} referenceId - - * @param {ISaleInvoice[]} sales - - */ - public async inventoryEntries(transactions: IInventoryCostEntity[]) { - const receivableAccount = { id: 10 }; - const payableAccount = { id: 11 }; - - transactions.forEach((sale: IInventoryCostEntity) => { - const commonEntry = { - date: sale.date, - referenceId: sale.referenceId, - referenceType: sale.referenceType, - }; - switch (sale.referenceType) { - case 'Bill': - const inventoryDebit: JournalEntry = new JournalEntry({ - ...commonEntry, - debit: sale.inventory, - account: sale.inventoryAccount, - }); - const payableEntry: JournalEntry = new JournalEntry({ - ...commonEntry, - credit: sale.inventory, - account: payableAccount.id, - }); - this.journal.debit(inventoryDebit); - this.journal.credit(payableEntry); - break; - case 'SaleInvoice': - const receivableEntry: JournalEntry = new JournalEntry({ - ...commonEntry, - debit: sale.income, - account: receivableAccount.id, - }); - const incomeEntry: JournalEntry = new JournalEntry({ - ...commonEntry, - credit: sale.income, - account: sale.incomeAccount, - }); - // Cost journal transaction. - const costEntry: JournalEntry = new JournalEntry({ - ...commonEntry, - debit: sale.cost, - account: sale.costAccount, - }); - const inventoryCredit: JournalEntry = new JournalEntry({ - ...commonEntry, - credit: sale.cost, - account: sale.inventoryAccount, - }); - this.journal.debit(receivableEntry); - this.journal.debit(costEntry); - - this.journal.credit(incomeEntry); - this.journal.credit(inventoryCredit); - break; - } - }); - } - /** * Writes journal entries for given sale invoice. - * ---------- - * - Receivable accounts -> Debit -> XXXX - * - Income -> Credit -> XXXX - * + * ------- * - Cost of goods sold -> Debit -> YYYY - * - Inventory assets -> YYYY + * - Inventory assets -> Credit -> YYYY * * @param {ISaleInvoice} saleInvoice * @param {JournalPoster} journal */ - saleInvoice( - saleInvoice: ISaleInvoice & { - costTransactions: IInventoryLotCost[]; - entries: IItemEntry & { item: IItem }; - }, - receivableAccountsId: number + saleInvoiceInventoryCost( + inventoryCostLot: IInventoryLotCost & { item: IItem } ) { - let inventoryTotal: number = 0; - const commonEntry = { referenceType: 'SaleInvoice', - referenceId: saleInvoice.id, - date: saleInvoice.invoiceDate, + referenceId: inventoryCostLot.transactionId, + date: inventoryCostLot.date, }; - const costTransactions: Map = new Map( - saleInvoice.costTransactions.map((trans: IInventoryLotCost) => [ - trans.entryId, - trans.cost, - ]) - ); - // XXX Debit - Receivable account. - const receivableEntry = new JournalEntry({ + // XXX Debit - Cost account. + const costEntry = new JournalEntry({ ...commonEntry, - debit: saleInvoice.balance, - account: receivableAccountsId, - index: 1, + debit: inventoryCostLot.cost, + account: inventoryCostLot.item.costAccountId, + index: 3, }); - this.journal.debit(receivableEntry); - - saleInvoice.entries.forEach( - (entry: IItemEntry & { item: IItem }, index) => { - const cost: number = costTransactions.get(entry.id); - const income: number = entry.quantity * entry.rate; - - if (entry.item.type === 'inventory' && cost) { - // XXX Debit - Cost account. - const costEntry = new JournalEntry({ - ...commonEntry, - debit: cost, - account: entry.item.costAccountId, - note: entry.description, - index: index + 3, - }); - this.journal.debit(costEntry); - inventoryTotal += cost; - } - // XXX Credit - Income account. - const incomeEntry = new JournalEntry({ - ...commonEntry, - credit: income, - account: entry.item.sellAccountId, - note: entry.description, - index: index + 2, - }); - this.journal.credit(incomeEntry); - - if (inventoryTotal > 0) { - // XXX Credit - Inventory account. - const inventoryEntry = new JournalEntry({ - ...commonEntry, - credit: inventoryTotal, - account: entry.item.inventoryAccountId, - index: index + 4, - }); - this.journal.credit(inventoryEntry); - } - } - ); + // XXX Credit - Inventory account. + const inventoryEntry = new JournalEntry({ + ...commonEntry, + credit: inventoryCostLot.cost, + account: inventoryCostLot.item.inventoryAccountId, + index: 4, + }); + this.journal.credit(inventoryEntry); + this.journal.debit(costEntry); } /** + * Writes the sale invoice income journal entries. + * ----- + * - Receivable accounts -> Debit -> XXXX + * - Income -> Credit -> XXXX * - * @param {ISaleInvoice} saleInvoice - * @param {number} receivableAccountsId - * @param {number} authorizedUserId + * @param {ISaleInvoice} saleInvoice + * @param {number} receivableAccountsId + * @param {number} authorizedUserId */ - saleInvoiceNonInventory( + async saleInvoiceIncomeEntries( saleInvoice: ISaleInvoice & { entries: IItemEntry & { item: IItem }; }, - receivableAccountsId: number, - authorizedUserId: number, - ) { + receivableAccountId: number + ): Promise { const commonEntry = { referenceType: 'SaleInvoice', referenceId: saleInvoice.id, date: saleInvoice.invoiceDate, - userId: authorizedUserId, + userId: saleInvoice.userId, }; - // XXX Debit - Receivable account. const receivableEntry = new JournalEntry({ ...commonEntry, debit: saleInvoice.balance, - account: receivableAccountsId, + account: receivableAccountId, index: 1, }); this.journal.debit(receivableEntry); diff --git a/server/src/services/Contacts/CustomersService.ts b/server/src/services/Contacts/CustomersService.ts index 31028b1f5..8b05da20e 100644 --- a/server/src/services/Contacts/CustomersService.ts +++ b/server/src/services/Contacts/CustomersService.ts @@ -17,7 +17,6 @@ import { IContactEditDTO, IContact, ISaleInvoice, - ISystemService, ISystemUser, } from 'interfaces'; import { ServiceError } from 'exceptions'; diff --git a/server/src/services/Inventory/Inventory.ts b/server/src/services/Inventory/Inventory.ts index 714d001bd..270d8fe24 100644 --- a/server/src/services/Inventory/Inventory.ts +++ b/server/src/services/Inventory/Inventory.ts @@ -1,10 +1,16 @@ import { Container, Service, Inject } from 'typedi'; import { pick } from 'lodash'; +import config from 'config'; import { EventDispatcher, EventDispatcherInterface, } from 'decorators/eventDispatcher'; -import { IInventoryLotCost, IInventoryTransaction, IItem, IItemEntry } from 'interfaces' +import { + IInventoryLotCost, + IInventoryTransaction, + IItem, + IItemEntry, +} from 'interfaces'; import InventoryAverageCost from 'services/Inventory/InventoryAverageCost'; import InventoryCostLotTracker from 'services/Inventory/InventoryCostLotTracker'; import TenancyService from 'services/Tenancy/TenancyService'; @@ -27,9 +33,9 @@ export default class InventoryService { itemEntries: IItemEntry[], transactionType: string, transactionId: number, - direction: 'IN'|'OUT', - date: Date|string, - lotNumber: number, + direction: 'IN' | 'OUT', + date: Date | string, + lotNumber: number ) { return itemEntries.map((entry: IItemEntry) => ({ ...pick(entry, ['itemId', 'quantity', 'rate']), @@ -62,13 +68,21 @@ export default class InventoryService { let costMethodComputer: IInventoryCostMethod; // Switch between methods based on the item cost method. - switch('AVG') { + switch ('AVG') { case 'FIFO': case 'LIFO': - costMethodComputer = new InventoryCostLotTracker(tenantId, fromDate, itemId); + costMethodComputer = new InventoryCostLotTracker( + tenantId, + fromDate, + itemId + ); break; case 'AVG': - costMethodComputer = new InventoryAverageCost(tenantId, fromDate, itemId); + costMethodComputer = new InventoryAverageCost( + tenantId, + fromDate, + itemId + ); break; } return costMethodComputer.computeItemCost(); @@ -77,20 +91,24 @@ export default class InventoryService { /** * Schedule item cost compute job. * @param {number} tenantId - * @param {number} itemId - * @param {Date} startingDate + * @param {number} itemId + * @param {Date} startingDate */ - async scheduleComputeItemCost(tenantId: number, itemId: number, startingDate: Date|string) { + async scheduleComputeItemCost( + tenantId: number, + itemId: number, + startingDate: Date | string + ) { const agenda = Container.get('agenda'); - // Cancel any `compute-item-cost` in the queue has upper starting date + // Cancel any `compute-item-cost` in the queue has upper starting date // with the same given item. await agenda.cancel({ name: 'compute-item-cost', nextRunAt: { $ne: null }, 'data.tenantId': tenantId, 'data.itemId': itemId, - 'data.startingDate': { "$gt": startingDate } + 'data.startingDate': { $gt: startingDate }, }); // Retrieve any `compute-item-cost` in the queue has lower starting date @@ -100,23 +118,29 @@ export default class InventoryService { nextRunAt: { $ne: null }, 'data.tenantId': tenantId, 'data.itemId': itemId, - 'data.startingDate': { "$lte": startingDate } + 'data.startingDate': { $lte: startingDate }, }); if (dependsJobs.length === 0) { - await agenda.schedule('in 30 seconds', 'compute-item-cost', { - startingDate, itemId, tenantId, - }); + await agenda.schedule( + config.scheduleComputeItemCost, + 'compute-item-cost', + { + startingDate, + itemId, + tenantId, + } + ); // Triggers `onComputeItemCostJobScheduled` event. await this.eventDispatcher.dispatch( events.inventory.onComputeItemCostJobScheduled, - { startingDate, itemId, tenantId }, + { startingDate, itemId, tenantId } ); } } /** - * Records the inventory transactions. + * Records the inventory transactions. * @param {number} tenantId - Tenant id. * @param {Bill} bill - Bill model object. * @param {number} billId - Bill id. @@ -125,27 +149,23 @@ export default class InventoryService { async recordInventoryTransactions( tenantId: number, inventoryEntries: IInventoryTransaction[], - deleteOld: boolean, + deleteOld: boolean ): Promise { inventoryEntries.forEach(async (entry: IInventoryTransaction) => { - await this.recordInventoryTransaction( - tenantId, - entry, - deleteOld, - ); + await this.recordInventoryTransaction(tenantId, entry, deleteOld); }); } /** - * - * @param {number} tenantId - * @param {IInventoryTransaction} inventoryEntry - * @param {boolean} deleteOld + * + * @param {number} tenantId + * @param {IInventoryTransaction} inventoryEntry + * @param {boolean} deleteOld */ async recordInventoryTransaction( tenantId: number, inventoryEntry: IInventoryTransaction, - deleteOld: boolean = false, + deleteOld: boolean = false ): Promise { const { InventoryTransaction, Item } = this.tenancy.models(tenantId); @@ -153,7 +173,7 @@ export default class InventoryService { await this.deleteInventoryTransactions( tenantId, inventoryEntry.transactionId, - inventoryEntry.transactionType, + inventoryEntry.transactionType ); } return InventoryTransaction.query().insert({ @@ -165,14 +185,14 @@ export default class InventoryService { /** * Deletes the given inventory transactions. * @param {number} tenantId - Tenant id. - * @param {string} transactionType - * @param {number} transactionId + * @param {string} transactionType + * @param {number} transactionId * @return {Promise} */ async deleteInventoryTransactions( tenantId: number, transactionId: number, - transactionType: string, + transactionType: string ): Promise { const { InventoryTransaction } = this.tenancy.models(tenantId); @@ -184,16 +204,16 @@ export default class InventoryService { /** * Records the inventory cost lot transaction. - * @param {number} tenantId - * @param {IInventoryLotCost} inventoryLotEntry + * @param {number} tenantId + * @param {IInventoryLotCost} inventoryLotEntry * @return {Promise} */ async recordInventoryCostLotTransaction( tenantId: number, - inventoryLotEntry: IInventoryLotCost, + inventoryLotEntry: IInventoryLotCost ): Promise { const { InventoryCostLotTracker } = this.tenancy.models(tenantId); - + return InventoryCostLotTracker.query().insert({ ...inventoryLotEntry, }); @@ -209,13 +229,14 @@ export default class InventoryService { const LOT_NUMBER_KEY = 'lot_number_increment'; const storedLotNumber = settings.find({ key: LOT_NUMBER_KEY }); - return (storedLotNumber && storedLotNumber.value) ? - parseInt(storedLotNumber.value, 10) : 1; + return storedLotNumber && storedLotNumber.value + ? parseInt(storedLotNumber.value, 10) + : 1; } /** * Increment the next inventory LOT number. - * @param {number} tenantId + * @param {number} tenantId * @return {Promise} */ async incrementNextLotNumber(tenantId: number) { @@ -236,4 +257,4 @@ export default class InventoryService { return lotNumber; } -} \ No newline at end of file +} diff --git a/server/src/services/Sales/JournalPosterService.ts b/server/src/services/Sales/JournalPosterService.ts index 4b93d17cb..403f75524 100644 --- a/server/src/services/Sales/JournalPosterService.ts +++ b/server/src/services/Sales/JournalPosterService.ts @@ -19,7 +19,7 @@ export default class JournalPosterService { tenantId: number, referenceId: number|number[], referenceType: string - ) { + ): Promise { const journal = new JournalPoster(tenantId); const journalCommand = new JournalCommands(journal); diff --git a/server/src/services/Sales/SalesInvoices.ts b/server/src/services/Sales/SalesInvoices.ts index d965b3da6..b9c0d9b06 100644 --- a/server/src/services/Sales/SalesInvoices.ts +++ b/server/src/services/Sales/SalesInvoices.ts @@ -13,10 +13,13 @@ import { IPaginationMeta, IFilterMeta, ISystemUser, + IItem, + IItemEntry, } from 'interfaces'; +import JournalPoster from 'services/Accounting/JournalPoster'; +import JournalCommands from 'services/Accounting/JournalCommands'; import events from 'subscribers/events'; import InventoryService from 'services/Inventory/Inventory'; -import SalesInvoicesCost from 'services/Sales/SalesInvoicesCost'; import TenancyService from 'services/Tenancy/TenancyService'; import DynamicListingService from 'services/DynamicListing/DynamicListService'; import { formatDateFields } from 'utils'; @@ -25,6 +28,7 @@ import ItemsService from 'services/Items/ItemsService'; import ItemsEntriesService from 'services/Items/ItemsEntriesService'; import CustomersService from 'services/Contacts/CustomersService'; import SaleEstimateService from 'services/Sales/SalesEstimate'; +import JournalPosterService from './JournalPosterService'; const ERRORS = { INVOICE_NUMBER_NOT_UNIQUE: 'INVOICE_NUMBER_NOT_UNIQUE', @@ -42,7 +46,7 @@ const ERRORS = { * @service */ @Service() -export default class SaleInvoicesService extends SalesInvoicesCost { +export default class SaleInvoicesService { @Inject() tenancy: TenancyService; @@ -70,6 +74,9 @@ export default class SaleInvoicesService extends SalesInvoicesCost { @Inject() saleEstimatesService: SaleEstimateService; + @Inject() + journalService: JournalPosterService; + /** * Validate whether sale invoice number unqiue on the storage. */ @@ -101,6 +108,28 @@ export default class SaleInvoicesService extends SalesInvoicesCost { } } + /** + * Validate the sale invoice has no payment entries. + * @param {number} tenantId + * @param {number} saleInvoiceId + */ + async validateInvoiceHasNoPaymentEntries( + tenantId: number, + saleInvoiceId: number + ) { + const { PaymentReceiveEntry } = this.tenancy.models(tenantId); + + // Retrieve the sale invoice associated payment receive entries. + const entries = await PaymentReceiveEntry.query().where( + 'invoice_id', + saleInvoiceId + ); + if (entries.length > 0) { + throw new ServiceError(ERRORS.INVOICE_HAS_ASSOCIATED_PAYMENT_ENTRIES); + } + return entries; + } + /** * Validate whether sale invoice exists on the storage. * @param {Request} req @@ -148,7 +177,7 @@ export default class SaleInvoicesService extends SalesInvoicesCost { balance, paymentAmount: 0, entries: saleInvoiceDTO.entries.map((entry) => ({ - reference_type: 'SaleInvoice', + referenceType: 'SaleInvoice', ...omit(entry, ['amount', 'id']), })), }; @@ -208,7 +237,7 @@ export default class SaleInvoicesService extends SalesInvoicesCost { }); this.logger.info('[sale_invoice] successfully inserted.', { tenantId, - saleInvoice, + saleInvoiceId: saleInvoice.id, }); return saleInvoice; @@ -238,19 +267,19 @@ export default class SaleInvoicesService extends SalesInvoicesCost { const saleInvoiceObj = this.transformDTOToModel( tenantId, saleInvoiceDTO, - oldSaleInvoice, + oldSaleInvoice ); // Validate customer existance. await this.customersService.getCustomerByIdOrThrowError( tenantId, - saleInvoiceDTO.customerId, + saleInvoiceDTO.customerId ); // Validate sale invoice number uniquiness. if (saleInvoiceDTO.invoiceNo) { await this.validateInvoiceNumberUnique( tenantId, saleInvoiceDTO.invoiceNo, - saleInvoiceId, + saleInvoiceId ); } // Validate items ids existance. @@ -261,7 +290,7 @@ export default class SaleInvoicesService extends SalesInvoicesCost { // Validate non-sellable entries items. await this.itemsEntriesService.validateNonSellableEntriesItems( tenantId, - saleInvoiceDTO.entries, + saleInvoiceDTO.entries ); // Validate the items entries existance. await this.itemsEntriesService.validateEntriesIdsExistance( @@ -270,7 +299,6 @@ export default class SaleInvoicesService extends SalesInvoicesCost { 'SaleInvoice', saleInvoiceDTO.entries ); - this.logger.info('[sale_invoice] trying to update sale invoice.'); const saleInvoice: ISaleInvoice = await SaleInvoice.query().upsertGraphAndFetch( { @@ -280,10 +308,10 @@ export default class SaleInvoicesService extends SalesInvoicesCost { ); // Triggers `onSaleInvoiceEdited` event. await this.eventDispatcher.dispatch(events.saleInvoice.onEdited, { - saleInvoice, - oldSaleInvoice, tenantId, saleInvoiceId, + saleInvoice, + oldSaleInvoice, authorizedUser, }); return saleInvoice; @@ -303,51 +331,33 @@ export default class SaleInvoicesService extends SalesInvoicesCost { const { saleInvoiceRepository } = this.tenancy.repositories(tenantId); // Retrieve details of the given sale invoice id. - const saleInvoice = await this.getInvoiceOrThrowError( + const oldSaleInvoice = await this.getInvoiceOrThrowError( tenantId, saleInvoiceId ); - // Throws error in case the sale invoice already published. - if (saleInvoice.isDelivered) { + if (oldSaleInvoice.isDelivered) { throw new ServiceError(ERRORS.SALE_INVOICE_ALREADY_DELIVERED); } // Record the delivered at on the storage. await saleInvoiceRepository.update( - { - deliveredAt: moment().toMySqlDateTime(), - }, + { deliveredAt: moment().toMySqlDateTime(), }, { id: saleInvoiceId } ); - } - - /** - * Validate the sale invoice has no payment entries. - * @param {number} tenantId - * @param {number} saleInvoiceId - */ - async validateInvoiceHasNoPaymentEntries( - tenantId: number, - saleInvoiceId: number - ) { - const { PaymentReceiveEntry } = this.tenancy.models(tenantId); - - // Retrieve the sale invoice associated payment receive entries. - const entries = await PaymentReceiveEntry.query().where( - 'invoice_id', - saleInvoiceId - ); - if (entries.length > 0) { - throw new ServiceError(ERRORS.INVOICE_HAS_ASSOCIATED_PAYMENT_ENTRIES); - } - return entries; + // Triggers `onSaleInvoiceDelivered` event. + this.eventDispatcher.dispatch(events.saleInvoice.onDelivered, { + tenantId, + saleInvoiceId, + oldSaleInvoice, + }); } /** * Deletes the given sale invoice with associated entries * and journal transactions. - * @async + * @param {number} tenantId - Tenant id. * @param {Number} saleInvoiceId - The given sale invoice id. + * @param {ISystemUser} authorizedUser - */ public async deleteSaleInvoice( tenantId: number, @@ -376,15 +386,15 @@ export default class SaleInvoicesService extends SalesInvoicesCost { tenantId, saleInvoiceId ); - this.logger.info('[sale_invoice] delete sale invoice with entries.'); - await saleInvoiceRepository.deleteById(saleInvoiceId); await ItemEntry.query() .where('reference_id', saleInvoiceId) .where('reference_type', 'SaleInvoice') .delete(); + await saleInvoiceRepository.deleteById(saleInvoiceId); + // Triggers `onSaleInvoiceDeleted` event. await this.eventDispatcher.dispatch(events.saleInvoice.onDeleted, { tenantId, @@ -408,7 +418,7 @@ export default class SaleInvoicesService extends SalesInvoicesCost { tenantId: number, saleInvoiceId: number, saleInvoiceDate: Date, - override?: boolean, + override?: boolean ): Promise { // Gets the next inventory lot number. const lotNumber = this.inventoryService.getNextLotNumber(tenantId); @@ -451,41 +461,38 @@ export default class SaleInvoicesService extends SalesInvoicesCost { } /** - * Records the journal entries of the given sale invoice just - * in case the invoice has no inventory items entries. - * - * @param {number} tenantId - - * @param {number} saleInvoiceId - * @param {boolean} override - * @return {Promise} + * Writes the sale invoice income journal entries. + * @param {number} tenantId - Tenant id. + * @param {ISaleInvoice} saleInvoice - Sale invoice id. */ - public async recordNonInventoryJournalEntries( + public async writesIncomeJournalEntries( tenantId: number, - saleInvoiceId: number, - authorizedUserId: number, + saleInvoice: ISaleInvoice & { + entries: IItemEntry & { item: IItem }; + }, override: boolean = false ): Promise { - const { saleInvoiceRepository } = this.tenancy.repositories(tenantId); + const { accountRepository } = this.tenancy.repositories(tenantId); - // Loads the inventory items entries of the given sale invoice. - const inventoryEntries = await this.itemsEntriesService.getInventoryEntries( - tenantId, - 'SaleInvoice', - saleInvoiceId - ); - // Can't continue if the sale invoice has inventory items entries. - if (inventoryEntries.length > 0) return; + const journal = new JournalPoster(tenantId); + const journalCommands = new JournalCommands(journal); - const saleInvoice = await saleInvoiceRepository.findOneById( - saleInvoiceId, - 'entries.item' - ); - await this.writeNonInventoryInvoiceEntries( - tenantId, + const receivableAccount = await accountRepository.findOne({ + slug: 'accounts-receivable', + }); + if (override) { + await journalCommands.revertInvoiceIncomeEntries(saleInvoice.id); + } + // Records the sale invoice journal entries. + await journalCommands.saleInvoiceIncomeEntries( saleInvoice, - authorizedUserId, - override + receivableAccount.id ); + await Promise.all([ + journal.deleteEntries(), + journal.saveBalance(), + journal.saveEntries() + ]); } /** @@ -519,6 +526,23 @@ export default class SaleInvoicesService extends SalesInvoicesCost { ); } + /** + * Reverting the sale invoice journal entries. + * @param {number} tenantId + * @param {number} saleInvoiceId + * @return {Promise} + */ + public async revertInvoiceJournalEntries( + tenantId: number, + saleInvoiceId: number | number[] + ): Promise { + return this.journalService.revertJournalTransactions( + tenantId, + saleInvoiceId, + 'SaleInvoice' + ); + } + /** * Retrieve sale invoice with associated entries. * @async diff --git a/server/src/services/Sales/SalesInvoicesCost.ts b/server/src/services/Sales/SalesInvoicesCost.ts index f3f5e26de..291487f68 100644 --- a/server/src/services/Sales/SalesInvoicesCost.ts +++ b/server/src/services/Sales/SalesInvoicesCost.ts @@ -24,7 +24,7 @@ export default class SaleInvoicesCost { async scheduleComputeCostByItemsIds( tenantId: number, inventoryItemsIds: number[], - startingDate: Date, + startingDate: Date ) { const asyncOpers: Promise<[]>[] = []; @@ -32,7 +32,7 @@ export default class SaleInvoicesCost { const oper: Promise<[]> = this.inventoryService.scheduleComputeItemCost( tenantId, inventoryItemId, - startingDate, + startingDate ); asyncOpers.push(oper); }); @@ -49,7 +49,7 @@ export default class SaleInvoicesCost { */ async scheduleComputeCostByInvoiceId( tenantId: number, - saleInvoiceId: number, + saleInvoiceId: number ) { const { SaleInvoice } = this.tenancy.models(tenantId); @@ -62,7 +62,7 @@ export default class SaleInvoicesCost { return this.scheduleComputeCostByEntries( tenantId, saleInvoice.entries, - saleInvoice.invoiceDate, + saleInvoice.invoiceDate ); } @@ -82,24 +82,24 @@ export default class SaleInvoicesCost { const bill = await Bill.query() .findById(billId) .withGraphFetched('entries'); - + return this.scheduleComputeCostByEntries( tenantId, bill.entries, - bill.billDate, + bill.billDate ); } /** * Schedules the compute inventory items by the given invoice. - * @param {number} tenantId - * @param {ISaleInvoice & { entries: IItemEntry[] }} saleInvoice - * @param {boolean} override + * @param {number} tenantId + * @param {ISaleInvoice & { entries: IItemEntry[] }} saleInvoice + * @param {boolean} override */ async scheduleComputeCostByEntries( tenantId: number, entries: IItemEntry[], - startingDate: Date, + startingDate: Date ) { const { Item } = this.tenancy.models(tenantId); @@ -121,105 +121,57 @@ export default class SaleInvoicesCost { /** * Schedule writing journal entries. - * @param {Date} startingDate + * @param {Date} startingDate * @return {Promise} */ scheduleWriteJournalEntries(tenantId: number, startingDate?: Date) { const agenda = Container.get('agenda'); return agenda.schedule('in 3 seconds', 'rewrite-invoices-journal-entries', { - startingDate, tenantId, + startingDate, + tenantId, }); } /** * Writes journal entries from sales invoices. * @param {number} tenantId - The tenant id. - * @param {Date} startingDate - * @param {boolean} override + * @param {Date} startingDate - Starting date. + * @param {boolean} override */ - async writeJournalEntries(tenantId: number, startingDate: Date, override: boolean) { - const { AccountTransaction, SaleInvoice, Account } = this.tenancy.models(tenantId); + async writeInventoryCostJournalEntries( + tenantId: number, + startingDate: Date, + override: boolean + ) { + const { InventoryCostLotTracker } = this.tenancy.models(tenantId); const { accountRepository } = this.tenancy.repositories(tenantId); - const receivableAccount = await accountRepository.findOne({ - slug: 'accounts-receivable', - }); - const salesInvoices = await SaleInvoice.query() - .onBuild((builder: any) => { - builder.modify('filterDateRange', startingDate); - builder.orderBy('invoice_date', 'ASC'); + const inventoryCostLotTrans = await InventoryCostLotTracker.query() + .where('direction', 'OUT') + .modify('groupedEntriesCost') + .modify('filterDateRange', startingDate) + .orderBy('date', 'ASC') + .where('cost', '>', 0) + .withGraphFetched('item'); - builder.withGraphFetched('entries.item'); - builder.withGraphFetched('costTransactions(groupedEntriesCost)'); - }); const accountsDepGraph = await accountRepository.getDependencyGraph(); - const journal = new JournalPoster(tenantId, accountsDepGraph); + const journal = new JournalPoster(tenantId, accountsDepGraph); const journalCommands = new JournalCommands(journal); if (override) { - const oldTransactions = await AccountTransaction.query() - .whereIn('reference_type', ['SaleInvoice']) - .onBuild((builder: any) => { - builder.modify('filterDateRange', startingDate); - }) - .withGraphFetched('account.type'); - - journal.fromTransactions(oldTransactions); - journal.removeEntries(); + await journalCommands.revertInventoryCostJournalEntries(startingDate); } - salesInvoices.forEach((saleInvoice: ISaleInvoice & { - costTransactions: IInventoryLotCost[], - entries: IItemEntry & { item: IItem }, - }) => { - journalCommands.saleInvoice(saleInvoice, receivableAccount.id); - }); + inventoryCostLotTrans.forEach( + (inventoryCostLot: IInventoryLotCost & { item: IItem }) => { + journalCommands.saleInvoiceInventoryCost(inventoryCostLot); + } + ); return Promise.all([ journal.deleteEntries(), journal.saveEntries(), - journal.saveBalance(), + journal.saveBalance() ]); } - - /** - * Writes the sale invoice journal entries. - */ - async writeNonInventoryInvoiceEntries( - tenantId: number, - saleInvoice: ISaleInvoice, - authorizedUserId: number, - override: boolean = false, - ) { - const { accountRepository } = this.tenancy.repositories(tenantId); - const { AccountTransaction } = this.tenancy.models(tenantId); - - // Receivable account. - const receivableAccount = await accountRepository.findOne({ - slug: 'accounts-receivable', - }); - const journal = new JournalPoster(tenantId); - const journalCommands = new JournalCommands(journal); - - if (override) { - const oldTransactions = await AccountTransaction.query() - .where('reference_type', 'SaleInvoice') - .where('reference_id', saleInvoice.id) - .withGraphFetched('account.type'); - - journal.fromTransactions(oldTransactions); - journal.removeEntries(); - } - journalCommands.saleInvoiceNonInventory( - saleInvoice, - receivableAccount.id, - authorizedUserId, - ); - - await Promise.all([ - journal.deleteEntries(), - journal.saveEntries(), - journal.saveBalance(), - ]); - } -} \ No newline at end of file +} diff --git a/server/src/subscribers/events.ts b/server/src/subscribers/events.ts index 5b665a378..0660387b1 100644 --- a/server/src/subscribers/events.ts +++ b/server/src/subscribers/events.ts @@ -80,6 +80,7 @@ export default { onEdited: 'onSaleInvoiceEdited', onDelete: 'onSaleInvoiceDelete', onDeleted: 'onSaleInvoiceDeleted', + onDelivered: 'onSaleInvoiceDelivered', onBulkDelete: 'onSaleInvoiceBulkDeleted', onPublished: 'onSaleInvoicePublished', onInventoryTransactionsCreated: 'onInvoiceInventoryTransactionsCreated', diff --git a/server/src/subscribers/saleInvoices.ts b/server/src/subscribers/saleInvoices.ts index 72ca5ce00..ff34f97d7 100644 --- a/server/src/subscribers/saleInvoices.ts +++ b/server/src/subscribers/saleInvoices.ts @@ -7,6 +7,7 @@ import SettingsService from 'services/Settings/SettingsService'; import SaleEstimateService from 'services/Sales/SalesEstimate'; import SaleInvoicesService from 'services/Sales/SalesInvoices'; import ItemsEntriesService from 'services/Items/ItemsEntriesService'; +import SalesInvoicesCost from 'services/Sales/SalesInvoicesCost'; @EventSubscriber() export default class SaleInvoiceSubscriber { @@ -16,6 +17,7 @@ export default class SaleInvoiceSubscriber { saleEstimatesService: SaleEstimateService; saleInvoicesService: SaleInvoicesService; itemsEntriesService: ItemsEntriesService; + salesInvoicesCost: SalesInvoicesCost; constructor() { this.logger = Container.get('logger'); @@ -24,6 +26,7 @@ export default class SaleInvoiceSubscriber { this.saleEstimatesService = Container.get(SaleEstimateService); this.saleInvoicesService = Container.get(SaleInvoicesService); this.itemsEntriesService = Container.get(ItemsEntriesService); + this.salesInvoicesCost = Container.get(SalesInvoicesCost); } /** @@ -95,15 +98,38 @@ export default class SaleInvoiceSubscriber { } /** - * Records journal entries of the non-inventory invoice. + * Handles handle write income journal entries of sale invoice. */ @On(events.saleInvoice.onCreated) - @On(events.saleInvoice.onEdited) - public async handleWritingNonInventoryEntries({ tenantId, saleInvoice, authorizedUser }) { - await this.saleInvoicesService.recordNonInventoryJournalEntries( + public async handleWriteInvoiceIncomeJournalEntries({ + tenantId, + saleInvoiceId, + saleInvoice, + authorizedUser, + }) { + const { saleInvoiceRepository } = this.tenancy.repositories(tenantId); + + const saleInvoiceWithItems = await saleInvoiceRepository.findOneById( + saleInvoiceId, + 'entries.item' + ); + await this.saleInvoicesService.writesIncomeJournalEntries( tenantId, - saleInvoice.id, - authorizedUser.id, + saleInvoiceWithItems + ); + } + + /** + * Increments the sale invoice items once the invoice created. + */ + @On(events.saleInvoice.onCreated) + public async handleDecrementSaleInvoiceItemsQuantity({ + tenantId, + saleInvoice, + }) { + await this.itemsEntriesService.decrementItemsQuantity( + tenantId, + saleInvoice.entries ); } @@ -146,6 +172,29 @@ export default class SaleInvoiceSubscriber { ); } + /** + * Records journal entries of the non-inventory invoice. + */ + @On(events.saleInvoice.onEdited) + public async handleRewriteJournalEntriesOnceInvoiceEdit({ + tenantId, + saleInvoiceId, + saleInvoice, + authorizedUser, + }) { + const { saleInvoiceRepository } = this.tenancy.repositories(tenantId); + + const saleInvoiceWithItems = await saleInvoiceRepository.findOneById( + saleInvoiceId, + 'entries.item' + ); + await this.saleInvoicesService.writesIncomeJournalEntries( + tenantId, + saleInvoiceWithItems, + true + ); + } + /** * Handles customer balance decrement once sale invoice deleted. */ @@ -166,6 +215,20 @@ export default class SaleInvoiceSubscriber { ); } + /** + * Handle reverting journal entries once sale invoice delete. + */ + @On(events.saleInvoice.onDelete) + public async handleRevertingInvoiceJournalEntriesOnDelete({ + tenantId, + saleInvoiceId, + }) { + await this.saleInvoicesService.revertInvoiceJournalEntries( + tenantId, + saleInvoiceId, + ); + } + /** * Handles deleting the inventory transactions once the invoice deleted. */ @@ -203,7 +266,7 @@ export default class SaleInvoiceSubscriber { saleInvoiceId, } ); - await this.saleInvoicesService.scheduleComputeCostByInvoiceId( + await this.salesInvoicesCost.scheduleComputeCostByInvoiceId( tenantId, saleInvoiceId ); @@ -222,27 +285,13 @@ export default class SaleInvoiceSubscriber { const startingDates = map(oldInventoryTransactions, 'date'); const startingDate = head(startingDates); - await this.saleInvoicesService.scheduleComputeCostByItemsIds( + await this.salesInvoicesCost.scheduleComputeCostByItemsIds( tenantId, inventoryItemsIds, startingDate ); } - /** - * Increments the sale invoice items once the invoice created. - */ - @On(events.saleInvoice.onCreated) - public async handleDecrementSaleInvoiceItemsQuantity({ - tenantId, - saleInvoice, - }) { - await this.itemsEntriesService.decrementItemsQuantity( - tenantId, - saleInvoice.entries - ); - } - /** * Decrements the sale invoice items once the invoice deleted. */