@devlas/dte-sii 2.9.8 → 2.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/EnviadorSII.js +100 -64
- package/LICENSE +27 -27
- package/LibroCompraVenta.js +141 -17
- package/LibroGuia.js +36 -25
- package/SiiCertificacion.js +85 -3
- package/SiiPortalAuth.js +85 -19
- package/WsReclamo.js +434 -434
- package/cert/BoletaCert.js +41 -4
- package/cert/CertRunner.js +1123 -1209
- package/cert/LibroCompras.js +3 -2
- package/cert/LibroGuias.js +2 -1
- package/cert/LibroVentas.js +2 -1
- package/cert/MuestrasImpresas.js +831 -131
- package/cert/comunaOficina.js +458 -458
- package/cert/index.js +122 -122
- package/cert/types.js +328 -328
- package/package.json +2 -3
- package/test-muestras.js +180 -0
- package/test-qdetestlibro.js +174 -0
- package/utils/progress.js +4 -0
- package/utils/browser.js +0 -79
package/cert/MuestrasImpresas.js
CHANGED
|
@@ -20,7 +20,6 @@ const fs = require('fs');
|
|
|
20
20
|
const path = require('path');
|
|
21
21
|
const { XMLParser, XMLBuilder } = require('fast-xml-parser');
|
|
22
22
|
const bwipjs = require('bwip-js');
|
|
23
|
-
const { launchBrowser } = require('../utils/browser');
|
|
24
23
|
|
|
25
24
|
// Usar constantes del core
|
|
26
25
|
const {
|
|
@@ -52,6 +51,89 @@ const formatMonto = (value) => {
|
|
|
52
51
|
return Number(value).toLocaleString('es-CL');
|
|
53
52
|
};
|
|
54
53
|
|
|
54
|
+
// ═══════════════════════════════════════════════════════════════
|
|
55
|
+
// Constantes de layout PDF (SII Manual Muestras Impresas v3.0)
|
|
56
|
+
// 1 cm = 28.3465 puntos tipográficos (pt)
|
|
57
|
+
// ═══════════════════════════════════════════════════════════════
|
|
58
|
+
|
|
59
|
+
const PDF_LAYOUT = {
|
|
60
|
+
page: {
|
|
61
|
+
width: 609.45, // 21.5 cm — ancho estándar hoja SII
|
|
62
|
+
minHeight: 311.81, // 11.0 cm — mínimo SII
|
|
63
|
+
maxHeight: 935.43, // 33.0 cm — máximo SII
|
|
64
|
+
},
|
|
65
|
+
margin: 14.17, // 0.5 cm mínimo SII en todos los bordes
|
|
66
|
+
|
|
67
|
+
gap: {
|
|
68
|
+
section: 10,
|
|
69
|
+
line: 13,
|
|
70
|
+
small: 6,
|
|
71
|
+
tiny: 4,
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
recuadro: {
|
|
75
|
+
width: 155.91, // 5.5 cm — mínimo SII
|
|
76
|
+
minHeight: 42.52, // 1.5 cm — mínimo SII
|
|
77
|
+
maxHeight: 113.39, // 4.0 cm — máximo SII
|
|
78
|
+
border: 1.5, // 0.5–1 mm según SII; usamos valor medio
|
|
79
|
+
padX: 8,
|
|
80
|
+
padY: 6,
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
ted: {
|
|
84
|
+
minWidth: 141.73, // 5.0 cm — mínimo SII
|
|
85
|
+
minHeight: 56.69, // 2.0 cm — mínimo SII
|
|
86
|
+
maxWidth: 255.12, // 9.0 cm — máximo SII
|
|
87
|
+
maxHeight: 113.39, // 4.0 cm — máximo SII
|
|
88
|
+
marginLeft: 56.69, // 2.0 cm desde borde izquierdo (mínimo SII)
|
|
89
|
+
legendGap: 5,
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
font: {
|
|
93
|
+
razonSocial: 11,
|
|
94
|
+
normal: 10,
|
|
95
|
+
small: 9,
|
|
96
|
+
tiny: 8,
|
|
97
|
+
legal: 7,
|
|
98
|
+
legend: 7,
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
lineH: {
|
|
102
|
+
normal: 13,
|
|
103
|
+
small: 11,
|
|
104
|
+
tiny: 10,
|
|
105
|
+
legal: 9,
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
table: {
|
|
109
|
+
rowH: 12,
|
|
110
|
+
headerH: 14,
|
|
111
|
+
padX: 3,
|
|
112
|
+
padY: 2,
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
acuse: {
|
|
116
|
+
padX: 8,
|
|
117
|
+
padY: 8,
|
|
118
|
+
fieldH: 14,
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Formatea RUT con separador de miles '.' según convención chilena SII.
|
|
124
|
+
* Ej: "12345678-9" → "12.345.678-9"
|
|
125
|
+
*/
|
|
126
|
+
function _formatRutConPuntos(rut) {
|
|
127
|
+
if (!rut) return '';
|
|
128
|
+
const str = String(rut).trim().replace(/\./g, '');
|
|
129
|
+
const idx = str.lastIndexOf('-');
|
|
130
|
+
if (idx === -1) return str;
|
|
131
|
+
const num = str.slice(0, idx);
|
|
132
|
+
const dv = str.slice(idx + 1);
|
|
133
|
+
const formatted = num.replace(/\B(?=(\d{3})+(?!\d))/g, '.');
|
|
134
|
+
return `${formatted}-${dv}`;
|
|
135
|
+
}
|
|
136
|
+
|
|
55
137
|
class MuestrasImpresas {
|
|
56
138
|
/**
|
|
57
139
|
* @param {Object} options
|
|
@@ -488,45 +570,721 @@ class MuestrasImpresas {
|
|
|
488
570
|
* Genera PDF desde HTML y lo escribe a disco.
|
|
489
571
|
* @private
|
|
490
572
|
*/
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
573
|
+
// ═══════════════════════════════════════════════════════════════
|
|
574
|
+
// Helpers internos para generarPDFBuffer (pdf-lib, sin Chromium)
|
|
575
|
+
// ═══════════════════════════════════════════════════════════════
|
|
576
|
+
|
|
577
|
+
/** Parte texto en líneas que caben dentro de maxWidth usando la fuente dada. */
|
|
578
|
+
_pdfWrapText(text, font, fontSize, maxWidth) {
|
|
579
|
+
const str = String(text || '').trim();
|
|
580
|
+
if (!str) return [''];
|
|
581
|
+
const words = str.split(' ');
|
|
582
|
+
const lines = [];
|
|
583
|
+
let line = '';
|
|
584
|
+
for (const word of words) {
|
|
585
|
+
const test = line ? `${line} ${word}` : word;
|
|
586
|
+
if (font.widthOfTextAtSize(test, fontSize) <= maxWidth) {
|
|
587
|
+
line = test;
|
|
588
|
+
} else {
|
|
589
|
+
if (line) lines.push(line);
|
|
590
|
+
// Palabra sola más larga que maxWidth: dividir por caracteres
|
|
591
|
+
if (font.widthOfTextAtSize(word, fontSize) > maxWidth) {
|
|
592
|
+
let chars = '';
|
|
593
|
+
for (const ch of word) {
|
|
594
|
+
if (font.widthOfTextAtSize(chars + ch, fontSize) <= maxWidth) {
|
|
595
|
+
chars += ch;
|
|
596
|
+
} else {
|
|
597
|
+
if (chars) lines.push(chars);
|
|
598
|
+
chars = ch;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
line = chars;
|
|
602
|
+
} else {
|
|
603
|
+
line = word;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
if (line) lines.push(line);
|
|
608
|
+
return lines.length ? lines : [''];
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/** Dibuja texto en coordenadas desde el tope de la página. */
|
|
612
|
+
_pdfText(page, text, x, yFromTop, H, font, size, color) {
|
|
613
|
+
const y = H - yFromTop - size;
|
|
614
|
+
if (y < -size || y > H + size) return; // fuera de página
|
|
615
|
+
page.drawText(String(text || ''), { x: Math.round(x), y: Math.round(y), font, size, color });
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/** Dibuja texto centrado horizontalmente dentro de [x, x+width]. */
|
|
619
|
+
_pdfTextCentered(page, text, x, width, yFromTop, H, font, size, color) {
|
|
620
|
+
const str = String(text || '');
|
|
621
|
+
const textW = font.widthOfTextAtSize(str, size);
|
|
622
|
+
this._pdfText(page, str, x + Math.max(0, (width - textW) / 2), yFromTop, H, font, size, color);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/** Dibuja un rectángulo (borde, relleno o ambos). */
|
|
626
|
+
_pdfRect(page, x, yFromTop, width, height, H, opts = {}) {
|
|
627
|
+
const drawOpts = {
|
|
628
|
+
x: Math.round(x),
|
|
629
|
+
y: Math.round(H - yFromTop - height),
|
|
630
|
+
width: Math.round(width),
|
|
631
|
+
height: Math.round(height),
|
|
632
|
+
};
|
|
633
|
+
if (opts.fill) drawOpts.color = opts.fill;
|
|
634
|
+
if (opts.stroke) { drawOpts.borderColor = opts.stroke; drawOpts.borderWidth = opts.strokeWidth || 0.5; }
|
|
635
|
+
page.drawRectangle(drawOpts);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/** Dibuja una línea horizontal. */
|
|
639
|
+
_pdfHLine(page, x1, x2, yFromTop, H, opts = {}) {
|
|
640
|
+
page.drawLine({
|
|
641
|
+
start: { x: Math.round(x1), y: Math.round(H - yFromTop) },
|
|
642
|
+
end: { x: Math.round(x2), y: Math.round(H - yFromTop) },
|
|
643
|
+
thickness: opts.thickness || 0.5,
|
|
644
|
+
color: opts.color,
|
|
500
645
|
});
|
|
501
|
-
await page.close();
|
|
502
646
|
}
|
|
503
647
|
|
|
504
648
|
/**
|
|
505
|
-
* Genera
|
|
506
|
-
*
|
|
507
|
-
*
|
|
508
|
-
* @param {string} html - HTML a convertir a PDF
|
|
509
|
-
* @param {object} [opts]
|
|
510
|
-
* @param {string} [opts.width='215mm']
|
|
511
|
-
* @param {string} [opts.height='280mm']
|
|
512
|
-
* @returns {Promise<Buffer>}
|
|
649
|
+
* Genera el PNG del TED como Buffer (no data URI) para embeber en pdf-lib.
|
|
650
|
+
* Preserva el encoding latin1/binarytext que garantiza validez de firma SII.
|
|
513
651
|
*/
|
|
514
|
-
async
|
|
515
|
-
|
|
652
|
+
async _generarTedPngBuffer(tedXml) {
|
|
653
|
+
if (!tedXml || !tedXml.includes('<TED')) {
|
|
654
|
+
throw new Error('[MuestrasImpresas] _generarTedPngBuffer: TED XML inválido');
|
|
655
|
+
}
|
|
656
|
+
const latin1Buffer = Buffer.from(tedXml, 'latin1');
|
|
657
|
+
const binaryString = latin1Buffer.toString('binary');
|
|
658
|
+
return bwipjs.toBuffer({
|
|
659
|
+
bcid: 'pdf417',
|
|
660
|
+
text: binaryString,
|
|
661
|
+
scale: 3,
|
|
662
|
+
height: 10,
|
|
663
|
+
padding: 6,
|
|
664
|
+
includetext: false,
|
|
665
|
+
binarytext: true,
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/** Retorna true si la guía de despacho es de traslado interno (sin cedible). */
|
|
670
|
+
_esGuiaInterna(doc) {
|
|
671
|
+
return doc.tipoDte === 52 && [5, 6].includes(doc.indTraslado);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/** Retorna true si el documento debe incluir acuse de recibo. */
|
|
675
|
+
_pdfNecesitaAcuse(doc, cedible) {
|
|
676
|
+
if (!cedible) return false;
|
|
677
|
+
if (NO_CEDIBLE_TIPOS.has(doc.tipoDte)) return false;
|
|
678
|
+
if (!CEDIBLE_TIPOS.has(doc.tipoDte)) return false;
|
|
679
|
+
if (this._esGuiaInterna(doc)) return false;
|
|
680
|
+
return true;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/** Carga logo como imagen pdf-lib (PNG o JPG). Retorna null si no hay logo. */
|
|
684
|
+
async _pdfLoadLogo(pdfDoc) {
|
|
685
|
+
if (!this.logoDataUri) return null;
|
|
516
686
|
try {
|
|
517
|
-
const
|
|
518
|
-
|
|
519
|
-
const
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
687
|
+
const match = this.logoDataUri.match(/^data:image\/(png|jpe?g);base64,(.+)$/i);
|
|
688
|
+
if (!match) return null;
|
|
689
|
+
const buf = Buffer.from(match[2], 'base64');
|
|
690
|
+
return /jpe?g/i.test(match[1]) ? await pdfDoc.embedJpg(buf) : await pdfDoc.embedPng(buf);
|
|
691
|
+
} catch {
|
|
692
|
+
return null;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// ── Cálculos de altura (fase 1: sin dibujar) ─────────────────
|
|
697
|
+
|
|
698
|
+
_pdfCalcRecuadroHeight(doc, fonts) {
|
|
699
|
+
const innerW = PDF_LAYOUT.recuadro.width - PDF_LAYOUT.recuadro.padX * 2;
|
|
700
|
+
const tipoLines = this._pdfWrapText(
|
|
701
|
+
NOMBRES_DTE_IMPRESOS[doc.tipoDte] || `DTE ${doc.tipoDte}`,
|
|
702
|
+
fonts.bold, PDF_LAYOUT.font.normal, innerW
|
|
703
|
+
);
|
|
704
|
+
const content =
|
|
705
|
+
PDF_LAYOUT.recuadro.padY +
|
|
706
|
+
PDF_LAYOUT.lineH.small + // RUT
|
|
707
|
+
PDF_LAYOUT.gap.tiny +
|
|
708
|
+
tipoLines.length * PDF_LAYOUT.lineH.small + // tipo DTE (puede ser multilinea)
|
|
709
|
+
PDF_LAYOUT.gap.tiny +
|
|
710
|
+
PDF_LAYOUT.lineH.small + // folio
|
|
711
|
+
PDF_LAYOUT.recuadro.padY;
|
|
712
|
+
return Math.min(PDF_LAYOUT.recuadro.maxHeight, Math.max(PDF_LAYOUT.recuadro.minHeight, content));
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
_pdfCalcHeaderHeight(doc, fonts, logoImage) {
|
|
716
|
+
let leftH = 0;
|
|
717
|
+
if (logoImage) leftH += 25 + PDF_LAYOUT.gap.small;
|
|
718
|
+
leftH += PDF_LAYOUT.lineH.normal; // razón social
|
|
719
|
+
leftH += PDF_LAYOUT.lineH.small; // giro
|
|
720
|
+
leftH += PDF_LAYOUT.lineH.small; // dirección
|
|
721
|
+
if (doc.emisor && doc.emisor.Sucursal) leftH += PDF_LAYOUT.lineH.small;
|
|
722
|
+
|
|
723
|
+
const recuadroH = this._pdfCalcRecuadroHeight(doc, fonts);
|
|
724
|
+
const rightH = recuadroH + PDF_LAYOUT.gap.small + PDF_LAYOUT.lineH.small; // recuadro + oficina SII
|
|
725
|
+
return Math.max(leftH, rightH);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
_pdfCalcReceptorHeight() {
|
|
729
|
+
return PDF_LAYOUT.acuse.padY * 2 + PDF_LAYOUT.lineH.small * 3 + PDF_LAYOUT.gap.tiny * 2;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
_pdfCalcReferencesHeight(doc) {
|
|
733
|
+
if (!doc.referencias || !doc.referencias.length) return 0;
|
|
734
|
+
return (
|
|
735
|
+
PDF_LAYOUT.lineH.normal + PDF_LAYOUT.gap.small +
|
|
736
|
+
PDF_LAYOUT.table.headerH +
|
|
737
|
+
doc.referencias.length * PDF_LAYOUT.table.rowH +
|
|
738
|
+
PDF_LAYOUT.gap.section
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
_pdfCalcDetalleHeight(doc, fonts, W, M) {
|
|
743
|
+
const tableW = W - 2 * M;
|
|
744
|
+
const descW = Math.round(tableW * 0.37) - PDF_LAYOUT.table.padX * 2;
|
|
745
|
+
let rowsH = 0;
|
|
746
|
+
for (const item of (doc.detalle || [])) {
|
|
747
|
+
const lines = this._pdfWrapText(safeText(item && item.NmbItem || ''), fonts.normal, PDF_LAYOUT.font.tiny, descW);
|
|
748
|
+
const hasDcto = !!(item && (item.DescuentoMonto || item.DescuentoPct));
|
|
749
|
+
rowsH += Math.max(
|
|
750
|
+
PDF_LAYOUT.table.rowH,
|
|
751
|
+
lines.length * PDF_LAYOUT.lineH.tiny + PDF_LAYOUT.table.padY * 2 + (hasDcto ? PDF_LAYOUT.lineH.tiny : 0)
|
|
752
|
+
);
|
|
753
|
+
}
|
|
754
|
+
return PDF_LAYOUT.lineH.normal + PDF_LAYOUT.gap.small + PDF_LAYOUT.table.headerH + rowsH;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
_pdfCalcTotalesHeight(doc) {
|
|
758
|
+
const { totales = {}, descuentosGlobales = [], tipoDte } = doc;
|
|
759
|
+
let rows = 0;
|
|
760
|
+
if (tipoDte === 34) {
|
|
761
|
+
if (totales.MntExe) rows++;
|
|
762
|
+
if (totales.MntTotal) rows++;
|
|
763
|
+
} else {
|
|
764
|
+
rows += (descuentosGlobales || []).length;
|
|
765
|
+
if (totales.MntNeto) rows++;
|
|
766
|
+
if (totales.MntExe) rows++;
|
|
767
|
+
if (totales.IVA) rows++;
|
|
768
|
+
if (totales.MntTotal) rows++;
|
|
769
|
+
}
|
|
770
|
+
return rows * (PDF_LAYOUT.table.rowH + 2) + 8;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
_pdfCalcAcuseHeight(leyendaLines) {
|
|
774
|
+
const fieldsH = PDF_LAYOUT.acuse.fieldH * 4 + PDF_LAYOUT.gap.small * 3;
|
|
775
|
+
const leyendaH = (leyendaLines || 3) * PDF_LAYOUT.lineH.legal;
|
|
776
|
+
return PDF_LAYOUT.acuse.padY * 2 + fieldsH + PDF_LAYOUT.gap.small + leyendaH + PDF_LAYOUT.gap.small;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// ── Renderizadores de sección (fase 2: dibujar) ───────────────
|
|
780
|
+
|
|
781
|
+
_pdfRenderHeader(page, doc, fonts, logoImage, y, H, W, M, rgb) {
|
|
782
|
+
const recuadroW = PDF_LAYOUT.recuadro.width;
|
|
783
|
+
const recuadroH = this._pdfCalcRecuadroHeight(doc, fonts);
|
|
784
|
+
const recuadroX = W - M - recuadroW;
|
|
785
|
+
const emisor = doc.emisor || {};
|
|
786
|
+
const BLACK = rgb(0, 0, 0);
|
|
787
|
+
const RED = rgb(0.75, 0, 0);
|
|
788
|
+
|
|
789
|
+
// ── Columna izquierda: logo + datos emisor ──────────────────
|
|
790
|
+
let lx = M;
|
|
791
|
+
let ly = y;
|
|
792
|
+
|
|
793
|
+
if (logoImage) {
|
|
794
|
+
// Resolver Promise si fue retornada como tal
|
|
795
|
+
const img = logoImage;
|
|
796
|
+
const maxW = Math.min((recuadroX - M - PDF_LAYOUT.gap.section) * 0.4, 120);
|
|
797
|
+
const maxLogoH = 25;
|
|
798
|
+
const scale = Math.min(maxW / img.width, maxLogoH / img.height);
|
|
799
|
+
const lw = img.width * scale;
|
|
800
|
+
const lh = img.height * scale;
|
|
801
|
+
page.drawImage(img, { x: lx, y: H - ly - lh, width: lw, height: lh });
|
|
802
|
+
ly += lh + PDF_LAYOUT.gap.small;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
this._pdfText(page, safeText(emisor.RznSoc || emisor.RznSocEmisor || ''), lx, ly, H, fonts.bold, PDF_LAYOUT.font.razonSocial, BLACK);
|
|
806
|
+
ly += PDF_LAYOUT.lineH.normal;
|
|
807
|
+
this._pdfText(page, safeText(emisor.GiroEmis || ''), lx, ly, H, fonts.normal, PDF_LAYOUT.font.small, BLACK);
|
|
808
|
+
ly += PDF_LAYOUT.lineH.small;
|
|
809
|
+
const dir = `Casa Matriz: ${safeText(emisor.DirOrigen || '')}${emisor.CmnaOrigen ? ', ' + safeText(emisor.CmnaOrigen) : ''}`;
|
|
810
|
+
this._pdfText(page, dir, lx, ly, H, fonts.normal, PDF_LAYOUT.font.small, BLACK);
|
|
811
|
+
ly += PDF_LAYOUT.lineH.small;
|
|
812
|
+
if (emisor.Sucursal) {
|
|
813
|
+
this._pdfText(page, `Sucursal: ${safeText(emisor.Sucursal)}`, lx, ly, H, fonts.normal, PDF_LAYOUT.font.small, BLACK);
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// ── Columna derecha: recuadro SII ───────────────────────────
|
|
817
|
+
this._pdfRect(page, recuadroX, y, recuadroW, recuadroH, H, { stroke: RED, strokeWidth: PDF_LAYOUT.recuadro.border });
|
|
818
|
+
|
|
819
|
+
const innerW = recuadroW - PDF_LAYOUT.recuadro.padX * 2;
|
|
820
|
+
let ry = y + PDF_LAYOUT.recuadro.padY;
|
|
821
|
+
|
|
822
|
+
// RUT emisor
|
|
823
|
+
const rutText = `R.U.T.: ${_formatRutConPuntos(safeText(emisor.RUTEmisor || ''))}`;
|
|
824
|
+
this._pdfTextCentered(page, rutText, recuadroX, recuadroW, ry, H, fonts.bold, PDF_LAYOUT.font.small, RED);
|
|
825
|
+
ry += PDF_LAYOUT.lineH.small + PDF_LAYOUT.gap.tiny;
|
|
826
|
+
|
|
827
|
+
// Tipo DTE (puede ocupar 2 líneas)
|
|
828
|
+
const tipoNombre = NOMBRES_DTE_IMPRESOS[doc.tipoDte] || `DTE ${doc.tipoDte}`;
|
|
829
|
+
const tipoLines = this._pdfWrapText(tipoNombre, fonts.bold, PDF_LAYOUT.font.normal, innerW);
|
|
830
|
+
for (const line of tipoLines) {
|
|
831
|
+
this._pdfTextCentered(page, line, recuadroX, recuadroW, ry, H, fonts.bold, PDF_LAYOUT.font.normal, RED);
|
|
832
|
+
ry += PDF_LAYOUT.lineH.small;
|
|
833
|
+
}
|
|
834
|
+
ry += PDF_LAYOUT.gap.tiny;
|
|
835
|
+
|
|
836
|
+
// Folio
|
|
837
|
+
this._pdfTextCentered(page, `N° ${safeText(doc.folio)}`, recuadroX, recuadroW, ry, H, fonts.bold, PDF_LAYOUT.font.small, RED);
|
|
838
|
+
|
|
839
|
+
// Oficina SII bajo el recuadro
|
|
840
|
+
this._pdfTextCentered(
|
|
841
|
+
page, this.siiOficina,
|
|
842
|
+
recuadroX, recuadroW,
|
|
843
|
+
y + recuadroH + PDF_LAYOUT.gap.small,
|
|
844
|
+
H, fonts.normal, PDF_LAYOUT.font.tiny, BLACK
|
|
845
|
+
);
|
|
846
|
+
|
|
847
|
+
const headerBottom = Math.max(ly, y + recuadroH + PDF_LAYOUT.gap.small + PDF_LAYOUT.lineH.small);
|
|
848
|
+
return headerBottom;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
_pdfRenderFecha(page, doc, fonts, y, H, M, rgb) {
|
|
852
|
+
this._pdfText(page, `Fecha Emisión: ${safeText(doc.fechaEmision)}`, M, y, H, fonts.bold, PDF_LAYOUT.font.normal, rgb(0,0,0));
|
|
853
|
+
return y + PDF_LAYOUT.lineH.normal;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
_pdfRenderReceptor(page, doc, fonts, y, H, W, M, rgb) {
|
|
857
|
+
const receptor = doc.receptor || {};
|
|
858
|
+
const boxH = this._pdfCalcReceptorHeight();
|
|
859
|
+
const boxW = W - 2 * M;
|
|
860
|
+
const BLACK = rgb(0,0,0);
|
|
861
|
+
const GRAY = rgb(0.5, 0.5, 0.5);
|
|
862
|
+
const fs = PDF_LAYOUT.font.small;
|
|
863
|
+
const lh = PDF_LAYOUT.lineH.small;
|
|
864
|
+
const px = M + PDF_LAYOUT.acuse.padX;
|
|
865
|
+
|
|
866
|
+
this._pdfRect(page, M, y, boxW, boxH, H, { stroke: GRAY, strokeWidth: 0.5 });
|
|
867
|
+
|
|
868
|
+
let py = y + PDF_LAYOUT.acuse.padY;
|
|
869
|
+
|
|
870
|
+
// Fila 1: Señor(es) + RUT
|
|
871
|
+
const sLabel = 'Señor(es): ';
|
|
872
|
+
this._pdfText(page, sLabel, px, py, H, fonts.bold, fs, BLACK);
|
|
873
|
+
this._pdfText(page, safeText(receptor.RznSocRecep || ''), px + fonts.bold.widthOfTextAtSize(sLabel, fs), py, H, fonts.normal, fs, BLACK);
|
|
874
|
+
const rutLabel = 'RUT: ';
|
|
875
|
+
const rutVal = safeText(receptor.RUTRecep || '');
|
|
876
|
+
const rutTotal = `${rutLabel}${rutVal}`;
|
|
877
|
+
const rutX = W - M - PDF_LAYOUT.acuse.padX - fonts.normal.widthOfTextAtSize(rutTotal, fs);
|
|
878
|
+
this._pdfText(page, rutLabel, rutX, py, H, fonts.bold, fs, BLACK);
|
|
879
|
+
this._pdfText(page, rutVal, rutX + fonts.bold.widthOfTextAtSize(rutLabel, fs), py, H, fonts.normal, fs, BLACK);
|
|
880
|
+
py += lh + PDF_LAYOUT.gap.tiny;
|
|
881
|
+
|
|
882
|
+
// Fila 2: Dirección + Comuna
|
|
883
|
+
const dLabel = 'Dirección: ';
|
|
884
|
+
this._pdfText(page, dLabel, px, py, H, fonts.bold, fs, BLACK);
|
|
885
|
+
this._pdfText(page, safeText(receptor.DirRecep || ''), px + fonts.bold.widthOfTextAtSize(dLabel, fs), py, H, fonts.normal, fs, BLACK);
|
|
886
|
+
const cLabel = 'Comuna: ';
|
|
887
|
+
const cVal = safeText(receptor.CmnaRecep || '');
|
|
888
|
+
const cTotal = `${cLabel}${cVal}`;
|
|
889
|
+
const cX = W - M - PDF_LAYOUT.acuse.padX - fonts.normal.widthOfTextAtSize(cTotal, fs);
|
|
890
|
+
this._pdfText(page, cLabel, cX, py, H, fonts.bold, fs, BLACK);
|
|
891
|
+
this._pdfText(page, cVal, cX + fonts.bold.widthOfTextAtSize(cLabel, fs), py, H, fonts.normal, fs, BLACK);
|
|
892
|
+
py += lh + PDF_LAYOUT.gap.tiny;
|
|
893
|
+
|
|
894
|
+
// Fila 3: Giro
|
|
895
|
+
const gLabel = 'Giro: ';
|
|
896
|
+
this._pdfText(page, gLabel, px, py, H, fonts.bold, fs, BLACK);
|
|
897
|
+
this._pdfText(page, safeText(receptor.GiroRecep || ''), px + fonts.bold.widthOfTextAtSize(gLabel, fs), py, H, fonts.normal, fs, BLACK);
|
|
898
|
+
|
|
899
|
+
return y + boxH;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
_pdfRenderTraslado(page, doc, fonts, y, H, M, rgb) {
|
|
903
|
+
const texto = `Tipo de Traslado: ${doc.indTraslado} - ${NOMBRES_TRASLADO[doc.indTraslado] || ''}`;
|
|
904
|
+
this._pdfText(page, texto, M, y, H, fonts.normal, PDF_LAYOUT.font.small, rgb(0,0,0));
|
|
905
|
+
return y + PDF_LAYOUT.lineH.small;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
_pdfRenderReferencias(page, doc, fonts, y, H, W, M, rgb) {
|
|
909
|
+
const tableW = W - 2 * M;
|
|
910
|
+
const BLACK = rgb(0,0,0);
|
|
911
|
+
const DARK = rgb(0.4, 0.4, 0.4);
|
|
912
|
+
const LGRAY = rgb(0.88, 0.88, 0.88);
|
|
913
|
+
|
|
914
|
+
this._pdfText(page, 'Referencias a otros documentos', M, y, H, fonts.bold, PDF_LAYOUT.font.normal, BLACK);
|
|
915
|
+
y += PDF_LAYOUT.lineH.normal + PDF_LAYOUT.gap.small;
|
|
916
|
+
|
|
917
|
+
const colW = {
|
|
918
|
+
tipo: Math.round(tableW * 0.30),
|
|
919
|
+
folio: Math.round(tableW * 0.15),
|
|
920
|
+
fecha: Math.round(tableW * 0.20),
|
|
921
|
+
razon: tableW - Math.round(tableW * 0.30) - Math.round(tableW * 0.15) - Math.round(tableW * 0.20),
|
|
922
|
+
};
|
|
923
|
+
const headers = ['Tipo Documento', 'Folio', 'Fecha', 'Razón Referencia'];
|
|
924
|
+
const widths = [colW.tipo, colW.folio, colW.fecha, colW.razon];
|
|
925
|
+
|
|
926
|
+
// Encabezado tabla
|
|
927
|
+
this._pdfRect(page, M, y, tableW, PDF_LAYOUT.table.headerH, H, { fill: LGRAY });
|
|
928
|
+
let cx = M;
|
|
929
|
+
for (let i = 0; i < headers.length; i++) {
|
|
930
|
+
this._pdfRect(page, cx, y, widths[i], PDF_LAYOUT.table.headerH, H, { stroke: DARK, strokeWidth: 0.5 });
|
|
931
|
+
this._pdfText(page, headers[i], cx + PDF_LAYOUT.table.padX, y + PDF_LAYOUT.table.padY, H, fonts.bold, PDF_LAYOUT.font.tiny, BLACK);
|
|
932
|
+
cx += widths[i];
|
|
529
933
|
}
|
|
934
|
+
y += PDF_LAYOUT.table.headerH;
|
|
935
|
+
|
|
936
|
+
// Filas
|
|
937
|
+
for (const ref of doc.referencias) {
|
|
938
|
+
const tipRef = ref && ref.TpoDocRef ? (NOMBRES_DTE_IMPRESOS[ref.TpoDocRef] || `Tipo ${ref.TpoDocRef}`) : '';
|
|
939
|
+
const values = [
|
|
940
|
+
safeText(tipRef),
|
|
941
|
+
safeText(ref && ref.FolioRef || ''),
|
|
942
|
+
safeText(ref && ref.FchRef || ''),
|
|
943
|
+
safeText(ref && ref.RazonRef || ''),
|
|
944
|
+
];
|
|
945
|
+
cx = M;
|
|
946
|
+
for (let i = 0; i < values.length; i++) {
|
|
947
|
+
this._pdfRect(page, cx, y, widths[i], PDF_LAYOUT.table.rowH, H, { stroke: DARK, strokeWidth: 0.5 });
|
|
948
|
+
this._pdfText(page, values[i], cx + PDF_LAYOUT.table.padX, y + PDF_LAYOUT.table.padY, H, fonts.normal, PDF_LAYOUT.font.tiny, BLACK);
|
|
949
|
+
cx += widths[i];
|
|
950
|
+
}
|
|
951
|
+
y += PDF_LAYOUT.table.rowH;
|
|
952
|
+
}
|
|
953
|
+
return y;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
_pdfRenderDetalle(page, doc, fonts, y, H, W, M, rgb) {
|
|
957
|
+
const tableW = W - 2 * M;
|
|
958
|
+
const BLACK = rgb(0,0,0);
|
|
959
|
+
const DARK = rgb(0.4, 0.4, 0.4);
|
|
960
|
+
const LGRAY = rgb(0.88, 0.88, 0.88);
|
|
961
|
+
const MGRAY = rgb(0.4, 0.4, 0.4);
|
|
962
|
+
|
|
963
|
+
this._pdfText(page, 'Detalle', M, y, H, fonts.bold, PDF_LAYOUT.font.normal, BLACK);
|
|
964
|
+
y += PDF_LAYOUT.lineH.normal + PDF_LAYOUT.gap.small;
|
|
965
|
+
|
|
966
|
+
// Definición de columnas
|
|
967
|
+
const T = tableW;
|
|
968
|
+
const colW = {
|
|
969
|
+
num: Math.round(T * 0.05),
|
|
970
|
+
cod: Math.round(T * 0.10),
|
|
971
|
+
cant: Math.round(T * 0.09),
|
|
972
|
+
unid: Math.round(T * 0.07),
|
|
973
|
+
punit: Math.round(T * 0.13),
|
|
974
|
+
valor: Math.round(T * 0.14),
|
|
975
|
+
};
|
|
976
|
+
colW.desc = T - colW.num - colW.cod - colW.cant - colW.unid - colW.punit - colW.valor;
|
|
977
|
+
|
|
978
|
+
const colDefs = [
|
|
979
|
+
{ key: 'num', w: colW.num, label: '#', align: 'center' },
|
|
980
|
+
{ key: 'cod', w: colW.cod, label: 'Código', align: 'left' },
|
|
981
|
+
{ key: 'desc', w: colW.desc, label: 'Descripción', align: 'left', wrap: true },
|
|
982
|
+
{ key: 'cant', w: colW.cant, label: 'Cant.', align: 'right' },
|
|
983
|
+
{ key: 'unid', w: colW.unid, label: 'Unid.', align: 'center' },
|
|
984
|
+
{ key: 'punit', w: colW.punit, label: 'P.Unit.', align: 'right' },
|
|
985
|
+
{ key: 'valor', w: colW.valor, label: 'Valor', align: 'right' },
|
|
986
|
+
];
|
|
987
|
+
|
|
988
|
+
// Encabezado
|
|
989
|
+
this._pdfRect(page, M, y, tableW, PDF_LAYOUT.table.headerH, H, { fill: LGRAY });
|
|
990
|
+
let cx = M;
|
|
991
|
+
for (const col of colDefs) {
|
|
992
|
+
this._pdfRect(page, cx, y, col.w, PDF_LAYOUT.table.headerH, H, { stroke: DARK, strokeWidth: 0.5 });
|
|
993
|
+
const lw = fonts.bold.widthOfTextAtSize(col.label, PDF_LAYOUT.font.tiny);
|
|
994
|
+
const lx = col.align === 'center'
|
|
995
|
+
? cx + (col.w - lw) / 2
|
|
996
|
+
: col.align === 'right'
|
|
997
|
+
? cx + col.w - lw - PDF_LAYOUT.table.padX
|
|
998
|
+
: cx + PDF_LAYOUT.table.padX;
|
|
999
|
+
this._pdfText(page, col.label, lx, y + PDF_LAYOUT.table.padY, H, fonts.bold, PDF_LAYOUT.font.tiny, BLACK);
|
|
1000
|
+
cx += col.w;
|
|
1001
|
+
}
|
|
1002
|
+
y += PDF_LAYOUT.table.headerH;
|
|
1003
|
+
|
|
1004
|
+
// Filas de detalle
|
|
1005
|
+
for (let idx = 0; idx < (doc.detalle || []).length; idx++) {
|
|
1006
|
+
const item = doc.detalle[idx] || {};
|
|
1007
|
+
const descW = colW.desc - PDF_LAYOUT.table.padX * 2;
|
|
1008
|
+
const descLines = this._pdfWrapText(safeText(item.NmbItem || ''), fonts.normal, PDF_LAYOUT.font.tiny, descW);
|
|
1009
|
+
const hasDcto = !!(item.DescuentoMonto || item.DescuentoPct);
|
|
1010
|
+
const rowH = Math.max(
|
|
1011
|
+
PDF_LAYOUT.table.rowH,
|
|
1012
|
+
descLines.length * PDF_LAYOUT.lineH.tiny + PDF_LAYOUT.table.padY * 2 + (hasDcto ? PDF_LAYOUT.lineH.tiny : 0)
|
|
1013
|
+
);
|
|
1014
|
+
|
|
1015
|
+
const vals = {
|
|
1016
|
+
num: String(idx + 1),
|
|
1017
|
+
cod: safeText((item.CdgItem && item.CdgItem.VlrCodigo) || item.CdgItem || ''),
|
|
1018
|
+
cant: item.QtyItem != null ? String(item.QtyItem) : '',
|
|
1019
|
+
unid: safeText(item.UnmdItem || 'UN'),
|
|
1020
|
+
punit: item.PrcItem != null ? `$${formatMonto(item.PrcItem)}` : '',
|
|
1021
|
+
valor: item.MontoItem != null ? `$${formatMonto(item.MontoItem)}` : '',
|
|
1022
|
+
};
|
|
1023
|
+
|
|
1024
|
+
cx = M;
|
|
1025
|
+
for (const col of colDefs) {
|
|
1026
|
+
this._pdfRect(page, cx, y, col.w, rowH, H, { stroke: DARK, strokeWidth: 0.5 });
|
|
1027
|
+
|
|
1028
|
+
if (col.key === 'desc') {
|
|
1029
|
+
let ty = y + PDF_LAYOUT.table.padY;
|
|
1030
|
+
for (const line of descLines) {
|
|
1031
|
+
this._pdfText(page, line, cx + PDF_LAYOUT.table.padX, ty, H, fonts.normal, PDF_LAYOUT.font.tiny, BLACK);
|
|
1032
|
+
ty += PDF_LAYOUT.lineH.tiny;
|
|
1033
|
+
}
|
|
1034
|
+
if (hasDcto) {
|
|
1035
|
+
const dctoStr = item.DescuentoMonto
|
|
1036
|
+
? `Dcto: $${formatMonto(item.DescuentoMonto)}`
|
|
1037
|
+
: `Dcto: ${item.DescuentoPct}%`;
|
|
1038
|
+
this._pdfText(page, dctoStr, cx + PDF_LAYOUT.table.padX, ty, H, fonts.normal, PDF_LAYOUT.font.legal, MGRAY);
|
|
1039
|
+
}
|
|
1040
|
+
} else {
|
|
1041
|
+
const val = vals[col.key] || '';
|
|
1042
|
+
const vW = fonts.normal.widthOfTextAtSize(val, PDF_LAYOUT.font.tiny);
|
|
1043
|
+
const valX = col.align === 'center'
|
|
1044
|
+
? cx + (col.w - vW) / 2
|
|
1045
|
+
: col.align === 'right'
|
|
1046
|
+
? cx + col.w - vW - PDF_LAYOUT.table.padX
|
|
1047
|
+
: cx + PDF_LAYOUT.table.padX;
|
|
1048
|
+
this._pdfText(page, val, valX, y + (rowH - PDF_LAYOUT.font.tiny) / 2, H, fonts.normal, PDF_LAYOUT.font.tiny, BLACK);
|
|
1049
|
+
}
|
|
1050
|
+
cx += col.w;
|
|
1051
|
+
}
|
|
1052
|
+
y += rowH;
|
|
1053
|
+
}
|
|
1054
|
+
return y;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
_pdfRenderTotales(page, doc, fonts, y, H, W, M, rgb) {
|
|
1058
|
+
const { totales = {}, descuentosGlobales = [], tipoDte } = doc;
|
|
1059
|
+
const esExenta = tipoDte === 34;
|
|
1060
|
+
const boxW = Math.round(W * 0.45);
|
|
1061
|
+
const boxX = W - M - boxW;
|
|
1062
|
+
const rowH = PDF_LAYOUT.table.rowH + 2;
|
|
1063
|
+
const fs = PDF_LAYOUT.font.normal;
|
|
1064
|
+
const DARK = rgb(0.4, 0.4, 0.4);
|
|
1065
|
+
const BLACK = rgb(0,0,0);
|
|
1066
|
+
|
|
1067
|
+
const rows = [];
|
|
1068
|
+
if (esExenta) {
|
|
1069
|
+
if (totales.MntExe) rows.push(['Monto Exento', `$${formatMonto(totales.MntExe)}`, false]);
|
|
1070
|
+
if (totales.MntTotal) rows.push(['Monto Total', `$${formatMonto(totales.MntTotal)}`, true]);
|
|
1071
|
+
} else {
|
|
1072
|
+
for (const dg of (descuentosGlobales || [])) {
|
|
1073
|
+
const label = dg && dg.TpoMov === 'D' ? 'Descuento Global' : 'Recargo Global';
|
|
1074
|
+
const valor = dg && dg.ValorDR ? `$${formatMonto(dg.ValorDR)}` : (dg && dg.PctDR ? `${dg.PctDR}%` : '');
|
|
1075
|
+
rows.push([label, valor, false]);
|
|
1076
|
+
}
|
|
1077
|
+
if (totales.MntNeto) rows.push(['Monto Neto', `$${formatMonto(totales.MntNeto)}`, false]);
|
|
1078
|
+
if (totales.MntExe) rows.push(['Monto Exento', `$${formatMonto(totales.MntExe)}`, false]);
|
|
1079
|
+
if (totales.IVA) rows.push([`IVA (${totales.TasaIVA || 19}%)`, `$${formatMonto(totales.IVA)}`, false]);
|
|
1080
|
+
if (totales.MntTotal) rows.push(['Monto Total', `$${formatMonto(totales.MntTotal)}`, true]);
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
let ry = y + 2;
|
|
1084
|
+
const labelW = Math.round(boxW * 0.55);
|
|
1085
|
+
const valorW = boxW - labelW;
|
|
1086
|
+
|
|
1087
|
+
for (const [label, valor, isBold] of rows) {
|
|
1088
|
+
const font = isBold ? fonts.bold : fonts.normal;
|
|
1089
|
+
this._pdfRect(page, boxX, ry, labelW, rowH, H, { stroke: DARK, strokeWidth: 0.5 });
|
|
1090
|
+
this._pdfRect(page, boxX + labelW, ry, valorW, rowH, H, { stroke: DARK, strokeWidth: 0.5 });
|
|
1091
|
+
this._pdfText(page, label, boxX + PDF_LAYOUT.table.padX, ry + PDF_LAYOUT.table.padY, H, font, fs, BLACK);
|
|
1092
|
+
const vW = font.widthOfTextAtSize(valor, fs);
|
|
1093
|
+
this._pdfText(page, valor, boxX + labelW + valorW - vW - PDF_LAYOUT.table.padX, ry + PDF_LAYOUT.table.padY, H, font, fs, BLACK);
|
|
1094
|
+
ry += rowH;
|
|
1095
|
+
}
|
|
1096
|
+
return y + rows.length * rowH + 8;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
_pdfRenderAcuse(page, doc, fonts, y, H, W, M, rgb, leyendaMaxW) {
|
|
1100
|
+
const BLACK = rgb(0,0,0);
|
|
1101
|
+
const GRAY = rgb(0.5, 0.5, 0.5);
|
|
1102
|
+
const fs = PDF_LAYOUT.font.small;
|
|
1103
|
+
const px = M + PDF_LAYOUT.acuse.padX;
|
|
1104
|
+
|
|
1105
|
+
const leyendaLines = this._pdfWrapText(DECLARACION_RECIBO, fonts.normal, PDF_LAYOUT.font.legal, leyendaMaxW);
|
|
1106
|
+
const boxH = this._pdfCalcAcuseHeight(leyendaLines.length);
|
|
1107
|
+
const boxW = W - 2 * M;
|
|
1108
|
+
|
|
1109
|
+
this._pdfRect(page, M, y, boxW, boxH, H, { stroke: BLACK, strokeWidth: 0.5 });
|
|
1110
|
+
|
|
1111
|
+
let py = y + PDF_LAYOUT.acuse.padY;
|
|
1112
|
+
|
|
1113
|
+
this._pdfText(page, 'Acuse de Recibo', px, py, H, fonts.bold, fs, BLACK);
|
|
1114
|
+
py += PDF_LAYOUT.acuse.fieldH;
|
|
1115
|
+
|
|
1116
|
+
this._pdfText(page, 'Nombre: ____________________________', px, py, H, fonts.normal, fs, BLACK);
|
|
1117
|
+
this._pdfText(page, 'R.U.T.: ___________________', px + (W - 2 * M - PDF_LAYOUT.acuse.padX * 2) * 0.5, py, H, fonts.normal, fs, BLACK);
|
|
1118
|
+
py += PDF_LAYOUT.acuse.fieldH;
|
|
1119
|
+
|
|
1120
|
+
this._pdfText(page, 'Fecha: ___________________', px, py, H, fonts.normal, fs, BLACK);
|
|
1121
|
+
this._pdfText(page, 'Recinto: __________________', px + (W - 2 * M - PDF_LAYOUT.acuse.padX * 2) * 0.5, py, H, fonts.normal, fs, BLACK);
|
|
1122
|
+
py += PDF_LAYOUT.acuse.fieldH;
|
|
1123
|
+
|
|
1124
|
+
this._pdfText(page, 'Firma: ____________________________', px, py, H, fonts.normal, fs, BLACK);
|
|
1125
|
+
py += PDF_LAYOUT.acuse.fieldH + PDF_LAYOUT.gap.small;
|
|
1126
|
+
|
|
1127
|
+
this._pdfHLine(page, px, M + boxW - PDF_LAYOUT.acuse.padX, py, H, { thickness: 0.5, color: GRAY });
|
|
1128
|
+
py += PDF_LAYOUT.gap.tiny + 2;
|
|
1129
|
+
|
|
1130
|
+
for (const line of leyendaLines) {
|
|
1131
|
+
this._pdfText(page, line, px, py, H, fonts.normal, PDF_LAYOUT.font.legal, BLACK);
|
|
1132
|
+
py += PDF_LAYOUT.lineH.legal;
|
|
1133
|
+
}
|
|
1134
|
+
return y + boxH;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
async _pdfRenderTed(pdfDoc, page, tedPng, fonts, y, H, W, M, rgb) {
|
|
1138
|
+
const BLACK = rgb(0,0,0);
|
|
1139
|
+
const pdfImage = await pdfDoc.embedPng(tedPng);
|
|
1140
|
+
const dims = pdfImage.scale(1);
|
|
1141
|
+
|
|
1142
|
+
// Escalar respetando mínimos y máximos SII
|
|
1143
|
+
const targetW = Math.max(PDF_LAYOUT.ted.minWidth, Math.min(PDF_LAYOUT.ted.maxWidth, dims.width));
|
|
1144
|
+
const scale = targetW / dims.width;
|
|
1145
|
+
const scaledW = Math.round(targetW);
|
|
1146
|
+
const scaledH = Math.max(PDF_LAYOUT.ted.minHeight, Math.min(PDF_LAYOUT.ted.maxHeight, Math.round(dims.height * scale)));
|
|
1147
|
+
|
|
1148
|
+
// Distancia mínima de 2 cm desde el borde izquierdo del documento (incluye margen)
|
|
1149
|
+
const tedX = M + PDF_LAYOUT.ted.marginLeft;
|
|
1150
|
+
|
|
1151
|
+
y += PDF_LAYOUT.gap.section;
|
|
1152
|
+
page.drawImage(pdfImage, { x: tedX, y: H - y - scaledH, width: scaledW, height: scaledH });
|
|
1153
|
+
y += scaledH + PDF_LAYOUT.ted.legendGap;
|
|
1154
|
+
|
|
1155
|
+
// Leyenda obligatoria (≥ 6pt según SII)
|
|
1156
|
+
this._pdfText(page, 'Timbre Electrónico SII', tedX, y, H, fonts.bold, PDF_LAYOUT.font.legend, BLACK);
|
|
1157
|
+
y += PDF_LAYOUT.lineH.legal;
|
|
1158
|
+
this._pdfText(page, this.resolucion, tedX, y, H, fonts.normal, PDF_LAYOUT.font.legend, BLACK);
|
|
1159
|
+
y += PDF_LAYOUT.lineH.legal;
|
|
1160
|
+
this._pdfText(page, 'Verifique documento: www.sii.cl', tedX, y, H, fonts.normal, PDF_LAYOUT.font.legend, BLACK);
|
|
1161
|
+
y += PDF_LAYOUT.lineH.legal;
|
|
1162
|
+
return y;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
_pdfRenderCedible(page, doc, fonts, H, W, M, rgb) {
|
|
1166
|
+
const texto = doc.tipoDte === 52 ? 'CEDIBLE CON SU FACTURA' : 'CEDIBLE';
|
|
1167
|
+
const tW = fonts.bold.widthOfTextAtSize(texto, PDF_LAYOUT.font.razonSocial);
|
|
1168
|
+
// Posición: inferior derecha del documento
|
|
1169
|
+
this._pdfText(page, texto, W - M - tW, H - M - PDF_LAYOUT.font.razonSocial - PDF_LAYOUT.lineH.normal, H, fonts.bold, PDF_LAYOUT.font.razonSocial, rgb(0,0,0));
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
// ═══════════════════════════════════════════════════════════════
|
|
1173
|
+
// Método público principal — sin Chromium, usa pdf-lib
|
|
1174
|
+
// ═══════════════════════════════════════════════════════════════
|
|
1175
|
+
|
|
1176
|
+
/**
|
|
1177
|
+
* Genera un Buffer PDF de la muestra impresa según Manual SII v3.0.
|
|
1178
|
+
* No requiere Chromium ni ninguna dependencia del sistema operativo.
|
|
1179
|
+
*
|
|
1180
|
+
* El parámetro `doc` debe ser un elemento del array que retorna parseEnvioDTE().
|
|
1181
|
+
*
|
|
1182
|
+
* @param {Object} doc - Documento DTE parseado
|
|
1183
|
+
* @param {Object} [opts={}]
|
|
1184
|
+
* @param {boolean} [opts.cedible=false] - Generar copia cedible con acuse de recibo
|
|
1185
|
+
* @returns {Promise<Buffer>} - Buffer PDF listo para enviar por HTTP
|
|
1186
|
+
*/
|
|
1187
|
+
async generarPDFBuffer(doc, opts = {}) {
|
|
1188
|
+
if (!doc || typeof doc !== 'object') {
|
|
1189
|
+
throw new Error('[MuestrasImpresas] generarPDFBuffer: doc es requerido (resultado de parseEnvioDTE)');
|
|
1190
|
+
}
|
|
1191
|
+
if (!doc.tipoDte) {
|
|
1192
|
+
throw new Error('[MuestrasImpresas] generarPDFBuffer: doc.tipoDte no encontrado');
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
const { cedible = false } = opts;
|
|
1196
|
+
const { PDFDocument, StandardFonts, rgb } = require('pdf-lib');
|
|
1197
|
+
|
|
1198
|
+
const pdfDoc = await PDFDocument.create();
|
|
1199
|
+
const fontNormal = await pdfDoc.embedFont(StandardFonts.Helvetica);
|
|
1200
|
+
const fontBold = await pdfDoc.embedFont(StandardFonts.HelveticaBold);
|
|
1201
|
+
const fonts = { normal: fontNormal, bold: fontBold };
|
|
1202
|
+
const logoImage = this.logoDataUri ? await this._pdfLoadLogo(pdfDoc) : null;
|
|
1203
|
+
|
|
1204
|
+
const W = PDF_LAYOUT.page.width;
|
|
1205
|
+
const M = PDF_LAYOUT.margin;
|
|
1206
|
+
|
|
1207
|
+
// ── Fase 1: precalcular alturas para dimensionar la página ────
|
|
1208
|
+
const needsAcuse = this._pdfNecesitaAcuse(doc, cedible);
|
|
1209
|
+
const leyendaW = W - 2 * M - PDF_LAYOUT.acuse.padX * 2;
|
|
1210
|
+
const leyendaLines = needsAcuse
|
|
1211
|
+
? this._pdfWrapText(DECLARACION_RECIBO, fontNormal, PDF_LAYOUT.font.legal, leyendaW)
|
|
1212
|
+
: [];
|
|
1213
|
+
|
|
1214
|
+
const headerH = this._pdfCalcHeaderHeight(doc, fonts, logoImage);
|
|
1215
|
+
const fechaH = PDF_LAYOUT.lineH.normal;
|
|
1216
|
+
const receptorH = this._pdfCalcReceptorHeight();
|
|
1217
|
+
const trasladoH = (doc.tipoDte === 52 && doc.indTraslado) ? PDF_LAYOUT.lineH.small + PDF_LAYOUT.gap.small : 0;
|
|
1218
|
+
const refsH = this._pdfCalcReferencesHeight(doc);
|
|
1219
|
+
const detalleH = this._pdfCalcDetalleHeight(doc, fonts, W, M);
|
|
1220
|
+
const totalesH = this._pdfCalcTotalesHeight(doc);
|
|
1221
|
+
const acuseH = needsAcuse ? this._pdfCalcAcuseHeight(leyendaLines.length) + PDF_LAYOUT.gap.section : 0;
|
|
1222
|
+
const tedH = doc.tedXml
|
|
1223
|
+
? PDF_LAYOUT.gap.section + PDF_LAYOUT.ted.maxHeight + PDF_LAYOUT.ted.legendGap + PDF_LAYOUT.lineH.legal * 3 + M
|
|
1224
|
+
: M;
|
|
1225
|
+
|
|
1226
|
+
const totalH =
|
|
1227
|
+
M +
|
|
1228
|
+
headerH + PDF_LAYOUT.gap.section +
|
|
1229
|
+
fechaH + PDF_LAYOUT.gap.small +
|
|
1230
|
+
receptorH + PDF_LAYOUT.gap.section +
|
|
1231
|
+
trasladoH +
|
|
1232
|
+
refsH +
|
|
1233
|
+
detalleH + PDF_LAYOUT.gap.section +
|
|
1234
|
+
totalesH + PDF_LAYOUT.gap.section +
|
|
1235
|
+
acuseH +
|
|
1236
|
+
tedH;
|
|
1237
|
+
|
|
1238
|
+
const H = Math.min(
|
|
1239
|
+
PDF_LAYOUT.page.maxHeight,
|
|
1240
|
+
Math.max(PDF_LAYOUT.page.minHeight, Math.ceil(totalH))
|
|
1241
|
+
);
|
|
1242
|
+
|
|
1243
|
+
// ── Fase 2: crear página y renderizar ─────────────────────────
|
|
1244
|
+
const page = pdfDoc.addPage([W, H]);
|
|
1245
|
+
let y = M;
|
|
1246
|
+
|
|
1247
|
+
y = this._pdfRenderHeader(page, doc, fonts, logoImage, y, H, W, M, rgb);
|
|
1248
|
+
y += PDF_LAYOUT.gap.section;
|
|
1249
|
+
|
|
1250
|
+
y = this._pdfRenderFecha(page, doc, fonts, y, H, M, rgb);
|
|
1251
|
+
y += PDF_LAYOUT.gap.small;
|
|
1252
|
+
|
|
1253
|
+
y = this._pdfRenderReceptor(page, doc, fonts, y, H, W, M, rgb);
|
|
1254
|
+
y += PDF_LAYOUT.gap.section;
|
|
1255
|
+
|
|
1256
|
+
if (trasladoH > 0) {
|
|
1257
|
+
y = this._pdfRenderTraslado(page, doc, fonts, y, H, M, rgb);
|
|
1258
|
+
y += PDF_LAYOUT.gap.small;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
if (refsH > 0) {
|
|
1262
|
+
y = this._pdfRenderReferencias(page, doc, fonts, y, H, W, M, rgb);
|
|
1263
|
+
y += PDF_LAYOUT.gap.section;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
y = this._pdfRenderDetalle(page, doc, fonts, y, H, W, M, rgb);
|
|
1267
|
+
y += PDF_LAYOUT.gap.section;
|
|
1268
|
+
|
|
1269
|
+
y = this._pdfRenderTotales(page, doc, fonts, y, H, W, M, rgb);
|
|
1270
|
+
y += PDF_LAYOUT.gap.section;
|
|
1271
|
+
|
|
1272
|
+
if (needsAcuse) {
|
|
1273
|
+
y = this._pdfRenderAcuse(page, doc, fonts, y, H, W, M, rgb, leyendaW);
|
|
1274
|
+
y += PDF_LAYOUT.gap.section;
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
if (doc.tedXml) {
|
|
1278
|
+
const tedPng = await this._generarTedPngBuffer(doc.tedXml);
|
|
1279
|
+
y = await this._pdfRenderTed(pdfDoc, page, tedPng, fonts, y, H, W, M, rgb);
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
if (cedible && CEDIBLE_TIPOS.has(doc.tipoDte) && !this._esGuiaInterna(doc)) {
|
|
1283
|
+
this._pdfRenderCedible(page, doc, fonts, H, W, M, rgb);
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
const pdfBytes = await pdfDoc.save();
|
|
1287
|
+
return Buffer.from(pdfBytes);
|
|
530
1288
|
}
|
|
531
1289
|
|
|
532
1290
|
/**
|
|
@@ -538,122 +1296,64 @@ class MuestrasImpresas {
|
|
|
538
1296
|
* @returns {Promise<Object>} Resultado con estadísticas y archivos generados
|
|
539
1297
|
*/
|
|
540
1298
|
async generarMuestras({ xmlFiles, outDir, generarCedible = false }) {
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
// Crear subdirectorios según requisitos del SII
|
|
544
|
-
const pruebasDir = path.join(outDir, 'SET-PRUEBAS');
|
|
1299
|
+
const pruebasDir = path.join(outDir, 'SET-PRUEBAS');
|
|
545
1300
|
const simulacionDir = path.join(outDir, 'SET-SIMULACION');
|
|
546
|
-
fs.mkdirSync(pruebasDir,
|
|
1301
|
+
fs.mkdirSync(pruebasDir, { recursive: true });
|
|
547
1302
|
fs.mkdirSync(simulacionDir, { recursive: true });
|
|
548
1303
|
|
|
549
|
-
console.log('\n' + '═'.repeat(60));
|
|
550
|
-
console.log('GENERACIÓN DE MUESTRAS IMPRESAS');
|
|
551
|
-
console.log('═'.repeat(60));
|
|
552
|
-
console.log(` SET-PRUEBAS: ${pruebasDir}`);
|
|
553
|
-
console.log(` SET-SIMULACION: ${simulacionDir}`);
|
|
554
|
-
|
|
555
|
-
const browser = await launchBrowser();
|
|
556
1304
|
const resultado = {
|
|
557
|
-
success: true,
|
|
558
|
-
|
|
559
|
-
totalPdfs: 0,
|
|
560
|
-
archivos: [],
|
|
561
|
-
errores: [],
|
|
562
|
-
setPruebas: 0,
|
|
563
|
-
setSimulacion: 0,
|
|
1305
|
+
success: true, totalDocs: 0, totalPdfs: 0,
|
|
1306
|
+
archivos: [], errores: [], setPruebas: 0, setSimulacion: 0,
|
|
564
1307
|
};
|
|
565
1308
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
1309
|
+
for (const filePath of xmlFiles) {
|
|
1310
|
+
const sourceFile = path.basename(filePath).toLowerCase();
|
|
1311
|
+
const isPruebas = /envio-set-(basico|guia|exenta|compra)\.xml/i.test(sourceFile);
|
|
1312
|
+
const targetDir = isPruebas ? pruebasDir : simulacionDir;
|
|
1313
|
+
|
|
1314
|
+
let docs;
|
|
1315
|
+
try {
|
|
1316
|
+
docs = this.parseEnvioDTE(fs.readFileSync(filePath, 'utf8'));
|
|
1317
|
+
} catch (e) {
|
|
1318
|
+
resultado.errores.push({ file: filePath, error: e.message });
|
|
1319
|
+
continue;
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
for (const doc of docs) {
|
|
1323
|
+
resultado.totalDocs++;
|
|
1324
|
+
const base = `muestra_${doc.tipoDte}_${doc.folio}`;
|
|
1325
|
+
|
|
572
1326
|
try {
|
|
573
|
-
|
|
1327
|
+
const buf = await this.generarPDFBuffer(doc, { cedible: false });
|
|
1328
|
+
const out = path.join(targetDir, `${base}.pdf`);
|
|
1329
|
+
fs.writeFileSync(out, buf);
|
|
1330
|
+
resultado.totalPdfs++;
|
|
1331
|
+
resultado.archivos.push(out);
|
|
1332
|
+
if (isPruebas) resultado.setPruebas++; else resultado.setSimulacion++;
|
|
1333
|
+
console.log(` ✓ ${base}.pdf (${buf.length} bytes)`);
|
|
574
1334
|
} catch (e) {
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
continue;
|
|
1335
|
+
resultado.errores.push({ tipo: doc.tipoDte, folio: doc.folio, error: e.message });
|
|
1336
|
+
console.error(` ✗ ${base}.pdf → ${e.message}`);
|
|
578
1337
|
}
|
|
579
1338
|
|
|
580
|
-
if (!
|
|
581
|
-
console.log(' [!] Sin documentos');
|
|
582
|
-
continue;
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
// Determinar directorio de salida según archivo fuente
|
|
586
|
-
const sourceFile = path.basename(filePath).toLowerCase();
|
|
587
|
-
const isPruebas = /envio-set-(basico|guia|exenta|compra)\.xml/i.test(sourceFile);
|
|
588
|
-
const targetDir = isPruebas ? pruebasDir : simulacionDir;
|
|
589
|
-
const categoria = isPruebas ? 'PRUEBAS' : 'SIMULACION';
|
|
590
|
-
console.log(` Categoria: SET-${categoria}`);
|
|
591
|
-
|
|
592
|
-
for (const doc of documentos) {
|
|
593
|
-
resultado.totalDocs++;
|
|
594
|
-
|
|
1339
|
+
if (generarCedible && CEDIBLE_TIPOS.has(doc.tipoDte) && !this._esGuiaInterna(doc)) {
|
|
595
1340
|
try {
|
|
596
|
-
const
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
const html = this._buildHtml({ doc, esCedible: false, tedDataUri });
|
|
600
|
-
|
|
601
|
-
// PDFs organizados en subdirectorios según categoría SII
|
|
602
|
-
const outputName = `muestra_${doc.tipoDte}_${doc.folio}.pdf`;
|
|
603
|
-
const outputPath = path.join(targetDir, outputName);
|
|
604
|
-
|
|
605
|
-
await this._generarPdf({ html, outputPath, browser });
|
|
1341
|
+
const buf = await this.generarPDFBuffer(doc, { cedible: true });
|
|
1342
|
+
const out = path.join(targetDir, `${base}_cedible.pdf`);
|
|
1343
|
+
fs.writeFileSync(out, buf);
|
|
606
1344
|
resultado.totalPdfs++;
|
|
607
|
-
resultado.archivos.push(
|
|
608
|
-
if (isPruebas) resultado.setPruebas++;
|
|
609
|
-
|
|
610
|
-
console.log(` ✓ ${outputName}`);
|
|
611
|
-
|
|
612
|
-
// Generar copia cedible si corresponde
|
|
613
|
-
if (generarCedible && CEDIBLE_TIPOS.has(doc.tipoDte)) {
|
|
614
|
-
// Guía de traslado interno no tiene cedible
|
|
615
|
-
if (doc.tipoDte === 52 && [5, 6].includes(doc.indTraslado)) {
|
|
616
|
-
console.log(' Guia traslado interno - sin cedible');
|
|
617
|
-
continue;
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
const htmlCedible = this._buildHtml({ doc, esCedible: true, tedDataUri });
|
|
621
|
-
const outputNameCedible = `muestra_${doc.tipoDte}_${doc.folio}_cedible.pdf`;
|
|
622
|
-
const outputPathCedible = path.join(targetDir, outputNameCedible);
|
|
623
|
-
|
|
624
|
-
await this._generarPdf({ html: htmlCedible, outputPath: outputPathCedible, browser });
|
|
625
|
-
resultado.totalPdfs++;
|
|
626
|
-
resultado.archivos.push(outputPathCedible);
|
|
627
|
-
if (isPruebas) resultado.setPruebas++;
|
|
628
|
-
else resultado.setSimulacion++;
|
|
629
|
-
console.log(` ✓ ${outputNameCedible}`);
|
|
630
|
-
}
|
|
631
|
-
|
|
1345
|
+
resultado.archivos.push(out);
|
|
1346
|
+
if (isPruebas) resultado.setPruebas++; else resultado.setSimulacion++;
|
|
1347
|
+
console.log(` ✓ ${base}_cedible.pdf (${buf.length} bytes)`);
|
|
632
1348
|
} catch (e) {
|
|
633
|
-
|
|
634
|
-
|
|
1349
|
+
resultado.errores.push({ tipo: doc.tipoDte, folio: doc.folio, cedible: true, error: e.message });
|
|
1350
|
+
console.error(` ✗ ${base}_cedible.pdf → ${e.message}`);
|
|
635
1351
|
}
|
|
636
1352
|
}
|
|
637
1353
|
}
|
|
638
|
-
} finally {
|
|
639
|
-
await browser.close();
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
// Resumen
|
|
643
|
-
console.log('\n' + '═'.repeat(60));
|
|
644
|
-
console.log('[OK] MUESTRAS IMPRESAS GENERADAS');
|
|
645
|
-
console.log('═'.repeat(60));
|
|
646
|
-
console.log(` Documentos procesados: ${resultado.totalDocs}`);
|
|
647
|
-
console.log(` PDFs generados: ${resultado.totalPdfs}`);
|
|
648
|
-
console.log(` SET-PRUEBAS: ${resultado.setPruebas} PDFs`);
|
|
649
|
-
console.log(` SET-SIMULACION: ${resultado.setSimulacion} PDFs`);
|
|
650
|
-
console.log(` Directorio base: ${outDir}`);
|
|
651
|
-
|
|
652
|
-
if (resultado.errores.length > 0) {
|
|
653
|
-
resultado.success = false;
|
|
654
|
-
console.log(` [!] Errores: ${resultado.errores.length}`);
|
|
655
1354
|
}
|
|
656
1355
|
|
|
1356
|
+
if (resultado.errores.length > 0) resultado.success = false;
|
|
657
1357
|
return resultado;
|
|
658
1358
|
}
|
|
659
1359
|
|