cfdi40
Version:
Libreria para crear y sellar xml cfdi V4.0
706 lines (640 loc) • 22.8 kB
JavaScript
'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;