@devlas/dte-sii 2.5.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/BoletaService.js +109 -0
- package/CAF.js +173 -0
- package/CafSolicitor.js +380 -0
- package/Certificado.js +123 -0
- package/ConsumoFolio.js +376 -0
- package/DTE.js +399 -0
- package/EnviadorSII.js +1304 -0
- package/Envio.js +196 -0
- package/FolioRegistry.js +553 -0
- package/FolioService.js +703 -0
- package/LICENSE +27 -0
- package/LibroBase.js +134 -0
- package/LibroCompraVenta.js +205 -0
- package/LibroGuia.js +225 -0
- package/README.md +239 -0
- package/Signer.js +94 -0
- package/SiiCertificacion.js +1189 -0
- package/SiiPortalAuth.js +460 -0
- package/SiiSession.js +499 -0
- package/cert/BoletaCert.js +731 -0
- package/cert/CertFolioHelper.js +185 -0
- package/cert/CertRunner.js +2658 -0
- package/cert/ConfigLoader.js +133 -0
- package/cert/IntercambioCert.js +429 -0
- package/cert/LibroCompras.js +359 -0
- package/cert/LibroGuias.js +171 -0
- package/cert/LibroVentas.js +153 -0
- package/cert/MuestrasImpresas.js +676 -0
- package/cert/SetBase.js +321 -0
- package/cert/SetBasico.js +413 -0
- package/cert/SetCompra.js +472 -0
- package/cert/SetExenta.js +490 -0
- package/cert/SetGuia.js +283 -0
- package/cert/SetParser.js +1184 -0
- package/cert/SetsProvider.js +499 -0
- package/cert/Simulacion.js +521 -0
- package/cert/comunaOficina.js +460 -0
- package/cert/index.js +124 -0
- package/cert/types.js +330 -0
- package/dte-sii.d.ts +458 -0
- package/index.js +428 -0
- package/package.json +48 -0
- package/utils/c14n.js +275 -0
- package/utils/calculo.js +396 -0
- package/utils/config.js +276 -0
- package/utils/constants.js +302 -0
- package/utils/emisor.js +174 -0
- package/utils/endpoints.js +225 -0
- package/utils/error.js +235 -0
- package/utils/index.js +339 -0
- package/utils/logger.js +239 -0
- package/utils/pfx.js +203 -0
- package/utils/receptor.js +218 -0
- package/utils/referencia.js +169 -0
- package/utils/resolucion.js +119 -0
- package/utils/rut.js +169 -0
- package/utils/sanitize.js +124 -0
- package/utils/tokenCache.js +214 -0
- package/utils/xml.js +358 -0
- package/utils.js +4 -0
|
@@ -0,0 +1,676 @@
|
|
|
1
|
+
// Copyright (c) 2026 Devlas SpA — https://devlas.cl
|
|
2
|
+
// Licencia MIT. Ver archivo LICENSE para mas detalles.
|
|
3
|
+
/**
|
|
4
|
+
* MuestrasImpresas.js
|
|
5
|
+
*
|
|
6
|
+
* Módulo para generar PDFs de muestras impresas según Manual SII v4.0
|
|
7
|
+
* para la etapa "Documentos Impresos" del proceso de certificación.
|
|
8
|
+
*
|
|
9
|
+
* Especificaciones:
|
|
10
|
+
* - Dimensiones: 21.5x11 cm (mín) a 21.5x33 cm (máx)
|
|
11
|
+
* - Borde sin letras: 0.5 cm mínimo
|
|
12
|
+
* - Recuadro tipo DTE: 1.5x5.5 cm (mín) a 4x8 cm (máx), negro/rojo
|
|
13
|
+
* - Timbre PDF417: 2x5 cm (mín) a 4x9 cm (máx)
|
|
14
|
+
* - Timbre a 2 cm mínimo del borde izquierdo
|
|
15
|
+
*
|
|
16
|
+
* @module dte-sii/cert/MuestrasImpresas
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
const { XMLParser, XMLBuilder } = require('fast-xml-parser');
|
|
22
|
+
const bwipjs = require('bwip-js');
|
|
23
|
+
const puppeteer = require('puppeteer');
|
|
24
|
+
|
|
25
|
+
// Usar constantes del core
|
|
26
|
+
const {
|
|
27
|
+
NOMBRES_DTE_IMPRESOS,
|
|
28
|
+
NOMBRES_TRASLADO,
|
|
29
|
+
TIPOS_CEDIBLES,
|
|
30
|
+
TIPOS_NO_CEDIBLES,
|
|
31
|
+
DECLARACION_RECIBO,
|
|
32
|
+
} = require('../utils/constants');
|
|
33
|
+
const { normalizeArray } = require('../index');
|
|
34
|
+
|
|
35
|
+
// Sets para lookup rápido
|
|
36
|
+
const CEDIBLE_TIPOS = new Set(TIPOS_CEDIBLES);
|
|
37
|
+
const NO_CEDIBLE_TIPOS = new Set(TIPOS_NO_CEDIBLES);
|
|
38
|
+
|
|
39
|
+
// ═══════════════════════════════════════════════════════════════
|
|
40
|
+
// Helpers
|
|
41
|
+
// ═══════════════════════════════════════════════════════════════
|
|
42
|
+
|
|
43
|
+
const toArray = (value) => (Array.isArray(value) ? value : value ? [value] : []);
|
|
44
|
+
|
|
45
|
+
const safeText = (value) => {
|
|
46
|
+
if (value === undefined || value === null) return '';
|
|
47
|
+
return String(value).trim();
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const formatMonto = (value) => {
|
|
51
|
+
if (!value) return '0';
|
|
52
|
+
return Number(value).toLocaleString('es-CL');
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
class MuestrasImpresas {
|
|
56
|
+
/**
|
|
57
|
+
* @param {Object} options
|
|
58
|
+
* @param {Object} options.emisor - Datos del emisor { rut, razonSocial, giro, direccion, comuna }
|
|
59
|
+
* @param {string} [options.siiOficina] - Oficina SII (ej: "S.I.I. - SANTIAGO CENTRO")
|
|
60
|
+
* @param {string} [options.resolucion] - Texto resolución (ej: "Res. Ex. SII N° 0 del 2026")
|
|
61
|
+
* @param {string} [options.logoPath] - Ruta al logo PNG (opcional)
|
|
62
|
+
* @param {string} [options.debugDir] - Directorio para guardar PDFs
|
|
63
|
+
*/
|
|
64
|
+
constructor({ emisor, siiOficina, resolucion, logoPath, debugDir }) {
|
|
65
|
+
this.emisor = emisor;
|
|
66
|
+
this.siiOficina = siiOficina || 'S.I.I. - SANTIAGO CENTRO';
|
|
67
|
+
this.resolucion = resolucion || `Res. Ex. SII N° 0 del ${new Date().getFullYear()}`;
|
|
68
|
+
this.logoDataUri = this._loadLogo(logoPath);
|
|
69
|
+
this.debugDir = debugDir;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Carga logo como data URI
|
|
74
|
+
* @private
|
|
75
|
+
*/
|
|
76
|
+
_loadLogo(logoPath) {
|
|
77
|
+
if (!logoPath) return '';
|
|
78
|
+
const resolved = path.resolve(logoPath);
|
|
79
|
+
if (!fs.existsSync(resolved)) return '';
|
|
80
|
+
const ext = path.extname(resolved).toLowerCase().replace('.', '') || 'png';
|
|
81
|
+
const data = fs.readFileSync(resolved);
|
|
82
|
+
return `data:image/${ext};base64,${data.toString('base64')}`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Parsea un EnvioDTE XML y extrae los documentos
|
|
87
|
+
* @param {string} xml - XML del EnvioDTE (en UTF-8)
|
|
88
|
+
* @returns {Object[]} Array de documentos parseados
|
|
89
|
+
*/
|
|
90
|
+
parseEnvioDTE(xml) {
|
|
91
|
+
const parser = new XMLParser({
|
|
92
|
+
ignoreAttributes: false,
|
|
93
|
+
attributeNamePrefix: '@_',
|
|
94
|
+
trimValues: true,
|
|
95
|
+
parseTagValue: true,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const data = parser.parse(xml);
|
|
99
|
+
const envio = data?.EnvioDTE || data?.EnvioDTEB || data?.EnvioDTETraslado || data?.EnvioBOLETA || {};
|
|
100
|
+
const setDte = envio?.SetDTE || data?.SetDTE || {};
|
|
101
|
+
const dtes = normalizeArray(setDte?.DTE);
|
|
102
|
+
|
|
103
|
+
const builder = new XMLBuilder({
|
|
104
|
+
ignoreAttributes: false,
|
|
105
|
+
attributeNamePrefix: '@_',
|
|
106
|
+
format: false,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Extraer TEDs directamente del XML original
|
|
110
|
+
// El TED debe extraerse exactamente como está en el XML para preservar la firma
|
|
111
|
+
const extractTedsFromOriginal = (originalXml) => {
|
|
112
|
+
const teds = [];
|
|
113
|
+
const tedRegex = /<TED[^>]*>[\s\S]*?<\/TED>/g;
|
|
114
|
+
let match;
|
|
115
|
+
while ((match = tedRegex.exec(originalXml)) !== null) {
|
|
116
|
+
teds.push(match[0]);
|
|
117
|
+
}
|
|
118
|
+
return teds;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// Extraer TEDs del XML como UTF-8 (que es el encoding real del archivo)
|
|
122
|
+
const tedsOriginales = extractTedsFromOriginal(xml);
|
|
123
|
+
|
|
124
|
+
const extractTedValue = (ted, tag) => {
|
|
125
|
+
if (!ted) return '';
|
|
126
|
+
const regex = new RegExp(`<${tag}>([^<]*)</${tag}>`);
|
|
127
|
+
const match = ted.match(regex);
|
|
128
|
+
return match ? match[1] : '';
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
return dtes.map((dte, idx) => {
|
|
132
|
+
const doc = dte?.Documento || dte?.DTE?.Documento || {};
|
|
133
|
+
const encabezado = doc?.Encabezado || {};
|
|
134
|
+
const idDoc = encabezado?.IdDoc || {};
|
|
135
|
+
const emisor = encabezado?.Emisor || {};
|
|
136
|
+
const receptor = encabezado?.Receptor || {};
|
|
137
|
+
const totales = encabezado?.Totales || {};
|
|
138
|
+
const transporte = encabezado?.Transporte || {};
|
|
139
|
+
const detalle = toArray(doc?.Detalle);
|
|
140
|
+
const referencias = toArray(doc?.Referencia);
|
|
141
|
+
const descuentosGlobales = toArray(doc?.DscRcgGlobal);
|
|
142
|
+
|
|
143
|
+
// Obtener TED original del XML (preserva encoding ISO-8859-1 para firma válida)
|
|
144
|
+
// Buscar el TED que corresponda al folio de este documento
|
|
145
|
+
const docFolio = idDoc?.Folio?.toString?.() || '';
|
|
146
|
+
const docTipo = idDoc?.TipoDTE?.toString?.() || '';
|
|
147
|
+
|
|
148
|
+
let tedXml = '';
|
|
149
|
+
// Buscar TED original que coincida con este documento
|
|
150
|
+
for (const ted of tedsOriginales) {
|
|
151
|
+
const tedFolio = extractTedValue(ted, 'F');
|
|
152
|
+
const tedTipo = extractTedValue(ted, 'TD');
|
|
153
|
+
if (tedFolio === docFolio && tedTipo === docTipo) {
|
|
154
|
+
tedXml = ted;
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Fallback: si no encontramos, usar siguiente TED disponible en orden
|
|
160
|
+
if (!tedXml && tedsOriginales.length > idx) {
|
|
161
|
+
tedXml = tedsOriginales[idx];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const tipoDteTed = Number(extractTedValue(tedXml, 'TD') || 0);
|
|
165
|
+
const folioTed = extractTedValue(tedXml, 'F');
|
|
166
|
+
const fechaTed = extractTedValue(tedXml, 'FE');
|
|
167
|
+
|
|
168
|
+
const tipoDte = Number(idDoc?.TipoDTE || tipoDteTed || 0);
|
|
169
|
+
const folio = (idDoc?.Folio?.toString?.() ?? '') || folioTed || '';
|
|
170
|
+
const fechaEmision = (idDoc?.FchEmis?.toString?.() ?? '') || fechaTed || '';
|
|
171
|
+
const indTraslado = idDoc?.IndTraslado ? Number(idDoc.IndTraslado) : null;
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
tipoDte,
|
|
175
|
+
folio,
|
|
176
|
+
fechaEmision,
|
|
177
|
+
indTraslado,
|
|
178
|
+
emisor,
|
|
179
|
+
receptor,
|
|
180
|
+
totales,
|
|
181
|
+
transporte,
|
|
182
|
+
detalle,
|
|
183
|
+
referencias,
|
|
184
|
+
descuentosGlobales,
|
|
185
|
+
tedXml,
|
|
186
|
+
};
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Genera código de barras PDF417 del TED
|
|
192
|
+
* El PDF417 debe contener exactamente los mismos bytes que se usaron para firmar el TED.
|
|
193
|
+
*
|
|
194
|
+
* La firma en CAF.sign() usa forge con 'latin1', lo que toma cada codepoint Unicode
|
|
195
|
+
* del string JavaScript y lo usa directamente como byte. Por ejemplo:
|
|
196
|
+
* - 'ñ' (U+00F1 = 241) → byte 0xF1
|
|
197
|
+
* - 'ó' (U+00F3 = 243) → byte 0xF3
|
|
198
|
+
*
|
|
199
|
+
* El SII espera que el PDF417 contenga estos mismos bytes (ISO-8859-1).
|
|
200
|
+
* Si pasamos el string directamente a bwip-js, internamente lo convierte a UTF-8:
|
|
201
|
+
* - 'ñ' → bytes 0xC3 0xB1 (UTF-8)
|
|
202
|
+
* Esto causa que el SII vea "ñ" en lugar de "ñ" → firma inválida.
|
|
203
|
+
*
|
|
204
|
+
* Solución: Convertir el string a Buffer con encoding 'latin1' para que
|
|
205
|
+
* cada codepoint Unicode se mapee directamente a un byte (codepoint → byte),
|
|
206
|
+
* luego convertir de vuelta a string binary para que bwip-js lo acepte.
|
|
207
|
+
*
|
|
208
|
+
* @param {string} tedXml - XML del TED
|
|
209
|
+
* @returns {Promise<string>} Data URI de la imagen PNG
|
|
210
|
+
*/
|
|
211
|
+
async generarPdf417(tedXml) {
|
|
212
|
+
if (!tedXml || !tedXml.includes('<TED')) {
|
|
213
|
+
throw new Error('No se encontró TED para generar PDF417');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Convertir string a Buffer con encoding 'latin1' y luego a string 'binary'
|
|
217
|
+
// Esto asegura que cada codepoint Unicode se convierta a exactamente un byte
|
|
218
|
+
// y que bwip-js reciba un string con esos bytes
|
|
219
|
+
const latin1Buffer = Buffer.from(tedXml, 'latin1');
|
|
220
|
+
const binaryString = latin1Buffer.toString('binary');
|
|
221
|
+
|
|
222
|
+
const buffer = await bwipjs.toBuffer({
|
|
223
|
+
bcid: 'pdf417',
|
|
224
|
+
text: binaryString,
|
|
225
|
+
scale: 3,
|
|
226
|
+
height: 12,
|
|
227
|
+
padding: 6,
|
|
228
|
+
includetext: false,
|
|
229
|
+
// CRÍTICO: Indicar a bwip-js que el texto ya está en formato binario 8-bit
|
|
230
|
+
// Sin esto, bwip-js convierte el string a UTF-8 internamente, lo que
|
|
231
|
+
// causa que ñ (0xF1) se convierta a 0xC3 0xB1 y la firma falle
|
|
232
|
+
binarytext: true,
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
return `data:image/png;base64,${buffer.toString('base64')}`;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Construye HTML de la muestra impresa según Manual SII
|
|
240
|
+
* @private
|
|
241
|
+
*/
|
|
242
|
+
_buildHtml({ doc, esCedible, tedDataUri }) {
|
|
243
|
+
const tipoNombre = NOMBRES_DTE_IMPRESOS[doc.tipoDte] || `DTE ${doc.tipoDte}`;
|
|
244
|
+
const puedeSerCedible = CEDIBLE_TIPOS.has(doc.tipoDte);
|
|
245
|
+
const esNota = NO_CEDIBLE_TIPOS.has(doc.tipoDte);
|
|
246
|
+
|
|
247
|
+
// Guía con traslado interno no tiene cedible
|
|
248
|
+
const esGuiaInterna = doc.tipoDte === 52 && [5, 6].includes(doc.indTraslado);
|
|
249
|
+
const mostrarCedible = esCedible && puedeSerCedible && !esGuiaInterna;
|
|
250
|
+
const cedibleTexto = doc.tipoDte === 52 ? 'CEDIBLE CON SU FACTURA' : 'CEDIBLE';
|
|
251
|
+
|
|
252
|
+
const emisor = doc.emisor || {};
|
|
253
|
+
const receptor = doc.receptor || {};
|
|
254
|
+
const totales = doc.totales || {};
|
|
255
|
+
|
|
256
|
+
// Detalle con descuentos
|
|
257
|
+
const detallesHtml = doc.detalle.map((item, idx) => {
|
|
258
|
+
const descuento = item?.DescuentoMonto || item?.DescuentoPct ?
|
|
259
|
+
`<br><small>Dcto: ${item.DescuentoMonto ? '$' + formatMonto(item.DescuentoMonto) : item.DescuentoPct + '%'}</small>` : '';
|
|
260
|
+
return `
|
|
261
|
+
<tr>
|
|
262
|
+
<td>${idx + 1}</td>
|
|
263
|
+
<td>${safeText(item?.CdgItem?.VlrCodigo || item?.CdgItem || '')}</td>
|
|
264
|
+
<td>${safeText(item?.NmbItem || '')}${descuento}</td>
|
|
265
|
+
<td class="num">${item?.QtyItem ?? ''}</td>
|
|
266
|
+
<td>${safeText(item?.UnmdItem || 'UN')}</td>
|
|
267
|
+
<td class="num">${item?.PrcItem ? '$' + formatMonto(item.PrcItem) : ''}</td>
|
|
268
|
+
<td class="num">${item?.MontoItem ? '$' + formatMonto(item.MontoItem) : ''}</td>
|
|
269
|
+
</tr>
|
|
270
|
+
`;
|
|
271
|
+
}).join('');
|
|
272
|
+
|
|
273
|
+
// Referencias
|
|
274
|
+
const refsHtml = doc.referencias.length
|
|
275
|
+
? `
|
|
276
|
+
<div class="seccion">
|
|
277
|
+
<div class="seccion-titulo">Referencias a otros documentos</div>
|
|
278
|
+
<table class="refs">
|
|
279
|
+
<thead>
|
|
280
|
+
<tr><th>Tipo Documento</th><th>Folio</th><th>Fecha</th><th>Razón Referencia</th></tr>
|
|
281
|
+
</thead>
|
|
282
|
+
<tbody>
|
|
283
|
+
${doc.referencias.map((ref) => {
|
|
284
|
+
const tipoRef = ref?.TpoDocRef ? (NOMBRES_DTE_IMPRESOS[ref.TpoDocRef] || `Tipo ${ref.TpoDocRef}`) : '';
|
|
285
|
+
return `
|
|
286
|
+
<tr>
|
|
287
|
+
<td>${safeText(tipoRef)}</td>
|
|
288
|
+
<td>${safeText(ref?.FolioRef || '')}</td>
|
|
289
|
+
<td>${safeText(ref?.FchRef || '')}</td>
|
|
290
|
+
<td>${safeText(ref?.RazonRef || '')}</td>
|
|
291
|
+
</tr>
|
|
292
|
+
`;
|
|
293
|
+
}).join('')}
|
|
294
|
+
</tbody>
|
|
295
|
+
</table>
|
|
296
|
+
</div>
|
|
297
|
+
`
|
|
298
|
+
: '';
|
|
299
|
+
|
|
300
|
+
// Tipo de traslado para guías
|
|
301
|
+
const trasladoHtml = doc.tipoDte === 52 && doc.indTraslado
|
|
302
|
+
? `<div class="traslado"><strong>Tipo de Traslado:</strong> ${doc.indTraslado} - ${NOMBRES_TRASLADO[doc.indTraslado] || ''}</div>`
|
|
303
|
+
: '';
|
|
304
|
+
|
|
305
|
+
// Descuentos globales
|
|
306
|
+
const dctoGlobalHtml = doc.descuentosGlobales.length
|
|
307
|
+
? doc.descuentosGlobales.map((dg) => {
|
|
308
|
+
const esDesc = dg?.TpoMov === 'D';
|
|
309
|
+
const label = esDesc ? 'Descuento Global' : 'Recargo Global';
|
|
310
|
+
const valor = dg?.ValorDR ? '$' + formatMonto(dg.ValorDR) : (dg?.PctDR ? dg.PctDR + '%' : '');
|
|
311
|
+
return `<tr><td>${label}</td><td class="num">${valor}</td></tr>`;
|
|
312
|
+
}).join('')
|
|
313
|
+
: '';
|
|
314
|
+
|
|
315
|
+
// Totales según tipo de documento
|
|
316
|
+
let totalesHtml = '';
|
|
317
|
+
const esExenta = doc.tipoDte === 34;
|
|
318
|
+
|
|
319
|
+
if (esExenta) {
|
|
320
|
+
totalesHtml = `
|
|
321
|
+
${totales?.MntExe ? `<tr><td>Monto Exento</td><td class="num">$${formatMonto(totales.MntExe)}</td></tr>` : ''}
|
|
322
|
+
${totales?.MntTotal ? `<tr><td><strong>Monto Total</strong></td><td class="num"><strong>$${formatMonto(totales.MntTotal)}</strong></td></tr>` : ''}
|
|
323
|
+
`;
|
|
324
|
+
} else {
|
|
325
|
+
totalesHtml = `
|
|
326
|
+
${dctoGlobalHtml}
|
|
327
|
+
${totales?.MntNeto ? `<tr><td>Monto Neto</td><td class="num">$${formatMonto(totales.MntNeto)}</td></tr>` : ''}
|
|
328
|
+
${totales?.MntExe ? `<tr><td>Monto Exento</td><td class="num">$${formatMonto(totales.MntExe)}</td></tr>` : ''}
|
|
329
|
+
${totales?.IVA ? `<tr><td>IVA (${totales?.TasaIVA || 19}%)</td><td class="num">$${formatMonto(totales.IVA)}</td></tr>` : ''}
|
|
330
|
+
${totales?.MntTotal ? `<tr><td><strong>Monto Total</strong></td><td class="num"><strong>$${formatMonto(totales.MntTotal)}</strong></td></tr>` : ''}
|
|
331
|
+
`;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Acuse de recibo (solo en cedible y tipos que aplican)
|
|
335
|
+
const acuseHtml = mostrarCedible && !esNota
|
|
336
|
+
? `
|
|
337
|
+
<div class="acuse">
|
|
338
|
+
<table class="acuse-tabla">
|
|
339
|
+
<tr>
|
|
340
|
+
<td width="50%">Nombre: _______________________________</td>
|
|
341
|
+
<td width="50%">R.U.T.: _______________________________</td>
|
|
342
|
+
</tr>
|
|
343
|
+
<tr>
|
|
344
|
+
<td>Fecha: _______________________________</td>
|
|
345
|
+
<td>Recinto: _______________________________</td>
|
|
346
|
+
</tr>
|
|
347
|
+
<tr>
|
|
348
|
+
<td colspan="2">Firma: _______________________________</td>
|
|
349
|
+
</tr>
|
|
350
|
+
</table>
|
|
351
|
+
<div class="acuse-leyenda">${DECLARACION_RECIBO}</div>
|
|
352
|
+
</div>
|
|
353
|
+
`
|
|
354
|
+
: '';
|
|
355
|
+
|
|
356
|
+
return `
|
|
357
|
+
<!doctype html>
|
|
358
|
+
<html lang="es">
|
|
359
|
+
<head>
|
|
360
|
+
<meta charset="utf-8" />
|
|
361
|
+
<style>
|
|
362
|
+
@page { size: 215mm 280mm; margin: 10mm; }
|
|
363
|
+
* { box-sizing: border-box; }
|
|
364
|
+
body { font-family: Arial, sans-serif; font-size: 10px; color: #000; margin: 0; padding: 0; line-height: 1.3; }
|
|
365
|
+
|
|
366
|
+
.header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 10px; }
|
|
367
|
+
.emisor-info { flex: 1; }
|
|
368
|
+
.emisor-info .razon-social { font-size: 12px; font-weight: bold; margin-bottom: 3px; }
|
|
369
|
+
.emisor-info .giro { margin-bottom: 2px; }
|
|
370
|
+
.logo { max-width: 60mm; max-height: 15mm; margin-bottom: 5px; }
|
|
371
|
+
|
|
372
|
+
.recuadro { border: 1.5px solid #C00; padding: 8px 12px; text-align: center; width: 55mm; min-height: 15mm; }
|
|
373
|
+
.recuadro .rut { font-size: 11px; font-weight: bold; color: #C00; }
|
|
374
|
+
.recuadro .tipo { font-size: 10px; font-weight: bold; color: #C00; margin: 4px 0; }
|
|
375
|
+
.recuadro .folio { font-size: 11px; font-weight: bold; color: #C00; }
|
|
376
|
+
.recuadro-sii { font-size: 9px; color: #C00; margin-top: 3px; text-align: center; }
|
|
377
|
+
|
|
378
|
+
.receptor { border: 1px solid #999; padding: 8px; margin: 10px 0; font-size: 9px; }
|
|
379
|
+
.receptor-row { margin: 2px 0; }
|
|
380
|
+
.receptor-label { font-weight: bold; }
|
|
381
|
+
|
|
382
|
+
.fecha-emision { font-size: 10px; margin: 8px 0; }
|
|
383
|
+
.traslado { font-size: 9px; margin: 5px 0; padding: 4px; background: #f5f5f5; }
|
|
384
|
+
|
|
385
|
+
.seccion { margin: 10px 0; }
|
|
386
|
+
.seccion-titulo { font-weight: bold; font-size: 10px; margin-bottom: 5px; border-bottom: 1px solid #000; padding-bottom: 2px; }
|
|
387
|
+
|
|
388
|
+
table { width: 100%; border-collapse: collapse; }
|
|
389
|
+
.detalle th, .detalle td { border: 1px solid #666; padding: 3px 4px; font-size: 9px; }
|
|
390
|
+
.detalle th { background: #e0e0e0; font-weight: bold; }
|
|
391
|
+
.refs th, .refs td { border: 1px solid #999; padding: 2px 4px; font-size: 8px; }
|
|
392
|
+
.refs th { background: #f0f0f0; }
|
|
393
|
+
.num { text-align: right; }
|
|
394
|
+
|
|
395
|
+
.totales-container { display: flex; justify-content: flex-end; margin-top: 10px; }
|
|
396
|
+
.totales { width: 45%; }
|
|
397
|
+
.totales td { padding: 3px 6px; font-size: 10px; border: 1px solid #999; }
|
|
398
|
+
|
|
399
|
+
.acuse { border: 1px solid #000; padding: 8px; margin-top: 15px; font-size: 9px; }
|
|
400
|
+
.acuse-tabla { border: none; }
|
|
401
|
+
.acuse-tabla td { border: none; padding: 4px 0; }
|
|
402
|
+
.acuse-leyenda { margin-top: 8px; font-size: 8px; text-align: justify; border-top: 1px solid #999; padding-top: 5px; }
|
|
403
|
+
|
|
404
|
+
.timbre-container { margin-top: 15px; margin-left: 20mm; display: flex; align-items: flex-start; gap: 10px; }
|
|
405
|
+
.timbre-img { width: 50mm; height: auto; }
|
|
406
|
+
.timbre-text { font-size: 8px; }
|
|
407
|
+
.timbre-text div { margin: 2px 0; }
|
|
408
|
+
|
|
409
|
+
.cedible-marca { font-weight: bold; font-size: 12px; text-align: right; margin-top: 10px; }
|
|
410
|
+
</style>
|
|
411
|
+
</head>
|
|
412
|
+
<body>
|
|
413
|
+
<div class="header">
|
|
414
|
+
<div class="emisor-info">
|
|
415
|
+
${this.logoDataUri ? `<img class="logo" src="${this.logoDataUri}" />` : ''}
|
|
416
|
+
<div class="razon-social">${safeText(emisor?.RznSoc || emisor?.RznSocEmisor || '')}</div>
|
|
417
|
+
<div class="giro">${safeText(emisor?.GiroEmis || '')}</div>
|
|
418
|
+
<div>Casa Matriz: ${safeText(emisor?.DirOrigen || '')}${emisor?.CmnaOrigen ? ', ' + safeText(emisor.CmnaOrigen) : ''}</div>
|
|
419
|
+
${emisor?.Sucursal ? `<div>Sucursal: ${safeText(emisor.Sucursal)}</div>` : ''}
|
|
420
|
+
</div>
|
|
421
|
+
<div>
|
|
422
|
+
<div class="recuadro">
|
|
423
|
+
<div class="rut">R.U.T.: ${safeText(emisor?.RUTEmisor || '').replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1.')}</div>
|
|
424
|
+
<div class="tipo">${safeText(tipoNombre)}</div>
|
|
425
|
+
<div class="folio">N° ${safeText(doc.folio)}</div>
|
|
426
|
+
</div>
|
|
427
|
+
<div class="recuadro-sii">${this.siiOficina}</div>
|
|
428
|
+
</div>
|
|
429
|
+
</div>
|
|
430
|
+
|
|
431
|
+
<div class="fecha-emision"><strong>Fecha Emisión:</strong> ${safeText(doc.fechaEmision)}</div>
|
|
432
|
+
|
|
433
|
+
<div class="receptor">
|
|
434
|
+
<div class="receptor-row"><span class="receptor-label">Señor(es):</span> ${safeText(receptor?.RznSocRecep || '')} <span class="receptor-label">RUT:</span> ${safeText(receptor?.RUTRecep || '')}</div>
|
|
435
|
+
<div class="receptor-row"><span class="receptor-label">Dirección:</span> ${safeText(receptor?.DirRecep || '')} <span class="receptor-label">Comuna:</span> ${safeText(receptor?.CmnaRecep || '')}</div>
|
|
436
|
+
<div class="receptor-row"><span class="receptor-label">Giro:</span> ${safeText(receptor?.GiroRecep || '')}</div>
|
|
437
|
+
</div>
|
|
438
|
+
|
|
439
|
+
${trasladoHtml}
|
|
440
|
+
${refsHtml}
|
|
441
|
+
|
|
442
|
+
<div class="seccion">
|
|
443
|
+
<div class="seccion-titulo">Detalle</div>
|
|
444
|
+
<table class="detalle">
|
|
445
|
+
<thead>
|
|
446
|
+
<tr>
|
|
447
|
+
<th width="5%">#</th>
|
|
448
|
+
<th width="10%">Código</th>
|
|
449
|
+
<th width="40%">Descripción</th>
|
|
450
|
+
<th width="10%">Cant.</th>
|
|
451
|
+
<th width="8%">Unid.</th>
|
|
452
|
+
<th width="12%">P.Unit.</th>
|
|
453
|
+
<th width="15%">Valor</th>
|
|
454
|
+
</tr>
|
|
455
|
+
</thead>
|
|
456
|
+
<tbody>
|
|
457
|
+
${detallesHtml}
|
|
458
|
+
</tbody>
|
|
459
|
+
</table>
|
|
460
|
+
</div>
|
|
461
|
+
|
|
462
|
+
<div class="totales-container">
|
|
463
|
+
<table class="totales">
|
|
464
|
+
<tbody>
|
|
465
|
+
${totalesHtml}
|
|
466
|
+
</tbody>
|
|
467
|
+
</table>
|
|
468
|
+
</div>
|
|
469
|
+
|
|
470
|
+
${acuseHtml}
|
|
471
|
+
|
|
472
|
+
<div class="timbre-container">
|
|
473
|
+
<img class="timbre-img" src="${tedDataUri}" />
|
|
474
|
+
<div class="timbre-text">
|
|
475
|
+
<div><strong>Timbre Electrónico SII</strong></div>
|
|
476
|
+
<div>${this.resolucion}</div>
|
|
477
|
+
<div>Verifique documento: www.sii.cl</div>
|
|
478
|
+
</div>
|
|
479
|
+
</div>
|
|
480
|
+
|
|
481
|
+
${mostrarCedible ? `<div class="cedible-marca">${cedibleTexto}</div>` : ''}
|
|
482
|
+
</body>
|
|
483
|
+
</html>
|
|
484
|
+
`;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Genera PDF desde HTML
|
|
489
|
+
* @private
|
|
490
|
+
*/
|
|
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' },
|
|
500
|
+
});
|
|
501
|
+
await page.close();
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Genera muestras impresas desde archivos XML
|
|
506
|
+
* @param {Object} options
|
|
507
|
+
* @param {string[]} options.xmlFiles - Array de rutas a archivos XML
|
|
508
|
+
* @param {string} options.outDir - Directorio de salida
|
|
509
|
+
* @param {boolean} [options.generarCedible=false] - Generar también copias cedibles
|
|
510
|
+
* @returns {Promise<Object>} Resultado con estadísticas y archivos generados
|
|
511
|
+
*/
|
|
512
|
+
async generarMuestras({ xmlFiles, outDir, generarCedible = false }) {
|
|
513
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
514
|
+
|
|
515
|
+
// Crear subdirectorios según requisitos del SII
|
|
516
|
+
const pruebasDir = path.join(outDir, 'SET-PRUEBAS');
|
|
517
|
+
const simulacionDir = path.join(outDir, 'SET-SIMULACION');
|
|
518
|
+
fs.mkdirSync(pruebasDir, { recursive: true });
|
|
519
|
+
fs.mkdirSync(simulacionDir, { recursive: true });
|
|
520
|
+
|
|
521
|
+
console.log('\n' + '═'.repeat(60));
|
|
522
|
+
console.log('📄 GENERACIÓN DE MUESTRAS IMPRESAS');
|
|
523
|
+
console.log('═'.repeat(60));
|
|
524
|
+
console.log(` 📂 SET-PRUEBAS: ${pruebasDir}`);
|
|
525
|
+
console.log(` 📂 SET-SIMULACION: ${simulacionDir}`);
|
|
526
|
+
|
|
527
|
+
const browser = await puppeteer.launch({ headless: true });
|
|
528
|
+
const resultado = {
|
|
529
|
+
success: true,
|
|
530
|
+
totalDocs: 0,
|
|
531
|
+
totalPdfs: 0,
|
|
532
|
+
archivos: [],
|
|
533
|
+
errores: [],
|
|
534
|
+
setPruebas: 0,
|
|
535
|
+
setSimulacion: 0,
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
try {
|
|
539
|
+
for (const filePath of xmlFiles) {
|
|
540
|
+
console.log(`\n 📄 Procesando: ${path.basename(filePath)}`);
|
|
541
|
+
const xml = fs.readFileSync(filePath, 'utf8');
|
|
542
|
+
|
|
543
|
+
let documentos;
|
|
544
|
+
try {
|
|
545
|
+
documentos = this.parseEnvioDTE(xml);
|
|
546
|
+
} catch (e) {
|
|
547
|
+
console.log(` ⚠️ Error parseando: ${e.message}`);
|
|
548
|
+
resultado.errores.push({ file: filePath, error: e.message });
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
if (!documentos.length) {
|
|
553
|
+
console.log(' ⚠️ Sin documentos');
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Determinar directorio de salida según archivo fuente
|
|
558
|
+
const sourceFile = path.basename(filePath).toLowerCase();
|
|
559
|
+
const isPruebas = /envio-set-(basico|guia|exenta|compra)\.xml/i.test(sourceFile);
|
|
560
|
+
const targetDir = isPruebas ? pruebasDir : simulacionDir;
|
|
561
|
+
const categoria = isPruebas ? 'PRUEBAS' : 'SIMULACION';
|
|
562
|
+
console.log(` 📁 Categoría: SET-${categoria}`);
|
|
563
|
+
|
|
564
|
+
for (const doc of documentos) {
|
|
565
|
+
resultado.totalDocs++;
|
|
566
|
+
|
|
567
|
+
try {
|
|
568
|
+
const tedDataUri = await this.generarPdf417(doc.tedXml);
|
|
569
|
+
|
|
570
|
+
// Generar ejemplar tributario (sin cedible)
|
|
571
|
+
const html = this._buildHtml({ doc, esCedible: false, tedDataUri });
|
|
572
|
+
|
|
573
|
+
// PDFs organizados en subdirectorios según categoría SII
|
|
574
|
+
const outputName = `muestra_${doc.tipoDte}_${doc.folio}.pdf`;
|
|
575
|
+
const outputPath = path.join(targetDir, outputName);
|
|
576
|
+
|
|
577
|
+
await this._generarPdf({ html, outputPath, browser });
|
|
578
|
+
resultado.totalPdfs++;
|
|
579
|
+
resultado.archivos.push(outputPath);
|
|
580
|
+
if (isPruebas) resultado.setPruebas++;
|
|
581
|
+
else resultado.setSimulacion++;
|
|
582
|
+
console.log(` ✓ ${outputName}`);
|
|
583
|
+
|
|
584
|
+
// Generar copia cedible si corresponde
|
|
585
|
+
if (generarCedible && CEDIBLE_TIPOS.has(doc.tipoDte)) {
|
|
586
|
+
// Guía de traslado interno no tiene cedible
|
|
587
|
+
if (doc.tipoDte === 52 && [5, 6].includes(doc.indTraslado)) {
|
|
588
|
+
console.log(` ⏭️ Guía traslado interno - sin cedible`);
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const htmlCedible = this._buildHtml({ doc, esCedible: true, tedDataUri });
|
|
593
|
+
const outputNameCedible = `muestra_${doc.tipoDte}_${doc.folio}_cedible.pdf`;
|
|
594
|
+
const outputPathCedible = path.join(targetDir, outputNameCedible);
|
|
595
|
+
|
|
596
|
+
await this._generarPdf({ html: htmlCedible, outputPath: outputPathCedible, browser });
|
|
597
|
+
resultado.totalPdfs++;
|
|
598
|
+
resultado.archivos.push(outputPathCedible);
|
|
599
|
+
if (isPruebas) resultado.setPruebas++;
|
|
600
|
+
else resultado.setSimulacion++;
|
|
601
|
+
console.log(` ✓ ${outputNameCedible}`);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
} catch (e) {
|
|
605
|
+
console.log(` ❌ Error: ${e.message}`);
|
|
606
|
+
resultado.errores.push({ tipo: doc.tipoDte, folio: doc.folio, error: e.message });
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
} finally {
|
|
611
|
+
await browser.close();
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// Resumen
|
|
615
|
+
console.log('\n' + '═'.repeat(60));
|
|
616
|
+
console.log('✅ MUESTRAS IMPRESAS GENERADAS');
|
|
617
|
+
console.log('═'.repeat(60));
|
|
618
|
+
console.log(` 📄 Documentos procesados: ${resultado.totalDocs}`);
|
|
619
|
+
console.log(` 📄 PDFs generados: ${resultado.totalPdfs}`);
|
|
620
|
+
console.log(` 📂 SET-PRUEBAS: ${resultado.setPruebas} PDFs`);
|
|
621
|
+
console.log(` 📂 SET-SIMULACION: ${resultado.setSimulacion} PDFs`);
|
|
622
|
+
console.log(` 📂 Directorio base: ${outDir}`);
|
|
623
|
+
|
|
624
|
+
if (resultado.errores.length > 0) {
|
|
625
|
+
resultado.success = false;
|
|
626
|
+
console.log(` ⚠️ Errores: ${resultado.errores.length}`);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
return resultado;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* Busca archivos XML recursivamente en una carpeta
|
|
634
|
+
* Filtra solo envíos que contienen DTEs (no respuestas de intercambio)
|
|
635
|
+
* @param {string} inputPath - Ruta a archivo o carpeta
|
|
636
|
+
* @returns {string[]} Array de rutas a archivos XML
|
|
637
|
+
*/
|
|
638
|
+
static buscarXmls(inputPath) {
|
|
639
|
+
if (!inputPath) return [];
|
|
640
|
+
const resolved = path.resolve(inputPath);
|
|
641
|
+
if (!fs.existsSync(resolved)) return [];
|
|
642
|
+
const stats = fs.statSync(resolved);
|
|
643
|
+
if (stats.isFile()) return [resolved];
|
|
644
|
+
if (stats.isDirectory()) {
|
|
645
|
+
const files = [];
|
|
646
|
+
const scanDir = (dir) => {
|
|
647
|
+
const items = fs.readdirSync(dir);
|
|
648
|
+
for (const item of items) {
|
|
649
|
+
const fullPath = path.join(dir, item);
|
|
650
|
+
const stat = fs.statSync(fullPath);
|
|
651
|
+
if (stat.isDirectory()) {
|
|
652
|
+
scanDir(fullPath);
|
|
653
|
+
} else if (item.toLowerCase().endsWith('.xml')) {
|
|
654
|
+
const lowerItem = item.toLowerCase();
|
|
655
|
+
// Incluir: envio-set-*, envio-simulacion, EnvioBOLETA
|
|
656
|
+
// Excluir: respuesta-*, envio-recibos (intercambio sin DTEs)
|
|
657
|
+
const isEnvioDTE = (
|
|
658
|
+
lowerItem.includes('envio-set-') ||
|
|
659
|
+
lowerItem.includes('envio-simulacion') ||
|
|
660
|
+
lowerItem.includes('envioboleta') ||
|
|
661
|
+
lowerItem === 'envioboleta.xml'
|
|
662
|
+
);
|
|
663
|
+
if (isEnvioDTE) {
|
|
664
|
+
files.push(fullPath);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
};
|
|
669
|
+
scanDir(resolved);
|
|
670
|
+
return files;
|
|
671
|
+
}
|
|
672
|
+
return [];
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
module.exports = MuestrasImpresas;
|