- fix: store children accounts with Redux store.

- fix: store expense payment date with transactions.
- fix: Total assets, liabilities and equity on balance sheet.
- tweaks: dashboard content and sidebar style.
- fix: reset form with contact list on journal entry form.
- feat: Add hints to filter accounts in financial statements.
This commit is contained in:
Ahmed Bouhuolia
2020-07-12 12:31:12 +02:00
parent 4bd8f1628d
commit 9d9c7c1568
60 changed files with 1685 additions and 929 deletions

View File

@@ -1,43 +1,51 @@
import React, { useCallback, useState } from 'react'; import React, { useCallback, useMemo } from 'react';
import { import { MenuItem, Button } from '@blueprintjs/core';
MenuItem,
Button,
} from '@blueprintjs/core';
import ListSelect from 'components/ListSelect'; import ListSelect from 'components/ListSelect';
import { FormattedMessage as T } from 'react-intl'; import { FormattedMessage as T } from 'react-intl';
export default function ContactsListField({ export default function ContactsListField({
contacts, contacts,
onContactSelected, onContactSelected,
error, selectedContactId,
initialContact, selectedContactType,
defautlSelectText = (<T id={'select_contact'} />) defautlSelectText = <T id={'select_contact'} />,
}) { }) {
const [selectedContact, setSelectedContact] = useState( // Contact item of select accounts field.
initialContact || null const contactRenderer = useCallback(
(item, { handleClick, modifiers, query }) => (
<MenuItem text={item.display_name} key={item.id} onClick={handleClick} />
),
[],
); );
// Contact item of select accounts field. const onContactSelect = useCallback(
const contactItem = useCallback((item, { handleClick, modifiers, query }) => ( (contact) => {
<MenuItem text={item.display_name} key={item.id} onClick={handleClick} />
), []);
const onContactSelect = useCallback((contact) => {
setSelectedContact(contact.id);
onContactSelected && onContactSelected(contact); onContactSelected && onContactSelected(contact);
}, [setSelectedContact, onContactSelected]); },
[onContactSelected],
);
const items = useMemo(
() =>
contacts.map((contact) => ({
...contact,
_id: `${contact.id}_${contact.contact_type}`,
})),
[contacts],
);
return ( return (
<ListSelect <ListSelect
items={contacts} items={items}
noResults={<MenuItem disabled={true} text='No results.' />} noResults={<MenuItem disabled={true} text="No results." />}
itemRenderer={contactItem} itemRenderer={contactRenderer}
popoverProps={{ minimal: true }} popoverProps={{ minimal: true }}
filterable={true} filterable={true}
onItemSelect={onContactSelect} onItemSelect={onContactSelect}
labelProp={'display_name'} labelProp={'display_name'}
selectedItem={selectedContact} selectedItem={`${selectedContactId}_${selectedContactType}`}
selectedItemProp={'id'} selectedItemProp={'_id'}
defaultText={defautlSelectText} /> defaultText={defautlSelectText}
/>
); );
} }

View File

