diff --git a/server/src/api/controllers/Inventory/InventoryAdjustments.ts b/server/src/api/controllers/Inventory/InventoryAdjustments.ts index bf6553f23..e08f721f3 100644 --- a/server/src/api/controllers/Inventory/InventoryAdjustments.ts +++ b/server/src/api/controllers/Inventory/InventoryAdjustments.ts @@ -115,8 +115,8 @@ export default class InventoryAdjustmentsController extends BaseController { tenantId, adjustmentId ); - return res.status(200).send({ + id: adjustmentId, message: 'The inventory adjustment has been deleted successfully.', }); } catch (error) { diff --git a/server/src/api/controllers/Items.ts b/server/src/api/controllers/Items.ts index 96cdc4ab4..f96b1e0b6 100644 --- a/server/src/api/controllers/Items.ts +++ b/server/src/api/controllers/Items.ts @@ -25,14 +25,14 @@ export default class ItemsController extends BaseController { router.post( '/', - [...this.validateItemSchema, ...this.validateNewItemSchema], + this.validateItemSchema, this.validationResult, asyncMiddleware(this.newItem.bind(this)), this.handlerServiceErrors ); router.post( '/:id/activate', - [...this.validateSpecificItemSchema], + this.validateSpecificItemSchema, this.validationResult, asyncMiddleware(this.activateItem.bind(this)), this.handlerServiceErrors @@ -83,30 +83,6 @@ export default class ItemsController extends BaseController { return router; } - /** - * New item validation schema. - */ - get validateNewItemSchema(): ValidationChain[] { - return [ - check('opening_quantity').default(0).isInt({ min: 0 }).toInt(), - check('opening_cost') - .if(body('opening_quantity').exists().isInt({ min: 1 })) - .exists() - .isFloat(), - check('opening_cost') - .optional({ nullable: true }) - .isFloat({ min: 0 }) - .toFloat(), - check('opening_date') - .if( - body('opening_quantity').exists().isFloat({ min: 1 }) || - body('opening_cost').exists().isFloat({ min: 1 }) - ) - .exists(), - check('opening_date').optional({ nullable: true }).isISO8601().toDate(), - ]; - } - /** * Validate item schema. */ @@ -503,6 +479,11 @@ export default class ItemsController extends BaseController { errors: [{ type: 'ITEM_HAS_ASSOCIATED_TRANSACTINS', code: 320 }], }); } + if (error.errorType === 'ITEM_HAS_ASSOCIATED_INVENTORY_ADJUSTMENT') { + return res.status(400).send({ + errors: [{ type: 'ITEM_HAS_ASSOCIATED_INVENTORY_ADJUSTMENT', code: 330 }], + }); + } } next(error); } diff --git a/server/src/database/migrations/20190822214306_create_items_table.js b/server/src/database/migrations/20190822214306_create_items_table.js index 7b5afe204..7d0100506 100644 --- a/server/src/database/migrations/20190822214306_create_items_table.js +++ b/server/src/database/migrations/20190822214306_create_items_table.js @@ -18,10 +18,6 @@ exports.up = function (knex) { table.text('purchase_description').nullable(); table.integer('quantity_on_hand'); - table.integer('opening_quantity'); - table.decimal('opening_cost', 13, 3); - table.date('opening_date'); - table.text('note').nullable(); table.boolean('active'); table.integer('category_id').unsigned().index().references('id').inTable('items_categories'); diff --git a/server/src/database/migrations/20200722164251_create_inventory_transactions_table.js b/server/src/database/migrations/20200722164251_create_inventory_transactions_table.js index 371a57ca6..4e7db963a 100644 --- a/server/src/database/migrations/20200722164251_create_inventory_transactions_table.js +++ b/server/src/database/migrations/20200722164251_create_inventory_transactions_table.js @@ -10,7 +10,8 @@ exports.up = function(knex) { table.integer('quantity').unsigned(); table.decimal('rate', 13, 3).unsigned(); - table.string('lot_number').index(); + table.integer('lot_number').index(); + table.integer('cost_account_id').unsigned().index().references('id').inTable('accounts'); table.string('transaction_type').index(); table.integer('transaction_id').unsigned().index(); diff --git a/server/src/interfaces/InventoryAdjustment.ts b/server/src/interfaces/InventoryAdjustment.ts index 7ae92f701..e9a4402ad 100644 --- a/server/src/interfaces/InventoryAdjustment.ts +++ b/server/src/interfaces/InventoryAdjustment.ts @@ -1,5 +1,5 @@ -type IAdjustmentTypes = 'increment' | 'decrement' | 'value_adjustment'; +type IAdjustmentTypes = 'increment' | 'decrement'; export interface IQuickInventoryAdjustmentDTO { date: Date | string; @@ -20,6 +20,7 @@ export interface IInventoryAdjustment { reason: string; description: string; referenceNo: string; + inventoryDirection?: 'IN' | 'OUT', entries: IInventoryAdjustmentEntry[]; userId: number; }; diff --git a/server/src/interfaces/InventoryTransaction.ts b/server/src/interfaces/InventoryTransaction.ts index bafeae34e..efa3323c5 100644 --- a/server/src/interfaces/InventoryTransaction.ts +++ b/server/src/interfaces/InventoryTransaction.ts @@ -3,14 +3,14 @@ export type TInventoryTransactionDirection = 'IN' | 'OUT'; export interface IInventoryTransaction { id?: number, - date: Date, + date: Date|string, direction: TInventoryTransactionDirection, itemId: number, quantity: number, rate: number, transactionType: string, transactionId: number, - lotNumber: string, + lotNumber: number, entryId: number, createdAt?: Date, updatedAt?: Date, @@ -25,7 +25,7 @@ export interface IInventoryLotCost { rate: number, remaining: number, cost: number, - lotNumber: string|number, + lotNumber: number, transactionType: string, transactionId: number, entryId: number diff --git a/server/src/interfaces/Item.ts b/server/src/interfaces/Item.ts index 7062c6cf1..031ad1201 100644 --- a/server/src/interfaces/Item.ts +++ b/server/src/interfaces/Item.ts @@ -22,10 +22,6 @@ export interface IItem{ quantityOnHand: number, - openingQuantity: number, - openingCost: number, - openingDate: Date, - note: string, active: boolean, @@ -58,10 +54,6 @@ export interface IItemDTO { quantityOnHand: number, - openingQuantity?: number, - openingCost?: number, - openingDate?: Date, - note: string, active: boolean, diff --git a/server/src/loaders/events.ts b/server/src/loaders/events.ts index 13ec089af..b394d510d 100644 --- a/server/src/loaders/events.ts +++ b/server/src/loaders/events.ts @@ -23,10 +23,12 @@ import 'subscribers/SaleReceipt/SyncItemsQuantity'; import 'subscribers/SaleReceipt/WriteInventoryTransactions'; import 'subscribers/SaleReceipt/WriteJournalEntries'; +import 'subscribers/Inventory/Inventory'; +import 'subscribers/Inventory/InventoryAdjustment'; + import 'subscribers/customers'; import 'subscribers/vendors'; import 'subscribers/paymentMades'; import 'subscribers/paymentReceives'; import 'subscribers/saleEstimates'; -import 'subscribers/inventory'; import 'subscribers/items'; \ No newline at end of file diff --git a/server/src/loaders/tenantModels.ts b/server/src/loaders/tenantModels.ts index 872b2cad1..5aa72131f 100644 --- a/server/src/loaders/tenantModels.ts +++ b/server/src/loaders/tenantModels.ts @@ -35,6 +35,8 @@ import ManualJournal from 'models/ManualJournal'; import ManualJournalEntry from 'models/ManualJournalEntry'; import Media from 'models/Media'; import MediaLink from 'models/MediaLink'; +import InventoryAdjustment from 'models/InventoryAdjustment'; +import InventoryAdjustmentEntry from 'models/InventoryAdjustmentEntry'; export default (knex) => { const models = { @@ -73,6 +75,8 @@ export default (knex) => { Vendor, Customer, Contact, + InventoryAdjustment, + InventoryAdjustmentEntry, }; return mapValues(models, (model) => model.bindKnex(knex)); } \ No newline at end of file diff --git a/server/src/models/AccountTransaction.js b/server/src/models/AccountTransaction.js index 309f62bc9..ea470d031 100644 --- a/server/src/models/AccountTransaction.js +++ b/server/src/models/AccountTransaction.js @@ -17,6 +17,34 @@ export default class AccountTransaction extends TenantModel { return ['createdAt']; } + static get virtualAttributes() { + return ['referenceTypeFormatted']; + } + + /** + * Retrieve formatted reference type. + * @return {string} + */ + get referenceTypeFormatted() { + return AccountTransaction.getReferenceTypeFormatted(this.referenceType); + } + + /** + * Reference type formatted. + */ + static getReferenceTypeFormatted(referenceType) { + const mapped = { + 'SaleInvoice': 'Sale invoice', + 'SaleReceipt': 'Sale receipt', + 'PaymentReceive': 'Payment receive', + 'BillPayment': 'Payment made', + 'VendorOpeningBalance': 'Vendor opening balance', + 'CustomerOpeningBalance': 'Customer opening balance', + 'InventoryAdjustment': 'Inventory adjustment' + }; + return mapped[referenceType] || ''; + } + /** * Model modifiers. */ diff --git a/server/src/models/InventoryAdjustment.js b/server/src/models/InventoryAdjustment.js index bbb31e86e..df16a1a5e 100644 --- a/server/src/models/InventoryAdjustment.js +++ b/server/src/models/InventoryAdjustment.js @@ -16,6 +16,28 @@ export default class InventoryAdjustment extends TenantModel { return ['created_at']; } + /** + * Virtual attributes. + */ + static get virtualAttributes() { + return ['inventoryDirection']; + } + + /** + * Retrieve formatted reference type. + */ + get inventoryDirection() { + return InventoryAdjustment.getInventoryDirection(this.type); + } + + static getInventoryDirection(type) { + const directions = { + 'increment': 'IN', + 'decrement': 'OUT', + }; + return directions[type] || ''; + } + /** * Relationship mapping. */ diff --git a/server/src/services/Accounting/JournalCommands.ts b/server/src/services/Accounting/JournalCommands.ts index 8b5d6e1b8..d9db78487 100644 --- a/server/src/services/Accounting/JournalCommands.ts +++ b/server/src/services/Accounting/JournalCommands.ts @@ -231,7 +231,6 @@ export default class JournalCommands { referenceId: expense.id, date: expense.paymentDate, userId, - draft: !expense.publishedAt, }; const paymentJournalEntry = new JournalEntry({ credit: expense.totalAmount, @@ -330,7 +329,6 @@ export default class JournalCommands { note: entry.note, date: manualJournalObj.date, userId: manualJournalObj.userId, - draft: !manualJournalObj.status, index: entry.index, }); if (entry.debit) { @@ -354,7 +352,7 @@ export default class JournalCommands { inventoryCostLot: IInventoryLotCost & { item: IItem } ) { const commonEntry = { - referenceType: 'SaleInvoice', + referenceType: inventoryCostLot.transactionType, referenceId: inventoryCostLot.transactionId, date: inventoryCostLot.date, }; diff --git a/server/src/services/Accounts/AccountsService.ts b/server/src/services/Accounts/AccountsService.ts index 9a0f8843d..2b3be14a3 100644 --- a/server/src/services/Accounts/AccountsService.ts +++ b/server/src/services/Accounts/AccountsService.ts @@ -164,7 +164,7 @@ export default class AccountsService { * @param {number} accountId * @return {IAccount} */ - private async getAccountOrThrowError(tenantId: number, accountId: number) { + public async getAccountOrThrowError(tenantId: number, accountId: number) { const { accountRepository } = this.tenancy.repositories(tenantId); this.logger.info('[accounts] validating the account existance.', { diff --git a/server/src/services/ExchangeRates/ExchangeRatesService.ts b/server/src/services/ExchangeRates/ExchangeRatesService.ts index 2896157b1..6937c585b 100644 --- a/server/src/services/ExchangeRates/ExchangeRatesService.ts +++ b/server/src/services/ExchangeRates/ExchangeRatesService.ts @@ -30,25 +30,34 @@ export default class ExchangeRatesService implements IExchangeRatesService { eventDispatcher: EventDispatcherInterface; @Inject() - tenancy: TenancyService; + tenancy: TenancyService; /** * Creates a new exchange rate. - * @param {number} tenantId - * @param {IExchangeRateDTO} exchangeRateDTO + * @param {number} tenantId + * @param {IExchangeRateDTO} exchangeRateDTO * @returns {Promise} */ - public async newExchangeRate(tenantId: number, exchangeRateDTO: IExchangeRateDTO): Promise { + public async newExchangeRate( + tenantId: number, + exchangeRateDTO: IExchangeRateDTO + ): Promise { const { ExchangeRate } = this.tenancy.models(tenantId); - this.logger.info('[exchange_rates] trying to insert new exchange rate.', { tenantId, exchangeRateDTO }); + this.logger.info('[exchange_rates] trying to insert new exchange rate.', { + tenantId, + exchangeRateDTO, + }); await this.validateExchangeRatePeriodExistance(tenantId, exchangeRateDTO); const exchangeRate = await ExchangeRate.query().insertAndFetch({ ...exchangeRateDTO, date: moment(exchangeRateDTO.date).format('YYYY-MM-DD'), }); - this.logger.info('[exchange_rates] inserted successfully.', { tenantId, exchangeRateDTO }); + this.logger.info('[exchange_rates] inserted successfully.', { + tenantId, + exchangeRateDTO, + }); return exchangeRate; } @@ -58,14 +67,28 @@ export default class ExchangeRatesService implements IExchangeRatesService { * @param {number} exchangeRateId - Exchange rate id. * @param {IExchangeRateEditDTO} editExRateDTO - Edit exchange rate DTO. */ - public async editExchangeRate(tenantId: number, exchangeRateId: number, editExRateDTO: IExchangeRateEditDTO): Promise { + public async editExchangeRate( + tenantId: number, + exchangeRateId: number, + editExRateDTO: IExchangeRateEditDTO + ): Promise { const { ExchangeRate } = this.tenancy.models(tenantId); - this.logger.info('[exchange_rates] trying to edit exchange rate.', { tenantId, exchangeRateId, editExRateDTO }); + this.logger.info('[exchange_rates] trying to edit exchange rate.', { + tenantId, + exchangeRateId, + editExRateDTO, + }); await this.validateExchangeRateExistance(tenantId, exchangeRateId); - await ExchangeRate.query().where('id', exchangeRateId).update({ ...editExRateDTO }); - this.logger.info('[exchange_rates] exchange rate edited successfully.', { tenantId, exchangeRateId, editExRateDTO }); + await ExchangeRate.query() + .where('id', exchangeRateId) + .update({ ...editExRateDTO }); + this.logger.info('[exchange_rates] exchange rate edited successfully.', { + tenantId, + exchangeRateId, + editExRateDTO, + }); } /** @@ -73,7 +96,10 @@ export default class ExchangeRatesService implements IExchangeRatesService { * @param {number} tenantId - Tenant id. * @param {number} exchangeRateId - Exchange rate id. */ - public async deleteExchangeRate(tenantId: number, exchangeRateId: number): Promise { + public async deleteExchangeRate( + tenantId: number, + exchangeRateId: number + ): Promise { const { ExchangeRate } = this.tenancy.models(tenantId); await this.validateExchangeRateExistance(tenantId, exchangeRateId); @@ -83,29 +109,43 @@ export default class ExchangeRatesService implements IExchangeRatesService { /** * Listing exchange rates details. * @param {number} tenantId - Tenant id. - * @param {IExchangeRateFilter} exchangeRateFilter - Exchange rates list filter. + * @param {IExchangeRateFilter} exchangeRateFilter - Exchange rates list filter. */ - public async listExchangeRates(tenantId: number, exchangeRateFilter: IExchangeRateFilter): Promise { + public async listExchangeRates( + tenantId: number, + exchangeRateFilter: IExchangeRateFilter + ): Promise { const { ExchangeRate } = this.tenancy.models(tenantId); - const exchangeRates = await ExchangeRate.query() - .pagination(exchangeRateFilter.page - 1, exchangeRateFilter.pageSize); - + const exchangeRates = await ExchangeRate.query().pagination( + exchangeRateFilter.page - 1, + exchangeRateFilter.pageSize + ); + return exchangeRates; } /** * Deletes exchange rates in bulk. - * @param {number} tenantId - * @param {number[]} exchangeRatesIds + * @param {number} tenantId + * @param {number[]} exchangeRatesIds */ - public async deleteBulkExchangeRates(tenantId: number, exchangeRatesIds: number[]): Promise { + public async deleteBulkExchangeRates( + tenantId: number, + exchangeRatesIds: number[] + ): Promise { const { ExchangeRate } = this.tenancy.models(tenantId); - this.logger.info('[exchange_rates] trying delete in bulk.', { tenantId, exchangeRatesIds }); + this.logger.info('[exchange_rates] trying delete in bulk.', { + tenantId, + exchangeRatesIds, + }); await this.validateExchangeRatesIdsExistance(tenantId, exchangeRatesIds); await ExchangeRate.query().whereIn('id', exchangeRatesIds).delete(); - this.logger.info('[exchange_rates] deleted successfully.', { tenantId, exchangeRatesIds }); + this.logger.info('[exchange_rates] deleted successfully.', { + tenantId, + exchangeRatesIds, + }); } /** @@ -114,16 +154,23 @@ export default class ExchangeRatesService implements IExchangeRatesService { * @param {IExchangeRateDTO} exchangeRateDTO - Exchange rate DTO. * @return {Promise} */ - private async validateExchangeRatePeriodExistance(tenantId: number, exchangeRateDTO: IExchangeRateDTO): Promise { + private async validateExchangeRatePeriodExistance( + tenantId: number, + exchangeRateDTO: IExchangeRateDTO + ): Promise { const { ExchangeRate } = this.tenancy.models(tenantId); - this.logger.info('[exchange_rates] trying to validate period existance.', { tenantId }); + this.logger.info('[exchange_rates] trying to validate period existance.', { + tenantId, + }); const foundExchangeRate = await ExchangeRate.query() .where('currency_code', exchangeRateDTO.currencyCode) .where('date', exchangeRateDTO.date); if (foundExchangeRate.length > 0) { - this.logger.info('[exchange_rates] given exchange rate period exists.', { tenantId }); + this.logger.info('[exchange_rates] given exchange rate period exists.', { + tenantId, + }); throw new ServiceError(ERRORS.EXCHANGE_RATE_PERIOD_EXISTS); } } @@ -134,14 +181,25 @@ export default class ExchangeRatesService implements IExchangeRatesService { * @param {number} exchangeRateId - Exchange rate id. * @returns {Promise} */ - private async validateExchangeRateExistance(tenantId: number, exchangeRateId: number) { + private async validateExchangeRateExistance( + tenantId: number, + exchangeRateId: number + ) { const { ExchangeRate } = this.tenancy.models(tenantId); - this.logger.info('[exchange_rates] trying to validate exchange rate id existance.', { tenantId, exchangeRateId }); - const foundExchangeRate = await ExchangeRate.query().findById(exchangeRateId); + this.logger.info( + '[exchange_rates] trying to validate exchange rate id existance.', + { tenantId, exchangeRateId } + ); + const foundExchangeRate = await ExchangeRate.query().findById( + exchangeRateId + ); if (!foundExchangeRate) { - this.logger.info('[exchange_rates] exchange rate not found.', { tenantId, exchangeRateId }); + this.logger.info('[exchange_rates] exchange rate not found.', { + tenantId, + exchangeRateId, + }); throw new ServiceError(ERRORS.EXCHANGE_RATE_NOT_FOUND); } } @@ -152,15 +210,26 @@ export default class ExchangeRatesService implements IExchangeRatesService { * @param {number[]} exchangeRatesIds - Exchange rates ids. * @returns {Promise} */ - private async validateExchangeRatesIdsExistance(tenantId: number, exchangeRatesIds: number[]): Promise { + private async validateExchangeRatesIdsExistance( + tenantId: number, + exchangeRatesIds: number[] + ): Promise { const { ExchangeRate } = this.tenancy.models(tenantId); - const storedExchangeRates = await ExchangeRate.query().whereIn('id', exchangeRatesIds); - const storedExchangeRatesIds = storedExchangeRates.map((category) => category.id); - const notFoundExRates = difference(exchangeRatesIds, storedExchangeRatesIds); + const storedExchangeRates = await ExchangeRate.query().whereIn( + 'id', + exchangeRatesIds + ); + const storedExchangeRatesIds = storedExchangeRates.map( + (category) => category.id + ); + const notFoundExRates = difference( + exchangeRatesIds, + storedExchangeRatesIds + ); if (notFoundExRates.length > 0) { throw new ServiceError(ERRORS.NOT_FOUND_EXCHANGE_RATES); } } -} \ No newline at end of file +} diff --git a/server/src/services/Inventory/Inventory.ts b/server/src/services/Inventory/Inventory.ts index c2090727a..7d2312f79 100644 --- a/server/src/services/Inventory/Inventory.ts +++ b/server/src/services/Inventory/Inventory.ts @@ -39,7 +39,7 @@ export default class InventoryService { direction: TInventoryTransactionDirection, date: Date | string, lotNumber: number - ) { + ): IInventoryTransaction[] { return itemEntries.map((entry: IItemEntry) => ({ ...pick(entry, ['itemId', 'quantity', 'rate']), lotNumber, @@ -150,19 +150,36 @@ export default class InventoryService { */ async recordInventoryTransactions( tenantId: number, - inventoryEntries: IInventoryTransaction[], - deleteOld: boolean + transactions: IInventoryTransaction[], + override: boolean = false ): Promise { - inventoryEntries.forEach(async (entry: IInventoryTransaction) => { - await this.recordInventoryTransaction(tenantId, entry, deleteOld); + const bulkInsertOpers = []; + + transactions.forEach((transaction: IInventoryTransaction) => { + const oper = this.recordInventoryTransaction( + tenantId, + transaction, + override + ); + bulkInsertOpers.push(oper); }); + const inventoryTransactions = await Promise.all(bulkInsertOpers); + + // Triggers `onInventoryTransactionsCreated` event. + this.eventDispatcher.dispatch( + events.inventory.onInventoryTransactionsCreated, + { + tenantId, + inventoryTransactions, + } + ); } /** - * Writes the inventory transactiosn on the storage from the given + * Writes the inventory transactiosn on the storage from the given * inventory transactions entries. - * - * @param {number} tenantId - + * + * @param {number} tenantId - * @param {IInventoryTransaction} inventoryEntry - * @param {boolean} deleteOld - */ @@ -203,7 +220,7 @@ export default class InventoryService { transactionDirection: TInventoryTransactionDirection, override: boolean = false ): Promise { - // Gets the next inventory lot number. + // Retrieve the next inventory lot number. const lotNumber = this.getNextLotNumber(tenantId); // Loads the inventory items entries of the given sale invoice. @@ -231,20 +248,6 @@ export default class InventoryService { ); // Increment and save the next lot number settings. await this.incrementNextLotNumber(tenantId); - - // Triggers `onInventoryTransactionsCreated` event. - this.eventDispatcher.dispatch( - events.inventory.onInventoryTransactionsCreated, - { - tenantId, - inventoryEntries, - transactionId, - transactionType, - transactionDate, - transactionDirection, - override, - } - ); } /** @@ -253,7 +256,7 @@ export default class InventoryService { * @param {string} transactionType * @param {number} transactionId * @return {Promise<{ - * oldInventoryTransactions: IInventoryTransaction[] + * oldInventoryTransactions: IInventoryTransaction[] * }>} */ async deleteInventoryTransactions( @@ -261,7 +264,9 @@ export default class InventoryService { transactionId: number, transactionType: string ): Promise<{ oldInventoryTransactions: IInventoryTransaction[] }> { - const { inventoryTransactionRepository } = this.tenancy.repositories(tenantId); + const { inventoryTransactionRepository } = this.tenancy.repositories( + tenantId + ); // Retrieve the inventory transactions of the given sale invoice. const oldInventoryTransactions = await inventoryTransactionRepository.find({ diff --git a/server/src/services/Inventory/InventoryAdjustmentService.ts b/server/src/services/Inventory/InventoryAdjustmentService.ts index 7f243f361..b448514bd 100644 --- a/server/src/services/Inventory/InventoryAdjustmentService.ts +++ b/server/src/services/Inventory/InventoryAdjustmentService.ts @@ -11,11 +11,13 @@ import { IPaginationMeta, IInventoryAdjustmentsFilter, ISystemUser, + IInventoryTransaction, } from 'interfaces'; import events from 'subscribers/events'; import AccountsService from 'services/Accounts/AccountsService'; import ItemsService from 'services/Items/ItemsService'; import HasTenancyService from 'services/Tenancy/TenancyService'; +import InventoryService from './Inventory'; const ERRORS = { INVENTORY_ADJUSTMENT_NOT_FOUND: 'INVENTORY_ADJUSTMENT_NOT_FOUND', @@ -39,6 +41,9 @@ export default class InventoryAdjustmentService { @EventDispatcher() eventDispatcher: EventDispatcherInterface; + @Inject() + inventoryService: InventoryService; + /** * Transformes the quick inventory adjustment DTO to model object. * @param {IQuickInventoryAdjustmentDTO} adjustmentDTO - @@ -55,14 +60,15 @@ export default class InventoryAdjustmentService { { index: 1, itemId: adjustmentDTO.itemId, - ...(['increment', 'decrement'].indexOf(adjustmentDTO.type) !== -1 - ? { - quantity: adjustmentDTO.quantity, - } - : {}), ...('increment' === adjustmentDTO.type ? { - cost: adjustmentDTO.cost, + quantity: adjustmentDTO.quantity, + cost: adjustmentDTO.cost, + } + : {}), + ...('decrement' === adjustmentDTO.type + ? { + quantity: adjustmentDTO.quantity, } : {}), }, @@ -138,6 +144,7 @@ export default class InventoryAdjustmentService { quickAdjustmentDTO, authorizedUser ); + // Saves the inventory adjustment with assocaited entries to the storage. const inventoryAdjustment = await InventoryAdjustment.query().upsertGraph({ ...invAdjustmentObject, }); @@ -146,6 +153,8 @@ export default class InventoryAdjustmentService { events.inventoryAdjustment.onQuickCreated, { tenantId, + inventoryAdjustment, + inventoryAdjustmentId: inventoryAdjustment.id, } ); this.logger.info( @@ -192,6 +201,7 @@ export default class InventoryAdjustmentService { // Triggers `onInventoryAdjustmentDeleted` event. await this.eventDispatcher.dispatch(events.inventoryAdjustment.onDeleted, { tenantId, + inventoryAdjustmentId, }); this.logger.info( '[inventory_adjustment] the adjustment deleted successfully.', @@ -226,4 +236,61 @@ export default class InventoryAdjustmentService { pagination, }; } + + /** + * Writes the inventory transactions from the inventory adjustment transaction. + * @param {number} tenantId + * @param {IInventoryAdjustment} inventoryAdjustment + * @return {Promise} + */ + async writeInventoryTransactions( + tenantId: number, + inventoryAdjustment: IInventoryAdjustment, + override: boolean = false, + ): Promise { + // Gets the next inventory lot number. + const lotNumber = this.inventoryService.getNextLotNumber(tenantId); + + const commonTransaction = { + direction: inventoryAdjustment.inventoryDirection, + date: inventoryAdjustment.date, + transactionType: 'InventoryAdjustment', + transactionId: inventoryAdjustment.id, + }; + const inventoryTransactions = []; + + inventoryAdjustment.entries.forEach((entry) => { + inventoryTransactions.push({ + ...commonTransaction, + itemId: entry.itemId, + quantity: entry.quantity, + rate: entry.cost, + lotNumber, + }); + }); + // Saves the given inventory transactions to the storage. + this.inventoryService.recordInventoryTransactions( + tenantId, + inventoryTransactions, + override + ); + // Increment and save the next lot number settings. + await this.inventoryService.incrementNextLotNumber(tenantId); + } + + /** + * Reverts the inventory transactions from the inventory adjustment transaction. + * @param {number} tenantId + * @param {number} inventoryAdjustmentId + */ + async revertInventoryTransactions( + tenantId: number, + inventoryAdjustmentId: number + ): Promise<{ oldInventoryTransactions: IInventoryTransaction[] }> { + return this.inventoryService.deleteInventoryTransactions( + tenantId, + inventoryAdjustmentId, + 'InventoryAdjustment' + ); + } } diff --git a/server/src/services/Items/ItemsService.ts b/server/src/services/Items/ItemsService.ts index 57c60d72d..fe25f5c1f 100644 --- a/server/src/services/Items/ItemsService.ts +++ b/server/src/services/Items/ItemsService.ts @@ -10,6 +10,7 @@ import DynamicListingService from 'services/DynamicListing/DynamicListService'; import TenancyService from 'services/Tenancy/TenancyService'; import { ServiceError } from 'exceptions'; import InventoryService from 'services/Inventory/Inventory'; +import InventoryAdjustmentEntry from 'models/InventoryAdjustmentEntry'; const ERRORS = { NOT_FOUND: 'NOT_FOUND', @@ -27,6 +28,8 @@ const ERRORS = { ITEMS_HAVE_ASSOCIATED_TRANSACTIONS: 'ITEMS_HAVE_ASSOCIATED_TRANSACTIONS', ITEM_HAS_ASSOCIATED_TRANSACTINS: 'ITEM_HAS_ASSOCIATED_TRANSACTINS', + + ITEM_HAS_ASSOCIATED_INVENTORY_ADJUSTMENT: 'ITEM_HAS_ASSOCIATED_INVENTORY_ADJUSTMENT', }; @Service() @@ -52,7 +55,7 @@ export default class ItemsService implements IItemsService { * @param {number} itemId * @return {Promise} */ - private async getItemOrThrowError( + public async getItemOrThrowError( tenantId: number, itemId: number ): Promise { @@ -235,16 +238,10 @@ export default class ItemsService implements IItemsService { * @return {IItem} */ private transformNewItemDTOToModel(itemDTO: IItemDTO) { - const inventoryAttrs = ['openingQuantity', 'openingCost', 'openingDate']; - return { - ...omit(itemDTO, inventoryAttrs), - ...(itemDTO.type === 'inventory' ? pick(itemDTO, inventoryAttrs) : {}), + ...itemDTO, active: defaultTo(itemDTO.active, 1), - quantityOnHand: - itemDTO.type === 'inventory' - ? defaultTo(itemDTO.openingQuantity, 0) - : null, + quantityOnHand: itemDTO.type === 'inventory' ? 0 : null, }; } @@ -282,7 +279,7 @@ export default class ItemsService implements IItemsService { ); } const item = await Item.query().insertAndFetch({ - ...this.transformNewItemDTOToModel(itemDTO) + ...this.transformNewItemDTOToModel(itemDTO), }); this.logger.info('[items] item inserted successfully.', { tenantId, @@ -297,46 +294,6 @@ export default class ItemsService implements IItemsService { return item; } - /** - * Records the opening items inventory transaction. - * @param {number} tenantId - - * @param itemId - - * @param openingQuantity - - * @param openingCost - - * @param openingDate - - */ - public async recordOpeningItemsInventoryTransaction( - tenantId: number, - itemId: number, - openingQuantity: number, - openingCost: number, - openingDate: Date - ): Promise { - // Gets the next inventory lot number. - const lotNumber = this.inventoryService.getNextLotNumber(tenantId); - - // Records the inventory transaction. - const inventoryTransaction = await this.inventoryService.recordInventoryTransaction( - tenantId, - { - date: openingDate, - quantity: openingQuantity, - rate: openingCost, - direction: 'IN', - transactionType: 'OpeningItem', - itemId, - lotNumber, - } - ); - // Records the inventory cost lot transaction. - await this.inventoryService.recordInventoryCostLotTransaction(tenantId, { - ...omit(inventoryTransaction, ['updatedAt', 'createdAt']), - cost: openingQuantity * openingCost, - remaining: 0, - }); - await this.inventoryService.incrementNextLotNumber(tenantId); - } - /** * Edits the item metadata. * @param {number} tenantId @@ -398,16 +355,19 @@ export default class ItemsService implements IItemsService { */ public async deleteItem(tenantId: number, itemId: number) { const { Item } = this.tenancy.models(tenantId); - this.logger.info('[items] trying to delete item.', { tenantId, itemId }); // Retreive the given item or throw not found service error. await this.getItemOrThrowError(tenantId, itemId); + // Validate the item has no associated inventory transactions. + await this.validateHasNoInventoryAdjustments(tenantId, itemId); + // Validate the item has no associated invoices or bills. await this.validateHasNoInvoicesOrBills(tenantId, itemId); - + await Item.query().findById(itemId).delete(); + this.logger.info('[items] deleted successfully.', { tenantId, itemId }); } @@ -518,6 +478,9 @@ export default class ItemsService implements IItemsService { // Validates the given items exist on the storage. await this.validateItemsIdsExists(tenantId, itemsIds); + // Validate the item has no associated inventory transactions. + await this.validateHasNoInventoryAdjustments(tenantId, itemsIds); + // Validate the items have no associated invoices or bills. await this.validateHasNoInvoicesOrBills(tenantId, itemsIds); @@ -541,7 +504,6 @@ export default class ItemsService implements IItemsService { Item, itemsFilter ); - const { results, pagination } = await Item.query() .onBuild((builder) => { builder.withGraphFetched('inventoryAccount'); @@ -584,4 +546,24 @@ export default class ItemsService implements IItemsService { ); } } + + /** + * Validates the given item has no associated inventory adjustment transactions. + * @param {number} tenantId - + * @param {number} itemId - + */ + private async validateHasNoInventoryAdjustments( + tenantId: number, + itemId: number[] | number, + ): Promise { + const { InventoryAdjustmentEntry } = this.tenancy.models(tenantId); + const itemsIds = Array.isArray(itemId) ? itemId : [itemId]; + + const inventoryAdjEntries = await InventoryAdjustmentEntry.query() + .whereIn('item_id', itemsIds); + + if (inventoryAdjEntries.length > 0) { + throw new ServiceError(ERRORS.ITEM_HAS_ASSOCIATED_INVENTORY_ADJUSTMENT); + } + } } diff --git a/server/src/services/Sales/SalesInvoicesCost.ts b/server/src/services/Sales/SalesInvoicesCost.ts index 0ff67295e..351bfe9ac 100644 --- a/server/src/services/Sales/SalesInvoicesCost.ts +++ b/server/src/services/Sales/SalesInvoicesCost.ts @@ -1,8 +1,10 @@ import { Container, Service, Inject } from 'typedi'; +import { chain } from 'lodash'; +import moment from 'moment'; import JournalPoster from 'services/Accounting/JournalPoster'; import InventoryService from 'services/Inventory/Inventory'; import TenancyService from 'services/Tenancy/TenancyService'; -import { IInventoryLotCost, IItem } from 'interfaces'; +import { IInventoryLotCost, IInventoryTransaction, IItem } from 'interfaces'; import JournalCommands from 'services/Accounting/JournalCommands'; @Service() @@ -24,7 +26,7 @@ export default class SaleInvoicesCost { tenantId: number, inventoryItemsIds: number[], startingDate: Date - ) { + ): Promise { const asyncOpers: Promise<[]>[] = []; inventoryItemsIds.forEach((inventoryItemId: number) => { @@ -35,7 +37,61 @@ export default class SaleInvoicesCost { ); asyncOpers.push(oper); }); - return Promise.all([...asyncOpers]); + await Promise.all([...asyncOpers]); + } + + /** + * Retrieve the max dated inventory transactions in the transactions that + * have the same item id. + * @param {IInventoryTransaction[]} inventoryTransactions + * @return {IInventoryTransaction[]} + */ + getMaxDateInventoryTransactions( + inventoryTransactions: IInventoryTransaction[] + ): IInventoryTransaction[] { + return chain(inventoryTransactions) + .reduce((acc: any, transaction) => { + const compatatorDate = acc[transaction.itemId]; + + if ( + !compatatorDate || + moment(compatatorDate.date).isBefore(transaction.date) + ) { + return { + ...acc, + [transaction.itemId]: { + ...transaction, + }, + }; + } + return acc; + }, {}) + .values() + .value(); + } + + /** + * Computes items costs by the given inventory transaction. + * @param {number} tenantId + * @param {IInventoryTransaction[]} inventoryTransactions + */ + async computeItemsCostByInventoryTransactions( + tenantId: number, + inventoryTransactions: IInventoryTransaction[] + ) { + const asyncOpers: Promise<[]>[] = []; + const reducedTransactions = this.getMaxDateInventoryTransactions( + inventoryTransactions + ); + reducedTransactions.forEach((transaction) => { + const oper: Promise<[]> = this.inventoryService.scheduleComputeItemCost( + tenantId, + transaction.itemId, + transaction.date + ); + asyncOpers.push(oper); + }); + await Promise.all([...asyncOpers]); } /** @@ -90,7 +146,7 @@ export default class SaleInvoicesCost { return Promise.all([ journal.deleteEntries(), journal.saveEntries(), - journal.saveBalance() + journal.saveBalance(), ]); } } diff --git a/server/src/subscribers/inventory.ts b/server/src/subscribers/Inventory/Inventory.ts similarity index 82% rename from server/src/subscribers/inventory.ts rename to server/src/subscribers/Inventory/Inventory.ts index f1f9ee94c..848519c97 100644 --- a/server/src/subscribers/inventory.ts +++ b/server/src/subscribers/Inventory/Inventory.ts @@ -43,19 +43,13 @@ export class InventorySubscriber { @On(events.inventory.onInventoryTransactionsCreated) async handleScheduleItemsCostOnInventoryTransactionsCreated({ tenantId, - inventoryEntries, - transactionId, - transactionType, - transactionDate, - transactionDirection, - override + inventoryTransactions }) { - const inventoryItemsIds = map(inventoryEntries, 'itemId'); + const inventoryItemsIds = map(inventoryTransactions, 'itemId'); - await this.saleInvoicesCost.scheduleComputeCostByItemsIds( + await this.saleInvoicesCost.computeItemsCostByInventoryTransactions( tenantId, - inventoryItemsIds, - transactionDate, + inventoryTransactions ); } @@ -69,6 +63,12 @@ export class InventorySubscriber { transactionId, oldInventoryTransactions }) { + // Ignore compute item cost with theses transaction types. + const ignoreWithTransactionTypes = ['OpeningItem']; + + if (ignoreWithTransactionTypes.indexOf(transactionType) !== -1) { + return; + } const inventoryItemsIds = map(oldInventoryTransactions, 'itemId'); const startingDates = map(oldInventoryTransactions, 'date'); const startingDate = head(startingDates); diff --git a/server/src/subscribers/Inventory/InventoryAdjustment.ts b/server/src/subscribers/Inventory/InventoryAdjustment.ts new file mode 100644 index 000000000..d454f8226 --- /dev/null +++ b/server/src/subscribers/Inventory/InventoryAdjustment.ts @@ -0,0 +1,49 @@ +import { Container } from 'typedi'; +import { On, EventSubscriber } from 'event-dispatch'; +import events from 'subscribers/events'; +import TenancyService from 'services/Tenancy/TenancyService'; +import InventoryAdjustmentService from 'services/Inventory/InventoryAdjustmentService'; + +@EventSubscriber() +export default class InventoryAdjustmentsSubscriber { + logger: any; + tenancy: TenancyService; + inventoryAdjustment: InventoryAdjustmentService; + + /** + * Constructor method. + */ + constructor() { + this.logger = Container.get('logger'); + this.tenancy = Container.get(TenancyService); + this.inventoryAdjustment = Container.get(InventoryAdjustmentService); + } + + /** + * Handles writing inventory transactions once the quick adjustment created. + */ + @On(events.inventoryAdjustment.onQuickCreated) + async handleWriteInventoryTransactionsOnceQuickCreated({ + tenantId, + inventoryAdjustment, + }) { + await this.inventoryAdjustment.writeInventoryTransactions( + tenantId, + inventoryAdjustment + ) + } + + /** + * Handles reverting invetory transactions once the inventory adjustment deleted. + */ + @On(events.inventoryAdjustment.onDeleted) + async handleRevertInventoryTransactionsOnceDeleted({ + tenantId, + inventoryAdjustmentId + }) { + await this.inventoryAdjustment.revertInventoryTransactions( + tenantId, + inventoryAdjustmentId, + ); + } +} \ No newline at end of file