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)
|
delivery_terms String? @db.VarChar(500)
|
||||||
payment_terms String? @db.VarChar(500)
|
payment_terms String? @db.VarChar(500)
|
||||||
issued_by String? @db.VarChar(255)
|
issued_by String? @db.VarChar(255)
|
||||||
|
order_text String? @db.VarChar(500)
|
||||||
notes String? @db.Text
|
notes String? @db.Text
|
||||||
internal_notes String? @db.Text
|
internal_notes String? @db.Text
|
||||||
created_at DateTime? @default(now()) @db.DateTime(0)
|
created_at DateTime? @default(now()) @db.DateTime(0)
|
||||||
|
|||||||
@@ -170,6 +170,23 @@ describe("createIssuedOrder", () => {
|
|||||||
const res = await createIssuedOrder({ supplier_id: 99999999 });
|
const res = await createIssuedOrder({ supplier_id: 99999999 });
|
||||||
expect("error" in res && res.error).toBe("supplier_not_found");
|
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", () => {
|
describe("updateIssuedOrder status transitions", () => {
|
||||||
@@ -507,6 +524,29 @@ describe("renderIssuedOrderHtml", () => {
|
|||||||
expect(html).not.toContain("alert(1)");
|
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", () => {
|
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);
|
const html = renderIssuedOrderHtml(order, items, null, null, "cs", issuer);
|
||||||
expect(html).toContain("%DPH");
|
expect(html).toContain("%DPH");
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ export interface IssuedOrderDetail extends IssuedOrder {
|
|||||||
delivery_terms: string | null;
|
delivery_terms: string | null;
|
||||||
payment_terms: string | null;
|
payment_terms: string | null;
|
||||||
issued_by: string | null;
|
issued_by: string | null;
|
||||||
|
order_text: string | null;
|
||||||
notes: string | null;
|
notes: string | null;
|
||||||
internal_notes: string | null;
|
internal_notes: string | null;
|
||||||
items: IssuedOrderItem[];
|
items: IssuedOrderItem[];
|
||||||
|
|||||||
@@ -171,6 +171,7 @@ interface OrderForm {
|
|||||||
delivery_terms: string;
|
delivery_terms: string;
|
||||||
payment_terms: string;
|
payment_terms: string;
|
||||||
issued_by: string;
|
issued_by: string;
|
||||||
|
order_text: string;
|
||||||
notes: string;
|
notes: string;
|
||||||
internal_notes: string;
|
internal_notes: string;
|
||||||
status: string;
|
status: string;
|
||||||
@@ -520,6 +521,7 @@ export default function IssuedOrderDetail() {
|
|||||||
delivery_terms: "",
|
delivery_terms: "",
|
||||||
payment_terms: "",
|
payment_terms: "",
|
||||||
issued_by: user?.fullName || "",
|
issued_by: user?.fullName || "",
|
||||||
|
order_text: "",
|
||||||
notes: "",
|
notes: "",
|
||||||
internal_notes: "",
|
internal_notes: "",
|
||||||
status: "draft",
|
status: "draft",
|
||||||
@@ -621,6 +623,7 @@ export default function IssuedOrderDetail() {
|
|||||||
delivery_terms: d.delivery_terms || "",
|
delivery_terms: d.delivery_terms || "",
|
||||||
payment_terms: d.payment_terms || "",
|
payment_terms: d.payment_terms || "",
|
||||||
issued_by: d.issued_by || "",
|
issued_by: d.issued_by || "",
|
||||||
|
order_text: d.order_text || "",
|
||||||
notes: d.notes || "",
|
notes: d.notes || "",
|
||||||
internal_notes: d.internal_notes || "",
|
internal_notes: d.internal_notes || "",
|
||||||
status: d.status,
|
status: d.status,
|
||||||
@@ -779,6 +782,7 @@ export default function IssuedOrderDetail() {
|
|||||||
delivery_terms: form.delivery_terms,
|
delivery_terms: form.delivery_terms,
|
||||||
payment_terms: form.payment_terms,
|
payment_terms: form.payment_terms,
|
||||||
issued_by: form.issued_by,
|
issued_by: form.issued_by,
|
||||||
|
order_text: form.order_text || null,
|
||||||
notes: form.notes,
|
notes: form.notes,
|
||||||
internal_notes: form.internal_notes,
|
internal_notes: form.internal_notes,
|
||||||
items: items
|
items: items
|
||||||
@@ -1419,6 +1423,16 @@ export default function IssuedOrderDetail() {
|
|||||||
|
|
||||||
{/* Notes & terms */}
|
{/* Notes & terms */}
|
||||||
<Card sx={{ mb: 3 }}>
|
<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">
|
<Field label="Dodací podmínky">
|
||||||
<TextField
|
<TextField
|
||||||
value={form.delivery_terms}
|
value={form.delivery_terms}
|
||||||
|
|||||||
@@ -195,7 +195,7 @@ const translations: Record<Lang, Record<string, string>> = {
|
|||||||
buyer: "Odběratel",
|
buyer: "Odběratel",
|
||||||
issue_date: "Datum vystavení:",
|
issue_date: "Datum vystavení:",
|
||||||
delivery_date: "Požadované dodání:",
|
delivery_date: "Požadované dodání:",
|
||||||
billing: "Objednáváme u Vás:",
|
billing: "Objednáváme si u Vás:",
|
||||||
col_no: "Č.",
|
col_no: "Č.",
|
||||||
col_desc: "Popis",
|
col_desc: "Popis",
|
||||||
col_qty: "Množství",
|
col_qty: "Množství",
|
||||||
@@ -257,6 +257,8 @@ interface IssuedOrderPdfData {
|
|||||||
delivery_terms: string | null;
|
delivery_terms: string | null;
|
||||||
payment_terms: string | null;
|
payment_terms: string | null;
|
||||||
issued_by: string | null;
|
issued_by: string | null;
|
||||||
|
// Editable heading above the items table; null → t.billing default.
|
||||||
|
order_text?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IssuedOrderPdfItem {
|
interface IssuedOrderPdfItem {
|
||||||
@@ -771,7 +773,7 @@ ${indentCSS}
|
|||||||
</table>
|
</table>
|
||||||
|
|
||||||
<!-- Polozky -->
|
<!-- Polozky -->
|
||||||
<div class="billing-label">${escapeHtml(t.billing)}</div>
|
<div class="billing-label">${escapeHtml(order.order_text || t.billing)}</div>
|
||||||
<table class="items">
|
<table class="items">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
@@ -40,6 +40,9 @@ export const CreateIssuedOrderSchema = z.object({
|
|||||||
delivery_terms: z.string().max(500).nullish(),
|
delivery_terms: z.string().max(500).nullish(),
|
||||||
payment_terms: z.string().max(500).nullish(),
|
payment_terms: z.string().max(500).nullish(),
|
||||||
issued_by: z.string().max(255).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(),
|
notes: z.string().nullish(),
|
||||||
internal_notes: z.string().nullish(),
|
internal_notes: z.string().nullish(),
|
||||||
items: z.array(IssuedOrderItemSchema).optional(),
|
items: z.array(IssuedOrderItemSchema).optional(),
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export interface IssuedOrderInput {
|
|||||||
delivery_terms?: string | null;
|
delivery_terms?: string | null;
|
||||||
payment_terms?: string | null;
|
payment_terms?: string | null;
|
||||||
issued_by?: string | null;
|
issued_by?: string | null;
|
||||||
|
order_text?: string | null;
|
||||||
notes?: string | null;
|
notes?: string | null;
|
||||||
internal_notes?: string | null;
|
internal_notes?: string | null;
|
||||||
items?: IssuedOrderItemInput[];
|
items?: IssuedOrderItemInput[];
|
||||||
@@ -304,6 +305,7 @@ export async function createIssuedOrder(body: IssuedOrderInput) {
|
|||||||
: null,
|
: null,
|
||||||
payment_terms: body.payment_terms ? String(body.payment_terms) : null,
|
payment_terms: body.payment_terms ? String(body.payment_terms) : null,
|
||||||
issued_by: body.issued_by ? String(body.issued_by) : 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,
|
notes: body.notes ? String(body.notes) : null,
|
||||||
internal_notes: body.internal_notes
|
internal_notes: body.internal_notes
|
||||||
? String(body.internal_notes)
|
? String(body.internal_notes)
|
||||||
@@ -354,6 +356,7 @@ export async function updateIssuedOrder(id: number, body: IssuedOrderInput) {
|
|||||||
"delivery_terms",
|
"delivery_terms",
|
||||||
"payment_terms",
|
"payment_terms",
|
||||||
"issued_by",
|
"issued_by",
|
||||||
|
"order_text",
|
||||||
];
|
];
|
||||||
for (const f of strFields) {
|
for (const f of strFields) {
|
||||||
if (body[f] !== undefined) data[f] = body[f] ? String(body[f]) : null;
|
if (body[f] !== undefined) data[f] = body[f] ? String(body[f]) : null;
|
||||||
|
|||||||
Reference in New Issue
Block a user