@devlas/dte-sii 2.5.12 → 2.5.13
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/SiiCertificacion.js +123 -15
- package/cert/CertRunner.js +145 -21
- package/cert/LibroCompras.js +45 -5
- package/cert/SetParser.js +96 -13
- package/package.json +1 -1
package/SiiCertificacion.js
CHANGED
|
@@ -378,14 +378,22 @@ class SiiCertificacion {
|
|
|
378
378
|
// Parsear estado de cada set/libro
|
|
379
379
|
const estadoSets = {};
|
|
380
380
|
|
|
381
|
-
//
|
|
382
|
-
//
|
|
383
|
-
const
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
const
|
|
388
|
-
|
|
381
|
+
// Parsear estado de cada set/libro fila por fila para evitar falsos positivos
|
|
382
|
+
// (la regex global cruzaba filas y asignaba REVISADO CONFORME de SET CASO GENERAL a LIBRO DE VENTAS)
|
|
383
|
+
const rows = html.split(/<\/tr>/i);
|
|
384
|
+
for (const row of rows) {
|
|
385
|
+
const nameMatch = /(SET[^<\n\r]*|LIBRO[^<\n\r]*)<\/font><\/td>/i.exec(row);
|
|
386
|
+
if (!nameMatch) continue;
|
|
387
|
+
const nombre = nameMatch[1].trim();
|
|
388
|
+
// Estado explícito en <b>...</b>
|
|
389
|
+
const stateMatch = /<b>([^<]+)<\/b>/i.exec(row);
|
|
390
|
+
if (stateMatch) {
|
|
391
|
+
estadoSets[nombre] = stateMatch[1].trim().toUpperCase();
|
|
392
|
+
} else {
|
|
393
|
+
// Fila con campos input: leer valor EST oculto (S01 = sin declarar, S21 = declarado)
|
|
394
|
+
const estMatch = /name="EST\d+"[^>]*value="([^"]+)"/i.exec(row);
|
|
395
|
+
estadoSets[nombre] = estMatch ? estMatch[1] : 'S01';
|
|
396
|
+
}
|
|
389
397
|
}
|
|
390
398
|
|
|
391
399
|
// Verificar si todos los sets requeridos están REVISADO CONFORME
|
|
@@ -541,16 +549,17 @@ class SiiCertificacion {
|
|
|
541
549
|
// El SII cambia el orden según qué sets están aprobados
|
|
542
550
|
const fieldMapping = {};
|
|
543
551
|
|
|
544
|
-
|
|
552
|
+
// Guardar pe_avance2 para debug (siempre)
|
|
553
|
+
{
|
|
545
554
|
const fs = require('fs');
|
|
546
555
|
const path = require('path');
|
|
547
|
-
const debugDir = process.env.SII_DEBUG_DIR || path.join(__dirname, '../../debug');
|
|
556
|
+
const debugDir = process.env.SII_DEBUG_DIR || path.join(__dirname, '../../debug/cert-v2');
|
|
548
557
|
if (!fs.existsSync(debugDir)) {
|
|
549
558
|
fs.mkdirSync(debugDir, { recursive: true });
|
|
550
559
|
}
|
|
551
|
-
const debugPath = path.join(debugDir, '
|
|
560
|
+
const debugPath = path.join(debugDir, 'pe_avance2_form.html');
|
|
552
561
|
fs.writeFileSync(debugPath, formHtml, 'utf8');
|
|
553
|
-
console.log('
|
|
562
|
+
console.log(' 📄 HTML pe_avance2 formulario guardado en debug/cert-v2/pe_avance2_form.html');
|
|
554
563
|
}
|
|
555
564
|
|
|
556
565
|
// Extraer todas las filas <tr>...</tr> del formulario
|
|
@@ -737,13 +746,112 @@ class SiiCertificacion {
|
|
|
737
746
|
// Éxito si el form se envió sin errores de sesión/contenido.
|
|
738
747
|
// El estado del envío (ERRORES O REPAROS / EN REVISION / REVISADO CONFORME)
|
|
739
748
|
// NO determina el éxito de la declaración — eso se resuelve vía polling.
|
|
740
|
-
const
|
|
749
|
+
const postOk = (!hasError || hasSuccess) && !sesionExpirada && !contenidoNoCorresponde;
|
|
750
|
+
|
|
751
|
+
if (!postOk) {
|
|
752
|
+
return {
|
|
753
|
+
success: false,
|
|
754
|
+
error: errorMsg || undefined,
|
|
755
|
+
status: declareResponse.status,
|
|
756
|
+
rawHtml: body,
|
|
757
|
+
formHtml,
|
|
758
|
+
setsDeclarados: Object.keys(sets),
|
|
759
|
+
formDataSent: formData,
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// Guardar HTML de pe_avance3 (respuesta de declaración) siempre para debug
|
|
764
|
+
{
|
|
765
|
+
const fs = require('fs');
|
|
766
|
+
const path = require('path');
|
|
767
|
+
const debugDir = process.env.SII_DEBUG_DIR || path.join(__dirname, '../../debug/cert-v2');
|
|
768
|
+
if (!fs.existsSync(debugDir)) fs.mkdirSync(debugDir, { recursive: true });
|
|
769
|
+
fs.writeFileSync(path.join(debugDir, 'pe_avance3_response.html'), body, 'utf8');
|
|
770
|
+
console.log(' 📄 HTML pe_avance3 guardado en debug/cert-v2/pe_avance3_response.html');
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// 7. VERIFICACIÓN POST-DECLARACIÓN: Re-leer pe_avance2 y confirmar que los TrackIDs se guardaron
|
|
774
|
+
// Si aparece "EN REVISION" → la declaración fue aceptada y el SII está procesando
|
|
775
|
+
// Si reaparecen inputs vacíos → la declaración no se guardó
|
|
776
|
+
let verificado = true;
|
|
777
|
+
let verificacionError = '';
|
|
778
|
+
let enRevision = false;
|
|
779
|
+
try {
|
|
780
|
+
const verifyResponse = await this.session.submitForm(
|
|
781
|
+
'/cvc_cgi/dte/pe_avance2',
|
|
782
|
+
{ RUT_EMP: this.rutEmpresa, DV_EMP: this.dvEmpresa, ACEPTAR: 'Continuar' },
|
|
783
|
+
'https://maullin.sii.cl/cvc_cgi/dte/pe_avance1'
|
|
784
|
+
);
|
|
785
|
+
const verifyHtml = verifyResponse.body || '';
|
|
786
|
+
|
|
787
|
+
// Guardar HTML de verificación pe_avance2 siempre
|
|
788
|
+
{
|
|
789
|
+
const fs = require('fs');
|
|
790
|
+
const path = require('path');
|
|
791
|
+
const debugDir = process.env.SII_DEBUG_DIR || path.join(__dirname, '../../debug/cert-v2');
|
|
792
|
+
if (!fs.existsSync(debugDir)) fs.mkdirSync(debugDir, { recursive: true });
|
|
793
|
+
fs.writeFileSync(path.join(debugDir, 'pe_avance2_verify.html'), verifyHtml, 'utf8');
|
|
794
|
+
console.log(' 📄 HTML pe_avance2 verificación guardado en debug/cert-v2/pe_avance2_verify.html');
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// Para cada set que declaramos, verificar estado
|
|
798
|
+
const verifyRows = verifyHtml.split(/<\/tr>/i);
|
|
799
|
+
const camposVacios = [];
|
|
800
|
+
const camposEnRevision = [];
|
|
801
|
+
for (const [setName, index] of Object.entries(fieldMapping)) {
|
|
802
|
+
if (!sets[setName]) continue;
|
|
803
|
+
for (const row of verifyRows) {
|
|
804
|
+
const numEnvMatch = row.match(new RegExp(`NAME="NUM_ENV${index}"`, 'i'));
|
|
805
|
+
if (!numEnvMatch) {
|
|
806
|
+
// Si no hay input NUM_ENV, verificar si aparece REVISADO CONFORME o EN REVISION
|
|
807
|
+
const labelPattern = patterns.find(p => p.name === setName);
|
|
808
|
+
if (labelPattern && labelPattern.label.test(row)) {
|
|
809
|
+
if (/<b>[^<]*REVISADO CONFORME[^<]*<\/b>/i.test(row)) break;
|
|
810
|
+
if (/EN REVISION/i.test(row)) {
|
|
811
|
+
camposEnRevision.push(setName);
|
|
812
|
+
break;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
continue;
|
|
816
|
+
}
|
|
817
|
+
// Tiene input — verificar si tiene valor o está REVISADO CONFORME
|
|
818
|
+
const tieneConforme = /<b>[^<]*REVISADO CONFORME[^<]*<\/b>/i.test(row);
|
|
819
|
+
if (tieneConforme) break;
|
|
820
|
+
if (/EN REVISION/i.test(row)) {
|
|
821
|
+
camposEnRevision.push(setName);
|
|
822
|
+
break;
|
|
823
|
+
}
|
|
824
|
+
const valueMatch = row.match(new RegExp(`NAME="NUM_ENV${index}"[^>]*value="([^"]*)"`, 'i'));
|
|
825
|
+
const tieneValor = valueMatch && valueMatch[1] && valueMatch[1].trim() !== '';
|
|
826
|
+
if (!tieneValor) {
|
|
827
|
+
camposVacios.push(setName);
|
|
828
|
+
}
|
|
829
|
+
break;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
if (camposEnRevision.length > 0) {
|
|
834
|
+
enRevision = true;
|
|
835
|
+
console.log(` 🔄 EN REVISION: ${camposEnRevision.join(', ')} — declaración aceptada, SII procesando`);
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
if (camposVacios.length > 0 && !enRevision) {
|
|
839
|
+
verificado = false;
|
|
840
|
+
verificacionError = `Declaración NO se guardó en el portal SII. Campos vacíos para: ${camposVacios.join(', ')}. Posible error de sesión o TrackID no reconocido.`;
|
|
841
|
+
console.log(` ⚠️ ${verificacionError}`);
|
|
842
|
+
}
|
|
843
|
+
} catch (verifyErr) {
|
|
844
|
+
console.log(` ⚠️ No se pudo verificar la declaración: ${verifyErr.message}`);
|
|
845
|
+
}
|
|
741
846
|
|
|
742
847
|
return {
|
|
743
|
-
success,
|
|
744
|
-
error: errorMsg || undefined,
|
|
848
|
+
success: verificado || enRevision,
|
|
849
|
+
error: verificacionError || errorMsg || undefined,
|
|
850
|
+
verificado,
|
|
851
|
+
enRevision,
|
|
745
852
|
status: declareResponse.status,
|
|
746
853
|
rawHtml: body,
|
|
854
|
+
formHtml,
|
|
747
855
|
setsDeclarados: Object.keys(sets),
|
|
748
856
|
formDataSent: formData,
|
|
749
857
|
};
|
package/cert/CertRunner.js
CHANGED
|
@@ -348,6 +348,16 @@ class CertRunner {
|
|
|
348
348
|
const result = await this.siiCert.declararAvance({ sets });
|
|
349
349
|
lastResult = result;
|
|
350
350
|
|
|
351
|
+
// Guardar el form pe_avance2 (antes del POST) para debug
|
|
352
|
+
if (result.formHtml) {
|
|
353
|
+
fs.writeFileSync(
|
|
354
|
+
path.join(this.debugDir, `${debugPrefix}-pe_avance2-${intento}.html`),
|
|
355
|
+
result.formHtml,
|
|
356
|
+
'utf8'
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Guardar la respuesta pe_avance3 (despues del POST) para debug
|
|
351
361
|
if (result.rawHtml) {
|
|
352
362
|
fs.writeFileSync(
|
|
353
363
|
path.join(this.debugDir, `${debugPrefix}-${intento}.html`),
|
|
@@ -370,6 +380,11 @@ class CertRunner {
|
|
|
370
380
|
if (noProcessedError && intento < maxIntentos) {
|
|
371
381
|
console.log(` ⏳ SII aún procesando, reintentando en ${intervalo / 1000}s...`);
|
|
372
382
|
await sleep(intervalo);
|
|
383
|
+
} else if (result.verificado === false && intento < maxIntentos) {
|
|
384
|
+
// Verificación post-declaración falló: los campos quedaron vacíos en el portal
|
|
385
|
+
console.log(` ⚠️ Verificación fallida: ${result.error}`);
|
|
386
|
+
console.log(` 🔄 Reintentando declaración en ${intervalo / 1000}s...`);
|
|
387
|
+
await sleep(intervalo);
|
|
373
388
|
} else if (!result.success) {
|
|
374
389
|
console.log(` ⚠️ Error declarando ${label}: ${result.error || 'desconocido'}`);
|
|
375
390
|
break;
|
|
@@ -460,6 +475,7 @@ class CertRunner {
|
|
|
460
475
|
* @param {Object} [options] - Opciones
|
|
461
476
|
* @param {Object} [options.setBasicoResult] - Resultado del SetBasico
|
|
462
477
|
* @param {Object} [options.setGuiaResult] - Resultado del SetGuia
|
|
478
|
+
* @param {Object} [options.setsResultados] - Track IDs de los sets (basico/guia/exenta/compra) para incluirlos en la declaración conjunta
|
|
463
479
|
* @returns {Promise<Object>} Resultado con todos los libros
|
|
464
480
|
*/
|
|
465
481
|
async ejecutarFase4Libros(options = {}) {
|
|
@@ -471,12 +487,49 @@ class CertRunner {
|
|
|
471
487
|
const resultados = {};
|
|
472
488
|
const errores = [];
|
|
473
489
|
|
|
490
|
+
// Verificar cuáles libros ya están REVISADO CONFORME en el portal (no re-enviar)
|
|
491
|
+
let _estadoActual = {};
|
|
492
|
+
try {
|
|
493
|
+
const _consultaPrevia = await this.siiCert.consultarEstadoSets();
|
|
494
|
+
if (_consultaPrevia.success) _estadoActual = _consultaPrevia.estadoSets || {};
|
|
495
|
+
const _yaConformes = Object.entries(_estadoActual)
|
|
496
|
+
.filter(([k, v]) => k.toUpperCase().includes('LIBRO') && v === 'REVISADO CONFORME')
|
|
497
|
+
.map(([k]) => k);
|
|
498
|
+
if (_yaConformes.length) {
|
|
499
|
+
console.log(` ℹ️ Ya en REVISADO CONFORME (se omitirán): ${_yaConformes.join(', ')}`);
|
|
500
|
+
}
|
|
501
|
+
} catch (_e) { /* ignorar error de consulta previa */ }
|
|
502
|
+
|
|
503
|
+
const _estaConforme = (nombre) => {
|
|
504
|
+
const nombreUpper = nombre.toUpperCase();
|
|
505
|
+
const e = Object.entries(_estadoActual).find(([k]) => {
|
|
506
|
+
const ku = k.toUpperCase();
|
|
507
|
+
if (nombreUpper === 'LIBRO DE COMPRAS') return ku.includes('LIBRO DE COMPRAS') && !ku.includes('EXENTOS');
|
|
508
|
+
return ku.includes(nombreUpper);
|
|
509
|
+
});
|
|
510
|
+
return e && e[1] === 'REVISADO CONFORME';
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
// Helper: guardar resultados parciales a disco tras cada envío exitoso
|
|
514
|
+
const _resultadosLibrosPath = path.join(this.debugDir, 'resultados-libros.json');
|
|
515
|
+
const _guardarResultadosParciales = () => {
|
|
516
|
+
try {
|
|
517
|
+
fs.writeFileSync(_resultadosLibrosPath, JSON.stringify(resultados, null, 2));
|
|
518
|
+
} catch (_e) { /* ignorar */ }
|
|
519
|
+
};
|
|
520
|
+
|
|
474
521
|
try {
|
|
475
522
|
// 1. Libro de Compras (usa datos del SII)
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
523
|
+
if (_estaConforme('LIBRO DE COMPRAS')) {
|
|
524
|
+
console.log('\n✅ Libro de Compras ya está REVISADO CONFORME — omitiendo');
|
|
525
|
+
} else {
|
|
526
|
+
console.log('\n📖 Enviando Libro de Compras...');
|
|
527
|
+
resultados.libroCompras = await this.ejecutarLibroCompras(options);
|
|
528
|
+
if (!resultados.libroCompras.success) {
|
|
529
|
+
errores.push(`Libro Compras: ${resultados.libroCompras.error}`);
|
|
530
|
+
} else {
|
|
531
|
+
_guardarResultadosParciales();
|
|
532
|
+
}
|
|
480
533
|
}
|
|
481
534
|
} catch (e) {
|
|
482
535
|
errores.push(`Libro Compras: ${e.message}`);
|
|
@@ -484,10 +537,16 @@ class CertRunner {
|
|
|
484
537
|
|
|
485
538
|
try {
|
|
486
539
|
// 2. Libro de Ventas (usa SetBasico)
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
540
|
+
if (_estaConforme('LIBRO DE VENTAS')) {
|
|
541
|
+
console.log('\n✅ Libro de Ventas ya está REVISADO CONFORME — omitiendo');
|
|
542
|
+
} else {
|
|
543
|
+
console.log('\n📖 Enviando Libro de Ventas...');
|
|
544
|
+
resultados.libroVentas = await this.ejecutarLibroVentas(options);
|
|
545
|
+
if (!resultados.libroVentas.success) {
|
|
546
|
+
errores.push(`Libro Ventas: ${resultados.libroVentas.error}`);
|
|
547
|
+
} else {
|
|
548
|
+
_guardarResultadosParciales();
|
|
549
|
+
}
|
|
491
550
|
}
|
|
492
551
|
} catch (e) {
|
|
493
552
|
errores.push(`Libro Ventas: ${e.message}`);
|
|
@@ -495,22 +554,34 @@ class CertRunner {
|
|
|
495
554
|
|
|
496
555
|
try {
|
|
497
556
|
// 3. Libro de Guías (usa SetGuia)
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
557
|
+
if (_estaConforme('LIBRO DE GUIAS')) {
|
|
558
|
+
console.log('\n✅ Libro de Guías ya está REVISADO CONFORME — omitiendo');
|
|
559
|
+
} else {
|
|
560
|
+
console.log('\n📖 Enviando Libro de Guías...');
|
|
561
|
+
resultados.libroGuias = await this.ejecutarLibroGuias(options);
|
|
562
|
+
if (!resultados.libroGuias.success) {
|
|
563
|
+
errores.push(`Libro Guías: ${resultados.libroGuias.error}`);
|
|
564
|
+
} else {
|
|
565
|
+
_guardarResultadosParciales();
|
|
566
|
+
}
|
|
502
567
|
}
|
|
503
568
|
} catch (e) {
|
|
504
569
|
errores.push(`Libro Guías: ${e.message}`);
|
|
505
570
|
}
|
|
506
571
|
|
|
507
|
-
// 4. Libro de Compras para Exentos (solo si el SII lo entregó)
|
|
572
|
+
// 4. Libro de Compras para Exentos (solo si el SII lo entregó y no está ya aprobado)
|
|
508
573
|
if (this._estructuras?.libroComprasExentos) {
|
|
509
574
|
try {
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
575
|
+
if (_estaConforme('LIBRO DE COMPRAS PARA EXENTOS')) {
|
|
576
|
+
console.log('\n✅ Libro Compras Exentos ya está REVISADO CONFORME — omitiendo');
|
|
577
|
+
} else {
|
|
578
|
+
console.log('\n📖 Enviando Libro de Compras para Exentos...');
|
|
579
|
+
resultados.libroComprasExentos = await this.ejecutarLibroComprasExentos(options);
|
|
580
|
+
if (!resultados.libroComprasExentos.success) {
|
|
581
|
+
errores.push(`Libro Compras Exentos: ${resultados.libroComprasExentos.error}`);
|
|
582
|
+
} else {
|
|
583
|
+
_guardarResultadosParciales();
|
|
584
|
+
}
|
|
514
585
|
}
|
|
515
586
|
} catch (e) {
|
|
516
587
|
errores.push(`Libro Compras Exentos: ${e.message}`);
|
|
@@ -518,18 +589,65 @@ class CertRunner {
|
|
|
518
589
|
}
|
|
519
590
|
|
|
520
591
|
// Contar libros obligatorios (ventas + compras + guías)
|
|
592
|
+
// Un libro cuenta como OK si fue enviado exitosamente O si ya era REVISADO CONFORME (omitido)
|
|
521
593
|
const librosObligatorios = ['libroVentas', 'libroCompras', 'libroGuias'];
|
|
522
|
-
const librosEnviados = librosObligatorios.filter(k =>
|
|
594
|
+
const librosEnviados = librosObligatorios.filter(k => {
|
|
595
|
+
if (resultados[k]?.success) return true;
|
|
596
|
+
// Mapeo clave→nombre para consultar _estaConforme
|
|
597
|
+
const nombreMap = { libroVentas: 'LIBRO DE VENTAS', libroCompras: 'LIBRO DE COMPRAS', libroGuias: 'LIBRO DE GUIAS' };
|
|
598
|
+
return _estaConforme(nombreMap[k]);
|
|
599
|
+
}).length;
|
|
523
600
|
|
|
524
601
|
if (librosEnviados === 3) {
|
|
525
|
-
// 4. Declarar los libros
|
|
602
|
+
// 4. Declarar los libros (incluyendo sets para evitar que pe_avance3 los resetee)
|
|
526
603
|
console.log('\n📝 Declarando libros...');
|
|
527
604
|
try {
|
|
528
|
-
const declaracion = await this.declararLibros();
|
|
605
|
+
const declaracion = await this.declararLibros({ ...resultados, ...(options.setsResultados || {}) });
|
|
529
606
|
resultados.declaracion = declaracion;
|
|
530
607
|
|
|
531
608
|
if (declaracion.success) {
|
|
532
|
-
console.log('\n✅
|
|
609
|
+
console.log('\n✅ Libros declarados — esperando revisión del SII...');
|
|
610
|
+
|
|
611
|
+
// Polling real: esperar hasta REVISADO CONFORME o S25 (máx 20 intentos x 15s = 5 min)
|
|
612
|
+
const _librosAEsperar = ['LIBRO DE VENTAS', 'LIBRO DE COMPRAS', 'LIBRO DE GUIAS'];
|
|
613
|
+
if (this._estructuras?.libroComprasExentos) _librosAEsperar.push('LIBRO DE COMPRAS PARA EXENTOS');
|
|
614
|
+
// Solo esperar los libros que realmente enviamos (no los que ya eran REVISADO CONFORME)
|
|
615
|
+
const _librosEnviados = _librosAEsperar.filter(n => !_estaConforme(n));
|
|
616
|
+
|
|
617
|
+
console.log(`\n⏳ Esperando aprobación del SII para: ${_librosEnviados.join(', ')}`);
|
|
618
|
+
let _aprobados = false;
|
|
619
|
+
for (let _i = 0; _i < 20; _i++) {
|
|
620
|
+
await sleep(15000);
|
|
621
|
+
const _poll = await this.siiCert.consultarEstadoSets();
|
|
622
|
+
if (!_poll.success) continue;
|
|
623
|
+
const _ss = _poll.estadoSets || {};
|
|
624
|
+
const _info = Object.entries(_ss)
|
|
625
|
+
.filter(([k]) => k.toUpperCase().includes('LIBRO'))
|
|
626
|
+
.map(([k, v]) => `${k.trim()}: ${v}`);
|
|
627
|
+
if (_info.length) console.log(` 🔄 Intento ${_i + 1}/20: ${_info.join(' | ')}`);
|
|
628
|
+
|
|
629
|
+
const _todosOk = _librosEnviados.every(nombre => {
|
|
630
|
+
const e = Object.entries(_ss).find(([k]) => k.toUpperCase().includes(nombre));
|
|
631
|
+
return e && (e[1] === 'REVISADO CONFORME' || e[1] === 'S25');
|
|
632
|
+
});
|
|
633
|
+
const _algunError = _librosEnviados.some(nombre => {
|
|
634
|
+
const e = Object.entries(_ss).find(([k]) => k.toUpperCase().includes(nombre));
|
|
635
|
+
return e && (e[1] === 'LNC' || e[1] === 'LRH' || e[1].includes('RECHAZADO') || e[1].includes('ERROR'));
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
if (_todosOk) {
|
|
639
|
+
console.log('\n🎉 ¡LIBROS APROBADOS POR EL SII!');
|
|
640
|
+
_aprobados = true;
|
|
641
|
+
break;
|
|
642
|
+
}
|
|
643
|
+
if (_algunError) {
|
|
644
|
+
console.log('\n❌ Hay libros rechazados. Revisar emails del SII.');
|
|
645
|
+
break;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
if (!_aprobados) {
|
|
649
|
+
console.log('\n⚠️ Timeout (5 min). El SII aún no responde. Verifica con --avance más tarde.');
|
|
650
|
+
}
|
|
533
651
|
} else {
|
|
534
652
|
console.log(`\n⚠️ Libros enviados pero declaración con error: ${declaracion.error}`);
|
|
535
653
|
}
|
|
@@ -563,6 +681,12 @@ class CertRunner {
|
|
|
563
681
|
const sets = {};
|
|
564
682
|
|
|
565
683
|
const mapping = {
|
|
684
|
+
// Sets — incluirlos para que pe_avance3 no los resetee a S01
|
|
685
|
+
basico: 'setBasico',
|
|
686
|
+
guia: 'setGuiaDespacho',
|
|
687
|
+
exenta: 'setFacturaExenta',
|
|
688
|
+
compra: 'setFacturaCompra',
|
|
689
|
+
// Libros
|
|
566
690
|
libroVentas: 'libroVentas',
|
|
567
691
|
libroCompras: 'libroCompras',
|
|
568
692
|
libroGuias: 'libroGuias',
|
package/cert/LibroCompras.js
CHANGED
|
@@ -120,10 +120,32 @@ class LibroCompras {
|
|
|
120
120
|
else if (doc.IVANoRec) {
|
|
121
121
|
docAjustado.TasaImp = 0;
|
|
122
122
|
docAjustado.MntIVA = 0;
|
|
123
|
+
// SII requiere MntNeto presente (aunque sea 0) cuando hay IVANoRec — LBR-3 si falta
|
|
124
|
+
if (docAjustado.MntNeto === undefined || docAjustado.MntNeto === null) {
|
|
125
|
+
docAjustado.MntNeto = 0;
|
|
126
|
+
}
|
|
123
127
|
}
|
|
124
128
|
// Caso normal: Asegurar TasaImp sea número entero (19) no decimal (0.19)
|
|
129
|
+
// Si hay TasaImp pero no hay MntNeto, el SII igual exige TasaImp + MntNeto=0 + MntIVA=0 explícitos
|
|
125
130
|
else if (doc.TasaImp !== undefined) {
|
|
126
|
-
|
|
131
|
+
const tasaNum = doc.TasaImp < 1 ? Math.round(doc.TasaImp * 100) : doc.TasaImp;
|
|
132
|
+
const tieneMntNeto = doc.MntNeto !== undefined && Number(doc.MntNeto) > 0;
|
|
133
|
+
const tieneMntExe = doc.MntExe !== undefined && Number(doc.MntExe) > 0;
|
|
134
|
+
if (tasaNum > 0 && !tieneMntNeto && tieneMntExe) {
|
|
135
|
+
// Documento con TasaImp + MntExe pero sin MntNeto (p.ej. TipoDoc=30 folio exento)
|
|
136
|
+
// SII exige TasaImp + MntNeto + MntIVA presentes; con MntNeto=0 da LOK+LBR-2 (aceptado)
|
|
137
|
+
// Borrar los campos da LRH+LBR-3 (rechazado) — NO borrar
|
|
138
|
+
docAjustado.TasaImp = tasaNum;
|
|
139
|
+
docAjustado.MntNeto = 0;
|
|
140
|
+
docAjustado.MntIVA = 0;
|
|
141
|
+
} else if (tasaNum > 0 && !tieneMntNeto) {
|
|
142
|
+
// TasaImp presente pero sin MntNeto ni MntExe: SII exige ceros explícitos
|
|
143
|
+
docAjustado.TasaImp = tasaNum;
|
|
144
|
+
docAjustado.MntNeto = 0;
|
|
145
|
+
docAjustado.MntIVA = 0;
|
|
146
|
+
} else {
|
|
147
|
+
docAjustado.TasaImp = tasaNum;
|
|
148
|
+
}
|
|
127
149
|
}
|
|
128
150
|
|
|
129
151
|
return docAjustado;
|
|
@@ -262,6 +284,7 @@ class LibroCompras {
|
|
|
262
284
|
TotIVANoRec: {},
|
|
263
285
|
TotOtrosImp: {},
|
|
264
286
|
TotIVARetTotal: 0,
|
|
287
|
+
TotMntNoFact: 0,
|
|
265
288
|
});
|
|
266
289
|
}
|
|
267
290
|
|
|
@@ -271,6 +294,7 @@ class LibroCompras {
|
|
|
271
294
|
r.TotMntNeto += Number(doc.MntNeto || 0);
|
|
272
295
|
r.TotMntIVA += Number(doc.MntIVA || 0);
|
|
273
296
|
r.TotMntTotal += Number(doc.MntTotal || 0);
|
|
297
|
+
r.TotMntNoFact += Number(doc.MntNoFact || 0);
|
|
274
298
|
|
|
275
299
|
// IVA Uso Común
|
|
276
300
|
if (doc.IVAUsoComun) {
|
|
@@ -319,10 +343,26 @@ class LibroCompras {
|
|
|
319
343
|
TotDoc: r.TotDoc,
|
|
320
344
|
};
|
|
321
345
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
346
|
+
// TotMntExe siempre presente (incluso como 0)
|
|
347
|
+
// TotMntNeto y TotMntIVA solo se emiten cuando son > 0:
|
|
348
|
+
// - Para docs afectos: siempre > 0 → se emiten
|
|
349
|
+
// - Para docs puramente exentos (TpoDoc=30/34/etc sin IVA): son 0
|
|
350
|
+
// y emitir <TotMntNeto>0</TotMntNeto> causa 'El Monto Neto No Cuadra'
|
|
351
|
+
// porque el SII espera que esté AUSENTE, no cero
|
|
352
|
+
limpio.TotMntExe = r.TotMntExe;
|
|
353
|
+
// El XSD LibroCV requiere el orden estricto: TotMntNeto → TotMntIVA → TotIVANoRec/TotOtrosImp
|
|
354
|
+
// TotMntNeto y TotMntIVA son SIEMPRE requeridos por el XSD (incluso como 0)
|
|
355
|
+
const tieneIVANoRec = Object.keys(r.TotIVANoRec).length > 0;
|
|
356
|
+
const tieneOtrosImp = Object.keys(r.TotOtrosImp).length > 0;
|
|
357
|
+
const necesitaIVAFields = tieneIVANoRec || tieneOtrosImp;
|
|
358
|
+
limpio.TotMntNeto = r.TotMntNeto;
|
|
359
|
+
limpio.TotMntIVA = r.TotMntIVA;
|
|
360
|
+
limpio.TotMntTotal = r.TotMntTotal;
|
|
361
|
+
|
|
362
|
+
// MntNoFact (TpoDoc=32 Liquidación-Factura)
|
|
363
|
+
if (r.TotMntNoFact > 0) {
|
|
364
|
+
limpio.TotMntNoFact = r.TotMntNoFact;
|
|
365
|
+
}
|
|
326
366
|
|
|
327
367
|
// IVA Uso Común
|
|
328
368
|
if (r.TotOpIVAUsoComun > 0) {
|
package/cert/SetParser.js
CHANGED
|
@@ -124,12 +124,12 @@ function mapearTipoDocLibro(tipo) {
|
|
|
124
124
|
if (t.includes('FACTURA EXENTA ELECTRONICA') || (t.includes('NO AFECTA') && t.includes('ELECTRONICA'))) return 34;
|
|
125
125
|
if (t.includes('FACTURA ELECTRONICA')) return 33;
|
|
126
126
|
if (t.includes('FACTURA EXENTA') || t.includes('FACTURA NO AFECTA')) return 30; // exenta papel
|
|
127
|
-
if (t === 'FACTURA') return 30;
|
|
127
|
+
if (t === 'FACTURA') return 30; // factura afecta papel → TpoDoc=30 (regular libro); exentos libro usa lógica especial
|
|
128
128
|
if (t.includes('NOTA DE CREDITO') && (t.includes('ELECTRONICA') || t.includes('ELECTRONICO'))) return 61;
|
|
129
129
|
if (t.includes('NOTA DE CREDITO')) return 60;
|
|
130
130
|
if (t.includes('NOTA DE DEBITO') && (t.includes('ELECTRONICA') || t.includes('ELECTRONICO'))) return 56;
|
|
131
131
|
if (t.includes('NOTA DE DEBITO')) return 55;
|
|
132
|
-
return
|
|
132
|
+
return null;
|
|
133
133
|
}
|
|
134
134
|
|
|
135
135
|
// ═══════════════════════════════════════════════════════════════
|
|
@@ -991,14 +991,26 @@ function generarEstructuraSetExportacion(set) {
|
|
|
991
991
|
/**
|
|
992
992
|
* Genera estructura para LIBRO DE COMPRAS (IECV)
|
|
993
993
|
*/
|
|
994
|
-
function generarEstructuraLibroCompras(set) {
|
|
994
|
+
function generarEstructuraLibroCompras(set, opts = {}) {
|
|
995
|
+
const esExentos = opts.esExentos === true;
|
|
995
996
|
const detalle = [];
|
|
996
997
|
const resumen = {};
|
|
997
998
|
|
|
998
999
|
for (const doc of set.documentosLibro) {
|
|
999
|
-
|
|
1000
|
+
let tipoDoc = mapearTipoDocLibro(doc.tipoDocumento);
|
|
1000
1001
|
const tasaIva = 0.19;
|
|
1001
1002
|
|
|
1003
|
+
// En libro de compras EXENTOS, los TpoDoc correctos son:
|
|
1004
|
+
// FACTURA (papel afecta) → 30 (con IVANoRec)
|
|
1005
|
+
// FACTURA EXENTA (papel) → 32 (Fac. Venta B&S No Afectos/Exentos, papel)
|
|
1006
|
+
// FACTURA ELECTRONICA → 33 (con IVANoRec si tiene montoAfecto)
|
|
1007
|
+
// FACTURA EXENTA ELECTRONICA → 34 (puramente exenta)
|
|
1008
|
+
// NC/NCE/ND/NDE → 60/61/55/56
|
|
1009
|
+
// Total: 7 tipos distintos = 7 líneas de resumen
|
|
1010
|
+
const esFacturaExentaPapel = esExentos && tipoDoc === 30
|
|
1011
|
+
&& !doc.montoAfecto && !!doc.montoExento;
|
|
1012
|
+
if (esFacturaExentaPapel) tipoDoc = 32;
|
|
1013
|
+
|
|
1002
1014
|
const detalleDoc = {
|
|
1003
1015
|
TpoDoc: tipoDoc,
|
|
1004
1016
|
NroDoc: doc.folio,
|
|
@@ -1035,16 +1047,56 @@ function generarEstructuraLibroCompras(set) {
|
|
|
1035
1047
|
detalleDoc.IVARetTotal = Math.round((doc.montoAfecto || 0) * tasaIva);
|
|
1036
1048
|
}
|
|
1037
1049
|
|
|
1038
|
-
// Referencia para notas de crédito
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1050
|
+
// Referencia para notas de crédito/débito (TpoDoc 60/61/55/56)
|
|
1051
|
+
const esNota = (tipoDoc === 60 || tipoDoc === 61 || tipoDoc === 55 || tipoDoc === 56);
|
|
1052
|
+
if (esNota && doc.observacion) {
|
|
1053
|
+
// El folio referenciado es siempre el último número de la observación
|
|
1054
|
+
const matchFolio = doc.observacion.match(/(\d+)\s*$/i);
|
|
1055
|
+
if (matchFolio) {
|
|
1056
|
+
detalleDoc.FolioDocRef = parseInt(matchFolio[1]);
|
|
1057
|
+
if (doc.observacion.match(/FACTURA EXENTA ELECTRONICA/i)) {
|
|
1058
|
+
detalleDoc.TpoDocRef = 34;
|
|
1059
|
+
} else if (doc.observacion.match(/FACTURA EXENTA/i)) {
|
|
1060
|
+
detalleDoc.TpoDocRef = esExentos ? 32 : 30;
|
|
1061
|
+
} else if (doc.observacion.match(/FACTURA.*ELECTRONICA/i)) {
|
|
1062
|
+
detalleDoc.TpoDocRef = 33;
|
|
1063
|
+
} else if (doc.observacion.match(/FACTURA/i)) {
|
|
1064
|
+
detalleDoc.TpoDocRef = 30;
|
|
1065
|
+
}
|
|
1066
|
+
// Sin TpoDocRef si no se menciona FACTURA (ej. referencia a otra NC/ND)
|
|
1044
1067
|
}
|
|
1045
1068
|
}
|
|
1046
1069
|
|
|
1047
|
-
|
|
1070
|
+
// En libro de compras EXENTOS: lógica por tipo de doc
|
|
1071
|
+
// Docs con montoAfecto → IVANoRec (IVA no recuperable)
|
|
1072
|
+
// Docs puramente exentos (TpoDoc=32/34) → sin IVANoRec
|
|
1073
|
+
// TpoDoc=46 → usa IVARetTotal (no IVANoRec)
|
|
1074
|
+
const esPuramenteExento = !doc.montoAfecto && !!doc.montoExento;
|
|
1075
|
+
if (esExentos && tipoDoc !== 46 && !esPuramenteExento) {
|
|
1076
|
+
const mntIVA = detalleDoc.MntIVA || 0;
|
|
1077
|
+
detalleDoc.IVANoRec = { CodIVANoRec: 1, MntIVANoRec: mntIVA };
|
|
1078
|
+
detalleDoc.MntIVA = 0;
|
|
1079
|
+
if (detalleDoc.MntNeto === undefined || detalleDoc.MntNeto === null) {
|
|
1080
|
+
detalleDoc.MntNeto = 0;
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
// TpoDoc=32 (Fac. Exenta papel): solo MntExe + MntTotal, sin TasaImp/MntNeto/MntIVA/IVANoRec
|
|
1084
|
+
if (esFacturaExentaPapel) {
|
|
1085
|
+
delete detalleDoc.TasaImp;
|
|
1086
|
+
delete detalleDoc.MntNeto;
|
|
1087
|
+
delete detalleDoc.MntIVA;
|
|
1088
|
+
delete detalleDoc.IVANoRec;
|
|
1089
|
+
}
|
|
1090
|
+
// Para docs puramente exentos que NO son TpoDoc=32: TasaImp=19 + MntNeto=0 + MntIVA=0
|
|
1091
|
+
if (esExentos && esPuramenteExento && !esFacturaExentaPapel) {
|
|
1092
|
+
detalleDoc.TasaImp = Math.round(tasaIva * 100); // TasaImp=19 requerido
|
|
1093
|
+
detalleDoc.MntNeto = 0;
|
|
1094
|
+
detalleDoc.MntIVA = 0;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
// MntTotal incluye IVANoRec (IVA pagado pero no deducible)
|
|
1098
|
+
const mntIvaNoRecTotal = (esExentos && detalleDoc.IVANoRec) ? (detalleDoc.IVANoRec.MntIVANoRec || 0) : 0;
|
|
1099
|
+
detalleDoc.MntTotal = (detalleDoc.MntNeto || 0) + (detalleDoc.MntIVA || 0) + (detalleDoc.MntExe || 0) + mntIvaNoRecTotal;
|
|
1048
1100
|
if (doc.retencionTotal && tipoDoc === 46) {
|
|
1049
1101
|
detalleDoc.MntTotal = detalleDoc.MntNeto || 0; // Sin IVA pagado
|
|
1050
1102
|
}
|
|
@@ -1069,6 +1121,36 @@ function generarEstructuraLibroCompras(set) {
|
|
|
1069
1121
|
resumen[tipoDoc].TotMntTotal += detalleDoc.MntTotal || 0;
|
|
1070
1122
|
}
|
|
1071
1123
|
|
|
1124
|
+
// Post-proceso para libro de compras EXENTOS:
|
|
1125
|
+
// NC/NCE con sin montos declarados que anulan un doc con montos deben heredar esos montos
|
|
1126
|
+
if (esExentos) {
|
|
1127
|
+
for (const doc of detalle) {
|
|
1128
|
+
const esNotaAnulacion = (doc.TpoDoc === 60 || doc.TpoDoc === 61 || doc.TpoDoc === 55 || doc.TpoDoc === 56)
|
|
1129
|
+
&& !doc.MntNeto && !doc.MntExe && doc.FolioDocRef;
|
|
1130
|
+
if (esNotaAnulacion) {
|
|
1131
|
+
const refDoc = detalle.find(d => d.NroDoc === doc.FolioDocRef);
|
|
1132
|
+
if (refDoc) {
|
|
1133
|
+
if (refDoc.MntExe) doc.MntExe = refDoc.MntExe;
|
|
1134
|
+
if (refDoc.MntNeto) {
|
|
1135
|
+
doc.MntNeto = refDoc.MntNeto;
|
|
1136
|
+
// Heredar IVANoRec del doc referenciado
|
|
1137
|
+
if (refDoc.IVANoRec) {
|
|
1138
|
+
doc.IVANoRec = { CodIVANoRec: 1, MntIVANoRec: refDoc.IVANoRec.MntIVANoRec || 0 };
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
const mntIvaNoRecDoc = (doc.IVANoRec ? (doc.IVANoRec.MntIVANoRec || 0) : 0);
|
|
1142
|
+
doc.MntTotal = (doc.MntNeto || 0) + (doc.MntExe || 0) + mntIvaNoRecDoc;
|
|
1143
|
+
// Actualizar resumen para este TpoDoc
|
|
1144
|
+
if (resumen[doc.TpoDoc]) {
|
|
1145
|
+
resumen[doc.TpoDoc].TotMntExe += doc.MntExe || 0;
|
|
1146
|
+
resumen[doc.TpoDoc].TotMntNeto += doc.MntNeto || 0;
|
|
1147
|
+
resumen[doc.TpoDoc].TotMntTotal += doc.MntTotal;
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1072
1154
|
return {
|
|
1073
1155
|
numeroAtencion: set.numeroAtencion,
|
|
1074
1156
|
factorProporcionalidad: set.factorProporcionalidad || 0.6,
|
|
@@ -1136,8 +1218,9 @@ function generarEstructurasParaScripts(datosExtraidos, receptorConfig = {}) {
|
|
|
1136
1218
|
estructuras.libroCompras = generarEstructuraLibroCompras(set);
|
|
1137
1219
|
break;
|
|
1138
1220
|
case 'LIBRO_COMPRAS_EXENTOS':
|
|
1139
|
-
//
|
|
1140
|
-
|
|
1221
|
+
// En el libro de exentos, FACTURA afecta papel usa TpoDoc=29 (Factura de Inicio)
|
|
1222
|
+
// para distinguirse de FACTURA EXENTA (TpoDoc=30) en el ResumenPeriodo
|
|
1223
|
+
estructuras.libroComprasExentos = generarEstructuraLibroCompras(set, { esExentos: true });
|
|
1141
1224
|
break;
|
|
1142
1225
|
case 'LIBRO_VENTAS':
|
|
1143
1226
|
// El libro de ventas se genera con los datos del set básico o exento
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devlas/dte-sii",
|
|
3
|
-
"version": "2.5.
|
|
3
|
+
"version": "2.5.13",
|
|
4
4
|
"description": "Facturación y boletas electrónicas para el SII de Chile. Genera, timbra, firma y envía DTEs, libros electrónicos y automatiza la certificación.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "dte-sii.d.ts",
|