From 1a897308556ce68c96b04a0925ed84712bdc9c38 Mon Sep 17 00:00:00 2001 From: "a.bouhuolia" Date: Thu, 21 Jan 2021 14:32:31 +0200 Subject: [PATCH] feat: journal and general ledger report. --- client/src/components/Dashboard/Dashboard.js | 4 +- client/src/components/DataTable.js | 20 +- client/src/components/Datatable/Cells.js | 5 + client/src/components/FinancialSheet.js | 24 +- client/src/components/index.js | 2 +- .../containers/Accounts/AccountsDataTable.js | 4 +- .../ARAgingSummary/ARAgingSummaryTable.js | 1 + .../BalanceSheet/BalanceSheetTable.js | 34 +-- .../FinancialAccountsFilter.js | 2 + .../GeneralLedger/GeneralLedger.js | 1 + .../GeneralLedgerHeaderGeneralPane.js | 14 +- .../GeneralLedger/GeneralLedgerTable.js | 119 +++++----- .../GeneralLedger/common.js | 16 ++ .../Journal/JournalTable.js | 65 +++--- .../ProfitLossSheet/ProfitLossSheetTable.js | 5 +- .../TrialBalanceSheetTable.js | 8 +- .../containers/FinancialStatements/common.js | 2 +- client/src/lang/en/index.js | 4 +- .../financialStatements.mappers.js | 218 +++++++++++------- .../style/components/DataTable/DataTable.scss | 19 ++ .../FinancialStatements/FinancialSheet.scss | 7 +- .../FinancialStatements/ARAgingSummary.scss | 28 +-- .../FinancialStatements/BalanceSheet.scss | 2 +- .../FinancialStatements/FinancialSheet.scss | 10 +- .../FinancialStatements/GeneralLedger.scss | 61 ++++- .../pages/FinancialStatements/Journal.scss | 26 ++- client/src/utils.js | 10 + .../FinancialStatements/JournalSheet.ts | 48 ++-- server/src/data/BalanceSheetStructure.ts | 56 +++-- .../src/database/seeds/data/accounts_types.js | 26 ++- server/src/interfaces/BalanceSheet.ts | 4 +- server/src/interfaces/GeneralLedgerSheet.ts | 17 +- server/src/interfaces/ProfitLossSheet.ts | 2 +- server/src/models/AccountType.js | 26 +-- .../src/services/Accounting/JournalPoster.ts | 7 + .../BalanceSheet/BalanceSheet.ts | 4 +- .../GeneralLedger/GeneralLedger.ts | 135 ++++++++--- .../GeneralLedger/GeneralLedgerService.ts | 8 + .../JournalSheet/JournalSheet.ts | 74 ++++-- .../JournalSheet/JournalSheetService.ts | 11 +- .../ProfitLossSheet/ProfitLossSheet.ts | 18 +- .../TrialBalanceSheet/TrialBalanceSheet.ts | 12 +- server/src/utils/index.js | 10 + 43 files changed, 797 insertions(+), 372 deletions(-) create mode 100644 client/src/components/Datatable/Cells.js create mode 100644 client/src/containers/FinancialStatements/GeneralLedger/common.js diff --git a/client/src/components/Dashboard/Dashboard.js b/client/src/components/Dashboard/Dashboard.js index 6ead8488c..c00bb07d4 100644 --- a/client/src/components/Dashboard/Dashboard.js +++ b/client/src/components/Dashboard/Dashboard.js @@ -2,6 +2,8 @@ import React from 'react'; import { Switch, Route } from 'react-router'; import { useQuery } from 'react-query'; +import 'style/pages/Dashboard/Dashboard.scss'; + import DashboardLoadingIndicator from './DashboardLoadingIndicator'; import Sidebar from 'components/Sidebar/Sidebar'; @@ -15,8 +17,6 @@ import withSettingsActions from 'containers/Settings/withSettingsActions'; import { compose } from 'utils'; -import 'style/pages/Dashboard/Dashboard.scss'; - /** * Dashboard page. */ diff --git a/client/src/components/DataTable.js b/client/src/components/DataTable.js index 6dd95ff0a..f808755d9 100644 --- a/client/src/components/DataTable.js +++ b/client/src/components/DataTable.js @@ -191,7 +191,7 @@ export default function DataTable({ // Renders table cell. const RenderCell = useCallback( - ({ row, cell, index }) => ( + ({ row, cell, column, index }) => ( ( @@ -199,6 +199,7 @@ export default function DataTable({ style={{ 'padding-left': `${row.depth * expandColumnSpace}rem`, }} + className={'expend-padding'} > {children} @@ -224,7 +225,14 @@ export default function DataTable({ /> - {cell.render('Cell')} + + ( + { children } + )}> + {cell.render('Cell')} + ), [expandable, expandToggleColumn, expandColumnSpace], @@ -276,7 +284,13 @@ export default function DataTable({ return (
diff --git a/client/src/components/Datatable/Cells.js b/client/src/components/Datatable/Cells.js new file mode 100644 index 000000000..8c5efd370 --- /dev/null +++ b/client/src/components/Datatable/Cells.js @@ -0,0 +1,5 @@ +import React from 'react'; + +export function CellTextSpan({ cell: { value } }) { + return ({ value }) +} diff --git a/client/src/components/FinancialSheet.js b/client/src/components/FinancialSheet.js index a46eafdf6..181747f35 100644 --- a/client/src/components/FinancialSheet.js +++ b/client/src/components/FinancialSheet.js @@ -3,9 +3,10 @@ import moment from 'moment'; import classnames from 'classnames'; import { FormattedMessage as T, useIntl } from 'react-intl'; +import 'style/pages/FinancialStatements/FinancialSheet.scss'; + import { If, LoadingIndicator, MODIFIER } from 'components'; -import 'style/pages/FinancialStatements/FinancialSheet.scss'; export default function FinancialSheet({ companyName, @@ -20,7 +21,8 @@ export default function FinancialSheet({ className, basis, minimal = false, - fullWidth = false + fullWidth = false, + currentDate = true, }) { const { formatMessage } = useIntl(); const format = 'DD MMMM YYYY'; @@ -84,11 +86,19 @@ export default function FinancialSheet({
{children}
{accountingBasis}
- {basisLabel && ( -
- {basisLabel} -
- )} +
); diff --git a/client/src/components/index.js b/client/src/components/index.js index a11744c50..2b1316f8b 100644 --- a/client/src/components/index.js +++ b/client/src/components/index.js @@ -91,5 +91,5 @@ export { InputPrependText, PageFormBigNumber, AccountsMultiSelect, - DataTableEditable + DataTableEditable, }; diff --git a/client/src/containers/Accounts/AccountsDataTable.js b/client/src/containers/Accounts/AccountsDataTable.js index 2b91620d2..14ab2c163 100644 --- a/client/src/containers/Accounts/AccountsDataTable.js +++ b/client/src/containers/Accounts/AccountsDataTable.js @@ -27,8 +27,6 @@ import withAccounts from 'containers/Accounts/withAccounts'; import withDialogActions from 'containers/Dialog/withDialogActions'; import withCurrentView from 'containers/Views/withCurrentView'; -import { accountNameAccessor } from './utils'; - function AccountsDataTable({ // #withDashboardActions accountsTable, @@ -136,7 +134,7 @@ function AccountsDataTable({ { id: 'name', Header: formatMessage({ id: 'account_name' }), - accessor: accountNameAccessor, + accessor: 'name', className: 'account_name', width: 220, }, diff --git a/client/src/containers/FinancialStatements/ARAgingSummary/ARAgingSummaryTable.js b/client/src/containers/FinancialStatements/ARAgingSummary/ARAgingSummaryTable.js index d3d74a593..88dc2f23f 100644 --- a/client/src/containers/FinancialStatements/ARAgingSummary/ARAgingSummaryTable.js +++ b/client/src/containers/FinancialStatements/ARAgingSummary/ARAgingSummaryTable.js @@ -38,6 +38,7 @@ function ReceivableAgingSummaryTable({ className: 'customer_name', sticky: 'left', width: 240, + textOverview: true, }, { Header: , diff --git a/client/src/containers/FinancialStatements/BalanceSheet/BalanceSheetTable.js b/client/src/containers/FinancialStatements/BalanceSheet/BalanceSheetTable.js index d1d14fac8..c3c2044d5 100644 --- a/client/src/containers/FinancialStatements/BalanceSheet/BalanceSheetTable.js +++ b/client/src/containers/FinancialStatements/BalanceSheet/BalanceSheetTable.js @@ -4,33 +4,12 @@ import classNames from 'classnames'; import FinancialSheet from 'components/FinancialSheet'; import DataTable from 'components/DataTable'; +import { CellTextSpan } from 'components/Datatable/Cells'; import withBalanceSheetDetail from './withBalanceSheetDetail'; import { compose, defaultExpanderReducer, getColumnWidth } from 'utils'; -// Total cell. -function TotalCell({ cell }) { - const row = cell.row.original; - - if (row.total) { - return row.total.formatted_amount; - } - return ''; -} - -// Total period cell. -const TotalPeriodCell = (index) => ({ cell }) => { - const { original } = cell.row; - - if (original.total_periods && original.total_periods[index]) { - const amount = original.total_periods[index].formatted_amount; - - return amount; - } - return ''; -}; - /** * Balance sheet table. */ @@ -52,14 +31,15 @@ function BalanceSheetTable({ Header: formatMessage({ id: 'account_name' }), accessor: (row) => (row.code ? `${row.name} - ${row.code}` : row.name), className: 'account_name', + textOverview: true, width: 240, }, ...(balanceSheetQuery.display_columns_type === 'total' ? [ { Header: formatMessage({ id: 'total' }), - accessor: 'balance.formatted_amount', - Cell: TotalCell, + accessor: 'total.formatted_amount', + Cell: CellTextSpan, className: 'total', width: 140, }, @@ -69,8 +49,8 @@ function BalanceSheetTable({ ? balanceSheetColumns.map((column, index) => ({ id: `date_period_${index}`, Header: column, - accessor: `total_periods[${index}]`, - Cell: TotalPeriodCell(index), + Cell: CellTextSpan, + accessor: `total_periods[${index}].formatted_amount`, className: classNames('total-period', `total-periods-${index}`), width: getColumnWidth( balanceSheetTableRows, @@ -93,7 +73,7 @@ function BalanceSheetTable({ const { original } = row; const rowTypes = Array.isArray(original.row_types) ? original.row_types - : []; + : [original.row_types]; return { ...rowTypes.reduce((acc, rowType) => { diff --git a/client/src/containers/FinancialStatements/FinancialAccountsFilter.js b/client/src/containers/FinancialStatements/FinancialAccountsFilter.js index 233865ad0..68c6da790 100644 --- a/client/src/containers/FinancialStatements/FinancialAccountsFilter.js +++ b/client/src/containers/FinancialStatements/FinancialAccountsFilter.js @@ -14,6 +14,8 @@ import { CLASSES } from 'common/classes'; import { Col, Row, ListSelect, MODIFIER } from 'components'; import { filterAccountsOptions } from './common'; + + export default function FinancialAccountsFilter({ ...restProps }) { const SUBMENU_POPOVER_MODIFIERS = { flip: { boundariesElement: 'viewport', padding: 20 }, diff --git a/client/src/containers/FinancialStatements/GeneralLedger/GeneralLedger.js b/client/src/containers/FinancialStatements/GeneralLedger/GeneralLedger.js index b36a53803..927582c62 100644 --- a/client/src/containers/FinancialStatements/GeneralLedger/GeneralLedger.js +++ b/client/src/containers/FinancialStatements/GeneralLedger/GeneralLedger.js @@ -49,6 +49,7 @@ function GeneralLedger({ fromDate: moment().startOf('year').format('YYYY-MM-DD'), toDate: moment().endOf('year').format('YYYY-MM-DD'), basis: 'accural', + accountsFilter: 'with-transactions', }); // Change page title of the dashboard. diff --git a/client/src/containers/FinancialStatements/GeneralLedger/GeneralLedgerHeaderGeneralPane.js b/client/src/containers/FinancialStatements/GeneralLedger/GeneralLedgerHeaderGeneralPane.js index b9d3cccc8..7f7f455d1 100644 --- a/client/src/containers/FinancialStatements/GeneralLedger/GeneralLedgerHeaderGeneralPane.js +++ b/client/src/containers/FinancialStatements/GeneralLedger/GeneralLedgerHeaderGeneralPane.js @@ -1,16 +1,21 @@ import React from 'react'; -import { FormGroup, Classes } from '@blueprintjs/core'; +import { + FormGroup, + Classes, +} from '@blueprintjs/core'; import { FormattedMessage as T } from 'react-intl'; import classNames from 'classnames'; +import { compose } from 'redux'; import { AccountsMultiSelect, Row, Col } from 'components'; import FinancialStatementDateRange from 'containers/FinancialStatements/FinancialStatementDateRange'; import RadiosAccountingBasis from '../RadiosAccountingBasis'; +import FinancialAccountsFilter from '../FinancialAccountsFilter'; import withAccounts from 'containers/Accounts/withAccounts'; -import { compose } from 'redux'; +import { filterAccountsOptions } from './common'; /** * General ledger (GL) - Header - General panel. @@ -22,7 +27,10 @@ function GeneralLedgerHeaderGeneralPane({ return (
- + { - switch (row.rowType) { - case ROW_TYPE.OPENING_BALANCE: - return 'Opening Balance'; - case ROW_TYPE.CLOSING_BALANCE: - return 'Closing Balance'; - default: - return row.name; - } - }; - - // Date accessor. - const dateAccessor = (row) => { - const TYPES = [ - ROW_TYPE.OPENING_BALANCE, - ROW_TYPE.CLOSING_BALANCE, - ROW_TYPE.TRANSACTION, - ]; - - return TYPES.indexOf(row.rowType) !== -1 - ? moment(row.date).format('DD MMM YYYY') - : ''; - }; - - // Amount cell - const amountCell = useCallback(({ cell }) => { - const transaction = cell.row.original; - - if (transaction.rowType === ROW_TYPE.ACCOUNT) { - return !cell.row.isExpanded ? ( - - ) : ( - '' - ); - } - return ; - }, []); - const columns = useMemo( () => [ { - Header: formatMessage({ id: 'account_name' }), - accessor: accountNameAccessor, - className: 'name', - width: 225, + Header: formatMessage({ id: 'date' }), + accessor: (row) => { + if (row.rowType === 'ACCOUNT_ROW') { + return ( + + {row.date} + + ); + } + return row.date; + }, + className: 'date', + width: 120, }, { - Header: formatMessage({ id: 'date' }), - accessor: dateAccessor, - className: 'date', - width: 115, + Header: formatMessage({ id: 'account_name' }), + accessor: 'name', + className: 'name', + textOverview: true, + // width: 200, }, { Header: formatMessage({ id: 'transaction_type' }), - accessor: 'referenceType', + accessor: 'reference_type_formatted', className: 'transaction_type', - width: 145, + width: 125 , }, { - Header: formatMessage({ id: 'trans_num' }), + Header: formatMessage({ id: 'transaction_number' }), accessor: 'reference_id', className: 'transaction_number', - width: 110, + width: 100, }, { Header: formatMessage({ id: 'description' }), accessor: 'note', className: 'description', - width: 145, + // width: 145, + }, + { + Header: formatMessage({ id: 'credit' }), + accessor: 'formatted_credit', + className: 'credit', + width: getColumnWidth(generalLedgerTableRows, 'formatted_credit', { + minWidth: 100, + magicSpacing: 10, + }), + }, + { + Header: formatMessage({ id: 'debit' }), + accessor: 'formatted_debit', + className: 'debit', + width: getColumnWidth(generalLedgerTableRows, 'formatted_debit', { + minWidth: 100, + magicSpacing: 10, + }), }, { Header: formatMessage({ id: 'amount' }), - Cell: amountCell, + accessor: 'formatted_amount', className: 'amount', - width: 150, + width: getColumnWidth(generalLedgerTableRows, 'formatted_amount', { + minWidth: 100, + magicSpacing: 10, + }), }, { - Header: formatMessage({ id: 'balance' }), - Cell: amountCell, - className: 'balance', - width: 150, + Header: formatMessage({ id: 'running_balance' }), + accessor: 'formatted_running_balance', + className: 'running_balance', + width: getColumnWidth(generalLedgerTableRows, 'formatted_running_balance', { + minWidth: 100, + magicSpacing: 10, + }), }, ], - [], + [formatMessage, generalLedgerTableRows], ); // Default expanded rows of general ledger table. @@ -140,7 +139,7 @@ function GeneralLedgerTable({ rowClassNames={rowClassNames} expanded={expandedRows} virtualizedRows={true} - fixedItemSize={37} + fixedItemSize={30} fixedSizeHeight={1000} expandable={true} expandToggleColumn={1} diff --git a/client/src/containers/FinancialStatements/GeneralLedger/common.js b/client/src/containers/FinancialStatements/GeneralLedger/common.js new file mode 100644 index 000000000..76c57d092 --- /dev/null +++ b/client/src/containers/FinancialStatements/GeneralLedger/common.js @@ -0,0 +1,16 @@ +import { formatMessage } from 'services/intl'; + +export const filterAccountsOptions = [ + { + key: 'all-accounts', + name: formatMessage({ id: 'all_accounts' }), + hint: formatMessage({ id: 'all_accounts_including_with_zero_balance' }), + }, + { + key: 'with-transactions', + name: formatMessage({ id: 'accounts_with_transactions' }), + hint: formatMessage({ + id: 'include_accounts_once_has_transactions_on_given_date_period', + }), + }, +]; diff --git a/client/src/containers/FinancialStatements/Journal/JournalTable.js b/client/src/containers/FinancialStatements/Journal/JournalTable.js index 93779678f..0c36b2661 100644 --- a/client/src/containers/FinancialStatements/Journal/JournalTable.js +++ b/client/src/containers/FinancialStatements/Journal/JournalTable.js @@ -8,7 +8,7 @@ import Money from 'components/Money'; import withJournal from './withJournal'; -import { compose, defaultExpanderReducer } from 'utils'; +import { compose, defaultExpanderReducer, getForceWidth } from 'utils'; function JournalSheetTable({ // #withJournal @@ -22,70 +22,52 @@ function JournalSheetTable({ }) { const { formatMessage } = useIntl(); - const rowTypeFilter = (rowType, value, types) => { - return types.indexOf(rowType) === -1 ? '' : value; - }; - - const exceptRowTypes = (rowType, value, types) => { - return types.indexOf(rowType) !== -1 ? '' : value; - }; - const columns = useMemo( () => [ { Header: formatMessage({ id: 'date' }), - accessor: (r) => - rowTypeFilter(r.rowType, moment(r.date).format('YYYY MMM DD'), [ - 'first_entry', - ]), + accessor: row => row.date ? moment(row.date).format('YYYY MMM DD') : '', className: 'date', - width: 85, + width: 100, }, { Header: formatMessage({ id: 'transaction_type' }), - accessor: (r) => - rowTypeFilter(r.rowType, r.transaction_type, ['first_entry']), + accessor: 'reference_type_formatted', className: 'reference_type_formatted', - width: 145, + width: 120, }, { Header: formatMessage({ id: 'num' }), - accessor: (r) => - rowTypeFilter(r.rowType, r.reference_id, ['first_entry']), + accessor: 'reference_id', className: 'reference_id', width: 70, }, { Header: formatMessage({ id: 'description' }), accessor: 'note', + className: 'note' }, { Header: formatMessage({ id: 'acc_code' }), - accessor: 'account.code', + accessor: 'account_code', width: 95, className: 'account_code', }, { Header: formatMessage({ id: 'account' }), - accessor: 'account.name', + accessor: 'account_name', + className: 'account_name', + textOverview: true, }, { Header: formatMessage({ id: 'credit' }), - accessor: (r) => - exceptRowTypes( - r.rowType, - , - ['space_entry'], - ), + accessor: 'formatted_credit', + className: 'credit' }, { Header: formatMessage({ id: 'debit' }), - accessor: (r) => - exceptRowTypes( - r.rowType, - , - ['space_entry'], - ), + accessor: 'formatted_debit', + className: 'debit' }, ], [formatMessage], @@ -101,6 +83,20 @@ function JournalSheetTable({ // Default expanded rows of general journal table. const expandedRows = useMemo(() => defaultExpanderReducer([], 1), []); + const rowClassNames = useCallback((row) => { + const { original } = row; + const rowTypes = Array.isArray(original.rowType) + ? original.rowType + : [original.rowType]; + + return { + ...rowTypes.reduce((acc, rowType) => { + acc[`row_type--${rowType}`] = rowType; + return acc; + }, {}), + }; + }, []); + return ( + > (row.code ? `${row.name} - ${row.code}` : row.name), className: 'name', + textOverview: true, width: 240, }, ...(profitLossQuery.display_columns_type === 'total' ? [ { Header: formatMessage({ id: 'total' }), + Cell: CellTextSpan, accessor: 'total.formatted_amount', className: 'total', width: 140, @@ -42,6 +44,7 @@ function ProfitLossSheetTable({ ? profitLossColumns.map((column, index) => ({ id: `date_period_${index}`, Header: column, + Cell: CellTextSpan, accessor: `total_periods[${index}].formatted_amount`, width: getColumnWidth( profitLossTableRows, diff --git a/client/src/containers/FinancialStatements/TrialBalanceSheet/TrialBalanceSheetTable.js b/client/src/containers/FinancialStatements/TrialBalanceSheet/TrialBalanceSheetTable.js index cf60323a0..73395aac3 100644 --- a/client/src/containers/FinancialStatements/TrialBalanceSheet/TrialBalanceSheetTable.js +++ b/client/src/containers/FinancialStatements/TrialBalanceSheet/TrialBalanceSheetTable.js @@ -3,7 +3,7 @@ import { useIntl } from 'react-intl'; import FinancialSheet from 'components/FinancialSheet'; import DataTable from 'components/DataTable'; -import Money from 'components/Money'; +import { CellTextSpan } from 'components/Datatable/Cells'; import withTrialBalance from './withTrialBalance'; @@ -28,9 +28,11 @@ function TrialBalanceSheetTable({ accessor: (row) => (row.code ? `${row.name} - ${row.code}` : row.name), className: 'name', width: 160, + textOverview: true, }, { Header: formatMessage({ id: 'credit' }), + Cell: CellTextSpan, accessor: 'formatted_credit', className: 'credit', width: getColumnWidth(trialBalanceTableRows, `credit`, { @@ -39,11 +41,13 @@ function TrialBalanceSheetTable({ }, { Header: formatMessage({ id: 'debit' }), + Cell: CellTextSpan, accessor: 'formatted_debit', width: getColumnWidth(trialBalanceTableRows, `debit`, { minWidth: 95 }), }, { Header: formatMessage({ id: 'balance' }), + Cell: CellTextSpan, accessor: 'formatted_balance', className: 'balance', width: getColumnWidth(trialBalanceTableRows, `balance`, { @@ -56,7 +60,7 @@ function TrialBalanceSheetTable({ const rowClassNames = (row) => { const { original } = row; - const rowTypes = Array.isArray(original.rowTypes) ? original.rowTypes : []; + const rowTypes = Array.isArray(original.rowType) ? original.rowType : [original.rowType]; return { ...rowTypes.reduce((acc, rowType) => { diff --git a/client/src/containers/FinancialStatements/common.js b/client/src/containers/FinancialStatements/common.js index a4ea8fc64..216e06ab4 100644 --- a/client/src/containers/FinancialStatements/common.js +++ b/client/src/containers/FinancialStatements/common.js @@ -1,4 +1,4 @@ -import { mapKeys, omit, snakeCase } from 'lodash'; +import { omit } from 'lodash'; import { transformToCamelCase, flatObject } from 'utils'; import { formatMessage } from 'services/intl'; diff --git a/client/src/lang/en/index.js b/client/src/lang/en/index.js index 02bb0bb5b..c9d834ca3 100644 --- a/client/src/lang/en/index.js +++ b/client/src/lang/en/index.js @@ -970,5 +970,7 @@ export default { 'You could not delete item that has associated inventory adjustments transactions', format: 'Format', current: 'Current', - adjustment_reasons: 'Adjustment reasons' + adjustment_reasons: 'Adjustment reasons', + transaction_number: 'Transaction #', + running_balance: 'Running balance' }; diff --git a/client/src/store/financialStatement/financialStatements.mappers.js b/client/src/store/financialStatement/financialStatements.mappers.js index dd8d4e419..bc21867f5 100644 --- a/client/src/store/financialStatement/financialStatements.mappers.js +++ b/client/src/store/financialStatement/financialStatements.mappers.js @@ -1,14 +1,8 @@ -import { omit } from 'lodash'; +import { omit, chain } from 'lodash'; +import moment from 'moment'; 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([ @@ -31,51 +25,79 @@ export const mapBalanceSheetToTableRows = (accounts) => { }; 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; - }, []); + const TYPES = { + ENTRY: 'ENTRY', + TOTAL_ENTRIES: 'TOTAL_ENTRIES', + EMPTY_ROW: 'EMPTY_ROW', + }; + + const entriesMapper = (transaction) => { + return transaction.entries.map((entry, index) => ({ + ...(index === 0 + ? { + date: transaction.date, + reference_type: transaction.reference_type, + reference_id: transaction.reference_id, + reference_type_formatted: transaction.reference_type_formatted, + } + : {}), + rowType: TYPES.ENTRY, + ...entry, + })); + }; + + return chain(journal) + .map((transaction) => { + const entries = entriesMapper(transaction); + + return [ + ...entries, + { + rowType: TYPES.TOTAL_ENTRIES, + currency_code: transaction.currency_code, + credit: transaction.credit, + debit: transaction.debit, + formatted_credit: transaction.formatted_credit, + formatted_debit: transaction.formatted_debit, + }, + { + rowType: TYPES.EMPTY_ROW, + }, + ]; + }) + .flatten() + .value(); }; - 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; - }, []); + return chain(accounts) + .map((account) => { + return { + name: '', + code: account.code, + rowType: 'ACCOUNT_ROW', + date: account.name, + children: [ + { + ...account.opening_balance, + name: 'Opening balance', + rowType: 'OPENING_BALANCE', + }, + ...account.transactions.map((transaction) => ({ + ...transaction, + name: account.name, + code: account.code, + date: moment(transaction.date).format('DD MMM YYYY'), + })), + { + ...account.closing_balance, + name: 'Closing balance', + rowType: 'CLOSING_BALANCE', + }, + ], + }; + }) + .value(); }; export const ARAgingSummaryTableRowsMapper = (sheet, total) => { @@ -109,25 +131,32 @@ export const ARAgingSummaryTableRowsMapper = (sheet, total) => { current: sheet.total.current.formatted_amount, ...mapAging(sheet.total.aging), total: sheet.total.total.formatted_amount, - } - ]; -}; - -export const mapTrialBalanceSheetToRows = (sheet) => { - return [ - ...sheet.accounts, - { - name: 'Total', - rowTypes: ['total'], - ...sheet.total, }, ]; }; -export const profitLossToTableRowsMapper = (profitLoss) => { +export const mapTrialBalanceSheetToRows = (sheet) => { + const results = []; - return [ - { + if (sheet.accounts) { + sheet.accounts.forEach((account) => { + results.push(account); + }); + } + if (sheet.total) { + results.push({ + rowType: 'total', + ...sheet.total, + }); + } + return results; +}; + +export const profitLossToTableRowsMapper = (profitLoss) => { + const results = []; + + if (profitLoss.income) { + results.push({ name: 'Income', total: profitLoss.income.total, children: [ @@ -140,8 +169,10 @@ export const profitLossToTableRowsMapper = (profitLoss) => { }, ], total_periods: profitLoss.income.total_periods, - }, - { + }); + } + if (profitLoss.cost_of_sales) { + results.push({ name: 'Cost of sales', total: profitLoss.cost_of_sales.total, children: [ @@ -153,15 +184,19 @@ export const profitLossToTableRowsMapper = (profitLoss) => { rowTypes: ['cogs_total', 'section_total', 'total'], }, ], - total_periods: profitLoss.cost_of_sales.total_periods - }, - { + total_periods: profitLoss.cost_of_sales.total_periods, + }); + } + if (profitLoss.gross_profit) { + results.push({ name: 'Gross profit', total: profitLoss.gross_profit.total, total_periods: profitLoss.gross_profit.total_periods, rowTypes: ['gross_total', 'section_total', 'total'], - }, - { + }) + } + if (profitLoss.expenses) { + results.push({ name: 'Expenses', total: profitLoss.expenses.total, children: [ @@ -174,14 +209,34 @@ export const profitLossToTableRowsMapper = (profitLoss) => { }, ], total_periods: profitLoss.expenses.total_periods, - }, - { + }) + } + if (profitLoss.operating_profit) { + results.push({ name: 'Net Operating income', total: profitLoss.operating_profit.total, total_periods: profitLoss.income.total_periods, rowTypes: ['net_operating_total', 'section_total', 'total'], - }, - { + }) + } + if (profitLoss.other_income) { + results.push({ + name: 'Other Income', + total: profitLoss.other_income.total, + total_periods: profitLoss.other_income.total_periods, + children: [ + ...profitLoss.other_income.accounts, + { + name: 'Total other income', + total: profitLoss.other_income.total, + total_periods: profitLoss.other_income.total_periods, + rowTypes: ['expenses_total', 'section_total', 'total'], + }, + ], + }); + } + if (profitLoss.other_expenses) { + results.push({ name: 'Other expenses', total: profitLoss.other_expenses.total, total_periods: profitLoss.other_expenses.total_periods, @@ -194,12 +249,15 @@ export const profitLossToTableRowsMapper = (profitLoss) => { rowTypes: ['expenses_total', 'section_total', 'total'], }, ], - }, - { + }); + } + if (profitLoss.net_income) { + results.push({ name: 'Net Income', total: profitLoss.net_income.total, total_periods: profitLoss.net_income.total_periods, rowTypes: ['net_income_total', 'section_total', 'total'], - }, - ]; -}; \ No newline at end of file + }) + }; + return results; +}; diff --git a/client/src/style/components/DataTable/DataTable.scss b/client/src/style/components/DataTable/DataTable.scss index c9fb01445..1724bb489 100644 --- a/client/src/style/components/DataTable/DataTable.scss +++ b/client/src/style/components/DataTable/DataTable.scss @@ -27,6 +27,12 @@ color: #58667b; font-weight: 500; border-bottom: 1px solid rgb(224, 224, 224); + + > div{ + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } } .sort-icon { width: 0; @@ -141,10 +147,23 @@ .placeholder { color: #a0a0a0; } + .text-overview{ + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + + } .bp3-form-group { width: 100%; } + + &.is-text-overview { + .expend-padding{ + display: flex; + width: 100%; + } + } } .tr:hover .td { background: #f3f7fc; diff --git a/client/src/style/containers/FinancialStatements/FinancialSheet.scss b/client/src/style/containers/FinancialStatements/FinancialSheet.scss index b2bf7c1e4..18b3c7957 100644 --- a/client/src/style/containers/FinancialStatements/FinancialSheet.scss +++ b/client/src/style/containers/FinancialStatements/FinancialSheet.scss @@ -60,12 +60,17 @@ display: none; } } - &__basis { + &__footer { color: #888; text-align: center; margin-top: auto; padding-top: 18px; font-size: 13px; + + + > span + span{ + padding-left: 10px; + } } .dashboard__loading-indicator { margin: auto; diff --git a/client/src/style/pages/FinancialStatements/ARAgingSummary.scss b/client/src/style/pages/FinancialStatements/ARAgingSummary.scss index c331c9511..afdc364cc 100644 --- a/client/src/style/pages/FinancialStatements/ARAgingSummary.scss +++ b/client/src/style/pages/FinancialStatements/ARAgingSummary.scss @@ -13,20 +13,22 @@ } } .tbody{ - .tr .td{ - border-bottom: 0; - padding-top: 0.4rem; - padding-bottom: 0.4rem; - } - .tr:not(:first-child) .td{ - border-top: 1px solid transparent; - } - .tr.row-type--total{ - font-weight: 500; - + .tr:not(.no-results) { .td{ - border-top: 1px solid #333; - border-bottom: 3px double #333; + border-bottom: 0; + padding-top: 0.4rem; + padding-bottom: 0.4rem; + } + &.row-type--total{ + font-weight: 500; + + .td{ + border-top: 1px solid #333; + border-bottom: 3px double #333; + } + } + &:not(:first-child) .td{ + border-top: 1px solid transparent; } } } diff --git a/client/src/style/pages/FinancialStatements/BalanceSheet.scss b/client/src/style/pages/FinancialStatements/BalanceSheet.scss index 35e909077..e145fd445 100644 --- a/client/src/style/pages/FinancialStatements/BalanceSheet.scss +++ b/client/src/style/pages/FinancialStatements/BalanceSheet.scss @@ -36,7 +36,7 @@ .tr.is-expanded{ .td.total, .td.total-period{ - > span{ + > span.cell-text{ display: none; } } diff --git a/client/src/style/pages/FinancialStatements/FinancialSheet.scss b/client/src/style/pages/FinancialStatements/FinancialSheet.scss index f3e7530ba..5c37bfdd3 100644 --- a/client/src/style/pages/FinancialStatements/FinancialSheet.scss +++ b/client/src/style/pages/FinancialStatements/FinancialSheet.scss @@ -3,9 +3,17 @@ &--financial-report{ .table { + .tbody{ + + .tr.no-results { + .td{ + border-bottom: 1px solid #DDD; + } + } + } .thead{ .tr .th{ - background: transparent; + background-color: #fff; border-top: 1px solid #666; border-bottom: 1px solid #666; diff --git a/client/src/style/pages/FinancialStatements/GeneralLedger.scss b/client/src/style/pages/FinancialStatements/GeneralLedger.scss index f3fd1fa6f..96dd9a8a9 100644 --- a/client/src/style/pages/FinancialStatements/GeneralLedger.scss +++ b/client/src/style/pages/FinancialStatements/GeneralLedger.scss @@ -2,25 +2,64 @@ .financial-sheet{ &--general-ledger{ .financial-sheet__table{ + .tbody, + .thead{ + .tr .td, + .tr .th{ + &.credit, + &.debit, + &.running_balance, + &.amount{ + justify-content: flex-end; + } + } + } .tbody{ + + .tr .td{ + padding-top: 0.2rem; + padding-bottom: 0.2rem; + border-top-color: transparent; + border-bottom-color: transparent; + + &.date{ + > div{ + display: flex; + } + span.force-width{ + position: relative; + } + } + } + .tr:not(.no-results) .td{ + border-left: 1px solid #ececec; + } .tr.row-type{ - &--opening_balance, - &--closing_balance{ + + &--ACCOUNT_ROW{ .td{ - border-top: 1px solid #333; + &.date{ + font-weight: 500; + } + &.name{ + border-left-color: transparent; + } } - .name, - .amount, - .balance{ + &:not(:first-child).is-expanded .td{ + border-top: 1px solid #DDD; + } + } + &--OPENING_BALANCE, + &--CLOSING_BALANCE{ + .amount{ font-weight: 500; } } - &--closing_balance .td{ - border-bottom-color: #666; - } - &--account_name .td.name{ - font-weight: 500; + &--CLOSING_BALANCE{ + .name{ + font-weight: 500; + } } } } diff --git a/client/src/style/pages/FinancialStatements/Journal.scss b/client/src/style/pages/FinancialStatements/Journal.scss index 7447d752b..cd7792e9b 100644 --- a/client/src/style/pages/FinancialStatements/Journal.scss +++ b/client/src/style/pages/FinancialStatements/Journal.scss @@ -3,19 +3,39 @@ &--journal{ .financial-sheet__table{ - + .tr .td.credit, + .tr .th.credit, + .tr .td.debit, + .tr .th.debit{ + justify-content: flex-end; + } .tbody{ .tr:not(.no-results) .td{ - padding: 0.4rem; + padding: 0.3rem 0.4rem; color: #000; border-bottom-color: transparent; - min-height: 32px; + min-height: 28px; border-left: 1px solid #ececec; &:first-of-type{ border-left: 0; } + &.account_name, + &.reference_type_formatted{ + white-space: nowrap; + overflow: hidden; + text-overflow:ellipsis; + } } + .tr:not(.no-results):last-child{ + .td{ + border-bottom: 1px solid #dbdbdb; + } + } + .tr.row_type--TOTAL_ENTRIES{ + font-weight: 600; + } + } } } diff --git a/client/src/utils.js b/client/src/utils.js index 0ba62b39f..a68ea7bae 100644 --- a/client/src/utils.js +++ b/client/src/utils.js @@ -389,6 +389,16 @@ export const getColumnWidth = ( return result; }; +export const getForceWidth = ( + text, + magicSpacing = 14, +) => { + const textLength = text.length; + const result = textLength * magicSpacing + + return result; +} + export const toSafeNumber = (number) => { return _.toNumber(_.defaultTo(number, 0)); }; diff --git a/server/src/api/controllers/FinancialStatements/JournalSheet.ts b/server/src/api/controllers/FinancialStatements/JournalSheet.ts index f832fedf6..350a80dbb 100644 --- a/server/src/api/controllers/FinancialStatements/JournalSheet.ts +++ b/server/src/api/controllers/FinancialStatements/JournalSheet.ts @@ -16,7 +16,8 @@ export default class JournalSheetController extends BaseFinancialReportControlle router() { const router = Router(); - router.get('/', + router.get( + '/', this.journalValidationSchema, this.validationResult, this.asyncMiddleware(this.journal.bind(this)) @@ -31,18 +32,20 @@ export default class JournalSheetController extends BaseFinancialReportControlle return [ query('from_date').optional().isISO8601(), query('to_date').optional().isISO8601(), - oneOf([ - query('transaction_types').optional().isArray({ min: 1 }), - query('transaction_types.*').optional().isNumeric().toInt(), - ], [ - query('transaction_types').optional().trim().escape(), - ]), - oneOf([ - query('account_ids').optional().isArray({ min: 1 }), - query('account_ids.*').optional().isNumeric().toInt(), - ], [ - query('account_ids').optional().isNumeric().toInt(), - ]), + oneOf( + [ + query('transaction_types').optional().isArray({ min: 1 }), + query('transaction_types.*').optional().isNumeric().toInt(), + ], + [query('transaction_types').optional().trim().escape()] + ), + oneOf( + [ + query('account_ids').optional().isArray({ min: 1 }), + query('account_ids.*').optional().isNumeric().toInt(), + ], + [query('account_ids').optional().isNumeric().toInt()] + ), query('from_range').optional().isNumeric().toInt(), query('to_range').optional().isNumeric().toInt(), query('number_format.no_cents').optional().isBoolean().toBoolean(), @@ -52,7 +55,7 @@ export default class JournalSheetController extends BaseFinancialReportControlle /** * Retrieve the ledger report of the given account. - * @param {Request} req - + * @param {Request} req - * @param {Response} res - */ async journal(req: Request, res: Response, next: NextFunction) { @@ -63,11 +66,20 @@ export default class JournalSheetController extends BaseFinancialReportControlle ...filter, accountsIds: castArray(filter.accountsIds), }; - const organizationName = settings.get({ group: 'organization', key: 'name' }); - const baseCurrency = settings.get({ group: 'organization', key: 'base_currency' }); + const organizationName = settings.get({ + group: 'organization', + key: 'name', + }); + const baseCurrency = settings.get({ + group: 'organization', + key: 'base_currency', + }); try { - const { data, query } = await this.journalService.journalSheet(tenantId, filter); + const { data, query } = await this.journalService.journalSheet( + tenantId, + filter + ); return res.status(200).send({ organization_name: organizationName, @@ -79,4 +91,4 @@ export default class JournalSheetController extends BaseFinancialReportControlle next(error); } } -} \ No newline at end of file +} diff --git a/server/src/data/BalanceSheetStructure.ts b/server/src/data/BalanceSheetStructure.ts index d57a58d9c..bdb785000 100644 --- a/server/src/data/BalanceSheetStructure.ts +++ b/server/src/data/BalanceSheetStructure.ts @@ -8,19 +8,42 @@ const balanceSheetStructure: IBalanceSheetStructureSection[] = [ children: [ { name: 'Current Asset', - type: 'accounts_section', - accountsTypesRelated: ['current_asset'], + sectionType: 'assets', + type: 'section', + children: [ + { + name: 'Cash and cash equivalents', + type: 'accounts_section', + accountsTypes: ['cash', 'bank'], + }, + { + name: 'Accounts Receivable', + type: 'accounts_section', + accountsTypes: ['accounts_receivable'], + }, + { + name: 'Inventories', + type: 'accounts_section', + accountsTypes: ['inventory'], + }, + { + name: 'Other current assets', + type: 'accounts_section', + accountsTypes: ['other_current_asset'], + }, + ], + alwaysShow: true, }, { name: 'Fixed Asset', type: 'accounts_section', - accountsTypesRelated: ['fixed_asset'], + accountsTypes: ['fixed_asset'], }, { - name: 'Other Asset', + name: 'Non-Current Assets', type: 'accounts_section', - accountsTypesRelated: ['other_asset'], - }, + accountsTypes: ['non_current_asset'], + } ], alwaysShow: true, }, @@ -35,27 +58,32 @@ const balanceSheetStructure: IBalanceSheetStructureSection[] = [ type: 'section', children: [ { - name: 'Current Liability', + name: 'Current Liabilties', type: 'accounts_section', - accountsTypesRelated: ['current_liability'], + accountsTypes: [ + 'accounts_payable', + 'tax_payable', + 'credit_card', + 'other_current_liability' + ], }, { - name: 'Long Term Liability', + name: 'Long-Term Liabilities', type: 'accounts_section', - accountsTypesRelated: ['long_term_liability'], + accountsTypes: ['long_term_liability'], }, { - name: 'Other Liability', + name: 'Non-Current Liabilities', type: 'accounts_section', - accountsTypesRelated: ['other_liability'], - }, + accountsTypes: ['non_current_liability'], + } ], }, { name: 'Equity', sectionType: 'equity', type: 'accounts_section', - accountsTypesRelated: ['equity'], + accountsTypes: ['equity'], }, ], alwaysShow: true, diff --git a/server/src/database/seeds/data/accounts_types.js b/server/src/database/seeds/data/accounts_types.js index 11c2a5f13..b38da463a 100644 --- a/server/src/database/seeds/data/accounts_types.js +++ b/server/src/database/seeds/data/accounts_types.js @@ -53,7 +53,7 @@ export default [ key: 'non_current_asset', normal: 'debit', root_type: 'asset', - child_type: 'non_current_asset', + child_type: 'fixed_asset', balance_sheet: true, income_sheet: false, }, @@ -81,14 +81,6 @@ export default [ balance_sheet: true, income_sheet: false, }, - { - key: 'long_term_liability', - normal: 'credit', - root_type: 'liability', - child_type: 'long_term_liability', - balance_sheet: false, - income_sheet: true, - }, { key: 'other_current_liability', normal: 'credit', @@ -97,6 +89,22 @@ export default [ balance_sheet: false, income_sheet: true, }, + { + key: 'non_current_liability', + normal: 'credit', + root_type: 'liability', + child_type: 'current_liability', + balance_sheet: false, + income_sheet: true, + }, + { + key: 'long_term_liability', + normal: 'credit', + root_type: 'liability', + child_type: 'long_term_liability', + balance_sheet: false, + income_sheet: true, + }, { key: 'equity', normal: 'credit', diff --git a/server/src/interfaces/BalanceSheet.ts b/server/src/interfaces/BalanceSheet.ts index f9a8e631d..cf330b1dc 100644 --- a/server/src/interfaces/BalanceSheet.ts +++ b/server/src/interfaces/BalanceSheet.ts @@ -42,7 +42,7 @@ export interface IBalanceSheetStructureSection { sectionType?: string; type: 'section' | 'accounts_section'; children?: IBalanceSheetStructureSection[]; - accountsTypesRelated?: string[]; + accountsTypes?: string[]; alwaysShow?: boolean; } @@ -74,6 +74,6 @@ export interface IBalanceSheetSection { total: IBalanceSheetAccountTotal; totalPeriods?: IBalanceSheetAccountTotal[]; - accountsTypesRelated?: string[]; + accountsTypes?: string[]; _forceShow?: boolean; } diff --git a/server/src/interfaces/GeneralLedgerSheet.ts b/server/src/interfaces/GeneralLedgerSheet.ts index 5c75a1137..84d0cc607 100644 --- a/server/src/interfaces/GeneralLedgerSheet.ts +++ b/server/src/interfaces/GeneralLedgerSheet.ts @@ -14,13 +14,26 @@ export interface IGeneralLedgerSheetQuery { export interface IGeneralLedgerSheetAccountTransaction { id: number, + amount: number, + runningBalance: number, + credit: number, + debit: number, + formattedAmount: string, + formattedCredit: string, + formattedDebit: string, + formattedRunningBalance: string, + currencyCode: string, note?: string, + transactionType?: string, + transactionNumber: string, + referenceId?: number, referenceType?: string, + date: Date|string, }; @@ -38,8 +51,8 @@ export interface IGeneralLedgerSheetAccount { index: number, parentAccountId: number, transactions: IGeneralLedgerSheetAccountTransaction[], - opening: IGeneralLedgerSheetAccountBalance, - closing: IGeneralLedgerSheetAccountBalance, + openingBalance: IGeneralLedgerSheetAccountBalance, + closingBalance: IGeneralLedgerSheetAccountBalance, } export interface IAccountTransaction { diff --git a/server/src/interfaces/ProfitLossSheet.ts b/server/src/interfaces/ProfitLossSheet.ts index da0b81059..f070c1f1b 100644 --- a/server/src/interfaces/ProfitLossSheet.ts +++ b/server/src/interfaces/ProfitLossSheet.ts @@ -50,7 +50,7 @@ export interface IProfitLossSheetStatement { costOfSales: IProfitLossSheetAccountsSection, expenses: IProfitLossSheetAccountsSection, otherExpenses: IProfitLossSheetAccountsSection, - + otherIncome: IProfitLossSheetAccountsSection, netIncome: IProfitLossSheetTotalSection; operatingProfit: IProfitLossSheetTotalSection; grossProfit: IProfitLossSheetTotalSection; diff --git a/server/src/models/AccountType.js b/server/src/models/AccountType.js index 452ac71d9..8d16b1dee 100644 --- a/server/src/models/AccountType.js +++ b/server/src/models/AccountType.js @@ -58,23 +58,23 @@ export default class AccountType extends TenantModel { static get labels() { return { inventory: 'Inventory', - other_current_asset: 'Other current asset', - bank: 'Bank account', + other_current_asset: 'Other Current Asset', + bank: 'Bank Account', cash: 'Cash', - fixed_asset: 'Fixed asset', - non_current_asset: 'Non-current asset', - accounts_payable: 'Accounts payable (A/P)', - accounts_receivable: 'Accounts receivable (A/R)', - credit_card: 'Credit card', - long_term_liability: 'Long term liability', - other_current_liability: 'Other current liability', - other_liability: 'Other liability', + fixed_asset: 'Fixed Asset', + non_current_asset: 'Non-Current Asset', + accounts_payable: 'Accounts Payable (A/P)', + accounts_receivable: 'Accounts Receivable (A/R)', + credit_card: 'Credit Card', + long_term_liability: 'Long Term Liability', + other_current_liability: 'Other Current Liability', + other_liability: 'Other Liability', equity: "Equity", expense: "Expense", income: "Income", - other_income: "Other income", - other_expense: "Other expense", - cost_of_goods_sold: "Cost of goods sold (COGS)", + other_income: "Other Income", + other_expense: "Other Expense", + cost_of_goods_sold: "Cost of Goods Sold (COGS)", }; } } diff --git a/server/src/services/Accounting/JournalPoster.ts b/server/src/services/Accounting/JournalPoster.ts index 9d33e8461..ddf8e855a 100644 --- a/server/src/services/Accounting/JournalPoster.ts +++ b/server/src/services/Accounting/JournalPoster.ts @@ -71,6 +71,13 @@ export default class JournalPoster implements IJournalPoster { } } + /** + * + */ + public isEmpty() { + return this.entries.length === 0; + } + /** * Writes the credit entry for the given account. * @param {IJournalEntry} entry - diff --git a/server/src/services/FinancialStatements/BalanceSheet/BalanceSheet.ts b/server/src/services/FinancialStatements/BalanceSheet/BalanceSheet.ts index 6c36c3c31..62141bb7a 100644 --- a/server/src/services/FinancialStatements/BalanceSheet/BalanceSheet.ts +++ b/server/src/services/FinancialStatements/BalanceSheet/BalanceSheet.ts @@ -184,7 +184,7 @@ export default class BalanceSheetStatement extends FinancialSheet { const filteredAccounts = accounts // Filter accounts that associated to the section accounts types. .filter( - (account) => sectionAccountsTypes.indexOf(account.type.childType) !== -1 + (account) => sectionAccountsTypes.indexOf(account.type.key) !== -1 ) .map((account) => this.balanceSheetAccountMapper(account)) // Filter accounts that have no transaction when `noneTransactions` is on. @@ -258,7 +258,7 @@ export default class BalanceSheetStatement extends FinancialSheet { type: structure.type, ...(structure.type === 'accounts_section' ? this.structureRelatedAccountsMapper( - structure.accountsTypesRelated, + structure.accountsTypes, accounts ) : this.structureSectionMapper(structure, accounts)), diff --git a/server/src/services/FinancialStatements/GeneralLedger/GeneralLedger.ts b/server/src/services/FinancialStatements/GeneralLedger/GeneralLedger.ts index 98b6a3c2d..48df05ef8 100644 --- a/server/src/services/FinancialStatements/GeneralLedger/GeneralLedger.ts +++ b/server/src/services/FinancialStatements/GeneralLedger/GeneralLedger.ts @@ -1,4 +1,4 @@ -import { pick } from 'lodash'; +import { pick, get, last } from 'lodash'; import { IGeneralLedgerSheetQuery, IGeneralLedgerSheetAccount, @@ -8,9 +8,13 @@ import { IJournalPoster, IAccountType, IJournalEntry, + IContact, } from 'interfaces'; import FinancialSheet from '../FinancialSheet'; +/** + * General ledger sheet. + */ export default class GeneralLedgerSheet extends FinancialSheet { tenantId: number; accounts: IAccount[]; @@ -18,6 +22,7 @@ export default class GeneralLedgerSheet extends FinancialSheet { openingBalancesJournal: IJournalPoster; closingBalancesJournal: IJournalPoster; transactions: IJournalPoster; + contactsMap: Map; baseCurrency: string; /** @@ -32,6 +37,7 @@ export default class GeneralLedgerSheet extends FinancialSheet { tenantId: number, query: IGeneralLedgerSheetQuery, accounts: IAccount[], + contactsByIdMap: Map, transactions: IJournalPoster, openingBalancesJournal: IJournalPoster, closingBalancesJournal: IJournalPoster, @@ -43,48 +49,100 @@ export default class GeneralLedgerSheet extends FinancialSheet { this.query = query; this.numberFormat = this.query.numberFormat; this.accounts = accounts; + this.contactsMap = contactsByIdMap; this.transactions = transactions; this.openingBalancesJournal = openingBalancesJournal; this.closingBalancesJournal = closingBalancesJournal; this.baseCurrency = baseCurrency; } + /** + * Retrieve the transaction amount. + * @param {number} credit - Credit amount. + * @param {number} debit - Debit amount. + * @param {string} normal - Credit or debit. + */ + getAmount(credit: number, debit: number, normal: string) { + return normal === 'credit' ? credit - debit : debit - credit; + } + + /** + * Entry mapper. + * @param {IJournalEntry} entry - + * @return {IGeneralLedgerSheetAccountTransaction} + */ + entryReducer( + entries: IGeneralLedgerSheetAccountTransaction[], + entry: IJournalEntry, + index: number + ): IGeneralLedgerSheetAccountTransaction[] { + const lastEntry = last(entries); + const openingBalance = 0; + + const contact = this.contactsMap.get(entry.contactId); + const amount = this.getAmount( + entry.credit, + entry.debit, + entry.accountNormal + ); + const runningBalance = + (entries.length === 0 + ? openingBalance + : lastEntry + ? lastEntry.runningBalance + : 0) + amount; + + const newEntry = { + date: entry.date, + entryId: entry.id, + + referenceType: entry.referenceType, + referenceId: entry.referenceId, + referenceTypeFormatted: entry.referenceTypeFormatted, + + contactName: get(contact, 'displayName'), + contactType: get(contact, 'contactService'), + + transactionType: entry.transactionType, + index: entry.index, + note: entry.note, + + credit: entry.credit, + debit: entry.debit, + amount, + runningBalance, + + formattedAmount: this.formatNumber(amount), + formattedCredit: this.formatNumber(entry.credit), + formattedDebit: this.formatNumber(entry.debit), + formattedRunningBalance: this.formatNumber(runningBalance), + + currencyCode: this.baseCurrency, + }; + entries.push(newEntry); + + return entries; + } + /** * Mapping the account transactions to general ledger transactions of the given account. * @param {IAccount} account * @return {IGeneralLedgerSheetAccountTransaction[]} */ private accountTransactionsMapper( - account: IAccount & { type: IAccountType } + account: IAccount & { type: IAccountType }, + openingBalance: number ): IGeneralLedgerSheetAccountTransaction[] { const entries = this.transactions.getAccountEntries(account.id); - return entries.map( - (transaction: IJournalEntry): IGeneralLedgerSheetAccountTransaction => { - let amount = 0; - - if (account.type.normal === 'credit') { - amount += transaction.credit - transaction.debit; - } else if (account.type.normal === 'debit') { - amount += transaction.debit - transaction.credit; - } - const formattedAmount = this.formatNumber(amount); - - return { - ...pick(transaction, [ - 'id', - 'note', - 'transactionType', - 'referenceType', - 'referenceId', - 'referenceTypeFormatted', - 'date', - ]), - amount, - formattedAmount, - currencyCode: this.baseCurrency, - }; - } + return entries.reduce( + ( + entries: IGeneralLedgerSheetAccountTransaction[], + entry: IJournalEntry + ) => { + return this.entryReducer(entries, entry, openingBalance); + }, + [] ); } @@ -128,11 +186,21 @@ export default class GeneralLedgerSheet extends FinancialSheet { private accountMapper( account: IAccount & { type: IAccountType } ): IGeneralLedgerSheetAccount { + const openingBalance = this.accountOpeningBalance(account); + const closingBalance = this.accountClosingBalance(account); + return { - ...pick(account, ['id', 'name', 'code', 'index', 'parentAccountId']), - opening: this.accountOpeningBalance(account), - transactions: this.accountTransactionsMapper(account), - closing: this.accountClosingBalance(account), + id: account.id, + name: account.name, + code: account.code, + index: account.index, + parentAccountId: account.parentAccountId, + openingBalance, + transactions: this.accountTransactionsMapper( + account, + openingBalance.amount + ), + closingBalance, }; } @@ -149,7 +217,8 @@ export default class GeneralLedgerSheet extends FinancialSheet { .map((account: IAccount & { type: IAccountType }) => this.accountMapper(account) ) - // Filter general ledger accounts that have no transactions when `noneTransactions` is on. + // Filter general ledger accounts that have no transactions + // when`noneTransactions` is on. .filter( (generalLedgerAccount: IGeneralLedgerSheetAccount) => !( diff --git a/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerService.ts b/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerService.ts index 071951cf4..8fd09ac5f 100644 --- a/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerService.ts +++ b/server/src/services/FinancialStatements/GeneralLedger/GeneralLedgerService.ts @@ -7,6 +7,8 @@ import TenancyService from 'services/Tenancy/TenancyService'; import Journal from 'services/Accounting/JournalPoster'; import GeneralLedgerSheet from 'services/FinancialStatements/GeneralLedger/GeneralLedger'; +import { transformToMap } from 'utils'; + const ERRORS = { ACCOUNTS_NOT_FOUND: 'ACCOUNTS_NOT_FOUND', }; @@ -70,6 +72,7 @@ export default class GeneralLedgerService { const { accountRepository, transactionsRepository, + contactRepository } = this.tenancy.repositories(tenantId); const settings = this.tenancy.settings(tenantId); @@ -89,6 +92,10 @@ export default class GeneralLedgerService { const accounts = await accountRepository.all('type'); const accountsGraph = await accountRepository.getDependencyGraph(); + // Retrieve all contacts on the storage. + const contacts = await contactRepository.all(); + const contactsByIdMap = transformToMap(contacts, 'id'); + // Retreive journal transactions from/to the given date. const transactions = await transactionsRepository.journal({ fromDate: filter.fromDate, @@ -127,6 +134,7 @@ export default class GeneralLedgerService { tenantId, filter, accounts, + contactsByIdMap, transactionsJournal, openingTransJournal, closingTransJournal, diff --git a/server/src/services/FinancialStatements/JournalSheet/JournalSheet.ts b/server/src/services/FinancialStatements/JournalSheet/JournalSheet.ts index 718b75076..313eb58f9 100644 --- a/server/src/services/FinancialStatements/JournalSheet/JournalSheet.ts +++ b/server/src/services/FinancialStatements/JournalSheet/JournalSheet.ts @@ -1,19 +1,20 @@ -import { sumBy, chain, omit } from 'lodash'; +import { sumBy, chain, get, head } from 'lodash'; import { IJournalEntry, IJournalPoster, IJournalReportEntriesGroup, IJournalReportQuery, IJournalReport, + IContact, } from 'interfaces'; import FinancialSheet from '../FinancialSheet'; -import { AccountTransaction } from 'models'; export default class JournalSheet extends FinancialSheet { tenantId: number; journal: IJournalPoster; query: IJournalReportQuery; baseCurrency: string; + readonly contactsById: Map; /** * Constructor method. @@ -24,6 +25,8 @@ export default class JournalSheet extends FinancialSheet { tenantId: number, query: IJournalReportQuery, journal: IJournalPoster, + accountsGraph: any, + contactsById: Map, baseCurrency: string ) { super(); @@ -32,22 +35,48 @@ export default class JournalSheet extends FinancialSheet { this.journal = journal; this.query = query; this.numberFormat = this.query.numberFormat; + this.accountsGraph = accountsGraph; + this.contactsById = contactsById; this.baseCurrency = baseCurrency; } /** - * Mappes the journal entries. - * @param {IJournalEntry[]} entries - + * Entry mapper. + * @param {IJournalEntry} entry */ - entriesMapper( - entries: IJournalEntry[], - ) { - return entries.map((entry: IJournalEntry) => { - return { - ...omit(entry, 'account'), - currencyCode: this.baseCurrency, - }; - }) + entryMapper(entry: IJournalEntry) { + const account = this.accountsGraph.getNodeData(entry.accountId); + const contact = this.contactsById.get(entry.contactId); + + return { + entryId: entry.id, + index: entry.index, + note: entry.note, + + contactName: get(contact, 'displayName'), + contactType: get(contact, 'contactService'), + + accountName: account.name, + accountCode: account.code, + transactionNumber: entry.transactionNumber, + + currencyCode: this.baseCurrency, + formattedCredit: this.formatNumber(entry.credit), + formattedDebit: this.formatNumber(entry.debit), + + credit: entry.credit, + debit: entry.debit, + + createdAt: entry.createdAt, + }; + } + + /** + * Mappes the journal entries. + * @param {IJournalEntry[]} entries - + */ + entriesMapper(entries: IJournalEntry[]) { + return entries.map(this.entryMapper.bind(this)); } /** @@ -58,13 +87,17 @@ export default class JournalSheet extends FinancialSheet { */ entriesGroupsMapper( entriesGroup: IJournalEntry[], - key: string + groupEntry: IJournalEntry ): IJournalReportEntriesGroup { const totalCredit = sumBy(entriesGroup, 'credit'); const totalDebit = sumBy(entriesGroup, 'debit'); return { - id: key, + date: groupEntry.date, + referenceType: groupEntry.referenceType, + referenceId: groupEntry.referenceId, + referenceTypeFormatted: groupEntry.referenceTypeFormatted, + entries: this.entriesMapper(entriesGroup), currencyCode: this.baseCurrency, @@ -72,8 +105,8 @@ export default class JournalSheet extends FinancialSheet { credit: totalCredit, debit: totalDebit, - formattedCredit: this.formatNumber(totalCredit), - formattedDebit: this.formatNumber(totalDebit), + formattedCredit: this.formatTotalNumber(totalCredit), + formattedDebit: this.formatTotalNumber(totalDebit), }; } @@ -85,9 +118,10 @@ export default class JournalSheet extends FinancialSheet { entriesWalker(entries: IJournalEntry[]): IJournalReportEntriesGroup[] { return chain(entries) .groupBy((entry) => `${entry.referenceId}-${entry.referenceType}`) - .map((entriesGroup: IJournalEntry[], key: string) => - this.entriesGroupsMapper(entriesGroup, key) - ) + .map((entriesGroup: IJournalEntry[], key: string) => { + const headEntry = head(entriesGroup); + return this.entriesGroupsMapper(entriesGroup, headEntry); + }) .value(); } diff --git a/server/src/services/FinancialStatements/JournalSheet/JournalSheetService.ts b/server/src/services/FinancialStatements/JournalSheet/JournalSheetService.ts index 866bd1db2..686399c53 100644 --- a/server/src/services/FinancialStatements/JournalSheet/JournalSheetService.ts +++ b/server/src/services/FinancialStatements/JournalSheet/JournalSheetService.ts @@ -1,10 +1,13 @@ import { Service, Inject } from 'typedi'; import { IJournalReportQuery } from 'interfaces'; import moment from 'moment'; + import JournalSheet from './JournalSheet'; import TenancyService from 'services/Tenancy/TenancyService'; import Journal from 'services/Accounting/JournalPoster'; +import { transformToMap } from 'utils'; + @Service() export default class JournalSheetService { @Inject() @@ -40,6 +43,7 @@ export default class JournalSheetService { const { accountRepository, transactionsRepository, + contactRepository, } = this.tenancy.repositories(tenantId); const filter = { @@ -50,7 +54,6 @@ export default class JournalSheetService { tenantId, filter, }); - // Settings service. const settings = this.tenancy.settings(tenantId); const baseCurrency = settings.get({ @@ -60,6 +63,10 @@ export default class JournalSheetService { // Retrieve all accounts on the storage. const accountsGraph = await accountRepository.getDependencyGraph(); + // Retrieve all contacts on the storage. + const contacts = await contactRepository.all(); + const contactsByIdMap = transformToMap(contacts, 'id'); + // Retrieve all journal transactions based on the given query. const transactions = await transactionsRepository.journal({ fromDate: filter.fromDate, @@ -79,6 +86,8 @@ export default class JournalSheetService { tenantId, filter, transactionsJournal, + accountsGraph, + contactsByIdMap, baseCurrency ); // Retrieve journal report columns. diff --git a/server/src/services/FinancialStatements/ProfitLossSheet/ProfitLossSheet.ts b/server/src/services/FinancialStatements/ProfitLossSheet/ProfitLossSheet.ts index 9562acdf6..d5d16232b 100644 --- a/server/src/services/FinancialStatements/ProfitLossSheet/ProfitLossSheet.ts +++ b/server/src/services/FinancialStatements/ProfitLossSheet/ProfitLossSheet.ts @@ -50,6 +50,10 @@ export default class ProfitLossSheet extends FinancialSheet { this.initDateRangeCollection(); } + get otherIncomeAccounts() { + return this.accounts.filter((a) => a.type.key === 'other_income'); + } + /** * Filtering income accounts. * @return {IAccount & { type: IAccountType }[]} @@ -235,6 +239,14 @@ export default class ProfitLossSheet extends FinancialSheet { }; } + private get otherIncomeSection(): any { + return { + name: 'Other Income', + entryNormal: 'credit', + ...this.sectionMapper(this.otherIncomeAccounts) + } + } + /** * Retreive expenses section. * @return {IProfitLossSheetLossSection} @@ -343,10 +355,14 @@ export default class ProfitLossSheet extends FinancialSheet { * @return {IProfitLossSheetStatement} */ public reportData(): IProfitLossSheetStatement { + if (this.journal.isEmpty()) { + return null; + } const income = this.incomeSection; const costOfSales = this.costOfSalesSection; const expenses = this.expensesSection; const otherExpenses = this.otherExpensesSection; + const otherIncome = this.otherIncomeSection; // - Gross profit = Total income - COGS. const grossProfit = this.getSummarySection(income, costOfSales); @@ -356,7 +372,6 @@ export default class ProfitLossSheet extends FinancialSheet { expenses, costOfSales, ]); - // - Net income = Operating profit - Other expenses. const netIncome = this.getSummarySection(operatingProfit, otherExpenses); @@ -365,6 +380,7 @@ export default class ProfitLossSheet extends FinancialSheet { costOfSales, grossProfit, expenses, + otherIncome, otherExpenses, netIncome, operatingProfit, diff --git a/server/src/services/FinancialStatements/TrialBalanceSheet/TrialBalanceSheet.ts b/server/src/services/FinancialStatements/TrialBalanceSheet/TrialBalanceSheet.ts index 9c13915b1..86020ea0b 100644 --- a/server/src/services/FinancialStatements/TrialBalanceSheet/TrialBalanceSheet.ts +++ b/server/src/services/FinancialStatements/TrialBalanceSheet/TrialBalanceSheet.ts @@ -4,6 +4,7 @@ import { ITrialBalanceAccount, IAccount, ITrialBalanceTotal, + ITrialBalanceSheetData, IAccountType, } from 'interfaces'; import FinancialSheet from '../FinancialSheet'; @@ -49,6 +50,7 @@ export default class TrialBalanceSheet extends FinancialSheet { /** * Account mapper. * @param {IAccount} account + * @return {ITrialBalanceAccount} */ private accountMapper( account: IAccount & { type: IAccountType } @@ -80,6 +82,7 @@ export default class TrialBalanceSheet extends FinancialSheet { /** * Accounts walker. * @param {IAccount[]} accounts + * @return {ITrialBalanceAccount[]} */ private accountsWalker( accounts: IAccount & { type: IAccountType }[] @@ -136,8 +139,15 @@ export default class TrialBalanceSheet extends FinancialSheet { /** * Retrieve trial balance sheet statement data. + * Note: Retruns null in case there is no transactions between the given date periods. + * + * @return {ITrialBalanceSheetData} */ - public reportData() { + public reportData(): ITrialBalanceSheetData { + // Don't return noting if the journal has no transactions. + if (this.journalFinancial.isEmpty()) { + return null; + } const accounts = this.accountsWalker(this.accounts); const total = this.tatalSection(accounts); diff --git a/server/src/utils/index.js b/server/src/utils/index.js index 688dc4b18..09d648265 100644 --- a/server/src/utils/index.js +++ b/server/src/utils/index.js @@ -278,6 +278,15 @@ function defaultToTransform(value, defaultOrTransformedValue, defaultValue) { : _transfromedValue; } +const transformToMap = (objects, key) => { + const map = new Map(); + + objects.forEach(object => { + map.set(object[key], object); + }); + return map; +} + export { hashPassword, origin, @@ -299,4 +308,5 @@ export { formatNumber, isBlank, defaultToTransform, + transformToMap };