unova_factur_x 0.1.4 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a55d91791d062b326c0c4ae531507b4ad84a7249f3e1c7b8c1567040dddfb4c2
4
- data.tar.gz: 5c38746bf64d6a103ffad89afa73b5bcf85478f14bdfdbe26508da2f09c03be6
3
+ metadata.gz: ea40c89024db0ae23f9c9ae3810842a3287fca547b7e71173cdad0a76484cef2
4
+ data.tar.gz: 8e7fa13a6e7231e3c45b54d3d79c295355712546aac6341e0d055ed05171fcf5
5
5
  SHA512:
6
- metadata.gz: d44e83f78f0d4d4fa62dff23e05b3a696f2141da10dd5106bae8a0fe3f6f3c6a111832dd53632f054fb762032bf44c1d6f9d59f6c61dcf7b002404ba8d9523c2
7
- data.tar.gz: a9988c5d634cfccc5e173a02e490f12c59a11a4c6cf644a2d2580af5c4b82e7ccaf9d358081a7faf1688bf4659f0729af9a4a49eb4eb78d8f899dcb1be2960cd
6
+ metadata.gz: a77cfcd8c5d379ed7db10607c23f992cc3e09d335b84a1bf3286611ef89b4997395ccfbde90e68f84ecde8672420d9a394d0b08f0b6e0dd2d6c293ba8e98da5d
7
+ data.tar.gz: 0bb7a2cbc4cd9bedc7ff7a5e69e1048fabde9484d91f1e45001fd558dc11df0bf02b3b9feb1c21693310c2400a651c2068488b1ae197a70fe295df2e3e1b3268
data/README.md CHANGED
@@ -1,181 +1,194 @@
1
1
  # UnovaFacturX
2
2
 
3
- Gem permettant la génération de factures et d'avoirs au format Factur-X.
3
+ Factur-X is a hybrid electronic invoice format with a human-readable PDF file that contains, as an attachment, a structured XML file.
4
+ This gem allows you to transform invoices and credits notes from the PDF format to the Factur-X format.
4
5
 
5
- ## Installation
6
+ ## Setup
6
7
 
7
- Ajouter la gem au gemfile et faire un ```bundle install```
8
+ Add the gem to your gemfile and `bundle install`:
8
9
  ```ruby
9
- gem "unova_factur_x", git: "git@gitlab.unova.fr:unova-factur-x/unova-factur-x.git"
10
+ gem "unova_factur_x"
10
11
  ```
12
+ If you want to use the validator for the generated XML file of the Factur-X, you also need to have Java installed on your computer.
11
13
 
12
- ## Utilisation
14
+ ## Usage
13
15
 
14
- Il suffit d'appeler la fonction generate de la gem à l'envoi du PDF par le controller avec les paramètres suivants :
15
- - pdf: Le fichier PDF de la facture/avoir à transformer en Factur-X. Prends en charge ces deux formes :
16
- - Depuis un fichier :
16
+ Simply call the `generate` method of the gem when you want to send the PDF to the user from your controller.
17
+ The method accepts the following parameters:
18
+ - pdf: The PDF file of the invoice/credit note to transform to Factur-X. It can be provided in two ways:
19
+ - From a file:
17
20
  ```ruby
18
21
  path = ActiveStorage::Blob.service.send(:path_for, @invoice.file.key)
19
22
  pdf = File.open(path, 'rb')
20
23
  ```
21
- - Depuis une génération Prawn :
24
+ - From a Prawn-generated PDF:
22
25
  ```ruby
23
- pdf = DocumentPdf.new(**options).render
26
+ pdf = PdfDocument.new(**options).render
24
27
  ```
25
- - document_hash: Le hash d'entrée pour la génération du XML, voir ci-après pour plus de détails.
26
- - [optionnel] type: Le type de document (:invoice par défaut):
27
- - :invoice pour une facture,
28
- - :credit pour un avoir,
29
- - [optionnel] with_validations: true ou false, si à true, va essayer de valider les données du hash fourni pour Factur-X /!\ Nécessite Java, à désactiver si Java non présent /!\ (true par défaut)
30
- - [optionnel] devise: pour configurer la monnaie utilisée sur la facture/l'avoir (Euros 'EUR' par défaut).
28
+ - document_hash: The hash required to generate the XML part of the Factur-X. See below for more details.
29
+ - [optional] type: The document type:
30
+ - `:invoice` for an invoice (default value),
31
+ - `:credit` for a credit note,
32
+ - [optional] with_validations: `true` or `false`, default as `true`. If true, the generated XML file will be checked using the validator. **WARNING: Java is required for this to work**
33
+ - [optional] currency: To configure the used currency (Default is euros 'EUR').
31
34
  ```ruby
32
- # Exemple d'utilisation :
33
- send_data UnovaFacturX.generate(pdf: pdf, document_hash: document_hash, type: :invoice, with_validations: true, devise: "USD"),
35
+ # Usage example:
36
+ send_data UnovaFacturX.generate(pdf: pdf, document_hash: document_hash, type: :invoice, with_validations: true, currency: "USD"),
34
37
  filename: "Factur-X.pdf",
35
38
  type: 'application/pdf',
36
39
  disposition: 'attachment'
37
40
  ```
38
41
 
