feat: Inventory item details report.

feat: Cash flow statement report.
This commit is contained in:
a.bouhuolia
2021-05-31 13:17:02 +02:00
parent 256d915f06
commit d47633b8ea
80 changed files with 5474 additions and 376 deletions

View File

@@ -62,6 +62,7 @@
"postcss-normalize": "8.0.1", "postcss-normalize": "8.0.1",
"postcss-preset-env": "6.7.0", "postcss-preset-env": "6.7.0",
"postcss-safe-parser": "4.0.1", "postcss-safe-parser": "4.0.1",
"ramda": "^0.27.1",
"react": "^16.12.0", "react": "^16.12.0",
"react-app-polyfill": "^1.0.6", "react-app-polyfill": "^1.0.6",
"react-body-classname": "^1.3.1", "react-body-classname": "^1.3.1",

View File

@@ -73,7 +73,7 @@ export default function NumberFormatFields({}) {
label={<T id={'money_format'} />} label={<T id={'money_format'} />}
helperText={<ErrorMessage name="formatMoney" />} helperText={<ErrorMessage name="formatMoney" />}
intent={inputIntent({ error, touched })} intent={inputIntent({ error, touched })}
className={classNames(CLASSES.FILL)} className={classNames('form-group--money-format', CLASSES.FILL)}
> >
<ListSelect <ListSelect
items={moneyFormat} items={moneyFormat}

View File

@@ -4,44 +4,42 @@ export const financialReportMenus = [
reports: [ reports: [
{ {
title: 'Balance Sheet Report', title: 'Balance Sheet Report',
desc: desc: "Reports a company's assets, liabilities and shareholders' equity at a specific point in time with comparison period(s).",
"Reports a company's assets, liabilities and shareholders' equity at a specific point in time with comparison period(s).",
link: '/financial-reports/balance-sheet', link: '/financial-reports/balance-sheet',
}, },
{ {
title: 'Trial Balance Sheet', title: 'Trial Balance Sheet',
desc: desc: 'Summarizes the credit and debit balance of each account in your chart of accounts at a specific point in time.',
'Summarizes the credit and debit balance of each account in your chart of accounts at a specific point in time.',
link: '/financial-reports/trial-balance-sheet', link: '/financial-reports/trial-balance-sheet',
}, },
{
title: 'Journal Report',
desc:
'The debit and credit entries of system transactions, sorted by date.',
link: '/financial-reports/journal-sheet',
},
{ {
title: 'Profit/Loss Report', title: 'Profit/Loss Report',
desc: desc: 'Reports the revenues, costs and expenses incurred during a specific point in time with comparison period(s).',
'Reports the revenues, costs and expenses incurred during a specific point in time with comparison period(s).',
link: '/financial-reports/profit-loss-sheet', link: '/financial-reports/profit-loss-sheet',
}, },
{
title: 'Cash Flow Statement',
desc: 'Reports inflow and outflow of cash and cash equivalents between a specific two points of time.',
link: '/financial-reports/cash-flow',
},
{
title: 'Journal Report',
desc: 'The debit and credit entries of system transactions, sorted by date.',
link: '/financial-reports/journal-sheet',
},
{ {
title: 'General Ledger Report', title: 'General Ledger Report',
desc: desc: 'Reports every transaction going in and out of your accounts and organized by accounts and date to monitoring activity of accounts.',
'Reports every transaction going in and out of your accounts and organized by accounts and date to monitoring activity of accounts.',
link: '/financial-reports/general-ledger', link: '/financial-reports/general-ledger',
}, },
{ {
title: 'Receivable Aging Summary', title: 'Receivable Aging Summary',
desc: desc: 'Summarize total unpaid balances of customers invoices with number of days the unpaid invoice is overdue.',
'Summarize total unpaid balances of customers invoices with number of days the unpaid invoice is overdue.',
link: '/financial-reports/receivable-aging-summary', link: '/financial-reports/receivable-aging-summary',
}, },
{ {
title: 'Payable Aging Summary', title: 'Payable Aging Summary',
desc: desc: 'Summarize total unpaid balances of vendors purchase invoices with the number of days the unpaid invoice is overdue.',
'Summarize total unpaid balances of vendors purchase invoices with the number of days the unpaid invoice is overdue.',
link: '/financial-reports/payable-aging-summary', link: '/financial-reports/payable-aging-summary',
}, },
], ],
@@ -54,20 +52,17 @@ export const SalesAndPurchasesReportMenus = [
reports: [ reports: [
{ {
title: 'Purchases By Items', title: 'Purchases By Items',
desc: desc: 'Shows the average age of unresolved issues for a project or filter. This helps you see whether your backlog is being kept up to date.',
'Shows the average age of unresolved issues for a project or filter. This helps you see whether your backlog is being kept up to date.',
link: '/financial-reports/purchases-by-items', link: '/financial-reports/purchases-by-items',
}, },
{ {
title: 'Sales By Items', title: 'Sales By Items',
desc: desc: 'Summarize the businesss sold items quantity, income and average income rate of each item during a specific point in time.',
'Summarize the businesss sold items quantity, income and average income rate of each item during a specific point in time.',
link: '/financial-reports/sales-by-items', link: '/financial-reports/sales-by-items',
}, },
{ {
title: 'Inventory valuation', title: 'Inventory valuation',
desc: desc: 'Summarize the businesss purchase items quantity, cost and average cost rate of each item during a specific point in time.',
'Summarize the businesss purchase items quantity, cost and average cost rate of each item during a specific point in time.',
link: '/financial-reports/inventory-valuation', link: '/financial-reports/inventory-valuation',
}, },
{ {
@@ -90,6 +85,11 @@ export const SalesAndPurchasesReportMenus = [
desc: 'Reports every transaction going in and out of each vendor/supplier.', desc: 'Reports every transaction going in and out of each vendor/supplier.',
link: '/financial-reports/transactions-by-vendors', link: '/financial-reports/transactions-by-vendors',
}, },
{
title: 'Inventory Item details',
desc: 'Reports every transaction going in and out of your items to monitoring activity of items.',
link: '/financial-reports/inventory-item-details',
},
], ],
}, },
]; ];

View File

@@ -0,0 +1,88 @@
import React, { useState, useEffect } from 'react';
import moment from 'moment';
import 'style/pages/FinancialStatements/CashFlowStatement.scss';
import { FinancialStatement } from 'components';
import DashboardPageContent from 'components/Dashboard/DashboardPageContent';
import CashFlowStatementHeader from './CashFlowStatementHeader';
import CashFlowStatementTable from './CashFlowStatementTable';
import CashFlowStatementActionsBar from './CashFlowStatementActionsBar';
import withSettings from 'containers/Settings/withSettings';
import withCashFlowStatementActions from './withCashFlowStatementActions';
import { CashFlowStatementProvider } from './CashFlowStatementProvider';
import { CashFlowStatementLoadingBar } from './components';
import { compose } from 'utils';
/**
* Cash flow statement.
*/
function CashFlowStatement({
// #withPreferences
organizationName,
//#withCashStatementActions
toggleCashFlowStatementFilterDrawer,
}) {
// filter
const [filter, setFilter] = useState({
fromDate: moment().startOf('year').format('YYYY-MM-DD'),
toDate: moment().endOf('year').format('YYYY-MM-DD'),
basis: 'cash',
displayColumnsType: 'total',
});
// Handle refetch cash flow after filter change.
const handleFilterSubmit = (filter) => {
const _filter = {
...filter,
fromDate: moment(filter.fromDate).format('YYYY-MM-DD'),
toDate: moment(filter.toDate).format('YYYY-MM-DD'),
};
setFilter({ ..._filter });
};
// Handle format number submit.
const handleNumberFormatSubmit = (values) => {
setFilter({
...filter,
numberFormat: values,
});
};
useEffect(
() => () => {
toggleCashFlowStatementFilterDrawer(false);
},
[toggleCashFlowStatementFilterDrawer],
);
return (
<CashFlowStatementProvider filter={filter}>
<CashFlowStatementActionsBar
numberFormat={filter.numberFormat}
onNumberFormatSubmit={handleNumberFormatSubmit}
/>
<CashFlowStatementLoadingBar />
<DashboardPageContent>
<FinancialStatement>
<CashFlowStatementHeader
pageFilter={filter}
onSubmitFilter={handleFilterSubmit}
/>
<div class="financial-statement__body">
<CashFlowStatementTable companyName={organizationName} />
</div>
</FinancialStatement>
</DashboardPageContent>
</CashFlowStatementProvider>
);
}
export default compose(
withSettings(({ organizationSettings }) => ({
organizationName: organizationSettings?.name,
})),
withCashFlowStatementActions,
)(CashFlowStatement);

View File

@@ -0,0 +1,134 @@
import React from 'react';
import {
NavbarGroup,
NavbarDivider,
Button,
Classes,
Popover,
PopoverInteractionKind,
Position,
} from '@blueprintjs/core';
import { FormattedMessage as T } from 'react-intl';
import classNames from 'classnames';
import { Icon } from 'components';
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
import NumberFormatDropdown from 'components/NumberFormatDropdown';
import { useCashFlowStatementContext } from './CashFlowStatementProvider';
import withCashFlowStatement from './withCashFlowStatement';
import withCashFlowStatementActions from './withCashFlowStatementActions';
import { compose, saveInvoke } from 'utils';
/**
* Cash flow statement actions bar.
*/
function CashFlowStatementActionsBar({
//#withCashFlowStatement
isFilterDrawerOpen,
//#withCashStatementActions
toggleCashFlowStatementFilterDrawer,
//#ownProps
numberFormat,
onNumberFormatSubmit,
}) {
const { isCashFlowLoading, refetchCashFlow } = useCashFlowStatementContext();
// Handle filter toggle click.
const handleFilterToggleClick = () => {
toggleCashFlowStatementFilterDrawer();
};
// Handle recalculate report button.
const handleRecalculateReport = () => {
refetchCashFlow();
};
// handle number format form submit.
const handleNumberFormatSubmit = (values) =>
saveInvoke(onNumberFormatSubmit, values);
return (
<DashboardActionsBar>
<NavbarGroup>
<Button
className={classNames(Classes.MINIMAL, 'button--gray-highlight')}
text={<T id={'recalc_report'} />}
onClick={handleRecalculateReport}
icon={<Icon icon="refresh-16" iconSize={16} />}
/>
<NavbarDivider />
<Button
className={classNames(Classes.MINIMAL, 'button--table-views')}
icon={<Icon icon="cog-16" iconSize={16} />}
text={
isFilterDrawerOpen ? (
<T id={'hide_customizer'} />
) : (
<T id={'customize_report'} />
)
}
onClick={handleFilterToggleClick}
active={isFilterDrawerOpen}
/>
<NavbarDivider />
<Popover
content={
<NumberFormatDropdown
numberFormat={numberFormat}
onSubmit={handleNumberFormatSubmit}
submitDisabled={isCashFlowLoading}
/>
}
minimal={true}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
>
<Button
className={classNames(Classes.MINIMAL, 'button--filter')}
text={<T id={'format'} />}
icon={<Icon icon="numbers" width={23} height={16} />}
/>
</Popover>
<Popover
// content={}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
>
<Button
className={classNames(Classes.MINIMAL, 'button--filter')}
text={<T id={'filter'} />}
icon={<Icon icon="filter-16" iconSize={16} />}
/>
</Popover>
<NavbarDivider />
<Button
className={Classes.MINIMAL}
icon={<Icon icon="print-16" iconSize={16} />}
text={<T id={'print'} />}
/>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="file-export-16" iconSize={16} />}
text={<T id={'export'} />}
/>
</NavbarGroup>
</DashboardActionsBar>
);
}
export default compose(
withCashFlowStatement(({ cashFlowStatementDrawerFilter }) => ({
isFilterDrawerOpen: cashFlowStatementDrawerFilter,
})),
withCashFlowStatementActions,
)(CashFlowStatementActionsBar);

View File

@@ -0,0 +1,20 @@
import React from 'react';
import FinancialStatementDateRange from 'containers/FinancialStatements/FinancialStatementDateRange';
import FinancialAccountsFilter from '../FinancialAccountsFilter';
import RadiosAccountingBasis from '../RadiosAccountingBasis';
import SelectDisplayColumnsBy from '../SelectDisplayColumnsBy';
/**
* Cash flow statement header - General panel.
*/
export default function CashFlowStatementHeaderGeneralPanel() {
return (
<div>
<FinancialStatementDateRange />
<SelectDisplayColumnsBy />
<FinancialAccountsFilter initialSelectedItem={'all-accounts'} />
<RadiosAccountingBasis key={'basis'} />
</div>
);
}

View File

@@ -0,0 +1,102 @@
import React from 'react';
import { Tabs, Tab, Button, Intent } from '@blueprintjs/core';
import { FormattedMessage as T, useIntl } from 'react-intl';
import moment from 'moment';
import * as Yup from 'yup';
import { Formik, Form } from 'formik';
import FinancialStatementHeader from 'containers/FinancialStatements/FinancialStatementHeader';
import CashFlowStatementGeneralPanel from './CashFlowStatementGeneralPanel';
import withCashFlowStatement from './withCashFlowStatement';
import withCashFlowStatementActions from './withCashFlowStatementActions';
import { compose } from 'utils';
/**
* Cash flow statement header.
*/
function CashFlowStatementHeader({
// #ownProps
onSubmitFilter,
pageFilter,
//#withCashFlowStatement
isFilterDrawerOpen,
//#withCashStatementActions
toggleCashFlowStatementFilterDrawer,
}) {
const { formatMessage } = useIntl();
// filter form initial values.
const initialValues = {
...pageFilter,
fromDate: moment(pageFilter.fromDate).toDate(),
toDate: moment(pageFilter.toDate).toDate(),
};
// Validation schema.
const validationSchema = Yup.object().shape({
dateRange: Yup.string().optional(),
fromDate: Yup.date()
.required()
.label(formatMessage({ id: 'fromDate' })),
toDate: Yup.date()
.min(Yup.ref('fromDate'))
.required()
.label(formatMessage({ id: 'toDate' })),
displayColumnsType: Yup.string(),
});
// Handle form submit.
const handleSubmit = (values, { setSubmitting }) => {
onSubmitFilter(values);
toggleCashFlowStatementFilterDrawer(false);
setSubmitting(false);
};
// Handle drawer close action.
const handleDrawerClose = () => {
toggleCashFlowStatementFilterDrawer(false);
};
return (
<FinancialStatementHeader
isOpen={isFilterDrawerOpen}
drawerProps={{ onClose: handleDrawerClose }}
>
<Formik
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={handleSubmit}
>
<Form>
<Tabs animate={true} vertical={true} renderActiveTabPanelOnly={true}>
<Tab
id="general"
title={<T id={'general'} />}
panel={<CashFlowStatementGeneralPanel />}
/>
</Tabs>
<div class="financial-header-drawer__footer">
<Button className={'mr1'} intent={Intent.PRIMARY} type={'submit'}>
<T id={'calculate_report'} />
</Button>
<Button onClick={handleDrawerClose} minimal={true}>
<T id={'cancel'} />
</Button>
</div>
</Form>
</Formik>
</FinancialStatementHeader>
);
}
export default compose(
withCashFlowStatement(({ cashFlowStatementDrawerFilter }) => ({
isFilterDrawerOpen: cashFlowStatementDrawerFilter,
})),
withCashFlowStatementActions,
)(CashFlowStatementHeader);

View File

@@ -0,0 +1,45 @@
import React from 'react';
import FinancialReportPage from '../FinancialReportPage';
import { useCashFlowStatementReport } from 'hooks/query';
import { transformFilterFormToQuery } from '../common';
const CashFLowStatementContext = React.createContext();
/**
* Cash flow statement provider.
*/
function CashFlowStatementProvider({ filter, ...props }) {
// transforms the given filter to query.
const query = React.useMemo(
() => transformFilterFormToQuery(filter),
[filter],
);
// fetch the cash flow statement report.
const {
data: cashFlowStatement,
isFetching: isCashFlowFetching,
isLoading: isCashFlowLoading,
refetch: refetchCashFlow,
} = useCashFlowStatementReport(query, { keepPreviousData: true });
const provider = {
cashFlowStatement,
isCashFlowFetching,
isCashFlowLoading,
refetchCashFlow,
query,
filter,
};
return (
<FinancialReportPage name="cash-flow-statement">
<CashFLowStatementContext.Provider value={provider} {...props} />
</FinancialReportPage>
);
}
const useCashFlowStatementContext = () =>
React.useContext(CashFLowStatementContext);
export { CashFlowStatementProvider, useCashFlowStatementContext };

View File

@@ -0,0 +1,63 @@
import React, { useMemo } from 'react';
import { useIntl } from 'react-intl';
import { DataTable } from 'components';
import FinancialSheet from 'components/FinancialSheet';
import { useCashFlowStatementColumns } from './components';
import { useCashFlowStatementContext } from './CashFlowStatementProvider';
import { defaultExpanderReducer } from 'utils';
/**
* Cash flow statement table.
*/
export default function CashFlowStatementTable({
// #ownProps
companyName,
}) {
const { formatMessage } = useIntl();
const {
cashFlowStatement: { tableRows },
isCashFlowLoading,
query,
} = useCashFlowStatementContext();
const columns = useCashFlowStatementColumns();
const expandedRows = useMemo(
() => defaultExpanderReducer(tableRows, 4),
[tableRows],
);
const rowClassNames = (row) => {
return [
`row-type--${row.original.rowTypes}`,
`row-type--${row.original.id}`,
];
};
return (
<FinancialSheet
name="cash-flow-statement"
companyName={companyName}
sheetType={formatMessage({ id: 'statement_of_cash_flow' })}
loading={isCashFlowLoading}
fromDate={query.from_date}
toDate={query.to_date}
basis={query.basis}
>
<DataTable
className="bigcapital-datatable--financial-report"
columns={columns}
data={tableRows}
rowClassNames={rowClassNames}
noInitialFetch={true}
expandable={true}
expanded={expandedRows}
expandToggleColumn={1}
expandColumnSpace={0.8}
/>
</FinancialSheet>
);
}

View File

@@ -0,0 +1,29 @@
import React from 'react';
import { If } from 'components';
import { dynamicColumns } from './utils';
import { useCashFlowStatementContext } from './CashFlowStatementProvider';
import FinancialLoadingBar from '../FinancialLoadingBar';
/**
* Retrieve cash flow statement columns.
*/
export const useCashFlowStatementColumns = () => {
const {
cashFlowStatement: { columns, data },
} = useCashFlowStatementContext();
return React.useMemo(() => dynamicColumns(columns, data), [columns, data]);
};
/**
* Cash flow statement loading bar.
*/
export function CashFlowStatementLoadingBar() {
const { isCashFlowLoading } = useCashFlowStatementContext();
return (
<If condition={isCashFlowLoading}>
<FinancialLoadingBar />
</If>
);
}

View File

@@ -0,0 +1,68 @@
import * as R from 'ramda';
import { CellTextSpan } from 'components/Datatable/Cells';
import { getColumnWidth } from 'utils';
import { formatMessage } from 'services/intl';
/**
* Account name column mapper.
*/
const accountNameMapper = (column) => ({
id: column.key,
key: column.key,
Header: formatMessage({ id: 'account_name' }),
accessor: 'cells[0].value',
className: 'account_name',
textOverview: true,
width: 240,
disableSortBy: true,
});
/**
* Date range columns mapper.
*/
const dateRangeMapper = (data, index, column) => ({
id: column.key,
Header: column.label,
key: column.key,
accessor: `cells[${index}].value`,
width: getColumnWidth(data, `cells.${index}.value`, { minWidth: 100 }),
className: `date-period ${column.key}`,
disableSortBy: true,
});
/**
* Total column mapper.
*/
const totalMapper = (data, index, column) => ({
key: 'total',
Header: formatMessage({ id: 'total' }),
accessor: `cells[${index}].value`,
className: 'total',
textOverview: true,
Cell: CellTextSpan,
width: getColumnWidth(data, `cells[${index}].value`, { minWidth: 100 }),
disableSortBy: true,
});
/**
* Detarmines the given string starts with `date-range` string.
*/
const isMatchesDateRange = (r) => R.match(/^date-range/g, r).length > 0;
/**
* Cash flow dynamic columns.
*/
export const dynamicColumns = (columns, data) => {
const mapper = (column, index) => {
return R.compose(
R.when(
R.pathSatisfies(isMatchesDateRange, ['key']),
R.curry(dateRangeMapper)(data, index),
),
R.when(R.pathEq(['key'], 'name'), accountNameMapper),
R.when(R.pathEq(['key'], 'total'), R.curry(totalMapper)(data, index)),
)(column);
};
return columns.map(mapper);
};

View File

@@ -0,0 +1,12 @@
import { connect } from 'react-redux';
import { getCashFlowStatementFilterDrawer } from 'store/financialStatement/financialStatements.selectors';
export default (mapState) => {
const mapStateToProps = (state, props) => {
const mapped = {
cashFlowStatementDrawerFilter: getCashFlowStatementFilterDrawer(state),
};
return mapState ? mapState(mapped, state, props) : mapped;
};
return connect(mapStateToProps);
};

View File

@@ -0,0 +1,9 @@
import { connect } from 'react-redux';
import { toggleCashFlowStatementFilterDrawer } from 'store/financialStatement/financialStatements.actions';
const mapDispatchToProps = (dispatch) => ({
toggleCashFlowStatementFilterDrawer: (toggle) =>
dispatch(toggleCashFlowStatementFilterDrawer(toggle)),
});
export default connect(null, mapDispatchToProps);

View File

