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
<?xml version="1.0" encoding="UTF-8"?> <Comprobante xmlns="http://www.sat.gob.mx/cfd/2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sat.gob.mx/cfd/2 http://www.sat.gob.mx/sitio_internet/cfd/2/cfdv2.xsd http://incuvox.com/cfd/incuvox.xsd" version="2.0" serie="AA" folio="1619" fecha="2011-03-27T18:20:36" sello="rVoq96VkMeQ6gt993DnVuaWiAXwjNSeJKT27gJ8pOuC/tHo9SzvB6t0zM0hj03ivHC/gWvC2kK9nQ5JxCiJ96f08pyGP00jSD4z0/LCplTxb3nASc7/gDV5L42RW0drh7dtV2UWMfrrZE7Xfj9l38kigH5+cvo8/V8IiJLBKmLc=" noAprobacion="263844" anoAprobacion="2010" tipoDeComprobante="ingreso" formaDePago="Contado en una sola exibición" noCertificado="00001000000102302081" certificado=" MIIE/TCCA+WgAwIBAgIUMzAwMDEwMDAwMDAxMDAwMDA4MDcwDQYJKoZIhvcNAQEFBQAwggFvMRgwFgYDVQQDDA9BLkMuIGRlIHBydWViYXMxLzAtBgNVBAoMJlNlcnZpY2lvIGRlIEFkbWluaXN0cmFjacOzbiBUcmlidXRhcmlhMTgwNgYDVQQLDC9BZG1pbmlzdHJhY2nDs24gZGUgU2VndXJpZGFkIGRlIGxhIEluZm9ybWFjacOzbjEpMCcGCSqGSIb3DQEJARYaYXNpc25ldEBwcnVlYmFzLnNhdC5nb2IubXgxJjAkBgNVBAkMHUF2LiBIaWRhbGdvIDc3LCBDb2wuIEd1ZXJyZXJvMQ4wDAYDVQQRDAUwNjMwMDELMAkGA1UEBhMCTVgxGTAXBgNVBAgMEERpc3RyaXRvIEZlZGVyYWwxEjAQBgNVBAcMCUNveW9hY8OhbjEVMBMGA1UELRMMU0FUOTcwNzAxTk4zMTIwMAYJKoZIhvcNAQkCDCNSZXNwb25zYWJsZTogSMOpY3RvciBPcm5lbGFzIEFyY2lnYTAeFw0xMDA3MzAxNjU4NDZaFw0xMjA3MjkxNjU4NDZaMIGWMRIwEAYDVQQDDAlNYXRyaXogU0ExEjAQBgNVBCkMCU1hdHJpeiBTQTESMBAGA1UECgwJTWF0cml6IFNBMSUwIwYDVQQtExxBQUEwMTAxMDFBQUEgLyBBQUFBMDEwMTAxQUFBMR4wHAYDVQQFExUgLyBBQUFBMDEwMTAxSERGUlhYMDExETAPBgNVBAsMCFVuaWRhZCA4MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDVvLjg4dC8rIWzeRV6v+ZxYTpzgiK11bHLeYRYF8Pho+1fTeRn7oOUdGfYSUEsqXV8s99F+rdJeA9Ma9iAt5zYBW+qkWlr8NDkkxQyMiOv8oEoJiXFsNWekAhnniyFoPn9pbA1FVBfBsaIe82HKi+Vq0Nd0QKh28afj2f6yhwZnwIDAQABo4HqMIHnMAwGA1UdEwEB/wQCMAAwCwYDVR0PBAQDAgbAMB0GA1UdDgQWBBSXXSAB98HZaYvKFyKXq5u8z4SG+zAuBgNVHR8EJzAlMCOgIaAfhh1odHRwOi8vcGtpLnNhdC5nb2IubXgvc2F0LmNybDAzBggrBgEFBQcBAQQnMCUwIwYIKwYBBQUHMAGGF2h0dHA6Ly9vY3NwLnNhdC5nb2IubXgvMB8GA1UdIwQYMBaAFOtZfQQimlONnnEaoFiWKfU54KDFMBAGA1UdIAQJMAcwBQYDKgMEMBMGA1UdJQQMMAoGCCsGAQUFBwMCMA0GCSqGSIb3DQEBBQUAA4IBAQAlyBvfqLEsWYW11levSQLbsvCpaWmIqtbhfM65Ly+b3848+S3WF9qZJLbH6NkViGUKy9WtTOwegicPSY/dihx1ZDOplARutbrscpLGqwfKsg/qh7ppK7CwqZv5rvrjnaflI8KDV9b+2hsyI0oGY9kVnZb0GgpNZMAKvUmvqXq5Z4ehoNWJmvixV7MgnX/heCZEcKZbR1pIoropUgiR5M8TUF9SUQlPPyRHgNA/v6vNwwZT+JYa62x65IrguUgLCg3DJgo4hk4zADM81Irn3jiTLBZcF2LTQageqxYxSj8MCWz7vGoB8Kv70CLY+Xin/IscCmZ2ohbG636sFNi6so4T" subTotal="1450.50" descuento="0" Moneda="USD" TipoCambio="12.13" total="1682.58"> <Emisor rfc="COS101109Q36" nombre="Cofradia Software, S.A. de C.V."> <DomicilioFiscal calle="Av. División del Norte" noExterior="1354" noInterior="202" colonia="Letrán-Valle" localidad="Distrito Federal" referencia="Esquina con Miguel Laurent" municipio="Benito Juárez" estado="Distrito Federal" pais="México" codigoPostal="03650"/> <ExpedidoEn calle="Av. División del Norte" noExterior="1354" noInterior="202" colonia="Letrán-Valle" localidad="Distrito Federal" municipio="Benito Juárez" estado="Distrito Federal" pais="México" codigoPostal="03650"/> </Emisor> <Receptor rfc="JCC960111P76" nombre="Juan Camanei Company, S.A. de C.V."> <Domicilio calle="Av. Insurgentes Sur" noExterior="1600" noInterior="301" colonia="Guadalupe Inn" localidad="Distrito Federal" municipio="Álvaro Obregón" estado="Distrito Federal" pais="México" codigoPostal="01020"/> </Receptor> <Conceptos> <Concepto cantidad="10" unidad="Hora" noIdentificacion="SPREDV" descripcion="hora de servicios profesionales en análisis de Redes y VoIP" valorUnitario="120.00" importe="1200.00"/> <Concepto cantidad="3" unidad="pz" noIdentificacion="6731i" descripcion="TELEFONO AASTRA MOD. 6731i SIP" valorUnitario="83.50" importe="250.50"> <Parte cantidad="1.00" noIdentificacion="0D10361C9D" descripcion="TELEFONO AASTRA MOD. 6731i SIP"> <InformacionAduanera numero="37780005637" fecha="2010-11-05" aduana="LAREDO"> </InformacionAduanera> </Parte> <Parte cantidad="1.00" noIdentificacion="0D10361C91" descripcion="TELEFONO AASTRA MOD. 6731i SIP"> <InformacionAduanera numero="37780005637" fecha="2010-11-05" aduana="LAREDO"> </InformacionAduanera> </Parte> <Parte cantidad="1.00" noIdentificacion="0D10361CAF" descripcion="TELEFONO AASTRA MOD. 6731i SIP"> <InformacionAduanera numero="37780005699" fecha="2010-11-08" aduana="LAREDO"> </InformacionAduanera> </Parte> </Concepto> </Conceptos> <Impuestos totalImpuestosTrasladados="232.08"> <Traslados> <Traslado impuesto="IVA" tasa="16.00" importe="232.08"/> </Traslados> </Impuestos> <Addenda> <Incuvox Note="Servicios solicitadas por la gente que quiere hacer facturas"/> </Addenda> </Comprobante> |
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:
Se nota que el diseño no se me da jajaja, mi versión es extremadamente mas simplista
Interesante. Otra forma podría ser que diseñaras tu plantilla en un documento de OpenOffice y solo le pasaras los parámetros con algún script, en este caso con PHP, así el diseño puede modificarse(wysiwyg) sin necesidad de modificar el código, esto se puede hacer con la siguiente librería..
http://tinydoc.unesolution.fr/
Más info aquí:
http://blog.oaxrom.com/index.php/blog/show/Generando-reportes-en-PHP-usando-plantillas-creadas-con-OpenOffice-%28WYSIWYG%29..html