Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/vendor/
/.idea/
composer.lock
.phpstan.cache
.php-cs-fixer.cache
Expand Down
97 changes: 59 additions & 38 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
# Factur-X PHP

This is a modern (>PHP 8.1) toolbox for the Factur-X specification also compatible with Chorus Pro.
That use [Mpdf](https://mpdf.github.io/) to build a PDF/A-3 document.
A modern PHP 8.1+ library for creating Factur-X electronic invoices, compatible with Chorus Pro.
Uses [mPDF](https://mpdf.github.io/) to generate PDF/A-3 documents.

- Create a FacturX document
- Verify a document
- Read a FacturX document (soon)
- Create a Factur-X document
- Validate against official XSD schemas
- Read a Factur-X document (coming soon)

## Installation

```
```bash
composer require stann/factur-x
```

## Usage

# How to use
### Create a Factur-X document

### Create a FacturX document
Using the builder (recommended):

```php
<?php
Expand All @@ -25,59 +26,79 @@ use Stann\FacturX\AppendixParts\ExchangedDocument;
use Stann\FacturX\AppendixParts\MonetarySummation;
use Stann\FacturX\AppendixParts\TradeParty;
use Stann\FacturX\AppendixParts\TypeCode;
use Stann\FacturX\CrossIndustryInvoice;
use Stann\FacturX\CrossIndustryInvoiceBuilder;
use Stann\FacturX\FacturX;
use Stann\FacturX\ValueObjects\CountryCode;
use Stann\FacturX\ValueObjects\VATNumber;

$cii = new CrossIndustryInvoice(
$invoice = CrossIndustryInvoiceBuilder::minimum()
->withDocument(new ExchangedDocument('F0003', TypeCode::INVOICE, new DateTimeImmutable()))
->withSeller(new TradeParty('Stann', '34261828837536', CountryCode::FRANCE, new VATNumber('FR520000000')))
->withBuyer(new TradeParty('Mairie de Toulouse', '10881392400937', CountryCode::FRANCE, new VATNumber('FR980000000')))
->withAmounts(new MonetarySummation('EUR', 100.00, 120.00, 120.00))
->withBuyerReference('1004') // Optional, for Chorus Pro "Service Exécutant"
->withBuyerOrderReference('BDC001') // Optional, for Chorus Pro "Engagement Juridique"
->build();

$facturX = (new FacturX())->createFile(
fopen(__DIR__ . '/assets/facture.pdf', 'rb'),
$invoice,
);
```

Or using the constructor directly:

```php
$invoice = new CrossIndustryInvoice(
document: new ExchangedDocument('F0003', TypeCode::INVOICE, new DateTimeImmutable()),
sellerTradeParty: new TradeParty('Stann', '34261828837536', CountryCode::FRANCE, new VATNumber('FR520000000')),
buyerTradeParty: new TradeParty('Mairie de Toulouse', '10881392400937', CountryCode::FRANCE, new VATNumber('FR980000000')),
monetarySummation: new MonetarySummation('EUR', 100, 120, 120),
// The following are optionnal but useful for Chorus Pro
monetarySummation: new MonetarySummation('EUR', 100.00, 120.00, 120.00),
buyerReference: '1004',
buyerOrderReference: 'BDC000001',
buyerContractReference: 'C000001',
buyerOrderReference: 'BDC001',
);
```

$facturXService = new FacturX();
$facturX = $facturXService->createFile(
fopen(__DIR__ . '/assets/facture.pdf', 'rb'),
$cii,
);
### Available profiles

The builder provides factory methods for each Factur-X profile:

```php
CrossIndustryInvoiceBuilder::minimum(); // Simplest profile
CrossIndustryInvoiceBuilder::basicWL(); // Basic Without Lines
CrossIndustryInvoiceBuilder::basic(); // With invoice lines
CrossIndustryInvoiceBuilder::en16931(); // European standard
CrossIndustryInvoiceBuilder::extended(); // Full features
```

## Validate a FacturX
> **Note:** `withBuyerContractReference()` is only available for BASIC_WL and higher profiles.

## Validation

You have a number of APIs available to validate your document :
Validate your document against official XSD schemas:

```php
<?php

use Stann\FacturX\CrossIndustryInvoice;
use Stann\FacturX\FacturX;
use Stann\FacturX\Schema;

$cii = new CrossIndustryInvoice(/*...*/);

$facturXService = new FacturX();
$facturX = new FacturX();

// Returns void or throw an InvalidXMLSchema that contains an array
// of LibXMLError according compatibility with the Schema.
$facturXService->validate($cii, Schema::MINIMUM);
// Throws InvalidXMLSchema with LibXMLError[] on failure
$facturX->validate($invoice->toXml(), Schema::MINIMUM);

// Returns true or false according compatibility with the Schema.
$facturXService->isValid($cii, Schema::MINIMUM);
// Returns boolean
$facturX->isValid($invoice->toXml(), Schema::MINIMUM);

// Returns an array of LibXMLError according compatibility with the Schema.
$errors = $facturXService->getValidationErrors($cii, Schema::MINIMUM);
// Returns LibXMLError[]
$errors = $facturX->getValidationErrors($invoice->toXml(), Schema::MINIMUM);
```

The `Schema::class` enum object inclues all the specifications available for FacturX that you can use :
### Available schemas

- Schema::MINIUM
- Schema::BASIC_WL
- Schema::BASIC
- Schema::EN16931
- Schema::EXTENDED
- `Schema::MINIMUM`
- `Schema::BASIC_WL`
- `Schema::BASIC`
- `Schema::EN16931`
- `Schema::EXTENDED`
8 changes: 4 additions & 4 deletions src/AppendixParts/ExchangedDocument.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,16 @@
class ExchangedDocument
{
public function __construct(
private readonly string $invoiceId,
private readonly string $id,
private readonly TypeCode $typeCode,
private readonly DateTimeImmutable $issuedAt,
private readonly DateTimeImmutable $issueDateTime,
) {}

public function toXml(XmlBuilder $xml): void
{
$xml->ram('ID', $this->invoiceId);
$xml->ram('ID', $this->id);
$xml->ram('TypeCode', (string) $this->typeCode->value);
$xml->ram('IssueDateTime')
->udt('DateTimeString', $this->issuedAt->format('Ymd'))->attr('format', '102');
->udt('DateTimeString', $this->issueDateTime->format('Ymd'))->attr('format', '102');
}
}
10 changes: 5 additions & 5 deletions src/AppendixParts/MonetarySummation.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,18 @@ class MonetarySummation
{
public function __construct(
private readonly string $currency,
private readonly float $totalAmountExcludingTaxes,
private readonly float $totalAmountIncludingTaxes,
private readonly float $taxBasisTotalAmount,
private readonly float $grandTotalAmount,
private readonly float $duePayableAmount,
) {}

public function toXml(XmlBuilder $xml): void
{
$xml->ram('InvoiceCurrencyCode', $this->currency);
$summation = $xml->ram('SpecifiedTradeSettlementHeaderMonetarySummation');
$summation->ram('TaxBasisTotalAmount', (string) $this->totalAmountExcludingTaxes);
$summation->ram('TaxTotalAmount', (string) ($this->totalAmountIncludingTaxes - $this->totalAmountExcludingTaxes));
$summation->ram('GrandTotalAmount', (string) $this->totalAmountIncludingTaxes);
$summation->ram('TaxBasisTotalAmount', (string) $this->taxBasisTotalAmount);
$summation->ram('TaxTotalAmount', (string) ($this->grandTotalAmount - $this->taxBasisTotalAmount));
$summation->ram('GrandTotalAmount', (string) $this->grandTotalAmount);
$summation->ram('DuePayableAmount', (string) $this->duePayableAmount);
}
}
4 changes: 2 additions & 2 deletions src/AppendixParts/TradeParty.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
class TradeParty
{
public function __construct(
private readonly string $legalName,
private readonly string $name,
/**
* For exemple in France, that's corresponding to the SIRET number.
*/
Expand All @@ -22,7 +22,7 @@ public function __construct(

public function toXml(XmlBuilder $xml): void
{
$xml->ram('Name', $this->legalName);
$xml->ram('Name', $this->name);

if (!empty($this->registrationNumber)) {
$xml->ram('SpecifiedLegalOrganization')
Expand Down
19 changes: 4 additions & 15 deletions src/CrossIndustryInvoice.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,27 +19,16 @@ public function __construct(
private readonly TradeParty $sellerTradeParty,
private readonly TradeParty $buyerTradeParty,
private readonly MonetarySummation $monetarySummation,
/**
* For Chorus Pro it is the "Service Exécutant". It is mandatory for some buyers.
* It must belong to the Chorus Pro repository.
*
* @see https://communaute.chorus-pro.gouv.fr/annuaire-cpro/
*/
private readonly string $buyerReference = '',
/**
* For Chorus Pro Chorus Pro this is the "Legal Commitment".
* It is mandatory for some buyers. You should refer to the ChorusPro Directory to
* identify these public entity buyers that make it mandatory.
*/
private readonly string $buyerOrderReference = '',
/**
* For Chorus Pro Chorus Pro this is the "numéro de Marché".
*/
private readonly string $buyerContractReference = '',
private readonly Schema $schema = Schema::MINIMUM,
) {}

public function toXml(): string
{
$profileUrn = $this->schema->urn();

$xml = new SimpleXMLElement(trim('
<?xml version="1.0" encoding="UTF-8"?>
<rsm:CrossIndustryInvoice
Expand All @@ -50,7 +39,7 @@ public function toXml(): string
xmlns:xsi="' . Namespaces::NS_XSI->value . '">
<rsm:ExchangedDocumentContext>
<ram:GuidelineSpecifiedDocumentContextParameter>
<ram:ID>urn:factur-x.eu:1p0:minimum</ram:ID>
<ram:ID>' . $profileUrn . '</ram:ID>
</ram:GuidelineSpecifiedDocumentContextParameter>
</rsm:ExchangedDocumentContext>
</rsm:CrossIndustryInvoice>
Expand Down
Loading