The approving Manager decides how much of the PO is paid first, via a 0–100% slider on the approval card (default 100% = full). The slider is convenience only — the resolved ABSOLUTE amount is stored on PurchaseOrder.suggestedAdvancePayment (Decimal(12,2), nullable). - schema + migration: add suggestedAdvancePayment (null = no explicit advance ⇒ full payment, preserves legacy behaviour). - approvePo(): accepts the amount, clamps to [0, totalAmount], persists it, records it on the APPROVED audit row. Set once at approval; never edited afterwards. - approval-actions.tsx: whole-percent slider showing the resolved ₹ amount + remaining balance; value sent with Approve / Approve-with-Remarks. - Accounts surface: payment queue + PO detail show the advance; it prefills the FIRST payment amount (only when nothing is paid yet and it is a true partial). Balance runs the normal PARTIALLY_PAID loop. - Not shown on the exported PO/invoice (po-export-layout untouched). - Tests: persist + audit metadata + clamp-to-total. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
91 lines
3.5 KiB
TypeScript
91 lines
3.5 KiB
TypeScript
import { z } from "zod";
|
|
|
|
export const lineItemSchema = z.object({
|
|
name: z.string().min(1, "Item name is required"),
|
|
description: z.string().optional(),
|
|
quantity: z.coerce.number().positive("Quantity must be positive"),
|
|
unit: z.string().min(1, "Unit is required"),
|
|
size: z.string().optional(),
|
|
unitPrice: z.coerce.number().nonnegative("Unit price must be non-negative"),
|
|
gstRate: z.coerce.number().min(0).max(1).default(0.18),
|
|
productId: z.string().optional(),
|
|
accountId: z.string().optional(),
|
|
});
|
|
|
|
export const TC_FIXED_LINE =
|
|
"Please quote this purchase order no. for further communications and invoices pertaining to this indent.";
|
|
|
|
export const TC_FIXED_LINE_2 =
|
|
"We encourage bulk packaging and avoid plastic. No asbestos to be used in any product or packing material.";
|
|
|
|
export const TC_DEFAULTS = {
|
|
tcDelivery: "Within 4 to 5 days",
|
|
tcDispatch: "To be transported to site address as above. Freight Supplier's A/C",
|
|
tcInspection: "NA",
|
|
tcTransitInsurance: "NA",
|
|
tcPaymentTerms: "Within 30 days from delivery.",
|
|
tcOthers: "",
|
|
};
|
|
|
|
export const createPoSchema = z.object({
|
|
title: z.string().min(1, "Title is required").max(200),
|
|
vesselId: z.string().min(1, "Cost Centre is required"),
|
|
accountId: z.string().min(1, "Accounting Code is required"),
|
|
companyId: z.string().optional(),
|
|
poDate: z.string().optional(),
|
|
projectCode: z.string().optional(),
|
|
dateRequired: z.string().optional(),
|
|
vendorId: z.string().optional(),
|
|
currency: z.string().default("INR"),
|
|
piQuotationNo: z.string().optional(),
|
|
piQuotationDate: z.string().optional(),
|
|
requisitionNo: z.string().optional(),
|
|
requisitionDate: z.string().optional(),
|
|
placeOfDelivery: z.string().optional(),
|
|
tcDelivery: z.string().optional(),
|
|
tcDispatch: z.string().optional(),
|
|
tcInspection: z.string().optional(),
|
|
tcTransitInsurance: z.string().optional(),
|
|
tcPaymentTerms: z.string().optional(),
|
|
tcOthers: z.string().optional(),
|
|
lineItems: z.array(lineItemSchema).min(1, "At least one line item is required"),
|
|
});
|
|
|
|
export const approvePoSchema = z.object({
|
|
note: z.string().optional(),
|
|
// Absolute advance amount the Manager wants paid first (issue #92). The UI
|
|
// slider works in whole percent of totalAmount; the resolved amount is what we
|
|
// persist. Validated against the PO total in the action. Omitted ⇒ full payment.
|
|
suggestedAdvancePayment: z.coerce
|
|
.number()
|
|
.nonnegative("Advance payment cannot be negative")
|
|
.optional(),
|
|
});
|
|
|
|
export const rejectPoSchema = z.object({
|
|
note: z.string().min(1, "A rejection reason is required"),
|
|
});
|
|
|
|
export const requestEditsSchema = z.object({
|
|
note: z.string().min(1, "Please specify what edits are needed"),
|
|
});
|
|
|
|
export const processPaymentSchema = z.object({
|
|
paymentRef: z.string().min(1, "Payment reference is required"),
|
|
paymentAmount: z.number().positive("Payment amount must be greater than 0").optional(),
|
|
paymentDate: z.coerce
|
|
.date({ required_error: "Payment date is required", invalid_type_error: "Payment date is required" })
|
|
.refine((d) => {
|
|
// Not in the future — compare against end of today (local)
|
|
const endOfToday = new Date();
|
|
endOfToday.setHours(23, 59, 59, 999);
|
|
return d.getTime() <= endOfToday.getTime();
|
|
}, "Payment date cannot be in the future"),
|
|
});
|
|
|
|
export const confirmReceiptSchema = z.object({
|
|
notes: z.string().optional(),
|
|
});
|
|
|
|
export type CreatePoInput = z.infer<typeof createPoSchema>;
|
|
export type LineItemInput = z.infer<typeof lineItemSchema>;
|