Crónicas de una facturación electrónica desde PHP anunciada

Posted by

¡Al fin!

Sólo me tomó 7 semanas sin dormir para poder concluir un sistema básico de emisión de documentos electrónicos de la SUNAT para IcaServer 😑

Una buena parte del tiempo se me fue aprendiendo más a fondo XML, desde cómo crearlo con PHP hasta firmarlo con el certificado digital (y entender todo el proceso). Otra parte se me fue (como a muchos contadores) entendiendo las inconsistencias de la SUNAT…

Y estas son algunas notas de las vicisitudes que encontré en el camino.

XML es horrible

No saben cuánto extrañé JSON con REST 😛 Fue tentador para mi crear el XML a mano en un fichero, y luego reemplazar su contenido, como si fuera una plantilla. Pero en el proceso de desarrollo me topé con varios errores que requerían mover nodos, así que acabé con un híbrido: parte de nodos generados con código, y algunos segmentos con ficheros tipo plantillas.

El resultado es monstruoso. Funciona, pero es inmantenible. Decidí que al reescribirlo crearía una librería para hacer XML facilito.

Y ya existe 😀 Le presento a Semeele, XML sin dolor de cabeza 😁

Por ejemplo, para generar un XHTML, puedes usar este código:

$xml = new drmad\semeele\Document('html');
$xml->child('head')
    ->add('title', 'An XHTML')
    ->add('meta', ['charset' => 'utf-8'])
    ->parent()
->child('body')
    ->add('h1', 'An XHTML')
    ->add('p', 'This is a XML-valid HTML. Yay!')
;

echo $xml->getXML();

Así de sencillo. Ese código genera este XML:

<?xml version="1.0" encoding="utf-8"?><html><head><title>An XHTML</title><meta charset="utf-8"/></head><body><h1>An XHTML</h1><p>This is a XML-valid HTML. Yay!</p></body></html>

Hay más ejemplos y algo de documentación en la página de GitHub de Semeele.

La firma y el firmado

El certificado digital lo adquirí en llama.pe, fue uno de los pocos que muestran el precio del certificado en su web, sin solicitar una cotización ni otras trabas. Y no me equivoqué, me lo dieron en prácticamente horas.

Me lo entregaron en formato PKCS#12, heredero del peor formato criptográfico de la historia. Para aumentar el estrés, SUNAT requiere el certificado en formato distinto, ‘CER’  Para nuestra salvación, Linux tiene el fabuloso comando openssl, que hace todo el trabajo feo por nosotros.

Primero, obtenemos los certificados desde el fichero PKCS#12 (en formato PEM, que es ASCII plano)

openssl pkcs12 -in CERTIFICADO_PKCS.pfx -nokeys -out solo_certificados.crt

Luego lo recodificamos a DER (¿no era CER? Puedes leer aquí para confundirte un poco más…):

openssl x509 -in solo_certificados.crt -outform der -out certificado.cer

Y listo.

A parte de convertir de formato, con openssl 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ás. openssl es muy paja 😛)

En vez de usar las librerías de OpenSSL de PHP para firmar el documento (y ver otros temas relacionados, como la canonicalización), preferí usar XMLSec, y funcionó a la ferpección.

ZIP de la discordia

La documentación del ZIP para el envío del XML a la SUNAT dice:

En caso de las facturas y sus correspondientes notas de crédito y débito, se enviará un único comprobante, razón por la que se espera recibir un único 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ón de la extensión.

Los dummies son los que escribieron eso 😒 Perdí buena cantidad de horas para darme cuenta que no debe existir esa carpeta en el ZIP,  únicamente el XML.

WS-Security

La librería de SOAP de PHP no tiene soporte nativo para WS-Security, requerido por la SUNAT. Pero implementarlo es “””sencillo””” (entre múltiples comillas). Usando el mismo ejemplo del Manual del Programador de la SUNAT, y con mucha ayuda de Google, llegué a este código:

