@devlas/dte-sii 2.5.11 → 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.
@@ -1,359 +1,399 @@
1
1
  // Copyright (c) 2026 Devlas SpA — https://devlas.cl
2
2
  // Licencia MIT. Ver archivo LICENSE para mas detalles.
3
- /**
4
- * LibroCompras.js - Generador de Libro de Compras para Certificación SII
5
- *
6
- * Construye el libro de compras a partir de los resultados del SetCompra.
7
- *
8
- * Documentos incluidos:
9
- * - Facturas de Compra (46) con IVA Retenido Total
10
- * - Notas de Crédito (61)
11
- * - Notas de Débito (56)
12
- *
13
- * Maneja campos especiales:
14
- * - IVANoRec (IVA no recuperable)
15
- * - IVAUsoComun (IVA de uso común con factor proporcionalidad)
16
- * - OtrosImp (otros impuestos)
17
- * - IVARetTotal (IVA retenido total)
18
- *
19
- * @module dte-sii/cert/LibroCompras
20
- */
21
-
22
- const { LibroCompraVenta } = require('../index');
23
-
24
- // Factor de proporcionalidad para IVA de uso común (60%)
25
- const FACTOR_PROPORCIONALIDAD = 0.6;
26
-
27
- /**
28
- * @typedef {Object} LibroComprasConfig
29
- * @property {Object} emisor - Datos del emisor
30
- * @property {string} periodo - Período tributario (YYYY-MM)
31
- * @property {Object} certificado - Instancia de Certificado
32
- */
33
-
34
- class LibroCompras {
35
- /**
36
- * @param {LibroComprasConfig} config
37
- */
38
- constructor(config) {
39
- this.config = config;
40
- this.emisor = config.emisor;
41
- this.periodo = config.periodo;
42
- this.certificado = config.certificado;
43
- }
44
-
45
- /**
46
- * Genera el libro de compras desde los resultados del SetCompra
47
- * @param {Object} setCompraResult - Resultado de SetCompra.ejecutar()
48
- * @returns {Object} { libro, xml, detalle, resumen }
49
- */
50
- generar(setCompraResult) {
51
- const { documentos } = setCompraResult;
52
-
53
- if (!documentos || documentos.length === 0) {
54
- throw new Error('LibroCompras: No hay documentos del SetCompra para generar libro');
55
- }
56
-
57
- const fechaBase = `${this.periodo}-15`;
58
- const detalles = [];
59
-
60
- // Procesar cada documento del set compra
61
- for (const doc of documentos) {
62
- const detalle = this._buildDetalle(doc, fechaBase);
63
- detalles.push(detalle);
64
- }
65
-
66
- // Calcular resumen automáticamente desde el detalle
67
- const resumen = this._calcularResumenDesdeDetalle(detalles);
68
-
69
- // Crear libro
70
- const libro = new LibroCompraVenta(this.certificado);
71
- libro.setCaratula({
72
- RutEmisorLibro: this.emisor.rut,
73
- RutEnvia: this.certificado.rut || this.emisor.rut,
74
- PeriodoTributario: this.periodo,
75
- FchResol: this.emisor.fch_resol,
76
- NroResol: this.emisor.nro_resol,
77
- TipoOperacion: 'COMPRA',
78
- TipoLibro: 'MENSUAL',
79
- TipoEnvio: 'TOTAL',
80
- });
81
- libro.setResumen(resumen);
82
- libro.setDetalle(detalles);
83
- libro.generar();
84
-
85
- return {
86
- libro,
87
- xml: libro.getXML(),
88
- detalle: detalles,
89
- resumen,
90
- };
91
- }
92
-
93
- /**
94
- * Genera el libro de compras desde los datos pre-procesados del SII (estructuras)
95
- * @param {Object} libroComprasData - Datos del libro de compras { detalle, resumen, factorProporcionalidad }
96
- * @param {string} periodo - Período tributario (YYYY-MM)
97
- * @returns {Object} { libro, xml, detalle, resumen }
98
- */
99
- generarDesdeEstructuras(libroComprasData, periodo) {
100
- const { detalle, resumen, factorProporcionalidad } = libroComprasData;
101
-
102
- if (!detalle || detalle.length === 0) {
103
- throw new Error('LibroCompras: No hay detalle en los datos del SII');
104
- }
105
-
106
- // Ajustar fechas al período correcto y aplicar reglas SII
107
- const fechaBase = `${periodo}-15`;
108
- const detalleAjustado = detalle.map(doc => {
109
- const docAjustado = {
110
- ...doc,
111
- FchDoc: fechaBase,
112
- };
113
-
114
- // Regla SII: Cuando hay IVAUsoComun, TasaImp y MntIVA van en 0
115
- if (doc.IVAUsoComun) {
116
- docAjustado.TasaImp = 0;
117
- docAjustado.MntIVA = 0;
118
- }
119
- // Regla SII: Cuando hay IVANoRec, TasaImp y MntIVA van en 0
120
- else if (doc.IVANoRec) {
121
- docAjustado.TasaImp = 0;
122
- docAjustado.MntIVA = 0;
123
- }
124
- // Caso normal: Asegurar TasaImp sea número entero (19) no decimal (0.19)
125
- else if (doc.TasaImp !== undefined) {
126
- docAjustado.TasaImp = doc.TasaImp < 1 ? Math.round(doc.TasaImp * 100) : doc.TasaImp;
127
- }
128
-
129
- return docAjustado;
130
- });
131
-
132
- // Recalcular resumen desde el detalle ajustado
133
- const resumenRecalculado = this._calcularResumenDesdeDetalle(detalleAjustado, factorProporcionalidad);
134
-
135
- // Crear libro
136
- const libro = new LibroCompraVenta(this.certificado);
137
- libro.setCaratula({
138
- RutEmisorLibro: this.emisor.rut,
139
- RutEnvia: this.certificado.rut || this.emisor.rut,
140
- PeriodoTributario: periodo,
141
- FchResol: this.emisor.fch_resol,
142
- NroResol: this.emisor.nro_resol,
143
- TipoOperacion: 'COMPRA',
144
- TipoLibro: 'MENSUAL',
145
- TipoEnvio: 'TOTAL',
146
- });
147
- libro.setResumen(resumenRecalculado);
148
- libro.setDetalle(detalleAjustado);
149
- libro.generar();
150
-
151
- return {
152
- libro,
153
- xml: libro.getXML(),
154
- detalle: detalleAjustado,
155
- resumen: resumenRecalculado,
156
- };
157
- }
158
-
159
- /**
160
- * Construye un registro de detalle desde un documento
161
- * @private
162
- */
163
- _buildDetalle(doc, fechaBase) {
164
- const tipoDte = doc.tipoDte;
165
- const totales = doc.totales || {};
166
-
167
- // En Factura de Compra (46), el emisor ES el receptor del documento
168
- // porque nosotros RECIBIMOS la factura de compra
169
- const rutDoc = this.emisor.rut;
170
- const rznSoc = this.emisor.razon_social;
171
-
172
- const detalle = {
173
- TpoDoc: tipoDte,
174
- NroDoc: doc.folio,
175
- FchDoc: fechaBase,
176
- RUTDoc: rutDoc,
177
- RznSoc: rznSoc,
178
- };
179
-
180
- // Montos básicos
181
- const mntNeto = Number(totales.MntNeto || 0);
182
- const mntExe = Number(totales.MntExe || 0);
183
- const mntIva = Number(totales.IVA || 0);
184
- const ivaRetTotal = Number(totales.IVARetTotal || 0);
185
-
186
- if (mntExe > 0) detalle.MntExe = mntExe;
187
- if (mntNeto > 0) detalle.MntNeto = mntNeto;
188
-
189
- // Tasa de IVA - siempre informar si hay MntNeto
190
- const tasaIva = Number(totales.TasaIVA || 19);
191
-
192
- // IVA No Recuperable
193
- if (totales.IVANoRec) {
194
- detalle.IVANoRec = {
195
- CodIVANoRec: totales.IVANoRec.CodIVANoRec || 1,
196
- MntIVANoRec: Number(totales.IVANoRec.MntIVANoRec || 0),
197
- };
198
- // Con IVA no recuperable, TasaImp y MntIVA van en 0
199
- detalle.TasaImp = 0;
200
- detalle.MntIVA = 0;
201
- } else if (totales.IVAUsoComun !== undefined && totales.IVAUsoComun > 0) {
202
- // IVA Uso Común
203
- detalle.IVAUsoComun = Number(totales.IVAUsoComun);
204
- // Con IVA uso común, TasaImp y MntIVA van en 0
205
- detalle.TasaImp = 0;
206
- detalle.MntIVA = 0;
207
- } else if (mntNeto > 0) {
208
- // Caso normal: informar TasaImp y MntIVA
209
- detalle.TasaImp = tasaIva;
210
- detalle.MntIVA = mntIva;
211
- }
212
-
213
- // IVA Retenido Total (para Factura de Compra tipo 46 y sus NC/ND)
214
- // Se informa ADEMÁS del MntIVA, no en lugar de
215
- if (ivaRetTotal > 0) {
216
- detalle.IVARetTotal = ivaRetTotal;
217
- }
218
-
219
- // Otros Impuestos
220
- const otrosImpMonto = totales.OtrosImp ? Number(totales.OtrosImp.MntImp || 0) : 0;
221
- if (totales.OtrosImp) {
222
- detalle.OtrosImp = {
223
- CodImp: totales.OtrosImp.CodImp,
224
- TasaImp: totales.OtrosImp.TasaImp || 0,
225
- MntImp: otrosImpMonto,
226
- };
227
- }
228
-
229
- // Monto Total - RECALCULAR según fórmula del libro:
230
- // MntTotal = MntNeto + MntExe + MntIVA + IVANoRec + IVAUsoComun + OtrosImp - IVARetTotal
231
- const mntIvaNoRec = totales.IVANoRec ? Number(totales.IVANoRec.MntIVANoRec || 0) : 0;
232
- const ivaUsoComun = Number(totales.IVAUsoComun || 0);
233
- detalle.MntTotal = mntNeto + mntExe + mntIva + mntIvaNoRec + ivaUsoComun + otrosImpMonto - ivaRetTotal;
234
-
235
- return detalle;
236
- }
237
-
238
- /**
239
- * Calcula el RESUMEN automáticamente desde el DETALLE
240
- * @private
241
- * @param {Array} detalle - Array de documentos
242
- * @param {number} [factorProp] - Factor de proporcionalidad (default 0.6)
243
- */
244
- _calcularResumenDesdeDetalle(detalle, factorProp) {
245
- const factor = factorProp || FACTOR_PROPORCIONALIDAD;
246
- const resumenMap = new Map();
247
-
248
- for (const doc of detalle) {
249
- const tipo = doc.TpoDoc;
250
-
251
- if (!resumenMap.has(tipo)) {
252
- resumenMap.set(tipo, {
253
- TpoDoc: tipo,
254
- TotDoc: 0,
255
- TotMntExe: 0,
256
- TotMntNeto: 0,
257
- TotMntIVA: 0,
258
- TotMntTotal: 0,
259
- TotOpIVAUsoComun: 0,
260
- TotIVAUsoComun: 0,
261
- TotCredIVAUsoComun: 0,
262
- TotIVANoRec: {},
263
- TotOtrosImp: {},
264
- TotIVARetTotal: 0,
265
- });
266
- }
267
-
268
- const r = resumenMap.get(tipo);
269
- r.TotDoc += 1;
270
- r.TotMntExe += Number(doc.MntExe || 0);
271
- r.TotMntNeto += Number(doc.MntNeto || 0);
272
- r.TotMntIVA += Number(doc.MntIVA || 0);
273
- r.TotMntTotal += Number(doc.MntTotal || 0);
274
-
275
- // IVA Uso Común
276
- if (doc.IVAUsoComun) {
277
- r.TotOpIVAUsoComun += 1;
278
- r.TotIVAUsoComun += Number(doc.IVAUsoComun);
279
- r.TotCredIVAUsoComun += Math.round(Number(doc.IVAUsoComun) * factor);
280
- r.FctProp = factor;
281
- }
282
-
283
- // IVA No Recuperable
284
- if (doc.IVANoRec) {
285
- const codIVA = doc.IVANoRec.CodIVANoRec;
286
- if (!r.TotIVANoRec[codIVA]) {
287
- r.TotIVANoRec[codIVA] = {
288
- CodIVANoRec: codIVA,
289
- TotOpIVANoRec: 0,
290
- TotMntIVANoRec: 0,
291
- };
292
- }
293
- r.TotIVANoRec[codIVA].TotOpIVANoRec += 1;
294
- r.TotIVANoRec[codIVA].TotMntIVANoRec += Number(doc.IVANoRec.MntIVANoRec || 0);
295
- }
296
-
297
- // Otros Impuestos
298
- if (doc.OtrosImp) {
299
- const codImp = doc.OtrosImp.CodImp;
300
- if (!r.TotOtrosImp[codImp]) {
301
- r.TotOtrosImp[codImp] = {
302
- CodImp: codImp,
303
- TotMntImp: 0,
304
- };
305
- }
306
- r.TotOtrosImp[codImp].TotMntImp += Number(doc.OtrosImp.MntImp || 0);
307
- }
308
-
309
- // IVA Retenido Total
310
- if (doc.IVARetTotal) {
311
- r.TotIVARetTotal += Number(doc.IVARetTotal);
312
- }
313
- }
314
-
315
- // Convertir a array limpio
316
- const resumenArray = Array.from(resumenMap.values()).map((r) => {
317
- const limpio = {
318
- TpoDoc: r.TpoDoc,
319
- TotDoc: r.TotDoc,
320
- };
321
-
322
- if (r.TotMntExe > 0) limpio.TotMntExe = r.TotMntExe;
323
- if (r.TotMntNeto > 0) limpio.TotMntNeto = r.TotMntNeto;
324
- if (r.TotMntIVA > 0) limpio.TotMntIVA = r.TotMntIVA;
325
- if (r.TotMntTotal > 0) limpio.TotMntTotal = r.TotMntTotal;
326
-
327
- // IVA Uso Común
328
- if (r.TotOpIVAUsoComun > 0) {
329
- limpio.TotOpIVAUsoComun = r.TotOpIVAUsoComun;
330
- limpio.TotIVAUsoComun = r.TotIVAUsoComun;
331
- limpio.FctProp = r.FctProp;
332
- limpio.TotCredIVAUsoComun = r.TotCredIVAUsoComun;
333
- }
334
-
335
- // IVA No Recuperable
336
- const ivaNoRecArray = Object.values(r.TotIVANoRec);
337
- if (ivaNoRecArray.length > 0) {
338
- limpio.TotIVANoRec = ivaNoRecArray;
339
- }
340
-
341
- // Otros Impuestos
342
- const otrosImpArray = Object.values(r.TotOtrosImp);
343
- if (otrosImpArray.length > 0) {
344
- limpio.TotOtrosImp = otrosImpArray;
345
- }
346
-
347
- // IVA Retenido Total
348
- if (r.TotIVARetTotal > 0) {
349
- limpio.TotIVARetTotal = r.TotIVARetTotal;
350
- }
351
-
352
- return limpio;
353
- });
354
-
355
- return resumenArray;
356
- }
357
- }
358
-
359
- module.exports = LibroCompras;
3
+ /**
4
+ * LibroCompras.js - Generador de Libro de Compras para Certificación SII
5
+ *
6
+ * Construye el libro de compras a partir de los resultados del SetCompra.
7
+ *
8
+ * Documentos incluidos:
9
+ * - Facturas de Compra (46) con IVA Retenido Total
10
+ * - Notas de Crédito (61)
11
+ * - Notas de Débito (56)
12
+ *
13
+ * Maneja campos especiales:
14
+ * - IVANoRec (IVA no recuperable)
15
+ * - IVAUsoComun (IVA de uso común con factor proporcionalidad)
16
+ * - OtrosImp (otros impuestos)
17
+ * - IVARetTotal (IVA retenido total)
18
+ *
19
+ * @module dte-sii/cert/LibroCompras
20
+ */
21
+
22
+ const { LibroCompraVenta } = require('../index');
23
+
24
+ // Factor de proporcionalidad para IVA de uso común (60%)
25
+ const FACTOR_PROPORCIONALIDAD = 0.6;
26
+
27
+ /**
28
+ * @typedef {Object} LibroComprasConfig
29
+ * @property {Object} emisor - Datos del emisor
30
+ * @property {string} periodo - Período tributario (YYYY-MM)
31
+ * @property {Object} certificado - Instancia de Certificado
32
+ */
33
+
34
+ class LibroCompras {
35
+ /**
36
+ * @param {LibroComprasConfig} config
37
+ */
38
+ constructor(config) {
39
+ this.config = config;
40
+ this.emisor = config.emisor;
41
+ this.periodo = config.periodo;
42
+ this.certificado = config.certificado;
43
+ }
44
+
45
+ /**
46
+ * Genera el libro de compras desde los resultados del SetCompra
47
+ * @param {Object} setCompraResult - Resultado de SetCompra.ejecutar()
48
+ * @returns {Object} { libro, xml, detalle, resumen }
49
+ */
50
+ generar(setCompraResult) {
51
+ const { documentos } = setCompraResult;
52
+
53
+ if (!documentos || documentos.length === 0) {
54
+ throw new Error('LibroCompras: No hay documentos del SetCompra para generar libro');
55
+ }
56
+
57
+ const fechaBase = `${this.periodo}-15`;
58
+ const detalles = [];
59
+
60
+ // Procesar cada documento del set compra
61
+ for (const doc of documentos) {
62
+ const detalle = this._buildDetalle(doc, fechaBase);
63
+ detalles.push(detalle);
64
+ }
65
+
66
+ // Calcular resumen automáticamente desde el detalle
67
+ const resumen = this._calcularResumenDesdeDetalle(detalles);
68
+
69
+ // Crear libro
70
+ const libro = new LibroCompraVenta(this.certificado);
71
+ libro.setCaratula({
72
+ RutEmisorLibro: this.emisor.rut,
73
+ RutEnvia: this.certificado.rut || this.emisor.rut,
74
+ PeriodoTributario: this.periodo,
75
+ FchResol: this.emisor.fch_resol,
76
+ NroResol: this.emisor.nro_resol,
77
+ TipoOperacion: 'COMPRA',
78
+ TipoLibro: 'MENSUAL',
79
+ TipoEnvio: 'TOTAL',
80
+ });
81
+ libro.setResumen(resumen);
82
+ libro.setDetalle(detalles);
83
+ libro.generar();
84
+
85
+ return {
86
+ libro,
87
+ xml: libro.getXML(),
88
+ detalle: detalles,
89
+ resumen,
90
+ };
91
+ }
92
+
93
+ /**
94
+ * Genera el libro de compras desde los datos pre-procesados del SII (estructuras)
95
+ * @param {Object} libroComprasData - Datos del libro de compras { detalle, resumen, factorProporcionalidad }
96
+ * @param {string} periodo - Período tributario (YYYY-MM)
97
+ * @returns {Object} { libro, xml, detalle, resumen }
98
+ */
99
+ generarDesdeEstructuras(libroComprasData, periodo) {
100
+ const { detalle, resumen, factorProporcionalidad } = libroComprasData;
101
+
102
+ if (!detalle || detalle.length === 0) {
103
+ throw new Error('LibroCompras: No hay detalle en los datos del SII');
104
+ }
105
+
106
+ // Ajustar fechas al período correcto y aplicar reglas SII
107
+ const fechaBase = `${periodo}-15`;
108
+ const detalleAjustado = detalle.map(doc => {
109
+ const docAjustado = {
110
+ ...doc,
111
+ FchDoc: fechaBase,
112
+ };
113
+
114
+ // Regla SII: Cuando hay IVAUsoComun, TasaImp y MntIVA van en 0
115
+ if (doc.IVAUsoComun) {
116
+ docAjustado.TasaImp = 0;
117
+ docAjustado.MntIVA = 0;
118
+ }
119
+ // Regla SII: Cuando hay IVANoRec, TasaImp y MntIVA van en 0
120
+ else if (doc.IVANoRec) {
121
+ docAjustado.TasaImp = 0;
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
+ }
127
+ }
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
130
+ else if (doc.TasaImp !== undefined) {
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
+ }
149
+ }
150
+
151
+ return docAjustado;
152
+ });
153
+
154
+ // Recalcular resumen desde el detalle ajustado
155
+ const resumenRecalculado = this._calcularResumenDesdeDetalle(detalleAjustado, factorProporcionalidad);
156
+
157
+ // Crear libro
158
+ const libro = new LibroCompraVenta(this.certificado);
159
+ libro.setCaratula({
160
+ RutEmisorLibro: this.emisor.rut,
161
+ RutEnvia: this.certificado.rut || this.emisor.rut,
162
+ PeriodoTributario: periodo,
163
+ FchResol: this.emisor.fch_resol,
164
+ NroResol: this.emisor.nro_resol,
165
+ TipoOperacion: 'COMPRA',
166
+ TipoLibro: 'MENSUAL',
167
+ TipoEnvio: 'TOTAL',
168
+ });
169
+ libro.setResumen(resumenRecalculado);
170
+ libro.setDetalle(detalleAjustado);
171
+ libro.generar();
172
+
173
+ return {
174
+ libro,
175
+ xml: libro.getXML(),
176
+ detalle: detalleAjustado,
177
+ resumen: resumenRecalculado,
178
+ };
179
+ }
180
+
181
+ /**
182
+ * Construye un registro de detalle desde un documento
183
+ * @private
184
+ */
185
+ _buildDetalle(doc, fechaBase) {
186
+ const tipoDte = doc.tipoDte;
187
+ const totales = doc.totales || {};
188
+
189
+ // En Factura de Compra (46), el emisor ES el receptor del documento
190
+ // porque nosotros RECIBIMOS la factura de compra
191
+ const rutDoc = this.emisor.rut;
192
+ const rznSoc = this.emisor.razon_social;
193
+
194
+ const detalle = {
195
+ TpoDoc: tipoDte,
196
+ NroDoc: doc.folio,
197
+ FchDoc: fechaBase,
198
+ RUTDoc: rutDoc,
199
+ RznSoc: rznSoc,
200
+ };
201
+
202
+ // Montos básicos
203
+ const mntNeto = Number(totales.MntNeto || 0);
204
+ const mntExe = Number(totales.MntExe || 0);
205
+ const mntIva = Number(totales.IVA || 0);
206
+ const ivaRetTotal = Number(totales.IVARetTotal || 0);
207
+
208
+ if (mntExe > 0) detalle.MntExe = mntExe;
209
+ if (mntNeto > 0) detalle.MntNeto = mntNeto;
210
+
211
+ // Tasa de IVA - siempre informar si hay MntNeto
212
+ const tasaIva = Number(totales.TasaIVA || 19);
213
+
214
+ // IVA No Recuperable
215
+ if (totales.IVANoRec) {
216
+ detalle.IVANoRec = {
217
+ CodIVANoRec: totales.IVANoRec.CodIVANoRec || 1,
218
+ MntIVANoRec: Number(totales.IVANoRec.MntIVANoRec || 0),
219
+ };
220
+ // Con IVA no recuperable, TasaImp y MntIVA van en 0
221
+ detalle.TasaImp = 0;
222
+ detalle.MntIVA = 0;
223
+ } else if (totales.IVAUsoComun !== undefined && totales.IVAUsoComun > 0) {
224
+ // IVA Uso Común
225
+ detalle.IVAUsoComun = Number(totales.IVAUsoComun);
226
+ // Con IVA uso común, TasaImp y MntIVA van en 0
227
+ detalle.TasaImp = 0;
228
+ detalle.MntIVA = 0;
229
+ } else if (mntNeto > 0) {
230
+ // Caso normal: informar TasaImp y MntIVA
231
+ detalle.TasaImp = tasaIva;
232
+ detalle.MntIVA = mntIva;
233
+ }
234
+
235
+ // IVA Retenido Total (para Factura de Compra tipo 46 y sus NC/ND)
236
+ // Se informa ADEMÁS del MntIVA, no en lugar de
237
+ if (ivaRetTotal > 0) {
238
+ detalle.IVARetTotal = ivaRetTotal;
239
+ }
240
+
241
+ // Otros Impuestos
242
+ const otrosImpMonto = totales.OtrosImp ? Number(totales.OtrosImp.MntImp || 0) : 0;
243
+ if (totales.OtrosImp) {
244
+ detalle.OtrosImp = {
245
+ CodImp: totales.OtrosImp.CodImp,
246
+ TasaImp: totales.OtrosImp.TasaImp || 0,
247
+ MntImp: otrosImpMonto,
248
+ };
249
+ }
250
+
251
+ // Monto Total - RECALCULAR según fórmula del libro:
252
+ // MntTotal = MntNeto + MntExe + MntIVA + IVANoRec + IVAUsoComun + OtrosImp - IVARetTotal
253
+ const mntIvaNoRec = totales.IVANoRec ? Number(totales.IVANoRec.MntIVANoRec || 0) : 0;
254
+ const ivaUsoComun = Number(totales.IVAUsoComun || 0);
255
+ detalle.MntTotal = mntNeto + mntExe + mntIva + mntIvaNoRec + ivaUsoComun + otrosImpMonto - ivaRetTotal;
256
+
257
+ return detalle;
258
+ }
259
+
260
+ /**
261
+ * Calcula el RESUMEN automáticamente desde el DETALLE
262
+ * @private
263
+ * @param {Array} detalle - Array de documentos
264
+ * @param {number} [factorProp] - Factor de proporcionalidad (default 0.6)
265
+ */
266
+ _calcularResumenDesdeDetalle(detalle, factorProp) {
267
+ const factor = factorProp || FACTOR_PROPORCIONALIDAD;
268
+ const resumenMap = new Map();
269
+
270
+ for (const doc of detalle) {
271
+ const tipo = doc.TpoDoc;
272
+
273
+ if (!resumenMap.has(tipo)) {
274
+ resumenMap.set(tipo, {
275
+ TpoDoc: tipo,
276
+ TotDoc: 0,
277
+ TotMntExe: 0,
278
+ TotMntNeto: 0,
279
+ TotMntIVA: 0,
280
+ TotMntTotal: 0,
281
+ TotOpIVAUsoComun: 0,
282
+ TotIVAUsoComun: 0,
283
+ TotCredIVAUsoComun: 0,
284
+ TotIVANoRec: {},
285
+ TotOtrosImp: {},
286
+ TotIVARetTotal: 0,
287
+ TotMntNoFact: 0,
288
+ });
289
+ }
290
+
291
+ const r = resumenMap.get(tipo);
292
+ r.TotDoc += 1;
293
+ r.TotMntExe += Number(doc.MntExe || 0);
294
+ r.TotMntNeto += Number(doc.MntNeto || 0);
295
+ r.TotMntIVA += Number(doc.MntIVA || 0);
296
+ r.TotMntTotal += Number(doc.MntTotal || 0);
297
+ r.TotMntNoFact += Number(doc.MntNoFact || 0);
298
+
299
+ // IVA Uso Común
300
+ if (doc.IVAUsoComun) {
301
+ r.TotOpIVAUsoComun += 1;
302
+ r.TotIVAUsoComun += Number(doc.IVAUsoComun);
303
+ r.TotCredIVAUsoComun += Math.round(Number(doc.IVAUsoComun) * factor);
304
+ r.FctProp = factor;
305
+ }
306
+
307
+ // IVA No Recuperable
308
+ if (doc.IVANoRec) {
309
+ const codIVA = doc.IVANoRec.CodIVANoRec;
310
+ if (!r.TotIVANoRec[codIVA]) {
311
+ r.TotIVANoRec[codIVA] = {
312
+ CodIVANoRec: codIVA,
313
+ TotOpIVANoRec: 0,
314
+ TotMntIVANoRec: 0,
315
+ };
316
+ }
317
+ r.TotIVANoRec[codIVA].TotOpIVANoRec += 1;
318
+ r.TotIVANoRec[codIVA].TotMntIVANoRec += Number(doc.IVANoRec.MntIVANoRec || 0);
319
+ }
320
+
321
+ // Otros Impuestos
322
+ if (doc.OtrosImp) {
323
+ const codImp = doc.OtrosImp.CodImp;
324
+ if (!r.TotOtrosImp[codImp]) {
325
+ r.TotOtrosImp[codImp] = {
326
+ CodImp: codImp,
327
+ TotMntImp: 0,
328
+ };
329
+ }
330
+ r.TotOtrosImp[codImp].TotMntImp += Number(doc.OtrosImp.MntImp || 0);
331
+ }
332
+
333
+ // IVA Retenido Total
334
+ if (doc.IVARetTotal) {
335
+ r.TotIVARetTotal += Number(doc.IVARetTotal);
336
+ }
337
+ }
338
+
339
+ // Convertir a array limpio
340
+ const resumenArray = Array.from(resumenMap.values()).map((r) => {
341
+ const limpio = {
342
+ TpoDoc: r.TpoDoc,
343
+ TotDoc: r.TotDoc,
344
+ };
345
+
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
+ }
366
+
367
+ // IVA Uso Común
368
+ if (r.TotOpIVAUsoComun > 0) {
369
+ limpio.TotOpIVAUsoComun = r.TotOpIVAUsoComun;
370
+ limpio.TotIVAUsoComun = r.TotIVAUsoComun;
371
+ limpio.FctProp = r.FctProp;
372
+ limpio.TotCredIVAUsoComun = r.TotCredIVAUsoComun;
373
+ }
374
+
375
+ // IVA No Recuperable
376
+ const ivaNoRecArray = Object.values(r.TotIVANoRec);
377
+ if (ivaNoRecArray.length > 0) {
378
+ limpio.TotIVANoRec = ivaNoRecArray;
379
+ }
380
+
381
+ // Otros Impuestos
382
+ const otrosImpArray = Object.values(r.TotOtrosImp);
383
+ if (otrosImpArray.length > 0) {
384
+ limpio.TotOtrosImp = otrosImpArray;
385
+ }
386
+
387
+ // IVA Retenido Total
388
+ if (r.TotIVARetTotal > 0) {
389
+ limpio.TotIVARetTotal = r.TotIVARetTotal;
390
+ }
391
+
392
+ return limpio;
393
+ });
394
+
395
+ return resumenArray;
396
+ }
397
+ }
398
+
399
+ module.exports = LibroCompras;