feat(documents): per-item Sleva (% discount) on offers + invoices
Adds an optional per-line-item percentage discount ("Sleva") to offers and
issued invoices, on both the detail-page editor and the PDF.
- DB: discount Decimal(5,2) DEFAULT 0 on quotation_items + invoice_items
(migration 20260613195833_add_item_discount).
- Totals/VAT: new shared lineNet() (qty × price × (1 − discount/100)); offers'
NET total and invoices' subtotal AND VAT now compute on the discounted net
(every getBase/enrichQuotation/stats path). Backward compatible: discount 0
is identical to before.
- Schemas: discount = numberInRange(0,100), default 0; persisted on
create/update (+ offer duplicate).
- Detail editors: editable "Sleva %" column — offers via DocumentItemsEditor's
new opt-in showDiscount prop (issued orders unaffected); invoices in their own
editor. Spinners hidden + 7rem column so "100" is fully visible. Read-only
invoice view shows the column when any line has a discount.
- PDFs (offers + invoices): conditional "Sleva"/"Discount" column rendered only
when an item has a discount; line/total/VAT always use the discounted net.
Tests: +16 (money, totals incl. VAT-on-discounted-net, schema range, PDF
column present/absent). Full suite 665 pass, tsc -b clean, lint 0. Editor
verified live via Playwright. Design spec in docs/superpowers/specs/.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -93,6 +93,7 @@ interface OfferItemPayload {
|
||||
quantity: number;
|
||||
unit: string;
|
||||
unit_price: number;
|
||||
discount: number;
|
||||
is_included_in_total: boolean;
|
||||
position: number;
|
||||
}
|
||||
@@ -303,6 +304,7 @@ export default function OfferDetail() {
|
||||
quantity: numberOr(it.quantity, 1),
|
||||
unit: it.unit || "",
|
||||
unit_price: Number(it.unit_price) || 0,
|
||||
discount: Number(it.discount) || 0,
|
||||
is_included_in_total: it.is_included_in_total !== false,
|
||||
}))
|
||||
: [emptyDocumentItem()];
|
||||
@@ -433,6 +435,7 @@ export default function OfferDetail() {
|
||||
quantity: Number(item.quantity) || 0,
|
||||
unit: item.unit,
|
||||
unit_price: Number(item.unit_price) || 0,
|
||||
discount: Number(item.discount) || 0,
|
||||
is_included_in_total: item.is_included_in_total !== false,
|
||||
position: i,
|
||||
})),
|
||||
@@ -936,6 +939,7 @@ export default function OfferDetail() {
|
||||
readOnly={readOnly}
|
||||
error={errors.items}
|
||||
showIncludedInTotal
|
||||
showDiscount
|
||||
itemDescriptionMaxLength={8000}
|
||||
templatesSlot={
|
||||
itemTemplates && itemTemplates.length > 0 ? (
|
||||
@@ -957,6 +961,7 @@ export default function OfferDetail() {
|
||||
quantity: 1,
|
||||
unit: "ks",
|
||||
unit_price: template.default_price || 0,
|
||||
discount: 0,
|
||||
is_included_in_total: true,
|
||||
},
|
||||
]);
|
||||
|
||||
Reference in New Issue
Block a user