UNPKG

cfdi40

Version:

Libreria para crear y sellar xml cfdi V4.0

706 lines (640 loc) 22.8 kB
'use strict'; const os = require('os'); const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); const convert = require('xml-js'); const _ = require('lodash'); const xsltproc = require('node-xsltproc'); const forge = require('node-forge'); const openssl = require('./utils/openssl'); const pki = forge.pki; const baseCFDI = require('./utils/base'); const FileSystem = require('./utils/FileSystem'); function pushConcepto(cfdi, c) { const len = cfdi.elements[0].elements.length; const inconce = _.findIndex(cfdi.elements[0].elements, { name: 'cfdi:Conceptos' }); // AGREGAR BASE DE CONCEPTOS if(inconce === -1){ cfdi.elements[0].elements.push({ order: 3, type: 'element', name: 'cfdi:Conceptos', elements: [] }); } // RUTA BASE DE CONCEPTOS const base = (inconce === -1)? cfdi.elements[0].elements[len].elements: cfdi.elements[0].elements[inconce].elements; // AGREGAR CONCEPTO A CONCEPTOS const attrConcepto = JSON.parse(JSON.stringify(c)); delete attrConcepto.Impuestos; const concepto = { type: 'element', name: 'cfdi:Concepto', attributes: attrConcepto, elements: [] }; if(c.Impuestos.Traslados.length > 0 || c.Impuestos.Retenciones.length > 0){ let index = 0; // AGREGAR IMPUESTOS A CONCEPTO concepto.elements.push({ type: 'element', name: 'cfdi:Impuestos', elements: [] }); if(c.Impuestos.Traslados.length > 0){ // AGREGAR TRASLADOS A IMPUESTOS concepto.elements[0].elements.push({ type: 'element', name: 'cfdi:Traslados', elements: [] }); // RECORRER TRASLADOS c.Impuestos.Traslados.forEach(traslado => { // AGREGAR TRASLADO A TRASLADOS concepto.elements[0].elements[index].elements.push({ type: 'element', name: 'cfdi:Traslado', attributes: traslado }); }); index++; } if(c.Impuestos.Retenciones.length > 0){ // AGREGAR RETENCIONES A IMPUESTOS concepto.elements[0].elements.push({ type: 'element', name: 'cfdi:Retenciones', elements: [] }); // RECORRER TRASLADOS c.Impuestos.Retenciones.forEach(retencion => { // AGREGAR RETENCION A RETENCIONES concepto.elements[0].elements[index].elements.push({ type: 'element', name: 'cfdi:Retencion', attributes: retencion }); }); } } base.push(concepto); } function pushComplemento(cfdi, c, version) { const len = cfdi.elements[0].elements.length; const inconce = _.findIndex(cfdi.elements[0].elements, { name: 'cfdi:Complemento' }); // AGREGAR BASE DE COMPLEMENTO if(inconce === -1){ cfdi.elements[0].elements.push({ order: 4, type: 'element', name: 'cfdi:Complemento', elements: [{ type: 'element', name: `pago${version === '4.0' ? '20': '10'}:Pagos`, attributes: { Version: version === '4.0' ? '2.0': '1.0' }, elements: [] }] }); } // RUTA BASE DE COMPLEMENTO const base = (inconce === -1)? cfdi.elements[0].elements[len].elements[0].elements: cfdi.elements[0].elements[inconce].elements[0].elements; // AGREGAR CONCEPTO A CONCEPTOS const pagos = { type: 'element', name: `pago${version === '4.0' ? '20': '10'}:Pago`, attributes: version === '4.0' ? { FechaPago: c.FechaPago || '', FormaDePagoP: c.FormaDePagoP || '', MonedaP: c.MonedaP || '', Monto: c.Monto || '', TipoCambioP: c.TipoCambioP || '' } : { FechaPago: c.FechaPago || '', FormaDePagoP: c.FormaDePagoP || '', MonedaP: c.MonedaP || '', Monto: c.Monto || '' }, elements: [] } if ( c.RfcEmisorCtaOrd) pagos.attributes.RfcEmisorCtaOrd = c.RfcEmisorCtaOrd; if ( c.NumOperacion) pagos.attributes.NumOperacion = c.NumOperacion; if (version === '4.0') { let totales = null if (c.Totales.length > 0) { c.Totales.forEach(total => { totales ={ type: 'element', name: `pago${version === '4.0' ? '20': '10'}:Totales`, attributes: total } }) base.push(totales); } } if(c.Pago.DoctoRelacionado.length > 0){ let index = 0; // AGREGAR TRASLADOS A IMPUESTOS c.Pago.DoctoRelacionado.forEach(docto => { pagos.elements.push({ type: 'element', name: `pago${version === '4.0' ? '20': '10'}:DoctoRelacionado`, attributes: docto, elements: [] }); }); if (c.Pago.RetencionesDR.length > 0 || c.Pago.TrasladosDR.length > 0) { const impuestos = { type: 'element', name: `pago${version === '4.0' ? '20': '10'}:ImpuestosDR`, elements: [] } if (c.Pago.RetencionesDR.length > 0 ) { // AGREGAR TRASLADOS A IMPUESTOS const retenciones = { type: 'element', name: `pago${version === '4.0' ? '20': '10'}:RetencionesDR`, elements: [] } c.Pago.RetencionesDR.forEach(retencion => { retenciones.elements.push({ type: 'element', name: `pago${version === '4.0' ? '20': '10'}:RetencionDR`, attributes: retencion, }); }); impuestos.elements.push(retenciones); } if (c.Pago.TrasladosDR.length > 0) { const traslados = { type: 'element', name: `pago${version === '4.0' ? '20': '10'}:TrasladosDR`, elements: [] } c.Pago.TrasladosDR.forEach(traslado => { traslados.elements.push({ type: 'element', name: `pago${version === '4.0' ? '20': '10'}:TrasladoDR`, attributes: traslado, }); }); impuestos.elements.push(traslados); } pagos.elements[index].elements.push(impuestos); } index++; } if (c.Pago.ImpuestosP.length > 0) { const impuestos = { type: 'element', name: `pago${version === '4.0' ? '20': '10'}:ImpuestosP`, elements: [] } const traslados = { type: 'element', name: `pago${version === '4.0' ? '20': '10'}:TrasladosP`, elements: [] } c.Pago.ImpuestosP.forEach(impuesto => { traslados.elements.push({ type: 'element', name: `pago${version === '4.0' ? '20': '10'}:TrasladoP`, attributes: impuesto, }); }); impuestos.elements.push(traslados); pagos.elements.push(impuestos); } base.push(pagos); } class concept { /** * @param {Object} concepto * @param {String} concepto.ClaveProdServ * @param {String} concepto.ClaveUnidad * @param {String} concepto.NoIdentificacion * @param {String} concepto.Cantidad * @param {String} concepto.Unidad * @param {String} concepto.Descripcion * @param {String} concepto.ValorUnitario * @param {String} concepto.Importe * @param {String} concepto.Descuento * @param {Object} concepto.Impuestos * @param {Object[]} concepto.Impuestos.Traslados * @param {Object[]} concepto.Impuestos.Retenciones * @param {String} concepto.Impuestos.Traslados.Base * @param {String} concepto.Impuestos.Traslados.Impuesto * @param {String} concepto.Impuestos.Traslados.TipoFactor * @param {String} concepto.Impuestos.Traslados.TasaOCuota * @param {String} concepto.Impuestos.Traslados.Importe * @param {String} concepto.Impuestos.Retenciones.Base * @param {String} concepto.Impuestos.Retenciones.Impuesto * @param {String} concepto.Impuestos.Retenciones.TipoFactor * @param {String} concepto.Impuestos.Retenciones.TasaOCuota * @param {String} concepto.Impuestos.Retenciones.Importe */ constructor(concepto){ this.concepto = concepto; this.concepto.Impuestos = { Traslados: [], Retenciones: [] } //console.log(this.opensslDir); } /** * @param {Object} traslado * @param {String} traslado.Base * @param {String} traslado.Impuesto * @param {String} traslado.TipoFactor * @param {String} traslado.TasaOCuota * @param {String} traslado.Importe */ traslado(traslado){ this.concepto.Impuestos.Traslados.push(traslado); return this; } /** * @param {Object} retencion * @param {String} retencion.Base * @param {String} retencion.Impuesto * @param {String} retencion.TipoFactor * @param {String} retencion.TasaOCuota * @param {String} retencion.Importe */ retencion(retencion){ this.concepto.Impuestos.Retenciones.push(retencion); return this; } agregar(cfdi){ pushConcepto(cfdi.jxml, this.concepto); } } class complement { /** *@param {Object} complemento *@param {Object[]} complemento.Pagos *@param {String} complemento.Pagos.Version */ /** *@param {Object} complemento.Pagos.Totales *@param {String} complemento.Pagos.Totales.MontoTotalPagos *@param {String} complemento.Pagos.Totales.TotalRetencionesIVA *@param {String} complemento.Pagos.Totales.TotalTrasladosBaseIVA16 *@param {String} complemento.Pagos.Totales.TotalTrasladosImpuestoIVA16 */ /** *@param {Object[]} complemento.Pagos.Pago *@param {String} complemento.Pagos.Pago.FechaPago *@param {String} complemento.Pagos.Pago.FormaDePagoP *@param {String} complemento.Pagos.Pago.MonedaP *@param {String} complemento.Pagos.Pago.TipoCambioP *@param {String} complemento.Pagos.Pago.Monto *@param {Object[]} complemento.Pagos.Pago.DoctoRelacionado *@param {String} complemento.Pagos.Pago.DoctoRelacionado.IdDocumento *@param {String} complemento.Pagos.Pago.DoctoRelacionado.Serie *@param {String} complemento.Pagos.Pago.DoctoRelacionado.Folio *@param {String} complemento.Pagos.Pago.DoctoRelacionado.MonedaDR *@param {String} complemento.Pagos.Pago.DoctoRelacionado.TipoCambioDR *@param {String} complemento.Pagos.Pago.DoctoRelacionado.MetodoDePagoDR *@param {String} complemento.Pagos.Pago.DoctoRelacionado.NumParcialidad *@param {String} complemento.Pagos.Pago.DoctoRelacionado.ImpSaldoAnt *@param {String} complemento.Pagos.Pago.DoctoRelacionado.ImpPagado *@param {String} complemento.Pagos.Pago.DoctoRelacionado.ImpSaldoInsoluto */ constructor(complemento, version){ this.version = version; this.complemento = complemento; this.complemento.Totales = [] this.complemento.Pago = { DoctoRelacionado: [], RetencionesDR: [], TrasladosDR: [] }, this.complemento.Pago.ImpuestosP = [] } Totales(totales){ this.complemento.Totales.push(totales); return this; } DoctoRelacionado(doctoRelacionado){ this.complemento.Pago.DoctoRelacionado.push(doctoRelacionado); return this; } RetencionesDR(retencionesDR){ this.complemento.Pago.RetencionesDR.push(retencionesDR); return this; } TrasladosDR(trasladosDR){ this.complemento.Pago.TrasladosDR.push(trasladosDR); return this; } impuestosP(i) { this.complemento.Pago.ImpuestosP.push(i); return this; } agregar(cfdi){ pushComplemento(cfdi.jxml, this.complemento, this.version); } } class CFDI { /** * @param {String} opensslDir * @param {String} libxmlDir * @param {String} stylesheetDir * @param {Object} comprobante * @param {String} comprobante.Serie * @param {String} comprobante.Folio * @param {String} comprobante.Fecha * @param {String} comprobante.SubTotal * @param {String} comprobante.Moneda * @param {String} comprobante.Total * @param {String} comprobante.TipoDeComprobante * @param {String} comprobante.FormaPago * @param {String} comprobante.MetodoPago * @param {String} comprobante.CondicionesDePago * @param {String} comprobante.Descuento * @param {String} comprobante.TipoCambio * @param {String} comprobante.LugarExpedicion * @param {String} comprobante.Confirmacion, */ constructor(comprobante, opensslDir, libxmlDir, stylesheetDir) { this.comprobante = comprobante; this.jxml = new baseCFDI(); this.jxml.elements[0].attributes = comprobante; this.jxml.elements[0].attributes['xmlns:xsi'] = 'http://www.w3.org/2001/XMLSchema-instance'; this.jxml.elements[0].attributes['xsi:schemaLocation'] = comprobante.Version === '4.0'? 'http://www.sat.gob.mx/cfd/4 http://www.sat.gob.mx/sitio_internet/cfd/4/cfdv40.xsd' : 'http://www.sat.gob.mx/cfd/3 http://www.sat.gob.mx/sitio_internet/cfd/3/cfdv33.xsd' ; this.jxml.elements[0].attributes['xmlns:cfdi'] = comprobante.Version === '4.0'? 'http://www.sat.gob.mx/cfd/4' : 'http://www.sat.gob.mx/cfd/3'; this.jxml.elements[0].attributes['Version'] = comprobante.Version; this.opensslDir = opensslDir || os.platform() === 'linux' ? '/usr/bin' : path.join(path.resolve(__dirname, '../'), 'lib', 'win', 'openssl'); this.libxmlDir = libxmlDir || os.platform() === 'linux' ? '/usr/bin' : path.join(path.resolve(__dirname, '../'), 'lib', 'win','libxml'); this.stylesheetDir = stylesheetDir || path.join(__dirname, 'resources', comprobante.Version === '4.0'? 'cadenaoriginal_4_0.xslt' : 'cadenaoriginal_3_3.xslt'); } /** * @param {Object} relacionados * @param {String} relacionados.TipoRelacion * @param {String[]} relacionados.CfdiRelacionados */ CfdiRelacionados(relacionados){ const r = { order: 0, type: 'element', name: 'cfdi:CfdiRelacionados', attributes: { TipoRelacion: relacionados.TipoRelacion }, elements: [] }; (relacionados.CfdiRelacionados).forEach(rel => { r.elements.push({ type: 'element', name: 'cfdi:CfdiRelacionado', attributes: { UUID: rel } }); }); this.jxml.elements[0].elements.push(r); return this; } /** * @param {Object} emisor * @param {String} emisor.Rfc * @param {String} emisor.Nombre * @param {String} emisor.RegimenFiscal */ emisor(emisor) { this.jxml.elements[0].elements.push({ order: 1, type: 'element', name: 'cfdi:Emisor', attributes: emisor }); return this; } /** * @param {Object} receptor * @param {String} receptor.Rfc * @param {String} receptor.Nombre * @param {String} receptor.RegimenFiscal */ receptor(receptor) { this.jxml.elements[0].elements.push({ order: 2, type: 'element', name: 'cfdi:Receptor', attributes: receptor }); return this; } /** * @param {Object} impuestos * @param {String} impuestos.TotalImpuestosTrasladados * @param {Object[]} impuestos.Retenciones * @param {Object[]} impuestos.Traslados * @param {String} impuestos.Retenciones.Impuesto * @param {String} impuestos.Retenciones.TipoFactor * @param {String} impuestos.Retenciones.TasaOCuota * @param {String} impuestos.Retenciones.Importe * @param {String} impuestos.Traslados.Impuesto * @param {String} impuestos.Traslados.TipoFactor * @param {String} impuestos.Traslados.TasaOCuota * @param {String} impuestos.Traslados.Importe */ impuestos(i) { // CREANDO BASE DE IMPUESTOS GLOBALES const impuestos = { order: 4, type: 'element', name: 'cfdi:Impuestos', attributes: { }, elements: [] }; let index = 0; // AGREGAR SI CONTIENE IMPUESTOS RETENIDOS if(_.has(i, 'TotalImpuestosRetenidos')){ impuestos.attributes['TotalImpuestosRetenidos'] = i.TotalImpuestosRetenidos; // AGREGAR RETENCIONES A IMPUESTOS impuestos.elements.push({ type: 'element', name: 'cfdi:Retenciones', elements: [] }); // RECORRER TRASLADOS i.Retenciones.forEach(retencion => { // AGREGAR TRASLADO A TRASLADOS impuestos.elements[index].elements.push({ type: 'element', name: 'cfdi:Retencion', attributes: retencion }); }); index++; } // AGREGAR SI CONTIENE IMPUESTOS TRASLADADOS if(_.has(i, 'TotalImpuestosTrasladados')){ impuestos.attributes['TotalImpuestosTrasladados'] = i.TotalImpuestosTrasladados; // AGREGAR TRASLADOS A IMPUESTOS impuestos.elements.push({ type: 'element', name: 'cfdi:Traslados', elements: [] }); // RECORRER TRASLADOS i.Traslados.forEach(traslado => { // AGREGAR TRASLADO A TRASLADOS impuestos.elements[index].elements.push({ type: 'element', name: 'cfdi:Traslado', attributes: traslado }); }); } else if (_.has(i, 'Traslados')) { impuestos.elements.push({ type: 'element', name: 'cfdi:Traslados', elements: [] }); // RECORRER TRASLADOS i.Traslados.forEach(traslado => { // AGREGAR TRASLADO A TRASLADOS impuestos.elements[index].elements.push({ type: 'element', name: 'cfdi:Traslado', attributes: traslado }); }); } // AGREGAR IMPUESTOS A COMPROBANTE this.jxml.elements[0].elements.push(impuestos); return this; } /** * @param {Object} concepto * @param {String} concepto.ClaveProdServ * @param {String} concepto.ClaveUnidad * @param {String} concepto.NoIdentificacion * @param {String} concepto.Cantidad * @param {String} concepto.Unidad * @param {String} concepto.Descripcion * @param {String} concepto.ValorUnitario * @param {String} concepto.Importe * @param {String} concepto.Descuento * @param {Object} concepto.Impuestos * @param {Object[]} concepto.Impuestos.Retenciones * @param {Object[]} concepto.Impuestos.Traslados * @param {String} concepto.Impuestos.Retenciones.Base * @param {String} concepto.Impuestos.Retenciones.Impuesto * @param {String} concepto.Impuestos.Retenciones.TipoFactor * @param {String} concepto.Impuestos.Retenciones.TasaOCuota * @param {String} concepto.Impuestos.Retenciones.Importe * @param {String} concepto.Impuestos.Traslados.Base * @param {String} concepto.Impuestos.Traslados.Impuesto * @param {String} concepto.Impuestos.Traslados.TipoFactor * @param {String} concepto.Impuestos.Traslados.TasaOCuota * @param {String} concepto.Impuestos.Traslados.Importe */ concepto(concepto) { return new concept(concepto); } /** *@param {Object} complemento *@param {Object[]} complemento.Pagos *@param {String} complemento.Pagos.Version */ /** *@param {Object} complemento.Pagos.Totales *@param {String} complemento.Pagos.Totales.MontoTotalPagos *@param {String} complemento.Pagos.Totales.TotalRetencionesIVA *@param {String} complemento.Pagos.Totales.TotalTrasladosBaseIVA16 *@param {String} complemento.Pagos.Totales.TotalTrasladosImpuestoIVA16 */ /** *@param {Object} complemento.Pagos.Pago *@param {String} complemento.Pagos.Pago.FechaPago *@param {String} complemento.Pagos.Pago.FormaDePagoP *@param {String} complemento.Pagos.Pago.MonedaP *@param {String} complemento.Pagos.Pago.TipoCambioP *@param {String} complemento.Pagos.Pago.Monto *@param {Object[]} complemento.Pagos.Pago.DoctoRelacionado *@param {String} complemento.Pagos.Pago.DoctoRelacionado.IdDocumento *@param {String} complemento.Pagos.Pago.DoctoRelacionado.Serie *@param {String} complemento.Pagos.Pago.DoctoRelacionado.Folio *@param {String} complemento.Pagos.Pago.DoctoRelacionado.MonedaDR *@param {String} complemento.Pagos.Pago.DoctoRelacionado.TipoCambioDR *@param {String} complemento.Pagos.Pago.DoctoRelacionado.MetodoDePagoDR *@param {String} complemento.Pagos.Pago.DoctoRelacionado.NumParcialidad *@param {String} complemento.Pagos.Pago.DoctoRelacionado.ImpSaldoAnt *@param {String} complemento.Pagos.Pago.DoctoRelacionado.ImpPagado *@param {String} complemento.Pagos.Pago.DoctoRelacionado.ImpSaldoInsoluto */ Pago(complemento) { this.jxml.elements[0].attributes[`xmlns:pago${this.comprobante.Version === '4.0' ? '20': '10'}`] = this.comprobante.Version === '4.0' ? 'http://www.sat.gob.mx/Pagos20': 'http://www.sat.gob.mx/Pagos'; this.jxml.elements[0].attributes['xsi:schemaLocation'] = this.comprobante.Version === '4.0' ? 'http://www.sat.gob.mx/cfd/4 http://www.sat.gob.mx/sitio_internet/cfd/4/cfdv40.xsd http://www.sat.gob.mx/Pagos20 http://www.sat.gob.mx/sitio_internet/cfd/Pagos/Pagos20.xsd' : 'http://www.sat.gob.mx/cfd/3 http://www.sat.gob.mx/sitio_internet/cfd/3/cfdv33.xsd http://www.sat.gob.mx/Pagos http://www.sat.gob.mx/sitio_internet/cfd/Pagos/Pagos10.xsd'; return new complement(complemento, this.comprobante.Version); } /** * @param {String} certificado */ certificar(certificado) { const cer = fs.readFileSync(certificado, 'base64'); const pem = '-----BEGIN CERTIFICATE-----\n' + cer + '\n-----END CERTIFICATE-----'; const serialNumber = pki .certificateFromPem(pem) .serialNumber.match(/.{1,2}/g) .map(function(v) { return String.fromCharCode(parseInt(v, 16)); }) .join(''); this.jxml.elements[0].attributes['NoCertificado'] = serialNumber; this.jxml.elements[0].attributes['Certificado'] = cer; return this; } /** * @returns {Promise} */ xml() { return new Promise((resolve, reject) => { try { this.jxml.elements[0].elements = _.orderBy(this.jxml.elements[0].elements, ['order']); const xml = convert.json2xml(this.jxml); resolve(xml); }catch(err) { reject(err); } }); } /** * @param {String} llave Directorio de la llave * @param {String} password Contraseña de la llave * @returns {Promise} */ xmlSellado(llave, password) { if(!fs.existsSync(this.libxmlDir)) return Promise.reject("No se encontro libxml en la ruta: "+ this.libxmlDir); if(!fs.existsSync(this.opensslDir)) return Promise.reject("No se encontro openss en la ruta: "+ this.opensslDir); const fullPath = path.join(os.tmpdir(), `${FileSystem.generateNameTemp()}.xml`); this.jxml.elements[0].elements = _.orderBy(this.jxml.elements[0].elements, ['order']); fs.writeFileSync(fullPath, convert.json2xml(this.jxml), 'utf8'); return xsltproc({ xsltproc_path: this.libxmlDir }) .transform([this.stylesheetDir, fullPath]) .then(cadena => { fs.unlinkSync(fullPath); return Promise.all([ cadena, openssl.decryptPKCS8PrivateKey({ openssl_path: this.opensslDir, in: llave, pass: password }) ]); }) .then((prm) => { const pem = prm[1]; const cadena = prm[0]; const sign = crypto.createSign('RSA-SHA256'); sign.update(cadena.result); const sello = sign.sign(pem.trim(), 'base64'); this.jxml.elements[0].attributes['Sello'] = sello; return convert.json2xml(this.jxml); }); } } module.exports = CFDI;