zipdatev 0.1.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.
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_model"
4
+ require "date"
5
+
6
+ # Validates that a date is within the DATEV-allowed range.
7
+ #
8
+ # DATEV requires dates to be between 1753-01-01 and 9999-12-31.
9
+ # This validator enforces that constraint.
10
+ #
11
+ # @example Validate date is in DATEV range
12
+ # validates :date, date_range: true
13
+ #
14
+ # @example Allow nil values
15
+ # validates :due_date, date_range: true, allow_nil: true
16
+ class DateRangeValidator < ActiveModel::EachValidator
17
+ MIN_DATE = Date.new(1753, 1, 1)
18
+ MAX_DATE = Date.new(9999, 12, 31)
19
+
20
+ def validate_each(record, attribute, value)
21
+ return if value.nil?
22
+
23
+ unless value.is_a?(Date)
24
+ record.errors.add(attribute, options[:message] || "must be a valid date")
25
+ return
26
+ end
27
+
28
+ if value < MIN_DATE
29
+ record.errors.add(
30
+ attribute,
31
+ options[:message] || "must be on or after #{MIN_DATE}"
32
+ )
33
+ end
34
+
35
+ return unless value > MAX_DATE
36
+
37
+ record.errors.add(
38
+ attribute,
39
+ options[:message] || "must be on or before #{MAX_DATE}"
40
+ )
41
+ end
42
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_model"
4
+ require "bigdecimal"
5
+
6
+ # Validates that a decimal value has exactly the specified number of decimal places.
7
+ #
8
+ # DATEV requires specific precision for different field types:
9
+ # - Amount fields: 2 decimal places
10
+ # - Tax percentages: 2 decimal places
11
+ # - Exchange rates: 6 decimal places
12
+ # - Cost amounts: 4 decimal places
13
+ #
14
+ # @example Validate amount has 2 decimal places
15
+ # validates :amount, decimal_precision: { places: 2 }
16
+ #
17
+ # @example Allow nil values
18
+ # validates :tax, decimal_precision: { places: 2 }, allow_nil: true
19
+ class DecimalPrecisionValidator < ActiveModel::EachValidator
20
+ def validate_each(record, attribute, value)
21
+ return if value.nil?
22
+
23
+ places = options.fetch(:places, 2)
24
+
25
+ return if has_exact_decimal_places?(value, places)
26
+
27
+ record.errors.add(
28
+ attribute,
29
+ options[:message] || "must have exactly #{places} decimal places"
30
+ )
31
+ end
32
+
33
+ private
34
+
35
+ def has_exact_decimal_places?(value, places)
36
+ # Convert to BigDecimal for consistent handling
37
+ decimal = value.is_a?(BigDecimal) ? value : BigDecimal(value.to_s)
38
+
39
+ # Use string representation to check decimal places
40
+ # Format with many decimal places to avoid rounding
41
+ str = format("%.#{places + 10}f", decimal)
42
+ parts = str.split(".")
43
+
44
+ return places.zero? if parts.length == 1
45
+
46
+ # Get the decimal part
47
+ decimal_part = parts[1]
48
+
49
+ # Strip trailing zeros beyond the required places
50
+ significant_decimals = decimal_part.sub(/0+\z/, "")
51
+
52
+ # Check if the significant decimals fit within the required places
53
+ significant_decimals.length <= places
54
+ end
55
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_model"
4
+
5
+ module ZipDatev
6
+ module Validators
7
+ # Validates the chronological ordering of discount-related dates.
8
+ #
9
+ # DATEV requires discount dates to be in a specific order:
10
+ # 1. invoice date < discount_payment_date
11
+ # 2. discount_payment_date < discount_payment_date_2
12
+ # 3. discount_payment_date_2 < due_date
13
+ #
14
+ # Also validates that delivery_date is not after invoice date.
15
+ #
16
+ # @example Apply to Invoice or LineItem
17
+ # validates_with ZipDatev::Validators::DiscountDatesValidator
18
+ class DiscountDatesValidator < ActiveModel::Validator
19
+ def validate(record)
20
+ validate_delivery_date(record)
21
+ validate_dates_after_invoice(record)
22
+ validate_discount_ordering(record)
23
+ validate_due_date_ordering(record)
24
+ end
25
+
26
+ private
27
+
28
+ def validate_delivery_date(record)
29
+ return unless record.date && record.delivery_date
30
+ return unless record.delivery_date > record.date
31
+
32
+ record.errors.add(:delivery_date, "must not be after invoice date")
33
+ end
34
+
35
+ def validate_dates_after_invoice(record)
36
+ invoice_date = record.date
37
+ return unless invoice_date
38
+
39
+ validate_date_after_invoice(record, :discount_payment_date, invoice_date)
40
+ validate_date_after_invoice(record, :discount_payment_date_2, invoice_date)
41
+ validate_date_after_invoice(record, :due_date, invoice_date)
42
+ end
43
+
44
+ def validate_date_after_invoice(record, field, invoice_date)
45
+ date_value = record.public_send(field)
46
+ return unless date_value && date_value <= invoice_date
47
+
48
+ record.errors.add(field, "must be after invoice date")
49
+ end
50
+
51
+ def validate_discount_ordering(record)
52
+ discount_1 = record.discount_payment_date
53
+ discount_2 = record.discount_payment_date_2
54
+ return unless discount_1 && discount_2 && discount_1 >= discount_2
55
+
56
+ record.errors.add(:discount_payment_date_2, "must be after discount_payment_date")
57
+ end
58
+
59
+ def validate_due_date_ordering(record)
60
+ due_date = record.due_date
61
+ return unless due_date
62
+
63
+ discount_2 = record.discount_payment_date_2
64
+ discount_1 = record.discount_payment_date
65
+
66
+ if discount_2 && discount_2 >= due_date
67
+ record.errors.add(:due_date, "must be after discount_payment_date_2")
68
+ elsif discount_1 && !discount_2 && discount_1 >= due_date
69
+ record.errors.add(:due_date, "must be after discount_payment_date")
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_model"
4
+
5
+ module ZipDatev
6
+ module Validators
7
+ # Validates that payment_conditions_id and manual discount fields are mutually exclusive.
8
+ #
9
+ # DATEV rule: If payment_conditions_id is set, no manual discount fields are allowed.
10
+ # The manual discount fields include:
11
+ # - discount_amount
12
+ # - discount_percentage
13
+ # - discount_payment_date
14
+ # - discount_amount_2
15
+ # - discount_percentage_2
16
+ # - discount_payment_date_2
17
+ # - due_date
18
+ #
19
+ # @example Apply to Invoice or LineItem
20
+ # validates_with PaymentConditionsValidator
21
+ class PaymentConditionsValidator < ActiveModel::Validator
22
+ DISCOUNT_FIELDS = %i[
23
+ discount_amount
24
+ discount_percentage
25
+ discount_payment_date
26
+ discount_amount_2
27
+ discount_percentage_2
28
+ discount_payment_date_2
29
+ due_date
30
+ ].freeze
31
+
32
+ def validate(record)
33
+ return unless record.payment_conditions_id
34
+
35
+ present_discount_fields = DISCOUNT_FIELDS.select do |field|
36
+ record.public_send(field).present?
37
+ end
38
+
39
+ return if present_discount_fields.empty?
40
+
41
+ record.errors.add(
42
+ :payment_conditions_id,
43
+ "cannot be combined with manual discount fields " \
44
+ "(#{present_discount_fields.join(", ")})"
45
+ )
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ZipDatev
4
+ VERSION = "0.1.0"
5
+ end
data/lib/zipdatev.rb ADDED
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "zipdatev/version"
4
+ require_relative "zipdatev/errors"
5
+ require_relative "zipdatev/constants"
6
+ require_relative "zipdatev/validators/decimal_precision_validator"
7
+ require_relative "zipdatev/validators/date_range_validator"
8
+ require_relative "zipdatev/validators/discount_dates_validator"
9
+ require_relative "zipdatev/validators/payment_conditions_validator"
10
+ require_relative "zipdatev/validators/consolidation_validator"
11
+ require_relative "zipdatev/line_item"
12
+ require_relative "zipdatev/invoice"
13
+ require_relative "zipdatev/document"
14
+ require_relative "zipdatev/repository"
15
+ require_relative "zipdatev/package"
16
+ require_relative "zipdatev/generators/base"
17
+ require_relative "zipdatev/generators/ledger_xml"
18
+ require_relative "zipdatev/generators/document_xml"
19
+ require_relative "zipdatev/schema_validator"
20
+
21
+ # ZipDatev provides functionality for creating DATEV-compliant ZIP packages
22
+ # for incoming invoices (Eingangsrechnungen). It generates XML files according
23
+ # to the DATEV Belegverwaltung online interface specification (version 6.0).
24
+ #
25
+ # @example Creating a simple package
26
+ # package = ZipDatev.create_package(
27
+ # generator_info: "MyCompany",
28
+ # generating_system: "MyApp"
29
+ # )
30
+ #
31
+ # invoice = ZipDatev::Invoice.new(
32
+ # date: Date.today,
33
+ # amount: 1190.00,
34
+ # currency_code: "EUR",
35
+ # invoice_id: "INV-001"
36
+ # )
37
+ #
38
+ # package.add_document(invoice: invoice, attachments: ["invoice.pdf"])
39
+ # package.build("output.zip")
40
+ #
41
+ module ZipDatev
42
+ class << self
43
+ # Create a new package with the given configuration.
44
+ #
45
+ # @param generator_info [String] Generator information (company name)
46
+ # @param generating_system [String] System identifier
47
+ # @return [Package] A new package instance
48
+ def create_package(generator_info:, generating_system:)
49
+ Package.new(
50
+ generator_info: generator_info,
51
+ generating_system: generating_system
52
+ )
53
+ end
54
+ end
55
+ end
metadata ADDED
@@ -0,0 +1,110 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: zipdatev
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Dexter Team
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: activemodel
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '7.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '7.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: nokogiri
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '1.15'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '1.15'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rubyzip
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '2.3'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '2.3'
54
+ description: A Ruby gem for creating DATEV-compliant ZIP packages for incoming invoices.
55
+ Generates document.xml and ledger XML files per DATEV XML interface v6.0.
56
+ email:
57
+ - team@getdexter.com
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - LICENSE
63
+ - lib/zipdatev.rb
64
+ - lib/zipdatev/constants.rb
65
+ - lib/zipdatev/document.rb
66
+ - lib/zipdatev/errors.rb
67
+ - lib/zipdatev/generators/base.rb
68
+ - lib/zipdatev/generators/document_xml.rb
69
+ - lib/zipdatev/generators/ledger_xml.rb
70
+ - lib/zipdatev/invoice.rb
71
+ - lib/zipdatev/line_item.rb
72
+ - lib/zipdatev/package.rb
73
+ - lib/zipdatev/repository.rb
74
+ - lib/zipdatev/schema_validator.rb
75
+ - lib/zipdatev/schemas/Belegverwaltung_online_ledger_import_v060.xsd
76
+ - lib/zipdatev/schemas/Belegverwaltung_online_ledger_types_v060.xsd
77
+ - lib/zipdatev/schemas/Document_types_v060.xsd
78
+ - lib/zipdatev/schemas/Document_v060.xsd
79
+ - lib/zipdatev/validators/consolidation_validator.rb
80
+ - lib/zipdatev/validators/date_range_validator.rb
81
+ - lib/zipdatev/validators/decimal_precision_validator.rb
82
+ - lib/zipdatev/validators/discount_dates_validator.rb
83
+ - lib/zipdatev/validators/payment_conditions_validator.rb
84
+ - lib/zipdatev/version.rb
85
+ homepage: https://github.com/getdexter/zipdatev
86
+ licenses:
87
+ - MIT
88
+ metadata:
89
+ homepage_uri: https://github.com/getdexter/zipdatev
90
+ source_code_uri: https://github.com/getdexter/zipdatev
91
+ changelog_uri: https://github.com/getdexter/zipdatev/blob/main/CHANGELOG.md
92
+ rubygems_mfa_required: 'true'
93
+ rdoc_options: []
94
+ require_paths:
95
+ - lib
96
+ required_ruby_version: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - ">="
99
+ - !ruby/object:Gem::Version
100
+ version: 3.2.0
101
+ required_rubygems_version: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - ">="
104
+ - !ruby/object:Gem::Version
105
+ version: '0'
106
+ requirements: []
107
+ rubygems_version: 3.6.9
108
+ specification_version: 4
109
+ summary: Generate DATEV-compliant ZIP packages for incoming invoices
110
+ test_files: []