mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-22 07:40:32 +00:00
- 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:
@@ -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(
|
||||||
|
|||||||
46
client/src/components/AccountsTypesSelect.js
Normal file
46
client/src/components/AccountsTypesSelect.js
Normal 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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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(
|
||||||
updateData(index, id, account.id);
|
(account) => {
|
||||||
}, [updateData, index, id]);
|
updateData(index, id, account.id);
|
||||||
|
},
|
||||||
const { account_id = false } = (errors[index] || {});
|
[updateData, index, id],
|
||||||
|
);
|
||||||
// const initialAccount = useMemo(() =>
|
const error = errors?.[index]?.[id];
|
||||||
// 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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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 (
|
||||||
value={value}
|
<FormGroup>
|
||||||
onChange={onChange}
|
<InputGroup
|
||||||
onBlur={onBlur}
|
value={value}
|
||||||
fill={true} />);
|
onChange={onChange}
|
||||||
|
onBlur={onBlur}
|
||||||
|
fill={true}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default InputEditableCell;
|
export default InputEditableCell;
|
||||||
|
|||||||
@@ -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];
|
||||||
value={value}
|
|
||||||
prefix={'$'}
|
return (
|
||||||
onChange={handleFieldChange}
|
<FormGroup
|
||||||
inputGroupProps={{
|
intent={error ? Intent.DANGER : null}>
|
||||||
fill: true,
|
<MoneyInputGroup
|
||||||
onBlur,
|
value={value}
|
||||||
}} />)
|
prefix={'$'}
|
||||||
|
onChange={handleFieldChange}
|
||||||
|
inputGroupProps={{
|
||||||
|
fill: true,
|
||||||
|
onBlur,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default MoneyFieldCellRenderer;
|
export default MoneyFieldCellRenderer;
|
||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
@@ -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}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
const afterSubmit = () => {
|
||||||
|
closeDialog(dialogName);
|
||||||
|
queryCache.invalidateQueries('accounts-table');
|
||||||
|
queryCache.invalidateQueries('accounts-list');
|
||||||
|
};
|
||||||
|
|
||||||
|
const afterErrors = (errors) => {
|
||||||
|
const errorsTransformed = transformApiErrors(errors);
|
||||||
|
setErrors({ ...errorsTransformed });
|
||||||
|
setSubmitting(false);
|
||||||
|
};
|
||||||
|
|
||||||
if (payload.action === 'edit') {
|
if (payload.action === 'edit') {
|
||||||
requestEditAccount(payload.id, values)
|
requestEditAccount(payload.id, form)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
closeDialog(dialogName);
|
afterSubmit(response);
|
||||||
queryCache.invalidateQueries('accounts-table');
|
|
||||||
|
|
||||||
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>
|
||||||
|
|||||||
@@ -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',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
|||||||
@@ -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: [
|
||||||
...pick(category, Object.keys(defaultCategory)),
|
...expense.categories.map((category) => ({
|
||||||
})),
|
...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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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);
|
||||||
};
|
};
|
||||||
@@ -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.'
|
||||||
};
|
};
|
||||||
|
|||||||
23
client/src/services/yup.js
Normal file
23
client/src/services/yup.js
Normal 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;
|
||||||
@@ -262,7 +262,9 @@ export const fetchAccount = ({ id }) => {
|
|||||||
.then((response) => {
|
.then((response) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: t.ACCOUNT_SET,
|
type: t.ACCOUNT_SET,
|
||||||
account: response.data.account,
|
payload: {
|
||||||
|
account: response.data.account,
|
||||||
|
}
|
||||||
});
|
});
|
||||||
resolve(response);
|
resolve(response);
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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{
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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');
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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');;
|
||||||
|
|||||||
@@ -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()
|
||||||
dynamicFilter.buildQuery()(builder);
|
.onBuild((builder) => {
|
||||||
}).pagination(filter.page - 1, filter.page_size);
|
dynamicFilter.buildQuery()(builder);
|
||||||
|
})
|
||||||
|
.pagination(filter.page - 1, filter.page_size);
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
manualJournals: {
|
manualJournals: {
|
||||||
...manualJournals,
|
...manualJournals,
|
||||||
...(view) ? {
|
...(view
|
||||||
viewMeta: {
|
? {
|
||||||
customViewId: view.id,
|
viewMeta: {
|
||||||
}
|
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,86 +267,111 @@ 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')
|
||||||
);
|
);
|
||||||
|
|
||||||
if (entriesHasNoReceivableAccount.length > 0) {
|
if (entriesHasNoReceivableAccount.length > 0) {
|
||||||
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')
|
||||||
);
|
);
|
||||||
|
|
||||||
if (entriesHasNoVendorContact.length > 0) {
|
if (entriesHasNoVendorContact.length > 0) {
|
||||||
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,16 +709,14 @@ 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)
|
reference: form.reference,
|
||||||
.update({
|
journal_type: form.journal_type,
|
||||||
reference: form.reference,
|
journalNumber: form.journal_number,
|
||||||
transaction_type: 'Journal',
|
amount: totalCredit,
|
||||||
journalNumber: form.journal_number,
|
date: formattedDate,
|
||||||
amount: totalCredit,
|
description: form.description,
|
||||||
date: formattedDate,
|
});
|
||||||
description: form.description,
|
|
||||||
});
|
|
||||||
|
|
||||||
const transactions = await AccountTransaction.query()
|
const transactions = await AccountTransaction.query()
|
||||||
.whereIn('reference_type', ['Journal'])
|
.whereIn('reference_type', ['Journal'])
|
||||||
@@ -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 });
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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) {
|
||||||
@@ -341,16 +365,18 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const query = Account.query()
|
const query = Account.query()
|
||||||
// .remember()
|
// .remember()
|
||||||
.onBuild((builder) => {
|
.onBuild((builder) => {
|
||||||
builder.modify('filterAccountTypes', filter.account_types);
|
builder.modify('filterAccountTypes', filter.account_types);
|
||||||
builder.withGraphFetched('type');
|
builder.withGraphFetched('type');
|
||||||
builder.withGraphFetched('balance');
|
builder.withGraphFetched('balance');
|
||||||
|
|
||||||
dynamicFilter.buildQuery()(builder);
|
dynamicFilter.buildQuery()(builder);
|
||||||
|
|
||||||
// console.log(builder.toKnexQuery().toSQL());
|
// 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',
|
{
|
||||||
code: 200,
|
type: 'ACCOUNTS.IDS.NOT.FOUND',
|
||||||
ids: notFoundAccounts,
|
code: 200,
|
||||||
}],
|
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 });
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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()
|
||||||
builder.withGraphFetched('paymentAccount');
|
.onBuild((builder) => {
|
||||||
builder.withGraphFetched('categories.expenseAccount');
|
builder.withGraphFetched('paymentAccount');
|
||||||
builder.withGraphFetched('user');
|
builder.withGraphFetched('categories.expenseAccount');
|
||||||
dynamicFilter.buildQuery()(builder);
|
builder.withGraphFetched('user');
|
||||||
}).pagination(filter.page - 1, filter.page_size);;
|
dynamicFilter.buildQuery()(builder);
|
||||||
|
})
|
||||||
|
.pagination(filter.page - 1, filter.page_size);
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
expenses: {
|
expenses: {
|
||||||
...expenses,
|
...expenses,
|
||||||
...(view) ? {
|
...(view
|
||||||
viewMeta: {
|
? {
|
||||||
viewColumns: view.columns,
|
viewMeta: {
|
||||||
customViewId: view.id,
|
viewColumns: view.columns,
|
||||||
}
|
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 });
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user