{"id":195,"date":"2017-05-27T14:12:11","date_gmt":"2017-05-27T19:12:11","guid":{"rendered":"https:\/\/drmad.org\/blog\/?p=195"},"modified":"2017-09-11T06:08:45","modified_gmt":"2017-09-11T11:08:45","slug":"cronicas-de-una-facturacion-electronica-desde-php-anunciada","status":"publish","type":"post","link":"https:\/\/drmad.org\/blog\/cronicas-de-una-facturacion-electronica-desde-php-anunciada.html","title":{"rendered":"Cr\u00f3nicas de una facturaci\u00f3n electr\u00f3nica desde PHP anunciada"},"content":{"rendered":"<h1><strong>\u00a1Al fin! <\/strong><\/h1>\n<p>S\u00f3lo me tom\u00f3 7 semanas sin dormir para poder concluir un sistema b\u00e1sico de emisi\u00f3n de documentos electr\u00f3nicos de\u00a0la\u00a0<a href=\"https:\/\/sunat.gob.pe\">SUNAT<\/a>\u00a0para\u00a0<a href=\"https:\/\/icaserver.com\">IcaServer<\/a>\u00a0\ud83d\ude11<\/p>\n<p>Una buena parte del tiempo se me fue aprendiendo m\u00e1s a fondo XML, desde c\u00f3mo crearlo con PHP hasta firmarlo con\u00a0el certificado digital (y entender todo el proceso). Otra parte se me fue (como a muchos contadores) entendiendo las inconsistencias de la SUNAT&#8230;<\/p>\n<p>Y estas\u00a0son algunas notas de las vicisitudes que encontr\u00e9 en el camino.<\/p>\n<p><!--more--><\/p>\n<h2>XML es\u00a0<span style=\"color: #222222; font-family: 'Helvetica Neue', Helvetica, 'Nimbus Sans L', Arial, 'Liberation Sans', sans-serif;\"><span style=\"line-height: 26.3999996185303px; background-color: #ffffff;\"><i>horrible<\/i><\/span><\/span><\/h2>\n<p>No saben cu\u00e1nto extra\u00f1\u00e9 JSON con REST \ud83d\ude1b Fue tentador para mi crear el XML <i>a mano <\/i>en un fichero, y luego reemplazar su contenido, como si fuera una plantilla. Pero en el proceso de desarrollo me top\u00e9 con varios errores que requer\u00edan mover nodos, as\u00ed que acab\u00e9 con un h\u00edbrido: parte de nodos generados con c\u00f3digo, y algunos segmentos con ficheros tipo plantillas.<\/p>\n<p>El resultado es\u00a0<i>monstruoso<\/i>. Funciona, pero es inmantenible. Decid\u00ed que al reescribirlo crear\u00eda una librer\u00eda para hacer XML facilito.<\/p>\n<p>Y ya existe \ud83d\ude00 Le presento a <a href=\"https:\/\/github.com\/drmad\/semeele\">Semeele<\/a>, XML sin dolor de cabeza \ud83d\ude01<\/p>\n<p>Por ejemplo, para generar un XHTML, puedes usar este c\u00f3digo:<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"php\">$xml = new drmad\\semeele\\Document('html');\r\n$xml-&gt;child('head')\r\n    -&gt;add('title', 'An XHTML')\r\n    -&gt;add('meta', ['charset' =&gt; 'utf-8'])\r\n    -&gt;parent()\r\n-&gt;child('body')\r\n    -&gt;add('h1', 'An XHTML')\r\n    -&gt;add('p', 'This is a XML-valid HTML. Yay!')\r\n;\r\n\r\necho $xml-&gt;getXML();<\/pre>\n<p>As\u00ed de sencillo. Ese c\u00f3digo genera este XML:<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"xml\">&lt;?xml version=\"1.0\" encoding=\"utf-8\"?&gt;&lt;html&gt;&lt;head&gt;&lt;title&gt;An XHTML&lt;\/title&gt;&lt;meta charset=\"utf-8\"\/&gt;&lt;\/head&gt;&lt;body&gt;&lt;h1&gt;An XHTML&lt;\/h1&gt;&lt;p&gt;This is a XML-valid HTML. Yay!&lt;\/p&gt;&lt;\/body&gt;&lt;\/html&gt;\r\n<\/pre>\n<p>Hay m\u00e1s ejemplos y algo de documentaci\u00f3n en la <a href=\"https:\/\/github.com\/drmad\/semeele\">p\u00e1gina de GitHub de Semeele<\/a>.<\/p>\n<h2>La firma y el firmado<\/h2>\n<p>El certificado digital lo adquir\u00ed en <a href=\"https:\/\/llama.pe\">llama.pe<\/a>, fue uno de los pocos que muestran\u00a0el precio del certificado en su web, sin solicitar una cotizaci\u00f3n ni otras trabas. Y no me equivoqu\u00e9, me lo dieron en pr\u00e1cticamente horas.<\/p>\n<p>Me lo entregaron en formato PKCS#12, <a href=\"https:\/\/www.cs.auckland.ac.nz\/~pgut001\/pubs\/pfx.html\">heredero del peor formato criptogr\u00e1fico de la historia<\/a>. Para aumentar el estr\u00e9s, SUNAT requiere el certificado en formato distinto, &#8216;CER&#8217; \u00a0Para nuestra salvaci\u00f3n, Linux tiene el fabuloso comando <code>openssl<\/code>, que hace todo el trabajo feo por nosotros.<\/p>\n<p>Primero, obtenemos los certificados desde el fichero PKCS#12 (en formato PEM, que es ASCII plano)<\/p>\n<p><code>openssl pkcs12 -in CERTIFICADO_PKCS.pfx -nokeys -out solo_certificados.crt<\/code><\/p>\n<p>Luego lo recodificamos a DER (<em>\u00bfno era CER?<\/em>\u00a0Puedes leer <a href=\"https:\/\/support.ssl.com\/Knowledgebase\/Article\/View\/19\/0\/der-vs-crt-vs-cer-vs-pem-certificates-and-how-to-convert-them\">aqu\u00ed<\/a> para confundirte un poco m\u00e1s&#8230;):<\/p>\n<p><code>openssl x509 -in solo_certificados.crt -outform der -out certificado.cer<\/code><\/p>\n<p>Y listo.<\/p>\n<p>A parte de convertir de formato, con <code>openssl<\/code> puedes sacar cada certificado y la llave privada del PKCS#12, y guardar cada uno en el formato PEM (entre otras docenas de cosas m\u00e1s. <code>openssl<\/code> es muy paja \ud83d\ude1b)<\/p>\n<p>En vez de usar las librer\u00edas de OpenSSL de PHP para firmar el documento (y ver otros temas relacionados, como la <em>canonicalizaci\u00f3n<\/em>), prefer\u00ed usar <a href=\"https:\/\/www.aleksey.com\/xmlsec\/\">XMLSec<\/a>, y funcion\u00f3 a la\u00a0<em>ferpecci\u00f3n<\/em>.<\/p>\n<h2>ZIP de la discordia<\/h2>\n<p>La <a href=\"http:\/\/contenido.app.sunat.gob.pe\/insc\/ComprobantesDePago+Electronicos\/eFacturas+d+sistemas+contrib\/Act23dic2014\/Manual+de+autorizacion.pdf\">documentaci\u00f3n<\/a> del ZIP para el env\u00edo del XML a la SUNAT dice:<\/p>\n<blockquote><p>En caso de las facturas y sus correspondientes notas de cr\u00e9dito y d\u00e9bito, se enviar\u00e1 un \u00fanico comprobante, raz\u00f3n por la que se espera recibir un \u00fanico archivo ZIP y dentro de este, una carpeta de nombre dummy (vacio) y un documento XML. Los nombres de los archivos deben coincidir a excepci\u00f3n de la extensi\u00f3n.<\/p><\/blockquote>\n<p>Los\u00a0<em>dummies<\/em> son los que escribieron eso\u00a0<span class=\"emoji\">\ud83d\ude12\u00a0Perd\u00ed buena cantidad de horas para darme cuenta que\u00a0<em>no debe existir<\/em><em>\u00a0<\/em>esa carpeta en el ZIP, \u00a0\u00fanicamente el XML.<\/span><\/p>\n<h2>WS-Security<\/h2>\n<p>La librer\u00eda de SOAP de PHP no tiene soporte nativo para WS-Security, requerido por la SUNAT. Pero implementarlo es \u00ab\u00bb\u00bbsencillo\u00bb\u00bb\u00bb (entre m\u00faltiples comillas). Usando el mismo ejemplo del Manual del Programador de la SUNAT, y con mucha ayuda de Google, llegu\u00e9 a este c\u00f3digo:<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"php\">$WSHeader = '&lt;wsse:Security xmlns:wsse=\"http:\/\/docs.oasis-open.org\/wss\/2004\/01\/oasis-200401-wss-wssecurity-secext-1.0.xsd\"&gt;\r\n    &lt;wsse:UsernameToken&gt;\r\n        &lt;wsse:Username&gt;' . $RUC . $USUARIO_SOL . '&lt;\/wsse:Username&gt;\r\n        &lt;wsse:Password&gt;' . $CONTRASE\u00d1A_SOL . '&lt;\/wsse:Password&gt;\r\n    &lt;\/wsse:UsernameToken&gt;\r\n&lt;\/wsse:Security&gt;';\r\n$headers = new SoapHeader('http:\/\/docs.oasis-open.org\/wss\/2004\/01\/oasis-200401-wss-wssecurity-secext-1.0.xsd', 'Security', new SoapVar($WSHeader, XSD_ANYXML));\r\n$soap = new SoapClient('ruta\/al\/wsdl')\r\n$soap-&gt;__soapCall('sendBill', $argumentos, null, $headers);<\/pre>\n<p><code>sendBill<\/code> y <code>$argumentos<\/code> dependen de la acci\u00f3n que quieras realizar.\u00a0<strong>OJO<\/strong> que <code>$argumentos<\/code>, por alguna raz\u00f3n esot\u00e9rica, debe ser un array asociativo <em>dentro<\/em> de otro array\u00a0\ud83d\ude1d\u00a0algo como\u00a0<code class=\"EnlighterJSRAW\" data-enlighter-language=\"php\">[['filename' =&gt; '123abc.zip', 'contentFile' =&gt; $zipfile]]<\/code>. Bendito PHP&#8230;<\/p>\n<p>El par\u00e1metro de <code>new SoapClient<\/code> es una ruta a un fichero, que explico en el siguiente apartado.<\/p>\n<h2>PHP y su bug de 9 a\u00f1os<\/h2>\n<p>Las pruebas de env\u00edo usando la librer\u00eda de SOAP de PHP fueron bien. Los env\u00edos de los comprobantes para la homologaci\u00f3n fueron bien. Pero cuando intent\u00e9 enviar mi primera factura en modo producci\u00f3n, \u00a1BOOM! Excepci\u00f3n del SOAP\u00a0<span class=\"emoji\">\ud83d\ude20<\/span><\/p>\n<p><code>Parsing WSDL: &lt;binding&gt; 'BillServicePortBinding' already defined<\/code><\/p>\n<p>Resulta que la librer\u00eda de SOAP de PHP <a href=\"https:\/\/bugs.php.net\/bug.php?id=45282\">tiene un problema con WSDLs que usan <em>namespaces<\/em><\/a> (que es el caso del WSDL del SOAP de la SUNAT para producci\u00f3n)<em>,<\/em> reportado el 16 de junio del <em>2008<\/em>, \u00a0y nunca fue reparado&#8230;<\/p>\n<p>No hay otras librer\u00edas decentes de SOAP para PHP. Tampoco hay para Python, pues pens\u00e9 en usarlo para el env\u00edo. Nada. Pens\u00e9 en hacer mi propia librer\u00eda de SOAP&#8230; pero ya estaba muy abrumado por no acabar este tema.<\/p>\n<p>Al final, leyendo sobre WSDL, se me ocurri\u00f3 un\u00a0<em>hack<\/em>:<em>\u00a0<\/em>El\u00a0WSDL de la SUNAT tiene definiciones de funciones para dos versiones \u00a0de SOAP, \u00a01.1 y 1.2, usando los\u00a0<em>namespaces<\/em>. El bendito\u00a0<code>BillServicePortBinding<\/code> se define primero en en la URL relativa importada\u00a0<code>billService?ns2.wsdl<\/code>\u00a0(con <em>bindings<\/em> para SOAP 1.2), y luego se vuelve a definir en WSDL principal.<\/p>\n<p>ASI QUE simplemente remov\u00ed la l\u00ednea donde importa el\u00a0<code>billService?ns2.wsdl<\/code>\u00a0\ud83d\ude01\u00a0Ah\u00ed se define el SOAP 1.2, pero PHP y la SUNAT trabajan bien con la versi\u00f3n 1.1, y problema resuelto. Para remover la l\u00ednea tuve que descargar el WSDL con sus dos ficheros relacionados: <code>billService?ns1.wsdl<\/code> y\u00a0<code>billService.xsd2.xsd<\/code>, y crear el objeto <code>SoapClient<\/code> pas\u00e1ndole la ruta del fichero principal, y no la URL del WSDL p\u00fablico.<\/p>\n<p>Claro est\u00e1 que si a la SUNAT se le ocurre cambiar el WSDL, todo el c\u00f3digo se romper\u00e1, hasta actualizar la copia local. Espero que eso no pase pronto.<\/p>\n<h2>Bienvenidos al futuro<\/h2>\n<p>Ahora <a href=\"https:\/\/icaserver.com\">IcaServer<\/a> es un emisor de facturas electr\u00f3nicas\u00a0\ud83d\ude01\u00a0Y, obviamente, estoy brindando asesor\u00eda y herramientas para las empresas que quieran (o deban) hacer lo mismo. <a href=\"https:\/\/icaserver.com\/alojamiento#contacto\">Ponte en contacto conmigo<\/a> para brindarte mayor informaci\u00f3n.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>\u00a1Al fin! S\u00f3lo me tom\u00f3 7 semanas sin dormir para poder concluir un sistema b\u00e1sico de emisi\u00f3n de documentos electr\u00f3nicos<\/p>\n","protected":false},"author":1,"featured_media":209,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[18,2,14,8,17,19],"tags":[],"class_list":["post-195","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-facturacion-electronica","category-ica","category-php","category-php7","category-soap","category-sunat"],"jetpack_featured_media_url":"https:\/\/i0.wp.com\/drmad.org\/blog\/wp-content\/uploads\/2017\/05\/xml.jpg?fit=1920%2C540&ssl=1","jetpack_sharing_enabled":true,"jetpack_shortlink":"https:\/\/wp.me\/p6p3z1-39","jetpack-related-posts":[],"jetpack_likes_enabled":true,"_links":{"self":[{"href":"https:\/\/drmad.org\/blog\/wp-json\/wp\/v2\/posts\/195","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/drmad.org\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/drmad.org\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/drmad.org\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/drmad.org\/blog\/wp-json\/wp\/v2\/comments?post=195"}],"version-history":[{"count":13,"href":"https:\/\/drmad.org\/blog\/wp-json\/wp\/v2\/posts\/195\/revisions"}],"predecessor-version":[{"id":654,"href":"https:\/\/drmad.org\/blog\/wp-json\/wp\/v2\/posts\/195\/revisions\/654"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/drmad.org\/blog\/wp-json\/wp\/v2\/media\/209"}],"wp:attachment":[{"href":"https:\/\/drmad.org\/blog\/wp-json\/wp\/v2\/media?parent=195"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/drmad.org\/blog\/wp-json\/wp\/v2\/categories?post=195"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/drmad.org\/blog\/wp-json\/wp\/v2\/tags?post=195"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}