@devlas/dte-sii 2.9.8 → 2.12.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.
@@ -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
- async _generarPdf({ html, outputPath, browser }) {
492
- const page = await browser.newPage();
493
- await page.setContent(html, { waitUntil: 'networkidle0' });
494
- await page.pdf({
495
- path: outputPath,
496
- printBackground: true,
497
- width: '215mm',
498
- height: '280mm',
499
- margin: { top: '5mm', right: '5mm', bottom: '5mm', left: '5mm' },
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 un PDF desde HTML y devuelve el contenido como Buffer (sin escribir a disco).
506
- * Útil para servir el PDF directamente desde una respuesta HTTP.
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 generarPdfBuffer(html, opts = {}) {
515
- const browser = await launchBrowser();
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 page = await browser.newPage();
518
- await page.setContent(html, { waitUntil: 'networkidle0' });
519
- const pdfBuffer = await page.pdf({
520
- printBackground: true,
521
- width: opts.width ?? '215mm',
522
- height: opts.height ?? '280mm',
523
- margin: { top: '10mm', right: '10mm', bottom: '10mm', left: '10mm' },
524
- });
525
- await page.close();
526
- return Buffer.from(pdfBuffer);
527
- } finally {
528
- await browser.close();
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
- fs.mkdirSync(outDir, { recursive: true });
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, { recursive: true });
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
- totalDocs: 0,
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
- try {
567
- for (const filePath of xmlFiles) {
568
- console.log(`\n Procesando: ${path.basename(filePath)}`);
569
- const xml = fs.readFileSync(filePath, 'utf8');
570
-
571
- let documentos;
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
- documentos = this.parseEnvioDTE(xml);
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
- console.log(` [!] Error parseando: ${e.message}`);
576
- resultado.errores.push({ file: filePath, error: e.message });
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 (!documentos.length) {
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 tedDataUri = await this.generarPdf417(doc.tedXml);
597
-
598
- // Generar ejemplar tributario (sin cedible)
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(outputPath);
608
- if (isPruebas) resultado.setPruebas++;
609
- else resultado.setSimulacion++;
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
- console.log(` [ERR] Error: ${e.message}`);
634
- resultado.errores.push({ tipo: doc.tipoDte, folio: doc.folio, error: e.message });
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