diff --git a/client/package.json b/client/package.json
index 61f6187e5..e1aabffbe 100644
--- a/client/package.json
+++ b/client/package.json
@@ -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",
diff --git a/client/src/components/NumberFormatDropdown/NumberFormatFields.js b/client/src/components/NumberFormatDropdown/NumberFormatFields.js
index 169a5a5e9..776024de3 100644
--- a/client/src/components/NumberFormatDropdown/NumberFormatFields.js
+++ b/client/src/components/NumberFormatDropdown/NumberFormatFields.js
@@ -73,7 +73,7 @@ export default function NumberFormatFields({}) {
label={}
helperText={}
intent={inputIntent({ error, touched })}
- className={classNames(CLASSES.FILL)}
+ className={classNames('form-group--money-format', CLASSES.FILL)}
>
{
+ 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 (
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default compose(
+ withSettings(({ organizationSettings }) => ({
+ organizationName: organizationSettings?.name,
+ })),
+ withCashFlowStatementActions,
+)(CashFlowStatement);
diff --git a/client/src/containers/FinancialStatements/CashFlowStatement/CashFlowStatementActionsBar.js b/client/src/containers/FinancialStatements/CashFlowStatement/CashFlowStatementActionsBar.js
new file mode 100644
index 000000000..c8c487ea4
--- /dev/null
+++ b/client/src/containers/FinancialStatements/CashFlowStatement/CashFlowStatementActionsBar.js
@@ -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 (
+
+
+ }
+ onClick={handleRecalculateReport}
+ icon={}
+ />
+
+
+
+ }
+ text={
+ isFilterDrawerOpen ? (
+
+ ) : (
+
+ )
+ }
+ onClick={handleFilterToggleClick}
+ active={isFilterDrawerOpen}
+ />
+
+
+
+ }
+ minimal={true}
+ interactionKind={PopoverInteractionKind.CLICK}
+ position={Position.BOTTOM_LEFT}
+ >
+ }
+ icon={}
+ />
+
+
+
+ }
+ icon={}
+ />
+
+
+
+
+ }
+ text={}
+ />
+ }
+ text={}
+ />
+
+
+ );
+}
+
+export default compose(
+ withCashFlowStatement(({ cashFlowStatementDrawerFilter }) => ({
+ isFilterDrawerOpen: cashFlowStatementDrawerFilter,
+ })),
+ withCashFlowStatementActions,
+)(CashFlowStatementActionsBar);
diff --git a/client/src/containers/FinancialStatements/CashFlowStatement/CashFlowStatementGeneralPanel.js b/client/src/containers/FinancialStatements/CashFlowStatement/CashFlowStatementGeneralPanel.js
new file mode 100644
index 000000000..436d984d6
--- /dev/null
+++ b/client/src/containers/FinancialStatements/CashFlowStatement/CashFlowStatementGeneralPanel.js
@@ -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 (
+
+
+
+
+
+
+ );
+}
diff --git a/client/src/containers/FinancialStatements/CashFlowStatement/CashFlowStatementHeader.js b/client/src/containers/FinancialStatements/CashFlowStatement/CashFlowStatementHeader.js
new file mode 100644
index 000000000..87585762b
--- /dev/null
+++ b/client/src/containers/FinancialStatements/CashFlowStatement/CashFlowStatementHeader.js
@@ -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 (
+
+
+
+
+
+ );
+}
+
+export default compose(
+ withCashFlowStatement(({ cashFlowStatementDrawerFilter }) => ({
+ isFilterDrawerOpen: cashFlowStatementDrawerFilter,
+ })),
+ withCashFlowStatementActions,
+)(CashFlowStatementHeader);
diff --git a/client/src/containers/FinancialStatements/CashFlowStatement/CashFlowStatementProvider.js b/client/src/containers/FinancialStatements/CashFlowStatement/CashFlowStatementProvider.js
new file mode 100644
index 000000000..c7ffa3ea8
--- /dev/null
+++ b/client/src/containers/FinancialStatements/CashFlowStatement/CashFlowStatementProvider.js
@@ -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 (
+
+
+
+ );
+}
+
+const useCashFlowStatementContext = () =>
+ React.useContext(CashFLowStatementContext);
+
+export { CashFlowStatementProvider, useCashFlowStatementContext };
diff --git a/client/src/containers/FinancialStatements/CashFlowStatement/CashFlowStatementTable.js b/client/src/containers/FinancialStatements/CashFlowStatement/CashFlowStatementTable.js
new file mode 100644
index 000000000..81425a88a
--- /dev/null
+++ b/client/src/containers/FinancialStatements/CashFlowStatement/CashFlowStatementTable.js
@@ -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 (
+
+
+
+ );
+}
diff --git a/client/src/containers/FinancialStatements/CashFlowStatement/components.js b/client/src/containers/FinancialStatements/CashFlowStatement/components.js
new file mode 100644
index 000000000..91c328871
--- /dev/null
+++ b/client/src/containers/FinancialStatements/CashFlowStatement/components.js
@@ -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 (
+
+
+
+ );
+}
diff --git a/client/src/containers/FinancialStatements/CashFlowStatement/utils.js b/client/src/containers/FinancialStatements/CashFlowStatement/utils.js
new file mode 100644
index 000000000..16c5a710a
--- /dev/null
+++ b/client/src/containers/FinancialStatements/CashFlowStatement/utils.js
@@ -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);
+};
diff --git a/client/src/containers/FinancialStatements/CashFlowStatement/withCashFlowStatement.js b/client/src/containers/FinancialStatements/CashFlowStatement/withCashFlowStatement.js
new file mode 100644
index 000000000..ab3d6519a
--- /dev/null
+++ b/client/src/containers/FinancialStatements/CashFlowStatement/withCashFlowStatement.js
@@ -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);
+};
diff --git a/client/src/containers/FinancialStatements/CashFlowStatement/withCashFlowStatementActions.js b/client/src/containers/FinancialStatements/CashFlowStatement/withCashFlowStatementActions.js
new file mode 100644
index 000000000..689d7ac62
--- /dev/null
+++ b/client/src/containers/FinancialStatements/CashFlowStatement/withCashFlowStatementActions.js
@@ -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);
diff --git a/client/src/containers/FinancialStatements/CustomersTransactions/components.js b/client/src/containers/FinancialStatements/CustomersTransactions/components.js
index 04715aa57..4fb5d3b5e 100644
--- a/client/src/containers/FinancialStatements/CustomersTransactions/components.js
+++ b/client/src/containers/FinancialStatements/CustomersTransactions/components.js
@@ -9,6 +9,7 @@ import { CellTextSpan } from 'components/Datatable/Cells';
/**
* Retrieve customers transactions columns.
*/
+
export const useCustomersTransactionsColumns = () => {
const {
customersTransactions: { tableRows },
diff --git a/client/src/containers/FinancialStatements/InventoryItemDetails/InventoryItemDetails.js b/client/src/containers/FinancialStatements/InventoryItemDetails/InventoryItemDetails.js
new file mode 100644
index 000000000..696d5b6cf
--- /dev/null
+++ b/client/src/containers/FinancialStatements/InventoryItemDetails/InventoryItemDetails.js
@@ -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 (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default compose(
+ withSettings(({ organizationSettings }) => ({
+ organizationName: organizationSettings?.name,
+ })),
+ withInventoryItemDetailsActions,
+)(InventoryItemDetails);
diff --git a/client/src/containers/FinancialStatements/InventoryItemDetails/InventoryItemDetailsActionsBar.js b/client/src/containers/FinancialStatements/InventoryItemDetails/InventoryItemDetailsActionsBar.js
new file mode 100644
index 000000000..90fe45962
--- /dev/null
+++ b/client/src/containers/FinancialStatements/InventoryItemDetails/InventoryItemDetailsActionsBar.js
@@ -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 (
+
+
+ }
+ onClick={handleRecalcReport}
+ icon={}
+ />
+
+ }
+ text={
+ isFilterDrawerOpen ? (
+
+ ) : (
+
+ )
+ }
+ onClick={handleFilterToggleClick}
+ active={isFilterDrawerOpen}
+ />
+
+
+ }
+ minimal={true}
+ interactionKind={PopoverInteractionKind.CLICK}
+ position={Position.BOTTOM_LEFT}
+ >
+ }
+ icon={}
+ />
+
+
+
+ }
+ icon={}
+ />
+
+
+
+
+ }
+ text={}
+ />
+ }
+ text={}
+ />
+
+
+ );
+}
+
+export default compose(
+ withInventoryItemDetails(({ inventoryItemDetailDrawerFilter }) => ({
+ isFilterDrawerOpen: inventoryItemDetailDrawerFilter,
+ })),
+ withInventoryItemDetailsActions,
+)(InventoryItemDetailsActionsBar);
diff --git a/client/src/containers/FinancialStatements/InventoryItemDetails/InventoryItemDetailsHeader.js b/client/src/containers/FinancialStatements/InventoryItemDetails/InventoryItemDetailsHeader.js
new file mode 100644
index 000000000..431ee4276
--- /dev/null
+++ b/client/src/containers/FinancialStatements/InventoryItemDetails/InventoryItemDetailsHeader.js
@@ -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 (
+
+
+
+
+
+ );
+}
+
+export default compose(
+ withInventoryItemDetails(({ inventoryItemDetailDrawerFilter }) => ({
+ isFilterDrawerOpen: inventoryItemDetailDrawerFilter,
+ })),
+ withInventoryItemDetailsActions,
+)(InventoryItemDetailsHeader);
diff --git a/client/src/containers/FinancialStatements/InventoryItemDetails/InventoryItemDetailsHeaderGeneralPanel.js b/client/src/containers/FinancialStatements/InventoryItemDetails/InventoryItemDetailsHeaderGeneralPanel.js
new file mode 100644
index 000000000..b3aafc5fb
--- /dev/null
+++ b/client/src/containers/FinancialStatements/InventoryItemDetails/InventoryItemDetailsHeaderGeneralPanel.js
@@ -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 (
+
+
+
+ );
+}
diff --git a/client/src/containers/FinancialStatements/InventoryItemDetails/InventoryItemDetailsProvider.js b/client/src/containers/FinancialStatements/InventoryItemDetails/InventoryItemDetailsProvider.js
new file mode 100644
index 000000000..746fce999
--- /dev/null
+++ b/client/src/containers/FinancialStatements/InventoryItemDetails/InventoryItemDetailsProvider.js
@@ -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 (
+
+
+
+ );
+}
+const useInventoryItemDetailsContext = () =>
+ React.useContext(InventoryItemDetailsContext);
+
+export { InventoryItemDetailsProvider, useInventoryItemDetailsContext };
diff --git a/client/src/containers/FinancialStatements/InventoryItemDetails/InventoryItemDetailsTable.js b/client/src/containers/FinancialStatements/InventoryItemDetails/InventoryItemDetailsTable.js
new file mode 100644
index 000000000..5fa384c0c
--- /dev/null
+++ b/client/src/containers/FinancialStatements/InventoryItemDetails/InventoryItemDetailsTable.js
@@ -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 (
+
+
+
+ );
+}
diff --git a/client/src/containers/FinancialStatements/InventoryItemDetails/components.js b/client/src/containers/FinancialStatements/InventoryItemDetails/components.js
new file mode 100644
index 000000000..95c378daa
--- /dev/null
+++ b/client/src/containers/FinancialStatements/InventoryItemDetails/components.js
@@ -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 (
+
+
+
+ );
+}
diff --git a/client/src/containers/FinancialStatements/InventoryItemDetails/utils.js b/client/src/containers/FinancialStatements/InventoryItemDetails/utils.js
new file mode 100644
index 000000000..b0798ece3
--- /dev/null
+++ b/client/src/containers/FinancialStatements/InventoryItemDetails/utils.js
@@ -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 (
+
+ {cells[index]?.value}
+
+ );
+ },
+ 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);
+};
diff --git a/client/src/containers/FinancialStatements/InventoryItemDetails/withInventoryItemDetails.js b/client/src/containers/FinancialStatements/InventoryItemDetails/withInventoryItemDetails.js
new file mode 100644
index 000000000..bcdeb7169
--- /dev/null
+++ b/client/src/containers/FinancialStatements/InventoryItemDetails/withInventoryItemDetails.js
@@ -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);
+};
diff --git a/client/src/containers/FinancialStatements/InventoryItemDetails/withInventoryItemDetailsActions.js b/client/src/containers/FinancialStatements/InventoryItemDetails/withInventoryItemDetailsActions.js
new file mode 100644
index 000000000..1daaa3eb5
--- /dev/null
+++ b/client/src/containers/FinancialStatements/InventoryItemDetails/withInventoryItemDetailsActions.js
@@ -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);
diff --git a/client/src/hooks/query/financialReports.js b/client/src/hooks/query/financialReports.js
index 4989099ca..476a3f1af 100644
--- a/client/src/hooks/query/financialReports.js
+++ b/client/src/hooks/query/financialReports.js
@@ -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,
+ },
+ );
+}
\ No newline at end of file
diff --git a/client/src/hooks/query/types.js b/client/src/hooks/query/types.js
index 61db1c84e..c2bfb67d4 100644
--- a/client/src/hooks/query/types.js
+++ b/client/src/hooks/query/types.js
@@ -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 = {
diff --git a/client/src/lang/en/index.js b/client/src/lang/en/index.js
index 987743345..15e63018b 100644
--- a/client/src/lang/en/index.js
+++ b/client/src/lang/en/index.js
@@ -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'
};
diff --git a/client/src/routes/dashboard.js b/client/src/routes/dashboard.js
index 74121459a..5e2e31aa4 100644
--- a/client/src/routes/dashboard.js
+++ b/client/src/routes/dashboard.js
@@ -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(() =>
diff --git a/client/src/store/financialStatement/financialStatements.actions.js b/client/src/store/financialStatement/financialStatements.actions.js
index a972d42db..0ef8f9a41 100644
--- a/client/src/store/financialStatement/financialStatements.actions.js
+++ b/client/src/store/financialStatement/financialStatements.actions.js
@@ -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,
+ },
+ };
+}
diff --git a/client/src/store/financialStatement/financialStatements.reducer.js b/client/src/store/financialStatement/financialStatements.reducer.js
index e0b725b33..63fea46b2 100644
--- a/client/src/store/financialStatement/financialStatements.reducer.js
+++ b/client/src/store/financialStatement/financialStatements.reducer.js
@@ -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',
+ ),
});
diff --git a/client/src/store/financialStatement/financialStatements.selectors.js b/client/src/store/financialStatement/financialStatements.selectors.js
index c87c10cc9..f19337fa2 100644
--- a/client/src/store/financialStatement/financialStatements.selectors.js
+++ b/client/src/store/financialStatement/financialStatements.selectors.js
@@ -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;
+ },
+);
diff --git a/client/src/store/financialStatement/financialStatements.types.js b/client/src/store/financialStatement/financialStatements.types.js
index 7b49a6739..ef82d5b92 100644
--- a/client/src/store/financialStatement/financialStatements.types.js
+++ b/client/src/store/financialStatement/financialStatements.types.js
@@ -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',
};
diff --git a/client/src/style/pages/FinancialStatements/CashFlowStatement.scss b/client/src/style/pages/FinancialStatements/CashFlowStatement.scss
new file mode 100644
index 000000000..2ff22b8f2
--- /dev/null
+++ b/client/src/style/pages/FinancialStatements/CashFlowStatement.scss
@@ -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;
+ }
+ }
+}
diff --git a/client/src/style/pages/FinancialStatements/InventoryItemDetails.scss b/client/src/style/pages/FinancialStatements/InventoryItemDetails.scss
new file mode 100644
index 000000000..d2458d1c3
--- /dev/null
+++ b/client/src/style/pages/FinancialStatements/InventoryItemDetails.scss
@@ -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;
+ }
+ }
+}
diff --git a/server/package.json b/server/package.json
index 9667985c8..b61acca96 100644
--- a/server/package.json
+++ b/server/package.json
@@ -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",
diff --git a/server/src/api/controllers/BaseController.ts b/server/src/api/controllers/BaseController.ts
index 5627d57cb..403504c52 100644
--- a/server/src/api/controllers/BaseController.ts
+++ b/server/src/api/controllers/BaseController.ts
@@ -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);
}
}
diff --git a/server/src/api/controllers/FinancialStatements.ts b/server/src/api/controllers/FinancialStatements.ts
index 497b499cc..dbda169f6 100644
--- a/server/src/api/controllers/FinancialStatements.ts
+++ b/server/src/api/controllers/FinancialStatements.ts
@@ -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;
}
}
diff --git a/server/src/api/controllers/FinancialStatements/CashFlow/CashFlow.ts b/server/src/api/controllers/FinancialStatements/CashFlow/CashFlow.ts
new file mode 100644
index 000000000..5434f33c1
--- /dev/null
+++ b/server/src/api/controllers/FinancialStatements/CashFlow/CashFlow.ts
@@ -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);
+ }
+ }
+}
diff --git a/server/src/api/controllers/FinancialStatements/InventoryDetails/index.ts b/server/src/api/controllers/FinancialStatements/InventoryDetails/index.ts
new file mode 100644
index 000000000..74f30ded9
--- /dev/null
+++ b/server/src/api/controllers/FinancialStatements/InventoryDetails/index.ts
@@ -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);
+ }
+ }
+}
diff --git a/server/src/data/AccountTypes.ts b/server/src/data/AccountTypes.ts
index f3b108627..ceef16bb1 100644
--- a/server/src/data/AccountTypes.ts
+++ b/server/src/data/AccountTypes.ts
@@ -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',
diff --git a/server/src/database/migrations/20200722164255_create_inventory_transaction_meta_table.js b/server/src/database/migrations/20200722164255_create_inventory_transaction_meta_table.js
new file mode 100644
index 000000000..15f348a17
--- /dev/null
+++ b/server/src/database/migrations/20200722164255_create_inventory_transaction_meta_table.js
@@ -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) {};
+
\ No newline at end of file
diff --git a/server/src/database/migrations/20200810121807_create_inventory_cost_lot_tracker_table.js b/server/src/database/migrations/20200810121807_create_inventory_cost_lot_tracker_table.js
index 7f45dbdef..d490cbcc7 100644
--- a/server/src/database/migrations/20200810121807_create_inventory_cost_lot_tracker_table.js
+++ b/server/src/database/migrations/20200810121807_create_inventory_cost_lot_tracker_table.js
@@ -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();
});
diff --git a/server/src/interfaces/Account.ts b/server/src/interfaces/Account.ts
index 39303e829..60cf1dbee 100644
--- a/server/src/interfaces/Account.ts
+++ b/server/src/interfaces/Account.ts
@@ -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 {
diff --git a/server/src/interfaces/CashFlow.ts b/server/src/interfaces/CashFlow.ts
new file mode 100644
index 000000000..1a8877b4d
--- /dev/null
+++ b/server/src/interfaces/CashFlow.ts
@@ -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;
+}
+
+// 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,
+}
\ No newline at end of file
diff --git a/server/src/interfaces/InventoryDetails.ts b/server/src/interfaces/InventoryDetails.ts
new file mode 100644
index 000000000..5ce72b343
--- /dev/null
+++ b/server/src/interfaces/InventoryDetails.ts
@@ -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[];
diff --git a/server/src/interfaces/InventoryTransaction.ts b/server/src/interfaces/InventoryTransaction.ts
index 47e4d4219..f56666bc3 100644
--- a/server/src/interfaces/InventoryTransaction.ts
+++ b/server/src/interfaces/InventoryTransaction.ts
@@ -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,
-};
\ No newline at end of file
+ itemId: number;
+ balanceChange: number;
+}
diff --git a/server/src/interfaces/Ledger.ts b/server/src/interfaces/Ledger.ts
index e3dd19288..938a997b5 100644
--- a/server/src/interfaces/Ledger.ts
+++ b/server/src/interfaces/Ledger.ts
@@ -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,
}
diff --git a/server/src/interfaces/Table.ts b/server/src/interfaces/Table.ts
index f04d844e0..944ace6db 100644
--- a/server/src/interfaces/Table.ts
+++ b/server/src/interfaces/Table.ts
@@ -12,4 +12,14 @@ export interface ITableCell {
export type ITableRow = {
rows: ITableCell[];
-};
\ No newline at end of file
+};
+
+export interface ITableColumn {
+ key: string,
+ label: string,
+}
+
+export interface ITable {
+ columns: ITableColumn[],
+ data: ITableRow[],
+}
\ No newline at end of file
diff --git a/server/src/interfaces/TransactionsByContacts.ts b/server/src/interfaces/TransactionsByContacts.ts
index 9f4569ba0..82a38002a 100644
--- a/server/src/interfaces/TransactionsByContacts.ts
+++ b/server/src/interfaces/TransactionsByContacts.ts
@@ -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;
diff --git a/server/src/interfaces/index.ts b/server/src/interfaces/index.ts
index 090f22f32..ba054121d 100644
--- a/server/src/interfaces/index.ts
+++ b/server/src/interfaces/index.ts
@@ -50,4 +50,6 @@ export * from './TransactionsByCustomers';
export * from './TransactionsByContacts';
export * from './TransactionsByVendors';
export * from './Table';
-export * from './Ledger';
\ No newline at end of file
+export * from './Ledger';
+export * from './CashFlow';
+export * from './InventoryDetails';
diff --git a/server/src/models/AccountTransaction.js b/server/src/models/AccountTransaction.js
index 0d615a294..0cf9e246a 100644
--- a/server/src/models/AccountTransaction.js
+++ b/server/src/models/AccountTransaction.js
@@ -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}')`);
}
};
}
diff --git a/server/src/models/InventoryTransaction.js b/server/src/models/InventoryTransaction.js
index 87146ced0..8c2b10f4c 100644
--- a/server/src/models/InventoryTransaction.js
+++ b/server/src/models/InventoryTransaction.js
@@ -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,
diff --git a/server/src/models/InventoryTransactionMeta.js b/server/src/models/InventoryTransactionMeta.js
new file mode 100644
index 000000000..62a232b64
--- /dev/null
+++ b/server/src/models/InventoryTransactionMeta.js
@@ -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'
+ }
+ }
+ };
+ }
+}
diff --git a/server/src/services/Accounting/Ledger.ts b/server/src/services/Accounting/Ledger.ts
index 93870dfdb..fc78e554f 100644
--- a/server/src/services/Accounting/Ledger.ts
+++ b/server/src/services/Accounting/Ledger.ts
@@ -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);
}
diff --git a/server/src/services/FinancialStatements/BalanceSheet/BalanceSheet.ts b/server/src/services/FinancialStatements/BalanceSheet/BalanceSheet.ts
index c166e123a..d8b45706c 100644
--- a/server/src/services/FinancialStatements/BalanceSheet/BalanceSheet.ts
+++ b/server/src/services/FinancialStatements/BalanceSheet/BalanceSheet.ts
@@ -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;
diff --git a/server/src/services/FinancialStatements/BalanceSheet/BalanceSheetService.ts b/server/src/services/FinancialStatements/BalanceSheet/BalanceSheetService.ts
index 5fb30b244..3c0c7b5ba 100644
--- a/server/src/services/FinancialStatements/BalanceSheet/BalanceSheetService.ts
+++ b/server/src/services/FinancialStatements/BalanceSheet/BalanceSheetService.ts
@@ -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(
diff --git a/server/src/services/FinancialStatements/CashFlow/CashFlow.ts b/server/src/services/FinancialStatements/CashFlow/CashFlow.ts
new file mode 100644
index 000000000..d000210b8
--- /dev/null
+++ b/server/src/services/FinancialStatements/CashFlow/CashFlow.ts
@@ -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;
+ readonly cashFlowSchemaSeq: Array;
+ readonly accountByTypeMap: Map;
+ readonly accountsByRootType: Map;
+ 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;
diff --git a/server/src/services/FinancialStatements/CashFlow/CashFlowDatePeriods.ts b/server/src/services/FinancialStatements/CashFlow/CashFlowDatePeriods.ts
new file mode 100644
index 000000000..75465b74b
--- /dev/null
+++ b/server/src/services/FinancialStatements/CashFlow/CashFlowDatePeriods.ts
@@ -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);
+ }
+}
diff --git a/server/src/services/FinancialStatements/CashFlow/CashFlowRepository.ts b/server/src/services/FinancialStatements/CashFlow/CashFlowRepository.ts
new file mode 100644
index 000000000..1bb2431d2
--- /dev/null
+++ b/server/src/services/FinancialStatements/CashFlow/CashFlowRepository.ts
@@ -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}
+ */
+ public async cashFlowAccounts(tenantId: number): Promise {
+ 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}
+ */
+ public cashAtBeginningTotalTransactions(
+ tenantId: number,
+ filter: ICashFlowStatementQuery
+ ): Promise {
+ 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}
+ */
+ public getAccountsTransactions(
+ tenantId: number,
+ filter: ICashFlowStatementQuery
+ ): Promise {
+ 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}
+ */
+ public getNetIncomeTransactions(
+ tenantId: number,
+ filter: ICashFlowStatementQuery
+ ): Promise {
+ 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}
+ */
+ public cashAtBeginningPeriodTransactions(
+ tenantId: number,
+ filter: ICashFlowStatementQuery
+ ): Promise {
+ 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);
+ });
+ }
+}
diff --git a/server/src/services/FinancialStatements/CashFlow/CashFlowService.ts b/server/src/services/FinancialStatements/CashFlow/CashFlowService.ts
new file mode 100644
index 000000000..ddd28365c
--- /dev/null
+++ b/server/src/services/FinancialStatements/CashFlow/CashFlowService.ts
@@ -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}
+ */
+ private async cashAtBeginningTransactions(
+ tenantId: number,
+ filter: ICashFlowStatementQuery
+ ): Promise {
+ 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}
+ */
+ 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,
+ };
+ }
+}
diff --git a/server/src/services/FinancialStatements/CashFlow/CashFlowTable.ts b/server/src/services/FinancialStatements/CashFlow/CashFlowTable.ts
new file mode 100644
index 000000000..35eee34bc
--- /dev/null
+++ b/server/src/services/FinancialStatements/CashFlow/CashFlowTable.ts
@@ -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())
+ )([]);
+ }
+}
diff --git a/server/src/services/FinancialStatements/CashFlow/schema.ts b/server/src/services/FinancialStatements/CashFlow/schema.ts
new file mode 100644
index 000000000..ce786c2e9
--- /dev/null
+++ b/server/src/services/FinancialStatements/CashFlow/schema.ts
@@ -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[];
diff --git a/server/src/services/FinancialStatements/CustomerBalanceSummary/CustomerBalanceSummaryRepository.ts b/server/src/services/FinancialStatements/CustomerBalanceSummary/CustomerBalanceSummaryRepository.ts
new file mode 100644
index 000000000..5c5f6842f
--- /dev/null
+++ b/server/src/services/FinancialStatements/CustomerBalanceSummary/CustomerBalanceSummaryRepository.ts
@@ -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}
+ */
+ public getReceivableAccounts(tenantId: number): Promise {
+ 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;
+ }
+}
diff --git a/server/src/services/FinancialStatements/CustomerBalanceSummary/CustomerBalanceSummaryService.ts b/server/src/services/FinancialStatements/CustomerBalanceSummary/CustomerBalanceSummaryService.ts
index 7f648194a..e3ad79584 100644
--- a/server/src/services/FinancialStatements/CustomerBalanceSummary/CustomerBalanceSummaryService.ts
+++ b/server/src/services/FinancialStatements/CustomerBalanceSummary/CustomerBalanceSummaryService.ts
@@ -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}
*/
- 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 {
+ 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,
};
}
}
diff --git a/server/src/services/FinancialStatements/FinancialSheet.ts b/server/src/services/FinancialStatements/FinancialSheet.ts
index d1a83ad31..8fe170244 100644
--- a/server/src/services/FinancialStatements/FinancialSheet.ts
+++ b/server/src/services/FinancialStatements/FinancialSheet.ts
@@ -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(),
+ };
}
}
diff --git a/server/src/services/FinancialStatements/InventoryDetails/InventoryDetails.ts b/server/src/services/FinancialStatements/InventoryDetails/InventoryDetails.ts
new file mode 100644
index 000000000..c94ec6e94
--- /dev/null
+++ b/server/src/services/FinancialStatements/InventoryDetails/InventoryDetails.ts
@@ -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;
+ readonly openingBalanceTransactions: Map;
+ 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);
+ }
+}
diff --git a/server/src/services/FinancialStatements/InventoryDetails/InventoryDetailsRepository.ts b/server/src/services/FinancialStatements/InventoryDetails/InventoryDetailsRepository.ts
new file mode 100644
index 000000000..c8d0abe87
--- /dev/null
+++ b/server/src/services/FinancialStatements/InventoryDetails/InventoryDetailsRepository.ts
@@ -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}
+ */
+ public getInventoryItems(tenantId: number): Promise {
+ 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}
+ */
+ public async openingBalanceTransactions(
+ tenantId: number,
+ filter: IInventoryDetailsQuery
+ ): Promise {
+ 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}
+ */
+ public async itemInventoryTransactions(
+ tenantId: number,
+ filter: IInventoryDetailsQuery
+ ): Promise {
+ const { InventoryTransaction } = this.tenancy.models(tenantId);
+
+ const inventoryTransactions = InventoryTransaction.query()
+ .modify('filterDateRange', filter.fromDate, filter.toDate)
+ .withGraphFetched('meta')
+ .withGraphFetched('costLotAggregated');
+
+ return inventoryTransactions;
+ }
+}
diff --git a/server/src/services/FinancialStatements/InventoryDetails/InventoryDetailsService.ts b/server/src/services/FinancialStatements/InventoryDetails/InventoryDetailsService.ts
new file mode 100644
index 000000000..186d5d29e
--- /dev/null
+++ b/server/src/services/FinancialStatements/InventoryDetails/InventoryDetailsService.ts
@@ -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 {
+ // 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(),
+ };
+ }
+}
diff --git a/server/src/services/FinancialStatements/InventoryDetails/InventoryDetailsTable.ts b/server/src/services/FinancialStatements/InventoryDetails/InventoryDetailsTable.ts
new file mode 100644
index 000000000..9af965aad
--- /dev/null
+++ b/server/src/services/FinancialStatements/InventoryDetails/InventoryDetailsTable.ts
@@ -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' },
+ ];
+ }
+}
diff --git a/server/src/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomersRepository.ts b/server/src/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomersRepository.ts
new file mode 100644
index 000000000..603c339db
--- /dev/null
+++ b/server/src/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomersRepository.ts
@@ -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}
+ */
+ 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}
+ */
+ public async getReceivableAccounts(tenantId: number): Promise {
+ 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}
+ */
+ public async getCustomersOpeningBalanceTransactions(
+ tenantId: number,
+ openingDate: Date,
+ customersIds?: number[]
+ ): Promise {
+ 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}
+ */
+ public async getCustomersPeriodTransactions(
+ tenantId: number,
+ fromDate: Date,
+ toDate: Date
+ ): Promise {
+ 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;
+ }
+}
diff --git a/server/src/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomersService.ts b/server/src/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomersService.ts
index 0bf91adba..0de5a070c 100644
--- a/server/src/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomersService.ts
+++ b/server/src/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomersService.ts
@@ -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}
*/
- 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 {
- 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}
*/
- async getCustomersPeriodTransactions(
+ private async getCustomersPeriodsEntries(
tenantId: number,
- fromDate: Date,
- toDate: Date
+ fromDate: Date|string,
+ toDate: Date|string,
): Promise {
- 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 {
- 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(
diff --git a/server/src/services/FinancialStatements/TransactionsByVendor/TransactionsByVendorRepository.ts b/server/src/services/FinancialStatements/TransactionsByVendor/TransactionsByVendorRepository.ts
new file mode 100644
index 000000000..c67b09741
--- /dev/null
+++ b/server/src/services/FinancialStatements/TransactionsByVendor/TransactionsByVendorRepository.ts
@@ -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}
+ */
+ public getVendors(tenantId: number): Promise {
+ const { Vendor } = this.tenancy.models(tenantId);
+
+ return Vendor.query().orderBy('displayName');
+ }
+
+ /**
+ * Retrieve the accounts receivable.
+ * @param {number} tenantId
+ * @returns {Promise}
+ */
+ private async getPayableAccounts(tenantId: number): Promise {
+ 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 {
+ 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 {
+ 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;
+ }
+}
diff --git a/server/src/services/FinancialStatements/TransactionsByVendor/TransactionsByVendorService.ts b/server/src/services/FinancialStatements/TransactionsByVendor/TransactionsByVendorService.ts
index df7686a1d..354e5fa45 100644
--- a/server/src/services/FinancialStatements/TransactionsByVendor/TransactionsByVendorService.ts
+++ b/server/src/services/FinancialStatements/TransactionsByVendor/TransactionsByVendorService.ts
@@ -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 {
- 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}
*/
- private async getVendorsOpeningBalance(
+ private async getVendorsOpeningBalanceEntries(
tenantId: number,
openingDate: Date,
customersIds?: number[]
): Promise {
- 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 {
- 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}
+ */
+ private async getReportEntries(
+ tenantId: number,
+ fromDate: Date,
+ toDate: Date
+ ): Promise {
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 {
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(
diff --git a/server/src/services/FinancialStatements/VendorBalanceSummary/VendorBalanceSummaryRepository.ts b/server/src/services/FinancialStatements/VendorBalanceSummary/VendorBalanceSummaryRepository.ts
new file mode 100644
index 000000000..ed929bb9d
--- /dev/null
+++ b/server/src/services/FinancialStatements/VendorBalanceSummary/VendorBalanceSummaryRepository.ts
@@ -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 {
+ 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}
+ */
+ public getPayableAccounts(tenantId: number): Promise {
+ 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;
+ }
+}
diff --git a/server/src/services/FinancialStatements/VendorBalanceSummary/VendorBalanceSummaryService.ts b/server/src/services/FinancialStatements/VendorBalanceSummary/VendorBalanceSummaryService.ts
index bf8a93673..19065abb3 100644
--- a/server/src/services/FinancialStatements/VendorBalanceSummary/VendorBalanceSummaryService.ts
+++ b/server/src/services/FinancialStatements/VendorBalanceSummary/VendorBalanceSummaryService.ts
@@ -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}
*/
- getReportVendors(
+ private async getReportVendorsEntries(
tenantId: number,
- vendorsIds?: number[]
- ): Promise {
- 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 {
+ 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
diff --git a/server/src/services/Inventory/Inventory.ts b/server/src/services/Inventory/Inventory.ts
index 64b03fa15..22d909c09 100644
--- a/server/src/services/Inventory/Inventory.ts
+++ b/server/src/services/Inventory/Inventory.ts
@@ -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,
});
}
diff --git a/server/src/services/Inventory/InventoryAverageCost.ts b/server/src/services/Inventory/InventoryAverageCost.ts
index ca7801dd6..b69613a91 100644
--- a/server/src/services/Inventory/InventoryAverageCost.ts
+++ b/server/src/services/Inventory/InventoryAverageCost.ts
@@ -165,9 +165,9 @@ export default class InventoryAverageCostMethod
'transactionId',
'transactionType',
'createdAt',
-
'costAccountId',
]),
+ inventoryTransactionId: invTransaction.id,
};
switch (invTransaction.direction) {
case 'IN':
diff --git a/server/src/services/Sales/SalesInvoices.ts b/server/src/services/Sales/SalesInvoices.ts
index d8094ca0e..dfdf04290 100644
--- a/server/src/services/Sales/SalesInvoices.ts
+++ b/server/src/services/Sales/SalesInvoices.ts
@@ -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',
diff --git a/server/src/utils/deepdash.ts b/server/src/utils/deepdash.ts
new file mode 100644
index 000000000..b1e2f96f4
--- /dev/null
+++ b/server/src/utils/deepdash.ts
@@ -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,
+};
diff --git a/server/src/utils/index.ts b/server/src/utils/index.ts
index 9d8fb0edd..a5f894765 100644
--- a/server/src/utils/index.ts
+++ b/server/src/utils/index.ts
@@ -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
};