make account service with grn
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
import { useParams } from "next/navigation"
|
||||
import Link from "next/link"
|
||||
import { CheckCircle2, PackageCheck, PackageX, ShieldAlert, XCircle } from "lucide-react"
|
||||
import { BookOpen, CheckCircle2, CircleDollarSign, PackageCheck, PackageX, ShieldAlert, XCircle } from "lucide-react"
|
||||
|
||||
import { grnsApi } from "@/lib/api/grns"
|
||||
import { warehousesApi } from "@/lib/api/warehouses"
|
||||
@@ -11,8 +11,9 @@ import { itemsApi } from "@/lib/api/items"
|
||||
import { uomsApi } from "@/lib/api/uoms"
|
||||
import { baseUomLabel } from "@/lib/uom-label"
|
||||
import { errorMessage } from "@/lib/error-map"
|
||||
import { formatAmount } from "@/lib/format"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { ConfirmGrnResponse, Grn } from "@/types/grn"
|
||||
import { ConfirmGrnResponse, Grn, GrnPayment } from "@/types/grn"
|
||||
import { Bin, ItemListItem, Uom } from "@/types/master-data"
|
||||
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
@@ -20,7 +21,8 @@ import { Button, buttonVariants } from "@/components/ui/button"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||
import { toast } from "@/components/ui/toast"
|
||||
import { GrnStatusBadge, HoldStatusBadge } from "@/components/receiving/status-badges"
|
||||
import { GrnPaymentStatusBadge, GrnStatusBadge, HoldStatusBadge } from "@/components/receiving/status-badges"
|
||||
import { GrnPaymentDialog } from "@/components/receiving/GrnPaymentDialog"
|
||||
|
||||
export default function GrnDetailPage() {
|
||||
const params = useParams<{ id: string }>()
|
||||
@@ -36,6 +38,9 @@ export default function GrnDetailPage() {
|
||||
const [confirmResult, setConfirmResult] = useState<ConfirmGrnResponse | null>(null)
|
||||
const [releasingLineId, setReleasingLineId] = useState<number | null>(null)
|
||||
|
||||
const [payments, setPayments] = useState<GrnPayment[]>([])
|
||||
const [payDialogOpen, setPayDialogOpen] = useState(false)
|
||||
|
||||
// Stable per detail-page-session key so a retried confirm click doesn't double-post.
|
||||
const idempotencyKey = useRef(crypto.randomUUID())
|
||||
|
||||
@@ -55,6 +60,18 @@ export default function GrnDetailPage() {
|
||||
warehousesApi.listBins(grn.warehouseId).then(setBins).catch(() => setBins([]))
|
||||
}, [grn?.warehouseId])
|
||||
|
||||
useEffect(() => {
|
||||
if (!grn || grn.status === "Draft") return
|
||||
grnsApi.listPayments(grn.grnId).then(setPayments).catch(() => setPayments([]))
|
||||
}, [grn?.grnId, grn?.status])
|
||||
|
||||
function handlePaid(_updated: Grn, payment: GrnPayment) {
|
||||
setGrn((prev) =>
|
||||
prev ? { ...prev, paidAmount: prev.paidAmount + payment.amount, balanceAmount: prev.balanceAmount - payment.amount } : prev
|
||||
)
|
||||
setPayments((prev) => [payment, ...prev])
|
||||
}
|
||||
|
||||
function itemFor(itemId: number) {
|
||||
return items.find((i) => i.itemId === itemId)
|
||||
}
|
||||
@@ -70,8 +87,10 @@ export default function GrnDetailPage() {
|
||||
try {
|
||||
const result = await grnsApi.confirm(grn.grnId, idempotencyKey.current)
|
||||
setConfirmResult(result)
|
||||
setGrn((prev) => (prev ? { ...prev, status: result.status } : prev))
|
||||
toast.success("GRN confirmed", `${result.createdLayers.length} layer(s) posted to stock.`)
|
||||
setGrn((prev) =>
|
||||
prev ? { ...prev, status: result.status, glJournalNo: result.glJournalNo, balanceAmount: result.balanceAmount } : prev
|
||||
)
|
||||
toast.success("GRN confirmed", `${result.createdLayers.length} layer(s) posted to stock. Journal ${result.glJournalNo}.`)
|
||||
} catch (err) {
|
||||
setError(errorMessage(err))
|
||||
toast.error("Could not confirm GRN", errorMessage(err))
|
||||
@@ -121,6 +140,7 @@ export default function GrnDetailPage() {
|
||||
<div className="flex items-center gap-3">
|
||||
<h1 className="text-2xl font-bold text-foreground">{grn.docNo}</h1>
|
||||
<GrnStatusBadge status={grn.status} />
|
||||
{grn.status !== "Draft" && <GrnPaymentStatusBadge paidAmount={grn.paidAmount} balanceAmount={grn.balanceAmount} />}
|
||||
</div>
|
||||
<p className="text-base text-muted-foreground">
|
||||
{grn.poId ? `Against PO #${grn.poId}` : "Direct receipt"} — Vendor #{grn.vendorId} — Warehouse #{grn.warehouseId}
|
||||
@@ -128,14 +148,36 @@ export default function GrnDetailPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{grn.status === "Draft" && (
|
||||
<Button size="lg" onClick={handleConfirm} disabled={confirming}>
|
||||
<PackageCheck className="size-5" />
|
||||
{confirming ? "Confirming…" : "Confirm GRN"}
|
||||
</Button>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
{grn.status !== "Draft" && (
|
||||
<>
|
||||
<Link
|
||||
href="/dashboard/ledgers/general-ledger"
|
||||
className={cn(buttonVariants({ variant: "outline", size: "lg" }))}
|
||||
title="View this GRN's postings in the General Ledger report"
|
||||
>
|
||||
<BookOpen className="size-5" />
|
||||
View in Ledger
|
||||
</Link>
|
||||
{grn.balanceAmount > 0 && (
|
||||
<Button size="lg" onClick={() => setPayDialogOpen(true)}>
|
||||
<CircleDollarSign className="size-5" />
|
||||
Pay
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{grn.status === "Draft" && (
|
||||
<Button size="lg" onClick={handleConfirm} disabled={confirming}>
|
||||
<PackageCheck className="size-5" />
|
||||
{confirming ? "Confirming…" : "Confirm GRN"}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<GrnPaymentDialog grn={grn} open={payDialogOpen} onOpenChange={setPayDialogOpen} onPaid={handlePaid} />
|
||||
|
||||
{error && (
|
||||
<div className="rounded-lg border border-destructive/30 bg-destructive/5 p-5 text-base text-destructive">{error}</div>
|
||||
)}
|
||||
@@ -150,6 +192,7 @@ export default function GrnDetailPage() {
|
||||
Layers: {confirmResult.createdLayers.map((l) => `#${l.layerId} (${l.qtyReceived} @ ${l.unitCost})`).join(", ")}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">Ledger refs: {confirmResult.ledgerRefs.join(", ")}</div>
|
||||
<div className="text-sm text-muted-foreground">GL journal entry: {confirmResult.glJournalNo}</div>
|
||||
{confirmResult.poStatus && <div className="text-sm text-muted-foreground">PO status: {confirmResult.poStatus}</div>}
|
||||
</div>
|
||||
)}
|
||||
@@ -258,7 +301,7 @@ export default function GrnDetailPage() {
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-8 border-t border-border pt-4 text-base">
|
||||
<div className="flex flex-wrap justify-end gap-8 border-t border-border pt-4 text-base">
|
||||
<div className="flex gap-3">
|
||||
<span className="text-muted-foreground">Stock value (excl. VAT)</span>
|
||||
<span className="tabular-nums">{grn.lines.reduce((s, l) => s + l.receivedValue, 0).toFixed(2)}</span>
|
||||
@@ -271,7 +314,49 @@ export default function GrnDetailPage() {
|
||||
<span className="text-muted-foreground">Document total</span>
|
||||
<span className="font-semibold tabular-nums">{grn.lines.reduce((s, l) => s + l.lineTotal, 0).toFixed(2)}</span>
|
||||
</div>
|
||||
{grn.status !== "Draft" && (
|
||||
<>
|
||||
<div className="flex gap-3">
|
||||
<span className="text-muted-foreground">Amount paid</span>
|
||||
<span className="tabular-nums">{formatAmount(grn.paidAmount)}</span>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<span className="text-muted-foreground">Balance due</span>
|
||||
<span className="font-semibold tabular-nums">{formatAmount(grn.balanceAmount)}</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{grn.status !== "Draft" && payments.length > 0 && (
|
||||
<div className="flex flex-col gap-3">
|
||||
<h2 className="text-lg font-semibold text-foreground">Payment History</h2>
|
||||
<div className="overflow-x-auto">
|
||||
<Table className="text-base">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="h-12 px-3 text-sm">Date</TableHead>
|
||||
<TableHead className="h-12 px-3 text-sm text-right">Amount</TableHead>
|
||||
<TableHead className="h-12 px-3 text-sm">Account</TableHead>
|
||||
<TableHead className="h-12 px-3 text-sm">Reference</TableHead>
|
||||
<TableHead className="h-12 px-3 text-sm">GL Journal</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{payments.map((p) => (
|
||||
<TableRow key={p.grnPaymentId}>
|
||||
<TableCell className="px-3 py-3.5">{new Date(p.paymentDate).toLocaleString()}</TableCell>
|
||||
<TableCell className="px-3 py-3.5 text-right tabular-nums">{formatAmount(p.amount)}</TableCell>
|
||||
<TableCell className="px-3 py-3.5">{p.bankAccountName}</TableCell>
|
||||
<TableCell className="px-3 py-3.5">{p.reference ?? "—"}</TableCell>
|
||||
<TableCell className="px-3 py-3.5">{p.glJournalNo ?? "—"}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ChevronLeft, ChevronRight, Eye, PackageSearch, Plus, Search } from "luc
|
||||
|
||||
import { grnsApi } from "@/lib/api/grns"
|
||||
import { errorMessage } from "@/lib/error-map"
|
||||
import { formatAmount } from "@/lib/format"
|
||||
import { GrnStatus, GrnSummary } from "@/types/grn"
|
||||
import { PaginationMeta } from "@/types/common"
|
||||
import { cn } from "@/lib/utils"
|
||||
@@ -22,7 +23,7 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table"
|
||||
import { GrnStatusBadge } from "@/components/receiving/status-badges"
|
||||
import { GrnPaymentStatusBadge, GrnStatusBadge } from "@/components/receiving/status-badges"
|
||||
|
||||
type StatusFilter = GrnStatus | "All"
|
||||
|
||||
@@ -174,6 +175,8 @@ export default function GrnListPage() {
|
||||
<TableHead className="h-12 px-3 text-sm">Vendor</TableHead>
|
||||
<TableHead className="h-12 px-3 text-sm">Warehouse</TableHead>
|
||||
<TableHead className="h-12 px-3 text-sm">Status</TableHead>
|
||||
<TableHead className="h-12 px-3 text-sm text-right">Balance</TableHead>
|
||||
<TableHead className="h-12 px-3 text-sm">Payment</TableHead>
|
||||
<TableHead className="h-12 px-3 text-sm">Created</TableHead>
|
||||
<TableHead className="h-12 px-3 text-sm">Actions</TableHead>
|
||||
</TableRow>
|
||||
@@ -196,6 +199,16 @@ export default function GrnListPage() {
|
||||
<TableCell className="px-3 py-3.5">
|
||||
<GrnStatusBadge status={grn.status} />
|
||||
</TableCell>
|
||||
<TableCell className="px-3 py-3.5 text-right tabular-nums">
|
||||
{grn.status === "Draft" ? "—" : formatAmount(grn.balanceAmount)}
|
||||
</TableCell>
|
||||
<TableCell className="px-3 py-3.5">
|
||||
{grn.status === "Draft" ? (
|
||||
<span className="text-sm text-muted-foreground">—</span>
|
||||
) : (
|
||||
<GrnPaymentStatusBadge paidAmount={grn.paidAmount} balanceAmount={grn.balanceAmount} />
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="px-3 py-3.5">{new Date(grn.createdAt).toLocaleString()}</TableCell>
|
||||
<TableCell className="px-3 py-3.5">
|
||||
<div className="flex items-center gap-1">
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useState } from "react"
|
||||
|
||||
import { bankAccountsApi } from "@/lib/api/general-ledger"
|
||||
import { grnsApi } from "@/lib/api/grns"
|
||||
import { errorMessage } from "@/lib/error-map"
|
||||
import { formatAmount } from "@/lib/format"
|
||||
import { CashAndBankAccountDto } from "@/types/general-ledger"
|
||||
import { Grn, GrnPayment } from "@/types/grn"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
||||
import { Field, FieldError, FieldGroup, FieldLabel } from "@/components/ui/field"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { toast } from "@/components/ui/toast"
|
||||
|
||||
interface GrnPaymentDialogProps {
|
||||
grn: Grn | null
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
onPaid: (grn: Grn, payment: GrnPayment) => void
|
||||
}
|
||||
|
||||
/** Pay the vendor against a confirmed GRN's balance — full or partial, from an existing
|
||||
* GL cash/bank account. Modeled on ReceivedChequeDialog's "pick an account, submit" shape. */
|
||||
export function GrnPaymentDialog({ grn, open, onOpenChange, onPaid }: GrnPaymentDialogProps) {
|
||||
const [accounts, setAccounts] = useState<CashAndBankAccountDto[] | null>(null)
|
||||
const [glBankAccountId, setGlBankAccountId] = useState("")
|
||||
const [amount, setAmount] = useState("")
|
||||
const [reference, setReference] = useState("")
|
||||
const [errors, setErrors] = useState<Record<string, string>>({})
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
|
||||
// Reset the form fields for a fresh open (or a different GRN) during render, React's own
|
||||
// sanctioned "adjust state while rendering" pattern (see the General Ledger report page's
|
||||
// periodKey comment) — not inside the effect below, which would be a synchronous
|
||||
// setState-in-effect (react-hooks/set-state-in-effect).
|
||||
const openKey = open && grn ? `${grn.grnId}` : null
|
||||
const [resetFor, setResetFor] = useState<string | null>(null)
|
||||
if (openKey !== null && resetFor !== openKey) {
|
||||
setResetFor(openKey)
|
||||
setAmount(grn!.balanceAmount.toFixed(2))
|
||||
setGlBankAccountId("")
|
||||
setReference("")
|
||||
setErrors({})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || accounts !== null) return
|
||||
bankAccountsApi.list("Both").then(setAccounts).catch(() => setAccounts([]))
|
||||
}, [open, accounts])
|
||||
|
||||
if (!grn) return null
|
||||
|
||||
async function submit() {
|
||||
if (!grn) return
|
||||
const nextErrors: Record<string, string> = {}
|
||||
const amountNum = Number(amount)
|
||||
if (!glBankAccountId) nextErrors.glBankAccountId = "Select an account to pay from"
|
||||
if (!amount || Number.isNaN(amountNum) || amountNum <= 0) nextErrors.amount = "Enter a valid amount"
|
||||
else if (amountNum > grn.balanceAmount) nextErrors.amount = `Cannot exceed the balance (${formatAmount(grn.balanceAmount)})`
|
||||
setErrors(nextErrors)
|
||||
if (Object.keys(nextErrors).length > 0) return
|
||||
|
||||
setSubmitting(true)
|
||||
try {
|
||||
const payment = await grnsApi.pay(grn.grnId, {
|
||||
amount: amountNum,
|
||||
glBankAccountId: Number(glBankAccountId),
|
||||
reference: reference || undefined,
|
||||
})
|
||||
toast.success("Payment recorded", `${formatAmount(amountNum)} posted to the ledger (${payment.glJournalNo ?? "—"}).`)
|
||||
onPaid(grn, payment)
|
||||
onOpenChange(false)
|
||||
} catch (err) {
|
||||
toast.error("Could not record payment", errorMessage(err))
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Pay GRN {grn.docNo}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Balance due: <span className="font-medium text-foreground tabular-nums">{formatAmount(grn.balanceAmount)}</span>
|
||||
</p>
|
||||
|
||||
<FieldGroup>
|
||||
<Field data-invalid={!!errors.glBankAccountId}>
|
||||
<FieldLabel htmlFor="grn-pay-account">Pay from</FieldLabel>
|
||||
<Select<string> value={glBankAccountId} onValueChange={(v) => setGlBankAccountId(v ?? "")}>
|
||||
<SelectTrigger id="grn-pay-account" className="w-full text-base" aria-invalid={!!errors.glBankAccountId}>
|
||||
<SelectValue placeholder={accounts === null ? "Loading…" : "Select a cash/bank account"} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(accounts ?? []).map((a) => (
|
||||
<SelectItem key={a.accountId} value={String(a.accountId)} label={a.accountName} className="text-base">
|
||||
{a.accountName} ({a.accountType})
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FieldError errors={[errors.glBankAccountId ? { message: errors.glBankAccountId } : undefined]} />
|
||||
</Field>
|
||||
|
||||
<Field data-invalid={!!errors.amount}>
|
||||
<FieldLabel htmlFor="grn-pay-amount">Amount</FieldLabel>
|
||||
<Input
|
||||
id="grn-pay-amount"
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0.01"
|
||||
max={grn.balanceAmount}
|
||||
value={amount}
|
||||
onChange={(e) => setAmount(e.target.value)}
|
||||
aria-invalid={!!errors.amount}
|
||||
/>
|
||||
<FieldError errors={[errors.amount ? { message: errors.amount } : undefined]} />
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldLabel htmlFor="grn-pay-reference">Reference (optional)</FieldLabel>
|
||||
<Input id="grn-pay-reference" value={reference} onChange={(e) => setReference(e.target.value)} />
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={submitting}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={submit} disabled={submitting}>
|
||||
{submitting ? "Recording…" : "Record payment"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -32,3 +32,32 @@ export function HoldStatusBadge({ status }: { status: HoldStatus }) {
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
|
||||
export type GrnPaymentStatus = "Unpaid" | "PartiallyPaid" | "Paid"
|
||||
|
||||
export function grnPaymentStatus(paidAmount: number, balanceAmount: number): GrnPaymentStatus {
|
||||
if (balanceAmount <= 0) return "Paid"
|
||||
if (paidAmount > 0) return "PartiallyPaid"
|
||||
return "Unpaid"
|
||||
}
|
||||
|
||||
function paymentClass(status: GrnPaymentStatus) {
|
||||
if (status === "Paid") return "bg-success/10 text-success border-transparent"
|
||||
if (status === "PartiallyPaid") return "bg-warning/10 text-warning border-transparent"
|
||||
return "bg-muted text-muted-foreground border-transparent" // Unpaid
|
||||
}
|
||||
|
||||
const paymentLabel: Record<GrnPaymentStatus, string> = {
|
||||
Unpaid: "Unpaid",
|
||||
PartiallyPaid: "Partial",
|
||||
Paid: "Paid",
|
||||
}
|
||||
|
||||
export function GrnPaymentStatusBadge({ paidAmount, balanceAmount }: { paidAmount: number; balanceAmount: number }) {
|
||||
const status = grnPaymentStatus(paidAmount, balanceAmount)
|
||||
return (
|
||||
<Badge variant="outline" className={`${badgeSize} ${paymentClass(status)}`}>
|
||||
{paymentLabel[status]}
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10,8 +10,10 @@ import { apiRequest, buildQuery } from "@/lib/api-client"
|
||||
import { PagedResponse } from "@/types/common"
|
||||
import {
|
||||
ConfirmGrnResponse,
|
||||
CreateGrnPaymentRequest,
|
||||
CreateGrnRequest,
|
||||
Grn,
|
||||
GrnPayment,
|
||||
GrnStatus,
|
||||
GrnSummary,
|
||||
ReleaseAction,
|
||||
@@ -58,4 +60,14 @@ export const grnsApi = {
|
||||
body: { action },
|
||||
})
|
||||
},
|
||||
|
||||
/** Pay the vendor against the GRN's balance, in full or in installments. Posts a real GL journal entry. */
|
||||
pay(grnId: number, request: CreateGrnPaymentRequest): Promise<GrnPayment> {
|
||||
return apiRequest<GrnPayment>(`/grns/${grnId}/payments`, { method: "POST", body: request })
|
||||
},
|
||||
|
||||
/** Payment history for a GRN, newest first. */
|
||||
listPayments(grnId: number): Promise<GrnPayment[]> {
|
||||
return apiRequest<GrnPayment[]>(`/grns/${grnId}/payments`)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -288,6 +288,8 @@ export interface CashAndBankAccountDto {
|
||||
cashAccountTypeName: string | null
|
||||
accountNumber: string | null
|
||||
glAccountId: number
|
||||
/** The underlying GL account's business code (added GL Phase 38, 2026-08-12). */
|
||||
glAccountCode: string
|
||||
currencyCode: string
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
@@ -100,6 +100,10 @@ export interface Grn {
|
||||
createdBy: number
|
||||
createdAt: string
|
||||
postedAt: string | null
|
||||
/** Journal number of the real GL journal entry posted for this GRN's receipt (set on confirm). */
|
||||
glJournalNo: string | null
|
||||
paidAmount: number
|
||||
balanceAmount: number
|
||||
lines: GrnLine[]
|
||||
}
|
||||
|
||||
@@ -114,6 +118,8 @@ export interface GrnSummary {
|
||||
createdAt: string
|
||||
postedAt: string | null
|
||||
lineCount: number
|
||||
paidAmount: number
|
||||
balanceAmount: number
|
||||
}
|
||||
|
||||
/** docs/11 §4.2 confirm response. */
|
||||
@@ -132,6 +138,8 @@ export interface ConfirmGrnResponse {
|
||||
grnId: number
|
||||
status: GrnStatus
|
||||
postedAt: string
|
||||
glJournalNo: string
|
||||
balanceAmount: number
|
||||
createdLayers: CreatedLayer[]
|
||||
ledgerRefs: number[]
|
||||
poStatus: PurchaseOrderStatus | null
|
||||
@@ -147,3 +155,23 @@ export interface ReleaseGrnLineResponse {
|
||||
grnLineId: number
|
||||
holdStatus: HoldStatus
|
||||
}
|
||||
|
||||
/** A vendor payment against a confirmed GRN's balance (installments allowed). */
|
||||
export interface GrnPayment {
|
||||
grnPaymentId: number
|
||||
grnId: number
|
||||
amount: number
|
||||
paymentDate: string
|
||||
glBankAccountId: number
|
||||
bankAccountName: string
|
||||
reference: string | null
|
||||
/** Journal number of the real GL journal entry this payment posted. */
|
||||
glJournalNo: string | null
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export interface CreateGrnPaymentRequest {
|
||||
amount: number
|
||||
glBankAccountId: number
|
||||
reference?: string | null
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user