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={classNames(Classes.MINIMAL, 'button--table-views')}
className={Classes.MINIMAL} icon={<Icon icon="cog-16" iconSize={16} />}
text={<T id={'hide_filter'} />} 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: { basis: 'cash',
...pageFilter, ...pageFilter,
basis: 'cash', fromDate: moment(pageFilter.fromDate).toDate(),
from_date: moment(pageFilter.from_date).toDate(), toDate: moment(pageFilter.toDate).toDate(),
to_date: moment(pageFilter.to_date).toDate(), };
none_zero: false,
}, // Validation schema.
validationSchema: Yup.object().shape({ const validationSchema = Yup.object().shape({
from_date: Yup.date() dateRange: Yup.string().optional(),
.required() fromDate: Yup.date()
.label(formatMessage({ id: 'from_data' })), .required()
to_date: Yup.date() .label(formatMessage({ id: 'fromDate' })),
.min(Yup.ref('from_date')) toDate: Yup.date()
.required() .min(Yup.ref('fromDate'))
.label(formatMessage({ id: 'to_date' })), .required()
none_zero: Yup.boolean(), .label(formatMessage({ id: 'toDate' })),
}), accountsFilter: Yup.string(),
onSubmit: (values, actions) => { displayColumnsType: Yup.string(),
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> <Formik
<Col width={'100%'} /> initialValues={initialValues}
</Visible> validationSchema={validationSchema}
onSubmit={handleSubmit}
<Col width={260} offset={10}> >
<SelectDisplayColumnsBy onItemSelect={onItemSelectDisplayColumns} /> <Form>
</Col> <Tabs animate={true} vertical={true} renderActiveTabPanelOnly={true}>
<Tab
<Col width={260}> id="general"
<FormGroup title={<T id={'general'} />}
label={<T id={'filter_accounts'} />} panel={<BalanceSheetHeaderGeneralPanal />}
className="form-group--select-list bp3-fill"
inline={false}
>
<FinancialAccountsFilter
initialSelectedItem={'all-accounts'}
onItemSelect={handleAccountsFilterSelect}
/> />
</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,73 +1,70 @@
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} position={Position.RIGHT_TOP}
position={Position.RIGHT_TOP} content={item.hint}
content={item.hint} modifiers={SUBMENU_POPOVER_MODIFIERS}
modifiers={SUBMENU_POPOVER_MODIFIERS} inline={true}
inline={true} minimal={true}
minimal={true} className={MODIFIER.SELECT_LIST_TOOLTIP_ITEMS}
className={MODIFIER.SELECT_LIST_TOOLTIP_ITEMS} >
> <MenuItem text={item.name} key={item.key} onClick={handleClick} />
<MenuItem text={item.name} key={item.key} onClick={handleClick} /> </Tooltip>
</Tooltip> );
); };
},
[],
);
return ( return (
<ListSelect <Row>
items={filterAccountsOptions} <Col xs={4}>
itemRenderer={filterAccountRenderer} <FastField name={'accountsFilter'}>
popoverProps={{ minimal: true, }} {({ form: { setFieldValue }, field: { value } }) => (
filterable={false} <FormGroup
selectedItemProp={'key'} label={<T id={'filter_accounts'} />}
labelProp={'name'} className="form-group--select-list bp3-fill"
// className={} inline={false}
{...restProps} >
/> <ListSelect
items={filterAccountsOptions}
itemRenderer={filterAccountRenderer}
popoverProps={{ minimal: true }}
filterable={false}
selectedItem={value}
selectedItemProp={'key'}
labelProp={'name'}
onItemSelect={(item) => {
setFieldValue('accountsFilter', item.key);
}}
className={classNames(CLASSES.SELECT_LIST_FILL_POPOVER)}
{...restProps}
/>
</FormGroup>
)}
</FastField>
</Col>
</Row>
); );
} }

View File