39
- Pour le hash du document attendu :
40
- - Les montants fournis doivent être arithmétiquement cohérents, aucune correction automatique n’est effectuée.
41
- - Tous les attributs de la facture/du crédit sont attendus en String.
42
- - Respecter la forme ci-dessous :
42
+ ### Document hash structure overview
43
+
44
+
45
+ For the expected document hash:
46
+ - The document hash is a structured Ruby hash composed of the following main sections:
47
+ - `seller`
48
+ - `buyer`
49
+ - `items`
50
+ - `payment_means`
51
+ - `vat_breakdown`
52
+ - `totals`
53
+ - Provided amounts must be arithmetically consistent; no automatic correction is performed.
54
+ - All attributes must be provided as strings.
55
+ - Follow the structure below:
43
56
  ```ruby
44
- # Exemple de hash pour une facture (Même chose pour un avoir /!\ Ne pas mettre les valeurs de l'avoir en négatif /!\) :
57
+ # Example hash for an invoice (the same applies to a credit note; IMPORTANT: do not use negative values for credit note):
45
58
  document_hash = {
46
- id: "Numéro unique de facture (BT-1) [OBLIGATOIRE]",
47
- issue_date: "Date d'émission format YYYYMMDD (BT-2) [OBLIGATOIRE]",
48
-
49
- seller: {
50
- name: "Nom légal du vendeur (BT-27) [OBLIGATOIRE]",
51
- legal_id: "Identifiant légal (SIREN/SIRET) (BT-30) [OPTIONNEL]",
52
- vat_number: "Numéro TVA avec préfixe pays acheteur (ex: FR123...) (BT-31) [OPTIONNEL]",
53
- address: {
54
- line1: "Rue (BT-35) [OBLIGATOIRE]",
55
- line2: "Complément adresse [OPTIONNEL]",
56
- postcode: "Code postal (BT-38) [OBLIGATOIRE]",
57
- city: "Ville (BT-37) [OBLIGATOIRE]",
58
- country: "Code pays ISO 3166-1 alpha-2 (BT-40) [OBLIGATOIRE]",
59
- }
59
+ id: "Unique invoice number (BT-1) [REQUIRED]",
60
+ issue_date: "Issue date in YYYYMMDD format (BT-2) [REQUIRED]",
61
+
62
+ seller: {
63
+ name: "Seller legal name (BT-27) [REQUIRED]",
64
+ legal_id: "Legal identifier (SIREN/SIRET) (BT-30) [OPTIONAL]",
65
+ vat_number: "VAT number including buyer country prefix (e.g. FR123...) (BT-31) [OPTIONAL]",
66
+ address: {
67
+ line1: "Street address (BT-35) [REQUIRED]",
68
+ line2: "Address complement [OPTIONAL]",
69
+ postcode: "Postal code (BT-38) [REQUIRED]",
70
+ city: "City (BT-37) [REQUIRED]",
71
+ country: "ISO 3166-1 alpha-2 country code (BT-40) [REQUIRED]",
72
+ }
73
+ },
74
+
75
+ # [REQUIRED BLOCK]
76
+ buyer: {
77
+ id: "Internal customer identifier (BT-46) [OPTIONAL]",
78
+ name: "Customer legal name (BT-44) [REQUIRED]",
79
+ vat_number: "VAT number including buyer country prefix (e.g. FR123...) (BT-48) [OPTIONAL]",
80
+ contact: { # [OPTIONAL]
81
+ name: "Customer contact name (BT-56) [OPTIONAL]",
60
82
  },
61
-
62
- # [BLOC OBLIGATOIRE]
63
- buyer: {
64
- id: "Identifiant interne client (BT-46) [OPTIONNEL]",
65
- name: "Nom légal du client (BT-44) [OBLIGATOIRE]",
66
- vat_number: "Numéro TVA avec préfixe pays acheteur (ex: FR123...) (BT-48) [OPTIONNEL]",
67
- contact: { # [OPTIONNEL]
68
- name: "Nom du contact client (BT-56) [OPTIONNEL]",
83
+ address: {
84
+ line1: "Street address (BT-50) [REQUIRED]",
85
+ line2: "Address complement [OPTIONAL]",
86
+ postcode: "Postal code (BT-53) [REQUIRED]",
87
+ city: "City (BT-52) [REQUIRED]",
88
+ country: "ISO 3166-1 alpha-2 country code (BT-55) [REQUIRED]",
89
+ }
90
+ },
91
+
92
+ # [OPTIONAL BLOCK]
93
+ delivery: {
94
+ gln: "GLN identifier (schemeID 0088) (BT-71) [OPTIONAL]",
95
+ gln_scheme: "0088: GLN (GS1), 0002: SIRENE (France), 9906: SIRET, 9915: French intra-community VAT, 0060: DUNS [OPTIONAL | REQUIRED IF GLN IS PROVIDED]",
96
+ date: "Actual delivery date in YYYYMMDD format (BT-72) [OPTIONAL]",
97
+ address: {
98
+ line1: "Delivery street address (BT-75) [OPTIONAL]",
99
+ line2: "Delivery address complement [OPTIONAL]",
100
+ postcode: "Delivery postal code (BT-75) [OPTIONAL]",
101
+ city: "Delivery city (BT-74) [OPTIONAL]",
102
+ country: "ISO 3166-1 alpha-2 country code (BT-76) [OPTIONAL]",
103
+ }
104
+ },
105
+
106
+ # [REQUIRED BLOCK] (minimum 1 item)
107
+ items: [
108
+ {
109
+ line_id: "Line number (BT-126) [REQUIRED]",
110
+ seller_assigned_id: "Internal product identifier (BT-155) [OPTIONAL]",
111
+ name: "Product/service description (BT-153) [REQUIRED]",
112
+ quantity: "Quantity (BT-129) [REQUIRED]",
113
+ unit_code: "UN/ECE Rec20 unit code (e.g. H87, C62, DAY) (BT-130) [REQUIRED]",
114
+ price_ht: "Net unit price excluding VAT (BT-146) [REQUIRED]",
115
+ vat_rate: "VAT rate (BT-152) [REQUIRED]",
116
+ vat_category: "VAT category (S, Z, E, AE...) (BT-151) [REQUIRED]",
117
+ discount: { # [OPTIONAL]
118
+ total_amount: "Discount amount applicable to the invoice line (BT-136) [OPTIONAL unless discount block is present]",
119
+ percentage: "Discount percentage applicable to the invoice line (BT-138) [OPTIONAL unless discount block is present]",
120
+ # reason OR reason_code [REQUIRED] if block is present
121
+ reason: "Reason for the invoice line discount (BT-139) [OPTIONAL unless discount block is present]",
122
+ reason_code: "Reason code for the invoice line discount (BT-140) [OPTIONAL unless discount block is present]"
69
123
  },
70
- address: {
71
- line1: "Rue (BT-50) [OBLIGATOIRE]",
72
- line2: "Complément adresse [OPTIONNEL]",
73
- postcode: "Code postal (BT-53) [OBLIGATOIRE]",
74
- city: "Ville (BT-52) [OBLIGATOIRE]",
75
- country: "Code pays ISO 3166-1 alpha-2 (BT-55) [OBLIGATOIRE]",
76
- }
77
- },
78
-
79
- # [BLOC OPTIONNEL]
80
- delivery: {
81
- gln: "Identifiant GLN (schemeID 0088) (BT-71) [OPTIONNEL]",
82
- gln_scheme: "0088: GLN (GS1), 0002: SIRENE (France), 9906: SIRET, 9915: TVA intracom FR, 0060: DUNS [OPTIONNEL | OBLIGATOIRE SI GLN]",
83
- date: "Date réelle de livraison format YYYYMMDD (BT-72) [OPTIONNEL]",
84
- address: {
85
- line1: "Rue livraison (BT-75) [OPTIONNEL]",
86
- line2: "Complément adresse livraison [OPTIONNEL]",
87
- postcode: "Code postal livraison (BT-75) [OPTIONNEL]",
88
- city: "Ville livraison (BT-74) [OPTIONNEL]",
89
- country: "Code pays ISO 3166-1 alpha-2 (BT-76) [OPTIONNEL]",
90
- }
91
- },
92
-
93
- # [BLOC OBLIGATOIRE] (minimum 1 item)
94
- items: [
95
- {
96
- line_id: "Numéro de ligne (BT-126) [OBLIGATOIRE]",
97
- seller_assigned_id: "Identifiant interne produit (BT-155) [OPTIONNEL]",
98
- name: "Désignation produit/service (BT-153) [OBLIGATOIRE]",
99
- quantity: "Quantité (BT-129) [OBLIGATOIRE]",
100
- unit_code: "Code unité UN/ECE Rec20 (ex: H87, C62, DAY) (BT-130) [OBLIGATOIRE]",
101
- price_ht: "Prix unitaire net HT (BT-146) [OBLIGATOIRE]",
102
- vat_rate: "Taux TVA (BT-152) [OBLIGATOIRE]",
103
- vat_category: "Catégorie TVA (S, Z, E, AE...) (BT-151) [OBLIGATOIRE]",
104
- discount: { # [OPTIONNEL]
105
- total_amount: "Montant de la remise applicable à la ligne de facture (BT-136) [OPTIONNEL sauf si discount]",
106
- percentage: "Pourcentage de remise applicable à la ligne de facture (BT-138) [OPTIONNEL sauf si discount]",
107
- # reason OU reason_code [OBLIGATOIRE] si bloc présent
108
- reason: "Motif de la remise applicable à la ligne de facture (BT-139) [OPTIONNEL sauf si discount]",
109
- reason_code: "Code de motif de la remise applicable à la ligne de facture (BT-140) [OPTIONNEL sauf si discount]"
110
- },
111
- line_total: "Montant net de la ligne HT = Quantité × Prix unitaire net (BT-131)"
112
- }
113
- ],
114
-
115
- # [BLOC OBLIGATOIRE]
116
- payment_means: {
117
- type_code: "Code UNCL 4461 (30 = virement) (BT-81) [OBLIGATOIRE]",
118
- iban: "IBAN bénéficiaire (BT-84) [OBLIGATOIRE si virement]",
119
- },
120
-
121
- # [BLOC OBLIGATOIRE]
122
- vat_breakdown: [
123
- {
124
- vat_category: "Catégorie TVA (BT-118) [OBLIGATOIRE]",
125
- vat_rate: "Taux TVA % (BT-119) [OBLIGATOIRE]",
126
- taxable_amount: "Base HT pour ce taux (BT-116) [OBLIGATOIRE]",
127
- tax_amount: "Montant TVA pour ce taux (BT-117) [OBLIGATOIRE]",
128
- # exemption_reason OU exemption_reason_code [OBLIGATOIRE] si vat_category = "E" (Exempt)
129
- exemption_reason: "Motif d'exonération de la TVA (BT-120)",
130
- exemption_reason_code: "Code de motif d'exonération de la TVA (BT-121)"
131
- }
132
- ],
133
-
134
- # [BLOC OPTIONNEL]
135
- discount: [ # Ce bloc est un tableau avec un item par taux de TVA d'item. Il doit donc avoir la même longueur que le bloc vat_breakdown
136
- {
137
- vat_category: "Catégorie TVA (BT-118) [OBLIGATOIRE si le bloc est présent]",
138
- vat_rate: "Taux TVA % (BT-119) [OBLIGATOIRE si le bloc est présent]",
139
- total_amount: "Montant total de la remise pour le taux de TVA [OBLIGATOIRE si percentage présent]",
140
- percentage: "% de remise au niveau du document si la remise est en % (BT-94) [OPTIONNEL]",
141
- # reason OU reason_code [OBLIGATOIRE] si bloc présent
142
- reason: "Motif de la remise au niveau du document (BT-97)",
143
- reason_code: "Code de motif de la remise au niveau du document (BT-98)",
144
- }
145
- ],
146
-
147
- # [BLOC OBLIGATOIRE]
148
- totals: {
149
- line_total_ht: "Total HT lignes (BT-106) [OBLIGATOIRE]",
150
- total_discount: "Somme des remises au niveau du document (BT-107) [OBLIGATOIRE si bloc discount présent]",
151
- tax_basis_total_ht: "Total bases taxables (BT-109) [OBLIGATOIRE]",
152
- tax_total: "Total TVA (BT-110) [OBLIGATOIRE]",
153
- grand_total_ttc: "Total TTC (BT-112) [OBLIGATOIRE]",
154
- amount_due: "Montant à payer (BT-115) [OPTIONNEL]",
155
- # due_date OU description [OBLIGATOIRE] si amount_due est défini et positif
156
- due_date: "Date due du paiement format YYYYMMDD (BT-9)",
157
- description: "Termes du paiement (BT-20)"
124
+ line_total: "Net line amount excluding VAT = Quantity × Net unit price (BT-131)"
125
+ }
126
+ ],
127
+
128
+ # [REQUIRED BLOCK]
129
+ payment_means: {
130
+ type_code: "UNCL 4461 code (30 = bank transfer) (BT-81) [REQUIRED]",
131
+ iban: "Beneficiary IBAN (BT-84) [REQUIRED for bank transfer]",
132
+ },
133
+
134
+ # [REQUIRED BLOCK]
135
+ vat_breakdown: [
136
+ {
137
+ vat_category: "VAT category (BT-118) [REQUIRED]",
138
+ vat_rate: "VAT rate % (BT-119) [REQUIRED]",
139
+ taxable_amount: "Taxable amount excluding VAT for this rate (BT-116) [REQUIRED]",
140
+ tax_amount: "VAT amount for this rate (BT-117) [REQUIRED]",
141
+ # exemption_reason OR exemption_reason_code [REQUIRED] if vat_category = 'E' (Exempt)
142
+ exemption_reason: "VAT exemption reason (BT-120)",
143
+ exemption_reason_code: "VAT exemption reason code (BT-121)"
144
+ }
145
+ ],
146
+
147
+ # [OPTIONAL BLOCK]
148
+ discount: [ # This block is an array with one item per item VAT rate. Therefore, it must have the same length as the vat_breakdown block.
149
+ {
150
+ vat_category: "VAT category (BT-118) [REQUIRED if block is present]",
151
+ vat_rate: "VAT rate % (BT-119) [REQUIRED if block is present]",
152
+ total_amount: "Total discount amount for the VAT rate [REQUIRED if percentage is present]",
153
+ percentage: "Document-level discount percentage if the discount is expressed as a percentage (BT-94) [OPTIONAL]",
154
+ # reason OR reason_code [REQUIRED] if block is present
155
+ reason: "Reason for the document-level discount (BT-97)",
156
+ reason_code: "Reason code for the document-level discount (BT-98)",
158
157
  }
158
+ ],
159
+
160
+ # [REQUIRED BLOCK]
161
+ totals: {
162
+ line_total_ht: "Total line amount excluding VAT (BT-106) [REQUIRED]",
163
+ total_discount: "Sum of document-level discounts (BT-107) [REQUIRED if discount block is present]",
164
+ tax_basis_total_ht: "Total taxable amount (BT-109) [REQUIRED]",
165
+ tax_total: "Total VAT amount (BT-110) [REQUIRED]",
166
+ grand_total_ttc: "Grand total including VAT (BT-112) [REQUIRED]",
167
+ amount_due: "Amount due for payment (BT-115) [OPTIONAL]",
168
+ # due_date OR description [REQUIRED] if amount_due is defined and positive
169
+ due_date: "Payment due date in YYYYMMDD format (BT-9)",
170
+ description: "Payment terms (BT-20)"
171
+ }
159
172
  }
160
173
  ```
