feat: Expenses resource.

This commit is contained in:
Ahmed Bouhuolia
2020-06-07 22:20:52 +02:00
parent 54f10f6a9e
commit fe240c058b
14 changed files with 1226 additions and 288 deletions

View File

@@ -6,16 +6,21 @@ import {
validationResult,
} from 'express-validator';
import moment from 'moment';
import { difference, chain, omit } from 'lodash';
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 ResourceCustomFieldRepository from '@/services/CustomFields/ResourceCustomFieldRepository';
import {
validateViewRoles,
mapViewRolesToConditionals,
} from '@/lib/ViewRolesBuilder';
import {
DynamicFilter,
DynamicFilterSortBy,
DynamicFilterViews,
DynamicFilterFilterRoles,
} from '@/lib/DynamicFilter';
export default {
/**
@@ -37,10 +42,6 @@ export default {
this.deleteExpense.validation,
asyncMiddleware(this.deleteExpense.handler));
router.post('/bulk',
this.bulkAddExpenses.validation,
asyncMiddleware(this.bulkAddExpenses.handler));
router.post('/:id',
this.updateExpense.validation,
asyncMiddleware(this.updateExpense.handler));
@@ -49,9 +50,9 @@ export default {
this.listExpenses.validation,
asyncMiddleware(this.listExpenses.handler));
// router.get('/:id',
// this.getExpense.validation,
// asyncMiddleware(this.getExpense.handler));
router.get('/:id',
this.getExpense.validation,
asyncMiddleware(this.getExpense.handler));
return router;
},
@@ -61,15 +62,21 @@ export default {
*/
newExpense: {
validation: [
check('date').optional(),
check('reference_no').optional().trim().escape(),
check('payment_date').isISO8601().optional(),
check('payment_account_id').exists().isNumeric().toInt(),
check('expense_account_id').exists().isNumeric().toInt(),
check('description').optional(),
check('amount').exists().isNumeric().toFloat(),
check('currency_code').optional(),
check('exchange_rate').optional().isNumeric().toFloat(),
check('publish').optional().isBoolean().toBoolean(),
check('custom_fields').optional().isArray({ min: 1 }),
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('custom_fields').optional().isArray({ min: 0 }),
check('custom_fields.*.key').exists().trim().escape(),
check('custom_fields.*.value').exists(),
],
@@ -81,170 +88,94 @@ export default {
code: 'validation_error', ...validationErrors,
});
}
const { user } = req;
const { Expense, ExpenseCategory, Account } = req.models;
const form = {
date: new Date(),
published: false,
custom_fields: [],
categories: [],
...req.body,
};
const { Account, Expense } = req.models;
// Convert the date to the general format.
form.date = moment(form.date).format('YYYY-MM-DD');
const totalAmount = sumBy(form.categories, 'amount');
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 errorReasons = [];
const paymentAccount = await Account.query()
.findById(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: 100 });
errorReasons.push({
type: 'PAYMENT.ACCOUNT.NOT.FOUND', code: 500,
});
}
const expenseAccount = await Account.query().findById(form.expense_account_id).first();
if (!expenseAccount) {
errorReasons.push({ type: 'EXPENSE.ACCOUNT.NOT.FOUND', code: 200 });
if (notStoredExpensesAccountsIds.length > 0) {
errorReasons.push({
type: 'EXPENSE.ACCOUNTS.IDS.NOT.STORED', code: 400, ids: notStoredExpensesAccountsIds,
});
}
if (totalAmount <= 0) {
errorReasons.push({ type: 'TOTAL.AMOUNT.EQUALS.ZERO', code: 300 });
}
// const customFields = new ResourceCustomFieldRepository(Expense);
// await customFields.load();
// if (customFields.validateExistCustomFields()) {
// errorReasons.push({ type: 'CUSTOM.FIELDS.SLUGS.NOT.EXISTS', code: 400 });
// }
if (errorReasons.length > 0) {
return res.status(400).send({ errors: errorReasons });
}
const expenseTransaction = await Expense.query().insertAndFetch({
...omit(form, ['custom_fields']),
const expenseTransaction = await Expense.query().insert({
total_amount: totalAmount,
payment_account_id: form.payment_account_id,
reference_no: form.reference_no,
description: form.description,
payment_date: moment(form.payment_date).format('YYYY-MM-DD'),
user_id: user.id,
});
// customFields.fillCustomFields(expenseTransaction.id, form.custom_fields);
const storeExpenseCategoriesOper = [];
const journalEntries = new JournalPoster();
const creditEntry = new JournalEntry({
credit: form.amount,
form.categories.forEach((category) => {
const oper = ExpenseCategory.query().insert({
expense_id: expenseTransaction.id,
...category,
});
storeExpenseCategoriesOper.push(oper);
});
const accountsDepGraph = await Account.depGraph().query();
const journalPoster = new JournalPoster(accountsDepGraph);
const mixinEntry = {
referenceType: 'Expense',
referenceId: expenseTransaction.id,
referenceType: Expense.referenceType,
date: form.date,
account: expenseAccount.id,
accountNormal: 'debit',
draft: !form.published,
});
const debitEntry = new JournalEntry({
debit: form.amount,
referenceId: expenseTransaction.id,
referenceType: Expense.referenceType,
date: form.date,
userId: user.id,
draft: !form.publish,
};
const paymentJournalEntry = new JournalEntry({
credit: totalAmount,
account: paymentAccount.id,
accountNormal: 'debit',
draft: !form.published,
...mixinEntry,
});
journalEntries.credit(creditEntry);
journalEntries.debit(debitEntry);
journalPoster.credit(paymentJournalEntry)
form.categories.forEach((category) => {
const expenseJournalEntry = new JournalEntry({
account: category.expense_account_id,
debit: category.amount,
note: category.description,
...mixinEntry,
});
journalPoster.debit(expenseJournalEntry);
});
await Promise.all([
// customFields.saveCustomFields(expenseTransaction.id),
journalEntries.saveEntries(),
journalEntries.saveBalance(),
]);
return res.status(200).send({ id: expenseTransaction.id });
},
},
/**
* Bulk add expneses to the given accounts.
*/
bulkAddExpenses: {
validation: [
check('expenses').exists().isArray({ min: 1 }),
check('expenses.*.date').optional().isISO8601(),
check('expenses.*.payment_account_id').exists().isNumeric().toInt(),
check('expenses.*.expense_account_id').exists().isNumeric().toInt(),
check('expenses.*.description').optional(),
check('expenses.*.amount').exists().isNumeric().toFloat(),
check('expenses.*.currency_code').optional(),
check('expenses.*.exchange_rate').optional().isNumeric().toFloat(),
],
async handler(req, res) {
const validationErrors = validationResult(req);
if (!validationErrors.isEmpty()) {
return res.boom.badData(null, {
code: 'validation_error', ...validationErrors,
});
}
const { Account, Expense } = req.models;
const form = { ...req.body };
const errorReasons = [];
const paymentAccountsIds = chain(form.expenses)
.map((e) => e.payment_account_id).uniq().value();
const expenseAccountsIds = chain(form.expenses)
.map((e) => e.expense_account_id).uniq().value();
const [expensesAccounts, paymentAccounts] = await Promise.all([
Account.query().whereIn('id', expenseAccountsIds),
Account.query().whereIn('id', paymentAccountsIds),
]);
const storedExpensesAccountsIds = expensesAccounts.map((a) => a.id);
const storedPaymentAccountsIds = paymentAccounts.map((a) => a.id);
const notFoundPaymentAccountsIds = difference(expenseAccountsIds, storedExpensesAccountsIds);
const notFoundExpenseAccountsIds = difference(paymentAccountsIds, storedPaymentAccountsIds);
if (notFoundPaymentAccountsIds.length > 0) {
errorReasons.push({
type: 'PAYMENY.ACCOUNTS.NOT.FOUND',
code: 100,
accounts: notFoundPaymentAccountsIds,
});
}
if (notFoundExpenseAccountsIds.length > 0) {
errorReasons.push({
type: 'EXPENSE.ACCOUNTS.NOT.FOUND',
code: 200,
accounts: notFoundExpenseAccountsIds,
});
}
if (errorReasons.length > 0) {
return res.boom.badRequest(null, { reasons: errorReasons });
}
const expenseSaveOpers = [];
const journalPoster = new JournalPoster();
form.expenses.forEach(async (expense) => {
const expenseSaveOper = Expense.query().insert({ ...expense });
expenseSaveOpers.push(expenseSaveOper);
});
// Wait unit save all expense transactions.
const savedExpenseTransactions = await Promise.all(expenseSaveOpers);
savedExpenseTransactions.forEach((expense) => {
const date = moment(expense.date).format('YYYY-DD-MM');
const debit = new JournalEntry({
debit: expense.amount,
referenceId: expense.id,
referenceType: Expense.referenceType,
account: expense.payment_account_id,
accountNormal: 'debit',
date,
});
const credit = new JournalEntry({
credit: expense.amount,
referenceId: expense.id,
referenceType: Expense.referenceId,
account: expense.expense_account_id,
accountNormal: 'debit',
date,
});
journalPoster.credit(credit);
journalPoster.debit(debit);
});
// Save expense journal entries and balance change.
await Promise.all([
...storeExpenseCategoriesOper,
journalPoster.saveEntries(),
journalPoster.saveBalance(),
(form.status) && journalPoster.saveBalance(),
]);
return res.status(200).send();
return res.status(200).send({ id: expenseTransaction.id });
},
},
@@ -263,16 +194,13 @@ export default {
code: 'validation_error', ...validationErrors,
});
}
const { id } = req.params;
const { Expense, AccountTransaction } = req.models;
const errorReasons = [];
const expense = await Expense.query().findById(id);
const errorReasons = [];
if (!expense) {
errorReasons.push({ type: 'EXPENSE.NOT.FOUND', code: 100 });
}
if (errorReasons.length > 0) {
return res.status(400).send({ errors: errorReasons });
}
if (expense.published) {
@@ -281,18 +209,33 @@ export default {
if (errorReasons.length > 0) {
return res.status(400).send({ errors: errorReasons });
}
const transactions = await AccountTransaction.query()
.whereIn('reference_type', ['Expense'])
.where('reference_id', expense.id)
.withGraphFetched('account.type');
await AccountTransaction.query()
const accountsDepGraph = await Account.depGraph().query().remember();
const journal = new JournalPoster(accountsDepGraph);
journal.loadEntries(transactions);
journal.calculateEntriesBalanceChange();
const updateAccTransactionsOper = AccountTransaction.query()
.where('reference_id', expense.id)
.where('reference_type', 'Expense')
.patch({
draft: false,
});
await Expense.query()
const updateExpenseOper = Expense.query()
.where('id', expense.id)
.update({ published: true });
await Promise.all([
updateAccTransactionsOper,
updateExpenseOper,
journal.saveBalance(),
]);
return res.status(200).send();
},
},
@@ -301,25 +244,15 @@ export default {
* Retrieve paginated expenses list.
*/
listExpenses: {
validation: [
query('expense_account_id').optional().isNumeric().toInt(),
query('payment_account_id').optional().isNumeric().toInt(),
query('note').optional(),
query('range_from').optional().isNumeric().toFloat(),
query('range_to').optional().isNumeric().toFloat(),
query('date_from').optional().isISO8601(),
query('date_to').optional().isISO8601(),
query('column_sort_order').optional().isIn(['created_at', 'date', 'amount']),
query('sort_order').optional().isIn(['desc', 'asc']),
validation: [
query('page').optional().isNumeric().toInt(),
query('page_size').optional().isNumeric().toInt(),
query('custom_view_id').optional().isNumeric().toInt(),
query('filter_roles').optional().isArray(),
query('filter_roles.*.field_key').exists().escape().trim(),
query('filter_roles.*.value').exists().escape().trim(),
query('filter_roles.*.comparator').exists().escape().trim(),
query('filter_roles.*.index').exists().isNumeric().toInt(),
query('custom_view_id').optional().isNumeric().toInt(),
query('stringified_filter_roles').optional().isJSON(),
query('column_sort_by').optional(),
query('sort_order').optional().isIn(['desc', 'asc']),
],
async handler(req, res) {
const validationErrors = validationResult(req);
@@ -329,77 +262,99 @@ export default {
code: 'validation_error', ...validationErrors,
});
}
const filter = {
page_size: 10,
sort_order: 'asc',
filter_roles: [],
page_size: 15,
page: 1,
...req.query,
};
const { Resource, View, Expense } = req.models;
const errorReasons = [];
const expenseResource = await Resource.query().where('name', 'expenses').first();
const { Resource, Expense, View } = req.models;
if (!expenseResource) {
errorReasons.push({ type: 'EXPENSE_RESOURCE_NOT_FOUND', code: 300 });
const expensesResource = await Resource.query()
.remember()
.where('name', 'expenses')
.withGraphFetched('fields')
.first();
const expensesResourceFields = expensesResource.fields.map(f => f.key);
if (!expensesResource) {
return res.status(400).send({
errors: [{ type: 'EXPENSES.RESOURCE.NOT.FOUND', code: 200 }],
});
}
const view = await View.query().onBuild((builder) => {
if (filter.csutom_view_id) {
builder.where('id', filter.csutom_view_id);
} else {
builder.where('favourite', true);
}
builder.withGraphFetched('roles.field');
builder.withGraphFetched('columns');
builder.first();
});
const dynamicFilter = new DynamicFilter(Expense.tableName);
// Column sorting.
if (filter.column_sort_by) {
if (expensesResourceFields.indexOf(filter.column_sort_by) === -1) {
errorReasons.push({ type: 'COLUMN.SORT.ORDER.NOT.FOUND', code: 300 });
}
const sortByFilter = new DynamicFilterSortBy(
filter.column_sort_by,
filter.sort_order,
);
dynamicFilter.setFilter(sortByFilter);
}
// Custom view roles.
if (view && view.roles.length > 0) {
const viewFilter = new DynamicFilterViews(
mapViewRolesToConditionals(view.roles),
view.rolesLogicExpression,
);
if (viewFilter.validateFilterRoles()) {
errorReasons.push({ type: 'VIEW.LOGIC.EXPRESSION.INVALID', code: 400 });
}
dynamicFilter.setFilter(viewFilter);
}
// Filter roles.
if (filter.filter_roles.length > 0) {
const filterRoles = new DynamicFilterFilterRoles(
mapFilterRolesToDynamicFilter(filter.filter_roles),
expensesResource.fields,
);
if (filterRoles.validateFilterRoles().length > 0) {
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 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', expenseResource.id);
builder.withGraphFetched('viewRoles.field');
builder.withGraphFetched('columns');
builder.first();
});
let viewConditionals = [];
if (view && view.viewRoles.length > 0) {
viewConditionals = mapViewRolesToConditionals(view.viewRoles);
if (!validateViewRoles(viewConditionals, view.rolesLogicExpression)) {
errorReasons.push({ type: 'VIEW.LOGIC.EXPRESSION.INVALID', code: 400 });
}
}
if (!view && filter.custom_view_id) {
errorReasons.push({ type: 'VIEW_NOT_FOUND', code: 100 });
}
if (errorReasons.length > 0) {
return res.boom.badRequest(null, { errors: errorReasons });
}
const expenses = await Expense.query().onBuild((builder) => {
builder.withGraphFetched('paymentAccount');
builder.withGraphFetched('expenseAccount');
builder.withGraphFetched('categories');
builder.withGraphFetched('user');
if (viewConditionals.length) {
builder.modify('viewRolesBuilder', viewConditionals, view.rolesLogicExpression);
}
builder.modify('filterByAmountRange', filter.range_from, filter.to_range);
builder.modify('filterByDateRange', filter.date_from, filter.date_to);
builder.modify('filterByExpenseAccount', filter.expense_account_id);
builder.modify('filterByPaymentAccount', filter.payment_account_id);
builder.modify('orderBy', filter.column_sort_order, filter.sort_order);
}).page(filter.page - 1, filter.page_size);
dynamicFilter.buildQuery()(builder);
}).pagination(filter.page - 1, filter.page_size);;
return res.status(200).send({
...(view) ? {
customViewId: view.id,
viewColumns: view.columns,
viewConditionals,
} : {},
expenses,
page_size: filter.page_size,
page: filter.page,
...(view) ? {
viewColumns: view.columns,
customViewId: view.id,
} : {},
});
},
},
/**
* Delete the given account.
* Delete the given expense transaction.
*/
deleteExpense: {
validation: [
@@ -414,26 +369,37 @@ export default {
});
}
const { id } = req.params;
const { Expense, AccountTransaction } = req.models;
const expenseTransaction = await Expense.query().findById(id);
const {
Expense,
ExpenseCategory,
AccountTransaction,
Account,
} = req.models;
if (!expenseTransaction) {
return res.status(404).send({
errors: [{ type: 'EXPENSE.TRANSACTION.NOT.FOUND', code: 100 }],
});
const expense = await Expense.query().where('id', id).first();
if (!expense) {
return res.status(404).send({ errors: [{
type: 'EXPENSE.NOT.FOUND', code: 200,
}] });
}
const expenseEntries = await AccountTransaction.query()
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')
.where('reference_id', expenseTransaction.id);
.where('reference_id', expense.id);
const expenseEntriesCollect = new JournalPoster();
expenseEntriesCollect.loadEntries(expenseEntries);
expenseEntriesCollect.reverseEntries();
const accountsDepGraph = await Account.depGraph().query().remember();
const journalEntries = new JournalPoster(accountsDepGraph);
journalEntries.loadEntries(expenseTransactions);
journalEntries.removeEntries();
await Promise.all([
Expense.query().findById(expenseTransaction.id).delete(),
expenseEntriesCollect.deleteEntries(),
expenseEntriesCollect.saveBalance(),
deleteExpenseOper,
journalEntries.deleteEntries(),
journalEntries.saveBalance(),
]);
return res.status(200).send();
},
@@ -445,13 +411,20 @@ export default {
updateExpense: {
validation: [
param('id').isNumeric().toInt(),
check('date').optional().isISO8601(),
check('reference_no').optional().trim().escape(),
check('payment_date').isISO8601().optional(),
check('payment_account_id').exists().isNumeric().toInt(),
check('expense_account_id').exists().isNumeric().toInt(),
check('description').optional(),
check('amount').exists().isNumeric().toFloat(),
check('currency_code').optional(),
check('exchange_rate').optional().isNumeric().toFloat(),
check('publish').optional().isBoolean().toBoolean(),
check('categories').exists().isArray({ min: 1 }),
check('categories.*.id').optional().isNumeric().toInt(),
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(),
],
async handler(req, res) {
const validationErrors = validationResult(req);
@@ -462,14 +435,149 @@ export default {
});
}
const { id } = req.params;
const { Expense } = req.models;
const expenseTransaction = await Expense.query().findById(id);
const { user } = req;
const { Account, Expense, ExpenseCategory, AccountTransaction } = req.models;
if (!expenseTransaction) {
const form = {
categories: [],
...req.body,
};
if (!Array.isArray(form.categories)) {
form.categories = [form.categories];
}
const expense = await Expense.query()
.where('id', id)
.withGraphFetched('categories')
.first();
if (!expense) {
return res.status(404).send({
errors: [{ type: 'EXPENSE.TRANSACTION.NOT.FOUND', code: 100 }],
errors: [{ type: 'EXPENSE.NOT.FOUND', code: 200 }],
});
}
const errorReasons = [];
const paymentAccount = await Account.query()
.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 expenseCategoriesIds = expense.categories.map((c) => c.id);
const formExpenseCategoriesIds = categoriesHasId.map(c => c.id);
const categoriesIdsDeleted = difference(
formExpenseCategoriesIds, expenseCategoriesIds,
);
const categoriesShouldDelete = difference(
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 expenseAccountsIdsNotFound = difference(
formExpensesAccountsIds, storedExpenseAccountsIds,
);
const totalAmount = sumBy(form.categories, 'amount');
if (expenseAccountsIdsNotFound.length > 0) {
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 });
}
if (totalAmount <= 0) {
errorReasons.push({ type: 'TOTAL.AMOUNT.EQUALS.ZERO', code: 500 });
}
// Handle all error reasons.
if (errorReasons.length > 0) {
return res.status(400).send({ errors: errorReasons });
}
const expenseCategoriesMap = new Map(expense.categories
.map(category => [category.id, category]));
const categoriesInsertOpers = [];
const categoriesUpdateOpers = [];
categoriesHasNoId.forEach((category) => {
const oper = ExpenseCategory.query().insert({
...category,
expense_id: expense.id,
});
categoriesInsertOpers.push(oper);
});
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)
.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();
// Update the journal entries.
const transactions = await AccountTransaction.query()
.whereIn('reference_type', ['Expense'])
.where('reference_id', expense.id)
.withGraphFetched('account.type');
const accountsDepGraph = await Account.depGraph().query().remember();
const journal = new JournalPoster(accountsDepGraph);
journal.loadEntries(transactions);
journal.removeEntries();
const mixinEntry = {
referenceType: 'Expense',
referenceId: expense.id,
userId: user.id,
draft: !form.publish,
};
const paymentJournalEntry = new JournalEntry({
credit: totalAmount,
account: paymentAccount.id,
...mixinEntry,
});
journal.credit(paymentJournalEntry);
form.categories.forEach((category) => {
const entry = new JournalEntry({
account: category.expense_account_id,
debit: category.amount,
note: category.description,
...mixinEntry,
});
journal.debit(entry);
});
await Promise.all([
...categoriesInsertOpers,
...categoriesUpdateOpers,
updateExpenseOper,
deleteCategoriesOper,
journal.saveEntries(),
(form.status) && journal.saveBalance(),
]);
return res.status(200).send({ id });
},
},
@@ -489,26 +597,30 @@ export default {
});
}
const { id } = req.params;
const { Expense } = req.models;
const expenseTransaction = await Expense.query().findById(id);
const { Expense, AccountTransaction } = req.models;
if (!expenseTransaction) {
const expense = await Expense.query()
.where('id', id)
.withGraphFetched('categories')
.withGraphFetched('paymentAccount')
.withGraphFetched('user')
.first();
if (!expense) {
return res.status(404).send({
errors: [{ type: 'EXPENSE.TRANSACTION.NOT.FOUND', code: 100 }],
errors: [{ type: 'EXPENSE.NOT.FOUND', code: 200 }],
});
}
const expenseCFMetadataRepo = new ResourceCustomFieldRepository(Expense);
await expenseCFMetadataRepo.load();
await expenseCFMetadataRepo.fetchCustomFieldsMetadata(expenseTransaction.id);
const expenseCusFieldsMetadata = expenseCFMetadataRepo.getMetadata(expenseTransaction.id);
const journalEntries = await AccountTransaction.query()
.where('reference_id', expense.id)
.where('reference_type', 'Expense');
return res.status(200).send({
...expenseTransaction,
custom_fields: [
...expenseCusFieldsMetadata.toArray(),
],
expense: {
...expense,
journalEntries,
}
});
},
},

