Giter Club home page Giter Club logo

facturae-php's People

Contributors

alphp avatar ct2glx avatar duhow avatar franbarro avatar josemmo avatar kaiservito avatar peter279k avatar phoneixs avatar txusms avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

facturae-php's Issues

Formato de factura no válido

Al generar una factura con la extensión Faceb2b y añadir la entidad pública y la referencia del contrato con:

$b2b->setPublicOrganismCode('E00003301');
$b2b->setContractReference('333000');

No consigo validadr el fichero .xsig generado. Los validadores me dan este error:

Formato de factura no válido: (1): [Extensions] - (l2:c3400) - El comodín coincidente es estricto, pero no se ha encontrado ninguna declaración para el elemento 'fb2b

Redondeo de decimales

Dado que FacturaE 3.2.X solo permite usar dos decimales, se presenta un problema adicional al ya solventado #23: el redondeo.

Más que un bug, esto es un problema de diseño importante que plantea una nueva incógnita: dado que hay que perder precisión a la fuerza porque lo exige el Estado, ¿es mejor que Facturae-PHP obligue al usuario a hacer facturas con decimales precisos (lanzando, por ejemplo, una excepción si el redondeo no es correcto), o que ajuste automáticamente los importes resultantes para que coincida con el total?

Esta es una factura de ejemplo en la que ocurre el problema del redondeo:
Ejemplo

Error, cuando solicito totales en Facturae

Con estos datos, donde además de poner descuento en línea de detalle, indico un descuento general en la factura.
Observo que el iva lo calcula de la base en vez del total sin impuestos.

$fac = new Facturae();
    ...    
$fac->addItem(new FacturaeItem([
"name"=>"Linea 1"],
"quantity"=>1,
"unitPriceWithoutTax"=>580,
"discounts"=>[
                    ["reason"=>"Descuento linea","rate"=>10]
                ],
                "taxes"=>[
                    Facturae::TAX_IVA => 10
                ]
            ]));
        
  $fac->addDiscount('Dto.sobre base imponible total',20);
  $totales=$fac->getTotals();

image

Como se observa el iva no se calcula del total sin impuestos sino de la base.

Gracias anticipadas

Class FacturaeExportable not found

Hola Jose, estoy intentando usar tu código para subir una factura, pero al ejecutar el ejemplo de envío de factura me dice
PHP Fatal error: Class 'josemmo\Facturae\Controller\FacturaeExportable' not found in /var/www/facturae/Facturae-PHP/src/Facturae.php on line 9

Lo uso sin composer, simplemente clonando el git.

Y pongo los requiere

require_once 'Facturae-PHP/src/Facturae.php';
require_once 'Facturae-PHP/src/FacturaeCentre.php';
require_once 'Facturae-PHP/src/FacturaeItem.php';
require_once 'Facturae-PHP/src/FacturaeParty.php';
require_once 'Facturae-PHP/src/FacturaExportable.php';

Rediseño del redondeo de decimales

Dado que este es un problema recurrente, voy a intentar rediseñar la forma en la que Facturae-PHP gestiona el redondeo de decimales dentro de una factura.

Ahora mismo se redondea un mismo valor varias veces con la pérdida de precisión que eso conlleva.
El objetivo es hacer el redondeo solo una vez (justo antes de generar el documento).

Este feat es un experimento y puede que no llegue a una versión estable si los validadores de FacturaE no admiten los cálculos de esta segunda forma.

Error al exportar una factura con FACTURAE

Hola.

Obtengo el siguiente error al intentar realizar una exportación de una factura:

Fatal error: Call to a member function getXML() on null in /var/www/localhost/htdocs/facturacion/lib/Facturae-PHP-master/src/FacturaeTraits/ExportableTrait.php on line 90

¿Los decimales en las facturas ya se validan correctamente?

Estamos teniendo problemas con el redondeo de los decimales de los conceptos en las facturas y veo que tras el problema de #23 se optó por no dejar más decimales.

Pero creo que ya sí que se admiten más decimales; aunque creo que no en el total, sí que deja más decimales en Item/UnitPriceWithoutTax y Item/TotalAmountWithoutTax por lo menos. Al menos con el validador de https://se-face.redsara.es/es/facturas/validar-visualizar-facturas sí que las pasa como válidas.

¿Qué validador usas para comprobar el formato de las facturas?

En caso de que el sistema ya admitiera la cantidad de decimales que indican las especificaciones, habría que cambiar el array de las constantes de decimales para adecuarlas a las nuevas cantidades.

Problema al calcular el total de la factura cuando hay varios impuestos

Hola, estoy probando tu librería para sustituir a la mía en la generación de facturae en FacturaScripts.

El caso es que yo genero miles de facturas con datos aleatorios para encontrar fallos, y me he encontrado que con las facturas con IRPF falla la validación contable.

El fichero seleccionado no es validable: Error en su lectura: Totalfactura de la factura número 1 no es igual a TotalImporteBrutoAntesImpuestos + TotalImpuestosRepercutidos - TotalImpuestosRetenidos, debería ser : 618.37

Mirando un poco más detenidamente veo que tu librería calcula incorrectamente el total y le baila un céntimo, dando como resultado 618.38

Ta adjunto el xml.
FAC2018IR1_facturae.zip

Mañana seguiré revisando. Y disculpa por si al final fuese problema mio ;-)

Número de decimales en los totales de los elementos de una factura

Muy buenas Josemmo, antetodo gracias por tu gran trabajo, al intentar validar hoy el .xsig generado por su clase, me ha dado varios errores de validación y he modificado la clase Facturae.php, te adjunto la clase entera ya modificada por mi, he agregado 2 atributos para los decimales de los totales de los items que son a 6 decimales, y como usas padTotal para varios sitios he creado padTotalItem para aquellos que me daban error.

Te pongo estas modificaciones para que si puedes lo agreges a la clase.

Mostrar código

<?php
namespace josemmo\Facturae;

/**
 * Facturae
 *
 * This file contains everything you need to create invoices.
 *
 * @package josemmo\Facturae
 * @version 1.2.3
 * @license http://www.opensource.org/licenses/mit-license.php  MIT License
 * @author  josemmo
 */


/**
 * Facturae
 *
 * Standalone class designed to create full compliance Facturae files from
 * scratch, without the need of any other tools for signing.
 */
class Facturae {

  /* CONSTANTS */
  const SCHEMA_3_2 = "3.2";
  const SCHEMA_3_2_1 = "3.2.1";
  const SCHEMA_3_2_2 = "3.2.2";
  const SIGN_POLICY_3_1 = array(
    "name" => "Política de Firma FacturaE v3.1",
    "url" => "http://www.facturae.es/politica_de_firma_formato_facturae/politica_de_firma_formato_facturae_v3_1.pdf",
    "digest" => "Ohixl6upD6av8N7pEvDABhEL6hM="
  );

  const PAYMENT_CASH = "01";
  const PAYMENT_TRANSFER = "04";

  const TAX_IVA = "01";
  const TAX_IPSI = "02";
  const TAX_IGIC = "03";
  const TAX_IRPF = "04";
  const TAX_OTHER = "05";
  const TAX_ITPAJD = "06";
  const TAX_IE = "07";
  const TAX_RA = "08";
  const TAX_IGTECM = "09";
  const TAX_IECDPCAC = "10";
  const TAX_IIIMAB = "11";
  const TAX_ICIO = "12";
  const TAX_IMVDN = "13";
  const TAX_IMSN = "14";
  const TAX_IMGSN = "15";
  const TAX_IMPN = "16";
  const TAX_REIVA = "17";
  const TAX_REIGIC = "18";
  const TAX_REIPSI = "19";