@@ -1,106 +1,120 @@
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>
<FormGroup <Col xs={4}>
label={intl.formatMessage({ id: 'report_date_range' })} <FastField name={'date_range'}>
labelInfo={<Hint />} {({
minimal={true} form: { setFieldValue },
fill={true} field: { value },
> }) => (
<HTMLSelect <FormGroup
fill={true} label={formatMessage({ id: 'report_date_range' })}
options={dateRangeOptions} labelInfo={<Hint />}
value={reportDateRange} minimal={true}
onChange={handleDateRangeChange} fill={true}
/> >
</FormGroup> <HTMLSelect
</Col> fill={true}
options={dateRangeOptions}
value={value}
onChange={(e) => {
const newValue = e.target.value;
<Col width={260}> if (newValue !== 'custom') {
<FormGroup const dateRange = parseDateRangeQuery(newValue);
label={intl.formatMessage({ id: 'from_date' })}
labelInfo={<Hint />}
fill={true}
intent={formik.errors.from_date && Intent.DANGER}
>
<DateInput
{...momentFormatter('YYYY/MM/DD')}
value={formik.values.from_date}
onChange={handleDateChange('from_date')}
popoverProps={{ position: Position.BOTTOM }}
minimal={true}
fill={true}
/>
</FormGroup>
</Col>
<Col width={260}> if (dateRange) {
<FormGroup setFieldValue('fromDate', moment(dateRange.fromDate).toDate());
label={intl.formatMessage({ id: 'to_date' })} setFieldValue('toDate', moment(dateRange.toDate).toDate());
labelInfo={<Hint />} }
fill={true} }
intent={formik.errors.to_date && Intent.DANGER} setFieldValue('dateRange', newValue);
> }}
<DateInput />
{...momentFormatter('YYYY/MM/DD')} </FormGroup>
value={formik.values.to_date} )}
onChange={handleDateChange('to_date')} </FastField>
popoverProps={{ position: Position.BOTTOM }} </Col>
fill={true} </Row>
minimal={true}
intent={formik.errors.to_date && Intent.DANGER} <Row>
/> <Col xs={4}>
</FormGroup> <FastField name={'fromDate'}>
</Col> {({
form: { setFieldValue },
field: { value },
meta: { error, touched },
}) => (
<FormGroup
label={formatMessage({ id: 'from_date' })}
labelInfo={<Hint />}
fill={true}
intent={error && Intent.DANGER}
helperText={<ErrorMessage name={'fromDate'} />}
>
<DateInput
{...momentFormatter('YYYY-MM-DD')}
value={value}
onChange={(selectedDate) => {
setFieldValue('fromDate', selectedDate);
}}
popoverProps={{ minimal: true, position: Position.BOTTOM }}
canClearSelection={false}
minimal={true}
fill={true}
/>
</FormGroup>
)}
</FastField>
</Col>
<Col xs={4}>
<FastField name={'toDate'}>
{({
form: { setFieldValue },
field: { value },
meta: { error },
}) => (
<FormGroup
label={formatMessage({ id: 'to_date' })}
labelInfo={<Hint />}
fill={true}
intent={error && Intent.DANGER}
helperText={<ErrorMessage name={'toDate'} />}
>
<DateInput
{...momentFormatter('YYYY-MM-DD')}
value={value}
onChange={(selectedDate) => {
setFieldValue('toDate', selectedDate);
}}
popoverProps={{ minimal: true, position: Position.BOTTOM }}
canClearSelection={false}
fill={true}
minimal={true}
intent={error && Intent.DANGER}
/>
</FormGroup>
)}
</FastField>
</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,
},
)}
> >
{children} <Drawer
isOpen={isOpen}
usePortal={false}
hasBackdrop={true}
position={Position.TOP}
canOutsideClickClose={true}
canEscapeKeyClose={true}
{...drawerProps}
>
{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,73 +16,101 @@ 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.
useEffect(() => { useEffect(() => {
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(
const parsedFilter = { (filter) => {
...filter, const parsedFilter = {
from_date: moment(filter.from_date).format('YYYY-MM-DD'), ...filter,
to_date: moment(filter.to_date).format('YYYY-MM-DD'), fromDate: moment(filter.fromDate).format('YYYY-MM-DD'),
}; toDate: moment(filter.toDate).format('YYYY-MM-DD'),
setFilter(parsedFilter); };
}, [setFilter]); setFilter(parsedFilter);
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,62 +43,50 @@ 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={classNames(Classes.MINIMAL, 'button--table-views')}
className={Classes.MINIMAL} icon={<Icon icon="cog-16" iconSize={16} />}
text={<T id={'hide_filter'} />} text={
icon={<Icon icon="arrow-to-top" />} generalLedgerSheetFilter ? (
onClick={handleFilterClick} <T id={'hide_customizer'} />
/> ) : (
</If> <T id={'customize_report'} />
)
<If condition={!generalLedgerSheetFilter}> }
<Button onClick={handleCustomizeClick}
className={Classes.MINIMAL} active={generalLedgerSheetFilter}
text={<T id={'show_filter'} />} />
icon={<Icon icon="arrow-to-bottom" />} <NavbarDivider />
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>
</DashboardActionsBar> </DashboardActionsBar>
@@ -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, fromDate: moment(pageFilter.fromDate).toDate(),
from_date: moment(pageFilter.from_date).toDate(), toDate: moment(pageFilter.toDate).toDate(),
to_date: moment(pageFilter.to_date).toDate(), };
},
validationSchema: Yup.object().shape({ // Validation schema.
from_date: Yup.date().required(), const validationSchema = Yup.object().shape({
to_date: Yup.date().min(Yup.ref('from_date')).required(), dateRange: Yup.string().optional(),
}), fromDate: Yup.date().required(),
onSubmit(values, actions) { toDate: Yup.date().min(Yup.ref('fromDate')).required(),
onSubmitFilter(values);
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> <Formik
validationSchema={validationSchema}
<Col width={260}> initialValues={initialValues}
<FormGroup onSubmit={handleSubmit}
label={<T id={'specific_accounts'} />} >
className={classNames('form-group--select-list', Classes.FILL)} <Form>
> <Tabs animate={true} vertical={true} renderActiveTabPanelOnly={true}>
<AccountsMultiSelect <Tab
accounts={accountsList} id="general"
onAccountSelected={onAccountSelected} 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,35 +26,29 @@ 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'; case ROW_TYPE.CLOSING_BALANCE:
case ROW_TYPE.CLOSING_BALANCE: return 'Closing Balance';
return 'Closing Balance'; 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, ROW_TYPE.TRANSACTION,
ROW_TYPE.TRANSACTION, ];
];
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={classNames(Classes.MINIMAL, 'button--table-views')}
className={Classes.MINIMAL} icon={<Icon icon="cog-16" iconSize={16} />}
text={<T id={'show_filter'} />} text={
icon={<Icon icon="arrow-to-bottom" />} (journalSheetFilter) ? (
onClick={handleFilterToggleClick} <T id={'hide_customizer'} />
/> ) : (
</If> <T id={'customize_report'} />
)
}
active={journalSheetFilter}
onClick={handleFilterToggleClick}
/>
<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, ...pageFilter,
initialValues: { fromDate: moment(pageFilter.fromDate).toDate(),
...pageFilter, toDate: moment(pageFilter.toDate).toDate(),
from_date: moment(pageFilter.from_date).toDate(), };
to_date: moment(pageFilter.to_date).toDate(),
}, // Validation schema.
validationSchema: Yup.object().shape({ const validationSchema = Yup.object().shape({
from_date: Yup.date().required(), fromDate: Yup.date().required(),
to_date: Yup.date().min(Yup.ref('from_date')).required(), toDate: Yup.date().min(Yup.ref('fromDate')).required(),
}),
onSubmit: (values, { setSubmitting }) => {
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(), .required()
to_date: moment(pageFilter.to_date).toDate(), .label(formatMessage({ id: 'from_date' })),
}, toDate: Yup.date()
validationSchema: Yup.object().shape({ .min(Yup.ref('fromDate'))
from_date: Yup.date() .required()
.required() .label(formatMessage({ id: 'to_date' })),
.label(formatMessage({ id: 'from_date' })), accountsFilter: Yup.string(),
to_date: Yup.date() displayColumnsType: Yup.string(),
.min(Yup.ref('from_date'))
.required()
.label(formatMessage({ id: 'to_date' })),
}),
onSubmit: (values, actions) => {
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> >
<Formik
<Col width={260}> validationSchema={validationSchema}
<SelectsListColumnsBy onItemSelect={handleItemSelectDisplayColumns} /> initialValues={initialValues}
</Col> onSubmit={handleSubmit}
>
<Col width={260}> <Form>
<FormGroup <Tabs animate={true} vertical={true} renderActiveTabPanelOnly={true}>
label={<T id={'filter_accounts'} />} <Tab
className="form-group--select-list bp3-fill" id="general"
inline={false} title={<T id={'general'} />}
> panel={<ProfitLossSheetHeaderGeneralPane />}
<FinancialAccountsFilter
initialSelectedItem={'all-accounts'}
onItemSelect={handleAccountsFilterSelect}
/> />
</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 {handleStringChange} from 'utils'; import { FastField } from 'formik';
import {useIntl} from 'react-intl'; import { handleStringChange } from 'utils';
import { import { useIntl } from 'react-intl';
RadioGroup, import { RadioGroup, Radio } from '@blueprintjs/core';
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 (
<RadioGroup <FastField name={'basis'}>
inline={true} {({
label={formatMessage({'id': 'accounting_basis'})} form: { setFieldValue },
name="basis" field: { value },
onChange={handleStringChange((value) => { }) => (
onChange && onChange(value); <RadioGroup
})} inline={true}
className={'radio-group---accounting-basis'} label={formatMessage({ id: 'accounting_basis' })}
{...rest}> name="basis"
<Radio label={formatMessage({id:'cash'})} value="cash" /> onChange={handleStringChange((value) => {
<Radio label={formatMessage({id:'accrual'})} value="accural" /> setFieldValue(key, value);
</RadioGroup> })}
className={'radio-group---accounting-basis'}
selectedValue={value}
{...rest}
>
<Radio label={formatMessage({ id: 'cash' })} value="cash" />
<Radio label={formatMessage({ id: 'accrual' })} value="accural" />
</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 (
<FormGroup <Row>
label={<T id={'display_report_columns'}/>} <Col xs={4}>
className="form-group-display-columns-by form-group--select-list bp3-fill" <FastField name={'displayColumnsType'}>
inline={false} {({ form, field: { value }, meta: { error, touched } }) => (
{...formGroupProps}> <FormGroup
label={<T id={'display_report_columns'} />}
<SelectList className="form-group-display-columns-by form-group--select-list bp3-fill"
items={displayColumnsByOptions} inline={false}
noResults={<MenuItem disabled={true} text="No results." />} {...formGroupProps}
filterable={false} >
itemRenderer={itemRenderer} <ListSelect
popoverProps={{ minimal: true, usePortal: false, inline: true }} items={displayColumnsByOptions}
buttonLabel={buttonLabel} filterable={false}
onItemSelect={handleItemSelect} selectedItem={value}
className={classNames(MODIFIER.SELECT_LIST_FILL_POPOVER)} selectedItemProp={'key'}
{...selectListProps} /> labelProp={'name'}
</FormGroup> onItemSelect={(item) => {
form.setFieldValue('displayColumnsType', item.key);
}}
popoverProps={{ minimal: true }}
{...selectListProps}
/>
</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(), .required()
to_date: moment(pageFilter.to_date).toDate(), .label(formatMessage({ id: 'from_date' })),
}, toDate: Yup.date()
validationSchema: Yup.object().shape({ .min(Yup.ref('fromDate'))
from_date: Yup.date() .required()
.required() .label(formatMessage({ id: 'to_date' })),
.label(formatMessage({ id: 'from_date' })),
to_date: Yup.date()
.min(Yup.ref('from_date'))
.required()
.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> <Formik
<Col width={'100%'} /> initialValues={initialValues}
</Visible> validationSchema={validationSchema}
onSubmit={handleSubmit}
<Col width={260}> >
<FormGroup <Form>
label={<T id={'filter_accounts'} />} <Tabs animate={true} vertical={true} renderActiveTabPanelOnly={true}>
className="form-group--select-list bp3-fill" <Tab
inline={false} id="general"
> title={<T id={'general'} />}
<FinancialAccountsFilter panel={<TrialBalanceSheetHeaderGeneralPanel />}
initialSelectedItem={'all-accounts'}
onItemSelect={handleAccountsFilterSelect}
/> />
</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',
@@ -929,7 +929,8 @@ export default {
'Are you sure you want to activate this item? You will be able to inactivate it later', 'Are you sure you want to activate this item? You will be able to inactivate it later',
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

@@ -112,7 +112,7 @@ export default [
component: LazyLoader({ component: LazyLoader({
loader: () => loader: () =>
import( import(
'containers/FinancialStatements/TrialBalanceSheet/TrialBalanceSheet' 'containers/FinancialStatements/TrialBalanceSheet/TrialBalanceSheet'
), ),
}), }),
breadcrumb: 'Trial Balance Sheet', breadcrumb: 'Trial Balance Sheet',
@@ -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(
return (sheet && sheet.columns) ? sheet.columns : []; sheetByTypeSelector(sheetType),
}; (sheet) => {
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(
return (sheet && sheet.query) ? sheet.query : {}; sheetByTypeSelector(sheetType),
}; (sheet) => {
return (sheet && sheet.query) ? sheet.query : {};
},
);
/**
* Retrieve financial statement accounts by the given sheet index.
*/
export const getFinancialSheetAccountsFactory = (sheetType) =>
createSelector(
sheetByTypeSelector(sheetType),
(sheet) => {
return (sheet && sheet.accounts) ? sheet.accounts : [];
}
);
export const getFinancialSheetAccounts = (sheets, index) => { /**
const sheet = getFinancialSheet(sheets, index); * Retrieve financial statement table rows by the given sheet index.
return (sheet && sheet.accounts) ? sheet.accounts : []; */
}; export const getFinancialSheetTableRowsFactory = (sheetType) =>
createSelector(
sheetByTypeSelector(sheetType),
export const getFinancialSheetTableRows = (sheets, index) => { (sheet) => {
const sheet = getFinancialSheet(sheets, index); 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,32 +6,7 @@
.financial-statement{ .financial-statement{
&__header{ &__header{
padding: 25px 26px 25px;
background: #FDFDFD;
&.is-hidden{
display: none;
}
.bp3-form-group,
.radio-group---accounting-basis{
.bp3-label{
font-weight: 500;
font-size: 13px;
color: #444;
}
}
.bp3-button.button--submit-filter{
min-height: 34px;
padding-left: 16px;
padding-right: 16px;
}
.radio-group---accounting-basis{
.bp3-label{
margin-bottom: 12px;
}
}
} }
&__body{ &__body{
@@ -41,25 +16,146 @@
justify-content: center; justify-content: center;
align-items: center; align-items: center;
} }
}
&__header.is-hidden + .financial-statement__body{ .financial-header-drawer{
.financial-sheet{ padding: 25px 26px 25px;
margin-top: 40px; position: absolute;
top: 101px;
bottom: 0;
left: 0;
right: 0;
overflow: hidden;
&.is-hidden{
visibility: hidden;
}
.row{
.col{
max-width: 400px;
min-width: 250px;
}
}
.bp3-drawer{
box-shadow: 0 0 0 transparent;
max-height: 550px;
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{
min-height: 34px;
padding-left: 16px;
padding-right: 16px;
}
.radio-group---accounting-basis{
.bp3-label{
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;
}
}
.bp3-tabs.bp3-vertical{
flex: 1 0 0;
.bp3-tab-list{
width: 220px;
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;
}
}
.bp3-tab{
color: #333;
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,11 +142,10 @@ 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,
message: 'The payment receive has been created successfully.', message: 'The payment receive has been created successfully.',
@@ -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,13 +222,14 @@ 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({
payment_receive: this.transfromToResponse({ ...paymentReceive }), payment_receive: this.transfromToResponse({ ...paymentReceive }),
receivable_invoices: this.transfromToResponse([ ...receivableInvoices ]), receivable_invoices: this.transfromToResponse([...receivableInvoices]),
payment_invoices: this.transfromToResponse([ ...paymentReceiveInvoices ]), payment_invoices: this.transfromToResponse([...paymentReceiveInvoices]),
}); });
} catch (error) { } catch (error) {
next(error); next(error);
@@ -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, {
data: { type: 'INVOICES_NOT_DELIVERED_YET',
not_delivered_invoices_ids: error.payload.notDeliveredInvoices.map( code: 200,
(invoice) => invoice.id data: {
) not_delivered_invoices_ids: error.payload.notDeliveredInvoices.map(
} (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,16 +255,19 @@ 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 (
.map((structure: IBalanceSheetStructureSection) => reportStructure
this.balanceSheetStructureMapper(structure, balanceSheetAccounts) .map((structure: IBalanceSheetStructureSection) =>
) this.balanceSheetStructureMapper(structure, balanceSheetAccounts)
// Filter the structure sections that have no children. )
.filter((structure: IBalanceSheetSection) => // Filter the structure sections that have no children.
structure.children.length > 0 || structure._forceShow .filter(
); (structure: IBalanceSheetSection) =>
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,12 +1,12 @@
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 {
@Inject() @Inject()
tenancy: TenancyService; tenancy: TenancyService;
@@ -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.total.amount === 0 && this.query.noneZero) (account: IProfitLossSheetAccount) =>
!(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');
@@ -299,23 +309,24 @@ export default class ProfitLossSheet extends FinancialSheet {
* @param * @param
*/ */
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(
const entryInvoice = storedInvoicesMap.get(entry.invoiceId); (entry: IPaymentReceiveEntryDTO, index: number) => {
const { dueAmount } = entryInvoice; const entryInvoice = storedInvoicesMap.get(entry.invoiceId);
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,41 +268,66 @@ 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']), [
...formatDateFields(omit(paymentReceiveDTO, ['entries']), ['paymentDate']), 'paymentDate',
]),
entries: paymentReceiveDTO.entries.map((entry) => ({ entries: paymentReceiveDTO.entries.map((entry) => ({
...omit(entry, ['id']), ...omit(entry, ['id']),
})), })),
}); });
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']), [
...formatDateFields(omit(paymentReceiveDTO, ['entries']), ['paymentDate']), '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,10 +476,10 @@ 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()
.findById(paymentReceiveId) .findById(paymentReceiveId)
@@ -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,19 +543,29 @@ 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,
const { results, pagination } = await PaymentReceive.query().onBuild((builder) => { PaymentReceive,
builder.withGraphFetched('customer'); paymentReceivesFilter
builder.withGraphFetched('depositAccount');
dynamicFilter.buildQuery()(builder);
}).pagination(
paymentReceivesFilter.page - 1,
paymentReceivesFilter.pageSize,
); );
const { results, pagination } = await PaymentReceive.query()
.onBuild((builder) => {
builder.withGraphFetched('customer');
builder.withGraphFetched('depositAccount');
dynamicFilter.buildQuery()(builder);
})
.pagination(
paymentReceivesFilter.page - 1,
paymentReceivesFilter.pageSize
);
return { return {
paymentReceives: results, paymentReceives: results,
pagination, pagination,
@@ -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,
@@ -560,6 +688,6 @@ export default class PaymentReceiveService {
); );
opers.push(oper); opers.push(oper);
}); });
await Promise.all([ ...opers ]); await Promise.all([...opers]);
} }
} }