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',
PREFERENCES_PAGE: 'preferences-page',
PREFERENCES_PAGE_SIDEBAR: 'preferences-page__sidebar',
PREFERENCES_PAGE_TOPBAR: 'preferences-page__topbar',

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,126 +1,105 @@
import React, { useCallback, useEffect } from 'react';
import FinancialStatementHeader from 'containers/FinancialStatements/FinancialStatementHeader';
import { Row, Col, Visible } from 'react-grid-system';
import { FormGroup } from '@blueprintjs/core';
import React, { useEffect } from 'react';
import { Tabs, Tab, Button, Intent } from '@blueprintjs/core';
import { FormattedMessage as T, useIntl } from 'react-intl';
import moment from 'moment';
import * as Yup from 'yup';
import { useFormik } from 'formik';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { Formik, Form } from 'formik';
import FinancialStatementDateRange from 'containers/FinancialStatements/FinancialStatementDateRange';
import SelectDisplayColumnsBy from '../SelectDisplayColumnsBy';
import RadiosAccountingBasis from '../RadiosAccountingBasis';
import FinancialAccountsFilter from '../FinancialAccountsFilter';
import FinancialStatementHeader from 'containers/FinancialStatements/FinancialStatementHeader';
import withBalanceSheet from './withBalanceSheetDetail';
import withBalanceSheetActions from './withBalanceSheetActions';
import { compose } from 'utils';
import BalanceSheetHeaderGeneralPanal from './BalanceSheetHeaderGeneralPanal';
function BalanceSheetHeader({
// #ownProps
onSubmitFilter,
pageFilter,
show,
refresh,
// #withBalanceSheet
balanceSheetFilter,
// #withBalanceSheetActions
refreshBalanceSheet,
toggleBalanceSheetFilter,
}) {
const { formatMessage } = useIntl();
const formik = useFormik({
enableReinitialize: true,
initialValues: {
...pageFilter,
basis: 'cash',
from_date: moment(pageFilter.from_date).toDate(),
to_date: moment(pageFilter.to_date).toDate(),
none_zero: false,
},
validationSchema: Yup.object().shape({
from_date: Yup.date()
.required()
.label(formatMessage({ id: 'from_data' })),
to_date: Yup.date()
.min(Yup.ref('from_date'))
.required()
.label(formatMessage({ id: 'to_date' })),
none_zero: Yup.boolean(),
}),
onSubmit: (values, actions) => {
onSubmitFilter(values);
actions.setSubmitting(false);
},
// Filter form initial values.
const initialValues = {
basis: 'cash',
...pageFilter,
fromDate: moment(pageFilter.fromDate).toDate(),
toDate: moment(pageFilter.toDate).toDate(),
};
// Validation schema.
const validationSchema = Yup.object().shape({
dateRange: Yup.string().optional(),
fromDate: Yup.date()
.required()
.label(formatMessage({ id: 'fromDate' })),
toDate: Yup.date()
.min(Yup.ref('fromDate'))
.required()
.label(formatMessage({ id: 'toDate' })),
accountsFilter: Yup.string(),
displayColumnsType: Yup.string(),
});
// Handle item select of `display columns by` field.
const onItemSelectDisplayColumns = useCallback(
(item) => {
formik.setFieldValue('display_columns_type', item.type);
formik.setFieldValue('display_columns_by', item.by);
},
[formik],
);
// Handle form submit.
const handleSubmit = (values, actions) => {
onSubmitFilter(values);
toggleBalanceSheetFilter();
actions.setSubmitting(false);
};
const handleAccountingBasisChange = useCallback(
(value) => {
formik.setFieldValue('basis', value);
},
[formik],
);
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);
// Handle cancel button click.
const handleCancelClick = () => {
toggleBalanceSheetFilter();
};
// Handle drawer close action.
const handleDrawerClose = () => {
toggleBalanceSheetFilter();
};
return (
<FinancialStatementHeader show={show}>
<Row>
<FinancialStatementDateRange formik={formik} />
<Visible xl>
<Col width={'100%'} />
</Visible>
<Col width={260} offset={10}>
<SelectDisplayColumnsBy onItemSelect={onItemSelectDisplayColumns} />
</Col>
<Col width={260}>
<FormGroup
label={<T id={'filter_accounts'} />}
className="form-group--select-list bp3-fill"
inline={false}
>
<FinancialAccountsFilter
initialSelectedItem={'all-accounts'}
onItemSelect={handleAccountsFilterSelect}
<FinancialStatementHeader
isOpen={balanceSheetFilter}
drawerProps={{ onClose: handleDrawerClose }}
>
<Formik
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={handleSubmit}
>
<Form>
<Tabs animate={true} vertical={true} renderActiveTabPanelOnly={true}>
<Tab
id="general"
title={<T id={'general'} />}
panel={<BalanceSheetHeaderGeneralPanal />}
/>
</FormGroup>
</Col>
</Tabs>
<Col width={260}>
<RadiosAccountingBasis
selectedValue={formik.values.basis}
onChange={handleAccountingBasisChange}
/>
</Col>
</Row>
<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>
);
}
export default compose(
withBalanceSheet(({ balanceSheetRefresh }) => ({
refresh: balanceSheetRefresh,
withBalanceSheet(({ balanceSheetFilter }) => ({
balanceSheetFilter,
})),
withBalanceSheetActions,
)(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 { connect } from 'react-redux';
import { useIntl } from 'react-intl';
import classNames from 'classnames';
import Money from 'components/Money';
import FinancialSheet from 'components/FinancialSheet';
import DataTable from 'components/DataTable';
import withSettings from 'containers/Settings/withSettings';
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({
// #withPreferences
organizationSettings,
// #withBalanceSheetDetail
balanceSheetAccounts,
balanceSheetTableRows,
balanceSheetColumns,
balanceSheetQuery,
balanceSheetLoading,
// #ownProps
onFetchData,
companyName,
}) {
const { formatMessage } = useIntl();
@@ -33,35 +58,18 @@ function BalanceSheetTable({
() => [
{
Header: formatMessage({ id: 'account_name' }),
accessor: 'name',
accessor: (row) => (row.code ? `${row.name} - ${row.code}` : row.name),
className: 'account_name',
width: 120,
},
{
Header: formatMessage({ id: 'code' }),
accessor: 'code',
className: 'code',
width: 60,
width: 240,
},
...(balanceSheetQuery.display_columns_type === 'total'
? [
{
Header: formatMessage({ id: 'total' }),
accessor: 'balance.formatted_amount',
Cell: ({ cell }) => {
const row = cell.row.original;
if (row.total) {
return (
<Money
amount={row.total.formatted_amount}
currency={'USD'}
/>
);
}
return '';
},
Cell: TotalCell,
className: 'total',
width: 80,
width: 140,
},
]
: []),
@@ -70,44 +78,43 @@ function BalanceSheetTable({
id: `date_period_${index}`,
Header: column,
accessor: `total_periods[${index}]`,
Cell: ({ cell }) => {
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 '';
},
Cell: TotalPeriodCell(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.
const expandedRows = useMemo(
() => defaultExpanderReducer(balanceSheetTableRows, 3),
() => defaultExpanderReducer(balanceSheetTableRows, 4),
[balanceSheetTableRows],
);
const rowClassNames = (row) => {
const rowClassNames = useCallback((row) => {
const { original } = row;
console.log(row);
const rowTypes = Array.isArray(original.row_types)
? original.row_types
: [];
return {
[`row_type--${original.row_type}`]: original.row_type,
...rowTypes.reduce((acc, rowType) => {
acc[`row_type--${rowType}`] = rowType;
return acc;
}, {}),
};
};
}, []);
return (
<FinancialSheet
name="balance-sheet"
companyName={organizationSettings.name}
companyName={companyName}
sheetType={formatMessage({ id: 'balance_sheet' })}
fromDate={balanceSheetQuery.from_date}
toDate={balanceSheetQuery.to_date}
@@ -119,46 +126,29 @@ function BalanceSheetTable({
columns={columns}
data={balanceSheetTableRows}
rowClassNames={rowClassNames}
onFetchData={handleFetchData}
noInitialFetch={true}
expanded={expandedRows}
expandable={true}
expanded={expandedRows}
expandToggleColumn={1}
sticky={true}
expandColumnSpace={0.8}
sticky={true}
/>
</FinancialSheet>
);
}
const mapStateToProps = (state, props) => {
const { balanceSheetQuery } = props;
return {
balanceSheetIndex: getFinancialSheetIndexByQuery(
state.financialStatements.balanceSheet.sheets,
balanceSheetQuery,
),
};
};
const withBalanceSheetTable = connect(mapStateToProps);
export default compose(
withBalanceSheetTable,
withBalanceSheetDetail(
({
balanceSheetAccounts,
balanceSheetTableRows,
balanceSheetColumns,
balanceSheetQuery,
balanceSheetLoading,
}) => ({
balanceSheetAccounts,
balanceSheetTableRows,
balanceSheetColumns,
balanceSheetQuery,
balanceSheetLoading,
}),
),
withSettings,
)(BalanceSheetTable);

View File

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

View File

@@ -1,73 +1,70 @@
import React, { useMemo, useCallback } from 'react';
import React from 'react';
import {
PopoverInteractionKind,
Tooltip,
MenuItem,
Position,
FormGroup,
} from '@blueprintjs/core';
import { useIntl } from 'react-intl';
import { ListSelect, MODIFIER } from 'components';
import { FormattedMessage as T } from 'react-intl';
import classNames from 'classnames';
import { FastField } from 'formik';
export default function FinancialAccountsFilter({
...restProps
}) {
const { formatMessage } = useIntl();
const filterAccountsOptions = useMemo(
() => [
{
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],
);
import { CLASSES } from 'common/classes';
import { Col, Row, ListSelect, MODIFIER } from 'components';
import { filterAccountsOptions } from './common';
export default function FinancialAccountsFilter({ ...restProps }) {
const SUBMENU_POPOVER_MODIFIERS = {
flip: { boundariesElement: 'viewport', padding: 20 },
offset: { offset: '0, 10' },
preventOverflow: { boundariesElement: 'viewport', padding: 40 },
};
const filterAccountRenderer = useCallback(
(item, { handleClick, modifiers, query }) => {
return (
<Tooltip
interactionKind={PopoverInteractionKind.HOVER}
position={Position.RIGHT_TOP}
content={item.hint}
modifiers={SUBMENU_POPOVER_MODIFIERS}
inline={true}
minimal={true}
className={MODIFIER.SELECT_LIST_TOOLTIP_ITEMS}
>
<MenuItem text={item.name} key={item.key} onClick={handleClick} />
</Tooltip>
);
},
[],
);
const filterAccountRenderer = (item, { handleClick, modifiers, query }) => {
return (
<Tooltip
interactionKind={PopoverInteractionKind.HOVER}
position={Position.RIGHT_TOP}
content={item.hint}
modifiers={SUBMENU_POPOVER_MODIFIERS}
inline={true}
minimal={true}
className={MODIFIER.SELECT_LIST_TOOLTIP_ITEMS}
>
<MenuItem text={item.name} key={item.key} onClick={handleClick} />
</Tooltip>
);
};
return (
<ListSelect
items={filterAccountsOptions}
itemRenderer={filterAccountRenderer}
popoverProps={{ minimal: true, }}
filterable={false}
selectedItemProp={'key'}
labelProp={'name'}
// className={}
{...restProps}
/>
<Row>
<Col xs={4}>
<FastField name={'accountsFilter'}>
{({ form: { setFieldValue }, field: { value } }) => (
<FormGroup
label={<T id={'filter_accounts'} />}
className="form-group--select-list bp3-fill"
inline={false}
>
<ListSelect
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 { Row, Col } from 'react-grid-system';
import { momentFormatter } from 'utils';
import React from 'react';
import { FastField, ErrorMessage } from 'formik';
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 { useIntl } from 'react-intl';
import { HTMLSelect, FormGroup, Intent, Position } from '@blueprintjs/core';
import { Hint } from 'components';
import { parseDateRangeQuery } from 'utils';
import { dateRangeOptions } from 'containers/FinancialStatements/common';
export default function FinancialStatementDateRange({ formik }) {
const intl = useIntl();
const [reportDateRange, setReportDateRange] = useState('this_year');
const dateRangeOptions = useMemo(
() => [
{ 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],
);
/**
* Financial statement - Date range select.
*/
export default function FinancialStatementDateRange() {
const { formatMessage } = useIntl();
return (
<>
<Col width={260}>
<FormGroup
label={intl.formatMessage({ id: 'report_date_range' })}
labelInfo={<Hint />}
minimal={true}
fill={true}
>
<HTMLSelect
fill={true}
options={dateRangeOptions}
value={reportDateRange}
onChange={handleDateRangeChange}
/>
</FormGroup>
</Col>
<Row>
<Col xs={4}>
<FastField name={'date_range'}>
{({
form: { setFieldValue },
field: { value },
}) => (
<FormGroup
label={formatMessage({ id: 'report_date_range' })}
labelInfo={<Hint />}
minimal={true}
fill={true}
>
<HTMLSelect
fill={true}
options={dateRangeOptions}
value={value}
onChange={(e) => {
const newValue = e.target.value;
<Col width={260}>
<FormGroup
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>
if (newValue !== 'custom') {
const dateRange = parseDateRangeQuery(newValue);
<Col width={260}>
<FormGroup
label={intl.formatMessage({ id: 'to_date' })}
labelInfo={<Hint />}
fill={true}
intent={formik.errors.to_date && Intent.DANGER}
>
<DateInput
{...momentFormatter('YYYY/MM/DD')}
value={formik.values.to_date}
onChange={handleDateChange('to_date')}
popoverProps={{ position: Position.BOTTOM }}
fill={true}
minimal={true}
intent={formik.errors.to_date && Intent.DANGER}
/>
</FormGroup>
</Col>
if (dateRange) {
setFieldValue('fromDate', moment(dateRange.fromDate).toDate());
setFieldValue('toDate', moment(dateRange.toDate).toDate());
}
}
setFieldValue('dateRange', newValue);
}}
/>
</FormGroup>
)}
</FastField>
</Col>
</Row>
<Row>
<Col xs={4}>
<FastField name={'fromDate'}>
{({
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 { 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 (
<div
className={classNames('financial-statement__header', {
'is-hidden': !show,
})}
className={classNames(
'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>
);
}

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

View File

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

View File

@@ -1,113 +1,100 @@
import React, { useEffect, useCallback } from 'react';
import { Button, FormGroup, Classes } from '@blueprintjs/core';
import { Row, Col, Visible } from 'react-grid-system';
import React from 'react';
import moment from 'moment';
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 AccountsMultiSelect from 'components/AccountsMultiSelect';
import FinancialStatementHeader from 'containers/FinancialStatements/FinancialStatementHeader';
import withAccounts from 'containers/Accounts/withAccounts';
import classNames from 'classnames';
import FinancialStatementDateRange from 'containers/FinancialStatements/FinancialStatementDateRange';
import RadiosAccountingBasis from '../RadiosAccountingBasis';
import GeneralLedgerHeaderGeneralPane from './GeneralLedgerHeaderGeneralPane';
import withGeneralLedger from './withGeneralLedger';
import withGeneralLedgerActions from './withGeneralLedgerActions';
import { compose } from 'utils';
/**
* Geenral Ledger (GL) - Header.
*/
function GeneralLedgerHeader({
// #ownProps
onSubmitFilter,
pageFilter,
// #withAccounts
accountsList,
// #withGeneralLedgerActions
refreshGeneralLedgerSheet,
toggleGeneralLedgerSheetFilter,
// #withGeneralLedger
generalLedgerSheetFilter,
generalLedgerSheetRefresh
}) {
const formik = useFormik({
enableReinitialize: true,
initialValues: {
...pageFilter,
from_date: moment(pageFilter.from_date).toDate(),
to_date: moment(pageFilter.to_date).toDate(),
},
validationSchema: Yup.object().shape({
from_date: Yup.date().required(),
to_date: Yup.date().min(Yup.ref('from_date')).required(),
}),
onSubmit(values, actions) {
onSubmitFilter(values);
actions.setSubmitting(false);
},
// Initial values.
const initialValues = {
...pageFilter,
fromDate: moment(pageFilter.fromDate).toDate(),
toDate: moment(pageFilter.toDate).toDate(),
};
// Validation schema.
const validationSchema = Yup.object().shape({
dateRange: Yup.string().optional(),
fromDate: Yup.date().required(),
toDate: Yup.date().min(Yup.ref('fromDate')).required(),
});
const onAccountSelected = useCallback((selectedAccounts) => {
formik.setFieldValue('accounts_ids', Object.keys(selectedAccounts));
}, [formik.setFieldValue]);
// Handle form submit.
const handleSubmit = (values, { setSubmitting }) => {
onSubmitFilter(values);
toggleGeneralLedgerSheetFilter();
setSubmitting(false);
};
const handleAccountingBasisChange = useCallback(
(value) => {
formik.setFieldValue('basis', value);
},
[formik],
);
// handle submit filter submit button.
useEffect(() => {
if (generalLedgerSheetRefresh) {
formik.submitForm();
refreshGeneralLedgerSheet(false);
}
}, [formik, generalLedgerSheetRefresh])
// Handle cancel button click.
const handleCancelClick = () => {
toggleGeneralLedgerSheetFilter(false);
};
// Handle drawer close action.
const handleDrawerClose = () => {
toggleGeneralLedgerSheetFilter(false);
};
return (
<FinancialStatementHeader show={generalLedgerSheetFilter}>
<Row>
<FinancialStatementDateRange formik={formik} />
<Visible xl><Col width={'100%'} /></Visible>
<Col width={260}>
<FormGroup
label={<T id={'specific_accounts'} />}
className={classNames('form-group--select-list', Classes.FILL)}
>
<AccountsMultiSelect
accounts={accountsList}
onAccountSelected={onAccountSelected}
<FinancialStatementHeader
isOpen={generalLedgerSheetFilter}
drawerProps={{ onClose: handleDrawerClose }}
>
<Formik
validationSchema={validationSchema}
initialValues={initialValues}
onSubmit={handleSubmit}
>
<Form>
<Tabs animate={true} vertical={true} renderActiveTabPanelOnly={true}>
<Tab
id="general"
title={<T id={'general'} />}
panel={<GeneralLedgerHeaderGeneralPane />}
/>
</FormGroup>
</Col>
</Tabs>
<Col width={260}>
<RadiosAccountingBasis
onChange={handleAccountingBasisChange}
selectedValue={formik.values.basis}
/>
</Col>
<div class="financial-header-drawer__footer">
<Button className={'mr1'} intent={Intent.PRIMARY} type={'submit'}>
<T id={'calculate_report'} />
</Button>
</Row>
<Button onClick={handleCancelClick} minimal={true}>
<T id={'cancel'} />
</Button>
</div>
</Form>
</Formik>
</FinancialStatementHeader>
);
}
export default compose(
withAccounts(({ accountsList }) => ({
accountsList,
})),
withGeneralLedger(({ generalLedgerSheetFilter, generalLedgerSheetRefresh }) => ({
withGeneralLedger(({ generalLedgerSheetFilter }) => ({
generalLedgerSheetFilter,
generalLedgerSheetRefresh,
})),
withGeneralLedgerActions,
)(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 moment from 'moment';
import { connect } from 'react-redux';
import { defaultExpanderReducer, compose } from 'utils';
import { useIntl } from 'react-intl';
@@ -8,7 +7,6 @@ import FinancialSheet from 'components/FinancialSheet';
import DataTable from 'components/DataTable';
import Money from 'components/Money';
import { getFinancialSheetIndexByQuery } from 'store/financialStatement/financialStatements.selectors';
import withGeneralLedger from './withGeneralLedger';
const ROW_TYPE = {
@@ -20,7 +18,6 @@ const ROW_TYPE = {
function GeneralLedgerTable({
companyName,
onFetchData,
generalLedgerSheetLoading,
generalLedgerTableRows,
@@ -29,35 +26,29 @@ function GeneralLedgerTable({
const { formatMessage } = useIntl();
// Account name column accessor.
const accountNameAccessor = useCallback(
(row) => {
switch (row.rowType) {
case ROW_TYPE.OPENING_BALANCE:
return 'Opening Balance';
case ROW_TYPE.CLOSING_BALANCE:
return 'Closing Balance';
default:
return row.name;
}
},
[ROW_TYPE],
);
const accountNameAccessor = (row) => {
switch (row.rowType) {
case ROW_TYPE.OPENING_BALANCE:
return 'Opening Balance';
case ROW_TYPE.CLOSING_BALANCE:
return 'Closing Balance';
default:
return row.name;
}
};
// Date accessor.
const dateAccessor = useCallback(
(row) => {
const TYPES = [
ROW_TYPE.OPENING_BALANCE,
ROW_TYPE.CLOSING_BALANCE,
ROW_TYPE.TRANSACTION,
];
const dateAccessor = (row) => {
const TYPES = [
ROW_TYPE.OPENING_BALANCE,
ROW_TYPE.CLOSING_BALANCE,
ROW_TYPE.TRANSACTION,
];
return TYPES.indexOf(row.rowType) !== -1
? moment(row.date).format('DD MMM YYYY')
: '';
},
[moment, ROW_TYPE],
);
return TYPES.indexOf(row.rowType) !== -1
? moment(row.date).format('DD MMM YYYY')
: '';
};
// Amount cell
const amountCell = useCallback(({ cell }) => {
@@ -73,10 +64,6 @@ function GeneralLedgerTable({
return <Money amount={transaction.amount} currency={'USD'} />;
}, []);
const referenceLink = useCallback((row) => {
return <a href="">{row.referenceId}</a>;
});
const columns = useMemo(
() => [
{
@@ -99,7 +86,7 @@ function GeneralLedgerTable({
},
{
Header: formatMessage({ id: 'trans_num' }),
accessor: referenceLink,
accessor: 'reference_id',
className: 'transaction_number',
width: 110,
},
@@ -125,10 +112,6 @@ function GeneralLedgerTable({
[],
);
const handleFetchData = useCallback(() => {
onFetchData && onFetchData();
}, [onFetchData]);
// Default expanded rows of general ledger table.
const expandedRows = useMemo(
() => defaultExpanderReducer(generalLedgerTableRows, 1),
@@ -140,12 +123,11 @@ function GeneralLedgerTable({
return (
<FinancialSheet
companyName={companyName}
// sheetType={formatMessage({ id: 'general_ledger_sheet' })}
sheetType={formatMessage({ id: 'general_ledger_sheet' })}
fromDate={generalLedgerQuery.from_date}
toDate={generalLedgerQuery.to_date}
name="general-ledger"
loading={generalLedgerSheetLoading}
minimal={true}
fullWidth={true}
>
<DataTable
@@ -155,7 +137,6 @@ function GeneralLedgerTable({
})}
columns={columns}
data={generalLedgerTableRows}
onFetchData={handleFetchData}
rowClassNames={rowClassNames}
expanded={expandedRows}
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(
withGeneralLedgerTable,
withGeneralLedger(
({
generalLedgerTableRows,

View File

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

View File

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

View File

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

View File

@@ -1,12 +1,12 @@
import React, { useCallback, useEffect } from 'react';
import { Row, Col } from 'react-grid-system';
import { Button } from '@blueprintjs/core';
import React from 'react';
import moment from 'moment';
import { useFormik } from 'formik';
import { FormattedMessage as T } from 'react-intl';
import { Formik, Form } from 'formik';
import { Tab, Tabs, Button, Intent } from '@blueprintjs/core';
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 withJournal from './withJournal';
@@ -23,49 +23,75 @@ function JournalHeader({
// #withJournalActions
refreshJournalSheet,
toggleJournalSheetFilter,
// #withJournal
journalSheetFilter,
journalSheetRefresh,
}) {
const formik = useFormik({
enableReinitialize: true,
initialValues: {
...pageFilter,
from_date: moment(pageFilter.from_date).toDate(),
to_date: moment(pageFilter.to_date).toDate(),
},
validationSchema: Yup.object().shape({
from_date: Yup.date().required(),
to_date: Yup.date().min(Yup.ref('from_date')).required(),
}),
onSubmit: (values, { setSubmitting }) => {
onSubmitFilter(values);
setSubmitting(false);
},
const initialValues = {
...pageFilter,
fromDate: moment(pageFilter.fromDate).toDate(),
toDate: moment(pageFilter.toDate).toDate(),
};
// Validation schema.
const validationSchema = Yup.object().shape({
fromDate: Yup.date().required(),
toDate: Yup.date().min(Yup.ref('fromDate')).required(),
});
useEffect(() => {
if (journalSheetRefresh) {
formik.submitForm();
refreshJournalSheet(false);
}
}, [formik, journalSheetRefresh]);
// Handle form submit.
const handleSubmit = (values, { setSubmitting }) => {
onSubmitFilter(values);
setSubmitting(false);
toggleJournalSheetFilter();
};
// Handle cancel journal drawer header.
const handleCancelClick = () => {
toggleJournalSheetFilter();
};
const handleDrawerClose = () => {
toggleJournalSheetFilter();
};
return (
<FinancialStatementHeader show={journalSheetFilter}>
<Row>
<FinancialStatementDateRange formik={formik} />
</Row>
<FinancialStatementHeader
isOpen={journalSheetFilter}
drawerProps={{ onClose: handleDrawerClose }}
>
<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>
);
}
export default compose(
withJournal(({
journalSheetFilter,
journalSheetRefresh
}) => ({
withJournal(({ journalSheetFilter, journalSheetRefresh }) => ({
journalSheetFilter,
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 { connect } from 'react-redux';
import moment from 'moment';
import { useIntl } from 'react-intl';
import FinancialSheet from 'components/FinancialSheet';
import DataTable from 'components/DataTable';
import { compose, defaultExpanderReducer } from 'utils';
import Money from 'components/Money';
import { getFinancialSheetIndexByQuery } from 'store/financialStatement/financialStatements.selectors';
import withJournal from './withJournal';
import { compose, defaultExpanderReducer } from 'utils';
function JournalSheetTable({
// #withJournal
journalSheetTableRows,
@@ -106,12 +104,12 @@ function JournalSheetTable({
return (
<FinancialSheet
companyName={companyName}
// sheetType={formatMessage({ id: 'journal_sheet' })}
sheetType={formatMessage({ id: 'journal_sheet' })}
fromDate={journalSheetQuery.from_date}
toDate={journalSheetQuery.to_date}
name="journal"
loading={journalSheetLoading}
minimal={true}
// minimal={true}
fullWidth={true}
>
<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(
withJournalTable,
withJournal(
({ journalSheetTableRows, journalSheetLoading, journalSheetQuery }) => ({
journalSheetTableRows,

View File

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

View File

@@ -1,18 +1,24 @@
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 classNames from 'classnames';
import Icon from 'components/Icon';
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
import { If } from 'components';
import withProfitLossActions from './withProfitLossActions';
import withProfitLoss from './withProfitLoss';
import { compose } from 'utils';
function ProfitLossActionsBar({
// #withProfitLoss
profitLossSheetFilter,
@@ -33,45 +39,43 @@ function ProfitLossActionsBar({
<DashboardActionsBar>
<NavbarGroup>
<Button
className={classNames(Classes.MINIMAL, 'button--table-views')}
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'}
className={classNames(Classes.MINIMAL, 'button--gray-highlight')}
text={<T id={'recalc_report'} />}
onClick={handleRecalcReport}
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 />
<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
className={Classes.MINIMAL}
icon={<Icon icon='print-16' iconSize={16} />}
icon={<Icon icon="print-16" iconSize={16} />}
text={<T id={'print'} />}
/>
<Button

View File

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

View File

@@ -1,126 +1,102 @@
import React, { useCallback, useEffect } from 'react';
import { Row, Col, Visible } from 'react-grid-system';
import React, { useEffect } from 'react';
import moment from 'moment';
import { useFormik } from 'formik';
import { Formik, Form } from 'formik';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { FormGroup } from '@blueprintjs/core';
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 SelectsListColumnsBy from '../SelectDisplayColumnsBy';
import RadiosAccountingBasis from '../RadiosAccountingBasis';
import FinancialAccountsFilter from '../FinancialAccountsFilter';
import ProfitLossSheetHeaderGeneralPane from './ProfitLossSheetHeaderGeneralPane';
import withProfitLoss from './withProfitLoss';
import withProfitLossActions from './withProfitLossActions';
import { compose } from 'utils';
function ProfitLossHeader({
// #ownProps
pageFilter,
onSubmitFilter,
// #withProfitLoss
profitLossSheetFilter,
profitLossSheetRefresh,
// #withProfitLossActions
refreshProfitLossSheet,
toggleProfitLossSheetFilter,
}) {
const { formatMessage } = useIntl();
const formik = useFormik({
enableReinitialize: true,
initialValues: {
...pageFilter,
from_date: moment(pageFilter.from_date).toDate(),
to_date: moment(pageFilter.to_date).toDate(),
},
validationSchema: Yup.object().shape({
from_date: Yup.date()
.required()
.label(formatMessage({ id: 'from_date' })),
to_date: Yup.date()
.min(Yup.ref('from_date'))
.required()
.label(formatMessage({ id: 'to_date' })),
}),
onSubmit: (values, actions) => {
onSubmitFilter(values);
actions.setSubmitting(false);
},
// Validation schema.
const validationSchema = Yup.object().shape({
fromDate: Yup.date()
.required()
.label(formatMessage({ id: 'from_date' })),
toDate: Yup.date()
.min(Yup.ref('fromDate'))
.required()
.label(formatMessage({ id: 'to_date' })),
accountsFilter: Yup.string(),
displayColumnsType: Yup.string(),
});
// Handle item select of `display columns by` field.
const handleItemSelectDisplayColumns = useCallback(
(item) => {
formik.setFieldValue('display_columns_type', item.type);
formik.setFieldValue('display_columns_by', item.by);
},
[formik],
);
// Initial values.
const initialValues = {
...pageFilter,
fromDate: moment(pageFilter.fromDate).toDate(),
toDate: moment(pageFilter.toDate).toDate(),
};
const handleAccountingBasisChange = useCallback(
(value) => {
formik.setFieldValue('basis', value);
},
[formik],
);
// Handle form submit.
const handleSubmit = (values, actions) => {
onSubmitFilter(values);
toggleProfitLossSheetFilter();
};
useEffect(() => {
if (profitLossSheetRefresh) {
formik.submitForm();
refreshProfitLossSheet(false);
}
}, [profitLossSheetRefresh]);
const handleAccountsFilterSelect = (filterType) => {
const noneZero = filterType.key === 'without-zero-balance' ? true : false;
formik.setFieldValue('none_zero', noneZero);
// Handles the cancel button click.
const handleCancelClick = () => {
toggleProfitLossSheetFilter();
};
// Handles the drawer close action.
const handleDrawerClose = () => {
toggleProfitLossSheetFilter();
};
return (
<FinancialStatementHeader show={profitLossSheetFilter}>
<Row>
<FinancialStatementDateRange formik={formik} />
<Visible xl><Col width={'100%'} /></Visible>
<Col width={260}>
<SelectsListColumnsBy onItemSelect={handleItemSelectDisplayColumns} />
</Col>
<Col width={260}>
<FormGroup
label={<T id={'filter_accounts'} />}
className="form-group--select-list bp3-fill"
inline={false}
>
<FinancialAccountsFilter
initialSelectedItem={'all-accounts'}
onItemSelect={handleAccountsFilterSelect}
<FinancialStatementHeader
isOpen={profitLossSheetFilter}
drawerProps={{ onClose: handleDrawerClose }}
>
<Formik
validationSchema={validationSchema}
initialValues={initialValues}
onSubmit={handleSubmit}
>
<Form>
<Tabs animate={true} vertical={true} renderActiveTabPanelOnly={true}>
<Tab
id="general"
title={<T id={'general'} />}
panel={<ProfitLossSheetHeaderGeneralPane />}
/>
</FormGroup>
</Col>
</Tabs>
<Col width={260}>
<RadiosAccountingBasis
selectedValue={formik.values.basis}
onChange={handleAccountingBasisChange}
/>
</Col>
</Row>
<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>
);
}
export default compose(
withProfitLoss(({
withProfitLoss(({ profitLossSheetFilter }) => ({
profitLossSheetFilter,
profitLossSheetRefresh,
}) => ({
profitLossSheetFilter,
profitLossSheetRefresh,
})),
withProfitLossActions,
)(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 { connect } from 'react-redux';
import { FormattedMessage as T, useIntl } from 'react-intl';
import FinancialSheet from 'components/FinancialSheet';
import DataTable from 'components/DataTable';
import Money from 'components/Money';
import { compose, defaultExpanderReducer } from 'utils';
import { getFinancialSheetIndexByQuery } from 'store/financialStatement/financialStatements.selectors';
import { compose, defaultExpanderReducer, getColumnWidth } from 'utils';
import withProfitLossDetail from './withProfitLoss';
function ProfitLossSheetTable({
@@ -18,7 +16,6 @@ function ProfitLossSheetTable({
profitLossSheetLoading,
// #ownProps
onFetchData,
companyName,
}) {
const { formatMessage } = useIntl();
@@ -26,14 +23,10 @@ function ProfitLossSheetTable({
const columns = useMemo(
() => [
{
Header: formatMessage({ id: 'account_name' }),
accessor: 'name',
Header: formatMessage({ id: 'account' }),
accessor: (row) => (row.code ? `${row.name} - ${row.code}` : row.name),
className: 'name',
},
{
Header: formatMessage({ id: 'account_code' }),
accessor: 'code',
className: 'account_code',
width: 240,
},
...(profitLossQuery.display_columns_type === 'total'
? [
@@ -45,13 +38,14 @@ function ProfitLossSheetTable({
return (
<Money
amount={row.total.formatted_amount}
currency={'USD'}
currency={row.total.currency_code}
/>
);
}
return '';
},
className: 'total',
width: 140,
},
]
: []),
@@ -60,40 +54,44 @@ function ProfitLossSheetTable({
id: `date_period_${index}`,
Header: column,
accessor: (row) => {
if (row.periods && row.periods[index]) {
const amount = row.periods[index].formatted_amount;
if (row.total_periods && row.total_periods[index]) {
const amount = row.total_periods[index].formatted_amount;
return <Money amount={amount} currency={'USD'} />;
}
return '';
},
width: 100,
width: getColumnWidth(
profitLossTableRows,
`total_periods.${index}.formatted_amount`,
{ minWidth: 100 },
),
className: 'total-period',
}))
: []),
],
[profitLossQuery.display_columns_type, profitLossColumns, formatMessage],
);
// Handle data table fetch data.
const handleFetchData = useCallback(
(...args) => {
onFetchData && onFetchData(...args);
},
[onFetchData],
[profitLossQuery.display_columns_type, profitLossTableRows, profitLossColumns, formatMessage],
);
// Retrieve default expanded rows of balance sheet.
const expandedRows = useMemo(
() => defaultExpanderReducer(profitLossTableRows, 1),
() => defaultExpanderReducer(profitLossTableRows, 3),
[profitLossTableRows],
);
// Retrieve conditional datatable row classnames.
const rowClassNames = useCallback(
(row) => ({
[`row--${row.rowType}`]: row.rowType,
}),
[],
);
const rowClassNames = useCallback((row) => {
const { original } = row;
const rowTypes = Array.isArray(original.rowTypes)
? original.rowTypes
: [];
return {
...rowTypes.reduce((acc, rowType) => {
acc[`row_type--${rowType}`] = rowType;
return acc;
}, {}),
};
}, []);
return (
<FinancialSheet
@@ -109,7 +107,6 @@ function ProfitLossSheetTable({
className="bigcapital-datatable--financial-report"
columns={columns}
data={profitLossTableRows}
onFetchData={handleFetchData}
noInitialFetch={true}
expanded={expandedRows}
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(
withProfitLossTable,
withProfitLossDetail(
({ profitLossQuery, profitLossColumns, profitLossTableRows, profitLossSheetLoading }) => ({
({
profitLossQuery,
profitLossColumns,
profitLossTableRows,
profitLossSheetLoading,
}) => ({
profitLossColumns,
profitLossQuery,
profitLossTableRows,

View File

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

View File

@@ -1,28 +1,34 @@
import React from 'react';
import {handleStringChange} from 'utils';
import {useIntl} from 'react-intl';
import {
RadioGroup,
Radio,
} from "@blueprintjs/core";
import { FastField } from 'formik';
import { handleStringChange } from 'utils';
import { useIntl } from 'react-intl';
import { RadioGroup, Radio } from '@blueprintjs/core';
export default function RadiosAccountingBasis(props) {
const { onChange, ...rest } = props;
const {formatMessage} = useIntl();
const { key = 'basis', ...rest } = props;
const { formatMessage } = useIntl();
return (
<RadioGroup
inline={true}
label={formatMessage({'id': 'accounting_basis'})}
name="basis"
onChange={handleStringChange((value) => {
onChange && onChange(value);
})}
className={'radio-group---accounting-basis'}
{...rest}>
<Radio label={formatMessage({id:'cash'})} value="cash" />
<Radio label={formatMessage({id:'accrual'})} value="accural" />
</RadioGroup>
<FastField name={'basis'}>
{({
form: { setFieldValue },
field: { value },
}) => (
<RadioGroup
inline={true}
label={formatMessage({ id: 'accounting_basis' })}
name="basis"
onChange={handleStringChange((value) => {
setFieldValue(key, value);
})}
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, { useMemo, useState, useCallback } from 'react';
import SelectList from 'components/SelectList';
import {
FormGroup,
MenuItem,
} from '@blueprintjs/core';
import React from 'react';
import { FormGroup } from '@blueprintjs/core';
import { FastField } from 'formik';
import { FormattedMessage as T } from 'react-intl';
import classNames from 'classnames';
import { MODIFIER } from 'components';
import { Row, Col, ListSelect } from 'components';
import { displayColumnsByOptions } from 'containers/FinancialStatements/common';
/**
* Financial statement - Display columns by and type select.
*/
export default function SelectsListColumnsBy(props) {
const { onItemSelect, 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]);
const { formGroupProps, selectListProps } = props;
return (
<FormGroup
label={<T id={'display_report_columns'}/>}
className="form-group-display-columns-by form-group--select-list bp3-fill"
inline={false}
{...formGroupProps}>
<SelectList
items={displayColumnsByOptions}
noResults={<MenuItem disabled={true} text="No results." />}
filterable={false}
itemRenderer={itemRenderer}
popoverProps={{ minimal: true, usePortal: false, inline: true }}
buttonLabel={buttonLabel}
onItemSelect={handleItemSelect}
className={classNames(MODIFIER.SELECT_LIST_FILL_POPOVER)}
{...selectListProps} />
</FormGroup>
<Row>
<Col xs={4}>
<FastField name={'displayColumnsType'}>
{({ form, field: { value }, meta: { error, touched } }) => (
<FormGroup
label={<T id={'display_report_columns'} />}
className="form-group-display-columns-by form-group--select-list bp3-fill"
inline={false}
{...formGroupProps}
>
<ListSelect
items={displayColumnsByOptions}
filterable={false}
selectedItem={value}
selectedItemProp={'key'}
labelProp={'name'}
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 { NavbarGroup, Button, Classes, NavbarDivider } from '@blueprintjs/core';
import Icon from 'components/Icon';
import { FormattedMessage as T } from 'react-intl';
import DashboardActionsBar from 'components/Dashboard/DashboardActionsBar';
import {
NavbarGroup,
Button,
Classes,
NavbarDivider,
Popover,
PopoverInteractionKind,
Position,
} from '@blueprintjs/core';
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 withTrialBalanceActions from './withTrialBalanceActions';
import { compose } from 'utils';
function TrialBalanceActionsBar({
// #withTrialBalance
trialBalanceSheetFilter,
@@ -22,7 +26,6 @@ function TrialBalanceActionsBar({
toggleTrialBalanceFilter,
refreshTrialBalance,
}) {
const handleFilterToggleClick = () => {
toggleTrialBalanceFilter();
};
@@ -35,45 +38,43 @@ function TrialBalanceActionsBar({
<DashboardActionsBar>
<NavbarGroup>
<Button
className={classNames(Classes.MINIMAL, 'button--table-views')}
icon={<Icon icon="cog-16" iconSize={16} />}
text={<T id={'customize_report'} />}
/>
<NavbarDivider />
<Button
className={classNames(
Classes.MINIMAL,
'button--gray-highlight',
)}
className={classNames(Classes.MINIMAL, 'button--gray-highlight')}
text={'Re-calc Report'}
onClick={handleRecalcReport}
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 />
<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
className={Classes.MINIMAL}
icon={<Icon icon='print-16' iconSize={16} />}
icon={<Icon icon="print-16" iconSize={16} />}
text={<T id={'print'} />}
/>
<Button
@@ -87,6 +88,8 @@ function TrialBalanceActionsBar({
}
export default compose(
withTrialBalance(({ trialBalanceSheetFilter }) => ({ trialBalanceSheetFilter })),
withTrialBalanceActions
)(TrialBalanceActionsBar);
withTrialBalance(({ trialBalanceSheetFilter }) => ({
trialBalanceSheetFilter,
})),
withTrialBalanceActions,
)(TrialBalanceActionsBar);

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

@@ -95,7 +95,7 @@ export const fetchProfitLossSheet = ({ query }) => {
ApiService.get('/financial_statements/profit_loss_sheet', { params: query }).then((response) => {
dispatch({
type: t.PROFIT_LOSS_SHEET_SET,
profitLoss: response.data.profitLoss,
profitLoss: response.data.data,
columns: response.data.columns,
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 t from 'store/types';
import { getFinancialSheetIndexByQuery } from './financialStatements.selectors';
import { omit } from 'lodash';
import {
mapBalanceSheetToTableRows,
journalToTableRowsMapper,
generalLedgerToTableRows,
profitLossToTableRowsMapper
} from './financialStatements.mappers';
const initialState = {
balanceSheet: {
sheets: [],
sheet: {},
loading: true,
filter: true,
refresh: false,
},
trialBalance: {
sheets: [],
sheet: {},
loading: true,
filter: true,
refresh: false,
},
generalLedger: {
sheets: [],
sheet: {},
loading: false,
filter: true,
refresh: false,
},
journal: {
sheets: [],
sheet: {},
loading: false,
tableRows: [],
filter: true,
refresh: true,
},
profitLoss: {
sheets: [],
sheet: {},
loading: true,
tableRows: [],
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 rows = [];
@@ -120,70 +81,6 @@ const mapContactAgingSummary = (sheet) => {
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) => {
return {
[`${financialName}_FILTER_TOGGLE`]: (state, action) => {
@@ -194,22 +91,13 @@ const financialStatementFilterToggle = (financialName, statePath) => {
export default createReducer(initialState, {
[t.BALANCE_SHEET_STATEMENT_SET]: (state, action) => {
const index = getFinancialSheetIndexByQuery(
state.balanceSheet.sheets,
action.query,
);
const balanceSheet = {
sheet: action.data.balanceSheet,
columns: Object.values(action.data.columns),
sheet: action.data.data,
columns: action.data.columns,
query: action.data.query,
tableRows: mapTotalToChildrenRows(action.data.balance_sheet),
tableRows: mapBalanceSheetToTableRows(action.data.data),
};
if (index !== -1) {
state.balanceSheet.sheets[index] = balanceSheet;
} else {
state.balanceSheet.sheets.push(balanceSheet);
}
state.balanceSheet.sheet = balanceSheet;
},
[t.BALANCE_SHEET_LOADING]: (state, action) => {
@@ -224,19 +112,11 @@ export default createReducer(initialState, {
...financialStatementFilterToggle('BALANCE_SHEET', 'balanceSheet'),
[t.TRAIL_BALANCE_STATEMENT_SET]: (state, action) => {
const index = getFinancialSheetIndexByQuery(
state.trialBalance.sheets,
action.query,
);
const trailBalanceSheet = {
accounts: action.data.accounts,
data: action.data.data,
query: action.data.query,
};
if (index !== -1) {
state.trialBalance.sheets[index] = trailBalanceSheet;
} else {
state.trialBalance.sheets.push(trailBalanceSheet);
}
state.trialBalance.sheet = trailBalanceSheet;
},
[t.TRIAL_BALANCE_SHEET_LOADING]: (state, action) => {
@@ -251,21 +131,12 @@ export default createReducer(initialState, {
...financialStatementFilterToggle('TRIAL_BALANCE', 'trialBalance'),
[t.JOURNAL_SHEET_SET]: (state, action) => {
const index = getFinancialSheetIndexByQuery(
state.journal.sheets,
action.query,
);
const journal = {
query: action.data.query,
journal: action.data.journal,
tableRows: mapJournalTableRows(action.data.journal),
data: action.data.data,
tableRows: journalToTableRowsMapper(action.data.data),
};
if (index !== -1) {
state.journal.sheets[index] = journal;
} else {
state.journal.sheets.push(journal);
}
state.journal.sheet = journal;
},
[t.JOURNAL_SHEET_LOADING]: (state, action) => {
@@ -278,21 +149,12 @@ export default createReducer(initialState, {
...financialStatementFilterToggle('JOURNAL', 'journal'),
[t.GENERAL_LEDGER_STATEMENT_SET]: (state, action) => {
const index = getFinancialSheetIndexByQuery(
state.generalLedger.sheets,
action.query,
);
const generalLedger = {
query: action.data.query,
accounts: action.data.accounts,
tableRows: mapGeneralLedgerAccountsToRows(action.data.accounts),
accounts: action.data.data,
tableRows: generalLedgerToTableRows(action.data.data),
};
if (index !== -1) {
state.generalLedger.sheets[index] = generalLedger;
} else {
state.generalLedger.sheets.push(generalLedger);
}
state.generalLedger.sheet = generalLedger;
},
[t.GENERAL_LEDGER_SHEET_LOADING]: (state, action) => {
@@ -305,22 +167,13 @@ export default createReducer(initialState, {
...financialStatementFilterToggle('GENERAL_LEDGER', 'generalLedger'),
[t.PROFIT_LOSS_SHEET_SET]: (state, action) => {
const index = getFinancialSheetIndexByQuery(
state.profitLoss.sheets,
action.query,
);
const profitLossSheet = {
query: action.query,
profitLoss: action.profitLoss,
columns: action.columns,
tableRows: mapProfitLossToTableRows(action.profitLoss),
tableRows: profitLossToTableRowsMapper(action.profitLoss),
};
if (index !== -1) {
state.profitLoss.sheets[index] = profitLossSheet;
} else {
state.profitLoss.sheets.push(profitLossSheet);
}
state.profitLoss.sheet = profitLossSheet;
},
[t.PROFIT_LOSS_SHEET_LOADING]: (state, action) => {
@@ -334,34 +187,34 @@ export default createReducer(initialState, {
...financialStatementFilterToggle('PROFIT_LOSS', 'profitLoss'),
[t.RECEIVABLE_AGING_SUMMARY_LOADING]: (state, action) => {
const { loading } = action.payload;
state.receivableAgingSummary.loading = loading;
},
// [t.RECEIVABLE_AGING_SUMMARY_LOADING]: (state, action) => {
// const { loading } = action.payload;
// state.receivableAgingSummary.loading = loading;
// },
[t.RECEIVABLE_AGING_SUMMARY_SET]: (state, action) => {
const { aging, columns, query } = action.payload;
const index = getFinancialSheetIndexByQuery(
state.receivableAgingSummary.sheets,
query,
);
// [t.RECEIVABLE_AGING_SUMMARY_SET]: (state, action) => {
// const { aging, columns, query } = action.payload;
// const index = getFinancialSheetIndexByQuery(
// state.receivableAgingSummary.sheets,
// query,
// );
const receivableSheet = {
query,
columns,
aging,
tableRows: mapContactAgingSummary(aging),
};
if (index !== -1) {
state.receivableAgingSummary[index] = receivableSheet;
} else {
state.receivableAgingSummary.sheets.push(receivableSheet);
}
},
[t.RECEIVABLE_AGING_SUMMARY_REFRESH]: (state, action) => {
const { refresh } = action.payload;
state.receivableAgingSummary.refresh = !!refresh;
},
// const receivableSheet = {
// query,
// columns,
// aging,
// tableRows: mapContactAgingSummary(aging),
// };
// if (index !== -1) {
// state.receivableAgingSummary[index] = receivableSheet;
// } else {
// state.receivableAgingSummary.sheets.push(receivableSheet);
// }
// },
// [t.RECEIVABLE_AGING_SUMMARY_REFRESH]: (state, action) => {
// const { refresh } = action.payload;
// state.receivableAgingSummary.refresh = !!refresh;
// },
...financialStatementFilterToggle(
'RECEIVABLE_AGING_SUMMARY',
'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.
/**
* Retrieve financial statement sheet by the given query.
* @param {array} sheets
* @param {object} query
*/
export const getFinancialSheetIndexByQuery = (sheets, query) => {
return sheets.findIndex(balanceSheet => (
getObjectDiff(query, balanceSheet.query).length === 0
));
export const sheetByTypeSelector = (sheetType) => (state, props) => {
const sheetName = transformSheetType(sheetType);
return state.financialStatements[sheetName].sheet;
};
/**
@@ -19,38 +16,56 @@ export const getFinancialSheetIndexByQuery = (sheets, query) => {
* @param {array} sheets
* @param {number} index
*/
export const getFinancialSheet = (sheets, index) => {
return (typeof sheets[index] !== 'undefined') ? sheets[index] : null;
};
export const getFinancialSheetFactory = (sheetType) =>
createSelector(
sheetByTypeSelector(sheetType),
(sheet) => {
return sheet;
},
);
/**
* Retrieve financial statement columns by the given sheet index.
* @param {array} sheets
* @param {number} index
*/
export const getFinancialSheetColumns = (sheets, index) => {
const sheet = getFinancialSheet(sheets, index);
return (sheet && sheet.columns) ? sheet.columns : [];
};
export const getFinancialSheetColumnsFactory = (sheetType) =>
createSelector(
sheetByTypeSelector(sheetType),
(sheet) => {
return (sheet && sheet.columns) ? sheet.columns : [];
},
);
/**
* Retrieve financial statement query by the given sheet index.
* @param {array} sheets
* @param {number} index
*/
export const getFinancialSheetQuery = (sheets, index) => {
const sheet = getFinancialSheet(sheets, index);
return (sheet && sheet.query) ? sheet.query : {};
};
export const getFinancialSheetQueryFactory = (sheetType) =>
createSelector(
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);
return (sheet && sheet.accounts) ? sheet.accounts : [];
};
export const getFinancialSheetTableRows = (sheets, index) => {
const sheet = getFinancialSheet(sheets, index);
return (sheet && sheet.tableRows) ? sheet.tableRows : [];
};
/**
* Retrieve financial statement table rows by the given sheet index.
*/
export const getFinancialSheetTableRowsFactory = (sheetType) =>
createSelector(
sheetByTypeSelector(sheetType),
(sheet) => {
return (sheet && sheet.tableRows) ? sheet.tableRows : [];
}
);

View File

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

View File

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

View File

@@ -6,32 +6,7 @@
.financial-statement{
&__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{
@@ -41,25 +16,146 @@
justify-content: center;
align-items: center;
}
}
&__header.is-hidden + .financial-statement__body{
.financial-sheet{
margin-top: 40px;
.financial-header-drawer{
padding: 25px 26px 25px;
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{
border: 2px solid #F1F1F1;
border: 2px solid #EBEBEB;
border-radius: 10px;
min-width: 640px;
width: auto;
padding: 30px 18px;
max-width: 100%;
margin: 15px auto 35px;
margin: 35px auto;
min-height: 400px;
display: flex;
flex-direction: column;
background: #fff;
&__title{
margin: 0;
@@ -103,7 +199,6 @@
}
}
}
&__inner{
&.is-loading{
display: none;
@@ -113,8 +208,8 @@
color: #888;
text-align: center;
margin-top: auto;
padding-top: 16px;
font-size: 12px;
padding-top: 18px;
font-size: 13px;
}
.dashboard__loading-indicator{
margin: auto;
@@ -137,13 +232,19 @@
}
&--general-ledger{
.financial-sheet__table{
.tbody{
.tbody{
.tr.row-type{
&--opening_balance,
&--closing_balance{
.td{
background-color: #fbfbfb;
border-top: 1px solid #333;
}
.name,
.amount,
.balance{
font-weight: 500;
}
}
@@ -185,18 +286,36 @@
&--profit-loss-sheet{
.financial-sheet__table{
.thead,
.tbody{
.total.td {
border-bottom-color: #000;
.tr .td:not(:first-child),
.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,
.row--expense_total,
.row--net_income{
font-weight: 600;
.total.td{
border-bottom-color: #555;
.tr.is-expanded{
.td.total,
.td.total-period{
> span{
display: none;
}
}
}
}
@@ -205,13 +324,28 @@
&--balance-sheet{
.financial-sheet__table{
.thead,
.tbody{
.total.td{
border-bottom-color: #000;
.tr .td.account_name ~ .td,
.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{
.total.td,
.account_name.td{
.account_name.td,
.total-period.td{
font-weight: 600;
color: #333;
}
@@ -267,6 +401,7 @@
&.is-full-width{
width: 100%;
margin-top: 25px;
}
}
@@ -309,4 +444,14 @@
margin-bottom: 0;
}
}
}
.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];
return {
from_date: moment().startOf(query.range).toDate(),
to_date: moment().endOf(query.range).toDate(),
fromDate: moment().startOf(query.range).toDate(),
toDate: moment().endOf(query.range).toDate(),
};
};
export const defaultExpanderReducer = (tableRows, level) => {
let currentLevel = 1;
const expended = [];
const walker = (rows, parentIndex = null) => {
const walker = (rows, parentIndex = null, currentLevel = 1) => {
return rows.forEach((row, index) => {
const _index = parentIndex ? `${parentIndex}.${index}` : `${index}`;
expended[_index] = true;
if (row.children && currentLevel < level) {
walker(row.children, _index);
walker(row.children, _index, currentLevel + 1);
}
currentLevel++;
}, {});
};
walker(tableRows);
@@ -371,4 +369,22 @@ export function defaultToTransform(
export function isBlank(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' });
try {
const data = await this.journalService.journalSheet(tenantId, filter);
const { data, query } = await this.journalService.journalSheet(tenantId, filter);
return res.status(200).send({
organization_name: organizationName,

View File

@@ -1,6 +1,6 @@
import { Inject, Service } from 'typedi';
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 ItemsService from 'services/Items/ItemsService';
import BaseController from 'api/controllers/BaseController';
@@ -23,70 +23,62 @@ export default class ItemsController extends BaseController {
router() {
const router = Router();
router.post('/', [
...this.validateItemSchema,
...this.validateNewItemSchema,
],
router.post(
'/',
[...this.validateItemSchema, ...this.validateNewItemSchema],
this.validationResult,
asyncMiddleware(this.newItem.bind(this)),
this.handlerServiceErrors,
this.handlerServiceErrors
);
router.post(
'/:id/activate', [
...this.validateSpecificItemSchema,
],
'/:id/activate',
[...this.validateSpecificItemSchema],
this.validationResult,
asyncMiddleware(this.activateItem.bind(this)),
this.handlerServiceErrors
);
router.post(
'/:id/inactivate', [
...this.validateSpecificItemSchema,
],
'/:id/inactivate',
[...this.validateSpecificItemSchema],
this.validationResult,
asyncMiddleware(this.inactivateItem.bind(this)),
this.handlerServiceErrors,
)
this.handlerServiceErrors
);
router.post(
'/:id', [
...this.validateItemSchema,
...this.validateSpecificItemSchema,
],
'/:id',
[...this.validateItemSchema, ...this.validateSpecificItemSchema],
this.validationResult,
asyncMiddleware(this.editItem.bind(this)),
this.handlerServiceErrors,
this.handlerServiceErrors
);
router.delete('/', [
...this.validateBulkSelectSchema,
],
router.delete(
'/',
[...this.validateBulkSelectSchema],
this.validationResult,
asyncMiddleware(this.bulkDeleteItems.bind(this)),
this.handlerServiceErrors
);
router.delete(
'/:id', [
...this.validateSpecificItemSchema,
],
'/:id',
[...this.validateSpecificItemSchema],
this.validationResult,
asyncMiddleware(this.deleteItem.bind(this)),
this.handlerServiceErrors,
this.handlerServiceErrors
);
router.get(
'/:id', [
...this.validateSpecificItemSchema,
],
'/:id',
[...this.validateSpecificItemSchema],
this.validationResult,
asyncMiddleware(this.getItem.bind(this)),
this.handlerServiceErrors,
this.handlerServiceErrors
);
router.get(
'/', [
...this.validateListQuerySchema,
],
'/',
[...this.validateListQuerySchema],
this.validationResult,
asyncMiddleware(this.getItemsList.bind(this)),
this.dynamicListService.handlerErrorsToResponse,
this.handlerServiceErrors,
this.handlerServiceErrors
);
return router;
}
@@ -97,8 +89,21 @@ export default class ItemsController extends BaseController {
get validateNewItemSchema(): ValidationChain[] {
return [
check('opening_quantity').default(0).isInt({ min: 0 }).toInt(),
check('opening_cost').optional({ nullable: true }).isFloat({ min: 0 }).toFloat(),
check('opening_date').optional({ nullable: true }).isISO8601(),
check('opening_cost')
.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[] {
return [
check('name').exists().isString().isLength({ max: DATATYPES_LENGTH.STRING }),
check('type').exists()
check('name')
.exists()
.isString()
.isLength({ max: DATATYPES_LENGTH.STRING }),
check('type')
.exists()
.isString()
.trim()
.escape()
@@ -127,12 +136,11 @@ export default class ItemsController extends BaseController {
.toFloat()
.if(check('purchasable').equals('true'))
.exists(),
check('cost_account_id').if(check('purchasable').equals('true')).exists(),
check('cost_account_id')
.optional({ nullable: true })
.isInt({ min: 0, max: DATATYPES_LENGTH.INT_10 })
.toInt()
.if(check('purchasable').equals('true'))
.exists(),
.toInt(),
// Sell attributes.
check('sellable').optional().isBoolean().toBoolean(),
check('sell_price')
@@ -141,18 +149,18 @@ export default class ItemsController extends BaseController {
.toFloat()
.if(check('sellable').equals('true'))
.exists(),
check('sell_account_id').if(check('sellable').equals('true')).exists(),
check('sell_account_id')
.optional({ nullable: true })
.isInt({ min: 0, max: DATATYPES_LENGTH.INT_10 })
.toInt()
.if(check('sellable').equals('true'))
.toInt(),
check('inventory_account_id')
.if(check('type').equals('inventory'))
.exists(),
check('inventory_account_id')
.optional({ nullable: true })
.isInt({ min: 0, max: DATATYPES_LENGTH.INT_10 })
.toInt()
.if(check('type').equals('inventory'))
.exists(),
.toInt(),
check('sell_description')
.optional({ nullable: true })
.isString()
@@ -187,9 +195,7 @@ export default class ItemsController extends BaseController {
* @return {ValidationChain[]}
*/
get validateSpecificItemSchema(): ValidationChain[] {
return [
param('id').exists().isNumeric().toInt(),
];
return [param('id').exists().isNumeric().toInt()];
}
/**
@@ -216,13 +222,13 @@ export default class ItemsController extends BaseController {
query('custom_view_id').optional().isNumeric().toInt(),
query('stringified_filter_roles').optional().isJSON(),
]
];
}
/**
* Stores the given item details to the storage.
* @param {Request} req
* @param {Response} res
* @param {Request} req
* @param {Response} res
*/
async newItem(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
@@ -242,14 +248,14 @@ export default class ItemsController extends BaseController {
/**
* Updates the given item details on the storage.
* @param {Request} req
* @param {Response} res
* @param {Request} req
* @param {Response} res
*/
async editItem(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const itemId: number = req.params.id;
const item: IItemDTO = this.matchedBodyData(req);
try {
await this.itemsService.editItem(tenantId, itemId, item);
return res.status(200).send({ id: itemId });
@@ -260,9 +266,9 @@ export default class ItemsController extends BaseController {
/**
* Activates the given item.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
async activateItem(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
@@ -282,9 +288,9 @@ export default class ItemsController extends BaseController {
/**
* Inactivates the given item.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
async inactivateItem(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
@@ -304,8 +310,8 @@ export default class ItemsController extends BaseController {
/**
* Deletes the given item from the storage.
* @param {Request} req
* @param {Response} res
* @param {Request} req
* @param {Response} res
*/
async deleteItem(req: Request, res: Response, next: NextFunction) {
const itemId: number = req.params.id;
@@ -320,10 +326,10 @@ export default class ItemsController extends BaseController {
}
/**
* Retrieve details the given item id.
* @param {Request} req
* @param {Response} res
* @return {Response}
* Retrieve details the given item id.
* @param {Request} req
* @param {Response} res
* @return {Response}
*/
async getItem(req: Request, res: Response, next: NextFunction) {
const itemId: number = req.params.id;
@@ -336,14 +342,14 @@ export default class ItemsController extends BaseController {
} catch (error) {
console.log(error);
next(error)
next(error);
}
}
/**
* Retrieve items datatable list.
* @param {Request} req
* @param {Response} res
* @param {Request} req
* @param {Response} res
*/
async getItemsList(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
@@ -362,7 +368,7 @@ export default class ItemsController extends BaseController {
const {
items,
pagination,
filterMeta
filterMeta,
} = await this.itemsService.itemsList(tenantId, filter);
return res.status(200).send({
@@ -377,14 +383,14 @@ export default class ItemsController extends BaseController {
/**
* Deletes items in bulk.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
async bulkDeleteItems(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const { ids: itemsIds } = req.query;
try {
await this.itemsService.bulkDeleteItems(tenantId, itemsIds);
@@ -399,12 +405,17 @@ export default class ItemsController extends BaseController {
/**
* Handles service errors.
* @param {Error} error
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @param {Error} error
* @param {Request} req
* @param {Response} res
* @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.errorType === 'NOT_FOUND') {
return res.status(400).send({
@@ -479,9 +490,9 @@ export default class ItemsController extends BaseController {
if (error.errorType === 'ITEM_HAS_ASSOCIATED_TRANSACTINS') {
return res.status(400).send({
errors: [{ type: 'ITEM_HAS_ASSOCIATED_TRANSACTINS', code: 320 }],
})
});
}
}
next(error);
}
}
}

View File

@@ -7,7 +7,6 @@ import asyncMiddleware from 'api/middleware/asyncMiddleware';
import PaymentReceiveService from 'services/Sales/PaymentsReceives';
import DynamicListingService from 'services/DynamicListing/DynamicListService';
import { ServiceError } from 'exceptions';
import HasItemEntries from 'services/Sales/HasItemsEntries';
/**
* Payments receives controller.
@@ -20,7 +19,7 @@ export default class PaymentReceivesController extends BaseController {
@Inject()
dynamicListService: DynamicListingService;
/**
* Router constructor.
*/
@@ -32,29 +31,28 @@ export default class PaymentReceivesController extends BaseController {
this.editPaymentReceiveValidation,
this.validationResult,
asyncMiddleware(this.editPaymentReceive.bind(this)),
this.handleServiceErrors,
this.handleServiceErrors
);
router.post(
'/', [
...this.newPaymentReceiveValidation,
],
'/',
[...this.newPaymentReceiveValidation],
this.validationResult,
asyncMiddleware(this.newPaymentReceive.bind(this)),
this.handleServiceErrors,
this.handleServiceErrors
);
router.get(
'/:id/invoices',
this.paymentReceiveValidation,
this.validationResult,
asyncMiddleware(this.getPaymentReceiveInvoices.bind(this)),
this.handleServiceErrors,
this.handleServiceErrors
);
router.get(
'/:id',
this.paymentReceiveValidation,
this.validationResult,
asyncMiddleware(this.getPaymentReceive.bind(this)),
this.handleServiceErrors,
this.handleServiceErrors
);
router.get(
'/',
@@ -62,14 +60,14 @@ export default class PaymentReceivesController extends BaseController {
this.validationResult,
asyncMiddleware(this.getPaymentReceiveList.bind(this)),
this.handleServiceErrors,
this.dynamicListService.handlerErrorsToResponse,
this.dynamicListService.handlerErrorsToResponse
);
router.delete(
'/:id',
this.paymentReceiveValidation,
this.validationResult,
asyncMiddleware(this.deletePaymentReceive.bind(this)),
this.handleServiceErrors,
this.handleServiceErrors
);
return router;
}
@@ -105,7 +103,7 @@ export default class PaymentReceivesController extends BaseController {
query('sort_order').optional().isIn(['desc', 'asc']),
query('page').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);
try {
const storedPaymentReceive = await this.paymentReceiveService
.createPaymentReceive(
tenantId,
paymentReceive,
);
const storedPaymentReceive = await this.paymentReceiveService.createPaymentReceive(
tenantId,
paymentReceive
);
return res.status(200).send({
id: storedPaymentReceive.id,
message: 'The payment receive has been created successfully.',
@@ -172,12 +169,14 @@ export default class PaymentReceivesController extends BaseController {
try {
await this.paymentReceiveService.editPaymentReceive(
tenantId, paymentReceiveId, paymentReceive,
tenantId,
paymentReceiveId,
paymentReceive
);
return res.status(200).send({
id: paymentReceiveId,
message: 'The payment receive has been edited successfully.'
});
message: 'The payment receive has been edited successfully.',
});
} catch (error) {
next(error);
}
@@ -193,7 +192,10 @@ export default class PaymentReceivesController extends BaseController {
const { id: paymentReceiveId } = req.params;
try {
await this.paymentReceiveService.deletePaymentReceive(tenantId, paymentReceiveId);
await this.paymentReceiveService.deletePaymentReceive(
tenantId,
paymentReceiveId
);
return res.status(200).send({
id: paymentReceiveId,
@@ -220,13 +222,14 @@ export default class PaymentReceivesController extends BaseController {
receivableInvoices,
paymentReceiveInvoices,
} = await this.paymentReceiveService.getPaymentReceive(
tenantId, paymentReceiveId
tenantId,
paymentReceiveId
);
return res.status(200).send({
payment_receive: this.transfromToResponse({ ...paymentReceive }),
receivable_invoices: this.transfromToResponse([ ...receivableInvoices ]),
payment_invoices: this.transfromToResponse([ ...paymentReceiveInvoices ]),
receivable_invoices: this.transfromToResponse([...receivableInvoices]),
payment_invoices: this.transfromToResponse([...paymentReceiveInvoices]),
});
} catch (error) {
next(error);
@@ -235,17 +238,22 @@ export default class PaymentReceivesController extends BaseController {
/**
* Retrieve sale invoices that associated with the given payment receive.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
async getPaymentReceiveInvoices(req: Request, res: Response, next: NextFunction) {
async getPaymentReceiveInvoices(
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId } = req;
const { id: paymentReceiveId } = req.params;
try {
const invoices = await this.paymentReceiveService.getPaymentReceiveInvoices(
tenantId, paymentReceiveId,
tenantId,
paymentReceiveId
);
return res.status(200).send({ sale_invoices: invoices });
} catch (error) {
@@ -255,9 +263,9 @@ export default class PaymentReceivesController extends BaseController {
/**
* Retrieve payment receive list with pagination metadata.
* @param {Request} req
* @param {Response} res
* @return {Response}
* @param {Request} req
* @param {Response} res
* @return {Response}
*/
async getPaymentReceiveList(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
@@ -278,7 +286,10 @@ export default class PaymentReceivesController extends BaseController {
paymentReceives,
pagination,
filterMeta,
} = await this.paymentReceiveService.listPaymentReceives(tenantId, filter);
} = await this.paymentReceiveService.listPaymentReceives(
tenantId,
filter
);
return res.status(200).send({
payment_receives: paymentReceives,
@@ -292,12 +303,17 @@ export default class PaymentReceivesController extends BaseController {
/**
* Handles service errors.
* @param error
* @param req
* @param res
* @param next
* @param error
* @param req
* @param res
* @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.errorType === 'DEPOSIT_ACCOUNT_NOT_FOUND') {
return res.boom.badRequest(null, {
@@ -316,7 +332,9 @@ export default class PaymentReceivesController extends BaseController {
}
if (error.errorType === 'DEPOSIT_ACCOUNT_NOT_CURRENT_ASSET_TYPE') {
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') {
@@ -346,14 +364,17 @@ export default class PaymentReceivesController extends BaseController {
}
if (error.errorType === 'INVOICES_NOT_DELIVERED_YET') {
return res.boom.badRequest(null, {
errors: [{
type: 'INVOICES_NOT_DELIVERED_YET', code: 200,
data: {
not_delivered_invoices_ids: error.payload.notDeliveredInvoices.map(
(invoice) => invoice.id
)
}
}],
errors: [
{
type: 'INVOICES_NOT_DELIVERED_YET',
code: 200,
data: {
not_delivered_invoices_ids: error.payload.notDeliveredInvoices.map(
(invoice) => invoice.id
),
},
},
],
});
}
}

View File

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

View File

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

View File

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

View File

@@ -81,8 +81,6 @@ export default class JournalSheet extends FinancialSheet {
* @return {IJournalReport}
*/
reportData(): IJournalReport {
return {
entries: this.entriesWalker(this.journal.entries),
};
return 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 moment from 'moment';
import JournalSheet from "./JournalSheet";
import TenancyService from "services/Tenancy/TenancyService";
import Journal from "services/Accounting/JournalPoster";
import JournalSheet from './JournalSheet';
import TenancyService from 'services/Tenancy/TenancyService';
import Journal from 'services/Accounting/JournalPoster';
@Service()
export default class JournalSheetService {
export default class JournalSheetService {
@Inject()
tenancy: TenancyService;
@@ -33,13 +33,10 @@ export default class JournalSheetService {
/**
* Journal sheet.
* @param {number} tenantId
* @param {IJournalSheetFilterQuery} query
* @param {number} tenantId
* @param {IJournalSheetFilterQuery} query
*/
async journalSheet(
tenantId: number,
query: IJournalReportQuery,
) {
async journalSheet(tenantId: number, query: IJournalReportQuery) {
const {
accountRepository,
transactionsRepository,
@@ -49,11 +46,17 @@ export default class JournalSheetService {
...this.defaultQuery,
...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.
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.
const accountsGraph = await accountRepository.getDependencyGraph();
@@ -66,10 +69,12 @@ export default class JournalSheetService {
fromAmount: filter.fromRange,
toAmount: filter.toRange,
});
// Transform the transactions array to journal collection.
const transactionsJournal = Journal.fromTransactions(transactions, tenantId, accountsGraph);
const transactionsJournal = Journal.fromTransactions(
transactions,
tenantId,
accountsGraph
);
// Journal report instance.
const journalSheetInstance = new JournalSheet(
tenantId,
@@ -80,6 +85,9 @@ export default class JournalSheetService {
// Retrieve journal report columns.
const journalSheetData = journalSheetInstance.reportData();
return journalSheetData;
return {
data: journalSheetData,
query: filter,
};
}
}
}

View File

@@ -1,6 +1,6 @@
import { flatten, pick, sumBy } from 'lodash';
import { IProfitLossSheetQuery } from "interfaces/ProfitLossSheet";
import FinancialSheet from "../FinancialSheet";
import { IProfitLossSheetQuery } from 'interfaces/ProfitLossSheet';
import FinancialSheet from '../FinancialSheet';
import {
IAccount,
IAccountType,
@@ -34,7 +34,7 @@ export default class ProfitLossSheet extends FinancialSheet {
query: IProfitLossSheetQuery,
accounts: IAccount & { type: IAccountType }[],
journal: IJournalPoster,
baseCurrency: string,
baseCurrency: string
) {
super();
@@ -44,9 +44,8 @@ export default class ProfitLossSheet extends FinancialSheet {
this.accounts = accounts;
this.journal = journal;
this.baseCurrency = baseCurrency;
this.comparatorDateType = query.displayColumnsType === 'total'
? 'day'
: query.displayColumnsBy;
this.comparatorDateType =
query.displayColumnsType === 'total' ? 'day' : query.displayColumnsBy;
this.initDateRangeCollection();
}
@@ -56,7 +55,7 @@ export default class ProfitLossSheet extends FinancialSheet {
* @return {IAccount & { type: IAccountType }[]}
*/
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 }[]}
*/
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 }[]}}
*/
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 }[]}
*/
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(
account.id,
this.query.toDate,
this.comparatorDateType,
this.comparatorDateType
);
const formattedAmount = this.formatNumber(amount);
const currencyCode = this.baseCurrency;
@@ -123,13 +122,13 @@ export default class ProfitLossSheet extends FinancialSheet {
const amount = this.journal.getAccountBalance(
account.id,
date,
this.comparatorDateType,
this.comparatorDateType
);
const formattedAmount = this.formatNumber(amount);
const currencyCode = this.baseCurrency;
return { date, amount, formattedAmount, currencyCode };
})
});
}
/**
@@ -153,26 +152,30 @@ export default class ProfitLossSheet extends FinancialSheet {
}
/**
*
*
* @param {IAccount[]} accounts -
* @return {IProfitLossSheetAccount[]}
*/
private accountsWalker(accounts: IAccount & { type: IAccountType }[]): IProfitLossSheetAccount[] {
private accountsWalker(
accounts: IAccount & { type: IAccountType }[]
): IProfitLossSheetAccount[] {
const flattenAccounts = accounts
.map(this.accountMapper.bind(this))
// Filter accounts that have no transaction when `noneTransactions` is on.
.filter((account: IProfitLossSheetAccount) =>
!(!account.hasTransactions && this.query.noneTransactions),
.filter(
(account: IProfitLossSheetAccount) =>
!(!account.hasTransactions && this.query.noneTransactions)
)
// Filter accounts that have zero total amount when `noneZero` is on.
.filter((account: IProfitLossSheetAccount) =>
!(account.total.amount === 0 && this.query.noneZero)
.filter(
(account: IProfitLossSheetAccount) =>
!(account.total.amount === 0 && this.query.noneZero)
);
return flatToNestedArray(
flattenAccounts,
{ id: 'id', parentId: 'parentAccountId' },
);
return flatToNestedArray(flattenAccounts, {
id: 'id',
parentId: 'parentAccountId',
});
}
/**
@@ -180,7 +183,9 @@ export default class ProfitLossSheet extends FinancialSheet {
* @param {IAccount[]} accounts -
* @return {IProfitLossSheetTotal}
*/
private gatTotalSection(accounts: IProfitLossSheetAccount[]): IProfitLossSheetTotal {
private gatTotalSection(
accounts: IProfitLossSheetAccount[]
): IProfitLossSheetTotal {
const amount = sumBy(accounts, 'total.amount');
const formattedAmount = this.formatNumber(amount);
const currencyCode = this.baseCurrency;
@@ -190,10 +195,12 @@ export default class ProfitLossSheet extends FinancialSheet {
/**
* Retrieve the report total section in periods display type.
* @param {IAccount} accounts -
* @param {IAccount} accounts -
* @return {IProfitLossSheetTotal[]}
*/
private getTotalPeriodsSection(accounts: IProfitLossSheetAccount[]): IProfitLossSheetTotal[] {
private getTotalPeriodsSection(
accounts: IProfitLossSheetAccount[]
): IProfitLossSheetTotal[] {
return this.dateRangeSet.map((date, index) => {
const amount = sumBy(accounts, `totalPeriods[${index}].amount`);
const formattedAmount = this.formatNumber(amount);
@@ -213,7 +220,7 @@ export default class ProfitLossSheet extends FinancialSheet {
...(this.query.displayColumnsType === 'date_periods' && {
totalPeriods: this.getTotalPeriodsSection(accounts),
}),
}
};
}
/**
@@ -266,10 +273,13 @@ export default class ProfitLossSheet extends FinancialSheet {
private getSummarySectionDatePeriods(
positiveSections: IProfitLossSheetTotalSection[],
minesSections: IProfitLossSheetTotalSection[],
minesSections: IProfitLossSheetTotalSection[]
) {
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 amount = totalPositive - totalMines;
@@ -278,11 +288,11 @@ export default class ProfitLossSheet extends FinancialSheet {
return { date, amount, formattedAmount, currencyCode };
});
};
}
private getSummarySectionTotal(
positiveSections: IProfitLossSheetTotalSection[],
minesSections: IProfitLossSheetTotalSection[],
minesSections: IProfitLossSheetTotalSection[]
) {
const totalPositiveSections = sumBy(positiveSections, 'total.amount');
const totalMinesSections = sumBy(minesSections, 'total.amount');
@@ -291,36 +301,37 @@ export default class ProfitLossSheet extends FinancialSheet {
const formattedAmount = this.formatNumber(amount);
const currencyCode = this.baseCurrency;
return { amount, formattedAmount, currencyCode };
return { amount, formattedAmount, currencyCode };
}
/**
* Retrieve the summary section
* @param
* @param
*/
private getSummarySection(
sections: IProfitLossSheetTotalSection|IProfitLossSheetTotalSection[],
subtractSections: IProfitLossSheetTotalSection|IProfitLossSheetTotalSection[]
sections: IProfitLossSheetTotalSection | IProfitLossSheetTotalSection[],
subtractSections:
| IProfitLossSheetTotalSection
| IProfitLossSheetTotalSection[]
): IProfitLossSheetTotalSection {
const positiveSections = Array.isArray(sections) ? sections : [sections];
const minesSections = Array.isArray(subtractSections) ? subtractSections : [subtractSections];
const minesSections = Array.isArray(subtractSections)
? subtractSections
: [subtractSections];
return {
total: this.getSummarySectionTotal(positiveSections, minesSections),
...(this.query.displayColumnsType === 'date_periods' && {
totalPeriods: [
...this.getSummarySectionDatePeriods(
positiveSections,
minesSections,
),
...this.getSummarySectionDatePeriods(positiveSections, minesSections),
],
}),
}
};
}
/**
* Retrieve date range columns of the given query.
* @param {IBalanceSheetQuery} query
* @param {IBalanceSheetQuery} query
* @return {string[]}
*/
private dateRangeColumns(): string[] {
@@ -341,7 +352,10 @@ export default class ProfitLossSheet extends FinancialSheet {
const grossProfit = this.getSummarySection(income, costOfSales);
// - 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.
const netIncome = this.getSummarySection(operatingProfit, otherExpenses);
@@ -366,4 +380,4 @@ export default class ProfitLossSheet extends FinancialSheet {
? this.dateRangeColumns()
: ['total'];
}
}
}

View File

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

View File

@@ -55,6 +55,10 @@ export default class TrialBalanceSheetService {
transactionsRepository,
} = 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 });
// Retrieve all accounts on the storage.
@@ -76,6 +80,7 @@ export default class TrialBalanceSheetService {
filter,
accounts,
transactionsJournal,
baseCurrency
);
// Trial balance sheet data.
const trialBalanceSheetData = trialBalanceInstance.reportData();

View File

@@ -13,11 +13,11 @@ import {
IPaymentReceive,
IPaymentReceiveDTO,
IPaymentReceiveCreateDTO,
IPaymentReceiveEditDTO,
IPaymentReceiveEditDTO,
IPaymentReceiveEntry,
IPaymentReceiveEntryDTO,
IPaymentReceivesFilter,
ISaleInvoice
ISaleInvoice,
} from 'interfaces';
import AccountsService from 'services/Accounts/AccountsService';
import JournalPoster from 'services/Accounting/JournalPoster';
@@ -34,11 +34,12 @@ const ERRORS = {
PAYMENT_RECEIVE_NO_EXISTS: 'PAYMENT_RECEIVE_NO_EXISTS',
PAYMENT_RECEIVE_NOT_EXISTS: 'PAYMENT_RECEIVE_NOT_EXISTS',
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',
INVOICES_IDS_NOT_FOUND: 'INVOICES_IDS_NOT_FOUND',
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.
@@ -72,8 +73,8 @@ export default class PaymentReceiveService {
/**
* Validates the payment receive number existance.
* @param {number} tenantId -
* @param {string} paymentReceiveNo -
* @param {number} tenantId -
* @param {string} paymentReceiveNo -
*/
async validatePaymentReceiveNoExistance(
tenantId: number,
@@ -81,7 +82,8 @@ export default class PaymentReceiveService {
notPaymentReceiveId?: number
): Promise<void> {
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) => {
if (notPaymentReceiveId) {
builder.whereNot('id', notPaymentReceiveId);
@@ -95,8 +97,8 @@ export default class PaymentReceiveService {
/**
* Validates the payment receive existance.
* @param {number} tenantId -
* @param {number} paymentReceiveId -
* @param {number} tenantId -
* @param {number} paymentReceiveId -
*/
async getPaymentReceiveOrThrowError(
tenantId: number,
@@ -115,16 +117,26 @@ export default class PaymentReceiveService {
/**
* Validate the deposit account id existance.
* @param {number} tenantId -
* @param {number} depositAccountId -
* @param {number} tenantId -
* @param {number} depositAccountId -
*/
async getDepositAccountOrThrowError(tenantId: number, depositAccountId: number): Promise<IAccount> {
const { accountTypeRepository, accountRepository } = this.tenancy.repositories(tenantId);
async getDepositAccountOrThrowError(
tenantId: number,
depositAccountId: number
): Promise<IAccount> {
const {
accountTypeRepository,
accountRepository,
} = this.tenancy.repositories(tenantId);
const currentAssetTypes = await accountTypeRepository.getByChildType('current_asset');
const depositAccount = await accountRepository.findOneById(depositAccountId);
const currentAssetTypes = await accountTypeRepository.getByChildType(
'current_asset'
);
const depositAccount = await accountRepository.findOneById(
depositAccountId
);
const currentAssetTypesIds = currentAssetTypes.map(type => type.id);
const currentAssetTypesIds = currentAssetTypes.map((type) => type.id);
if (!depositAccount) {
throw new ServiceError(ERRORS.DEPOSIT_ACCOUNT_NOT_FOUND);
@@ -134,16 +146,22 @@ export default class PaymentReceiveService {
}
return depositAccount;
}
/**
* Validates the invoices IDs existance.
* @param {number} tenantId -
* @param {number} tenantId -
* @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 invoicesIds = paymentReceiveEntries.map((e: IPaymentReceiveEntryDTO) => e.invoiceId);
const invoicesIds = paymentReceiveEntries.map(
(e: IPaymentReceiveEntryDTO) => e.invoiceId
);
const storedInvoices = await SaleInvoice.query()
.whereIn('id', invoicesIds)
.where('customer_id', customerId);
@@ -155,10 +173,14 @@ export default class PaymentReceiveService {
throw new ServiceError(ERRORS.INVOICES_IDS_NOT_FOUND);
}
// Filters the not delivered invoices.
const notDeliveredInvoices = storedInvoices.filter((invoice) => !invoice.isDelivered);
const notDeliveredInvoices = storedInvoices.filter(
(invoice) => !invoice.isDelivered
);
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;
}
@@ -172,32 +194,38 @@ export default class PaymentReceiveService {
async validateInvoicesPaymentsAmount(
tenantId: number,
paymentReceiveEntries: IPaymentReceiveEntryDTO[],
oldPaymentEntries: IPaymentReceiveEntry[] = [],
oldPaymentEntries: IPaymentReceiveEntry[] = []
) {
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 storedInvoicesMap = new Map(
storedInvoices
.map((invoice: ISaleInvoice) => {
const oldEntries = oldPaymentEntries.filter(entry => entry.invoiceId);
const oldPaymentAmount = sumBy(oldEntries, 'paymentAmount') || 0,
storedInvoices.map((invoice: ISaleInvoice) => {
const oldEntries = oldPaymentEntries.filter((entry) => entry.invoiceId);
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[] = [];
paymentReceiveEntries.forEach((entry: IPaymentReceiveEntryDTO, index: number) => {
const entryInvoice = storedInvoicesMap.get(entry.invoiceId);
const { dueAmount } = entryInvoice;
paymentReceiveEntries.forEach(
(entry: IPaymentReceiveEntryDTO, index: number) => {
const entryInvoice = storedInvoicesMap.get(entry.invoiceId);
const { dueAmount } = entryInvoice;
if (dueAmount < entry.paymentAmount) {
hasWrongPaymentAmount.push({ index, due_amount: dueAmount });
if (dueAmount < entry.paymentAmount) {
hasWrongPaymentAmount.push({ index, due_amount: dueAmount });
}
}
});
);
if (hasWrongPaymentAmount.length > 0) {
throw new ServiceError(ERRORS.INVALID_PAYMENT_AMOUNT);
}
@@ -205,14 +233,14 @@ export default class PaymentReceiveService {
/**
* Validate the payment receive entries IDs existance.
* @param {number} tenantId
* @param {number} paymentReceiveId
* @param {IPaymentReceiveEntryDTO[]} paymentReceiveEntries
* @param {number} tenantId
* @param {number} paymentReceiveId
* @param {IPaymentReceiveEntryDTO[]} paymentReceiveEntries
*/
private async validateEntriesIdsExistance(
tenantId: number,
paymentReceiveId: number,
paymentReceiveEntries: IPaymentReceiveEntryDTO[],
paymentReceiveEntries: IPaymentReceiveEntryDTO[]
) {
const { PaymentReceiveEntry } = this.tenancy.models(tenantId);
@@ -220,17 +248,19 @@ export default class PaymentReceiveService {
.filter((entry) => entry.id)
.map((entry) => entry.id);
const storedEntries = await PaymentReceiveEntry.query()
.where('payment_receive_id', paymentReceiveId);
const storedEntries = await PaymentReceiveEntry.query().where(
'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);
if (notFoundEntriesIds.length > 0) {
throw new ServiceError(ERRORS.ENTRIES_IDS_NOT_EXISTS);
}
}
/**
* Creates a new payment receive and store it to the storage
* with associated invoices payment and journal transactions.
@@ -238,41 +268,66 @@ export default class PaymentReceiveService {
* @param {number} tenantId - Tenant id.
* @param {IPaymentReceive} paymentReceive
*/
public async createPaymentReceive(tenantId: number, paymentReceiveDTO: IPaymentReceiveCreateDTO) {
public async createPaymentReceive(
tenantId: number,
paymentReceiveDTO: IPaymentReceiveCreateDTO
) {
const { PaymentReceive } = this.tenancy.models(tenantId);
const paymentAmount = sumBy(paymentReceiveDTO.entries, 'paymentAmount');
// Validate payment receive number uniquiness.
if (paymentReceiveDTO.paymentReceiveNo) {
await this.validatePaymentReceiveNoExistance(tenantId, paymentReceiveDTO.paymentReceiveNo);
await this.validatePaymentReceiveNoExistance(
tenantId,
paymentReceiveDTO.paymentReceiveNo
);
}
// Validate customer existance.
await this.customersService.getCustomerByIdOrThrowError(tenantId, paymentReceiveDTO.customerId);
await this.customersService.getCustomerByIdOrThrowError(
tenantId,
paymentReceiveDTO.customerId
);
// 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.
await this.validateInvoicesIDsExistance(tenantId, paymentReceiveDTO.customerId, paymentReceiveDTO.entries);
await this.validateInvoicesIDsExistance(
tenantId,
paymentReceiveDTO.customerId,
paymentReceiveDTO.entries
);
// 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.');
const paymentReceive = await PaymentReceive.query()
.insertGraphAndFetch({
amount: paymentAmount,
...formatDateFields(omit(paymentReceiveDTO, ['entries']), ['paymentDate']),
const paymentReceive = await PaymentReceive.query().insertGraphAndFetch({
amount: paymentAmount,
...formatDateFields(omit(paymentReceiveDTO, ['entries']), [
'paymentDate',
]),
entries: paymentReceiveDTO.entries.map((entry) => ({
...omit(entry, ['id']),
})),
});
entries: paymentReceiveDTO.entries.map((entry) => ({
...omit(entry, ['id']),
})),
});
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;
}
@@ -288,52 +343,85 @@ export default class PaymentReceiveService {
* - Update the different customer balances.
* - Update the different invoice payment amount.
* @async
* @param {number} tenantId -
* @param {number} tenantId -
* @param {Integer} paymentReceiveId -
* @param {IPaymentReceive} paymentReceive -
*/
public async editPaymentReceive(
tenantId: number,
paymentReceiveId: number,
paymentReceiveDTO: IPaymentReceiveEditDTO,
paymentReceiveDTO: IPaymentReceiveEditDTO
) {
const { PaymentReceive } = this.tenancy.models(tenantId);
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.
const oldPaymentReceive = await this.getPaymentReceiveOrThrowError(tenantId, paymentReceiveId);
const oldPaymentReceive = await this.getPaymentReceiveOrThrowError(
tenantId,
paymentReceiveId
);
// Validate payment receive number uniquiness.
if (paymentReceiveDTO.paymentReceiveNo) {
await this.validatePaymentReceiveNoExistance(tenantId, paymentReceiveDTO.paymentReceiveNo, paymentReceiveId);
await this.validatePaymentReceiveNoExistance(
tenantId,
paymentReceiveDTO.paymentReceiveNo,
paymentReceiveId
);
}
// 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.
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.
await this.validateInvoicesIDsExistance(tenantId, oldPaymentReceive.customerId, paymentReceiveDTO.entries);
await this.validateInvoicesIDsExistance(
tenantId,
oldPaymentReceive.customerId,
paymentReceiveDTO.entries
);
// 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.
const paymentReceive = await PaymentReceive.query()
.upsertGraphAndFetch({
id: paymentReceiveId,
amount: paymentAmount,
...formatDateFields(omit(paymentReceiveDTO, ['entries']), ['paymentDate']),
entries: paymentReceiveDTO.entries,
});
const paymentReceive = await PaymentReceive.query().upsertGraphAndFetch({
id: paymentReceiveId,
amount: paymentAmount,
...formatDateFields(omit(paymentReceiveDTO, ['entries']), [
'paymentDate',
]),
entries: paymentReceiveDTO.entries,
});
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.
*/
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.
await PaymentReceiveEntry.query().where('payment_receive_id', paymentReceiveId).delete();
await PaymentReceiveEntry.query()
.where('payment_receive_id', paymentReceiveId)
.delete();
// Deletes the payment receive transaction.
await PaymentReceive.query().findById(paymentReceiveId).delete();
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,
paymentReceiveId: number
): Promise<{
paymentReceive: IPaymentReceive,
receivableInvoices: ISaleInvoice[],
paymentReceiveInvoices: ISaleInvoice[],
}> {
paymentReceive: IPaymentReceive;
receivableInvoices: ISaleInvoice[];
paymentReceiveInvoices: ISaleInvoice[];
}> {
const { PaymentReceive, SaleInvoice } = this.tenancy.models(tenantId);
const paymentReceive = await PaymentReceive.query()
.findById(paymentReceiveId)
@@ -401,8 +501,8 @@ export default class PaymentReceiveService {
// Retrieve all payment receive associated invoices.
const paymentReceiveInvoices = paymentReceive.entries.map((entry) => ({
...(entry.invoice),
dueAmount: (entry.invoice.dueAmount + entry.paymentAmount),
...entry.invoice,
dueAmount: entry.invoice.dueAmount + entry.paymentAmount,
}));
return { paymentReceive, receivableInvoices, paymentReceiveInvoices };
@@ -414,37 +514,58 @@ export default class PaymentReceiveService {
* @param {number} paymentReceiveId - Payment receive id.
* @return {Promise<ISaleInvoice>}
*/
public async getPaymentReceiveInvoices(tenantId: number, paymentReceiveId: number) {
public async getPaymentReceiveInvoices(
tenantId: number,
paymentReceiveId: number
) {
const { SaleInvoice } = this.tenancy.models(tenantId);
const paymentReceive = await this.getPaymentReceiveOrThrowError(tenantId, paymentReceiveId);
const paymentReceiveInvoicesIds = paymentReceive.entries.map(entry => entry.invoiceId);
const paymentReceive = await this.getPaymentReceiveOrThrowError(
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;
}
/**
* Retrieve payment receives paginated and filterable list.
* @param {number} tenantId
* @param {IPaymentReceivesFilter} paymentReceivesFilter
* @param {number} tenantId
* @param {IPaymentReceivesFilter} paymentReceivesFilter
*/
public async listPaymentReceives(
tenantId: number,
paymentReceivesFilter: IPaymentReceivesFilter,
): Promise<{ paymentReceives: IPaymentReceive[], pagination: IPaginationMeta, filterMeta: IFilterMeta }> {
paymentReceivesFilter: IPaymentReceivesFilter
): Promise<{
paymentReceives: IPaymentReceive[];
pagination: IPaginationMeta;
filterMeta: IFilterMeta;
}> {
const { PaymentReceive } = this.tenancy.models(tenantId);
const dynamicFilter = await this.dynamicListService.dynamicList(tenantId, PaymentReceive, paymentReceivesFilter);
const { results, pagination } = await PaymentReceive.query().onBuild((builder) => {
builder.withGraphFetched('customer');
builder.withGraphFetched('depositAccount');
dynamicFilter.buildQuery()(builder);
}).pagination(
paymentReceivesFilter.page - 1,
paymentReceivesFilter.pageSize,
const dynamicFilter = await this.dynamicListService.dynamicList(
tenantId,
PaymentReceive,
paymentReceivesFilter
);
const { results, pagination } = await PaymentReceive.query()
.onBuild((builder) => {
builder.withGraphFetched('customer');
builder.withGraphFetched('depositAccount');
dynamicFilter.buildQuery()(builder);
})
.pagination(
paymentReceivesFilter.page - 1,
paymentReceivesFilter.pageSize
);
return {
paymentReceives: results,
pagination,
@@ -456,7 +577,10 @@ export default class PaymentReceiveService {
* Retrieve the payment receive details with associated invoices.
* @param {Integer} paymentReceiveId
*/
async getPaymentReceiveWithInvoices(tenantId: number, paymentReceiveId: number) {
async getPaymentReceiveWithInvoices(
tenantId: number,
paymentReceiveId: number
) {
const { PaymentReceive } = this.tenancy.models(tenantId);
return PaymentReceive.query()
.where('id', paymentReceiveId)
@@ -466,7 +590,7 @@ export default class PaymentReceiveService {
/**
* Records payment receive journal transactions.
*
*
* Invoice payment journals.
* --------
* - Account receivable -> Debit
@@ -484,7 +608,9 @@ export default class PaymentReceiveService {
const { Account, AccountTransaction } = this.tenancy.models(tenantId);
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(
tenantId,
'accounts_receivable'
@@ -540,7 +666,7 @@ export default class PaymentReceiveService {
public async saveChangeInvoicePaymentAmount(
tenantId: number,
newPaymentReceiveEntries: IPaymentReceiveEntryDTO[],
oldPaymentReceiveEntries?: IPaymentReceiveEntryDTO[],
oldPaymentReceiveEntries?: IPaymentReceiveEntryDTO[]
): Promise<void> {
const { SaleInvoice } = this.tenancy.models(tenantId);
const opers: Promise<void>[] = [];
@@ -549,10 +675,12 @@ export default class PaymentReceiveService {
newPaymentReceiveEntries,
oldPaymentReceiveEntries,
'paymentAmount',
'invoiceId',
'invoiceId'
);
diffEntries.forEach((diffEntry: any) => {
if (diffEntry.paymentAmount === 0) { return; }
if (diffEntry.paymentAmount === 0) {
return;
}
const oper = SaleInvoice.changePaymentAmount(
diffEntry.invoiceId,
@@ -560,6 +688,6 @@ export default class PaymentReceiveService {
);
opers.push(oper);
});
await Promise.all([ ...opers ]);
await Promise.all([...opers]);
}
}