Generacion Mensaje XML de Comprobante Fiscal Digital
Factura Electronica

Contenido/contents:
Introducion
Firma
Codigo Fuente
Uso de la funcion
Pagina anterior/Previous page

Introduccion
Esta funcion cumple las reglas estipuladas en el famoso Anexo 20 que publico la Secretaria de Hacienda y Credito Publico en el Diario Oficial de la Federacion de fecha 1 de septiembre del 2004.
Ademas esta version ya cumple con los requisitos de la vesion 2 publicada en julio del 2006 Anexo 20, es la famosa version 2 del formato que ya incluye mas campos para cuplir con la ley del IVA entre otras cosas.
Firma
El siguiente programa hace refrencia a unos archivos de firma (.key.pem). Estos son los arhivos proporcionados por el SAT y llamados 'Certificados de Firma Digital', que son archivos hijos de los archivos de Firma Electronica Avanzada (FEA) pero se genera uno por cada localidad o sucursal que emite los Comprobantes Fiscales Digitales (CFD).
El 'problema' es que los archivos que proporciona el SAT estan en formato DER y PHP los requiere en formato PEM. En esta pagina explico los pasos que segui para convertir los archivos.
Codigo Fuente
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
<?php
//
// +---------------------------------------------------------------------------+
// | satxmlsv2.php : Procesa el arreglo asociativo de intercambio y genera un  |
// |               mensaje XML con los requisitos del SAT de la version 2      |
// |               publicada en el DOF del 5 de julio del 2006.                |
// |                                                                           |
// |               Si se incluye un texto en edidata se agrega como Addenda    |
// +---------------------------------------------------------------------------+
// | Copyright (c) 2005  Fabrica de Jabon la Corona, SA de CV                  |
// +---------------------------------------------------------------------------+
// | This program is free software; you can redistribute it and/or             |
// | modify it under the terms of the GNU General Public License               |
// | as published by the Free Software Foundation; either version 2            |
// | of the License, or (at your option) any later version.                    |
// |                                                                           |
// | This program is distributed in the hope that it will be useful,           |
// | but WITHOUT ANY WARRANTY; without even the implied warranty of            |
// | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the             |
// | GNU General Public License for more details.                              |
// |                                                                           |
// | You should have received a copy of the GNU General Public License         |
// | along with this program; if not, write to the Free Software               |
// | Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA|
// +---------------------------------------------------------------------------|
// | Autor: Fernando Ortiz <fortiz@lacorona.com.mx>                            |
// +---------------------------------------------------------------------------+
// | 10/ago/2006  Toma del arreglo asociativo de entrada el nombre y numero de |
// |              de certificado                                               |
// |                                                                           |
// | 7/dic/2006  Como los de Levicom son tan necios que agregan los elemntos   |
// |             de schema ellos mismos NO los debo de agregar yo, aunque      |
// |             el sat diga que si. se queda como opcional                    |
// |                                                                           |
// |11/dic/2006  Se corrige el orden de campos para la cadena original, tipo   |
// |             de comprobante va despues de ano aprobacion.                  |
// |                                                                           |
// | 5/ene/2007  Se le quitan todos los espacios en blanco por uno solo, regla |
// |             5a y 5d del anexo 20, inciso D.                               |
// +---------------------------------------------------------------------------+
//
 
