mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-16 21:00:31 +00:00
- 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:
62
server/src/data/BalanceSheetStructure.js
Normal file
62
server/src/data/BalanceSheetStructure.js
Normal 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,
|
||||
},
|
||||
];
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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)],
|
||||
});
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
0
server/src/http/controllers/Sales/SalesEstimates.js
Normal file
0
server/src/http/controllers/Sales/SalesEstimates.js
Normal file
0
server/src/http/controllers/Sales/SalesInvoicing.js
Normal file
0
server/src/http/controllers/Sales/SalesInvoicing.js
Normal file
0
server/src/http/controllers/Sales/SalesReceipt.js
Normal file
0
server/src/http/controllers/Sales/SalesReceipt.js
Normal 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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user