Representación impresa de factura electrónica del SAT en versión 2.2

Written by

in

,
0
(0)

En el artículo anterior titulado Convirtiendo facturas electrónicas del XML a PDF describo el proceso para crear una representación impresa de una factura electrónica en la versión 2.0, basado en el famoso anexo 20 del SAT.

Ahora les dejo el código fuente del programa para la versión 2.2 (requiere aplicar el archivo de transformación cadenaoriginal_2_2.xslt), que debe usarse a partir del 1o de julio de este año…

#!/usr/bin/perl
#===================================================================#
# Program => CFD_to_PDF_2_2.pl (In Perl 5.0)          version 0.0.1 #
#===================================================================#
# Autor         => Fernando "El Pop" Romo        (pop@cofradia.org) #
# Creation date => 01/jul/2012                                      #
#-------------------------------------------------------------------#
# Info => This program take a CFD file in xml format and convert to #
#         PDF using a template                                      #
#-------------------------------------------------------------------#
#                    (c) 2012 - 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     => '/Users/pop/SAT/util/factura_clean_incuvox_2_2.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        => 49,            # $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');

# 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) = @_;
    my $dec_pos = 2;
    if ($number =~  /\./) {
        my $len = length($number);
        $dec_pos = rindex($number,"\.");
        $dec_pos = $len - $dec_pos - 1 ;
    }
    my $number = sprintf("%.$dec_pos".'f', $number);
    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]->{'cfd:InformacionAduanera'})) {
                $Pedimentos{ $$ref_parte->[$i]->{'cfd:InformacionAduanera'}{numero} }{aduana} = 
                    $$ref_parte->[$i]->{'cfd:InformacionAduanera'}{aduana};
                $Pedimentos{ $$ref_parte->[$i]->{'cfd:InformacionAduanera'}{numero} }{fecha} =
                    $$ref_parte->[$i]->{'cfd:InformacionAduanera'}{fecha};
                push @{ $Pedimentos{ $$ref_parte->[$i]->{'cfd: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->{'cfd:InformacionAduanera'})) {
            $row = $row - 8;
            my $aduana_aux = 'Pedimento: ' . $$ref_parte->{'cfd:InformacionAduanera'}{numero} .
                             ', Aduana: ' . $$ref_parte->{'cfd:InformacionAduanera'}{aduana} .
                             ', fecha: ' . $$ref_parte->{'cfd:InformacionAduanera'}{fecha};
            $row = Wrap_Text(\$aduana_aux,85,$row,70,6,8,1);
        }

        if (exists($$ref_parte->{noIdentificacion})) {
            $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,60,8,10,1);
            if (exists($$ref_concepto->[$i]->{'cfd:Parte'})) {
                $row = Parte(\$$ref_concepto->[$i]->{'cfd: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->{'cfd:Parte'})) {
            $row = Parte(\$$ref_concepto->{'cfd:Parte'},$row);
        }
        $row = $row - 10;
    }
    return $row; 
}

# Datos del CFD

prFontSize (10);
prText( 140, 793, "$Fac->{noCertificado}", 'left');
prText( 140, 780, "$Fac->{noAprobacion}", 'left');
prText( 300, 780, "$Fac->{anoAprobacion}", 'left');
prText( 140, 769, "$Fac->{tipoDeComprobante}",, 'left');
prText( 275, 768, "$Fac->{fecha}",, 'left');
prText( 140, 757, "$Fac->{formaDePago}", 'left');
prText( 140, 745, "$Fac->{metodoDePago}", 'left');
prFontSize (16);
prText( 140, 728, "$Fac->{serie}-$Fac->{folio}", 'left');

# Emisor

prFontSize (10);
prFont('Helvetica-Bold');
prText( 402, 758, "$Fac->{'cfd:Emisor'}{nombre}", 'left');
prFont('Helvetica');
prFontSize (8);
prText( 402, 746, "$Fac->{'cfd:Emisor'}{'cfd:DomicilioFiscal'}{calle} $Fac->{'cfd:Emisor'}{'cfd:DomicilioFiscal'}{noExterior} $Fac->{'cfd:Emisor'}{'cfd:DomicilioFiscal'}{noInterior}", 'left');
prText( 402, 736, "Col. $Fac->{'cfd:Emisor'}{'cfd:DomicilioFiscal'}{colonia} C.P. $Fac->{'cfd:Emisor'}{'cfd:DomicilioFiscal'}{codigoPostal}", 'left');
prText( 402, 726, "$Fac->{'cfd:Emisor'}{'cfd:DomicilioFiscal'}{municipio}, $Fac->{'cfd:Emisor'}{'cfd:DomicilioFiscal'}{estado}", 'left');
prText( 402, 716, "$Fac->{'cfd:Emisor'}{'cfd:DomicilioFiscal'}{localidad}, $Fac->{'cfd:Emisor'}{'cfd:DomicilioFiscal'}{pais}", 'left');
prText( 402, 706, 'info@incuvox.com', 'left');
prText( 480, 706, 'http://incuvox.com', 'left');
prText( 402, 696, 'Tel. +52(55)4162-5800', 'left');
prFontSize (10);
prText( 402, 684, "R.F.C.:", 'left');
prFont('Helvetica-Bold');
prText( 437, 684, "$Fac->{'cfd:Emisor'}{rfc}", 'left');
prFont('Helvetica');

# Lugar de emision

prFontSize (8);
prText( 402, 650, "$Fac->{'cfd:Emisor'}{'cfd:ExpedidoEn'}{calle} $Fac->{'cfd:Emisor'}{'cfd:ExpedidoEn'}{noExterior} $Fac->{'cfd:Emisor'}{'cfd:ExpedidoEn'}{noInterior}", 'left');
prText( 402, 640, "$Fac->{'cfd:Emisor'}{'cfd:ExpedidoEn'}{colonia}, C.P. $Fac->{'cfd:Emisor'}{'cfd:ExpedidoEn'}{codigoPostal}", 'left');
prText( 402, 630, "$Fac->{'cfd:Emisor'}{'cfd:ExpedidoEn'}{municipio}, $Fac->{'cfd:Emisor'}{'cfd:ExpedidoEn'}{estado}", 'left');
prText( 402, 620, "$Fac->{'cfd:Emisor'}{'cfd:ExpedidoEn'}{localidad}, $Fac->{'cfd:Emisor'}{'cfd:ExpedidoEn'}{pais}", 'left');

# Facturado A

prFontSize (10);
prFont('Helvetica-Bold');
prText( 20, 690, "$Fac->{'cfd:Receptor'}{nombre}", 'left');
prFont('Helvetica');
prText( 20, 678, "$Fac->{'cfd:Receptor'}{'cfd:Domicilio'}{calle} $Fac->{'cfd:Receptor'}{'cfd:Domicilio'}{noExterior} $Fac->{'cfd:Receptor'}{'cfd:Domicilio'}{noInterior}" , 'left');
prText( 20, 666, "$Fac->{'cfd:Receptor'}{'cfd:Domicilio'}{colonia}, C.P. $Fac->{'cfd:Receptor'}{'cfd:Domicilio'}{codigoPostal}", 'left');
prText( 20, 654, "$Fac->{'cfd:Receptor'}{'cfd:Domicilio'}{municipio}, $Fac->{'cfd:Receptor'}{'cfd:Domicilio'}{localidad}", 'left');
prText( 20, 642, "$Fac->{'cfd:Receptor'}{'cfd:Domicilio'}{estado}, $Fac->{'cfd:Receptor'}{'cfd:Domicilio'}{pais}", 'left');
prText( 20, 620, "R.F.C.:", 'left');
prFont('Helvetica-Bold');
prText( 56, 620, "$Fac->{'cfd:Receptor'}{rfc}", 'left');
prFont('Helvetica');

# partidas

my $row = Concepto(\$Fac->{'cfd:Conceptos'}{'cfd:Concepto'},579);

# Addenda

if (exists($Fac->{'cfd:Addenda'}{Incuvox}{Note})) {
    prFont('Helvetica-Oblique');
    my $nota = 'Nota: ' .  $Fac->{'cfd:Addenda'}{Incuvox}{Note};
    Wrap_Text(\$nota,85,($row - 3),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,314,70,8,10,1);

# Totales

prFontSize (8);
prText( 580, 314, Dinero($Fac->{subTotal}) . " $Moneda", 'right');
prText( 580, 303, Dinero($Fac->{descuento}) . " $Moneda", 'right');
prText( 458, 292, '16', 'right');
prText( 580, 292, Dinero($Fac->{'cfd:Impuestos'}{totalImpuestosTrasladados}) . " $Moneda", 'right');
prFont('Helvetica-Bold');
prText( 580, 281, Dinero($Fac->{total}) . " $Moneda", 'right');
prFont('Helvetica');

# Cadena Original

prFont('Courier');
my $cadena = Escape_UTF8(qx(xsltproc ~/SAT/xslt/cadenaoriginal_2_2.xslt $file.xml));
Wrap_Text(\$cadena,20,252,155,6,8,0);

# Sello

Wrap_Text(\$Fac->{sello},75,100,130,8,10,0);

# Verifica si esta cancelada y pone sello
if (exists($Fac->{'cfd:Addenda'}{Incuvox}{Factura}{Status})) {
    if (lc($Fac->{'cfd:Addenda'}{Incuvox}{Factura}{Status}) eq 'cancelada') {
        prAdd("q\nBT\n/" . prFont('Courier') . " 2 Tf\n40 45 -45 40 100 160 Tm\n1 0 0 rg\n(CANCELADO) Tj\nET\nQ\n");
        prAdd("q\nBT\n/" . prFont('Courier') . " .3 Tf\n40 45 -45 40 190 220 Tm\n1 0 0 rg\n(No puede ser usado como comprobante fiscal) Tj\nET\nQ\n");
    }
}

prEnd();

Esto procesa un XML como este:





    
            
            
            
    

    
              
    

    
        
                  
                         
                  
        
        
                  
                         
                  
                  
                         
                  
                  
                         
                  
                  
                         
                  
        
        
                  
                         
                  
        
        
        
     

    
               
                   
                   
    

    
        
                 
                 
                 Equipo solicitado por Juan Camanei
        
    
 

Los cambios importantes son el uso del prefijo “cfd:” en los tags de XML que usa la factura electrónica, así como la inclusión de los campos “Moneda”, “TipoCambio”, “metodoDePago” y en el emisor el campo “Regimen”. En el artículo anterior describo las dependencias de este programa.

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

One response to “Representación impresa de factura electrónica del SAT en versión 2.2”

  1. Mauricio Baeza Avatar

    En CFDI, el prefijo CFDI esta claro tanto el XSD: http://www.sat.gob.mx/cfd/3/cfdv32.xsd
    como en el XSLT: http://www.sat.gob.mx/sitio_internet/cfd/3/cadenaoriginal_3_0/cadenaoriginal_3_2.xslt

    Pero para CFD, en el XLST, si viene el prefijo CFD: http://www.sat.gob.mx/sitio_internet/cfd/2/cadenaoriginal_2_2/cadenaoriginal_2_2.xslt
    pero en el XSD no: http://www.sat.gob.mx/sitio_internet/cfd/2/cfdv22.xsd

    El validador del SAT: https://www.consulta.sat.gob.mx/SICOFI_WEB/ModuloECFD_Plus/ValidadorComprobantes/Validador.asp
    pasa correctamente la validación con y sin prefijo CFD, pero muchos validadores que están pululando por ahí, incluyendo los de varios que se creen dueños de la verdad en cuestión de facturacíon electrónica.

    Lo menciono para que tomen sus precauciones.

    Saludos

Leave a Reply