Fix: Purchases Bills & feat: BillNumberDialog

This commit is contained in:
elforjani3
2020-10-24 22:20:26 +02:00
parent 173c14bd14
commit d468e69a0d
18 changed files with 283 additions and 66 deletions

View File

@@ -8,13 +8,14 @@ import AccountFormDialog from 'containers/Dialogs/AccountFormDialog';
// import InviteUserDialog from 'containers/Dialogs/InviteUserDialog'; // import InviteUserDialog from 'containers/Dialogs/InviteUserDialog';
// import ExchangeRateDialog from 'containers/Dialogs/ExchangeRateDialog'; // import ExchangeRateDialog from 'containers/Dialogs/ExchangeRateDialog';
import JournalNumberDialog from 'containers/Dialogs/JournalNumberDialog'; import JournalNumberDialog from 'containers/Dialogs/JournalNumberDialog';
import BillNumberDialog from 'containers/Dialogs/BillNumberDialog';
export default function DialogsContainer() { export default function DialogsContainer() {
return ( return (
<div> <div>
<AccountFormDialog dialogName={'account-form'} /> <AccountFormDialog dialogName={'account-form'} />
<JournalNumberDialog dialogName={'journal-number-form'} /> <JournalNumberDialog dialogName={'journal-number-form'} />
<BillNumberDialog dialogName={'bill-number-form'} />
</div> </div>
); );
} }

View File

@@ -0,0 +1,79 @@
import React from 'react';
import { DialogContent } from 'components';
import { useQuery, queryCache } from 'react-query';
import ReferenceNumberForm from 'containers/JournalNumber/ReferenceNumberForm';
import withDialogActions from 'containers/Dialog/withDialogActions';
import withSettingsActions from 'containers/Settings/withSettingsActions';
import withSettings from 'containers/Settings/withSettings';
import withBillActions from 'containers/Purchases/Bill/withBillActions';
import { compose, optionsMapToArray } from 'utils';
/**
* bill number dialog's content.
*/
function BillNumberDialogContent({
// #withSettings
nextNumber,
numberPrefix,
// #withSettingsActions
requestFetchOptions,
requestSubmitOptions,
// #withDialogActions
closeDialog,
// #withBillActions
setBillNumberChanged,
}) {
const fetchSettings = useQuery(['settings'], () => requestFetchOptions({}));
const handleSubmitForm = (values, { setSubmitting }) => {
const options = optionsMapToArray(values).map((option) => {
return { key: option.key, ...option, group: 'bills' };
});
requestSubmitOptions({ options })
.then(() => {
setSubmitting(false);
closeDialog('bill-number-form');
setBillNumberChanged(true);
setTimeout(() => {
queryCache.invalidateQueries('settings');
}, 250);
})
.catch(() => {
setSubmitting(false);
});
};
const handleClose = () => {
closeDialog('bill-number-form');
};
return (
<DialogContent isLoading={fetchSettings.isFetching}>
<ReferenceNumberForm
initialNumber={nextNumber}
initialPrefix={numberPrefix}
onSubmit={handleSubmitForm}
onClose={handleClose}
/>
</DialogContent>
);
}
export default compose(
withDialogActions,
withSettingsActions,
withSettings(({ billsettings }) => ({
nextNumber: billsettings?.next_number,
numberPrefix: billsettings?.number_prefix,
})),
withBillActions,
)(BillNumberDialogContent);

View File

@@ -0,0 +1,26 @@
import React, { lazy } from 'react';
import { FormattedMessage as T } from 'react-intl';
import { Dialog, DialogSuspense } from 'components';
import withDialogRedux from 'components/DialogReduxConnect';
import { compose } from 'utils';
const BillNumberDialogContent = lazy(() => import('./BillNumberDialogContent'));
function BillNumberDialog({ dialogName, payload = { id: null }, isOpen }) {
return (
<Dialog
name={dialogName}
title={<T id={'bill_number_settings'} />}
autoFocus={true}
canEscapeKeyClose={true}
isOpen={isOpen}
className={'dialog--journal-number-settings'}
>
<DialogSuspense>
<BillNumberDialogContent billNumberId={payload.id} />
</DialogSuspense>
</Dialog>
);
}
export default compose(withDialogRedux())(BillNumberDialog);

View File

@@ -20,8 +20,10 @@ import BillFormFooter from './BillFormFooter';
import withDashboardActions from 'containers/Dashboard/withDashboardActions'; import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withMediaActions from 'containers/Media/withMediaActions'; import withMediaActions from 'containers/Media/withMediaActions';
import withBills from './withBills';
import withBillActions from './withBillActions'; import withBillActions from './withBillActions';
import withBillDetail from './withBillDetail'; import withBillDetail from './withBillDetail';
import withSettings from 'containers/Settings/withSettings';
import { AppToaster } from 'components'; import { AppToaster } from 'components';
import Dragzone from 'components/Dragzone'; import Dragzone from 'components/Dragzone';
@@ -29,7 +31,7 @@ import useMedia from 'hooks/useMedia';
import { compose, repeatValue } from 'utils'; import { compose, repeatValue } from 'utils';
const MIN_LINES_NUMBER = 4; const MIN_LINES_NUMBER = 5;
function BillForm({ function BillForm({
//#WithMedia //#WithMedia
@@ -39,10 +41,17 @@ function BillForm({
//#withBillActions //#withBillActions
requestSubmitBill, requestSubmitBill,
requestEditBill, requestEditBill,
setBillNumberChanged,
//#withDashboard //#withDashboard
changePageTitle, changePageTitle,
changePageSubtitle,
// #withBills
nextBillNumberChanged,
// #withSettings
billNextNumber,
billNumberPrefix,
//#withBillDetail //#withBillDetail
bill, bill,
@@ -83,7 +92,6 @@ function BillForm({
} }
}, [changePageTitle, bill, formatMessage]); }, [changePageTitle, bill, formatMessage]);
// @todo abstruct validation schema to sperated file.
const validationSchema = Yup.object().shape({ const validationSchema = Yup.object().shape({
vendor_id: Yup.number() vendor_id: Yup.number()
.required() .required()
@@ -94,11 +102,11 @@ function BillForm({
due_date: Yup.date() due_date: Yup.date()
.required() .required()
.label(formatMessage({ id: 'due_date_' })), .label(formatMessage({ id: 'due_date_' })),
bill_number: Yup.number() bill_number: Yup.string()
.required() .required()
.label(formatMessage({ id: 'bill_number_' })), .label(formatMessage({ id: 'bill_number_' })),
reference_no: Yup.string().min(1).max(255), reference_no: Yup.string().nullable().min(1).max(255),
status: Yup.string().required().nullable(), // status: Yup.string().required().nullable(),
note: Yup.string() note: Yup.string()
.trim() .trim()
.min(1) .min(1)
@@ -128,27 +136,34 @@ function BillForm({
[onFormSubmit], [onFormSubmit],
); );
const defaultBill = useMemo(() => ({ const defaultBill = useMemo(
() => ({
index: 0, index: 0,
item_id: null, item_id: null,
rate: null, rate: null,
discount: 0, discount: 0,
quantity: null, quantity: null,
description: '', description: '',
})); }),
[],
);
const billNumber = billNumberPrefix
? `${billNumberPrefix}-${billNextNumber}`
: billNextNumber;
const defaultInitialValues = useMemo( const defaultInitialValues = useMemo(
() => ({ () => ({
vendor_id: '', vendor_id: '',
bill_number: '', bill_number: billNumber,
bill_date: moment(new Date()).format('YYYY-MM-DD'), bill_date: moment(new Date()).format('YYYY-MM-DD'),
due_date: moment(new Date()).format('YYYY-MM-DD'), due_date: moment(new Date()).format('YYYY-MM-DD'),
status: 'Bill', // status: 'Bill',
reference_no: '', reference_no: '',
note: '', note: '',
entries: [...repeatValue(defaultBill, MIN_LINES_NUMBER)], entries: [...repeatValue(defaultBill, MIN_LINES_NUMBER)],
}), }),
[defaultBill], [defaultBill, billNumber],
); );
const orderingIndex = (_bill) => { const orderingIndex = (_bill) => {
@@ -209,9 +224,12 @@ function BillForm({
requestEditBill(bill.id, requestForm) requestEditBill(bill.id, requestForm)
.then((response) => { .then((response) => {
AppToaster.show({ AppToaster.show({
message: formatMessage({ message: formatMessage(
{
id: 'the_bill_has_been_successfully_edited', id: 'the_bill_has_been_successfully_edited',
}), },
{ number: values.bill_number },
),
intent: Intent.SUCCESS, intent: Intent.SUCCESS,
}); });
setSubmitting(false); setSubmitting(false);
@@ -242,10 +260,16 @@ function BillForm({
}, },
}); });
useEffect(() => {
formik.setFieldValue('bill_number', billNumber);
setBillNumberChanged(false);
}, [nextBillNumberChanged, billNumber]);
const handleSubmitClick = useCallback( const handleSubmitClick = useCallback(
(payload) => { (payload) => {
setPayload(payload); setPayload(payload);
formik.submitForm(); formik.submitForm();
formik.setSubmitting(false);
}, },
[setPayload, formik], [setPayload, formik],
); );
@@ -315,6 +339,7 @@ function BillForm({
formik={formik} formik={formik}
onSubmitClick={handleSubmitClick} onSubmitClick={handleSubmitClick}
bill={bill} bill={bill}
disabled={formik.isSubmitting}
onCancelClick={handleCancelClick} onCancelClick={handleCancelClick}
/> />
</div> </div>
@@ -323,7 +348,12 @@ function BillForm({
export default compose( export default compose(
withBillActions, withBillActions,
withBillDetail(),
withBills(({ nextBillNumberChanged }) => ({ nextBillNumberChanged })),
withDashboardActions, withDashboardActions,
withMediaActions, withMediaActions,
withBillDetail(), withSettings(({ billsettings }) => ({
billNextNumber: billsettings?.next_number,
billNumberPrefix: billsettings?.number_prefix,
})),
)(BillForm); )(BillForm);

View File

@@ -12,6 +12,7 @@ export default function BillFormFooter({
<div className={'form__floating-footer'}> <div className={'form__floating-footer'}>
<Button <Button
disabled={isSubmitting} disabled={isSubmitting}
loading={isSubmitting}
intent={Intent.PRIMARY} intent={Intent.PRIMARY}
type="submit" type="submit"
onClick={() => { onClick={() => {
@@ -23,6 +24,7 @@ export default function BillFormFooter({
<Button <Button
disabled={isSubmitting} disabled={isSubmitting}
loading={isSubmitting}
intent={Intent.PRIMARY} intent={Intent.PRIMARY}
className={'ml1'} className={'ml1'}
name={'save'} name={'save'}

View File

@@ -14,16 +14,17 @@ import moment from 'moment';
import { momentFormatter, compose, tansformDateValue } from 'utils'; import { momentFormatter, compose, tansformDateValue } from 'utils';
import classNames from 'classnames'; import classNames from 'classnames';
import { import {
AccountsSelectList,
ListSelect, ListSelect,
ErrorMessage, ErrorMessage,
FieldRequiredHint, FieldRequiredHint,
Hint, Icon,
InputPrependButton,
} from 'components'; } from 'components';
// import withCustomers from 'containers/Customers/withCustomers'; // import withCustomers from 'containers/Customers/withCustomers';
import withVendors from 'containers/Vendors/withVendors'; import withVendors from 'containers/Vendors/withVendors';
import withAccounts from 'containers/Accounts/withAccounts'; import withAccounts from 'containers/Accounts/withAccounts';
import withDialogActions from 'containers/Dialog/withDialogActions';
function BillFormHeader({ function BillFormHeader({
formik: { errors, touched, setFieldValue, getFieldProps, values }, formik: { errors, touched, setFieldValue, getFieldProps, values },
@@ -33,6 +34,9 @@ function BillFormHeader({
vendorItems, vendorItems,
//#withAccouts //#withAccouts
accountsList, accountsList,
// #withDialog
openDialog,
}) { }) {
const handleDateChange = useCallback( const handleDateChange = useCallback(
(date_filed) => (date) => { (date_filed) => (date) => {
@@ -76,6 +80,10 @@ function BillFormHeader({
} }
}; };
const handleBillNumberChange = useCallback(() => {
openDialog('bill-number-form', {});
}, [openDialog]);
return ( return (
<div className="page-form page-form--bill"> <div className="page-form page-form--bill">
<div className={'page-form__primary-section'}> <div className={'page-form__primary-section'}>
@@ -91,7 +99,7 @@ function BillFormHeader({
} }
> >
<ListSelect <ListSelect
items={vendorsCurrentPage} items={vendorItems}
noResults={<MenuItem disabled={true} text="No results." />} noResults={<MenuItem disabled={true} text="No results." />}
itemRenderer={vendorNameRenderer} itemRenderer={vendorNameRenderer}
itemPredicate={filterVendorAccount} itemPredicate={filterVendorAccount}
@@ -146,7 +154,7 @@ function BillFormHeader({
<FormGroup <FormGroup
label={<T id={'bill_number'} />} label={<T id={'bill_number'} />}
inline={true} inline={true}
className={('form-group--estimate', Classes.FILL)} className={('form-group--bill_number', Classes.FILL)}
labelInfo={<FieldRequiredHint />} labelInfo={<FieldRequiredHint />}
intent={errors.bill_number && touched.bill_number && Intent.DANGER} intent={errors.bill_number && touched.bill_number && Intent.DANGER}
helperText={ helperText={
@@ -156,6 +164,19 @@ function BillFormHeader({
<InputGroup <InputGroup
intent={errors.bill_number && touched.bill_number && Intent.DANGER} intent={errors.bill_number && touched.bill_number && Intent.DANGER}
minimal={true} minimal={true}
rightElement={
<InputPrependButton
buttonProps={{
onClick: handleBillNumberChange,
icon: <Icon icon={'settings-18'} />,
}}
tooltip={true}
tooltipProps={{
content: 'Setting your auto-generated bill number',
position: Position.BOTTOM_LEFT,
}}
/>
}
{...getFieldProps('bill_number')} {...getFieldProps('bill_number')}
/> />
</FormGroup> </FormGroup>
@@ -185,4 +206,5 @@ export default compose(
withAccounts(({ accountsList }) => ({ withAccounts(({ accountsList }) => ({
accountsList, accountsList,
})), })),
withDialogActions,
)(BillFormHeader); )(BillFormHeader);

View File

@@ -9,6 +9,7 @@ import withVendorActions from 'containers/Vendors/withVendorActions';
import withAccountsActions from 'containers/Accounts/withAccountsActions'; import withAccountsActions from 'containers/Accounts/withAccountsActions';
import withItemsActions from 'containers/Items/withItemsActions'; import withItemsActions from 'containers/Items/withItemsActions';
import withBillActions from './withBillActions'; import withBillActions from './withBillActions';
import withSettingsActions from 'containers/Settings/withSettingsActions';
import { compose } from 'utils'; import { compose } from 'utils';
@@ -24,6 +25,9 @@ function Bills({
//# withBilleActions //# withBilleActions
requestFetchBill, requestFetchBill,
// #withSettingsActions
requestFetchOptions,
}) { }) {
const history = useHistory(); const history = useHistory();
const { id } = useParams(); const { id } = useParams();
@@ -41,6 +45,8 @@ function Bills({
// Handle fetch Items data table or list // Handle fetch Items data table or list
const fetchItems = useQuery('items-list', () => requestFetchItems({})); const fetchItems = useQuery('items-list', () => requestFetchItems({}));
const fetchSettings = useQuery(['settings'], () => requestFetchOptions({}));
const handleFormSubmit = useCallback( const handleFormSubmit = useCallback(
(payload) => { (payload) => {
payload.redirect && history.push('/bills'); payload.redirect && history.push('/bills');
@@ -82,4 +88,5 @@ export default compose(
withVendorActions, withVendorActions,
withItemsActions, withItemsActions,
withAccountsActions, withAccountsActions,
withSettingsActions,
)(Bills); )(Bills);

View File

@@ -96,9 +96,13 @@ function BillsDataTable({
const actionMenuList = useCallback( const actionMenuList = useCallback(
(bill) => ( (bill) => (
<Menu> <Menu>
<MenuItem text={formatMessage({ id: 'view_details' })} /> <MenuItem
icon={<Icon icon="reader-18" />}
text={formatMessage({ id: 'view_details' })}
/>
<MenuDivider /> <MenuDivider />
<MenuItem <MenuItem
icon={<Icon icon="pen-18" />}
text={formatMessage({ id: 'edit_bill' })} text={formatMessage({ id: 'edit_bill' })}
onClick={handleEditBill(bill)} onClick={handleEditBill(bill)}
/> />

View File

@@ -11,7 +11,7 @@ import t from 'store/types';
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
requestSubmitBill: (form) => dispatch(submitBill({ form })), requestSubmitBill: (form) => dispatch(submitBill({ form })),
requestFetchBill: (id) => dispatch(fetchBill({ id })), requestFetchBill: (id) => dispatch(fetchBill({ id })),
requestEditBill: (id, form) => dispatch(editBill( id, form )), requestEditBill: (id, form) => dispatch(editBill(id, form)),
requestDeleteBill: (id) => dispatch(deleteBill({ id })), requestDeleteBill: (id) => dispatch(deleteBill({ id })),
requestFetchBillsTable: (query = {}) => requestFetchBillsTable: (query = {}) =>
dispatch(fetchBillsTable({ query: { ...query } })), dispatch(fetchBillsTable({ query: { ...query } })),
@@ -27,6 +27,11 @@ const mapDispatchToProps = (dispatch) => ({
type: t.BILLS_TABLE_QUERIES_ADD, type: t.BILLS_TABLE_QUERIES_ADD,
queries, queries,
}), }),
setBillNumberChanged: (isChanged) =>
dispatch({
type: t.BILL_NUMBER_CHANGED,
payload: { isChanged },
}),
}); });
export default connect(null, mapDispatchToProps); export default connect(null, mapDispatchToProps);

View File

@@ -20,9 +20,9 @@ export default (mapState) => {
billsItems: state.bills.items, billsItems: state.bills.items,
billsTableQuery: tableQuery, billsTableQuery: tableQuery,
// @todo un-unncessery shit.
billsPageination: getBillsPaginationMeta(state, props, tableQuery), billsPageination: getBillsPaginationMeta(state, props, tableQuery),
billsLoading: state.bills.loading, billsLoading: state.bills.loading,
nextBillNumberChanged: state.bills.nextBillNumberChanged,
}; };
return mapState ? mapState(mapped, state, props) : mapped; return mapState ? mapState(mapped, state, props) : mapped;
}; };

View File

@@ -5,9 +5,10 @@ export default (mapState) => {
const mapped = { const mapped = {
organizationSettings: state.settings.data.organization, organizationSettings: state.settings.data.organization,
manualJournalsSettings: state.settings.data.manual_journals, manualJournalsSettings: state.settings.data.manual_journals,
billsettings: state.settings.data.bills,
}; };
return mapState ? mapState(mapped, state, props) : mapped; return mapState ? mapState(mapped, state, props) : mapped;
}; };
return connect(mapStateToProps); return connect(mapStateToProps);
} };

View File

@@ -16,7 +16,7 @@ export default (mapState) => {
const mapped = { const mapped = {
vendorsCurrentPage: getVendorsItems(state, props, query), vendorsCurrentPage: getVendorsItems(state, props, query),
vendorViews: getResourceViews(state, props, 'vendors'), vendorViews: getResourceViews(state, props, 'vendors'),
vendorItems: state.vendors.items, vendorItems: Object.values(state.vendors.items),
vendorTableQuery: query, vendorTableQuery: query,
vendorsPageination: getVendorsPaginationMeta(state, props, query), vendorsPageination: getVendorsPaginationMeta(state, props, query),
vendorsLoading: state.vendors.loading, vendorsLoading: state.vendors.loading,

View File

@@ -777,4 +777,5 @@ export default {
prefix: 'Prefix', prefix: 'Prefix',
next_number: 'Next Number', next_number: 'Next Number',
journal_number_settings: 'Journal number settings', journal_number_settings: 'Journal number settings',
bill_number_settings: 'Bill number settings',
}; };

View File

@@ -19,21 +19,21 @@ export const fetchBillsTable = ({ query = {} }) => {
dispatch({ dispatch({
type: t.BILLS_PAGE_SET, type: t.BILLS_PAGE_SET,
payload: { payload: {
bills: response.data.bills.results, bills: response.data.bills,
pagination: response.data.bills.pagination, pagination: response.data.pagination,
customViewId: response.data.customViewId || -1, customViewId: response.data.customViewId || -1,
}, },
}); });
dispatch({ dispatch({
type: t.BILLS_ITEMS_SET, type: t.BILLS_ITEMS_SET,
payload: { payload: {
bills: response.data.bills.results, bills: response.data.bills,
}, },
}); });
dispatch({ dispatch({
type: t.BILLS_PAGINATION_SET, type: t.BILLS_PAGINATION_SET,
payload: { payload: {
pagination: response.data.bills.pagination, pagination: response.data.pagination,
customViewId: response.data.customViewId || -1, customViewId: response.data.customViewId || -1,
}, },
}); });
@@ -66,7 +66,19 @@ export const deleteBill = ({ id }) => {
}; };
export const submitBill = ({ form }) => { export const submitBill = ({ form }) => {
return (dispatch) => ApiService.post('purchases/bills', form); return (dispatch) =>
new Promise((resolve, reject) => {
ApiService.post('purchases/bills', form)
.then((response) => {
resolve(response);
})
.catch((error) => {
const { response } = error;
const { data } = response;
reject(data?.errors);
});
});
}; };
export const fetchBill = ({ id }) => { export const fetchBill = ({ id }) => {
@@ -91,5 +103,17 @@ export const fetchBill = ({ id }) => {
}; };
export const editBill = (id, form) => { export const editBill = (id, form) => {
return (dispatch) => ApiService.post(`purchases/bills/${id}`, form); return (dispatch) =>
new Promise((resolve, reject) => {
ApiService.post(`purchases/bills/${id}`, form)
.then((response) => {
resolve(response);
})
.catch((error) => {
const { response } = error;
const { data } = response;
reject(data?.errors);
});
});
}; };

View File

@@ -12,6 +12,7 @@ const initialState = {
page_size: 5, page_size: 5,
page: 1, page: 1,
}, },
nextBillNumberChanged: false,
}; };
const defaultBill = { const defaultBill = {
@@ -98,6 +99,11 @@ const reducer = createReducer(initialState, {
}, },
}; };
}, },
[t.BILL_NUMBER_CHANGED]: (state, action) => {
const { isChanged } = action.payload;
state.nextBillNumberChanged = isChanged;
},
}); });
export default createTableQueryReducers('bills', reducer); export default createTableQueryReducers('bills', reducer);

View File

@@ -9,4 +9,5 @@ export default {
BILLS_PAGINATION_SET: 'BILLS_PAGINATION_SET', BILLS_PAGINATION_SET: 'BILLS_PAGINATION_SET',
BILLS_PAGE_SET: 'BILLS_PAGE_SET', BILLS_PAGE_SET: 'BILLS_PAGE_SET',
BILLS_ITEMS_SET: 'BILLS_ITEMS_SET', BILLS_ITEMS_SET: 'BILLS_ITEMS_SET',
BILL_NUMBER_CHANGED: 'BILL_NUMBER_CHANGED',
}; };

View File

@@ -17,7 +17,7 @@ export const fetchVendorsTable = ({ query }) => {
payload: { payload: {
vendors: response.data.vendors, vendors: response.data.vendors,
pagination: response.data.pagination, pagination: response.data.pagination,
customViewId: response.data.customViewId, customViewId: response.data.customViewId || -1,
}, },
}); });
dispatch({ dispatch({

View File

@@ -1,58 +1,66 @@
export default { export default {
organization: [ organization: [
{ {
key: 'name', key: "name",
type: 'string', type: "string",
config: true, config: true,
}, },
{ {
key: 'base_currency', key: "base_currency",
type: 'string', type: "string",
config: true, config: true,
}, },
{ {
key: 'industry', key: "industry",
type: 'string', type: "string",
}, },
{ {
key: 'location', key: "location",
type: 'string', type: "string",
}, },
{ {
key: 'fiscal_year', key: "fiscal_year",
type: 'string', type: "string",
// config: true, // config: true,
}, },
{ {
key: 'financial_date_start', key: "financial_date_start",
type: 'string', type: "string",
}, },
{ {
key: 'language', key: "language",
type: 'string', type: "string",
config: true, config: true,
}, },
{ {
key: 'time_zone', key: "time_zone",
type: 'string', type: "string",
// config: true, // config: true,
}, },
{ {
key: 'date_format', key: "date_format",
type: 'string', type: "string",
// config: true, // config: true,
}, },
], ],
manual_journals: [ manual_journals: [
{ {
key: 'next_number', key: "next_number",
type: 'number', type: "number",
}, },
{ {
key: 'number_prefix', key: "number_prefix",
type: 'string', type: "string",
}, },
] ],
bills: [
{
key: "next_number",
type: "number",
},
{
key: "number_prefix",
type: "string",
},
],
}; };