feat(documents): PDF prints only the document's selected company custom fields
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,9 @@ import {
|
|||||||
} from "../utils/custom-fields";
|
} from "../utils/custom-fields";
|
||||||
import { afterAll } from "vitest";
|
import { afterAll } from "vitest";
|
||||||
import { createOffer, getOffer } from "../services/offers.service";
|
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";
|
import prisma from "../config/database";
|
||||||
|
|
||||||
describe("selected custom fields encode/decode", () => {
|
describe("selected custom fields encode/decode", () => {
|
||||||
@@ -71,3 +74,56 @@ describe("offers selected_custom_fields round-trip", () => {
|
|||||||
expect(row?.selected_custom_fields).toBeNull();
|
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");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { getRate } from "../../services/exchange-rates";
|
|||||||
import { localDateStr } from "../../utils/date";
|
import { localDateStr } from "../../utils/date";
|
||||||
import { parseId, success } from "../../utils/response";
|
import { parseId, success } from "../../utils/response";
|
||||||
import { lineNet } from "../../utils/money";
|
import { lineNet } from "../../utils/money";
|
||||||
|
import { parseSelectedCustomFields } from "../../utils/custom-fields";
|
||||||
import {
|
import {
|
||||||
formatDate,
|
formatDate,
|
||||||
formatNum,
|
formatNum,
|
||||||
@@ -34,6 +35,7 @@ function buildAddressLines(
|
|||||||
entity: Record<string, unknown> | null,
|
entity: Record<string, unknown> | null,
|
||||||
isSupplier: boolean,
|
isSupplier: boolean,
|
||||||
tObj: Record<string, string>,
|
tObj: Record<string, string>,
|
||||||
|
selectedCustomFields?: number[],
|
||||||
): AddressResult {
|
): AddressResult {
|
||||||
if (!entity) return { name: "", lines: [] };
|
if (!entity) return { name: "", lines: [] };
|
||||||
|
|
||||||
@@ -89,7 +91,9 @@ function buildAddressLines(
|
|||||||
fieldMap.company_id = `${tObj.ico}${entity.company_id}`;
|
fieldMap.company_id = `${tObj.ico}${entity.company_id}`;
|
||||||
if (entity.vat_id) fieldMap.vat_id = `${tObj.dic}${entity.vat_id}`;
|
if (entity.vat_id) fieldMap.vat_id = `${tObj.dic}${entity.vat_id}`;
|
||||||
|
|
||||||
|
const filterCustom = Array.isArray(selectedCustomFields);
|
||||||
cfData.forEach((cf, i) => {
|
cfData.forEach((cf, i) => {
|
||||||
|
if (filterCustom && !selectedCustomFields!.includes(i)) return;
|
||||||
const cfName = (cf.name || "").trim();
|
const cfName = (cf.name || "").trim();
|
||||||
const cfValue = (cf.value || "").trim();
|
const cfValue = (cf.value || "").trim();
|
||||||
const showLabel = cf.showLabel !== false;
|
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 cust = buildAddressLines(customer, false, t);
|
||||||
|
|
||||||
const suppLinesHtml = supp.lines
|
const suppLinesHtml = supp.lines
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { requirePermission } from "../../middleware/auth";
|
|||||||
import { parseId, success } from "../../utils/response";
|
import { parseId, success } from "../../utils/response";
|
||||||
import { htmlToPdf } from "../../utils/html-to-pdf";
|
import { htmlToPdf } from "../../utils/html-to-pdf";
|
||||||
import { nasOrdersManager } from "../../services/nas-financials-manager";
|
import { nasOrdersManager } from "../../services/nas-financials-manager";
|
||||||
|
import { parseSelectedCustomFields } from "../../utils/custom-fields";
|
||||||
import {
|
import {
|
||||||
formatDate,
|
formatDate,
|
||||||
formatNum,
|
formatNum,
|
||||||
@@ -31,6 +32,7 @@ function buildAddressLines(
|
|||||||
entity: Record<string, unknown> | null,
|
entity: Record<string, unknown> | null,
|
||||||
isCompany: boolean,
|
isCompany: boolean,
|
||||||
tObj: Record<string, string>,
|
tObj: Record<string, string>,
|
||||||
|
selectedCustomFields?: number[],
|
||||||
): AddressResult {
|
): AddressResult {
|
||||||
if (!entity) return { name: "", lines: [] };
|
if (!entity) return { name: "", lines: [] };
|
||||||
|
|
||||||
@@ -86,7 +88,9 @@ function buildAddressLines(
|
|||||||
fieldMap.company_id = `${tObj.ico}${entity.company_id}`;
|
fieldMap.company_id = `${tObj.ico}${entity.company_id}`;
|
||||||
if (entity.vat_id) fieldMap.vat_id = `${tObj.dic}${entity.vat_id}`;
|
if (entity.vat_id) fieldMap.vat_id = `${tObj.dic}${entity.vat_id}`;
|
||||||
|
|
||||||
|
const filterCustom = Array.isArray(selectedCustomFields);
|
||||||
cfData.forEach((cf, i) => {
|
cfData.forEach((cf, i) => {
|
||||||
|
if (filterCustom && !selectedCustomFields!.includes(i)) return;
|
||||||
const cfName = (cf.name || "").trim();
|
const cfName = (cf.name || "").trim();
|
||||||
const cfValue = (cf.value || "").trim();
|
const cfValue = (cf.value || "").trim();
|
||||||
const showLabel = cf.showLabel !== false;
|
const showLabel = cf.showLabel !== false;
|
||||||
@@ -313,7 +317,14 @@ export function renderIssuedOrderHtml(
|
|||||||
|
|
||||||
// PO direction: our company (settings) = Odběratel (buyer);
|
// PO direction: our company (settings) = Odběratel (buyer);
|
||||||
// the sklad_suppliers record = Dodavatel (supplier).
|
// 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 supplierAddr = buildSupplierLines(supplier, t); // supplier → Dodavatel
|
||||||
|
|
||||||
const buyerLinesHtml = buyer.lines
|
const buyerLinesHtml = buyer.lines
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { nasOffersManager } from "../../services/nas-offers-manager";
|
|||||||
import { htmlToPdf } from "../../utils/html-to-pdf";
|
import { htmlToPdf } from "../../utils/html-to-pdf";
|
||||||
import { parseId, success } from "../../utils/response";
|
import { parseId, success } from "../../utils/response";
|
||||||
import { lineNet } from "../../utils/money";
|
import { lineNet } from "../../utils/money";
|
||||||
|
import { parseSelectedCustomFields } from "../../utils/custom-fields";
|
||||||
import {
|
import {
|
||||||
formatDate,
|
formatDate,
|
||||||
formatNum,
|
formatNum,
|
||||||
@@ -29,6 +30,7 @@ function buildAddressLines(
|
|||||||
entity: Record<string, unknown> | null,
|
entity: Record<string, unknown> | null,
|
||||||
isSupplier: boolean,
|
isSupplier: boolean,
|
||||||
t: (key: string) => string,
|
t: (key: string) => string,
|
||||||
|
selectedCustomFields?: number[],
|
||||||
): AddressResult {
|
): AddressResult {
|
||||||
if (!entity) return { name: "", lines: [] };
|
if (!entity) return { name: "", lines: [] };
|
||||||
|
|
||||||
@@ -85,7 +87,9 @@ function buildAddressLines(
|
|||||||
fieldMap.company_id = `${t("ico")}: ${entity.company_id}`;
|
fieldMap.company_id = `${t("ico")}: ${entity.company_id}`;
|
||||||
if (entity.vat_id) fieldMap.vat_id = `${t("dic")}: ${entity.vat_id}`;
|
if (entity.vat_id) fieldMap.vat_id = `${t("dic")}: ${entity.vat_id}`;
|
||||||
|
|
||||||
|
const filterCustom = Array.isArray(selectedCustomFields);
|
||||||
cfData.forEach((cf, i) => {
|
cfData.forEach((cf, i) => {
|
||||||
|
if (filterCustom && !selectedCustomFields!.includes(i)) return;
|
||||||
const cfName = (cf.name || "").trim();
|
const cfName = (cf.name || "").trim();
|
||||||
const cfValue = (cf.value || "").trim();
|
const cfValue = (cf.value || "").trim();
|
||||||
const showLabel = cf.showLabel !== false;
|
const showLabel = cf.showLabel !== false;
|
||||||
@@ -210,6 +214,10 @@ export function renderOfferHtml(
|
|||||||
settings as unknown as Record<string, unknown>,
|
settings as unknown as Record<string, unknown>,
|
||||||
true,
|
true,
|
||||||
t,
|
t,
|
||||||
|
parseSelectedCustomFields(
|
||||||
|
(quotation as { selected_custom_fields?: unknown })
|
||||||
|
.selected_custom_fields,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const custLinesHtml = cust.lines
|
const custLinesHtml = cust.lines
|
||||||
|
|||||||
Reference in New Issue
Block a user