Skip to content

Commit

Permalink
Merge pull request #125 from josemmo/develop
Browse files Browse the repository at this point in the history
v1.7.6
  • Loading branch information
josemmo authored Mar 25, 2023
2 parents b864968 + 64ae4b7 commit fb876dd
Show file tree
Hide file tree
Showing 11 changed files with 201 additions and 33 deletions.
19 changes: 19 additions & 0 deletions doc/anexos/constantes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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|
Expand Down Expand Up @@ -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|
Expand Down
3 changes: 2 additions & 1 deletion doc/ejemplos/sin-composer.md
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
26 changes: 26 additions & 0 deletions doc/entidades/terceros.md
Original file line number Diff line number Diff line change
@@ -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" => "[email protected]"
]));
```

El tipo de emisor de una factura cambiará automáticamente a `Facturae::ISSUER_THIRD_PARTY` al establecer los datos de un tercero.
19 changes: 19 additions & 0 deletions doc/productos/impuestos.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
]));
```
6 changes: 5 additions & 1 deletion src/Facturae.php
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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;

Expand Down
6 changes: 6 additions & 0 deletions src/FacturaeItem.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down
8 changes: 4 additions & 4 deletions src/FacturaeParty.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '<TaxIdentification>' .
'<PersonTypeCode>' . ($this->isLegalEntity ? 'J' : 'F') . '</PersonTypeCode>' .
Expand All @@ -73,7 +73,7 @@ public function getXML($schema) {
'</TaxIdentification>';

// Add administrative centres
if (count($this->centres) > 0) {
if ($includeAdministrativeCentres && count($this->centres) > 0) {
$xml .= '<AdministrativeCentres>';
foreach ($this->centres as $centre) {
$xml .= '<AdministrativeCentre>';
Expand Down
57 changes: 32 additions & 25 deletions src/FacturaeTraits/ExportableTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,29 +49,32 @@ public function export($filePath=null) {

// 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->pad($totals['invoiceAmount'], 'InvoiceTotal') . '</TotalAmount>' .
'</TotalInvoicesAmount>' .
'<TotalOutstandingAmount>' .
'<TotalAmount>' . $this->pad($totals['totalOutstandingAmount'], 'InvoiceTotal') . '</TotalAmount>' .
'</TotalOutstandingAmount>' .
'<TotalExecutableAmount>' .
'<TotalAmount>' . $this->pad($totals['totalExecutableAmount'], 'InvoiceTotal') . '</TotalAmount>' .
'</TotalExecutableAmount>' .
'<InvoiceCurrencyCode>' . $this->currency . '</InvoiceCurrencyCode>' .
'</Batch>';
$xml .= '<FileHeader>';
$xml .= '<SchemaVersion>' . $this->version .'</SchemaVersion>';
$xml .= '<Modality>I</Modality>';
$xml .= '<InvoiceIssuerType>' . $this->header['issuerType'] . '</InvoiceIssuerType>';
if (!is_null($this->parties['thirdParty'])) {
$xml .= '<ThirdParty>' . $this->parties['thirdParty']->getXML(false) . '</ThirdParty>';
}
$xml .= '<Batch>' .
'<BatchIdentifier>' . $batchIdentifier . '</BatchIdentifier>' .
'<InvoicesCount>1</InvoicesCount>' .
'<TotalInvoicesAmount>' .
'<TotalAmount>' . $this->pad($totals['invoiceAmount'], 'InvoiceTotal') . '</TotalAmount>' .
'</TotalInvoicesAmount>' .
'<TotalOutstandingAmount>' .
'<TotalAmount>' . $this->pad($totals['totalOutstandingAmount'], 'InvoiceTotal') . '</TotalAmount>' .
'</TotalOutstandingAmount>' .
'<TotalExecutableAmount>' .
'<TotalAmount>' . $this->pad($totals['totalExecutableAmount'], 'InvoiceTotal') . '</TotalAmount>' .
'</TotalExecutableAmount>' .
'<InvoiceCurrencyCode>' . $this->currency . '</InvoiceCurrencyCode>' .
'</Batch>';

// Add factoring assignment data
if (!is_null($this->parties['assignee'])) {
$xml .= '<FactoringAssignmentData>';
$xml .= '<Assignee>' . $this->parties['assignee']->getXML($this->version) . '</Assignee>';
$xml .= '<Assignee>' . $this->parties['assignee']->getXML(false) . '</Assignee>';
$xml .= $paymentDetailsXML;
if (!is_null($this->header['assignmentClauses'])) {
$xml .= '<FactoringAssignmentClauses>' .
Expand All @@ -86,8 +89,8 @@ public function export($filePath=null) {

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

// Add invoice data
Expand Down Expand Up @@ -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 .= '<SpecialTaxableEvent>';
$xml .= '<SpecialTaxableEventCode>' . XmlTools::escape($item['specialTaxableEventCode']) . '</SpecialTaxableEventCode>';
$xml .= '<SpecialTaxableEventReason>' . XmlTools::escape($item['specialTaxableEventReason']) . '</SpecialTaxableEventReason>';
$xml .= '</SpecialTaxableEvent>';
}
$xml .= $this->addOptionalFields($item, ["articleCode"]);

// Close invoice line
$xml .= '</InvoiceLine>';
Expand Down
43 changes: 43 additions & 0 deletions src/FacturaeTraits/PropertiesTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -33,6 +34,7 @@ trait PropertiesTrait {
"additionalInformation" => null
);
protected $parties = array(
"thirdParty" => null,
"assignee" => null,
"seller" => null,
"buyer" => null
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
25 changes: 24 additions & 1 deletion tests/ExtensionsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,37 @@ public function testExtensions() {
$extXml = explode('</Extensions>', $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, '<LegalReference>' . $disclaimer->getDisclaimer() . '</LegalReference>');
$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;
}
}
22 changes: 21 additions & 1 deletion tests/InvoiceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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([
Expand Down Expand Up @@ -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" => "[email protected]"
]));
$this->assertEquals(Facturae::ISSUER_THIRD_PARTY, $fac->getIssuerType());
$fac->setAssignee(new FacturaeParty([
"taxNumber" => "B00000000",
"name" => "Cesionario S.L.",
Expand Down

0 comments on commit fb876dd

Please sign in to comment.