@timbra-ec/sri 0.1.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/.turbo/turbo-build.log +4 -0
- package/.turbo/turbo-lint.log +5 -0
- package/.turbo/turbo-typecheck.log +5 -0
- package/dist/clave-acceso.d.ts +31 -0
- package/dist/clave-acceso.d.ts.map +1 -0
- package/dist/clave-acceso.js +70 -0
- package/dist/clave-acceso.js.map +1 -0
- package/dist/errors.d.ts +22 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +26 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/signing/certificate.d.ts +27 -0
- package/dist/signing/certificate.d.ts.map +1 -0
- package/dist/signing/certificate.js +78 -0
- package/dist/signing/certificate.js.map +1 -0
- package/dist/signing/xades-bes.d.ts +10 -0
- package/dist/signing/xades-bes.d.ts.map +1 -0
- package/dist/signing/xades-bes.js +166 -0
- package/dist/signing/xades-bes.js.map +1 -0
- package/dist/soap/autorizacion.d.ts +21 -0
- package/dist/soap/autorizacion.d.ts.map +1 -0
- package/dist/soap/autorizacion.js +99 -0
- package/dist/soap/autorizacion.js.map +1 -0
- package/dist/soap/client.d.ts +6 -0
- package/dist/soap/client.d.ts.map +1 -0
- package/dist/soap/client.js +37 -0
- package/dist/soap/client.js.map +1 -0
- package/dist/soap/recepcion.d.ts +9 -0
- package/dist/soap/recepcion.d.ts.map +1 -0
- package/dist/soap/recepcion.js +60 -0
- package/dist/soap/recepcion.js.map +1 -0
- package/dist/xml/builder.d.ts +31 -0
- package/dist/xml/builder.d.ts.map +1 -0
- package/dist/xml/builder.js +287 -0
- package/dist/xml/builder.js.map +1 -0
- package/dist/xml/shared.d.ts +21 -0
- package/dist/xml/shared.d.ts.map +1 -0
- package/dist/xml/shared.js +93 -0
- package/dist/xml/shared.js.map +1 -0
- package/dist/xml/types.d.ts +179 -0
- package/dist/xml/types.d.ts.map +1 -0
- package/dist/xml/types.js +2 -0
- package/dist/xml/types.js.map +1 -0
- package/package.json +37 -0
- package/src/clave-acceso.ts +106 -0
- package/src/errors.ts +32 -0
- package/src/index.ts +49 -0
- package/src/signing/certificate.ts +103 -0
- package/src/signing/xades-bes.ts +188 -0
- package/src/soap/autorizacion.ts +139 -0
- package/src/soap/client.ts +45 -0
- package/src/soap/recepcion.ts +83 -0
- package/src/xml/builder.ts +344 -0
- package/src/xml/shared.ts +110 -0
- package/src/xml/types.ts +195 -0
- package/tsconfig.json +8 -0
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import { create } from 'xmlbuilder2'
|
|
2
|
+
import type { Invoice, InvoiceItem, Organization, PaymentDetail } from '@timbra-ec/types'
|
|
3
|
+
import type { XMLBuilder } from 'xmlbuilder2/lib/interfaces'
|
|
4
|
+
import type {
|
|
5
|
+
NotaCreditoInput,
|
|
6
|
+
NotaDebitoInput,
|
|
7
|
+
GuiaRemisionInput,
|
|
8
|
+
RetencionInput,
|
|
9
|
+
AtsInput,
|
|
10
|
+
} from './types'
|
|
11
|
+
import {
|
|
12
|
+
money,
|
|
13
|
+
toSriDate,
|
|
14
|
+
aggregateTaxTotals,
|
|
15
|
+
buildInfoTributaria,
|
|
16
|
+
buildInfoAdicional,
|
|
17
|
+
buildDetalles,
|
|
18
|
+
} from './shared'
|
|
19
|
+
|
|
20
|
+
/** Build pagos section */
|
|
21
|
+
function buildPagos(parent: XMLBuilder, pagos: PaymentDetail[]): void {
|
|
22
|
+
const pagosEle = parent.ele('pagos')
|
|
23
|
+
for (const pago of pagos) {
|
|
24
|
+
const pagoEle = pagosEle.ele('pago')
|
|
25
|
+
pagoEle.ele('formaPago').txt(pago.formaPago)
|
|
26
|
+
pagoEle.ele('total').txt(money(pago.total))
|
|
27
|
+
if (pago.plazo !== undefined) {
|
|
28
|
+
pagoEle.ele('plazo').txt(pago.plazo.toString())
|
|
29
|
+
pagoEle.ele('unidadTiempo').txt(pago.unidadTiempo ?? 'dias')
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Build totalConImpuestos section from aggregated tax totals */
|
|
35
|
+
function buildTotalConImpuestos(parent: XMLBuilder, items: InvoiceItem[]): void {
|
|
36
|
+
const taxTotals = aggregateTaxTotals(items)
|
|
37
|
+
const totalConImpuestos = parent.ele('totalConImpuestos')
|
|
38
|
+
for (const tax of taxTotals) {
|
|
39
|
+
const totalImpuesto = totalConImpuestos.ele('totalImpuesto')
|
|
40
|
+
totalImpuesto.ele('codigo').txt(tax.codigo)
|
|
41
|
+
totalImpuesto.ele('codigoPorcentaje').txt(tax.codigoPorcentaje)
|
|
42
|
+
totalImpuesto.ele('baseImponible').txt(money(tax.baseImponible))
|
|
43
|
+
totalImpuesto.ele('valor').txt(money(tax.valor))
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Build SRI-compliant factura XML (codDoc=01, version 2.1.0).
|
|
49
|
+
*
|
|
50
|
+
* The returned XML has `id="comprobante"` on the root element,
|
|
51
|
+
* which is required for XAdES-BES signing to reference.
|
|
52
|
+
*/
|
|
53
|
+
export function buildFacturaXml(invoice: Invoice, org: Organization): string {
|
|
54
|
+
const doc = create({ version: '1.0', encoding: 'UTF-8' })
|
|
55
|
+
const factura = doc.ele('factura', { id: 'comprobante', version: '2.1.0' })
|
|
56
|
+
|
|
57
|
+
// --- infoTributaria ---
|
|
58
|
+
buildInfoTributaria(factura, {
|
|
59
|
+
ambiente: invoice.ambiente,
|
|
60
|
+
tipoEmision: invoice.tipoEmision,
|
|
61
|
+
ruc: org.ruc,
|
|
62
|
+
claveAcceso: invoice.claveAcceso,
|
|
63
|
+
codDoc: invoice.codDoc,
|
|
64
|
+
estab: invoice.estab,
|
|
65
|
+
ptoEmi: invoice.ptoEmi,
|
|
66
|
+
secuencial: invoice.secuencial,
|
|
67
|
+
razonSocial: org.razonSocial,
|
|
68
|
+
nombreComercial: org.nombreComercial,
|
|
69
|
+
direccionMatriz: org.direccionMatriz,
|
|
70
|
+
regimenRimpe: org.regimenRimpe,
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
// --- infoFactura ---
|
|
74
|
+
const infoFact = factura.ele('infoFactura')
|
|
75
|
+
infoFact.ele('fechaEmision').txt(toSriDate(invoice.fechaEmision))
|
|
76
|
+
infoFact.ele('obligadoContabilidad').txt(org.obligadoContabilidad ? 'SI' : 'NO')
|
|
77
|
+
infoFact.ele('tipoIdentificacionComprador').txt(invoice.tipoIdentificacionComprador)
|
|
78
|
+
infoFact.ele('razonSocialComprador').txt(invoice.razonSocialComprador)
|
|
79
|
+
infoFact.ele('identificacionComprador').txt(invoice.identificacionComprador)
|
|
80
|
+
infoFact.ele('totalSinImpuestos').txt(money(invoice.totalSinImpuestos))
|
|
81
|
+
infoFact.ele('totalDescuento').txt(money(invoice.totalDescuento))
|
|
82
|
+
buildTotalConImpuestos(infoFact, invoice.items)
|
|
83
|
+
infoFact.ele('propina').txt(money(invoice.propina ?? 0))
|
|
84
|
+
infoFact.ele('importeTotal').txt(money(invoice.importeTotal))
|
|
85
|
+
infoFact.ele('moneda').txt(invoice.moneda)
|
|
86
|
+
buildPagos(infoFact, invoice.pagos)
|
|
87
|
+
|
|
88
|
+
// --- detalles ---
|
|
89
|
+
buildDetalles(factura, invoice.items)
|
|
90
|
+
|
|
91
|
+
// --- infoAdicional ---
|
|
92
|
+
buildInfoAdicional(factura, invoice.infoAdicional)
|
|
93
|
+
|
|
94
|
+
return doc.end({ prettyPrint: false })
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Build SRI-compliant Nota de Credito XML (codDoc=04, version 1.1.0).
|
|
99
|
+
*/
|
|
100
|
+
export function buildNotaCreditoXml(input: NotaCreditoInput): string {
|
|
101
|
+
const doc = create({ version: '1.0', encoding: 'UTF-8' })
|
|
102
|
+
const root = doc.ele('notaCredito', { id: 'comprobante', version: '1.1.0' })
|
|
103
|
+
|
|
104
|
+
buildInfoTributaria(root, input.infoTributaria)
|
|
105
|
+
|
|
106
|
+
const info = root.ele('infoNotaCredito')
|
|
107
|
+
info.ele('fechaEmision').txt(toSriDate(input.fechaEmision))
|
|
108
|
+
info.ele('obligadoContabilidad').txt(input.obligadoContabilidad ? 'SI' : 'NO')
|
|
109
|
+
info.ele('tipoIdentificacionComprador').txt(input.tipoIdentificacionComprador)
|
|
110
|
+
info.ele('razonSocialComprador').txt(input.razonSocialComprador)
|
|
111
|
+
info.ele('identificacionComprador').txt(input.identificacionComprador)
|
|
112
|
+
info.ele('codDocModificado').txt(input.codDocModificado)
|
|
113
|
+
info.ele('numDocModificado').txt(input.numDocModificado)
|
|
114
|
+
info.ele('fechaEmisionDocSustento').txt(toSriDate(input.fechaEmisionDocSustento))
|
|
115
|
+
info.ele('totalSinImpuestos').txt(money(input.totalSinImpuestos))
|
|
116
|
+
buildTotalConImpuestos(info, input.items)
|
|
117
|
+
info.ele('motivo').txt(input.motivo)
|
|
118
|
+
info.ele('importeTotal').txt(money(input.importeTotal))
|
|
119
|
+
buildPagos(info, input.pagos)
|
|
120
|
+
|
|
121
|
+
buildDetalles(root, input.items)
|
|
122
|
+
buildInfoAdicional(root, input.infoAdicional)
|
|
123
|
+
|
|
124
|
+
return doc.end({ prettyPrint: false })
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Build SRI-compliant Nota de Debito XML (codDoc=05, version 1.0.0).
|
|
129
|
+
*/
|
|
130
|
+
export function buildNotaDebitoXml(input: NotaDebitoInput): string {
|
|
131
|
+
const doc = create({ version: '1.0', encoding: 'UTF-8' })
|
|
132
|
+
const root = doc.ele('notaDebito', { id: 'comprobante', version: '1.0.0' })
|
|
133
|
+
|
|
134
|
+
buildInfoTributaria(root, input.infoTributaria)
|
|
135
|
+
|
|
136
|
+
const info = root.ele('infoNotaDebito')
|
|
137
|
+
info.ele('fechaEmision').txt(toSriDate(input.fechaEmision))
|
|
138
|
+
info.ele('obligadoContabilidad').txt(input.obligadoContabilidad ? 'SI' : 'NO')
|
|
139
|
+
info.ele('tipoIdentificacionComprador').txt(input.tipoIdentificacionComprador)
|
|
140
|
+
info.ele('razonSocialComprador').txt(input.razonSocialComprador)
|
|
141
|
+
info.ele('identificacionComprador').txt(input.identificacionComprador)
|
|
142
|
+
info.ele('codDocModificado').txt(input.codDocModificado)
|
|
143
|
+
info.ele('numDocModificado').txt(input.numDocModificado)
|
|
144
|
+
info.ele('fechaEmisionDocSustento').txt(toSriDate(input.fechaEmisionDocSustento))
|
|
145
|
+
info.ele('totalSinImpuestos').txt(money(input.totalSinImpuestos))
|
|
146
|
+
|
|
147
|
+
const impuestosEle = info.ele('impuestos')
|
|
148
|
+
for (const imp of input.impuestos) {
|
|
149
|
+
const impuesto = impuestosEle.ele('impuesto')
|
|
150
|
+
impuesto.ele('codigo').txt(imp.codigo)
|
|
151
|
+
impuesto.ele('codigoPorcentaje').txt(imp.codigoPorcentaje)
|
|
152
|
+
impuesto.ele('tarifa').txt(money(imp.tarifa))
|
|
153
|
+
impuesto.ele('baseImponible').txt(money(imp.baseImponible))
|
|
154
|
+
impuesto.ele('valor').txt(money(imp.valor))
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
info.ele('importeTotal').txt(money(input.importeTotal))
|
|
158
|
+
buildPagos(info, input.pagos)
|
|
159
|
+
|
|
160
|
+
const motivos = root.ele('motivos')
|
|
161
|
+
for (const m of input.motivos) {
|
|
162
|
+
const motivo = motivos.ele('motivo')
|
|
163
|
+
motivo.ele('razon').txt(m.razon)
|
|
164
|
+
motivo.ele('valor').txt(money(m.valor))
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
buildInfoAdicional(root, input.infoAdicional)
|
|
168
|
+
|
|
169
|
+
return doc.end({ prettyPrint: false })
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Build SRI-compliant Guia de Remision XML (codDoc=06, version 1.1.0).
|
|
174
|
+
*/
|
|
175
|
+
export function buildGuiaRemisionXml(input: GuiaRemisionInput): string {
|
|
176
|
+
const doc = create({ version: '1.0', encoding: 'UTF-8' })
|
|
177
|
+
const root = doc.ele('guiaRemision', { id: 'comprobante', version: '1.1.0' })
|
|
178
|
+
|
|
179
|
+
buildInfoTributaria(root, input.infoTributaria)
|
|
180
|
+
|
|
181
|
+
const info = root.ele('infoGuiaRemision')
|
|
182
|
+
info.ele('dirEstablecimiento').txt(input.infoTributaria.direccionMatriz)
|
|
183
|
+
info.ele('dirPartida').txt(input.dirPartida)
|
|
184
|
+
info.ele('razonSocialTransportista').txt(input.razonSocialTransportista)
|
|
185
|
+
info.ele('tipoIdentificacionTransportista').txt(input.tipoIdentificacionTransportista)
|
|
186
|
+
info.ele('rucTransportista').txt(input.rucTransportista)
|
|
187
|
+
info.ele('obligadoContabilidad').txt(input.obligadoContabilidad ? 'SI' : 'NO')
|
|
188
|
+
info.ele('fechaIniTransporte').txt(toSriDate(input.fechaIniTransporte))
|
|
189
|
+
info.ele('fechaFinTransporte').txt(toSriDate(input.fechaFinTransporte))
|
|
190
|
+
info.ele('placa').txt(input.placa)
|
|
191
|
+
|
|
192
|
+
const destinatarios = root.ele('destinatarios')
|
|
193
|
+
for (const dest of input.destinatarios) {
|
|
194
|
+
const destinatario = destinatarios.ele('destinatario')
|
|
195
|
+
destinatario.ele('identificacionDestinatario').txt(dest.identificacionDestinatario)
|
|
196
|
+
destinatario.ele('razonSocialDestinatario').txt(dest.razonSocialDestinatario)
|
|
197
|
+
destinatario.ele('dirDestinatario').txt(dest.dirDestinatario)
|
|
198
|
+
destinatario.ele('motivoTraslado').txt(dest.motivoTraslado)
|
|
199
|
+
|
|
200
|
+
if (dest.codDocSustento) {
|
|
201
|
+
destinatario.ele('codDocSustento').txt(dest.codDocSustento)
|
|
202
|
+
}
|
|
203
|
+
if (dest.numDocSustento) {
|
|
204
|
+
destinatario.ele('numDocSustento').txt(dest.numDocSustento)
|
|
205
|
+
}
|
|
206
|
+
if (dest.numAutDocSustento) {
|
|
207
|
+
destinatario.ele('numAutDocSustento').txt(dest.numAutDocSustento)
|
|
208
|
+
}
|
|
209
|
+
if (dest.fechaEmisionDocSustento) {
|
|
210
|
+
destinatario.ele('fechaEmisionDocSustento').txt(toSriDate(dest.fechaEmisionDocSustento))
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const detalles = destinatario.ele('detalles')
|
|
214
|
+
for (const item of dest.detalles) {
|
|
215
|
+
const detalle = detalles.ele('detalle')
|
|
216
|
+
detalle.ele('codigoInterno').txt(item.codigoInterno)
|
|
217
|
+
detalle.ele('descripcion').txt(item.descripcion)
|
|
218
|
+
detalle.ele('cantidad').txt(money(item.cantidad))
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
buildInfoAdicional(root, input.infoAdicional)
|
|
223
|
+
|
|
224
|
+
return doc.end({ prettyPrint: false })
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Build SRI-compliant Comprobante de Retencion XML (codDoc=07, Version ATS format).
|
|
229
|
+
*/
|
|
230
|
+
export function buildRetencionXml(input: RetencionInput): string {
|
|
231
|
+
const doc = create({ version: '1.0', encoding: 'UTF-8' })
|
|
232
|
+
const root = doc.ele('comprobanteRetencion', { id: 'comprobante', version: '2.0.0' })
|
|
233
|
+
|
|
234
|
+
buildInfoTributaria(root, input.infoTributaria)
|
|
235
|
+
|
|
236
|
+
const info = root.ele('infoCompRetencion')
|
|
237
|
+
info.ele('fechaEmision').txt(toSriDate(input.fechaEmision))
|
|
238
|
+
info.ele('obligadoContabilidad').txt(input.obligadoContabilidad ? 'SI' : 'NO')
|
|
239
|
+
info.ele('tipoIdentificacionSujetoRetenido').txt(input.tipoIdentificacionSujetoRetenido)
|
|
240
|
+
info.ele('razonSocialSujetoRetenido').txt(input.razonSocialSujetoRetenido)
|
|
241
|
+
info.ele('identificacionSujetoRetenido').txt(input.identificacionSujetoRetenido)
|
|
242
|
+
info.ele('periodoFiscal').txt(input.periodoFiscal)
|
|
243
|
+
|
|
244
|
+
const docsSustento = root.ele('docsSustento')
|
|
245
|
+
// Group retention details by sustaining document
|
|
246
|
+
const byDoc = new Map<string, typeof input.impuestos>()
|
|
247
|
+
for (const imp of input.impuestos) {
|
|
248
|
+
const key = `${imp.codDocSustento}-${imp.numDocSustento}-${imp.fechaEmisionDocSustento}`
|
|
249
|
+
const group = byDoc.get(key) ?? []
|
|
250
|
+
group.push(imp)
|
|
251
|
+
byDoc.set(key, group)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
for (const [, retenciones] of byDoc) {
|
|
255
|
+
const first = retenciones[0]!
|
|
256
|
+
const docSustento = docsSustento.ele('docSustento')
|
|
257
|
+
docSustento.ele('codSustento').txt('01')
|
|
258
|
+
docSustento.ele('codDocSustento').txt(first.codDocSustento)
|
|
259
|
+
docSustento.ele('numDocSustento').txt(first.numDocSustento)
|
|
260
|
+
docSustento.ele('fechaEmisionDocSustento').txt(toSriDate(first.fechaEmisionDocSustento))
|
|
261
|
+
|
|
262
|
+
const retencionesEle = docSustento.ele('retenciones')
|
|
263
|
+
for (const ret of retenciones) {
|
|
264
|
+
const retencion = retencionesEle.ele('retencion')
|
|
265
|
+
retencion.ele('codigo').txt(ret.codigo)
|
|
266
|
+
retencion.ele('codigoRetencion').txt(ret.codigoRetencion)
|
|
267
|
+
retencion.ele('baseImponible').txt(money(ret.baseImponible))
|
|
268
|
+
retencion.ele('porcentajeRetener').txt(money(ret.porcentajeRetener))
|
|
269
|
+
retencion.ele('valorRetenido').txt(money(ret.valorRetenido))
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
buildInfoAdicional(root, input.infoAdicional)
|
|
274
|
+
|
|
275
|
+
return doc.end({ prettyPrint: false })
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Build ATS (Anexo Transaccional Simplificado) XML for monthly filing.
|
|
280
|
+
* This is NOT an electronic document — it's a report XML uploaded to SRI portal.
|
|
281
|
+
*/
|
|
282
|
+
export function buildAtsXml(input: AtsInput): string {
|
|
283
|
+
const doc = create({ version: '1.0', encoding: 'UTF-8' })
|
|
284
|
+
const iva = doc.ele('iva')
|
|
285
|
+
|
|
286
|
+
iva.ele('TipoIDInformante').txt(input.tipoIDInformante)
|
|
287
|
+
iva.ele('IdInformante').txt(input.idInformante)
|
|
288
|
+
iva.ele('razonSocial').txt(input.razonSocial)
|
|
289
|
+
iva.ele('Anio').txt(input.anio)
|
|
290
|
+
iva.ele('Mes').txt(input.mes)
|
|
291
|
+
iva.ele('numEstabRuc').txt(input.numEstabRuc)
|
|
292
|
+
iva.ele('totalVentas').txt(money(input.totalVentas))
|
|
293
|
+
iva.ele('codigoOperativo').txt(input.codigoOperativo)
|
|
294
|
+
|
|
295
|
+
if (input.compras.length > 0) {
|
|
296
|
+
const compras = iva.ele('compras')
|
|
297
|
+
for (const c of input.compras) {
|
|
298
|
+
const detalle = compras.ele('detalleCompras')
|
|
299
|
+
detalle.ele('codSustento').txt(c.codSustento)
|
|
300
|
+
detalle.ele('tpIdProv').txt(c.tpIdProv)
|
|
301
|
+
detalle.ele('idProv').txt(c.idProv)
|
|
302
|
+
detalle.ele('tipoComprobante').txt(c.tipoComprobante)
|
|
303
|
+
detalle.ele('parteRel').txt(c.parteRel)
|
|
304
|
+
detalle.ele('fechaRegistro').txt(c.fechaRegistro)
|
|
305
|
+
detalle.ele('establecimiento').txt(c.establecimiento)
|
|
306
|
+
detalle.ele('puntoEmision').txt(c.puntoEmision)
|
|
307
|
+
detalle.ele('secuencial').txt(c.secuencial)
|
|
308
|
+
detalle.ele('fechaEmision').txt(c.fechaEmision)
|
|
309
|
+
detalle.ele('autorizacion').txt(c.autorizacion)
|
|
310
|
+
detalle.ele('baseNoGraIva').txt(money(c.baseNoGraIva))
|
|
311
|
+
detalle.ele('baseImponible').txt(money(c.baseImponible))
|
|
312
|
+
detalle.ele('baseImpGrav').txt(money(c.baseImpGrav))
|
|
313
|
+
detalle.ele('montoIva').txt(money(c.montoIva))
|
|
314
|
+
detalle.ele('valRetBien10').txt(money(c.valRetBien10))
|
|
315
|
+
detalle.ele('valRetServ20').txt(money(c.valRetServ20))
|
|
316
|
+
detalle.ele('valorRetBienes').txt(money(c.valorRetBienes))
|
|
317
|
+
detalle.ele('valRetServ50').txt(money(c.valRetServ50))
|
|
318
|
+
detalle.ele('valorRetServicios').txt(money(c.valorRetServicios))
|
|
319
|
+
detalle.ele('valRetServ100').txt(money(c.valRetServ100))
|
|
320
|
+
detalle.ele('totbasesImpReemb').txt(money(c.totbasesImpReemb))
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (input.ventas.length > 0) {
|
|
325
|
+
const ventas = iva.ele('ventas')
|
|
326
|
+
for (const v of input.ventas) {
|
|
327
|
+
const detalle = ventas.ele('detalleVentas')
|
|
328
|
+
detalle.ele('tpIdCliente').txt(v.tpIdCliente)
|
|
329
|
+
detalle.ele('idCliente').txt(v.idCliente)
|
|
330
|
+
detalle.ele('tipoComprobante').txt(v.tipoComprobante)
|
|
331
|
+
detalle.ele('tipoEmision').txt(v.tipoEmision)
|
|
332
|
+
detalle.ele('numeroComprobantes').txt(v.numeroComprobantes.toString())
|
|
333
|
+
detalle.ele('baseNoGraIva').txt(money(v.baseNoGraIva))
|
|
334
|
+
detalle.ele('baseImponible').txt(money(v.baseImponible))
|
|
335
|
+
detalle.ele('baseImpGrav').txt(money(v.baseImpGrav))
|
|
336
|
+
detalle.ele('montoIva').txt(money(v.montoIva))
|
|
337
|
+
detalle.ele('montoIce').txt(money(v.montoIce))
|
|
338
|
+
detalle.ele('valorRetIva').txt(money(v.valorRetIva))
|
|
339
|
+
detalle.ele('valorRetRenta').txt(money(v.valorRetRenta))
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return doc.end({ prettyPrint: false })
|
|
344
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { XMLBuilder } from 'xmlbuilder2/lib/interfaces'
|
|
2
|
+
import type { InvoiceItem } from '@timbra-ec/types'
|
|
3
|
+
import type { InfoTributariaInput } from './types'
|
|
4
|
+
|
|
5
|
+
/** Format a number to exactly 2 decimal places */
|
|
6
|
+
export function money(value: number): string {
|
|
7
|
+
return value.toFixed(2)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Convert ISO date (YYYY-MM-DD) to SRI format (dd/MM/yyyy) */
|
|
11
|
+
export function toSriDate(isoDate: string): string {
|
|
12
|
+
const [year, month, day] = isoDate.split('-')
|
|
13
|
+
return `${day}/${month}/${year}`
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Aggregate tax totals by codigoPorcentaje across all items */
|
|
17
|
+
export function aggregateTaxTotals(items: InvoiceItem[]) {
|
|
18
|
+
const totals = new Map<
|
|
19
|
+
string,
|
|
20
|
+
{ codigo: string; codigoPorcentaje: string; baseImponible: number; valor: number }
|
|
21
|
+
>()
|
|
22
|
+
|
|
23
|
+
for (const item of items) {
|
|
24
|
+
for (const imp of item.impuestos) {
|
|
25
|
+
const key = `${imp.codigo}-${imp.codigoPorcentaje}`
|
|
26
|
+
const existing = totals.get(key)
|
|
27
|
+
if (existing) {
|
|
28
|
+
existing.baseImponible += imp.baseImponible
|
|
29
|
+
existing.valor += imp.valor
|
|
30
|
+
} else {
|
|
31
|
+
totals.set(key, {
|
|
32
|
+
codigo: imp.codigo,
|
|
33
|
+
codigoPorcentaje: imp.codigoPorcentaje,
|
|
34
|
+
baseImponible: imp.baseImponible,
|
|
35
|
+
valor: imp.valor,
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return Array.from(totals.values())
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Build the infoTributaria section (common to all SRI documents) */
|
|
45
|
+
export function buildInfoTributaria(parent: XMLBuilder, input: InfoTributariaInput): void {
|
|
46
|
+
const infoTrib = parent.ele('infoTributaria')
|
|
47
|
+
infoTrib.ele('ambiente').txt(input.ambiente)
|
|
48
|
+
infoTrib.ele('tipoEmision').txt(input.tipoEmision)
|
|
49
|
+
infoTrib.ele('razonSocial').txt(input.razonSocial)
|
|
50
|
+
if (input.nombreComercial) {
|
|
51
|
+
infoTrib.ele('nombreComercial').txt(input.nombreComercial)
|
|
52
|
+
}
|
|
53
|
+
infoTrib.ele('ruc').txt(input.ruc)
|
|
54
|
+
infoTrib.ele('claveAcceso').txt(input.claveAcceso)
|
|
55
|
+
infoTrib.ele('codDoc').txt(input.codDoc)
|
|
56
|
+
infoTrib.ele('estab').txt(input.estab)
|
|
57
|
+
infoTrib.ele('ptoEmi').txt(input.ptoEmi)
|
|
58
|
+
infoTrib.ele('secuencial').txt(input.secuencial)
|
|
59
|
+
infoTrib.ele('dirMatriz').txt(input.direccionMatriz)
|
|
60
|
+
|
|
61
|
+
if (input.regimenRimpe === 'emprendedor') {
|
|
62
|
+
infoTrib.ele('contribuyenteRimpe').txt('CONTRIBUYENTE RÉGIMEN RIMPE')
|
|
63
|
+
} else if (input.regimenRimpe === 'negocio_popular') {
|
|
64
|
+
infoTrib.ele('contribuyenteRimpe').txt('CONTRIBUYENTE NEGOCIO POPULAR - RÉGIMEN RIMPE')
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Build infoAdicional section */
|
|
69
|
+
export function buildInfoAdicional(
|
|
70
|
+
parent: XMLBuilder,
|
|
71
|
+
infoAdicional?: Record<string, string>,
|
|
72
|
+
): void {
|
|
73
|
+
if (infoAdicional && Object.keys(infoAdicional).length > 0) {
|
|
74
|
+
const section = parent.ele('infoAdicional')
|
|
75
|
+
for (const [nombre, valor] of Object.entries(infoAdicional)) {
|
|
76
|
+
section.ele('campoAdicional', { nombre }).txt(valor)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Build detalles section from InvoiceItems */
|
|
82
|
+
export function buildDetalles(parent: XMLBuilder, items: InvoiceItem[]): void {
|
|
83
|
+
const detalles = parent.ele('detalles')
|
|
84
|
+
for (const item of items) {
|
|
85
|
+
const detalle = detalles.ele('detalle')
|
|
86
|
+
detalle.ele('codigoPrincipal').txt(item.codigoPrincipal)
|
|
87
|
+
detalle.ele('descripcion').txt(item.descripcion)
|
|
88
|
+
detalle.ele('cantidad').txt(money(item.cantidad))
|
|
89
|
+
detalle.ele('precioUnitario').txt(money(item.precioUnitario))
|
|
90
|
+
detalle.ele('descuento').txt(money(item.descuento))
|
|
91
|
+
detalle.ele('precioTotalSinImpuesto').txt(money(item.precioTotalSinImpuesto))
|
|
92
|
+
|
|
93
|
+
const impuestos = detalle.ele('impuestos')
|
|
94
|
+
for (const imp of item.impuestos) {
|
|
95
|
+
const impuesto = impuestos.ele('impuesto')
|
|
96
|
+
impuesto.ele('codigo').txt(imp.codigo)
|
|
97
|
+
impuesto.ele('codigoPorcentaje').txt(imp.codigoPorcentaje)
|
|
98
|
+
impuesto.ele('tarifa').txt(money(imp.tarifa))
|
|
99
|
+
impuesto.ele('baseImponible').txt(money(imp.baseImponible))
|
|
100
|
+
impuesto.ele('valor').txt(money(imp.valor))
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (item.detallesAdicionales && item.detallesAdicionales.length > 0) {
|
|
104
|
+
const detAdicionales = detalle.ele('detallesAdicionales')
|
|
105
|
+
for (const da of item.detallesAdicionales) {
|
|
106
|
+
detAdicionales.ele('detAdicional', { nombre: da.nombre }).txt(da.valor)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
package/src/xml/types.ts
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DocumentTypeCode,
|
|
3
|
+
EmissionType,
|
|
4
|
+
InvoiceItem,
|
|
5
|
+
PaymentDetail,
|
|
6
|
+
SriEnvironment,
|
|
7
|
+
} from '@timbra-ec/types'
|
|
8
|
+
|
|
9
|
+
/** Common fields for all SRI documents (used in infoTributaria) */
|
|
10
|
+
export interface InfoTributariaInput {
|
|
11
|
+
ambiente: SriEnvironment
|
|
12
|
+
tipoEmision: EmissionType
|
|
13
|
+
ruc: string
|
|
14
|
+
claveAcceso: string
|
|
15
|
+
codDoc: DocumentTypeCode
|
|
16
|
+
estab: string
|
|
17
|
+
ptoEmi: string
|
|
18
|
+
secuencial: string
|
|
19
|
+
razonSocial: string
|
|
20
|
+
nombreComercial?: string
|
|
21
|
+
direccionMatriz: string
|
|
22
|
+
regimenRimpe?: 'emprendedor' | 'negocio_popular' | null
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Input for building a Nota de Credito XML (codDoc=04) */
|
|
26
|
+
export interface NotaCreditoInput {
|
|
27
|
+
infoTributaria: InfoTributariaInput
|
|
28
|
+
/** Date in YYYY-MM-DD format */
|
|
29
|
+
fechaEmision: string
|
|
30
|
+
obligadoContabilidad: boolean
|
|
31
|
+
tipoIdentificacionComprador: string
|
|
32
|
+
identificacionComprador: string
|
|
33
|
+
razonSocialComprador: string
|
|
34
|
+
/** codDoc of the modified document (e.g. '01' for factura) */
|
|
35
|
+
codDocModificado: DocumentTypeCode
|
|
36
|
+
/** Document number being modified: estab-ptoEmi-secuencial (e.g. '001-001-000000001') */
|
|
37
|
+
numDocModificado: string
|
|
38
|
+
/** Emission date of the modified document in YYYY-MM-DD */
|
|
39
|
+
fechaEmisionDocSustento: string
|
|
40
|
+
/** Reason for the credit note */
|
|
41
|
+
motivo: string
|
|
42
|
+
totalSinImpuestos: number
|
|
43
|
+
importeTotal: number
|
|
44
|
+
items: InvoiceItem[]
|
|
45
|
+
pagos: PaymentDetail[]
|
|
46
|
+
infoAdicional?: Record<string, string>
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Motivo entry for a Nota de Debito */
|
|
50
|
+
export interface MotivoDebito {
|
|
51
|
+
razon: string
|
|
52
|
+
valor: number
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Input for building a Nota de Debito XML (codDoc=05) */
|
|
56
|
+
export interface NotaDebitoInput {
|
|
57
|
+
infoTributaria: InfoTributariaInput
|
|
58
|
+
fechaEmision: string
|
|
59
|
+
obligadoContabilidad: boolean
|
|
60
|
+
tipoIdentificacionComprador: string
|
|
61
|
+
identificacionComprador: string
|
|
62
|
+
razonSocialComprador: string
|
|
63
|
+
codDocModificado: DocumentTypeCode
|
|
64
|
+
numDocModificado: string
|
|
65
|
+
fechaEmisionDocSustento: string
|
|
66
|
+
totalSinImpuestos: number
|
|
67
|
+
importeTotal: number
|
|
68
|
+
impuestos: {
|
|
69
|
+
codigo: string
|
|
70
|
+
codigoPorcentaje: string
|
|
71
|
+
tarifa: number
|
|
72
|
+
baseImponible: number
|
|
73
|
+
valor: number
|
|
74
|
+
}[]
|
|
75
|
+
motivos: MotivoDebito[]
|
|
76
|
+
pagos: PaymentDetail[]
|
|
77
|
+
infoAdicional?: Record<string, string>
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Destinatario for a Guia de Remision */
|
|
81
|
+
export interface Destinatario {
|
|
82
|
+
identificacionDestinatario: string
|
|
83
|
+
razonSocialDestinatario: string
|
|
84
|
+
dirDestinatario: string
|
|
85
|
+
motivoTraslado: string
|
|
86
|
+
/** Optional: referencing a factura or liquidacion */
|
|
87
|
+
codDocSustento?: DocumentTypeCode
|
|
88
|
+
numDocSustento?: string
|
|
89
|
+
numAutDocSustento?: string
|
|
90
|
+
fechaEmisionDocSustento?: string
|
|
91
|
+
detalles: {
|
|
92
|
+
codigoInterno: string
|
|
93
|
+
descripcion: string
|
|
94
|
+
cantidad: number
|
|
95
|
+
}[]
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Input for building a Guia de Remision XML (codDoc=06) */
|
|
99
|
+
export interface GuiaRemisionInput {
|
|
100
|
+
infoTributaria: InfoTributariaInput
|
|
101
|
+
fechaEmision: string
|
|
102
|
+
obligadoContabilidad: boolean
|
|
103
|
+
dirPartida: string
|
|
104
|
+
razonSocialTransportista: string
|
|
105
|
+
tipoIdentificacionTransportista: string
|
|
106
|
+
rucTransportista: string
|
|
107
|
+
placa: string
|
|
108
|
+
fechaIniTransporte: string
|
|
109
|
+
fechaFinTransporte: string
|
|
110
|
+
destinatarios: Destinatario[]
|
|
111
|
+
infoAdicional?: Record<string, string>
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Retention line item */
|
|
115
|
+
export interface RetencionDetalle {
|
|
116
|
+
/** '1' = renta, '2' = IVA, '6' = ISD */
|
|
117
|
+
codigo: string
|
|
118
|
+
codigoRetencion: string
|
|
119
|
+
baseImponible: number
|
|
120
|
+
porcentajeRetener: number
|
|
121
|
+
valorRetenido: number
|
|
122
|
+
/** codDoc of the sustaining document */
|
|
123
|
+
codDocSustento: DocumentTypeCode
|
|
124
|
+
numDocSustento: string
|
|
125
|
+
fechaEmisionDocSustento: string
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Input for building a Comprobante de Retencion XML (codDoc=07, Version ATS) */
|
|
129
|
+
export interface RetencionInput {
|
|
130
|
+
infoTributaria: InfoTributariaInput
|
|
131
|
+
fechaEmision: string
|
|
132
|
+
obligadoContabilidad: boolean
|
|
133
|
+
tipoIdentificacionSujetoRetenido: string
|
|
134
|
+
identificacionSujetoRetenido: string
|
|
135
|
+
razonSocialSujetoRetenido: string
|
|
136
|
+
periodoFiscal: string
|
|
137
|
+
impuestos: RetencionDetalle[]
|
|
138
|
+
infoAdicional?: Record<string, string>
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Purchase detail for ATS */
|
|
142
|
+
export interface AtsCompra {
|
|
143
|
+
codSustento: string
|
|
144
|
+
tpIdProv: string
|
|
145
|
+
idProv: string
|
|
146
|
+
tipoComprobante: string
|
|
147
|
+
parteRel: 'SI' | 'NO'
|
|
148
|
+
fechaRegistro: string
|
|
149
|
+
establecimiento: string
|
|
150
|
+
puntoEmision: string
|
|
151
|
+
secuencial: string
|
|
152
|
+
fechaEmision: string
|
|
153
|
+
autorizacion: string
|
|
154
|
+
baseNoGraIva: number
|
|
155
|
+
baseImponible: number
|
|
156
|
+
baseImpGrav: number
|
|
157
|
+
montoIva: number
|
|
158
|
+
valRetBien10: number
|
|
159
|
+
valRetServ20: number
|
|
160
|
+
valorRetBienes: number
|
|
161
|
+
valRetServ50: number
|
|
162
|
+
valorRetServicios: number
|
|
163
|
+
valRetServ100: number
|
|
164
|
+
totbasesImpReemb: number
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Sales detail for ATS */
|
|
168
|
+
export interface AtsVenta {
|
|
169
|
+
tpIdCliente: string
|
|
170
|
+
idCliente: string
|
|
171
|
+
tipoComprobante: string
|
|
172
|
+
tipoEmision: 'E' | 'F'
|
|
173
|
+
numeroComprobantes: number
|
|
174
|
+
baseNoGraIva: number
|
|
175
|
+
baseImponible: number
|
|
176
|
+
baseImpGrav: number
|
|
177
|
+
montoIva: number
|
|
178
|
+
montoIce: number
|
|
179
|
+
valorRetIva: number
|
|
180
|
+
valorRetRenta: number
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Input for building ATS XML */
|
|
184
|
+
export interface AtsInput {
|
|
185
|
+
tipoIDInformante: 'R' | 'C'
|
|
186
|
+
idInformante: string
|
|
187
|
+
razonSocial: string
|
|
188
|
+
anio: string
|
|
189
|
+
mes: string
|
|
190
|
+
numEstabRuc: string
|
|
191
|
+
totalVentas: number
|
|
192
|
+
codigoOperativo: string
|
|
193
|
+
compras: AtsCompra[]
|
|
194
|
+
ventas: AtsVenta[]
|
|
195
|
+
}
|