Convirtiendo facturas electrónicas del XML a PDF

0
(0)

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:

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:





How useful was this post?

Click on a star to rate it!

Average rating 0 / 5. Vote count: 0

No votes so far! Be the first to rate this post.

2 thoughts on “Convirtiendo facturas electrónicas del XML a PDF

  1. 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

Leave a Reply