josemmo / facturae-php Goto Github PK
View Code? Open in Web Editor NEW📝 Genera, firma, envía y recibe facturas electrónicas sin necesidad de ninguna librería adicional
Home Page: https://josemmo.github.io/Facturae-PHP/
License: MIT License
📝 Genera, firma, envía y recibe facturas electrónicas sin necesidad de ninguna librería adicional
Home Page: https://josemmo.github.io/Facturae-PHP/
License: MIT License
Con la reciente incorporación de las extensiones a Facturae-PHP surge un nuevo reto: su validación.
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.
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.
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).
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!
Hola José,
parece que el calculo de la base imponible, y por consecuencia del IVA y del total de la factura, no tiene en cuenta eventuales descuentos que hemos aplicado a la linea individual.
Saludos
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"';
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.
It will be nice to connect your project with this invoice application.
Hello,
How can I generate certificate to sign invoice?
Regards
¿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"?
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
Facturae-PHP/src/FacturaeTraits/SignableTrait.php
Lines 90 to 94 in 3f4885d
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));
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
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!
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é
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
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();
Como se observa el iva no se calcula del total sin impuestos sino de la base.
Gracias anticipadas
Hola, tengo resuelto el tema de subir las facturas a FACe:
https://github.com/fawno/Facturae
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.
Muy buenas Jose, ¿tienes algo desarrollado por el descuento por linea en los detalles de las facturas?
Gracias ante todo.
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!
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.
<?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;
}
}
Seguramente sería buena idea publicar la clase como un paquete de composer (https://getcomposer.org/) para facilitar su uso y distribución ya que es la forma más común de publicar librerías para php.
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.
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
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,
Validacion.pdf
Me aparece ese error al validar el fichero que he generado, ¿a que es posible?
Gracias y un saludo.
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).
Buenas tardes
Estamos implementando el sistema de facturación electrónica, y nos esta dando un error al enviar la factura
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
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>';
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?
Creación de tests para los servicios web de FACe y FACeB2B y rediseño del resto de tests.
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.
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:
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 ;-)
Hola @josemmo,
Muchas gracias por esta libería tan útil y necesaria!
Estoy intentando generar facturas con cesión de crédito, y me pregunto si hay alguna forma de hacerlo con este librería. Según las FAQs del FACe (https://lalin.gal/files/FACe%20Preguntas%20frecuentes%20Proveedores%201-2-3.pdf punto 2.8) esto es compatible con el FACe en versión 3.2.2 de Facturae.
Gracias!
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';
Hi Josemmo, thks for sharing you package. I'am trying your class but y have Error 104
Url: https://se-face-webservice.redsara.es/facturasspp2?wsdl
Object:
Array
(
[correo] => [email protected]
[factura] => Array
(
[factura] => PD94bW[...]0dXJhZT4=
[nombre] => facturae_F1801019.xsig
[mime] => text/plain
)
)
Código Error 104, La petición SOAP no está bien construida: no se encuentra el SOAP Header
Could you say me something?
Thanks in avance
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
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!
Hola, la clase utiliza xades-epes para la firma de los xml ?
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?
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.
Saludos, una consulta el digest value que está en el sign policy 3_1, que elementos se procesan para obtener ese valor?
Gracias,
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
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
Hola buenas tardes, tengo un problema a la hora de enviar la factura, alguien me puede ayudar?
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?
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.
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?
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
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.
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);
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.