From bce02808460014392701a2f0b36fbc771ac38e8c Mon Sep 17 00:00:00 2001 From: BOHA Date: Tue, 16 Jun 2026 20:15:26 +0200 Subject: [PATCH] feat(documents): PDF prints only the document's selected company custom fields Co-Authored-By: Claude Opus 4.8 (1M context) --- src/__tests__/selected-custom-fields.test.ts | 56 ++++++++++++++++++++ src/routes/admin/invoices-pdf.ts | 14 ++++- src/routes/admin/issued-orders-pdf.ts | 13 ++++- src/routes/admin/offers-pdf.ts | 8 +++ 4 files changed, 89 insertions(+), 2 deletions(-) diff --git a/src/__tests__/selected-custom-fields.test.ts b/src/__tests__/selected-custom-fields.test.ts index 00f707d..0bfdaf3 100644 --- a/src/__tests__/selected-custom-fields.test.ts +++ b/src/__tests__/selected-custom-fields.test.ts @@ -5,6 +5,9 @@ import { } from "../utils/custom-fields"; import { afterAll } from "vitest"; import { createOffer, getOffer } from "../services/offers.service"; +import { renderOfferHtml } from "../routes/admin/offers-pdf"; +import type { OfferForPdf } from "../routes/admin/offers-pdf"; +import type { company_settings } from "@prisma/client"; import prisma from "../config/database"; describe("selected custom fields encode/decode", () => { @@ -71,3 +74,56 @@ describe("offers selected_custom_fields round-trip", () => { expect(row?.selected_custom_fields).toBeNull(); }); }); + +describe("offer PDF prints only selected company custom fields", () => { + // Two company custom fields; the document selects only index 0. + const settings = { + company_name: "Test s.r.o.", + custom_fields: JSON.stringify({ + fields: [ + { name: "Email", value: "selected@example.com", showLabel: false }, + { name: "Web", value: "notselected.example.com", showLabel: false }, + ], + field_order: [], + }), + logo_data: null, + } as unknown as company_settings; + + const baseQuotation = { + quotation_number: "TEST-2098-001", + language: "EN", + currency: "EUR", + valid_until: new Date("2098-01-01"), + project_code: null, + created_at: new Date("2098-01-01"), + customers: { name: "Cust" }, + quotation_items: [], + scope_sections: [], + }; + + it("renders the selected custom field and omits the non-selected one", () => { + const html = renderOfferHtml( + { + ...baseQuotation, + selected_custom_fields: "[0]", + } as unknown as OfferForPdf, + settings, + ); + expect(html).toContain("selected@example.com"); + expect(html).not.toContain("notselected.example.com"); + }); + + it("prints NO company custom fields when the document selects none", () => { + // The company call site always passes a parsed array; a null column + // decodes to [] → empty selection → no custom fields on the document. + const html = renderOfferHtml( + { + ...baseQuotation, + selected_custom_fields: null, + } as unknown as OfferForPdf, + settings, + ); + expect(html).not.toContain("selected@example.com"); + expect(html).not.toContain("notselected.example.com"); + }); +}); diff --git a/src/routes/admin/invoices-pdf.ts b/src/routes/admin/invoices-pdf.ts index 59ee18a..70f6d0b 100644 --- a/src/routes/admin/invoices-pdf.ts +++ b/src/routes/admin/invoices-pdf.ts @@ -8,6 +8,7 @@ import { getRate } from "../../services/exchange-rates"; import { localDateStr } from "../../utils/date"; import { parseId, success } from "../../utils/response"; import { lineNet } from "../../utils/money"; +import { parseSelectedCustomFields } from "../../utils/custom-fields"; import { formatDate, formatNum, @@ -34,6 +35,7 @@ function buildAddressLines( entity: Record | null, isSupplier: boolean, tObj: Record, + selectedCustomFields?: number[], ): AddressResult { if (!entity) return { name: "", lines: [] }; @@ -89,7 +91,9 @@ function buildAddressLines( fieldMap.company_id = `${tObj.ico}${entity.company_id}`; if (entity.vat_id) fieldMap.vat_id = `${tObj.dic}${entity.vat_id}`; + const filterCustom = Array.isArray(selectedCustomFields); cfData.forEach((cf, i) => { + if (filterCustom && !selectedCustomFields!.includes(i)) return; const cfName = (cf.name || "").trim(); const cfValue = (cf.value || "").trim(); const showLabel = cf.showLabel !== false; @@ -454,7 +458,15 @@ export default async function invoicesPdfRoutes( } } - const supp = buildAddressLines(settings, true, t); + const supp = buildAddressLines( + settings, + true, + t, + parseSelectedCustomFields( + (invoice as { selected_custom_fields?: unknown }) + .selected_custom_fields, + ), + ); const cust = buildAddressLines(customer, false, t); const suppLinesHtml = supp.lines diff --git a/src/routes/admin/issued-orders-pdf.ts b/src/routes/admin/issued-orders-pdf.ts index 79e5e78..bc94e0d 100644 --- a/src/routes/admin/issued-orders-pdf.ts +++ b/src/routes/admin/issued-orders-pdf.ts @@ -4,6 +4,7 @@ import { requirePermission } from "../../middleware/auth"; import { parseId, success } from "../../utils/response"; import { htmlToPdf } from "../../utils/html-to-pdf"; import { nasOrdersManager } from "../../services/nas-financials-manager"; +import { parseSelectedCustomFields } from "../../utils/custom-fields"; import { formatDate, formatNum, @@ -31,6 +32,7 @@ function buildAddressLines( entity: Record | null, isCompany: boolean, tObj: Record, + selectedCustomFields?: number[], ): AddressResult { if (!entity) return { name: "", lines: [] }; @@ -86,7 +88,9 @@ function buildAddressLines( fieldMap.company_id = `${tObj.ico}${entity.company_id}`; if (entity.vat_id) fieldMap.vat_id = `${tObj.dic}${entity.vat_id}`; + const filterCustom = Array.isArray(selectedCustomFields); cfData.forEach((cf, i) => { + if (filterCustom && !selectedCustomFields!.includes(i)) return; const cfName = (cf.name || "").trim(); const cfValue = (cf.value || "").trim(); const showLabel = cf.showLabel !== false; @@ -313,7 +317,14 @@ export function renderIssuedOrderHtml( // PO direction: our company (settings) = Odběratel (buyer); // the sklad_suppliers record = Dodavatel (supplier). - const buyer = buildAddressLines(settings, true, t); // company → Odběratel + const buyer = buildAddressLines( + settings, + true, + t, + parseSelectedCustomFields( + (order as { selected_custom_fields?: unknown }).selected_custom_fields, + ), + ); // company → Odběratel const supplierAddr = buildSupplierLines(supplier, t); // supplier → Dodavatel const buyerLinesHtml = buyer.lines diff --git a/src/routes/admin/offers-pdf.ts b/src/routes/admin/offers-pdf.ts index 7010def..b388de9 100644 --- a/src/routes/admin/offers-pdf.ts +++ b/src/routes/admin/offers-pdf.ts @@ -6,6 +6,7 @@ import { nasOffersManager } from "../../services/nas-offers-manager"; import { htmlToPdf } from "../../utils/html-to-pdf"; import { parseId, success } from "../../utils/response"; import { lineNet } from "../../utils/money"; +import { parseSelectedCustomFields } from "../../utils/custom-fields"; import { formatDate, formatNum, @@ -29,6 +30,7 @@ function buildAddressLines( entity: Record | null, isSupplier: boolean, t: (key: string) => string, + selectedCustomFields?: number[], ): AddressResult { if (!entity) return { name: "", lines: [] }; @@ -85,7 +87,9 @@ function buildAddressLines( fieldMap.company_id = `${t("ico")}: ${entity.company_id}`; if (entity.vat_id) fieldMap.vat_id = `${t("dic")}: ${entity.vat_id}`; + const filterCustom = Array.isArray(selectedCustomFields); cfData.forEach((cf, i) => { + if (filterCustom && !selectedCustomFields!.includes(i)) return; const cfName = (cf.name || "").trim(); const cfValue = (cf.value || "").trim(); const showLabel = cf.showLabel !== false; @@ -210,6 +214,10 @@ export function renderOfferHtml( settings as unknown as Record, true, t, + parseSelectedCustomFields( + (quotation as { selected_custom_fields?: unknown }) + .selected_custom_fields, + ), ); const custLinesHtml = cust.lines