Compare commits
11 Commits
vercel-tes
...
vercel-ign
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a142b734d3 | ||
|
|
2263cf5657 | ||
|
|
e488c0eea9 | ||
|
|
f093239a15 | ||
|
|
5c537e094d | ||
|
|
a371fd44f7 | ||
|
|
59cb168331 | ||
|
|
8a5fbfc041 | ||
|
|
e3a072e267 | ||
|
|
b03606406e | ||
|
|
a1a7ee2b5b |
5697
package-lock.json
generated
Normal file
5697
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,7 @@ import DynamicListingService from '@/services/DynamicListing/DynamicListService'
|
||||
import { DATATYPES_LENGTH } from '@/data/DataTypes';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
import { AccountsApplication } from '@/services/Accounts/AccountsApplication';
|
||||
import { MAX_ACCOUNTS_CHART_DEPTH } from 'services/Accounts/constants';
|
||||
|
||||
@Service()
|
||||
export default class AccountsController extends BaseController {
|
||||
@@ -494,6 +495,22 @@ export default class AccountsController extends BaseController {
|
||||
}
|
||||
);
|
||||
}
|
||||
if (error.errorType === 'PARENT_ACCOUNT_EXCEEDED_THE_DEPTH_LEVEL') {
|
||||
return res.boom.badRequest(
|
||||
'The parent account exceeded the depth level of accounts chart.',
|
||||
{
|
||||
errors: [
|
||||
{
|
||||
type: 'PARENT_ACCOUNT_EXCEEDED_THE_DEPTH_LEVEL',
|
||||
code: 1500,
|
||||
data: {
|
||||
maxDepth: MAX_ACCOUNTS_CHART_DEPTH,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import TenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import { IAccountDTO, IAccount, IAccountCreateDTO } from '@/interfaces';
|
||||
import AccountTypesUtils from '@/lib/AccountTypes';
|
||||
import { ERRORS } from './constants';
|
||||
import { ERRORS, MAX_ACCOUNTS_CHART_DEPTH } from './constants';
|
||||
|
||||
@Service()
|
||||
export class CommandAccountValidators {
|
||||
@@ -154,13 +154,13 @@ export class CommandAccountValidators {
|
||||
* parent account.
|
||||
* @param {IAccountCreateDTO} accountDTO
|
||||
* @param {IAccount} parentAccount
|
||||
* @param {string} baseCurrency -
|
||||
* @param {string} baseCurrency -
|
||||
* @throws {ServiceError(ERRORS.ACCOUNT_CURRENCY_NOT_SAME_PARENT_ACCOUNT)}
|
||||
*/
|
||||
public validateCurrentSameParentAccount = (
|
||||
accountDTO: IAccountCreateDTO,
|
||||
parentAccount: IAccount,
|
||||
baseCurrency: string,
|
||||
baseCurrency: string
|
||||
) => {
|
||||
// If the account DTO currency not assigned and the parent account has no base currency.
|
||||
if (
|
||||
@@ -208,4 +208,24 @@ export class CommandAccountValidators {
|
||||
}
|
||||
return account;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the max depth level of accounts chart.
|
||||
* @param {numebr} tenantId - Tenant id.
|
||||
* @param {number} parentAccountId - Parent account id.
|
||||
*/
|
||||
public async validateMaxParentAccountDepthLevels(
|
||||
tenantId: number,
|
||||
parentAccountId: number
|
||||
) {
|
||||
const { accountRepository } = this.tenancy.repositories(tenantId);
|
||||
|
||||
const accountsGraph = await accountRepository.getDependencyGraph();
|
||||
|
||||
const parentDependantsIds = accountsGraph.dependantsOf(parentAccountId);
|
||||
|
||||
if (parentDependantsIds.length >= MAX_ACCOUNTS_CHART_DEPTH) {
|
||||
throw new ServiceError(ERRORS.PARENT_ACCOUNT_EXCEEDED_THE_DEPTH_LEVEL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +70,11 @@ export class CreateAccount {
|
||||
parentAccount,
|
||||
baseCurrency
|
||||
);
|
||||
// Validates the max depth level of accounts chart.
|
||||
await this.validator.validateMaxParentAccountDepthLevels(
|
||||
tenantId,
|
||||
accountDTO.parentAccountId
|
||||
);
|
||||
}
|
||||
// Validates the given account type supports the multi-currency.
|
||||
this.validator.validateAccountTypeSupportCurrency(accountDTO, baseCurrency);
|
||||
|
||||
@@ -5,6 +5,7 @@ import TenancyService from '@/services/Tenancy/TenancyService';
|
||||
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
|
||||
import { AccountTransformer } from './AccountTransform';
|
||||
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||
import { flatToNestedArray } from '@/utils';
|
||||
|
||||
@Service()
|
||||
export class GetAccounts {
|
||||
@@ -53,11 +54,17 @@ export class GetAccounts {
|
||||
builder.modify('inactiveMode', filter.inactiveMode);
|
||||
});
|
||||
// Retrievs the formatted accounts collection.
|
||||
const transformedAccounts = await this.transformer.transform(
|
||||
const preTransformedAccounts = await this.transformer.transform(
|
||||
tenantId,
|
||||
accounts,
|
||||
new AccountTransformer()
|
||||
);
|
||||
// Transform accounts to nested array.
|
||||
const transformedAccounts = flatToNestedArray(preTransformedAccounts, {
|
||||
id: 'id',
|
||||
parentId: 'parentAccountId',
|
||||
});
|
||||
|
||||
return {
|
||||
accounts: transformedAccounts,
|
||||
filterMeta: dynamicList.getResponseMeta(),
|
||||
|
||||
@@ -13,8 +13,12 @@ export const ERRORS = {
|
||||
CLOSE_ACCOUNT_AND_TO_ACCOUNT_NOT_SAME_TYPE:
|
||||
'close_account_and_to_account_not_same_type',
|
||||
ACCOUNTS_NOT_FOUND: 'accounts_not_found',
|
||||
ACCOUNT_TYPE_NOT_SUPPORTS_MULTI_CURRENCY: 'ACCOUNT_TYPE_NOT_SUPPORTS_MULTI_CURRENCY',
|
||||
ACCOUNT_CURRENCY_NOT_SAME_PARENT_ACCOUNT: 'ACCOUNT_CURRENCY_NOT_SAME_PARENT_ACCOUNT',
|
||||
ACCOUNT_TYPE_NOT_SUPPORTS_MULTI_CURRENCY:
|
||||
'ACCOUNT_TYPE_NOT_SUPPORTS_MULTI_CURRENCY',
|
||||
ACCOUNT_CURRENCY_NOT_SAME_PARENT_ACCOUNT:
|
||||
'ACCOUNT_CURRENCY_NOT_SAME_PARENT_ACCOUNT',
|
||||
PARENT_ACCOUNT_EXCEEDED_THE_DEPTH_LEVEL:
|
||||
'PARENT_ACCOUNT_EXCEEDED_THE_DEPTH_LEVEL',
|
||||
};
|
||||
|
||||
// Default views columns.
|
||||
@@ -27,6 +31,8 @@ export const DEFAULT_VIEW_COLUMNS = [
|
||||
{ key: 'currencyCode', label: 'Currency' },
|
||||
];
|
||||
|
||||
export const MAX_ACCOUNTS_CHART_DEPTH = 5;
|
||||
|
||||
// Accounts default views.
|
||||
export const DEFAULT_VIEWS = [
|
||||
{
|
||||
@@ -43,7 +49,12 @@ export const DEFAULT_VIEWS = [
|
||||
slug: 'liabilities',
|
||||
rolesLogicExpression: '1',
|
||||
roles: [
|
||||
{ fieldKey: 'root_type', index: 1, comparator: 'equals', value: 'liability' },
|
||||
{
|
||||
fieldKey: 'root_type',
|
||||
index: 1,
|
||||
comparator: 'equals',
|
||||
value: 'liability',
|
||||
},
|
||||
],
|
||||
columns: DEFAULT_VIEW_COLUMNS,
|
||||
},
|
||||
@@ -52,7 +63,12 @@ export const DEFAULT_VIEWS = [
|
||||
slug: 'equity',
|
||||
rolesLogicExpression: '1',
|
||||
roles: [
|
||||
{ fieldKey: 'root_type', index: 1, comparator: 'equals', value: 'equity' },
|
||||
{
|
||||
fieldKey: 'root_type',
|
||||
index: 1,
|
||||
comparator: 'equals',
|
||||
value: 'equity',
|
||||
},
|
||||
],
|
||||
columns: DEFAULT_VIEW_COLUMNS,
|
||||
},
|
||||
@@ -61,7 +77,12 @@ export const DEFAULT_VIEWS = [
|
||||
slug: 'income',
|
||||
rolesLogicExpression: '1',
|
||||
roles: [
|
||||
{ fieldKey: 'root_type', index: 1, comparator: 'equals', value: 'income' },
|
||||
{
|
||||
fieldKey: 'root_type',
|
||||
index: 1,
|
||||
comparator: 'equals',
|
||||
value: 'income',
|
||||
},
|
||||
],
|
||||
columns: DEFAULT_VIEW_COLUMNS,
|
||||
},
|
||||
@@ -70,7 +91,12 @@ export const DEFAULT_VIEWS = [
|
||||
slug: 'expenses',
|
||||
rolesLogicExpression: '1',
|
||||
roles: [
|
||||
{ fieldKey: 'root_type', index: 1, comparator: 'equals', value: 'expense' },
|
||||
{
|
||||
fieldKey: 'root_type',
|
||||
index: 1,
|
||||
comparator: 'equals',
|
||||
value: 'expense',
|
||||
},
|
||||
],
|
||||
columns: DEFAULT_VIEW_COLUMNS,
|
||||
},
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
"yup": "^0.28.1"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "craco start",
|
||||
"dev": "PORT=4000 craco start",
|
||||
"build": "craco build",
|
||||
"test": "node scripts/test.js",
|
||||
"storybook": "start-storybook -p 6006"
|
||||
|
||||
@@ -72,6 +72,7 @@ export default function TableCell({ cell, row, index }) {
|
||||
[`td-${cell.column.id}`]: cell.column.id,
|
||||
[`td-${cellType}-type`]: !!cellType,
|
||||
}),
|
||||
tabindex: 0,
|
||||
onClick: handleCellClick,
|
||||
})}
|
||||
>
|
||||
|
||||
@@ -3,7 +3,6 @@ import React, { useCallback } from 'react';
|
||||
import intl from 'react-intl-universal';
|
||||
import { Intent } from '@blueprintjs/core';
|
||||
import { Formik } from 'formik';
|
||||
import { omit } from 'lodash';
|
||||
import { AppToaster } from '@/components';
|
||||
|
||||
import AccountDialogFormContent from './AccountDialogFormContent';
|
||||
@@ -14,7 +13,11 @@ import {
|
||||
CreateAccountFormSchema,
|
||||
} from './AccountForm.schema';
|
||||
import { compose, transformToForm } from '@/utils';
|
||||
import { transformApiErrors, transformAccountToForm } from './utils';
|
||||
import {
|
||||
transformApiErrors,
|
||||
transformAccountToForm,
|
||||
transformFormToReq,
|
||||
} from './utils';
|
||||
|
||||
import '@/style/pages/Accounts/AccountFormDialog.scss';
|
||||
import { useAccountDialogContext } from './AccountDialogProvider';
|
||||
@@ -26,7 +29,7 @@ const defaultInitialValues = {
|
||||
name: '',
|
||||
code: '',
|
||||
description: '',
|
||||
currency_code:'',
|
||||
currency_code: '',
|
||||
subaccount: false,
|
||||
};
|
||||
|
||||
@@ -43,7 +46,6 @@ function AccountFormDialogContent({
|
||||
createAccountMutate,
|
||||
account,
|
||||
|
||||
accountId,
|
||||
payload,
|
||||
isNewMode,
|
||||
dialogName,
|
||||
@@ -56,7 +58,7 @@ function AccountFormDialogContent({
|
||||
|
||||
// Callbacks handles form submit.
|
||||
const handleFormSubmit = (values, { setSubmitting, setErrors }) => {
|
||||
const form = omit(values, ['subaccount']);
|
||||
const form = transformFormToReq(values);
|
||||
const toastAccountName = values.code
|
||||
? `${values.code} - ${values.name}`
|
||||
: values.name;
|
||||
@@ -90,8 +92,8 @@ function AccountFormDialogContent({
|
||||
setErrors({ ...errorsTransformed });
|
||||
setSubmitting(false);
|
||||
};
|
||||
if (accountId) {
|
||||
editAccountMutate([accountId, form])
|
||||
if (payload.accountId) {
|
||||
editAccountMutate([payload.accountId, form])
|
||||
.then(handleSuccess)
|
||||
.catch(handleError);
|
||||
} else {
|
||||
@@ -113,7 +115,6 @@ function AccountFormDialogContent({
|
||||
defaultInitialValues,
|
||||
),
|
||||
};
|
||||
|
||||
// Handles dialog close.
|
||||
const handleClose = useCallback(() => {
|
||||
closeDialog(dialogName);
|
||||
|
||||
@@ -26,6 +26,7 @@ import { inputIntent, compose } from '@/utils';
|
||||
import { useAutofocus } from '@/hooks';
|
||||
import { FOREIGN_CURRENCY_ACCOUNTS } from '@/constants/accountTypes';
|
||||
import { useAccountDialogContext } from './AccountDialogProvider';
|
||||
import { parentAccountShouldUpdate } from './utils';
|
||||
|
||||
/**
|
||||
* Account form dialogs fields.
|
||||
@@ -115,12 +116,7 @@ function AccountFormDialogFields({
|
||||
>
|
||||
<Checkbox
|
||||
inline={true}
|
||||
label={
|
||||
<>
|
||||
<T id={'sub_account'} />
|
||||
<Hint />
|
||||
</>
|
||||
}
|
||||
label={<T id={'sub_account'} />}
|
||||
name={'subaccount'}
|
||||
{...field}
|
||||
/>
|
||||
@@ -128,37 +124,36 @@ function AccountFormDialogFields({
|
||||
)}
|
||||
</Field>
|
||||
|
||||
<If condition={values.subaccount}>
|
||||
<FastField name={'parent_account_id'}>
|
||||
{({
|
||||
form: { values, setFieldValue },
|
||||
field: { value },
|
||||
meta: { error, touched },
|
||||
}) => (
|
||||
<FormGroup
|
||||
label={<T id={'parent_account'} />}
|
||||
className={classNames(
|
||||
'form-group--parent-account',
|
||||
Classes.FILL,
|
||||
)}
|
||||
inline={true}
|
||||
intent={inputIntent({ error, touched })}
|
||||
helperText={<ErrorMessage name="parent_account_id" />}
|
||||
>
|
||||
<AccountsSelectList
|
||||
accounts={accounts}
|
||||
onAccountSelected={(account) => {
|
||||
setFieldValue('parent_account_id', account.id);
|
||||
}}
|
||||
defaultSelectText={<T id={'select_parent_account'} />}
|
||||
selectedAccountId={value}
|
||||
popoverFill={true}
|
||||
filterByTypes={values.account_type}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
</FastField>
|
||||
</If>
|
||||
<FastField
|
||||
name={'parent_account_id'}
|
||||
shouldUpdate={parentAccountShouldUpdate}
|
||||
>
|
||||
{({
|
||||
form: { values, setFieldValue },
|
||||
field: { value },
|
||||
meta: { error, touched },
|
||||
}) => (
|
||||
<FormGroup
|
||||
label={<T id={'parent_account'} />}
|
||||
className={classNames('form-group--parent-account', Classes.FILL)}
|
||||
inline={true}
|
||||
intent={inputIntent({ error, touched })}
|
||||
helperText={<ErrorMessage name="parent_account_id" />}
|
||||
>
|
||||
<AccountsSelectList
|
||||
accounts={accounts}
|
||||
onAccountSelected={(account) => {
|
||||
setFieldValue('parent_account_id', account.id);
|
||||
}}
|
||||
defaultSelectText={<T id={'select_parent_account'} />}
|
||||
selectedAccountId={value}
|
||||
popoverFill={true}
|
||||
filterByTypes={values.account_type}
|
||||
disabled={!values.subaccount}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
</FastField>
|
||||
|
||||
<If condition={FOREIGN_CURRENCY_ACCOUNTS.includes(values.account_type)}>
|
||||
{/*------------ Currency -----------*/}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import intl from 'react-intl-universal';
|
||||
import * as R from 'ramda';
|
||||
import { isUndefined } from 'lodash';
|
||||
import { defaultFastFieldShouldUpdate } from '@/utils';
|
||||
|
||||
export const AccountDialogAction = {
|
||||
Edit: 'edit',
|
||||
@@ -33,7 +34,7 @@ export const transformApiErrors = (errors) => {
|
||||
/**
|
||||
* Payload transformer in account edit mode.
|
||||
*/
|
||||
function tranformNewChildAccountPayload(payload) {
|
||||
function tranformNewChildAccountPayload(account, payload) {
|
||||
return {
|
||||
parent_account_id: payload.parentAccountId || '',
|
||||
account_type: payload.accountType || '',
|
||||
@@ -44,7 +45,7 @@ function tranformNewChildAccountPayload(payload) {
|
||||
/**
|
||||
* Payload transformer in new account with defined type.
|
||||
*/
|
||||
function transformNewDefinedTypePayload(payload) {
|
||||
function transformNewDefinedTypePayload(account, payload) {
|
||||
return {
|
||||
account_type: payload.accountType || '',
|
||||
};
|
||||
@@ -63,7 +64,9 @@ const mergeWithAccount = R.curry((transformed, account) => {
|
||||
/**
|
||||
* Default account payload transformer.
|
||||
*/
|
||||
const defaultPayloadTransform = () => ({});
|
||||
const defaultPayloadTransform = (account, payload) => ({
|
||||
subaccount: !!account.parent_account_id,
|
||||
});
|
||||
|
||||
/**
|
||||
* Defined payload transformers.
|
||||
@@ -89,7 +92,7 @@ export const transformAccountToForm = (account, payload) => {
|
||||
|
||||
return [
|
||||
condition[0] === payload.action ? R.T : R.F,
|
||||
mergeWithAccount(transformer(payload)),
|
||||
mergeWithAccount(transformer(account, payload)),
|
||||
];
|
||||
});
|
||||
return R.cond(results)(account);
|
||||
@@ -106,3 +109,29 @@ export const getDisabledFormFields = (account, payload) => {
|
||||
payload.action === AccountDialogAction.NewDefinedType,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Detarmines whether should update the parent account field.
|
||||
* @param newProps
|
||||
* @param oldProps
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const parentAccountShouldUpdate = (newProps, oldProps) => {
|
||||
return (
|
||||
newProps.formik.values.subaccount !== oldProps.formik.values.subaccount ||
|
||||
defaultFastFieldShouldUpdate(newProps, oldProps)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Transformes the form values to the request.
|
||||
*/
|
||||
export const transformFormToReq = (form) => {
|
||||
return R.compose(
|
||||
R.omit(['subaccount']),
|
||||
R.when(
|
||||
R.propSatisfies(R.equals(R.__, false), 'subaccount'),
|
||||
R.assoc(['parent_account_id'], ''),
|
||||
),
|
||||
)(form);
|
||||
};
|
||||
|
||||
@@ -105,7 +105,7 @@ export function ItemsActionMenuList({
|
||||
</Can>
|
||||
<Can I={ItemAction.Create} a={AbilitySubject.Item}>
|
||||
<MenuItem
|
||||
icon={<Icon icon="duplicate-16" />}
|
||||
icon={<Icon icon="content-copy" iconSize={16} />}
|
||||
text={intl.get('duplicate')}
|
||||
onClick={safeCallback(onDuplicate, original)}
|
||||
/>
|
||||
|
||||
@@ -559,4 +559,10 @@ export default {
|
||||
],
|
||||
viewBox: '0 0 24 24',
|
||||
},
|
||||
'content-copy': {
|
||||
path: [
|
||||
'M15 0H5c-.55 0-1 .45-1 1v2h2V2h8v7h-1v2h2c.55 0 1-.45 1-1V1c0-.55-.45-1-1-1zm-4 4H1c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h10c.55 0 1-.45 1-1V5c0-.55-.45-1-1-1zm-1 10H2V6h8v8z'
|
||||
],
|
||||
viewBox: '0 0 16 16'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
display: block;
|
||||
|
||||
.thead .thead-inner,
|
||||
.tbody .tbody-inner{
|
||||
.tbody .tbody-inner {
|
||||
min-width: fit-content;
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
font-weight: 400;
|
||||
border-bottom: 1px solid #d2dde2;
|
||||
|
||||
>div {
|
||||
> div {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
@@ -208,6 +208,10 @@
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
&:focus {
|
||||
outline: 1px solid rgba(0, 82, 204, 0.7);
|
||||
outline-offset: -1px;
|
||||
}
|
||||
}
|
||||
|
||||
.tr:hover .td {
|
||||
@@ -357,13 +361,9 @@
|
||||
position: sticky;
|
||||
}
|
||||
|
||||
[data-sticky-last-left-td] {
|
||||
[data-sticky-last-left-td] {}
|
||||
|
||||
}
|
||||
|
||||
[data-sticky-first-right-td] {
|
||||
|
||||
}
|
||||
[data-sticky-first-right-td] {}
|
||||
}
|
||||
|
||||
&.has-virtualized-rows {
|
||||
|
||||
Reference in New Issue
Block a user