@devlas/dte-sii 2.9.7 → 2.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/EnviadorSII.js +100 -54
- package/LICENSE +27 -27
- package/LibroCompraVenta.js +141 -17
- package/LibroGuia.js +36 -25
- package/SiiCertificacion.js +85 -3
- package/SiiPortalAuth.js +85 -19
- package/WsReclamo.js +434 -434
- package/cert/BoletaCert.js +41 -4
- package/cert/CertRunner.js +1123 -1209
- package/cert/LibroCompras.js +3 -2
- package/cert/LibroGuias.js +2 -1
- package/cert/LibroVentas.js +2 -1
- package/cert/MuestrasImpresas.js +831 -131
- package/cert/comunaOficina.js +458 -458
- package/cert/index.js +122 -122
- package/cert/types.js +328 -328
- package/package.json +2 -3
- package/test-muestras.js +180 -0
- package/test-qdetestlibro.js +174 -0
- package/utils/progress.js +4 -0
- package/utils/browser.js +0 -79
package/EnviadorSII.js
CHANGED
|
@@ -12,8 +12,9 @@
|
|
|
12
12
|
* - Consulta de estado de envíos
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
const https
|
|
16
|
-
const
|
|
15
|
+
const https = require('https');
|
|
16
|
+
const crypto = require('crypto');
|
|
17
|
+
const forge = require('node-forge');
|
|
17
18
|
const FormData = require('form-data');
|
|
18
19
|
const {
|
|
19
20
|
saveEnvioArtifacts,
|
|
@@ -84,6 +85,49 @@ class EnviadorSII {
|
|
|
84
85
|
};
|
|
85
86
|
}
|
|
86
87
|
|
|
88
|
+
// ============================================
|
|
89
|
+
// TLS helper — remplaza fetch() para endpoints SII
|
|
90
|
+
// ============================================
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* POST HTTPS a un endpoint SII con opciones TLS compatibles (rejectUnauthorized:false,
|
|
94
|
+
* TLSv1.2 max, legacy renegotiation). Reemplaza fetch() que usa undici y no acepta
|
|
95
|
+
* estas opciones, causando EPROTO/alert 48 "unknown_ca" en maullin/palena.
|
|
96
|
+
* @param {string} urlStr
|
|
97
|
+
* @param {{ headers?: Record<string,string>, body?: Buffer|string, timeoutMs?: number }} opts
|
|
98
|
+
* @returns {Promise<{ ok: boolean, status: number, text: string }>}
|
|
99
|
+
*/
|
|
100
|
+
_siiPost(urlStr, { headers = {}, body = '', timeoutMs = 30000 } = {}) {
|
|
101
|
+
return new Promise((resolve, reject) => {
|
|
102
|
+
const url = new URL(urlStr);
|
|
103
|
+
const buf = Buffer.isBuffer(body) ? body : Buffer.from(body, 'latin1');
|
|
104
|
+
const opts = {
|
|
105
|
+
hostname: url.hostname,
|
|
106
|
+
port: url.port || 443,
|
|
107
|
+
path: url.pathname + url.search,
|
|
108
|
+
method: 'POST',
|
|
109
|
+
headers: { 'Content-Length': buf.length, ...headers },
|
|
110
|
+
rejectUnauthorized: false,
|
|
111
|
+
maxVersion: 'TLSv1.2',
|
|
112
|
+
secureOptions:
|
|
113
|
+
crypto.constants.SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION |
|
|
114
|
+
crypto.constants.SSL_OP_LEGACY_SERVER_CONNECT,
|
|
115
|
+
};
|
|
116
|
+
const req = https.request(opts, (res) => {
|
|
117
|
+
const chunks = [];
|
|
118
|
+
res.on('data', (c) => chunks.push(c));
|
|
119
|
+
res.on('end', () => {
|
|
120
|
+
const text = Buffer.concat(chunks).toString('latin1');
|
|
121
|
+
resolve({ ok: res.statusCode >= 200 && res.statusCode < 300, status: res.statusCode, text });
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
req.on('error', reject);
|
|
125
|
+
req.setTimeout(timeoutMs, () => { req.destroy(); reject(new Error(`_siiPost timeout: ${urlStr}`)); });
|
|
126
|
+
req.write(buf);
|
|
127
|
+
req.end();
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
87
131
|
// ============================================
|
|
88
132
|
// AUTENTICACIÓN REST (Boletas)
|
|
89
133
|
// ============================================
|
|
@@ -198,13 +242,10 @@ class EnviadorSII {
|
|
|
198
242
|
await wait(attempt * 1000);
|
|
199
243
|
}
|
|
200
244
|
|
|
201
|
-
const response = await
|
|
202
|
-
|
|
203
|
-
headers: {
|
|
204
|
-
'Content-Type': 'text/xml; charset=utf-8',
|
|
205
|
-
'SOAPAction': '',
|
|
206
|
-
},
|
|
245
|
+
const response = await this._siiPost(url, {
|
|
246
|
+
headers: { 'Content-Type': 'text/xml; charset=utf-8', 'SOAPAction': '' },
|
|
207
247
|
body: soapEnvelope,
|
|
248
|
+
timeoutMs: getConfigSection('timeout')?.soap || 30000,
|
|
208
249
|
});
|
|
209
250
|
|
|
210
251
|
if (!response.ok) {
|
|
@@ -215,7 +256,7 @@ class EnviadorSII {
|
|
|
215
256
|
throw siiError(`Error obteniendo semilla SOAP: ${response.status}`, ERROR_CODES.SII_CONNECTION_FAILED);
|
|
216
257
|
}
|
|
217
258
|
|
|
218
|
-
const xml =
|
|
259
|
+
const xml = response.text;
|
|
219
260
|
// Usar utilidad centralizada para decodificar entidades
|
|
220
261
|
const decodedXml = decodeXmlEntities(xml);
|
|
221
262
|
|
|
@@ -233,7 +274,7 @@ class EnviadorSII {
|
|
|
233
274
|
throw siiError('No se pudo extraer semilla de la respuesta SOAP', ERROR_CODES.SII_INVALID_RESPONSE);
|
|
234
275
|
} catch (error) {
|
|
235
276
|
if (isRetryableError(error) && attempt < maxRetries) {
|
|
236
|
-
log.log(` [!] Error de conexión semilla SOAP (${error.cause?.code || 'socket'}), reintentando...`);
|
|
277
|
+
log.log(` [!] Error de conexión semilla SOAP (${error.code || error.cause?.code || 'socket'}), reintentando...`);
|
|
237
278
|
continue;
|
|
238
279
|
}
|
|
239
280
|
throw error;
|
|
@@ -282,25 +323,21 @@ class EnviadorSII {
|
|
|
282
323
|
</soapenv:Body>
|
|
283
324
|
</soapenv:Envelope>`;
|
|
284
325
|
|
|
285
|
-
const response = await
|
|
286
|
-
|
|
287
|
-
headers: {
|
|
288
|
-
'Content-Type': 'text/xml; charset=utf-8',
|
|
289
|
-
'SOAPAction': '',
|
|
290
|
-
},
|
|
326
|
+
const response = await this._siiPost(url, {
|
|
327
|
+
headers: { 'Content-Type': 'text/xml; charset=utf-8', 'SOAPAction': '' },
|
|
291
328
|
body: soapEnvelope,
|
|
329
|
+
timeoutMs: getConfigSection('timeout')?.soap || 30000,
|
|
292
330
|
});
|
|
293
331
|
|
|
294
332
|
if (!response.ok) {
|
|
295
|
-
const errorText = await response.text();
|
|
296
333
|
if (isRetryableStatus(response.status) && attempt < maxRetries) {
|
|
297
334
|
log.log(` [!] Error token SOAP (${response.status}), reintentando...`);
|
|
298
335
|
continue;
|
|
299
336
|
}
|
|
300
|
-
throw siiError(`Error obteniendo token SOAP: ${response.status} - ${
|
|
337
|
+
throw siiError(`Error obteniendo token SOAP: ${response.status} - ${response.text}`, ERROR_CODES.SII_CONNECTION_FAILED);
|
|
301
338
|
}
|
|
302
339
|
|
|
303
|
-
const xml =
|
|
340
|
+
const xml = response.text;
|
|
304
341
|
const decodedXml = xml
|
|
305
342
|
.replace(/</g, '<')
|
|
306
343
|
.replace(/>/g, '>')
|
|
@@ -329,7 +366,7 @@ class EnviadorSII {
|
|
|
329
366
|
throw siiError('No se pudo obtener token SOAP del SII', ERROR_CODES.SII_INVALID_RESPONSE);
|
|
330
367
|
} catch (error) {
|
|
331
368
|
if (isRetryableError(error) && attempt < maxRetries) {
|
|
332
|
-
log.log(` [!] Error de conexión token SOAP (${error.cause?.code || 'socket'}), reintentando...`);
|
|
369
|
+
log.log(` [!] Error de conexión token SOAP (${error.code || error.cause?.code || 'socket'}), reintentando...`);
|
|
333
370
|
continue;
|
|
334
371
|
}
|
|
335
372
|
throw error;
|
|
@@ -756,19 +793,14 @@ class EnviadorSII {
|
|
|
756
793
|
log.log('Consultando estado SOAP:', urlQueryEstUp);
|
|
757
794
|
log.log(' TrackID:', trackId, 'RUT:', rutEmisor);
|
|
758
795
|
|
|
759
|
-
const response = await
|
|
760
|
-
|
|
761
|
-
headers: {
|
|
762
|
-
'Content-Type': 'text/xml; charset=utf-8',
|
|
763
|
-
'SOAPAction': '',
|
|
764
|
-
},
|
|
796
|
+
const response = await this._siiPost(urlQueryEstUp, {
|
|
797
|
+
headers: { 'Content-Type': 'text/xml; charset=utf-8', 'SOAPAction': '' },
|
|
765
798
|
body: soapBody,
|
|
799
|
+
timeoutMs: 30000,
|
|
766
800
|
});
|
|
767
|
-
|
|
768
|
-
const text = await response.text();
|
|
769
|
-
|
|
801
|
+
|
|
770
802
|
// Usar decodeXmlEntities centralizado
|
|
771
|
-
const decoded = decodeXmlEntities(text).replace(/
/g, '\n');
|
|
803
|
+
const decoded = decodeXmlEntities(response.text).replace(/
/g, '\n');
|
|
772
804
|
|
|
773
805
|
|
|
774
806
|
|
|
@@ -810,6 +842,10 @@ class EnviadorSII {
|
|
|
810
842
|
mensaje = '[!] Aceptado con Reparos';
|
|
811
843
|
esExitoso = true;
|
|
812
844
|
break;
|
|
845
|
+
case 'LOK':
|
|
846
|
+
mensaje = `[OK] Envio de Libro Aceptado - Cuadrado: ${glosa || ''}`.trim();
|
|
847
|
+
esExitoso = true;
|
|
848
|
+
break;
|
|
813
849
|
case 'REC':
|
|
814
850
|
mensaje = '[...] Envío Recibido - Esperando validación';
|
|
815
851
|
esIntermedio = true;
|
|
@@ -867,6 +903,20 @@ class EnviadorSII {
|
|
|
867
903
|
mensaje = '[...] Envío en Proceso - Validando...';
|
|
868
904
|
esIntermedio = true;
|
|
869
905
|
break;
|
|
906
|
+
case 'LNC':
|
|
907
|
+
mensaje = `[ERR] Tipo de Envio de Libro No Corresponde: ${glosa || 'Período ya tiene LTC'}`;
|
|
908
|
+
esRechazado = true;
|
|
909
|
+
break;
|
|
910
|
+
case 'LRH':
|
|
911
|
+
mensaje = `[ERR] Libro Rechazado con Historial: ${glosa || 'Rechazo con registro'}`;
|
|
912
|
+
esRechazado = true;
|
|
913
|
+
break;
|
|
914
|
+
case 'LSO':
|
|
915
|
+
// Schema de Envio de Libro Correcto: schema validado, SII aún no procesó el contenido.
|
|
916
|
+
// Es un estado INTERMEDIO — el libro no está en LTC todavía.
|
|
917
|
+
mensaje = '[...] Schema de Libro Correcto - Procesando contenido...';
|
|
918
|
+
esIntermedio = true;
|
|
919
|
+
break;
|
|
870
920
|
// Códigos numéricos negativos = errores del servicio de consulta SII (NO rechazo del documento)
|
|
871
921
|
// Según doc SII: son errores del sistema de consulta, el documento puede estar OK
|
|
872
922
|
case '-11':
|
|
@@ -1226,15 +1276,7 @@ class EnviadorSII {
|
|
|
1226
1276
|
const backoffMultiplier = retryConfig?.backoffMultiplier || 1.8;
|
|
1227
1277
|
let lastError = null;
|
|
1228
1278
|
|
|
1229
|
-
// Calcular Content-Length según tipo de body
|
|
1230
|
-
const contentLength = Buffer.isBuffer(body)
|
|
1231
|
-
? body.length.toString()
|
|
1232
|
-
: Buffer.byteLength(body).toString();
|
|
1233
|
-
|
|
1234
1279
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
1235
|
-
const controller = new AbortController();
|
|
1236
|
-
const timeout = setTimeout(() => controller.abort(), 60000);
|
|
1237
|
-
|
|
1238
1280
|
try {
|
|
1239
1281
|
if (attempt > 1) {
|
|
1240
1282
|
const delay = Math.min(initialDelay * Math.pow(backoffMultiplier, attempt - 2), 15000);
|
|
@@ -1242,28 +1284,23 @@ class EnviadorSII {
|
|
|
1242
1284
|
await new Promise(resolve => setTimeout(resolve, delay));
|
|
1243
1285
|
}
|
|
1244
1286
|
|
|
1245
|
-
const response = await
|
|
1246
|
-
method: 'POST',
|
|
1287
|
+
const response = await this._siiPost(url, {
|
|
1247
1288
|
headers: {
|
|
1248
1289
|
'Cookie': `TOKEN=${this.tokenSoap}`,
|
|
1249
1290
|
'Content-Type': `multipart/form-data; boundary=${boundary}`,
|
|
1250
1291
|
'User-Agent': 'Mozilla/4.0 ( compatible; PROG 1.0; Windows NT)',
|
|
1251
1292
|
'Accept': '*/*',
|
|
1252
1293
|
'Connection': 'keep-alive',
|
|
1253
|
-
'Content-Length': contentLength,
|
|
1254
1294
|
},
|
|
1255
1295
|
body: body,
|
|
1256
|
-
|
|
1296
|
+
timeoutMs: 60000,
|
|
1257
1297
|
});
|
|
1258
|
-
clearTimeout(timeout);
|
|
1259
1298
|
|
|
1260
|
-
const
|
|
1261
|
-
|
|
1262
|
-
const result = this._parsearRespuestaSoap(responseText, response.ok, response.status, tipoEnvio);
|
|
1299
|
+
const result = this._parsearRespuestaSoap(response.text, response.ok, response.status, tipoEnvio);
|
|
1263
1300
|
|
|
1264
1301
|
saveEnvioArtifacts({
|
|
1265
1302
|
xml,
|
|
1266
|
-
responseText,
|
|
1303
|
+
responseText: response.text,
|
|
1267
1304
|
responseOk: response.ok,
|
|
1268
1305
|
responseStatus: response.status,
|
|
1269
1306
|
trackId: result.trackId,
|
|
@@ -1273,27 +1310,26 @@ class EnviadorSII {
|
|
|
1273
1310
|
|
|
1274
1311
|
return result;
|
|
1275
1312
|
} catch (fetchError) {
|
|
1276
|
-
clearTimeout(timeout);
|
|
1277
1313
|
lastError = fetchError;
|
|
1278
|
-
|
|
1314
|
+
|
|
1279
1315
|
// Usar isRetryableError centralizado de utils
|
|
1280
1316
|
if (isRetryableError(fetchError) && attempt < maxRetries) {
|
|
1281
|
-
log.log(` [!] Error de conexión ${tipoEnvio} (${fetchError.cause?.code || 'socket'}), reintentando...`);
|
|
1317
|
+
log.log(` [!] Error de conexión ${tipoEnvio} (${fetchError.code || fetchError.cause?.code || 'socket'}), reintentando...`);
|
|
1282
1318
|
continue;
|
|
1283
1319
|
}
|
|
1284
|
-
|
|
1320
|
+
|
|
1285
1321
|
saveEnvioArtifacts({
|
|
1286
1322
|
xml,
|
|
1287
|
-
responseText: `
|
|
1323
|
+
responseText: `ERROR: ${fetchError.message}`,
|
|
1288
1324
|
responseOk: false,
|
|
1289
1325
|
responseStatus: null,
|
|
1290
1326
|
trackId: null,
|
|
1291
1327
|
ambiente: this.ambiente,
|
|
1292
1328
|
tipoEnvio,
|
|
1293
|
-
error: fetchError
|
|
1329
|
+
error: fetchError,
|
|
1294
1330
|
});
|
|
1295
1331
|
|
|
1296
|
-
log.error('
|
|
1332
|
+
log.error('Error en envío SII:', fetchError.message);
|
|
1297
1333
|
throw fetchError;
|
|
1298
1334
|
}
|
|
1299
1335
|
}
|
|
@@ -1373,6 +1409,16 @@ class EnviadorSII {
|
|
|
1373
1409
|
};
|
|
1374
1410
|
}
|
|
1375
1411
|
|
|
1412
|
+
// cvc-*: error de validación XSD (p.ej. elemento fuera de orden, tipo incorrecto)
|
|
1413
|
+
// El SII puede retornar status 7 para errores XSD sin prefijo SCH-00001
|
|
1414
|
+
if (detailMsg.includes('cvc-')) {
|
|
1415
|
+
return {
|
|
1416
|
+
ok: false, status,
|
|
1417
|
+
error: `XML rechazado por SII: error de validación XSD. Detail: ${detailMsg}`,
|
|
1418
|
+
respuesta: responseText, duplicado: false,
|
|
1419
|
+
};
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1376
1422
|
// Duplicado real: SII ya recibió este SetDTE ID
|
|
1377
1423
|
if (trackId) {
|
|
1378
1424
|
return { ok: true, status, trackId, archivo: fileName, mensaje: `[DUPLICADO] TrackID recuperado: ${trackId}`, respuesta: responseText };
|
package/LICENSE
CHANGED
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 Devlas SpA — https://devlas.cl
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
22
|
-
|
|
23
|
-
---
|
|
24
|
-
|
|
25
|
-
Esta librería implementa el protocolo XML del Servicio de Impuestos Internos (SII)
|
|
26
|
-
de Chile tal como está documentado públicamente por el propio SII.
|
|
27
|
-
Inspirada conceptualmente en LibreDTE de SASCO SpA (https://libredte.cl).
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Devlas SpA — https://devlas.cl
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
Esta librería implementa el protocolo XML del Servicio de Impuestos Internos (SII)
|
|
26
|
+
de Chile tal como está documentado públicamente por el propio SII.
|
|
27
|
+
Inspirada conceptualmente en LibreDTE de SASCO SpA (https://libredte.cl).
|
package/LibroCompraVenta.js
CHANGED
|
@@ -13,6 +13,16 @@ const LibroBase = require('./LibroBase');
|
|
|
13
13
|
class LibroCompraVenta extends LibroBase {
|
|
14
14
|
constructor(certificado) {
|
|
15
15
|
super(certificado);
|
|
16
|
+
this.ltcTotales = null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Establece los totales del envío LTC (MENSUAL/TOTAL) previo para este período.
|
|
21
|
+
* Cuando se genera un AJUSTE, TotalesPeriodo = LTC + delta actual.
|
|
22
|
+
* @param {Array} totalesLtc - Array de resumen del libro TOTAL (misma estructura que setResumen)
|
|
23
|
+
*/
|
|
24
|
+
setLtcTotales(totalesLtc) {
|
|
25
|
+
this.ltcTotales = totalesLtc || null;
|
|
16
26
|
}
|
|
17
27
|
|
|
18
28
|
setCaratula(caratula) {
|
|
@@ -24,10 +34,11 @@ class LibroCompraVenta extends LibroBase {
|
|
|
24
34
|
if (!this.caratula) {
|
|
25
35
|
throw new Error('Debe establecer la carátula antes de generar');
|
|
26
36
|
}
|
|
27
|
-
|
|
37
|
+
|
|
38
|
+
const tipoEnvio = String(this.caratula?.TipoEnvio || '').toUpperCase();
|
|
28
39
|
const caratulaXml = this._renderCaratula();
|
|
29
40
|
const resumenXml = this._renderResumen();
|
|
30
|
-
const detalleXml = this._renderDetalle();
|
|
41
|
+
const detalleXml = this._renderDetalle(tipoEnvio === 'AJUSTE');
|
|
31
42
|
const envioLibroId = this.id;
|
|
32
43
|
const tmstFirma = this._getTmstFirma();
|
|
33
44
|
|
|
@@ -64,13 +75,53 @@ class LibroCompraVenta extends LibroBase {
|
|
|
64
75
|
|
|
65
76
|
_renderResumen() {
|
|
66
77
|
if (!this.resumen.length) return '';
|
|
67
|
-
|
|
78
|
+
|
|
68
79
|
const tipoEnvio = String(this.caratula?.TipoEnvio || '').toUpperCase();
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
80
|
+
|
|
81
|
+
if (tipoEnvio === 'AJUSTE') {
|
|
82
|
+
// AJUSTE: ResumenSegmento (delta actual) + ResumenPeriodo (LTC acumulado + delta)
|
|
83
|
+
const segmentoXml = this._renderResumenSection('ResumenSegmento', 'TotalesSegmento');
|
|
84
|
+
if (this.ltcTotales && this.ltcTotales.length > 0) {
|
|
85
|
+
// TotalesPeriodo = totales LTC previos + delta de este AJUSTE
|
|
86
|
+
const acumulado = this._acumularResumen(this.ltcTotales, this.resumen);
|
|
87
|
+
const savedResumen = this.resumen;
|
|
88
|
+
this.resumen = acumulado;
|
|
89
|
+
const periodoXml = this._renderResumenSection('ResumenPeriodo', 'TotalesPeriodo');
|
|
90
|
+
this.resumen = savedResumen;
|
|
91
|
+
return segmentoXml + periodoXml;
|
|
92
|
+
}
|
|
93
|
+
// Sin LTC guardado: ambas secciones con los mismos datos (ajuste inicial)
|
|
94
|
+
return segmentoXml +
|
|
95
|
+
this._renderResumenSection('ResumenPeriodo', 'TotalesPeriodo');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const useSegmento = tipoEnvio === 'PARCIAL';
|
|
99
|
+
return this._renderResumenSection(
|
|
100
|
+
useSegmento ? 'ResumenSegmento' : 'ResumenPeriodo',
|
|
101
|
+
useSegmento ? 'TotalesSegmento' : 'TotalesPeriodo'
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
_renderResumenSection(resumenTag, totalesTag) {
|
|
106
|
+
// TotalesPeriodo y TotalesSegmento tienen ordenamientos XSD distintos para FctProp:
|
|
107
|
+
// Periodo: FctProp viene al final, después de TotImpVehiculo
|
|
108
|
+
// Segmento: FctProp viene antes de TotMntTotal, después de TotIVAUsoComun
|
|
109
|
+
// y no incluye TotImpVehiculo en la secuencia post-TotMntTotal
|
|
110
|
+
const isSegmento = totalesTag === 'TotalesSegmento';
|
|
111
|
+
// TotalesSegmento (AJUSTE/PARCIAL): FctProp/TotCredIVAUsoComun no existen en ese tipo XSD.
|
|
112
|
+
// TotalesPeriodo: orden original validado por el SII — FctProp inmediatamente después de TotIVAUsoComun.
|
|
113
|
+
const order = isSegmento ? [
|
|
114
|
+
'TpoDoc', 'TpoImp', 'TotDoc', 'TotAnulado', 'TotOpExe', 'TotMntExe',
|
|
115
|
+
'TotMntNeto', 'TotOpIVARec', 'TotMntIVA', 'TotOpActivoFijo',
|
|
116
|
+
'TotMntActivoFijo', 'TotMntIVAActivoFijo', 'TotIVANoRec',
|
|
117
|
+
'TotOpIVAUsoComun', 'TotIVAUsoComun',
|
|
118
|
+
'TotIVAFueraPlazo', 'TotIVAPropio', 'TotIVATerceros', 'TotLey18211',
|
|
119
|
+
'TotOtrosImp', 'TotImpSinCredito', 'TotOpIVARetTotal', 'TotIVARetTotal',
|
|
120
|
+
'TotOpIVARetParcial', 'TotIVARetParcial', 'TotCredEC', 'TotDepEnvase',
|
|
121
|
+
'TotLiquidaciones', 'TotMntTotal', 'TotOpIVANoRetenido', 'TotIVANoRetenido',
|
|
122
|
+
'TotMntNoFact', 'TotMntPeriodo', 'TotPsjNac', 'TotPsjInt', 'TotTabPuros',
|
|
123
|
+
'TotTabCigarrillos', 'TotTabElaborado',
|
|
124
|
+
] : [
|
|
74
125
|
'TpoDoc', 'TpoImp', 'TotDoc', 'TotAnulado', 'TotOpExe', 'TotMntExe',
|
|
75
126
|
'TotMntNeto', 'TotOpIVARec', 'TotMntIVA', 'TotOpActivoFijo',
|
|
76
127
|
'TotMntActivoFijo', 'TotMntIVAActivoFijo', 'TotIVANoRec',
|
|
@@ -82,7 +133,7 @@ class LibroCompraVenta extends LibroBase {
|
|
|
82
133
|
'TotMntNoFact', 'TotMntPeriodo', 'TotPsjNac', 'TotPsjInt', 'TotTabPuros',
|
|
83
134
|
'TotTabCigarrillos', 'TotTabElaborado', 'TotImpVehiculo',
|
|
84
135
|
];
|
|
85
|
-
|
|
136
|
+
|
|
86
137
|
let xml = `<${resumenTag}>`;
|
|
87
138
|
for (const r of this.resumen) {
|
|
88
139
|
const normalized = { ...r };
|
|
@@ -94,12 +145,12 @@ class LibroCompraVenta extends LibroBase {
|
|
|
94
145
|
) {
|
|
95
146
|
normalized.TotMntExe = 0;
|
|
96
147
|
}
|
|
97
|
-
|
|
148
|
+
|
|
98
149
|
xml += `<${totalesTag}>`;
|
|
99
150
|
order.forEach((key) => {
|
|
100
151
|
const value = normalized[key];
|
|
101
152
|
if (value === undefined || value === null || value === '') return;
|
|
102
|
-
|
|
153
|
+
|
|
103
154
|
if (Array.isArray(value)) {
|
|
104
155
|
value.forEach((item) => {
|
|
105
156
|
if (item && typeof item === 'object') {
|
|
@@ -132,9 +183,77 @@ class LibroCompraVenta extends LibroBase {
|
|
|
132
183
|
return xml;
|
|
133
184
|
}
|
|
134
185
|
|
|
135
|
-
|
|
186
|
+
/**
|
|
187
|
+
* Acumula dos arrays de resumen (base LTC + delta AJUSTE) sumando campos numéricos por TpoDoc.
|
|
188
|
+
* @private
|
|
189
|
+
*/
|
|
190
|
+
_acumularResumen(base, delta) {
|
|
191
|
+
const map = new Map();
|
|
192
|
+
for (const item of base) {
|
|
193
|
+
map.set(item.TpoDoc, { ...item });
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Key field per array element type — used to match base vs delta items
|
|
197
|
+
const ARRAY_KEY_FIELDS = {
|
|
198
|
+
TotIVANoRec: 'CodIVANoRec',
|
|
199
|
+
TotOtrosImp: 'CodImp',
|
|
200
|
+
};
|
|
201
|
+
// Rate/factor fields — keep existing value (don't sum), set from delta if absent
|
|
202
|
+
const RATE_KEYS = new Set(['TpoDoc', 'TpoImp', 'FctProp']);
|
|
203
|
+
|
|
204
|
+
// Merge two arrays of sub-objects: sum all numeric fields except the key field
|
|
205
|
+
const _mergeArrayField = (baseArr, deltaArr, keyField) => {
|
|
206
|
+
const m = new Map();
|
|
207
|
+
for (const item of (baseArr || [])) m.set(item[keyField], { ...item });
|
|
208
|
+
for (const item of (deltaArr || [])) {
|
|
209
|
+
if (m.has(item[keyField])) {
|
|
210
|
+
const acc = m.get(item[keyField]);
|
|
211
|
+
for (const [k, v] of Object.entries(item)) {
|
|
212
|
+
if (k === keyField) continue;
|
|
213
|
+
const n = Number(v);
|
|
214
|
+
if (!Number.isNaN(n) && v !== '' && v !== null && v !== undefined) {
|
|
215
|
+
acc[k] = (Number(acc[k] || 0)) + n;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
} else {
|
|
219
|
+
m.set(item[keyField], { ...item });
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return Array.from(m.values());
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
for (const item of delta) {
|
|
226
|
+
if (map.has(item.TpoDoc)) {
|
|
227
|
+
const acc = map.get(item.TpoDoc);
|
|
228
|
+
for (const [key, val] of Object.entries(item)) {
|
|
229
|
+
if (RATE_KEYS.has(key)) {
|
|
230
|
+
// Keep existing value; propagate from delta only if absent in base
|
|
231
|
+
if (acc[key] === undefined || acc[key] === null) acc[key] = val;
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
if (Array.isArray(val)) {
|
|
235
|
+
const keyField = ARRAY_KEY_FIELDS[key];
|
|
236
|
+
if (keyField) {
|
|
237
|
+
acc[key] = _mergeArrayField(acc[key], val, keyField);
|
|
238
|
+
}
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
const numVal = Number(val);
|
|
242
|
+
if (!Number.isNaN(numVal) && val !== '' && val !== null && val !== undefined) {
|
|
243
|
+
acc[key] = (Number(acc[key] || 0)) + numVal;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
} else {
|
|
247
|
+
map.set(item.TpoDoc, { ...item });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return Array.from(map.values());
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// isAjuste: cuando true, agrega Operacion=1 (agrega) a cada ítem que no la tenga
|
|
254
|
+
_renderDetalle(isAjuste = false) {
|
|
136
255
|
if (!this.detalle.length) return '';
|
|
137
|
-
|
|
256
|
+
|
|
138
257
|
const order = [
|
|
139
258
|
'TpoDoc', 'Emisor', 'IndFactCompra', 'NroDoc', 'Anulado', 'Operacion',
|
|
140
259
|
'TpoImp', 'TasaImp', 'NumInt', 'IndServicio', 'IndSinCosto', 'FchDoc',
|
|
@@ -146,11 +265,16 @@ class LibroCompraVenta extends LibroBase {
|
|
|
146
265
|
'MntNoFact', 'MntPeriodo', 'PsjNac', 'PsjInt', 'TabPuros', 'TabCigarrillos',
|
|
147
266
|
'TabElaborado', 'ImpVehiculo',
|
|
148
267
|
];
|
|
149
|
-
|
|
268
|
+
|
|
150
269
|
let xml = '';
|
|
151
270
|
for (const d of this.detalle) {
|
|
152
271
|
const normalized = { ...d };
|
|
153
|
-
|
|
272
|
+
|
|
273
|
+
// AJUSTE: Operacion=1 (agrega) por defecto si no está explícito
|
|
274
|
+
if (isAjuste && (normalized.Operacion === undefined || normalized.Operacion === null || normalized.Operacion === '')) {
|
|
275
|
+
normalized.Operacion = 1;
|
|
276
|
+
}
|
|
277
|
+
|
|
154
278
|
// Calcular TasaImp si no existe
|
|
155
279
|
if (
|
|
156
280
|
(normalized.TasaImp === undefined || normalized.TasaImp === null || normalized.TasaImp === '') &&
|
|
@@ -164,12 +288,12 @@ class LibroCompraVenta extends LibroBase {
|
|
|
164
288
|
normalized.TasaImp = tasa.toFixed(2).replace(/\.00$/, '.0');
|
|
165
289
|
}
|
|
166
290
|
}
|
|
167
|
-
|
|
291
|
+
|
|
168
292
|
xml += '<Detalle>';
|
|
169
293
|
order.forEach((key) => {
|
|
170
294
|
const value = normalized[key];
|
|
171
295
|
if (value === undefined || value === null || value === '') return;
|
|
172
|
-
|
|
296
|
+
|
|
173
297
|
if (Array.isArray(value)) {
|
|
174
298
|
value.forEach((item) => {
|
|
175
299
|
if (item && typeof item === 'object') {
|
package/LibroGuia.js
CHANGED
|
@@ -40,8 +40,10 @@ class LibroGuia extends LibroBase {
|
|
|
40
40
|
throw new Error('Debe establecer la carátula antes de generar');
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
const tipoEnvio = String(this.caratula?.TipoEnvio || '').toUpperCase();
|
|
43
44
|
const caratulaXml = this._renderCaratula();
|
|
44
45
|
const resumenXml = this._renderResumen();
|
|
46
|
+
// AJUSTE con documentos: Detalle + ResumenSegmento + ResumenPeriodo
|
|
45
47
|
const detalleXml = this._renderDetalle();
|
|
46
48
|
|
|
47
49
|
const schemaLoc = 'http://www.sii.cl/SiiDte LibroGuia_v10.xsd';
|
|
@@ -87,7 +89,9 @@ class LibroGuia extends LibroBase {
|
|
|
87
89
|
_renderResumen() {
|
|
88
90
|
const resumen = this._buildResumenPeriodo();
|
|
89
91
|
if (!resumen) return '';
|
|
90
|
-
|
|
92
|
+
|
|
93
|
+
const tipoEnvio = String(this.caratula?.TipoEnvio || '').toUpperCase();
|
|
94
|
+
|
|
91
95
|
const order = [
|
|
92
96
|
'TotFolAnulado',
|
|
93
97
|
'TotGuiaAnulada',
|
|
@@ -95,30 +99,37 @@ class LibroGuia extends LibroBase {
|
|
|
95
99
|
'TotMntGuiaVta',
|
|
96
100
|
'TotTraslado',
|
|
97
101
|
];
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
102
|
+
|
|
103
|
+
const _renderSection = (tag) => {
|
|
104
|
+
let xml = `<${tag}>`;
|
|
105
|
+
order.forEach((key) => {
|
|
106
|
+
const value = resumen[key];
|
|
107
|
+
if (value === undefined || value === null || value === '' || value === false) return;
|
|
108
|
+
if (Array.isArray(value)) {
|
|
109
|
+
value.forEach((item) => {
|
|
110
|
+
if (item && typeof item === 'object') {
|
|
111
|
+
xml += `<${key}>`;
|
|
112
|
+
Object.keys(item).forEach((itemKey) => {
|
|
113
|
+
if (item[itemKey] !== undefined && item[itemKey] !== null && item[itemKey] !== '') {
|
|
114
|
+
xml += `<${itemKey}>${this._escapeXmlText(String(item[itemKey]))}</${itemKey}>`;
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
xml += `</${key}>`;
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
} else {
|
|
121
|
+
xml += `<${key}>${this._escapeXmlText(String(value))}</${key}>`;
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
xml += `</${tag}>`;
|
|
125
|
+
return xml;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// AJUSTE con documentos: ResumenSegmento (delta) + ResumenPeriodo (acumulado)
|
|
129
|
+
if (tipoEnvio === 'AJUSTE' && this.detalle.length > 0) {
|
|
130
|
+
return _renderSection('ResumenSegmento') + _renderSection('ResumenPeriodo');
|
|
131
|
+
}
|
|
132
|
+
return _renderSection('ResumenPeriodo');
|
|
122
133
|
}
|
|
123
134
|
|
|
124
135
|
_renderDetalle() {
|