From aa8ba546aadeb43964251379731727836ea8b0b9 Mon Sep 17 00:00:00 2001 From: elforjani3 Date: Tue, 2 Mar 2021 11:45:20 +0200 Subject: [PATCH] feat: convert estimate to invoice. --- client/src/common/errors.js | 2 ++ .../ExchangeRateFormDialogContent.js | 4 +-- .../EstimatesLanding/EstimatesDataTable.js | 8 ++++- .../Estimates/EstimatesLanding/components.js | 7 ++++- .../Sales/Invoices/InvoiceForm/InvoiceForm.js | 20 +++++------- .../InvoiceForm/InvoiceForm.schema.js | 4 ++- .../InvoiceForm/InvoiceFormProvider.js | 31 +++++++++++++++---- .../Sales/Invoices/InvoiceForm/utils.js | 26 ++++++++++++++++ client/src/lang/en/index.js | 10 +++--- client/src/routes/dashboard.js | 11 +++++++ client/src/static/json/icons.js | 28 ++++++++++------- 11 files changed, 111 insertions(+), 40 deletions(-) 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', + }, };