From faa81e3c258bf05360c5b52d186bf1b2623e4028 Mon Sep 17 00:00:00 2001 From: "a.bouhuolia" Date: Sat, 2 Jan 2021 15:48:59 +0200 Subject: [PATCH] fix: record authorized user id for customer/vendor opening balance. --- .../src/api/controllers/Contacts/Customers.ts | 168 +++++---- .../src/api/controllers/Contacts/Vendors.ts | 23 +- .../services/Accounting/JournalCommands.ts | 332 ++++++++++-------- .../src/services/Contacts/CustomersService.ts | 37 +- .../src/services/Contacts/VendorsService.ts | 57 ++- server/src/subscribers/customers.ts | 31 +- server/src/subscribers/vendors.ts | 8 +- 7 files changed, 398 insertions(+), 258 deletions(-) diff --git a/server/src/api/controllers/Contacts/Customers.ts b/server/src/api/controllers/Contacts/Customers.ts index 5ad76980e..d6971307f 100644 --- a/server/src/api/controllers/Contacts/Customers.ts +++ b/server/src/api/controllers/Contacts/Customers.ts @@ -22,12 +22,14 @@ export default class CustomersController extends ContactsController { router() { const router = Router(); - router.post('/', [ - ...this.contactDTOSchema, - ...this.contactNewDTOSchema, - ...this.customerDTOSchema, - ...this.createCustomerDTOSchema, - ], + router.post( + '/', + [ + ...this.contactDTOSchema, + ...this.contactNewDTOSchema, + ...this.customerDTOSchema, + ...this.createCustomerDTOSchema, + ], this.validationResult, asyncMiddleware(this.newCustomer.bind(this)), this.handlerServiceErrors @@ -41,41 +43,43 @@ export default class CustomersController extends ContactsController { ], this.validationResult, asyncMiddleware(this.editOpeningBalanceCustomer.bind(this)), - this.handlerServiceErrors, + this.handlerServiceErrors ); - router.post('/:id', [ - ...this.contactDTOSchema, - ...this.contactEditDTOSchema, - ...this.customerDTOSchema, - ], + router.post( + '/:id', + [ + ...this.contactDTOSchema, + ...this.contactEditDTOSchema, + ...this.customerDTOSchema, + ], this.validationResult, asyncMiddleware(this.editCustomer.bind(this)), - this.handlerServiceErrors, + this.handlerServiceErrors ); - router.delete('/:id', [ - ...this.specificContactSchema, - ], + router.delete( + '/:id', + [...this.specificContactSchema], this.validationResult, asyncMiddleware(this.deleteCustomer.bind(this)), - this.handlerServiceErrors, + this.handlerServiceErrors ); - router.delete('/', [ - ...this.bulkContactsSchema, - ], + router.delete( + '/', + [...this.bulkContactsSchema], this.validationResult, asyncMiddleware(this.deleteBulkCustomers.bind(this)), - this.handlerServiceErrors, + this.handlerServiceErrors ); - router.get('/', [ - ...this.validateListQuerySchema, - ], + router.get( + '/', + [...this.validateListQuerySchema], this.validationResult, asyncMiddleware(this.getCustomersList.bind(this)), - this.dynamicListService.handlerErrorsToResponse, + this.dynamicListService.handlerErrorsToResponse ); - router.get('/:id', [ - ...this.specificContactSchema, - ], + router.get( + '/:id', + [...this.specificContactSchema], this.validationResult, asyncMiddleware(this.getCustomer.bind(this)), this.handlerServiceErrors @@ -128,16 +132,20 @@ export default class CustomersController extends ContactsController { /** * Creates a new customer. - * @param {Request} req - * @param {Response} res - * @param {NextFunction} next + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next */ async newCustomer(req: Request, res: Response, next: NextFunction) { const contactDTO: ICustomerNewDTO = this.matchedBodyData(req); - const { tenantId } = req; + const { tenantId, user } = req; try { - const contact = await this.customersService.newCustomer(tenantId, contactDTO); + const contact = await this.customersService.newCustomer( + tenantId, + contactDTO, + user + ); return res.status(200).send({ id: contact.id, @@ -150,17 +158,22 @@ export default class CustomersController extends ContactsController { /** * Edits the given customer details. - * @param {Request} req - * @param {Response} res - * @param {NextFunction} next + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next */ async editCustomer(req: Request, res: Response, next: NextFunction) { const contactDTO: ICustomerEditDTO = this.matchedBodyData(req); - const { tenantId } = req; + const { tenantId, user } = req; const { id: contactId } = req.params; try { - await this.customersService.editCustomer(tenantId, contactId, contactDTO); + await this.customersService.editCustomer( + tenantId, + contactId, + contactDTO, + user + ); return res.status(200).send({ id: contactId, @@ -177,24 +190,26 @@ export default class CustomersController extends ContactsController { * @param {Response} res - * @param {NextFunction} next - */ - async editOpeningBalanceCustomer(req: Request, res: Response, next: NextFunction) { - const { tenantId } = req; + async editOpeningBalanceCustomer( + req: Request, + res: Response, + next: NextFunction + ) { + const { tenantId, user } = req; const { id: customerId } = req.params; - const { - openingBalance, - openingBalanceAt, - } = this.matchedBodyData(req); + const { openingBalance, openingBalanceAt } = this.matchedBodyData(req); try { await this.customersService.changeOpeningBalance( tenantId, customerId, openingBalance, - openingBalanceAt, + openingBalanceAt ); return res.status(200).send({ id: customerId, - message: 'The opening balance of the given customer has been changed successfully.', + message: + 'The opening balance of the given customer has been changed successfully.', }); } catch (error) { next(error); @@ -203,16 +218,20 @@ export default class CustomersController extends ContactsController { /** * Retrieve details of the given customer id. - * @param {Request} req - * @param {Response} res - * @param {NextFunction} next + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next */ async getCustomer(req: Request, res: Response, next: NextFunction) { - const { tenantId } = req; + const { tenantId, user } = req; const { id: contactId } = req.params; try { - const customer = await this.customersService.getCustomer(tenantId, contactId); + const customer = await this.customersService.getCustomer( + tenantId, + contactId, + user + ); return res.status(200).send({ customer: this.transfromToResponse(customer), @@ -224,16 +243,16 @@ export default class CustomersController extends ContactsController { /** * Deletes the given customer from the storage. - * @param {Request} req - * @param {Response} res - * @param {NextFunction} next + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next */ async deleteCustomer(req: Request, res: Response, next: NextFunction) { - const { tenantId } = req; + const { tenantId, user } = req; const { id: contactId } = req.params; try { - await this.customersService.deleteCustomer(tenantId, contactId); + await this.customersService.deleteCustomer(tenantId, contactId, user); return res.status(200).send({ id: contactId, @@ -246,16 +265,20 @@ export default class CustomersController extends ContactsController { /** * Deletes customers in bulk. - * @param {Request} req - * @param {Response} res - * @param {NextFunction} next + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next */ async deleteBulkCustomers(req: Request, res: Response, next: NextFunction) { const { ids: contactsIds } = req.query; - const { tenantId } = req; + const { tenantId, user } = req; try { - await this.customersService.deleteBulkCustomers(tenantId, contactsIds) + await this.customersService.deleteBulkCustomers( + tenantId, + contactsIds, + user + ); return res.status(200).send({ ids: contactsIds, @@ -268,9 +291,9 @@ export default class CustomersController extends ContactsController { /** * Retrieve customers paginated and filterable list. - * @param {Request} req - * @param {Response} res - * @param {NextFunction} next + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next */ async getCustomersList(req: Request, res: Response, next: NextFunction) { const { tenantId } = req; @@ -305,12 +328,17 @@ export default class CustomersController extends ContactsController { /** * Handles service errors. - * @param {Error} error - * @param {Request} req - * @param {Response} res - * @param {NextFunction} next + * @param {Error} error + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next */ - handlerServiceErrors(error: Error, req: Request, res: Response, next: NextFunction) { + handlerServiceErrors( + error: Error, + req: Request, + res: Response, + next: NextFunction + ) { if (error instanceof ServiceError) { if (error.errorType === 'contact_not_found') { return res.boom.badRequest(null, { @@ -340,4 +368,4 @@ export default class CustomersController extends ContactsController { } next(error); } -} \ No newline at end of file +} diff --git a/server/src/api/controllers/Contacts/Vendors.ts b/server/src/api/controllers/Contacts/Vendors.ts index f14b4fa03..cc03a1ba8 100644 --- a/server/src/api/controllers/Contacts/Vendors.ts +++ b/server/src/api/controllers/Contacts/Vendors.ts @@ -118,10 +118,10 @@ export default class VendorsController extends ContactsController { */ async newVendor(req: Request, res: Response, next: NextFunction) { const contactDTO: IVendorNewDTO = this.matchedBodyData(req); - const { tenantId } = req; + const { tenantId, user } = req; try { - const vendor = await this.vendorsService.newVendor(tenantId, contactDTO); + const vendor = await this.vendorsService.newVendor(tenantId, contactDTO, user); return res.status(200).send({ id: vendor.id, @@ -140,11 +140,11 @@ export default class VendorsController extends ContactsController { */ async editVendor(req: Request, res: Response, next: NextFunction) { const contactDTO: IVendorEditDTO = this.matchedBodyData(req); - const { tenantId } = req; + const { tenantId, user } = req; const { id: contactId } = req.params; try { - await this.vendorsService.editVendor(tenantId, contactId, contactDTO); + await this.vendorsService.editVendor(tenantId, contactId, contactDTO, user); return res.status(200).send({ id: contactId, @@ -162,7 +162,7 @@ export default class VendorsController extends ContactsController { * @param {NextFunction} next - */ async editOpeningBalanceVendor(req: Request, res: Response, next: NextFunction) { - const { tenantId } = req; + const { tenantId, user } = req; const { id: vendorId } = req.params; const { openingBalance, @@ -192,11 +192,11 @@ export default class VendorsController extends ContactsController { * @param {NextFunction} next */ async deleteVendor(req: Request, res: Response, next: NextFunction) { - const { tenantId } = req; + const { tenantId, user } = req; const { id: contactId } = req.params; try { - await this.vendorsService.deleteVendor(tenantId, contactId) + await this.vendorsService.deleteVendor(tenantId, contactId, user) return res.status(200).send({ id: contactId, @@ -214,11 +214,12 @@ export default class VendorsController extends ContactsController { * @param {NextFunction} next */ async getVendor(req: Request, res: Response, next: NextFunction) { - const { tenantId } = req; + const { tenantId, user } = req; const { id: vendorId } = req.params; try { - const vendor = await this.vendorsService.getVendor(tenantId, vendorId) + const vendor = await this.vendorsService.getVendor(tenantId, vendorId, user) + return res.status(200).send({ vendor }); } catch (error) { next(error); @@ -233,10 +234,10 @@ export default class VendorsController extends ContactsController { */ async deleteBulkVendors(req: Request, res: Response, next: NextFunction) { const { ids: contactsIds } = req.query; - const { tenantId } = req; + const { tenantId, user } = req; try { - await this.vendorsService.deleteBulkVendors(tenantId, contactsIds) + await this.vendorsService.deleteBulkVendors(tenantId, contactsIds, user) return res.status(200).send({ ids: contactsIds, diff --git a/server/src/services/Accounting/JournalCommands.ts b/server/src/services/Accounting/JournalCommands.ts index 50d21aca1..d62ebeac5 100644 --- a/server/src/services/Accounting/JournalCommands.ts +++ b/server/src/services/Accounting/JournalCommands.ts @@ -1,8 +1,8 @@ import { sumBy, chain } from 'lodash'; import moment from 'moment'; -import { IBill } from 'interfaces'; -import JournalPoster from "./JournalPoster"; -import JournalEntry from "./JournalEntry"; +import { IBill, ISystemUser } from 'interfaces'; +import JournalPoster from './JournalPoster'; +import JournalEntry from './JournalEntry'; import { AccountTransaction } from 'models'; import { IInventoryTransaction, @@ -16,37 +16,37 @@ import { } from 'interfaces'; interface IInventoryCostEntity { - date: Date, + date: Date; - referenceType: string, - referenceId: number, + referenceType: string; + referenceId: number; - costAccount: number, - incomeAccount: number, - inventoryAccount: number, + costAccount: number; + incomeAccount: number; + inventoryAccount: number; - inventory: number, - cost: number, - income: number, -}; + inventory: number; + cost: number; + income: number; +} interface NonInventoryJEntries { - date: Date, + date: Date; - referenceType: string, - referenceId: number, + referenceType: string; + referenceId: number; - receivable: number, - payable: number, + receivable: number; + payable: number; - incomeAccountId: number, - income: number, + incomeAccountId: number; + income: number; - costAccountId: number, - cost: number, -}; + costAccountId: number; + cost: number; +} -export default class JournalCommands{ +export default class JournalCommands { journal: JournalPoster; models: any; @@ -54,18 +54,18 @@ export default class JournalCommands{ /** * Constructor method. - * @param {JournalPoster} journal - + * @param {JournalPoster} journal - */ constructor(journal: JournalPoster) { this.journal = journal; - + this.repositories = this.journal.repositories; this.models = this.journal.models; } /** * Records the bill journal entries. - * @param {IBill} bill + * @param {IBill} bill * @param {boolean} override - Override the old bill entries. */ async bill(bill: IBill, override: boolean = false): Promise { @@ -78,7 +78,9 @@ export default class JournalCommands{ 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 payableAccount = await accountRepository.findOne({ + slug: 'accounts-payable', + }); const formattedDate = moment(bill.billDate).format('YYYY-MM-DD'); const commonJournalMeta = { @@ -127,32 +129,45 @@ export default class JournalCommands{ /** * Customer opening balance journals. - * @param {number} customerId - * @param {number} openingBalance + * @param {number} customerId + * @param {number} openingBalance */ - async customerOpeningBalance(customerId: number, openingBalance: number) { + async customerOpeningBalance( + customerId: number, + openingBalance: number, + openingBalanceAt: Date | string, + userId: number + ) { const { accountRepository } = this.repositories; - const openingBalanceAccount = await accountRepository.findOne({ slug: 'opening-balance' }); - const receivableAccount = await accountRepository.findOne({ slug: 'accounts-receivable' }); + const openingBalanceAccount = await accountRepository.findOne({ + slug: 'opening-balance', + }); + const receivableAccount = await accountRepository.findOne({ + slug: 'accounts-receivable', + }); const commonEntry = { referenceType: 'CustomerOpeningBalance', referenceId: customerId, contactType: 'Customer', contactId: customerId, + date: openingBalanceAt, + userId, }; - const creditEntry = new JournalEntry({ - ...commonEntry, - credit: openingBalance, - debit: 0, - account: openingBalanceAccount.id, - }); const debitEntry = new JournalEntry({ ...commonEntry, credit: 0, debit: openingBalance, account: receivableAccount.id, + index: 1, + }); + const creditEntry = new JournalEntry({ + ...commonEntry, + credit: openingBalance, + debit: 0, + account: openingBalanceAccount.id, + index: 2, }); this.journal.debit(debitEntry); this.journal.credit(creditEntry); @@ -160,32 +175,47 @@ export default class JournalCommands{ /** * Vendor opening balance journals - * @param {number} vendorId - * @param {number} openingBalance + * @param {number} vendorId + * @param {number} openingBalance + * @param {Date|string} openingBalanceAt + * @param {number} authorizedUserId */ - async vendorOpeningBalance(vendorId: number, openingBalance: number) { + async vendorOpeningBalance( + vendorId: number, + openingBalance: number, + openingBalanceAt: Date|string, + authorizedUserId: ISystemUser + ) { const { accountRepository } = this.repositories; - const payableAccount = await accountRepository.findOne({ slug: 'accounts-payable' }); - const otherCost = await accountRepository.findOne({ slug: 'other-expenses' }); + const payableAccount = await accountRepository.findOne({ + slug: 'accounts-payable', + }); + const otherCost = await accountRepository.findOne({ + slug: 'other-expenses', + }); const commonEntry = { referenceType: 'VendorOpeningBalance', referenceId: vendorId, contactType: 'Vendor', contactId: vendorId, + date: openingBalanceAt, + userId: authorizedUserId, }; const creditEntry = new JournalEntry({ ...commonEntry, account: payableAccount.id, credit: openingBalance, debit: 0, + index: 1, }); const debitEntry = new JournalEntry({ ...commonEntry, account: otherCost.id, debit: openingBalance, credit: 0, + index: 2, }); this.journal.debit(debitEntry); this.journal.credit(creditEntry); @@ -193,7 +223,7 @@ export default class JournalCommands{ /** * Writes journal entries of expense model object. - * @param {IExpense} expense + * @param {IExpense} expense */ expense(expense: IExpense) { const mixinEntry = { @@ -224,32 +254,37 @@ export default class JournalCommands{ } /** - * - * @param {number|number[]} referenceId - * @param {string} referenceType + * + * @param {number|number[]} referenceId + * @param {string} referenceType */ async revertJournalEntries( - referenceId: number|number[], + referenceId: number | number[], referenceType: string ) { const { AccountTransaction } = this.models; const transactions = await AccountTransaction.query() .where('reference_type', referenceType) - .whereIn('reference_id', Array.isArray(referenceId) ? referenceId : [referenceId]) + .whereIn( + 'reference_id', + Array.isArray(referenceId) ? referenceId : [referenceId] + ) .withGraphFetched('account.type'); this.journal.fromTransactions(transactions); this.journal.removeEntries(); } - /** * Writes journal entries from manual journal model object. - * @param {IManualJournal} manualJournalObj - * @param {number} manualJournalId + * @param {IManualJournal} manualJournalObj + * @param {number} manualJournalId */ - async manualJournal(manualJournalObj: IManualJournal, manualJournalId: number) { + async manualJournal( + manualJournalObj: IManualJournal, + manualJournalId: number + ) { manualJournalObj.entries.forEach((entry) => { const jouranlEntry = new JournalEntry({ debit: entry.debit, @@ -276,39 +311,49 @@ export default class JournalCommands{ /** * Removes and revert accounts balance journal entries that associated * to the given inventory transactions. - * @param {IInventoryTransaction[]} inventoryTransactions - * @param {Journal} journal + * @param {IInventoryTransaction[]} inventoryTransactions + * @param {Journal} journal */ - revertEntriesFromInventoryTransactions(inventoryTransactions: IInventoryTransaction[]) { + revertEntriesFromInventoryTransactions( + inventoryTransactions: IInventoryTransaction[] + ) { const groupedInvTransactions = chain(inventoryTransactions) - .groupBy((invTransaction: IInventoryTransaction) => invTransaction.transactionType) - .map((groupedTrans: IInventoryTransaction[], transType: string) => [groupedTrans, transType]) + .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); + 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'); + 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)); + if (_transactions.length > 0) { + this.journal.loadEntries(_transactions); + this.journal.removeEntries(_transactions.map((t: any) => t.id)); + } } - }) + ) ); } - public async nonInventoryEntries( - transactions: NonInventoryJEntries[] - ) { + public async nonInventoryEntries(transactions: NonInventoryJEntries[]) { const receivableAccount = { id: 10 }; - const payableAccount = {id: 11}; + const payableAccount = { id: 11 }; transactions.forEach((trans: NonInventoryJEntries) => { const commonEntry = { @@ -317,12 +362,12 @@ export default class JournalCommands{ referenceType: trans.referenceType, }; - switch(trans.referenceType) { + switch (trans.referenceType) { case 'Bill': const payableEntry: JournalEntry = new JournalEntry({ ...commonEntry, credit: trans.payable, - account: payableAccount.id, + account: payableAccount.id, }); const costEntry: JournalEntry = new JournalEntry({ ...commonEntry, @@ -349,14 +394,12 @@ export default class JournalCommands{ } /** - * + * * @param {string} referenceType - * @param {number} referenceId - * @param {ISaleInvoice[]} sales - */ - public async inventoryEntries( - transactions: IInventoryCostEntity[], - ) { + public async inventoryEntries(transactions: IInventoryCostEntity[]) { const receivableAccount = { id: 10 }; const payableAccount = { id: 11 }; @@ -366,12 +409,12 @@ export default class JournalCommands{ referenceId: sale.referenceId, referenceType: sale.referenceType, }; - switch(sale.referenceType) { + switch (sale.referenceType) { case 'Bill': const inventoryDebit: JournalEntry = new JournalEntry({ ...commonEntry, debit: sale.inventory, - account: sale.inventoryAccount, + account: sale.inventoryAccount, }); const payableEntry: JournalEntry = new JournalEntry({ ...commonEntry, @@ -401,7 +444,7 @@ export default class JournalCommands{ const inventoryCredit: JournalEntry = new JournalEntry({ ...commonEntry, credit: sale.cost, - account: sale.inventoryAccount, + account: sale.inventoryAccount, }); this.journal.debit(receivableEntry); this.journal.debit(costEntry); @@ -418,19 +461,19 @@ export default class JournalCommands{ * ---------- * - Receivable accounts -> Debit -> XXXX * - Income -> Credit -> XXXX - * + * * - Cost of goods sold -> Debit -> YYYY * - Inventory assets -> YYYY - * - * @param {ISaleInvoice} saleInvoice - * @param {JournalPoster} journal + * + * @param {ISaleInvoice} saleInvoice + * @param {JournalPoster} journal */ saleInvoice( saleInvoice: ISaleInvoice & { - costTransactions: IInventoryLotCost[], - entries: IItemEntry & { item: IItem }, + costTransactions: IInventoryLotCost[]; + entries: IItemEntry & { item: IItem }; }, - receivableAccountsId: number, + receivableAccountsId: number ) { let inventoryTotal: number = 0; @@ -441,8 +484,9 @@ export default class JournalCommands{ }; const costTransactions: Map = new Map( saleInvoice.costTransactions.map((trans: IInventoryLotCost) => [ - trans.entryId, trans.cost, - ]), + trans.entryId, + trans.cost, + ]) ); // XXX Debit - Receivable account. const receivableEntry = new JournalEntry({ @@ -453,50 +497,52 @@ export default class JournalCommands{ }); 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); + saleInvoice.entries.forEach( + (entry: IItemEntry & { item: IItem }, index) => { + const cost: number = costTransactions.get(entry.id); + const income: number = entry.quantity * entry.rate; - if (inventoryTotal > 0) { - // XXX Credit - Inventory account. - const inventoryEntry = new JournalEntry({ + 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: inventoryTotal, - account: entry.item.inventoryAccountId, - index: index + 4, + credit: income, + account: entry.item.sellAccountId, + note: entry.description, + index: index + 2, }); - this.journal.credit(inventoryEntry); + 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); + } } - }); + ); } saleInvoiceNonInventory( saleInvoice: ISaleInvoice & { - entries: IItemEntry & { item: IItem }, + entries: IItemEntry & { item: IItem }; }, - receivableAccountsId: number, + receivableAccountsId: number ) { const commonEntry = { referenceType: 'SaleInvoice', @@ -513,18 +559,20 @@ export default class JournalCommands{ }); this.journal.debit(receivableEntry); - saleInvoice.entries.forEach((entry: IItemEntry & { item: IItem }, index: number) => { - const income: number = entry.quantity * entry.rate; - - // 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); - }); + saleInvoice.entries.forEach( + (entry: IItemEntry & { item: IItem }, index: number) => { + const income: number = entry.quantity * entry.rate; + + // 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); + } + ); } -} \ No newline at end of file +} diff --git a/server/src/services/Contacts/CustomersService.ts b/server/src/services/Contacts/CustomersService.ts index d2a49b5d2..31028b1f5 100644 --- a/server/src/services/Contacts/CustomersService.ts +++ b/server/src/services/Contacts/CustomersService.ts @@ -17,6 +17,8 @@ import { IContactEditDTO, IContact, ISaleInvoice, + ISystemService, + ISystemUser, } from 'interfaces'; import { ServiceError } from 'exceptions'; import TenancyService from 'services/Tenancy/TenancyService'; @@ -90,20 +92,20 @@ export default class CustomersService { */ public async newCustomer( tenantId: number, - customerDTO: ICustomerNewDTO + customerDTO: ICustomerNewDTO, + authorizedUser: ISystemUser ): Promise { this.logger.info('[customer] trying to create a new customer.', { tenantId, customerDTO, }); - const customerObj = this.transformNewCustomerDTO(customerDTO); + const customer = await this.contactService.newContact( tenantId, customerObj, 'customer' ); - this.logger.info('[customer] created successfully.', { tenantId, customerDTO, @@ -112,6 +114,7 @@ export default class CustomersService { customer, tenantId, customerId: customer.id, + authorizedUser, }); return customer; @@ -127,7 +130,8 @@ export default class CustomersService { public async editCustomer( tenantId: number, customerId: number, - customerDTO: ICustomerEditDTO + customerDTO: ICustomerEditDTO, + authorizedUser: ISystemUser ): Promise { const contactDTO = this.customerToContactDTO(customerDTO); @@ -143,11 +147,13 @@ export default class CustomersService { 'customer' ); + // Triggers `onCustomerEdited` event. this.eventDispatcher.dispatch(events.customers.onEdited); this.logger.info('[customer] edited successfully.', { tenantId, customerId, customer, + authorizedUser, }); return customer; @@ -161,7 +167,8 @@ export default class CustomersService { */ public async deleteCustomer( tenantId: number, - customerId: number + customerId: number, + authorizedUser: ISystemUser ): Promise { this.logger.info('[customer] trying to delete customer.', { tenantId, @@ -184,6 +191,7 @@ export default class CustomersService { this.logger.info('[customer] deleted successfully.', { tenantId, customerId, + authorizedUser, }); } @@ -192,7 +200,11 @@ export default class CustomersService { * @param {number} tenantId * @param {number} customerId */ - public async getCustomer(tenantId: number, customerId: number) { + public async getCustomer( + tenantId: number, + customerId: number, + authorizedUser: ISystemUser + ) { const contact = await this.contactService.getContact( tenantId, customerId, @@ -245,7 +257,8 @@ export default class CustomersService { tenantId: number, customerId: number, openingBalance: number, - openingBalanceAt: Date | string + openingBalanceAt: Date | string, + authorizedUserId: number ) { const journal = new JournalPoster(tenantId); const journalCommands = new JournalCommands(journal); @@ -253,7 +266,8 @@ export default class CustomersService { await journalCommands.customerOpeningBalance( customerId, openingBalance, - openingBalanceAt + openingBalanceAt, + authorizedUserId ); await Promise.all([journal.saveBalance(), journal.saveEntries()]); } @@ -316,7 +330,11 @@ export default class CustomersService { * @param {number[]} customersIds * @return {Promise} */ - public async deleteBulkCustomers(tenantId: number, customersIds: number[]) { + public async deleteBulkCustomers( + tenantId: number, + customersIds: number[], + authorizedUser: ISystemUser, + ): Promise { const { Contact } = this.tenancy.models(tenantId); // Validate the customers existance on the storage. @@ -332,6 +350,7 @@ export default class CustomersService { await this.eventDispatcher.dispatch(events.customers.onBulkDeleted, { tenantId, customersIds, + authorizedUser, }); } diff --git a/server/src/services/Contacts/VendorsService.ts b/server/src/services/Contacts/VendorsService.ts index 2d024ea29..178498fd5 100644 --- a/server/src/services/Contacts/VendorsService.ts +++ b/server/src/services/Contacts/VendorsService.ts @@ -14,6 +14,7 @@ import { IVendorsFilter, IPaginationMeta, IFilterMeta, + ISystemUser, } from 'interfaces'; import { ServiceError } from 'exceptions'; import DynamicListingService from 'services/DynamicListing/DynamicListService'; @@ -55,24 +56,28 @@ export default class VendorsService { * @param {IVendorNewDTO} vendorDTO * @return {Promise} */ - public async newVendor(tenantId: number, vendorDTO: IVendorNewDTO) { + public async newVendor( + tenantId: number, + vendorDTO: IVendorNewDTO, + authorizedUser: ISystemUser + ) { this.logger.info('[vendor] trying create a new vendor.', { tenantId, vendorDTO, }); - const contactDTO = this.vendorToContactDTO(vendorDTO); + const vendor = await this.contactService.newContact( tenantId, contactDTO, 'vendor' ); - // Triggers `onVendorCreated` event. await this.eventDispatcher.dispatch(events.vendors.onCreated, { tenantId, vendorId: vendor.id, vendor, + authorizedUser, }); return vendor; } @@ -85,7 +90,8 @@ export default class VendorsService { public async editVendor( tenantId: number, vendorId: number, - vendorDTO: IVendorEditDTO + vendorDTO: IVendorEditDTO, + authorizedUser: ISystemUser ) { const contactDTO = this.vendorToContactDTO(vendorDTO); const vendor = await this.contactService.editContact( @@ -95,8 +101,13 @@ export default class VendorsService { 'vendor' ); - await this.eventDispatcher.dispatch(events.vendors.onEdited); - + // Triggers `onVendorEdited` event. + await this.eventDispatcher.dispatch(events.vendors.onEdited, { + tenantId, + vendorId, + vendor, + authorizedUser, + }); return vendor; } @@ -119,8 +130,15 @@ export default class VendorsService { * @param {number} vendorId * @return {Promise} */ - public async deleteVendor(tenantId: number, vendorId: number) { + public async deleteVendor( + tenantId: number, + vendorId: number, + authorizedUser: ISystemUser + ) { + // Validate the vendor existance on the storage. await this.getVendorByIdOrThrowError(tenantId, vendorId); + + // Validate the vendor has no associated bills. await this.vendorHasNoBillsOrThrowError(tenantId, vendorId); this.logger.info('[vendor] trying to delete vendor.', { @@ -129,9 +147,11 @@ export default class VendorsService { }); await this.contactService.deleteContact(tenantId, vendorId, 'vendor'); + // Triggers `onVendorDeleted` event. await this.eventDispatcher.dispatch(events.vendors.onDeleted, { tenantId, vendorId, + authorizedUser, }); this.logger.info('[vendor] deleted successfully.', { tenantId, vendorId }); } @@ -155,7 +175,9 @@ export default class VendorsService { public async writeVendorOpeningBalanceJournal( tenantId: number, vendorId: number, - openingBalance: number + openingBalance: number, + openingBalanceAt: Date | string, + user: ISystemUser ) { const journal = new JournalPoster(tenantId); const journalCommands = new JournalCommands(journal); @@ -164,8 +186,12 @@ export default class VendorsService { tenantId, vendorId, }); - await journalCommands.vendorOpeningBalance(vendorId, openingBalance); - + await journalCommands.vendorOpeningBalance( + vendorId, + openingBalance, + openingBalanceAt, + user + ); await Promise.all([journal.saveBalance(), journal.saveEntries()]); } @@ -183,7 +209,7 @@ export default class VendorsService { this.logger.info( '[customer] trying to revert opening balance journal entries.', - { tenantId, customerId } + { tenantId, vendorId } ); await this.contactService.revertJEntriesContactsOpeningBalance( tenantId, @@ -216,7 +242,8 @@ export default class VendorsService { */ public async deleteBulkVendors( tenantId: number, - vendorsIds: number[] + vendorsIds: number[], + authorizedUser: ISystemUser ): Promise { const { Contact } = this.tenancy.models(tenantId); @@ -228,9 +255,11 @@ export default class VendorsService { await Contact.query().whereIn('id', vendorsIds).delete(); + // Triggers `onVendorsBulkDeleted` event. await this.eventDispatcher.dispatch(events.vendors.onBulkDeleted, { tenantId, vendorsIds, + authorizedUser, }); this.logger.info('[vendor] bulk deleted successfully.', { @@ -266,7 +295,7 @@ export default class VendorsService { */ private async vendorsHaveNoBillsOrThrowError( tenantId: number, - vendorsIds: number[], + vendorsIds: number[] ) { const { billRepository } = this.tenancy.repositories(tenantId); @@ -299,12 +328,12 @@ export default class VendorsService { filterMeta: IFilterMeta; }> { const { Vendor } = this.tenancy.models(tenantId); + const dynamicFilter = await this.dynamicListService.dynamicList( tenantId, Vendor, vendorsFilter ); - const { results, pagination } = await Vendor.query() .onBuild((builder) => { dynamicFilter.buildQuery()(builder); diff --git a/server/src/subscribers/customers.ts b/server/src/subscribers/customers.ts index f2e0b652f..3edda50e2 100644 --- a/server/src/subscribers/customers.ts +++ b/server/src/subscribers/customers.ts @@ -19,8 +19,12 @@ export default class CustomersSubscriber { * Handles the writing opening balance journal entries once the customer created. */ @On(events.customers.onCreated) - async handleWriteOpenBalanceEntries({ tenantId, customerId, customer }) { - + async handleWriteOpenBalanceEntries({ + tenantId, + customerId, + customer, + authorizedUser, + }) { // Writes the customer opening balance journal entries. if (customer.openingBalance) { await this.customersService.writeCustomerOpeningBalanceJournal( @@ -28,6 +32,7 @@ export default class CustomersSubscriber { customer.id, customer.openingBalance, customer.openingBalanceAt, + authorizedUser.id ); } } @@ -36,10 +41,14 @@ export default class CustomersSubscriber { * Handles the deleting opeing balance journal entrise once the customer deleted. */ @On(events.customers.onDeleted) - async handleRevertOpeningBalanceEntries({ tenantId, customerId }) { - + async handleRevertOpeningBalanceEntries({ + tenantId, + customerId, + authorizedUser, + }) { await this.customersService.revertOpeningBalanceEntries( - tenantId, customerId, + tenantId, + customerId ); } @@ -48,10 +57,14 @@ export default class CustomersSubscriber { * customers deleted. */ @On(events.customers.onBulkDeleted) - async handleBulkRevertOpeningBalanceEntries({ tenantId, customersIds }) { - + async handleBulkRevertOpeningBalanceEntries({ + tenantId, + customersIds, + authorizedUser, + }) { await this.customersService.revertOpeningBalanceEntries( - tenantId, customersIds, + tenantId, + customersIds ); } -} \ No newline at end of file +} diff --git a/server/src/subscribers/vendors.ts b/server/src/subscribers/vendors.ts index c0db44a27..ac5fe4e92 100644 --- a/server/src/subscribers/vendors.ts +++ b/server/src/subscribers/vendors.ts @@ -22,13 +22,15 @@ export default class VendorsSubscriber { * Writes the open balance journal entries once the vendor created. */ @On(events.vendors.onCreated) - async handleWriteOpeningBalanceEntries({ tenantId, vendorId, vendor }) { + async handleWriteOpeningBalanceEntries({ tenantId, vendorId, vendor, authorizedUser }) { // Writes the vendor opening balance journal entries. if (vendor.openingBalance) { await this.vendorsService.writeVendorOpeningBalanceJournal( tenantId, vendor.id, vendor.openingBalance, + vendor.openingBalanceAt, + authorizedUser.id ); } } @@ -37,7 +39,7 @@ export default class VendorsSubscriber { * Revert the opening balance journal entries once the vendor deleted. */ @On(events.vendors.onDeleted) - async handleRevertOpeningBalanceEntries({ tenantId, vendorId }) { + async handleRevertOpeningBalanceEntries({ tenantId, vendorId, authorizedUser }) { await this.vendorsService.revertOpeningBalanceEntries( tenantId, vendorId, ); @@ -47,7 +49,7 @@ export default class VendorsSubscriber { * Revert the opening balance journal entries once the vendors deleted in bulk. */ @On(events.vendors.onBulkDeleted) - async handleBulkRevertOpeningBalanceEntries({ tenantId, vendorsIds }) { + async handleBulkRevertOpeningBalanceEntries({ tenantId, vendorsIds, authorizedUser }) { await this.vendorsService.revertOpeningBalanceEntries( tenantId, vendorsIds, );