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


 


    
            
            
    
 
    
              
    
 
    
         
        
                  
                         
                        
                  
                  
                         
                         
                 
                  
                         
                         
                 
          
     

    
               
                   
               
    

    
        
    
 

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.

Comments

2 responses to “Convirtiendo facturas electrónicas del XML a PDF”

  1. el_pjuan Avatar

    Se nota que el diseño no se me da jajaja, mi versión es extremadamente mas simplista

  2. phylevn Avatar

    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