$WSHeader = '<wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
    <wsse:UsernameToken>
        <wsse:Username>' . $RUC . $USUARIO_SOL . '</wsse:Username>
        <wsse:Password>' . $CONTRASEÑA_SOL . '</wsse:Password>
    </wsse:UsernameToken>
</wsse:Security>';
$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));
$soap = new SoapClient('ruta/al/wsdl')
$soap->__soapCall('sendBill', $argumentos, null, $headers);

sendBill y $argumentos dependen de la acción que quieras realizar. OJO que $argumentos, por alguna razón esotérica, debe ser un array asociativo dentro de otro array 😝 algo como [['filename' => '123abc.zip', 'contentFile' => $zipfile]]. Bendito PHP…

El parámetro de new SoapClient es una ruta a un fichero, que explico en el siguiente apartado.

PHP y su bug de 9 años

Las pruebas de envío usando la librería de SOAP de PHP fueron bien. Los envíos de los comprobantes para la homologación fueron bien. Pero cuando intenté enviar mi primera factura en modo producción, ¡BOOM! Excepción del SOAP 😠

Parsing WSDL: <binding> 'BillServicePortBinding' already defined

Resulta que la librería de SOAP de PHP tiene un problema con WSDLs que usan namespaces (que es el caso del WSDL del SOAP de la SUNAT para producción), reportado el 16 de junio del 2008,  y nunca fue reparado…

No hay otras librerías decentes de SOAP para PHP. Tampoco hay para Python, pues pensé en usarlo para el envío. Nada. Pensé en hacer mi propia librería de SOAP… pero ya estaba muy abrumado por no acabar este tema.

Al final, leyendo sobre WSDL, se me ocurrió un hack: El WSDL de la SUNAT tiene definiciones de funciones para dos versiones  de SOAP,  1.1 y 1.2, usando los namespaces. El bendito BillServicePortBinding se define primero en en la URL relativa importada billService?ns2.wsdl (con bindings para SOAP 1.2), y luego se vuelve a definir en WSDL principal.

ASI QUE simplemente removí la línea donde importa el billService?ns2.wsdl 😁 Ahí se define el SOAP 1.2, pero PHP y la SUNAT trabajan bien con la versión 1.1, y problema resuelto. Para remover la línea tuve que descargar el WSDL con sus dos ficheros relacionados: billService?ns1.wsdlbillService.xsd2.xsd, y crear el objeto SoapClient pasándole la ruta del fichero principal, y no la URL del WSDL público.

Claro está que si a la SUNAT se le ocurre cambiar el WSDL, todo el código se romperá, hasta actualizar la copia local. Espero que eso no pase pronto.

Bienvenidos al futuro

Ahora IcaServer es un emisor de facturas electrónicas 😁 Y, obviamente, estoy brindando asesoría y herramientas para las empresas que quieran (o deban) hacer lo mismo. Ponte en contacto conmigo para brindarte mayor información.

