Nota de horas: datos de pago (bancarios) + periodo en el nombre del archivo
- Las notas "por pagar" ahora incluyen los datos bancarios de cobro configurados (Transferencia Nacional/Internacional: cuenta, CLABE, beneficiario, RFC, banco, SWIFT), igual que el PDF de cotizacion. Solo se muestran en las notas por pagar. - El nombre del PDF incluye el periodo cobrado (fecha minima a maxima de los registros), formato DD-MM-YYYY_DD-MM-YYYY. Ej: "Nota de horas por pagar UJ2606AG777 - 24-06-2026_24-06-2026". Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
feb88de203
commit
3824dd00be
@@ -271,10 +271,17 @@ export function RegistroHorasPanel({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const blob = await res.blob();
|
const blob = await res.blob();
|
||||||
|
// Periodo cobrado para el nombre: fecha minima_maxima de los registros de la nota.
|
||||||
|
const fechas = filtrados.map((r) => r.fecha.slice(0, 10)).sort();
|
||||||
|
const dd = (iso: string) => {
|
||||||
|
const [y, mo, d] = iso.split("-");
|
||||||
|
return `${d}-${mo}-${y}`;
|
||||||
|
};
|
||||||
|
const rango = fechas.length ? `${dd(fechas[0])}_${dd(fechas[fechas.length - 1])}` : "";
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const a = document.createElement("a");
|
const a = document.createElement("a");
|
||||||
a.href = url;
|
a.href = url;
|
||||||
a.download = `${TITULO_NOTA[estadoFiltro]} ${numero}.pdf`;
|
a.download = `${TITULO_NOTA[estadoFiltro]} ${numero}${rango ? ` - ${rango}` : ""}.pdf`;
|
||||||
document.body.appendChild(a);
|
document.body.appendChild(a);
|
||||||
a.click();
|
a.click();
|
||||||
a.remove();
|
a.remove();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { prisma } from "@/lib/db";
|
import { prisma } from "@/lib/db";
|
||||||
import { getConfigBranding } from "@/lib/config-helpers";
|
import { getConfigBranding, getConfigBancaria } from "@/lib/config-helpers";
|
||||||
import { generateNotaHorasPDF, type NotaHorasGrupo } from "@/lib/nota-horas-pdf";
|
import { generateNotaHorasPDF, type NotaHorasGrupo } from "@/lib/nota-horas-pdf";
|
||||||
import {
|
import {
|
||||||
IVA_RATE,
|
IVA_RATE,
|
||||||
@@ -41,12 +41,13 @@ export async function POST(
|
|||||||
const from: string = ISO_RE.test(body?.from) ? body.from : "";
|
const from: string = ISO_RE.test(body?.from) ? body.from : "";
|
||||||
const to: string = ISO_RE.test(body?.to) ? body.to : "";
|
const to: string = ISO_RE.test(body?.to) ? body.to : "";
|
||||||
|
|
||||||
const [cot, branding] = await Promise.all([
|
const [cot, branding, bancaria] = await Promise.all([
|
||||||
prisma.cotizacion.findUnique({
|
prisma.cotizacion.findUnique({
|
||||||
where: { id },
|
where: { id },
|
||||||
include: { cliente: true, registrosHoras: true },
|
include: { cliente: true, registrosHoras: true },
|
||||||
}),
|
}),
|
||||||
getConfigBranding(),
|
getConfigBranding(),
|
||||||
|
getConfigBancaria(),
|
||||||
]);
|
]);
|
||||||
if (!cot) return NextResponse.json({ error: "No encontrada" }, { status: 404 });
|
if (!cot) return NextResponse.json({ error: "No encontrada" }, { status: 404 });
|
||||||
|
|
||||||
@@ -96,6 +97,32 @@ export async function POST(
|
|||||||
: "Periodo: todos los registros";
|
: "Periodo: todos los registros";
|
||||||
const modoLabel = MODOS_AGRUPACION.find((mm) => mm.value === modo)?.label ?? "Detalle";
|
const modoLabel = MODOS_AGRUPACION.find((mm) => mm.value === modo)?.label ?? "Detalle";
|
||||||
|
|
||||||
|
// Datos bancarios para el cobro (misma extraccion que el PDF de cotizacion).
|
||||||
|
// Solo se muestran en las notas "por pagar" (las que se envian para cobro).
|
||||||
|
const cfg = bancaria || {};
|
||||||
|
const datosPago =
|
||||||
|
estado === "por_pagar"
|
||||||
|
? {
|
||||||
|
razonSocial: cfg.razon_social || "URIEL JARETH ALVARADO ORTIZ",
|
||||||
|
rfc: cfg.rfc || "AAOU970201SU7",
|
||||||
|
cuentaNacional: cfg.cuenta_nacional || "",
|
||||||
|
clabeNacional: cfg.clabe_interbancaria || cfg.cuenta_nacional || "",
|
||||||
|
bancoNacional: cfg.cuenta_nacional ? "BBVA" : "",
|
||||||
|
clabeInternacional: cfg.cuenta_internacional_swift?.match(/CLABE[^:]*:\s*(\S+)/i)?.[1] || "",
|
||||||
|
swift: cfg.cuenta_internacional_swift?.match(/SWIFT[^:]*:\s*(\S+)/i)?.[1] || "BCMRMXMMPYM",
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
// Periodo cobrado para el nombre del archivo: fecha minima_maxima (DD-MM-YYYY).
|
||||||
|
const fechasISO = regs.map((r) => r.fecha.toISOString().slice(0, 10)).sort();
|
||||||
|
const ddmmyyyy = (iso: string) => {
|
||||||
|
const [yy, mo, dd] = iso.split("-");
|
||||||
|
return `${dd}-${mo}-${yy}`;
|
||||||
|
};
|
||||||
|
const rango = fechasISO.length
|
||||||
|
? `${ddmmyyyy(fechasISO[0])}_${ddmmyyyy(fechasISO[fechasISO.length - 1])}`
|
||||||
|
: "";
|
||||||
|
|
||||||
const buffer = await generateNotaHorasPDF({
|
const buffer = await generateNotaHorasPDF({
|
||||||
numero: cot.numero,
|
numero: cot.numero,
|
||||||
proyecto: cot.proyecto,
|
proyecto: cot.proyecto,
|
||||||
@@ -123,10 +150,11 @@ export async function POST(
|
|||||||
iva,
|
iva,
|
||||||
total,
|
total,
|
||||||
etiquetaTotal: ETIQUETA_TOTAL[estado],
|
etiquetaTotal: ETIQUETA_TOTAL[estado],
|
||||||
|
datosPago,
|
||||||
...branding,
|
...branding,
|
||||||
});
|
});
|
||||||
|
|
||||||
const nombre = sanitizeFilename(`${TITULO[estado]} ${cot.numero}`);
|
const nombre = sanitizeFilename(`${TITULO[estado]} ${cot.numero}${rango ? ` - ${rango}` : ""}`);
|
||||||
return new NextResponse(new Uint8Array(buffer), {
|
return new NextResponse(new Uint8Array(buffer), {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/pdf",
|
"Content-Type": "application/pdf",
|
||||||
|
|||||||
@@ -37,6 +37,16 @@ export interface NotaHorasPDFData {
|
|||||||
iva: number;
|
iva: number;
|
||||||
total: number;
|
total: number;
|
||||||
etiquetaTotal: string; // "Total a pagar" | "Total pagado" | "Total"
|
etiquetaTotal: string; // "Total a pagar" | "Total pagado" | "Total"
|
||||||
|
// Datos bancarios para el cobro (se muestran solo en las notas "por pagar").
|
||||||
|
datosPago?: {
|
||||||
|
razonSocial: string;
|
||||||
|
rfc: string;
|
||||||
|
cuentaNacional: string;
|
||||||
|
clabeNacional: string;
|
||||||
|
bancoNacional: string;
|
||||||
|
clabeInternacional: string;
|
||||||
|
swift: string;
|
||||||
|
};
|
||||||
colorPrimario?: string;
|
colorPrimario?: string;
|
||||||
colorSecundario?: string;
|
colorSecundario?: string;
|
||||||
logoBase64?: string;
|
logoBase64?: string;
|
||||||
@@ -212,6 +222,44 @@ export async function generateNotaHorasPDF(data: NotaHorasPDFData): Promise<Buff
|
|||||||
if (data.incluirIva) line("IVA (16%)", money(data.iva));
|
if (data.incluirIva) line("IVA (16%)", money(data.iva));
|
||||||
line(data.etiquetaTotal, money(data.total), true, true);
|
line(data.etiquetaTotal, money(data.total), true, true);
|
||||||
|
|
||||||
|
// ── DATOS PARA EL PAGO (solo notas por pagar) ──
|
||||||
|
const dp = data.datosPago;
|
||||||
|
if (dp) {
|
||||||
|
const boxH = 52;
|
||||||
|
y = need(boxH + 30, y) + 16;
|
||||||
|
doc.rect(L, y, 3, 10).fill(PRIMARY);
|
||||||
|
doc.font("Helvetica-Bold").fontSize(10).fillColor(DARK).text("Datos para el pago", L + 10, y);
|
||||||
|
y += 16;
|
||||||
|
|
||||||
|
// Transferencia Nacional
|
||||||
|
doc.save();
|
||||||
|
doc.rect(L, y, W, boxH).fill(HEAD_BG).stroke(BORDER);
|
||||||
|
doc.font("Helvetica-Bold").fontSize(8).fillColor(DARK).text("Transferencia Nacional", L + 8, y + 5);
|
||||||
|
doc.font("Helvetica").fontSize(8.5).fillColor(DARK);
|
||||||
|
if (dp.cuentaNacional) doc.text(`Cuenta: ${dp.cuentaNacional}`, L + 8, y + 18, { lineBreak: false });
|
||||||
|
if (dp.clabeNacional) doc.text(`CLABE: ${dp.clabeNacional}`, L + W * 0.5, y + 18, { lineBreak: false });
|
||||||
|
doc.text(`Beneficiario: ${dp.razonSocial}`, L + 8, y + 30, { lineBreak: false });
|
||||||
|
doc.text(`RFC: ${dp.rfc}`, L + W * 0.5, y + 30, { lineBreak: false });
|
||||||
|
if (dp.bancoNacional) doc.text(`Banco: ${dp.bancoNacional}`, L + 8, y + 42, { lineBreak: false });
|
||||||
|
doc.restore();
|
||||||
|
y += boxH + 8;
|
||||||
|
|
||||||
|
// Transferencia Internacional (si hay datos)
|
||||||
|
if (dp.clabeInternacional || dp.swift) {
|
||||||
|
y = need(boxH, y);
|
||||||
|
doc.save();
|
||||||
|
doc.rect(L, y, W, boxH).fill(HEAD_BG).stroke(BORDER);
|
||||||
|
doc.font("Helvetica-Bold").fontSize(8).fillColor(DARK).text("Transferencia Internacional", L + 8, y + 5);
|
||||||
|
doc.font("Helvetica").fontSize(8.5).fillColor(DARK);
|
||||||
|
doc.text(`Beneficiario: ${dp.razonSocial}`, L + 8, y + 18, { lineBreak: false });
|
||||||
|
if (dp.clabeInternacional) doc.text(`CLABE: ${dp.clabeInternacional}`, L + W * 0.5, y + 18, { lineBreak: false });
|
||||||
|
doc.text(`Banco: BBVA Mexico`, L + 8, y + 30, { lineBreak: false });
|
||||||
|
if (dp.swift) doc.text(`SWIFT: ${dp.swift}`, L + W * 0.5, y + 30, { lineBreak: false });
|
||||||
|
doc.restore();
|
||||||
|
y += boxH + 8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── PIE ───────────────────────────────────────
|
// ── PIE ───────────────────────────────────────
|
||||||
y = need(30, y) + 14;
|
y = need(30, y) + 14;
|
||||||
doc.font("Helvetica").fontSize(8).fillColor(MUTED).text(
|
doc.font("Helvetica").fontSize(8).fillColor(MUTED).text(
|
||||||
|
|||||||
Reference in New Issue
Block a user