@@ -9,6 +9,7 @@ import { CellTextSpan } from 'components/Datatable/Cells';
/** /**
* Retrieve customers transactions columns. * Retrieve customers transactions columns.
*/ */
export const useCustomersTransactionsColumns = () => { export const useCustomersTransactionsColumns = () => {
const { const {
customersTransactions: { tableRows }, customersTransactions: { tableRows },

View File

@@ -0,0 +1,83 @@
import React, { useEffect, useState } from 'react';
import moment from 'moment';
import 'style/pages/FinancialStatements/InventoryItemDetails.scss';
import { FinancialStatement } from 'components';
import DashboardPageContent from 'components/Dashboard/DashboardPageContent';
import InventoryItemDetailsActionsBar from './InventoryItemDetailsActionsBar';
import InventoryItemDetailsHeader from './InventoryItemDetailsHeader';
import InventoryItemDetailsTable from './InventoryItemDetailsTable';
import withInventoryItemDetailsActions from './withInventoryItemDetailsActions';
import withSettings from 'containers/Settings/withSettings';
import { InventoryItemDetailsProvider } from './InventoryItemDetailsProvider';
import { InventoryItemDetailsLoadingBar } from './components';
import { compose } from 'utils';
/**
* inventory item details.
*/
function InventoryItemDetails({
// #withSettings
organizationName,
//#withInventoryItemDetailsActions
toggleInventoryItemDetailsFilterDrawer: toggleFilterDrawer,
}) {
const [filter, setFilter] = useState({
fromDate: moment().startOf('year').format('YYYY-MM-DD'),
toDate: moment().endOf('year').format('YYYY-MM-DD'),
});
const handleFilterSubmit = (filter) => {
const _filter = {
...filter,
fromDate: moment(filter.fromDate).format('YYYY-MM-DD'),
toDate: moment(filter.toDate).format('YYYY-MM-DD'),
};
setFilter({ ..._filter });
};
// Handle number format submit.
const handleNumberFormatSubmit = (values) => {
setFilter({
...filter,
numberFormat: values,
});
};
useEffect(() => () => toggleFilterDrawer(false), [toggleFilterDrawer]);
return (
<InventoryItemDetailsProvider filter={filter}>
<InventoryItemDetailsActionsBar
numberFormat={filter.numberFormat}
onNumberFormatSubmit={handleNumberFormatSubmit}
/>
<InventoryItemDetailsLoadingBar />
<DashboardPageContent>
<FinancialStatement>
<div className={'financial-statement--inventory-details'}>
<InventoryItemDetailsHeader
pageFilter={filter}
onSubmitFilter={handleFilterSubmit}
/>
</div>
<div class="financial-statement__body">
<InventoryItemDetailsTable companyName={organizationName} />
</div>
</FinancialStatement>
</DashboardPageContent>
</InventoryItemDetailsProvider>
);
}
export default compose(
withSettings(({ organizationSettings }) => ({
organizationName: organizationSettings?.name,
})),
withInventoryItemDetailsActions,
)(InventoryItemDetails);

View File

@@ -0,0 +1,131 @@
import React from 'react';
import {
NavbarGroup,
Button,
Classes,
NavbarDivider,
Popover,
PopoverInteractionKind,
Position,
} from '@blueprintjs/core';
import { FormattedMessage as T } from 'react-intl';
import classNames from 'classnames';
import { Icon } from 'components';
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
import NumberFormatDropdown from 'components/NumberFormatDropdown';
import { useInventoryItemDetailsContext } from './InventoryItemDetailsProvider';
import withInventoryItemDetails from './withInventoryItemDetails';
import withInventoryItemDetailsActions from './withInventoryItemDetailsActions';
import { compose, saveInvoke } from 'utils';
/**
* Inventory item details actions bar.
*/
function InventoryItemDetailsActionsBar({
// #ownProps
numberFormat,
onNumberFormatSubmit,
//#withInventoryItemDetails
isFilterDrawerOpen,
//#withInventoryItemDetailsActions
toggleInventoryItemDetailsFilterDrawer: toggleFilterDrawer,
}) {
const { isInventoryItemDetailsLoading, inventoryItemDetailsRefetch } =
useInventoryItemDetailsContext();
// Handle filter toggle click.
const handleFilterToggleClick = () => {
toggleFilterDrawer();
};
//Handle recalculate the report button.
const handleRecalcReport = () => {
inventoryItemDetailsRefetch();
};
// Handle number format form submit.
const handleNumberFormatSubmit = (values) => {
saveInvoke(onNumberFormatSubmit, values);
};
return (
<DashboardActionsBar>
<NavbarGroup>
<Button
className={classNames(Classes.MINIMAL, 'button--gray-highlight')}
text={<T id={'recalc_report'} />}
onClick={handleRecalcReport}
icon={<Icon icon="refresh-16" iconSize={16} />}
/>
<NavbarDivider />
<Button
className={classNames(Classes.MINIMAL, 'button--table-views')}
icon={<Icon icon="cog-16" iconSize={16} />}
text={
isFilterDrawerOpen ? (
<T id={'hide_customizer'} />
) : (
<T id={'customize_report'} />
)
}
onClick={handleFilterToggleClick}
active={isFilterDrawerOpen}
/>
<NavbarDivider />
<Popover
content={
<NumberFormatDropdown
numberFormat={numberFormat}
onSubmit={handleNumberFormatSubmit}
submitDisabled={isInventoryItemDetailsLoading}
/>
}
minimal={true}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
>
<Button
className={classNames(Classes.MINIMAL, 'button--filter')}
text={<T id={'format'} />}
icon={<Icon icon="numbers" width={23} height={16} />}
/>
</Popover>
<Popover
// content={}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
>
<Button
className={classNames(Classes.MINIMAL, 'button--filter')}
text={<T id={'filter'} />}
icon={<Icon icon="filter-16" iconSize={16} />}
/>
</Popover>
<NavbarDivider />
<Button
className={Classes.MINIMAL}
icon={<Icon icon="print-16" iconSize={16} />}
text={<T id={'print'} />}
/>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="file-export-16" iconSize={16} />}
text={<T id={'export'} />}
/>
</NavbarGroup>
</DashboardActionsBar>
);
}
export default compose(
withInventoryItemDetails(({ inventoryItemDetailDrawerFilter }) => ({
isFilterDrawerOpen: inventoryItemDetailDrawerFilter,
})),
withInventoryItemDetailsActions,
)(InventoryItemDetailsActionsBar);

View File

@@ -0,0 +1,99 @@
import React from 'react';
import * as Yup from 'yup';
import moment from 'moment';
import { Formik, Form } from 'formik';
import { Tabs, Tab, Button, Intent } from '@blueprintjs/core';
import { FormattedMessage as T, useIntl } from 'react-intl';
import FinancialStatementHeader from 'containers/FinancialStatements/FinancialStatementHeader';
import InventoryItemDetailsHeaderGeneralPanel from './InventoryItemDetailsHeaderGeneralPanel';
import withInventoryItemDetails from './withInventoryItemDetails';
import withInventoryItemDetailsActions from './withInventoryItemDetailsActions';
import { compose } from 'utils';
/**
* Inventory item details header.
*/
function InventoryItemDetailsHeader({
// #ownProps
onSubmitFilter,
pageFilter,
//#withInventoryItemDetails
isFilterDrawerOpen,
//#withInventoryItemDetailsActions
toggleInventoryItemDetailsFilterDrawer: toggleFilterDrawer,
}) {
const { formatMessage } = useIntl();
//Filter form initial values.
const initialValues = {
...pageFilter,
fromDate: moment(pageFilter.fromDate).toDate(),
toDate: moment(pageFilter.toDate).toDate(),
};
// Validation schema.
const validationSchema = Yup.object().shape({
fromDate: Yup.date()
.required()
.label(formatMessage({ id: 'fromDate' })),
toDate: Yup.date()
.min(Yup.ref('fromDate'))
.required()
.label(formatMessage({ id: 'toDate' })),
});
;
// Handle form submit.
const handleSubmit = (values, { setSubmitting }) => {
onSubmitFilter(values);
toggleFilterDrawer(false);
setSubmitting(false);
};
// Handle drawer close action.
const handleDrawerClose = () => {
toggleFilterDrawer(false);
};
return (
<FinancialStatementHeader
isOpen={isFilterDrawerOpen}
drawerProps={{ onClose: handleDrawerClose }}
>
<Formik
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={handleSubmit}
>
<Form>
<Tabs animate={true} vertical={true} renderActiveTabPanelOnly={true}>
<Tab
id="general"
title={<T id={'general'} />}
panel={<InventoryItemDetailsHeaderGeneralPanel />}
/>
</Tabs>
<div class="financial-header-drawer__footer">
<Button className={'mr1'} intent={Intent.PRIMARY} type={'submit'}>
<T id={'calculate_report'} />
</Button>
<Button onClick={handleDrawerClose} minimal={true}>
<T id={'cancel'} />
</Button>
</div>
</Form>
</Formik>
</FinancialStatementHeader>
);
}
export default compose(
withInventoryItemDetails(({ inventoryItemDetailDrawerFilter }) => ({
isFilterDrawerOpen: inventoryItemDetailDrawerFilter,
})),
withInventoryItemDetailsActions,
)(InventoryItemDetailsHeader);

View File

@@ -0,0 +1,13 @@
import React from 'react';
import FinancialStatementDateRange from 'containers/FinancialStatements/FinancialStatementDateRange';
/**
* Inventory item details header - General panel.
*/
export default function InventoryItemDetailsHeaderGeneralPanel() {
return (
<div>
<FinancialStatementDateRange />
</div>
);
}

View File

@@ -0,0 +1,43 @@
import React from 'react';
import FinancialReportPage from '../FinancialReportPage';
import { useInventoryItemDetailsReport } from 'hooks/query';
import { transformFilterFormToQuery } from '../common';
const InventoryItemDetailsContext = React.createContext();
/**
* Inventory item details provider.
*/
function InventoryItemDetailsProvider({ filter, ...props }) {
const query = React.useMemo(
() => transformFilterFormToQuery(filter),
[filter],
);
// fetch inventory item details.
const {
data: inventoryItemDetails,
isFetching: isInventoryItemDetailsFetching,
isLoading: isInventoryItemDetailsLoading,
refetch: inventoryItemDetailsRefetch,
} = useInventoryItemDetailsReport(query, { keepPreviousData: true });
const provider = {
inventoryItemDetails,
isInventoryItemDetailsFetching,
isInventoryItemDetailsLoading,
inventoryItemDetailsRefetch,
query,
filter,
};
return (
<FinancialReportPage name={'inventory-item-details'}>
<InventoryItemDetailsContext.Provider value={provider} {...props} />
</FinancialReportPage>
);
}
const useInventoryItemDetailsContext = () =>
React.useContext(InventoryItemDetailsContext);
export { InventoryItemDetailsProvider, useInventoryItemDetailsContext };

View File

@@ -0,0 +1,59 @@
import React, { useMemo, useCallback } from 'react';
import { formatMessage } from 'services/intl';
import classNames from 'classnames';
import FinancialSheet from 'components/FinancialSheet';
import { DataTable } from 'components';
import { useInventoryItemDetailsColumns } from './components';
import { useInventoryItemDetailsContext } from './InventoryItemDetailsProvider';
import { defaultExpanderReducer } from 'utils';
/**
* Inventory item detail table.
*/
export default function InventoryItemDetailsTable({
// #ownProps
companyName,
}) {
const {
inventoryItemDetails: { tableRows },
isInventoryItemDetailsLoading,
query,
} = useInventoryItemDetailsContext();
const columns = useInventoryItemDetailsColumns();
const expandedRows = useMemo(
() => defaultExpanderReducer(tableRows, 4),
[tableRows],
);
const rowClassNames = (row) => {
return [`row-type--${row.original.rowTypes}`];
};
return (
<FinancialSheet
name="inventory-item-details"
companyName={companyName}
sheetType={formatMessage({ id: 'inventory_item_details' })}
loading={isInventoryItemDetailsLoading}
fromDate={query.from_date}
toDate={query.to_date}
>
<DataTable
className="bigcapital-datatable--financial-report"
columns={columns}
data={tableRows}
rowClassNames={rowClassNames}
noInitialFetch={true}
expandable={true}
expanded={expandedRows}
expandToggleColumn={1}
expandColumnSpace={0.8}
/>
</FinancialSheet>
);
}

View File

@@ -0,0 +1,29 @@
import React from 'react';
import { If } from 'components';
import { dynamicColumns } from './utils';
import FinancialLoadingBar from '../FinancialLoadingBar';
import { useInventoryItemDetailsContext } from './InventoryItemDetailsProvider';
/**
* Retrieve inventory item details columns.
*/
export const useInventoryItemDetailsColumns = () => {
const {
inventoryItemDetails: { columns, data },
} = useInventoryItemDetailsContext();
return React.useMemo(() => dynamicColumns(columns, data), [columns, data]);
};
/**
* Cash inventory item details loading bar.
*/
export function InventoryItemDetailsLoadingBar() {
const { isInventoryItemDetailsLoading } = useInventoryItemDetailsContext();
return (
<If condition={isInventoryItemDetailsLoading}>
<FinancialLoadingBar />
</If>
);
}

View File

@@ -0,0 +1,42 @@
import React from 'react';
import * as R from 'ramda';
import { getColumnWidth, getForceWidth } from 'utils';
/**
* columns mapper.
*/
const columnsMapper = (data, index, column) => ({
id: column.key,
key: column.key,
Header: column.label,
accessor: ({ cells }) => {
return (
<span
className={'force-width'}
style={{
minWidth: getForceWidth(cells[0].value),
}}
>
{cells[index]?.value}
</span>
);
},
className: column.key,
width: getColumnWidth(data, `cells.${index}.key`, {
minWidth: 130,
magicSpacing: 10,
}),
disableSortBy: true,
});
/**
* Inventory item details columns.
*/
export const dynamicColumns = (columns, data) => {
const mapper = (column, index) => {
return R.compose(
R.when(R.pathEq(['key']), R.curry(columnsMapper)(data, index)),
)(column);
};
return columns.map(mapper);
};

View File

@@ -0,0 +1,15 @@
import { connect } from 'react-redux';
import { getInventoryItemDetailsFilterDrawer } from 'store/financialStatement/financialStatements.selectors';
export default (mapState) => {
const mapStateToProps = (state, props) => {
const mapped = {
inventoryItemDetailDrawerFilter: getInventoryItemDetailsFilterDrawer(
state,
props,
),
};
return mapState ? mapState(mapped, state, props) : mapped;
};
return connect(mapStateToProps);
};

View File

@@ -0,0 +1,9 @@
import { connect } from 'react-redux';
import { toggleInventoryItemDetailsFilterDrawer } from 'store/financialStatement/financialStatements.actions';
const mapActionsToProps = (dispatch) => ({
toggleInventoryItemDetailsFilterDrawer: (toggle) =>
dispatch(toggleInventoryItemDetailsFilterDrawer(toggle)),
});
export default connect(null, mapActionsToProps);

View File

@@ -402,3 +402,63 @@ export function useVendorsTransactionsReport(query, props) {
}, },
); );
} }
/**
* Retrieve cash flow statement report.
*/
export function useCashFlowStatementReport(query, props) {
return useRequestQuery(
[t.FINANCIAL_REPORT, t.CASH_FLOW_STATEMENT, query],
{
method: 'get',
url: '/financial_statements/cash-flow',
params: query,
headers: {
Accept: 'application/json+table',
},
},
{
select: (res) => ({
columns: res.data.table.columns,
data: res.data.table.data,
tableRows: res.data.table.data,
}),
defaultData: {
tableRows: [],
data: [],
columns: [],
},
...props,
},
);
}
/**
* Retrieve inventory item detail report.
*/
export function useInventoryItemDetailsReport(query, props) {
return useRequestQuery(
[t.FINANCIAL_REPORT, t.INVENTORY_ITEM_DETAILS, query],
{
method: 'get',
url: '/financial_statements/inventory-item-details',
params: query,
headers: {
Accept: 'application/json+table',
},
},
{
select: (res) => ({
columns: res.data.table.columns,
data: res.data.table.data,
tableRows: res.data.table.data,
}),
defaultData: {
tableRows: [],
data: [],
columns: [],
},
...props,
},
);
}

View File

@@ -20,7 +20,9 @@ const FINANCIAL_REPORTS = {
CUSTOMERS_BALANCE_SUMMARY: 'CUSTOMERS_BALANCE_SUMMARY', CUSTOMERS_BALANCE_SUMMARY: 'CUSTOMERS_BALANCE_SUMMARY',
SALES_BY_ITEMS: 'SALES_BY_ITEMS', SALES_BY_ITEMS: 'SALES_BY_ITEMS',
PURCHASES_BY_ITEMS: 'PURCHASES_BY_ITEMS', PURCHASES_BY_ITEMS: 'PURCHASES_BY_ITEMS',
INVENTORY_VALUATION: 'INVENTORY_VALUATION' INVENTORY_VALUATION: 'INVENTORY_VALUATION',
CASH_FLOW_STATEMENT: 'CASH_FLOW_STATEMENT',
INVENTORY_ITEM_DETAILS:'INVENTORY_ITEM_DETAILS'
}; };
const BILLS = { const BILLS = {

View File

@@ -1064,4 +1064,7 @@ export default {
vendors_transactions: 'Vendors Transactions', vendors_transactions: 'Vendors Transactions',
reference_type: 'Reference type', reference_type: 'Reference type',
transaction_number: 'Transaction number', transaction_number: 'Transaction number',
cash_flow_statement: 'Cash Flow Statement',
statement_of_cash_flow: 'Statement of Cash Flow ',
inventory_item_details:'Inventory Item Details'
}; };

View File

@@ -112,8 +112,7 @@ export default [
import('containers/FinancialStatements/GeneralLedger/GeneralLedger'), import('containers/FinancialStatements/GeneralLedger/GeneralLedger'),
), ),
breadcrumb: 'General Ledger', breadcrumb: 'General Ledger',
hint: hint: 'Reports every transaction going in and out of your accounts and organized by accounts and date to monitoring activity of accounts.',
'Reports every transaction going in and out of your accounts and organized by accounts and date to monitoring activity of accounts.',
hotkey: 'shift+4', hotkey: 'shift+4',
pageTitle: formatMessage({ id: 'general_ledger' }), pageTitle: formatMessage({ id: 'general_ledger' }),
backLink: true, backLink: true,
@@ -125,8 +124,7 @@ export default [
import('containers/FinancialStatements/BalanceSheet/BalanceSheet'), import('containers/FinancialStatements/BalanceSheet/BalanceSheet'),
), ),
breadcrumb: 'Balance Sheet', breadcrumb: 'Balance Sheet',
hint: hint: "Reports a company's assets, liabilities and shareholders' equity at a specific point in time with comparison period(s).",
"Reports a company's assets, liabilities and shareholders' equity at a specific point in time with comparison period(s).",
hotkey: 'shift+1', hotkey: 'shift+1',
pageTitle: formatMessage({ id: 'balance_sheet' }), pageTitle: formatMessage({ id: 'balance_sheet' }),
backLink: true, backLink: true,
@@ -140,8 +138,7 @@ export default [
), ),
), ),
breadcrumb: 'Trial Balance Sheet', breadcrumb: 'Trial Balance Sheet',
hint: hint: 'Summarizes the credit and debit balance of each account in your chart of accounts at a specific point in time. ',
'Summarizes the credit and debit balance of each account in your chart of accounts at a specific point in time. ',
hotkey: 'shift+5', hotkey: 'shift+5',
pageTitle: formatMessage({ id: 'trial_balance_sheet' }), pageTitle: formatMessage({ id: 'trial_balance_sheet' }),
backLink: true, backLink: true,
@@ -153,8 +150,7 @@ export default [
import('containers/FinancialStatements/ProfitLossSheet/ProfitLossSheet'), import('containers/FinancialStatements/ProfitLossSheet/ProfitLossSheet'),
), ),
breadcrumb: 'Profit Loss Sheet', breadcrumb: 'Profit Loss Sheet',
hint: hint: 'Reports the revenues, costs and expenses incurred during a specific point in time with comparison period(s).',
'Reports the revenues, costs and expenses incurred during a specific point in time with comparison period(s).',
hotkey: 'shift+2', hotkey: 'shift+2',
pageTitle: formatMessage({ id: 'profit_loss_sheet' }), pageTitle: formatMessage({ id: 'profit_loss_sheet' }),
backLink: true, backLink: true,
@@ -166,8 +162,7 @@ export default [
import('containers/FinancialStatements/ARAgingSummary/ARAgingSummary'), import('containers/FinancialStatements/ARAgingSummary/ARAgingSummary'),
), ),
breadcrumb: 'Receivable Aging Summary', breadcrumb: 'Receivable Aging Summary',
hint: hint: 'Summarize total unpaid balances of customers invoices with number of days the unpaid invoice is overdue.',
'Summarize total unpaid balances of customers invoices with number of days the unpaid invoice is overdue.',
pageTitle: formatMessage({ id: 'receivable_aging_summary' }), pageTitle: formatMessage({ id: 'receivable_aging_summary' }),
backLink: true, backLink: true,
sidebarExpand: false, sidebarExpand: false,
@@ -178,8 +173,7 @@ export default [
import('containers/FinancialStatements/APAgingSummary/APAgingSummary'), import('containers/FinancialStatements/APAgingSummary/APAgingSummary'),
), ),
breadcrumb: 'Payable Aging Summary', breadcrumb: 'Payable Aging Summary',
hint: hint: 'Summarize total unpaid balances of vendors purchase invoices with the number of days the unpaid invoice is overdue.',
'Summarize total unpaid balances of vendors purchase invoices with the number of days the unpaid invoice is overdue.',
pageTitle: formatMessage({ id: 'payable_aging_summary' }), pageTitle: formatMessage({ id: 'payable_aging_summary' }),
backLink: true, backLink: true,
sidebarExpand: false, sidebarExpand: false,
@@ -190,8 +184,7 @@ export default [
import('containers/FinancialStatements/Journal/Journal'), import('containers/FinancialStatements/Journal/Journal'),
), ),
breadcrumb: 'Journal Sheet', breadcrumb: 'Journal Sheet',
hint: hint: 'The debit and credit entries of system transactions, sorted by date.',
'The debit and credit entries of system transactions, sorted by date.',
hotkey: 'shift+3', hotkey: 'shift+3',
pageTitle: formatMessage({ id: 'journal_sheet' }), pageTitle: formatMessage({ id: 'journal_sheet' }),
sidebarExpand: false, sidebarExpand: false,
@@ -217,8 +210,7 @@ export default [
), ),
breadcrumb: 'Sales by Items', breadcrumb: 'Sales by Items',
pageTitle: formatMessage({ id: 'sales_by_items' }), pageTitle: formatMessage({ id: 'sales_by_items' }),
hint: hint: 'Summarize the businesss sold items quantity, income and average income rate of each item during a specific point in time.',
'Summarize the businesss sold items quantity, income and average income rate of each item during a specific point in time.',
backLink: true, backLink: true,
sidebarExpand: false, sidebarExpand: false,
}, },
@@ -230,8 +222,7 @@ export default [
), ),
), ),
breadcrumb: 'Inventory Valuation ', breadcrumb: 'Inventory Valuation ',
hint: hint: 'Summerize your transactions for each inventory item and how they affect quantity, valuation and weighted average.',
'Summerize your transactions for each inventory item and how they affect quantity, valuation and weighted average.',
pageTitle: formatMessage({ id: 'inventory_valuation' }), pageTitle: formatMessage({ id: 'inventory_valuation' }),
backLink: true, backLink: true,
sidebarExpand: false, sidebarExpand: false,
@@ -257,7 +248,7 @@ export default [
), ),
), ),
breadcrumb: 'Vendors Balance Summary ', breadcrumb: 'Vendors Balance Summary ',
hint: '..', hint: 'Summerize the total amount your business owes each vendor.',
pageTitle: formatMessage({ id: 'vendors_balance_summary' }), pageTitle: formatMessage({ id: 'vendors_balance_summary' }),
backLink: true, backLink: true,
sidebarExpand: false, sidebarExpand: false,
@@ -270,7 +261,7 @@ export default [
), ),
), ),
breadcrumb: 'Customers Transactions ', breadcrumb: 'Customers Transactions ',
hint: '..', hint: 'Reports every transaction going in and out of each customer.',
pageTitle: formatMessage({ id: 'customers_transactions' }), pageTitle: formatMessage({ id: 'customers_transactions' }),
backLink: true, backLink: true,
sidebarExpand: false, sidebarExpand: false,
@@ -283,11 +274,37 @@ export default [
), ),
), ),
breadcrumb: 'Vendors Transactions ', breadcrumb: 'Vendors Transactions ',
hint: '..', hint: 'Reports every transaction going in and out of each vendor/supplier.',
pageTitle: formatMessage({ id: 'vendors_transactions' }), pageTitle: formatMessage({ id: 'vendors_transactions' }),
backLink: true, backLink: true,
sidebarExpand: false, sidebarExpand: false,
}, },
{
path: `/financial-reports/cash-flow`,
component: lazy(() =>
import(
'containers/FinancialStatements/CashFlowStatement/CashFlowStatement'
),
),
breadcrumb: 'Cash Flow Statement',
hint: 'Reports inflow and outflow of cash and cash equivalents between a specific two points of time.',
pageTitle: formatMessage({ id: 'cash_flow_statement' }),
backLink: true,
sidebarExpand: false,
},
{
path: `/financial-reports/inventory-item-details`,
component: lazy(() =>
import(
'containers/FinancialStatements/InventoryItemDetails/InventoryItemDetails'
),
),
breadcrumb: 'Inventory Item Details',
hint: 'Reports every transaction going in and out of your items to monitoring activity of items.',
pageTitle: formatMessage({ id: 'inventory_item_details' }),
backLink: true,
sidebarExpand: false,
},
{ {
path: '/financial-reports', path: '/financial-reports',
component: lazy(() => component: lazy(() =>

View File

@@ -178,3 +178,29 @@ export function toggleVendorsTransactionsFilterDrawer(toggle) {
}, },
}; };
} }
/**
* Toggle display of the cash flow statement filter drawer.
* @param {boolean} toggle
*/
export function toggleCashFlowStatementFilterDrawer(toggle) {
return {
type: `${t.CASH_FLOW_STATEMENT}/${t.DISPLAY_FILTER_DRAWER_TOGGLE}`,
payload: {
toggle,
},
};
}
/**
* Toggles display of the inventory item details filter drawer.
* @param {boolean} toggle
*/
export function toggleInventoryItemDetailsFilterDrawer(toggle) {
return {
type: `${t.INVENTORY_ITEM_DETAILS}/${t.DISPLAY_FILTER_DRAWER_TOGGLE}`,
payload: {
toggle,
},
};
}

View File

@@ -45,6 +45,12 @@ const initialState = {
vendorsTransactions: { vendorsTransactions: {
displayFilterDrawer: false, displayFilterDrawer: false,
}, },
cashFlowStatement: {
displayFilterDrawer: false,
},
inventoryItemDetails: {
displayFilterDrawer: false,
},
}; };
/** /**
@@ -91,4 +97,9 @@ export default createReducer(initialState, {
t.VENDORS_TRANSACTIONS, t.VENDORS_TRANSACTIONS,
'vendorsTransactions', 'vendorsTransactions',
), ),
...financialStatementFilterToggle(t.CASH_FLOW_STATEMENT, 'cashFlowStatement'),
...financialStatementFilterToggle(
t.INVENTORY_ITEM_DETAILS,
'inventoryItemDetails',
),
}); });

View File

@@ -65,6 +65,14 @@ export const vendorsTransactionsFilterDrawerSelector = (state) => {
return filterDrawerByTypeSelector('vendorsTransactions')(state); return filterDrawerByTypeSelector('vendorsTransactions')(state);
}; };
export const cashFlowStatementFilterDrawerSelector = (state) => {
return filterDrawerByTypeSelector('cashFlowStatement')(state);
};
export const inventoryItemDetailsDrawerFilter = (state) => {
return filterDrawerByTypeSelector('inventoryItemDetails')(state);
};
/** /**
* Retrieve balance sheet filter drawer. * Retrieve balance sheet filter drawer.
*/ */
@@ -211,3 +219,23 @@ export const getVendorsTransactionsFilterDrawer = createSelector(
return isOpen; return isOpen;
}, },
); );
/**
* Retrieve cash flow statement filter drawer.
*/
export const getCashFlowStatementFilterDrawer = createSelector(
cashFlowStatementFilterDrawerSelector,
(isOpen) => {
return isOpen;
},
);
/**
* Retrieve inventory item details filter drawer.
*/
export const getInventoryItemDetailsFilterDrawer = createSelector(
inventoryItemDetailsDrawerFilter,
(isOpen) => {
return isOpen;
},
);

View File

@@ -14,4 +14,6 @@ export default {
VENDORS_BALANCE_SUMMARY: 'VENDORS BALANCE SUMMARY', VENDORS_BALANCE_SUMMARY: 'VENDORS BALANCE SUMMARY',
CUSTOMERS_TRANSACTIONS: 'CUSTOMERS TRANSACTIONS', CUSTOMERS_TRANSACTIONS: 'CUSTOMERS TRANSACTIONS',
VENDORS_TRANSACTIONS: 'VENDORS TRANSACTIONS', VENDORS_TRANSACTIONS: 'VENDORS TRANSACTIONS',
CASH_FLOW_STATEMENT: 'CASH FLOW STATEMENT',
INVENTORY_ITEM_DETAILS: 'INVENTORY ITEM DETAILS',
}; };

View File

@@ -0,0 +1,50 @@
.financial-sheet {
&--cash-flow-statement {
.financial-sheet__table {
.thead,
.tbody {
.tr .td.account_name ~ .td,
.tr .th.account_name ~ .th {
text-align: right;
}
}
.tbody {
.tr:not(.no-results) {
&.row-type--CASH_END_PERIOD{
border-bottom: 3px double #333;
}
.td {
border-bottom: 0;
padding-top: 0.4rem;
padding-bottom: 0.4rem;
}
&.row-type--TOTAL {
font-weight: 500;
&:not(:first-child) .td {
border-top: 1px solid #bbb;
}
}
}
.tr.is-expanded {
.td.total,
.td.date-period{
.cell-inner {
display: none;
}
}
}
}
}
}
}
.financial-statement--cash-flow {
.financial-header-drawer {
.bp3-drawer {
max-height: 450px;
}
}
}

View File

