mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-18 05:40:31 +00:00
235 lines
8.4 KiB
JavaScript
235 lines
8.4 KiB
JavaScript
import express from 'express';
|
|
import { query, oneOf, validationResult } from 'express-validator';
|
|
import moment from 'moment';
|
|
import { pick } from 'lodash';
|
|
import JournalPoster from '@/services/Accounting/JournalPoster';
|
|
import { dateRangeCollection } from '@/utils';
|
|
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
|
import { formatNumberClosure } from './FinancialStatementMixin';
|
|
import DependencyGraph from '@/lib/DependencyGraph';
|
|
|
|
export default {
|
|
/**
|
|
* Router constructor.
|
|
*/
|
|
router() {
|
|
const router = express.Router();
|
|
|
|
router.get('/',
|
|
this.profitLossSheet.validation,
|
|
asyncMiddleware(this.profitLossSheet.handler));
|
|
|
|
return router;
|
|
},
|
|
|
|
/**
|
|
* Retrieve profit/loss financial statement.
|
|
*/
|
|
profitLossSheet: {
|
|
validation: [
|
|
query('basis').optional(),
|
|
query('from_date').optional().isISO8601(),
|
|
query('to_date').optional().isISO8601(),
|
|
query('number_format.no_cents').optional().isBoolean(),
|
|
query('number_format.divide_1000').optional().isBoolean(),
|
|
query('basis').optional(),
|
|
query('none_zero').optional().isBoolean().toBoolean(),
|
|
query('account_ids').isArray().optional(),
|
|
query('account_ids.*').isNumeric().toInt(),
|
|
query('display_columns_type').optional().isIn([
|
|
'total', 'date_periods',
|
|
]),
|
|
query('display_columns_by').optional({ nullable: true, checkFalsy: true })
|
|
.isIn(['year', 'month', 'week', 'day', 'quarter']),
|
|
],
|
|
async handler(req, res) {
|
|
const validationErrors = validationResult(req);
|
|
|
|
if (!validationErrors.isEmpty()) {
|
|
return res.boom.badData(null, {
|
|
code: 'validation_error', ...validationErrors,
|
|
});
|
|
}
|
|
const { Account, AccountType } = req.models;
|
|
const filter = {
|
|
from_date: moment().startOf('year').format('YYYY-MM-DD'),
|
|
to_date: moment().endOf('year').format('YYYY-MM-DD'),
|
|
number_format: {
|
|
no_cents: false,
|
|
divide_1000: false,
|
|
},
|
|
basis: 'accural',
|
|
none_zero: false,
|
|
display_columns_type: 'total',
|
|
display_columns_by: 'month',
|
|
account_ids: [],
|
|
...req.query,
|
|
};
|
|
if (!Array.isArray(filter.account_ids)) {
|
|
filter.account_ids = [filter.account_ids];
|
|
}
|
|
const incomeStatementTypes = await AccountType.query().where('income_sheet', true);
|
|
|
|
// Fetch all income accounts from storage.
|
|
const accounts = await Account.query()
|
|
// .remember('profit_loss_accounts')
|
|
.modify('filterAccounts', filter.account_ids)
|
|
.whereIn('account_type_id', incomeStatementTypes.map((t) => t.id))
|
|
.withGraphFetched('type')
|
|
.withGraphFetched('transactions');
|
|
|
|
// Accounts dependency graph.
|
|
const accountsGraph = DependencyGraph.fromArray(
|
|
accounts, { itemId: 'id', parentItemId: 'parentAccountId' }
|
|
);
|
|
|
|
// Filter all none zero accounts if it was enabled.
|
|
const filteredAccounts = accounts.filter((account) => (
|
|
account.transactions.length > 0 || !filter.none_zero
|
|
));
|
|
const journalEntriesCollected = Account.collectJournalEntries(accounts);
|
|
const journalEntries = new JournalPoster(accountsGraph);
|
|
journalEntries.loadEntries(journalEntriesCollected);
|
|
|
|
// Account balance formmatter based on the given query.
|
|
const numberFormatter = formatNumberClosure(filter.number_format);
|
|
const comparatorDateType = filter.display_columns_type === 'total'
|
|
? 'day' : filter.display_columns_by;
|
|
|
|
// Gets the date range set from start to end date.
|
|
const dateRangeSet = dateRangeCollection(
|
|
filter.from_date,
|
|
filter.to_date,
|
|
comparatorDateType,
|
|
);
|
|
|
|
const accountsMapper = (incomeExpenseAccounts) => (
|
|
incomeExpenseAccounts.map((account) => ({
|
|
...pick(account, ['id', 'index', 'name', 'code', 'parentAccountId']),
|
|
|
|
// Total closing balance of the account.
|
|
...(filter.display_columns_type === 'total') && {
|
|
total: (() => {
|
|
const amount = journalEntries.getAccountBalance(account.id, filter.to_date);
|
|
return { amount, date: filter.to_date, formatted_amount: numberFormatter(amount) };
|
|
})(),
|
|
},
|
|
// Date periods when display columns type `periods`.
|
|
...(filter.display_columns_type === 'date_periods') && {
|
|
periods: dateRangeSet.map((date) => {
|
|
const type = comparatorDateType;
|
|
const amount = journalEntries.getAccountBalance(account.id, date, type);
|
|
|
|
return { date, amount, formatted_amount: numberFormatter(amount) };
|
|
}),
|
|
},
|
|
})));
|
|
|
|
const totalAccountsReducer = (incomeExpenseAccounts) => (
|
|
incomeExpenseAccounts.reduce((acc, account) => {
|
|
const amount = (account) ? account.total.amount : 0;
|
|
return amount + acc;
|
|
}, 0));
|
|
|
|
const accountsIncome = Account.toNestedArray(accountsMapper(filteredAccounts
|
|
.filter((account) => account.type.normal === 'credit')));
|
|
|
|
const accountsExpenses = Account.toNestedArray(accountsMapper(filteredAccounts
|
|
.filter((account) => account.type.normal === 'debit')));
|
|
|
|
// @return {Array}
|
|
const totalPeriodsMapper = (incomeExpenseAccounts) => (
|
|
Object.values(dateRangeSet.reduce((acc, date, index) => {
|
|
let amount = 0;
|
|
|
|
incomeExpenseAccounts.forEach((account) => {
|
|
const currentDate = account.periods[index];
|
|
amount += currentDate.amount || 0;
|
|
});
|
|
acc[date] = { date, amount, formatted_amount: numberFormatter(amount) };
|
|
return acc;
|
|
}, {})));
|
|
|
|
// Total income(date) - Total expenses(date) = Net income(date)
|
|
// @return {Array}
|
|
const netIncomePeriodsMapper = (totalIncomeAcocunts, totalExpenseAccounts) => (
|
|
dateRangeSet.map((date, index) => {
|
|
const totalIncome = totalIncomeAcocunts[index];
|
|
const totalExpenses = totalExpenseAccounts[index];
|
|
|
|
let amount = totalIncome.amount || 0;
|
|
amount -= totalExpenses.amount || 0;
|
|
return { date, amount, formatted_amount: numberFormatter(amount) };
|
|
}));
|
|
|
|
// @return {Object}
|
|
const netIncomeTotal = (totalIncome, totalExpenses) => {
|
|
const netIncomeAmount = totalIncome.amount - totalExpenses.amount;
|
|
return { amount: netIncomeAmount, formatted_amount: netIncomeAmount, date: filter.to_date };
|
|
};
|
|
|
|
const incomeResponse = {
|
|
entry_normal: 'credit',
|
|
accounts: accountsIncome,
|
|
...(filter.display_columns_type === 'total') && (() => {
|
|
const totalIncomeAccounts = totalAccountsReducer(accountsIncome);
|
|
return {
|
|
total: {
|
|
amount: totalIncomeAccounts,
|
|
date: filter.to_date,
|
|
formatted_amount: numberFormatter(totalIncomeAccounts),
|
|
},
|
|
};
|
|
})(),
|
|
...(filter.display_columns_type === 'date_periods') && {
|
|
total_periods: [
|
|
...totalPeriodsMapper(accountsIncome),
|
|
],
|
|
},
|
|
};
|
|
const expenseResponse = {
|
|
entry_normal: 'debit',
|
|
accounts: accountsExpenses,
|
|
...(filter.display_columns_type === 'total') && (() => {
|
|
const totalExpensesAccounts = totalAccountsReducer(accountsExpenses);
|
|
return {
|
|
total: {
|
|
amount: totalExpensesAccounts,
|
|
date: filter.to_date,
|
|
formatted_amount: numberFormatter(totalExpensesAccounts),
|
|
},
|
|
};
|
|
})(),
|
|
...(filter.display_columns_type === 'date_periods') && {
|
|
total_periods: [
|
|
...totalPeriodsMapper(accountsExpenses),
|
|
],
|
|
},
|
|
};
|
|
const netIncomeResponse = {
|
|
...(filter.display_columns_type === 'total') && {
|
|
total: {
|
|
...netIncomeTotal(incomeResponse.total, expenseResponse.total),
|
|
},
|
|
},
|
|
...(filter.display_columns_type === 'date_periods') && {
|
|
total_periods: [
|
|
...netIncomePeriodsMapper(
|
|
incomeResponse.total_periods,
|
|
expenseResponse.total_periods,
|
|
),
|
|
],
|
|
},
|
|
};
|
|
return res.status(200).send({
|
|
query: { ...filter },
|
|
columns: [...dateRangeSet],
|
|
profitLoss: {
|
|
income: incomeResponse,
|
|
expenses: expenseResponse,
|
|
net_income: netIncomeResponse,
|
|
},
|
|
});
|
|
},
|
|
},
|
|
} |