  /* PRIVATE CONSTANTS */
  private static $SCHEMA_NS = array(
    self::SCHEMA_3_2   => "http://www.facturae.es/Facturae/2009/v3.2/Facturae",
    self::SCHEMA_3_2_1 => "http://www.facturae.es/Facturae/2014/v3.2.1/Facturae",
    self::SCHEMA_3_2_2 => "http://www.facturae.gob.es/formato/Versiones/Facturaev3_2_2.xml"
  );
  private static $USER_AGENT = "FacturaePHP/1.2.3";


  /* ATTRIBUTES */
  private $currency = "EUR";
  private $itemsPrecision = 6;
  private $itemsPadding = 6;
  private $totalsPrecision = 2;
  private $totalsPadding = 2;
  private $totalitemsPrecision = 6;
  private $totalitemsPadding = 6;

  private $version = NULL;
  private $header = array(
    "serie" => NULL,
    "number" => NULL,
    "issueDate" => NULL,
    "dueDate" => NULL,
    "startDate" => NULL,
    "endDate" => NULL,
    "paymentMethod" => NULL,
    "paymentIBAN" => NULL
  );
  private $parties = array(
    "seller" => NULL,
    "buyer" => NULL
  );
  private $items = array();
  private $legalLiterals = array();

  private $signTime = NULL;
  private $timestampServer = NULL;
  private $timestampUser = NULL;
  private $timestampPass = NULL;
  private $signPolicy = NULL;
  private $publicKey = NULL;
  private $privateKey = NULL;


  /**
   * Construct
   *
   * @param string $schemaVersion If omitted, latest version available
   */
  public function __construct($schemaVersion=self::SCHEMA_3_2_1) {
    $this->setSchemaVersion($schemaVersion);
  }


  /**
   * Generate random ID
   *
   * This method is used for generating random IDs required when signing the
   * document.
   *
   * @return int Random number
   */
  private function random() {
    if (function_exists('random_int')) {
      return random_int(0x10000000, 0x7FFFFFFF);
    } else {
      return rand(100000, 999999);
    }
  }


  /**
   * Pad
   *
   * @param  float  $val       Input
   * @param  int    $precision Decimals to round
   * @param  int    $padding   Decimals to pad
   * @return string            Padded value
   */
  private function pad($val, $precision, $padding) {
    return number_format(round($val, $precision), $padding, ".", "");
  }


  /**
   * Pad total value
   *
   * @param  float $val Input
   * @return string     Padded value
   */
  private function padTotal($val) {
    return $this->pad($val, $this->totalsPrecision, $this->totalsPadding);
  }
  /**
   * Pad total value
   *
   * @param  float $val Input
   * @return string     Padded value
   */
  private function padTotalItem($val) {
    return $this->pad($val, $this->totalitemsPrecision, $this->totalitemsPadding);
  }


  /**
   * Pad item value
   *
   * @param  float  $val Input
   * @return string      Padded value
   */
  private function padItem($val) {
    return $this->pad($val, $this->itemsPrecision, $this->itemsPadding);
  }


  /**
   * Is withheld tax
   *
   * This method returns if a tax type is, by default, a withheld tax
   *
   * @param  string  $taxCode Tax
   * @return boolean          Is withheld
   */
  public static function isWithheldTax($taxCode) {
    return in_array($taxCode, [self::TAX_IRPF]);
  }


  /**
   * Set schema version
   *
   * @param string $schemaVersion Facturae schema version to use
   */
  public function setSchemaVersion($schemaVersion) {
    $this->version = $schemaVersion;
  }


  /**
   * Set seller
   *
   * @param FacturaeParty $seller Seller information
   */
  public function setSeller($seller) {
    $this->parties['seller'] = $seller;
  }


  /**
   * Set buyer
   *
   * @param FacturaeParty $buyer Buyer information
   */
  public function setBuyer($buyer) {
    $this->parties['buyer'] = $buyer;
  }


  /**
   * Set invoice number
   *
   * @param string     $serie  Serie code of the invoice
   * @param int|string $number Invoice number in given serie
   */
  public function setNumber($serie, $number) {
    $this->header['serie'] = $serie;
    $this->header['number'] = $number;
  }


  /**
   * Set issue date
   *
   * @param int|string $date Issue date
   */
  public function setIssueDate($date) {
    $this->header['issueDate'] = is_string($date) ? strtotime($date) : $date;
  }


  /**
   * Set due date
   *
   * @param int|string $date Due date
   */
  public function setDueDate($date) {
    $this->header['dueDate'] = is_string($date) ? strtotime($date) : $date;
  }


  /**
   * Set billing period
   *
   * @param int|string $date Start date
   * @param int|string $date End date
   */
  public function setBillingPeriod($startDate=NULL, $endDate=NULL) {
    $d_start = is_string($startDate) ? strtotime($startDate) : $startDate;
    $d_end = is_string($endDate) ? strtotime($endDate) : $endDate;
    $this->header['startDate'] = $d_start;
    $this->header['endDate'] = $d_end;
  }


  /**
   * Set dates
   *
   * This is a shortcut for setting both issue and due date in a single line
   *
   * @param int|string $issueDate Issue date
   * @param int|string $dueDate Due date
   */
  public function setDates($issueDate, $dueDate=NULL) {
    $this->setIssueDate($issueDate);
    $this->setDueDate($dueDate);
  }


  /**
   * Set payment method
   *
   * @param string $method Payment method
   * @param string $iban   Bank account in case of bank transfer
   */
  public function setPaymentMethod($method=self::PAYMENT_CASH, $iban=NULL) {
    $this->header['paymentMethod'] = $method;
    if (!is_null($iban)) $iban = str_replace(" ", "", $iban);
    $this->header['paymentIBAN'] = $iban;
  }


  /**
   * Add item
   *
   * Adds an item row to invoice. The fist parameter ($desc), can be an string
   * representing the item description or a 2 element array containing the item
   * description and an additional string of information.
   *
   * @param FacturaeItem|string|array $desc      Item to add or description
   * @param float                     $unitPrice Price per unit, taxes included
   * @param float                     $quantity  Quantity
   * @param int                       $taxType   Tax type
   * @param float                     $taxRate   Tax rate
   */
  public function addItem($desc, $unitPrice=NULL, $quantity=1, $taxType=NULL, $taxRate=NULL) {
    if ($desc instanceOf FacturaeItem) {
      $item = $desc;
    } else {
      $item = new FacturaeItem([
        "name" => is_array($desc) ? $desc[0] : $desc,
        "description" => is_array($desc) ? $desc[1] : NULL,
        "quantity" => $quantity,
        "unitPrice" => $unitPrice,
        "taxes" => array($taxType => $taxRate)
      ]);
    }
    array_push($this->items, $item);
  }


  /**
   * Add legal literal
   *
   * @param string $message Legal literal reference
   */
  public function addLegalLiteral($message) {
    $this->legalLiterals[] = $message;
  }


  /**
   * Get totals
   *
   * @return array Invoice totals
   */
  public function getTotals() {
    // Define starting values
    $totals = array(
      "taxesOutputs" => array(),
      "taxesWithheld" => array(),
      "invoiceAmount" => 0,
      "grossAmount" => 0,
      "grossAmountBeforeTaxes" => 0,
      "totalTaxesOutputs" => 0,
      "totalTaxesWithheld" => 0
    );

    // Run through every item
    foreach ($this->items as $itemObj) {
      $item = $itemObj->getData();
      $totals['invoiceAmount'] += $item['totalAmount'];
      $totals['grossAmount'] += $item['grossAmount'];
      $totals['totalTaxesOutputs'] += $item['totalTaxesOutputs'];
      $totals['totalTaxesWithheld'] += $item['totalTaxesWithheld'];

      // Get taxes
      foreach (["taxesOutputs", "taxesWithheld"] as $taxGroup) {
        foreach ($item[$taxGroup] as $type=>$tax) {
          if (!isset($totals[$taxGroup][$type]))
            $totals[$taxGroup][$type] = array();
          if (!isset($totals[$taxGroup][$type][$tax['rate']]))
            $totals[$taxGroup][$type][$tax['rate']] = array("base"=>0, "amount"=>0);
          $totals[$taxGroup][$type][$tax['rate']]['base'] +=
            $item['totalAmountWithoutTax'];
          $totals[$taxGroup][$type][$tax['rate']]['amount'] += $tax['amount'];
        }
      }
    }

    // Fill rest of values
    $totals['grossAmountBeforeTaxes'] = $totals['grossAmount'];

    return $totals;
  }


