zugpferd 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/lib/zugpferd/cii/mapping.rb +153 -0
- data/lib/zugpferd/cii/reader.rb +219 -0
- data/lib/zugpferd/cii/writer.rb +314 -0
- data/lib/zugpferd/model/allowance_charge.rb +48 -0
- data/lib/zugpferd/model/contact.rb +16 -0
- data/lib/zugpferd/model/invoice.rb +51 -0
- data/lib/zugpferd/model/item.rb +23 -0
- data/lib/zugpferd/model/line_item.rb +32 -0
- data/lib/zugpferd/model/monetary_totals.rb +35 -0
- data/lib/zugpferd/model/payment_instructions.rb +29 -0
- data/lib/zugpferd/model/postal_address.rb +19 -0
- data/lib/zugpferd/model/price.rb +19 -0
- data/lib/zugpferd/model/tax_breakdown.rb +23 -0
- data/lib/zugpferd/model/tax_subtotal.rb +32 -0
- data/lib/zugpferd/model/trade_party.rb +32 -0
- data/lib/zugpferd/ubl/mapping.rb +135 -0
- data/lib/zugpferd/ubl/reader.rb +218 -0
- data/lib/zugpferd/ubl/writer.rb +286 -0
- data/lib/zugpferd.rb +20 -0
- metadata +119 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
require "nokogiri"
|
|
2
|
+
require "date"
|
|
3
|
+
require "bigdecimal"
|
|
4
|
+
require_relative "mapping"
|
|
5
|
+
|
|
6
|
+
module Zugpferd
|
|
7
|
+
module UBL
|
|
8
|
+
# Reads UBL 2.1 Invoice XML into {Model::Invoice}.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# invoice = Zugpferd::UBL::Reader.new.read(File.read("invoice.xml"))
|
|
12
|
+
class Reader
|
|
13
|
+
include Mapping
|
|
14
|
+
|
|
15
|
+
# Parses a UBL 2.1 Invoice XML string.
|
|
16
|
+
#
|
|
17
|
+
# @param xml_string [String] valid UBL 2.1 Invoice XML
|
|
18
|
+
# @return [Model::Invoice]
|
|
19
|
+
# @raise [Nokogiri::XML::SyntaxError] if the XML is malformed
|
|
20
|
+
def read(xml_string)
|
|
21
|
+
doc = Nokogiri::XML(xml_string) { |config| config.strict }
|
|
22
|
+
root = doc.root
|
|
23
|
+
build_invoice(root)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def build_invoice(root)
|
|
29
|
+
Model::Invoice.new(
|
|
30
|
+
number: text(root, INVOICE[:number]),
|
|
31
|
+
issue_date: parse_date(text(root, INVOICE[:issue_date])),
|
|
32
|
+
due_date: parse_date(text(root, INVOICE[:due_date])),
|
|
33
|
+
type_code: text(root, INVOICE[:type_code]),
|
|
34
|
+
currency_code: text(root, INVOICE[:currency_code]),
|
|
35
|
+
buyer_reference: text(root, INVOICE[:buyer_reference]),
|
|
36
|
+
customization_id: text(root, INVOICE[:customization_id]),
|
|
37
|
+
profile_id: text(root, INVOICE[:profile_id]),
|
|
38
|
+
note: text(root, INVOICE[:note]),
|
|
39
|
+
seller: build_party(root.at_xpath(SELLER, NS)),
|
|
40
|
+
buyer: build_party(root.at_xpath(BUYER, NS)),
|
|
41
|
+
line_items: root.xpath(INVOICE_LINE, NS).map { |n| build_line_item(n) },
|
|
42
|
+
allowance_charges: root.xpath(ALLOWANCE_CHARGE, NS).map { |n| build_allowance_charge(n) },
|
|
43
|
+
tax_breakdown: build_tax_breakdown(root.at_xpath(TAX_TOTAL, NS)),
|
|
44
|
+
monetary_totals: build_monetary_totals(root.at_xpath(MONETARY_TOTAL, NS)),
|
|
45
|
+
payment_instructions: build_payment_instructions(root),
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def build_party(node)
|
|
50
|
+
return nil unless node
|
|
51
|
+
|
|
52
|
+
party = Model::TradeParty.new(
|
|
53
|
+
name: text(node, PARTY[:name]),
|
|
54
|
+
trading_name: text(node, PARTY[:trading_name]),
|
|
55
|
+
identifier: text(node, PARTY[:identifier]),
|
|
56
|
+
legal_registration_id: text(node, PARTY[:legal_registration_id]),
|
|
57
|
+
legal_form: text(node, PARTY[:legal_form]),
|
|
58
|
+
vat_identifier: text(node, PARTY[:vat_identifier]),
|
|
59
|
+
electronic_address: text(node, PARTY[:electronic_address]),
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
endpoint = node.at_xpath(PARTY[:electronic_address], NS)
|
|
63
|
+
party.electronic_address_scheme = endpoint["schemeID"] if endpoint
|
|
64
|
+
|
|
65
|
+
addr_node = node.at_xpath(POSTAL_ADDRESS, NS)
|
|
66
|
+
party.postal_address = build_postal_address(addr_node) if addr_node
|
|
67
|
+
|
|
68
|
+
contact_node = node.at_xpath(CONTACT, NS)
|
|
69
|
+
party.contact = build_contact(contact_node) if contact_node
|
|
70
|
+
|
|
71
|
+
party
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def build_postal_address(node)
|
|
75
|
+
Model::PostalAddress.new(
|
|
76
|
+
country_code: text(node, ADDRESS[:country_code]),
|
|
77
|
+
street_name: text(node, ADDRESS[:street_name]),
|
|
78
|
+
city_name: text(node, ADDRESS[:city_name]),
|
|
79
|
+
postal_zone: text(node, ADDRESS[:postal_zone]),
|
|
80
|
+
)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def build_contact(node)
|
|
84
|
+
Model::Contact.new(
|
|
85
|
+
name: text(node, CONTACT_FIELDS[:name]),
|
|
86
|
+
telephone: text(node, CONTACT_FIELDS[:telephone]),
|
|
87
|
+
email: text(node, CONTACT_FIELDS[:email]),
|
|
88
|
+
)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def build_payment_instructions(root)
|
|
92
|
+
means_node = root.at_xpath(PAYMENT_MEANS, NS)
|
|
93
|
+
return nil unless means_node
|
|
94
|
+
|
|
95
|
+
# BT-90: In UBL, creditor reference is a PartyIdentification with schemeID="SEPA" on the seller
|
|
96
|
+
creditor_ref = root.at_xpath(
|
|
97
|
+
"#{SELLER}/cac:PartyIdentification/cbc:ID[@schemeID='SEPA']", NS
|
|
98
|
+
)&.text
|
|
99
|
+
|
|
100
|
+
Model::PaymentInstructions.new(
|
|
101
|
+
payment_means_code: text(means_node, PAYMENT[:payment_means_code]),
|
|
102
|
+
payment_id: text(means_node, PAYMENT[:payment_id]),
|
|
103
|
+
account_id: text(means_node, PAYMENT[:account_id]),
|
|
104
|
+
card_account_id: text(means_node, PAYMENT[:card_account_id]),
|
|
105
|
+
card_network_id: text(means_node, PAYMENT[:card_network_id]),
|
|
106
|
+
card_holder_name: text(means_node, PAYMENT[:card_holder_name]),
|
|
107
|
+
mandate_reference: text(means_node, PAYMENT[:mandate_reference]),
|
|
108
|
+
debited_account_id: text(means_node, PAYMENT[:debited_account_id]),
|
|
109
|
+
creditor_reference_id: creditor_ref,
|
|
110
|
+
note: text(root, PAYMENT_TERMS_NOTE),
|
|
111
|
+
)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def build_tax_breakdown(node)
|
|
115
|
+
return nil unless node
|
|
116
|
+
|
|
117
|
+
currency = node.at_xpath("cbc:TaxAmount/@currencyID", NS)&.text
|
|
118
|
+
|
|
119
|
+
breakdown = Model::TaxBreakdown.new(
|
|
120
|
+
tax_amount: text(node, "cbc:TaxAmount"),
|
|
121
|
+
currency_code: currency,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
breakdown.subtotals = node.xpath(TAX_SUBTOTAL, NS).map do |sub|
|
|
125
|
+
sub_currency = sub.at_xpath("cbc:TaxableAmount/@currencyID", NS)&.text
|
|
126
|
+
Model::TaxSubtotal.new(
|
|
127
|
+
taxable_amount: text(sub, TAX[:taxable_amount]),
|
|
128
|
+
tax_amount: text(sub, TAX[:tax_amount]),
|
|
129
|
+
category_code: text(sub, TAX[:category_code]),
|
|
130
|
+
percent: parse_decimal(text(sub, TAX[:percent])),
|
|
131
|
+
currency_code: sub_currency,
|
|
132
|
+
exemption_reason: text(sub, TAX[:exemption_reason]),
|
|
133
|
+
exemption_reason_code: text(sub, TAX[:exemption_reason_code]),
|
|
134
|
+
)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
breakdown
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def build_monetary_totals(node)
|
|
141
|
+
return nil unless node
|
|
142
|
+
|
|
143
|
+
Model::MonetaryTotals.new(
|
|
144
|
+
line_extension_amount: text(node, TOTALS[:line_extension_amount]),
|
|
145
|
+
tax_exclusive_amount: text(node, TOTALS[:tax_exclusive_amount]),
|
|
146
|
+
tax_inclusive_amount: text(node, TOTALS[:tax_inclusive_amount]),
|
|
147
|
+
prepaid_amount: parse_decimal(text(node, TOTALS[:prepaid_amount])),
|
|
148
|
+
payable_rounding_amount: parse_decimal(text(node, TOTALS[:payable_rounding_amount])),
|
|
149
|
+
allowance_total_amount: parse_decimal(text(node, TOTALS[:allowance_total_amount])),
|
|
150
|
+
charge_total_amount: parse_decimal(text(node, TOTALS[:charge_total_amount])),
|
|
151
|
+
payable_amount: text(node, TOTALS[:payable_amount]),
|
|
152
|
+
)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def build_line_item(node)
|
|
156
|
+
item_node = node.at_xpath(ITEM, NS)
|
|
157
|
+
price_node = node.at_xpath(PRICE, NS)
|
|
158
|
+
|
|
159
|
+
Model::LineItem.new(
|
|
160
|
+
id: text(node, LINE[:id]),
|
|
161
|
+
invoiced_quantity: text(node, LINE[:invoiced_quantity]),
|
|
162
|
+
unit_code: node.at_xpath(LINE[:unit_code], NS)&.text,
|
|
163
|
+
line_extension_amount: text(node, LINE[:line_extension_amount]),
|
|
164
|
+
note: text(node, LINE[:note]),
|
|
165
|
+
item: build_item(item_node),
|
|
166
|
+
price: build_price(price_node),
|
|
167
|
+
)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def build_item(node)
|
|
171
|
+
return nil unless node
|
|
172
|
+
|
|
173
|
+
Model::Item.new(
|
|
174
|
+
name: text(node, ITEM_FIELDS[:name]),
|
|
175
|
+
description: text(node, ITEM_FIELDS[:description]),
|
|
176
|
+
sellers_identifier: text(node, ITEM_FIELDS[:sellers_identifier]),
|
|
177
|
+
tax_category: text(node, ITEM_FIELDS[:tax_category]),
|
|
178
|
+
tax_percent: parse_decimal(text(node, ITEM_FIELDS[:tax_percent])),
|
|
179
|
+
)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def build_price(node)
|
|
183
|
+
return nil unless node
|
|
184
|
+
|
|
185
|
+
Model::Price.new(
|
|
186
|
+
amount: text(node, PRICE_FIELDS[:amount]),
|
|
187
|
+
)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def build_allowance_charge(node)
|
|
191
|
+
currency = node.at_xpath("cbc:Amount/@currencyID", NS)&.text
|
|
192
|
+
Model::AllowanceCharge.new(
|
|
193
|
+
charge_indicator: text(node, ALLOWANCE_CHARGE_FIELDS[:charge_indicator]) == "true",
|
|
194
|
+
reason: text(node, ALLOWANCE_CHARGE_FIELDS[:reason]),
|
|
195
|
+
reason_code: text(node, ALLOWANCE_CHARGE_FIELDS[:reason_code]),
|
|
196
|
+
amount: text(node, ALLOWANCE_CHARGE_FIELDS[:amount]),
|
|
197
|
+
base_amount: parse_decimal(text(node, ALLOWANCE_CHARGE_FIELDS[:base_amount])),
|
|
198
|
+
multiplier_factor: parse_decimal(text(node, ALLOWANCE_CHARGE_FIELDS[:multiplier_factor])),
|
|
199
|
+
tax_category_code: text(node, ALLOWANCE_CHARGE_FIELDS[:tax_category_code]),
|
|
200
|
+
tax_percent: parse_decimal(text(node, ALLOWANCE_CHARGE_FIELDS[:tax_percent])),
|
|
201
|
+
currency_code: currency,
|
|
202
|
+
)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def text(node, xpath)
|
|
206
|
+
node.at_xpath(xpath, NS)&.text
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def parse_date(str)
|
|
210
|
+
Date.parse(str) if str
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def parse_decimal(str)
|
|
214
|
+
BigDecimal(str) if str
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
require "nokogiri"
|
|
2
|
+
require_relative "mapping"
|
|
3
|
+
|
|
4
|
+
module Zugpferd
|
|
5
|
+
module UBL
|
|
6
|
+
# Writes {Model::Invoice} to UBL 2.1 Invoice XML.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# xml = Zugpferd::UBL::Writer.new.write(invoice)
|
|
10
|
+
class Writer
|
|
11
|
+
include Mapping
|
|
12
|
+
|
|
13
|
+
# Serializes an invoice to UBL 2.1 XML.
|
|
14
|
+
#
|
|
15
|
+
# @param invoice [Model::Invoice] the invoice to serialize
|
|
16
|
+
# @return [String] UTF-8 encoded XML string
|
|
17
|
+
def write(invoice)
|
|
18
|
+
builder = Nokogiri::XML::Builder.new(encoding: "UTF-8") do |xml|
|
|
19
|
+
xml.Invoice(xmlns: NS["ubl"],
|
|
20
|
+
"xmlns:cac" => NS["cac"],
|
|
21
|
+
"xmlns:cbc" => NS["cbc"]) do
|
|
22
|
+
build_invoice(xml, invoice)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
builder.to_xml
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def build_invoice(xml, inv)
|
|
31
|
+
xml["cbc"].CustomizationID inv.customization_id if inv.customization_id
|
|
32
|
+
xml["cbc"].ProfileID inv.profile_id if inv.profile_id
|
|
33
|
+
xml["cbc"].ID inv.number
|
|
34
|
+
xml["cbc"].IssueDate inv.issue_date.to_s
|
|
35
|
+
xml["cbc"].DueDate inv.due_date.to_s if inv.due_date
|
|
36
|
+
xml["cbc"].InvoiceTypeCode inv.type_code
|
|
37
|
+
xml["cbc"].Note inv.note if inv.note
|
|
38
|
+
xml["cbc"].DocumentCurrencyCode inv.currency_code
|
|
39
|
+
xml["cbc"].BuyerReference inv.buyer_reference if inv.buyer_reference
|
|
40
|
+
|
|
41
|
+
build_supplier(xml, inv.seller, inv.payment_instructions) if inv.seller
|
|
42
|
+
build_customer(xml, inv.buyer) if inv.buyer
|
|
43
|
+
build_payment_means(xml, inv.payment_instructions) if inv.payment_instructions
|
|
44
|
+
build_payment_terms(xml, inv.payment_instructions) if inv.payment_instructions&.note
|
|
45
|
+
inv.allowance_charges.each { |ac| build_allowance_charge(xml, ac, inv.currency_code) }
|
|
46
|
+
build_tax_total(xml, inv.tax_breakdown) if inv.tax_breakdown
|
|
47
|
+
build_monetary_total(xml, inv.monetary_totals, inv.currency_code) if inv.monetary_totals
|
|
48
|
+
inv.line_items.each { |li| build_invoice_line(xml, li, inv.currency_code) }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def build_supplier(xml, party, payment_instructions = nil)
|
|
52
|
+
xml["cac"].AccountingSupplierParty do
|
|
53
|
+
build_party(xml, party,
|
|
54
|
+
creditor_reference_id: payment_instructions&.creditor_reference_id)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def build_customer(xml, party)
|
|
59
|
+
xml["cac"].AccountingCustomerParty do
|
|
60
|
+
build_party(xml, party)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def build_party(xml, party, creditor_reference_id: nil)
|
|
65
|
+
xml["cac"].Party do
|
|
66
|
+
if party.electronic_address
|
|
67
|
+
attrs = {}
|
|
68
|
+
attrs[:schemeID] = party.electronic_address_scheme if party.electronic_address_scheme
|
|
69
|
+
xml["cbc"].EndpointID(party.electronic_address, attrs)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
if party.identifier
|
|
73
|
+
xml["cac"].PartyIdentification do
|
|
74
|
+
xml["cbc"].ID party.identifier
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
if creditor_reference_id
|
|
79
|
+
xml["cac"].PartyIdentification do
|
|
80
|
+
xml["cbc"].ID(creditor_reference_id, schemeID: "SEPA")
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
if party.trading_name
|
|
85
|
+
xml["cac"].PartyName do
|
|
86
|
+
xml["cbc"].Name party.trading_name
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
build_postal_address(xml, party.postal_address) if party.postal_address
|
|
91
|
+
|
|
92
|
+
if party.vat_identifier
|
|
93
|
+
xml["cac"].PartyTaxScheme do
|
|
94
|
+
xml["cbc"].CompanyID party.vat_identifier
|
|
95
|
+
xml["cac"].TaxScheme do
|
|
96
|
+
xml["cbc"].ID "VAT"
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
xml["cac"].PartyLegalEntity do
|
|
102
|
+
xml["cbc"].RegistrationName party.name
|
|
103
|
+
xml["cbc"].CompanyID party.legal_registration_id if party.legal_registration_id
|
|
104
|
+
xml["cbc"].CompanyLegalForm party.legal_form if party.legal_form
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
build_contact(xml, party.contact) if party.contact
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def build_postal_address(xml, addr)
|
|
112
|
+
xml["cac"].PostalAddress do
|
|
113
|
+
xml["cbc"].StreetName addr.street_name if addr.street_name
|
|
114
|
+
xml["cbc"].CityName addr.city_name if addr.city_name
|
|
115
|
+
xml["cbc"].PostalZone addr.postal_zone if addr.postal_zone
|
|
116
|
+
xml["cac"].Country do
|
|
117
|
+
xml["cbc"].IdentificationCode addr.country_code
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def build_contact(xml, contact)
|
|
123
|
+
xml["cac"].Contact do
|
|
124
|
+
xml["cbc"].Name contact.name if contact.name
|
|
125
|
+
xml["cbc"].Telephone contact.telephone if contact.telephone
|
|
126
|
+
xml["cbc"].ElectronicMail contact.email if contact.email
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def build_payment_means(xml, payment)
|
|
131
|
+
xml["cac"].PaymentMeans do
|
|
132
|
+
xml["cbc"].PaymentMeansCode payment.payment_means_code
|
|
133
|
+
xml["cbc"].PaymentID payment.payment_id if payment.payment_id
|
|
134
|
+
if payment.card_account_id
|
|
135
|
+
xml["cac"].CardAccount do
|
|
136
|
+
xml["cbc"].PrimaryAccountNumberID payment.card_account_id
|
|
137
|
+
xml["cbc"].NetworkID(payment.card_network_id || "mapped-from-cii")
|
|
138
|
+
xml["cbc"].HolderName payment.card_holder_name if payment.card_holder_name
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
if payment.account_id
|
|
142
|
+
xml["cac"].PayeeFinancialAccount do
|
|
143
|
+
xml["cbc"].ID payment.account_id
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
if payment.mandate_reference
|
|
147
|
+
xml["cac"].PaymentMandate do
|
|
148
|
+
xml["cbc"].ID payment.mandate_reference
|
|
149
|
+
if payment.debited_account_id
|
|
150
|
+
xml["cac"].PayerFinancialAccount do
|
|
151
|
+
xml["cbc"].ID payment.debited_account_id
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def build_payment_terms(xml, payment)
|
|
160
|
+
xml["cac"].PaymentTerms do
|
|
161
|
+
xml["cbc"].Note payment.note
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def build_tax_total(xml, breakdown)
|
|
166
|
+
xml["cac"].TaxTotal do
|
|
167
|
+
xml["cbc"].TaxAmount(format_decimal(breakdown.tax_amount),
|
|
168
|
+
currencyID: breakdown.currency_code)
|
|
169
|
+
breakdown.subtotals.each do |sub|
|
|
170
|
+
xml["cac"].TaxSubtotal do
|
|
171
|
+
xml["cbc"].TaxableAmount(format_decimal(sub.taxable_amount),
|
|
172
|
+
currencyID: sub.currency_code)
|
|
173
|
+
xml["cbc"].TaxAmount(format_decimal(sub.tax_amount),
|
|
174
|
+
currencyID: sub.currency_code)
|
|
175
|
+
xml["cac"].TaxCategory do
|
|
176
|
+
xml["cbc"].ID sub.category_code
|
|
177
|
+
xml["cbc"].Percent format_decimal(sub.percent) if sub.percent
|
|
178
|
+
xml["cbc"].TaxExemptionReasonCode sub.exemption_reason_code if sub.exemption_reason_code
|
|
179
|
+
xml["cbc"].TaxExemptionReason sub.exemption_reason if sub.exemption_reason
|
|
180
|
+
xml["cac"].TaxScheme do
|
|
181
|
+
xml["cbc"].ID "VAT"
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def build_monetary_total(xml, totals, currency_code)
|
|
190
|
+
xml["cac"].LegalMonetaryTotal do
|
|
191
|
+
xml["cbc"].LineExtensionAmount(format_decimal(totals.line_extension_amount),
|
|
192
|
+
currencyID: currency_code)
|
|
193
|
+
xml["cbc"].TaxExclusiveAmount(format_decimal(totals.tax_exclusive_amount),
|
|
194
|
+
currencyID: currency_code)
|
|
195
|
+
xml["cbc"].TaxInclusiveAmount(format_decimal(totals.tax_inclusive_amount),
|
|
196
|
+
currencyID: currency_code)
|
|
197
|
+
if totals.allowance_total_amount
|
|
198
|
+
xml["cbc"].AllowanceTotalAmount(format_decimal(totals.allowance_total_amount),
|
|
199
|
+
currencyID: currency_code)
|
|
200
|
+
end
|
|
201
|
+
if totals.charge_total_amount
|
|
202
|
+
xml["cbc"].ChargeTotalAmount(format_decimal(totals.charge_total_amount),
|
|
203
|
+
currencyID: currency_code)
|
|
204
|
+
end
|
|
205
|
+
if totals.prepaid_amount
|
|
206
|
+
xml["cbc"].PrepaidAmount(format_decimal(totals.prepaid_amount),
|
|
207
|
+
currencyID: currency_code)
|
|
208
|
+
end
|
|
209
|
+
if totals.payable_rounding_amount
|
|
210
|
+
xml["cbc"].PayableRoundingAmount(format_decimal(totals.payable_rounding_amount),
|
|
211
|
+
currencyID: currency_code)
|
|
212
|
+
end
|
|
213
|
+
xml["cbc"].PayableAmount(format_decimal(totals.payable_amount),
|
|
214
|
+
currencyID: currency_code)
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def build_allowance_charge(xml, ac, currency_code)
|
|
219
|
+
xml["cac"].AllowanceCharge do
|
|
220
|
+
xml["cbc"].ChargeIndicator ac.charge_indicator.to_s
|
|
221
|
+
xml["cbc"].AllowanceChargeReasonCode ac.reason_code if ac.reason_code
|
|
222
|
+
xml["cbc"].AllowanceChargeReason ac.reason if ac.reason
|
|
223
|
+
xml["cbc"].MultiplierFactorNumeric format_decimal(ac.multiplier_factor) if ac.multiplier_factor
|
|
224
|
+
xml["cbc"].Amount(format_decimal(ac.amount), currencyID: currency_code)
|
|
225
|
+
xml["cbc"].BaseAmount(format_decimal(ac.base_amount), currencyID: currency_code) if ac.base_amount
|
|
226
|
+
if ac.tax_category_code
|
|
227
|
+
xml["cac"].TaxCategory do
|
|
228
|
+
xml["cbc"].ID ac.tax_category_code
|
|
229
|
+
xml["cbc"].Percent format_decimal(ac.tax_percent) if ac.tax_percent
|
|
230
|
+
xml["cac"].TaxScheme do
|
|
231
|
+
xml["cbc"].ID "VAT"
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def build_invoice_line(xml, line, currency_code)
|
|
239
|
+
xml["cac"].InvoiceLine do
|
|
240
|
+
xml["cbc"].ID line.id
|
|
241
|
+
xml["cbc"].Note line.note if line.note
|
|
242
|
+
xml["cbc"].InvoicedQuantity(format_decimal(line.invoiced_quantity),
|
|
243
|
+
unitCode: line.unit_code)
|
|
244
|
+
xml["cbc"].LineExtensionAmount(format_decimal(line.line_extension_amount),
|
|
245
|
+
currencyID: currency_code)
|
|
246
|
+
build_item(xml, line.item) if line.item
|
|
247
|
+
build_price(xml, line.price, currency_code) if line.price
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def build_item(xml, item)
|
|
252
|
+
xml["cac"].Item do
|
|
253
|
+
xml["cbc"].Description item.description if item.description
|
|
254
|
+
xml["cbc"].Name item.name
|
|
255
|
+
if item.sellers_identifier
|
|
256
|
+
xml["cac"].SellersItemIdentification do
|
|
257
|
+
xml["cbc"].ID item.sellers_identifier
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
if item.tax_category
|
|
261
|
+
xml["cac"].ClassifiedTaxCategory do
|
|
262
|
+
xml["cbc"].ID item.tax_category
|
|
263
|
+
xml["cbc"].Percent format_decimal(item.tax_percent) if item.tax_percent
|
|
264
|
+
xml["cac"].TaxScheme do
|
|
265
|
+
xml["cbc"].ID "VAT"
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def build_price(xml, price, currency_code)
|
|
273
|
+
xml["cac"].Price do
|
|
274
|
+
xml["cbc"].PriceAmount(format_decimal(price.amount), currencyID: currency_code)
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def format_decimal(value)
|
|
279
|
+
return value.to_s unless value.is_a?(BigDecimal)
|
|
280
|
+
# Remove trailing zeros but keep at least one decimal
|
|
281
|
+
str = value.to_s("F")
|
|
282
|
+
str.sub(/\.?0+$/, "")
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
end
|
data/lib/zugpferd.rb
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
require_relative "zugpferd/model/invoice"
|
|
2
|
+
require_relative "zugpferd/model/trade_party"
|
|
3
|
+
require_relative "zugpferd/model/postal_address"
|
|
4
|
+
require_relative "zugpferd/model/contact"
|
|
5
|
+
require_relative "zugpferd/model/line_item"
|
|
6
|
+
require_relative "zugpferd/model/item"
|
|
7
|
+
require_relative "zugpferd/model/price"
|
|
8
|
+
require_relative "zugpferd/model/monetary_totals"
|
|
9
|
+
require_relative "zugpferd/model/tax_breakdown"
|
|
10
|
+
require_relative "zugpferd/model/tax_subtotal"
|
|
11
|
+
require_relative "zugpferd/model/payment_instructions"
|
|
12
|
+
require_relative "zugpferd/model/allowance_charge"
|
|
13
|
+
require_relative "zugpferd/ubl/reader"
|
|
14
|
+
require_relative "zugpferd/ubl/writer"
|
|
15
|
+
require_relative "zugpferd/cii/reader"
|
|
16
|
+
require_relative "zugpferd/cii/writer"
|
|
17
|
+
|
|
18
|
+
module Zugpferd
|
|
19
|
+
class Error < StandardError; end
|
|
20
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: zugpferd
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Alexander Zeitler
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-02-15 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: nokogiri
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '1.16'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '1.16'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: bigdecimal
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '3.1'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '3.1'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: minitest
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '5.25'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '5.25'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: rake
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '13.0'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '13.0'
|
|
69
|
+
description:
|
|
70
|
+
email:
|
|
71
|
+
executables: []
|
|
72
|
+
extensions: []
|
|
73
|
+
extra_rdoc_files: []
|
|
74
|
+
files:
|
|
75
|
+
- lib/zugpferd.rb
|
|
76
|
+
- lib/zugpferd/cii/mapping.rb
|
|
77
|
+
- lib/zugpferd/cii/reader.rb
|
|
78
|
+
- lib/zugpferd/cii/writer.rb
|
|
79
|
+
- lib/zugpferd/model/allowance_charge.rb
|
|
80
|
+
- lib/zugpferd/model/contact.rb
|
|
81
|
+
- lib/zugpferd/model/invoice.rb
|
|
82
|
+
- lib/zugpferd/model/item.rb
|
|
83
|
+
- lib/zugpferd/model/line_item.rb
|
|
84
|
+
- lib/zugpferd/model/monetary_totals.rb
|
|
85
|
+
- lib/zugpferd/model/payment_instructions.rb
|
|
86
|
+
- lib/zugpferd/model/postal_address.rb
|
|
87
|
+
- lib/zugpferd/model/price.rb
|
|
88
|
+
- lib/zugpferd/model/tax_breakdown.rb
|
|
89
|
+
- lib/zugpferd/model/tax_subtotal.rb
|
|
90
|
+
- lib/zugpferd/model/trade_party.rb
|
|
91
|
+
- lib/zugpferd/ubl/mapping.rb
|
|
92
|
+
- lib/zugpferd/ubl/reader.rb
|
|
93
|
+
- lib/zugpferd/ubl/writer.rb
|
|
94
|
+
homepage: https://alexzeitler.github.io/zugpferd/
|
|
95
|
+
licenses:
|
|
96
|
+
- MIT
|
|
97
|
+
metadata:
|
|
98
|
+
source_code_uri: https://github.com/alexzeitler/zugpferd
|
|
99
|
+
homepage_uri: https://alexzeitler.github.io/zugpferd/
|
|
100
|
+
post_install_message:
|
|
101
|
+
rdoc_options: []
|
|
102
|
+
require_paths:
|
|
103
|
+
- lib
|
|
104
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
105
|
+
requirements:
|
|
106
|
+
- - ">="
|
|
107
|
+
- !ruby/object:Gem::Version
|
|
108
|
+
version: '3.2'
|
|
109
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
110
|
+
requirements:
|
|
111
|
+
- - ">="
|
|
112
|
+
- !ruby/object:Gem::Version
|
|
113
|
+
version: '0'
|
|
114
|
+
requirements: []
|
|
115
|
+
rubygems_version: 3.5.20
|
|
116
|
+
signing_key:
|
|
117
|
+
specification_version: 4
|
|
118
|
+
summary: EN 16931 E-Invoice library for Ruby (UBL + CII)
|
|
119
|
+
test_files: []
|