fix: writing journal entries of manual journal.

This commit is contained in:
a.bouhuolia
2021-01-03 19:39:17 +02:00
parent a2284945f1
commit ccf4fa55d9
13 changed files with 715 additions and 361 deletions

View File

@@ -3,14 +3,13 @@ import { check, param, query } from 'express-validator';
import BaseController from 'api/controllers/BaseController'; import BaseController from 'api/controllers/BaseController';
import asyncMiddleware from 'api/middleware/asyncMiddleware'; import asyncMiddleware from 'api/middleware/asyncMiddleware';
import ManualJournalsService from 'services/ManualJournals/ManualJournalsService'; import ManualJournalsService from 'services/ManualJournals/ManualJournalsService';
import { Inject, Service } from "typedi"; import { Inject, Service } from 'typedi';
import { ServiceError } from 'exceptions'; import { ServiceError } from 'exceptions';
import DynamicListingService from 'services/DynamicListing/DynamicListService'; import DynamicListingService from 'services/DynamicListing/DynamicListService';
import { DATATYPES_LENGTH } from 'data/DataTypes'; import { DATATYPES_LENGTH } from 'data/DataTypes';
@Service() @Service()
export default class ManualJournalsController extends BaseController { export default class ManualJournalsController extends BaseController {
@Inject() @Inject()
manualJournalsService: ManualJournalsService; manualJournalsService: ManualJournalsService;
@@ -24,67 +23,59 @@ export default class ManualJournalsController extends BaseController {
const router = Router(); const router = Router();
router.get( router.get(
'/', [ '/',
...this.manualJournalsListSchema, [...this.manualJournalsListSchema],
],
this.validationResult, this.validationResult,
asyncMiddleware(this.getManualJournalsList.bind(this)), asyncMiddleware(this.getManualJournalsList.bind(this)),
this.dynamicListService.handlerErrorsToResponse, this.dynamicListService.handlerErrorsToResponse,
this.catchServiceErrors.bind(this), this.catchServiceErrors.bind(this)
); );
router.get( router.get(
'/:id', '/:id',
asyncMiddleware(this.getManualJournal.bind(this)), asyncMiddleware(this.getManualJournal.bind(this)),
this.catchServiceErrors.bind(this), this.catchServiceErrors.bind(this)
); );
router.post( router.post(
'/publish', [ '/publish',
...this.manualJournalIdsSchema, [...this.manualJournalIdsSchema],
],
this.validationResult, this.validationResult,
asyncMiddleware(this.publishManualJournals.bind(this)), asyncMiddleware(this.publishManualJournals.bind(this)),
this.catchServiceErrors.bind(this), this.catchServiceErrors.bind(this)
); );
router.post( router.post(
'/:id/publish', [ '/:id/publish',
...this.manualJournalParamSchema, [...this.manualJournalParamSchema],
],
this.validationResult, this.validationResult,
asyncMiddleware(this.publishManualJournal.bind(this)), asyncMiddleware(this.publishManualJournal.bind(this)),
this.catchServiceErrors.bind(this), this.catchServiceErrors.bind(this)
); );
router.post( router.post(
'/:id', [ '/:id',
...this.manualJournalValidationSchema, [...this.manualJournalValidationSchema, ...this.manualJournalParamSchema],
...this.manualJournalParamSchema,
],
this.validationResult, this.validationResult,
asyncMiddleware(this.editManualJournal.bind(this)), asyncMiddleware(this.editManualJournal.bind(this)),
this.catchServiceErrors.bind(this), this.catchServiceErrors.bind(this)
); );
router.delete( router.delete(
'/:id', [ '/:id',
...this.manualJournalParamSchema, [...this.manualJournalParamSchema],
],
this.validationResult, this.validationResult,
asyncMiddleware(this.deleteManualJournal.bind(this)), asyncMiddleware(this.deleteManualJournal.bind(this)),
this.catchServiceErrors.bind(this), this.catchServiceErrors.bind(this)
); );
router.delete( router.delete(
'/', [ '/',
...this.manualJournalIdsSchema, [...this.manualJournalIdsSchema],
],
this.validationResult, this.validationResult,
asyncMiddleware(this.deleteBulkManualJournals.bind(this)), asyncMiddleware(this.deleteBulkManualJournals.bind(this)),
this.catchServiceErrors.bind(this), this.catchServiceErrors.bind(this)
); );
router.post( router.post(
'/', [ '/',
...this.manualJournalValidationSchema, [...this.manualJournalValidationSchema],
],
this.validationResult, this.validationResult,
asyncMiddleware(this.makeJournalEntries.bind(this)), asyncMiddleware(this.makeJournalEntries.bind(this)),
this.catchServiceErrors.bind(this), this.catchServiceErrors.bind(this)
); );
return router; return router;
} }
@@ -93,9 +84,7 @@ export default class ManualJournalsController extends BaseController {
* Specific manual journal id param validation schema. * Specific manual journal id param validation schema.
*/ */
get manualJournalParamSchema() { get manualJournalParamSchema() {
return [ return [param('id').exists().isNumeric().toInt()];
param('id').exists().isNumeric().toInt()
];
} }
/** /**
@@ -105,7 +94,7 @@ export default class ManualJournalsController extends BaseController {
return [ return [
query('ids').isArray({ min: 1 }), query('ids').isArray({ min: 1 }),
query('ids.*').isNumeric().toInt(), query('ids.*').isNumeric().toInt(),
] ];
} }
/** /**
@@ -138,11 +127,12 @@ export default class ManualJournalsController extends BaseController {
.trim() .trim()
.escape() .escape()
.isLength({ max: DATATYPES_LENGTH.TEXT }), .isLength({ max: DATATYPES_LENGTH.TEXT }),
check('status').optional().isBoolean().toBoolean(), check('publish').optional().isBoolean().toBoolean(),
check('entries').isArray({ min: 2 }), check('entries').isArray({ min: 2 }),
check('entries.*.index') check('entries.*.index')
.exists() .exists()
.isInt({ max: DATATYPES_LENGTH.INT_10 }).toInt(), .isInt({ max: DATATYPES_LENGTH.INT_10 })
.toInt(),
check('entries.*.credit') check('entries.*.credit')
.optional({ nullable: true }) .optional({ nullable: true })
.isFloat({ min: 0, max: DATATYPES_LENGTH.DECIMAL_13_3 }) .isFloat({ min: 0, max: DATATYPES_LENGTH.DECIMAL_13_3 })
@@ -151,7 +141,9 @@ export default class ManualJournalsController extends BaseController {
.optional({ nullable: true }) .optional({ nullable: true })
.isFloat({ min: 0, max: DATATYPES_LENGTH.DECIMAL_13_3 }) .isFloat({ min: 0, max: DATATYPES_LENGTH.DECIMAL_13_3 })
.toFloat(), .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') check('entries.*.note')
.optional({ nullable: true }) .optional({ nullable: true })
.isString() .isString()
@@ -161,7 +153,7 @@ export default class ManualJournalsController extends BaseController {
.isInt({ max: DATATYPES_LENGTH.INT_10 }) .isInt({ max: DATATYPES_LENGTH.INT_10 })
.toInt(), .toInt(),
check('entries.*.contact_type').optional().isIn(['vendor', 'customer']), check('entries.*.contact_type').optional().isIn(['vendor', 'customer']),
] ];
} }
/** /**
@@ -180,106 +172,6 @@ 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. * Make manual journal.
* @param {Request} req * @param {Request} req
@@ -291,8 +183,13 @@ export default class ManualJournalsController extends BaseController {
const manualJournalDTO = this.matchedBodyData(req); const manualJournalDTO = this.matchedBodyData(req);
try { try {
const { manualJournal } = await this.manualJournalsService const {
.makeJournalEntries(tenantId, manualJournalDTO, user); manualJournal,
} = await this.manualJournalsService.makeJournalEntries(
tenantId,
manualJournalDTO,
user
);
return res.status(200).send({ return res.status(200).send({
id: manualJournal.id, id: manualJournal.id,
@@ -315,11 +212,13 @@ export default class ManualJournalsController extends BaseController {
const manualJournalDTO = this.matchedBodyData(req); const manualJournalDTO = this.matchedBodyData(req);
try { try {
const { manualJournal } = await this.manualJournalsService.editJournalEntries( const {
manualJournal,
} = await this.manualJournalsService.editJournalEntries(
tenantId, tenantId,
manualJournalId, manualJournalId,
manualJournalDTO, manualJournalDTO,
user, user
); );
return res.status(200).send({ return res.status(200).send({
id: manualJournal.id, id: manualJournal.id,
@@ -330,6 +229,134 @@ 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. * Retrieve manual journals list.
* @param {Request} req * @param {Request} req
@@ -345,7 +372,7 @@ export default class ManualJournalsController extends BaseController {
page: 1, page: 1,
pageSize: 12, pageSize: 12,
...this.matchedQueryData(req), ...this.matchedQueryData(req),
} };
if (filter.stringifiedFilterRoles) { if (filter.stringifiedFilterRoles) {
filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles); filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles);
} }
@@ -353,7 +380,7 @@ export default class ManualJournalsController extends BaseController {
const { const {
manualJournals, manualJournals,
pagination, pagination,
filterMeta filterMeta,
} = await this.manualJournalsService.getManualJournals(tenantId, filter); } = await this.manualJournalsService.getManualJournals(tenantId, filter);
return res.status(200).send({ return res.status(200).send({
@@ -376,54 +403,59 @@ export default class ManualJournalsController extends BaseController {
catchServiceErrors(error, req: Request, res: Response, next: NextFunction) { catchServiceErrors(error, req: Request, res: Response, next: NextFunction) {
if (error instanceof ServiceError) { if (error instanceof ServiceError) {
if (error.errorType === 'manual_journal_not_found') { if (error.errorType === 'manual_journal_not_found') {
res.boom.badRequest( res.boom.badRequest('Manual journal not found.', {
'Manual journal not found.', errors: [{ type: 'MANUAL.JOURNAL.NOT.FOUND', code: 100 }],
{ errors: [{ type: 'MANUAL.JOURNAL.NOT.FOUND', code: 100 }], } });
)
} }
if (error.errorType === 'credit_debit_not_equal_zero') { if (error.errorType === 'credit_debit_not_equal_zero') {
return res.boom.badRequest( return res.boom.badRequest(
'Credit and debit should not be equal zero.', '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') { if (error.errorType === 'credit_debit_not_equal') {
return res.boom.badRequest( return res.boom.badRequest('Credit and debit should be equal.', {
'Credit and debit should be equal.', errors: [{ type: 'CREDIT.DEBIT.NOT.EQUALS', code: 300 }],
{ errors: [{ type: 'CREDIT.DEBIT.NOT.EQUALS', code: 300 }] } });
)
} }
if (error.errorType === 'acccounts_ids_not_found') { if (error.errorType === 'acccounts_ids_not_found') {
return res.boom.badRequest( return res.boom.badRequest(
'Journal entries some of accounts ids not exists.', 'Journal entries some of accounts ids not exists.',
{ errors: [{ type: 'ACCOUNTS.IDS.NOT.FOUND', code: 400 }] } { errors: [{ type: 'ACCOUNTS.IDS.NOT.FOUND', code: 400 }] }
) );
} }
if (error.errorType === 'journal_number_exists') { if (error.errorType === 'journal_number_exists') {
return res.boom.badRequest( return res.boom.badRequest('Journal number should be unique.', {
'Journal number should be unique.', errors: [{ type: 'JOURNAL.NUMBER.ALREADY.EXISTS', code: 500 }],
{ errors: [{ type: 'JOURNAL.NUMBER.ALREADY.EXISTS', code: 500 }] }, });
);
} }
if (error.errorType === 'ENTRIES_SHOULD_ASSIGN_WITH_CONTACT') { if (error.errorType === 'ENTRIES_SHOULD_ASSIGN_WITH_CONTACT') {
return res.boom.badRequest( return res.boom.badRequest('', {
'', errors: [
{ {
errors: [ type: 'ENTRIES_SHOULD_ASSIGN_WITH_CONTACT',
{ code: 600,
type: 'ENTRIES_SHOULD_ASSIGN_WITH_CONTACT', meta: this.transfromToResponse(error.payload),
code: 600, },
meta: this.transfromToResponse(error.payload), ],
} });
]
},
);
} }
if (error.errorType === 'contacts_not_found') { if (error.errorType === 'contacts_not_found') {
return res.boom.badRequest( return res.boom.badRequest('', {
'', errors: [{ type: 'CONTACTS_NOT_FOUND', code: 700 }],
{ 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); next(error);

View File

@@ -7,8 +7,8 @@ exports.up = function(knex) {
table.string('journal_type').index(); table.string('journal_type').index();
table.decimal('amount', 13, 3); table.decimal('amount', 13, 3);
table.date('date').index(); table.date('date').index();
table.boolean('status').defaultTo(false).index();
table.string('description'); table.string('description');
table.date('published_at').index();
table.string('attachment_file'); table.string('attachment_file');
table.integer('user_id').unsigned().index(); table.integer('user_id').unsigned().index();
table.timestamps(); table.timestamps();

View File

@@ -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');
};

View File

@@ -1,53 +1,98 @@
import { IDynamicListFilterDTO } from "./DynamicFilter"; import { IDynamicListFilterDTO } from './DynamicFilter';
import { IJournalEntry } from "./Journal"; import { IJournalEntry } from './Journal';
import { ISystemUser } from "./User"; import { ISystemUser } from './User';
export interface IManualJournal { export interface IManualJournal {
id: number, id: number;
date: Date|string, date: Date | string;
journalNumber: number, journalNumber: number;
journalType: string, journalType: string;
amount: number, amount: number;
status: boolean, publishedAt: Date | null;
description: string, description: string;
userId: number, userId: number;
entries: IJournalEntry[], entries: IManualJournalEntry[];
}
export interface IManualJournalEntry {
index: number;
credit: number;
debit: number;
accountId: number;
note: string;
contactId?: number;
contactType?: string;
} }
export interface IManualJournalEntryDTO { export interface IManualJournalEntryDTO {
index: number, index: number;
credit: number, credit: number;
debit: number, debit: number;
accountId: number, accountId: number;
note?: string, note: string;
contactId?: number, contactId?: number;
contactType?: string, contactType?: string;
} }
export interface IManualJournalDTO { export interface IManualJournalDTO {
date: Date, date: Date;
journalNumber: number, journalNumber: number;
journalType: string, journalType: string;
reference?: string, reference?: string;
description?: string, description?: string;
status?: string, publish?: boolean;
entries: IManualJournalEntryDTO[], entries: IManualJournalEntryDTO[];
mediaIds: number[],
} }
export interface IManualJournalsFilter extends IDynamicListFilterDTO { export interface IManualJournalsFilter extends IDynamicListFilterDTO {
stringifiedFilterRoles?: string, stringifiedFilterRoles?: string;
page: number, page: number;
pageSize: number, pageSize: number;
} }
export interface IManualJournalsService { export interface IManualJournalsService {
makeJournalEntries(tenantId: number, manualJournalDTO: IManualJournalDTO, authorizedUser: ISystemUser): Promise<{ manualJournal: IManualJournal }>; makeJournalEntries(
editJournalEntries(tenantId: number, manualJournalId: number, manualJournalDTO: IManualJournalDTO, authorizedUser): Promise<{ manualJournal: IManualJournal }>; tenantId: number,
deleteManualJournal(tenantId: number, manualJournalId: number): Promise<void>; manualJournalDTO: IManualJournalDTO,
deleteManualJournals(tenantId: number, manualJournalsIds: number[]): Promise<void>; authorizedUser: ISystemUser
publishManualJournals(tenantId: number, manualJournalsIds: number[]): Promise<void>; ): Promise<{ manualJournal: IManualJournal }>;
publishManualJournal(tenantId: number, manualJournalId: number): Promise<void>;
getManualJournals(tenantId: number, filter: IManualJournalsFilter): Promise<{ manualJournals: IManualJournal, pagination: IPaginationMeta, filterMeta: IFilterMeta }>; editJournalEntries(
tenantId: number,
manualJournalId: number,
manualJournalDTO: IManualJournalDTO,
authorizedUser
): Promise<{ manualJournal: IManualJournal }>;
deleteManualJournal(tenantId: number, manualJournalId: number): Promise<void>;
deleteManualJournals(
tenantId: number,
manualJournalsIds: number[]
): Promise<void>;
publishManualJournals(
tenantId: number,
manualJournalsIds: number[]
): Promise<{
meta: {
alreadyPublished: number;
published: number;
total: number;
};
}>;
publishManualJournal(
tenantId: number,
manualJournalId: number
): Promise<void>;
getManualJournals(
tenantId: number,
filter: IManualJournalsFilter
): Promise<{
manualJournals: IManualJournal;
pagination: IPaginationMeta;
filterMeta: IFilterMeta;
}>;
} }

View File

@@ -32,6 +32,7 @@ import Option from 'models/Option';
import InventoryCostLotTracker from 'models/InventoryCostLotTracker'; import InventoryCostLotTracker from 'models/InventoryCostLotTracker';
import InventoryTransaction from 'models/InventoryTransaction'; import InventoryTransaction from 'models/InventoryTransaction';
import ManualJournal from 'models/ManualJournal'; import ManualJournal from 'models/ManualJournal';
import ManualJournalEntry from 'models/ManualJournalEntry';
import Media from 'models/Media'; import Media from 'models/Media';
import MediaLink from 'models/MediaLink'; import MediaLink from 'models/MediaLink';
@@ -45,6 +46,7 @@ export default (knex) => {
ItemCategory, ItemCategory,
ItemEntry, ItemEntry,
ManualJournal, ManualJournal,
ManualJournalEntry,
Bill, Bill,
BillPayment, BillPayment,
BillPaymentEntry, BillPaymentEntry,

View File

@@ -1,6 +1,5 @@
import { Model } from 'objection'; import { Model } from 'objection';
import TenantModel from 'models/TenantModel'; import TenantModel from 'models/TenantModel';
import { AccountTransaction } from 'models';
export default class ManualJournal extends TenantModel { export default class ManualJournal extends TenantModel {
/** /**
@@ -27,9 +26,18 @@ export default class ManualJournal extends TenantModel {
static get relationMappings() { static get relationMappings() {
const Media = require('models/Media'); const Media = require('models/Media');
const AccountTransaction = require('models/AccountTransaction'); const AccountTransaction = require('models/AccountTransaction');
const ManualJournalEntry = require('models/ManualJournalEntry');
return { return {
entries: { entries: {
relation: Model.HasManyRelation,
modelClass: ManualJournalEntry.default,
join: {
from: 'manual_journals.id',
to: 'manual_journals_entries.manualJournalId',
},
},
transactions: {
relation: Model.HasManyRelation, relation: Model.HasManyRelation,
modelClass: AccountTransaction.default, modelClass: AccountTransaction.default,
join: { join: {

View File

@@ -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 [];
}
}

View File

@@ -1,6 +1,6 @@
import { sumBy, chain } from 'lodash'; import { sumBy, chain } from 'lodash';
import moment from 'moment'; import moment from 'moment';
import { IBill, ISystemUser } from 'interfaces'; import { IBill, IManualJournalEntry, ISystemUser } from 'interfaces';
import JournalPoster from './JournalPoster'; import JournalPoster from './JournalPoster';
import JournalEntry from './JournalEntry'; import JournalEntry from './JournalEntry';
import { AccountTransaction } from 'models'; import { AccountTransaction } from 'models';
@@ -257,9 +257,9 @@ export default class JournalCommands {
} }
/** /**
* * Reverts the jouranl entries.
* @param {number|number[]} referenceId * @param {number|number[]} referenceId - Reference id.
* @param {string} referenceType * @param {string} referenceType - Reference type.
*/ */
async revertJournalEntries( async revertJournalEntries(
referenceId: number | number[], referenceId: number | number[],
@@ -286,15 +286,14 @@ export default class JournalCommands {
*/ */
async manualJournal( async manualJournal(
manualJournalObj: IManualJournal, manualJournalObj: IManualJournal,
manualJournalId: number
) { ) {
manualJournalObj.entries.forEach((entry) => { manualJournalObj.entries.forEach((entry: IManualJournalEntry) => {
const jouranlEntry = new JournalEntry({ const jouranlEntry = new JournalEntry({
debit: entry.debit, debit: entry.debit,
credit: entry.credit, credit: entry.credit,
account: entry.account, account: entry.accountId,
referenceType: 'Journal', referenceType: 'Journal',
referenceId: manualJournalId, referenceId: manualJournalObj.id,
contactType: entry.contactType, contactType: entry.contactType,
contactId: entry.contactId, contactId: entry.contactId,
note: entry.note, note: entry.note,

View File

@@ -674,7 +674,7 @@ export default class ExpensesService implements IExpensesService {
// Filters the published expenses. // Filters the published expenses.
const publishedExpenses = this.getPublishedExpenses(oldExpenses); 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'); const notPublishedExpensesIds = map(notPublishedExpenses, 'id');
if (notPublishedExpensesIds.length > 0) { if (notPublishedExpensesIds.length > 0) {

View File

@@ -1,4 +1,4 @@
import { difference, sumBy, omit, groupBy } from 'lodash'; import { difference, sumBy, omit, map } from 'lodash';
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import moment from 'moment'; import moment from 'moment';
import { ServiceError } from 'exceptions'; import { ServiceError } from 'exceptions';
@@ -8,7 +8,6 @@ import {
IManualJournalsFilter, IManualJournalsFilter,
ISystemUser, ISystemUser,
IManualJournal, IManualJournal,
IManualJournalEntryDTO,
IPaginationMeta, IPaginationMeta,
} from 'interfaces'; } from 'interfaces';
import TenancyService from 'services/Tenancy/TenancyService'; import TenancyService from 'services/Tenancy/TenancyService';
@@ -20,6 +19,7 @@ import {
} from 'decorators/eventDispatcher'; } from 'decorators/eventDispatcher';
import JournalPoster from 'services/Accounting/JournalPoster'; import JournalPoster from 'services/Accounting/JournalPoster';
import JournalCommands from 'services/Accounting/JournalCommands'; import JournalCommands from 'services/Accounting/JournalCommands';
import JournalPosterService from 'services/Sales/JournalPosterService';
const ERRORS = { const ERRORS = {
NOT_FOUND: 'manual_journal_not_found', NOT_FOUND: 'manual_journal_not_found',
@@ -29,11 +29,20 @@ const ERRORS = {
JOURNAL_NUMBER_EXISTS: 'journal_number_exists', JOURNAL_NUMBER_EXISTS: 'journal_number_exists',
ENTRIES_SHOULD_ASSIGN_WITH_CONTACT: 'ENTRIES_SHOULD_ASSIGN_WITH_CONTACT', ENTRIES_SHOULD_ASSIGN_WITH_CONTACT: 'ENTRIES_SHOULD_ASSIGN_WITH_CONTACT',
CONTACTS_NOT_FOUND: 'contacts_not_found', CONTACTS_NOT_FOUND: 'contacts_not_found',
MANUAL_JOURNAL_ALREADY_PUBLISHED: 'MANUAL_JOURNAL_ALREADY_PUBLISHED',
}; };
const CONTACTS_CONFIG = [ 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() @Service()
@@ -44,6 +53,9 @@ export default class ManualJournalsService implements IManualJournalsService {
@Inject() @Inject()
dynamicListService: DynamicListingService; dynamicListService: DynamicListingService;
@Inject()
journalService: JournalPosterService;
@Inject('logger') @Inject('logger')
logger: any; logger: any;
@@ -55,7 +67,7 @@ export default class ManualJournalsService implements IManualJournalsService {
* @param {number} tenantId * @param {number} tenantId
* @param {number} manualJournalId * @param {number} manualJournalId
*/ */
private async validateManualJournalExistance( private async getManualJournalOrThrowError(
tenantId: number, tenantId: number,
manualJournalId: number manualJournalId: number
) { ) {
@@ -65,7 +77,9 @@ export default class ManualJournalsService implements IManualJournalsService {
tenantId, tenantId,
manualJournalId, manualJournalId,
}); });
const manualJournal = await ManualJournal.query().findById(manualJournalId); const manualJournal = await ManualJournal.query()
.findById(manualJournalId)
.withGraphFetched('entries');
if (!manualJournal) { if (!manualJournal) {
this.logger.warn('[manual_journal] not exists on the storage.', { 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); throw new ServiceError(ERRORS.NOT_FOUND);
} }
return manualJournal;
} }
/** /**
* Validate manual journals existance. * Validate manual journals existance.
* @param {number} tenantId * @param {number} tenantId - Tenant id.
* @param {number[]} manualJournalsIds * @param {number[]} manualJournalsIds - Manual jorunal ids.
* @throws {ServiceError} * @throws {ServiceError}
*/ */
private async validateManualJournalsExistance( private async getManualJournalsOrThrowError(
tenantId: number, tenantId: number,
manualJournalsIds: number[] manualJournalsIds: number[]
) { ) {
const { ManualJournal } = this.tenancy.models(tenantId); const { ManualJournal } = this.tenancy.models(tenantId);
const manualJournals = await ManualJournal.query().whereIn( const manualJournals = await ManualJournal.query()
'id', .whereIn('id', manualJournalsIds)
manualJournalsIds .withGraphFetched('entries');
);
const notFoundManualJournals = difference( const notFoundManualJournals = difference(
manualJournalsIds, manualJournalsIds,
@@ -100,6 +114,7 @@ export default class ManualJournalsService implements IManualJournalsService {
if (notFoundManualJournals.length > 0) { if (notFoundManualJournals.length > 0) {
throw new ServiceError(ERRORS.NOT_FOUND); 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. * Validate manual entries accounts existance on the storage.
* @param {number} tenantId * @param {number} tenantId -
* @param {IManualJournalDTO} manualJournalDTO * @param {IManualJournalDTO} manualJournalDTO -
*/ */
private async validateAccountsExistance( private async validateAccountsExistance(
tenantId: number, tenantId: number,
@@ -194,10 +209,12 @@ export default class ManualJournalsService implements IManualJournalsService {
manualJournalDTO: IManualJournalDTO, manualJournalDTO: IManualJournalDTO,
accountBySlug: string, accountBySlug: string,
contactType: string, contactType: string,
contactRequired: boolean = true, contactRequired: boolean = true
): Promise<void> { ): Promise<void> {
const { accountRepository } = this.tenancy.repositories(tenantId); 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( const entriesHasNoVendorContact = manualJournalDTO.entries.filter(
(e) => (e) =>
@@ -205,12 +222,12 @@ export default class ManualJournalsService implements IManualJournalsService {
((!e.contactId && contactRequired) || e.contactType !== contactType) ((!e.contactId && contactRequired) || e.contactType !== contactType)
); );
if (entriesHasNoVendorContact.length > 0) { 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, '', { throw new ServiceError(ERRORS.ENTRIES_SHOULD_ASSIGN_WITH_CONTACT, '', {
contactType, contactType,
accountBySlug, accountBySlug,
indexes indexes,
}); });
} }
} }
@@ -222,8 +239,8 @@ export default class ManualJournalsService implements IManualJournalsService {
*/ */
private async dynamicValidateAccountsWithContactType( private async dynamicValidateAccountsWithContactType(
tenantId: number, tenantId: number,
manualJournalDTO: IManualJournalDTO, manualJournalDTO: IManualJournalDTO
): Promise<any>{ ): Promise<any> {
return Promise.all( return Promise.all(
CONTACTS_CONFIG.map(({ accountBySlug, contactService, assignRequired }) => CONTACTS_CONFIG.map(({ accountBySlug, contactService, assignRequired }) =>
this.validateAccountsWithContactType( this.validateAccountsWithContactType(
@@ -232,7 +249,7 @@ export default class ManualJournalsService implements IManualJournalsService {
accountBySlug, accountBySlug,
contactService, contactService,
assignRequired assignRequired
), )
) )
); );
} }
@@ -244,23 +261,28 @@ export default class ManualJournalsService implements IManualJournalsService {
*/ */
private async validateContactsExistance( private async validateContactsExistance(
tenantId: number, tenantId: number,
manualJournalDTO: IManualJournalDTO, manualJournalDTO: IManualJournalDTO
) { ) {
const { contactRepository } = this.tenancy.repositories(tenantId); const { contactRepository } = this.tenancy.repositories(tenantId);
// Filters the entries that have contact only. // Filters the entries that have contact only.
const entriesContactPairs = manualJournalDTO.entries const entriesContactPairs = manualJournalDTO.entries.filter(
.filter((entry) => entry.contactId); (entry) => entry.contactId
);
if (entriesContactPairs.length > 0) { 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. // Retrieve all stored contacts on the storage from contacts entries.
const storedContacts = await contactRepository.findByIds( const storedContacts = await contactRepository.findByIds(
entriesContactsIds, entriesContactsIds
); );
// Converts the stored contacts to map with id as key and entry as value. // 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 = []; const notFoundContactsIds = [];
entriesContactPairs.forEach((contactEntry) => { entriesContactPairs.forEach((contactEntry) => {
@@ -284,35 +306,49 @@ export default class ManualJournalsService implements IManualJournalsService {
} }
/** /**
* Transform manual journal DTO to graphed model to save it. * Transform the new manual journal DTO to upsert graph operation.
* @param {IManualJournalDTO} manualJournalDTO * @param {IManualJournalDTO} manualJournalDTO - Manual jorunal DTO.
* @param {ISystemUser} authorizedUser * @param {ISystemUser} authorizedUser
*/ */
private transformDTOToModel( private transformNewDTOToModel(
manualJournalDTO: IManualJournalDTO, manualJournalDTO: IManualJournalDTO,
user: ISystemUser authorizedUser: ISystemUser
): IManualJournal { ) {
const amount = sumBy(manualJournalDTO.entries, 'credit') || 0; const amount = sumBy(manualJournalDTO.entries, 'credit') || 0;
const date = moment(manualJournalDTO.date).format('YYYY-MM-DD'); const date = moment(manualJournalDTO.date).format('YYYY-MM-DD');
return { return {
...manualJournalDTO, ...omit(manualJournalDTO, ['publish']),
...(manualJournalDTO.publish
? { publishedAt: moment().toMySqlDateTime() }
: {}),
amount, amount,
date, date,
userId: user.id, userId: authorizedUser.id,
entries: this.transformDTOToEntriesModel(manualJournalDTO.entries),
}; };
} }
/** /**
* Transform DTO to model. * Transform the edit manual journal DTO to upsert graph operation.
* @param {IManualJournalEntryDTO[]} entries * @param {IManualJournalDTO} manualJournalDTO - Manual jorunal DTO.
* @param {IManualJournal} oldManualJournal
*/ */
private transformDTOToEntriesModel(entries: IManualJournalEntryDTO[]) { private transformEditDTOToModel(
return entries.map((entry: IManualJournalEntryDTO) => ({ manualJournalDTO: IManualJournalDTO,
...omit(entry, ['accountId']), oldManualJournal: IManualJournal
account: entry.accountId, ) {
})); 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); await this.validateManualJournalNoUnique(tenantId, manualJournalDTO);
// Validate accounts with contact type from the given config. // Validate accounts with contact type from the given config.
await this.dynamicValidateAccountsWithContactType(tenantId, manualJournalDTO); await this.dynamicValidateAccountsWithContactType(
tenantId,
manualJournalDTO
);
this.logger.info( this.logger.info(
'[manual_journal] trying to save manual journal to the storage.', '[manual_journal] trying to save manual journal to the storage.',
{ tenantId, manualJournalDTO } { tenantId, manualJournalDTO }
); );
const manualJournalObj = this.transformDTOToModel( const manualJournalObj = this.transformNewDTOToModel(
manualJournalDTO, manualJournalDTO,
authorizedUser authorizedUser
); );
const manualJournal = await ManualJournal.query().insertAndFetch({ const manualJournal = await ManualJournal.query().upsertGraph({
...omit(manualJournalObj, ['entries']), ...manualJournalObj,
}); });
// Triggers `onManualJournalCreated` event. // Triggers `onManualJournalCreated` event.
this.eventDispatcher.dispatch(events.manualJournals.onCreated, { this.eventDispatcher.dispatch(events.manualJournals.onCreated, {
tenantId, tenantId,
manualJournal: { ...manualJournal, entries: manualJournalObj.entries }, manualJournal,
manualJournalId: manualJournal.id,
}); });
this.logger.info( this.logger.info(
'[manual_journal] the manual journal inserted successfully.', '[manual_journal] the manual journal inserted successfully.',
@@ -379,12 +418,17 @@ export default class ManualJournalsService implements IManualJournalsService {
manualJournalId: number, manualJournalId: number,
manualJournalDTO: IManualJournalDTO, manualJournalDTO: IManualJournalDTO,
authorizedUser: ISystemUser authorizedUser: ISystemUser
): Promise<{ manualJournal: IManualJournal }> { ): Promise<{
manualJournal: IManualJournal;
oldManualJournal: IManualJournal;
}> {
const { ManualJournal } = this.tenancy.models(tenantId); const { ManualJournal } = this.tenancy.models(tenantId);
// Validates the manual journal existance on the storage. // 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. // Validates the total credit and debit to be equals.
this.valdiateCreditDebitTotalEquals(manualJournalDTO); this.valdiateCreditDebitTotalEquals(manualJournalDTO);
@@ -401,29 +445,30 @@ export default class ManualJournalsService implements IManualJournalsService {
manualJournalId manualJournalId
); );
// Validate accounts with contact type from the given config. // Validate accounts with contact type from the given config.
await this.dynamicValidateAccountsWithContactType(tenantId, manualJournalDTO); await this.dynamicValidateAccountsWithContactType(
tenantId,
const manualJournalObj = this.transformDTOToModel( manualJournalDTO
manualJournalDTO,
authorizedUser
); );
// Transform manual journal DTO to model.
const storedManualJournal = await ManualJournal.query() const manualJournalObj = this.transformEditDTOToModel(
.where('id', manualJournalId) manualJournalDTO,
.patch({ oldManualJournal
...omit(manualJournalObj, ['entries']), );
}); await ManualJournal.query().upsertGraph({
const manualJournal: IManualJournal = {
...manualJournalObj, ...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. // Triggers `onManualJournalEdited` event.
this.eventDispatcher.dispatch(events.manualJournals.onEdited, { this.eventDispatcher.dispatch(events.manualJournals.onEdited, {
tenantId, tenantId,
manualJournal, manualJournal,
oldManualJournal,
}); });
return { manualJournal }; return { manualJournal, oldManualJournal };
} }
/** /**
@@ -435,25 +480,40 @@ export default class ManualJournalsService implements IManualJournalsService {
public async deleteManualJournal( public async deleteManualJournal(
tenantId: number, tenantId: number,
manualJournalId: number manualJournalId: number
): Promise<void> { ): Promise<{
const { ManualJournal } = this.tenancy.models(tenantId); oldManualJournal: IManualJournal;
await this.validateManualJournalExistance(tenantId, manualJournalId); }> {
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.', { this.logger.info('[manual_journal] trying to delete the manual journal.', {
tenantId, tenantId,
manualJournalId, 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(); await ManualJournal.query().findById(manualJournalId).delete();
// Triggers `onManualJournalDeleted` event. // Triggers `onManualJournalDeleted` event.
this.eventDispatcher.dispatch(events.manualJournals.onDeleted, { this.eventDispatcher.dispatch(events.manualJournals.onDeleted, {
tenantId, tenantId,
manualJournalId, manualJournalId,
oldManualJournal,
}); });
this.logger.info( this.logger.info(
'[manual_journal] the given manual journal deleted successfully.', '[manual_journal] the given manual journal deleted successfully.',
{ tenantId, manualJournalId } { tenantId, manualJournalId }
); );
return { oldManualJournal };
} }
/** /**
@@ -465,14 +525,26 @@ export default class ManualJournalsService implements IManualJournalsService {
public async deleteManualJournals( public async deleteManualJournals(
tenantId: number, tenantId: number,
manualJournalsIds: number[] manualJournalsIds: number[]
): Promise<void> { ): Promise<{
const { ManualJournal } = this.tenancy.models(tenantId); oldManualJournals: IManualJournal[]
await this.validateManualJournalsExistance(tenantId, manualJournalsIds); }> {
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.', { this.logger.info('[manual_journal] trying to delete the manual journals.', {
tenantId, tenantId,
manualJournalsIds, 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(); await ManualJournal.query().whereIn('id', manualJournalsIds).delete();
// Triggers `onManualJournalDeletedBulk` event. // Triggers `onManualJournalDeletedBulk` event.
@@ -484,6 +556,7 @@ export default class ManualJournalsService implements IManualJournalsService {
'[manual_journal] the given manual journals deleted successfully.', '[manual_journal] the given manual journals deleted successfully.',
{ tenantId, manualJournalsIds } { tenantId, manualJournalsIds }
); );
return { oldManualJournals };
} }
/** /**
@@ -494,51 +567,130 @@ export default class ManualJournalsService implements IManualJournalsService {
public async publishManualJournals( public async publishManualJournals(
tenantId: number, tenantId: number,
manualJournalsIds: number[] manualJournalsIds: number[]
): Promise<void> { ): Promise<{
meta: {
alreadyPublished: number;
published: number;
total: number;
};
}> {
const { ManualJournal } = this.tenancy.models(tenantId); 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.', { this.logger.info('[manual_journal] trying to publish the manual journal.', {
tenantId, tenantId,
manualJournalsIds, 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. // Triggers `onManualJournalPublishedBulk` event.
this.eventDispatcher.dispatch(events.manualJournals.onPublishedBulk, { this.eventDispatcher.dispatch(events.manualJournals.onPublishedBulk, {
tenantId, tenantId,
manualJournalsIds, manualJournalsIds,
oldManualJournals,
}); });
this.logger.info( this.logger.info(
'[manual_journal] the given manula journal published successfully.', '[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. * Publish the given manual journal.
* @param {number} tenantId * @param {number} tenantId - Tenant id.
* @param {number} manualJournalId * @param {number} manualJournalId - Manual journal id.
*/ */
public async publishManualJournal( public async publishManualJournal(
tenantId: number, tenantId: number,
manualJournalId: number manualJournalId: number
): Promise<void> { ): Promise<void> {
const { ManualJournal } = this.tenancy.models(tenantId); 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.', { this.logger.info('[manual_journal] trying to publish the manual journal.', {
tenantId, tenantId,
manualJournalId, 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. // Triggers `onManualJournalPublishedBulk` event.
this.eventDispatcher.dispatch(events.manualJournals.onPublished, { this.eventDispatcher.dispatch(events.manualJournals.onPublished, {
tenantId, tenantId,
manualJournal,
manualJournalId, manualJournalId,
oldManualJournal,
}); });
this.logger.info( this.logger.info(
'[manual_journal] the given manula journal published successfully.', '[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) { public async getManualJournal(tenantId: number, manualJournalId: number) {
const { ManualJournal } = this.tenancy.models(tenantId); const { ManualJournal } = this.tenancy.models(tenantId);
await this.validateManualJournalExistance(tenantId, manualJournalId); await this.getManualJournalOrThrowError(tenantId, manualJournalId);
this.logger.info( this.logger.info(
'[manual_journals] trying to get specific manual journal.', '[manual_journals] trying to get specific manual journal.',
@@ -601,11 +753,33 @@ export default class ManualJournalsService implements IManualJournalsService {
const manualJournal = await ManualJournal.query() const manualJournal = await ManualJournal.query()
.findById(manualJournalId) .findById(manualJournalId)
.withGraphFetched('entries') .withGraphFetched('entries')
.withGraphFetched('transactions')
.withGraphFetched('media'); .withGraphFetched('media');
return manualJournal; return manualJournal;
} }
/**
* Reverts the manual journal journal entries.
* @param {number} tenantId
* @param {number|number[]} manualJournalId
* @return {Promise<void>}
*/
public async revertJournalEntries(
tenantId: number,
manualJournalId: number | number[]
): Promise<void> {
this.logger.info('[manual_journal] trying to revert journal entries.', {
tenantId,
manualJournalId,
});
return this.journalService.revertJournalTransactions(
tenantId,
manualJournalId,
'Journal'
);
}
/** /**
* Write manual journal entries. * Write manual journal entries.
* @param {number} tenantId * @param {number} tenantId
@@ -615,26 +789,30 @@ export default class ManualJournalsService implements IManualJournalsService {
*/ */
public async writeJournalEntries( public async writeJournalEntries(
tenantId: number, tenantId: number,
manualJournalId: number, manualJorunal: IManualJournal | IManualJournal[],
manualJournalObj?: IManualJournal | null, override: Boolean = false
override?: Boolean
) { ) {
const journal = new JournalPoster(tenantId); const journal = new JournalPoster(tenantId);
const journalCommands = new JournalCommands(journal); const journalCommands = new JournalCommands(journal);
const manualJournals = Array.isArray(manualJorunal)
? manualJorunal
: [manualJorunal];
const manualJournalsIds = map(manualJournals, 'id');
if (override) { if (override) {
this.logger.info('[manual_journal] trying to revert journal entries.', { this.logger.info('[manual_journal] trying to revert journal entries.', {
tenantId, tenantId,
manualJournalId, manualJorunal,
}); });
await journalCommands.revertJournalEntries(manualJournalId, 'Journal'); await journalCommands.revertJournalEntries(manualJournalsIds, 'Journal');
}
if (manualJournalObj) {
journalCommands.manualJournal(manualJournalObj, manualJournalId);
} }
manualJournals.forEach((manualJournal) => {
journalCommands.manualJournal(manualJournal);
});
this.logger.info('[manual_journal] trying to save journal entries.', { this.logger.info('[manual_journal] trying to save journal entries.', {
tenantId, tenantId,
manualJournalId, manualJorunal,
}); });
await Promise.all([ await Promise.all([
journal.saveBalance(), journal.saveBalance(),
@@ -643,7 +821,7 @@ export default class ManualJournalsService implements IManualJournalsService {
]); ]);
this.logger.info( this.logger.info(
'[manual_journal] the journal entries saved successfully.', '[manual_journal] the journal entries saved successfully.',
{ tenantId, manualJournalId } { tenantId, manualJournalId: manualJorunal.id }
); );
} }
} }

View File

@@ -17,7 +17,7 @@ export default class JournalPosterService {
*/ */
async revertJournalTransactions( async revertJournalTransactions(
tenantId: number, tenantId: number,
referenceId: number, referenceId: number|number[],
referenceType: string referenceType: string
) { ) {
const journal = new JournalPoster(tenantId); const journal = new JournalPoster(tenantId);

View File

@@ -370,7 +370,6 @@ export default class PaymentReceiveService {
tenantId, tenantId,
paymentReceiveId paymentReceiveId
); );
// Validate payment receive number uniquiness. // Validate payment receive number uniquiness.
if (paymentReceiveDTO.paymentReceiveNo) { if (paymentReceiveDTO.paymentReceiveNo) {
await this.validatePaymentReceiveNoExistance( await this.validatePaymentReceiveNoExistance(
@@ -391,21 +390,18 @@ export default class PaymentReceiveService {
paymentReceiveId, paymentReceiveId,
paymentReceiveDTO.entries paymentReceiveDTO.entries
); );
// Validate payment receive invoices IDs existance and associated to the given customer id. // Validate payment receive invoices IDs existance and associated to the given customer id.
await this.validateInvoicesIDsExistance( await this.validateInvoicesIDsExistance(
tenantId, tenantId,
oldPaymentReceive.customerId, oldPaymentReceive.customerId,
paymentReceiveDTO.entries paymentReceiveDTO.entries
); );
// Validate invoice payment amount. // Validate invoice payment amount.
await this.validateInvoicesPaymentsAmount( await this.validateInvoicesPaymentsAmount(
tenantId, tenantId,
paymentReceiveDTO.entries, paymentReceiveDTO.entries,
oldPaymentReceive.entries oldPaymentReceive.entries
); );
// Update the payment receive transaction. // Update the payment receive transaction.
const paymentReceive = await PaymentReceive.query().upsertGraphAndFetch({ const paymentReceive = await PaymentReceive.query().upsertGraphAndFetch({
id: paymentReceiveId, id: paymentReceiveId,
@@ -669,8 +665,8 @@ export default class PaymentReceiveService {
/** /**
* Reverts the given payment receive journal entries. * Reverts the given payment receive journal entries.
* @param {number} tenantId * @param {number} tenantId - Tenant id.
* @param {number} paymentReceiveId * @param {number} paymentReceiveId - Payment receive id.
*/ */
async revertPaymentReceiveJournalEntries( async revertPaymentReceiveJournalEntries(
tenantId: number, tenantId: number,

View File

@@ -1,5 +1,5 @@
import { Inject, Container } from 'typedi'; import { Inject, Container } from 'typedi';
import { On, EventSubscriber } from "event-dispatch"; import { On, EventSubscriber } from 'event-dispatch';
import events from 'subscribers/events'; import events from 'subscribers/events';
import SettingsService from 'services/Settings/SettingsService'; import SettingsService from 'services/Settings/SettingsService';
import ManualJournalsService from 'services/ManualJournals/ManualJournalsService'; import ManualJournalsService from 'services/ManualJournals/ManualJournalsService';
@@ -8,46 +8,104 @@ import ManualJournalsService from 'services/ManualJournals/ManualJournalsService
export class ManualJournalSubscriber { export class ManualJournalSubscriber {
logger: any; logger: any;
settingsService: SettingsService; settingsService: SettingsService;
manualJournalsService: ManualJournalsService;
constructor() { constructor() {
this.logger = Container.get('logger'); this.logger = Container.get('logger');
this.settingsService = Container.get(SettingsService); this.settingsService = Container.get(SettingsService);
this.manualJournalsService = Container.get(ManualJournalsService);
} }
/** /**
* Handle manual journal created event. * Handle manual journal created event.
* @param {{ tenantId: number, manualJournal: IManualJournal }}
*/ */
@On(events.manualJournals.onCreated) @On(events.manualJournals.onCreated)
public async handleWriteJournalEntries({ tenantId, manualJournal }) { public async handleWriteJournalEntriesOnCreated({ tenantId, manualJournal }) {
const manualJournalsService = Container.get(ManualJournalsService); // Ingore writing manual journal journal entries in case was not published.
if (manualJournal.publishedAt) {
await manualJournalsService await this.manualJournalsService.writeJournalEntries(
.writeJournalEntries(tenantId, manualJournal.id, manualJournal); tenantId,
manualJournal
);
}
} }
/** /**
* Handle manual journal edited event. * Handle manual journal edited event.
* @param {{ tenantId: number, manualJournal: IManualJournal }}
*/ */
@On(events.manualJournals.onEdited) @On(events.manualJournals.onEdited)
public async handleRewriteJournalEntries({ tenantId, manualJournal }) { public async handleRewriteJournalEntriesOnEdited({
const manualJournalsService = Container.get(ManualJournalsService); 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. * Handle manual journal deleted event.
* @param {{ tenantId: number, manualJournalId: number }}
*/ */
@On(events.manualJournals.onDeleted) @On(events.manualJournals.onDeleted)
public async handleRevertJournalEntries({ tenantId, manualJournalId, }) { public async handleRevertJournalEntries({
const manualJournalsService = Container.get(ManualJournalsService); 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
);
} }
/** /**