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 +4 -4
- data/CHANGELOG.md +28 -0
- data/Gemfile.lock +1 -1
- data/README.md +70 -0
- data/lib/factur_x/codes.rb +8 -0
- data/lib/factur_x/party.rb +15 -0
- data/lib/factur_x/validation/rules.rb +38 -2
- data/lib/factur_x/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: aa8357e047314e65502e6f758ee4ec36cb9b58af3fbfd8b171cf0b4c69d1692a
|
|
4
|
+
data.tar.gz: f574b56e54c9e19768599b2092d4d38b3559df165289f6435d76db6d8fdc5bb8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
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
|
data/lib/factur_x/codes.rb
CHANGED
|
@@ -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"
|
data/lib/factur_x/party.rb
CHANGED
|
@@ -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)
|
|
188
|
-
"
|
|
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)
|
data/lib/factur_x/version.rb
CHANGED
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
|
+
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-
|
|
11
|
+
date: 2026-11-12 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: nokogiri
|