mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-16 04: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({
|
||||
accounts,
|
||||
onAccountSelected,
|
||||
error = [],
|
||||
initialAccountId,
|
||||
selectedAccountId,
|
||||
defaultSelectText = 'Select account',
|
||||
onAccountSelected,
|
||||
}) {
|
||||
// Find initial account object to set it as default account in initial render.
|
||||
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
|
||||
);
|
||||
|
||||
const contactTypeLabel = (contactType) => {
|
||||
switch(contactType) {
|
||||
case 'customer':
|
||||
return 'Customer';
|
||||
case 'vendor':
|
||||
return 'Vendor';
|
||||
}
|
||||
};
|
||||
// Contact item of select accounts field.
|
||||
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) => {
|
||||
|
||||
@@ -1,44 +1,43 @@
|
||||
import React, {useCallback, useMemo} from 'react';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import AccountsSelectList from 'components/AccountsSelectList';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
FormGroup,
|
||||
Classes,
|
||||
Intent,
|
||||
} from '@blueprintjs/core';
|
||||
import { FormGroup, Classes, Intent } from '@blueprintjs/core';
|
||||
|
||||
// Account cell renderer.
|
||||
const AccountCellRenderer = ({
|
||||
column: { id },
|
||||
column: { id, accountsDataProp },
|
||||
row: { index, original },
|
||||
cell: { value: initialValue },
|
||||
payload: { accounts, updateData, errors },
|
||||
payload: { accounts: defaultAccounts, updateData, errors, ...restProps },
|
||||
}) => {
|
||||
const handleAccountSelected = useCallback((account) => {
|
||||
updateData(index, id, account.id);
|
||||
}, [updateData, index, id]);
|
||||
|
||||
const { account_id = false } = (errors[index] || {});
|
||||
|
||||
// const initialAccount = useMemo(() =>
|
||||
// accounts.find(a => a.id === initialValue),
|
||||
// [accounts, initialValue]);
|
||||
const handleAccountSelected = useCallback(
|
||||
(account) => {
|
||||
updateData(index, id, account.id);
|
||||
},
|
||||
[updateData, index, id],
|
||||
);
|
||||
const error = errors?.[index]?.[id];
|
||||
|
||||
const accounts = useMemo(
|
||||
() => restProps[accountsDataProp] || defaultAccounts,
|
||||
[restProps, defaultAccounts, accountsDataProp],
|
||||
);
|
||||
return (
|
||||
<FormGroup
|
||||
intent={account_id ? Intent.DANGER : ''}
|
||||
intent={error ? Intent.DANGER : null}
|
||||
className={classNames(
|
||||
'form-group--select-list',
|
||||
'form-group--account',
|
||||
Classes.FILL)}
|
||||
>
|
||||
Classes.FILL,
|
||||
)}
|
||||
>
|
||||
<AccountsSelectList
|
||||
accounts={accounts}
|
||||
onAccountSelected={handleAccountSelected}
|
||||
error={account_id}
|
||||
selectedAccountId={initialValue} />
|
||||
selectedAccountId={initialValue}
|
||||
/>
|
||||
</FormGroup>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccountCellRenderer;
|
||||
export default AccountCellRenderer;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { FormGroup, Classes } from "@blueprintjs/core";
|
||||
import { FormGroup, Intent, Classes } from "@blueprintjs/core";
|
||||
import classNames from 'classnames';
|
||||
import ContactsListField from 'components/ContactsListField';
|
||||
|
||||
@@ -20,8 +20,11 @@ export default function ContactsListCellRenderer({
|
||||
return contacts.find(c => c.id === initialValue);
|
||||
}, [contacts, initialValue]);
|
||||
|
||||
const error = errors?.[index]?.[id];
|
||||
|
||||
return (
|
||||
<FormGroup
|
||||
intent={error ? Intent.DANGER : null}
|
||||
className={classNames(
|
||||
'form-group--select-list',
|
||||
'form-group--contacts-list',
|
||||
@@ -32,6 +35,7 @@ export default function ContactsListCellRenderer({
|
||||
contacts={contacts}
|
||||
onContactSelected={handleContactSelected}
|
||||
initialContact={initialContact}
|
||||
|
||||
/>
|
||||
</FormGroup>
|
||||
)
|
||||
|
||||
@@ -1,31 +1,35 @@
|
||||
import React, {useState, useEffect} from 'react';
|
||||
import {
|
||||
InputGroup
|
||||
} from '@blueprintjs/core';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { Classes, InputGroup, FormGroup } from '@blueprintjs/core';
|
||||
|
||||
const InputEditableCell = ({
|
||||
row: { index },
|
||||
column: { id, },
|
||||
column: { id },
|
||||
cell: { value: initialValue },
|
||||
payload,
|
||||
}) => {
|
||||
const [value, setValue] = useState(initialValue)
|
||||
const [value, setValue] = useState(initialValue);
|
||||
|
||||
const onChange = e => {
|
||||
setValue(e.target.value)
|
||||
}
|
||||
const onChange = (e) => {
|
||||
setValue(e.target.value);
|
||||
};
|
||||
const onBlur = () => {
|
||||
payload.updateData(index, id, value)
|
||||
}
|
||||
payload.updateData(index, id, value);
|
||||
};
|
||||
useEffect(() => {
|
||||
setValue(initialValue)
|
||||
}, [initialValue])
|
||||
setValue(initialValue);
|
||||
}, [initialValue]);
|
||||
|
||||
return (<InputGroup
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
fill={true} />);
|
||||
return (
|
||||
<FormGroup>
|
||||
<InputGroup
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
fill={true}
|
||||
/>
|
||||
</FormGroup>
|
||||
);
|
||||
};
|
||||
|
||||
export default InputEditableCell;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useCallback, useState, useEffect } from 'react';
|
||||
import { FormGroup, Intent } from '@blueprintjs/core';
|
||||
import MoneyInputGroup from 'components/MoneyInputGroup';
|
||||
|
||||
// Input form cell renderer.
|
||||
@@ -6,7 +7,7 @@ const MoneyFieldCellRenderer = ({
|
||||
row: { index },
|
||||
column: { id },
|
||||
cell: { value: initialValue },
|
||||
payload
|
||||
payload: { errors, updateData },
|
||||
}) => {
|
||||
const [value, setValue] = useState(initialValue);
|
||||
|
||||
@@ -15,26 +16,36 @@ const MoneyFieldCellRenderer = ({
|
||||
}, []);
|
||||
|
||||
function isNumeric(data) {
|
||||
return !isNaN(parseFloat(data)) && isFinite(data) && data.constructor !== Array;
|
||||
return (
|
||||
!isNaN(parseFloat(data)) && isFinite(data) && data.constructor !== Array
|
||||
);
|
||||
}
|
||||
|
||||
const onBlur = () => {
|
||||
const updateValue = isNumeric(value) ? parseFloat(value) : value;
|
||||
payload.updateData(index, id, updateValue);
|
||||
updateData(index, id, updateValue);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setValue(initialValue);
|
||||
}, [initialValue])
|
||||
}, [initialValue]);
|
||||
|
||||
return (<MoneyInputGroup
|
||||
value={value}
|
||||
prefix={'$'}
|
||||
onChange={handleFieldChange}
|
||||
inputGroupProps={{
|
||||
fill: true,
|
||||
onBlur,
|
||||
}} />)
|
||||
const error = errors?.[index]?.[id];
|
||||
|
||||
return (
|
||||
<FormGroup
|
||||
intent={error ? Intent.DANGER : null}>
|
||||
<MoneyInputGroup
|
||||
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} />;
|
||||
|
||||
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 (
|
||||
|
||||
@@ -19,6 +19,7 @@ import Dialog from './Dialog';
|
||||
import AppToaster from './AppToaster';
|
||||
import DataTable from './DataTable';
|
||||
import AccountsSelectList from './AccountsSelectList';
|
||||
import AccountsTypesSelect from './AccountsTypesSelect';
|
||||
|
||||
const Hint = FieldHint;
|
||||
|
||||
@@ -45,4 +46,5 @@ export {
|
||||
AppToaster,
|
||||
DataTable,
|
||||
AccountsSelectList,
|
||||
AccountsTypesSelect,
|
||||
};
|
||||
@@ -10,7 +10,7 @@ import { useFormik } from 'formik';
|
||||
import moment from 'moment';
|
||||
import { Intent } from '@blueprintjs/core';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { pick } from 'lodash';
|
||||
import { pick, setWith } from 'lodash';
|
||||
|
||||
import MakeJournalEntriesHeader from './MakeJournalEntriesHeader';
|
||||
import MakeJournalEntriesFooter from './MakeJournalEntriesFooter';
|
||||
@@ -89,19 +89,23 @@ function MakeJournalEntriesForm({
|
||||
const validationSchema = Yup.object().shape({
|
||||
journal_number: Yup.string()
|
||||
.required()
|
||||
.min(1)
|
||||
.max(255)
|
||||
.label(formatMessage({ id: 'journal_number_' })),
|
||||
journal_type: Yup.string()
|
||||
.required()
|
||||
.min(1)
|
||||
.max(255)
|
||||
.label(formatMessage({ id: 'journal_type' })),
|
||||
date: Yup.date()
|
||||
.required()
|
||||
.label(formatMessage({ id: 'date' })),
|
||||
reference: Yup.string(),
|
||||
description: Yup.string(),
|
||||
reference: Yup.string().min(1).max(255),
|
||||
description: Yup.string().min(1).max(1024),
|
||||
entries: Yup.array().of(
|
||||
Yup.object().shape({
|
||||
credit: Yup.number().nullable(),
|
||||
debit: Yup.number().nullable(),
|
||||
credit: Yup.number().decimalScale(13).nullable(),
|
||||
debit: Yup.number().decimalScale(13).nullable(),
|
||||
account_id: Yup.number()
|
||||
.nullable()
|
||||
.when(['credit', 'debit'], {
|
||||
@@ -109,7 +113,7 @@ function MakeJournalEntriesForm({
|
||||
then: Yup.number().required(),
|
||||
}),
|
||||
contact_id: Yup.number().nullable(),
|
||||
note: Yup.string().nullable(),
|
||||
note: Yup.string().max(255).nullable(),
|
||||
}),
|
||||
),
|
||||
});
|
||||
@@ -180,48 +184,66 @@ function MakeJournalEntriesForm({
|
||||
}, [manualJournal]);
|
||||
|
||||
// Transform API errors in toasts messages.
|
||||
const transformErrors = (errors, { setErrors }) => {
|
||||
const hasError = (errorType) => errors.some((e) => e.type === errorType);
|
||||
const transformErrors = (resErrors, { setErrors, errors }) => {
|
||||
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)) {
|
||||
AppToaster.show({
|
||||
message: formatMessage({
|
||||
id: 'customers_should_assign_with_receivable_account_only',
|
||||
}),
|
||||
intent: Intent.DANGER,
|
||||
const setEntriesErrors = (indexes, prop, message) =>
|
||||
indexes.forEach((i) => {
|
||||
const index = Math.max(i - 1, 0);
|
||||
newErrors = setWith(newErrors, `entries.[${index}].${prop}`, message);
|
||||
});
|
||||
}
|
||||
if (hasError(ERROR.PAYABLE_ENTRIES_HAS_NO_VENDORS)) {
|
||||
AppToaster.show({
|
||||
message: formatMessage({
|
||||
id: 'vendors_should_assign_with_payable_account_only',
|
||||
|
||||
if ((error = getError(ERROR.PAYABLE_ENTRIES_HAS_NO_VENDORS))) {
|
||||
toastMessages.push(
|
||||
formatMessage({
|
||||
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)) {
|
||||
AppToaster.show({
|
||||
message: formatMessage({
|
||||
id: 'entries_with_receivable_account_no_assigned_with_customers',
|
||||
if ((error = getError(ERROR.RECEIVABLE_ENTRIES_HAS_NO_CUSTOMERS))) {
|
||||
toastMessages.push(
|
||||
formatMessage({
|
||||
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)) {
|
||||
AppToaster.show({
|
||||
message: formatMessage({
|
||||
id: 'entries_with_payable_account_no_assigned_with_vendors',
|
||||
if ((error = getError(ERROR.CUSTOMERS_NOT_WITH_RECEVIABLE_ACC))) {
|
||||
toastMessages.push(
|
||||
formatMessage({
|
||||
id: 'customers_should_selected_with_receivable_account_only',
|
||||
}),
|
||||
intent: Intent.DANGER,
|
||||
});
|
||||
);
|
||||
setEntriesErrors(error.indexes, 'account_id', 'error');
|
||||
}
|
||||
if (hasError(ERROR.JOURNAL_NUMBER_ALREADY_EXISTS)) {
|
||||
setErrors({
|
||||
journal_number: formatMessage({
|
||||
if ((error = getError(ERROR.VENDORS_NOT_WITH_PAYABLE_ACCOUNT))) {
|
||||
toastMessages.push(
|
||||
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',
|
||||
}),
|
||||
});
|
||||
);
|
||||
}
|
||||
setErrors({ ...newErrors });
|
||||
AppToaster.show({
|
||||
message: toastMessages.map((message) => {
|
||||
return <div>- {message}</div>;
|
||||
}),
|
||||
intent: Intent.DANGER,
|
||||
});
|
||||
};
|
||||
|
||||
const formik = useFormik({
|
||||
@@ -255,7 +277,7 @@ function MakeJournalEntriesForm({
|
||||
} else if (totalCredit === 0 || totalDebit === 0) {
|
||||
AppToaster.show({
|
||||
message: formatMessage({
|
||||
id: 'should_total_of_credit_and_debit_be_bigger_then_zero',
|
||||
id: 'amount_cannot_be_zero_or_empty',
|
||||
}),
|
||||
intent: Intent.DANGER,
|
||||
});
|
||||
@@ -353,7 +375,10 @@ function MakeJournalEntriesForm({
|
||||
|
||||
// Handle click on add a new line/row.
|
||||
const handleClickAddNewRow = () => {
|
||||
formik.setFieldValue('entries', [...formik.values.entries, defaultEntry]);
|
||||
formik.setFieldValue(
|
||||
'entries',
|
||||
reorderingEntriesIndex([...formik.values.entries, defaultEntry]),
|
||||
);
|
||||
};
|
||||
|
||||
// Handle click `Clear all lines` button.
|
||||
@@ -370,13 +395,12 @@ function MakeJournalEntriesForm({
|
||||
<MakeJournalEntriesHeader formik={formik} />
|
||||
|
||||
<MakeJournalEntriesTable
|
||||
values={formik.values}
|
||||
values={formik.values.entries}
|
||||
formik={formik}
|
||||
defaultRow={defaultEntry}
|
||||
onClickClearAllLines={handleClickClearLines}
|
||||
onClickAddNewRow={handleClickAddNewRow}
|
||||
/>
|
||||
|
||||
<MakeJournalEntriesFooter
|
||||
formik={formik}
|
||||
onSubmitClick={handleSubmitClick}
|
||||
|
||||
@@ -109,7 +109,7 @@ function MakeJournalEntriesTable({
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
useEffect(() => {
|
||||
setRows([...values.entries.map((e) => ({ ...e, rowType: 'editor' }))]);
|
||||
setRows([...values.map((e) => ({ ...e, rowType: 'editor' }))]);
|
||||
}, [values, setRows]);
|
||||
|
||||
// Final table rows editor rows and total and final blank row.
|
||||
@@ -217,6 +217,9 @@ function MakeJournalEntriesTable({
|
||||
|
||||
const handleRemoveRow = useCallback(
|
||||
(rowIndex) => {
|
||||
// Can't continue if there is just one row line or less.
|
||||
if (rows.length <= 2) { return; }
|
||||
|
||||
const removeIndex = parseInt(rowIndex, 10);
|
||||
const newRows = rows.filter((row, index) => index !== removeIndex);
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ import withManualJournalsActions from 'containers/Accounting/withManualJournalsA
|
||||
function StatusAccessor(row) {
|
||||
return (
|
||||
<Choose>
|
||||
<Choose.When condition={row.status}>
|
||||
<Choose.When condition={!!row.status}>
|
||||
<Tag minimal={true}>
|
||||
<T id={'published'} />
|
||||
</Tag>
|
||||
@@ -178,7 +178,7 @@ function ManualJournalsDataTable({
|
||||
{
|
||||
id: 'status',
|
||||
Header: formatMessage({ id: 'status' }),
|
||||
accessor: StatusAccessor,
|
||||
accessor: row => StatusAccessor(row),
|
||||
width: 95,
|
||||
className: 'status',
|
||||
},
|
||||
|
||||
@@ -306,7 +306,7 @@ function AccountsChart({
|
||||
setBulkInactiveAccounts(false);
|
||||
AppToaster.show({
|
||||
message: formatMessage({
|
||||
id: 'the_accounts_has_been_successfully_inactivated',
|
||||
id: 'the_accounts_have_been_successfully_inactivated',
|
||||
}),
|
||||
intent: Intent.SUCCESS,
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import React, { useCallback, useMemo, useEffect } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Classes,
|
||||
@@ -6,24 +6,24 @@ import {
|
||||
InputGroup,
|
||||
Intent,
|
||||
TextArea,
|
||||
MenuItem,
|
||||
Checkbox,
|
||||
Position,
|
||||
} from '@blueprintjs/core';
|
||||
import * as Yup from 'yup';
|
||||
import { useFormik } from 'formik';
|
||||
import { FormattedMessage as T, useIntl } from 'react-intl';
|
||||
import { If } from 'components';
|
||||
import { omit, pick } from 'lodash';
|
||||
import { pick } from 'lodash';
|
||||
import { useQuery, queryCache } from 'react-query';
|
||||
import classNames from 'classnames';
|
||||
import Yup from 'services/yup';
|
||||
import {
|
||||
ListSelect,
|
||||
If,
|
||||
ErrorMessage,
|
||||
Dialog,
|
||||
AppToaster,
|
||||
FieldRequiredHint,
|
||||
Hint,
|
||||
AccountsSelectList,
|
||||
AccountsTypesSelect,
|
||||
} from 'components';
|
||||
import AccountFormDialogContainer from 'containers/Dialogs/AccountFormDialog.container';
|
||||
|
||||
@@ -53,33 +53,33 @@ function AccountFormDialog({
|
||||
closeDialog,
|
||||
}) {
|
||||
const { formatMessage } = useIntl();
|
||||
const accountFormValidationSchema = Yup.object().shape({
|
||||
const validationSchema = Yup.object().shape({
|
||||
name: Yup.string()
|
||||
.required()
|
||||
.min(3)
|
||||
.max(255)
|
||||
.label(formatMessage({ id: 'account_name_' })),
|
||||
code: Yup.number(),
|
||||
account_type_id: Yup.string()
|
||||
.nullable()
|
||||
code: Yup.string().digits().min(3).max(6),
|
||||
account_type_id: Yup.number()
|
||||
.required()
|
||||
.label(formatMessage({ id: 'account_type_id' })),
|
||||
description: Yup.string().nullable().trim(),
|
||||
parent_account_id: Yup.string().nullable(),
|
||||
description: Yup.string().min(3).max(512).nullable().trim(),
|
||||
parent_account_id: Yup.number().nullable(),
|
||||
});
|
||||
const initialValues = useMemo(
|
||||
() => ({
|
||||
account_type_id: null,
|
||||
name: '',
|
||||
description: '',
|
||||
code: '',
|
||||
type: '',
|
||||
description: '',
|
||||
parent_account_id: null,
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
const transformApiErrors = (errors) => {
|
||||
const fields = {};
|
||||
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;
|
||||
};
|
||||
@@ -98,20 +98,33 @@ function AccountFormDialog({
|
||||
enableReinitialize: true,
|
||||
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 }) => {
|
||||
const exclude = ['subaccount'];
|
||||
const form = pick(values, Object.keys(initialValues));
|
||||
|
||||
const toastAccountName = values.code
|
||||
? `${values.code} - ${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') {
|
||||
requestEditAccount(payload.id, values)
|
||||
requestEditAccount(payload.id, form)
|
||||
.then((response) => {
|
||||
closeDialog(dialogName);
|
||||
queryCache.invalidateQueries('accounts-table');
|
||||
afterSubmit(response);
|
||||
|
||||
AppToaster.show({
|
||||
message: formatMessage(
|
||||
@@ -124,19 +137,11 @@ function AccountFormDialog({
|
||||
intent: Intent.SUCCESS,
|
||||
});
|
||||
})
|
||||
.catch((errors) => {
|
||||
const errorsTransformed = transformApiErrors(errors);
|
||||
setErrors({ ...errorsTransformed });
|
||||
setSubmitting(false);
|
||||
});
|
||||
.catch(afterErrors);
|
||||
} else {
|
||||
requestSubmitAccount({
|
||||
payload: payload.parent_account_id,
|
||||
form: { ...omit(values, exclude) },
|
||||
})
|
||||
requestSubmitAccount({ form })
|
||||
.then((response) => {
|
||||
closeDialog(dialogName);
|
||||
queryCache.invalidateQueries('accounts-table', { force: true });
|
||||
afterSubmit(response);
|
||||
|
||||
AppToaster.show({
|
||||
message: formatMessage(
|
||||
@@ -150,70 +155,28 @@ function AccountFormDialog({
|
||||
position: Position.BOTTOM,
|
||||
});
|
||||
})
|
||||
.catch((errors) => {
|
||||
const errorsTransformed = transformApiErrors(errors);
|
||||
setErrors({ ...errorsTransformed });
|
||||
setSubmitting(false);
|
||||
});
|
||||
.catch(afterErrors);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (values.parent_account_id) {
|
||||
setFieldValue('subaccount', true);
|
||||
}
|
||||
}, [values.parent_account_id]);
|
||||
|
||||
// Filtered accounts based on the given account type.
|
||||
const filteredAccounts = useMemo(
|
||||
() =>
|
||||
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],
|
||||
);
|
||||
|
||||
// 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.
|
||||
const handleClose = useCallback(() => {
|
||||
closeDialog(dialogName);
|
||||
@@ -256,12 +219,12 @@ function AccountFormDialog({
|
||||
fetchAccount.refetch();
|
||||
}
|
||||
if (payload.action === 'new_child') {
|
||||
setFieldValue('subaccount', true);
|
||||
setFieldValue('parent_account_id', payload.parentAccountId);
|
||||
setFieldValue('account_type_id', payload.accountTypeId);
|
||||
}
|
||||
}, [fetchAccount, fetchAccountsList, fetchAccountsTypes]);
|
||||
}, [payload, fetchAccount, fetchAccountsList, fetchAccountsTypes]);
|
||||
|
||||
// Handle account type change.
|
||||
const onChangeAccountType = useCallback(
|
||||
(accountType) => {
|
||||
setFieldValue('account_type_id', accountType.id);
|
||||
@@ -277,21 +240,11 @@ function AccountFormDialog({
|
||||
[setFieldValue],
|
||||
);
|
||||
|
||||
// Handle dialog on closed.
|
||||
const onDialogClosed = useCallback(() => {
|
||||
resetForm();
|
||||
}, [resetForm]);
|
||||
|
||||
const subAccountLabel = useMemo(() => {
|
||||
return (
|
||||
<span>
|
||||
<T id={'sub_account'} />
|
||||
<Hint />
|
||||
</span>
|
||||
);
|
||||
}, []);
|
||||
|
||||
const requiredSpan = useMemo(() => <span class="required">*</span>, []);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
name={dialogName}
|
||||
@@ -332,18 +285,13 @@ function AccountFormDialog({
|
||||
errors.account_type_id && touched.account_type_id && Intent.DANGER
|
||||
}
|
||||
>
|
||||
<ListSelect
|
||||
items={accountsTypes}
|
||||
noResults={<MenuItem disabled={true} text="No results." />}
|
||||
itemRenderer={accountTypeItem}
|
||||
itemPredicate={filterAccountTypeItems}
|
||||
popoverProps={{ minimal: true }}
|
||||
onItemSelect={onChangeAccountType}
|
||||
selectedItem={values.account_type_id}
|
||||
selectedItemProp={'id'}
|
||||
defaultText={<T id={'select_account_type'} />}
|
||||
labelProp={'name'}
|
||||
<AccountsTypesSelect
|
||||
accountsTypes={accountsTypes}
|
||||
selectedTypeId={values.account_type_id}
|
||||
defaultSelectText={<T id={'select_account_type'} />}
|
||||
onTypeSelected={onChangeAccountType}
|
||||
buttonProps={{ disabled: payload.action === 'edit' }}
|
||||
popoverProps={{ minimal: true }}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
@@ -384,7 +332,12 @@ function AccountFormDialog({
|
||||
>
|
||||
<Checkbox
|
||||
inline={true}
|
||||
label={subAccountLabel}
|
||||
label={
|
||||
<>
|
||||
<T id={'sub_account'} />
|
||||
<Hint />
|
||||
</>
|
||||
}
|
||||
{...getFieldProps('subaccount')}
|
||||
checked={values.subaccount}
|
||||
/>
|
||||
@@ -400,17 +353,11 @@ function AccountFormDialog({
|
||||
)}
|
||||
inline={true}
|
||||
>
|
||||
<ListSelect
|
||||
items={filteredAccounts}
|
||||
noResults={<MenuItem disabled={true} text="No results." />}
|
||||
itemRenderer={accountItem}
|
||||
itemPredicate={filterAccountsPredicater}
|
||||
popoverProps={{ minimal: true }}
|
||||
onItemSelect={onChangeSubaccount}
|
||||
selectedItem={values.parent_account_id}
|
||||
selectedItemProp={'id'}
|
||||
defaultText={<T id={'select_parent_account'} />}
|
||||
labelProp={'name'}
|
||||
<AccountsSelectList
|
||||
accounts={filteredAccounts}
|
||||
onAccountSelected={onChangeSubaccount}
|
||||
defaultSelectText={<T id={'select_parent_account'} />}
|
||||
selectedAccountId={values.parent_account_id}
|
||||
/>
|
||||
</FormGroup>
|
||||
</If>
|
||||
@@ -424,7 +371,7 @@ function AccountFormDialog({
|
||||
>
|
||||
<TextArea
|
||||
growVertically={true}
|
||||
large={true}
|
||||
height={280}
|
||||
{...getFieldProps('description')}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
@@ -154,7 +154,7 @@ function ExpensesDataTable({
|
||||
{
|
||||
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,
|
||||
className: 'payment_date',
|
||||
},
|
||||
|
||||
@@ -12,8 +12,8 @@ export default function ExpenseFloatingFooter({
|
||||
return (
|
||||
<div className={'form__floating-footer'}>
|
||||
<Button
|
||||
intent={Intent.PRIMARY}
|
||||
disabled={isSubmitting}
|
||||
intent={Intent.PRIMARY}
|
||||
type="submit"
|
||||
onClick={() => {
|
||||
onSubmitClick({ publish: true, redirect: true });
|
||||
|
||||
@@ -28,6 +28,8 @@ import Dragzone from 'components/Dragzone';
|
||||
import useMedia from 'hooks/useMedia';
|
||||
import { compose, repeatValue } from 'utils';
|
||||
|
||||
const MIN_LINES_NUMBER = 4;
|
||||
|
||||
function ExpenseForm({
|
||||
// #withMedia
|
||||
requestSubmitMedia,
|
||||
@@ -81,22 +83,26 @@ function ExpenseForm({
|
||||
|
||||
const validationSchema = Yup.object().shape({
|
||||
beneficiary: Yup.string().label(formatMessage({ id: 'beneficiary' })),
|
||||
payment_account_id: Yup.string()
|
||||
payment_account_id: Yup.number()
|
||||
.required()
|
||||
.label(formatMessage({ id: 'payment_account_' })),
|
||||
payment_date: Yup.date()
|
||||
.required()
|
||||
.label(formatMessage({ id: 'payment_date_' })),
|
||||
reference_no: Yup.string(),
|
||||
currency_code: Yup.string().label(formatMessage({ id: 'currency_code' })),
|
||||
reference_no: Yup.string().min(1).max(255),
|
||||
currency_code: Yup.string()
|
||||
.nullable()
|
||||
.label(formatMessage({ id: 'currency_code' })),
|
||||
description: Yup.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.max(1024)
|
||||
.label(formatMessage({ id: 'description' })),
|
||||
publish: Yup.boolean().label(formatMessage({ id: 'publish' })),
|
||||
categories: Yup.array().of(
|
||||
Yup.object().shape({
|
||||
index: Yup.number().nullable(),
|
||||
amount: Yup.number().nullable(),
|
||||
index: Yup.number().min(1).max(1000).nullable(),
|
||||
amount: Yup.number().decimalScale(13).nullable(),
|
||||
expense_account_id: Yup.number()
|
||||
.nullable()
|
||||
.when(['amount'], {
|
||||
@@ -134,9 +140,7 @@ function ExpenseForm({
|
||||
description: '',
|
||||
reference_no: '',
|
||||
currency_code: '',
|
||||
categories: [
|
||||
...repeatValue(defaultCategory, 4),
|
||||
],
|
||||
categories: [...repeatValue(defaultCategory, MIN_LINES_NUMBER)],
|
||||
}),
|
||||
[defaultCategory],
|
||||
);
|
||||
@@ -153,9 +157,15 @@ function ExpenseForm({
|
||||
...(expense
|
||||
? {
|
||||
...pick(expense, Object.keys(defaultInitialValues)),
|
||||
categories: expense.categories.map((category) => ({
|
||||
...pick(category, Object.keys(defaultCategory)),
|
||||
})),
|
||||
categories: [
|
||||
...expense.categories.map((category) => ({
|
||||
...pick(category, Object.keys(defaultCategory)),
|
||||
})),
|
||||
...repeatValue(
|
||||
defaultCategory,
|
||||
Math.max(MIN_LINES_NUMBER - expense.categories.length, 0),
|
||||
),
|
||||
],
|
||||
}
|
||||
: {
|
||||
...defaultInitialValues,
|
||||
@@ -184,6 +194,20 @@ function ExpenseForm({
|
||||
...initialValues,
|
||||
},
|
||||
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(
|
||||
(category) =>
|
||||
category.amount && category.index && category.expense_account_id,
|
||||
@@ -193,7 +217,6 @@ function ExpenseForm({
|
||||
publish: payload.publish,
|
||||
categories,
|
||||
};
|
||||
|
||||
const saveExpense = (mdeiaIds) =>
|
||||
new Promise((resolve, reject) => {
|
||||
const requestForm = { ...form, media_ids: mdeiaIds };
|
||||
@@ -237,11 +260,9 @@ function ExpenseForm({
|
||||
intent: Intent.SUCCESS,
|
||||
});
|
||||
setSubmitting(false);
|
||||
formik.resetForm();
|
||||
resetForm();
|
||||
saveInvokeSubmit({ action: 'new', ...payload });
|
||||
clearSavedMediaIds();
|
||||
|
||||
// resolve(response);
|
||||
})
|
||||
.catch((errors) => {
|
||||
setSubmitting(false);
|
||||
@@ -298,11 +319,9 @@ function ExpenseForm({
|
||||
const handleClearAllLines = () => {
|
||||
formik.setFieldValue(
|
||||
'categories',
|
||||
orderingCategoriesIndex([
|
||||
...repeatValue(defaultCategory, 4),
|
||||
]),
|
||||
);
|
||||
}
|
||||
orderingCategoriesIndex([...repeatValue(defaultCategory, MIN_LINES_NUMBER)]),
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={'expense-form'}>
|
||||
@@ -324,7 +343,6 @@ function ExpenseForm({
|
||||
>
|
||||
<TextArea
|
||||
growVertically={true}
|
||||
large={true}
|
||||
{...formik.getFieldProps('description')}
|
||||
/>
|
||||
</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.
|
||||
const onChangeAccount = useCallback(
|
||||
(account) => {
|
||||
@@ -112,6 +82,12 @@ function ExpenseFormHeader({
|
||||
[setFieldValue, selectedItems],
|
||||
);
|
||||
|
||||
// Filter payment accounts.
|
||||
const paymentAccounts = useMemo(
|
||||
() => accountsList.filter(a => a?.type?.key === 'current_asset'),
|
||||
[accountsList],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={'dashboard__insider--expense-form__header'}>
|
||||
<Row>
|
||||
@@ -165,7 +141,7 @@ function ExpenseFormHeader({
|
||||
}
|
||||
>
|
||||
<AccountsSelectList
|
||||
accounts={accountsList}
|
||||
accounts={paymentAccounts}
|
||||
onAccountSelected={onChangeAccount}
|
||||
defaultSelectText={<T id={'select_payment_account'} />}
|
||||
selectedAccountId={values.payment_account_id}
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
} from 'components/DataTableCells';
|
||||
import withAccounts from 'containers/Accounts/withAccounts';
|
||||
|
||||
|
||||
const ExpenseCategoryHeaderCell = () => {
|
||||
return (
|
||||
<>
|
||||
@@ -22,10 +21,10 @@ const ExpenseCategoryHeaderCell = () => {
|
||||
<Hint />
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Actions cell renderer.
|
||||
const ActionsCellRenderer = ({
|
||||
// Actions cell renderer.
|
||||
const ActionsCellRenderer = ({
|
||||
row: { index },
|
||||
column: { id },
|
||||
cell: { value: initialValue },
|
||||
@@ -101,16 +100,11 @@ function ExpenseTable({
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
useEffect(() => {
|
||||
setRows([
|
||||
...categories.map((e) => ({ ...e, rowType: 'editor' })),
|
||||
]);
|
||||
setRows([...categories.map((e) => ({ ...e, rowType: 'editor' }))]);
|
||||
}, [categories]);
|
||||
|
||||
// Final table rows editor rows and total and final blank row.
|
||||
const tableRows = useMemo(
|
||||
() => [...rows, { rowType: 'total' }],
|
||||
[rows],
|
||||
);
|
||||
const tableRows = useMemo(() => [...rows, { rowType: 'total' }], [rows]);
|
||||
|
||||
// Memorized data table columns.
|
||||
const columns = useMemo(
|
||||
@@ -133,6 +127,7 @@ function ExpenseTable({
|
||||
disableSortBy: true,
|
||||
disableResizing: true,
|
||||
width: 250,
|
||||
accountsDataProp: 'expenseAccounts',
|
||||
},
|
||||
{
|
||||
Header: formatMessage({ id: 'amount_currency' }, { currency: 'USD' }),
|
||||
@@ -187,6 +182,11 @@ function ExpenseTable({
|
||||
// Handles click remove datatable row.
|
||||
const handleRemoveRow = useCallback(
|
||||
(rowIndex) => {
|
||||
// Can't continue if there is just one row line or less.
|
||||
if (rows.length <= 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const removeIndex = parseInt(rowIndex, 10);
|
||||
const newRows = rows.filter((row, index) => index !== removeIndex);
|
||||
|
||||
@@ -220,6 +220,12 @@ function ExpenseTable({
|
||||
[rows],
|
||||
);
|
||||
|
||||
// Filter expense accounts.
|
||||
const expenseAccounts = useMemo(
|
||||
() => accountsList.filter((a) => a?.type?.root_type === 'expense'),
|
||||
[accountsList],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={'dashboard__insider--expense-form__table'}>
|
||||
<DataTable
|
||||
@@ -229,6 +235,7 @@ function ExpenseTable({
|
||||
sticky={true}
|
||||
payload={{
|
||||
accounts: accountsList,
|
||||
expenseAccounts,
|
||||
errors: errors.categories || [],
|
||||
updateData: handleUpdateData,
|
||||
removeRow: handleRemoveRow,
|
||||
|
||||
@@ -109,7 +109,7 @@ function ExpensesList({
|
||||
.then(() => {
|
||||
AppToaster.show({
|
||||
message: formatMessage(
|
||||
{ id: 'the_expenses_has_been_successfully_deleted' },
|
||||
{ id: 'the_expenses_have_been_successfully_deleted' },
|
||||
{ count: selectedRowsCount },
|
||||
),
|
||||
intent: Intent.SUCCESS,
|
||||
@@ -160,6 +160,7 @@ function ExpensesList({
|
||||
requestPublishExpense(expense.id).then(() => {
|
||||
AppToaster.show({
|
||||
message: formatMessage({ id: 'the_expense_id_has_been_published' }),
|
||||
intent: Intent.SUCCESS,
|
||||
});
|
||||
});
|
||||
fetchExpenses.refetch();
|
||||
|
||||
@@ -5,7 +5,7 @@ export default () => {
|
||||
const getExpenseById = getExpenseByIdFactory();
|
||||
|
||||
const mapStateToProps = (state, props) => ({
|
||||
expenseDetail: getExpenseById(state, props),
|
||||
expense: getExpenseById(state, props),
|
||||
});
|
||||
return connect(mapStateToProps);
|
||||
};
|
||||
@@ -436,11 +436,10 @@ export default {
|
||||
'The expense #{number} has been successfully edited.',
|
||||
the_expense_has_been_successfully_deleted:
|
||||
'The expense has been successfully deleted',
|
||||
the_expenses_has_been_successfully_deleted:
|
||||
'The expenses has been successfully deleted',
|
||||
the_expenses_have_been_successfully_deleted:
|
||||
'The expenses #{number} have been successfully deleted',
|
||||
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?",
|
||||
|
||||
the_expense_id_has_been_published: 'The expense id has been published',
|
||||
select_beneficiary_account: 'Select Beneficiary Account',
|
||||
total_amount_equals_zero: 'Total amount equals zero',
|
||||
@@ -529,4 +528,13 @@ export default {
|
||||
logic_expression: 'logic expression',
|
||||
assign_to_customer: 'Assign to Customer',
|
||||
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) => {
|
||||
dispatch({
|
||||
type: t.ACCOUNT_SET,
|
||||
account: response.data.account,
|
||||
payload: {
|
||||
account: response.data.account,
|
||||
}
|
||||
});
|
||||
resolve(response);
|
||||
})
|
||||
|
||||
@@ -51,7 +51,11 @@ const accountsReducer = createReducer(initialState, {
|
||||
},
|
||||
|
||||
[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) => {
|
||||
|
||||
@@ -8,7 +8,7 @@ const initialState = {
|
||||
views: {},
|
||||
loading: false,
|
||||
tableQuery: {
|
||||
page_size: 4,
|
||||
page_size: 12,
|
||||
page: 1,
|
||||
},
|
||||
currentViewId: -1,
|
||||
|
||||
@@ -7,8 +7,7 @@ $form-check-input-indeterminate-bg-image: url("data:image/svg+xml,<svg xmlns='ht
|
||||
&__floating-footer{
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 220px;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
background: #fff;
|
||||
padding: 14px 18px;
|
||||
border-top: 1px solid #ececec;
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
.account_name{
|
||||
> div{
|
||||
width: 100%;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.bp3-popover-wrapper--inactive-semafro{
|
||||
@@ -78,14 +79,14 @@
|
||||
&.form-group--description{
|
||||
|
||||
.bp3-form-content{
|
||||
width: 280px;
|
||||
width: 280px;
|
||||
}
|
||||
|
||||
textarea{
|
||||
min-width: 100%;
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
max-height: 120px;
|
||||
}
|
||||
textarea{
|
||||
min-width: 100%;
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
min-height: 60px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -105,6 +106,11 @@
|
||||
top: -2px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.form-group--description{
|
||||
textarea{
|
||||
padding: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
&:before{
|
||||
content: "";
|
||||
height: 2px;
|
||||
height: 1px;
|
||||
background: #01194e;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
@@ -203,6 +203,7 @@
|
||||
align-items: center;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
&__title{
|
||||
align-items: center;;
|
||||
display: flex;
|
||||
@@ -243,6 +244,9 @@
|
||||
&__subtitle{
|
||||
|
||||
}
|
||||
&__insider{
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
&-content{
|
||||
display: flex;
|
||||
|
||||
@@ -102,19 +102,16 @@
|
||||
}
|
||||
}
|
||||
.tr {
|
||||
.bp3-input,
|
||||
.bp3-form-group .bp3-input,
|
||||
.form-group--select-list .bp3-button {
|
||||
border-color: #e5e5e5;
|
||||
border-radius: 3px;
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
.form-group--select-list {
|
||||
&.bp3-intent-danger {
|
||||
.bp3-button:not(.bp3-minimal) {
|
||||
border-color: #efa8a8;
|
||||
}
|
||||
}
|
||||
|
||||
.bp3-form-group:not(.bp3-intent-danger) .bp3-input,
|
||||
.form-group--select-list:not(.bp3-intent-danger) .bp3-button {
|
||||
border-color: #E5E5E5;
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
@@ -155,6 +152,12 @@
|
||||
|
||||
.td {
|
||||
border-bottom: 1px dotted #999;
|
||||
|
||||
&.description{
|
||||
.bp3-form-group{
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.actions.td {
|
||||
@@ -217,7 +220,7 @@
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.description{
|
||||
.td.description{
|
||||
.bp3-icon{
|
||||
color: #666;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
.make-journal-entries{
|
||||
padding-bottom: 80px;
|
||||
padding-bottom: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
}
|
||||
}
|
||||
.tr{
|
||||
.bp3-input,
|
||||
.bp3-form-group:not(.bp3-intent-danger) .bp3-input,
|
||||
.form-group--select-list .bp3-button{
|
||||
border-color: #E5E5E5;
|
||||
border-radius: 3px;
|
||||
@@ -84,13 +84,12 @@
|
||||
.form-group--select-list{
|
||||
&.bp3-intent-danger{
|
||||
.bp3-button:not(.bp3-minimal){
|
||||
border-color: #efa8a8;
|
||||
border-color: #db3737;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:last-of-type{
|
||||
|
||||
.td{
|
||||
border-bottom: transparent;
|
||||
|
||||
@@ -129,6 +128,14 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.td{
|
||||
&.note{
|
||||
.bp3-form-group{
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.th{
|
||||
|
||||
@@ -5,8 +5,8 @@ exports.up = function (knex) {
|
||||
table.string('name');
|
||||
table.string('type');
|
||||
table.string('sku');
|
||||
table.decimal('cost_price').unsigned();
|
||||
table.decimal('sell_price').unsigned();
|
||||
table.decimal('cost_price', 13, 3).unsigned();
|
||||
table.decimal('sell_price', 13, 3).unsigned();
|
||||
table.string('currency_code', 3);
|
||||
table.string('picture_uri');
|
||||
table.integer('cost_account_id').unsigned();
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
exports.up = function(knex) {
|
||||
return knex.schema.createTable('accounts_transactions', (table) => {
|
||||
table.increments();
|
||||
table.decimal('credit');
|
||||
table.decimal('debit');
|
||||
table.decimal('credit', 13, 3);
|
||||
table.decimal('debit', 13, 3);
|
||||
table.string('transaction_type');
|
||||
table.string('reference_type');
|
||||
table.integer('reference_id');
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
exports.up = function(knex) {
|
||||
return knex.schema.createTable('expenses_transactions', (table) => {
|
||||
table.increments();
|
||||
table.decimal('total_amount');
|
||||
table.string('currency_code');
|
||||
table.decimal('total_amount', 13, 3);
|
||||
table.string('currency_code', 3);
|
||||
table.text('description');
|
||||
table.integer('payment_account_id').unsigned();
|
||||
table.integer('payee_id').unsigned();
|
||||
|
||||
@@ -4,7 +4,7 @@ exports.up = function(knex) {
|
||||
table.increments();
|
||||
table.date('date');
|
||||
table.string('currency_code');
|
||||
table.decimal('exchange_rate');
|
||||
table.decimal('exchange_rate', 8, 5);
|
||||
table.string('note');
|
||||
});
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@ exports.up = function(knex) {
|
||||
table.string('journal_number');
|
||||
table.string('reference');
|
||||
table.string('journal_type');
|
||||
table.decimal('amount');
|
||||
table.decimal('amount', 13, 3);
|
||||
table.date('date');
|
||||
table.boolean('status').defaultTo(false);
|
||||
table.string('description');
|
||||
|
||||
@@ -5,7 +5,7 @@ exports.up = function(knex) {
|
||||
table.integer('expense_account_id').unsigned();
|
||||
table.integer('index').unsigned();
|
||||
table.text('description');
|
||||
table.decimal('amount');
|
||||
table.decimal('amount', 13, 3);
|
||||
table.integer('expense_id').unsigned();
|
||||
table.timestamps();
|
||||
}).raw('ALTER TABLE `EXPENSE_TRANSACTION_CATEGORIES` AUTO_INCREMENT = 1000');;
|
||||
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
DynamicFilterFilterRoles,
|
||||
} from '@/lib/DynamicFilter';
|
||||
|
||||
|
||||
export default {
|
||||
/**
|
||||
* Router constructor.
|
||||
@@ -24,45 +23,63 @@ export default {
|
||||
router() {
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/manual-journals/:id',
|
||||
router.get(
|
||||
'/manual-journals/:id',
|
||||
this.getManualJournal.validation,
|
||||
asyncMiddleware(this.getManualJournal.handler));
|
||||
asyncMiddleware(this.getManualJournal.handler)
|
||||
);
|
||||
|
||||
router.get('/manual-journals',
|
||||
router.get(
|
||||
'/manual-journals',
|
||||
this.manualJournals.validation,
|
||||
asyncMiddleware(this.manualJournals.handler));
|
||||
asyncMiddleware(this.manualJournals.handler)
|
||||
);
|
||||
|
||||
router.post('/make-journal-entries',
|
||||
router.post(
|
||||
'/make-journal-entries',
|
||||
this.validateMediaIds,
|
||||
this.validateContactEntries,
|
||||
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,
|
||||
asyncMiddleware(this.publishManualJournal.handler));
|
||||
asyncMiddleware(this.publishManualJournal.handler)
|
||||
);
|
||||
|
||||
router.post('/manual-journals/:id',
|
||||
router.post(
|
||||
'/manual-journals/:id',
|
||||
this.validateMediaIds,
|
||||
this.validateContactEntries,
|
||||
this.editManualJournal.validation,
|
||||
asyncMiddleware(this.editManualJournal.handler));
|
||||
asyncMiddleware(this.editManualJournal.handler)
|
||||
);
|
||||
|
||||
router.delete('/manual-journals/:id',
|
||||
router.delete(
|
||||
'/manual-journals/:id',
|
||||
this.deleteManualJournal.validation,
|
||||
asyncMiddleware(this.deleteManualJournal.handler));
|
||||
asyncMiddleware(this.deleteManualJournal.handler)
|
||||
);
|
||||
|
||||
router.delete('/manual-journals',
|
||||
router.delete(
|
||||
'/manual-journals',
|
||||
this.deleteBulkManualJournals.validation,
|
||||
asyncMiddleware(this.deleteBulkManualJournals.handler));
|
||||
asyncMiddleware(this.deleteBulkManualJournals.handler)
|
||||
);
|
||||
|
||||
router.post('/recurring-journal-entries',
|
||||
router.post(
|
||||
'/recurring-journal-entries',
|
||||
this.recurringJournalEntries.validation,
|
||||
asyncMiddleware(this.recurringJournalEntries.handler));
|
||||
asyncMiddleware(this.recurringJournalEntries.handler)
|
||||
);
|
||||
|
||||
router.post('quick-journal-entries',
|
||||
router.post(
|
||||
'quick-journal-entries',
|
||||
this.quickJournalEntries.validation,
|
||||
asyncMiddleware(this.quickJournalEntries.handler));
|
||||
asyncMiddleware(this.quickJournalEntries.handler)
|
||||
);
|
||||
|
||||
return router;
|
||||
},
|
||||
@@ -76,7 +93,7 @@ export default {
|
||||
query('page_size').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('stringified_filter_roles').optional().isJSON(),
|
||||
@@ -86,7 +103,8 @@ export default {
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
code: 'validation_error',
|
||||
...validationErrors,
|
||||
});
|
||||
}
|
||||
const filter = {
|
||||
@@ -126,17 +144,22 @@ export default {
|
||||
builder.remember();
|
||||
});
|
||||
|
||||
const resourceFieldsKeys = manualJournalsResource.fields.map((c) => c.key);
|
||||
const resourceFieldsKeys = manualJournalsResource.fields.map(
|
||||
(c) => c.key
|
||||
);
|
||||
const dynamicFilter = new DynamicFilter(ManualJournal.tableName);
|
||||
|
||||
// Dynamic filter with view roles.
|
||||
if (view && view.roles.length > 0) {
|
||||
const viewFilter = new DynamicFilterViews(
|
||||
mapViewRolesToConditionals(view.roles),
|
||||
view.rolesLogicExpression,
|
||||
view.rolesLogicExpression
|
||||
);
|
||||
if (!viewFilter.validateFilterRoles()) {
|
||||
errorReasons.push({ type: 'VIEW.LOGIC.EXPRESSION.INVALID', code: 400 });
|
||||
errorReasons.push({
|
||||
type: 'VIEW.LOGIC.EXPRESSION.INVALID',
|
||||
code: 400,
|
||||
});
|
||||
}
|
||||
dynamicFilter.setFilter(viewFilter);
|
||||
}
|
||||
@@ -145,12 +168,15 @@ export default {
|
||||
// Validate the accounts resource fields.
|
||||
const filterRoles = new DynamicFilterFilterRoles(
|
||||
mapFilterRolesToDynamicFilter(filter.filter_roles),
|
||||
manualJournalsResource.fields,
|
||||
manualJournalsResource.fields
|
||||
);
|
||||
dynamicFilter.setFilter(filterRoles);
|
||||
|
||||
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.
|
||||
@@ -160,7 +186,7 @@ export default {
|
||||
}
|
||||
const sortByFilter = new DynamicFilterSortBy(
|
||||
filter.column_sort_by,
|
||||
filter.sort_order,
|
||||
filter.sort_order
|
||||
);
|
||||
dynamicFilter.setFilter(sortByFilter);
|
||||
}
|
||||
@@ -168,18 +194,22 @@ export default {
|
||||
return res.status(400).send({ errors: errorReasons });
|
||||
}
|
||||
// Manual journals.
|
||||
const manualJournals = await ManualJournal.query().onBuild((builder) => {
|
||||
dynamicFilter.buildQuery()(builder);
|
||||
}).pagination(filter.page - 1, filter.page_size);
|
||||
const manualJournals = await ManualJournal.query()
|
||||
.onBuild((builder) => {
|
||||
dynamicFilter.buildQuery()(builder);
|
||||
})
|
||||
.pagination(filter.page - 1, filter.page_size);
|
||||
|
||||
return res.status(200).send({
|
||||
manualJournals: {
|
||||
...manualJournals,
|
||||
...(view) ? {
|
||||
viewMeta: {
|
||||
customViewId: view.id,
|
||||
}
|
||||
} : {},
|
||||
...(view
|
||||
? {
|
||||
viewMeta: {
|
||||
customViewId: view.id,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
});
|
||||
},
|
||||
@@ -199,28 +229,37 @@ export default {
|
||||
// Validate if media ids was not already exists on the storage.
|
||||
if (form.media_ids.length > 0) {
|
||||
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) {
|
||||
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.push(...errorReasons) : errorReasons;
|
||||
next();
|
||||
req.errorReasons =
|
||||
Array.isArray(req.errorReasons) && req.errorReasons.length
|
||||
? req.errorReasons.push(...errorReasons)
|
||||
: errorReasons;
|
||||
next();
|
||||
},
|
||||
|
||||
/**
|
||||
* Validate form entries with contact customers and vendors.
|
||||
*
|
||||
* Validate form entries with contact customers and vendors.
|
||||
*
|
||||
* - Validate the entries that with receivable has no customer contact.
|
||||
* - Validate the entries that with payable has no vendor contact.
|
||||
* - Validate the entries with customers contacts that not found on the storage.
|
||||
* - Validate the entries with vendors contacts that not found on the storage.
|
||||
*
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Function} next
|
||||
*/
|
||||
async validateContactEntries(req, res, next) {
|
||||
const form = { entries: [], ...req.body };
|
||||
@@ -228,86 +267,111 @@ export default {
|
||||
const errorReasons = [];
|
||||
|
||||
// Validate the entries contact type and ids.
|
||||
const formEntriesCustomersIds = form.entries.filter(e => e.contact_type === 'customer');
|
||||
const formEntriesVendorsIds = form.entries.filter(e => e.contact_type === 'vendor');
|
||||
const formEntriesCustomersIds = form.entries.filter(
|
||||
(e) => e.contact_type === 'customer'
|
||||
);
|
||||
const formEntriesVendorsIds = form.entries.filter(
|
||||
(e) => e.contact_type === 'vendor'
|
||||
);
|
||||
|
||||
const accountsTypes = await AccountType.query();
|
||||
|
||||
const payableAccountsType = accountsTypes.find(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 payableAccountOper = Account.query().where('account_type_id', payableAccountsType.id).first();
|
||||
const payableAccountsType = accountsTypes.find(
|
||||
(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 payableAccountOper = Account.query()
|
||||
.where('account_type_id', payableAccountsType.id)
|
||||
.first();
|
||||
|
||||
const [receivableAccount, payableAccount] = await Promise.all([
|
||||
receivableAccountOper, payableAccountOper,
|
||||
receivableAccountOper,
|
||||
payableAccountOper,
|
||||
]);
|
||||
|
||||
const entriesHasNoReceivableAccount = form.entries
|
||||
.filter(e =>
|
||||
(e.account_id === receivableAccount.id) &&
|
||||
const entriesHasNoReceivableAccount = form.entries.filter(
|
||||
(e) =>
|
||||
e.account_id === receivableAccount.id &&
|
||||
(!e.contact_id || e.contact_type !== 'customer')
|
||||
);
|
||||
);
|
||||
|
||||
if (entriesHasNoReceivableAccount.length > 0) {
|
||||
errorReasons.push({
|
||||
type: 'RECEIVABLE.ENTRIES.HAS.NO.CUSTOMERS',
|
||||
code: 900,
|
||||
indexes: entriesHasNoReceivableAccount.map(e => e.index),
|
||||
indexes: entriesHasNoReceivableAccount.map((e) => e.index),
|
||||
});
|
||||
}
|
||||
|
||||
const entriesHasNoVendorContact = form.entries
|
||||
.filter(e =>
|
||||
(e.account_id === payableAccount.id) &&
|
||||
const entriesHasNoVendorContact = form.entries.filter(
|
||||
(e) =>
|
||||
e.account_id === payableAccount.id &&
|
||||
(!e.contact_id || e.contact_type !== 'contact')
|
||||
);
|
||||
);
|
||||
|
||||
if (entriesHasNoVendorContact.length > 0) {
|
||||
errorReasons.push({
|
||||
type: 'PAYABLE.ENTRIES.HAS.NO.VENDORS',
|
||||
code: 1000,
|
||||
indexes: entriesHasNoVendorContact.map(e => e.index),
|
||||
indexes: entriesHasNoVendorContact.map((e) => e.index),
|
||||
});
|
||||
}
|
||||
|
||||
// Validate customers contacts.
|
||||
if (formEntriesCustomersIds.length > 0) {
|
||||
const customersContactsIds = formEntriesCustomersIds.map(c => c.contact_id);
|
||||
const storedContacts = await Customer.query().whereIn('id', customersContactsIds);
|
||||
const customersContactsIds = formEntriesCustomersIds.map(
|
||||
(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(
|
||||
formEntriesCustomersIds.map(c => c.contact_id),
|
||||
storedContactsIds,
|
||||
formEntriesCustomersIds.map((c) => c.contact_id),
|
||||
storedContactsIds
|
||||
);
|
||||
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(
|
||||
c => c.account_id !== receivableAccount.id
|
||||
(c) => c.account_id !== receivableAccount.id
|
||||
);
|
||||
if (notReceivableAccounts.length > 0) {
|
||||
errorReasons.push({
|
||||
type: 'CUSTOMERS.NOT.WITH.RECEIVABLE.ACCOUNT',
|
||||
code: 700,
|
||||
indexes: notReceivableAccounts.map(a => a.index),
|
||||
indexes: notReceivableAccounts.map((a) => a.index),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Validate vendors contacts.
|
||||
if (formEntriesVendorsIds.length > 0) {
|
||||
const vendorsContactsIds = formEntriesVendorsIds.map(c => c.contact_id);
|
||||
const storedContacts = await Vendor.query().where('id', vendorsContactsIds);
|
||||
const vendorsContactsIds = formEntriesVendorsIds.map((c) => c.contact_id);
|
||||
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(
|
||||
formEntriesVendorsIds.map(v => v.contact_id),
|
||||
storedContactsIds,
|
||||
formEntriesVendorsIds.map((v) => v.contact_id),
|
||||
storedContactsIds
|
||||
);
|
||||
if (notFoundContactsIds.length > 0) {
|
||||
errorReasons.push({
|
||||
@@ -317,19 +381,21 @@ export default {
|
||||
});
|
||||
}
|
||||
const notPayableAccounts = formEntriesVendorsIds.filter(
|
||||
v => v.contact_id === payableAccount.id
|
||||
(v) => v.contact_id === payableAccount.id
|
||||
);
|
||||
if (notPayableAccounts.length > 0) {
|
||||
errorReasons.push({
|
||||
type: 'VENDORS.NOT.WITH.PAYABLE.ACCOUNT',
|
||||
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.push(...errorReasons) : errorReasons;
|
||||
req.errorReasons =
|
||||
Array.isArray(req.errorReasons) && req.errorReasons.length
|
||||
? req.errorReasons.push(...errorReasons)
|
||||
: errorReasons;
|
||||
|
||||
next();
|
||||
},
|
||||
@@ -347,11 +413,24 @@ export default {
|
||||
check('status').optional().isBoolean().toBoolean(),
|
||||
check('entries').isArray({ min: 2 }),
|
||||
check('entries.*.index').exists().isNumeric().toInt(),
|
||||
check('entries.*.credit').optional({ nullable: true }).isNumeric().toInt(),
|
||||
check('entries.*.debit').optional({ nullable: true }).isNumeric().toInt(),
|
||||
check('entries.*.credit')
|
||||
.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.*.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('media_ids').optional().isArray(),
|
||||
check('media_ids.*').exists().isNumeric().toInt(),
|
||||
@@ -361,7 +440,8 @@ export default {
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
code: 'validation_error',
|
||||
...validationErrors,
|
||||
});
|
||||
}
|
||||
const form = {
|
||||
@@ -371,18 +451,16 @@ export default {
|
||||
media_ids: [],
|
||||
...req.body,
|
||||
};
|
||||
const {
|
||||
ManualJournal,
|
||||
Account,
|
||||
MediaLink,
|
||||
} = req.models;
|
||||
const { ManualJournal, Account, MediaLink } = req.models;
|
||||
|
||||
let totalCredit = 0;
|
||||
let totalDebit = 0;
|
||||
|
||||
const { user } = req;
|
||||
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');
|
||||
|
||||
entries.forEach((entry) => {
|
||||
@@ -414,8 +492,10 @@ export default {
|
||||
errorReasons.push({ type: 'ACCOUNTS.IDS.NOT.FOUND', code: 200 });
|
||||
}
|
||||
|
||||
const journalNumber = await ManualJournal.query()
|
||||
.where('journal_number', form.journal_number);
|
||||
const journalNumber = await ManualJournal.query().where(
|
||||
'journal_number',
|
||||
form.journal_number
|
||||
);
|
||||
|
||||
if (journalNumber.length > 0) {
|
||||
errorReasons.push({ type: 'JOURNAL.NUMBER.ALREADY.EXISTS', code: 300 });
|
||||
@@ -478,7 +558,7 @@ export default {
|
||||
await Promise.all([
|
||||
...bulkSaveMediaLink,
|
||||
journalPoster.saveEntries(),
|
||||
(form.status) && journalPoster.saveBalance(),
|
||||
form.status && journalPoster.saveBalance(),
|
||||
]);
|
||||
return res.status(200).send({ id: manualJournal.id });
|
||||
},
|
||||
@@ -503,7 +583,8 @@ export default {
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
code: 'validation_error',
|
||||
...validationErrors,
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -517,15 +598,29 @@ export default {
|
||||
param('id').exists().isNumeric().toInt(),
|
||||
check('date').exists().isISO8601(),
|
||||
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('description').optional().trim().escape(),
|
||||
check('entries').isArray({ min: 2 }),
|
||||
check('entries.*.credit').optional({ nullable: true }).isNumeric().toInt(),
|
||||
check('entries.*.debit').optional({ nullable: true }).isNumeric().toInt(),
|
||||
// check('entries.*.index').exists().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.*.contact_id').optional().isNumeric().toInt(),
|
||||
check('entries.*.contact_type').optional().isIn(['vendor', 'customer']).isNumeric().toInt(),
|
||||
check('entries.*.contact_id')
|
||||
.optional({ nullable: true })
|
||||
.isNumeric()
|
||||
.toInt(),
|
||||
check('entries.*.contact_type')
|
||||
.optional()
|
||||
.isIn(['vendor', 'customer'])
|
||||
.isNumeric()
|
||||
.toInt(),
|
||||
check('entries.*.note').optional(),
|
||||
check('media_ids').optional().isArray(),
|
||||
check('media_ids.*').isNumeric().toInt(),
|
||||
@@ -535,24 +630,30 @@ export default {
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
code: 'validation_error',
|
||||
...validationErrors,
|
||||
});
|
||||
}
|
||||
const form = {
|
||||
date: new Date(),
|
||||
transaction_type: 'journal',
|
||||
journal_type: 'Journal',
|
||||
reference: '',
|
||||
media_ids: [],
|
||||
...req.body,
|
||||
};
|
||||
const { id } = req.params;
|
||||
const {
|
||||
ManualJournal, AccountTransaction, Account, Media, MediaLink,
|
||||
ManualJournal,
|
||||
AccountTransaction,
|
||||
Account,
|
||||
Media,
|
||||
MediaLink,
|
||||
} = req.models;
|
||||
|
||||
const manualJournal = await ManualJournal.query()
|
||||
.where('id', id)
|
||||
.withGraphFetched('media').first();
|
||||
.withGraphFetched('media')
|
||||
.first();
|
||||
|
||||
if (!manualJournal) {
|
||||
return res.status(4040).send({
|
||||
@@ -564,7 +665,9 @@ export default {
|
||||
|
||||
const { user } = req;
|
||||
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');
|
||||
|
||||
entries.forEach((entry) => {
|
||||
@@ -593,7 +696,8 @@ export default {
|
||||
errorReasons.push({ type: 'JOURNAL.NUMBER.ALREADY.EXISTS', code: 300 });
|
||||
}
|
||||
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');
|
||||
|
||||
const storedAccountsIds = accounts.map((account) => account.id);
|
||||
@@ -605,16 +709,14 @@ export default {
|
||||
return res.status(400).send({ errors: errorReasons });
|
||||
}
|
||||
|
||||
await ManualJournal.query()
|
||||
.where('id', manualJournal.id)
|
||||
.update({
|
||||
reference: form.reference,
|
||||
transaction_type: 'Journal',
|
||||
journalNumber: form.journal_number,
|
||||
amount: totalCredit,
|
||||
date: formattedDate,
|
||||
description: form.description,
|
||||
});
|
||||
await ManualJournal.query().where('id', manualJournal.id).update({
|
||||
reference: form.reference,
|
||||
journal_type: form.journal_type,
|
||||
journalNumber: form.journal_number,
|
||||
amount: totalCredit,
|
||||
date: formattedDate,
|
||||
description: form.description,
|
||||
});
|
||||
|
||||
const transactions = await AccountTransaction.query()
|
||||
.whereIn('reference_type', ['Journal'])
|
||||
@@ -674,26 +776,20 @@ export default {
|
||||
},
|
||||
|
||||
publishManualJournal: {
|
||||
validation: [
|
||||
param('id').exists().isNumeric().toInt(),
|
||||
],
|
||||
validation: [param('id').exists().isNumeric().toInt()],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
code: 'validation_error',
|
||||
...validationErrors,
|
||||
});
|
||||
}
|
||||
const {
|
||||
ManualJournal,
|
||||
AccountTransaction,
|
||||
Account,
|
||||
} = req.models;
|
||||
const { ManualJournal, AccountTransaction, Account } = req.models;
|
||||
|
||||
const { id } = req.params;
|
||||
const manualJournal = await ManualJournal.query()
|
||||
.where('id', id).first();
|
||||
const manualJournal = await ManualJournal.query().where('id', id).first();
|
||||
|
||||
if (!manualJournal) {
|
||||
return res.status(404).send({
|
||||
@@ -721,7 +817,10 @@ export default {
|
||||
journal.calculateEntriesBalanceChange();
|
||||
|
||||
const updateAccountsTransactionsOper = AccountTransaction.query()
|
||||
.whereIn('id', transactions.map((t) => t.id))
|
||||
.whereIn(
|
||||
'id',
|
||||
transactions.map((t) => t.id)
|
||||
)
|
||||
.update({ draft: 0 });
|
||||
|
||||
await Promise.all([
|
||||
@@ -734,20 +833,17 @@ export default {
|
||||
},
|
||||
|
||||
getManualJournal: {
|
||||
validation: [
|
||||
param('id').exists().isNumeric().toInt(),
|
||||
],
|
||||
validation: [param('id').exists().isNumeric().toInt()],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
code: 'validation_error',
|
||||
...validationErrors,
|
||||
});
|
||||
}
|
||||
const {
|
||||
ManualJournal, AccountTransaction,
|
||||
} = req.models;
|
||||
const { ManualJournal, AccountTransaction } = req.models;
|
||||
|
||||
const { id } = req.params;
|
||||
const manualJournal = await ManualJournal.query()
|
||||
@@ -759,7 +855,7 @@ export default {
|
||||
return res.status(404).send({
|
||||
errors: [{ type: 'MANUAL.JOURNAL.NOT.FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
}
|
||||
const transactions = await AccountTransaction.query()
|
||||
.whereIn('reference_type', ['Journal', 'ManualJournal'])
|
||||
.where('reference_id', manualJournal.id);
|
||||
@@ -767,9 +863,7 @@ export default {
|
||||
return res.status(200).send({
|
||||
manual_journal: {
|
||||
...manualJournal.toJSON(),
|
||||
entries: [
|
||||
...transactions,
|
||||
],
|
||||
entries: [...transactions],
|
||||
},
|
||||
});
|
||||
},
|
||||
@@ -780,15 +874,14 @@ export default {
|
||||
* accounts transactions.
|
||||
*/
|
||||
deleteManualJournal: {
|
||||
validation: [
|
||||
param('id').exists().isNumeric().toInt(),
|
||||
],
|
||||
validation: [param('id').exists().isNumeric().toInt()],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
code: 'validation_error',
|
||||
...validationErrors,
|
||||
});
|
||||
}
|
||||
const { id } = req.params;
|
||||
@@ -799,8 +892,7 @@ export default {
|
||||
Account,
|
||||
} = req.models;
|
||||
|
||||
const manualJournal = await ManualJournal.query()
|
||||
.where('id', id).first();
|
||||
const manualJournal = await ManualJournal.query().where('id', id).first();
|
||||
|
||||
if (!manualJournal) {
|
||||
return res.status(404).send({
|
||||
@@ -823,14 +915,9 @@ export default {
|
||||
.where('model_id', manualJournal.id)
|
||||
.delete();
|
||||
|
||||
await ManualJournal.query()
|
||||
.where('id', manualJournal.id)
|
||||
.delete();
|
||||
await ManualJournal.query().where('id', manualJournal.id).delete();
|
||||
|
||||
await Promise.all([
|
||||
journal.deleteEntries(),
|
||||
journal.saveBalance(),
|
||||
]);
|
||||
await Promise.all([journal.deleteEntries(), journal.saveBalance()]);
|
||||
return res.status(200).send({ id });
|
||||
},
|
||||
},
|
||||
@@ -846,7 +933,8 @@ export default {
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
code: 'validation_error',
|
||||
...validationErrors,
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -866,7 +954,8 @@ export default {
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
code: 'validation_error',
|
||||
...validationErrors,
|
||||
});
|
||||
}
|
||||
const errorReasons = [];
|
||||
@@ -877,8 +966,12 @@ export default {
|
||||
.where('id', form.credit_account_id)
|
||||
.orWhere('id', form.debit_account_id);
|
||||
|
||||
const creditAccount = foundAccounts.find((a) => a.id === form.credit_account_id);
|
||||
const debitAccount = foundAccounts.find((a) => a.id === form.debit_account_id);
|
||||
const creditAccount = foundAccounts.find(
|
||||
(a) => a.id === form.credit_account_id
|
||||
);
|
||||
const debitAccount = foundAccounts.find(
|
||||
(a) => a.id === form.debit_account_id
|
||||
);
|
||||
|
||||
if (!creditAccount) {
|
||||
errorReasons.push({ type: 'CREDIT_ACCOUNT.NOT.EXIST', code: 100 });
|
||||
@@ -892,9 +985,9 @@ export default {
|
||||
|
||||
// const journalPoster = new JournalPoster();
|
||||
// const journalCredit = new JournalEntry({
|
||||
// debit:
|
||||
// debit:
|
||||
// account: debitAccount.id,
|
||||
// referenceId:
|
||||
// referenceId:
|
||||
// })
|
||||
|
||||
return res.status(200).send();
|
||||
@@ -914,16 +1007,27 @@ export default {
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
code: 'validation_error',
|
||||
...validationErrors,
|
||||
});
|
||||
}
|
||||
const filter = { ...req.query };
|
||||
const { ManualJournal, AccountTransaction, Account, MediaLink } = req.models;
|
||||
const {
|
||||
ManualJournal,
|
||||
AccountTransaction,
|
||||
Account,
|
||||
MediaLink,
|
||||
} = req.models;
|
||||
|
||||
const manualJournals = await ManualJournal.query()
|
||||
.whereIn('id', filter.ids);
|
||||
const manualJournals = await ManualJournal.query().whereIn(
|
||||
'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) {
|
||||
return res.status(404).send({
|
||||
@@ -945,14 +1049,10 @@ export default {
|
||||
.whereIn('model_id', filter.ids)
|
||||
.delete();
|
||||
|
||||
await ManualJournal.query()
|
||||
.whereIn('id', filter.ids).delete();
|
||||
await ManualJournal.query().whereIn('id', filter.ids).delete();
|
||||
|
||||
await Promise.all([
|
||||
journal.deleteEntries(),
|
||||
journal.saveBalance(),
|
||||
]);
|
||||
await Promise.all([journal.deleteEntries(), journal.saveBalance()]);
|
||||
return res.status(200).send({ ids: filter.ids });
|
||||
},
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import express from 'express';
|
||||
import {
|
||||
check,
|
||||
validationResult,
|
||||
param,
|
||||
query,
|
||||
} from 'express-validator';
|
||||
import { check, validationResult, param, query } from 'express-validator';
|
||||
import { difference } from 'lodash';
|
||||
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
||||
import JournalPoster from '@/services/Accounting/JournalPoster';
|
||||
@@ -20,7 +15,6 @@ import {
|
||||
DynamicFilterFilterRoles,
|
||||
} from '@/lib/DynamicFilter';
|
||||
|
||||
|
||||
export default {
|
||||
/**
|
||||
* Router constructor method.
|
||||
@@ -28,49 +22,71 @@ export default {
|
||||
router() {
|
||||
const router = express.Router();
|
||||
|
||||
router.post('/',
|
||||
router.post(
|
||||
'/',
|
||||
this.newAccount.validation,
|
||||
asyncMiddleware(this.newAccount.handler));
|
||||
asyncMiddleware(this.newAccount.handler)
|
||||
);
|
||||
|
||||
router.post('/:id',
|
||||
router.post(
|
||||
'/:id',
|
||||
this.editAccount.validation,
|
||||
asyncMiddleware(this.editAccount.handler));
|
||||
asyncMiddleware(this.editAccount.handler)
|
||||
);
|
||||
|
||||
router.get('/:id',
|
||||
router.get(
|
||||
'/:id',
|
||||
this.getAccount.validation,
|
||||
asyncMiddleware(this.getAccount.handler));
|
||||
asyncMiddleware(this.getAccount.handler)
|
||||
);
|
||||
|
||||
router.get('/',
|
||||
router.get(
|
||||
'/',
|
||||
this.getAccountsList.validation,
|
||||
asyncMiddleware(this.getAccountsList.handler));
|
||||
asyncMiddleware(this.getAccountsList.handler)
|
||||
);
|
||||
|
||||
router.delete('/',
|
||||
router.delete(
|
||||
'/',
|
||||
this.deleteBulkAccounts.validation,
|
||||
asyncMiddleware(this.deleteBulkAccounts.handler));
|
||||
asyncMiddleware(this.deleteBulkAccounts.handler)
|
||||
);
|
||||
|
||||
router.delete('/:id',
|
||||
router.delete(
|
||||
'/:id',
|
||||
this.deleteAccount.validation,
|
||||
asyncMiddleware(this.deleteAccount.handler));
|
||||
asyncMiddleware(this.deleteAccount.handler)
|
||||
);
|
||||
|
||||
router.post('/:id/active',
|
||||
router.post(
|
||||
'/:id/active',
|
||||
this.activeAccount.validation,
|
||||
asyncMiddleware(this.activeAccount.handler));
|
||||
asyncMiddleware(this.activeAccount.handler)
|
||||
);
|
||||
|
||||
router.post('/:id/inactive',
|
||||
router.post(
|
||||
'/:id/inactive',
|
||||
this.inactiveAccount.validation,
|
||||
asyncMiddleware(this.inactiveAccount.handler));
|
||||
asyncMiddleware(this.inactiveAccount.handler)
|
||||
);
|
||||
|
||||
router.post('/:id/recalculate-balance',
|
||||
router.post(
|
||||
'/:id/recalculate-balance',
|
||||
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,
|
||||
asyncMiddleware(this.transferToAnotherAccount.handler));
|
||||
asyncMiddleware(this.transferToAnotherAccount.handler)
|
||||
);
|
||||
|
||||
router.post('/bulk/:type(activate|inactivate)',
|
||||
router.post(
|
||||
'/bulk/:type(activate|inactivate)',
|
||||
this.bulkInactivateAccounts.validation,
|
||||
asyncMiddleware(this.bulkInactivateAccounts.handler));
|
||||
asyncMiddleware(this.bulkInactivateAccounts.handler)
|
||||
);
|
||||
|
||||
return router;
|
||||
},
|
||||
@@ -80,34 +96,34 @@ export default {
|
||||
*/
|
||||
newAccount: {
|
||||
validation: [
|
||||
check('name').exists().isLength({ min: 3 })
|
||||
.trim()
|
||||
.escape(),
|
||||
check('code').optional().isLength({ max: 10 })
|
||||
.trim()
|
||||
.escape(),
|
||||
check('name').exists().isLength({ min: 3, max: 255 }).trim().escape(),
|
||||
check('code').optional().isLength({ min: 3, max: 6, }).trim().escape(),
|
||||
check('account_type_id').exists().isNumeric().toInt(),
|
||||
check('description').optional().trim().escape(),
|
||||
check('description').optional().isLength({ max: 512 }).trim().escape(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
code: 'validation_error',
|
||||
...validationErrors,
|
||||
});
|
||||
}
|
||||
const form = { ...req.body };
|
||||
const { AccountType, Account } = req.models;
|
||||
|
||||
const foundAccountCodePromise = form.code
|
||||
? Account.query().where('code', form.code) : null;
|
||||
? Account.query().where('code', form.code)
|
||||
: null;
|
||||
|
||||
const foundAccountTypePromise = AccountType.query()
|
||||
.findById(form.account_type_id);
|
||||
const foundAccountTypePromise = AccountType.query().findById(
|
||||
form.account_type_id
|
||||
);
|
||||
|
||||
const [foundAccountCode, foundAccountType] = await Promise.all([
|
||||
foundAccountCodePromise, foundAccountTypePromise,
|
||||
foundAccountCodePromise,
|
||||
foundAccountTypePromise,
|
||||
]);
|
||||
|
||||
if (foundAccountCodePromise && foundAccountCode.length > 0) {
|
||||
@@ -132,14 +148,10 @@ export default {
|
||||
editAccount: {
|
||||
validation: [
|
||||
param('id').exists().toInt(),
|
||||
check('name').exists().isLength({ min: 3 })
|
||||
.trim()
|
||||
.escape(),
|
||||
check('code').optional().isLength({ max: 10 })
|
||||
.trim()
|
||||
.escape(),
|
||||
check('name').exists().isLength({ min: 3, max: 255, }).trim().escape(),
|
||||
check('code').optional().isLength({ min: 3, max: 6, }).trim().escape(),
|
||||
check('account_type_id').exists().isNumeric().toInt(),
|
||||
check('description').optional().trim().escape(),
|
||||
check('description').optional().isLength({ max: 512 }).trim().escape(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const { id } = req.params;
|
||||
@@ -147,7 +159,8 @@ export default {
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
code: 'validation_error',
|
||||
...validationErrors,
|
||||
});
|
||||
}
|
||||
const { Account, AccountType } = req.models;
|
||||
@@ -162,12 +175,15 @@ export default {
|
||||
// Validate the account type is not changed.
|
||||
if (account.account_type_id != form.accountTypeId) {
|
||||
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.
|
||||
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) {
|
||||
errorReasons.push({ type: 'NOT_UNIQUE_CODE', code: 200 });
|
||||
@@ -178,7 +194,10 @@ export default {
|
||||
return res.status(400).send({ errors: errorReasons });
|
||||
}
|
||||
// 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 } });
|
||||
},
|
||||
@@ -188,18 +207,16 @@ export default {
|
||||
* Get details of the given account.
|
||||
*/
|
||||
getAccount: {
|
||||
validation: [
|
||||
param('id').toInt(),
|
||||
],
|
||||
validation: [param('id').toInt()],
|
||||
async handler(req, res) {
|
||||
const { id } = req.params;
|
||||
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) {
|
||||
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.
|
||||
*/
|
||||
deleteAccount: {
|
||||
validation: [
|
||||
param('id').toInt(),
|
||||
],
|
||||
validation: [param('id').toInt()],
|
||||
async handler(req, res) {
|
||||
const { id } = req.params;
|
||||
const { Account, AccountTransaction } = req.models;
|
||||
@@ -220,11 +235,13 @@ export default {
|
||||
}
|
||||
if (account.predefined) {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'ACCOUNT.PREDEFINED' , code: 200 }],
|
||||
errors: [{ type: 'ACCOUNT.PREDEFINED', code: 200 }],
|
||||
});
|
||||
}
|
||||
const accountTransactions = await AccountTransaction.query()
|
||||
.where('account_id', account.id);
|
||||
const accountTransactions = await AccountTransaction.query().where(
|
||||
'account_id',
|
||||
account.id
|
||||
);
|
||||
|
||||
if (accountTransactions.length > 0) {
|
||||
return res.boom.badRequest(null, {
|
||||
@@ -257,7 +274,8 @@ export default {
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
code: 'validation_error',
|
||||
...validationErrors,
|
||||
});
|
||||
}
|
||||
const filter = {
|
||||
@@ -307,7 +325,7 @@ export default {
|
||||
}
|
||||
const sortByFilter = new DynamicFilterSortBy(
|
||||
filter.column_sort_by,
|
||||
filter.sort_order,
|
||||
filter.sort_order
|
||||
);
|
||||
dynamicFilter.setFilter(sortByFilter);
|
||||
}
|
||||
@@ -316,10 +334,13 @@ export default {
|
||||
if (view && view.roles.length > 0) {
|
||||
const viewFilter = new DynamicFilterViews(
|
||||
mapViewRolesToConditionals(view.roles),
|
||||
view.rolesLogicExpression,
|
||||
view.rolesLogicExpression
|
||||
);
|
||||
if (!viewFilter.validateFilterRoles()) {
|
||||
errorReasons.push({ type: 'VIEW.LOGIC.EXPRESSION.INVALID', code: 400 });
|
||||
errorReasons.push({
|
||||
type: 'VIEW.LOGIC.EXPRESSION.INVALID',
|
||||
code: 400,
|
||||
});
|
||||
}
|
||||
dynamicFilter.setFilter(viewFilter);
|
||||
}
|
||||
@@ -328,12 +349,15 @@ export default {
|
||||
// Validate the accounts resource fields.
|
||||
const filterRoles = new DynamicFilterFilterRoles(
|
||||
mapFilterRolesToDynamicFilter(filter.filter_roles),
|
||||
accountsResource.fields,
|
||||
accountsResource.fields
|
||||
);
|
||||
dynamicFilter.setFilter(filterRoles);
|
||||
|
||||
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) {
|
||||
@@ -341,16 +365,18 @@ export default {
|
||||
}
|
||||
|
||||
const query = Account.query()
|
||||
// .remember()
|
||||
.onBuild((builder) => {
|
||||
builder.modify('filterAccountTypes', filter.account_types);
|
||||
builder.withGraphFetched('type');
|
||||
builder.withGraphFetched('balance');
|
||||
// .remember()
|
||||
.onBuild((builder) => {
|
||||
builder.modify('filterAccountTypes', filter.account_types);
|
||||
builder.withGraphFetched('type');
|
||||
builder.withGraphFetched('balance');
|
||||
|
||||
dynamicFilter.buildQuery()(builder);
|
||||
dynamicFilter.buildQuery()(builder);
|
||||
|
||||
// console.log(builder.toKnexQuery().toSQL());
|
||||
}).toKnexQuery().toSQL();
|
||||
// console.log(builder.toKnexQuery().toSQL());
|
||||
})
|
||||
.toKnexQuery()
|
||||
.toSQL();
|
||||
|
||||
console.log(query);
|
||||
|
||||
@@ -370,9 +396,11 @@ export default {
|
||||
|
||||
return res.status(200).send({
|
||||
accounts: nestedAccounts,
|
||||
...(view) ? {
|
||||
customViewId: view.id,
|
||||
} : {},
|
||||
...(view
|
||||
? {
|
||||
customViewId: view.id,
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
},
|
||||
},
|
||||
@@ -381,16 +409,10 @@ export default {
|
||||
* Re-calculates balance of the given account.
|
||||
*/
|
||||
recalcualteBalanace: {
|
||||
validation: [
|
||||
param('id').isNumeric().toInt(),
|
||||
],
|
||||
validation: [param('id').isNumeric().toInt()],
|
||||
async handler(req, res) {
|
||||
const { id } = req.params;
|
||||
const {
|
||||
Account,
|
||||
AccountTransaction,
|
||||
AccountBalance,
|
||||
} = req.models;
|
||||
const { Account, AccountTransaction, AccountBalance } = req.models;
|
||||
const account = await Account.findById(id);
|
||||
|
||||
if (!account) {
|
||||
@@ -398,8 +420,10 @@ export default {
|
||||
errors: [{ type: 'ACCOUNT.NOT.FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
const accountTransactions = AccountTransaction.query()
|
||||
.where('account_id', account.id);
|
||||
const accountTransactions = AccountTransaction.query().where(
|
||||
'account_id',
|
||||
account.id
|
||||
);
|
||||
|
||||
const journalEntries = new JournalPoster();
|
||||
journalEntries.loadFromCollection(accountTransactions);
|
||||
@@ -418,9 +442,7 @@ export default {
|
||||
* Active the given account.
|
||||
*/
|
||||
activeAccount: {
|
||||
validation: [
|
||||
param('id').exists().isNumeric().toInt(),
|
||||
],
|
||||
validation: [param('id').exists().isNumeric().toInt()],
|
||||
async handler(req, res) {
|
||||
const { id } = req.params;
|
||||
const { Account } = req.models;
|
||||
@@ -431,9 +453,7 @@ export default {
|
||||
errors: [{ type: 'ACCOUNT.NOT.FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
await Account.query()
|
||||
.where('id', id)
|
||||
.patch({ active: true });
|
||||
await Account.query().where('id', id).patch({ active: true });
|
||||
|
||||
return res.status(200).send({ id: account.id });
|
||||
},
|
||||
@@ -443,9 +463,7 @@ export default {
|
||||
* Inactive the given account.
|
||||
*/
|
||||
inactiveAccount: {
|
||||
validation: [
|
||||
param('id').exists().isNumeric().toInt(),
|
||||
],
|
||||
validation: [param('id').exists().isNumeric().toInt()],
|
||||
async handler(req, res) {
|
||||
const { id } = req.params;
|
||||
const { Account } = req.models;
|
||||
@@ -456,9 +474,7 @@ export default {
|
||||
errors: [{ type: 'ACCOUNT.NOT.FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
await Account.query()
|
||||
.where('id', id)
|
||||
.patch({ active: false });
|
||||
await Account.query().where('id', id).patch({ active: false });
|
||||
|
||||
return res.status(200).send({ id: account.id });
|
||||
},
|
||||
@@ -477,7 +493,8 @@ export default {
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
code: 'validation_error',
|
||||
...validationErrors,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -505,7 +522,8 @@ export default {
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
code: 'validation_error',
|
||||
...validationErrors,
|
||||
});
|
||||
}
|
||||
const filter = { ids: [], ...req.query };
|
||||
@@ -518,23 +536,27 @@ export default {
|
||||
});
|
||||
const accountsIds = accounts.map((a) => a.id);
|
||||
const notFoundAccounts = difference(filter.ids, accountsIds);
|
||||
const predefinedAccounts = accounts.filter(account => account.predefined);
|
||||
const predefinedAccounts = accounts.filter(
|
||||
(account) => account.predefined
|
||||
);
|
||||
const errorReasons = [];
|
||||
|
||||
if (notFoundAccounts.length > 0) {
|
||||
return res.status(404).send({
|
||||
errors: [{
|
||||
type: 'ACCOUNTS.IDS.NOT.FOUND',
|
||||
code: 200,
|
||||
ids: notFoundAccounts,
|
||||
}],
|
||||
errors: [
|
||||
{
|
||||
type: 'ACCOUNTS.IDS.NOT.FOUND',
|
||||
code: 200,
|
||||
ids: notFoundAccounts,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (predefinedAccounts.length > 0) {
|
||||
errorReasons.push({
|
||||
type: 'ACCOUNT.PREDEFINED',
|
||||
code: 200,
|
||||
ids: predefinedAccounts.map(a => a.id),
|
||||
ids: predefinedAccounts.map((a) => a.id),
|
||||
});
|
||||
}
|
||||
const accountsTransactions = await AccountTransaction.query()
|
||||
@@ -554,14 +576,17 @@ export default {
|
||||
errorReasons.push({
|
||||
type: 'ACCOUNT.HAS.ASSOCIATED.TRANSACTIONS',
|
||||
code: 300,
|
||||
ids: accountsHasTransactions
|
||||
ids: accountsHasTransactions,
|
||||
});
|
||||
}
|
||||
if (errorReasons.length > 0) {
|
||||
return res.status(400).send({ errors: errorReasons });
|
||||
}
|
||||
await Account.query()
|
||||
.whereIn('id', accounts.map((a) => a.id))
|
||||
.whereIn(
|
||||
'id',
|
||||
accounts.map((a) => a.id)
|
||||
)
|
||||
.delete();
|
||||
|
||||
return res.status(200).send();
|
||||
@@ -582,7 +607,8 @@ export default {
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
code: 'validation_error',
|
||||
...validationErrors,
|
||||
});
|
||||
}
|
||||
const filter = {
|
||||
@@ -608,6 +634,6 @@ export default {
|
||||
});
|
||||
|
||||
return res.status(200).send({ ids: storedAccountsIds });
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,19 +1,12 @@
|
||||
import express from 'express';
|
||||
import {
|
||||
check,
|
||||
param,
|
||||
query,
|
||||
validationResult,
|
||||
} from 'express-validator';
|
||||
import { check, param, query, validationResult } from 'express-validator';
|
||||
import moment from 'moment';
|
||||
import { difference, sumBy, omit } from 'lodash';
|
||||
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
||||
import JournalPoster from '@/services/Accounting/JournalPoster';
|
||||
import JournalEntry from '@/services/Accounting/JournalEntry';
|
||||
import JWTAuth from '@/http/middleware/jwtAuth';
|
||||
import {
|
||||
mapViewRolesToConditionals,
|
||||
} from '@/lib/ViewRolesBuilder';
|
||||
import { mapViewRolesToConditionals } from '@/lib/ViewRolesBuilder';
|
||||
import {
|
||||
DynamicFilter,
|
||||
DynamicFilterSortBy,
|
||||
@@ -21,7 +14,6 @@ import {
|
||||
DynamicFilterFilterRoles,
|
||||
} from '@/lib/DynamicFilter';
|
||||
|
||||
|
||||
export default {
|
||||
/**
|
||||
* Router constructor.
|
||||
@@ -30,33 +22,47 @@ export default {
|
||||
const router = express.Router();
|
||||
router.use(JWTAuth);
|
||||
|
||||
router.post('/',
|
||||
router.post(
|
||||
'/',
|
||||
this.newExpense.validation,
|
||||
asyncMiddleware(this.newExpense.handler));
|
||||
asyncMiddleware(this.newExpense.handler)
|
||||
);
|
||||
|
||||
router.post('/:id/publish',
|
||||
router.post(
|
||||
'/:id/publish',
|
||||
this.publishExpense.validation,
|
||||
asyncMiddleware(this.publishExpense.handler));
|
||||
asyncMiddleware(this.publishExpense.handler)
|
||||
);
|
||||
|
||||
router.delete('/:id',
|
||||
router.delete(
|
||||
'/:id',
|
||||
this.deleteExpense.validation,
|
||||
asyncMiddleware(this.deleteExpense.handler));
|
||||
asyncMiddleware(this.deleteExpense.handler)
|
||||
);
|
||||
|
||||
router.delete('/',
|
||||
router.delete(
|
||||
'/',
|
||||
this.deleteBulkExpenses.validation,
|
||||
asyncMiddleware(this.deleteBulkExpenses.handler));
|
||||
asyncMiddleware(this.deleteBulkExpenses.handler)
|
||||
);
|
||||
|
||||
router.post('/:id',
|
||||
router.post(
|
||||
'/:id',
|
||||
this.updateExpense.validation,
|
||||
asyncMiddleware(this.updateExpense.handler));
|
||||
asyncMiddleware(this.updateExpense.handler)
|
||||
);
|
||||
|
||||
router.get('/',
|
||||
router.get(
|
||||
'/',
|
||||
this.listExpenses.validation,
|
||||
asyncMiddleware(this.listExpenses.handler));
|
||||
asyncMiddleware(this.listExpenses.handler)
|
||||
);
|
||||
|
||||
router.get('/:id',
|
||||
router.get(
|
||||
'/:id',
|
||||
this.getExpense.validation,
|
||||
asyncMiddleware(this.getExpense.handler));
|
||||
asyncMiddleware(this.getExpense.handler)
|
||||
);
|
||||
|
||||
return router;
|
||||
},
|
||||
@@ -66,20 +72,27 @@ export default {
|
||||
*/
|
||||
newExpense: {
|
||||
validation: [
|
||||
check('reference_no').optional().trim().escape(),
|
||||
check('reference_no').optional().trim().escape().isLength({
|
||||
max: 255,
|
||||
}),
|
||||
check('payment_date').isISO8601().optional(),
|
||||
check('payment_account_id').exists().isNumeric().toInt(),
|
||||
check('description').optional(),
|
||||
check('currency_code').optional(),
|
||||
check('exchange_rate').optional().isNumeric().toFloat(),
|
||||
check('publish').optional().isBoolean().toBoolean(),
|
||||
|
||||
check('categories').exists().isArray({ min: 1 }),
|
||||
check('categories.*.index').exists().isNumeric().toInt(),
|
||||
check('categories.*.expense_account_id').exists().isNumeric().toInt(),
|
||||
check('categories.*.amount').optional().isNumeric().toFloat(),
|
||||
check('categories.*.description').optional().trim().escape(),
|
||||
|
||||
check('categories.*.amount')
|
||||
.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.*.key').exists().trim().escape(),
|
||||
check('custom_fields.*.value').exists(),
|
||||
@@ -89,7 +102,8 @@ export default {
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
code: 'validation_error',
|
||||
...validationErrors,
|
||||
});
|
||||
}
|
||||
const { user } = req;
|
||||
@@ -103,24 +117,37 @@ export default {
|
||||
...req.body,
|
||||
};
|
||||
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 storedExpenseAccountsIds = storedExpenseAccounts.map(a => a.id);
|
||||
|
||||
const notStoredExpensesAccountsIds = difference(expenseAccountsIds, storedExpenseAccountsIds);
|
||||
const storedExpenseAccounts = await Account.query().whereIn(
|
||||
'id',
|
||||
expenseAccountsIds
|
||||
);
|
||||
const storedExpenseAccountsIds = storedExpenseAccounts.map((a) => a.id);
|
||||
|
||||
const notStoredExpensesAccountsIds = difference(
|
||||
expenseAccountsIds,
|
||||
storedExpenseAccountsIds
|
||||
);
|
||||
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) {
|
||||
errorReasons.push({
|
||||
type: 'PAYMENT.ACCOUNT.NOT.FOUND', code: 500,
|
||||
type: 'PAYMENT.ACCOUNT.NOT.FOUND',
|
||||
code: 500,
|
||||
});
|
||||
}
|
||||
if (notStoredExpensesAccountsIds.length > 0) {
|
||||
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) {
|
||||
@@ -129,7 +156,7 @@ export default {
|
||||
if (errorReasons.length > 0) {
|
||||
return res.status(400).send({ errors: errorReasons });
|
||||
}
|
||||
|
||||
|
||||
const expenseTransaction = await Expense.query().insert({
|
||||
total_amount: totalAmount,
|
||||
payment_account_id: form.payment_account_id,
|
||||
@@ -146,7 +173,7 @@ export default {
|
||||
...category,
|
||||
});
|
||||
storeExpenseCategoriesOper.push(oper);
|
||||
});
|
||||
});
|
||||
|
||||
const accountsDepGraph = await Account.depGraph().query();
|
||||
const journalPoster = new JournalPoster(accountsDepGraph);
|
||||
@@ -155,14 +182,14 @@ export default {
|
||||
referenceType: 'Expense',
|
||||
referenceId: expenseTransaction.id,
|
||||
userId: user.id,
|
||||
draft: !form.publish,
|
||||
draft: !form.publish,
|
||||
};
|
||||
const paymentJournalEntry = new JournalEntry({
|
||||
credit: totalAmount,
|
||||
account: paymentAccount.id,
|
||||
...mixinEntry,
|
||||
});
|
||||
journalPoster.credit(paymentJournalEntry)
|
||||
journalPoster.credit(paymentJournalEntry);
|
||||
|
||||
form.categories.forEach((category) => {
|
||||
const expenseJournalEntry = new JournalEntry({
|
||||
@@ -176,9 +203,9 @@ export default {
|
||||
await Promise.all([
|
||||
...storeExpenseCategoriesOper,
|
||||
journalPoster.saveEntries(),
|
||||
(form.status) && journalPoster.saveBalance(),
|
||||
form.status && journalPoster.saveBalance(),
|
||||
]);
|
||||
|
||||
|
||||
return res.status(200).send({ id: expenseTransaction.id });
|
||||
},
|
||||
},
|
||||
@@ -187,15 +214,14 @@ export default {
|
||||
* Publish the given expense id.
|
||||
*/
|
||||
publishExpense: {
|
||||
validation: [
|
||||
param('id').exists().isNumeric().toInt(),
|
||||
],
|
||||
validation: [param('id').exists().isNumeric().toInt()],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
code: 'validation_error',
|
||||
...validationErrors,
|
||||
});
|
||||
}
|
||||
const { id } = req.params;
|
||||
@@ -248,7 +274,7 @@ export default {
|
||||
* Retrieve paginated expenses list.
|
||||
*/
|
||||
listExpenses: {
|
||||
validation: [
|
||||
validation: [
|
||||
query('page').optional().isNumeric().toInt(),
|
||||
query('page_size').optional().isNumeric().toInt(),
|
||||
|
||||
@@ -263,7 +289,8 @@ export default {
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
code: 'validation_error',
|
||||
...validationErrors,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -282,8 +309,8 @@ export default {
|
||||
.where('name', 'expenses')
|
||||
.withGraphFetched('fields')
|
||||
.first();
|
||||
|
||||
const expensesResourceFields = expensesResource.fields.map(f => f.key);
|
||||
|
||||
const expensesResourceFields = expensesResource.fields.map((f) => f.key);
|
||||
|
||||
if (!expensesResource) {
|
||||
return res.status(400).send({
|
||||
@@ -309,18 +336,21 @@ export default {
|
||||
}
|
||||
const sortByFilter = new DynamicFilterSortBy(
|
||||
filter.column_sort_by,
|
||||
filter.sort_order,
|
||||
filter.sort_order
|
||||
);
|
||||
dynamicFilter.setFilter(sortByFilter);
|
||||
dynamicFilter.setFilter(sortByFilter);
|
||||
}
|
||||
// Custom view roles.
|
||||
if (view && view.roles.length > 0) {
|
||||
const viewFilter = new DynamicFilterViews(
|
||||
mapViewRolesToConditionals(view.roles),
|
||||
view.rolesLogicExpression,
|
||||
view.rolesLogicExpression
|
||||
);
|
||||
if (viewFilter.validateFilterRoles()) {
|
||||
errorReasons.push({ type: 'VIEW.LOGIC.EXPRESSION.INVALID', code: 400 });
|
||||
errorReasons.push({
|
||||
type: 'VIEW.LOGIC.EXPRESSION.INVALID',
|
||||
code: 400,
|
||||
});
|
||||
}
|
||||
dynamicFilter.setFilter(viewFilter);
|
||||
}
|
||||
@@ -328,32 +358,39 @@ export default {
|
||||
if (filter.filter_roles.length > 0) {
|
||||
const filterRoles = new DynamicFilterFilterRoles(
|
||||
mapFilterRolesToDynamicFilter(filter.filter_roles),
|
||||
expensesResource.fields,
|
||||
expensesResource.fields
|
||||
);
|
||||
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);
|
||||
}
|
||||
if (errorReasons.length > 0) {
|
||||
return res.status(400).send({ errors: errorReasons });
|
||||
}
|
||||
const expenses = await Expense.query().onBuild((builder) => {
|
||||
builder.withGraphFetched('paymentAccount');
|
||||
builder.withGraphFetched('categories.expenseAccount');
|
||||
builder.withGraphFetched('user');
|
||||
dynamicFilter.buildQuery()(builder);
|
||||
}).pagination(filter.page - 1, filter.page_size);;
|
||||
const expenses = await Expense.query()
|
||||
.onBuild((builder) => {
|
||||
builder.withGraphFetched('paymentAccount');
|
||||
builder.withGraphFetched('categories.expenseAccount');
|
||||
builder.withGraphFetched('user');
|
||||
dynamicFilter.buildQuery()(builder);
|
||||
})
|
||||
.pagination(filter.page - 1, filter.page_size);
|
||||
|
||||
return res.status(200).send({
|
||||
expenses: {
|
||||
...expenses,
|
||||
...(view) ? {
|
||||
viewMeta: {
|
||||
viewColumns: view.columns,
|
||||
customViewId: view.id,
|
||||
}
|
||||
} : {},
|
||||
...(view
|
||||
? {
|
||||
viewMeta: {
|
||||
viewColumns: view.columns,
|
||||
customViewId: view.id,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
});
|
||||
},
|
||||
@@ -363,15 +400,14 @@ export default {
|
||||
* Delete the given expense transaction.
|
||||
*/
|
||||
deleteExpense: {
|
||||
validation: [
|
||||
param('id').isNumeric().toInt(),
|
||||
],
|
||||
validation: [param('id').isNumeric().toInt()],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
code: 'validation_error',
|
||||
...validationErrors,
|
||||
});
|
||||
}
|
||||
const { id } = req.params;
|
||||
@@ -385,12 +421,17 @@ export default {
|
||||
const expense = await Expense.query().where('id', id).first();
|
||||
|
||||
if (!expense) {
|
||||
return res.status(404).send({ errors: [{
|
||||
type: 'EXPENSE.NOT.FOUND', code: 200,
|
||||
}] });
|
||||
return res.status(404).send({
|
||||
errors: [
|
||||
{
|
||||
type: 'EXPENSE.NOT.FOUND',
|
||||
code: 200,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
await ExpenseCategory.query().where('expense_id', id).delete();
|
||||
|
||||
|
||||
const deleteExpenseOper = Expense.query().where('id', id).delete();
|
||||
const expenseTransactions = await AccountTransaction.query()
|
||||
.where('reference_type', 'Expense')
|
||||
@@ -437,12 +478,18 @@ export default {
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
code: 'validation_error',
|
||||
...validationErrors,
|
||||
});
|
||||
}
|
||||
const { id } = req.params;
|
||||
const { user } = req;
|
||||
const { Account, Expense, ExpenseCategory, AccountTransaction } = req.models;
|
||||
const {
|
||||
Account,
|
||||
Expense,
|
||||
ExpenseCategory,
|
||||
AccountTransaction,
|
||||
} = req.models;
|
||||
|
||||
const form = {
|
||||
categories: [],
|
||||
@@ -463,39 +510,55 @@ export default {
|
||||
}
|
||||
const errorReasons = [];
|
||||
const paymentAccount = await Account.query()
|
||||
.where('id', form.payment_account_id).first();
|
||||
.where('id', form.payment_account_id)
|
||||
.first();
|
||||
|
||||
if (!paymentAccount) {
|
||||
errorReasons.push({ type: 'PAYMENT.ACCOUNT.NOT.FOUND', code: 400 });
|
||||
}
|
||||
const categoriesHasNoId = form.categories.filter(c => !c.id);
|
||||
const categoriesHasId = form.categories.filter(c => c.id);
|
||||
const categoriesHasNoId = form.categories.filter((c) => !c.id);
|
||||
const categoriesHasId = form.categories.filter((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(
|
||||
formExpenseCategoriesIds, expenseCategoriesIds,
|
||||
formExpenseCategoriesIds,
|
||||
expenseCategoriesIds
|
||||
);
|
||||
const categoriesShouldDelete = difference(
|
||||
expenseCategoriesIds, formExpenseCategoriesIds,
|
||||
expenseCategoriesIds,
|
||||
formExpenseCategoriesIds
|
||||
);
|
||||
|
||||
const formExpensesAccountsIds = form.categories.map(c => c.expense_account_id);
|
||||
const storedExpenseAccounts = await Account.query().whereIn('id', formExpensesAccountsIds);
|
||||
const storedExpenseAccountsIds = storedExpenseAccounts.map(a => a.id);
|
||||
const formExpensesAccountsIds = form.categories.map(
|
||||
(c) => c.expense_account_id
|
||||
);
|
||||
const storedExpenseAccounts = await Account.query().whereIn(
|
||||
'id',
|
||||
formExpensesAccountsIds
|
||||
);
|
||||
const storedExpenseAccountsIds = storedExpenseAccounts.map((a) => a.id);
|
||||
|
||||
const expenseAccountsIdsNotFound = difference(
|
||||
formExpensesAccountsIds, storedExpenseAccountsIds,
|
||||
);
|
||||
formExpensesAccountsIds,
|
||||
storedExpenseAccountsIds
|
||||
);
|
||||
const totalAmount = sumBy(form.categories, 'amount');
|
||||
|
||||
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) {
|
||||
errorReasons.push({ type: 'EXPENSE.CATEGORIES.IDS.NOT.FOUND', code: 300 });
|
||||
errorReasons.push({
|
||||
type: 'EXPENSE.CATEGORIES.IDS.NOT.FOUND',
|
||||
code: 300,
|
||||
});
|
||||
}
|
||||
if (totalAmount <= 0) {
|
||||
errorReasons.push({ type: 'TOTAL.AMOUNT.EQUALS.ZERO', code: 500 });
|
||||
@@ -504,12 +567,13 @@ export default {
|
||||
if (errorReasons.length > 0) {
|
||||
return res.status(400).send({ errors: errorReasons });
|
||||
}
|
||||
const expenseCategoriesMap = new Map(expense.categories
|
||||
.map(category => [category.id, category]));
|
||||
const expenseCategoriesMap = new Map(
|
||||
expense.categories.map((category) => [category.id, category])
|
||||
);
|
||||
|
||||
const categoriesInsertOpers = [];
|
||||
const categoriesUpdateOpers = [];
|
||||
|
||||
|
||||
categoriesHasNoId.forEach((category) => {
|
||||
const oper = ExpenseCategory.query().insert({
|
||||
...category,
|
||||
@@ -518,26 +582,31 @@ export default {
|
||||
categoriesInsertOpers.push(oper);
|
||||
});
|
||||
|
||||
categoriesHasId.forEach((category) => {
|
||||
const oper = ExpenseCategory.query().where('id', category.id)
|
||||
categoriesHasId.forEach((category) => {
|
||||
const oper = ExpenseCategory.query()
|
||||
.where('id', category.id)
|
||||
.patch({
|
||||
...omit(category, ['id']),
|
||||
});
|
||||
categoriesUpdateOpers.push(oper);
|
||||
});
|
||||
|
||||
const updateExpenseOper = Expense.query().where('id', id)
|
||||
const updateExpenseOper = Expense.query()
|
||||
.where('id', id)
|
||||
.update({
|
||||
payment_date: moment(form.payment_date).format('YYYY-MM-DD'),
|
||||
total_amount: totalAmount,
|
||||
description: form.description,
|
||||
payment_account_id: form.payment_account_id,
|
||||
reference_no: form.reference_no,
|
||||
})
|
||||
});
|
||||
|
||||
const deleteCategoriesOper = (categoriesShouldDelete.length > 0) ?
|
||||
ExpenseCategory.query().whereIn('id', categoriesShouldDelete).delete() :
|
||||
Promise.resolve();
|
||||
const deleteCategoriesOper =
|
||||
categoriesShouldDelete.length > 0
|
||||
? ExpenseCategory.query()
|
||||
.whereIn('id', categoriesShouldDelete)
|
||||
.delete()
|
||||
: Promise.resolve();
|
||||
|
||||
// Update the journal entries.
|
||||
const transactions = await AccountTransaction.query()
|
||||
@@ -555,7 +624,7 @@ export default {
|
||||
referenceType: 'Expense',
|
||||
referenceId: expense.id,
|
||||
userId: user.id,
|
||||
draft: !form.publish,
|
||||
draft: !form.publish,
|
||||
};
|
||||
const paymentJournalEntry = new JournalEntry({
|
||||
credit: totalAmount,
|
||||
@@ -573,7 +642,7 @@ export default {
|
||||
});
|
||||
journal.debit(entry);
|
||||
});
|
||||
|
||||
|
||||
await Promise.all([
|
||||
...categoriesInsertOpers,
|
||||
...categoriesUpdateOpers,
|
||||
@@ -581,7 +650,7 @@ export default {
|
||||
deleteCategoriesOper,
|
||||
|
||||
journal.saveEntries(),
|
||||
(form.status) && journal.saveBalance(),
|
||||
form.status && journal.saveBalance(),
|
||||
]);
|
||||
return res.status(200).send({ id });
|
||||
},
|
||||
@@ -591,15 +660,14 @@ export default {
|
||||
* Retrieve details of the given expense id.
|
||||
*/
|
||||
getExpense: {
|
||||
validation: [
|
||||
param('id').exists().isNumeric().toInt(),
|
||||
],
|
||||
validation: [param('id').exists().isNumeric().toInt()],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
code: 'validation_error',
|
||||
...validationErrors,
|
||||
});
|
||||
}
|
||||
const { id } = req.params;
|
||||
@@ -624,9 +692,9 @@ export default {
|
||||
|
||||
return res.status(200).send({
|
||||
expense: {
|
||||
...expense,
|
||||
...expense.toJSON(),
|
||||
journalEntries,
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
@@ -644,16 +712,16 @@ export default {
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
code: 'validation_error',
|
||||
...validationErrors,
|
||||
});
|
||||
}
|
||||
const filter = { ...req.query };
|
||||
const { Expense, AccountTransaction, Account, MediaLink } = req.models;
|
||||
|
||||
const expenses = await Expense.query()
|
||||
.whereIn('id', filter.ids)
|
||||
const expenses = await Expense.query().whereIn('id', filter.ids);
|
||||
|
||||
const storedExpensesIds = expenses.map(e => e.id);
|
||||
const storedExpensesIds = expenses.map((e) => e.id);
|
||||
const notFoundExpenses = difference(filter.ids, storedExpensesIds);
|
||||
|
||||
if (notFoundExpenses.length > 0) {
|
||||
@@ -663,11 +731,12 @@ export default {
|
||||
}
|
||||
|
||||
const deleteExpensesOper = Expense.query()
|
||||
.whereIn('id', storedExpensesIds).delete();
|
||||
.whereIn('id', storedExpensesIds)
|
||||
.delete();
|
||||
|
||||
const transactions = await AccountTransaction.query()
|
||||
.whereIn('reference_type', ['Expense'])
|
||||
.whereIn('reference_id', filter.ids)
|
||||
.whereIn('reference_id', filter.ids);
|
||||
|
||||
const accountsDepGraph = await Account.depGraph().query().remember();
|
||||
const journal = new JournalPoster(accountsDepGraph);
|
||||
@@ -686,6 +755,6 @@ export default {
|
||||
journal.saveBalance(),
|
||||
]);
|
||||
return res.status(200).send({ ids: filter.ids });
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user