mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-16 04:40:32 +00:00
- feat: Filter expense and payment accounts on expense form.
- feat: Make journal errors with receivable and payable accounts. - fix: Handle database big numbers. - fix: Indexing lines when add a new line on make journal form. - fix: Abstruct accounts type component.
This commit is contained in:
@@ -5,8 +5,8 @@ exports.up = function (knex) {
|
||||
table.string('name');
|
||||
table.string('type');
|
||||
table.string('sku');
|
||||
table.decimal('cost_price').unsigned();
|
||||
table.decimal('sell_price').unsigned();
|
||||
table.decimal('cost_price', 13, 3).unsigned();
|
||||
table.decimal('sell_price', 13, 3).unsigned();
|
||||
table.string('currency_code', 3);
|
||||
table.string('picture_uri');
|
||||
table.integer('cost_account_id').unsigned();
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
exports.up = function(knex) {
|
||||
return knex.schema.createTable('accounts_transactions', (table) => {
|
||||
table.increments();
|
||||
table.decimal('credit');
|
||||
table.decimal('debit');
|
||||
table.decimal('credit', 13, 3);
|
||||
table.decimal('debit', 13, 3);
|
||||
table.string('transaction_type');
|
||||
table.string('reference_type');
|
||||
table.integer('reference_id');
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
exports.up = function(knex) {
|
||||
return knex.schema.createTable('expenses_transactions', (table) => {
|
||||
table.increments();
|
||||
table.decimal('total_amount');
|
||||
table.string('currency_code');
|
||||
table.decimal('total_amount', 13, 3);
|
||||
table.string('currency_code', 3);
|
||||
table.text('description');
|
||||
table.integer('payment_account_id').unsigned();
|
||||
table.integer('payee_id').unsigned();
|
||||
|
||||
@@ -4,7 +4,7 @@ exports.up = function(knex) {
|
||||
table.increments();
|
||||
table.date('date');
|
||||
table.string('currency_code');
|
||||
table.decimal('exchange_rate');
|
||||
table.decimal('exchange_rate', 8, 5);
|
||||
table.string('note');
|
||||
});
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@ exports.up = function(knex) {
|
||||
table.string('journal_number');
|
||||
table.string('reference');
|
||||
table.string('journal_type');
|
||||
table.decimal('amount');
|
||||
table.decimal('amount', 13, 3);
|
||||
table.date('date');
|
||||
table.boolean('status').defaultTo(false);
|
||||
table.string('description');
|
||||
|
||||
@@ -5,7 +5,7 @@ exports.up = function(knex) {
|
||||
table.integer('expense_account_id').unsigned();
|
||||
table.integer('index').unsigned();
|
||||
table.text('description');
|
||||
table.decimal('amount');
|
||||
table.decimal('amount', 13, 3);
|
||||
table.integer('expense_id').unsigned();
|
||||
table.timestamps();
|
||||
}).raw('ALTER TABLE `EXPENSE_TRANSACTION_CATEGORIES` AUTO_INCREMENT = 1000');;
|
||||
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
DynamicFilterFilterRoles,
|
||||
} from '@/lib/DynamicFilter';
|
||||
|
||||
|
||||
export default {
|
||||
/**
|
||||
* Router constructor.
|
||||
@@ -24,45 +23,63 @@ export default {
|
||||
router() {
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/manual-journals/:id',
|
||||
router.get(
|
||||
'/manual-journals/:id',
|
||||
this.getManualJournal.validation,
|
||||
asyncMiddleware(this.getManualJournal.handler));
|
||||
asyncMiddleware(this.getManualJournal.handler)
|
||||
);
|
||||
|
||||
router.get('/manual-journals',
|
||||
router.get(
|
||||
'/manual-journals',
|
||||
this.manualJournals.validation,
|
||||
asyncMiddleware(this.manualJournals.handler));
|
||||
asyncMiddleware(this.manualJournals.handler)
|
||||
);
|
||||
|
||||
router.post('/make-journal-entries',
|
||||
router.post(
|
||||
'/make-journal-entries',
|
||||
this.validateMediaIds,
|
||||
this.validateContactEntries,
|
||||
this.makeJournalEntries.validation,
|
||||
asyncMiddleware(this.makeJournalEntries.handler));
|
||||
asyncMiddleware(this.makeJournalEntries.handler)
|
||||
);
|
||||
|
||||
router.post('/manual-journals/:id/publish',
|
||||
router.post(
|
||||
'/manual-journals/:id/publish',
|
||||
this.publishManualJournal.validation,
|
||||
asyncMiddleware(this.publishManualJournal.handler));
|
||||
asyncMiddleware(this.publishManualJournal.handler)
|
||||
);
|
||||
|
||||
router.post('/manual-journals/:id',
|
||||
router.post(
|
||||
'/manual-journals/:id',
|
||||
this.validateMediaIds,
|
||||
this.validateContactEntries,
|
||||
this.editManualJournal.validation,
|
||||
asyncMiddleware(this.editManualJournal.handler));
|
||||
asyncMiddleware(this.editManualJournal.handler)
|
||||
);
|
||||
|
||||
router.delete('/manual-journals/:id',
|
||||
router.delete(
|
||||
'/manual-journals/:id',
|
||||
this.deleteManualJournal.validation,
|
||||
asyncMiddleware(this.deleteManualJournal.handler));
|
||||
asyncMiddleware(this.deleteManualJournal.handler)
|
||||
);
|
||||
|
||||
router.delete('/manual-journals',
|
||||
router.delete(
|
||||
'/manual-journals',
|
||||
this.deleteBulkManualJournals.validation,
|
||||
asyncMiddleware(this.deleteBulkManualJournals.handler));
|
||||
asyncMiddleware(this.deleteBulkManualJournals.handler)
|
||||
);
|
||||
|
||||
router.post('/recurring-journal-entries',
|
||||
router.post(
|
||||
'/recurring-journal-entries',
|
||||
this.recurringJournalEntries.validation,
|
||||
asyncMiddleware(this.recurringJournalEntries.handler));
|
||||
asyncMiddleware(this.recurringJournalEntries.handler)
|
||||
);
|
||||
|
||||
router.post('quick-journal-entries',
|
||||
router.post(
|
||||
'quick-journal-entries',
|
||||
this.quickJournalEntries.validation,
|
||||
asyncMiddleware(this.quickJournalEntries.handler));
|
||||
asyncMiddleware(this.quickJournalEntries.handler)
|
||||
);
|
||||
|
||||
return router;
|
||||
},
|
||||
@@ -76,7 +93,7 @@ export default {
|
||||
query('page_size').optional().isNumeric().toInt(),
|
||||
query('custom_view_id').optional().isNumeric().toInt(),
|
||||
|
||||
query('column_sort_by').optional(),
|
||||
query('column_sort_by').optional().trim().escape(),
|
||||
query('sort_order').optional().isIn(['desc', 'asc']),
|
||||
|
||||
query('stringified_filter_roles').optional().isJSON(),
|
||||
@@ -86,7 +103,8 @@ export default {
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
code: 'validation_error',
|
||||
...validationErrors,
|
||||
});
|
||||
}
|
||||
const filter = {
|
||||
@@ -126,17 +144,22 @@ export default {
|
||||
builder.remember();
|
||||
});
|
||||
|
||||
const resourceFieldsKeys = manualJournalsResource.fields.map((c) => c.key);
|
||||
const resourceFieldsKeys = manualJournalsResource.fields.map(
|
||||
(c) => c.key
|
||||
);
|
||||
const dynamicFilter = new DynamicFilter(ManualJournal.tableName);
|
||||
|
||||
// Dynamic filter with view roles.
|
||||
if (view && view.roles.length > 0) {
|
||||
const viewFilter = new DynamicFilterViews(
|
||||
mapViewRolesToConditionals(view.roles),
|
||||
view.rolesLogicExpression,
|
||||
view.rolesLogicExpression
|
||||
);
|
||||
if (!viewFilter.validateFilterRoles()) {
|
||||
errorReasons.push({ type: 'VIEW.LOGIC.EXPRESSION.INVALID', code: 400 });
|
||||
errorReasons.push({
|
||||
type: 'VIEW.LOGIC.EXPRESSION.INVALID',
|
||||
code: 400,
|
||||
});
|
||||
}
|
||||
dynamicFilter.setFilter(viewFilter);
|
||||
}
|
||||
@@ -145,12 +168,15 @@ export default {
|
||||
// Validate the accounts resource fields.
|
||||
const filterRoles = new DynamicFilterFilterRoles(
|
||||
mapFilterRolesToDynamicFilter(filter.filter_roles),
|
||||
manualJournalsResource.fields,
|
||||
manualJournalsResource.fields
|
||||
);
|
||||
dynamicFilter.setFilter(filterRoles);
|
||||
|
||||
if (filterRoles.validateFilterRoles().length > 0) {
|
||||
errorReasons.push({ type: 'MANUAL.JOURNAL.HAS.NO.FIELDS', code: 500 });
|
||||
errorReasons.push({
|
||||
type: 'MANUAL.JOURNAL.HAS.NO.FIELDS',
|
||||
code: 500,
|
||||
});
|
||||
}
|
||||
}
|
||||
// Dynamic filter with column sort order.
|
||||
@@ -160,7 +186,7 @@ export default {
|
||||
}
|
||||
const sortByFilter = new DynamicFilterSortBy(
|
||||
filter.column_sort_by,
|
||||
filter.sort_order,
|
||||
filter.sort_order
|
||||
);
|
||||
dynamicFilter.setFilter(sortByFilter);
|
||||
}
|
||||
@@ -168,18 +194,22 @@ export default {
|
||||
return res.status(400).send({ errors: errorReasons });
|
||||
}
|
||||
// Manual journals.
|
||||
const manualJournals = await ManualJournal.query().onBuild((builder) => {
|
||||
dynamicFilter.buildQuery()(builder);
|
||||
}).pagination(filter.page - 1, filter.page_size);
|
||||
const manualJournals = await ManualJournal.query()
|
||||
.onBuild((builder) => {
|
||||
dynamicFilter.buildQuery()(builder);
|
||||
})
|
||||
.pagination(filter.page - 1, filter.page_size);
|
||||
|
||||
return res.status(200).send({
|
||||
manualJournals: {
|
||||
...manualJournals,
|
||||
...(view) ? {
|
||||
viewMeta: {
|
||||
customViewId: view.id,
|
||||
}
|
||||
} : {},
|
||||
...(view
|
||||
? {
|
||||
viewMeta: {
|
||||
customViewId: view.id,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
});
|
||||
},
|
||||
@@ -199,28 +229,37 @@ export default {
|
||||
// Validate if media ids was not already exists on the storage.
|
||||
if (form.media_ids.length > 0) {
|
||||
const storedMedia = await Media.query().whereIn('id', form.media_ids);
|
||||
const notFoundMedia = difference(form.media_ids, storedMedia.map((m) => m.id));
|
||||
|
||||
const notFoundMedia = difference(
|
||||
form.media_ids,
|
||||
storedMedia.map((m) => m.id)
|
||||
);
|
||||
|
||||
if (notFoundMedia.length > 0) {
|
||||
errorReasons.push({ type: 'MEDIA.IDS.NOT.FOUND', code: 400, ids: notFoundMedia });
|
||||
errorReasons.push({
|
||||
type: 'MEDIA.IDS.NOT.FOUND',
|
||||
code: 400,
|
||||
ids: notFoundMedia,
|
||||
});
|
||||
}
|
||||
}
|
||||
req.errorReasons = Array.isArray(req.errorReasons) && req.errorReasons.length
|
||||
? req.errorReasons.push(...errorReasons) : errorReasons;
|
||||
next();
|
||||
req.errorReasons =
|
||||
Array.isArray(req.errorReasons) && req.errorReasons.length
|
||||
? req.errorReasons.push(...errorReasons)
|
||||
: errorReasons;
|
||||
next();
|
||||
},
|
||||
|
||||
/**
|
||||
* Validate form entries with contact customers and vendors.
|
||||
*
|
||||
* Validate form entries with contact customers and vendors.
|
||||
*
|
||||
* - Validate the entries that with receivable has no customer contact.
|
||||
* - Validate the entries that with payable has no vendor contact.
|
||||
* - Validate the entries with customers contacts that not found on the storage.
|
||||
* - Validate the entries with vendors contacts that not found on the storage.
|
||||
*
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
async validateContactEntries(req, res, next) {
|
||||
const form = { entries: [], ...req.body };
|
||||
@@ -228,86 +267,111 @@ export default {
|
||||
const errorReasons = [];
|
||||
|
||||
// Validate the entries contact type and ids.
|
||||
const formEntriesCustomersIds = form.entries.filter(e => e.contact_type === 'customer');
|
||||
const formEntriesVendorsIds = form.entries.filter(e => e.contact_type === 'vendor');
|
||||
const formEntriesCustomersIds = form.entries.filter(
|
||||
(e) => e.contact_type === 'customer'
|
||||
);
|
||||
const formEntriesVendorsIds = form.entries.filter(
|
||||
(e) => e.contact_type === 'vendor'
|
||||
);
|
||||
|
||||
const accountsTypes = await AccountType.query();
|
||||
|
||||
const payableAccountsType = accountsTypes.find(t => t.key === 'accounts_payable');;
|
||||
const receivableAccountsType = accountsTypes.find(t => t.key === 'accounts_receivable');
|
||||
|
||||
const receivableAccountOper = Account.query().where('account_type_id', receivableAccountsType.id).first();
|
||||
const payableAccountOper = Account.query().where('account_type_id', payableAccountsType.id).first();
|
||||
const payableAccountsType = accountsTypes.find(
|
||||
(t) => t.key === 'accounts_payable'
|
||||
);
|
||||
const receivableAccountsType = accountsTypes.find(
|
||||
(t) => t.key === 'accounts_receivable'
|
||||
);
|
||||
|
||||
const receivableAccountOper = Account.query()
|
||||
.where('account_type_id', receivableAccountsType.id)
|
||||
.first();
|
||||
const payableAccountOper = Account.query()
|
||||
.where('account_type_id', payableAccountsType.id)
|
||||
.first();
|
||||
|
||||
const [receivableAccount, payableAccount] = await Promise.all([
|
||||
receivableAccountOper, payableAccountOper,
|
||||
receivableAccountOper,
|
||||
payableAccountOper,
|
||||
]);
|
||||
|
||||
const entriesHasNoReceivableAccount = form.entries
|
||||
.filter(e =>
|
||||
(e.account_id === receivableAccount.id) &&
|
||||
const entriesHasNoReceivableAccount = form.entries.filter(
|
||||
(e) =>
|
||||
e.account_id === receivableAccount.id &&
|
||||
(!e.contact_id || e.contact_type !== 'customer')
|
||||
);
|
||||
);
|
||||
|
||||
if (entriesHasNoReceivableAccount.length > 0) {
|
||||
errorReasons.push({
|
||||
type: 'RECEIVABLE.ENTRIES.HAS.NO.CUSTOMERS',
|
||||
code: 900,
|
||||
indexes: entriesHasNoReceivableAccount.map(e => e.index),
|
||||
indexes: entriesHasNoReceivableAccount.map((e) => e.index),
|
||||
});
|
||||
}
|
||||
|
||||
const entriesHasNoVendorContact = form.entries
|
||||
.filter(e =>
|
||||
(e.account_id === payableAccount.id) &&
|
||||
const entriesHasNoVendorContact = form.entries.filter(
|
||||
(e) =>
|
||||
e.account_id === payableAccount.id &&
|
||||
(!e.contact_id || e.contact_type !== 'contact')
|
||||
);
|
||||
);
|
||||
|
||||
if (entriesHasNoVendorContact.length > 0) {
|
||||
errorReasons.push({
|
||||
type: 'PAYABLE.ENTRIES.HAS.NO.VENDORS',
|
||||
code: 1000,
|
||||
indexes: entriesHasNoVendorContact.map(e => e.index),
|
||||
indexes: entriesHasNoVendorContact.map((e) => e.index),
|
||||
});
|
||||
}
|
||||
|
||||
// Validate customers contacts.
|
||||
if (formEntriesCustomersIds.length > 0) {
|
||||
const customersContactsIds = formEntriesCustomersIds.map(c => c.contact_id);
|
||||
const storedContacts = await Customer.query().whereIn('id', customersContactsIds);
|
||||
const customersContactsIds = formEntriesCustomersIds.map(
|
||||
(c) => c.contact_id
|
||||
);
|
||||
const storedContacts = await Customer.query().whereIn(
|
||||
'id',
|
||||
customersContactsIds
|
||||
);
|
||||
|
||||
const storedContactsIds = storedContacts.map(c => c.id);
|
||||
const storedContactsIds = storedContacts.map((c) => c.id);
|
||||
|
||||
const notFoundContactsIds = difference(
|
||||
formEntriesCustomersIds.map(c => c.contact_id),
|
||||
storedContactsIds,
|
||||
formEntriesCustomersIds.map((c) => c.contact_id),
|
||||
storedContactsIds
|
||||
);
|
||||
if (notFoundContactsIds.length > 0) {
|
||||
errorReasons.push({ type: 'CUSTOMERS.CONTACTS.NOT.FOUND', code: 500, ids: notFoundContactsIds });
|
||||
errorReasons.push({
|
||||
type: 'CUSTOMERS.CONTACTS.NOT.FOUND',
|
||||
code: 500,
|
||||
ids: notFoundContactsIds,
|
||||
});
|
||||
}
|
||||
|
||||
const notReceivableAccounts = formEntriesCustomersIds.filter(
|
||||
c => c.account_id !== receivableAccount.id
|
||||
(c) => c.account_id !== receivableAccount.id
|
||||
);
|
||||
if (notReceivableAccounts.length > 0) {
|
||||
errorReasons.push({
|
||||
type: 'CUSTOMERS.NOT.WITH.RECEIVABLE.ACCOUNT',
|
||||
code: 700,
|
||||
indexes: notReceivableAccounts.map(a => a.index),
|
||||
indexes: notReceivableAccounts.map((a) => a.index),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Validate vendors contacts.
|
||||
if (formEntriesVendorsIds.length > 0) {
|
||||
const vendorsContactsIds = formEntriesVendorsIds.map(c => c.contact_id);
|
||||
const storedContacts = await Vendor.query().where('id', vendorsContactsIds);
|
||||
const vendorsContactsIds = formEntriesVendorsIds.map((c) => c.contact_id);
|
||||
const storedContacts = await Vendor.query().where(
|
||||
'id',
|
||||
vendorsContactsIds
|
||||
);
|
||||
|
||||
const storedContactsIds = storedContacts.map(c => c.id);
|
||||
const storedContactsIds = storedContacts.map((c) => c.id);
|
||||
|
||||
const notFoundContactsIds = difference(
|
||||
formEntriesVendorsIds.map(v => v.contact_id),
|
||||
storedContactsIds,
|
||||
formEntriesVendorsIds.map((v) => v.contact_id),
|
||||
storedContactsIds
|
||||
);
|
||||
if (notFoundContactsIds.length > 0) {
|
||||
errorReasons.push({
|
||||
@@ -317,19 +381,21 @@ export default {
|
||||
});
|
||||
}
|
||||
const notPayableAccounts = formEntriesVendorsIds.filter(
|
||||
v => v.contact_id === payableAccount.id
|
||||
(v) => v.contact_id === payableAccount.id
|
||||
);
|
||||
if (notPayableAccounts.length > 0) {
|
||||
errorReasons.push({
|
||||
type: 'VENDORS.NOT.WITH.PAYABLE.ACCOUNT',
|
||||
code: 800,
|
||||
indexes: notPayableAccounts.map(a => a.index),
|
||||
indexes: notPayableAccounts.map((a) => a.index),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
req.errorReasons = Array.isArray(req.errorReasons) && req.errorReasons.length
|
||||
? req.errorReasons.push(...errorReasons) : errorReasons;
|
||||
req.errorReasons =
|
||||
Array.isArray(req.errorReasons) && req.errorReasons.length
|
||||
? req.errorReasons.push(...errorReasons)
|
||||
: errorReasons;
|
||||
|
||||
next();
|
||||
},
|
||||
@@ -347,11 +413,24 @@ export default {
|
||||
check('status').optional().isBoolean().toBoolean(),
|
||||
check('entries').isArray({ min: 2 }),
|
||||
check('entries.*.index').exists().isNumeric().toInt(),
|
||||
check('entries.*.credit').optional({ nullable: true }).isNumeric().toInt(),
|
||||
check('entries.*.debit').optional({ nullable: true }).isNumeric().toInt(),
|
||||
check('entries.*.credit')
|
||||
.optional({ nullable: true })
|
||||
.isNumeric()
|
||||
.isDecimal()
|
||||
.isFloat({ max: 9999999999.999 }) // 13, 3
|
||||
.toFloat(),
|
||||
check('entries.*.debit')
|
||||
.optional({ nullable: true })
|
||||
.isNumeric()
|
||||
.isDecimal()
|
||||
.isFloat({ max: 9999999999.999 }) // 13, 3
|
||||
.toFloat(),
|
||||
check('entries.*.account_id').isNumeric().toInt(),
|
||||
check('entries.*.note').optional(),
|
||||
check('entries.*.contact_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||
check('entries.*.contact_id')
|
||||
.optional({ nullable: true })
|
||||
.isNumeric()
|
||||
.toInt(),
|
||||
check('entries.*.contact_type').optional().isIn(['vendor', 'customer']),
|
||||
check('media_ids').optional().isArray(),
|
||||
check('media_ids.*').exists().isNumeric().toInt(),
|
||||
@@ -361,7 +440,8 @@ export default {
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
code: 'validation_error',
|
||||
...validationErrors,
|
||||
});
|
||||
}
|
||||
const form = {
|
||||
@@ -371,18 +451,16 @@ export default {
|
||||
media_ids: [],
|
||||
...req.body,
|
||||
};
|
||||
const {
|
||||
ManualJournal,
|
||||
Account,
|
||||
MediaLink,
|
||||
} = req.models;
|
||||
const { ManualJournal, Account, MediaLink } = req.models;
|
||||
|
||||
let totalCredit = 0;
|
||||
let totalDebit = 0;
|
||||
|
||||
const { user } = req;
|
||||
const errorReasons = [...(req.errorReasons || [])];
|
||||
const entries = form.entries.filter((entry) => (entry.credit || entry.debit));
|
||||
const entries = form.entries.filter(
|
||||
(entry) => entry.credit || entry.debit
|
||||
);
|
||||
const formattedDate = moment(form.date).format('YYYY-MM-DD');
|
||||
|
||||
entries.forEach((entry) => {
|
||||
@@ -414,8 +492,10 @@ export default {
|
||||
errorReasons.push({ type: 'ACCOUNTS.IDS.NOT.FOUND', code: 200 });
|
||||
}
|
||||
|
||||
const journalNumber = await ManualJournal.query()
|
||||
.where('journal_number', form.journal_number);
|
||||
const journalNumber = await ManualJournal.query().where(
|
||||
'journal_number',
|
||||
form.journal_number
|
||||
);
|
||||
|
||||
if (journalNumber.length > 0) {
|
||||
errorReasons.push({ type: 'JOURNAL.NUMBER.ALREADY.EXISTS', code: 300 });
|
||||
@@ -478,7 +558,7 @@ export default {
|
||||
await Promise.all([
|
||||
...bulkSaveMediaLink,
|
||||
journalPoster.saveEntries(),
|
||||
(form.status) && journalPoster.saveBalance(),
|
||||
form.status && journalPoster.saveBalance(),
|
||||
]);
|
||||
return res.status(200).send({ id: manualJournal.id });
|
||||
},
|
||||
@@ -503,7 +583,8 @@ export default {
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
code: 'validation_error',
|
||||
...validationErrors,
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -517,15 +598,29 @@ export default {
|
||||
param('id').exists().isNumeric().toInt(),
|
||||
check('date').exists().isISO8601(),
|
||||
check('journal_number').exists().trim().escape(),
|
||||
check('transaction_type').optional({ nullable: true }).trim().escape(),
|
||||
check('journal_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.*.index').exists().isNumeric().toInt(),
|
||||
check('entries.*.credit')
|
||||
.optional({ nullable: true })
|
||||
.isNumeric()
|
||||
.toFloat(),
|
||||
check('entries.*.debit')
|
||||
.optional({ nullable: true })
|
||||
.isNumeric()
|
||||
.toFloat(),
|
||||
check('entries.*.account_id').isNumeric().toInt(),
|
||||
check('entries.*.contact_id').optional().isNumeric().toInt(),
|
||||
check('entries.*.contact_type').optional().isIn(['vendor', 'customer']).isNumeric().toInt(),
|
||||
check('entries.*.contact_id')
|
||||
.optional({ nullable: true })
|
||||
.isNumeric()
|
||||
.toInt(),
|
||||
check('entries.*.contact_type')
|
||||
.optional()
|
||||
.isIn(['vendor', 'customer'])
|
||||
.isNumeric()
|
||||
.toInt(),
|
||||
check('entries.*.note').optional(),
|
||||
check('media_ids').optional().isArray(),
|
||||
check('media_ids.*').isNumeric().toInt(),
|
||||
@@ -535,24 +630,30 @@ export default {
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
code: 'validation_error',
|
||||
...validationErrors,
|
||||
});
|
||||
}
|
||||
const form = {
|
||||
date: new Date(),
|
||||
transaction_type: 'journal',
|
||||
journal_type: 'Journal',
|
||||
reference: '',
|
||||
media_ids: [],
|
||||
...req.body,
|
||||
};
|
||||
const { id } = req.params;
|
||||
const {
|
||||
ManualJournal, AccountTransaction, Account, Media, MediaLink,
|
||||
ManualJournal,
|
||||
AccountTransaction,
|
||||
Account,
|
||||
Media,
|
||||
MediaLink,
|
||||
} = req.models;
|
||||
|
||||
const manualJournal = await ManualJournal.query()
|
||||
.where('id', id)
|
||||
.withGraphFetched('media').first();
|
||||
.withGraphFetched('media')
|
||||
.first();
|
||||
|
||||
if (!manualJournal) {
|
||||
return res.status(4040).send({
|
||||
@@ -564,7 +665,9 @@ export default {
|
||||
|
||||
const { user } = req;
|
||||
const errorReasons = [...(req.errorReasons || [])];
|
||||
const entries = form.entries.filter((entry) => (entry.credit || entry.debit));
|
||||
const entries = form.entries.filter(
|
||||
(entry) => entry.credit || entry.debit
|
||||
);
|
||||
const formattedDate = moment(form.date).format('YYYY-MM-DD');
|
||||
|
||||
entries.forEach((entry) => {
|
||||
@@ -593,7 +696,8 @@ export default {
|
||||
errorReasons.push({ type: 'JOURNAL.NUMBER.ALREADY.EXISTS', code: 300 });
|
||||
}
|
||||
const accountsIds = entries.map((entry) => entry.account_id);
|
||||
const accounts = await Account.query().whereIn('id', accountsIds)
|
||||
const accounts = await Account.query()
|
||||
.whereIn('id', accountsIds)
|
||||
.withGraphFetched('type');
|
||||
|
||||
const storedAccountsIds = accounts.map((account) => account.id);
|
||||
@@ -605,16 +709,14 @@ export default {
|
||||
return res.status(400).send({ errors: errorReasons });
|
||||
}
|
||||
|
||||
await ManualJournal.query()
|
||||
.where('id', manualJournal.id)
|
||||
.update({
|
||||
reference: form.reference,
|
||||
transaction_type: 'Journal',
|
||||
journalNumber: form.journal_number,
|
||||
amount: totalCredit,
|
||||
date: formattedDate,
|
||||
description: form.description,
|
||||
});
|
||||
await ManualJournal.query().where('id', manualJournal.id).update({
|
||||
reference: form.reference,
|
||||
journal_type: form.journal_type,
|
||||
journalNumber: form.journal_number,
|
||||
amount: totalCredit,
|
||||
date: formattedDate,
|
||||
description: form.description,
|
||||
});
|
||||
|
||||
const transactions = await AccountTransaction.query()
|
||||
.whereIn('reference_type', ['Journal'])
|
||||
@@ -674,26 +776,20 @@ export default {
|
||||
},
|
||||
|
||||
publishManualJournal: {
|
||||
validation: [
|
||||
param('id').exists().isNumeric().toInt(),
|
||||
],
|
||||
validation: [param('id').exists().isNumeric().toInt()],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
code: 'validation_error',
|
||||
...validationErrors,
|
||||
});
|
||||
}
|
||||
const {
|
||||
ManualJournal,
|
||||
AccountTransaction,
|
||||
Account,
|
||||
} = req.models;
|
||||
const { ManualJournal, AccountTransaction, Account } = req.models;
|
||||
|
||||
const { id } = req.params;
|
||||
const manualJournal = await ManualJournal.query()
|
||||
.where('id', id).first();
|
||||
const manualJournal = await ManualJournal.query().where('id', id).first();
|
||||
|
||||
if (!manualJournal) {
|
||||
return res.status(404).send({
|
||||
@@ -721,7 +817,10 @@ export default {
|
||||
journal.calculateEntriesBalanceChange();
|
||||
|
||||
const updateAccountsTransactionsOper = AccountTransaction.query()
|
||||
.whereIn('id', transactions.map((t) => t.id))
|
||||
.whereIn(
|
||||
'id',
|
||||
transactions.map((t) => t.id)
|
||||
)
|
||||
.update({ draft: 0 });
|
||||
|
||||
await Promise.all([
|
||||
@@ -734,20 +833,17 @@ export default {
|
||||
},
|
||||
|
||||
getManualJournal: {
|
||||
validation: [
|
||||
param('id').exists().isNumeric().toInt(),
|
||||
],
|
||||
validation: [param('id').exists().isNumeric().toInt()],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
code: 'validation_error',
|
||||
...validationErrors,
|
||||
});
|
||||
}
|
||||
const {
|
||||
ManualJournal, AccountTransaction,
|
||||
} = req.models;
|
||||
const { ManualJournal, AccountTransaction } = req.models;
|
||||
|
||||
const { id } = req.params;
|
||||
const manualJournal = await ManualJournal.query()
|
||||
@@ -759,7 +855,7 @@ export default {
|
||||
return res.status(404).send({
|
||||
errors: [{ type: 'MANUAL.JOURNAL.NOT.FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
}
|
||||
const transactions = await AccountTransaction.query()
|
||||
.whereIn('reference_type', ['Journal', 'ManualJournal'])
|
||||
.where('reference_id', manualJournal.id);
|
||||
@@ -767,9 +863,7 @@ export default {
|
||||
return res.status(200).send({
|
||||
manual_journal: {
|
||||
...manualJournal.toJSON(),
|
||||
entries: [
|
||||
...transactions,
|
||||
],
|
||||
entries: [...transactions],
|
||||
},
|
||||
});
|
||||
},
|
||||
@@ -780,15 +874,14 @@ export default {
|
||||
* accounts transactions.
|
||||
*/
|
||||
deleteManualJournal: {
|
||||
validation: [
|
||||
param('id').exists().isNumeric().toInt(),
|
||||
],
|
||||
validation: [param('id').exists().isNumeric().toInt()],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
code: 'validation_error',
|
||||
...validationErrors,
|
||||
});
|
||||
}
|
||||
const { id } = req.params;
|
||||
@@ -799,8 +892,7 @@ export default {
|
||||
Account,
|
||||
} = req.models;
|
||||
|
||||
const manualJournal = await ManualJournal.query()
|
||||
.where('id', id).first();
|
||||
const manualJournal = await ManualJournal.query().where('id', id).first();
|
||||
|
||||
if (!manualJournal) {
|
||||
return res.status(404).send({
|
||||
@@ -823,14 +915,9 @@ export default {
|
||||
.where('model_id', manualJournal.id)
|
||||
.delete();
|
||||
|
||||
await ManualJournal.query()
|
||||
.where('id', manualJournal.id)
|
||||
.delete();
|
||||
await ManualJournal.query().where('id', manualJournal.id).delete();
|
||||
|
||||
await Promise.all([
|
||||
journal.deleteEntries(),
|
||||
journal.saveBalance(),
|
||||
]);
|
||||
await Promise.all([journal.deleteEntries(), journal.saveBalance()]);
|
||||
return res.status(200).send({ id });
|
||||
},
|
||||
},
|
||||
@@ -846,7 +933,8 @@ export default {
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
code: 'validation_error',
|
||||
...validationErrors,
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -866,7 +954,8 @@ export default {
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
code: 'validation_error',
|
||||
...validationErrors,
|
||||
});
|
||||
}
|
||||
const errorReasons = [];
|
||||
@@ -877,8 +966,12 @@ export default {
|
||||
.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);
|
||||
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 });
|
||||
@@ -892,9 +985,9 @@ export default {
|
||||
|
||||
// const journalPoster = new JournalPoster();
|
||||
// const journalCredit = new JournalEntry({
|
||||
// debit:
|
||||
// debit:
|
||||
// account: debitAccount.id,
|
||||
// referenceId:
|
||||
// referenceId:
|
||||
// })
|
||||
|
||||
return res.status(200).send();
|
||||
@@ -914,16 +1007,27 @@ export default {
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
code: 'validation_error',
|
||||
...validationErrors,
|
||||
});
|
||||
}
|
||||
const filter = { ...req.query };
|
||||
const { ManualJournal, AccountTransaction, Account, MediaLink } = req.models;
|
||||
const {
|
||||
ManualJournal,
|
||||
AccountTransaction,
|
||||
Account,
|
||||
MediaLink,
|
||||
} = req.models;
|
||||
|
||||
const manualJournals = await ManualJournal.query()
|
||||
.whereIn('id', filter.ids);
|
||||
const manualJournals = await ManualJournal.query().whereIn(
|
||||
'id',
|
||||
filter.ids
|
||||
);
|
||||
|
||||
const notFoundManualJournals = difference(filter.ids, manualJournals.map(m => m.id));
|
||||
const notFoundManualJournals = difference(
|
||||
filter.ids,
|
||||
manualJournals.map((m) => m.id)
|
||||
);
|
||||
|
||||
if (notFoundManualJournals.length > 0) {
|
||||
return res.status(404).send({
|
||||
@@ -945,14 +1049,10 @@ export default {
|
||||
.whereIn('model_id', filter.ids)
|
||||
.delete();
|
||||
|
||||
await ManualJournal.query()
|
||||
.whereIn('id', filter.ids).delete();
|
||||
await ManualJournal.query().whereIn('id', filter.ids).delete();
|
||||
|
||||
await Promise.all([
|
||||
journal.deleteEntries(),
|
||||
journal.saveBalance(),
|
||||
]);
|
||||
await Promise.all([journal.deleteEntries(), journal.saveBalance()]);
|
||||
return res.status(200).send({ ids: filter.ids });
|
||||
},
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import express from 'express';
|
||||
import {
|
||||
check,
|
||||
validationResult,
|
||||
param,
|
||||
query,
|
||||
} from 'express-validator';
|
||||
import { check, validationResult, param, query } from 'express-validator';
|
||||
import { difference } from 'lodash';
|
||||
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
||||
import JournalPoster from '@/services/Accounting/JournalPoster';
|
||||
@@ -20,7 +15,6 @@ import {
|
||||
DynamicFilterFilterRoles,
|
||||
} from '@/lib/DynamicFilter';
|
||||
|
||||
|
||||
export default {
|
||||
/**
|
||||
* Router constructor method.
|
||||
@@ -28,49 +22,71 @@ export default {
|
||||
router() {
|
||||
const router = express.Router();
|
||||
|
||||
router.post('/',
|
||||
router.post(
|
||||
'/',
|
||||
this.newAccount.validation,
|
||||
asyncMiddleware(this.newAccount.handler));
|
||||
asyncMiddleware(this.newAccount.handler)
|
||||
);
|
||||
|
||||
router.post('/:id',
|
||||
router.post(
|
||||
'/:id',
|
||||
this.editAccount.validation,
|
||||
asyncMiddleware(this.editAccount.handler));
|
||||
asyncMiddleware(this.editAccount.handler)
|
||||
);
|
||||
|
||||
router.get('/:id',
|
||||
router.get(
|
||||
'/:id',
|
||||
this.getAccount.validation,
|
||||
asyncMiddleware(this.getAccount.handler));
|
||||
asyncMiddleware(this.getAccount.handler)
|
||||
);
|
||||
|
||||
router.get('/',
|
||||
router.get(
|
||||
'/',
|
||||
this.getAccountsList.validation,
|
||||
asyncMiddleware(this.getAccountsList.handler));
|
||||
asyncMiddleware(this.getAccountsList.handler)
|
||||
);
|
||||
|
||||
router.delete('/',
|
||||
router.delete(
|
||||
'/',
|
||||
this.deleteBulkAccounts.validation,
|
||||
asyncMiddleware(this.deleteBulkAccounts.handler));
|
||||
asyncMiddleware(this.deleteBulkAccounts.handler)
|
||||
);
|
||||
|
||||
router.delete('/:id',
|
||||
router.delete(
|
||||
'/:id',
|
||||
this.deleteAccount.validation,
|
||||
asyncMiddleware(this.deleteAccount.handler));
|
||||
asyncMiddleware(this.deleteAccount.handler)
|
||||
);
|
||||
|
||||
router.post('/:id/active',
|
||||
router.post(
|
||||
'/:id/active',
|
||||
this.activeAccount.validation,
|
||||
asyncMiddleware(this.activeAccount.handler));
|
||||
asyncMiddleware(this.activeAccount.handler)
|
||||
);
|
||||
|
||||
router.post('/:id/inactive',
|
||||
router.post(
|
||||
'/:id/inactive',
|
||||
this.inactiveAccount.validation,
|
||||
asyncMiddleware(this.inactiveAccount.handler));
|
||||
asyncMiddleware(this.inactiveAccount.handler)
|
||||
);
|
||||
|
||||
router.post('/:id/recalculate-balance',
|
||||
router.post(
|
||||
'/:id/recalculate-balance',
|
||||
this.recalcualteBalanace.validation,
|
||||
asyncMiddleware(this.recalcualteBalanace.handler));
|
||||
asyncMiddleware(this.recalcualteBalanace.handler)
|
||||
);
|
||||
|
||||
router.post('/:id/transfer_account/:toAccount',
|
||||
router.post(
|
||||
'/:id/transfer_account/:toAccount',
|
||||
this.transferToAnotherAccount.validation,
|
||||
asyncMiddleware(this.transferToAnotherAccount.handler));
|
||||
asyncMiddleware(this.transferToAnotherAccount.handler)
|
||||
);
|
||||
|
||||
router.post('/bulk/:type(activate|inactivate)',
|
||||
router.post(
|
||||
'/bulk/:type(activate|inactivate)',
|
||||
this.bulkInactivateAccounts.validation,
|
||||
asyncMiddleware(this.bulkInactivateAccounts.handler));
|
||||
asyncMiddleware(this.bulkInactivateAccounts.handler)
|
||||
);
|
||||
|
||||
return router;
|
||||
},
|
||||
@@ -80,34 +96,34 @@ export default {
|
||||
*/
|
||||
newAccount: {
|
||||
validation: [
|
||||
check('name').exists().isLength({ min: 3 })
|
||||
.trim()
|
||||
.escape(),
|
||||
check('code').optional().isLength({ max: 10 })
|
||||
.trim()
|
||||
.escape(),
|
||||
check('name').exists().isLength({ min: 3, max: 255 }).trim().escape(),
|
||||
check('code').optional().isLength({ min: 3, max: 6, }).trim().escape(),
|
||||
check('account_type_id').exists().isNumeric().toInt(),
|
||||
check('description').optional().trim().escape(),
|
||||
check('description').optional().isLength({ max: 512 }).trim().escape(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
code: 'validation_error',
|
||||
...validationErrors,
|
||||
});
|
||||
}
|
||||
const form = { ...req.body };
|
||||
const { AccountType, Account } = req.models;
|
||||
|
||||
const foundAccountCodePromise = form.code
|
||||
? Account.query().where('code', form.code) : null;
|
||||
? Account.query().where('code', form.code)
|
||||
: null;
|
||||
|
||||
const foundAccountTypePromise = AccountType.query()
|
||||
.findById(form.account_type_id);
|
||||
const foundAccountTypePromise = AccountType.query().findById(
|
||||
form.account_type_id
|
||||
);
|
||||
|
||||
const [foundAccountCode, foundAccountType] = await Promise.all([
|
||||
foundAccountCodePromise, foundAccountTypePromise,
|
||||
foundAccountCodePromise,
|
||||
foundAccountTypePromise,
|
||||
]);
|
||||
|
||||
if (foundAccountCodePromise && foundAccountCode.length > 0) {
|
||||
@@ -132,14 +148,10 @@ export default {
|
||||
editAccount: {
|
||||
validation: [
|
||||
param('id').exists().toInt(),
|
||||
check('name').exists().isLength({ min: 3 })
|
||||
.trim()
|
||||
.escape(),
|
||||
check('code').optional().isLength({ max: 10 })
|
||||
.trim()
|
||||
.escape(),
|
||||
check('name').exists().isLength({ min: 3, max: 255, }).trim().escape(),
|
||||
check('code').optional().isLength({ min: 3, max: 6, }).trim().escape(),
|
||||
check('account_type_id').exists().isNumeric().toInt(),
|
||||
check('description').optional().trim().escape(),
|
||||
check('description').optional().isLength({ max: 512 }).trim().escape(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const { id } = req.params;
|
||||
@@ -147,7 +159,8 @@ export default {
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
code: 'validation_error',
|
||||
...validationErrors,
|
||||
});
|
||||
}
|
||||
const { Account, AccountType } = req.models;
|
||||
@@ -162,12 +175,15 @@ export default {
|
||||
// Validate the account type is not changed.
|
||||
if (account.account_type_id != form.accountTypeId) {
|
||||
errorReasons.push({
|
||||
type: 'NOT.ALLOWED.TO.CHANGE.ACCOUNT.TYPE', code: 100,
|
||||
type: 'NOT.ALLOWED.TO.CHANGE.ACCOUNT.TYPE',
|
||||
code: 100,
|
||||
});
|
||||
}
|
||||
// Validate the account code not exists on the storage.
|
||||
if (form.code && form.code !== account.code) {
|
||||
const foundAccountCode = await Account.query().where('code', form.code).whereNot('id', account.id);
|
||||
const foundAccountCode = await Account.query()
|
||||
.where('code', form.code)
|
||||
.whereNot('id', account.id);
|
||||
|
||||
if (foundAccountCode.length > 0) {
|
||||
errorReasons.push({ type: 'NOT_UNIQUE_CODE', code: 200 });
|
||||
@@ -178,7 +194,10 @@ export default {
|
||||
return res.status(400).send({ errors: errorReasons });
|
||||
}
|
||||
// Update the account on the storage.
|
||||
const updatedAccount = await Account.query().patchAndFetchById(account.id, { ...form });
|
||||
const updatedAccount = await Account.query().patchAndFetchById(
|
||||
account.id,
|
||||
{ ...form }
|
||||
);
|
||||
|
||||
return res.status(200).send({ account: { ...updatedAccount } });
|
||||
},
|
||||
@@ -188,18 +207,16 @@ export default {
|
||||
* Get details of the given account.
|
||||
*/
|
||||
getAccount: {
|
||||
validation: [
|
||||
param('id').toInt(),
|
||||
],
|
||||
validation: [param('id').toInt()],
|
||||
async handler(req, res) {
|
||||
const { id } = req.params;
|
||||
const { Account } = req.models;
|
||||
const account = await Account.query().remember().where('id', id).first();
|
||||
const account = await Account.query().where('id', id).first();
|
||||
|
||||
if (!account) {
|
||||
return res.boom.notFound();
|
||||
}
|
||||
return res.status(200).send({ account: { ...account } });
|
||||
return res.status(200).send({ account });
|
||||
},
|
||||
},
|
||||
|
||||
@@ -207,9 +224,7 @@ export default {
|
||||
* Delete the given account.
|
||||
*/
|
||||
deleteAccount: {
|
||||
validation: [
|
||||
param('id').toInt(),
|
||||
],
|
||||
validation: [param('id').toInt()],
|
||||
async handler(req, res) {
|
||||
const { id } = req.params;
|
||||
const { Account, AccountTransaction } = req.models;
|
||||
@@ -220,11 +235,13 @@ export default {
|
||||
}
|
||||
if (account.predefined) {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'ACCOUNT.PREDEFINED' , code: 200 }],
|
||||
errors: [{ type: 'ACCOUNT.PREDEFINED', code: 200 }],
|
||||
});
|
||||
}
|
||||
const accountTransactions = await AccountTransaction.query()
|
||||
.where('account_id', account.id);
|
||||
const accountTransactions = await AccountTransaction.query().where(
|
||||
'account_id',
|
||||
account.id
|
||||
);
|
||||
|
||||
if (accountTransactions.length > 0) {
|
||||
return res.boom.badRequest(null, {
|
||||
@@ -257,7 +274,8 @@ export default {
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
code: 'validation_error',
|
||||
...validationErrors,
|
||||
});
|
||||
}
|
||||
const filter = {
|
||||
@@ -307,7 +325,7 @@ export default {
|
||||
}
|
||||
const sortByFilter = new DynamicFilterSortBy(
|
||||
filter.column_sort_by,
|
||||
filter.sort_order,
|
||||
filter.sort_order
|
||||
);
|
||||
dynamicFilter.setFilter(sortByFilter);
|
||||
}
|
||||
@@ -316,10 +334,13 @@ export default {
|
||||
if (view && view.roles.length > 0) {
|
||||
const viewFilter = new DynamicFilterViews(
|
||||
mapViewRolesToConditionals(view.roles),
|
||||
view.rolesLogicExpression,
|
||||
view.rolesLogicExpression
|
||||
);
|
||||
if (!viewFilter.validateFilterRoles()) {
|
||||
errorReasons.push({ type: 'VIEW.LOGIC.EXPRESSION.INVALID', code: 400 });
|
||||
errorReasons.push({
|
||||
type: 'VIEW.LOGIC.EXPRESSION.INVALID',
|
||||
code: 400,
|
||||
});
|
||||
}
|
||||
dynamicFilter.setFilter(viewFilter);
|
||||
}
|
||||
@@ -328,12 +349,15 @@ export default {
|
||||
// Validate the accounts resource fields.
|
||||
const filterRoles = new DynamicFilterFilterRoles(
|
||||
mapFilterRolesToDynamicFilter(filter.filter_roles),
|
||||
accountsResource.fields,
|
||||
accountsResource.fields
|
||||
);
|
||||
dynamicFilter.setFilter(filterRoles);
|
||||
|
||||
if (filterRoles.validateFilterRoles().length > 0) {
|
||||
errorReasons.push({ type: 'ACCOUNTS.RESOURCE.HAS.NO.GIVEN.FIELDS', code: 500 });
|
||||
errorReasons.push({
|
||||
type: 'ACCOUNTS.RESOURCE.HAS.NO.GIVEN.FIELDS',
|
||||
code: 500,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (errorReasons.length > 0) {
|
||||
@@ -341,16 +365,18 @@ export default {
|
||||
}
|
||||
|
||||
const query = Account.query()
|
||||
// .remember()
|
||||
.onBuild((builder) => {
|
||||
builder.modify('filterAccountTypes', filter.account_types);
|
||||
builder.withGraphFetched('type');
|
||||
builder.withGraphFetched('balance');
|
||||
// .remember()
|
||||
.onBuild((builder) => {
|
||||
builder.modify('filterAccountTypes', filter.account_types);
|
||||
builder.withGraphFetched('type');
|
||||
builder.withGraphFetched('balance');
|
||||
|
||||
dynamicFilter.buildQuery()(builder);
|
||||
dynamicFilter.buildQuery()(builder);
|
||||
|
||||
// console.log(builder.toKnexQuery().toSQL());
|
||||
}).toKnexQuery().toSQL();
|
||||
// console.log(builder.toKnexQuery().toSQL());
|
||||
})
|
||||
.toKnexQuery()
|
||||
.toSQL();
|
||||
|
||||
console.log(query);
|
||||
|
||||
@@ -370,9 +396,11 @@ export default {
|
||||
|
||||
return res.status(200).send({
|
||||
accounts: nestedAccounts,
|
||||
...(view) ? {
|
||||
customViewId: view.id,
|
||||
} : {},
|
||||
...(view
|
||||
? {
|
||||
customViewId: view.id,
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
},
|
||||
},
|
||||
@@ -381,16 +409,10 @@ export default {
|
||||
* Re-calculates balance of the given account.
|
||||
*/
|
||||
recalcualteBalanace: {
|
||||
validation: [
|
||||
param('id').isNumeric().toInt(),
|
||||
],
|
||||
validation: [param('id').isNumeric().toInt()],
|
||||
async handler(req, res) {
|
||||
const { id } = req.params;
|
||||
const {
|
||||
Account,
|
||||
AccountTransaction,
|
||||
AccountBalance,
|
||||
} = req.models;
|
||||
const { Account, AccountTransaction, AccountBalance } = req.models;
|
||||
const account = await Account.findById(id);
|
||||
|
||||
if (!account) {
|
||||
@@ -398,8 +420,10 @@ export default {
|
||||
errors: [{ type: 'ACCOUNT.NOT.FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
const accountTransactions = AccountTransaction.query()
|
||||
.where('account_id', account.id);
|
||||
const accountTransactions = AccountTransaction.query().where(
|
||||
'account_id',
|
||||
account.id
|
||||
);
|
||||
|
||||
const journalEntries = new JournalPoster();
|
||||
journalEntries.loadFromCollection(accountTransactions);
|
||||
@@ -418,9 +442,7 @@ export default {
|
||||
* Active the given account.
|
||||
*/
|
||||
activeAccount: {
|
||||
validation: [
|
||||
param('id').exists().isNumeric().toInt(),
|
||||
],
|
||||
validation: [param('id').exists().isNumeric().toInt()],
|
||||
async handler(req, res) {
|
||||
const { id } = req.params;
|
||||
const { Account } = req.models;
|
||||
@@ -431,9 +453,7 @@ export default {
|
||||
errors: [{ type: 'ACCOUNT.NOT.FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
await Account.query()
|
||||
.where('id', id)
|
||||
.patch({ active: true });
|
||||
await Account.query().where('id', id).patch({ active: true });
|
||||
|
||||
return res.status(200).send({ id: account.id });
|
||||
},
|
||||
@@ -443,9 +463,7 @@ export default {
|
||||
* Inactive the given account.
|
||||
*/
|
||||
inactiveAccount: {
|
||||
validation: [
|
||||
param('id').exists().isNumeric().toInt(),
|
||||
],
|
||||
validation: [param('id').exists().isNumeric().toInt()],
|
||||
async handler(req, res) {
|
||||
const { id } = req.params;
|
||||
const { Account } = req.models;
|
||||
@@ -456,9 +474,7 @@ export default {
|
||||
errors: [{ type: 'ACCOUNT.NOT.FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
await Account.query()
|
||||
.where('id', id)
|
||||
.patch({ active: false });
|
||||
await Account.query().where('id', id).patch({ active: false });
|
||||
|
||||
return res.status(200).send({ id: account.id });
|
||||
},
|
||||
@@ -477,7 +493,8 @@ export default {
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
code: 'validation_error',
|
||||
...validationErrors,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -505,7 +522,8 @@ export default {
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
code: 'validation_error',
|
||||
...validationErrors,
|
||||
});
|
||||
}
|
||||
const filter = { ids: [], ...req.query };
|
||||
@@ -518,23 +536,27 @@ export default {
|
||||
});
|
||||
const accountsIds = accounts.map((a) => a.id);
|
||||
const notFoundAccounts = difference(filter.ids, accountsIds);
|
||||
const predefinedAccounts = accounts.filter(account => account.predefined);
|
||||
const predefinedAccounts = accounts.filter(
|
||||
(account) => account.predefined
|
||||
);
|
||||
const errorReasons = [];
|
||||
|
||||
if (notFoundAccounts.length > 0) {
|
||||
return res.status(404).send({
|
||||
errors: [{
|
||||
type: 'ACCOUNTS.IDS.NOT.FOUND',
|
||||
code: 200,
|
||||
ids: notFoundAccounts,
|
||||
}],
|
||||
errors: [
|
||||
{
|
||||
type: 'ACCOUNTS.IDS.NOT.FOUND',
|
||||
code: 200,
|
||||
ids: notFoundAccounts,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (predefinedAccounts.length > 0) {
|
||||
errorReasons.push({
|
||||
type: 'ACCOUNT.PREDEFINED',
|
||||
code: 200,
|
||||
ids: predefinedAccounts.map(a => a.id),
|
||||
ids: predefinedAccounts.map((a) => a.id),
|
||||
});
|
||||
}
|
||||
const accountsTransactions = await AccountTransaction.query()
|
||||
@@ -554,14 +576,17 @@ export default {
|
||||
errorReasons.push({
|
||||
type: 'ACCOUNT.HAS.ASSOCIATED.TRANSACTIONS',
|
||||
code: 300,
|
||||
ids: accountsHasTransactions
|
||||
ids: accountsHasTransactions,
|
||||
});
|
||||
}
|
||||
if (errorReasons.length > 0) {
|
||||
return res.status(400).send({ errors: errorReasons });
|
||||
}
|
||||
await Account.query()
|
||||
.whereIn('id', accounts.map((a) => a.id))
|
||||
.whereIn(
|
||||
'id',
|
||||
accounts.map((a) => a.id)
|
||||
)
|
||||
.delete();
|
||||
|
||||
return res.status(200).send();
|
||||
@@ -582,7 +607,8 @@ export default {
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
code: 'validation_error',
|
||||
...validationErrors,
|
||||
});
|
||||
}
|
||||
const filter = {
|
||||
@@ -608,6 +634,6 @@ export default {
|
||||
});
|
||||
|
||||
return res.status(200).send({ ids: storedAccountsIds });
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,19 +1,12 @@
|
||||
import express from 'express';
|
||||
import {
|
||||
check,
|
||||
param,
|
||||
query,
|
||||
validationResult,
|
||||
} from 'express-validator';
|
||||
import { check, param, query, validationResult } from 'express-validator';
|
||||
import moment from 'moment';
|
||||
import { difference, sumBy, omit } from 'lodash';
|
||||
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
||||
import JournalPoster from '@/services/Accounting/JournalPoster';
|
||||
import JournalEntry from '@/services/Accounting/JournalEntry';
|
||||
import JWTAuth from '@/http/middleware/jwtAuth';
|
||||
import {
|
||||
mapViewRolesToConditionals,
|
||||
} from '@/lib/ViewRolesBuilder';
|
||||
import { mapViewRolesToConditionals } from '@/lib/ViewRolesBuilder';
|
||||
import {
|
||||
DynamicFilter,
|
||||
DynamicFilterSortBy,
|
||||
@@ -21,7 +14,6 @@ import {
|
||||
DynamicFilterFilterRoles,
|
||||
} from '@/lib/DynamicFilter';
|
||||
|
||||
|
||||
export default {
|
||||
/**
|
||||
* Router constructor.
|
||||
@@ -30,33 +22,47 @@ export default {
|
||||
const router = express.Router();
|
||||
router.use(JWTAuth);
|
||||
|
||||
router.post('/',
|
||||
router.post(
|
||||
'/',
|
||||
this.newExpense.validation,
|
||||
asyncMiddleware(this.newExpense.handler));
|
||||
asyncMiddleware(this.newExpense.handler)
|
||||
);
|
||||
|
||||
router.post('/:id/publish',
|
||||
router.post(
|
||||
'/:id/publish',
|
||||
this.publishExpense.validation,
|
||||
asyncMiddleware(this.publishExpense.handler));
|
||||
asyncMiddleware(this.publishExpense.handler)
|
||||
);
|
||||
|
||||
router.delete('/:id',
|
||||
router.delete(
|
||||
'/:id',
|
||||
this.deleteExpense.validation,
|
||||
asyncMiddleware(this.deleteExpense.handler));
|
||||
asyncMiddleware(this.deleteExpense.handler)
|
||||
);
|
||||
|
||||
router.delete('/',
|
||||
router.delete(
|
||||
'/',
|
||||
this.deleteBulkExpenses.validation,
|
||||
asyncMiddleware(this.deleteBulkExpenses.handler));
|
||||
asyncMiddleware(this.deleteBulkExpenses.handler)
|
||||
);
|
||||
|
||||
router.post('/:id',
|
||||
router.post(
|
||||
'/:id',
|
||||
this.updateExpense.validation,
|
||||
asyncMiddleware(this.updateExpense.handler));
|
||||
asyncMiddleware(this.updateExpense.handler)
|
||||
);
|
||||
|
||||
router.get('/',
|
||||
router.get(
|
||||
'/',
|
||||
this.listExpenses.validation,
|
||||
asyncMiddleware(this.listExpenses.handler));
|
||||
asyncMiddleware(this.listExpenses.handler)
|
||||
);
|
||||
|
||||
router.get('/:id',
|
||||
router.get(
|
||||
'/:id',
|
||||
this.getExpense.validation,
|
||||
asyncMiddleware(this.getExpense.handler));
|
||||
asyncMiddleware(this.getExpense.handler)
|
||||
);
|
||||
|
||||
return router;
|
||||
},
|
||||
@@ -66,20 +72,27 @@ export default {
|
||||
*/
|
||||
newExpense: {
|
||||
validation: [
|
||||
check('reference_no').optional().trim().escape(),
|
||||
check('reference_no').optional().trim().escape().isLength({
|
||||
max: 255,
|
||||
}),
|
||||
check('payment_date').isISO8601().optional(),
|
||||
check('payment_account_id').exists().isNumeric().toInt(),
|
||||
check('description').optional(),
|
||||
check('currency_code').optional(),
|
||||
check('exchange_rate').optional().isNumeric().toFloat(),
|
||||
check('publish').optional().isBoolean().toBoolean(),
|
||||
|
||||
check('categories').exists().isArray({ min: 1 }),
|
||||
check('categories.*.index').exists().isNumeric().toInt(),
|
||||
check('categories.*.expense_account_id').exists().isNumeric().toInt(),
|
||||
check('categories.*.amount').optional().isNumeric().toFloat(),
|
||||
check('categories.*.description').optional().trim().escape(),
|
||||
|
||||
check('categories.*.amount')
|
||||
.optional({ nullable: true })
|
||||
.isNumeric()
|
||||
.isDecimal()
|
||||
.isFloat({ max: 9999999999.999 }) // 13, 3
|
||||
.toFloat(),
|
||||
check('categories.*.description').optional().trim().escape().isLength({
|
||||
max: 255,
|
||||
}),
|
||||
check('custom_fields').optional().isArray({ min: 0 }),
|
||||
check('custom_fields.*.key').exists().trim().escape(),
|
||||
check('custom_fields.*.value').exists(),
|
||||
@@ -89,7 +102,8 @@ export default {
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
code: 'validation_error',
|
||||
...validationErrors,
|
||||
});
|
||||
}
|
||||
const { user } = req;
|
||||
@@ -103,24 +117,37 @@ export default {
|
||||
...req.body,
|
||||
};
|
||||
const totalAmount = sumBy(form.categories, 'amount');
|
||||
const expenseAccountsIds = form.categories.map((account) => account.expense_account_id)
|
||||
const expenseAccountsIds = form.categories.map(
|
||||
(account) => account.expense_account_id
|
||||
);
|
||||
|
||||
const storedExpenseAccounts = await Account.query().whereIn('id', expenseAccountsIds);
|
||||
const storedExpenseAccountsIds = storedExpenseAccounts.map(a => a.id);
|
||||
|
||||
const notStoredExpensesAccountsIds = difference(expenseAccountsIds, storedExpenseAccountsIds);
|
||||
const storedExpenseAccounts = await Account.query().whereIn(
|
||||
'id',
|
||||
expenseAccountsIds
|
||||
);
|
||||
const storedExpenseAccountsIds = storedExpenseAccounts.map((a) => a.id);
|
||||
|
||||
const notStoredExpensesAccountsIds = difference(
|
||||
expenseAccountsIds,
|
||||
storedExpenseAccountsIds
|
||||
);
|
||||
const errorReasons = [];
|
||||
|
||||
const paymentAccount = await Account.query().where('id', form.payment_account_id).first();
|
||||
const paymentAccount = await Account.query()
|
||||
.where('id', form.payment_account_id)
|
||||
.first();
|
||||
|
||||
if (!paymentAccount) {
|
||||
errorReasons.push({
|
||||
type: 'PAYMENT.ACCOUNT.NOT.FOUND', code: 500,
|
||||
type: 'PAYMENT.ACCOUNT.NOT.FOUND',
|
||||
code: 500,
|
||||
});
|
||||
}
|
||||
if (notStoredExpensesAccountsIds.length > 0) {
|
||||
errorReasons.push({
|
||||
type: 'EXPENSE.ACCOUNTS.IDS.NOT.STORED', code: 400, ids: notStoredExpensesAccountsIds,
|
||||
type: 'EXPENSE.ACCOUNTS.IDS.NOT.STORED',
|
||||
code: 400,
|
||||
ids: notStoredExpensesAccountsIds,
|
||||
});
|
||||
}
|
||||
if (totalAmount <= 0) {
|
||||
@@ -129,7 +156,7 @@ export default {
|
||||
if (errorReasons.length > 0) {
|
||||
return res.status(400).send({ errors: errorReasons });
|
||||
}
|
||||
|
||||
|
||||
const expenseTransaction = await Expense.query().insert({
|
||||
total_amount: totalAmount,
|
||||
payment_account_id: form.payment_account_id,
|
||||
@@ -146,7 +173,7 @@ export default {
|
||||
...category,
|
||||
});
|
||||
storeExpenseCategoriesOper.push(oper);
|
||||
});
|
||||
});
|
||||
|
||||
const accountsDepGraph = await Account.depGraph().query();
|
||||
const journalPoster = new JournalPoster(accountsDepGraph);
|
||||
@@ -155,14 +182,14 @@ export default {
|
||||
referenceType: 'Expense',
|
||||
referenceId: expenseTransaction.id,
|
||||
userId: user.id,
|
||||
draft: !form.publish,
|
||||
draft: !form.publish,
|
||||
};
|
||||
const paymentJournalEntry = new JournalEntry({
|
||||
credit: totalAmount,
|
||||
account: paymentAccount.id,
|
||||
...mixinEntry,
|
||||
});
|
||||
journalPoster.credit(paymentJournalEntry)
|
||||
journalPoster.credit(paymentJournalEntry);
|
||||
|
||||
form.categories.forEach((category) => {
|
||||
const expenseJournalEntry = new JournalEntry({
|
||||
@@ -176,9 +203,9 @@ export default {
|
||||
await Promise.all([
|
||||
...storeExpenseCategoriesOper,
|
||||
journalPoster.saveEntries(),
|
||||
(form.status) && journalPoster.saveBalance(),
|
||||
form.status && journalPoster.saveBalance(),
|
||||
]);
|
||||
|
||||
|
||||
return res.status(200).send({ id: expenseTransaction.id });
|
||||
},
|
||||
},
|
||||
@@ -187,15 +214,14 @@ export default {
|
||||
* Publish the given expense id.
|
||||
*/
|
||||
publishExpense: {
|
||||
validation: [
|
||||
param('id').exists().isNumeric().toInt(),
|
||||
],
|
||||
validation: [param('id').exists().isNumeric().toInt()],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
code: 'validation_error',
|
||||
...validationErrors,
|
||||
});
|
||||
}
|
||||
const { id } = req.params;
|
||||
@@ -248,7 +274,7 @@ export default {
|
||||
* Retrieve paginated expenses list.
|
||||
*/
|
||||
listExpenses: {
|
||||
validation: [
|
||||
validation: [
|
||||
query('page').optional().isNumeric().toInt(),
|
||||
query('page_size').optional().isNumeric().toInt(),
|
||||
|
||||
@@ -263,7 +289,8 @@ export default {
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
code: 'validation_error',
|
||||
...validationErrors,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -282,8 +309,8 @@ export default {
|
||||
.where('name', 'expenses')
|
||||
.withGraphFetched('fields')
|
||||
.first();
|
||||
|
||||
const expensesResourceFields = expensesResource.fields.map(f => f.key);
|
||||
|
||||
const expensesResourceFields = expensesResource.fields.map((f) => f.key);
|
||||
|
||||
if (!expensesResource) {
|
||||
return res.status(400).send({
|
||||
@@ -309,18 +336,21 @@ export default {
|
||||
}
|
||||
const sortByFilter = new DynamicFilterSortBy(
|
||||
filter.column_sort_by,
|
||||
filter.sort_order,
|
||||
filter.sort_order
|
||||
);
|
||||
dynamicFilter.setFilter(sortByFilter);
|
||||
dynamicFilter.setFilter(sortByFilter);
|
||||
}
|
||||
// Custom view roles.
|
||||
if (view && view.roles.length > 0) {
|
||||
const viewFilter = new DynamicFilterViews(
|
||||
mapViewRolesToConditionals(view.roles),
|
||||
view.rolesLogicExpression,
|
||||
view.rolesLogicExpression
|
||||
);
|
||||
if (viewFilter.validateFilterRoles()) {
|
||||
errorReasons.push({ type: 'VIEW.LOGIC.EXPRESSION.INVALID', code: 400 });
|
||||
errorReasons.push({
|
||||
type: 'VIEW.LOGIC.EXPRESSION.INVALID',
|
||||
code: 400,
|
||||
});
|
||||
}
|
||||
dynamicFilter.setFilter(viewFilter);
|
||||
}
|
||||
@@ -328,32 +358,39 @@ export default {
|
||||
if (filter.filter_roles.length > 0) {
|
||||
const filterRoles = new DynamicFilterFilterRoles(
|
||||
mapFilterRolesToDynamicFilter(filter.filter_roles),
|
||||
expensesResource.fields,
|
||||
expensesResource.fields
|
||||
);
|
||||
if (filterRoles.validateFilterRoles().length > 0) {
|
||||
errorReasons.push({ type: 'ACCOUNTS.RESOURCE.HAS.NO.GIVEN.FIELDS', code: 500 });
|
||||
errorReasons.push({
|
||||
type: 'ACCOUNTS.RESOURCE.HAS.NO.GIVEN.FIELDS',
|
||||
code: 500,
|
||||
});
|
||||
}
|
||||
dynamicFilter.setFilter(filterRoles);
|
||||
}
|
||||
if (errorReasons.length > 0) {
|
||||
return res.status(400).send({ errors: errorReasons });
|
||||
}
|
||||
const expenses = await Expense.query().onBuild((builder) => {
|
||||
builder.withGraphFetched('paymentAccount');
|
||||
builder.withGraphFetched('categories.expenseAccount');
|
||||
builder.withGraphFetched('user');
|
||||
dynamicFilter.buildQuery()(builder);
|
||||
}).pagination(filter.page - 1, filter.page_size);;
|
||||
const expenses = await Expense.query()
|
||||
.onBuild((builder) => {
|
||||
builder.withGraphFetched('paymentAccount');
|
||||
builder.withGraphFetched('categories.expenseAccount');
|
||||
builder.withGraphFetched('user');
|
||||
dynamicFilter.buildQuery()(builder);
|
||||
})
|
||||
.pagination(filter.page - 1, filter.page_size);
|
||||
|
||||
return res.status(200).send({
|
||||
expenses: {
|
||||
...expenses,
|
||||
...(view) ? {
|
||||
viewMeta: {
|
||||
viewColumns: view.columns,
|
||||
customViewId: view.id,
|
||||
}
|
||||
} : {},
|
||||
...(view
|
||||
? {
|
||||
viewMeta: {
|
||||
viewColumns: view.columns,
|
||||
customViewId: view.id,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
});
|
||||
},
|
||||
@@ -363,15 +400,14 @@ export default {
|
||||
* Delete the given expense transaction.
|
||||
*/
|
||||
deleteExpense: {
|
||||
validation: [
|
||||
param('id').isNumeric().toInt(),
|
||||
],
|
||||
validation: [param('id').isNumeric().toInt()],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
code: 'validation_error',
|
||||
...validationErrors,
|
||||
});
|
||||
}
|
||||
const { id } = req.params;
|
||||
@@ -385,12 +421,17 @@ export default {
|
||||
const expense = await Expense.query().where('id', id).first();
|
||||
|
||||
if (!expense) {
|
||||
return res.status(404).send({ errors: [{
|
||||
type: 'EXPENSE.NOT.FOUND', code: 200,
|
||||
}] });
|
||||
return res.status(404).send({
|
||||
errors: [
|
||||
{
|
||||
type: 'EXPENSE.NOT.FOUND',
|
||||
code: 200,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
await ExpenseCategory.query().where('expense_id', id).delete();
|
||||
|
||||
|
||||
const deleteExpenseOper = Expense.query().where('id', id).delete();
|
||||
const expenseTransactions = await AccountTransaction.query()
|
||||
.where('reference_type', 'Expense')
|
||||
@@ -437,12 +478,18 @@ export default {
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
code: 'validation_error',
|
||||
...validationErrors,
|
||||
});
|
||||
}
|
||||
const { id } = req.params;
|
||||
const { user } = req;
|
||||
const { Account, Expense, ExpenseCategory, AccountTransaction } = req.models;
|
||||
const {
|
||||
Account,
|
||||
Expense,
|
||||
ExpenseCategory,
|
||||
AccountTransaction,
|
||||
} = req.models;
|
||||
|
||||
const form = {
|
||||
categories: [],
|
||||
@@ -463,39 +510,55 @@ export default {
|
||||
}
|
||||
const errorReasons = [];
|
||||
const paymentAccount = await Account.query()
|
||||
.where('id', form.payment_account_id).first();
|
||||
.where('id', form.payment_account_id)
|
||||
.first();
|
||||
|
||||
if (!paymentAccount) {
|
||||
errorReasons.push({ type: 'PAYMENT.ACCOUNT.NOT.FOUND', code: 400 });
|
||||
}
|
||||
const categoriesHasNoId = form.categories.filter(c => !c.id);
|
||||
const categoriesHasId = form.categories.filter(c => c.id);
|
||||
const categoriesHasNoId = form.categories.filter((c) => !c.id);
|
||||
const categoriesHasId = form.categories.filter((c) => c.id);
|
||||
|
||||
const expenseCategoriesIds = expense.categories.map((c) => c.id);
|
||||
const formExpenseCategoriesIds = categoriesHasId.map(c => c.id);
|
||||
const formExpenseCategoriesIds = categoriesHasId.map((c) => c.id);
|
||||
|
||||
const categoriesIdsDeleted = difference(
|
||||
formExpenseCategoriesIds, expenseCategoriesIds,
|
||||
formExpenseCategoriesIds,
|
||||
expenseCategoriesIds
|
||||
);
|
||||
const categoriesShouldDelete = difference(
|
||||
expenseCategoriesIds, formExpenseCategoriesIds,
|
||||
expenseCategoriesIds,
|
||||
formExpenseCategoriesIds
|
||||
);
|
||||
|
||||
const formExpensesAccountsIds = form.categories.map(c => c.expense_account_id);
|
||||
const storedExpenseAccounts = await Account.query().whereIn('id', formExpensesAccountsIds);
|
||||
const storedExpenseAccountsIds = storedExpenseAccounts.map(a => a.id);
|
||||
const formExpensesAccountsIds = form.categories.map(
|
||||
(c) => c.expense_account_id
|
||||
);
|
||||
const storedExpenseAccounts = await Account.query().whereIn(
|
||||
'id',
|
||||
formExpensesAccountsIds
|
||||
);
|
||||
const storedExpenseAccountsIds = storedExpenseAccounts.map((a) => a.id);
|
||||
|
||||
const expenseAccountsIdsNotFound = difference(
|
||||
formExpensesAccountsIds, storedExpenseAccountsIds,
|
||||
);
|
||||
formExpensesAccountsIds,
|
||||
storedExpenseAccountsIds
|
||||
);
|
||||
const totalAmount = sumBy(form.categories, 'amount');
|
||||
|
||||
if (expenseAccountsIdsNotFound.length > 0) {
|
||||
errorReasons.push({ type: 'EXPENSE.ACCOUNTS.IDS.NOT.FOUND', code: 600, ids: expenseAccountsIdsNotFound })
|
||||
errorReasons.push({
|
||||
type: 'EXPENSE.ACCOUNTS.IDS.NOT.FOUND',
|
||||
code: 600,
|
||||
ids: expenseAccountsIdsNotFound,
|
||||
});
|
||||
}
|
||||
|
||||
if (categoriesIdsDeleted.length > 0) {
|
||||
errorReasons.push({ type: 'EXPENSE.CATEGORIES.IDS.NOT.FOUND', code: 300 });
|
||||
errorReasons.push({
|
||||
type: 'EXPENSE.CATEGORIES.IDS.NOT.FOUND',
|
||||
code: 300,
|
||||
});
|
||||
}
|
||||
if (totalAmount <= 0) {
|
||||
errorReasons.push({ type: 'TOTAL.AMOUNT.EQUALS.ZERO', code: 500 });
|
||||
@@ -504,12 +567,13 @@ export default {
|
||||
if (errorReasons.length > 0) {
|
||||
return res.status(400).send({ errors: errorReasons });
|
||||
}
|
||||
const expenseCategoriesMap = new Map(expense.categories
|
||||
.map(category => [category.id, category]));
|
||||
const expenseCategoriesMap = new Map(
|
||||
expense.categories.map((category) => [category.id, category])
|
||||
);
|
||||
|
||||
const categoriesInsertOpers = [];
|
||||
const categoriesUpdateOpers = [];
|
||||
|
||||
|
||||
categoriesHasNoId.forEach((category) => {
|
||||
const oper = ExpenseCategory.query().insert({
|
||||
...category,
|
||||
@@ -518,26 +582,31 @@ export default {
|
||||
categoriesInsertOpers.push(oper);
|
||||
});
|
||||
|
||||
categoriesHasId.forEach((category) => {
|
||||
const oper = ExpenseCategory.query().where('id', category.id)
|
||||
categoriesHasId.forEach((category) => {
|
||||
const oper = ExpenseCategory.query()
|
||||
.where('id', category.id)
|
||||
.patch({
|
||||
...omit(category, ['id']),
|
||||
});
|
||||
categoriesUpdateOpers.push(oper);
|
||||
});
|
||||
|
||||
const updateExpenseOper = Expense.query().where('id', id)
|
||||
const updateExpenseOper = Expense.query()
|
||||
.where('id', id)
|
||||
.update({
|
||||
payment_date: moment(form.payment_date).format('YYYY-MM-DD'),
|
||||
total_amount: totalAmount,
|
||||
description: form.description,
|
||||
payment_account_id: form.payment_account_id,
|
||||
reference_no: form.reference_no,
|
||||
})
|
||||
});
|
||||
|
||||
const deleteCategoriesOper = (categoriesShouldDelete.length > 0) ?
|
||||
ExpenseCategory.query().whereIn('id', categoriesShouldDelete).delete() :
|
||||
Promise.resolve();
|
||||
const deleteCategoriesOper =
|
||||
categoriesShouldDelete.length > 0
|
||||
? ExpenseCategory.query()
|
||||
.whereIn('id', categoriesShouldDelete)
|
||||
.delete()
|
||||
: Promise.resolve();
|
||||
|
||||
// Update the journal entries.
|
||||
const transactions = await AccountTransaction.query()
|
||||
@@ -555,7 +624,7 @@ export default {
|
||||
referenceType: 'Expense',
|
||||
referenceId: expense.id,
|
||||
userId: user.id,
|
||||
draft: !form.publish,
|
||||
draft: !form.publish,
|
||||
};
|
||||
const paymentJournalEntry = new JournalEntry({
|
||||
credit: totalAmount,
|
||||
@@ -573,7 +642,7 @@ export default {
|
||||
});
|
||||
journal.debit(entry);
|
||||
});
|
||||
|
||||
|
||||
await Promise.all([
|
||||
...categoriesInsertOpers,
|
||||
...categoriesUpdateOpers,
|
||||
@@ -581,7 +650,7 @@ export default {
|
||||
deleteCategoriesOper,
|
||||
|
||||
journal.saveEntries(),
|
||||
(form.status) && journal.saveBalance(),
|
||||
form.status && journal.saveBalance(),
|
||||
]);
|
||||
return res.status(200).send({ id });
|
||||
},
|
||||
@@ -591,15 +660,14 @@ export default {
|
||||
* Retrieve details of the given expense id.
|
||||
*/
|
||||
getExpense: {
|
||||
validation: [
|
||||
param('id').exists().isNumeric().toInt(),
|
||||
],
|
||||
validation: [param('id').exists().isNumeric().toInt()],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
code: 'validation_error',
|
||||
...validationErrors,
|
||||
});
|
||||
}
|
||||
const { id } = req.params;
|
||||
@@ -624,9 +692,9 @@ export default {
|
||||
|
||||
return res.status(200).send({
|
||||
expense: {
|
||||
...expense,
|
||||
...expense.toJSON(),
|
||||
journalEntries,
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
@@ -644,16 +712,16 @@ export default {
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
code: 'validation_error',
|
||||
...validationErrors,
|
||||
});
|
||||
}
|
||||
const filter = { ...req.query };
|
||||
const { Expense, AccountTransaction, Account, MediaLink } = req.models;
|
||||
|
||||
const expenses = await Expense.query()
|
||||
.whereIn('id', filter.ids)
|
||||
const expenses = await Expense.query().whereIn('id', filter.ids);
|
||||
|
||||
const storedExpensesIds = expenses.map(e => e.id);
|
||||
const storedExpensesIds = expenses.map((e) => e.id);
|
||||
const notFoundExpenses = difference(filter.ids, storedExpensesIds);
|
||||
|
||||
if (notFoundExpenses.length > 0) {
|
||||
@@ -663,11 +731,12 @@ export default {
|
||||
}
|
||||
|
||||
const deleteExpensesOper = Expense.query()
|
||||
.whereIn('id', storedExpensesIds).delete();
|
||||
.whereIn('id', storedExpensesIds)
|
||||
.delete();
|
||||
|
||||
const transactions = await AccountTransaction.query()
|
||||
.whereIn('reference_type', ['Expense'])
|
||||
.whereIn('reference_id', filter.ids)
|
||||
.whereIn('reference_id', filter.ids);
|
||||
|
||||
const accountsDepGraph = await Account.depGraph().query().remember();
|
||||
const journal = new JournalPoster(accountsDepGraph);
|
||||
@@ -686,6 +755,6 @@ export default {
|
||||
journal.saveBalance(),
|
||||
]);
|
||||
return res.status(200).send({ ids: filter.ids });
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user