View File

@@ -12,6 +12,10 @@ export default {
router() {
const router = express.Router();
router.get('/:resource_slug/data',
this.resourceData.validation,
asyncMiddleware(this.resourceData.handler));
router.get('/:resource_slug/columns',
this.resourceColumns.validation,
asyncMiddleware(this.resourceColumns.handler));
@@ -23,6 +27,26 @@ export default {
return router;
},
/**
* Retrieve resource data of the given resource key/slug.
*/
resourceData: {
validation: [
param('resource_slug').trim().escape().exists(),
],
async handler(req, res) {
const { AccountType } = req.models;
const { resource_slug: resourceSlug } = req.params;
const data = await AccountType.query();
return res.status(200).send({
data,
resource_slug: resourceSlug,
});
},
},
/**
* Retrieve resource columns of the given resource.
*/

View File

@@ -13,12 +13,12 @@ import Views from '@/http/controllers/Views';
// import CustomFields from '@/http/controllers/Fields';
import Accounting from '@/http/controllers/Accounting';
import FinancialStatements from '@/http/controllers/FinancialStatements';
// import Expenses from '@/http/controllers/Expenses';
import Expenses from '@/http/controllers/Expenses';
import Options from '@/http/controllers/Options';
// import Budget from '@/http/controllers/Budget';
// import BudgetReports from '@/http/controllers/BudgetReports';
import Currencies from '@/http/controllers/Currencies';
// import Customers from '@/http/controllers/Customers';
import Customers from '@/http/controllers/Customers';
// import Suppliers from '@/http/controllers/Suppliers';
// import Bills from '@/http/controllers/Bills';
// import CurrencyAdjustment from './controllers/CurrencyAdjustment';
@@ -52,11 +52,11 @@ export default (app) => {
// app.use('/api/fields', CustomFields.router());
dashboard.use('/api/items', Items.router());
dashboard.use('/api/item_categories', ItemCategories.router());
// app.use('/api/expenses', Expenses.router());
dashboard.use('/api/expenses', Expenses.router());
dashboard.use('/api/financial_statements', FinancialStatements.router());
dashboard.use('/api/options', Options.router());
// app.use('/api/budget_reports', BudgetReports.router());
// app.use('/api/customers', Customers.router());
// dashboard.use('/api/customers', Customers.router());
// app.use('/api/suppliers', Suppliers.router());
// app.use('/api/bills', Bills.router());
// app.use('/api/budget', Budget.router());