Pues hemos mencionado la generación de sellos para las facturas electrónicas del SAT, su validación y ahora vamos a dar cierre al ciclo de la emisión, con lo que se le conoce como la representación impresa del comprobante fiscal digital.
Me dí a la tarea de hacer un pequeño script en Perl que convierte el XML de la factura electrónica a su versión en PDF, tomando en cuenta que yo lo hago por Medios Propios (versión 2.0).
Este utiliza varios módulos para Perl de CPAN:
- PDF::Reuse para la manipulación de PDF’s
- Lingua::ES::Numeros para convertir números a leyenda en letra
- XML::Simple para la manipulación del XML de la factura electrónica
- Text::Wrap::Smart para poner bonito el texto
Para instalar estos módulos sigan la guía de instalación de CPAN.
Es necesario para el manejo de la cadena original tener instalado el xsltproc y el archivo descriptor de xslt del SAT para usarlo: cadenaoriginal_2_0.xslt (liga al ftp del SAT).
Mis ejemplos los pueden bajar aquí (archivo CFD_PDF.tar.gz, mide 285 Kb).
El programa usa un machote de PDF previamente hecho con alguna herramienta o procesador de texto. En mi caso use un programa para Mac llamado Omnigraffle y generé un template (factura.pdf).
Tomé un logo de prueba y lo dejé en formato jpeg (logo.jpg) y deje un ejemplo de un XML de factura electrónica para transformalo a PDF (Fac_Sample.xml).
El programa es este (que esta contenido en el archivo de ejemplos mencionados anteriormente):
CFD_to_PDF.pl
#!/usr/bin/perl
#===================================================================#
# Program => CFD_to_PDF.pl (In Perl 5.0) version 0.0.1 #
#===================================================================#
# Autor => Fernando "El Pop" Romo (pop@cofradia.org) #
# Creation date => 06/feb/2011 #
#-------------------------------------------------------------------#
# Info => This program take a CFD file in xml format and convert to #
# PDF using a template #
#-------------------------------------------------------------------#
# (c) 2011 - Fernando Romo #
#-------------------------------------------------------------------#
# Release under the GNU/GPL License v3.0 #
#===================================================================#
# Load Modules
use strict;
use PDF::Reuse;
use Lingua::ES::Numeros ":constants";
use XML::Simple;
use Text::Wrap::Smart qw(wrap_smart);
my $file = $ARGV[0];
my $Fac = XMLin($file);
$file =~ s/.xml$//g;
prFile ( { Name => "$file.pdf",
HideToolbar => 1, # 1 or 0
HideMenubar => 1, # 1 or 0
HideWindowUI => 1, # 1 or 0
FitWindow => 1, # 1 or 0
CenterWindow => 1 } ); # 1 or 0
prForm ( { file => 'factura.pdf', # template file
page => 1, # page number (of imported template)
adjust => 0, # try to fill the media box
effect => 0, # action to be taken
tolerant => 1, # continue even with an invalid form
x => -13, # $x points from the left
y => 50, # $y points from the bottom
rotate => 0, # rotate
size => 1, # multiply everything by $size
xsize => 1, # multiply horizontally by $xsize
ysize => 1, } ); # multiply vertically by $ysize
prFont('Helvetica');
# ejemplo insertando Logo
my $intName = prJpeg("logo.jpg", 300, 104);
my $str = "q\n";
$str .= "110 0 0 38 430 773 cm\n";
$str .= "/$intName Do\n";
$str .= "Q\n";
prAdd($str);
# Moneda
my $Sufijo = 'PESOS';
my $Moneda = 'M.N.';
if (exists($Fac->{Moneda})) {
if ($Fac->{Moneda} eq 'MXN') {
$Sufijo = 'PESOS';
$Moneda = 'M.N.';
}
else {
$Moneda = "$Fac->{Moneda}";
$Sufijo = '';
}
}
sub Escape_UTF8 {
my @out = @_;
for (@out) {
s/á/\x{e1}/g;
s/é/\x{e9}/g;
s/í/\x{ed}/g;
s/ó/\x{f3}/g;
s/ú/\x{fa}/g;
s/Á/\x{c1}/g;
s/É/\x{c9}/g;
s/Í/\x{cd}/g;
s/Ó/\x{d3}/g;
s/Ú/\x{da}/g;
s/ñ/\x{f1}/g;
s/Ñ/\x{d1}/g;
}
return wantarray ? @out : $out[0];
}
sub Wrap_Text {
my ($ref_string, $col, $row, $width, $font_size, $space, $split) = @_;
my @text = wrap_smart($$ref_string, { no_split => $split, max_msg_size => $width,} );
prFontSize ($font_size);
foreach my $linea (@text) {
prText( $col, $row, $linea, 'left');
$row = $row - $space;
}
return $row + $space;
}
sub Dinero {
my $number = sprintf "%.2f", shift @_;
1 while $number =~ s/^(-?\d+)(\d\d\d)/$1,$2/;
$number =~ s/^(-?)/$1\$/;
return $number;
}
sub Cantidad {
my $number = sprintf "%.2f", shift @_;
1 while $number =~ s/^(-?\d+)(\d\d\d)/$1,$2/;
return $number;
}
sub Parte {
my ($ref_parte, $row) = @_;
my %Pedimentos = ();
my @SinPedimento = ();
my $parte_aux = 'N.S.: ';
if (ref($$ref_parte) eq 'ARRAY') {
for (my $i=0; $i <= (@{ $$ref_parte } - 1) ; $i++) {
if (exists($$ref_parte->[$i]->{InformacionAduanera})) {
$Pedimentos{ $$ref_parte->[$i]->{InformacionAduanera}{numero} }{aduana} =
$$ref_parte->[$i]->{InformacionAduanera}{aduana};
$Pedimentos{ $$ref_parte->[$i]->{InformacionAduanera}{numero} }{fecha} =
$$ref_parte->[$i]->{InformacionAduanera}{fecha};
push @{ $Pedimentos{ $$ref_parte->[$i]->{InformacionAduanera}{numero} }{serie} },
$$ref_parte->[$i]->{noIdentificacion};
}
else {
push @SinPedimento, $$ref_parte->[$i]->{noIdentificacion};
}
}
foreach my $numero (sort keys %Pedimentos) {
$parte_aux = 'N.S.: ';
$row = $row - 8;
$row = Wrap_Text(\"Pedimento: $numero, Aduana: $Pedimentos{$numero}{aduana}, Fecha: $Pedimentos{$numero}{fecha}",85,$row,70,6,8,1);
for (my $x=0; $x <= (@{ $Pedimentos{$numero}{serie} } - 1) ; $x++) {
$parte_aux .= "$Pedimentos{$numero}{serie}[$x], ";
}
$parte_aux =~ s/, $//;
$row = $row - 8;
$row = Wrap_Text(\$parte_aux,85,$row,70,6,8,1);
}
if ($#SinPedimento >= 0) {
$parte_aux = 'N.S.: ';
foreach my $serie (sort @SinPedimento) {
$parte_aux .= $serie . ', ';
}
$parte_aux =~ s/, $//;
$row = $row - 8;
$row = Wrap_Text(\$parte_aux,85,$row,70,6,8,1);
}
}
else {
if (exists($$ref_parte->{InformacionAduanera})) {
$row = $row - 8;
my $aduana_aux = 'Pedimento: ' . $$ref_parte->{InformacionAduanera}{numero} .
', Aduana: ' . $$ref_parte->{InformacionAduanera}{aduana} .
', fecha: ' . $$ref_parte->{InformacionAduanera}{fecha};
$row = Wrap_Text(\$aduana_aux,85,$row,70,6,8,1);
}
$row = $row - 8;
$parte_aux .= $$ref_parte->{noIdentificacion};
$row = Wrap_Text(\$parte_aux,85,$row,70,6,8,1);
}
return $row;
}
sub Concepto {
my ($ref_concepto, $row) = @_;
if (ref($$ref_concepto) eq 'ARRAY') {
for (my $i=0; $i <= (@{ $$ref_concepto } - 1) ; $i++) {
prFontSize (8);
prText( 18, $row, $$ref_concepto->[$i]->{noIdentificacion}, 'left');
prText( 375, $row, Cantidad($$ref_concepto->[$i]->{cantidad}), 'right');
prText( 477, $row, Dinero($$ref_concepto->[$i]->{valorUnitario}) . " $Moneda", 'right');
prText( 580, $row, Dinero($$ref_concepto->[$i]->{importe}) . " $Moneda", 'right');
$row = Wrap_Text(\$$ref_concepto->[$i]->{descripcion},85,$row,55,8,10,1);
if (exists($$ref_concepto->[$i]->{Parte})) {
$row = Parte(\$$ref_concepto->[$i]->{Parte},$row);
}
$row = $row - 10;
}
}
else {
prFontSize (8);
prText( 18, $row, $$ref_concepto->{noIdentificacion}, 'left');
prText( 375, $row, Cantidad($$ref_concepto->{cantidad}), 'right');
prText( 477, $row, Dinero($$ref_concepto->{valorUnitario}) . " $Moneda", 'right');
prText( 580, $row, Dinero($$ref_concepto->{importe}) . " $Moneda", 'right');
$row = Wrap_Text(\$$ref_concepto->{descripcion},85,$row,55,8,10,1);
if (exists($$ref_concepto->{Parte})) {
$row = Parte(\$$ref_concepto->{Parte},$row);
}
$row = $row - 10;
}
return $row;
}
# Datos del CSD
prFontSize (10);
prText( 140, 790, "$Fac->{noCertificado}", 'left');
prText( 140, 778, "$Fac->{noAprobacion}", 'left');
prText( 300, 778, "$Fac->{anoAprobacion}", 'left');
prText( 140, 766, "$Fac->{tipoDeComprobante}",, 'left');
prText( 275, 766, "$Fac->{fecha}",, 'left');
prText( 140, 754, "$Fac->{formaDePago}", 'left');
prFontSize (16);
prText( 140, 736, "$Fac->{serie}-$Fac->{folio}", 'left');
# Emisor
prFontSize (10);
prFont('Helvetica-Bold');
prText( 402, 768, "$Fac->{Emisor}{nombre}", 'left');
prFont('Helvetica');
prFontSize (8);
prText( 402, 756, "$Fac->{Emisor}{DomicilioFiscal}{calle} $Fac->{Emisor}{DomicilioFiscal}{noExterior} $Fac->{Emisor}{DomicilioFiscal}{noInterior}", 'left');
prText( 402, 746, "Col. $Fac->{Emisor}{DomicilioFiscal}{colonia} C.P. $Fac->{Emisor}{DomicilioFiscal}{codigoPostal}", 'left');
prText( 402, 736, "$Fac->{Emisor}{DomicilioFiscal}{municipio}, $Fac->{Emisor}{DomicilioFiscal}{estado}", 'left');
prText( 402, 726, "$Fac->{Emisor}{DomicilioFiscal}{localidad}, $Fac->{Emisor}{DomicilioFiscal}{pais}", 'left');
prFontSize (10);
prText( 402, 684, "R.F.C.:", 'left');
prFont('Helvetica-Bold');
prText( 437, 684, "$Fac->{Emisor}{rfc}", 'left');
prFont('Helvetica');
# Lugar de emision
prFontSize (8);
prText( 402, 650, "$Fac->{Emisor}{ExpedidoEn}{calle} $Fac->{Emisor}{ExpedidoEn}{noExterior} $Fac->{Emisor}{ExpedidoEn}{noInterior}", 'left');
prText( 402, 640, "$Fac->{Emisor}{ExpedidoEn}{colonia}, C.P. $Fac->{Emisor}{ExpedidoEn}{codigoPostal}", 'left');
prText( 402, 630, "$Fac->{Emisor}{ExpedidoEn}{municipio}, $Fac->{Emisor}{ExpedidoEn}{estado}", 'left');
prText( 402, 620, "$Fac->{Emisor}{ExpedidoEn}{localidad}, $Fac->{Emisor}{ExpedidoEn}{pais}", 'left');
# Facturado A
prFontSize (10);
prFont('Helvetica-Bold');
prText( 20, 690, "$Fac->{Receptor}{nombre}", 'left');
prFont('Helvetica');
prText( 20, 678, "$Fac->{Receptor}{Domicilio}{calle} $Fac->{Receptor}{Domicilio}{noExterior} $Fac->{Receptor}{Domicilio}{noInterior}" , 'left');
prText( 20, 666, "$Fac->{Receptor}{Domicilio}{colonia}, C.P. $Fac->{Receptor}{Domicilio}{codigoPostal}", 'left');
prText( 20, 654, "$Fac->{Receptor}{Domicilio}{municipio}, $Fac->{Receptor}{Domicilio}{localidad}", 'left');
prText( 20, 642, "$Fac->{Receptor}{Domicilio}{estado}, $Fac->{Receptor}{Domicilio}{pais}", 'left');
prText( 20, 620, "R.F.C.:", 'left');
prFont('Helvetica-Bold');
prText( 56, 620, "$Fac->{Receptor}{rfc}", 'left');
prFont('Helvetica');
# partidas
my $row = Concepto(\$Fac->{Conceptos}{Concepto},575);
# Addenda
if (exists($Fac->{Addenda}{Incuvox}{Note})) {
prFont('Helvetica-Oblique');
my $nota = 'Nota: ' . $Fac->{Addenda}{Incuvox}{Note};
Wrap_Text(\$nota,85,($row - 10),80,8,10,1);
prFont('Helvetica');
}
# Numero en letras
my ( $pesos, $centavos ) = split( /\./, $Fac->{total} );
$pesos = 0 unless( $pesos );
$centavos = 0 unless( $centavos );
my $obj = new Lingua::ES::Numeros ('MAYUSCULAS' => 1, ACENTOS => 0);
my $letrero = $obj->cardinal($pesos) . " $Sufijo " . "$centavos/100 $Moneda\n";
Wrap_Text(\$letrero,65,334,70,8,10,1);
# Totales
prFontSize (8);
prText( 580, 334, Dinero($Fac->{descuento}) . " $Moneda", 'right');
prText( 580, 323, Dinero($Fac->{subTotal}) . " $Moneda", 'right');
prText( 458, 312, '16', 'right');
prText( 580, 312, Dinero($Fac->{Impuestos}{totalImpuestosTrasladados}) . " $Moneda", 'right');
prFont('Helvetica-Bold');
prText( 580, 301, Dinero($Fac->{total}) . " $Moneda", 'right');
prFont('Helvetica');
# Cadena Original
prFont('Courier');
my $cadena = Escape_UTF8(qx(xsltproc ~/SAT/xslt/cadenaoriginal_2_0.xslt $file.xml));
Wrap_Text(\$cadena,20,269,155,6,8,0);
# Sello
Wrap_Text(\$Fac->{sello},75,124,130,8,10,0);
prEnd();
Para utilizarlo (instalando mis scripts y las dependencias de CPAN previamente) solo tendran que hacer esto:
./CFD_to_PDF.pl Fac_Sample.xml
El XML que procesamos para este ejemplo:
Fac_Sample.xml
y el resultado queda así:
Queda al lector ajustar su PDF de template y la salida deseada.
Autor: Fernando “El Pop” Romo (pop at cofradia.org), twitter @El_Pop
Si este artículo te es útil, ayúdanos con un donativo de 100.00 MXN para poder seguir operando:

Leave a Reply
You must be logged in to post a comment.