@ocelotlstudio/cfdi-pdf
Version:
Creates a pdf based on an XML CFDI
566 lines (555 loc) • 18.6 kB
text/typescript
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;
};