  /**
   * Set sign time
   *
   * @param int|string $time Time of the signature
   */
  public function setSignTime($time) {
    $this->signTime = is_string($time) ? strtotime($time) : $time;
  }


  /**
   * Set timestamp server
   *
   * @param string $server Timestamp Authority URL
   * @param string $user   TSA User
   * @param string $pass   TSA Password
   */
  public function setTimestampServer($server, $user=NULL, $pass=NULL) {
    $this->timestampServer = $server;
    $this->timestampUser = $user;
    $this->timestampPass = $pass;
  }


  /**
   * Load a PKCS#12 Certificate Store
   *
   * @param  string  $pkcs12File The certificate store file name
   * @param  string  $pkcs12Pass Encryption password for unlocking the PKCS#12 file
   * @return boolean             Success
   */
  private function loadPkcs12($pkcs12File, $pkcs12Pass="") {
    if (!is_file($pkcs12File)) return false;

    // Extract public and private keys from store
    if (openssl_pkcs12_read(file_get_contents($pkcs12File), $certs, $pkcs12Pass)) {
      $this->publicKey = openssl_x509_read($certs['cert']);
      $this->privateKey = openssl_pkey_get_private($certs['pkey']);
    }

    return (!empty($this->publicKey) && !empty($this->privateKey));
  }


  /**
   * Load a X.509 certificate and PEM encoded private key
   *
   * @param  string  $publicPath  Path to public key PEM file
   * @param  string  $privatePath Path to private key PEM file
   * @param  string  $passphrase  Private key passphrase
   * @return boolean              Success
   */
  private function loadX509($publicPath, $privatePath, $passphrase="") {
    if (is_file($publicPath) && is_file($privatePath)) {
      $this->publicKey = openssl_x509_read(file_get_contents($publicPath));
      $this->privateKey = openssl_pkey_get_private(
        file_get_contents($privatePath),
        $passphrase
      );
    }
    return (!empty($this->publicKey) && !empty($this->privateKey));
  }


  /**
   * Sign
   *
   * @param  string  $publicPath  Path to public key PEM file or PKCS#12 certificate store
   * @param  string  $privatePath Path to private key PEM file (should be NULL in case of PKCS#12)
   * @param  string  $passphrase  Private key passphrase
   * @param  array   $policy      Facturae sign policy
   * @return boolean              Success
   */
  public function sign($publicPath, $privatePath=NULL, $passphrase="", $policy=self::SIGN_POLICY_3_1) {
    $this->publicKey = NULL;
    $this->privateKey = NULL;
    $this->signPolicy = $policy;

    // Generate random IDs
    $this->signatureID = $this->random();
    $this->signedInfoID = $this->random();
    $this->signedPropertiesID = $this->random();
    $this->signatureValueID = $this->random();
    $this->certificateID = $this->random();
    $this->referenceID = $this->random();
    $this->signatureSignedPropertiesID = $this->random();
    $this->signatureObjectID = $this->random();

    // Load public and private keys
    if (empty($privatePath)) {
      return $this->loadPkcs12($publicPath, $passphrase);
    } else {
      return $this->loadX509($publicPath, $privatePath, $passphrase);
    }
  }


  /**
   * Get XML NameSpaces
   *
   * NOTE: Should be defined in alphabetical order
   *
   * @return string XML NameSpaces
   */
  private function getNamespaces() {
    $xmlns = array();
    $xmlns[] = 'xmlns:ds="http://www.w3.org/2000/09/xmldsig#"';
    $xmlns[] = 'xmlns:fe="' . self::$SCHEMA_NS[$this->version] . '"';
    $xmlns[] = 'xmlns:xades="http://uri.etsi.org/01903/v1.3.2#"';
    $xmlns = implode(' ', $xmlns);
    return $xmlns;
  }


  /**
   * Inject timestamp
   *
   * @param  string $signedXml Signed XML document
   * @return string            Signed and timestamped XML document
   */
  private function injectTimestamp($signedXml) {
    // Prepare data to timestamp
    $payload = explode('<ds:SignatureValue', $signedXml)[1];
    $payload = explode('</ds:SignatureValue>', $payload)[0];
    $payload = '<ds:SignatureValue ' . $this->getNamespaces() . $payload . '</ds:SignatureValue>';

    // Create TimeStampQuery in ASN1 using SHA-1
    $tsq = "302c0201013021300906052b0e03021a05000414";
    $tsq .= hash('sha1', $payload);
    $tsq .= "0201000101ff";
    $tsq = hex2bin($tsq);

    // Await TimeStampRequest
    $chOpts = array(
      CURLOPT_URL => $this->timestampServer,
      CURLOPT_RETURNTRANSFER => 1,
      CURLOPT_BINARYTRANSFER => 1,
      CURLOPT_SSL_VERIFYPEER => 0,
      CURLOPT_FOLLOWLOCATION => 1,
      CURLOPT_CONNECTTIMEOUT => 0,
      CURLOPT_TIMEOUT => 10, // 10 seconds timeout
      CURLOPT_POST => 1,
      CURLOPT_POSTFIELDS => $tsq,
      CURLOPT_HTTPHEADER => array("Content-Type: application/timestamp-query"),
      CURLOPT_USERAGENT => self::$USER_AGENT
    );
    if (!empty($this->timestampUser) && !empty($this->timestampPass)) {
      $chOpts[CURLOPT_USERPWD] = $this->timestampUser . ":" . $this->timestampPass;
    }
    $ch = curl_init();
    curl_setopt_array($ch, $chOpts);
    $tsr = curl_exec($ch);
    if ($tsr === false) throw new \Exception('cURL error: ' . curl_error($ch));
    curl_close($ch);

    // Validate TimeStampRequest
    $responseCode = substr($tsr, 6, 3);
    if ($responseCode !== "\02\01\00") { // Bytes for INTEGER 0 in ASN1
      throw new \Exception('Invalid TSR response code');
    }

    // Extract TimeStamp from TimeStampRequest and inject into XML document
    $timeStamp = substr($tsr, 9);
    $tsXml = '<xades:UnsignedProperties Id="Signature' . $this->signatureID . '-UnsignedProperties' . $this->random() . '">' .
               '<xades:UnsignedSignatureProperties>' .
                 '<xades:SignatureTimeStamp Id="Timestamp-' . $this->random() . '">' .
                   '<ds:CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315">' .
                   '</ds:CanonicalizationMethod>' .
                   '<xades:EncapsulatedTimeStamp>' . "\n" .
                     str_replace("\r", "", chunk_split(base64_encode($timeStamp), 76)) .
                   '</xades:EncapsulatedTimeStamp>' .
                 '</xades:SignatureTimeStamp>' .
               '</xades:UnsignedSignatureProperties>' .
             '</xades:UnsignedProperties>';
    $signedXml = str_replace('</xades:QualifyingProperties>', $tsXml . '</xades:QualifyingProperties>', $signedXml);
    return $signedXml;
  }


