xrechnung 0.6.5 → 0.7.2

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: dcb000b6c51385ec97931308733407e6400186d86a632db4a30d79adab353bda
4
- data.tar.gz: e94c31a304cda4d231b3adb69b6cd331cb50616f2655c68e369f7ebc3824dea4
3
+ metadata.gz: bf7b2b0425ffb15eefdf3c311b4880aa2f55ad7c080f15ae853f597451f175fa
4
+ data.tar.gz: b25e7331e0c5a19ead78358dda7d6fe600d7463531d5c375f5749c3aba455d30
5
5
  SHA512:
6
- metadata.gz: d948ae1c1ea11cec41beb17eccd81b59183995b6015a78303dd8a4adf004ef4c713c040b918002bfd3fdafaa6bdd5d309b102b1e1ffbe1138cc1a8442ce25580
7
- data.tar.gz: a44907195bf5a95cf8154fc743e225046c1f9b5c2ab29671c03175f69ea6b4cb37928a67602e3c68d8d9913bef5374f77f0a8e942e40547546a9bf458986bcb0
6
+ metadata.gz: b8cf3e9c4b1e05ff361597ab9b7fd0fde10d1b3dfcf308111c0b3fb38eff48ec3d65fb050d8f799d2890b75c7fcb3543f3424b2b25f6a47fde38beb2bac5da54
7
+ data.tar.gz: fad567481b7393fe3fb7ee28c09006ca11779080a4681cf2f9d4fe487c745fdaf08cb4b764696e2a4b28f1bac7351b0f598c2b69579835eea505030585f8a9db
@@ -8,7 +8,7 @@ jobs:
8
8
  name: Ruby ${{ matrix.ruby }}
9
9
  strategy:
10
10
  matrix:
11
- ruby: ['3.2', '3.3', '3.4']
11
+ ruby: ['3.2', '3.3', '3.4', '4.0']
12
12
 
13
13
  steps:
14
14
  - uses: actions/checkout@v6
@@ -11,12 +11,12 @@ jobs:
11
11
 
12
12
  - uses: actions/setup-java@v5
13
13
  with:
14
- distribution: "adopt"
15
- java-version: "17"
14
+ distribution: "temurin"
15
+ java-version: "25"
16
16
 
17
17
  - uses: ruby/setup-ruby@v1
18
18
  with:
19
- ruby-version: "3.4"
19
+ ruby-version: "4.0"
20
20
  bundler-cache: true
21
21
 
22
22
  - name: Prepare validator
data/Gemfile CHANGED
@@ -3,6 +3,7 @@ source "https://rubygems.org"
3
3
  # Specify your gem's dependencies in xrechnung.gemspec
4
4
  gemspec
5
5
 
6
+ gem "debug"
6
7
  gem "rake"
7
8
  gem "rspec"
8
9
  gem "rubocop-rspec"
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- xrechnung (0.6.5)
4
+ xrechnung (0.7.2)
5
5
  activesupport
6
6
  base64
7
7
  bigdecimal
@@ -10,7 +10,7 @@ PATH
10
10
  GEM
11
11
  remote: https://rubygems.org/
12
12
  specs:
13
- activesupport (8.1.2)
13
+ activesupport (8.1.3.1)
14
14
  base64
15
15
  bigdecimal
16
16
  concurrent-ruby (~> 1.0, >= 1.3.1)
@@ -25,37 +25,62 @@ GEM
25
25
  uri (>= 0.13.1)
26
26
  ast (2.4.3)
27
27
  base64 (0.3.0)
28
- bigdecimal (4.0.1)
28
+ bigdecimal (4.1.2)
29
29
  builder (3.3.0)
30
- concurrent-ruby (1.3.6)
30
+ concurrent-ruby (1.3.8)
31
31
  connection_pool (3.0.2)
32
- csv (3.3.5)
32
+ csv (3.3.6)
33
+ debug (1.11.1)
34
+ irb (~> 1.10)
35
+ reline (>= 0.3.8)
33
36
  diff-lcs (1.6.2)
