feat(efectivo): the EFE cash receipt book, folio and posting in one step
The office still issues cash receipts from Access (EFE MENU -> EFE). This is that form: cliente, fecha, monto + moneda, concepto, elaboró (the logged-in user), fecha de depósito, and a folio that continues the legacy book. - cash_receipts, folio unique. Next folio = one past the highest of the platform's receipts and the imported utilities EFECTIVO journal, so after the cutover import it lands on the office's next number (13935 on prod today). A typed folio the legacy book already used is refused. - Issuing a receipt posts its credit in the same database transaction, as legacy posted to DATOS2: reference C<folio>, CASH DEPOSIT, pseudo check CASH<MMDD><P|D>. Dollars with a rate credit a pesos ledger (1,500 USD at 16.80 -> 25,200 MXN, as C13830 did). Voiding that row voids the receipt. - /efectivo: issue, search, record the deposit date; /efectivo/<folio> prints the receipt. - Reporte de efectivo now lists platform receipts by receipt, not by check, so each is counted once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
fd5df240bc
commit
ca76f4d57a
14 files changed
+959
-3
No files matched your search
@@ -0,0 +1,98 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { getCashReceipt } from "@/lib/api";
|
||||
import { formatDate, formatMoney } from "@/lib/labels";
|
||||
import type { CashReceipt, CompanyHeader } from "@/lib/types";
|
||||
|
||||
/**
|
||||
* One EFE receipt, laid out to print and hand to the customer. No app shell:
|
||||
* the page is the paper.
|
||||
*/
|
||||
export default function CashReceiptPrint({ params }: { params: { folio: string } }) {
|
||||
const [r, setR] = useState<(CashReceipt & { company: CompanyHeader }) | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
getCashReceipt(Number(params.folio))
|
||||
.then(setR)
|
||||
.catch((e) => setError((e as Error)?.message ?? "No se encontró el recibo."));
|
||||
}, [params.folio]);
|
||||
|
||||
if (error) return <div className="state-box state-error">{error}</div>;
|
||||
if (!r) return <div className="state-box">Cargando…</div>;
|
||||
|
||||
const c = r.company;
|
||||
const voided = r.transaction.voidedAt != null;
|
||||
const converted = r.exchangeRate != null;
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: 720, margin: "24px auto", padding: 24, fontSize: 14 }}>
|
||||
<style>{"@media print { .no-print { display: none } body { background: #fff } }"}</style>
|
||||
<div className="no-print" style={{ marginBottom: 16 }}>
|
||||
<button type="button" className="btn btn-primary" onClick={() => window.print()}>
|
||||
Imprimir
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style={{ border: "1px solid #333", padding: 24 }}>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", gap: 16 }}>
|
||||
<div>
|
||||
<strong style={{ fontSize: 18 }}>{c.name}</strong>
|
||||
<div>{c.addressLine1}</div>
|
||||
{c.addressLine2 && <div>{c.addressLine2}</div>}
|
||||
<div>{c.cityState}</div>
|
||||
{c.mailingAddress && <div>{c.mailingAddress}</div>}
|
||||
<div>
|
||||
{c.phone} · {c.email}
|
||||
</div>
|
||||
{c.taxId && <div>RFC {c.taxId}</div>}
|
||||
</div>
|
||||
<div style={{ textAlign: "right" }}>
|
||||
<div style={{ fontSize: 18, fontWeight: 700 }}>RECIBO DE EFECTIVO</div>
|
||||
<div style={{ fontSize: 22, fontWeight: 700 }}>Folio {r.folio}</div>
|
||||
<div>{formatDate(r.receivedAt)}</div>
|
||||
{voided && <div style={{ color: "#b00", fontWeight: 700 }}>ANULADO</div>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table style={{ width: "100%", marginTop: 24, borderCollapse: "collapse" }}>
|
||||
<tbody>
|
||||
<Row label="Cliente">
|
||||
{r.customer.name}
|
||||
{r.customer.folderNumber != null && ` · Carpeta ${r.customer.folderNumber}`}
|
||||
</Row>
|
||||
<Row label="Monto">
|
||||
<strong>{formatMoney(r.amount, r.currency)}</strong>
|
||||
{converted &&
|
||||
` (abonado ${formatMoney(r.transaction.amount, r.transaction.currency)} a T.C. ${Number(r.exchangeRate).toFixed(4)})`}
|
||||
</Row>
|
||||
<Row label="Concepto">{r.concept}</Row>
|
||||
<Row label="Elaboró">{r.preparedBy.name}</Row>
|
||||
{r.depositedAt && <Row label="Fecha de depósito">{formatDate(r.depositedAt)}</Row>}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div style={{ marginTop: 56, display: "flex", justifyContent: "space-between" }}>
|
||||
<div style={{ borderTop: "1px solid #333", width: 240, textAlign: "center", paddingTop: 4 }}>
|
||||
Recibí
|
||||
</div>
|
||||
<div style={{ borderTop: "1px solid #333", width: 240, textAlign: "center", paddingTop: 4 }}>
|
||||
Entregó
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Row({ label, children }: { label: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<tr>
|
||||
<td style={{ padding: "6px 12px 6px 0", width: 160, color: "#555", verticalAlign: "top" }}>
|
||||
{label}
|
||||
</td>
|
||||
<td style={{ padding: "6px 0" }}>{children}</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user