  /**
   * Inject signature
   *
   * @param  string $xml Unsigned XML document
   * @return string      Signed XML document
   */
  private function injectSignature($xml) {
    // Make sure we have all we need to sign the document
    if (empty($this->publicKey) || empty($this->privateKey)) return $xml;

    // Normalize document
    $xml = str_replace("\r", "", $xml);

    // Define namespace
    $xmlns = $this->getNamespaces();

    // Prepare signed properties
    $signTime = is_null($this->signTime) ? time() : $this->signTime;
    $certData = openssl_x509_parse($this->publicKey);
    $certDigest = openssl_x509_fingerprint($this->publicKey, "sha1", true);
    $certDigest = base64_encode($certDigest);
    $certIssuer = array();
    foreach ($certData['issuer'] as $item=>$value) {
      $certIssuer[] = $item . '=' . $value;
    }
    $certIssuer = implode(',', $certIssuer);

    // Generate signed properties
    $prop = '<xades:SignedProperties Id="Signature' . $this->signatureID .
            '-SignedProperties' . $this->signatureSignedPropertiesID . '">' .
              '<xades:SignedSignatureProperties>' .
                '<xades:SigningTime>' . date('c', $signTime) . '</xades:SigningTime>' .
                '<xades:SigningCertificate>' .
                  '<xades:Cert>' .
                    '<xades:CertDigest>' .
                      '<ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"></ds:DigestMethod>' .
                      '<ds:DigestValue>' . $certDigest . '</ds:DigestValue>' .
                    '</xades:CertDigest>' .
                    '<xades:IssuerSerial>' .
                      '<ds:X509IssuerName>' . $certIssuer . '</ds:X509IssuerName>' .
                      '<ds:X509SerialNumber>' . $certData['serialNumber'] . '</ds:X509SerialNumber>' .
                    '</xades:IssuerSerial>' .
                  '</xades:Cert>' .
                '</xades:SigningCertificate>' .
                '<xades:SignaturePolicyIdentifier>' .
                  '<xades:SignaturePolicyId>' .
                    '<xades:SigPolicyId>' .
                      '<xades:Identifier>' . $this->signPolicy['url'] . '</xades:Identifier>' .
                      '<xades:Description>' . $this->signPolicy['name'] . '</xades:Description>' .
                    '</xades:SigPolicyId>' .
                    '<xades:SigPolicyHash>' .
                      '<ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"></ds:DigestMethod>' .
                      '<ds:DigestValue>' . $this->signPolicy['digest'] . '</ds:DigestValue>' .
                    '</xades:SigPolicyHash>' .
                  '</xades:SignaturePolicyId>' .
                '</xades:SignaturePolicyIdentifier>' .
                '<xades:SignerRole>' .
                  '<xades:ClaimedRoles>' .
                    '<xades:ClaimedRole>emisor</xades:ClaimedRole>' .
                  '</xades:ClaimedRoles>' .
                '</xades:SignerRole>' .
              '</xades:SignedSignatureProperties>' .
              '<xades:SignedDataObjectProperties>' .
                '<xades:DataObjectFormat ObjectReference="#Reference-ID-' . $this->referenceID . '">' .
                  '<xades:Description>Factura electrónica</xades:Description>' .
                  '<xades:MimeType>text/xml</xades:MimeType>' .
                '</xades:DataObjectFormat>' .
              '</xades:SignedDataObjectProperties>' .
            '</xades:SignedProperties>';

    // Prepare key info
    $publicPEM = "";
    openssl_x509_export($this->publicKey, $publicPEM);
    $publicPEM = str_replace("-----BEGIN CERTIFICATE-----", "", $publicPEM);
    $publicPEM = str_replace("-----END CERTIFICATE-----", "", $publicPEM);
    $publicPEM = str_replace("\n", "", $publicPEM);
    $publicPEM = str_replace("\r", "", chunk_split($publicPEM, 76));

    $privateData = openssl_pkey_get_details($this->privateKey);
    $modulus = chunk_split(base64_encode($privateData['rsa']['n']), 76);
    $modulus = str_replace("\r", "", $modulus);
    $exponent = base64_encode($privateData['rsa']['e']);

    // Generate KeyInfo
    $kInfo = '<ds:KeyInfo Id="Certificate' . $this->certificateID . '">' . "\n" .
               '<ds:X509Data>' . "\n" .
                 '<ds:X509Certificate>' . "\n" . $publicPEM . '</ds:X509Certificate>' . "\n" .
               '</ds:X509Data>' . "\n" .
               '<ds:KeyValue>' . "\n" .
                 '<ds:RSAKeyValue>' . "\n" .
                   '<ds:Modulus>' . "\n" . $modulus . '</ds:Modulus>' . "\n" .
                   '<ds:Exponent>' . $exponent . '</ds:Exponent>' . "\n" .
                 '</ds:RSAKeyValue>' . "\n" .
               '</ds:KeyValue>' . "\n" .
             '</ds:KeyInfo>';

    // Calculate digests
    $propDigest = base64_encode(sha1(str_replace('<xades:SignedProperties',
      '<xades:SignedProperties ' . $xmlns, $prop), true));
    $kInfoDigest = base64_encode(sha1(str_replace('<ds:KeyInfo',
      '<ds:KeyInfo ' . $xmlns, $kInfo), true));
    $documentDigest = base64_encode(sha1($xml, true));

    // Generate SignedInfo
    $sInfo = '<ds:SignedInfo Id="Signature-SignedInfo' . $this->signedInfoID . '">' . "\n" .
               '<ds:CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315">' .
               '</ds:CanonicalizationMethod>' . "\n" .
               '<ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1">' .
               '</ds:SignatureMethod>' . "\n" .
               '<ds:Reference Id="SignedPropertiesID' . $this->signedPropertiesID . '" ' .
               'Type="http://uri.etsi.org/01903#SignedProperties" ' .
               'URI="#Signature' . $this->signatureID . '-SignedProperties' .
               $this->signatureSignedPropertiesID . '">' . "\n" .
                 '<ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1">' .
                 '</ds:DigestMethod>' . "\n" .
                 '<ds:DigestValue>' . $propDigest . '</ds:DigestValue>' . "\n" .
               '</ds:Reference>' . "\n" .
               '<ds:Reference URI="#Certificate' . $this->certificateID . '">' . "\n" .
                 '<ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1">' .
                 '</ds:DigestMethod>' . "\n" .
                 '<ds:DigestValue>' . $kInfoDigest . '</ds:DigestValue>' . "\n" .
               '</ds:Reference>' . "\n" .
               '<ds:Reference Id="Reference-ID-' . $this->referenceID . '" URI="">' . "\n" .
                 '<ds:Transforms>' . "\n" .
                   '<ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature">' .
                   '</ds:Transform>' . "\n" .
                 '</ds:Transforms>' . "\n" .
                 '<ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1">' .
                 '</ds:DigestMethod>' . "\n" .
                 '<ds:DigestValue>' . $documentDigest . '</ds:DigestValue>' . "\n" .
               '</ds:Reference>' . "\n" .
             '</ds:SignedInfo>';

    // Calculate signature
    $signaturePayload = str_replace('<ds:SignedInfo', '<ds:SignedInfo ' . $xmlns, $sInfo);
    openssl_sign($signaturePayload, $signatureResult, $this->privateKey);
    $signatureResult = chunk_split(base64_encode($signatureResult), 76);
    $signatureResult = str_replace("\r", "", $signatureResult);

    // Make signature
    $sig = '<ds:Signature xmlns:xades="http://uri.etsi.org/01903/v1.3.2#" Id="Signature' . $this->signatureID . '">' . "\n" .
             $sInfo . "\n" .
             '<ds:SignatureValue Id="SignatureValue' . $this->signatureValueID . '">' . "\n" .
               $signatureResult .
             '</ds:SignatureValue>' . "\n" .
             $kInfo . "\n" .
             '<ds:Object Id="Signature' . $this->signatureID . '-Object' . $this->signatureObjectID . '">' .
               '<xades:QualifyingProperties Target="#Signature' . $this->signatureID . '">' .
                 $prop .
               '</xades:QualifyingProperties>' .
             '</ds:Object>' .
           '</ds:Signature>';

    // Inject signature
    $xml = str_replace('</fe:Facturae>', $sig . '</fe:Facturae>', $xml);

    // Inject timestamp
    if (!empty($this->timestampServer)) {
      $xml = $this->injectTimestamp($xml);
    }

    return $xml;
  }