34
37
  drb (2.2.3)
38
+ erb (6.0.6)
35
39
  httparty (0.24.2)
36
40
  csv
37
41
  mini_mime (>= 1.0.0)
38
42
  multi_xml (>= 0.5.2)
39
- i18n (1.14.8)
43
+ i18n (1.15.2)
40
44
  concurrent-ruby (~> 1.0)
41
- json (2.18.0)
42
- language_server-protocol (3.17.0.5)
45
+ io-console (0.8.2)
46
+ irb (1.18.0)
47
+ pp (>= 0.6.0)
48
+ prism (>= 1.3.0)
49
+ rdoc (>= 4.0.0)
50
+ reline (>= 0.4.2)
51
+ json (2.21.2)
52
+ language_server-protocol (3.17.0.6)
43
53
  lint_roller (1.1.0)
44
54
  logger (1.7.0)
45
55
  mini_mime (1.1.5)
46
- minitest (6.0.1)
56
+ minitest (6.0.6)
57
+ drb (~> 2.0)
47
58
  prism (~> 1.5)
48
- multi_xml (0.8.1)
59
+ multi_xml (0.9.1)
49
60
  bigdecimal (>= 3.1, < 5)
50
- parallel (1.27.0)
51
- parser (3.3.10.1)
61
+ parallel (1.28.0)
62
+ parser (3.3.12.0)
52
63
  ast (~> 2.4.1)
53
64
  racc
65
+ pp (0.6.4)
66
+ prettyprint
67
+ prettyprint (0.2.0)
54
68
  prism (1.9.0)
55
69
  racc (1.8.1)
56
70
  rainbow (3.1.1)
57
- rake (13.3.1)
58
- regexp_parser (2.11.3)
71
+ rake (13.4.2)
72
+ rbs (4.1.1)
73
+ logger
74
+ prism (>= 1.6.0)
75
+ tsort
76
+ rdoc (8.0.0)
77
+ erb
78
+ prism (>= 1.6.0)
79
+ rbs (>= 4.0.0)
80
+ tsort
81
+ regexp_parser (2.12.0)
82
+ reline (0.6.3)
83
+ io-console (~> 0.5)
59
84
  rspec (3.13.2)
60
85
  rspec-core (~> 3.13.0)
61
86
  rspec-expectations (~> 3.13.0)
@@ -65,33 +90,35 @@ GEM
65
90
  rspec-expectations (3.13.5)
66
91
  diff-lcs (>= 1.2.0, < 2.0)
67
92
  rspec-support (~> 3.13.0)
68
- rspec-mocks (3.13.7)
93
+ rspec-mocks (3.13.8)
69
94
  diff-lcs (>= 1.2.0, < 2.0)
70
95
  rspec-support (~> 3.13.0)
71
96
  rspec-support (3.13.7)
72
- rubocop (1.84.0)
97
+ rubocop (1.88.2)
73
98
  json (~> 2.3)
74
99
  language_server-protocol (~> 3.17.0.2)
75
100
  lint_roller (~> 1.1.0)
76
- parallel (~> 1.10)
101
+ parallel (>= 1.10)
77
102
  parser (>= 3.3.0.2)
78
103
  rainbow (>= 2.2.2, < 4.0)
79
104
  regexp_parser (>= 2.9.3, < 3.0)
80
105
  rubocop-ast (>= 1.49.0, < 2.0)
81
106
  ruby-progressbar (~> 1.7)
82
107
  unicode-display_width (>= 2.4.0, < 4.0)
83
- rubocop-ast (1.49.0)
108
+ rubocop-ast (1.50.0)
84
109
  parser (>= 3.3.7.2)
85
110
  prism (~> 1.7)
86
111
  rubocop-rake (0.7.1)
87
112
  lint_roller (~> 1.1)
88
113
  rubocop (>= 1.72.1)