161
174
 
162
- ## Composants tiers
175
+ ## Third-Party Components
163
176
 
164
- Cette gem embarque les composants suivants :
177
+ This gem includes the following third-party component:
165
178
 
166
- - Saxon-HE (processeur XSLT et XQuery) développé par Saxonica Limited, sous licence Mozilla Public License 2.0 (MPL-2.0).
179
+ - Saxon-HE (XSLT and XQuery processor), developed by Saxonica Limited and licensed under the Mozilla Public License 2.0 (MPL-2.0).
167
180
 
168
- Le JAR Saxon-HE est redistribué tel quel et reste soumis à sa licence d’origine.
169
- Une copie de la licence MPL-2.0 est incluse dans ce repository sous le dossier LICENSES.
181
+ The Saxon-HE JAR is redistributed as-is and remains subject to its original license terms.
182
+ A copy of the MPL-2.0 license is included in this repository under the LICENSES directory.
170
183
 
171
- ## Licence
184
+ ## License
172
185
 
173
- Ce projet est sous licence Apache 2.0.
186
+ This project is licensed under the Apache License 2.0.
174
187
 
175
- La licence Apache 2.0 autorise l'utilisation, la modification, la distribution et la commercialisation du logiciel, à condition de conserver les mentions de droits d'auteur et de respecter les termes de la licence.
188
+ The Apache License 2.0 permits the use, modification, distribution, and commercialization of the software, provided that copyright notices are retained and the terms of the license are respected.
176
189
 
177
- Sauf disposition contraire prévue par la loi ou convenue par écrit, ce logiciel est distribué « en l'état », sans garantie ni condition d'aucune sorte, expresse ou implicite.
190
+ Unless required by applicable law or agreed to in writing, this software is distributed on an "AS IS" basis, without warranties or conditions of any kind, either express or implied.
178
191
 
179
- Pour consulter l'intégralité des conditions, reportez-vous au fichier `LICENSE` situé à la racine du dépôt ou au texte officiel : https://www.apache.org/licenses/LICENSE-2.0
192
+ For the full license terms, refer to the LICENSE file located at the root of the repository or to the official license text: https://www.apache.org/licenses/LICENSE-2.0
180
193
 
181
- Copyright (C) UNOVA
194
+ Copyright (c) 2026 UNOVA
@@ -0,0 +1,29 @@
1
+ NOTICE AND INFORMATION ABOUT THIRD-PARTY SOFTWARE
2
+
3
+ This project includes third-party software components.
4
+
5
+ ------------------------------------------------------------
6
+ 1. Saxon-HE (XSLT and XQuery processor)
7
+ ------------------------------------------------------------
8
+
9
+ Name: Saxon-HE
10
+ Version: 12.4
11
+ Author: Saxonica Limited
12
+ Official website: https://www.saxonica.com/
13
+
14
+ License: Mozilla Public License 2.0 (MPL-2.0)
15
+
16
+ Copyright:
17
+ Copyright (c) Saxonica Limited
18
+
19
+ Saxon-HE is included in this distribution as a precompiled JAR file and remains subject to its original license terms.
20
+
21
+ The full text of the MPL-2.0 license is available here:
22
+ https://www.mozilla.org/en-US/MPL/2.0/
23
+
24
+ A copy of the MPL-2.0 license is included in this project:
25
+ LICENSES/MPL-2.0.txt
26
+
27
+ ------------------------------------------------------------
28
+ END OF NOTICE
29
+ ------------------------------------------------------------
@@ -6,7 +6,7 @@ module UnovaFacturX
6
6
  end
7
7
 
8
8
  def call
9
- # Override règles de base HexaPDF pour pas qu'il force la version en 2.0
9
+ # Override HexaPDF default rules to prevent it from forcing PDF version 2.0
10
10
  begin
11
11
  ::HexaPDF::Type::Catalog.send(:remove_field, :AF)
12
12
  rescue StandardError
@@ -14,20 +14,20 @@ module UnovaFacturX
14
14
  end
