feat: balance sheet report.

feat: trial balance sheet.
feat: general ledger report.
feat: journal report.
feat: profit/loss report.
This commit is contained in:
a.bouhuolia
2020-12-30 20:39:17 +02:00
parent de9f6d9521
commit 7ae73ed6cd
62 changed files with 2403 additions and 1850 deletions

View File

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

View File

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

View File

@@ -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',

View File

@@ -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 : [];
}
);