- 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

@@ -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,
};