This commit is contained in:
elforjani3
2020-12-31 12:26:53 +02:00
63 changed files with 2496 additions and 1932 deletions

View File

@@ -49,7 +49,6 @@ const CLASSES = {
SELECT_LIST_FILL_POPOVER: 'select-list--fill-popover', SELECT_LIST_FILL_POPOVER: 'select-list--fill-popover',
PREFERENCES_PAGE: 'preferences-page', PREFERENCES_PAGE: 'preferences-page',
PREFERENCES_PAGE_SIDEBAR: 'preferences-page__sidebar', PREFERENCES_PAGE_SIDEBAR: 'preferences-page__sidebar',
PREFERENCES_PAGE_TOPBAR: 'preferences-page__topbar', PREFERENCES_PAGE_TOPBAR: 'preferences-page__topbar',

View File

@@ -104,6 +104,7 @@ export default function DataTable({
initialState: { initialState: {
pageIndex: initialPageIndex, pageIndex: initialPageIndex,
pageSize: initialPageSize, pageSize: initialPageSize,
expanded
}, },
manualPagination, manualPagination,
pageCount: controlledPageCount, pageCount: controlledPageCount,

View File

@@ -41,6 +41,7 @@ import EmptyStatus from './EmptyStatus';
import DashboardCard from './Dashboard/DashboardCard'; import DashboardCard from './Dashboard/DashboardCard';
import InputPrependText from './Forms/InputPrependText'; import InputPrependText from './Forms/InputPrependText';
import PageFormBigNumber from './PageFormBigNumber'; import PageFormBigNumber from './PageFormBigNumber';
import AccountsMultiSelect from './AccountsMultiSelect';
const Hint = FieldHint; const Hint = FieldHint;
@@ -87,5 +88,6 @@ export {
EmptyStatus, EmptyStatus,
DashboardCard, DashboardCard,
InputPrependText, InputPrependText,
PageFormBigNumber PageFormBigNumber,
AccountsMultiSelect,
}; };

View File

@@ -4,6 +4,7 @@ import { compose } from 'utils';
import { useQuery } from 'react-query'; import { useQuery } from 'react-query';
import moment from 'moment'; import moment from 'moment';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { queryCache } from 'react-query';
import BalanceSheetHeader from './BalanceSheetHeader'; import BalanceSheetHeader from './BalanceSheetHeader';
import BalanceSheetTable from './BalanceSheetTable'; import BalanceSheetTable from './BalanceSheetTable';
@@ -18,66 +19,74 @@ import withSettings from 'containers/Settings/withSettings';
import withBalanceSheetActions from './withBalanceSheetActions'; import withBalanceSheetActions from './withBalanceSheetActions';
import withBalanceSheetDetail from './withBalanceSheetDetail'; import withBalanceSheetDetail from './withBalanceSheetDetail';
import { transformFilterFormToQuery } from 'containers/FinancialStatements/common';
function BalanceSheet({ function BalanceSheet({
// #withDashboardActions // #withDashboardActions
changePageTitle, changePageTitle,
setDashboardBackLink,
// #withBalanceSheetActions // #withBalanceSheetActions
fetchBalanceSheet, fetchBalanceSheet,
refreshBalanceSheet,
// #withBalanceSheetDetail // #withBalanceSheetDetail
balanceSheetFilter, balanceSheetRefresh,
// #withPreferences // #withPreferences
organizationSettings, organizationName,
}) { }) {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const [filter, setFilter] = useState({ const [filter, setFilter] = useState({
from_date: moment().startOf('year').format('YYYY-MM-DD'), fromDate: moment().startOf('year').format('YYYY-MM-DD'),
to_date: moment().endOf('year').format('YYYY-MM-DD'), toDate: moment().endOf('year').format('YYYY-MM-DD'),
basis: 'cash', basis: 'cash',
display_columns_type: 'total', displayColumnsType: 'total',
display_columns_by: '', accountsFilter: 'all-accounts',
none_zero: false,
}); });
const [refresh, setRefresh] = useState(true);
const fetchHook = useQuery( // Fetches the balance sheet.
['balance-sheet', filter], const fetchHook = useQuery(['balance-sheet', filter], (key, query) =>
(key, query) => fetchBalanceSheet({ ...query }), fetchBalanceSheet({ ...transformFilterFormToQuery(query) }),
{ manual: true },
); );
// Handle fetch the data of balance sheet.
const handleFetchData = useCallback(() => {
setRefresh(true);
}, []);
useEffect(() => { useEffect(() => {
changePageTitle(formatMessage({ id: 'balance_sheet' })); changePageTitle(formatMessage({ id: 'balance_sheet' }));
}, [changePageTitle, formatMessage]); }, [changePageTitle, formatMessage]);
// Observes the balance sheet refresh to invalid the query to refresh it.
useEffect(() => {
if (balanceSheetRefresh) {
queryCache.invalidateQueries('balance-sheet');
refreshBalanceSheet(false);
}
}, [balanceSheetRefresh, refreshBalanceSheet]);
useEffect(() => {
// Show the back link on dashboard topbar.
setDashboardBackLink(true);
return () => {
// Hide the back link on dashboard topbar.
setDashboardBackLink(false);
};
});
// Handle re-fetch balance sheet after filter change. // Handle re-fetch balance sheet after filter change.
const handleFilterSubmit = useCallback( const handleFilterSubmit = useCallback(
(filter) => { (filter) => {
const _filter = { const _filter = {
...filter, ...filter,
from_date: moment(filter.from_date).format('YYYY-MM-DD'), fromDate: moment(filter.fromDate).format('YYYY-MM-DD'),
to_date: moment(filter.to_date).format('YYYY-MM-DD'), toDate: moment(filter.toDate).format('YYYY-MM-DD'),
}; };
setFilter({ ..._filter }); setFilter({ ..._filter });
setRefresh(true); refreshBalanceSheet(true);
}, },
[setFilter], [setFilter, refreshBalanceSheet],
); );
useEffect(() => {
if (refresh) {
fetchHook.refetch({ force: true });
setRefresh(false);
}
}, [refresh]);
return ( return (
<DashboardInsider> <DashboardInsider>
<BalanceSheetActionsBar /> <BalanceSheetActionsBar />
@@ -87,15 +96,9 @@ function BalanceSheet({
<BalanceSheetHeader <BalanceSheetHeader
pageFilter={filter} pageFilter={filter}
onSubmitFilter={handleFilterSubmit} onSubmitFilter={handleFilterSubmit}
show={balanceSheetFilter}
/> />
<div class="financial-statement__body"> <div class="financial-statement__body">
<BalanceSheetTable <BalanceSheetTable companyName={organizationName} />
companyName={organizationSettings.name}
balanceSheetQuery={filter}
onFetchData={handleFetchData}
/>
</div> </div>
</FinancialStatement> </FinancialStatement>
</DashboardPageContent> </DashboardPageContent>
@@ -106,8 +109,10 @@ function BalanceSheet({
export default compose( export default compose(
withDashboardActions, withDashboardActions,
withBalanceSheetActions, withBalanceSheetActions,
withBalanceSheetDetail(({ balanceSheetFilter }) => ({ withBalanceSheetDetail(({ balanceSheetRefresh }) => ({
balanceSheetFilter, balanceSheetRefresh,
})),
withSettings(({ organizationSettings }) => ({
organizationName: organizationSettings.name,
})), })),
withSettings,
)(BalanceSheet); )(BalanceSheet);

View File

@@ -1,4 +1,4 @@
import React, { useState } from 'react'; import React from 'react';
import { import {
NavbarGroup, NavbarGroup,
Button, Button,
@@ -13,26 +13,24 @@ import classNames from 'classnames';
import Icon from 'components/Icon'; import Icon from 'components/Icon';
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar'; import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
import FilterDropdown from 'components/FilterDropdown';
import { If } from 'components';
import { compose } from 'utils'; import { compose } from 'utils';
import withBalanceSheetDetail from './withBalanceSheetDetail'; import withBalanceSheetDetail from './withBalanceSheetDetail';
import withBalanceSheetActions from './withBalanceSheetActions'; import withBalanceSheetActions from './withBalanceSheetActions';
function BalanceSheetActionsBar({ function BalanceSheetActionsBar({
// #withBalanceSheetDetail // #withBalanceSheetDetail
balanceSheetFilter, balanceSheetFilter,
// #withBalanceSheetActions // #withBalanceSheetActions
toggleBalanceSheetFilter, toggleBalanceSheetFilter,
refreshBalanceSheet refreshBalanceSheet,
}) { }) {
const handleFilterToggleClick = () => { const handleFilterToggleClick = () => {
toggleBalanceSheetFilter(); toggleBalanceSheetFilter();
}; };
// Handle recalculate the report button.
const handleRecalcReport = () => { const handleRecalcReport = () => {
refreshBalanceSheet(true); refreshBalanceSheet(true);
}; };
@@ -41,39 +39,21 @@ function BalanceSheetActionsBar({
<DashboardActionsBar> <DashboardActionsBar>
<NavbarGroup> <NavbarGroup>
<Button <Button
className={classNames(Classes.MINIMAL, 'button--table-views')} className={classNames(Classes.MINIMAL, 'button--gray-highlight')}
icon={<Icon icon="cog-16" iconSize={16} />}
text={<T id={'customize_report'} />}
/>
<NavbarDivider />
<Button
className={classNames(
Classes.MINIMAL,
'button--gray-highlight',
)}
text={<T id={'recalc_report'} />} text={<T id={'recalc_report'} />}
onClick={handleRecalcReport} onClick={handleRecalcReport}
icon={<Icon icon="refresh-16" iconSize={16} />} icon={<Icon icon="refresh-16" iconSize={16} />}
/> />
<NavbarDivider />
<If condition={balanceSheetFilter}>
<Button <Button
className={Classes.MINIMAL} className={classNames(Classes.MINIMAL, 'button--table-views')}
text={<T id={'hide_filter'} />} icon={<Icon icon="cog-16" iconSize={16} />}
text={!balanceSheetFilter ? <T id={'customize_report'} /> : <T id={'hide_customizer'} />}
onClick={handleFilterToggleClick} onClick={handleFilterToggleClick}
icon={<Icon icon="arrow-to-top" />} active={balanceSheetFilter}
/> />
</If> <NavbarDivider />
<If condition={!balanceSheetFilter}>
<Button
className={Classes.MINIMAL}
text={<T id={'show_filter'} />}
onClick={handleFilterToggleClick}
icon={<Icon icon="arrow-to-bottom" />}
/>
</If>
<Popover <Popover
// content={} // content={}
@@ -91,7 +71,7 @@ function BalanceSheetActionsBar({
<Button <Button
className={Classes.MINIMAL} className={Classes.MINIMAL}
icon={<Icon icon='print-16' iconSize={16} />} icon={<Icon icon="print-16" iconSize={16} />}
text={<T id={'print'} />} text={<T id={'print'} />}
/> />
<Button <Button

View File

@@ -1,126 +1,105 @@
import React, { useCallback, useEffect } from 'react'; import React, { useEffect } from 'react';
import FinancialStatementHeader from 'containers/FinancialStatements/FinancialStatementHeader'; import { Tabs, Tab, Button, Intent } from '@blueprintjs/core';
import { Row, Col, Visible } from 'react-grid-system'; import { FormattedMessage as T, useIntl } from 'react-intl';
import { FormGroup } from '@blueprintjs/core';
import moment from 'moment'; import moment from 'moment';
import * as Yup from 'yup'; import * as Yup from 'yup';
import { useFormik } from 'formik'; import { Formik, Form } from 'formik';
import { FormattedMessage as T, useIntl } from 'react-intl';
import FinancialStatementDateRange from 'containers/FinancialStatements/FinancialStatementDateRange'; import FinancialStatementHeader from 'containers/FinancialStatements/FinancialStatementHeader';
import SelectDisplayColumnsBy from '../SelectDisplayColumnsBy';
import RadiosAccountingBasis from '../RadiosAccountingBasis';
import FinancialAccountsFilter from '../FinancialAccountsFilter';
import withBalanceSheet from './withBalanceSheetDetail'; import withBalanceSheet from './withBalanceSheetDetail';
import withBalanceSheetActions from './withBalanceSheetActions'; import withBalanceSheetActions from './withBalanceSheetActions';
import { compose } from 'utils'; import { compose } from 'utils';
import BalanceSheetHeaderGeneralPanal from './BalanceSheetHeaderGeneralPanal';
function BalanceSheetHeader({ function BalanceSheetHeader({
// #ownProps
onSubmitFilter, onSubmitFilter,
pageFilter, pageFilter,
show,
refresh, // #withBalanceSheet
balanceSheetFilter,
// #withBalanceSheetActions // #withBalanceSheetActions
refreshBalanceSheet, toggleBalanceSheetFilter,
}) { }) {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const formik = useFormik({ // Filter form initial values.
enableReinitialize: true, const initialValues = {
initialValues: {
...pageFilter,
basis: 'cash', basis: 'cash',
from_date: moment(pageFilter.from_date).toDate(), ...pageFilter,
to_date: moment(pageFilter.to_date).toDate(), fromDate: moment(pageFilter.fromDate).toDate(),
none_zero: false, toDate: moment(pageFilter.toDate).toDate(),
}, };
validationSchema: Yup.object().shape({
from_date: Yup.date() // Validation schema.
const validationSchema = Yup.object().shape({
dateRange: Yup.string().optional(),
fromDate: Yup.date()
.required() .required()
.label(formatMessage({ id: 'from_data' })), .label(formatMessage({ id: 'fromDate' })),
to_date: Yup.date() toDate: Yup.date()
.min(Yup.ref('from_date')) .min(Yup.ref('fromDate'))
.required() .required()
.label(formatMessage({ id: 'to_date' })), .label(formatMessage({ id: 'toDate' })),
none_zero: Yup.boolean(), accountsFilter: Yup.string(),
}), displayColumnsType: Yup.string(),
onSubmit: (values, actions) => {
onSubmitFilter(values);
actions.setSubmitting(false);
},
}); });
// Handle item select of `display columns by` field. // Handle form submit.
const onItemSelectDisplayColumns = useCallback( const handleSubmit = (values, actions) => {
(item) => { onSubmitFilter(values);
formik.setFieldValue('display_columns_type', item.type); toggleBalanceSheetFilter();
formik.setFieldValue('display_columns_by', item.by); actions.setSubmitting(false);
}, };
[formik],
);
const handleAccountingBasisChange = useCallback( // Handle cancel button click.
(value) => { const handleCancelClick = () => {
formik.setFieldValue('basis', value); toggleBalanceSheetFilter();
}, };
[formik], // Handle drawer close action.
); const handleDrawerClose = () => {
toggleBalanceSheetFilter();
useEffect(() => {
if (refresh) {
formik.submitForm();
refreshBalanceSheet(false);
}
}, [refresh]);
const handleAccountsFilterSelect = (filterType) => {
const noneZero = filterType.key === 'without-zero-balance' ? true : false;
formik.setFieldValue('none_zero', noneZero);
}; };
return ( return (
<FinancialStatementHeader show={show}> <FinancialStatementHeader
<Row> isOpen={balanceSheetFilter}
<FinancialStatementDateRange formik={formik} /> drawerProps={{ onClose: handleDrawerClose }}
<Visible xl>
<Col width={'100%'} />
</Visible>
<Col width={260} offset={10}>
<SelectDisplayColumnsBy onItemSelect={onItemSelectDisplayColumns} />
</Col>
<Col width={260}>
<FormGroup
label={<T id={'filter_accounts'} />}
className="form-group--select-list bp3-fill"
inline={false}
> >
<FinancialAccountsFilter <Formik
initialSelectedItem={'all-accounts'} initialValues={initialValues}
onItemSelect={handleAccountsFilterSelect} validationSchema={validationSchema}
onSubmit={handleSubmit}
>
<Form>
<Tabs animate={true} vertical={true} renderActiveTabPanelOnly={true}>
<Tab
id="general"
title={<T id={'general'} />}
panel={<BalanceSheetHeaderGeneralPanal />}
/> />
</FormGroup> </Tabs>
</Col>
<Col width={260}> <div class="financial-header-drawer__footer">
<RadiosAccountingBasis <Button className={'mr1'} intent={Intent.PRIMARY} type={'submit'}>
selectedValue={formik.values.basis} <T id={'calculate_report'} />
onChange={handleAccountingBasisChange} </Button>
/> <Button onClick={handleCancelClick} minimal={true}>
</Col> <T id={'cancel'} />
</Row> </Button>
</div>
</Form>
</Formik>
</FinancialStatementHeader> </FinancialStatementHeader>
); );
} }
export default compose( export default compose(
withBalanceSheet(({ balanceSheetRefresh }) => ({ withBalanceSheet(({ balanceSheetFilter }) => ({
refresh: balanceSheetRefresh, balanceSheetFilter,
})), })),
withBalanceSheetActions, withBalanceSheetActions,
)(BalanceSheetHeader); )(BalanceSheetHeader);

View File

@@ -0,0 +1,21 @@
import React from 'react';
import FinancialStatementDateRange from 'containers/FinancialStatements/FinancialStatementDateRange';
import SelectDisplayColumnsBy from '../SelectDisplayColumnsBy';
import RadiosAccountingBasis from '../RadiosAccountingBasis';
import FinancialAccountsFilter from '../FinancialAccountsFilter';
/**
* Balance sheet header - General panal.
*/
export default function BalanceSheetHeaderGeneralTab({}) {
return (
<div>
<FinancialStatementDateRange />
<SelectDisplayColumnsBy />
<FinancialAccountsFilter
initialSelectedItem={'all-accounts'}
/>
<RadiosAccountingBasis key={'basis'} />
</div>
);
}

View File

@@ -1,31 +1,56 @@
import React, { useMemo, useCallback } from 'react'; import React, { useMemo, useCallback } from 'react';
import { connect } from 'react-redux';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import classNames from 'classnames'; import classNames from 'classnames';
import Money from 'components/Money'; import Money from 'components/Money';
import FinancialSheet from 'components/FinancialSheet'; import FinancialSheet from 'components/FinancialSheet';
import DataTable from 'components/DataTable'; import DataTable from 'components/DataTable';
import withSettings from 'containers/Settings/withSettings';
import withBalanceSheetDetail from './withBalanceSheetDetail'; import withBalanceSheetDetail from './withBalanceSheetDetail';
import { getFinancialSheetIndexByQuery } from 'store/financialStatement/financialStatements.selectors';
import { compose, defaultExpanderReducer } from 'utils'; import { compose, defaultExpanderReducer, getColumnWidth } from 'utils';
// Total cell.
function TotalCell({ cell }) {
const row = cell.row.original;
if (row.total) {
return (
<Money
amount={row.total.formatted_amount}
currency={row.total.currency_code}
/>
);
}
return '';
}
// Total period cell.
const TotalPeriodCell = (index) => ({ cell }) => {
const { original } = cell.row;
if (original.total_periods && original.total_periods[index]) {
const amount = original.total_periods[index].formatted_amount;
const currencyCode = original.total_periods[index].currency_code;
return <Money amount={amount} currency={currencyCode} />;
}
return '';
};
/**
* Balance sheet table.
*/
function BalanceSheetTable({ function BalanceSheetTable({
// #withPreferences
organizationSettings,
// #withBalanceSheetDetail // #withBalanceSheetDetail
balanceSheetAccounts,
balanceSheetTableRows, balanceSheetTableRows,
balanceSheetColumns, balanceSheetColumns,
balanceSheetQuery, balanceSheetQuery,
balanceSheetLoading, balanceSheetLoading,
// #ownProps // #ownProps
onFetchData, companyName,
}) { }) {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
@@ -33,35 +58,18 @@ function BalanceSheetTable({
() => [ () => [
{ {
Header: formatMessage({ id: 'account_name' }), Header: formatMessage({ id: 'account_name' }),
accessor: 'name', accessor: (row) => (row.code ? `${row.name} - ${row.code}` : row.name),
className: 'account_name', className: 'account_name',
width: 120, width: 240,
},
{
Header: formatMessage({ id: 'code' }),
accessor: 'code',
className: 'code',
width: 60,
}, },
...(balanceSheetQuery.display_columns_type === 'total' ...(balanceSheetQuery.display_columns_type === 'total'
? [ ? [
{ {
Header: formatMessage({ id: 'total' }), Header: formatMessage({ id: 'total' }),
accessor: 'balance.formatted_amount', accessor: 'balance.formatted_amount',
Cell: ({ cell }) => { Cell: TotalCell,
const row = cell.row.original;
if (row.total) {
return (
<Money
amount={row.total.formatted_amount}
currency={'USD'}
/>
);
}
return '';
},
className: 'total', className: 'total',
width: 80, width: 140,
}, },
] ]
: []), : []),
@@ -70,44 +78,43 @@ function BalanceSheetTable({
id: `date_period_${index}`, id: `date_period_${index}`,
Header: column, Header: column,
accessor: `total_periods[${index}]`, accessor: `total_periods[${index}]`,
Cell: ({ cell }) => { Cell: TotalPeriodCell(index),
const { original } = cell.row;
if (original.total_periods && original.total_periods[index]) {
const amount = original.total_periods[index].formatted_amount;
return <Money amount={amount} currency={'USD'} />;
}
return '';
},
className: classNames('total-period', `total-periods-${index}`), className: classNames('total-period', `total-periods-${index}`),
width: 80, width: getColumnWidth(
balanceSheetTableRows,
`total_periods.${index}.formatted_amount`,
{ minWidth: 100 },
),
})) }))
: []), : []),
], ],
[balanceSheetQuery, balanceSheetColumns, formatMessage], [balanceSheetQuery, balanceSheetColumns, balanceSheetTableRows, formatMessage],
); );
const handleFetchData = useCallback(() => {
onFetchData && onFetchData();
}, [onFetchData]);
// Calculates the default expanded rows of balance sheet table. // Calculates the default expanded rows of balance sheet table.
const expandedRows = useMemo( const expandedRows = useMemo(
() => defaultExpanderReducer(balanceSheetTableRows, 3), () => defaultExpanderReducer(balanceSheetTableRows, 4),
[balanceSheetTableRows], [balanceSheetTableRows],
); );
const rowClassNames = (row) => { const rowClassNames = useCallback((row) => {
const { original } = row; const { original } = row;
console.log(row); const rowTypes = Array.isArray(original.row_types)
? original.row_types
: [];
return { return {
[`row_type--${original.row_type}`]: original.row_type, ...rowTypes.reduce((acc, rowType) => {
}; acc[`row_type--${rowType}`] = rowType;
return acc;
}, {}),
}; };
}, []);
return ( return (
<FinancialSheet <FinancialSheet
name="balance-sheet" name="balance-sheet"
companyName={organizationSettings.name} companyName={companyName}
sheetType={formatMessage({ id: 'balance_sheet' })} sheetType={formatMessage({ id: 'balance_sheet' })}
fromDate={balanceSheetQuery.from_date} fromDate={balanceSheetQuery.from_date}
toDate={balanceSheetQuery.to_date} toDate={balanceSheetQuery.to_date}
@@ -119,46 +126,29 @@ function BalanceSheetTable({
columns={columns} columns={columns}
data={balanceSheetTableRows} data={balanceSheetTableRows}
rowClassNames={rowClassNames} rowClassNames={rowClassNames}
onFetchData={handleFetchData}
noInitialFetch={true} noInitialFetch={true}
expanded={expandedRows}
expandable={true} expandable={true}
expanded={expandedRows}
expandToggleColumn={1} expandToggleColumn={1}
sticky={true}
expandColumnSpace={0.8} expandColumnSpace={0.8}
sticky={true}
/> />
</FinancialSheet> </FinancialSheet>
); );
} }
const mapStateToProps = (state, props) => {
const { balanceSheetQuery } = props;
return {
balanceSheetIndex: getFinancialSheetIndexByQuery(
state.financialStatements.balanceSheet.sheets,
balanceSheetQuery,
),
};
};
const withBalanceSheetTable = connect(mapStateToProps);
export default compose( export default compose(
withBalanceSheetTable,
withBalanceSheetDetail( withBalanceSheetDetail(
({ ({
balanceSheetAccounts,
balanceSheetTableRows, balanceSheetTableRows,
balanceSheetColumns, balanceSheetColumns,
balanceSheetQuery, balanceSheetQuery,
balanceSheetLoading, balanceSheetLoading,
}) => ({ }) => ({
balanceSheetAccounts,
balanceSheetTableRows, balanceSheetTableRows,
balanceSheetColumns, balanceSheetColumns,
balanceSheetQuery, balanceSheetQuery,
balanceSheetLoading, balanceSheetLoading,
}), }),
), ),
withSettings,
)(BalanceSheetTable); )(BalanceSheetTable);

View File

@@ -1,22 +1,30 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { import {
getFinancialSheet, getFinancialSheetFactory,
getFinancialSheetAccounts, getFinancialSheetAccountsFactory,
getFinancialSheetColumns, getFinancialSheetColumnsFactory,
getFinancialSheetQuery, getFinancialSheetQueryFactory,
getFinancialSheetTableRows, getFinancialSheetTableRowsFactory,
} from 'store/financialStatement/financialStatements.selectors'; } from 'store/financialStatement/financialStatements.selectors';
export default (mapState) => { export default (mapState) => {
const mapStateToProps = (state, props) => { const mapStateToProps = (state, props) => {
const { balanceSheetIndex } = props; const getBalanceSheet = getFinancialSheetFactory('balanceSheet');
const getBalanceSheetAccounts = getFinancialSheetAccountsFactory(
'balanceSheet',
);
const getBalanceSheetTableRows = getFinancialSheetTableRowsFactory(
'balanceSheet',
);
const getBalanceSheetColumns = getFinancialSheetColumnsFactory('balanceSheet');
const getBalanceSheetQuery = getFinancialSheetQueryFactory('balanceSheet');
const mapped = { const mapped = {
balanceSheet: getFinancialSheet(state.financialStatements.balanceSheet.sheets, balanceSheetIndex), balanceSheet: getBalanceSheet(state, props),
balanceSheetAccounts: getFinancialSheetAccounts(state.financialStatements.balanceSheet.sheets, balanceSheetIndex), balanceSheetAccounts: getBalanceSheetAccounts(state, props),
balanceSheetTableRows: getFinancialSheetTableRows(state.financialStatements.balanceSheet.sheets, balanceSheetIndex), balanceSheetTableRows: getBalanceSheetTableRows(state, props),
balanceSheetColumns: getFinancialSheetColumns(state.financialStatements.balanceSheet.sheets, balanceSheetIndex), balanceSheetColumns: getBalanceSheetColumns(state, props),
balanceSheetQuery: getFinancialSheetQuery(state.financialStatements.balanceSheet.sheets, balanceSheetIndex), balanceSheetQuery: getBalanceSheetQuery(state, props),
balanceSheetLoading: state.financialStatements.balanceSheet.loading, balanceSheetLoading: state.financialStatements.balanceSheet.loading,
balanceSheetFilter: state.financialStatements.balanceSheet.filter, balanceSheetFilter: state.financialStatements.balanceSheet.filter,
balanceSheetRefresh: state.financialStatements.balanceSheet.refresh, balanceSheetRefresh: state.financialStatements.balanceSheet.refresh,
@@ -25,4 +33,4 @@ export default (mapState) => {
}; };
return connect(mapStateToProps); return connect(mapStateToProps);
} };

View File

@@ -1,46 +1,27 @@
import React, { useMemo, useCallback } from 'react'; import React from 'react';
import { import {
PopoverInteractionKind, PopoverInteractionKind,
Tooltip, Tooltip,
MenuItem, MenuItem,
Position, Position,
FormGroup,
} from '@blueprintjs/core'; } from '@blueprintjs/core';
import { useIntl } from 'react-intl'; import { FormattedMessage as T } from 'react-intl';
import { ListSelect, MODIFIER } from 'components'; import classNames from 'classnames';
import { FastField } from 'formik';
export default function FinancialAccountsFilter({ import { CLASSES } from 'common/classes';
...restProps import { Col, Row, ListSelect, MODIFIER } from 'components';
}) { import { filterAccountsOptions } from './common';
const { formatMessage } = useIntl();
const filterAccountsOptions = useMemo( export default function FinancialAccountsFilter({ ...restProps }) {
() => [
{
key: 'all-accounts',
name: formatMessage({ id: 'all_accounts' }),
hint: formatMessage({ id: 'all_accounts_including_with_zero_balance' }),
},
{
key: 'without-zero-balance',
name: formatMessage({ id: 'accounts_without_zero_balance' }),
hint: formatMessage({ id: 'include_accounts_and_exclude_zero_balance' }),
},
{
key: 'with-transactions',
name: formatMessage({ id: 'accounts_with_transactions' }),
hint: formatMessage({ id: 'include_accounts_once_has_transactions_on_given_date_period' }),
},
],
[formatMessage],
);
const SUBMENU_POPOVER_MODIFIERS = { const SUBMENU_POPOVER_MODIFIERS = {
flip: { boundariesElement: 'viewport', padding: 20 }, flip: { boundariesElement: 'viewport', padding: 20 },
offset: { offset: '0, 10' }, offset: { offset: '0, 10' },
preventOverflow: { boundariesElement: 'viewport', padding: 40 }, preventOverflow: { boundariesElement: 'viewport', padding: 40 },
}; };
const filterAccountRenderer = useCallback( const filterAccountRenderer = (item, { handleClick, modifiers, query }) => {
(item, { handleClick, modifiers, query }) => {
return ( return (
<Tooltip <Tooltip
interactionKind={PopoverInteractionKind.HOVER} interactionKind={PopoverInteractionKind.HOVER}
@@ -54,20 +35,36 @@ export default function FinancialAccountsFilter({
<MenuItem text={item.name} key={item.key} onClick={handleClick} /> <MenuItem text={item.name} key={item.key} onClick={handleClick} />
</Tooltip> </Tooltip>
); );
}, };
[],
);
return ( return (
<Row>
<Col xs={4}>
<FastField name={'accountsFilter'}>
{({ form: { setFieldValue }, field: { value } }) => (
<FormGroup
label={<T id={'filter_accounts'} />}
className="form-group--select-list bp3-fill"
inline={false}
>
<ListSelect <ListSelect
items={filterAccountsOptions} items={filterAccountsOptions}
itemRenderer={filterAccountRenderer} itemRenderer={filterAccountRenderer}
popoverProps={{ minimal: true, }} popoverProps={{ minimal: true }}
filterable={false} filterable={false}
selectedItem={value}
selectedItemProp={'key'} selectedItemProp={'key'}
labelProp={'name'} labelProp={'name'}
// className={} onItemSelect={(item) => {
setFieldValue('accountsFilter', item.key);
}}
className={classNames(CLASSES.SELECT_LIST_FILL_POPOVER)}
{...restProps} {...restProps}
/> />
</FormGroup>
)}
</FastField>
</Col>
</Row>
); );
} }

View File

@@ -1,57 +1,30 @@
import React, { useState, useCallback, useMemo } from 'react'; import React from 'react';
import { Row, Col } from 'react-grid-system'; import { FastField, ErrorMessage } from 'formik';
import { momentFormatter } from 'utils'; import { HTMLSelect, FormGroup, Intent, Position } from '@blueprintjs/core';
import moment from 'moment';
import { Row, Col, Hint } from 'components';
import { momentFormatter, parseDateRangeQuery } from 'utils';
import { DateInput } from '@blueprintjs/datetime'; import { DateInput } from '@blueprintjs/datetime';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { HTMLSelect, FormGroup, Intent, Position } from '@blueprintjs/core'; import { dateRangeOptions } from 'containers/FinancialStatements/common';
import { Hint } from 'components';
import { parseDateRangeQuery } from 'utils';
export default function FinancialStatementDateRange({ formik }) { /**
const intl = useIntl(); * Financial statement - Date range select.
const [reportDateRange, setReportDateRange] = useState('this_year'); */
export default function FinancialStatementDateRange() {
const dateRangeOptions = useMemo( const { formatMessage } = useIntl();
() => [
{ value: 'today', label: 'Today' },
{ value: 'this_week', label: 'This Week' },
{ value: 'this_month', label: 'This Month' },
{ value: 'this_quarter', label: 'This Quarter' },
{ value: 'this_year', label: 'This Year' },
{ value: 'custom', label: 'Custom Range' },
],
[],
);
const handleDateChange = useCallback(
(name) => (date) => {
setReportDateRange('custom');
formik.setFieldValue(name, date);
},
[setReportDateRange, formik],
);
// Handles date range field change.
const handleDateRangeChange = useCallback(
(e) => {
const value = e.target.value;
if (value !== 'custom') {
const dateRange = parseDateRangeQuery(value);
if (dateRange) {
formik.setFieldValue('from_date', dateRange.from_date);
formik.setFieldValue('to_date', dateRange.to_date);
}
}
setReportDateRange(value);
},
[formik],
);
return ( return (
<> <>
<Col width={260}> <Row>
<Col xs={4}>
<FastField name={'date_range'}>
{({
form: { setFieldValue },
field: { value },
}) => (
<FormGroup <FormGroup
label={intl.formatMessage({ id: 'report_date_range' })} label={formatMessage({ id: 'report_date_range' })}
labelInfo={<Hint />} labelInfo={<Hint />}
minimal={true} minimal={true}
fill={true} fill={true}
@@ -59,48 +32,89 @@ export default function FinancialStatementDateRange({ formik }) {
<HTMLSelect <HTMLSelect
fill={true} fill={true}
options={dateRangeOptions} options={dateRangeOptions}
value={reportDateRange} value={value}
onChange={handleDateRangeChange} onChange={(e) => {
const newValue = e.target.value;
if (newValue !== 'custom') {
const dateRange = parseDateRangeQuery(newValue);
if (dateRange) {
setFieldValue('fromDate', moment(dateRange.fromDate).toDate());
setFieldValue('toDate', moment(dateRange.toDate).toDate());
}
}
setFieldValue('dateRange', newValue);
}}
/> />
</FormGroup> </FormGroup>
)}
</FastField>
</Col> </Col>
</Row>
<Col width={260}> <Row>
<Col xs={4}>
<FastField name={'fromDate'}>
{({
form: { setFieldValue },
field: { value },
meta: { error, touched },
}) => (
<FormGroup <FormGroup
label={intl.formatMessage({ id: 'from_date' })} label={formatMessage({ id: 'from_date' })}
labelInfo={<Hint />} labelInfo={<Hint />}
fill={true} fill={true}
intent={formik.errors.from_date && Intent.DANGER} intent={error && Intent.DANGER}
helperText={<ErrorMessage name={'fromDate'} />}
> >
<DateInput <DateInput
{...momentFormatter('YYYY/MM/DD')} {...momentFormatter('YYYY-MM-DD')}
value={formik.values.from_date} value={value}
onChange={handleDateChange('from_date')} onChange={(selectedDate) => {
popoverProps={{ position: Position.BOTTOM }} setFieldValue('fromDate', selectedDate);
}}
popoverProps={{ minimal: true, position: Position.BOTTOM }}
canClearSelection={false}
minimal={true} minimal={true}
fill={true} fill={true}
/> />
</FormGroup> </FormGroup>
)}
</FastField>
</Col> </Col>
<Col width={260}> <Col xs={4}>
<FastField name={'toDate'}>
{({
form: { setFieldValue },
field: { value },
meta: { error },
}) => (
<FormGroup <FormGroup
label={intl.formatMessage({ id: 'to_date' })} label={formatMessage({ id: 'to_date' })}
labelInfo={<Hint />} labelInfo={<Hint />}
fill={true} fill={true}
intent={formik.errors.to_date && Intent.DANGER} intent={error && Intent.DANGER}
helperText={<ErrorMessage name={'toDate'} />}
> >
<DateInput <DateInput
{...momentFormatter('YYYY/MM/DD')} {...momentFormatter('YYYY-MM-DD')}
value={formik.values.to_date} value={value}
onChange={handleDateChange('to_date')} onChange={(selectedDate) => {
popoverProps={{ position: Position.BOTTOM }} setFieldValue('toDate', selectedDate);
}}
popoverProps={{ minimal: true, position: Position.BOTTOM }}
canClearSelection={false}
fill={true} fill={true}
minimal={true} minimal={true}
intent={formik.errors.to_date && Intent.DANGER} intent={error && Intent.DANGER}
/> />
</FormGroup> </FormGroup>
)}
</FastField>
</Col> </Col>
</Row>
</> </>
); );
} }

View File

@@ -1,14 +1,59 @@
import React from 'react'; import React, { useEffect, useState } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { Position, Drawer } from '@blueprintjs/core';
export default function FinancialStatementHeader({
children,
isOpen,
drawerProps,
}) {
const timeoutRef = React.useRef();
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
// Hides the content scrollbar and scroll to the top of the page once the drawer open.
useEffect(() => {
const contentPanel = document.querySelector('body');
contentPanel.classList.toggle('hide-scrollbar', isOpen);
if (isOpen) {
document.querySelector('.Pane2').scrollTo(0, 0);
}
return () => {
contentPanel.classList.remove('hide-scrollbar');
};
}, [isOpen]);
useEffect(() => {
clearTimeout(timeoutRef.current);
if (isOpen) {
setIsDrawerOpen(isOpen);
} else {
timeoutRef.current = setTimeout(() => setIsDrawerOpen(isOpen), 300);
}
}, [isOpen]);
export default function FinancialStatementHeader({ show, children }) {
return ( return (
<div <div
className={classNames('financial-statement__header', { className={classNames(
'is-hidden': !show, 'financial-statement__header',
})} 'financial-header-drawer',
{
'is-hidden': !isDrawerOpen,
},
)}
>
<Drawer
isOpen={isOpen}
usePortal={false}
hasBackdrop={true}
position={Position.TOP}
canOutsideClickClose={true}
canEscapeKeyClose={true}
{...drawerProps}
> >
{children} {children}
</Drawer>
</div> </div>
); );
} }

View File

@@ -1,14 +1,13 @@
import React, { useEffect, useCallback, useState } from 'react'; import React, { useEffect, useCallback, useState } from 'react';
import moment from 'moment'; import moment from 'moment';
import GeneralLedgerTable from 'containers/FinancialStatements/GeneralLedger/GeneralLedgerTable';
import { useQuery } from 'react-query'; import { useQuery } from 'react-query';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { queryCache } from 'react-query';
import GeneralLedgerTable from 'containers/FinancialStatements/GeneralLedger/GeneralLedgerTable';
import GeneralLedgerHeader from './GeneralLedgerHeader'; import GeneralLedgerHeader from './GeneralLedgerHeader';
import { compose } from 'utils'; import DashboardInsider from 'components/Dashboard/DashboardInsider';
import DashboardInsider from 'components/Dashboard/DashboardInsider'
import DashboardPageContent from 'components/Dashboard/DashboardPageContent'; import DashboardPageContent from 'components/Dashboard/DashboardPageContent';
import GeneralLedgerActionsBar from './GeneralLedgerActionsBar'; import GeneralLedgerActionsBar from './GeneralLedgerActionsBar';
@@ -17,26 +16,37 @@ import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withAccountsActions from 'containers/Accounts/withAccountsActions'; import withAccountsActions from 'containers/Accounts/withAccountsActions';
import withSettings from 'containers/Settings/withSettings'; import withSettings from 'containers/Settings/withSettings';
import { compose } from 'utils';
import { transformFilterFormToQuery } from 'containers/FinancialStatements/common';
import withGeneralLedger from './withGeneralLedger';
/**
* General Ledger (GL) sheet.
*/
function GeneralLedger({ function GeneralLedger({
// #withDashboardActions // #withDashboardActions
changePageTitle, changePageTitle,
setDashboardBackLink,
// #withGeneralLedgerActions // #withGeneralLedgerActions
fetchGeneralLedger, fetchGeneralLedger,
refreshGeneralLedgerSheet,
// #withAccountsActions // #withAccountsActions
requestFetchAccounts, requestFetchAccounts,
// #withGeneralLedger
generalLedgerSheetRefresh,
// #withSettings // #withSettings
organizationSettings, organizationName,
}) { }) {
const { formatMessage } = useIntl() const { formatMessage } = useIntl();
const [filter, setFilter] = useState({ const [filter, setFilter] = useState({
from_date: moment().startOf('year').format('YYYY-MM-DD'), fromDate: moment().startOf('year').format('YYYY-MM-DD'),
to_date: moment().endOf('year').format('YYYY-MM-DD'), toDate: moment().endOf('year').format('YYYY-MM-DD'),
basis: 'accural', basis: 'accural',
none_zero: true,
}); });
// Change page title of the dashboard. // Change page title of the dashboard.
@@ -44,46 +54,63 @@ function GeneralLedger({
changePageTitle(formatMessage({ id: 'general_ledger' })); changePageTitle(formatMessage({ id: 'general_ledger' }));
}, [changePageTitle, formatMessage]); }, [changePageTitle, formatMessage]);
const fetchAccounts = useQuery(['accounts-list'], useEffect(() => {
() => requestFetchAccounts()); // Show the back link on dashboard topbar.
setDashboardBackLink(true);
const fetchSheet = useQuery(['general-ledger', filter], return () => {
(key, query) => fetchGeneralLedger(query), // Hide the back link on dashboard topbar.
{ manual: true }); setDashboardBackLink(false);
};
});
// Handle fetch data of trial balance table. // Observes the GL sheet refresh to invalid the query to refresh it.
const handleFetchData = useCallback(() => { useEffect(() => {
fetchSheet.refetch({ force: true }); if (generalLedgerSheetRefresh) {
}, []); queryCache.invalidateQueries('general-ledger');
refreshGeneralLedgerSheet(false);
}
}, [generalLedgerSheetRefresh, refreshGeneralLedgerSheet]);
// Fetches accounts list.
const fetchAccounts = useQuery(['accounts-list'], () =>
requestFetchAccounts(),
);
// Fetches the general ledger sheet.
const fetchSheet = useQuery(['general-ledger', filter], (key, q) =>
fetchGeneralLedger({ ...transformFilterFormToQuery(q) }),
);
// Handle financial statement filter change. // Handle financial statement filter change.
const handleFilterSubmit = useCallback((filter) => { const handleFilterSubmit = useCallback(
(filter) => {
const parsedFilter = { const parsedFilter = {
...filter, ...filter,
from_date: moment(filter.from_date).format('YYYY-MM-DD'), fromDate: moment(filter.fromDate).format('YYYY-MM-DD'),
to_date: moment(filter.to_date).format('YYYY-MM-DD'), toDate: moment(filter.toDate).format('YYYY-MM-DD'),
}; };
setFilter(parsedFilter); setFilter(parsedFilter);
}, [setFilter]); refreshGeneralLedgerSheet(true);
},
const handleFilterChanged = () => { }; [setFilter, refreshGeneralLedgerSheet],
);
return ( return (
<DashboardInsider> <DashboardInsider>
<GeneralLedgerActionsBar <GeneralLedgerActionsBar />
onFilterChanged={handleFilterChanged} />
<DashboardPageContent> <DashboardPageContent>
<div class="financial-statement financial-statement--general-ledger"> <div class="financial-statement financial-statement--general-ledger">
<GeneralLedgerHeader <GeneralLedgerHeader
pageFilter={filter} pageFilter={filter}
onSubmitFilter={handleFilterSubmit} /> onSubmitFilter={handleFilterSubmit}
/>
<div class="financial-statement__body"> <div class="financial-statement__body">
<GeneralLedgerTable <GeneralLedgerTable
companyName={organizationSettings.name} companyName={organizationName}
generalLedgerQuery={filter} generalLedgerQuery={filter}
onFetchData={handleFetchData} /> />
</div> </div>
</div> </div>
</DashboardPageContent> </DashboardPageContent>
@@ -95,5 +122,10 @@ export default compose(
withGeneralLedgerActions, withGeneralLedgerActions,
withDashboardActions, withDashboardActions,
withAccountsActions, withAccountsActions,
withSettings, withGeneralLedger(({ generalLedgerSheetRefresh }) => ({
generalLedgerSheetRefresh,
})),
withSettings(({ organizationSettings }) => ({
organizationName: organizationSettings.name,
})),
)(GeneralLedger); )(GeneralLedger);

View File

@@ -9,11 +9,10 @@ import {
Position, Position,
} from '@blueprintjs/core'; } from '@blueprintjs/core';
import { FormattedMessage as T } from 'react-intl'; import { FormattedMessage as T } from 'react-intl';
import Icon from 'components/Icon';
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar'
import { If } from 'components';
import classNames from 'classnames'; import classNames from 'classnames';
import FilterDropdown from 'components/FilterDropdown';
import Icon from 'components/Icon';
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
import withGeneralLedger from './withGeneralLedger'; import withGeneralLedger from './withGeneralLedger';
import withGeneralLedgerActions from './withGeneralLedgerActions'; import withGeneralLedgerActions from './withGeneralLedgerActions';
@@ -21,7 +20,7 @@ import withGeneralLedgerActions from './withGeneralLedgerActions';
import { compose } from 'utils'; import { compose } from 'utils';
/** /**
* General ledger actions bar. * General ledger - Actions bar.
*/ */
function GeneralLedgerActionsBar({ function GeneralLedgerActionsBar({
// #withGeneralLedger // #withGeneralLedger
@@ -29,12 +28,13 @@ function GeneralLedgerActionsBar({
// #withGeneralLedgerActions // #withGeneralLedgerActions
toggleGeneralLedgerSheetFilter, toggleGeneralLedgerSheetFilter,
refreshGeneralLedgerSheet refreshGeneralLedgerSheet,
}) { }) {
const handleFilterClick = () => { // Handle customize button click.
const handleCustomizeClick = () => {
toggleGeneralLedgerSheetFilter(); toggleGeneralLedgerSheetFilter();
}; };
// Handle re-calculate button click.
const handleRecalcReport = () => { const handleRecalcReport = () => {
refreshGeneralLedgerSheet(true); refreshGeneralLedgerSheet(true);
}; };
@@ -43,61 +43,49 @@ function GeneralLedgerActionsBar({
<DashboardActionsBar> <DashboardActionsBar>
<NavbarGroup> <NavbarGroup>
<Button <Button
className={classNames(Classes.MINIMAL, 'button--table-views')} className={classNames(Classes.MINIMAL, 'button--gray-highlight')}
icon={<Icon icon='cog-16' iconSize={16} />} text={<T id={'recalc_report'} />}
text={<T id={'customize_report'}/>}
/>
<NavbarDivider />
<Button
className={classNames(
Classes.MINIMAL,
'button--gray-highlight',
)}
text={'Re-calc Report'}
onClick={handleRecalcReport} onClick={handleRecalcReport}
icon={<Icon icon="refresh-16" iconSize={16} />} icon={<Icon icon="refresh-16" iconSize={16} />}
/> />
<NavbarDivider />
<If condition={generalLedgerSheetFilter}>
<Button <Button
className={Classes.MINIMAL} className={classNames(Classes.MINIMAL, 'button--table-views')}
text={<T id={'hide_filter'} />} icon={<Icon icon="cog-16" iconSize={16} />}
icon={<Icon icon="arrow-to-top" />} text={
onClick={handleFilterClick} generalLedgerSheetFilter ? (
<T id={'hide_customizer'} />
) : (
<T id={'customize_report'} />
)
}
onClick={handleCustomizeClick}
active={generalLedgerSheetFilter}
/> />
</If> <NavbarDivider />
<If condition={!generalLedgerSheetFilter}>
<Button
className={Classes.MINIMAL}
text={<T id={'show_filter'} />}
icon={<Icon icon="arrow-to-bottom" />}
onClick={handleFilterClick}
/>
</If>
<Popover <Popover
interactionKind={PopoverInteractionKind.CLICK} interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}> position={Position.BOTTOM_LEFT}
>
<Button <Button
className={classNames(Classes.MINIMAL, 'button--filter')} className={classNames(Classes.MINIMAL, 'button--filter')}
text={<T id={'filter'} />} text={<T id={'filter'} />}
icon={<Icon icon="filter-16" iconSize={16} /> } /> icon={<Icon icon="filter-16" iconSize={16} />}
/>
</Popover> </Popover>
<NavbarDivider /> <NavbarDivider />
<Button <Button
className={Classes.MINIMAL} className={Classes.MINIMAL}
icon={<Icon icon='print-16' iconSize={16} />} icon={<Icon icon="print-16" iconSize={16} />}
text={<T id={'print'} />} text={<T id={'print'} />}
/> />
<Button <Button
className={Classes.MINIMAL} className={Classes.MINIMAL}
icon={<Icon icon='file-export-16' iconSize={16} />} icon={<Icon icon="file-export-16" iconSize={16} />}
text={<T id={'export'} />} text={<T id={'export'} />}
/> />
</NavbarGroup> </NavbarGroup>
@@ -106,6 +94,8 @@ function GeneralLedgerActionsBar({
} }
export default compose( export default compose(
withGeneralLedger(({ generalLedgerSheetFilter }) => ({ generalLedgerSheetFilter })), withGeneralLedger(({ generalLedgerSheetFilter }) => ({
generalLedgerSheetFilter,
})),
withGeneralLedgerActions, withGeneralLedgerActions,
)(GeneralLedgerActionsBar); )(GeneralLedgerActionsBar);

View File

@@ -1,113 +1,100 @@
import React, { useEffect, useCallback } from 'react'; import React from 'react';
import { Button, FormGroup, Classes } from '@blueprintjs/core';
import { Row, Col, Visible } from 'react-grid-system';
import moment from 'moment'; import moment from 'moment';
import * as Yup from 'yup'; import * as Yup from 'yup';
import { useFormik } from 'formik'; import { Formik, Form } from 'formik';
import { Tabs, Tab, Button, Intent } from '@blueprintjs/core';
import { FormattedMessage as T } from 'react-intl'; import { FormattedMessage as T } from 'react-intl';
import AccountsMultiSelect from 'components/AccountsMultiSelect';
import FinancialStatementHeader from 'containers/FinancialStatements/FinancialStatementHeader'; import FinancialStatementHeader from 'containers/FinancialStatements/FinancialStatementHeader';
import withAccounts from 'containers/Accounts/withAccounts'; import GeneralLedgerHeaderGeneralPane from './GeneralLedgerHeaderGeneralPane';
import classNames from 'classnames';
import FinancialStatementDateRange from 'containers/FinancialStatements/FinancialStatementDateRange';
import RadiosAccountingBasis from '../RadiosAccountingBasis';
import withGeneralLedger from './withGeneralLedger'; import withGeneralLedger from './withGeneralLedger';
import withGeneralLedgerActions from './withGeneralLedgerActions'; import withGeneralLedgerActions from './withGeneralLedgerActions';
import { compose } from 'utils'; import { compose } from 'utils';
/**
* Geenral Ledger (GL) - Header.
*/
function GeneralLedgerHeader({ function GeneralLedgerHeader({
// #ownProps
onSubmitFilter, onSubmitFilter,
pageFilter, pageFilter,
// #withAccounts
accountsList,
// #withGeneralLedgerActions // #withGeneralLedgerActions
refreshGeneralLedgerSheet, toggleGeneralLedgerSheetFilter,
// #withGeneralLedger // #withGeneralLedger
generalLedgerSheetFilter, generalLedgerSheetFilter,
generalLedgerSheetRefresh
}) { }) {
const formik = useFormik({ // Initial values.
enableReinitialize: true, const initialValues = {
initialValues: {
...pageFilter, ...pageFilter,
from_date: moment(pageFilter.from_date).toDate(), fromDate: moment(pageFilter.fromDate).toDate(),
to_date: moment(pageFilter.to_date).toDate(), toDate: moment(pageFilter.toDate).toDate(),
}, };
validationSchema: Yup.object().shape({
from_date: Yup.date().required(), // Validation schema.
to_date: Yup.date().min(Yup.ref('from_date')).required(), const validationSchema = Yup.object().shape({
}), dateRange: Yup.string().optional(),
onSubmit(values, actions) { fromDate: Yup.date().required(),
onSubmitFilter(values); toDate: Yup.date().min(Yup.ref('fromDate')).required(),
actions.setSubmitting(false);
},
}); });
const onAccountSelected = useCallback((selectedAccounts) => { // Handle form submit.
formik.setFieldValue('accounts_ids', Object.keys(selectedAccounts)); const handleSubmit = (values, { setSubmitting }) => {
}, [formik.setFieldValue]); onSubmitFilter(values);
toggleGeneralLedgerSheetFilter();
setSubmitting(false);
};
const handleAccountingBasisChange = useCallback( // Handle cancel button click.
(value) => { const handleCancelClick = () => {
formik.setFieldValue('basis', value); toggleGeneralLedgerSheetFilter(false);
}, };
[formik],
);
// handle submit filter submit button. // Handle drawer close action.
useEffect(() => { const handleDrawerClose = () => {
if (generalLedgerSheetRefresh) { toggleGeneralLedgerSheetFilter(false);
formik.submitForm(); };
refreshGeneralLedgerSheet(false);
}
}, [formik, generalLedgerSheetRefresh])
return ( return (
<FinancialStatementHeader show={generalLedgerSheetFilter}> <FinancialStatementHeader
<Row> isOpen={generalLedgerSheetFilter}
<FinancialStatementDateRange formik={formik} /> drawerProps={{ onClose: handleDrawerClose }}
<Visible xl><Col width={'100%'} /></Visible>
<Col width={260}>
<FormGroup
label={<T id={'specific_accounts'} />}
className={classNames('form-group--select-list', Classes.FILL)}
> >
<AccountsMultiSelect <Formik
accounts={accountsList} validationSchema={validationSchema}
onAccountSelected={onAccountSelected} initialValues={initialValues}
onSubmit={handleSubmit}
>
<Form>
<Tabs animate={true} vertical={true} renderActiveTabPanelOnly={true}>
<Tab
id="general"
title={<T id={'general'} />}
panel={<GeneralLedgerHeaderGeneralPane />}
/> />
</FormGroup> </Tabs>
</Col>
<Col width={260}> <div class="financial-header-drawer__footer">
<RadiosAccountingBasis <Button className={'mr1'} intent={Intent.PRIMARY} type={'submit'}>
onChange={handleAccountingBasisChange} <T id={'calculate_report'} />
selectedValue={formik.values.basis} </Button>
/>
</Col>
</Row> <Button onClick={handleCancelClick} minimal={true}>
<T id={'cancel'} />
</Button>
</div>
</Form>
</Formik>
</FinancialStatementHeader> </FinancialStatementHeader>
); );
} }
export default compose( export default compose(
withAccounts(({ accountsList }) => ({ withGeneralLedger(({ generalLedgerSheetFilter }) => ({
accountsList,
})),
withGeneralLedger(({ generalLedgerSheetFilter, generalLedgerSheetRefresh }) => ({
generalLedgerSheetFilter, generalLedgerSheetFilter,
generalLedgerSheetRefresh,
})), })),
withGeneralLedgerActions, withGeneralLedgerActions,
)(GeneralLedgerHeader); )(GeneralLedgerHeader);

View File

@@ -0,0 +1,44 @@
import React from 'react';
import { FormGroup, Classes } from '@blueprintjs/core';
import { FormattedMessage as T } from 'react-intl';
import classNames from 'classnames';
import { AccountsMultiSelect, Row, Col } from 'components';
import FinancialStatementDateRange from 'containers/FinancialStatements/FinancialStatementDateRange';
import RadiosAccountingBasis from '../RadiosAccountingBasis';
import withAccounts from 'containers/Accounts/withAccounts';
import { compose } from 'redux';
/**
* General ledger (GL) - Header - General panel.
*/
function GeneralLedgerHeaderGeneralPane({
// #withAccounts
accountsList,
}) {
return (
<div>
<FinancialStatementDateRange />
<Row>
<Col xs={4}>
<FormGroup
label={<T id={'specific_accounts'} />}
className={classNames('form-group--select-list', Classes.FILL)}
>
<AccountsMultiSelect accounts={accountsList} />
</FormGroup>
</Col>
</Row>
<RadiosAccountingBasis key={'basis'} />
</div>
);
}
export default compose(withAccounts(({ accountsList }) => ({ accountsList })))(
GeneralLedgerHeaderGeneralPane,
);

View File

@@ -1,6 +1,5 @@
import React, { useCallback, useMemo } from 'react'; import React, { useCallback, useMemo } from 'react';
import moment from 'moment'; import moment from 'moment';
import { connect } from 'react-redux';
import { defaultExpanderReducer, compose } from 'utils'; import { defaultExpanderReducer, compose } from 'utils';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
@@ -8,7 +7,6 @@ import FinancialSheet from 'components/FinancialSheet';
import DataTable from 'components/DataTable'; import DataTable from 'components/DataTable';
import Money from 'components/Money'; import Money from 'components/Money';
import { getFinancialSheetIndexByQuery } from 'store/financialStatement/financialStatements.selectors';
import withGeneralLedger from './withGeneralLedger'; import withGeneralLedger from './withGeneralLedger';
const ROW_TYPE = { const ROW_TYPE = {
@@ -20,7 +18,6 @@ const ROW_TYPE = {
function GeneralLedgerTable({ function GeneralLedgerTable({
companyName, companyName,
onFetchData,
generalLedgerSheetLoading, generalLedgerSheetLoading,
generalLedgerTableRows, generalLedgerTableRows,
@@ -29,8 +26,7 @@ function GeneralLedgerTable({
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
// Account name column accessor. // Account name column accessor.
const accountNameAccessor = useCallback( const accountNameAccessor = (row) => {
(row) => {
switch (row.rowType) { switch (row.rowType) {
case ROW_TYPE.OPENING_BALANCE: case ROW_TYPE.OPENING_BALANCE:
return 'Opening Balance'; return 'Opening Balance';
@@ -39,13 +35,10 @@ function GeneralLedgerTable({
default: default:
return row.name; return row.name;
} }
}, };
[ROW_TYPE],
);
// Date accessor. // Date accessor.
const dateAccessor = useCallback( const dateAccessor = (row) => {
(row) => {
const TYPES = [ const TYPES = [
ROW_TYPE.OPENING_BALANCE, ROW_TYPE.OPENING_BALANCE,
ROW_TYPE.CLOSING_BALANCE, ROW_TYPE.CLOSING_BALANCE,
@@ -55,9 +48,7 @@ function GeneralLedgerTable({
return TYPES.indexOf(row.rowType) !== -1 return TYPES.indexOf(row.rowType) !== -1
? moment(row.date).format('DD MMM YYYY') ? moment(row.date).format('DD MMM YYYY')
: ''; : '';
}, };
[moment, ROW_TYPE],
);
// Amount cell // Amount cell
const amountCell = useCallback(({ cell }) => { const amountCell = useCallback(({ cell }) => {
@@ -73,10 +64,6 @@ function GeneralLedgerTable({
return <Money amount={transaction.amount} currency={'USD'} />; return <Money amount={transaction.amount} currency={'USD'} />;
}, []); }, []);
const referenceLink = useCallback((row) => {
return <a href="">{row.referenceId}</a>;
});
const columns = useMemo( const columns = useMemo(
() => [ () => [
{ {
@@ -99,7 +86,7 @@ function GeneralLedgerTable({
}, },
{ {
Header: formatMessage({ id: 'trans_num' }), Header: formatMessage({ id: 'trans_num' }),
accessor: referenceLink, accessor: 'reference_id',
className: 'transaction_number', className: 'transaction_number',
width: 110, width: 110,
}, },
@@ -125,10 +112,6 @@ function GeneralLedgerTable({
[], [],
); );
const handleFetchData = useCallback(() => {
onFetchData && onFetchData();
}, [onFetchData]);
// Default expanded rows of general ledger table. // Default expanded rows of general ledger table.
const expandedRows = useMemo( const expandedRows = useMemo(
() => defaultExpanderReducer(generalLedgerTableRows, 1), () => defaultExpanderReducer(generalLedgerTableRows, 1),
@@ -140,12 +123,11 @@ function GeneralLedgerTable({
return ( return (
<FinancialSheet <FinancialSheet
companyName={companyName} companyName={companyName}
// sheetType={formatMessage({ id: 'general_ledger_sheet' })} sheetType={formatMessage({ id: 'general_ledger_sheet' })}
fromDate={generalLedgerQuery.from_date} fromDate={generalLedgerQuery.from_date}
toDate={generalLedgerQuery.to_date} toDate={generalLedgerQuery.to_date}
name="general-ledger" name="general-ledger"
loading={generalLedgerSheetLoading} loading={generalLedgerSheetLoading}
minimal={true}
fullWidth={true} fullWidth={true}
> >
<DataTable <DataTable
@@ -155,7 +137,6 @@ function GeneralLedgerTable({
})} })}
columns={columns} columns={columns}
data={generalLedgerTableRows} data={generalLedgerTableRows}
onFetchData={handleFetchData}
rowClassNames={rowClassNames} rowClassNames={rowClassNames}
expanded={expandedRows} expanded={expandedRows}
virtualizedRows={true} virtualizedRows={true}
@@ -169,21 +150,7 @@ function GeneralLedgerTable({
); );
} }
const mapStateToProps = (state, props) => {
const { generalLedgerQuery } = props;
return {
generalLedgerIndex: getFinancialSheetIndexByQuery(
state.financialStatements.generalLedger.sheets,
generalLedgerQuery,
),
};
};
const withGeneralLedgerTable = connect(mapStateToProps);
export default compose( export default compose(
withGeneralLedgerTable,
withGeneralLedger( withGeneralLedger(
({ ({
generalLedgerTableRows, generalLedgerTableRows,

View File

@@ -1,27 +1,20 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { import {
getFinancialSheet, getFinancialSheetFactory,
getFinancialSheetQuery, getFinancialSheetQueryFactory,
getFinancialSheetTableRows, getFinancialSheetTableRowsFactory,
} from 'store/financialStatement/financialStatements.selectors'; } from 'store/financialStatement/financialStatements.selectors';
export default (mapState) => { export default (mapState) => {
const mapStateToProps = (state, props) => { const mapStateToProps = (state, props) => {
const { generalLedgerIndex } = props; const getGeneralLedgerSheet = getFinancialSheetFactory('generalLedger');
const getSheetTableRows = getFinancialSheetTableRowsFactory('generalLedger');
const getSheetQuery = getFinancialSheetQueryFactory('generalLedger');
const mapped = { const mapped = {
generalLedgerSheet: getFinancialSheet( generalLedgerSheet: getGeneralLedgerSheet(state, props),
state.financialStatements.generalLedger.sheets, generalLedgerTableRows: getSheetTableRows(state, props),
generalLedgerIndex, generalLedgerQuery: getSheetQuery(state, props),
),
generalLedgerTableRows: getFinancialSheetTableRows(
state.financialStatements.generalLedger.sheets,
generalLedgerIndex,
),
generalLedgerQuery: getFinancialSheetQuery(
state.financialStatements.generalLedger.sheets,
generalLedgerIndex,
),
generalLedgerSheetLoading: generalLedgerSheetLoading:
state.financialStatements.generalLedger.loading, state.financialStatements.generalLedger.loading,
generalLedgerSheetFilter: state.financialStatements.generalLedger.filter, generalLedgerSheetFilter: state.financialStatements.generalLedger.filter,

View File

@@ -2,6 +2,7 @@ import React, { useState, useCallback, useEffect } from 'react';
import { useQuery } from 'react-query'; import { useQuery } from 'react-query';
import moment from 'moment'; import moment from 'moment';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { queryCache } from 'react-query';
import { compose } from 'utils'; import { compose } from 'utils';
import JournalTable from './JournalTable'; import JournalTable from './JournalTable';
@@ -12,79 +13,89 @@ import DashboardPageContent from 'components/Dashboard/DashboardPageContent';
import DashboardInsider from 'components/Dashboard/DashboardInsider'; import DashboardInsider from 'components/Dashboard/DashboardInsider';
import withSettings from 'containers/Settings/withSettings'; import withSettings from 'containers/Settings/withSettings';
import withDashboardActions from 'containers/Dashboard/withDashboardActions'; import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withJournalActions from './withJournalActions'; import withJournalActions from './withJournalActions';
import withJournal from './withJournal';
import { transformFilterFormToQuery } from 'containers/FinancialStatements/common';
function Journal({ function Journal({
// #withJournalActions // #withJournalActions
requestFetchJournalSheet, requestFetchJournalSheet,
refreshJournalSheet,
// #withJournal
journalSheetRefresh,
// #withDashboardActions // #withDashboardActions
changePageTitle, changePageTitle,
setDashboardBackLink,
// #withPreferences // #withPreferences
organizationSettings, organizationName,
}) { }) {
const [filter, setFilter] = useState({ const [filter, setFilter] = useState({
from_date: moment().startOf('year').format('YYYY-MM-DD'), fromDate: moment().startOf('year').format('YYYY-MM-DD'),
to_date: moment().endOf('year').format('YYYY-MM-DD'), toDate: moment().endOf('year').format('YYYY-MM-DD'),
basis: 'accural', basis: 'accural',
}); });
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const fetchJournalSheet = useQuery(['journal-sheet', filter], (key, query) =>
requestFetchJournalSheet({
...transformFilterFormToQuery(filter),
}),
);
useEffect(() => { useEffect(() => {
changePageTitle(formatMessage({ id: 'journal_sheet' })); changePageTitle(formatMessage({ id: 'journal_sheet' }));
}, [changePageTitle, formatMessage]); }, [changePageTitle, formatMessage]);
const fetchHook = useQuery( useEffect(() => {
['journal', filter], // Show the back link on dashboard topbar.
(key, query) => requestFetchJournalSheet(query), setDashboardBackLink(true);
{ manual: true },
); return () => {
// Hide the back link on dashboard topbar.
setDashboardBackLink(false);
};
});
useEffect(() => {
if (journalSheetRefresh) {
queryCache.invalidateQueries('journal-sheet');
refreshJournalSheet(false);
}
}, [journalSheetRefresh, refreshJournalSheet]);
// Handle financial statement filter change. // Handle financial statement filter change.
const handleFilterSubmit = useCallback( const handleFilterSubmit = useCallback(
(filter) => { (filter) => {
const _filter = { const _filter = {
...filter, ...filter,
from_date: moment(filter.from_date).format('YYYY-MM-DD'), fromDate: moment(filter.fromDate).format('YYYY-MM-DD'),
to_date: moment(filter.to_date).format('YYYY-MM-DD'), toDate: moment(filter.toDate).format('YYYY-MM-DD'),
}; };
setFilter(_filter); setFilter(_filter);
fetchHook.refetch({ force: true }); queryCache.invalidateQueries('journal-sheet');
}, },
[fetchHook], [setFilter],
); );
const handlePrintClick = useCallback(() => {}, []);
const handleExportClick = useCallback(() => {}, []);
const handleFetchData = useCallback(({ sortBy, pageIndex, pageSize }) => {
fetchHook.refetch({ force: true });
}, []);
return ( return (
<DashboardInsider> <DashboardInsider>
<JournalActionsBar <JournalActionsBar />
onSubmitFilter={handleFilterSubmit}
onPrintClick={handlePrintClick}
onExportClick={handleExportClick}
/>
<DashboardPageContent> <DashboardPageContent>
<div class="financial-statement financial-statement--journal"> <div class="financial-statement financial-statement--journal">
<JournalHeader <JournalHeader
pageFilter={filter}
onSubmitFilter={handleFilterSubmit} onSubmitFilter={handleFilterSubmit}
pageFilter={filter}
/> />
<div class="financial-statement__body"> <div class="financial-statement__body">
<JournalTable <JournalTable
companyName={organizationSettings.name} companyName={organizationName}
journalQuery={filter} journalQuery={filter}
onFetchData={handleFetchData}
/> />
</div> </div>
</div> </div>
@@ -96,5 +107,10 @@ function Journal({
export default compose( export default compose(
withDashboardActions, withDashboardActions,
withJournalActions, withJournalActions,
withSettings, withSettings(({ organizationSettings }) => ({
organizationName: organizationSettings.name,
})),
withJournal(({ journalSheetRefresh }) => ({
journalSheetRefresh,
})),
)(Journal); )(Journal);

View File

@@ -11,16 +11,16 @@ import {
import { FormattedMessage as T } from 'react-intl'; import { FormattedMessage as T } from 'react-intl';
import Icon from 'components/Icon'; import Icon from 'components/Icon';
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar'; import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
import FilterDropdown from 'components/FilterDropdown';
import classNames from 'classnames'; import classNames from 'classnames';
import { If } from 'components';
import withJournalActions from './withJournalActions'; import withJournalActions from './withJournalActions';
import withJournal from './withJournal'; import withJournal from './withJournal';
import { compose } from 'utils'; import { compose } from 'utils';
/**
* Journal sheeet - Actions bar.
*/
function JournalActionsBar({ function JournalActionsBar({
// #withJournal // #withJournal
journalSheetFilter, journalSheetFilter,
@@ -40,36 +40,28 @@ function JournalActionsBar({
return ( return (
<DashboardActionsBar> <DashboardActionsBar>
<NavbarGroup> <NavbarGroup>
<Button
className={classNames(Classes.MINIMAL, 'button--table-views')}
icon={<Icon icon="cog-16" iconSize={16} />}
text={<T id={'customize_report'} />}
/>
<NavbarDivider />
<Button <Button
className={classNames(Classes.MINIMAL, 'button--gray-highlight')} className={classNames(Classes.MINIMAL, 'button--gray-highlight')}
text={<T id={'recalc_report'} />} text={<T id={'recalc_report'} />}
onClick={handleRecalcReport} onClick={handleRecalcReport}
icon={<Icon icon="refresh-16" iconSize={16} />} icon={<Icon icon="refresh-16" iconSize={16} />}
/> />
<If condition={journalSheetFilter}> <NavbarDivider />
<Button
className={Classes.MINIMAL}
text={<T id={'hide_filter'} />}
icon={<Icon icon="arrow-to-top" />}
onClick={handleFilterToggleClick}
/>
</If>
<If condition={!journalSheetFilter}>
<Button <Button
className={Classes.MINIMAL} className={classNames(Classes.MINIMAL, 'button--table-views')}
text={<T id={'show_filter'} />} icon={<Icon icon="cog-16" iconSize={16} />}
icon={<Icon icon="arrow-to-bottom" />} text={
(journalSheetFilter) ? (
<T id={'hide_customizer'} />
) : (
<T id={'customize_report'} />
)
}
active={journalSheetFilter}
onClick={handleFilterToggleClick} onClick={handleFilterToggleClick}
/> />
</If> <NavbarDivider />
<Popover <Popover
interactionKind={PopoverInteractionKind.CLICK} interactionKind={PopoverInteractionKind.CLICK}

View File

@@ -1,12 +1,12 @@
import React, { useCallback, useEffect } from 'react'; import React from 'react';
import { Row, Col } from 'react-grid-system';
import { Button } from '@blueprintjs/core';
import moment from 'moment'; import moment from 'moment';
import { useFormik } from 'formik'; import { Formik, Form } from 'formik';
import { FormattedMessage as T } from 'react-intl'; import { Tab, Tabs, Button, Intent } from '@blueprintjs/core';
import * as Yup from 'yup'; import * as Yup from 'yup';
import { FormattedMessage as T } from 'react-intl';
import JournalSheetHeaderGeneralPanel from './JournalSheetHeaderGeneralPanel';
import FinancialStatementDateRange from 'containers/FinancialStatements/FinancialStatementDateRange';
import FinancialStatementHeader from 'containers/FinancialStatements/FinancialStatementHeader'; import FinancialStatementHeader from 'containers/FinancialStatements/FinancialStatementHeader';
import withJournal from './withJournal'; import withJournal from './withJournal';
@@ -23,49 +23,75 @@ function JournalHeader({
// #withJournalActions // #withJournalActions
refreshJournalSheet, refreshJournalSheet,
toggleJournalSheetFilter,
// #withJournal // #withJournal
journalSheetFilter, journalSheetFilter,
journalSheetRefresh, journalSheetRefresh,
}) { }) {
const formik = useFormik({ const initialValues = {
enableReinitialize: true,
initialValues: {
...pageFilter, ...pageFilter,
from_date: moment(pageFilter.from_date).toDate(), fromDate: moment(pageFilter.fromDate).toDate(),
to_date: moment(pageFilter.to_date).toDate(), toDate: moment(pageFilter.toDate).toDate(),
}, };
validationSchema: Yup.object().shape({
from_date: Yup.date().required(), // Validation schema.
to_date: Yup.date().min(Yup.ref('from_date')).required(), const validationSchema = Yup.object().shape({
}), fromDate: Yup.date().required(),
onSubmit: (values, { setSubmitting }) => { toDate: Yup.date().min(Yup.ref('fromDate')).required(),
onSubmitFilter(values);
setSubmitting(false);
},
}); });
useEffect(() => { // Handle form submit.
if (journalSheetRefresh) { const handleSubmit = (values, { setSubmitting }) => {
formik.submitForm(); onSubmitFilter(values);
refreshJournalSheet(false); setSubmitting(false);
} toggleJournalSheetFilter();
}, [formik, journalSheetRefresh]); };
// Handle cancel journal drawer header.
const handleCancelClick = () => {
toggleJournalSheetFilter();
};
const handleDrawerClose = () => {
toggleJournalSheetFilter();
};
return ( return (
<FinancialStatementHeader show={journalSheetFilter}> <FinancialStatementHeader
<Row> isOpen={journalSheetFilter}
<FinancialStatementDateRange formik={formik} /> drawerProps={{ onClose: handleDrawerClose }}
</Row> >
<Formik
initialValues={initialValues}
onSubmit={handleSubmit}
validationSchema={validationSchema}
>
<Form>
<Tabs animate={true} vertical={true} renderActiveTabPanelOnly={true}>
<Tab
id="general"
title={'General'}
panel={<JournalSheetHeaderGeneralPanel />}
/>
</Tabs>
<div class="financial-header-drawer__footer">
<Button className={'mr1'} intent={Intent.PRIMARY} type={'submit'}>
<T id={'calculate_report'} />
</Button>
<Button onClick={handleCancelClick} minimal={true}>
<T id={'cancel'} />
</Button>
</div>
</Form>
</Formik>
</FinancialStatementHeader> </FinancialStatementHeader>
); );
} }
export default compose( export default compose(
withJournal(({ withJournal(({ journalSheetFilter, journalSheetRefresh }) => ({
journalSheetFilter,
journalSheetRefresh
}) => ({
journalSheetFilter, journalSheetFilter,
journalSheetRefresh, journalSheetRefresh,
})), })),

View File

@@ -0,0 +1,10 @@
import React from 'react';
import FinancialStatementDateRange from 'containers/FinancialStatements/FinancialStatementDateRange';
export default function JournalSheetHeaderGeneralPanel({}) {
return (
<div>
<FinancialStatementDateRange />
</div>
);
}

View File

@@ -1,17 +1,15 @@
import React, { useCallback, useMemo } from 'react'; import React, { useCallback, useMemo } from 'react';
import { connect } from 'react-redux';
import moment from 'moment'; import moment from 'moment';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import FinancialSheet from 'components/FinancialSheet'; import FinancialSheet from 'components/FinancialSheet';
import DataTable from 'components/DataTable'; import DataTable from 'components/DataTable';
import { compose, defaultExpanderReducer } from 'utils';
import Money from 'components/Money'; import Money from 'components/Money';
import { getFinancialSheetIndexByQuery } from 'store/financialStatement/financialStatements.selectors';
import withJournal from './withJournal'; import withJournal from './withJournal';
import { compose, defaultExpanderReducer } from 'utils';
function JournalSheetTable({ function JournalSheetTable({
// #withJournal // #withJournal
journalSheetTableRows, journalSheetTableRows,
@@ -106,12 +104,12 @@ function JournalSheetTable({
return ( return (
<FinancialSheet <FinancialSheet
companyName={companyName} companyName={companyName}
// sheetType={formatMessage({ id: 'journal_sheet' })} sheetType={formatMessage({ id: 'journal_sheet' })}
fromDate={journalSheetQuery.from_date} fromDate={journalSheetQuery.from_date}
toDate={journalSheetQuery.to_date} toDate={journalSheetQuery.to_date}
name="journal" name="journal"
loading={journalSheetLoading} loading={journalSheetLoading}
minimal={true} // minimal={true}
fullWidth={true} fullWidth={true}
> >
<DataTable <DataTable
@@ -129,20 +127,7 @@ function JournalSheetTable({
); );
} }
const mapStateToProps = (state, props) => {
const { journalQuery } = props;
return {
journalIndex: getFinancialSheetIndexByQuery(
state.financialStatements.journal.sheets,
journalQuery,
),
};
};
const withJournalTable = connect(mapStateToProps);
export default compose( export default compose(
withJournalTable,
withJournal( withJournal(
({ journalSheetTableRows, journalSheetLoading, journalSheetQuery }) => ({ ({ journalSheetTableRows, journalSheetLoading, journalSheetQuery }) => ({
journalSheetTableRows, journalSheetTableRows,

View File

@@ -1,34 +1,27 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { import {
getFinancialSheetIndexByQuery, getFinancialSheetFactory,
getFinancialSheet, getFinancialSheetTableRowsFactory,
getFinancialSheetTableRows, getFinancialSheetQueryFactory,
getFinancialSheetQuery,
} from 'store/financialStatement/financialStatements.selectors'; } from 'store/financialStatement/financialStatements.selectors';
export default (mapState) => { export default (mapState) => {
const mapStateToProps = (state, props) => { const mapStateToProps = (state, props) => {
const { journalIndex } = props; const getJournalSheet = getFinancialSheetFactory('journal');
const getJournalSheetTableRows = getFinancialSheetTableRowsFactory(
'journal',
);
const getJournalSheetQuery = getFinancialSheetQueryFactory('journal');
const mapped = { const mapped = {
journalSheet: getFinancialSheet( journalSheet: getJournalSheet(state, props),
state.financialStatements.journal.sheets, journalSheetTableRows: getJournalSheetTableRows(state, props),
journalIndex journalSheetQuery: getJournalSheetQuery(state, props),
),
journalSheetTableRows: getFinancialSheetTableRows(
state.financialStatements.journal.sheets,
journalIndex
),
journalSheetQuery: getFinancialSheetQuery(
state.financialStatements.journal.sheets,
journalIndex,
),
journalSheetLoading: state.financialStatements.journal.loading, journalSheetLoading: state.financialStatements.journal.loading,
journalSheetFilter: state.financialStatements.journal.filter, journalSheetFilter: state.financialStatements.journal.filter,
journalSheetRefresh: state.financialStatements.journal.refresh, journalSheetRefresh: state.financialStatements.journal.refresh,
}; };
return mapState ? mapState(mapped, state, props) : mapped; return mapState ? mapState(mapped, state, props) : mapped;
}; };
return connect(mapStateToProps); return connect(mapStateToProps);
}; };

View File

@@ -1,18 +1,24 @@
import React from 'react'; import React from 'react';
import { NavbarGroup, Button, Classes, NavbarDivider } from '@blueprintjs/core'; import {
NavbarGroup,
Button,
Classes,
NavbarDivider,
Popover,
Position,
PopoverInteractionKind,
} from '@blueprintjs/core';
import { FormattedMessage as T } from 'react-intl'; import { FormattedMessage as T } from 'react-intl';
import classNames from 'classnames'; import classNames from 'classnames';
import Icon from 'components/Icon'; import Icon from 'components/Icon';
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar'; import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
import { If } from 'components';
import withProfitLossActions from './withProfitLossActions'; import withProfitLossActions from './withProfitLossActions';
import withProfitLoss from './withProfitLoss'; import withProfitLoss from './withProfitLoss';
import { compose } from 'utils'; import { compose } from 'utils';
function ProfitLossActionsBar({ function ProfitLossActionsBar({
// #withProfitLoss // #withProfitLoss
profitLossSheetFilter, profitLossSheetFilter,
@@ -33,45 +39,43 @@ function ProfitLossActionsBar({
<DashboardActionsBar> <DashboardActionsBar>
<NavbarGroup> <NavbarGroup>
<Button <Button
className={classNames(Classes.MINIMAL, 'button--table-views')} className={classNames(Classes.MINIMAL, 'button--gray-highlight')}
icon={<Icon icon="cog-16" iconSize={16} />} text={<T id={'recalc_report'} />}
text={<T id={'customize_report'} />}
/>
<NavbarDivider />
<Button
className={classNames(
Classes.MINIMAL,
'button--gray-highlight',
)}
text={'Re-calc Report'}
onClick={handleRecalcReport} onClick={handleRecalcReport}
icon={<Icon icon="refresh-16" iconSize={16} />} icon={<Icon icon="refresh-16" iconSize={16} />}
/> />
<If condition={profitLossSheetFilter}>
<Button
className={Classes.MINIMAL}
text={<T id={'hide_filter'} />}
icon={<Icon icon="arrow-to-top" />}
onClick={handleFilterClick}
/>
</If>
<If condition={!profitLossSheetFilter}>
<Button
className={Classes.MINIMAL}
text={<T id={'show_filter'} />}
icon={<Icon icon="arrow-to-bottom" />}
onClick={handleFilterClick}
/>
</If>
<NavbarDivider /> <NavbarDivider />
<Button
className={classNames(Classes.MINIMAL, 'button--table-views')}
icon={<Icon icon="cog-16" iconSize={16} />}
text={
profitLossSheetFilter ? (
<T id={'hide_customizer'} />
) : (
<T id={'customize_report'} />
)
}
onClick={handleFilterClick}
active={profitLossSheetFilter}
/>
<NavbarDivider />
<Popover
// content={}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
>
<Button
className={classNames(Classes.MINIMAL, 'button--filter')}
text={<T id={'filter'} />}
icon={<Icon icon="filter-16" iconSize={16} />}
/>
</Popover>
<Button <Button
className={Classes.MINIMAL} className={Classes.MINIMAL}
icon={<Icon icon='print-16' iconSize={16} />} icon={<Icon icon="print-16" iconSize={16} />}
text={<T id={'print'} />} text={<T id={'print'} />}
/> />
<Button <Button

View File

@@ -2,6 +2,8 @@ import React, {useState, useCallback, useEffect} from 'react';
import moment from 'moment'; import moment from 'moment';
import {compose} from 'utils'; import {compose} from 'utils';
import { useQuery } from 'react-query'; import { useQuery } from 'react-query';
import { useIntl } from 'react-intl';
import { queryCache } from 'react-query';
import ProfitLossSheetHeader from './ProfitLossSheetHeader'; import ProfitLossSheetHeader from './ProfitLossSheetHeader';
import ProfitLossSheetTable from './ProfitLossSheetTable'; import ProfitLossSheetTable from './ProfitLossSheetTable';
@@ -13,59 +15,71 @@ import DashboardPageContent from 'components/Dashboard/DashboardPageContent'
import withDashboardActions from 'containers/Dashboard/withDashboardActions'; import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withProfitLossActions from './withProfitLossActions'; import withProfitLossActions from './withProfitLossActions';
import withProfitLoss from './withProfitLoss'; import withProfitLoss from './withProfitLoss';
// import SettingsConnect from 'connectors/Settings.connect';
import withSettings from 'containers/Settings/withSettings'; import withSettings from 'containers/Settings/withSettings';
import { transformFilterFormToQuery } from 'containers/FinancialStatements/common';
function ProfitLossSheet({ function ProfitLossSheet({
// #withDashboardActions // #withDashboardActions
changePageTitle, changePageTitle,
setDashboardBackLink,
// #withProfitLoss
profitLossSheetRefresh,
// #withProfitLossActions // #withProfitLossActions
fetchProfitLossSheet, fetchProfitLossSheet,
refreshProfitLossSheet,
// #withPreferences // #withPreferences
organizationSettings, organizationName,
}) { }) {
const [filter, setFilter] = useState({ const [filter, setFilter] = useState({
basis: 'cash', basis: 'cash',
from_date: moment().startOf('year').format('YYYY-MM-DD'), fromDate: moment().startOf('year').format('YYYY-MM-DD'),
to_date: moment().endOf('year').format('YYYY-MM-DD'), toDate: moment().endOf('year').format('YYYY-MM-DD'),
displayColumnsType: 'total',
accountsFilter: 'all-accounts',
}); });
const [refresh, setRefresh] = useState(true); const { formatMessage } = useIntl();
// Change page title of the dashboard. // Change page title of the dashboard.
useEffect(() => { useEffect(() => {
changePageTitle('Profit/Loss Sheet'); changePageTitle(formatMessage({ id: 'profit_loss_sheet' }));
}, [changePageTitle]); }, [changePageTitle, formatMessage]);
// Observes the P&L sheet refresh to invalid the query to refresh it.
useEffect(() => {
if (profitLossSheetRefresh) {
refreshProfitLossSheet(false);
queryCache.invalidateQueries('profit-loss-sheet');
}
}, [profitLossSheetRefresh, refreshProfitLossSheet]);
useEffect(() => {
// Show the back link on dashboard topbar.
setDashboardBackLink(true);
return () => {
// Hide the back link on dashboard topbar.
setDashboardBackLink(false);
};
});
// Fetches profit/loss sheet. // Fetches profit/loss sheet.
const fetchHook = useQuery(['profit-loss', filter], const fetchSheetHook = useQuery(['profit-loss-sheet', filter],
(key, query) => fetchProfitLossSheet(query), (key, query) => fetchProfitLossSheet({ ...transformFilterFormToQuery(query) }),
{ manual: true }); { manual: true });
// Handle submit filter. // Handle submit filter.
const handleSubmitFilter = useCallback((filter) => { const handleSubmitFilter = useCallback((filter) => {
const _filter = { const _filter = {
...filter, ...filter,
from_date: moment(filter.from_date).format('YYYY-MM-DD'), fromDate: moment(filter.fromDate).format('YYYY-MM-DD'),
to_date: moment(filter.to_date).format('YYYY-MM-DD'), toDate: moment(filter.toDate).format('YYYY-MM-DD'),
}; };
setFilter(_filter); setFilter(_filter);
setRefresh(true); }, [setFilter]);
}, []);
// Handle fetch data of profit/loss sheet table.
const handleFetchData = useCallback(() => {
setRefresh(true);
}, []);
useEffect(() => {
if (refresh) {
fetchHook.refetch({ force: true });
setRefresh(false);
}
}, [refresh, fetchHook]);
return ( return (
<DashboardInsider> <DashboardInsider>
@@ -79,9 +93,8 @@ function ProfitLossSheet({
<div class="financial-statement__body"> <div class="financial-statement__body">
<ProfitLossSheetTable <ProfitLossSheetTable
companyName={organizationSettings.name} companyName={organizationName}
profitLossQuery={filter} profitLossQuery={filter} />
onFetchData={handleFetchData} />
</div> </div>
</div> </div>
</DashboardPageContent> </DashboardPageContent>
@@ -92,5 +105,8 @@ function ProfitLossSheet({
export default compose( export default compose(
withDashboardActions, withDashboardActions,
withProfitLossActions, withProfitLossActions,
withSettings, withProfitLoss(({ profitLossSheetRefresh }) => ({ profitLossSheetRefresh })),
withSettings(({ organizationSettings }) => ({
organizationName: organizationSettings.name,
})),
)(ProfitLossSheet); )(ProfitLossSheet);

View File

@@ -1,126 +1,102 @@
import React, { useCallback, useEffect } from 'react'; import React, { useEffect } from 'react';
import { Row, Col, Visible } from 'react-grid-system';
import moment from 'moment'; import moment from 'moment';
import { useFormik } from 'formik'; import { Formik, Form } from 'formik';
import { FormattedMessage as T, useIntl } from 'react-intl'; import { FormattedMessage as T, useIntl } from 'react-intl';
import { FormGroup } from '@blueprintjs/core';
import * as Yup from 'yup'; import * as Yup from 'yup';
import { Tabs, Tab, Button, Intent } from '@blueprintjs/core';
import FinancialStatementDateRange from 'containers/FinancialStatements/FinancialStatementDateRange';
import FinancialStatementHeader from 'containers/FinancialStatements/FinancialStatementHeader'; import FinancialStatementHeader from 'containers/FinancialStatements/FinancialStatementHeader';
import SelectsListColumnsBy from '../SelectDisplayColumnsBy'; import ProfitLossSheetHeaderGeneralPane from './ProfitLossSheetHeaderGeneralPane';
import RadiosAccountingBasis from '../RadiosAccountingBasis';
import FinancialAccountsFilter from '../FinancialAccountsFilter';
import withProfitLoss from './withProfitLoss'; import withProfitLoss from './withProfitLoss';
import withProfitLossActions from './withProfitLossActions'; import withProfitLossActions from './withProfitLossActions';
import { compose } from 'utils'; import { compose } from 'utils';
function ProfitLossHeader({ function ProfitLossHeader({
// #ownProps
pageFilter, pageFilter,
onSubmitFilter, onSubmitFilter,
// #withProfitLoss // #withProfitLoss
profitLossSheetFilter, profitLossSheetFilter,
profitLossSheetRefresh,
// #withProfitLossActions // #withProfitLossActions
refreshProfitLossSheet, toggleProfitLossSheetFilter,
}) { }) {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const formik = useFormik({
enableReinitialize: true, // Validation schema.
initialValues: { const validationSchema = Yup.object().shape({
...pageFilter, fromDate: Yup.date()
from_date: moment(pageFilter.from_date).toDate(),
to_date: moment(pageFilter.to_date).toDate(),
},
validationSchema: Yup.object().shape({
from_date: Yup.date()
.required() .required()
.label(formatMessage({ id: 'from_date' })), .label(formatMessage({ id: 'from_date' })),
to_date: Yup.date() toDate: Yup.date()
.min(Yup.ref('from_date')) .min(Yup.ref('fromDate'))
.required() .required()
.label(formatMessage({ id: 'to_date' })), .label(formatMessage({ id: 'to_date' })),
}), accountsFilter: Yup.string(),
onSubmit: (values, actions) => { displayColumnsType: Yup.string(),
onSubmitFilter(values);
actions.setSubmitting(false);
},
}); });
// Handle item select of `display columns by` field. // Initial values.
const handleItemSelectDisplayColumns = useCallback( const initialValues = {
(item) => { ...pageFilter,
formik.setFieldValue('display_columns_type', item.type); fromDate: moment(pageFilter.fromDate).toDate(),
formik.setFieldValue('display_columns_by', item.by); toDate: moment(pageFilter.toDate).toDate(),
}, };
[formik],
);
const handleAccountingBasisChange = useCallback( // Handle form submit.
(value) => { const handleSubmit = (values, actions) => {
formik.setFieldValue('basis', value); onSubmitFilter(values);
}, toggleProfitLossSheetFilter();
[formik], };
);
useEffect(() => { // Handles the cancel button click.
if (profitLossSheetRefresh) { const handleCancelClick = () => {
formik.submitForm(); toggleProfitLossSheetFilter();
refreshProfitLossSheet(false); };
} // Handles the drawer close action.
}, [profitLossSheetRefresh]); const handleDrawerClose = () => {
toggleProfitLossSheetFilter();
const handleAccountsFilterSelect = (filterType) => {
const noneZero = filterType.key === 'without-zero-balance' ? true : false;
formik.setFieldValue('none_zero', noneZero);
}; };
return ( return (
<FinancialStatementHeader show={profitLossSheetFilter}> <FinancialStatementHeader
<Row> isOpen={profitLossSheetFilter}
<FinancialStatementDateRange formik={formik} /> drawerProps={{ onClose: handleDrawerClose }}
<Visible xl><Col width={'100%'} /></Visible>
<Col width={260}>
<SelectsListColumnsBy onItemSelect={handleItemSelectDisplayColumns} />
</Col>
<Col width={260}>
<FormGroup
label={<T id={'filter_accounts'} />}
className="form-group--select-list bp3-fill"
inline={false}
> >
<FinancialAccountsFilter <Formik
initialSelectedItem={'all-accounts'} validationSchema={validationSchema}
onItemSelect={handleAccountsFilterSelect} initialValues={initialValues}
onSubmit={handleSubmit}
>
<Form>
<Tabs animate={true} vertical={true} renderActiveTabPanelOnly={true}>
<Tab
id="general"
title={<T id={'general'} />}
panel={<ProfitLossSheetHeaderGeneralPane />}
/> />
</FormGroup> </Tabs>
</Col>
<Col width={260}> <div class="financial-header-drawer__footer">
<RadiosAccountingBasis <Button className={'mr1'} intent={Intent.PRIMARY} type={'submit'}>
selectedValue={formik.values.basis} <T id={'calculate_report'} />
onChange={handleAccountingBasisChange} </Button>
/> <Button onClick={handleCancelClick} minimal={true}>
</Col> <T id={'cancel'} />
</Row> </Button>
</div>
</Form>
</Formik>
</FinancialStatementHeader> </FinancialStatementHeader>
); );
} }
export default compose( export default compose(
withProfitLoss(({ withProfitLoss(({ profitLossSheetFilter }) => ({
profitLossSheetFilter, profitLossSheetFilter,
profitLossSheetRefresh,
}) => ({
profitLossSheetFilter,
profitLossSheetRefresh,
})), })),
withProfitLossActions, withProfitLossActions,
)(ProfitLossHeader); )(ProfitLossHeader);

View File

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

View File

@@ -1,13 +1,11 @@
import React, { useMemo, useCallback } from 'react'; import React, { useMemo, useCallback } from 'react';
import { connect } from 'react-redux';
import { FormattedMessage as T, useIntl } from 'react-intl'; import { FormattedMessage as T, useIntl } from 'react-intl';
import FinancialSheet from 'components/FinancialSheet'; import FinancialSheet from 'components/FinancialSheet';
import DataTable from 'components/DataTable'; import DataTable from 'components/DataTable';
import Money from 'components/Money'; import Money from 'components/Money';
import { compose, defaultExpanderReducer } from 'utils'; import { compose, defaultExpanderReducer, getColumnWidth } from 'utils';
import { getFinancialSheetIndexByQuery } from 'store/financialStatement/financialStatements.selectors';
import withProfitLossDetail from './withProfitLoss'; import withProfitLossDetail from './withProfitLoss';
function ProfitLossSheetTable({ function ProfitLossSheetTable({
@@ -18,7 +16,6 @@ function ProfitLossSheetTable({
profitLossSheetLoading, profitLossSheetLoading,
// #ownProps // #ownProps
onFetchData,
companyName, companyName,
}) { }) {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
@@ -26,14 +23,10 @@ function ProfitLossSheetTable({
const columns = useMemo( const columns = useMemo(
() => [ () => [
{ {
Header: formatMessage({ id: 'account_name' }), Header: formatMessage({ id: 'account' }),
accessor: 'name', accessor: (row) => (row.code ? `${row.name} - ${row.code}` : row.name),
className: 'name', className: 'name',
}, width: 240,
{
Header: formatMessage({ id: 'account_code' }),
accessor: 'code',
className: 'account_code',
}, },
...(profitLossQuery.display_columns_type === 'total' ...(profitLossQuery.display_columns_type === 'total'
? [ ? [
@@ -45,13 +38,14 @@ function ProfitLossSheetTable({
return ( return (
<Money <Money
amount={row.total.formatted_amount} amount={row.total.formatted_amount}
currency={'USD'} currency={row.total.currency_code}
/> />
); );
} }
return ''; return '';
}, },
className: 'total', className: 'total',
width: 140,
}, },
] ]
: []), : []),
@@ -60,40 +54,44 @@ function ProfitLossSheetTable({
id: `date_period_${index}`, id: `date_period_${index}`,
Header: column, Header: column,
accessor: (row) => { accessor: (row) => {
if (row.periods && row.periods[index]) { if (row.total_periods && row.total_periods[index]) {
const amount = row.periods[index].formatted_amount; const amount = row.total_periods[index].formatted_amount;
return <Money amount={amount} currency={'USD'} />; return <Money amount={amount} currency={'USD'} />;
} }
return ''; return '';
}, },
width: 100, width: getColumnWidth(
profitLossTableRows,
`total_periods.${index}.formatted_amount`,
{ minWidth: 100 },
),
className: 'total-period',
})) }))
: []), : []),
], ],
[profitLossQuery.display_columns_type, profitLossColumns, formatMessage], [profitLossQuery.display_columns_type, profitLossTableRows, profitLossColumns, formatMessage],
);
// Handle data table fetch data.
const handleFetchData = useCallback(
(...args) => {
onFetchData && onFetchData(...args);
},
[onFetchData],
); );
// Retrieve default expanded rows of balance sheet. // Retrieve default expanded rows of balance sheet.
const expandedRows = useMemo( const expandedRows = useMemo(
() => defaultExpanderReducer(profitLossTableRows, 1), () => defaultExpanderReducer(profitLossTableRows, 3),
[profitLossTableRows], [profitLossTableRows],
); );
// Retrieve conditional datatable row classnames. // Retrieve conditional datatable row classnames.
const rowClassNames = useCallback( const rowClassNames = useCallback((row) => {
(row) => ({ const { original } = row;
[`row--${row.rowType}`]: row.rowType, const rowTypes = Array.isArray(original.rowTypes)
}), ? original.rowTypes
[], : [];
);
return {
...rowTypes.reduce((acc, rowType) => {
acc[`row_type--${rowType}`] = rowType;
return acc;
}, {}),
};
}, []);
return ( return (
<FinancialSheet <FinancialSheet
@@ -109,7 +107,6 @@ function ProfitLossSheetTable({
className="bigcapital-datatable--financial-report" className="bigcapital-datatable--financial-report"
columns={columns} columns={columns}
data={profitLossTableRows} data={profitLossTableRows}
onFetchData={handleFetchData}
noInitialFetch={true} noInitialFetch={true}
expanded={expandedRows} expanded={expandedRows}
rowClassNames={rowClassNames} rowClassNames={rowClassNames}
@@ -121,19 +118,14 @@ function ProfitLossSheetTable({
); );
} }
const mapStateToProps = (state, props) => ({
profitLossIndex: getFinancialSheetIndexByQuery(
state.financialStatements.profitLoss.sheets,
props.profitLossQuery,
),
});
const withProfitLossTable = connect(mapStateToProps);
export default compose( export default compose(
withProfitLossTable,
withProfitLossDetail( withProfitLossDetail(
({ profitLossQuery, profitLossColumns, profitLossTableRows, profitLossSheetLoading }) => ({ ({
profitLossQuery,
profitLossColumns,
profitLossTableRows,
profitLossSheetLoading,
}) => ({
profitLossColumns, profitLossColumns,
profitLossQuery, profitLossQuery,
profitLossTableRows, profitLossTableRows,

View File

@@ -1,22 +1,23 @@
import {connect} from 'react-redux'; import {connect} from 'react-redux';
import { import {
getFinancialSheetIndexByQuery, getFinancialSheetFactory,
getFinancialSheet, getFinancialSheetColumnsFactory,
getFinancialSheetColumns, getFinancialSheetQueryFactory,
getFinancialSheetQuery, getFinancialSheetTableRowsFactory,
getFinancialSheetTableRows,
} from 'store/financialStatement/financialStatements.selectors'; } from 'store/financialStatement/financialStatements.selectors';
export default (mapState) => { export default (mapState) => {
const mapStateToProps = (state, props) => { const mapStateToProps = (state, props) => {
const { profitLossIndex } = props; const getProfitLossSheet = getFinancialSheetFactory('profitLoss');
const getProfitLossColumns = getFinancialSheetColumnsFactory('profitLoss');
const getProfitLossQuery = getFinancialSheetQueryFactory('profitLoss');
const getProfitLossTableRows = getFinancialSheetTableRowsFactory('profitLoss');
const mapped = { const mapped = {
profitLossSheet: getFinancialSheet(state.financialStatements.profitLoss.sheets, profitLossIndex), profitLossSheet: getProfitLossSheet(state, props),
profitLossColumns: getFinancialSheetColumns(state.financialStatements.profitLoss.sheets, profitLossIndex), profitLossColumns: getProfitLossColumns(state, props),
profitLossQuery: getFinancialSheetQuery(state.financialStatements.profitLoss.sheets, profitLossIndex), profitLossQuery: getProfitLossQuery(state, props),
profitLossTableRows: getFinancialSheetTableRows(state.financialStatements.profitLoss.sheets, profitLossIndex), profitLossTableRows: getProfitLossTableRows(state, props),
profitLossSheetLoading: state.financialStatements.profitLoss.loading, profitLossSheetLoading: state.financialStatements.profitLoss.loading,
profitLossSheetFilter: state.financialStatements.profitLoss.filter, profitLossSheetFilter: state.financialStatements.profitLoss.filter,

View File

@@ -1,28 +1,34 @@
import React from 'react'; import React from 'react';
import { FastField } from 'formik';
import { handleStringChange } from 'utils'; import { handleStringChange } from 'utils';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { import { RadioGroup, Radio } from '@blueprintjs/core';
RadioGroup,
Radio,
} from "@blueprintjs/core";
export default function RadiosAccountingBasis(props) { export default function RadiosAccountingBasis(props) {
const { onChange, ...rest } = props; const { key = 'basis', ...rest } = props;
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
return ( return (
<FastField name={'basis'}>
{({
form: { setFieldValue },
field: { value },
}) => (
<RadioGroup <RadioGroup
inline={true} inline={true}
label={formatMessage({'id': 'accounting_basis'})} label={formatMessage({ id: 'accounting_basis' })}
name="basis" name="basis"
onChange={handleStringChange((value) => { onChange={handleStringChange((value) => {
onChange && onChange(value); setFieldValue(key, value);
})} })}
className={'radio-group---accounting-basis'} className={'radio-group---accounting-basis'}
{...rest}> selectedValue={value}
{...rest}
>
<Radio label={formatMessage({ id: 'cash' })} value="cash" /> <Radio label={formatMessage({ id: 'cash' })} value="cash" />
<Radio label={formatMessage({ id: 'accrual' })} value="accural" /> <Radio label={formatMessage({ id: 'accrual' })} value="accural" />
</RadioGroup> </RadioGroup>
)}
</FastField>
); );
} }

View File

@@ -1,58 +1,43 @@
import React from 'react';
import { FormGroup } from '@blueprintjs/core';
import React, { useMemo, useState, useCallback } from 'react'; import { FastField } from 'formik';
import SelectList from 'components/SelectList';
import {
FormGroup,
MenuItem,
} from '@blueprintjs/core';
import { FormattedMessage as T } from 'react-intl'; import { FormattedMessage as T } from 'react-intl';
import classNames from 'classnames'; import { Row, Col, ListSelect } from 'components';
import { MODIFIER } from 'components'; import { displayColumnsByOptions } from 'containers/FinancialStatements/common';
/**
* Financial statement - Display columns by and type select.
*/
export default function SelectsListColumnsBy(props) { export default function SelectsListColumnsBy(props) {
const { onItemSelect, formGroupProps, selectListProps } = props; const { formGroupProps, selectListProps } = props;
const [itemSelected, setItemSelected] = useState(null);
const displayColumnsByOptions = useMemo(() => [
{key: 'total', name: 'Total', type: 'total', by: '', },
{key: 'year', name: 'Date/Year', type: 'date_periods', by: 'year'},
{key: 'month', name: 'Date/Month', type: 'date_periods', by: 'month'},
{key: 'week', name: 'Date/Week', type: 'date_periods', by: 'month'},
{key: 'day', name: 'Date/Day', type: 'date_periods', by: 'day'},
{key: 'quarter', name: 'Date/Quarter', type: 'date_periods', by: 'quarter'},
],[]);
const itemRenderer = useCallback((item, { handleClick, modifiers, query }) => {
return (<MenuItem text={item.name} key={item.id} onClick={handleClick} />);
}, []);
const handleItemSelect = useCallback((item) => {
setItemSelected(item);
onItemSelect && onItemSelect(item);
}, [setItemSelected, onItemSelect]);
const buttonLabel = useMemo(() =>
itemSelected ? itemSelected.name : <T id={'select_display_columns_by'}/>,
[itemSelected]);
return ( return (
<Row>
<Col xs={4}>
<FastField name={'displayColumnsType'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup <FormGroup
label={<T id={'display_report_columns'} />} label={<T id={'display_report_columns'} />}
className="form-group-display-columns-by form-group--select-list bp3-fill" className="form-group-display-columns-by form-group--select-list bp3-fill"
inline={false} inline={false}
{...formGroupProps}> {...formGroupProps}
>
<SelectList <ListSelect
items={displayColumnsByOptions} items={displayColumnsByOptions}
noResults={<MenuItem disabled={true} text="No results." />}
filterable={false} filterable={false}
itemRenderer={itemRenderer} selectedItem={value}
popoverProps={{ minimal: true, usePortal: false, inline: true }} selectedItemProp={'key'}
buttonLabel={buttonLabel} labelProp={'name'}
onItemSelect={handleItemSelect} onItemSelect={(item) => {
className={classNames(MODIFIER.SELECT_LIST_FILL_POPOVER)} form.setFieldValue('displayColumnsType', item.key);
{...selectListProps} /> }}
popoverProps={{ minimal: true }}
{...selectListProps}
/>
</FormGroup> </FormGroup>
)}
</FastField>
</Col>
</Row>
); );
} }

View File

@@ -1,20 +1,24 @@
import React from 'react'; import React from 'react';
import { NavbarGroup, Button, Classes, NavbarDivider } from '@blueprintjs/core'; import {
import Icon from 'components/Icon'; NavbarGroup,
import { FormattedMessage as T } from 'react-intl'; Button,
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar'; Classes,
NavbarDivider,
Popover,
PopoverInteractionKind,
Position,
} from '@blueprintjs/core';
import classNames from 'classnames'; import classNames from 'classnames';
// import FilterDropdown from 'components/FilterDropdown'; import { FormattedMessage as T } from 'react-intl';
import { If } from 'components'; import Icon from 'components/Icon';
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
import withTrialBalance from './withTrialBalance'; import withTrialBalance from './withTrialBalance';
import withTrialBalanceActions from './withTrialBalanceActions'; import withTrialBalanceActions from './withTrialBalanceActions';
import { compose } from 'utils'; import { compose } from 'utils';
function TrialBalanceActionsBar({ function TrialBalanceActionsBar({
// #withTrialBalance // #withTrialBalance
trialBalanceSheetFilter, trialBalanceSheetFilter,
@@ -22,7 +26,6 @@ function TrialBalanceActionsBar({
toggleTrialBalanceFilter, toggleTrialBalanceFilter,
refreshTrialBalance, refreshTrialBalance,
}) { }) {
const handleFilterToggleClick = () => { const handleFilterToggleClick = () => {
toggleTrialBalanceFilter(); toggleTrialBalanceFilter();
}; };
@@ -35,45 +38,43 @@ function TrialBalanceActionsBar({
<DashboardActionsBar> <DashboardActionsBar>
<NavbarGroup> <NavbarGroup>
<Button <Button
className={classNames(Classes.MINIMAL, 'button--table-views')} className={classNames(Classes.MINIMAL, 'button--gray-highlight')}
icon={<Icon icon="cog-16" iconSize={16} />}
text={<T id={'customize_report'} />}
/>
<NavbarDivider />
<Button
className={classNames(
Classes.MINIMAL,
'button--gray-highlight',
)}
text={'Re-calc Report'} text={'Re-calc Report'}
onClick={handleRecalcReport} onClick={handleRecalcReport}
icon={<Icon icon="refresh-16" iconSize={16} />} icon={<Icon icon="refresh-16" iconSize={16} />}
/> />
<If condition={trialBalanceSheetFilter}>
<Button
className={Classes.MINIMAL}
text={<T id={'hide_filter'} />}
icon={<Icon icon="arrow-to-top" />}
onClick={handleFilterToggleClick}
/>
</If>
<If condition={!trialBalanceSheetFilter}>
<Button
className={Classes.MINIMAL}
text={<T id={'show_filter'} />}
icon={<Icon icon="arrow-to-bottom" />}
onClick={handleFilterToggleClick}
/>
</If>
<NavbarDivider /> <NavbarDivider />
<Button
className={classNames(Classes.MINIMAL, 'button--table-views')}
icon={<Icon icon="cog-16" iconSize={16} />}
text={
trialBalanceSheetFilter ? (
<T id={'hide_customizer'} />
) : (
<T id={'customize_report'} />
)
}
active={trialBalanceSheetFilter}
onClick={handleFilterToggleClick}
/>
<NavbarDivider />
<Popover
// content={}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
>
<Button
className={classNames(Classes.MINIMAL, 'button--filter')}
text={<T id={'filter'} />}
icon={<Icon icon="filter-16" iconSize={16} />}
/>
</Popover>
<Button <Button
className={Classes.MINIMAL} className={Classes.MINIMAL}
icon={<Icon icon='print-16' iconSize={16} />} icon={<Icon icon="print-16" iconSize={16} />}
text={<T id={'print'} />} text={<T id={'print'} />}
/> />
<Button <Button
@@ -87,6 +88,8 @@ function TrialBalanceActionsBar({
} }
export default compose( export default compose(
withTrialBalance(({ trialBalanceSheetFilter }) => ({ trialBalanceSheetFilter })), withTrialBalance(({ trialBalanceSheetFilter }) => ({
withTrialBalanceActions trialBalanceSheetFilter,
})),
withTrialBalanceActions,
)(TrialBalanceActionsBar); )(TrialBalanceActionsBar);

View File

@@ -2,73 +2,93 @@ import React, { useEffect, useCallback, useState } from 'react';
import { useQuery } from 'react-query'; import { useQuery } from 'react-query';
import moment from 'moment'; import moment from 'moment';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { queryCache } from 'react-query';
import TrialBalanceActionsBar from './TrialBalanceActionsBar';
import TrialBalanceSheetHeader from './TrialBalanceSheetHeader'; import TrialBalanceSheetHeader from './TrialBalanceSheetHeader';
import TrialBalanceSheetTable from './TrialBalanceSheetTable'; import TrialBalanceSheetTable from './TrialBalanceSheetTable';
import TrialBalanceActionsBar from './TrialBalanceActionsBar';
import DashboardInsider from 'components/Dashboard/DashboardInsider'; import DashboardInsider from 'components/Dashboard/DashboardInsider';
import { compose } from 'utils'; import { compose } from 'utils';
import { transformFilterFormToQuery } from 'containers/FinancialStatements/common';
import DashboardPageContent from 'components/Dashboard/DashboardPageContent'; import DashboardPageContent from 'components/Dashboard/DashboardPageContent';
import withDashboardActions from 'containers/Dashboard/withDashboardActions'; import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withTrialBalanceActions from './withTrialBalanceActions'; import withTrialBalanceActions from './withTrialBalanceActions';
import withSettings from 'containers/Settings/withSettings'; import withSettings from 'containers/Settings/withSettings';
import withTrialBalance from './withTrialBalance';
/**
* Trial balance sheet.
*/
function TrialBalanceSheet({ function TrialBalanceSheet({
// #withDashboardActions // #withDashboardActions
changePageTitle, changePageTitle,
setDashboardBackLink,
// #withTrialBalance
trialBalanceSheetRefresh,
// #withTrialBalanceActions // #withTrialBalanceActions
fetchTrialBalanceSheet, fetchTrialBalanceSheet,
refreshTrialBalance,
// #withPreferences // #withPreferences
organizationSettings, organizationName,
}) { }) {
const [filter, setFilter] = useState({
from_date: moment().startOf('year').format('YYYY-MM-DD'),
to_date: moment().endOf('year').format('YYYY-MM-DD'),
basis: 'accural',
none_zero: false,
});
const [refresh, setRefresh] = useState(true);
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const fetchHook = useQuery( const [filter, setFilter] = useState({
['trial-balance', filter], fromDate: moment().startOf('year').format('YYYY-MM-DD'),
(key, query) => fetchTrialBalanceSheet(query), toDate: moment().endOf('year').format('YYYY-MM-DD'),
basis: 'accural',
accountsFilter: 'all-accounts',
});
// Fetches trial balance sheet.
const fetchSheet = useQuery(
['trial-balance-sheet', filter],
(key, query) =>
fetchTrialBalanceSheet({
...transformFilterFormToQuery(query),
}),
{ manual: true }, { manual: true },
); );
// handle fetch data of trial balance table.
const handleFetchData = useCallback(() => {
setRefresh(true);
}, []);
// Change page title of the dashboard. // Change page title of the dashboard.
useEffect(() => { useEffect(() => {
changePageTitle(formatMessage({ id: 'trial_balance_sheet' })); changePageTitle(formatMessage({ id: 'trial_balance_sheet' }));
}, [changePageTitle, formatMessage]); }, [changePageTitle, formatMessage]);
useEffect(() => {
// Show the back link on dashboard topbar.
setDashboardBackLink(true);
return () => {
// Hide the back link on dashboard topbar.
setDashboardBackLink(false);
};
});
const handleFilterSubmit = useCallback( const handleFilterSubmit = useCallback(
(filter) => { (filter) => {
const parsedFilter = { const parsedFilter = {
...filter, ...filter,
from_date: moment(filter.from_date).format('YYYY-MM-DD'), fromDate: moment(filter.fromDate).format('YYYY-MM-DD'),
to_date: moment(filter.to_date).format('YYYY-MM-DD'), toDate: moment(filter.toDate).format('YYYY-MM-DD'),
}; };
setFilter(parsedFilter); setFilter(parsedFilter);
setRefresh(true); refreshTrialBalance(true);
}, },
[fetchHook], [setFilter, refreshTrialBalance],
); );
// Observes the trial balance sheet refresh to invaoid the query.
useEffect(() => { useEffect(() => {
if (refresh) { if (trialBalanceSheetRefresh) {
fetchHook.refetch({ force: true }); queryCache.invalidateQueries('trial-balance-sheet');
setRefresh(false); refreshTrialBalance(false);
} }
}, [refresh, fetchHook.refetch]); }, [trialBalanceSheetRefresh, refreshTrialBalance]);
return ( return (
<DashboardInsider> <DashboardInsider>
@@ -82,11 +102,7 @@ function TrialBalanceSheet({
/> />
<div class="financial-statement__body"> <div class="financial-statement__body">
<TrialBalanceSheetTable <TrialBalanceSheetTable companyName={organizationName} />
companyName={organizationSettings.name}
trialBalanceQuery={filter}
onFetchData={handleFetchData}
/>
</div> </div>
</div> </div>
</DashboardPageContent> </DashboardPageContent>
@@ -97,5 +113,10 @@ function TrialBalanceSheet({
export default compose( export default compose(
withDashboardActions, withDashboardActions,
withTrialBalanceActions, withTrialBalanceActions,
withSettings, withTrialBalance(({ trialBalanceSheetRefresh }) => ({
trialBalanceSheetRefresh,
})),
withSettings(({ organizationSettings }) => ({
organizationName: organizationSettings.name,
})),
)(TrialBalanceSheet); )(TrialBalanceSheet);

View File

@@ -1,15 +1,12 @@
import React, { useEffect, useCallback } from 'react'; import React from 'react';
import * as Yup from 'yup'; import * as Yup from 'yup';
import moment from 'moment'; import moment from 'moment';
import { Row, Col, Visible } from 'react-grid-system';
import { FormattedMessage as T, useIntl } from 'react-intl'; import { FormattedMessage as T, useIntl } from 'react-intl';
import { FormGroup } from '@blueprintjs/core'; import { Formik, Form } from 'formik';
import { useFormik } from 'formik'; import { Tabs, Tab, Button, Intent } from '@blueprintjs/core';
import FinancialStatementHeader from 'containers/FinancialStatements/FinancialStatementHeader'; import FinancialStatementHeader from 'containers/FinancialStatements/FinancialStatementHeader';
import FinancialStatementDateRange from 'containers/FinancialStatements/FinancialStatementDateRange'; import TrialBalanceSheetHeaderGeneralPanel from './TrialBalanceSheetHeaderGeneralPanel';
import RadiosAccountingBasis from '../RadiosAccountingBasis';
import FinancialAccountsFilter from '../FinancialAccountsFilter';
import withTrialBalance from './withTrialBalance'; import withTrialBalance from './withTrialBalance';
import withTrialBalanceActions from './withTrialBalanceActions'; import withTrialBalanceActions from './withTrialBalanceActions';
@@ -17,6 +14,7 @@ import withTrialBalanceActions from './withTrialBalanceActions';
import { compose } from 'utils'; import { compose } from 'utils';
function TrialBalanceSheetHeader({ function TrialBalanceSheetHeader({
// #ownProps
pageFilter, pageFilter,
onSubmitFilter, onSubmitFilter,
@@ -26,78 +24,78 @@ function TrialBalanceSheetHeader({
// #withTrialBalanceActions // #withTrialBalanceActions
refreshTrialBalance, refreshTrialBalance,
toggleTrialBalanceFilter
}) { }) {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const formik = useFormik({
enableReinitialize: true, // Form validation schema.
initialValues: { const validationSchema = Yup.object().shape({
...pageFilter, fromDate: Yup.date()
from_date: moment(pageFilter.from_date).toDate(),
to_date: moment(pageFilter.to_date).toDate(),
},
validationSchema: Yup.object().shape({
from_date: Yup.date()
.required() .required()
.label(formatMessage({ id: 'from_date' })), .label(formatMessage({ id: 'from_date' })),
to_date: Yup.date() toDate: Yup.date()
.min(Yup.ref('from_date')) .min(Yup.ref('fromDate'))
.required() .required()
.label(formatMessage({ id: 'to_date' })), .label(formatMessage({ id: 'to_date' })),
}),
onSubmit: (values, { setSubmitting }) => {
onSubmitFilter(values);
setSubmitting(false);
},
}); });
useEffect(() => { // Initial values.
if (trialBalanceSheetRefresh) { const initialValues = {
formik.submitForm(); ...pageFilter,
refreshTrialBalance(false); fromDate: moment(pageFilter.fromDate).toDate(),
} toDate: moment(pageFilter.toDate).toDate(),
}, [formik, trialBalanceSheetRefresh]); };
const handleAccountingBasisChange = useCallback( // Handle form submit.
(value) => { const handleSubmit = (values, { setSubmitting }) => {
formik.setFieldValue('basis', value); onSubmitFilter(values);
}, setSubmitting(false);
[formik], toggleTrialBalanceFilter(false);
); };
const handleAccountsFilterSelect = (filterType) => { // Handle drawer close action.
const noneZero = filterType.key === 'without-zero-balance' ? true : false; const handleDrawerClose = () => {
formik.setFieldValue('none_zero', noneZero); toggleTrialBalanceFilter(false);
};
// Handle cancel button click.
const handleCancelClick = () => {
toggleTrialBalanceFilter(false);
}; };
return ( return (
<FinancialStatementHeader show={trialBalanceSheetFilter}> <FinancialStatementHeader
<Row> isOpen={trialBalanceSheetFilter}
<FinancialStatementDateRange formik={formik} /> drawerProps={{ onClose: handleDrawerClose }}
<Visible xl>
<Col width={'100%'} />
</Visible>
<Col width={260}>
<FormGroup
label={<T id={'filter_accounts'} />}
className="form-group--select-list bp3-fill"
inline={false}
> >
<FinancialAccountsFilter <Formik
initialSelectedItem={'all-accounts'} initialValues={initialValues}
onItemSelect={handleAccountsFilterSelect} validationSchema={validationSchema}
onSubmit={handleSubmit}
>
<Form>
<Tabs animate={true} vertical={true} renderActiveTabPanelOnly={true}>
<Tab
id="general"
title={<T id={'general'} />}
panel={<TrialBalanceSheetHeaderGeneralPanel />}
/> />
</FormGroup> </Tabs>
</Col>
<Col width={260}> <div class="financial-header-drawer__footer">
<RadiosAccountingBasis <Button
selectedValue={formik.values.basis} className={'mr1'}
onChange={handleAccountingBasisChange} intent={Intent.PRIMARY}
/> type={'submit'}
</Col> >
</Row> <T id={'calculate_report'} />
</Button>
<Button onClick={handleCancelClick} minimal={true}>
<T id={'cancel'} />
</Button>
</div>
</Form>
</Formik>
</FinancialStatementHeader> </FinancialStatementHeader>
); );
} }

View File

@@ -0,0 +1,19 @@
import React from 'react';
import FinancialStatementDateRange from 'containers/FinancialStatements/FinancialStatementDateRange';
import RadiosAccountingBasis from '../RadiosAccountingBasis';
import FinancialAccountsFilter from '../FinancialAccountsFilter';
/**
* Trial balance sheet - Drawer header - General panel.
*/
export default function TrialBalanceSheetHeaderGeneralPanel({
}) {
return (
<div>
<FinancialStatementDateRange />
<FinancialAccountsFilter initialSelectedItem={'all-accounts'} />
<RadiosAccountingBasis />
</div>
);
}

View File

@@ -1,11 +1,9 @@
import React, { useCallback, useMemo } from 'react'; import React, { useMemo } from 'react';
import { connect } from 'react-redux';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import FinancialSheet from 'components/FinancialSheet'; import FinancialSheet from 'components/FinancialSheet';
import DataTable from 'components/DataTable'; import DataTable from 'components/DataTable';
import Money from 'components/Money'; import Money from 'components/Money';
import { getFinancialSheetIndexByQuery } from 'store/financialStatement/financialStatements.selectors';
import withTrialBalance from './withTrialBalance'; import withTrialBalance from './withTrialBalance';
@@ -13,14 +11,12 @@ import { compose } from 'utils';
function TrialBalanceSheetTable({ function TrialBalanceSheetTable({
// #withTrialBalanceDetail // #withTrialBalanceDetail
trialBalanceAccounts, trialBalance,
trialBalanceSheetLoading, trialBalanceSheetLoading,
// #withTrialBalanceTable // #withTrialBalanceTable
trialBalanceIndex,
trialBalanceQuery, trialBalanceQuery,
onFetchData,
companyName, companyName,
}) { }) {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
@@ -29,55 +25,46 @@ function TrialBalanceSheetTable({
() => [ () => [
{ {
Header: formatMessage({ id: 'account_name' }), Header: formatMessage({ id: 'account_name' }),
accessor: 'name', accessor: (row) => (row.code ? `${row.name} - ${row.code}` : row.name),
className: 'name', className: 'name',
minWidth: 150, minWidth: 150,
maxWidth: 150, maxWidth: 150,
width: 150, width: 150,
}, },
{
Header: formatMessage({ id: 'code' }),
accessor: 'code',
className: 'code',
minWidth: 80,
maxWidth: 80,
width: 80,
},
{ {
Header: formatMessage({ id: 'credit' }), Header: formatMessage({ id: 'credit' }),
accessor: 'credit', accessor: 'credit',
Cell: ({ cell }) => <Money amount={cell.row.original.credit} currency="USD" />, Cell: ({ cell }) => {
const { currency_code, credit } = cell.row.original;
return (<Money amount={credit} currency={currency_code} />);
},
className: 'credit', className: 'credit',
minWidth: 95,
maxWidth: 95,
width: 95, width: 95,
}, },
{ {
Header: formatMessage({ id: 'debit' }), Header: formatMessage({ id: 'debit' }),
accessor: 'debit', accessor: 'debit',
Cell: ({ cell }) => <Money amount={cell.row.original.debit} currency="USD" />, Cell: ({ cell }) => {
const { currency_code, debit } = cell.row.original;
return (<Money amount={debit} currency={currency_code} />);
},
className: 'debit', className: 'debit',
minWidth: 95,
maxWidth: 95,
width: 95, width: 95,
}, },
{ {
Header: formatMessage({ id: 'balance' }), Header: formatMessage({ id: 'balance' }),
accessor: 'balance', accessor: 'balance',
Cell: ({ cell }) => <Money amount={cell.row.original.balance} currency="USD" />, Cell: ({ cell }) => {
const { currency_code, balance } = cell.row.original;
return (<Money amount={balance} currency={currency_code} />);
},
className: 'balance', className: 'balance',
minWidth: 95,
maxWidth: 95,
width: 95, width: 95,
}, },
], ],
[formatMessage], [formatMessage],
); );
const handleFetchData = useCallback(() => {
onFetchData && onFetchData();
}, [onFetchData]);
return ( return (
<FinancialSheet <FinancialSheet
companyName={companyName} companyName={companyName}
@@ -86,12 +73,12 @@ function TrialBalanceSheetTable({
toDate={trialBalanceQuery.to_date} toDate={trialBalanceQuery.to_date}
name="trial-balance" name="trial-balance"
loading={trialBalanceSheetLoading} loading={trialBalanceSheetLoading}
basis={'cash'}
> >
<DataTable <DataTable
className="bigcapital-datatable--financial-report" className="bigcapital-datatable--financial-report"
columns={columns} columns={columns}
data={trialBalanceAccounts} data={trialBalance.data}
onFetchData={handleFetchData}
expandable={true} expandable={true}
expandToggleColumn={1} expandToggleColumn={1}
expandColumnSpace={1} expandColumnSpace={1}
@@ -101,25 +88,14 @@ function TrialBalanceSheetTable({
); );
} }
const mapStateToProps = (state, props) => {
const { trialBalanceQuery } = props;
return {
trialBalanceIndex: getFinancialSheetIndexByQuery(
state.financialStatements.trialBalance.sheets,
trialBalanceQuery,
),
};
};
const withTrialBalanceTable = connect(mapStateToProps);
export default compose( export default compose(
withTrialBalanceTable,
withTrialBalance(({ withTrialBalance(({
trialBalanceAccounts, trialBalance,
trialBalanceSheetLoading, trialBalanceSheetLoading,
trialBalanceQuery
}) => ({ }) => ({
trialBalanceAccounts, trialBalance,
trialBalanceSheetLoading trialBalanceSheetLoading,
trialBalanceQuery
})), })),
)(TrialBalanceSheetTable); )(TrialBalanceSheetTable);

View File

@@ -1,28 +1,22 @@
import {connect} from 'react-redux'; import {connect} from 'react-redux';
import { import {
getFinancialSheetAccounts, getFinancialSheetFactory,
getFinancialSheetQuery, getFinancialSheetQueryFactory,
} from 'store/financialStatement/financialStatements.selectors'; } from 'store/financialStatement/financialStatements.selectors';
export default (mapState) => { export default (mapState) => {
const mapStateToProps = (state, props) => { const mapStateToProps = (state, props) => {
const { trialBalanceIndex } = props; const getTrialBalance = getFinancialSheetFactory('trialBalance');
const getBalanceSheetQuery = getFinancialSheetQueryFactory('trialBalance');
const mapped = { const mapped = {
trialBalanceAccounts: getFinancialSheetAccounts( trialBalance: getTrialBalance(state, props),
state.financialStatements.trialBalance.sheets, trialBalanceQuery: getBalanceSheetQuery(state, props),
trialBalanceIndex
),
trialBalanceQuery: getFinancialSheetQuery(
state.financialStatements.trialBalance.sheets,
trialBalanceIndex
),
trialBalanceSheetLoading: state.financialStatements.trialBalance.loading, trialBalanceSheetLoading: state.financialStatements.trialBalance.loading,
trialBalanceSheetFilter: state.financialStatements.trialBalance.filter, trialBalanceSheetFilter: state.financialStatements.trialBalance.filter,
trialBalanceSheetRefresh: state.financialStatements.trialBalance.refresh, trialBalanceSheetRefresh: state.financialStatements.trialBalance.refresh,
}; };
return mapState ? mapState(mapped, state, props) : mapped; return mapState ? mapState(mapped, state, props) : mapped;
}; };
return connect(mapStateToProps); return connect(mapStateToProps);
}; };

View File

@@ -0,0 +1,61 @@
import { mapKeys, omit, snakeCase } from 'lodash';
import { formatMessage } from 'services/intl';
export const displayColumnsByOptions = [
{ key: 'total', name: 'Total', type: 'total', by: '' },
{ key: 'year', name: 'Date/Year', type: 'date_periods', by: 'year' },
{ key: 'month', name: 'Date/Month', type: 'date_periods', by: 'month' },
{ key: 'week', name: 'Date/Week', type: 'date_periods', by: 'month' },
{ key: 'day', name: 'Date/Day', type: 'date_periods', by: 'day' },
{ key: 'quarter', name: 'Date/Quarter', type: 'date_periods', by: 'quarter' },
];
export const dateRangeOptions = [
{ value: 'today', label: 'Today' },
{ value: 'this_week', label: 'This Week' },
{ value: 'this_month', label: 'This Month' },
{ value: 'this_quarter', label: 'This Quarter' },
{ value: 'this_year', label: 'This Year' },
{ value: 'custom', label: 'Custom Range' },
];
export const filterAccountsOptions = [
{
key: 'all-accounts',
name: formatMessage({ id: 'all_accounts' }),
hint: formatMessage({ id: 'all_accounts_including_with_zero_balance' }),
},
{
key: 'without-zero-balance',
name: formatMessage({ id: 'accounts_without_zero_balance' }),
hint: formatMessage({
id: 'include_accounts_and_exclude_zero_balance',
}),
},
{
key: 'with-transactions',
name: formatMessage({ id: 'accounts_with_transactions' }),
hint: formatMessage({
id: 'include_accounts_once_has_transactions_on_given_date_period',
}),
},
];
export const transformDisplayColumnsType = (form) => {
const columnType = displayColumnsByOptions.find(
(o) => o.key === form.displayColumnsType,
);
return {
displayColumnsBy: columnType ? columnType.by : '',
displayColumnsType: columnType ? columnType.type : 'total',
};
};
export const transformFilterFormToQuery = (form) => {
return mapKeys({
...omit(form, ['accountsFilter']),
...transformDisplayColumnsType(form),
noneZero: form.accountsFilter === 'without-zero-balance',
noneTransactions: form.accountsFilter === 'with-transactions',
}, (v, k) => snakeCase(k));
};

View File

@@ -9,6 +9,14 @@ import * as serviceWorker from 'serviceWorker';
import { store, persistor } from 'store/createStore'; import { store, persistor } from 'store/createStore';
import AppProgress from 'components/NProgress/AppProgress'; import AppProgress from 'components/NProgress/AppProgress';
if (process.env.NODE_ENV === 'development') {
const whyDidYouRender = require('@welldone-software/why-did-you-render');
whyDidYouRender(React, {
trackAllPureComponents: false,
});
}
ReactDOM.render( ReactDOM.render(
<Provider store={store}> <Provider store={store}>
<PersistGate loading={null} persistor={persistor}> <PersistGate loading={null} persistor={persistor}>

View File

@@ -256,7 +256,7 @@ export default {
accrual: 'Accrual', accrual: 'Accrual',
from: 'From', from: 'From',
to: 'To', to: 'To',
accounting_basis: 'Accounting Basis:', accounting_basis: 'Accounting basis:',
general: 'General', general: 'General',
users: 'Users', users: 'Users',
currencies: 'Currencies', currencies: 'Currencies',
@@ -282,7 +282,7 @@ export default {
journal: 'Journal', journal: 'Journal',
general_ledger: 'General Ledger', general_ledger: 'General Ledger',
general_ledger_sheet: 'General Ledger Sheet', general_ledger_sheet: 'General Ledger Sheet',
profit_loss_sheet: 'Profit Loss Sheet', profit_loss_sheet: 'Profit/Loss Sheet',
expenses: 'Expenses', expenses: 'Expenses',
expenses_list: 'Expenses List', expenses_list: 'Expenses List',
new_expenses: 'New Expenses', new_expenses: 'New Expenses',
@@ -335,8 +335,8 @@ export default {
export: 'Export', export: 'Export',
accounts_with_zero_balance: 'Accounts with Zero Balance', accounts_with_zero_balance: 'Accounts with Zero Balance',
all_transactions: 'All Transactions', all_transactions: 'All Transactions',
filter_accounts: 'Filter Accounts', filter_accounts: 'Filter accounts',
calculate_report: 'Calculate Report', calculate_report: 'Calculate report',
total: 'Total', total: 'Total',
specific_accounts: 'Specific Accounts', specific_accounts: 'Specific Accounts',
trans_num: 'Trans. NUM', trans_num: 'Trans. NUM',
@@ -930,6 +930,7 @@ export default {
inactivate_item: 'Inactivate Item', inactivate_item: 'Inactivate Item',
activate_item: 'Activate Item', activate_item: 'Activate Item',
all_payments:'All Payments', all_payments:'All Payments',
hide_customizer: 'Hide Customizer',
opening_quantity_: 'Opening quantity', opening_quantity_: 'Opening quantity',
opening_average_cost: 'Opening average cost', opening_average_cost: 'Opening average cost',
opening_cost_: 'Opening cost ', opening_cost_: 'Opening cost ',

View File

@@ -127,16 +127,16 @@ export default [
}), }),
breadcrumb: 'Profit Loss Sheet', breadcrumb: 'Profit Loss Sheet',
}, },
{ // {
path: '/financial-reports/receivable-aging-summary', // path: '/financial-reports/receivable-aging-summary',
component: LazyLoader({ // component: LazyLoader({
loader: () => // loader: () =>
import( // import(
'containers/FinancialStatements/ReceivableAgingSummary/ReceivableAgingSummary' // 'containers/FinancialStatements/ReceivableAgingSummary/ReceivableAgingSummary'
), // ),
}), // }),
breadcrumb: 'Receivable Aging Summary', // breadcrumb: 'Receivable Aging Summary',
}, // },
{ {
path: `/financial-reports/journal-sheet`, path: `/financial-reports/journal-sheet`,
component: LazyLoader({ component: LazyLoader({

View File

@@ -95,7 +95,7 @@ export const fetchProfitLossSheet = ({ query }) => {
ApiService.get('/financial_statements/profit_loss_sheet', { params: query }).then((response) => { ApiService.get('/financial_statements/profit_loss_sheet', { params: query }).then((response) => {
dispatch({ dispatch({
type: t.PROFIT_LOSS_SHEET_SET, type: t.PROFIT_LOSS_SHEET_SET,
profitLoss: response.data.profitLoss, profitLoss: response.data.data,
columns: response.data.columns, columns: response.data.columns,
query: response.data.query, query: response.data.query,
}); });

View File

@@ -0,0 +1,159 @@
import { omit } from 'lodash';
export const mapBalanceSheetToTableRows = (accounts) => {
return accounts.map((account) => {
const PRIMARY_SECTIONS = ['assets', 'liability', 'equity'];
const rowTypes = [
'total_row',
...(PRIMARY_SECTIONS.indexOf(account.section_type) !== -1
? ['total_assets']
: []),
];
return {
...account,
children: mapBalanceSheetToTableRows([
...(account.children ? account.children : []),
...(account.total && account.children && account.children.length > 0
? [
{
name: `Total ${account.name}`,
row_types: rowTypes,
total: { ...account.total },
...(account.total_periods && {
total_periods: account.total_periods,
}),
},
]
: []),
]),
};
});
};
export const journalToTableRowsMapper = (journal) => {
return journal.reduce((rows, journal) => {
journal.entries.forEach((entry, index) => {
rows.push({
...entry,
rowType: index === 0 ? 'first_entry' : 'entry',
});
});
rows.push({
credit: journal.credit,
debit: journal.debit,
rowType: 'entries_total',
});
rows.push({
rowType: 'space_entry',
});
return rows;
}, []);
};
export const generalLedgerToTableRows = (accounts) => {
return accounts.reduce((tableRows, account) => {
const children = [];
children.push({
...account.opening,
rowType: 'opening_balance',
});
account.transactions.map((transaction) => {
children.push({
...transaction,
...omit(account, ['transactions']),
rowType: 'transaction',
});
});
children.push({
...account.closing,
rowType: 'closing_balance',
});
tableRows.push({
...omit(account, ['transactions']),
children,
rowType: 'account_name',
});
return tableRows;
}, []);
};
export const profitLossToTableRowsMapper = (profitLoss) => {
return [
{
name: 'Income',
total: profitLoss.income.total,
children: [
...profitLoss.income.accounts,
{
name: 'Total Income',
total: profitLoss.income.total,
total_periods: profitLoss.income.total_periods,
rowTypes: ['income_total', 'section_total', 'total'],
},
],
total_periods: profitLoss.income.total_periods,
},
{
name: 'Cost of sales',
total: profitLoss.cost_of_sales.total,
children: [
...profitLoss.cost_of_sales.accounts,
{
name: 'Total cost of sales',
total: profitLoss.cost_of_sales.total,
total_periods: profitLoss.cost_of_sales.total_periods,
rowTypes: ['cogs_total', 'section_total', 'total'],
},
],
total_periods: profitLoss.cost_of_sales.total_periods
},
{
name: 'Gross profit',
total: profitLoss.gross_profit.total,
total_periods: profitLoss.gross_profit.total_periods,
rowTypes: ['gross_total', 'section_total', 'total'],
},
{
name: 'Expenses',
total: profitLoss.expenses.total,
children: [
...profitLoss.expenses.accounts,
{
name: 'Total Expenses',
total: profitLoss.expenses.total,
total_periods: profitLoss.expenses.total_periods,
rowTypes: ['expenses_total', 'section_total', 'total'],
},
],
total_periods: profitLoss.expenses.total_periods,
},
{
name: 'Net Operating income',
total: profitLoss.operating_profit.total,
total_periods: profitLoss.income.total_periods,
rowTypes: ['net_operating_total', 'section_total', 'total'],
},
{
name: 'Other expenses',
total: profitLoss.other_expenses.total,
total_periods: profitLoss.other_expenses.total_periods,
children: [
...profitLoss.other_expenses.accounts,
{
name: 'Total other expenses',
total: profitLoss.other_expenses.total,
total_periods: profitLoss.other_expenses.total_periods,
rowTypes: ['expenses_total', 'section_total', 'total'],
},
],
},
{
name: 'Net Income',
total: profitLoss.net_income.total,
total_periods: profitLoss.net_income.total_periods,
rowTypes: ['net_income_total', 'section_total', 'total'],
},
];
};

View File

@@ -1,36 +1,41 @@
import { createReducer } from '@reduxjs/toolkit'; import { createReducer } from '@reduxjs/toolkit';
import t from 'store/types'; import t from 'store/types';
import { getFinancialSheetIndexByQuery } from './financialStatements.selectors';
import { omit } from 'lodash'; import { omit } from 'lodash';
import {
mapBalanceSheetToTableRows,
journalToTableRowsMapper,
generalLedgerToTableRows,
profitLossToTableRowsMapper
} from './financialStatements.mappers';
const initialState = { const initialState = {
balanceSheet: { balanceSheet: {
sheets: [], sheet: {},
loading: true, loading: true,
filter: true, filter: true,
refresh: false, refresh: false,
}, },
trialBalance: { trialBalance: {
sheets: [], sheet: {},
loading: true, loading: true,
filter: true, filter: true,
refresh: false, refresh: false,
}, },
generalLedger: { generalLedger: {
sheets: [], sheet: {},
loading: false, loading: false,
filter: true, filter: true,
refresh: false, refresh: false,
}, },
journal: { journal: {
sheets: [], sheet: {},
loading: false, loading: false,
tableRows: [], tableRows: [],
filter: true, filter: true,
refresh: true, refresh: true,
}, },
profitLoss: { profitLoss: {
sheets: [], sheet: {},
loading: true, loading: true,
tableRows: [], tableRows: [],
filter: true, filter: true,
@@ -44,52 +49,8 @@ const initialState = {
}, },
}; };
const mapGeneralLedgerAccountsToRows = (accounts) => {
return accounts.reduce((tableRows, account) => {
const children = [];
children.push({
...account.opening,
rowType: 'opening_balance',
});
account.transactions.map((transaction) => {
children.push({
...transaction,
...omit(account, ['transactions']),
rowType: 'transaction',
});
});
children.push({
...account.closing,
rowType: 'closing_balance',
});
tableRows.push({
...omit(account, ['transactions']),
children,
rowType: 'account_name',
});
return tableRows;
}, []);
};
const mapJournalTableRows = (journal) => {
return journal.reduce((rows, journal) => {
journal.entries.forEach((entry, index) => {
rows.push({
...entry,
rowType: index === 0 ? 'first_entry' : 'entry',
});
});
rows.push({
credit: journal.credit,
debit: journal.debit,
rowType: 'entries_total',
});
rows.push({
rowType: 'space_entry',
});
return rows;
}, []);
};
const mapContactAgingSummary = (sheet) => { const mapContactAgingSummary = (sheet) => {
const rows = []; const rows = [];
@@ -120,70 +81,6 @@ const mapContactAgingSummary = (sheet) => {
return rows; return rows;
}; };
const mapProfitLossToTableRows = (profitLoss) => {
return [
{
name: 'Income',
total: profitLoss.income.total,
children: [
...profitLoss.income.accounts,
{
name: 'Total Income',
total: profitLoss.income.total,
rowType: 'income_total',
},
],
},
{
name: 'Expenses',
total: profitLoss.expenses.total,
children: [
...profitLoss.expenses.accounts,
{
name: 'Total Expenses',
total: profitLoss.expenses.total,
rowType: 'expense_total',
},
],
},
{
name: 'Net Income',
total: profitLoss.net_income.total,
rowType: 'net_income',
},
];
};
const mapTotalToChildrenRows = (accounts) => {
return accounts.map((account) => {
return {
...account,
children: mapTotalToChildrenRows([
...(account.children ? account.children : []),
...(account.total &&
account.children &&
account.children.length > 0 &&
account.row_type !== 'total_row'
? [
{
name: `Total ${account.name}`,
row_type: 'total_row',
total: { ...account.total },
...(account.total_periods && {
total_periods: account.total_periods,
}),
},
]
: []),
]),
};
});
};
const mapBalanceSheetRows = (balanceSheet) => {
return balanceSheet.map((section) => {});
};
const financialStatementFilterToggle = (financialName, statePath) => { const financialStatementFilterToggle = (financialName, statePath) => {
return { return {
[`${financialName}_FILTER_TOGGLE`]: (state, action) => { [`${financialName}_FILTER_TOGGLE`]: (state, action) => {
@@ -194,22 +91,13 @@ const financialStatementFilterToggle = (financialName, statePath) => {
export default createReducer(initialState, { export default createReducer(initialState, {
[t.BALANCE_SHEET_STATEMENT_SET]: (state, action) => { [t.BALANCE_SHEET_STATEMENT_SET]: (state, action) => {
const index = getFinancialSheetIndexByQuery(
state.balanceSheet.sheets,
action.query,
);
const balanceSheet = { const balanceSheet = {
sheet: action.data.balanceSheet, sheet: action.data.data,
columns: Object.values(action.data.columns), columns: action.data.columns,
query: action.data.query, query: action.data.query,
tableRows: mapTotalToChildrenRows(action.data.balance_sheet), tableRows: mapBalanceSheetToTableRows(action.data.data),
}; };
if (index !== -1) { state.balanceSheet.sheet = balanceSheet;
state.balanceSheet.sheets[index] = balanceSheet;
} else {
state.balanceSheet.sheets.push(balanceSheet);
}
}, },
[t.BALANCE_SHEET_LOADING]: (state, action) => { [t.BALANCE_SHEET_LOADING]: (state, action) => {
@@ -224,19 +112,11 @@ export default createReducer(initialState, {
...financialStatementFilterToggle('BALANCE_SHEET', 'balanceSheet'), ...financialStatementFilterToggle('BALANCE_SHEET', 'balanceSheet'),
[t.TRAIL_BALANCE_STATEMENT_SET]: (state, action) => { [t.TRAIL_BALANCE_STATEMENT_SET]: (state, action) => {
const index = getFinancialSheetIndexByQuery(
state.trialBalance.sheets,
action.query,
);
const trailBalanceSheet = { const trailBalanceSheet = {
accounts: action.data.accounts, data: action.data.data,
query: action.data.query, query: action.data.query,
}; };
if (index !== -1) { state.trialBalance.sheet = trailBalanceSheet;
state.trialBalance.sheets[index] = trailBalanceSheet;
} else {
state.trialBalance.sheets.push(trailBalanceSheet);
}
}, },
[t.TRIAL_BALANCE_SHEET_LOADING]: (state, action) => { [t.TRIAL_BALANCE_SHEET_LOADING]: (state, action) => {
@@ -251,21 +131,12 @@ export default createReducer(initialState, {
...financialStatementFilterToggle('TRIAL_BALANCE', 'trialBalance'), ...financialStatementFilterToggle('TRIAL_BALANCE', 'trialBalance'),
[t.JOURNAL_SHEET_SET]: (state, action) => { [t.JOURNAL_SHEET_SET]: (state, action) => {
const index = getFinancialSheetIndexByQuery(
state.journal.sheets,
action.query,
);
const journal = { const journal = {
query: action.data.query, query: action.data.query,
journal: action.data.journal, data: action.data.data,
tableRows: mapJournalTableRows(action.data.journal), tableRows: journalToTableRowsMapper(action.data.data),
}; };
if (index !== -1) { state.journal.sheet = journal;
state.journal.sheets[index] = journal;
} else {
state.journal.sheets.push(journal);
}
}, },
[t.JOURNAL_SHEET_LOADING]: (state, action) => { [t.JOURNAL_SHEET_LOADING]: (state, action) => {
@@ -278,21 +149,12 @@ export default createReducer(initialState, {
...financialStatementFilterToggle('JOURNAL', 'journal'), ...financialStatementFilterToggle('JOURNAL', 'journal'),
[t.GENERAL_LEDGER_STATEMENT_SET]: (state, action) => { [t.GENERAL_LEDGER_STATEMENT_SET]: (state, action) => {
const index = getFinancialSheetIndexByQuery(
state.generalLedger.sheets,
action.query,
);
const generalLedger = { const generalLedger = {
query: action.data.query, query: action.data.query,
accounts: action.data.accounts, accounts: action.data.data,
tableRows: mapGeneralLedgerAccountsToRows(action.data.accounts), tableRows: generalLedgerToTableRows(action.data.data),
}; };
if (index !== -1) { state.generalLedger.sheet = generalLedger;
state.generalLedger.sheets[index] = generalLedger;
} else {
state.generalLedger.sheets.push(generalLedger);
}
}, },
[t.GENERAL_LEDGER_SHEET_LOADING]: (state, action) => { [t.GENERAL_LEDGER_SHEET_LOADING]: (state, action) => {
@@ -305,22 +167,13 @@ export default createReducer(initialState, {
...financialStatementFilterToggle('GENERAL_LEDGER', 'generalLedger'), ...financialStatementFilterToggle('GENERAL_LEDGER', 'generalLedger'),
[t.PROFIT_LOSS_SHEET_SET]: (state, action) => { [t.PROFIT_LOSS_SHEET_SET]: (state, action) => {
const index = getFinancialSheetIndexByQuery(
state.profitLoss.sheets,
action.query,
);
const profitLossSheet = { const profitLossSheet = {
query: action.query, query: action.query,
profitLoss: action.profitLoss, profitLoss: action.profitLoss,
columns: action.columns, columns: action.columns,
tableRows: mapProfitLossToTableRows(action.profitLoss), tableRows: profitLossToTableRowsMapper(action.profitLoss),
}; };
if (index !== -1) { state.profitLoss.sheet = profitLossSheet;
state.profitLoss.sheets[index] = profitLossSheet;
} else {
state.profitLoss.sheets.push(profitLossSheet);
}
}, },
[t.PROFIT_LOSS_SHEET_LOADING]: (state, action) => { [t.PROFIT_LOSS_SHEET_LOADING]: (state, action) => {
@@ -334,34 +187,34 @@ export default createReducer(initialState, {
...financialStatementFilterToggle('PROFIT_LOSS', 'profitLoss'), ...financialStatementFilterToggle('PROFIT_LOSS', 'profitLoss'),
[t.RECEIVABLE_AGING_SUMMARY_LOADING]: (state, action) => { // [t.RECEIVABLE_AGING_SUMMARY_LOADING]: (state, action) => {
const { loading } = action.payload; // const { loading } = action.payload;
state.receivableAgingSummary.loading = loading; // state.receivableAgingSummary.loading = loading;
}, // },
[t.RECEIVABLE_AGING_SUMMARY_SET]: (state, action) => { // [t.RECEIVABLE_AGING_SUMMARY_SET]: (state, action) => {
const { aging, columns, query } = action.payload; // const { aging, columns, query } = action.payload;
const index = getFinancialSheetIndexByQuery( // const index = getFinancialSheetIndexByQuery(
state.receivableAgingSummary.sheets, // state.receivableAgingSummary.sheets,
query, // query,
); // );
const receivableSheet = { // const receivableSheet = {
query, // query,
columns, // columns,
aging, // aging,
tableRows: mapContactAgingSummary(aging), // tableRows: mapContactAgingSummary(aging),
}; // };
if (index !== -1) { // if (index !== -1) {
state.receivableAgingSummary[index] = receivableSheet; // state.receivableAgingSummary[index] = receivableSheet;
} else { // } else {
state.receivableAgingSummary.sheets.push(receivableSheet); // state.receivableAgingSummary.sheets.push(receivableSheet);
} // }
}, // },
[t.RECEIVABLE_AGING_SUMMARY_REFRESH]: (state, action) => { // [t.RECEIVABLE_AGING_SUMMARY_REFRESH]: (state, action) => {
const { refresh } = action.payload; // const { refresh } = action.payload;
state.receivableAgingSummary.refresh = !!refresh; // state.receivableAgingSummary.refresh = !!refresh;
}, // },
...financialStatementFilterToggle( ...financialStatementFilterToggle(
'RECEIVABLE_AGING_SUMMARY', 'RECEIVABLE_AGING_SUMMARY',
'receivableAgingSummary', 'receivableAgingSummary',

View File

@@ -1,17 +1,14 @@
import {getObjectDiff} from 'utils'; import { createSelector } from 'reselect';
import { camelCase } from 'lodash';
const transformSheetType = (sheetType) => {
return camelCase(sheetType);
};
// Financial Statements selectors. // Financial Statements selectors.
export const sheetByTypeSelector = (sheetType) => (state, props) => {
/** const sheetName = transformSheetType(sheetType);
* Retrieve financial statement sheet by the given query. return state.financialStatements[sheetName].sheet;
* @param {array} sheets
* @param {object} query
*/
export const getFinancialSheetIndexByQuery = (sheets, query) => {
return sheets.findIndex(balanceSheet => (
getObjectDiff(query, balanceSheet.query).length === 0
));
}; };
/** /**
@@ -19,38 +16,56 @@ export const getFinancialSheetIndexByQuery = (sheets, query) => {
* @param {array} sheets * @param {array} sheets
* @param {number} index * @param {number} index
*/ */
export const getFinancialSheet = (sheets, index) => { export const getFinancialSheetFactory = (sheetType) =>
return (typeof sheets[index] !== 'undefined') ? sheets[index] : null; createSelector(
}; sheetByTypeSelector(sheetType),
(sheet) => {
return sheet;
},
);
/** /**
* Retrieve financial statement columns by the given sheet index. * Retrieve financial statement columns by the given sheet index.
* @param {array} sheets * @param {array} sheets
* @param {number} index * @param {number} index
*/ */
export const getFinancialSheetColumns = (sheets, index) => { export const getFinancialSheetColumnsFactory = (sheetType) =>
const sheet = getFinancialSheet(sheets, index); createSelector(
sheetByTypeSelector(sheetType),
(sheet) => {
return (sheet && sheet.columns) ? sheet.columns : []; return (sheet && sheet.columns) ? sheet.columns : [];
}; },
);
/** /**
* Retrieve financial statement query by the given sheet index. * Retrieve financial statement query by the given sheet index.
* @param {array} sheets
* @param {number} index
*/ */
export const getFinancialSheetQuery = (sheets, index) => { export const getFinancialSheetQueryFactory = (sheetType) =>
const sheet = getFinancialSheet(sheets, index); createSelector(
sheetByTypeSelector(sheetType),
(sheet) => {
return (sheet && sheet.query) ? sheet.query : {}; return (sheet && sheet.query) ? sheet.query : {};
}; },
);
/**
export const getFinancialSheetAccounts = (sheets, index) => { * Retrieve financial statement accounts by the given sheet index.
const sheet = getFinancialSheet(sheets, index); */
export const getFinancialSheetAccountsFactory = (sheetType) =>
createSelector(
sheetByTypeSelector(sheetType),
(sheet) => {
return (sheet && sheet.accounts) ? sheet.accounts : []; return (sheet && sheet.accounts) ? sheet.accounts : [];
}; }
);
/**
export const getFinancialSheetTableRows = (sheets, index) => { * Retrieve financial statement table rows by the given sheet index.
const sheet = getFinancialSheet(sheets, index); */
export const getFinancialSheetTableRowsFactory = (sheetType) =>
createSelector(
sheetByTypeSelector(sheetType),
(sheet) => {
return (sheet && sheet.tableRows) ? sheet.tableRows : []; return (sheet && sheet.tableRows) ? sheet.tableRows : [];
}; }
);

View File

@@ -117,6 +117,9 @@ $button-background-color-hover: #CFDCEE !default;
body.authentication { body.authentication {
background-color: #fcfdff; background-color: #fcfdff;
} }
body.hide-scrollbar .Pane2{
overflow: hidden;
}
.bp3-toast { .bp3-toast {
box-shadow: none; box-shadow: none;

View File

@@ -296,6 +296,12 @@
.tbody{ .tbody{
.tr .td{ .tr .td{
border-bottom: 0;
}
.tr:not(:first-child) .td{
border-top: 1px dotted #CCC;
}
.tr:last-child .td{
border-bottom: 1px dotted #CCC; border-bottom: 1px dotted #CCC;
} }
} }

View File

@@ -6,22 +6,63 @@
.financial-statement{ .financial-statement{
&__header{ &__header{
}
&__body{
padding-left: 15px;
padding-right: 15px;
display: flex;
justify-content: center;
align-items: center;
}
}
.financial-header-drawer{
padding: 25px 26px 25px; padding: 25px 26px 25px;
background: #FDFDFD; position: absolute;
top: 101px;
bottom: 0;
left: 0;
right: 0;
overflow: hidden;
&.is-hidden{ &.is-hidden{
display: none; visibility: hidden;
} }
.bp3-form-group, .row{
.radio-group---accounting-basis{ .col{
max-width: 400px;
min-width: 250px;
}
}
.bp3-label{ .bp3-drawer{
font-weight: 500; box-shadow: 0 0 0 transparent;
font-size: 13px; max-height: 550px;
color: #444; height: 100%;
padding-bottom: 49px;
> form{
display: flex;
flex-direction: column;
flex: 1 0 0;
height: 100%;
}
.bp3-drawer-backdrop{
background-color: rgba(2, 9, 19, 0.65);
} }
} }
.bp3-form-group{
margin-bottom: 22px;
label.bp3-label{
margin-bottom: 6px;
}
}
.bp3-button.button--submit-filter{ .bp3-button.button--submit-filter{
min-height: 34px; min-height: 34px;
padding-left: 16px; padding-left: 16px;
@@ -32,34 +73,89 @@
margin-bottom: 12px; margin-bottom: 12px;
} }
} }
.bp3-tabs{
height: 100%;
&.bp3-vertical > .bp3-tab-panel{
flex: 1 0 0;
border-top: 24px solid transparent;
padding-left: 20px;
padding-right: 20px;
padding-bottom: 24px;
overflow: auto;
}
} }
&__body{ .bp3-tabs.bp3-vertical{
padding-left: 15px; flex: 1 0 0;
padding-right: 15px;
display: flex; .bp3-tab-list{
justify-content: center; width: 220px;
align-items: center; border-right: 1px solid #c3cdd5;
padding-top: 10px;
> *:not(:last-child){
margin-right: 0;
}
.bp3-tab-indicator-wrapper{
width: 100%;
.bp3-tab-indicator{
border-left: 3px solid #0350f8;
background-color: #edf5ff;
border-radius: 0;
}
} }
&__header.is-hidden + .financial-statement__body{ .bp3-tab{
.financial-sheet{ color: #333;
margin-top: 40px; line-height: 45px;
border-radius: 0;
padding-left: 14px;
padding-right: 14px;
font-weight: 500;
}
}
}
.bp3-tab-panel{
}
&__footer{
background-color: #ecf0f3;
border-top: 1px solid #c3cdd5;
padding: 8px;
padding-left: 230px;
position: absolute;
bottom: 0;
width: 100%;
}
.row{
margin-left: -0.85rem;
margin-right: -0.85rem;
.col{
padding-left: 0.85rem;
padding-right: 0.85rem;
} }
} }
} }
.financial-sheet{ .financial-sheet{
border: 2px solid #F1F1F1; border: 2px solid #EBEBEB;
border-radius: 10px; border-radius: 10px;
min-width: 640px; min-width: 640px;
width: auto; width: auto;
padding: 30px 18px; padding: 30px 18px;
max-width: 100%; max-width: 100%;
margin: 15px auto 35px; margin: 35px auto;
min-height: 400px; min-height: 400px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background: #fff;
&__title{ &__title{
margin: 0; margin: 0;
@@ -103,7 +199,6 @@
} }
} }
} }
&__inner{ &__inner{
&.is-loading{ &.is-loading{
display: none; display: none;
@@ -113,8 +208,8 @@
color: #888; color: #888;
text-align: center; text-align: center;
margin-top: auto; margin-top: auto;
padding-top: 16px; padding-top: 18px;
font-size: 12px; font-size: 13px;
} }
.dashboard__loading-indicator{ .dashboard__loading-indicator{
margin: auto; margin: auto;
@@ -143,7 +238,13 @@
&--opening_balance, &--opening_balance,
&--closing_balance{ &--closing_balance{
.td{ .td{
background-color: #fbfbfb; border-top: 1px solid #333;
}
.name,
.amount,
.balance{
font-weight: 500;
} }
} }
@@ -185,18 +286,36 @@
&--profit-loss-sheet{ &--profit-loss-sheet{
.financial-sheet__table{ .financial-sheet__table{
.thead,
.tbody{ .tbody{
.total.td { .tr .td:not(:first-child),
border-bottom-color: #000; .tr .th:not(:first-child) {
justify-content: flex-end;
}
}
.tbody{
.tr .td:not(:first-child) {
border-top-color: #000;
}
.tr.row_type--total{
font-weight: 500;
}
.tr.row_type--section_total .td{
border-top: 1px solid #BBB
}
.tr.row_type--section_total + .tr .td{
border-top: 1px solid #666;
}
.tr.row_type--section_total:last-child .td{
border-bottom: 1px solid #666;
} }
.row--income_total, .tr.is-expanded{
.row--expense_total, .td.total,
.row--net_income{ .td.total-period{
font-weight: 600; > span{
display: none;
.total.td{ }
border-bottom-color: #555;
} }
} }
} }
@@ -205,13 +324,28 @@
&--balance-sheet{ &--balance-sheet{
.financial-sheet__table{ .financial-sheet__table{
.thead,
.tbody{ .tbody{
.total.td{ .tr .td.account_name ~ .td,
border-bottom-color: #000; .tr .th.account_name ~ .th{
justify-content: flex-end;
}
}
.tbody{
.tr .total.td{
border-top-color: #000;
}
.tr.row_type--total_row .td{
border-top: 1px solid #BBB;
}
.tr.row_type--total_assets + .tr .td{
border-top: 1px solid #666;
} }
.tr.row_type--total_row{ .tr.row_type--total_row{
.total.td, .total.td,
.account_name.td{ .account_name.td,
.total-period.td{
font-weight: 600; font-weight: 600;
color: #333; color: #333;
} }
@@ -267,6 +401,7 @@
&.is-full-width{ &.is-full-width{
width: 100%; width: 100%;
margin-top: 25px;
} }
} }
@@ -310,3 +445,13 @@
} }
} }
} }
.financial-statement--journal{
.financial-header-drawer{
.bp3-drawer{
max-height: 350px;
}
}
}

View File

@@ -122,24 +122,22 @@ export const parseDateRangeQuery = (keyword) => {
const query = queries[keyword]; const query = queries[keyword];
return { return {
from_date: moment().startOf(query.range).toDate(), fromDate: moment().startOf(query.range).toDate(),
to_date: moment().endOf(query.range).toDate(), toDate: moment().endOf(query.range).toDate(),
}; };
}; };
export const defaultExpanderReducer = (tableRows, level) => { export const defaultExpanderReducer = (tableRows, level) => {
let currentLevel = 1;
const expended = []; const expended = [];
const walker = (rows, parentIndex = null) => { const walker = (rows, parentIndex = null, currentLevel = 1) => {
return rows.forEach((row, index) => { return rows.forEach((row, index) => {
const _index = parentIndex ? `${parentIndex}.${index}` : `${index}`; const _index = parentIndex ? `${parentIndex}.${index}` : `${index}`;
expended[_index] = true; expended[_index] = true;
if (row.children && currentLevel < level) { if (row.children && currentLevel < level) {
walker(row.children, _index); walker(row.children, _index, currentLevel + 1);
} }
currentLevel++;
}, {}); }, {});
}; };
walker(tableRows); walker(tableRows);
@@ -372,3 +370,21 @@ export function defaultToTransform(
export function isBlank(value) { export function isBlank(value) {
return _.isEmpty(value) && !_.isNumber(value) || _.isNaN(value); return _.isEmpty(value) && !_.isNumber(value) || _.isNaN(value);
} }
export const getColumnWidth = (
rows,
accessor,
{ maxWidth, minWidth, magicSpacing = 14 },
) => {
const cellLength = Math.max(
...rows.map((row) => (`${_.get(row, accessor)}` || '').length),
);
let result = cellLength * magicSpacing;
result = minWidth ? Math.max(minWidth, result) : result;
result = maxWidth ? Math.min(maxWidth, result) : result;
return result;
};

View File

@@ -67,7 +67,7 @@ export default class JournalSheetController extends BaseController {
const baseCurrency = settings.get({ group: 'organization', key: 'base_currency' }); const baseCurrency = settings.get({ group: 'organization', key: 'base_currency' });
try { try {
const data = await this.journalService.journalSheet(tenantId, filter); const { data, query } = await this.journalService.journalSheet(tenantId, filter);
return res.status(200).send({ return res.status(200).send({
organization_name: organizationName, organization_name: organizationName,

View File

@@ -1,6 +1,6 @@
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import { Router, Request, Response, NextFunction } from 'express'; import { Router, Request, Response, NextFunction } from 'express';
import { check, param, query, ValidationChain } from 'express-validator'; import { check, param, body, query, ValidationChain } from 'express-validator';
import asyncMiddleware from 'api/middleware/asyncMiddleware'; import asyncMiddleware from 'api/middleware/asyncMiddleware';
import ItemsService from 'services/Items/ItemsService'; import ItemsService from 'services/Items/ItemsService';
import BaseController from 'api/controllers/BaseController'; import BaseController from 'api/controllers/BaseController';
@@ -23,70 +23,62 @@ export default class ItemsController extends BaseController {
router() { router() {
const router = Router(); const router = Router();
router.post('/', [ router.post(
...this.validateItemSchema, '/',
...this.validateNewItemSchema, [...this.validateItemSchema, ...this.validateNewItemSchema],
],
this.validationResult, this.validationResult,
asyncMiddleware(this.newItem.bind(this)), asyncMiddleware(this.newItem.bind(this)),
this.handlerServiceErrors, this.handlerServiceErrors
); );
router.post( router.post(
'/:id/activate', [ '/:id/activate',
...this.validateSpecificItemSchema, [...this.validateSpecificItemSchema],
],
this.validationResult, this.validationResult,
asyncMiddleware(this.activateItem.bind(this)), asyncMiddleware(this.activateItem.bind(this)),
this.handlerServiceErrors this.handlerServiceErrors
); );
router.post( router.post(
'/:id/inactivate', [ '/:id/inactivate',
...this.validateSpecificItemSchema, [...this.validateSpecificItemSchema],
],
this.validationResult, this.validationResult,
asyncMiddleware(this.inactivateItem.bind(this)), asyncMiddleware(this.inactivateItem.bind(this)),
this.handlerServiceErrors, this.handlerServiceErrors
) );
router.post( router.post(
'/:id', [ '/:id',
...this.validateItemSchema, [...this.validateItemSchema, ...this.validateSpecificItemSchema],
...this.validateSpecificItemSchema,
],
this.validationResult, this.validationResult,
asyncMiddleware(this.editItem.bind(this)), asyncMiddleware(this.editItem.bind(this)),
this.handlerServiceErrors, this.handlerServiceErrors
); );
router.delete('/', [ router.delete(
...this.validateBulkSelectSchema, '/',
], [...this.validateBulkSelectSchema],
this.validationResult, this.validationResult,
asyncMiddleware(this.bulkDeleteItems.bind(this)), asyncMiddleware(this.bulkDeleteItems.bind(this)),
this.handlerServiceErrors this.handlerServiceErrors
); );
router.delete( router.delete(
'/:id', [ '/:id',
...this.validateSpecificItemSchema, [...this.validateSpecificItemSchema],
],
this.validationResult, this.validationResult,
asyncMiddleware(this.deleteItem.bind(this)), asyncMiddleware(this.deleteItem.bind(this)),
this.handlerServiceErrors, this.handlerServiceErrors
); );
router.get( router.get(
'/:id', [ '/:id',
...this.validateSpecificItemSchema, [...this.validateSpecificItemSchema],
],
this.validationResult, this.validationResult,
asyncMiddleware(this.getItem.bind(this)), asyncMiddleware(this.getItem.bind(this)),
this.handlerServiceErrors, this.handlerServiceErrors
); );
router.get( router.get(
'/', [ '/',
...this.validateListQuerySchema, [...this.validateListQuerySchema],
],
this.validationResult, this.validationResult,
asyncMiddleware(this.getItemsList.bind(this)), asyncMiddleware(this.getItemsList.bind(this)),
this.dynamicListService.handlerErrorsToResponse, this.dynamicListService.handlerErrorsToResponse,
this.handlerServiceErrors, this.handlerServiceErrors
); );
return router; return router;
} }
@@ -97,8 +89,21 @@ export default class ItemsController extends BaseController {
get validateNewItemSchema(): ValidationChain[] { get validateNewItemSchema(): ValidationChain[] {
return [ return [
check('opening_quantity').default(0).isInt({ min: 0 }).toInt(), check('opening_quantity').default(0).isInt({ min: 0 }).toInt(),
check('opening_cost').optional({ nullable: true }).isFloat({ min: 0 }).toFloat(), check('opening_cost')
check('opening_date').optional({ nullable: true }).isISO8601(), .if(body('opening_quantity').exists().isInt({ min: 1 }))
.exists()
.isFloat(),
check('opening_cost')
.optional({ nullable: true })
.isFloat({ min: 0 })
.toFloat(),
check('opening_date')
.if(
body('opening_quantity').exists().isFloat({ min: 1 }) ||
body('opening_cost').exists().isFloat({ min: 1 })
)
.exists(),
check('opening_date').optional({ nullable: true }).isISO8601().toDate(),
]; ];
} }
@@ -107,8 +112,12 @@ export default class ItemsController extends BaseController {
*/ */
get validateItemSchema(): ValidationChain[] { get validateItemSchema(): ValidationChain[] {
return [ return [
check('name').exists().isString().isLength({ max: DATATYPES_LENGTH.STRING }), check('name')
check('type').exists() .exists()
.isString()
.isLength({ max: DATATYPES_LENGTH.STRING }),
check('type')
.exists()
.isString() .isString()
.trim() .trim()
.escape() .escape()
@@ -127,12 +136,11 @@ export default class ItemsController extends BaseController {
.toFloat() .toFloat()
.if(check('purchasable').equals('true')) .if(check('purchasable').equals('true'))
.exists(), .exists(),
check('cost_account_id').if(check('purchasable').equals('true')).exists(),
check('cost_account_id') check('cost_account_id')
.optional({ nullable: true }) .optional({ nullable: true })
.isInt({ min: 0, max: DATATYPES_LENGTH.INT_10 }) .isInt({ min: 0, max: DATATYPES_LENGTH.INT_10 })
.toInt() .toInt(),
.if(check('purchasable').equals('true'))
.exists(),
// Sell attributes. // Sell attributes.
check('sellable').optional().isBoolean().toBoolean(), check('sellable').optional().isBoolean().toBoolean(),
check('sell_price') check('sell_price')
@@ -141,18 +149,18 @@ export default class ItemsController extends BaseController {
.toFloat() .toFloat()
.if(check('sellable').equals('true')) .if(check('sellable').equals('true'))
.exists(), .exists(),
check('sell_account_id').if(check('sellable').equals('true')).exists(),
check('sell_account_id') check('sell_account_id')
.optional({ nullable: true }) .optional({ nullable: true })
.isInt({ min: 0, max: DATATYPES_LENGTH.INT_10 }) .isInt({ min: 0, max: DATATYPES_LENGTH.INT_10 })
.toInt() .toInt(),
.if(check('sellable').equals('true')) check('inventory_account_id')
.if(check('type').equals('inventory'))
.exists(), .exists(),
check('inventory_account_id') check('inventory_account_id')
.optional({ nullable: true }) .optional({ nullable: true })
.isInt({ min: 0, max: DATATYPES_LENGTH.INT_10 }) .isInt({ min: 0, max: DATATYPES_LENGTH.INT_10 })
.toInt() .toInt(),
.if(check('type').equals('inventory'))
.exists(),
check('sell_description') check('sell_description')
.optional({ nullable: true }) .optional({ nullable: true })
.isString() .isString()
@@ -187,9 +195,7 @@ export default class ItemsController extends BaseController {
* @return {ValidationChain[]} * @return {ValidationChain[]}
*/ */
get validateSpecificItemSchema(): ValidationChain[] { get validateSpecificItemSchema(): ValidationChain[] {
return [ return [param('id').exists().isNumeric().toInt()];
param('id').exists().isNumeric().toInt(),
];
} }
/** /**
@@ -216,7 +222,7 @@ export default class ItemsController extends BaseController {
query('custom_view_id').optional().isNumeric().toInt(), query('custom_view_id').optional().isNumeric().toInt(),
query('stringified_filter_roles').optional().isJSON(), query('stringified_filter_roles').optional().isJSON(),
] ];
} }
/** /**
@@ -336,7 +342,7 @@ export default class ItemsController extends BaseController {
} catch (error) { } catch (error) {
console.log(error); console.log(error);
next(error) next(error);
} }
} }
@@ -362,7 +368,7 @@ export default class ItemsController extends BaseController {
const { const {
items, items,
pagination, pagination,
filterMeta filterMeta,
} = await this.itemsService.itemsList(tenantId, filter); } = await this.itemsService.itemsList(tenantId, filter);
return res.status(200).send({ return res.status(200).send({
@@ -404,7 +410,12 @@ export default class ItemsController extends BaseController {
* @param {Response} res * @param {Response} res
* @param {NextFunction} next * @param {NextFunction} next
*/ */
handlerServiceErrors(error: Error, req: Request, res: Response, next: NextFunction) { handlerServiceErrors(
error: Error,
req: Request,
res: Response,
next: NextFunction
) {
if (error instanceof ServiceError) { if (error instanceof ServiceError) {
if (error.errorType === 'NOT_FOUND') { if (error.errorType === 'NOT_FOUND') {
return res.status(400).send({ return res.status(400).send({
@@ -479,7 +490,7 @@ export default class ItemsController extends BaseController {
if (error.errorType === 'ITEM_HAS_ASSOCIATED_TRANSACTINS') { if (error.errorType === 'ITEM_HAS_ASSOCIATED_TRANSACTINS') {
return res.status(400).send({ return res.status(400).send({
errors: [{ type: 'ITEM_HAS_ASSOCIATED_TRANSACTINS', code: 320 }], errors: [{ type: 'ITEM_HAS_ASSOCIATED_TRANSACTINS', code: 320 }],
}) });
} }
} }
next(error); next(error);

View File

@@ -7,7 +7,6 @@ import asyncMiddleware from 'api/middleware/asyncMiddleware';
import PaymentReceiveService from 'services/Sales/PaymentsReceives'; import PaymentReceiveService from 'services/Sales/PaymentsReceives';
import DynamicListingService from 'services/DynamicListing/DynamicListService'; import DynamicListingService from 'services/DynamicListing/DynamicListService';
import { ServiceError } from 'exceptions'; import { ServiceError } from 'exceptions';
import HasItemEntries from 'services/Sales/HasItemsEntries';
/** /**
* Payments receives controller. * Payments receives controller.
@@ -32,29 +31,28 @@ export default class PaymentReceivesController extends BaseController {
this.editPaymentReceiveValidation, this.editPaymentReceiveValidation,
this.validationResult, this.validationResult,
asyncMiddleware(this.editPaymentReceive.bind(this)), asyncMiddleware(this.editPaymentReceive.bind(this)),
this.handleServiceErrors, this.handleServiceErrors
); );
router.post( router.post(
'/', [ '/',
...this.newPaymentReceiveValidation, [...this.newPaymentReceiveValidation],
],
this.validationResult, this.validationResult,
asyncMiddleware(this.newPaymentReceive.bind(this)), asyncMiddleware(this.newPaymentReceive.bind(this)),
this.handleServiceErrors, this.handleServiceErrors
); );
router.get( router.get(
'/:id/invoices', '/:id/invoices',
this.paymentReceiveValidation, this.paymentReceiveValidation,
this.validationResult, this.validationResult,
asyncMiddleware(this.getPaymentReceiveInvoices.bind(this)), asyncMiddleware(this.getPaymentReceiveInvoices.bind(this)),
this.handleServiceErrors, this.handleServiceErrors
); );
router.get( router.get(
'/:id', '/:id',
this.paymentReceiveValidation, this.paymentReceiveValidation,
this.validationResult, this.validationResult,
asyncMiddleware(this.getPaymentReceive.bind(this)), asyncMiddleware(this.getPaymentReceive.bind(this)),
this.handleServiceErrors, this.handleServiceErrors
); );
router.get( router.get(
'/', '/',
@@ -62,14 +60,14 @@ export default class PaymentReceivesController extends BaseController {
this.validationResult, this.validationResult,
asyncMiddleware(this.getPaymentReceiveList.bind(this)), asyncMiddleware(this.getPaymentReceiveList.bind(this)),
this.handleServiceErrors, this.handleServiceErrors,
this.dynamicListService.handlerErrorsToResponse, this.dynamicListService.handlerErrorsToResponse
); );
router.delete( router.delete(
'/:id', '/:id',
this.paymentReceiveValidation, this.paymentReceiveValidation,
this.validationResult, this.validationResult,
asyncMiddleware(this.deletePaymentReceive.bind(this)), asyncMiddleware(this.deletePaymentReceive.bind(this)),
this.handleServiceErrors, this.handleServiceErrors
); );
return router; return router;
} }
@@ -105,7 +103,7 @@ export default class PaymentReceivesController extends BaseController {
query('sort_order').optional().isIn(['desc', 'asc']), query('sort_order').optional().isIn(['desc', 'asc']),
query('page').optional().isNumeric().toInt(), query('page').optional().isNumeric().toInt(),
query('page_size').optional().isNumeric().toInt(), query('page_size').optional().isNumeric().toInt(),
] ];
} }
/** /**
@@ -144,10 +142,9 @@ export default class PaymentReceivesController extends BaseController {
const paymentReceive: IPaymentReceiveDTO = this.matchedBodyData(req); const paymentReceive: IPaymentReceiveDTO = this.matchedBodyData(req);
try { try {
const storedPaymentReceive = await this.paymentReceiveService const storedPaymentReceive = await this.paymentReceiveService.createPaymentReceive(
.createPaymentReceive(
tenantId, tenantId,
paymentReceive, paymentReceive
); );
return res.status(200).send({ return res.status(200).send({
id: storedPaymentReceive.id, id: storedPaymentReceive.id,
@@ -172,11 +169,13 @@ export default class PaymentReceivesController extends BaseController {
try { try {
await this.paymentReceiveService.editPaymentReceive( await this.paymentReceiveService.editPaymentReceive(
tenantId, paymentReceiveId, paymentReceive, tenantId,
paymentReceiveId,
paymentReceive
); );
return res.status(200).send({ return res.status(200).send({
id: paymentReceiveId, id: paymentReceiveId,
message: 'The payment receive has been edited successfully.' message: 'The payment receive has been edited successfully.',
}); });
} catch (error) { } catch (error) {
next(error); next(error);
@@ -193,7 +192,10 @@ export default class PaymentReceivesController extends BaseController {
const { id: paymentReceiveId } = req.params; const { id: paymentReceiveId } = req.params;
try { try {
await this.paymentReceiveService.deletePaymentReceive(tenantId, paymentReceiveId); await this.paymentReceiveService.deletePaymentReceive(
tenantId,
paymentReceiveId
);
return res.status(200).send({ return res.status(200).send({
id: paymentReceiveId, id: paymentReceiveId,
@@ -220,7 +222,8 @@ export default class PaymentReceivesController extends BaseController {
receivableInvoices, receivableInvoices,
paymentReceiveInvoices, paymentReceiveInvoices,
} = await this.paymentReceiveService.getPaymentReceive( } = await this.paymentReceiveService.getPaymentReceive(
tenantId, paymentReceiveId tenantId,
paymentReceiveId
); );
return res.status(200).send({ return res.status(200).send({
@@ -239,13 +242,18 @@ export default class PaymentReceivesController extends BaseController {
* @param {Response} res * @param {Response} res
* @param {NextFunction} next * @param {NextFunction} next
*/ */
async getPaymentReceiveInvoices(req: Request, res: Response, next: NextFunction) { async getPaymentReceiveInvoices(
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId } = req; const { tenantId } = req;
const { id: paymentReceiveId } = req.params; const { id: paymentReceiveId } = req.params;
try { try {
const invoices = await this.paymentReceiveService.getPaymentReceiveInvoices( const invoices = await this.paymentReceiveService.getPaymentReceiveInvoices(
tenantId, paymentReceiveId, tenantId,
paymentReceiveId
); );
return res.status(200).send({ sale_invoices: invoices }); return res.status(200).send({ sale_invoices: invoices });
} catch (error) { } catch (error) {
@@ -278,7 +286,10 @@ export default class PaymentReceivesController extends BaseController {
paymentReceives, paymentReceives,
pagination, pagination,
filterMeta, filterMeta,
} = await this.paymentReceiveService.listPaymentReceives(tenantId, filter); } = await this.paymentReceiveService.listPaymentReceives(
tenantId,
filter
);
return res.status(200).send({ return res.status(200).send({
payment_receives: paymentReceives, payment_receives: paymentReceives,
@@ -297,7 +308,12 @@ export default class PaymentReceivesController extends BaseController {
* @param res * @param res
* @param next * @param next
*/ */
handleServiceErrors(error: Error, req: Request, res: Response, next: NextFunction) { handleServiceErrors(
error: Error,
req: Request,
res: Response,
next: NextFunction
) {
if (error instanceof ServiceError) { if (error instanceof ServiceError) {
if (error.errorType === 'DEPOSIT_ACCOUNT_NOT_FOUND') { if (error.errorType === 'DEPOSIT_ACCOUNT_NOT_FOUND') {
return res.boom.badRequest(null, { return res.boom.badRequest(null, {
@@ -316,7 +332,9 @@ export default class PaymentReceivesController extends BaseController {
} }
if (error.errorType === 'DEPOSIT_ACCOUNT_NOT_CURRENT_ASSET_TYPE') { if (error.errorType === 'DEPOSIT_ACCOUNT_NOT_CURRENT_ASSET_TYPE') {
return res.boom.badRequest(null, { return res.boom.badRequest(null, {
errors: [{ type: 'DEPOSIT_ACCOUNT_NOT_CURRENT_ASSET_TYPE', code: 300 }], errors: [
{ type: 'DEPOSIT_ACCOUNT_NOT_CURRENT_ASSET_TYPE', code: 300 },
],
}); });
} }
if (error.errorType === 'INVALID_PAYMENT_AMOUNT_INVALID') { if (error.errorType === 'INVALID_PAYMENT_AMOUNT_INVALID') {
@@ -346,14 +364,17 @@ export default class PaymentReceivesController extends BaseController {
} }
if (error.errorType === 'INVOICES_NOT_DELIVERED_YET') { if (error.errorType === 'INVOICES_NOT_DELIVERED_YET') {
return res.boom.badRequest(null, { return res.boom.badRequest(null, {
errors: [{ errors: [
type: 'INVOICES_NOT_DELIVERED_YET', code: 200, {
type: 'INVOICES_NOT_DELIVERED_YET',
code: 200,
data: { data: {
not_delivered_invoices_ids: error.payload.notDeliveredInvoices.map( not_delivered_invoices_ids: error.payload.notDeliveredInvoices.map(
(invoice) => invoice.id (invoice) => invoice.id
) ),
} },
}], },
],
}); });
} }
} }

View File

@@ -23,6 +23,7 @@ export interface ITrialBalanceAccount {
credit: number, credit: number,
debit: number, debit: number,
balance: number, balance: number,
currencyCode: string,
formattedCredit: string, formattedCredit: string,
formattedDebit: string, formattedDebit: string,

View File

@@ -9,10 +9,7 @@ import {
IJournalPoster, IJournalPoster,
IAccountType, IAccountType,
} from 'interfaces'; } from 'interfaces';
import { import { dateRangeCollection, flatToNestedArray } from 'utils';
dateRangeCollection,
flatToNestedArray,
} from 'utils';
import BalanceSheetStructure from 'data/BalanceSheetStructure'; import BalanceSheetStructure from 'data/BalanceSheetStructure';
import FinancialSheet from '../FinancialSheet'; import FinancialSheet from '../FinancialSheet';
@@ -37,7 +34,7 @@ export default class BalanceSheetStatement extends FinancialSheet {
query: IBalanceSheetQuery, query: IBalanceSheetQuery,
accounts: IAccount & { type: IAccountType }[], accounts: IAccount & { type: IAccountType }[],
journalFinancial: IJournalPoster, journalFinancial: IJournalPoster,
baseCurrency: string, baseCurrency: string
) { ) {
super(); super();
@@ -48,9 +45,8 @@ export default class BalanceSheetStatement extends FinancialSheet {
this.journalFinancial = journalFinancial; this.journalFinancial = journalFinancial;
this.baseCurrency = baseCurrency; this.baseCurrency = baseCurrency;
this.comparatorDateType = query.displayColumnsType === 'total' this.comparatorDateType =
? 'day' query.displayColumnsType === 'total' ? 'day' : query.displayColumnsBy;
: query.displayColumnsBy;
this.initDateRangeCollection(); this.initDateRangeCollection();
} }
@@ -73,20 +69,24 @@ export default class BalanceSheetStatement extends FinancialSheet {
* @param {IBalanceSheetSection[]} sections - * @param {IBalanceSheetSection[]} sections -
* @return {IBalanceSheetAccountTotal} * @return {IBalanceSheetAccountTotal}
*/ */
private getSectionTotal(sections: IBalanceSheetSection[]): IBalanceSheetAccountTotal { private getSectionTotal(
sections: IBalanceSheetSection[]
): IBalanceSheetAccountTotal {
const amount = sumBy(sections, 'total.amount'); const amount = sumBy(sections, 'total.amount');
const formattedAmount = this.formatNumber(amount); const formattedAmount = this.formatNumber(amount);
const currencyCode = this.baseCurrency; const currencyCode = this.baseCurrency;
return { amount, formattedAmount, currencyCode }; return { amount, formattedAmount, currencyCode };
}; }
/** /**
* Retrieve accounts total periods. * Retrieve accounts total periods.
* @param {IBalanceSheetAccount[]} sections - * @param {IBalanceSheetAccount[]} sections -
* @return {IBalanceSheetAccountTotal[]} * @return {IBalanceSheetAccountTotal[]}
*/ */
private getSectionTotalPeriods(sections: IBalanceSheetAccount[]): IBalanceSheetAccountTotal[] { private getSectionTotalPeriods(
sections: Array<IBalanceSheetAccount|IBalanceSheetSection>
): IBalanceSheetAccountTotal[] {
return this.dateRangeSet.map((date, index) => { return this.dateRangeSet.map((date, index) => {
const amount = sumBy(sections, `totalPeriods[${index}].amount`); const amount = sumBy(sections, `totalPeriods[${index}].amount`);
const formattedAmount = this.formatNumber(amount); const formattedAmount = this.formatNumber(amount);
@@ -101,7 +101,9 @@ export default class BalanceSheetStatement extends FinancialSheet {
* @param {IAccount} account * @param {IAccount} account
* @return {IBalanceSheetAccountTotal[]} * @return {IBalanceSheetAccountTotal[]}
*/ */
private getAccountTotalPeriods (account: IAccount): IBalanceSheetAccountTotal[] { private getAccountTotalPeriods(
account: IAccount
): IBalanceSheetAccountTotal[] {
return this.dateRangeSet.map((date) => { return this.dateRangeSet.map((date) => {
const amount = this.journalFinancial.getAccountBalance( const amount = this.journalFinancial.getAccountBalance(
account.id, account.id,
@@ -124,19 +126,20 @@ export default class BalanceSheetStatement extends FinancialSheet {
private balanceSheetAccountMapper(account: IAccount): IBalanceSheetAccount { private balanceSheetAccountMapper(account: IAccount): IBalanceSheetAccount {
// Calculates the closing balance of the given account in the specific date point. // Calculates the closing balance of the given account in the specific date point.
const amount = this.journalFinancial.getAccountBalance( const amount = this.journalFinancial.getAccountBalance(
account.id, this.query.toDate, account.id,
this.query.toDate
); );
const formattedAmount = this.formatNumber(amount); const formattedAmount = this.formatNumber(amount);
// Retrieve all entries that associated to the given account. // Retrieve all entries that associated to the given account.
const entries = this.journalFinancial.getAccountEntries(account.id) const entries = this.journalFinancial.getAccountEntries(account.id);
return { return {
...pick(account, ['id', 'index', 'name', 'code', 'parentAccountId']), ...pick(account, ['id', 'index', 'name', 'code', 'parentAccountId']),
type: 'account', type: 'account',
hasTransactions: entries.length > 0, hasTransactions: entries.length > 0,
// Total date periods. // Total date periods.
...this.query.displayColumnsType === 'date_periods' && ({ ...(this.query.displayColumnsType === 'date_periods' && {
totalPeriods: this.getAccountTotalPeriods(account), totalPeriods: this.getAccountTotalPeriods(account),
}), }),
total: { total: {
@@ -145,7 +148,7 @@ export default class BalanceSheetStatement extends FinancialSheet {
currencyCode: this.baseCurrency, currencyCode: this.baseCurrency,
}, },
}; };
}; }
/** /**
* Strcuture accounts related mapper. * Strcuture accounts related mapper.
@@ -155,10 +158,10 @@ export default class BalanceSheetStatement extends FinancialSheet {
*/ */
private structureRelatedAccountsMapper( private structureRelatedAccountsMapper(
sectionAccountsTypes: string[], sectionAccountsTypes: string[],
accounts: IAccount & { type: IAccountType }[], accounts: IAccount & { type: IAccountType }[]
): { ): {
children: IBalanceSheetAccount[], children: IBalanceSheetAccount[];
total: IBalanceSheetAccountTotal, total: IBalanceSheetAccountTotal;
} { } {
const filteredAccounts = accounts const filteredAccounts = accounts
// Filter accounts that associated to the section accounts types. // Filter accounts that associated to the section accounts types.
@@ -169,7 +172,7 @@ export default class BalanceSheetStatement extends FinancialSheet {
// Filter accounts that have no transaction when `noneTransactions` is on. // Filter accounts that have no transaction when `noneTransactions` is on.
.filter( .filter(
(section: IBalanceSheetAccount) => (section: IBalanceSheetAccount) =>
!(!section.hasTransactions && this.query.noneTransactions), !(!section.hasTransactions && this.query.noneTransactions)
) )
// Filter accounts that have zero total amount when `noneZero` is on. // Filter accounts that have zero total amount when `noneZero` is on.
.filter( .filter(
@@ -181,10 +184,10 @@ export default class BalanceSheetStatement extends FinancialSheet {
const totalAmount = sumBy(filteredAccounts, 'total.amount'); const totalAmount = sumBy(filteredAccounts, 'total.amount');
return { return {
children: flatToNestedArray( children: flatToNestedArray(filteredAccounts, {
filteredAccounts, id: 'id',
{ id: 'id', parentId: 'parentAccountId' } parentId: 'parentAccountId',
), }),
total: { total: {
amount: totalAmount, amount: totalAmount,
formattedAmount: this.formatNumber(totalAmount), formattedAmount: this.formatNumber(totalAmount),
@@ -196,7 +199,31 @@ export default class BalanceSheetStatement extends FinancialSheet {
} }
: {}), : {}),
}; };
}
/**
* Mappes the structure sections.
* @param {IBalanceSheetStructureSection} structure
* @param {IAccount} accounts
*/
private structureSectionMapper(
structure: IBalanceSheetStructureSection,
accounts: IAccount[]
) {
const children = this.balanceSheetStructureWalker(
structure.children,
accounts
);
return {
children,
total: this.getSectionTotal(children),
...(this.query.displayColumnsType === 'date_periods'
? {
totalPeriods: this.getSectionTotalPeriods(children),
}
: {}),
}; };
}
/** /**
* Balance sheet structure mapper. * Balance sheet structure mapper.
@@ -205,29 +232,18 @@ export default class BalanceSheetStatement extends FinancialSheet {
*/ */
private balanceSheetStructureMapper( private balanceSheetStructureMapper(
structure: IBalanceSheetStructureSection, structure: IBalanceSheetStructureSection,
accounts: IAccount & { type: IAccountType }[], accounts: IAccount & { type: IAccountType }[]
): IBalanceSheetSection { ): IBalanceSheetSection {
const result = { const result = {
name: structure.name, name: structure.name,
sectionType: structure.sectionType, sectionType: structure.sectionType,
type: structure.type, type: structure.type,
...(structure.type === 'accounts_section' ...(structure.type === 'accounts_section')
? { ? this.structureRelatedAccountsMapper(
...this.structureRelatedAccountsMapper(
structure._accountsTypesRelated, structure._accountsTypesRelated,
accounts, accounts
), )
} : this.structureSectionMapper(structure, accounts),
: (() => {
const children = this.balanceSheetStructureWalker(
structure.children,
accounts,
);
return {
children,
total: this.getSectionTotal(children),
};
})()),
}; };
return result; return result;
} }
@@ -239,15 +255,18 @@ export default class BalanceSheetStatement extends FinancialSheet {
*/ */
private balanceSheetStructureWalker( private balanceSheetStructureWalker(
reportStructure: IBalanceSheetStructureSection[], reportStructure: IBalanceSheetStructureSection[],
balanceSheetAccounts: IAccount & { type: IAccountType }[], balanceSheetAccounts: IAccount & { type: IAccountType }[]
): IBalanceSheetSection[] { ): IBalanceSheetSection[] {
return reportStructure return (
reportStructure
.map((structure: IBalanceSheetStructureSection) => .map((structure: IBalanceSheetStructureSection) =>
this.balanceSheetStructureMapper(structure, balanceSheetAccounts) this.balanceSheetStructureMapper(structure, balanceSheetAccounts)
) )
// Filter the structure sections that have no children. // Filter the structure sections that have no children.
.filter((structure: IBalanceSheetSection) => .filter(
(structure: IBalanceSheetSection) =>
structure.children.length > 0 || structure._forceShow structure.children.length > 0 || structure._forceShow
)
); );
} }
@@ -278,7 +297,7 @@ export default class BalanceSheetStatement extends FinancialSheet {
public reportData(): IBalanceSheetSection[] { public reportData(): IBalanceSheetSection[] {
return this.balanceSheetStructureWalker( return this.balanceSheetStructureWalker(
BalanceSheetStructure, BalanceSheetStructure,
this.accounts, this.accounts
) );
} }
} }

View File

@@ -72,7 +72,8 @@ export default class BalanceSheetStatementService
// Retrieve all journal transactions based on the given query. // Retrieve all journal transactions based on the given query.
const transactions = await transactionsRepository.journal({ const transactions = await transactionsRepository.journal({
fromDate: query.toDate, fromDate: query.fromDate,
toDate: query.toDate,
}); });
// Transform transactions to journal collection. // Transform transactions to journal collection.
const transactionsJournal = Journal.fromTransactions( const transactionsJournal = Journal.fromTransactions(

View File

@@ -81,8 +81,6 @@ export default class JournalSheet extends FinancialSheet {
* @return {IJournalReport} * @return {IJournalReport}
*/ */
reportData(): IJournalReport { reportData(): IJournalReport {
return { return this.entriesWalker(this.journal.entries);
entries: this.entriesWalker(this.journal.entries),
};
} }
} }

View File

@@ -1,9 +1,9 @@
import { Service, Inject } from "typedi"; import { Service, Inject } from 'typedi';
import { IJournalReportQuery } from 'interfaces'; import { IJournalReportQuery } from 'interfaces';
import moment from 'moment'; import moment from 'moment';
import JournalSheet from "./JournalSheet"; import JournalSheet from './JournalSheet';
import TenancyService from "services/Tenancy/TenancyService"; import TenancyService from 'services/Tenancy/TenancyService';
import Journal from "services/Accounting/JournalPoster"; import Journal from 'services/Accounting/JournalPoster';
@Service() @Service()
export default class JournalSheetService { export default class JournalSheetService {
@@ -36,10 +36,7 @@ export default class JournalSheetService {
* @param {number} tenantId * @param {number} tenantId
* @param {IJournalSheetFilterQuery} query * @param {IJournalSheetFilterQuery} query
*/ */
async journalSheet( async journalSheet(tenantId: number, query: IJournalReportQuery) {
tenantId: number,
query: IJournalReportQuery,
) {
const { const {
accountRepository, accountRepository,
transactionsRepository, transactionsRepository,
@@ -49,11 +46,17 @@ export default class JournalSheetService {
...this.defaultQuery, ...this.defaultQuery,
...query, ...query,
}; };
this.logger.info('[journal] trying to calculate the report.', { tenantId, filter }); this.logger.info('[journal] trying to calculate the report.', {
tenantId,
filter,
});
// Settings service. // Settings service.
const settings = this.tenancy.settings(tenantId); const settings = this.tenancy.settings(tenantId);
const baseCurrency = settings.get({ group: 'organization', key: 'base_currency' }); const baseCurrency = settings.get({
group: 'organization',
key: 'base_currency',
});
// Retrieve all accounts on the storage. // Retrieve all accounts on the storage.
const accountsGraph = await accountRepository.getDependencyGraph(); const accountsGraph = await accountRepository.getDependencyGraph();
@@ -66,10 +69,12 @@ export default class JournalSheetService {
fromAmount: filter.fromRange, fromAmount: filter.fromRange,
toAmount: filter.toRange, toAmount: filter.toRange,
}); });
// Transform the transactions array to journal collection. // Transform the transactions array to journal collection.
const transactionsJournal = Journal.fromTransactions(transactions, tenantId, accountsGraph); const transactionsJournal = Journal.fromTransactions(
transactions,
tenantId,
accountsGraph
);
// Journal report instance. // Journal report instance.
const journalSheetInstance = new JournalSheet( const journalSheetInstance = new JournalSheet(
tenantId, tenantId,
@@ -80,6 +85,9 @@ export default class JournalSheetService {
// Retrieve journal report columns. // Retrieve journal report columns.
const journalSheetData = journalSheetInstance.reportData(); const journalSheetData = journalSheetInstance.reportData();
return journalSheetData; return {
data: journalSheetData,
query: filter,
};
} }
} }

View File

@@ -1,6 +1,6 @@
import { flatten, pick, sumBy } from 'lodash'; import { flatten, pick, sumBy } from 'lodash';
import { IProfitLossSheetQuery } from "interfaces/ProfitLossSheet"; import { IProfitLossSheetQuery } from 'interfaces/ProfitLossSheet';
import FinancialSheet from "../FinancialSheet"; import FinancialSheet from '../FinancialSheet';
import { import {
IAccount, IAccount,
IAccountType, IAccountType,
@@ -34,7 +34,7 @@ export default class ProfitLossSheet extends FinancialSheet {
query: IProfitLossSheetQuery, query: IProfitLossSheetQuery,
accounts: IAccount & { type: IAccountType }[], accounts: IAccount & { type: IAccountType }[],
journal: IJournalPoster, journal: IJournalPoster,
baseCurrency: string, baseCurrency: string
) { ) {
super(); super();
@@ -44,9 +44,8 @@ export default class ProfitLossSheet extends FinancialSheet {
this.accounts = accounts; this.accounts = accounts;
this.journal = journal; this.journal = journal;
this.baseCurrency = baseCurrency; this.baseCurrency = baseCurrency;
this.comparatorDateType = query.displayColumnsType === 'total' this.comparatorDateType =
? 'day' query.displayColumnsType === 'total' ? 'day' : query.displayColumnsBy;
: query.displayColumnsBy;
this.initDateRangeCollection(); this.initDateRangeCollection();
} }
@@ -56,7 +55,7 @@ export default class ProfitLossSheet extends FinancialSheet {
* @return {IAccount & { type: IAccountType }[]} * @return {IAccount & { type: IAccountType }[]}
*/ */
get incomeAccounts() { get incomeAccounts() {
return this.accounts.filter(a => a.type.key === 'income'); return this.accounts.filter((a) => a.type.key === 'income');
} }
/** /**
@@ -64,7 +63,7 @@ export default class ProfitLossSheet extends FinancialSheet {
* @return {IAccount & { type: IAccountType }[]} * @return {IAccount & { type: IAccountType }[]}
*/ */
get expensesAccounts() { get expensesAccounts() {
return this.accounts.filter(a => a.type.key === 'expense'); return this.accounts.filter((a) => a.type.key === 'expense');
} }
/** /**
@@ -72,7 +71,7 @@ export default class ProfitLossSheet extends FinancialSheet {
* @return {IAccount & { type: IAccountType }[]}} * @return {IAccount & { type: IAccountType }[]}}
*/ */
get otherExpensesAccounts() { get otherExpensesAccounts() {
return this.accounts.filter(a => a.type.key === 'other_expense'); return this.accounts.filter((a) => a.type.key === 'other_expense');
} }
/** /**
@@ -80,7 +79,7 @@ export default class ProfitLossSheet extends FinancialSheet {
* @return {IAccount & { type: IAccountType }[]} * @return {IAccount & { type: IAccountType }[]}
*/ */
get costOfSalesAccounts() { get costOfSalesAccounts() {
return this.accounts.filter(a => a.type.key === 'cost_of_goods_sold'); return this.accounts.filter((a) => a.type.key === 'cost_of_goods_sold');
} }
/** /**
@@ -105,7 +104,7 @@ export default class ProfitLossSheet extends FinancialSheet {
const amount = this.journal.getAccountBalance( const amount = this.journal.getAccountBalance(
account.id, account.id,
this.query.toDate, this.query.toDate,
this.comparatorDateType, this.comparatorDateType
); );
const formattedAmount = this.formatNumber(amount); const formattedAmount = this.formatNumber(amount);
const currencyCode = this.baseCurrency; const currencyCode = this.baseCurrency;
@@ -123,13 +122,13 @@ export default class ProfitLossSheet extends FinancialSheet {
const amount = this.journal.getAccountBalance( const amount = this.journal.getAccountBalance(
account.id, account.id,
date, date,
this.comparatorDateType, this.comparatorDateType
); );
const formattedAmount = this.formatNumber(amount); const formattedAmount = this.formatNumber(amount);
const currencyCode = this.baseCurrency; const currencyCode = this.baseCurrency;
return { date, amount, formattedAmount, currencyCode }; return { date, amount, formattedAmount, currencyCode };
}) });
} }
/** /**
@@ -157,22 +156,26 @@ export default class ProfitLossSheet extends FinancialSheet {
* @param {IAccount[]} accounts - * @param {IAccount[]} accounts -
* @return {IProfitLossSheetAccount[]} * @return {IProfitLossSheetAccount[]}
*/ */
private accountsWalker(accounts: IAccount & { type: IAccountType }[]): IProfitLossSheetAccount[] { private accountsWalker(
accounts: IAccount & { type: IAccountType }[]
): IProfitLossSheetAccount[] {
const flattenAccounts = accounts const flattenAccounts = accounts
.map(this.accountMapper.bind(this)) .map(this.accountMapper.bind(this))
// Filter accounts that have no transaction when `noneTransactions` is on. // Filter accounts that have no transaction when `noneTransactions` is on.
.filter((account: IProfitLossSheetAccount) => .filter(
!(!account.hasTransactions && this.query.noneTransactions), (account: IProfitLossSheetAccount) =>
!(!account.hasTransactions && this.query.noneTransactions)
) )
// Filter accounts that have zero total amount when `noneZero` is on. // Filter accounts that have zero total amount when `noneZero` is on.
.filter((account: IProfitLossSheetAccount) => .filter(
(account: IProfitLossSheetAccount) =>
!(account.total.amount === 0 && this.query.noneZero) !(account.total.amount === 0 && this.query.noneZero)
); );
return flatToNestedArray( return flatToNestedArray(flattenAccounts, {
flattenAccounts, id: 'id',
{ id: 'id', parentId: 'parentAccountId' }, parentId: 'parentAccountId',
); });
} }
/** /**
@@ -180,7 +183,9 @@ export default class ProfitLossSheet extends FinancialSheet {
* @param {IAccount[]} accounts - * @param {IAccount[]} accounts -
* @return {IProfitLossSheetTotal} * @return {IProfitLossSheetTotal}
*/ */
private gatTotalSection(accounts: IProfitLossSheetAccount[]): IProfitLossSheetTotal { private gatTotalSection(
accounts: IProfitLossSheetAccount[]
): IProfitLossSheetTotal {
const amount = sumBy(accounts, 'total.amount'); const amount = sumBy(accounts, 'total.amount');
const formattedAmount = this.formatNumber(amount); const formattedAmount = this.formatNumber(amount);
const currencyCode = this.baseCurrency; const currencyCode = this.baseCurrency;
@@ -193,7 +198,9 @@ export default class ProfitLossSheet extends FinancialSheet {
* @param {IAccount} accounts - * @param {IAccount} accounts -
* @return {IProfitLossSheetTotal[]} * @return {IProfitLossSheetTotal[]}
*/ */
private getTotalPeriodsSection(accounts: IProfitLossSheetAccount[]): IProfitLossSheetTotal[] { private getTotalPeriodsSection(
accounts: IProfitLossSheetAccount[]
): IProfitLossSheetTotal[] {
return this.dateRangeSet.map((date, index) => { return this.dateRangeSet.map((date, index) => {
const amount = sumBy(accounts, `totalPeriods[${index}].amount`); const amount = sumBy(accounts, `totalPeriods[${index}].amount`);
const formattedAmount = this.formatNumber(amount); const formattedAmount = this.formatNumber(amount);
@@ -213,7 +220,7 @@ export default class ProfitLossSheet extends FinancialSheet {
...(this.query.displayColumnsType === 'date_periods' && { ...(this.query.displayColumnsType === 'date_periods' && {
totalPeriods: this.getTotalPeriodsSection(accounts), totalPeriods: this.getTotalPeriodsSection(accounts),
}), }),
} };
} }
/** /**
@@ -266,10 +273,13 @@ export default class ProfitLossSheet extends FinancialSheet {
private getSummarySectionDatePeriods( private getSummarySectionDatePeriods(
positiveSections: IProfitLossSheetTotalSection[], positiveSections: IProfitLossSheetTotalSection[],
minesSections: IProfitLossSheetTotalSection[], minesSections: IProfitLossSheetTotalSection[]
) { ) {
return this.dateRangeSet.map((date, index: number) => { return this.dateRangeSet.map((date, index: number) => {
const totalPositive = sumBy(positiveSections, `totalPeriods[${index}].amount`); const totalPositive = sumBy(
positiveSections,
`totalPeriods[${index}].amount`
);
const totalMines = sumBy(minesSections, `totalPeriods[${index}].amount`); const totalMines = sumBy(minesSections, `totalPeriods[${index}].amount`);
const amount = totalPositive - totalMines; const amount = totalPositive - totalMines;
@@ -278,11 +288,11 @@ export default class ProfitLossSheet extends FinancialSheet {
return { date, amount, formattedAmount, currencyCode }; return { date, amount, formattedAmount, currencyCode };
}); });
}; }
private getSummarySectionTotal( private getSummarySectionTotal(
positiveSections: IProfitLossSheetTotalSection[], positiveSections: IProfitLossSheetTotalSection[],
minesSections: IProfitLossSheetTotalSection[], minesSections: IProfitLossSheetTotalSection[]
) { ) {
const totalPositiveSections = sumBy(positiveSections, 'total.amount'); const totalPositiveSections = sumBy(positiveSections, 'total.amount');
const totalMinesSections = sumBy(minesSections, 'total.amount'); const totalMinesSections = sumBy(minesSections, 'total.amount');
@@ -300,22 +310,23 @@ export default class ProfitLossSheet extends FinancialSheet {
*/ */
private getSummarySection( private getSummarySection(
sections: IProfitLossSheetTotalSection | IProfitLossSheetTotalSection[], sections: IProfitLossSheetTotalSection | IProfitLossSheetTotalSection[],
subtractSections: IProfitLossSheetTotalSection|IProfitLossSheetTotalSection[] subtractSections:
| IProfitLossSheetTotalSection
| IProfitLossSheetTotalSection[]
): IProfitLossSheetTotalSection { ): IProfitLossSheetTotalSection {
const positiveSections = Array.isArray(sections) ? sections : [sections]; const positiveSections = Array.isArray(sections) ? sections : [sections];
const minesSections = Array.isArray(subtractSections) ? subtractSections : [subtractSections]; const minesSections = Array.isArray(subtractSections)
? subtractSections
: [subtractSections];
return { return {
total: this.getSummarySectionTotal(positiveSections, minesSections), total: this.getSummarySectionTotal(positiveSections, minesSections),
...(this.query.displayColumnsType === 'date_periods' && { ...(this.query.displayColumnsType === 'date_periods' && {
totalPeriods: [ totalPeriods: [
...this.getSummarySectionDatePeriods( ...this.getSummarySectionDatePeriods(positiveSections, minesSections),
positiveSections,
minesSections,
),
], ],
}), }),
} };
} }
/** /**
@@ -341,7 +352,10 @@ export default class ProfitLossSheet extends FinancialSheet {
const grossProfit = this.getSummarySection(income, costOfSales); const grossProfit = this.getSummarySection(income, costOfSales);
// - Operating profit = Gross profit - Expenses. // - Operating profit = Gross profit - Expenses.
const operatingProfit = this.getSummarySection(grossProfit, [expenses, costOfSales]); const operatingProfit = this.getSummarySection(grossProfit, [
expenses,
costOfSales,
]);
// - Net income = Operating profit - Other expenses. // - Net income = Operating profit - Other expenses.
const netIncome = this.getSummarySection(operatingProfit, otherExpenses); const netIncome = this.getSummarySection(operatingProfit, otherExpenses);

View File

@@ -13,6 +13,7 @@ export default class TrialBalanceSheet extends FinancialSheet{
query: ITrialBalanceSheetQuery; query: ITrialBalanceSheetQuery;
accounts: IAccount & { type: IAccountType }[]; accounts: IAccount & { type: IAccountType }[];
journalFinancial: any; journalFinancial: any;
baseCurrency: string;
/** /**
* Constructor method. * Constructor method.
@@ -25,7 +26,8 @@ export default class TrialBalanceSheet extends FinancialSheet{
tenantId: number, tenantId: number,
query: ITrialBalanceSheetQuery, query: ITrialBalanceSheetQuery,
accounts: IAccount & { type: IAccountType }[], accounts: IAccount & { type: IAccountType }[],
journalFinancial: any journalFinancial: any,
baseCurrency: string,
) { ) {
super(); super();
@@ -35,6 +37,7 @@ export default class TrialBalanceSheet extends FinancialSheet{
this.accounts = accounts; this.accounts = accounts;
this.journalFinancial = journalFinancial; this.journalFinancial = journalFinancial;
this.numberFormat = this.query.numberFormat; this.numberFormat = this.query.numberFormat;
this.baseCurrency = baseCurrency;
} }
/** /**
@@ -58,6 +61,7 @@ export default class TrialBalanceSheet extends FinancialSheet{
credit: trial.credit, credit: trial.credit,
debit: trial.debit, debit: trial.debit,
balance: trial.balance, balance: trial.balance,
currencyCode: this.baseCurrency,
formattedCredit: this.formatNumber(trial.credit), formattedCredit: this.formatNumber(trial.credit),
formattedDebit: this.formatNumber(trial.debit), formattedDebit: this.formatNumber(trial.debit),

View File

@@ -55,6 +55,10 @@ export default class TrialBalanceSheetService {
transactionsRepository, transactionsRepository,
} = this.tenancy.repositories(tenantId); } = this.tenancy.repositories(tenantId);
// Settings tenant service.
const settings = this.tenancy.settings(tenantId);
const baseCurrency = settings.get({ group: 'organization', key: 'base_currency' });
this.logger.info('[trial_balance_sheet] trying to calcualte the report.', { tenantId, filter }); this.logger.info('[trial_balance_sheet] trying to calcualte the report.', { tenantId, filter });
// Retrieve all accounts on the storage. // Retrieve all accounts on the storage.
@@ -76,6 +80,7 @@ export default class TrialBalanceSheetService {
filter, filter,
accounts, accounts,
transactionsJournal, transactionsJournal,
baseCurrency
); );
// Trial balance sheet data. // Trial balance sheet data.
const trialBalanceSheetData = trialBalanceInstance.reportData(); const trialBalanceSheetData = trialBalanceInstance.reportData();

View File

@@ -17,7 +17,7 @@ import {
IPaymentReceiveEntry, IPaymentReceiveEntry,
IPaymentReceiveEntryDTO, IPaymentReceiveEntryDTO,
IPaymentReceivesFilter, IPaymentReceivesFilter,
ISaleInvoice ISaleInvoice,
} from 'interfaces'; } from 'interfaces';
import AccountsService from 'services/Accounts/AccountsService'; import AccountsService from 'services/Accounts/AccountsService';
import JournalPoster from 'services/Accounting/JournalPoster'; import JournalPoster from 'services/Accounting/JournalPoster';
@@ -34,11 +34,12 @@ const ERRORS = {
PAYMENT_RECEIVE_NO_EXISTS: 'PAYMENT_RECEIVE_NO_EXISTS', PAYMENT_RECEIVE_NO_EXISTS: 'PAYMENT_RECEIVE_NO_EXISTS',
PAYMENT_RECEIVE_NOT_EXISTS: 'PAYMENT_RECEIVE_NOT_EXISTS', PAYMENT_RECEIVE_NOT_EXISTS: 'PAYMENT_RECEIVE_NOT_EXISTS',
DEPOSIT_ACCOUNT_NOT_FOUND: 'DEPOSIT_ACCOUNT_NOT_FOUND', DEPOSIT_ACCOUNT_NOT_FOUND: 'DEPOSIT_ACCOUNT_NOT_FOUND',
DEPOSIT_ACCOUNT_NOT_CURRENT_ASSET_TYPE: 'DEPOSIT_ACCOUNT_NOT_CURRENT_ASSET_TYPE', DEPOSIT_ACCOUNT_NOT_CURRENT_ASSET_TYPE:
'DEPOSIT_ACCOUNT_NOT_CURRENT_ASSET_TYPE',
INVALID_PAYMENT_AMOUNT: 'INVALID_PAYMENT_AMOUNT', INVALID_PAYMENT_AMOUNT: 'INVALID_PAYMENT_AMOUNT',
INVOICES_IDS_NOT_FOUND: 'INVOICES_IDS_NOT_FOUND', INVOICES_IDS_NOT_FOUND: 'INVOICES_IDS_NOT_FOUND',
ENTRIES_IDS_NOT_EXISTS: 'ENTRIES_IDS_NOT_EXISTS', ENTRIES_IDS_NOT_EXISTS: 'ENTRIES_IDS_NOT_EXISTS',
INVOICES_NOT_DELIVERED_YET: 'INVOICES_NOT_DELIVERED_YET' INVOICES_NOT_DELIVERED_YET: 'INVOICES_NOT_DELIVERED_YET',
}; };
/** /**
* Payment receive service. * Payment receive service.
@@ -81,7 +82,8 @@ export default class PaymentReceiveService {
notPaymentReceiveId?: number notPaymentReceiveId?: number
): Promise<void> { ): Promise<void> {
const { PaymentReceive } = this.tenancy.models(tenantId); const { PaymentReceive } = this.tenancy.models(tenantId);
const paymentReceive = await PaymentReceive.query().findOne('payment_receive_no', paymentReceiveNo) const paymentReceive = await PaymentReceive.query()
.findOne('payment_receive_no', paymentReceiveNo)
.onBuild((builder) => { .onBuild((builder) => {
if (notPaymentReceiveId) { if (notPaymentReceiveId) {
builder.whereNot('id', notPaymentReceiveId); builder.whereNot('id', notPaymentReceiveId);
@@ -118,13 +120,23 @@ export default class PaymentReceiveService {
* @param {number} tenantId - * @param {number} tenantId -
* @param {number} depositAccountId - * @param {number} depositAccountId -
*/ */
async getDepositAccountOrThrowError(tenantId: number, depositAccountId: number): Promise<IAccount> { async getDepositAccountOrThrowError(
const { accountTypeRepository, accountRepository } = this.tenancy.repositories(tenantId); tenantId: number,
depositAccountId: number
): Promise<IAccount> {
const {
accountTypeRepository,
accountRepository,
} = this.tenancy.repositories(tenantId);
const currentAssetTypes = await accountTypeRepository.getByChildType('current_asset'); const currentAssetTypes = await accountTypeRepository.getByChildType(
const depositAccount = await accountRepository.findOneById(depositAccountId); 'current_asset'
);
const depositAccount = await accountRepository.findOneById(
depositAccountId
);
const currentAssetTypesIds = currentAssetTypes.map(type => type.id); const currentAssetTypesIds = currentAssetTypes.map((type) => type.id);
if (!depositAccount) { if (!depositAccount) {
throw new ServiceError(ERRORS.DEPOSIT_ACCOUNT_NOT_FOUND); throw new ServiceError(ERRORS.DEPOSIT_ACCOUNT_NOT_FOUND);
@@ -140,10 +152,16 @@ export default class PaymentReceiveService {
* @param {number} tenantId - * @param {number} tenantId -
* @param {} paymentReceiveEntries - * @param {} paymentReceiveEntries -
*/ */
async validateInvoicesIDsExistance(tenantId: number, customerId: number, paymentReceiveEntries: IPaymentReceiveEntryDTO[]): Promise<void> { async validateInvoicesIDsExistance(
tenantId: number,
customerId: number,
paymentReceiveEntries: IPaymentReceiveEntryDTO[]
): Promise<void> {
const { SaleInvoice } = this.tenancy.models(tenantId); const { SaleInvoice } = this.tenancy.models(tenantId);
const invoicesIds = paymentReceiveEntries.map((e: IPaymentReceiveEntryDTO) => e.invoiceId); const invoicesIds = paymentReceiveEntries.map(
(e: IPaymentReceiveEntryDTO) => e.invoiceId
);
const storedInvoices = await SaleInvoice.query() const storedInvoices = await SaleInvoice.query()
.whereIn('id', invoicesIds) .whereIn('id', invoicesIds)
.where('customer_id', customerId); .where('customer_id', customerId);
@@ -155,10 +173,14 @@ export default class PaymentReceiveService {
throw new ServiceError(ERRORS.INVOICES_IDS_NOT_FOUND); throw new ServiceError(ERRORS.INVOICES_IDS_NOT_FOUND);
} }
// Filters the not delivered invoices. // Filters the not delivered invoices.
const notDeliveredInvoices = storedInvoices.filter((invoice) => !invoice.isDelivered); const notDeliveredInvoices = storedInvoices.filter(
(invoice) => !invoice.isDelivered
);
if (notDeliveredInvoices.length > 0) { if (notDeliveredInvoices.length > 0) {
throw new ServiceError(ERRORS.INVOICES_NOT_DELIVERED_YET, null, { notDeliveredInvoices }); throw new ServiceError(ERRORS.INVOICES_NOT_DELIVERED_YET, null, {
notDeliveredInvoices,
});
} }
return storedInvoices; return storedInvoices;
} }
@@ -172,32 +194,38 @@ export default class PaymentReceiveService {
async validateInvoicesPaymentsAmount( async validateInvoicesPaymentsAmount(
tenantId: number, tenantId: number,
paymentReceiveEntries: IPaymentReceiveEntryDTO[], paymentReceiveEntries: IPaymentReceiveEntryDTO[],
oldPaymentEntries: IPaymentReceiveEntry[] = [], oldPaymentEntries: IPaymentReceiveEntry[] = []
) { ) {
const { SaleInvoice } = this.tenancy.models(tenantId); const { SaleInvoice } = this.tenancy.models(tenantId);
const invoicesIds = paymentReceiveEntries.map((e: IPaymentReceiveEntryDTO) => e.invoiceId); const invoicesIds = paymentReceiveEntries.map(
(e: IPaymentReceiveEntryDTO) => e.invoiceId
);
const storedInvoices = await SaleInvoice.query().whereIn('id', invoicesIds); const storedInvoices = await SaleInvoice.query().whereIn('id', invoicesIds);
const storedInvoicesMap = new Map( const storedInvoicesMap = new Map(
storedInvoices storedInvoices.map((invoice: ISaleInvoice) => {
.map((invoice: ISaleInvoice) => { const oldEntries = oldPaymentEntries.filter((entry) => entry.invoiceId);
const oldEntries = oldPaymentEntries.filter(entry => entry.invoiceId); const oldPaymentAmount = sumBy(oldEntries, 'paymentAmount') || 0;
const oldPaymentAmount = sumBy(oldEntries, 'paymentAmount') || 0,
return [invoice.id, { ...invoice, dueAmount: invoice.dueAmount + oldPaymentAmount }]; return [
invoice.id,
{ ...invoice, dueAmount: invoice.dueAmount + oldPaymentAmount },
];
}) })
); );
const hasWrongPaymentAmount: any[] = []; const hasWrongPaymentAmount: any[] = [];
paymentReceiveEntries.forEach((entry: IPaymentReceiveEntryDTO, index: number) => { paymentReceiveEntries.forEach(
(entry: IPaymentReceiveEntryDTO, index: number) => {
const entryInvoice = storedInvoicesMap.get(entry.invoiceId); const entryInvoice = storedInvoicesMap.get(entry.invoiceId);
const { dueAmount } = entryInvoice; const { dueAmount } = entryInvoice;
if (dueAmount < entry.paymentAmount) { if (dueAmount < entry.paymentAmount) {
hasWrongPaymentAmount.push({ index, due_amount: dueAmount }); hasWrongPaymentAmount.push({ index, due_amount: dueAmount });
} }
}); }
);
if (hasWrongPaymentAmount.length > 0) { if (hasWrongPaymentAmount.length > 0) {
throw new ServiceError(ERRORS.INVALID_PAYMENT_AMOUNT); throw new ServiceError(ERRORS.INVALID_PAYMENT_AMOUNT);
} }
@@ -212,7 +240,7 @@ export default class PaymentReceiveService {
private async validateEntriesIdsExistance( private async validateEntriesIdsExistance(
tenantId: number, tenantId: number,
paymentReceiveId: number, paymentReceiveId: number,
paymentReceiveEntries: IPaymentReceiveEntryDTO[], paymentReceiveEntries: IPaymentReceiveEntryDTO[]
) { ) {
const { PaymentReceiveEntry } = this.tenancy.models(tenantId); const { PaymentReceiveEntry } = this.tenancy.models(tenantId);
@@ -220,8 +248,10 @@ export default class PaymentReceiveService {
.filter((entry) => entry.id) .filter((entry) => entry.id)
.map((entry) => entry.id); .map((entry) => entry.id);
const storedEntries = await PaymentReceiveEntry.query() const storedEntries = await PaymentReceiveEntry.query().where(
.where('payment_receive_id', paymentReceiveId); 'payment_receive_id',
paymentReceiveId
);
const storedEntriesIds = storedEntries.map((entry: any) => entry.id); const storedEntriesIds = storedEntries.map((entry: any) => entry.id);
const notFoundEntriesIds = difference(entriesIds, storedEntriesIds); const notFoundEntriesIds = difference(entriesIds, storedEntriesIds);
@@ -238,31 +268,51 @@ export default class PaymentReceiveService {
* @param {number} tenantId - Tenant id. * @param {number} tenantId - Tenant id.
* @param {IPaymentReceive} paymentReceive * @param {IPaymentReceive} paymentReceive
*/ */
public async createPaymentReceive(tenantId: number, paymentReceiveDTO: IPaymentReceiveCreateDTO) { public async createPaymentReceive(
tenantId: number,
paymentReceiveDTO: IPaymentReceiveCreateDTO
) {
const { PaymentReceive } = this.tenancy.models(tenantId); const { PaymentReceive } = this.tenancy.models(tenantId);
const paymentAmount = sumBy(paymentReceiveDTO.entries, 'paymentAmount'); const paymentAmount = sumBy(paymentReceiveDTO.entries, 'paymentAmount');
// Validate payment receive number uniquiness. // Validate payment receive number uniquiness.
if (paymentReceiveDTO.paymentReceiveNo) { if (paymentReceiveDTO.paymentReceiveNo) {
await this.validatePaymentReceiveNoExistance(tenantId, paymentReceiveDTO.paymentReceiveNo); await this.validatePaymentReceiveNoExistance(
tenantId,
paymentReceiveDTO.paymentReceiveNo
);
} }
// Validate customer existance. // Validate customer existance.
await this.customersService.getCustomerByIdOrThrowError(tenantId, paymentReceiveDTO.customerId); await this.customersService.getCustomerByIdOrThrowError(
tenantId,
paymentReceiveDTO.customerId
);
// Validate the deposit account existance and type. // Validate the deposit account existance and type.
await this.getDepositAccountOrThrowError(tenantId, paymentReceiveDTO.depositAccountId); await this.getDepositAccountOrThrowError(
tenantId,
paymentReceiveDTO.depositAccountId
);
// Validate payment receive invoices IDs existance. // Validate payment receive invoices IDs existance.
await this.validateInvoicesIDsExistance(tenantId, paymentReceiveDTO.customerId, paymentReceiveDTO.entries); await this.validateInvoicesIDsExistance(
tenantId,
paymentReceiveDTO.customerId,
paymentReceiveDTO.entries
);
// Validate invoice payment amount. // Validate invoice payment amount.
await this.validateInvoicesPaymentsAmount(tenantId, paymentReceiveDTO.entries); await this.validateInvoicesPaymentsAmount(
tenantId,
paymentReceiveDTO.entries
);
this.logger.info('[payment_receive] inserting to the storage.'); this.logger.info('[payment_receive] inserting to the storage.');
const paymentReceive = await PaymentReceive.query() const paymentReceive = await PaymentReceive.query().insertGraphAndFetch({
.insertGraphAndFetch({
amount: paymentAmount, amount: paymentAmount,
...formatDateFields(omit(paymentReceiveDTO, ['entries']), ['paymentDate']), ...formatDateFields(omit(paymentReceiveDTO, ['entries']), [
'paymentDate',
]),
entries: paymentReceiveDTO.entries.map((entry) => ({ entries: paymentReceiveDTO.entries.map((entry) => ({
...omit(entry, ['id']), ...omit(entry, ['id']),
@@ -270,9 +320,14 @@ export default class PaymentReceiveService {
}); });
await this.eventDispatcher.dispatch(events.paymentReceive.onCreated, { await this.eventDispatcher.dispatch(events.paymentReceive.onCreated, {
tenantId, paymentReceive, paymentReceiveId: paymentReceive.id, tenantId,
paymentReceive,
paymentReceiveId: paymentReceive.id,
});
this.logger.info('[payment_receive] updated successfully.', {
tenantId,
paymentReceive,
}); });
this.logger.info('[payment_receive] updated successfully.', { tenantId, paymentReceive });
return paymentReceive; return paymentReceive;
} }
@@ -295,45 +350,78 @@ export default class PaymentReceiveService {
public async editPaymentReceive( public async editPaymentReceive(
tenantId: number, tenantId: number,
paymentReceiveId: number, paymentReceiveId: number,
paymentReceiveDTO: IPaymentReceiveEditDTO, paymentReceiveDTO: IPaymentReceiveEditDTO
) { ) {
const { PaymentReceive } = this.tenancy.models(tenantId); const { PaymentReceive } = this.tenancy.models(tenantId);
const paymentAmount = sumBy(paymentReceiveDTO.entries, 'paymentAmount'); const paymentAmount = sumBy(paymentReceiveDTO.entries, 'paymentAmount');
this.logger.info('[payment_receive] trying to edit payment receive.', { tenantId, paymentReceiveId, paymentReceiveDTO }); this.logger.info('[payment_receive] trying to edit payment receive.', {
tenantId,
paymentReceiveId,
paymentReceiveDTO,
});
// Validate the payment receive existance. // Validate the payment receive existance.
const oldPaymentReceive = await this.getPaymentReceiveOrThrowError(tenantId, paymentReceiveId); const oldPaymentReceive = await this.getPaymentReceiveOrThrowError(
tenantId,
paymentReceiveId
);
// Validate payment receive number uniquiness. // Validate payment receive number uniquiness.
if (paymentReceiveDTO.paymentReceiveNo) { if (paymentReceiveDTO.paymentReceiveNo) {
await this.validatePaymentReceiveNoExistance(tenantId, paymentReceiveDTO.paymentReceiveNo, paymentReceiveId); await this.validatePaymentReceiveNoExistance(
tenantId,
paymentReceiveDTO.paymentReceiveNo,
paymentReceiveId
);
} }
// Validate the deposit account existance and type. // Validate the deposit account existance and type.
this.getDepositAccountOrThrowError(tenantId, paymentReceiveDTO.depositAccountId); this.getDepositAccountOrThrowError(
tenantId,
paymentReceiveDTO.depositAccountId
);
// Validate the entries ids existance on payment receive type. // Validate the entries ids existance on payment receive type.
await this.validateEntriesIdsExistance(tenantId, paymentReceiveId, paymentReceiveDTO.entries); await this.validateEntriesIdsExistance(
tenantId,
paymentReceiveId,
paymentReceiveDTO.entries
);
// Validate payment receive invoices IDs existance and associated to the given customer id. // Validate payment receive invoices IDs existance and associated to the given customer id.
await this.validateInvoicesIDsExistance(tenantId, oldPaymentReceive.customerId, paymentReceiveDTO.entries); await this.validateInvoicesIDsExistance(
tenantId,
oldPaymentReceive.customerId,
paymentReceiveDTO.entries
);
// Validate invoice payment amount. // Validate invoice payment amount.
await this.validateInvoicesPaymentsAmount(tenantId, paymentReceiveDTO.entries, oldPaymentReceive.entries); await this.validateInvoicesPaymentsAmount(
tenantId,
paymentReceiveDTO.entries,
oldPaymentReceive.entries
);
// Update the payment receive transaction. // Update the payment receive transaction.
const paymentReceive = await PaymentReceive.query() const paymentReceive = await PaymentReceive.query().upsertGraphAndFetch({
.upsertGraphAndFetch({
id: paymentReceiveId, id: paymentReceiveId,
amount: paymentAmount, amount: paymentAmount,
...formatDateFields(omit(paymentReceiveDTO, ['entries']), ['paymentDate']), ...formatDateFields(omit(paymentReceiveDTO, ['entries']), [
'paymentDate',
]),
entries: paymentReceiveDTO.entries, entries: paymentReceiveDTO.entries,
}); });
await this.eventDispatcher.dispatch(events.paymentReceive.onEdited, { await this.eventDispatcher.dispatch(events.paymentReceive.onEdited, {
tenantId, paymentReceiveId, paymentReceive, oldPaymentReceive tenantId,
paymentReceiveId,
paymentReceive,
oldPaymentReceive,
});
this.logger.info('[payment_receive] upserted successfully.', {
tenantId,
paymentReceiveId,
}); });
this.logger.info('[payment_receive] upserted successfully.', { tenantId, paymentReceiveId });
} }
/** /**
@@ -351,20 +439,32 @@ export default class PaymentReceiveService {
* @param {IPaymentReceive} paymentReceive - Payment receive object. * @param {IPaymentReceive} paymentReceive - Payment receive object.
*/ */
async deletePaymentReceive(tenantId: number, paymentReceiveId: number) { async deletePaymentReceive(tenantId: number, paymentReceiveId: number) {
const { PaymentReceive, PaymentReceiveEntry } = this.tenancy.models(tenantId); const { PaymentReceive, PaymentReceiveEntry } = this.tenancy.models(
tenantId
);
const oldPaymentReceive = await this.getPaymentReceiveOrThrowError(tenantId, paymentReceiveId); const oldPaymentReceive = await this.getPaymentReceiveOrThrowError(
tenantId,
paymentReceiveId
);
// Deletes the payment receive associated entries. // Deletes the payment receive associated entries.
await PaymentReceiveEntry.query().where('payment_receive_id', paymentReceiveId).delete(); await PaymentReceiveEntry.query()
.where('payment_receive_id', paymentReceiveId)
.delete();
// Deletes the payment receive transaction. // Deletes the payment receive transaction.
await PaymentReceive.query().findById(paymentReceiveId).delete(); await PaymentReceive.query().findById(paymentReceiveId).delete();
await this.eventDispatcher.dispatch(events.paymentReceive.onDeleted, { await this.eventDispatcher.dispatch(events.paymentReceive.onDeleted, {
tenantId, paymentReceiveId, oldPaymentReceive, tenantId,
paymentReceiveId,
oldPaymentReceive,
});
this.logger.info('[payment_receive] deleted successfully.', {
tenantId,
paymentReceiveId,
}); });
this.logger.info('[payment_receive] deleted successfully.', { tenantId, paymentReceiveId });
} }
/** /**
@@ -376,9 +476,9 @@ export default class PaymentReceiveService {
tenantId: number, tenantId: number,
paymentReceiveId: number paymentReceiveId: number
): Promise<{ ): Promise<{
paymentReceive: IPaymentReceive, paymentReceive: IPaymentReceive;
receivableInvoices: ISaleInvoice[], receivableInvoices: ISaleInvoice[];
paymentReceiveInvoices: ISaleInvoice[], paymentReceiveInvoices: ISaleInvoice[];
}> { }> {
const { PaymentReceive, SaleInvoice } = this.tenancy.models(tenantId); const { PaymentReceive, SaleInvoice } = this.tenancy.models(tenantId);
const paymentReceive = await PaymentReceive.query() const paymentReceive = await PaymentReceive.query()
@@ -401,8 +501,8 @@ export default class PaymentReceiveService {
// Retrieve all payment receive associated invoices. // Retrieve all payment receive associated invoices.
const paymentReceiveInvoices = paymentReceive.entries.map((entry) => ({ const paymentReceiveInvoices = paymentReceive.entries.map((entry) => ({
...(entry.invoice), ...entry.invoice,
dueAmount: (entry.invoice.dueAmount + entry.paymentAmount), dueAmount: entry.invoice.dueAmount + entry.paymentAmount,
})); }));
return { paymentReceive, receivableInvoices, paymentReceiveInvoices }; return { paymentReceive, receivableInvoices, paymentReceiveInvoices };
@@ -414,13 +514,24 @@ export default class PaymentReceiveService {
* @param {number} paymentReceiveId - Payment receive id. * @param {number} paymentReceiveId - Payment receive id.
* @return {Promise<ISaleInvoice>} * @return {Promise<ISaleInvoice>}
*/ */
public async getPaymentReceiveInvoices(tenantId: number, paymentReceiveId: number) { public async getPaymentReceiveInvoices(
tenantId: number,
paymentReceiveId: number
) {
const { SaleInvoice } = this.tenancy.models(tenantId); const { SaleInvoice } = this.tenancy.models(tenantId);
const paymentReceive = await this.getPaymentReceiveOrThrowError(tenantId, paymentReceiveId); const paymentReceive = await this.getPaymentReceiveOrThrowError(
const paymentReceiveInvoicesIds = paymentReceive.entries.map(entry => entry.invoiceId); tenantId,
paymentReceiveId
);
const paymentReceiveInvoicesIds = paymentReceive.entries.map(
(entry) => entry.invoiceId
);
const saleInvoices = await SaleInvoice.query().whereIn('id', paymentReceiveInvoicesIds); const saleInvoices = await SaleInvoice.query().whereIn(
'id',
paymentReceiveInvoicesIds
);
return saleInvoices; return saleInvoices;
} }
@@ -432,18 +543,28 @@ export default class PaymentReceiveService {
*/ */
public async listPaymentReceives( public async listPaymentReceives(
tenantId: number, tenantId: number,
paymentReceivesFilter: IPaymentReceivesFilter, paymentReceivesFilter: IPaymentReceivesFilter
): Promise<{ paymentReceives: IPaymentReceive[], pagination: IPaginationMeta, filterMeta: IFilterMeta }> { ): Promise<{
paymentReceives: IPaymentReceive[];
pagination: IPaginationMeta;
filterMeta: IFilterMeta;
}> {
const { PaymentReceive } = this.tenancy.models(tenantId); const { PaymentReceive } = this.tenancy.models(tenantId);
const dynamicFilter = await this.dynamicListService.dynamicList(tenantId, PaymentReceive, paymentReceivesFilter); const dynamicFilter = await this.dynamicListService.dynamicList(
tenantId,
PaymentReceive,
paymentReceivesFilter
);
const { results, pagination } = await PaymentReceive.query().onBuild((builder) => { const { results, pagination } = await PaymentReceive.query()
.onBuild((builder) => {
builder.withGraphFetched('customer'); builder.withGraphFetched('customer');
builder.withGraphFetched('depositAccount'); builder.withGraphFetched('depositAccount');
dynamicFilter.buildQuery()(builder); dynamicFilter.buildQuery()(builder);
}).pagination( })
.pagination(
paymentReceivesFilter.page - 1, paymentReceivesFilter.page - 1,
paymentReceivesFilter.pageSize, paymentReceivesFilter.pageSize
); );
return { return {
paymentReceives: results, paymentReceives: results,
@@ -456,7 +577,10 @@ export default class PaymentReceiveService {
* Retrieve the payment receive details with associated invoices. * Retrieve the payment receive details with associated invoices.
* @param {Integer} paymentReceiveId * @param {Integer} paymentReceiveId
*/ */
async getPaymentReceiveWithInvoices(tenantId: number, paymentReceiveId: number) { async getPaymentReceiveWithInvoices(
tenantId: number,
paymentReceiveId: number
) {
const { PaymentReceive } = this.tenancy.models(tenantId); const { PaymentReceive } = this.tenancy.models(tenantId);
return PaymentReceive.query() return PaymentReceive.query()
.where('id', paymentReceiveId) .where('id', paymentReceiveId)
@@ -484,7 +608,9 @@ export default class PaymentReceiveService {
const { Account, AccountTransaction } = this.tenancy.models(tenantId); const { Account, AccountTransaction } = this.tenancy.models(tenantId);
const paymentAmount = sumBy(paymentReceive.entries, 'payment_amount'); const paymentAmount = sumBy(paymentReceive.entries, 'payment_amount');
const formattedDate = moment(paymentReceive.payment_date).format('YYYY-MM-DD'); const formattedDate = moment(paymentReceive.payment_date).format(
'YYYY-MM-DD'
);
const receivableAccount = await this.accountsService.getAccountByType( const receivableAccount = await this.accountsService.getAccountByType(
tenantId, tenantId,
'accounts_receivable' 'accounts_receivable'
@@ -540,7 +666,7 @@ export default class PaymentReceiveService {
public async saveChangeInvoicePaymentAmount( public async saveChangeInvoicePaymentAmount(
tenantId: number, tenantId: number,
newPaymentReceiveEntries: IPaymentReceiveEntryDTO[], newPaymentReceiveEntries: IPaymentReceiveEntryDTO[],
oldPaymentReceiveEntries?: IPaymentReceiveEntryDTO[], oldPaymentReceiveEntries?: IPaymentReceiveEntryDTO[]
): Promise<void> { ): Promise<void> {
const { SaleInvoice } = this.tenancy.models(tenantId); const { SaleInvoice } = this.tenancy.models(tenantId);
const opers: Promise<void>[] = []; const opers: Promise<void>[] = [];
@@ -549,10 +675,12 @@ export default class PaymentReceiveService {
newPaymentReceiveEntries, newPaymentReceiveEntries,
oldPaymentReceiveEntries, oldPaymentReceiveEntries,
'paymentAmount', 'paymentAmount',
'invoiceId', 'invoiceId'
); );
diffEntries.forEach((diffEntry: any) => { diffEntries.forEach((diffEntry: any) => {
if (diffEntry.paymentAmount === 0) { return; } if (diffEntry.paymentAmount === 0) {
return;
}
const oper = SaleInvoice.changePaymentAmount( const oper = SaleInvoice.changePaymentAmount(
diffEntry.invoiceId, diffEntry.invoiceId,