  /**
   * Export
   *
   * Get Facturae XML data
   *
   * @param  string     $filePath Path to save invoice
   * @return string|int           XML data|Written file bytes
   */
  public function export($filePath=NULL) {
    // Prepare document
    $xml = '<fe:Facturae xmlns:ds="http://www.w3.org/2000/09/xmldsig#" ' .
           'xmlns:fe="' . self::$SCHEMA_NS[$this->version] . '">';
    $totals = $this->getTotals();

    // Add header
    $batchIdentifier = $this->parties['seller']->taxNumber .
      $this->header['number'] . $this->header['serie'];
    $xml .= '<FileHeader>' .
              '<SchemaVersion>' . $this->version .'</SchemaVersion>' .
              '<Modality>I</Modality>' .
              '<InvoiceIssuerType>EM</InvoiceIssuerType>' .
              '<Batch>' .
                '<BatchIdentifier>' . $batchIdentifier . '</BatchIdentifier>' .
                '<InvoicesCount>1</InvoicesCount>' .
                '<TotalInvoicesAmount>' .
                  '<TotalAmount>' . $this->padTotal($totals['invoiceAmount']) . '</TotalAmount>' .
                '</TotalInvoicesAmount>' .
                '<TotalOutstandingAmount>' .
                  '<TotalAmount>' . $this->padTotal($totals['invoiceAmount']) . '</TotalAmount>' .
                '</TotalOutstandingAmount>' .
                '<TotalExecutableAmount>' .
                  '<TotalAmount>' . $this->padTotal($totals['invoiceAmount']) . '</TotalAmount>' .
                '</TotalExecutableAmount>' .
                '<InvoiceCurrencyCode>' . $this->currency . '</InvoiceCurrencyCode>' .
              '</Batch>' .
            '</FileHeader>';

    // Add parties
    $xml .= '<Parties>' .
              '<SellerParty>' . $this->parties['seller']->getXML($this->version) . '</SellerParty>' .
              '<BuyerParty>' . $this->parties['buyer']->getXML($this->version) . '</BuyerParty>' .
            '</Parties>';

    // Add invoice data
    $xml .= '<Invoices>' .
              '<Invoice>' .
                '<InvoiceHeader>' .
                  '<InvoiceNumber>' . $this->header['number'] . '</InvoiceNumber>' .
                  '<InvoiceSeriesCode>' . $this->header['serie'] . '</InvoiceSeriesCode>' .
                  '<InvoiceDocumentType>FC</InvoiceDocumentType>' .
                  '<InvoiceClass>OO</InvoiceClass>' .
                '</InvoiceHeader>' .
                '<InvoiceIssueData>' .
                  '<IssueDate>' . date('Y-m-d', $this->header['issueDate']) . '</IssueDate>';
    if (!is_null($this->header['startDate'])) {
      $xml .=     '<InvoicingPeriod>' .
                    '<StartDate>' . date('Y-m-d', $this->header['startDate']) . '</StartDate>' .
                    '<EndDate>' . date('Y-m-d', $this->header['endDate']) . '</EndDate>' .
                  '</InvoicingPeriod>';
    }
    $xml .=       '<InvoiceCurrencyCode>' . $this->currency . '</InvoiceCurrencyCode>' .
                  '<TaxCurrencyCode>' . $this->currency . '</TaxCurrencyCode>' .
                  '<LanguageName>es</LanguageName>' .
                '</InvoiceIssueData>';

    // Add invoice taxes
    foreach (["taxesOutputs", "taxesWithheld"] as $i=>$taxesGroup) {
      if (count($totals[$taxesGroup]) == 0) continue;
      $xmlTag = ucfirst($taxesGroup); // Just capitalize variable name
      $xml .= "<$xmlTag>";
      foreach ($totals[$taxesGroup] as $type=>$taxRows) {
        foreach ($taxRows as $rate=>$tax) {
          $xml .= '<Tax>' .
                    '<TaxTypeCode>' . $type . '</TaxTypeCode>' .
                    '<TaxRate>' . $rate . '</TaxRate>' .
                    '<TaxableBase>' .
                      '<TotalAmount>' . $this->padTotal($tax['base']) . '</TotalAmount>' .
                    '</TaxableBase>' .
                    '<TaxAmount>' .
                      '<TotalAmount>' . $this->padTotal($tax['amount']) . '</TotalAmount>' .
                    '</TaxAmount>' .
                  '</Tax>';
        }
      }
      $xml .= "</$xmlTag>";
    }

    // Add invoice totals
    $xml .= '<InvoiceTotals>' .
              '<TotalGrossAmount>' . $this->padTotal($totals['grossAmount']) . '</TotalGrossAmount>' .
              '<TotalGeneralDiscounts>0.00</TotalGeneralDiscounts>' .
              '<TotalGeneralSurcharges>0.00</TotalGeneralSurcharges>' .
              '<TotalGrossAmountBeforeTaxes>' . $this->padTotal($totals['grossAmountBeforeTaxes']) . '</TotalGrossAmountBeforeTaxes>' .
              '<TotalTaxOutputs>' . $this->padTotal($totals['totalTaxesOutputs']) . '</TotalTaxOutputs>' .
              '<TotalTaxesWithheld>' . $this->padTotal($totals['totalTaxesWithheld']) . '</TotalTaxesWithheld>' .
              '<InvoiceTotal>' . $this->padTotal($totals['invoiceAmount']) . '</InvoiceTotal>' .
              '<TotalOutstandingAmount>' . $this->padTotal($totals['invoiceAmount']) . '</TotalOutstandingAmount>' .
              '<TotalExecutableAmount>' . $this->padTotal($totals['invoiceAmount']) . '</TotalExecutableAmount>' .
            '</InvoiceTotals>';

    // Add invoice items
    $xml .= '<Items>';
    foreach ($this->items as $itemObj) {
      $item = $itemObj->getData();
      $xml .= '<InvoiceLine>' .
                '<ItemDescription>' . $item['name'] . '</ItemDescription>' .
                '<Quantity>' . $this->padTotal($item['quantity']) . '</Quantity>' .
                '<UnitOfMeasure>01</UnitOfMeasure>' .
                '<UnitPriceWithoutTax>' . $this->padItem($item['unitPriceWithoutTax']) . '</UnitPriceWithoutTax>' .
                '<TotalCost>' . $this->padTotalItem($item['totalAmountWithoutTax']) . '</TotalCost>' .
                '<GrossAmount>' . $this->padTotalItem($item['grossAmount']) . '</GrossAmount>';

      // Add item taxes
      // NOTE: As you can see here, taxesWithheld is before taxesOutputs.
      // This is intentional, as most official administrations would mark the
      // invoice as invalid XML if the order is incorrect.
      foreach (["taxesWithheld", "taxesOutputs"] as $taxesGroup) {
        if (count($item[$taxesGroup]) == 0) continue;
        $xmlTag = ucfirst($taxesGroup); // Just capitalize variable name
        $xml .= "<$xmlTag>";
        foreach ($item[$taxesGroup] as $type=>$tax) {
          $xml .= '<Tax>' .
                    '<TaxTypeCode>' . $type . '</TaxTypeCode>' .
                    '<TaxRate>' . $tax['rate'] . '</TaxRate>' .
                    '<TaxableBase>' .
                      '<TotalAmount>' . $this->padTotal($item['totalAmountWithoutTax']) . '</TotalAmount>' .
                    '</TaxableBase>' .
                    '<TaxAmount>' .
                      '<TotalAmount>' . $this->padTotal($tax['amount']) . '</TotalAmount>' .
                    '</TaxAmount>' .
                  '</Tax>';
        }
        $xml .= "</$xmlTag>";
      }

      // Add item additional information
      if (!is_null($item['description'])) {
        $xml .= '<AdditionalLineItemInformation>' . $item['description'] . '</AdditionalLineItemInformation>';
      }
      $xml .= '</InvoiceLine>';
    }
    $xml .= '</Items>';

    // Add payment details
    if (!is_null($this->header['paymentMethod'])) {
      $dueDate = is_null($this->header['dueDate']) ?
        $this->header['issueDate'] :
        $this->header['dueDate'];
      $xml .= '<PaymentDetails>' .
                '<Installment>' .
                  '<InstallmentDueDate>' . date('Y-m-d', $dueDate) . '</InstallmentDueDate>' .
                  '<InstallmentAmount>' . $this->padTotal($totals['invoiceAmount']) . '</InstallmentAmount>' .
                  '<PaymentMeans>' . $this->header['paymentMethod'] . '</PaymentMeans>';
      if ($this->header['paymentMethod'] == self::PAYMENT_TRANSFER) {
        $xml .=   '<AccountToBeCredited>' .
                    '<IBAN>' . $this->header['paymentIBAN'] . '</IBAN>' .
                  '</AccountToBeCredited>';
      }
      $xml .=   '</Installment>' .
              '</PaymentDetails>';
    }

    // Add legal literals
    if (count($this->legalLiterals) > 0) {
      $xml .= '<LegalLiterals>';
      foreach ($this->legalLiterals as $reference) {
        $xml .= '<LegalReference>' . $reference . '</LegalReference>';
      }
      $xml .= '</LegalLiterals>';
    }

    // Close invoice and document
    $xml .= '</Invoice></Invoices></fe:Facturae>';

    // Add signature
    $xml = $this->injectSignature($xml);

    // Prepend content type
    $xml = '<?xml version="1.0" encoding="UTF-8"?>' . "\n" . $xml;

    // Save document
    if (!is_null($filePath)) return file_put_contents($filePath, $xml);
    return $xml;
  }

}

