Add migration to include AccruedExpensesCode in gl_account_config table

- Introduced a new column 'AccruedExpensesCode' of type varchar(20) to the 'gl_account_config' table.
- Added SQL command to update existing records with a default value of '2300' for the 'AccruedExpensesCode' where it is currently null.
This commit is contained in:
Dhananjaya99
2026-10-18 16:21:38 +05:30
parent dcdb628647
commit a0793b35d3
31 changed files with 23185 additions and 94 deletions
@@ -3,7 +3,7 @@
import { useEffect, useMemo, useState } from "react"
import { useParams, usePathname, useRouter } from "next/navigation"
import Link from "next/link"
import { AlertTriangle, ArrowLeft, BadgeDollarSign, Boxes, Package, PackageCheck, Pencil, Save, ShoppingCart, TrendingUp } from "lucide-react"
import { AlertOctagon, AlertTriangle, ArrowLeft, BadgeDollarSign, Boxes, Package, PackageCheck, Pencil, Save, ShoppingCart, TrendingUp } from "lucide-react"
import { approvalsApi, priceAdjustmentsApi } from "@/lib/api/approvals"
import { itemsApi } from "@/lib/api/items"
@@ -14,6 +14,8 @@ import { uomsApi } from "@/lib/api/uoms"
import { taxRatesApi } from "@/lib/api/tax-rates"
import { warehousesApi } from "@/lib/api/warehouses"
import { stockApi } from "@/lib/api/stock"
import { reasonCodesApi } from "@/lib/api/reason-codes"
import { WastageRecord, wastageApi } from "@/lib/api/wastage"
import { errorMessage, fieldErrors } from "@/lib/error-map"
import { validateItemForm } from "@/lib/validations/master-data"
import { cn } from "@/lib/utils"
@@ -104,6 +106,9 @@ function ItemDetailPage() {
// Profile activity: full recent movement history for this item, across every warehouse.
const [ledger, setLedger] = useState<LedgerEntry[] | null>(null)
// Damage / theft / expiry write-offs of this item (stock adjustments with a wastage reason), and the reason names.
const [wastage, setWastage] = useState<WastageRecord[] | null>(null)
const [reasonNames, setReasonNames] = useState<Map<number, string>>(new Map())
const [stockValue, setStockValue] = useState<number | null>(null)
// Basic info form
@@ -218,6 +223,11 @@ function ItemDetailPage() {
useEffect(() => {
if (!Number.isFinite(itemId)) return
stockApi.ledger({ itemId, pageSize: 500 }).then((res) => setLedger(res.items)).catch(() => setLedger([]))
wastageApi.list({ itemId, pageSize: 100 }).then(setWastage).catch(() => setWastage([]))
reasonCodesApi
.list("Adjustment", { pageSize: 200 })
.then((r) => setReasonNames(new Map(r.items.map((x) => [x.reasonCodeId, x.description]))))
.catch(() => {})
stockApi
.onHandList({ itemId, pageSize: 200 })
.then((res) => res.items.filter((r) => r.onHand > 0))
@@ -280,6 +290,8 @@ function ItemDetailPage() {
.reduce((sum, r) => sum + r.qtyBase, 0),
[sortedLedger, cutoff30]
)
// Wastage that has actually reduced stock counts; a request still waiting for approval is listed but not counted.
const wastedQty = useMemo(() => (wastage ?? []).filter((w) => w.status === "Posted").reduce((sum, w) => sum + w.qty, 0), [wastage])
const recentSales = useMemo(
() => sortedLedger.filter((r) => r.direction === "Out" && SALES_DOC_TYPES.has(r.sourceDocType)).slice(-8).reverse(),
[sortedLedger]
@@ -626,7 +638,7 @@ function ItemDetailPage() {
</div>
</div>
<div className="grid grid-cols-2 gap-3 lg:grid-cols-4">
<div className="grid grid-cols-2 gap-3 lg:grid-cols-5">
<StatChip icon={Boxes} label={`On hand (${uomName(item.baseUomId)})`} value={currentStockQty.toLocaleString()} />
<StatChip
icon={BadgeDollarSign}
@@ -635,6 +647,7 @@ function ItemDetailPage() {
/>
<StatChip icon={ShoppingCart} label="Sold, last 30 days" value={salesQty30d.toLocaleString()} />
<StatChip icon={PackageCheck} label="Purchases on record" value={recentGrns.length > 0 ? `${sortedLedger.filter((r) => r.sourceDocType === "GRN").length}` : "0"} />
<StatChip icon={AlertOctagon} label="Wasted, all time" value={wastage === null ? "…" : wastedQty.toLocaleString()} />
</div>
{conflict && (
@@ -919,6 +932,76 @@ function ItemDetailPage() {
<div className="flex flex-col gap-3 rounded-xl border p-5">
<h2 className="text-base font-semibold text-foreground">Recent wastage</h2>
{wastage === null ? (
<Skeleton className="h-32 w-full" />
) : wastage.length === 0 ? (
<p className="py-6 text-center text-sm text-muted-foreground">No wastage recorded for this item.</p>
) : (
<>
{/* Mobile: stacked cards. */}
<div className="flex flex-col gap-2 md:hidden">
{wastage.slice(0, 8).map((w) => (
<div key={`${w.adjustmentId}-${w.adjLineId}`} className="flex flex-col gap-1.5 rounded-lg border p-3 text-sm">
<div className="flex items-center justify-between gap-2">
<Link href={`/dashboard/stock/adjustments/${w.adjustmentId}`} className="truncate font-medium text-primary hover:underline">
{w.docNo}
</Link>
<span className="shrink-0 text-xs text-muted-foreground">{new Date(w.createdAt).toLocaleDateString()}</span>
</div>
<div className="flex items-center justify-between text-muted-foreground">
<span>{reasonNames.get(w.reasonCodeId) ?? `Reason #${w.reasonCodeId}`} · Qty {w.qty.toLocaleString()}</span>
<span className="font-medium text-foreground">
{w.status === "Posted" ? `LKR ${w.value.toLocaleString(undefined, { maximumFractionDigits: 2 })}` : "Pending approval"}
</span>
</div>
</div>
))}
</div>
{/* Desktop: table. */}
<div className="hidden overflow-x-auto md:block">
<Table className="text-sm">
<TableHeader>
<TableRow>
<TableHead className="h-9 px-2 text-xs">Date</TableHead>
<TableHead className="h-9 px-2 text-xs">Document</TableHead>
<TableHead className="h-9 px-2 text-xs">Reason</TableHead>
<TableHead className="h-9 px-2 text-xs">Qty</TableHead>
<TableHead className="h-9 px-2 text-xs">Value</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{wastage.slice(0, 8).map((w) => (
<TableRow key={`${w.adjustmentId}-${w.adjLineId}`}>
<TableCell className="px-2 py-2 text-muted-foreground">{new Date(w.createdAt).toLocaleDateString()}</TableCell>
<TableCell className="px-2 py-2">
<Link href={`/dashboard/stock/adjustments/${w.adjustmentId}`} className="font-medium text-primary hover:underline">
{w.docNo}
</Link>
</TableCell>
<TableCell className="px-2 py-2 text-muted-foreground">{reasonNames.get(w.reasonCodeId) ?? `#${w.reasonCodeId}`}</TableCell>
<TableCell className="px-2 py-2 font-medium text-destructive">-{w.qty.toLocaleString()}</TableCell>
<TableCell className="px-2 py-2">
{w.status === "Posted" ? (
`LKR ${w.value.toLocaleString(undefined, { maximumFractionDigits: 2 })}`
) : (
<span className="rounded bg-warning/10 px-1.5 py-0.5 text-xs text-warning">Pending approval</span>
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
<Link href="/dashboard/stock/wastage" className="self-end text-sm font-medium text-primary hover:underline">
All wastage
</Link>
</>
)}
</div>
<p className="text-sm text-muted-foreground">
{uomName(item.baseUomId)} is the base UOM — every transaction records quantities as a count of it (FR-MD-03).
There are no conversions: a differently sized pack is a different item.
@@ -14,6 +14,7 @@ import { Field, FieldDescription, FieldLabel } from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Skeleton } from "@/components/ui/skeleton"
import { Switch } from "@/components/ui/switch"
import { toast } from "@/components/ui/toast"
const levelOptions = [
@@ -28,6 +29,7 @@ export default function ApprovalSettingsPage() {
const [error, setError] = useState<string | null>(null)
const [stockLevels, setStockLevels] = useState("2")
const [stockThreshold, setStockThreshold] = useState("0")
const [wastageDual, setWastageDual] = useState(false)
const [priceLevels, setPriceLevels] = useState("2")
const [priceThreshold, setPriceThreshold] = useState("0")
const [level2Roles, setLevel2Roles] = useState<number[]>([])
@@ -38,6 +40,7 @@ export default function ApprovalSettingsPage() {
setConfig(c)
setStockLevels(String(c.stockAdjustmentLevels))
setStockThreshold(String(c.stockAdjustmentLevel2ThresholdValue))
setWastageDual((c.wastageLevels ?? 0) > 0)
setPriceLevels(String(c.priceAdjustmentLevels))
setPriceThreshold(String(c.priceAdjustmentLevel2ThresholdPercent))
setLevel2Roles(c.level2RoleIds)
@@ -60,6 +63,7 @@ export default function ApprovalSettingsPage() {
const saved = await approvalsApi.updateConfig({
stockAdjustmentLevels: Number(stockLevels),
stockAdjustmentLevel2ThresholdValue: sv,
wastageLevels: wastageDual ? 2 : 0,
priceAdjustmentLevels: Number(priceLevels),
priceAdjustmentLevel2ThresholdPercent: pv,
level2RoleIds: level2Roles,
@@ -81,7 +85,7 @@ export default function ApprovalSettingsPage() {
<div>
<h1 className="text-2xl font-bold text-foreground">Approvals</h1>
<p className="text-base text-muted-foreground">
How many people must approve a stock or price adjustment before it takes effect. The requester can never approve their own request,
How many people must approve a stock adjustment, wastage or price adjustment before it takes effect. The requester can never approve their own request,
and with two approvals the second must be a different person from the first. Changes apply to new requests; pending ones keep their rule.
</p>
</div>
@@ -89,7 +93,7 @@ export default function ApprovalSettingsPage() {
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
<section className="flex flex-col gap-4 rounded-2xl border bg-card p-5 shadow-sm">
<h2 className="text-lg font-semibold text-foreground">Stock adjustments</h2>
<p className="text-sm text-muted-foreground">Adjustments, wastage and stock-count variances.</p>
<p className="text-sm text-muted-foreground">Manual adjustments and stock-count variances. Wastage has its own switch below.</p>
<Field>
<FieldLabel>Approvals required</FieldLabel>
<Select<string> value={stockLevels} onValueChange={(v) => v && setStockLevels(v)}>
@@ -106,6 +110,27 @@ export default function ApprovalSettingsPage() {
)}
</section>
<section className="flex flex-col gap-4 rounded-2xl border bg-card p-5 shadow-sm">
<h2 className="text-lg font-semibold text-foreground">Wastage</h2>
<p className="text-sm text-muted-foreground">Damage, theft / loss and expiry write-offs.</p>
<div className="flex items-start gap-4 py-1">
<Switch
checked={wastageDual}
onCheckedChange={setWastageDual}
disabled={saving}
aria-label="Wastage needs dual approval"
className="mt-1 shrink-0"
/>
<div className="flex flex-col gap-1">
<span className="text-base font-medium text-foreground">Wastage needs dual approval</span>
<span className="text-sm text-muted-foreground">
Off: recording wastage reduces stock immediately. On: two different approvers must approve before stock is reduced
(the request waits in the Approval Queue until then).
</span>
</div>
</div>
</section>
<section className="flex flex-col gap-4 rounded-2xl border bg-card p-5 shadow-sm">
<h2 className="text-lg font-semibold text-foreground">Price adjustments</h2>
<p className="text-sm text-muted-foreground">
@@ -2,16 +2,18 @@
import { useEffect, useMemo, useState } from "react"
import Link from "next/link"
import { AlertOctagon, CheckCircle2, X } from "lucide-react"
import { useRouter } from "next/navigation"
import { AlertOctagon, X } from "lucide-react"
import { isWastageReasonCode, wastageApi } from "@/lib/api/wastage"
import { reasonCodesApi } from "@/lib/api/reason-codes"
import { warehousesApi } from "@/lib/api/warehouses"
import { itemsApi } from "@/lib/api/items"
import { approvalsApi } from "@/lib/api/approvals"
import { stockApi } from "@/lib/api/stock"
import { errorMessage } from "@/lib/error-map"
import { cn } from "@/lib/utils"
import { OnHand, ReasonCode, StockAdjustment } from "@/types/stock"
import { OnHand, ReasonCode } from "@/types/stock"
import { Bin, ItemListItem, Warehouse } from "@/types/master-data"
import { useAuth } from "@/components/auth/AuthProvider"
@@ -25,6 +27,7 @@ import { SuggestInput, SuggestOption } from "@/components/ui/suggest-input"
import { toast } from "@/components/ui/toast"
export default function NewWastagePage() {
const router = useRouter()
const { isAdmin, branchId: myBranchId, activeBranchId, branchingEnabled } = useAuth()
const [warehouses, setWarehouses] = useState<Warehouse[] | null>(null)
const [items, setItems] = useState<ItemListItem[] | null>(null)
@@ -45,7 +48,12 @@ export default function NewWastagePage() {
const [errors, setErrors] = useState<Record<string, string>>({})
const [submitError, setSubmitError] = useState<string | null>(null)
const [submitting, setSubmitting] = useState(false)
const [result, setResult] = useState<StockAdjustment | null>(null)
// Settings -> Approvals "Wastage needs dual approval". Null until read; the server decides either way.
const [needsApproval, setNeedsApproval] = useState<boolean | null>(null)
useEffect(() => {
approvalsApi.getConfig().then((c) => setNeedsApproval((c.wastageLevels ?? 0) > 0)).catch(() => setNeedsApproval(null))
}, [])
useEffect(() => {
const allActiveItems = async () => {
@@ -119,9 +127,10 @@ export default function NewWastagePage() {
qty: Number(qty),
reasonCodeId: reasonCodeId as number,
})
setResult(adjustment)
if (adjustment.status === "Posted") toast.success("Wastage recorded", `${adjustment.docNo} — stock written off.`)
else toast.success("Wastage submitted for approval", `${adjustment.docNo} is written off once ${adjustment.requiredApprovals === 2 ? "two people approve it" : "it is approved"}.`)
// Straight back to the wastage table, where the new record is listed.
router.push("/dashboard/stock/wastage")
} catch (err) {
setSubmitError(errorMessage(err))
toast.error("Could not record wastage", errorMessage(err))
@@ -130,16 +139,6 @@ export default function NewWastagePage() {
}
}
function startAnother() {
setResult(null)
setItemId(null)
setQty("")
setReasonCodeId(null)
setSubmitError(null)
setOnHand(null)
setOnHandError(null)
}
// Same picker as Create Sale / New GRN: type a SKU or name, pick from the list.
const itemOptions = useMemo<SuggestOption[]>(
() => (items ?? []).map((i) => ({ value: String(i.itemId), label: `${i.sku} — ${i.name}` })),
@@ -154,7 +153,11 @@ export default function NewWastagePage() {
<div className="flex items-center gap-3">
<div>
<h1 className="text-2xl font-bold text-foreground">Record Wastage</h1>
<p className="text-base text-muted-foreground">Posts immediately as a stock adjustment (FR-STK-07) — a reason code is mandatory.</p>
<p className="text-base text-muted-foreground">
{needsApproval
? "Needs two approvals before stock is reduced (set in Settings → Approvals) — a reason code is mandatory."
: "Reduces stock immediately, as a stock adjustment (FR-STK-07) — a reason code is mandatory."}
</p>
</div>
</div>
@@ -164,38 +167,7 @@ export default function NewWastagePage() {
{loading && !loadError && <Skeleton className="h-12 w-full" />}
{!loading && result && (
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-2 rounded-xl border border-success/30 bg-success/5 p-5">
<div className="flex items-center gap-2 text-success">
<CheckCircle2 className="size-6" />
<p className="text-base font-semibold">
{result.status === "Posted" ? `${result.docNo} posted` : `${result.docNo} submitted for approval`}
</p>
</div>
<div className="text-sm text-muted-foreground">
{result.status === "Posted" ? (
<>Ledger refs: {result.ledgerRefs.join(", ")}</>
) : (
<>
Stock is written off once approved.{" "}
<Link href={`/dashboard/stock/adjustments/${result.adjustmentId}`} className="font-medium text-primary hover:underline">View request</Link>
</>
)}
</div>
</div>
<div className="flex gap-3">
<Link href="/dashboard/stock/wastage" className={cn(buttonVariants({ variant: "outline", size: "lg" }))}>
Back to wastage report
</Link>
<Button size="lg" type="button" onClick={startAnother}>
Record another
</Button>
</div>
</div>
)}
{!loading && !result && (
{!loading && (
<>
{(reasonCodes ?? []).length === 0 && (
<div className="flex items-center gap-2 rounded-lg border border-warning/30 bg-warning/5 p-4 text-base text-warning">
@@ -40,10 +40,17 @@ export default function WastagePage() {
const [pageSize, setPageSize] = useState(PAGE_SIZE_OPTIONS[1])
useEffect(() => {
Promise.all([reasonCodesApi.list("Adjustment"), itemsApi.list({ pageSize: 200 }), warehousesApi.list()])
const allItems = async () => {
const first = await itemsApi.list({ pageSize: 200 })
const rest = await Promise.all(
Array.from({ length: Math.max(first.pagination.totalPages - 1, 0) }, (_, i) => itemsApi.list({ pageSize: 200, page: i + 2 })),
)
return [first, ...rest].flatMap((r) => r.items)
}
Promise.all([reasonCodesApi.list("Adjustment"), allItems(), warehousesApi.list()])
.then(([rc, it, wh]) => {
setReasonCodes(rc.items.filter((r) => isWastageReasonCode(r.code)))
setItems(it.items)
setItems(it)
setWarehouses(wh.items)
})
.catch((err) => setError(errorMessage(err)))
@@ -72,11 +79,16 @@ export default function WastagePage() {
: null
const pagedRecords = records?.slice((page - 1) * pageSize, page * pageSize) ?? null
// Only wastage that has actually reduced stock counts towards the totals; requests still waiting for approval are
// counted apart (they have no cost yet — FIFO costs are fixed when the stock is written off).
const totals = useMemo(() => {
if (!records) return { qty: 0, value: 0 }
if (!records) return { qty: 0, value: 0, pending: 0 }
return records.reduce(
(acc, r) => ({ qty: acc.qty + r.qty, value: Math.round((acc.value + r.value) * 100) / 100 }),
{ qty: 0, value: 0 }
(acc, r) =>
r.status === "Posted"
? { ...acc, qty: acc.qty + r.qty, value: Math.round((acc.value + r.value) * 100) / 100 }
: { ...acc, pending: acc.pending + 1 },
{ qty: 0, value: 0, pending: 0 }
)
}, [records])
@@ -107,7 +119,7 @@ export default function WastagePage() {
)}
{!loading && (
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
<div className="rounded-xl border border-destructive/20 bg-destructive/5 p-5">
<p className="text-sm text-muted-foreground">Total quantity wasted</p>
<p className="text-2xl font-semibold text-destructive">{totals.qty}</p>
@@ -116,6 +128,10 @@ export default function WastagePage() {
<p className="text-sm text-muted-foreground">Total value wasted (LKR)</p>
<p className="text-2xl font-semibold text-destructive">{totals.value.toFixed(2)}</p>
</div>
<div className="rounded-xl border border-warning/30 bg-warning/5 p-5">
<p className="text-sm text-muted-foreground">Waiting for approval</p>
<p className="text-2xl font-semibold text-warning">{totals.pending}</p>
</div>
</div>
)}
@@ -216,7 +232,7 @@ export default function WastagePage() {
</div>
<div>
<p className="text-muted-foreground">Value (LKR)</p>
<p className="font-medium text-foreground">{r.value.toFixed(2)}</p>
<p className="font-medium text-foreground">{r.status === "Posted" ? r.value.toFixed(2) : "—"}</p>
</div>
<div className="col-span-2">
<p className="text-muted-foreground">Date</p>
@@ -268,7 +284,7 @@ export default function WastagePage() {
</Badge>
</TableCell>
<TableCell className="text-right font-semibold text-destructive tabular-nums">-{r.qty}</TableCell>
<TableCell className="text-right text-sm text-muted-foreground tabular-nums">{r.value.toFixed(2)}</TableCell>
<TableCell className="text-right text-sm text-muted-foreground tabular-nums">{r.status === "Posted" ? r.value.toFixed(2) : "—"}</TableCell>
</TableRow>
)
})}
@@ -13,6 +13,10 @@ export interface ListAdjustmentsParams {
branchId?: number
reasonCodeId?: number
status?: AdjustmentStatus
/** Only wastage: adjustments with a damage / theft / expiry reason code. */
wastage?: boolean
/** Only adjustments with a line for this item. */
itemId?: number
sort?: string
}
+8 -7
View File
@@ -44,6 +44,8 @@ export interface WastageRecord {
}
export interface ListWastageParams {
/** Only this item's wastage (the product page). */
itemId?: number
warehouseId?: number
branchId?: number
reasonCodeId?: number
@@ -71,20 +73,19 @@ export const wastageApi = {
* because the adjustment page is already bounded.
*/
async list(params: ListWastageParams = {}): Promise<WastageRecord[]> {
const wastageIds = new Set(await wastageReasonCodeIds())
if (wastageIds.size === 0) return []
// The API filters by a single reasonCodeId, so a specific pick can be pushed down;
// otherwise fetch the page and keep the loss-type rows.
// The server returns only wastage (damage / theft / expiry reasons), newest first, so a busy adjustment history
// can't push the wastage out of the page the way a browser-side filter over mixed adjustments could.
const res = await stockAdjustmentsApi.list({
warehouseId: params.warehouseId,
branchId: params.branchId,
reasonCodeId: params.reasonCodeId,
itemId: params.itemId,
wastage: true,
page: params.page,
pageSize: params.pageSize ?? 50,
})
// A rejected or withdrawn request never wrote anything off.
const summaries = res.items.filter((a) => wastageIds.has(a.reasonCodeId) && a.status !== "Rejected" && a.status !== "Cancelled")
const summaries = res.items.filter((a) => a.status !== "Rejected" && a.status !== "Cancelled")
const detailed = await Promise.all(
summaries.map(async (summary) => {
@@ -94,7 +95,7 @@ export const wastageApi = {
])
return adjustment.lines
.filter((line) => line.qtyDelta < 0) // wastage is always a decrease
.filter((line) => line.qtyDelta < 0 && (params.itemId === undefined || line.itemId === params.itemId)) // wastage is always a decrease
.map<WastageRecord>((line) => {
const value = ledger.items
.filter((l) => l.itemId === line.itemId && l.direction === "Out")
+2
View File
@@ -43,6 +43,8 @@ export interface ApprovalConfig {
stockAdjustmentLevels: number
/** With 2 levels: adjustments worth less than this need one approval. 0 = always two. */
stockAdjustmentLevel2ThresholdValue: number
/** Approvals wastage needs: 0 = none (stock reduces on save), 2 = two approvers first. */
wastageLevels: number
priceAdjustmentLevels: number
/** With 2 levels: price changes smaller than this % need one approval. 0 = always two. */
priceAdjustmentLevel2ThresholdPercent: number
@@ -21,6 +21,7 @@ export type GlAccountRole =
| "Cogs"
| "InventoryLoss"
| "Cash"
| "AccruedExpenses"
export interface GlAccountOption {
accountCode: string
@@ -62,6 +63,7 @@ export interface UpdateGlAccountConfigRequest {
cogsAccountCode: string | null
inventoryLossAccountCode: string | null
cashAccountCode: string | null
accruedExpensesCode: string | null
}
/** Request-field name per role, so the page can build the PUT body from the mapping list. */
@@ -78,6 +80,7 @@ export const GL_ROLE_FIELD: Record<GlAccountRole, keyof UpdateGlAccountConfigReq
Cogs: "cogsAccountCode",
InventoryLoss: "inventoryLossAccountCode",
Cash: "cashAccountCode",
AccruedExpenses: "accruedExpensesCode",
}
/** What each role means in accounting terms, and which flows post to it. */
@@ -94,6 +97,7 @@ export const GL_ROLE_LABELS: Record<GlAccountRole, { label: string; description:
Cogs: { label: "Cost of Goods Sold", description: "Cost of sales — day-end, sales returns" },
InventoryLoss: { label: "Inventory Loss", description: "Stock written off by an adjustment or count" },
Cash: { label: "Cash", description: "Cash takings at day-end close" },
AccruedExpenses: { label: "Accrued Expenses", description: "Expenses booked but not yet paid — Accounts → Expenses (post and pay)" },
}
/** Display order: assets, liabilities, income, expense — the order an accountant reads them in. */
@@ -105,6 +109,7 @@ export const GL_ROLE_ORDER: GlAccountRole[] = [
"Clearing",
"TaxPayable",
"AccountsPayable",
"AccruedExpenses",
"SalesRevenue",
"SalesDiscount",
"InventoryGain",