From d027e3dc67c675be51d00710db10210de746bbe1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Szymkiewicz?= Date: Tue, 23 Apr 2024 19:40:28 +0200 Subject: [PATCH] Feature: "forced invoice number" enabled --- README.md | 8 +- package.json | 2 +- .../admin/document-invoice-settings/route.ts | 30 ++++ src/api/admin/invoice/display-number/route.ts | 39 +++++ src/api/admin/invoice/generate/route.ts | 2 +- src/services/document-invoice-settings.ts | 55 ++++-- src/services/invoice.ts | 21 ++- .../settings-invoice-display-number.tsx | 59 +++++++ .../settings/settings-invoice.tsx | 156 +++++++++++++----- src/ui-components/types/api.ts | 6 + 10 files changed, 311 insertions(+), 67 deletions(-) create mode 100644 src/api/admin/invoice/display-number/route.ts create mode 100644 src/ui-components/settings/settings-invoice-display-number.tsx diff --git a/README.md b/README.md index b6422ff..765d8b4 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,9 @@ By default, invoice number is generated based on the last assigned invoice numbe We know that your businesss may require different numbering. In such case - go to `Settings` tab and click `Change settings` in `Invoice`. You will see that you can change how your invoice number will look like. For instance, you can make something like `ABC123{invoice_number}`. If your last invoice has base number `10`, then you will get `ABC12311` as your next invoice number. -Protip: After setting change, you can always go to `Templates` to see a preview with invoice number. +Sometimes you may want to set your next invoice number (for instance when you have many different clients). You can do it by setting `Forced number` in `Settings`. Please remember that this setting will be applied for newly generated invoice and the incrementation will start over from this new number. + +Protip: After setting change, you can always go to `Templates` to see a preview with your next invoice number. ## Q&A @@ -106,9 +108,7 @@ Anyway, we encourage you to save your invoice when you generate. ### I clicked generate invoice, invoice number has been assigned, but I want to go back to previous number -In short - you cannot (at this moment). If you generate invoice, we are taking the next number always based on the last generated invoice (remember: we are incrementing base number). The only ultimate workaround here is to revert migrations and run them again - please remember it will remove your data! - -However, this functionality is one of the highest priority task on the roadmap. +Now you can do it! Just got to `Settings` tab and click `Change settings` in `Invoice`. Use `Forced number` field to put your next invoice number. ### Provided templates are not enough for me, I want more of them, I want customization, I want hide some information etc. diff --git a/package.json b/package.json index b40bd96..80cec4e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@rsc-labs/medusa-documents", - "version": "0.5.0", + "version": "0.6.0", "description": "Generate documents from Medusa", "main": "dist/index.js", "author": "RSC Labs (https://rsoftcon.com)", diff --git a/src/api/admin/document-invoice-settings/route.ts b/src/api/admin/document-invoice-settings/route.ts index e5aff58..086432d 100644 --- a/src/api/admin/document-invoice-settings/route.ts +++ b/src/api/admin/document-invoice-settings/route.ts @@ -16,6 +16,7 @@ import type { } from "@medusajs/medusa" import DocumentInvoiceSettingsService from "../../../services/document-invoice-settings"; import { DocumentInvoiceSettings } from "../../..//models/document-invoice-settings"; +import { TemplateKind } from "../../../services/types/template-kind"; export const GET = async ( req: MedusaRequest, @@ -35,4 +36,33 @@ export const GET = async ( message: e.message }) } +} + +export const POST = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + + const documentInvoiceSettingsService: DocumentInvoiceSettingsService = req.scope.resolve('documentInvoiceSettingsService'); + const formatNumber: string | undefined = req.body.formatNumber; + const forcedNumber: string | undefined = req.body.forcedNumber; + const invoiceTemplate: string | undefined = req.body.template; + + try { + const newSettings: DocumentInvoiceSettings = await documentInvoiceSettingsService.updateSettings(formatNumber, forcedNumber, invoiceTemplate as TemplateKind); + if (newSettings !== undefined) { + res.status(201).json({ + settings: newSettings + }); + } else { + res.status(400).json({ + message: 'Cant update invoice settings' + }) + } + + } catch (e) { + res.status(400).json({ + message: e.message + }) + } } \ No newline at end of file diff --git a/src/api/admin/invoice/display-number/route.ts b/src/api/admin/invoice/display-number/route.ts new file mode 100644 index 0000000..fd43231 --- /dev/null +++ b/src/api/admin/invoice/display-number/route.ts @@ -0,0 +1,39 @@ +/* + * Copyright 2024 RSC-Labs, https://rsoftcon.com/ + * + * MIT License + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { + MedusaRequest, + MedusaResponse, +} from "@medusajs/medusa" +import InvoiceService from "../../../../services/invoice"; + +export const GET = async ( + req: MedusaRequest, + res: MedusaResponse +) => { + + const invoiceService: InvoiceService = req.scope.resolve('invoiceService'); + + const formatNumber: string | undefined = req.query.formatNumber as string; + const forcedNumber: string | undefined = req.query.forcedNumber as string; + + try { + const nextDisplayNumber = await invoiceService.getTestDisplayNumber(formatNumber, forcedNumber); + res.status(201).json({ + displayNumber: nextDisplayNumber + }) + } catch (e) { + res.status(400).json({ + message: e.message + }) + } +} \ No newline at end of file diff --git a/src/api/admin/invoice/generate/route.ts b/src/api/admin/invoice/generate/route.ts index 2cac78c..36bcafc 100644 --- a/src/api/admin/invoice/generate/route.ts +++ b/src/api/admin/invoice/generate/route.ts @@ -14,7 +14,7 @@ import type { MedusaRequest, MedusaResponse, } from "@medusajs/medusa" -import { TemplateKind } from "../../../..//services/types/template-kind"; +import { TemplateKind } from "../../../../services/types/template-kind"; import InvoiceService from "../../../../services/invoice"; export const GET = async ( diff --git a/src/services/document-invoice-settings.ts b/src/services/document-invoice-settings.ts index 882918a..28a845f 100644 --- a/src/services/document-invoice-settings.ts +++ b/src/services/document-invoice-settings.ts @@ -13,7 +13,6 @@ import { TransactionBaseService } from "@medusajs/medusa" import { DocumentInvoiceSettings } from "../models/document-invoice-settings"; import { MedusaError } from "@medusajs/utils" -import { isNumber } from "lodash"; import { TemplateKind } from "./types/template-kind"; export default class DocumentInvoiceSettingsService extends TransactionBaseService { @@ -26,32 +25,34 @@ export default class DocumentInvoiceSettingsService extends TransactionBaseServi } } - async getLastDocumentInvoiceSettings() : Promise | undefined { - const documentInvoiceSettingsRepository = this.activeManager_.getRepository(DocumentInvoiceSettings); - const lastDocumentInvoiceSettings = await documentInvoiceSettingsRepository.createQueryBuilder('documentInvoiceSettings') - .orderBy('documentInvoiceSettings.created_at', 'DESC') - .getOne() - - return lastDocumentInvoiceSettings; + async getInvoiceForcedNumber() : Promise | undefined { + const lastDocumentInvoiceSettings = await this.getLastDocumentInvoiceSettings(); + if (lastDocumentInvoiceSettings && lastDocumentInvoiceSettings.invoice_forced_number) { + const nextNumber: string = lastDocumentInvoiceSettings.invoice_forced_number.toString(); + return nextNumber; + } + return undefined; } - async resetInvoiceSettingsByCreatingNewOne() : Promise { + async resetForcedNumberByCreatingNewSettings() : Promise { const documentInvoiceSettingsRepository = this.activeManager_.getRepository(DocumentInvoiceSettings); const newDocumentInvoiceSettings = this.activeManager_.create(DocumentInvoiceSettings); const lastDocumentInvoiceSettings = await this.getLastDocumentInvoiceSettings(); this.copySettingsIfPossible(newDocumentInvoiceSettings, lastDocumentInvoiceSettings); + + newDocumentInvoiceSettings.invoice_forced_number = undefined; + const result = await documentInvoiceSettingsRepository.save(newDocumentInvoiceSettings); return result; } - async getInvoiceForcedNumber() : Promise | undefined { - const lastDocumentInvoiceSettings = await this.getLastDocumentInvoiceSettings(); - if (lastDocumentInvoiceSettings && lastDocumentInvoiceSettings.invoice_forced_number) { - const nextNumber: string = lastDocumentInvoiceSettings.invoice_forced_number.toString(); - await this.resetInvoiceSettingsByCreatingNewOne(); - return nextNumber; - } - return undefined; + async getLastDocumentInvoiceSettings() : Promise | undefined { + const documentInvoiceSettingsRepository = this.activeManager_.getRepository(DocumentInvoiceSettings); + const lastDocumentInvoiceSettings = await documentInvoiceSettingsRepository.createQueryBuilder('documentInvoiceSettings') + .orderBy('documentInvoiceSettings.created_at', 'DESC') + .getOne() + + return lastDocumentInvoiceSettings; } async getInvoiceTemplate() : Promise | undefined { @@ -63,7 +64,7 @@ export default class DocumentInvoiceSettingsService extends TransactionBaseServi } async updateInvoiceForcedNumber(forcedNumber: string | undefined) : Promise | undefined { - if (forcedNumber && isNumber(parseInt(forcedNumber))) { + if (forcedNumber && !isNaN(Number(forcedNumber))) { const documentInvoiceSettingsRepository = this.activeManager_.getRepository(DocumentInvoiceSettings); const lastDocumentInvoiceSettings = await this.getLastDocumentInvoiceSettings(); const newDocumentInvoiceSettings = this.activeManager_.create(DocumentInvoiceSettings); @@ -101,4 +102,22 @@ export default class DocumentInvoiceSettingsService extends TransactionBaseServi return result; } + + async updateSettings(newFormatNumber?: string, forcedNumber?: string, invoiceTemplate?: TemplateKind) : Promise | undefined { + const documentInvoiceSettingsRepository = this.activeManager_.getRepository(DocumentInvoiceSettings); + const newDocumentInvoiceSettings = this.activeManager_.create(DocumentInvoiceSettings); + const lastDocumentInvoiceSettings = await this.getLastDocumentInvoiceSettings(); + this.copySettingsIfPossible(newDocumentInvoiceSettings, lastDocumentInvoiceSettings); + if (newFormatNumber) { + newDocumentInvoiceSettings.invoice_number_format = newFormatNumber; + } + if (forcedNumber !== undefined && !isNaN(Number(forcedNumber))) { + newDocumentInvoiceSettings.invoice_forced_number = parseInt(forcedNumber); + } + if (invoiceTemplate) { + newDocumentInvoiceSettings.invoice_template = invoiceTemplate; + } + const result = await documentInvoiceSettingsRepository.save(newDocumentInvoiceSettings); + return result; + } } \ No newline at end of file diff --git a/src/services/invoice.ts b/src/services/invoice.ts index 310e2a8..e2acde3 100644 --- a/src/services/invoice.ts +++ b/src/services/invoice.ts @@ -58,10 +58,13 @@ export default class InvoiceService extends TransactionBaseService { return undefined; } - private async getNextInvoiceNumber() { + private async getNextInvoiceNumber(resetForcedNumber?: boolean) { const forcedNumber: string | undefined = await this.documentInvoiceSettingsService.getInvoiceForcedNumber(); - if (forcedNumber) { + if (forcedNumber !== undefined) { + if (resetForcedNumber) { + await this.documentInvoiceSettingsService.resetForcedNumberByCreatingNewSettings(); + } return forcedNumber; } @@ -85,6 +88,17 @@ export default class InvoiceService extends TransactionBaseService { } } + async getTestDisplayNumber(formatNumber?: string, forcedNumber?: string) : Promise | undefined { + const nextNumber: string | undefined = forcedNumber !== undefined ? forcedNumber : await this.getNextInvoiceNumber(); + if (nextNumber) { + return formatNumber ? formatNumber.replace(INVOICE_NUMBER_PLACEHOLDER, nextNumber) : nextNumber; + } + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + 'Neither forced number is set or any order present' + ); + } + async getInvoiceTemplate() : Promise | undefined { const documentSettingsRepository = this.activeManager_.getRepository(DocumentSettings); @@ -226,7 +240,8 @@ export default class InvoiceService extends TransactionBaseService { const calculatedTemplateKind = this.calculateTemplateKind(settings, invoiceSettings); const [validationPassed, info] = validateInputForProvidedKind(calculatedTemplateKind, settings); if (validationPassed) { - const nextNumber: string = await this.getNextInvoiceNumber(); + const RESET_FORCED_NUMBER = true; + const nextNumber: string = await this.getNextInvoiceNumber(RESET_FORCED_NUMBER); const newEntry = this.activeManager_.create(Invoice); newEntry.number = nextNumber; diff --git a/src/ui-components/settings/settings-invoice-display-number.tsx b/src/ui-components/settings/settings-invoice-display-number.tsx new file mode 100644 index 0000000..6d6ec29 --- /dev/null +++ b/src/ui-components/settings/settings-invoice-display-number.tsx @@ -0,0 +1,59 @@ +/* + * Copyright 2024 RSC-Labs, https://rsoftcon.com/ + * + * MIT License + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Heading, Text, FocusModal, Button, Input, Label, Toaster, Alert } from "@medusajs/ui" +import { CircularProgress, Grid } from "@mui/material"; +import { useAdminCustomQuery } from "medusa-react" + +type AdminStoreDocumentInvoiceSettingsDisplayNumberQueryReq = { + formatNumber: string, + forcedNumber: number +} + +type StoreDocumentInvoiceSettingsDisplayNumberResult = { + displayNumber: string +} + +const InvoiceSettingsDisplayNumber = ({ formatNumber, forcedNumber } : {formatNumber?: string, forcedNumber?: number}) => { + + const { data, isLoading } = useAdminCustomQuery + ( + "/invoice/display-number", + [formatNumber, forcedNumber], + { + formatNumber: formatNumber, + forcedNumber: forcedNumber + } + ) + + if (isLoading) { + return ( + + + + ) + } + + return ( + + + + ) +} + + +export default InvoiceSettingsDisplayNumber \ No newline at end of file diff --git a/src/ui-components/settings/settings-invoice.tsx b/src/ui-components/settings/settings-invoice.tsx index 5a77a59..773e1c9 100644 --- a/src/ui-components/settings/settings-invoice.tsx +++ b/src/ui-components/settings/settings-invoice.tsx @@ -10,61 +10,64 @@ * limitations under the License. */ -import { Heading, Text, FocusModal, Button, Input, Label, Toaster, Alert } from "@medusajs/ui" +import { Heading, Text, FocusModal, Button, Input, Label, Alert } from "@medusajs/ui" import { CircularProgress, Grid } from "@mui/material"; import { useAdminCustomPost, useAdminCustomQuery } from "medusa-react" import { useForm } from "react-hook-form"; import { useToast } from "@medusajs/ui" import { useState } from "react"; -import { AdminStoreDocumentInvoiceSettingsQueryReq, AdminStoreDocumentSettingsQueryReq, StoreDocumentInvoiceSettingsResult, StoreDocumentSettingsResult } from "../types/api"; +import { AdminStoreDocumentInvoiceSettingsPostReq, AdminStoreDocumentInvoiceSettingsQueryReq, DocumentInvoiceSettings, StoreDocumentInvoiceSettingsResult, StoreDocumentSettingsResult } from "../types/api"; +import InvoiceSettingsDisplayNumber from "./settings-invoice-display-number"; +import { isBoolean } from "lodash"; -type AdminStoreInvoiceNumberFormatPostReq = { - formatNumber: string +type InvoiceSettings = { + formatNumber: string, + forcedNumber: number } -type InvoiceNumberFormat = { - formatNumber: string -} - -const InvoiceSettingsForm = ({ currentFormatNumber, setOpenModal } : {currentFormatNumber?: string, setOpenModal: any}) => { +const InvoiceSettingsForm = ({ invoiceSettings, setOpenModal } : {invoiceSettings: DocumentInvoiceSettings, setOpenModal: any}) => { const { toast } = useToast() - const { register, handleSubmit, formState: { errors } } = useForm() + const { register, handleSubmit, formState: { errors } } = useForm() + const [formatNumber, setFormatNumber] = useState(invoiceSettings.invoice_number_format); + const [forcedNumber, setForcedNumber] = useState(invoiceSettings.invoice_forced_number); + const [ error, setError ] = useState(undefined); const { mutate } = useAdminCustomPost< - AdminStoreInvoiceNumberFormatPostReq, + AdminStoreDocumentInvoiceSettingsPostReq, StoreDocumentInvoiceSettingsResult > ( - `/document-invoice-settings/format-number`, - ["format-number"] + `/document-invoice-settings`, + ['document-invoice-settings'] ) - const onSubmit = (data: InvoiceNumberFormat) => { + const onSubmit = (data: InvoiceSettings) => { return mutate( { - formatNumber: data.formatNumber + formatNumber: data.formatNumber, + forcedNumber: data.forcedNumber }, { onSuccess: async ( { response, settings } ) => { - if (response.status == 201 && settings && settings.invoice_number_format) { + if (response.status == 201 && settings) { toast({ - title: 'Format number', - description: "New format number saved", + title: 'Invoice settings', + description: "New invoice settings saved", variant: 'success' }); setOpenModal(false); } else { toast({ - title: 'Format number', - description: "New format number cannot be saved, some error happened.", + title: 'Invoice settings', + description: "New invoice settings cannot be saved, some error happened.", variant: 'error' }); } }, onError: ( { } ) => { toast({ - title: 'Format number', - description: "New format number cannot be saved, some error happened.", + title: 'Invoice settings', + description: "New invoice settings cannot be saved, some error happened.", variant: 'error' }); }, @@ -72,40 +75,106 @@ const InvoiceSettingsForm = ({ currentFormatNumber, setOpenModal } : {currentFor ) } - const validateInvoiceNumber = (value) => { + const errorText = `Text {invoice_number} needs to be included in input.` + const LABEL_MUST_FORMAT = `Format must include {invoice_number}`; + const LABEL_MUST_FORCED = `Forced number must be a number`; + const LABEL_INFO_FORCED = `It will auto-increment starting from this number.`; + + const validateFormatNumber = (value) => { if (!value.includes('{invoice_number}')) { - return "Input must contain '{invoice_number}'"; + return LABEL_MUST_FORMAT; + } + return true; + }; + const validateForcedNumber = (value) => { + if (value.length && isNaN(Number(value))) { + return LABEL_MUST_FORCED; } return true; }; - - const errorText = `Text {invoice_number} needs to be included in input.` - const LABEL_MUST = `Format MUST include {invoice_number}`; return (
- - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + {errors.formatNumber == undefined && errors.forcedNumber == undefined && error == undefined && + + } +