mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-15 04:10:32 +00:00
feat: Inventory item details report.
feat: Cash flow statement report.
This commit is contained in:
@@ -62,6 +62,7 @@
|
||||
"postcss-normalize": "8.0.1",
|
||||
"postcss-preset-env": "6.7.0",
|
||||
"postcss-safe-parser": "4.0.1",
|
||||
"ramda": "^0.27.1",
|
||||
"react": "^16.12.0",
|
||||
"react-app-polyfill": "^1.0.6",
|
||||
"react-body-classname": "^1.3.1",
|
||||
|
||||
@@ -73,7 +73,7 @@ export default function NumberFormatFields({}) {
|
||||
label={<T id={'money_format'} />}
|
||||
helperText={<ErrorMessage name="formatMoney" />}
|
||||
intent={inputIntent({ error, touched })}
|
||||
className={classNames(CLASSES.FILL)}
|
||||
className={classNames('form-group--money-format', CLASSES.FILL)}
|
||||
>
|
||||
<ListSelect
|
||||
items={moneyFormat}
|
||||
|
||||
@@ -4,44 +4,42 @@ export const financialReportMenus = [
|
||||
reports: [
|
||||
{
|
||||
title: 'Balance Sheet Report',
|
||||
desc:
|
||||
"Reports a company's assets, liabilities and shareholders' equity at a specific point in time with comparison period(s).",
|
||||
desc: "Reports a company's assets, liabilities and shareholders' equity at a specific point in time with comparison period(s).",
|
||||
link: '/financial-reports/balance-sheet',
|
||||
},
|
||||
{
|
||||
title: 'Trial Balance Sheet',
|
||||
desc:
|
||||
'Summarizes the credit and debit balance of each account in your chart of accounts at a specific point in time.',
|
||||
desc: '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',
|
||||
},
|
||||
{
|
||||
title: 'Journal Report',
|
||||
desc:
|
||||
'The debit and credit entries of system transactions, sorted by date.',
|
||||
link: '/financial-reports/journal-sheet',
|
||||
},
|
||||
{
|
||||
title: 'Profit/Loss Report',
|
||||
desc:
|
||||
'Reports the revenues, costs and expenses incurred during a specific point in time with comparison period(s).',
|
||||
desc: 'Reports the revenues, costs and expenses incurred during a specific point in time with comparison period(s).',
|
||||
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',
|
||||
desc:
|
||||
'Reports every transaction going in and out of your accounts and organized by accounts and date to monitoring activity of accounts.',
|
||||
desc: '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',
|
||||
},
|
||||
{
|
||||
title: 'Receivable Aging Summary',
|
||||
desc:
|
||||
'Summarize total unpaid balances of customers invoices with number of days the unpaid invoice is overdue.',
|
||||
desc: 'Summarize total unpaid balances of customers invoices with number of days the unpaid invoice is overdue.',
|
||||
link: '/financial-reports/receivable-aging-summary',
|
||||
},
|
||||
{
|
||||
title: 'Payable Aging Summary',
|
||||
desc:
|
||||
'Summarize total unpaid balances of vendors purchase invoices with the number of days the unpaid invoice is overdue.',
|
||||
desc: 'Summarize total unpaid balances of vendors purchase invoices with the number of days the unpaid invoice is overdue.',
|
||||
link: '/financial-reports/payable-aging-summary',
|
||||
},
|
||||
],
|
||||
@@ -54,20 +52,17 @@ export const SalesAndPurchasesReportMenus = [
|
||||
reports: [
|
||||
{
|
||||
title: 'Purchases By Items',
|
||||
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.',
|
||||
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.',
|
||||
link: '/financial-reports/purchases-by-items',
|
||||
},
|
||||
{
|
||||
title: 'Sales By Items',
|
||||
desc:
|
||||
'Summarize the business’s sold items quantity, income and average income rate of each item during a specific point in time.',
|
||||
desc: 'Summarize the business’s sold items quantity, income and average income rate of each item during a specific point in time.',
|
||||
link: '/financial-reports/sales-by-items',
|
||||
},
|
||||
{
|
||||
title: 'Inventory valuation',
|
||||
desc:
|
||||
'Summarize the business’s purchase items quantity, cost and average cost rate of each item during a specific point in time.',
|
||||
desc: 'Summarize the business’s purchase items quantity, cost and average cost rate of each item during a specific point in time.',
|
||||
link: '/financial-reports/inventory-valuation',
|
||||
},
|
||||
{
|
||||
@@ -90,6 +85,11 @@ export const SalesAndPurchasesReportMenus = [
|
||||
desc: 'Reports every transaction going in and out of each vendor/supplier.',
|
||||
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',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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 };
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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);
|
||||
@@ -9,6 +9,7 @@ import { CellTextSpan } from 'components/Datatable/Cells';
|
||||
/**
|
||||
* Retrieve customers transactions columns.
|
||||
*/
|
||||
|
||||
export const useCustomersTransactionsColumns = () => {
|
||||
const {
|
||||
customersTransactions: { tableRows },
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 };
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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);
|
||||
@@ -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,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -20,7 +20,9 @@ const FINANCIAL_REPORTS = {
|
||||
CUSTOMERS_BALANCE_SUMMARY: 'CUSTOMERS_BALANCE_SUMMARY',
|
||||
SALES_BY_ITEMS: 'SALES_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 = {
|
||||
|
||||
@@ -1064,4 +1064,7 @@ export default {
|
||||
vendors_transactions: 'Vendors Transactions',
|
||||
reference_type: 'Reference type',
|
||||
transaction_number: 'Transaction number',
|
||||
cash_flow_statement: 'Cash Flow Statement',
|
||||
statement_of_cash_flow: 'Statement of Cash Flow ',
|
||||
inventory_item_details:'Inventory Item Details'
|
||||
};
|
||||
|
||||
@@ -112,8 +112,7 @@ export default [
|
||||
import('containers/FinancialStatements/GeneralLedger/GeneralLedger'),
|
||||
),
|
||||
breadcrumb: 'General Ledger',
|
||||
hint:
|
||||
'Reports every transaction going in and out of your accounts and organized by accounts and date to monitoring activity of accounts.',
|
||||
hint: 'Reports every transaction going in and out of your accounts and organized by accounts and date to monitoring activity of accounts.',
|
||||
hotkey: 'shift+4',
|
||||
pageTitle: formatMessage({ id: 'general_ledger' }),
|
||||
backLink: true,
|
||||
@@ -125,8 +124,7 @@ export default [
|
||||
import('containers/FinancialStatements/BalanceSheet/BalanceSheet'),
|
||||
),
|
||||
breadcrumb: 'Balance Sheet',
|
||||
hint:
|
||||
"Reports a company's assets, liabilities and shareholders' equity at a specific point in time with comparison period(s).",
|
||||
hint: "Reports a company's assets, liabilities and shareholders' equity at a specific point in time with comparison period(s).",
|
||||
hotkey: 'shift+1',
|
||||
pageTitle: formatMessage({ id: 'balance_sheet' }),
|
||||
backLink: true,
|
||||
@@ -140,8 +138,7 @@ export default [
|
||||
),
|
||||
),
|
||||
breadcrumb: 'Trial Balance Sheet',
|
||||
hint:
|
||||
'Summarizes the credit and debit balance of each account in your chart of accounts at a specific point in time. ',
|
||||
hint: 'Summarizes the credit and debit balance of each account in your chart of accounts at a specific point in time. ',
|
||||
hotkey: 'shift+5',
|
||||
pageTitle: formatMessage({ id: 'trial_balance_sheet' }),
|
||||
backLink: true,
|
||||
@@ -153,8 +150,7 @@ export default [
|
||||
import('containers/FinancialStatements/ProfitLossSheet/ProfitLossSheet'),
|
||||
),
|
||||
breadcrumb: 'Profit Loss Sheet',
|
||||
hint:
|
||||
'Reports the revenues, costs and expenses incurred during a specific point in time with comparison period(s).',
|
||||
hint: 'Reports the revenues, costs and expenses incurred during a specific point in time with comparison period(s).',
|
||||
hotkey: 'shift+2',
|
||||
pageTitle: formatMessage({ id: 'profit_loss_sheet' }),
|
||||
backLink: true,
|
||||
@@ -166,8 +162,7 @@ export default [
|
||||
import('containers/FinancialStatements/ARAgingSummary/ARAgingSummary'),
|
||||
),
|
||||
breadcrumb: 'Receivable Aging Summary',
|
||||
hint:
|
||||
'Summarize total unpaid balances of customers invoices with number of days the unpaid invoice is overdue.',
|
||||
hint: 'Summarize total unpaid balances of customers invoices with number of days the unpaid invoice is overdue.',
|
||||
pageTitle: formatMessage({ id: 'receivable_aging_summary' }),
|
||||
backLink: true,
|
||||
sidebarExpand: false,
|
||||
@@ -178,8 +173,7 @@ export default [
|
||||
import('containers/FinancialStatements/APAgingSummary/APAgingSummary'),
|
||||
),
|
||||
breadcrumb: 'Payable Aging Summary',
|
||||
hint:
|
||||
'Summarize total unpaid balances of vendors purchase invoices with the number of days the unpaid invoice is overdue.',
|
||||
hint: 'Summarize total unpaid balances of vendors purchase invoices with the number of days the unpaid invoice is overdue.',
|
||||
pageTitle: formatMessage({ id: 'payable_aging_summary' }),
|
||||
backLink: true,
|
||||
sidebarExpand: false,
|
||||
@@ -190,8 +184,7 @@ export default [
|
||||
import('containers/FinancialStatements/Journal/Journal'),
|
||||
),
|
||||
breadcrumb: 'Journal Sheet',
|
||||
hint:
|
||||
'The debit and credit entries of system transactions, sorted by date.',
|
||||
hint: 'The debit and credit entries of system transactions, sorted by date.',
|
||||
hotkey: 'shift+3',
|
||||
pageTitle: formatMessage({ id: 'journal_sheet' }),
|
||||
sidebarExpand: false,
|
||||
@@ -217,8 +210,7 @@ export default [
|
||||
),
|
||||
breadcrumb: 'Sales by Items',
|
||||
pageTitle: formatMessage({ id: 'sales_by_items' }),
|
||||
hint:
|
||||
'Summarize the business’s sold items quantity, income and average income rate of each item during a specific point in time.',
|
||||
hint: 'Summarize the business’s sold items quantity, income and average income rate of each item during a specific point in time.',
|
||||
backLink: true,
|
||||
sidebarExpand: false,
|
||||
},
|
||||
@@ -230,8 +222,7 @@ export default [
|
||||
),
|
||||
),
|
||||
breadcrumb: 'Inventory Valuation ',
|
||||
hint:
|
||||
'Summerize your transactions for each inventory item and how they affect quantity, valuation and weighted average.',
|
||||
hint: 'Summerize your transactions for each inventory item and how they affect quantity, valuation and weighted average.',
|
||||
pageTitle: formatMessage({ id: 'inventory_valuation' }),
|
||||
backLink: true,
|
||||
sidebarExpand: false,
|
||||
@@ -257,7 +248,7 @@ export default [
|
||||
),
|
||||
),
|
||||
breadcrumb: 'Vendors Balance Summary ',
|
||||
hint: '..',
|
||||
hint: 'Summerize the total amount your business owes each vendor.',
|
||||
pageTitle: formatMessage({ id: 'vendors_balance_summary' }),
|
||||
backLink: true,
|
||||
sidebarExpand: false,
|
||||
@@ -270,7 +261,7 @@ export default [
|
||||
),
|
||||
),
|
||||
breadcrumb: 'Customers Transactions ',
|
||||
hint: '..',
|
||||
hint: 'Reports every transaction going in and out of each customer.',
|
||||
pageTitle: formatMessage({ id: 'customers_transactions' }),
|
||||
backLink: true,
|
||||
sidebarExpand: false,
|
||||
@@ -283,11 +274,37 @@ export default [
|
||||
),
|
||||
),
|
||||
breadcrumb: 'Vendors Transactions ',
|
||||
hint: '..',
|
||||
hint: 'Reports every transaction going in and out of each vendor/supplier.',
|
||||
pageTitle: formatMessage({ id: 'vendors_transactions' }),
|
||||
backLink: true,
|
||||
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',
|
||||
component: lazy(() =>
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -45,6 +45,12 @@ const initialState = {
|
||||
vendorsTransactions: {
|
||||
displayFilterDrawer: false,
|
||||
},
|
||||
cashFlowStatement: {
|
||||
displayFilterDrawer: false,
|
||||
},
|
||||
inventoryItemDetails: {
|
||||
displayFilterDrawer: false,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -91,4 +97,9 @@ export default createReducer(initialState, {
|
||||
t.VENDORS_TRANSACTIONS,
|
||||
'vendorsTransactions',
|
||||
),
|
||||
...financialStatementFilterToggle(t.CASH_FLOW_STATEMENT, 'cashFlowStatement'),
|
||||
...financialStatementFilterToggle(
|
||||
t.INVENTORY_ITEM_DETAILS,
|
||||
'inventoryItemDetails',
|
||||
),
|
||||
});
|
||||
|
||||
@@ -65,6 +65,14 @@ export const vendorsTransactionsFilterDrawerSelector = (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.
|
||||
*/
|
||||
@@ -211,3 +219,23 @@ export const getVendorsTransactionsFilterDrawer = createSelector(
|
||||
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;
|
||||
},
|
||||
);
|
||||
|
||||
@@ -14,4 +14,6 @@ export default {
|
||||
VENDORS_BALANCE_SUMMARY: 'VENDORS BALANCE SUMMARY',
|
||||
CUSTOMERS_TRANSACTIONS: 'CUSTOMERS TRANSACTIONS',
|
||||
VENDORS_TRANSACTIONS: 'VENDORS TRANSACTIONS',
|
||||
CASH_FLOW_STATEMENT: 'CASH FLOW STATEMENT',
|
||||
INVENTORY_ITEM_DETAILS: 'INVENTORY ITEM DETAILS',
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@
|
||||
"dependencies": {
|
||||
"@hapi/boom": "^7.4.3",
|
||||
"@types/i18n": "^0.8.7",
|
||||
"@types/mathjs": "^6.0.12",
|
||||
"accepts": "^1.3.7",
|
||||
"accounting": "^0.4.1",
|
||||
"agenda": "^3.1.0",
|
||||
@@ -34,6 +35,7 @@
|
||||
"crypto-random-string": "^3.2.0",
|
||||
"csurf": "^1.10.0",
|
||||
"deep-map": "^2.0.0",
|
||||
"deepdash": "^5.3.7",
|
||||
"dotenv": "^8.1.0",
|
||||
"errorhandler": "^1.5.1",
|
||||
"es6-weak-map": "^2.0.3",
|
||||
@@ -55,6 +57,7 @@
|
||||
"knex-db-manager": "^0.6.1",
|
||||
"libphonenumber-js": "^1.9.6",
|
||||
"lodash": "^4.17.15",
|
||||
"mathjs": "^9.4.0",
|
||||
"memory-cache": "^0.2.0",
|
||||
"moment": "^2.24.0",
|
||||
"moment-range": "^4.0.2",
|
||||
|
||||
@@ -10,7 +10,7 @@ export default class BaseController {
|
||||
* Converts plain object keys to cameCase style.
|
||||
* @param {Object} data
|
||||
*/
|
||||
private dataToCamelCase(data) {
|
||||
protected dataToCamelCase(data) {
|
||||
return mapKeysDeep(data, (v, k) => camelCase(k));
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ export default class BaseController {
|
||||
* @param {Request} req
|
||||
* @param options
|
||||
*/
|
||||
matchedBodyData(req: Request, options: any = {}) {
|
||||
protected matchedBodyData(req: Request, options: any = {}) {
|
||||
const data = matchedData(req, {
|
||||
locations: ['body'],
|
||||
includeOptionals: true,
|
||||
@@ -32,7 +32,7 @@ export default class BaseController {
|
||||
* Matches the query data from validation schema.
|
||||
* @param {Request} req
|
||||
*/
|
||||
matchedQueryData(req: Request) {
|
||||
protected matchedQueryData(req: Request) {
|
||||
const data = matchedData(req, {
|
||||
locations: ['query'],
|
||||
});
|
||||
@@ -45,7 +45,7 @@ export default class BaseController {
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
validationResult(req: Request, res: Response, next: NextFunction) {
|
||||
protected validationResult(req: Request, res: Response, next: NextFunction) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
@@ -61,7 +61,7 @@ export default class BaseController {
|
||||
* Transform the given data to response.
|
||||
* @param {any} data
|
||||
*/
|
||||
transfromToResponse(
|
||||
protected transfromToResponse(
|
||||
data: any,
|
||||
translatable?: string | string[],
|
||||
req?: Request
|
||||
@@ -85,16 +85,16 @@ export default class BaseController {
|
||||
* Async middleware.
|
||||
* @param {function} callback
|
||||
*/
|
||||
asyncMiddleware(callback) {
|
||||
protected asyncMiddleware(callback) {
|
||||
return asyncMiddleware(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Request} req
|
||||
* @returns
|
||||
*
|
||||
* @param {Request} req
|
||||
* @returns
|
||||
*/
|
||||
accepts(req) {
|
||||
protected accepts(req) {
|
||||
return accepts(req);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ import CustomerBalanceSummaryController from './FinancialStatements/CustomerBala
|
||||
import VendorBalanceSummaryController from './FinancialStatements/VendorBalanceSummary';
|
||||
import TransactionsByCustomers from './FinancialStatements/TransactionsByCustomers';
|
||||
import TransactionsByVendors from './FinancialStatements/TransactionsByVendors';
|
||||
import CashFlowStatementController from './FinancialStatements/CashFlow/CashFlow';
|
||||
import InventoryDetailsController from './FinancialStatements/InventoryDetails';
|
||||
|
||||
@Service()
|
||||
export default class FinancialStatementsService {
|
||||
@@ -77,6 +79,14 @@ export default class FinancialStatementsService {
|
||||
'/transactions-by-vendors',
|
||||
Container.get(TransactionsByVendors).router(),
|
||||
);
|
||||
router.use(
|
||||
'/cash-flow',
|
||||
Container.get(CashFlowStatementController).router(),
|
||||
);
|
||||
router.use(
|
||||
'/inventory-item-details',
|
||||
Container.get(InventoryDetailsController).router(),
|
||||
);
|
||||
return router;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,9 @@ export const ACCOUNT_TYPE = {
|
||||
BANK: 'bank',
|
||||
ACCOUNTS_RECEIVABLE: 'accounts-receivable',
|
||||
INVENTORY: 'inventory',
|
||||
OTHER_CURRENT_ASSET: 'other-ACCOUNT_PARENT_TYPE.CURRENT_ASSET',
|
||||
OTHER_CURRENT_ASSET: 'other-current-asset',
|
||||
FIXED_ASSET: 'fixed-asset',
|
||||
NON_CURRENT_ASSET: 'non-ACCOUNT_PARENT_TYPE.CURRENT_ASSET',
|
||||
NON_CURRENT_ASSET: 'none-current-asset',
|
||||
|
||||
ACCOUNTS_PAYABLE: 'accounts-payable',
|
||||
CREDIT_CARD: 'credit-card',
|
||||
@@ -25,7 +25,7 @@ export const ACCOUNT_TYPE = {
|
||||
export const ACCOUNT_PARENT_TYPE = {
|
||||
CURRENT_ASSET: 'current-asset',
|
||||
FIXED_ASSET: 'fixed-asset',
|
||||
NON_CURRENT_ASSET: 'non-ACCOUNT_PARENT_TYPE.CURRENT_ASSET',
|
||||
NON_CURRENT_ASSET: 'non-current-asset',
|
||||
|
||||
CURRENT_LIABILITY: 'current-liability',
|
||||
LOGN_TERM_LIABILITY: 'long-term-liability',
|
||||
|
||||
@@ -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) {};
|
||||
|
||||
@@ -15,6 +15,7 @@ exports.up = function (knex) {
|
||||
|
||||
table.integer('entry_id').unsigned().index();
|
||||
table.integer('cost_account_id').unsigned();
|
||||
table.integer('inventory_transaction_id').unsigned().index();
|
||||
|
||||
table.datetime('created_at').index();
|
||||
});
|
||||
|
||||
@@ -33,7 +33,13 @@ export interface IAccountsTransactionsFilter {
|
||||
}
|
||||
|
||||
export interface IAccountTransaction {
|
||||
|
||||
credit: number;
|
||||
debit: number;
|
||||
accountId: number;
|
||||
contactId: number;
|
||||
date: string|Date;
|
||||
referenceNumber: string;
|
||||
account: IAccount;
|
||||
}
|
||||
export interface IAccountResponse extends IAccount {
|
||||
|
||||
|
||||
190
server/src/interfaces/CashFlow.ts
Normal file
190
server/src/interfaces/CashFlow.ts
Normal 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,
|
||||
}
|
||||
76
server/src/interfaces/InventoryDetails.ts
Normal file
76
server/src/interfaces/InventoryDetails.ts
Normal 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[];
|
||||
@@ -1,38 +1,51 @@
|
||||
|
||||
export type TInventoryTransactionDirection = 'IN' | 'OUT';
|
||||
|
||||
export interface IInventoryTransaction {
|
||||
id?: number,
|
||||
date: Date|string,
|
||||
direction: TInventoryTransactionDirection,
|
||||
itemId: number,
|
||||
id?: number;
|
||||
date: Date | string;
|
||||
direction: TInventoryTransactionDirection;
|
||||
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,
|
||||
rate: number,
|
||||
transactionType: string,
|
||||
transactionId: number,
|
||||
entryId: number,
|
||||
costAccountId: number,
|
||||
createdAt?: Date,
|
||||
updatedAt?: Date,
|
||||
};
|
||||
|
||||
export interface IInventoryLotCost {
|
||||
id?: number,
|
||||
date: Date,
|
||||
direction: string,
|
||||
itemId: number,
|
||||
quantity: number,
|
||||
rate: number,
|
||||
remaining: number,
|
||||
cost: number,
|
||||
transactionType: string,
|
||||
transactionId: number,
|
||||
costAccountId: number,
|
||||
entryId: number,
|
||||
createdAt: Date,
|
||||
};
|
||||
id?: number;
|
||||
date: Date;
|
||||
direction: string;
|
||||
itemId: number;
|
||||
quantity: number;
|
||||
rate: number;
|
||||
remaining: number;
|
||||
cost: number;
|
||||
transactionType: string;
|
||||
transactionId: number;
|
||||
costAccountId: number;
|
||||
entryId: number;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export interface IItemsQuantityChanges {
|
||||
itemId: number,
|
||||
balanceChange: number,
|
||||
};
|
||||
itemId: number;
|
||||
balanceChange: number;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ export interface ILedger {
|
||||
entries: ILedgerEntry[];
|
||||
|
||||
getEntries(): ILedgerEntry[];
|
||||
whereAccountId(accountId: number): ILedger;
|
||||
whereContactId(contactId: number): ILedger;
|
||||
whereFromDate(fromDate: Date | string): ILedger;
|
||||
whereToDate(toDate: Date | string): ILedger;
|
||||
@@ -15,6 +16,6 @@ export interface ILedgerEntry {
|
||||
accountNormal: string;
|
||||
contactId?: number;
|
||||
date: Date | string;
|
||||
transactionType: string,
|
||||
transactionNumber: string,
|
||||
transactionType?: string,
|
||||
transactionNumber?: string,
|
||||
}
|
||||
|
||||
@@ -12,4 +12,14 @@ export interface ITableCell {
|
||||
|
||||
export type ITableRow = {
|
||||
rows: ITableCell[];
|
||||
};
|
||||
};
|
||||
|
||||
export interface ITableColumn {
|
||||
key: string,
|
||||
label: string,
|
||||
}
|
||||
|
||||
export interface ITable {
|
||||
columns: ITableColumn[],
|
||||
data: ITableRow[],
|
||||
}
|
||||
@@ -25,8 +25,8 @@ export interface ITransactionsByContactsContact {
|
||||
}
|
||||
|
||||
export interface ITransactionsByContactsFilter {
|
||||
fromDate: Date;
|
||||
toDate: Date;
|
||||
fromDate: Date|string;
|
||||
toDate: Date|string;
|
||||
numberFormat: INumberFormatQuery;
|
||||
noneTransactions: boolean;
|
||||
noneZero: boolean;
|
||||
|
||||
@@ -50,4 +50,6 @@ export * from './TransactionsByCustomers';
|
||||
export * from './TransactionsByContacts';
|
||||
export * from './TransactionsByVendors';
|
||||
export * from './Table';
|
||||
export * from './Ledger';
|
||||
export * from './Ledger';
|
||||
export * from './CashFlow';
|
||||
export * from './InventoryDetails';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Model } from 'objection';
|
||||
import { Model, raw } from 'objection';
|
||||
import moment from 'moment';
|
||||
import TenantModel from 'models/TenantModel';
|
||||
|
||||
@@ -138,6 +138,21 @@ export default class AccountTransaction extends TenantModel {
|
||||
query.sum('credit as credit');
|
||||
query.sum('debit as debit');
|
||||
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}')`);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,6 +17,33 @@ export default class InventoryTransaction extends TenantModel {
|
||||
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.
|
||||
*/
|
||||
@@ -59,8 +86,47 @@ export default class InventoryTransaction extends TenantModel {
|
||||
static get relationMappings() {
|
||||
const Item = require('models/Item');
|
||||
const ItemEntry = require('models/ItemEntry');
|
||||
const InventoryTransactionMeta = require('models/InventoryTransactionMeta');
|
||||
const InventoryCostLots = require('models/InventoryCostLotTracker');
|
||||
|
||||
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: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: Item.default,
|
||||
|
||||
29
server/src/models/InventoryTransactionMeta.js
Normal file
29
server/src/models/InventoryTransactionMeta.js
Normal 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'
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,6 @@
|
||||
import moment from 'moment';
|
||||
import { defaultTo } from 'lodash';
|
||||
import {
|
||||
ILedger,
|
||||
ILedgerEntry
|
||||
} from 'interfaces';
|
||||
import { IAccountTransaction, ILedger, ILedgerEntry } from 'interfaces';
|
||||
import EntityRepository from 'repositories/EntityRepository';
|
||||
|
||||
export default class Ledger implements ILedger {
|
||||
@@ -11,7 +8,7 @@ export default class Ledger implements ILedger {
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {ILedgerEntry[]} entries
|
||||
* @param {ILedgerEntry[]} entries
|
||||
*/
|
||||
constructor(entries: ILedgerEntry[]) {
|
||||
this.entries = entries;
|
||||
@@ -20,26 +17,45 @@ export default class Ledger implements ILedger {
|
||||
/**
|
||||
* Filters the ledegr entries.
|
||||
* @param callback
|
||||
* @returns
|
||||
* @returns {ILedger}
|
||||
*/
|
||||
filter(callback) {
|
||||
public filter(callback): ILedger {
|
||||
const entries = this.entries.filter(callback);
|
||||
return new Ledger(entries);
|
||||
}
|
||||
|
||||
getEntries(): ILedgerEntry[] {
|
||||
/**
|
||||
* Retrieve the all entries of the ledger.
|
||||
* @return {ILedgerEntry[]}
|
||||
*/
|
||||
public getEntries(): ILedgerEntry[] {
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
return this.filter(
|
||||
@@ -59,15 +80,14 @@ export default class Ledger implements ILedger {
|
||||
|
||||
/**
|
||||
* Retrieve the closing balance of the entries.
|
||||
* @returns {number}
|
||||
* @returns {number}
|
||||
*/
|
||||
getClosingBalance() {
|
||||
public getClosingBalance(): number {
|
||||
let closingBalance = 0;
|
||||
|
||||
this.entries.forEach((entry) => {
|
||||
if (entry.accountNormal === 'credit') {
|
||||
closingBalance += entry.credit - entry.debit;
|
||||
|
||||
} else if (entry.accountNormal === 'debit') {
|
||||
closingBalance += entry.debit - entry.credit;
|
||||
}
|
||||
@@ -75,15 +95,25 @@ export default class Ledger implements ILedger {
|
||||
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);
|
||||
}
|
||||
|
||||
static mapTransaction(entry): ILedgerEntry {
|
||||
|
||||
/**
|
||||
* Mappes the account transaction to ledger entry.
|
||||
* @param {IAccountTransaction} entry
|
||||
* @returns {ILedgerEntry}
|
||||
*/
|
||||
static mapTransaction(entry: IAccountTransaction): ILedgerEntry {
|
||||
return {
|
||||
credit: defaultTo(entry.credit, 0),
|
||||
debit: defaultTo(entry.debit, 0),
|
||||
accountNormal: entry.accountNormal,
|
||||
accountNormal: entry.account.accountNormal,
|
||||
accountId: entry.accountId,
|
||||
contactId: entry.contactId,
|
||||
date: entry.date,
|
||||
@@ -91,10 +121,15 @@ export default class Ledger implements ILedger {
|
||||
transactionType: entry.referenceTypeFormatted,
|
||||
referenceNumber: entry.referenceNumber,
|
||||
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);
|
||||
return new Ledger(entries);
|
||||
}
|
||||
|
||||
@@ -16,13 +16,13 @@ import FinancialSheet from '../FinancialSheet';
|
||||
|
||||
export default class BalanceSheetStatement extends FinancialSheet {
|
||||
readonly query: IBalanceSheetQuery;
|
||||
readonly numberFormat: INumberFormatQuery;
|
||||
readonly tenantId: number;
|
||||
readonly accounts: IAccount & { type: IAccountType }[];
|
||||
readonly journalFinancial: IJournalPoster;
|
||||
readonly comparatorDateType: string;
|
||||
readonly dateRangeSet: string[];
|
||||
readonly baseCurrency: string;
|
||||
readonly numberFormat: INumberFormatQuery;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
@@ -46,7 +46,6 @@ export default class BalanceSheetStatement extends FinancialSheet {
|
||||
this.accounts = accounts;
|
||||
this.journalFinancial = journalFinancial;
|
||||
this.baseCurrency = baseCurrency;
|
||||
|
||||
this.comparatorDateType =
|
||||
query.displayColumnsType === 'total' ? 'day' : query.displayColumnsBy;
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ export default class BalanceSheetStatementService
|
||||
* @param {number} tenantId -
|
||||
* @returns {IBalanceSheetMeta}
|
||||
*/
|
||||
reportMetadata(tenantId: number): IBalanceSheetMeta {
|
||||
private reportMetadata(tenantId: number): IBalanceSheetMeta {
|
||||
const settings = this.tenancy.settings(tenantId);
|
||||
|
||||
const isCostComputeRunning = this.inventoryService
|
||||
@@ -113,7 +113,7 @@ export default class BalanceSheetStatementService
|
||||
// Retrieve all journal transactions based on the given query.
|
||||
const transactions = await transactionsRepository.journal({
|
||||
fromDate: query.fromDate,
|
||||
toDate: query.toDate,
|
||||
toDate: query.toDate,
|
||||
});
|
||||
// Transform transactions to journal collection.
|
||||
const transactionsJournal = Journal.fromTransactions(
|
||||
|
||||
761
server/src/services/FinancialStatements/CashFlow/CashFlow.ts
Normal file
761
server/src/services/FinancialStatements/CashFlow/CashFlow.ts
Normal 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;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
)([]);
|
||||
}
|
||||
}
|
||||
75
server/src/services/FinancialStatements/CashFlow/schema.ts
Normal file
75
server/src/services/FinancialStatements/CashFlow/schema.ts
Normal 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[];
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -7,20 +7,26 @@ import {
|
||||
ICustomerBalanceSummaryService,
|
||||
ICustomerBalanceSummaryQuery,
|
||||
ICustomerBalanceSummaryStatement,
|
||||
ICustomer
|
||||
ICustomer,
|
||||
ILedgerEntry,
|
||||
} from 'interfaces';
|
||||
import { CustomerBalanceSummaryReport } from './CustomerBalanceSummary';
|
||||
import { ACCOUNT_TYPE } from 'data/AccountTypes';
|
||||
|
||||
import Ledger from 'services/Accounting/Ledger';
|
||||
import CustomerBalanceSummaryRepository from './CustomerBalanceSummaryRepository';
|
||||
|
||||
export default class CustomerBalanceSummaryService
|
||||
implements ICustomerBalanceSummaryService {
|
||||
implements ICustomerBalanceSummaryService
|
||||
{
|
||||
@Inject()
|
||||
tenancy: TenancyService;
|
||||
|
||||
@Inject('logger')
|
||||
logger: any;
|
||||
|
||||
@Inject()
|
||||
reportRepository: CustomerBalanceSummaryRepository;
|
||||
|
||||
/**
|
||||
* Defaults balance sheet filter query.
|
||||
* @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
|
||||
* @returns
|
||||
* @param {Date|string} asDate
|
||||
* @returns {Promise<ILedgerEntry[]>}
|
||||
*/
|
||||
private async getReportCustomersTransactions(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');
|
||||
}
|
||||
private async getReportCustomersEntries(
|
||||
tenantId: number,
|
||||
asDate: Date | string
|
||||
): Promise<ILedgerEntry[]> {
|
||||
const transactions = await this.reportRepository.getCustomersTransactions(
|
||||
tenantId,
|
||||
asDate
|
||||
);
|
||||
const commonProps = { accountNormal: 'debit', date: asDate };
|
||||
|
||||
return R.map(R.merge(commonProps))(customersTranasctions);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
});
|
||||
return R.map(R.merge(commonProps))(transactions);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -130,17 +96,17 @@ export default class CustomerBalanceSummaryService
|
||||
}
|
||||
);
|
||||
// Retrieve the customers list ordered by the display name.
|
||||
const customers = await this.getReportCustomers(
|
||||
const customers = await this.reportRepository.getCustomers(
|
||||
tenantId,
|
||||
query.customersIds
|
||||
);
|
||||
// Retrieve the customers debit/credit totals.
|
||||
const customersTransactions = await this.getReportCustomersTransactions(
|
||||
const customersEntries = await this.getReportCustomersEntries(
|
||||
tenantId,
|
||||
filter.asDate
|
||||
);
|
||||
// Ledger query.
|
||||
const ledger = Ledger.fromTransactions(customersTransactions);
|
||||
const ledger = new Ledger(customersEntries);
|
||||
|
||||
// Report instance.
|
||||
const report = new CustomerBalanceSummaryReport(
|
||||
@@ -153,7 +119,7 @@ export default class CustomerBalanceSummaryService
|
||||
return {
|
||||
data: report.reportData(),
|
||||
columns: report.reportColumns(),
|
||||
query: filter
|
||||
query: filter,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { IFormatNumberSettings, INumberFormatQuery } from 'interfaces';
|
||||
import moment from 'moment';
|
||||
import {
|
||||
ICashFlowStatementTotal,
|
||||
IFormatNumberSettings,
|
||||
INumberFormatQuery,
|
||||
} from 'interfaces';
|
||||
import { formatNumber } from 'utils';
|
||||
|
||||
export default class FinancialSheet {
|
||||
@@ -37,7 +42,7 @@ export default class FinancialSheet {
|
||||
};
|
||||
return formatNumber(number, settings);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Formatting full amount with different format settings.
|
||||
* @param {number} amount -
|
||||
@@ -52,24 +57,68 @@ export default class FinancialSheet {
|
||||
return this.formatNumber(amount, {
|
||||
money: numberFormat.formatMoney === 'none' ? false : true,
|
||||
excerptZero: false,
|
||||
...settings
|
||||
...settings,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Formates the amount to the percentage string.
|
||||
* @param {number} amount
|
||||
* @param {number} amount
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formatPercentage(
|
||||
amount
|
||||
): string {
|
||||
protected formatPercentage(amount): string {
|
||||
const percentage = amount * 100;
|
||||
|
||||
return formatNumber(percentage, {
|
||||
symbol: '%',
|
||||
excerptZero: true,
|
||||
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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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' },
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,29 @@
|
||||
import { Inject } from 'typedi';
|
||||
import * as R from 'ramda';
|
||||
import moment from 'moment';
|
||||
import { map } from 'lodash';
|
||||
import TenancyService from 'services/Tenancy/TenancyService';
|
||||
import {
|
||||
ITransactionsByCustomersService,
|
||||
ITransactionsByCustomersFilter,
|
||||
ITransactionsByCustomersStatement,
|
||||
ILedgerEntry,
|
||||
} from 'interfaces';
|
||||
import TransactionsByCustomers from './TransactionsByCustomers';
|
||||
import Ledger from 'services/Accounting/Ledger';
|
||||
import { ACCOUNT_TYPE } from 'data/AccountTypes';
|
||||
import TransactionsByCustomersRepository from './TransactionsByCustomersRepository';
|
||||
|
||||
export default class TransactionsByCustomersService
|
||||
implements ITransactionsByCustomersService {
|
||||
implements ITransactionsByCustomersService
|
||||
{
|
||||
@Inject()
|
||||
tenancy: TenancyService;
|
||||
|
||||
@Inject('logger')
|
||||
logger: any;
|
||||
|
||||
@Inject()
|
||||
reportRepository: TransactionsByCustomersRepository;
|
||||
|
||||
/**
|
||||
* Defaults balance sheet filter query.
|
||||
* @return {ICustomerBalanceSummaryQuery}
|
||||
@@ -44,43 +48,24 @@ export default class TransactionsByCustomersService
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the accounts receivable.
|
||||
* Retrieve the customers opening balance ledger entries.
|
||||
* @param {number} tenantId
|
||||
* @returns
|
||||
* @param {Date} openingDate
|
||||
* @param {number[]} customersIds
|
||||
* @returns {Promise<ILedgerEntry[]>}
|
||||
*/
|
||||
async getReceivableAccounts(tenantId: number) {
|
||||
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(
|
||||
private async getCustomersOpeningBalanceEntries(
|
||||
tenantId: number,
|
||||
openingDate: Date,
|
||||
customersIds?: number[]
|
||||
): 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(
|
||||
R.map(R.assoc('date', openingDate)),
|
||||
R.map(R.assoc('accountNormal', 'debit'))
|
||||
@@ -88,38 +73,29 @@ export default class TransactionsByCustomersService
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Retrieve the customers periods ledger entries.
|
||||
* @param {number} tenantId
|
||||
* @param {Date|string} openingDate
|
||||
* @param {number[]} customersIds
|
||||
* @param {Date} fromDate
|
||||
* @param {Date} toDate
|
||||
* @returns {Promise<ILedgerEntry[]>}
|
||||
*/
|
||||
async getCustomersPeriodTransactions(
|
||||
private async getCustomersPeriodsEntries(
|
||||
tenantId: number,
|
||||
fromDate: Date,
|
||||
toDate: Date
|
||||
fromDate: Date|string,
|
||||
toDate: Date|string,
|
||||
): Promise<ILedgerEntry[]> {
|
||||
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);
|
||||
});
|
||||
|
||||
const transactions =
|
||||
await this.reportRepository.getCustomersPeriodTransactions(
|
||||
tenantId,
|
||||
fromDate,
|
||||
toDate
|
||||
);
|
||||
return R.compose(
|
||||
R.map(R.assoc('accountNormal', 'debit')),
|
||||
R.map((trans) => ({
|
||||
...trans,
|
||||
referenceTypeFormatted: trans.referenceTypeFormatted,
|
||||
})),
|
||||
}))
|
||||
)(transactions);
|
||||
}
|
||||
|
||||
@@ -133,7 +109,6 @@ export default class TransactionsByCustomersService
|
||||
tenantId: number,
|
||||
query: ITransactionsByCustomersFilter
|
||||
): Promise<ITransactionsByCustomersStatement> {
|
||||
const { Customer } = this.tenancy.models(tenantId);
|
||||
const { accountRepository } = this.tenancy.repositories(tenantId);
|
||||
|
||||
// Settings tenant service.
|
||||
@@ -148,29 +123,31 @@ export default class TransactionsByCustomersService
|
||||
...query,
|
||||
};
|
||||
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)
|
||||
.subtract(1, 'days')
|
||||
.toDate();
|
||||
|
||||
// Retrieve all ledger transactions of the opening balance of.
|
||||
const openingBalanceTransactions = await this.getCustomersOpeningBalance(
|
||||
const openingBalanceEntries = await this.getCustomersOpeningBalanceEntries(
|
||||
tenantId,
|
||||
openingBalanceDate
|
||||
);
|
||||
// Retrieve all ledger transactions between opeing and closing period.
|
||||
const customersTransactions = await this.getCustomersPeriodTransactions(
|
||||
const customersTransactions = await this.getCustomersPeriodsEntries(
|
||||
tenantId,
|
||||
query.fromDate,
|
||||
query.toDate
|
||||
);
|
||||
// Concats the opening balance and period customer ledger transactions.
|
||||
const journalTransactions = [
|
||||
...openingBalanceTransactions,
|
||||
...openingBalanceEntries,
|
||||
...customersTransactions,
|
||||
];
|
||||
const journal = Ledger.fromTransactions(journalTransactions);
|
||||
const journal = new Ledger(journalTransactions);
|
||||
|
||||
// Transactions by customers data mapper.
|
||||
const reportInstance = new TransactionsByCustomers(
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -4,23 +4,27 @@ import * as R from 'ramda';
|
||||
import { map } from 'lodash';
|
||||
import TenancyService from 'services/Tenancy/TenancyService';
|
||||
import {
|
||||
IVendor,
|
||||
ITransactionsByVendorsService,
|
||||
ITransactionsByVendorsFilter,
|
||||
ITransactionsByVendorsStatement,
|
||||
ILedgerEntry,
|
||||
} from 'interfaces';
|
||||
import TransactionsByVendor from './TransactionsByVendor';
|
||||
import { ACCOUNT_TYPE } from 'data/AccountTypes';
|
||||
import Ledger from 'services/Accounting/Ledger';
|
||||
import TransactionsByVendorRepository from './TransactionsByVendorRepository';
|
||||
|
||||
export default class TransactionsByVendorsService
|
||||
implements ITransactionsByVendorsService {
|
||||
implements ITransactionsByVendorsService
|
||||
{
|
||||
@Inject()
|
||||
tenancy: TenancyService;
|
||||
|
||||
@Inject('logger')
|
||||
logger: any;
|
||||
|
||||
@Inject()
|
||||
reportRepository: TransactionsByVendorRepository;
|
||||
|
||||
/**
|
||||
* Defaults balance sheet filter query.
|
||||
* @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.
|
||||
* @param {number} tenantId
|
||||
* @param {number} openingDate
|
||||
* @param {number} customersIds
|
||||
* @returns {}
|
||||
* @returns {Promise<ILedgerEntry[]>}
|
||||
*/
|
||||
private async getVendorsOpeningBalance(
|
||||
private async getVendorsOpeningBalanceEntries(
|
||||
tenantId: number,
|
||||
openingDate: Date,
|
||||
customersIds?: number[]
|
||||
): Promise<ILedgerEntry[]> {
|
||||
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
|
||||
);
|
||||
const openingTransactions =
|
||||
await this.reportRepository.getVendorsOpeningBalance(
|
||||
tenantId,
|
||||
openingDate,
|
||||
customersIds
|
||||
);
|
||||
return R.compose(
|
||||
R.map(R.assoc('date', openingDate)),
|
||||
R.map(R.assoc('accountNormal', 'credit'))
|
||||
@@ -105,42 +78,46 @@ export default class TransactionsByVendorsService
|
||||
* @param {Date|string} openingDate
|
||||
* @param {number[]} customersIds
|
||||
*/
|
||||
async getVendorsPeriodTransactions(
|
||||
private async getVendorsPeriodEntries(
|
||||
tenantId: number,
|
||||
fromDate: Date,
|
||||
toDate: Date
|
||||
): Promise<ILedgerEntry[]> {
|
||||
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);
|
||||
});
|
||||
|
||||
const transactions =
|
||||
await this.reportRepository.getVendorsPeriodTransactions(
|
||||
tenantId,
|
||||
fromDate,
|
||||
toDate
|
||||
);
|
||||
return R.compose(
|
||||
R.map(R.assoc('accountNormal', 'credit')),
|
||||
R.map((trans) => ({
|
||||
...trans,
|
||||
referenceTypeFormatted: trans.referenceTypeFormatted,
|
||||
})),
|
||||
}))
|
||||
)(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();
|
||||
|
||||
return [
|
||||
...(await this.getVendorsOpeningBalance(tenantId, openingBalanceDate)),
|
||||
...(await this.getVendorsPeriodTransactions(tenantId, fromDate, toDate)),
|
||||
...(await this.getVendorsOpeningBalanceEntries(
|
||||
tenantId,
|
||||
openingBalanceDate
|
||||
)),
|
||||
...(await this.getVendorsPeriodEntries(tenantId, fromDate, toDate)),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -155,7 +132,7 @@ export default class TransactionsByVendorsService
|
||||
query: ITransactionsByVendorsFilter
|
||||
): Promise<ITransactionsByVendorsStatement> {
|
||||
const { accountRepository } = this.tenancy.repositories(tenantId);
|
||||
|
||||
|
||||
// Settings tenant service.
|
||||
const settings = this.tenancy.settings(tenantId);
|
||||
const baseCurrency = settings.get({
|
||||
@@ -166,19 +143,19 @@ export default class TransactionsByVendorsService
|
||||
const filter = { ...this.defaultQuery, ...query };
|
||||
|
||||
// Retrieve the report vendors.
|
||||
const vendors = await this.getReportVendors(tenantId);
|
||||
const vendors = await this.reportRepository.getVendors(tenantId);
|
||||
|
||||
// Retrieve the accounts graph.
|
||||
const accountsGraph = await accountRepository.getDependencyGraph();
|
||||
|
||||
// Journal transactions.
|
||||
const journalTransactions = await this.getReportTransactions(
|
||||
const reportEntries = await this.getReportEntries(
|
||||
tenantId,
|
||||
filter.fromDate,
|
||||
filter.toDate
|
||||
);
|
||||
// Ledger collection.
|
||||
const journal = Ledger.fromTransactions(journalTransactions);
|
||||
const journal = new Ledger(reportEntries);
|
||||
|
||||
// Transactions by customers data mapper.
|
||||
const reportInstance = new TransactionsByVendor(
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -8,20 +8,24 @@ import {
|
||||
IVendorBalanceSummaryService,
|
||||
IVendorBalanceSummaryQuery,
|
||||
IVendorBalanceSummaryStatement,
|
||||
ILedgerEntry,
|
||||
} from 'interfaces';
|
||||
import { VendorBalanceSummaryReport } from './VendorBalanceSummary';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { ACCOUNT_TYPE } from 'data/AccountTypes';
|
||||
import Ledger from 'services/Accounting/Ledger';
|
||||
import VendorBalanceSummaryRepository from './VendorBalanceSummaryRepository';
|
||||
|
||||
export default class VendorBalanceSummaryService
|
||||
implements IVendorBalanceSummaryService {
|
||||
implements IVendorBalanceSummaryService
|
||||
{
|
||||
@Inject()
|
||||
tenancy: TenancyService;
|
||||
|
||||
@Inject('logger')
|
||||
logger: any;
|
||||
|
||||
@Inject()
|
||||
reportRepo: VendorBalanceSummaryRepository;
|
||||
|
||||
/**
|
||||
* Defaults balance sheet filter query.
|
||||
* @return {IVendorBalanceSummaryQuery}
|
||||
@@ -45,59 +49,22 @@ export default class VendorBalanceSummaryService
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the report vendors.
|
||||
* @param {number} tenantId
|
||||
* @param {number[]} vendorsIds - Vendors ids.
|
||||
* @returns {IVendor[]}
|
||||
* Retrieve the vendors ledger entrjes.
|
||||
* @param {number} tenantId -
|
||||
* @param {Date|string} date -
|
||||
* @returns {Promise<ILedgerEntry>}
|
||||
*/
|
||||
getReportVendors(
|
||||
private async getReportVendorsEntries(
|
||||
tenantId: number,
|
||||
vendorsIds?: number[]
|
||||
): Promise<IVendor[]> {
|
||||
const { Vendor } = this.tenancy.models(tenantId);
|
||||
|
||||
return Vendor.query()
|
||||
.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');
|
||||
}
|
||||
date: Date | string
|
||||
): Promise<ILedgerEntry[]> {
|
||||
const transactions = await this.reportRepo.getVendorsTransactions(
|
||||
tenantId,
|
||||
date
|
||||
);
|
||||
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.
|
||||
const vendorsTransactions = await this.getReportVendorsTransactions(
|
||||
const vendorsEntries = await this.getReportVendorsEntries(
|
||||
tenantId,
|
||||
query.asDate
|
||||
);
|
||||
// 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.
|
||||
const ledger = Ledger.fromTransactions(vendorsTransactions);
|
||||
const vendorsLedger = new Ledger(vendorsEntries);
|
||||
|
||||
// Report instance.
|
||||
const reportInstance = new VendorBalanceSummaryReport(
|
||||
ledger,
|
||||
vendorsLedger,
|
||||
vendors,
|
||||
filter,
|
||||
baseCurrency
|
||||
|
||||
@@ -37,6 +37,7 @@ export default class InventoryService {
|
||||
transformItemEntriesToInventory(transaction: {
|
||||
transactionId: number;
|
||||
transactionType: IItemEntryTransactionType;
|
||||
transactionNumber?: string;
|
||||
|
||||
date: Date | string;
|
||||
direction: TInventoryTransactionDirection;
|
||||
@@ -56,6 +57,10 @@ export default class InventoryService {
|
||||
entryId: entry.id,
|
||||
createdAt: transaction.createdAt,
|
||||
costAccountId: entry.costAccountId,
|
||||
meta: {
|
||||
transactionNumber: transaction.transactionNumber,
|
||||
description: entry.description,
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -205,7 +210,7 @@ export default class InventoryService {
|
||||
inventoryEntry.transactionType
|
||||
);
|
||||
}
|
||||
return InventoryTransaction.query().insert({
|
||||
return InventoryTransaction.query().insertGraph({
|
||||
...inventoryEntry,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -165,9 +165,9 @@ export default class InventoryAverageCostMethod
|
||||
'transactionId',
|
||||
'transactionType',
|
||||
'createdAt',
|
||||
|
||||
'costAccountId',
|
||||
]),
|
||||
inventoryTransactionId: invTransaction.id,
|
||||
};
|
||||
switch (invTransaction.direction) {
|
||||
case 'IN':
|
||||
|
||||
@@ -536,6 +536,7 @@ export default class SaleInvoicesService implements ISalesInvoicesService {
|
||||
const transaction = {
|
||||
transactionId: saleInvoice.id,
|
||||
transactionType: 'SaleInvoice',
|
||||
transactionNumber: saleInvoice.invoiceNo,
|
||||
|
||||
date: saleInvoice.invoiceDate,
|
||||
direction: 'OUT',
|
||||
|
||||
52
server/src/utils/deepdash.ts
Normal file
52
server/src/utils/deepdash.ts
Normal 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,
|
||||
};
|
||||
@@ -51,6 +51,30 @@ const dateRangeCollection = (
|
||||
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) => {
|
||||
switch (rangeType) {
|
||||
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 {
|
||||
accumSum,
|
||||
increment,
|
||||
hashPassword,
|
||||
origin,
|
||||
@@ -354,4 +398,7 @@ export {
|
||||
defaultToTransform,
|
||||
transformToMap,
|
||||
transactionIncrement,
|
||||
transformToMapBy,
|
||||
dateRangeFromToCollection,
|
||||
transformToMapKeyValue
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user