@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,1184 @@
|
|
|
1
|
+
// Copyright (c) 2026 Devlas SpA — https://devlas.cl
|
|
2
|
+
// Licencia MIT. Ver archivo LICENSE para mas detalles.
|
|
3
|
+
/**
|
|
4
|
+
* SetParser - Parseo de Sets de Prueba del SII
|
|
5
|
+
*
|
|
6
|
+
* Extrae casos de prueba del HTML/texto descargado del portal SII
|
|
7
|
+
* y genera estructuras compatibles con los módulos de certificación.
|
|
8
|
+
*
|
|
9
|
+
* @module dte-sii/cert/SetParser
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// ═══════════════════════════════════════════════════════════════
|
|
13
|
+
// FUNCIONES AUXILIARES
|
|
14
|
+
// ═══════════════════════════════════════════════════════════════
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Detecta el tipo de set según el nombre
|
|
18
|
+
*/
|
|
19
|
+
function detectarTipoSet(nombreSet) {
|
|
20
|
+
const nombre = nombreSet.toUpperCase();
|
|
21
|
+
if (nombre.includes('LIBRO DE COMPRAS PARA EXENTOS')) return 'LIBRO_COMPRAS_EXENTOS';
|
|
22
|
+
if (nombre.includes('LIBRO DE COMPRAS')) return 'LIBRO_COMPRAS';
|
|
23
|
+
if (nombre.includes('LIBRO DE VENTAS')) return 'LIBRO_VENTAS';
|
|
24
|
+
if (nombre.includes('LIBRO DE GUIAS')) return 'LIBRO_GUIAS';
|
|
25
|
+
if (nombre.includes('GUIA DE DESPACHO') || nombre.includes('GUIA')) return 'GUIA_DESPACHO';
|
|
26
|
+
if (nombre.includes('FACTURA EXENTA') || nombre.includes('NO AFECTA')) return 'FACTURA_EXENTA';
|
|
27
|
+
if (nombre.includes('EXPORTACION')) return 'EXPORTACION';
|
|
28
|
+
if (nombre.includes('LIQUIDACION')) return 'LIQUIDACION';
|
|
29
|
+
if (nombre.includes('FACTURA DE COMPRA') || nombre.includes('EMISOR DE FACTURA DE COMPRA')) return 'FACTURA_COMPRA';
|
|
30
|
+
if (nombre.includes('BASICO')) return 'BASICO';
|
|
31
|
+
return 'OTRO';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Detecta el tipo DTE a partir del nombre del documento
|
|
36
|
+
*/
|
|
37
|
+
function detectarTipoDTE(documento) {
|
|
38
|
+
if (!documento) return null;
|
|
39
|
+
const doc = documento.toUpperCase();
|
|
40
|
+
|
|
41
|
+
if (doc.includes('LIQUIDACION') || doc.includes('LIQUIDACI')) {
|
|
42
|
+
return { codigo: 43, nombre: 'Liquidación Factura' };
|
|
43
|
+
}
|
|
44
|
+
if (doc.includes('FACTURA DE COMPRA')) {
|
|
45
|
+
return { codigo: 46, nombre: 'Factura de Compra' };
|
|
46
|
+
}
|
|
47
|
+
if (doc.includes('FACTURA DE EXPORTACION') || doc.includes('FACTURA DE EXPORTACI')) {
|
|
48
|
+
return { codigo: 110, nombre: 'Factura de Exportación' };
|
|
49
|
+
}
|
|
50
|
+
if (doc.includes('NOTA DE CREDITO') && doc.includes('EXPORTACION')) {
|
|
51
|
+
return { codigo: 112, nombre: 'NC Exportación' };
|
|
52
|
+
}
|
|
53
|
+
if (doc.includes('NOTA DE DEBITO') && doc.includes('EXPORTACION')) {
|
|
54
|
+
return { codigo: 111, nombre: 'ND Exportación' };
|
|
55
|
+
}
|
|
56
|
+
if (doc.includes('NO AFECTA') || doc.includes('EXENTA')) {
|
|
57
|
+
return { codigo: 34, nombre: 'Factura Exenta' };
|
|
58
|
+
}
|
|
59
|
+
if (doc.includes('GUIA DE DESPACHO') || doc.includes('GUIA')) {
|
|
60
|
+
return { codigo: 52, nombre: 'Guía de Despacho' };
|
|
61
|
+
}
|
|
62
|
+
if (doc.includes('NOTA DE CREDITO') || doc.includes('NOTA DE CRÉDITO')) {
|
|
63
|
+
return { codigo: 61, nombre: 'Nota de Crédito' };
|
|
64
|
+
}
|
|
65
|
+
if (doc.includes('NOTA DE DEBITO') || doc.includes('NOTA DE DÉBITO')) {
|
|
66
|
+
return { codigo: 56, nombre: 'Nota de Débito' };
|
|
67
|
+
}
|
|
68
|
+
if (doc.includes('FACTURA')) {
|
|
69
|
+
return { codigo: 33, nombre: 'Factura Electrónica' };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Determina código de referencia según razón
|
|
77
|
+
*/
|
|
78
|
+
function determinarCodRef(razon) {
|
|
79
|
+
if (!razon) return 1;
|
|
80
|
+
const r = razon.toUpperCase();
|
|
81
|
+
if (r.includes('ANULA')) return 1;
|
|
82
|
+
if (r.includes('CORRIGE') && r.includes('GIRO')) return 2;
|
|
83
|
+
if (r.includes('CORRIGE') && r.includes('TEXTO')) return 2;
|
|
84
|
+
if (r.includes('DEVOLUCION')) return 3;
|
|
85
|
+
if (r.includes('MODIFICA')) return 3;
|
|
86
|
+
return 1;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Determina indicador de traslado para guías
|
|
91
|
+
*/
|
|
92
|
+
function determinarIndTraslado(motivo) {
|
|
93
|
+
if (!motivo) return 1;
|
|
94
|
+
const m = motivo.toUpperCase();
|
|
95
|
+
if (m.includes('TRASLADO') && (m.includes('INTERNO') || m.includes('BODEGA'))) return 5;
|
|
96
|
+
if (m.includes('VENTA')) return 1;
|
|
97
|
+
if (m.includes('CONSIGNACION')) return 2;
|
|
98
|
+
if (m.includes('ENTREGA GRATUITA')) return 3;
|
|
99
|
+
if (m.includes('COMPROBANTE')) return 4;
|
|
100
|
+
if (m.includes('TRASLADO')) return 5;
|
|
101
|
+
if (m.includes('DEVOLUCION')) return 6;
|
|
102
|
+
return 1;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Determina tipo de despacho
|
|
107
|
+
*/
|
|
108
|
+
function determinarTpoDespacho(trasladoPor) {
|
|
109
|
+
if (!trasladoPor) return null;
|
|
110
|
+
const t = trasladoPor.toUpperCase();
|
|
111
|
+
// IMPORTANTE: Buscar EMISOR primero porque frases como
|
|
112
|
+
// "EMISOR DEL DOCUMENTO AL LOCAL DEL CLIENTE" contienen ambas palabras
|
|
113
|
+
if (t.includes('EMISOR')) return 2;
|
|
114
|
+
if (t.includes('CLIENTE')) return 1;
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Mapea tipo documento de texto a código SII
|
|
120
|
+
*/
|
|
121
|
+
function mapearTipoDocLibro(tipo) {
|
|
122
|
+
const t = tipo.toUpperCase();
|
|
123
|
+
if (t.includes('FACTURA DE COMPRA ELECTRONICA')) return 46;
|
|
124
|
+
if (t.includes('FACTURA EXENTA ELECTRONICA') || (t.includes('NO AFECTA') && t.includes('ELECTRONICA'))) return 34;
|
|
125
|
+
if (t.includes('FACTURA ELECTRONICA')) return 33;
|
|
126
|
+
if (t.includes('FACTURA EXENTA') || t.includes('FACTURA NO AFECTA')) return 30; // exenta papel
|
|
127
|
+
if (t === 'FACTURA') return 30;
|
|
128
|
+
if (t.includes('NOTA DE CREDITO') && (t.includes('ELECTRONICA') || t.includes('ELECTRONICO'))) return 61;
|
|
129
|
+
if (t.includes('NOTA DE CREDITO')) return 60;
|
|
130
|
+
if (t.includes('NOTA DE DEBITO') && (t.includes('ELECTRONICA') || t.includes('ELECTRONICO'))) return 56;
|
|
131
|
+
if (t.includes('NOTA DE DEBITO')) return 55;
|
|
132
|
+
return 30;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ═══════════════════════════════════════════════════════════════
|
|
136
|
+
// PARSER PRINCIPAL
|
|
137
|
+
// ═══════════════════════════════════════════════════════════════
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Extrae los casos de prueba del texto del set descargado del SII
|
|
141
|
+
*
|
|
142
|
+
* @param {string} texto - Texto plano del set (HTML limpio)
|
|
143
|
+
* @returns {Object} Datos extraídos con sets y casos
|
|
144
|
+
*/
|
|
145
|
+
function extraerCasosDelSet(texto) {
|
|
146
|
+
const resultado = {
|
|
147
|
+
sets: [],
|
|
148
|
+
totalCasos: 0,
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const lineas = texto.split('\n');
|
|
152
|
+
|
|
153
|
+
let setActual = null;
|
|
154
|
+
let casoActual = null;
|
|
155
|
+
let enCabecera = false;
|
|
156
|
+
let enLibroCompras = false;
|
|
157
|
+
let enLibroComprasExentos = false;
|
|
158
|
+
let docActual = null;
|
|
159
|
+
let esperandoMontos = false;
|
|
160
|
+
|
|
161
|
+
for (let i = 0; i < lineas.length; i++) {
|
|
162
|
+
const linea = lineas[i];
|
|
163
|
+
const lineaTrim = linea.trim();
|
|
164
|
+
|
|
165
|
+
// Detectar inicio de SET
|
|
166
|
+
// Regex flexible: ATENCION puede tener tilde corrupta (�), tilde UTF-8 (Ó) o sin tilde (O)
|
|
167
|
+
const matchSet = lineaTrim.match(/^SET\s+(.+?)\s*[-:]\s*NUMERO DE ATENCI[^\d:]*[:\s]*(\d+)/i);
|
|
168
|
+
if (matchSet) {
|
|
169
|
+
// Guardar caso anterior si existe
|
|
170
|
+
if (casoActual && setActual) {
|
|
171
|
+
setActual.casos.push(casoActual);
|
|
172
|
+
casoActual = null;
|
|
173
|
+
}
|
|
174
|
+
// Guardar documento actual del libro si existe
|
|
175
|
+
if (docActual && setActual) {
|
|
176
|
+
setActual.documentosLibro.push(docActual);
|
|
177
|
+
docActual = null;
|
|
178
|
+
}
|
|
179
|
+
// Guardar set anterior si existe
|
|
180
|
+
if (setActual) {
|
|
181
|
+
resultado.sets.push(setActual);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const nombreSet = matchSet[1].trim();
|
|
185
|
+
const tipoSet = detectarTipoSet(nombreSet);
|
|
186
|
+
setActual = {
|
|
187
|
+
nombre: nombreSet,
|
|
188
|
+
numeroAtencion: matchSet[2],
|
|
189
|
+
tipo: tipoSet,
|
|
190
|
+
casos: [],
|
|
191
|
+
instrucciones: [],
|
|
192
|
+
documentosLibro: [], // Para SET LIBRO DE COMPRAS
|
|
193
|
+
observaciones: null,
|
|
194
|
+
factorProporcionalidad: null,
|
|
195
|
+
};
|
|
196
|
+
enLibroCompras = (tipoSet === 'LIBRO_COMPRAS');
|
|
197
|
+
enLibroComprasExentos = (tipoSet === 'LIBRO_COMPRAS_EXENTOS');
|
|
198
|
+
esperandoMontos = false;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Detectar inicio de CASO (formato: CASO 4670590-1)
|
|
203
|
+
const matchCaso = lineaTrim.match(/^CASO\s+(\d+)-(\d+)\s*$/i);
|
|
204
|
+
if (matchCaso) {
|
|
205
|
+
// Guardar caso anterior si existe
|
|
206
|
+
if (casoActual && setActual) {
|
|
207
|
+
setActual.casos.push(casoActual);
|
|
208
|
+
}
|
|
209
|
+
// Guardar documento actual si existe
|
|
210
|
+
if (docActual && setActual) {
|
|
211
|
+
setActual.documentosLibro.push(docActual);
|
|
212
|
+
docActual = null;
|
|
213
|
+
}
|
|
214
|
+
enLibroCompras = false;
|
|
215
|
+
enLibroComprasExentos = false;
|
|
216
|
+
|
|
217
|
+
casoActual = {
|
|
218
|
+
setId: matchCaso[1],
|
|
219
|
+
numeroCaso: parseInt(matchCaso[2]),
|
|
220
|
+
id: `${matchCaso[1]}-${matchCaso[2]}`,
|
|
221
|
+
documento: null,
|
|
222
|
+
tipoDTE: null,
|
|
223
|
+
items: [],
|
|
224
|
+
referencia: null,
|
|
225
|
+
razonReferencia: null,
|
|
226
|
+
descuentoGlobal: null,
|
|
227
|
+
motivo: null,
|
|
228
|
+
trasladoPor: null,
|
|
229
|
+
moneda: null,
|
|
230
|
+
unidadMedida: null,
|
|
231
|
+
// Campos de exportación
|
|
232
|
+
formaPago: null,
|
|
233
|
+
modalidadVenta: null,
|
|
234
|
+
clausulaVenta: null,
|
|
235
|
+
totalClausula: null,
|
|
236
|
+
viaTransporte: null,
|
|
237
|
+
puertoEmbarque: null,
|
|
238
|
+
puertoDesembarque: null,
|
|
239
|
+
tipoBulto: null,
|
|
240
|
+
totalBultos: null,
|
|
241
|
+
flete: null,
|
|
242
|
+
seguro: null,
|
|
243
|
+
paisDestino: null,
|
|
244
|
+
comisionExtranjero: null,
|
|
245
|
+
raw: [],
|
|
246
|
+
};
|
|
247
|
+
enCabecera = true;
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Ignorar línea de separación ======
|
|
252
|
+
if (lineaTrim.match(/^=+$/)) {
|
|
253
|
+
enCabecera = false;
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Si estamos en un caso, parsear contenido
|
|
258
|
+
if (casoActual) {
|
|
259
|
+
casoActual.raw.push(lineaTrim);
|
|
260
|
+
|
|
261
|
+
// Detectar tipo de documento
|
|
262
|
+
const matchDocumento = lineaTrim.match(/^DOCUMENTO\s+(.+)$/i);
|
|
263
|
+
if (matchDocumento) {
|
|
264
|
+
casoActual.documento = matchDocumento[1].trim();
|
|
265
|
+
casoActual.tipoDTE = detectarTipoDTE(casoActual.documento);
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Detectar referencia (puede tener : o no)
|
|
270
|
+
const matchRef = lineaTrim.match(/^REFERENCIA[:\s]+(.+)$/i);
|
|
271
|
+
if (matchRef) {
|
|
272
|
+
casoActual.referencia = matchRef[1].trim();
|
|
273
|
+
// Extraer caso referenciado
|
|
274
|
+
const matchCasoRef = casoActual.referencia.match(/CASO\s+(\d+-\d+)/i);
|
|
275
|
+
if (matchCasoRef) {
|
|
276
|
+
casoActual.casoReferenciado = matchCasoRef[1];
|
|
277
|
+
}
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Detectar razón referencia
|
|
282
|
+
const matchRazon = lineaTrim.match(/^RAZON\s+REFERENCIA\s+(.+)$/i);
|
|
283
|
+
if (matchRazon) {
|
|
284
|
+
casoActual.razonReferencia = matchRazon[1].trim();
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Detectar motivo (para guías)
|
|
289
|
+
const matchMotivo = lineaTrim.match(/^MOTIVO[:\s]+(.+)$/i);
|
|
290
|
+
if (matchMotivo) {
|
|
291
|
+
casoActual.motivo = matchMotivo[1].trim();
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Detectar traslado por
|
|
296
|
+
const matchTraslado = lineaTrim.match(/^TRASLADO\s+POR[:\s]+(.+)$/i);
|
|
297
|
+
if (matchTraslado) {
|
|
298
|
+
casoActual.trasladoPor = matchTraslado[1].trim();
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Detectar descuento global
|
|
303
|
+
const matchDescGlobal = lineaTrim.match(/^DESCUENTO\s+GLOBAL.+?(\d+)%/i);
|
|
304
|
+
if (matchDescGlobal) {
|
|
305
|
+
casoActual.descuentoGlobal = parseInt(matchDescGlobal[1]);
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Detectar moneda
|
|
310
|
+
const matchMoneda = lineaTrim.match(/^MONEDA\s+DE\s+LA\s+OPERACION[:\s]+(.+)$/i);
|
|
311
|
+
if (matchMoneda) {
|
|
312
|
+
casoActual.moneda = matchMoneda[1].trim();
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ===== CAMPOS DE EXPORTACIÓN =====
|
|
317
|
+
const matchFormaPago = lineaTrim.match(/^FORMA\s+DE\s+PAGO\s+EXPORTACION[:\s]+(.+)$/i);
|
|
318
|
+
if (matchFormaPago) {
|
|
319
|
+
casoActual.formaPago = matchFormaPago[1].trim();
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const matchModalidad = lineaTrim.match(/^MODALIDAD\s+DE\s+VENTA[:\s]+(.+)$/i);
|
|
324
|
+
if (matchModalidad) {
|
|
325
|
+
casoActual.modalidadVenta = matchModalidad[1].trim();
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const matchClausula = lineaTrim.match(/^CLAUSULA\s+DE\s+VENTA.+?[:\s]+(.+)$/i);
|
|
330
|
+
if (matchClausula) {
|
|
331
|
+
casoActual.clausulaVenta = matchClausula[1].trim();
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const matchTotalClausula = lineaTrim.match(/^TOTAL\s+CLAUSULA\s+DE\s+VENTA[:\s]+(.+)$/i);
|
|
336
|
+
if (matchTotalClausula) {
|
|
337
|
+
casoActual.totalClausula = parseFloat(matchTotalClausula[1].trim());
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const matchVia = lineaTrim.match(/^VIA\s+DE\s+TRANSPORTE[:\s]+(.+)$/i);
|
|
342
|
+
if (matchVia) {
|
|
343
|
+
casoActual.viaTransporte = matchVia[1].trim();
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const matchPuertoEmb = lineaTrim.match(/^PUERTO\s+DE\s+EMBARQUE[:\s]+(.+)$/i);
|
|
348
|
+
if (matchPuertoEmb) {
|
|
349
|
+
casoActual.puertoEmbarque = matchPuertoEmb[1].trim();
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const matchPuertoDes = lineaTrim.match(/^PUERTO\s+DE\s+DESEMBARQUE[:\s]+(.+)$/i);
|
|
354
|
+
if (matchPuertoDes) {
|
|
355
|
+
casoActual.puertoDesembarque = matchPuertoDes[1].trim();
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const matchTipoBulto = lineaTrim.match(/^TIPO\s+DE\s+BULTO[:\s]+(.+)$/i);
|
|
360
|
+
if (matchTipoBulto) {
|
|
361
|
+
casoActual.tipoBulto = matchTipoBulto[1].trim();
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const matchTotalBultos = lineaTrim.match(/^TOTAL\s+BULTOS[:\s]+(\d+)/i);
|
|
366
|
+
if (matchTotalBultos) {
|
|
367
|
+
casoActual.totalBultos = parseInt(matchTotalBultos[1]);
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const matchFlete = lineaTrim.match(/^FLETE[^:]*[:\s]+(.+)$/i);
|
|
372
|
+
if (matchFlete) {
|
|
373
|
+
casoActual.flete = parseFloat(matchFlete[1].trim());
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const matchSeguro = lineaTrim.match(/^SEGURO[^:]*[:\s]+(.+)$/i);
|
|
378
|
+
if (matchSeguro) {
|
|
379
|
+
casoActual.seguro = parseFloat(matchSeguro[1].trim());
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const matchPais = lineaTrim.match(/^PAIS\s+RECEPTOR.+?[:\s]+(.+)$/i);
|
|
384
|
+
if (matchPais) {
|
|
385
|
+
casoActual.paisDestino = matchPais[1].trim();
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const matchComision = lineaTrim.match(/^COMISIONES?\s+EN\s+EL\s+EXTRANJERO.+?(\d+)%/i);
|
|
390
|
+
if (matchComision) {
|
|
391
|
+
casoActual.comisionExtranjero = parseInt(matchComision[1]);
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Ignorar cabeceras de items
|
|
396
|
+
if (lineaTrim.match(/^ITEM\s+(CANTIDAD|VALOR)/i)) {
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// ===== PARSING DE ITEMS =====
|
|
401
|
+
// Dividir por tabs o múltiples espacios para manejar diferentes formatos
|
|
402
|
+
const partes = lineaTrim.split(/\t+|\s{2,}/).map(p => p.trim()).filter(p => p);
|
|
403
|
+
|
|
404
|
+
// Formato con tabs: "NOMBRE CANTIDAD UNIDAD PRECIO" o "NOMBRE CANTIDAD PRECIO DESCUENTO"
|
|
405
|
+
if (partes.length >= 2 && !lineaTrim.startsWith('DOCUMENTO') && !lineaTrim.startsWith('REFERENCIA')
|
|
406
|
+
&& !lineaTrim.startsWith('RAZON') && !lineaTrim.includes(':')) {
|
|
407
|
+
|
|
408
|
+
const nombre = partes[0];
|
|
409
|
+
|
|
410
|
+
// Ignorar si parece una cabecera
|
|
411
|
+
if (nombre === 'ITEM' || nombre.includes('CANTIDAD') || nombre.includes('UNITARIO')) {
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Detectar formato según número de partes
|
|
416
|
+
if (partes.length === 4) {
|
|
417
|
+
// Puede ser: NOMBRE, CANTIDAD, UNIDAD, PRECIO o NOMBRE, CANTIDAD, PRECIO, DESCUENTO
|
|
418
|
+
const posibleUnidad = partes[2];
|
|
419
|
+
if (posibleUnidad.match(/^[A-Za-z]+$/) && !posibleUnidad.includes('%')) {
|
|
420
|
+
// Es una unidad de medida: NOMBRE, QTY, UNIDAD, PRECIO
|
|
421
|
+
casoActual.items.push({
|
|
422
|
+
nombre: nombre,
|
|
423
|
+
cantidad: parseInt(partes[1]) || 1,
|
|
424
|
+
unidadMedida: posibleUnidad,
|
|
425
|
+
precioUnitario: parseInt(partes[3]) || 0,
|
|
426
|
+
});
|
|
427
|
+
} else {
|
|
428
|
+
// Es descuento: NOMBRE, QTY, PRECIO, DESCUENTO
|
|
429
|
+
casoActual.items.push({
|
|
430
|
+
nombre: nombre,
|
|
431
|
+
cantidad: parseInt(partes[1]) || 1,
|
|
432
|
+
precioUnitario: parseInt(partes[2]) || 0,
|
|
433
|
+
descuento: partes[3],
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (partes.length === 3) {
|
|
440
|
+
// NOMBRE, CANTIDAD, PRECIO/VALOR
|
|
441
|
+
const qty = parseInt(partes[1]);
|
|
442
|
+
const precio = parseInt(partes[2]);
|
|
443
|
+
if (!isNaN(qty) && !isNaN(precio)) {
|
|
444
|
+
casoActual.items.push({
|
|
445
|
+
nombre: nombre,
|
|
446
|
+
cantidad: qty,
|
|
447
|
+
precioUnitario: precio,
|
|
448
|
+
});
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (partes.length === 2) {
|
|
454
|
+
// NOMBRE, CANTIDAD (guías sin precio) o NOMBRE, VALOR (NC/ND)
|
|
455
|
+
const valor = parseInt(partes[1]);
|
|
456
|
+
if (!isNaN(valor)) {
|
|
457
|
+
// Para NC/ND que modifican monto, el segundo valor es el precio unitario modificado
|
|
458
|
+
if (casoActual.razonReferencia?.includes('MODIFICA MONTO')) {
|
|
459
|
+
casoActual.items.push({
|
|
460
|
+
nombre: nombre,
|
|
461
|
+
cantidad: 1,
|
|
462
|
+
precioUnitario: valor,
|
|
463
|
+
});
|
|
464
|
+
} else {
|
|
465
|
+
// Para guías o NC por devolución, es la cantidad
|
|
466
|
+
casoActual.items.push({
|
|
467
|
+
nombre: nombre,
|
|
468
|
+
cantidad: valor,
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Fallback: items con formato de espacios simples
|
|
477
|
+
// "ITEM 1 56" - nombre con espacio, luego cantidad
|
|
478
|
+
const matchItemSimple = lineaTrim.match(/^(ITEM\s+\d+|[A-Z][^0-9]+?)\s+(\d+)$/i);
|
|
479
|
+
if (matchItemSimple) {
|
|
480
|
+
casoActual.items.push({
|
|
481
|
+
nombre: matchItemSimple[1].trim(),
|
|
482
|
+
cantidad: parseInt(matchItemSimple[2]),
|
|
483
|
+
});
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// ===== SET LIBRO DE COMPRAS PARA EXENTOS =====
|
|
489
|
+
// Formato columnar: TIPO FOLIO [MONTO_EXENTO] [MONTO_AFECTO] (todo en una línea)
|
|
490
|
+
// Observaciones van en línea siguiente con prefijo "OBS:"
|
|
491
|
+
if (enLibroComprasExentos && setActual && !casoActual) {
|
|
492
|
+
if (lineaTrim.includes('TIPO DOCUMENTO') && lineaTrim.includes('FOLIO')) continue;
|
|
493
|
+
if (lineaTrim.includes('MONTO EXENTO') && lineaTrim.includes('MONTO AFECTO')) continue;
|
|
494
|
+
if (lineaTrim.match(/^=+$/)) continue;
|
|
495
|
+
|
|
496
|
+
// OBS: observación para el documento actual
|
|
497
|
+
if (lineaTrim.match(/^OBS:/i)) {
|
|
498
|
+
if (docActual) docActual.observacion = lineaTrim.replace(/^OBS:\s*/i, '').trim();
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Parsear línea columnar: TIPO FOLIO [MONTO_EXENTO] [MONTO_AFECTO]
|
|
503
|
+
const partes = lineaTrim.split(/\t+|\s{2,}/).map(p => p.trim()).filter(p => p);
|
|
504
|
+
if (partes.length >= 2 && /^\d+$/.test(partes[1])) {
|
|
505
|
+
if (docActual) setActual.documentosLibro.push(docActual);
|
|
506
|
+
const tipoDoc = partes[0].toUpperCase();
|
|
507
|
+
docActual = {
|
|
508
|
+
tipoDocumento: tipoDoc,
|
|
509
|
+
folio: parseInt(partes[1]),
|
|
510
|
+
observacion: null,
|
|
511
|
+
montoExento: null,
|
|
512
|
+
montoAfecto: null,
|
|
513
|
+
ivaUsoComun: false,
|
|
514
|
+
codigoIvaNoRec: null,
|
|
515
|
+
};
|
|
516
|
+
if (partes.length >= 4) {
|
|
517
|
+
// Dos montos: exento + afecto
|
|
518
|
+
docActual.montoExento = parseInt(partes[2]) || null;
|
|
519
|
+
docActual.montoAfecto = parseInt(partes[3]) || null;
|
|
520
|
+
} else if (partes.length === 3) {
|
|
521
|
+
// Un monto: determinar columna por tipo de documento
|
|
522
|
+
const esExento = /EXENTA|EXENTO|DEBITO/i.test(tipoDoc);
|
|
523
|
+
if (esExento) docActual.montoExento = parseInt(partes[2]) || null;
|
|
524
|
+
else docActual.montoAfecto = parseInt(partes[2]) || null;
|
|
525
|
+
}
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// ===== SET LIBRO DE COMPRAS =====
|
|
532
|
+
if (enLibroCompras && setActual && !casoActual) {
|
|
533
|
+
// Detectar cabecera de tabla
|
|
534
|
+
if (lineaTrim.includes('TIPO DOCUMENTO') && lineaTrim.includes('FOLIO')) {
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
if (lineaTrim.includes('MONTO EXENTO') && lineaTrim.includes('MONTO AFECTO')) {
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Detectar documento con folio
|
|
542
|
+
// Formato: "FACTURA 234" o "FACTURA ELECTRONICA 32"
|
|
543
|
+
const matchDocFolio = lineaTrim.match(/^(FACTURA DE COMPRA ELECTRONICA|FACTURA ELECTRONICA|FACTURA|NOTA DE CREDITO)\s+(\d+)\s*$/i);
|
|
544
|
+
if (matchDocFolio) {
|
|
545
|
+
// Guardar documento anterior si existe
|
|
546
|
+
if (docActual) {
|
|
547
|
+
setActual.documentosLibro.push(docActual);
|
|
548
|
+
}
|
|
549
|
+
docActual = {
|
|
550
|
+
tipoDocumento: matchDocFolio[1].trim().toUpperCase(),
|
|
551
|
+
folio: parseInt(matchDocFolio[2]),
|
|
552
|
+
observacion: null,
|
|
553
|
+
montoExento: null,
|
|
554
|
+
montoAfecto: null,
|
|
555
|
+
codigoIvaNoRec: null,
|
|
556
|
+
ivaUsoComun: false,
|
|
557
|
+
};
|
|
558
|
+
esperandoMontos = false;
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Detectar observación del documento
|
|
563
|
+
if (docActual && !esperandoMontos) {
|
|
564
|
+
// Líneas de observación conocidas
|
|
565
|
+
if (lineaTrim.match(/^(FACTURA DEL GIRO|FACTURA CON IVA|NOTA DE CREDITO POR|ENTREGA GRATUITA|COMPRA CON RETENCION)/i)) {
|
|
566
|
+
docActual.observacion = lineaTrim;
|
|
567
|
+
// Detectar casos especiales
|
|
568
|
+
if (lineaTrim.includes('IVA USO COMUN')) {
|
|
569
|
+
docActual.ivaUsoComun = true;
|
|
570
|
+
}
|
|
571
|
+
if (lineaTrim.includes('ENTREGA GRATUITA')) {
|
|
572
|
+
docActual.codigoIvaNoRec = 4; // IVA no recuperable - entrega gratuita
|
|
573
|
+
}
|
|
574
|
+
if (lineaTrim.includes('RETENCION TOTAL')) {
|
|
575
|
+
docActual.retencionTotal = true;
|
|
576
|
+
}
|
|
577
|
+
esperandoMontos = true;
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Detectar montos (puede ser una o dos columnas)
|
|
583
|
+
if (docActual && esperandoMontos) {
|
|
584
|
+
// Formato: " 7893 3897" (exento afecto) o " 4305" (solo afecto)
|
|
585
|
+
const matchDosMonto = lineaTrim.match(/^\s*(\d+)\s+(\d+)\s*$/);
|
|
586
|
+
if (matchDosMonto) {
|
|
587
|
+
docActual.montoExento = parseInt(matchDosMonto[1]);
|
|
588
|
+
docActual.montoAfecto = parseInt(matchDosMonto[2]);
|
|
589
|
+
esperandoMontos = false;
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Solo monto afecto (con espacios iniciales)
|
|
594
|
+
const matchUnMonto = lineaTrim.match(/^\s*(\d+)\s*$/);
|
|
595
|
+
if (matchUnMonto) {
|
|
596
|
+
docActual.montoAfecto = parseInt(matchUnMonto[1]);
|
|
597
|
+
esperandoMontos = false;
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Detectar factor de proporcionalidad
|
|
603
|
+
const matchFactor = lineaTrim.match(/FACTOR\s+DE\s+PROPORCIONALIDAD.+?(\d+\.?\d*)/i);
|
|
604
|
+
if (matchFactor) {
|
|
605
|
+
setActual.factorProporcionalidad = parseFloat(matchFactor[1]);
|
|
606
|
+
setActual.observaciones = `Factor proporcionalidad IVA: ${matchFactor[1]}`;
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// Línea separadora de sets
|
|
612
|
+
if (lineaTrim.match(/^-{20,}$/)) {
|
|
613
|
+
// Guardar caso actual si existe
|
|
614
|
+
if (casoActual && setActual) {
|
|
615
|
+
setActual.casos.push(casoActual);
|
|
616
|
+
casoActual = null;
|
|
617
|
+
}
|
|
618
|
+
// Guardar documento actual si existe
|
|
619
|
+
if (docActual && setActual) {
|
|
620
|
+
setActual.documentosLibro.push(docActual);
|
|
621
|
+
docActual = null;
|
|
622
|
+
}
|
|
623
|
+
enLibroCompras = false;
|
|
624
|
+
enLibroComprasExentos = false;
|
|
625
|
+
esperandoMontos = false;
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// Guardar últimos elementos
|
|
631
|
+
if (casoActual && setActual) {
|
|
632
|
+
setActual.casos.push(casoActual);
|
|
633
|
+
}
|
|
634
|
+
if (docActual && setActual) {
|
|
635
|
+
setActual.documentosLibro.push(docActual);
|
|
636
|
+
}
|
|
637
|
+
if (setActual) {
|
|
638
|
+
resultado.sets.push(setActual);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// Contar total de casos
|
|
642
|
+
for (const set of resultado.sets) {
|
|
643
|
+
resultado.totalCasos += set.casos.length;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
return resultado;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// ═══════════════════════════════════════════════════════════════
|
|
650
|
+
// GENERADORES DE ESTRUCTURAS
|
|
651
|
+
// ═══════════════════════════════════════════════════════════════
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* Genera estructura para SET BASICO (Facturas 33, NC 61, ND 56)
|
|
655
|
+
*/
|
|
656
|
+
function generarEstructuraSetBasico(set) {
|
|
657
|
+
const casosFactura = [];
|
|
658
|
+
const casosNC = [];
|
|
659
|
+
const casosND = [];
|
|
660
|
+
|
|
661
|
+
for (const caso of set.casos) {
|
|
662
|
+
const tipoCodigo = caso.tipoDTE?.codigo;
|
|
663
|
+
|
|
664
|
+
if (tipoCodigo === 33) {
|
|
665
|
+
casosFactura.push({
|
|
666
|
+
id: caso.id,
|
|
667
|
+
items: caso.items.map(item => ({
|
|
668
|
+
nombre: item.nombre,
|
|
669
|
+
cantidad: item.cantidad,
|
|
670
|
+
precio: item.precioUnitario,
|
|
671
|
+
...(item.descuento ? { descuentoPct: parseInt(item.descuento) } : {}),
|
|
672
|
+
...(item.nombre.includes('EXENTO') || item.nombre.includes('SERVICIO EXENTO') ? { exento: true } : {}),
|
|
673
|
+
})),
|
|
674
|
+
...(caso.descuentoGlobal ? { descuentoGlobalPct: caso.descuentoGlobal } : {}),
|
|
675
|
+
});
|
|
676
|
+
} else if (tipoCodigo === 61) {
|
|
677
|
+
casosNC.push({
|
|
678
|
+
id: caso.id,
|
|
679
|
+
tipoDte: 61,
|
|
680
|
+
referenciaCaso: caso.casoReferenciado,
|
|
681
|
+
codRef: determinarCodRef(caso.razonReferencia),
|
|
682
|
+
razonRef: caso.razonReferencia,
|
|
683
|
+
items: caso.items.length > 0 ? caso.items.map(item => ({
|
|
684
|
+
nombre: item.nombre,
|
|
685
|
+
cantidad: item.cantidad,
|
|
686
|
+
precio: item.precioUnitario || 0,
|
|
687
|
+
})) : [{ nombre: caso.razonReferencia, cantidad: 1, precio: 0 }],
|
|
688
|
+
...(caso.razonReferencia?.includes('ANULA') ? { itemsFromCaso: caso.casoReferenciado } : {}),
|
|
689
|
+
});
|
|
690
|
+
} else if (tipoCodigo === 56) {
|
|
691
|
+
casosND.push({
|
|
692
|
+
id: caso.id,
|
|
693
|
+
tipoDte: 56,
|
|
694
|
+
referenciaCaso: caso.casoReferenciado,
|
|
695
|
+
codRef: determinarCodRef(caso.razonReferencia),
|
|
696
|
+
razonRef: caso.razonReferencia,
|
|
697
|
+
items: caso.items.length > 0 ? caso.items.map(item => ({
|
|
698
|
+
nombre: item.nombre,
|
|
699
|
+
cantidad: item.cantidad,
|
|
700
|
+
precio: item.precioUnitario || 0,
|
|
701
|
+
})) : [{ nombre: caso.razonReferencia, cantidad: 1, precio: 0 }],
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
return {
|
|
707
|
+
numeroAtencion: set.numeroAtencion,
|
|
708
|
+
casosFactura,
|
|
709
|
+
casosNC,
|
|
710
|
+
casosND,
|
|
711
|
+
cafRequired: {
|
|
712
|
+
33: casosFactura.length,
|
|
713
|
+
61: casosNC.length,
|
|
714
|
+
56: casosND.length,
|
|
715
|
+
},
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* Genera estructura para SET FACTURA EXENTA (34, NC 61, ND 56)
|
|
721
|
+
*/
|
|
722
|
+
function generarEstructuraSetExenta(set) {
|
|
723
|
+
const casosFactura = [];
|
|
724
|
+
const casosNC = [];
|
|
725
|
+
const casosND = [];
|
|
726
|
+
|
|
727
|
+
for (const caso of set.casos) {
|
|
728
|
+
const tipoCodigo = caso.tipoDTE?.codigo;
|
|
729
|
+
|
|
730
|
+
if (tipoCodigo === 34) {
|
|
731
|
+
casosFactura.push({
|
|
732
|
+
id: caso.id,
|
|
733
|
+
items: caso.items.map(item => ({
|
|
734
|
+
nombre: item.nombre,
|
|
735
|
+
cantidad: item.cantidad,
|
|
736
|
+
unidad: item.unidadMedida || 'UN',
|
|
737
|
+
precio: item.precioUnitario,
|
|
738
|
+
exento: true,
|
|
739
|
+
})),
|
|
740
|
+
});
|
|
741
|
+
} else if (tipoCodigo === 61) {
|
|
742
|
+
casosNC.push({
|
|
743
|
+
id: caso.id,
|
|
744
|
+
referenciaCaso: caso.casoReferenciado,
|
|
745
|
+
codRef: determinarCodRef(caso.razonReferencia),
|
|
746
|
+
razonRef: caso.razonReferencia,
|
|
747
|
+
items: caso.items.map(item => ({
|
|
748
|
+
nombre: item.nombre,
|
|
749
|
+
cantidad: item.cantidad || 1,
|
|
750
|
+
precio: item.precioUnitario || 0,
|
|
751
|
+
exento: true,
|
|
752
|
+
})),
|
|
753
|
+
});
|
|
754
|
+
} else if (tipoCodigo === 56) {
|
|
755
|
+
casosND.push({
|
|
756
|
+
id: caso.id,
|
|
757
|
+
referenciaCaso: caso.casoReferenciado,
|
|
758
|
+
codRef: determinarCodRef(caso.razonReferencia),
|
|
759
|
+
razonRef: caso.razonReferencia,
|
|
760
|
+
items: caso.items.map(item => ({
|
|
761
|
+
nombre: item.nombre,
|
|
762
|
+
cantidad: item.cantidad || 1,
|
|
763
|
+
precio: item.precioUnitario || 0,
|
|
764
|
+
exento: true,
|
|
765
|
+
})),
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
return {
|
|
771
|
+
numeroAtencion: set.numeroAtencion,
|
|
772
|
+
casosFactura,
|
|
773
|
+
casosNC,
|
|
774
|
+
casosND,
|
|
775
|
+
cafRequired: {
|
|
776
|
+
34: casosFactura.length,
|
|
777
|
+
61: casosNC.length,
|
|
778
|
+
56: casosND.length,
|
|
779
|
+
},
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* Genera estructura para SET GUIA DE DESPACHO (52)
|
|
785
|
+
*/
|
|
786
|
+
function generarEstructuraSetGuia(set) {
|
|
787
|
+
const casos = [];
|
|
788
|
+
|
|
789
|
+
for (const caso of set.casos) {
|
|
790
|
+
const indTraslado = determinarIndTraslado(caso.motivo);
|
|
791
|
+
const tpoDespacho = determinarTpoDespacho(caso.trasladoPor);
|
|
792
|
+
|
|
793
|
+
casos.push({
|
|
794
|
+
id: caso.id,
|
|
795
|
+
indTraslado,
|
|
796
|
+
...(tpoDespacho ? { tpoDespacho } : {}),
|
|
797
|
+
items: caso.items.map(item => ({
|
|
798
|
+
nombre: item.nombre,
|
|
799
|
+
cantidad: item.cantidad,
|
|
800
|
+
...(item.precioUnitario ? { precio: item.precioUnitario } : { monto: 0 }),
|
|
801
|
+
})),
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
return {
|
|
806
|
+
numeroAtencion: set.numeroAtencion,
|
|
807
|
+
casos,
|
|
808
|
+
cafRequired: { 52: casos.length },
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
/**
|
|
813
|
+
* Genera estructura para Libro de Guías a partir del SET GUIA DE DESPACHO
|
|
814
|
+
*/
|
|
815
|
+
function generarEstructuraLibroGuiasDesdeSetGuia(setGuia, receptorConfig = {}) {
|
|
816
|
+
const casos = [];
|
|
817
|
+
const receptorBase = receptorConfig.rut && receptorConfig.razon_social
|
|
818
|
+
? { rut: receptorConfig.rut, razon: receptorConfig.razon_social }
|
|
819
|
+
: null;
|
|
820
|
+
|
|
821
|
+
(setGuia?.casos || []).forEach((caso) => {
|
|
822
|
+
const items = (caso.items || []).map((item) => ({
|
|
823
|
+
nombre: item.nombre,
|
|
824
|
+
cantidad: item.cantidad || 1,
|
|
825
|
+
precio: item.precio ?? 0,
|
|
826
|
+
}));
|
|
827
|
+
|
|
828
|
+
const totalFromPrecio = (caso.items || []).reduce((acc, item) => (
|
|
829
|
+
acc + ((item.precio || 0) * (item.cantidad || 1))
|
|
830
|
+
), 0);
|
|
831
|
+
const totalFromMonto = (caso.items || []).reduce((acc, item) => (
|
|
832
|
+
acc + (item.monto || 0)
|
|
833
|
+
), 0);
|
|
834
|
+
const total = totalFromPrecio + totalFromMonto;
|
|
835
|
+
const hasMontoOnly = (caso.items || []).some((item) => (
|
|
836
|
+
item.monto !== undefined && item.precio == null
|
|
837
|
+
));
|
|
838
|
+
|
|
839
|
+
const tpoOper = caso.indTraslado === 1
|
|
840
|
+
? 1
|
|
841
|
+
: (Number.isFinite(Number(caso.indTraslado)) ? Number(caso.indTraslado) : 2);
|
|
842
|
+
const entry = {
|
|
843
|
+
tpoOper,
|
|
844
|
+
items,
|
|
845
|
+
};
|
|
846
|
+
|
|
847
|
+
if (hasMontoOnly) {
|
|
848
|
+
entry.mntTotalOverride = total;
|
|
849
|
+
}
|
|
850
|
+
if (tpoOper === 1 && receptorBase) {
|
|
851
|
+
entry.receptor = receptorBase;
|
|
852
|
+
}
|
|
853
|
+
casos.push(entry);
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
return {
|
|
857
|
+
numeroAtencion: setGuia?.numeroAtencion,
|
|
858
|
+
casos,
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
/**
|
|
863
|
+
* Genera estructura para SET FACTURA DE COMPRA (46, NC 61, ND 56)
|
|
864
|
+
*/
|
|
865
|
+
function generarEstructuraSetFacturaCompra(set) {
|
|
866
|
+
let casoFactura = null;
|
|
867
|
+
let casoNC = null;
|
|
868
|
+
let casoND = null;
|
|
869
|
+
|
|
870
|
+
for (const caso of set.casos) {
|
|
871
|
+
const tipoCodigo = caso.tipoDTE?.codigo;
|
|
872
|
+
|
|
873
|
+
if (tipoCodigo === 46) {
|
|
874
|
+
casoFactura = {
|
|
875
|
+
id: caso.id,
|
|
876
|
+
items: caso.items.map(item => ({
|
|
877
|
+
nombre: item.nombre,
|
|
878
|
+
cantidad: item.cantidad,
|
|
879
|
+
precio: item.precioUnitario,
|
|
880
|
+
})),
|
|
881
|
+
};
|
|
882
|
+
} else if (tipoCodigo === 61) {
|
|
883
|
+
casoNC = {
|
|
884
|
+
id: caso.id,
|
|
885
|
+
referenciaCaso: caso.casoReferenciado,
|
|
886
|
+
codRef: determinarCodRef(caso.razonReferencia),
|
|
887
|
+
razonRef: caso.razonReferencia,
|
|
888
|
+
items: caso.items.map(item => ({
|
|
889
|
+
nombre: item.nombre,
|
|
890
|
+
cantidad: item.cantidad,
|
|
891
|
+
precio: item.precioUnitario,
|
|
892
|
+
})),
|
|
893
|
+
};
|
|
894
|
+
} else if (tipoCodigo === 56) {
|
|
895
|
+
casoND = {
|
|
896
|
+
id: caso.id,
|
|
897
|
+
referenciaCaso: caso.casoReferenciado,
|
|
898
|
+
codRef: determinarCodRef(caso.razonReferencia),
|
|
899
|
+
razonRef: caso.razonReferencia,
|
|
900
|
+
items: casoNC?.items || [],
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
return {
|
|
906
|
+
numeroAtencion: set.numeroAtencion,
|
|
907
|
+
casoFactura,
|
|
908
|
+
casoNC,
|
|
909
|
+
casoND,
|
|
910
|
+
cafRequired: {
|
|
911
|
+
46: casoFactura ? 1 : 0,
|
|
912
|
+
61: casoNC ? 1 : 0,
|
|
913
|
+
56: casoND ? 1 : 0,
|
|
914
|
+
},
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
/**
|
|
919
|
+
* Genera estructura para SET LIQUIDACIONES (43)
|
|
920
|
+
*/
|
|
921
|
+
function generarEstructuraSetLiquidaciones(set) {
|
|
922
|
+
const casos = [];
|
|
923
|
+
|
|
924
|
+
for (const caso of set.casos) {
|
|
925
|
+
casos.push({
|
|
926
|
+
id: caso.id,
|
|
927
|
+
items: caso.items.map(item => ({
|
|
928
|
+
nombre: item.nombre,
|
|
929
|
+
cantidad: item.cantidad,
|
|
930
|
+
totalLinea: item.totalLinea || item.precioUnitario * item.cantidad,
|
|
931
|
+
...(item.nombre.includes('EXENTO') ? { exento: true } : {}),
|
|
932
|
+
})),
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
return {
|
|
937
|
+
numeroAtencion: set.numeroAtencion,
|
|
938
|
+
casos,
|
|
939
|
+
cafRequired: { 43: casos.length },
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
/**
|
|
944
|
+
* Genera estructura para SET EXPORTACION (110, 111, 112)
|
|
945
|
+
*/
|
|
946
|
+
function generarEstructuraSetExportacion(set) {
|
|
947
|
+
const casos = [];
|
|
948
|
+
|
|
949
|
+
for (const caso of set.casos) {
|
|
950
|
+
casos.push({
|
|
951
|
+
id: caso.id,
|
|
952
|
+
tipoDTE: caso.tipoDTE?.codigo,
|
|
953
|
+
items: caso.items.map(item => ({
|
|
954
|
+
nombre: item.nombre,
|
|
955
|
+
cantidad: item.cantidad,
|
|
956
|
+
unidad: item.unidadMedida,
|
|
957
|
+
precio: item.precioUnitario || item.valorLinea,
|
|
958
|
+
})),
|
|
959
|
+
moneda: caso.moneda,
|
|
960
|
+
formaPago: caso.formaPago,
|
|
961
|
+
modalidadVenta: caso.modalidadVenta,
|
|
962
|
+
clausulaVenta: caso.clausulaVenta,
|
|
963
|
+
totalClausula: caso.totalClausula,
|
|
964
|
+
viaTransporte: caso.viaTransporte,
|
|
965
|
+
puertoEmbarque: caso.puertoEmbarque,
|
|
966
|
+
puertoDesembarque: caso.puertoDesembarque,
|
|
967
|
+
tipoBulto: caso.tipoBulto,
|
|
968
|
+
totalBultos: caso.totalBultos,
|
|
969
|
+
flete: caso.flete,
|
|
970
|
+
seguro: caso.seguro,
|
|
971
|
+
paisDestino: caso.paisDestino,
|
|
972
|
+
comisionExtranjero: caso.comisionExtranjero,
|
|
973
|
+
...(caso.casoReferenciado ? {
|
|
974
|
+
referenciaCaso: caso.casoReferenciado,
|
|
975
|
+
razonRef: caso.razonReferencia,
|
|
976
|
+
} : {}),
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
return {
|
|
981
|
+
numeroAtencion: set.numeroAtencion,
|
|
982
|
+
casos,
|
|
983
|
+
cafRequired: {
|
|
984
|
+
110: casos.filter(c => c.tipoDTE === 110).length,
|
|
985
|
+
111: casos.filter(c => c.tipoDTE === 111).length,
|
|
986
|
+
112: casos.filter(c => c.tipoDTE === 112).length,
|
|
987
|
+
},
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
/**
|
|
992
|
+
* Genera estructura para LIBRO DE COMPRAS (IECV)
|
|
993
|
+
*/
|
|
994
|
+
function generarEstructuraLibroCompras(set) {
|
|
995
|
+
const detalle = [];
|
|
996
|
+
const resumen = {};
|
|
997
|
+
|
|
998
|
+
for (const doc of set.documentosLibro) {
|
|
999
|
+
const tipoDoc = mapearTipoDocLibro(doc.tipoDocumento);
|
|
1000
|
+
const tasaIva = 0.19;
|
|
1001
|
+
|
|
1002
|
+
const detalleDoc = {
|
|
1003
|
+
TpoDoc: tipoDoc,
|
|
1004
|
+
NroDoc: doc.folio,
|
|
1005
|
+
TasaImp: tasaIva,
|
|
1006
|
+
FchDoc: new Date().toISOString().split('T')[0], // Se debe ajustar
|
|
1007
|
+
RUTDoc: '17096073-4', // RUT por defecto para certificación
|
|
1008
|
+
RznSoc: 'Razon Social',
|
|
1009
|
+
};
|
|
1010
|
+
|
|
1011
|
+
if (doc.montoExento) detalleDoc.MntExe = doc.montoExento;
|
|
1012
|
+
if (doc.montoAfecto) {
|
|
1013
|
+
detalleDoc.MntNeto = doc.montoAfecto;
|
|
1014
|
+
detalleDoc.MntIVA = Math.round(doc.montoAfecto * tasaIva);
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
if (doc.ivaUsoComun) {
|
|
1018
|
+
detalleDoc.TpoImp = 1;
|
|
1019
|
+
detalleDoc.IVAUsoComun = Math.round((doc.montoAfecto || 0) * tasaIva);
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
if (doc.codigoIvaNoRec) {
|
|
1023
|
+
detalleDoc.IVANoRec = {
|
|
1024
|
+
CodIVANoRec: doc.codigoIvaNoRec,
|
|
1025
|
+
MntIVANoRec: Math.round((doc.montoAfecto || 0) * tasaIva),
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
if (doc.retencionTotal && tipoDoc === 46) {
|
|
1030
|
+
detalleDoc.OtrosImp = {
|
|
1031
|
+
CodImp: 15,
|
|
1032
|
+
TasaImp: tasaIva,
|
|
1033
|
+
MntImp: Math.round((doc.montoAfecto || 0) * tasaIva),
|
|
1034
|
+
};
|
|
1035
|
+
detalleDoc.IVARetTotal = Math.round((doc.montoAfecto || 0) * tasaIva);
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
// Referencia para notas de crédito
|
|
1039
|
+
if (tipoDoc === 60 && doc.observacion) {
|
|
1040
|
+
const matchRef = doc.observacion.match(/FACTURA(?:\s+ELECTRONICA)?\s+(\d+)/i);
|
|
1041
|
+
if (matchRef) {
|
|
1042
|
+
detalleDoc.TpoDocRef = doc.observacion.includes('ELECTRONICA') ? 33 : 30;
|
|
1043
|
+
detalleDoc.FolioDocRef = parseInt(matchRef[1]);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
detalleDoc.MntTotal = (detalleDoc.MntNeto || 0) + (detalleDoc.MntIVA || 0) + (detalleDoc.MntExe || 0);
|
|
1048
|
+
if (doc.retencionTotal && tipoDoc === 46) {
|
|
1049
|
+
detalleDoc.MntTotal = detalleDoc.MntNeto || 0; // Sin IVA pagado
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
detalle.push(detalleDoc);
|
|
1053
|
+
|
|
1054
|
+
// Acumular resumen
|
|
1055
|
+
if (!resumen[tipoDoc]) {
|
|
1056
|
+
resumen[tipoDoc] = {
|
|
1057
|
+
TpoDoc: tipoDoc,
|
|
1058
|
+
TotDoc: 0,
|
|
1059
|
+
TotMntExe: 0,
|
|
1060
|
+
TotMntNeto: 0,
|
|
1061
|
+
TotMntIVA: 0,
|
|
1062
|
+
TotMntTotal: 0,
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1065
|
+
resumen[tipoDoc].TotDoc++;
|
|
1066
|
+
resumen[tipoDoc].TotMntExe += detalleDoc.MntExe || 0;
|
|
1067
|
+
resumen[tipoDoc].TotMntNeto += detalleDoc.MntNeto || 0;
|
|
1068
|
+
resumen[tipoDoc].TotMntIVA += detalleDoc.MntIVA || 0;
|
|
1069
|
+
resumen[tipoDoc].TotMntTotal += detalleDoc.MntTotal || 0;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
return {
|
|
1073
|
+
numeroAtencion: set.numeroAtencion,
|
|
1074
|
+
factorProporcionalidad: set.factorProporcionalidad || 0.6,
|
|
1075
|
+
detalle,
|
|
1076
|
+
resumen: Object.values(resumen),
|
|
1077
|
+
documentosOriginales: set.documentosLibro,
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
// ═══════════════════════════════════════════════════════════════
|
|
1082
|
+
// FUNCIÓN PRINCIPAL
|
|
1083
|
+
// ═══════════════════════════════════════════════════════════════
|
|
1084
|
+
|
|
1085
|
+
/**
|
|
1086
|
+
* Genera estructuras compatibles con los scripts de certificación
|
|
1087
|
+
* a partir de los datos extraídos del set
|
|
1088
|
+
*
|
|
1089
|
+
* @param {Object} datosExtraidos - Resultado de extraerCasosDelSet()
|
|
1090
|
+
* @param {Object} [receptorConfig] - Configuración del receptor para libros
|
|
1091
|
+
* @returns {Object} Estructuras para cada set/libro
|
|
1092
|
+
*/
|
|
1093
|
+
function generarEstructurasParaScripts(datosExtraidos, receptorConfig = {}) {
|
|
1094
|
+
const estructuras = {
|
|
1095
|
+
setBasico: null,
|
|
1096
|
+
setFacturaExenta: null,
|
|
1097
|
+
setGuiaDespacho: null,
|
|
1098
|
+
setFacturaCompra: null,
|
|
1099
|
+
setLiquidaciones: null,
|
|
1100
|
+
setExportacion1: null,
|
|
1101
|
+
setExportacion2: null,
|
|
1102
|
+
libroVentas: null,
|
|
1103
|
+
libroCompras: null,
|
|
1104
|
+
libroComprasExentos: null,
|
|
1105
|
+
libroGuias: null,
|
|
1106
|
+
};
|
|
1107
|
+
|
|
1108
|
+
for (const set of datosExtraidos.sets) {
|
|
1109
|
+
switch (set.tipo) {
|
|
1110
|
+
case 'BASICO':
|
|
1111
|
+
estructuras.setBasico = generarEstructuraSetBasico(set);
|
|
1112
|
+
break;
|
|
1113
|
+
case 'FACTURA_EXENTA':
|
|
1114
|
+
estructuras.setFacturaExenta = generarEstructuraSetExenta(set);
|
|
1115
|
+
break;
|
|
1116
|
+
case 'GUIA_DESPACHO':
|
|
1117
|
+
estructuras.setGuiaDespacho = generarEstructuraSetGuia(set);
|
|
1118
|
+
if (!estructuras.libroGuias) {
|
|
1119
|
+
estructuras.libroGuias = generarEstructuraLibroGuiasDesdeSetGuia(estructuras.setGuiaDespacho, receptorConfig);
|
|
1120
|
+
}
|
|
1121
|
+
break;
|
|
1122
|
+
case 'FACTURA_COMPRA':
|
|
1123
|
+
estructuras.setFacturaCompra = generarEstructuraSetFacturaCompra(set);
|
|
1124
|
+
break;
|
|
1125
|
+
case 'LIQUIDACION':
|
|
1126
|
+
estructuras.setLiquidaciones = generarEstructuraSetLiquidaciones(set);
|
|
1127
|
+
break;
|
|
1128
|
+
case 'EXPORTACION':
|
|
1129
|
+
if (set.nombre.includes('(1)')) {
|
|
1130
|
+
estructuras.setExportacion1 = generarEstructuraSetExportacion(set);
|
|
1131
|
+
} else if (set.nombre.includes('(2)')) {
|
|
1132
|
+
estructuras.setExportacion2 = generarEstructuraSetExportacion(set);
|
|
1133
|
+
}
|
|
1134
|
+
break;
|
|
1135
|
+
case 'LIBRO_COMPRAS':
|
|
1136
|
+
estructuras.libroCompras = generarEstructuraLibroCompras(set);
|
|
1137
|
+
break;
|
|
1138
|
+
case 'LIBRO_COMPRAS_EXENTOS':
|
|
1139
|
+
// Misma estructura que LIBRO_COMPRAS pero marcado como exentos
|
|
1140
|
+
estructuras.libroComprasExentos = generarEstructuraLibroCompras(set);
|
|
1141
|
+
break;
|
|
1142
|
+
case 'LIBRO_VENTAS':
|
|
1143
|
+
// El libro de ventas se genera con los datos del set básico o exento
|
|
1144
|
+
estructuras.libroVentas = { numeroAtencion: set.numeroAtencion, instruccion: 'Usar DTEs del SET BASICO' };
|
|
1145
|
+
break;
|
|
1146
|
+
case 'LIBRO_GUIAS':
|
|
1147
|
+
// El libro de guías se genera con los datos del set guía
|
|
1148
|
+
estructuras.libroGuias = estructuras.libroGuias || { numeroAtencion: set.numeroAtencion, instruccion: 'Usar DTEs del SET GUIA DE DESPACHO' };
|
|
1149
|
+
break;
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
return estructuras;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
// ═══════════════════════════════════════════════════════════════
|
|
1157
|
+
// EXPORTS
|
|
1158
|
+
// ═══════════════════════════════════════════════════════════════
|
|
1159
|
+
|
|
1160
|
+
module.exports = {
|
|
1161
|
+
// Función principal de extracción
|
|
1162
|
+
extraerCasosDelSet,
|
|
1163
|
+
|
|
1164
|
+
// Generador de estructuras
|
|
1165
|
+
generarEstructurasParaScripts,
|
|
1166
|
+
|
|
1167
|
+
// Generadores individuales (para uso avanzado)
|
|
1168
|
+
generarEstructuraSetBasico,
|
|
1169
|
+
generarEstructuraSetExenta,
|
|
1170
|
+
generarEstructuraSetGuia,
|
|
1171
|
+
generarEstructuraSetFacturaCompra,
|
|
1172
|
+
generarEstructuraSetLiquidaciones,
|
|
1173
|
+
generarEstructuraSetExportacion,
|
|
1174
|
+
generarEstructuraLibroCompras,
|
|
1175
|
+
generarEstructuraLibroGuiasDesdeSetGuia,
|
|
1176
|
+
|
|
1177
|
+
// Helpers de detección
|
|
1178
|
+
detectarTipoDTE,
|
|
1179
|
+
detectarTipoSet,
|
|
1180
|
+
determinarCodRef,
|
|
1181
|
+
determinarIndTraslado,
|
|
1182
|
+
determinarTpoDespacho,
|
|
1183
|
+
mapearTipoDocLibro,
|
|
1184
|
+
};
|