feat(odin): document detail tools — line items for offers, orders, invoices, projects
Five read-only detail tools (offer, order, issued order, invoice, project) looked up by document number (partial match). They call the same getX(id) services the detail routes use under the same .view permissions (parity verified route-by-route), and return compact payloads: line items capped at 50 rows with full totals, is_included_in_total respected, rich text stripped to plain excerpts, invoice VAT broken down per the editor rule (per-item rate + apply_vat). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -176,6 +176,57 @@ beforeAll(async () => {
|
||||
});
|
||||
});
|
||||
|
||||
const FIX_OFFER_NUMBER = "2098/NA/99999";
|
||||
let fixOfferId = 0;
|
||||
|
||||
beforeAll(async () => {
|
||||
await prisma.quotations.deleteMany({
|
||||
where: { quotation_number: FIX_OFFER_NUMBER },
|
||||
});
|
||||
const q = await prisma.quotations.create({
|
||||
data: {
|
||||
quotation_number: FIX_OFFER_NUMBER,
|
||||
status: "active",
|
||||
currency: "EUR",
|
||||
quotation_items: {
|
||||
create: [
|
||||
{
|
||||
position: 1,
|
||||
description: "Rozvaděč RVO-1",
|
||||
item_description: "<p>Hlavní <b>rozvaděč</b> včetně montáže</p>",
|
||||
quantity: 2,
|
||||
unit: "ks",
|
||||
unit_price: 1000,
|
||||
},
|
||||
{
|
||||
position: 2,
|
||||
description: "Doprava (informativní)",
|
||||
quantity: 1,
|
||||
unit: "ks",
|
||||
unit_price: 500,
|
||||
is_included_in_total: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
scope_sections: {
|
||||
create: [
|
||||
{
|
||||
position: 1,
|
||||
title_cz: "Rozsah projektu",
|
||||
content: "<p>Dodávka & montáž <i>na klíč</i>.</p>",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
fixOfferId = q.id;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// Items/sections cascade with the quotation.
|
||||
await prisma.quotations.deleteMany({ where: { id: fixOfferId } });
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// FK-safe order: plan entries carry a Restrict FK on created_by.
|
||||
const fixUserIds = [fixUserId, fixUser2Id];
|
||||
@@ -410,6 +461,56 @@ describe("ai-tools — employees, attendance detail, trips, work plan", () => {
|
||||
expect(o.total_km).toBe(52); // foreign 100km trip excluded from totals too
|
||||
});
|
||||
|
||||
it("get_offer_detail returns line items, totals and stripped sections", async () => {
|
||||
const res = await executeTool(
|
||||
"get_offer_detail",
|
||||
{ number: "99999" }, // partial number lookup
|
||||
ADMIN,
|
||||
);
|
||||
expect(res.ok).toBe(true);
|
||||
const r = res.result as {
|
||||
number: string;
|
||||
currency: string;
|
||||
item_count: number;
|
||||
items_total: number;
|
||||
items: {
|
||||
name: string;
|
||||
detail?: string;
|
||||
line_total: number;
|
||||
excluded_from_total?: boolean;
|
||||
}[];
|
||||
sections: { title: string | null; text: string }[];
|
||||
};
|
||||
expect(r.number).toBe(FIX_OFFER_NUMBER);
|
||||
expect(r.currency).toBe("EUR");
|
||||
expect(r.item_count).toBe(2);
|
||||
expect(r.items[0]).toMatchObject({
|
||||
name: "Rozvaděč RVO-1",
|
||||
detail: "Hlavní rozvaděč včetně montáže", // HTML stripped
|
||||
line_total: 2000,
|
||||
});
|
||||
expect(r.items[1].excluded_from_total).toBe(true);
|
||||
expect(r.items_total).toBe(2000); // excluded row not counted
|
||||
expect(r.sections[0]).toMatchObject({ title: "Rozsah projektu" });
|
||||
expect(r.sections[0].text).toContain("Dodávka & montáž na klíč");
|
||||
|
||||
// Unknown number → explicit Czech error, flagged not-ok.
|
||||
const miss = await executeTool(
|
||||
"get_offer_detail",
|
||||
{ number: "NEEXISTUJE-123" },
|
||||
ADMIN,
|
||||
);
|
||||
expect(miss.ok).toBe(false);
|
||||
|
||||
// No offers.view → denied.
|
||||
const denied = await executeTool(
|
||||
"get_offer_detail",
|
||||
{ number: "99999" },
|
||||
NOBODY,
|
||||
);
|
||||
expect(denied.ok).toBe(false);
|
||||
});
|
||||
|
||||
it("get_work_plan resolves the range-entry into per-day rows", async () => {
|
||||
const res = await executeTool(
|
||||
"get_work_plan",
|
||||
|
||||
Reference in New Issue
Block a user