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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/lib/zipdatev/constants.rb +160 -0
- data/lib/zipdatev/document.rb +60 -0
- data/lib/zipdatev/errors.rb +45 -0
- data/lib/zipdatev/generators/base.rb +146 -0
- data/lib/zipdatev/generators/document_xml.rb +143 -0
- data/lib/zipdatev/generators/ledger_xml.rb +144 -0
- data/lib/zipdatev/invoice.rb +339 -0
- data/lib/zipdatev/line_item.rb +203 -0
- data/lib/zipdatev/package.rb +267 -0
- data/lib/zipdatev/repository.rb +42 -0
- data/lib/zipdatev/schema_validator.rb +151 -0
- data/lib/zipdatev/schemas/Belegverwaltung_online_ledger_import_v060.xsd +546 -0
- data/lib/zipdatev/schemas/Belegverwaltung_online_ledger_types_v060.xsd +1181 -0
- data/lib/zipdatev/schemas/Document_types_v060.xsd +410 -0
- data/lib/zipdatev/schemas/Document_v060.xsd +567 -0
- data/lib/zipdatev/validators/consolidation_validator.rb +70 -0
- data/lib/zipdatev/validators/date_range_validator.rb +42 -0
- data/lib/zipdatev/validators/decimal_precision_validator.rb +55 -0
- data/lib/zipdatev/validators/discount_dates_validator.rb +74 -0
- data/lib/zipdatev/validators/payment_conditions_validator.rb +49 -0
- data/lib/zipdatev/version.rb +5 -0
- data/lib/zipdatev.rb +55 -0
- metadata +110 -0
|
@@ -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
|
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: []
|