El formato de la factura es incorrecto

Screenshot_7

Estoy realizando pruebas en el entorno de pruebas, el certificado digital que tenemos es válido y no está caducado, lo hemos subido al entorno de pruebas y al querer probar de generar una factura y enviar a Face (staging) nos salta este error:

SimpleXMLElement Object ( [resultado] => SimpleXMLElement Object ( [codigo] => 408 [descripcion] => El formato de la factura es incorrecto [codigoSeguimiento] => 5fc8f2ce360d8 ) [factura] => SimpleXMLElement Object ( ) )

En la imagen adjunta nos aparecen algunos campos conforme no son correctos, pero no sabemos que es lo que tenemos que hacer para que sea corregirlos.

¿Tenéis alguna idea?

Muchas gracias

Aplicar retención a una factura

¿Cómo podría establecerse una retención en una factura?

Por ejemplo, en la factura:

    
<InvoiceTotals>
        <TotalGrossAmount>4.81</TotalGrossAmount>
        <TotalGeneralDiscounts>0.00</TotalGeneralDiscounts>
        <TotalGeneralSurcharges>0.00</TotalGeneralSurcharges>
        <TotalGrossAmountBeforeTaxes>4.81</TotalGrossAmountBeforeTaxes>
        <TotalTaxOutputs>1.01</TotalTaxOutputs>
        <TotalTaxesWithheld>0.00</TotalTaxesWithheld>
        <InvoiceTotal>5.82</InvoiceTotal>
        <TotalFinancialExpenses>0.00</TotalFinancialExpenses>
        <TotalOutstandingAmount>5.82</TotalOutstandingAmount>
        <TotalPaymentsOnAccount>0.00</TotalPaymentsOnAccount>
        <AmountsWithheld>
          <WithholdingReason>Garantía</WithholdingReason>
          <WithholdingAmount>2.50</WithholdingAmount>
        </AmountsWithheld>
        <TotalExecutableAmount>3.32</TotalExecutableAmount>
      </InvoiceTotals>

Los campos para la retención serían y
¿Alguno de los métodos de Facturae-PHP añade esos nodos al XML? ¿o existe algún modo de añadir esos nodos "a mano"?

MIME no válido FACe

Buenas,

 Tal y como se especificó en el issue #45, en Faceb2bClient está especificado que el MIME de la factura sea text/xml, y así me ha estado funcionando todo este tiempo. 
 
 Sin embargo, ayer me empezaron a fallar los envíos de las facturas. Recibo error 311 - El MIME de la factura es incorrecto. Actualmente no tengo acceso al código que firma y envía la factura, solo a la generación del xml que se envía para firmar, y antes de poder solicitar que cambien esa línea por "application/xml", quería preguntar si os está pasando también, o si es algún otro error que no esté encontrando con un texto que no corresponde. 

 En la documentación de FACe, en el uso del servicio web para enviar, aparece en los ejemplos "application/xml", aunque es verdad que es un manual de 2019 y hasta ahora me había funcionado text/xml. ¿Sabéis si se ha cambiado algo? Gracias!

Politica de firma de Facturae

