mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-21 15:20:34 +00:00
Merge branch 'master' of https://github.com/abouolia/Ratteb into feature/editItem
This commit is contained in:
@@ -74,6 +74,7 @@
|
|||||||
"react-loadable": "^5.5.0",
|
"react-loadable": "^5.5.0",
|
||||||
"react-query": "^1.3.3",
|
"react-query": "^1.3.3",
|
||||||
"react-redux": "^7.1.3",
|
"react-redux": "^7.1.3",
|
||||||
|
"react-router-breadcrumbs-hoc": "^3.2.10",
|
||||||
"react-router-dom": "^5.1.2",
|
"react-router-dom": "^5.1.2",
|
||||||
"react-scrollbars-custom": "^4.0.21",
|
"react-scrollbars-custom": "^4.0.21",
|
||||||
"react-sortablejs": "^2.0.11",
|
"react-sortablejs": "^2.0.11",
|
||||||
|
|||||||
@@ -10,8 +10,13 @@ export default class NestedSet {
|
|||||||
id: 'id',
|
id: 'id',
|
||||||
...options,
|
...options,
|
||||||
};
|
};
|
||||||
|
this.items = items || [];
|
||||||
|
this.tree = this.linkChildren();
|
||||||
|
}
|
||||||
|
|
||||||
|
setItems(items) {
|
||||||
this.items = items;
|
this.items = items;
|
||||||
this.collection = {};
|
this.tree = this.linkChildren();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -23,37 +28,71 @@ export default class NestedSet {
|
|||||||
const map = {};
|
const map = {};
|
||||||
this.items.forEach((item) => {
|
this.items.forEach((item) => {
|
||||||
map[item.id] = item;
|
map[item.id] = item;
|
||||||
map[item.id].children = [];
|
map[item.id].children = {};
|
||||||
});
|
});
|
||||||
|
|
||||||
this.items.forEach((item) => {
|
this.items.forEach((item) => {
|
||||||
const parentNodeId = item[this.options.parentId];
|
const parentNodeId = item[this.options.parentId];
|
||||||
if (parentNodeId) {
|
if (parentNodeId) {
|
||||||
map[parentNodeId].children.push(item);
|
map[parentNodeId].children[item.id] = item;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
toTree() {
|
toArray() {
|
||||||
const map = this.linkChildren();
|
const stack = [];
|
||||||
const tree = [];
|
const treeNodes = this.items.map((i) => ({ ...i }));
|
||||||
|
|
||||||
this.items.forEach((item) => {
|
const walk = (nodes) => {
|
||||||
const parentNodeId = item[this.options.parentId];
|
nodes.forEach((node) => {
|
||||||
if (!parentNodeId) {
|
if (!node[this.options.parentId]) {
|
||||||
tree.push(map[item.id]);
|
stack.push(node);
|
||||||
}
|
}
|
||||||
});
|
if (node.children) {
|
||||||
this.collection = Object.values(tree);
|
const childrenNodes = Object.values(node.children)
|
||||||
return this.collection;
|
.map((i) => ({ ...i }));
|
||||||
|
|
||||||
|
node.children = childrenNodes;
|
||||||
|
walk(childrenNodes);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
walk(treeNodes);
|
||||||
|
return stack;
|
||||||
}
|
}
|
||||||
|
|
||||||
getTree() {
|
getTree() {
|
||||||
return this.collection;
|
return this.tree;
|
||||||
}
|
}
|
||||||
|
|
||||||
flattenTree(nodeMapper) {
|
getElementById(id) {
|
||||||
|
return this.tree[id] || null
|
||||||
|
}
|
||||||
|
|
||||||
|
getParents(id) {
|
||||||
|
const item = this.getElementById(id);
|
||||||
|
const parents = [];
|
||||||
|
let index = 0;
|
||||||
|
|
||||||
|
const walk = (_item) => {
|
||||||
|
if (!item) return;
|
||||||
|
|
||||||
|
if (index) {
|
||||||
|
parents.push(_item);
|
||||||
|
}
|
||||||
|
if (_item[this.options.parentId]) {
|
||||||
|
const parentItem = this.getElementById(_item[this.options.parentId]);
|
||||||
|
|
||||||
|
index++;
|
||||||
|
walk(parentItem);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
walk(item);
|
||||||
|
return parents;
|
||||||
|
}
|
||||||
|
|
||||||
|
toFlattenArray(nodeMapper) {
|
||||||
const flattenTree = [];
|
const flattenTree = [];
|
||||||
|
|
||||||
const traversal = (nodes, parentNode) => {
|
const traversal = (nodes, parentNode) => {
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ export default {
|
|||||||
builder.withGraphFetched('roles.field');
|
builder.withGraphFetched('roles.field');
|
||||||
builder.withGraphFetched('columns');
|
builder.withGraphFetched('columns');
|
||||||
builder.first();
|
builder.first();
|
||||||
|
builder.remember();
|
||||||
});
|
});
|
||||||
|
|
||||||
const resourceFieldsKeys = manualJournalsResource.fields.map((c) => c.key);
|
const resourceFieldsKeys = manualJournalsResource.fields.map((c) => c.key);
|
||||||
@@ -229,8 +230,10 @@ export default {
|
|||||||
errorReasons.push({ type: 'CREDIT.DEBIT.NOT.EQUALS', code: 100 });
|
errorReasons.push({ type: 'CREDIT.DEBIT.NOT.EQUALS', code: 100 });
|
||||||
}
|
}
|
||||||
const accountsIds = entries.map((entry) => entry.account_id);
|
const accountsIds = entries.map((entry) => entry.account_id);
|
||||||
const accounts = await Account.query().whereIn('id', accountsIds)
|
const accounts = await Account.query()
|
||||||
.withGraphFetched('type');
|
.whereIn('id', accountsIds)
|
||||||
|
.withGraphFetched('type')
|
||||||
|
.remember();
|
||||||
|
|
||||||
const storedAccountsIds = accounts.map((account) => account.id);
|
const storedAccountsIds = accounts.map((account) => account.id);
|
||||||
|
|
||||||
@@ -266,7 +269,9 @@ export default {
|
|||||||
status: form.status,
|
status: form.status,
|
||||||
user_id: user.id,
|
user_id: user.id,
|
||||||
});
|
});
|
||||||
const journalPoster = new JournalPoster();
|
|
||||||
|
const accountsDepGraph = await Account.depGraph().query().remember();
|
||||||
|
const journalPoster = new JournalPoster(accountsDepGraph);
|
||||||
|
|
||||||
entries.forEach((entry) => {
|
entries.forEach((entry) => {
|
||||||
const account = accounts.find((a) => a.id === entry.account_id);
|
const account = accounts.find((a) => a.id === entry.account_id);
|
||||||
@@ -456,7 +461,9 @@ export default {
|
|||||||
.where('reference_id', manualJournal.id)
|
.where('reference_id', manualJournal.id)
|
||||||
.withGraphFetched('account.type');
|
.withGraphFetched('account.type');
|
||||||
|
|
||||||
const journal = new JournalPoster();
|
const accountsDepGraph = await Account.depGraph().query().remember();
|
||||||
|
const journal = new JournalPoster(accountsDepGraph);
|
||||||
|
|
||||||
journal.loadEntries(transactions);
|
journal.loadEntries(transactions);
|
||||||
journal.removeEntries();
|
journal.removeEntries();
|
||||||
|
|
||||||
@@ -521,6 +528,7 @@ export default {
|
|||||||
const {
|
const {
|
||||||
ManualJournal,
|
ManualJournal,
|
||||||
AccountTransaction,
|
AccountTransaction,
|
||||||
|
Account,
|
||||||
} = req.models;
|
} = req.models;
|
||||||
|
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
@@ -546,7 +554,9 @@ export default {
|
|||||||
.where('reference_id', manualJournal.id)
|
.where('reference_id', manualJournal.id)
|
||||||
.withGraphFetched('account.type');
|
.withGraphFetched('account.type');
|
||||||
|
|
||||||
const journal = new JournalPoster();
|
const accountsDepGraph = await Account.depGraph().query().remember();
|
||||||
|
const journal = new JournalPoster(accountsDepGraph);
|
||||||
|
|
||||||
journal.loadEntries(transactions);
|
journal.loadEntries(transactions);
|
||||||
journal.calculateEntriesBalanceChange();
|
journal.calculateEntriesBalanceChange();
|
||||||
|
|
||||||
@@ -626,7 +636,9 @@ export default {
|
|||||||
ManualJournal,
|
ManualJournal,
|
||||||
AccountTransaction,
|
AccountTransaction,
|
||||||
MediaLink,
|
MediaLink,
|
||||||
|
Account,
|
||||||
} = req.models;
|
} = req.models;
|
||||||
|
|
||||||
const manualJournal = await ManualJournal.query()
|
const manualJournal = await ManualJournal.query()
|
||||||
.where('id', id).first();
|
.where('id', id).first();
|
||||||
|
|
||||||
@@ -640,7 +652,9 @@ export default {
|
|||||||
.where('reference_id', manualJournal.id)
|
.where('reference_id', manualJournal.id)
|
||||||
.withGraphFetched('account.type');
|
.withGraphFetched('account.type');
|
||||||
|
|
||||||
const journal = new JournalPoster();
|
const accountsDepGraph = await Account.depGraph().query().remember();
|
||||||
|
const journal = new JournalPoster(accountsDepGraph);
|
||||||
|
|
||||||
journal.loadEntries(transactions);
|
journal.loadEntries(transactions);
|
||||||
journal.removeEntries();
|
journal.removeEntries();
|
||||||
|
|
||||||
@@ -744,7 +758,7 @@ export default {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
const filter = { ...req.query };
|
const filter = { ...req.query };
|
||||||
const { ManualJournal, AccountTransaction, MediaLink } = req.models;
|
const { ManualJournal, AccountTransaction, Account, MediaLink } = req.models;
|
||||||
|
|
||||||
const manualJournals = await ManualJournal.query()
|
const manualJournals = await ManualJournal.query()
|
||||||
.whereIn('id', filter.ids);
|
.whereIn('id', filter.ids);
|
||||||
@@ -760,7 +774,8 @@ export default {
|
|||||||
.whereIn('reference_type', ['Journal', 'ManualJournal'])
|
.whereIn('reference_type', ['Journal', 'ManualJournal'])
|
||||||
.whereIn('reference_id', filter.ids);
|
.whereIn('reference_id', filter.ids);
|
||||||
|
|
||||||
const journal = new JournalPoster();
|
const accountsDepGraph = await Account.depGraph().query().remember();
|
||||||
|
const journal = new JournalPoster(accountsDepGraph);
|
||||||
|
|
||||||
journal.loadEntries(transactions);
|
journal.loadEntries(transactions);
|
||||||
journal.removeEntries();
|
journal.removeEntries();
|
||||||
|
|||||||
@@ -348,11 +348,10 @@ export default {
|
|||||||
dynamicFilter.buildQuery()(builder);
|
dynamicFilter.buildQuery()(builder);
|
||||||
});
|
});
|
||||||
|
|
||||||
const nestedAccounts = new NestedSet(accounts, { parentId: 'parentAccountId' });
|
const nestedAccounts = Account.toNestedArray(accounts);
|
||||||
const nestedSetAccounts = nestedAccounts.toTree();
|
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
accounts: nestedSetAccounts,
|
accounts: nestedAccounts,
|
||||||
...(view) ? {
|
...(view) ? {
|
||||||
customViewId: view.id,
|
customViewId: view.id,
|
||||||
} : {},
|
} : {},
|
||||||
|
|||||||
@@ -1,22 +1,10 @@
|
|||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { query, oneOf, validationResult } from 'express-validator';
|
|
||||||
import moment from 'moment';
|
|
||||||
import { pick, difference, groupBy } from 'lodash';
|
|
||||||
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
|
||||||
import JournalPoster from '@/services/Accounting/JournalPoster';
|
|
||||||
import { dateRangeCollection } from '@/utils';
|
|
||||||
|
|
||||||
const formatNumberClosure = (filter) => (balance) => {
|
import BalanceSheetController from './FinancialStatements/BalanceSheet';
|
||||||
let formattedBalance = parseFloat(balance);
|
import TrialBalanceSheetController from './FinancialStatements/TrialBalanceSheet';
|
||||||
|
import GeneralLedgerController from './FinancialStatements/generalLedger';
|
||||||
if (filter.no_cents) {
|
import JournalSheetController from './FinancialStatements/JournalSheet';
|
||||||
formattedBalance = parseInt(formattedBalance, 10);
|
import ProfitLossController from './FinancialStatements/ProfitLossSheet';
|
||||||
}
|
|
||||||
if (filter.divide_1000) {
|
|
||||||
formattedBalance /= 1000;
|
|
||||||
}
|
|
||||||
return formattedBalance;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
/**
|
/**
|
||||||
@@ -25,673 +13,13 @@ export default {
|
|||||||
router() {
|
router() {
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
router.get('/journal',
|
// router.use('/journal', JournalController);
|
||||||
this.journal.validation,
|
router.use('/balance_sheet', BalanceSheetController.router());
|
||||||
asyncMiddleware(this.journal.handler));
|
router.use('/profit_loss_sheet', ProfitLossController.router());
|
||||||
|
router.use('/general_ledger', GeneralLedgerController.router());
|
||||||
router.get('/general_ledger',
|
router.use('/trial_balance_sheet', TrialBalanceSheetController.router());
|
||||||
this.generalLedger.validation,
|
router.use('/journal', JournalSheetController.router());
|
||||||
asyncMiddleware(this.generalLedger.handler));
|
|
||||||
|
|
||||||
router.get('/balance_sheet',
|
|
||||||
this.balanceSheet.validation,
|
|
||||||
asyncMiddleware(this.balanceSheet.handler));
|
|
||||||
|
|
||||||
router.get('/trial_balance_sheet',
|
|
||||||
this.trialBalanceSheet.validation,
|
|
||||||
asyncMiddleware(this.trialBalanceSheet.handler));
|
|
||||||
|
|
||||||
router.get('/profit_loss_sheet',
|
|
||||||
this.profitLossSheet.validation,
|
|
||||||
asyncMiddleware(this.profitLossSheet.handler));
|
|
||||||
|
|
||||||
router.get('/cash_flow_statement',
|
|
||||||
this.cashFlowStatement.validation,
|
|
||||||
asyncMiddleware(this.cashFlowStatement.handler));
|
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve the ledger report of the given account.
|
|
||||||
*/
|
|
||||||
journal: {
|
|
||||||
validation: [
|
|
||||||
query('from_date').optional().isISO8601(),
|
|
||||||
query('to_date').optional().isISO8601(),
|
|
||||||
oneOf([
|
|
||||||
query('transaction_types').optional().isArray({ min: 1 }),
|
|
||||||
query('transaction_types.*').optional().isNumeric().toInt(),
|
|
||||||
], [
|
|
||||||
query('transaction_types').optional().trim().escape(),
|
|
||||||
]),
|
|
||||||
oneOf([
|
|
||||||
query('account_ids').optional().isArray({ min: 1 }),
|
|
||||||
query('account_ids.*').optional().isNumeric().toInt(),
|
|
||||||
], [
|
|
||||||
query('account_ids').optional().isNumeric().toInt(),
|
|
||||||
]),
|
|
||||||
query('from_range').optional().isNumeric().toInt(),
|
|
||||||
query('to_range').optional().isNumeric().toInt(),
|
|
||||||
query('number_format.no_cents').optional().isBoolean().toBoolean(),
|
|
||||||
query('number_format.divide_1000').optional().isBoolean().toBoolean(),
|
|
||||||
],
|
|
||||||
async handler(req, res) {
|
|
||||||
const validationErrors = validationResult(req);
|
|
||||||
|
|
||||||
if (!validationErrors.isEmpty()) {
|
|
||||||
return res.boom.badData(null, {
|
|
||||||
code: 'validation_error', ...validationErrors,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const { AccountTransaction } = req.models;
|
|
||||||
const filter = {
|
|
||||||
from_date: moment().startOf('year').format('YYYY-MM-DD'),
|
|
||||||
to_date: moment().endOf('year').format('YYYY-MM-DD'),
|
|
||||||
from_range: null,
|
|
||||||
to_range: null,
|
|
||||||
account_ids: [],
|
|
||||||
transaction_types: [],
|
|
||||||
number_format: {
|
|
||||||
no_cents: false,
|
|
||||||
divide_1000: false,
|
|
||||||
},
|
|
||||||
...req.query,
|
|
||||||
};
|
|
||||||
if (!Array.isArray(filter.transaction_types)) {
|
|
||||||
filter.transaction_types = [filter.transaction_types];
|
|
||||||
}
|
|
||||||
if (!Array.isArray(filter.account_ids)) {
|
|
||||||
filter.account_ids = [filter.account_ids];
|
|
||||||
}
|
|
||||||
filter.account_ids = filter.account_ids.map((id) => parseInt(id, 10));
|
|
||||||
|
|
||||||
const accountsJournalEntries = await AccountTransaction.query()
|
|
||||||
.remember()
|
|
||||||
.modify('filterDateRange', filter.from_date, filter.to_date)
|
|
||||||
.modify('filterAccounts', filter.account_ids)
|
|
||||||
.modify('filterTransactionTypes', filter.transaction_types)
|
|
||||||
.modify('filterAmountRange', filter.from_range, filter.to_range)
|
|
||||||
.withGraphFetched('account.type');
|
|
||||||
|
|
||||||
const formatNumber = formatNumberClosure(filter.number_format);
|
|
||||||
|
|
||||||
const journalGrouped = groupBy(accountsJournalEntries,
|
|
||||||
(entry) => `${entry.referenceId}-${entry.referenceType}`);
|
|
||||||
|
|
||||||
const journal = Object.keys(journalGrouped).map((key) => {
|
|
||||||
const transactionsGroup = journalGrouped[key];
|
|
||||||
|
|
||||||
const journalPoster = new JournalPoster();
|
|
||||||
journalPoster.loadEntries(transactionsGroup);
|
|
||||||
|
|
||||||
const trialBalance = journalPoster.getTrialBalance();
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: key,
|
|
||||||
entries: transactionsGroup,
|
|
||||||
credit: formatNumber(trialBalance.credit),
|
|
||||||
debit: formatNumber(trialBalance.debit),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
query: { ...filter },
|
|
||||||
journal,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve the general ledger financial statement.
|
|
||||||
*/
|
|
||||||
generalLedger: {
|
|
||||||
validation: [
|
|
||||||
query('from_date').optional().isISO8601(),
|
|
||||||
query('to_date').optional().isISO8601(),
|
|
||||||
query('basis').optional(),
|
|
||||||
query('number_format.no_cents').optional().isBoolean().toBoolean(),
|
|
||||||
query('number_format.divide_1000').optional().isBoolean().toBoolean(),
|
|
||||||
query('none_zero').optional().isBoolean().toBoolean(),
|
|
||||||
query('accounts_ids').optional(),
|
|
||||||
query('accounts_ids.*').isNumeric().toInt(),
|
|
||||||
query('orderBy').optional().isIn(['created_at', 'name', 'code']),
|
|
||||||
query('order').optional().isIn(['desc', 'asc']),
|
|
||||||
],
|
|
||||||
async handler(req, res) {
|
|
||||||
const validationErrors = validationResult(req);
|
|
||||||
|
|
||||||
if (!validationErrors.isEmpty()) {
|
|
||||||
return res.boom.badData(null, {
|
|
||||||
code: 'validation_error', ...validationErrors,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const { AccountTransaction, Account } = req.models;
|
|
||||||
const filter = {
|
|
||||||
from_date: moment().startOf('year').format('YYYY-MM-DD'),
|
|
||||||
to_date: moment().endOf('year').format('YYYY-MM-DD'),
|
|
||||||
basis: 'cash',
|
|
||||||
number_format: {
|
|
||||||
no_cents: false,
|
|
||||||
divide_1000: false,
|
|
||||||
},
|
|
||||||
none_zero: false,
|
|
||||||
accounts_ids: [],
|
|
||||||
...req.query,
|
|
||||||
};
|
|
||||||
if (!Array.isArray(filter.accounts_ids)) {
|
|
||||||
filter.accounts_ids = [filter.accounts_ids];
|
|
||||||
}
|
|
||||||
filter.accounts_ids = filter.accounts_ids.map((id) => parseInt(id, 10));
|
|
||||||
|
|
||||||
const errorReasons = [];
|
|
||||||
|
|
||||||
if (filter.accounts_ids.length > 0) {
|
|
||||||
const accounts = await Account.query().whereIn('id', filter.accounts_ids);
|
|
||||||
const accountsIds = accounts.map((a) => a.id);
|
|
||||||
|
|
||||||
if (difference(filter.accounts_ids, accountsIds).length > 0) {
|
|
||||||
errorReasons.push({ type: 'FILTER.ACCOUNTS.IDS.NOT.FOUND', code: 200 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (errorReasons.length > 0) {
|
|
||||||
return res.status(400).send({ error: errorReasons });
|
|
||||||
}
|
|
||||||
const accounts = await Account.query()
|
|
||||||
.remember('general_ledger_accounts')
|
|
||||||
.orderBy('index', 'DESC')
|
|
||||||
.modify('filterAccounts', filter.accounts_ids)
|
|
||||||
.withGraphFetched('type')
|
|
||||||
.withGraphFetched('transactions')
|
|
||||||
.modifyGraph('transactions', (builder) => {
|
|
||||||
builder.modify('filterDateRange', filter.from_date, filter.to_date);
|
|
||||||
});
|
|
||||||
|
|
||||||
const openingBalanceTransactions = await AccountTransaction.query()
|
|
||||||
.remember()
|
|
||||||
.modify('filterDateRange', null, filter.from_date)
|
|
||||||
.modify('sumationCreditDebit')
|
|
||||||
.withGraphFetched('account.type');
|
|
||||||
|
|
||||||
const closingBalanceTransactions = await AccountTransaction.query()
|
|
||||||
.remember()
|
|
||||||
.modify('filterDateRange', null, filter.to_date)
|
|
||||||
.modify('sumationCreditDebit')
|
|
||||||
.withGraphFetched('account.type');
|
|
||||||
|
|
||||||
const opeingBalanceCollection = new JournalPoster();
|
|
||||||
const closingBalanceCollection = new JournalPoster();
|
|
||||||
|
|
||||||
opeingBalanceCollection.loadEntries(openingBalanceTransactions);
|
|
||||||
closingBalanceCollection.loadEntries(closingBalanceTransactions);
|
|
||||||
|
|
||||||
// Transaction amount formatter based on the given query.
|
|
||||||
const formatNumber = formatNumberClosure(filter.number_format);
|
|
||||||
|
|
||||||
const items = accounts
|
|
||||||
.filter((account) => (
|
|
||||||
account.transactions.length > 0 || !filter.none_zero
|
|
||||||
))
|
|
||||||
.map((account) => ({
|
|
||||||
...pick(account, ['id', 'name', 'code', 'index']),
|
|
||||||
transactions: [
|
|
||||||
...account.transactions.map((transaction) => {
|
|
||||||
let amount = 0;
|
|
||||||
|
|
||||||
if (account.type.normal === 'credit') {
|
|
||||||
amount += transaction.credit - transaction.debit;
|
|
||||||
} else if (account.type.normal === 'debit') {
|
|
||||||
amount += transaction.debit - transaction.credit;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...pick(transaction, ['id', 'note', 'transactionType', 'referenceType',
|
|
||||||
'referenceId', 'date', 'createdAt']),
|
|
||||||
amount: formatNumber(amount),
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
opening: {
|
|
||||||
date: filter.from_date,
|
|
||||||
amount: formatNumber(opeingBalanceCollection.getClosingBalance(account.id)),
|
|
||||||
},
|
|
||||||
closing: {
|
|
||||||
date: filter.to_date,
|
|
||||||
amount: formatNumber(closingBalanceCollection.getClosingBalance(account.id)),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
query: { ...filter },
|
|
||||||
accounts: items,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve the balance sheet.
|
|
||||||
*/
|
|
||||||
balanceSheet: {
|
|
||||||
validation: [
|
|
||||||
query('accounting_method').optional().isIn(['cash', 'accural']),
|
|
||||||
query('from_date').optional(),
|
|
||||||
query('to_date').optional(),
|
|
||||||
query('display_columns_type').optional().isIn(['date_periods', 'total']),
|
|
||||||
query('display_columns_by').optional({ nullable: true, checkFalsy: true })
|
|
||||||
.isIn(['year', 'month', 'week', 'day', 'quarter']),
|
|
||||||
query('number_format.no_cents').optional().isBoolean().toBoolean(),
|
|
||||||
query('number_format.divide_1000').optional().isBoolean().toBoolean(),
|
|
||||||
query('account_ids').isArray().optional(),
|
|
||||||
query('account_ids.*').isNumeric().toInt(),
|
|
||||||
query('none_zero').optional().isBoolean().toBoolean(),
|
|
||||||
],
|
|
||||||
async handler(req, res) {
|
|
||||||
const validationErrors = validationResult(req);
|
|
||||||
|
|
||||||
if (!validationErrors.isEmpty()) {
|
|
||||||
return res.boom.badData(null, {
|
|
||||||
code: 'validation_error', ...validationErrors,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const { Account, AccountType } = req.models;
|
|
||||||
const filter = {
|
|
||||||
display_columns_type: 'total',
|
|
||||||
display_columns_by: '',
|
|
||||||
from_date: moment().startOf('year').format('YYYY-MM-DD'),
|
|
||||||
to_date: moment().endOf('year').format('YYYY-MM-DD'),
|
|
||||||
number_format: {
|
|
||||||
no_cents: false,
|
|
||||||
divide_1000: false,
|
|
||||||
},
|
|
||||||
none_zero: false,
|
|
||||||
basis: 'cash',
|
|
||||||
account_ids: [],
|
|
||||||
...req.query,
|
|
||||||
};
|
|
||||||
if (!Array.isArray(filter.account_ids)) {
|
|
||||||
filter.account_ids = [filter.account_ids];
|
|
||||||
}
|
|
||||||
|
|
||||||
const balanceSheetTypes = await AccountType.query().where('balance_sheet', true);
|
|
||||||
|
|
||||||
// Fetch all balance sheet accounts.
|
|
||||||
const accounts = await Account.query()
|
|
||||||
.remember('balance_sheet_accounts')
|
|
||||||
.whereIn('account_type_id', balanceSheetTypes.map((a) => a.id))
|
|
||||||
.modify('filterAccounts', filter.account_ids)
|
|
||||||
.withGraphFetched('type')
|
|
||||||
.withGraphFetched('transactions')
|
|
||||||
.modifyGraph('transactions', (builder) => {
|
|
||||||
builder.modify('filterDateRange', null, filter.to_date);
|
|
||||||
});
|
|
||||||
|
|
||||||
const journalEntriesCollected = Account.collectJournalEntries(accounts);
|
|
||||||
const journalEntries = new JournalPoster();
|
|
||||||
|
|
||||||
journalEntries.loadEntries(journalEntriesCollected);
|
|
||||||
|
|
||||||
// Account balance formmatter based on the given query.
|
|
||||||
const balanceFormatter = formatNumberClosure(filter.number_format);
|
|
||||||
const comparatorDateType = filter.display_columns_type === 'total'
|
|
||||||
? 'day' : filter.display_columns_by;
|
|
||||||
|
|
||||||
const dateRangeSet = (filter.display_columns_type === 'date_periods')
|
|
||||||
? dateRangeCollection(
|
|
||||||
filter.from_date, filter.to_date, comparatorDateType,
|
|
||||||
) : [];
|
|
||||||
|
|
||||||
const totalPeriods = (account) => ({
|
|
||||||
// Gets the date range set from start to end date.
|
|
||||||
total_periods: dateRangeSet.map((date) => {
|
|
||||||
const balance = journalEntries.getClosingBalance(account.id, date, comparatorDateType);
|
|
||||||
return {
|
|
||||||
date,
|
|
||||||
formatted_amount: balanceFormatter(balance),
|
|
||||||
amount: balance,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const accountsMapper = (balanceSheetAccounts) => [
|
|
||||||
...balanceSheetAccounts.map((account) => {
|
|
||||||
// Calculates the closing balance to the given date.
|
|
||||||
const closingBalance = journalEntries.getClosingBalance(account.id, filter.to_date);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...pick(account, ['id', 'index', 'name', 'code']),
|
|
||||||
|
|
||||||
// Date periods when display columns.
|
|
||||||
...(filter.display_columns_type === 'date_periods') && totalPeriods(account),
|
|
||||||
|
|
||||||
total: {
|
|
||||||
formatted_amount: balanceFormatter(closingBalance),
|
|
||||||
amount: closingBalance,
|
|
||||||
date: filter.to_date,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
// Retrieve all assets accounts.
|
|
||||||
const assetsAccounts = accounts.filter((account) => (
|
|
||||||
account.type.normal === 'debit'
|
|
||||||
&& (account.transactions.length > 0 || !filter.none_zero)));
|
|
||||||
|
|
||||||
// Retrieve all liability accounts.
|
|
||||||
const liabilitiesAccounts = accounts.filter((account) => (
|
|
||||||
account.type.normal === 'credit'
|
|
||||||
&& (account.transactions.length > 0 || !filter.none_zero)));
|
|
||||||
|
|
||||||
// Retrieve the asset balance sheet.
|
|
||||||
const assets = accountsMapper(assetsAccounts);
|
|
||||||
|
|
||||||
// Retrieve liabilities and equity balance sheet.
|
|
||||||
const liabilitiesEquity = accountsMapper(liabilitiesAccounts);
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
query: { ...filter },
|
|
||||||
columns: { ...dateRangeSet },
|
|
||||||
accounts: [
|
|
||||||
{
|
|
||||||
name: 'Assets',
|
|
||||||
type: 'assets',
|
|
||||||
children: [...assets],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Liabilities & Equity',
|
|
||||||
type: 'liabilities_equity',
|
|
||||||
children: [...liabilitiesEquity],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve the trial balance sheet.
|
|
||||||
*/
|
|
||||||
trialBalanceSheet: {
|
|
||||||
validation: [
|
|
||||||
query('basis').optional(),
|
|
||||||
query('from_date').optional().isISO8601(),
|
|
||||||
query('to_date').optional().isISO8601(),
|
|
||||||
query('number_format.no_cents').optional().isBoolean().toBoolean(),
|
|
||||||
query('number_format.1000_divide').optional().isBoolean().toBoolean(),
|
|
||||||
query('basis').optional(),
|
|
||||||
query('none_zero').optional().isBoolean().toBoolean(),
|
|
||||||
],
|
|
||||||
async handler(req, res) {
|
|
||||||
const validationErrors = validationResult(req);
|
|
||||||
|
|
||||||
if (!validationErrors.isEmpty()) {
|
|
||||||
return res.boom.badData(null, {
|
|
||||||
code: 'validation_error', ...validationErrors,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const {
|
|
||||||
Account,
|
|
||||||
} = req.models;
|
|
||||||
const filter = {
|
|
||||||
from_date: moment().startOf('year').format('YYYY-MM-DD'),
|
|
||||||
to_date: moment().endOf('year').format('YYYY-MM-DD'),
|
|
||||||
number_format: {
|
|
||||||
no_cents: false,
|
|
||||||
divide_1000: false,
|
|
||||||
},
|
|
||||||
basis: 'accural',
|
|
||||||
none_zero: false,
|
|
||||||
...req.query,
|
|
||||||
};
|
|
||||||
|
|
||||||
const accounts = await Account.query()
|
|
||||||
.remember('trial_balance_accounts')
|
|
||||||
.withGraphFetched('type')
|
|
||||||
.withGraphFetched('transactions')
|
|
||||||
.modifyGraph('transactions', (builder) => {
|
|
||||||
builder.modify('sumationCreditDebit');
|
|
||||||
builder.modify('filterDateRange', filter.from_date, filter.to_date);
|
|
||||||
});
|
|
||||||
|
|
||||||
const journalEntriesCollect = Account.collectJournalEntries(accounts);
|
|
||||||
const journalEntries = new JournalPoster();
|
|
||||||
journalEntries.loadEntries(journalEntriesCollect);
|
|
||||||
|
|
||||||
// Account balance formmatter based on the given query.
|
|
||||||
const balanceFormatter = formatNumberClosure(filter.number_format);
|
|
||||||
|
|
||||||
const items = accounts
|
|
||||||
.filter((account) => (
|
|
||||||
account.transactions.length > 0 || !filter.none_zero
|
|
||||||
))
|
|
||||||
.map((account) => {
|
|
||||||
const trial = journalEntries.getTrialBalance(account.id);
|
|
||||||
return {
|
|
||||||
account_id: account.id,
|
|
||||||
name: account.name,
|
|
||||||
code: account.code,
|
|
||||||
accountNormal: account.type.normal,
|
|
||||||
credit: balanceFormatter(trial.credit),
|
|
||||||
debit: balanceFormatter(trial.debit),
|
|
||||||
balance: balanceFormatter(trial.balance),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
return res.status(200).send({
|
|
||||||
query: { ...filter },
|
|
||||||
accounts: [...items],
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve profit/loss financial statement.
|
|
||||||
*/
|
|
||||||
profitLossSheet: {
|
|
||||||
validation: [
|
|
||||||
query('basis').optional(),
|
|
||||||
query('from_date').optional().isISO8601(),
|
|
||||||
query('to_date').optional().isISO8601(),
|
|
||||||
query('number_format.no_cents').optional().isBoolean(),
|
|
||||||
query('number_format.divide_1000').optional().isBoolean(),
|
|
||||||
query('basis').optional(),
|
|
||||||
query('none_zero').optional().isBoolean().toBoolean(),
|
|
||||||
query('display_columns_type').optional().isIn([
|
|
||||||
'total', 'date_periods',
|
|
||||||
]),
|
|
||||||
query('display_columns_by').optional({ nullable: true, checkFalsy: true })
|
|
||||||
.isIn(['year', 'month', 'week', 'day', 'quarter']),
|
|
||||||
],
|
|
||||||
async handler(req, res) {
|
|
||||||
const validationErrors = validationResult(req);
|
|
||||||
|
|
||||||
if (!validationErrors.isEmpty()) {
|
|
||||||
return res.boom.badData(null, {
|
|
||||||
code: 'validation_error', ...validationErrors,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const { Account, AccountType } = req.models;
|
|
||||||
const filter = {
|
|
||||||
from_date: moment().startOf('year').format('YYYY-MM-DD'),
|
|
||||||
to_date: moment().endOf('year').format('YYYY-MM-DD'),
|
|
||||||
number_format: {
|
|
||||||
no_cents: false,
|
|
||||||
divide_1000: false,
|
|
||||||
},
|
|
||||||
basis: 'accural',
|
|
||||||
none_zero: false,
|
|
||||||
display_columns_type: 'total',
|
|
||||||
display_columns_by: 'month',
|
|
||||||
...req.query,
|
|
||||||
};
|
|
||||||
const incomeStatementTypes = await AccountType.query().where('income_sheet', true);
|
|
||||||
|
|
||||||
// Fetch all income accounts from storage.
|
|
||||||
const accounts = await Account.query()
|
|
||||||
.remember('profit_loss_accounts')
|
|
||||||
.whereIn('account_type_id', incomeStatementTypes.map((t) => t.id))
|
|
||||||
.withGraphFetched('type')
|
|
||||||
.withGraphFetched('transactions');
|
|
||||||
|
|
||||||
// Filter all none zero accounts if it was enabled.
|
|
||||||
const filteredAccounts = accounts.filter((account) => (
|
|
||||||
account.transactions.length > 0 || !filter.none_zero
|
|
||||||
));
|
|
||||||
const journalEntriesCollected = Account.collectJournalEntries(accounts);
|
|
||||||
const journalEntries = new JournalPoster();
|
|
||||||
journalEntries.loadEntries(journalEntriesCollected);
|
|
||||||
|
|
||||||
// Account balance formmatter based on the given query.
|
|
||||||
const numberFormatter = formatNumberClosure(filter.number_format);
|
|
||||||
const comparatorDateType = filter.display_columns_type === 'total'
|
|
||||||
? 'day' : filter.display_columns_by;
|
|
||||||
|
|
||||||
// Gets the date range set from start to end date.
|
|
||||||
const dateRangeSet = dateRangeCollection(
|
|
||||||
filter.from_date,
|
|
||||||
filter.to_date,
|
|
||||||
comparatorDateType,
|
|
||||||
);
|
|
||||||
|
|
||||||
const accountsMapper = (incomeExpenseAccounts) => (
|
|
||||||
incomeExpenseAccounts.map((account) => ({
|
|
||||||
...pick(account, ['id', 'index', 'name', 'code']),
|
|
||||||
|
|
||||||
// Total closing balance of the account.
|
|
||||||
...(filter.display_columns_type === 'total') && {
|
|
||||||
total: (() => {
|
|
||||||
const amount = journalEntries.getClosingBalance(account.id, filter.to_date);
|
|
||||||
return { amount, date: filter.to_date, formatted_amount: numberFormatter(amount) };
|
|
||||||
})(),
|
|
||||||
},
|
|
||||||
// Date periods when display columns type `periods`.
|
|
||||||
...(filter.display_columns_type === 'date_periods') && {
|
|
||||||
periods: dateRangeSet.map((date) => {
|
|
||||||
const type = comparatorDateType;
|
|
||||||
const amount = journalEntries.getClosingBalance(account.id, date, type);
|
|
||||||
|
|
||||||
return { date, amount, formatted_amount: numberFormatter(amount) };
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
})));
|
|
||||||
|
|
||||||
const totalAccountsReducer = (incomeExpenseAccounts) => (
|
|
||||||
incomeExpenseAccounts.reduce((acc, account) => {
|
|
||||||
const amount = (account) ? account.total.amount : 0;
|
|
||||||
return amount + acc;
|
|
||||||
}, 0));
|
|
||||||
|
|
||||||
const accountsIncome = accountsMapper(filteredAccounts
|
|
||||||
.filter((account) => account.type.normal === 'credit'));
|
|
||||||
|
|
||||||
const accountsExpenses = accountsMapper(filteredAccounts
|
|
||||||
.filter((account) => account.type.normal === 'debit'));
|
|
||||||
|
|
||||||
// @return {Array}
|
|
||||||
const totalPeriodsMapper = (incomeExpenseAccounts) => (
|
|
||||||
Object.values(dateRangeSet.reduce((acc, date, index) => {
|
|
||||||
let amount = 0;
|
|
||||||
|
|
||||||
incomeExpenseAccounts.forEach((account) => {
|
|
||||||
const currentDate = account.periods[index];
|
|
||||||
amount += currentDate.amount || 0;
|
|
||||||
});
|
|
||||||
acc[date] = { date, amount, formatted_amount: numberFormatter(amount) };
|
|
||||||
return acc;
|
|
||||||
}, {})));
|
|
||||||
|
|
||||||
// Total income(date) - Total expenses(date) = Net income(date)
|
|
||||||
// @return {Array}
|
|
||||||
const netIncomePeriodsMapper = (totalIncomeAcocunts, totalExpenseAccounts) => (
|
|
||||||
dateRangeSet.map((date, index) => {
|
|
||||||
const totalIncome = totalIncomeAcocunts[index];
|
|
||||||
const totalExpenses = totalExpenseAccounts[index];
|
|
||||||
|
|
||||||
let amount = totalIncome.amount || 0;
|
|
||||||
amount -= totalExpenses.amount || 0;
|
|
||||||
return { date, amount, formatted_amount: numberFormatter(amount) };
|
|
||||||
}));
|
|
||||||
|
|
||||||
// @return {Object}
|
|
||||||
const netIncomeTotal = (totalIncome, totalExpenses) => {
|
|
||||||
const netIncomeAmount = totalIncome.amount - totalExpenses.amount;
|
|
||||||
return { amount: netIncomeAmount, formatted_amount: netIncomeAmount, date: filter.to_date };
|
|
||||||
};
|
|
||||||
|
|
||||||
const incomeResponse = {
|
|
||||||
entry_normal: 'credit',
|
|
||||||
accounts: accountsIncome,
|
|
||||||
...(filter.display_columns_type === 'total') && (() => {
|
|
||||||
const totalIncomeAccounts = totalAccountsReducer(accountsIncome);
|
|
||||||
return {
|
|
||||||
total: {
|
|
||||||
amount: totalIncomeAccounts,
|
|
||||||
date: filter.to_date,
|
|
||||||
formatted_amount: numberFormatter(totalIncomeAccounts),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
})(),
|
|
||||||
...(filter.display_columns_type === 'date_periods') && {
|
|
||||||
total_periods: [
|
|
||||||
...totalPeriodsMapper(accountsIncome),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const expenseResponse = {
|
|
||||||
entry_normal: 'debit',
|
|
||||||
accounts: accountsExpenses,
|
|
||||||
...(filter.display_columns_type === 'total') && (() => {
|
|
||||||
const totalExpensesAccounts = totalAccountsReducer(accountsExpenses);
|
|
||||||
return {
|
|
||||||
total: {
|
|
||||||
amount: totalExpensesAccounts,
|
|
||||||
date: filter.to_date,
|
|
||||||
formatted_amount: numberFormatter(totalExpensesAccounts),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
})(),
|
|
||||||
...(filter.display_columns_type === 'date_periods') && {
|
|
||||||
total_periods: [
|
|
||||||
...totalPeriodsMapper(accountsExpenses),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const netIncomeResponse = {
|
|
||||||
...(filter.display_columns_type === 'total') && {
|
|
||||||
total: {
|
|
||||||
...netIncomeTotal(incomeResponse.total, expenseResponse.total),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
...(filter.display_columns_type === 'date_periods') && {
|
|
||||||
total_periods: [
|
|
||||||
...netIncomePeriodsMapper(
|
|
||||||
incomeResponse.total_periods,
|
|
||||||
expenseResponse.total_periods,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
return res.status(200).send({
|
|
||||||
query: { ...filter },
|
|
||||||
columns: [...dateRangeSet],
|
|
||||||
profitLoss: {
|
|
||||||
income: incomeResponse,
|
|
||||||
expenses: expenseResponse,
|
|
||||||
net_income: netIncomeResponse,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
cashFlowStatement: {
|
|
||||||
validation: [
|
|
||||||
query('date_from').optional(),
|
|
||||||
query('date_to').optional(),
|
|
||||||
],
|
|
||||||
async handler(req, res) {
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
meta: {},
|
|
||||||
operating: [],
|
|
||||||
financing: [],
|
|
||||||
investing: [],
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|||||||
172
server/src/http/controllers/FinancialStatements/BalanceSheet.js
Normal file
172
server/src/http/controllers/FinancialStatements/BalanceSheet.js
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import { query, oneOf, validationResult } from 'express-validator';
|
||||||
|
import moment from 'moment';
|
||||||
|
import { pick, difference, groupBy } from 'lodash';
|
||||||
|
import JournalPoster from '@/services/Accounting/JournalPoster';
|
||||||
|
import { dateRangeCollection } from '@/utils';
|
||||||
|
import DependencyGraph from '@/lib/DependencyGraph';
|
||||||
|
import asyncMiddleware from '@/http/middleware/asyncMiddleware'
|
||||||
|
import { formatNumberClosure } from './FinancialStatementMixin';
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
/**
|
||||||
|
* Router constructor.
|
||||||
|
*/
|
||||||
|
router() {
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.get('/',
|
||||||
|
this.balanceSheet.validation,
|
||||||
|
asyncMiddleware(this.balanceSheet.handler));
|
||||||
|
|
||||||
|
return router;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the balance sheet.
|
||||||
|
*/
|
||||||
|
balanceSheet: {
|
||||||
|
validation: [
|
||||||
|
query('accounting_method').optional().isIn(['cash', 'accural']),
|
||||||
|
query('from_date').optional(),
|
||||||
|
query('to_date').optional(),
|
||||||
|
query('display_columns_type').optional().isIn(['date_periods', 'total']),
|
||||||
|
query('display_columns_by').optional({ nullable: true, checkFalsy: true })
|
||||||
|
.isIn(['year', 'month', 'week', 'day', 'quarter']),
|
||||||
|
query('number_format.no_cents').optional().isBoolean().toBoolean(),
|
||||||
|
query('number_format.divide_1000').optional().isBoolean().toBoolean(),
|
||||||
|
query('account_ids').isArray().optional(),
|
||||||
|
query('account_ids.*').isNumeric().toInt(),
|
||||||
|
query('none_zero').optional().isBoolean().toBoolean(),
|
||||||
|
],
|
||||||
|
async handler(req, res) {
|
||||||
|
const validationErrors = validationResult(req);
|
||||||
|
|
||||||
|
if (!validationErrors.isEmpty()) {
|
||||||
|
return res.boom.badData(null, {
|
||||||
|
code: 'validation_error', ...validationErrors,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const { Account, AccountType } = req.models;
|
||||||
|
|
||||||
|
const filter = {
|
||||||
|
display_columns_type: 'total',
|
||||||
|
display_columns_by: '',
|
||||||
|
from_date: moment().startOf('year').format('YYYY-MM-DD'),
|
||||||
|
to_date: moment().endOf('year').format('YYYY-MM-DD'),
|
||||||
|
number_format: {
|
||||||
|
no_cents: false,
|
||||||
|
divide_1000: false,
|
||||||
|
},
|
||||||
|
none_zero: false,
|
||||||
|
basis: 'cash',
|
||||||
|
account_ids: [],
|
||||||
|
...req.query,
|
||||||
|
};
|
||||||
|
if (!Array.isArray(filter.account_ids)) {
|
||||||
|
filter.account_ids = [filter.account_ids];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Account balance formmatter based on the given query.
|
||||||
|
const balanceFormatter = formatNumberClosure(filter.number_format);
|
||||||
|
const comparatorDateType = filter.display_columns_type === 'total' ? 'day' : filter.display_columns_by;
|
||||||
|
|
||||||
|
const balanceSheetTypes = await AccountType.query().where('balance_sheet', true);
|
||||||
|
|
||||||
|
// Fetch all balance sheet accounts from the storage.
|
||||||
|
const accounts = await Account.query()
|
||||||
|
// .remember('balance_sheet_accounts')
|
||||||
|
.whereIn('account_type_id', balanceSheetTypes.map((a) => a.id))
|
||||||
|
.modify('filterAccounts', filter.account_ids)
|
||||||
|
.withGraphFetched('type')
|
||||||
|
.withGraphFetched('transactions')
|
||||||
|
.modifyGraph('transactions', (builder) => {
|
||||||
|
builder.modify('filterDateRange', null, filter.to_date);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Accounts dependency graph.
|
||||||
|
const accountsGraph = DependencyGraph.fromArray(
|
||||||
|
accounts, { itemId: 'id', parentItemId: 'parentAccountId' }
|
||||||
|
);
|
||||||
|
// Load all entries that associated to the given accounts.
|
||||||
|
const journalEntriesCollected = Account.collectJournalEntries(accounts);
|
||||||
|
const journalEntries = new JournalPoster(accountsGraph);
|
||||||
|
|
||||||
|
journalEntries.loadEntries(journalEntriesCollected);
|
||||||
|
|
||||||
|
// Date range collection.
|
||||||
|
const dateRangeSet = (filter.display_columns_type === 'date_periods')
|
||||||
|
? dateRangeCollection(
|
||||||
|
filter.from_date, filter.to_date, comparatorDateType,
|
||||||
|
) : [];
|
||||||
|
|
||||||
|
// Gets the date range set from start to end date.
|
||||||
|
const totalPeriods = (account) => ({
|
||||||
|
total_periods: dateRangeSet.map((date) => {
|
||||||
|
const amount = journalEntries.getAccountBalance(account.id, date, comparatorDateType);
|
||||||
|
|
||||||
|
return {
|
||||||
|
amount,
|
||||||
|
formatted_amount: balanceFormatter(amount),
|
||||||
|
date,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const accountsMapperToResponse = (account) => {
|
||||||
|
// Calculates the closing balance to the given date.
|
||||||
|
const closingBalance = journalEntries.getAccountBalance(account.id, filter.to_date);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...pick(account, ['id', 'index', 'name', 'code', 'parentAccountId']),
|
||||||
|
|
||||||
|
// Date periods when display columns.
|
||||||
|
...(filter.display_columns_type === 'date_periods') && totalPeriods(account),
|
||||||
|
|
||||||
|
total: {
|
||||||
|
amount: closingBalance,
|
||||||
|
formatted_amount: balanceFormatter(closingBalance),
|
||||||
|
date: filter.to_date,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Retrieve all assets accounts.
|
||||||
|
const assetsAccounts = accounts.filter((account) => (
|
||||||
|
account.type.normal === 'debit'
|
||||||
|
&& (account.transactions.length > 0 || !filter.none_zero)))
|
||||||
|
.map(accountsMapperToResponse);
|
||||||
|
|
||||||
|
// Retrieve all liability accounts.
|
||||||
|
const liabilitiesAccounts = accounts.filter((account) => (
|
||||||
|
account.type.normal === 'credit'
|
||||||
|
&& (account.transactions.length > 0 || !filter.none_zero)))
|
||||||
|
.map(accountsMapperToResponse);
|
||||||
|
|
||||||
|
// Retrieve the asset balance sheet.
|
||||||
|
const assetsAccountsResponse = Account.toNestedArray(assetsAccounts);
|
||||||
|
|
||||||
|
// Retrieve liabilities and equity balance sheet.
|
||||||
|
const liabilitiesEquityResponse = Account.toNestedArray(liabilitiesAccounts);
|
||||||
|
|
||||||
|
// Response.
|
||||||
|
return res.status(200).send({
|
||||||
|
query: { ...filter },
|
||||||
|
columns: { ...dateRangeSet },
|
||||||
|
accounts: [
|
||||||
|
{
|
||||||
|
name: 'Assets',
|
||||||
|
type: 'assets',
|
||||||
|
children: [...assetsAccountsResponse],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Liabilities & Equity',
|
||||||
|
type: 'liabilities_equity',
|
||||||
|
children: [...liabilitiesEquityResponse],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
|
||||||
|
|
||||||
|
export const formatNumberClosure = (filter) => (balance) => {
|
||||||
|
let formattedBalance = parseFloat(balance);
|
||||||
|
|
||||||
|
if (filter.no_cents) {
|
||||||
|
formattedBalance = parseInt(formattedBalance, 10);
|
||||||
|
}
|
||||||
|
if (filter.divide_1000) {
|
||||||
|
formattedBalance /= 1000;
|
||||||
|
}
|
||||||
|
return formattedBalance;
|
||||||
|
};
|
||||||
165
server/src/http/controllers/FinancialStatements/GeneralLedger.js
Normal file
165
server/src/http/controllers/FinancialStatements/GeneralLedger.js
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import { query, validationResult } from 'express-validator';
|
||||||
|
import moment from 'moment';
|
||||||
|
import { pick, difference } from 'lodash';
|
||||||
|
import JournalPoster from '@/services/Accounting/JournalPoster';
|
||||||
|
import { formatNumberClosure } from './FinancialStatementMixin';
|
||||||
|
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
||||||
|
import DependencyGraph from '@/lib/DependencyGraph';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
/**
|
||||||
|
* Router constructor.
|
||||||
|
*/
|
||||||
|
router() {
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.get('/',
|
||||||
|
this.generalLedger.validation,
|
||||||
|
asyncMiddleware(this.generalLedger.handler));
|
||||||
|
|
||||||
|
return router;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the general ledger financial statement.
|
||||||
|
*/
|
||||||
|
generalLedger: {
|
||||||
|
validation: [
|
||||||
|
query('from_date').optional().isISO8601(),
|
||||||
|
query('to_date').optional().isISO8601(),
|
||||||
|
query('basis').optional(),
|
||||||
|
query('number_format.no_cents').optional().isBoolean().toBoolean(),
|
||||||
|
query('number_format.divide_1000').optional().isBoolean().toBoolean(),
|
||||||
|
query('none_zero').optional().isBoolean().toBoolean(),
|
||||||
|
query('accounts_ids').optional(),
|
||||||
|
query('accounts_ids.*').isNumeric().toInt(),
|
||||||
|
query('orderBy').optional().isIn(['created_at', 'name', 'code']),
|
||||||
|
query('order').optional().isIn(['desc', 'asc']),
|
||||||
|
],
|
||||||
|
async handler(req, res) {
|
||||||
|
const validationErrors = validationResult(req);
|
||||||
|
|
||||||
|
if (!validationErrors.isEmpty()) {
|
||||||
|
return res.boom.badData(null, {
|
||||||
|
code: 'validation_error', ...validationErrors,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const { AccountTransaction, Account } = req.models;
|
||||||
|
|
||||||
|
const filter = {
|
||||||
|
from_date: moment().startOf('year').format('YYYY-MM-DD'),
|
||||||
|
to_date: moment().endOf('year').format('YYYY-MM-DD'),
|
||||||
|
basis: 'cash',
|
||||||
|
number_format: {
|
||||||
|
no_cents: false,
|
||||||
|
divide_1000: false,
|
||||||
|
},
|
||||||
|
none_zero: false,
|
||||||
|
accounts_ids: [],
|
||||||
|
...req.query,
|
||||||
|
};
|
||||||
|
if (!Array.isArray(filter.accounts_ids)) {
|
||||||
|
filter.accounts_ids = [filter.accounts_ids];
|
||||||
|
}
|
||||||
|
filter.accounts_ids = filter.accounts_ids.map((id) => parseInt(id, 10));
|
||||||
|
|
||||||
|
const errorReasons = [];
|
||||||
|
|
||||||
|
if (filter.accounts_ids.length > 0) {
|
||||||
|
const accounts = await Account.query().whereIn('id', filter.accounts_ids);
|
||||||
|
const accountsIds = accounts.map((a) => a.id);
|
||||||
|
|
||||||
|
if (difference(filter.accounts_ids, accountsIds).length > 0) {
|
||||||
|
errorReasons.push({ type: 'FILTER.ACCOUNTS.IDS.NOT.FOUND', code: 200 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (errorReasons.length > 0) {
|
||||||
|
return res.status(400).send({ error: errorReasons });
|
||||||
|
}
|
||||||
|
const accounts = await Account.query()
|
||||||
|
// .remember('general_ledger_accounts')
|
||||||
|
.orderBy('index', 'DESC')
|
||||||
|
.modify('filterAccounts', filter.accounts_ids)
|
||||||
|
.withGraphFetched('type')
|
||||||
|
.withGraphFetched('transactions')
|
||||||
|
.modifyGraph('transactions', (builder) => {
|
||||||
|
builder.modify('filterDateRange', filter.from_date, filter.to_date);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Accounts dependency graph.
|
||||||
|
const accountsGraph = DependencyGraph.fromArray(
|
||||||
|
accounts, { itemId: 'id', parentItemId: 'parentAccountId' }
|
||||||
|
);
|
||||||
|
|
||||||
|
const openingBalanceTransactions = await AccountTransaction.query()
|
||||||
|
// .remember()
|
||||||
|
.modify('filterDateRange', null, filter.from_date)
|
||||||
|
.modify('sumationCreditDebit')
|
||||||
|
.withGraphFetched('account.type');
|
||||||
|
|
||||||
|
const closingBalanceTransactions = await AccountTransaction.query()
|
||||||
|
// .remember()
|
||||||
|
.modify('filterDateRange', null, filter.to_date)
|
||||||
|
.modify('sumationCreditDebit')
|
||||||
|
.withGraphFetched('account.type');
|
||||||
|
|
||||||
|
const opeingBalanceCollection = new JournalPoster(accountsGraph);
|
||||||
|
const closingBalanceCollection = new JournalPoster(accountsGraph);
|
||||||
|
|
||||||
|
opeingBalanceCollection.loadEntries(openingBalanceTransactions);
|
||||||
|
closingBalanceCollection.loadEntries(closingBalanceTransactions);
|
||||||
|
|
||||||
|
// Transaction amount formatter based on the given query.
|
||||||
|
const formatNumber = formatNumberClosure(filter.number_format);
|
||||||
|
|
||||||
|
const accountsResponse = accounts
|
||||||
|
.filter((account) => (
|
||||||
|
account.transactions.length > 0 || !filter.none_zero
|
||||||
|
))
|
||||||
|
.map((account) => ({
|
||||||
|
...pick(account, ['id', 'name', 'code', 'index', 'parentAccountId']),
|
||||||
|
transactions: [
|
||||||
|
...account.transactions.map((transaction) => {
|
||||||
|
let amount = 0;
|
||||||
|
|
||||||
|
if (account.type.normal === 'credit') {
|
||||||
|
amount += transaction.credit - transaction.debit;
|
||||||
|
} else if (account.type.normal === 'debit') {
|
||||||
|
amount += transaction.debit - transaction.credit;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...pick(transaction, ['id', 'note', 'transactionType', 'referenceType',
|
||||||
|
'referenceId', 'date', 'createdAt']),
|
||||||
|
amount,
|
||||||
|
formatted_amount: formatNumber(amount),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
opening: (() => {
|
||||||
|
const openingAmount = opeingBalanceCollection.getAccountBalance(account.id);
|
||||||
|
|
||||||
|
return {
|
||||||
|
date: filter.from_date,
|
||||||
|
amount: openingAmount,
|
||||||
|
formatted_amount: formatNumber(openingAmount),
|
||||||
|
}
|
||||||
|
})(),
|
||||||
|
closing: (() => {
|
||||||
|
const closingAmount = closingBalanceCollection.getAccountBalance(account.id);
|
||||||
|
|
||||||
|
return {
|
||||||
|
date: filter.to_date,
|
||||||
|
amount: closingAmount,
|
||||||
|
formatted_amount: formatNumber(closingAmount),
|
||||||
|
}
|
||||||
|
})(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
query: { ...filter },
|
||||||
|
accounts: Account.toNestedArray(accountsResponse),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
120
server/src/http/controllers/FinancialStatements/JournalSheet.js
Normal file
120
server/src/http/controllers/FinancialStatements/JournalSheet.js
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import { query, oneOf, validationResult } from 'express-validator';
|
||||||
|
import moment from 'moment';
|
||||||
|
import { groupBy } from 'lodash';
|
||||||
|
import JournalPoster from '@/services/Accounting/JournalPoster';
|
||||||
|
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
||||||
|
import { formatNumberClosure } from './FinancialStatementMixin';
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
/**
|
||||||
|
* Router constructor.
|
||||||
|
*/
|
||||||
|
router() {
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.get('/',
|
||||||
|
this.journal.validation,
|
||||||
|
asyncMiddleware(this.journal.handler));
|
||||||
|
|
||||||
|
return router;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the ledger report of the given account.
|
||||||
|
*/
|
||||||
|
journal: {
|
||||||
|
validation: [
|
||||||
|
query('from_date').optional().isISO8601(),
|
||||||
|
query('to_date').optional().isISO8601(),
|
||||||
|
oneOf([
|
||||||
|
query('transaction_types').optional().isArray({ min: 1 }),
|
||||||
|
query('transaction_types.*').optional().isNumeric().toInt(),
|
||||||
|
], [
|
||||||
|
query('transaction_types').optional().trim().escape(),
|
||||||
|
]),
|
||||||
|
oneOf([
|
||||||
|
query('account_ids').optional().isArray({ min: 1 }),
|
||||||
|
query('account_ids.*').optional().isNumeric().toInt(),
|
||||||
|
], [
|
||||||
|
query('account_ids').optional().isNumeric().toInt(),
|
||||||
|
]),
|
||||||
|
query('from_range').optional().isNumeric().toInt(),
|
||||||
|
query('to_range').optional().isNumeric().toInt(),
|
||||||
|
query('number_format.no_cents').optional().isBoolean().toBoolean(),
|
||||||
|
query('number_format.divide_1000').optional().isBoolean().toBoolean(),
|
||||||
|
],
|
||||||
|
async handler(req, res) {
|
||||||
|
const validationErrors = validationResult(req);
|
||||||
|
|
||||||
|
if (!validationErrors.isEmpty()) {
|
||||||
|
return res.boom.badData(null, {
|
||||||
|
code: 'validation_error', ...validationErrors,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const { AccountTransaction } = req.models;
|
||||||
|
|
||||||
|
const filter = {
|
||||||
|
from_date: moment().startOf('year').format('YYYY-MM-DD'),
|
||||||
|
to_date: moment().endOf('year').format('YYYY-MM-DD'),
|
||||||
|
from_range: null,
|
||||||
|
to_range: null,
|
||||||
|
account_ids: [],
|
||||||
|
transaction_types: [],
|
||||||
|
number_format: {
|
||||||
|
no_cents: false,
|
||||||
|
divide_1000: false,
|
||||||
|
},
|
||||||
|
...req.query,
|
||||||
|
};
|
||||||
|
if (!Array.isArray(filter.transaction_types)) {
|
||||||
|
filter.transaction_types = [filter.transaction_types];
|
||||||
|
}
|
||||||
|
if (!Array.isArray(filter.account_ids)) {
|
||||||
|
filter.account_ids = [filter.account_ids];
|
||||||
|
}
|
||||||
|
filter.account_ids = filter.account_ids.map((id) => parseInt(id, 10));
|
||||||
|
|
||||||
|
const accountsJournalEntries = await AccountTransaction.query()
|
||||||
|
// .remember()
|
||||||
|
.modify('filterDateRange', filter.from_date, filter.to_date)
|
||||||
|
.modify('filterAccounts', filter.account_ids)
|
||||||
|
.modify('filterTransactionTypes', filter.transaction_types)
|
||||||
|
.modify('filterAmountRange', filter.from_range, filter.to_range)
|
||||||
|
.withGraphFetched('account.type');
|
||||||
|
|
||||||
|
const formatNumber = formatNumberClosure(filter.number_format);
|
||||||
|
|
||||||
|
const journalGrouped = groupBy(accountsJournalEntries,
|
||||||
|
(entry) => `${entry.referenceId}-${entry.referenceType}`);
|
||||||
|
|
||||||
|
const journal = Object.keys(journalGrouped).map((key) => {
|
||||||
|
const transactionsGroup = journalGrouped[key];
|
||||||
|
|
||||||
|
const journalPoster = new JournalPoster();
|
||||||
|
journalPoster.loadEntries(transactionsGroup);
|
||||||
|
|
||||||
|
const trialBalance = journalPoster.getTrialBalance();
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: key,
|
||||||
|
entries: transactionsGroup,
|
||||||
|
|
||||||
|
credit: trialBalance.credit,
|
||||||
|
debit: trialBalance.debit,
|
||||||
|
|
||||||
|
formatted_credit: formatNumber(trialBalance.credit),
|
||||||
|
formatted_debit: formatNumber(trialBalance.debit),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
query: { ...filter },
|
||||||
|
journal,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,235 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import { query, oneOf, validationResult } from 'express-validator';
|
||||||
|
import moment from 'moment';
|
||||||
|
import { pick } from 'lodash';
|
||||||
|
import JournalPoster from '@/services/Accounting/JournalPoster';
|
||||||
|
import { dateRangeCollection } from '@/utils';
|
||||||
|
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
||||||
|
import { formatNumberClosure } from './FinancialStatementMixin';
|
||||||
|
import DependencyGraph from '@/lib/DependencyGraph';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
/**
|
||||||
|
* Router constructor.
|
||||||
|
*/
|
||||||
|
router() {
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.get('/',
|
||||||
|
this.profitLossSheet.validation,
|
||||||
|
asyncMiddleware(this.profitLossSheet.handler));
|
||||||
|
|
||||||
|
return router;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve profit/loss financial statement.
|
||||||
|
*/
|
||||||
|
profitLossSheet: {
|
||||||
|
validation: [
|
||||||
|
query('basis').optional(),
|
||||||
|
query('from_date').optional().isISO8601(),
|
||||||
|
query('to_date').optional().isISO8601(),
|
||||||
|
query('number_format.no_cents').optional().isBoolean(),
|
||||||
|
query('number_format.divide_1000').optional().isBoolean(),
|
||||||
|
query('basis').optional(),
|
||||||
|
query('none_zero').optional().isBoolean().toBoolean(),
|
||||||
|
query('account_ids').isArray().optional(),
|
||||||
|
query('account_ids.*').isNumeric().toInt(),
|
||||||
|
query('display_columns_type').optional().isIn([
|
||||||
|
'total', 'date_periods',
|
||||||
|
]),
|
||||||
|
query('display_columns_by').optional({ nullable: true, checkFalsy: true })
|
||||||
|
.isIn(['year', 'month', 'week', 'day', 'quarter']),
|
||||||
|
],
|
||||||
|
async handler(req, res) {
|
||||||
|
const validationErrors = validationResult(req);
|
||||||
|
|
||||||
|
if (!validationErrors.isEmpty()) {
|
||||||
|
return res.boom.badData(null, {
|
||||||
|
code: 'validation_error', ...validationErrors,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const { Account, AccountType } = req.models;
|
||||||
|
const filter = {
|
||||||
|
from_date: moment().startOf('year').format('YYYY-MM-DD'),
|
||||||
|
to_date: moment().endOf('year').format('YYYY-MM-DD'),
|
||||||
|
number_format: {
|
||||||
|
no_cents: false,
|
||||||
|
divide_1000: false,
|
||||||
|
},
|
||||||
|
basis: 'accural',
|
||||||
|
none_zero: false,
|
||||||
|
display_columns_type: 'total',
|
||||||
|
display_columns_by: 'month',
|
||||||
|
account_ids: [],
|
||||||
|
...req.query,
|
||||||
|
};
|
||||||
|
if (!Array.isArray(filter.account_ids)) {
|
||||||
|
filter.account_ids = [filter.account_ids];
|
||||||
|
}
|
||||||
|
const incomeStatementTypes = await AccountType.query().where('income_sheet', true);
|
||||||
|
|
||||||
|
// Fetch all income accounts from storage.
|
||||||
|
const accounts = await Account.query()
|
||||||
|
// .remember('profit_loss_accounts')
|
||||||
|
.modify('filterAccounts', filter.account_ids)
|
||||||
|
.whereIn('account_type_id', incomeStatementTypes.map((t) => t.id))
|
||||||
|
.withGraphFetched('type')
|
||||||
|
.withGraphFetched('transactions');
|
||||||
|
|
||||||
|
// Accounts dependency graph.
|
||||||
|
const accountsGraph = DependencyGraph.fromArray(
|
||||||
|
accounts, { itemId: 'id', parentItemId: 'parentAccountId' }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Filter all none zero accounts if it was enabled.
|
||||||
|
const filteredAccounts = accounts.filter((account) => (
|
||||||
|
account.transactions.length > 0 || !filter.none_zero
|
||||||
|
));
|
||||||
|
const journalEntriesCollected = Account.collectJournalEntries(accounts);
|
||||||
|
const journalEntries = new JournalPoster(accountsGraph);
|
||||||
|
journalEntries.loadEntries(journalEntriesCollected);
|
||||||
|
|
||||||
|
// Account balance formmatter based on the given query.
|
||||||
|
const numberFormatter = formatNumberClosure(filter.number_format);
|
||||||
|
const comparatorDateType = filter.display_columns_type === 'total'
|
||||||
|
? 'day' : filter.display_columns_by;
|
||||||
|
|
||||||
|
// Gets the date range set from start to end date.
|
||||||
|
const dateRangeSet = dateRangeCollection(
|
||||||
|
filter.from_date,
|
||||||
|
filter.to_date,
|
||||||
|
comparatorDateType,
|
||||||
|
);
|
||||||
|
|
||||||
|
const accountsMapper = (incomeExpenseAccounts) => (
|
||||||
|
incomeExpenseAccounts.map((account) => ({
|
||||||
|
...pick(account, ['id', 'index', 'name', 'code', 'parentAccountId']),
|
||||||
|
|
||||||
|
// Total closing balance of the account.
|
||||||
|
...(filter.display_columns_type === 'total') && {
|
||||||
|
total: (() => {
|
||||||
|
const amount = journalEntries.getAccountBalance(account.id, filter.to_date);
|
||||||
|
return { amount, date: filter.to_date, formatted_amount: numberFormatter(amount) };
|
||||||
|
})(),
|
||||||
|
},
|
||||||
|
// Date periods when display columns type `periods`.
|
||||||
|
...(filter.display_columns_type === 'date_periods') && {
|
||||||
|
periods: dateRangeSet.map((date) => {
|
||||||
|
const type = comparatorDateType;
|
||||||
|
const amount = journalEntries.getAccountBalance(account.id, date, type);
|
||||||
|
|
||||||
|
return { date, amount, formatted_amount: numberFormatter(amount) };
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
})));
|
||||||
|
|
||||||
|
const totalAccountsReducer = (incomeExpenseAccounts) => (
|
||||||
|
incomeExpenseAccounts.reduce((acc, account) => {
|
||||||
|
const amount = (account) ? account.total.amount : 0;
|
||||||
|
return amount + acc;
|
||||||
|
}, 0));
|
||||||
|
|
||||||
|
const accountsIncome = Account.toNestedArray(accountsMapper(filteredAccounts
|
||||||
|
.filter((account) => account.type.normal === 'credit')));
|
||||||
|
|
||||||
|
const accountsExpenses = Account.toNestedArray(accountsMapper(filteredAccounts
|
||||||
|
.filter((account) => account.type.normal === 'debit')));
|
||||||
|
|
||||||
|
// @return {Array}
|
||||||
|
const totalPeriodsMapper = (incomeExpenseAccounts) => (
|
||||||
|
Object.values(dateRangeSet.reduce((acc, date, index) => {
|
||||||
|
let amount = 0;
|
||||||
|
|
||||||
|
incomeExpenseAccounts.forEach((account) => {
|
||||||
|
const currentDate = account.periods[index];
|
||||||
|
amount += currentDate.amount || 0;
|
||||||
|
});
|
||||||
|
acc[date] = { date, amount, formatted_amount: numberFormatter(amount) };
|
||||||
|
return acc;
|
||||||
|
}, {})));
|
||||||
|
|
||||||
|
// Total income(date) - Total expenses(date) = Net income(date)
|
||||||
|
// @return {Array}
|
||||||
|
const netIncomePeriodsMapper = (totalIncomeAcocunts, totalExpenseAccounts) => (
|
||||||
|
dateRangeSet.map((date, index) => {
|
||||||
|
const totalIncome = totalIncomeAcocunts[index];
|
||||||
|
const totalExpenses = totalExpenseAccounts[index];
|
||||||
|
|
||||||
|
let amount = totalIncome.amount || 0;
|
||||||
|
amount -= totalExpenses.amount || 0;
|
||||||
|
return { date, amount, formatted_amount: numberFormatter(amount) };
|
||||||
|
}));
|
||||||
|
|
||||||
|
// @return {Object}
|
||||||
|
const netIncomeTotal = (totalIncome, totalExpenses) => {
|
||||||
|
const netIncomeAmount = totalIncome.amount - totalExpenses.amount;
|
||||||
|
return { amount: netIncomeAmount, formatted_amount: netIncomeAmount, date: filter.to_date };
|
||||||
|
};
|
||||||
|
|
||||||
|
const incomeResponse = {
|
||||||
|
entry_normal: 'credit',
|
||||||
|
accounts: accountsIncome,
|
||||||
|
...(filter.display_columns_type === 'total') && (() => {
|
||||||
|
const totalIncomeAccounts = totalAccountsReducer(accountsIncome);
|
||||||
|
return {
|
||||||
|
total: {
|
||||||
|
amount: totalIncomeAccounts,
|
||||||
|
date: filter.to_date,
|
||||||
|
formatted_amount: numberFormatter(totalIncomeAccounts),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
})(),
|
||||||
|
...(filter.display_columns_type === 'date_periods') && {
|
||||||
|
total_periods: [
|
||||||
|
...totalPeriodsMapper(accountsIncome),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const expenseResponse = {
|
||||||
|
entry_normal: 'debit',
|
||||||
|
accounts: accountsExpenses,
|
||||||
|
...(filter.display_columns_type === 'total') && (() => {
|
||||||
|
const totalExpensesAccounts = totalAccountsReducer(accountsExpenses);
|
||||||
|
return {
|
||||||
|
total: {
|
||||||
|
amount: totalExpensesAccounts,
|
||||||
|
date: filter.to_date,
|
||||||
|
formatted_amount: numberFormatter(totalExpensesAccounts),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
})(),
|
||||||
|
...(filter.display_columns_type === 'date_periods') && {
|
||||||
|
total_periods: [
|
||||||
|
...totalPeriodsMapper(accountsExpenses),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const netIncomeResponse = {
|
||||||
|
...(filter.display_columns_type === 'total') && {
|
||||||
|
total: {
|
||||||
|
...netIncomeTotal(incomeResponse.total, expenseResponse.total),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...(filter.display_columns_type === 'date_periods') && {
|
||||||
|
total_periods: [
|
||||||
|
...netIncomePeriodsMapper(
|
||||||
|
incomeResponse.total_periods,
|
||||||
|
expenseResponse.total_periods,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return res.status(200).send({
|
||||||
|
query: { ...filter },
|
||||||
|
columns: [...dateRangeSet],
|
||||||
|
profitLoss: {
|
||||||
|
income: incomeResponse,
|
||||||
|
expenses: expenseResponse,
|
||||||
|
net_income: netIncomeResponse,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import { query, validationResult } from 'express-validator';
|
||||||
|
import moment from 'moment';
|
||||||
|
import JournalPoster from '@/services/Accounting/JournalPoster';
|
||||||
|
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
||||||
|
import DependencyGraph from '@/lib/DependencyGraph';
|
||||||
|
import { formatNumberClosure }from './FinancialStatementMixin';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
/**
|
||||||
|
* Router constructor.
|
||||||
|
*/
|
||||||
|
router() {
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.get('/',
|
||||||
|
this.trialBalanceSheet.validation,
|
||||||
|
asyncMiddleware(this.trialBalanceSheet.handler));
|
||||||
|
|
||||||
|
return router;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the trial balance sheet.
|
||||||
|
*/
|
||||||
|
trialBalanceSheet: {
|
||||||
|
validation: [
|
||||||
|
query('basis').optional(),
|
||||||
|
query('from_date').optional().isISO8601(),
|
||||||
|
query('to_date').optional().isISO8601(),
|
||||||
|
query('number_format.no_cents').optional().isBoolean().toBoolean(),
|
||||||
|
query('number_format.1000_divide').optional().isBoolean().toBoolean(),
|
||||||
|
query('account_ids').isArray().optional(),
|
||||||
|
query('account_ids.*').isNumeric().toInt(),
|
||||||
|
query('basis').optional(),
|
||||||
|
query('none_zero').optional().isBoolean().toBoolean(),
|
||||||
|
],
|
||||||
|
async handler(req, res) {
|
||||||
|
const validationErrors = validationResult(req);
|
||||||
|
|
||||||
|
if (!validationErrors.isEmpty()) {
|
||||||
|
return res.boom.badData(null, {
|
||||||
|
code: 'validation_error', ...validationErrors,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const { Account } = req.models;
|
||||||
|
const filter = {
|
||||||
|
from_date: moment().startOf('year').format('YYYY-MM-DD'),
|
||||||
|
to_date: moment().endOf('year').format('YYYY-MM-DD'),
|
||||||
|
number_format: {
|
||||||
|
no_cents: false,
|
||||||
|
divide_1000: false,
|
||||||
|
},
|
||||||
|
basis: 'accural',
|
||||||
|
none_zero: false,
|
||||||
|
account_ids: [],
|
||||||
|
...req.query,
|
||||||
|
};
|
||||||
|
if (!Array.isArray(filter.account_ids)) {
|
||||||
|
filter.account_ids = [filter.account_ids];
|
||||||
|
}
|
||||||
|
|
||||||
|
const accounts = await Account.query()
|
||||||
|
// .remember('trial_balance_accounts')
|
||||||
|
.modify('filterAccounts', filter.account_ids)
|
||||||
|
.withGraphFetched('type')
|
||||||
|
.withGraphFetched('transactions')
|
||||||
|
.modifyGraph('transactions', (builder) => {
|
||||||
|
builder.modify('sumationCreditDebit');
|
||||||
|
builder.modify('filterDateRange', filter.from_date, filter.to_date);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Accounts dependency graph.
|
||||||
|
const accountsGraph = DependencyGraph.fromArray(
|
||||||
|
accounts, { itemId: 'id', parentItemId: 'parentAccountId' }
|
||||||
|
);
|
||||||
|
|
||||||
|
const journalEntriesCollect = Account.collectJournalEntries(accounts);
|
||||||
|
const journalEntries = new JournalPoster(accountsGraph);
|
||||||
|
journalEntries.loadEntries(journalEntriesCollect);
|
||||||
|
|
||||||
|
// Account balance formmatter based on the given query.
|
||||||
|
const balanceFormatter = formatNumberClosure(filter.number_format);
|
||||||
|
|
||||||
|
const accountsResponse = accounts
|
||||||
|
.filter((account) => (
|
||||||
|
account.transactions.length > 0 || !filter.none_zero
|
||||||
|
))
|
||||||
|
.map((account) => {
|
||||||
|
const trial = journalEntries.getTrialBalanceWithDepands(account.id);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: account.id,
|
||||||
|
parentAccountId: account.parentAccountId,
|
||||||
|
name: account.name,
|
||||||
|
code: account.code,
|
||||||
|
accountNormal: account.type.normal,
|
||||||
|
|
||||||
|
credit: trial.credit,
|
||||||
|
debit: trial.debit,
|
||||||
|
balance: trial.balance,
|
||||||
|
|
||||||
|
formatted_credit: balanceFormatter(trial.credit),
|
||||||
|
formatted_debit: balanceFormatter(trial.debit),
|
||||||
|
formatted_balance: balanceFormatter(trial.balance),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
query: { ...filter },
|
||||||
|
accounts: [...Account.toNestedArray(accountsResponse) ],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -37,6 +37,10 @@ export default {
|
|||||||
this.deleteItem.validation,
|
this.deleteItem.validation,
|
||||||
asyncMiddleware(this.deleteItem.handler));
|
asyncMiddleware(this.deleteItem.handler));
|
||||||
|
|
||||||
|
router.delete('/',
|
||||||
|
this.bulkDeleteItems.validation,
|
||||||
|
asyncMiddleware(this.bulkDeleteItems.handler));
|
||||||
|
|
||||||
router.get('/',
|
router.get('/',
|
||||||
this.listItems.validation,
|
this.listItems.validation,
|
||||||
asyncMiddleware(this.listItems.handler));
|
asyncMiddleware(this.listItems.handler));
|
||||||
@@ -337,6 +341,44 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bulk delete the given items ids.
|
||||||
|
*/
|
||||||
|
bulkDeleteItems: {
|
||||||
|
validation: [
|
||||||
|
query('ids').isArray({ min: 2 }),
|
||||||
|
query('ids.*').isNumeric().toInt(),
|
||||||
|
],
|
||||||
|
async handler(req, res) {
|
||||||
|
const validationErrors = validationResult(req);
|
||||||
|
|
||||||
|
if (!validationErrors.isEmpty()) {
|
||||||
|
return res.boom.badData(null, {
|
||||||
|
code: 'validation_error', ...validationErrors,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const filter = { ids: [], ...req.query };
|
||||||
|
const { Item } = req.models;
|
||||||
|
|
||||||
|
const items = await Item.query().whereIn('id', filter.ids);
|
||||||
|
|
||||||
|
const storedItemsIds = items.map((a) => a.id);
|
||||||
|
const notFoundItems = difference(filter.ids, storedItemsIds);
|
||||||
|
|
||||||
|
// Validate the not found items.
|
||||||
|
if (notFoundItems.length > 0) {
|
||||||
|
return res.status(404).send({
|
||||||
|
errors: [{ type: 'ITEMS.NOT.FOUND', code: 200, ids: notFoundItems }],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the given items ids.
|
||||||
|
await Item.query().whereIn('id', storedItemsIds).delete();
|
||||||
|
|
||||||
|
return res.status(200).send();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrive the list items with pagination meta.
|
* Retrive the list items with pagination meta.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import Media from '@/http/controllers/Media';
|
|||||||
import JWTAuth from '@/http/middleware/jwtAuth';
|
import JWTAuth from '@/http/middleware/jwtAuth';
|
||||||
import TenancyMiddleware from '@/http/middleware/TenancyMiddleware';
|
import TenancyMiddleware from '@/http/middleware/TenancyMiddleware';
|
||||||
|
|
||||||
|
|
||||||
export default (app) => {
|
export default (app) => {
|
||||||
// app.use('/api/oauth2', OAuth2.router());
|
// app.use('/api/oauth2', OAuth2.router());
|
||||||
app.use('/api/auth', Authentication.router());
|
app.use('/api/auth', Authentication.router());
|
||||||
|
|||||||
349
server/src/lib/DependencyGraph/index.js
Normal file
349
server/src/lib/DependencyGraph/index.js
Normal file
@@ -0,0 +1,349 @@
|
|||||||
|
/**
|
||||||
|
* A simple dependency graph
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper for creating a Topological Sort using Depth-First-Search on a set of edges.
|
||||||
|
*
|
||||||
|
* Detects cycles and throws an Error if one is detected (unless the "circular"
|
||||||
|
* parameter is "true" in which case it ignores them).
|
||||||
|
*
|
||||||
|
* @param edges The set of edges to DFS through
|
||||||
|
* @param leavesOnly Whether to only return "leaf" nodes (ones who have no edges)
|
||||||
|
* @param result An array in which the results will be populated
|
||||||
|
* @param circular A boolean to allow circular dependencies
|
||||||
|
*/
|
||||||
|
function createDFS(edges, leavesOnly, result, circular) {
|
||||||
|
var visited = {};
|
||||||
|
return function(start) {
|
||||||
|
if (visited[start]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var inCurrentPath = {};
|
||||||
|
var currentPath = [];
|
||||||
|
var todo = []; // used as a stack
|
||||||
|
todo.push({ node: start, processed: false });
|
||||||
|
while (todo.length > 0) {
|
||||||
|
var current = todo[todo.length - 1]; // peek at the todo stack
|
||||||
|
var processed = current.processed;
|
||||||
|
var node = current.node;
|
||||||
|
if (!processed) {
|
||||||
|
// Haven't visited edges yet (visiting phase)
|
||||||
|
if (visited[node]) {
|
||||||
|
todo.pop();
|
||||||
|
continue;
|
||||||
|
} else if (inCurrentPath[node]) {
|
||||||
|
// It's not a DAG
|
||||||
|
if (circular) {
|
||||||
|
todo.pop();
|
||||||
|
// If we're tolerating cycles, don't revisit the node
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
currentPath.push(node);
|
||||||
|
throw new DepGraphCycleError(currentPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
inCurrentPath[node] = true;
|
||||||
|
currentPath.push(node);
|
||||||
|
var nodeEdges = edges[node];
|
||||||
|
// (push edges onto the todo stack in reverse order to be order-compatible with the old DFS implementation)
|
||||||
|
for (var i = nodeEdges.length - 1; i >= 0; i--) {
|
||||||
|
todo.push({ node: nodeEdges[i], processed: false });
|
||||||
|
}
|
||||||
|
current.processed = true;
|
||||||
|
} else {
|
||||||
|
// Have visited edges (stack unrolling phase)
|
||||||
|
todo.pop();
|
||||||
|
currentPath.pop();
|
||||||
|
inCurrentPath[node] = false;
|
||||||
|
visited[node] = true;
|
||||||
|
if (!leavesOnly || edges[node].length === 0) {
|
||||||
|
result.push(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple Dependency Graph
|
||||||
|
*/
|
||||||
|
var DepGraph = (DepGraph = function DepGraph(opts) {
|
||||||
|
this.nodes = {}; // Node -> Node/Data (treated like a Set)
|
||||||
|
this.outgoingEdges = {}; // Node -> [Dependency Node]
|
||||||
|
this.incomingEdges = {}; // Node -> [Dependant Node]
|
||||||
|
this.circular = opts && !!opts.circular; // Allows circular deps
|
||||||
|
});
|
||||||
|
|
||||||
|
DepGraph.fromArray = (items, options = { itemId: 'id', parentItemId: 'parent_id' }) => {
|
||||||
|
const depGraph = new DepGraph();
|
||||||
|
|
||||||
|
items.forEach((item) => {
|
||||||
|
depGraph.addNode(item[options.itemId], item);
|
||||||
|
});
|
||||||
|
items.forEach((item) => {
|
||||||
|
if (item[options.parentItemId]) {
|
||||||
|
depGraph.addDependency(item[options.parentItemId], item[options.itemId]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return depGraph;
|
||||||
|
}
|
||||||
|
|
||||||
|
DepGraph.prototype = {
|
||||||
|
/**
|
||||||
|
* The number of nodes in the graph.
|
||||||
|
*/
|
||||||
|
size: function() {
|
||||||
|
return Object.keys(this.nodes).length;
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Add a node to the dependency graph. If a node already exists, this method will do nothing.
|
||||||
|
*/
|
||||||
|
addNode: function(node, data) {
|
||||||
|
if (!this.hasNode(node)) {
|
||||||
|
// Checking the arguments length allows the user to add a node with undefined data
|
||||||
|
if (arguments.length === 2) {
|
||||||
|
this.nodes[node] = data;
|
||||||
|
} else {
|
||||||
|
this.nodes[node] = node;
|
||||||
|
}
|
||||||
|
this.outgoingEdges[node] = [];
|
||||||
|
this.incomingEdges[node] = [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Remove a node from the dependency graph. If a node does not exist, this method will do nothing.
|
||||||
|
*/
|
||||||
|
removeNode: function(node) {
|
||||||
|
if (this.hasNode(node)) {
|
||||||
|
delete this.nodes[node];
|
||||||
|
delete this.outgoingEdges[node];
|
||||||
|
delete this.incomingEdges[node];
|
||||||
|
[this.incomingEdges, this.outgoingEdges].forEach(function(edgeList) {
|
||||||
|
Object.keys(edgeList).forEach(function(key) {
|
||||||
|
var idx = edgeList[key].indexOf(node);
|
||||||
|
if (idx >= 0) {
|
||||||
|
edgeList[key].splice(idx, 1);
|
||||||
|
}
|
||||||
|
}, this);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Check if a node exists in the graph
|
||||||
|
*/
|
||||||
|
hasNode: function(node) {
|
||||||
|
return this.nodes.hasOwnProperty(node);
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Get the data associated with a node name
|
||||||
|
*/
|
||||||
|
getNodeData: function(node) {
|
||||||
|
if (this.hasNode(node)) {
|
||||||
|
return this.nodes[node];
|
||||||
|
} else {
|
||||||
|
throw new Error("Node does not exist: " + node);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the associated data for a given node name. If the node does not exist, this method will throw an error
|
||||||
|
*/
|
||||||
|
setNodeData: function(node, data) {
|
||||||
|
if (this.hasNode(node)) {
|
||||||
|
this.nodes[node] = data;
|
||||||
|
} else {
|
||||||
|
throw new Error("Node does not exist: " + node);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Add a dependency between two nodes. If either of the nodes does not exist,
|
||||||
|
* an Error will be thrown.
|
||||||
|
*/
|
||||||
|
addDependency: function(from, to) {
|
||||||
|
if (!this.hasNode(from)) {
|
||||||
|
throw new Error("Node does not exist: " + from);
|
||||||
|
}
|
||||||
|
if (!this.hasNode(to)) {
|
||||||
|
throw new Error("Node does not exist: " + to);
|
||||||
|
}
|
||||||
|
if (this.outgoingEdges[from].indexOf(to) === -1) {
|
||||||
|
this.outgoingEdges[from].push(to);
|
||||||
|
}
|
||||||
|
if (this.incomingEdges[to].indexOf(from) === -1) {
|
||||||
|
this.incomingEdges[to].push(from);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Remove a dependency between two nodes.
|
||||||
|
*/
|
||||||
|
removeDependency: function(from, to) {
|
||||||
|
var idx;
|
||||||
|
if (this.hasNode(from)) {
|
||||||
|
idx = this.outgoingEdges[from].indexOf(to);
|
||||||
|
if (idx >= 0) {
|
||||||
|
this.outgoingEdges[from].splice(idx, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.hasNode(to)) {
|
||||||
|
idx = this.incomingEdges[to].indexOf(from);
|
||||||
|
if (idx >= 0) {
|
||||||
|
this.incomingEdges[to].splice(idx, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Return a clone of the dependency graph. If any custom data is attached
|
||||||
|
* to the nodes, it will only be shallow copied.
|
||||||
|
*/
|
||||||
|
clone: function() {
|
||||||
|
var source = this;
|
||||||
|
var result = new DepGraph();
|
||||||
|
var keys = Object.keys(source.nodes);
|
||||||
|
keys.forEach(function(n) {
|
||||||
|
result.nodes[n] = source.nodes[n];
|
||||||
|
result.outgoingEdges[n] = source.outgoingEdges[n].slice(0);
|
||||||
|
result.incomingEdges[n] = source.incomingEdges[n].slice(0);
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Get an array containing the nodes that the specified node depends on (transitively).
|
||||||
|
*
|
||||||
|
* Throws an Error if the graph has a cycle, or the specified node does not exist.
|
||||||
|
*
|
||||||
|
* If `leavesOnly` is true, only nodes that do not depend on any other nodes will be returned
|
||||||
|
* in the array.
|
||||||
|
*/
|
||||||
|
dependenciesOf: function(node, leavesOnly) {
|
||||||
|
if (this.hasNode(node)) {
|
||||||
|
var result = [];
|
||||||
|
var DFS = createDFS(
|
||||||
|
this.outgoingEdges,
|
||||||
|
leavesOnly,
|
||||||
|
result,
|
||||||
|
this.circular
|
||||||
|
);
|
||||||
|
DFS(node);
|
||||||
|
var idx = result.indexOf(node);
|
||||||
|
if (idx >= 0) {
|
||||||
|
result.splice(idx, 1);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} else {
|
||||||
|
throw new Error("Node does not exist: " + node);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* get an array containing the nodes that depend on the specified node (transitively).
|
||||||
|
*
|
||||||
|
* Throws an Error if the graph has a cycle, or the specified node does not exist.
|
||||||
|
*
|
||||||
|
* If `leavesOnly` is true, only nodes that do not have any dependants will be returned in the array.
|
||||||
|
*/
|
||||||
|
dependantsOf: function(node, leavesOnly) {
|
||||||
|
if (this.hasNode(node)) {
|
||||||
|
var result = [];
|
||||||
|
var DFS = createDFS(
|
||||||
|
this.incomingEdges,
|
||||||
|
leavesOnly,
|
||||||
|
result,
|
||||||
|
this.circular
|
||||||
|
);
|
||||||
|
DFS(node);
|
||||||
|
var idx = result.indexOf(node);
|
||||||
|
if (idx >= 0) {
|
||||||
|
result.splice(idx, 1);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} else {
|
||||||
|
throw new Error("Node does not exist: " + node);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Construct the overall processing order for the dependency graph.
|
||||||
|
*
|
||||||
|
* Throws an Error if the graph has a cycle.
|
||||||
|
*
|
||||||
|
* If `leavesOnly` is true, only nodes that do not depend on any other nodes will be returned.
|
||||||
|
*/
|
||||||
|
overallOrder: function(leavesOnly) {
|
||||||
|
var self = this;
|
||||||
|
var result = [];
|
||||||
|
var keys = Object.keys(this.nodes);
|
||||||
|
if (keys.length === 0) {
|
||||||
|
return result; // Empty graph
|
||||||
|
} else {
|
||||||
|
if (!this.circular) {
|
||||||
|
// Look for cycles - we run the DFS starting at all the nodes in case there
|
||||||
|
// are several disconnected subgraphs inside this dependency graph.
|
||||||
|
var CycleDFS = createDFS(this.outgoingEdges, false, [], this.circular);
|
||||||
|
keys.forEach(function(n) {
|
||||||
|
CycleDFS(n);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var DFS = createDFS(
|
||||||
|
this.outgoingEdges,
|
||||||
|
leavesOnly,
|
||||||
|
result,
|
||||||
|
this.circular
|
||||||
|
);
|
||||||
|
// Find all potential starting points (nodes with nothing depending on them) an
|
||||||
|
// run a DFS starting at these points to get the order
|
||||||
|
keys
|
||||||
|
.filter(function(node) {
|
||||||
|
return self.incomingEdges[node].length === 0;
|
||||||
|
})
|
||||||
|
.forEach(function(n) {
|
||||||
|
DFS(n);
|
||||||
|
});
|
||||||
|
|
||||||
|
// If we're allowing cycles - we need to run the DFS against any remaining
|
||||||
|
// nodes that did not end up in the initial result (as they are part of a
|
||||||
|
// subgraph that does not have a clear starting point)
|
||||||
|
if (this.circular) {
|
||||||
|
keys
|
||||||
|
.filter(function(node) {
|
||||||
|
return result.indexOf(node) === -1;
|
||||||
|
})
|
||||||
|
.forEach(function(n) {
|
||||||
|
DFS(n);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
mapNodes(mapper) {
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cycle error, including the path of the cycle.
|
||||||
|
*/
|
||||||
|
var DepGraphCycleError = (exports.DepGraphCycleError = function(cyclePath) {
|
||||||
|
var message = "Dependency Cycle Found: " + cyclePath.join(" -> ");
|
||||||
|
var instance = new Error(message);
|
||||||
|
instance.cyclePath = cyclePath;
|
||||||
|
Object.setPrototypeOf(instance, Object.getPrototypeOf(this));
|
||||||
|
if (Error.captureStackTrace) {
|
||||||
|
Error.captureStackTrace(instance, DepGraphCycleError);
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
|
});
|
||||||
|
DepGraphCycleError.prototype = Object.create(Error.prototype, {
|
||||||
|
constructor: {
|
||||||
|
value: Error,
|
||||||
|
enumerable: false,
|
||||||
|
writable: true,
|
||||||
|
configurable: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Object.setPrototypeOf(DepGraphCycleError, Error);
|
||||||
|
|
||||||
|
export default DepGraph;
|
||||||
9
server/src/lib/NestedSet/NestedSetNode.js
Normal file
9
server/src/lib/NestedSet/NestedSetNode.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
|
||||||
|
|
||||||
|
class NestedSetNode {
|
||||||
|
|
||||||
|
// Saves
|
||||||
|
appendToNode($parent) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,7 +9,8 @@ import {
|
|||||||
import CachableQueryBuilder from '@/lib/Cachable/CachableQueryBuilder';
|
import CachableQueryBuilder from '@/lib/Cachable/CachableQueryBuilder';
|
||||||
import CachableModel from '@/lib/Cachable/CachableModel';
|
import CachableModel from '@/lib/Cachable/CachableModel';
|
||||||
import DateSession from '@/models/DateSession';
|
import DateSession from '@/models/DateSession';
|
||||||
|
import { flatToNestedArray } from '@/utils';
|
||||||
|
import DependencyGraph from '@/lib/DependencyGraph';
|
||||||
|
|
||||||
export default class Account extends mixin(TenantModel, [CachableModel, DateSession]) {
|
export default class Account extends mixin(TenantModel, [CachableModel, DateSession]) {
|
||||||
/**
|
/**
|
||||||
@@ -26,6 +27,25 @@ export default class Account extends mixin(TenantModel, [CachableModel, DateSess
|
|||||||
return CachableQueryBuilder;
|
return CachableQueryBuilder;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static query(...args) {
|
||||||
|
return super.query(...args).runAfter((result) => {
|
||||||
|
if (Array.isArray(result)) {
|
||||||
|
return this.isDepGraph ?
|
||||||
|
Account.toDependencyGraph(result) :
|
||||||
|
this.collection.from(result);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert the array result to dependency graph.
|
||||||
|
*/
|
||||||
|
static depGraph() {
|
||||||
|
this.isDepGraph = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Model modifiers.
|
* Model modifiers.
|
||||||
*/
|
*/
|
||||||
@@ -106,4 +126,19 @@ export default class Account extends mixin(TenantModel, [CachableModel, DateSess
|
|||||||
accountNormal: account.type.normal,
|
accountNormal: account.type.normal,
|
||||||
}))));
|
}))));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts flatten accounts list to nested array.
|
||||||
|
* @param {Array} accounts
|
||||||
|
* @param {Object} options
|
||||||
|
*/
|
||||||
|
static toNestedArray(accounts, options = { children: 'children' }) {
|
||||||
|
return flatToNestedArray(accounts, { id: 'id', parentId: 'parentAccountId' })
|
||||||
|
}
|
||||||
|
|
||||||
|
static toDependencyGraph(accounts) {
|
||||||
|
return DependencyGraph.fromArray(
|
||||||
|
accounts, { itemId: 'id', parentItemId: 'parentAccountId' }
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { Model } from 'objection';
|
import { Model, mixin } from 'objection';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import TenantModel from '@/models/TenantModel';
|
import TenantModel from '@/models/TenantModel';
|
||||||
import CachableQueryBuilder from '@/lib/Cachable/CachableQueryBuilder';
|
import CachableQueryBuilder from '@/lib/Cachable/CachableQueryBuilder';
|
||||||
|
import CachableModel from '@/lib/Cachable/CachableModel';
|
||||||
|
import DateSession from '@/models/DateSession';
|
||||||
|
|
||||||
|
|
||||||
export default class AccountTransaction extends TenantModel {
|
export default class AccountTransaction extends mixin(TenantModel, [CachableModel, DateSession]) {
|
||||||
/**
|
/**
|
||||||
* Table name
|
* Table name
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -32,6 +32,11 @@ export default class Option extends TenantModel {
|
|||||||
return MetableCollection;
|
return MetableCollection;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the given options is defined or either not.
|
||||||
|
* @param {Array} options
|
||||||
|
* @return {Boolean}
|
||||||
|
*/
|
||||||
static validateDefined(options) {
|
static validateDefined(options) {
|
||||||
const notDefined = [];
|
const notDefined = [];
|
||||||
|
|
||||||
|
|||||||
@@ -4,16 +4,21 @@ import JournalEntry from '@/services/Accounting/JournalEntry';
|
|||||||
import AccountTransaction from '@/models/AccountTransaction';
|
import AccountTransaction from '@/models/AccountTransaction';
|
||||||
import AccountBalance from '@/models/AccountBalance';
|
import AccountBalance from '@/models/AccountBalance';
|
||||||
import {promiseSerial} from '@/utils';
|
import {promiseSerial} from '@/utils';
|
||||||
import Account from '../../models/Account';
|
import Account from '@/models/Account';
|
||||||
|
import NestedSet from '../../collection/NestedSet';
|
||||||
|
|
||||||
|
|
||||||
export default class JournalPoster {
|
export default class JournalPoster {
|
||||||
/**
|
/**
|
||||||
* Journal poster constructor.
|
* Journal poster constructor.
|
||||||
*/
|
*/
|
||||||
constructor() {
|
constructor(accountsGraph) {
|
||||||
this.entries = [];
|
this.entries = [];
|
||||||
this.balancesChange = {};
|
this.balancesChange = {};
|
||||||
this.deletedEntriesIds = [];
|
this.deletedEntriesIds = [];
|
||||||
|
|
||||||
|
this.accountsBalanceTable = {};
|
||||||
|
this.accountsGraph = accountsGraph;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -29,7 +34,7 @@ export default class JournalPoster {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Writes the debit entry for the given account.
|
* Writes the debit entr y for the given account.
|
||||||
* @param {JournalEntry} entry -
|
* @param {JournalEntry} entry -
|
||||||
*/
|
*/
|
||||||
debit(entryModel) {
|
debit(entryModel) {
|
||||||
@@ -45,18 +50,47 @@ export default class JournalPoster {
|
|||||||
* @param {JournalEntry} entry
|
* @param {JournalEntry} entry
|
||||||
* @param {String} type
|
* @param {String} type
|
||||||
*/
|
*/
|
||||||
setAccountBalanceChange(entry, type) {
|
setAccountBalanceChange(entry, entryType) {
|
||||||
if (!this.balancesChange[entry.account]) {
|
const depAccountsIds = this.accountsGraph.dependantsOf(entry.account);
|
||||||
this.balancesChange[entry.account] = 0;
|
|
||||||
|
const balanceChangeEntry = {
|
||||||
|
debit: entry.debit,
|
||||||
|
credit: entry.credit,
|
||||||
|
entryType,
|
||||||
|
accountNormal: entry.accountNormal,
|
||||||
|
};
|
||||||
|
this._setAccountBalanceChange({
|
||||||
|
...balanceChangeEntry,
|
||||||
|
accountId: entry.account,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Effect parent accounts of the given account id.
|
||||||
|
depAccountsIds.forEach((accountId) => {
|
||||||
|
this._setAccountBalanceChange({
|
||||||
|
...balanceChangeEntry,
|
||||||
|
accountId,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets account balance change.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_setAccountBalanceChange({
|
||||||
|
accountId, accountNormal, debit, credit, entryType
|
||||||
|
}) {
|
||||||
|
if (!this.balancesChange[accountId]) {
|
||||||
|
this.balancesChange[accountId] = 0;
|
||||||
}
|
}
|
||||||
let change = 0;
|
let change = 0;
|
||||||
|
|
||||||
if (entry.accountNormal === 'credit') {
|
if (accountNormal === 'credit') {
|
||||||
change = (type === 'credit') ? entry.credit : -1 * entry.debit;
|
change = (entryType === 'credit') ? credit : -1 * debit;
|
||||||
} else if (entry.accountNormal === 'debit') {
|
} else if (accountNormal === 'debit') {
|
||||||
change = (type === 'debit') ? entry.debit : -1 * entry.credit;
|
change = (entryType === 'debit') ? debit : -1 * credit;
|
||||||
}
|
}
|
||||||
this.balancesChange[entry.account] += change;
|
this.balancesChange[accountId] += change;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -86,11 +120,10 @@ export default class JournalPoster {
|
|||||||
const balanceFindOneOpers = [];
|
const balanceFindOneOpers = [];
|
||||||
let balanceAccounts = [];
|
let balanceAccounts = [];
|
||||||
|
|
||||||
const effectAccountsOpers = [];
|
|
||||||
|
|
||||||
balancesList.forEach((balance) => {
|
balancesList.forEach((balance) => {
|
||||||
const oper = AccountBalance.tenant()
|
const oper = AccountBalance.tenant()
|
||||||
.query().findOne('account_id', balance.account_id);
|
.query()
|
||||||
|
.findOne('account_id', balance.account_id);
|
||||||
balanceFindOneOpers.push(oper);
|
balanceFindOneOpers.push(oper);
|
||||||
});
|
});
|
||||||
balanceAccounts = await Promise.all(balanceFindOneOpers);
|
balanceAccounts = await Promise.all(balanceFindOneOpers);
|
||||||
@@ -102,9 +135,11 @@ export default class JournalPoster {
|
|||||||
const foundAccBalance = balanceAccounts.some((account) => (
|
const foundAccBalance = balanceAccounts.some((account) => (
|
||||||
account && account.account_id === balance.account_id
|
account && account.account_id === balance.account_id
|
||||||
));
|
));
|
||||||
|
|
||||||
if (foundAccBalance) {
|
if (foundAccBalance) {
|
||||||
const query = AccountBalance.tenant()
|
const query = AccountBalance.tenant()
|
||||||
.query()[method]('amount', Math.abs(balance.amount))
|
.query()
|
||||||
|
[method]('amount', Math.abs(balance.amount))
|
||||||
.where('account_id', balance.account_id);
|
.where('account_id', balance.account_id);
|
||||||
|
|
||||||
balanceUpdateOpers.push(query);
|
balanceUpdateOpers.push(query);
|
||||||
@@ -116,67 +151,12 @@ export default class JournalPoster {
|
|||||||
});
|
});
|
||||||
balanceInsertOpers.push(query);
|
balanceInsertOpers.push(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
const effectedAccountsOper = this.effectAssociatedAccountsBalance(
|
|
||||||
balance.accountId, amount, 'USD', method,
|
|
||||||
);
|
|
||||||
effectAccountsOpers.push(effectedAccountsOper);
|
|
||||||
});
|
});
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
...balanceUpdateOpers, ...balanceInsertOpers,
|
...balanceUpdateOpers, ...balanceInsertOpers,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Effect associated descendants and parent accounts
|
|
||||||
* of the given account id.
|
|
||||||
* @param {Number} accountId
|
|
||||||
* @param {Number} amount
|
|
||||||
* @param {String} currencyCode
|
|
||||||
* @param {*} method
|
|
||||||
*/
|
|
||||||
async effectAssociatedAccountsBalance(accountId, amount, currencyCode = 'USD', method) {
|
|
||||||
const accounts = await Account.query().withGraphFetched('balance');
|
|
||||||
|
|
||||||
const accountsDecendences = accounts.getDescendants();
|
|
||||||
|
|
||||||
const asyncOpers = [];
|
|
||||||
const accountsInsertBalance = [];
|
|
||||||
const accountsUpdateBalance = [];
|
|
||||||
|
|
||||||
accounts.forEach((account) => {
|
|
||||||
const accountBalances = account.balance;
|
|
||||||
const currencyBalance = accountBalances
|
|
||||||
.find(balance => balance.currencyCode === currencyCode);
|
|
||||||
|
|
||||||
if (currencyBalance) {
|
|
||||||
accountsInsertBalance.push(account.id);
|
|
||||||
} else {
|
|
||||||
accountsUpdateBalance.push(account.id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
accountsInsertBalance.forEach((accountId) => {
|
|
||||||
const oper = AccountBalance.tenant().query().insert({
|
|
||||||
account_id: accountId,
|
|
||||||
amount: method === 'decrement' ? amount * -1 : amount,
|
|
||||||
currency_code: currencyCode,
|
|
||||||
});
|
|
||||||
asyncOpers.push(oper);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (accountsUpdateBalance.length > 0) {
|
|
||||||
const oper = AccountBalance.tenant().query()
|
|
||||||
.whereIn('account_id', accountsUpdateBalance);
|
|
||||||
[method]('amount', Math.abs(amount))
|
|
||||||
.where('currency_code', currencyCode);
|
|
||||||
|
|
||||||
asyncOpers.push(oper);
|
|
||||||
}
|
|
||||||
await Promise.all(asyncOpers);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Saves the stacked journal entries to the storage.
|
* Saves the stacked journal entries to the storage.
|
||||||
*/
|
*/
|
||||||
@@ -236,6 +216,9 @@ export default class JournalPoster {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete all the stacked entries.
|
||||||
|
*/
|
||||||
async deleteEntries() {
|
async deleteEntries() {
|
||||||
if (this.deletedEntriesIds.length > 0) {
|
if (this.deletedEntriesIds.length > 0) {
|
||||||
await AccountTransaction.tenant().query()
|
await AccountTransaction.tenant().query()
|
||||||
@@ -269,6 +252,29 @@ export default class JournalPoster {
|
|||||||
return closingBalance;
|
return closingBalance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the given account balance with dependencies accounts.
|
||||||
|
* @param {Number} accountId
|
||||||
|
* @param {Date} closingDate
|
||||||
|
* @param {String} dateType
|
||||||
|
* @return {Number}
|
||||||
|
*/
|
||||||
|
getAccountBalance(accountId, closingDate, dateType) {
|
||||||
|
const accountNode = this.accountsGraph.getNodeData(accountId);
|
||||||
|
const depAccountsIds = this.accountsGraph.dependenciesOf(accountId);
|
||||||
|
const depAccounts = depAccountsIds.map((id) => this.accountsGraph.getNodeData(id));
|
||||||
|
let balance = 0;
|
||||||
|
|
||||||
|
[...depAccounts, accountNode].forEach((account) => {
|
||||||
|
// if (!this.accountsBalanceTable[account.id]) {
|
||||||
|
const closingBalance = this.getClosingBalance(account.id, closingDate, dateType);
|
||||||
|
this.accountsBalanceTable[account.id] = closingBalance;
|
||||||
|
// }
|
||||||
|
balance += this.accountsBalanceTable[account.id];
|
||||||
|
});
|
||||||
|
return balance;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve the credit/debit sumation for the given account and date.
|
* Retrieve the credit/debit sumation for the given account and date.
|
||||||
* @param {Number} account -
|
* @param {Number} account -
|
||||||
@@ -299,6 +305,30 @@ export default class JournalPoster {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve trial balance of the given account with depends.
|
||||||
|
* @param {Number} accountId
|
||||||
|
* @param {Date} closingDate
|
||||||
|
* @param {String} dateType
|
||||||
|
* @return {Number}
|
||||||
|
*/
|
||||||
|
getTrialBalanceWithDepands(accountId, closingDate, dateType) {
|
||||||
|
const accountNode = this.accountsGraph.getNodeData(accountId);
|
||||||
|
const depAccountsIds = this.accountsGraph.dependenciesOf(accountId);
|
||||||
|
const depAccounts = depAccountsIds.map((id) => this.accountsGraph.getNodeData(id));
|
||||||
|
|
||||||
|
const trialBalance = { credit: 0, debit: 0, balance: 0 };
|
||||||
|
|
||||||
|
[...depAccounts, accountNode].forEach((account) => {
|
||||||
|
const _trialBalance = this.getTrialBalance(account.id, closingDate, dateType);
|
||||||
|
|
||||||
|
trialBalance.credit += _trialBalance.credit;
|
||||||
|
trialBalance.debit += _trialBalance.debit;
|
||||||
|
trialBalance.balance += _trialBalance.balance;
|
||||||
|
});
|
||||||
|
return trialBalance;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load fetched accounts journal entries.
|
* Load fetched accounts journal entries.
|
||||||
* @param {Array} entries -
|
* @param {Array} entries -
|
||||||
@@ -324,8 +354,4 @@ export default class JournalPoster {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
static loadAccounts() {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,6 +72,30 @@ const promiseSerial = (funcs) => {
|
|||||||
Promise.resolve([]));
|
Promise.resolve([]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const flatToNestedArray = (data, config = { id: 'id', parentId: 'parent_id' }) => {
|
||||||
|
const map = {};
|
||||||
|
const nestedArray = [];
|
||||||
|
|
||||||
|
data.forEach((item) => {
|
||||||
|
map[item[config.id]] = item;
|
||||||
|
map[item[config.id]].children = [];
|
||||||
|
});
|
||||||
|
|
||||||
|
data.forEach((item) => {
|
||||||
|
const parentItemId = item[config.parentId];
|
||||||
|
|
||||||
|
if (!item[config.parentId]) {
|
||||||
|
nestedArray.push(item);
|
||||||
|
}
|
||||||
|
if(parentItemId) {
|
||||||
|
map[parentItemId].children.push(item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return nestedArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
hashPassword,
|
hashPassword,
|
||||||
origin,
|
origin,
|
||||||
@@ -80,4 +104,5 @@ export {
|
|||||||
mapValuesDeep,
|
mapValuesDeep,
|
||||||
mapKeysDeep,
|
mapKeysDeep,
|
||||||
promiseSerial,
|
promiseSerial,
|
||||||
|
flatToNestedArray,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,61 +2,129 @@ import { expect } from '~/testInit';
|
|||||||
import NestedSet from '@/collection/NestedSet';
|
import NestedSet from '@/collection/NestedSet';
|
||||||
|
|
||||||
describe('NestedSet', () => {
|
describe('NestedSet', () => {
|
||||||
it('Should link parent and children nodes.', () => {
|
describe('linkChildren()', () => {
|
||||||
const flattenArray = [
|
it('Should link parent and children nodes.', () => {
|
||||||
{ id: 10 },
|
const flattenArray = [
|
||||||
{ id: 1 },
|
{ id: 10 },
|
||||||
{
|
{ id: 1 },
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
parent_id: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
parent_id: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
parent_id: 3,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const nestSet = new NestedSet(flattenArray);
|
||||||
|
const treeGroups = nestSet.linkChildren();
|
||||||
|
|
||||||
|
expect(treeGroups['1']).deep.equals({
|
||||||
|
id: 1,
|
||||||
|
children: {
|
||||||
|
'2': { id: 2, parent_id: 1, children: {} },
|
||||||
|
'3': {
|
||||||
|
id: 3, parent_id: 1, children: {
|
||||||
|
'4': { id: 4, parent_id: 3, children: {} }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(treeGroups['2']).deep.equals({
|
||||||
|
id: 2, parent_id: 1, children: {},
|
||||||
|
});
|
||||||
|
expect(treeGroups['3']).deep.equals({
|
||||||
id: 3,
|
id: 3,
|
||||||
parent_id: 1,
|
parent_id: 1,
|
||||||
},
|
children: { '4': { id: 4, parent_id: 3, children: {} } }
|
||||||
{
|
});
|
||||||
id: 2,
|
expect(treeGroups['4']).deep.equals({
|
||||||
parent_id: 1,
|
id: 4, parent_id: 3, children: {},
|
||||||
},
|
});
|
||||||
{
|
});
|
||||||
id: 4,
|
|
||||||
parent_id: 3,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const collection = new NestedSet(flattenArray);
|
|
||||||
const treeGroups = collection.toTree();
|
|
||||||
|
|
||||||
expect(treeGroups[0].id).equals(10);
|
|
||||||
expect(treeGroups[1].id).equals(1);
|
|
||||||
|
|
||||||
expect(treeGroups[1].children.length).equals(2);
|
|
||||||
expect(treeGroups[1].children[0].id).equals(3);
|
|
||||||
expect(treeGroups[1].children[1].id).equals(2);
|
|
||||||
|
|
||||||
expect(treeGroups[1].children[0].children[0].id).equals(4);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should flatten the nested set collection.', () => {
|
describe('toArray()', () => {
|
||||||
const flattenArray = [
|
it('Should retrieve nested sets as array.', () => {
|
||||||
{ id: 1 },
|
const flattenArray = [
|
||||||
{
|
{ id: 10 },
|
||||||
id: 2,
|
{ id: 1 },
|
||||||
parent_id: 1,
|
{
|
||||||
},
|
id: 3,
|
||||||
{
|
parent_id: 1,
|
||||||
id: 3,
|
},
|
||||||
parent_id: 1,
|
{
|
||||||
},
|
id: 2,
|
||||||
{
|
parent_id: 1,
|
||||||
id: 4,
|
},
|
||||||
parent_id: 3,
|
{
|
||||||
},
|
id: 4,
|
||||||
];
|
parent_id: 3,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const nestSet = new NestedSet(flattenArray);
|
||||||
|
const treeArray = nestSet.toArray();
|
||||||
|
|
||||||
const collection = new NestedSet(flattenArray);
|
expect(treeArray[0]).deep.equals({
|
||||||
const treeGroups = collection.toTree();
|
id: 10, children: [],
|
||||||
const flatten = collection.flattenTree();
|
});
|
||||||
|
expect(treeArray[1]).deep.equals({
|
||||||
|
id: 1,
|
||||||
|
children: [
|
||||||
|
{ id: 2, parent_id: 1, children: [] },
|
||||||
|
{ id: 3, parent_id: 1, children: [{
|
||||||
|
id: 4, parent_id: 3, children: []
|
||||||
|
}] }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
expect(flatten.length).equals(4);
|
describe('getParents(id)', () => {
|
||||||
expect(flatten[0].id).equals(1);
|
it('Should retrieve parent nodes of the given node id.', () => {
|
||||||
expect(flatten[1].id).equals(2);
|
const flattenArray = [
|
||||||
expect(flatten[2].id).equals(3);
|
{ id: 10 },
|
||||||
expect(flatten[3].id).equals(4);
|
{ id: 1 },
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
parent_id: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
parent_id: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
parent_id: 3,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const nestSet = new NestedSet(flattenArray);
|
||||||
|
const parentNodes = nestSet.getParents(4);
|
||||||
|
|
||||||
|
expect(parentNodes).deep.equals([
|
||||||
|
{ id: 4, parent_id: 3, children: {} },
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
parent_id: 1,
|
||||||
|
children: { '4': { id: 4, parent_id: 3, children: {} } }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
children: {
|
||||||
|
'2': { id: 2, parent_id: 1, children: {} },
|
||||||
|
'3': {
|
||||||
|
id: 3, parent_id: 1, children: {
|
||||||
|
'4': { id: 4, parent_id: 3, children: {} }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
});
|
||||||
})
|
})
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
systemFactory,
|
systemFactory,
|
||||||
dropTenant,
|
dropTenant,
|
||||||
} from '~/testInit';
|
} from '~/testInit';
|
||||||
|
import CacheService from '@/services/Cache';
|
||||||
|
|
||||||
let tenantWebsite;
|
let tenantWebsite;
|
||||||
let tenantFactory;
|
let tenantFactory;
|
||||||
@@ -19,6 +20,8 @@ beforeEach(async () => {
|
|||||||
|
|
||||||
bindTenantModel(tenantWebsite.tenantDb);
|
bindTenantModel(tenantWebsite.tenantDb);
|
||||||
loginRes = await login(tenantWebsite);
|
loginRes = await login(tenantWebsite);
|
||||||
|
|
||||||
|
CacheService.flush();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
} from '~/dbInit';
|
} from '~/dbInit';
|
||||||
import { times } from 'lodash';
|
import { times } from 'lodash';
|
||||||
|
|
||||||
describe.only('CachableModel', () => {
|
describe('CachableModel', () => {
|
||||||
describe('remember()', () => {
|
describe('remember()', () => {
|
||||||
it('Should retrieve the data from the storage.', async () => {
|
it('Should retrieve the data from the storage.', async () => {
|
||||||
|
|
||||||
|
|||||||
@@ -337,7 +337,6 @@ describe('routes: /accounts/', () => {
|
|||||||
.get('/api/accounts')
|
.get('/api/accounts')
|
||||||
.set('x-access-token', loginRes.body.token)
|
.set('x-access-token', loginRes.body.token)
|
||||||
.set('organization-id', tenantWebsite.organizationId)
|
.set('organization-id', tenantWebsite.organizationId)
|
||||||
.query({ display_type: 'tree' })
|
|
||||||
.send();
|
.send();
|
||||||
|
|
||||||
expect(res.status).equals(200);
|
expect(res.status).equals(200);
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
|
|
||||||
let creditAccount;
|
let creditAccount;
|
||||||
let debitAccount;
|
let debitAccount;
|
||||||
|
let incomeAccount;
|
||||||
let incomeType;
|
let incomeType;
|
||||||
|
|
||||||
describe('routes: `/financial_statements`', () => {
|
describe('routes: `/financial_statements`', () => {
|
||||||
@@ -28,7 +29,7 @@ describe('routes: `/financial_statements`', () => {
|
|||||||
debitAccount = await tenantFactory.create('account', { account_type_id: assetType.id });
|
debitAccount = await tenantFactory.create('account', { account_type_id: assetType.id });
|
||||||
|
|
||||||
// Income && expenses accounts.
|
// Income && expenses accounts.
|
||||||
const incomeAccount = await tenantFactory.create('account', { account_type_id: incomeType.id });
|
incomeAccount = await tenantFactory.create('account', { account_type_id: incomeType.id });
|
||||||
const expenseAccount = await tenantFactory.create('account', { account_type_id: expenseType.id });
|
const expenseAccount = await tenantFactory.create('account', { account_type_id: expenseType.id });
|
||||||
// const income2Account = await tenantFactory.create('account', { account_type_id: incomeType.id });
|
// const income2Account = await tenantFactory.create('account', { account_type_id: incomeType.id });
|
||||||
|
|
||||||
@@ -183,8 +184,8 @@ describe('routes: `/financial_statements`', () => {
|
|||||||
|
|
||||||
const journal = res.body.journal.find((j) => j.id === '1-Expense');
|
const journal = res.body.journal.find((j) => j.id === '1-Expense');
|
||||||
|
|
||||||
expect(journal.credit).equals(1);
|
expect(journal.formatted_credit).equals(1);
|
||||||
expect(journal.debit).equals(1);
|
expect(journal.formatted_debit).equals(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -244,10 +245,10 @@ describe('routes: `/financial_statements`', () => {
|
|||||||
|
|
||||||
expect(targetAccount).to.be.an('object');
|
expect(targetAccount).to.be.an('object');
|
||||||
expect(targetAccount.opening).to.deep.equal({
|
expect(targetAccount.opening).to.deep.equal({
|
||||||
amount: 0, date: '2020-01-01',
|
amount: 0, formatted_amount: 0, date: '2020-01-01',
|
||||||
});
|
});
|
||||||
expect(targetAccount.closing).to.deep.equal({
|
expect(targetAccount.closing).to.deep.equal({
|
||||||
amount: 4000, date: '2020-12-31',
|
amount: 4000, formatted_amount: 4000, date: '2020-12-31',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -263,16 +264,14 @@ describe('routes: `/financial_statements`', () => {
|
|||||||
})
|
})
|
||||||
.send();
|
.send();
|
||||||
|
|
||||||
console.log(res.body);
|
|
||||||
|
|
||||||
const targetAccount = res.body.accounts.find((a) => a.id === creditAccount.id);
|
const targetAccount = res.body.accounts.find((a) => a.id === creditAccount.id);
|
||||||
|
|
||||||
expect(targetAccount).to.be.an('object');
|
expect(targetAccount).to.be.an('object');
|
||||||
expect(targetAccount.opening).to.deep.equal({
|
expect(targetAccount.opening).to.deep.equal({
|
||||||
amount: 4000, date: '2020-01-01',
|
amount: 0, formatted_amount: 0, date: '2018-01-01',
|
||||||
});
|
});
|
||||||
expect(targetAccount.closing).to.deep.equal({
|
expect(targetAccount.closing).to.deep.equal({
|
||||||
amount: 4000, date: '2020-03-30',
|
amount: 4000, formatted_amount: 4000, date: '2020-03-30',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -286,10 +285,25 @@ describe('routes: `/financial_statements`', () => {
|
|||||||
})
|
})
|
||||||
.send();
|
.send();
|
||||||
|
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should retrieve accounts transactions only that between date range.', async () => {
|
it('Should retrieve accounts transactions only that between date range.', async () => {
|
||||||
|
const res = await request()
|
||||||
|
.get('/api/financial_statements/general_ledger')
|
||||||
|
.set('x-access-token', loginRes.body.token)
|
||||||
|
.set('organization-id', tenantWebsite.organizationId)
|
||||||
|
.query({
|
||||||
|
from_date: '2020-01-01',
|
||||||
|
to_date: '2020-03-30',
|
||||||
|
none_zero: true,
|
||||||
|
})
|
||||||
|
.send();
|
||||||
|
|
||||||
|
|
||||||
|
// console.log(res.body.accounts);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should retrieve no accounts with given date period has not transactions.', async () => {
|
||||||
const res = await request()
|
const res = await request()
|
||||||
.get('/api/financial_statements/general_ledger')
|
.get('/api/financial_statements/general_ledger')
|
||||||
.set('x-access-token', loginRes.body.token)
|
.set('x-access-token', loginRes.body.token)
|
||||||
@@ -301,23 +315,7 @@ describe('routes: `/financial_statements`', () => {
|
|||||||
})
|
})
|
||||||
.send();
|
.send();
|
||||||
|
|
||||||
});
|
expect(res.body.accounts.length).equals(0);
|
||||||
|
|
||||||
it('Should not retrieve all accounts that have no transactions in the given date range when `none_zero` is `false`.', async () => {
|
|
||||||
const res = await request()
|
|
||||||
.get('/api/financial_statements/general_ledger')
|
|
||||||
.set('x-access-token', loginRes.body.token)
|
|
||||||
.set('organization-id', tenantWebsite.organizationId)
|
|
||||||
.query({
|
|
||||||
from_date: '2020-01-20',
|
|
||||||
to_date: '2020-03-30',
|
|
||||||
none_zero: false,
|
|
||||||
})
|
|
||||||
.send();
|
|
||||||
|
|
||||||
res.body.accounts.forEach((account) => {
|
|
||||||
expect(account.transactions.length).not.equals(0);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should retrieve all accounts even it have no transactions in the given date range when `none_zero` is `true`', async () => {
|
it('Should retrieve all accounts even it have no transactions in the given date range when `none_zero` is `true`', async () => {
|
||||||
@@ -335,7 +333,7 @@ describe('routes: `/financial_statements`', () => {
|
|||||||
const accountsNoTransactions = res.body.accounts.filter(a => a.transactions.length === 0);
|
const accountsNoTransactions = res.body.accounts.filter(a => a.transactions.length === 0);
|
||||||
const accountsWithTransactions = res.body.accounts.filter(a => a.transactions.length > 0);
|
const accountsWithTransactions = res.body.accounts.filter(a => a.transactions.length > 0);
|
||||||
|
|
||||||
expect(accountsNoTransactions.length).not.equals(0);
|
expect(accountsNoTransactions.length).equals(0);
|
||||||
expect(accountsWithTransactions.length).not.equals(0);
|
expect(accountsWithTransactions.length).not.equals(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -347,6 +345,7 @@ describe('routes: `/financial_statements`', () => {
|
|||||||
.query({
|
.query({
|
||||||
from_date: '2020-01-01',
|
from_date: '2020-01-01',
|
||||||
to_date: '2020-03-30',
|
to_date: '2020-03-30',
|
||||||
|
accounts_ids: [creditAccount.id],
|
||||||
number_format: {
|
number_format: {
|
||||||
divide_1000: true,
|
divide_1000: true,
|
||||||
},
|
},
|
||||||
@@ -358,6 +357,8 @@ describe('routes: `/financial_statements`', () => {
|
|||||||
name: creditAccount.name,
|
name: creditAccount.name,
|
||||||
code: creditAccount.code,
|
code: creditAccount.code,
|
||||||
index: null,
|
index: null,
|
||||||
|
parentAccountId: null,
|
||||||
|
children: [],
|
||||||
transactions: [
|
transactions: [
|
||||||
{
|
{
|
||||||
id: 1002,
|
id: 1002,
|
||||||
@@ -367,11 +368,12 @@ describe('routes: `/financial_statements`', () => {
|
|||||||
referenceId: null,
|
referenceId: null,
|
||||||
date: '2020-01-09T22:00:00.000Z',
|
date: '2020-01-09T22:00:00.000Z',
|
||||||
createdAt: null,
|
createdAt: null,
|
||||||
amount: 4
|
formatted_amount: 4,
|
||||||
|
amount: 4000,
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
opening: { date: '2020-01-01', amount: 0 },
|
opening: { date: '2020-01-01', formatted_amount: 0, amount: 0 },
|
||||||
closing: { date: '2020-03-30', amount: 4 }
|
closing: { date: '2020-03-30', formatted_amount: 4, amount: 4000 }
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -395,7 +397,7 @@ describe('routes: `/financial_statements`', () => {
|
|||||||
})
|
})
|
||||||
.send();
|
.send();
|
||||||
|
|
||||||
expect(res.body.accounts[0].transactions[2].amount).equal(2);
|
expect(res.body.accounts[0].transactions[2].formatted_amount).equal(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should retrieve only accounts that given in the query.', async () => {
|
it('Should retrieve only accounts that given in the query.', async () => {
|
||||||
@@ -413,6 +415,25 @@ describe('routes: `/financial_statements`', () => {
|
|||||||
|
|
||||||
expect(res.body.accounts.length).equals(1);
|
expect(res.body.accounts.length).equals(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Should retrieve accounts in nested array structure as parent/children accounts.', async () => {
|
||||||
|
const childAccount = await tenantFactory.create('account', {
|
||||||
|
parent_account_id: debitAccount.id,
|
||||||
|
account_type_id: 1
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await request()
|
||||||
|
.get('/api/financial_statements/general_ledger')
|
||||||
|
.set('x-access-token', loginRes.body.token)
|
||||||
|
.set('organization-id', tenantWebsite.organizationId)
|
||||||
|
.query({
|
||||||
|
accounts_ids: [childAccount.id, debitAccount.id],
|
||||||
|
})
|
||||||
|
.send();
|
||||||
|
|
||||||
|
expect(res.body.accounts[0].children.length).equals(1);
|
||||||
|
expect(res.body.accounts[0].children[0].id).equals(childAccount.id);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('routes: `financial_statements/balance_sheet`', () => {
|
describe('routes: `financial_statements/balance_sheet`', () => {
|
||||||
@@ -480,6 +501,8 @@ describe('routes: `/financial_statements`', () => {
|
|||||||
index: null,
|
index: null,
|
||||||
name: debitAccount.name,
|
name: debitAccount.name,
|
||||||
code: debitAccount.code,
|
code: debitAccount.code,
|
||||||
|
parentAccountId: null,
|
||||||
|
children: [],
|
||||||
total: { formatted_amount: 5000, amount: 5000, date: '2032-02-02' }
|
total: { formatted_amount: 5000, amount: 5000, date: '2032-02-02' }
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -488,6 +511,8 @@ describe('routes: `/financial_statements`', () => {
|
|||||||
index: null,
|
index: null,
|
||||||
name: creditAccount.name,
|
name: creditAccount.name,
|
||||||
code: creditAccount.code,
|
code: creditAccount.code,
|
||||||
|
parentAccountId: null,
|
||||||
|
children: [],
|
||||||
total: { formatted_amount: 4000, amount: 4000, date: '2032-02-02' }
|
total: { formatted_amount: 4000, amount: 4000, date: '2032-02-02' }
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -565,6 +590,8 @@ describe('routes: `/financial_statements`', () => {
|
|||||||
index: debitAccount.index,
|
index: debitAccount.index,
|
||||||
name: debitAccount.name,
|
name: debitAccount.name,
|
||||||
code: debitAccount.code,
|
code: debitAccount.code,
|
||||||
|
parentAccountId: null,
|
||||||
|
children: [],
|
||||||
total_periods: [
|
total_periods: [
|
||||||
{ date: '2020-01-08', formatted_amount: 0, amount: 0 },
|
{ date: '2020-01-08', formatted_amount: 0, amount: 0 },
|
||||||
{ date: '2020-01-09', formatted_amount: 0, amount: 0 },
|
{ date: '2020-01-09', formatted_amount: 0, amount: 0 },
|
||||||
@@ -579,6 +606,8 @@ describe('routes: `/financial_statements`', () => {
|
|||||||
index: creditAccount.index,
|
index: creditAccount.index,
|
||||||
name: creditAccount.name,
|
name: creditAccount.name,
|
||||||
code: creditAccount.code,
|
code: creditAccount.code,
|
||||||
|
parentAccountId: null,
|
||||||
|
children: [],
|
||||||
total_periods: [
|
total_periods: [
|
||||||
{ date: '2020-01-08', formatted_amount: 0, amount: 0 },
|
{ date: '2020-01-08', formatted_amount: 0, amount: 0 },
|
||||||
{ date: '2020-01-09', formatted_amount: 0, amount: 0 },
|
{ date: '2020-01-09', formatted_amount: 0, amount: 0 },
|
||||||
@@ -608,6 +637,8 @@ describe('routes: `/financial_statements`', () => {
|
|||||||
index: debitAccount.index,
|
index: debitAccount.index,
|
||||||
name: debitAccount.name,
|
name: debitAccount.name,
|
||||||
code: debitAccount.code,
|
code: debitAccount.code,
|
||||||
|
parentAccountId: null,
|
||||||
|
children: [],
|
||||||
total_periods: [
|
total_periods: [
|
||||||
{ date: '2019-07', formatted_amount: 0, amount: 0 },
|
{ date: '2019-07', formatted_amount: 0, amount: 0 },
|
||||||
{ date: '2019-08', formatted_amount: 0, amount: 0 },
|
{ date: '2019-08', formatted_amount: 0, amount: 0 },
|
||||||
@@ -644,6 +675,8 @@ describe('routes: `/financial_statements`', () => {
|
|||||||
index: debitAccount.index,
|
index: debitAccount.index,
|
||||||
name: debitAccount.name,
|
name: debitAccount.name,
|
||||||
code: debitAccount.code,
|
code: debitAccount.code,
|
||||||
|
parentAccountId: null,
|
||||||
|
children: [],
|
||||||
total_periods: [
|
total_periods: [
|
||||||
{ date: '2020-03', formatted_amount: 5000, amount: 5000 },
|
{ date: '2020-03', formatted_amount: 5000, amount: 5000 },
|
||||||
{ date: '2020-06', formatted_amount: 5000, amount: 5000 },
|
{ date: '2020-06', formatted_amount: 5000, amount: 5000 },
|
||||||
@@ -678,6 +711,8 @@ describe('routes: `/financial_statements`', () => {
|
|||||||
index: debitAccount.index,
|
index: debitAccount.index,
|
||||||
name: debitAccount.name,
|
name: debitAccount.name,
|
||||||
code: debitAccount.code,
|
code: debitAccount.code,
|
||||||
|
parentAccountId: null,
|
||||||
|
children: [],
|
||||||
total_periods: [
|
total_periods: [
|
||||||
{ date: '2020-03', formatted_amount: 5000, amount: 5000.25 },
|
{ date: '2020-03', formatted_amount: 5000, amount: 5000.25 },
|
||||||
{ date: '2020-06', formatted_amount: 5000, amount: 5000.25 },
|
{ date: '2020-06', formatted_amount: 5000, amount: 5000.25 },
|
||||||
@@ -709,6 +744,8 @@ describe('routes: `/financial_statements`', () => {
|
|||||||
index: debitAccount.index,
|
index: debitAccount.index,
|
||||||
name: debitAccount.name,
|
name: debitAccount.name,
|
||||||
code: debitAccount.code,
|
code: debitAccount.code,
|
||||||
|
parentAccountId: null,
|
||||||
|
children: [],
|
||||||
total_periods: [
|
total_periods: [
|
||||||
{ date: '2020-03', formatted_amount: 5, amount: 5000 },
|
{ date: '2020-03', formatted_amount: 5, amount: 5000 },
|
||||||
{ date: '2020-06', formatted_amount: 5, amount: 5000 },
|
{ date: '2020-06', formatted_amount: 5, amount: 5000 },
|
||||||
@@ -739,6 +776,104 @@ describe('routes: `/financial_statements`', () => {
|
|||||||
expect(res.body.accounts[0].children.length).equals(0);
|
expect(res.body.accounts[0].children.length).equals(0);
|
||||||
expect(res.body.accounts[1].children.length).equals(0);
|
expect(res.body.accounts[1].children.length).equals(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Should retrieve accounts in nested structure parent and children accounts.', async () => {
|
||||||
|
const childAccount = await tenantFactory.create('account', {
|
||||||
|
parent_account_id: debitAccount.id,
|
||||||
|
account_type_id: 1
|
||||||
|
});
|
||||||
|
const res = await request()
|
||||||
|
.get('/api/financial_statements/balance_sheet')
|
||||||
|
.set('x-access-token', loginRes.body.token)
|
||||||
|
.set('organization-id', tenantWebsite.organizationId)
|
||||||
|
.query({
|
||||||
|
none_zero: false,
|
||||||
|
account_ids: [childAccount.id, debitAccount.id]
|
||||||
|
})
|
||||||
|
.send();
|
||||||
|
|
||||||
|
expect(res.body.accounts[0].children).include.something.deep.equals({
|
||||||
|
id: debitAccount.id,
|
||||||
|
index: null,
|
||||||
|
name: debitAccount.name,
|
||||||
|
code: debitAccount.code,
|
||||||
|
parentAccountId: null,
|
||||||
|
total: { formatted_amount: 5000, amount: 5000, date: '2020-12-31' },
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: childAccount.id,
|
||||||
|
index: null,
|
||||||
|
name: childAccount.name,
|
||||||
|
code: childAccount.code,
|
||||||
|
parentAccountId: debitAccount.id,
|
||||||
|
total: { formatted_amount: 0, amount: 0, date: '2020-12-31' },
|
||||||
|
children: [],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should parent account balance sumation of total balane all children accounts.', async () => {
|
||||||
|
const childAccount = await tenantFactory.create('account', {
|
||||||
|
parent_account_id: debitAccount.id,
|
||||||
|
account_type_id: 1
|
||||||
|
});
|
||||||
|
await tenantFactory.create('account_transaction', {
|
||||||
|
credit: 0, debit: 1000, account_id: childAccount.id, date: '2020-1-10'
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await request()
|
||||||
|
.get('/api/financial_statements/balance_sheet')
|
||||||
|
.set('x-access-token', loginRes.body.token)
|
||||||
|
.set('organization-id', tenantWebsite.organizationId)
|
||||||
|
.query({
|
||||||
|
none_zero: false,
|
||||||
|
account_ids: [childAccount.id, debitAccount.id]
|
||||||
|
})
|
||||||
|
.send();
|
||||||
|
|
||||||
|
expect(res.body.accounts[0].children[0].total.amount).equals(6000);
|
||||||
|
expect(res.body.accounts[0].children[0].total.formatted_amount).equals(6000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should parent account balance sumation of total periods amounts all children accounts.', async () => {
|
||||||
|
const childAccount = await tenantFactory.create('account', {
|
||||||
|
parent_account_id: debitAccount.id,
|
||||||
|
account_type_id: 1
|
||||||
|
});
|
||||||
|
await tenantFactory.create('account_transaction', {
|
||||||
|
credit: 0, debit: 1000, account_id: childAccount.id, date: '2020-2-10'
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await request()
|
||||||
|
.get('/api/financial_statements/balance_sheet')
|
||||||
|
.set('x-access-token', loginRes.body.token)
|
||||||
|
.set('organization-id', tenantWebsite.organizationId)
|
||||||
|
.query({
|
||||||
|
none_zero: false,
|
||||||
|
account_ids: [childAccount.id, debitAccount.id],
|
||||||
|
display_columns_type: 'date_periods',
|
||||||
|
display_columns_by: 'month',
|
||||||
|
from_date: '2020-01-01',
|
||||||
|
to_date: '2020-12-12',
|
||||||
|
})
|
||||||
|
.send();
|
||||||
|
|
||||||
|
expect(res.body.accounts[0].children[0].total_periods).deep.equals([
|
||||||
|
{ amount: 5000, formatted_amount: 5000, date: '2020-01' },
|
||||||
|
{ amount: 6000, formatted_amount: 6000, date: '2020-02' },
|
||||||
|
{ amount: 6000, formatted_amount: 6000, date: '2020-03' },
|
||||||
|
{ amount: 6000, formatted_amount: 6000, date: '2020-04' },
|
||||||
|
{ amount: 6000, formatted_amount: 6000, date: '2020-05' },
|
||||||
|
{ amount: 6000, formatted_amount: 6000, date: '2020-06' },
|
||||||
|
{ amount: 6000, formatted_amount: 6000, date: '2020-07' },
|
||||||
|
{ amount: 6000, formatted_amount: 6000, date: '2020-08' },
|
||||||
|
{ amount: 6000, formatted_amount: 6000, date: '2020-09' },
|
||||||
|
{ amount: 6000, formatted_amount: 6000, date: '2020-10' },
|
||||||
|
{ amount: 6000, formatted_amount: 6000, date: '2020-11' },
|
||||||
|
{ amount: 6000, formatted_amount: 6000, date: '2020-12' }
|
||||||
|
])
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('routes: `/financial_statements/trial_balance`', () => {
|
describe('routes: `/financial_statements/trial_balance`', () => {
|
||||||
@@ -758,22 +893,37 @@ describe('routes: `/financial_statements`', () => {
|
|||||||
.send();
|
.send();
|
||||||
|
|
||||||
expect(res.body.accounts).include.something.deep.equals({
|
expect(res.body.accounts).include.something.deep.equals({
|
||||||
account_id: debitAccount.id,
|
id: debitAccount.id,
|
||||||
name: debitAccount.name,
|
name: debitAccount.name,
|
||||||
code: debitAccount.code,
|
code: debitAccount.code,
|
||||||
|
parentAccountId: null,
|
||||||
accountNormal: 'debit',
|
accountNormal: 'debit',
|
||||||
credit: 1000,
|
credit: 1000,
|
||||||
debit: 6000,
|
debit: 6000,
|
||||||
balance: 5000,
|
balance: 5000,
|
||||||
|
|
||||||
|
formatted_credit: 1000,
|
||||||
|
formatted_debit: 6000,
|
||||||
|
formatted_balance: 5000,
|
||||||
|
|
||||||
|
children: [],
|
||||||
});
|
});
|
||||||
expect(res.body.accounts).include.something.deep.equals({
|
expect(res.body.accounts).include.something.deep.equals({
|
||||||
account_id: creditAccount.id,
|
id: creditAccount.id,
|
||||||
name: creditAccount.name,
|
name: creditAccount.name,
|
||||||
code: creditAccount.code,
|
code: creditAccount.code,
|
||||||
accountNormal: 'credit',
|
accountNormal: 'credit',
|
||||||
|
parentAccountId: null,
|
||||||
|
|
||||||
credit: 4000,
|
credit: 4000,
|
||||||
debit: 0,
|
debit: 0,
|
||||||
balance: 4000,
|
balance: 4000,
|
||||||
|
|
||||||
|
formatted_credit: 4000,
|
||||||
|
formatted_debit: 0,
|
||||||
|
formatted_balance: 4000,
|
||||||
|
|
||||||
|
children: [],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -807,13 +957,20 @@ describe('routes: `/financial_statements`', () => {
|
|||||||
.send();
|
.send();
|
||||||
|
|
||||||
expect(res.body.accounts).include.something.deep.equals({
|
expect(res.body.accounts).include.something.deep.equals({
|
||||||
account_id: creditAccount.id,
|
id: creditAccount.id,
|
||||||
name: creditAccount.name,
|
name: creditAccount.name,
|
||||||
code: creditAccount.code,
|
code: creditAccount.code,
|
||||||
accountNormal: 'credit',
|
accountNormal: 'credit',
|
||||||
|
parentAccountId: null,
|
||||||
credit: 4000,
|
credit: 4000,
|
||||||
debit: 0,
|
debit: 0,
|
||||||
balance: 4000
|
balance: 4000,
|
||||||
|
|
||||||
|
formatted_credit: 4000,
|
||||||
|
formatted_debit: 0,
|
||||||
|
formatted_balance: 4000,
|
||||||
|
|
||||||
|
children: []
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -833,13 +990,21 @@ describe('routes: `/financial_statements`', () => {
|
|||||||
.send();
|
.send();
|
||||||
|
|
||||||
expect(res.body.accounts).include.something.deep.equals({
|
expect(res.body.accounts).include.something.deep.equals({
|
||||||
account_id: creditAccount.id,
|
id: creditAccount.id,
|
||||||
name: creditAccount.name,
|
name: creditAccount.name,
|
||||||
code: creditAccount.code,
|
code: creditAccount.code,
|
||||||
accountNormal: 'credit',
|
accountNormal: 'credit',
|
||||||
credit: 4,
|
parentAccountId: null,
|
||||||
|
|
||||||
|
credit: 4000,
|
||||||
debit: 0,
|
debit: 0,
|
||||||
balance: 4
|
balance: 4000,
|
||||||
|
|
||||||
|
formatted_credit: 4,
|
||||||
|
formatted_debit: 0,
|
||||||
|
formatted_balance: 4,
|
||||||
|
|
||||||
|
children: [],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -862,6 +1027,25 @@ describe('routes: `/financial_statements`', () => {
|
|||||||
it('Should retrieve associated account details in accounts list.', async () => {
|
it('Should retrieve associated account details in accounts list.', async () => {
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Should retrieve account with nested array structure as parent/children accounts.', async () => {
|
||||||
|
const childAccount = await tenantFactory.create('account', {
|
||||||
|
parent_account_id: debitAccount.id,
|
||||||
|
account_type_id: 1
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await request()
|
||||||
|
.get('/api/financial_statements/trial_balance_sheet')
|
||||||
|
.set('x-access-token', loginRes.body.token)
|
||||||
|
.set('organization-id', tenantWebsite.organizationId)
|
||||||
|
.query({
|
||||||
|
account_ids: [debitAccount.id, childAccount.id],
|
||||||
|
})
|
||||||
|
.send();
|
||||||
|
|
||||||
|
expect(res.body.accounts[0].children.length).equals(1);
|
||||||
|
expect(res.body.accounts[0].children[0].id).equals(childAccount.id);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('routes: `/api/financial_statements/profit_loss_sheet`', () => {
|
describe('routes: `/api/financial_statements/profit_loss_sheet`', () => {
|
||||||
@@ -955,7 +1139,7 @@ describe('routes: `/financial_statements`', () => {
|
|||||||
.set('organization-id', tenantWebsite.organizationId)
|
.set('organization-id', tenantWebsite.organizationId)
|
||||||
.query({
|
.query({
|
||||||
from_date: moment('2020-01-01').startOf('month').format('YYYY-MM-DD'),
|
from_date: moment('2020-01-01').startOf('month').format('YYYY-MM-DD'),
|
||||||
to_date: moment('2020-01-01').endOf('month').format('YYYY-MM-DD'),
|
to_date: moment('2020-01-31').endOf('month').format('YYYY-MM-DD'),
|
||||||
display_columns_type: 'total',
|
display_columns_type: 'total',
|
||||||
display_columns_by: 'month',
|
display_columns_by: 'month',
|
||||||
none_zero: false,
|
none_zero: false,
|
||||||
@@ -967,6 +1151,8 @@ describe('routes: `/financial_statements`', () => {
|
|||||||
index: zeroAccount.index,
|
index: zeroAccount.index,
|
||||||
name: zeroAccount.name,
|
name: zeroAccount.name,
|
||||||
code: zeroAccount.code,
|
code: zeroAccount.code,
|
||||||
|
parentAccountId: null,
|
||||||
|
children: [],
|
||||||
total: { amount: 0, date: '2020-01-31', formatted_amount: 0 },
|
total: { amount: 0, date: '2020-01-31', formatted_amount: 0 },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1120,5 +1306,90 @@ describe('routes: `/financial_statements`', () => {
|
|||||||
|
|
||||||
expect(res.body.profitLoss.income.accounts.length).equals(1);
|
expect(res.body.profitLoss.income.accounts.length).equals(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Should retrieve accounts in nested array structure as parent/children accounts.', async () => {
|
||||||
|
const childAccount = await tenantFactory.create('account', {
|
||||||
|
parent_account_id: incomeAccount.id,
|
||||||
|
account_type_id: 7
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await request()
|
||||||
|
.get('/api/financial_statements/profit_loss_sheet')
|
||||||
|
.set('x-access-token', loginRes.body.token)
|
||||||
|
.set('organization-id', tenantWebsite.organizationId)
|
||||||
|
.query({
|
||||||
|
account_ids: [childAccount.id, incomeAccount.id],
|
||||||
|
})
|
||||||
|
.send();
|
||||||
|
|
||||||
|
expect(res.body.profitLoss.income.accounts.length).equals(1);
|
||||||
|
expect(res.body.profitLoss.income.accounts[0].children.length).equals(1);
|
||||||
|
expect(res.body.profitLoss.income.accounts[0].children[0].id).equals(childAccount.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should parent account credit/debit sumation of total periods amounts all children accounts.', async () => {
|
||||||
|
const childAccount = await tenantFactory.create('account', {
|
||||||
|
parent_account_id: incomeAccount.id,
|
||||||
|
account_type_id: 7,
|
||||||
|
});
|
||||||
|
await tenantFactory.create('account_transaction', {
|
||||||
|
credit: 1000, debit: 0, account_id: childAccount.id, date: '2020-2-10'
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await request()
|
||||||
|
.get('/api/financial_statements/profit_loss_sheet')
|
||||||
|
.set('x-access-token', loginRes.body.token)
|
||||||
|
.set('organization-id', tenantWebsite.organizationId)
|
||||||
|
.query({
|
||||||
|
account_ids: [childAccount.id, incomeAccount.id],
|
||||||
|
from_date: '2020-01-01',
|
||||||
|
to_date: '2020-12-12',
|
||||||
|
})
|
||||||
|
.send();
|
||||||
|
|
||||||
|
expect(res.body.profitLoss.income.accounts[0].total).deep.equals({
|
||||||
|
amount: 3000, date: '2020-12-12', formatted_amount: 3000
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should parent account credit/debit sumation of total date periods.', async () => {
|
||||||
|
const childAccount = await tenantFactory.create('account', {
|
||||||
|
parent_account_id: incomeAccount.id,
|
||||||
|
account_type_id: 7,
|
||||||
|
});
|
||||||
|
await tenantFactory.create('account_transaction', {
|
||||||
|
credit: 1000, debit: 0, account_id: childAccount.id, date: '2020-2-10'
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await request()
|
||||||
|
.get('/api/financial_statements/profit_loss_sheet')
|
||||||
|
.set('x-access-token', loginRes.body.token)
|
||||||
|
.set('organization-id', tenantWebsite.organizationId)
|
||||||
|
.query({
|
||||||
|
account_ids: [childAccount.id, incomeAccount.id],
|
||||||
|
display_columns_type: 'date_periods',
|
||||||
|
display_columns_by: 'month',
|
||||||
|
from_date: '2020-01-01',
|
||||||
|
to_date: '2020-12-12',
|
||||||
|
})
|
||||||
|
.send();
|
||||||
|
|
||||||
|
const periods = [
|
||||||
|
{ date: '2020-01', amount: 2000, formatted_amount: 2000 },
|
||||||
|
{ date: '2020-02', amount: 3000, formatted_amount: 3000 },
|
||||||
|
{ date: '2020-03', amount: 3000, formatted_amount: 3000 },
|
||||||
|
{ date: '2020-04', amount: 3000, formatted_amount: 3000 },
|
||||||
|
{ date: '2020-05', amount: 3000, formatted_amount: 3000 },
|
||||||
|
{ date: '2020-06', amount: 3000, formatted_amount: 3000 },
|
||||||
|
{ date: '2020-07', amount: 3000, formatted_amount: 3000 },
|
||||||
|
{ date: '2020-08', amount: 3000, formatted_amount: 3000 },
|
||||||
|
{ date: '2020-09', amount: 3000, formatted_amount: 3000 },
|
||||||
|
{ date: '2020-10', amount: 3000, formatted_amount: 3000 },
|
||||||
|
{ date: '2020-11', amount: 3000, formatted_amount: 3000 },
|
||||||
|
{ date: '2020-12', amount: 3000, formatted_amount: 3000 }
|
||||||
|
];
|
||||||
|
expect(res.body.profitLoss.income.accounts[0].periods).deep.equals(periods);
|
||||||
|
expect(res.body.profitLoss.income.total_periods).deep.equals(periods);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
} from '~/dbInit';
|
} from '~/dbInit';
|
||||||
|
|
||||||
|
|
||||||
describe('routes: `/items`', () => {
|
describe.only('routes: `/items`', () => {
|
||||||
describe('POST: `/items`', () => {
|
describe('POST: `/items`', () => {
|
||||||
it('Should not create a new item if the user was not authorized.', async () => {
|
it('Should not create a new item if the user was not authorized.', async () => {
|
||||||
const res = await request()
|
const res = await request()
|
||||||
@@ -553,6 +553,43 @@ describe('routes: `/items`', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('DELETE: `items?ids=`', () => {
|
||||||
|
it('Should response in case one of items ids where not exists.', async () => {
|
||||||
|
const res = await request()
|
||||||
|
.delete('/api/items')
|
||||||
|
.set('x-access-token', loginRes.body.token)
|
||||||
|
.set('organization-id', tenantWebsite.organizationId)
|
||||||
|
.query({
|
||||||
|
ids: [100, 200],
|
||||||
|
})
|
||||||
|
.send();
|
||||||
|
|
||||||
|
expect(res.status).equals(404);
|
||||||
|
expect(res.body.errors).include.something.that.deep.equals({
|
||||||
|
type: 'ITEMS.NOT.FOUND', code: 200, ids: [100, 200],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should delete the given items from the storage.', async () => {
|
||||||
|
const item1 = await tenantFactory.create('item');
|
||||||
|
const item2 = await tenantFactory.create('item');
|
||||||
|
|
||||||
|
const res = await request()
|
||||||
|
.delete('/api/items')
|
||||||
|
.set('x-access-token', loginRes.body.token)
|
||||||
|
.set('organization-id', tenantWebsite.organizationId)
|
||||||
|
.query({
|
||||||
|
ids: [item1.id, item2.id],
|
||||||
|
})
|
||||||
|
.send();
|
||||||
|
|
||||||
|
const foundItems = await Item.tenant().query();
|
||||||
|
|
||||||
|
expect(res.status).equals(200);
|
||||||
|
expect(foundItems.length).equals(0)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('GET: `items`', () => {
|
describe('GET: `items`', () => {
|
||||||
it('Should response unauthorized access in case the user not authenticated.', async () => {
|
it('Should response unauthorized access in case the user not authenticated.', async () => {
|
||||||
const res = await request()
|
const res = await request()
|
||||||
|
|||||||
@@ -1,13 +1,26 @@
|
|||||||
import { expect, create } from '~/testInit';
|
import { expect } from '~/testInit';
|
||||||
import JournalPoster from '@/services/Accounting/JournalPoster';
|
import JournalPoster from '@/services/Accounting/JournalPoster';
|
||||||
import JournalEntry from '@/services/Accounting/JournalEntry';
|
import JournalEntry from '@/services/Accounting/JournalEntry';
|
||||||
import AccountBalance from '@/models/AccountBalance';
|
import AccountBalance from '@/models/AccountBalance';
|
||||||
import AccountTransaction from '@/models/AccountTransaction';
|
import AccountTransaction from '@/models/AccountTransaction';
|
||||||
|
import Account from '@/models/Account';
|
||||||
|
import {
|
||||||
|
tenantWebsite,
|
||||||
|
tenantFactory,
|
||||||
|
loginRes
|
||||||
|
} from '~/dbInit';
|
||||||
|
import { omit } from 'lodash';
|
||||||
|
import DependencyGraph from '@/lib/DependencyGraph';
|
||||||
|
|
||||||
|
let accountsDepGraph;
|
||||||
|
|
||||||
describe('JournalPoster', () => {
|
describe('JournalPoster', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
accountsDepGraph = await Account.tenant().depGraph().query().remember();
|
||||||
|
});
|
||||||
describe('credit()', () => {
|
describe('credit()', () => {
|
||||||
it('Should write credit entry to journal entries stack.', () => {
|
it('Should write credit entry to journal entries stack.', () => {
|
||||||
const journalEntries = new JournalPoster();
|
const journalEntries = new JournalPoster(accountsDepGraph);
|
||||||
const journalEntry = new JournalEntry({
|
const journalEntry = new JournalEntry({
|
||||||
referenceId: 1,
|
referenceId: 1,
|
||||||
referenceType: 'Expense',
|
referenceType: 'Expense',
|
||||||
@@ -21,7 +34,7 @@ describe('JournalPoster', () => {
|
|||||||
|
|
||||||
describe('debit()', () => {
|
describe('debit()', () => {
|
||||||
it('Should write debit entry to journal entries stack.', () => {
|
it('Should write debit entry to journal entries stack.', () => {
|
||||||
const journalEntries = new JournalPoster();
|
const journalEntries = new JournalPoster(accountsDepGraph);
|
||||||
const journalEntry = new JournalEntry({
|
const journalEntry = new JournalEntry({
|
||||||
referenceId: 1,
|
referenceId: 1,
|
||||||
referenceType: 'Expense',
|
referenceType: 'Expense',
|
||||||
@@ -36,7 +49,7 @@ describe('JournalPoster', () => {
|
|||||||
|
|
||||||
describe('setBalanceChange()', () => {
|
describe('setBalanceChange()', () => {
|
||||||
it('Should increment balance amount after credit entry with credit normal account.', () => {
|
it('Should increment balance amount after credit entry with credit normal account.', () => {
|
||||||
const journalEntries = new JournalPoster();
|
const journalEntries = new JournalPoster(accountsDepGraph);
|
||||||
const journalEntry = new JournalEntry({
|
const journalEntry = new JournalEntry({
|
||||||
referenceId: 1,
|
referenceId: 1,
|
||||||
referenceType: 'Expense',
|
referenceType: 'Expense',
|
||||||
@@ -50,7 +63,7 @@ describe('JournalPoster', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('Should decrement balance amount after debit entry wiht debit normal account.', () => {
|
it('Should decrement balance amount after debit entry wiht debit normal account.', () => {
|
||||||
const journalEntries = new JournalPoster();
|
const journalEntries = new JournalPoster(accountsDepGraph);
|
||||||
const journalEntry = new JournalEntry({
|
const journalEntry = new JournalEntry({
|
||||||
referenceId: 1,
|
referenceId: 1,
|
||||||
referenceType: 'Expense',
|
referenceType: 'Expense',
|
||||||
@@ -65,7 +78,7 @@ describe('JournalPoster', () => {
|
|||||||
|
|
||||||
describe('saveEntries()', () => {
|
describe('saveEntries()', () => {
|
||||||
it('Should save all stacked entries to the storage.', async () => {
|
it('Should save all stacked entries to the storage.', async () => {
|
||||||
const journalEntries = new JournalPoster();
|
const journalEntries = new JournalPoster(accountsDepGraph);
|
||||||
const journalEntry = new JournalEntry({
|
const journalEntry = new JournalEntry({
|
||||||
referenceId: 1,
|
referenceId: 1,
|
||||||
referenceType: 'Expense',
|
referenceType: 'Expense',
|
||||||
@@ -77,7 +90,7 @@ describe('JournalPoster', () => {
|
|||||||
journalEntries.debit(journalEntry);
|
journalEntries.debit(journalEntry);
|
||||||
await journalEntries.saveEntries();
|
await journalEntries.saveEntries();
|
||||||
|
|
||||||
const storedJournalEntries = await AccountTransaction.query();
|
const storedJournalEntries = await AccountTransaction.tenant().query();
|
||||||
|
|
||||||
expect(storedJournalEntries.length).equals(1);
|
expect(storedJournalEntries.length).equals(1);
|
||||||
expect(storedJournalEntries[0]).to.deep.include({
|
expect(storedJournalEntries[0]).to.deep.include({
|
||||||
@@ -92,9 +105,10 @@ describe('JournalPoster', () => {
|
|||||||
|
|
||||||
describe('saveBalance()', () => {
|
describe('saveBalance()', () => {
|
||||||
it('Should save account balance increment.', async () => {
|
it('Should save account balance increment.', async () => {
|
||||||
const account = await create('account');
|
const account = await tenantFactory.create('account');
|
||||||
|
const depGraph = await Account.tenant().depGraph().query();
|
||||||
|
|
||||||
const journalEntries = new JournalPoster();
|
const journalEntries = new JournalPoster(depGraph);
|
||||||
const journalEntry = new JournalEntry({
|
const journalEntry = new JournalEntry({
|
||||||
referenceId: 1,
|
referenceId: 1,
|
||||||
referenceType: 'Expense',
|
referenceType: 'Expense',
|
||||||
@@ -106,16 +120,17 @@ describe('JournalPoster', () => {
|
|||||||
|
|
||||||
await journalEntries.saveBalance();
|
await journalEntries.saveBalance();
|
||||||
|
|
||||||
const storedAccountBalance = await AccountBalance.query();
|
const storedAccountBalance = await AccountBalance.tenant().query();
|
||||||
|
|
||||||
expect(storedAccountBalance.length).equals(1);
|
expect(storedAccountBalance.length).equals(1);
|
||||||
expect(storedAccountBalance[0].amount).equals(100);
|
expect(storedAccountBalance[0].amount).equals(100);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should save account balance decrement.', async () => {
|
it('Should save account balance decrement.', async () => {
|
||||||
const account = await create('account');
|
const account = await tenantFactory.create('account');
|
||||||
|
const depGraph = await Account.tenant().depGraph().query();
|
||||||
|
|
||||||
const journalEntries = new JournalPoster();
|
const journalEntries = new JournalPoster(depGraph);
|
||||||
const journalEntry = new JournalEntry({
|
const journalEntry = new JournalEntry({
|
||||||
referenceId: 1,
|
referenceId: 1,
|
||||||
referenceType: 'Expense',
|
referenceType: 'Expense',
|
||||||
@@ -127,7 +142,7 @@ describe('JournalPoster', () => {
|
|||||||
|
|
||||||
await journalEntries.saveBalance();
|
await journalEntries.saveBalance();
|
||||||
|
|
||||||
const storedAccountBalance = await AccountBalance.query();
|
const storedAccountBalance = await AccountBalance.tenant().query();
|
||||||
|
|
||||||
expect(storedAccountBalance.length).equals(1);
|
expect(storedAccountBalance.length).equals(1);
|
||||||
expect(storedAccountBalance[0].amount).equals(-100);
|
expect(storedAccountBalance[0].amount).equals(-100);
|
||||||
@@ -136,7 +151,7 @@ describe('JournalPoster', () => {
|
|||||||
|
|
||||||
describe('getClosingBalance', () => {
|
describe('getClosingBalance', () => {
|
||||||
it('Should retrieve closing balance the given account id.', () => {
|
it('Should retrieve closing balance the given account id.', () => {
|
||||||
const journalEntries = new JournalPoster();
|
const journalEntries = new JournalPoster(accountsDepGraph);
|
||||||
const journalEntry = new JournalEntry({
|
const journalEntry = new JournalEntry({
|
||||||
referenceId: 1,
|
referenceId: 1,
|
||||||
referenceType: 'Expense',
|
referenceType: 'Expense',
|
||||||
@@ -161,7 +176,7 @@ describe('JournalPoster', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('Should retrieve closing balance the given closing date period.', () => {
|
it('Should retrieve closing balance the given closing date period.', () => {
|
||||||
const journalEntries = new JournalPoster();
|
const journalEntries = new JournalPoster(accountsDepGraph);
|
||||||
const journalEntry = new JournalEntry({
|
const journalEntry = new JournalEntry({
|
||||||
referenceId: 1,
|
referenceId: 1,
|
||||||
referenceType: 'Expense',
|
referenceType: 'Expense',
|
||||||
@@ -188,7 +203,7 @@ describe('JournalPoster', () => {
|
|||||||
|
|
||||||
describe('getTrialBalance(account, closeDate, dateType)', () => {
|
describe('getTrialBalance(account, closeDate, dateType)', () => {
|
||||||
it('Should retrieve the trial balance of the given account id.', () => {
|
it('Should retrieve the trial balance of the given account id.', () => {
|
||||||
const journalEntries = new JournalPoster();
|
const journalEntries = new JournalPoster(accountsDepGraph);
|
||||||
const journalEntry = new JournalEntry({
|
const journalEntry = new JournalEntry({
|
||||||
referenceId: 1,
|
referenceId: 1,
|
||||||
referenceType: 'Expense',
|
referenceType: 'Expense',
|
||||||
@@ -222,7 +237,7 @@ describe('JournalPoster', () => {
|
|||||||
|
|
||||||
describe('removeEntries', () => {
|
describe('removeEntries', () => {
|
||||||
it('Should remove all entries in the collection.', () => {
|
it('Should remove all entries in the collection.', () => {
|
||||||
const journalPoster = new JournalPoster();
|
const journalPoster = new JournalPoster(accountsDepGraph);
|
||||||
const journalEntry1 = new JournalEntry({
|
const journalEntry1 = new JournalEntry({
|
||||||
id: 1,
|
id: 1,
|
||||||
credit: 1000,
|
credit: 1000,
|
||||||
@@ -244,7 +259,7 @@ describe('JournalPoster', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('Should remove the given entries ids from the collection.', () => {
|
it('Should remove the given entries ids from the collection.', () => {
|
||||||
const journalPoster = new JournalPoster();
|
const journalPoster = new JournalPoster(accountsDepGraph);
|
||||||
const journalEntry1 = new JournalEntry({
|
const journalEntry1 = new JournalEntry({
|
||||||
id: 1,
|
id: 1,
|
||||||
credit: 1000,
|
credit: 1000,
|
||||||
@@ -265,7 +280,7 @@ describe('JournalPoster', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('Should the removed entries ids be stacked to deleted entries ids.', () => {
|
it('Should the removed entries ids be stacked to deleted entries ids.', () => {
|
||||||
const journalPoster = new JournalPoster();
|
const journalPoster = new JournalPoster(accountsDepGraph);
|
||||||
const journalEntry1 = new JournalEntry({
|
const journalEntry1 = new JournalEntry({
|
||||||
id: 1,
|
id: 1,
|
||||||
credit: 1000,
|
credit: 1000,
|
||||||
@@ -289,7 +304,7 @@ describe('JournalPoster', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('Should revert the account balance after remove the entries.', () => {
|
it('Should revert the account balance after remove the entries.', () => {
|
||||||
const journalPoster = new JournalPoster();
|
const journalPoster = new JournalPoster(accountsDepGraph);
|
||||||
const journalEntry1 = new JournalEntry({
|
const journalEntry1 = new JournalEntry({
|
||||||
id: 1,
|
id: 1,
|
||||||
credit: 1000,
|
credit: 1000,
|
||||||
@@ -318,6 +333,59 @@ describe('JournalPoster', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('effectParentAccountsBalance()', () => {
|
||||||
|
it('Should all parent accounts increment after one of child accounts balance increment.', async () => {
|
||||||
|
const debitType = await tenantFactory.create('account_type', { normal: 'debit', balance_sheet: true });
|
||||||
|
const mixin = { account_type_id: debitType.id };
|
||||||
|
|
||||||
|
const accountA = await tenantFactory.create('account', { ...mixin });
|
||||||
|
const accountB = await tenantFactory.create('account', { ...mixin });
|
||||||
|
|
||||||
|
const accountAC = await tenantFactory.create('account', { parent_account_id: accountA.id, ...mixin });
|
||||||
|
const accountBD = await tenantFactory.create('account', { ...mixin });
|
||||||
|
|
||||||
|
const depGraph = await Account.tenant().depGraph().query();
|
||||||
|
const journalPoster = new JournalPoster(depGraph);
|
||||||
|
const journalEntryA = new JournalEntry({
|
||||||
|
id: 1,
|
||||||
|
debit: 1000,
|
||||||
|
account: accountAC.id,
|
||||||
|
accountNormal: 'debit',
|
||||||
|
});
|
||||||
|
const journalEntryB = new JournalEntry({
|
||||||
|
id: 1,
|
||||||
|
debit: 1000,
|
||||||
|
account: accountBD.id,
|
||||||
|
accountNormal: 'debit',
|
||||||
|
});
|
||||||
|
|
||||||
|
journalPoster.debit(journalEntryA);
|
||||||
|
journalPoster.debit(journalEntryB);
|
||||||
|
|
||||||
|
await journalPoster.saveBalance();
|
||||||
|
|
||||||
|
const accountBalances = await AccountBalance.tenant().query();
|
||||||
|
const simplifiedArray = accountBalances.map(x => ({ ...omit(x, ['id']) }));
|
||||||
|
|
||||||
|
expect(simplifiedArray.length).equals(3);
|
||||||
|
expect(simplifiedArray).to.include.something.deep.equals({
|
||||||
|
accountId: accountA.id,
|
||||||
|
amount: 1000,
|
||||||
|
currencyCode: 'USD'
|
||||||
|
});
|
||||||
|
expect(simplifiedArray).to.include.something.deep.equals({
|
||||||
|
accountId: accountAC.id,
|
||||||
|
amount: 1000,
|
||||||
|
currencyCode: 'USD'
|
||||||
|
});
|
||||||
|
expect(simplifiedArray).to.include.something.deep.equals({
|
||||||
|
accountId: accountBD.id,
|
||||||
|
amount: 1000,
|
||||||
|
currencyCode: 'USD'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('reverseEntries()', () => {
|
describe('reverseEntries()', () => {
|
||||||
|
|
||||||
});
|
});
|
||||||
Reference in New Issue
Block a user