einvoicing 0.5.0 → 0.6.0
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/config/locales/einvoicing.en.yml +6 -0
- data/config/locales/einvoicing.fr.yml +6 -0
- data/lib/einvoicing/formats/facturx.rb +3 -3
- data/lib/einvoicing/i18n.rb +18 -7
- data/lib/einvoicing/invoice.rb +1 -1
- data/lib/einvoicing/party.rb +4 -4
- data/lib/einvoicing/tax.rb +1 -1
- data/lib/einvoicing/validators/fr.rb +3 -3
- data/lib/einvoicing/validators/peppol.rb +4 -4
- data/lib/einvoicing/version.rb +1 -1
- data/lib/einvoicing/xml_builder.rb +1 -1
- data/lib/einvoicing.rb +3 -3
- metadata +45 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 24047fa12d3baaca75ebea76468a98ee023e5716bdee7dfa90f7f532e13ab5d1
|
|
4
|
+
data.tar.gz: 934ca40e5c588c9593d39b839e13eabd413b34b3f36f56d5481e99dddb5a1b69
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 54accf1d18eb7e32a35aba28d1c5c9f362eb1de5f19025b89c784724285ce59535394c603b9272a678a8bda7b1b13ae6f6a4dfca949528a303cf41a070681c43
|
|
7
|
+
data.tar.gz: 2eebc04105e663f581c83c6e81523aceba000188f0fc4b00c15138094d7659008bf59ce98b7afa5f867d875e307b806dbbea5281824d7d8c3eb11659976f7e86
|
|
@@ -25,3 +25,9 @@ en:
|
|
|
25
25
|
quantity_invalid: "Line %{index}: quantity must be positive"
|
|
26
26
|
unit_price_invalid: "Line %{index}: unit price must be non-negative"
|
|
27
27
|
vat_rate_invalid: "Line %{index}: VAT rate must be a known French rate (0%%, 5.5%%, 10%%, 20%%)"
|
|
28
|
+
formats:
|
|
29
|
+
unknown_format: "Unknown format: %{fmt}. Use :cii or :ubl"
|
|
30
|
+
unknown_market: "Unknown market: %{market}. Use :fr"
|
|
31
|
+
invalid_pdf: "pdf_data does not appear to be a valid PDF (missing %PDF- magic bytes)"
|
|
32
|
+
tax:
|
|
33
|
+
invalid_rate: "rate must be >= 0, got %{rate}"
|
|
@@ -25,3 +25,9 @@ fr:
|
|
|
25
25
|
quantity_invalid: "Ligne %{index} : la quantité doit être positive"
|
|
26
26
|
unit_price_invalid: "Ligne %{index} : le prix unitaire doit être non négatif"
|
|
27
27
|
vat_rate_invalid: "Ligne %{index} : le taux de TVA doit être un taux français standard (0%%, 5,5%%, 10%%, 20%%)"
|
|
28
|
+
formats:
|
|
29
|
+
unknown_format: "Format inconnu : %{fmt}. Utilisez :cii ou :ubl"
|
|
30
|
+
unknown_market: "Marché inconnu : %{market}. Utilisez :fr"
|
|
31
|
+
invalid_pdf: "pdf_data ne semble pas être un PDF valide (octets magiques %PDF- manquants)"
|
|
32
|
+
tax:
|
|
33
|
+
invalid_rate: "le taux doit être >= 0, valeur reçue : %{rate}"
|
|
@@ -31,7 +31,7 @@ module Einvoicing
|
|
|
31
31
|
# @return [String] binary Factur-X PDF/A-3 content
|
|
32
32
|
def self.embed(pdf_data, xml_string, profile: CONFORMANCE)
|
|
33
33
|
unless pdf_data.to_s.b.start_with?("%PDF-")
|
|
34
|
-
raise ArgumentError, "
|
|
34
|
+
raise ArgumentError, Einvoicing::I18n.t("formats.invalid_pdf")
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
require "hexapdf"
|
|
@@ -66,7 +66,7 @@ module Einvoicing
|
|
|
66
66
|
names_dict[:EmbeddedFiles][:Names] << FILENAME << filespec
|
|
67
67
|
|
|
68
68
|
# 3. Set AF array on the catalog.
|
|
69
|
-
doc.catalog[:AF] = [filespec]
|
|
69
|
+
doc.catalog[:AF] = [ filespec ]
|
|
70
70
|
|
|
71
71
|
# 4. Add OutputIntent (required for PDF/A-3 conformance).
|
|
72
72
|
add_output_intent(doc)
|
|
@@ -183,7 +183,7 @@ module Einvoicing
|
|
|
183
183
|
DestOutputProfile: icc_stream
|
|
184
184
|
})
|
|
185
185
|
|
|
186
|
-
doc.catalog[:OutputIntents] = [output_intent]
|
|
186
|
+
doc.catalog[:OutputIntents] = [ output_intent ]
|
|
187
187
|
end
|
|
188
188
|
|
|
189
189
|
private_class_method def self.md5(bytes)
|
data/lib/einvoicing/i18n.rb
CHANGED
|
@@ -1,22 +1,33 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "i18n"
|
|
4
|
+
|
|
3
5
|
module Einvoicing
|
|
4
|
-
# Thin wrapper around ::I18n
|
|
5
|
-
#
|
|
6
|
-
#
|
|
6
|
+
# Thin wrapper around ::I18n for gem-internal translations.
|
|
7
|
+
# Loads the gem's own locale files on setup; in Rails apps the engine
|
|
8
|
+
# already handles the load_path, so duplicates are skipped.
|
|
7
9
|
module I18n
|
|
8
10
|
DEFAULT_LOCALE = :en
|
|
11
|
+
LOCALES_PATH = File.expand_path("../../config/locales", __dir__)
|
|
9
12
|
|
|
10
|
-
def self.
|
|
11
|
-
|
|
13
|
+
def self.setup
|
|
14
|
+
locale_files = Dir[File.join(LOCALES_PATH, "*.yml")]
|
|
15
|
+
new_files = locale_files - ::I18n.load_path
|
|
16
|
+
return if new_files.empty?
|
|
17
|
+
|
|
18
|
+
::I18n.load_path += new_files
|
|
19
|
+
::I18n.backend.load_translations
|
|
20
|
+
end
|
|
12
21
|
|
|
13
|
-
|
|
22
|
+
def self.t(key, **options)
|
|
23
|
+
locale = options.delete(:locale) { ::I18n.locale }
|
|
14
24
|
::I18n.t("einvoicing.#{key}", locale: locale, **options)
|
|
15
25
|
rescue ::I18n::MissingTranslationData
|
|
16
|
-
# Fallback to English if translation missing in current locale
|
|
17
26
|
::I18n.t("einvoicing.#{key}", locale: DEFAULT_LOCALE, **options)
|
|
18
27
|
rescue StandardError
|
|
19
28
|
key.to_s
|
|
20
29
|
end
|
|
21
30
|
end
|
|
22
31
|
end
|
|
32
|
+
|
|
33
|
+
Einvoicing::I18n.setup
|
data/lib/einvoicing/invoice.rb
CHANGED
|
@@ -88,7 +88,7 @@ module Einvoicing
|
|
|
88
88
|
private
|
|
89
89
|
|
|
90
90
|
def compute_tax_breakdown(lines)
|
|
91
|
-
grouped = lines.group_by { |l| [l.vat_rate, l.category] }
|
|
91
|
+
grouped = lines.group_by { |l| [ l.vat_rate, l.category ] }
|
|
92
92
|
grouped.map do |(rate, category), rate_lines|
|
|
93
93
|
taxable = rate_lines.sum(BigDecimal("0"), &:net_amount).round(2, :half_up)
|
|
94
94
|
tax_amt = rate_lines.sum(BigDecimal("0"), &:vat_amount).round(2, :half_up)
|
data/lib/einvoicing/party.rb
CHANGED
|
@@ -27,13 +27,13 @@ module Einvoicing
|
|
|
27
27
|
resolved_endpoint_id = endpoint_id || siret || email
|
|
28
28
|
resolved_endpoint_scheme = if endpoint_scheme
|
|
29
29
|
endpoint_scheme
|
|
30
|
-
|
|
30
|
+
elsif endpoint_id
|
|
31
31
|
nil # caller must supply scheme when explicit
|
|
32
|
-
|
|
32
|
+
elsif siret
|
|
33
33
|
"0002" # SIRET scheme — Peppol EAS FR standard
|
|
34
|
-
|
|
34
|
+
elsif email
|
|
35
35
|
"EM" # email fallback (not in Peppol EAS, use for non-Peppol)
|
|
36
|
-
|
|
36
|
+
end
|
|
37
37
|
super(name: name, street: street, city: city, postal_code: postal_code,
|
|
38
38
|
country_code: country_code, siren: siren, siret: siret,
|
|
39
39
|
vat_number: vat_number, email: email,
|
data/lib/einvoicing/tax.rb
CHANGED
|
@@ -11,7 +11,7 @@ module Einvoicing
|
|
|
11
11
|
# @param tax_amount [Numeric] VAT amount for this rate
|
|
12
12
|
# @param category [Symbol, nil] nil for standard/zero, :reverse_charge for AE
|
|
13
13
|
def initialize(rate:, taxable_amount:, tax_amount:, category: nil)
|
|
14
|
-
raise ArgumentError, "
|
|
14
|
+
raise ArgumentError, Einvoicing::I18n.t("tax.invalid_rate", rate: rate) if rate.to_f.negative?
|
|
15
15
|
|
|
16
16
|
super
|
|
17
17
|
end
|
|
@@ -159,8 +159,8 @@ module Einvoicing
|
|
|
159
159
|
|
|
160
160
|
def self.validate_lines(lines)
|
|
161
161
|
if lines.nil? || lines.empty?
|
|
162
|
-
return [{ field: :lines, error: :lines_empty,
|
|
163
|
-
message: Einvoicing::I18n.t("errors.invoice.lines_empty") }]
|
|
162
|
+
return [ { field: :lines, error: :lines_empty,
|
|
163
|
+
message: Einvoicing::I18n.t("errors.invoice.lines_empty") } ]
|
|
164
164
|
end
|
|
165
165
|
|
|
166
166
|
lines.each_with_index.flat_map do |line, idx|
|
|
@@ -178,7 +178,7 @@ module Einvoicing
|
|
|
178
178
|
{ field: :"line_#{n}_unit_price", error: :unit_price_invalid,
|
|
179
179
|
message: Einvoicing::I18n.t("errors.line.unit_price_invalid", index: n) }
|
|
180
180
|
end),
|
|
181
|
-
(unless [0.0, 0.055, 0.10, 0.20].include?(line.vat_rate.to_f.round(3))
|
|
181
|
+
(unless [ 0.0, 0.055, 0.10, 0.20 ].include?(line.vat_rate.to_f.round(3))
|
|
182
182
|
{ field: :"line_#{n}_vat_rate", error: :vat_rate_invalid,
|
|
183
183
|
message: Einvoicing::I18n.t("errors.line.vat_rate_invalid", index: n) }
|
|
184
184
|
end)
|
|
@@ -86,18 +86,18 @@ module Einvoicing
|
|
|
86
86
|
private_class_method :download
|
|
87
87
|
|
|
88
88
|
def self.run_saxon(xml_string)
|
|
89
|
-
input = Tempfile.new(["peppol-input", ".xml"])
|
|
90
|
-
output = Tempfile.new(["peppol-output", ".svrl"])
|
|
89
|
+
input = Tempfile.new([ "peppol-input", ".xml" ])
|
|
90
|
+
output = Tempfile.new([ "peppol-output", ".svrl" ])
|
|
91
91
|
|
|
92
92
|
begin
|
|
93
93
|
input.write(xml_string)
|
|
94
94
|
input.close
|
|
95
95
|
output.close
|
|
96
96
|
|
|
97
|
-
cmd = ["java", "-jar", SAXON_JAR,
|
|
97
|
+
cmd = [ "java", "-jar", SAXON_JAR,
|
|
98
98
|
"-s:#{input.path}",
|
|
99
99
|
"-xsl:#{XSLT_PATH}",
|
|
100
|
-
"-o:#{output.path}"]
|
|
100
|
+
"-o:#{output.path}" ]
|
|
101
101
|
|
|
102
102
|
_, stderr, status = Open3.capture3(*cmd)
|
|
103
103
|
|
data/lib/einvoicing/version.rb
CHANGED
data/lib/einvoicing.rb
CHANGED
|
@@ -61,7 +61,7 @@ module Einvoicing
|
|
|
61
61
|
case format
|
|
62
62
|
when :cii then Formats::CII.generate(invoice)
|
|
63
63
|
when :ubl then Formats::UBL.generate(invoice)
|
|
64
|
-
else raise ArgumentError, "
|
|
64
|
+
else raise ArgumentError, Einvoicing::I18n.t("formats.unknown_format", fmt: format.inspect)
|
|
65
65
|
end
|
|
66
66
|
end
|
|
67
67
|
|
|
@@ -81,7 +81,7 @@ module Einvoicing
|
|
|
81
81
|
def self.validate(invoice, market: :fr)
|
|
82
82
|
case market
|
|
83
83
|
when :fr then Validators::FR.validate(invoice)
|
|
84
|
-
else raise ArgumentError, "
|
|
84
|
+
else raise ArgumentError, Einvoicing::I18n.t("formats.unknown_market", market: market.inspect)
|
|
85
85
|
end
|
|
86
86
|
end
|
|
87
87
|
|
|
@@ -98,6 +98,6 @@ module Einvoicing
|
|
|
98
98
|
pdf_out = pdf ? embed(pdf, xml_str) : nil
|
|
99
99
|
{ valid: errors.empty?, errors: errors, xml: xml_str, pdf: pdf_out }
|
|
100
100
|
rescue StandardError => e
|
|
101
|
-
{ valid: false, errors: [{ field: :unknown, error: :exception, message: e.message }], xml: nil, pdf: nil }
|
|
101
|
+
{ valid: false, errors: [ { field: :unknown, error: :exception, message: e.message } ], xml: nil, pdf: nil }
|
|
102
102
|
end
|
|
103
103
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: einvoicing
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Nathan Le Ray
|
|
@@ -24,6 +24,20 @@ dependencies:
|
|
|
24
24
|
- - "~>"
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
26
|
version: '1.0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: i18n
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '1.0'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '1.0'
|
|
27
41
|
- !ruby/object:Gem::Dependency
|
|
28
42
|
name: rspec
|
|
29
43
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -44,14 +58,42 @@ dependencies:
|
|
|
44
58
|
requirements:
|
|
45
59
|
- - "~>"
|
|
46
60
|
- !ruby/object:Gem::Version
|
|
47
|
-
version: '1.
|
|
61
|
+
version: '1.70'
|
|
48
62
|
type: :development
|
|
49
63
|
prerelease: false
|
|
50
64
|
version_requirements: !ruby/object:Gem::Requirement
|
|
51
65
|
requirements:
|
|
52
66
|
- - "~>"
|
|
53
67
|
- !ruby/object:Gem::Version
|
|
54
|
-
version: '1.
|
|
68
|
+
version: '1.70'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: rubocop-rails-omakase
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - ">="
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '0'
|
|
76
|
+
type: :development
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - ">="
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '0'
|
|
83
|
+
- !ruby/object:Gem::Dependency
|
|
84
|
+
name: rubocop-rspec
|
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - "~>"
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '3.0'
|
|
90
|
+
type: :development
|
|
91
|
+
prerelease: false
|
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - "~>"
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '3.0'
|
|
55
97
|
- !ruby/object:Gem::Dependency
|
|
56
98
|
name: nokogiri
|
|
57
99
|
requirement: !ruby/object:Gem::Requirement
|