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.",