@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
package/utils/c14n.js
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
// Copyright (c) 2026 Devlas SpA — https://devlas.cl
|
|
2
|
+
// Licencia MIT. Ver archivo LICENSE para mas detalles.
|
|
3
|
+
/**
|
|
4
|
+
* Módulo de Canonicalización XML (C14N)
|
|
5
|
+
*
|
|
6
|
+
* Funciones compartidas para serialización canónica de XML
|
|
7
|
+
* según el estándar W3C Canonical XML Version 1.0.
|
|
8
|
+
*
|
|
9
|
+
* Usado por: DTE.js, Signer.js, LibroBase.js, ConsumoFolio.js
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Escapa atributos XML según C14N
|
|
14
|
+
* @param {string} value - Valor del atributo
|
|
15
|
+
* @returns {string} - Valor escapado
|
|
16
|
+
*/
|
|
17
|
+
function escapeAttr(value) {
|
|
18
|
+
return (value || '')
|
|
19
|
+
.replace(/&/g, '&')
|
|
20
|
+
.replace(/</g, '<')
|
|
21
|
+
.replace(/"/g, '"')
|
|
22
|
+
.replace(/\t/g, '	')
|
|
23
|
+
.replace(/\n/g, '
')
|
|
24
|
+
.replace(/\r/g, '
');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Escapa contenido de texto XML según C14N
|
|
29
|
+
* @param {string} text - Contenido de texto
|
|
30
|
+
* @returns {string} - Texto escapado
|
|
31
|
+
*/
|
|
32
|
+
function escapeText(text) {
|
|
33
|
+
return (text || '')
|
|
34
|
+
.replace(/&/g, '&')
|
|
35
|
+
.replace(/</g, '<')
|
|
36
|
+
.replace(/>/g, '>')
|
|
37
|
+
.replace(/\r/g, '
');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Corrige entidades en contenido XML (apóstrofes y comillas)
|
|
42
|
+
* @param {string} xml - XML a procesar
|
|
43
|
+
* @returns {string} - XML con entidades corregidas
|
|
44
|
+
*/
|
|
45
|
+
function fixEntities(xml) {
|
|
46
|
+
let out = '';
|
|
47
|
+
let inContent = false;
|
|
48
|
+
for (let i = 0; i < xml.length; i++) {
|
|
49
|
+
const ch = xml[i];
|
|
50
|
+
if (ch === '>') inContent = true;
|
|
51
|
+
if (ch === '<') inContent = false;
|
|
52
|
+
if (inContent && ch === "'") { out += '''; continue; }
|
|
53
|
+
if (inContent && ch === '"') { out += '"'; continue; }
|
|
54
|
+
out += ch;
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Serializa un nodo DOM a XML canónico
|
|
61
|
+
* @param {Node} node - Nodo DOM a serializar
|
|
62
|
+
* @param {Map} inheritedNs - Namespaces heredados
|
|
63
|
+
* @param {Object} options - Opciones
|
|
64
|
+
* @param {boolean} [options.omitNsFromChildren=false] - No incluir xmlns en hijos
|
|
65
|
+
* @returns {string} - XML serializado
|
|
66
|
+
*/
|
|
67
|
+
function serializeNode(node, inheritedNs, options = {}) {
|
|
68
|
+
const { omitNsFromChildren = false } = options;
|
|
69
|
+
|
|
70
|
+
// Nodo de texto
|
|
71
|
+
if (node.nodeType === 3) {
|
|
72
|
+
return escapeText(node.nodeValue || '');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Solo elementos
|
|
76
|
+
if (node.nodeType !== 1) return '';
|
|
77
|
+
|
|
78
|
+
const tagName = node.tagName || node.nodeName;
|
|
79
|
+
const currentNs = new Map(inheritedNs);
|
|
80
|
+
let result = `<${tagName}`;
|
|
81
|
+
|
|
82
|
+
// Recoger y ordenar atributos
|
|
83
|
+
const attrs = [];
|
|
84
|
+
for (let i = 0; i < node.attributes.length; i++) {
|
|
85
|
+
const attr = node.attributes[i];
|
|
86
|
+
|
|
87
|
+
// Manejar namespaces
|
|
88
|
+
if (attr.name.startsWith('xmlns')) {
|
|
89
|
+
// Si ya está en el contexto heredado con el mismo valor, omitir
|
|
90
|
+
if (currentNs.get(attr.name) === attr.value) continue;
|
|
91
|
+
// Opción: omitir xmlns de hijos
|
|
92
|
+
if (omitNsFromChildren && (attr.name === 'xmlns' || attr.name === 'xmlns:xsi')) continue;
|
|
93
|
+
currentNs.set(attr.name, attr.value);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
attrs.push({ name: attr.name, value: attr.value });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Ordenar: xmlns primero, luego xmlns:*, luego otros alfabéticamente
|
|
100
|
+
attrs.sort((a, b) => {
|
|
101
|
+
if (a.name === 'xmlns') return -1;
|
|
102
|
+
if (b.name === 'xmlns') return 1;
|
|
103
|
+
const aXmlns = a.name.startsWith('xmlns:');
|
|
104
|
+
const bXmlns = b.name.startsWith('xmlns:');
|
|
105
|
+
if (aXmlns && !bXmlns) return -1;
|
|
106
|
+
if (!aXmlns && bXmlns) return 1;
|
|
107
|
+
return a.name.localeCompare(b.name);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
for (const attr of attrs) {
|
|
111
|
+
result += ` ${attr.name}="${escapeAttr(attr.value)}"`;
|
|
112
|
+
}
|
|
113
|
+
result += '>';
|
|
114
|
+
|
|
115
|
+
// Serializar hijos
|
|
116
|
+
for (let i = 0; i < node.childNodes.length; i++) {
|
|
117
|
+
result += serializeNode(node.childNodes[i], currentNs, options);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
result += `</${tagName}>`;
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Serializa un elemento con sus hijos para C14N (versión simplificada para Libros)
|
|
126
|
+
* @param {Element} elemento - Elemento DOM
|
|
127
|
+
* @param {Object} options - Opciones
|
|
128
|
+
* @param {boolean} [options.omitRootNs=false] - Omitir xmlns/xmlns:xsi del elemento
|
|
129
|
+
* @returns {string} - XML serializado
|
|
130
|
+
*/
|
|
131
|
+
function serializeElement(elemento, options = {}) {
|
|
132
|
+
const { omitRootNs = false } = options;
|
|
133
|
+
const tagName = elemento.tagName || elemento.nodeName;
|
|
134
|
+
let result = `<${tagName}`;
|
|
135
|
+
|
|
136
|
+
// Recoger y ordenar atributos
|
|
137
|
+
const attrs = [];
|
|
138
|
+
for (let i = 0; i < elemento.attributes.length; i++) {
|
|
139
|
+
const attr = elemento.attributes[i];
|
|
140
|
+
// Opcionalmente omitir xmlns del root
|
|
141
|
+
if (omitRootNs && (attr.name === 'xmlns' || attr.name === 'xmlns:xsi')) continue;
|
|
142
|
+
attrs.push({ name: attr.name, value: attr.value });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
attrs.sort((a, b) => a.name.localeCompare(b.name));
|
|
146
|
+
|
|
147
|
+
for (const attr of attrs) {
|
|
148
|
+
result += ` ${attr.name}="${escapeAttr(attr.value)}"`;
|
|
149
|
+
}
|
|
150
|
+
result += '>';
|
|
151
|
+
|
|
152
|
+
// Serializar hijos
|
|
153
|
+
for (let i = 0; i < elemento.childNodes.length; i++) {
|
|
154
|
+
const child = elemento.childNodes[i];
|
|
155
|
+
if (child.nodeType === 1) {
|
|
156
|
+
result += serializeElement(child, { omitRootNs: false });
|
|
157
|
+
} else if (child.nodeType === 3) {
|
|
158
|
+
result += escapeText(child.nodeValue);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
result += `</${tagName}>`;
|
|
163
|
+
return result;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Canonicaliza un elemento raíz con namespaces específicos
|
|
168
|
+
* @param {Element} elemento - Elemento DOM raíz
|
|
169
|
+
* @param {Object} config - Configuración
|
|
170
|
+
* @param {string} config.tagName - Nombre del tag raíz
|
|
171
|
+
* @param {string} config.id - ID del elemento
|
|
172
|
+
* @param {string} config.xmlns - Namespace por defecto
|
|
173
|
+
* @param {string} [config.xmlnsXsi] - Namespace XSI (opcional)
|
|
174
|
+
* @returns {string} - XML canonicalizado
|
|
175
|
+
*/
|
|
176
|
+
function canonicalizeRoot(elemento, config) {
|
|
177
|
+
const { tagName, id, xmlns, xmlnsXsi } = config;
|
|
178
|
+
|
|
179
|
+
let c14n = `<${tagName}`;
|
|
180
|
+
if (xmlns) c14n += ` xmlns="${xmlns}"`;
|
|
181
|
+
if (xmlnsXsi) c14n += ` xmlns:xsi="${xmlnsXsi}"`;
|
|
182
|
+
if (id) c14n += ` ID="${id}"`;
|
|
183
|
+
c14n += '>';
|
|
184
|
+
|
|
185
|
+
// Serializar hijos (sin heredar xmlns del padre)
|
|
186
|
+
for (let i = 0; i < elemento.childNodes.length; i++) {
|
|
187
|
+
const child = elemento.childNodes[i];
|
|
188
|
+
if (child.nodeType === 1) {
|
|
189
|
+
c14n += serializeElement(child, { omitRootNs: true });
|
|
190
|
+
} else if (child.nodeType === 3) {
|
|
191
|
+
c14n += escapeText(child.nodeValue);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
c14n += `</${tagName}>`;
|
|
196
|
+
return c14n;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ============================================
|
|
200
|
+
// XML-DSig SignedInfo / Signature Helpers
|
|
201
|
+
// ============================================
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Constantes XML-DSig
|
|
205
|
+
*/
|
|
206
|
+
const XMLDSIG_NS = 'http://www.w3.org/2000/09/xmldsig#';
|
|
207
|
+
const C14N_ALGORITHM = 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315';
|
|
208
|
+
const RSA_SHA1_ALGORITHM = 'http://www.w3.org/2000/09/xmldsig#rsa-sha1';
|
|
209
|
+
const SHA1_ALGORITHM = 'http://www.w3.org/2000/09/xmldsig#sha1';
|
|
210
|
+
const ENVELOPED_SIGNATURE = 'http://www.w3.org/2000/09/xmldsig#enveloped-signature';
|
|
211
|
+
const XSI_NS = 'http://www.w3.org/2001/XMLSchema-instance';
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Construye el elemento SignedInfo para firma XML-DSig
|
|
215
|
+
* @param {string} refId - ID del elemento referenciado (con o sin #)
|
|
216
|
+
* @param {string} digestValue - DigestValue calculado en base64
|
|
217
|
+
* @param {Object} options - Opciones de construcción
|
|
218
|
+
* @param {boolean} [options.expandTags=false] - Expandir tags vacíos (para cálculo de firma)
|
|
219
|
+
* @param {boolean} [options.includeXsi=true] - Incluir xmlns:xsi
|
|
220
|
+
* @returns {string} - SignedInfo XML
|
|
221
|
+
*/
|
|
222
|
+
function buildSignedInfo(refId, digestValue, options = {}) {
|
|
223
|
+
const { expandTags = false, includeXsi = true } = options;
|
|
224
|
+
|
|
225
|
+
// Normalizar refId (agregar # si no lo tiene)
|
|
226
|
+
const uri = refId.startsWith('#') ? refId : `#${refId}`;
|
|
227
|
+
|
|
228
|
+
const xmlns = `xmlns="${XMLDSIG_NS}"`;
|
|
229
|
+
const xsi = includeXsi ? ` xmlns:xsi="${XSI_NS}"` : '';
|
|
230
|
+
|
|
231
|
+
if (expandTags) {
|
|
232
|
+
// Tags expandidos: <Tag></Tag> - usado para calcular firma
|
|
233
|
+
return `<SignedInfo ${xmlns}${xsi}><CanonicalizationMethod Algorithm="${C14N_ALGORITHM}"></CanonicalizationMethod><SignatureMethod Algorithm="${RSA_SHA1_ALGORITHM}"></SignatureMethod><Reference URI="${uri}"><Transforms><Transform Algorithm="${ENVELOPED_SIGNATURE}"></Transform></Transforms><DigestMethod Algorithm="${SHA1_ALGORITHM}"></DigestMethod><DigestValue>${digestValue}</DigestValue></Reference></SignedInfo>`;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Tags compactos: <Tag/> - usado para guardar en XML
|
|
237
|
+
return `<SignedInfo ${xmlns}${xsi}><CanonicalizationMethod Algorithm="${C14N_ALGORITHM}"/><SignatureMethod Algorithm="${RSA_SHA1_ALGORITHM}"/><Reference URI="${uri}"><Transforms><Transform Algorithm="${ENVELOPED_SIGNATURE}"/></Transforms><DigestMethod Algorithm="${SHA1_ALGORITHM}"/><DigestValue>${digestValue}</DigestValue></Reference></SignedInfo>`;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Construye la Signature completa XML-DSig
|
|
242
|
+
* @param {string} signedInfo - SignedInfo XML (formato compacto)
|
|
243
|
+
* @param {string} signatureValue - Valor de firma en base64
|
|
244
|
+
* @param {Object} keyInfo - Información de la clave
|
|
245
|
+
* @param {string} keyInfo.modulus - Modulus RSA en base64
|
|
246
|
+
* @param {string} keyInfo.exponent - Exponent RSA en base64
|
|
247
|
+
* @param {string} keyInfo.certificate - Certificado X509 en base64
|
|
248
|
+
* @returns {string} - Signature XML completa
|
|
249
|
+
*/
|
|
250
|
+
function buildSignature(signedInfo, signatureValue, keyInfo) {
|
|
251
|
+
const { modulus, exponent, certificate } = keyInfo;
|
|
252
|
+
|
|
253
|
+
return `<Signature xmlns="${XMLDSIG_NS}">${signedInfo}<SignatureValue>\n${signatureValue}</SignatureValue><KeyInfo><KeyValue><RSAKeyValue><Modulus>${modulus}</Modulus><Exponent>${exponent}</Exponent></RSAKeyValue></KeyValue><X509Data><X509Certificate>${certificate}</X509Certificate></X509Data></KeyInfo></Signature>`;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
module.exports = {
|
|
257
|
+
// Escape/Text functions
|
|
258
|
+
escapeAttr,
|
|
259
|
+
escapeText,
|
|
260
|
+
fixEntities,
|
|
261
|
+
// Serialization
|
|
262
|
+
serializeNode,
|
|
263
|
+
serializeElement,
|
|
264
|
+
canonicalizeRoot,
|
|
265
|
+
// XML-DSig helpers
|
|
266
|
+
buildSignedInfo,
|
|
267
|
+
buildSignature,
|
|
268
|
+
// Constantes
|
|
269
|
+
XMLDSIG_NS,
|
|
270
|
+
C14N_ALGORITHM,
|
|
271
|
+
RSA_SHA1_ALGORITHM,
|
|
272
|
+
SHA1_ALGORITHM,
|
|
273
|
+
ENVELOPED_SIGNATURE,
|
|
274
|
+
XSI_NS,
|
|
275
|
+
};
|
package/utils/calculo.js
ADDED
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
// Copyright (c) 2026 Devlas SpA — https://devlas.cl
|
|
2
|
+
// Licencia MIT. Ver archivo LICENSE para mas detalles.
|
|
3
|
+
/**
|
|
4
|
+
* Utilidades de Cálculo
|
|
5
|
+
*
|
|
6
|
+
* Funciones centralizadas para cálculo de totales y construcción de detalle DTE.
|
|
7
|
+
* Estas funciones son reutilizables tanto en certificación como en producción.
|
|
8
|
+
*
|
|
9
|
+
* @module dte-sii/utils/calculo
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// Usar la constante centralizada de constants.js
|
|
13
|
+
const { TASA_IVA } = require('./constants');
|
|
14
|
+
|
|
15
|
+
// Alias para compatibilidad con código existente
|
|
16
|
+
const TASA_IVA_DEFAULT = TASA_IVA;
|
|
17
|
+
|
|
18
|
+
// ============================================
|
|
19
|
+
// FORMATEO
|
|
20
|
+
// ============================================
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Formatea un número con decimales fijos
|
|
24
|
+
* @param {number} value - Valor a formatear
|
|
25
|
+
* @param {number} [decimals=6] - Cantidad de decimales
|
|
26
|
+
* @returns {string}
|
|
27
|
+
*/
|
|
28
|
+
function formatDecimal(value, decimals = 6) {
|
|
29
|
+
return Number(value).toFixed(decimals);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Calcula el monto de un ítem con descuento
|
|
34
|
+
* @param {number} cantidad - Cantidad de unidades
|
|
35
|
+
* @param {number} precio - Precio unitario
|
|
36
|
+
* @param {number} [descuentoPct=0] - Porcentaje de descuento
|
|
37
|
+
* @returns {{ base: number, descuentoMonto: number, montoItem: number }}
|
|
38
|
+
*/
|
|
39
|
+
function calcularMontoItem(cantidad, precio, descuentoPct = 0) {
|
|
40
|
+
const qty = Number(cantidad || 1);
|
|
41
|
+
const prc = Number(precio || 0);
|
|
42
|
+
const base = Math.round(qty * prc);
|
|
43
|
+
const descuentoMonto = descuentoPct > 0 ? Math.round(base * (descuentoPct / 100)) : 0;
|
|
44
|
+
const montoItem = base - descuentoMonto;
|
|
45
|
+
return { base, descuentoMonto, montoItem };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ============================================
|
|
49
|
+
// CÁLCULO DE TOTALES
|
|
50
|
+
// ============================================
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Calcula totales desde un array de items
|
|
54
|
+
* Función unificada que maneja todos los tipos de DTE
|
|
55
|
+
*
|
|
56
|
+
* @param {Array} items - Array de items con { cantidad, precio, exento?, descuentoPct? }
|
|
57
|
+
* @param {Object} [options] - Opciones de cálculo
|
|
58
|
+
* @param {number} [options.tasaIva=19] - Tasa de IVA
|
|
59
|
+
* @param {number} [options.descuentoGlobalPct=0] - Descuento global en porcentaje
|
|
60
|
+
* @param {boolean} [options.preciosNetos=true] - Si los precios son netos (sin IVA)
|
|
61
|
+
* @param {boolean} [options.soloExento=false] - Si el documento es solo exento (ej: tipo 34)
|
|
62
|
+
* @param {boolean} [options.conRetencion=false] - Si aplica retención total IVA (ej: tipo 46)
|
|
63
|
+
* @param {number} [options.tipoImpRetencion=15] - Tipo de impuesto para retención
|
|
64
|
+
* @returns {{ totales: Object, descuentoGlobalMonto: number }}
|
|
65
|
+
*/
|
|
66
|
+
function calcularTotalesDesdeItems(items, optionsOrDescuento = {}) {
|
|
67
|
+
// Compatibilidad: si el segundo parámetro es número, es descuentoGlobalPct (API antigua)
|
|
68
|
+
const options = typeof optionsOrDescuento === 'number' || optionsOrDescuento === null
|
|
69
|
+
? { descuentoGlobalPct: optionsOrDescuento || 0 }
|
|
70
|
+
: (optionsOrDescuento || {});
|
|
71
|
+
|
|
72
|
+
const {
|
|
73
|
+
tasaIva = TASA_IVA_DEFAULT,
|
|
74
|
+
descuentoGlobalPct = 0,
|
|
75
|
+
preciosNetos = true,
|
|
76
|
+
soloExento = false,
|
|
77
|
+
conRetencion = false,
|
|
78
|
+
tipoImpRetencion = 15,
|
|
79
|
+
} = options;
|
|
80
|
+
|
|
81
|
+
let mntAfecto = 0;
|
|
82
|
+
let mntExento = 0;
|
|
83
|
+
|
|
84
|
+
(items || []).forEach((item) => {
|
|
85
|
+
const { montoItem } = calcularMontoItem(
|
|
86
|
+
item.cantidad,
|
|
87
|
+
item.precio,
|
|
88
|
+
item.descuentoPct
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
if (item.exento || soloExento) {
|
|
92
|
+
mntExento += montoItem;
|
|
93
|
+
} else {
|
|
94
|
+
mntAfecto += montoItem;
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Aplicar descuento global solo a afectos
|
|
99
|
+
const descuentoGlobalMonto = descuentoGlobalPct > 0
|
|
100
|
+
? Math.round(mntAfecto * (descuentoGlobalPct / 100))
|
|
101
|
+
: 0;
|
|
102
|
+
|
|
103
|
+
// Calcular neto
|
|
104
|
+
let mntNeto;
|
|
105
|
+
if (preciosNetos) {
|
|
106
|
+
mntNeto = Math.max(0, mntAfecto - descuentoGlobalMonto);
|
|
107
|
+
} else {
|
|
108
|
+
// Precios incluyen IVA - extraer neto
|
|
109
|
+
const afectoNeto = mntAfecto - descuentoGlobalMonto;
|
|
110
|
+
mntNeto = afectoNeto > 0 ? Math.round(afectoNeto / (1 + (tasaIva / 100))) : 0;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Calcular IVA
|
|
114
|
+
const iva = mntNeto > 0 ? Math.round(mntNeto * (tasaIva / 100)) : 0;
|
|
115
|
+
|
|
116
|
+
// Calcular total
|
|
117
|
+
// Con retención: MntTotal = MntNeto + MntExe (IVA se cancela con retención)
|
|
118
|
+
// Sin retención: MntTotal = MntNeto + IVA + MntExe
|
|
119
|
+
const mntTotal = conRetencion
|
|
120
|
+
? mntNeto + mntExento
|
|
121
|
+
: mntNeto + iva + mntExento;
|
|
122
|
+
|
|
123
|
+
// Construir objeto de totales en orden SII
|
|
124
|
+
const totales = {};
|
|
125
|
+
if (mntNeto > 0) totales.MntNeto = mntNeto;
|
|
126
|
+
if (mntExento > 0) totales.MntExe = mntExento;
|
|
127
|
+
if (mntNeto > 0) totales.TasaIVA = tasaIva;
|
|
128
|
+
if (mntNeto > 0) totales.IVA = iva;
|
|
129
|
+
|
|
130
|
+
// Retención de IVA (para Factura de Compra tipo 46)
|
|
131
|
+
if (conRetencion && iva > 0) {
|
|
132
|
+
totales.ImptoReten = [{
|
|
133
|
+
TipoImp: tipoImpRetencion,
|
|
134
|
+
TasaImp: tasaIva,
|
|
135
|
+
MontoImp: iva,
|
|
136
|
+
}];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
totales.MntTotal = mntTotal;
|
|
140
|
+
|
|
141
|
+
return { totales, descuentoGlobalMonto };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Calcula totales desde un array de detalle ya construido
|
|
146
|
+
* Útil para Guías de Despacho y otros casos donde el detalle ya está armado
|
|
147
|
+
*
|
|
148
|
+
* @param {Array} detalle - Array de líneas de detalle con { MontoItem, IndExe? }
|
|
149
|
+
* @param {Object} [options] - Opciones de cálculo
|
|
150
|
+
* @param {number} [options.tasaIva=19] - Tasa de IVA
|
|
151
|
+
* @param {boolean} [options.preciosNetos=true] - Si los precios son netos
|
|
152
|
+
* @param {boolean} [options.conRetencion=false] - Si aplica retención de IVA
|
|
153
|
+
* @param {boolean} [options.sinValores=false] - Si es traslado sin valores (IndTraslado=5)
|
|
154
|
+
* @returns {Object} Totales para el DTE
|
|
155
|
+
*/
|
|
156
|
+
function calcularTotalesDesdeDetalle(detalle, options = {}) {
|
|
157
|
+
const {
|
|
158
|
+
tasaIva = TASA_IVA_DEFAULT,
|
|
159
|
+
preciosNetos = true,
|
|
160
|
+
conRetencion = false,
|
|
161
|
+
sinValores = false,
|
|
162
|
+
} = options;
|
|
163
|
+
|
|
164
|
+
// Traslado sin valores
|
|
165
|
+
if (sinValores) {
|
|
166
|
+
return { TasaIVA: tasaIva, MntTotal: 0 };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
let mntBruto = 0;
|
|
170
|
+
let mntExento = 0;
|
|
171
|
+
|
|
172
|
+
(detalle || []).forEach((det) => {
|
|
173
|
+
const monto = Number(det.MontoItem || 0);
|
|
174
|
+
if (det.IndExe === 1) {
|
|
175
|
+
mntExento += monto;
|
|
176
|
+
} else {
|
|
177
|
+
mntBruto += monto;
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
if (mntBruto === 0 && mntExento === 0) {
|
|
182
|
+
return { TasaIVA: tasaIva, MntTotal: 0 };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const mntNeto = preciosNetos
|
|
186
|
+
? mntBruto
|
|
187
|
+
: (mntBruto > 0 ? Math.round(mntBruto / (1 + (tasaIva / 100))) : 0);
|
|
188
|
+
|
|
189
|
+
const iva = mntNeto > 0 ? Math.round(mntNeto * (tasaIva / 100)) : 0;
|
|
190
|
+
|
|
191
|
+
const mntTotal = conRetencion
|
|
192
|
+
? mntNeto + mntExento
|
|
193
|
+
: mntNeto + iva + mntExento;
|
|
194
|
+
|
|
195
|
+
const totales = {};
|
|
196
|
+
if (mntNeto > 0) totales.MntNeto = mntNeto;
|
|
197
|
+
if (mntExento > 0) totales.MntExe = mntExento;
|
|
198
|
+
if (mntNeto > 0) totales.TasaIVA = tasaIva;
|
|
199
|
+
if (mntNeto > 0) totales.IVA = iva;
|
|
200
|
+
|
|
201
|
+
if (conRetencion && iva > 0) {
|
|
202
|
+
totales.ImptoReten = [{
|
|
203
|
+
TipoImp: 15,
|
|
204
|
+
TasaImp: tasaIva,
|
|
205
|
+
MontoImp: iva,
|
|
206
|
+
}];
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
totales.MntTotal = mntTotal;
|
|
210
|
+
|
|
211
|
+
return totales;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ============================================
|
|
215
|
+
// CONSTRUCCIÓN DE DETALLE
|
|
216
|
+
// ============================================
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Construye array de detalle para DTE desde items
|
|
220
|
+
* Función unificada que maneja todos los tipos de documentos
|
|
221
|
+
*
|
|
222
|
+
* @param {Array} items - Items con { nombre, cantidad?, precio?, unidad?, exento?, descuentoPct? }
|
|
223
|
+
* @param {Object} [options] - Opciones de construcción
|
|
224
|
+
* @param {boolean} [options.allowIndExe=true] - Agregar IndExe=1 para items exentos (default: true para compatibilidad)
|
|
225
|
+
* @param {number} [options.codImpAdic=null] - Código de impuesto adicional (ej: 15 para F. Compra)
|
|
226
|
+
* @param {boolean} [options.forcePriced=false] - Forzar QtyItem/PrcItem aunque precio sea 0
|
|
227
|
+
* @param {boolean} [options.includeUnidad=false] - Incluir UnmdItem siempre
|
|
228
|
+
* @param {Function} [options.sanitize=null] - Función para sanitizar texto
|
|
229
|
+
* @returns {Array} Detalle formateado para DTE
|
|
230
|
+
*/
|
|
231
|
+
function buildDetalle(items, options = {}) {
|
|
232
|
+
const {
|
|
233
|
+
allowIndExe = true, // Por defecto true para mantener compatibilidad con código original
|
|
234
|
+
codImpAdic = null,
|
|
235
|
+
forcePriced = false,
|
|
236
|
+
includeUnidad = false,
|
|
237
|
+
sanitize = (v) => String(v || '').trim(),
|
|
238
|
+
} = options;
|
|
239
|
+
|
|
240
|
+
return (items || []).map((item, idx) => {
|
|
241
|
+
const qty = Number(item.cantidad ?? 1);
|
|
242
|
+
const prc = Number(item.precio ?? 0);
|
|
243
|
+
const descuentoPct = Number(item.descuentoPct || 0);
|
|
244
|
+
const nombreItem = sanitize(item.nombre);
|
|
245
|
+
|
|
246
|
+
const { base, descuentoMonto, montoItem } = calcularMontoItem(qty, prc, descuentoPct);
|
|
247
|
+
|
|
248
|
+
// Construir línea de detalle en orden del schema SII
|
|
249
|
+
const det = {
|
|
250
|
+
NroLinDet: idx + 1,
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
// IndExe antes de NmbItem
|
|
254
|
+
if (allowIndExe && item.exento) {
|
|
255
|
+
det.IndExe = 1;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
det.NmbItem = nombreItem;
|
|
259
|
+
|
|
260
|
+
// QtyItem, UnmdItem, PrcItem
|
|
261
|
+
if (prc > 0 || forcePriced) {
|
|
262
|
+
det.QtyItem = qty;
|
|
263
|
+
if (item.unidad || includeUnidad) {
|
|
264
|
+
det.UnmdItem = item.unidad || 'UN';
|
|
265
|
+
}
|
|
266
|
+
det.PrcItem = prc > 0 ? formatDecimal(Math.max(0.000001, prc)) : prc;
|
|
267
|
+
|
|
268
|
+
// Descuento por línea
|
|
269
|
+
if (descuentoPct > 0) {
|
|
270
|
+
det.DescuentoPct = descuentoPct;
|
|
271
|
+
det.DescuentoMonto = descuentoMonto;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// CodImpAdic (antes de MontoItem según schema)
|
|
276
|
+
if (codImpAdic && !item.exento) {
|
|
277
|
+
det.CodImpAdic = codImpAdic;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
det.MontoItem = montoItem;
|
|
281
|
+
|
|
282
|
+
return det;
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Construye detalle para Guía de Despacho
|
|
288
|
+
* Similar a buildDetalle pero con lógica específica para guías
|
|
289
|
+
*
|
|
290
|
+
* @param {Array} items - Items de la guía
|
|
291
|
+
* @param {Object} [options] - Opciones
|
|
292
|
+
* @returns {Array} Detalle formateado
|
|
293
|
+
*/
|
|
294
|
+
function buildDetalleGuia(items, options = {}) {
|
|
295
|
+
const { sanitize = (v) => String(v || '').trim() } = options;
|
|
296
|
+
|
|
297
|
+
return (items || []).map((item, idx) => {
|
|
298
|
+
const qty = Number(item.cantidad ?? 1);
|
|
299
|
+
const prc = item.precio !== undefined && item.precio !== null ? Number(item.precio) : null;
|
|
300
|
+
const monto = prc !== null ? Math.round(qty * prc) : (item.monto ?? undefined);
|
|
301
|
+
|
|
302
|
+
// Orden XSD: NroLinDet, CdgItem?, IndExe?, NmbItem, DscItem?, QtyRef?, UnmdRef?, PrcRef?, QtyItem?, UnmdItem?, PrcItem?, MontoItem?
|
|
303
|
+
const det = {
|
|
304
|
+
NroLinDet: idx + 1,
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
// IndExe va ANTES de NmbItem según XSD
|
|
308
|
+
if (item.exento) {
|
|
309
|
+
det.IndExe = 1;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
det.NmbItem = sanitize(item.nombre);
|
|
313
|
+
det.QtyItem = qty;
|
|
314
|
+
det.UnmdItem = item.unidad || 'UN';
|
|
315
|
+
|
|
316
|
+
if (prc !== null && prc > 0) {
|
|
317
|
+
det.PrcItem = prc;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (monto !== undefined) {
|
|
321
|
+
det.MontoItem = monto;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return det;
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Construye detalle para Factura de Compra (tipo 46)
|
|
330
|
+
* Similar a buildDetalle pero siempre incluye unidad y soporta CodImpAdic
|
|
331
|
+
*
|
|
332
|
+
* @param {Array} items - Items de la factura de compra
|
|
333
|
+
* @param {Object} [options] - Opciones
|
|
334
|
+
* @param {number} [options.codImpAdic=15] - Código impuesto adicional (15 = IVA Retenido Total)
|
|
335
|
+
* @param {Function} [options.sanitize] - Función para sanitizar texto
|
|
336
|
+
* @returns {Array} Detalle formateado
|
|
337
|
+
*/
|
|
338
|
+
function buildDetalleCompra(items, options = {}) {
|
|
339
|
+
const { codImpAdic = 15, sanitize = (v) => String(v || '').trim() } = options;
|
|
340
|
+
|
|
341
|
+
return buildDetalle(items, {
|
|
342
|
+
allowIndExe: true,
|
|
343
|
+
codImpAdic,
|
|
344
|
+
forcePriced: true,
|
|
345
|
+
includeUnidad: true,
|
|
346
|
+
sanitize,
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// ============================================
|
|
351
|
+
// DESCUENTO/RECARGO GLOBAL
|
|
352
|
+
// ============================================
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Construye estructura DscRcgGlobal para descuento global
|
|
356
|
+
*
|
|
357
|
+
* @param {number} descuentoPct - Porcentaje de descuento
|
|
358
|
+
* @param {string} [glosa='DESCUENTO GLOBAL ITEMES AFECTOS'] - Descripción
|
|
359
|
+
* @returns {Array|null} Estructura DscRcgGlobal o null si no hay descuento
|
|
360
|
+
*/
|
|
361
|
+
function buildDescuentoGlobal(descuentoPct, glosa = 'DESCUENTO GLOBAL ITEMES AFECTOS') {
|
|
362
|
+
if (!descuentoPct || descuentoPct <= 0) return null;
|
|
363
|
+
|
|
364
|
+
return [{
|
|
365
|
+
NroLinDR: 1,
|
|
366
|
+
TpoMov: 'D',
|
|
367
|
+
GlosaDR: glosa,
|
|
368
|
+
TpoValor: '%',
|
|
369
|
+
ValorDR: descuentoPct,
|
|
370
|
+
}];
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// ============================================
|
|
374
|
+
// EXPORTS
|
|
375
|
+
// ============================================
|
|
376
|
+
|
|
377
|
+
module.exports = {
|
|
378
|
+
// Constantes
|
|
379
|
+
TASA_IVA_DEFAULT,
|
|
380
|
+
|
|
381
|
+
// Formateo
|
|
382
|
+
formatDecimal,
|
|
383
|
+
calcularMontoItem,
|
|
384
|
+
|
|
385
|
+
// Cálculo de totales
|
|
386
|
+
calcularTotalesDesdeItems,
|
|
387
|
+
calcularTotalesDesdeDetalle,
|
|
388
|
+
|
|
389
|
+
// Construcción de detalle
|
|
390
|
+
buildDetalle,
|
|
391
|
+
buildDetalleGuia,
|
|
392
|
+
buildDetalleCompra,
|
|
393
|
+
|
|
394
|
+
// Descuento global
|
|
395
|
+
buildDescuentoGlobal,
|
|
396
|
+
};
|