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,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