factur-x-builder 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: da7a9bafbb80d9e9ac88ee325e49b76c1855e91774f3a1bf1c4dacdcf679cdc9
4
- data.tar.gz: 729c3209aa5378bbde0b2400c797db122d887a34cb370d588c22e61df9a94d0b
3
+ metadata.gz: aa8357e047314e65502e6f758ee4ec36cb9b58af3fbfd8b171cf0b4c69d1692a
4
+ data.tar.gz: f574b56e54c9e19768599b2092d4d38b3559df165289f6435d76db6d8fdc5bb8
5
5
  SHA512:
6
- metadata.gz: bd23212eaeb87434500c7f45b41708b1bb9f72bea9f8c178a35953194d0503a41d939e0f36db9b24ae0ba1ace8bdb79035905e70516d0886f54c3af52da26b3a
7
- data.tar.gz: 362f6dfba5482d591a1131e866e29ffbf1300873fe2aa73841f65ce3e36b829a8b417c3376cf4fbfc5a2fca6e0038159103b372d9e4a3e10d8907fb1dea55a4c
6
+ metadata.gz: 4df9ad9574aeb30e5c34d6b790e64fc7997137d0947f7370c964e8dea902bfe86e2a022f50434e0b13f2ef96acf23227f08dbc5d5a24480a1e01daa4113c3481
7
+ data.tar.gz: 129bbecb17198fdb472cd294d27ed702bd5ec6486e7ff85cd81c73632b9f5b023830f80ae64a80a0553dbafff112bf4a37f4a889e8bb0d721d2eafb4490f4b11
data/CHANGELOG.md CHANGED
@@ -1,4 +1,23 @@
1
- ## [Unreleased]
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.
2
21
 
3
22
  ### Added
4
23
 
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- factur-x-builder (0.1.4)
4
+ factur-x-builder (0.1.5)
5
5
  nokogiri (~> 1.15, < 1.16)
6
6
 
7
7
  GEM
data/README.md CHANGED
@@ -151,6 +151,58 @@ FacturX::Invoice.new(
151
151
  )
152
152
  ```
153
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
+
154
206
  ### Routing address
155
207
 
156
208
  `routing_id` is the electronic address the platform delivers to (BT-34 for the seller, BT-49 for the
@@ -92,6 +92,9 @@ module FacturX
92
92
  GTIN = "0160"
93
93
  VAT = "VA"
94
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"
95
98
  end
96
99
 
97
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
@@ -138,6 +138,23 @@ module FacturX
138
138
 
139
139
  check_french_identifiers(invoice.seller, "seller")
140
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}")
141
158
  end
142
159
 
143
160
  def check_party(party, role)
@@ -199,8 +216,12 @@ module FacturX
199
216
  # conveys the direction, not the sign of the prices.
200
217
  def check_unit_prices(line, label)
201
218
  if negative?(line.unit_price)
202
- add("#{label}: unit price must not be negative (BT-146, BR-27) - a credit note uses " \
203
- "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")
204
225
  end
205
226
 
206
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.4"
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.4
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-08 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