diff --git a/client/src/common/errors.js b/client/src/common/errors.js
index 34db1734c..14f501a90 100644
--- a/client/src/common/errors.js
+++ b/client/src/common/errors.js
@@ -5,6 +5,8 @@ export const ERROR = {
// Sales Invoices
SALE_INVOICE_NUMBER_IS_EXISTS: 'SALE.INVOICE.NUMBER.IS.EXISTS',
SALE_INVOICE_NO_NOT_UNIQUE: 'SALE_INVOICE_NO_NOT_UNIQUE',
+ SALE_ESTIMATE_IS_ALREADY_CONVERTED_TO_INVOICE:
+ 'SALE_ESTIMATE_IS_ALREADY_CONVERTED_TO_INVOICE',
// Sales Receipts
SALE_RECEIPT_NUMBER_NOT_UNIQUE: 'SALE_RECEIPT_NUMBER_NOT_UNIQUE',
diff --git a/client/src/containers/Dialogs/ExchangeRateFormDialog/ExchangeRateFormDialogContent.js b/client/src/containers/Dialogs/ExchangeRateFormDialog/ExchangeRateFormDialogContent.js
index ad2a39e69..88e260f38 100644
--- a/client/src/containers/Dialogs/ExchangeRateFormDialog/ExchangeRateFormDialogContent.js
+++ b/client/src/containers/Dialogs/ExchangeRateFormDialog/ExchangeRateFormDialogContent.js
@@ -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({
);
}
-
-export default compose(withExchangeRateDetail)(ExchangeRateFormDialogContent);
diff --git a/client/src/containers/Sales/Estimates/EstimatesLanding/EstimatesDataTable.js b/client/src/containers/Sales/Estimates/EstimatesLanding/EstimatesDataTable.js
index 339d61800..0913d4050 100644
--- a/client/src/containers/Sales/Estimates/EstimatesLanding/EstimatesDataTable.js
+++ b/client/src/containers/Sales/Estimates/EstimatesLanding/EstimatesDataTable.js
@@ -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,
}}
/>
);
diff --git a/client/src/containers/Sales/Estimates/EstimatesLanding/components.js b/client/src/containers/Sales/Estimates/EstimatesLanding/components.js
index 9a391bc34..afb0870c3 100644
--- a/client/src/containers/Sales/Estimates/EstimatesLanding/components.js
+++ b/client/src/containers/Sales/Estimates/EstimatesLanding/components.js
@@ -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)}
/>
+ }
+ text={formatMessage({ id: 'convert_to_invoice' })}
+ onClick={safeCallback(onConvert, original)}
+ />
}
diff --git a/client/src/containers/Sales/Invoices/InvoiceForm/InvoiceForm.js b/client/src/containers/Sales/Invoices/InvoiceForm/InvoiceForm.js
index e0439be35..b58e11a22 100644
--- a/client/src/containers/Sales/Invoices/InvoiceForm/InvoiceForm.js
+++ b/client/src/containers/Sales/Invoices/InvoiceForm/InvoiceForm.js
@@ -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);
};
diff --git a/client/src/containers/Sales/Invoices/InvoiceForm/InvoiceForm.schema.js b/client/src/containers/Sales/Invoices/InvoiceForm/InvoiceForm.schema.js
index 9f16449ff..41c646b3c 100644
--- a/client/src/containers/Sales/Invoices/InvoiceForm/InvoiceForm.schema.js
+++ b/client/src/containers/Sales/Invoices/InvoiceForm/InvoiceForm.schema.js
@@ -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(),
diff --git a/client/src/containers/Sales/Invoices/InvoiceForm/InvoiceFormProvider.js b/client/src/containers/Sales/Invoices/InvoiceForm/InvoiceFormProvider.js
index 4ac8e43b5..f873ee6dc 100644
--- a/client/src/containers/Sales/Invoices/InvoiceForm/InvoiceFormProvider.js
+++ b/client/src/containers/Sales/Invoices/InvoiceForm/InvoiceFormProvider.js
@@ -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'}
diff --git a/client/src/containers/Sales/Invoices/InvoiceForm/utils.js b/client/src/containers/Sales/Invoices/InvoiceForm/utils.js
index a3b418261..14b1c5008 100644
--- a/client/src/containers/Sales/Invoices/InvoiceForm/utils.js
+++ b/client/src/containers/Sales/Invoices/InvoiceForm/utils.js
@@ -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,
+ });
+ }
+};
diff --git a/client/src/lang/en/index.js b/client/src/lang/en/index.js
index 2413e08d2..8dc555168 100644
--- a/client/src/lang/en/index.js
+++ b/client/src/lang/en/index.js
@@ -984,8 +984,10 @@ export default {
financial_accounting: ' Financial Accounting',
products_services_inventory: 'Products,Services & Inventory',
payable_a_p: 'Payable A/P',
- keyboard_shortcuts:'Keyboard Shortcuts',
- shortcut_keys:'Shortcut Keys',
- oK_:'Ok'
-
+ keyboard_shortcuts: 'Keyboard Shortcuts',
+ shortcut_keys: 'Shortcut Keys',
+ oK_: 'Ok',
+ convert_to_invoice: 'Convert to Invoice',
+ sale_estimate_is_already_converted_to_invoice:
+ 'Sale estimate is already converted to invoice.',
};
diff --git a/client/src/routes/dashboard.js b/client/src/routes/dashboard.js
index 1a8ce6849..7ea5bb2aa 100644
--- a/client/src/routes/dashboard.js
+++ b/client/src/routes/dashboard.js
@@ -299,6 +299,17 @@ export default [
backLink: true,
sidebarShrink: true,
},
+ {
+ path: `/invoices/new?from_estimate_id=/:id`,
+ component: lazy(() =>
+ import('containers/Sales/Estimates/EstimateForm/EstimateFormPage'),
+ ),
+ name: 'convert-to-invoice',
+ breadcrumb: 'New Estimate',
+ pageTitle: formatMessage({ id: 'new_estimate' }),
+ backLink: true,
+ sidebarShrink: true,
+ },
{
path: `/estimates/new`,
component: lazy(() =>
diff --git a/client/src/static/json/icons.js b/client/src/static/json/icons.js
index 466d05dc9..86a0d3ed4 100644
--- a/client/src/static/json/icons.js
+++ b/client/src/static/json/icons.js
@@ -366,30 +366,28 @@ export default {
path: [
'M10 20C4.48 20 0 15.52 0 10S4.48 0 10 0s10 4.48 10 10-4.48 10-10 10zm5-14c-.28 0-.53.11-.71.29L8 12.59l-2.29-2.3a1.003 1.003 0 00-1.42 1.42l3 3c.18.18.43.29.71.29.28 0 .53-.11.71-.29l7-7A1.003 1.003 0 0015 6z',
],
- viewBox: '0 0 20 20'
+ viewBox: '0 0 20 20',
},
'swap-vert': {
path: [
'M10.6,10.9V5.4H9v5.5H6.7L9.8,14l3.1-3.1ZM5.1,0,2,3.1H4.3V8.6H5.9V3.1H8.2Z',
],
- viewBox: '0 0 14 14'
+ viewBox: '0 0 14 14',
},
- "check": {
+ check: {
path: [
- 'M17 4c-.28 0-.53.11-.71.29L7 13.59 3.71 10.3A.965.965 0 003 10a1.003 1.003 0 00-.71 1.71l4 4c.18.18.43.29.71.29s.53-.11.71-.29l10-10A1.003 1.003 0 0017 4z'
+ 'M17 4c-.28 0-.53.11-.71.29L7 13.59 3.71 10.3A.965.965 0 003 10a1.003 1.003 0 00-.71 1.71l4 4c.18.18.43.29.71.29s.53-.11.71-.29l10-10A1.003 1.003 0 0017 4z',
],
viewBox: '0 0 24 24',
},
- "close-black": {
+ 'close-black': {
path: [
'M11.41 10l4.29-4.29c.19-.18.3-.43.3-.71a1.003 1.003 0 00-1.71-.71L10 8.59l-4.29-4.3a1.003 1.003 0 00-1.42 1.42L8.59 10 4.3 14.29c-.19.18-.3.43-.3.71a1.003 1.003 0 001.71.71l4.29-4.3 4.29 4.29c.18.19.43.3.71.3a1.003 1.003 0 00.71-1.71L11.41 10z',
],
viewBox: '0 0 20 20',
},
- "send": {
- path: [
- 'M2.01 21L23 12 2.01 3 2 10l15 2-15 2z'
- ],
+ send: {
+ path: ['M2.01 21L23 12 2.01 3 2 10l15 2-15 2z'],
viewBox: '0 0 24 24',
},
'arrow-top-right': {
@@ -397,7 +395,15 @@ export default {
viewBox: '0 0 24 24',
},
'receipt-24': {
- path: ['M19.5 3.5L18 2l-1.5 1.5L15 2l-1.5 1.5L12 2l-1.5 1.5L9 2 7.5 3.5 6 2 4.5 3.5 3 2v20l1.5-1.5L6 22l1.5-1.5L9 22l1.5-1.5L12 22l1.5-1.5L15 22l1.5-1.5L18 22l1.5-1.5L21 22V2l-1.5 1.5zM19 19.09H5V4.91h14v14.18zM6 15h12v2H6zm0-4h12v2H6zm0-4h12v2H6z'],
+ path: [
+ 'M19.5 3.5L18 2l-1.5 1.5L15 2l-1.5 1.5L12 2l-1.5 1.5L9 2 7.5 3.5 6 2 4.5 3.5 3 2v20l1.5-1.5L6 22l1.5-1.5L9 22l1.5-1.5L12 22l1.5-1.5L15 22l1.5-1.5L18 22l1.5-1.5L21 22V2l-1.5 1.5zM19 19.09H5V4.91h14v14.18zM6 15h12v2H6zm0-4h12v2H6zm0-4h12v2H6z',
+ ],
viewBox: '0 0 24 24',
- }
+ },
+ convert_to: {
+ path: [
+ 'M19 8l-4 4h3c0 3.31-2.69 6-6 6-1.01 0-1.97-.25-2.8-.7l-1.46 1.46C8.97 19.54 10.43 20 12 20c4.42 0 8-3.58 8-8h3l-4-4zM6 12c0-3.31 2.69-6 6-6 1.01 0 1.97.25 2.8.7l1.46-1.46C15.03 4.46 13.57 4 12 4c-4.42 0-8 3.58-8 8H1l4 4 4-4H6z',
+ ],
+ viewBox: '0 0 24 24',
+ },
};