factur-x-builder 0.1.3 → 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: 224858d0b676c38126632f450793b3ea65302d795ee6eba2f5fd34c532d7bc51
4
- data.tar.gz: 67e574e617b043c7e2e433b95c17193c31dcd98df49d0d454338773a2b461acc
3
+ metadata.gz: aa8357e047314e65502e6f758ee4ec36cb9b58af3fbfd8b171cf0b4c69d1692a
4
+ data.tar.gz: f574b56e54c9e19768599b2092d4d38b3559df165289f6435d76db6d8fdc5bb8
5
5
  SHA512:
6
- metadata.gz: 3a674455d7d5c036df7469dd489cd4d932912b1f0113a2aaecb48508a4f48abd4efd42174cdc525123f69e48b81193f8e052b8cbf2c72abe196f55f74d7c747d
7
- data.tar.gz: d283359e423f7e88dcfd0a88cd56721812ed6be147dc9de9ce39f5cc784740115531011a43941c4174673c3b1baf137e47d91e9a88cdb266af24af783139c7e0
6
+ metadata.gz: 4df9ad9574aeb30e5c34d6b790e64fc7997137d0947f7370c964e8dea902bfe86e2a022f50434e0b13f2ef96acf23227f08dbc5d5a24480a1e01daa4113c3481
7
+ data.tar.gz: 129bbecb17198fdb472cd294d27ed702bd5ec6486e7ff85cd81c73632b9f5b023830f80ae64a80a0553dbafff112bf4a37f4a889e8bb0d721d2eafb4490f4b11
data/CHANGELOG.md CHANGED
@@ -1,3 +1,31 @@
1
+ ## [0.1.5] - 2026-11-10
2
+
3
+ ### Added
4
+
5
+ - `Party.addressed_by_email` builds a party with no registry identifier, routed by email under
6
+ scheme `EM` (`Codes::Scheme::EMAIL`), for invoices exchanged directly rather than deposited on an
7
+ accredited platform. The seller is deliberately excluded: BR-CO-9 still requires it to identify
8
+ itself, though it may be addressed by email while keeping its identifiers.
9
+ - An email routing address declared under scheme `0225` is now rejected. That scheme designates a
10
+ French directory address, and no schematron checks the value against it, so a mislabelled address
11
+ would have gone out silently.
12
+ - Conformance coverage for invoicing mode `S2` on a settled invoice, for a due date (BT-9) that
13
+ precedes the issue date, for an email-addressed buyer, and for a VAT exemption carried by the
14
+ reason code (BT-121) rather than the reason text (BT-120) — BR-E-10 accepts either.
15
+
16
+ ### Fixed
17
+
18
+ - The BR-27 message on a negative unit price only mentioned credit notes, sending anyone trying to
19
+ bill a discount down the wrong path. It now names the two ways EN 16931 expresses a reduction —
20
+ a document allowance and a unit price discount — and keeps the credit note as the refund case.
21
+
22
+ ### Added
23
+
24
+ - A credit note is now checked against BR-FR-CO-05: it must reference the invoice it corrects with
25
+ both the number (BT-25) and the issue date (BT-26). A reference missing its date failed the
26
+ French schematron exactly like an absent one, and neither was caught before generating.
27
+ `Codes::CREDIT_NOTE_TYPES` names the document types the rule applies to.
28
+
1
29
  ## [0.1.2] - 2026-08-15
2
30
 
3
31
  ### Added
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- factur-x-builder (0.1.3)
4
+ factur-x-builder (0.1.5)
5
5
  nokogiri (~> 1.15, < 1.16)
6
6
 
7
7
  GEM
data/README.md CHANGED
@@ -133,6 +133,76 @@ anything is generated.
133
133
  A credit note carries **positive** amounts; the type code conveys the direction. Negative unit
134
134
  prices are rejected (`BR-27`, `BR-28`).
135
135
 