76 comments

      1. me he bajado
        e-factura.sunat.gob.pe/ol-ti-itcpfegem/billService?wsdl
        e-factura.sunat.gob.pe/ol-ti-itcpfegem/billService?ns1.wsdl
        e-factura.sunat.gob.pe/ol-ti-itcpfegem/billService?ns2.wsdl
        e-factura.sunat.gob.pe/ol-ti-itcpfegem/billService.xsd2.xsd

        Que archivo modifico. Y Que le modifico
        todos esta dentro de una carpeta en mi ser local.

        1. El chiste está en el fichero billService?wsdl. Dentro hay una línea que dice <wsdl:import location="billService?ns2.wsdl".... Borra esa línea, y se feliz 😀

          1. Hola drmad, eliminé la linea que dices, para crear el soapcliente lo hago asi:
            $soap = new \SoapClient(‘wsdl-sunat/billService.xml?wsdl’, [
            ‘cache_wsdl’ => WSDL_CACHE_NONE,
            ‘trace’ => TRUE ,
            ‘soap_version’ => SOAP_1_1 ]
            );

            ahora me sale este error:
            SOAP-ERROR: Parsing WSDL: Couldn’t load from ‘wsdl-sunat/billService.xml?wsdl’ : failed to load external entity “wsdl-sunat/billService.xml?wsdl”

            NOTA: dentro de la carpeta “wsdl-sunat” tengo los archivos que corresponde a los archivos que menciona katty y Giova :

            billService.xml
            billService-ns1.xml
            billService-ns2.xml
            billService.xsd2.xsd

          2. Hi Jorge.

            Ojo que el fichero debe llamarse billService.xml?wsdl, así tal cual. El error de SOAP indica que no puede ‘cargar la entidad externa’, debe deberse a que estás intentando cargar un fichero con un nombre que no corresponde al fichero que tienes en el sistema de ficheros.

            También podrías cambiar el parámetro de SoapClient a “billService.xml“, debería de funcionar sin problemas.

          3. No logre hacer funcionar hacia con los cambios que me dices, primero: en Windows no acepta ese tipo de nombres para los archivos, Segundo: lo pase a Linux y tampoco funcionó; finalmente tuve que crear un ejecutable que solamente se encargue del envío de los comprobantes al servidor de la sunat y lo ejecuto desde PHP (en windows lo hice en .NET), no es una solución limpia pero por ahora me da tiempo a seguir viendo como solucionar este tema… Gracias a todos.

          4. Windows cochino 😀 Realmente no importa el nombre del fichero, con tal que sea consistente con el nombre que usas en el programa (y en los demás ficheros relacionados) para llamarlo. E.g puedes usar $soap = new \SoapCliente('c:\bendito.wsdl', ..., y renombrar/movier el fichero billService.xml?wsdl con dicho nombre. Este fichero, dentro, tiene referencias relativas a otros ficheros más. Nuevamente: funciona si renombras el fichero en ámbos lugares: el sistema de ficheros y el código/XML que lo referencia.

            Es cierto, el tema de SOAP en PHP es un gran problema, y esto que he hecho también es un hack feo. Espero y pronto se pongan las pilas con el bug de ya pronto 9 años.

      2. Hola drmad

        Tengo el mismo problema que indicas arriba cuando apunto a producción pero estoy usando python con suds.
        Puedes ayudarme con consultoria?

        Me comentas

    1. En el WSDL de la sunat, todas las referencias a otros ficheros son relativas. Así que si copias los ficheros a tu computadora con el mismo nombre, cuando cargues el fichero billService?wsdl (o el nombre que quieras ponerle, en ese fichero el nombre no es relevante), cargará del mismo directorio los demás ficheros (siempre y cuando tengan el mismo nombre).

      Saludos!

  1. Hola drmad, si me pudieras ayudar porque estoy tratando de enviar mi archivo zip, pero no tengo respuesta del web service SUNAT, este es el codigo:
    $service = ‘https://e-beta.sunat.gob.pe/ol-ti-itcpfegem-beta/billService?wsdl’;
    $NomArch = “20100073723-01-F607-00000420″;
    $fileName=”20100073723-01-F607-00000420.”.zip;
    $archivo = base64_encode(file_get_contents($NomArch.”.zip”));
    $RUC = “20100073723”;
    $USUARIO_SOL = “MODDATOS”;
    $CONTRASENA_SOL = “MODDATOS”;

    $soap = new SoapClient($service, [
    ‘cache_wsdl’ => WSDL_CACHE_NONE,
    ‘trace’ => TRUE ,
    ‘soap_version’ => SOAP_1_1 ]
    );

    $argumentos = array( ‘fileName’ => $fileName, ‘contentFile’ => base64_encode(file_get_contents($fileName)) );

    $WSHeader = ‘

    ‘.$RUC.$USUARIO_SOL.’
    ‘.$CONTRASENA_SOL.’

    ‘;
    $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));
    // $soap = new SoapClient(‘ruta/al/wsdl’)
    $result = $soap->__soapCall(‘sendBill’, $argumentos, null, $headers);
    echo $result;

    1. Puse un gran “OJO” debajo del código del ws-security, la librería de SOAP del PHP espera un array dentro de un array para los argumentos (usando la función retro array() que usas, debería ser un array(array('fileName'=>...).

      A parte, la librería de SOAP va a encodear el zip, no uses el base64_encode.

      A parte, en la 3ra línea, donde defines $fileName hay un .zip fuera del string. Verifica que eso no esté así en tu código.

  2. Hola drmad, hize los cambios y me sale el siguiente error:

    Fatal error: Uncaught SoapFault exception: [soap-env:Client.1036] Número de documento en el nombre del archivo no coincide con el consignado en el contenido del XMLDetalle: xxx.xxx.xxx value=’ticket: 1505145143245 error: numero de comprobante del xml diferente al numero del archivo 420 diff 00000420′ in C:\AppServ\www\ws\soapphp4.php:30 Stack trace: #0 C:\AppServ\www\ws\soapphp4.php(30): SoapClient->__soapCall(‘sendBill’, Array, NULL, Object(SoapHeader)) #1 {main} thrown in C:\AppServ\www\ws\soapphp4.php on line 30

    El codigo es:
    $service = ‘https://e-beta.sunat.gob.pe/ol-ti-itcpfegem-beta/billService?wsdl’;
    $fileName=”20100073723-01-F607-00000420.zip”;

    $RUC = “20100073723”;
    $USUARIO_SOL = “MODDATOS”;
    $CONTRASENA_SOL = “MODDATOS”;

    $soap = new SoapClient($service, [
    ‘cache_wsdl’ => WSDL_CACHE_NONE,
    ‘trace’ => TRUE ,
    ‘soap_version’ => SOAP_1_1 ]
    );

    $argumentos = array( array(‘fileName’ => $fileName, ‘contentFile’ => (file_get_contents($fileName))) );

    $WSHeader = ‘

    ‘.$RUC.$USUARIO_SOL.’
    ‘.$CONTRASENA_SOL.’

    ‘;
    $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));
    // $soap = new SoapClient(‘ruta/al/wsdl’)
    $result = $soap->__soapCall(‘sendBill’, $argumentos, null, $headers);
    echo $result;

  3. drmad, estoy utilizando para convertir la respuesta SUNAT, $result = $soap->__soapCall(‘sendBill’, $argumentos, null, $headers);
    print_r($result);
    $archivo = fopen(‘X’.$NomArch.’.xml’,’w+’);
    fputs($archivo, $result);
    fclose($archivo);
    pero sale un error: Warning: fputs() expects parameter 2 to be string, object given

    Habra otra manera de capturar la respuesta de SUNAT:
    stdClass Object ( [applicationResponse] => PK�+Kdummy/PK�+K0�����R-20515290142-03-BT10-1899.XML�VMs�8�ϯp��TM֑m0�]�)��Y6Y6�@6s�0Jl�% ��W��&N L�>ȭׯ[��eܯ�8R��qL�@5.uUAħ&�@]�o�k����3J��PH�� %)ҙp���aġ�c�#���xU���2r��F1tv�y&�?P�����J���s�6�|��#bFuKJ|>}�7��yg�x���l �z�2]pPTjt���;��|@�%�ng� K���eۖ�wAa�w�t�TLS7�4]>��Н��t�ZAs,�u'{j�̞�ʯS���#�66���4��v\pCߩ�S�%�<.f�y�t���=db�۲�4�2Vߛ�F�ٕ?Ӷ��ث���%u�V�L�p�%'�U �u���I��T�4�7�Bd&�*U�ߔ5T8�}�����]����L��y�}%Y�G�N���X����~�߳�甤�����PK�+Kdummy/PK�+K0�����&R-20515290142-03-BT10-1899.XMLPK�0 )

  4. Hola drmad, espero me ayudes con lo de firma digital, lo que he realizado es convertir el archivo .pfx en .pem los cuales al abrirlo el .pem con texto hay dos certificados: BEGIN CERTIFICATE con datos y localKeyID=.. y BEGIN PRIVATE KEY con localKeyID= .., quería saber con cual se firma, y dentro del UBL para firmar comienzo con (ext:UBLExtension) .. pero en ds:DigestValue que contenido es? ds:SignatureValue que contenido es? y en ds:X509Certificate que contenido es?, creo que en ds:X509Certificate va el contenido del archivo .pem los datos de BEGIN CERTIFICATE .. END CERTIFICATE, por favor espero me ayudes a ubicar los contenidos de la firma digital gracias.

    1. Si realmente quieres saber lo que va en cada elemento, tienes harto por leer 😀 Empezando por este post 😛 donde he detallado que usé XMLSec, un programa externo, para que haga el firmado del XML. Dale una leída a este enlace, que me sirvió para entender todo.

      En teoría podrías usar las librerías de PHP OpenSSL y DOM para hacer la C14N (“canonicalización“. me gusta esa palabra 😀 ), digest y firmado del XML usando el certificado X509. A mi me dio flojera ir por ese camino 😀

  5. Hola drmad, espero me ayudes con lo de firma digital, estoy utilizando xmlsec para firmar pero me sale error, te envio el codigo que estoy utilizando para firmar el XML:

    loadXML($xmlstr);

    $objSign = new XMLSecurityDSig($ruc);
    // Use the c14n exclusive canonicalization
    $objSign->setCanonicalMethod(XMLSecurityDSig::C14N);
    // Sign using SHA-256
    $objSign->addReference(
    $domDocument,
    XMLSecurityDSig::SHA1,
    array(‘http://www.w3.org/2000/09/xmldsig#enveloped-signature’),
    $options = array(‘force_uri’ => true)
    );

    $objKey = new XMLSecurityKey(XMLSecurityKey::RSA_SHA1, array(‘type’=>’private’));
    // Load the private key
    $objKey->loadKey($privateKey);
    /*
    If key has a passphrase, set it using
    $objKey->passphrase = ”;
    */
    // Sign the XML file
    $objSign->sign($objKey, $domDocument->getElementsByTagName($ReferenceNodeName)->item(1));
    // Add the associated public key to the signature
    $objSign->add509Cert($publicKey);

    // Append the signature to the XML
    //$objSign->appendSignature($ReferenceNodeName);

    $content = $domDocument->saveXML();
    ?>

  6. Los errores que me arroja el codigo son:

    Warning: DOMDocument::loadXML(): StartTag: invalid element name in Entity, line: 1 in C:\AppServ\www\xmlseclibs-master\src\XMLSecurityDSig.php on line 119

    Warning: DOMDocument::loadXML(): Extra content at the end of the document in Entity, line: 1 in C:\AppServ\www\xmlseclibs-master\src\XMLSecurityDSig.php on line 119

    Fatal error: Call to undefined function RobRichards\XMLSecLibs\openssl_get_privatekey() in C:\AppServ\www\xmlseclibs-master\src\XMLSecurityKey.php on line 345

  7. Nuevamente, yo no uso esa librería, ni habia oido de ella.

    Pero tienes un error Fatal, “RobRichards\XMLSecLibs\openssl_get_privatekey()” no existe. Lee su documentación, ahi debe decir cómo incluir la librería donde se define dicha función.

  8. Hola drmad, no se si me faltan inluir algo mas estoy utilizando lo siguiente:
    $xmlseclibs_srcdir = dirname(__FILE__) . ‘/src/’;
    require $xmlseclibs_srcdir . ‘/XMLSecurityKey.php’;
    require $xmlseclibs_srcdir . ‘/XMLSecurityDSig.php’;
    require $xmlseclibs_srcdir . ‘/XMLSecEnc.php’;

    tengo el siguiente error:
    Call to a member function getElementsByTagName() on string in C:\AppServ\www\xmlseclibs-master\firmar2.php on line 43

    y la linea 43 tiene el siguiente codigo:
    $objSign->sign($objKey, $domDocument->getElementsByTagName($ReferenceNodeName)->item(1));

    donde en lineas anteriores de codigo menciono lo siguiente para llamar al objeto:

    $domDocument = new DOMDocument();

    y otro dos errores:

    Warning: DOMDocument::loadXML(): StartTag: invalid element name in Entity, line: 1 in C:\AppServ\www\xmlseclibs-master\src\XMLSecurityDSig.php on line 119

    Warning: DOMDocument::loadXML(): Extra content at the end of the document in Entity, line: 1 in C:\AppServ\www\xmlseclibs-master\src\XMLSecurityDSig.php on line 119

    gracias por la ayuda

    1. Nuevamente, no conozco esa librería 😛 Lee su documentación sobre cómo debes de usarla. Prueba usar var_dump ($domDocument); para que veas qué es lo que contiene. Te apuesto que tiene un string, y no un objeto DOMDocument 😀

      Esos dos errores del loadXML() es probable que sea por que tu XML está mal formado.

  9. Hola, Oliver.

    La próxima semana empiezo mi proyecto de facturación con Python y el framework Django, pero comentas que no hay librerías decentes de SOAP en Python, además no se si encontraré alguna librería para firmar el XML.

    Qué problemas crees que me pueda encontrar si desarrollo el proyecto en Python? Algún consejo?

    1. Hola Victor

      Paja que uses Django 😀 Yo hice una búsqueda rápida (por desesperación para acabar el proyecto), pero sí existen varias librerías para SOAP en Python. Es cuestión que pruebes. También hay librerías para firmar XML. Hay de todo en Python 😀 Yo me rendí a la flojera, y usé el libxml2, que es un programa externo 🙂

      Ya estoy a finales de acabar mi API de facturación, por si quieres evitar la fatiga de desarrollar todo 😉 publicaré más detalles aquí, en este blog.

  10. Esta semana voy a probar librerías de Python para firmar los XML y enviarlo al servicio SOAP de Sunat, después de haber investigado un poco creo que esta es la principal barrera.
    Si no logro hacerlo, mi segunda alternativa era justamente trabajar con una API, para no reinventar la rueda. Espero noticias de tu API.

  11. Yo tengo el siguiente error al enviar una factura electrónica, el xml está firmado con un certificado de pruebas, y lo envio al web service beta.

    “Uncaught SoapFault exception: [soap-env:Client.2335] El documento electrónico ingresado ha sido alterado – Detalle: Incorrect reference digest value “

    1. Hola Alfredo, estás firmando mal el certificado 🙂 Usa alguna herramientas para comprobar el firmado de un XML, hay varias por ahi. Yo uso para firmar el XML xmlsec, el que también tiene una función para comprobar: xmlsec1 --verify --trusted-pem ruta/al/certificado/ca.pem ruta/al/comprobante.xml

  12. Hola, cuando firmo con el .p12 me salen 4 firmas en el archivo. También ya no estoy usando el sha1, llama.pe me dijo que ahora es sha256. Las preguntas son:

    1. Uso xmlsec pero me sale 4 firmas en X509Certificate
    2. Que tan cierto es eso? De no usar el SHa1

    1. ¡Hola!

      El PKCS puede contener varios certificados. En los certificados que he visto venían 2: el certificado en si, y su certificado ‘papá’ (que le llaman una “cadena de confianza“). Solo es necesario el último certificado (el tuyo) es el necesario. No recuerdo ahorita, pero con el comando openssl puedes extraerlo. Vale la pena una googleadita 🙂

      Sobre el SHA1, es cierto. El SHA1 ya ha caído en desuso por que han encontrado formas de romperlo (Google logró construir dos PDFs distintos con el mismo hash SHA1. Más info aquí). Tienes razón, debería yo también usar el SHA256, gracias por el dato 👍

      1. Tenías razón, había más certificados. Saque mi certificado y mi llave pública y con eso los firme.

        Ahora tengo un problema con el servidor de producción, me sale este error
        IndexError: No definition ‘{http://service.sunat.gob.pe}billService’ in ‘port_types’ found.

        He mirado que el xml de producción varia con el beta, hay algo para tener en cuenta antes del envío?

        1. Recuerda que php tiene un bug que no permite usar namespaces, que usa el wsdl del server de produccion de la sunat. Por eso hice el hack que describí en este blog post

  13. Hola al momento de hacer esto:
    $ruc = ‘20563050501’;
    $usuarioSol = ‘MODDATOS’;
    $claveSol = ‘moddatos’;

    $WSHeader = ‘

    ‘ . $ruc. $usuarioSol. ‘
    ‘ . $claveSol. ‘

    ‘;

    $fileName = ‘20563050501-03-B002-00000001.zip’;
    $contentFile = (file_exists(‘../documentos/pagosxml/mayo/20180514/20563050501-03-B002-00000001.zip’) ? base64_encode(file_get_contents(‘../documentos/pagosxml/mayo/20180514/20563050501-03-B002-00000001.zip’)) : ‘no existe el archivo’);
    $array = array(‘filename’ => $fileName, ‘contentFile’ => $contentFile);
    $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));
    $soap = new SoapClient(‘https://e-beta.sunat.gob.pe/ol-ti-itcpfegem-beta/billService?wsdl’);
    $soap->__soapCall(‘sendBill’, $array, null, $headers);

    No me retorna nada, queda en vacío, me ayudas con esto por favor.

    1. Hola! Recuerda que el 2do parámetro de __soapCall es un array dentro de un array, por alguna razón. Esa línea debería quedar algo como $soap->__soapCall(‘sendBill’, [$array], null, $headers);.

      1. Hola DrMad que tal, me muestra este error: Fatal error: Uncaught SoapFault exception: [soap-env:Client.0151] El nombre del archivo ZIP es incorrecto – Detalle: xxx.xxx.xxx value=’ticket: error: Error de nombre archivo ” codigo cpe: ” no es un cpe valido’ in C:\ubicacion\test.php:45 Stack trace: #0 C:\ubicacion\test.php(45): SoapClient->__soapCall(‘sendBill’, Array, NULL, Object(SoapHeader)) #1 [internal function]: Test->index() #2 C:\ubicacion\core\CodeIgniter.php(359): call_user_func_array(Array, Array) #3 C:\ubicacion\index.php(215): require_once(‘C:\\ubicacion..’) #4 {main} thrown in C:\ubicacion\test.php on line 45.

        quería saber si estoy por buen camino, entiendo que debo cambiar el nombre del zip y xml segun el formato de la sunat y crei que debia poner una carpeta dummy dentro del zip por lo que decia la documentacion de la sunat. Saludos

        1. Hola de nuevo.

          No, no va la carpeta ‘dummy’, tal como lo describo en este mismo post. Sería bueno que me comentes qué nombre estás colocando al ZIP y al XML dentro, pues la SUNAT te informa que estas colocándolo mal.

          1. Hola Drmad, el zip y xml tienen el mismo nombre (obviamente diferente extension :B), que es así
            RUC-TIPOcomprobante-[(B)o(F) depende si es fact o bol]serie-correlativo(numeracion de la boleta) y finalmente la extensión. solo es el zip y xsml ya saque la carpeta dummy y a pesar de eso me muestra el mensaje de error de nombre. Help me!!! Saludos hermano y gracias por la ayuda que me brindas 😀

          2. Hey Alejandro.

            Obviamente, estás formando mal el nombre, por eso te pedí que me enviaras exactamente lo que estás consignando ahi.

            Si quieres, puedes enviarlo directo a mi al correo “yo” en este dominio 🙂

  14. Hola drMad, el nombre de los archivos esta así: 20563050501-03-B002-00023275(.zip)(.xml). lo que no entiendo es que se pone en el digestValue, lo que vi en la doc de la sunat es el valor hash codificado en Base64, no entiendo esa parte(que es lo que se debe codificar en base64? que valor? ) .

    1. El nombre está bien, es probable que tu programa lo esté creando mal 😀 El digestValue se obtiene después de firmar el documento. Es un procedimiento complejo, yo opté por usar un programa externo para ello. También lo podrías hacer tu, PHP tiene librerías de OpenSSL para ello.

    1. Hola.

      Como digo en el post, yo no uso PHP para el firmado del XML, uso un programa externo. Luego leo el XML firmado, y obtengo esos valores. Es posible firmar el XML con PHP, usando OpenSSL o alguna clase que sé que existe en las interwebs 🙂 pero conmigo, la flojera pudo más 😀

  15. Hola drmad, puedes por favor compartirnos el comando exacto del xmlsec1 para firmar un xml? O explicar un poco mas acerca del proceso de firma de un xml? (si es que hay que manipular la estructura del xml previamente al firmado). Gracias!!

    1. Hola, este sería el comando con xmlsec1 :

      $xmlsec1 –sign –output documento_firmado.xml –pkcs12 certificado.p12 –pwd clave_certificado documento_a_firmar.xml

      El archivo con extension p12 contiene un certificado y su clave privada respectiva. Eso lo puedes armar con la libreria OpenSSL.

      Saludos.

  16. Hola, tu post me alivia mucho. Ya que yo estoy intentando hacer lo mismo, pero en mi caso necesito hacerlo en Python. Tengo el armado de trama y el envío (el cuale estoy haciendo mediante la librería Zeep). Sin embargo no logro que el ws de la sunat me de una respuesta de que la factura se ha ingresado, al menos no por código. Sin embargo, si es que mi xml, dentro de mi .zip lo introduzco en SoapUI directamente junto al resto de la trama requerida. El documento ingresa.
    Regresando a lo aliviado que me siento por tu post, la razón es que indica que no utilicemos la librería base64. ¿Por qué lo dices? ¿Sabes de otra librería que podría codificar en base64 mi .zip para realizar el envío de la trama utilizando Python?
    Gracias de antemano.

    1. 😃 Me alegra saber tu alivio, y que usas python 😄

      Creo que te refieres a un comentario donde indico que no use la función base64_encode en el zip antes de enviarlo por SOAP. En ese caso, es por que la librería de SOAP de PHP va a codificar el adjunto automáticamente. Si usas el base64_encode, el fichero acabará sobre-codificado, y enviará el base64 del base64 del zip.

      1. Ese error lo cometía yo también en Python con la libreria respectiva.

        Tu post también me fue de mucha ayuda, Oliver, quería hacerte una consulta aprovechando la duda del amigo. ¿Lograste desarrollar las salidas en XML para la versión 2.1 de ubl?

        Gracias de antemano.

        1. Hola Marco, me alegra también leer eso 😁

          Y no, aun no he acabado de implementar el 2.1 😟 Problemas, problemas everywhere…

          Aunque hoy recibí una buena noticia: La SUNAT va a empezar a dar gratis los certificados digitales a las empresas con ingresos anuales que no pasen los 300 UIT 😋

          Eso se merece un blogpost 😁

          1. Jaja, sí, esa es una buena noticia! Aparte lo de los OSE, no sé cómo van a hacer con ese tema, hasta hay una demanda de por medio a la SUNAT. Lo de los certificados gratis creo que se dará hasta el otro año, pero bueno es un avance… Lo malo de la versión 2.1 es la cantidad de tipo de factura o notas de crédito/débito que hay, por ejemplo o los pocos modelos de ejemplo que publica SUNAT para los diferentes casos. Esperemos que mejoren esa parte para implementarlo.

            Saludos!

        2. Oof!
          Eso era lo único que estaba haciendo mal. Lo estaba codificando a base64 dos veces. Yo utilizo Zeep y Zeep también lo codifica en base64 automáticamente antes de enviarlo. Que gran alivio. Ahora solo falta esperar a que me den mi firma digital gratis :v Por si acaso esperaré sentado :v

    2. Hola yo uso Python 3.6 para enviar mis documentos electrónicos, tuve algunos problemas, pero ya están solucionados. Si deseas ayuda puedes hablarme. Saludos!

Leave a Reply

Su dirección de correo no se hará público. Los campos requeridos están marcados *