feat: Implement free issue tracking and scheme discount handling in invoices and Shop POs

This commit is contained in:
2026-10-22 16:39:00 +05:30
parent 801276559d
commit 87e3112a9a
4 changed files with 172 additions and 25 deletions
@@ -34,9 +34,11 @@ const statusLabel = (invoice: InvoiceResponse) =>
// Removed lines stay on the document (struck through) but are excluded from every total.
function totals(invoice: InvoiceResponse, payments: PaymentResponse[]) {
const total = invoice.items.filter((i) => !i.isRemoved).reduce((s, i) => s + i.quantity * i.unitPrice, 0)
const subtotal = invoice.items.filter((i) => !i.isRemoved).reduce((s, i) => s + i.quantity * i.unitPrice, 0)
const discount = Math.min(invoice.discountAmount ?? 0, subtotal)
const total = subtotal - discount
const paid = payments.reduce((s, p) => s + p.amount, 0)
return { total, paid, balance: total - paid }
return { subtotal, discount, total, paid, balance: total - paid }
}
export function InvoiceTemplate({ paper = 'A4', ...props }: InvoiceTemplateProps) {
@@ -56,9 +58,12 @@ function A4Sheet({
showAgentEdits,
className,
}: Omit<InvoiceTemplateProps, 'paper'>) {
const { total, paid, balance } = totals(invoice, payments)
const { subtotal, discount, total, paid, balance } = totals(invoice, payments)
const status = statusLabel(invoice)
const hasRemoved = invoice.items.some((i) => i.isRemoved)
const hasFree = invoice.items.some((i) => i.isFree)
// Free issues are listed after every paid line.
const rows = [...invoice.items].sort((a, b) => Number(a.isFree) - Number(b.isFree))
return (
<div
@@ -107,6 +112,7 @@ function A4Sheet({
<tr className="border-b border-neutral-300 text-xs uppercase tracking-wide text-neutral-500">
<th className="px-2 py-2 text-left font-medium">Item</th>
<th className="w-24 px-2 py-2 text-right font-medium">Qty</th>
{hasFree && <th className="w-24 px-2 py-2 text-right font-medium">Free Issue</th>}
<th className="w-36 px-2 py-2 text-right font-medium">Unit Price</th>
<th className="w-40 px-2 py-2 text-right font-medium">Amount</th>
{renderActions && <th className="w-24 px-2 py-2 print:hidden" />}
@@ -118,15 +124,25 @@ function A4Sheet({
<td colSpan={4} className="px-2 py-6 text-center text-neutral-500">No items.</td>
</tr>
) : (
invoice.items.map((item) => (
rows.map((item) => (
<tr
key={item.id}
className={cn('break-inside-avoid border-b border-neutral-200', item.isRemoved && 'text-neutral-400 line-through')}
>
<td className="px-2 py-2.5">{item.itemName}</td>
<td className="px-2 py-2.5 text-right">
{renderQuantity && !item.isRemoved ? renderQuantity(item) : fmtQty(item.quantity)}
<td className="px-2 py-2.5">
{item.itemName}
{item.isFree && item.schemeName && (
<span className="block text-xs text-neutral-500 no-underline">Free issue · {item.schemeName}</span>
)}
</td>
<td className="px-2 py-2.5 text-right">
{item.isFree ? '-' : renderQuantity && !item.isRemoved ? renderQuantity(item) : fmtQty(item.quantity)}
</td>
{hasFree && (
<td className="px-2 py-2.5 text-right">
{item.isFree ? (renderQuantity && !item.isRemoved ? renderQuantity(item) : fmtQty(item.quantity)) : '-'}
</td>
)}
<td className="px-2 py-2.5 text-right">
{renderUnitPrice && !item.isRemoved ? renderUnitPrice(item) : fmtMoney(item.unitPrice)}
</td>
@@ -160,8 +176,14 @@ function A4Sheet({
<dl className="w-72 break-inside-avoid text-sm">
<div className="flex justify-between border-b py-1.5">
<dt className="text-neutral-500">Subtotal</dt>
<dd>{fmtMoney(total)}</dd>
<dd>{fmtMoney(subtotal)}</dd>
</div>
{discount > 0 && (
<div className="flex justify-between border-b py-1.5">
<dt className="text-neutral-500">Scheme discount</dt>
<dd>-{fmtMoney(discount)}</dd>
</div>
)}
<div className="flex justify-between border-y py-2 text-base font-bold" style={{ borderColor: RULE }}>
<dt>Total</dt>
<dd>{fmtMoney(total)}</dd>
@@ -225,7 +247,7 @@ function ReceiptSheet({
showAgentEdits,
className,
}: Omit<InvoiceTemplateProps, 'paper'> & { paper: Exclude<InvoicePaper, 'A4'> }) {
const { total, paid, balance } = totals(invoice, payments)
const { subtotal, discount, total, paid, balance } = totals(invoice, payments)
const status = statusLabel(invoice)
const narrow = paper === '58mm'
const rule = <div className="my-2 border-t border-dashed border-black" />
@@ -252,12 +274,21 @@ function ReceiptSheet({
</div>
{rule}
<div className="space-y-1.5">
{invoice.items.map((i) => (
{[...invoice.items].sort((a, b) => Number(a.isFree) - Number(b.isFree)).map((i) => (
<div key={i.id} className={cn(i.isRemoved && 'line-through opacity-60')}>
<p>{i.itemName}</p>
<p className="flex justify-between">
<span>{fmtQty(i.quantity)} x {num(i.unitPrice)}</span>
<span>{num(i.quantity * i.unitPrice)}</span>
{i.isFree ? (
<>
<span>{fmtQty(i.quantity)} FREE{i.schemeName ? ` (${i.schemeName})` : ''}</span>
<span>{num(0)}</span>
</>
) : (
<>
<span>{fmtQty(i.quantity)} x {num(i.unitPrice)}</span>
<span>{num(i.quantity * i.unitPrice)}</span>
</>
)}
</p>
</div>
))}
@@ -265,8 +296,14 @@ function ReceiptSheet({
{rule}
<p className="flex justify-between">
<span>Subtotal</span>
<span>{fmtMoney(total)}</span>
<span>{fmtMoney(subtotal)}</span>
</p>
{discount > 0 && (
<p className="flex justify-between">
<span>Scheme discount</span>
<span>-{fmtMoney(discount)}</span>
</p>
)}
<div className="my-1 border-y-2 border-black py-1 text-sm font-bold">
<p className="flex justify-between">
<span>TOTAL</span>