mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-19 06:10:31 +00:00
feat: convert estimate to invoice.
This commit is contained in:
@@ -11,7 +11,7 @@ import 'style/pages/ExchangeRate/ExchangeRateDialog.scss';
|
||||
/**
|
||||
* Exchange rate form content.
|
||||
*/
|
||||
function ExchangeRateFormDialogContent({
|
||||
export default function ExchangeRateFormDialogContent({
|
||||
// #ownProp
|
||||
action,
|
||||
exchangeRateId,
|
||||
@@ -27,5 +27,3 @@ function ExchangeRateFormDialogContent({
|
||||
</ExchangeRateFormProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default compose(withExchangeRateDetail)(ExchangeRateFormDialogContent);
|
||||
|
||||
@@ -66,12 +66,17 @@ function EstimatesDataTable({
|
||||
const handleRejectEstimate = ({ id }) => {
|
||||
openAlert('estimate-reject', { estimateId: id });
|
||||
};
|
||||
|
||||
|
||||
// Handle drawer estimate.
|
||||
const handleDrawerEstimate = () => {
|
||||
openDrawer('estimate-drawer', {});
|
||||
};
|
||||
|
||||
// Handle convent to invoice.
|
||||
const handleConvertToInvoice = ({ id }) => {
|
||||
history.push(`/invoices/new?from_estimate_id=${id}`, { action: id });
|
||||
};
|
||||
|
||||
// Handles fetch data.
|
||||
const handleFetchData = useCallback(
|
||||
({ pageIndex, pageSize, sortBy }) => {
|
||||
@@ -114,6 +119,7 @@ function EstimatesDataTable({
|
||||
onDeliver: handleDeliverEstimate,
|
||||
onDelete: handleDeleteEstimate,
|
||||
onDrawer: handleDrawerEstimate,
|
||||
onConvert: handleConvertToInvoice,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -49,7 +49,7 @@ export const statusAccessor = (row) => (
|
||||
*/
|
||||
export function ActionsMenu({
|
||||
row: { original },
|
||||
payload: { onEdit, onDeliver, onReject, onApprove, onDelete, onDrawer },
|
||||
payload: { onEdit, onDeliver, onReject, onApprove, onDelete, onDrawer ,onConvert },
|
||||
}) {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
@@ -65,6 +65,11 @@ export function ActionsMenu({
|
||||
text={formatMessage({ id: 'edit_estimate' })}
|
||||
onClick={safeCallback(onEdit, original)}
|
||||
/>
|
||||
<MenuItem
|
||||
icon={<Icon icon="convert_to" />}
|
||||
text={formatMessage({ id: 'convert_to_invoice' })}
|
||||
onClick={safeCallback(onConvert, original)}
|
||||
/>
|
||||
<If condition={!original.is_delivered}>
|
||||
<MenuItem
|
||||
icon={<Icon icon={'check'} iconSize={18} />}
|
||||
|
||||
@@ -22,10 +22,9 @@ import withMediaActions from 'containers/Media/withMediaActions';
|
||||
import withSettings from 'containers/Settings/withSettings';
|
||||
|
||||
import { AppToaster } from 'components';
|
||||
import { ERROR } from 'common/errors';
|
||||
import { compose, orderingLinesIndexes, transactionNumber } from 'utils';
|
||||
import { useInvoiceFormContext } from './InvoiceFormProvider';
|
||||
import { transformToEditForm, defaultInvoice } from './utils';
|
||||
import { transformToEditForm, defaultInvoice, transformErrors } from './utils';
|
||||
|
||||
/**
|
||||
* Invoice form.
|
||||
@@ -42,6 +41,8 @@ function InvoiceForm({
|
||||
const {
|
||||
isNewMode,
|
||||
invoice,
|
||||
estimateId,
|
||||
newInvoice,
|
||||
createInvoiceMutate,
|
||||
editInvoiceMutate,
|
||||
submitPayload,
|
||||
@@ -62,20 +63,12 @@ function InvoiceForm({
|
||||
...defaultInvoice,
|
||||
invoice_no: invoiceNumber,
|
||||
entries: orderingLinesIndexes(defaultInvoice.entries),
|
||||
...newInvoice,
|
||||
}),
|
||||
}),
|
||||
[invoice, invoiceNumber],
|
||||
[invoice, newInvoice,invoiceNumber],
|
||||
);
|
||||
|
||||
// Handle form errors.
|
||||
const handleErrors = (errors, { setErrors }) => {
|
||||
if (errors.some((e) => e.type === ERROR.SALE_INVOICE_NUMBER_IS_EXISTS)) {
|
||||
setErrors({
|
||||
invoice_no: formatMessage({ id: 'sale_invoice_number_is_exists' }),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Handles form submit.
|
||||
const handleSubmit = (values, { setSubmitting, setErrors, resetForm }) => {
|
||||
setSubmitting(true);
|
||||
@@ -97,6 +90,7 @@ function InvoiceForm({
|
||||
const form = {
|
||||
...values,
|
||||
delivered: submitPayload.deliver,
|
||||
from_estimate_id: estimateId,
|
||||
entries: entries.map((entry) => ({ ...omit(entry, ['total']) })),
|
||||
};
|
||||
// Handle the request success.
|
||||
@@ -129,7 +123,7 @@ function InvoiceForm({
|
||||
},
|
||||
}) => {
|
||||
if (errors) {
|
||||
handleErrors(errors, { setErrors });
|
||||
transformErrors(errors, { setErrors });
|
||||
}
|
||||
setSubmitting(false);
|
||||
};
|
||||
|
||||
@@ -18,6 +18,7 @@ const Schema = Yup.object().shape({
|
||||
.label(formatMessage({ id: 'invoice_no_' })),
|
||||
reference_no: Yup.string().min(1).max(DATATYPES_LENGTH.STRING),
|
||||
delivered: Yup.boolean(),
|
||||
from_estimate_id: Yup.string(),
|
||||
invoice_message: Yup.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
@@ -31,7 +32,8 @@ const Schema = Yup.object().shape({
|
||||
entries: Yup.array().of(
|
||||
Yup.object().shape({
|
||||
quantity: Yup.number()
|
||||
.nullable().max(DATATYPES_LENGTH.INT_10)
|
||||
.nullable()
|
||||
.max(DATATYPES_LENGTH.INT_10)
|
||||
.when(['rate'], {
|
||||
is: (rate) => rate,
|
||||
then: Yup.number().required(),
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import React, { createContext, useState } from 'react';
|
||||
import { isEmpty, pick } from 'lodash';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import DashboardInsider from 'components/Dashboard/DashboardInsider';
|
||||
import { transformToEditForm } from './utils';
|
||||
import {
|
||||
useInvoice,
|
||||
useItems,
|
||||
@@ -7,6 +10,7 @@ import {
|
||||
useCreateInvoice,
|
||||
useEditInvoice,
|
||||
useSettingsInvoices,
|
||||
useEstimate,
|
||||
} from 'hooks/query';
|
||||
|
||||
const InvoiceFormContext = createContext();
|
||||
@@ -15,12 +19,24 @@ const InvoiceFormContext = createContext();
|
||||
* Accounts chart data provider.
|
||||
*/
|
||||
function InvoiceFormProvider({ invoiceId, ...props }) {
|
||||
const { data: invoice, isLoading: isInvoiceLoading } = useInvoice(
|
||||
invoiceId,
|
||||
{
|
||||
enabled: !!invoiceId,
|
||||
},
|
||||
);
|
||||
const { state } = useLocation();
|
||||
|
||||
const estimateId = state?.action;
|
||||
|
||||
const { data: invoice, isLoading: isInvoiceLoading } = useInvoice(invoiceId, {
|
||||
enabled: !!invoiceId,
|
||||
});
|
||||
|
||||
const {
|
||||
data: estimate,
|
||||
isFetching: isEstimateFetching,
|
||||
} = useEstimate(estimateId, { enabled: !!estimateId });
|
||||
|
||||
const newInvoice = !isEmpty(estimate)
|
||||
? transformToEditForm({
|
||||
...pick(estimate, ['customer_id', 'customer', 'entries']),
|
||||
})
|
||||
: [];
|
||||
|
||||
// Handle fetching the items table based on the given query.
|
||||
const {
|
||||
@@ -52,6 +68,8 @@ function InvoiceFormProvider({ invoiceId, ...props }) {
|
||||
invoice,
|
||||
items,
|
||||
customers,
|
||||
newInvoice,
|
||||
estimateId,
|
||||
submitPayload,
|
||||
|
||||
isInvoiceLoading,
|
||||
@@ -71,6 +89,7 @@ function InvoiceFormProvider({ invoiceId, ...props }) {
|
||||
isInvoiceLoading ||
|
||||
isItemsLoading ||
|
||||
isCustomersLoading ||
|
||||
isEstimateFetching ||
|
||||
isSettingsLoading
|
||||
}
|
||||
name={'invoice-form'}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import moment from 'moment';
|
||||
import { compose, transformToForm, repeatValue } from 'utils';
|
||||
import { updateItemsEntriesTotal } from 'containers/Entries/utils';
|
||||
import { ERROR } from 'common/errors';
|
||||
|
||||
import { Intent } from '@blueprintjs/core';
|
||||
import { formatMessage } from 'services/intl';
|
||||
import { AppToaster } from 'components';
|
||||
|
||||
export const MIN_LINES_NUMBER = 4;
|
||||
|
||||
@@ -47,3 +52,24 @@ export function transformToEditForm(invoice) {
|
||||
entries,
|
||||
};
|
||||
}
|
||||
|
||||
export const transformErrors = (errors, { setErrors }) => {
|
||||
if (errors.some((e) => e.type === ERROR.SALE_INVOICE_NUMBER_IS_EXISTS)) {
|
||||
setErrors({
|
||||
invoice_no: formatMessage({ id: 'sale_invoice_number_is_exists' }),
|
||||
});
|
||||
}
|
||||
if (
|
||||
errors.some(
|
||||
({ type }) =>
|
||||
type === ERROR.SALE_ESTIMATE_IS_ALREADY_CONVERTED_TO_INVOICE,
|
||||
)
|
||||
) {
|
||||
AppToaster.show({
|
||||
message: formatMessage({
|
||||
id: 'sale_estimate_is_already_converted_to_invoice',
|
||||
}),
|
||||
intent: Intent.DANGER,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user