Propuesta IA: el documento lista TODAS las partidas de la cotizacion
Encontrado con datos reales de produccion. UJ2606UR001 tiene 58 partidas y el schema acota alcance a 40, asi que 18 servicios cotizados desaparecian del documento del cliente en silencio: aparecian en el PDF economico pero no en el consultivo, para el mismo envio. El arreglo no es subir el tope. El generador ahora recorre las partidas de la COTIZACION, no las que la IA alcanzo a describir, y usa la prosa de la IA cuando existe. Si falta, imprime el detalle del catalogo (entregables y tiempo de entrega) como respaldo. La completitud la manda la base de datos; la IA solo aporta la redaccion. Es el mismo principio que ya rige para el dinero. Verificado contra la propuesta real que genero produccion: 58 de 58 presentes, cero faltantes. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
7682fcb86b
commit
d20007d252
@@ -144,6 +144,17 @@ async function main() {
|
||||
});
|
||||
check("una partida inventada no se dibuja", !textoDePdf(conFantasma).includes("MARCADORFANTASMA"));
|
||||
|
||||
// Completitud: la cotizacion manda, no lo que la IA alcanzo a describir.
|
||||
// Verificado con UJ2606UR001 en produccion: 58 partidas, la IA describio 40.
|
||||
const econGrande = { ...econ, partidas: [...econ.partidas,
|
||||
{ refPartida: "P03", servicioCotizadoId: "c3", nombre: "MARCADORNODESCRITA servicio extra", fase: 3, tipoPago: "unico" as const, precio: 5000, tiempoEntrega: "1 semana", modeloCobro: "fijo", horas: null, tarifaHora: null, entregables: ["Entregable A", "Entregable B"] },
|
||||
] };
|
||||
const conNoDescrita = await generarPropuestaPDF({ ...datos, economia: econGrande });
|
||||
const txtND = textoDePdf(conNoDescrita);
|
||||
check("una partida SIN descripcion de la IA igual aparece", txtND.includes("MARCADORNODESCRITA"));
|
||||
check("y aparece con su precio", txtND.includes("5,000"));
|
||||
check("y con su detalle del catalogo como respaldo", txtND.includes("Entregable A"));
|
||||
|
||||
// ── Anexo interno ──
|
||||
const anexo = await generarAnexoInternoPDF(datos);
|
||||
const txtA = textoDePdf(anexo);
|
||||
|
||||
@@ -59,6 +59,16 @@ function hexSeguro(v: string | undefined, porDefecto: string): string {
|
||||
return v && /^#[0-9a-fA-F]{6}$/.test(v) ? v : porDefecto;
|
||||
}
|
||||
|
||||
/** Respaldo cuando la IA no describio una partida: se arma con los datos del
|
||||
* catalogo, para que la partida nunca desaparezca del documento. */
|
||||
function detalleDePartida(p: { tiempoEntrega: string; entregables: string[]; tipoPago: string }): string {
|
||||
const trozos: string[] = [];
|
||||
if (p.entregables.length) trozos.push(p.entregables.slice(0, 4).join(" · "));
|
||||
if (p.tiempoEntrega) trozos.push(`Entrega: ${p.tiempoEntrega}`);
|
||||
if (p.tipoPago === "mensual") trozos.push("Servicio mensual");
|
||||
return trozos.join(" | ");
|
||||
}
|
||||
|
||||
interface Lienzo {
|
||||
doc: PDFKit.PDFDocument;
|
||||
W: number;
|
||||
@@ -206,14 +216,18 @@ export async function generarPropuestaPDF(d: DatosPropuestaPDF): Promise<Buffer>
|
||||
y += 8;
|
||||
}
|
||||
|
||||
// ── ALCANCE: la IA pone la prosa, el codigo pone el dinero ──
|
||||
// ── ALCANCE ──
|
||||
// Se recorren las partidas de la COTIZACION, no las que la IA alcanzo a describir.
|
||||
// Motivo: el schema acota `alcance` y una cotizacion con mas partidas que ese tope
|
||||
// dejaria servicios cotizados fuera del documento del cliente, en silencio.
|
||||
// Verificado con UJ2606UR001: 58 partidas, la IA describio 40, faltaban 18.
|
||||
// La completitud la manda la base de datos; la IA solo aporta la prosa.
|
||||
titulo("Alcance de la inversion");
|
||||
const porRef = new Map(d.economia.partidas.map((p) => [p.refPartida, p]));
|
||||
for (const a of d.propuesta.redaccion.alcance) {
|
||||
const p = porRef.get(a.refPartida);
|
||||
if (!p) continue; // R2 ya lo marco como bloqueante; aqui simplemente no se dibuja
|
||||
const descPorRef = new Map(d.propuesta.redaccion.alcance.map((a) => [a.refPartida, a.descripcionResultado]));
|
||||
for (const p of d.economia.partidas) {
|
||||
const descripcion = descPorRef.get(p.refPartida);
|
||||
const hNombre = txtH(p.nombre, W * 0.68, 9.5);
|
||||
const hDesc = txtH(a.descripcionResultado, W * 0.68, 8);
|
||||
const hDesc = descripcion ? txtH(descripcion, W * 0.68, 8) : 0;
|
||||
y = need(hNombre + hDesc + 14, y);
|
||||
doc.font("Helvetica-Bold").fontSize(9.5).fillColor(DARK).text(p.nombre, L, y, { width: W * 0.68 });
|
||||
doc
|
||||
@@ -222,8 +236,20 @@ export async function generarPropuestaPDF(d: DatosPropuestaPDF): Promise<Buffer>
|
||||
.fillColor(PRIMARY)
|
||||
.text(formatCurrency(p.precio), L + W * 0.7, y, { width: W * 0.3, align: "right" });
|
||||
y += hNombre + 2;
|
||||
doc.font("Helvetica").fontSize(8).fillColor(MUTED).text(a.descripcionResultado, L, y, { width: W * 0.68 });
|
||||
y += hDesc + 10;
|
||||
if (descripcion) {
|
||||
doc.font("Helvetica").fontSize(8).fillColor(MUTED).text(descripcion, L, y, { width: W * 0.68 });
|
||||
y += hDesc + 10;
|
||||
} else {
|
||||
// Sin prosa de la IA: la partida SI aparece, con su detalle del catalogo.
|
||||
const respaldo = detalleDePartida(p);
|
||||
if (respaldo) {
|
||||
const hR = txtH(respaldo, W * 0.68, 8);
|
||||
doc.font("Helvetica").fontSize(8).fillColor(MUTED).text(respaldo, L, y, { width: W * 0.68 });
|
||||
y += hR + 10;
|
||||
} else {
|
||||
y += 8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
y = need(58, y);
|
||||
|
||||
Reference in New Issue
Block a user