15
15
  ::HexaPDF::Type::Catalog.define_field :AF, type: ::HexaPDF::PDFArray
16
16
 
17
- # Génération du XML grace au services (hash + xml)
17
+ # Generate the XML using the generator
18
18
  xml_doc = @xml
19
19
  xml_string = xml_doc.to_xml
20
20
  xml_io = StringIO.new(xml_string)
21
21
 
22
- # Transformation du PDF en StringIO
22
+ # Convert the PDF into a StringIO object
23
23
  pdf_io = @pdf.is_a?(File) ? @pdf : StringIO.new(@pdf)
24
24
  pdf_io.rewind
25
25
 
26
- # Création d'un nouveau PDF avec HexaPDF
26
+ # Create a new PDF using HexaPDF
27
27
  doc = ::HexaPDF::Document.new(io: pdf_io)
28
28
  doc.task(:pdfa, level: "3b") # PDF format A/3
29
29
 
30
- # Ajout du XML au PDF
30
+ # Attach the XML file to the PDF
31
31
  file_spec = doc.files.add(
32
32
  xml_io,
33
33
  name: "factur-x.xml",
@@ -39,16 +39,16 @@ module UnovaFacturX
39
39
  doc.catalog[:AF] ||= []
40
40
  doc.catalog[:AF] << file_spec
41
41
 
42
- # Réécriture des Metadata pour correspondre à Factur-X
42
+ # Rewrite the metadata to comply with Factur-X requirements
43
43
  doc.metadata.custom_metadata(metadata_xml)
44
44
 
45
- # Renvoi du PDF en StringIO
45
+ # Return the PDF as a StringIO object
46
46
  doc.write_to_string
47
47
  end
48
48
 
49
49
  private
50
50
 
51
- def metadata_xml # TODO : Utiliser les bonnes valeurs
51
+ def metadata_xml # TODO : Use invoice values
52
52
  <<~XML
53
53
  <rdf:Description rdf:about=""
54
54
  xmlns:pdfaExtension="http://www.aiim.org/pdfa/ns/extension/"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module UnovaFacturX
4
- VERSION = "0.1.4"
4
+ VERSION = "0.1.5"
5
5
  end
@@ -1,147 +1,14 @@
1
1
  module UnovaFacturX
2
2
  class XmlGenerator
3
- # Générateur de fichiers XML de type CrossIndustryInvoice pour facture électronique
3
+ # @param document [Hash] A hash representing an invoice or a credit note.
4
+ # @param type [Symbol] Document type: :invoice for a standard invoice, :credit for a credit note (:invoice by default).
5
+ # @param currency [String] ISO 4217 currency code.
4
6
  #
5
- # Cette classe permet de générer dynamiquement un fichier XML structuré selon le standard CII (EN16931), à partir d'un objet ruby.
6
- # Elle supporte les factures simples (type 380) ainsi que les avoirs (type 381).
7
- # Les montants fournis doivent être arithmétiquement cohérents, aucune correction automatique n’est effectuée.
8
- # Aussi, pour plus de simplicité, tous les attributs de la facture/du crédit sont attendus en String.
9
- # @example
10
- # # Générer un XML de facture standard
11
- # xml = XmlGenerator.new(invoice).call
12
- #
13
- # # Générer un XML pour un avoir
14
- # xml = XmlGenerator.new(credit, type: :credit).call
15
- #
16
- # # Configurer la monnaie utilisée sur la facture
17
- # xml = XmlGenerator.new(invoice, devise: "USD") # 'EUR' par défaut
18
- #
19
- # # Exemple de hash pour une facture (Même chose pour un avoir /!\ Ne pas mettre les valeurs de l'avoir en négatif /!\) :
20
- # document = {
21
- # id: "Numéro unique de facture (BT-1) [OBLIGATOIRE]",
22
- # issue_date: "Date d'émission format YYYYMMDD (BT-2) [OBLIGATOIRE]",
23
- #
24
- # seller: {
25
- # name: "Nom légal du vendeur (BT-27) [OBLIGATOIRE]",
26
- # legal_id: "Identifiant légal (SIREN/SIRET) (BT-30) [OPTIONNEL]",
27
- # vat_number: "Numéro TVA avec préfixe pays acheteur (ex: FR123...) (BT-31) [OPTIONNEL]",
28
- # address: {
29
- # line1: "Rue (BT-35) [OBLIGATOIRE]",
30
- # line2: "Complément adresse [OPTIONNEL]",
31
- # postcode: "Code postal (BT-38) [OBLIGATOIRE]",
32
- # city: "Ville (BT-37) [OBLIGATOIRE]",
33
- # country: "Code pays ISO 3166-1 alpha-2 (BT-40) [OBLIGATOIRE]",
34
- # }
35
- # },
36
- #
37
- # # [BLOC OBLIGATOIRE]
38
- # buyer: {
39
- # id: "Identifiant interne client (BT-46) [OPTIONNEL]",
40
- # name: "Nom légal du client (BT-44) [OBLIGATOIRE]",
41
- # vat_number: "Numéro TVA avec préfixe pays acheteur (ex: FR123...) (BT-48) [OPTIONNEL]",
42
- # contact: { # [OPTIONNEL]
43
- # name: "Nom du contact client (BT-56) [OPTIONNEL]",
44
- # },
45
- # address: {
46
- # line1: "Rue (BT-50) [OBLIGATOIRE]",
47
- # line2: "Complément adresse [OPTIONNEL]",
48
- # postcode: "Code postal (BT-53) [OBLIGATOIRE]",
49
- # city: "Ville (BT-52) [OBLIGATOIRE]",
50
- # country: "Code pays ISO 3166-1 alpha-2 (BT-55) [OBLIGATOIRE]",
51
- # }
52
- # },
53
- #
54
- # # [BLOC OPTIONNEL]
55
- # delivery: {
56
- # gln: "Identifiant GLN (schemeID 0088) (BT-71) [OPTIONNEL]",
57
- # gln_scheme: "0088: GLN (GS1), 0002: SIRENE (France), 9906: SIRET, 9915: TVA intracom FR, 0060: DUNS [OPTIONNEL | OBLIGATOIRE SI GLN]",
58
- # date: "Date réelle de livraison format YYYYMMDD (BT-72) [OPTIONNEL]",
59
- # address: {
60
- # line1: "Rue livraison (BT-75) [OPTIONNEL]",
61
- # line2: "Complément adresse livraison [OPTIONNEL]",
62
- # postcode: "Code postal livraison (BT-75) [OPTIONNEL]",
63
- # city: "Ville livraison (BT-74) [OPTIONNEL]",
64
- # country: "Code pays ISO 3166-1 alpha-2 (BT-76) [OPTIONNEL]",
65
- # }
66
- # },
67
- #
68
- # # [BLOC OBLIGATOIRE] (minimum 1 item)
69
- # items: [
70
- # {
71
- # line_id: "Numéro de ligne (BT-126) [OBLIGATOIRE]",
72
- # seller_assigned_id: "Identifiant interne produit (BT-155) [OPTIONNEL]",
73
- # name: "Désignation produit/service (BT-153) [OBLIGATOIRE]",
74
- # quantity: "Quantité (BT-129) [OBLIGATOIRE]",
75
- # unit_code: "Code unité UN/ECE Rec20 (ex: H87, C62, DAY) (BT-130) [OBLIGATOIRE]",
76
- # price_ht: "Prix unitaire net HT (BT-146) [OBLIGATOIRE]",
77
- # vat_rate: "Taux TVA (BT-152) [OBLIGATOIRE]",
78
- # vat_category: "Catégorie TVA (S, Z, E, AE...) (BT-151) [OBLIGATOIRE]",
79
- # discount: { # [OPTIONNEL]
80
- # total_amount: "Montant de la remise applicable à la ligne de facture (BT-136) [OPTIONNEL sauf si discount]",
81
- # percentage: "Pourcentage de remise applicable à la ligne de facture (BT-138) [OPTIONNEL sauf si discount]",
82
- # # reason OU reason_code [OBLIGATOIRE] si bloc présent
83
- # reason: "Motif de la remise applicable à la ligne de facture (BT-139) [OPTIONNEL sauf si discount]",
84
- # reason_code: "Code de motif de la remise applicable à la ligne de facture (BT-140) [OPTIONNEL sauf si discount]"
85
- # }
86
- # line_total: "Montant net de la ligne HT = Quantité × Prix unitaire net (BT-131)"
87
- # }
88
- # ],
89
- #
90
- # # [BLOC OBLIGATOIRE]
91
- # payment_means: {
92
- # type_code: "Code UNCL 4461 (30 = virement) (BT-81) [OBLIGATOIRE]",
93
- # iban: "IBAN bénéficiaire (BT-84) [OBLIGATOIRE si virement]",
94
- # },
95
- #
96
- # # [BLOC OBLIGATOIRE]
97
- # vat_breakdown: [
98
- # {
99
- # vat_category: "Catégorie TVA (BT-118) [OBLIGATOIRE]",
100
- # vat_rate: "Taux TVA % (BT-119) [OBLIGATOIRE]",
101
- # taxable_amount: "Base HT pour ce taux (BT-116) [OBLIGATOIRE]",
102
- # tax_amount: "Montant TVA pour ce taux (BT-117) [OBLIGATOIRE]",
103
- # # exemption_reason OU exemption_reason_code [OBLIGATOIRE] si vat_category = "E" (Exempt)
104
- # exemption_reason: "Motif d'exonération de la TVA (BT-120)",
105
- # exemption_reason_code: "Code de motif d'exonération de la TVA (BT-121)"
106
- # }
107
- # ],
108
- #
109
- # # [BLOC OPTIONNEL]
110
- # discount: [ # Ce bloc est un tableau avec un item par taux de TVA d'item. Il doit donc avoir la même longueur que le bloc vat_breakdown
111
- # {
112
- # vat_category: "Catégorie TVA (BT-118) [OBLIGATOIRE si le bloc est présent]",
113
- # vat_rate: "Taux TVA % (BT-119) [OBLIGATOIRE si le bloc est présent]",
114
- # total_amount: "Montant total de la remise pour le taux de TVA [OBLIGATOIRE si percentage présent]",
115
- # percentage: "% de remise au niveau du document si la remise est en % (BT-94) [OPTIONNEL]",
116
- # # reason OU reason_code [OBLIGATOIRE] si bloc présent
117
- # reason: "Motif de la remise au niveau du document (BT-97)",
118
- # reason_code: "Code de motif de la remise au niveau du document (BT-98)",
119
- # }
120
- # ],
121
- #
122
- # # [BLOC OBLIGATOIRE]
123
- # totals: {
124
- # line_total_ht: "Total HT lignes (BT-106) [OBLIGATOIRE]",
125
- # total_discount: "Somme des remises au niveau du document (BT-107) [OBLIGATOIRE si bloc discount présent]",
126
- # tax_basis_total_ht: "Total bases taxables (BT-109) [OBLIGATOIRE]",
127
- # tax_total: "Total TVA (BT-110) [OBLIGATOIRE]",
128
- # grand_total_ttc: "Total TTC (BT-112) [OBLIGATOIRE]",
129
- # amount_due: "Montant à payer (BT-115) [OPTIONNEL]",
130
- # # due_date OU description [OBLIGATOIRE] si amount_due est défini et positif
131
- # due_date: "Date due du paiement format YYYYMMDD (BT-9)",
132
- # description: "Termes du paiement (BT-20)"
133
- # }
134
- # }
135
- #
136
- # @param document [Hash] Un hash représentant une facture ou un crédit.
137
- # @param type [Symbol] Type de document : `:invoice` pour une facture normale, `:credit` pour un avoir. (`:invoice` par défaut)
138
- # @param devise [String] Code ISO 4217
139
- #
140
- # @return [Nokogiri::XML::Document] Le document XML généré
141
- def initialize(document, type: :invoice, devise: "EUR", validate: true)
7
+ # @return [Nokogiri::XML::Document] The generated XML document
8
+ def initialize(document, type: :invoice, currency: "EUR", validate: true)
142
9
  @document = document
143
10
  @type = type
144
- @devise = devise
11
+ @currency = currency
145
12
  @validate = validate
146
13
  end
147
14
 
@@ -161,7 +28,7 @@ module UnovaFacturX
161
28
 
162
29
  validate_with_xslt!(builder.doc) if @validate
163
30
 
164
- # Retourne le XML
31
+ # Returns the XML document
165
32
  builder.doc
166
33
  end
167
34
 
@@ -170,18 +37,18 @@ module UnovaFacturX
170
37
  def build_exchanged_document_context(xml)
171
38
  xml['rsm'].ExchangedDocumentContext do
172
39
  xml['ram'].GuidelineSpecifiedDocumentContextParameter do
173
- xml['ram'].ID("urn:cen.eu:en16931:2017") # Identifiant du type de facture (Ici la norme Européenne)
40
+ xml['ram'].ID("urn:cen.eu:en16931:2017") # Identifier of the invoice specification (European EN16931 standard)
174
41
  end
175
42
  end
176
43
  end
177
44
 
178
45
  def build_exchanged_document(xml)
179
46
  xml['rsm'].ExchangedDocument do
180
- xml['ram'].ID(@document[:id]) # Numéro de la facture BT-1 (Doit être unique/vendeur)
181
- xml['ram'].TypeCode(@type == :credit ? "381" : "380") # Type de document (380 = facture, 381 = avoir) (BT-3)
47
+ xml['ram'].ID(@document[:id]) # Invoice number (BT-1) - must be unique per seller
48
+ xml['ram'].TypeCode(@type == :credit ? "381" : "380") # Document type (380 = invoice, 381 = credit note) (BT-3)
182
49
  if @document[:issue_date].present?
183
50
  xml['ram'].IssueDateTime do
184
- xml['udt'].DateTimeString(@document[:issue_date], format: "102") # Date d’émission de la facture, format YYYYMMDD (BT-2)
51
+ xml['udt'].DateTimeString(@document[:issue_date], format: "102") # Invoice issue date in YYYYMMDD format (BT-2)
185
52
  end
186
53
  end
187
54
  end
@@ -197,47 +64,47 @@ module UnovaFacturX
197
64
  end
198
65
 
199
66
  def build_line_items(xml)
200
- raise StandardError, "Besoin d'au moins un item" if @document[:items].blank?
67
+ raise StandardError, "At least one item is required" if @document[:items].blank?
201
68
 
202
69
  @document[:items].each do |item|
203
70
  xml['ram'].IncludedSupplyChainTradeLineItem do
204
71
  xml['ram'].AssociatedDocumentLineDocument do
205
- xml['ram'].LineID(item[:line_id]) # Numéro de la ligne (BT-126)
72
+ xml['ram'].LineID(item[:line_id]) # Line number (BT-126)
206
73
  end
207
74
  xml['ram'].SpecifiedTradeProduct do
208
- xml['ram'].SellerAssignedID(item[:seller_assigned_id]) if item[:seller_assigned_id].present? # Identifiant interne du produit attribué par le vendeur (BT-155)
209
- xml['ram'].Name(item[:name]) # Désignation du produit/service (BT-153)
75
+ xml['ram'].SellerAssignedID(item[:seller_assigned_id]) if item[:seller_assigned_id].present? # Internal product identifier assigned by the seller (BT-155)
76
+ xml['ram'].Name(item[:name]) # Product/service description (BT-153)
210
77
  end
211
78
  xml['ram'].SpecifiedLineTradeAgreement do
212
79
  xml['ram'].NetPriceProductTradePrice do
213
- xml['ram'].ChargeAmount(item[:price_ht]) # Prix unitaire net HT hors remise (BT-146)
80
+ xml['ram'].ChargeAmount(item[:price_ht]) # Net unit price excluding VAT (BT-146)
214
81
  end
215
82
  end
216
83
  xml['ram'].SpecifiedLineTradeDelivery do
217
- xml['ram'].BilledQuantity(item[:quantity], unitCode: item[:unit_code]) # Quantité facturée (BT-129) avec code unité de mesure UN/ECE Rec.20/21 (BT-130)
84
+ xml['ram'].BilledQuantity(item[:quantity], unitCode: item[:unit_code]) # Quantity invoiced (BT-129) with unit code UN/ECE Rec. 20 (BT-130)
218
85
  end
219
86
 
220
87
  xml['ram'].SpecifiedLineTradeSettlement do
221
88
  xml['ram'].ApplicableTradeTax do
222
- xml['ram'].TypeCode("VAT") # Type de taxe (VAT), toujours "VAT" en UE (BT-151)
223
- xml['ram'].CategoryCode(item[:vat_category]) # Code catégorie TVA de la ligne (BT-151). Une valeur parmi : {'A', 'AA', 'AB', 'AC', 'AD', 'AE', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'O', 'S', 'Z'}
224
- xml['ram'].RateApplicablePercent(item[:vat_rate]) # Taux de TVA appliqué (%) (BT-152)
89
+ xml['ram'].TypeCode("VAT") # VAT type (always "VAT" in the EU) (BT-151)
90
+ xml['ram'].CategoryCode(item[:vat_category]) # VAT category code for the line item (BT-151). One among these: {'A', 'AA', 'AB', 'AC', 'AD', 'AE', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'O', 'S', 'Z'}
91
+ xml['ram'].RateApplicablePercent(item[:vat_rate]) # VAT rate applied (%) (BT-152)
225
92
  end
226
93
  if item[:discount].present?
227
94
  discount = item[:discount]
228
95
  xml['ram'].SpecifiedTradeAllowanceCharge do
229
96
  xml['ram'].ChargeIndicator do
230
- xml['udt'].Indicator("false") # true = charge (frais supplémentaires), false = allowance (réduction/remise)
97
+ xml['udt'].Indicator("false") # true = charge (additional fees), false = allowance (discount/allowance)
231
98
  end
232
- xml['ram'].CalculationPercent(discount[:percentage]) if discount[:percentage].present? # Pourcentage de remise applicable à la ligne de facture (BT-138)
233
- xml['ram'].ActualAmount(discount[:total_amount]) # Montant de la remise applicable à la ligne de facture (BT-136)
234
- xml['ram'].Reason(discount[:reason]) if discount[:reason].present? # Motif de la remise applicable à la ligne de facture (BT-139)
235
- xml['ram'].ReasonCode(discount[:reason_code]) if discount[:reason_code].present? # Code de motif de la remise applicable à la ligne de facture (BT-140)
236
- # xml['ram'].BasisAmount(discount[:base]) # Assiette de la remise applicable à la ligne de facture (BT-137)
99
+ xml['ram'].CalculationPercent(discount[:percentage]) if discount[:percentage].present? # Discount percentage applicable to the invoice line (BT-138)
100
+ xml['ram'].ActualAmount(discount[:total_amount]) # Discount amount applicable to the invoice line (BT-136)
101
+ xml['ram'].Reason(discount[:reason]) if discount[:reason].present? # Reason for the discount on the invoice line (BT-139)
102
+ xml['ram'].ReasonCode(discount[:reason_code]) if discount[:reason_code].present? # Reason code for the discount on the invoice line (BT-140)
103
+ # xml['ram'].BasisAmount(discount[:base]) # Discount basis applicable to invoice line (BT-137)
237
104
  end
238
105
  end
239
106
  xml['ram'].SpecifiedTradeSettlementLineMonetarySummation do
240
- xml['ram'].LineTotalAmount(item[:line_total]) # Montant net de la ligne HT = Quantité × Prix unitaire net (BT-131)
107
+ xml['ram'].LineTotalAmount(item[:line_total]) # Net line amount excluding VAT = Quantity × Net unit price (BT-131)
241
108
  end
242
109
  end
243
110
  end
@@ -260,23 +127,23 @@ module UnovaFacturX
260
127
 
261
128
  xml['ram'].ApplicableHeaderTradeDelivery do
262
129
  xml['ram'].ShipToTradeParty do
263
- xml['ram'].GlobalID(delivery[:gln], schemeID: delivery[:gln_scheme]) if delivery[:gln].present? && delivery[:gln_scheme].present? # Identifiant global du lieu de livraison (BT-71) (optionnel) | schemeID -> 0088: GLN (GS1), 0002: SIRENE (France), 9906: SIRET, 9915: TVA intracom FR, 0060: DUNS
130
+ xml['ram'].GlobalID(delivery[:gln], schemeID: delivery[:gln_scheme]) if delivery[:gln].present? && delivery[:gln_scheme].present? # Global identifier of the delivery location (BT-71) | Scheme identifiers: 0088: GLN (GS1), 0002: SIRENE (France), 9906: SIRET, 9915: EU VAT number (FR), 0060: DUNS
264
131
 
265
132
  if delivery[:address].present?
266
133
  address = delivery[:address]
267
134
  xml['ram'].PostalTradeAddress do
268
- xml['ram'].PostcodeCode(address[:postcode]) if address[:postcode].present? # Code postal
269
- xml['ram'].LineOne(address[:line1]) if address[:line1].present? # Rue
270
- xml['ram'].LineTwo(address[:line2]) if address[:line2].present? # Complément
271
- xml['ram'].CityName(address[:city]) if address[:city].present? # Ville
272
- xml['ram'].CountryID(address[:country]) if address[:country].present? # Code pays
135
+ xml['ram'].PostcodeCode(address[:postcode]) if address[:postcode].present? # Delivery postal code
136
+ xml['ram'].LineOne(address[:line1]) if address[:line1].present? # Street address
137
+ xml['ram'].LineTwo(address[:line2]) if address[:line2].present? # Address complement
138
+ xml['ram'].CityName(address[:city]) if address[:city].present? # City
139
+ xml['ram'].CountryID(address[:country]) if address[:country].present? # Country code
273
140
  end
274
141
  end
275
142
  end
276
143
  if delivery[:date].present?
277
144
  xml['ram'].ActualDeliverySupplyChainEvent do
278
145
  xml['ram'].OccurrenceDateTime do
279
- xml['udt'].DateTimeString(delivery[:date], format: "102") # Date effective de livraison, format YYYYMMDD (BT-72)
146
+ xml['udt'].DateTimeString(delivery[:date], format: "102") # Actual delivery date in YYYYMMDD format (BT-72)
280
147
  end
281
148
  end
282
149
  end
@@ -287,45 +154,45 @@ module UnovaFacturX
287
154
  xml['ram'].ApplicableHeaderTradeAgreement do
288
155
  xml['ram'].SellerTradeParty do
289
156
  seller = @document[:seller]
290
- xml['ram'].Name(seller[:name]) # Nom légal du vendeur (BT-27)
157
+ xml['ram'].Name(seller[:name]) # Seller legal name (BT-27)
291
158
  xml['ram'].SpecifiedLegalOrganization do
292
- xml['ram'].ID(seller[:legal_id]) # Identifiant légal du vendeur (SIREN/SIRET ou équivalent) (BT-30)
159
+ xml['ram'].ID(seller[:legal_id]) # Seller legal identifier (SIREN/SIRET or equivalent) (BT-30)
293
160
  end
294
161
  xml['ram'].PostalTradeAddress do
295
162
  address = seller[:address]
296
- xml['ram'].PostcodeCode(address[:postcode]) # Code postal
297
- xml['ram'].LineOne(address[:line1]) # Rue
298
- xml['ram'].LineTwo(address[:line2]) if address[:line2].present? # Complément
299
- xml['ram'].CityName(address[:city]) # Ville
300
- xml['ram'].CountryID(address[:country]) # Code pays
163
+ xml['ram'].PostcodeCode(address[:postcode]) # Postal code
164
+ xml['ram'].LineOne(address[:line1]) # Street address
165
+ xml['ram'].LineTwo(address[:line2]) if address[:line2].present? # Address complement
166
+ xml['ram'].CityName(address[:city]) # City
167
+ xml['ram'].CountryID(address[:country]) # Country code
301
168
  end
302
169
  if seller[:vat_number].present?
303
170
  xml['ram'].SpecifiedTaxRegistration do
304
- xml['ram'].ID(seller[:vat_number], schemeID: "VA") # Numéro TVA vendeur (BT-31)
171
+ xml['ram'].ID(seller[:vat_number], schemeID: "VA") # Seller VAT number (BT-31)
305
172
  end
306
173
  end
307
174
  end
308
175
  xml['ram'].BuyerTradeParty do
309
176
  buyer = @document[:buyer]
310
- xml['ram'].ID(buyer[:id]) # Identifiant du client (BT-46)
311
- xml['ram'].Name(buyer[:name]) # Nom légal de l'entreprise cliente (BT-44)
177
+ xml['ram'].ID(buyer[:id]) # Buyer identifier (BT-46)
178
+ xml['ram'].Name(buyer[:name]) # Buyer legal name (BT-44)
312
179
  if buyer[:contact].present?
313
180
  contact = buyer[:contact]
314
181
  xml['ram'].DefinedTradeContact do
315
- xml['ram'].PersonName(contact[:name]) # Nom du contact chez le client (BT-56) (optionnel)
182
+ xml['ram'].PersonName(contact[:name]) # Contact name for the buyer (BT-56)
316
183
  end
317
184
  end
318
185
  xml['ram'].PostalTradeAddress do
319
186
  address = buyer[:address]
320
- xml['ram'].PostcodeCode(address[:postcode]) # Code postal
321
- xml['ram'].LineOne(address[:line1]) # Rue
322
- xml['ram'].LineTwo(address[:line2]) if address[:line2].present? # Complément
323
- xml['ram'].CityName(address[:city]) # Ville
324
- xml['ram'].CountryID(address[:country]) # Code pays
187
+ xml['ram'].PostcodeCode(address[:postcode]) # Buyer postal code
188
+ xml['ram'].LineOne(address[:line1]) # Street address
189
+ xml['ram'].LineTwo(address[:line2]) if address[:line2].present? # Address complement
190
+ xml['ram'].CityName(address[:city]) # City
191
+ xml['ram'].CountryID(address[:country]) # Country code
325
192
  end
326
193
  if buyer[:vat_number].present?
327
194
  xml['ram'].SpecifiedTaxRegistration do
328
- xml['ram'].ID(buyer[:vat_number], schemeID: "VA") # Numéro TVA vendeur (BT-48)
195
+ xml['ram'].ID(buyer[:vat_number], schemeID: "VA") # Buyer VAT number (BT-48)
329
196
  end
330
197
  end
331
198
  end
@@ -335,25 +202,25 @@ module UnovaFacturX
335
202
  def build_applicable_header_trade_settlement(xml)
336
203
  xml['ram'].ApplicableHeaderTradeSettlement do
337
204
  payment = @document[:payment_means]
338
- # xml['ram'].PaymentReference("XXXX") # Référence de paiement (ex : référence virement) (BT-83)
339
- xml['ram'].InvoiceCurrencyCode(@devise)
205
+ # xml['ram'].PaymentReference("XXXX") # Payment reference (e.g. bank transfer reference) (BT-83)
206
+ xml['ram'].InvoiceCurrencyCode(@currency) # Invoice currency code
340
207
  xml['ram'].SpecifiedTradeSettlementPaymentMeans do
341
- xml['ram'].TypeCode(payment[:type_code]) # Code moyen de paiement UNCL 4461 (BT-81)
342
- if %w[30 49].include?(payment[:type_code].to_s) && payment[:iban].present? # 30 : Virement, 49 : Prélèvement automatique
208
+ xml['ram'].TypeCode(payment[:type_code]) # Payment method code UNCL 4461 (BT-81)
209
+ if %w[30 49].include?(payment[:type_code].to_s) && payment[:iban].present? # 30: Bank transfer, 49: Direct debit
343
210
  xml['ram'].PayeePartyCreditorFinancialAccount do
344
- xml['ram'].IBANID(payment[:iban]) # IBAN du compte bénéficiaire (BT-84)
211
+ xml['ram'].IBANID(payment[:iban]) # Payee IBAN (BT-84)
345
212
  end
346
213
  end
347
214
  end
348
215
  @document[:vat_breakdown].each do |vat|
349
216
  xml['ram'].ApplicableTradeTax do
350
- xml['ram'].CalculatedAmount(vat[:tax_amount]) # Montant total de TVA pour le taux concerné (BT-117)
351
- xml['ram'].TypeCode("VAT") # TVA
352
- xml['ram'].ExemptionReason(vat[:exemption_reason]) if vat[:vat_category] == "E" && vat[:exemption_reason].present? # Motif d'exonération de la TVA (BT-120)
353
- xml['ram'].ExemptionReasonCode(vat[:exemption_reason_code]) if vat[:vat_category] == "E" && vat[:exemption_reason_code].present? # Code de motif d'exonération de la TVA (BT-121)
354
- xml['ram'].BasisAmount(vat[:taxable_amount]) # Base taxable HT pour le taux concerné (BT-116)
355
- xml['ram'].CategoryCode(vat[:vat_category]) # Code catégorie TVA du breakdown (BT-118). Une valeur parmi : {'A', 'AA', 'AB', 'AC', 'AD', 'AE', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'O', 'S', 'Z'}
356
- xml['ram'].RateApplicablePercent(vat[:vat_rate]) # Taux de TVA appliqué (%) (BT-119)
217
+ xml['ram'].CalculatedAmount(vat[:tax_amount]) # VAT calculated amount for the given rate (BT-117)
218
+ xml['ram'].TypeCode("VAT") # VAT type (always "VAT" in the EU)
219
+ xml['ram'].ExemptionReason(vat[:exemption_reason]) if vat[:vat_category] == "E" && vat[:exemption_reason].present? # VAT exemption reason (BT-120)
220
+ xml['ram'].ExemptionReasonCode(vat[:exemption_reason_code]) if vat[:vat_category] == "E" && vat[:exemption_reason_code].present? # VAT exemption reason code (BT-121)
221
+ xml['ram'].BasisAmount(vat[:taxable_amount]) # Taxable base amount for the VAT rate (BT-116)
222
+ xml['ram'].CategoryCode(vat[:vat_category]) # VAT category code (BT-118). One among these : {'A', 'AA', 'AB', 'AC', 'AD', 'AE', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'O', 'S', 'Z'}
223
+ xml['ram'].RateApplicablePercent(vat[:vat_rate]) # VAT rate applied (%) (BT-119)
357
224
  end
358
225
  end
359
226
 
@@ -361,19 +228,19 @@ module UnovaFacturX
361
228
  @document[:discount].each do |discount|
362
229
  xml['ram'].SpecifiedTradeAllowanceCharge do
363
230
  xml['ram'].ChargeIndicator do
364
- xml['udt'].Indicator("false") # true = charge (frais supplémentaires), false = allowance (réduction/remise)
231
+ xml['udt'].Indicator("false") # true = charge (additional fees), false = allowance (discount/allowance)
365
232
  end
366
- xml['ram'].CalculationPercent(discount[:percentage]) if discount[:percentage].present? # % de remise au niveau du document si la remise est en % (BT-94)
367
- xml['ram'].ActualAmount(discount[:total_amount]) # Montant de la remise au niveau du document pour le taux TVA concerné (BT-92)
368
- xml['ram'].Reason(discount[:reason]) if discount[:reason].present? # Motif de la remise au niveau du document (BT-97)
369
- xml['ram'].ReasonCode(discount[:reason_code]) if discount[:reason_code].present? # Code de motif de la remise au niveau du document (BT-98)
370
- # xml['ram'].BasisAmount(discount[:base]) # Base sur laquelle la remise est appliquée au niveau du document pour le taux TVA concerné (BT-93)
233
+ xml['ram'].CalculationPercent(discount[:percentage]) if discount[:percentage].present? # Discount percentage at document level (BT-94)
234
+ xml['ram'].ActualAmount(discount[:total_amount]) # Discount amount at document level for VAT rate (BT-92)
235
+ xml['ram'].Reason(discount[:reason]) if discount[:reason].present? # Reason for document-level discount (BT-97)
236
+ xml['ram'].ReasonCode(discount[:reason_code]) if discount[:reason_code].present? # Reason code for document-level discount (BT-98)
237
+ # xml['ram'].BasisAmount(discount[:base]) # Base amount used for discount calculation (BT-93)
371
238
  xml['ram'].CategoryTradeTax do
372
- xml['ram'].TypeCode("VAT") # Code de type de TVA de la remise (BT-95)
373
- xml['ram'].CategoryCode(discount[:vat_category]) # Code catégorie TVA (BT-118). Une valeur parmi : {'A', 'AA', 'AB', 'AC', 'AD', 'AE', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'O', 'S', 'Z'}
374
- xml['ram'].RateApplicablePercent(discount[:vat_rate]) # Taux de TVA appliqué (%) (BT-119)
239
+ xml['ram'].TypeCode("VAT") # VAT type (always "VAT" in the EU) (BT-95)
240
+ xml['ram'].CategoryCode(discount[:vat_category]) # VAT category for discount (BT-118). One among these : {'A', 'AA', 'AB', 'AC', 'AD', 'AE', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'O', 'S', 'Z'}
241
+ xml['ram'].RateApplicablePercent(discount[:vat_rate]) # VAT rate applied to discount (BT-119)
375
242
  end
376
- # xml['ram'].RateApplicablePercent("0.00") # Taux de TVA de la remise au niveau du document (BT-96)
243
+ # xml['ram'].RateApplicablePercent("0.00") # VAT rate for the document-level discount (BT-96)
377
244
  end
378
245
  end
379
246
  end
@@ -386,16 +253,16 @@ module UnovaFacturX
386
253
  xml['udt'].DateTimeString(totals[:due_date], format: "102") # Payment due date (BT-9)
387
254
  end
388
255
  end
389
- xml['ram'].Description(totals[:description]) if totals[:description].present? # Payment terms (BT-20)
256
+ xml['ram'].Description(totals[:description]) if totals[:description].present? # Payment terms description (BT-20)
390
257
  end
391
258
  end
392
259
  xml['ram'].SpecifiedTradeSettlementHeaderMonetarySummation do
393
- xml['ram'].LineTotalAmount(totals[:line_total_ht]) # Total HT des lignes sans discounts (BT-106)
394
- xml['ram'].AllowanceTotalAmount(totals[:total_discount]) if @document[:discount].present? && totals[:total_discount].present? # Somme des remises au niveau du document (BT-107)
395
- xml['ram'].TaxBasisTotalAmount(totals[:tax_basis_total_ht]) # Total des bases taxables HT (avec discounts) (BT-109)
396
- xml['ram'].TaxTotalAmount(totals[:tax_total], currencyID: @devise) # Montant total de TVA (BT-110)
397
- xml['ram'].GrandTotalAmount(totals[:grand_total_ttc]) # Total TTC (BT-112)
398
- xml['ram'].DuePayableAmount(totals[:amount_due]) if totals[:amount_due].present? # Montant restant (BT-115)
260
+ xml['ram'].LineTotalAmount(totals[:line_total_ht]) # Total line amount excluding VAT (BT-106)
261
+ xml['ram'].AllowanceTotalAmount(totals[:total_discount]) if @document[:discount].present? && totals[:total_discount].present? # Total document-level discounts (BT-107)
262
+ xml['ram'].TaxBasisTotalAmount(totals[:tax_basis_total_ht]) # Total taxable base amount (BT-109)
263
+ xml['ram'].TaxTotalAmount(totals[:tax_total], currencyID: @currency) # Total VAT amount (BT-110)
264
+ xml['ram'].GrandTotalAmount(totals[:grand_total_ttc]) # Total amount including VAT (BT-112)
265
+ xml['ram'].DuePayableAmount(totals[:amount_due]) if totals[:amount_due].present? # Amount remaining to be paid (BT-115)
399
266
  end
400
267
  end
401
268
  end
@@ -415,7 +282,7 @@ module UnovaFacturX
415
282
  )
