Invoices now mirror the issued-orders document model: - New invoice_sections table (CZ/EN rich-text "Obsah") edited via the shared SectionsEditor, printed inline right after the items on the PDF. Full-replace on update, same transaction as items. - Printed notes dropped: the notes column is removed (migration merges existing content into internal_notes first); the form field is now "Interni poznamky", never printed. Legacy payloads sending notes are silently stripped. - Form cleanup: Cislo faktury and Vystavil fields removed (number lives in the header, issued_by auto-fills); page header title restyled to the orders/offers pattern (number span + status chip). - Unified per-page PDF header for the red-accent family: shared buildPdfHeaderTemplate in pdf-shared (22mm logo, red heading, red rule) rendered by a Puppeteer headerTemplate on EVERY page of both invoices and issued orders (incl. the /file fallback render); body headers are print-hidden. htmlToPdf gained the headerTemplate option. - Footer parity: invoices get the per-page "Vystavil + Strana X z Y" footer; the invoice bottom block (notice + QR/VAT recap + Prevzal) is break-inside: avoid so a page break can never split it. - @page margins now match the template space (32mm top, 18mm bottom) - Chromium lays out by CSS @page margins, which also fixes issued orders' content running into the 18mm footer zone on full pages. BREAKING CHANGE: invoices.notes column dropped (data merged into internal_notes); deploy must run prisma migrate deploy + generate. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
78 lines
3.3 KiB
TypeScript
78 lines
3.3 KiB
TypeScript
import { z } from "zod";
|
|
import {
|
|
numberFromForm,
|
|
numberInRange,
|
|
nonNegativeNumberFromForm,
|
|
positiveNumberFromForm,
|
|
nullableIntIdFromForm,
|
|
booleanFromForm,
|
|
DocumentSectionSchema,
|
|
} from "./common";
|
|
|
|
const InvoiceItemSchema = z.object({
|
|
description: z.string().max(8000).nullish(),
|
|
item_description: z.string().max(8000).nullish(),
|
|
quantity: positiveNumberFromForm.default(1),
|
|
unit: z.string().max(255).nullish(),
|
|
unit_price: numberFromForm.default(0),
|
|
vat_rate: numberInRange(0, 100).optional().default(21.0),
|
|
position: nonNegativeNumberFromForm.optional(),
|
|
});
|
|
|
|
export const CreateInvoiceSchema = z.object({
|
|
invoice_number: z.string().max(255).nullish(),
|
|
order_id: nullableIntIdFromForm.nullish(),
|
|
customer_id: nullableIntIdFromForm.nullish(),
|
|
status: z.string().max(20).optional().default("draft"),
|
|
currency: z.string().max(20).optional().default("CZK"),
|
|
vat_rate: numberInRange(0, 100).optional().default(21.0),
|
|
apply_vat: booleanFromForm.optional().default(true),
|
|
payment_method: z.string().max(255).nullish(),
|
|
constant_symbol: z.string().max(255).nullish(),
|
|
bank_name: z.string().max(255).nullish(),
|
|
bank_swift: z.string().max(255).nullish(),
|
|
bank_iban: z.string().max(255).nullish(),
|
|
bank_account: z.string().max(255).nullish(),
|
|
issue_date: z.string().max(255).nullish(),
|
|
due_date: z.string().max(255).nullish(),
|
|
tax_date: z.string().max(255).nullish(),
|
|
issued_by: z.string().max(255).nullish(),
|
|
billing_text: z.string().max(8000).nullish(),
|
|
language: z.string().max(20).optional().default("cs"),
|
|
// `notes` was dropped (printed-notes block removed — free-form PDF content
|
|
// lives in the sections); legacy clients still sending it are silently
|
|
// stripped by z.object. internal_notes is never printed.
|
|
internal_notes: z.string().max(8000).nullish(),
|
|
items: z.array(InvoiceItemSchema).optional(),
|
|
// Rich-text CZ/EN "Obsah" sections rendered after the items on the PDF
|
|
// (mirrors issued orders — shared DocumentSectionSchema, DB-aligned limits).
|
|
sections: z.array(DocumentSectionSchema).optional(),
|
|
});
|
|
|
|
export const UpdateInvoiceSchema = z.object({
|
|
status: z.string().max(20).optional(),
|
|
currency: z.string().max(20).optional(),
|
|
language: z.string().max(20).optional(),
|
|
payment_method: z.string().max(255).nullish(),
|
|
constant_symbol: z.string().max(255).nullish(),
|
|
bank_name: z.string().max(255).nullish(),
|
|
bank_swift: z.string().max(255).nullish(),
|
|
bank_iban: z.string().max(255).nullish(),
|
|
bank_account: z.string().max(255).nullish(),
|
|
issued_by: z.string().max(255).nullish(),
|
|
billing_text: z.string().max(8000).nullish(),
|
|
customer_id: nullableIntIdFromForm.optional(),
|
|
vat_rate: numberInRange(0, 100).optional(),
|
|
apply_vat: booleanFromForm.optional(),
|
|
issue_date: z.union([z.string().max(255), z.null()]).optional(),
|
|
due_date: z.union([z.string().max(255), z.null()]).optional(),
|
|
tax_date: z.union([z.string().max(255), z.null()]).optional(),
|
|
internal_notes: z.string().max(8000).nullish(),
|
|
paid_date: z.union([z.string().max(255), z.null()]).optional(),
|
|
items: z.array(InvoiceItemSchema).optional(),
|
|
sections: z.array(DocumentSectionSchema).optional(),
|
|
});
|
|
|
|
export type CreateInvoiceInput = z.infer<typeof CreateInvoiceSchema>;
|
|
export type UpdateInvoiceInput = z.infer<typeof UpdateInvoiceSchema>;
|