Refactor GRN configuration page layout
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { ProductCapabilitiesCard } from './_components/product-capabilities-card'
|
||||
import { GrnTaxCard } from './_components/grn-tax-card'
|
||||
|
||||
export default function ConfigurationPage() {
|
||||
return (
|
||||
@@ -10,7 +11,15 @@ export default function ConfigurationPage() {
|
||||
<p className="text-sm text-muted-foreground mt-0.5">System-wide settings, grouped by area</p>
|
||||
</div>
|
||||
|
||||
<ProductCapabilitiesCard />
|
||||
{/* 12-column grid: each card takes 6 columns so the two sit side by side; they stack on narrow screens. */}
|
||||
<div className="grid grid-cols-12 items-stretch gap-6">
|
||||
<div className="col-span-12 flex lg:col-span-6 [&>*]:w-full">
|
||||
<ProductCapabilitiesCard />
|
||||
</div>
|
||||
<div className="col-span-12 flex lg:col-span-6 [&>*]:w-full">
|
||||
<GrnTaxCard />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import { Field, FieldGroup, FieldLabel } from '@/components/ui/field'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { toast } from '@/components/ui/toast'
|
||||
import type { GrnItemBody } from '@/lib/api/procurement'
|
||||
import type { GrnTaxSettings } from '@/lib/api/sys-config'
|
||||
import {
|
||||
grn as grnApi,
|
||||
purchaseOrders as purchaseOrdersApi,
|
||||
@@ -27,6 +28,8 @@ import {
|
||||
users as usersApi,
|
||||
suppliers as suppliersApi,
|
||||
items as itemsApi,
|
||||
grnTaxSettings,
|
||||
DEFAULT_GRN_TAX_SETTINGS,
|
||||
getApiError,
|
||||
getPaginatedItems,
|
||||
} from '@/lib/api/index'
|
||||
@@ -37,10 +40,11 @@ import type {
|
||||
SupplierResponse,
|
||||
ItemSummaryResponse,
|
||||
} from '@/types/api'
|
||||
import { LineTracking, trackingPayload, trackingProgress } from '../_components/grn-tracking'
|
||||
import { LineTracking, parseSerials, trackingPayload, trackingProgress } from '../_components/grn-tracking'
|
||||
import {
|
||||
DISCOUNT_OPTIONS,
|
||||
VAT_OPTIONS,
|
||||
NBT_OPTIONS,
|
||||
GrnTotals,
|
||||
NumberCell,
|
||||
PercentSelect,
|
||||
@@ -64,6 +68,7 @@ const lineSchema = z.object({
|
||||
freeQuantity: z.number().min(0, 'Must be 0 or more'),
|
||||
unitCost: z.number().min(0, 'Must be 0 or more'),
|
||||
discountPercent: percent,
|
||||
nbtPercent: percent,
|
||||
vatPercent: percent,
|
||||
// One serial number per line (newline or comma separated) — used for serial-tracked items.
|
||||
serialText: z.string(),
|
||||
@@ -77,6 +82,7 @@ const schema = z.object({
|
||||
receivedAt: z.string().min(1, 'Received date is required'),
|
||||
receivedByUserId: z.string().min(1, 'Received by is required'),
|
||||
discountPercent: percent,
|
||||
nbtPercent: percent,
|
||||
vatPercent: percent,
|
||||
items: z.array(lineSchema).min(1, 'Add at least one item'),
|
||||
})
|
||||
@@ -91,6 +97,7 @@ const emptyLine = {
|
||||
freeQuantity: 0,
|
||||
unitCost: 0,
|
||||
discountPercent: 0,
|
||||
nbtPercent: 0,
|
||||
vatPercent: 0,
|
||||
serialText: '',
|
||||
batches: [],
|
||||
@@ -116,6 +123,9 @@ export default function NewGrnPage() {
|
||||
const [poId, setPoId] = useState('')
|
||||
const [selectedPO, setSelectedPO] = useState<PurchaseOrderResponse | null>(null)
|
||||
const [loadingPO, setLoadingPO] = useState(false)
|
||||
// Which taxes apply to a GRN, and whether each is mandatory (Configuration → GRN Taxes).
|
||||
const [tax, setTax] = useState<GrnTaxSettings>(DEFAULT_GRN_TAX_SETTINGS)
|
||||
const [taxBlank, setTaxBlank] = useState({ vat: false, nbt: false })
|
||||
// Tracking flags for items loaded from a PO, keyed by itemId (the PO line response doesn't carry them).
|
||||
const [poItemInfo, setPoItemInfo] = useState<Record<string, ItemSummaryResponse>>({})
|
||||
|
||||
@@ -127,6 +137,7 @@ export default function NewGrnPage() {
|
||||
receivedAt: todayIso(),
|
||||
receivedByUserId: '',
|
||||
discountPercent: 0,
|
||||
nbtPercent: 0,
|
||||
vatPercent: 0,
|
||||
items: [{ ...emptyLine }],
|
||||
},
|
||||
@@ -154,9 +165,25 @@ export default function NewGrnPage() {
|
||||
.catch(err =>
|
||||
toast.add({ title: 'Failed to load reference data', description: getApiError(err), type: 'error' })
|
||||
)
|
||||
|
||||
// New GRNs start from the configured default rates; a disabled tax stays at 0.
|
||||
grnTaxSettings
|
||||
.get()
|
||||
.then(t => {
|
||||
setTax(t)
|
||||
applyToAllLines('vatPercent', t.vatMode === 'Disabled' ? 0 : t.vatDefaultRate)
|
||||
applyToAllLines('nbtPercent', t.nbtMode === 'Disabled' ? 0 : t.nbtDefaultRate)
|
||||
})
|
||||
.catch(err =>
|
||||
toast.add({ title: 'Failed to load GRN tax settings', description: getApiError(err), type: 'error' })
|
||||
)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
const isPoMode = mode === 'po'
|
||||
const vatOn = tax.vatMode !== 'Disabled'
|
||||
const nbtOn = tax.nbtMode !== 'Disabled'
|
||||
const colCount = 9 + (vatOn ? 1 : 0) + (nbtOn ? 1 : 0)
|
||||
const infoFor = (itemId: string) => poItemInfo[itemId] ?? itemsList.find(i => i.id === itemId)
|
||||
const poLines = selectedPO?.items?.filter(i => !i.isCancelled) ?? []
|
||||
const poLine = (poItemId: string) => poLines.find(i => i.id === poItemId)
|
||||
@@ -164,6 +191,7 @@ export default function NewGrnPage() {
|
||||
const watchedItems = form.watch('items')
|
||||
const headerDiscount = form.watch('discountPercent')
|
||||
const headerVat = form.watch('vatPercent')
|
||||
const headerNbt = form.watch('nbtPercent')
|
||||
// Block Save until every tracked line has its serials/batches fully entered (accepted + free units).
|
||||
const trackingIncomplete = watchedItems.some(v => {
|
||||
const p = trackingProgress(infoFor(v.itemId), v.acceptedQuantity + v.freeQuantity, v.serialText, v.batches)
|
||||
@@ -171,9 +199,9 @@ export default function NewGrnPage() {
|
||||
})
|
||||
|
||||
// The GRN-wide percentage is a default: choosing one fills every line, and a line can still be edited.
|
||||
function applyToAllLines(field: 'discountPercent' | 'vatPercent', value: number) {
|
||||
function applyToAllLines(field: 'discountPercent' | 'vatPercent' | 'nbtPercent', value: number) {
|
||||
form.setValue(field, value, { shouldDirty: true })
|
||||
fields.forEach((_, i) => form.setValue(`items.${i}.${field}`, value, { shouldDirty: true }))
|
||||
form.getValues('items').forEach((_, i) => form.setValue(`items.${i}.${field}`, value, { shouldDirty: true }))
|
||||
}
|
||||
|
||||
function changeMode(next: GrnMode) {
|
||||
@@ -183,9 +211,10 @@ export default function NewGrnPage() {
|
||||
setSelectedPO(null)
|
||||
const discountPercent = form.getValues('discountPercent')
|
||||
const vatPercent = form.getValues('vatPercent')
|
||||
const nbtPercent = form.getValues('nbtPercent')
|
||||
// Direct starts with one blank line; Against PO stays empty until an order is picked.
|
||||
form.setValue('supplierId', NO_SUPPLIER)
|
||||
replace(next === 'direct' ? [{ ...emptyLine, discountPercent, vatPercent }] : [])
|
||||
replace(next === 'direct' ? [{ ...emptyLine, discountPercent, vatPercent, nbtPercent }] : [])
|
||||
}
|
||||
|
||||
// A line in PO mode points at one of the order's lines; re-picking resets the cost and tracking.
|
||||
@@ -203,6 +232,7 @@ export default function NewGrnPage() {
|
||||
setPoId(nextId)
|
||||
const discountPercent = form.getValues('discountPercent')
|
||||
const vatPercent = form.getValues('vatPercent')
|
||||
const nbtPercent = form.getValues('nbtPercent')
|
||||
|
||||
setLoadingPO(true)
|
||||
try {
|
||||
@@ -225,6 +255,7 @@ export default function NewGrnPage() {
|
||||
unitCost: item.unitCost,
|
||||
discountPercent,
|
||||
vatPercent,
|
||||
nbtPercent,
|
||||
})),
|
||||
)
|
||||
} catch (err) {
|
||||
@@ -266,7 +297,8 @@ export default function NewGrnPage() {
|
||||
freeQuantity: v.freeQuantity,
|
||||
unitCost: v.unitCost,
|
||||
discountPercent: v.discountPercent,
|
||||
vatPercent: v.vatPercent,
|
||||
...(vatOn ? { vatPercent: v.vatPercent } : {}),
|
||||
...(nbtOn ? { nbtPercent: v.nbtPercent } : {}),
|
||||
...payload,
|
||||
})
|
||||
}
|
||||
@@ -279,7 +311,8 @@ export default function NewGrnPage() {
|
||||
receivedAt: `${values.receivedAt}T00:00:00.000Z`,
|
||||
items,
|
||||
discountPercent: values.discountPercent,
|
||||
vatPercent: values.vatPercent,
|
||||
...(vatOn ? { vatPercent: values.vatPercent } : {}),
|
||||
...(nbtOn ? { nbtPercent: values.nbtPercent } : {}),
|
||||
receivedByUserId: values.receivedByUserId,
|
||||
})
|
||||
} else {
|
||||
@@ -288,7 +321,8 @@ export default function NewGrnPage() {
|
||||
supplierId: values.supplierId === NO_SUPPLIER ? undefined : values.supplierId,
|
||||
receivedByUserId: values.receivedByUserId,
|
||||
discountPercent: values.discountPercent,
|
||||
vatPercent: values.vatPercent,
|
||||
...(vatOn ? { vatPercent: values.vatPercent } : {}),
|
||||
...(nbtOn ? { nbtPercent: values.nbtPercent } : {}),
|
||||
items,
|
||||
})
|
||||
}
|
||||
@@ -429,12 +463,27 @@ export default function NewGrnPage() {
|
||||
onChange={v => applyToAllLines('discountPercent', v)}
|
||||
/>
|
||||
|
||||
<PercentSelect
|
||||
label="GRN VAT %"
|
||||
value={headerVat}
|
||||
options={VAT_OPTIONS}
|
||||
onChange={v => applyToAllLines('vatPercent', v)}
|
||||
/>
|
||||
{nbtOn && (
|
||||
<PercentSelect
|
||||
label="GRN NBT %"
|
||||
value={headerNbt}
|
||||
options={NBT_OPTIONS}
|
||||
required={tax.nbtMode === 'Mandatory'}
|
||||
onBlankChange={blank => setTaxBlank(b => ({ ...b, nbt: blank }))}
|
||||
onChange={v => applyToAllLines('nbtPercent', v)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{vatOn && (
|
||||
<PercentSelect
|
||||
label="GRN VAT %"
|
||||
value={headerVat}
|
||||
options={VAT_OPTIONS}
|
||||
required={tax.vatMode === 'Mandatory'}
|
||||
onBlankChange={blank => setTaxBlank(b => ({ ...b, vat: blank }))}
|
||||
onChange={v => applyToAllLines('vatPercent', v)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
@@ -468,7 +517,7 @@ export default function NewGrnPage() {
|
||||
{isPoMode
|
||||
? 'Lines start from the purchase order. Add Item adds another line from the same order (e.g. a second batch). Unit cost starts at the PO price; change it if the supplier invoiced differently.'
|
||||
: 'Add each received item as its own line.'}{' '}
|
||||
GRN discount % and VAT % fill every line; edit a line to override it. Only accepted units are costed;
|
||||
GRN discount %{nbtOn ? ', NBT %' : ''}{vatOn ? ' and VAT %' : ''} fill every line; edit a line to override it. Only accepted units are costed;
|
||||
free units are stocked at no cost.
|
||||
</p>
|
||||
</div>
|
||||
@@ -477,7 +526,9 @@ export default function NewGrnPage() {
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={isPoMode && !selectedPO}
|
||||
onClick={() => append({ ...emptyLine, discountPercent: headerDiscount, vatPercent: headerVat })}
|
||||
onClick={() =>
|
||||
append({ ...emptyLine, discountPercent: headerDiscount, vatPercent: headerVat, nbtPercent: headerNbt })
|
||||
}
|
||||
>
|
||||
<HugeiconsIcon icon={Add01Icon} size={16} />
|
||||
Add Item
|
||||
@@ -505,7 +556,8 @@ export default function NewGrnPage() {
|
||||
<th className="w-24 px-3 py-2 text-left font-medium">Free Qty</th>
|
||||
<th className="w-28 px-3 py-2 text-left font-medium">Unit Cost</th>
|
||||
<th className="w-24 px-3 py-2 text-left font-medium">Disc %</th>
|
||||
<th className="w-24 px-3 py-2 text-left font-medium">VAT %</th>
|
||||
{nbtOn && <th className="w-24 px-3 py-2 text-left font-medium">NBT %</th>}
|
||||
{vatOn && <th className="w-24 px-3 py-2 text-left font-medium">VAT %</th>}
|
||||
<th className="w-32 px-3 py-2 text-right font-medium">Line Total</th>
|
||||
<th className="w-16 px-3 py-2"></th>
|
||||
</tr>
|
||||
@@ -519,7 +571,8 @@ export default function NewGrnPage() {
|
||||
acceptedQuantity: v?.acceptedQuantity ?? 0,
|
||||
unitCost: v?.unitCost ?? 0,
|
||||
discountPercent: v?.discountPercent ?? 0,
|
||||
vatPercent: v?.vatPercent ?? 0,
|
||||
nbtPercent: nbtOn ? (v?.nbtPercent ?? 0) : 0,
|
||||
vatPercent: vatOn ? (v?.vatPercent ?? 0) : 0,
|
||||
})
|
||||
return (
|
||||
<Fragment key={f.id}>
|
||||
@@ -596,9 +649,16 @@ export default function NewGrnPage() {
|
||||
<td className="px-3 py-2">
|
||||
<NumberCell control={form.control} name={`items.${index}.discountPercent`} step={0.01} max={100} />
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
<NumberCell control={form.control} name={`items.${index}.vatPercent`} step={0.01} max={100} />
|
||||
</td>
|
||||
{nbtOn && (
|
||||
<td className="px-3 py-2">
|
||||
<NumberCell control={form.control} name={`items.${index}.nbtPercent`} step={0.01} max={100} />
|
||||
</td>
|
||||
)}
|
||||
{vatOn && (
|
||||
<td className="px-3 py-2">
|
||||
<NumberCell control={form.control} name={`items.${index}.vatPercent`} step={0.01} max={100} />
|
||||
</td>
|
||||
)}
|
||||
|
||||
<td className="px-3 py-3 text-right font-medium">{money(amounts.total)}</td>
|
||||
|
||||
@@ -619,11 +679,14 @@ export default function NewGrnPage() {
|
||||
|
||||
{info && v && (
|
||||
<tr>
|
||||
<td colSpan={10} className="px-3 pb-3">
|
||||
<td colSpan={colCount} className="px-3 pb-3">
|
||||
<LineTracking
|
||||
info={info}
|
||||
name={info.name}
|
||||
accepted={v.acceptedQuantity + v.freeQuantity}
|
||||
otherSerials={watchedItems.flatMap((o, j) =>
|
||||
j !== index && o.itemId === v.itemId ? parseSerials(o.serialText) : [],
|
||||
)}
|
||||
serialText={v.serialText}
|
||||
batches={v.batches}
|
||||
onSerialText={val => form.setValue(`items.${index}.serialText`, val)}
|
||||
@@ -639,7 +702,15 @@ export default function NewGrnPage() {
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<GrnTotals lines={watchedItems} />
|
||||
<GrnTotals
|
||||
lines={watchedItems.map(v => ({
|
||||
...v,
|
||||
nbtPercent: nbtOn ? v.nbtPercent : 0,
|
||||
vatPercent: vatOn ? v.vatPercent : 0,
|
||||
}))}
|
||||
showNbt={nbtOn}
|
||||
showVat={vatOn}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -648,7 +719,14 @@ export default function NewGrnPage() {
|
||||
<Button type="button" variant="outline" onClick={goBack}>Cancel</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={form.formState.isSubmitting || loadingPO || fields.length === 0 || trackingIncomplete}
|
||||
disabled={
|
||||
form.formState.isSubmitting ||
|
||||
loadingPO ||
|
||||
fields.length === 0 ||
|
||||
trackingIncomplete ||
|
||||
(tax.vatMode === 'Mandatory' && taxBlank.vat) ||
|
||||
(tax.nbtMode === 'Mandatory' && taxBlank.nbt)
|
||||
}
|
||||
>
|
||||
{form.formState.isSubmitting ? 'Saving…' : 'Record Receipt'}
|
||||
</Button>
|
||||
|
||||
@@ -11,7 +11,7 @@ export { stock } from './stock'
|
||||
export { productReports } from './reports'
|
||||
export { shopPos, invoices, payments } from './sales'
|
||||
export { saleReturns, claims, supplierClaims } from './returns-claims'
|
||||
export { sysConfig, ITEM_TRACKING_KEYS } from './sys-config'
|
||||
export { sysConfig, ITEM_TRACKING_KEYS, GRN_TAX_KEYS, grnTaxSettings, DEFAULT_GRN_TAX_SETTINGS } from './sys-config'
|
||||
export { getPaginatedItems } from './paginated'
|
||||
|
||||
export type { PageParams } from './master'
|
||||
|
||||
@@ -82,6 +82,7 @@ export interface GrnItemBody {
|
||||
/** Omitted falls back to the GRN-wide percentage. */
|
||||
discountPercent?: number
|
||||
vatPercent?: number
|
||||
nbtPercent?: number
|
||||
/** Free-issue units: stocked but not costed. Serials/batches must cover accepted + free. */
|
||||
freeQuantity?: number
|
||||
}
|
||||
@@ -94,6 +95,7 @@ export interface GrnBody {
|
||||
/** GRN-wide defaults for lines that don't set their own. */
|
||||
discountPercent?: number
|
||||
vatPercent?: number
|
||||
nbtPercent?: number
|
||||
// TEMPORARY: required while auth doesn't resolve a logged-in user server-side — see testbug.md #28.
|
||||
receivedByUserId: string
|
||||
}
|
||||
@@ -104,6 +106,7 @@ export interface DirectGrnBody {
|
||||
items: GrnItemBody[]
|
||||
discountPercent?: number
|
||||
vatPercent?: number
|
||||
nbtPercent?: number
|
||||
// TEMPORARY: required while auth doesn't resolve a logged-in user server-side — see testbug.md #28.
|
||||
receivedByUserId: string
|
||||
}
|
||||
|
||||
@@ -47,6 +47,12 @@ export const stock = {
|
||||
adjust: (body: AdjustmentBody) =>
|
||||
api.post('/stock/adjustment', body).then(r => r.data),
|
||||
|
||||
/** Next free number for auto-generated serials: one past the highest `<prefix><digits>` already saved for the item. */
|
||||
nextSerialSequence: (itemId: string, prefix: string) =>
|
||||
api
|
||||
.get<{ next: number }>('/stock/serials/next-sequence', { params: { itemId, prefix } })
|
||||
.then(r => r.data.next),
|
||||
|
||||
updateMovementReason: (id: string, reasonCode: string) =>
|
||||
api.patch(`/stock/movements/${id}/reason`, { reasonCode }).then(r => r.data),
|
||||
}
|
||||
|
||||
@@ -22,3 +22,53 @@ export const sysConfig = {
|
||||
set: (key: string, value: string, userId?: string | null) =>
|
||||
api.put<SysConfigResponse>(`/sys-config/${key}`, { value, userId }).then(r => r.data),
|
||||
}
|
||||
|
||||
// ─── GRN tax settings (must match Distribution.Features.SysConfig.SysConfigService) ──────────
|
||||
|
||||
export type TaxMode = 'Disabled' | 'Optional' | 'Mandatory'
|
||||
|
||||
export const GRN_TAX_KEYS = {
|
||||
vatMode: 'Grn.Tax.Vat.Mode',
|
||||
vatRate: 'Grn.Tax.Vat.DefaultRate',
|
||||
nbtMode: 'Grn.Tax.Nbt.Mode',
|
||||
nbtRate: 'Grn.Tax.Nbt.DefaultRate',
|
||||
} as const
|
||||
|
||||
export interface GrnTaxSettings {
|
||||
vatMode: TaxMode
|
||||
vatDefaultRate: number
|
||||
nbtMode: TaxMode
|
||||
nbtDefaultRate: number
|
||||
}
|
||||
|
||||
const TAX_MODES: TaxMode[] = ['Disabled', 'Optional', 'Mandatory']
|
||||
|
||||
/** Unconfigured, VAT is Optional (as GRNs behaved before the setting existed) and NBT is off. */
|
||||
export const DEFAULT_GRN_TAX_SETTINGS: GrnTaxSettings = {
|
||||
vatMode: 'Optional',
|
||||
vatDefaultRate: 0,
|
||||
nbtMode: 'Disabled',
|
||||
nbtDefaultRate: 0,
|
||||
}
|
||||
|
||||
export function parseGrnTaxSettings(byKey: Record<string, string>): GrnTaxSettings {
|
||||
const mode = (raw: string | undefined, fallback: TaxMode): TaxMode =>
|
||||
TAX_MODES.includes(raw as TaxMode) ? (raw as TaxMode) : fallback
|
||||
const rate = (raw: string | undefined) => {
|
||||
const n = Number(raw)
|
||||
return raw !== undefined && Number.isFinite(n) && n >= 0 && n <= 100 ? n : 0
|
||||
}
|
||||
return {
|
||||
vatMode: mode(byKey[GRN_TAX_KEYS.vatMode], DEFAULT_GRN_TAX_SETTINGS.vatMode),
|
||||
vatDefaultRate: rate(byKey[GRN_TAX_KEYS.vatRate]),
|
||||
nbtMode: mode(byKey[GRN_TAX_KEYS.nbtMode], DEFAULT_GRN_TAX_SETTINGS.nbtMode),
|
||||
nbtDefaultRate: rate(byKey[GRN_TAX_KEYS.nbtRate]),
|
||||
}
|
||||
}
|
||||
|
||||
export const grnTaxSettings = {
|
||||
get: () =>
|
||||
sysConfig
|
||||
.getAll()
|
||||
.then(all => parseGrnTaxSettings(Object.fromEntries(all.map(c => [c.key, c.value])))),
|
||||
}
|
||||
|
||||
@@ -429,6 +429,8 @@ export interface GrnItemResponse {
|
||||
lineTotal: number
|
||||
/** Free-issue units received at no charge (in stock, not costed). */
|
||||
freeQuantity: number
|
||||
nbtPercent: number
|
||||
nbtAmount: number
|
||||
}
|
||||
|
||||
export interface GrnResponse {
|
||||
@@ -447,10 +449,12 @@ export interface GrnResponse {
|
||||
vatPercent: number
|
||||
subTotal: number
|
||||
discountAmount: number
|
||||
/** Subtotal - discount; the base VAT is charged on. */
|
||||
/** Subtotal - discount; the base NBT is charged on (VAT is charged on this plus NBT). */
|
||||
taxableAmount: number
|
||||
vatAmount: number
|
||||
totalAmount: number
|
||||
nbtPercent: number
|
||||
nbtAmount: number
|
||||
}
|
||||
|
||||
export interface PurchaseReturnItemResponse {
|
||||
|
||||
Reference in New Issue
Block a user