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:
Leave a Reply
You must be logged in to post a comment.