Skip to content

Commit

Permalink
Feature: "forced invoice number" enabled
Browse files Browse the repository at this point in the history
  • Loading branch information
radoslaw-sz committed Apr 23, 2024
1 parent d61cbee commit d027e3d
Show file tree
Hide file tree
Showing 10 changed files with 311 additions and 67 deletions.
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<b>Protip:</b> 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.

<b>Protip:</b> After setting change, you can always go to `Templates` to see a preview with your next invoice number.

## Q&A

Expand All @@ -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 <b>base number</b>). 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.

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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)",
Expand Down
30 changes: 30 additions & 0 deletions src/api/admin/document-invoice-settings/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
})
}
}
39 changes: 39 additions & 0 deletions src/api/admin/invoice/display-number/route.ts
Original file line number Diff line number Diff line change
@@ -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
})
}
}
2 changes: 1 addition & 1 deletion src/api/admin/invoice/generate/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
55 changes: 37 additions & 18 deletions src/services/document-invoice-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -26,32 +25,34 @@ export default class DocumentInvoiceSettingsService extends TransactionBaseServi
}
}

async getLastDocumentInvoiceSettings() : Promise<DocumentInvoiceSettings> | undefined {
const documentInvoiceSettingsRepository = this.activeManager_.getRepository(DocumentInvoiceSettings);
const lastDocumentInvoiceSettings = await documentInvoiceSettingsRepository.createQueryBuilder('documentInvoiceSettings')
.orderBy('documentInvoiceSettings.created_at', 'DESC')
.getOne()

return lastDocumentInvoiceSettings;
async getInvoiceForcedNumber() : Promise<string> | 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<DocumentInvoiceSettings> {
async resetForcedNumberByCreatingNewSettings() : Promise<DocumentInvoiceSettings> {
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<string> | 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<DocumentInvoiceSettings> | undefined {
const documentInvoiceSettingsRepository = this.activeManager_.getRepository(DocumentInvoiceSettings);
const lastDocumentInvoiceSettings = await documentInvoiceSettingsRepository.createQueryBuilder('documentInvoiceSettings')
.orderBy('documentInvoiceSettings.created_at', 'DESC')
.getOne()

return lastDocumentInvoiceSettings;
}

async getInvoiceTemplate() : Promise<string> | undefined {
Expand All @@ -63,7 +64,7 @@ export default class DocumentInvoiceSettingsService extends TransactionBaseServi
}

async updateInvoiceForcedNumber(forcedNumber: string | undefined) : Promise<DocumentInvoiceSettings> | 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);
Expand Down Expand Up @@ -101,4 +102,22 @@ export default class DocumentInvoiceSettingsService extends TransactionBaseServi

return result;
}

async updateSettings(newFormatNumber?: string, forcedNumber?: string, invoiceTemplate?: TemplateKind) : Promise<DocumentInvoiceSettings> | 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;
}
}
21 changes: 18 additions & 3 deletions src/services/invoice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -85,6 +88,17 @@ export default class InvoiceService extends TransactionBaseService {
}
}

async getTestDisplayNumber(formatNumber?: string, forcedNumber?: string) : Promise<string> | 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<string> | undefined {

const documentSettingsRepository = this.activeManager_.getRepository(DocumentSettings);
Expand Down Expand Up @@ -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;

Expand Down
59 changes: 59 additions & 0 deletions src/ui-components/settings/settings-invoice-display-number.tsx
Original file line number Diff line number Diff line change
@@ -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
<AdminStoreDocumentInvoiceSettingsDisplayNumberQueryReq, StoreDocumentInvoiceSettingsDisplayNumberResult>(
"/invoice/display-number",
[formatNumber, forcedNumber],
{
formatNumber: formatNumber,
forcedNumber: forcedNumber
}
)

if (isLoading) {
return (
<Grid item>
<Input
readOnly={true}
/>
</Grid>
)
}

return (
<Grid item>
<Input key={`display-number-${data.displayNumber}`}
defaultValue={data.displayNumber}
readOnly={true}
/>
</Grid>
)
}


export default InvoiceSettingsDisplayNumber
Loading

0 comments on commit d027e3d

Please sign in to comment.