89
- rubocop-rspec (3.9.0)
114
+ rubocop-rspec (3.10.2)
90
115
  lint_roller (~> 1.1)
91
- rubocop (~> 1.81)
116
+ regexp_parser (>= 2.0)
117
+ rubocop (~> 1.86, >= 1.86.2)
92
118
  ruby-progressbar (1.13.0)
93
- rubyzip (3.2.2)
119
+ rubyzip (3.4.1)
94
120
  securerandom (0.4.1)
121
+ tsort (0.2.0)
95
122
  tzinfo (2.0.6)
96
123
  concurrent-ruby (~> 1.0)
97
124
  unicode-display_width (3.2.0)
@@ -103,6 +130,7 @@ PLATFORMS
103
130
  ruby
104
131
 
105
132
  DEPENDENCIES
133
+ debug
106
134
  httparty
107
135
  rake
108
136
  rspec
data/Rakefile CHANGED
@@ -9,45 +9,46 @@ RSpec::Core::RakeTask.new(:spec)
9
9
 
10
10
  task default: :spec
11
11
 
12
+ Source = Data.define(:filename, :release_url)
13
+
12
14
  VALIDATOR_SOURCES = {
13
- tool: {
14
- filename: "validator/validator-1.6.0-standalone.jar",
15
- release_url: "https://github.com/itplr-kosit/validator/releases/download/v1.6.0/validator-1.6.0.zip",
16
- },
17
- scenarios: {
18
- filename: "validator/scenarios.xml",
19
- release_url: "https://github.com/itplr-kosit/validator-configuration-xrechnung/releases/download/release-2025-07-10/validator-configuration-xrechnung_3.0.2_2025-07-10.zip",
20
- },
15
+ tool: Source.new(
16
+ "validator/validator-1.6.2-standalone.jar",
17
+ "https://github.com/itplr-kosit/validator/releases/download/v1.6.2/validator-1.6.2-standalone.jar",
18
+ ),
19
+ scenarios: Source.new(
20
+ "validator/scenarios.xml",
21
+ "https://github.com/itplr-kosit/validator-configuration-xrechnung/releases/download/v2026-01-31/xrechnung-3.0.2-validator-configuration-2026-01-31.zip",
22
+ ),
21
23
  }.freeze
22
24
 
23
25
  namespace :validator do
24
26
  VALIDATOR_SOURCES.each_value do |v|
25
- base = Pathname.new(__dir__).join("validator")
26
- zipfile = base.join(File.basename(v[:release_url]))
27
+ base = Pathname.new(__dir__).join("validator").tap(&:mkpath)
27
28
 
28
- file zipfile do
29
- base.mkpath unless base.exist?
29
+ file v.filename do
30
+ basename = base.join(File.basename(v.release_url))
30
31
 
31
- res = HTTParty.get(v[:release_url], follow_redirects: true)
32
- File.binwrite(zipfile, res.body)
33
- end
32
+ res = HTTParty.get(v.release_url, follow_redirects: true)
33
+ File.binwrite(basename, res.body)
34
34
 
35
- file v[:filename] => zipfile do
36
- Zip::File.foreach(zipfile) do |entry|
37
- entry.extract destination_directory: base
35
+ if basename.extname == ".zip"
36
+ Zip::File.foreach(basename) do |entry|
37
+ entry.extract destination_directory: base
38
+ end
38
39
  end
39
40
  end
40
41
  end
41
42
 
42
43
  desc "Download official validator and scenarios"
43
- task download: VALIDATOR_SOURCES.map { |_, v| v[:filename] }
44
+ task download: VALIDATOR_SOURCES.values.map(&:filename)
44
45
 
45
46
  desc "Run validator on test fixtures"
46
47
  task run: :download do
47
48
  fixtures = Pathname.new(__dir__).join("spec/fixtures/*.xml")
48
49
  output = Pathname.new(__dir__).join("validator/results").tap(&:mkpath)