function satxmlsv2($arr, $edidata=false, $dir="./tmp/",$nodo="",$esquemas=true) {
// {{{  Parametros generales
global $xml, $cadena_original, $conn;
error_reporting(E_ALL);
$cadena_original='||';
$noatt=  array();
$nufa = $arr['serie'].$arr['folio'];    // Junta el numero de factura   serie + folio
// }}}
// {{{  Datos generales del Comprobante
$xml = new DOMdocument("1.0","UTF-8");
$root = $xml->createElement("Comprobante");
$root = $xml->appendChild($root);
if ($esquemas==true) {
    # 7/Dic/2006  porque los necios de Levicom (para chedraui) no quieren estos elmentos, dicen
    #             que su validador lo agrega
    cargaAtt($root, array("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"
                         )
                );
}
                       
cargaAtt($root, array("version"=>"2.0",
                      "serie"=>$arr['serie'],
                      "folio"=>$arr['folio'],
                      "fecha"=>xml_fech($arr['fecha']),
                      "sello"=>"@",
                      "noAprobacion"=>$arr['noAprobacion'],
                      "anoAprobacion"=>$arr['anoAprobacion'],
                      "tipoDeComprobante"=>$arr['tipoDeComprobante'],
                      "formaDePago"=>$arr['formaDePago'],
                      "noCertificado"=>$arr['noCertificado'],
                      "certificado"=>"@",
                      "subTotal"=>$arr['subTotal'],
                      "total"=>$arr['total']
                   )
                );
// }}}
// {{{ Datos del Emisor
$emisor = $xml->createElement("Emisor");
$emisor = $root->appendChild($emisor);
cargaAtt($emisor, array("rfc"=>$arr['Emisor']['rfc'],
                       "nombre"=>$arr['Emisor']['nombre']
                   )
                );
$domfis = $xml->createElement("DomicilioFiscal");
$domfis = $emisor->appendChild($domfis);
cargaAtt($domfis, array("calle"=>"CARLOS B. ZETINA",
                        "noExterior"=>"80",
                        "noInterior"=>"",
                        "colonia"=>"INDUSTRIAL XALOSTOC",
                        "localidad"=>"ECATEPEC DE MORELOS",
                        "municipio"=>"ECATEPEC",
                        "estado"=>"MEXICO",
                        "pais"=>"MEXICO",
                        "codigoPostal"=>"55348"
                   )
                );
$expedido = $xml->createElement("ExpedidoEn");
$expedido = $emisor->appendChild($expedido);
cargaAtt($expedido, array("calle"=>$arr['Emisor']['ExpedidoEn']['calle'],
                        "noExterior"=>$arr['Emisor']['ExpedidoEn']['noExterior'],
                        "noInterior"=>$arr['Emisor']['ExpedidoEn']['noInterior'],
                        "localidad"=>$arr['Emisor']['ExpedidoEn']['localidad'],
                        "municipio"=>$arr['Emisor']['ExpedidoEn']['municipio'],
                        "estado"=>$arr['Emisor']['ExpedidoEn']['estado'],
                        "pais"=>$arr['Emisor']['ExpedidoEn']['pais'],
                        "codigoPostal"=>$arr['Emisor']['ExpedidoEn']['codigoPostal']
                   )
                );
// }}}
// {{{ Datos del Receptor
$receptor = $xml->createElement("Receptor");
$receptor = $root->appendChild($receptor);
cargaAtt($receptor, array("rfc"=>$arr['Receptor']['rfc'],
                          "nombre"=>$arr['Receptor']['nombre']
                      )
                  );
$domicilio = $xml->createElement("Domicilio");
$domicilio = $receptor->appendChild($domicilio);
cargaAtt($domicilio, array("calle"=>$arr['Receptor']['Domicilio']['calle'],
                        "noExterior"=>$arr['Receptor']['Domicilio']['noExterior'],
                        "noInterior"=>$arr['Receptor']['Domicilio']['noInterior'],
                       "colonia"=>$arr['Receptor']['Domicilio']['colonia'],
                       "localidad"=>$arr['Receptor']['Domicilio']['localidad'],
                       "municipio"=>$arr['Receptor']['Domicilio']['municipio'],
                       "estado"=>$arr['Receptor']['Domicilio']['estado'],
                       "pais"=>$arr['Receptor']['Domicilio']['pais'],
                       "codigoPostal"=>$arr['Receptor']['Domicilio']['codigoPostal'],
                   )
               );
// }}}
// {{{ Detalle de los conceptos/produtos de la factura
$conceptos = $xml->createElement("Conceptos");
$conceptos = $root->appendChild($conceptos);
for ($i=1; $i<=sizeof($arr['Conceptos']); $i++) {
    $concepto = $xml->createElement("Concepto");
    $concepto = $conceptos->appendChild($concepto);
    cargaAtt($concepto, array("cantidad"=>$arr['Conceptos'][$i]['cantidad'],
                              "descripcion"=>$arr['Conceptos'][$i]['descripcion'],
                              "valorUnitario"=>round($arr['Conceptos'][$i]['valorUnitario'],2),
                              "importe"=>$arr['Conceptos'][$i]['importe'],
                   )
                );
}
// }}}
// {{{ Impuesto (IVA)
$impuestos = $xml->createElement("Impuestos");
$impuestos = $root->appendChild($impuestos);
# 7/ago/2006  Ojoj, no confundir tasa 0 con excento
if (isset($arr['Traslados']['importe'])) {
    $traslados = $xml->createElement("Traslados");
    $traslados = $impuestos->appendChild($traslados);
    $traslado = $xml->createElement("Traslado");
    $traslado = $traslados->appendChild($traslado);
    cargaAtt($traslado, array("impuesto"=>$arr['Traslados']['impuesto'],
                              "tasa"=>$arr['Traslados']['tasa'],
                              "importe"=>$arr['Traslados']['importe']
                             )
                         );
}
// }}}
// {{{ Calculo de sello
$cadena_original .= "|";                 // termina la cadena original con el doble ||
$certificado = $arr['noCertificado'];
$maquina = trim(`uname -n`);
$ruta = ($maquina == "www.fjcorona.com.mx") ? "/home/httpd/sat/" : "./";
$file=$ruta.$certificado.".key.pem";      // Ruta al archivo
 
 
$pkeyid = openssl_get_privatekey(file_get_contents($file));
openssl_sign($cadena_original, $crypttext, $pkeyid, OPENSSL_ALGO_MD5);
openssl_free_key($pkeyid);
 
$sello = base64_encode($crypttext);             // lo codifica en formato base64
$root->setAttribute("sello",$sello);
 
$file=$ruta.$certificado.".cer.pem";      // Ruta al archivo
$datos = file($file);
$certificado = ""; $carga=false;
for ($i=0; $i<sizeof($datos); $i++) {
    if (strstr($datos[$i],"END CERTIFICATE")) $carga=false;
    if ($carga) $certificado .= trim($datos[$i]);
    if (strstr($datos[$i],"BEGIN CERTIFICATE")) $carga=true;
}
$root->setAttribute("certificado",$certificado);
// }}}
// {{{ Addenda si se requiere
if ($edidata) {
    $Addenda = $xml->createElement("Addenda");
    // $Addenda->setAttribute("xmlns:foo","http://www.amece.org.mx/cfd/documento");
    $Addenda = $root->appendChild($Addenda);
    if (substr($edidata,0,5) == "<?xml") {
        // Es XML por ejemplo Soriana
        $smp = simplexml_load_string($edidata);
        $Documento = dom_import_simplexml($smp);
        $Documento = $xml->importNode($Documento, true);
    } else {
        if ($nodo=="") {
            // Va el EDIDATA directo sin nodo adiconal. por ejemplo Corvi
            $Documento = $xml->createTextNode(utf8_encode($edidata));
        } else {
            // Va el EDIDATA dentro de un nodo. por ejemplo Walmart
            $Documento = $xml->createElement($nodo,utf8_encode($edidata));
        }
    }
    $Documento = $Addenda->appendChild($Documento);
}
// }}}
// {{{ Genera un archivo de texto con el mensaje XML + EDI  O lo guarda en cfdsello
$todo = $xml->saveXML();
if ($dir != "/dev/null") {
    $xml->formatOutput = true;
    $xml->save($dir.$nufa.".xml");
} else {
    $conn->replace("cfdsello",array("selldocu"=>$nufa),"selldocu",true);
    $where=" selldocu = '$nufa'";
    $conn->UpdateBlob("cfdsello","sellcade",$cadena_original,$where,'TEXT');
    $conn->UpdateBlob("cfdsello","sellxml",$todo,$where,'TEXT');
    }
// }}}
return($todo);
}
// {{{ Funcion que carga los atributos a la etiqueta XML
function cargaAtt(&$nodo, $attr) {
// +-------------------------------------------------------------------------------+
// | Ademas le concatena a la variable global los valores para la cadena origianl  |
// +-------------------------------------------------------------------------------+
global $xml, $cadena_original;
$quitar = array('sello'=>1,'noCertificado'=>1,'certificado'=>1);
foreach ($attr as $key => $val) {
    $val = preg_replace('/\s\s+/', ' ', $val);   // Regla 5a y 5c
    $val = trim($val);                           // Regla 5b
    if (strlen($val)>0) {   // Regla 6
        $val = utf8_encode(str_replace("|","/",$val)); // Regla 1
        $nodo->setAttribute($key,$val);
        if (!isset($quitar[$key])) 
            if (substr($key,0,3) != "xml" &&
                substr($key,0,4) != "xsi:")
             $cadena_original .= $val . "|";
    }
}
}
// }}}
// {{{ Formateo de la fecha en el formato XML requerido (ISO)
function xml_fech($fech) {
    $ano = substr($fech,0,4);
    $mes = substr($fech,4,2);
    $dia = substr($fech,6,2);
    $hor = substr($fech,8,2);
    $min = substr($fech,10,2);
    $seg = substr($fech,12,2);
    $aux = $ano."-".$mes."-".$dia."T".$hor.":".$min.":".$seg;
    return ($aux);
}
?>
Ejemplo de uso de la funcion
Este ya es un programa completo que:
  • Genera el mensaje EDI en base a la base de datos del ERP usando la funcion satxinvo.
  • Genera un arreglo asociativo de formato 'universal' para poder generar el mensaje XML satxarre.
  • Procesa el arreglo asociativo generado en el paso anterior y genera la cadena XML fiscal usando la funcion satxmlsv2.
  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    <?php
    require_once("lib/conecta.php");
    require_once("satxwalm.php");
    require_once("satxarre.php");
    require_once("satxmlsv2.php");
    $nufa = "FAXA051661";    // Factura a tomar del ERP
    $edidata = satxwalm($nufa);    // Genera cadena EDI en base a ERP
    $data = satxarre($nufa);              // Genera arreglo asociativo de la factura
    $xml = satxmlsv2($data,$edidata,"/dev/null","edi");    // Genera cadena XML en base a arreglo asociativo y cadena EDI
    print $xml;         // Muestra el XML
    ?>
    Al ejecutarse este codigo despliega precisamente lo que se mostro en esta pagina.