416
283
  classpath = "#{jar_path}:#{resolver_path}"
417
284
 
418
- # Écrit le XML dans un fichier temp
285
+ # Writes the XML to a temporary file.
419
286
  tmp_xml = Tempfile.new(['invoice', '.xml'])
420
287
  tmp_xml.write(doc.to_xml)
421
288
  tmp_xml.flush
@@ -429,7 +296,7 @@ module UnovaFacturX
429
296
  tmp_xml.close
430
297
  tmp_xml.unlink
431
298
 
432
- raise "Java non disponible" unless status.exitstatus != 127
299
+ raise "Java not available" if status.exitstatus == 127
433
300
 
434
301
  report = ::Nokogiri::XML(stdout)
435
302
 
@@ -445,7 +312,7 @@ module UnovaFacturX
445
312
  e.at_xpath('svrl:text', svrl_namespace)&.text&.strip
446
313
  end
447
314
 
448
- raise "EN16931 validation échouée :\n#{messages.join("\n")}"
315
+ raise "EN16931 validation failed:\n#{messages.join("\n")}"
449
316
  end
450
317
  end
451
318
  end
@@ -10,13 +10,13 @@ require_relative "unova_factur_x/factur_x_generator"
10
10
  module UnovaFacturX
11
11
  class Error < StandardError; end
