feat(receipt): allow partial receipt confirmation with per-item delivery tracking

Submitters can now mark individual item quantities as received when
confirming delivery, rather than treating a PO as all-or-nothing.

Schema (migration: 20260516103013_partial_receipt):
- POStatus: new PARTIALLY_CLOSED value between PAID_DELIVERED and CLOSED
- ActionType: new PARTIAL_RECEIPT_CONFIRMED value
- POLineItem: new deliveredQuantity Decimal? field — accumulates delivered qty
  across multiple receipt events

State machine:
- PAID_DELIVERED → confirm_partial_receipt → PARTIALLY_CLOSED (new)
- PARTIALLY_CLOSED → confirm_receipt → CLOSED (all delivered)
- PARTIALLY_CLOSED → confirm_partial_receipt → PARTIALLY_CLOSED (more partial)

Receipt page / form:
- Loads line items with ordered qty, previously delivered qty, and remaining qty
- Per-row numeric input for "receiving now" defaulting to all remaining
- "Mark all remaining" shortcut
- Dynamic button: "Confirm Partial Receipt" vs "Confirm Receipt & Close PO"
- Info banner telling user if the PO will stay open or close

Receipt action:
- Accumulates deliveredQuantity per line item
- If all lines fully delivered → CLOSED + fires notifications + updates inventory
- If any line still outstanding → PARTIALLY_CLOSED (no notifications yet)
- Inventory auto-update runs per-event for the delivered quantities only

Dashboard & PO detail:
- Open Orders count now includes PARTIALLY_CLOSED
- "Confirm Receipt" CTA in po-detail handles PARTIALLY_CLOSED with
  distinct amber styling and "Confirm Remaining" label
- Activity log shows PARTIAL_RECEIPT_CONFIRMED with appropriate label
- PARTIALLY_CLOSED gets warning (amber) badge variant

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Hardik 2026-05-16 16:02:44 +05:30
parent b2bfa63f61
commit 3b3a26eafe
9 changed files with 378 additions and 93 deletions

View file

