price adjustment and stock adjustments
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
// Approvals inbox and settings (docs/DUAL_APPROVAL_PLAN.md §7, §10), plus price adjustments (§6).
|
||||
import { apiRequest, buildQuery } from "@/lib/api-client"
|
||||
import { PagedResponse } from "@/types/common"
|
||||
import {
|
||||
ApprovalConfig,
|
||||
CreatePriceAdjustmentRequest,
|
||||
ItemPriceHistory,
|
||||
PendingApprovals,
|
||||
PriceAdjustment,
|
||||
PriceAdjustmentStatus,
|
||||
PriceAdjustmentSummary,
|
||||
UpdateApprovalConfigRequest,
|
||||
} from "@/types/approvals"
|
||||
|
||||
export const approvalsApi = {
|
||||
/** What you can approve, plus your own requests that are waiting for others. */
|
||||
pending(): Promise<PendingApprovals> {
|
||||
return apiRequest<PendingApprovals>("/approvals/pending")
|
||||
},
|
||||
|
||||
getConfig(): Promise<ApprovalConfig> {
|
||||
return apiRequest<ApprovalConfig>("/approvals/config")
|
||||
},
|
||||
|
||||
/** Needs settings.approvals. */
|
||||
updateConfig(request: UpdateApprovalConfigRequest): Promise<ApprovalConfig> {
|
||||
return apiRequest<ApprovalConfig>("/approvals/config", { method: "PUT", body: request })
|
||||
},
|
||||
}
|
||||
|
||||
export const priceAdjustmentsApi = {
|
||||
list(params: { page?: number; pageSize?: number; q?: string; status?: PriceAdjustmentStatus; itemId?: number } = {}): Promise<PagedResponse<PriceAdjustmentSummary>> {
|
||||
return apiRequest<PagedResponse<PriceAdjustmentSummary>>(`/price-adjustments${buildQuery(params)}`)
|
||||
},
|
||||
|
||||
get(priceAdjustmentId: number): Promise<PriceAdjustment> {
|
||||
return apiRequest<PriceAdjustment>(`/price-adjustments/${priceAdjustmentId}`)
|
||||
},
|
||||
|
||||
/** 409 PRICE_ADJUSTMENT_ITEM_PENDING when an item already has a pending request. */
|
||||
create(request: CreatePriceAdjustmentRequest): Promise<PriceAdjustment> {
|
||||
return apiRequest<PriceAdjustment>("/price-adjustments", { method: "POST", body: request })
|
||||
},
|
||||
|
||||
/** The final approval applies the prices; 409 PRICE_ADJUSTMENT_STALE if an item was repriced meanwhile. */
|
||||
approve(priceAdjustmentId: number, comment?: string): Promise<PriceAdjustment> {
|
||||
return apiRequest<PriceAdjustment>(`/price-adjustments/${priceAdjustmentId}/approve`, { method: "POST", body: { comment } })
|
||||
},
|
||||
|
||||
reject(priceAdjustmentId: number, reason: string): Promise<PriceAdjustment> {
|
||||
return apiRequest<PriceAdjustment>(`/price-adjustments/${priceAdjustmentId}/reject`, { method: "POST", body: { reason } })
|
||||
},
|
||||
|
||||
/** Requester only, while pending. */
|
||||
cancel(priceAdjustmentId: number): Promise<PriceAdjustment> {
|
||||
return apiRequest<PriceAdjustment>(`/price-adjustments/${priceAdjustmentId}/cancel`, { method: "POST" })
|
||||
},
|
||||
|
||||
itemHistory(itemId: number): Promise<ItemPriceHistory[]> {
|
||||
return apiRequest<ItemPriceHistory[]>(`/items/${itemId}/price-history`)
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// Bank deposits of till cash (docs/BANK_DEPOSIT_MODULE_PLAN.md §7.3). create() posts GL (Debit bank /
|
||||
// Credit cash) and is rejected above the cash in hand; cancel() posts the reversing entry.
|
||||
import { apiRequest, buildQuery } from "@/lib/api-client"
|
||||
import {
|
||||
BankDeposit,
|
||||
BankDepositList,
|
||||
BankDepositListFilterParams,
|
||||
CashInHand,
|
||||
CashInHandDaily,
|
||||
CreateBankDepositRequest,
|
||||
} from "@/types/bank-deposits"
|
||||
|
||||
export const bankDepositsApi = {
|
||||
list(params: BankDepositListFilterParams = {}): Promise<BankDepositList> {
|
||||
return apiRequest<BankDepositList>(`/bank-deposits${buildQuery(params)}`)
|
||||
},
|
||||
|
||||
get(bankDepositId: number): Promise<BankDeposit> {
|
||||
return apiRequest<BankDeposit>(`/bank-deposits/${bankDepositId}`)
|
||||
},
|
||||
|
||||
/** 400 BANK_DEPOSIT_EXCEEDS_CASH_IN_HAND when the amount is more than can still be deposited for that day. */
|
||||
create(request: CreateBankDepositRequest): Promise<BankDeposit> {
|
||||
return apiRequest<BankDeposit>("/bank-deposits", { method: "POST", body: request })
|
||||
},
|
||||
|
||||
/** Posted only; puts the cash back in hand. */
|
||||
cancel(bankDepositId: number, reason: string): Promise<BankDeposit> {
|
||||
return apiRequest<BankDeposit>(`/bank-deposits/${bankDepositId}/cancel`, { method: "POST", body: { reason } })
|
||||
},
|
||||
|
||||
/** `date` omitted ⇒ today in the business time zone. `branchId` is ignored for branch users. */
|
||||
cashInHand(params: { date?: string; branchId?: number | null } = {}): Promise<CashInHand> {
|
||||
return apiRequest<CashInHand>(`/bank-deposits/cash-in-hand${buildQuery({ date: params.date, branchId: params.branchId ?? undefined })}`)
|
||||
},
|
||||
|
||||
/** At most 92 days. */
|
||||
cashInHandDaily(params: { from: string; to: string; branchId?: number | null }): Promise<CashInHandDaily> {
|
||||
return apiRequest<CashInHandDaily>(
|
||||
`/bank-deposits/cash-in-hand/daily${buildQuery({ from: params.from, to: params.to, branchId: params.branchId ?? undefined })}`
|
||||
)
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// Day-End Sales Report (docs/DAY_END_SALES_REPORT_PLAN.md §4). Read-only: one day's sales,
|
||||
// payments by method, expenses and income, and the same day-wise over a range (≤ 92 days).
|
||||
import { apiRequest, buildQuery } from "@/lib/api-client"
|
||||
import { DayEndDailyReport, DayEndReport } from "@/types/sales"
|
||||
|
||||
export const dayEndReportApi = {
|
||||
/** `date` omitted ⇒ today in the report's time zone. `branchId` is ignored for non-admins. */
|
||||
getDay(params: { date?: string; branchId?: number | null } = {}): Promise<DayEndReport> {
|
||||
return apiRequest<DayEndReport>(`/day-end-report${buildQuery({ date: params.date, branchId: params.branchId ?? undefined })}`)
|
||||
},
|
||||
|
||||
getDaily(params: { from: string; to: string; branchId?: number | null }): Promise<DayEndDailyReport> {
|
||||
return apiRequest<DayEndDailyReport>(
|
||||
`/day-end-report/daily${buildQuery({ from: params.from, to: params.to, branchId: params.branchId ?? undefined })}`
|
||||
)
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
// One typed client method per Expense endpoint (docs/EXPENSE_MODULE_PLAN.md §6). Draft
|
||||
// expenses are editable via update(); post() accrues to GL, pay()/postAndPay() settle it.
|
||||
import { apiRequest, apiRequestWithETag, buildQuery } from "@/lib/api-client"
|
||||
import { ApiResult, PagedResponse } from "@/types/common"
|
||||
import {
|
||||
CreateExpensePaymentRequest,
|
||||
CreateExpenseRequest,
|
||||
CreateExpenseTitleRequest,
|
||||
Expense,
|
||||
ExpenseCategory,
|
||||
ExpenseListFilterParams,
|
||||
ExpensePayment,
|
||||
ExpenseSummary,
|
||||
ExpenseSummaryReport,
|
||||
ExpenseTitle,
|
||||
UpdateExpenseCategoryRequest,
|
||||
UpdateExpenseRequest,
|
||||
UpdateExpenseTitleRequest,
|
||||
} from "@/types/expenses"
|
||||
|
||||
export const expenseSetupApi = {
|
||||
/** Categories with their titles, in display order. Inactive rows only when asked for. */
|
||||
list(includeInactive = false): Promise<ExpenseCategory[]> {
|
||||
return apiRequest<ExpenseCategory[]>(`/expense-categories${buildQuery({ includeInactive: includeInactive || undefined })}`)
|
||||
},
|
||||
|
||||
/** Categories are fixed (no create); in practice only the active flag changes — they're all built-in. */
|
||||
updateCategory(expenseCategoryId: number, request: UpdateExpenseCategoryRequest): Promise<ExpenseCategory> {
|
||||
return apiRequest<ExpenseCategory>(`/expense-categories/${expenseCategoryId}`, { method: "PUT", body: request })
|
||||
},
|
||||
|
||||
createTitle(expenseCategoryId: number, request: CreateExpenseTitleRequest): Promise<ExpenseTitle> {
|
||||
return apiRequest<ExpenseTitle>(`/expense-categories/${expenseCategoryId}/titles`, { method: "POST", body: request })
|
||||
},
|
||||
|
||||
/** 409 EXPENSE_SYSTEM_ROW_READONLY when renaming a built-in title (GL code and status stay editable). */
|
||||
updateTitle(expenseTitleId: number, request: UpdateExpenseTitleRequest): Promise<ExpenseTitle> {
|
||||
return apiRequest<ExpenseTitle>(`/expense-titles/${expenseTitleId}`, { method: "PUT", body: request })
|
||||
},
|
||||
}
|
||||
|
||||
export const expensesApi = {
|
||||
list(params: ExpenseListFilterParams = {}): Promise<PagedResponse<ExpenseSummary>> {
|
||||
return apiRequest<PagedResponse<ExpenseSummary>>(`/expenses${buildQuery(params)}`)
|
||||
},
|
||||
|
||||
/** Posted expenses dated in [from, to] (default: this month), plus the unpaid/overdue position. */
|
||||
summary(params: { from?: string; to?: string } = {}): Promise<ExpenseSummaryReport> {
|
||||
return apiRequest<ExpenseSummaryReport>(`/expenses/summary${buildQuery(params)}`)
|
||||
},
|
||||
|
||||
get(expenseId: number): Promise<ApiResult<Expense>> {
|
||||
return apiRequestWithETag<Expense>(`/expenses/${expenseId}`)
|
||||
},
|
||||
|
||||
create(request: CreateExpenseRequest): Promise<ApiResult<Expense>> {
|
||||
return apiRequestWithETag<Expense>("/expenses", { method: "POST", body: request })
|
||||
},
|
||||
|
||||
/** Draft only (409 EXPENSE_NOT_DRAFT otherwise). Echo the ETag from get()/create() as ifMatch. */
|
||||
update(expenseId: number, request: UpdateExpenseRequest, ifMatch: string): Promise<ApiResult<Expense>> {
|
||||
return apiRequestWithETag<Expense>(`/expenses/${expenseId}`, { method: "PUT", body: request, ifMatch })
|
||||
},
|
||||
|
||||
/** Draft only. */
|
||||
remove(expenseId: number): Promise<void> {
|
||||
return apiRequest<void>(`/expenses/${expenseId}`, { method: "DELETE" })
|
||||
},
|
||||
|
||||
/** Accrue to GL: Debit the title's expense account / Credit Accrued Expenses. */
|
||||
post(expenseId: number): Promise<Expense> {
|
||||
return apiRequest<Expense>(`/expenses/${expenseId}/post`, { method: "POST" })
|
||||
},
|
||||
|
||||
/** Draft only; posts and pays in one transaction — full or partial (the rest stays as the balance). */
|
||||
postAndPay(expenseId: number, request: CreateExpensePaymentRequest): Promise<Expense> {
|
||||
return apiRequest<Expense>(`/expenses/${expenseId}/post-and-pay`, { method: "POST", body: request })
|
||||
},
|
||||
|
||||
/** Draft, or Posted with no payments (posts a reversing GL entry). */
|
||||
cancel(expenseId: number, reason: string): Promise<Expense> {
|
||||
return apiRequest<Expense>(`/expenses/${expenseId}/cancel`, { method: "POST", body: { reason } })
|
||||
},
|
||||
|
||||
/** Posted only; total capped at the balance. Returns one row per payment line. */
|
||||
pay(expenseId: number, request: CreateExpensePaymentRequest): Promise<ExpensePayment[]> {
|
||||
return apiRequest<ExpensePayment[]>(`/expenses/${expenseId}/payments`, { method: "POST", body: request })
|
||||
},
|
||||
|
||||
listPayments(expenseId: number): Promise<ExpensePayment[]> {
|
||||
return apiRequest<ExpensePayment[]>(`/expenses/${expenseId}/payments`)
|
||||
},
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
ItemImage,
|
||||
ItemListItem,
|
||||
ItemOverview,
|
||||
ItemPurchasePrice,
|
||||
ItemReorderSetting,
|
||||
ItemScanResult,
|
||||
StockNature,
|
||||
@@ -95,6 +96,11 @@ export const itemsApi = {
|
||||
return apiRequest<void>(`/items/${itemId}/status`, { method: "PATCH", body: { status } })
|
||||
},
|
||||
|
||||
/** Latest posted GRN receipts of the item (newest first) — what it was bought at. */
|
||||
purchasePrices(itemId: number, take = 5): Promise<ItemPurchasePrice[]> {
|
||||
return apiRequest<ItemPurchasePrice[]>(`/items/${itemId}/purchase-prices${buildQuery({ take })}`)
|
||||
},
|
||||
|
||||
updateReorder(itemId: number, request: UpdateItemReorderRequest): Promise<{ settings: ItemReorderSetting[] }> {
|
||||
return apiRequest<{ settings: ItemReorderSetting[] }>(`/items/${itemId}/reorder`, {
|
||||
method: "PUT",
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
// One typed client method per adjustment endpoint (docs/11-BACKEND-PHASE1.md §5.5).
|
||||
// Auto-posts on create with a mandatory reason code (FR-STK-07).
|
||||
// One typed client method per adjustment endpoint (docs/11-BACKEND-PHASE1.md §5.5). Creating
|
||||
// submits a request with a mandatory reason code; it moves stock once approved
|
||||
// (docs/DUAL_APPROVAL_PLAN.md §5), or at once when approvals are switched off.
|
||||
import { apiRequest, buildQuery } from "@/lib/api-client"
|
||||
import { PagedResponse } from "@/types/common"
|
||||
import { CreateAdjustmentRequest, StockAdjustment, StockAdjustmentSummary } from "@/types/stock"
|
||||
import { AdjustmentStatus, CreateAdjustmentRequest, StockAdjustment, StockAdjustmentSummary } from "@/types/stock"
|
||||
|
||||
export interface ListAdjustmentsParams {
|
||||
page?: number
|
||||
@@ -11,6 +12,7 @@ export interface ListAdjustmentsParams {
|
||||
warehouseId?: number
|
||||
branchId?: number
|
||||
reasonCodeId?: number
|
||||
status?: AdjustmentStatus
|
||||
sort?: string
|
||||
}
|
||||
|
||||
@@ -30,4 +32,18 @@ export const stockAdjustmentsApi = {
|
||||
create(request: CreateAdjustmentRequest): Promise<StockAdjustment> {
|
||||
return apiRequest<StockAdjustment>("/stock-adjustments", { method: "POST", body: request })
|
||||
},
|
||||
|
||||
/** Next approval; the final one posts (409 STOCK_NEGATIVE_BLOCKED if stock no longer allows it). */
|
||||
approve(adjustmentId: number, comment?: string): Promise<StockAdjustment> {
|
||||
return apiRequest<StockAdjustment>(`/stock-adjustments/${adjustmentId}/approve`, { method: "POST", body: { comment } })
|
||||
},
|
||||
|
||||
reject(adjustmentId: number, reason: string): Promise<StockAdjustment> {
|
||||
return apiRequest<StockAdjustment>(`/stock-adjustments/${adjustmentId}/reject`, { method: "POST", body: { reason } })
|
||||
},
|
||||
|
||||
/** Requester only, while pending. */
|
||||
cancel(adjustmentId: number): Promise<StockAdjustment> {
|
||||
return apiRequest<StockAdjustment>(`/stock-adjustments/${adjustmentId}/cancel`, { method: "POST" })
|
||||
},
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import { reasonCodesApi } from "@/lib/api/reason-codes"
|
||||
import { stockAdjustmentsApi } from "@/lib/api/stock-adjustments"
|
||||
import { stockApi } from "@/lib/api/stock"
|
||||
import { StockAdjustment } from "@/types/stock"
|
||||
import { AdjustmentStatus, StockAdjustment } from "@/types/stock"
|
||||
|
||||
/**
|
||||
* Reason codes treated as "wastage" (loss-type) causes. These match the seeded codes in
|
||||
@@ -37,8 +37,10 @@ export interface WastageRecord {
|
||||
reasonCodeId: number
|
||||
/** Positive quantity wasted (the underlying adjustment line is a negative qtyDelta). */
|
||||
qty: number
|
||||
/** FIFO cost of the wasted quantity, summed from the ledger entries this adjustment posted. */
|
||||
/** FIFO cost of the wasted quantity, summed from the ledger entries this adjustment posted (0 until posted). */
|
||||
value: number
|
||||
/** Posted, or still waiting for approval (rejected/withdrawn requests are left out). */
|
||||
status: AdjustmentStatus
|
||||
}
|
||||
|
||||
export interface ListWastageParams {
|
||||
@@ -81,7 +83,8 @@ export const wastageApi = {
|
||||
page: params.page,
|
||||
pageSize: params.pageSize ?? 50,
|
||||
})
|
||||
const summaries = res.items.filter((a) => wastageIds.has(a.reasonCodeId))
|
||||
// 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 detailed = await Promise.all(
|
||||
summaries.map(async (summary) => {
|
||||
@@ -107,6 +110,7 @@ export const wastageApi = {
|
||||
reasonCodeId: adjustment.reasonCodeId,
|
||||
qty: Math.abs(line.qtyDelta),
|
||||
value: Math.round(value * 100) / 100,
|
||||
status: adjustment.status,
|
||||
}
|
||||
})
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user