49
- tool = VALIDATOR_SOURCES[:tool][:filename]
50
- scenarios = VALIDATOR_SOURCES[:scenarios][:filename]
50
+ tool = VALIDATOR_SOURCES[:tool].filename
51
+ scenarios = VALIDATOR_SOURCES[:scenarios].filename
51
52
 
52
53
  sh "java -jar #{tool} -r validator -s #{scenarios} --output-directory #{output} --html #{fixtures}"
53
54
  end
@@ -8,7 +8,7 @@ module Xrechnung
8
8
 
9
9
  # @!attribute allowance_charge_reason_code
10
10
  # @return [String]
11
- member :allowance_charge_reason_code, type: Integer
11
+ member :allowance_charge_reason_code, type: String
12
12
 
13
13
  # @!attribute allowance_charge_reason
14
14
  # @return [String]
@@ -31,15 +31,13 @@ module Xrechnung
31
31
  member :tax_category, type: Xrechnung::TaxCategory
32
32
 
33
33
  def initialize(**kwargs)
34
- unless kwargs[:amount].is_a?(Currency)
35
- kwargs[:amount] = Currency::EUR(kwargs[:amount])
36
- end
34
+ kwargs[:amount] = Currency::EUR(kwargs[:amount]) unless kwargs[:amount].is_a?(Currency)
37
35
 
38
36
  unless kwargs[:base_amount].is_a?(Currency) || kwargs[:base_amount].nil?
39
37
  kwargs[:base_amount] = Currency::EUR(kwargs[:base_amount])
40
38
  end
41
39
 
42
- super(**kwargs)
40
+ super
43
41
  end
44
42
 
45
43
  # noinspection RubyResolve
@@ -47,23 +45,15 @@ module Xrechnung
47
45
  xml.cac :AllowanceCharge do
48
46
  xml.cbc :ChargeIndicator, charge_indicator
49
47
 
50
- if allowance_charge_reason_code
51
- xml.cbc :AllowanceChargeReasonCode, allowance_charge_reason_code
52
- end
48
+ xml.cbc :AllowanceChargeReasonCode, allowance_charge_reason_code if allowance_charge_reason_code
53
49
 
54
- if allowance_charge_reason
55
- xml.cbc :AllowanceChargeReason, allowance_charge_reason
56
- end
50
+ xml.cbc :AllowanceChargeReason, allowance_charge_reason if allowance_charge_reason
57
51
 
58
- if multiplier_factor_numeric
59
- xml.cbc :MultiplierFactorNumeric, format("%.2f", multiplier_factor_numeric)
60
- end
52
+ xml.cbc :MultiplierFactorNumeric, format("%.2f", multiplier_factor_numeric) if multiplier_factor_numeric
61
53
 
62
54
  xml.cbc :Amount, *amount.xml_args
63
55
 
64
- if base_amount
65
- xml.cbc :BaseAmount, *base_amount.xml_args
66
- end
56
+ xml.cbc :BaseAmount, *base_amount.xml_args if base_amount
67
57
 
68
58
  tax_category&.to_xml(xml)
69
59
  end
@@ -10,6 +10,22 @@ module Xrechnung
10
10
  def xml_args
11
11
  [value_to_s, { currencyID: currency_id }]
12
12
  end
13
+
14
+ def self.check_matching_attributes(one, other)
15
+ raise ArgumentError, "other must be a Currency" unless one.is_a? Currency
16
+ raise ArgumentError, "other must be a Currency" unless other.is_a? Currency
17
+ raise ArgumentError, "currency_id must match" unless one.currency_id == other.currency_id
18
+ end
19
+
20
+ def +(other)
21
+ self.class.check_matching_attributes(self, other)
22
+ Currency.new(currency_id: currency_id, value: value + other.value)
23
+ end
24
+
25
+ def -(other)
26
+ self.class.check_matching_attributes(self, other)
27
+ Currency.new(currency_id: currency_id, value: value - other.value)
28
+ end
13
29
  end
14
30
 
15
31
  class << Currency
@@ -24,7 +24,7 @@ module Xrechnung
24
24
  send("#{key}=", value)
25
25
  end
26
26
 
27
- Member = Struct.new(:type, :default, :optional, :transform_value, keyword_init: true)
27
+ Member = Struct.new(:type, :default, :optional, :is_private, :transform_value, keyword_init: true)
28
28
 
29
29
  module ClassMethods
30
30
  def members
@@ -53,6 +53,11 @@ module Xrechnung
53
53
 
54
54
  instance_variable_set :"@#{member_name}", in_value
55
55
  end
56
+
57
+ return unless member.is_private
58
+
59
+ private member_name
60
+ private setter_name
56
61
  end
57
62
  end
58
63
  end
@@ -43,6 +43,16 @@ module Xrechnung
43
43
  # @return [String]
44
44
  member :tax_exemption_reason, type: String
45
45
 
46
+ def ==(other)
47
+ return false unless other.is_a?(Xrechnung::TaxCategory)
48
+
49
+ id == other.id &&
50
+ percent == other.percent &&
51
+ tax_scheme_id == other.tax_scheme_id &&
52
+ tax_exemption_reason_code == other.tax_exemption_reason_code &&
53
+ tax_exemption_reason == other.tax_exemption_reason
54
+ end
55
+
46
56
  # noinspection RubyResolve
47
57
  def to_xml(xml, root_tag_name: :TaxCategory)
48
58
  xml.cac root_tag_name do
@@ -32,6 +32,7 @@ module Xrechnung
32
32
 
33
33
  def initialize(**kwargs)
34
34
  super
35
+ self.tax_amount ||= Currency::EUR(0)
35
36
  self.taxable_amount ||= Currency::EUR(0)
36
37
  end
37
38
 
@@ -43,5 +44,11 @@ module Xrechnung
43
44
  tax_category&.to_xml(xml)
44
45
  end
45
46
  end
47
+
48
+ def update_amount
49
+ value = taxable_amount.value * tax_category.percent / 100
50
+ self.tax_amount = Currency::EUR(value)
51
+ value
52
+ end
46
53
  end
47
54
  end
@@ -42,5 +42,21 @@ module Xrechnung
42
42
  end
43
43
  xml.target!
44
44
  end
45
+
46
+ def update_amounts
47
+ value = tax_subtotals.sum(&:update_amount)
48
+ self.tax_amount = Xrechnung::Currency::EUR(value)
49
+ end
50
+
51
+ def get_tax_subtotal(tax_category)
52
+ element = tax_subtotals.find { _1.tax_category == tax_category }
53
+
54
+ unless element
55
+ element = Xrechnung::TaxSubtotal.new(tax_category: tax_category)
56
+ tax_subtotals << element
57
+ end
58
+
59
+ element
60
+ end
45
61
  end
46
62
  end
@@ -1,3 +1,3 @@
1
1
  module Xrechnung
2
- VERSION = "0.6.5".freeze
2
+ VERSION = "0.7.2".freeze
3
3
  end
data/lib/xrechnung.rb CHANGED
@@ -262,7 +262,7 @@ module Xrechnung
262
262
  #
263
263
  # @!attribute tax_total
264
264
  # @return [Xrechnung::TaxTotal]
265
- member :tax_total, type: Xrechnung::TaxTotal
265
+ member :tax_total, type: Xrechnung::TaxTotal, is_private: true
266
266
 
267
267
  # DOCUMENT TOTALS BG-22
268
268
  #
@@ -271,7 +271,7 @@ module Xrechnung
271
271
  #
272
272
  # @!attribute legal_monetary_total
273
273
  # @return [Xrechnung::LegalMonetaryTotal]
274
- member :legal_monetary_total, type: Xrechnung::LegalMonetaryTotal
274
+ member :legal_monetary_total, type: Xrechnung::LegalMonetaryTotal, is_private: true
275
275
 
276
276
  # INVOICE LINE BG-25
277
277
  #
