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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c103791ffce05b80340cbae5a1b09a00f4b5408edd8a70bfac567898be951ee9
4
- data.tar.gz: 7db7faa121b1e2ab57c6ed9d467602eef8771a61acd57d0ea28eb53050ad9301
3
+ metadata.gz: 24047fa12d3baaca75ebea76468a98ee023e5716bdee7dfa90f7f532e13ab5d1
4
+ data.tar.gz: 934ca40e5c588c9593d39b839e13eabd413b34b3f36f56d5481e99dddb5a1b69
5
5
  SHA512:
6
- metadata.gz: 5812428fa6b471a0cc5a34787e74fd473e3ffd5520226e736ce0fc6b144b054caf70c636f8b8b2a2acd834e4ff0d1f8db4bf47747b934e5dfa29e0d217333117
7
- data.tar.gz: 1cf58d2a1cc101430435611c5091510ec62e48d9c2d0e33e01a827853aecf92ed69d1a57b9cc20a1d245e97768014ff350b669bf51c928d9892b18ebb21c13e4
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, "pdf_data does not appear to be a valid PDF (missing %PDF- magic bytes)"
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)
@@ -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 with graceful fallback for standalone use.
5
- # When used outside Rails, ::I18n may not be available; in that case the
6
- # dotted key string is returned as-is.
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.t(key, **options)
11
- return key.to_s unless defined?(::I18n)
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
- locale = options.delete(:locale) { ::I18n.locale rescue DEFAULT_LOCALE }
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
@@ -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)
@@ -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
- elsif endpoint_id
30
+ elsif endpoint_id
31
31
  nil # caller must supply scheme when explicit
32
- elsif siret
32
+ elsif siret
33
33
  "0002" # SIRET scheme — Peppol EAS FR standard
34
- elsif email
34
+ elsif email
35
35
  "EM" # email fallback (not in Peppol EAS, use for non-Peppol)
36
- end
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,
@@ -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, "rate must be >= 0, got #{rate}" if rate.to_f.negative?
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
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Einvoicing
4
- VERSION = "0.5.0"
4
+ VERSION = "0.6.0"
5
5
  end
@@ -5,7 +5,7 @@ module Einvoicing
5
5
  # Produces indented XML with proper attribute and text escaping.
6
6
  class XMLBuilder
7
7
  def initialize
8
- @parts = ['<?xml version="1.0" encoding="UTF-8"?>']
8
+ @parts = [ '<?xml version="1.0" encoding="UTF-8"?>' ]
9
9
  @depth = 0
10
10
  end
11
11
 
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, "Unknown format: #{format.inspect}. Use :cii or :ubl"
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, "Unknown market: #{market.inspect}. Use :fr"
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.5.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.65'
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.65'
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