feat(po): multi-account support per line item

Schema
- POLineItem.accountId String? — optional per-row account override
- Account.lineItems back-relation added
- Migration: 20260514185519

Line Items Editor
- New props: multiAccount, accounts, defaultAccountId
- When multiAccount=true: Account column appears in the editable table,
  each row pre-selected with defaultAccountId, independently changeable
- Read-only view: Account column shown when any line item carries an
  accountId (displays account code)

New & Edit PO forms
- "Multiple accounts" checkbox (default off) sits inline with the
  Account label
- When checked: label changes to "Default Account / Cost Centre";
  per-row dropdowns appear in the line items table pre-filled with
  the chosen default account
- Edit form: checkbox initialises to checked when the PO already has
  per-line accounts; existing accountId values are pre-populated
- accountId per line item is included in form data and persisted

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Hardik 2026-05-15 00:30:23 +05:30
parent 9d1496a3bf
commit e9ed0f8eb0
8 changed files with 136 additions and 24 deletions

View file

@ -18,6 +18,7 @@ function parseLineItems(formData: FormData) {
size: (formData.get(`lineItems[${i}].size`) as string) || undefined, size: (formData.get(`lineItems[${i}].size`) as string) || undefined,
unitPrice: Number(formData.get(`lineItems[${i}].unitPrice`)), unitPrice: Number(formData.get(`lineItems[${i}].unitPrice`)),
gstRate: Number(formData.get(`lineItems[${i}].gstRate`) ?? 0.18), gstRate: Number(formData.get(`lineItems[${i}].gstRate`) ?? 0.18),
accountId: (formData.get(`lineItems[${i}].accountId`) as string) || undefined,
}); });
i++; i++;
} }
@ -110,6 +111,7 @@ export async function updatePo(
totalPrice: item.quantity * item.unitPrice, totalPrice: item.quantity * item.unitPrice,
gstRate: item.gstRate, gstRate: item.gstRate,
sortOrder: idx, sortOrder: idx,
accountId: (item as { accountId?: string }).accountId ?? null,
})), })),
}, },
actions: { actions: {

View file

@ -24,6 +24,7 @@ type SerializedLineItem = {
gstRate: number; gstRate: number;
sortOrder: number; sortOrder: number;
productId: string | null; productId: string | null;
accountId: string | null;
}; };
type PoWithItems = Omit<PurchaseOrder, "totalAmount"> & { type PoWithItems = Omit<PurchaseOrder, "totalAmount"> & {
@ -49,10 +50,14 @@ export function EditPoForm({ po, vessels, accounts, vendors }: Props) {
size: li.size ?? undefined, size: li.size ?? undefined,
unitPrice: li.unitPrice, unitPrice: li.unitPrice,
gstRate: li.gstRate, gstRate: li.gstRate,
accountId: li.accountId ?? undefined,
})) }))
); );
const [submitting, setSubmitting] = useState<"save" | "resubmit" | null>(null); const [submitting, setSubmitting] = useState<"save" | "resubmit" | null>(null);
const [error, setError] = useState(""); const [error, setError] = useState("");
const hasPerLineAccounts = po.lineItems.some((li) => li.accountId);
const [multiAccount, setMultiAccount] = useState(hasPerLineAccounts);
const [defaultAccountId, setDefaultAccountId] = useState(po.accountId ?? "");
const canResubmit = po.status === "EDITS_REQUESTED"; const canResubmit = po.status === "EDITS_REQUESTED";
@ -70,6 +75,7 @@ export function EditPoForm({ po, vessels, accounts, vendors }: Props) {
data.set(`lineItems[${i}].size`, item.size ?? ""); data.set(`lineItems[${i}].size`, item.size ?? "");
data.set(`lineItems[${i}].unitPrice`, String(item.unitPrice)); data.set(`lineItems[${i}].unitPrice`, String(item.unitPrice));
data.set(`lineItems[${i}].gstRate`, String(item.gstRate ?? 0.18)); data.set(`lineItems[${i}].gstRate`, String(item.gstRate ?? 0.18));
if (multiAccount && item.accountId) data.set(`lineItems[${i}].accountId`, item.accountId);
}); });
const result = await updatePo(po.id, data); const result = await updatePo(po.id, data);
@ -124,10 +130,27 @@ export function EditPoForm({ po, vessels, accounts, vendors }: Props) {
</select> </select>
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-neutral-700 mb-1.5"> <div className="flex items-center justify-between mb-1.5">
Account / Cost Centre <span className="text-danger">*</span> <label className="text-sm font-medium text-neutral-700">
{multiAccount ? "Default Account" : "Account / Cost Centre"} <span className="text-danger">*</span>
</label> </label>
<select name="accountId" required defaultValue={po.accountId} className={INPUT_CLS}> <label className="flex items-center gap-1.5 text-xs text-neutral-500 cursor-pointer select-none">
<input
type="checkbox"
checked={multiAccount}
onChange={(e) => setMultiAccount(e.target.checked)}
className="rounded border-neutral-300"
/>
Multiple accounts
</label>
</div>
<select
name="accountId"
required
value={defaultAccountId}
onChange={(e) => setDefaultAccountId(e.target.value)}
className={INPUT_CLS}
>
<option value="">Select account</option> <option value="">Select account</option>
{accounts.map((a) => <option key={a.id} value={a.id}>{a.name} ({a.code})</option>)} {accounts.map((a) => <option key={a.id} value={a.id}>{a.name} ({a.code})</option>)}
</select> </select>
@ -185,7 +208,13 @@ export function EditPoForm({ po, vessels, accounts, vendors }: Props) {
{/* Line Items */} {/* Line Items */}
<section className="rounded-lg border border-neutral-200 bg-white p-6"> <section className="rounded-lg border border-neutral-200 bg-white p-6">
<h2 className="text-base font-semibold text-neutral-900 mb-4">Line Items</h2> <h2 className="text-base font-semibold text-neutral-900 mb-4">Line Items</h2>
<LineItemsEditor items={lineItems} onChange={setLineItems} /> <LineItemsEditor
items={lineItems}
onChange={setLineItems}
multiAccount={multiAccount}
accounts={accounts.map((a) => ({ id: a.id, name: a.name, code: a.code }))}
defaultAccountId={defaultAccountId || undefined}
/>
</section> </section>
{/* Vendor */} {/* Vendor */}

View file

@ -31,6 +31,7 @@ export async function createPo(
unitPrice: number; unitPrice: number;
gstRate: number; gstRate: number;
productId?: string; productId?: string;
accountId?: string;
}> = []; }> = [];
let i = 0; let i = 0;
while (formData.has(`lineItems[${i}].name`)) { while (formData.has(`lineItems[${i}].name`)) {
@ -43,6 +44,7 @@ export async function createPo(
unitPrice: Number(formData.get(`lineItems[${i}].unitPrice`)), unitPrice: Number(formData.get(`lineItems[${i}].unitPrice`)),
gstRate: Number(formData.get(`lineItems[${i}].gstRate`) ?? 0.18), gstRate: Number(formData.get(`lineItems[${i}].gstRate`) ?? 0.18),
productId: (formData.get(`lineItems[${i}].productId`) as string) || undefined, productId: (formData.get(`lineItems[${i}].productId`) as string) || undefined,
accountId: (formData.get(`lineItems[${i}].accountId`) as string) || undefined,
}); });
i++; i++;
} }
@ -116,6 +118,7 @@ export async function createPo(
gstRate: item.gstRate, gstRate: item.gstRate,
sortOrder: idx, sortOrder: idx,
productId: item.productId ?? null, productId: item.productId ?? null,
accountId: item.accountId ?? null,
})), })),
}, },
actions: { actions: {

View file

@ -27,6 +27,8 @@ export function NewPoForm({ vessels, accounts, vendors }: Props) {
const [files, setFiles] = useState<File[]>([]); const [files, setFiles] = useState<File[]>([]);
const [submitting, setSubmitting] = useState<"draft" | "submit" | null>(null); const [submitting, setSubmitting] = useState<"draft" | "submit" | null>(null);
const [error, setError] = useState(""); const [error, setError] = useState("");
const [multiAccount, setMultiAccount] = useState(false);
const [defaultAccountId, setDefaultAccountId] = useState("");
async function handleSubmit(intent: "draft" | "submit") { async function handleSubmit(intent: "draft" | "submit") {
setSubmitting(intent); setSubmitting(intent);
@ -43,6 +45,7 @@ export function NewPoForm({ vessels, accounts, vendors }: Props) {
data.set(`lineItems[${i}].unitPrice`, String(item.unitPrice)); data.set(`lineItems[${i}].unitPrice`, String(item.unitPrice));
data.set(`lineItems[${i}].gstRate`, String(item.gstRate ?? 0.18)); data.set(`lineItems[${i}].gstRate`, String(item.gstRate ?? 0.18));
if (item.productId) data.set(`lineItems[${i}].productId`, item.productId); if (item.productId) data.set(`lineItems[${i}].productId`, item.productId);
if (multiAccount && item.accountId) data.set(`lineItems[${i}].accountId`, item.accountId);
}); });
const result = await createPo(data); const result = await createPo(data);
@ -84,10 +87,27 @@ export function NewPoForm({ vessels, accounts, vendors }: Props) {
</select> </select>
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-neutral-700 mb-1.5"> <div className="flex items-center justify-between mb-1.5">
Account / Cost Centre <span className="text-danger">*</span> <label className="text-sm font-medium text-neutral-700">
{multiAccount ? "Default Account" : "Account / Cost Centre"} <span className="text-danger">*</span>
</label> </label>
<select name="accountId" required className={INPUT_CLS}> <label className="flex items-center gap-1.5 text-xs text-neutral-500 cursor-pointer select-none">
<input
type="checkbox"
checked={multiAccount}
onChange={(e) => setMultiAccount(e.target.checked)}
className="rounded border-neutral-300"
/>
Multiple accounts
</label>
</div>
<select
name="accountId"
required
value={defaultAccountId}
onChange={(e) => setDefaultAccountId(e.target.value)}
className={INPUT_CLS}
>
<option value="">Select account</option> <option value="">Select account</option>
{accounts.map((a) => <option key={a.id} value={a.id}>{a.name} ({a.code})</option>)} {accounts.map((a) => <option key={a.id} value={a.id}>{a.name} ({a.code})</option>)}
</select> </select>
@ -150,7 +170,13 @@ export function NewPoForm({ vessels, accounts, vendors }: Props) {
{/* Line Items */} {/* Line Items */}
<section className="rounded-lg border border-neutral-200 bg-white p-6"> <section className="rounded-lg border border-neutral-200 bg-white p-6">
<h2 className="text-base font-semibold text-neutral-900 mb-4">Line Items</h2> <h2 className="text-base font-semibold text-neutral-900 mb-4">Line Items</h2>
<LineItemsEditor items={lineItems} onChange={setLineItems} /> <LineItemsEditor
items={lineItems}
onChange={setLineItems}
multiAccount={multiAccount}
accounts={accounts.map((a) => ({ id: a.id, name: a.name, code: a.code }))}
defaultAccountId={defaultAccountId || undefined}
/>
</section> </section>
{/* Vendor */} {/* Vendor */}

View file

@ -33,11 +33,18 @@ type ProductHit = {
vendorPrices: { vendorId: string; vendorName: string; price: number }[]; vendorPrices: { vendorId: string; vendorName: string; price: number }[];
}; };
export type AccountOption = { id: string; name: string; code: string };
interface Props { interface Props {
items: LineItemInput[]; items: LineItemInput[];
onChange?: (items: LineItemInput[]) => void; onChange?: (items: LineItemInput[]) => void;
readOnly?: boolean; readOnly?: boolean;
originalItems?: LineItemInput[]; originalItems?: LineItemInput[];
/** When true, show per-row account selector */
multiAccount?: boolean;
accounts?: AccountOption[];
/** The PO-level default account id — pre-selected in each row dropdown */
defaultAccountId?: string;
} }
type EditRow = { type EditRow = {
@ -49,6 +56,7 @@ type EditRow = {
unitPrice: string; unitPrice: string;
gstRate: string; gstRate: string;
productId?: string; productId?: string;
accountId?: string;
}; };
function toEditRow(item: LineItemInput): EditRow { function toEditRow(item: LineItemInput): EditRow {
@ -61,6 +69,7 @@ function toEditRow(item: LineItemInput): EditRow {
unitPrice: item.unitPrice ? String(item.unitPrice) : "", unitPrice: item.unitPrice ? String(item.unitPrice) : "",
gstRate: String(item.gstRate ?? 0.18), gstRate: String(item.gstRate ?? 0.18),
productId: item.productId, productId: item.productId,
accountId: item.accountId,
}; };
} }
@ -74,6 +83,7 @@ function toLineItem(row: EditRow): LineItemInput {
unitPrice: parseFloat(row.unitPrice) || 0, unitPrice: parseFloat(row.unitPrice) || 0,
gstRate: parseFloat(row.gstRate) || 0.18, gstRate: parseFloat(row.gstRate) || 0.18,
productId: row.productId || undefined, productId: row.productId || undefined,
accountId: row.accountId || undefined,
}; };
} }
@ -187,7 +197,15 @@ function NameCell({
); );
} }
export function LineItemsEditor({ items, onChange, readOnly = false, originalItems }: Props) { export function LineItemsEditor({
items,
onChange,
readOnly = false,
originalItems,
multiAccount = false,
accounts = [],
defaultAccountId,
}: Props) {
const [rows, setRows] = useState<EditRow[]>(() => items.map(toEditRow)); const [rows, setRows] = useState<EditRow[]>(() => items.map(toEditRow));
function updateRows(next: EditRow[]) { function updateRows(next: EditRow[]) {
@ -215,16 +233,23 @@ export function LineItemsEditor({ items, onChange, readOnly = false, originalIte
} }
function add() { function add() {
updateRows([...rows, { name: "", description: "", quantity: "1", unit: "pc", size: "", unitPrice: "", gstRate: "0.18" }]); updateRows([...rows, {
name: "", description: "", quantity: "1", unit: "pc", size: "", unitPrice: "", gstRate: "0.18",
accountId: defaultAccountId,
}]);
} }
function remove(index: number) { function remove(index: number) {
updateRows(rows.filter((_, i) => i !== index)); updateRows(rows.filter((_, i) => i !== index));
} }
const accountMap = Object.fromEntries(accounts.map((a) => [a.id, a]));
// ── Read-only view ───────────────────────────────────────────────────────────
if (readOnly) { if (readOnly) {
const hasSize = items.some((item) => item.size); const hasSize = items.some((item) => item.size);
const hasDiff = originalItems && originalItems.length > 0; const hasDiff = originalItems && originalItems.length > 0;
const showAccount = items.some((item) => item.accountId);
const { taxable, gst, grand } = calcTotals(items); const { taxable, gst, grand } = calcTotals(items);
return ( return (
@ -245,6 +270,7 @@ export function LineItemsEditor({ items, onChange, readOnly = false, originalIte
<th className="pb-2 text-right font-medium text-neutral-600 pl-4 whitespace-nowrap">Taxable</th> <th className="pb-2 text-right font-medium text-neutral-600 pl-4 whitespace-nowrap">Taxable</th>
<th className="pb-2 text-right font-medium text-neutral-600 pl-4 whitespace-nowrap">GST%</th> <th className="pb-2 text-right font-medium text-neutral-600 pl-4 whitespace-nowrap">GST%</th>
<th className="pb-2 text-right font-medium text-neutral-600 pl-4 whitespace-nowrap">Total</th> <th className="pb-2 text-right font-medium text-neutral-600 pl-4 whitespace-nowrap">Total</th>
{showAccount && <th className="pb-2 text-left font-medium text-neutral-600 pl-4 whitespace-nowrap">Account</th>}
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-neutral-100"> <tbody className="divide-y divide-neutral-100">
@ -255,6 +281,7 @@ export function LineItemsEditor({ items, onChange, readOnly = false, originalIte
const nameChanged = orig && orig.name !== item.name; const nameChanged = orig && orig.name !== item.name;
const taxableAmt = item.quantity * item.unitPrice; const taxableAmt = item.quantity * item.unitPrice;
const gstAmt = taxableAmt * (item.gstRate ?? 0.18); const gstAmt = taxableAmt * (item.gstRate ?? 0.18);
const acct = item.accountId ? accountMap[item.accountId] : null;
return ( return (
<tr key={i}> <tr key={i}>
<td className="py-2 pr-4"> <td className="py-2 pr-4">
@ -277,21 +304,26 @@ export function LineItemsEditor({ items, onChange, readOnly = false, originalIte
<td className="py-2 pl-4 text-right">{formatCurrency(taxableAmt)}</td> <td className="py-2 pl-4 text-right">{formatCurrency(taxableAmt)}</td>
<td className="py-2 pl-4 text-right text-neutral-500">{Math.round((item.gstRate ?? 0.18) * 100)}%</td> <td className="py-2 pl-4 text-right text-neutral-500">{Math.round((item.gstRate ?? 0.18) * 100)}%</td>
<td className="py-2 pl-4 text-right">{formatCurrency(taxableAmt + gstAmt)}</td> <td className="py-2 pl-4 text-right">{formatCurrency(taxableAmt + gstAmt)}</td>
{showAccount && (
<td className="py-2 pl-4 text-xs text-neutral-500 font-mono">
{acct ? acct.code : "—"}
</td>
)}
</tr> </tr>
); );
})} })}
</tbody> </tbody>
<tfoot> <tfoot>
<tr className="border-t border-neutral-100"> <tr className="border-t border-neutral-100">
<td colSpan={hasSize ? 5 : 4} className="pt-3 text-right text-sm text-neutral-500">Taxable subtotal</td> <td colSpan={hasSize ? (showAccount ? 6 : 5) : (showAccount ? 5 : 4)} className="pt-3 text-right text-sm text-neutral-500">Taxable subtotal</td>
<td className="pt-3 pl-4 text-right text-sm text-neutral-700" colSpan={3}>{formatCurrency(taxable)}</td> <td className="pt-3 pl-4 text-right text-sm text-neutral-700" colSpan={3}>{formatCurrency(taxable)}</td>
</tr> </tr>
<tr> <tr>
<td colSpan={hasSize ? 5 : 4} className="py-0.5 text-right text-sm text-neutral-500">GST</td> <td colSpan={hasSize ? (showAccount ? 6 : 5) : (showAccount ? 5 : 4)} className="py-0.5 text-right text-sm text-neutral-500">GST</td>
<td className="py-0.5 pl-4 text-right text-sm text-neutral-700" colSpan={3}>{formatCurrency(gst)}</td> <td className="py-0.5 pl-4 text-right text-sm text-neutral-700" colSpan={3}>{formatCurrency(gst)}</td>
</tr> </tr>
<tr className="border-t border-neutral-200"> <tr className="border-t border-neutral-200">
<td colSpan={hasSize ? 5 : 4} className="pt-2 text-right text-sm font-semibold text-neutral-900">Grand Total</td> <td colSpan={hasSize ? (showAccount ? 6 : 5) : (showAccount ? 5 : 4)} className="pt-2 text-right text-sm font-semibold text-neutral-900">Grand Total</td>
<td className="pt-2 pl-4 text-right text-sm font-semibold text-neutral-900" colSpan={3}>{formatCurrency(grand)}</td> <td className="pt-2 pl-4 text-right text-sm font-semibold text-neutral-900" colSpan={3}>{formatCurrency(grand)}</td>
</tr> </tr>
</tfoot> </tfoot>
@ -300,6 +332,7 @@ export function LineItemsEditor({ items, onChange, readOnly = false, originalIte
); );
} }
// ── Edit view ────────────────────────────────────────────────────────────────
const liveItems = rows.map(toLineItem); const liveItems = rows.map(toLineItem);
const { taxable, gst, grand } = calcTotals(liveItems); const { taxable, gst, grand } = calcTotals(liveItems);
@ -315,6 +348,7 @@ export function LineItemsEditor({ items, onChange, readOnly = false, originalIte
<th className="pb-2 text-left font-medium text-neutral-600 pl-3 whitespace-nowrap">Size</th> <th className="pb-2 text-left font-medium text-neutral-600 pl-3 whitespace-nowrap">Size</th>
<th className="pb-2 text-right font-medium text-neutral-600 pl-4 whitespace-nowrap">Unit Price</th> <th className="pb-2 text-right font-medium text-neutral-600 pl-4 whitespace-nowrap">Unit Price</th>
<th className="pb-2 text-right font-medium text-neutral-600 pl-4 whitespace-nowrap">GST%</th> <th className="pb-2 text-right font-medium text-neutral-600 pl-4 whitespace-nowrap">GST%</th>
{multiAccount && <th className="pb-2 text-left font-medium text-neutral-600 pl-4 whitespace-nowrap">Account</th>}
<th className="pb-2 text-right font-medium text-neutral-600 pl-4 whitespace-nowrap">Total</th> <th className="pb-2 text-right font-medium text-neutral-600 pl-4 whitespace-nowrap">Total</th>
<th className="pb-2 pl-4 w-8" /> <th className="pb-2 pl-4 w-8" />
</tr> </tr>
@ -335,9 +369,7 @@ export function LineItemsEditor({ items, onChange, readOnly = false, originalIte
</td> </td>
<td className="py-2 pl-4"> <td className="py-2 pl-4">
<input <input
type="number" type="number" min="0" step="any"
min="0"
step="any"
value={row.quantity} value={row.quantity}
onChange={(e) => update(i, "quantity", e.target.value)} onChange={(e) => update(i, "quantity", e.target.value)}
className="w-20 rounded border border-neutral-200 px-2 py-1.5 text-sm text-right focus:border-primary-500 focus:outline-none" className="w-20 rounded border border-neutral-200 px-2 py-1.5 text-sm text-right focus:border-primary-500 focus:outline-none"
@ -364,9 +396,7 @@ export function LineItemsEditor({ items, onChange, readOnly = false, originalIte
</td> </td>
<td className="py-2 pl-4"> <td className="py-2 pl-4">
<input <input
type="number" type="number" min="0" step="any"
min="0"
step="any"
value={row.unitPrice} value={row.unitPrice}
onChange={(e) => update(i, "unitPrice", e.target.value)} onChange={(e) => update(i, "unitPrice", e.target.value)}
placeholder="0.00" placeholder="0.00"
@ -386,6 +416,19 @@ export function LineItemsEditor({ items, onChange, readOnly = false, originalIte
<option value="0.28">28%</option> <option value="0.28">28%</option>
</select> </select>
</td> </td>
{multiAccount && (
<td className="py-2 pl-4">
<select
value={row.accountId ?? defaultAccountId ?? ""}
onChange={(e) => update(i, "accountId", e.target.value)}
className="w-36 rounded border border-neutral-200 px-2 py-1.5 text-sm focus:border-primary-500 focus:outline-none"
>
{accounts.map((a) => (
<option key={a.id} value={a.id}>{a.name} ({a.code})</option>
))}
</select>
</td>
)}
<td className="py-2 pl-4 text-right text-sm"> <td className="py-2 pl-4 text-right text-sm">
{formatCurrency(taxableAmt * (1 + gstR))} {formatCurrency(taxableAmt * (1 + gstR))}
</td> </td>
@ -406,15 +449,15 @@ export function LineItemsEditor({ items, onChange, readOnly = false, originalIte
</tbody> </tbody>
<tfoot> <tfoot>
<tr className="border-t border-neutral-100"> <tr className="border-t border-neutral-100">
<td colSpan={6} className="pt-3 text-right text-sm text-neutral-500">Taxable subtotal</td> <td colSpan={multiAccount ? 7 : 6} className="pt-3 text-right text-sm text-neutral-500">Taxable subtotal</td>
<td className="pt-3 pl-4 text-right text-sm text-neutral-700" colSpan={2}>{formatCurrency(taxable)}</td> <td className="pt-3 pl-4 text-right text-sm text-neutral-700" colSpan={2}>{formatCurrency(taxable)}</td>
</tr> </tr>
<tr> <tr>
<td colSpan={6} className="py-0.5 text-right text-sm text-neutral-500">GST</td> <td colSpan={multiAccount ? 7 : 6} className="py-0.5 text-right text-sm text-neutral-500">GST</td>
<td className="py-0.5 pl-4 text-right text-sm text-neutral-700" colSpan={2}>{formatCurrency(gst)}</td> <td className="py-0.5 pl-4 text-right text-sm text-neutral-700" colSpan={2}>{formatCurrency(gst)}</td>
</tr> </tr>
<tr className="border-t border-neutral-200"> <tr className="border-t border-neutral-200">
<td colSpan={6} className="pt-2 text-right text-sm font-semibold text-neutral-900">Grand Total</td> <td colSpan={multiAccount ? 7 : 6} className="pt-2 text-right text-sm font-semibold text-neutral-900">Grand Total</td>
<td className="pt-2 pl-4 text-right text-sm font-semibold text-neutral-900" colSpan={2}>{formatCurrency(grand)}</td> <td className="pt-2 pl-4 text-right text-sm font-semibold text-neutral-900" colSpan={2}>{formatCurrency(grand)}</td>
</tr> </tr>
</tfoot> </tfoot>

View file

@ -9,6 +9,7 @@ export const lineItemSchema = z.object({
unitPrice: z.coerce.number().nonnegative("Unit price must be non-negative"), unitPrice: z.coerce.number().nonnegative("Unit price must be non-negative"),
gstRate: z.coerce.number().min(0).max(1).default(0.18), gstRate: z.coerce.number().min(0).max(1).default(0.18),
productId: z.string().optional(), productId: z.string().optional(),
accountId: z.string().optional(),
}); });
export const TC_FIXED_LINE = export const TC_FIXED_LINE =

View file

@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE "POLineItem" ADD COLUMN "accountId" TEXT;
-- AddForeignKey
ALTER TABLE "POLineItem" ADD CONSTRAINT "POLineItem_accountId_fkey" FOREIGN KEY ("accountId") REFERENCES "Account"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View file

@ -101,6 +101,7 @@ model Account {
isActive Boolean @default(true) isActive Boolean @default(true)
purchaseOrders PurchaseOrder[] purchaseOrders PurchaseOrder[]
lineItems POLineItem[]
} }
model Vendor { model Vendor {
@ -244,6 +245,8 @@ model POLineItem {
size String? size String?
productId String? productId String?
product Product? @relation(fields: [productId], references: [id]) product Product? @relation(fields: [productId], references: [id])
accountId String?
account Account? @relation(fields: [accountId], references: [id])
poId String poId String
po PurchaseOrder @relation(fields: [poId], references: [id], onDelete: Cascade) po PurchaseOrder @relation(fields: [poId], references: [id], onDelete: Cascade)