- 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.
49 lines
1.8 KiB
TypeScript
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" };
|
|
}
|
|
}
|