136
+ Cancelling invoice A with credit note B changes four things and nothing else — B's own number
137
+ (BT-1) and issue date (BT-2), the type code `381` (BT-3), and a reference to A carrying **both** its
138
+ number and its issue date (BG-3). That reference is required by the French profile (`BR-FR-CO-05`)
139
+ though not by EN 16931 alone, and a reference without a date fails just like a missing one. Parties,
140
+ lines, prices, VAT breakdown and totals are copied over untouched.
141
+
142
+ ```ruby
143
+ FacturX::Invoice.new(
144
+ number: "AV-2026-0012",
145
+ issued_on: Date.today,
146
+ type_code: FacturX::Codes::CREDIT_NOTE,
147
+ preceding_invoices: [
148
+ FacturX::ReferencedInvoice.new(number: "5-2026-0004", issued_on: Date.new(2026, 8, 4))
149
+ ],
150
+ # … everything else identical to the invoice being cancelled
151
+ )
152
+ ```
153
+
154
+ ### A party with no SIREN
155
+
156
+ For an invoice exchanged directly rather than deposited on an accredited platform — a private
157
+ individual, a foreign counterpart, any buyer whose SIREN is unknown — the buyer can be addressed by
158
+ email instead:
159
+
160
+ ```ruby
161
+ buyer = FacturX::Party.addressed_by_email(
162
+ name: "Client particulier",
163
+ email: "client@example.net",
164
+ address: FacturX::Address.new(line_one: "12 rue des Lilas", postcode: "69001",
165
+ city: "Lyon", country_code: "FR")
166
+ )
167
+ ```
168
+
169
+ No `GlobalID`, no `SpecifiedLegalOrganization`, no `SpecifiedTaxRegistration`; the routing address
170
+ becomes `<ram:URIID schemeID="EM">client@example.net</ram:URIID>`. Name and postal address stay —
171
+ BR-11 still requires the buyer country code.
172
+
173
+ The **seller** cannot do the same: it is liable for the VAT and BR-CO-9 requires an identifier. It
174
+ may still be *addressed* by email while keeping its SIRET, SIREN and VAT number — `addressed_by_email`
175
+ only changes the routing; dropping the identifiers is the separate choice of not passing them.
176
+
177
+ Scheme `0225` means "French directory address", so an email declared under it is mislabelled. No
178
+ schematron notices, so the gem rejects that combination itself.
179
+
180
+ ### Discounts
181
+
182
+ EN 16931 has no negative line. A reduction is an **allowance**, and a negative unit price is
183
+ rejected by BR-27. Two mechanisms, both covered by the conformance suite:
184
+
185
+ ```ruby
186
+ # On the whole invoice (BG-20) — reduces the taxable base
187
+ allowances: [
188
+ FacturX::AllowanceCharge.allowance(amount: 159.20, basis_amount: 1592.00, percentage: 10.0,
189
+ reason: "Remise commerciale 10 %",
190
+ vat_category: "S", vat_rate: 20.0)
191
+ ]
192
+
193
+ # Off a line's unit price (BT-147) — gross price, discount, net price billed
194
+ FacturX::Line.new(name: "Option spot TV",
195
+ gross_unit_price: 1290.00, unit_discount: 290.00, unit_price: 1000.00,
196
+ quantity: 1, vat_rate: 20.0, net_amount: 1000.00)
197
+ ```
198
+
199
+ A document allowance carries its own VAT category and rate, and is subtracted from the taxable base
200
+ of that rate — `line_total − allowance_total + charge_total = tax_basis_total` (BR-CO-13), checked
201
+ before generating.
202
+
203
+ Line level allowances (BG-27) are not implemented: a flat discount on a line is currently expressed
204
+ through the unit price.
205
+
136
206
  ### Routing address
137
207
 
138
208
  `routing_id` is the electronic address the platform delivers to (BT-34 for the seller, BT-49 for the
@@ -10,6 +10,11 @@ module FacturX
10
10
 
11
11
  DOCUMENT_TYPES = [COMMERCIAL_INVOICE, CREDIT_NOTE, SELF_BILLED_INVOICE].freeze
12
12
 
13
+ # The document types BR-FR-CO-05 treats as credit notes. Only 381 is
14
+ # emitted by this gem; the others are listed so the rule keeps matching
15
+ # if DOCUMENT_TYPES ever widens.
16
+ CREDIT_NOTE_TYPES = %w[261 381 396 502 503].freeze
17
+
13
18
  # Invoicing modes accepted by the French e-invoicing reform (BR-FR-08).
14
19
  BUSINESS_PROCESSES = %w[B1 S1 M1 B2 S2 M2 S3 B4 S4 M4 S5 S6 B7 S7 B8 S8 M8 B9 S9 M9].freeze
15
20
 
@@ -87,6 +92,9 @@ module FacturX
87
92
  GTIN = "0160"
88
93
  VAT = "VA"
89
94
  FISCAL = "FC"
95
+ # Addresses a party by email rather than by a directory identifier, for
96
+ # invoices that are not deposited on an accredited platform.
97
+ EMAIL = "EM"
90
98
  end
91
99
 
92
100
  PIECE_UNIT = "C62"
@@ -52,6 +52,21 @@ module FacturX
52
52
  @routing_id_scheme = routing_id_scheme || (Codes::Scheme::ROUTING_SIRET if @routing_id)
53
53
  end
54
54
 
55
+ # A party with no SIREN available, addressed by email instead (scheme EM).
56
+ # No GlobalID, no legal organization and no tax registration are emitted,
57
+ # which keeps the document valid for an invoice exchanged directly rather
58
+ # than deposited on an accredited platform.
59
+ def self.addressed_by_email(name:, email:, address: nil, contact: nil, **attributes)
60
+ new(name: name, address: address,
61
+ contact: contact || Contact.new(email: email),
62
+ routing_id: email, routing_id_scheme: Codes::Scheme::EMAIL,
63
+ **attributes)
64
+ end
65
+
66
+ def addressed_by_email?
67
+ routing_id_scheme == Codes::Scheme::EMAIL
68
+ end
69
+
55
70
  def contact?
56
71
  !contact.nil? && !contact.empty?
57
72
  end
@@ -91,6 +91,21 @@ module FacturX
91
91
  "notes (BT-22 with subject code #{code}, BR-FR-05) - " \
92
92
  "Note.french_legal_mentions provides the three of them")