12
12
 
13
- def self.generate(pdf:, document_hash:, type: :invoice, with_validations: true, devise: "EUR")
13
+ def self.generate(pdf:, document_hash:, type: :invoice, with_validations: true, currency: "EUR")
14
14
  unless %i[invoice credit].include?(type)
15
15
  raise ArgumentError, "Type must be :invoice or :credit (default is :invoice)"
16
16
  end
17
17
 
18
18
  # Génération du XML à partir du hash en entré
19
- xml = UnovaFacturX::XmlGenerator.new(document_hash, type: type, validate: with_validations, devise: devise).call
19
+ xml = UnovaFacturX::XmlGenerator.new(document_hash, type: type, validate: with_validations, currency: currency).call
20
20
 
21
21
  # Génération et retour du PDF FacturX à partir du PDF en entré et du XML généré
22
22
  UnovaFacturX::FacturXGenerator.new(pdf: pdf, xml: xml).call
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: unova_factur_x
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.1.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rodolphe Limousin
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-06-08 00:00:00.000000000 Z
11
+ date: 2026-06-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: hexapdf
@@ -38,8 +38,8 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '1.18'
41
- description: Prends en entré un PDF et un hash de ses données et retourne un PDF au
42
- format Factur-X
41
+ description: Takes a PDF and a data hash as input and returns a Factur-X-compliant
42
+ PDF
43
43
  email:
