diff --git a/doc/anexos/constantes.md b/doc/anexos/constantes.md
index 2288f34..7a26bac 100644
--- a/doc/anexos/constantes.md
+++ b/doc/anexos/constantes.md
@@ -26,6 +26,16 @@ permalink: /anexos/constantes.html
---
+## Tipos de emisor
+
+|Constante|Descripción|
+|--------:|:----------|
+|`Facturae::ISSUER_SELLER`|Proveedor (emisor)|
+|`Facturae::ISSUER_BUYER`|Destinatario (receptor)|
+|`Facturae::ISSUER_THIRD_PARTY`|Tercero|
+
+---
+
## Modos de precisión
|Constante|Descripción|
@@ -108,6 +118,15 @@ permalink: /anexos/constantes.html
---
+## Códigos de fiscalidad especial
+
+|Constante|Descripción|
+|--------:|:----------|
+|`FacturaeItem::SPECIAL_TAXABLE_EVENT_EXEMPT`|Operación sujeta y exenta|
+|`FacturaeItem::SPECIAL_TAXABLE_EVENT_NON_SUBJECT`|Operación no sujeta|
+
+---
+
## Unidades de medida
|Constante|Descripción|
diff --git a/doc/ejemplos/sin-composer.md b/doc/ejemplos/sin-composer.md
index 26fb0b3..15f7680 100644
--- a/doc/ejemplos/sin-composer.md
+++ b/doc/ejemplos/sin-composer.md
@@ -9,7 +9,8 @@ permalink: /ejemplos/sin-composer.html
Este ejemplo muestra cómo usar `Facturae-PHP` sin tener configurado un entorno de Composer, solo descargando el código fuente de la librería.
```php
-require_once 'ruta/hacia/Facturae-PHP/src/Common/KeyPairReader.php';
+require_once 'ruta/hacia/Facturae-PHP/src/Common/FacturaeSigner.php';
+require_once 'ruta/hacia/Facturae-PHP/src/Common/KeyPairReaderTrait.php';
require_once 'ruta/hacia/Facturae-PHP/src/Common/XmlTools.php';
require_once 'ruta/hacia/Facturae-PHP/src/FacturaeTraits/PropertiesTrait.php';
require_once 'ruta/hacia/Facturae-PHP/src/FacturaeTraits/UtilsTrait.php';
diff --git a/doc/entidades/terceros.md b/doc/entidades/terceros.md
new file mode 100644
index 0000000..7f94209
--- /dev/null
+++ b/doc/entidades/terceros.md
@@ -0,0 +1,26 @@
+---
+title: Terceros
+parent: Entidades
+nav_order: 4
+permalink: /entidades/terceros.html
+---
+
+# Terceros
+Un tercero o *Third-Party* es la entidad que genera y firma una factura cuando esta no coincide con el emisor.
+Por ejemplo, en el caso de una gestoría que trabaja con varios clientes y emite las facturas en su nombre.
+
+En el caso de Facturae-PHP, pueden especificarse los datos de un tercero de la siguiente forma:
+```php
+$fac->setThirdParty(new FacturaeParty([
+ "taxNumber" => "B99999999",
+ "name" => "Gestoría de Ejemplo, S.L.",
+ "address" => "C/ de la Gestoría, 24",
+ "postCode" => "23456",
+ "town" => "Madrid",
+ "province" => "Madrid",
+ "phone" => "915555555",
+ "email" => "noexiste@gestoria.com"
+]));
+```
+
+El tipo de emisor de una factura cambiará automáticamente a `Facturae::ISSUER_THIRD_PARTY` al establecer los datos de un tercero.
diff --git a/doc/productos/impuestos.md b/doc/productos/impuestos.md
index 43773b0..66c890a 100644
--- a/doc/productos/impuestos.md
+++ b/doc/productos/impuestos.md
@@ -66,3 +66,22 @@ $fac->addItem(new FacturaeItem([
]
]));
```
+
+## Fiscalidad especial
+Algunas operaciones son subjetivas de una fiscalidad especial (*special taxable event* en inglés). Por ejemplo, determinados productos se ven exentos de impuestos.
+Habitualmente, la forma en la que se declaran estos casos es marcando la línea de producto con IVA al 0% y especificando la justificación de la fiscalidad especial:
+```php
+$fac->addItem(new FacturaeItem([
+ "name" => "Un producto exento de IVA",
+ "unitPrice" => 100,
+
+ // Se marca la línea con IVA 0%
+ "taxes" => [Facturae::TAX_IVA => 0],
+
+ // Se declara el producto como exento de IVA
+ "specialTaxableEventCode" => FacturaeItem::SPECIAL_TAXABLE_EVENT_EXEMPT,
+
+ // Se detalla el motivo
+ "specialTaxableEventReason" => "El motivo detallado de la exención de impuestos"
+]));
+```
diff --git a/src/Facturae.php b/src/Facturae.php
index 52bc37d..e3e5cef 100644
--- a/src/Facturae.php
+++ b/src/Facturae.php
@@ -10,7 +10,7 @@
* Class for creating electronic invoices that comply with the Spanish FacturaE format.
*/
class Facturae {
- const VERSION = "1.7.5";
+ const VERSION = "1.7.6";
const USER_AGENT = "FacturaePHP/" . self::VERSION;
const SCHEMA_3_2 = "3.2";
@@ -26,6 +26,10 @@ class Facturae {
const INVOICE_FULL = "FC";
const INVOICE_SIMPLIFIED = "FA";
+ const ISSUER_SELLER = "EM";
+ const ISSUER_BUYER = "RE";
+ const ISSUER_THIRD_PARTY = "TE";
+
const PRECISION_LINE = 1;
const PRECISION_INVOICE = 2;
diff --git a/src/FacturaeItem.php b/src/FacturaeItem.php
index e64a222..4739d75 100644
--- a/src/FacturaeItem.php
+++ b/src/FacturaeItem.php
@@ -7,6 +7,10 @@
* Represents an invoice item
*/
class FacturaeItem {
+ /** Subject and exempt operation */
+ const SPECIAL_TAXABLE_EVENT_EXEMPT = "01";
+ /** Non-subject operation */
+ const SPECIAL_TAXABLE_EVENT_NON_SUBJECT = "02";
private $articleCode = null;
private $name = null;
@@ -19,6 +23,8 @@ class FacturaeItem {
private $charges = array();
private $taxesOutputs = array();
private $taxesWithheld = array();
+ private $specialTaxableEventCode = null;
+ private $specialTaxableEventReason = null;
private $issuerContractReference = null;
private $issuerContractDate = null;
diff --git a/src/FacturaeParty.php b/src/FacturaeParty.php
index f318e6d..d0eff4e 100644
--- a/src/FacturaeParty.php
+++ b/src/FacturaeParty.php
@@ -61,10 +61,10 @@ public function __construct($properties=array()) {
/**
* Get XML
*
- * @param string $schema Facturae schema version
- * @return string Entity as Facturae XML
+ * @param boolean $includeAdministrativeCentres Whether to include administrative centers or not
+ * @return string Entity as Facturae XML
*/
- public function getXML($schema) {
+ public function getXML($includeAdministrativeCentres) {
// Add tax identification
$xml = '' .
'' . ($this->isLegalEntity ? 'J' : 'F') . '' .
@@ -73,7 +73,7 @@ public function getXML($schema) {
'';
// Add administrative centres
- if (count($this->centres) > 0) {
+ if ($includeAdministrativeCentres && count($this->centres) > 0) {
$xml .= '';
foreach ($this->centres as $centre) {
$xml .= '';
diff --git a/src/FacturaeTraits/ExportableTrait.php b/src/FacturaeTraits/ExportableTrait.php
index e0d9dc2..6b7c4d3 100644
--- a/src/FacturaeTraits/ExportableTrait.php
+++ b/src/FacturaeTraits/ExportableTrait.php
@@ -49,29 +49,32 @@ public function export($filePath=null) {
// Add header
$batchIdentifier = $this->parties['seller']->taxNumber . $this->header['number'] . $this->header['serie'];
- $xml .= '' .
- '' . $this->version .'' .
- 'I' .
- 'EM' .
- '' .
- '' . $batchIdentifier . '' .
- '1' .
- '' .
- '' . $this->pad($totals['invoiceAmount'], 'InvoiceTotal') . '' .
- '' .
- '' .
- '' . $this->pad($totals['totalOutstandingAmount'], 'InvoiceTotal') . '' .
- '' .
- '' .
- '' . $this->pad($totals['totalExecutableAmount'], 'InvoiceTotal') . '' .
- '' .
- '' . $this->currency . '' .
- '';
+ $xml .= '';
+ $xml .= '' . $this->version .'';
+ $xml .= 'I';
+ $xml .= '' . $this->header['issuerType'] . '';
+ if (!is_null($this->parties['thirdParty'])) {
+ $xml .= '' . $this->parties['thirdParty']->getXML(false) . '';
+ }
+ $xml .= '' .
+ '' . $batchIdentifier . '' .
+ '1' .
+ '' .
+ '' . $this->pad($totals['invoiceAmount'], 'InvoiceTotal') . '' .
+ '' .
+ '' .
+ '' . $this->pad($totals['totalOutstandingAmount'], 'InvoiceTotal') . '' .
+ '' .
+ '' .
+ '' . $this->pad($totals['totalExecutableAmount'], 'InvoiceTotal') . '' .
+ '' .
+ '' . $this->currency . '' .
+ '';
// Add factoring assignment data
if (!is_null($this->parties['assignee'])) {
$xml .= '';
- $xml .= '' . $this->parties['assignee']->getXML($this->version) . '';
+ $xml .= '' . $this->parties['assignee']->getXML(false) . '';
$xml .= $paymentDetailsXML;
if (!is_null($this->header['assignmentClauses'])) {
$xml .= '' .
@@ -86,8 +89,8 @@ public function export($filePath=null) {
// Add parties
$xml .= '' .
- '' . $this->parties['seller']->getXML($this->version) . '' .
- '' . $this->parties['buyer']->getXML($this->version) . '' .
+ '' . $this->parties['seller']->getXML(true) . '' .
+ '' . $this->parties['buyer']->getXML(true) . '' .
'';
// Add invoice data
@@ -327,10 +330,14 @@ public function export($filePath=null) {
}
// Add more optional fields
- $xml .= $this->addOptionalFields($item, [
- "description" => "AdditionalLineItemInformation",
- "articleCode"
- ]);
+ $xml .= $this->addOptionalFields($item, ["description" => "AdditionalLineItemInformation"]);
+ if (!is_null($item['specialTaxableEventCode']) && !is_null($item['specialTaxableEventReason'])) {
+ $xml .= '';
+ $xml .= '' . XmlTools::escape($item['specialTaxableEventCode']) . '';
+ $xml .= '' . XmlTools::escape($item['specialTaxableEventReason']) . '';
+ $xml .= '';
+ }
+ $xml .= $this->addOptionalFields($item, ["articleCode"]);
// Close invoice line
$xml .= '';
diff --git a/src/FacturaeTraits/PropertiesTrait.php b/src/FacturaeTraits/PropertiesTrait.php
index 6906a99..d1f566b 100644
--- a/src/FacturaeTraits/PropertiesTrait.php
+++ b/src/FacturaeTraits/PropertiesTrait.php
@@ -19,6 +19,7 @@ trait PropertiesTrait {
protected $precision = self::PRECISION_LINE;
protected $header = array(
"type" => self::INVOICE_FULL,
+ "issuerType" => self::ISSUER_SELLER,
"serie" => null,
"number" => null,
"issueDate" => null,
@@ -33,6 +34,7 @@ trait PropertiesTrait {
"additionalInformation" => null
);
protected $parties = array(
+ "thirdParty" => null,
"assignee" => null,
"seller" => null,
"buyer" => null
@@ -99,6 +101,27 @@ public function setPrecision($precision) {
}
+ /**
+ * Set third party
+ * @param FacturaeParty $assignee Third party information
+ * @return Facturae Invoice instance
+ */
+ public function setThirdParty($thirdParty) {
+ $this->parties['thirdParty'] = $thirdParty;
+ $this->setIssuerType(self::ISSUER_THIRD_PARTY);
+ return $this;
+ }
+
+
+ /**
+ * Get third party
+ * @return FacturaeParty|null Third party information
+ */
+ public function getThirdParty() {
+ return $this->parties['thirdParty'];
+ }
+
+
/**
* Set assignee
* @param FacturaeParty $assignee Assignee information
@@ -219,6 +242,26 @@ public function getType() {
}
+ /**
+ * Set issuer type
+ * @param string $issuerType Issuer type
+ * @return Facturae Invoice instance
+ */
+ public function setIssuerType($issuerType) {
+ $this->header['issuerType'] = $issuerType;
+ return $this;
+ }
+
+
+ /**
+ * Get issuer type
+ * @return string Issuer type
+ */
+ public function getIssuerType() {
+ return $this->header['issuerType'];
+ }
+
+
/**
* Set invoice number
* @param string $serie Serie code of the invoice
diff --git a/tests/ExtensionsTest.php b/tests/ExtensionsTest.php
index 009942c..ed228e5 100644
--- a/tests/ExtensionsTest.php
+++ b/tests/ExtensionsTest.php
@@ -69,14 +69,37 @@ public function testExtensions() {
$extXml = explode('', $extXml[1])[0];
// Validamos la parte de FACeB2B
+ $schemaPath = $this->getSchema();
$faceXml = new \DOMDocument();
$faceXml->loadXML($extXml);
- $isValidXml = $faceXml->schemaValidate(self::FB2B_XSD_PATH);
+ $isValidXml = $faceXml->schemaValidate($schemaPath);
$this->assertTrue($isValidXml);
+ unlink($schemaPath);
// Validamos la ejecución de DisclaimerExtension
$disclaimerPos = strpos($rawXml, '' . $disclaimer->getDisclaimer() . '');
$this->assertTrue($disclaimerPos !== false);
}
+ /**
+ * Get path to FaceB2B schema file
+ * @return string Path to schema file
+ */
+ private function getSchema() {
+ // Get XSD contents
+ $ch = curl_init();
+ curl_setopt($ch, CURLOPT_URL, self::FB2B_XSD_PATH);
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
+ curl_setopt($ch, CURLOPT_COOKIEFILE, '');
+ $res = curl_exec($ch);
+ curl_close($ch);
+ unset($ch);
+
+ // Save to disk
+ $path = self::OUTPUT_DIR . "/faceb2b.xsd";
+ file_put_contents($path, $res);
+
+ return $path;
+ }
}
diff --git a/tests/InvoiceTest.php b/tests/InvoiceTest.php
index 2af3fa5..2c31c5d 100644
--- a/tests/InvoiceTest.php
+++ b/tests/InvoiceTest.php
@@ -116,6 +116,15 @@ public function testCreateInvoice($schemaVersion, $isPfx) {
// Y ahora, una línea con IVA al 0%
$fac->addItem("Algo exento de IVA", 100, 1, Facturae::TAX_IVA, 0);
+ // Otra línea con IVA 0% y código de fiscalidad especial
+ $fac->addItem(new FacturaeItem([
+ "name" => "Otro algo exento de IVA",
+ "unitPrice" => 50,
+ "taxes" => [Facturae::TAX_IVA => 0],
+ "specialTaxableEventCode" => FacturaeItem::SPECIAL_TAXABLE_EVENT_EXEMPT,
+ "specialTaxableEventReason" => "El motivo detallado de la exención de impuestos"
+ ]));
+
// Vamos a añadir un producto utilizando la API avanzada
// que tenga IVA al 10%, IRPF al 15%, descuento del 10% y recargo del 5%
$fac->addItem(new FacturaeItem([
@@ -208,8 +217,19 @@ public function testCreateInvoice($schemaVersion, $isPfx) {
"amount" => 99.9991172
]));
- // Establecemos un un cesionario (solo en algunos casos)
+ // Establecemos un tercero y un cesionario (solo en algunos casos)
if ($isPfx) {
+ $fac->setThirdParty(new FacturaeParty([
+ "taxNumber" => "B99999999",
+ "name" => "Gestoría de Ejemplo, S.L.",
+ "address" => "C/ de la Gestoría, 24",
+ "postCode" => "23456",
+ "town" => "Madrid",
+ "province" => "Madrid",
+ "phone" => "915555555",
+ "email" => "noexiste@gestoria.com"
+ ]));
+ $this->assertEquals(Facturae::ISSUER_THIRD_PARTY, $fac->getIssuerType());
$fac->setAssignee(new FacturaeParty([
"taxNumber" => "B00000000",
"name" => "Cesionario S.L.",