@@ -0,0 +1,77 @@
.financial-sheet {
&--inventory-item-details {
width: 100%;
.financial-sheet__table {
.tbody,
.thead {
.tr .td.transaction_id ~ .td,
.tr .th.transaction_id ~ .th {
text-align: right;
}
}
.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 {
&--ITEM {
.td {
&.transaction_type {
border-left-color: transparent;
}
}
&:not(:first-child).is-expanded .td {
border-top: 1px solid #ddd;
}
}
&--ITEM,
&--OPENING_ENTRY,
&--CLOSING_ENTRY {
font-weight: 500;
}
&--ITEM {
&.is-expanded {
.td.value .cell-inner {
display: none;
}
}
}
}
}
}
}
}
.number-format-dropdown {
.toggles-fields {
.bp3-form-group:first-child {
display: none;
}
}
.form-group--money-format {
display: none;
}
}
.financial-statement--inventory-details {
.financial-header-drawer {
.bp3-drawer {
max-height: 350px;
}
}
}

View File

@@ -19,6 +19,7 @@
"dependencies": { "dependencies": {
"@hapi/boom": "^7.4.3", "@hapi/boom": "^7.4.3",
"@types/i18n": "^0.8.7", "@types/i18n": "^0.8.7",
"@types/mathjs": "^6.0.12",
"accepts": "^1.3.7", "accepts": "^1.3.7",
"accounting": "^0.4.1", "accounting": "^0.4.1",
"agenda": "^3.1.0", "agenda": "^3.1.0",
@@ -34,6 +35,7 @@
"crypto-random-string": "^3.2.0", "crypto-random-string": "^3.2.0",
"csurf": "^1.10.0", "csurf": "^1.10.0",
"deep-map": "^2.0.0", "deep-map": "^2.0.0",
"deepdash": "^5.3.7",
"dotenv": "^8.1.0", "dotenv": "^8.1.0",
"errorhandler": "^1.5.1", "errorhandler": "^1.5.1",
"es6-weak-map": "^2.0.3", "es6-weak-map": "^2.0.3",
@@ -55,6 +57,7 @@
"knex-db-manager": "^0.6.1", "knex-db-manager": "^0.6.1",
"libphonenumber-js": "^1.9.6", "libphonenumber-js": "^1.9.6",
"lodash": "^4.17.15", "lodash": "^4.17.15",
"mathjs": "^9.4.0",
"memory-cache": "^0.2.0", "memory-cache": "^0.2.0",
"moment": "^2.24.0", "moment": "^2.24.0",
"moment-range": "^4.0.2", "moment-range": "^4.0.2",

View File

@@ -10,7 +10,7 @@ export default class BaseController {
* Converts plain object keys to cameCase style. * Converts plain object keys to cameCase style.
* @param {Object} data * @param {Object} data
*/ */
private dataToCamelCase(data) { protected dataToCamelCase(data) {
return mapKeysDeep(data, (v, k) => camelCase(k)); return mapKeysDeep(data, (v, k) => camelCase(k));
} }
@@ -19,7 +19,7 @@ export default class BaseController {
* @param {Request} req * @param {Request} req
* @param options * @param options
*/ */
matchedBodyData(req: Request, options: any = {}) { protected matchedBodyData(req: Request, options: any = {}) {
const data = matchedData(req, { const data = matchedData(req, {
locations: ['body'], locations: ['body'],
includeOptionals: true, includeOptionals: true,
@@ -32,7 +32,7 @@ export default class BaseController {
* Matches the query data from validation schema. * Matches the query data from validation schema.
* @param {Request} req * @param {Request} req
*/ */
matchedQueryData(req: Request) { protected matchedQueryData(req: Request) {
const data = matchedData(req, { const data = matchedData(req, {
locations: ['query'], locations: ['query'],
}); });
@@ -45,7 +45,7 @@ export default class BaseController {
* @param {Response} res * @param {Response} res
* @param {NextFunction} next * @param {NextFunction} next
*/ */
validationResult(req: Request, res: Response, next: NextFunction) { protected validationResult(req: Request, res: Response, next: NextFunction) {
const validationErrors = validationResult(req); const validationErrors = validationResult(req);
if (!validationErrors.isEmpty()) { if (!validationErrors.isEmpty()) {
@@ -61,7 +61,7 @@ export default class BaseController {
* Transform the given data to response. * Transform the given data to response.
* @param {any} data * @param {any} data
*/ */
transfromToResponse( protected transfromToResponse(
data: any, data: any,
translatable?: string | string[], translatable?: string | string[],
req?: Request req?: Request
@@ -85,16 +85,16 @@ export default class BaseController {
* Async middleware. * Async middleware.
* @param {function} callback * @param {function} callback
*/ */
asyncMiddleware(callback) { protected asyncMiddleware(callback) {
return asyncMiddleware(callback); return asyncMiddleware(callback);
} }
/** /**
* *
* @param {Request} req * @param {Request} req
* @returns * @returns
*/ */
accepts(req) { protected accepts(req) {
return accepts(req); return accepts(req);
} }
} }

View File

@@ -15,6 +15,8 @@ import CustomerBalanceSummaryController from './FinancialStatements/CustomerBala
import VendorBalanceSummaryController from './FinancialStatements/VendorBalanceSummary'; import VendorBalanceSummaryController from './FinancialStatements/VendorBalanceSummary';
import TransactionsByCustomers from './FinancialStatements/TransactionsByCustomers'; import TransactionsByCustomers from './FinancialStatements/TransactionsByCustomers';
import TransactionsByVendors from './FinancialStatements/TransactionsByVendors'; import TransactionsByVendors from './FinancialStatements/TransactionsByVendors';
import CashFlowStatementController from './FinancialStatements/CashFlow/CashFlow';
import InventoryDetailsController from './FinancialStatements/InventoryDetails';
@Service() @Service()
export default class FinancialStatementsService { export default class FinancialStatementsService {
@@ -77,6 +79,14 @@ export default class FinancialStatementsService {
'/transactions-by-vendors', '/transactions-by-vendors',
Container.get(TransactionsByVendors).router(), Container.get(TransactionsByVendors).router(),
); );
router.use(
'/cash-flow',
Container.get(CashFlowStatementController).router(),
);
router.use(
'/inventory-item-details',
Container.get(InventoryDetailsController).router(),
);
return router; return router;
} }
} }

View File

@@ -0,0 +1,113 @@
import { Inject, Service } from 'typedi';
import { query } from 'express-validator';
import {
NextFunction,
Router,
Request,
Response,
ValidationChain,
} from 'express';
import BaseFinancialReportController from '../BaseFinancialReportController';
import CashFlowStatementService from 'services/FinancialStatements/CashFlow/CashFlowService';
import { ICashFlowStatement } from 'interfaces';
import CashFlowTable from 'services/FinancialStatements/CashFlow/CashFlowTable';
@Service()
export default class CashFlowController extends BaseFinancialReportController {
@Inject()
cashFlowService: CashFlowStatementService;
/**
* Router constructor.
*/
router() {
const router = Router();
router.get(
'/',
this.cashflowValidationSchema,
this.validationResult,
this.asyncMiddleware(this.cashFlow.bind(this))
);
return router;
}
/**
* Balance sheet validation schecma.
* @returns {ValidationChain[]}
*/
get cashflowValidationSchema(): ValidationChain[] {
return [
...this.sheetNumberFormatValidationSchema,
query('from_date').optional(),
query('to_date').optional(),
query('display_columns_type').optional().isIn(['date_periods', 'total']),
query('display_columns_by')
.optional({ nullable: true, checkFalsy: true })
.isIn(['year', 'month', 'week', 'day', 'quarter']),
query('none_zero').optional().isBoolean().toBoolean(),
query('none_transactions').optional().isBoolean().toBoolean(),
];
}
/**
* Retrieve the cashflow statment to json response.
* @param {ICashFlowStatement} cashFlow -
*/
private transformJsonResponse(cashFlow: ICashFlowStatement) {
const { data, query } = cashFlow;
return {
data: this.transfromToResponse(data),
meta: this.transfromToResponse(query),
};
}
/**
* Transformes the report statement to table rows.
* @param {ITransactionsByVendorsStatement} statement -
*
*/
private transformToTableRows(cashFlow: ICashFlowStatement) {
const cashFlowTable = new CashFlowTable(cashFlow);
return {
table: {
data: cashFlowTable.tableRows(),
columns: cashFlowTable.tableColumns(),
},
meta: this.transfromToResponse(cashFlow.query),
};
}
/**
* Retrieve the cash flow statment.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @returns {Response}
*/
async cashFlow(req: Request, res: Response, next: NextFunction) {
const { tenantId, settings } = req;
const filter = {
...this.matchedQueryData(req),
};
try {
const cashFlow = await this.cashFlowService.cashFlow(tenantId, filter);
const accept = this.accepts(req);
const acceptType = accept.types(['json', 'application/json+table']);
switch (acceptType) {
case 'application/json+table':
return res.status(200).send(this.transformToTableRows(cashFlow));
case 'json':
default:
return res.status(200).send(this.transformJsonResponse(cashFlow));
}
} catch (error) {
next(error);
}
}
}

View File

@@ -0,0 +1,120 @@
import { Inject, Service } from 'typedi';
import { query } from 'express-validator';
import {
NextFunction,
Router,
Request,
Response,
ValidationChain,
} from 'express';
import BaseController from 'api/controllers/BaseController';
import InventoryDetailsService from 'services/FinancialStatements/InventoryDetails/InventoryDetailsService';
import InventoryDetailsTable from 'services/FinancialStatements/InventoryDetails/InventoryDetailsTable';
@Service()
export default class InventoryDetailsController extends BaseController {
@Inject()
inventoryDetailsService: InventoryDetailsService;
/**
* Router constructor.
*/
router() {
const router = Router();
router.get(
'/',
this.validationSchema,
this.validationResult,
this.asyncMiddleware(this.inventoryDetails.bind(this))
);
return router;
}
/**
* Balance sheet validation schecma.
* @returns {ValidationChain[]}
*/
get validationSchema(): ValidationChain[] {
return [
query('number_format.precision')
.optional()
.isInt({ min: 0, max: 5 })
.toInt(),
query('number_format.divide_on_1000').optional().isBoolean().toBoolean(),
query('number_format.negative_format')
.optional()
.isIn(['parentheses', 'mines'])
.trim()
.escape(),
query('from_date').optional(),
query('to_date').optional(),
query('none_zero').optional().isBoolean().toBoolean(),
query('none_transactions').optional().isBoolean().toBoolean(),
];
}
/**
* Retrieve the cashflow statment to json response.
* @param {ICashFlowStatement} cashFlow -
*/
private transformJsonResponse(inventoryDetails) {
const { data, query } = inventoryDetails;
return {
data: this.transfromToResponse(data),
meta: this.transfromToResponse(query),
};
}
/**
* Transformes the report statement to table rows.
*/
private transformToTableRows(inventoryDetails) {
const inventoryDetailsTable = new InventoryDetailsTable(inventoryDetails);
return {
table: {
data: inventoryDetailsTable.tableData(),
columns: inventoryDetailsTable.tableColumns(),
},
meta: this.transfromToResponse(inventoryDetails.query),
};
}
/**
* Retrieve the cash flow statment.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @returns {Response}
*/
async inventoryDetails(req: Request, res: Response, next: NextFunction) {
const { tenantId, settings } = req;
const filter = {
...this.matchedQueryData(req),
};
try {
const inventoryDetails =
await this.inventoryDetailsService.inventoryDetails(tenantId, filter);
const accept = this.accepts(req);
const acceptType = accept.types(['json', 'application/json+table']);
switch (acceptType) {
case 'application/json+table':
return res
.status(200)
.send(this.transformToTableRows(inventoryDetails));
case 'json':
default:
return res
.status(200)
.send(this.transformJsonResponse(inventoryDetails));
}
} catch (error) {
next(error);
}
}
}

View File

@@ -3,9 +3,9 @@ export const ACCOUNT_TYPE = {
BANK: 'bank', BANK: 'bank',
ACCOUNTS_RECEIVABLE: 'accounts-receivable', ACCOUNTS_RECEIVABLE: 'accounts-receivable',
INVENTORY: 'inventory', INVENTORY: 'inventory',
OTHER_CURRENT_ASSET: 'other-ACCOUNT_PARENT_TYPE.CURRENT_ASSET', OTHER_CURRENT_ASSET: 'other-current-asset',
FIXED_ASSET: 'fixed-asset', FIXED_ASSET: 'fixed-asset',
NON_CURRENT_ASSET: 'non-ACCOUNT_PARENT_TYPE.CURRENT_ASSET', NON_CURRENT_ASSET: 'none-current-asset',
ACCOUNTS_PAYABLE: 'accounts-payable', ACCOUNTS_PAYABLE: 'accounts-payable',
CREDIT_CARD: 'credit-card', CREDIT_CARD: 'credit-card',
@@ -25,7 +25,7 @@ export const ACCOUNT_TYPE = {
export const ACCOUNT_PARENT_TYPE = { export const ACCOUNT_PARENT_TYPE = {
CURRENT_ASSET: 'current-asset', CURRENT_ASSET: 'current-asset',
FIXED_ASSET: 'fixed-asset', FIXED_ASSET: 'fixed-asset',
NON_CURRENT_ASSET: 'non-ACCOUNT_PARENT_TYPE.CURRENT_ASSET', NON_CURRENT_ASSET: 'non-current-asset',
CURRENT_LIABILITY: 'current-liability', CURRENT_LIABILITY: 'current-liability',
LOGN_TERM_LIABILITY: 'long-term-liability', LOGN_TERM_LIABILITY: 'long-term-liability',

View File

@@ -0,0 +1,11 @@
exports.up = function (knex) {
return knex.schema.createTable('inventory_transaction_meta', (table) => {
table.increments('id');
table.string('transaction_number');
table.text('description');
table.integer('inventory_transaction_id').unsigned();
});
};
exports.down = function (knex) {};

View File

@@ -15,6 +15,7 @@ exports.up = function (knex) {
table.integer('entry_id').unsigned().index(); table.integer('entry_id').unsigned().index();
table.integer('cost_account_id').unsigned(); table.integer('cost_account_id').unsigned();
table.integer('inventory_transaction_id').unsigned().index();
table.datetime('created_at').index(); table.datetime('created_at').index();
}); });

View File

@@ -33,7 +33,13 @@ export interface IAccountsTransactionsFilter {
} }
export interface IAccountTransaction { export interface IAccountTransaction {
credit: number;
debit: number;
accountId: number;
contactId: number;
date: string|Date;
referenceNumber: string;
account: IAccount;
} }
export interface IAccountResponse extends IAccount { export interface IAccountResponse extends IAccount {

View File

@@ -0,0 +1,190 @@
import { INumberFormatQuery } from './FinancialStatements';
import { IAccount } from './Account';
import { ILedger } from './Ledger';
import { ITableRow } from './Table';
export interface ICashFlowStatementQuery {
fromDate: Date|string;
toDate: Date|string;
displayColumnsBy: string;
displayColumnsType: string;
noneZero: boolean;
noneTransactions: boolean;
numberFormat: INumberFormatQuery;
basis: string;
}
export interface ICashFlowStatementTotal {
amount: number;
formattedAmount: string;
currencyCode: string;
}
export interface ICashFlowStatementTotalPeriod {
fromDate: Date;
toDate: Date;
total: ICashFlowStatementTotal;
}
export interface ICashFlowStatementCommonSection {
id: string;
label: string;
total: ICashFlowStatementTotal;
footerLabel?: string;
}
export interface ICashFlowStatementAccountMeta {
id: number;
label: string;
code: string;
total: ICashFlowStatementTotal;
accountType: string;
adjusmentType: string;
sectionType: ICashFlowStatementSectionType.ACCOUNT;
}
export enum ICashFlowStatementSectionType {
REGULAR = 'REGULAR',
AGGREGATE = 'AGGREGATE',
NET_INCOME = 'NET_INCOME',
ACCOUNT = 'ACCOUNT',
ACCOUNTS = 'ACCOUNTS',
TOTAL = 'TOTAL',
CASH_AT_BEGINNING = 'CASH_AT_BEGINNING',
}
export interface ICashFlowStatementAccountSection
extends ICashFlowStatementCommonSection {
sectionType: ICashFlowStatementSectionType.ACCOUNTS;
children: ICashFlowStatementAccountMeta[];
total: ICashFlowStatementTotal;
}
export interface ICashFlowStatementNetIncomeSection
extends ICashFlowStatementCommonSection {
sectionType: ICashFlowStatementSectionType.NET_INCOME;
}
export interface ICashFlowStatementTotalSection
extends ICashFlowStatementCommonSection {
sectionType: ICashFlowStatementSectionType.TOTAL;
}
export type ICashFlowStatementSection =
| ICashFlowStatementAccountSection
| ICashFlowStatementNetIncomeSection
| ICashFlowStatementTotalSection
| ICashFlowStatementCommonSection;
export interface ICashFlowStatementColumn {}
export interface ICashFlowStatementMeta {}
export interface ICashFlowStatementService {
cashFlow(
tenantId: number,
query: ICashFlowStatementQuery
): Promise<ICashFlowStatement>;
}
// CASH FLOW SCHEMA TYPES.
// -----------------------------
export interface ICashFlowSchemaCommonSection {
id: string;
label: string;
children: ICashFlowSchemaSection[];
footerLabel?: string;
}
export enum CASH_FLOW_ACCOUNT_RELATION {
MINES = 'mines',
PLUS = 'plus',
}
export enum CASH_FLOW_SECTION_ID {
NET_INCOME = 'NET_INCOME',
OPERATING = 'OPERATING',
OPERATING_ACCOUNTS = 'OPERATING_ACCOUNTS',
INVESTMENT = 'INVESTMENT',
FINANCIAL = 'FINANCIAL',
NET_OPERATING = 'NET_OPERATING',
NET_INVESTMENT = 'NET_INVESTMENT',
NET_FINANCIAL = 'NET_FINANCIAL',
CASH_BEGINNING_PERIOD = 'CASH_BEGINNING_PERIOD',
CASH_END_PERIOD = 'CASH_END_PERIOD',
NET_CASH_INCREASE = 'NET_CASH_INCREASE',
}
export interface ICashFlowSchemaAccountsSection
extends ICashFlowSchemaCommonSection {
sectionType: ICashFlowStatementSectionType.ACCOUNT;
accountsRelations: ICashFlowSchemaAccountRelation[];
}
export interface ICashFlowSchemaTotalSection
extends ICashFlowStatementCommonSection {
sectionType: ICashFlowStatementSectionType.TOTAL;
equation: string;
}
export type ICashFlowSchemaSection =
| ICashFlowSchemaAccountsSection
| ICashFlowSchemaTotalSection
| ICashFlowSchemaCommonSection;
export type ICashFlowStatementData = ICashFlowSchemaSection[];
export interface ICashFlowSchemaAccountRelation {
type: string;
direction: CASH_FLOW_ACCOUNT_RELATION.PLUS;
}
export interface ICashFlowSchemaSectionAccounts
extends ICashFlowStatementCommonSection {
type: ICashFlowStatementSectionType.ACCOUNT;
accountsRelations: ICashFlowSchemaAccountRelation[];
}
export interface ICashFlowSchemaSectionTotal {
type: ICashFlowStatementSectionType.TOTAL;
totalEquation: string;
}
export interface ICashFlowDatePeriod {
fromDate: ICashFlowDate;
toDate: ICashFlowDate;
total: ICashFlowStatementTotal;
}
export interface ICashFlowDate {
formattedDate: string;
date: Date;
}
export interface ICashFlowStatement {
/**
* Constructor method.
* @constructor
*/
constructor(
accounts: IAccount[],
ledger: ILedger,
cashLedger: ILedger,
netIncomeLedger: ILedger,
query: ICashFlowStatementQuery,
baseCurrency: string
): void;
reportData(): ICashFlowStatementData;
}
export interface ICashFlowTable {
constructor(reportStatement: ICashFlowStatement): void;
tableRows(): ITableRow[];
}
export interface IDateRange {
fromDate: Date,
toDate: Date,
}

View File

@@ -0,0 +1,76 @@
import {
INumberFormatQuery,
} from './FinancialStatements';
export interface IInventoryDetailsQuery {
fromDate: Date | string;
toDate: Date | string;
numberFormat: INumberFormatQuery;
noneTransactions: boolean;
}
export interface IInventoryDetailsNumber {
number: number;
formattedNumber: string;
}
export interface IInventoryDetailsMoney {
amount: number;
formattedAmount: string;
currencyCode: string;
}
export interface IInventoryDetailsDate {
date: Date;
formattedDate: string;
}
export interface IInventoryDetailsOpening {
nodeType: 'OPENING_ENTRY';
date: IInventoryDetailsDate;
quantity: IInventoryDetailsNumber;
value: IInventoryDetailsNumber;
}
export interface IInventoryDetailsClosing extends IInventoryDetailsOpening {
nodeType: 'CLOSING_ENTRY';
}
export interface IInventoryDetailsItem {
id: number;
nodeType: string;
name: string;
code: string;
children: (
| IInventoryDetailsItemTransaction
| IInventoryDetailsOpening
| IInventoryDetailsClosing
)[];
}
export interface IInventoryDetailsItemTransaction {
nodeType: string;
date: IInventoryDetailsDate;
transactionType: string;
transactionNumber: string;
quantityMovement: IInventoryDetailsNumber;
valueMovement: IInventoryDetailsNumber;
quantity: IInventoryDetailsNumber;
value: IInventoryDetailsNumber;
cost: IInventoryDetailsNumber;
profitMargin: IInventoryDetailsNumber;
rate: IInventoryDetailsNumber;
runningQuantity: IInventoryDetailsNumber;
runningValuation: IInventoryDetailsNumber;
direction: string;
}
export type IInventoryDetailsNode =
| IInventoryDetailsItem
| IInventoryDetailsItemTransaction;
export type IInventoryDetailsData = IInventoryDetailsItem[];

View File

@@ -1,38 +1,51 @@
export type TInventoryTransactionDirection = 'IN' | 'OUT'; export type TInventoryTransactionDirection = 'IN' | 'OUT';
export interface IInventoryTransaction { export interface IInventoryTransaction {
id?: number, id?: number;
date: Date|string, date: Date | string;
direction: TInventoryTransactionDirection, direction: TInventoryTransactionDirection;
itemId: number, itemId: number;
quantity: number;
rate: number;
transactionType: string;
transcationTypeFormatted: string;
transactionId: number;
entryId: number;
costAccountId: number;
meta?: IInventoryTransactionMeta;
costLotAggregated?: IInventoryCostLotAggregated;
createdAt?: Date;
updatedAt?: Date;
}
export interface IInventoryTransactionMeta {
id?: number;
transactionNumber: string;
description: string;
}
export interface IInventoryCostLotAggregated {
cost: number,
quantity: number, quantity: number,
rate: number,
transactionType: string,
transactionId: number,
entryId: number,
costAccountId: number,
createdAt?: Date,
updatedAt?: Date,
}; };
export interface IInventoryLotCost { export interface IInventoryLotCost {
id?: number, id?: number;
date: Date, date: Date;
direction: string, direction: string;
itemId: number, itemId: number;
quantity: number, quantity: number;
rate: number, rate: number;
remaining: number, remaining: number;
cost: number, cost: number;
transactionType: string, transactionType: string;
transactionId: number, transactionId: number;
costAccountId: number, costAccountId: number;
entryId: number, entryId: number;
createdAt: Date, createdAt: Date;
}; }
export interface IItemsQuantityChanges { export interface IItemsQuantityChanges {
itemId: number, itemId: number;
balanceChange: number, balanceChange: number;
}; }

View File

@@ -2,6 +2,7 @@ export interface ILedger {
entries: ILedgerEntry[]; entries: ILedgerEntry[];
getEntries(): ILedgerEntry[]; getEntries(): ILedgerEntry[];
whereAccountId(accountId: number): ILedger;
whereContactId(contactId: number): ILedger; whereContactId(contactId: number): ILedger;
whereFromDate(fromDate: Date | string): ILedger; whereFromDate(fromDate: Date | string): ILedger;
whereToDate(toDate: Date | string): ILedger; whereToDate(toDate: Date | string): ILedger;
@@ -15,6 +16,6 @@ export interface ILedgerEntry {
accountNormal: string; accountNormal: string;
contactId?: number; contactId?: number;
date: Date | string; date: Date | string;
transactionType: string, transactionType?: string,
transactionNumber: string, transactionNumber?: string,
} }

View File

@@ -12,4 +12,14 @@ export interface ITableCell {
export type ITableRow = { export type ITableRow = {
rows: ITableCell[]; rows: ITableCell[];
}; };
export interface ITableColumn {
key: string,
label: string,
}
export interface ITable {
columns: ITableColumn[],
data: ITableRow[],
}

View File

@@ -25,8 +25,8 @@ export interface ITransactionsByContactsContact {
} }
export interface ITransactionsByContactsFilter { export interface ITransactionsByContactsFilter {
fromDate: Date; fromDate: Date|string;
toDate: Date; toDate: Date|string;
numberFormat: INumberFormatQuery; numberFormat: INumberFormatQuery;
noneTransactions: boolean; noneTransactions: boolean;
noneZero: boolean; noneZero: boolean;

View File

@@ -50,4 +50,6 @@ export * from './TransactionsByCustomers';
export * from './TransactionsByContacts'; export * from './TransactionsByContacts';
export * from './TransactionsByVendors'; export * from './TransactionsByVendors';
export * from './Table'; export * from './Table';
export * from './Ledger'; export * from './Ledger';
export * from './CashFlow';
export * from './InventoryDetails';

View File

@@ -1,4 +1,4 @@
import { Model } from 'objection'; import { Model, raw } from 'objection';
import moment from 'moment'; import moment from 'moment';
import TenantModel from 'models/TenantModel'; import TenantModel from 'models/TenantModel';
@@ -138,6 +138,21 @@ export default class AccountTransaction extends TenantModel {
query.sum('credit as credit'); query.sum('credit as credit');
query.sum('debit as debit'); query.sum('debit as debit');
query.select('contactId'); query.select('contactId');
},
creditDebitSummation(query) {
query.sum('credit as credit');
query.sum('debit as debit');
},
groupByDateFormat(query, groupType = 'month') {
const groupBy = {
'day': '%Y-%m-%d',
'month': '%Y-%m',
'year': '%Y',
};
const dateFormat = groupBy[groupType];
query.select(raw(`DATE_FORMAT(DATE, '${dateFormat}')`).as('date'));
query.groupByRaw(`DATE_FORMAT(DATE, '${dateFormat}')`);
} }
}; };
} }

View File

@@ -17,6 +17,33 @@ export default class InventoryTransaction extends TenantModel {
return ['createdAt', 'updatedAt']; return ['createdAt', 'updatedAt'];
} }
/**
* Retrieve formatted reference type.
* @return {string}
*/
get transcationTypeFormatted() {
return InventoryTransaction.getReferenceTypeFormatted(this.transactionType);
}
/**
* Reference type formatted.
*/
static getReferenceTypeFormatted(referenceType) {
const mapped = {
'SaleInvoice': 'Sale invoice',
'SaleReceipt': 'Sale receipt',
'PaymentReceive': 'Payment receive',
'Bill': 'Bill',
'BillPayment': 'Payment made',
'VendorOpeningBalance': 'Vendor opening balance',
'CustomerOpeningBalance': 'Customer opening balance',
'InventoryAdjustment': 'Inventory adjustment',
'ManualJournal': 'Manual journal',
'Journal': 'Manual journal',
};
return mapped[referenceType] || '';
}
/** /**
* Model modifiers. * Model modifiers.
*/ */
@@ -59,8 +86,47 @@ export default class InventoryTransaction extends TenantModel {
static get relationMappings() { static get relationMappings() {
const Item = require('models/Item'); const Item = require('models/Item');
const ItemEntry = require('models/ItemEntry'); const ItemEntry = require('models/ItemEntry');
const InventoryTransactionMeta = require('models/InventoryTransactionMeta');
const InventoryCostLots = require('models/InventoryCostLotTracker');
return { return {
// Transaction meta.
meta: {
relation: Model.HasOneRelation,
modelClass: InventoryTransactionMeta.default,
join: {
from: 'inventory_transactions.id',
to: 'inventory_transaction_meta.inventoryTransactionId',
},
},
// Item cost aggregated.
itemCostAggregated: {
relation: Model.HasOneRelation,
modelClass: InventoryCostLots.default,
join: {
from: 'inventory_transactions.itemId',
to: 'inventory_cost_lot_tracker.itemId',
},
filter(query) {
query.select('itemId');
query.sum('cost as cost');
query.sum('quantity as quantity');
query.groupBy('itemId');
},
},
costLotAggregated: {
relation: Model.HasOneRelation,
modelClass: InventoryCostLots.default,
join: {
from: 'inventory_transactions.id',
to: 'inventory_cost_lot_tracker.inventoryTransactionId',
},
filter(query) {
query.sum('cost as cost');
query.sum('quantity as quantity');
query.groupBy('inventoryTransactionId');
}
},
item: { item: {
relation: Model.BelongsToOneRelation, relation: Model.BelongsToOneRelation,
modelClass: Item.default, modelClass: Item.default,

View File

@@ -0,0 +1,29 @@
import { Model, raw } from 'objection';
import TenantModel from 'models/TenantModel';
export default class InventoryTransactionMeta extends TenantModel {
/**
* Table name
*/
static get tableName() {
return 'inventory_transaction_meta';
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const InventoryTransactions = require('models/InventoryTransaction');
return {
inventoryTransaction: {
relation: Model.BelongsToOneRelation,
modelClass: InventoryTransactions.default,
join: {
from: 'inventory_transaction_meta.inventoryTransactionId',
to: 'inventory_transactions.inventoryTransactionId'
}
}
};
}
}

View File

@@ -1,9 +1,6 @@
import moment from 'moment'; import moment from 'moment';
import { defaultTo } from 'lodash'; import { defaultTo } from 'lodash';
import { import { IAccountTransaction, ILedger, ILedgerEntry } from 'interfaces';
ILedger,
ILedgerEntry
} from 'interfaces';
import EntityRepository from 'repositories/EntityRepository'; import EntityRepository from 'repositories/EntityRepository';
export default class Ledger implements ILedger { export default class Ledger implements ILedger {
@@ -11,7 +8,7 @@ export default class Ledger implements ILedger {
/** /**
* Constructor method. * Constructor method.
* @param {ILedgerEntry[]} entries * @param {ILedgerEntry[]} entries
*/ */
constructor(entries: ILedgerEntry[]) { constructor(entries: ILedgerEntry[]) {
this.entries = entries; this.entries = entries;
@@ -20,26 +17,45 @@ export default class Ledger implements ILedger {
/** /**
* Filters the ledegr entries. * Filters the ledegr entries.
* @param callback * @param callback
* @returns * @returns {ILedger}
*/ */
filter(callback) { public filter(callback): ILedger {
const entries = this.entries.filter(callback); const entries = this.entries.filter(callback);
return new Ledger(entries); return new Ledger(entries);
} }
getEntries(): ILedgerEntry[] { /**
* Retrieve the all entries of the ledger.
* @return {ILedgerEntry[]}
*/
public getEntries(): ILedgerEntry[] {
return this.entries; return this.entries;
} }
whereContactId(contactId: number): ILedger { /**
* Filters entries by th given contact id and returns a new ledger.
* @param {number} contactId
* @returns {ILedger}
*/
public whereContactId(contactId: number): ILedger {
return this.filter((entry) => entry.contactId === contactId); return this.filter((entry) => entry.contactId === contactId);
} }
whereAccountId(accountId: number): ILedger { /**
* Filters entries by the given account id and returns a new ledger.
* @param {number} accountId
* @returns {ILedger}
*/
public whereAccountId(accountId: number): ILedger {
return this.filter((entry) => entry.accountId === accountId); return this.filter((entry) => entry.accountId === accountId);
} }
whereFromDate(fromDate: Date | string): ILedger { /**
* Filters entries that before or same the given date and returns a new ledger.
* @param {Date|string} fromDate
* @returns {ILedger}
*/
public whereFromDate(fromDate: Date | string): ILedger {
const fromDateParsed = moment(fromDate); const fromDateParsed = moment(fromDate);
return this.filter( return this.filter(
@@ -48,7 +64,12 @@ export default class Ledger implements ILedger {
); );
} }
whereToDate(toDate: Date | string): ILedger { /**
* Filters ledger entries that after the given date and retruns a new ledger.
* @param {Date|string} toDate
* @returns {ILedger}
*/
public whereToDate(toDate: Date | string): ILedger {
const toDateParsed = moment(toDate); const toDateParsed = moment(toDate);
return this.filter( return this.filter(
@@ -59,15 +80,14 @@ export default class Ledger implements ILedger {
/** /**
* Retrieve the closing balance of the entries. * Retrieve the closing balance of the entries.
* @returns {number} * @returns {number}
*/ */
getClosingBalance() { public getClosingBalance(): number {
let closingBalance = 0; let closingBalance = 0;
this.entries.forEach((entry) => { this.entries.forEach((entry) => {
if (entry.accountNormal === 'credit') { if (entry.accountNormal === 'credit') {
closingBalance += entry.credit - entry.debit; closingBalance += entry.credit - entry.debit;
} else if (entry.accountNormal === 'debit') { } else if (entry.accountNormal === 'debit') {
closingBalance += entry.debit - entry.credit; closingBalance += entry.debit - entry.credit;
} }
@@ -75,15 +95,25 @@ export default class Ledger implements ILedger {
return closingBalance; return closingBalance;
} }
static mappingTransactions(entries): ILedgerEntry[] { /**
* Mappes the account transactions to ledger entries.
* @param {IAccountTransaction[]} entries
* @returns {ILedgerEntry[]}
*/
static mappingTransactions(entries: IAccountTransaction[]): ILedgerEntry[] {
return entries.map(this.mapTransaction); return entries.map(this.mapTransaction);
} }
static mapTransaction(entry): ILedgerEntry { /**
* Mappes the account transaction to ledger entry.
* @param {IAccountTransaction} entry
* @returns {ILedgerEntry}
*/
static mapTransaction(entry: IAccountTransaction): ILedgerEntry {
return { return {
credit: defaultTo(entry.credit, 0), credit: defaultTo(entry.credit, 0),
debit: defaultTo(entry.debit, 0), debit: defaultTo(entry.debit, 0),
accountNormal: entry.accountNormal, accountNormal: entry.account.accountNormal,
accountId: entry.accountId, accountId: entry.accountId,
contactId: entry.contactId, contactId: entry.contactId,
date: entry.date, date: entry.date,
@@ -91,10 +121,15 @@ export default class Ledger implements ILedger {
transactionType: entry.referenceTypeFormatted, transactionType: entry.referenceTypeFormatted,
referenceNumber: entry.referenceNumber, referenceNumber: entry.referenceNumber,
referenceType: entry.referenceType, referenceType: entry.referenceType,
} };
} }
static fromTransactions(transactions) { /**
* Mappes the account transactions to ledger entries.
* @param {IAccountTransaction[]} transactions
* @returns {ILedger}
*/
static fromTransactions(transactions: IAccountTransaction[]): ILedger {
const entries = Ledger.mappingTransactions(transactions); const entries = Ledger.mappingTransactions(transactions);
return new Ledger(entries); return new Ledger(entries);
} }

View File

@@ -16,13 +16,13 @@ import FinancialSheet from '../FinancialSheet';
export default class BalanceSheetStatement extends FinancialSheet { export default class BalanceSheetStatement extends FinancialSheet {
readonly query: IBalanceSheetQuery; readonly query: IBalanceSheetQuery;
readonly numberFormat: INumberFormatQuery;
readonly tenantId: number; readonly tenantId: number;
readonly accounts: IAccount & { type: IAccountType }[]; readonly accounts: IAccount & { type: IAccountType }[];
readonly journalFinancial: IJournalPoster; readonly journalFinancial: IJournalPoster;
readonly comparatorDateType: string; readonly comparatorDateType: string;
readonly dateRangeSet: string[]; readonly dateRangeSet: string[];
readonly baseCurrency: string; readonly baseCurrency: string;
readonly numberFormat: INumberFormatQuery;
/** /**
* Constructor method. * Constructor method.
@@ -46,7 +46,6 @@ export default class BalanceSheetStatement extends FinancialSheet {
this.accounts = accounts; this.accounts = accounts;
this.journalFinancial = journalFinancial; this.journalFinancial = journalFinancial;
this.baseCurrency = baseCurrency; this.baseCurrency = baseCurrency;
this.comparatorDateType = this.comparatorDateType =
query.displayColumnsType === 'total' ? 'day' : query.displayColumnsBy; query.displayColumnsType === 'total' ? 'day' : query.displayColumnsBy;

View File

@@ -53,7 +53,7 @@ export default class BalanceSheetStatementService
* @param {number} tenantId - * @param {number} tenantId -
* @returns {IBalanceSheetMeta} * @returns {IBalanceSheetMeta}
*/ */
reportMetadata(tenantId: number): IBalanceSheetMeta { private reportMetadata(tenantId: number): IBalanceSheetMeta {
const settings = this.tenancy.settings(tenantId); const settings = this.tenancy.settings(tenantId);
const isCostComputeRunning = this.inventoryService const isCostComputeRunning = this.inventoryService
@@ -113,7 +113,7 @@ export default class BalanceSheetStatementService
// Retrieve all journal transactions based on the given query. // Retrieve all journal transactions based on the given query.
const transactions = await transactionsRepository.journal({ const transactions = await transactionsRepository.journal({
fromDate: query.fromDate, fromDate: query.fromDate,
toDate: query.toDate, toDate: query.toDate,
}); });
// Transform transactions to journal collection. // Transform transactions to journal collection.
const transactionsJournal = Journal.fromTransactions( const transactionsJournal = Journal.fromTransactions(

View File

@@ -0,0 +1,761 @@
import * as R from 'ramda';
import { defaultTo, map, set, sumBy, isEmpty, mapValues, get } from 'lodash';
import * as mathjs from 'mathjs';
import moment from 'moment';
import {
IAccount,
ILedger,
INumberFormatQuery,
ICashFlowSchemaSection,
ICashFlowStatementQuery,
ICashFlowStatementNetIncomeSection,
ICashFlowStatementAccountSection,
ICashFlowSchemaSectionAccounts,
ICashFlowStatementAccountMeta,
ICashFlowSchemaAccountRelation,
ICashFlowStatementSectionType,
ICashFlowStatementData,
ICashFlowDatePeriod,
ICashFlowStatement,
ICashFlowSchemaTotalSection,
ICashFlowStatementTotalSection,
ICashFlowStatementSection,
} from 'interfaces';
import CASH_FLOW_SCHEMA from './schema';
import FinancialSheet from '../FinancialSheet';
import {
transformToMapBy,
accumSum,
dateRangeFromToCollection,
applyMixins,
} from 'utils';
import {
reduceDeep,
iteratee,
mapValuesDeep,
filterDeep,
} from 'utils/deepdash';
import { ACCOUNT_ROOT_TYPE } from 'data/AccountTypes';
import CashFlowDatePeriods from './CashFlowDatePeriods';
const MAP_CONFIG = { childrenPath: 'children', pathFormat: 'array' };
const DISPLAY_COLUMNS_BY = {
DATE_PERIODS: 'date_periods',
TOTAL: 'total',
};
class CashFlowStatement extends FinancialSheet implements ICashFlowStatement {
readonly baseCurrency: string;
readonly sectionsByIds = {};
readonly cashFlowSchemaMap: Map<string, ICashFlowSchemaSection>;
readonly cashFlowSchemaSeq: Array<string>;
readonly accountByTypeMap: Map<string, IAccount[]>;
readonly accountsByRootType: Map<string, IAccount[]>;
readonly ledger: ILedger;
readonly cashLedger: ILedger;
readonly netIncomeLedger: ILedger;
readonly schemaSectionParserIteratee: any;
readonly query: ICashFlowStatementQuery;
readonly numberFormat: INumberFormatQuery;
readonly comparatorDateType: string;
readonly dateRangeSet: { fromDate: Date; toDate: Date }[];
/**
* Constructor method.
* @constructor
*/
constructor(
accounts: IAccount[],
ledger: ILedger,
cashLedger: ILedger,
netIncomeLedger: ILedger,
query: ICashFlowStatementQuery,
baseCurrency: string
) {
super();
this.baseCurrency = baseCurrency;
this.ledger = ledger;
this.cashLedger = cashLedger;
this.netIncomeLedger = netIncomeLedger;
this.accountByTypeMap = transformToMapBy(accounts, 'accountType');
this.accountsByRootType = transformToMapBy(accounts, 'accountRootType');
this.schemaSectionParserIteratee = iteratee(this.schemaSectionParser);
this.query = query;
this.numberFormat = this.query.numberFormat;
this.dateRangeSet = [];
this.comparatorDateType =
query.displayColumnsType === 'total' ? 'day' : query.displayColumnsBy;
this.initDateRangeCollection();
}
// --------------------------------------------
// # GENERAL UTILITIES
// --------------------------------------------
/**
* Retrieve the expense accounts ids.
* @return {number[]}
*/
private getAccountsIdsByType(accountType: string): number[] {
const expenseAccounts = this.accountsByRootType.get(accountType);
const expenseAccountsIds = map(expenseAccounts, 'id');
return expenseAccountsIds;
}
/**
* Detarmines the given display columns by type.
* @param {string} displayColumnsBy
* @returns {boolean}
*/
private isDisplayColumnsBy(displayColumnsBy: string): boolean {
return this.query.displayColumnsType === displayColumnsBy;
}
/**
* Adjustments the given amount.
* @param {string} direction
* @param {number} amount -
* @return {number}
*/
private amountAdjustment(direction: 'mines' | 'plus', amount): number {
return R.when(
R.always(R.equals(direction, 'mines')),
R.multiply(-1)
)(amount);
}
// --------------------------------------------
// # NET INCOME NODE
// --------------------------------------------
/**
* Retrieve the accounts net income.
* @returns {number} - Amount of net income.
*/
private getAccountsNetIncome(): number {
// Mapping income/expense accounts ids.
const incomeAccountsIds = this.getAccountsIdsByType(
ACCOUNT_ROOT_TYPE.INCOME
);
const expenseAccountsIds = this.getAccountsIdsByType(
ACCOUNT_ROOT_TYPE.EXPENSE
);
// Income closing balance.
const incomeClosingBalance = accumSum(incomeAccountsIds, (id) =>
this.netIncomeLedger.whereAccountId(id).getClosingBalance()
);
// Expense closing balance.
const expenseClosingBalance = accumSum(expenseAccountsIds, (id) =>
this.netIncomeLedger.whereAccountId(id).getClosingBalance()
);
// Net income = income - expenses.
const netIncome = incomeClosingBalance - expenseClosingBalance;
return netIncome;
}
/**
* Parses the net income section from the given section schema.
* @param {ICashFlowSchemaSection} sectionSchema - Report section schema.
* @returns {ICashFlowStatementNetIncomeSection}
*/
private netIncomeSectionMapper(
sectionSchema: ICashFlowSchemaSection
): ICashFlowStatementNetIncomeSection {
const netIncome = this.getAccountsNetIncome();
return R.compose(
R.when(
R.always(this.isDisplayColumnsBy(DISPLAY_COLUMNS_BY.DATE_PERIODS)),
this.assocPeriodsToNetIncomeNode.bind(this)
)
)({
id: sectionSchema.id,
label: sectionSchema.label,
total: this.getAmountMeta(netIncome),
sectionType: ICashFlowStatementSectionType.NET_INCOME,
});
}
// --------------------------------------------
// # ACCOUNT NODE
// --------------------------------------------
/**
* Retrieve account meta.
* @param {ICashFlowSchemaAccountRelation} relation - Account relation.
* @param {IAccount} account -
* @returns {ICashFlowStatementAccountMeta}
*/
private accountMetaMapper(
relation: ICashFlowSchemaAccountRelation,
account: IAccount
): ICashFlowStatementAccountMeta {
// Retrieve the closing balance of the given account.
const getClosingBalance = (id) =>
this.ledger.whereAccountId(id).getClosingBalance();
const closingBalance = R.compose(
// Multiplies the amount by -1 in case the relation in mines.
R.curry(this.amountAdjustment)(relation.direction)
)(getClosingBalance(account.id));
return R.compose(
R.when(
R.always(this.isDisplayColumnsBy(DISPLAY_COLUMNS_BY.DATE_PERIODS)),
this.assocPeriodsToAccountNode.bind(this)
)
)({
id: account.id,
code: account.code,
label: account.name,
accountType: account.accountType,
adjusmentType: relation.direction,
total: this.getAmountMeta(closingBalance),
sectionType: ICashFlowStatementSectionType.ACCOUNT,
});
}
/**
* Retrieve accounts sections by the given schema relation.
* @param {ICashFlowSchemaAccountRelation} relation
* @returns {ICashFlowStatementAccountMeta[]}
*/
private getAccountsBySchemaRelation(
relation: ICashFlowSchemaAccountRelation
): ICashFlowStatementAccountMeta[] {
const accounts = defaultTo(this.accountByTypeMap.get(relation.type), []);
const accountMetaMapper = R.curry(this.accountMetaMapper.bind(this))(
relation
);
return R.map(accountMetaMapper)(accounts);
}
/**
* Retrieve the accounts meta.
* @param {string[]} types
* @returns {ICashFlowStatementAccountMeta[]}
*/
private getAccountsBySchemaRelations(
relations: ICashFlowSchemaAccountRelation[]
): ICashFlowStatementAccountMeta[] {
return R.pipe(
R.append(R.map(this.getAccountsBySchemaRelation.bind(this))(relations)),
R.flatten
)([]);
}
/**
* Calculates the accounts total
* @param {ICashFlowStatementAccountMeta[]} accounts
* @returns {number}
*/
private getAccountsMetaTotal(
accounts: ICashFlowStatementAccountMeta[]
): number {
return sumBy(accounts, 'total.amount');
}
/**
* Retrieve the accounts section from the section schema.
* @param {ICashFlowSchemaSectionAccounts} sectionSchema
* @returns {ICashFlowStatementAccountSection}
*/
private accountsSectionParser(
sectionSchema: ICashFlowSchemaSectionAccounts
): ICashFlowStatementAccountSection {
const { accountsRelations } = sectionSchema;
const accounts = this.getAccountsBySchemaRelations(accountsRelations);
const total = this.getAccountsMetaTotal(accounts);
return R.compose(
R.when(
R.always(this.isDisplayColumnsBy(DISPLAY_COLUMNS_BY.DATE_PERIODS)),
this.assocPeriodsToAggregateNode.bind(this)
)
)({
sectionType: ICashFlowStatementSectionType.ACCOUNTS,
id: sectionSchema.id,
label: sectionSchema.label,
footerLabel: sectionSchema.footerLabel,
children: accounts,
total: this.getTotalAmountMeta(total),
});
}
/**
* Detarmines the schema section type.
* @param {string} type
* @param {ICashFlowSchemaSection} section
* @returns {boolean}
*/
private isSchemaSectionType(
type: string,
section: ICashFlowSchemaSection
): boolean {
return type === section.sectionType;
}
// --------------------------------------------
// # AGGREGATE NODE
// --------------------------------------------
/**
*
* @param {ICashFlowSchemaSection} schemaSection
* @returns
*/
private regularSectionParser(
schemaSection: ICashFlowSchemaSection
): ICashFlowStatementSection {
return {
id: schemaSection.id,
label: schemaSection.label,
footerLabel: schemaSection.footerLabel,
sectionType: ICashFlowStatementSectionType.REGULAR,
};
}
private transformSectionsToMap(sections: ICashFlowSchemaSection[]) {
return reduceDeep(
sections,
(acc, section) => {
if (section.id) {
acc[`${section.id}`] = section;
}
return acc;
},
{},
MAP_CONFIG
);
}
// --------------------------------------------
// # TOTAL EQUATION NODE
// --------------------------------------------
private sectionsMapToTotal(mappedSections: { [key: number]: any }) {
return mapValues(mappedSections, (node) => get(node, 'total.amount') || 0);
}
/**
* Evauluate equaation string with the given scope table.
* @param {string} equation -
* @param {{ [key: string]: number }} scope -
* @return {number}
*/
private evaluateEquation(
equation: string,
scope: { [key: string | number]: number }
): number {
return mathjs.evaluate(equation, scope);
}
/**
* Retrieve the total section from the eqauation parser.
* @param {ICashFlowSchemaTotalSection} sectionSchema
* @param {ICashFlowSchemaSection[]} accumlatedSections
*/
private totalEquationSectionParser(
accumlatedSections: ICashFlowSchemaSection[],
sectionSchema: ICashFlowSchemaTotalSection
): ICashFlowStatementTotalSection {
const mappedSectionsById = this.transformSectionsToMap(accumlatedSections);
const nodesTotalById = this.sectionsMapToTotal(mappedSectionsById);
const total = this.evaluateEquation(sectionSchema.equation, nodesTotalById);
return R.compose(
R.when(
R.always(this.isDisplayColumnsBy(DISPLAY_COLUMNS_BY.DATE_PERIODS)),
R.curry(this.assocTotalEquationDatePeriods.bind(this))(
mappedSectionsById,
sectionSchema.equation
)
)
)({
sectionType: ICashFlowStatementSectionType.TOTAL,
id: sectionSchema.id,
label: sectionSchema.label,
total: this.getTotalAmountMeta(total),
});
}
/**
* Retrieve the beginning cash from date.
* @param {Date|string} fromDate -
* @return {Date}
*/
private beginningCashFrom(fromDate: string | Date): Date {
return moment(fromDate).subtract(1, 'days').toDate();
}
/**
* Retrieve account meta.
* @param {ICashFlowSchemaAccountRelation} relation
* @param {IAccount} account
* @returns {ICashFlowStatementAccountMeta}
*/
private cashAccountMetaMapper(
relation: ICashFlowSchemaAccountRelation,
account: IAccount
): ICashFlowStatementAccountMeta {
const cashToDate = this.beginningCashFrom(this.query.fromDate);
const closingBalance = this.cashLedger
.whereToDate(cashToDate)
.whereAccountId(account.id)
.getClosingBalance();
return R.compose(
R.when(
R.always(this.isDisplayColumnsBy(DISPLAY_COLUMNS_BY.DATE_PERIODS)),
this.assocCashAtBeginningAccountDatePeriods.bind(this)
)
)({
id: account.id,
code: account.code,
label: account.name,
accountType: account.accountType,
adjusmentType: relation.direction,
total: this.getAmountMeta(closingBalance),
sectionType: ICashFlowStatementSectionType.ACCOUNT,
});
}
/**
* Retrieve accounts sections by the given schema relation.
* @param {ICashFlowSchemaAccountRelation} relation
* @returns {ICashFlowStatementAccountMeta[]}
*/
private getCashAccountsBySchemaRelation(
relation: ICashFlowSchemaAccountRelation
): ICashFlowStatementAccountMeta[] {
const accounts = this.accountByTypeMap.get(relation.type) || [];
const accountMetaMapper = R.curry(this.cashAccountMetaMapper.bind(this))(
relation
);
return accounts.map(accountMetaMapper);
}
/**
* Retrieve the accounts meta.
* @param {string[]} types
* @returns {ICashFlowStatementAccountMeta[]}
*/
private getCashAccountsBySchemaRelations(
relations: ICashFlowSchemaAccountRelation[]
): ICashFlowStatementAccountMeta[] {
return R.concat(
...R.map(this.getCashAccountsBySchemaRelation.bind(this))(relations)
);
}
/**
* Parses the cash at beginning section.
* @param {ICashFlowSchemaTotalSection} sectionSchema -
* @return {}
*/
private cashAtBeginningSectionParser(sectionSchema) {
const { accountsRelations } = sectionSchema;
const children = this.getCashAccountsBySchemaRelations(accountsRelations);
const total = this.getAccountsMetaTotal(children);
return R.compose(
R.when(
R.always(this.isDisplayColumnsBy(DISPLAY_COLUMNS_BY.DATE_PERIODS)),
this.assocCashAtBeginningDatePeriods.bind(this)
)
)({
sectionType: ICashFlowStatementSectionType.CASH_AT_BEGINNING,
id: sectionSchema.id,
label: sectionSchema.label,
children,
total: this.getTotalAmountMeta(total),
});
}
/**
* Parses the schema section.
* @param {ICashFlowSchemaSection} section
* @returns {ICashFlowSchemaSection}
*/
private schemaSectionParser(
section: ICashFlowSchemaSection,
key: number,
parentValue: ICashFlowSchemaSection[],
context,
accumlatedSections: ICashFlowSchemaSection[]
): ICashFlowSchemaSection {
const isSchemaSectionType = R.curry(this.isSchemaSectionType);
return R.compose(
// Accounts node.
R.when(
isSchemaSectionType(ICashFlowStatementSectionType.ACCOUNTS),
this.accountsSectionParser.bind(this)
),
// Net income node.
R.when(
isSchemaSectionType(ICashFlowStatementSectionType.NET_INCOME),
this.netIncomeSectionMapper.bind(this)
),
// Cash at beginning node.
R.when(
isSchemaSectionType(ICashFlowStatementSectionType.CASH_AT_BEGINNING),
R.curry(this.cashAtBeginningSectionParser.bind(this))
),
// Aggregate node. (that has no section type).
R.when(
isSchemaSectionType(ICashFlowStatementSectionType.AGGREGATE),
this.regularSectionParser.bind(this)
)
)(section);
}
/**
* Parses the schema section.
* @param {ICashFlowSchemaSection} section
* @returns {ICashFlowSchemaSection}
*/
private schemaSectionTotalParser(
section: ICashFlowSchemaSection | ICashFlowStatementSection,
key: number,
parentValue: ICashFlowSchemaSection[],
context,
accumlatedSections: ICashFlowSchemaSection | ICashFlowStatementSection[]
): ICashFlowSchemaSection {
const isSchemaSectionType = R.curry(this.isSchemaSectionType);
return R.compose(
// Total equation section.
R.when(
isSchemaSectionType(ICashFlowStatementSectionType.TOTAL),
R.curry(this.totalEquationSectionParser.bind(this))(accumlatedSections)
)
)(section);
}
/**
*
* @param acc
* @param value
* @param key
* @param parentValue
* @param context
* @returns
*/
private schemaSectionsReducer(acc, value, key, parentValue, context) {
set(
acc,
context.path,
this.schemaSectionParserIteratee(value, key, parentValue, context, acc)
);
return acc;
}
/**
* Schema sections parser.
* @param {ICashFlowSchemaSection[]}schema
* @returns
*/
private schemaSectionsParser(schema: ICashFlowSchemaSection[]) {
return reduceDeep(
schema,
this.schemaSectionsReducer.bind(this),
[],
MAP_CONFIG
);
}
/**
* Writes the `total` property to the aggregate node.
* @return {ICashFlowStatementSection}
*/
private assocRegularSectionTotal(section: ICashFlowStatementSection) {
const total = this.getAccountsMetaTotal(section.children);
return R.assoc('total', this.getTotalAmountMeta(total), section);
}
/**
* Parses the given node on stage 2.
* @param {ICashFlowStatementSection} node
* @return {ICashFlowStatementSection}
*/
private sectionMapperAfterParsing(section: ICashFlowStatementSection) {
const isSchemaSectionType = R.curry(this.isSchemaSectionType);
return R.compose(
R.when(
isSchemaSectionType(ICashFlowStatementSectionType.REGULAR),
this.assocRegularSectionTotal.bind(this)
),
R.when(
isSchemaSectionType(ICashFlowStatementSectionType.REGULAR),
R.when(
R.always(this.isDisplayColumnsBy(DISPLAY_COLUMNS_BY.DATE_PERIODS)),
this.assocPeriodsToAggregateNode.bind(this)
)
)
)(section);
}
private regularSectionsTotal(
sections: ICashFlowSchemaSection[]
): ICashFlowSchemaSection[] {
return mapValuesDeep(
sections,
this.sectionMapperAfterParsing.bind(this),
MAP_CONFIG
);
}
private totalSectionsParser(
sections: ICashFlowSchemaSection | ICashFlowStatementSection[]
) {
return reduceDeep(
sections,
(acc, value, key, parentValue, context) => {
set(
acc,
context.path,
this.schemaSectionTotalParser(value, key, parentValue, context, acc)
);
return acc;
},
[],
MAP_CONFIG
);
}
// --------------------------------------------
// REPORT FILTERING
// --------------------------------------------
/**
* Detarmines the given section has children and not empty.
* @param {ICashFlowStatementSection} section
* @returns {boolean}
*/
private isSectionHasChildren(section: ICashFlowStatementSection): boolean {
return !isEmpty(section.children);
}
/**
* Detarmines whether the section has no zero amount.
* @param {ICashFlowStatementSection} section
* @returns {boolean}
*/
private isSectionNoneZero(section: ICashFlowStatementSection): boolean {
return section.total.amount !== 0;
}
/**
* Detarmines whether the parent accounts sections has children.
* @param {ICashFlowStatementSection} section
* @returns {boolean}
*/
private isAccountsSectionHasChildren(
section: ICashFlowStatementSection[]
): boolean {
const isSchemaSectionType = R.curry(this.isSchemaSectionType);
return R.ifElse(
isSchemaSectionType(ICashFlowStatementSectionType.ACCOUNTS),
this.isSectionHasChildren.bind(this),
R.always(true)
)(section);
}
/**
* Detarmines the account section has no zero otherwise returns true.
* @param {ICashFlowStatementSection} section
* @returns {boolean}
*/
private isAccountLeafNoneZero(section: ICashFlowStatementSection[]): boolean {
const isSchemaSectionType = R.curry(this.isSchemaSectionType);
return R.ifElse(
isSchemaSectionType(ICashFlowStatementSectionType.ACCOUNT),
this.isSectionNoneZero.bind(this),
R.always(true)
)(section);
}
/**
* Deep filters the non-zero accounts leafs of the report sections.
* @param {ICashFlowStatementSection[]} sections
* @returns {ICashFlowStatementSection[]}
*/
private filterNoneZeroAccountsLeafs(sections: ICashFlowStatementSection[]) {
return filterDeep(
sections,
this.isAccountLeafNoneZero.bind(this),
MAP_CONFIG
);
}
/**
* Deep filter the non-children sections of the report sections.
* @param {ICashFlowStatementSection[]} sections
* @returns {ICashFlowStatementSection[]}
*/
private filterNoneChildrenSections(sections: ICashFlowStatementSection[]) {
return filterDeep(
sections,
this.isAccountsSectionHasChildren.bind(this),
MAP_CONFIG
);
}
/**
* Filters the report data.
* @param {ICashFlowStatementSection[]} sections
* @returns {ICashFlowStatementSection[]}
*/
private filterReportData(sections: ICashFlowStatementSection[]) {
return R.compose(
this.filterNoneChildrenSections.bind(this),
this.filterNoneZeroAccountsLeafs.bind(this)
)(sections);
}
/**
* Schema parser.
* @param {ICashFlowSchemaSection[]} schema
* @returns {ICashFlowSchemaSection[]}
*/
private schemaParser(
schema: ICashFlowSchemaSection[]
): ICashFlowSchemaSection[] {
return R.compose(
R.when(
R.always(!this.query.noneTransactions && !this.query.noneZero),
this.filterReportData.bind(this)
),
this.totalSectionsParser.bind(this),
this.regularSectionsTotal.bind(this),
this.schemaSectionsParser.bind(this)
)(schema);
}
/**
* Retrieve the cashflow statement data.
* @return {ICashFlowStatementData}
*/
public reportData(): ICashFlowStatementData {
return this.schemaParser(R.clone(CASH_FLOW_SCHEMA));
}
}
applyMixins(CashFlowStatement, [CashFlowDatePeriods]);
export default CashFlowStatement;

View File

@@ -0,0 +1,410 @@
import * as R from 'ramda';
import { sumBy, mapValues, get } from 'lodash';
import { ACCOUNT_ROOT_TYPE } from 'data/AccountTypes';
import { accumSum, dateRangeFromToCollection } from 'utils';
import {
ICashFlowDatePeriod,
ICashFlowStatementNetIncomeSection,
ICashFlowStatementAccountSection,
ICashFlowStatementSection,
ICashFlowSchemaTotalSection,
IFormatNumberSettings,
ICashFlowStatementTotalSection,
IDateRange,
ICashFlowStatementQuery
} from 'interfaces';
export default class CashFlowStatementDatePeriods {
dateRangeSet: IDateRange[];
query: ICashFlowStatementQuery;
/**
* Initialize date range set.
*/
private initDateRangeCollection() {
this.dateRangeSet = dateRangeFromToCollection(
this.query.fromDate,
this.query.toDate,
this.comparatorDateType
);
}
/**
* Retrieve the date period meta.
* @param {number} total - Total amount.
* @param {Date} fromDate - From date.
* @param {Date} toDate - To date.
* @return {ICashFlowDatePeriod}
*/
private getDatePeriodTotalMeta(
total: number,
fromDate: Date,
toDate: Date,
overrideSettings: IFormatNumberSettings = {}
): ICashFlowDatePeriod {
return this.getDatePeriodMeta(total, fromDate, toDate, {
money: true,
...overrideSettings,
});
}
/**
* Retrieve the date period meta.
* @param {number} total - Total amount.
* @param {Date} fromDate - From date.
* @param {Date} toDate - To date.
* @return {ICashFlowDatePeriod}
*/
private getDatePeriodMeta(
total: number,
fromDate: Date,
toDate: Date,
overrideSettings?: IFormatNumberSettings
): ICashFlowDatePeriod {
return {
fromDate: this.getDateMeta(fromDate),
toDate: this.getDateMeta(toDate),
total: this.getAmountMeta(total, overrideSettings),
};
}
// Net income --------------------
/**
* Retrieve the net income between the given date range.
* @param {Date} fromDate
* @param {Date} toDate
* @returns {number}
*/
private getNetIncomeDateRange(fromDate: Date, toDate: Date) {
// Mapping income/expense accounts ids.
const incomeAccountsIds = this.getAccountsIdsByType(
ACCOUNT_ROOT_TYPE.INCOME
);
const expenseAccountsIds = this.getAccountsIdsByType(
ACCOUNT_ROOT_TYPE.EXPENSE
);
// Income closing balance.
const incomeClosingBalance = accumSum(incomeAccountsIds, (id) =>
this.netIncomeLedger
.whereFromDate(fromDate)
.whereToDate(toDate)
.whereAccountId(id)
.getClosingBalance()
);
// Expense closing balance.
const expenseClosingBalance = accumSum(expenseAccountsIds, (id) =>
this.netIncomeLedger
.whereToDate(toDate)
.whereFromDate(fromDate)
.whereAccountId(id)
.getClosingBalance()
);
// Net income = income - expenses.
const netIncome = incomeClosingBalance - expenseClosingBalance;
return netIncome;
}
/**
* Retrieve the net income of date period.
* @param {IDateRange} dateRange -
* @retrun {ICashFlowDatePeriod}
*/
private getNetIncomeDatePeriod(dateRange): ICashFlowDatePeriod {
const total = this.getNetIncomeDateRange(
dateRange.fromDate,
dateRange.toDate
);
return this.getDatePeriodMeta(total, dateRange.fromDate, dateRange.toDate);
}
/**
* Retrieve the net income node between the given date ranges.
* @param {Date} fromDate
* @param {Date} toDate
* @returns {ICashFlowDatePeriod[]}
*/
private getNetIncomeDatePeriods(
section: ICashFlowStatementNetIncomeSection
): ICashFlowDatePeriod[] {
return this.dateRangeSet.map(this.getNetIncomeDatePeriod.bind(this));
}
/**
* Writes periods property to net income section.
* @param {ICashFlowStatementNetIncomeSection} section
* @returns {ICashFlowStatementNetIncomeSection}
*/
private assocPeriodsToNetIncomeNode(
section: ICashFlowStatementNetIncomeSection
): ICashFlowStatementNetIncomeSection {
const incomeDatePeriods = this.getNetIncomeDatePeriods(section);
return R.assoc('periods', incomeDatePeriods, section);
}
// Account nodes --------------------
/**
* Retrieve the account total between date range.
* @param {Date} fromDate - From date.
* @param {Date} toDate - To date.
* @return {number}
*/
private getAccountTotalDateRange(
node: ICashFlowStatementAccountSection,
fromDate: Date,
toDate: Date
): number {
const closingBalance = this.ledger
.whereFromDate(fromDate)
.whereToDate(toDate)
.whereAccountId(node.id)
.getClosingBalance();
return this.amountAdjustment(node.adjusmentType, closingBalance);
}
/**
* Retrieve the given account node total date period.
* @param {ICashFlowStatementAccountSection} node -
* @param {Date} fromDate - From date.
* @param {Date} toDate - To date.
* @return {ICashFlowDatePeriod}
*/
private getAccountTotalDatePeriod(
node: ICashFlowStatementAccountSection,
fromDate: Date,
toDate: Date
): ICashFlowDatePeriod {
const total = this.getAccountTotalDateRange(node, fromDate, toDate);
return this.getDatePeriodMeta(total, fromDate, toDate);
}
/**
* Retrieve the accounts date periods nodes of the give account node.
* @param {ICashFlowStatementAccountSection} node -
* @return {ICashFlowDatePeriod[]}
*/
private getAccountDatePeriods(
node: ICashFlowStatementAccountSection
): ICashFlowDatePeriod[] {
return this.getNodeDatePeriods(
node,
this.getAccountTotalDatePeriod.bind(this)
);
}
/**
* Writes `periods` property to account node.
* @param {ICashFlowStatementAccountSection} node -
* @return {ICashFlowStatementAccountSection}
*/
private assocPeriodsToAccountNode(
node: ICashFlowStatementAccountSection
): ICashFlowStatementAccountSection {
const datePeriods = this.getAccountDatePeriods(node);
return R.assoc('periods', datePeriods, node);
}
// Aggregate node -------------------------
/**
* Retrieve total of the given period index for node that has children nodes.
* @return {number}
*/
private getChildrenTotalPeriodByIndex(
node: ICashFlowStatementSection,
index: number
): number {
return sumBy(node.children, `periods[${index}].total.amount`);
}
/**
* Retrieve date period meta of the given node index.
* @param {ICashFlowStatementSection} node -
* @param {number} index - Loop index.
* @param {Date} fromDate - From date.
* @param {Date} toDate - To date.
*/
private getChildrenTotalPeriodMetaByIndex(
node: ICashFlowStatementSection,
index: number,
fromDate: Date,
toDate: Date
) {
const total = this.getChildrenTotalPeriodByIndex(node, index);
return this.getDatePeriodTotalMeta(total, fromDate, toDate);
}
/**
* Retrieve the date periods of aggregate node.
* @param {ICashFlowStatementSection} node
*/
private getAggregateNodeDatePeriods(node: ICashFlowStatementSection) {
const getChildrenTotalPeriodMetaByIndex = R.curry(
this.getChildrenTotalPeriodMetaByIndex.bind(this)
)(node);
return this.dateRangeSet.map((dateRange, index) =>
getChildrenTotalPeriodMetaByIndex(
index,
dateRange.fromDate,
dateRange.toDate
)
);
}
/**
* Writes `periods` property to aggregate section node.
* @param {ICashFlowStatementSection} node -
* @return {ICashFlowStatementSection}
*/
private assocPeriodsToAggregateNode(
node: ICashFlowStatementSection
): ICashFlowStatementSection {
const datePeriods = this.getAggregateNodeDatePeriods(node);
return R.assoc('periods', datePeriods, node);
}
// Total equation node --------------------
private sectionsMapToTotalPeriod(
mappedSections: { [key: number]: any },
index
) {
return mapValues(
mappedSections,
(node) => get(node, `periods[${index}].total.amount`) || 0
);
}
/**
* Retrieve the date periods of the given total equation.
* @param {}
* @param {string} equation -
* @return {ICashFlowDatePeriod[]}
*/
private getTotalEquationDatePeriods(
node: ICashFlowSchemaTotalSection,
equation: string,
nodesTable
): ICashFlowDatePeriod[] {
return this.getNodeDatePeriods(node, (node, fromDate, toDate, index) => {
const periodScope = this.sectionsMapToTotalPeriod(nodesTable, index);
const total = this.evaluateEquation(equation, periodScope);
return this.getDatePeriodTotalMeta(total, fromDate, toDate);
});
}
/**
* Associates the total periods of total equation to the ginve total node..
* @param {ICashFlowSchemaTotalSection} totalSection -
* @return {ICashFlowStatementTotalSection}
*/
private assocTotalEquationDatePeriods(
nodesTable: any,
equation: string,
node: ICashFlowSchemaTotalSection
): ICashFlowStatementTotalSection {
const datePeriods = this.getTotalEquationDatePeriods(
node,
equation,
nodesTable
);
return R.assoc('periods', datePeriods, node);
}
// Cash at beginning ----------------------
/**
* Retrieve the date preioods of the given node and accumlated function.
* @param {} node
* @param {}
* @return {}
*/
private getNodeDatePeriods(node, callback) {
const curriedCallback = R.curry(callback)(node);
return this.dateRangeSet.map((dateRange, index) => {
return curriedCallback(dateRange.fromDate, dateRange.toDate, index);
});
}
/**
* Retrieve the account total between date range.
* @param {Date} fromDate - From date.
* @param {Date} toDate - To date.
* @return {number}
*/
private getBeginningCashAccountDateRange(
node: ICashFlowStatementSection,
fromDate: Date,
toDate: Date
) {
const cashToDate = this.beginningCashFrom(fromDate);
return this.cashLedger
.whereToDate(cashToDate)
.whereAccountId(node.id)
.getClosingBalance();
}
/**
* Retrieve the beginning cash date period.
* @param {ICashFlowStatementSection} node -
* @param {Date} fromDate - From date.
* @param {Date} toDate - To date.
* @return {ICashFlowDatePeriod}
*/
private getBeginningCashDatePeriod(
node: ICashFlowStatementSection,
fromDate: Date,
toDate: Date
) {
const total = this.getBeginningCashAccountDateRange(node, fromDate, toDate);
return this.getDatePeriodTotalMeta(total, fromDate, toDate);
}
/**
* Retrieve the beginning cash account periods.
* @param {ICashFlowStatementSection} node
* @return {ICashFlowDatePeriod}
*/
private getBeginningCashAccountPeriods(
node: ICashFlowStatementSection
): ICashFlowDatePeriod {
return this.getNodeDatePeriods(
node,
this.getBeginningCashDatePeriod.bind(this)
);
}
/**
* Writes `periods` property to cash at beginning date periods.
* @param {ICashFlowStatementSection} section -
* @return {ICashFlowStatementSection}
*/
private assocCashAtBeginningDatePeriods(
node: ICashFlowStatementSection
): ICashFlowStatementSection {
const datePeriods = this.getAggregateNodeDatePeriods(node);
return R.assoc('periods', datePeriods, node);
}
/**
* Associates `periods` propery to cash at beginning account node.
* @param {ICashFlowStatementSection} node -
* @return {ICashFlowStatementSection}
*/
private assocCashAtBeginningAccountDatePeriods(
node: ICashFlowStatementSection
): ICashFlowStatementSection {
const datePeriods = this.getBeginningCashAccountPeriods(node);
return R.assoc('periods', datePeriods, node);
}
}

View File

@@ -0,0 +1,149 @@
import { Inject, Service } from 'typedi';
import moment from 'moment';
import {
ICashFlowStatementQuery,
IAccountTransaction,
IAccount,
} from 'interfaces';
import HasTenancyService from 'services/Tenancy/TenancyService';
@Service()
export default class CashFlowRepository {
@Inject()
tenancy: HasTenancyService;
/**
* Retrieve the group type from periods type.
* @param {string} displayType
* @returns {string}
*/
protected getGroupTypeFromPeriodsType(displayType: string) {
const displayTypes = {
year: 'year',
day: 'day',
month: 'month',
quarter: 'month',
week: 'day',
};
return displayTypes[displayType] || 'month';
}
/**
* Retrieve the cashflow accounts.
* @returns {Promise<IAccount[]>}
*/
public async cashFlowAccounts(tenantId: number): Promise<IAccount[]> {
const { Account } = this.tenancy.models(tenantId);
const accounts = await Account.query();
return accounts;
}
/**
* Retrieve total of csah at beginning transactions.
* @param {number} tenantId -
* @param {ICashFlowStatementQuery} filter -
* @return {Promise<IAccountTransaction[]>}
*/
public cashAtBeginningTotalTransactions(
tenantId: number,
filter: ICashFlowStatementQuery
): Promise<IAccountTransaction[]> {
const { AccountTransaction } = this.tenancy.models(tenantId);
const cashBeginningPeriod = moment(filter.fromDate)
.subtract(1, 'day')
.toDate();
return AccountTransaction.query().onBuild((query) => {
query.modify('creditDebitSummation');
query.select('accountId');
query.groupBy('accountId');
query.withGraphFetched('account');
query.modify('filterDateRange', null, cashBeginningPeriod);
});
}
/**
* Retrieve accounts transactions.
* @param {number} tenantId -
* @param {ICashFlowStatementQuery} filter
* @return {Promise<IAccountTransaction>}
*/
public getAccountsTransactions(
tenantId: number,
filter: ICashFlowStatementQuery
): Promise<IAccountTransaction[]> {
const { AccountTransaction } = this.tenancy.models(tenantId);
const groupByDateType = this.getGroupTypeFromPeriodsType(
filter.displayColumnsBy
);
return AccountTransaction.query().onBuild((query) => {
query.modify('creditDebitSummation');
query.modify('groupByDateFormat', groupByDateType);
query.select('accountId');
query.groupBy('accountId');
query.withGraphFetched('account');
query.modify('filterDateRange', filter.fromDate, filter.toDate);
});
}
/**
* Retrieve the net income tranasctions.
* @param {number} tenantId -
* @param {ICashFlowStatementQuery} query -
* @return {Promise<IAccountTransaction[]>}
*/
public getNetIncomeTransactions(
tenantId: number,
filter: ICashFlowStatementQuery
): Promise<IAccountTransaction[]> {
const { AccountTransaction } = this.tenancy.models(tenantId);
const groupByDateType = this.getGroupTypeFromPeriodsType(
filter.displayColumnsBy
);
return AccountTransaction.query().onBuild((query) => {
query.modify('creditDebitSummation');
query.modify('groupByDateFormat', groupByDateType);
query.select('accountId');
query.groupBy('accountId');
query.withGraphFetched('account');
query.modify('filterDateRange', filter.fromDate, filter.toDate);
});
}
/**
* Retrieve peridos of cash at beginning transactions.
* @param {number} tenantId -
* @param {ICashFlowStatementQuery} filter -
* @return {Promise<IAccountTransaction[]>}
*/
public cashAtBeginningPeriodTransactions(
tenantId: number,
filter: ICashFlowStatementQuery
): Promise<IAccountTransaction[]> {
const { AccountTransaction } = this.tenancy.models(tenantId);
const groupByDateType = this.getGroupTypeFromPeriodsType(
filter.displayColumnsBy
);
return AccountTransaction.query().onBuild((query) => {
query.modify('creditDebitSummation');
query.modify('groupByDateFormat', groupByDateType);
query.select('accountId');
query.groupBy('accountId');
query.withGraphFetched('account');
query.modify('filterDateRange', filter.fromDate, filter.toDate);
});
}
}

View File

@@ -0,0 +1,144 @@
import moment from 'moment';
import { Service, Inject } from 'typedi';
import * as R from 'ramda';
import TenancyService from 'services/Tenancy/TenancyService';
import FinancialSheet from '../FinancialSheet';
import {
ICashFlowStatementService,
ICashFlowStatementQuery,
ICashFlowStatement,
IAccountTransaction,
} from 'interfaces';
import CashFlowStatement from './CashFlow';
import Ledger from 'services/Accounting/Ledger';
import CashFlowRepository from './CashFlowRepository';
@Service()
export default class CashFlowStatementService
extends FinancialSheet
implements ICashFlowStatementService
{
@Inject()
tenancy: TenancyService;
@Inject()
cashFlowRepo: CashFlowRepository;
/**
* Defaults balance sheet filter query.
* @return {IBalanceSheetQuery}
*/
get defaultQuery(): ICashFlowStatementQuery {
return {
displayColumnsType: 'total',
displayColumnsBy: 'day',
fromDate: moment().startOf('year').format('YYYY-MM-DD'),
toDate: moment().endOf('year').format('YYYY-MM-DD'),
numberFormat: {
precision: 2,
divideOn1000: false,
showZero: false,
formatMoney: 'total',
negativeFormat: 'mines',
},
noneZero: false,
noneTransactions: false,
basis: 'cash',
};
}
/**
* Retrieve cash at beginning transactions.
* @param {number} tenantId -
* @param {ICashFlowStatementQuery} filter -
* @retrun {Promise<IAccountTransaction[]>}
*/
private async cashAtBeginningTransactions(
tenantId: number,
filter: ICashFlowStatementQuery
): Promise<IAccountTransaction[]> {
const appendPeriodsOperToChain = (trans) =>
R.append(
this.cashFlowRepo.cashAtBeginningPeriodTransactions(tenantId, filter),
trans
);
const promisesChain = R.pipe(
R.append(
this.cashFlowRepo.cashAtBeginningTotalTransactions(tenantId, filter)
),
R.when(
R.always(R.equals(filter.displayColumnsType, 'date_periods')),
appendPeriodsOperToChain
)
)([]);
const promisesResults = await Promise.all(promisesChain);
const transactions = R.flatten(promisesResults);
return transactions;
}
/**
* Retrieve the cash flow sheet statement.
* @param {number} tenantId
* @param {ICashFlowStatementQuery} query
* @returns {Promise<ICashFlowStatement>}
*/
public async cashFlow(
tenantId: number,
query: ICashFlowStatementQuery
): Promise<{
data: ICashFlowStatement;
query: ICashFlowStatementQuery;
}> {
// Retrieve all accounts on the storage.
const accounts = await this.cashFlowRepo.cashFlowAccounts(tenantId);
// Settings tenant service.
const settings = this.tenancy.settings(tenantId);
const baseCurrency = settings.get({
group: 'organization',
key: 'base_currency',
});
const filter = {
...this.defaultQuery,
...query,
};
// Retrieve the accounts transactions.
const transactions = await this.cashFlowRepo.getAccountsTransactions(
tenantId,
filter
);
// Retrieve the net income transactions.
const netIncome = await this.cashFlowRepo.getNetIncomeTransactions(
tenantId,
filter
);
// Retrieve the cash at beginning transactions.
const cashAtBeginningTransactions = await this.cashAtBeginningTransactions(
tenantId,
filter
);
// Transformes the transactions to ledgers.
const ledger = Ledger.fromTransactions(transactions);
const cashLedger = Ledger.fromTransactions(cashAtBeginningTransactions);
const netIncomeLedger = Ledger.fromTransactions(netIncome);
// Cash flow statement.
const cashFlowInstance = new CashFlowStatement(
accounts,
ledger,
cashLedger,
netIncomeLedger,
filter,
baseCurrency
);
return {
data: cashFlowInstance.reportData(),
query: filter,
};
}
}

View File

@@ -0,0 +1,365 @@
import * as R from 'ramda';
import { isEmpty } from 'lodash';
import moment from 'moment';
import {
ICashFlowStatementSection,
ICashFlowStatementSectionType,
ICashFlowStatement,
ITableRow,
ITableColumn,
ICashFlowStatementQuery,
IDateRange
} from 'interfaces';
import { dateRangeFromToCollection, tableRowMapper } from 'utils';
import { mapValuesDeep } from 'utils/deepdash';
enum IROW_TYPE {
AGGREGATE = 'AGGREGATE',
NET_INCOME = 'NET_INCOME',
ACCOUNTS = 'ACCOUNTS',
ACCOUNT = 'ACCOUNT',
TOTAL = 'TOTAL',
}
const DEEP_CONFIG = { childrenPath: 'children', pathFormat: 'array' };
const DISPLAY_COLUMNS_BY = {
DATE_PERIODS: 'date_periods',
TOTAL: 'total',
};
export default class CashFlowTable implements ICashFlowTable {
private report: {
data: ICashFlowStatement;
query: ICashFlowStatementQuery;
};
private dateRangeSet: IDateRange[];
/**
* Constructor method.
* @param {ICashFlowStatement} reportStatement
*/
constructor(reportStatement: {
data: ICashFlowStatement;
query: ICashFlowStatementQuery;
}) {
this.report = reportStatement;
this.dateRangeSet = [];
this.initDateRangeCollection();
}
/**
* Initialize date range set.
*/
private initDateRangeCollection() {
this.dateRangeSet = dateRangeFromToCollection(
this.report.query.fromDate,
this.report.query.toDate,
this.report.query.displayColumnsBy,
);
}
/**
* Retrieve the date periods columns accessors.
*/
private datePeriodsColumnsAccessors() {
return this.dateRangeSet.map((dateRange: IDateRange, index) => ({
key: `date-range-${index}`,
accessor: `periods[${index}].total.formattedAmount`,
}));
}
/**
* Retrieve the total column accessor.
*/
private totalColumnAccessor() {
return [{ key: 'total', accessor: 'total.formattedAmount' }];
}
/**
* Retrieve the common columns for all report nodes.
*/
private commonColumns() {
return R.compose(
R.concat([{ key: 'label', accessor: 'label' }]),
R.when(
R.always(this.isDisplayColumnsBy(DISPLAY_COLUMNS_BY.DATE_PERIODS)),
R.concat(this.datePeriodsColumnsAccessors()),
),
R.concat(this.totalColumnAccessor()),
)([]);
}
/**
* Retrieve the table rows of regular section.
* @param {ICashFlowStatementSection} section
* @returns {ITableRow[]}
*/
private regularSectionMapper(section: ICashFlowStatementSection): ITableRow {
const columns = this.commonColumns();
return tableRowMapper(section, columns, {
rowTypes: [IROW_TYPE.AGGREGATE],
id: section.id,
});
}
/**
* Retrieve the net income table rows of the section.
* @param {ICashFlowStatementSection} section
* @returns {ITableRow}
*/
private netIncomeSectionMapper(
section: ICashFlowStatementSection
): ITableRow {
const columns = this.commonColumns();
return tableRowMapper(section, columns, {
rowTypes: [IROW_TYPE.NET_INCOME, IROW_TYPE.TOTAL],
id: section.id,
});
}
/**
* Retrieve the accounts table rows of the section.
* @param {ICashFlowStatementSection} section
* @returns {ITableRow}
*/
private accountsSectionMapper(section: ICashFlowStatementSection): ITableRow {
const columns = this.commonColumns();
return tableRowMapper(section, columns, {
rowTypes: [IROW_TYPE.ACCOUNTS],
id: section.id,
});
}
/**
* Retrieve the account table row of account section.
* @param {ICashFlowStatementSection} section
* @returns {ITableRow}
*/
private accountSectionMapper(section: ICashFlowStatementSection): ITableRow {
const columns = this.commonColumns();
return tableRowMapper(section, columns, {
rowTypes: [IROW_TYPE.ACCOUNT],
id: `account-${section.id}`,
});
}
/**
* Retrieve the total table rows from the given total section.
* @param {ICashFlowStatementSection} section
* @returns {ITableRow}
*/
private totalSectionMapper(section: ICashFlowStatementSection): ITableRow {
const columns = this.commonColumns();
return tableRowMapper(section, columns, {
rowTypes: [IROW_TYPE.TOTAL],
id: section.id,
});
}
/**
* Detarmines the schema section type.
* @param {string} type
* @param {ICashFlowSchemaSection} section
* @returns {boolean}
*/
private isSectionHasType(
type: string,
section: ICashFlowStatementSection
): boolean {
return type === section.sectionType;
}
/**
* The report section mapper.
* @param {ICashFlowStatementSection} section
* @returns {ITableRow}
*/
private sectionMapper(
section: ICashFlowStatementSection,
key: string,
parentSection: ICashFlowStatementSection
): ITableRow {
const isSectionHasType = R.curry(this.isSectionHasType);
return R.pipe(
R.when(
isSectionHasType(ICashFlowStatementSectionType.REGULAR),
this.regularSectionMapper.bind(this)
),
R.when(
isSectionHasType(ICashFlowStatementSectionType.CASH_AT_BEGINNING),
this.regularSectionMapper.bind(this)
),
R.when(
isSectionHasType(ICashFlowStatementSectionType.NET_INCOME),
this.netIncomeSectionMapper.bind(this)
),
R.when(
isSectionHasType(ICashFlowStatementSectionType.ACCOUNTS),
this.accountsSectionMapper.bind(this)
),
R.when(
isSectionHasType(ICashFlowStatementSectionType.ACCOUNT),
this.accountSectionMapper.bind(this)
),
R.when(
isSectionHasType(ICashFlowStatementSectionType.TOTAL),
this.totalSectionMapper.bind(this)
)
)(section);
}
/**
* Mappes the sections to the table rows.
* @param {ICashFlowStatementSection[]} sections
* @returns {ITableRow[]}
*/
private mapSectionsToTableRows(
sections: ICashFlowStatementSection[]
): ITableRow[] {
return mapValuesDeep(sections, this.sectionMapper.bind(this), DEEP_CONFIG);
}
/**
* Appends the total to section's children.
* @param {ICashFlowStatementSection} section
* @returns {ICashFlowStatementSection}
*/
private appendTotalToSectionChildren(
section: ICashFlowStatementSection
): ICashFlowStatementSection {
const label = section.footerLabel
? section.footerLabel
: `Total ${section.label}`;
section.children.push({
sectionType: ICashFlowStatementSectionType.TOTAL,
label,
periods: section.periods,
total: section.total,
});
return section;
}
/**
*
* @param {ICashFlowStatementSection} section
* @returns {ICashFlowStatementSection}
*/
private mapSectionsToAppendTotalChildren(
section: ICashFlowStatementSection
): ICashFlowStatementSection {
const isSectionHasChildren = (section) => !isEmpty(section.children);
return R.compose(
R.when(isSectionHasChildren, this.appendTotalToSectionChildren.bind(this))
)(section);
}
/**
* Appends total node to children section.
* @param {ICashFlowStatementSection[]} sections
* @returns {ICashFlowStatementSection[]}
*/
private appendTotalToChildren(sections: ICashFlowStatementSection[]) {
return mapValuesDeep(
sections,
this.mapSectionsToAppendTotalChildren.bind(this),
DEEP_CONFIG
);
}
/**
* Retrieve the table rows of cash flow statement.
* @param {ICashFlowStatementSection[]} sections
* @returns {ITableRow[]}
*/
public tableRows(): ITableRow[] {
const sections = this.report.data;
return R.pipe(
this.appendTotalToChildren.bind(this),
this.mapSectionsToTableRows.bind(this)
)(sections);
}
/**
* Retrieve the total columns.
* @returns {ITableColumn}
*/
private totalColumns(): ITableColumn[] {
return [{ key: 'total', label: 'Total' }];
}
/**
* Retrieve the formatted column label from the given date range.
* @param {ICashFlowDateRange} dateRange -
* @return {string}
*/
private formatColumnLabel(dateRange: ICashFlowDateRange) {
const monthFormat = (range) => moment(range.toDate).format('YYYY-MM');
const yearFormat = (range) => moment(range.toDate).format('YYYY');
const dayFormat = (range) => moment(range.toDate).format('YYYY-MM-DD');
const conditions = [
['month', monthFormat],
['year', yearFormat],
['day', dayFormat],
['quarter', monthFormat],
['week', dayFormat],
];
const conditionsPairs = R.map(([type, formatFn]) => ([
R.always(this.isDisplayColumnsType(type)), formatFn,
]), conditions);
return R.compose(R.cond(conditionsPairs))(dateRange);
}
/**
* Date periods columns.
* @returns {ITableColumn[]}
*/
private datePeriodsColumns(): ITableColumn[] {
return this.dateRangeSet.map((dateRange, index) => ({
key: `date-range-${index}`,
label: this.formatColumnLabel(dateRange),
}));
}
/**
* Detarmines the given column type is the current.
* @reutrns {boolean}
*/
private isDisplayColumnsBy(displayColumnsType: string): Boolean {
return this.report.query.displayColumnsType === displayColumnsType;
}
/**
* Detarmines whether the given display columns type is the current.
* @param {string} displayColumnsBy
* @returns {boolean}
*/
private isDisplayColumnsType(displayColumnsBy: string): Boolean {
return this.report.query.displayColumnsBy === displayColumnsBy;
}
/**
* Retrieve the table columns.
* @return {ITableColumn[]}
*/
public tableColumns(): ITableColumn[] {
return R.compose(
R.concat([{ key: 'name', label: 'Account name' }]),
R.when(
R.always(this.isDisplayColumnsBy(DISPLAY_COLUMNS_BY.DATE_PERIODS)),
R.concat(this.datePeriodsColumns())
),
R.concat(this.totalColumns())
)([]);
}
}

View File

@@ -0,0 +1,75 @@
import { ICashFlowSchemaSection, CASH_FLOW_SECTION_ID, ICashFlowStatementSectionType } from 'interfaces';
import { ACCOUNT_TYPE } from 'data/AccountTypes';
export default [
{
id: CASH_FLOW_SECTION_ID.OPERATING,
label: 'OPERATING ACTIVITIES',
sectionType: ICashFlowStatementSectionType.AGGREGATE,
children: [
{
id: CASH_FLOW_SECTION_ID.NET_INCOME,
label: 'Net income',
sectionType: ICashFlowStatementSectionType.NET_INCOME,
},
{
id: CASH_FLOW_SECTION_ID.OPERATING_ACCOUNTS,
label: 'Adjustments net income by operating activities.',
sectionType: ICashFlowStatementSectionType.ACCOUNTS,
accountsRelations: [
{ type: ACCOUNT_TYPE.ACCOUNTS_RECEIVABLE, direction: 'mines' },
{ type: ACCOUNT_TYPE.INVENTORY, direction: 'mines' },
{ type: ACCOUNT_TYPE.NON_CURRENT_ASSET, direction: 'mines' },
{ type: ACCOUNT_TYPE.ACCOUNTS_PAYABLE, direction: 'plus' },
{ type: ACCOUNT_TYPE.CREDIT_CARD, direction: 'plus' },
{ type: ACCOUNT_TYPE.TAX_PAYABLE, direction: 'plus' },
{ type: ACCOUNT_TYPE.OTHER_CURRENT_ASSET, direction: 'mines' },
{ type: ACCOUNT_TYPE.OTHER_CURRENT_LIABILITY, direction: 'plus' },
{ type: ACCOUNT_TYPE.NON_CURRENT_LIABILITY, direction: 'plus' },
],
showAlways: true,
},
],
footerLabel: 'Net cash provided by operating activities',
},
{
id: CASH_FLOW_SECTION_ID.INVESTMENT,
sectionType: ICashFlowStatementSectionType.ACCOUNTS,
label: 'INVESTMENT ACTIVITIES',
accountsRelations: [
{ type: ACCOUNT_TYPE.FIXED_ASSET, direction: 'mines' }
],
footerLabel: 'Net cash provided by investing activities',
},
{
id: CASH_FLOW_SECTION_ID.FINANCIAL,
label: 'FINANCIAL ACTIVITIES',
sectionType: ICashFlowStatementSectionType.ACCOUNTS,
accountsRelations: [
{ type: ACCOUNT_TYPE.LOGN_TERM_LIABILITY, direction: 'plus' },
{ type: ACCOUNT_TYPE.EQUITY, direction: 'plus' },
],
footerLabel: 'Net cash provided by financing activities',
},
{
id: CASH_FLOW_SECTION_ID.CASH_BEGINNING_PERIOD,
sectionType: ICashFlowStatementSectionType.CASH_AT_BEGINNING,
label: 'Cash at beginning of period',
accountsRelations: [
{ type: ACCOUNT_TYPE.CASH, direction: 'plus' },
{ type: ACCOUNT_TYPE.BANK, direction: 'plus' },
],
},
{
id: CASH_FLOW_SECTION_ID.NET_CASH_INCREASE,
sectionType: ICashFlowStatementSectionType.TOTAL,
equation: 'OPERATING + INVESTMENT + FINANCIAL',
label: 'NET CASH INCREASE FOR PERIOD',
},
{
id: CASH_FLOW_SECTION_ID.CASH_END_PERIOD,
label: 'CASH AT END OF PERIOD',
sectionType: ICashFlowStatementSectionType.TOTAL,
equation: 'NET_CASH_INCREASE + CASH_BEGINNING_PERIOD',
},
] as ICashFlowSchemaSection[];

View File

@@ -0,0 +1,69 @@
import { Inject, Service } from 'typedi';
import { map, isEmpty } from 'lodash';
import { ICustomer, IAccount } from 'interfaces';
import HasTenancyService from 'services/Tenancy/TenancyService';
import { ACCOUNT_TYPE } from 'data/AccountTypes';
@Service()
export default class CustomerBalanceSummaryRepository {
@Inject()
tenancy: HasTenancyService;
/**
* Retrieve the report customers.
* @param {number} tenantId
* @param {number[]} customersIds
* @returns {ICustomer[]}
*/
public getCustomers(tenantId: number, customersIds: number[]): ICustomer[] {
const { Customer } = this.tenancy.models(tenantId);
return Customer.query()
.orderBy('displayName')
.onBuild((query) => {
if (!isEmpty(customersIds)) {
query.whereIn('id', customersIds);
}
});
}
/**
* Retrieve the A/R accounts.
* @param {number} tenantId
* @returns {Promise<IAccount[]>}
*/
public getReceivableAccounts(tenantId: number): Promise<IAccount> {
const { Account } = this.tenancy.models(tenantId);
return Account.query().where(
'accountType',
ACCOUNT_TYPE.ACCOUNTS_RECEIVABLE
);
}
/**
* Retrieve the customers credit/debit totals
* @param {number} tenantId
* @returns
*/
public async getCustomersTransactions(tenantId: number, asDate: any) {
const { AccountTransaction } = this.tenancy.models(tenantId);
// Retrieve the receivable accounts A/R.
const receivableAccounts = await this.getReceivableAccounts(tenantId);
const receivableAccountsIds = map(receivableAccounts, 'id');
// Retrieve the customers transactions of A/R accounts.
const customersTranasctions = await AccountTransaction.query().onBuild(
(query) => {
query.whereIn('accountId', receivableAccountsIds);
query.modify('filterDateRange', null, asDate);
query.groupBy('contactId');
query.sum('credit as credit');
query.sum('debit as debit');
query.select('contactId');
}
);
return customersTranasctions;
}
}

View File

@@ -7,20 +7,26 @@ import {
ICustomerBalanceSummaryService, ICustomerBalanceSummaryService,
ICustomerBalanceSummaryQuery, ICustomerBalanceSummaryQuery,
ICustomerBalanceSummaryStatement, ICustomerBalanceSummaryStatement,
ICustomer ICustomer,
ILedgerEntry,
} from 'interfaces'; } from 'interfaces';
import { CustomerBalanceSummaryReport } from './CustomerBalanceSummary'; import { CustomerBalanceSummaryReport } from './CustomerBalanceSummary';
import { ACCOUNT_TYPE } from 'data/AccountTypes';
import Ledger from 'services/Accounting/Ledger'; import Ledger from 'services/Accounting/Ledger';
import CustomerBalanceSummaryRepository from './CustomerBalanceSummaryRepository';
export default class CustomerBalanceSummaryService export default class CustomerBalanceSummaryService
implements ICustomerBalanceSummaryService { implements ICustomerBalanceSummaryService
{
@Inject() @Inject()
tenancy: TenancyService; tenancy: TenancyService;
@Inject('logger') @Inject('logger')
logger: any; logger: any;
@Inject()
reportRepository: CustomerBalanceSummaryRepository;
/** /**
* Defaults balance sheet filter query. * Defaults balance sheet filter query.
* @return {ICustomerBalanceSummaryQuery} * @return {ICustomerBalanceSummaryQuery}
@@ -43,64 +49,24 @@ export default class CustomerBalanceSummaryService
}; };
} }
/**
* Retrieve the A/R accounts.
* @param tenantId
* @returns
*/
private getReceivableAccounts(tenantId: number) {
const { Account } = this.tenancy.models(tenantId);
return Account.query().where(
'accountType',
ACCOUNT_TYPE.ACCOUNTS_RECEIVABLE
);
}
/** /**
* Retrieve the customers credit/debit totals * Retrieve the customers ledger entries mapped from accounts transactions.
* @param {number} tenantId * @param {number} tenantId
* @returns * @param {Date|string} asDate
* @returns {Promise<ILedgerEntry[]>}
*/ */
private async getReportCustomersTransactions(tenantId: number, asDate: any) { private async getReportCustomersEntries(
const { AccountTransaction } = this.tenancy.models(tenantId); tenantId: number,
asDate: Date | string
// Retrieve the receivable accounts A/R. ): Promise<ILedgerEntry[]> {
const receivableAccounts = await this.getReceivableAccounts(tenantId); const transactions = await this.reportRepository.getCustomersTransactions(
const receivableAccountsIds = map(receivableAccounts, 'id'); tenantId,
asDate
// Retrieve the customers transactions of A/R accounts.
const customersTranasctions = await AccountTransaction.query().onBuild(
(query) => {
query.whereIn('accountId', receivableAccountsIds);
query.modify('filterDateRange', null, asDate);
query.groupBy('contactId');
query.sum('credit as credit');
query.sum('debit as debit');
query.select('contactId');
}
); );
const commonProps = { accountNormal: 'debit', date: asDate }; const commonProps = { accountNormal: 'debit', date: asDate };
return R.map(R.merge(commonProps))(customersTranasctions); return R.map(R.merge(commonProps))(transactions);
}
/**
* Retrieve the report customers.
* @param {number} tenantId
* @param {number[]} customersIds
* @returns {ICustomer[]}
*/
private getReportCustomers(tenantId: number, customersIds: number[]): ICustomer[] {
const { Customer } = this.tenancy.models(tenantId);
return Customer.query()
.orderBy('displayName')
.onBuild((query) => {
if (!isEmpty(customersIds)) {
query.whereIn('id', customersIds);
}
});
} }
/** /**
@@ -130,17 +96,17 @@ export default class CustomerBalanceSummaryService
} }
); );
// Retrieve the customers list ordered by the display name. // Retrieve the customers list ordered by the display name.
const customers = await this.getReportCustomers( const customers = await this.reportRepository.getCustomers(
tenantId, tenantId,
query.customersIds query.customersIds
); );
// Retrieve the customers debit/credit totals. // Retrieve the customers debit/credit totals.
const customersTransactions = await this.getReportCustomersTransactions( const customersEntries = await this.getReportCustomersEntries(
tenantId, tenantId,
filter.asDate filter.asDate
); );
// Ledger query. // Ledger query.
const ledger = Ledger.fromTransactions(customersTransactions); const ledger = new Ledger(customersEntries);
// Report instance. // Report instance.
const report = new CustomerBalanceSummaryReport( const report = new CustomerBalanceSummaryReport(
@@ -153,7 +119,7 @@ export default class CustomerBalanceSummaryService
return { return {
data: report.reportData(), data: report.reportData(),
columns: report.reportColumns(), columns: report.reportColumns(),
query: filter query: filter,
}; };
} }
} }

View File

@@ -1,4 +1,9 @@
import { IFormatNumberSettings, INumberFormatQuery } from 'interfaces'; import moment from 'moment';
import {
ICashFlowStatementTotal,
IFormatNumberSettings,
INumberFormatQuery,
} from 'interfaces';
import { formatNumber } from 'utils'; import { formatNumber } from 'utils';
export default class FinancialSheet { export default class FinancialSheet {
@@ -37,7 +42,7 @@ export default class FinancialSheet {
}; };
return formatNumber(number, settings); return formatNumber(number, settings);
} }
/** /**
* Formatting full amount with different format settings. * Formatting full amount with different format settings.
* @param {number} amount - * @param {number} amount -
@@ -52,24 +57,68 @@ export default class FinancialSheet {
return this.formatNumber(amount, { return this.formatNumber(amount, {
money: numberFormat.formatMoney === 'none' ? false : true, money: numberFormat.formatMoney === 'none' ? false : true,
excerptZero: false, excerptZero: false,
...settings ...settings,
}); });
} }
/** /**
* Formates the amount to the percentage string. * Formates the amount to the percentage string.
* @param {number} amount * @param {number} amount
* @returns {string} * @returns {string}
*/ */
protected formatPercentage( protected formatPercentage(amount): string {
amount
): string {
const percentage = amount * 100; const percentage = amount * 100;
return formatNumber(percentage, { return formatNumber(percentage, {
symbol: '%', symbol: '%',
excerptZero: true, excerptZero: true,
money: false, money: false,
}) });
}
/**
* Retrieve the amount meta object.
* @param {number} amount
* @returns {ICashFlowStatementTotal}
*/
protected getAmountMeta(
amount: number,
overrideSettings?: IFormatNumberSettings
): ICashFlowStatementTotal {
return {
amount,
formattedAmount: this.formatNumber(amount, overrideSettings),
currencyCode: this.baseCurrency,
};
}
/**
* Retrieve the total amount meta object.
* @param {number} amount
* @returns {ICashFlowStatementTotal}
*/
protected getTotalAmountMeta(
amount: number,
title?: string
): ICashFlowStatementTotal {
return {
...(title ? { title } : {}),
amount,
formattedAmount: this.formatTotalNumber(amount),
currencyCode: this.baseCurrency,
};
}
/**
* Retrieve the date meta.
* @param {Date} date
* @param {string} format
* @returns
*/
protected getDateMeta(date: Date, format = 'YYYY-MM-DD') {
return {
formattedDate: moment(date).format(format),
date: moment(date).toDate(),
};
} }
} }

View File

@@ -0,0 +1,407 @@
import * as R from 'ramda';
import { defaultTo, sumBy, get } from 'lodash';
import {
IInventoryDetailsQuery,
IItem,
IInventoryTransaction,
TInventoryTransactionDirection,
IInventoryDetailsNumber,
IInventoryDetailsDate,
IInventoryDetailsData,
IInventoryDetailsItem,
IInventoryDetailsClosing,
INumberFormatQuery,
IInventoryDetailsOpening,
IInventoryDetailsItemTransaction,
IFormatNumberSettings,
} from 'interfaces';
import FinancialSheet from '../FinancialSheet';
import { transformToMapBy, transformToMapKeyValue } from 'utils';
import { filterDeep } from 'utils/deepdash';
import moment from 'moment';
const MAP_CONFIG = { childrenPath: 'children', pathFormat: 'array' };
enum INodeTypes {
ITEM = 'item',
TRANSACTION = 'transaction',
OPENING_ENTRY = 'OPENING_ENTRY',
CLOSING_ENTRY = 'CLOSING_ENTRY',
}
export default class InventoryDetails extends FinancialSheet {
readonly inventoryTransactionsByItemId: Map<number, IInventoryTransaction[]>;
readonly openingBalanceTransactions: Map<number, IInventoryTransaction>;
readonly query: IInventoryDetailsQuery;
readonly numberFormat: INumberFormatQuery;
readonly baseCurrency: string;
readonly items: IItem[];
/**
* Constructor method.
* @param {IItem[]} items - Items.
* @param {IInventoryTransaction[]} inventoryTransactions - Inventory transactions.
* @param {IInventoryDetailsQuery} query - Report query.
* @param {string} baseCurrency - The base currency.
*/
constructor(
items: IItem[],
openingBalanceTransactions: IInventoryTransaction[],
inventoryTransactions: IInventoryTransaction[],
query: IInventoryDetailsQuery,
baseCurrency: string
) {
super();
this.inventoryTransactionsByItemId = transformToMapBy(
inventoryTransactions,
'itemId'
);
this.openingBalanceTransactions = transformToMapKeyValue(
openingBalanceTransactions,
'itemId'
);
this.query = query;
this.numberFormat = this.query.numberFormat;
this.items = items;
this.baseCurrency = baseCurrency;
}
/**
* Retrieve the number meta.
* @param {number} number
* @returns
*/
private getNumberMeta(
number: number,
settings?: IFormatNumberSettings
): IInventoryDetailsNumber {
return {
formattedNumber: this.formatNumber(number, {
excerptZero: true,
money: false,
...settings,
}),
number: number,
};
}
/**
* Retrieve the total number meta.
* @param {number} number -
* @param {IFormatNumberSettings} settings -
* @retrun {IInventoryDetailsNumber}
*/
private getTotalNumberMeta(
number: number,
settings?: IFormatNumberSettings
): IInventoryDetailsNumber {
return this.getNumberMeta(number, { excerptZero: false, ...settings });
}
/**
* Retrieve the date meta.
* @param {Date|string} date
* @returns {IInventoryDetailsDate}
*/
private getDateMeta(date: Date | string): IInventoryDetailsDate {
return {
formattedDate: moment(date).format('YYYY-MM-DD'),
date: moment(date).toDate(),
};
}
/**
* Adjusts the movement amount.
* @param {number} amount
* @param {TInventoryTransactionDirection} direction
* @returns {number}
*/
private adjustAmountMovement(
direction: TInventoryTransactionDirection,
amount: number
): number {
return direction === 'OUT' ? amount * -1 : amount;
}
/**
* Accumlate and mapping running quantity on transactions.
* @param {IInventoryDetailsItemTransaction[]} transactions
* @returns {IInventoryDetailsItemTransaction[]}
*/
private mapAccumTransactionsRunningQuantity(
transactions: IInventoryDetailsItemTransaction[]
): IInventoryDetailsItemTransaction[] {
const initial = this.getNumberMeta(0);
const mapAccumAppender = (a, b) => {
const total = a.runningQuantity.number + b.quantityMovement.number;
const totalMeta = this.getNumberMeta(total, { excerptZero: false });
const accum = { ...b, runningQuantity: totalMeta };
return [accum, accum];
};
return R.mapAccum(
mapAccumAppender,
{ runningQuantity: initial },
transactions
)[1];
}
/**
* Accumlate and mapping running valuation on transactions.
* @param {IInventoryDetailsItemTransaction[]} transactions
* @returns {IInventoryDetailsItemTransaction}
*/
private mapAccumTransactionsRunningValuation(
transactions: IInventoryDetailsItemTransaction[]
): IInventoryDetailsItemTransaction[] {
const initial = this.getNumberMeta(0);
const mapAccumAppender = (a, b) => {
const total = a.runningValuation.number + b.valueMovement.number;
const totalMeta = this.getNumberMeta(total, { excerptZero: false });
const accum = { ...b, runningValuation: totalMeta };
return [accum, accum];
};
return R.mapAccum(
mapAccumAppender,
{ runningValuation: initial },
transactions
)[1];
}
/**
* Mappes the item transaction to inventory item transaction node.
* @param {IItem} item
* @param {IInvetoryTransaction} transaction
* @returns {IInventoryDetailsItemTransaction}
*/
private itemTransactionMapper(
item: IItem,
transaction: IInventoryTransaction
): IInventoryDetailsItemTransaction {
const value = transaction.quantity * transaction.rate;
const amountMovement = R.curry(this.adjustAmountMovement)(
transaction.direction
);
// Quantity movement.
const quantityMovement = amountMovement(transaction.quantity);
const valueMovement = amountMovement(value);
// Profit margin.
const profitMargin = Math.max(
value - transaction.costLotAggregated.cost,
0
);
return {
nodeType: INodeTypes.TRANSACTION,
date: this.getDateMeta(transaction.date),
transactionType: transaction.transcationTypeFormatted,
transactionNumber: transaction.meta.transactionNumber,
direction: transaction.direction,
quantityMovement: this.getNumberMeta(quantityMovement),
valueMovement: this.getNumberMeta(valueMovement),
quantity: this.getNumberMeta(transaction.quantity),
value: this.getNumberMeta(value),
rate: this.getNumberMeta(transaction.rate),
cost: this.getNumberMeta(transaction.costLotAggregated.cost),
profitMargin: this.getNumberMeta(profitMargin),
runningQuantity: this.getNumberMeta(0),
runningValuation: this.getNumberMeta(0),
};
}
/**
* Retrieve the inventory transcations by item id.
* @param {number} itemId
* @returns {IInventoryTransaction[]}
*/
private getInventoryTransactionsByItemId(
itemId: number
): IInventoryTransaction[] {
return defaultTo(this.inventoryTransactionsByItemId.get(itemId + ''), []);
}
/**
* Retrieve the item transaction node by the given item.
* @param {IItem} item
* @returns {IInventoryDetailsItemTransaction[]}
*/
private getItemTransactions(item: IItem): IInventoryDetailsItemTransaction[] {
const transactions = this.getInventoryTransactionsByItemId(item.id);
return R.compose(
this.mapAccumTransactionsRunningQuantity.bind(this),
this.mapAccumTransactionsRunningValuation.bind(this),
R.map(R.curry(this.itemTransactionMapper.bind(this))(item))
)(transactions);
}
/**
* Mappes the given item transactions.
* @param {IItem} item -
* @returns {(
* IInventoryDetailsItemTransaction
* | IInventoryDetailsOpening
* | IInventoryDetailsClosing
* )[]}
*/
private itemTransactionsMapper(
item: IItem
): (
| IInventoryDetailsItemTransaction
| IInventoryDetailsOpening
| IInventoryDetailsClosing
)[] {
const transactions = this.getItemTransactions(item);
const openingValuation = this.getItemOpeingValuation(item);
const closingValuation = this.getItemClosingValuation(item, transactions);
const hasTransactions = transactions.length > 0;
const isItemHasOpeningBalance = this.isItemHasOpeningBalance(item.id);
return R.pipe(
R.concat(transactions),
R.when(R.always(isItemHasOpeningBalance), R.prepend(openingValuation)),
R.when(R.always(hasTransactions), R.append(closingValuation))
)([]);
}
/**
* Detarmines the given item has opening balance transaction.
* @param {number} itemId - Item id.
* @return {boolean}
*/
private isItemHasOpeningBalance(itemId: number): boolean {
return !!this.openingBalanceTransactions.get(itemId);
}
/**
* Retrieve the given item opening valuation.
* @param {IItem} item -
* @returns {IInventoryDetailsOpening}
*/
private getItemOpeingValuation(item: IItem): IInventoryDetailsOpening {
const openingBalance = this.openingBalanceTransactions.get(item.id);
const quantity = defaultTo(get(openingBalance, 'quantity'), 0);
const value = defaultTo(get(openingBalance, 'value'), 0);
return {
nodeType: INodeTypes.OPENING_ENTRY,
date: this.getDateMeta(this.query.fromDate),
quantity: this.getTotalNumberMeta(quantity),
value: this.getTotalNumberMeta(value),
};
}
/**
* Retrieve the given item closing valuation.
* @param {IItem} item -
* @returns {IInventoryDetailsOpening}
*/
private getItemClosingValuation(
item: IItem,
transactions: IInventoryDetailsItemTransaction[]
): IInventoryDetailsOpening {
const value = sumBy(transactions, 'valueMovement.number');
const quantity = sumBy(transactions, 'quantityMovement.number');
const profitMargin = sumBy(transactions, 'profitMargin.number');
const cost = sumBy(transactions, 'cost.number');
return {
nodeType: INodeTypes.CLOSING_ENTRY,
date: this.getDateMeta(this.query.toDate),
quantity: this.getTotalNumberMeta(quantity),
value: this.getTotalNumberMeta(value),
cost: this.getTotalNumberMeta(cost),
profitMargin: this.getTotalNumberMeta(profitMargin),
};
}
/**
* Retrieve the item node of the report.
* @param {IItem} item
* @returns {IInventoryDetailsItem}
*/
private itemsNodeMapper(item: IItem): IInventoryDetailsItem {
return {
id: item.id,
name: item.name,
code: item.code,
nodeType: INodeTypes.ITEM,
children: this.itemTransactionsMapper(item),
};
}
/**
* Detarmines the given node equals the given type.
* @param {string} nodeType
* @param {IItem} node
* @returns {boolean}
*/
private isNodeTypeEquals(
nodeType: string,
node: IInventoryDetailsItem
): boolean {
return nodeType === node.nodeType;
}
/**
* Detarmines whether the given item node has transactions.
* @param {IInventoryDetailsItem} item
* @returns {boolean}
*/
private isItemNodeHasTransactions(item: IInventoryDetailsItem) {
return !!this.inventoryTransactionsByItemId.get(item.id);
}
/**
* Detarmines the filter
* @param {IInventoryDetailsItem} item
* @return {boolean}
*/
private isFilterNode(item: IInventoryDetailsItem): boolean {
return R.ifElse(
R.curry(this.isNodeTypeEquals)(INodeTypes.ITEM),
this.isItemNodeHasTransactions.bind(this),
R.always(true)
)(item);
}
/**
* Filters items nodes.
* @param {IInventoryDetailsItem[]} items -
* @returns {IInventoryDetailsItem[]}
*/
private filterItemsNodes(items: IInventoryDetailsItem[]) {
return filterDeep(items, this.isFilterNode.bind(this), MAP_CONFIG);
}
/**
* Retrieve the items nodes of the report.
* @param {IItem} items
* @returns {IInventoryDetailsItem[]}
*/
private itemsNodes(items: IItem[]): IInventoryDetailsItem[] {
return R.compose(
this.filterItemsNodes.bind(this),
R.map(this.itemsNodeMapper.bind(this))
)(items);
}
/**
* Retrieve the inventory item details report data.
* @returns {IInventoryDetailsData}
*/
public reportData(): IInventoryDetailsData {
return this.itemsNodes(this.items);
}
}

View File

@@ -0,0 +1,87 @@
import { Inject } from 'typedi';
import { raw } from 'objection';
import moment from 'moment';
import { IItem, IInventoryDetailsQuery, IInventoryTransaction } from 'interfaces';
import HasTenancyService from 'services/Tenancy/TenancyService';
export default class InventoryDetailsRepository {
@Inject()
tenancy: HasTenancyService;
/**
* Retrieve inventory items.
* @param {number} tenantId -
* @returns {Promise<IItem>}
*/
public getInventoryItems(tenantId: number): Promise<IItem[]> {
const { Item } = this.tenancy.models(tenantId);
return Item.query().where('type', 'inventory');
}
/**
* Retrieve the items opening balance transactions.
* @param {number} tenantId -
* @param {IInventoryDetailsQuery}
* @return {Promise<IInventoryTransaction>}
*/
public async openingBalanceTransactions(
tenantId: number,
filter: IInventoryDetailsQuery
): Promise<IInventoryTransaction[]> {
const { InventoryTransaction } = this.tenancy.models(tenantId);
const openingBalanceDate = moment(filter.fromDate)
.subtract(1, 'days')
.toDate();
// Opening inventory transactions.
const openingTransactions = InventoryTransaction.query()
.select('*')
.select(raw("IF(`DIRECTION` = 'IN', `QUANTITY`, 0) as 'QUANTITY_IN'"))
.select(raw("IF(`DIRECTION` = 'OUT', `QUANTITY`, 0) as 'QUANTITY_OUT'"))
.select(
raw("IF(`DIRECTION` = 'IN', `QUANTITY` * `RATE`, 0) as 'VALUE_IN'")
)
.select(
raw("IF(`DIRECTION` = 'OUT', `QUANTITY` * `RATE`, 0) as 'VALUE_OUT'")
)
.modify('filterDateRange', null, openingBalanceDate)
.as('inventory_transactions');
const openingBalanceTransactions = await InventoryTransaction.query()
.from(openingTransactions)
.select('itemId')
.select(raw('SUM(`QUANTITY_IN` - `QUANTITY_OUT`) AS `QUANTITY`'))
.select(raw('SUM(`VALUE_IN` - `VALUE_OUT`) AS `VALUE`'))
.groupBy('itemId')
.sum('rate as rate')
.sum('quantityIn as quantityIn')
.sum('quantityOut as quantityOut')
.sum('valueIn as valueIn')
.sum('valueOut as valueOut')
.withGraphFetched('itemCostAggregated');
return openingBalanceTransactions;
}
/**
* Retrieve the items inventory tranasactions.
* @param {number} tenantId -
* @param {IInventoryDetailsQuery}
* @return {Promise<IInventoryTransaction>}
*/
public async itemInventoryTransactions(
tenantId: number,
filter: IInventoryDetailsQuery
): Promise<IInventoryTransaction[]> {
const { InventoryTransaction } = this.tenancy.models(tenantId);
const inventoryTransactions = InventoryTransaction.query()
.modify('filterDateRange', filter.fromDate, filter.toDate)
.withGraphFetched('meta')
.withGraphFetched('costLotAggregated');
return inventoryTransactions;
}
}

View File

@@ -0,0 +1,81 @@
import moment from 'moment';
import { Service, Inject } from 'typedi';
import { raw } from 'objection';
import { IInventoryDetailsQuery, IInventoryTransaction } from 'interfaces';
import TenancyService from 'services/Tenancy/TenancyService';
import InventoryDetails from './InventoryDetails';
import FinancialSheet from '../FinancialSheet';
import InventoryDetailsRepository from './InventoryDetailsRepository';
@Service()
export default class InventoryDetailsService extends FinancialSheet {
@Inject()
tenancy: TenancyService;
@Inject()
reportRepo: InventoryDetailsRepository;
/**
* Defaults balance sheet filter query.
* @return {IBalanceSheetQuery}
*/
get defaultQuery(): IInventoryDetailsQuery {
return {
fromDate: moment().startOf('year').format('YYYY-MM-DD'),
toDate: moment().endOf('year').format('YYYY-MM-DD'),
numberFormat: {
precision: 2,
divideOn1000: false,
showZero: false,
formatMoney: 'total',
negativeFormat: 'mines',
},
noneTransactions: false,
};
}
/**
* Retrieve the inventory details report data.
* @param {number} tenantId -
* @param {IInventoryDetailsQuery} query -
*/
public async inventoryDetails(
tenantId: number,
query: IInventoryDetailsQuery
): Promise<any> {
// Settings tenant service.
const settings = this.tenancy.settings(tenantId);
const baseCurrency = settings.get({
group: 'organization',
key: 'base_currency',
});
const filter = {
...this.defaultQuery,
...query,
};
// Retrieves the items.
const items = await this.reportRepo.getInventoryItems(tenantId);
// Opening balance transactions.
const openingBalanceTransactions =
await this.reportRepo.openingBalanceTransactions(tenantId, filter);
// Retrieves the inventory transaction.
const inventoryTransactions =
await this.reportRepo.itemInventoryTransactions(tenantId, filter);
// Inventory details report mapper.
const inventoryDetailsInstance = new InventoryDetails(
items,
openingBalanceTransactions,
inventoryTransactions,
filter,
baseCurrency
);
return {
data: inventoryDetailsInstance.reportData(),
};
}
}

View File

@@ -0,0 +1,183 @@
import * as R from 'ramda';
import {
IInventoryDetailsItem,
IInventoryDetailsItemTransaction,
IInventoryDetailsClosing,
ITableColumn,
ITableRow,
IInventoryDetailsNode,
IInventoryDetailsOpening,
} from 'interfaces';
import { mapValuesDeep } from 'utils/deepdash';
import { tableRowMapper } from 'utils';
enum IROW_TYPE {
ITEM = 'ITEM',
TRANSACTION = 'TRANSACTION',
CLOSING_ENTRY = 'CLOSING_ENTRY',
OPENING_ENTRY = 'OPENING_ENTRY',
}
const MAP_CONFIG = { childrenPath: 'children', pathFormat: 'array' };
export default class InventoryDetailsTable {
/**
* Constructor methiod.
* @param {ICashFlowStatement} reportStatement
*/
constructor(reportStatement) {
this.report = reportStatement;
}
/**
* Mappes the item node to table rows.
* @param {IInventoryDetailsItem} item
* @returns {ITableRow}
*/
private itemNodeMapper(item: IInventoryDetailsItem) {
const columns = [{ key: 'item_name', accessor: 'name' }];
return tableRowMapper(item, columns, {
rowTypes: [IROW_TYPE.ITEM],
});
}
/**
* Mappes the item inventory transaction to table row.
* @param {IInventoryDetailsItemTransaction} transaction
* @returns {ITableRow}
*/
private itemTransactionNodeMapper(
transaction: IInventoryDetailsItemTransaction
) {
const columns = [
{ key: 'date', accessor: 'date.formattedDate' },
{ key: 'transaction_type', accessor: 'transactionType' },
{ key: 'transaction_id', accessor: 'transactionNumber' },
{
key: 'quantity_movement',
accessor: 'quantityMovement.formattedNumber',
},
{ key: 'rate', accessor: 'rate.formattedNumber' },
{ key: 'value_movement', accessor: 'valueMovement.formattedNumber' },
{ key: 'cost', accessor: 'cost.formattedNumber' },
{ key: 'profit_margin', accessor: 'profitMargin.formattedNumber' },
{ key: 'running_quantity', accessor: 'runningQuantity.formattedNumber' },
{ key: 'running_valuation', accessor: 'runningValuation.formattedNumber' },
];
return tableRowMapper(transaction, columns, {
rowTypes: [IROW_TYPE.TRANSACTION],
});
}
/**
* Opening balance transaction mapper to table row.
* @param {IInventoryDetailsOpening} transaction
* @returns {ITableRow}
*/
private openingNodeMapper(transaction: IInventoryDetailsOpening): ITableRow {
const columns = [
{ key: 'date', accessor: 'date.formattedDate' },
{ key: 'closing', value: 'Opening Balance' },
{ key: 'empty' },
{ key: 'quantity', accessor: 'quantity.formattedNumber' },
{ key: 'empty' },
{ key: 'value', accessor: 'value.formattedNumber' },
];
return tableRowMapper(transaction, columns, {
rowTypes: [IROW_TYPE.OPENING_ENTRY],
});
}
/**
* Closing balance transaction mapper to table raw.
* @param {IInventoryDetailsClosing} transaction
* @returns {ITableRow}
*/
private closingNodeMapper(transaction: IInventoryDetailsClosing): ITableRow {
const columns = [
{ key: 'date', accessor: 'date.formattedDate' },
{ key: 'closing', value: 'Closing Balance' },
{ key: 'empty' },
{ key: 'quantity', accessor: 'quantity.formattedNumber' },
{ key: 'empty' },
{ key: 'value', accessor: 'value.formattedNumber' },
{ key: 'cost', accessor: 'cost.formattedNumber' },
{ key: 'profitMargin', accessor: 'profitMargin.formattedNumber' },
];
return tableRowMapper(transaction, columns, {
rowTypes: [IROW_TYPE.CLOSING_ENTRY],
});
}
/**
* Detarmines the ginve inventory details node type.
* @param {string} type
* @param {IInventoryDetailsNode} node
* @returns {boolean}
*/
private isNodeTypeEquals(type: string, node: IInventoryDetailsNode): boolean {
return node.nodeType === type;
}
/**
* Mappes the given item or transactions node to table rows.
* @param {IInventoryDetailsNode} node -
* @return {ITableRow}
*/
private itemMapper(node: IInventoryDetailsNode): ITableRow {
return R.compose(
R.when(
R.curry(this.isNodeTypeEquals)('OPENING_ENTRY'),
this.openingNodeMapper
),
R.when(
R.curry(this.isNodeTypeEquals)('CLOSING_ENTRY'),
this.closingNodeMapper
),
R.when(R.curry(this.isNodeTypeEquals)('item'), this.itemNodeMapper),
R.when(
R.curry(this.isNodeTypeEquals)('transaction'),
this.itemTransactionNodeMapper.bind(this)
)
)(node);
}
/**
* Mappes the items nodes to table rows.
* @param {IInventoryDetailsItem[]} items
* @returns {ITableRow[]}
*/
private itemsMapper(items: IInventoryDetailsItem[]): ITableRow[] {
return mapValuesDeep(items, this.itemMapper.bind(this), MAP_CONFIG);
}
/**
* Retrieve the table rows of the inventory item details.
* @returns {ITableRow[]}
*/
public tableData(): ITableRow[] {
return this.itemsMapper(this.report.data);
}
/**
* Retrieve the table columns of inventory details report.
* @returns {ITableColumn[]}
*/
public tableColumns(): ITableColumn[] {
return [
{ key: 'date', label: 'Date' },
{ key: 'transaction_type', label: 'Transaction type' },
{ key: 'transaction_id', label: 'Transaction #' },
{ key: 'quantity_movement', label: 'Quantity' },
{ key: 'rate', label: 'Rate' },
{ key: 'value_movement', label: 'Value' },
{ key: 'cost', label: 'Cost' },
{ key: 'profit_margin', label: 'Profit Margin' },
{ key: 'quantity_on_hand', label: 'Running quantity' },
{ key: 'value', label: 'Running Value' },
];
}
}

View File

@@ -0,0 +1,92 @@
import { map } from 'lodash';
import { IAccount, IAccountTransaction } from 'interfaces';
import { ACCOUNT_TYPE } from 'data/AccountTypes';
import HasTenancyService from 'services/Tenancy/TenancyService';
import { Inject } from 'typedi';
export default class TransactionsByCustomersRepository {
@Inject()
tenancy: HasTenancyService;
/**
* Retrieve the report customers.
* @param {number} tenantId
* @returns {Promise<ICustomer[]>}
*/
public async getCustomers(tenantId: number) {
const { Customer } = this.tenancy.models(tenantId);
return Customer.query().orderBy('displayName');
}
/**
* Retrieve the accounts receivable.
* @param {number} tenantId
* @returns {Promise<IAccount[]>}
*/
public async getReceivableAccounts(tenantId: number): Promise<IAccount[]> {
const { Account } = this.tenancy.models(tenantId);
const accounts = await Account.query().where(
'accountType',
ACCOUNT_TYPE.ACCOUNTS_RECEIVABLE
);
return accounts;
}
/**
* Retrieve the customers opening balance transactions.
* @param {number} tenantId - Tenant id.
* @param {number} openingDate - Opening date.
* @param {number} customersIds - Customers ids.
* @returns {Promise<IAccountTransaction[]>}
*/
public async getCustomersOpeningBalanceTransactions(
tenantId: number,
openingDate: Date,
customersIds?: number[]
): Promise<IAccountTransaction[]> {
const { AccountTransaction } = this.tenancy.models(tenantId);
const receivableAccounts = await this.getReceivableAccounts(tenantId);
const receivableAccountsIds = map(receivableAccounts, 'id');
const openingTransactions = await AccountTransaction.query().modify(
'contactsOpeningBalance',
openingDate,
receivableAccountsIds,
customersIds
);
return openingTransactions;
}
/**
* Retrieve the customers periods transactions.
* @param {number} tenantId - Tenant id.
* @param {Date|string} openingDate - Opening date.
* @param {number[]} customersIds - Customers ids.
* @return {Promise<IAccountTransaction[]>}
*/
public async getCustomersPeriodTransactions(
tenantId: number,
fromDate: Date,
toDate: Date
): Promise<IAccountTransaction[]> {
const { AccountTransaction } = this.tenancy.models(tenantId);
const receivableAccounts = await this.getReceivableAccounts(tenantId);
const receivableAccountsIds = map(receivableAccounts, 'id');
const transactions = await AccountTransaction.query().onBuild((query) => {
// Filter by date.
query.modify('filterDateRange', fromDate, toDate);
// Filter by customers.
query.whereNot('contactId', null);
// Filter by accounts.
query.whereIn('accountId', receivableAccountsIds);
});
return transactions;
}
}

View File

@@ -1,25 +1,29 @@
import { Inject } from 'typedi'; import { Inject } from 'typedi';
import * as R from 'ramda'; import * as R from 'ramda';
import moment from 'moment'; import moment from 'moment';
import { map } from 'lodash';
import TenancyService from 'services/Tenancy/TenancyService'; import TenancyService from 'services/Tenancy/TenancyService';
import { import {
ITransactionsByCustomersService, ITransactionsByCustomersService,
ITransactionsByCustomersFilter, ITransactionsByCustomersFilter,
ITransactionsByCustomersStatement, ITransactionsByCustomersStatement,
ILedgerEntry,
} from 'interfaces'; } from 'interfaces';
import TransactionsByCustomers from './TransactionsByCustomers'; import TransactionsByCustomers from './TransactionsByCustomers';
import Ledger from 'services/Accounting/Ledger'; import Ledger from 'services/Accounting/Ledger';
import { ACCOUNT_TYPE } from 'data/AccountTypes'; import TransactionsByCustomersRepository from './TransactionsByCustomersRepository';
export default class TransactionsByCustomersService export default class TransactionsByCustomersService
implements ITransactionsByCustomersService { implements ITransactionsByCustomersService
{
@Inject() @Inject()
tenancy: TenancyService; tenancy: TenancyService;
@Inject('logger') @Inject('logger')
logger: any; logger: any;
@Inject()
reportRepository: TransactionsByCustomersRepository;
/** /**
* Defaults balance sheet filter query. * Defaults balance sheet filter query.
* @return {ICustomerBalanceSummaryQuery} * @return {ICustomerBalanceSummaryQuery}
@@ -44,43 +48,24 @@ export default class TransactionsByCustomersService
} }
/** /**
* Retrieve the accounts receivable. * Retrieve the customers opening balance ledger entries.
* @param {number} tenantId * @param {number} tenantId
* @returns * @param {Date} openingDate
* @param {number[]} customersIds
* @returns {Promise<ILedgerEntry[]>}
*/ */
async getReceivableAccounts(tenantId: number) { private async getCustomersOpeningBalanceEntries(
const { Account } = this.tenancy.models(tenantId);
const accounts = await Account.query().where(
'accountType',
ACCOUNT_TYPE.ACCOUNTS_RECEIVABLE
);
return accounts;
}
/**
* Retrieve the customers opening balance transactions.
* @param {number} tenantId
* @param {number} openingDate
* @param {number} customersIds
* @returns {}
*/
async getCustomersOpeningBalance(
tenantId: number, tenantId: number,
openingDate: Date, openingDate: Date,
customersIds?: number[] customersIds?: number[]
): Promise<ILedgerEntry[]> { ): Promise<ILedgerEntry[]> {
const { AccountTransaction } = this.tenancy.models(tenantId); const openingTransactions =
await this.reportRepository.getCustomersOpeningBalanceTransactions(
tenantId,
openingDate,
customersIds
);
const receivableAccounts = await this.getReceivableAccounts(tenantId);
const receivableAccountsIds = map(receivableAccounts, 'id');
const openingTransactions = await AccountTransaction.query().modify(
'contactsOpeningBalance',
openingDate,
receivableAccountsIds,
customersIds
);
return R.compose( return R.compose(
R.map(R.assoc('date', openingDate)), R.map(R.assoc('date', openingDate)),
R.map(R.assoc('accountNormal', 'debit')) R.map(R.assoc('accountNormal', 'debit'))
@@ -88,38 +73,29 @@ export default class TransactionsByCustomersService
} }
/** /**
* * Retrieve the customers periods ledger entries.
* @param {number} tenantId * @param {number} tenantId
* @param {Date|string} openingDate * @param {Date} fromDate
* @param {number[]} customersIds * @param {Date} toDate
* @returns {Promise<ILedgerEntry[]>}
*/ */
async getCustomersPeriodTransactions( private async getCustomersPeriodsEntries(
tenantId: number, tenantId: number,
fromDate: Date, fromDate: Date|string,
toDate: Date toDate: Date|string,
): Promise<ILedgerEntry[]> { ): Promise<ILedgerEntry[]> {
const { AccountTransaction } = this.tenancy.models(tenantId); const transactions =
await this.reportRepository.getCustomersPeriodTransactions(
const receivableAccounts = await this.getReceivableAccounts(tenantId); tenantId,
const receivableAccountsIds = map(receivableAccounts, 'id'); fromDate,
toDate
const transactions = await AccountTransaction.query().onBuild((query) => { );
// Filter by date.
query.modify('filterDateRange', fromDate, toDate);
// Filter by customers.
query.whereNot('contactId', null);
// Filter by accounts.
query.whereIn('accountId', receivableAccountsIds);
});
return R.compose( return R.compose(
R.map(R.assoc('accountNormal', 'debit')), R.map(R.assoc('accountNormal', 'debit')),
R.map((trans) => ({ R.map((trans) => ({
...trans, ...trans,
referenceTypeFormatted: trans.referenceTypeFormatted, referenceTypeFormatted: trans.referenceTypeFormatted,
})), }))
)(transactions); )(transactions);
} }
@@ -133,7 +109,6 @@ export default class TransactionsByCustomersService
tenantId: number, tenantId: number,
query: ITransactionsByCustomersFilter query: ITransactionsByCustomersFilter
): Promise<ITransactionsByCustomersStatement> { ): Promise<ITransactionsByCustomersStatement> {
const { Customer } = this.tenancy.models(tenantId);
const { accountRepository } = this.tenancy.repositories(tenantId); const { accountRepository } = this.tenancy.repositories(tenantId);
// Settings tenant service. // Settings tenant service.
@@ -148,29 +123,31 @@ export default class TransactionsByCustomersService
...query, ...query,
}; };
const accountsGraph = await accountRepository.getDependencyGraph(); const accountsGraph = await accountRepository.getDependencyGraph();
const customers = await Customer.query().orderBy('displayName');
// Retrieve the report customers.
const customers = await this.reportRepository.getCustomers(tenantId);
const openingBalanceDate = moment(filter.fromDate) const openingBalanceDate = moment(filter.fromDate)
.subtract(1, 'days') .subtract(1, 'days')
.toDate(); .toDate();
// Retrieve all ledger transactions of the opening balance of. // Retrieve all ledger transactions of the opening balance of.
const openingBalanceTransactions = await this.getCustomersOpeningBalance( const openingBalanceEntries = await this.getCustomersOpeningBalanceEntries(
tenantId, tenantId,
openingBalanceDate openingBalanceDate
); );
// Retrieve all ledger transactions between opeing and closing period. // Retrieve all ledger transactions between opeing and closing period.
const customersTransactions = await this.getCustomersPeriodTransactions( const customersTransactions = await this.getCustomersPeriodsEntries(
tenantId, tenantId,
query.fromDate, query.fromDate,
query.toDate query.toDate
); );
// Concats the opening balance and period customer ledger transactions. // Concats the opening balance and period customer ledger transactions.
const journalTransactions = [ const journalTransactions = [
...openingBalanceTransactions, ...openingBalanceEntries,
...customersTransactions, ...customersTransactions,
]; ];
const journal = Ledger.fromTransactions(journalTransactions); const journal = new Ledger(journalTransactions);
// Transactions by customers data mapper. // Transactions by customers data mapper.
const reportInstance = new TransactionsByCustomers( const reportInstance = new TransactionsByCustomers(

View File

@@ -0,0 +1,92 @@
import { Inject, Service } from 'typedi';
import { map } from 'lodash';
import { IVendor, IAccount, IAccountTransaction } from 'interfaces';
import HasTenancyService from 'services/Tenancy/TenancyService';
import { ACCOUNT_TYPE } from 'data/AccountTypes';
@Service()
export default class TransactionsByVendorRepository {
@Inject()
tenancy: HasTenancyService;
/**
* Retrieve the report vendors.
* @param {number} tenantId
* @returns {Promise<IVendor[]>}
*/
public getVendors(tenantId: number): Promise<IVendor[]> {
const { Vendor } = this.tenancy.models(tenantId);
return Vendor.query().orderBy('displayName');
}
/**
* Retrieve the accounts receivable.
* @param {number} tenantId
* @returns {Promise<IAccount[]>}
*/
private async getPayableAccounts(tenantId: number): Promise<IAccount[]> {
const { Account } = this.tenancy.models(tenantId);
const accounts = await Account.query().where(
'accountType',
ACCOUNT_TYPE.ACCOUNTS_PAYABLE
);
return accounts;
}
/**
* Retrieve the customers opening balance transactions.
* @param {number} tenantId
* @param {number} openingDate
* @param {number} customersIds
* @returns {}
*/
public async getVendorsOpeningBalance(
tenantId: number,
openingDate: Date,
customersIds?: number[]
): Promise<IAccountTransaction[]> {
const { AccountTransaction } = this.tenancy.models(tenantId);
const payableAccounts = await this.getPayableAccounts(tenantId);
const payableAccountsIds = map(payableAccounts, 'id');
const openingTransactions = await AccountTransaction.query().modify(
'contactsOpeningBalance',
openingDate,
payableAccountsIds,
customersIds
);
return openingTransactions;
}
/**
* Retrieve vendors periods transactions.
* @param {number} tenantId
* @param {Date|string} openingDate
* @param {number[]} customersIds
*/
public async getVendorsPeriodTransactions(
tenantId: number,
fromDate: Date,
toDate: Date
): Promise<IAccountTransaction[]> {
const { AccountTransaction } = this.tenancy.models(tenantId);
const receivableAccounts = await this.getPayableAccounts(tenantId);
const receivableAccountsIds = map(receivableAccounts, 'id');
const transactions = await AccountTransaction.query().onBuild((query) => {
// Filter by date.
query.modify('filterDateRange', fromDate, toDate);
// Filter by customers.
query.whereNot('contactId', null);
// Filter by accounts.
query.whereIn('accountId', receivableAccountsIds);
});
return transactions;
}
}

View File

@@ -4,23 +4,27 @@ import * as R from 'ramda';
import { map } from 'lodash'; import { map } from 'lodash';
import TenancyService from 'services/Tenancy/TenancyService'; import TenancyService from 'services/Tenancy/TenancyService';
import { import {
IVendor,
ITransactionsByVendorsService, ITransactionsByVendorsService,
ITransactionsByVendorsFilter, ITransactionsByVendorsFilter,
ITransactionsByVendorsStatement, ITransactionsByVendorsStatement,
ILedgerEntry,
} from 'interfaces'; } from 'interfaces';
import TransactionsByVendor from './TransactionsByVendor'; import TransactionsByVendor from './TransactionsByVendor';
import { ACCOUNT_TYPE } from 'data/AccountTypes';
import Ledger from 'services/Accounting/Ledger'; import Ledger from 'services/Accounting/Ledger';
import TransactionsByVendorRepository from './TransactionsByVendorRepository';
export default class TransactionsByVendorsService export default class TransactionsByVendorsService
implements ITransactionsByVendorsService { implements ITransactionsByVendorsService
{
@Inject() @Inject()
tenancy: TenancyService; tenancy: TenancyService;
@Inject('logger') @Inject('logger')
logger: any; logger: any;
@Inject()
reportRepository: TransactionsByVendorRepository;
/** /**
* Defaults balance sheet filter query. * Defaults balance sheet filter query.
* @return {IVendorBalanceSummaryQuery} * @return {IVendorBalanceSummaryQuery}
@@ -44,55 +48,24 @@ export default class TransactionsByVendorsService
}; };
} }
/**
* Retrieve the report vendors.
* @param tenantId
* @returns
*/
private getReportVendors(tenantId: number): Promise<IVendor[]> {
const { Vendor } = this.tenancy.models(tenantId);
return Vendor.query().orderBy('displayName');
}
/**
* Retrieve the accounts receivable.
* @param {number} tenantId
* @returns
*/
private async getPayableAccounts(tenantId: number) {
const { Account } = this.tenancy.models(tenantId);
const accounts = await Account.query().where(
'accountType',
ACCOUNT_TYPE.ACCOUNTS_PAYABLE
);
return accounts;
}
/** /**
* Retrieve the customers opening balance transactions. * Retrieve the customers opening balance transactions.
* @param {number} tenantId * @param {number} tenantId
* @param {number} openingDate * @param {number} openingDate
* @param {number} customersIds * @param {number} customersIds
* @returns {} * @returns {Promise<ILedgerEntry[]>}
*/ */
private async getVendorsOpeningBalance( private async getVendorsOpeningBalanceEntries(
tenantId: number, tenantId: number,
openingDate: Date, openingDate: Date,
customersIds?: number[] customersIds?: number[]
): Promise<ILedgerEntry[]> { ): Promise<ILedgerEntry[]> {
const { AccountTransaction } = this.tenancy.models(tenantId); const openingTransactions =
await this.reportRepository.getVendorsOpeningBalance(
const payableAccounts = await this.getPayableAccounts(tenantId); tenantId,
const payableAccountsIds = map(payableAccounts, 'id'); openingDate,
customersIds
const openingTransactions = await AccountTransaction.query().modify( );
'contactsOpeningBalance',
openingDate,
payableAccountsIds,
customersIds
);
return R.compose( return R.compose(
R.map(R.assoc('date', openingDate)), R.map(R.assoc('date', openingDate)),
R.map(R.assoc('accountNormal', 'credit')) R.map(R.assoc('accountNormal', 'credit'))
@@ -105,42 +78,46 @@ export default class TransactionsByVendorsService
* @param {Date|string} openingDate * @param {Date|string} openingDate
* @param {number[]} customersIds * @param {number[]} customersIds
*/ */
async getVendorsPeriodTransactions( private async getVendorsPeriodEntries(
tenantId: number, tenantId: number,
fromDate: Date, fromDate: Date,
toDate: Date toDate: Date
): Promise<ILedgerEntry[]> { ): Promise<ILedgerEntry[]> {
const { AccountTransaction } = this.tenancy.models(tenantId); const transactions =
await this.reportRepository.getVendorsPeriodTransactions(
const receivableAccounts = await this.getPayableAccounts(tenantId); tenantId,
const receivableAccountsIds = map(receivableAccounts, 'id'); fromDate,
toDate
const transactions = await AccountTransaction.query().onBuild((query) => { );
// Filter by date.
query.modify('filterDateRange', fromDate, toDate);
// Filter by customers.
query.whereNot('contactId', null);
// Filter by accounts.
query.whereIn('accountId', receivableAccountsIds);
});
return R.compose( return R.compose(
R.map(R.assoc('accountNormal', 'credit')), R.map(R.assoc('accountNormal', 'credit')),
R.map((trans) => ({ R.map((trans) => ({
...trans, ...trans,
referenceTypeFormatted: trans.referenceTypeFormatted, referenceTypeFormatted: trans.referenceTypeFormatted,
})), }))
)(transactions); )(transactions);
} }
async getReportTransactions(tenantId: number, fromDate: Date, toDate: Date) { /**
* Retrieve the report ledger entries from repository.
* @param {number} tenantId
* @param {Date} fromDate
* @param {Date} toDate
* @returns {Promise<ILedgerEntry[]>}
*/
private async getReportEntries(
tenantId: number,
fromDate: Date,
toDate: Date
): Promise<ILedgerEntry[]> {
const openingBalanceDate = moment(fromDate).subtract(1, 'days').toDate(); const openingBalanceDate = moment(fromDate).subtract(1, 'days').toDate();
return [ return [
...(await this.getVendorsOpeningBalance(tenantId, openingBalanceDate)), ...(await this.getVendorsOpeningBalanceEntries(
...(await this.getVendorsPeriodTransactions(tenantId, fromDate, toDate)), tenantId,
openingBalanceDate
)),
...(await this.getVendorsPeriodEntries(tenantId, fromDate, toDate)),
]; ];
} }
@@ -155,7 +132,7 @@ export default class TransactionsByVendorsService
query: ITransactionsByVendorsFilter query: ITransactionsByVendorsFilter
): Promise<ITransactionsByVendorsStatement> { ): Promise<ITransactionsByVendorsStatement> {
const { accountRepository } = this.tenancy.repositories(tenantId); const { accountRepository } = this.tenancy.repositories(tenantId);
// Settings tenant service. // Settings tenant service.
const settings = this.tenancy.settings(tenantId); const settings = this.tenancy.settings(tenantId);
const baseCurrency = settings.get({ const baseCurrency = settings.get({
@@ -166,19 +143,19 @@ export default class TransactionsByVendorsService
const filter = { ...this.defaultQuery, ...query }; const filter = { ...this.defaultQuery, ...query };
// Retrieve the report vendors. // Retrieve the report vendors.
const vendors = await this.getReportVendors(tenantId); const vendors = await this.reportRepository.getVendors(tenantId);
// Retrieve the accounts graph. // Retrieve the accounts graph.
const accountsGraph = await accountRepository.getDependencyGraph(); const accountsGraph = await accountRepository.getDependencyGraph();
// Journal transactions. // Journal transactions.
const journalTransactions = await this.getReportTransactions( const reportEntries = await this.getReportEntries(
tenantId, tenantId,
filter.fromDate, filter.fromDate,
filter.toDate filter.toDate
); );
// Ledger collection. // Ledger collection.
const journal = Ledger.fromTransactions(journalTransactions); const journal = new Ledger(reportEntries);
// Transactions by customers data mapper. // Transactions by customers data mapper.
const reportInstance = new TransactionsByVendor( const reportInstance = new TransactionsByVendor(

View File

@@ -0,0 +1,69 @@
import { Inject, Service } from 'typedi';
import { isEmpty, map } from 'lodash';
import { IVendor, IAccount } from 'interfaces';
import HasTenancyService from 'services/Tenancy/TenancyService';
import { ACCOUNT_TYPE } from 'data/AccountTypes';
@Service()
export default class VendorBalanceSummaryRepository {
@Inject()
tenancy: HasTenancyService;
/**
* Retrieve the report vendors.
* @param {number} tenantId
* @param {number[]} vendorsIds - Vendors ids.
* @returns {IVendor[]}
*/
public getVendors(
tenantId: number,
vendorsIds?: number[]
): Promise<IVendor[]> {
const { Vendor } = this.tenancy.models(tenantId);
const vendorQuery = Vendor.query().orderBy('displayName');
if (!isEmpty(vendorsIds)) {
vendorQuery.whereIn('id', vendorsIds);
}
return vendorQuery;
}
/**
* Retrieve the payable accounts.
* @param {number} tenantId
* @returns {Promise<IAccount[]>}
*/
public getPayableAccounts(tenantId: number): Promise<IAccount[]> {
const { Account } = this.tenancy.models(tenantId);
return Account.query().where('accountType', ACCOUNT_TYPE.ACCOUNTS_PAYABLE);
}
/**
* Retrieve the vendors transactions.
* @param {number} tenantId
* @param {Date} asDate
* @returns
*/
public async getVendorsTransactions(tenantId: number, asDate: Date | string) {
const { AccountTransaction } = this.tenancy.models(tenantId);
// Retrieve payable accounts .
const payableAccounts = await this.getPayableAccounts(tenantId);
const payableAccountsIds = map(payableAccounts, 'id');
// Retrieve the customers transactions of A/R accounts.
const customersTranasctions = await AccountTransaction.query().onBuild(
(query) => {
query.whereIn('accountId', payableAccountsIds);
query.modify('filterDateRange', null, asDate);
query.groupBy('contactId');
query.sum('credit as credit');
query.sum('debit as debit');
query.select('contactId');
}
);
return customersTranasctions;
}
}

View File

@@ -8,20 +8,24 @@ import {
IVendorBalanceSummaryService, IVendorBalanceSummaryService,
IVendorBalanceSummaryQuery, IVendorBalanceSummaryQuery,
IVendorBalanceSummaryStatement, IVendorBalanceSummaryStatement,
ILedgerEntry,
} from 'interfaces'; } from 'interfaces';
import { VendorBalanceSummaryReport } from './VendorBalanceSummary'; import { VendorBalanceSummaryReport } from './VendorBalanceSummary';
import { isEmpty } from 'lodash';
import { ACCOUNT_TYPE } from 'data/AccountTypes';
import Ledger from 'services/Accounting/Ledger'; import Ledger from 'services/Accounting/Ledger';
import VendorBalanceSummaryRepository from './VendorBalanceSummaryRepository';
export default class VendorBalanceSummaryService export default class VendorBalanceSummaryService
implements IVendorBalanceSummaryService { implements IVendorBalanceSummaryService
{
@Inject() @Inject()
tenancy: TenancyService; tenancy: TenancyService;
@Inject('logger') @Inject('logger')
logger: any; logger: any;
@Inject()
reportRepo: VendorBalanceSummaryRepository;
/** /**
* Defaults balance sheet filter query. * Defaults balance sheet filter query.
* @return {IVendorBalanceSummaryQuery} * @return {IVendorBalanceSummaryQuery}
@@ -45,59 +49,22 @@ export default class VendorBalanceSummaryService
} }
/** /**
* Retrieve the report vendors. * Retrieve the vendors ledger entrjes.
* @param {number} tenantId * @param {number} tenantId -
* @param {number[]} vendorsIds - Vendors ids. * @param {Date|string} date -
* @returns {IVendor[]} * @returns {Promise<ILedgerEntry>}
*/ */
getReportVendors( private async getReportVendorsEntries(
tenantId: number, tenantId: number,
vendorsIds?: number[] date: Date | string
): Promise<IVendor[]> { ): Promise<ILedgerEntry[]> {
const { Vendor } = this.tenancy.models(tenantId); const transactions = await this.reportRepo.getVendorsTransactions(
tenantId,
return Vendor.query() date
.orderBy('displayName')
.onBuild((query) => {
if (!isEmpty(vendorsIds)) {
query.whereIn('id', vendorsIds);
}
});
}
getPayableAccounts(tenantId: number) {
const { Account } = this.tenancy.models(tenantId);
return Account.query().where('accountType', ACCOUNT_TYPE.ACCOUNTS_PAYABLE);
}
/**
* Retrieve
* @param tenantId
* @param asDate
* @returns
*/
async getReportVendorsTransactions(tenantId: number, asDate: Date | string) {
const { AccountTransaction } = this.tenancy.models(tenantId);
// Retrieve payable accounts .
const payableAccounts = await this.getPayableAccounts(tenantId);
const payableAccountsIds = map(payableAccounts, 'id');
// Retrieve the customers transactions of A/R accounts.
const customersTranasctions = await AccountTransaction.query().onBuild(
(query) => {
query.whereIn('accountId', payableAccountsIds);
query.modify('filterDateRange', null, asDate);
query.groupBy('contactId');
query.sum('credit as credit');
query.sum('debit as debit');
query.select('contactId');
}
); );
const commonProps = { accountNormal: 'credit', date: asDate }; const commonProps = { accountNormal: 'credit' };
return R.map(R.merge(commonProps))(customersTranasctions); return R.map(R.merge(commonProps))(transactions);
} }
/** /**
@@ -126,19 +93,21 @@ export default class VendorBalanceSummaryService
} }
); );
// Retrieve the vendors transactions. // Retrieve the vendors transactions.
const vendorsTransactions = await this.getReportVendorsTransactions( const vendorsEntries = await this.getReportVendorsEntries(
tenantId, tenantId,
query.asDate query.asDate
); );
// Retrieve the customers list ordered by the display name. // Retrieve the customers list ordered by the display name.
const vendors = await this.getReportVendors(tenantId, query.vendorsIds); const vendors = await this.reportRepo.getVendors(
tenantId,
query.vendorsIds
);
// Ledger query. // Ledger query.
const ledger = Ledger.fromTransactions(vendorsTransactions); const vendorsLedger = new Ledger(vendorsEntries);
// Report instance. // Report instance.
const reportInstance = new VendorBalanceSummaryReport( const reportInstance = new VendorBalanceSummaryReport(
ledger, vendorsLedger,
vendors, vendors,
filter, filter,
baseCurrency baseCurrency

View File

@@ -37,6 +37,7 @@ export default class InventoryService {
transformItemEntriesToInventory(transaction: { transformItemEntriesToInventory(transaction: {
transactionId: number; transactionId: number;
transactionType: IItemEntryTransactionType; transactionType: IItemEntryTransactionType;
transactionNumber?: string;
date: Date | string; date: Date | string;
direction: TInventoryTransactionDirection; direction: TInventoryTransactionDirection;
@@ -56,6 +57,10 @@ export default class InventoryService {
entryId: entry.id, entryId: entry.id,
createdAt: transaction.createdAt, createdAt: transaction.createdAt,
costAccountId: entry.costAccountId, costAccountId: entry.costAccountId,
meta: {
transactionNumber: transaction.transactionNumber,
description: entry.description,
}
})); }));
} }
@@ -205,7 +210,7 @@ export default class InventoryService {
inventoryEntry.transactionType inventoryEntry.transactionType
); );
} }
return InventoryTransaction.query().insert({ return InventoryTransaction.query().insertGraph({
...inventoryEntry, ...inventoryEntry,
}); });
} }

View File

@@ -165,9 +165,9 @@ export default class InventoryAverageCostMethod
'transactionId', 'transactionId',
'transactionType', 'transactionType',
'createdAt', 'createdAt',
'costAccountId', 'costAccountId',
]), ]),
inventoryTransactionId: invTransaction.id,
}; };
switch (invTransaction.direction) { switch (invTransaction.direction) {
case 'IN': case 'IN':

View File

@@ -536,6 +536,7 @@ export default class SaleInvoicesService implements ISalesInvoicesService {
const transaction = { const transaction = {
transactionId: saleInvoice.id, transactionId: saleInvoice.id,
transactionType: 'SaleInvoice', transactionType: 'SaleInvoice',
transactionNumber: saleInvoice.invoiceNo,
date: saleInvoice.invoiceDate, date: saleInvoice.invoiceDate,
direction: 'OUT', direction: 'OUT',

View File

@@ -0,0 +1,52 @@
import _ from 'lodash';
import deepdash from 'deepdash';
const {
condense,
condenseDeep,
eachDeep,
exists,
filterDeep,
findDeep,
findPathDeep,
findValueDeep,
forEachDeep,
index,
keysDeep,
mapDeep,
mapKeysDeep,
mapValuesDeep,
omitDeep,
pathMatches,
pathToString,
paths,
pickDeep,
reduceDeep,
someDeep,
iteratee,
} = deepdash(_);
export {
iteratee,
condense,
condenseDeep,
eachDeep,
exists,
filterDeep,
findDeep,
findPathDeep,
findValueDeep,
forEachDeep,
index,
keysDeep,
mapDeep,
mapKeysDeep,
mapValuesDeep,
omitDeep,
pathMatches,
pathToString,
paths,
pickDeep,
reduceDeep,
someDeep,
};

View File

@@ -51,6 +51,30 @@ const dateRangeCollection = (
return collection; return collection;
}; };
const dateRangeFromToCollection = (
fromDate,
toDate,
addType = 'day',
increment = 1
) => {
const collection = [];
const momentFromDate = moment(fromDate);
const dateFormat = 'YYYY-MM-DD';
for (
let i = momentFromDate;
i.isBefore(toDate, addType) || i.isSame(toDate, addType);
i.add(increment, `${addType}s`)
) {
collection.push({
fromDate: i.startOf(addType).format(dateFormat),
toDate: i.endOf(addType).format(dateFormat),
});
}
return collection;
};
const dateRangeFormat = (rangeType) => { const dateRangeFormat = (rangeType) => {
switch (rangeType) { switch (rangeType) {
case 'year': case 'year':
@@ -329,8 +353,28 @@ var increment = (n) => {
}; };
}; };
const transformToMapBy = (collection, key) => {
return new Map(
Object.entries(_.groupBy(collection, key)),
);
}
const transformToMapKeyValue = (collection, key) => {
return new Map(
collection.map((item) => [item[key], item]),
);
};
const accumSum = (data, callback) => {
return data.reduce((acc, _data) => {
const amount = callback(_data);
return acc + amount;
}, 0)
}
export { export {
accumSum,
increment, increment,
hashPassword, hashPassword,
origin, origin,
@@ -354,4 +398,7 @@ export {
defaultToTransform, defaultToTransform,
transformToMap, transformToMap,
transactionIncrement, transactionIncrement,
transformToMapBy,
dateRangeFromToCollection,
transformToMapKeyValue
}; };