44
44
  - rodolphe.limousin@unova.fr
45
45
  executables: []
@@ -54,9 +54,9 @@ files:
54
54
  - CHANGELOG.md
55
55
  - LICENSE.txt
56
56
  - LICENSES/MPL-2.0.txt
57
- - LOGICIELS_TIER.txt
58
57
  - README.md
59
58
  - Rakefile
59
+ - THIRD-PARTY_COMPONENTS.txt
60
60
  - lib/unova_factur_x.rb
61
61
  - lib/unova_factur_x/factur_x_generator.rb
62
62
  - lib/unova_factur_x/java/Saxon-HE-12.4.jar
@@ -67,7 +67,7 @@ files:
67
67
  - sig/unova_factur_x.rbs
68
68
  homepage: https://github.com/unovafr/unova_factur_x
69
69
  licenses:
70
- - MIT
70
+ - Apache-2.0
71
71
  metadata:
72
72
  homepage_uri: https://github.com/unovafr/unova_factur_x
73
73
  source_code_uri: https://github.com/unovafr/unova_factur_x
@@ -89,5 +89,5 @@ requirements: []
89
89
  rubygems_version: 3.5.22
90
90
  signing_key:
91
91
  specification_version: 4
92
- summary: Génération de factures/avoirs au format Factur-X
92
+ summary: Generation of invoices/credit notes in Factur-X format
93
93
  test_files: []
