feat(issued-orders): editable PDF heading 'Text objednavky (na PDF)' (order_text)
New nullable issued_orders.order_text column (migration applied to dev+test). The heading above the PDF items table is now editable per order, mirroring invoices' billing_text: empty falls back to the default, which is now 'Objednavame si u Vas:' per user wording. Field sits above Dodaci podminky in the detail form; persisted via create + update strFields (both schemas - the invoices Update-schema gap is not repeated). Tests: persistence round-trip incl. null-clears, PDF custom heading + default fallback. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE `issued_orders` ADD COLUMN `order_text` VARCHAR(500) NULL;
|
||||
|
||||
@@ -381,6 +381,7 @@ model issued_orders {
|
||||
delivery_terms String? @db.VarChar(500)
|
||||
payment_terms String? @db.VarChar(500)
|
||||
issued_by String? @db.VarChar(255)
|
||||
order_text String? @db.VarChar(500)
|
||||
notes String? @db.Text
|
||||
internal_notes String? @db.Text
|
||||
created_at DateTime? @default(now()) @db.DateTime(0)
|
||||
|
||||
@@ -170,6 +170,23 @@ describe("createIssuedOrder", () => {
|
||||
const res = await createIssuedOrder({ supplier_id: 99999999 });
|
||||
expect("error" in res && res.error).toBe("supplier_not_found");
|
||||
});
|
||||
|
||||
it("persists order_text on create, updates and clears it on update", async () => {
|
||||
const order = await mkIssued({ order_text: "Objednáváme dle smlouvy:" });
|
||||
let row = await prisma.issued_orders.findUnique({
|
||||
where: { id: order.id },
|
||||
});
|
||||
expect(row!.order_text).toBe("Objednáváme dle smlouvy:");
|
||||
|
||||
await updateIssuedOrder(order.id, { order_text: "Jiný text:" });
|
||||
row = await prisma.issued_orders.findUnique({ where: { id: order.id } });
|
||||
expect(row!.order_text).toBe("Jiný text:");
|
||||
|
||||
// null clears back to the PDF default.
|
||||
await updateIssuedOrder(order.id, { order_text: null });
|
||||
row = await prisma.issued_orders.findUnique({ where: { id: order.id } });
|
||||
expect(row!.order_text).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateIssuedOrder status transitions", () => {
|
||||
@@ -507,6 +524,29 @@ describe("renderIssuedOrderHtml", () => {
|
||||
expect(html).not.toContain("alert(1)");
|
||||
});
|
||||
|
||||
it("renders the custom order_text heading when set, default when not", () => {
|
||||
const custom = renderIssuedOrderHtml(
|
||||
{ ...order, order_text: "Objednáváme dle nabídky č. 123:" },
|
||||
items,
|
||||
null,
|
||||
null,
|
||||
"cs",
|
||||
issuer,
|
||||
);
|
||||
expect(custom).toContain("Objednáváme dle nabídky č. 123:");
|
||||
expect(custom).not.toContain("Objednáváme si u Vás:");
|
||||
|
||||
const fallback = renderIssuedOrderHtml(
|
||||
order,
|
||||
items,
|
||||
null,
|
||||
null,
|
||||
"cs",
|
||||
issuer,
|
||||
);
|
||||
expect(fallback).toContain("Objednáváme si u Vás:");
|
||||
});
|
||||
|
||||
it("with VAT: keeps the VAT columns and the DPH cell holds ONLY the line VAT", () => {
|
||||
const html = renderIssuedOrderHtml(order, items, null, null, "cs", issuer);
|
||||
expect(html).toContain("%DPH");
|
||||
|
||||
@@ -45,6 +45,7 @@ export interface IssuedOrderDetail extends IssuedOrder {
|
||||
delivery_terms: string | null;
|
||||
payment_terms: string | null;
|
||||
issued_by: string | null;
|
||||
order_text: string | null;
|
||||
notes: string | null;
|
||||
internal_notes: string | null;
|
||||
items: IssuedOrderItem[];
|
||||
|
||||
@@ -171,6 +171,7 @@ interface OrderForm {
|
||||
delivery_terms: string;
|
||||
payment_terms: string;
|
||||
issued_by: string;
|
||||
order_text: string;
|
||||
notes: string;
|
||||
internal_notes: string;
|
||||
status: string;
|
||||
@@ -520,6 +521,7 @@ export default function IssuedOrderDetail() {
|
||||
delivery_terms: "",
|
||||
payment_terms: "",
|
||||
issued_by: user?.fullName || "",
|
||||
order_text: "",
|
||||
notes: "",
|
||||
internal_notes: "",
|
||||
status: "draft",
|
||||
@@ -621,6 +623,7 @@ export default function IssuedOrderDetail() {
|
||||
delivery_terms: d.delivery_terms || "",
|
||||
payment_terms: d.payment_terms || "",
|
||||
issued_by: d.issued_by || "",
|
||||
order_text: d.order_text || "",
|
||||
notes: d.notes || "",
|
||||
internal_notes: d.internal_notes || "",
|
||||
status: d.status,
|
||||
@@ -779,6 +782,7 @@ export default function IssuedOrderDetail() {
|
||||
delivery_terms: form.delivery_terms,
|
||||
payment_terms: form.payment_terms,
|
||||
issued_by: form.issued_by,
|
||||
order_text: form.order_text || null,
|
||||
notes: form.notes,
|
||||
internal_notes: form.internal_notes,
|
||||
items: items
|
||||
@@ -1419,6 +1423,16 @@ export default function IssuedOrderDetail() {
|
||||
|
||||
{/* Notes & terms */}
|
||||
<Card sx={{ mb: 3 }}>
|
||||
<Field label="Text objednávky (na PDF)">
|
||||
<TextField
|
||||
value={form.order_text}
|
||||
disabled={!editable}
|
||||
onChange={(e) =>
|
||||
setForm((prev) => ({ ...prev, order_text: e.target.value }))
|
||||
}
|
||||
placeholder="Objednáváme si u Vás: (ponechte prázdné pro výchozí)"
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Dodací podmínky">
|
||||
<TextField
|
||||
value={form.delivery_terms}
|
||||
|
||||
@@ -195,7 +195,7 @@ const translations: Record<Lang, Record<string, string>> = {
|
||||
buyer: "Odběratel",
|
||||
issue_date: "Datum vystavení:",
|
||||
delivery_date: "Požadované dodání:",
|
||||
billing: "Objednáváme u Vás:",
|
||||
billing: "Objednáváme si u Vás:",
|
||||
col_no: "Č.",
|
||||
col_desc: "Popis",
|
||||
col_qty: "Množství",
|
||||
@@ -257,6 +257,8 @@ interface IssuedOrderPdfData {
|
||||
delivery_terms: string | null;
|
||||
payment_terms: string | null;
|
||||
issued_by: string | null;
|
||||
// Editable heading above the items table; null → t.billing default.
|
||||
order_text?: string | null;
|
||||
}
|
||||
|
||||
interface IssuedOrderPdfItem {
|
||||
@@ -771,7 +773,7 @@ ${indentCSS}
|
||||
</table>
|
||||
|
||||
<!-- Polozky -->
|
||||
<div class="billing-label">${escapeHtml(t.billing)}</div>
|
||||
<div class="billing-label">${escapeHtml(order.order_text || t.billing)}</div>
|
||||
<table class="items">
|
||||
<thead>
|
||||
<tr>
|
||||
|
||||
@@ -40,6 +40,9 @@ export const CreateIssuedOrderSchema = z.object({
|
||||
delivery_terms: z.string().max(500).nullish(),
|
||||
payment_terms: z.string().max(500).nullish(),
|
||||
issued_by: z.string().max(255).nullish(),
|
||||
// Editable heading above the PDF items table; empty/null falls back to the
|
||||
// default "Objednáváme si u Vás:" (issued-orders-pdf t.billing).
|
||||
order_text: z.string().max(500).nullish(),
|
||||
notes: z.string().nullish(),
|
||||
internal_notes: z.string().nullish(),
|
||||
items: z.array(IssuedOrderItemSchema).optional(),
|
||||
|
||||
@@ -32,6 +32,7 @@ export interface IssuedOrderInput {
|
||||
delivery_terms?: string | null;
|
||||
payment_terms?: string | null;
|
||||
issued_by?: string | null;
|
||||
order_text?: string | null;
|
||||
notes?: string | null;
|
||||
internal_notes?: string | null;
|
||||
items?: IssuedOrderItemInput[];
|
||||
@@ -304,6 +305,7 @@ export async function createIssuedOrder(body: IssuedOrderInput) {
|
||||
: null,
|
||||
payment_terms: body.payment_terms ? String(body.payment_terms) : null,
|
||||
issued_by: body.issued_by ? String(body.issued_by) : null,
|
||||
order_text: body.order_text ? String(body.order_text) : null,
|
||||
notes: body.notes ? String(body.notes) : null,
|
||||
internal_notes: body.internal_notes
|
||||
? String(body.internal_notes)
|
||||
@@ -354,6 +356,7 @@ export async function updateIssuedOrder(id: number, body: IssuedOrderInput) {
|
||||
"delivery_terms",
|
||||
"payment_terms",
|
||||
"issued_by",
|
||||
"order_text",
|
||||
];
|
||||
for (const f of strFields) {
|
||||
if (body[f] !== undefined) data[f] = body[f] ? String(body[f]) : null;
|
||||
|
||||
Reference in New Issue
Block a user