diff --git a/client/src/common/classes.js b/client/src/common/classes.js index 17edd9724..a6968d378 100644 --- a/client/src/common/classes.js +++ b/client/src/common/classes.js @@ -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', diff --git a/client/src/components/DataTable.js b/client/src/components/DataTable.js index cb88633ee..4e6d7f117 100644 --- a/client/src/components/DataTable.js +++ b/client/src/components/DataTable.js @@ -104,6 +104,7 @@ export default function DataTable({ initialState: { pageIndex: initialPageIndex, pageSize: initialPageSize, + expanded }, manualPagination, pageCount: controlledPageCount, diff --git a/client/src/components/index.js b/client/src/components/index.js index 37bbf113d..fa4a5a9b7 100644 --- a/client/src/components/index.js +++ b/client/src/components/index.js @@ -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, }; diff --git a/client/src/containers/FinancialStatements/BalanceSheet/BalanceSheet.js b/client/src/containers/FinancialStatements/BalanceSheet/BalanceSheet.js index 6f6ffe78f..5446c9d54 100644 --- a/client/src/containers/FinancialStatements/BalanceSheet/BalanceSheet.js +++ b/client/src/containers/FinancialStatements/BalanceSheet/BalanceSheet.js @@ -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 ( @@ -87,15 +96,9 @@ function BalanceSheet({ -
- +
@@ -106,8 +109,10 @@ function BalanceSheet({ export default compose( withDashboardActions, withBalanceSheetActions, - withBalanceSheetDetail(({ balanceSheetFilter }) => ({ - balanceSheetFilter, + withBalanceSheetDetail(({ balanceSheetRefresh }) => ({ + balanceSheetRefresh, + })), + withSettings(({ organizationSettings }) => ({ + organizationName: organizationSettings.name, })), - withSettings, )(BalanceSheet); diff --git a/client/src/containers/FinancialStatements/BalanceSheet/BalanceSheetActionsBar.js b/client/src/containers/FinancialStatements/BalanceSheet/BalanceSheetActionsBar.js index f75cb852a..ad79d6d36 100644 --- a/client/src/containers/FinancialStatements/BalanceSheet/BalanceSheetActionsBar.js +++ b/client/src/containers/FinancialStatements/BalanceSheet/BalanceSheetActionsBar.js @@ -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({ + + + + ); } export default compose( - withBalanceSheet(({ balanceSheetRefresh }) => ({ - refresh: balanceSheetRefresh, + withBalanceSheet(({ balanceSheetFilter }) => ({ + balanceSheetFilter, })), withBalanceSheetActions, )(BalanceSheetHeader); diff --git a/client/src/containers/FinancialStatements/BalanceSheet/BalanceSheetHeaderGeneralPanal.js b/client/src/containers/FinancialStatements/BalanceSheet/BalanceSheetHeaderGeneralPanal.js new file mode 100644 index 000000000..bd194a816 --- /dev/null +++ b/client/src/containers/FinancialStatements/BalanceSheet/BalanceSheetHeaderGeneralPanal.js @@ -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 ( +
+ + + + +
+ ); +} diff --git a/client/src/containers/FinancialStatements/BalanceSheet/BalanceSheetTable.js b/client/src/containers/FinancialStatements/BalanceSheet/BalanceSheetTable.js index 0060e214a..ad64fd54d 100644 --- a/client/src/containers/FinancialStatements/BalanceSheet/BalanceSheetTable.js +++ b/client/src/containers/FinancialStatements/BalanceSheet/BalanceSheetTable.js @@ -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 ( + + ); + } + 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 ; + } + 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 ( - - ); - } - 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 ; - } - 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 ( ); } -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); diff --git a/client/src/containers/FinancialStatements/BalanceSheet/withBalanceSheetDetail.js b/client/src/containers/FinancialStatements/BalanceSheet/withBalanceSheetDetail.js index 19a3c85ce..eefd2bd15 100644 --- a/client/src/containers/FinancialStatements/BalanceSheet/withBalanceSheetDetail.js +++ b/client/src/containers/FinancialStatements/BalanceSheet/withBalanceSheetDetail.js @@ -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); -} +}; diff --git a/client/src/containers/FinancialStatements/FinancialAccountsFilter.js b/client/src/containers/FinancialStatements/FinancialAccountsFilter.js index b2401dbfe..4f6e89585 100644 --- a/client/src/containers/FinancialStatements/FinancialAccountsFilter.js +++ b/client/src/containers/FinancialStatements/FinancialAccountsFilter.js @@ -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 ( - - - - ); - }, - [], - ); + const filterAccountRenderer = (item, { handleClick, modifiers, query }) => { + return ( + + + + ); + }; return ( - + + + + {({ form: { setFieldValue }, field: { value } }) => ( + } + className="form-group--select-list bp3-fill" + inline={false} + > + { + setFieldValue('accountsFilter', item.key); + }} + className={classNames(CLASSES.SELECT_LIST_FILL_POPOVER)} + {...restProps} + /> + + )} + + + ); -} \ No newline at end of file +} diff --git a/client/src/containers/FinancialStatements/FinancialStatementDateRange.js b/client/src/containers/FinancialStatements/FinancialStatementDateRange.js index e82749be1..c68c9ac2c 100644 --- a/client/src/containers/FinancialStatements/FinancialStatementDateRange.js +++ b/client/src/containers/FinancialStatements/FinancialStatementDateRange.js @@ -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 ( <> - - } - minimal={true} - fill={true} - > - - - + + + + {({ + form: { setFieldValue }, + field: { value }, + }) => ( + } + minimal={true} + fill={true} + > + { + const newValue = e.target.value; - - } - fill={true} - intent={formik.errors.from_date && Intent.DANGER} - > - - - + if (newValue !== 'custom') { + const dateRange = parseDateRangeQuery(newValue); - - } - fill={true} - intent={formik.errors.to_date && Intent.DANGER} - > - - - + if (dateRange) { + setFieldValue('fromDate', moment(dateRange.fromDate).toDate()); + setFieldValue('toDate', moment(dateRange.toDate).toDate()); + } + } + setFieldValue('dateRange', newValue); + }} + /> + + )} + + + + + + + + {({ + form: { setFieldValue }, + field: { value }, + meta: { error, touched }, + }) => ( + } + fill={true} + intent={error && Intent.DANGER} + helperText={} + > + { + setFieldValue('fromDate', selectedDate); + }} + popoverProps={{ minimal: true, position: Position.BOTTOM }} + canClearSelection={false} + minimal={true} + fill={true} + /> + + )} + + + + + + {({ + form: { setFieldValue }, + field: { value }, + meta: { error }, + }) => ( + } + fill={true} + intent={error && Intent.DANGER} + helperText={} + > + { + setFieldValue('toDate', selectedDate); + }} + popoverProps={{ minimal: true, position: Position.BOTTOM }} + canClearSelection={false} + fill={true} + minimal={true} + intent={error && Intent.DANGER} + /> + + )} + + + ); } diff --git a/client/src/containers/FinancialStatements/FinancialStatementHeader.js b/client/src/containers/FinancialStatements/FinancialStatementHeader.js index 8bbb15ffb..f8979892e 100644 --- a/client/src/containers/FinancialStatements/FinancialStatementHeader.js +++ b/client/src/containers/FinancialStatements/FinancialStatementHeader.js @@ -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 (
- {children} + + {children} +
); } diff --git a/client/src/containers/FinancialStatements/GeneralLedger/GeneralLedger.js b/client/src/containers/FinancialStatements/GeneralLedger/GeneralLedger.js index ac1f45d42..213fc5f45 100644 --- a/client/src/containers/FinancialStatements/GeneralLedger/GeneralLedger.js +++ b/client/src/containers/FinancialStatements/GeneralLedger/GeneralLedger.js @@ -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 ( - +
+ onSubmitFilter={handleFilterSubmit} + />
+ />
@@ -95,5 +122,10 @@ export default compose( withGeneralLedgerActions, withDashboardActions, withAccountsActions, - withSettings, -)(GeneralLedger); \ No newline at end of file + withGeneralLedger(({ generalLedgerSheetRefresh }) => ({ + generalLedgerSheetRefresh, + })), + withSettings(({ organizationSettings }) => ({ + organizationName: organizationSettings.name, + })), +)(GeneralLedger); diff --git a/client/src/containers/FinancialStatements/GeneralLedger/GeneralLedgerActionsBar.js b/client/src/containers/FinancialStatements/GeneralLedger/GeneralLedgerActionsBar.js index 7b1b18bfd..9fed4570b 100644 --- a/client/src/containers/FinancialStatements/GeneralLedger/GeneralLedgerActionsBar.js +++ b/client/src/containers/FinancialStatements/GeneralLedger/GeneralLedgerActionsBar.js @@ -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({ - + + + + ); } export default compose( - withAccounts(({ accountsList }) => ({ - accountsList, - })), - withGeneralLedger(({ generalLedgerSheetFilter, generalLedgerSheetRefresh }) => ({ + withGeneralLedger(({ generalLedgerSheetFilter }) => ({ generalLedgerSheetFilter, - generalLedgerSheetRefresh, })), withGeneralLedgerActions, )(GeneralLedgerHeader); diff --git a/client/src/containers/FinancialStatements/GeneralLedger/GeneralLedgerHeaderGeneralPane.js b/client/src/containers/FinancialStatements/GeneralLedger/GeneralLedgerHeaderGeneralPane.js new file mode 100644 index 000000000..b9d3cccc8 --- /dev/null +++ b/client/src/containers/FinancialStatements/GeneralLedger/GeneralLedgerHeaderGeneralPane.js @@ -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 ( +
+ + + + + } + className={classNames('form-group--select-list', Classes.FILL)} + > + + + + + + +
+ ); +} + +export default compose(withAccounts(({ accountsList }) => ({ accountsList })))( + GeneralLedgerHeaderGeneralPane, +); diff --git a/client/src/containers/FinancialStatements/GeneralLedger/GeneralLedgerTable.js b/client/src/containers/FinancialStatements/GeneralLedger/GeneralLedgerTable.js index 55d95800d..de0538606 100644 --- a/client/src/containers/FinancialStatements/GeneralLedger/GeneralLedgerTable.js +++ b/client/src/containers/FinancialStatements/GeneralLedger/GeneralLedgerTable.js @@ -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 ; }, []); - const referenceLink = useCallback((row) => { - return {row.referenceId}; - }); - 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 ( { - const { generalLedgerQuery } = props; - - return { - generalLedgerIndex: getFinancialSheetIndexByQuery( - state.financialStatements.generalLedger.sheets, - generalLedgerQuery, - ), - }; -}; - -const withGeneralLedgerTable = connect(mapStateToProps); - export default compose( - withGeneralLedgerTable, withGeneralLedger( ({ generalLedgerTableRows, diff --git a/client/src/containers/FinancialStatements/GeneralLedger/withGeneralLedger.js b/client/src/containers/FinancialStatements/GeneralLedger/withGeneralLedger.js index fd8ab6662..b77951902 100644 --- a/client/src/containers/FinancialStatements/GeneralLedger/withGeneralLedger.js +++ b/client/src/containers/FinancialStatements/GeneralLedger/withGeneralLedger.js @@ -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, diff --git a/client/src/containers/FinancialStatements/Journal/Journal.js b/client/src/containers/FinancialStatements/Journal/Journal.js index bdf305138..10d76c345 100644 --- a/client/src/containers/FinancialStatements/Journal/Journal.js +++ b/client/src/containers/FinancialStatements/Journal/Journal.js @@ -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 ( - +
@@ -96,5 +107,10 @@ function Journal({ export default compose( withDashboardActions, withJournalActions, - withSettings, + withSettings(({ organizationSettings }) => ({ + organizationName: organizationSettings.name, + })), + withJournal(({ journalSheetRefresh }) => ({ + journalSheetRefresh, + })), )(Journal); diff --git a/client/src/containers/FinancialStatements/Journal/JournalActionsBar.js b/client/src/containers/FinancialStatements/Journal/JournalActionsBar.js index 712b1629a..feb7b9e2a 100644 --- a/client/src/containers/FinancialStatements/Journal/JournalActionsBar.js +++ b/client/src/containers/FinancialStatements/Journal/JournalActionsBar.js @@ -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 ( - + + + + ); } export default compose( - withJournal(({ - journalSheetFilter, - journalSheetRefresh - }) => ({ + withJournal(({ journalSheetFilter, journalSheetRefresh }) => ({ journalSheetFilter, journalSheetRefresh, })), diff --git a/client/src/containers/FinancialStatements/Journal/JournalSheetHeaderGeneralPanel.js b/client/src/containers/FinancialStatements/Journal/JournalSheetHeaderGeneralPanel.js new file mode 100644 index 000000000..7cdc468b1 --- /dev/null +++ b/client/src/containers/FinancialStatements/Journal/JournalSheetHeaderGeneralPanel.js @@ -0,0 +1,10 @@ +import React from 'react'; +import FinancialStatementDateRange from 'containers/FinancialStatements/FinancialStatementDateRange'; + +export default function JournalSheetHeaderGeneralPanel({}) { + return ( +
+ +
+ ); +} \ No newline at end of file diff --git a/client/src/containers/FinancialStatements/Journal/JournalTable.js b/client/src/containers/FinancialStatements/Journal/JournalTable.js index 31ccf99ae..ae211e40b 100644 --- a/client/src/containers/FinancialStatements/Journal/JournalTable.js +++ b/client/src/containers/FinancialStatements/Journal/JournalTable.js @@ -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 ( { - const { journalQuery } = props; - return { - journalIndex: getFinancialSheetIndexByQuery( - state.financialStatements.journal.sheets, - journalQuery, - ), - }; -}; - -const withJournalTable = connect(mapStateToProps); - export default compose( - withJournalTable, withJournal( ({ journalSheetTableRows, journalSheetLoading, journalSheetQuery }) => ({ journalSheetTableRows, diff --git a/client/src/containers/FinancialStatements/Journal/withJournal.js b/client/src/containers/FinancialStatements/Journal/withJournal.js index f50cc1fd5..f3038f75b 100644 --- a/client/src/containers/FinancialStatements/Journal/withJournal.js +++ b/client/src/containers/FinancialStatements/Journal/withJournal.js @@ -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); }; diff --git a/client/src/containers/FinancialStatements/ProfitLossSheet.js b/client/src/containers/FinancialStatements/ProfitLossSheet.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/client/src/containers/FinancialStatements/ProfitLossSheet/ProfitLossActionsBar.js b/client/src/containers/FinancialStatements/ProfitLossSheet/ProfitLossActionsBar.js index 132a0474b..8874e2504 100644 --- a/client/src/containers/FinancialStatements/ProfitLossSheet/ProfitLossActionsBar.js +++ b/client/src/containers/FinancialStatements/ProfitLossSheet/ProfitLossActionsBar.js @@ -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({ + + + + ); } export default compose( - withProfitLoss(({ + withProfitLoss(({ profitLossSheetFilter }) => ({ profitLossSheetFilter, - profitLossSheetRefresh, - }) => ({ - profitLossSheetFilter, - profitLossSheetRefresh, })), withProfitLossActions, )(ProfitLossHeader); diff --git a/client/src/containers/FinancialStatements/ProfitLossSheet/ProfitLossSheetHeaderGeneralPane.js b/client/src/containers/FinancialStatements/ProfitLossSheet/ProfitLossSheetHeaderGeneralPane.js new file mode 100644 index 000000000..2853b54dc --- /dev/null +++ b/client/src/containers/FinancialStatements/ProfitLossSheet/ProfitLossSheetHeaderGeneralPane.js @@ -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 ( +
+ + + + +
+ ); +} diff --git a/client/src/containers/FinancialStatements/ProfitLossSheet/ProfitLossSheetTable.js b/client/src/containers/FinancialStatements/ProfitLossSheet/ProfitLossSheetTable.js index 2333c3cea..9a19962da 100644 --- a/client/src/containers/FinancialStatements/ProfitLossSheet/ProfitLossSheetTable.js +++ b/client/src/containers/FinancialStatements/ProfitLossSheet/ProfitLossSheetTable.js @@ -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 ( ); } 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 ; } 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 ( ({ - 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, diff --git a/client/src/containers/FinancialStatements/ProfitLossSheet/withProfitLoss.js b/client/src/containers/FinancialStatements/ProfitLossSheet/withProfitLoss.js index 9b4fb3da6..1d18527fd 100644 --- a/client/src/containers/FinancialStatements/ProfitLossSheet/withProfitLoss.js +++ b/client/src/containers/FinancialStatements/ProfitLossSheet/withProfitLoss.js @@ -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, diff --git a/client/src/containers/FinancialStatements/RadiosAccountingBasis.js b/client/src/containers/FinancialStatements/RadiosAccountingBasis.js index bdcf502f9..c06931850 100644 --- a/client/src/containers/FinancialStatements/RadiosAccountingBasis.js +++ b/client/src/containers/FinancialStatements/RadiosAccountingBasis.js @@ -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 ( - { - onChange && onChange(value); - })} - className={'radio-group---accounting-basis'} - {...rest}> - - - + + {({ + form: { setFieldValue }, + field: { value }, + }) => ( + { + setFieldValue(key, value); + })} + className={'radio-group---accounting-basis'} + selectedValue={value} + {...rest} + > + + + + )} + ); -} \ No newline at end of file +} diff --git a/client/src/containers/FinancialStatements/SelectDisplayColumnsBy.js b/client/src/containers/FinancialStatements/SelectDisplayColumnsBy.js index bdfa730f4..7c2f1446e 100644 --- a/client/src/containers/FinancialStatements/SelectDisplayColumnsBy.js +++ b/client/src/containers/FinancialStatements/SelectDisplayColumnsBy.js @@ -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 (); - }, []); - - const handleItemSelect = useCallback((item) => { - setItemSelected(item); - onItemSelect && onItemSelect(item); - }, [setItemSelected, onItemSelect]); - - const buttonLabel = useMemo(() => - itemSelected ? itemSelected.name : , - [itemSelected]); + const { formGroupProps, selectListProps } = props; return ( - } - className="form-group-display-columns-by form-group--select-list bp3-fill" - inline={false} - {...formGroupProps}> - - } - filterable={false} - itemRenderer={itemRenderer} - popoverProps={{ minimal: true, usePortal: false, inline: true }} - buttonLabel={buttonLabel} - onItemSelect={handleItemSelect} - className={classNames(MODIFIER.SELECT_LIST_FILL_POPOVER)} - {...selectListProps} /> - + + + + {({ form, field: { value }, meta: { error, touched } }) => ( + } + className="form-group-display-columns-by form-group--select-list bp3-fill" + inline={false} + {...formGroupProps} + > + { + form.setFieldValue('displayColumnsType', item.key); + }} + popoverProps={{ minimal: true }} + {...selectListProps} + /> + + )} + + + ); -} \ No newline at end of file +} diff --git a/client/src/containers/FinancialStatements/TrialBalanceSheet/TrialBalanceActionsBar.js b/client/src/containers/FinancialStatements/TrialBalanceSheet/TrialBalanceActionsBar.js index 91656578f..bfff94f59 100644 --- a/client/src/containers/FinancialStatements/TrialBalanceSheet/TrialBalanceActionsBar.js +++ b/client/src/containers/FinancialStatements/TrialBalanceSheet/TrialBalanceActionsBar.js @@ -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({ + + + + ); } diff --git a/client/src/containers/FinancialStatements/TrialBalanceSheet/TrialBalanceSheetHeaderGeneralPanel.js b/client/src/containers/FinancialStatements/TrialBalanceSheet/TrialBalanceSheetHeaderGeneralPanel.js new file mode 100644 index 000000000..766567c73 --- /dev/null +++ b/client/src/containers/FinancialStatements/TrialBalanceSheet/TrialBalanceSheetHeaderGeneralPanel.js @@ -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 ( +
+ + + +
+ ); +} diff --git a/client/src/containers/FinancialStatements/TrialBalanceSheet/TrialBalanceSheetTable.js b/client/src/containers/FinancialStatements/TrialBalanceSheet/TrialBalanceSheetTable.js index 2e2171970..dcd79e9b2 100644 --- a/client/src/containers/FinancialStatements/TrialBalanceSheet/TrialBalanceSheetTable.js +++ b/client/src/containers/FinancialStatements/TrialBalanceSheet/TrialBalanceSheetTable.js @@ -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 }) => , + Cell: ({ cell }) => { + const { currency_code, credit } = cell.row.original; + return (); + }, className: 'credit', - minWidth: 95, - maxWidth: 95, width: 95, }, { Header: formatMessage({ id: 'debit' }), accessor: 'debit', - Cell: ({ cell }) => , + Cell: ({ cell }) => { + const { currency_code, debit } = cell.row.original; + return (); + }, className: 'debit', - minWidth: 95, - maxWidth: 95, width: 95, }, { Header: formatMessage({ id: 'balance' }), accessor: 'balance', - Cell: ({ cell }) => , + Cell: ({ cell }) => { + const { currency_code, balance } = cell.row.original; + return (); + }, className: 'balance', - minWidth: 95, - maxWidth: 95, width: 95, }, ], [formatMessage], ); - const handleFetchData = useCallback(() => { - onFetchData && onFetchData(); - }, [onFetchData]); - return ( { - 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); diff --git a/client/src/containers/FinancialStatements/TrialBalanceSheet/withTrialBalance.js b/client/src/containers/FinancialStatements/TrialBalanceSheet/withTrialBalance.js index 421c18217..c2b5def47 100644 --- a/client/src/containers/FinancialStatements/TrialBalanceSheet/withTrialBalance.js +++ b/client/src/containers/FinancialStatements/TrialBalanceSheet/withTrialBalance.js @@ -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); }; diff --git a/client/src/containers/FinancialStatements/common.js b/client/src/containers/FinancialStatements/common.js new file mode 100644 index 000000000..6a82653ad --- /dev/null +++ b/client/src/containers/FinancialStatements/common.js @@ -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)); +}; diff --git a/client/src/index.js b/client/src/index.js index 24053eb30..f4d1ecca0 100644 --- a/client/src/index.js +++ b/client/src/index.js @@ -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( diff --git a/client/src/lang/en/index.js b/client/src/lang/en/index.js index 936b3ce89..a7ecf2ddc 100644 --- a/client/src/lang/en/index.js +++ b/client/src/lang/en/index.js @@ -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 ', diff --git a/client/src/routes/dashboard.js b/client/src/routes/dashboard.js index a9b12fdf0..414dceaa8 100644 --- a/client/src/routes/dashboard.js +++ b/client/src/routes/dashboard.js @@ -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({ diff --git a/client/src/store/financialStatement/financialStatements.actions.js b/client/src/store/financialStatement/financialStatements.actions.js index 7d1125637..31ef7f47d 100644 --- a/client/src/store/financialStatement/financialStatements.actions.js +++ b/client/src/store/financialStatement/financialStatements.actions.js @@ -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, }); diff --git a/client/src/store/financialStatement/financialStatements.mappers.js b/client/src/store/financialStatement/financialStatements.mappers.js new file mode 100644 index 000000000..b980c5875 --- /dev/null +++ b/client/src/store/financialStatement/financialStatements.mappers.js @@ -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'], + }, + ]; +}; \ No newline at end of file diff --git a/client/src/store/financialStatement/financialStatements.reducer.js b/client/src/store/financialStatement/financialStatements.reducer.js index 8576b228f..4e1a59502 100644 --- a/client/src/store/financialStatement/financialStatements.reducer.js +++ b/client/src/store/financialStatement/financialStatements.reducer.js @@ -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', diff --git a/client/src/store/financialStatement/financialStatements.selectors.js b/client/src/store/financialStatement/financialStatements.selectors.js index 9c2183e97..6c87f33a3 100644 --- a/client/src/store/financialStatement/financialStatements.selectors.js +++ b/client/src/store/financialStatement/financialStatements.selectors.js @@ -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 : []; -}; \ No newline at end of file +/** + * Retrieve financial statement table rows by the given sheet index. + */ +export const getFinancialSheetTableRowsFactory = (sheetType) => + createSelector( + sheetByTypeSelector(sheetType), + (sheet) => { + return (sheet && sheet.tableRows) ? sheet.tableRows : []; + } + ); diff --git a/client/src/style/App.scss b/client/src/style/App.scss index cafaad53f..5de0242af 100644 --- a/client/src/style/App.scss +++ b/client/src/style/App.scss @@ -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; diff --git a/client/src/style/components/data-table.scss b/client/src/style/components/data-table.scss index 000db5ae6..260ce47bb 100644 --- a/client/src/style/components/data-table.scss +++ b/client/src/style/components/data-table.scss @@ -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; } } diff --git a/client/src/style/pages/financial-statements.scss b/client/src/style/pages/financial-statements.scss index b4f6c3951..2c791ef75 100644 --- a/client/src/style/pages/financial-statements.scss +++ b/client/src/style/pages/financial-statements.scss @@ -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; + } + } } \ No newline at end of file diff --git a/client/src/utils.js b/client/src/utils.js index 452e97d41..c260d50c8 100644 --- a/client/src/utils.js +++ b/client/src/utils.js @@ -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); -} \ No newline at end of file +} + + + +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; +}; diff --git a/server/src/api/controllers/FinancialStatements/JournalSheet.ts b/server/src/api/controllers/FinancialStatements/JournalSheet.ts index 57ec8d05e..2b86184fd 100644 --- a/server/src/api/controllers/FinancialStatements/JournalSheet.ts +++ b/server/src/api/controllers/FinancialStatements/JournalSheet.ts @@ -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, diff --git a/server/src/api/controllers/Items.ts b/server/src/api/controllers/Items.ts index 3b93e30ff..c4e5715eb 100644 --- a/server/src/api/controllers/Items.ts +++ b/server/src/api/controllers/Items.ts @@ -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); } -} \ No newline at end of file +} diff --git a/server/src/api/controllers/Sales/PaymentReceives.ts b/server/src/api/controllers/Sales/PaymentReceives.ts index e5dda2676..ce7b25103 100644 --- a/server/src/api/controllers/Sales/PaymentReceives.ts +++ b/server/src/api/controllers/Sales/PaymentReceives.ts @@ -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 + ), + }, + }, + ], }); } } diff --git a/server/src/interfaces/TrialBalanceSheet.ts b/server/src/interfaces/TrialBalanceSheet.ts index 99f267846..fcaa4fd6d 100644 --- a/server/src/interfaces/TrialBalanceSheet.ts +++ b/server/src/interfaces/TrialBalanceSheet.ts @@ -23,6 +23,7 @@ export interface ITrialBalanceAccount { credit: number, debit: number, balance: number, + currencyCode: string, formattedCredit: string, formattedDebit: string, diff --git a/server/src/services/FinancialStatements/BalanceSheet/BalanceSheet.ts b/server/src/services/FinancialStatements/BalanceSheet/BalanceSheet.ts index 106b7f4fd..f87405be2 100644 --- a/server/src/services/FinancialStatements/BalanceSheet/BalanceSheet.ts +++ b/server/src/services/FinancialStatements/BalanceSheet/BalanceSheet.ts @@ -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 + ): 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 + ); } -} \ No newline at end of file +} diff --git a/server/src/services/FinancialStatements/BalanceSheet/BalanceSheetService.ts b/server/src/services/FinancialStatements/BalanceSheet/BalanceSheetService.ts index 3ed709422..fc1e96212 100644 --- a/server/src/services/FinancialStatements/BalanceSheet/BalanceSheetService.ts +++ b/server/src/services/FinancialStatements/BalanceSheet/BalanceSheetService.ts @@ -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( diff --git a/server/src/services/FinancialStatements/JournalSheet/JournalSheet.ts b/server/src/services/FinancialStatements/JournalSheet/JournalSheet.ts index a8be532b9..ce49f5483 100644 --- a/server/src/services/FinancialStatements/JournalSheet/JournalSheet.ts +++ b/server/src/services/FinancialStatements/JournalSheet/JournalSheet.ts @@ -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); } } \ No newline at end of file diff --git a/server/src/services/FinancialStatements/JournalSheet/JournalSheetService.ts b/server/src/services/FinancialStatements/JournalSheet/JournalSheetService.ts index fa72c03a6..27a7e2e0c 100644 --- a/server/src/services/FinancialStatements/JournalSheet/JournalSheetService.ts +++ b/server/src/services/FinancialStatements/JournalSheet/JournalSheetService.ts @@ -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, + }; } -} \ No newline at end of file +} diff --git a/server/src/services/FinancialStatements/ProfitLossSheet/ProfitLossSheet.ts b/server/src/services/FinancialStatements/ProfitLossSheet/ProfitLossSheet.ts index d70c8f3cc..1ab120beb 100644 --- a/server/src/services/FinancialStatements/ProfitLossSheet/ProfitLossSheet.ts +++ b/server/src/services/FinancialStatements/ProfitLossSheet/ProfitLossSheet.ts @@ -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']; } -} \ No newline at end of file +} diff --git a/server/src/services/FinancialStatements/TrialBalanceSheet/TrialBalanceSheet.ts b/server/src/services/FinancialStatements/TrialBalanceSheet/TrialBalanceSheet.ts index 7e7e2da29..17841111b 100644 --- a/server/src/services/FinancialStatements/TrialBalanceSheet/TrialBalanceSheet.ts +++ b/server/src/services/FinancialStatements/TrialBalanceSheet/TrialBalanceSheet.ts @@ -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), diff --git a/server/src/services/FinancialStatements/TrialBalanceSheet/TrialBalanceSheetService.ts b/server/src/services/FinancialStatements/TrialBalanceSheet/TrialBalanceSheetService.ts index 8f2a05755..b2c25937e 100644 --- a/server/src/services/FinancialStatements/TrialBalanceSheet/TrialBalanceSheetService.ts +++ b/server/src/services/FinancialStatements/TrialBalanceSheet/TrialBalanceSheetService.ts @@ -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(); diff --git a/server/src/services/Sales/PaymentsReceives.ts b/server/src/services/Sales/PaymentsReceives.ts index d6fa68687..f61a80275 100644 --- a/server/src/services/Sales/PaymentsReceives.ts +++ b/server/src/services/Sales/PaymentsReceives.ts @@ -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 { 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 { - const { accountTypeRepository, accountRepository } = this.tenancy.repositories(tenantId); + async getDepositAccountOrThrowError( + tenantId: number, + depositAccountId: number + ): Promise { + 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 { + async validateInvoicesIDsExistance( + tenantId: number, + customerId: number, + paymentReceiveEntries: IPaymentReceiveEntryDTO[] + ): Promise { 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} */ - 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 { const { SaleInvoice } = this.tenancy.models(tenantId); const opers: Promise[] = []; @@ -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]); } }