From ccf4fa55d92786a8ff66b41f767f9f5e2a9fc002 Mon Sep 17 00:00:00 2001 From: "a.bouhuolia" Date: Sun, 3 Jan 2021 19:39:17 +0200 Subject: [PATCH] fix: writing journal entries of manual journal. --- server/src/api/controllers/ManualJournals.ts | 416 ++++++++++-------- ...0105195823_create_manual_journals_table.js | 2 +- ...25_create_manual_journals_entries_table.js | 18 + server/src/interfaces/ManualJournal.ts | 121 +++-- server/src/loaders/tenantModels.ts | 2 + server/src/models/ManualJournal.js | 10 +- server/src/models/ManualJournalEntry.js | 18 + .../services/Accounting/JournalCommands.ts | 15 +- .../src/services/Expenses/ExpensesService.ts | 2 +- .../ManualJournals/ManualJournalsService.ts | 368 ++++++++++++---- .../services/Sales/JournalPosterService.ts | 2 +- server/src/services/Sales/PaymentsReceives.ts | 8 +- server/src/subscribers/manualJournals.ts | 94 +++- 13 files changed, 715 insertions(+), 361 deletions(-) create mode 100644 server/src/database/migrations/20200105195825_create_manual_journals_entries_table.js create mode 100644 server/src/models/ManualJournalEntry.js diff --git a/server/src/api/controllers/ManualJournals.ts b/server/src/api/controllers/ManualJournals.ts index a5c6cde61..879390143 100644 --- a/server/src/api/controllers/ManualJournals.ts +++ b/server/src/api/controllers/ManualJournals.ts @@ -3,14 +3,13 @@ import { check, param, query } from 'express-validator'; import BaseController from 'api/controllers/BaseController'; import asyncMiddleware from 'api/middleware/asyncMiddleware'; import ManualJournalsService from 'services/ManualJournals/ManualJournalsService'; -import { Inject, Service } from "typedi"; +import { Inject, Service } from 'typedi'; import { ServiceError } from 'exceptions'; import DynamicListingService from 'services/DynamicListing/DynamicListService'; import { DATATYPES_LENGTH } from 'data/DataTypes'; @Service() export default class ManualJournalsController extends BaseController { - @Inject() manualJournalsService: ManualJournalsService; @@ -24,67 +23,59 @@ export default class ManualJournalsController extends BaseController { const router = Router(); router.get( - '/', [ - ...this.manualJournalsListSchema, - ], + '/', + [...this.manualJournalsListSchema], this.validationResult, asyncMiddleware(this.getManualJournalsList.bind(this)), this.dynamicListService.handlerErrorsToResponse, - this.catchServiceErrors.bind(this), + this.catchServiceErrors.bind(this) ); router.get( '/:id', asyncMiddleware(this.getManualJournal.bind(this)), - this.catchServiceErrors.bind(this), + this.catchServiceErrors.bind(this) ); router.post( - '/publish', [ - ...this.manualJournalIdsSchema, - ], + '/publish', + [...this.manualJournalIdsSchema], this.validationResult, asyncMiddleware(this.publishManualJournals.bind(this)), - this.catchServiceErrors.bind(this), + this.catchServiceErrors.bind(this) ); router.post( - '/:id/publish', [ - ...this.manualJournalParamSchema, - ], + '/:id/publish', + [...this.manualJournalParamSchema], this.validationResult, asyncMiddleware(this.publishManualJournal.bind(this)), - this.catchServiceErrors.bind(this), + this.catchServiceErrors.bind(this) ); router.post( - '/:id', [ - ...this.manualJournalValidationSchema, - ...this.manualJournalParamSchema, - ], + '/:id', + [...this.manualJournalValidationSchema, ...this.manualJournalParamSchema], this.validationResult, asyncMiddleware(this.editManualJournal.bind(this)), - this.catchServiceErrors.bind(this), + this.catchServiceErrors.bind(this) ); router.delete( - '/:id', [ - ...this.manualJournalParamSchema, - ], + '/:id', + [...this.manualJournalParamSchema], this.validationResult, asyncMiddleware(this.deleteManualJournal.bind(this)), - this.catchServiceErrors.bind(this), + this.catchServiceErrors.bind(this) ); router.delete( - '/', [ - ...this.manualJournalIdsSchema, - ], + '/', + [...this.manualJournalIdsSchema], this.validationResult, asyncMiddleware(this.deleteBulkManualJournals.bind(this)), - this.catchServiceErrors.bind(this), + this.catchServiceErrors.bind(this) ); router.post( - '/', [ - ...this.manualJournalValidationSchema, - ], + '/', + [...this.manualJournalValidationSchema], this.validationResult, asyncMiddleware(this.makeJournalEntries.bind(this)), - this.catchServiceErrors.bind(this), + this.catchServiceErrors.bind(this) ); return router; } @@ -93,9 +84,7 @@ export default class ManualJournalsController extends BaseController { * Specific manual journal id param validation schema. */ get manualJournalParamSchema() { - return [ - param('id').exists().isNumeric().toInt() - ]; + return [param('id').exists().isNumeric().toInt()]; } /** @@ -105,7 +94,7 @@ export default class ManualJournalsController extends BaseController { return [ query('ids').isArray({ min: 1 }), query('ids.*').isNumeric().toInt(), - ] + ]; } /** @@ -138,11 +127,12 @@ export default class ManualJournalsController extends BaseController { .trim() .escape() .isLength({ max: DATATYPES_LENGTH.TEXT }), - check('status').optional().isBoolean().toBoolean(), + check('publish').optional().isBoolean().toBoolean(), check('entries').isArray({ min: 2 }), check('entries.*.index') .exists() - .isInt({ max: DATATYPES_LENGTH.INT_10 }).toInt(), + .isInt({ max: DATATYPES_LENGTH.INT_10 }) + .toInt(), check('entries.*.credit') .optional({ nullable: true }) .isFloat({ min: 0, max: DATATYPES_LENGTH.DECIMAL_13_3 }) @@ -151,7 +141,9 @@ export default class ManualJournalsController extends BaseController { .optional({ nullable: true }) .isFloat({ min: 0, max: DATATYPES_LENGTH.DECIMAL_13_3 }) .toFloat(), - check('entries.*.account_id').isInt({ max: DATATYPES_LENGTH.INT_10 }).toInt(), + check('entries.*.account_id') + .isInt({ max: DATATYPES_LENGTH.INT_10 }) + .toInt(), check('entries.*.note') .optional({ nullable: true }) .isString() @@ -161,7 +153,7 @@ export default class ManualJournalsController extends BaseController { .isInt({ max: DATATYPES_LENGTH.INT_10 }) .toInt(), check('entries.*.contact_type').optional().isIn(['vendor', 'customer']), - ] + ]; } /** @@ -180,119 +172,24 @@ export default class ManualJournalsController extends BaseController { ]; } - async getManualJournal(req: Request, res: Response, next: NextFunction) { - const { tenantId } = req; - const { id: manualJournalId } = req.params; - - try { - const manualJournal = await this.manualJournalsService.getManualJournal(tenantId, manualJournalId); - return res.status(200).send({ manualJournal }); - } catch (error) { - next(error); - }; - } - - /** - * Publish the given manual journal. - * @param {Request} req - * @param {Response} res - * @param {NextFunction} next - */ - async publishManualJournal(req: Request, res: Response, next: NextFunction) { - const { tenantId } = req; - const { id: manualJournalId } = req.params; - - try { - await this.manualJournalsService.publishManualJournal(tenantId, manualJournalId); - - return res.status(200).send({ - id: manualJournalId, - message: 'The manual journal has been published successfully.', - }); - } catch (error) { - next(error); - } - } - - /** - * Publish the given manual journals in bulk. - * @param {Request} req - * @param {Response} res - * @param {NextFunction} next - */ - async publishManualJournals(req: Request, res: Response, next: NextFunction) { - const { tenantId } = req; - const { ids: manualJournalsIds } = req.query; - - try { - await this.manualJournalsService.publishManualJournals(tenantId, manualJournalsIds); - - return res.status(200).send({ - ids: manualJournalsIds, - message: 'The manual journals have been published successfully.', - }); - } catch (error) { - next(error); - } - } - - /** - * Delete the given manual journal. - * @param {Request} req - * @param {Response} res - * @param {NextFunction} next - */ - async deleteManualJournal(req: Request, res: Response, next: NextFunction) { - const { tenantId, user } = req; - const { id: manualJournalId } = req.params; - - try { - await this.manualJournalsService.deleteManualJournal(tenantId, manualJournalId); - - return res.status(200).send({ - id: manualJournalId, - message: 'Manual journal has been deleted successfully.', - }); - } catch (error) { - next(error); - } - } - - /** - * Deletes manual journals in bulk. - * @param {Request} req - * @param {Response} res - * @param {NextFunction} next - */ - async deleteBulkManualJournals(req: Request, res: Response, next: NextFunction) { - const { tenantId } = req; - const { ids: manualJournalsIds } = req.query; - - try { - await this.manualJournalsService.deleteManualJournals(tenantId, manualJournalsIds); - - return res.status(200).send({ - ids: manualJournalsIds, - message: 'Manual journal have been delete successfully.', - }); - } catch (error) { - next(error); - } - } - /** * Make manual journal. - * @param {Request} req - * @param {Response} res - * @param {NextFunction} next + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next */ async makeJournalEntries(req: Request, res: Response, next: NextFunction) { const { tenantId, user } = req; const manualJournalDTO = this.matchedBodyData(req); try { - const { manualJournal } = await this.manualJournalsService - .makeJournalEntries(tenantId, manualJournalDTO, user); + const { + manualJournal, + } = await this.manualJournalsService.makeJournalEntries( + tenantId, + manualJournalDTO, + user + ); return res.status(200).send({ id: manualJournal.id, @@ -305,9 +202,9 @@ export default class ManualJournalsController extends BaseController { /** * Edit the given manual journal. - * @param {Request} req - * @param {Response} res - * @param {NextFunction} next + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next */ async editManualJournal(req: Request, res: Response, next: NextFunction) { const { tenantId, user } = req; @@ -315,11 +212,13 @@ export default class ManualJournalsController extends BaseController { const manualJournalDTO = this.matchedBodyData(req); try { - const { manualJournal } = await this.manualJournalsService.editJournalEntries( + const { + manualJournal, + } = await this.manualJournalsService.editJournalEntries( tenantId, manualJournalId, manualJournalDTO, - user, + user ); return res.status(200).send({ id: manualJournal.id, @@ -330,11 +229,139 @@ export default class ManualJournalsController extends BaseController { } } + /** + * Retrieve the given manual journal details. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + */ + async getManualJournal(req: Request, res: Response, next: NextFunction) { + const { tenantId } = req; + const { id: manualJournalId } = req.params; + + try { + const manualJournal = await this.manualJournalsService.getManualJournal( + tenantId, + manualJournalId + ); + return res.status(200).send({ manualJournal }); + } catch (error) { + next(error); + } + } + + /** + * Publish the given manual journal. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + */ + async publishManualJournal(req: Request, res: Response, next: NextFunction) { + const { tenantId } = req; + const { id: manualJournalId } = req.params; + + try { + await this.manualJournalsService.publishManualJournal( + tenantId, + manualJournalId + ); + + return res.status(200).send({ + id: manualJournalId, + message: 'The manual journal has been published successfully.', + }); + } catch (error) { + next(error); + } + } + + /** + * Publish the given manual journals in bulk. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + */ + async publishManualJournals(req: Request, res: Response, next: NextFunction) { + const { tenantId } = req; + const { ids: manualJournalsIds } = req.query; + + try { + const { + meta: { alreadyPublished, published, total }, + } = await this.manualJournalsService.publishManualJournals( + tenantId, + manualJournalsIds + ); + + return res.status(200).send({ + ids: manualJournalsIds, + message: 'The manual journals have been published successfully.', + meta: { alreadyPublished, published, total }, + }); + } catch (error) { + next(error); + } + } + + /** + * Delete the given manual journal. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + */ + async deleteManualJournal(req: Request, res: Response, next: NextFunction) { + const { tenantId, user } = req; + const { id: manualJournalId } = req.params; + + try { + await this.manualJournalsService.deleteManualJournal( + tenantId, + manualJournalId + ); + + return res.status(200).send({ + id: manualJournalId, + message: 'Manual journal has been deleted successfully.', + }); + } catch (error) { + next(error); + } + } + + /** + * Deletes manual journals in bulk. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + */ + async deleteBulkManualJournals( + req: Request, + res: Response, + next: NextFunction + ) { + const { tenantId } = req; + const { ids: manualJournalsIds } = req.query; + + try { + await this.manualJournalsService.deleteManualJournals( + tenantId, + manualJournalsIds + ); + + return res.status(200).send({ + ids: manualJournalsIds, + message: 'Manual journal have been delete successfully.', + }); + } catch (error) { + next(error); + } + } + /** * Retrieve manual journals list. - * @param {Request} req - * @param {Response} res - * @param {NextFunction} next + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next */ async getManualJournalsList(req: Request, res: Response, next: NextFunction) { const { tenantId } = req; @@ -345,7 +372,7 @@ export default class ManualJournalsController extends BaseController { page: 1, pageSize: 12, ...this.matchedQueryData(req), - } + }; if (filter.stringifiedFilterRoles) { filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles); } @@ -353,13 +380,13 @@ export default class ManualJournalsController extends BaseController { const { manualJournals, pagination, - filterMeta + filterMeta, } = await this.manualJournalsService.getManualJournals(tenantId, filter); return res.status(200).send({ manual_journals: manualJournals, pagination: this.transfromToResponse(pagination), - filter_meta: this.transfromToResponse(filterMeta), + filter_meta: this.transfromToResponse(filterMeta), }); } catch (error) { next(error); @@ -368,64 +395,69 @@ export default class ManualJournalsController extends BaseController { /** * Catches all service errors. - * @param error - * @param {Request} req - * @param {Response} res - * @param {NextFunction} next + * @param error + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next */ catchServiceErrors(error, req: Request, res: Response, next: NextFunction) { if (error instanceof ServiceError) { if (error.errorType === 'manual_journal_not_found') { - res.boom.badRequest( - 'Manual journal not found.', - { errors: [{ type: 'MANUAL.JOURNAL.NOT.FOUND', code: 100 }], } - ) + res.boom.badRequest('Manual journal not found.', { + errors: [{ type: 'MANUAL.JOURNAL.NOT.FOUND', code: 100 }], + }); } if (error.errorType === 'credit_debit_not_equal_zero') { return res.boom.badRequest( 'Credit and debit should not be equal zero.', - { errors: [{ type: 'CREDIT.DEBIT.SUMATION.SHOULD.NOT.EQUAL.ZERO', code: 200, }] } - ) + { + errors: [ + { + type: 'CREDIT.DEBIT.SUMATION.SHOULD.NOT.EQUAL.ZERO', + code: 200, + }, + ], + } + ); } if (error.errorType === 'credit_debit_not_equal') { - return res.boom.badRequest( - 'Credit and debit should be equal.', - { errors: [{ type: 'CREDIT.DEBIT.NOT.EQUALS', code: 300 }] } - ) + return res.boom.badRequest('Credit and debit should be equal.', { + errors: [{ type: 'CREDIT.DEBIT.NOT.EQUALS', code: 300 }], + }); } if (error.errorType === 'acccounts_ids_not_found') { return res.boom.badRequest( 'Journal entries some of accounts ids not exists.', { errors: [{ type: 'ACCOUNTS.IDS.NOT.FOUND', code: 400 }] } - ) + ); } if (error.errorType === 'journal_number_exists') { - return res.boom.badRequest( - 'Journal number should be unique.', - { errors: [{ type: 'JOURNAL.NUMBER.ALREADY.EXISTS', code: 500 }] }, - ); + return res.boom.badRequest('Journal number should be unique.', { + errors: [{ type: 'JOURNAL.NUMBER.ALREADY.EXISTS', code: 500 }], + }); } if (error.errorType === 'ENTRIES_SHOULD_ASSIGN_WITH_CONTACT') { - return res.boom.badRequest( - '', - { - errors: [ - { - type: 'ENTRIES_SHOULD_ASSIGN_WITH_CONTACT', - code: 600, - meta: this.transfromToResponse(error.payload), - } - ] - }, - ); + return res.boom.badRequest('', { + errors: [ + { + type: 'ENTRIES_SHOULD_ASSIGN_WITH_CONTACT', + code: 600, + meta: this.transfromToResponse(error.payload), + }, + ], + }); } if (error.errorType === 'contacts_not_found') { - return res.boom.badRequest( - '', - { errors: [{ type: 'CONTACTS_NOT_FOUND', code: 700 }] }, - ); + return res.boom.badRequest('', { + errors: [{ type: 'CONTACTS_NOT_FOUND', code: 700 }], + }); + } + if (error.errorType === 'MANUAL_JOURNAL_ALREADY_PUBLISHED') { + return res.boom.badRequest('', { + errors: [{ type: 'MANUAL_JOURNAL_ALREADY_PUBLISHED', code: 800 }], + }); } } next(error); } -} \ No newline at end of file +} diff --git a/server/src/database/migrations/20200105195823_create_manual_journals_table.js b/server/src/database/migrations/20200105195823_create_manual_journals_table.js index b4200c29a..426fa5536 100644 --- a/server/src/database/migrations/20200105195823_create_manual_journals_table.js +++ b/server/src/database/migrations/20200105195823_create_manual_journals_table.js @@ -7,8 +7,8 @@ exports.up = function(knex) { table.string('journal_type').index(); table.decimal('amount', 13, 3); table.date('date').index(); - table.boolean('status').defaultTo(false).index(); table.string('description'); + table.date('published_at').index(); table.string('attachment_file'); table.integer('user_id').unsigned().index(); table.timestamps(); diff --git a/server/src/database/migrations/20200105195825_create_manual_journals_entries_table.js b/server/src/database/migrations/20200105195825_create_manual_journals_entries_table.js new file mode 100644 index 000000000..b061f9fb5 --- /dev/null +++ b/server/src/database/migrations/20200105195825_create_manual_journals_entries_table.js @@ -0,0 +1,18 @@ + +exports.up = function(knex) { + return knex.schema.createTable('manual_journals_entries', (table) => { + table.increments(); + table.decimal('credit', 13, 3); + table.decimal('debit', 13, 3); + table.integer('index').unsigned(); + table.integer('account_id').unsigned().index().references('id').inTable('accounts'); + table.string('contact_type').nullable().index(); + table.integer('contact_id').unsigned().nullable().index(); + table.string('note'); + table.integer('manual_journal_id').unsigned().index().references('id').inTable('manual_journals'); + }).raw('ALTER TABLE `MANUAL_JOURNALS_ENTRIES` AUTO_INCREMENT = 1000'); +}; + +exports.down = function(knex) { + return knex.schema.dropTableIfExists('manual_journals_entries'); +}; diff --git a/server/src/interfaces/ManualJournal.ts b/server/src/interfaces/ManualJournal.ts index 4a861d47a..08b74b47c 100644 --- a/server/src/interfaces/ManualJournal.ts +++ b/server/src/interfaces/ManualJournal.ts @@ -1,53 +1,98 @@ -import { IDynamicListFilterDTO } from "./DynamicFilter"; -import { IJournalEntry } from "./Journal"; -import { ISystemUser } from "./User"; +import { IDynamicListFilterDTO } from './DynamicFilter'; +import { IJournalEntry } from './Journal'; +import { ISystemUser } from './User'; export interface IManualJournal { - id: number, - date: Date|string, - journalNumber: number, - journalType: string, - amount: number, - status: boolean, - description: string, - userId: number, - entries: IJournalEntry[], + id: number; + date: Date | string; + journalNumber: number; + journalType: string; + amount: number; + publishedAt: Date | null; + description: string; + userId: number; + entries: IManualJournalEntry[]; +} + +export interface IManualJournalEntry { + index: number; + credit: number; + debit: number; + accountId: number; + note: string; + contactId?: number; + contactType?: string; } export interface IManualJournalEntryDTO { - index: number, - credit: number, - debit: number, - accountId: number, - note?: string, - contactId?: number, - contactType?: string, + index: number; + credit: number; + debit: number; + accountId: number; + note: string; + contactId?: number; + contactType?: string; } export interface IManualJournalDTO { - date: Date, - journalNumber: number, - journalType: string, - reference?: string, - description?: string, - status?: string, - entries: IManualJournalEntryDTO[], - mediaIds: number[], + date: Date; + journalNumber: number; + journalType: string; + reference?: string; + description?: string; + publish?: boolean; + entries: IManualJournalEntryDTO[]; } export interface IManualJournalsFilter extends IDynamicListFilterDTO { - stringifiedFilterRoles?: string, - page: number, - pageSize: number, + stringifiedFilterRoles?: string; + page: number; + pageSize: number; } export interface IManualJournalsService { - makeJournalEntries(tenantId: number, manualJournalDTO: IManualJournalDTO, authorizedUser: ISystemUser): Promise<{ manualJournal: IManualJournal }>; - editJournalEntries(tenantId: number, manualJournalId: number, manualJournalDTO: IManualJournalDTO, authorizedUser): Promise<{ manualJournal: IManualJournal }>; - deleteManualJournal(tenantId: number, manualJournalId: number): Promise; - deleteManualJournals(tenantId: number, manualJournalsIds: number[]): Promise; - publishManualJournals(tenantId: number, manualJournalsIds: number[]): Promise; - publishManualJournal(tenantId: number, manualJournalId: number): Promise; + makeJournalEntries( + tenantId: number, + manualJournalDTO: IManualJournalDTO, + authorizedUser: ISystemUser + ): Promise<{ manualJournal: IManualJournal }>; - getManualJournals(tenantId: number, filter: IManualJournalsFilter): Promise<{ manualJournals: IManualJournal, pagination: IPaginationMeta, filterMeta: IFilterMeta }>; -} \ No newline at end of file + editJournalEntries( + tenantId: number, + manualJournalId: number, + manualJournalDTO: IManualJournalDTO, + authorizedUser + ): Promise<{ manualJournal: IManualJournal }>; + + deleteManualJournal(tenantId: number, manualJournalId: number): Promise; + + deleteManualJournals( + tenantId: number, + manualJournalsIds: number[] + ): Promise; + + publishManualJournals( + tenantId: number, + manualJournalsIds: number[] + ): Promise<{ + meta: { + alreadyPublished: number; + published: number; + total: number; + }; + }>; + + publishManualJournal( + tenantId: number, + manualJournalId: number + ): Promise; + + getManualJournals( + tenantId: number, + filter: IManualJournalsFilter + ): Promise<{ + manualJournals: IManualJournal; + pagination: IPaginationMeta; + filterMeta: IFilterMeta; + }>; +} diff --git a/server/src/loaders/tenantModels.ts b/server/src/loaders/tenantModels.ts index 887f919c7..872b2cad1 100644 --- a/server/src/loaders/tenantModels.ts +++ b/server/src/loaders/tenantModels.ts @@ -32,6 +32,7 @@ import Option from 'models/Option'; import InventoryCostLotTracker from 'models/InventoryCostLotTracker'; import InventoryTransaction from 'models/InventoryTransaction'; import ManualJournal from 'models/ManualJournal'; +import ManualJournalEntry from 'models/ManualJournalEntry'; import Media from 'models/Media'; import MediaLink from 'models/MediaLink'; @@ -45,6 +46,7 @@ export default (knex) => { ItemCategory, ItemEntry, ManualJournal, + ManualJournalEntry, Bill, BillPayment, BillPaymentEntry, diff --git a/server/src/models/ManualJournal.js b/server/src/models/ManualJournal.js index 5c470ba3c..899e37093 100644 --- a/server/src/models/ManualJournal.js +++ b/server/src/models/ManualJournal.js @@ -1,6 +1,5 @@ import { Model } from 'objection'; import TenantModel from 'models/TenantModel'; -import { AccountTransaction } from 'models'; export default class ManualJournal extends TenantModel { /** @@ -27,9 +26,18 @@ export default class ManualJournal extends TenantModel { static get relationMappings() { const Media = require('models/Media'); const AccountTransaction = require('models/AccountTransaction'); + const ManualJournalEntry = require('models/ManualJournalEntry'); return { entries: { + relation: Model.HasManyRelation, + modelClass: ManualJournalEntry.default, + join: { + from: 'manual_journals.id', + to: 'manual_journals_entries.manualJournalId', + }, + }, + transactions: { relation: Model.HasManyRelation, modelClass: AccountTransaction.default, join: { diff --git a/server/src/models/ManualJournalEntry.js b/server/src/models/ManualJournalEntry.js new file mode 100644 index 000000000..dbc59a6a3 --- /dev/null +++ b/server/src/models/ManualJournalEntry.js @@ -0,0 +1,18 @@ +import { Model } from 'objection'; +import TenantModel from 'models/TenantModel'; + +export default class ManualJournalEntry extends TenantModel { + /** + * Table name. + */ + static get tableName() { + return 'manual_journals_entries'; + } + + /** + * Model timestamps. + */ + get timestamps() { + return []; + } +} diff --git a/server/src/services/Accounting/JournalCommands.ts b/server/src/services/Accounting/JournalCommands.ts index 0ee0a92b2..cfdc96bbc 100644 --- a/server/src/services/Accounting/JournalCommands.ts +++ b/server/src/services/Accounting/JournalCommands.ts @@ -1,6 +1,6 @@ import { sumBy, chain } from 'lodash'; import moment from 'moment'; -import { IBill, ISystemUser } from 'interfaces'; +import { IBill, IManualJournalEntry, ISystemUser } from 'interfaces'; import JournalPoster from './JournalPoster'; import JournalEntry from './JournalEntry'; import { AccountTransaction } from 'models'; @@ -257,9 +257,9 @@ export default class JournalCommands { } /** - * - * @param {number|number[]} referenceId - * @param {string} referenceType + * Reverts the jouranl entries. + * @param {number|number[]} referenceId - Reference id. + * @param {string} referenceType - Reference type. */ async revertJournalEntries( referenceId: number | number[], @@ -286,15 +286,14 @@ export default class JournalCommands { */ async manualJournal( manualJournalObj: IManualJournal, - manualJournalId: number ) { - manualJournalObj.entries.forEach((entry) => { + manualJournalObj.entries.forEach((entry: IManualJournalEntry) => { const jouranlEntry = new JournalEntry({ debit: entry.debit, credit: entry.credit, - account: entry.account, + account: entry.accountId, referenceType: 'Journal', - referenceId: manualJournalId, + referenceId: manualJournalObj.id, contactType: entry.contactType, contactId: entry.contactId, note: entry.note, diff --git a/server/src/services/Expenses/ExpensesService.ts b/server/src/services/Expenses/ExpensesService.ts index 290f6f6ba..bf064a960 100644 --- a/server/src/services/Expenses/ExpensesService.ts +++ b/server/src/services/Expenses/ExpensesService.ts @@ -674,7 +674,7 @@ export default class ExpensesService implements IExpensesService { // Filters the published expenses. const publishedExpenses = this.getPublishedExpenses(oldExpenses); - // Mappes the published expenses to get id. + // Mappes the not-published expenses to get id. const notPublishedExpensesIds = map(notPublishedExpenses, 'id'); if (notPublishedExpensesIds.length > 0) { diff --git a/server/src/services/ManualJournals/ManualJournalsService.ts b/server/src/services/ManualJournals/ManualJournalsService.ts index e880e8509..bf0568064 100644 --- a/server/src/services/ManualJournals/ManualJournalsService.ts +++ b/server/src/services/ManualJournals/ManualJournalsService.ts @@ -1,4 +1,4 @@ -import { difference, sumBy, omit, groupBy } from 'lodash'; +import { difference, sumBy, omit, map } from 'lodash'; import { Service, Inject } from 'typedi'; import moment from 'moment'; import { ServiceError } from 'exceptions'; @@ -8,7 +8,6 @@ import { IManualJournalsFilter, ISystemUser, IManualJournal, - IManualJournalEntryDTO, IPaginationMeta, } from 'interfaces'; import TenancyService from 'services/Tenancy/TenancyService'; @@ -20,6 +19,7 @@ import { } from 'decorators/eventDispatcher'; import JournalPoster from 'services/Accounting/JournalPoster'; import JournalCommands from 'services/Accounting/JournalCommands'; +import JournalPosterService from 'services/Sales/JournalPosterService'; const ERRORS = { NOT_FOUND: 'manual_journal_not_found', @@ -29,11 +29,20 @@ const ERRORS = { JOURNAL_NUMBER_EXISTS: 'journal_number_exists', ENTRIES_SHOULD_ASSIGN_WITH_CONTACT: 'ENTRIES_SHOULD_ASSIGN_WITH_CONTACT', CONTACTS_NOT_FOUND: 'contacts_not_found', + MANUAL_JOURNAL_ALREADY_PUBLISHED: 'MANUAL_JOURNAL_ALREADY_PUBLISHED', }; const CONTACTS_CONFIG = [ - { accountBySlug: 'accounts-receivable', contactService: 'customer', assignRequired: false, }, - { accountBySlug: 'accounts-payable', contactService: 'vendor', assignRequired: true }, + { + accountBySlug: 'accounts-receivable', + contactService: 'customer', + assignRequired: false, + }, + { + accountBySlug: 'accounts-payable', + contactService: 'vendor', + assignRequired: true, + }, ]; @Service() @@ -44,6 +53,9 @@ export default class ManualJournalsService implements IManualJournalsService { @Inject() dynamicListService: DynamicListingService; + @Inject() + journalService: JournalPosterService; + @Inject('logger') logger: any; @@ -55,7 +67,7 @@ export default class ManualJournalsService implements IManualJournalsService { * @param {number} tenantId * @param {number} manualJournalId */ - private async validateManualJournalExistance( + private async getManualJournalOrThrowError( tenantId: number, manualJournalId: number ) { @@ -65,7 +77,9 @@ export default class ManualJournalsService implements IManualJournalsService { tenantId, manualJournalId, }); - const manualJournal = await ManualJournal.query().findById(manualJournalId); + const manualJournal = await ManualJournal.query() + .findById(manualJournalId) + .withGraphFetched('entries'); if (!manualJournal) { this.logger.warn('[manual_journal] not exists on the storage.', { @@ -74,24 +88,24 @@ export default class ManualJournalsService implements IManualJournalsService { }); throw new ServiceError(ERRORS.NOT_FOUND); } + return manualJournal; } /** * Validate manual journals existance. - * @param {number} tenantId - * @param {number[]} manualJournalsIds + * @param {number} tenantId - Tenant id. + * @param {number[]} manualJournalsIds - Manual jorunal ids. * @throws {ServiceError} */ - private async validateManualJournalsExistance( + private async getManualJournalsOrThrowError( tenantId: number, manualJournalsIds: number[] ) { const { ManualJournal } = this.tenancy.models(tenantId); - const manualJournals = await ManualJournal.query().whereIn( - 'id', - manualJournalsIds - ); + const manualJournals = await ManualJournal.query() + .whereIn('id', manualJournalsIds) + .withGraphFetched('entries'); const notFoundManualJournals = difference( manualJournalsIds, @@ -100,6 +114,7 @@ export default class ManualJournalsService implements IManualJournalsService { if (notFoundManualJournals.length > 0) { throw new ServiceError(ERRORS.NOT_FOUND); } + return manualJournals; } /** @@ -134,8 +149,8 @@ export default class ManualJournalsService implements IManualJournalsService { /** * Validate manual entries accounts existance on the storage. - * @param {number} tenantId - * @param {IManualJournalDTO} manualJournalDTO + * @param {number} tenantId - + * @param {IManualJournalDTO} manualJournalDTO - */ private async validateAccountsExistance( tenantId: number, @@ -183,7 +198,7 @@ export default class ManualJournalsService implements IManualJournalsService { } /** - * + * * @param {number} tenantId * @param {IManualJournalDTO} manualJournalDTO * @param {string} accountBySlug @@ -194,10 +209,12 @@ export default class ManualJournalsService implements IManualJournalsService { manualJournalDTO: IManualJournalDTO, accountBySlug: string, contactType: string, - contactRequired: boolean = true, + contactRequired: boolean = true ): Promise { const { accountRepository } = this.tenancy.repositories(tenantId); - const payableAccount = await accountRepository.findOne({ slug: accountBySlug }); + const payableAccount = await accountRepository.findOne({ + slug: accountBySlug, + }); const entriesHasNoVendorContact = manualJournalDTO.entries.filter( (e) => @@ -205,12 +222,12 @@ export default class ManualJournalsService implements IManualJournalsService { ((!e.contactId && contactRequired) || e.contactType !== contactType) ); if (entriesHasNoVendorContact.length > 0) { - const indexes = entriesHasNoVendorContact.map(e => e.index); + const indexes = entriesHasNoVendorContact.map((e) => e.index); throw new ServiceError(ERRORS.ENTRIES_SHOULD_ASSIGN_WITH_CONTACT, '', { contactType, accountBySlug, - indexes + indexes, }); } } @@ -222,8 +239,8 @@ export default class ManualJournalsService implements IManualJournalsService { */ private async dynamicValidateAccountsWithContactType( tenantId: number, - manualJournalDTO: IManualJournalDTO, - ): Promise{ + manualJournalDTO: IManualJournalDTO + ): Promise { return Promise.all( CONTACTS_CONFIG.map(({ accountBySlug, contactService, assignRequired }) => this.validateAccountsWithContactType( @@ -232,7 +249,7 @@ export default class ManualJournalsService implements IManualJournalsService { accountBySlug, contactService, assignRequired - ), + ) ) ); } @@ -244,23 +261,28 @@ export default class ManualJournalsService implements IManualJournalsService { */ private async validateContactsExistance( tenantId: number, - manualJournalDTO: IManualJournalDTO, + manualJournalDTO: IManualJournalDTO ) { const { contactRepository } = this.tenancy.repositories(tenantId); // Filters the entries that have contact only. - const entriesContactPairs = manualJournalDTO.entries - .filter((entry) => entry.contactId); + const entriesContactPairs = manualJournalDTO.entries.filter( + (entry) => entry.contactId + ); if (entriesContactPairs.length > 0) { - const entriesContactsIds = entriesContactPairs.map(entry => entry.contactId); + const entriesContactsIds = entriesContactPairs.map( + (entry) => entry.contactId + ); // Retrieve all stored contacts on the storage from contacts entries. const storedContacts = await contactRepository.findByIds( - entriesContactsIds, + entriesContactsIds ); // Converts the stored contacts to map with id as key and entry as value. - const storedContactsMap = new Map(storedContacts.map(contact => [contact.id, contact])); + const storedContactsMap = new Map( + storedContacts.map((contact) => [contact.id, contact]) + ); const notFoundContactsIds = []; entriesContactPairs.forEach((contactEntry) => { @@ -284,35 +306,49 @@ export default class ManualJournalsService implements IManualJournalsService { } /** - * Transform manual journal DTO to graphed model to save it. - * @param {IManualJournalDTO} manualJournalDTO + * Transform the new manual journal DTO to upsert graph operation. + * @param {IManualJournalDTO} manualJournalDTO - Manual jorunal DTO. * @param {ISystemUser} authorizedUser */ - private transformDTOToModel( + private transformNewDTOToModel( manualJournalDTO: IManualJournalDTO, - user: ISystemUser - ): IManualJournal { + authorizedUser: ISystemUser + ) { const amount = sumBy(manualJournalDTO.entries, 'credit') || 0; const date = moment(manualJournalDTO.date).format('YYYY-MM-DD'); return { - ...manualJournalDTO, + ...omit(manualJournalDTO, ['publish']), + ...(manualJournalDTO.publish + ? { publishedAt: moment().toMySqlDateTime() } + : {}), amount, date, - userId: user.id, - entries: this.transformDTOToEntriesModel(manualJournalDTO.entries), + userId: authorizedUser.id, }; } /** - * Transform DTO to model. - * @param {IManualJournalEntryDTO[]} entries + * Transform the edit manual journal DTO to upsert graph operation. + * @param {IManualJournalDTO} manualJournalDTO - Manual jorunal DTO. + * @param {IManualJournal} oldManualJournal */ - private transformDTOToEntriesModel(entries: IManualJournalEntryDTO[]) { - return entries.map((entry: IManualJournalEntryDTO) => ({ - ...omit(entry, ['accountId']), - account: entry.accountId, - })); + private transformEditDTOToModel( + manualJournalDTO: IManualJournalDTO, + oldManualJournal: IManualJournal + ) { + const amount = sumBy(manualJournalDTO.entries, 'credit') || 0; + const date = moment(manualJournalDTO.date).format('YYYY-MM-DD'); + + return { + id: oldManualJournal.id, + ...omit(manualJournalDTO, ['publish']), + ...(manualJournalDTO.publish && !oldManualJournal.publishedAt + ? { publishedAt: moment().toMySqlDateTime() } + : {}), + amount, + date, + }; } /** @@ -341,23 +377,26 @@ export default class ManualJournalsService implements IManualJournalsService { await this.validateManualJournalNoUnique(tenantId, manualJournalDTO); // Validate accounts with contact type from the given config. - await this.dynamicValidateAccountsWithContactType(tenantId, manualJournalDTO); - + await this.dynamicValidateAccountsWithContactType( + tenantId, + manualJournalDTO + ); this.logger.info( '[manual_journal] trying to save manual journal to the storage.', { tenantId, manualJournalDTO } ); - const manualJournalObj = this.transformDTOToModel( + const manualJournalObj = this.transformNewDTOToModel( manualJournalDTO, authorizedUser ); - const manualJournal = await ManualJournal.query().insertAndFetch({ - ...omit(manualJournalObj, ['entries']), + const manualJournal = await ManualJournal.query().upsertGraph({ + ...manualJournalObj, }); // Triggers `onManualJournalCreated` event. this.eventDispatcher.dispatch(events.manualJournals.onCreated, { tenantId, - manualJournal: { ...manualJournal, entries: manualJournalObj.entries }, + manualJournal, + manualJournalId: manualJournal.id, }); this.logger.info( '[manual_journal] the manual journal inserted successfully.', @@ -379,12 +418,17 @@ export default class ManualJournalsService implements IManualJournalsService { manualJournalId: number, manualJournalDTO: IManualJournalDTO, authorizedUser: ISystemUser - ): Promise<{ manualJournal: IManualJournal }> { + ): Promise<{ + manualJournal: IManualJournal; + oldManualJournal: IManualJournal; + }> { const { ManualJournal } = this.tenancy.models(tenantId); // Validates the manual journal existance on the storage. - await this.validateManualJournalExistance(tenantId, manualJournalId); - + const oldManualJournal = await this.getManualJournalOrThrowError( + tenantId, + manualJournalId + ); // Validates the total credit and debit to be equals. this.valdiateCreditDebitTotalEquals(manualJournalDTO); @@ -401,29 +445,30 @@ export default class ManualJournalsService implements IManualJournalsService { manualJournalId ); // Validate accounts with contact type from the given config. - await this.dynamicValidateAccountsWithContactType(tenantId, manualJournalDTO); - - const manualJournalObj = this.transformDTOToModel( - manualJournalDTO, - authorizedUser + await this.dynamicValidateAccountsWithContactType( + tenantId, + manualJournalDTO ); - - const storedManualJournal = await ManualJournal.query() - .where('id', manualJournalId) - .patch({ - ...omit(manualJournalObj, ['entries']), - }); - const manualJournal: IManualJournal = { + // Transform manual journal DTO to model. + const manualJournalObj = this.transformEditDTOToModel( + manualJournalDTO, + oldManualJournal + ); + await ManualJournal.query().upsertGraph({ ...manualJournalObj, - id: manualJournalId, - }; + }); + // Retrieve the given manual journal with associated entries after modifications. + const manualJournal = await ManualJournal.query() + .findById(manualJournalId) + .withGraphFetched('entries'); // Triggers `onManualJournalEdited` event. this.eventDispatcher.dispatch(events.manualJournals.onEdited, { tenantId, manualJournal, + oldManualJournal, }); - return { manualJournal }; + return { manualJournal, oldManualJournal }; } /** @@ -435,25 +480,40 @@ export default class ManualJournalsService implements IManualJournalsService { public async deleteManualJournal( tenantId: number, manualJournalId: number - ): Promise { - const { ManualJournal } = this.tenancy.models(tenantId); - await this.validateManualJournalExistance(tenantId, manualJournalId); + ): Promise<{ + oldManualJournal: IManualJournal; + }> { + const { ManualJournal, ManualJournalEntry } = this.tenancy.models(tenantId); + // Validate the manual journal exists on the storage. + const oldManualJournal = await this.getManualJournalOrThrowError( + tenantId, + manualJournalId + ); this.logger.info('[manual_journal] trying to delete the manual journal.', { tenantId, manualJournalId, }); + // Deletes the manual journal entries. + await ManualJournalEntry.query() + .where('manual_journal_id', manualJournalId) + .delete(); + + // Deletes the manual journal transaction. await ManualJournal.query().findById(manualJournalId).delete(); // Triggers `onManualJournalDeleted` event. this.eventDispatcher.dispatch(events.manualJournals.onDeleted, { tenantId, manualJournalId, + oldManualJournal, }); this.logger.info( '[manual_journal] the given manual journal deleted successfully.', { tenantId, manualJournalId } ); + + return { oldManualJournal }; } /** @@ -465,14 +525,26 @@ export default class ManualJournalsService implements IManualJournalsService { public async deleteManualJournals( tenantId: number, manualJournalsIds: number[] - ): Promise { - const { ManualJournal } = this.tenancy.models(tenantId); - await this.validateManualJournalsExistance(tenantId, manualJournalsIds); + ): Promise<{ + oldManualJournals: IManualJournal[] + }> { + const { ManualJournal, ManualJournalEntry } = this.tenancy.models(tenantId); + // Validate the manual journals exist on the storage. + const oldManualJournals = await this.getManualJournalsOrThrowError( + tenantId, + manualJournalsIds + ); this.logger.info('[manual_journal] trying to delete the manual journals.', { tenantId, manualJournalsIds, }); + // Deletes the manual journal entries. + await ManualJournalEntry.query() + .whereIn('manual_journal_id', manualJournalsIds) + .delete(); + + // Deletes the manual journal transaction. await ManualJournal.query().whereIn('id', manualJournalsIds).delete(); // Triggers `onManualJournalDeletedBulk` event. @@ -484,6 +556,7 @@ export default class ManualJournalsService implements IManualJournalsService { '[manual_journal] the given manual journals deleted successfully.', { tenantId, manualJournalsIds } ); + return { oldManualJournals }; } /** @@ -494,51 +567,130 @@ export default class ManualJournalsService implements IManualJournalsService { public async publishManualJournals( tenantId: number, manualJournalsIds: number[] - ): Promise { + ): Promise<{ + meta: { + alreadyPublished: number; + published: number; + total: number; + }; + }> { const { ManualJournal } = this.tenancy.models(tenantId); - await this.validateManualJournalsExistance(tenantId, manualJournalsIds); + + // Retrieve manual journals or throw service error. + const oldManualJournals = await this.getManualJournalsOrThrowError( + tenantId, + manualJournalsIds + ); + // Filters the not published journals. + const notPublishedJournals = this.getNonePublishedManualJournals( + oldManualJournals + ); + // Filters the published journals. + const publishedJournals = this.getPublishedManualJournals( + oldManualJournals + ); + // Mappes the not-published journals to get id. + const notPublishedJournalsIds = map(notPublishedJournals, 'id'); this.logger.info('[manual_journal] trying to publish the manual journal.', { tenantId, manualJournalsIds, }); - await ManualJournal.query() - .whereIn('id', manualJournalsIds) - .patch({ status: 1 }); + if (notPublishedJournals.length > 0) { + // Mark the given manual journals as published. + await ManualJournal.query().whereIn('id', notPublishedJournalsIds).patch({ + publishedAt: moment().toMySqlDateTime(), + }); + } // Triggers `onManualJournalPublishedBulk` event. this.eventDispatcher.dispatch(events.manualJournals.onPublishedBulk, { tenantId, manualJournalsIds, + oldManualJournals, }); this.logger.info( '[manual_journal] the given manula journal published successfully.', - { tenantId, manualJournalId } + { tenantId, manualJournalsIds } ); + + return { + meta: { + alreadyPublished: publishedJournals.length, + published: notPublishedJournals.length, + total: oldManualJournals.length, + }, + }; + } + + /** + * Validates expenses is not already published before. + * @param {IManualJournal} manualJournal + */ + private validateManualJournalIsNotPublished(manualJournal: IManualJournal) { + if (manualJournal.publishedAt) { + throw new ServiceError(ERRORS.MANUAL_JOURNAL_ALREADY_PUBLISHED); + } + } + + /** + * Filters the not published manual jorunals. + * @param {IManualJournal[]} manualJournal - Manual journal. + * @return {IManualJournal[]} + */ + public getNonePublishedManualJournals( + manualJournals: IManualJournal[] + ): IManualJournal[] { + return manualJournals.filter((manualJournal) => !manualJournal.publishedAt); + } + + /** + * Filters the published manual journals. + * @param {IManualJournal[]} manualJournal - Manual journal. + * @return {IManualJournal[]} + */ + public getPublishedManualJournals( + manualJournals: IManualJournal[] + ): IManualJournal[] { + return manualJournals.filter((expense) => expense.publishedAt); } /** * Publish the given manual journal. - * @param {number} tenantId - * @param {number} manualJournalId + * @param {number} tenantId - Tenant id. + * @param {number} manualJournalId - Manual journal id. */ public async publishManualJournal( tenantId: number, manualJournalId: number ): Promise { const { ManualJournal } = this.tenancy.models(tenantId); - await this.validateManualJournalExistance(tenantId, manualJournalId); + const oldManualJournal = await this.getManualJournalOrThrowError( + tenantId, + manualJournalId + ); this.logger.info('[manual_journal] trying to publish the manual journal.', { tenantId, manualJournalId, }); - await ManualJournal.query().findById(manualJournalId).patch({ status: 1 }); + this.validateManualJournalIsNotPublished(oldManualJournal); + + // Mark the given manual journal as published. + await ManualJournal.query().findById(manualJournalId).patch({ + publishedAt: moment().toMySqlDateTime(), + }); + // Retrieve the manual journal with enrties after modification. + const manualJournal = await ManualJournal.query() + .findById(manualJournalId) + .withGraphFetched('entries'); // Triggers `onManualJournalPublishedBulk` event. this.eventDispatcher.dispatch(events.manualJournals.onPublished, { tenantId, + manualJournal, manualJournalId, + oldManualJournal, }); this.logger.info( '[manual_journal] the given manula journal published successfully.', @@ -592,7 +744,7 @@ export default class ManualJournalsService implements IManualJournalsService { public async getManualJournal(tenantId: number, manualJournalId: number) { const { ManualJournal } = this.tenancy.models(tenantId); - await this.validateManualJournalExistance(tenantId, manualJournalId); + await this.getManualJournalOrThrowError(tenantId, manualJournalId); this.logger.info( '[manual_journals] trying to get specific manual journal.', @@ -601,11 +753,33 @@ export default class ManualJournalsService implements IManualJournalsService { const manualJournal = await ManualJournal.query() .findById(manualJournalId) .withGraphFetched('entries') + .withGraphFetched('transactions') .withGraphFetched('media'); return manualJournal; } + /** + * Reverts the manual journal journal entries. + * @param {number} tenantId + * @param {number|number[]} manualJournalId + * @return {Promise} + */ + public async revertJournalEntries( + tenantId: number, + manualJournalId: number | number[] + ): Promise { + this.logger.info('[manual_journal] trying to revert journal entries.', { + tenantId, + manualJournalId, + }); + return this.journalService.revertJournalTransactions( + tenantId, + manualJournalId, + 'Journal' + ); + } + /** * Write manual journal entries. * @param {number} tenantId @@ -615,26 +789,30 @@ export default class ManualJournalsService implements IManualJournalsService { */ public async writeJournalEntries( tenantId: number, - manualJournalId: number, - manualJournalObj?: IManualJournal | null, - override?: Boolean + manualJorunal: IManualJournal | IManualJournal[], + override: Boolean = false ) { const journal = new JournalPoster(tenantId); const journalCommands = new JournalCommands(journal); + const manualJournals = Array.isArray(manualJorunal) + ? manualJorunal + : [manualJorunal]; + const manualJournalsIds = map(manualJournals, 'id'); + if (override) { this.logger.info('[manual_journal] trying to revert journal entries.', { tenantId, - manualJournalId, + manualJorunal, }); - await journalCommands.revertJournalEntries(manualJournalId, 'Journal'); - } - if (manualJournalObj) { - journalCommands.manualJournal(manualJournalObj, manualJournalId); + await journalCommands.revertJournalEntries(manualJournalsIds, 'Journal'); } + manualJournals.forEach((manualJournal) => { + journalCommands.manualJournal(manualJournal); + }); this.logger.info('[manual_journal] trying to save journal entries.', { tenantId, - manualJournalId, + manualJorunal, }); await Promise.all([ journal.saveBalance(), @@ -643,7 +821,7 @@ export default class ManualJournalsService implements IManualJournalsService { ]); this.logger.info( '[manual_journal] the journal entries saved successfully.', - { tenantId, manualJournalId } + { tenantId, manualJournalId: manualJorunal.id } ); } } diff --git a/server/src/services/Sales/JournalPosterService.ts b/server/src/services/Sales/JournalPosterService.ts index 622f043e2..4b93d17cb 100644 --- a/server/src/services/Sales/JournalPosterService.ts +++ b/server/src/services/Sales/JournalPosterService.ts @@ -17,7 +17,7 @@ export default class JournalPosterService { */ async revertJournalTransactions( tenantId: number, - referenceId: number, + referenceId: number|number[], referenceType: string ) { const journal = new JournalPoster(tenantId); diff --git a/server/src/services/Sales/PaymentsReceives.ts b/server/src/services/Sales/PaymentsReceives.ts index fc54d90be..0cef3b14c 100644 --- a/server/src/services/Sales/PaymentsReceives.ts +++ b/server/src/services/Sales/PaymentsReceives.ts @@ -370,7 +370,6 @@ export default class PaymentReceiveService { tenantId, paymentReceiveId ); - // Validate payment receive number uniquiness. if (paymentReceiveDTO.paymentReceiveNo) { await this.validatePaymentReceiveNoExistance( @@ -391,21 +390,18 @@ export default class PaymentReceiveService { paymentReceiveId, paymentReceiveDTO.entries ); - // Validate payment receive invoices IDs existance and associated to the given customer id. await this.validateInvoicesIDsExistance( tenantId, oldPaymentReceive.customerId, paymentReceiveDTO.entries ); - // Validate invoice payment amount. await this.validateInvoicesPaymentsAmount( tenantId, paymentReceiveDTO.entries, oldPaymentReceive.entries ); - // Update the payment receive transaction. const paymentReceive = await PaymentReceive.query().upsertGraphAndFetch({ id: paymentReceiveId, @@ -669,8 +665,8 @@ export default class PaymentReceiveService { /** * Reverts the given payment receive journal entries. - * @param {number} tenantId - * @param {number} paymentReceiveId + * @param {number} tenantId - Tenant id. + * @param {number} paymentReceiveId - Payment receive id. */ async revertPaymentReceiveJournalEntries( tenantId: number, diff --git a/server/src/subscribers/manualJournals.ts b/server/src/subscribers/manualJournals.ts index 5010420ba..8a031f8d2 100644 --- a/server/src/subscribers/manualJournals.ts +++ b/server/src/subscribers/manualJournals.ts @@ -1,5 +1,5 @@ import { Inject, Container } from 'typedi'; -import { On, EventSubscriber } from "event-dispatch"; +import { On, EventSubscriber } from 'event-dispatch'; import events from 'subscribers/events'; import SettingsService from 'services/Settings/SettingsService'; import ManualJournalsService from 'services/ManualJournals/ManualJournalsService'; @@ -8,46 +8,104 @@ import ManualJournalsService from 'services/ManualJournals/ManualJournalsService export class ManualJournalSubscriber { logger: any; settingsService: SettingsService; + manualJournalsService: ManualJournalsService; constructor() { this.logger = Container.get('logger'); this.settingsService = Container.get(SettingsService); + this.manualJournalsService = Container.get(ManualJournalsService); } /** * Handle manual journal created event. - * @param {{ tenantId: number, manualJournal: IManualJournal }} */ @On(events.manualJournals.onCreated) - public async handleWriteJournalEntries({ tenantId, manualJournal }) { - const manualJournalsService = Container.get(ManualJournalsService); - - await manualJournalsService - .writeJournalEntries(tenantId, manualJournal.id, manualJournal); + public async handleWriteJournalEntriesOnCreated({ tenantId, manualJournal }) { + // Ingore writing manual journal journal entries in case was not published. + if (manualJournal.publishedAt) { + await this.manualJournalsService.writeJournalEntries( + tenantId, + manualJournal + ); + } } /** * Handle manual journal edited event. - * @param {{ tenantId: number, manualJournal: IManualJournal }} */ @On(events.manualJournals.onEdited) - public async handleRewriteJournalEntries({ tenantId, manualJournal }) { - const manualJournalsService = Container.get(ManualJournalsService); + public async handleRewriteJournalEntriesOnEdited({ + tenantId, + manualJournal, + oldManualJournal, + }) { + if (manualJournal.publishedAt) { + await this.manualJournalsService.writeJournalEntries( + tenantId, + manualJournal, + true + ); + } + } - await manualJournalsService - .writeJournalEntries(tenantId, manualJournal.id, manualJournal, true); + /** + * Handles writing journal entries once the manula journal publish. + */ + @On(events.manualJournals.onPublished) + public async handleWriteJournalEntriesOnPublished({ + tenantId, + manualJournal, + }) { + await this.manualJournalsService.writeJournalEntries( + tenantId, + manualJournal + ); } /** * Handle manual journal deleted event. - * @param {{ tenantId: number, manualJournalId: number }} */ @On(events.manualJournals.onDeleted) - public async handleRevertJournalEntries({ tenantId, manualJournalId, }) { - const manualJournalsService = Container.get(ManualJournalsService); + public async handleRevertJournalEntries({ + tenantId, + manualJournalId, + oldManualJournal, + }) { + await this.manualJournalsService.revertJournalEntries( + tenantId, + manualJournalId + ); + } - await manualJournalsService - .writeJournalEntries(tenantId, manualJournalId, null, true); + /** + * Handles the writing journal entries once the manual journals bulk published. + */ + @On(events.manualJournals.onPublishedBulk) + public async handleWritingJournalEntriesOnBulkPublish({ + tenantId, + oldManualJournals, + }) { + const notPublishedJournals = this.manualJournalsService.getNonePublishedManualJournals( + oldManualJournals + ); + await this.manualJournalsService.writeJournalEntries( + tenantId, + notPublishedJournals + ); + } + + /** + * Handles revert journal entries once manual journals bulk delete. + */ + @On(events.manualJournals.onDeletedBulk) + public async handleRevertJournalEntriesOnBulkDelete({ + tenantId, + manualJournalsIds, + }) { + await this.manualJournalsService.revertJournalEntries( + tenantId, + manualJournalsIds + ); } /** @@ -61,4 +119,4 @@ export class ManualJournalSubscriber { }; await this.settingsService.incrementNextNumber(tenantId, query); } -} \ No newline at end of file +}