mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-16 21:00:31 +00:00
333 lines
10 KiB
JavaScript
333 lines
10 KiB
JavaScript
import { check, query, oneOf, validationResult } from 'express-validator';
|
|
import express from 'express';
|
|
import { difference } from 'lodash';
|
|
import moment from 'moment';
|
|
import Account from '@/models/Account';
|
|
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
|
import JWTAuth from '@/http/middleware/jwtAuth';
|
|
import JournalPoster from '@/services/Accounting/JournalPoster';
|
|
import JournalEntry from '@/services/Accounting/JournalEntry';
|
|
import ManualJournal from '@/models/JournalEntry';
|
|
import Resource from '@/models/Resource';
|
|
import View from '@/models/View';
|
|
import {
|
|
mapViewRolesToConditionals,
|
|
validateViewRoles,
|
|
} from '@/lib/ViewRolesBuilder';
|
|
import FilterRoles from '@/lib/FilterRoles';
|
|
|
|
export default {
|
|
/**
|
|
* Router constructor.
|
|
*/
|
|
router() {
|
|
const router = express.Router();
|
|
router.use(JWTAuth);
|
|
|
|
router.get('/manual-journals',
|
|
this.manualJournals.validation,
|
|
asyncMiddleware(this.manualJournals.handler));
|
|
|
|
router.post('/make-journal-entries',
|
|
this.makeJournalEntries.validation,
|
|
asyncMiddleware(this.makeJournalEntries.handler));
|
|
|
|
router.post('/recurring-journal-entries',
|
|
this.recurringJournalEntries.validation,
|
|
asyncMiddleware(this.recurringJournalEntries.handler));
|
|
|
|
router.post('quick-journal-entries',
|
|
this.quickJournalEntries.validation,
|
|
asyncMiddleware(this.quickJournalEntries.handler));
|
|
|
|
return router;
|
|
},
|
|
|
|
/**
|
|
* Retrieve manual journals,
|
|
*/
|
|
manualJournals: {
|
|
validation: [
|
|
query('custom_view_id').optional().isNumeric().toInt(),
|
|
query('stringified_filter_roles').optional().isJSON(),
|
|
],
|
|
async handler(req, res) {
|
|
const validationErrors = validationResult(req);
|
|
|
|
if (!validationErrors.isEmpty()) {
|
|
return res.boom.badData(null, {
|
|
code: 'validation_error', ...validationErrors,
|
|
});
|
|
}
|
|
const filter = {
|
|
filter_roles: [],
|
|
...req.query,
|
|
};
|
|
if (filter.stringified_filter_roles) {
|
|
filter.filter_roles = JSON.parse(filter.stringified_filter_roles);
|
|
}
|
|
|
|
const errorReasons = [];
|
|
const viewConditionals = [];
|
|
const manualJournalsResource = await Resource.query()
|
|
.where('name', 'manual_journals')
|
|
.withGraphFetched('fields')
|
|
.first();
|
|
|
|
if (!manualJournalsResource) {
|
|
return res.status(400).send({
|
|
errors: [{ type: 'MANUAL_JOURNALS.RESOURCE.NOT.FOUND', code: 200 }],
|
|
});
|
|
}
|
|
|
|
const view = await View.query().onBuild((builder) => {
|
|
if (filter.custom_view_id) {
|
|
builder.where('id', filter.custom_view_id);
|
|
} else {
|
|
builder.where('favourite', true);
|
|
}
|
|
builder.where('resource_id', manualJournalsResource.id);
|
|
builder.withGraphFetched('roles.field');
|
|
builder.withGraphFetched('columns');
|
|
builder.first();
|
|
});
|
|
|
|
if (view && view.roles.length > 0) {
|
|
viewConditionals.push(
|
|
...mapViewRolesToConditionals(view.roles),
|
|
);
|
|
if (!validateViewRoles(viewConditionals, view.rolesLogicExpression)) {
|
|
errorReasons.push({ type: 'VIEW.LOGIC.EXPRESSION.INVALID', code: 400 });
|
|
}
|
|
}
|
|
// Validate the accounts resource fields.
|
|
const filterRoles = new FilterRoles(Resource.tableName,
|
|
filter.filter_roles.map((role) => ({ ...role, columnKey: role.fieldKey })),
|
|
manualJournalsResource.fields);
|
|
|
|
if (filterRoles.validateFilterRoles().length > 0) {
|
|
errorReasons.push({ type: 'ACCOUNTS.RESOURCE.HAS.NO.GIVEN.FIELDS', code: 500 });
|
|
}
|
|
if (errorReasons.length > 0) {
|
|
return res.status(400).send({ errors: errorReasons });
|
|
}
|
|
// Manual journals.
|
|
const manualJournals = await ManualJournal.query();
|
|
|
|
return res.status(200).send({
|
|
manualJournals,
|
|
});
|
|
},
|
|
},
|
|
|
|
/**
|
|
* Make journal entrires.
|
|
*/
|
|
makeJournalEntries: {
|
|
validation: [
|
|
check('date').isISO8601(),
|
|
check('journal_number').exists().trim().escape(),
|
|
check('transaction_type').optional({ nullable: true }).trim().escape(),
|
|
check('reference').optional({ nullable: true }),
|
|
check('description').optional().trim().escape(),
|
|
check('entries').isArray({ min: 2 }),
|
|
check('entries.*.credit').optional({ nullable: true }).isNumeric().toInt(),
|
|
check('entries.*.debit').optional({ nullable: true }).isNumeric().toInt(),
|
|
check('entries.*.account_id').isNumeric().toInt(),
|
|
check('entries.*.note').optional(),
|
|
],
|
|
async handler(req, res) {
|
|
const validationErrors = validationResult(req);
|
|
|
|
if (!validationErrors.isEmpty()) {
|
|
return res.boom.badData(null, {
|
|
code: 'validation_error', ...validationErrors,
|
|
});
|
|
}
|
|
const form = {
|
|
date: new Date(),
|
|
transaction_type: 'journal',
|
|
reference: '',
|
|
...req.body,
|
|
};
|
|
|
|
let totalCredit = 0;
|
|
let totalDebit = 0;
|
|
|
|
const { user } = req;
|
|
const errorReasons = [];
|
|
const entries = form.entries.filter((entry) => (entry.credit || entry.debit));
|
|
const formattedDate = moment(form.date).format('YYYY-MM-DD');
|
|
|
|
entries.forEach((entry) => {
|
|
if (entry.credit > 0) {
|
|
totalCredit += entry.credit;
|
|
}
|
|
if (entry.debit > 0) {
|
|
totalDebit += entry.debit;
|
|
}
|
|
});
|
|
|
|
if (totalCredit <= 0 || totalDebit <= 0) {
|
|
errorReasons.push({
|
|
type: 'CREDIT.DEBIT.SUMATION.SHOULD.NOT.EQUAL.ZERO',
|
|
code: 400,
|
|
});
|
|
}
|
|
if (totalCredit !== totalDebit) {
|
|
errorReasons.push({ type: 'CREDIT.DEBIT.NOT.EQUALS', code: 100 });
|
|
}
|
|
const accountsIds = entries.map((entry) => entry.account_id);
|
|
const accounts = await Account.query().whereIn('id', accountsIds)
|
|
.withGraphFetched('type');
|
|
|
|
const storedAccountsIds = accounts.map((account) => account.id);
|
|
|
|
if (difference(accountsIds, storedAccountsIds).length > 0) {
|
|
errorReasons.push({ type: 'ACCOUNTS.IDS.NOT.FOUND', code: 200 });
|
|
}
|
|
|
|
const journalNumber = await ManualJournal.query()
|
|
.where('journal_number', form.journal_number);
|
|
|
|
if (journalNumber.length > 0) {
|
|
errorReasons.push({ type: 'JOURNAL.NUMBER.ALREADY.EXISTS', code: 300 });
|
|
}
|
|
if (errorReasons.length > 0) {
|
|
return res.status(400).send({ errors: errorReasons });
|
|
}
|
|
|
|
// Save manual journal transaction.
|
|
const manualJournal = await ManualJournal.query().insert({
|
|
reference: form.reference,
|
|
transaction_type: 'Journal',
|
|
journal_number: form.journal_number,
|
|
amount: totalCredit,
|
|
date: formattedDate,
|
|
description: form.description,
|
|
user_id: user.id,
|
|
});
|
|
const journalPoster = new JournalPoster();
|
|
|
|
entries.forEach((entry) => {
|
|
const account = accounts.find((a) => a.id === entry.account_id);
|
|
|
|
const jouranlEntry = new JournalEntry({
|
|
debit: entry.debit,
|
|
credit: entry.credit,
|
|
account: account.id,
|
|
referenceType: 'Journal',
|
|
referenceId: manualJournal.id,
|
|
accountNormal: account.type.normal,
|
|
note: entry.note,
|
|
date: formattedDate,
|
|
userId: user.id,
|
|
});
|
|
if (entry.debit) {
|
|
journalPoster.debit(jouranlEntry);
|
|
} else {
|
|
journalPoster.credit(jouranlEntry);
|
|
}
|
|
});
|
|
|
|
// Saves the journal entries and accounts balance changes.
|
|
await Promise.all([
|
|
journalPoster.saveEntries(),
|
|
journalPoster.saveBalance(),
|
|
]);
|
|
return res.status(200).send({ id: manualJournal.id });
|
|
},
|
|
},
|
|
|
|
/**
|
|
* Saves recurring journal entries template.
|
|
*/
|
|
recurringJournalEntries: {
|
|
validation: [
|
|
check('template_name').exists(),
|
|
check('recurrence').exists(),
|
|
check('active').optional().isBoolean().toBoolean(),
|
|
check('entries').isArray({ min: 1 }),
|
|
check('entries.*.credit').isNumeric().toInt(),
|
|
check('entries.*.debit').isNumeric().toInt(),
|
|
check('entries.*.account_id').isNumeric().toInt(),
|
|
check('entries.*.note').optional(),
|
|
],
|
|
async handler(req, res) {
|
|
const validationErrors = validationResult(req);
|
|
|
|
if (!validationErrors.isEmpty()) {
|
|
return res.boom.badData(null, {
|
|
code: 'validation_error', ...validationErrors,
|
|
});
|
|
}
|
|
|
|
},
|
|
},
|
|
|
|
recurringJournalsList: {
|
|
validation: [
|
|
query('page').optional().isNumeric().toInt(),
|
|
query('page_size').optional().isNumeric().toInt(),
|
|
query('template_name').optional(),
|
|
],
|
|
async handler(req, res) {
|
|
const validationErrors = validationResult(req);
|
|
|
|
if (!validationErrors.isEmpty()) {
|
|
return res.boom.badData(null, {
|
|
code: 'validation_error', ...validationErrors,
|
|
});
|
|
}
|
|
},
|
|
},
|
|
|
|
quickJournalEntries: {
|
|
validation: [
|
|
check('date').exists().isISO8601(),
|
|
check('amount').exists().isNumeric().toFloat(),
|
|
check('credit_account_id').exists().isNumeric().toInt(),
|
|
check('debit_account_id').exists().isNumeric().toInt(),
|
|
check('transaction_type').exists(),
|
|
check('note').optional(),
|
|
],
|
|
async handler(req, res) {
|
|
const validationErrors = validationResult(req);
|
|
|
|
if (!validationErrors.isEmpty()) {
|
|
return res.boom.badData(null, {
|
|
code: 'validation_error', ...validationErrors,
|
|
});
|
|
}
|
|
const errorReasons = [];
|
|
const form = { ...req.body };
|
|
|
|
const foundAccounts = await Account.query()
|
|
.where('id', form.credit_account_id)
|
|
.orWhere('id', form.debit_account_id);
|
|
|
|
const creditAccount = foundAccounts.find((a) => a.id === form.credit_account_id);
|
|
const debitAccount = foundAccounts.find((a) => a.id === form.debit_account_id);
|
|
|
|
if (!creditAccount) {
|
|
errorReasons.push({ type: 'CREDIT_ACCOUNT.NOT.EXIST', code: 100 });
|
|
}
|
|
if (!debitAccount) {
|
|
errorReasons.push({ type: 'DEBIT_ACCOUNT.NOT.EXIST', code: 200 });
|
|
}
|
|
if (errorReasons.length > 0) {
|
|
return res.status(400).send({ errors: errorReasons });
|
|
}
|
|
|
|
// const journalPoster = new JournalPoster();
|
|
// const journalCredit = new JournalEntry({
|
|
// debit:
|
|
// account: debitAccount.id,
|
|
// referenceId:
|
|
// })
|
|
|
|
return res.status(200).send();
|
|
},
|
|
},
|
|
};
|