mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-19 06:10:31 +00:00
feat: balance sheet report.
feat: trial balance sheet. feat: general ledger report. feat: journal report. feat: profit/loss report.
This commit is contained in:
@@ -95,7 +95,7 @@ export const fetchProfitLossSheet = ({ query }) => {
|
||||
ApiService.get('/financial_statements/profit_loss_sheet', { params: query }).then((response) => {
|
||||
dispatch({
|
||||
type: t.PROFIT_LOSS_SHEET_SET,
|
||||
profitLoss: response.data.profitLoss,
|
||||
profitLoss: response.data.data,
|
||||
columns: response.data.columns,
|
||||
query: response.data.query,
|
||||
});
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
import { omit } from 'lodash';
|
||||
|
||||
export const mapBalanceSheetToTableRows = (accounts) => {
|
||||
return accounts.map((account) => {
|
||||
const PRIMARY_SECTIONS = ['assets', 'liability', 'equity'];
|
||||
const rowTypes = [
|
||||
'total_row',
|
||||
...(PRIMARY_SECTIONS.indexOf(account.section_type) !== -1
|
||||
? ['total_assets']
|
||||
: []),
|
||||
];
|
||||
return {
|
||||
...account,
|
||||
children: mapBalanceSheetToTableRows([
|
||||
...(account.children ? account.children : []),
|
||||
...(account.total && account.children && account.children.length > 0
|
||||
? [
|
||||
{
|
||||
name: `Total ${account.name}`,
|
||||
row_types: rowTypes,
|
||||
total: { ...account.total },
|
||||
...(account.total_periods && {
|
||||
total_periods: account.total_periods,
|
||||
}),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export const journalToTableRowsMapper = (journal) => {
|
||||
return journal.reduce((rows, journal) => {
|
||||
journal.entries.forEach((entry, index) => {
|
||||
rows.push({
|
||||
...entry,
|
||||
rowType: index === 0 ? 'first_entry' : 'entry',
|
||||
});
|
||||
});
|
||||
rows.push({
|
||||
credit: journal.credit,
|
||||
debit: journal.debit,
|
||||
rowType: 'entries_total',
|
||||
});
|
||||
rows.push({
|
||||
rowType: 'space_entry',
|
||||
});
|
||||
return rows;
|
||||
}, []);
|
||||
};
|
||||
|
||||
|
||||
export const generalLedgerToTableRows = (accounts) => {
|
||||
return accounts.reduce((tableRows, account) => {
|
||||
const children = [];
|
||||
children.push({
|
||||
...account.opening,
|
||||
rowType: 'opening_balance',
|
||||
});
|
||||
account.transactions.map((transaction) => {
|
||||
children.push({
|
||||
...transaction,
|
||||
...omit(account, ['transactions']),
|
||||
rowType: 'transaction',
|
||||
});
|
||||
});
|
||||
children.push({
|
||||
...account.closing,
|
||||
rowType: 'closing_balance',
|
||||
});
|
||||
tableRows.push({
|
||||
...omit(account, ['transactions']),
|
||||
children,
|
||||
rowType: 'account_name',
|
||||
});
|
||||
return tableRows;
|
||||
}, []);
|
||||
};
|
||||
|
||||
export const profitLossToTableRowsMapper = (profitLoss) => {
|
||||
|
||||
return [
|
||||
{
|
||||
name: 'Income',
|
||||
total: profitLoss.income.total,
|
||||
children: [
|
||||
...profitLoss.income.accounts,
|
||||
{
|
||||
name: 'Total Income',
|
||||
total: profitLoss.income.total,
|
||||
total_periods: profitLoss.income.total_periods,
|
||||
rowTypes: ['income_total', 'section_total', 'total'],
|
||||
},
|
||||
],
|
||||
total_periods: profitLoss.income.total_periods,
|
||||
},
|
||||
{
|
||||
name: 'Cost of sales',
|
||||
total: profitLoss.cost_of_sales.total,
|
||||
children: [
|
||||
...profitLoss.cost_of_sales.accounts,
|
||||
{
|
||||
name: 'Total cost of sales',
|
||||
total: profitLoss.cost_of_sales.total,
|
||||
total_periods: profitLoss.cost_of_sales.total_periods,
|
||||
rowTypes: ['cogs_total', 'section_total', 'total'],
|
||||
},
|
||||
],
|
||||
total_periods: profitLoss.cost_of_sales.total_periods
|
||||
},
|
||||
{
|
||||
name: 'Gross profit',
|
||||
total: profitLoss.gross_profit.total,
|
||||
total_periods: profitLoss.gross_profit.total_periods,
|
||||
rowTypes: ['gross_total', 'section_total', 'total'],
|
||||
},
|
||||
{
|
||||
name: 'Expenses',
|
||||
total: profitLoss.expenses.total,
|
||||
children: [
|
||||
...profitLoss.expenses.accounts,
|
||||
{
|
||||
name: 'Total Expenses',
|
||||
total: profitLoss.expenses.total,
|
||||
total_periods: profitLoss.expenses.total_periods,
|
||||
rowTypes: ['expenses_total', 'section_total', 'total'],
|
||||
},
|
||||
],
|
||||
total_periods: profitLoss.expenses.total_periods,
|
||||
},
|
||||
{
|
||||
name: 'Net Operating income',
|
||||
total: profitLoss.operating_profit.total,
|
||||
total_periods: profitLoss.income.total_periods,
|
||||
rowTypes: ['net_operating_total', 'section_total', 'total'],
|
||||
},
|
||||
{
|
||||
name: 'Other expenses',
|
||||
total: profitLoss.other_expenses.total,
|
||||
total_periods: profitLoss.other_expenses.total_periods,
|
||||
children: [
|
||||
...profitLoss.other_expenses.accounts,
|
||||
{
|
||||
name: 'Total other expenses',
|
||||
total: profitLoss.other_expenses.total,
|
||||
total_periods: profitLoss.other_expenses.total_periods,
|
||||
rowTypes: ['expenses_total', 'section_total', 'total'],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Net Income',
|
||||
total: profitLoss.net_income.total,
|
||||
total_periods: profitLoss.net_income.total_periods,
|
||||
rowTypes: ['net_income_total', 'section_total', 'total'],
|
||||
},
|
||||
];
|
||||
};
|
||||
@@ -1,36 +1,41 @@
|
||||
import { createReducer } from '@reduxjs/toolkit';
|
||||
import t from 'store/types';
|
||||
import { getFinancialSheetIndexByQuery } from './financialStatements.selectors';
|
||||
import { omit } from 'lodash';
|
||||
import {
|
||||
mapBalanceSheetToTableRows,
|
||||
journalToTableRowsMapper,
|
||||
generalLedgerToTableRows,
|
||||
profitLossToTableRowsMapper
|
||||
} from './financialStatements.mappers';
|
||||
|
||||
const initialState = {
|
||||
balanceSheet: {
|
||||
sheets: [],
|
||||
sheet: {},
|
||||
loading: true,
|
||||
filter: true,
|
||||
refresh: false,
|
||||
},
|
||||
trialBalance: {
|
||||
sheets: [],
|
||||
sheet: {},
|
||||
loading: true,
|
||||
filter: true,
|
||||
refresh: false,
|
||||
},
|
||||
generalLedger: {
|
||||
sheets: [],
|
||||
sheet: {},
|
||||
loading: false,
|
||||
filter: true,
|
||||
refresh: false,
|
||||
},
|
||||
journal: {
|
||||
sheets: [],
|
||||
sheet: {},
|
||||
loading: false,
|
||||
tableRows: [],
|
||||
filter: true,
|
||||
refresh: true,
|
||||
},
|
||||
profitLoss: {
|
||||
sheets: [],
|
||||
sheet: {},
|
||||
loading: true,
|
||||
tableRows: [],
|
||||
filter: true,
|
||||
@@ -44,52 +49,8 @@ const initialState = {
|
||||
},
|
||||
};
|
||||
|
||||
const mapGeneralLedgerAccountsToRows = (accounts) => {
|
||||
return accounts.reduce((tableRows, account) => {
|
||||
const children = [];
|
||||
children.push({
|
||||
...account.opening,
|
||||
rowType: 'opening_balance',
|
||||
});
|
||||
account.transactions.map((transaction) => {
|
||||
children.push({
|
||||
...transaction,
|
||||
...omit(account, ['transactions']),
|
||||
rowType: 'transaction',
|
||||
});
|
||||
});
|
||||
children.push({
|
||||
...account.closing,
|
||||
rowType: 'closing_balance',
|
||||
});
|
||||
tableRows.push({
|
||||
...omit(account, ['transactions']),
|
||||
children,
|
||||
rowType: 'account_name',
|
||||
});
|
||||
return tableRows;
|
||||
}, []);
|
||||
};
|
||||
|
||||
const mapJournalTableRows = (journal) => {
|
||||
return journal.reduce((rows, journal) => {
|
||||
journal.entries.forEach((entry, index) => {
|
||||
rows.push({
|
||||
...entry,
|
||||
rowType: index === 0 ? 'first_entry' : 'entry',
|
||||
});
|
||||
});
|
||||
rows.push({
|
||||
credit: journal.credit,
|
||||
debit: journal.debit,
|
||||
rowType: 'entries_total',
|
||||
});
|
||||
rows.push({
|
||||
rowType: 'space_entry',
|
||||
});
|
||||
return rows;
|
||||
}, []);
|
||||
};
|
||||
|
||||
|
||||
const mapContactAgingSummary = (sheet) => {
|
||||
const rows = [];
|
||||
@@ -120,70 +81,6 @@ const mapContactAgingSummary = (sheet) => {
|
||||
return rows;
|
||||
};
|
||||
|
||||
const mapProfitLossToTableRows = (profitLoss) => {
|
||||
return [
|
||||
{
|
||||
name: 'Income',
|
||||
total: profitLoss.income.total,
|
||||
children: [
|
||||
...profitLoss.income.accounts,
|
||||
{
|
||||
name: 'Total Income',
|
||||
total: profitLoss.income.total,
|
||||
rowType: 'income_total',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Expenses',
|
||||
total: profitLoss.expenses.total,
|
||||
children: [
|
||||
...profitLoss.expenses.accounts,
|
||||
{
|
||||
name: 'Total Expenses',
|
||||
total: profitLoss.expenses.total,
|
||||
rowType: 'expense_total',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Net Income',
|
||||
total: profitLoss.net_income.total,
|
||||
rowType: 'net_income',
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const mapTotalToChildrenRows = (accounts) => {
|
||||
return accounts.map((account) => {
|
||||
return {
|
||||
...account,
|
||||
children: mapTotalToChildrenRows([
|
||||
...(account.children ? account.children : []),
|
||||
...(account.total &&
|
||||
account.children &&
|
||||
account.children.length > 0 &&
|
||||
account.row_type !== 'total_row'
|
||||
? [
|
||||
{
|
||||
name: `Total ${account.name}`,
|
||||
row_type: 'total_row',
|
||||
total: { ...account.total },
|
||||
...(account.total_periods && {
|
||||
total_periods: account.total_periods,
|
||||
}),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const mapBalanceSheetRows = (balanceSheet) => {
|
||||
return balanceSheet.map((section) => {});
|
||||
};
|
||||
|
||||
const financialStatementFilterToggle = (financialName, statePath) => {
|
||||
return {
|
||||
[`${financialName}_FILTER_TOGGLE`]: (state, action) => {
|
||||
@@ -194,22 +91,13 @@ const financialStatementFilterToggle = (financialName, statePath) => {
|
||||
|
||||
export default createReducer(initialState, {
|
||||
[t.BALANCE_SHEET_STATEMENT_SET]: (state, action) => {
|
||||
const index = getFinancialSheetIndexByQuery(
|
||||
state.balanceSheet.sheets,
|
||||
action.query,
|
||||
);
|
||||
|
||||
const balanceSheet = {
|
||||
sheet: action.data.balanceSheet,
|
||||
columns: Object.values(action.data.columns),
|
||||
sheet: action.data.data,
|
||||
columns: action.data.columns,
|
||||
query: action.data.query,
|
||||
tableRows: mapTotalToChildrenRows(action.data.balance_sheet),
|
||||
tableRows: mapBalanceSheetToTableRows(action.data.data),
|
||||
};
|
||||
if (index !== -1) {
|
||||
state.balanceSheet.sheets[index] = balanceSheet;
|
||||
} else {
|
||||
state.balanceSheet.sheets.push(balanceSheet);
|
||||
}
|
||||
state.balanceSheet.sheet = balanceSheet;
|
||||
},
|
||||
|
||||
[t.BALANCE_SHEET_LOADING]: (state, action) => {
|
||||
@@ -224,19 +112,11 @@ export default createReducer(initialState, {
|
||||
...financialStatementFilterToggle('BALANCE_SHEET', 'balanceSheet'),
|
||||
|
||||
[t.TRAIL_BALANCE_STATEMENT_SET]: (state, action) => {
|
||||
const index = getFinancialSheetIndexByQuery(
|
||||
state.trialBalance.sheets,
|
||||
action.query,
|
||||
);
|
||||
const trailBalanceSheet = {
|
||||
accounts: action.data.accounts,
|
||||
data: action.data.data,
|
||||
query: action.data.query,
|
||||
};
|
||||
if (index !== -1) {
|
||||
state.trialBalance.sheets[index] = trailBalanceSheet;
|
||||
} else {
|
||||
state.trialBalance.sheets.push(trailBalanceSheet);
|
||||
}
|
||||
state.trialBalance.sheet = trailBalanceSheet;
|
||||
},
|
||||
|
||||
[t.TRIAL_BALANCE_SHEET_LOADING]: (state, action) => {
|
||||
@@ -251,21 +131,12 @@ export default createReducer(initialState, {
|
||||
...financialStatementFilterToggle('TRIAL_BALANCE', 'trialBalance'),
|
||||
|
||||
[t.JOURNAL_SHEET_SET]: (state, action) => {
|
||||
const index = getFinancialSheetIndexByQuery(
|
||||
state.journal.sheets,
|
||||
action.query,
|
||||
);
|
||||
|
||||
const journal = {
|
||||
query: action.data.query,
|
||||
journal: action.data.journal,
|
||||
tableRows: mapJournalTableRows(action.data.journal),
|
||||
data: action.data.data,
|
||||
tableRows: journalToTableRowsMapper(action.data.data),
|
||||
};
|
||||
if (index !== -1) {
|
||||
state.journal.sheets[index] = journal;
|
||||
} else {
|
||||
state.journal.sheets.push(journal);
|
||||
}
|
||||
state.journal.sheet = journal;
|
||||
},
|
||||
|
||||
[t.JOURNAL_SHEET_LOADING]: (state, action) => {
|
||||
@@ -278,21 +149,12 @@ export default createReducer(initialState, {
|
||||
...financialStatementFilterToggle('JOURNAL', 'journal'),
|
||||
|
||||
[t.GENERAL_LEDGER_STATEMENT_SET]: (state, action) => {
|
||||
const index = getFinancialSheetIndexByQuery(
|
||||
state.generalLedger.sheets,
|
||||
action.query,
|
||||
);
|
||||
|
||||
const generalLedger = {
|
||||
query: action.data.query,
|
||||
accounts: action.data.accounts,
|
||||
tableRows: mapGeneralLedgerAccountsToRows(action.data.accounts),
|
||||
accounts: action.data.data,
|
||||
tableRows: generalLedgerToTableRows(action.data.data),
|
||||
};
|
||||
if (index !== -1) {
|
||||
state.generalLedger.sheets[index] = generalLedger;
|
||||
} else {
|
||||
state.generalLedger.sheets.push(generalLedger);
|
||||
}
|
||||
state.generalLedger.sheet = generalLedger;
|
||||
},
|
||||
|
||||
[t.GENERAL_LEDGER_SHEET_LOADING]: (state, action) => {
|
||||
@@ -305,22 +167,13 @@ export default createReducer(initialState, {
|
||||
...financialStatementFilterToggle('GENERAL_LEDGER', 'generalLedger'),
|
||||
|
||||
[t.PROFIT_LOSS_SHEET_SET]: (state, action) => {
|
||||
const index = getFinancialSheetIndexByQuery(
|
||||
state.profitLoss.sheets,
|
||||
action.query,
|
||||
);
|
||||
|
||||
const profitLossSheet = {
|
||||
query: action.query,
|
||||
profitLoss: action.profitLoss,
|
||||
columns: action.columns,
|
||||
tableRows: mapProfitLossToTableRows(action.profitLoss),
|
||||
tableRows: profitLossToTableRowsMapper(action.profitLoss),
|
||||
};
|
||||
if (index !== -1) {
|
||||
state.profitLoss.sheets[index] = profitLossSheet;
|
||||
} else {
|
||||
state.profitLoss.sheets.push(profitLossSheet);
|
||||
}
|
||||
state.profitLoss.sheet = profitLossSheet;
|
||||
},
|
||||
|
||||
[t.PROFIT_LOSS_SHEET_LOADING]: (state, action) => {
|
||||
@@ -334,34 +187,34 @@ export default createReducer(initialState, {
|
||||
|
||||
...financialStatementFilterToggle('PROFIT_LOSS', 'profitLoss'),
|
||||
|
||||
[t.RECEIVABLE_AGING_SUMMARY_LOADING]: (state, action) => {
|
||||
const { loading } = action.payload;
|
||||
state.receivableAgingSummary.loading = loading;
|
||||
},
|
||||
// [t.RECEIVABLE_AGING_SUMMARY_LOADING]: (state, action) => {
|
||||
// const { loading } = action.payload;
|
||||
// state.receivableAgingSummary.loading = loading;
|
||||
// },
|
||||
|
||||
[t.RECEIVABLE_AGING_SUMMARY_SET]: (state, action) => {
|
||||
const { aging, columns, query } = action.payload;
|
||||
const index = getFinancialSheetIndexByQuery(
|
||||
state.receivableAgingSummary.sheets,
|
||||
query,
|
||||
);
|
||||
// [t.RECEIVABLE_AGING_SUMMARY_SET]: (state, action) => {
|
||||
// const { aging, columns, query } = action.payload;
|
||||
// const index = getFinancialSheetIndexByQuery(
|
||||
// state.receivableAgingSummary.sheets,
|
||||
// query,
|
||||
// );
|
||||
|
||||
const receivableSheet = {
|
||||
query,
|
||||
columns,
|
||||
aging,
|
||||
tableRows: mapContactAgingSummary(aging),
|
||||
};
|
||||
if (index !== -1) {
|
||||
state.receivableAgingSummary[index] = receivableSheet;
|
||||
} else {
|
||||
state.receivableAgingSummary.sheets.push(receivableSheet);
|
||||
}
|
||||
},
|
||||
[t.RECEIVABLE_AGING_SUMMARY_REFRESH]: (state, action) => {
|
||||
const { refresh } = action.payload;
|
||||
state.receivableAgingSummary.refresh = !!refresh;
|
||||
},
|
||||
// const receivableSheet = {
|
||||
// query,
|
||||
// columns,
|
||||
// aging,
|
||||
// tableRows: mapContactAgingSummary(aging),
|
||||
// };
|
||||
// if (index !== -1) {
|
||||
// state.receivableAgingSummary[index] = receivableSheet;
|
||||
// } else {
|
||||
// state.receivableAgingSummary.sheets.push(receivableSheet);
|
||||
// }
|
||||
// },
|
||||
// [t.RECEIVABLE_AGING_SUMMARY_REFRESH]: (state, action) => {
|
||||
// const { refresh } = action.payload;
|
||||
// state.receivableAgingSummary.refresh = !!refresh;
|
||||
// },
|
||||
...financialStatementFilterToggle(
|
||||
'RECEIVABLE_AGING_SUMMARY',
|
||||
'receivableAgingSummary',
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
import {getObjectDiff} from 'utils';
|
||||
import { createSelector } from 'reselect';
|
||||
import { camelCase } from 'lodash';
|
||||
|
||||
const transformSheetType = (sheetType) => {
|
||||
return camelCase(sheetType);
|
||||
};
|
||||
|
||||
// Financial Statements selectors.
|
||||
|
||||
/**
|
||||
* Retrieve financial statement sheet by the given query.
|
||||
* @param {array} sheets
|
||||
* @param {object} query
|
||||
*/
|
||||
export const getFinancialSheetIndexByQuery = (sheets, query) => {
|
||||
return sheets.findIndex(balanceSheet => (
|
||||
getObjectDiff(query, balanceSheet.query).length === 0
|
||||
));
|
||||
export const sheetByTypeSelector = (sheetType) => (state, props) => {
|
||||
const sheetName = transformSheetType(sheetType);
|
||||
return state.financialStatements[sheetName].sheet;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -19,38 +16,56 @@ export const getFinancialSheetIndexByQuery = (sheets, query) => {
|
||||
* @param {array} sheets
|
||||
* @param {number} index
|
||||
*/
|
||||
export const getFinancialSheet = (sheets, index) => {
|
||||
return (typeof sheets[index] !== 'undefined') ? sheets[index] : null;
|
||||
};
|
||||
export const getFinancialSheetFactory = (sheetType) =>
|
||||
createSelector(
|
||||
sheetByTypeSelector(sheetType),
|
||||
(sheet) => {
|
||||
return sheet;
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* Retrieve financial statement columns by the given sheet index.
|
||||
* @param {array} sheets
|
||||
* @param {number} index
|
||||
*/
|
||||
export const getFinancialSheetColumns = (sheets, index) => {
|
||||
const sheet = getFinancialSheet(sheets, index);
|
||||
return (sheet && sheet.columns) ? sheet.columns : [];
|
||||
};
|
||||
export const getFinancialSheetColumnsFactory = (sheetType) =>
|
||||
createSelector(
|
||||
sheetByTypeSelector(sheetType),
|
||||
(sheet) => {
|
||||
return (sheet && sheet.columns) ? sheet.columns : [];
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* Retrieve financial statement query by the given sheet index.
|
||||
* @param {array} sheets
|
||||
* @param {number} index
|
||||
*/
|
||||
export const getFinancialSheetQuery = (sheets, index) => {
|
||||
const sheet = getFinancialSheet(sheets, index);
|
||||
return (sheet && sheet.query) ? sheet.query : {};
|
||||
};
|
||||
export const getFinancialSheetQueryFactory = (sheetType) =>
|
||||
createSelector(
|
||||
sheetByTypeSelector(sheetType),
|
||||
(sheet) => {
|
||||
return (sheet && sheet.query) ? sheet.query : {};
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* Retrieve financial statement accounts by the given sheet index.
|
||||
*/
|
||||
export const getFinancialSheetAccountsFactory = (sheetType) =>
|
||||
createSelector(
|
||||
sheetByTypeSelector(sheetType),
|
||||
(sheet) => {
|
||||
return (sheet && sheet.accounts) ? sheet.accounts : [];
|
||||
}
|
||||
);
|
||||
|
||||
export const getFinancialSheetAccounts = (sheets, index) => {
|
||||
const sheet = getFinancialSheet(sheets, index);
|
||||
return (sheet && sheet.accounts) ? sheet.accounts : [];
|
||||
};
|
||||
|
||||
|
||||
export const getFinancialSheetTableRows = (sheets, index) => {
|
||||
const sheet = getFinancialSheet(sheets, index);
|
||||
return (sheet && sheet.tableRows) ? sheet.tableRows : [];
|
||||
};
|
||||
/**
|
||||
* Retrieve financial statement table rows by the given sheet index.
|
||||
*/
|
||||
export const getFinancialSheetTableRowsFactory = (sheetType) =>
|
||||
createSelector(
|
||||
sheetByTypeSelector(sheetType),
|
||||
(sheet) => {
|
||||
return (sheet && sheet.tableRows) ? sheet.tableRows : [];
|
||||
}
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user