UNPKG

@ocelotlstudio/cfdi-pdf

Version:
566 lines (555 loc) 18.6 kB
import { clavesUnidadesCatalog, formasPagoCatalog, impuestosCatalog, metodosPagoCatalog, monedasCatalog, regimenesFiscalesCatalog, tiposComprobantesCatalog, tiposRelacionesCatalog, usosCfdiCatalog, } from '../catalogs'; import { toCurrency } from '../utils/toCurrency'; import { formatCurrency, breakEveryNCharacters } from '../utils/helper'; import { exists, existsValue } from '../utils/check'; import { Cfdi, ComplementoPago, Concepto, DoctoRelacionado, Receptor } from '../parser/dataToCfdi'; import { TDocumentDefinitions } from 'pdfmake/interfaces'; export interface Options { text?: string; image?: string; address?: string; cadenaOriginal?: string; } const generateImpuestos = (concepto: Concepto) => { const arr = []; if (concepto.traslados.length > 0) { arr.push('Traslados'); const content = concepto.traslados.map((traslado) => { return [ impuestosCatalog[traslado.impuesto] ? `${traslado.impuesto} - ${impuestosCatalog[traslado.impuesto]}` : '', traslado.tipoFactor == 'Exento' ? 'EXENTO' : `${formatCurrency(traslado.importe)}`, ]; }); arr.push({ table: { body: content, }, layout: 'noBorders', }); } if (concepto.retenciones.length > 0) { arr.push('Retenciones'); const content = concepto.retenciones.map((retencion) => { return [ impuestosCatalog[retencion.impuesto] ? `${retencion.impuesto} - ${impuestosCatalog[retencion.impuesto]}` : '', `${formatCurrency(retencion.importe)}`, ]; }); arr.push({ table: { body: content, }, layout: 'noBorders', }); } return arr; }; const generateConceptsTable = (conceptos: Array<Concepto>) => { const arr: Array<any> = conceptos.map((concepto: Concepto) => [ concepto.clave, concepto.cantidad, concepto.claveUnidad, clavesUnidadesCatalog[concepto.claveUnidad], concepto.descripcion, `${formatCurrency(concepto.valorUnitario)}`, `${formatCurrency(concepto.descuento)}`, { colSpan: 2, stack: generateImpuestos(concepto), }, '', `${formatCurrency(concepto.importe)}`, ]); arr.unshift([ 'ClaveProdServ', 'Cant', 'Clave Unidad', 'Unidad', 'Descripción', 'Valor Unitario', 'Descuento', { colSpan: 2, text: 'Impuesto', }, '', 'Importe', ]); arr.unshift([ { text: 'PARTIDAS DEL COMPROBANTE', style: 'tableHeader', colSpan: 10, alignment: 'center', }, {}, {}, {}, {}, {}, {}, {}, {}, {}, ]); return arr; }; const generateRelatedDocs = (docs: Array<DoctoRelacionado>) => { const arr: Array<any> = docs.map((doc: DoctoRelacionado) => [ doc.uuid, doc.metodoPago, doc.moneda, doc.tipoCambio, doc.numParcialidad, `${formatCurrency(doc.saldoAnterior)}`, `${formatCurrency(doc.importePagado)}`, `${formatCurrency(doc.saldoInsoluto)}`, ]); arr.unshift([ 'UUID', 'Método de Pago', 'Moneda', 'Tipo de Cambio', 'Num. Parcialidad', 'Importe Saldo Anterior', 'Importe Pagado', 'Importe Saldo Insoluto', ]); arr.unshift([ { text: 'DOCUMENTOS RELACIONADOS', style: 'tableHeader', colSpan: 8, alignment: 'center', }, {}, {}, {}, {}, {}, {}, {}, ]); return arr; }; const generatePayments = (pagos: Array<ComplementoPago>) => { const arr = pagos.map((pago: ComplementoPago) => [ { style: 'tableContent', table: { widths: [95, '*', 95, '*'], body: [ [ { text: 'INFORMACIÓN DE PAGO', style: 'tableHeader', colSpan: 4, alignment: 'center', }, {}, {}, {}, ], [ 'FECHA:', pago.fecha, 'FORMA PAGO:', formasPagoCatalog[pago.formaPago] ? `${pago.formaPago} - ${formasPagoCatalog[pago.formaPago]}` : '', ], [ 'MONEDA:', monedasCatalog[pago.moneda] ? `${pago.moneda} - ${monedasCatalog[pago.moneda]}` : '', 'MONTO:', `${formatCurrency(pago.monto)}`, ], pago.tipoCambio ? ['TIPO DE CAMBIO:', pago.tipoCambio, '', ''] : ['', '', '', ''], ], }, layout: 'lightHorizontalLines', }, '\n', { style: 'tableList', table: { widths: ['*', 'auto', 'auto', 30, 20, 'auto', 'auto', 'auto'], body: generateRelatedDocs(pago.doctoRelacionados), }, layout: { fillColor(i: number) { return i % 2 !== 0 ? '#CCCCCC' : null; }, }, }, '\n', ]); return [].concat.apply([], arr); }; const generateQrCode = (json: Cfdi) => { const template = 'https://verificacfdi.facturaelectronica.sat.gob.mx/default.aspx?id={id}&re={re}&rr={rr}&tt={tt}&fe={fe}'; const qrCode = template .replace('{id}', json.timbreFiscalDigital.uuid) .replace('{re}', json.emisor.rfc) .replace('{rr}', json.receptor.rfc) .replace('{tt}', json.total) .replace( '{fe}', json.timbreFiscalDigital.selloCFD.substring( json.timbreFiscalDigital.selloCFD.length - 8, json.timbreFiscalDigital.selloCFD.length, ), ); return qrCode; }; const generateStampTable = (json: Cfdi) => { const arr = []; if (json.timbreFiscalDigital) { const fechaHoraCertificacion = json.timbreFiscalDigital.fechaTimbrado; arr.push( [ { colSpan: 1, rowSpan: 8, qr: generateQrCode(json), fit: 140, }, '', '', ], ['', 'NUMERO SERIE CERTIFICADO SAT', exists(json.timbreFiscalDigital.noCertificadoSAT)], ['', 'NUMERO SERIE CERTIFICADO EMISOR', exists(json.noCertificado)], ['', 'FECHA HORA CERTIFICACION', fechaHoraCertificacion], ['', 'FOLIO FISCAL UUID', exists(json.timbreFiscalDigital.uuid)], ['', 'SELLO DIGITAL', breakEveryNCharacters(exists(json.timbreFiscalDigital.selloCFD), 86)], ['', 'SELLO DEL SAT', breakEveryNCharacters(exists(json.timbreFiscalDigital.selloSAT), 86)], ); } arr.push(['', 'CADENA ORIGINAL CC:', { text: breakEveryNCharacters(json.cadenaOriginalCC, 86) }]); return arr; }; const generateAddress = (receptor: Receptor, address?: string) => { const arr = []; const addressArray = []; if (address) { addressArray.push('DOMICILIO:', address); } addressArray.push('USO CFDI:', { colSpan: address ? 1 : 3, text: usosCfdiCatalog[receptor.usoCFDI] ? `${receptor.usoCFDI} - ${usosCfdiCatalog[receptor.usoCFDI]}` : '', }); arr.push(addressArray); if (receptor.residenciaFiscal && receptor.numRegIdTrib) { arr.push([ 'RESIDENCIA FISCAL:', exists(receptor.residenciaFiscal), 'NUMERO ID TRIB.:', exists(receptor.numRegIdTrib), ]); } return arr; }; // generate content array used in PDFMake const generateContent = async (json: Cfdi, logo?: string, text?: string, address?: string) => { let content = []; // this block contains the logo image and general information const header: any = { alignment: 'center', style: 'tableContent', table: { widths: ['auto', 'auto', 'auto'], fontSize: 9, body: [ ['', 'SERIE:', json.serie], ['', 'FOLIO:', json.folio], ['', 'FECHA:', json.fecha], ['', 'EXPEDICION:', json.lugar], [ '', 'COMPROBANTE:', tiposComprobantesCatalog[json.tipoDeComprobante] ? `${json.tipoDeComprobante} - ${tiposComprobantesCatalog[json.tipoDeComprobante]}` : '', ], ], }, layout: 'lightHorizontalLines', }; if (logo) { header.table.body[0][0] = { rowSpan: 5, image: logo, fit: [260, 260] }; header.table.widths = ['*', 'auto', 'auto']; } content.push(header); // space content.push('\n'); // this block contains info. about "emisor" object content.push({ style: 'tableContent', table: { widths: ['auto', '*', 'auto', 'auto'], body: [ [ { text: 'EMISOR', style: 'tableHeader', colSpan: 4, alignment: 'center', }, {}, {}, {}, ], ['NOMBRE:', exists(json.emisor.nombre), 'RFC:', exists(json.emisor.rfc)], [ 'REGIMEN FISCAL:', { colSpan: 3, text: regimenesFiscalesCatalog[json.emisor.regimenFiscal] ? `${json.emisor.regimenFiscal} - ${regimenesFiscalesCatalog[json.emisor.regimenFiscal]}` : '', }, '', ], ], }, layout: 'lightHorizontalLines', }); // space content.push('\n'); // this block contains info. about "receptor" object content.push({ style: 'tableContent', table: { widths: ['auto', '*', 'auto', 'auto'], body: [ [ { text: 'RECEPTOR', style: 'tableHeader', colSpan: 4, alignment: 'center', }, {}, {}, {}, ], ['NOMBRE:', exists(json.receptor.nombre), 'RFC:', exists(json.receptor.rfc)], ...generateAddress(json.receptor, address), ], }, layout: 'lightHorizontalLines', }); // space content.push('\n'); // check type of invoice if (json.tipoDeComprobante.toUpperCase() === 'I' || json.tipoDeComprobante.toUpperCase() === 'E') { // this block contains general info. about the invoice content.push({ style: 'tableContent', table: { widths: [95, '*', 95, '*'], body: [ [ { text: 'DATOS GENERALES DEL COMPROBANTE', style: 'tableHeader', colSpan: 4, alignment: 'center', }, {}, {}, {}, ], [ 'MONEDA:', monedasCatalog[json.moneda] ? `${json.moneda} - ${monedasCatalog[json.moneda]}` : '', 'FORMA PAGO:', formasPagoCatalog[json.formaPago] ? `${json.formaPago} - ${formasPagoCatalog[json.formaPago]}` : '', ], ['TIPO DE CAMBIO:', json.tipoCambio, 'CONDICIONES DE PAGO:', json.condicionesDePago], [ 'CLAVE CONFIRMACION:', json.confirmacion, 'METODO DE PAGO:', metodosPagoCatalog[json.metodoPago] ? `${json.metodoPago} - ${metodosPagoCatalog[json.metodoPago]}` : '', ], ], }, layout: 'lightHorizontalLines', }); // space content.push('\n'); } // this block contains the concepts of the invoice content.push({ style: 'tableList', table: { widths: ['auto', 'auto', 'auto', 'auto', '*', 'auto', 'auto', 'auto', 'auto', 'auto'], body: generateConceptsTable(json.conceptos), }, layout: { fillColor(i: number) { return i % 2 !== 0 ? '#CCCCCC' : null; }, }, }); // space content.push('\n'); // check type of invoice if (json.tipoDeComprobante.toUpperCase() === 'I' || json.tipoDeComprobante.toUpperCase() === 'E') { // this block contains currency related info. content.push({ style: 'tableContent', table: { widths: ['auto', '*', 'auto', '*'], body: [ [ { text: 'CFDI RELACIONADO', style: 'tableHeader', colSpan: 4, alignment: 'center', }, {}, {}, {}, ], [ 'TIPO RELACION:', tiposRelacionesCatalog[json.cfdiRelacionado ? json.cfdiRelacionado.tipoRelacion : ''] ? `${json.cfdiRelacionado.tipoRelacion} - ${ tiposRelacionesCatalog[json.cfdiRelacionado.tipoRelacion] }` : '', 'CFDI RELACIONADO:', json.cfdiRelacionado ? exists(json.cfdiRelacionado.uuid) : '', ], ['SUBTOTAL:', `${formatCurrency(json.subTotal)}`, 'TOTAL:', `${formatCurrency(json.total)}`], [ 'DESCUENTO:', `${formatCurrency(json.descuento)}`, { text: 'IMPORTE CON LETRA:' }, { text: await toCurrency(parseFloat(json.total), json.moneda) }, ], [ 'TOTAL IMP. TRASLADADOS:', `${formatCurrency(existsValue(json.totalImpuestosTrasladados))}`, 'TOTAL IMP. RETENIDOS:', `${formatCurrency(existsValue(json.totalImpuestosRetenidos))}`, ], ], }, layout: 'lightHorizontalLines', }); // space content.push('\n'); } // check type of invoice if (json.tipoDeComprobante.toUpperCase() === 'P') { // this block contains info. about payment content = content.concat(generatePayments(json.pagos)); } if (text) { // observations content.push({ style: 'tableContent', table: { widths: ['*'], body: [[{ text: 'OBSERVACIONES', style: 'tableHeader' }], [text]], }, layout: 'lightHorizontalLines', }); // space content.push('\n'); } // this block contains info. about the stamp content.push({ style: 'tableSat', table: { widths: ['auto', 'auto', '*'], body: generateStampTable(json), }, layout: 'lightHorizontalLines', }); return content; }; /** * Receives a json and returns a pdf content object for pdfmake * @param {Cfdi} json result json from using parseData function */ export const generatePdfContent = async (json: Cfdi, options: Options) => { // look for a base64 image // eslint-disable-next-line const logo = options.image; if (options.cadenaOriginal) json.cadenaOriginalCC = options.cadenaOriginal; const dd: TDocumentDefinitions = { content: await generateContent(json, logo, options.text, options.address), styles: { tableHeader: { bold: true, fontSize: 10, color: 'black', }, tableContent: { fontSize: 8, color: 'black', alignment: 'left', }, tableList: { fontSize: 7, color: 'black', alignment: 'center', }, tableSat: { fontSize: 5, color: 'black', alignment: 'left', }, }, defaultStyle: { // alignment: 'justify' }, footer() { return { style: 'tableContent', table: { widths: ['auto', '*', 'auto', 'auto'], body: [ [ { text: 'Este documento es una representación impresa de un CFDI', style: 'tableList', colSpan: 4, alignment: 'center', }, {}, {}, {}, ], ], }, layout: 'lightHorizontalLines', }; }, }; return dd; };