Skip to content

Commit

Permalink
Merge pull request #16 from josemmo/develop
Browse files Browse the repository at this point in the history
v1.1.0
  • Loading branch information
josemmo authored Jan 4, 2018
2 parents 65c3023 + d8925e4 commit 4234d38
Show file tree
Hide file tree
Showing 9 changed files with 342 additions and 234 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
/vendor/
tests/salida.xsig
tests/salida-*.xsig
tests/cookies.txt
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2017 josemmo
Copyright (c) 2018 José M. Moreno <josemmo@protonmail.com> (https://github.com/josemmo)

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
16 changes: 12 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ Facturae-PHP es una clase escrita puramente en PHP que permite generar facturas

### Características
- [x] Generación de facturas 100% conformes con la [Ley 25/2013 del 27 de diciembre](https://www.boe.es/diario_boe/txt.php?id=BOE-A-2013-13722) listas para enviar a FACe
- [x] Exportación según el [formato Facturae 3.2.1](http://www.facturae.gob.es/formato/Paginas/version-3-2.aspx)
- [x] Exportación según las versiones [3.2, 3.2.1 y 3.2.2](http://www.facturae.gob.es/formato/Paginas/version-3-2.aspx) de Facturae
- [x] Firmado de acuerdo a la [política de firma de Facturae 3.1](http://www.facturae.gob.es/formato/Paginas/politicas-firma-electronica.aspx) basada en XAdES

### Funciones previstas
- [ ] Compatibilidad con el formato Facturae 3.2.2
- [ ] Firma con sellado de tiempo (TSA)
- [ ] Envío de facturas a FACe directamente desde la clase

---------------------------------------------------------
Expand Down Expand Up @@ -104,6 +104,12 @@ $fac->export("ruta/de/salida.xsig");
> require_once "../src/FacturaeItem.php";
> ```
### Versión de Facturae
Por defecto el paquete creará la factura siguiendo el formato Facturae 3.2.1 al ser actualmente el más extendido. Si se quisiera utilizar otra versión se deberá indicar al instanciar el objeto de la factura:
```php
$fac = new Facturae(Facturae::SCHEMA_3_2_2);
```
### Compradores y vendedores
Los compradores y vendedores son representados en Facturae-PHP con la clase `FacturaeParty` y pueden contener los siguientes atributos:
```php
Expand Down Expand Up @@ -345,7 +351,7 @@ $fac->addLegalLiteral("Y este, otro más");
```

> ##### NOTA
> El uso de `LegalLiterals` es obligatorio en determinadas facturas. Cosulta la legislación vigente para más información.
> El uso de `LegalLiterals` es obligatorio en determinadas facturas. Consulta la legislación vigente para más información.
#### Totales de la factura

Expand All @@ -370,6 +376,7 @@ $totales = $fac->getTotals();

|Constante|Descripción|
|--------:|:----------|
|`Facturae::SCHEMA_3_2`|Formato de Facturae 3.2|
|`Facturae::SCHEMA_3_2_1`|Formato de Facturae 3.2.1|
|`Facturae::SCHEMA_3_2_2`|Formato de Facturae 3.2.2|
|`Facturae::SIGN_POLICY_3_1`|Formato de firma 3.1|
Expand Down Expand Up @@ -405,6 +412,7 @@ $totales = $fac->getTotals();
### Herramientas de validación
Todas las facturas generadas y firmadas con Facturae-PHP son probadas con las siguientes herramientas online para garantizar el cumplimiento del estándar:

- https://viewer.facturadirecta.com/
- https://viewer.facturadirecta.com/ (no soporta 3.2.2)
- http://plataforma.firma-e.com/VisualizadorFacturae/ (no soporta 3.2.2)
- http://sedeaplicaciones2.minetur.gob.es/FacturaE/index.jsp
- https://face.gob.es/es/facturas/validar-visualizar-facturas
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"authors": [
{
"name": "josemmo",
"email": "[email protected]"
"email": "[email protected]",
"homepage": "https://github.com/josemmo"
}
],
"require": {
Expand Down
401 changes: 233 additions & 168 deletions src/Facturae.php

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion src/FacturaeCentre.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,11 @@ class FacturaeCentre {

/**
* Construct
*
* @param array $properties Party properties as an array
*/
public function __construct($properties=array()) {
foreach ($properties as $key=>$value) $this->{$key} = $value;
}

}
}
1 change: 1 addition & 0 deletions src/FacturaeItem.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class FacturaeItem {

/**
* Construct
*
* @param array $properties Item properties as an array
*/
public function __construct($properties=array()) {
Expand Down
97 changes: 54 additions & 43 deletions src/FacturaeParty.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ class FacturaeParty {

/**
* Construct
*
* @param array $properties Party properties as an array
*/
public function __construct($properties=array()) {
Expand All @@ -53,76 +54,86 @@ public function __construct($properties=array()) {

/**
* Get XML
*
* @param string $schema Facturae schema version
* @return string Entity as Facturae XML
*/
public function getXML($schema) {
// Add tax identification
$xml = '<TaxIdentification><PersonTypeCode>' .
($this->isLegalEntity ? 'J' : 'F') . '</PersonTypeCode>' .
'<ResidenceTypeCode>R</ResidenceTypeCode><TaxIdentificationNumber>' .
$this->taxNumber . '</TaxIdentificationNumber></TaxIdentification>';
$xml = '<TaxIdentification>' .
'<PersonTypeCode>' . ($this->isLegalEntity ? 'J' : 'F') . '</PersonTypeCode>' .
'<ResidenceTypeCode>R</ResidenceTypeCode>' .
'<TaxIdentificationNumber>' . $this->taxNumber . '</TaxIdentificationNumber>' .
'</TaxIdentification>';

// Add administrative centres
if (count($this->centres) > 0) {
$xml .= '<AdministrativeCentres>';
foreach ($this->centres as $centre) {
$xml .= '<AdministrativeCentre><CentreCode>' . $centre->code .
'</CentreCode><RoleTypeCode>' . $centre->role . '</RoleTypeCode>' .
'<Name>' . $centre->name . '</Name>';
if (!is_null($centre->firstSurname)) $xml .= '<FirstSurname>' .
$centre->firstSurname . '</FirstSurname>';
if (!is_null($centre->lastSurname)) $xml .= '<SecondSurname>' .
$centre->lastSurname . '</SecondSurname>';
$xml .= '<AddressInSpain><Address>' . $this->address . '</Address>' .
'<PostCode>' . $this->postCode . '</PostCode><Town>' . $this->town .
'</Town><Province>' . $this->province . '</Province><CountryCode>' .
$this->countryCode . '</CountryCode></AddressInSpain>';
if (!is_null($centre->description)) $xml .= '<CentreDescription>' .
$centre->description . '</CentreDescription>';
$xml .= '<AdministrativeCentre>' .
$xml .= '<CentreCode>' . $centre->code . '</CentreCode>';
$xml .= '<RoleTypeCode>' . $centre->role . '</RoleTypeCode>';
$xml .= '<Name>' . $centre->name . '</Name>';
if (!is_null($centre->firstSurname)) {
$xml .= '<FirstSurname>' . $centre->firstSurname . '</FirstSurname>';
}
if (!is_null($centre->lastSurname)) {
$xml .= '<SecondSurname>' . $centre->lastSurname . '</SecondSurname>';
}
$xml .= '<AddressInSpain>' .
'<Address>' . $this->address . '</Address>' .
'<PostCode>' . $this->postCode . '</PostCode>' .
'<Town>' . $this->town .'</Town>' .
'<Province>' . $this->province . '</Province>' .
'<CountryCode>' . $this->countryCode . '</CountryCode>' .
'</AddressInSpain>';
if (!is_null($centre->description)) {
$xml .= '<CentreDescription>' . $centre->description . '</CentreDescription>';
}
$xml .= '</AdministrativeCentre>';
}
$xml .= '</AdministrativeCentres>';
}

// Add custom block
// Add custom block (either `LegalEntity` or `Individual`)
$xml .= ($this->isLegalEntity) ? '<LegalEntity>' : '<Individual>';

// Add legal entity data
// Add data exclusive to `LegalEntity`
if ($this->isLegalEntity) {
$xml .= '<CorporateName>' . $this->name . '</CorporateName>';
if (
!is_null($this->book) || !is_null($this->merchantRegister) ||
!is_null($this->sheet) || !is_null($this->folio) ||
!is_null($this->section) || !is_null($this->volume)
) {
$fields = array("book", "merchantRegister", "sheet", "folio",
"section", "volume");

$nonEmptyFields = array();
foreach ($fields as $fieldName) {
if (!empty($this->{$fieldName})) $nonEmptyFields[] = $fieldName;
}

if (count($nonEmptyFields) > 0) {
$xml .= '<RegistrationData>';
if (!is_null($this->book)) $xml .= '<Book>' . $this->book . '</Book>';
if (!is_null($this->merchantRegister)) $xml .=
'<RegisterOfCompaniesLocation>' . $this->merchantRegister .
'</RegisterOfCompaniesLocation>';
if (!is_null($this->sheet)) $xml .= '<Sheet>' . $this->sheet . '</Sheet>';
if (!is_null($this->folio)) $xml .= '<Folio>' . $this->folio . '</Folio>';
if (!is_null($this->section)) $xml .= '<Section>' . $this->section .
'</Section>';
if (!is_null($this->volume)) $xml .= '<Volume>' . $this->volume .
'</Volume>';
foreach ($nonEmptyFields as $fieldName) {
$tag = ucfirst($fieldName);
$xml .= "<$tag>" . $this->{$fieldName} . "</$tag>";
}
$xml .= '</RegistrationData>';
}
}

// Add individual data
// Add data exclusive to `Individual`
if (!$this->isLegalEntity) {
$xml .= '<Name>' . $this->name . '</Name><FirstSurname>' .
$this->firstSurname . '</FirstSurname><SecondSurname>' .
$this->lastSurname . '</SecondSurname>';
$xml .= '<Name>' . $this->name . '</Name>';
$xml .= '<FirstSurname>' . $this->firstSurname . '</FirstSurname>';
$xml .= '<SecondSurname>' . $this->lastSurname . '</SecondSurname>';
}

// Add address
$xml .= '<AddressInSpain><Address>' . $this->address . '</Address>' .
'<PostCode>' . $this->postCode . '</PostCode><Town>' . $this->town .
'</Town><Province>' . $this->province . '</Province><CountryCode>' .
$this->countryCode . '</CountryCode></AddressInSpain>';
$xml .= '<AddressInSpain>' .
'<Address>' . $this->address . '</Address>' .
'<PostCode>' . $this->postCode . '</PostCode>' .
'<Town>' . $this->town . '</Town>' .
'<Province>' . $this->province . '</Province>' .
'<CountryCode>' . $this->countryCode . '</CountryCode>' .
'</AddressInSpain>';

// Close custom block
$xml .= ($this->isLegalEntity) ? '</LegalEntity>' : '</Individual>';
Expand All @@ -131,4 +142,4 @@ public function getXML($schema) {
return $xml;
}

}
}
51 changes: 36 additions & 15 deletions tests/FacturaeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,21 @@

final class FacturaeTest extends TestCase {

const FILE_PATH = __DIR__ . "/salida.xsig";
const FILE_PATH = __DIR__ . "/salida-*.xsig";
const COOKIES_PATH = __DIR__ . "/cookies.txt";


/**
* Test Create Invoice
* @param boolean $isPfx Whether to test with PFX signature or PEM files
*
* @param string $schemaVersion FacturaE Schema Version
* @param boolean $isPfx Whether to test with PFX signature or PEM files
*
* @dataProvider invoicesProvider
*/
public function testCreateInvoice($isPfx=false) {
public function testCreateInvoice($schemaVersion, $isPfx) {
// Creamos la factura
$fac = new Facturae();
$fac = new Facturae($schemaVersion);

// Asignamos el número EMP2017120003 a la factura
// Nótese que Facturae debe recibir el lote y el
Expand All @@ -33,7 +37,10 @@ public function testCreateInvoice($isPfx=false) {
"address" => "C/ Falsa, 123",
"postCode" => "23456",
"town" => "Madrid",
"province" => "Madrid"
"province" => "Madrid",
"book" => "0",
"merchantRegister" => "RG",
"sheet" => "1"
]));

// Incluimos los datos del comprador
Expand Down Expand Up @@ -107,29 +114,43 @@ public function testCreateInvoice($isPfx=false) {
$fac->sign(__DIR__ . "/public.pem", __DIR__ . "/private.pem", "12345");
}

// ... y exportarlo a un archivo
$res = $fac->export(self::FILE_PATH);
$this->assertEquals($res, true);
// ... exportarlo a un archivo ...
$isPfxStr = $isPfx ? "PKCS12" : "X509";
$outputPath = str_replace("*", "$schemaVersion-$isPfxStr", self::FILE_PATH);
$res = $fac->export($outputPath);

// ... y validar la factura
$this->validateInvoiceXML($outputPath);
}


/**
* Test PFX
* Invoices provider
*/
public function testPfx() {
$this->testCreateInvoice(true);
public function invoicesProvider() {
// TODO: uncomment last two tests
// Not ready for production as almost no provider supports v3.2.2,
// not even the Spanish Goverment itself. Maybe in 2018?
return [
"v3.2.1 (X.509)" => [Facturae::SCHEMA_3_2_1, false],
"v3.2.1 (PKCS#12)" => [Facturae::SCHEMA_3_2_1, true],
//"v3.2.2 (X.509)" => [Facturae::SCHEMA_3_2_2, false],
//"v3.2.2 (PKCS#12)" => [Facturae::SCHEMA_3_2_2, true]
];
}


/**
* Test Invoice Complies with Format
* Validate Invoice XML
*
* @param string $path Invoice path
*/
public function testInvoiceCompliesWithFormat() {
private function validateInvoiceXML($path) {
// Prepare file to upload
if (function_exists('curl_file_create')) {
$postFile = curl_file_create(self::FILE_PATH);
$postFile = curl_file_create($path);
} else {
$postFile = "@" . realpath(self::FILE_PATH);
$postFile = "@" . realpath($path);
}

// Send upload request
Expand Down

0 comments on commit 4234d38

Please sign in to comment.