Rechnungen: Rabatt je Position
This commit is contained in:
parent
e7e5f31ebe
commit
96df0c334a
5 changed files with 51 additions and 22 deletions
|
|
@ -15,8 +15,8 @@ import { renderInvoicePdf, type CompanySettings, type InvoiceForPdf } from './pd
|
|||
/** Kaufmännisches Runden (halb auf), wie in @kc/platform/pricing – hier lokal, weil Rechnungspositionen
|
||||
* (Freitext, Dezimalmenge) sich nicht in das Produkt-Preisschema von calculatePrice pressen lassen. */
|
||||
const divRound = (n: number, d: number): number => Math.floor((n * 2 + d) / (d * 2));
|
||||
function lineAmounts(unitNetCents: number, quantity: number, taxBp: number) {
|
||||
const net = Math.round(unitNetCents * quantity);
|
||||
function lineAmounts(unitNetCents: number, quantity: number, taxBp: number, discountBp = 0) {
|
||||
const net = divRound(Math.round(unitNetCents * quantity) * (10000 - discountBp), 10000);
|
||||
const tax = divRound(net * taxBp, 10000);
|
||||
return { net, tax, gross: net + tax };
|
||||
}
|
||||
|
|
@ -36,7 +36,7 @@ async function settings(): Promise<CompanySettings> {
|
|||
}
|
||||
const complete = (s: CompanySettings) => !!(s.name && s.street && s.zip && s.city && (s.taxNumber || s.vatId));
|
||||
|
||||
const itemView = (i: any) => ({ id: i.id, contractId: i.contract_id, description: i.description, quantity: Number(i.quantity), unitPriceNetCents: i.unit_price_net_cents, taxBp: i.tax_bp, netCents: i.net_cents, taxCents: i.tax_cents, grossCents: i.gross_cents });
|
||||
const itemView = (i: any) => ({ id: i.id, contractId: i.contract_id, description: i.description, quantity: Number(i.quantity), unitPriceNetCents: i.unit_price_net_cents, taxBp: i.tax_bp, discountBp: i.discount_bp, netCents: i.net_cents, taxCents: i.tax_cents, grossCents: i.gross_cents });
|
||||
const invoiceView = (v: any) => ({
|
||||
id: v.id, number: v.number, orgId: v.org_id, orgName: v.org_name, customerNumber: v.customer_number, status: v.status,
|
||||
issueDate: v.issue_date, dueDate: v.due_date, overdue: v.status === 'open' && v.due_date && new Date(v.due_date) < new Date(),
|
||||
|
|
@ -137,7 +137,7 @@ export const invoicesModule: KcModule = {
|
|||
app.put('/invoices/:id/items', async (req) => {
|
||||
const a = requirePermission(req, 'invoices.write'); const { id } = z.object({ id: z.string().uuid() }).parse(req.params);
|
||||
const b = z.object({ items: z.array(z.object({
|
||||
description: z.string().trim().min(1).max(300), quantity: z.number().positive().max(100000), unitPriceNetCents: z.number().int().min(0).max(100_000_00), taxBp: z.number().int().min(0).max(3000), contractId: z.string().uuid().optional(),
|
||||
description: z.string().trim().min(1).max(300), quantity: z.number().positive().max(100000), unitPriceNetCents: z.number().int().min(0).max(100_000_00), taxBp: z.number().int().min(0).max(3000), discountBp: z.number().int().min(0).max(10000).default(0), contractId: z.string().uuid().optional(),
|
||||
})).min(1).max(100) }).parse(req.body);
|
||||
const v = await one('SELECT * FROM invoices WHERE id = ?', [id]); if (!v) throw notFound();
|
||||
if (v.status !== 'draft') throw conflict('Nur Entwürfe können bearbeitet werden. Ausgestellte Rechnungen sind unveränderlich (nur Storno möglich).', 'INVOICE_NOT_DRAFT');
|
||||
|
|
@ -145,9 +145,9 @@ export const invoicesModule: KcModule = {
|
|||
await tx(async (c) => {
|
||||
await run('DELETE FROM invoice_items WHERE invoice_id = ?', [id], c);
|
||||
for (const [idx, it] of b.items.entries()) {
|
||||
const a2 = lineAmounts(it.unitPriceNetCents, it.quantity, it.taxBp); net += a2.net; tax += a2.tax; gross += a2.gross;
|
||||
await run('INSERT INTO invoice_items (id, invoice_id, contract_id, description, quantity, unit_price_net_cents, tax_bp, net_cents, tax_cents, gross_cents, sort_order) VALUES (?,?,?,?,?,?,?,?,?,?,?)',
|
||||
[randomUUID(), id, it.contractId ?? null, it.description, it.quantity, it.unitPriceNetCents, it.taxBp, a2.net, a2.tax, a2.gross, idx], c);
|
||||
const a2 = lineAmounts(it.unitPriceNetCents, it.quantity, it.taxBp, it.discountBp); net += a2.net; tax += a2.tax; gross += a2.gross;
|
||||
await run('INSERT INTO invoice_items (id, invoice_id, contract_id, description, quantity, unit_price_net_cents, tax_bp, discount_bp, net_cents, tax_cents, gross_cents, sort_order) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)',
|
||||
[randomUUID(), id, it.contractId ?? null, it.description, it.quantity, it.unitPriceNetCents, it.taxBp, it.discountBp, a2.net, a2.tax, a2.gross, idx], c);
|
||||
}
|
||||
await run('UPDATE invoices SET total_net_cents = ?, total_tax_cents = ?, total_gross_cents = ? WHERE id = ?', [net, tax, gross, id], c);
|
||||
});
|
||||
|
|
@ -252,8 +252,8 @@ export const invoicesModule: KcModule = {
|
|||
const n = await nextInvoiceNumber(c, s.invoicePrefix);
|
||||
await run('INSERT INTO invoices (id, number, org_id, status, issue_date, due_date, total_net_cents, total_tax_cents, total_gross_cents, note, seller_snapshot_json, buyer_snapshot_json, cancels_invoice_id, created_by, issued_at) VALUES (?,?,?,\'open\',CURDATE(),CURDATE(),?,?,?,?,?,?,?,?,UTC_TIMESTAMP(3))',
|
||||
[creditId, n, v.org_id, -v.total_net_cents, -v.total_tax_cents, -v.total_gross_cents, b.reason ? `Storno zu ${v.number}: ${b.reason}` : `Storno zu ${v.number}`, jstr(locked.seller_snapshot_json), jstr(locked.buyer_snapshot_json), id, a.user.id], c);
|
||||
for (const it of items) await run('INSERT INTO invoice_items (id, invoice_id, contract_id, description, quantity, unit_price_net_cents, tax_bp, net_cents, tax_cents, gross_cents, sort_order) VALUES (?,?,?,?,?,?,?,?,?,?,?)',
|
||||
[randomUUID(), creditId, it.contract_id, it.description, -Number(it.quantity), it.unit_price_net_cents, it.tax_bp, -it.net_cents, -it.tax_cents, -it.gross_cents, it.sort_order], c);
|
||||
for (const it of items) await run('INSERT INTO invoice_items (id, invoice_id, contract_id, description, quantity, unit_price_net_cents, tax_bp, discount_bp, net_cents, tax_cents, gross_cents, sort_order) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)',
|
||||
[randomUUID(), creditId, it.contract_id, it.description, -Number(it.quantity), it.unit_price_net_cents, it.tax_bp, it.discount_bp, -it.net_cents, -it.tax_cents, -it.gross_cents, it.sort_order], c);
|
||||
await run("UPDATE invoices SET status = 'cancelled', cancelled_at = UTC_TIMESTAMP(3), cancelled_by_invoice_id = ? WHERE id = ?", [creditId, id], c);
|
||||
return n;
|
||||
});
|
||||
|
|
@ -291,7 +291,7 @@ export const invoicesModule: KcModule = {
|
|||
const inv: InvoiceForPdf = {
|
||||
number: res.v.number, issueDate: res.v.issue_date, dueDate: res.v.due_date, status: res.v.status, paymentMethod: res.v.payment_method, note: res.v.note,
|
||||
totalNetCents: res.v.total_net_cents, totalTaxCents: res.v.total_tax_cents, totalGrossCents: res.v.total_gross_cents,
|
||||
customer, items: res.items.map((i) => ({ description: i.description, quantity: Number(i.quantity), unitPriceNetCents: i.unit_price_net_cents, taxBp: i.tax_bp, netCents: i.net_cents, taxCents: i.tax_cents, grossCents: i.gross_cents })),
|
||||
customer, items: res.items.map((i) => ({ description: i.description, quantity: Number(i.quantity), unitPriceNetCents: i.unit_price_net_cents, taxBp: i.tax_bp, discountBp: i.discount_bp, netCents: i.net_cents, taxCents: i.tax_cents, grossCents: i.gross_cents })),
|
||||
};
|
||||
reply.header('content-type', 'application/pdf').header('content-disposition', `inline; filename="${res.v.number}.pdf"`);
|
||||
return reply.send(renderInvoicePdf(inv, seller));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue