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,144 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module ZipDatev
|
|
6
|
+
module Generators
|
|
7
|
+
# Generates DATEV ledger XML files for accountsPayableLedger entries.
|
|
8
|
+
#
|
|
9
|
+
# Each invoice generates one ledger XML file containing the accounting
|
|
10
|
+
# data. Single-line invoices produce one accountsPayableLedger entry,
|
|
11
|
+
# while multi-line invoices produce multiple entries (one per line item).
|
|
12
|
+
#
|
|
13
|
+
# @example Generate ledger XML for a simple invoice
|
|
14
|
+
# generator = ZipDatev::Generators::LedgerXml.new(
|
|
15
|
+
# invoice: invoice,
|
|
16
|
+
# generator_info: "MyCompany",
|
|
17
|
+
# generating_system: "MyApp"
|
|
18
|
+
# )
|
|
19
|
+
# xml_doc = generator.generate
|
|
20
|
+
#
|
|
21
|
+
# @example Get the suggested filename
|
|
22
|
+
# generator.filename # => "Rechnungsdaten_RE_R2023x101.xml"
|
|
23
|
+
class LedgerXml < Base
|
|
24
|
+
# @return [Invoice] The invoice to generate XML for
|
|
25
|
+
attr_reader :invoice
|
|
26
|
+
|
|
27
|
+
# @return [String] Generator information (company name)
|
|
28
|
+
attr_reader :generator_info
|
|
29
|
+
|
|
30
|
+
# @return [String] Generating system identifier
|
|
31
|
+
attr_reader :generating_system
|
|
32
|
+
|
|
33
|
+
# Initialize a new ledger XML generator.
|
|
34
|
+
#
|
|
35
|
+
# @param invoice [Invoice] The invoice to generate XML for
|
|
36
|
+
# @param generator_info [String] Generator information (company name)
|
|
37
|
+
# @param generating_system [String] Generating system identifier
|
|
38
|
+
def initialize(invoice:, generator_info:, generating_system:)
|
|
39
|
+
super()
|
|
40
|
+
@invoice = invoice
|
|
41
|
+
@generator_info = generator_info
|
|
42
|
+
@generating_system = generating_system
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Generate the ledger XML document.
|
|
46
|
+
#
|
|
47
|
+
# @return [Nokogiri::XML::Document] The generated XML document
|
|
48
|
+
def generate
|
|
49
|
+
Nokogiri::XML::Builder.new(encoding: "UTF-8") do |xml|
|
|
50
|
+
xml.LedgerImport(ledger_import_attributes) do
|
|
51
|
+
xml.consolidate(consolidate_attributes) do
|
|
52
|
+
generate_entries(xml)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end.doc
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Get the suggested filename for this ledger XML.
|
|
59
|
+
#
|
|
60
|
+
# @return [String] The filename (e.g., "Rechnungsdaten_RE_R2023x101.xml")
|
|
61
|
+
def filename
|
|
62
|
+
ledger_filename(invoice.consolidated_invoice_id || invoice.invoice_id)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
# Attributes for the LedgerImport root element
|
|
68
|
+
def ledger_import_attributes
|
|
69
|
+
{
|
|
70
|
+
"xmlns" => LEDGER_XMLNS,
|
|
71
|
+
"xmlns:xsi" => XSI_XMLNS,
|
|
72
|
+
"xsi:schemaLocation" => LEDGER_XSI_SCHEMA_LOCATION,
|
|
73
|
+
"version" => Base::VERSION,
|
|
74
|
+
"generator_info" => generator_info,
|
|
75
|
+
"generating_system" => generating_system,
|
|
76
|
+
"xml_data" => XML_DATA
|
|
77
|
+
}
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Attributes for the consolidate element
|
|
81
|
+
def consolidate_attributes
|
|
82
|
+
attrs = {
|
|
83
|
+
"consolidatedAmount" => format_decimal(invoice.consolidated_amount),
|
|
84
|
+
"consolidatedDate" => format_date(invoice.consolidated_date),
|
|
85
|
+
"consolidatedInvoiceId" => invoice.consolidated_invoice_id,
|
|
86
|
+
"consolidatedCurrencyCode" => invoice.consolidated_currency_code
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
# Add optional consolidated attributes
|
|
90
|
+
delivery_date = invoice.consolidated_delivery_date
|
|
91
|
+
attrs["consolidatedDeliveryDate"] = format_date(delivery_date) if delivery_date
|
|
92
|
+
|
|
93
|
+
order_id = invoice.consolidated_order_id
|
|
94
|
+
attrs["consolidatedOrderId"] = order_id if order_id
|
|
95
|
+
|
|
96
|
+
attrs.compact
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Generate accountsPayableLedger entries
|
|
100
|
+
def generate_entries(xml)
|
|
101
|
+
if invoice.multi_line?
|
|
102
|
+
invoice.line_items.each do |line_item|
|
|
103
|
+
generate_entry(xml, line_item)
|
|
104
|
+
end
|
|
105
|
+
else
|
|
106
|
+
generate_entry(xml, invoice)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Generate a single accountsPayableLedger entry
|
|
111
|
+
#
|
|
112
|
+
# @param xml [Nokogiri::XML::Builder] The XML builder
|
|
113
|
+
# @param source [Invoice, LineItem] The source of attribute values
|
|
114
|
+
def generate_entry(xml, source)
|
|
115
|
+
xml.accountsPayableLedger do
|
|
116
|
+
ACCOUNTS_PAYABLE_LEDGER_ELEMENTS.each do |field|
|
|
117
|
+
value = get_field_value(source, field)
|
|
118
|
+
next if value.nil?
|
|
119
|
+
|
|
120
|
+
formatted = format_field_value(field, value)
|
|
121
|
+
next if formatted.nil? || formatted.empty?
|
|
122
|
+
|
|
123
|
+
xml.send(to_xml_name(field), formatted)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Get a field value from the source, with inheritance from invoice
|
|
129
|
+
# for line items with nil values.
|
|
130
|
+
#
|
|
131
|
+
# @param source [Invoice, LineItem] The primary source
|
|
132
|
+
# @param field [Symbol] The field name
|
|
133
|
+
# @return [Object, nil] The field value
|
|
134
|
+
def get_field_value(source, field)
|
|
135
|
+
value = source.public_send(field)
|
|
136
|
+
|
|
137
|
+
# If source is a line item and value is nil, try invoice
|
|
138
|
+
value = invoice.public_send(field) if value.nil? && source.is_a?(LineItem)
|
|
139
|
+
|
|
140
|
+
value
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_model"
|
|
4
|
+
require "date"
|
|
5
|
+
|
|
6
|
+
module ZipDatev
|
|
7
|
+
# Represents invoice data for a DATEV accountsPayableLedger entry.
|
|
8
|
+
#
|
|
9
|
+
# An invoice can either have a single set of values (simple invoice)
|
|
10
|
+
# or multiple line items (for invoices with multiple tax rates).
|
|
11
|
+
#
|
|
12
|
+
# When an invoice has line items, consolidation values are automatically
|
|
13
|
+
# computed from the line items. When there are no line items, the invoice's
|
|
14
|
+
# own values are used for consolidation.
|
|
15
|
+
#
|
|
16
|
+
# @example Simple invoice with single tax rate
|
|
17
|
+
# invoice = ZipDatev::Invoice.new(
|
|
18
|
+
# date: Date.new(2023, 1, 12),
|
|
19
|
+
# amount: 1190.00,
|
|
20
|
+
# currency_code: "EUR",
|
|
21
|
+
# invoice_id: "R2023x101",
|
|
22
|
+
# tax: 19.00
|
|
23
|
+
# )
|
|
24
|
+
#
|
|
25
|
+
# @example Invoice with multiple line items
|
|
26
|
+
# invoice = ZipDatev::Invoice.new(
|
|
27
|
+
# date: Date.new(2023, 2, 5),
|
|
28
|
+
# invoice_id: "R-2023-204",
|
|
29
|
+
# currency_code: "EUR"
|
|
30
|
+
# )
|
|
31
|
+
# invoice.add_line_item(amount: 119.00, tax: 19.00, booking_text: "Product A")
|
|
32
|
+
# invoice.add_line_item(amount: 107.00, tax: 7.00, booking_text: "Product B")
|
|
33
|
+
#
|
|
34
|
+
# @example Invoice with discount terms
|
|
35
|
+
# invoice = ZipDatev::Invoice.new(
|
|
36
|
+
# date: Date.new(2023, 1, 12),
|
|
37
|
+
# amount: 1190.00,
|
|
38
|
+
# currency_code: "EUR",
|
|
39
|
+
# invoice_id: "R2023x101",
|
|
40
|
+
# discount_amount: 35.70,
|
|
41
|
+
# discount_percentage: 3.00,
|
|
42
|
+
# discount_payment_date: Date.new(2023, 1, 17),
|
|
43
|
+
# discount_amount_2: 11.90,
|
|
44
|
+
# discount_percentage_2: 1.00,
|
|
45
|
+
# discount_payment_date_2: Date.new(2023, 1, 22),
|
|
46
|
+
# due_date: Date.new(2023, 1, 26)
|
|
47
|
+
# )
|
|
48
|
+
class Invoice
|
|
49
|
+
include ActiveModel::Model
|
|
50
|
+
include ActiveModel::Attributes
|
|
51
|
+
|
|
52
|
+
# Required field validations
|
|
53
|
+
validates :date, presence: true
|
|
54
|
+
validates :amount, presence: true
|
|
55
|
+
validates :currency_code, presence: true
|
|
56
|
+
validates :invoice_id, presence: true
|
|
57
|
+
|
|
58
|
+
# Amount validations
|
|
59
|
+
validates :amount, numericality: {
|
|
60
|
+
other_than: 0,
|
|
61
|
+
greater_than_or_equal_to: Constants::MIN_AMOUNT,
|
|
62
|
+
less_than_or_equal_to: Constants::MAX_AMOUNT
|
|
63
|
+
}, allow_nil: true
|
|
64
|
+
validates :amount, decimal_precision: { places: 2 }, allow_nil: true
|
|
65
|
+
|
|
66
|
+
# Discount amount validations
|
|
67
|
+
validates :discount_amount, numericality: {
|
|
68
|
+
greater_than_or_equal_to: Constants::MIN_DISCOUNT_AMOUNT,
|
|
69
|
+
less_than_or_equal_to: Constants::MAX_DISCOUNT_AMOUNT
|
|
70
|
+
}, allow_nil: true
|
|
71
|
+
validates :discount_amount, decimal_precision: { places: 2 }, allow_nil: true
|
|
72
|
+
|
|
73
|
+
validates :discount_amount_2, numericality: {
|
|
74
|
+
greater_than_or_equal_to: Constants::MIN_DISCOUNT_AMOUNT,
|
|
75
|
+
less_than_or_equal_to: Constants::MAX_DISCOUNT_AMOUNT
|
|
76
|
+
}, allow_nil: true
|
|
77
|
+
validates :discount_amount_2, decimal_precision: { places: 2 }, allow_nil: true
|
|
78
|
+
|
|
79
|
+
# Tax validations
|
|
80
|
+
validates :tax, numericality: {
|
|
81
|
+
greater_than_or_equal_to: Constants::MIN_TAX,
|
|
82
|
+
less_than_or_equal_to: Constants::MAX_TAX
|
|
83
|
+
}, allow_nil: true
|
|
84
|
+
validates :tax, decimal_precision: { places: 2 }, allow_nil: true
|
|
85
|
+
|
|
86
|
+
# Discount percentage validations
|
|
87
|
+
validates :discount_percentage, numericality: {
|
|
88
|
+
greater_than_or_equal_to: Constants::MIN_TAX,
|
|
89
|
+
less_than_or_equal_to: Constants::MAX_TAX
|
|
90
|
+
}, allow_nil: true
|
|
91
|
+
validates :discount_percentage, decimal_precision: { places: 2 }, allow_nil: true
|
|
92
|
+
|
|
93
|
+
validates :discount_percentage_2, numericality: {
|
|
94
|
+
greater_than_or_equal_to: Constants::MIN_TAX,
|
|
95
|
+
less_than_or_equal_to: Constants::MAX_TAX
|
|
96
|
+
}, allow_nil: true
|
|
97
|
+
validates :discount_percentage_2, decimal_precision: { places: 2 }, allow_nil: true
|
|
98
|
+
|
|
99
|
+
# Exchange rate validations
|
|
100
|
+
validates :exchange_rate, numericality: {
|
|
101
|
+
greater_than_or_equal_to: Constants::MIN_EXCHANGE_RATE,
|
|
102
|
+
less_than_or_equal_to: Constants::MAX_EXCHANGE_RATE
|
|
103
|
+
}, allow_nil: true
|
|
104
|
+
validates :exchange_rate, decimal_precision: { places: 6 }, allow_nil: true
|
|
105
|
+
|
|
106
|
+
# Cost amount validations
|
|
107
|
+
validates :cost_amount, decimal_precision: { places: 4 }, allow_nil: true
|
|
108
|
+
|
|
109
|
+
# Account number validations
|
|
110
|
+
validates :account_no, numericality: {
|
|
111
|
+
only_integer: true,
|
|
112
|
+
greater_than_or_equal_to: Constants::MIN_ACCOUNT_NO,
|
|
113
|
+
less_than_or_equal_to: Constants::MAX_ACCOUNT_NO
|
|
114
|
+
}, allow_nil: true
|
|
115
|
+
|
|
116
|
+
validates :bp_account_no, numericality: {
|
|
117
|
+
only_integer: true,
|
|
118
|
+
greater_than_or_equal_to: Constants::MIN_BP_ACCOUNT_NO,
|
|
119
|
+
less_than_or_equal_to: Constants::MAX_BP_ACCOUNT_NO
|
|
120
|
+
}, allow_nil: true
|
|
121
|
+
|
|
122
|
+
validates :bu_code, numericality: {
|
|
123
|
+
only_integer: true,
|
|
124
|
+
greater_than_or_equal_to: Constants::MIN_BU_CODE,
|
|
125
|
+
less_than_or_equal_to: Constants::MAX_BU_CODE
|
|
126
|
+
}, allow_nil: true
|
|
127
|
+
|
|
128
|
+
validates :payment_conditions_id, numericality: {
|
|
129
|
+
only_integer: true,
|
|
130
|
+
greater_than_or_equal_to: Constants::MIN_PAYMENT_CONDITIONS_ID,
|
|
131
|
+
less_than_or_equal_to: Constants::MAX_PAYMENT_CONDITIONS_ID
|
|
132
|
+
}, allow_nil: true
|
|
133
|
+
|
|
134
|
+
# String length validations
|
|
135
|
+
validates :invoice_id, length: { maximum: Constants::INVOICE_ID_MAX_LENGTH }
|
|
136
|
+
validates :internal_invoice_id, length: { maximum: Constants::INTERNAL_INVOICE_ID_MAX_LENGTH }, allow_nil: true
|
|
137
|
+
validates :order_id, length: { maximum: Constants::ORDER_ID_MAX_LENGTH }, allow_nil: true
|
|
138
|
+
validates :booking_text, length: { maximum: Constants::BOOKING_TEXT_MAX_LENGTH }, allow_nil: true
|
|
139
|
+
validates :information, length: { maximum: Constants::INFORMATION_MAX_LENGTH }, allow_nil: true
|
|
140
|
+
validates :supplier_name, length: { maximum: Constants::SUPPLIER_NAME_MAX_LENGTH }, allow_nil: true
|
|
141
|
+
validates :supplier_city, length: { maximum: Constants::SUPPLIER_CITY_MAX_LENGTH }, allow_nil: true
|
|
142
|
+
validates :account_name, length: { maximum: Constants::ACCOUNT_NAME_MAX_LENGTH }, allow_nil: true
|
|
143
|
+
validates :party_id, length: { maximum: Constants::PARTY_ID_MAX_LENGTH }, allow_nil: true
|
|
144
|
+
validates :vat_id, length: { maximum: Constants::VAT_ID_MAX_LENGTH }, allow_nil: true
|
|
145
|
+
validates :iban, length: { maximum: Constants::IBAN_MAX_LENGTH }, allow_nil: true
|
|
146
|
+
validates :swift_code, length: { maximum: Constants::SWIFT_MAX_LENGTH }, allow_nil: true
|
|
147
|
+
validates :bank_code, length: { maximum: Constants::BANK_CODE_MAX_LENGTH }, allow_nil: true
|
|
148
|
+
validates :bank_account, length: { maximum: Constants::BANK_ACCOUNT_MAX_LENGTH }, allow_nil: true
|
|
149
|
+
validates :cost_category_id, length: { maximum: Constants::COST_CATEGORY_ID_MAX_LENGTH }, allow_nil: true
|
|
150
|
+
validates :cost_category_id2, length: { maximum: Constants::COST_CATEGORY_ID_MAX_LENGTH }, allow_nil: true
|
|
151
|
+
|
|
152
|
+
# Format validations (codes and patterns)
|
|
153
|
+
validates :currency_code, inclusion: { in: Constants::CURRENCY_CODES }, allow_nil: true
|
|
154
|
+
validates :bank_country, inclusion: { in: Constants::COUNTRY_CODES }, allow_nil: true
|
|
155
|
+
validates :ship_from_country, inclusion: { in: Constants::COUNTRY_CODES }, allow_nil: true
|
|
156
|
+
validates :ship_to_country, inclusion: { in: Constants::COUNTRY_CODES }, allow_nil: true
|
|
157
|
+
validates :invoice_id, format: { with: Constants::INVOICE_ID_PATTERN }, allow_blank: true
|
|
158
|
+
validates :internal_invoice_id, format: { with: Constants::INTERNAL_INVOICE_ID_PATTERN }, allow_blank: true
|
|
159
|
+
validates :order_id, format: { with: Constants::ORDER_ID_PATTERN }, allow_blank: true
|
|
160
|
+
validates :iban, format: { with: Constants::IBAN_PATTERN }, allow_blank: true
|
|
161
|
+
validates :swift_code, format: { with: Constants::SWIFT_PATTERN }, allow_blank: true
|
|
162
|
+
validates :vat_id, format: { with: Constants::VAT_ID_PATTERN }, allow_blank: true
|
|
163
|
+
|
|
164
|
+
# Date range validations
|
|
165
|
+
validates :date, date_range: true, allow_nil: true
|
|
166
|
+
validates :due_date, date_range: true, allow_nil: true
|
|
167
|
+
validates :delivery_date, date_range: true, allow_nil: true
|
|
168
|
+
validates :paid_at, date_range: true, allow_nil: true
|
|
169
|
+
validates :discount_payment_date, date_range: true, allow_nil: true
|
|
170
|
+
validates :discount_payment_date_2, date_range: true, allow_nil: true
|
|
171
|
+
|
|
172
|
+
# Complex validators
|
|
173
|
+
validates_with Validators::DiscountDatesValidator
|
|
174
|
+
validates_with Validators::PaymentConditionsValidator
|
|
175
|
+
validates_with Validators::ConsolidationValidator
|
|
176
|
+
|
|
177
|
+
# Base element fields (from DATEV base xsd:extension)
|
|
178
|
+
attribute :date, :date # Document date (required)
|
|
179
|
+
attribute :amount, :decimal # Amount (required, non-zero)
|
|
180
|
+
attribute :discount_amount, :decimal # Discount amount (tier 1)
|
|
181
|
+
attribute :account_no, :integer # General ledger account (4-8 digits)
|
|
182
|
+
attribute :bu_code, :integer # Booking key
|
|
183
|
+
attribute :cost_amount, :decimal # Cost quantity (KOST Menge)
|
|
184
|
+
attribute :cost_category_id, :string # Cost center 1 (KOST1)
|
|
185
|
+
attribute :cost_category_id2, :string # Cost center 2 (KOST2)
|
|
186
|
+
attribute :tax, :decimal # Tax rate percentage
|
|
187
|
+
attribute :information, :string # Free text (max 120 chars)
|
|
188
|
+
|
|
189
|
+
# Base1 element fields (from DATEV base1 xsd:extension)
|
|
190
|
+
attribute :currency_code, :string # ISO 4217 currency code (required)
|
|
191
|
+
attribute :invoice_id, :string # Invoice number (required, max 36 chars)
|
|
192
|
+
attribute :booking_text, :string # Booking text (max 60 chars)
|
|
193
|
+
attribute :type_of_receivable, :string # Type of receivable
|
|
194
|
+
attribute :own_vat_id, :string # Own VAT ID (for OSS procedure)
|
|
195
|
+
attribute :ship_from_country, :string # Ship from country code (ISO 3166)
|
|
196
|
+
attribute :party_id, :string # Customer/supplier number
|
|
197
|
+
attribute :paid_at, :date # Payment date
|
|
198
|
+
attribute :internal_invoice_id, :string # Internal invoice number (max 12 chars)
|
|
199
|
+
attribute :vat_id, :string # Business partner VAT ID (max 15 chars)
|
|
200
|
+
attribute :ship_to_country, :string # Ship to country code (ISO 3166)
|
|
201
|
+
attribute :exchange_rate, :decimal # Exchange rate
|
|
202
|
+
attribute :bank_code, :string # Bank code (max 10 chars)
|
|
203
|
+
attribute :bank_account, :string # Bank account number (max 30 chars)
|
|
204
|
+
attribute :bank_country, :string # Bank country code (ISO 3166)
|
|
205
|
+
attribute :iban, :string # IBAN (max 34 chars)
|
|
206
|
+
attribute :swift_code, :string # SWIFT/BIC code (max 11 chars)
|
|
207
|
+
attribute :account_name, :string # Name of GL account (max 40 chars)
|
|
208
|
+
attribute :payment_conditions_id, :integer # Payment terms ID
|
|
209
|
+
attribute :payment_order, :boolean # Direct debit checkbox
|
|
210
|
+
attribute :discount_percentage, :decimal # Discount percentage tier 1
|
|
211
|
+
attribute :discount_payment_date, :date # Due date with discount tier 1
|
|
212
|
+
attribute :discount_amount_2, :decimal # Discount amount tier 2
|
|
213
|
+
attribute :discount_percentage_2, :decimal # Discount percentage tier 2
|
|
214
|
+
attribute :discount_payment_date_2, :date # Due date with discount tier 2
|
|
215
|
+
attribute :due_date, :date # Due date without discount
|
|
216
|
+
attribute :bp_account_no, :integer # Business partner account number
|
|
217
|
+
attribute :delivery_date, :date # Service/delivery date
|
|
218
|
+
attribute :order_id, :string # Order number (max 30 chars)
|
|
219
|
+
|
|
220
|
+
# accountsPayableLedger specific fields
|
|
221
|
+
attribute :supplier_name, :string # Supplier's name (max 50 chars)
|
|
222
|
+
attribute :supplier_city, :string # Supplier's location (max 30 chars)
|
|
223
|
+
|
|
224
|
+
# Initialize with optional line_items array
|
|
225
|
+
def initialize(attributes = {})
|
|
226
|
+
super
|
|
227
|
+
@line_items = []
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# @return [Array<LineItem>] The line items for this invoice
|
|
231
|
+
attr_reader :line_items
|
|
232
|
+
|
|
233
|
+
# Add a line item to this invoice.
|
|
234
|
+
#
|
|
235
|
+
# When line items are added, consolidation values are computed from
|
|
236
|
+
# the line items rather than from the invoice's own attributes.
|
|
237
|
+
#
|
|
238
|
+
# @param attributes [Hash] Line item attributes
|
|
239
|
+
# @return [LineItem] The created line item
|
|
240
|
+
def add_line_item(**attributes)
|
|
241
|
+
line_item = LineItem.new(**attributes)
|
|
242
|
+
@line_items << line_item
|
|
243
|
+
line_item
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Check if this invoice has multiple line items.
|
|
247
|
+
#
|
|
248
|
+
# @return [Boolean] true if the invoice has line items
|
|
249
|
+
def multi_line?
|
|
250
|
+
@line_items.any?
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Consolidated amount for the DATEV consolidate element.
|
|
254
|
+
# When there are line items, sums all line item amounts.
|
|
255
|
+
# Otherwise, returns the invoice's amount.
|
|
256
|
+
#
|
|
257
|
+
# @return [BigDecimal, nil] The consolidated amount
|
|
258
|
+
def consolidated_amount
|
|
259
|
+
if multi_line?
|
|
260
|
+
@line_items.sum { |item| item.amount || BigDecimal("0") }
|
|
261
|
+
else
|
|
262
|
+
amount
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Consolidated date for the DATEV consolidate element.
|
|
267
|
+
# When there are line items, returns the date from the first line item.
|
|
268
|
+
# Otherwise, returns the invoice's date.
|
|
269
|
+
#
|
|
270
|
+
# @return [Date, nil] The consolidated date
|
|
271
|
+
def consolidated_date
|
|
272
|
+
if multi_line?
|
|
273
|
+
@line_items.first.date || date
|
|
274
|
+
else
|
|
275
|
+
date
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# Consolidated invoice ID for the DATEV consolidate element.
|
|
280
|
+
# When there are line items, returns the invoice_id from the first line item.
|
|
281
|
+
# Otherwise, returns the invoice's invoice_id.
|
|
282
|
+
#
|
|
283
|
+
# @return [String, nil] The consolidated invoice ID
|
|
284
|
+
def consolidated_invoice_id
|
|
285
|
+
if multi_line?
|
|
286
|
+
@line_items.first.invoice_id || invoice_id
|
|
287
|
+
else
|
|
288
|
+
invoice_id
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
# Consolidated currency code for the DATEV consolidate element.
|
|
293
|
+
# When there are line items, returns the currency_code from the first line item.
|
|
294
|
+
# Otherwise, returns the invoice's currency_code.
|
|
295
|
+
#
|
|
296
|
+
# @return [String, nil] The consolidated currency code
|
|
297
|
+
def consolidated_currency_code
|
|
298
|
+
if multi_line?
|
|
299
|
+
@line_items.first.currency_code || currency_code
|
|
300
|
+
else
|
|
301
|
+
currency_code
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# Consolidated delivery date for the DATEV consolidate element.
|
|
306
|
+
# When there are line items, returns the delivery_date from the first line item.
|
|
307
|
+
# Otherwise, returns the invoice's delivery_date.
|
|
308
|
+
#
|
|
309
|
+
# @return [Date, nil] The consolidated delivery date
|
|
310
|
+
def consolidated_delivery_date
|
|
311
|
+
if multi_line?
|
|
312
|
+
@line_items.first.delivery_date || delivery_date
|
|
313
|
+
else
|
|
314
|
+
delivery_date
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# Consolidated order ID for the DATEV consolidate element.
|
|
319
|
+
# When there are line items, returns the order_id from the first line item.
|
|
320
|
+
# Otherwise, returns the invoice's order_id.
|
|
321
|
+
#
|
|
322
|
+
# @return [String, nil] The consolidated order ID
|
|
323
|
+
def consolidated_order_id
|
|
324
|
+
if multi_line?
|
|
325
|
+
@line_items.first.order_id || order_id
|
|
326
|
+
else
|
|
327
|
+
order_id
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# Return a hash of all attributes with non-nil values.
|
|
332
|
+
# Does not include line_items.
|
|
333
|
+
#
|
|
334
|
+
# @return [Hash] Attributes hash
|
|
335
|
+
def to_h
|
|
336
|
+
attributes.compact.transform_keys(&:to_sym)
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
end
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_model"
|
|
4
|
+
require "date"
|
|
5
|
+
|
|
6
|
+
module ZipDatev
|
|
7
|
+
# Represents a single line item within an invoice.
|
|
8
|
+
#
|
|
9
|
+
# Line items are used when an invoice has multiple tax rates or needs
|
|
10
|
+
# to be split across different accounts. Each line item becomes an
|
|
11
|
+
# individual <accountsPayableLedger> entry in the generated XML.
|
|
12
|
+
#
|
|
13
|
+
# Line items contain most of the fields from the DATEV base and base1
|
|
14
|
+
# element specifications, allowing detailed per-item data.
|
|
15
|
+
#
|
|
16
|
+
# @example Creating a line item with 19% tax
|
|
17
|
+
# line_item = ZipDatev::LineItem.new(
|
|
18
|
+
# amount: 119.00,
|
|
19
|
+
# tax: 19.00,
|
|
20
|
+
# account_no: 8400,
|
|
21
|
+
# booking_text: "Revenue 19%"
|
|
22
|
+
# )
|
|
23
|
+
class LineItem
|
|
24
|
+
include ActiveModel::Model
|
|
25
|
+
include ActiveModel::Attributes
|
|
26
|
+
|
|
27
|
+
# Amount validations (amount is optional for line items, inherited from invoice)
|
|
28
|
+
validates :amount, numericality: {
|
|
29
|
+
other_than: 0,
|
|
30
|
+
greater_than_or_equal_to: Constants::MIN_AMOUNT,
|
|
31
|
+
less_than_or_equal_to: Constants::MAX_AMOUNT
|
|
32
|
+
}, allow_nil: true
|
|
33
|
+
validates :amount, decimal_precision: { places: 2 }, allow_nil: true
|
|
34
|
+
|
|
35
|
+
# Discount amount validations
|
|
36
|
+
validates :discount_amount, numericality: {
|
|
37
|
+
greater_than_or_equal_to: Constants::MIN_DISCOUNT_AMOUNT,
|
|
38
|
+
less_than_or_equal_to: Constants::MAX_DISCOUNT_AMOUNT
|
|
39
|
+
}, allow_nil: true
|
|
40
|
+
validates :discount_amount, decimal_precision: { places: 2 }, allow_nil: true
|
|
41
|
+
|
|
42
|
+
validates :discount_amount_2, numericality: {
|
|
43
|
+
greater_than_or_equal_to: Constants::MIN_DISCOUNT_AMOUNT,
|
|
44
|
+
less_than_or_equal_to: Constants::MAX_DISCOUNT_AMOUNT
|
|
45
|
+
}, allow_nil: true
|
|
46
|
+
validates :discount_amount_2, decimal_precision: { places: 2 }, allow_nil: true
|
|
47
|
+
|
|
48
|
+
# Tax validations
|
|
49
|
+
validates :tax, numericality: {
|
|
50
|
+
greater_than_or_equal_to: Constants::MIN_TAX,
|
|
51
|
+
less_than_or_equal_to: Constants::MAX_TAX
|
|
52
|
+
}, allow_nil: true
|
|
53
|
+
validates :tax, decimal_precision: { places: 2 }, allow_nil: true
|
|
54
|
+
|
|
55
|
+
# Discount percentage validations
|
|
56
|
+
validates :discount_percentage, numericality: {
|
|
57
|
+
greater_than_or_equal_to: Constants::MIN_TAX,
|
|
58
|
+
less_than_or_equal_to: Constants::MAX_TAX
|
|
59
|
+
}, allow_nil: true
|
|
60
|
+
validates :discount_percentage, decimal_precision: { places: 2 }, allow_nil: true
|
|
61
|
+
|
|
62
|
+
validates :discount_percentage_2, numericality: {
|
|
63
|
+
greater_than_or_equal_to: Constants::MIN_TAX,
|
|
64
|
+
less_than_or_equal_to: Constants::MAX_TAX
|
|
65
|
+
}, allow_nil: true
|
|
66
|
+
validates :discount_percentage_2, decimal_precision: { places: 2 }, allow_nil: true
|
|
67
|
+
|
|
68
|
+
# Exchange rate validations
|
|
69
|
+
validates :exchange_rate, numericality: {
|
|
70
|
+
greater_than_or_equal_to: Constants::MIN_EXCHANGE_RATE,
|
|
71
|
+
less_than_or_equal_to: Constants::MAX_EXCHANGE_RATE
|
|
72
|
+
}, allow_nil: true
|
|
73
|
+
validates :exchange_rate, decimal_precision: { places: 6 }, allow_nil: true
|
|
74
|
+
|
|
75
|
+
# Cost amount validations
|
|
76
|
+
validates :cost_amount, decimal_precision: { places: 4 }, allow_nil: true
|
|
77
|
+
|
|
78
|
+
# Account number validations
|
|
79
|
+
validates :account_no, numericality: {
|
|
80
|
+
only_integer: true,
|
|
81
|
+
greater_than_or_equal_to: Constants::MIN_ACCOUNT_NO,
|
|
82
|
+
less_than_or_equal_to: Constants::MAX_ACCOUNT_NO
|
|
83
|
+
}, allow_nil: true
|
|
84
|
+
|
|
85
|
+
validates :bp_account_no, numericality: {
|
|
86
|
+
only_integer: true,
|
|
87
|
+
greater_than_or_equal_to: Constants::MIN_BP_ACCOUNT_NO,
|
|
88
|
+
less_than_or_equal_to: Constants::MAX_BP_ACCOUNT_NO
|
|
89
|
+
}, allow_nil: true
|
|
90
|
+
|
|
91
|
+
validates :bu_code, numericality: {
|
|
92
|
+
only_integer: true,
|
|
93
|
+
greater_than_or_equal_to: Constants::MIN_BU_CODE,
|
|
94
|
+
less_than_or_equal_to: Constants::MAX_BU_CODE
|
|
95
|
+
}, allow_nil: true
|
|
96
|
+
|
|
97
|
+
validates :payment_conditions_id, numericality: {
|
|
98
|
+
only_integer: true,
|
|
99
|
+
greater_than_or_equal_to: Constants::MIN_PAYMENT_CONDITIONS_ID,
|
|
100
|
+
less_than_or_equal_to: Constants::MAX_PAYMENT_CONDITIONS_ID
|
|
101
|
+
}, allow_nil: true
|
|
102
|
+
|
|
103
|
+
# String length validations
|
|
104
|
+
validates :invoice_id, length: { maximum: Constants::INVOICE_ID_MAX_LENGTH }, allow_nil: true
|
|
105
|
+
validates :internal_invoice_id, length: { maximum: Constants::INTERNAL_INVOICE_ID_MAX_LENGTH }, allow_nil: true
|
|
106
|
+
validates :order_id, length: { maximum: Constants::ORDER_ID_MAX_LENGTH }, allow_nil: true
|
|
107
|
+
validates :booking_text, length: { maximum: Constants::BOOKING_TEXT_MAX_LENGTH }, allow_nil: true
|
|
108
|
+
validates :information, length: { maximum: Constants::INFORMATION_MAX_LENGTH }, allow_nil: true
|
|
109
|
+
validates :supplier_name, length: { maximum: Constants::SUPPLIER_NAME_MAX_LENGTH }, allow_nil: true
|
|
110
|
+
validates :supplier_city, length: { maximum: Constants::SUPPLIER_CITY_MAX_LENGTH }, allow_nil: true
|
|
111
|
+
validates :account_name, length: { maximum: Constants::ACCOUNT_NAME_MAX_LENGTH }, allow_nil: true
|
|
112
|
+
validates :party_id, length: { maximum: Constants::PARTY_ID_MAX_LENGTH }, allow_nil: true
|
|
113
|
+
validates :vat_id, length: { maximum: Constants::VAT_ID_MAX_LENGTH }, allow_nil: true
|
|
114
|
+
validates :iban, length: { maximum: Constants::IBAN_MAX_LENGTH }, allow_nil: true
|
|
115
|
+
validates :swift_code, length: { maximum: Constants::SWIFT_MAX_LENGTH }, allow_nil: true
|
|
116
|
+
validates :bank_code, length: { maximum: Constants::BANK_CODE_MAX_LENGTH }, allow_nil: true
|
|
117
|
+
validates :bank_account, length: { maximum: Constants::BANK_ACCOUNT_MAX_LENGTH }, allow_nil: true
|
|
118
|
+
validates :cost_category_id, length: { maximum: Constants::COST_CATEGORY_ID_MAX_LENGTH }, allow_nil: true
|
|
119
|
+
validates :cost_category_id2, length: { maximum: Constants::COST_CATEGORY_ID_MAX_LENGTH }, allow_nil: true
|
|
120
|
+
|
|
121
|
+
# Format validations (codes and patterns)
|
|
122
|
+
validates :currency_code, inclusion: { in: Constants::CURRENCY_CODES }, allow_nil: true
|
|
123
|
+
validates :bank_country, inclusion: { in: Constants::COUNTRY_CODES }, allow_nil: true
|
|
124
|
+
validates :ship_from_country, inclusion: { in: Constants::COUNTRY_CODES }, allow_nil: true
|
|
125
|
+
validates :ship_to_country, inclusion: { in: Constants::COUNTRY_CODES }, allow_nil: true
|
|
126
|
+
validates :invoice_id, format: { with: Constants::INVOICE_ID_PATTERN }, allow_blank: true
|
|
127
|
+
validates :internal_invoice_id, format: { with: Constants::INTERNAL_INVOICE_ID_PATTERN }, allow_blank: true
|
|
128
|
+
validates :order_id, format: { with: Constants::ORDER_ID_PATTERN }, allow_blank: true
|
|
129
|
+
validates :iban, format: { with: Constants::IBAN_PATTERN }, allow_blank: true
|
|
130
|
+
validates :swift_code, format: { with: Constants::SWIFT_PATTERN }, allow_blank: true
|
|
131
|
+
validates :vat_id, format: { with: Constants::VAT_ID_PATTERN }, allow_blank: true
|
|
132
|
+
|
|
133
|
+
# Date range validations
|
|
134
|
+
validates :date, date_range: true, allow_nil: true
|
|
135
|
+
validates :due_date, date_range: true, allow_nil: true
|
|
136
|
+
validates :delivery_date, date_range: true, allow_nil: true
|
|
137
|
+
validates :paid_at, date_range: true, allow_nil: true
|
|
138
|
+
validates :discount_payment_date, date_range: true, allow_nil: true
|
|
139
|
+
validates :discount_payment_date_2, date_range: true, allow_nil: true
|
|
140
|
+
|
|
141
|
+
# Complex validators
|
|
142
|
+
validates_with Validators::DiscountDatesValidator
|
|
143
|
+
validates_with Validators::PaymentConditionsValidator
|
|
144
|
+
|
|
145
|
+
# Base element fields (from DATEV base xsd:extension)
|
|
146
|
+
attribute :date, :date # Document date
|
|
147
|
+
attribute :amount, :decimal # Amount (gross recommended, non-zero)
|
|
148
|
+
attribute :discount_amount, :decimal # Discount amount (tier 1)
|
|
149
|
+
attribute :account_no, :integer # General ledger account (4-8 digits)
|
|
150
|
+
attribute :bu_code, :integer # Booking key
|
|
151
|
+
attribute :cost_amount, :decimal # Cost quantity (KOST Menge)
|
|
152
|
+
attribute :cost_category_id, :string # Cost center 1 (KOST1)
|
|
153
|
+
attribute :cost_category_id2, :string # Cost center 2 (KOST2)
|
|
154
|
+
attribute :tax, :decimal # Tax rate percentage
|
|
155
|
+
attribute :information, :string # Free text (max 120 chars)
|
|
156
|
+
|
|
157
|
+
# Base1 element fields (from DATEV base1 xsd:extension)
|
|
158
|
+
attribute :currency_code, :string # ISO 4217 currency code
|
|
159
|
+
attribute :invoice_id, :string # Invoice number (max 36 chars)
|
|
160
|
+
attribute :booking_text, :string # Booking text (max 60 chars)
|
|
161
|
+
attribute :type_of_receivable, :string # Type of receivable
|
|
162
|
+
attribute :own_vat_id, :string # Own VAT ID (for OSS procedure)
|
|
163
|
+
attribute :ship_from_country, :string # Ship from country code (ISO 3166)
|
|
164
|
+
attribute :party_id, :string # Customer/supplier number
|
|
165
|
+
attribute :paid_at, :date # Payment date
|
|
166
|
+
attribute :internal_invoice_id, :string # Internal invoice number (max 12 chars)
|
|
167
|
+
attribute :vat_id, :string # Business partner VAT ID (max 15 chars)
|
|
168
|
+
attribute :ship_to_country, :string # Ship to country code (ISO 3166)
|
|
169
|
+
attribute :exchange_rate, :decimal # Exchange rate
|
|
170
|
+
attribute :bank_code, :string # Bank code (max 10 chars)
|
|
171
|
+
attribute :bank_account, :string # Bank account number (max 30 chars)
|
|
172
|
+
attribute :bank_country, :string # Bank country code (ISO 3166)
|
|
173
|
+
attribute :iban, :string # IBAN (max 34 chars)
|
|
174
|
+
attribute :swift_code, :string # SWIFT/BIC code (max 11 chars)
|
|
175
|
+
attribute :account_name, :string # Name of GL account (max 40 chars)
|
|
176
|
+
attribute :payment_conditions_id, :integer # Payment terms ID
|
|
177
|
+
attribute :payment_order, :boolean # Direct debit checkbox
|
|
178
|
+
attribute :discount_percentage, :decimal # Discount percentage tier 1
|
|
179
|
+
attribute :discount_payment_date, :date # Due date with discount tier 1
|
|
180
|
+
attribute :discount_amount_2, :decimal # Discount amount tier 2
|
|
181
|
+
attribute :discount_percentage_2, :decimal # Discount percentage tier 2
|
|
182
|
+
attribute :discount_payment_date_2, :date # Due date with discount tier 2
|
|
183
|
+
attribute :due_date, :date # Due date without discount
|
|
184
|
+
attribute :bp_account_no, :integer # Business partner account number
|
|
185
|
+
attribute :delivery_date, :date # Service/delivery date
|
|
186
|
+
attribute :order_id, :string # Order number (max 30 chars)
|
|
187
|
+
|
|
188
|
+
# accountsPayableLedger specific fields
|
|
189
|
+
attribute :supplier_name, :string # Supplier's name (max 50 chars)
|
|
190
|
+
attribute :supplier_city, :string # Supplier's location (max 30 chars)
|
|
191
|
+
|
|
192
|
+
# Return a hash of all attributes with non-nil values.
|
|
193
|
+
#
|
|
194
|
+
# @return [Hash] Attributes hash
|
|
195
|
+
def to_h
|
|
196
|
+
attributes.compact.transform_keys(&:to_sym)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Backward compatibility: support keyword argument initialization
|
|
200
|
+
# ActiveModel::Model already provides this, but we ensure it works
|
|
201
|
+
# the same way as before.
|
|
202
|
+
end
|
|
203
|
+
end
|