93
93
  end
94
+
95
+ check_corrected_invoice
96
+ end
97
+
98
+ # BR-FR-CO-05: a credit note must say which invoice it corrects. EN 16931
99
+ # alone is happy without it, the French profile is not. The number and the
100
+ # issue date are both required - a reference carrying only the number
101
+ # fails the rule just as an absent one does.
102
+ def check_corrected_invoice
103
+ return unless Codes::CREDIT_NOTE_TYPES.include?(invoice.type_code)
104
+ return if invoice.preceding_invoices.any? { |ref| !blank?(ref.number) && !ref.issued_on.nil? }
105
+
106
+ add("a credit note must reference the invoice it corrects, with both its number and " \
107
+ "its issue date (BT-25 and BT-26, BR-FR-CO-05) - pass preceding_invoices: " \
108
+ "[ReferencedInvoice.new(number:, issued_on:)]")
94
109
  end
95
110
 
96
111
  def check_document
@@ -123,6 +138,23 @@ module FacturX
123
138
 
124
139
  check_french_identifiers(invoice.seller, "seller")
125
140
  check_french_identifiers(invoice.buyer, "buyer")
141
+ check_routing_address(invoice.seller, "seller")
142
+ check_routing_address(invoice.buyer, "buyer")
143
+ end
144
+
145
+ # Scheme 0225 designates a French directory address, so an email carrying
146
+ # it is mislabelled. Neither schematron notices - they only check that the
147
+ # scheme code exists - which is why this is worth catching here. Pass
148
+ # Party.addressed_by_email, or routing_id_scheme: Codes::Scheme::EMAIL.
149
+ def check_routing_address(party, role)
150
+ return if party.nil? || party.routing_id.nil?
151
+ return unless party.routing_id.to_s.include?("@")
152
+ return unless party.routing_id_scheme == Codes::Scheme::ROUTING_SIRET
153
+
154
+ add("#{role} routing address #{party.routing_id.inspect} is an email address but is " \
155
+ "declared under scheme 0225, which designates a French directory address " \
156
+ "(BT-34-1 / BT-49-1) - use Party.addressed_by_email or scheme " \
157
+ "#{Codes::Scheme::EMAIL}")
126
158
  end
127
159
 
128
160
  def check_party(party, role)
@@ -184,8 +216,12 @@ module FacturX
184
216
  # conveys the direction, not the sign of the prices.
185
217
  def check_unit_prices(line, label)
186
218
  if negative?(line.unit_price)
187
- add("#{label}: unit price must not be negative (BT-146, BR-27) - a credit note uses " \
188
- "type code #{Codes::CREDIT_NOTE} with positive amounts")
219
+ add("#{label}: unit price must not be negative (BT-146, BR-27). EN 16931 expresses a " \
220
+ "reduction as an allowance, never as a negative line: use " \
221
+ "AllowanceCharge.allowance in `allowances` for a discount on the whole invoice, " \
222
+ "or gross_unit_price with unit_discount on the line for a discount off the unit " \
223
+ "price. A refund of a whole invoice is a credit note instead (type code " \
224
+ "#{Codes::CREDIT_NOTE}), also with positive amounts")
189
225
  end
190
226
 
191
227
  return unless negative?(line.gross_unit_price)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FacturX
4
- VERSION = "0.1.3"
4
+ VERSION = "0.1.5"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: factur-x-builder
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.1.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hotentic
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-09-03 00:00:00.000000000 Z
11
+ date: 2026-11-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: nokogiri