- fix: store children accounts with Redux store.

- fix: store expense payment date with transactions.
- fix: Total assets, liabilities and equity on balance sheet.
- tweaks: dashboard content and sidebar style.
- fix: reset form with contact list on journal entry form.
- feat: Add hints to filter accounts in financial statements.
This commit is contained in:
Ahmed Bouhuolia
2020-07-12 12:31:12 +02:00
parent 4bd8f1628d
commit 9d9c7c1568
60 changed files with 1685 additions and 929 deletions

View File

@@ -57,6 +57,19 @@ function log(text) {
console.log(text);
}
function getDeepValue(prop, obj) {
if (!Object.keys(obj).length) { return []; }
return Object.entries(obj).reduce((acc, [key, val]) => {
if (key === prop) {
acc.push(val);
} else {
acc.push(Array.isArray(val) ? val.map(getIds).flat() : getIds(val));
}
return acc.flat();
}, []);
}
module.exports = {
initTenantKnex,
initSystemKnex,
@@ -64,4 +77,5 @@ module.exports = {
exit,
success,
log,
getDeepValue,
}

View File

@@ -0,0 +1,62 @@
export default [
{
name: 'Assets',
section_type: 'assets',
type: 'section',
children: [
{
name: 'Current Asset',
type: 'section',
_accounts_types_related: ['current_asset'],
},
{
name: 'Fixed Asset',
type: 'section',
_accounts_types_related: ['fixed_asset'],
},
{
name: 'Other Asset',
type: 'section',
_accounts_types_related: ['other_asset'],
},
],
_forceShow: true,
},
{
name: 'Liabilities and Equity',
section_type: 'liabilities_equity',
type: 'section',
children: [
{
name: 'Liabilities',
section_type: 'liability',
type: 'section',
children: [
{
name: 'Current Liability',
type: 'section',
_accounts_types_related: ['current_liability'],
},
{
name: 'Long Term Liability',
type: 'section',
_accounts_types_related: ['long_term_liability'],
},
{
name: 'Other Liability',
type: 'section',
_accounts_types_related: ['other_liability'],
},
],
},
{
name: 'Equity',
section_type: 'equity',
type: 'section',
_accounts_types_related: ['equity'],
},
],
_forceShow: true,
},
];

View File

@@ -6,6 +6,7 @@ exports.up = (knex) => {
table.string('key');
table.string('normal');
table.string('root_type');
table.string('child_type');
table.boolean('balance_sheet');
table.boolean('income_sheet');
}).raw('ALTER TABLE `ACCOUNT_TYPES` AUTO_INCREMENT = 1000').then(() => {

View File

@@ -11,6 +11,7 @@ exports.seed = (knex) => {
key: 'fixed_asset',
normal: 'debit',
root_type: 'asset',
child_type: 'fixed_asset',
balance_sheet: true,
income_sheet: false,
},
@@ -20,6 +21,17 @@ exports.seed = (knex) => {
key: 'current_asset',
normal: 'debit',
root_type: 'asset',
child_type: 'current_asset',
balance_sheet: true,
income_sheet: false,
},
{
id: 14,
name: 'Other Asset',
key: 'other_asset',
normal: 'debit',
root_type: 'asset',
child_type: 'other_asset',
balance_sheet: true,
income_sheet: false,
},
@@ -29,6 +41,7 @@ exports.seed = (knex) => {
key: 'long_term_liability',
normal: 'credit',
root_type: 'liability',
child_type: 'long_term_liability',
balance_sheet: false,
income_sheet: true,
},
@@ -38,6 +51,17 @@ exports.seed = (knex) => {
key: 'current_liability',
normal: 'credit',
root_type: 'liability',
child_type: 'current_liability',
balance_sheet: false,
income_sheet: true,
},
{
id: 13,
name: 'Other Liability',
key: 'other_liability',
normal: 'credit',
root_type: 'liability',
child_type: 'other_liability',
balance_sheet: false,
income_sheet: true,
},
@@ -47,6 +71,7 @@ exports.seed = (knex) => {
key: 'equity',
normal: 'credit',
root_type: 'equity',
child_type: 'equity',
balance_sheet: true,
income_sheet: false,
},
@@ -56,6 +81,16 @@ exports.seed = (knex) => {
key: 'expense',
normal: 'debit',
root_type: 'expense',
child_type: 'expense',
balance_sheet: false,
income_sheet: true,
},
{
id: 10,
name: 'Other Expense',
key: 'other_expense',
normal: 'debit',
root_type: 'expense',
balance_sheet: false,
income_sheet: true,
},
@@ -65,24 +100,47 @@ exports.seed = (knex) => {
key: 'income',
normal: 'credit',
root_type: 'income',
child_type: 'income',
balance_sheet: false,
income_sheet: true,
},
{
id: 11,
name: 'Other Income',
key: 'other_income',
normal: 'credit',
root_type: 'income',
child_type: 'other_income',
balance_sheet: false,
income_sheet: true,
},
{
id: 12,
name: 'Cost of Goods Sold (COGS)',
key: 'cost_of_goods_sold',
normal: 'debit',
root_type: 'asset',
child_type: 'current_asset',
balance_sheet: true,
income_sheet: false,
},
{
id: 8,
name: 'Accounts Receivable',
name: 'Accounts Receivable (A/R)',
key: 'accounts_receivable',
normal: 'debit',
root_type: 'asset',
child_type: 'current_asset',
balance_sheet: true,
income_sheet: false,
},
{
id: 9,
name: 'Accounts Payable',
name: 'Accounts Payable (A/P)',
key: 'accounts_payable',
normal: 'credit',
root_type: 'liability',
child_type: 'current_liability',
balance_sheet: true,
income_sheet: false,
},

View File

@@ -3,7 +3,6 @@ import { check, validationResult, param, query } from 'express-validator';
import { difference } from 'lodash';
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
import JournalPoster from '@/services/Accounting/JournalPoster';
import NestedSet from '@/collection/NestedSet';
import {
mapViewRolesToConditionals,
mapFilterRolesToDynamicFilter,
@@ -97,9 +96,13 @@ export default {
newAccount: {
validation: [
check('name').exists().isLength({ min: 3, max: 255 }).trim().escape(),
check('code').optional().isLength({ min: 3, max: 6, }).trim().escape(),
check('code').optional().isLength({ min: 3, max: 6 }).trim().escape(),
check('account_type_id').exists().isNumeric().toInt(),
check('description').optional().isLength({ max: 512 }).trim().escape(),
check('parent_account_id')
.optional({ nullable: true })
.isNumeric()
.toInt(),
],
async handler(req, res) {
const validationErrors = validationResult(req);
@@ -125,7 +128,6 @@ export default {
foundAccountCodePromise,
foundAccountTypePromise,
]);
if (foundAccountCodePromise && foundAccountCode.length > 0) {
return res.boom.badRequest(null, {
errors: [{ type: 'NOT_UNIQUE_CODE', code: 100 }],
@@ -136,6 +138,24 @@ export default {
errors: [{ type: 'NOT_EXIST_ACCOUNT_TYPE', code: 200 }],
});
}
if (form.parent_account_id) {
const parentAccount = await Account.query()
.where('id', form.parent_account_id)
.first();
if (!parentAccount) {
return res.boom.badRequest(null, {
errors: [{ type: 'PARENT_ACCOUNT_NOT_FOUND', code: 300 }],
});
}
if (parentAccount.accountTypeId !== form.parent_account_id) {
return res.boom.badRequest(null, {
errors: [
{ type: 'PARENT.ACCOUNT.HAS.DIFFERENT.ACCOUNT.TYPE', code: 400 },
],
});
}
}
const insertedAccount = await Account.query().insertAndFetch({ ...form });
return res.status(200).send({ account: { ...insertedAccount } });
@@ -148,8 +168,8 @@ export default {
editAccount: {
validation: [
param('id').exists().toInt(),
check('name').exists().isLength({ min: 3, max: 255, }).trim().escape(),
check('code').optional().isLength({ min: 3, max: 6, }).trim().escape(),
check('name').exists().isLength({ min: 3, max: 255 }).trim().escape(),
check('code').optional().isLength({ min: 3, max: 6 }).trim().escape(),
check('account_type_id').exists().isNumeric().toInt(),
check('description').optional().isLength({ max: 512 }).trim().escape(),
],
@@ -189,7 +209,26 @@ export default {
errorReasons.push({ type: 'NOT_UNIQUE_CODE', code: 200 });
}
}
if (form.parent_account_id) {
const parentAccount = await Account.query()
.where('id', form.parent_account_id)
.whereNot('id', account.id)
.first();
if (!parentAccount) {
errorReasons.push({
type: 'PARENT_ACCOUNT_NOT_FOUND',
code: 300,
});
}
if (parentAccount.accountTypeId !== account.parentAccountId) {
return res.boom.badRequest(null, {
errors: [
{ type: 'PARENT.ACCOUNT.HAS.DIFFERENT.ACCOUNT.TYPE', code: 400 },
],
});
}
}
if (errorReasons.length > 0) {
return res.status(400).send({ errors: errorReasons });
}
@@ -238,11 +277,22 @@ export default {
errors: [{ type: 'ACCOUNT.PREDEFINED', code: 200 }],
});
}
// Validate the account has no child accounts.
const childAccounts = await Account.query().where(
'parent_account_id',
account.id
);
if (childAccounts.length > 0) {
return res.boom.badRequest(null, {
errors: [{ type: 'ACCOUNT.HAS.CHILD.ACCOUNTS', code: 300 }],
});
}
const accountTransactions = await AccountTransaction.query().where(
'account_id',
account.id
);
if (accountTransactions.length > 0) {
return res.boom.badRequest(null, {
errors: [{ type: 'ACCOUNT.HAS.ASSOCIATED.TRANSACTIONS', code: 100 }],
@@ -279,8 +329,8 @@ export default {
});
}
const filter = {
display_type: 'flat',
account_types: [],
display_type: 'tree',
filter_roles: [],
sort_order: 'asc',
...req.query,
@@ -364,38 +414,18 @@ export default {
return res.status(400).send({ errors: errorReasons });
}
const query = Account.query()
// .remember()
.onBuild((builder) => {
builder.modify('filterAccountTypes', filter.account_types);
builder.withGraphFetched('type');
builder.withGraphFetched('balance');
dynamicFilter.buildQuery()(builder);
// console.log(builder.toKnexQuery().toSQL());
})
.toKnexQuery()
.toSQL();
console.log(query);
const accounts = await Account.query()
// .remember()
.onBuild((builder) => {
builder.modify('filterAccountTypes', filter.account_types);
builder.withGraphFetched('type');
builder.withGraphFetched('balance');
dynamicFilter.buildQuery()(builder);
// console.log(builder.toKnexQuery().toSQL());
});
const nestedAccounts = Account.toNestedArray(accounts);
const accounts = await Account.query().onBuild((builder) => {
builder.modify('filterAccountTypes', filter.account_types);
builder.withGraphFetched('type');
builder.withGraphFetched('balance');
dynamicFilter.buildQuery()(builder);
});
return res.status(200).send({
accounts: nestedAccounts,
accounts:
filter.display_type === 'tree'
? Account.toNestedArray(accounts)
: accounts,
...(view
? {
customViewId: view.id,

View File

@@ -181,6 +181,7 @@ export default {
const mixinEntry = {
referenceType: 'Expense',
referenceId: expenseTransaction.id,
date: moment(form.payment_date).format('YYYY-MM-DD'),
userId: user.id,
draft: !form.publish,
};

View File

@@ -1,13 +1,12 @@
import express from 'express';
import { query, oneOf, validationResult } from 'express-validator';
import { query, validationResult } from 'express-validator';
import moment from 'moment';
import { pick, difference, groupBy } from 'lodash';
import { pick, omit, sumBy } from 'lodash';
import JournalPoster from '@/services/Accounting/JournalPoster';
import { dateRangeCollection } from '@/utils';
import DependencyGraph from '@/lib/DependencyGraph';
import asyncMiddleware from '@/http/middleware/asyncMiddleware'
import { dateRangeCollection, itemsStartWith, getTotalDeep } from '@/utils';
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
import { formatNumberClosure } from './FinancialStatementMixin';
import BalanceSheetStructure from '@/data/BalanceSheetStructure';
export default {
/**
@@ -16,13 +15,15 @@ export default {
router() {
const router = express.Router();
router.get('/',
router.get(
'/',
this.balanceSheet.validation,
asyncMiddleware(this.balanceSheet.handler));
asyncMiddleware(this.balanceSheet.handler)
);
return router;
},
/**
* Retrieve the balance sheet.
*/
@@ -32,7 +33,8 @@ export default {
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 })
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(),
@@ -45,7 +47,8 @@ export default {
if (!validationErrors.isEmpty()) {
return res.boom.badData(null, {
code: 'validation_error', ...validationErrors,
code: 'validation_error',
...validationErrors,
});
}
const { Account, AccountType } = req.models;
@@ -67,106 +70,168 @@ export default {
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);
const amountFormatter = 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))
.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' }
);
const accountsGraph = Account.toDependencyGraph(accounts);
// 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,
) : [];
// 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) => ({
const getAccountTotalPeriods = (account) => ({
total_periods: dateRangeSet.map((date) => {
const amount = journalEntries.getAccountBalance(account.id, date, comparatorDateType);
const amount = journalEntries.getAccountBalance(
account.id,
date,
comparatorDateType
);
return {
amount,
formatted_amount: balanceFormatter(amount),
date,
formatted_amount: amountFormatter(amount),
};
}),
});
const accountsMapperToResponse = (account) => {
// Calculates the closing balance to the given date.
const closingBalance = journalEntries.getAccountBalance(account.id, filter.to_date);
// Retrieve accounts total periods.
const getAccountsTotalPeriods = (_accounts) =>
Object.values(
dateRangeSet.reduce((acc, date, index) => {
const amount = sumBy(_accounts, `total_periods[${index}].amount`);
acc[date] = {
date,
amount,
formatted_amount: amountFormatter(amount),
};
return acc;
}, {})
);
// Retrieve account total and total periods with account meta.
const getAccountTotal = (account) => {
const closingBalance = journalEntries.getAccountBalance(
account.id,
filter.to_date
);
const totalPeriods =
(filter.display_columns_type === 'date_periods' &&
getAccountTotalPeriods(account)) ||
null;
return {
...pick(account, ['id', 'index', 'name', 'code', 'parentAccountId']),
// Date periods when display columns.
...(filter.display_columns_type === 'date_periods') && totalPeriods(account),
...(totalPeriods && { totalPeriods }),
total: {
amount: closingBalance,
formatted_amount: balanceFormatter(closingBalance),
formatted_amount: amountFormatter(closingBalance),
date: filter.to_date,
},
};
};
// Get accounts total of the given structure section
const getAccountsSectionTotal = (_accounts) => {
const total = getTotalDeep(_accounts, 'children', 'total.amount');
return {
total: {
total,
formatted_amount: amountFormatter(total),
},
};
};
// Strcuture accounts related mapper.
const structureAccountsRelatedMapper = (accountsTypes) => {
const filteredAccounts = accounts
// Filter accounts that have no transaction when `none_zero` is on.
.filter(
(account) => account.transactions.length > 0 || !filter.none_zero
)
// Filter accounts that associated to the section accounts types.
.filter(
(account) => accountsTypes.indexOf(account.type.childType) !== -1
)
.map(getAccountTotal);
// Gets total amount of the given accounts.
const totalAmount = sumBy(filteredAccounts, 'total.amount');
// 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);
return {
children: Account.toNestedArray(filteredAccounts),
total: {
amount: totalAmount,
formatted_amount: amountFormatter(totalAmount),
},
...(filter.display_columns_type === 'date_periods'
? {
total_periods: getAccountsTotalPeriods(filteredAccounts),
}
: {}),
};
};
// Structure section mapper.
const structureSectionMapper = (structure) => {
const result = {
...omit(structure, itemsStartWith(Object.keys(structure), '_')),
...(structure.children
? {
children: balanceSheetWalker(structure.children),
}
: {}),
...(structure._accounts_types_related
? {
...structureAccountsRelatedMapper(
structure._accounts_types_related
),
}
: {}),
};
return {
...result,
...(!structure._accounts_types_related
? getAccountsSectionTotal(result.children)
: {}),
};
};
const balanceSheetWalker = (reportStructure) =>
reportStructure.map(structureSectionMapper).filter(
// Filter the structure sections that have no children.
(structure) => structure.children.length > 0 || structure._forceShow
);
// 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],
},
],
balance_sheet: [...balanceSheetWalker(BalanceSheetStructure)],
});
},
},
}
};

View File

@@ -1,12 +1,11 @@
import express from 'express';
import { query, oneOf, validationResult } from 'express-validator';
import moment from 'moment';
import { pick } from 'lodash';
import { pick, sumBy } 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 {
/**
@@ -15,10 +14,11 @@ export default {
router() {
const router = express.Router();
router.get('/',
router.get(
'/',
this.profitLossSheet.validation,
asyncMiddleware(this.profitLossSheet.handler));
asyncMiddleware(this.profitLossSheet.handler)
);
return router;
},
@@ -36,10 +36,9 @@ export default {
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 })
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) {
@@ -47,7 +46,8 @@ export default {
if (!validationErrors.isEmpty()) {
return res.boom.badData(null, {
code: 'validation_error', ...validationErrors,
code: 'validation_error',
...validationErrors,
});
}
const { Account, AccountType } = req.models;
@@ -68,91 +68,110 @@ export default {
if (!Array.isArray(filter.account_ids)) {
filter.account_ids = [filter.account_ids];
}
const incomeStatementTypes = await AccountType.query().where('income_sheet', true);
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))
.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' }
);
const accountsGraph = Account.toDependencyGraph(accounts);
// Filter all none zero accounts if it was enabled.
const filteredAccounts = accounts.filter((account) => (
account.transactions.length > 0 || !filter.none_zero
));
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;
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,
comparatorDateType
);
const accountsMapper = (incomeExpenseAccounts) => (
const accountsMapper = (incomeExpenseAccounts) =>
incomeExpenseAccounts.map((account) => ({
...pick(account, ['id', 'index', 'name', 'code', 'parentAccountId']),
// Total closing balance of the account.
...(filter.display_columns_type === 'total') && {
...(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) };
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') && {
...(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 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')
)
);
const totalPeriodsMapper = (incomeExpenseAccounts) =>
Object.values(
dateRangeSet.reduce((acc, date, index) => {
let amount = sumBy(
incomeExpenseAccounts,
`periods[${index}].amount`
);
acc[date] = {
date,
amount,
formatted_amount: numberFormatter(amount),
};
return acc;
}, {})
);
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) => (
// Total income - Total expenses = Net income
const netIncomePeriodsMapper = (
totalIncomeAcocunts,
totalExpenseAccounts
) =>
dateRangeSet.map((date, index) => {
const totalIncome = totalIncomeAcocunts[index];
const totalExpenses = totalExpenseAccounts[index];
@@ -160,66 +179,71 @@ export default {
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 };
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),
],
},
...(filter.display_columns_type === 'total' &&
(() => {
const totalIncomeAccounts = sumBy(accountsIncome, 'total.amount');
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),
],
},
...(filter.display_columns_type === 'total' &&
(() => {
const totalExpensesAccounts = sumBy(
accountsExpenses,
'total.amount'
);
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') && {
...(filter.display_columns_type === 'total' && {
total: {
...netIncomeTotal(incomeResponse.total, expenseResponse.total),
},
},
...(filter.display_columns_type === 'date_periods') && {
}),
...(filter.display_columns_type === 'date_periods' && {
total_periods: [
...netIncomePeriodsMapper(
incomeResponse.total_periods,
expenseResponse.total_periods,
expenseResponse.total_periods
),
],
},
}),
};
return res.status(200).send({
query: { ...filter },
@@ -228,8 +252,8 @@ export default {
income: incomeResponse,
expenses: expenseResponse,
net_income: netIncomeResponse,
},
},
});
},
},
}
};

View File

@@ -1,7 +1,6 @@
import bcrypt from 'bcryptjs';
import moment from 'moment';
import _ from 'lodash';
const { map, isArray, isPlainObject, mapKeys, mapValues } = require('lodash');
const hashPassword = (password) =>
new Promise((resolve) => {
@@ -118,10 +117,21 @@ const flatToNestedArray = (
map[parentItemId].children.push(item);
}
});
return nestedArray;
};
const itemsStartWith = (items, char) => {
return items.filter((item) => item.indexOf(char) === 0);
};
const getTotalDeep = (items, deepProp, totalProp) =>
items.reduce((acc, item) => {
const total = Array.isArray(item[deepProp])
? getTotalDeep(item[deepProp], deepProp, totalProp)
: 0;
return _.sumBy(item, totalProp) + total + acc;
}, 0);
export {
hashPassword,
origin,
@@ -131,4 +141,6 @@ export {
mapKeysDeep,
promiseSerial,
flatToNestedArray,
itemsStartWith,
getTotalDeep,
};

View File

@@ -13,7 +13,7 @@ import {
} from '~/dbInit';
describe.only('routes: `/accounting`', () => {
describe('routes: `/accounting`', () => {
describe('route: `/accounting/make-journal-entries`', async () => {
it('Should sumation of credit or debit does not equal zero.', async () => {
const account = await tenantFactory.create('account');

View File

@@ -0,0 +1,541 @@
import moment from 'moment';
import {
request,
expect,
} from '~/testInit';
import {
tenantWebsite,
tenantFactory,
loginRes
} from '~/dbInit';
import { iteratee } from 'lodash';
let creditAccount;
let debitAccount;
let incomeAccount;
let incomeType;
describe('routes: `/financial_statements`', () => {
beforeEach(async () => {
const accountTransactionMixied = { date: '2020-1-10' };
// Expense --
// 1000 Credit - Cash account
// 1000 Debit - Bank account.
await tenantFactory.create('account_transaction', {
credit: 1000, debit: 0, account_id: 2, referenceType: 'Expense',
referenceId: 1, ...accountTransactionMixied,
});
await tenantFactory.create('account_transaction', {
credit: 0, debit: 1000, account_id: 7, referenceType: 'Expense',
referenceId: 1, ...accountTransactionMixied,
});
// Jounral
// 4000 Credit - Opening balance account.
// 2000 Debit - Bank account
// 2000 Debit - Bank account
await tenantFactory.create('account_transaction', {
credit: 4000, debit: 0, account_id: 5, ...accountTransactionMixied,
});
await tenantFactory.create('account_transaction', {
debit: 2000, credit: 0, account_id: 2, ...accountTransactionMixied,
});
await tenantFactory.create('account_transaction', {
debit: 2000, credit: 0, account_id: 2, ...accountTransactionMixied,
});
// Income Journal.
// 2000 Credit - Income account.
// 2000 Debit - Bank account.
await tenantFactory.create('account_transaction', {
credit: 2000, account_id: 4, ...accountTransactionMixied
});
await tenantFactory.create('account_transaction', {
debit: 2000, credit: 0, account_id: 2, ...accountTransactionMixied,
});
// -----------------------------------------
// Bank account balance = 5000 | Opening balance account balance = 4000
// Expense account balance = 1000 | Income account balance = 2000
});
describe.only('routes: `financial_statements/balance_sheet`', () => {
it('Should response unauthorzied in case the user was not authorized.', async () => {
const res = await request()
.get('/api/financial_statements/balance_sheet')
.send();
expect(res.status).equals(401);
});
it('Should retrieve query of the balance sheet with default values.', async () => {
const res = await request()
.get('/api/financial_statements/balance_sheet')
.set('x-access-token', loginRes.body.token)
.set('organization-id', tenantWebsite.organizationId)
.query({
display_columns_by: 'year',
from_date: '2020-01-01',
to_date: '2020-02-01',
})
.send();
expect(res.body.query.display_columns_by).equals('year');
expect(res.body.query.from_date).equals('2020-01-01');
expect(res.body.query.to_date).equals('2020-02-01');
expect(res.body.query.number_format.no_cents).equals(false);
expect(res.body.query.number_format.divide_1000).equals(false);
expect(res.body.query.none_zero).equals(false);
});
it('Should retrieve assets and liabilities/equity section.', async () => {
const res = await request()
.get('/api/financial_statements/balance_sheet')
.set('x-access-token', loginRes.body.token)
.set('organization-id', tenantWebsite.organizationId)
.query({
display_columns_by: 'year',
})
.send();
expect(res.body.balance_sheet[0].name).equals('Assets');
expect(res.body.balance_sheet[1].name).equals('Liabilities and Equity');
expect(res.body.balance_sheet[0].section_type).equals('assets');
expect(res.body.balance_sheet[1].section_type).equals('liabilities_equity');
expect(res.body.balance_sheet[0].type).equals('section');
expect(res.body.balance_sheet[1].type).equals('section');
});
it.only('Should retrieve assets and liabilities/equity total of each section.', async () => {
const res = await request()
.get('/api/financial_statements/balance_sheet')
.set('x-access-token', loginRes.body.token)
.set('organization-id', tenantWebsite.organizationId)
.query({
to_date: '2020-12-10',
})
.send();
expect(res.body.balance_sheet[0].total.amount).equals(5000);
expect(res.body.balance_sheet[1].total.amount).equals(4000);
});
it('Should retrieve the asset and liabilities/equity accounts.', async () => {
const res = await request()
.get('/api/financial_statements/balance_sheet')
.set('x-access-token', loginRes.body.token)
.set('organization-id', tenantWebsite.organizationId)
.query({
display_columns_type: 'total',
from_date: '2012-01-01',
to_date: '2032-02-02',
})
.send();
expect(res.body.balance_sheet[0].children).to.be.a('array');
expect(res.body.balance_sheet[0].children).to.be.a('array');
expect(res.body.balance_sheet[0].children.length).is.not.equals(0);
expect(res.body.balance_sheet[1].children.length).is.not.equals(0);
expect(res.body.balance_sheet[1].children[0].children.length).is.not.equals(0);
expect(res.body.balance_sheet[1].children[1].children.length).is.not.equals(0);
});
it('Should retrieve assets/liabilities total balance between the given date range.', async () => {
const res = await request()
.get('/api/financial_statements/balance_sheet')
.set('x-access-token', loginRes.body.token)
.set('organization-id', tenantWebsite.organizationId)
.query({
display_columns_type: 'total',
from_date: '2012-01-01',
to_date: '2032-02-02',
})
.send();
expect(res.body.accounts[0].children).include.something.deep.equals({
id: 1001,
index: null,
name: debitAccount.name,
code: debitAccount.code,
parentAccountId: null,
children: [],
total: { formatted_amount: 5000, amount: 5000, date: '2032-02-02' }
});
expect(res.body.accounts[1].children).include.something.deep.equals({
id: 1000,
index: null,
name: creditAccount.name,
code: creditAccount.code,
parentAccountId: null,
children: [],
total: { formatted_amount: 4000, amount: 4000, date: '2032-02-02' }
});
});
it('Should retrieve asset/liabilities balance sheet with display columns by `year`.', async () => {
const res = await request()
.get('/api/financial_statements/balance_sheet')
.set('x-access-token', loginRes.body.token)
.set('organization-id', tenantWebsite.organizationId)
.query({
display_columns_by: 'year',
display_columns_type: 'date_periods',
from_date: '2012-01-01',
to_date: '2018-02-02',
})
.send();
expect(res.body.accounts[0].children[0].total_periods.length).equals(7);
expect(res.body.accounts[1].children[0].total_periods.length).equals(7);
expect(res.body.accounts[0].children[0].total_periods).deep.equals([
{
amount: 0,
formatted_amount: 0,
date: '2012',
},
{
amount: 0,
formatted_amount: 0,
date: '2013',
},
{
amount: 0,
formatted_amount: 0,
date: '2014',
},
{
amount: 0,
formatted_amount: 0,
date: '2015',
},
{
amount: 0,
formatted_amount: 0,
date: '2016',
},
{
amount: 0,
formatted_amount: 0,
date: '2017',
},
{
amount: 0,
formatted_amount: 0,
date: '2018',
},
]);
});
it('Should retrieve balance sheet with display columns by `day`.', async () => {
const res = await request()
.get('/api/financial_statements/balance_sheet')
.set('x-access-token', loginRes.body.token)
.set('organization-id', tenantWebsite.organizationId)
.query({
display_columns_by: 'day',
display_columns_type: 'date_periods',
from_date: '2020-01-08',
to_date: '2020-01-12',
})
.send();
expect(res.body.accounts[0].children).include.something.deep.equals({
id: debitAccount.id,
index: debitAccount.index,
name: debitAccount.name,
code: debitAccount.code,
parentAccountId: null,
children: [],
total_periods: [
{ date: '2020-01-08', formatted_amount: 0, amount: 0 },
{ date: '2020-01-09', formatted_amount: 0, amount: 0 },
{ date: '2020-01-10', formatted_amount: 5000, amount: 5000 },
{ date: '2020-01-11', formatted_amount: 5000, amount: 5000 },
{ date: '2020-01-12', formatted_amount: 5000, amount: 5000 },
],
total: { formatted_amount: 5000, amount: 5000, date: '2020-01-12' }
});
expect(res.body.accounts[1].children).include.something.deep.equals({
id: creditAccount.id,
index: creditAccount.index,
name: creditAccount.name,
code: creditAccount.code,
parentAccountId: null,
children: [],
total_periods: [
{ date: '2020-01-08', formatted_amount: 0, amount: 0 },
{ date: '2020-01-09', formatted_amount: 0, amount: 0 },
{ date: '2020-01-10', formatted_amount: 4000, amount: 4000 },
{ date: '2020-01-11', formatted_amount: 4000, amount: 4000 },
{ date: '2020-01-12', formatted_amount: 4000, amount: 4000 }
],
total: { formatted_amount: 4000, amount: 4000, date: '2020-01-12' }
});
});
it('Should retrieve the balance sheet with display columns by `month`.', async () => {
const res = await request()
.get('/api/financial_statements/balance_sheet')
.set('x-access-token', loginRes.body.token)
.set('organization-id', tenantWebsite.organizationId)
.query({
display_columns_by: 'month',
display_columns_type: 'date_periods',
from_date: '2019-07-01',
to_date: '2020-06-30',
})
.send();
expect(res.body.accounts[0].children).include.something.deep.equals({
id: debitAccount.id,
index: debitAccount.index,
name: debitAccount.name,
code: debitAccount.code,
parentAccountId: null,
children: [],
total_periods: [
{ date: '2019-07', formatted_amount: 0, amount: 0 },
{ date: '2019-08', formatted_amount: 0, amount: 0 },
{ date: '2019-09', formatted_amount: 0, amount: 0 },
{ date: '2019-10', formatted_amount: 0, amount: 0 },
{ date: '2019-11', formatted_amount: 0, amount: 0 },
{ date: '2019-12', formatted_amount: 0, amount: 0 },
{ date: '2020-01', formatted_amount: 5000, amount: 5000 },
{ date: '2020-02', formatted_amount: 5000, amount: 5000 },
{ date: '2020-03', formatted_amount: 5000, amount: 5000 },
{ date: '2020-04', formatted_amount: 5000, amount: 5000 },
{ date: '2020-05', formatted_amount: 5000, amount: 5000 },
{ date: '2020-06', formatted_amount: 5000, amount: 5000 },
],
total: { formatted_amount: 5000, amount: 5000, date: '2020-06-30' }
});
});
it('Should retrieve the balance sheet with display columns `quarter`.', async () => {
const res = await request()
.get('/api/financial_statements/balance_sheet')
.set('x-access-token', loginRes.body.token)
.set('organization-id', tenantWebsite.organizationId)
.query({
display_columns_by: 'quarter',
display_columns_type: 'date_periods',
from_date: '2020-01-01',
to_date: '2020-12-31',
})
.send();
expect(res.body.accounts[0].children).include.something.deep.equals({
id: debitAccount.id,
index: debitAccount.index,
name: debitAccount.name,
code: debitAccount.code,
parentAccountId: null,
children: [],
total_periods: [
{ date: '2020-03', formatted_amount: 5000, amount: 5000 },
{ date: '2020-06', formatted_amount: 5000, amount: 5000 },
{ date: '2020-09', formatted_amount: 5000, amount: 5000 },
{ date: '2020-12', formatted_amount: 5000, amount: 5000 },
],
total: { formatted_amount: 5000, amount: 5000, date: '2020-12-31' },
});
});
it('Should retrieve the balance sheet amounts without cents.', async () => {
await tenantFactory.create('account_transaction', {
debit: 0.25, credit: 0, account_id: debitAccount.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({
display_columns_by: 'quarter',
display_columns_type: 'date_periods',
from_date: '2020-01-01',
to_date: '2020-12-31',
number_format: {
no_cents: true,
},
})
.send();
expect(res.body.accounts[0].children).include.something.deep.equals({
id: debitAccount.id,
index: debitAccount.index,
name: debitAccount.name,
code: debitAccount.code,
parentAccountId: null,
children: [],
total_periods: [
{ date: '2020-03', formatted_amount: 5000, amount: 5000.25 },
{ date: '2020-06', formatted_amount: 5000, amount: 5000.25 },
{ date: '2020-09', formatted_amount: 5000, amount: 5000.25 },
{ date: '2020-12', formatted_amount: 5000, amount: 5000.25 },
],
total: { formatted_amount: 5000, amount: 5000.25, date: '2020-12-31' },
});
});
it('Should retrieve the balance sheet amounts divided on 1000.', async () => {
const res = await request()
.get('/api/financial_statements/balance_sheet')
.set('x-access-token', loginRes.body.token)
.set('organization-id', tenantWebsite.organizationId)
.query({
display_columns_by: 'quarter',
display_columns_type: 'date_periods',
from_date: '2020',
to_date: '2021',
number_format: {
divide_1000: true,
},
})
.send();
expect(res.body.accounts[0].children).include.something.deep.equals({
id: debitAccount.id,
index: debitAccount.index,
name: debitAccount.name,
code: debitAccount.code,
parentAccountId: null,
children: [],
total_periods: [
{ date: '2020-03', formatted_amount: 5, amount: 5000 },
{ date: '2020-06', formatted_amount: 5, amount: 5000 },
{ date: '2020-09', formatted_amount: 5, amount: 5000 },
{ date: '2020-12', formatted_amount: 5, amount: 5000 },
{ date: '2021-03', formatted_amount: 5, amount: 5000 },
],
total: { formatted_amount: 5, amount: 5000, date: '2021' },
});
});
it('Should not retrieve accounts has no transactions between the given date range in case query none_zero is true.', async () => {
const res = await request()
.get('/api/financial_statements/balance_sheet')
.set('x-access-token', loginRes.body.token)
.set('organization-id', tenantWebsite.organizationId)
.query({
display_columns_by: 'quarter',
from_date: '2002',
to_date: '2003',
number_format: {
divide_1000: true,
},
none_zero: true,
})
.send();
expect(res.body.accounts[0].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' }
])
});
});
});

View File

@@ -8,6 +8,7 @@ import {
tenantFactory,
loginRes
} from '~/dbInit';
import { iteratee } from 'lodash';
let creditAccount;
let debitAccount;
@@ -436,446 +437,6 @@ describe('routes: `/financial_statements`', () => {
});
});
describe('routes: `financial_statements/balance_sheet`', () => {
it('Should response unauthorzied in case the user was not authorized.', async () => {
const res = await request()
.get('/api/financial_statements/balance_sheet')
.send();
expect(res.status).equals(401);
});
it('Should retrieve query of the balance sheet with default values.', async () => {
const res = await request()
.get('/api/financial_statements/balance_sheet')
.set('x-access-token', loginRes.body.token)
.set('organization-id', tenantWebsite.organizationId)
.query({
display_columns_by: 'year',
from_date: '2020-01-01',
to_date: '2020-02-01',
})
.send();
expect(res.body.query.display_columns_by).equals('year');
expect(res.body.query.from_date).equals('2020-01-01');
expect(res.body.query.to_date).equals('2020-02-01');
expect(res.body.query.number_format.no_cents).equals(false);
expect(res.body.query.number_format.divide_1000).equals(false);
expect(res.body.query.none_zero).equals(false);
});
it('Should retrieve the asset accounts balance.', async () => {
const res = await request()
.get('/api/financial_statements/balance_sheet')
.set('x-access-token', loginRes.body.token)
.set('organization-id', tenantWebsite.organizationId)
.query({
display_columns_by: 'year',
})
.send();
expect(res.body.accounts[0].children).to.be.a('array');
expect(res.body.accounts[1].children).to.be.a('array');
expect(res.body.accounts[0].children.length).is.not.equals(0);
expect(res.body.accounts[1].children.length).is.not.equals(0);
});
it('Should retrieve assets/liabilities total balance between the given date range.', async () => {
const res = await request()
.get('/api/financial_statements/balance_sheet')
.set('x-access-token', loginRes.body.token)
.set('organization-id', tenantWebsite.organizationId)
.query({
display_columns_type: 'total',
from_date: '2012-01-01',
to_date: '2032-02-02',
})
.send();
expect(res.body.accounts[0].children).include.something.deep.equals({
id: 1001,
index: null,
name: debitAccount.name,
code: debitAccount.code,
parentAccountId: null,
children: [],
total: { formatted_amount: 5000, amount: 5000, date: '2032-02-02' }
});
expect(res.body.accounts[1].children).include.something.deep.equals({
id: 1000,
index: null,
name: creditAccount.name,
code: creditAccount.code,
parentAccountId: null,
children: [],
total: { formatted_amount: 4000, amount: 4000, date: '2032-02-02' }
});
});
it('Should retrieve asset/liabilities balance sheet with display columns by `year`.', async () => {
const res = await request()
.get('/api/financial_statements/balance_sheet')
.set('x-access-token', loginRes.body.token)
.set('organization-id', tenantWebsite.organizationId)
.query({
display_columns_by: 'year',
display_columns_type: 'date_periods',
from_date: '2012-01-01',
to_date: '2018-02-02',
})
.send();
expect(res.body.accounts[0].children[0].total_periods.length).equals(7);
expect(res.body.accounts[1].children[0].total_periods.length).equals(7);
expect(res.body.accounts[0].children[0].total_periods).deep.equals([
{
amount: 0,
formatted_amount: 0,
date: '2012',
},
{
amount: 0,
formatted_amount: 0,
date: '2013',
},
{
amount: 0,
formatted_amount: 0,
date: '2014',
},
{
amount: 0,
formatted_amount: 0,
date: '2015',
},
{
amount: 0,
formatted_amount: 0,
date: '2016',
},
{
amount: 0,
formatted_amount: 0,
date: '2017',
},
{
amount: 0,
formatted_amount: 0,
date: '2018',
},
]);
});
it('Should retrieve balance sheet with display columns by `day`.', async () => {
const res = await request()
.get('/api/financial_statements/balance_sheet')
.set('x-access-token', loginRes.body.token)
.set('organization-id', tenantWebsite.organizationId)
.query({
display_columns_by: 'day',
display_columns_type: 'date_periods',
from_date: '2020-01-08',
to_date: '2020-01-12',
})
.send();
expect(res.body.accounts[0].children).include.something.deep.equals({
id: debitAccount.id,
index: debitAccount.index,
name: debitAccount.name,
code: debitAccount.code,
parentAccountId: null,
children: [],
total_periods: [
{ date: '2020-01-08', formatted_amount: 0, amount: 0 },
{ date: '2020-01-09', formatted_amount: 0, amount: 0 },
{ date: '2020-01-10', formatted_amount: 5000, amount: 5000 },
{ date: '2020-01-11', formatted_amount: 5000, amount: 5000 },
{ date: '2020-01-12', formatted_amount: 5000, amount: 5000 },
],
total: { formatted_amount: 5000, amount: 5000, date: '2020-01-12' }
});
expect(res.body.accounts[1].children).include.something.deep.equals({
id: creditAccount.id,
index: creditAccount.index,
name: creditAccount.name,
code: creditAccount.code,
parentAccountId: null,
children: [],
total_periods: [
{ date: '2020-01-08', formatted_amount: 0, amount: 0 },
{ date: '2020-01-09', formatted_amount: 0, amount: 0 },
{ date: '2020-01-10', formatted_amount: 4000, amount: 4000 },
{ date: '2020-01-11', formatted_amount: 4000, amount: 4000 },
{ date: '2020-01-12', formatted_amount: 4000, amount: 4000 }
],
total: { formatted_amount: 4000, amount: 4000, date: '2020-01-12' }
});
});
it('Should retrieve the balance sheet with display columns by `month`.', async () => {
const res = await request()
.get('/api/financial_statements/balance_sheet')
.set('x-access-token', loginRes.body.token)
.set('organization-id', tenantWebsite.organizationId)
.query({
display_columns_by: 'month',
display_columns_type: 'date_periods',
from_date: '2019-07-01',
to_date: '2020-06-30',
})
.send();
expect(res.body.accounts[0].children).include.something.deep.equals({
id: debitAccount.id,
index: debitAccount.index,
name: debitAccount.name,
code: debitAccount.code,
parentAccountId: null,
children: [],
total_periods: [
{ date: '2019-07', formatted_amount: 0, amount: 0 },
{ date: '2019-08', formatted_amount: 0, amount: 0 },
{ date: '2019-09', formatted_amount: 0, amount: 0 },
{ date: '2019-10', formatted_amount: 0, amount: 0 },
{ date: '2019-11', formatted_amount: 0, amount: 0 },
{ date: '2019-12', formatted_amount: 0, amount: 0 },
{ date: '2020-01', formatted_amount: 5000, amount: 5000 },
{ date: '2020-02', formatted_amount: 5000, amount: 5000 },
{ date: '2020-03', formatted_amount: 5000, amount: 5000 },
{ date: '2020-04', formatted_amount: 5000, amount: 5000 },
{ date: '2020-05', formatted_amount: 5000, amount: 5000 },
{ date: '2020-06', formatted_amount: 5000, amount: 5000 },
],
total: { formatted_amount: 5000, amount: 5000, date: '2020-06-30' }
});
});
it('Should retrieve the balance sheet with display columns `quarter`.', async () => {
const res = await request()
.get('/api/financial_statements/balance_sheet')
.set('x-access-token', loginRes.body.token)
.set('organization-id', tenantWebsite.organizationId)
.query({
display_columns_by: 'quarter',
display_columns_type: 'date_periods',
from_date: '2020-01-01',
to_date: '2020-12-31',
})
.send();
expect(res.body.accounts[0].children).include.something.deep.equals({
id: debitAccount.id,
index: debitAccount.index,
name: debitAccount.name,
code: debitAccount.code,
parentAccountId: null,
children: [],
total_periods: [
{ date: '2020-03', formatted_amount: 5000, amount: 5000 },
{ date: '2020-06', formatted_amount: 5000, amount: 5000 },
{ date: '2020-09', formatted_amount: 5000, amount: 5000 },
{ date: '2020-12', formatted_amount: 5000, amount: 5000 },
],
total: { formatted_amount: 5000, amount: 5000, date: '2020-12-31' },
});
});
it('Should retrieve the balance sheet amounts without cents.', async () => {
await tenantFactory.create('account_transaction', {
debit: 0.25, credit: 0, account_id: debitAccount.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({
display_columns_by: 'quarter',
display_columns_type: 'date_periods',
from_date: '2020-01-01',
to_date: '2020-12-31',
number_format: {
no_cents: true,
},
})
.send();
expect(res.body.accounts[0].children).include.something.deep.equals({
id: debitAccount.id,
index: debitAccount.index,
name: debitAccount.name,
code: debitAccount.code,
parentAccountId: null,
children: [],
total_periods: [
{ date: '2020-03', formatted_amount: 5000, amount: 5000.25 },
{ date: '2020-06', formatted_amount: 5000, amount: 5000.25 },
{ date: '2020-09', formatted_amount: 5000, amount: 5000.25 },
{ date: '2020-12', formatted_amount: 5000, amount: 5000.25 },
],
total: { formatted_amount: 5000, amount: 5000.25, date: '2020-12-31' },
});
});
it('Should retrieve the balance sheet amounts divided on 1000.', async () => {
const res = await request()
.get('/api/financial_statements/balance_sheet')
.set('x-access-token', loginRes.body.token)
.set('organization-id', tenantWebsite.organizationId)
.query({
display_columns_by: 'quarter',
display_columns_type: 'date_periods',
from_date: '2020',
to_date: '2021',
number_format: {
divide_1000: true,
},
})
.send();
expect(res.body.accounts[0].children).include.something.deep.equals({
id: debitAccount.id,
index: debitAccount.index,
name: debitAccount.name,
code: debitAccount.code,
parentAccountId: null,
children: [],
total_periods: [
{ date: '2020-03', formatted_amount: 5, amount: 5000 },
{ date: '2020-06', formatted_amount: 5, amount: 5000 },
{ date: '2020-09', formatted_amount: 5, amount: 5000 },
{ date: '2020-12', formatted_amount: 5, amount: 5000 },
{ date: '2021-03', formatted_amount: 5, amount: 5000 },
],
total: { formatted_amount: 5, amount: 5000, date: '2021' },
});
});
it('Should not retrieve accounts has no transactions between the given date range in case query none_zero is true.', async () => {
const res = await request()
.get('/api/financial_statements/balance_sheet')
.set('x-access-token', loginRes.body.token)
.set('organization-id', tenantWebsite.organizationId)
.query({
display_columns_by: 'quarter',
from_date: '2002',
to_date: '2003',
number_format: {
divide_1000: true,
},
none_zero: true,
})
.send();
expect(res.body.accounts[0].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`', () => {
it('Should response unauthorized in case the user was not authorized.', async () => {
const res = await request()