- feat: Filter expense and payment accounts on expense form.

- feat: Make journal errors with receivable and payable accounts.
- fix: Handle database big numbers.
- fix: Indexing lines when add a new line on make journal form.
- fix: Abstruct accounts type component.
This commit is contained in:
Ahmed Bouhuolia
2020-07-06 21:22:27 +02:00
parent 3fc390652d
commit 282da55d08
40 changed files with 1031 additions and 747 deletions

View File

@@ -5,11 +5,10 @@ import { FormattedMessage as T } from 'react-intl';
export default function AccountsSelectList({ export default function AccountsSelectList({
accounts, accounts,
onAccountSelected,
error = [],
initialAccountId, initialAccountId,
selectedAccountId, selectedAccountId,
defaultSelectText = 'Select account', defaultSelectText = 'Select account',
onAccountSelected,
}) { }) {
// Find initial account object to set it as default account in initial render. // Find initial account object to set it as default account in initial render.
const initialAccount = useMemo( const initialAccount = useMemo(

View File

@@ -0,0 +1,46 @@
import React, { useCallback } from 'react';
import {
ListSelect,
} from 'components';
export default function AccountsTypesSelect({
accountsTypes,
selectedTypeId,
defaultSelectText = 'Select account type',
onTypeSelected,
...restProps
}) {
// Filters accounts types items.
const filterAccountTypeItems = (query, accountType, _index, exactMatch) => {
const normalizedTitle = accountType.name.toLowerCase();
const normalizedQuery = query.toLowerCase();
if (exactMatch) {
return normalizedTitle === normalizedQuery;
} else {
return normalizedTitle.indexOf(normalizedQuery) >= 0;
}
};
// Handle item selected.
const handleItemSelected = (accountType) => {
onTypeSelected && onTypeSelected(accountType);
};
const items = accountsTypes.map((type) => ({
id: type.id, name: type.name,
}));
return (
<ListSelect
items={items}
selectedItemProp={'id'}
selectedItem={selectedTypeId}
labelProp={'name'}
defaultText={defaultSelectText}
onItemSelect={handleItemSelected}
itemPredicate={filterAccountTypeItems}
{...restProps}
/>
);
}

View File

@@ -17,17 +17,9 @@ export default function ContactsListField({
initialContact || null initialContact || null
); );
const contactTypeLabel = (contactType) => {
switch(contactType) {
case 'customer':
return 'Customer';
case 'vendor':
return 'Vendor';
}
};
// Contact item of select accounts field. // Contact item of select accounts field.
const contactItem = useCallback((item, { handleClick, modifiers, query }) => ( const contactItem = useCallback((item, { handleClick, modifiers, query }) => (
<MenuItem text={item.display_name} label={contactTypeLabel(item.contact_type)} key={item.id} onClick={handleClick} /> <MenuItem text={item.display_name} key={item.id} onClick={handleClick} />
), []); ), []);
const onContactSelect = useCallback((contact) => { const onContactSelect = useCallback((contact) => {

View File

@@ -1,42 +1,41 @@
import React, {useCallback, useMemo} from 'react'; import React, { useCallback, useMemo } from 'react';
import AccountsSelectList from 'components/AccountsSelectList'; import AccountsSelectList from 'components/AccountsSelectList';
import classNames from 'classnames'; import classNames from 'classnames';
import { import { FormGroup, Classes, Intent } from '@blueprintjs/core';
FormGroup,
Classes,
Intent,
} from '@blueprintjs/core';
// Account cell renderer. // Account cell renderer.
const AccountCellRenderer = ({ const AccountCellRenderer = ({
column: { id }, column: { id, accountsDataProp },
row: { index, original }, row: { index, original },
cell: { value: initialValue }, cell: { value: initialValue },
payload: { accounts, updateData, errors }, payload: { accounts: defaultAccounts, updateData, errors, ...restProps },
}) => { }) => {
const handleAccountSelected = useCallback((account) => { const handleAccountSelected = useCallback(
(account) => {
updateData(index, id, account.id); updateData(index, id, account.id);
}, [updateData, index, id]); },
[updateData, index, id],
const { account_id = false } = (errors[index] || {}); );
const error = errors?.[index]?.[id];
// const initialAccount = useMemo(() =>
// accounts.find(a => a.id === initialValue),
// [accounts, initialValue]);
const accounts = useMemo(
() => restProps[accountsDataProp] || defaultAccounts,
[restProps, defaultAccounts, accountsDataProp],
);
return ( return (
<FormGroup <FormGroup
intent={account_id ? Intent.DANGER : ''} intent={error ? Intent.DANGER : null}
className={classNames( className={classNames(
'form-group--select-list', 'form-group--select-list',
'form-group--account', 'form-group--account',
Classes.FILL)} Classes.FILL,
)}
> >
<AccountsSelectList <AccountsSelectList
accounts={accounts} accounts={accounts}
onAccountSelected={handleAccountSelected} onAccountSelected={handleAccountSelected}
error={account_id} selectedAccountId={initialValue}
selectedAccountId={initialValue} /> />
</FormGroup> </FormGroup>
); );
}; };

View File

@@ -1,5 +1,5 @@
import React, { useCallback, useMemo } from 'react'; import React, { useCallback, useMemo } from 'react';
import { FormGroup, 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';
@@ -20,8 +20,11 @@ export default function ContactsListCellRenderer({
return contacts.find(c => c.id === initialValue); return contacts.find(c => c.id === initialValue);
}, [contacts, initialValue]); }, [contacts, initialValue]);
const error = errors?.[index]?.[id];
return ( return (
<FormGroup <FormGroup
intent={error ? Intent.DANGER : null}
className={classNames( className={classNames(
'form-group--select-list', 'form-group--select-list',
'form-group--contacts-list', 'form-group--contacts-list',
@@ -32,6 +35,7 @@ export default function ContactsListCellRenderer({
contacts={contacts} contacts={contacts}
onContactSelected={handleContactSelected} onContactSelected={handleContactSelected}
initialContact={initialContact} initialContact={initialContact}
/> />
</FormGroup> </FormGroup>
) )

View File

@@ -1,31 +1,35 @@
import React, {useState, useEffect} from 'react'; import React, { useState, useEffect } from 'react';
import { import classNames from 'classnames';
InputGroup import { Classes, InputGroup, FormGroup } from '@blueprintjs/core';
} from '@blueprintjs/core';
const InputEditableCell = ({ const InputEditableCell = ({
row: { index }, row: { index },
column: { id, }, column: { id },
cell: { value: initialValue }, cell: { value: initialValue },
payload, payload,
}) => { }) => {
const [value, setValue] = useState(initialValue) const [value, setValue] = useState(initialValue);
const onChange = e => { const onChange = (e) => {
setValue(e.target.value) setValue(e.target.value);
} };
const onBlur = () => { const onBlur = () => {
payload.updateData(index, id, value) payload.updateData(index, id, value);
} };
useEffect(() => { useEffect(() => {
setValue(initialValue) setValue(initialValue);
}, [initialValue]) }, [initialValue]);
return (<InputGroup return (
<FormGroup>
<InputGroup
value={value} value={value}
onChange={onChange} onChange={onChange}
onBlur={onBlur} onBlur={onBlur}
fill={true} />); fill={true}
/>
</FormGroup>
);
}; };
export default InputEditableCell; export default InputEditableCell;

View File

@@ -1,4 +1,5 @@
import React, { useCallback, useState, useEffect } from 'react'; import React, { useCallback, useState, useEffect } from 'react';
import { FormGroup, Intent } from '@blueprintjs/core';
import MoneyInputGroup from 'components/MoneyInputGroup'; import MoneyInputGroup from 'components/MoneyInputGroup';
// Input form cell renderer. // Input form cell renderer.
@@ -6,7 +7,7 @@ const MoneyFieldCellRenderer = ({
row: { index }, row: { index },
column: { id }, column: { id },
cell: { value: initialValue }, cell: { value: initialValue },
payload payload: { errors, updateData },
}) => { }) => {
const [value, setValue] = useState(initialValue); const [value, setValue] = useState(initialValue);
@@ -15,26 +16,36 @@ const MoneyFieldCellRenderer = ({
}, []); }, []);
function isNumeric(data) { function isNumeric(data) {
return !isNaN(parseFloat(data)) && isFinite(data) && data.constructor !== Array; return (
!isNaN(parseFloat(data)) && isFinite(data) && data.constructor !== Array
);
} }
const onBlur = () => { const onBlur = () => {
const updateValue = isNumeric(value) ? parseFloat(value) : value; const updateValue = isNumeric(value) ? parseFloat(value) : value;
payload.updateData(index, id, updateValue); updateData(index, id, updateValue);
}; };
useEffect(() => { useEffect(() => {
setValue(initialValue); setValue(initialValue);
}, [initialValue]) }, [initialValue]);
return (<MoneyInputGroup const error = errors?.[index]?.[id];
return (
<FormGroup
intent={error ? Intent.DANGER : null}>
<MoneyInputGroup
value={value} value={value}
prefix={'$'} prefix={'$'}
onChange={handleFieldChange} onChange={handleFieldChange}
inputGroupProps={{ inputGroupProps={{
fill: true, fill: true,
onBlur, onBlur,
}} />) }}
/>
</FormGroup>
);
}; };
export default MoneyFieldCellRenderer; export default MoneyFieldCellRenderer;

View File

@@ -33,7 +33,7 @@ export default function ListSelect ({
('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]} />); return (<MenuItem text={item[labelProp]} key={item[selectedItemProp]} onClick={handleClick} />);
}; };
return ( return (

View File

@@ -19,6 +19,7 @@ import Dialog from './Dialog';
import AppToaster from './AppToaster'; import AppToaster from './AppToaster';
import DataTable from './DataTable'; import DataTable from './DataTable';
import AccountsSelectList from './AccountsSelectList'; import AccountsSelectList from './AccountsSelectList';
import AccountsTypesSelect from './AccountsTypesSelect';
const Hint = FieldHint; const Hint = FieldHint;
@@ -45,4 +46,5 @@ export {
AppToaster, AppToaster,
DataTable, DataTable,
AccountsSelectList, AccountsSelectList,
AccountsTypesSelect,
}; };

View File

@@ -10,7 +10,7 @@ import { useFormik } from 'formik';
import moment from 'moment'; import moment from 'moment';
import { Intent } from '@blueprintjs/core'; import { Intent } from '@blueprintjs/core';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { pick } from 'lodash'; import { pick, setWith } from 'lodash';
import MakeJournalEntriesHeader from './MakeJournalEntriesHeader'; import MakeJournalEntriesHeader from './MakeJournalEntriesHeader';
import MakeJournalEntriesFooter from './MakeJournalEntriesFooter'; import MakeJournalEntriesFooter from './MakeJournalEntriesFooter';
@@ -89,19 +89,23 @@ function MakeJournalEntriesForm({
const validationSchema = Yup.object().shape({ const validationSchema = Yup.object().shape({
journal_number: Yup.string() journal_number: Yup.string()
.required() .required()
.min(1)
.max(255)
.label(formatMessage({ id: 'journal_number_' })), .label(formatMessage({ id: 'journal_number_' })),
journal_type: Yup.string() journal_type: Yup.string()
.required() .required()
.min(1)
.max(255)
.label(formatMessage({ id: 'journal_type' })), .label(formatMessage({ id: 'journal_type' })),
date: Yup.date() date: Yup.date()
.required() .required()
.label(formatMessage({ id: 'date' })), .label(formatMessage({ id: 'date' })),
reference: Yup.string(), reference: Yup.string().min(1).max(255),
description: Yup.string(), description: Yup.string().min(1).max(1024),
entries: Yup.array().of( entries: Yup.array().of(
Yup.object().shape({ Yup.object().shape({
credit: Yup.number().nullable(), credit: Yup.number().decimalScale(13).nullable(),
debit: Yup.number().nullable(), debit: Yup.number().decimalScale(13).nullable(),
account_id: Yup.number() account_id: Yup.number()
.nullable() .nullable()
.when(['credit', 'debit'], { .when(['credit', 'debit'], {
@@ -109,7 +113,7 @@ function MakeJournalEntriesForm({
then: Yup.number().required(), then: Yup.number().required(),
}), }),
contact_id: Yup.number().nullable(), contact_id: Yup.number().nullable(),
note: Yup.string().nullable(), note: Yup.string().max(255).nullable(),
}), }),
), ),
}); });
@@ -180,48 +184,66 @@ function MakeJournalEntriesForm({
}, [manualJournal]); }, [manualJournal]);
// Transform API errors in toasts messages. // Transform API errors in toasts messages.
const transformErrors = (errors, { setErrors }) => { const transformErrors = (resErrors, { setErrors, errors }) => {
const hasError = (errorType) => errors.some((e) => e.type === errorType); const getError = (errorType) => resErrors.find((e) => e.type === errorType);
const toastMessages = [];
let error;
let newErrors = { ...errors, entries: [] };
if (hasError(ERROR.CUSTOMERS_NOT_WITH_RECEVIABLE_ACC)) { const setEntriesErrors = (indexes, prop, message) =>
AppToaster.show({ indexes.forEach((i) => {
message: formatMessage({ const index = Math.max(i - 1, 0);
id: 'customers_should_assign_with_receivable_account_only', newErrors = setWith(newErrors, `entries.[${index}].${prop}`, message);
}),
intent: Intent.DANGER,
}); });
}
if (hasError(ERROR.PAYABLE_ENTRIES_HAS_NO_VENDORS)) { if ((error = getError(ERROR.PAYABLE_ENTRIES_HAS_NO_VENDORS))) {
AppToaster.show({ toastMessages.push(
message: formatMessage({ formatMessage({
id: 'vendors_should_assign_with_payable_account_only', id: 'vendors_should_selected_with_payable_account_only',
}), }),
intent: Intent.DANGER, );
}); setEntriesErrors(error.indexes, 'contact_id', 'error');
} }
if (hasError(ERROR.RECEIVABLE_ENTRIES_HAS_NO_CUSTOMERS)) { if ((error = getError(ERROR.RECEIVABLE_ENTRIES_HAS_NO_CUSTOMERS))) {
AppToaster.show({ toastMessages.push(
message: formatMessage({ formatMessage({
id: 'entries_with_receivable_account_no_assigned_with_customers', id: 'should_select_customers_with_entries_have_receivable_account',
}), }),
intent: Intent.DANGER, );
}); setEntriesErrors(error.indexes, 'contact_id', 'error');
} }
if (hasError(ERROR.PAYABLE_ENTRIES_HAS_NO_VENDORS)) { if ((error = getError(ERROR.CUSTOMERS_NOT_WITH_RECEVIABLE_ACC))) {
AppToaster.show({ toastMessages.push(
message: formatMessage({ formatMessage({
id: 'entries_with_payable_account_no_assigned_with_vendors', id: 'customers_should_selected_with_receivable_account_only',
}), }),
intent: Intent.DANGER, );
}); setEntriesErrors(error.indexes, 'account_id', 'error');
} }
if (hasError(ERROR.JOURNAL_NUMBER_ALREADY_EXISTS)) { if ((error = getError(ERROR.VENDORS_NOT_WITH_PAYABLE_ACCOUNT))) {
setErrors({ toastMessages.push(
journal_number: formatMessage({ formatMessage({
id: 'vendors_should_selected_with_payable_account_only',
}),
);
setEntriesErrors(error.indexes, 'account_id', 'error');
}
if ((error = getError(ERROR.JOURNAL_NUMBER_ALREADY_EXISTS))) {
newErrors = setWith(
newErrors,
'journal_number',
formatMessage({
id: 'journal_number_is_already_used', id: 'journal_number_is_already_used',
}), }),
}); );
} }
setErrors({ ...newErrors });
AppToaster.show({
message: toastMessages.map((message) => {
return <div>- {message}</div>;
}),
intent: Intent.DANGER,
});
}; };
const formik = useFormik({ const formik = useFormik({
@@ -255,7 +277,7 @@ function MakeJournalEntriesForm({
} else if (totalCredit === 0 || totalDebit === 0) { } else if (totalCredit === 0 || totalDebit === 0) {
AppToaster.show({ AppToaster.show({
message: formatMessage({ message: formatMessage({
id: 'should_total_of_credit_and_debit_be_bigger_then_zero', id: 'amount_cannot_be_zero_or_empty',
}), }),
intent: Intent.DANGER, intent: Intent.DANGER,
}); });
@@ -353,7 +375,10 @@ function MakeJournalEntriesForm({
// Handle click on add a new line/row. // Handle click on add a new line/row.
const handleClickAddNewRow = () => { const handleClickAddNewRow = () => {
formik.setFieldValue('entries', [...formik.values.entries, defaultEntry]); formik.setFieldValue(
'entries',
reorderingEntriesIndex([...formik.values.entries, defaultEntry]),
);
}; };
// Handle click `Clear all lines` button. // Handle click `Clear all lines` button.
@@ -370,13 +395,12 @@ function MakeJournalEntriesForm({
<MakeJournalEntriesHeader formik={formik} /> <MakeJournalEntriesHeader formik={formik} />
<MakeJournalEntriesTable <MakeJournalEntriesTable
values={formik.values} values={formik.values.entries}
formik={formik} formik={formik}
defaultRow={defaultEntry} defaultRow={defaultEntry}
onClickClearAllLines={handleClickClearLines} onClickClearAllLines={handleClickClearLines}
onClickAddNewRow={handleClickAddNewRow} onClickAddNewRow={handleClickAddNewRow}
/> />
<MakeJournalEntriesFooter <MakeJournalEntriesFooter
formik={formik} formik={formik}
onSubmitClick={handleSubmitClick} onSubmitClick={handleSubmitClick}

View File

@@ -109,7 +109,7 @@ function MakeJournalEntriesTable({
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
useEffect(() => { useEffect(() => {
setRows([...values.entries.map((e) => ({ ...e, rowType: 'editor' }))]); setRows([...values.map((e) => ({ ...e, rowType: 'editor' }))]);
}, [values, setRows]); }, [values, setRows]);
// Final table rows editor rows and total and final blank row. // Final table rows editor rows and total and final blank row.
@@ -217,6 +217,9 @@ function MakeJournalEntriesTable({
const handleRemoveRow = useCallback( const handleRemoveRow = useCallback(
(rowIndex) => { (rowIndex) => {
// Can't continue if there is just one row line or less.
if (rows.length <= 2) { return; }
const removeIndex = parseInt(rowIndex, 10); const removeIndex = parseInt(rowIndex, 10);
const newRows = rows.filter((row, index) => index !== removeIndex); const newRows = rows.filter((row, index) => index !== removeIndex);

View File

@@ -28,7 +28,7 @@ import withManualJournalsActions from 'containers/Accounting/withManualJournalsA
function StatusAccessor(row) { function StatusAccessor(row) {
return ( return (
<Choose> <Choose>
<Choose.When condition={row.status}> <Choose.When condition={!!row.status}>
<Tag minimal={true}> <Tag minimal={true}>
<T id={'published'} /> <T id={'published'} />
</Tag> </Tag>
@@ -178,7 +178,7 @@ function ManualJournalsDataTable({
{ {
id: 'status', id: 'status',
Header: formatMessage({ id: 'status' }), Header: formatMessage({ id: 'status' }),
accessor: StatusAccessor, accessor: row => StatusAccessor(row),
width: 95, width: 95,
className: 'status', className: 'status',
}, },

View File

@@ -306,7 +306,7 @@ function AccountsChart({
setBulkInactiveAccounts(false); setBulkInactiveAccounts(false);
AppToaster.show({ AppToaster.show({
message: formatMessage({ message: formatMessage({
id: 'the_accounts_has_been_successfully_inactivated', id: 'the_accounts_have_been_successfully_inactivated',
}), }),
intent: Intent.SUCCESS, intent: Intent.SUCCESS,
}); });

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useMemo } from 'react'; import React, { useCallback, useMemo, useEffect } from 'react';
import { import {
Button, Button,
Classes, Classes,
@@ -6,24 +6,24 @@ import {
InputGroup, InputGroup,
Intent, Intent,
TextArea, TextArea,
MenuItem,
Checkbox, Checkbox,
Position, Position,
} from '@blueprintjs/core'; } from '@blueprintjs/core';
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 { If } from 'components'; import { pick } from 'lodash';
import { omit, pick } 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 { import {
ListSelect, If,
ErrorMessage, ErrorMessage,
Dialog, Dialog,
AppToaster, AppToaster,
FieldRequiredHint, FieldRequiredHint,
Hint, Hint,
AccountsSelectList,
AccountsTypesSelect,
} from 'components'; } from 'components';
import AccountFormDialogContainer from 'containers/Dialogs/AccountFormDialog.container'; import AccountFormDialogContainer from 'containers/Dialogs/AccountFormDialog.container';
@@ -53,33 +53,33 @@ function AccountFormDialog({
closeDialog, closeDialog,
}) { }) {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const accountFormValidationSchema = Yup.object().shape({ const validationSchema = Yup.object().shape({
name: Yup.string() name: Yup.string()
.required() .required()
.min(3)
.max(255)
.label(formatMessage({ id: 'account_name_' })), .label(formatMessage({ id: 'account_name_' })),
code: Yup.number(), code: Yup.string().digits().min(3).max(6),
account_type_id: Yup.string() account_type_id: Yup.number()
.nullable()
.required() .required()
.label(formatMessage({ id: 'account_type_id' })), .label(formatMessage({ id: 'account_type_id' })),
description: Yup.string().nullable().trim(), description: Yup.string().min(3).max(512).nullable().trim(),
parent_account_id: Yup.string().nullable(), parent_account_id: Yup.number().nullable(),
}); });
const initialValues = useMemo( const initialValues = useMemo(
() => ({ () => ({
account_type_id: null, account_type_id: null,
name: '', name: '',
description: '',
code: '', code: '',
type: '', description: '',
parent_account_id: null,
}), }),
[], [],
); );
const transformApiErrors = (errors) => { const transformApiErrors = (errors) => {
const fields = {}; const fields = {};
if (errors.find((e) => e.type === 'NOT_UNIQUE_CODE')) { if (errors.find((e) => e.type === 'NOT_UNIQUE_CODE')) {
fields.code = 'Account code is not unqiue.'; fields.code = formatMessage({ id: 'account_code_is_not_unique' });
} }
return fields; return fields;
}; };
@@ -98,20 +98,33 @@ function AccountFormDialog({
enableReinitialize: true, enableReinitialize: true,
initialValues: { initialValues: {
...initialValues, ...initialValues,
...(payload.action === 'edit' && pick(account, Object.keys(initialValues))), ...(payload.action === 'edit' &&
pick(account, Object.keys(initialValues))),
}, },
validationSchema: accountFormValidationSchema, validationSchema,
onSubmit: (values, { setSubmitting, setErrors }) => { onSubmit: (values, { setSubmitting, setErrors }) => {
const exclude = ['subaccount']; const form = pick(values, Object.keys(initialValues));
const toastAccountName = values.code const toastAccountName = values.code
? `${values.code} - ${values.name}` ? `${values.code} - ${values.name}`
: values.name; : values.name;
if (payload.action === 'edit') { const afterSubmit = () => {
requestEditAccount(payload.id, values)
.then((response) => {
closeDialog(dialogName); closeDialog(dialogName);
queryCache.invalidateQueries('accounts-table'); queryCache.invalidateQueries('accounts-table');
queryCache.invalidateQueries('accounts-list');
};
const afterErrors = (errors) => {
const errorsTransformed = transformApiErrors(errors);
setErrors({ ...errorsTransformed });
setSubmitting(false);
};
if (payload.action === 'edit') {
requestEditAccount(payload.id, form)
.then((response) => {
afterSubmit(response);
AppToaster.show({ AppToaster.show({
message: formatMessage( message: formatMessage(
@@ -124,19 +137,11 @@ function AccountFormDialog({
intent: Intent.SUCCESS, intent: Intent.SUCCESS,
}); });
}) })
.catch((errors) => { .catch(afterErrors);
const errorsTransformed = transformApiErrors(errors);
setErrors({ ...errorsTransformed });
setSubmitting(false);
});
} else { } else {
requestSubmitAccount({ requestSubmitAccount({ form })
payload: payload.parent_account_id,
form: { ...omit(values, exclude) },
})
.then((response) => { .then((response) => {
closeDialog(dialogName); afterSubmit(response);
queryCache.invalidateQueries('accounts-table', { force: true });
AppToaster.show({ AppToaster.show({
message: formatMessage( message: formatMessage(
@@ -150,70 +155,28 @@ function AccountFormDialog({
position: Position.BOTTOM, position: Position.BOTTOM,
}); });
}) })
.catch((errors) => { .catch(afterErrors);
const errorsTransformed = transformApiErrors(errors);
setErrors({ ...errorsTransformed });
setSubmitting(false);
});
} }
}, },
}); });
useEffect(() => {
if (values.parent_account_id) {
setFieldValue('subaccount', true);
}
}, [values.parent_account_id]);
// Filtered accounts based on the given account type. // Filtered accounts based on the given account type.
const filteredAccounts = useMemo( const filteredAccounts = useMemo(
() => () =>
accounts.filter( accounts.filter(
(account) => account.account_type_id === values.account_type_id, (account) =>
account.account_type_id === values.account_type_id ||
!values.account_type_id,
), ),
[accounts, values.account_type_id], [accounts, values.account_type_id],
); );
// Filters accounts types items.
const filterAccountTypeItems = (query, accountType, _index, exactMatch) => {
const normalizedTitle = accountType.name.toLowerCase();
const normalizedQuery = query.toLowerCase();
if (exactMatch) {
return normalizedTitle === normalizedQuery;
} else {
return normalizedTitle.indexOf(normalizedQuery) >= 0;
}
};
// Account type item of select filed.
const accountTypeItem = (item, { handleClick, modifiers, query }) => {
return <MenuItem text={item.name} key={item.id} onClick={handleClick} />;
};
// Account item of select accounts field.
const accountItem = (item, { handleClick, modifiers, query }) => {
return (
<MenuItem
text={item.name}
label={item.code}
key={item.id}
onClick={handleClick}
/>
);
};
// Filters accounts items.
const filterAccountsPredicater = useCallback(
(query, account, _index, exactMatch) => {
const normalizedTitle = account.name.toLowerCase();
const normalizedQuery = query.toLowerCase();
if (exactMatch) {
return normalizedTitle === normalizedQuery;
} else {
return (
`${account.code} ${normalizedTitle}`.indexOf(normalizedQuery) >= 0
);
}
},
[],
);
// Handles dialog close. // Handles dialog close.
const handleClose = useCallback(() => { const handleClose = useCallback(() => {
closeDialog(dialogName); closeDialog(dialogName);
@@ -256,12 +219,12 @@ function AccountFormDialog({
fetchAccount.refetch(); fetchAccount.refetch();
} }
if (payload.action === 'new_child') { if (payload.action === 'new_child') {
setFieldValue('subaccount', true);
setFieldValue('parent_account_id', payload.parentAccountId); setFieldValue('parent_account_id', payload.parentAccountId);
setFieldValue('account_type_id', payload.accountTypeId); setFieldValue('account_type_id', payload.accountTypeId);
} }
}, [fetchAccount, fetchAccountsList, fetchAccountsTypes]); }, [payload, fetchAccount, fetchAccountsList, fetchAccountsTypes]);
// Handle account type change.
const onChangeAccountType = useCallback( const onChangeAccountType = useCallback(
(accountType) => { (accountType) => {
setFieldValue('account_type_id', accountType.id); setFieldValue('account_type_id', accountType.id);
@@ -277,21 +240,11 @@ function AccountFormDialog({
[setFieldValue], [setFieldValue],
); );
// Handle dialog on closed.
const onDialogClosed = useCallback(() => { const onDialogClosed = useCallback(() => {
resetForm(); resetForm();
}, [resetForm]); }, [resetForm]);
const subAccountLabel = useMemo(() => {
return (
<span>
<T id={'sub_account'} />
<Hint />
</span>
);
}, []);
const requiredSpan = useMemo(() => <span class="required">*</span>, []);
return ( return (
<Dialog <Dialog
name={dialogName} name={dialogName}
@@ -332,18 +285,13 @@ function AccountFormDialog({
errors.account_type_id && touched.account_type_id && Intent.DANGER errors.account_type_id && touched.account_type_id && Intent.DANGER
} }
> >
<ListSelect <AccountsTypesSelect
items={accountsTypes} accountsTypes={accountsTypes}
noResults={<MenuItem disabled={true} text="No results." />} selectedTypeId={values.account_type_id}
itemRenderer={accountTypeItem} defaultSelectText={<T id={'select_account_type'} />}
itemPredicate={filterAccountTypeItems} onTypeSelected={onChangeAccountType}
popoverProps={{ minimal: true }}
onItemSelect={onChangeAccountType}
selectedItem={values.account_type_id}
selectedItemProp={'id'}
defaultText={<T id={'select_account_type'} />}
labelProp={'name'}
buttonProps={{ disabled: payload.action === 'edit' }} buttonProps={{ disabled: payload.action === 'edit' }}
popoverProps={{ minimal: true }}
/> />
</FormGroup> </FormGroup>
@@ -384,7 +332,12 @@ function AccountFormDialog({
> >
<Checkbox <Checkbox
inline={true} inline={true}
label={subAccountLabel} label={
<>
<T id={'sub_account'} />
<Hint />
</>
}
{...getFieldProps('subaccount')} {...getFieldProps('subaccount')}
checked={values.subaccount} checked={values.subaccount}
/> />
@@ -400,17 +353,11 @@ function AccountFormDialog({
)} )}
inline={true} inline={true}
> >
<ListSelect <AccountsSelectList
items={filteredAccounts} accounts={filteredAccounts}
noResults={<MenuItem disabled={true} text="No results." />} onAccountSelected={onChangeSubaccount}
itemRenderer={accountItem} defaultSelectText={<T id={'select_parent_account'} />}
itemPredicate={filterAccountsPredicater} selectedAccountId={values.parent_account_id}
popoverProps={{ minimal: true }}
onItemSelect={onChangeSubaccount}
selectedItem={values.parent_account_id}
selectedItemProp={'id'}
defaultText={<T id={'select_parent_account'} />}
labelProp={'name'}
/> />
</FormGroup> </FormGroup>
</If> </If>
@@ -424,7 +371,7 @@ function AccountFormDialog({
> >
<TextArea <TextArea
growVertically={true} growVertically={true}
large={true} height={280}
{...getFieldProps('description')} {...getFieldProps('description')}
/> />
</FormGroup> </FormGroup>

View File

@@ -154,7 +154,7 @@ function ExpensesDataTable({
{ {
id: 'payment_date', id: 'payment_date',
Header: formatMessage({ id: 'payment_date' }), Header: formatMessage({ id: 'payment_date' }),
accessor: () => moment().format('YYYY MMM DD'), accessor: (r) => moment(r.payment_date).format('YYYY MMM DD'),
width: 140, width: 140,
className: 'payment_date', className: 'payment_date',
}, },

View File

@@ -12,8 +12,8 @@ export default function ExpenseFloatingFooter({
return ( return (
<div className={'form__floating-footer'}> <div className={'form__floating-footer'}>
<Button <Button
intent={Intent.PRIMARY}
disabled={isSubmitting} disabled={isSubmitting}
intent={Intent.PRIMARY}
type="submit" type="submit"
onClick={() => { onClick={() => {
onSubmitClick({ publish: true, redirect: true }); onSubmitClick({ publish: true, redirect: true });

View File

@@ -28,6 +28,8 @@ import Dragzone from 'components/Dragzone';
import useMedia from 'hooks/useMedia'; import useMedia from 'hooks/useMedia';
import { compose, repeatValue } from 'utils'; import { compose, repeatValue } from 'utils';
const MIN_LINES_NUMBER = 4;
function ExpenseForm({ function ExpenseForm({
// #withMedia // #withMedia
requestSubmitMedia, requestSubmitMedia,
@@ -81,22 +83,26 @@ function ExpenseForm({
const validationSchema = Yup.object().shape({ const validationSchema = Yup.object().shape({
beneficiary: Yup.string().label(formatMessage({ id: 'beneficiary' })), beneficiary: Yup.string().label(formatMessage({ id: 'beneficiary' })),
payment_account_id: Yup.string() payment_account_id: Yup.number()
.required() .required()
.label(formatMessage({ id: 'payment_account_' })), .label(formatMessage({ id: 'payment_account_' })),
payment_date: Yup.date() payment_date: Yup.date()
.required() .required()
.label(formatMessage({ id: 'payment_date_' })), .label(formatMessage({ id: 'payment_date_' })),
reference_no: Yup.string(), reference_no: Yup.string().min(1).max(255),
currency_code: Yup.string().label(formatMessage({ id: 'currency_code' })), currency_code: Yup.string()
.nullable()
.label(formatMessage({ id: 'currency_code' })),
description: Yup.string() description: Yup.string()
.trim() .trim()
.min(1)
.max(1024)
.label(formatMessage({ id: 'description' })), .label(formatMessage({ id: 'description' })),
publish: Yup.boolean().label(formatMessage({ id: 'publish' })), publish: Yup.boolean().label(formatMessage({ id: 'publish' })),
categories: Yup.array().of( categories: Yup.array().of(
Yup.object().shape({ Yup.object().shape({
index: Yup.number().nullable(), index: Yup.number().min(1).max(1000).nullable(),
amount: Yup.number().nullable(), amount: Yup.number().decimalScale(13).nullable(),
expense_account_id: Yup.number() expense_account_id: Yup.number()
.nullable() .nullable()
.when(['amount'], { .when(['amount'], {
@@ -134,9 +140,7 @@ function ExpenseForm({
description: '', description: '',
reference_no: '', reference_no: '',
currency_code: '', currency_code: '',
categories: [ categories: [...repeatValue(defaultCategory, MIN_LINES_NUMBER)],
...repeatValue(defaultCategory, 4),
],
}), }),
[defaultCategory], [defaultCategory],
); );
@@ -153,9 +157,15 @@ function ExpenseForm({
...(expense ...(expense
? { ? {
...pick(expense, Object.keys(defaultInitialValues)), ...pick(expense, Object.keys(defaultInitialValues)),
categories: expense.categories.map((category) => ({ categories: [
...expense.categories.map((category) => ({
...pick(category, Object.keys(defaultCategory)), ...pick(category, Object.keys(defaultCategory)),
})), })),
...repeatValue(
defaultCategory,
Math.max(MIN_LINES_NUMBER - expense.categories.length, 0),
),
],
} }
: { : {
...defaultInitialValues, ...defaultInitialValues,
@@ -184,6 +194,20 @@ function ExpenseForm({
...initialValues, ...initialValues,
}, },
onSubmit: async (values, { setSubmitting, setErrors, resetForm }) => { onSubmit: async (values, { setSubmitting, setErrors, resetForm }) => {
setSubmitting(true);
const totalAmount = values.categories.reduce((total, item) => {
return total + item.amount;
}, 0);
if (totalAmount <= 0) {
AppToaster.show({
message: formatMessage({
id: 'amount_cannot_be_zero_or_empty',
}),
intent: Intent.DANGER,
});
return;
}
const categories = values.categories.filter( const categories = values.categories.filter(
(category) => (category) =>
category.amount && category.index && category.expense_account_id, category.amount && category.index && category.expense_account_id,
@@ -193,7 +217,6 @@ function ExpenseForm({
publish: payload.publish, publish: payload.publish,
categories, categories,
}; };
const saveExpense = (mdeiaIds) => const saveExpense = (mdeiaIds) =>
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
const requestForm = { ...form, media_ids: mdeiaIds }; const requestForm = { ...form, media_ids: mdeiaIds };
@@ -237,11 +260,9 @@ function ExpenseForm({
intent: Intent.SUCCESS, intent: Intent.SUCCESS,
}); });
setSubmitting(false); setSubmitting(false);
formik.resetForm(); resetForm();
saveInvokeSubmit({ action: 'new', ...payload }); saveInvokeSubmit({ action: 'new', ...payload });
clearSavedMediaIds(); clearSavedMediaIds();
// resolve(response);
}) })
.catch((errors) => { .catch((errors) => {
setSubmitting(false); setSubmitting(false);
@@ -298,11 +319,9 @@ function ExpenseForm({
const handleClearAllLines = () => { const handleClearAllLines = () => {
formik.setFieldValue( formik.setFieldValue(
'categories', 'categories',
orderingCategoriesIndex([ orderingCategoriesIndex([...repeatValue(defaultCategory, MIN_LINES_NUMBER)]),
...repeatValue(defaultCategory, 4),
]),
); );
} };
return ( return (
<div className={'expense-form'}> <div className={'expense-form'}>
@@ -324,7 +343,6 @@ function ExpenseForm({
> >
<TextArea <TextArea
growVertically={true} growVertically={true}
large={true}
{...formik.getFieldProps('description')} {...formik.getFieldProps('description')}
/> />
</FormGroup> </FormGroup>

View File

@@ -61,36 +61,6 @@ function ExpenseFormHeader({
} }
}; };
// Account item of select accounts field.
const accountItem = (item, { handleClick }) => {
return (
<MenuItem
key={item.id}
text={item.name}
label={item.code}
onClick={handleClick}
/>
);
};
// Filters accounts items.
// @filter accounts predicator resauble
const filterAccountsPredicater = useCallback(
(query, account, _index, exactMatch) => {
const normalizedTitle = account.name.toLowerCase();
const normalizedQuery = query.toLowerCase();
if (exactMatch) {
return normalizedTitle === normalizedQuery;
} else {
return (
`${account.code} ${normalizedTitle}`.indexOf(normalizedQuery) >= 0
);
}
},
[],
);
// Handles change account. // Handles change account.
const onChangeAccount = useCallback( const onChangeAccount = useCallback(
(account) => { (account) => {
@@ -112,6 +82,12 @@ function ExpenseFormHeader({
[setFieldValue, selectedItems], [setFieldValue, selectedItems],
); );
// Filter payment accounts.
const paymentAccounts = useMemo(
() => accountsList.filter(a => a?.type?.key === 'current_asset'),
[accountsList],
);
return ( return (
<div className={'dashboard__insider--expense-form__header'}> <div className={'dashboard__insider--expense-form__header'}>
<Row> <Row>
@@ -165,7 +141,7 @@ function ExpenseFormHeader({
} }
> >
<AccountsSelectList <AccountsSelectList
accounts={accountsList} accounts={paymentAccounts}
onAccountSelected={onChangeAccount} onAccountSelected={onChangeAccount}
defaultSelectText={<T id={'select_payment_account'} />} defaultSelectText={<T id={'select_payment_account'} />}
selectedAccountId={values.payment_account_id} selectedAccountId={values.payment_account_id}

View File

@@ -14,7 +14,6 @@ import {
} from 'components/DataTableCells'; } from 'components/DataTableCells';
import withAccounts from 'containers/Accounts/withAccounts'; import withAccounts from 'containers/Accounts/withAccounts';
const ExpenseCategoryHeaderCell = () => { const ExpenseCategoryHeaderCell = () => {
return ( return (
<> <>
@@ -22,10 +21,10 @@ const ExpenseCategoryHeaderCell = () => {
<Hint /> <Hint />
</> </>
); );
} };
// Actions cell renderer. // Actions cell renderer.
const ActionsCellRenderer = ({ const ActionsCellRenderer = ({
row: { index }, row: { index },
column: { id }, column: { id },
cell: { value: initialValue }, cell: { value: initialValue },
@@ -101,16 +100,11 @@ function ExpenseTable({
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
useEffect(() => { useEffect(() => {
setRows([ setRows([...categories.map((e) => ({ ...e, rowType: 'editor' }))]);
...categories.map((e) => ({ ...e, rowType: 'editor' })),
]);
}, [categories]); }, [categories]);
// Final table rows editor rows and total and final blank row. // Final table rows editor rows and total and final blank row.
const tableRows = useMemo( const tableRows = useMemo(() => [...rows, { rowType: 'total' }], [rows]);
() => [...rows, { rowType: 'total' }],
[rows],
);
// Memorized data table columns. // Memorized data table columns.
const columns = useMemo( const columns = useMemo(
@@ -133,6 +127,7 @@ function ExpenseTable({
disableSortBy: true, disableSortBy: true,
disableResizing: true, disableResizing: true,
width: 250, width: 250,
accountsDataProp: 'expenseAccounts',
}, },
{ {
Header: formatMessage({ id: 'amount_currency' }, { currency: 'USD' }), Header: formatMessage({ id: 'amount_currency' }, { currency: 'USD' }),
@@ -187,6 +182,11 @@ function ExpenseTable({
// Handles click remove datatable row. // Handles click remove datatable row.
const handleRemoveRow = useCallback( const handleRemoveRow = useCallback(
(rowIndex) => { (rowIndex) => {
// Can't continue if there is just one row line or less.
if (rows.length <= 1) {
return;
}
const removeIndex = parseInt(rowIndex, 10); const removeIndex = parseInt(rowIndex, 10);
const newRows = rows.filter((row, index) => index !== removeIndex); const newRows = rows.filter((row, index) => index !== removeIndex);
@@ -220,6 +220,12 @@ function ExpenseTable({
[rows], [rows],
); );
// Filter expense accounts.
const expenseAccounts = useMemo(
() => accountsList.filter((a) => a?.type?.root_type === 'expense'),
[accountsList],
);
return ( return (
<div className={'dashboard__insider--expense-form__table'}> <div className={'dashboard__insider--expense-form__table'}>
<DataTable <DataTable
@@ -229,6 +235,7 @@ function ExpenseTable({
sticky={true} sticky={true}
payload={{ payload={{
accounts: accountsList, accounts: accountsList,
expenseAccounts,
errors: errors.categories || [], errors: errors.categories || [],
updateData: handleUpdateData, updateData: handleUpdateData,
removeRow: handleRemoveRow, removeRow: handleRemoveRow,

View File

@@ -109,7 +109,7 @@ function ExpensesList({
.then(() => { .then(() => {
AppToaster.show({ AppToaster.show({
message: formatMessage( message: formatMessage(
{ id: 'the_expenses_has_been_successfully_deleted' }, { id: 'the_expenses_have_been_successfully_deleted' },
{ count: selectedRowsCount }, { count: selectedRowsCount },
), ),
intent: Intent.SUCCESS, intent: Intent.SUCCESS,
@@ -160,6 +160,7 @@ function ExpensesList({
requestPublishExpense(expense.id).then(() => { requestPublishExpense(expense.id).then(() => {
AppToaster.show({ AppToaster.show({
message: formatMessage({ id: 'the_expense_id_has_been_published' }), message: formatMessage({ id: 'the_expense_id_has_been_published' }),
intent: Intent.SUCCESS,
}); });
}); });
fetchExpenses.refetch(); fetchExpenses.refetch();

View File

@@ -5,7 +5,7 @@ export default () => {
const getExpenseById = getExpenseByIdFactory(); const getExpenseById = getExpenseByIdFactory();
const mapStateToProps = (state, props) => ({ const mapStateToProps = (state, props) => ({
expenseDetail: getExpenseById(state, props), expense: getExpenseById(state, props),
}); });
return connect(mapStateToProps); return connect(mapStateToProps);
}; };

View File

@@ -436,11 +436,10 @@ export default {
'The expense #{number} has been successfully edited.', 'The expense #{number} has been successfully edited.',
the_expense_has_been_successfully_deleted: the_expense_has_been_successfully_deleted:
'The expense has been successfully deleted', 'The expense has been successfully deleted',
the_expenses_has_been_successfully_deleted: the_expenses_have_been_successfully_deleted:
'The expenses has been successfully deleted', 'The expenses #{number} have been successfully deleted',
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_id_has_been_published: 'The expense id has been published', the_expense_id_has_been_published: 'The expense id has been published',
select_beneficiary_account: 'Select Beneficiary Account', select_beneficiary_account: 'Select Beneficiary Account',
total_amount_equals_zero: 'Total amount equals zero', total_amount_equals_zero: 'Total amount equals zero',
@@ -529,4 +528,13 @@ export default {
logic_expression: 'logic expression', logic_expression: 'logic expression',
assign_to_customer: 'Assign to Customer', assign_to_customer: 'Assign to Customer',
inactive: 'Inactive', inactive: 'Inactive',
should_select_customers_with_entries_have_receivable_account: 'Should select customers with entries that have receivable account.',
should_select_vendors_with_entries_have_payable_account: 'Should select vendors with entries that have payable account.',
vendors_should_selected_with_payable_account_only: 'Vendors contacts should selected with payable account only.',
customers_should_selected_with_receivable_account_only: 'Customers contacts should selected with receivable account only.',
amount_cannot_be_zero_or_empty: 'Amount cannot be zero or empty.',
should_total_of_credit_and_debit_be_equal: 'Should total of credit and debit be equal.',
no_accounts: 'No Accounts',
the_accounts_have_been_successfully_inactivated: 'The accounts have been successfully inactivated.',
account_code_is_not_unique: 'Account code is not unqiue.'
}; };

View File

@@ -0,0 +1,23 @@
import * as Yup from 'yup';
Yup.addMethod(Yup.string, 'digits', function () {
return this.test(
'is-digits',
'${path} should be digits only.',
value => /^(0|[1-9]\d*)$/.test(value),
);
});
Yup.addMethod(Yup.number, 'decimalScale', function(scale) {
return this.test(
'numeric-length',
'${path} should decimal length ',
(value) => {
const reg = new RegExp(/^(?:\d{1,13}|(?!.{15})\d+\.\d+)$/);
return reg.test(value);
},
);
})
export default Yup;

View File

@@ -262,7 +262,9 @@ export const fetchAccount = ({ id }) => {
.then((response) => { .then((response) => {
dispatch({ dispatch({
type: t.ACCOUNT_SET, type: t.ACCOUNT_SET,
payload: {
account: response.data.account, account: response.data.account,
}
}); });
resolve(response); resolve(response);
}) })

View File

@@ -51,7 +51,11 @@ const accountsReducer = createReducer(initialState, {
}, },
[t.ACCOUNT_SET]: (state, action) => { [t.ACCOUNT_SET]: (state, action) => {
state.accountsById[action.account.id] = action.account; const { account } = action.payload;
state.items[account.id] = {
...(state.items[account.id] || {}),
...account,
};
}, },
[t.ACCOUNT_DELETE]: (state, action) => { [t.ACCOUNT_DELETE]: (state, action) => {

View File

@@ -8,7 +8,7 @@ const initialState = {
views: {}, views: {},
loading: false, loading: false,
tableQuery: { tableQuery: {
page_size: 4, page_size: 12,
page: 1, page: 1,
}, },
currentViewId: -1, currentViewId: -1,

View File

@@ -7,8 +7,7 @@ $form-check-input-indeterminate-bg-image: url("data:image/svg+xml,<svg xmlns='ht
&__floating-footer{ &__floating-footer{
position: fixed; position: fixed;
bottom: 0; bottom: 0;
left: 220px; width: 100%;
right: 0;
background: #fff; background: #fff;
padding: 14px 18px; padding: 14px 18px;
border-top: 1px solid #ececec; border-top: 1px solid #ececec;

View File

@@ -18,6 +18,7 @@
.account_name{ .account_name{
> div{ > div{
width: 100%; width: 100%;
font-weight: 500;
} }
.bp3-popover-wrapper--inactive-semafro{ .bp3-popover-wrapper--inactive-semafro{
@@ -79,13 +80,13 @@
.bp3-form-content{ .bp3-form-content{
width: 280px; width: 280px;
}
textarea{ textarea{
min-width: 100%; min-width: 100%;
max-width: 100%; max-width: 100%;
width: 100%; width: 100%;
max-height: 120px; min-height: 60px;
}
} }
} }
} }
@@ -106,5 +107,10 @@
margin-left: 2px; margin-left: 2px;
} }
} }
.form-group--description{
textarea{
padding: 6px;
}
}
} }
} }

View File

@@ -6,7 +6,7 @@
&:before{ &:before{
content: ""; content: "";
height: 2px; height: 1px;
background: #01194e; background: #01194e;
position: fixed; position: fixed;
top: 0; top: 0;
@@ -203,6 +203,7 @@
align-items: center; align-items: center;
margin-left: 1rem; margin-left: 1rem;
} }
&__title{ &__title{
align-items: center;; align-items: center;;
display: flex; display: flex;
@@ -243,6 +244,9 @@
&__subtitle{ &__subtitle{
} }
&__insider{
margin-bottom: 40px;
}
&-content{ &-content{
display: flex; display: flex;

View File

@@ -102,19 +102,16 @@
} }
} }
.tr { .tr {
.bp3-input, .bp3-form-group .bp3-input,
.form-group--select-list .bp3-button { .form-group--select-list .bp3-button {
border-color: #e5e5e5;
border-radius: 3px; border-radius: 3px;
padding-left: 8px; padding-left: 8px;
padding-right: 8px; padding-right: 8px;
} }
.form-group--select-list {
&.bp3-intent-danger { .bp3-form-group:not(.bp3-intent-danger) .bp3-input,
.bp3-button:not(.bp3-minimal) { .form-group--select-list:not(.bp3-intent-danger) .bp3-button {
border-color: #efa8a8; border-color: #E5E5E5;
}
}
} }
&:last-of-type { &:last-of-type {
@@ -155,6 +152,12 @@
.td { .td {
border-bottom: 1px dotted #999; border-bottom: 1px dotted #999;
&.description{
.bp3-form-group{
width: 100%;
}
}
} }
.actions.td { .actions.td {
@@ -217,7 +220,7 @@
font-weight: 600; font-weight: 600;
} }
.description{ .td.description{
.bp3-icon{ .bp3-icon{
color: #666; color: #666;
} }

View File

@@ -1,6 +1,6 @@
.make-journal-entries{ .make-journal-entries{
padding-bottom: 80px; padding-bottom: 20px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -73,7 +73,7 @@
} }
} }
.tr{ .tr{
.bp3-input, .bp3-form-group:not(.bp3-intent-danger) .bp3-input,
.form-group--select-list .bp3-button{ .form-group--select-list .bp3-button{
border-color: #E5E5E5; border-color: #E5E5E5;
border-radius: 3px; border-radius: 3px;
@@ -84,13 +84,12 @@
.form-group--select-list{ .form-group--select-list{
&.bp3-intent-danger{ &.bp3-intent-danger{
.bp3-button:not(.bp3-minimal){ .bp3-button:not(.bp3-minimal){
border-color: #efa8a8; border-color: #db3737;
} }
} }
} }
&:last-of-type{ &:last-of-type{
.td{ .td{
border-bottom: transparent; border-bottom: transparent;
@@ -129,6 +128,14 @@
} }
} }
} }
.td{
&.note{
.bp3-form-group{
width: 100%;
}
}
}
} }
} }
.th{ .th{

View File

@@ -5,8 +5,8 @@ exports.up = function (knex) {
table.string('name'); table.string('name');
table.string('type'); table.string('type');
table.string('sku'); table.string('sku');
table.decimal('cost_price').unsigned(); table.decimal('cost_price', 13, 3).unsigned();
table.decimal('sell_price').unsigned(); table.decimal('sell_price', 13, 3).unsigned();
table.string('currency_code', 3); table.string('currency_code', 3);
table.string('picture_uri'); table.string('picture_uri');
table.integer('cost_account_id').unsigned(); table.integer('cost_account_id').unsigned();

View File

@@ -2,8 +2,8 @@
exports.up = function(knex) { exports.up = function(knex) {
return knex.schema.createTable('accounts_transactions', (table) => { return knex.schema.createTable('accounts_transactions', (table) => {
table.increments(); table.increments();
table.decimal('credit'); table.decimal('credit', 13, 3);
table.decimal('debit'); table.decimal('debit', 13, 3);
table.string('transaction_type'); table.string('transaction_type');
table.string('reference_type'); table.string('reference_type');
table.integer('reference_id'); table.integer('reference_id');

View File

@@ -2,8 +2,8 @@
exports.up = function(knex) { exports.up = function(knex) {
return knex.schema.createTable('expenses_transactions', (table) => { return knex.schema.createTable('expenses_transactions', (table) => {
table.increments(); table.increments();
table.decimal('total_amount'); table.decimal('total_amount', 13, 3);
table.string('currency_code'); table.string('currency_code', 3);
table.text('description'); table.text('description');
table.integer('payment_account_id').unsigned(); table.integer('payment_account_id').unsigned();
table.integer('payee_id').unsigned(); table.integer('payee_id').unsigned();

View File

@@ -4,7 +4,7 @@ exports.up = function(knex) {
table.increments(); table.increments();
table.date('date'); table.date('date');
table.string('currency_code'); table.string('currency_code');
table.decimal('exchange_rate'); table.decimal('exchange_rate', 8, 5);
table.string('note'); table.string('note');
}); });
}; };

View File

@@ -5,7 +5,7 @@ exports.up = function(knex) {
table.string('journal_number'); table.string('journal_number');
table.string('reference'); table.string('reference');
table.string('journal_type'); table.string('journal_type');
table.decimal('amount'); table.decimal('amount', 13, 3);
table.date('date'); table.date('date');
table.boolean('status').defaultTo(false); table.boolean('status').defaultTo(false);
table.string('description'); table.string('description');

View File

@@ -5,7 +5,7 @@ exports.up = function(knex) {
table.integer('expense_account_id').unsigned(); table.integer('expense_account_id').unsigned();
table.integer('index').unsigned(); table.integer('index').unsigned();
table.text('description'); table.text('description');
table.decimal('amount'); table.decimal('amount', 13, 3);
table.integer('expense_id').unsigned(); table.integer('expense_id').unsigned();
table.timestamps(); table.timestamps();
}).raw('ALTER TABLE `EXPENSE_TRANSACTION_CATEGORIES` AUTO_INCREMENT = 1000');; }).raw('ALTER TABLE `EXPENSE_TRANSACTION_CATEGORIES` AUTO_INCREMENT = 1000');;

View File

@@ -16,7 +16,6 @@ import {
DynamicFilterFilterRoles, DynamicFilterFilterRoles,
} from '@/lib/DynamicFilter'; } from '@/lib/DynamicFilter';
export default { export default {
/** /**
* Router constructor. * Router constructor.
@@ -24,45 +23,63 @@ export default {
router() { router() {
const router = express.Router(); const router = express.Router();
router.get('/manual-journals/:id', router.get(
'/manual-journals/:id',
this.getManualJournal.validation, this.getManualJournal.validation,
asyncMiddleware(this.getManualJournal.handler)); asyncMiddleware(this.getManualJournal.handler)
);
router.get('/manual-journals', router.get(
'/manual-journals',
this.manualJournals.validation, this.manualJournals.validation,
asyncMiddleware(this.manualJournals.handler)); asyncMiddleware(this.manualJournals.handler)
);
router.post('/make-journal-entries', router.post(
'/make-journal-entries',
this.validateMediaIds, this.validateMediaIds,
this.validateContactEntries, this.validateContactEntries,
this.makeJournalEntries.validation, this.makeJournalEntries.validation,
asyncMiddleware(this.makeJournalEntries.handler)); asyncMiddleware(this.makeJournalEntries.handler)
);
router.post('/manual-journals/:id/publish', router.post(
'/manual-journals/:id/publish',
this.publishManualJournal.validation, this.publishManualJournal.validation,
asyncMiddleware(this.publishManualJournal.handler)); asyncMiddleware(this.publishManualJournal.handler)
);
router.post('/manual-journals/:id', router.post(
'/manual-journals/:id',
this.validateMediaIds, this.validateMediaIds,
this.validateContactEntries, this.validateContactEntries,
this.editManualJournal.validation, this.editManualJournal.validation,
asyncMiddleware(this.editManualJournal.handler)); asyncMiddleware(this.editManualJournal.handler)
);
router.delete('/manual-journals/:id', router.delete(
'/manual-journals/:id',
this.deleteManualJournal.validation, this.deleteManualJournal.validation,
asyncMiddleware(this.deleteManualJournal.handler)); asyncMiddleware(this.deleteManualJournal.handler)
);
router.delete('/manual-journals', router.delete(
'/manual-journals',
this.deleteBulkManualJournals.validation, this.deleteBulkManualJournals.validation,
asyncMiddleware(this.deleteBulkManualJournals.handler)); asyncMiddleware(this.deleteBulkManualJournals.handler)
);
router.post('/recurring-journal-entries', router.post(
'/recurring-journal-entries',
this.recurringJournalEntries.validation, this.recurringJournalEntries.validation,
asyncMiddleware(this.recurringJournalEntries.handler)); asyncMiddleware(this.recurringJournalEntries.handler)
);
router.post('quick-journal-entries', router.post(
'quick-journal-entries',
this.quickJournalEntries.validation, this.quickJournalEntries.validation,
asyncMiddleware(this.quickJournalEntries.handler)); asyncMiddleware(this.quickJournalEntries.handler)
);
return router; return router;
}, },
@@ -76,7 +93,7 @@ export default {
query('page_size').optional().isNumeric().toInt(), query('page_size').optional().isNumeric().toInt(),
query('custom_view_id').optional().isNumeric().toInt(), query('custom_view_id').optional().isNumeric().toInt(),
query('column_sort_by').optional(), query('column_sort_by').optional().trim().escape(),
query('sort_order').optional().isIn(['desc', 'asc']), query('sort_order').optional().isIn(['desc', 'asc']),
query('stringified_filter_roles').optional().isJSON(), query('stringified_filter_roles').optional().isJSON(),
@@ -86,7 +103,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 filter = { const filter = {
@@ -126,17 +144,22 @@ export default {
builder.remember(); builder.remember();
}); });
const resourceFieldsKeys = manualJournalsResource.fields.map((c) => c.key); const resourceFieldsKeys = manualJournalsResource.fields.map(
(c) => c.key
);
const dynamicFilter = new DynamicFilter(ManualJournal.tableName); const dynamicFilter = new DynamicFilter(ManualJournal.tableName);
// Dynamic filter with view roles. // Dynamic filter with view roles.
if (view && view.roles.length > 0) { if (view && view.roles.length > 0) {
const viewFilter = new DynamicFilterViews( const viewFilter = new DynamicFilterViews(
mapViewRolesToConditionals(view.roles), mapViewRolesToConditionals(view.roles),
view.rolesLogicExpression, view.rolesLogicExpression
); );
if (!viewFilter.validateFilterRoles()) { if (!viewFilter.validateFilterRoles()) {
errorReasons.push({ type: 'VIEW.LOGIC.EXPRESSION.INVALID', code: 400 }); errorReasons.push({
type: 'VIEW.LOGIC.EXPRESSION.INVALID',
code: 400,
});
} }
dynamicFilter.setFilter(viewFilter); dynamicFilter.setFilter(viewFilter);
} }
@@ -145,12 +168,15 @@ export default {
// Validate the accounts resource fields. // Validate the accounts resource fields.
const filterRoles = new DynamicFilterFilterRoles( const filterRoles = new DynamicFilterFilterRoles(
mapFilterRolesToDynamicFilter(filter.filter_roles), mapFilterRolesToDynamicFilter(filter.filter_roles),
manualJournalsResource.fields, manualJournalsResource.fields
); );
dynamicFilter.setFilter(filterRoles); dynamicFilter.setFilter(filterRoles);
if (filterRoles.validateFilterRoles().length > 0) { if (filterRoles.validateFilterRoles().length > 0) {
errorReasons.push({ type: 'MANUAL.JOURNAL.HAS.NO.FIELDS', code: 500 }); errorReasons.push({
type: 'MANUAL.JOURNAL.HAS.NO.FIELDS',
code: 500,
});
} }
} }
// Dynamic filter with column sort order. // Dynamic filter with column sort order.
@@ -160,7 +186,7 @@ export default {
} }
const sortByFilter = new DynamicFilterSortBy( const sortByFilter = new DynamicFilterSortBy(
filter.column_sort_by, filter.column_sort_by,
filter.sort_order, filter.sort_order
); );
dynamicFilter.setFilter(sortByFilter); dynamicFilter.setFilter(sortByFilter);
} }
@@ -168,18 +194,22 @@ export default {
return res.status(400).send({ errors: errorReasons }); return res.status(400).send({ errors: errorReasons });
} }
// Manual journals. // Manual journals.
const manualJournals = await ManualJournal.query().onBuild((builder) => { const manualJournals = await ManualJournal.query()
.onBuild((builder) => {
dynamicFilter.buildQuery()(builder); dynamicFilter.buildQuery()(builder);
}).pagination(filter.page - 1, filter.page_size); })
.pagination(filter.page - 1, filter.page_size);
return res.status(200).send({ return res.status(200).send({
manualJournals: { manualJournals: {
...manualJournals, ...manualJournals,
...(view) ? { ...(view
? {
viewMeta: { viewMeta: {
customViewId: view.id, customViewId: view.id,
},
} }
} : {}, : {}),
}, },
}); });
}, },
@@ -199,14 +229,23 @@ export default {
// Validate if media ids was not already exists on the storage. // Validate if media ids was not already exists on the storage.
if (form.media_ids.length > 0) { if (form.media_ids.length > 0) {
const storedMedia = await Media.query().whereIn('id', form.media_ids); const storedMedia = await Media.query().whereIn('id', form.media_ids);
const notFoundMedia = difference(form.media_ids, storedMedia.map((m) => m.id)); const notFoundMedia = difference(
form.media_ids,
storedMedia.map((m) => m.id)
);
if (notFoundMedia.length > 0) { if (notFoundMedia.length > 0) {
errorReasons.push({ type: 'MEDIA.IDS.NOT.FOUND', code: 400, ids: notFoundMedia }); errorReasons.push({
type: 'MEDIA.IDS.NOT.FOUND',
code: 400,
ids: notFoundMedia,
});
} }
} }
req.errorReasons = Array.isArray(req.errorReasons) && req.errorReasons.length req.errorReasons =
? req.errorReasons.push(...errorReasons) : errorReasons; Array.isArray(req.errorReasons) && req.errorReasons.length
? req.errorReasons.push(...errorReasons)
: errorReasons;
next(); next();
}, },
@@ -228,24 +267,37 @@ export default {
const errorReasons = []; const errorReasons = [];
// Validate the entries contact type and ids. // Validate the entries contact type and ids.
const formEntriesCustomersIds = form.entries.filter(e => e.contact_type === 'customer'); const formEntriesCustomersIds = form.entries.filter(
const formEntriesVendorsIds = form.entries.filter(e => e.contact_type === 'vendor'); (e) => e.contact_type === 'customer'
);
const formEntriesVendorsIds = form.entries.filter(
(e) => e.contact_type === 'vendor'
);
const accountsTypes = await AccountType.query(); const accountsTypes = await AccountType.query();
const payableAccountsType = accountsTypes.find(t => t.key === 'accounts_payable');; const payableAccountsType = accountsTypes.find(
const receivableAccountsType = accountsTypes.find(t => t.key === 'accounts_receivable'); (t) => t.key === 'accounts_payable'
);
const receivableAccountsType = accountsTypes.find(
(t) => t.key === 'accounts_receivable'
);
const receivableAccountOper = Account.query().where('account_type_id', receivableAccountsType.id).first(); const receivableAccountOper = Account.query()
const payableAccountOper = Account.query().where('account_type_id', payableAccountsType.id).first(); .where('account_type_id', receivableAccountsType.id)
.first();
const payableAccountOper = Account.query()
.where('account_type_id', payableAccountsType.id)
.first();
const [receivableAccount, payableAccount] = await Promise.all([ const [receivableAccount, payableAccount] = await Promise.all([
receivableAccountOper, payableAccountOper, receivableAccountOper,
payableAccountOper,
]); ]);
const entriesHasNoReceivableAccount = form.entries const entriesHasNoReceivableAccount = form.entries.filter(
.filter(e => (e) =>
(e.account_id === receivableAccount.id) && e.account_id === receivableAccount.id &&
(!e.contact_id || e.contact_type !== 'customer') (!e.contact_id || e.contact_type !== 'customer')
); );
@@ -253,13 +305,13 @@ export default {
errorReasons.push({ errorReasons.push({
type: 'RECEIVABLE.ENTRIES.HAS.NO.CUSTOMERS', type: 'RECEIVABLE.ENTRIES.HAS.NO.CUSTOMERS',
code: 900, code: 900,
indexes: entriesHasNoReceivableAccount.map(e => e.index), indexes: entriesHasNoReceivableAccount.map((e) => e.index),
}); });
} }
const entriesHasNoVendorContact = form.entries const entriesHasNoVendorContact = form.entries.filter(
.filter(e => (e) =>
(e.account_id === payableAccount.id) && e.account_id === payableAccount.id &&
(!e.contact_id || e.contact_type !== 'contact') (!e.contact_id || e.contact_type !== 'contact')
); );
@@ -267,47 +319,59 @@ export default {
errorReasons.push({ errorReasons.push({
type: 'PAYABLE.ENTRIES.HAS.NO.VENDORS', type: 'PAYABLE.ENTRIES.HAS.NO.VENDORS',
code: 1000, code: 1000,
indexes: entriesHasNoVendorContact.map(e => e.index), indexes: entriesHasNoVendorContact.map((e) => e.index),
}); });
} }
// Validate customers contacts. // Validate customers contacts.
if (formEntriesCustomersIds.length > 0) { if (formEntriesCustomersIds.length > 0) {
const customersContactsIds = formEntriesCustomersIds.map(c => c.contact_id); const customersContactsIds = formEntriesCustomersIds.map(
const storedContacts = await Customer.query().whereIn('id', customersContactsIds); (c) => c.contact_id
);
const storedContacts = await Customer.query().whereIn(
'id',
customersContactsIds
);
const storedContactsIds = storedContacts.map(c => c.id); const storedContactsIds = storedContacts.map((c) => c.id);
const notFoundContactsIds = difference( const notFoundContactsIds = difference(
formEntriesCustomersIds.map(c => c.contact_id), formEntriesCustomersIds.map((c) => c.contact_id),
storedContactsIds, storedContactsIds
); );
if (notFoundContactsIds.length > 0) { if (notFoundContactsIds.length > 0) {
errorReasons.push({ type: 'CUSTOMERS.CONTACTS.NOT.FOUND', code: 500, ids: notFoundContactsIds }); errorReasons.push({
type: 'CUSTOMERS.CONTACTS.NOT.FOUND',
code: 500,
ids: notFoundContactsIds,
});
} }
const notReceivableAccounts = formEntriesCustomersIds.filter( const notReceivableAccounts = formEntriesCustomersIds.filter(
c => c.account_id !== receivableAccount.id (c) => c.account_id !== receivableAccount.id
); );
if (notReceivableAccounts.length > 0) { if (notReceivableAccounts.length > 0) {
errorReasons.push({ errorReasons.push({
type: 'CUSTOMERS.NOT.WITH.RECEIVABLE.ACCOUNT', type: 'CUSTOMERS.NOT.WITH.RECEIVABLE.ACCOUNT',
code: 700, code: 700,
indexes: notReceivableAccounts.map(a => a.index), indexes: notReceivableAccounts.map((a) => a.index),
}); });
} }
} }
// Validate vendors contacts. // Validate vendors contacts.
if (formEntriesVendorsIds.length > 0) { if (formEntriesVendorsIds.length > 0) {
const vendorsContactsIds = formEntriesVendorsIds.map(c => c.contact_id); const vendorsContactsIds = formEntriesVendorsIds.map((c) => c.contact_id);
const storedContacts = await Vendor.query().where('id', vendorsContactsIds); const storedContacts = await Vendor.query().where(
'id',
vendorsContactsIds
);
const storedContactsIds = storedContacts.map(c => c.id); const storedContactsIds = storedContacts.map((c) => c.id);
const notFoundContactsIds = difference( const notFoundContactsIds = difference(
formEntriesVendorsIds.map(v => v.contact_id), formEntriesVendorsIds.map((v) => v.contact_id),
storedContactsIds, storedContactsIds
); );
if (notFoundContactsIds.length > 0) { if (notFoundContactsIds.length > 0) {
errorReasons.push({ errorReasons.push({
@@ -317,19 +381,21 @@ export default {
}); });
} }
const notPayableAccounts = formEntriesVendorsIds.filter( const notPayableAccounts = formEntriesVendorsIds.filter(
v => v.contact_id === payableAccount.id (v) => v.contact_id === payableAccount.id
); );
if (notPayableAccounts.length > 0) { if (notPayableAccounts.length > 0) {
errorReasons.push({ errorReasons.push({
type: 'VENDORS.NOT.WITH.PAYABLE.ACCOUNT', type: 'VENDORS.NOT.WITH.PAYABLE.ACCOUNT',
code: 800, code: 800,
indexes: notPayableAccounts.map(a => a.index), indexes: notPayableAccounts.map((a) => a.index),
}); });
} }
} }
req.errorReasons = Array.isArray(req.errorReasons) && req.errorReasons.length req.errorReasons =
? req.errorReasons.push(...errorReasons) : errorReasons; Array.isArray(req.errorReasons) && req.errorReasons.length
? req.errorReasons.push(...errorReasons)
: errorReasons;
next(); next();
}, },
@@ -347,11 +413,24 @@ export default {
check('status').optional().isBoolean().toBoolean(), check('status').optional().isBoolean().toBoolean(),
check('entries').isArray({ min: 2 }), check('entries').isArray({ min: 2 }),
check('entries.*.index').exists().isNumeric().toInt(), check('entries.*.index').exists().isNumeric().toInt(),
check('entries.*.credit').optional({ nullable: true }).isNumeric().toInt(), check('entries.*.credit')
check('entries.*.debit').optional({ nullable: true }).isNumeric().toInt(), .optional({ nullable: true })
.isNumeric()
.isDecimal()
.isFloat({ max: 9999999999.999 }) // 13, 3
.toFloat(),
check('entries.*.debit')
.optional({ nullable: true })
.isNumeric()
.isDecimal()
.isFloat({ max: 9999999999.999 }) // 13, 3
.toFloat(),
check('entries.*.account_id').isNumeric().toInt(), check('entries.*.account_id').isNumeric().toInt(),
check('entries.*.note').optional(), check('entries.*.note').optional(),
check('entries.*.contact_id').optional({ nullable: true }).isNumeric().toInt(), check('entries.*.contact_id')
.optional({ nullable: true })
.isNumeric()
.toInt(),
check('entries.*.contact_type').optional().isIn(['vendor', 'customer']), check('entries.*.contact_type').optional().isIn(['vendor', 'customer']),
check('media_ids').optional().isArray(), check('media_ids').optional().isArray(),
check('media_ids.*').exists().isNumeric().toInt(), check('media_ids.*').exists().isNumeric().toInt(),
@@ -361,7 +440,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 form = { const form = {
@@ -371,18 +451,16 @@ export default {
media_ids: [], media_ids: [],
...req.body, ...req.body,
}; };
const { const { ManualJournal, Account, MediaLink } = req.models;
ManualJournal,
Account,
MediaLink,
} = req.models;
let totalCredit = 0; let totalCredit = 0;
let totalDebit = 0; let totalDebit = 0;
const { user } = req; const { user } = req;
const errorReasons = [...(req.errorReasons || [])]; const errorReasons = [...(req.errorReasons || [])];
const entries = form.entries.filter((entry) => (entry.credit || entry.debit)); const entries = form.entries.filter(
(entry) => entry.credit || entry.debit
);
const formattedDate = moment(form.date).format('YYYY-MM-DD'); const formattedDate = moment(form.date).format('YYYY-MM-DD');
entries.forEach((entry) => { entries.forEach((entry) => {
@@ -414,8 +492,10 @@ export default {
errorReasons.push({ type: 'ACCOUNTS.IDS.NOT.FOUND', code: 200 }); errorReasons.push({ type: 'ACCOUNTS.IDS.NOT.FOUND', code: 200 });
} }
const journalNumber = await ManualJournal.query() const journalNumber = await ManualJournal.query().where(
.where('journal_number', form.journal_number); 'journal_number',
form.journal_number
);
if (journalNumber.length > 0) { if (journalNumber.length > 0) {
errorReasons.push({ type: 'JOURNAL.NUMBER.ALREADY.EXISTS', code: 300 }); errorReasons.push({ type: 'JOURNAL.NUMBER.ALREADY.EXISTS', code: 300 });
@@ -478,7 +558,7 @@ export default {
await Promise.all([ await Promise.all([
...bulkSaveMediaLink, ...bulkSaveMediaLink,
journalPoster.saveEntries(), journalPoster.saveEntries(),
(form.status) && journalPoster.saveBalance(), form.status && journalPoster.saveBalance(),
]); ]);
return res.status(200).send({ id: manualJournal.id }); return res.status(200).send({ id: manualJournal.id });
}, },
@@ -503,7 +583,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,
}); });
} }
}, },
@@ -517,15 +598,29 @@ export default {
param('id').exists().isNumeric().toInt(), param('id').exists().isNumeric().toInt(),
check('date').exists().isISO8601(), check('date').exists().isISO8601(),
check('journal_number').exists().trim().escape(), check('journal_number').exists().trim().escape(),
check('transaction_type').optional({ nullable: true }).trim().escape(), check('journal_type').optional({ nullable: true }).trim().escape(),
check('reference').optional({ nullable: true }), check('reference').optional({ nullable: true }),
check('description').optional().trim().escape(), check('description').optional().trim().escape(),
check('entries').isArray({ min: 2 }), check('entries').isArray({ min: 2 }),
check('entries.*.credit').optional({ nullable: true }).isNumeric().toInt(), // check('entries.*.index').exists().isNumeric().toInt(),
check('entries.*.debit').optional({ nullable: true }).isNumeric().toInt(), check('entries.*.credit')
.optional({ nullable: true })
.isNumeric()
.toFloat(),
check('entries.*.debit')
.optional({ nullable: true })
.isNumeric()
.toFloat(),
check('entries.*.account_id').isNumeric().toInt(), check('entries.*.account_id').isNumeric().toInt(),
check('entries.*.contact_id').optional().isNumeric().toInt(), check('entries.*.contact_id')
check('entries.*.contact_type').optional().isIn(['vendor', 'customer']).isNumeric().toInt(), .optional({ nullable: true })
.isNumeric()
.toInt(),
check('entries.*.contact_type')
.optional()
.isIn(['vendor', 'customer'])
.isNumeric()
.toInt(),
check('entries.*.note').optional(), check('entries.*.note').optional(),
check('media_ids').optional().isArray(), check('media_ids').optional().isArray(),
check('media_ids.*').isNumeric().toInt(), check('media_ids.*').isNumeric().toInt(),
@@ -535,24 +630,30 @@ 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 form = { const form = {
date: new Date(), date: new Date(),
transaction_type: 'journal', journal_type: 'Journal',
reference: '', reference: '',
media_ids: [], media_ids: [],
...req.body, ...req.body,
}; };
const { id } = req.params; const { id } = req.params;
const { const {
ManualJournal, AccountTransaction, Account, Media, MediaLink, ManualJournal,
AccountTransaction,
Account,
Media,
MediaLink,
} = req.models; } = req.models;
const manualJournal = await ManualJournal.query() const manualJournal = await ManualJournal.query()
.where('id', id) .where('id', id)
.withGraphFetched('media').first(); .withGraphFetched('media')
.first();
if (!manualJournal) { if (!manualJournal) {
return res.status(4040).send({ return res.status(4040).send({
@@ -564,7 +665,9 @@ export default {
const { user } = req; const { user } = req;
const errorReasons = [...(req.errorReasons || [])]; const errorReasons = [...(req.errorReasons || [])];
const entries = form.entries.filter((entry) => (entry.credit || entry.debit)); const entries = form.entries.filter(
(entry) => entry.credit || entry.debit
);
const formattedDate = moment(form.date).format('YYYY-MM-DD'); const formattedDate = moment(form.date).format('YYYY-MM-DD');
entries.forEach((entry) => { entries.forEach((entry) => {
@@ -593,7 +696,8 @@ export default {
errorReasons.push({ type: 'JOURNAL.NUMBER.ALREADY.EXISTS', code: 300 }); errorReasons.push({ type: 'JOURNAL.NUMBER.ALREADY.EXISTS', code: 300 });
} }
const accountsIds = entries.map((entry) => entry.account_id); const accountsIds = entries.map((entry) => entry.account_id);
const accounts = await Account.query().whereIn('id', accountsIds) const accounts = await Account.query()
.whereIn('id', accountsIds)
.withGraphFetched('type'); .withGraphFetched('type');
const storedAccountsIds = accounts.map((account) => account.id); const storedAccountsIds = accounts.map((account) => account.id);
@@ -605,11 +709,9 @@ export default {
return res.status(400).send({ errors: errorReasons }); return res.status(400).send({ errors: errorReasons });
} }
await ManualJournal.query() await ManualJournal.query().where('id', manualJournal.id).update({
.where('id', manualJournal.id)
.update({
reference: form.reference, reference: form.reference,
transaction_type: 'Journal', journal_type: form.journal_type,
journalNumber: form.journal_number, journalNumber: form.journal_number,
amount: totalCredit, amount: totalCredit,
date: formattedDate, date: formattedDate,
@@ -674,26 +776,20 @@ export default {
}, },
publishManualJournal: { publishManualJournal: {
validation: [ validation: [param('id').exists().isNumeric().toInt()],
param('id').exists().isNumeric().toInt(),
],
async handler(req, res) { async handler(req, res) {
const validationErrors = validationResult(req); const validationErrors = validationResult(req);
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 { const { ManualJournal, AccountTransaction, Account } = req.models;
ManualJournal,
AccountTransaction,
Account,
} = req.models;
const { id } = req.params; const { id } = req.params;
const manualJournal = await ManualJournal.query() const manualJournal = await ManualJournal.query().where('id', id).first();
.where('id', id).first();
if (!manualJournal) { if (!manualJournal) {
return res.status(404).send({ return res.status(404).send({
@@ -721,7 +817,10 @@ export default {
journal.calculateEntriesBalanceChange(); journal.calculateEntriesBalanceChange();
const updateAccountsTransactionsOper = AccountTransaction.query() const updateAccountsTransactionsOper = AccountTransaction.query()
.whereIn('id', transactions.map((t) => t.id)) .whereIn(
'id',
transactions.map((t) => t.id)
)
.update({ draft: 0 }); .update({ draft: 0 });
await Promise.all([ await Promise.all([
@@ -734,20 +833,17 @@ export default {
}, },
getManualJournal: { getManualJournal: {
validation: [ validation: [param('id').exists().isNumeric().toInt()],
param('id').exists().isNumeric().toInt(),
],
async handler(req, res) { async handler(req, res) {
const validationErrors = validationResult(req); const validationErrors = validationResult(req);
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 { const { ManualJournal, AccountTransaction } = req.models;
ManualJournal, AccountTransaction,
} = req.models;
const { id } = req.params; const { id } = req.params;
const manualJournal = await ManualJournal.query() const manualJournal = await ManualJournal.query()
@@ -767,9 +863,7 @@ export default {
return res.status(200).send({ return res.status(200).send({
manual_journal: { manual_journal: {
...manualJournal.toJSON(), ...manualJournal.toJSON(),
entries: [ entries: [...transactions],
...transactions,
],
}, },
}); });
}, },
@@ -780,15 +874,14 @@ export default {
* accounts transactions. * accounts transactions.
*/ */
deleteManualJournal: { deleteManualJournal: {
validation: [ validation: [param('id').exists().isNumeric().toInt()],
param('id').exists().isNumeric().toInt(),
],
async handler(req, res) { async handler(req, res) {
const validationErrors = validationResult(req); const validationErrors = validationResult(req);
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 { id } = req.params; const { id } = req.params;
@@ -799,8 +892,7 @@ export default {
Account, Account,
} = req.models; } = req.models;
const manualJournal = await ManualJournal.query() const manualJournal = await ManualJournal.query().where('id', id).first();
.where('id', id).first();
if (!manualJournal) { if (!manualJournal) {
return res.status(404).send({ return res.status(404).send({
@@ -823,14 +915,9 @@ export default {
.where('model_id', manualJournal.id) .where('model_id', manualJournal.id)
.delete(); .delete();
await ManualJournal.query() await ManualJournal.query().where('id', manualJournal.id).delete();
.where('id', manualJournal.id)
.delete();
await Promise.all([ await Promise.all([journal.deleteEntries(), journal.saveBalance()]);
journal.deleteEntries(),
journal.saveBalance(),
]);
return res.status(200).send({ id }); return res.status(200).send({ id });
}, },
}, },
@@ -846,7 +933,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,
}); });
} }
}, },
@@ -866,7 +954,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 errorReasons = []; const errorReasons = [];
@@ -877,8 +966,12 @@ export default {
.where('id', form.credit_account_id) .where('id', form.credit_account_id)
.orWhere('id', form.debit_account_id); .orWhere('id', form.debit_account_id);
const creditAccount = foundAccounts.find((a) => a.id === form.credit_account_id); const creditAccount = foundAccounts.find(
const debitAccount = foundAccounts.find((a) => a.id === form.debit_account_id); (a) => a.id === form.credit_account_id
);
const debitAccount = foundAccounts.find(
(a) => a.id === form.debit_account_id
);
if (!creditAccount) { if (!creditAccount) {
errorReasons.push({ type: 'CREDIT_ACCOUNT.NOT.EXIST', code: 100 }); errorReasons.push({ type: 'CREDIT_ACCOUNT.NOT.EXIST', code: 100 });
@@ -914,16 +1007,27 @@ 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 filter = { ...req.query }; const filter = { ...req.query };
const { ManualJournal, AccountTransaction, Account, MediaLink } = req.models; const {
ManualJournal,
AccountTransaction,
Account,
MediaLink,
} = req.models;
const manualJournals = await ManualJournal.query() const manualJournals = await ManualJournal.query().whereIn(
.whereIn('id', filter.ids); 'id',
filter.ids
);
const notFoundManualJournals = difference(filter.ids, manualJournals.map(m => m.id)); const notFoundManualJournals = difference(
filter.ids,
manualJournals.map((m) => m.id)
);
if (notFoundManualJournals.length > 0) { if (notFoundManualJournals.length > 0) {
return res.status(404).send({ return res.status(404).send({
@@ -945,14 +1049,10 @@ export default {
.whereIn('model_id', filter.ids) .whereIn('model_id', filter.ids)
.delete(); .delete();
await ManualJournal.query() await ManualJournal.query().whereIn('id', filter.ids).delete();
.whereIn('id', filter.ids).delete();
await Promise.all([ await Promise.all([journal.deleteEntries(), journal.saveBalance()]);
journal.deleteEntries(),
journal.saveBalance(),
]);
return res.status(200).send({ ids: filter.ids }); return res.status(200).send({ ids: filter.ids });
}, },
} },
}; };

View File

@@ -1,10 +1,5 @@
import express from 'express'; import express from 'express';
import { import { check, validationResult, param, query } from 'express-validator';
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';
@@ -20,7 +15,6 @@ import {
DynamicFilterFilterRoles, DynamicFilterFilterRoles,
} from '@/lib/DynamicFilter'; } from '@/lib/DynamicFilter';
export default { export default {
/** /**
* Router constructor method. * Router constructor method.
@@ -28,49 +22,71 @@ export default {
router() { router() {
const router = express.Router(); const router = express.Router();
router.post('/', router.post(
'/',
this.newAccount.validation, this.newAccount.validation,
asyncMiddleware(this.newAccount.handler)); asyncMiddleware(this.newAccount.handler)
);
router.post('/:id', router.post(
'/:id',
this.editAccount.validation, this.editAccount.validation,
asyncMiddleware(this.editAccount.handler)); asyncMiddleware(this.editAccount.handler)
);
router.get('/:id', router.get(
'/:id',
this.getAccount.validation, this.getAccount.validation,
asyncMiddleware(this.getAccount.handler)); asyncMiddleware(this.getAccount.handler)
);
router.get('/', router.get(
'/',
this.getAccountsList.validation, this.getAccountsList.validation,
asyncMiddleware(this.getAccountsList.handler)); asyncMiddleware(this.getAccountsList.handler)
);
router.delete('/', router.delete(
'/',
this.deleteBulkAccounts.validation, this.deleteBulkAccounts.validation,
asyncMiddleware(this.deleteBulkAccounts.handler)); asyncMiddleware(this.deleteBulkAccounts.handler)
);
router.delete('/:id', router.delete(
'/:id',
this.deleteAccount.validation, this.deleteAccount.validation,
asyncMiddleware(this.deleteAccount.handler)); asyncMiddleware(this.deleteAccount.handler)
);
router.post('/:id/active', router.post(
'/:id/active',
this.activeAccount.validation, this.activeAccount.validation,
asyncMiddleware(this.activeAccount.handler)); asyncMiddleware(this.activeAccount.handler)
);
router.post('/:id/inactive', router.post(
'/:id/inactive',
this.inactiveAccount.validation, this.inactiveAccount.validation,
asyncMiddleware(this.inactiveAccount.handler)); asyncMiddleware(this.inactiveAccount.handler)
);
router.post('/:id/recalculate-balance', router.post(
'/:id/recalculate-balance',
this.recalcualteBalanace.validation, this.recalcualteBalanace.validation,
asyncMiddleware(this.recalcualteBalanace.handler)); asyncMiddleware(this.recalcualteBalanace.handler)
);
router.post('/:id/transfer_account/:toAccount', router.post(
'/:id/transfer_account/:toAccount',
this.transferToAnotherAccount.validation, this.transferToAnotherAccount.validation,
asyncMiddleware(this.transferToAnotherAccount.handler)); asyncMiddleware(this.transferToAnotherAccount.handler)
);
router.post('/bulk/:type(activate|inactivate)', router.post(
'/bulk/:type(activate|inactivate)',
this.bulkInactivateAccounts.validation, this.bulkInactivateAccounts.validation,
asyncMiddleware(this.bulkInactivateAccounts.handler)); asyncMiddleware(this.bulkInactivateAccounts.handler)
);
return router; return router;
}, },
@@ -80,34 +96,34 @@ export default {
*/ */
newAccount: { newAccount: {
validation: [ validation: [
check('name').exists().isLength({ min: 3 }) check('name').exists().isLength({ min: 3, max: 255 }).trim().escape(),
.trim() check('code').optional().isLength({ min: 3, max: 6, }).trim().escape(),
.escape(),
check('code').optional().isLength({ max: 10 })
.trim()
.escape(),
check('account_type_id').exists().isNumeric().toInt(), check('account_type_id').exists().isNumeric().toInt(),
check('description').optional().trim().escape(), check('description').optional().isLength({ max: 512 }).trim().escape(),
], ],
async handler(req, res) { async handler(req, res) {
const validationErrors = validationResult(req); const validationErrors = validationResult(req);
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 form = { ...req.body }; const form = { ...req.body };
const { AccountType, Account } = req.models; const { AccountType, Account } = req.models;
const foundAccountCodePromise = form.code const foundAccountCodePromise = form.code
? Account.query().where('code', form.code) : null; ? Account.query().where('code', form.code)
: null;
const foundAccountTypePromise = AccountType.query() const foundAccountTypePromise = AccountType.query().findById(
.findById(form.account_type_id); form.account_type_id
);
const [foundAccountCode, foundAccountType] = await Promise.all([ const [foundAccountCode, foundAccountType] = await Promise.all([
foundAccountCodePromise, foundAccountTypePromise, foundAccountCodePromise,
foundAccountTypePromise,
]); ]);
if (foundAccountCodePromise && foundAccountCode.length > 0) { if (foundAccountCodePromise && foundAccountCode.length > 0) {
@@ -132,14 +148,10 @@ export default {
editAccount: { editAccount: {
validation: [ validation: [
param('id').exists().toInt(), param('id').exists().toInt(),
check('name').exists().isLength({ min: 3 }) check('name').exists().isLength({ min: 3, max: 255, }).trim().escape(),
.trim() check('code').optional().isLength({ min: 3, max: 6, }).trim().escape(),
.escape(),
check('code').optional().isLength({ max: 10 })
.trim()
.escape(),
check('account_type_id').exists().isNumeric().toInt(), check('account_type_id').exists().isNumeric().toInt(),
check('description').optional().trim().escape(), check('description').optional().isLength({ max: 512 }).trim().escape(),
], ],
async handler(req, res) { async handler(req, res) {
const { id } = req.params; const { id } = req.params;
@@ -147,7 +159,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;
@@ -162,12 +175,15 @@ export default {
// Validate the account type is not changed. // Validate the account type is not changed.
if (account.account_type_id != form.accountTypeId) { if (account.account_type_id != form.accountTypeId) {
errorReasons.push({ errorReasons.push({
type: 'NOT.ALLOWED.TO.CHANGE.ACCOUNT.TYPE', code: 100, type: 'NOT.ALLOWED.TO.CHANGE.ACCOUNT.TYPE',
code: 100,
}); });
} }
// Validate the account code not exists on the storage. // Validate the account code not exists on the storage.
if (form.code && form.code !== account.code) { if (form.code && form.code !== account.code) {
const foundAccountCode = await Account.query().where('code', form.code).whereNot('id', account.id); const foundAccountCode = await Account.query()
.where('code', form.code)
.whereNot('id', account.id);
if (foundAccountCode.length > 0) { if (foundAccountCode.length > 0) {
errorReasons.push({ type: 'NOT_UNIQUE_CODE', code: 200 }); errorReasons.push({ type: 'NOT_UNIQUE_CODE', code: 200 });
@@ -178,7 +194,10 @@ export default {
return res.status(400).send({ errors: errorReasons }); return res.status(400).send({ errors: errorReasons });
} }
// Update the account on the storage. // Update the account on the storage.
const updatedAccount = await Account.query().patchAndFetchById(account.id, { ...form }); const updatedAccount = await Account.query().patchAndFetchById(
account.id,
{ ...form }
);
return res.status(200).send({ account: { ...updatedAccount } }); return res.status(200).send({ account: { ...updatedAccount } });
}, },
@@ -188,18 +207,16 @@ export default {
* Get details of the given account. * Get details of the given account.
*/ */
getAccount: { getAccount: {
validation: [ validation: [param('id').toInt()],
param('id').toInt(),
],
async handler(req, res) { async handler(req, res) {
const { id } = req.params; const { id } = req.params;
const { Account } = req.models; const { Account } = req.models;
const account = await Account.query().remember().where('id', id).first(); const account = await Account.query().where('id', id).first();
if (!account) { if (!account) {
return res.boom.notFound(); return res.boom.notFound();
} }
return res.status(200).send({ account: { ...account } }); return res.status(200).send({ account });
}, },
}, },
@@ -207,9 +224,7 @@ export default {
* Delete the given account. * Delete the given account.
*/ */
deleteAccount: { deleteAccount: {
validation: [ validation: [param('id').toInt()],
param('id').toInt(),
],
async handler(req, res) { async handler(req, res) {
const { id } = req.params; const { id } = req.params;
const { Account, AccountTransaction } = req.models; const { Account, AccountTransaction } = req.models;
@@ -220,11 +235,13 @@ export default {
} }
if (account.predefined) { if (account.predefined) {
return res.boom.badRequest(null, { return res.boom.badRequest(null, {
errors: [{ type: 'ACCOUNT.PREDEFINED' , code: 200 }], errors: [{ type: 'ACCOUNT.PREDEFINED', code: 200 }],
}); });
} }
const accountTransactions = await AccountTransaction.query() const accountTransactions = await AccountTransaction.query().where(
.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, {
@@ -257,7 +274,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 filter = { const filter = {
@@ -307,7 +325,7 @@ export default {
} }
const sortByFilter = new DynamicFilterSortBy( const sortByFilter = new DynamicFilterSortBy(
filter.column_sort_by, filter.column_sort_by,
filter.sort_order, filter.sort_order
); );
dynamicFilter.setFilter(sortByFilter); dynamicFilter.setFilter(sortByFilter);
} }
@@ -316,10 +334,13 @@ export default {
if (view && view.roles.length > 0) { if (view && view.roles.length > 0) {
const viewFilter = new DynamicFilterViews( const viewFilter = new DynamicFilterViews(
mapViewRolesToConditionals(view.roles), mapViewRolesToConditionals(view.roles),
view.rolesLogicExpression, view.rolesLogicExpression
); );
if (!viewFilter.validateFilterRoles()) { if (!viewFilter.validateFilterRoles()) {
errorReasons.push({ type: 'VIEW.LOGIC.EXPRESSION.INVALID', code: 400 }); errorReasons.push({
type: 'VIEW.LOGIC.EXPRESSION.INVALID',
code: 400,
});
} }
dynamicFilter.setFilter(viewFilter); dynamicFilter.setFilter(viewFilter);
} }
@@ -328,12 +349,15 @@ export default {
// Validate the accounts resource fields. // Validate the accounts resource fields.
const filterRoles = new DynamicFilterFilterRoles( const filterRoles = new DynamicFilterFilterRoles(
mapFilterRolesToDynamicFilter(filter.filter_roles), mapFilterRolesToDynamicFilter(filter.filter_roles),
accountsResource.fields, accountsResource.fields
); );
dynamicFilter.setFilter(filterRoles); dynamicFilter.setFilter(filterRoles);
if (filterRoles.validateFilterRoles().length > 0) { if (filterRoles.validateFilterRoles().length > 0) {
errorReasons.push({ type: 'ACCOUNTS.RESOURCE.HAS.NO.GIVEN.FIELDS', code: 500 }); errorReasons.push({
type: 'ACCOUNTS.RESOURCE.HAS.NO.GIVEN.FIELDS',
code: 500,
});
} }
} }
if (errorReasons.length > 0) { if (errorReasons.length > 0) {
@@ -350,7 +374,9 @@ export default {
dynamicFilter.buildQuery()(builder); dynamicFilter.buildQuery()(builder);
// console.log(builder.toKnexQuery().toSQL()); // console.log(builder.toKnexQuery().toSQL());
}).toKnexQuery().toSQL(); })
.toKnexQuery()
.toSQL();
console.log(query); console.log(query);
@@ -370,9 +396,11 @@ export default {
return res.status(200).send({ return res.status(200).send({
accounts: nestedAccounts, accounts: nestedAccounts,
...(view) ? { ...(view
? {
customViewId: view.id, customViewId: view.id,
} : {}, }
: {}),
}); });
}, },
}, },
@@ -381,16 +409,10 @@ export default {
* Re-calculates balance of the given account. * Re-calculates balance of the given account.
*/ */
recalcualteBalanace: { recalcualteBalanace: {
validation: [ validation: [param('id').isNumeric().toInt()],
param('id').isNumeric().toInt(),
],
async handler(req, res) { async handler(req, res) {
const { id } = req.params; const { id } = req.params;
const { const { Account, AccountTransaction, AccountBalance } = req.models;
Account,
AccountTransaction,
AccountBalance,
} = req.models;
const account = await Account.findById(id); const account = await Account.findById(id);
if (!account) { if (!account) {
@@ -398,8 +420,10 @@ export default {
errors: [{ type: 'ACCOUNT.NOT.FOUND', code: 100 }], errors: [{ type: 'ACCOUNT.NOT.FOUND', code: 100 }],
}); });
} }
const accountTransactions = AccountTransaction.query() const accountTransactions = AccountTransaction.query().where(
.where('account_id', account.id); 'account_id',
account.id
);
const journalEntries = new JournalPoster(); const journalEntries = new JournalPoster();
journalEntries.loadFromCollection(accountTransactions); journalEntries.loadFromCollection(accountTransactions);
@@ -418,9 +442,7 @@ export default {
* Active the given account. * Active the given account.
*/ */
activeAccount: { activeAccount: {
validation: [ validation: [param('id').exists().isNumeric().toInt()],
param('id').exists().isNumeric().toInt(),
],
async handler(req, res) { async handler(req, res) {
const { id } = req.params; const { id } = req.params;
const { Account } = req.models; const { Account } = req.models;
@@ -431,9 +453,7 @@ export default {
errors: [{ type: 'ACCOUNT.NOT.FOUND', code: 100 }], errors: [{ type: 'ACCOUNT.NOT.FOUND', code: 100 }],
}); });
} }
await Account.query() await Account.query().where('id', id).patch({ active: true });
.where('id', id)
.patch({ active: true });
return res.status(200).send({ id: account.id }); return res.status(200).send({ id: account.id });
}, },
@@ -443,9 +463,7 @@ export default {
* Inactive the given account. * Inactive the given account.
*/ */
inactiveAccount: { inactiveAccount: {
validation: [ validation: [param('id').exists().isNumeric().toInt()],
param('id').exists().isNumeric().toInt(),
],
async handler(req, res) { async handler(req, res) {
const { id } = req.params; const { id } = req.params;
const { Account } = req.models; const { Account } = req.models;
@@ -456,9 +474,7 @@ export default {
errors: [{ type: 'ACCOUNT.NOT.FOUND', code: 100 }], errors: [{ type: 'ACCOUNT.NOT.FOUND', code: 100 }],
}); });
} }
await Account.query() await Account.query().where('id', id).patch({ active: false });
.where('id', id)
.patch({ active: false });
return res.status(200).send({ id: account.id }); return res.status(200).send({ id: account.id });
}, },
@@ -477,7 +493,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,
}); });
} }
@@ -505,7 +522,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 filter = { ids: [], ...req.query }; const filter = { ids: [], ...req.query };
@@ -518,23 +536,27 @@ export default {
}); });
const accountsIds = accounts.map((a) => a.id); const accountsIds = accounts.map((a) => a.id);
const notFoundAccounts = difference(filter.ids, accountsIds); const notFoundAccounts = difference(filter.ids, accountsIds);
const predefinedAccounts = accounts.filter(account => account.predefined); const predefinedAccounts = accounts.filter(
(account) => account.predefined
);
const errorReasons = []; const errorReasons = [];
if (notFoundAccounts.length > 0) { if (notFoundAccounts.length > 0) {
return res.status(404).send({ return res.status(404).send({
errors: [{ errors: [
{
type: 'ACCOUNTS.IDS.NOT.FOUND', type: 'ACCOUNTS.IDS.NOT.FOUND',
code: 200, code: 200,
ids: notFoundAccounts, ids: notFoundAccounts,
}], },
],
}); });
} }
if (predefinedAccounts.length > 0) { if (predefinedAccounts.length > 0) {
errorReasons.push({ errorReasons.push({
type: 'ACCOUNT.PREDEFINED', type: 'ACCOUNT.PREDEFINED',
code: 200, code: 200,
ids: predefinedAccounts.map(a => a.id), ids: predefinedAccounts.map((a) => a.id),
}); });
} }
const accountsTransactions = await AccountTransaction.query() const accountsTransactions = await AccountTransaction.query()
@@ -554,14 +576,17 @@ export default {
errorReasons.push({ errorReasons.push({
type: 'ACCOUNT.HAS.ASSOCIATED.TRANSACTIONS', type: 'ACCOUNT.HAS.ASSOCIATED.TRANSACTIONS',
code: 300, code: 300,
ids: accountsHasTransactions ids: accountsHasTransactions,
}); });
} }
if (errorReasons.length > 0) { if (errorReasons.length > 0) {
return res.status(400).send({ errors: errorReasons }); return res.status(400).send({ errors: errorReasons });
} }
await Account.query() await Account.query()
.whereIn('id', accounts.map((a) => a.id)) .whereIn(
'id',
accounts.map((a) => a.id)
)
.delete(); .delete();
return res.status(200).send(); return res.status(200).send();
@@ -582,7 +607,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 filter = { const filter = {
@@ -608,6 +634,6 @@ export default {
}); });
return res.status(200).send({ ids: storedAccountsIds }); return res.status(200).send({ ids: storedAccountsIds });
} },
} },
}; };

View File

@@ -1,19 +1,12 @@
import express from 'express'; import express from 'express';
import { import { check, param, query, validationResult } from 'express-validator';
check,
param,
query,
validationResult,
} from 'express-validator';
import moment from 'moment'; import moment from 'moment';
import { difference, sumBy, omit } from 'lodash'; import { difference, sumBy, omit } 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 JournalEntry from '@/services/Accounting/JournalEntry'; import JournalEntry from '@/services/Accounting/JournalEntry';
import JWTAuth from '@/http/middleware/jwtAuth'; import JWTAuth from '@/http/middleware/jwtAuth';
import { import { mapViewRolesToConditionals } from '@/lib/ViewRolesBuilder';
mapViewRolesToConditionals,
} from '@/lib/ViewRolesBuilder';
import { import {
DynamicFilter, DynamicFilter,
DynamicFilterSortBy, DynamicFilterSortBy,
@@ -21,7 +14,6 @@ import {
DynamicFilterFilterRoles, DynamicFilterFilterRoles,
} from '@/lib/DynamicFilter'; } from '@/lib/DynamicFilter';
export default { export default {
/** /**
* Router constructor. * Router constructor.
@@ -30,33 +22,47 @@ export default {
const router = express.Router(); const router = express.Router();
router.use(JWTAuth); router.use(JWTAuth);
router.post('/', router.post(
'/',
this.newExpense.validation, this.newExpense.validation,
asyncMiddleware(this.newExpense.handler)); asyncMiddleware(this.newExpense.handler)
);
router.post('/:id/publish', router.post(
'/:id/publish',
this.publishExpense.validation, this.publishExpense.validation,
asyncMiddleware(this.publishExpense.handler)); asyncMiddleware(this.publishExpense.handler)
);
router.delete('/:id', router.delete(
'/:id',
this.deleteExpense.validation, this.deleteExpense.validation,
asyncMiddleware(this.deleteExpense.handler)); asyncMiddleware(this.deleteExpense.handler)
);
router.delete('/', router.delete(
'/',
this.deleteBulkExpenses.validation, this.deleteBulkExpenses.validation,
asyncMiddleware(this.deleteBulkExpenses.handler)); asyncMiddleware(this.deleteBulkExpenses.handler)
);
router.post('/:id', router.post(
'/:id',
this.updateExpense.validation, this.updateExpense.validation,
asyncMiddleware(this.updateExpense.handler)); asyncMiddleware(this.updateExpense.handler)
);
router.get('/', router.get(
'/',
this.listExpenses.validation, this.listExpenses.validation,
asyncMiddleware(this.listExpenses.handler)); asyncMiddleware(this.listExpenses.handler)
);
router.get('/:id', router.get(
'/:id',
this.getExpense.validation, this.getExpense.validation,
asyncMiddleware(this.getExpense.handler)); asyncMiddleware(this.getExpense.handler)
);
return router; return router;
}, },
@@ -66,20 +72,27 @@ export default {
*/ */
newExpense: { newExpense: {
validation: [ validation: [
check('reference_no').optional().trim().escape(), check('reference_no').optional().trim().escape().isLength({
max: 255,
}),
check('payment_date').isISO8601().optional(), check('payment_date').isISO8601().optional(),
check('payment_account_id').exists().isNumeric().toInt(), check('payment_account_id').exists().isNumeric().toInt(),
check('description').optional(), check('description').optional(),
check('currency_code').optional(), check('currency_code').optional(),
check('exchange_rate').optional().isNumeric().toFloat(), check('exchange_rate').optional().isNumeric().toFloat(),
check('publish').optional().isBoolean().toBoolean(), check('publish').optional().isBoolean().toBoolean(),
check('categories').exists().isArray({ min: 1 }), check('categories').exists().isArray({ min: 1 }),
check('categories.*.index').exists().isNumeric().toInt(), check('categories.*.index').exists().isNumeric().toInt(),
check('categories.*.expense_account_id').exists().isNumeric().toInt(), check('categories.*.expense_account_id').exists().isNumeric().toInt(),
check('categories.*.amount').optional().isNumeric().toFloat(), check('categories.*.amount')
check('categories.*.description').optional().trim().escape(), .optional({ nullable: true })
.isNumeric()
.isDecimal()
.isFloat({ max: 9999999999.999 }) // 13, 3
.toFloat(),
check('categories.*.description').optional().trim().escape().isLength({
max: 255,
}),
check('custom_fields').optional().isArray({ min: 0 }), check('custom_fields').optional().isArray({ min: 0 }),
check('custom_fields.*.key').exists().trim().escape(), check('custom_fields.*.key').exists().trim().escape(),
check('custom_fields.*.value').exists(), check('custom_fields.*.value').exists(),
@@ -89,7 +102,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 { user } = req; const { user } = req;
@@ -103,24 +117,37 @@ export default {
...req.body, ...req.body,
}; };
const totalAmount = sumBy(form.categories, 'amount'); const totalAmount = sumBy(form.categories, 'amount');
const expenseAccountsIds = form.categories.map((account) => account.expense_account_id) const expenseAccountsIds = form.categories.map(
(account) => account.expense_account_id
);
const storedExpenseAccounts = await Account.query().whereIn('id', expenseAccountsIds); const storedExpenseAccounts = await Account.query().whereIn(
const storedExpenseAccountsIds = storedExpenseAccounts.map(a => a.id); 'id',
expenseAccountsIds
);
const storedExpenseAccountsIds = storedExpenseAccounts.map((a) => a.id);
const notStoredExpensesAccountsIds = difference(expenseAccountsIds, storedExpenseAccountsIds); const notStoredExpensesAccountsIds = difference(
expenseAccountsIds,
storedExpenseAccountsIds
);
const errorReasons = []; const errorReasons = [];
const paymentAccount = await Account.query().where('id', form.payment_account_id).first(); const paymentAccount = await Account.query()
.where('id', form.payment_account_id)
.first();
if (!paymentAccount) { if (!paymentAccount) {
errorReasons.push({ errorReasons.push({
type: 'PAYMENT.ACCOUNT.NOT.FOUND', code: 500, type: 'PAYMENT.ACCOUNT.NOT.FOUND',
code: 500,
}); });
} }
if (notStoredExpensesAccountsIds.length > 0) { if (notStoredExpensesAccountsIds.length > 0) {
errorReasons.push({ errorReasons.push({
type: 'EXPENSE.ACCOUNTS.IDS.NOT.STORED', code: 400, ids: notStoredExpensesAccountsIds, type: 'EXPENSE.ACCOUNTS.IDS.NOT.STORED',
code: 400,
ids: notStoredExpensesAccountsIds,
}); });
} }
if (totalAmount <= 0) { if (totalAmount <= 0) {
@@ -162,7 +189,7 @@ export default {
account: paymentAccount.id, account: paymentAccount.id,
...mixinEntry, ...mixinEntry,
}); });
journalPoster.credit(paymentJournalEntry) journalPoster.credit(paymentJournalEntry);
form.categories.forEach((category) => { form.categories.forEach((category) => {
const expenseJournalEntry = new JournalEntry({ const expenseJournalEntry = new JournalEntry({
@@ -176,7 +203,7 @@ export default {
await Promise.all([ await Promise.all([
...storeExpenseCategoriesOper, ...storeExpenseCategoriesOper,
journalPoster.saveEntries(), journalPoster.saveEntries(),
(form.status) && journalPoster.saveBalance(), form.status && journalPoster.saveBalance(),
]); ]);
return res.status(200).send({ id: expenseTransaction.id }); return res.status(200).send({ id: expenseTransaction.id });
@@ -187,15 +214,14 @@ export default {
* Publish the given expense id. * Publish the given expense id.
*/ */
publishExpense: { publishExpense: {
validation: [ validation: [param('id').exists().isNumeric().toInt()],
param('id').exists().isNumeric().toInt(),
],
async handler(req, res) { async handler(req, res) {
const validationErrors = validationResult(req); const validationErrors = validationResult(req);
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 { id } = req.params; const { id } = req.params;
@@ -263,7 +289,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,
}); });
} }
@@ -283,7 +310,7 @@ export default {
.withGraphFetched('fields') .withGraphFetched('fields')
.first(); .first();
const expensesResourceFields = expensesResource.fields.map(f => f.key); const expensesResourceFields = expensesResource.fields.map((f) => f.key);
if (!expensesResource) { if (!expensesResource) {
return res.status(400).send({ return res.status(400).send({
@@ -309,7 +336,7 @@ export default {
} }
const sortByFilter = new DynamicFilterSortBy( const sortByFilter = new DynamicFilterSortBy(
filter.column_sort_by, filter.column_sort_by,
filter.sort_order, filter.sort_order
); );
dynamicFilter.setFilter(sortByFilter); dynamicFilter.setFilter(sortByFilter);
} }
@@ -317,10 +344,13 @@ export default {
if (view && view.roles.length > 0) { if (view && view.roles.length > 0) {
const viewFilter = new DynamicFilterViews( const viewFilter = new DynamicFilterViews(
mapViewRolesToConditionals(view.roles), mapViewRolesToConditionals(view.roles),
view.rolesLogicExpression, view.rolesLogicExpression
); );
if (viewFilter.validateFilterRoles()) { if (viewFilter.validateFilterRoles()) {
errorReasons.push({ type: 'VIEW.LOGIC.EXPRESSION.INVALID', code: 400 }); errorReasons.push({
type: 'VIEW.LOGIC.EXPRESSION.INVALID',
code: 400,
});
} }
dynamicFilter.setFilter(viewFilter); dynamicFilter.setFilter(viewFilter);
} }
@@ -328,32 +358,39 @@ export default {
if (filter.filter_roles.length > 0) { if (filter.filter_roles.length > 0) {
const filterRoles = new DynamicFilterFilterRoles( const filterRoles = new DynamicFilterFilterRoles(
mapFilterRolesToDynamicFilter(filter.filter_roles), mapFilterRolesToDynamicFilter(filter.filter_roles),
expensesResource.fields, expensesResource.fields
); );
if (filterRoles.validateFilterRoles().length > 0) { if (filterRoles.validateFilterRoles().length > 0) {
errorReasons.push({ type: 'ACCOUNTS.RESOURCE.HAS.NO.GIVEN.FIELDS', code: 500 }); errorReasons.push({
type: 'ACCOUNTS.RESOURCE.HAS.NO.GIVEN.FIELDS',
code: 500,
});
} }
dynamicFilter.setFilter(filterRoles); dynamicFilter.setFilter(filterRoles);
} }
if (errorReasons.length > 0) { if (errorReasons.length > 0) {
return res.status(400).send({ errors: errorReasons }); return res.status(400).send({ errors: errorReasons });
} }
const expenses = await Expense.query().onBuild((builder) => { const expenses = await Expense.query()
.onBuild((builder) => {
builder.withGraphFetched('paymentAccount'); builder.withGraphFetched('paymentAccount');
builder.withGraphFetched('categories.expenseAccount'); builder.withGraphFetched('categories.expenseAccount');
builder.withGraphFetched('user'); builder.withGraphFetched('user');
dynamicFilter.buildQuery()(builder); dynamicFilter.buildQuery()(builder);
}).pagination(filter.page - 1, filter.page_size);; })
.pagination(filter.page - 1, filter.page_size);
return res.status(200).send({ return res.status(200).send({
expenses: { expenses: {
...expenses, ...expenses,
...(view) ? { ...(view
? {
viewMeta: { viewMeta: {
viewColumns: view.columns, viewColumns: view.columns,
customViewId: view.id, customViewId: view.id,
},
} }
} : {}, : {}),
}, },
}); });
}, },
@@ -363,15 +400,14 @@ export default {
* Delete the given expense transaction. * Delete the given expense transaction.
*/ */
deleteExpense: { deleteExpense: {
validation: [ validation: [param('id').isNumeric().toInt()],
param('id').isNumeric().toInt(),
],
async handler(req, res) { async handler(req, res) {
const validationErrors = validationResult(req); const validationErrors = validationResult(req);
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 { id } = req.params; const { id } = req.params;
@@ -385,9 +421,14 @@ export default {
const expense = await Expense.query().where('id', id).first(); const expense = await Expense.query().where('id', id).first();
if (!expense) { if (!expense) {
return res.status(404).send({ errors: [{ return res.status(404).send({
type: 'EXPENSE.NOT.FOUND', code: 200, errors: [
}] }); {
type: 'EXPENSE.NOT.FOUND',
code: 200,
},
],
});
} }
await ExpenseCategory.query().where('expense_id', id).delete(); await ExpenseCategory.query().where('expense_id', id).delete();
@@ -437,12 +478,18 @@ 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 { id } = req.params; const { id } = req.params;
const { user } = req; const { user } = req;
const { Account, Expense, ExpenseCategory, AccountTransaction } = req.models; const {
Account,
Expense,
ExpenseCategory,
AccountTransaction,
} = req.models;
const form = { const form = {
categories: [], categories: [],
@@ -463,39 +510,55 @@ export default {
} }
const errorReasons = []; const errorReasons = [];
const paymentAccount = await Account.query() const paymentAccount = await Account.query()
.where('id', form.payment_account_id).first(); .where('id', form.payment_account_id)
.first();
if (!paymentAccount) { if (!paymentAccount) {
errorReasons.push({ type: 'PAYMENT.ACCOUNT.NOT.FOUND', code: 400 }); errorReasons.push({ type: 'PAYMENT.ACCOUNT.NOT.FOUND', code: 400 });
} }
const categoriesHasNoId = form.categories.filter(c => !c.id); const categoriesHasNoId = form.categories.filter((c) => !c.id);
const categoriesHasId = form.categories.filter(c => c.id); const categoriesHasId = form.categories.filter((c) => c.id);
const expenseCategoriesIds = expense.categories.map((c) => c.id); const expenseCategoriesIds = expense.categories.map((c) => c.id);
const formExpenseCategoriesIds = categoriesHasId.map(c => c.id); const formExpenseCategoriesIds = categoriesHasId.map((c) => c.id);
const categoriesIdsDeleted = difference( const categoriesIdsDeleted = difference(
formExpenseCategoriesIds, expenseCategoriesIds, formExpenseCategoriesIds,
expenseCategoriesIds
); );
const categoriesShouldDelete = difference( const categoriesShouldDelete = difference(
expenseCategoriesIds, formExpenseCategoriesIds, expenseCategoriesIds,
formExpenseCategoriesIds
); );
const formExpensesAccountsIds = form.categories.map(c => c.expense_account_id); const formExpensesAccountsIds = form.categories.map(
const storedExpenseAccounts = await Account.query().whereIn('id', formExpensesAccountsIds); (c) => c.expense_account_id
const storedExpenseAccountsIds = storedExpenseAccounts.map(a => a.id); );
const storedExpenseAccounts = await Account.query().whereIn(
'id',
formExpensesAccountsIds
);
const storedExpenseAccountsIds = storedExpenseAccounts.map((a) => a.id);
const expenseAccountsIdsNotFound = difference( const expenseAccountsIdsNotFound = difference(
formExpensesAccountsIds, storedExpenseAccountsIds, formExpensesAccountsIds,
storedExpenseAccountsIds
); );
const totalAmount = sumBy(form.categories, 'amount'); const totalAmount = sumBy(form.categories, 'amount');
if (expenseAccountsIdsNotFound.length > 0) { if (expenseAccountsIdsNotFound.length > 0) {
errorReasons.push({ type: 'EXPENSE.ACCOUNTS.IDS.NOT.FOUND', code: 600, ids: expenseAccountsIdsNotFound }) errorReasons.push({
type: 'EXPENSE.ACCOUNTS.IDS.NOT.FOUND',
code: 600,
ids: expenseAccountsIdsNotFound,
});
} }
if (categoriesIdsDeleted.length > 0) { if (categoriesIdsDeleted.length > 0) {
errorReasons.push({ type: 'EXPENSE.CATEGORIES.IDS.NOT.FOUND', code: 300 }); errorReasons.push({
type: 'EXPENSE.CATEGORIES.IDS.NOT.FOUND',
code: 300,
});
} }
if (totalAmount <= 0) { if (totalAmount <= 0) {
errorReasons.push({ type: 'TOTAL.AMOUNT.EQUALS.ZERO', code: 500 }); errorReasons.push({ type: 'TOTAL.AMOUNT.EQUALS.ZERO', code: 500 });
@@ -504,8 +567,9 @@ export default {
if (errorReasons.length > 0) { if (errorReasons.length > 0) {
return res.status(400).send({ errors: errorReasons }); return res.status(400).send({ errors: errorReasons });
} }
const expenseCategoriesMap = new Map(expense.categories const expenseCategoriesMap = new Map(
.map(category => [category.id, category])); expense.categories.map((category) => [category.id, category])
);
const categoriesInsertOpers = []; const categoriesInsertOpers = [];
const categoriesUpdateOpers = []; const categoriesUpdateOpers = [];
@@ -519,25 +583,30 @@ export default {
}); });
categoriesHasId.forEach((category) => { categoriesHasId.forEach((category) => {
const oper = ExpenseCategory.query().where('id', category.id) const oper = ExpenseCategory.query()
.where('id', category.id)
.patch({ .patch({
...omit(category, ['id']), ...omit(category, ['id']),
}); });
categoriesUpdateOpers.push(oper); categoriesUpdateOpers.push(oper);
}); });
const updateExpenseOper = Expense.query().where('id', id) const updateExpenseOper = Expense.query()
.where('id', id)
.update({ .update({
payment_date: moment(form.payment_date).format('YYYY-MM-DD'), payment_date: moment(form.payment_date).format('YYYY-MM-DD'),
total_amount: totalAmount, total_amount: totalAmount,
description: form.description, description: form.description,
payment_account_id: form.payment_account_id, payment_account_id: form.payment_account_id,
reference_no: form.reference_no, reference_no: form.reference_no,
}) });
const deleteCategoriesOper = (categoriesShouldDelete.length > 0) ? const deleteCategoriesOper =
ExpenseCategory.query().whereIn('id', categoriesShouldDelete).delete() : categoriesShouldDelete.length > 0
Promise.resolve(); ? ExpenseCategory.query()
.whereIn('id', categoriesShouldDelete)
.delete()
: Promise.resolve();
// Update the journal entries. // Update the journal entries.
const transactions = await AccountTransaction.query() const transactions = await AccountTransaction.query()
@@ -581,7 +650,7 @@ export default {
deleteCategoriesOper, deleteCategoriesOper,
journal.saveEntries(), journal.saveEntries(),
(form.status) && journal.saveBalance(), form.status && journal.saveBalance(),
]); ]);
return res.status(200).send({ id }); return res.status(200).send({ id });
}, },
@@ -591,15 +660,14 @@ export default {
* Retrieve details of the given expense id. * Retrieve details of the given expense id.
*/ */
getExpense: { getExpense: {
validation: [ validation: [param('id').exists().isNumeric().toInt()],
param('id').exists().isNumeric().toInt(),
],
async handler(req, res) { async handler(req, res) {
const validationErrors = validationResult(req); const validationErrors = validationResult(req);
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 { id } = req.params; const { id } = req.params;
@@ -624,9 +692,9 @@ export default {
return res.status(200).send({ return res.status(200).send({
expense: { expense: {
...expense, ...expense.toJSON(),
journalEntries, journalEntries,
} },
}); });
}, },
}, },
@@ -644,16 +712,16 @@ 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 filter = { ...req.query }; const filter = { ...req.query };
const { Expense, AccountTransaction, Account, MediaLink } = req.models; const { Expense, AccountTransaction, Account, MediaLink } = req.models;
const expenses = await Expense.query() const expenses = await Expense.query().whereIn('id', filter.ids);
.whereIn('id', filter.ids)
const storedExpensesIds = expenses.map(e => e.id); const storedExpensesIds = expenses.map((e) => e.id);
const notFoundExpenses = difference(filter.ids, storedExpensesIds); const notFoundExpenses = difference(filter.ids, storedExpensesIds);
if (notFoundExpenses.length > 0) { if (notFoundExpenses.length > 0) {
@@ -663,11 +731,12 @@ export default {
} }
const deleteExpensesOper = Expense.query() const deleteExpensesOper = Expense.query()
.whereIn('id', storedExpensesIds).delete(); .whereIn('id', storedExpensesIds)
.delete();
const transactions = await AccountTransaction.query() const transactions = await AccountTransaction.query()
.whereIn('reference_type', ['Expense']) .whereIn('reference_type', ['Expense'])
.whereIn('reference_id', filter.ids) .whereIn('reference_id', filter.ids);
const accountsDepGraph = await Account.depGraph().query().remember(); const accountsDepGraph = await Account.depGraph().query().remember();
const journal = new JournalPoster(accountsDepGraph); const journal = new JournalPoster(accountsDepGraph);
@@ -686,6 +755,6 @@ export default {
journal.saveBalance(), journal.saveBalance(),
]); ]);
return res.status(200).send({ ids: filter.ids }); return res.status(200).send({ ids: filter.ids });
} },
} },
}; };