mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-16 04:40:32 +00:00
fix: writing journal entries of manual journal.
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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');
|
||||
};
|
||||
@@ -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<void>;
|
||||
deleteManualJournals(tenantId: number, manualJournalsIds: number[]): Promise<void>;
|
||||
publishManualJournals(tenantId: number, manualJournalsIds: number[]): Promise<void>;
|
||||
publishManualJournal(tenantId: number, manualJournalId: number): Promise<void>;
|
||||
makeJournalEntries(
|
||||
tenantId: number,
|
||||
manualJournalDTO: IManualJournalDTO,
|
||||
authorizedUser: ISystemUser
|
||||
): Promise<{ manualJournal: IManualJournal }>;
|
||||
|
||||
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;
|
||||
}>;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: {
|
||||
|
||||
18
server/src/models/ManualJournalEntry.js
Normal file
18
server/src/models/ManualJournalEntry.js
Normal 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 [];
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<void> {
|
||||
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<any>{
|
||||
manualJournalDTO: IManualJournalDTO
|
||||
): Promise<any> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
): 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<void> {
|
||||
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<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.
|
||||
* @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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ export default class JournalPosterService {
|
||||
*/
|
||||
async revertJournalTransactions(
|
||||
tenantId: number,
|
||||
referenceId: number,
|
||||
referenceId: number|number[],
|
||||
referenceType: string
|
||||
) {
|
||||
const journal = new JournalPoster(tenantId);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user