feat(numbering): projects get their own dedicated number sequence (decoupled from orders)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,7 @@ const DEFAULT_OFFER_PATTERN = "{YYYY}/{PREFIX}/{NNN}";
|
||||
const DEFAULT_ORDER_PATTERN = "{YY}{CODE}{NNNN}";
|
||||
const DEFAULT_INVOICE_PATTERN = "{YY}{CODE}{NNNN}";
|
||||
const DEFAULT_ISSUED_ORDER_PATTERN = "{YY}{CODE}{NNNN}";
|
||||
const DEFAULT_PROJECT_PATTERN = "{YY}{CODE}{NNNN}";
|
||||
|
||||
/**
|
||||
* Apply a numbering pattern template.
|
||||
@@ -46,6 +47,8 @@ async function getSettings() {
|
||||
invoice_number_pattern: true,
|
||||
issued_order_number_pattern: true,
|
||||
issued_order_type_code: true,
|
||||
project_number_pattern: true,
|
||||
project_type_code: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -156,7 +159,9 @@ async function releaseSequence(
|
||||
: type === "issued_order"
|
||||
? settings?.issued_order_number_pattern ||
|
||||
DEFAULT_ISSUED_ORDER_PATTERN
|
||||
: settings?.offer_number_pattern || DEFAULT_OFFER_PATTERN;
|
||||
: type === "project"
|
||||
? settings?.project_number_pattern || DEFAULT_PROJECT_PATTERN
|
||||
: settings?.offer_number_pattern || DEFAULT_OFFER_PATTERN;
|
||||
const code =
|
||||
type === "shared"
|
||||
? settings?.order_type_code || "71"
|
||||
@@ -164,7 +169,9 @@ async function releaseSequence(
|
||||
? settings?.invoice_type_code || "81"
|
||||
: type === "issued_order"
|
||||
? settings?.issued_order_type_code || "72"
|
||||
: "";
|
||||
: type === "project"
|
||||
? settings?.project_type_code || "73"
|
||||
: "";
|
||||
const prefix = type === "offer" ? settings?.quotation_prefix || "NA" : "";
|
||||
|
||||
// Lock the sequence row for the duration of the decrement so a concurrent
|
||||
@@ -237,6 +244,14 @@ export async function isOrderNumberTaken(number: string): Promise<boolean> {
|
||||
return !!existing;
|
||||
}
|
||||
|
||||
/** Verify a project number is not already used. */
|
||||
export async function isProjectNumberTaken(number: string): Promise<boolean> {
|
||||
const existing = await prisma.projects.findFirst({
|
||||
where: { project_number: number },
|
||||
});
|
||||
return !!existing;
|
||||
}
|
||||
|
||||
/**
|
||||
* Next offer/quotation number (consumes sequence).
|
||||
* Verifies uniqueness against the quotations table; retries if taken.
|
||||
@@ -330,6 +345,54 @@ export async function previewSharedNumber(): Promise<string> {
|
||||
return applyPattern(pattern, { year, prefix: "", code, seq });
|
||||
}
|
||||
|
||||
/**
|
||||
* Next project number (consumes the dedicated `project` sequence).
|
||||
* Decoupled from the `shared` order sequence so a project (standalone OR
|
||||
* auto-created from an order) gets its own number from a separate pool.
|
||||
* Verifies uniqueness against the projects table; retries if taken.
|
||||
* Pass `tx` when calling inside an existing Prisma transaction.
|
||||
*/
|
||||
export async function generateProjectNumber(tx?: TxClient): Promise<string> {
|
||||
const settings = await getSettings();
|
||||
const pattern = settings?.project_number_pattern || DEFAULT_PROJECT_PATTERN;
|
||||
const code = settings?.project_type_code || "73";
|
||||
const year = new Date().getFullYear();
|
||||
|
||||
for (let attempt = 0; attempt < 100; attempt++) {
|
||||
const seq = await getNextSequence("project", year, tx);
|
||||
const number = applyPattern(pattern, { year, prefix: "", code, seq });
|
||||
if (!(await isProjectNumberTaken(number))) {
|
||||
return number;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("Nepodařilo se vygenerovat jedinečné číslo projektu");
|
||||
}
|
||||
|
||||
/**
|
||||
* Preview next project number (does NOT consume sequence).
|
||||
*/
|
||||
export async function previewProjectNumber(): Promise<string> {
|
||||
const settings = await getSettings();
|
||||
const pattern = settings?.project_number_pattern || DEFAULT_PROJECT_PATTERN;
|
||||
const code = settings?.project_type_code || "73";
|
||||
const year = new Date().getFullYear();
|
||||
|
||||
// Mirror generateProjectNumber's collision check (without consuming the
|
||||
// sequence). The number_sequences counter can lag behind real data — e.g.
|
||||
// when projects were imported or seeded without bumping it — so the raw next
|
||||
// sequence may already be taken. Advance past any number already used by a
|
||||
// project, so the preview matches what generateProjectNumber would actually
|
||||
// assign instead of showing a stale, already-used number.
|
||||
let seq = await previewNextSequence("project", year);
|
||||
for (let attempt = 0; attempt < 1000; attempt++) {
|
||||
const number = applyPattern(pattern, { year, prefix: "", code, seq });
|
||||
if (!(await isProjectNumberTaken(number))) return number;
|
||||
seq++;
|
||||
}
|
||||
return applyPattern(pattern, { year, prefix: "", code, seq });
|
||||
}
|
||||
|
||||
/**
|
||||
* Next invoice number (consumes sequence).
|
||||
* Verifies uniqueness against the invoices table; retries if taken.
|
||||
@@ -519,6 +582,18 @@ export async function releaseSharedNumber(
|
||||
);
|
||||
}
|
||||
|
||||
/** Release a project number back to the pool (decrement if highest). */
|
||||
export async function releaseProjectNumber(
|
||||
year?: number,
|
||||
deletedNumber?: string,
|
||||
) {
|
||||
await releaseSequence(
|
||||
"project",
|
||||
year || new Date().getFullYear(),
|
||||
deletedNumber,
|
||||
);
|
||||
}
|
||||
|
||||
/** Release an invoice number back to the pool (decrement if highest). */
|
||||
export async function releaseInvoiceNumber(
|
||||
year?: number,
|
||||
|
||||
Reference in New Issue
Block a user