Files
Voiced/src/lib/payments.ts
T
xavierk 3591bb9f7d Add the ERPNext settings UI, History and detail controls, and voiced.invoice.v1 export (Phase F3)
- Settings gets an Integrations tab: connection fields with a write-only API secret, a CA PEM option, Test connection with versions and warnings, live mapping lists loaded after a test, naming mode, toggles and its own Save.
- Pushing for a GST-registered vendor is refused until a connection test result is stored. The UI shows the reason and disables Send.
- History shows an ERPNext status column from one bulk call, a row Send and Open in ERPNext, and a bulk Send selected with a draft-or-submit confirm step and per-row results that never abort the batch. The detail view gets an ERPNext panel and per-payment send. All of it is hidden or disabled when the integration is not configured.
- Optional auto-push after issue runs in the background and never blocks or fails the issue.
- Generic export: a deterministic voiced.invoice.v1 JSON (and CSV line items) for selected invoices, documented in docs/voiced-invoice-v1.md with a golden test. Money is integer paise plus a decimal string.

The new screens have not been run in a webview yet. The ERPNext Payment Entry deduction fields still need a live check.
2026-10-04 20:57:31 +05:30

49 lines
1.8 KiB
TypeScript

import type { InvoiceStatus, PaymentStatus } from "./types";
export type PaymentMode = "bank_transfer" | "upi" | "cash" | "cheque" | "other";
export const PAYMENT_MODES: { id: PaymentMode; label: string }[] = [
{ id: "bank_transfer", label: "Bank transfer" },
{ id: "upi", label: "UPI" },
{ id: "cash", label: "Cash" },
{ id: "cheque", label: "Cheque" },
{ id: "other", label: "Other" },
];
export const paymentModeLabel = (mode: string): string => PAYMENT_MODES.find((m) => m.id === mode)?.label ?? mode;
/** A payment as stored: amounts are integer paise. `amountPaise` is cash; `tdsPaise` is tax the client deducted. */
export interface Payment {
id: number;
invoiceId: number;
paidOn: string;
amountPaise: number;
tdsPaise: number;
mode: PaymentMode;
reference: string;
notes: string;
createdAt: string;
/** Payment Entry name on ERPNext once the payment was sent. */
erpnextPaymentEntry?: string | null;
}
export type PaymentInput = Omit<Payment, "id" | "createdAt" | "erpnextPaymentEntry">;
type TagType = "red" | "green" | "blue" | "gray" | "warm-gray" | "cool-gray" | "purple";
/** What to show in the status column and badges: the cancelled state wins over payment state. */
export function statusBadge(status: InvoiceStatus, payment: PaymentStatus | undefined): { label: string; type: TagType } {
if (status === "cancelled") return { label: "Cancelled", type: "gray" };
if (status === "draft") return { label: "Draft", type: "warm-gray" };
switch (payment) {
case "paid":
return { label: "Paid", type: "green" };
case "partially_paid":
return { label: "Partly paid", type: "blue" };
case "overdue":
return { label: "Overdue", type: "red" };
default:
return { label: "Unpaid", type: "cool-gray" };
}
}