data/LOGICIELS_TIER.txt DELETED
@@ -1,29 +0,0 @@
1
- AVIS ET INFORMATIONS SUR LES LOGICIELS TIERS
2
-
3
- Ce projet inclut des composants logiciels tiers.
4
-
5
- ------------------------------------------------------------
6
- 1. Saxon-HE (processeur XSLT et XQuery)
7
- ------------------------------------------------------------
8
-
9
- Nom : Saxon-HE
10
- Version : 12.4
11
- Auteur : Saxonica Limited
12
- Site officiel : https://www.saxonica.com/
13
-
14
- Licence : Mozilla Public License 2.0 (MPL-2.0)
15
-
16
- Droits d’auteur :
17
- Copyright (c) Saxonica Limited
18
-
19
- Saxon-HE est inclus dans cette distribution sous forme de fichier JAR précompilé et reste soumis à sa licence d’origine.
20
-
21
- Le texte complet de la licence MPL-2.0 est disponible ici :
22
- https://www.mozilla.org/en-US/MPL/2.0/
23
-
24
- Une copie de la licence MPL-2.0 est incluse dans ce projet :
25
- LICENSES/MPL-2.0.txt
26
-
27
- ------------------------------------------------------------
28
- FIN DES AVIS
29
- ------------------------------------------------------------