Hola,
veo en el documento de la política de firma de facturae (https://www.facturae.gob.es/politica_de_firma_formato_facturae/politica_de_firma_formato_facturae_v3_1.pdf) que en el caso de usar el formato avanzado con evidencias ante terceros se tiene que usar el perfil XADES-X-L que incorpora el timestamp e información sobre los certificados y su estado de revocación.

En Facturae-PHP solo veo implementado la parte del timestamp, ¿es por quá a nivel practico no es necesaria la parte de información sobre los certificados en la generación de facturas de este tipo?

Consulta de DigestValue

Saludos, una consulta el digest value que está en el sign policy 3_1, que elementos se procesan para obtener ese valor?

Gracias,

Firmar factura con certificado nuevo

Hola,

he renovado el certificado hace poco y actualicé mi versión de Facturae-PHP a la 1.6.0 visto que había cambios en cuanto a los certificados. Sin embargo, desde entonce, FACe me devuelve el siguiente error:
{
'codigo' => '428'
'descripcion' => 'La firma de la factura es incorrecta'
'codigoSeguimiento' => '609cd63302015'
}

¿Qué puede estar pasando?

Como información extra indicar que esto estaba funcionando con la versión 1.5.2 y el certificado anterior, hago uso de un archivo de certificado .p12 y su correspondiente contraseña y adjunto la factura firmada que obtengo y que envío.

¡Gracias de antemano!

EDIT @josemmo: Anonimizada factura
mi-factura-redacted.zip

Factura a ayuntamientos

Hola José,

Te felicito por el proyecto. Me parece que tienes un error en el fichero FacturaeParty.php en la línea 73 al final de la línea es un ";" y no un "." ya concatena lo anterior con lo nuevo y genera un XML inválido.

Muchas gracias,

Propiedad LineItemPeriod no puede ser añadida

Hola,

Estoy tratando de añadir la propiedad "LineItemPeriod" dentro del Item con sus propiedades "StartDate" y "EndDate", he probado a ha añadirlo de diferentes maneras pero no he podido y no he encontrado en la documentación la forma de meterlo.

He visto que la propiedad esta en los schemas.

Ejemplo:

$fac->addItem(new FacturaeItem([
"description" => "Una descripción de hasta 2500 caracteres",
"articleCode" => 1234, // Código de artículo
"fileReference" => "000298172", // Referencia del expediente
"fileDate" => "2010-03-10", // Fecha del expediente
"sequenceNumber" => "1.0", // Número de secuencia o línea del pedido

"LineItemPeriod" => [
"StartDate" => "2010-03-10",
"EndDate" => "2011-03-10"
]
]));

No se si hay alguna otra forma de añadirlo.

Gracias.

MIME type no válido con FACeB2B

Estoy intentando enviar una factura a Faceb2b. Toda la parte de generación y firma de la factura funciona correctamente, pero la parte del envío no. Pongo la parte del código relacionada:

// Ya solo queda firmar la factura ...
$fac->sign(
  "clave_publica.pem",
  "clave_privada.pem",
  "miclavesupersecreta"
);

// ... y exportarlo a un archivo
$fac->export("salida.xsig");


// Cargamos la factura en una instancia de FacturaeFile
$invoice = new FacturaeFile();
$invoice->loadData($fac->export(), "salida.xsig");

// Creamos una conexion con FACeb2b
$face = new Faceb2bClient("clave_publica.pem", "clave_privada.pem", "miclavesecreta");
$face->setProduction(false); // Descomenta esta linea para entorno de desarrollo

// Subimos la factura a FACe

//$res = $face->sendInvoice("[email protected]", $invoice);
$res = $face->sendInvoice($invoice, $invoice);

echo "RESULTADO: " . $res->resultStatus->code . " \nMENSAJE: " . $res->resultStatus->message  . " \nDETALLE: " . $res->resultStatus->detail .  "\n";
//if ($res->resultado->codigo == 0) {
if ($res->resultStatus->code == 0) {
  // La factura ha sido aceptada
  echo "Numero de registro => " . $res->factura->numeroRegistro . "\n";
} else {
  // FACe ha rechazado la factura
//  echo "Factura rechazada " . $res->resultStatus->code . " " .$res->resultStatus->code . " " . $res->resultStatus->messaje . " " . $res->resultStatus->detail . " " ;
}

Y el resultado que obtengo al ejecutarlo:


RESULTADO: 0P001
MENSAJE: Parámetros de entrada inválidos
DETALLE: El parámetro mime de invoiceFile debe ser text/xml
El parámetro mime de attachmentFile debe ser application/zip

Numero de registro =>

Al parecer estoy haciendo algo mal a la hora de pasar los parámetros a sendInvoice, tanto la factura en sí como el adjunto, pero o sé qué

Cambio de namespace

Se abre este issue para discusión sobre el posible cambio del namespace de las facturas planteada por @alphp en #11. Actualmente el namespace se genera de la siguiente forma:

$xmlns = 'xmlns:ds="http://www.w3.org/2000/09/xmldsig#" ' .
  'xmlns:etsi="http://uri.etsi.org/01903/v1.3.2#" ' .
  'xmlns:fe="http://www.facturae.es/Facturae/2014/v' .
  $this->version . '/Facturae"';

Error at Facturae.php file

Hi Josemmo, congratullations for this project

But I am getting this error:
PHP Fatal error: Arrays are not allowed in class constants in /vendor/josemmo/facturae-php/src/Facturae.php on line 32

Referrer to this code:
const SIGN_POLICY_3_1 = array(
"name" => "Política de Firma FacturaE v3.1",
"url" => "http://www.facturae.es/politica_de_firma_formato_facturae/politica_de_firma_formato_facturae_v3_1.pdf",
"digest" => "Ohixl6upD6av8N7pEvDABhEL6hM="
);

Can you help me please?

Tests para Web Services

Creación de tests para los servicios web de FACe y FACeB2B y rediseño del resto de tests.

Versión de la extensión de faceb2b no encontrada o no soportada La unidad receptora no existe

Hola buenas tardes, tengo un problema a la hora de enviar la factura, alguien me puede ayudar?

Mi error es el siguiente:
image

El caso es que no se quien no está registrado, porque me he dado de alta en FaceB2B para poder enviar facturas desde el certificado de la empresa, pero no se, no entiendo bien este proceso. Lo más seguro es que sea de mi código. Esto estaría bien?

image

En "code" tengo que poner el NIF o DNI de cada una de las partes?

Agradecería un pequeño cable o una explicación de este proceso, muchas gracias de antemano, un saludo.
Javi.

Pruebas en STAGING

Hola @josemmo,

Estoy probando la librería con mi certificado registrado en FACe.

En PROD (https://webservice.face.gob.es/ facturasspp2) me funcionan correctamente las consultas, pero en STAGING (https://se-face-webservice.redsara.es/facturasspp2) siempre me devuelve el mismo error

[300] El certificado electrónico no está dado de alta en FACe. Para la presentación automatizada de facturas es necesario registrarse previamente en https://face.gob.es/es/proveedores

¿Hay que utilizar algún certificado distinto para las pruebas?

Muchas gracias.

Centro para Faceb2b

Hola, estoy intentando generear una factura para subirla a Faceb2b. En la documentación pone que para modificar el ejemplo de factura simple y que sirva para Faceb2b se añade el centro así:

$b2b = $fac->getExtension('Fb2b');
$b2b->setReceiver(new FacturaeCentre([
  "code" => "51558103JES0001",
  "name" => "Centro administrativo receptor"
]));

Pero, ¿de dónde saco el código que remplaza a 51558103JES0001? ¿es el código DIRe del cliente al que quiero facturar? ¿o acaso ese valor es fijo porque es el centro receptor de Faceb2b?

Muchas gracias

La librería no me genera el total de la factura.

Buenas, estoy intentando generar el total de la factura (importe bruto, etc) ya que no la genera el solo pero no se como. Me genera lo siguiente:

img

Aquí añado los items:
image

Supongo que será porque no me calcula los porcentajes del IVA.

Gestión del resultado del envío

En el ejemplo de envío de una factura a Face pone:

// Subimos la factura a FACe
$res = $face->sendInvoice("[email protected]", $invoice);
if ($res->resultado->codigo == 0) {
  // La factura ha sido aceptada
  echo "Número de registro => " . $res->factura->numeroRegistro . "\n";
} else {
  // FACe ha rechazado la factura
}

Creo que el acceso a la variable $res devuleta provoca errores, pues en lugar de
$res->resultado->codigo
debería ser
$res->resultStatus->code

Incidencia con Firma

Buenas tardes

Estamos implementando el sistema de facturación electrónica, y nos esta dando un error al enviar la factura

image

Hemos probado a firmar tanto con PEM como con P12 y arroja el mismo error "15 - 428 - La firma de la factura es incorrecta", el mismo certificado (FNMT) se utiliza para firmar a través de FACE sin problemas. el certificado caduca esta semana, no sabemos si cuando se renueve se resolverá el problema o no tiene nada que ver.

Un saludo y gracias

Firmar FacturaeFile

Hola @josemmo,

¿Se puede firmar una factura que el programa recoja de un fichero xml?

$fac = new Facturae();
// Importar factura desde fichero sin firmar
$invoice = new FacturaeFile();
$invoice->loadData($fac->export(), "test-invoice.xsig");
$face = new FaceClient("certificate.pfx", null, "pass");
$res = $face->sendInvoice("[email protected]", $invoice);

Factura sin IVA pero con IRPF

Me he encontrado el caso de que si crear una factura con IRPF pero sin IVA, el validador de facturae devuelve el error:

FacturaE 3.2.1,cvc-complex-type.2.4.a: Invalid content was found starting with element 'TaxesWithheld'. One of '{TaxesOutputs}' is expected.

Entiendo que primero espera encontrar un TaxesOutputs, es decir, un impuesto, y luego una retención. Como no encuentra TaxesOutputs, lanza el error.

¿Cómo estoy generando la factura?

  • Para cada línea de la factura, si tiene IVA le estoy asignando el taxes[Facturae::TAX_IVA] = $linea->iva
  • Para cada línea de la factura, si tiene IRPF le estoy asignando el taxes[Facturae::TAX_IRPF] = $linea->irpf

Es decir, si la línea no tiene IVA, no estoy asignando taxes[Facturae::TAX_IVA] ¿debería ponerlo a cero? He buscado un poco de información sobre este caso pero no he encontrado nada.

código DIR3 ?

Buenos dias,
Por ahora va genial el plugin, pero al mandar una facturae a una administración pública me retornan esto:

Imagen 3

Como se fija este código y cuando? gracias!

Problema con descuentos de cabecera

Buenos días,
El calculo de los impuestos en facturas que llevan descuentos de cabecera no lo esta haciendo correctamente. Parece que no tienen en cuenta la base imponible menos los descuentos de cabecera antes de hacer el calculo de las cantidades de los impuestos.

image

Un saludo,

Validador de extensiones

Con la reciente incorporación de las extensiones a Facturae-PHP surge un nuevo reto: su validación.

¿Qué son las extensiones?

Dicho de una forma muy resumida, las extensiones son bloques de XML que se pueden añadir a un documento de Facturae que se salen de la especificación estándar:

<fe:Facturae>
  <Invoices>
    <Invoice>
      <AdditionalData>
        <Extensions>
          <!-- Aquí van los bloques XML de las extensiones -->
        </Extensions>
      </AdditionalData>
    </Invoice>
  </Invoices>
</fe:Facturae>

Toda extensión tiene que disponer de un XSD asociado que especifique el formato.

¿Por qué implementarlas?

Principalmente por la extensión de FACeB2B, la cual es necesaria para enviar facturas entre empresas del sector privado de forma automatizada. Esta plataforma empezará a operar a finales de junio.

Si necesitas más información sobre FACeB2B, consulta esta página de la documentación.

¿Qué se necesita?

Lamentablemente, ningún validador online es capaz de aceptar facturas que contengan extensiones, ni siquiera el del Gobierno de España. Como puedes imaginar, esto supone un problema a la hora de hacer pruebas.

Creo este issue para proponer herramientas de validación que admitan documentos FacturaE con extensiones, a ser posible online o en su defecto que se puedan ejecutar en Travis (Linux).

Sellado de tiempo

He hecho un "intento" de implementar sellado de tiempo en Facturae-PHP, cuyo código ya está disponible en la rama develop.

Sin embargo, no parece funcionar del todo bien, ya que algunos validadores no aceptan el sellado de tiempo. Por ejemplo, VALIDe devuelve el siguiente error:

Firma inválida
es.gob.afirma.mfirma.exception.SignatureManagerException: Se ha producido un error procesando el firmante con Id [Signature1760457245].

Tras leer el W3C Note sobre XAdES no consigo entender por qué el documento XML generado no es válido.
El objetivo final es, por tanto, lograr que VALIDe acepte las facturas firmadas y con sellado de tiempo.

X509IssuerName error oid 2.5.4.97

organizationIdentifier 2.5.4.97 no se muestra correctamente , al validar con java el xml sale

SEVERE: El formato del nombre de Issuer de un certificado de SigningCertificate no se ajusta a X500: 
improperly specified input name: 
UNDEF=VATES-A66721499,CN=UANATACA CA2 2016,OU=TSP-UANATACA,O=UANATACA S.A.,L=Barcelona,C=ES

En X509IssuerName sale UNDEF=
UNDEF=VATES-A66721499,CN=UANATACA CA2 2016,OU=TSP-UANATACA,O=UANATACA S.A.,L=Barcelona,C=ES
pero deberia ser algo asi
OID.2.5.4.97=VATES-A66721499,CN=UANATACA CA1 2016,OU=TSP-UANATACA,O=UANATACA S.A.,L=Barcelona,C=ES
2.5.4.97=VATES-A66721499,CN=UANATACA CA1 2016,OU=TSP-UANATACA,O=UANATACA S.A.,L=Barcelona,C=ES

Ejemplos de 2.5.4.97:
https://github.com/luisgoncalves/xades4j/blob/855738c009524a721b4e29f1c22dc27ce6a730d5/src/test/java/xades4j/verification/Issue166Test.java#L19-L21

$certIssuer = [];
foreach ($certData['issuer'] as $item=>$value) {
$certIssuer[] = "$item=$value";
}
$certIssuer = implode(',', array_reverse($certIssuer));

Parece un problema de openssl_x509_parse, no es lo ideal, pero talvez corregirlo así por el momento

 $certIssuer = []; 
 foreach ($certData['issuer'] as $item=>$value) { 
   if($item=='UNDEF'){
      if($value=='VATES-A66721499') $item='OID.2.5.4.97';
      else if(in_array($value,['#0c0f56415445532d413636373231343939','#130f56415445532d413636373231343939'])) $item='2.5.4.97';
   }
   $certIssuer[] = "$item=$value"; 
 } 
 $certIssuer = implode(',', array_reverse($certIssuer)); 

Call to a member function getData() on string in Faceb2bClient.php on line 42

Al intentar subir una factura a Faceb2b me da el siguente mensaje de error
PHP Fatal error: Call to a member function getData() on string in /var/www/facturae/Facturae-PHP/src/Face/Faceb2bClient.php on line 42
La línea 42 de la clase Faceb2bClient es:

 $req .= '<invoiceFile>' .
        '<content>' . $tools->toBase64($invoice->getData()) . '</content>' .
        '<name>' . $invoice->getFilename() . '</name>' .
        '<mime>' . $invoice->getMimeType() . '</mime>' .
      '</invoiceFile>';

Posibilidad de añadir impuestos retenidos

Creo que el impuesto IRPF debería ir en impuestos retenidos (TaxesWithheld).

O en caso contrario sería interesante poder indicar si un impuesto es repercutido (TaxesOutputs) o retenido (TaxesWithheld).

Código SWIFT

Hola José Miguel,

Enhorabuena por el proyecto. Algunas entidades solicitan que se indique el código SWIFT o BIC junto al IBAN. ¿Está previsto incluirlo? Si lo prefieres, envío PR y lo valoras.

Gracias y Feliz Año Nuevo!

Facturas a organismos públicos

Hola @josemmo,
primero de todo, gracias por tu trabajo!

He visto que cuando se genera el XML referente a la parte de los centros administrativos de un organismo público, no se está teniendo en cuenta los campos de la dirección del centro (address, postcode, town, province y countrycode), aunque el centro te permite especificar una información distinta para cada uno. Esto se encuentra en la función getXML de la clase FacturaeParty.php.

Muchas gracias!

cvc enumeration valid error

Buenos dias,
Estaba mirando de validar una facturae que mandé a:
http://sedeaplicaciones2.minetur.gob.es/FacturaE/index.jsp

y me salta este error:
FacturaE 3.2.1,cvc-enumeration-valid: Value '4' is not facet-valid with respect to enumeration '[01, 02, 03, 04, 05, 06, 07, 08, 09, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]'. It must be a value from the enumeration.

No se que campo podría ser el causante y como subsanarlo, alguna idea? gracias mil!

xades-epes

Hola, la clase utiliza xades-epes para la firma de los xml ?

¿Hay algún ejemplo de factura con Recargo Equiv. + Retención + IVA + Descuento?

Hola!

Estoy probando la librería y he conseguido crear algunas facturas "sencillas" (con IVA, IVA+Retención, etc.) pero no consigo crear una factura que tenga IVA + Recargo de Equivalencia + Retención + Descuento por línea.

Es decir, la factura se genera, pero los importes, totales, etc. no coinciden con los de la factura original.

Este es el código que estoy usando para añadir una línea (item):

$fac->addItem(new FacturaeItem([ "name" => $descr, "articleCode" => $codigo, "quantity" => $cantidad, "unitPriceWithoutTax" => $precio, "discounts" => array( ["reason" => "Descuento de Línea", "rate" => $dto] ), "charges" => array( ["reason" => "Recargo Equiv.", "rate" => $re, "hasTaxes" => false] ), "taxes" => array( Facturae::TAX_IVA => $impuesto, Facturae::TAX_IRPF => $ret ) ]));

También he probado a añadir el Recargo Equiv. como "taxes" y no como "charges" con el mismo resultado. ¿Hay algún truco que se deba tener en cuenta cuando se usan todos estos impuestos?

Gracias

descuento por linea de detalle

Muy buenas Jose, ¿tienes algo desarrollado por el descuento por linea en los detalles de las facturas?

Gracias ante todo.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.