@@ -301,7 +301,18 @@ module Xrechnung
301
301
  "xsi:schemaLocation" => "urn:oasis:names:specification:ubl:schema:xsd:Invoice-2 http://docs.oasis-open.org/ubl/os-UBL-2.1/xsd/maindoc/UBL-Invoice-2.1.xsd",
302
302
  }.freeze
303
303
 
304
+ def initialize(...)
305
+ super
306
+ self.legal_monetary_total ||= Xrechnung::LegalMonetaryTotal.new
307
+ end
308
+
309
+ def prepaid_amount=(value)
310
+ legal_monetary_total.prepaid_amount = value
311
+ end
312
+
304
313
  def to_xml(indent: 2, target: "")
314
+ update_amounts
315
+
305
316
  xml = Builder::XmlMarkup.new(indent: indent, target: target)
306
317
  xml.instruct! :xml, version: "1.0", encoding: "UTF-8"
307
318
 
@@ -368,8 +379,10 @@ module Xrechnung
368
379
  end
369
380
  end
370
381
 
371
- xml.cac :PaymentMeans do
372
- payment_means&.to_xml(xml)
382
+ if payment_means
383
+ xml.cac :PaymentMeans do
384
+ payment_means&.to_xml(xml)
385
+ end
373
386
  end
374
387
 
375
388
  payee_party&.to_xml(xml) unless self.class.members[:payee_party].optional && payee_party.nil?
@@ -395,5 +408,48 @@ module Xrechnung
395
408
 
396
409
  target
397
410
  end
411
+
412
+ def update_amounts
413
+ self.tax_total = Xrechnung::TaxTotal.new
414
+
415
+ invoice_lines.each do |invoice_line|
416
+ tax_total.get_tax_subtotal(invoice_line.item.classified_tax_category).taxable_amount += invoice_line.line_extension_amount
417
+ end
418
+
419
+ allowance_charges.each do |ac|
420
+ element = tax_total.get_tax_subtotal(ac.tax_category)
421
+ if ac.charge_indicator
422
+ element.taxable_amount += ac.amount
423
+ else
424
+ element.taxable_amount -= ac.amount
425
+ end
426
+ end
427
+
428
+ tax_total.update_amounts
429
+
430
+ currency_id = tax_total.tax_amount.currency_id
431
+ zero = Xrechnung::Currency.new(currency_id: currency_id, value: BigDecimal(0))
432
+
433
+ legal_monetary_total.line_extension_amount = zero
434
+ legal_monetary_total.allowance_total_amount = zero
435
+ legal_monetary_total.charge_total_amount = zero
436
+ legal_monetary_total.prepaid_amount ||= zero
437
+
438
+ invoice_lines.each do |line|
439
+ legal_monetary_total.line_extension_amount += line.line_extension_amount
440
+ end
441
+
442
+ allowance_charges.each do |ac|
443
+ if ac.charge_indicator
444
+ legal_monetary_total.charge_total_amount += ac.amount
445
+ else
446
+ legal_monetary_total.allowance_total_amount += ac.amount
447
+ end
448
+ end
449
+
450
+ legal_monetary_total.tax_exclusive_amount = legal_monetary_total.line_extension_amount + legal_monetary_total.charge_total_amount - legal_monetary_total.allowance_total_amount
451
+ legal_monetary_total.tax_inclusive_amount = legal_monetary_total.tax_exclusive_amount + tax_total.tax_amount
452
+ legal_monetary_total.payable_amount = legal_monetary_total.tax_inclusive_amount - legal_monetary_total.prepaid_amount
453
+ end
398
454
  end
399
455
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: xrechnung
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.5
4
+ version: 0.7.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Julian Kornberger
@@ -136,7 +136,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
136
136
  - !ruby/object:Gem::Version
137
137
  version: '0'
138
138
  requirements: []
139
- rubygems_version: 4.0.13
139
+ rubygems_version: 4.0.17
140
140
  specification_version: 4
141
141
  summary: Library to create invoices in the XRechnung format.
142
142
  test_files: []