@@ -110,10 +110,15 @@ function DashboardTopbar({
icon={<Icon icon={'plus-24'} iconSize={20} />} icon={<Icon icon={'plus-24'} iconSize={20} />}
text={<T id={'quick_new'} />} text={<T id={'quick_new'} />}
/> />
<Tooltip
content={<T id={'notifications'} />}
position={Position.BOTTOM}
>
<Button <Button
className={Classes.MINIMAL} className={Classes.MINIMAL}
icon={<Icon icon={'notification-24'} iconSize={20} />} icon={<Icon icon={'notification-24'} iconSize={20} />}
/> />
</Tooltip>
<Button <Button
className={Classes.MINIMAL} className={Classes.MINIMAL}
icon={<Icon icon={'help-24'} iconSize={20} />} icon={<Icon icon={'help-24'} iconSize={20} />}

View File

@@ -8,7 +8,13 @@ import {
useSortBy, useSortBy,
useFlexLayout, useFlexLayout,
} from 'react-table'; } from 'react-table';
import { Checkbox, Spinner, ContextMenu, Menu, MenuItem } from '@blueprintjs/core'; import {
Checkbox,
Spinner,
ContextMenu,
Menu,
MenuItem,
} from '@blueprintjs/core';
import classnames from 'classnames'; import classnames from 'classnames';
import { FixedSizeList } from 'react-window'; import { FixedSizeList } from 'react-window';
import { useSticky } from 'react-table-sticky'; import { useSticky } from 'react-table-sticky';
@@ -30,7 +36,7 @@ export default function DataTable({
loading, loading,
onFetchData, onFetchData,
onSelectedRowsChange, onSelectedRowsChange,
manualSortBy = 'false', manualSortBy = false,
selectionColumn = false, selectionColumn = false,
expandSubRows = true, expandSubRows = true,
className, className,
@@ -51,7 +57,9 @@ export default function DataTable({
pagesCount: controlledPageCount, pagesCount: controlledPageCount,
initialPageIndex, initialPageIndex,
initialPageSize, initialPageSize,
rowContextMenu rowContextMenu,
expandColumnSpace = 1.5,
}) { }) {
const { const {
getTableProps, getTableProps,
@@ -91,7 +99,6 @@ export default function DataTable({
manualSortBy, manualSortBy,
expandSubRows, expandSubRows,
payload, payload,
autoResetSelectedRows: false,
}, },
useSortBy, useSortBy,
useExpanded, useExpanded,
@@ -148,7 +155,7 @@ export default function DataTable({
} else { } else {
onFetchData && onFetchData({ pageIndex, pageSize, sortBy }); onFetchData && onFetchData({ pageIndex, pageSize, sortBy });
} }
}, [pageIndex, pageSize, sortBy, onFetchData]); }, [pageIndex, pageSize, manualSortBy ? sortBy : null, onFetchData]);
useUpdateEffect(() => { useUpdateEffect(() => {
onSelectedRowsChange && onSelectedRowsChange(selectedFlatRows); onSelectedRowsChange && onSelectedRowsChange(selectedFlatRows);
@@ -162,7 +169,7 @@ export default function DataTable({
wrapper={(children) => ( wrapper={(children) => (
<div <div
style={{ style={{
'padding-left': `${row.depth * 1.5}rem`, 'padding-left': `${row.depth * expandColumnSpace}rem`,
}} }}
> >
{children} {children}
@@ -216,7 +223,9 @@ export default function DataTable({
return ( return (
<div <div
{...row.getRowProps({ {...row.getRowProps({
className: classnames('tr', rowClasses), className: classnames('tr', {
'is-expanded': row.isExpanded && row.canExpand,
}, rowClasses),
style, style,
})} })}
> >

View File

@@ -1,12 +1,12 @@
import React, { useCallback, useMemo } from 'react'; import React, { useState, useCallback, useMemo } from 'react';
import { FormGroup, Intent, Classes } from "@blueprintjs/core"; import { FormGroup, Intent, Classes } from "@blueprintjs/core";
import classNames from 'classnames'; import classNames from 'classnames';
import ContactsListField from 'components/ContactsListField'; import ContactsListField from 'components/ContactsListField';
export default function ContactsListCellRenderer({ export default function ContactsListCellRenderer({
column: { id, value }, column: { id },
row: { index, original }, row: { index, original },
cell: { value: initialValue }, cell: { value },
payload: { contacts, updateData, errors } payload: { contacts, updateData, errors }
}) { }) {
const handleContactSelected = useCallback((contact) => { const handleContactSelected = useCallback((contact) => {
@@ -16,10 +16,6 @@ export default function ContactsListCellRenderer({
}); });
}, [updateData, index, id]); }, [updateData, index, id]);
const initialContact = useMemo(() => {
return contacts.find(c => c.id === initialValue);
}, [contacts, initialValue]);
const error = errors?.[index]?.[id]; const error = errors?.[index]?.[id];
return ( return (
@@ -34,9 +30,9 @@ export default function ContactsListCellRenderer({
<ContactsListField <ContactsListField
contacts={contacts} contacts={contacts}
onContactSelected={handleContactSelected} onContactSelected={handleContactSelected}
initialContact={initialContact} selectedContactId={original?.contact_id}
selectedContactType={original?.contact_type}
/> />
</FormGroup> </FormGroup>
) );
} }

View File

@@ -1,7 +1,7 @@
import React, { useMemo, useCallback } from 'react'; import React, { useMemo, useCallback } from 'react';
import moment from 'moment'; import moment from 'moment';
import classnames from 'classnames'; import classnames from 'classnames';
import LoadingIndicator from 'components/LoadingIndicator'; import { LoadingIndicator, MODIFIER } from 'components';
import { FormattedMessage as T, useIntl } from 'react-intl'; import { FormattedMessage as T, useIntl } from 'react-intl';
import { If } from 'components'; import { If } from 'components';
@@ -17,6 +17,8 @@ export default function FinancialSheet({
loading, loading,
className, className,
basis, basis,
minimal = false,
fullWidth = false
}) { }) {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const format = 'DD MMMM YYYY'; const format = 'DD MMMM YYYY';
@@ -45,7 +47,12 @@ export default function FinancialSheet({
]); ]);
return ( return (
<div className={classnames('financial-sheet', nameModifer, className)}> <div
className={classnames('financial-sheet', nameModifer, className, {
[MODIFIER.FINANCIAL_SHEET_MINIMAL]: minimal,
'is-full-width': fullWidth,
})}
>
<LoadingIndicator loading={loading} spinnerSize={34} /> <LoadingIndicator loading={loading} spinnerSize={34} />
<div <div
@@ -53,8 +60,14 @@ export default function FinancialSheet({
'is-loading': loading, 'is-loading': loading,
})} })}
> >
<If condition={!!companyName}>
<h1 class="financial-sheet__title">{companyName}</h1> <h1 class="financial-sheet__title">{companyName}</h1>
</If>
<If condition={!!sheetType}>
<h6 class="financial-sheet__sheet-type">{sheetType}</h6> <h6 class="financial-sheet__sheet-type">{sheetType}</h6>
</If>
<div class="financial-sheet__date"> <div class="financial-sheet__date">
<If condition={asDate}> <If condition={asDate}>
<T id={'as'} /> {formattedAsDate} <T id={'as'} /> {formattedAsDate}

View File

@@ -1,44 +1,68 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useMemo, useEffect } from 'react';
import { import { Button, MenuItem } from '@blueprintjs/core';
Button,
MenuItem,
} from '@blueprintjs/core';
import { Select } from '@blueprintjs/select'; import { Select } from '@blueprintjs/select';
import { FormattedMessage as T } from 'react-intl'; import { FormattedMessage as T } from 'react-intl';
export default function ListSelect({ export default function ListSelect({
buttonProps, buttonProps,
defaultText, defaultText,
noResultsText = (<T id="no_results" />), noResultsText = <T id="no_results" />,
isLoading = false, isLoading = false,
labelProp, labelProp,
selectedItem, selectedItem,
selectedItemProp = 'id', selectedItemProp = 'id',
// itemTextProp, initialSelectedItem,
// itemLabelProp, onItemSelect,
...selectProps ...selectProps
}) { }) {
const [currentItem, setCurrentItem] = useState(null); const selectedItemObj = useMemo(
() => selectProps.items.find((i) => i[selectedItemProp] === selectedItem),
[selectProps.items, selectedItemProp, selectedItem],
);
const selectedInitialItem = useMemo(
() => selectProps.items.find((i) => i[selectedItemProp] === initialSelectedItem),
[initialSelectedItem],
);
const [currentItem, setCurrentItem] = useState(
(initialSelectedItem && selectedInitialItem) || null,
);
useEffect(() => { useEffect(() => {
if (selectedItem && selectedItemProp) { if (selectedItemObj) {
const item = selectProps.items.find(i => i[selectedItemProp] === selectedItem); setCurrentItem(selectedItemObj);
setCurrentItem(item);
} }
}, [selectedItem, selectedItemProp, selectProps.items]); }, [selectedItemObj, setCurrentItem]);
const noResults = isLoading ? const noResults = isLoading ? (
('loading') : <MenuItem disabled={true} text={noResultsText} />; 'loading'
) : (
<MenuItem disabled={true} text={noResultsText} />
);
const itemRenderer = (item, { handleClick, modifiers, query }) => { const itemRenderer = (item, { handleClick, modifiers, query }) => {
return (<MenuItem text={item[labelProp]} key={item[selectedItemProp]} onClick={handleClick} />); return (
<MenuItem
text={item[labelProp]}
key={item[selectedItemProp]}
onClick={handleClick}
/>
);
};
const handleItemSelect = (_item) => {
setCurrentItem(_item);
onItemSelect && onItemSelect(_item);
}; };
return ( return (
<Select <Select
itemRenderer={itemRenderer} itemRenderer={itemRenderer}
onItemSelect={handleItemSelect}
{...selectProps} {...selectProps}
noResults={noResults} noResults={noResults}
> >
@@ -48,5 +72,5 @@ export default function ListSelect ({
{...buttonProps} {...buttonProps}
/> />
</Select> </Select>
) );
} }

View File

@@ -20,6 +20,7 @@ import AppToaster from './AppToaster';
import DataTable from './DataTable'; import DataTable from './DataTable';
import AccountsSelectList from './AccountsSelectList'; import AccountsSelectList from './AccountsSelectList';
import AccountsTypesSelect from './AccountsTypesSelect'; import AccountsTypesSelect from './AccountsTypesSelect';
import LoadingIndicator from './LoadingIndicator';
const Hint = FieldHint; const Hint = FieldHint;
@@ -47,4 +48,5 @@ export {
DataTable, DataTable,
AccountsSelectList, AccountsSelectList,
AccountsTypesSelect, AccountsTypesSelect,
LoadingIndicator,
}; };

View File

@@ -1,4 +1,7 @@
export default { export default {
SELECT_LIST_FILL_POPOVER: 'select-list--fill-popover', SELECT_LIST_FILL_POPOVER: 'select-list--fill-popover',
SELECT_LIST_FILL_BUTTON: 'select-list--fill-button', SELECT_LIST_FILL_BUTTON: 'select-list--fill-button',
SELECT_LIST_TOOLTIP_ITEMS: 'select-list--tooltip-items',
FINANCIAL_SHEET_MINIMAL: 'financial-sheet--minimal'
} }

View File

@@ -117,6 +117,7 @@ function MakeJournalEntriesForm({
then: Yup.number().required(), then: Yup.number().required(),
}), }),
contact_id: Yup.number().nullable(), contact_id: Yup.number().nullable(),
contact_type: Yup.string().nullable(),
note: Yup.string().max(255).nullable(), note: Yup.string().max(255).nullable(),
}), }),
), ),
@@ -140,9 +141,9 @@ function MakeJournalEntriesForm({
const defaultEntry = useMemo( const defaultEntry = useMemo(
() => ({ () => ({
account_id: null, account_id: null,
contact_id: null,
credit: 0, credit: 0,
debit: 0, debit: 0,
contact_id: null,
note: '', note: '',
}), }),
[], [],

View File

@@ -104,7 +104,7 @@ function ManualJournalActionsBar({
<Button <Button
className={Classes.MINIMAL} className={Classes.MINIMAL}
icon={<Icon icon="plus" />} icon={<Icon icon="plus" />}
text={<T id={'new_journal'} />} text={<T id={'journal_entry'} />}
onClick={onClickNewManualJournal} onClick={onClickNewManualJournal}
/> />
<Popover <Popover

View File

@@ -127,6 +127,7 @@ function ManualJournalsDataTable({
/> />
<MenuItem <MenuItem
text={formatMessage({ id: 'delete_journal' })} text={formatMessage({ id: 'delete_journal' })}
icon={<Icon icon="trash-16" iconSize={16} />}
intent={Intent.DANGER} intent={Intent.DANGER}
onClick={handleDeleteJournal(journal)} onClick={handleDeleteJournal(journal)}
/> />

View File

@@ -109,6 +109,14 @@ function AccountsChart({
intent: Intent.DANGER, intent: Intent.DANGER,
}); });
} }
if (errors.find(e => e.type === 'ACCOUNT.HAS.CHILD.ACCOUNTS')) {
AppToaster.show({
message: formatMessage({
id: 'you_could_not_delete_account_has_child_accounts',
}),
intent: Intent.DANGER,
})
}
}; };
// Handle confirm account delete // Handle confirm account delete

View File

@@ -165,6 +165,7 @@ function AccountsDataTable({
onClick={handleEditAccount(account)} onClick={handleEditAccount(account)}
/> />
<MenuItem <MenuItem
icon={<Icon icon="plus" />}
text={formatMessage({ id: 'new_child_account' })} text={formatMessage({ id: 'new_child_account' })}
onClick={() => handleNewParentAccount(account)} onClick={() => handleNewParentAccount(account)}
/> />
@@ -172,17 +173,20 @@ function AccountsDataTable({
<If condition={account.active}> <If condition={account.active}>
<MenuItem <MenuItem
text={formatMessage({ id: 'inactivate_account' })} text={formatMessage({ id: 'inactivate_account' })}
icon={<Icon icon="pause-16" iconSize={16} />}
onClick={() => onInactiveAccount(account)} onClick={() => onInactiveAccount(account)}
/> />
</If> </If>
<If condition={!account.active}> <If condition={!account.active}>
<MenuItem <MenuItem
text={formatMessage({ id: 'activate_account' })} text={formatMessage({ id: 'activate_account' })}
icon={<Icon icon="play-16" iconSize={16} />}
onClick={() => onActivateAccount(account)} onClick={() => onActivateAccount(account)}
/> />
</If> </If>
<MenuItem <MenuItem
text={formatMessage({ id: 'delete_account' })} text={formatMessage({ id: 'delete_account' })}
icon={<Icon icon="trash-16" iconSize={16} />}
intent={Intent.DANGER} intent={Intent.DANGER}
onClick={() => onDeleteAccount(account)} onClick={() => onDeleteAccount(account)}
/> />
@@ -297,6 +301,7 @@ function AccountsDataTable({
onSelectedRowsChange={handleSelectedRowsChange} onSelectedRowsChange={handleSelectedRowsChange}
loading={accountsLoading && !isMounted} loading={accountsLoading && !isMounted}
rowContextMenu={rowContextMenu} rowContextMenu={rowContextMenu}
expandColumnSpace={1}
/> />
); );
} }

View File

@@ -6,7 +6,6 @@ import {
getResourceViews, getResourceViews,
} from 'store/customViews/customViews.selectors'; } from 'store/customViews/customViews.selectors';
export default (mapState) => { export default (mapState) => {
const getAccountsList = getAccountsListFactory(); const getAccountsList = getAccountsListFactory();

View File

@@ -21,12 +21,14 @@ function Customer({
const { id } = useParams(); const { id } = useParams();
const history = useHistory(); const history = useHistory();
const fetchCustomers = useQuery('customers-list', () => // Handle fetch customers data table
const fetchCustomers = useQuery('customers-table', () =>
requestFetchCustomers({}), requestFetchCustomers({}),
); );
// Handle fetch customer details.
const fetchCustomerDatails = useQuery(id && ['customer-detail', id], () => const fetchCustomer= useQuery(['customer', id], () =>
requestFetchCustomers(), requestFetchCustomers(),
{ enabled: !!id },
); );
const handleFormSubmit = useCallback( const handleFormSubmit = useCallback(
@@ -42,7 +44,7 @@ function Customer({
return ( return (
<DashboardInsider <DashboardInsider
loading={fetchCustomerDatails.isFetching || fetchCustomers.isFetching} loading={fetchCustomer.isFetching || fetchCustomers.isFetching}
name={'customer-form'} name={'customer-form'}
> >
<CustomerForm <CustomerForm

View File

@@ -45,16 +45,16 @@ const CustomerActionsBar = ({
history.push('/customers/new'); history.push('/customers/new');
}, [history]); }, [history]);
const filterDropdown = FilterDropdown({ // const filterDropdown = FilterDropdown({
fields: resourceFields, // fields: resourceFields,
onFilterChange: (filterConditions) => { // onFilterChange: (filterConditions) => {
setFilterCount(filterConditions.length || 0); // setFilterCount(filterConditions.length || 0);
addCustomersTableQueries({ // addCustomersTableQueries({
filter_roles: filterConditions || '', // filter_roles: filterConditions || '',
}); // });
onFilterChanged && onFilterChanged(filterConditions); // onFilterChanged && onFilterChanged(filterConditions);
}, // },
}); // });
const hasSelectedRows = useMemo(() => selectedRows.length > 0, [ const hasSelectedRows = useMemo(() => selectedRows.length > 0, [
selectedRows, selectedRows,
@@ -75,7 +75,7 @@ const CustomerActionsBar = ({
/> />
<NavbarDivider /> <NavbarDivider />
<Popover <Popover
content={filterDropdown} // content={filterDropdown}
interactionKind={PopoverInteractionKind.CLICK} interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT} position={Position.BOTTOM_LEFT}
> >
@@ -101,7 +101,6 @@ const CustomerActionsBar = ({
onClick={handleBulkDelete} onClick={handleBulkDelete}
/> />
</If> </If>
<Button <Button
className={Classes.MINIMAL} className={Classes.MINIMAL}
icon={<Icon icon="file-import-16" iconSize={16} />} icon={<Icon icon="file-import-16" iconSize={16} />}
@@ -120,13 +119,12 @@ const CustomerActionsBar = ({
const mapStateToProps = (state, props) => ({ const mapStateToProps = (state, props) => ({
resourceName: 'customers', resourceName: 'customers',
}); });
const withCustomersActionsBar = connect(mapStateToProps); const withCustomersActionsBar = connect(mapStateToProps);
export default compose( export default compose(
withCustomersActionsBar, withCustomersActionsBar,
withCustomersActions,
withResourceDetail(({ resourceFields }) => ({ withResourceDetail(({ resourceFields }) => ({
resourceFields, resourceFields,
})), })),
withCustomersActions,
)(CustomerActionsBar); )(CustomerActionsBar);

View File

@@ -3,25 +3,20 @@ import * as Yup from 'yup';
import { useFormik } from 'formik'; import { useFormik } from 'formik';
import { import {
FormGroup, FormGroup,
MenuItem,
Intent, Intent,
InputGroup, InputGroup,
Button,
Classes,
Checkbox, Checkbox,
} from '@blueprintjs/core'; } from '@blueprintjs/core';
import { Row, Col } from 'react-grid-system'; import { Row, Col } from 'react-grid-system';
import { FormattedMessage as T, useIntl } from 'react-intl'; import { FormattedMessage as T, useIntl } from 'react-intl';
import { queryCache, useQuery } from 'react-query';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { pick } from 'lodash'; import { pick } from 'lodash';
import classNames from 'classnames';
import AppToaster from 'components/AppToaster'; import AppToaster from 'components/AppToaster';
import ErrorMessage from 'components/ErrorMessage'; import ErrorMessage from 'components/ErrorMessage';
import CustomersTabs from 'containers/Customers/CustomersTabs'; import CustomersTabs from 'containers/Customers/CustomersTabs';
import RadioCustomer from 'containers/Customers/RadioCustomer'; import CustomerTypeRadioField from 'containers/Customers/CustomerTypeRadioField';
import withDashboardActions from 'containers/Dashboard/withDashboardActions'; import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withCustomerDetail from 'containers/Customers/withCustomerDetail'; import withCustomerDetail from 'containers/Customers/withCustomerDetail';
@@ -51,6 +46,7 @@ function CustomerForm({
// #withMediaActions // #withMediaActions
requestSubmitMedia, requestSubmitMedia,
requestDeleteMedia, requestDeleteMedia,
//#Props //#Props
onFormSubmit, onFormSubmit,
onCancelForm, onCancelForm,
@@ -258,7 +254,7 @@ function CustomerForm({
<div className={'customer-form'}> <div className={'customer-form'}>
<form onSubmit={formik.handleSubmit}> <form onSubmit={formik.handleSubmit}>
<div className={'customer-form__primary-section'}> <div className={'customer-form__primary-section'}>
<RadioCustomer <CustomerTypeRadioField
selectedValue={formik.values.customer_type} selectedValue={formik.values.customer_type}
onChange={handleCustomerTypeCahange} onChange={handleCustomerTypeCahange}
className={'form-group--customer-type'} className={'form-group--customer-type'}
@@ -440,4 +436,5 @@ export default compose(
})), })),
withDashboardActions, withDashboardActions,
withCustomersActions, withCustomersActions,
withMediaActions,
)(CustomerForm); )(CustomerForm);

View File

@@ -11,7 +11,7 @@ import {
} from '@blueprintjs/core'; } from '@blueprintjs/core';
import { useFormik } from 'formik'; import { useFormik } from 'formik';
import { FormattedMessage as T, useIntl } from 'react-intl'; import { FormattedMessage as T, useIntl } from 'react-intl';
import { pick } from 'lodash'; import { pick, omit } from 'lodash';
import { useQuery, queryCache } from 'react-query'; import { useQuery, queryCache } from 'react-query';
import classNames from 'classnames'; import classNames from 'classnames';
import Yup from 'services/yup'; import Yup from 'services/yup';
@@ -72,7 +72,6 @@ function AccountFormDialog({
name: '', name: '',
code: '', code: '',
description: '', description: '',
parent_account_id: null,
}), }),
[], [],
); );
@@ -103,8 +102,7 @@ function AccountFormDialog({
}, },
validationSchema, validationSchema,
onSubmit: (values, { setSubmitting, setErrors }) => { onSubmit: (values, { setSubmitting, setErrors }) => {
const form = pick(values, Object.keys(initialValues)); const form = omit(values, ['subaccount']);
const toastAccountName = values.code const toastAccountName = values.code
? `${values.code} - ${values.name}` ? `${values.code} - ${values.name}`
: values.name; : values.name;
@@ -114,13 +112,11 @@ function AccountFormDialog({
queryCache.invalidateQueries('accounts-table'); queryCache.invalidateQueries('accounts-table');
queryCache.invalidateQueries('accounts-list'); queryCache.invalidateQueries('accounts-list');
}; };
const afterErrors = (errors) => { const afterErrors = (errors) => {
const errorsTransformed = transformApiErrors(errors); const errorsTransformed = transformApiErrors(errors);
setErrors({ ...errorsTransformed }); setErrors({ ...errorsTransformed });
setSubmitting(false); setSubmitting(false);
}; };
if (payload.action === 'edit') { if (payload.action === 'edit') {
requestEditAccount(payload.id, form) requestEditAccount(payload.id, form)
.then((response) => { .then((response) => {
@@ -166,6 +162,11 @@ function AccountFormDialog({
} }
}, [values.parent_account_id]); }, [values.parent_account_id]);
// Reset `parent account id` after change `account type`.
useEffect(() => {
setFieldValue('parent_account_id', null);
}, [values.account_type_id]);
// Filtered accounts based on the given account type. // Filtered accounts based on the given account type.
const filteredAccounts = useMemo( const filteredAccounts = useMemo(
() => () =>
@@ -379,12 +380,13 @@ function AccountFormDialog({
<div className={Classes.DIALOG_FOOTER}> <div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}> <div className={Classes.DIALOG_FOOTER_ACTIONS}>
<Button onClick={handleClose}> <Button onClick={handleClose} style={{ minWidth: '75px' }}>
<T id={'close'} /> <T id={'close'} />
</Button> </Button>
<Button <Button
intent={Intent.PRIMARY} intent={Intent.PRIMARY}
disabled={isSubmitting} disabled={isSubmitting}
style={{ minWidth: '75px' }}
type="submit" type="submit"
> >
{payload.action === 'edit' ? ( {payload.action === 'edit' ? (

View File

@@ -113,7 +113,7 @@ function ExpenseFormHeader({
// onItemSelect={} // onItemSelect={}
selectedItem={values.beneficiary} selectedItem={values.beneficiary}
// selectedItemProp={'id'} // selectedItemProp={'id'}
defaultText={<T id={'select_beneficiary_account'} />} defaultText={<T id={'select_customer'} />}
labelProp={'beneficiary'} labelProp={'beneficiary'}
/> />
</FormGroup> </FormGroup>

View File

@@ -1,21 +1,16 @@
import React, { useMemo, useCallback, useEffect } from 'react'; import React, { useCallback, useEffect } from 'react';
import FinancialStatementHeader from 'containers/FinancialStatements/FinancialStatementHeader'; import FinancialStatementHeader from 'containers/FinancialStatements/FinancialStatementHeader';
import { Row, Col, Visible } from 'react-grid-system'; import { Row, Col, Visible } from 'react-grid-system';
import { import { FormGroup } from '@blueprintjs/core';
Button,
FormGroup,
MenuItem,
} from "@blueprintjs/core";
import moment from 'moment'; import moment from 'moment';
import * as Yup from 'yup'; import * as Yup from 'yup';
import { useFormik } from 'formik'; import { useFormik } from 'formik';
import { FormattedMessage as T, useIntl } from 'react-intl'; import { FormattedMessage as T, useIntl } from 'react-intl';
import Icon from 'components/Icon';
import SelectList from 'components/SelectList';
import FinancialStatementDateRange from 'containers/FinancialStatements/FinancialStatementDateRange'; import FinancialStatementDateRange from 'containers/FinancialStatements/FinancialStatementDateRange';
import SelectDisplayColumnsBy from '../SelectDisplayColumnsBy'; import SelectDisplayColumnsBy from '../SelectDisplayColumnsBy';
import RadiosAccountingBasis from '../RadiosAccountingBasis'; import RadiosAccountingBasis from '../RadiosAccountingBasis';
import FinancialAccountsFilter from '../FinancialAccountsFilter';
import withBalanceSheet from './withBalanceSheetDetail'; import withBalanceSheet from './withBalanceSheetDetail';
import withBalanceSheetActions from './withBalanceSheetActions'; import withBalanceSheetActions from './withBalanceSheetActions';
@@ -29,7 +24,7 @@ function BalanceSheetHeader({
refresh, refresh,
// #withBalanceSheetActions // #withBalanceSheetActions
refreshBalanceSheet refreshBalanceSheet,
}) { }) {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
@@ -40,10 +35,17 @@ function BalanceSheetHeader({
basis: 'cash', basis: 'cash',
from_date: moment(pageFilter.from_date).toDate(), from_date: moment(pageFilter.from_date).toDate(),
to_date: moment(pageFilter.to_date).toDate(), to_date: moment(pageFilter.to_date).toDate(),
none_zero: false,
}, },
validationSchema: Yup.object().shape({ validationSchema: Yup.object().shape({
from_date: Yup.date().required().label(formatMessage({id:'from_data'})), from_date: Yup.date()
to_date: Yup.date().min(Yup.ref('from_date')).required().label(formatMessage({id:'to_date'})), .required()
.label(formatMessage({ id: 'from_data' })),
to_date: Yup.date()
.min(Yup.ref('from_date'))
.required()
.label(formatMessage({ id: 'to_date' })),
none_zero: Yup.boolean(),
}), }),
onSubmit: (values, actions) => { onSubmit: (values, actions) => {
onSubmitFilter(values); onSubmitFilter(values);
@@ -52,25 +54,21 @@ function BalanceSheetHeader({
}); });
// Handle item select of `display columns by` field. // Handle item select of `display columns by` field.
const onItemSelectDisplayColumns = useCallback((item) => { const onItemSelectDisplayColumns = useCallback(
(item) => {
formik.setFieldValue('display_columns_type', item.type); formik.setFieldValue('display_columns_type', item.type);
formik.setFieldValue('display_columns_by', item.by); formik.setFieldValue('display_columns_by', item.by);
}, [formik]); },
[formik],
);
const filterAccountsOptions = useMemo(() => [ const handleAccountingBasisChange = useCallback(
{ key: '', name: formatMessage({ id: 'accounts_with_zero_balance' }) }, (value) => {
{ key: 'all-trans', name: formatMessage({ id: 'all_transactions' }) },
], [formatMessage]);
const filterAccountRenderer = useCallback((item, { handleClick, modifiers, query }) => {
return (<MenuItem text={item.name} key={item.id} onClick={handleClick} />);
}, []);
const handleAccountingBasisChange = useCallback((value) => {
formik.setFieldValue('basis', value); formik.setFieldValue('basis', value);
}, [formik]); },
[formik],
);
// Handle submit filter submit button.
useEffect(() => { useEffect(() => {
if (refresh) { if (refresh) {
formik.submitForm(); formik.submitForm();
@@ -78,41 +76,46 @@ function BalanceSheetHeader({
} }
}, [refresh]); }, [refresh]);
const handleAccountsFilterSelect = (filterType) => {
const noneZero = filterType.key === 'without-zero-balance' ? true : false;
formik.setFieldValue('none_zero', noneZero);
};
return ( return (
<FinancialStatementHeader show={show}> <FinancialStatementHeader show={show}>
<Row> <Row>
<FinancialStatementDateRange formik={formik} /> <FinancialStatementDateRange formik={formik} />
<Visible xl><Col width={'100%'} /></Visible> <Visible xl>
<Col width={'100%'} />
</Visible>
<Col width={260} offset={10}> <Col width={260} offset={10}>
<SelectDisplayColumnsBy <SelectDisplayColumnsBy onItemSelect={onItemSelectDisplayColumns} />
onItemSelect={onItemSelectDisplayColumns} />
</Col> </Col>
<Col width={260}> <Col width={260}>
<FormGroup <FormGroup
label={<T id={'filter_accounts'} />} label={<T id={'filter_accounts'} />}
className="form-group--select-list bp3-fill" className="form-group--select-list bp3-fill"
inline={false}> inline={false}
>
<SelectList <FinancialAccountsFilter
items={filterAccountsOptions} initialSelectedItem={'all-accounts'}
itemRenderer={filterAccountRenderer} onItemSelect={handleAccountsFilterSelect}
onItemSelect={onItemSelectDisplayColumns} />
popoverProps={{ minimal: true }}
filterable={false} />
</FormGroup> </FormGroup>
</Col> </Col>
<Col width={260}> <Col width={260}>
<RadiosAccountingBasis <RadiosAccountingBasis
selectedValue={formik.values.basis} selectedValue={formik.values.basis}
onChange={handleAccountingBasisChange} /> onChange={handleAccountingBasisChange}
/>
</Col> </Col>
</Row> </Row>
</FinancialStatementHeader> </FinancialStatementHeader>
) );
} }
export default compose( export default compose(

View File

@@ -1,6 +1,7 @@
import React, { useMemo, useEffect, useState, useCallback } from 'react'; import React, { useMemo, useCallback } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import classNames from 'classnames';
import Money from 'components/Money'; import Money from 'components/Money';
import FinancialSheet from 'components/FinancialSheet'; import FinancialSheet from 'components/FinancialSheet';
@@ -18,6 +19,7 @@ function BalanceSheetTable({
// #withBalanceSheetDetail // #withBalanceSheetDetail
balanceSheetAccounts, balanceSheetAccounts,
balanceSheetTableRows,
balanceSheetColumns, balanceSheetColumns,
balanceSheetQuery, balanceSheetQuery,
balanceSheetLoading, balanceSheetLoading,
@@ -33,13 +35,13 @@ function BalanceSheetTable({
Header: formatMessage({ id: 'account_name' }), Header: formatMessage({ id: 'account_name' }),
accessor: 'name', accessor: 'name',
className: 'account_name', className: 'account_name',
width: 100, width: 120,
}, },
{ {
Header: formatMessage({ id: 'account_code' }), Header: formatMessage({ id: 'code' }),
accessor: 'code', accessor: 'code',
className: 'code', className: 'code',
width: 80, width: 60,
}, },
...(balanceSheetQuery.display_columns_type === 'total' ...(balanceSheetQuery.display_columns_type === 'total'
? [ ? [
@@ -67,13 +69,16 @@ function BalanceSheetTable({
? balanceSheetColumns.map((column, index) => ({ ? balanceSheetColumns.map((column, index) => ({
id: `date_period_${index}`, id: `date_period_${index}`,
Header: column, Header: column,
accessor: (row) => { accessor: `total_periods[${index}]`,
if (row.total_periods && row.total_periods[index]) { Cell: ({ cell }) => {
const amount = row.total_periods[index].formatted_amount; const { original } = cell.row;
if (original.total_periods && original.total_periods[index]) {
const amount = original.total_periods[index].formatted_amount;
return <Money amount={amount} currency={'USD'} />; return <Money amount={amount} currency={'USD'} />;
} }
return ''; return '';
}, },
className: classNames('total-period', `total-periods-${index}`),
width: 80, width: 80,
})) }))
: []), : []),
@@ -87,10 +92,18 @@ function BalanceSheetTable({
// Calculates the default expanded rows of balance sheet table. // Calculates the default expanded rows of balance sheet table.
const expandedRows = useMemo( const expandedRows = useMemo(
() => defaultExpanderReducer(balanceSheetAccounts, 1), () => defaultExpanderReducer(balanceSheetTableRows, 3),
[balanceSheetAccounts], [balanceSheetTableRows],
); );
const rowClassNames = (row) => {
const { original } = row;
console.log(row);
return {
[`row_type--${original.row_type}`]: original.row_type,
};
};
return ( return (
<FinancialSheet <FinancialSheet
name="balance-sheet" name="balance-sheet"
@@ -104,12 +117,15 @@ function BalanceSheetTable({
<DataTable <DataTable
className="bigcapital-datatable--financial-report" className="bigcapital-datatable--financial-report"
columns={columns} columns={columns}
data={balanceSheetAccounts} data={balanceSheetTableRows}
rowClassNames={rowClassNames}
onFetchData={handleFetchData} onFetchData={handleFetchData}
noInitialFetch={true} noInitialFetch={true}
expanded={expandedRows} expanded={expandedRows}
expandSubRows={true} expandable={true}
expandToggleColumn={1}
sticky={true} sticky={true}
expandColumnSpace={0.8}
/> />
</FinancialSheet> </FinancialSheet>
); );
@@ -132,11 +148,13 @@ export default compose(
withBalanceSheetDetail( withBalanceSheetDetail(
({ ({
balanceSheetAccounts, balanceSheetAccounts,
balanceSheetTableRows,
balanceSheetColumns, balanceSheetColumns,
balanceSheetQuery, balanceSheetQuery,
balanceSheetLoading, balanceSheetLoading,
}) => ({ }) => ({
balanceSheetAccounts, balanceSheetAccounts,
balanceSheetTableRows,
balanceSheetColumns, balanceSheetColumns,
balanceSheetQuery, balanceSheetQuery,
balanceSheetLoading, balanceSheetLoading,

View File

@@ -4,6 +4,7 @@ import {
getFinancialSheetAccounts, getFinancialSheetAccounts,
getFinancialSheetColumns, getFinancialSheetColumns,
getFinancialSheetQuery, getFinancialSheetQuery,
getFinancialSheetTableRows,
} from 'store/financialStatement/financialStatements.selectors'; } from 'store/financialStatement/financialStatements.selectors';
@@ -13,6 +14,7 @@ export default (mapState) => {
const mapped = { const mapped = {
balanceSheet: getFinancialSheet(state.financialStatements.balanceSheet.sheets, balanceSheetIndex), balanceSheet: getFinancialSheet(state.financialStatements.balanceSheet.sheets, balanceSheetIndex),
balanceSheetAccounts: getFinancialSheetAccounts(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), balanceSheetColumns: getFinancialSheetColumns(state.financialStatements.balanceSheet.sheets, balanceSheetIndex),
balanceSheetQuery: getFinancialSheetQuery(state.financialStatements.balanceSheet.sheets, balanceSheetIndex), balanceSheetQuery: getFinancialSheetQuery(state.financialStatements.balanceSheet.sheets, balanceSheetIndex),
balanceSheetLoading: state.financialStatements.balanceSheet.loading, balanceSheetLoading: state.financialStatements.balanceSheet.loading,

View File

@@ -0,0 +1,73 @@
import React, { useMemo, useCallback } from 'react';
import {
PopoverInteractionKind,
Tooltip,
MenuItem,
Position,
} from '@blueprintjs/core';
import { useIntl } from 'react-intl';
import { ListSelect, MODIFIER } from 'components';
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],
);
const SUBMENU_POPOVER_MODIFIERS = {
flip: { boundariesElement: 'viewport', padding: 20 },
offset: { offset: '0, 10' },
preventOverflow: { boundariesElement: 'viewport', padding: 40 },
};
const filterAccountRenderer = useCallback(
(item, { handleClick, modifiers, query }) => {
return (
<Tooltip
interactionKind={PopoverInteractionKind.HOVER}
position={Position.RIGHT_TOP}
content={item.hint}
modifiers={SUBMENU_POPOVER_MODIFIERS}
inline={true}
minimal={true}
className={MODIFIER.SELECT_LIST_TOOLTIP_ITEMS}
>
<MenuItem text={item.name} key={item.key} onClick={handleClick} />
</Tooltip>
);
},
[],
);
return (
<ListSelect
items={filterAccountsOptions}
itemRenderer={filterAccountRenderer}
popoverProps={{ minimal: true, }}
filterable={false}
selectedItemProp={'key'}
labelProp={'name'}
// className={}
{...restProps}
/>
);
}

View File

@@ -64,7 +64,6 @@ function GeneralLedger({
to_date: moment(filter.to_date).format('YYYY-MM-DD'), to_date: moment(filter.to_date).format('YYYY-MM-DD'),
}; };
setFilter(parsedFilter); setFilter(parsedFilter);
fetchSheet.refetch({ force: true });
}, [setFilter]); }, [setFilter]);
const handleFilterChanged = () => { }; const handleFilterChanged = () => { };

View File

@@ -52,8 +52,8 @@ function GeneralLedgerHeader({
}); });
const onAccountSelected = useCallback((selectedAccounts) => { const onAccountSelected = useCallback((selectedAccounts) => {
formik.setFieldValue('accounts_ids', Object.keys(selectedAccounts));
}, []); }, [formik.setFieldValue]);
const handleAccountingBasisChange = useCallback( const handleAccountingBasisChange = useCallback(
(value) => { (value) => {

View File

@@ -53,7 +53,7 @@ function GeneralLedgerTable({
]; ];
return TYPES.indexOf(row.rowType) !== -1 return TYPES.indexOf(row.rowType) !== -1
? moment(row.date).format('DD-MM-YYYY') ? moment(row.date).format('DD MMM YYYY')
: ''; : '';
}, },
[moment, ROW_TYPE], [moment, ROW_TYPE],
@@ -83,36 +83,43 @@ function GeneralLedgerTable({
Header: formatMessage({ id: 'account_name' }), Header: formatMessage({ id: 'account_name' }),
accessor: accountNameAccessor, accessor: accountNameAccessor,
className: 'name', className: 'name',
width: 225,
}, },
{ {
Header: formatMessage({ id: 'date' }), Header: formatMessage({ id: 'date' }),
accessor: dateAccessor, accessor: dateAccessor,
className: 'date', className: 'date',
width: 115,
}, },
{ {
Header: formatMessage({ id: 'transaction_type' }), Header: formatMessage({ id: 'transaction_type' }),
accessor: 'referenceType', accessor: 'referenceType',
className: 'transaction_type', className: 'transaction_type',
width: 145,
}, },
{ {
Header: formatMessage({ id: 'trans_num' }), Header: formatMessage({ id: 'trans_num' }),
accessor: referenceLink, accessor: referenceLink,
className: 'transaction_number', className: 'transaction_number',
width: 110,
}, },
{ {
Header: formatMessage({ id: 'description' }), Header: formatMessage({ id: 'description' }),
accessor: 'note', accessor: 'note',
className: 'description', className: 'description',
width: 145,
}, },
{ {
Header: formatMessage({ id: 'amount' }), Header: formatMessage({ id: 'amount' }),
Cell: amountCell, Cell: amountCell,
className: 'amount', className: 'amount',
width: 150,
}, },
{ {
Header: formatMessage({ id: 'balance' }), Header: formatMessage({ id: 'balance' }),
Cell: amountCell, Cell: amountCell,
className: 'balance', className: 'balance',
width: 150,
}, },
], ],
[], [],
@@ -133,11 +140,13 @@ function GeneralLedgerTable({
return ( return (
<FinancialSheet <FinancialSheet
companyName={companyName} companyName={companyName}
sheetType={formatMessage({ id: 'general_ledger_sheet' })} // sheetType={formatMessage({ id: 'general_ledger_sheet' })}
fromDate={generalLedgerQuery.from_date} fromDate={generalLedgerQuery.from_date}
toDate={generalLedgerQuery.to_date} toDate={generalLedgerQuery.to_date}
name="general-ledger" name="general-ledger"
loading={generalLedgerSheetLoading} loading={generalLedgerSheetLoading}
minimal={true}
fullWidth={true}
> >
<DataTable <DataTable
className="bigcapital-datatable--financial-report" className="bigcapital-datatable--financial-report"

View File

@@ -37,7 +37,7 @@ function JournalSheetTable({
{ {
Header: formatMessage({ id: 'date' }), Header: formatMessage({ id: 'date' }),
accessor: (r) => accessor: (r) =>
rowTypeFilter(r.rowType, moment(r.date).format('YYYY/MM/DD'), [ rowTypeFilter(r.rowType, moment(r.date).format('YYYY MMM DD'), [
'first_entry', 'first_entry',
]), ]),
className: 'date', className: 'date',
@@ -64,7 +64,7 @@ function JournalSheetTable({
{ {
Header: formatMessage({ id: 'acc_code' }), Header: formatMessage({ id: 'acc_code' }),
accessor: 'account.code', accessor: 'account.code',
width: 120, width: 95,
className: 'account_code', className: 'account_code',
}, },
{ {
@@ -106,11 +106,13 @@ function JournalSheetTable({
return ( return (
<FinancialSheet <FinancialSheet
companyName={companyName} companyName={companyName}
sheetType={formatMessage({ id: 'journal_sheet' })} // sheetType={formatMessage({ id: 'journal_sheet' })}
fromDate={journalSheetQuery.from_date} fromDate={journalSheetQuery.from_date}
toDate={journalSheetQuery.to_date} toDate={journalSheetQuery.to_date}
name="journal" name="journal"
loading={journalSheetLoading} loading={journalSheetLoading}
minimal={true}
fullWidth={true}
> >
<DataTable <DataTable
className="bigcapital-datatable--financial-report" className="bigcapital-datatable--financial-report"

View File

@@ -1,14 +1,16 @@
import React, { useCallback, useEffect } from 'react'; import React, { useCallback, useEffect } from 'react';
import { Row, Col, Visible } from 'react-grid-system'; import { Row, Col, Visible } from 'react-grid-system';
import { Button } from '@blueprintjs/core';
import moment from 'moment'; import moment from 'moment';
import { useFormik } from 'formik'; import { useFormik } from 'formik';
import { FormattedMessage as T, useIntl } from 'react-intl'; import { FormattedMessage as T, useIntl } from 'react-intl';
import { FormGroup } from '@blueprintjs/core';
import * as Yup from 'yup'; import * as Yup from 'yup';
import FinancialStatementDateRange from 'containers/FinancialStatements/FinancialStatementDateRange'; import FinancialStatementDateRange from 'containers/FinancialStatements/FinancialStatementDateRange';
import FinancialStatementHeader from 'containers/FinancialStatements/FinancialStatementHeader'; import FinancialStatementHeader from 'containers/FinancialStatements/FinancialStatementHeader';
import SelectsListColumnsBy from '../SelectDisplayColumnsBy'; import SelectsListColumnsBy from '../SelectDisplayColumnsBy';
import RadiosAccountingBasis from '../RadiosAccountingBasis'; import RadiosAccountingBasis from '../RadiosAccountingBasis';
import FinancialAccountsFilter from '../FinancialAccountsFilter';
import withProfitLoss from './withProfitLoss'; import withProfitLoss from './withProfitLoss';
import withProfitLossActions from './withProfitLossActions'; import withProfitLossActions from './withProfitLossActions';
@@ -73,6 +75,11 @@ function ProfitLossHeader({
} }
}, [profitLossSheetRefresh]); }, [profitLossSheetRefresh]);
const handleAccountsFilterSelect = (filterType) => {
const noneZero = filterType.key === 'without-zero-balance' ? true : false;
formik.setFieldValue('none_zero', noneZero);
};
return ( return (
<FinancialStatementHeader show={profitLossSheetFilter}> <FinancialStatementHeader show={profitLossSheetFilter}>
<Row> <Row>
@@ -83,6 +90,19 @@ function ProfitLossHeader({
<SelectsListColumnsBy onItemSelect={handleItemSelectDisplayColumns} /> <SelectsListColumnsBy onItemSelect={handleItemSelectDisplayColumns} />
</Col> </Col>
<Col width={260}>
<FormGroup
label={<T id={'filter_accounts'} />}
className="form-group--select-list bp3-fill"
inline={false}
>
<FinancialAccountsFilter
initialSelectedItem={'all-accounts'}
onItemSelect={handleAccountsFilterSelect}
/>
</FormGroup>
</Col>
<Col width={260}> <Col width={260}>
<RadiosAccountingBasis <RadiosAccountingBasis
selectedValue={formik.values.basis} selectedValue={formik.values.basis}

View File

@@ -1,19 +1,21 @@
import React, { useEffect } from 'react'; import React, { useEffect, useCallback } from 'react';
import * as Yup from 'yup'; import * as Yup from 'yup';
import moment from 'moment'; import moment from 'moment';
import { Row, Col } from 'react-grid-system'; import { Row, Col, Visible } from 'react-grid-system';
import { FormattedMessage as T, useIntl } from 'react-intl'; import { FormattedMessage as T, useIntl } from 'react-intl';
import { FormGroup } from '@blueprintjs/core';
import { useFormik } from 'formik'; import { useFormik } from 'formik';
import { Button } from "@blueprintjs/core";
import FinancialStatementHeader from 'containers/FinancialStatements/FinancialStatementHeader'; import FinancialStatementHeader from 'containers/FinancialStatements/FinancialStatementHeader';
import FinancialStatementDateRange from 'containers/FinancialStatements/FinancialStatementDateRange'; import FinancialStatementDateRange from 'containers/FinancialStatements/FinancialStatementDateRange';
import RadiosAccountingBasis from '../RadiosAccountingBasis';
import FinancialAccountsFilter from '../FinancialAccountsFilter';
import withTrialBalance from './withTrialBalance'; import withTrialBalance from './withTrialBalance';
import withTrialBalanceActions from './withTrialBalanceActions'; import withTrialBalanceActions from './withTrialBalanceActions';
import { compose } from 'utils'; import { compose } from 'utils';
function TrialBalanceSheetHeader({ function TrialBalanceSheetHeader({
pageFilter, pageFilter,
onSubmitFilter, onSubmitFilter,
@@ -31,16 +33,21 @@ function TrialBalanceSheetHeader({
initialValues: { initialValues: {
...pageFilter, ...pageFilter,
from_date: moment(pageFilter.from_date).toDate(), from_date: moment(pageFilter.from_date).toDate(),
to_date: moment(pageFilter.to_date).toDate() to_date: moment(pageFilter.to_date).toDate(),
}, },
validationSchema: Yup.object().shape({ validationSchema: Yup.object().shape({
from_date: Yup.date().required().label(formatMessage({id:'from_date'})), from_date: Yup.date()
to_date: Yup.date().min(Yup.ref('from_date')).required().label(formatMessage({id:'to_date'})), .required()
.label(formatMessage({ id: 'from_date' })),
to_date: Yup.date()
.min(Yup.ref('from_date'))
.required()
.label(formatMessage({ id: 'to_date' })),
}), }),
onSubmit: (values, { setSubmitting }) => { onSubmit: (values, { setSubmitting }) => {
onSubmitFilter(values); onSubmitFilter(values);
setSubmitting(false); setSubmitting(false);
} },
}); });
useEffect(() => { useEffect(() => {
@@ -50,22 +57,55 @@ function TrialBalanceSheetHeader({
} }
}, [formik, trialBalanceSheetRefresh]); }, [formik, trialBalanceSheetRefresh]);
const handleAccountingBasisChange = useCallback(
(value) => {
formik.setFieldValue('basis', value);
},
[formik],
);
const handleAccountsFilterSelect = (filterType) => {
const noneZero = filterType.key === 'without-zero-balance' ? true : false;
formik.setFieldValue('none_zero', noneZero);
};
return ( return (
<FinancialStatementHeader show={trialBalanceSheetFilter}> <FinancialStatementHeader show={trialBalanceSheetFilter}>
<Row> <Row>
<FinancialStatementDateRange formik={formik} /> <FinancialStatementDateRange formik={formik} />
<Visible xl>
<Col width={'100%'} />
</Visible>
<Col width={260}>
<FormGroup
label={<T id={'filter_accounts'} />}
className="form-group--select-list bp3-fill"
inline={false}
>
<FinancialAccountsFilter
initialSelectedItem={'all-accounts'}
onItemSelect={handleAccountsFilterSelect}
/>
</FormGroup>
</Col>
<Col width={260}>
<RadiosAccountingBasis
selectedValue={formik.values.basis}
onChange={handleAccountingBasisChange}
/>
</Col>
</Row> </Row>
</FinancialStatementHeader> </FinancialStatementHeader>
); );
} }
export default compose( export default compose(
withTrialBalance(({ withTrialBalance(({ trialBalanceSheetFilter, trialBalanceSheetRefresh }) => ({
trialBalanceSheetFilter, trialBalanceSheetFilter,
trialBalanceSheetRefresh, trialBalanceSheetRefresh,
}) => ({
trialBalanceSheetFilter,
trialBalanceSheetRefresh
})), })),
withTrialBalanceActions, withTrialBalanceActions,
)(TrialBalanceSheetHeader); )(TrialBalanceSheetHeader);

View File

@@ -45,7 +45,8 @@ function TrialBalanceSheetTable({
}, },
{ {
Header: formatMessage({ id: 'credit' }), Header: formatMessage({ id: 'credit' }),
accessor: (r) => <Money amount={r.credit} currency="USD" />, accessor: 'credit',
Cell: ({ cell }) => <Money amount={cell.row.original.credit} currency="USD" />,
className: 'credit', className: 'credit',
minWidth: 95, minWidth: 95,
maxWidth: 95, maxWidth: 95,
@@ -53,7 +54,8 @@ function TrialBalanceSheetTable({
}, },
{ {
Header: formatMessage({ id: 'debit' }), Header: formatMessage({ id: 'debit' }),
accessor: (r) => <Money amount={r.debit} currency="USD" />, accessor: 'debit',
Cell: ({ cell }) => <Money amount={cell.row.original.debit} currency="USD" />,
className: 'debit', className: 'debit',
minWidth: 95, minWidth: 95,
maxWidth: 95, maxWidth: 95,
@@ -61,7 +63,8 @@ function TrialBalanceSheetTable({
}, },
{ {
Header: formatMessage({ id: 'balance' }), Header: formatMessage({ id: 'balance' }),
accessor: (r) => <Money amount={r.balance} currency="USD" />, accessor: 'balance',
Cell: ({ cell }) => <Money amount={cell.row.original.balance} currency="USD" />,
className: 'balance', className: 'balance',
minWidth: 95, minWidth: 95,
maxWidth: 95, maxWidth: 95,
@@ -91,6 +94,7 @@ function TrialBalanceSheetTable({
onFetchData={handleFetchData} onFetchData={handleFetchData}
expandable={true} expandable={true}
expandToggleColumn={1} expandToggleColumn={1}
expandColumnSpace={1}
sticky={true} sticky={true}
/> />
</FinancialSheet> </FinancialSheet>

View File

@@ -1,12 +1,14 @@
import {connect} from 'react-redux'; import {connect} from 'react-redux';
import { import {
getResourceColumns, getResourceColumns,
getResourceFields, getResourceFieldsFactory,
getResourceMetadata, getResourceMetadata,
getResourceData, getResourceData,
} from 'store/resources/resources.reducer'; } from 'store/resources/resources.reducer';
export default (mapState) => { export default (mapState) => {
const getResourceFields = getResourceFieldsFactory();
const mapStateToProps = (state, props) => { const mapStateToProps = (state, props) => {
const { resourceName } = props; const { resourceName } = props;
const mapped = { const mapped = {

View File

@@ -441,7 +441,7 @@ export default {
once_delete_these_expenses_you_will_not_able_restore_them: once_delete_these_expenses_you_will_not_able_restore_them:
"Once you delete these expenses, you won't be able to retrieve them later. Are you sure you want to delete them?", "Once you delete these expenses, you won't be able to retrieve them later. Are you sure you want to delete them?",
the_expense_has_been_published: 'The expense has been published', the_expense_has_been_published: 'The expense has been published',
select_beneficiary_account: 'Select Beneficiary Account', select_customer: 'Select customer',
total_amount_equals_zero: 'Total amount equals zero', total_amount_equals_zero: 'Total amount equals zero',
value: 'Value', value: 'Value',
you_reached_conditions_limit: 'You have reached to conditions limit.', you_reached_conditions_limit: 'You have reached to conditions limit.',
@@ -458,7 +458,7 @@ export default {
display_name_: 'Display name', display_name_: 'Display name',
new_customer: 'New Customer', new_customer: 'New Customer',
customer_type: 'Customer Type', customer_type: 'Customer Type',
business: 'business', business: 'Business',
individual: 'Individual', individual: 'Individual',
display_name: 'Display Name', display_name: 'Display Name',
the_customer_has_been_successfully_created: the_customer_has_been_successfully_created:
@@ -543,4 +543,13 @@ export default {
"Once you delete these journalss, you won't be able to retrieve them later. Are you sure you want to delete them?", "Once you delete these journalss, you won't be able to retrieve them later. Are you sure you want to delete them?",
once_delete_this_journal_you_will_able_to_restore_it: `Once you delete this journal, you won\'t be able to restore it later. Are you sure you want to delete ?`, once_delete_this_journal_you_will_able_to_restore_it: `Once you delete this journal, you won\'t be able to restore it later. Are you sure you want to delete ?`,
the_expense_is_already_published: 'The expense is already published.', the_expense_is_already_published: 'The expense is already published.',
accounts_without_zero_balance: 'Accounts without zero-balance',
accounts_with_transactions: 'Accounts with transactions',
include_accounts_once_has_transactions_on_given_date_period: 'Include accounts that onces have transactions on the given date period only.',
include_accounts_and_exclude_zero_balance: 'Include accounts and exclude that ones have zero-balance.',
all_accounts_including_with_zero_balance: 'All accounts, including that ones have zero-balance.',
notifications: 'Notifications',
you_could_not_delete_account_has_child_accounts: 'You could not delete account has child accounts.',
journal_entry: 'Journal Entry'
}; };

View File

@@ -1,13 +1,12 @@
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { pickItemsFromIds, getItemById } from 'store/selectors'; import { pickItemsFromIds, getItemById } from 'store/selectors';
import { flatToNestedArray } from 'utils';
const accountsViewsSelector = (state) => state.accounts.views; const accountsViewsSelector = (state) => state.accounts.views;
const accountsDataSelector = (state) => state.accounts.items; const accountsDataSelector = (state) => state.accounts.items;
const accountsCurrentViewSelector = (state) => state.accounts.currentViewId; const accountsCurrentViewSelector = (state) => state.accounts.currentViewId;
const accountIdPropSelector = (state, props) => props.accountId; const accountIdPropSelector = (state, props) => props.accountId;
const accountsListSelector = (state) => state.accounts.list;
const accountsListSelector = state => state.accounts.list;
export const getAccountsItems = createSelector( export const getAccountsItems = createSelector(
accountsViewsSelector, accountsViewsSelector,
@@ -15,25 +14,31 @@ export const getAccountsItems = createSelector(
accountsCurrentViewSelector, accountsCurrentViewSelector,
(accountsViews, accountsItems, viewId) => { (accountsViews, accountsItems, viewId) => {
const accountsView = accountsViews[viewId || -1]; const accountsView = accountsViews[viewId || -1];
const config = { id: 'id', parentId: 'parent_account_id' };
return typeof accountsView === 'object' const accounts =
typeof accountsView === 'object'
? pickItemsFromIds(accountsItems, accountsView.ids) || [] ? pickItemsFromIds(accountsItems, accountsView.ids) || []
: []; : [];
return flatToNestedArray(
accounts.map((a) => ({ ...a })),
config,
);
}, },
); );
export const getAccountsListFactory = () => createSelector( export const getAccountsListFactory = () =>
createSelector(
accountsListSelector, accountsListSelector,
accountsDataSelector, accountsDataSelector,
(accounts, accountsItems) => { (accounts, accountsItems) => {
return pickItemsFromIds(accountsItems, accounts); return pickItemsFromIds(accountsItems, accounts);
}, },
) );
export const getAccountById = createSelector( export const getAccountById = createSelector(
accountsDataSelector, accountsDataSelector,
accountIdPropSelector, accountIdPropSelector,
(accountsItems, accountId) => { (accountsItems, accountId) => {
return getItemById(accountsItems, accountId); return getItemById(accountsItems, accountId);
} },
); );

View File

@@ -1,8 +1,6 @@
import { createReducer } from '@reduxjs/toolkit'; import { createReducer } from '@reduxjs/toolkit';
import t from 'store/types'; import t from 'store/types';
import { import { getFinancialSheetIndexByQuery } from './financialStatements.selectors';
getFinancialSheetIndexByQuery,
} from './financialStatements.selectors';
import { omit } from 'lodash'; import { omit } from 'lodash';
const initialState = { const initialState = {
@@ -93,8 +91,7 @@ const mapJournalTableRows = (journal) => {
}, []); }, []);
}; };
const mapContactAgingSummary = (sheet) => {
const mapContactAgingSummary = sheet => {
const rows = []; const rows = [];
const mapAging = (agingPeriods) => { const mapAging = (agingPeriods) => {
@@ -102,7 +99,7 @@ const mapContactAgingSummary = sheet => {
acc[`aging-${index}`] = aging.formatted_total; acc[`aging-${index}`] = aging.formatted_total;
return acc; return acc;
}, {}); }, {});
} };
sheet.customers.forEach((customer) => { sheet.customers.forEach((customer) => {
const agingRow = mapAging(customer.aging); const agingRow = mapAging(customer.aging);
@@ -157,6 +154,36 @@ const mapProfitLossToTableRows = (profitLoss) => {
]; ];
}; };
const mapTotalToChildrenRows = (accounts) => {
return accounts.map((account) => {
return {
...account,
children: mapTotalToChildrenRows([
...(account.children ? account.children : []),
...(account.total &&
account.children &&
account.children.length > 0 &&
account.row_type !== 'total_row'
? [
{
name: `Total ${account.name}`,
row_type: 'total_row',
total: { ...account.total },
...(account.total_periods && {
total_periods: account.total_periods,
}),
},
]
: []),
]),
};
});
};
const mapBalanceSheetRows = (balanceSheet) => {
return balanceSheet.map((section) => {});
};
const financialStatementFilterToggle = (financialName, statePath) => { const financialStatementFilterToggle = (financialName, statePath) => {
return { return {
[`${financialName}_FILTER_TOGGLE`]: (state, action) => { [`${financialName}_FILTER_TOGGLE`]: (state, action) => {
@@ -173,9 +200,10 @@ export default createReducer(initialState, {
); );
const balanceSheet = { const balanceSheet = {
accounts: action.data.accounts, sheet: action.data.balanceSheet,
columns: Object.values(action.data.columns), columns: Object.values(action.data.columns),
query: action.data.query, query: action.data.query,
tableRows: mapTotalToChildrenRows(action.data.balance_sheet),
}; };
if (index !== -1) { if (index !== -1) {
state.balanceSheet.sheets[index] = balanceSheet; state.balanceSheet.sheets[index] = balanceSheet;
@@ -313,13 +341,16 @@ export default createReducer(initialState, {
[t.RECEIVABLE_AGING_SUMMARY_SET]: (state, action) => { [t.RECEIVABLE_AGING_SUMMARY_SET]: (state, action) => {
const { aging, columns, query } = action.payload; const { aging, columns, query } = action.payload;
const index = getFinancialSheetIndexByQuery(state.receivableAgingSummary.sheets, query); const index = getFinancialSheetIndexByQuery(
state.receivableAgingSummary.sheets,
query,
);
const receivableSheet = { const receivableSheet = {
query, query,
columns, columns,
aging, aging,
tableRows: mapContactAgingSummary(aging) tableRows: mapContactAgingSummary(aging),
}; };
if (index !== -1) { if (index !== -1) {
state.receivableAgingSummary[index] = receivableSheet; state.receivableAgingSummary[index] = receivableSheet;
@@ -331,5 +362,8 @@ export default createReducer(initialState, {
const { refresh } = action.payload; const { refresh } = action.payload;
state.receivableAgingSummary.refresh = !!refresh; state.receivableAgingSummary.refresh = !!refresh;
}, },
...financialStatementFilterToggle('RECEIVABLE_AGING_SUMMARY', 'receivableAgingSummary'), ...financialStatementFilterToggle(
'RECEIVABLE_AGING_SUMMARY',
'receivableAgingSummary',
),
}); });

View File

@@ -72,7 +72,7 @@ const resourceFieldsItemsSelector = (state) => state.resources.fields;
* @param {String} resourceSlug * @param {String} resourceSlug
* @return {Array} * @return {Array}
*/ */
export const getResourceFields = createSelector( export const getResourceFieldsFactory = () => createSelector(
resourceFieldsIdsSelector, resourceFieldsIdsSelector,
resourceFieldsItemsSelector, resourceFieldsItemsSelector,
(fieldsIds, fieldsItems) => { (fieldsIds, fieldsItems) => {

View File

@@ -22,10 +22,10 @@
overflow-x: hidden; overflow-x: hidden;
.th{ .th{
padding: 0.75rem 0.5rem; padding: 0.6rem 0.5rem;
background: #fafafa; background: #fafafa;
font-size: 14px; font-size: 14px;
color: rgb(59, 71, 91); color: #445165;
font-weight: 500; font-weight: 500;
border-bottom: 1px solid rgb(224, 224, 224); border-bottom: 1px solid rgb(224, 224, 224);
} }
@@ -156,13 +156,17 @@
border: 0; border: 0;
box-shadow: none; box-shadow: none;
padding: 5px 15px; padding: 5px 15px;
border-radius: 5px; border-radius: 8px;
&:hover, &:hover,
&:focus{ &:focus{
background-color: #CFDCEE; background-color: #CFDCEE;
} }
svg{
color: #425361
}
.bp3-icon-more-h-16{ .bp3-icon-more-h-16{
margin-top: 2px; margin-top: 2px;
} }
@@ -289,7 +293,7 @@
.tbody{ .tbody{
.tr .td{ .tr .td{
border-bottom: 1px dotted #BBB; border-bottom: 1px dotted #CCC;
} }
} }
} }

View File

@@ -141,6 +141,10 @@ $form-check-input-indeterminate-bg-image: url("data:image/svg+xml,<svg xmlns='ht
} }
} }
.select-list--tooltip-items .bp3-popover-target {
display: block;
}
@mixin control-checked-colors($selector: ":checked") { @mixin control-checked-colors($selector: ":checked") {
input#{$selector} ~ .#{$ns}-control-indicator { input#{$selector} ~ .#{$ns}-control-indicator {
@@ -333,3 +337,8 @@ $form-check-input-indeterminate-bg-image: url("data:image/svg+xml,<svg xmlns='ht
} }
} }
} }
.bp3-menu-item::before, .bp3-menu-item > .bp3-icon{
color: #4b5d6b;
}

View File

@@ -62,7 +62,7 @@
.dialog--account-form{ .dialog--account-form{
&:not(.dialog--loading) .bp3-dialog-body{ &:not(.dialog--loading) .bp3-dialog-body{
margin-bottom: 25px; margin-bottom: 30px;
} }
.bp3-dialog-body{ .bp3-dialog-body{
@@ -91,9 +91,6 @@
} }
} }
.form-group--parent-account{
margin-bottom: 35px;
}
.form-group--account-code{ .form-group--account-code{
margin-bottom: 16px; margin-bottom: 16px;
} }

View File

@@ -15,7 +15,7 @@
&__topbar{ &__topbar{
width: 100%; width: 100%;
min-height: 66px; min-height: 62px;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
border-bottom: 1px solid #F2EFEF; border-bottom: 1px solid #F2EFEF;
@@ -137,17 +137,17 @@
margin-right: 0; margin-right: 0;
} }
.#{$ns}-button{ .#{$ns}-button{
color: #4d4d4d; color: #32304a;
padding: 8px 12px; padding: 8px 12px;
&:hover{ &:hover{
background: rgba(167, 182, 194, 0.12); background: rgba(167, 182, 194, 0.12);
color: #4d4d4d; color: #32304a;
} }
&.bp3-minimal:active, &.bp3-minimal:active,
&.bp3-minimal.bp3-active{ &.bp3-minimal.bp3-active{
background: rgba(167, 182, 194, 0.12); background: rgba(167, 182, 194, 0.12);
color: #4d4d4d; color: #32304a;
} }
&.has-active-filters{ &.has-active-filters{
@@ -158,7 +158,7 @@
} }
} }
.#{$ns}-icon{ .#{$ns}-icon{
color: #4d4d4d; color: #2A293D;
margin-right: 7px; margin-right: 7px;
} }
&.#{$ns}-minimal.#{$ns}-intent-danger{ &.#{$ns}-minimal.#{$ns}-intent-danger{
@@ -207,12 +207,12 @@
&__title{ &__title{
align-items: center;; align-items: center;;
display: flex; display: flex;
margin-left: 4px; margin-left: 2px;
h1{ h1{
font-size: 26px; font-size: 26px;
font-weight: 300; font-weight: 300;
color: #050035; color: #2c2c39;
margin: 0; margin: 0;
} }
h3{ h3{
@@ -278,7 +278,7 @@
.tbody{ .tbody{
.th.selection, .th.selection,
.td.selection{ .td.selection{
padding-left: 14px; padding-left: 16px;
} }
} }
} }
@@ -350,7 +350,7 @@
.#{$ns}-tab-indicator-wrapper{ .#{$ns}-tab-indicator-wrapper{
.#{$ns}-tab-indicator{ .#{$ns}-tab-indicator{
height: 2px; height: 3px;
} }
} }

View File

@@ -35,15 +35,14 @@
} }
&__body{ &__body{
padding-left: 20px; padding-left: 15px;
padding-right: 20px; padding-right: 15px;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
} }
&__header.is-hidden + .financial-statement__body{ &__header.is-hidden + .financial-statement__body{
.financial-sheet{ .financial-sheet{
margin-top: 40px; margin-top: 40px;
} }
@@ -55,7 +54,7 @@
border-radius: 10px; border-radius: 10px;
min-width: 640px; min-width: 640px;
width: auto; width: auto;
padding: 30px 20px; padding: 30px 18px;
max-width: 100%; max-width: 100%;
margin: 15px auto 35px; margin: 15px auto 35px;
min-height: 400px; min-height: 400px;
@@ -65,7 +64,7 @@
&__title{ &__title{
margin: 0; margin: 0;
font-weight: 400; font-weight: 400;
font-size: 22px; font-size: 20px;
color: #464646; color: #464646;
text-align: center; text-align: center;
} }
@@ -93,6 +92,7 @@
background: #fff; background: #fff;
} }
} }
.tr.no-results{ .tr.no-results{
.td{ .td{
flex-direction: column; flex-direction: column;
@@ -117,7 +117,8 @@
font-size: 12px; font-size: 12px;
} }
.dashboard__loading-indicator{ .dashboard__loading-indicator{
margin: 60px auto 0; margin: auto;
padding: 0;
} }
&--expended{ &--expended{
@@ -141,8 +142,10 @@
&--opening_balance, &--opening_balance,
&--closing_balance{ &--closing_balance{
.td{
background-color: #fbfbfb; background-color: #fbfbfb;
} }
}
&--closing_balance .td{ &--closing_balance .td{
border-bottom-color: #666; border-bottom-color: #666;
@@ -158,28 +161,22 @@
&--general-ledger, &--general-ledger,
&--journal{ &--journal{
width: auto;
margin-left: 10px;
margin-right: 10px;
margin-top: 10px;
border-color: #EEEDED;
} }
&--journal{ &--journal{
.financial-sheet__table{ .financial-sheet__table{
.tbody{ .tbody{
.tr:not(.no-results) .td{ .tr:not(.no-results) .td{
padding: 0.4rem; padding: 0.4rem;
color: #444; color: #000;
border-bottom-color: #F0F0F0; border-bottom-color: #DDD;
min-height: 32px; min-height: 32px;
border-left: 1px solid #F0F0F0; border-left: 1px dotted #DDD;
&:first-of-type{ &:first-of-type{
border-left: 0; border-left: 0;
} }
&.account_code{
color: #666;
}
} }
} }
} }
@@ -189,9 +186,6 @@
.financial-sheet__table{ .financial-sheet__table{
.tbody{ .tbody{
.account_code.td{
color: #666;
}
.total.td { .total.td {
border-bottom-color: #000; border-bottom-color: #000;
} }
@@ -212,10 +206,25 @@
&--balance-sheet{ &--balance-sheet{
.financial-sheet__table{ .financial-sheet__table{
.tbody{ .tbody{
.total.td{ .total.td{
border-bottom-color: #000; border-bottom-color: #000;
} }
.tr.row_type--total_row{
.total.td,
.account_name.td{
font-weight: 600;
color: #333;
}
}
.tr.is-expanded{
.td.total,
.td.total-period{
> span{
display: none;
}
}
}
} }
} }
} }
@@ -236,6 +245,29 @@
} }
} }
} }
&--minimal{
border: 0;
padding: 0;
margin-top: 20px;
.financial-sheet{
&__title{
font-size: 18px;
color: #333;
}
&__title + .financial-sheet__date{
margin-top: 8px;
}
&__table{
margin-top: 20px;
}
}
}
&.is-full-width{
width: 100%;
}
} }
@@ -272,6 +304,5 @@
line-height: 1.55; line-height: 1.55;
margin-top: 12px; margin-top: 12px;
} }
} }
} }

View File

@@ -2,10 +2,11 @@ $sidebar-background: #01194e;
$sidebar-text-color: #fff; $sidebar-text-color: #fff;
$sidebar-width: 100%; $sidebar-width: 100%;
$sidebar-menu-item-color: rgba(255, 255, 255, 0.9); $sidebar-menu-item-color: rgba(255, 255, 255, 0.9);
$sidebar-menu-item-color-active: rgb(255, 255, 255);
$sidebar-popover-submenu-bg: rgb(1, 20, 62); $sidebar-popover-submenu-bg: rgb(1, 20, 62);
$sidebar-menu-label-color: rgba(255, 255, 255, 0.5); $sidebar-menu-label-color: rgba(255, 255, 255, 0.5);
$sidebar-submenu-item-color: rgba(255, 255, 255, 0.55); $sidebar-submenu-item-color: rgba(255, 255, 255, 0.6);
$sidebar-submenu-item-hover-color: rgba(255, 255, 255, 0.8); $sidebar-submenu-item-hover-color: rgba(255, 255, 255, 0.85);
$sidebar-logo-opacity: 0.55; $sidebar-logo-opacity: 0.55;
$sidebar-submenu-item-bg-color: #01287d; $sidebar-submenu-item-bg-color: #01287d;
@@ -71,7 +72,7 @@ $sidebar-submenu-item-bg-color: #01287d;
&:hover, &:hover,
&.bp3-active { &.bp3-active {
background: $sidebar-submenu-item-bg-color; background: $sidebar-submenu-item-bg-color;
color: $sidebar-menu-item-color; color: $sidebar-menu-item-color-active;
} }
&:focus, &:focus,
&:active { &:active {
@@ -105,9 +106,8 @@ $sidebar-submenu-item-bg-color: #01287d;
padding-bottom: 6px; padding-bottom: 6px;
padding-top: 6px; padding-top: 6px;
} }
.#{$ns}-menu-item { .#{$ns}-menu-item {
padding: 7px 16px 7px 18px; padding: 7px 16px;
font-size: 15px; font-size: 15px;
color: $sidebar-submenu-item-color; color: $sidebar-submenu-item-color;
@@ -116,7 +116,6 @@ $sidebar-submenu-item-bg-color: #01287d;
background: transparent; background: transparent;
color: $sidebar-submenu-item-hover-color; color: $sidebar-submenu-item-hover-color;
} }
&.bp3-active{ &.bp3-active{
font-weight: 500; font-weight: 500;
} }
@@ -173,7 +172,7 @@ $sidebar-submenu-item-bg-color: #01287d;
min-width: 50px; min-width: 50px;
&:hover{ &:hover{
min-width: 220px; min-width: 190px;
.sidebar__head-logo{ .sidebar__head-logo{

View File

@@ -219,3 +219,27 @@ export const repeatValue = (value, len) => {
} }
return arr; return arr;
}; };
export const flatToNestedArray = (
data,
config = { id: 'id', parentId: 'parent_id' }
) => {
const map = {};
const nestedArray = [];
data.forEach((item) => {
map[item[config.id]] = item;
map[item[config.id]].children = [];
});
data.forEach((item) => {
const parentItemId = item[config.parentId];
if (!item[config.parentId]) {
nestedArray.push(item);
}
if (parentItemId) {
map[parentItemId].children.push(item);
}
});
return nestedArray;
};

View File

@@ -1,4 +1,34 @@
export function roundTo(num, to = 2) { function roundTo(num, to = 2) {
return +(Math.round(num + "e+" + to) + "e-" + to); return +(Math.round(num + "e+" + to) + "e-" + to);
} }
const flatToNestedArray = (
data,
config = { id: 'id', parentId: 'parent_id' }
) => {
const map = {};
const nestedArray = [];
data.forEach((item) => {
map[item[config.id]] = { ...item };
map[item[config.id]].children = [];
});
data.forEach((item) => {
const parentItemId = item[config.parentId];
if (!item[config.parentId]) {
nestedArray.push(item);
}
if (parentItemId) {
map[parentItemId].children.push(item);
}
});
return nestedArray;
};
export default {
roundTo,
flatToNestedArray,
};

View File

@@ -57,6 +57,19 @@ function log(text) {
console.log(text); console.log(text);
} }
function getDeepValue(prop, obj) {
if (!Object.keys(obj).length) { return []; }
return Object.entries(obj).reduce((acc, [key, val]) => {
if (key === prop) {
acc.push(val);
} else {
acc.push(Array.isArray(val) ? val.map(getIds).flat() : getIds(val));
}
return acc.flat();
}, []);
}
module.exports = { module.exports = {
initTenantKnex, initTenantKnex,
initSystemKnex, initSystemKnex,
@@ -64,4 +77,5 @@ module.exports = {
exit, exit,
success, success,
log, log,
getDeepValue,
} }

View File

@@ -0,0 +1,62 @@
export default [
{
name: 'Assets',
section_type: 'assets',
type: 'section',
children: [
{
name: 'Current Asset',
type: 'section',
_accounts_types_related: ['current_asset'],
},
{
name: 'Fixed Asset',
type: 'section',
_accounts_types_related: ['fixed_asset'],
},
{
name: 'Other Asset',
type: 'section',
_accounts_types_related: ['other_asset'],
},
],
_forceShow: true,
},
{
name: 'Liabilities and Equity',
section_type: 'liabilities_equity',
type: 'section',
children: [
{
name: 'Liabilities',
section_type: 'liability',
type: 'section',
children: [
{
name: 'Current Liability',
type: 'section',
_accounts_types_related: ['current_liability'],
},
{
name: 'Long Term Liability',
type: 'section',
_accounts_types_related: ['long_term_liability'],
},
{
name: 'Other Liability',
type: 'section',
_accounts_types_related: ['other_liability'],
},
],
},
{
name: 'Equity',
section_type: 'equity',
type: 'section',
_accounts_types_related: ['equity'],
},
],
_forceShow: true,
},
];

View File

@@ -6,6 +6,7 @@ exports.up = (knex) => {
table.string('key'); table.string('key');
table.string('normal'); table.string('normal');
table.string('root_type'); table.string('root_type');
table.string('child_type');
table.boolean('balance_sheet'); table.boolean('balance_sheet');
table.boolean('income_sheet'); table.boolean('income_sheet');
}).raw('ALTER TABLE `ACCOUNT_TYPES` AUTO_INCREMENT = 1000').then(() => { }).raw('ALTER TABLE `ACCOUNT_TYPES` AUTO_INCREMENT = 1000').then(() => {

View File

@@ -11,6 +11,7 @@ exports.seed = (knex) => {
key: 'fixed_asset', key: 'fixed_asset',
normal: 'debit', normal: 'debit',
root_type: 'asset', root_type: 'asset',
child_type: 'fixed_asset',
balance_sheet: true, balance_sheet: true,
income_sheet: false, income_sheet: false,
}, },
@@ -20,6 +21,17 @@ exports.seed = (knex) => {
key: 'current_asset', key: 'current_asset',
normal: 'debit', normal: 'debit',
root_type: 'asset', root_type: 'asset',
child_type: 'current_asset',
balance_sheet: true,
income_sheet: false,
},
{
id: 14,
name: 'Other Asset',
key: 'other_asset',
normal: 'debit',
root_type: 'asset',
child_type: 'other_asset',
balance_sheet: true, balance_sheet: true,
income_sheet: false, income_sheet: false,
}, },
@@ -29,6 +41,7 @@ exports.seed = (knex) => {
key: 'long_term_liability', key: 'long_term_liability',
normal: 'credit', normal: 'credit',
root_type: 'liability', root_type: 'liability',
child_type: 'long_term_liability',
balance_sheet: false, balance_sheet: false,
income_sheet: true, income_sheet: true,
}, },
@@ -38,6 +51,17 @@ exports.seed = (knex) => {
key: 'current_liability', key: 'current_liability',
normal: 'credit', normal: 'credit',
root_type: 'liability', root_type: 'liability',
child_type: 'current_liability',
balance_sheet: false,
income_sheet: true,
},
{
id: 13,
name: 'Other Liability',
key: 'other_liability',
normal: 'credit',
root_type: 'liability',
child_type: 'other_liability',
balance_sheet: false, balance_sheet: false,
income_sheet: true, income_sheet: true,
}, },
@@ -47,6 +71,7 @@ exports.seed = (knex) => {
key: 'equity', key: 'equity',
normal: 'credit', normal: 'credit',
root_type: 'equity', root_type: 'equity',
child_type: 'equity',
balance_sheet: true, balance_sheet: true,
income_sheet: false, income_sheet: false,
}, },
@@ -56,6 +81,16 @@ exports.seed = (knex) => {
key: 'expense', key: 'expense',
normal: 'debit', normal: 'debit',
root_type: 'expense', root_type: 'expense',
child_type: 'expense',
balance_sheet: false,
income_sheet: true,
},
{
id: 10,
name: 'Other Expense',
key: 'other_expense',
normal: 'debit',
root_type: 'expense',
balance_sheet: false, balance_sheet: false,
income_sheet: true, income_sheet: true,
}, },
@@ -65,24 +100,47 @@ exports.seed = (knex) => {
key: 'income', key: 'income',
normal: 'credit', normal: 'credit',
root_type: 'income', root_type: 'income',
child_type: 'income',
balance_sheet: false, balance_sheet: false,
income_sheet: true, income_sheet: true,
}, },
{
id: 11,
name: 'Other Income',
key: 'other_income',
normal: 'credit',
root_type: 'income',
child_type: 'other_income',
balance_sheet: false,
income_sheet: true,
},
{
id: 12,
name: 'Cost of Goods Sold (COGS)',
key: 'cost_of_goods_sold',
normal: 'debit',
root_type: 'asset',
child_type: 'current_asset',
balance_sheet: true,
income_sheet: false,
},
{ {
id: 8, id: 8,
name: 'Accounts Receivable', name: 'Accounts Receivable (A/R)',
key: 'accounts_receivable', key: 'accounts_receivable',
normal: 'debit', normal: 'debit',
root_type: 'asset', root_type: 'asset',
child_type: 'current_asset',
balance_sheet: true, balance_sheet: true,
income_sheet: false, income_sheet: false,
}, },
{ {
id: 9, id: 9,
name: 'Accounts Payable', name: 'Accounts Payable (A/P)',
key: 'accounts_payable', key: 'accounts_payable',
normal: 'credit', normal: 'credit',
root_type: 'liability', root_type: 'liability',
child_type: 'current_liability',
balance_sheet: true, balance_sheet: true,
income_sheet: false, income_sheet: false,
}, },

View File

@@ -3,7 +3,6 @@ import { check, validationResult, param, query } from 'express-validator';
import { difference } from 'lodash'; import { difference } from 'lodash';
import asyncMiddleware from '@/http/middleware/asyncMiddleware'; import asyncMiddleware from '@/http/middleware/asyncMiddleware';
import JournalPoster from '@/services/Accounting/JournalPoster'; import JournalPoster from '@/services/Accounting/JournalPoster';
import NestedSet from '@/collection/NestedSet';
import { import {
mapViewRolesToConditionals, mapViewRolesToConditionals,
mapFilterRolesToDynamicFilter, mapFilterRolesToDynamicFilter,
@@ -97,9 +96,13 @@ export default {
newAccount: { newAccount: {
validation: [ validation: [
check('name').exists().isLength({ min: 3, max: 255 }).trim().escape(), check('name').exists().isLength({ min: 3, max: 255 }).trim().escape(),
check('code').optional().isLength({ min: 3, max: 6, }).trim().escape(), check('code').optional().isLength({ min: 3, max: 6 }).trim().escape(),
check('account_type_id').exists().isNumeric().toInt(), check('account_type_id').exists().isNumeric().toInt(),
check('description').optional().isLength({ max: 512 }).trim().escape(), check('description').optional().isLength({ max: 512 }).trim().escape(),
check('parent_account_id')
.optional({ nullable: true })
.isNumeric()
.toInt(),
], ],
async handler(req, res) { async handler(req, res) {
const validationErrors = validationResult(req); const validationErrors = validationResult(req);
@@ -125,7 +128,6 @@ export default {
foundAccountCodePromise, foundAccountCodePromise,
foundAccountTypePromise, foundAccountTypePromise,
]); ]);
if (foundAccountCodePromise && foundAccountCode.length > 0) { if (foundAccountCodePromise && foundAccountCode.length > 0) {
return res.boom.badRequest(null, { return res.boom.badRequest(null, {
errors: [{ type: 'NOT_UNIQUE_CODE', code: 100 }], errors: [{ type: 'NOT_UNIQUE_CODE', code: 100 }],
@@ -136,6 +138,24 @@ export default {
errors: [{ type: 'NOT_EXIST_ACCOUNT_TYPE', code: 200 }], errors: [{ type: 'NOT_EXIST_ACCOUNT_TYPE', code: 200 }],
}); });
} }
if (form.parent_account_id) {
const parentAccount = await Account.query()
.where('id', form.parent_account_id)
.first();
if (!parentAccount) {
return res.boom.badRequest(null, {
errors: [{ type: 'PARENT_ACCOUNT_NOT_FOUND', code: 300 }],
});
}
if (parentAccount.accountTypeId !== form.parent_account_id) {
return res.boom.badRequest(null, {
errors: [
{ type: 'PARENT.ACCOUNT.HAS.DIFFERENT.ACCOUNT.TYPE', code: 400 },
],
});
}
}
const insertedAccount = await Account.query().insertAndFetch({ ...form }); const insertedAccount = await Account.query().insertAndFetch({ ...form });
return res.status(200).send({ account: { ...insertedAccount } }); return res.status(200).send({ account: { ...insertedAccount } });
@@ -148,8 +168,8 @@ export default {
editAccount: { editAccount: {
validation: [ validation: [
param('id').exists().toInt(), param('id').exists().toInt(),
check('name').exists().isLength({ min: 3, max: 255, }).trim().escape(), check('name').exists().isLength({ min: 3, max: 255 }).trim().escape(),
check('code').optional().isLength({ min: 3, max: 6, }).trim().escape(), check('code').optional().isLength({ min: 3, max: 6 }).trim().escape(),
check('account_type_id').exists().isNumeric().toInt(), check('account_type_id').exists().isNumeric().toInt(),
check('description').optional().isLength({ max: 512 }).trim().escape(), check('description').optional().isLength({ max: 512 }).trim().escape(),
], ],
@@ -189,7 +209,26 @@ export default {
errorReasons.push({ type: 'NOT_UNIQUE_CODE', code: 200 }); errorReasons.push({ type: 'NOT_UNIQUE_CODE', code: 200 });
} }
} }
if (form.parent_account_id) {
const parentAccount = await Account.query()
.where('id', form.parent_account_id)
.whereNot('id', account.id)
.first();
if (!parentAccount) {
errorReasons.push({
type: 'PARENT_ACCOUNT_NOT_FOUND',
code: 300,
});
}
if (parentAccount.accountTypeId !== account.parentAccountId) {
return res.boom.badRequest(null, {
errors: [
{ type: 'PARENT.ACCOUNT.HAS.DIFFERENT.ACCOUNT.TYPE', code: 400 },
],
});
}
}
if (errorReasons.length > 0) { if (errorReasons.length > 0) {
return res.status(400).send({ errors: errorReasons }); return res.status(400).send({ errors: errorReasons });
} }
@@ -238,11 +277,22 @@ export default {
errors: [{ type: 'ACCOUNT.PREDEFINED', code: 200 }], errors: [{ type: 'ACCOUNT.PREDEFINED', code: 200 }],
}); });
} }
// Validate the account has no child accounts.
const childAccounts = await Account.query().where(
'parent_account_id',
account.id
);
if (childAccounts.length > 0) {
return res.boom.badRequest(null, {
errors: [{ type: 'ACCOUNT.HAS.CHILD.ACCOUNTS', code: 300 }],
});
}
const accountTransactions = await AccountTransaction.query().where( const accountTransactions = await AccountTransaction.query().where(
'account_id', 'account_id',
account.id account.id
); );
if (accountTransactions.length > 0) { if (accountTransactions.length > 0) {
return res.boom.badRequest(null, { return res.boom.badRequest(null, {
errors: [{ type: 'ACCOUNT.HAS.ASSOCIATED.TRANSACTIONS', code: 100 }], errors: [{ type: 'ACCOUNT.HAS.ASSOCIATED.TRANSACTIONS', code: 100 }],
@@ -279,8 +329,8 @@ export default {
}); });
} }
const filter = { const filter = {
display_type: 'flat',
account_types: [], account_types: [],
display_type: 'tree',
filter_roles: [], filter_roles: [],
sort_order: 'asc', sort_order: 'asc',
...req.query, ...req.query,
@@ -364,38 +414,18 @@ export default {
return res.status(400).send({ errors: errorReasons }); return res.status(400).send({ errors: errorReasons });
} }
const query = Account.query() const accounts = await Account.query().onBuild((builder) => {
// .remember()
.onBuild((builder) => {
builder.modify('filterAccountTypes', filter.account_types); builder.modify('filterAccountTypes', filter.account_types);
builder.withGraphFetched('type'); builder.withGraphFetched('type');
builder.withGraphFetched('balance'); builder.withGraphFetched('balance');
dynamicFilter.buildQuery()(builder); dynamicFilter.buildQuery()(builder);
// console.log(builder.toKnexQuery().toSQL());
})
.toKnexQuery()
.toSQL();
console.log(query);
const accounts = await Account.query()
// .remember()
.onBuild((builder) => {
builder.modify('filterAccountTypes', filter.account_types);
builder.withGraphFetched('type');
builder.withGraphFetched('balance');
dynamicFilter.buildQuery()(builder);
// console.log(builder.toKnexQuery().toSQL());
}); });
const nestedAccounts = Account.toNestedArray(accounts);
return res.status(200).send({ return res.status(200).send({
accounts: nestedAccounts, accounts:
filter.display_type === 'tree'
? Account.toNestedArray(accounts)
: accounts,
...(view ...(view
? { ? {
customViewId: view.id, customViewId: view.id,

View File

@@ -181,6 +181,7 @@ export default {
const mixinEntry = { const mixinEntry = {
referenceType: 'Expense', referenceType: 'Expense',
referenceId: expenseTransaction.id, referenceId: expenseTransaction.id,
date: moment(form.payment_date).format('YYYY-MM-DD'),
userId: user.id, userId: user.id,
draft: !form.publish, draft: !form.publish,
}; };

View File

@@ -1,13 +1,12 @@
import express from 'express'; import express from 'express';
import { query, oneOf, validationResult } from 'express-validator'; import { query, validationResult } from 'express-validator';
import moment from 'moment'; import moment from 'moment';
import { pick, difference, groupBy } from 'lodash'; import { pick, omit, sumBy } from 'lodash';
import JournalPoster from '@/services/Accounting/JournalPoster'; import JournalPoster from '@/services/Accounting/JournalPoster';
import { dateRangeCollection } from '@/utils'; import { dateRangeCollection, itemsStartWith, getTotalDeep } from '@/utils';
import DependencyGraph from '@/lib/DependencyGraph'; import asyncMiddleware from '@/http/middleware/asyncMiddleware';
import asyncMiddleware from '@/http/middleware/asyncMiddleware'
import { formatNumberClosure } from './FinancialStatementMixin'; import { formatNumberClosure } from './FinancialStatementMixin';
import BalanceSheetStructure from '@/data/BalanceSheetStructure';
export default { export default {
/** /**
@@ -16,9 +15,11 @@ export default {
router() { router() {
const router = express.Router(); const router = express.Router();
router.get('/', router.get(
'/',
this.balanceSheet.validation, this.balanceSheet.validation,
asyncMiddleware(this.balanceSheet.handler)); asyncMiddleware(this.balanceSheet.handler)
);
return router; return router;
}, },
@@ -32,7 +33,8 @@ export default {
query('from_date').optional(), query('from_date').optional(),
query('to_date').optional(), query('to_date').optional(),
query('display_columns_type').optional().isIn(['date_periods', 'total']), query('display_columns_type').optional().isIn(['date_periods', 'total']),
query('display_columns_by').optional({ nullable: true, checkFalsy: true }) query('display_columns_by')
.optional({ nullable: true, checkFalsy: true })
.isIn(['year', 'month', 'week', 'day', 'quarter']), .isIn(['year', 'month', 'week', 'day', 'quarter']),
query('number_format.no_cents').optional().isBoolean().toBoolean(), query('number_format.no_cents').optional().isBoolean().toBoolean(),
query('number_format.divide_1000').optional().isBoolean().toBoolean(), query('number_format.divide_1000').optional().isBoolean().toBoolean(),
@@ -45,7 +47,8 @@ export default {
if (!validationErrors.isEmpty()) { if (!validationErrors.isEmpty()) {
return res.boom.badData(null, { return res.boom.badData(null, {
code: 'validation_error', ...validationErrors, code: 'validation_error',
...validationErrors,
}); });
} }
const { Account, AccountType } = req.models; const { Account, AccountType } = req.models;
@@ -67,106 +70,168 @@ export default {
if (!Array.isArray(filter.account_ids)) { if (!Array.isArray(filter.account_ids)) {
filter.account_ids = [filter.account_ids]; filter.account_ids = [filter.account_ids];
} }
// Account balance formmatter based on the given query. // Account balance formmatter based on the given query.
const balanceFormatter = formatNumberClosure(filter.number_format); const amountFormatter = formatNumberClosure(filter.number_format);
const comparatorDateType = filter.display_columns_type === 'total' ? 'day' : filter.display_columns_by; const comparatorDateType =
filter.display_columns_type === 'total'
const balanceSheetTypes = await AccountType.query().where('balance_sheet', true); ? 'day'
: filter.display_columns_by;
const balanceSheetTypes = await AccountType.query().where(
'balance_sheet',
true
);
// Fetch all balance sheet accounts from the storage. // Fetch all balance sheet accounts from the storage.
const accounts = await Account.query() const accounts = await Account.query()
// .remember('balance_sheet_accounts') .whereIn(
.whereIn('account_type_id', balanceSheetTypes.map((a) => a.id)) 'account_type_id',
balanceSheetTypes.map((a) => a.id)
)
.modify('filterAccounts', filter.account_ids) .modify('filterAccounts', filter.account_ids)
.withGraphFetched('type') .withGraphFetched('type')
.withGraphFetched('transactions') .withGraphFetched('transactions')
.modifyGraph('transactions', (builder) => { .modifyGraph('transactions', (builder) => {
builder.modify('filterDateRange', null, filter.to_date); builder.modify('filterDateRange', null, filter.to_date);
}); });
// Accounts dependency graph. // Accounts dependency graph.
const accountsGraph = DependencyGraph.fromArray( const accountsGraph = Account.toDependencyGraph(accounts);
accounts, { itemId: 'id', parentItemId: 'parentAccountId' }
);
// Load all entries that associated to the given accounts. // Load all entries that associated to the given accounts.
const journalEntriesCollected = Account.collectJournalEntries(accounts); const journalEntriesCollected = Account.collectJournalEntries(accounts);
const journalEntries = new JournalPoster(accountsGraph); const journalEntries = new JournalPoster(accountsGraph);
journalEntries.loadEntries(journalEntriesCollected); journalEntries.loadEntries(journalEntriesCollected);
// Date range collection. // Date range collection.
const dateRangeSet = (filter.display_columns_type === 'date_periods') const dateRangeSet =
filter.display_columns_type === 'date_periods'
? dateRangeCollection( ? dateRangeCollection(
filter.from_date, filter.to_date, comparatorDateType, filter.from_date,
) : []; filter.to_date,
comparatorDateType
)
: [];
// Gets the date range set from start to end date. // Gets the date range set from start to end date.
const totalPeriods = (account) => ({ const getAccountTotalPeriods = (account) => ({
total_periods: dateRangeSet.map((date) => { total_periods: dateRangeSet.map((date) => {
const amount = journalEntries.getAccountBalance(account.id, date, comparatorDateType); const amount = journalEntries.getAccountBalance(
account.id,
date,
comparatorDateType
);
return { return {
amount, amount,
formatted_amount: balanceFormatter(amount),
date, date,
formatted_amount: amountFormatter(amount),
}; };
}), }),
}); });
// Retrieve accounts total periods.
const accountsMapperToResponse = (account) => { const getAccountsTotalPeriods = (_accounts) =>
// Calculates the closing balance to the given date. Object.values(
const closingBalance = journalEntries.getAccountBalance(account.id, filter.to_date); dateRangeSet.reduce((acc, date, index) => {
const amount = sumBy(_accounts, `total_periods[${index}].amount`);
acc[date] = {
date,
amount,
formatted_amount: amountFormatter(amount),
};
return acc;
}, {})
);
// Retrieve account total and total periods with account meta.
const getAccountTotal = (account) => {
const closingBalance = journalEntries.getAccountBalance(
account.id,
filter.to_date
);
const totalPeriods =
(filter.display_columns_type === 'date_periods' &&
getAccountTotalPeriods(account)) ||
null;
return { return {
...pick(account, ['id', 'index', 'name', 'code', 'parentAccountId']), ...pick(account, ['id', 'index', 'name', 'code', 'parentAccountId']),
...(totalPeriods && { totalPeriods }),
// Date periods when display columns.
...(filter.display_columns_type === 'date_periods') && totalPeriods(account),
total: { total: {
amount: closingBalance, amount: closingBalance,
formatted_amount: balanceFormatter(closingBalance), formatted_amount: amountFormatter(closingBalance),
date: filter.to_date, date: filter.to_date,
}, },
}; };
}; };
// Get accounts total of the given structure section
const getAccountsSectionTotal = (_accounts) => {
const total = getTotalDeep(_accounts, 'children', 'total.amount');
return {
total: {
total,
formatted_amount: amountFormatter(total),
},
};
};
// Strcuture accounts related mapper.
const structureAccountsRelatedMapper = (accountsTypes) => {
const filteredAccounts = accounts
// Filter accounts that have no transaction when `none_zero` is on.
.filter(
(account) => account.transactions.length > 0 || !filter.none_zero
)
// Filter accounts that associated to the section accounts types.
.filter(
(account) => accountsTypes.indexOf(account.type.childType) !== -1
)
.map(getAccountTotal);
// Gets total amount of the given accounts.
const totalAmount = sumBy(filteredAccounts, 'total.amount');
// Retrieve all assets accounts. return {
const assetsAccounts = accounts.filter((account) => ( children: Account.toNestedArray(filteredAccounts),
account.type.normal === 'debit' total: {
&& (account.transactions.length > 0 || !filter.none_zero))) amount: totalAmount,
.map(accountsMapperToResponse); formatted_amount: amountFormatter(totalAmount),
},
// Retrieve all liability accounts. ...(filter.display_columns_type === 'date_periods'
const liabilitiesAccounts = accounts.filter((account) => ( ? {
account.type.normal === 'credit' total_periods: getAccountsTotalPeriods(filteredAccounts),
&& (account.transactions.length > 0 || !filter.none_zero))) }
.map(accountsMapperToResponse); : {}),
};
// Retrieve the asset balance sheet. };
const assetsAccountsResponse = Account.toNestedArray(assetsAccounts); // Structure section mapper.
const structureSectionMapper = (structure) => {
// Retrieve liabilities and equity balance sheet. const result = {
const liabilitiesEquityResponse = Account.toNestedArray(liabilitiesAccounts); ...omit(structure, itemsStartWith(Object.keys(structure), '_')),
...(structure.children
? {
children: balanceSheetWalker(structure.children),
}
: {}),
...(structure._accounts_types_related
? {
...structureAccountsRelatedMapper(
structure._accounts_types_related
),
}
: {}),
};
return {
...result,
...(!structure._accounts_types_related
? getAccountsSectionTotal(result.children)
: {}),
};
};
const balanceSheetWalker = (reportStructure) =>
reportStructure.map(structureSectionMapper).filter(
// Filter the structure sections that have no children.
(structure) => structure.children.length > 0 || structure._forceShow
);
// Response. // Response.
return res.status(200).send({ return res.status(200).send({
query: { ...filter }, query: { ...filter },
columns: { ...dateRangeSet }, columns: { ...dateRangeSet },
accounts: [ balance_sheet: [...balanceSheetWalker(BalanceSheetStructure)],
{
name: 'Assets',
type: 'assets',
children: [...assetsAccountsResponse],
},
{
name: 'Liabilities & Equity',
type: 'liabilities_equity',
children: [...liabilitiesEquityResponse],
},
],
}); });
}, },
}, },
} };

View File

@@ -1,12 +1,11 @@
import express from 'express'; import express from 'express';
import { query, oneOf, validationResult } from 'express-validator'; import { query, oneOf, validationResult } from 'express-validator';
import moment from 'moment'; import moment from 'moment';
import { pick } from 'lodash'; import { pick, sumBy } from 'lodash';
import JournalPoster from '@/services/Accounting/JournalPoster'; import JournalPoster from '@/services/Accounting/JournalPoster';
import { dateRangeCollection } from '@/utils'; import { dateRangeCollection } from '@/utils';
import asyncMiddleware from '@/http/middleware/asyncMiddleware'; import asyncMiddleware from '@/http/middleware/asyncMiddleware';
import { formatNumberClosure } from './FinancialStatementMixin'; import { formatNumberClosure } from './FinancialStatementMixin';
import DependencyGraph from '@/lib/DependencyGraph';
export default { export default {
/** /**
@@ -15,10 +14,11 @@ export default {
router() { router() {
const router = express.Router(); const router = express.Router();
router.get('/', router.get(
'/',
this.profitLossSheet.validation, this.profitLossSheet.validation,
asyncMiddleware(this.profitLossSheet.handler)); asyncMiddleware(this.profitLossSheet.handler)
);
return router; return router;
}, },
@@ -36,10 +36,9 @@ export default {
query('none_zero').optional().isBoolean().toBoolean(), query('none_zero').optional().isBoolean().toBoolean(),
query('account_ids').isArray().optional(), query('account_ids').isArray().optional(),
query('account_ids.*').isNumeric().toInt(), query('account_ids.*').isNumeric().toInt(),
query('display_columns_type').optional().isIn([ query('display_columns_type').optional().isIn(['total', 'date_periods']),
'total', 'date_periods', query('display_columns_by')
]), .optional({ nullable: true, checkFalsy: true })
query('display_columns_by').optional({ nullable: true, checkFalsy: true })
.isIn(['year', 'month', 'week', 'day', 'quarter']), .isIn(['year', 'month', 'week', 'day', 'quarter']),
], ],
async handler(req, res) { async handler(req, res) {
@@ -47,7 +46,8 @@ export default {
if (!validationErrors.isEmpty()) { if (!validationErrors.isEmpty()) {
return res.boom.badData(null, { return res.boom.badData(null, {
code: 'validation_error', ...validationErrors, code: 'validation_error',
...validationErrors,
}); });
} }
const { Account, AccountType } = req.models; const { Account, AccountType } = req.models;
@@ -68,91 +68,110 @@ export default {
if (!Array.isArray(filter.account_ids)) { if (!Array.isArray(filter.account_ids)) {
filter.account_ids = [filter.account_ids]; filter.account_ids = [filter.account_ids];
} }
const incomeStatementTypes = await AccountType.query().where('income_sheet', true); const incomeStatementTypes = await AccountType.query().where(
'income_sheet',
true
);
// Fetch all income accounts from storage. // Fetch all income accounts from storage.
const accounts = await Account.query() const accounts = await Account.query()
// .remember('profit_loss_accounts') // .remember('profit_loss_accounts')
.modify('filterAccounts', filter.account_ids) .modify('filterAccounts', filter.account_ids)
.whereIn('account_type_id', incomeStatementTypes.map((t) => t.id)) .whereIn(
'account_type_id',
incomeStatementTypes.map((t) => t.id)
)
.withGraphFetched('type') .withGraphFetched('type')
.withGraphFetched('transactions'); .withGraphFetched('transactions');
// Accounts dependency graph. // Accounts dependency graph.
const accountsGraph = DependencyGraph.fromArray( const accountsGraph = Account.toDependencyGraph(accounts);
accounts, { itemId: 'id', parentItemId: 'parentAccountId' }
);
// Filter all none zero accounts if it was enabled. // Filter all none zero accounts if it was enabled.
const filteredAccounts = accounts.filter((account) => ( const filteredAccounts = accounts.filter(
account.transactions.length > 0 || !filter.none_zero (account) => account.transactions.length > 0 || !filter.none_zero
)); );
const journalEntriesCollected = Account.collectJournalEntries(accounts); const journalEntriesCollected = Account.collectJournalEntries(accounts);
const journalEntries = new JournalPoster(accountsGraph); const journalEntries = new JournalPoster(accountsGraph);
journalEntries.loadEntries(journalEntriesCollected); journalEntries.loadEntries(journalEntriesCollected);
// Account balance formmatter based on the given query. // Account balance formmatter based on the given query.
const numberFormatter = formatNumberClosure(filter.number_format); const numberFormatter = formatNumberClosure(filter.number_format);
const comparatorDateType = filter.display_columns_type === 'total' const comparatorDateType =
? 'day' : filter.display_columns_by; filter.display_columns_type === 'total'
? 'day'
: filter.display_columns_by;
// Gets the date range set from start to end date. // Gets the date range set from start to end date.
const dateRangeSet = dateRangeCollection( const dateRangeSet = dateRangeCollection(
filter.from_date, filter.from_date,
filter.to_date, filter.to_date,
comparatorDateType, comparatorDateType
); );
const accountsMapper = (incomeExpenseAccounts) =>
const accountsMapper = (incomeExpenseAccounts) => (
incomeExpenseAccounts.map((account) => ({ incomeExpenseAccounts.map((account) => ({
...pick(account, ['id', 'index', 'name', 'code', 'parentAccountId']), ...pick(account, ['id', 'index', 'name', 'code', 'parentAccountId']),
// Total closing balance of the account. // Total closing balance of the account.
...(filter.display_columns_type === 'total') && { ...(filter.display_columns_type === 'total' && {
total: (() => { total: (() => {
const amount = journalEntries.getAccountBalance(account.id, filter.to_date); const amount = journalEntries.getAccountBalance(
return { amount, date: filter.to_date, formatted_amount: numberFormatter(amount) }; account.id,
filter.to_date
);
return {
amount,
date: filter.to_date,
formatted_amount: numberFormatter(amount),
};
})(), })(),
}, }),
// Date periods when display columns type `periods`. // Date periods when display columns type `periods`.
...(filter.display_columns_type === 'date_periods') && { ...(filter.display_columns_type === 'date_periods' && {
periods: dateRangeSet.map((date) => { periods: dateRangeSet.map((date) => {
const type = comparatorDateType; const type = comparatorDateType;
const amount = journalEntries.getAccountBalance(account.id, date, type); const amount = journalEntries.getAccountBalance(
account.id,
return { date, amount, formatted_amount: numberFormatter(amount) }; date,
type
);
return {
date,
amount,
formatted_amount: numberFormatter(amount),
};
}), }),
}, }),
}))); }));
const totalAccountsReducer = (incomeExpenseAccounts) => ( const accountsIncome = Account.toNestedArray(
incomeExpenseAccounts.reduce((acc, account) => { accountsMapper(
const amount = (account) ? account.total.amount : 0; filteredAccounts.filter((account) => account.type.normal === 'credit')
return amount + acc; )
}, 0)); );
const accountsExpenses = Account.toNestedArray(
const accountsIncome = Account.toNestedArray(accountsMapper(filteredAccounts accountsMapper(
.filter((account) => account.type.normal === 'credit'))); filteredAccounts.filter((account) => account.type.normal === 'debit')
)
const accountsExpenses = Account.toNestedArray(accountsMapper(filteredAccounts );
.filter((account) => account.type.normal === 'debit'))); const totalPeriodsMapper = (incomeExpenseAccounts) =>
Object.values(
// @return {Array} dateRangeSet.reduce((acc, date, index) => {
const totalPeriodsMapper = (incomeExpenseAccounts) => ( let amount = sumBy(
Object.values(dateRangeSet.reduce((acc, date, index) => { incomeExpenseAccounts,
let amount = 0; `periods[${index}].amount`
);
incomeExpenseAccounts.forEach((account) => { acc[date] = {
const currentDate = account.periods[index]; date,
amount += currentDate.amount || 0; amount,
}); formatted_amount: numberFormatter(amount),
acc[date] = { date, amount, formatted_amount: numberFormatter(amount) }; };
return acc; return acc;
}, {}))); }, {})
);
// Total income(date) - Total expenses(date) = Net income(date) // Total income - Total expenses = Net income
// @return {Array} const netIncomePeriodsMapper = (
const netIncomePeriodsMapper = (totalIncomeAcocunts, totalExpenseAccounts) => ( totalIncomeAcocunts,
totalExpenseAccounts
) =>
dateRangeSet.map((date, index) => { dateRangeSet.map((date, index) => {
const totalIncome = totalIncomeAcocunts[index]; const totalIncome = totalIncomeAcocunts[index];
const totalExpenses = totalExpenseAccounts[index]; const totalExpenses = totalExpenseAccounts[index];
@@ -160,19 +179,24 @@ export default {
let amount = totalIncome.amount || 0; let amount = totalIncome.amount || 0;
amount -= totalExpenses.amount || 0; amount -= totalExpenses.amount || 0;
return { date, amount, formatted_amount: numberFormatter(amount) }; return { date, amount, formatted_amount: numberFormatter(amount) };
})); });
// @return {Object} // @return {Object}
const netIncomeTotal = (totalIncome, totalExpenses) => { const netIncomeTotal = (totalIncome, totalExpenses) => {
const netIncomeAmount = totalIncome.amount - totalExpenses.amount; const netIncomeAmount = totalIncome.amount - totalExpenses.amount;
return { amount: netIncomeAmount, formatted_amount: netIncomeAmount, date: filter.to_date }; return {
amount: netIncomeAmount,
formatted_amount: netIncomeAmount,
date: filter.to_date,
};
}; };
const incomeResponse = { const incomeResponse = {
entry_normal: 'credit', entry_normal: 'credit',
accounts: accountsIncome, accounts: accountsIncome,
...(filter.display_columns_type === 'total') && (() => { ...(filter.display_columns_type === 'total' &&
const totalIncomeAccounts = totalAccountsReducer(accountsIncome); (() => {
const totalIncomeAccounts = sumBy(accountsIncome, 'total.amount');
return { return {
total: { total: {
amount: totalIncomeAccounts, amount: totalIncomeAccounts,
@@ -180,18 +204,20 @@ export default {
formatted_amount: numberFormatter(totalIncomeAccounts), formatted_amount: numberFormatter(totalIncomeAccounts),
}, },
}; };
})(), })()),
...(filter.display_columns_type === 'date_periods') && { ...(filter.display_columns_type === 'date_periods' && {
total_periods: [ total_periods: [...totalPeriodsMapper(accountsIncome)],
...totalPeriodsMapper(accountsIncome), }),
],
},
}; };
const expenseResponse = { const expenseResponse = {
entry_normal: 'debit', entry_normal: 'debit',
accounts: accountsExpenses, accounts: accountsExpenses,
...(filter.display_columns_type === 'total') && (() => { ...(filter.display_columns_type === 'total' &&
const totalExpensesAccounts = totalAccountsReducer(accountsExpenses); (() => {
const totalExpensesAccounts = sumBy(
accountsExpenses,
'total.amount'
);
return { return {
total: { total: {
amount: totalExpensesAccounts, amount: totalExpensesAccounts,
@@ -199,27 +225,25 @@ export default {
formatted_amount: numberFormatter(totalExpensesAccounts), formatted_amount: numberFormatter(totalExpensesAccounts),
}, },
}; };
})(), })()),
...(filter.display_columns_type === 'date_periods') && { ...(filter.display_columns_type === 'date_periods' && {
total_periods: [ total_periods: [...totalPeriodsMapper(accountsExpenses)],
...totalPeriodsMapper(accountsExpenses), }),
],
},
}; };
const netIncomeResponse = { const netIncomeResponse = {
...(filter.display_columns_type === 'total') && { ...(filter.display_columns_type === 'total' && {
total: { total: {
...netIncomeTotal(incomeResponse.total, expenseResponse.total), ...netIncomeTotal(incomeResponse.total, expenseResponse.total),
}, },
}, }),
...(filter.display_columns_type === 'date_periods') && { ...(filter.display_columns_type === 'date_periods' && {
total_periods: [ total_periods: [
...netIncomePeriodsMapper( ...netIncomePeriodsMapper(
incomeResponse.total_periods, incomeResponse.total_periods,
expenseResponse.total_periods, expenseResponse.total_periods
), ),
], ],
}, }),
}; };
return res.status(200).send({ return res.status(200).send({
query: { ...filter }, query: { ...filter },
@@ -232,4 +256,4 @@ export default {
}); });
}, },
}, },
} };

View File

@@ -1,7 +1,6 @@
import bcrypt from 'bcryptjs'; import bcrypt from 'bcryptjs';
import moment from 'moment'; import moment from 'moment';
import _ from 'lodash'; import _ from 'lodash';
const { map, isArray, isPlainObject, mapKeys, mapValues } = require('lodash');
const hashPassword = (password) => const hashPassword = (password) =>
new Promise((resolve) => { new Promise((resolve) => {
@@ -118,10 +117,21 @@ const flatToNestedArray = (
map[parentItemId].children.push(item); map[parentItemId].children.push(item);
} }
}); });
return nestedArray; return nestedArray;
}; };
const itemsStartWith = (items, char) => {
return items.filter((item) => item.indexOf(char) === 0);
};
const getTotalDeep = (items, deepProp, totalProp) =>
items.reduce((acc, item) => {
const total = Array.isArray(item[deepProp])
? getTotalDeep(item[deepProp], deepProp, totalProp)
: 0;
return _.sumBy(item, totalProp) + total + acc;
}, 0);
export { export {
hashPassword, hashPassword,
origin, origin,
@@ -131,4 +141,6 @@ export {
mapKeysDeep, mapKeysDeep,
promiseSerial, promiseSerial,
flatToNestedArray, flatToNestedArray,
itemsStartWith,
getTotalDeep,
}; };

View File

@@ -13,7 +13,7 @@ import {
} from '~/dbInit'; } from '~/dbInit';
describe.only('routes: `/accounting`', () => { describe('routes: `/accounting`', () => {
describe('route: `/accounting/make-journal-entries`', async () => { describe('route: `/accounting/make-journal-entries`', async () => {
it('Should sumation of credit or debit does not equal zero.', async () => { it('Should sumation of credit or debit does not equal zero.', async () => {
const account = await tenantFactory.create('account'); const account = await tenantFactory.create('account');

View File

@@ -0,0 +1,541 @@
import moment from 'moment';
import {
request,
expect,
} from '~/testInit';
import {
tenantWebsite,
tenantFactory,
loginRes
} from '~/dbInit';
import { iteratee } from 'lodash';
let creditAccount;
let debitAccount;
let incomeAccount;
let incomeType;
describe('routes: `/financial_statements`', () => {
beforeEach(async () => {
const accountTransactionMixied = { date: '2020-1-10' };
// Expense --
// 1000 Credit - Cash account
// 1000 Debit - Bank account.
await tenantFactory.create('account_transaction', {
credit: 1000, debit: 0, account_id: 2, referenceType: 'Expense',
referenceId: 1, ...accountTransactionMixied,
});
await tenantFactory.create('account_transaction', {
credit: 0, debit: 1000, account_id: 7, referenceType: 'Expense',
referenceId: 1, ...accountTransactionMixied,
});
// Jounral
// 4000 Credit - Opening balance account.
// 2000 Debit - Bank account
// 2000 Debit - Bank account
await tenantFactory.create('account_transaction', {
credit: 4000, debit: 0, account_id: 5, ...accountTransactionMixied,
});
await tenantFactory.create('account_transaction', {
debit: 2000, credit: 0, account_id: 2, ...accountTransactionMixied,
});
await tenantFactory.create('account_transaction', {
debit: 2000, credit: 0, account_id: 2, ...accountTransactionMixied,
});
// Income Journal.
// 2000 Credit - Income account.
// 2000 Debit - Bank account.
await tenantFactory.create('account_transaction', {
credit: 2000, account_id: 4, ...accountTransactionMixied
});
await tenantFactory.create('account_transaction', {
debit: 2000, credit: 0, account_id: 2, ...accountTransactionMixied,
});
// -----------------------------------------
// Bank account balance = 5000 | Opening balance account balance = 4000
// Expense account balance = 1000 | Income account balance = 2000
});
describe.only('routes: `financial_statements/balance_sheet`', () => {
it('Should response unauthorzied in case the user was not authorized.', async () => {
const res = await request()
.get('/api/financial_statements/balance_sheet')
.send();
expect(res.status).equals(401);
});
it('Should retrieve query of the balance sheet with default values.', async () => {
const res = await request()
.get('/api/financial_statements/balance_sheet')
.set('x-access-token', loginRes.body.token)
.set('organization-id', tenantWebsite.organizationId)
.query({
display_columns_by: 'year',
from_date: '2020-01-01',
to_date: '2020-02-01',
})
.send();
expect(res.body.query.display_columns_by).equals('year');
expect(res.body.query.from_date).equals('2020-01-01');
expect(res.body.query.to_date).equals('2020-02-01');
expect(res.body.query.number_format.no_cents).equals(false);
expect(res.body.query.number_format.divide_1000).equals(false);
expect(res.body.query.none_zero).equals(false);
});
it('Should retrieve assets and liabilities/equity section.', async () => {
const res = await request()
.get('/api/financial_statements/balance_sheet')
.set('x-access-token', loginRes.body.token)
.set('organization-id', tenantWebsite.organizationId)
.query({
display_columns_by: 'year',
})
.send();
expect(res.body.balance_sheet[0].name).equals('Assets');
expect(res.body.balance_sheet[1].name).equals('Liabilities and Equity');
expect(res.body.balance_sheet[0].section_type).equals('assets');
expect(res.body.balance_sheet[1].section_type).equals('liabilities_equity');
expect(res.body.balance_sheet[0].type).equals('section');
expect(res.body.balance_sheet[1].type).equals('section');
});
it.only('Should retrieve assets and liabilities/equity total of each section.', async () => {
const res = await request()
.get('/api/financial_statements/balance_sheet')
.set('x-access-token', loginRes.body.token)
.set('organization-id', tenantWebsite.organizationId)
.query({
to_date: '2020-12-10',
})
.send();
expect(res.body.balance_sheet[0].total.amount).equals(5000);
expect(res.body.balance_sheet[1].total.amount).equals(4000);
});
it('Should retrieve the asset and liabilities/equity accounts.', async () => {
const res = await request()
.get('/api/financial_statements/balance_sheet')
.set('x-access-token', loginRes.body.token)
.set('organization-id', tenantWebsite.organizationId)
.query({
display_columns_type: 'total',
from_date: '2012-01-01',
to_date: '2032-02-02',
})
.send();
expect(res.body.balance_sheet[0].children).to.be.a('array');
expect(res.body.balance_sheet[0].children).to.be.a('array');
expect(res.body.balance_sheet[0].children.length).is.not.equals(0);
expect(res.body.balance_sheet[1].children.length).is.not.equals(0);
expect(res.body.balance_sheet[1].children[0].children.length).is.not.equals(0);
expect(res.body.balance_sheet[1].children[1].children.length).is.not.equals(0);
});
it('Should retrieve assets/liabilities total balance between the given date range.', async () => {
const res = await request()
.get('/api/financial_statements/balance_sheet')
.set('x-access-token', loginRes.body.token)
.set('organization-id', tenantWebsite.organizationId)
.query({
display_columns_type: 'total',
from_date: '2012-01-01',
to_date: '2032-02-02',
})
.send();
expect(res.body.accounts[0].children).include.something.deep.equals({
id: 1001,
index: null,
name: debitAccount.name,
code: debitAccount.code,
parentAccountId: null,
children: [],
total: { formatted_amount: 5000, amount: 5000, date: '2032-02-02' }
});
expect(res.body.accounts[1].children).include.something.deep.equals({
id: 1000,
index: null,
name: creditAccount.name,
code: creditAccount.code,
parentAccountId: null,
children: [],
total: { formatted_amount: 4000, amount: 4000, date: '2032-02-02' }
});
});
it('Should retrieve asset/liabilities balance sheet with display columns by `year`.', async () => {
const res = await request()
.get('/api/financial_statements/balance_sheet')
.set('x-access-token', loginRes.body.token)
.set('organization-id', tenantWebsite.organizationId)
.query({
display_columns_by: 'year',
display_columns_type: 'date_periods',
from_date: '2012-01-01',
to_date: '2018-02-02',
})
.send();
expect(res.body.accounts[0].children[0].total_periods.length).equals(7);
expect(res.body.accounts[1].children[0].total_periods.length).equals(7);
expect(res.body.accounts[0].children[0].total_periods).deep.equals([
{
amount: 0,
formatted_amount: 0,
date: '2012',
},
{
amount: 0,
formatted_amount: 0,
date: '2013',
},
{
amount: 0,
formatted_amount: 0,
date: '2014',
},
{
amount: 0,
formatted_amount: 0,
date: '2015',
},
{
amount: 0,
formatted_amount: 0,
date: '2016',
},
{
amount: 0,
formatted_amount: 0,
date: '2017',
},
{
amount: 0,
formatted_amount: 0,
date: '2018',
},
]);
});
it('Should retrieve balance sheet with display columns by `day`.', async () => {
const res = await request()
.get('/api/financial_statements/balance_sheet')
.set('x-access-token', loginRes.body.token)
.set('organization-id', tenantWebsite.organizationId)
.query({
display_columns_by: 'day',
display_columns_type: 'date_periods',
from_date: '2020-01-08',
to_date: '2020-01-12',
})
.send();
expect(res.body.accounts[0].children).include.something.deep.equals({
id: debitAccount.id,
index: debitAccount.index,
name: debitAccount.name,
code: debitAccount.code,
parentAccountId: null,
children: [],
total_periods: [
{ date: '2020-01-08', formatted_amount: 0, amount: 0 },
{ date: '2020-01-09', formatted_amount: 0, amount: 0 },
{ date: '2020-01-10', formatted_amount: 5000, amount: 5000 },
{ date: '2020-01-11', formatted_amount: 5000, amount: 5000 },
{ date: '2020-01-12', formatted_amount: 5000, amount: 5000 },
],
total: { formatted_amount: 5000, amount: 5000, date: '2020-01-12' }
});
expect(res.body.accounts[1].children).include.something.deep.equals({
id: creditAccount.id,
index: creditAccount.index,
name: creditAccount.name,
code: creditAccount.code,
parentAccountId: null,
children: [],
total_periods: [
{ date: '2020-01-08', formatted_amount: 0, amount: 0 },
{ date: '2020-01-09', formatted_amount: 0, amount: 0 },
{ date: '2020-01-10', formatted_amount: 4000, amount: 4000 },
{ date: '2020-01-11', formatted_amount: 4000, amount: 4000 },
{ date: '2020-01-12', formatted_amount: 4000, amount: 4000 }
],
total: { formatted_amount: 4000, amount: 4000, date: '2020-01-12' }
});
});
it('Should retrieve the balance sheet with display columns by `month`.', async () => {
const res = await request()
.get('/api/financial_statements/balance_sheet')
.set('x-access-token', loginRes.body.token)
.set('organization-id', tenantWebsite.organizationId)
.query({
display_columns_by: 'month',
display_columns_type: 'date_periods',
from_date: '2019-07-01',
to_date: '2020-06-30',
})
.send();
expect(res.body.accounts[0].children).include.something.deep.equals({
id: debitAccount.id,
index: debitAccount.index,
name: debitAccount.name,
code: debitAccount.code,
parentAccountId: null,
children: [],
total_periods: [
{ date: '2019-07', formatted_amount: 0, amount: 0 },
{ date: '2019-08', formatted_amount: 0, amount: 0 },
{ date: '2019-09', formatted_amount: 0, amount: 0 },
{ date: '2019-10', formatted_amount: 0, amount: 0 },
{ date: '2019-11', formatted_amount: 0, amount: 0 },
{ date: '2019-12', formatted_amount: 0, amount: 0 },
{ date: '2020-01', formatted_amount: 5000, amount: 5000 },
{ date: '2020-02', formatted_amount: 5000, amount: 5000 },
{ date: '2020-03', formatted_amount: 5000, amount: 5000 },
{ date: '2020-04', formatted_amount: 5000, amount: 5000 },
{ date: '2020-05', formatted_amount: 5000, amount: 5000 },
{ date: '2020-06', formatted_amount: 5000, amount: 5000 },
],
total: { formatted_amount: 5000, amount: 5000, date: '2020-06-30' }
});
});
it('Should retrieve the balance sheet with display columns `quarter`.', async () => {
const res = await request()
.get('/api/financial_statements/balance_sheet')
.set('x-access-token', loginRes.body.token)
.set('organization-id', tenantWebsite.organizationId)
.query({
display_columns_by: 'quarter',
display_columns_type: 'date_periods',
from_date: '2020-01-01',
to_date: '2020-12-31',
})
.send();
expect(res.body.accounts[0].children).include.something.deep.equals({
id: debitAccount.id,
index: debitAccount.index,
name: debitAccount.name,
code: debitAccount.code,
parentAccountId: null,
children: [],
total_periods: [
{ date: '2020-03', formatted_amount: 5000, amount: 5000 },
{ date: '2020-06', formatted_amount: 5000, amount: 5000 },
{ date: '2020-09', formatted_amount: 5000, amount: 5000 },
{ date: '2020-12', formatted_amount: 5000, amount: 5000 },
],
total: { formatted_amount: 5000, amount: 5000, date: '2020-12-31' },
});
});
it('Should retrieve the balance sheet amounts without cents.', async () => {
await tenantFactory.create('account_transaction', {
debit: 0.25, credit: 0, account_id: debitAccount.id, date: '2020-1-10',
});
const res = await request()
.get('/api/financial_statements/balance_sheet')
.set('x-access-token', loginRes.body.token)
.set('organization-id', tenantWebsite.organizationId)
.query({
display_columns_by: 'quarter',
display_columns_type: 'date_periods',
from_date: '2020-01-01',
to_date: '2020-12-31',
number_format: {
no_cents: true,
},
})
.send();
expect(res.body.accounts[0].children).include.something.deep.equals({
id: debitAccount.id,
index: debitAccount.index,
name: debitAccount.name,
code: debitAccount.code,
parentAccountId: null,
children: [],
total_periods: [
{ date: '2020-03', formatted_amount: 5000, amount: 5000.25 },
{ date: '2020-06', formatted_amount: 5000, amount: 5000.25 },
{ date: '2020-09', formatted_amount: 5000, amount: 5000.25 },
{ date: '2020-12', formatted_amount: 5000, amount: 5000.25 },
],
total: { formatted_amount: 5000, amount: 5000.25, date: '2020-12-31' },
});
});
it('Should retrieve the balance sheet amounts divided on 1000.', async () => {
const res = await request()
.get('/api/financial_statements/balance_sheet')
.set('x-access-token', loginRes.body.token)
.set('organization-id', tenantWebsite.organizationId)
.query({
display_columns_by: 'quarter',
display_columns_type: 'date_periods',
from_date: '2020',
to_date: '2021',
number_format: {
divide_1000: true,
},
})
.send();
expect(res.body.accounts[0].children).include.something.deep.equals({
id: debitAccount.id,
index: debitAccount.index,
name: debitAccount.name,
code: debitAccount.code,
parentAccountId: null,
children: [],
total_periods: [
{ date: '2020-03', formatted_amount: 5, amount: 5000 },
{ date: '2020-06', formatted_amount: 5, amount: 5000 },
{ date: '2020-09', formatted_amount: 5, amount: 5000 },
{ date: '2020-12', formatted_amount: 5, amount: 5000 },
{ date: '2021-03', formatted_amount: 5, amount: 5000 },
],
total: { formatted_amount: 5, amount: 5000, date: '2021' },
});
});
it('Should not retrieve accounts has no transactions between the given date range in case query none_zero is true.', async () => {
const res = await request()
.get('/api/financial_statements/balance_sheet')
.set('x-access-token', loginRes.body.token)
.set('organization-id', tenantWebsite.organizationId)
.query({
display_columns_by: 'quarter',
from_date: '2002',
to_date: '2003',
number_format: {
divide_1000: true,
},
none_zero: true,
})
.send();
expect(res.body.accounts[0].children.length).equals(0);
expect(res.body.accounts[1].children.length).equals(0);
});
it('Should retrieve accounts in nested structure parent and children accounts.', async () => {
const childAccount = await tenantFactory.create('account', {
parent_account_id: debitAccount.id,
account_type_id: 1
});
const res = await request()
.get('/api/financial_statements/balance_sheet')
.set('x-access-token', loginRes.body.token)
.set('organization-id', tenantWebsite.organizationId)
.query({
none_zero: false,
account_ids: [childAccount.id, debitAccount.id]
})
.send();
expect(res.body.accounts[0].children).include.something.deep.equals({
id: debitAccount.id,
index: null,
name: debitAccount.name,
code: debitAccount.code,
parentAccountId: null,
total: { formatted_amount: 5000, amount: 5000, date: '2020-12-31' },
children: [
{
id: childAccount.id,
index: null,
name: childAccount.name,
code: childAccount.code,
parentAccountId: debitAccount.id,
total: { formatted_amount: 0, amount: 0, date: '2020-12-31' },
children: [],
}
]
});
});
it('Should parent account balance sumation of total balane all children accounts.', async () => {
const childAccount = await tenantFactory.create('account', {
parent_account_id: debitAccount.id,
account_type_id: 1
});
await tenantFactory.create('account_transaction', {
credit: 0, debit: 1000, account_id: childAccount.id, date: '2020-1-10'
});
const res = await request()
.get('/api/financial_statements/balance_sheet')
.set('x-access-token', loginRes.body.token)
.set('organization-id', tenantWebsite.organizationId)
.query({
none_zero: false,
account_ids: [childAccount.id, debitAccount.id]
})
.send();
expect(res.body.accounts[0].children[0].total.amount).equals(6000);
expect(res.body.accounts[0].children[0].total.formatted_amount).equals(6000);
});
it('Should parent account balance sumation of total periods amounts all children accounts.', async () => {
const childAccount = await tenantFactory.create('account', {
parent_account_id: debitAccount.id,
account_type_id: 1
});
await tenantFactory.create('account_transaction', {
credit: 0, debit: 1000, account_id: childAccount.id, date: '2020-2-10'
});
const res = await request()
.get('/api/financial_statements/balance_sheet')
.set('x-access-token', loginRes.body.token)
.set('organization-id', tenantWebsite.organizationId)
.query({
none_zero: false,
account_ids: [childAccount.id, debitAccount.id],
display_columns_type: 'date_periods',
display_columns_by: 'month',
from_date: '2020-01-01',
to_date: '2020-12-12',
})
.send();
expect(res.body.accounts[0].children[0].total_periods).deep.equals([
{ amount: 5000, formatted_amount: 5000, date: '2020-01' },
{ amount: 6000, formatted_amount: 6000, date: '2020-02' },
{ amount: 6000, formatted_amount: 6000, date: '2020-03' },
{ amount: 6000, formatted_amount: 6000, date: '2020-04' },
{ amount: 6000, formatted_amount: 6000, date: '2020-05' },
{ amount: 6000, formatted_amount: 6000, date: '2020-06' },
{ amount: 6000, formatted_amount: 6000, date: '2020-07' },
{ amount: 6000, formatted_amount: 6000, date: '2020-08' },
{ amount: 6000, formatted_amount: 6000, date: '2020-09' },
{ amount: 6000, formatted_amount: 6000, date: '2020-10' },
{ amount: 6000, formatted_amount: 6000, date: '2020-11' },
{ amount: 6000, formatted_amount: 6000, date: '2020-12' }
])
});
});
});

View File

@@ -8,6 +8,7 @@ import {
tenantFactory, tenantFactory,
loginRes loginRes
} from '~/dbInit'; } from '~/dbInit';
import { iteratee } from 'lodash';
let creditAccount; let creditAccount;
let debitAccount; let debitAccount;
@@ -436,446 +437,6 @@ describe('routes: `/financial_statements`', () => {
}); });
}); });
describe('routes: `financial_statements/balance_sheet`', () => {
it('Should response unauthorzied in case the user was not authorized.', async () => {
const res = await request()
.get('/api/financial_statements/balance_sheet')
.send();
expect(res.status).equals(401);
});
it('Should retrieve query of the balance sheet with default values.', async () => {
const res = await request()
.get('/api/financial_statements/balance_sheet')
.set('x-access-token', loginRes.body.token)
.set('organization-id', tenantWebsite.organizationId)
.query({
display_columns_by: 'year',
from_date: '2020-01-01',
to_date: '2020-02-01',
})
.send();
expect(res.body.query.display_columns_by).equals('year');
expect(res.body.query.from_date).equals('2020-01-01');
expect(res.body.query.to_date).equals('2020-02-01');
expect(res.body.query.number_format.no_cents).equals(false);
expect(res.body.query.number_format.divide_1000).equals(false);
expect(res.body.query.none_zero).equals(false);
});
it('Should retrieve the asset accounts balance.', async () => {
const res = await request()
.get('/api/financial_statements/balance_sheet')
.set('x-access-token', loginRes.body.token)
.set('organization-id', tenantWebsite.organizationId)
.query({
display_columns_by: 'year',
})
.send();
expect(res.body.accounts[0].children).to.be.a('array');
expect(res.body.accounts[1].children).to.be.a('array');
expect(res.body.accounts[0].children.length).is.not.equals(0);
expect(res.body.accounts[1].children.length).is.not.equals(0);
});
it('Should retrieve assets/liabilities total balance between the given date range.', async () => {
const res = await request()
.get('/api/financial_statements/balance_sheet')
.set('x-access-token', loginRes.body.token)
.set('organization-id', tenantWebsite.organizationId)
.query({
display_columns_type: 'total',
from_date: '2012-01-01',
to_date: '2032-02-02',
})
.send();
expect(res.body.accounts[0].children).include.something.deep.equals({
id: 1001,
index: null,
name: debitAccount.name,
code: debitAccount.code,
parentAccountId: null,
children: [],
total: { formatted_amount: 5000, amount: 5000, date: '2032-02-02' }
});
expect(res.body.accounts[1].children).include.something.deep.equals({
id: 1000,
index: null,
name: creditAccount.name,
code: creditAccount.code,
parentAccountId: null,
children: [],
total: { formatted_amount: 4000, amount: 4000, date: '2032-02-02' }
});
});
it('Should retrieve asset/liabilities balance sheet with display columns by `year`.', async () => {
const res = await request()
.get('/api/financial_statements/balance_sheet')
.set('x-access-token', loginRes.body.token)
.set('organization-id', tenantWebsite.organizationId)
.query({
display_columns_by: 'year',
display_columns_type: 'date_periods',
from_date: '2012-01-01',
to_date: '2018-02-02',
})
.send();
expect(res.body.accounts[0].children[0].total_periods.length).equals(7);
expect(res.body.accounts[1].children[0].total_periods.length).equals(7);
expect(res.body.accounts[0].children[0].total_periods).deep.equals([
{
amount: 0,
formatted_amount: 0,
date: '2012',
},
{
amount: 0,
formatted_amount: 0,
date: '2013',
},
{
amount: 0,
formatted_amount: 0,
date: '2014',
},
{
amount: 0,
formatted_amount: 0,
date: '2015',
},
{
amount: 0,
formatted_amount: 0,
date: '2016',
},
{
amount: 0,
formatted_amount: 0,
date: '2017',
},
{
amount: 0,
formatted_amount: 0,
date: '2018',
},
]);
});
it('Should retrieve balance sheet with display columns by `day`.', async () => {
const res = await request()
.get('/api/financial_statements/balance_sheet')
.set('x-access-token', loginRes.body.token)
.set('organization-id', tenantWebsite.organizationId)
.query({
display_columns_by: 'day',
display_columns_type: 'date_periods',
from_date: '2020-01-08',
to_date: '2020-01-12',
})
.send();
expect(res.body.accounts[0].children).include.something.deep.equals({
id: debitAccount.id,
index: debitAccount.index,
name: debitAccount.name,
code: debitAccount.code,
parentAccountId: null,
children: [],
total_periods: [
{ date: '2020-01-08', formatted_amount: 0, amount: 0 },
{ date: '2020-01-09', formatted_amount: 0, amount: 0 },
{ date: '2020-01-10', formatted_amount: 5000, amount: 5000 },
{ date: '2020-01-11', formatted_amount: 5000, amount: 5000 },
{ date: '2020-01-12', formatted_amount: 5000, amount: 5000 },
],
total: { formatted_amount: 5000, amount: 5000, date: '2020-01-12' }
});
expect(res.body.accounts[1].children).include.something.deep.equals({
id: creditAccount.id,
index: creditAccount.index,
name: creditAccount.name,
code: creditAccount.code,
parentAccountId: null,
children: [],
total_periods: [
{ date: '2020-01-08', formatted_amount: 0, amount: 0 },
{ date: '2020-01-09', formatted_amount: 0, amount: 0 },
{ date: '2020-01-10', formatted_amount: 4000, amount: 4000 },
{ date: '2020-01-11', formatted_amount: 4000, amount: 4000 },
{ date: '2020-01-12', formatted_amount: 4000, amount: 4000 }
],
total: { formatted_amount: 4000, amount: 4000, date: '2020-01-12' }
});
});
it('Should retrieve the balance sheet with display columns by `month`.', async () => {
const res = await request()
.get('/api/financial_statements/balance_sheet')
.set('x-access-token', loginRes.body.token)
.set('organization-id', tenantWebsite.organizationId)
.query({
display_columns_by: 'month',
display_columns_type: 'date_periods',
from_date: '2019-07-01',
to_date: '2020-06-30',
})
.send();
expect(res.body.accounts[0].children).include.something.deep.equals({
id: debitAccount.id,
index: debitAccount.index,
name: debitAccount.name,
code: debitAccount.code,
parentAccountId: null,
children: [],
total_periods: [
{ date: '2019-07', formatted_amount: 0, amount: 0 },
{ date: '2019-08', formatted_amount: 0, amount: 0 },
{ date: '2019-09', formatted_amount: 0, amount: 0 },
{ date: '2019-10', formatted_amount: 0, amount: 0 },
{ date: '2019-11', formatted_amount: 0, amount: 0 },
{ date: '2019-12', formatted_amount: 0, amount: 0 },
{ date: '2020-01', formatted_amount: 5000, amount: 5000 },
{ date: '2020-02', formatted_amount: 5000, amount: 5000 },
{ date: '2020-03', formatted_amount: 5000, amount: 5000 },
{ date: '2020-04', formatted_amount: 5000, amount: 5000 },
{ date: '2020-05', formatted_amount: 5000, amount: 5000 },
{ date: '2020-06', formatted_amount: 5000, amount: 5000 },
],
total: { formatted_amount: 5000, amount: 5000, date: '2020-06-30' }
});
});
it('Should retrieve the balance sheet with display columns `quarter`.', async () => {
const res = await request()
.get('/api/financial_statements/balance_sheet')
.set('x-access-token', loginRes.body.token)
.set('organization-id', tenantWebsite.organizationId)
.query({
display_columns_by: 'quarter',
display_columns_type: 'date_periods',
from_date: '2020-01-01',
to_date: '2020-12-31',
})
.send();
expect(res.body.accounts[0].children).include.something.deep.equals({
id: debitAccount.id,
index: debitAccount.index,
name: debitAccount.name,
code: debitAccount.code,
parentAccountId: null,
children: [],
total_periods: [
{ date: '2020-03', formatted_amount: 5000, amount: 5000 },
{ date: '2020-06', formatted_amount: 5000, amount: 5000 },
{ date: '2020-09', formatted_amount: 5000, amount: 5000 },
{ date: '2020-12', formatted_amount: 5000, amount: 5000 },
],
total: { formatted_amount: 5000, amount: 5000, date: '2020-12-31' },
});
});
it('Should retrieve the balance sheet amounts without cents.', async () => {
await tenantFactory.create('account_transaction', {
debit: 0.25, credit: 0, account_id: debitAccount.id, date: '2020-1-10',
});
const res = await request()
.get('/api/financial_statements/balance_sheet')
.set('x-access-token', loginRes.body.token)
.set('organization-id', tenantWebsite.organizationId)
.query({
display_columns_by: 'quarter',
display_columns_type: 'date_periods',
from_date: '2020-01-01',
to_date: '2020-12-31',
number_format: {
no_cents: true,
},
})
.send();
expect(res.body.accounts[0].children).include.something.deep.equals({
id: debitAccount.id,
index: debitAccount.index,
name: debitAccount.name,
code: debitAccount.code,
parentAccountId: null,
children: [],
total_periods: [
{ date: '2020-03', formatted_amount: 5000, amount: 5000.25 },
{ date: '2020-06', formatted_amount: 5000, amount: 5000.25 },
{ date: '2020-09', formatted_amount: 5000, amount: 5000.25 },
{ date: '2020-12', formatted_amount: 5000, amount: 5000.25 },
],
total: { formatted_amount: 5000, amount: 5000.25, date: '2020-12-31' },
});
});
it('Should retrieve the balance sheet amounts divided on 1000.', async () => {
const res = await request()
.get('/api/financial_statements/balance_sheet')
.set('x-access-token', loginRes.body.token)
.set('organization-id', tenantWebsite.organizationId)
.query({
display_columns_by: 'quarter',
display_columns_type: 'date_periods',
from_date: '2020',
to_date: '2021',
number_format: {
divide_1000: true,
},
})
.send();
expect(res.body.accounts[0].children).include.something.deep.equals({
id: debitAccount.id,
index: debitAccount.index,
name: debitAccount.name,
code: debitAccount.code,
parentAccountId: null,
children: [],
total_periods: [
{ date: '2020-03', formatted_amount: 5, amount: 5000 },
{ date: '2020-06', formatted_amount: 5, amount: 5000 },
{ date: '2020-09', formatted_amount: 5, amount: 5000 },
{ date: '2020-12', formatted_amount: 5, amount: 5000 },
{ date: '2021-03', formatted_amount: 5, amount: 5000 },
],
total: { formatted_amount: 5, amount: 5000, date: '2021' },
});
});
it('Should not retrieve accounts has no transactions between the given date range in case query none_zero is true.', async () => {
const res = await request()
.get('/api/financial_statements/balance_sheet')
.set('x-access-token', loginRes.body.token)
.set('organization-id', tenantWebsite.organizationId)
.query({
display_columns_by: 'quarter',
from_date: '2002',
to_date: '2003',
number_format: {
divide_1000: true,
},
none_zero: true,
})
.send();
expect(res.body.accounts[0].children.length).equals(0);
expect(res.body.accounts[1].children.length).equals(0);
});
it('Should retrieve accounts in nested structure parent and children accounts.', async () => {
const childAccount = await tenantFactory.create('account', {
parent_account_id: debitAccount.id,
account_type_id: 1
});
const res = await request()
.get('/api/financial_statements/balance_sheet')
.set('x-access-token', loginRes.body.token)
.set('organization-id', tenantWebsite.organizationId)
.query({
none_zero: false,
account_ids: [childAccount.id, debitAccount.id]
})
.send();
expect(res.body.accounts[0].children).include.something.deep.equals({
id: debitAccount.id,
index: null,
name: debitAccount.name,
code: debitAccount.code,
parentAccountId: null,
total: { formatted_amount: 5000, amount: 5000, date: '2020-12-31' },
children: [
{
id: childAccount.id,
index: null,
name: childAccount.name,
code: childAccount.code,
parentAccountId: debitAccount.id,
total: { formatted_amount: 0, amount: 0, date: '2020-12-31' },
children: [],
}
]
});
});
it('Should parent account balance sumation of total balane all children accounts.', async () => {
const childAccount = await tenantFactory.create('account', {
parent_account_id: debitAccount.id,
account_type_id: 1
});
await tenantFactory.create('account_transaction', {
credit: 0, debit: 1000, account_id: childAccount.id, date: '2020-1-10'
});
const res = await request()
.get('/api/financial_statements/balance_sheet')
.set('x-access-token', loginRes.body.token)
.set('organization-id', tenantWebsite.organizationId)
.query({
none_zero: false,
account_ids: [childAccount.id, debitAccount.id]
})
.send();
expect(res.body.accounts[0].children[0].total.amount).equals(6000);
expect(res.body.accounts[0].children[0].total.formatted_amount).equals(6000);
});
it('Should parent account balance sumation of total periods amounts all children accounts.', async () => {
const childAccount = await tenantFactory.create('account', {
parent_account_id: debitAccount.id,
account_type_id: 1
});
await tenantFactory.create('account_transaction', {
credit: 0, debit: 1000, account_id: childAccount.id, date: '2020-2-10'
});
const res = await request()
.get('/api/financial_statements/balance_sheet')
.set('x-access-token', loginRes.body.token)
.set('organization-id', tenantWebsite.organizationId)
.query({
none_zero: false,
account_ids: [childAccount.id, debitAccount.id],
display_columns_type: 'date_periods',
display_columns_by: 'month',
from_date: '2020-01-01',
to_date: '2020-12-12',
})
.send();
expect(res.body.accounts[0].children[0].total_periods).deep.equals([
{ amount: 5000, formatted_amount: 5000, date: '2020-01' },
{ amount: 6000, formatted_amount: 6000, date: '2020-02' },
{ amount: 6000, formatted_amount: 6000, date: '2020-03' },
{ amount: 6000, formatted_amount: 6000, date: '2020-04' },
{ amount: 6000, formatted_amount: 6000, date: '2020-05' },
{ amount: 6000, formatted_amount: 6000, date: '2020-06' },
{ amount: 6000, formatted_amount: 6000, date: '2020-07' },
{ amount: 6000, formatted_amount: 6000, date: '2020-08' },
{ amount: 6000, formatted_amount: 6000, date: '2020-09' },
{ amount: 6000, formatted_amount: 6000, date: '2020-10' },
{ amount: 6000, formatted_amount: 6000, date: '2020-11' },
{ amount: 6000, formatted_amount: 6000, date: '2020-12' }
])
});
});
describe('routes: `/financial_statements/trial_balance`', () => { describe('routes: `/financial_statements/trial_balance`', () => {
it('Should response unauthorized in case the user was not authorized.', async () => { it('Should response unauthorized in case the user was not authorized.', async () => {
const res = await request() const res = await request()