@ -31,7 +31,7 @@ export default async function DashboardPage() {
async function SubmitterDashboard({ userId }: { userId: string }) {
const [openCount, pendingCount, closedCount, recentPos] = await Promise.all([
db.purchaseOrder.count({
where: { submitterId: userId, status: { in: ["DRAFT", "SUBMITTED", "MGR_REVIEW", "VENDOR_ID_PENDING", "EDITS_REQUESTED"] } },
where: { submitterId: userId, status: { in: ["DRAFT", "SUBMITTED", "MGR_REVIEW", "VENDOR_ID_PENDING", "EDITS_REQUESTED", "PARTIALLY_CLOSED"] } },
}),
db.purchaseOrder.count({
where: { submitterId: userId, status: "MGR_REVIEW" },

View file

@ -9,10 +9,17 @@ import { revalidatePath } from "next/cache";
export async function confirmReceipt({
poId,
notes,
deliveries,
}: {
poId: string;
notes?: string;
}): Promise<{ ok: true } | { error: string }> {
/**
* Per-line delivery quantities for this receipt event.
* Key = line item id, value = quantity delivered now (not cumulative).
* If omitted or empty, all items are treated as fully delivered.
*/
deliveries?: Record<string, number>;
}): Promise<{ ok: true; partial: boolean } | { error: string }> {
const session = await auth();
if (!session?.user) return { error: "Unauthorized" };
@ -25,44 +32,110 @@ export async function confirmReceipt({
},
});
if (!po) return { error: "PO not found" };
if (!canPerformAction(po.status, "confirm_receipt", session.user.role)) {
return { error: "You cannot confirm receipt on this PO." };
const isAllowedStatus =
po.status === "PAID_DELIVERED" || po.status === "PARTIALLY_CLOSED";
if (!isAllowedStatus) {
return { error: "You cannot confirm receipt on this PO in its current state." };
}
if (
!canPerformAction(po.status, "confirm_receipt", session.user.role) &&
!canPerformAction(po.status, "confirm_partial_receipt", session.user.role)
) {
return { error: "You do not have permission to confirm receipt on this PO." };
}
if (po.submitterId !== session.user.id && session.user.role !== "SUPERUSER") {
return { error: "You can only confirm receipt on your own purchase orders." };
}
// Compute the updated deliveredQuantity for each line item
const lineUpdates = po.lineItems.map((li) => {
const prevDelivered = Number(li.deliveredQuantity ?? 0);
const nowDelivered = deliveries ? (deliveries[li.id] ?? 0) : Number(li.quantity);
const totalDelivered = prevDelivered + nowDelivered;
const ordered = Number(li.quantity);
return {
id: li.id,
productId: li.productId,
quantity: ordered,
deliveredQuantity: Math.min(totalDelivered, ordered),
nowDelivered: Math.min(nowDelivered, ordered - prevDelivered),
};
});
// Determine if all items are now fully delivered
const allDelivered = lineUpdates.every((u) => u.deliveredQuantity >= u.quantity);
const newStatus = allDelivered ? "CLOSED" : "PARTIALLY_CLOSED";
const isPartial = !allDelivered;
// Persist delivery quantities
await Promise.all(
lineUpdates.map((u) =>
db.pOLineItem.update({
where: { id: u.id },
data: { deliveredQuantity: u.deliveredQuantity },
})
)
);
// Update PO status and log action
await db.purchaseOrder.update({
where: { id: poId },
data: {
status: "CLOSED",
closedAt: new Date(),
receipt: notes ? { create: { storageKey: "", fileName: "no-file", notes } } : undefined,
status: newStatus,
closedAt: allDelivered ? new Date() : undefined,
receipt: notes
? { create: { storageKey: "", fileName: "no-file", notes } }
: undefined,
actions: {
create: { actionType: "RECEIPT_CONFIRMED", actorId: session.user.id, note: notes ?? null },
create: {
actionType: isPartial ? "PARTIAL_RECEIPT_CONFIRMED" : "RECEIPT_CONFIRMED",
actorId: session.user.id,
note: notes ?? null,
metadata: isPartial
? {
deliveries: lineUpdates.map((u) => ({
lineItemId: u.id,
deliveredNow: u.nowDelivered,
totalDelivered: u.deliveredQuantity,
ordered: u.quantity,
})),
}
: undefined,
},
},
},
});
// Auto-update inventory: use PO siteId, fall back to vessel's home site
const siteId = (po as typeof po & { siteId?: string | null }).siteId ?? po.vessel?.site?.id ?? null;
// Auto-update inventory for delivered quantities
const siteId =
(po as typeof po & { siteId?: string | null }).siteId ??
po.vessel?.site?.id ??
null;
if (siteId) {
for (const li of po.lineItems) {
if (!li.productId) continue;
const qty = Number(li.quantity);
for (const u of lineUpdates) {
if (!u.productId || u.nowDelivered <= 0) continue;
await db.itemInventory.upsert({
where: { productId_siteId: { productId: li.productId, siteId } },
update: { quantity: { increment: qty } },
create: { productId: li.productId, siteId, quantity: qty },
where: { productId_siteId: { productId: u.productId, siteId } },
update: { quantity: { increment: u.nowDelivered } },
create: { productId: u.productId, siteId, quantity: u.nowDelivered },
});
}
revalidatePath(`/admin/sites/${siteId}`);
}
const [managers, accounts] = await Promise.all([
db.user.findMany({ where: { role: "MANAGER", isActive: true } }),
db.user.findMany({ where: { role: "ACCOUNTS", isActive: true } }),
]);
await notify({ event: "RECEIPT_CONFIRMED", po, recipients: [...managers, ...accounts] });
// Notify on full close only
if (allDelivered) {
const [managers, accounts] = await Promise.all([
db.user.findMany({ where: { role: "MANAGER", isActive: true } }),
db.user.findMany({ where: { role: "ACCOUNTS", isActive: true } }),
]);
await notify({ event: "RECEIPT_CONFIRMED", po, recipients: [...managers, ...accounts] });
}
revalidatePath(`/po/${poId}`);
revalidatePath("/dashboard");
return { ok: true };
revalidatePath("/my-orders");
return { ok: true, partial: isPartial };
}

View file

@ -18,24 +18,59 @@ export default async function ReceiptPage({ params }: Props) {
const po = await db.purchaseOrder.findUnique({
where: { id },
select: { id: true, poNumber: true, title: true, status: true, submitterId: true },
select: {
id: true,
poNumber: true,
title: true,
status: true,
submitterId: true,
lineItems: {
orderBy: { sortOrder: "asc" },
select: {
id: true,
name: true,
quantity: true,
unit: true,
deliveredQuantity: true,
},
},
},
});
if (!po) notFound();
if (po.status !== "PAID_DELIVERED") redirect(`/po/${id}`);
if (po.status !== "PAID_DELIVERED" && po.status !== "PARTIALLY_CLOSED") {
redirect(`/po/${id}`);
}
if (po.submitterId !== session.user.id && session.user.role !== "SUPERUSER") {
redirect(`/po/${id}`);
}
const lineItems = po.lineItems.map((li) => ({
id: li.id,
name: li.name,
quantity: Number(li.quantity),
unit: li.unit,
deliveredQuantity: li.deliveredQuantity !== null ? Number(li.deliveredQuantity) : null,
}));
const isPartiallyReceived = po.status === "PARTIALLY_CLOSED";
return (
<div className="max-w-lg">
<div className="max-w-2xl">
<div className="mb-6">
<h1 className="text-2xl font-semibold text-neutral-900">Confirm Receipt</h1>
<h1 className="text-2xl font-semibold text-neutral-900">
{isPartiallyReceived ? "Confirm Remaining Receipt" : "Confirm Receipt"}
</h1>
<p className="mt-1 text-sm text-neutral-500">
{po.poNumber} {po.title}
</p>
{isPartiallyReceived && (
<p className="mt-2 text-sm text-amber-700 bg-amber-50 border border-amber-200 rounded-lg px-3 py-2">
This PO has been partially received. Mark the quantities delivered in this batch.
</p>
)}
</div>
<ReceiptForm poId={po.id} />
<ReceiptForm poId={po.id} lineItems={lineItems} isPartiallyReceived={isPartiallyReceived} />
</div>
);
}

View file

@ -6,18 +6,57 @@ import { confirmReceipt } from "./actions";
import { FileUploader } from "@/components/po/file-uploader";
import { uploadAndLinkFiles } from "@/lib/upload-files";
export function ReceiptForm({ poId }: { poId: string }) {
interface LineItem {
id: string;
name: string;
quantity: number;
unit: string;
deliveredQuantity: number | null;
}
interface Props {
poId: string;
lineItems: LineItem[];
isPartiallyReceived: boolean;
}
export function ReceiptForm({ poId, lineItems, isPartiallyReceived }: Props) {
const router = useRouter();
const [notes, setNotes] = useState("");
const [files, setFiles] = useState<File[]>([]);
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState("");
// Per-item: how many are being delivered in this batch
const [deliveries, setDeliveries] = useState<Record<string, number>>(() => {
const init: Record<string, number> = {};
for (const li of lineItems) {
const remaining = li.quantity - (li.deliveredQuantity ?? 0);
init[li.id] = remaining; // default: all remaining
}
return init;
});
function setDelivery(id: string, value: number) {
const li = lineItems.find((l) => l.id === id)!;
const remaining = li.quantity - (li.deliveredQuantity ?? 0);
setDeliveries((prev) => ({ ...prev, [id]: Math.max(0, Math.min(value, remaining)) }));
}
function markAllRemaining() {
const all: Record<string, number> = {};
for (const li of lineItems) {
all[li.id] = li.quantity - (li.deliveredQuantity ?? 0);
}
setDeliveries(all);
}
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setSubmitting(true);
setError("");
const result = await confirmReceipt({ poId, notes });
const result = await confirmReceipt({ poId, notes, deliveries });
if ("error" in result) {
setError(result.error);
setSubmitting(false);
@ -35,55 +74,146 @@ export function ReceiptForm({ poId }: { poId: string }) {
router.refresh();
}
const allFullyDelivered = lineItems.every(
(li) => (li.deliveredQuantity ?? 0) + (deliveries[li.id] ?? 0) >= li.quantity
);
const nothingDelivered = Object.values(deliveries).every((v) => v === 0);
return (
<div className="rounded-lg border border-neutral-200 bg-white p-6">
<form onSubmit={handleSubmit} className="space-y-4">
<p className="text-sm text-neutral-600">
Confirming receipt will close this purchase order. Please verify that all
items have been received before proceeding.
</p>
<div>
<label className="block text-sm font-medium text-neutral-700 mb-1.5">
Delivery notes (optional)
</label>
<textarea
rows={4}
value={notes}
onChange={(e) => setNotes(e.target.value)}
className="w-full rounded-lg border border-neutral-300 px-3 py-2.5 text-sm focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-500/20 resize-none"
placeholder="Any notes about the delivery…"
/>
</div>
<div>
<label className="block text-sm font-medium text-neutral-700 mb-1.5">
Attachments (optional)
</label>
<FileUploader files={files} onChange={setFiles} disabled={submitting} />
</div>
{error && (
<p className="text-sm text-danger-700 bg-danger-50 rounded-lg px-3 py-2">{error}</p>
)}
<div className="flex gap-3">
<div className="space-y-5">
{/* Line items delivery tracker */}
<div className="rounded-lg border border-neutral-200 bg-white overflow-hidden">
<div className="px-4 py-3 border-b border-neutral-100 flex items-center justify-between">
<h3 className="text-sm font-semibold text-neutral-800">Items to Receive</h3>
<button
type="button"
onClick={() => router.back()}
className="rounded-lg border border-neutral-300 bg-white px-4 py-2.5 text-sm font-medium text-neutral-700 hover:bg-neutral-50 transition-colors"
onClick={markAllRemaining}
className="text-xs text-primary-600 hover:text-primary-700 font-medium"
>
Cancel
</button>
<button
type="submit"
disabled={submitting}
className="rounded-lg bg-success px-4 py-2.5 text-sm font-semibold text-white hover:opacity-90 disabled:opacity-60 transition-opacity"
>
{submitting ? "Confirming…" : "Confirm Receipt & Close PO"}
Mark all remaining
</button>
</div>
</form>
<table className="w-full text-sm">
<thead className="bg-neutral-50 border-b border-neutral-100">
<tr>
<th className="px-4 py-2.5 text-left font-medium text-neutral-600">Item</th>
<th className="px-4 py-2.5 text-right font-medium text-neutral-600">Ordered</th>
<th className="px-4 py-2.5 text-right font-medium text-neutral-600">Previously Received</th>
<th className="px-4 py-2.5 text-right font-medium text-neutral-600">Remaining</th>
<th className="px-4 py-2.5 text-right font-medium text-neutral-600">Receiving Now</th>
</tr>
</thead>
<tbody className="divide-y divide-neutral-100">
{lineItems.map((li) => {
const prevDelivered = li.deliveredQuantity ?? 0;
const remaining = li.quantity - prevDelivered;
const nowDelivering = deliveries[li.id] ?? 0;
const isFullyReceived = remaining <= 0;
return (
<tr key={li.id} className={isFullyReceived ? "bg-neutral-50" : ""}>
<td className="px-4 py-3 font-medium text-neutral-900">
{li.name}
{isFullyReceived && (
<span className="ml-2 text-xs text-success-600 font-normal"> fully received</span>
)}
</td>
<td className="px-4 py-3 text-right font-mono text-xs text-neutral-600">
{li.quantity} {li.unit}
</td>
<td className="px-4 py-3 text-right font-mono text-xs text-neutral-500">
{isPartiallyReceived || prevDelivered > 0 ? `${prevDelivered} ${li.unit}` : "—"}
</td>
<td className="px-4 py-3 text-right font-mono text-xs">
<span className={remaining > 0 ? "text-amber-700 font-medium" : "text-neutral-400"}>
{remaining > 0 ? `${remaining} ${li.unit}` : "—"}
</span>
</td>
<td className="px-4 py-3 text-right">
{isFullyReceived ? (
<span className="text-xs text-neutral-400"></span>
) : (
<input
type="number"
min={0}
max={remaining}
step="any"
value={nowDelivering}
onChange={(e) => setDelivery(li.id, parseFloat(e.target.value) || 0)}
className="w-24 rounded border border-neutral-300 px-2 py-1 text-right text-sm font-mono focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500/30"
/>
)}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
<div className="rounded-lg border border-neutral-200 bg-white p-5">
<form onSubmit={handleSubmit} className="space-y-4">
{allFullyDelivered ? (
<p className="text-sm text-success-700 bg-success-50 border border-success-100 rounded-lg px-3 py-2">
All items will be marked as received. Confirming will fully close this PO.
</p>
) : (
<p className="text-sm text-amber-700 bg-amber-50 border border-amber-200 rounded-lg px-3 py-2">
Not all items are being received. The PO will remain open as &ldquo;Partially Received&rdquo; until
all items are delivered.
</p>
)}
<div>
<label className="block text-sm font-medium text-neutral-700 mb-1.5">
Delivery notes (optional)
</label>
<textarea
rows={3}
value={notes}
onChange={(e) => setNotes(e.target.value)}
className="w-full rounded-lg border border-neutral-300 px-3 py-2.5 text-sm focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-500/20 resize-none"
placeholder="Any notes about this delivery batch…"
/>
</div>
<div>
<label className="block text-sm font-medium text-neutral-700 mb-1.5">
Attachments (optional)
</label>
<FileUploader files={files} onChange={setFiles} disabled={submitting} />
</div>
{error && (
<p className="text-sm text-danger-700 bg-danger-50 rounded-lg px-3 py-2">{error}</p>
)}
<div className="flex gap-3">
<button
type="button"
onClick={() => router.back()}
className="rounded-lg border border-neutral-300 bg-white px-4 py-2.5 text-sm font-medium text-neutral-700 hover:bg-neutral-50 transition-colors"
>
Cancel
</button>
<button
type="submit"
disabled={submitting || nothingDelivered}
className={`rounded-lg px-4 py-2.5 text-sm font-semibold text-white disabled:opacity-60 transition-opacity ${
allFullyDelivered
? "bg-success hover:opacity-90"
: "bg-primary-600 hover:bg-primary-700"
}`}
>
{submitting
? "Confirming…"
: allFullyDelivered
? "Confirm Receipt & Close PO"
: "Confirm Partial Receipt"}
</button>
</div>
</form>
</div>
</div>
);
}

View file

@ -82,6 +82,7 @@ const ACTION_LABELS: Record<string, string> = {
VENDOR_ID_PROVIDED: "Vendor ID provided",
PAYMENT_SENT: "Payment confirmed",
RECEIPT_CONFIRMED: "Receipt confirmed",
PARTIAL_RECEIPT_CONFIRMED: "Partial receipt confirmed",
CLOSED: "Closed",
MANAGER_LINE_EDIT: "Manager amended line items",
PRODUCT_PRICE_UPDATED: "Product prices updated",
@ -140,7 +141,7 @@ export async function PoDetail({ po, currentUserId, currentRole, readOnly = fals
);
const canConfirmReceipt =
po.status === "PAID_DELIVERED" &&
(po.status === "PAID_DELIVERED" || po.status === "PARTIALLY_CLOSED") &&
(po.submitter.id === currentUserId || currentRole === "SUPERUSER") &&
!readOnly;
@ -388,16 +389,28 @@ export async function PoDetail({ po, currentUserId, currentRole, readOnly = fals
{/* Confirm receipt CTA */}
{canConfirmReceipt && (
<div className="rounded-lg border border-success-100 bg-success-50 p-5 flex items-center justify-between">
<div className={`rounded-lg border p-5 flex items-center justify-between ${
po.status === "PARTIALLY_CLOSED"
? "border-warning-100 bg-warning-50"
: "border-success-100 bg-success-50"
}`}>
<div>
<p className="font-medium text-success-700">Payment confirmed</p>
<p className="text-sm text-success-700 mt-0.5">Please confirm that you have received all items.</p>
<p className={`font-medium ${po.status === "PARTIALLY_CLOSED" ? "text-warning-700" : "text-success-700"}`}>
{po.status === "PARTIALLY_CLOSED" ? "Partially received" : "Payment confirmed"}
</p>
<p className={`text-sm mt-0.5 ${po.status === "PARTIALLY_CLOSED" ? "text-warning-700" : "text-success-700"}`}>
{po.status === "PARTIALLY_CLOSED"
? "Some items are still outstanding. Confirm remaining deliveries."
: "Please confirm that you have received all items."}
</p>
</div>
<Link
href={`/po/${po.id}/receipt`}
className="rounded-lg bg-success px-4 py-2.5 text-sm font-semibold text-white hover:opacity-90"
className={`rounded-lg px-4 py-2.5 text-sm font-semibold text-white hover:opacity-90 ${
po.status === "PARTIALLY_CLOSED" ? "bg-warning-600" : "bg-success"
}`}
>
Confirm Receipt
{po.status === "PARTIALLY_CLOSED" ? "Confirm Remaining" : "Confirm Receipt"}
</Link>
</div>
)}

View file

@ -10,7 +10,8 @@ export type POAction =
| "provide_vendor_id"
| "process_payment"
| "mark_paid"
| "confirm_receipt";
| "confirm_receipt"
| "confirm_partial_receipt";
export type SideEffect =
| "EMAIL_MANAGER"
@ -110,6 +111,26 @@ const TRANSITIONS: Partial<Record<POStatus, TransitionMap>> = {
requiresNote: false,
sideEffects: ["EMAIL_MANAGER", "EMAIL_ACCOUNTS"],
},
confirm_partial_receipt: {
to: "PARTIALLY_CLOSED",
allowedRoles: ["TECHNICAL", "MANNING", "SUPERUSER"],
requiresNote: false,
sideEffects: [],
},
},
PARTIALLY_CLOSED: {
confirm_receipt: {
to: "CLOSED",
allowedRoles: ["TECHNICAL", "MANNING", "SUPERUSER"],
requiresNote: false,
sideEffects: ["EMAIL_MANAGER", "EMAIL_ACCOUNTS"],
},
confirm_partial_receipt: {
to: "PARTIALLY_CLOSED",
allowedRoles: ["TECHNICAL", "MANNING", "SUPERUSER"],
requiresNote: false,
sideEffects: [],
},
},
};

View file

@ -48,6 +48,7 @@ export const PO_STATUS_LABELS: Record<POStatus, string> = {
MGR_APPROVED: "Approved",
SENT_FOR_PAYMENT: "Sent for Payment",
PAID_DELIVERED: "Paid",
PARTIALLY_CLOSED: "Partially Received",
CLOSED: "Closed",
};
@ -69,5 +70,6 @@ export const PO_STATUS_VARIANTS: Record<POStatus, BadgeVariant> = {
MGR_APPROVED: "success",
SENT_FOR_PAYMENT: "default",
PAID_DELIVERED: "success",
PARTIALLY_CLOSED: "warning",
CLOSED: "secondary",
};

View file

@ -0,0 +1,8 @@
-- AlterEnum
ALTER TYPE "ActionType" ADD VALUE 'PARTIAL_RECEIPT_CONFIRMED';
-- AlterEnum
ALTER TYPE "POStatus" ADD VALUE 'PARTIALLY_CLOSED';
-- AlterTable
ALTER TABLE "POLineItem" ADD COLUMN "deliveredQuantity" DECIMAL(10,3);

View file

@ -27,6 +27,7 @@ enum POStatus {
MGR_APPROVED
SENT_FOR_PAYMENT
PAID_DELIVERED
PARTIALLY_CLOSED
CLOSED
}
@ -41,6 +42,7 @@ enum ActionType {
VENDOR_ID_PROVIDED
PAYMENT_SENT
RECEIPT_CONFIRMED
PARTIAL_RECEIPT_CONFIRMED
CLOSED
REASSIGNED
PRODUCT_PRICE_UPDATED
@ -244,20 +246,21 @@ model PurchaseOrder {
}
model POLineItem {
id String @id @default(cuid())
name String
description String?
quantity Decimal @db.Decimal(10, 3)
unit String
unitPrice Decimal @db.Decimal(12, 2)
totalPrice Decimal @db.Decimal(12, 2)
gstRate Decimal @default(0.18) @db.Decimal(5, 4)
sortOrder Int @default(0)
size String?
productId String?
product Product? @relation(fields: [productId], references: [id])
accountId String?
account Account? @relation(fields: [accountId], references: [id])
id String @id @default(cuid())
name String
description String?
quantity Decimal @db.Decimal(10, 3)
unit String
unitPrice Decimal @db.Decimal(12, 2)
totalPrice Decimal @db.Decimal(12, 2)
gstRate Decimal @default(0.18) @db.Decimal(5, 4)
sortOrder Int @default(0)
size String?
deliveredQuantity Decimal? @db.Decimal(10, 3)
productId String?
product Product? @relation(fields: [productId], references: [id])
accountId String?
account Account? @relation(fields: [accountId], references: [id])
poId String
po PurchaseOrder @relation(fields: [poId], references: [id], onDelete: Cascade)