zugpferd 0.1.0 → 0.2.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 +4 -4
- data/lib/zugpferd/cii/reader.rb +15 -4
- data/lib/zugpferd/cii/writer.rb +43 -43
- data/lib/zugpferd/model/billing_document.rb +56 -0
- data/lib/zugpferd/model/corrected_invoice.rb +13 -0
- data/lib/zugpferd/model/credit_note.rb +13 -0
- data/lib/zugpferd/model/invoice.rb +3 -40
- data/lib/zugpferd/model/partial_invoice.rb +13 -0
- data/lib/zugpferd/model/prepayment_invoice.rb +13 -0
- data/lib/zugpferd/model/self_billed_invoice.rb +13 -0
- data/lib/zugpferd/ubl/mapping.rb +6 -0
- data/lib/zugpferd/ubl/reader.rb +37 -27
- data/lib/zugpferd/ubl/writer.rb +47 -31
- data/lib/zugpferd.rb +6 -0
- metadata +7 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 132674bb432d021ca01155c3289c34abd9a0f9444242e74b79871aaa155e5dac
|
|
4
|
+
data.tar.gz: 0e03c97fedb053a5a3fce52216c92a64bfbd5671a9bc354fb629c1eae644fc23
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f9f3e87cd9e52a517724829498ed4063aba54b97c9ea78c5e305e99d6ae9a6cd5a7f393836e2c73aa4a1369dbd35b32ff5a7ad053723b616282f207f62447d30
|
|
7
|
+
data.tar.gz: bfa2cd8c8ef42b94f89b21c8482418284682217be802312a659bc4672413cb1a8032e40b163ad874caae5a6da63410f201ee5467ec66a29bafc2011b101ea24d
|
data/lib/zugpferd/cii/reader.rb
CHANGED
|
@@ -5,17 +5,25 @@ require_relative "mapping"
|
|
|
5
5
|
|
|
6
6
|
module Zugpferd
|
|
7
7
|
module CII
|
|
8
|
-
# Reads UN/CEFACT CII CrossIndustryInvoice XML into
|
|
8
|
+
# Reads UN/CEFACT CII CrossIndustryInvoice XML into the appropriate model class.
|
|
9
9
|
#
|
|
10
10
|
# @example
|
|
11
|
-
#
|
|
11
|
+
# doc = Zugpferd::CII::Reader.new.read(File.read("invoice.xml"))
|
|
12
12
|
class Reader
|
|
13
13
|
include Mapping
|
|
14
14
|
|
|
15
|
+
TYPE_CODE_MAP = {
|
|
16
|
+
"381" => Model::CreditNote,
|
|
17
|
+
"384" => Model::CorrectedInvoice,
|
|
18
|
+
"389" => Model::SelfBilledInvoice,
|
|
19
|
+
"326" => Model::PartialInvoice,
|
|
20
|
+
"386" => Model::PrepaymentInvoice,
|
|
21
|
+
}.freeze
|
|
22
|
+
|
|
15
23
|
# Parses a CII CrossIndustryInvoice XML string.
|
|
16
24
|
#
|
|
17
25
|
# @param xml_string [String] valid CII D16B XML
|
|
18
|
-
# @return [Model::
|
|
26
|
+
# @return [Model::BillingDocument]
|
|
19
27
|
# @raise [Nokogiri::XML::SyntaxError] if the XML is malformed
|
|
20
28
|
def read(xml_string)
|
|
21
29
|
doc = Nokogiri::XML(xml_string) { |config| config.strict }
|
|
@@ -27,7 +35,10 @@ module Zugpferd
|
|
|
27
35
|
|
|
28
36
|
def build_invoice(root)
|
|
29
37
|
settlement = root.at_xpath(SETTLEMENT, NS)
|
|
30
|
-
|
|
38
|
+
type_code = text(root, INVOICE[:type_code])
|
|
39
|
+
model_class = TYPE_CODE_MAP.fetch(type_code, Model::Invoice)
|
|
40
|
+
|
|
41
|
+
model_class.new(
|
|
31
42
|
number: text(root, INVOICE[:number]),
|
|
32
43
|
issue_date: parse_cii_date(text(root, INVOICE[:issue_date])),
|
|
33
44
|
due_date: parse_cii_date(settlement ? text(settlement, PAYMENT_TERMS_DUE_DATE) : nil),
|
data/lib/zugpferd/cii/writer.rb
CHANGED
|
@@ -3,18 +3,18 @@ require_relative "mapping"
|
|
|
3
3
|
|
|
4
4
|
module Zugpferd
|
|
5
5
|
module CII
|
|
6
|
-
# Writes
|
|
6
|
+
# Writes a billing document to UN/CEFACT CII CrossIndustryInvoice XML.
|
|
7
7
|
#
|
|
8
8
|
# @example
|
|
9
|
-
# xml = Zugpferd::CII::Writer.new.write(
|
|
9
|
+
# xml = Zugpferd::CII::Writer.new.write(document)
|
|
10
10
|
class Writer
|
|
11
11
|
include Mapping
|
|
12
12
|
|
|
13
|
-
# Serializes
|
|
13
|
+
# Serializes a billing document to CII D16B XML.
|
|
14
14
|
#
|
|
15
|
-
# @param
|
|
15
|
+
# @param document [Model::BillingDocument] the document to serialize
|
|
16
16
|
# @return [String] UTF-8 encoded XML string
|
|
17
|
-
def write(
|
|
17
|
+
def write(document)
|
|
18
18
|
builder = Nokogiri::XML::Builder.new(encoding: "UTF-8") do |xml|
|
|
19
19
|
xml["rsm"].CrossIndustryInvoice(
|
|
20
20
|
"xmlns:rsm" => NS["rsm"],
|
|
@@ -22,9 +22,9 @@ module Zugpferd
|
|
|
22
22
|
"xmlns:qdt" => NS["qdt"],
|
|
23
23
|
"xmlns:udt" => NS["udt"]
|
|
24
24
|
) do
|
|
25
|
-
build_document_context(xml,
|
|
26
|
-
build_exchanged_document(xml,
|
|
27
|
-
build_transaction(xml,
|
|
25
|
+
build_document_context(xml, document)
|
|
26
|
+
build_exchanged_document(xml, document)
|
|
27
|
+
build_transaction(xml, document)
|
|
28
28
|
end
|
|
29
29
|
end
|
|
30
30
|
builder.to_xml
|
|
@@ -32,50 +32,50 @@ module Zugpferd
|
|
|
32
32
|
|
|
33
33
|
private
|
|
34
34
|
|
|
35
|
-
def build_document_context(xml,
|
|
35
|
+
def build_document_context(xml, doc)
|
|
36
36
|
xml["rsm"].ExchangedDocumentContext do
|
|
37
|
-
if
|
|
37
|
+
if doc.profile_id
|
|
38
38
|
xml["ram"].BusinessProcessSpecifiedDocumentContextParameter do
|
|
39
|
-
xml["ram"].ID
|
|
39
|
+
xml["ram"].ID doc.profile_id
|
|
40
40
|
end
|
|
41
41
|
end
|
|
42
|
-
if
|
|
42
|
+
if doc.customization_id
|
|
43
43
|
xml["ram"].GuidelineSpecifiedDocumentContextParameter do
|
|
44
|
-
xml["ram"].ID
|
|
44
|
+
xml["ram"].ID doc.customization_id
|
|
45
45
|
end
|
|
46
46
|
end
|
|
47
47
|
end
|
|
48
48
|
end
|
|
49
49
|
|
|
50
|
-
def build_exchanged_document(xml,
|
|
50
|
+
def build_exchanged_document(xml, doc)
|
|
51
51
|
xml["rsm"].ExchangedDocument do
|
|
52
|
-
xml["ram"].ID
|
|
53
|
-
xml["ram"].TypeCode
|
|
52
|
+
xml["ram"].ID doc.number
|
|
53
|
+
xml["ram"].TypeCode doc.type_code
|
|
54
54
|
xml["ram"].IssueDateTime do
|
|
55
|
-
xml["udt"].DateTimeString(format_cii_date(
|
|
55
|
+
xml["udt"].DateTimeString(format_cii_date(doc.issue_date), format: "102")
|
|
56
56
|
end
|
|
57
|
-
if
|
|
57
|
+
if doc.note
|
|
58
58
|
xml["ram"].IncludedNote do
|
|
59
|
-
xml["ram"].Content
|
|
59
|
+
xml["ram"].Content doc.note
|
|
60
60
|
end
|
|
61
61
|
end
|
|
62
62
|
end
|
|
63
63
|
end
|
|
64
64
|
|
|
65
|
-
def build_transaction(xml,
|
|
65
|
+
def build_transaction(xml, doc)
|
|
66
66
|
xml["rsm"].SupplyChainTradeTransaction do
|
|
67
|
-
|
|
68
|
-
build_agreement(xml,
|
|
67
|
+
doc.line_items.each { |li| build_line_item(xml, li) }
|
|
68
|
+
build_agreement(xml, doc)
|
|
69
69
|
xml["ram"].ApplicableHeaderTradeDelivery
|
|
70
|
-
build_settlement(xml,
|
|
70
|
+
build_settlement(xml, doc)
|
|
71
71
|
end
|
|
72
72
|
end
|
|
73
73
|
|
|
74
|
-
def build_agreement(xml,
|
|
74
|
+
def build_agreement(xml, doc)
|
|
75
75
|
xml["ram"].ApplicableHeaderTradeAgreement do
|
|
76
|
-
xml["ram"].BuyerReference
|
|
77
|
-
build_party(xml, "SellerTradeParty",
|
|
78
|
-
build_party(xml, "BuyerTradeParty",
|
|
76
|
+
xml["ram"].BuyerReference doc.buyer_reference if doc.buyer_reference
|
|
77
|
+
build_party(xml, "SellerTradeParty", doc.seller) if doc.seller
|
|
78
|
+
build_party(xml, "BuyerTradeParty", doc.buyer) if doc.buyer
|
|
79
79
|
end
|
|
80
80
|
end
|
|
81
81
|
|
|
@@ -141,41 +141,41 @@ module Zugpferd
|
|
|
141
141
|
end
|
|
142
142
|
end
|
|
143
143
|
|
|
144
|
-
def build_settlement(xml,
|
|
144
|
+
def build_settlement(xml, doc)
|
|
145
145
|
xml["ram"].ApplicableHeaderTradeSettlement do
|
|
146
|
-
if
|
|
147
|
-
xml["ram"].CreditorReferenceID
|
|
146
|
+
if doc.payment_instructions&.creditor_reference_id
|
|
147
|
+
xml["ram"].CreditorReferenceID doc.payment_instructions.creditor_reference_id
|
|
148
148
|
end
|
|
149
149
|
|
|
150
|
-
if
|
|
151
|
-
xml["ram"].PaymentReference
|
|
150
|
+
if doc.payment_instructions&.payment_id
|
|
151
|
+
xml["ram"].PaymentReference doc.payment_instructions.payment_id
|
|
152
152
|
end
|
|
153
153
|
|
|
154
|
-
xml["ram"].InvoiceCurrencyCode
|
|
154
|
+
xml["ram"].InvoiceCurrencyCode doc.currency_code
|
|
155
155
|
|
|
156
|
-
build_payment_means(xml,
|
|
156
|
+
build_payment_means(xml, doc.payment_instructions) if doc.payment_instructions
|
|
157
157
|
|
|
158
|
-
if
|
|
159
|
-
|
|
158
|
+
if doc.tax_breakdown
|
|
159
|
+
doc.tax_breakdown.subtotals.each do |sub|
|
|
160
160
|
build_tax_subtotal(xml, sub)
|
|
161
161
|
end
|
|
162
162
|
end
|
|
163
163
|
|
|
164
|
-
|
|
164
|
+
doc.allowance_charges.each { |ac| build_allowance_charge(xml, ac) }
|
|
165
165
|
|
|
166
|
-
if
|
|
166
|
+
if doc.payment_instructions&.note || doc.due_date || doc.payment_instructions&.mandate_reference
|
|
167
167
|
xml["ram"].SpecifiedTradePaymentTerms do
|
|
168
|
-
xml["ram"].Description
|
|
169
|
-
if
|
|
168
|
+
xml["ram"].Description doc.payment_instructions.note if doc.payment_instructions&.note
|
|
169
|
+
if doc.due_date
|
|
170
170
|
xml["ram"].DueDateDateTime do
|
|
171
|
-
xml["udt"].DateTimeString(format_cii_date(
|
|
171
|
+
xml["udt"].DateTimeString(format_cii_date(doc.due_date), format: "102")
|
|
172
172
|
end
|
|
173
173
|
end
|
|
174
|
-
xml["ram"].DirectDebitMandateID
|
|
174
|
+
xml["ram"].DirectDebitMandateID doc.payment_instructions.mandate_reference if doc.payment_instructions&.mandate_reference
|
|
175
175
|
end
|
|
176
176
|
end
|
|
177
177
|
|
|
178
|
-
build_monetary_total(xml,
|
|
178
|
+
build_monetary_total(xml, doc.monetary_totals, doc.tax_breakdown) if doc.monetary_totals
|
|
179
179
|
end
|
|
180
180
|
end
|
|
181
181
|
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
require "bigdecimal"
|
|
2
|
+
|
|
3
|
+
module Zugpferd
|
|
4
|
+
module Model
|
|
5
|
+
# Shared behaviour for all billing document types (BG-0).
|
|
6
|
+
#
|
|
7
|
+
# Each including class must define a +TYPE_CODE+ constant that serves as
|
|
8
|
+
# the default value for +type_code+.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# class Invoice
|
|
12
|
+
# include BillingDocument
|
|
13
|
+
# TYPE_CODE = "380"
|
|
14
|
+
# end
|
|
15
|
+
module BillingDocument
|
|
16
|
+
# @return [String] BT-1 Invoice number
|
|
17
|
+
# @return [Date] BT-2 Issue date
|
|
18
|
+
# @return [Date, nil] BT-9 Payment due date
|
|
19
|
+
# @return [String] BT-3 Invoice type code
|
|
20
|
+
# @return [String] BT-5 Document currency code (default: "EUR")
|
|
21
|
+
# @return [String, nil] BT-10 Buyer reference
|
|
22
|
+
# @return [String, nil] BT-24 Specification identifier
|
|
23
|
+
# @return [String, nil] BT-23 Business process type
|
|
24
|
+
# @return [String, nil] BT-22 Invoice note
|
|
25
|
+
# @return [TradeParty, nil] BG-4 Seller party
|
|
26
|
+
# @return [TradeParty, nil] BG-7 Buyer party
|
|
27
|
+
# @return [Array<LineItem>] BG-25 Invoice lines
|
|
28
|
+
# @return [TaxBreakdown, nil] BG-23 VAT breakdown
|
|
29
|
+
# @return [MonetaryTotals, nil] BG-22 Document totals
|
|
30
|
+
# @return [PaymentInstructions, nil] BG-16 Payment information
|
|
31
|
+
# @return [Array<AllowanceCharge>] BG-20/BG-21 Document-level allowances and charges
|
|
32
|
+
attr_accessor :number, :issue_date, :due_date, :type_code,
|
|
33
|
+
:currency_code, :buyer_reference, :customization_id,
|
|
34
|
+
:profile_id, :note, :seller, :buyer, :line_items,
|
|
35
|
+
:tax_breakdown, :monetary_totals, :payment_instructions,
|
|
36
|
+
:allowance_charges
|
|
37
|
+
|
|
38
|
+
# @param number [String] BT-1 Invoice number
|
|
39
|
+
# @param issue_date [Date] BT-2 Issue date
|
|
40
|
+
# @param type_code [String] BT-3 Invoice type code (defaults to the class's TYPE_CODE)
|
|
41
|
+
# @param currency_code [String] BT-5 Document currency code
|
|
42
|
+
# @param rest [Hash] additional attributes set via accessors
|
|
43
|
+
def initialize(number:, issue_date:, type_code: self.class::TYPE_CODE,
|
|
44
|
+
currency_code: "EUR", **rest)
|
|
45
|
+
@number = number
|
|
46
|
+
@issue_date = issue_date
|
|
47
|
+
@type_code = type_code
|
|
48
|
+
@currency_code = currency_code
|
|
49
|
+
@line_items = []
|
|
50
|
+
@allowance_charges = []
|
|
51
|
+
@tax_breakdown = nil
|
|
52
|
+
rest.each { |k, v| public_send(:"#{k}=", v) }
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module Zugpferd
|
|
2
|
+
module Model
|
|
3
|
+
# Corrected Invoice (BG-0), type code 384.
|
|
4
|
+
#
|
|
5
|
+
# @example
|
|
6
|
+
# inv = CorrectedInvoice.new(number: "C-001", issue_date: Date.today)
|
|
7
|
+
class CorrectedInvoice
|
|
8
|
+
include BillingDocument
|
|
9
|
+
|
|
10
|
+
TYPE_CODE = "384"
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -1,51 +1,14 @@
|
|
|
1
|
-
require "bigdecimal"
|
|
2
|
-
|
|
3
1
|
module Zugpferd
|
|
4
2
|
module Model
|
|
5
|
-
#
|
|
3
|
+
# Commercial Invoice (BG-0), type code 380.
|
|
6
4
|
#
|
|
7
5
|
# @example
|
|
8
6
|
# invoice = Invoice.new(number: "INV-001", issue_date: Date.today)
|
|
9
7
|
# invoice.seller = TradeParty.new(name: "Seller GmbH")
|
|
10
8
|
class Invoice
|
|
11
|
-
|
|
12
|
-
# @return [Date] BT-2 Issue date
|
|
13
|
-
# @return [Date, nil] BT-9 Payment due date
|
|
14
|
-
# @return [String] BT-3 Invoice type code (default: "380")
|
|
15
|
-
# @return [String] BT-5 Document currency code (default: "EUR")
|
|
16
|
-
# @return [String, nil] BT-10 Buyer reference
|
|
17
|
-
# @return [String, nil] BT-24 Specification identifier
|
|
18
|
-
# @return [String, nil] BT-23 Business process type
|
|
19
|
-
# @return [String, nil] BT-22 Invoice note
|
|
20
|
-
# @return [TradeParty, nil] BG-4 Seller party
|
|
21
|
-
# @return [TradeParty, nil] BG-7 Buyer party
|
|
22
|
-
# @return [Array<LineItem>] BG-25 Invoice lines
|
|
23
|
-
# @return [TaxBreakdown, nil] BG-23 VAT breakdown
|
|
24
|
-
# @return [MonetaryTotals, nil] BG-22 Document totals
|
|
25
|
-
# @return [PaymentInstructions, nil] BG-16 Payment information
|
|
26
|
-
# @return [Array<AllowanceCharge>] BG-20/BG-21 Document-level allowances and charges
|
|
27
|
-
attr_accessor :number, :issue_date, :due_date, :type_code,
|
|
28
|
-
:currency_code, :buyer_reference, :customization_id,
|
|
29
|
-
:profile_id, :note, :seller, :buyer, :line_items,
|
|
30
|
-
:tax_breakdown, :monetary_totals, :payment_instructions,
|
|
31
|
-
:allowance_charges
|
|
9
|
+
include BillingDocument
|
|
32
10
|
|
|
33
|
-
|
|
34
|
-
# @param issue_date [Date] BT-2 Issue date
|
|
35
|
-
# @param type_code [String] BT-3 Invoice type code
|
|
36
|
-
# @param currency_code [String] BT-5 Document currency code
|
|
37
|
-
# @param rest [Hash] additional attributes set via accessors
|
|
38
|
-
def initialize(number:, issue_date:, type_code: "380",
|
|
39
|
-
currency_code: "EUR", **rest)
|
|
40
|
-
@number = number
|
|
41
|
-
@issue_date = issue_date
|
|
42
|
-
@type_code = type_code
|
|
43
|
-
@currency_code = currency_code
|
|
44
|
-
@line_items = []
|
|
45
|
-
@allowance_charges = []
|
|
46
|
-
@tax_breakdown = nil
|
|
47
|
-
rest.each { |k, v| public_send(:"#{k}=", v) }
|
|
48
|
-
end
|
|
11
|
+
TYPE_CODE = "380"
|
|
49
12
|
end
|
|
50
13
|
end
|
|
51
14
|
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module Zugpferd
|
|
2
|
+
module Model
|
|
3
|
+
# Prepayment Invoice (BG-0), type code 386.
|
|
4
|
+
#
|
|
5
|
+
# @example
|
|
6
|
+
# inv = PrepaymentInvoice.new(number: "PP-001", issue_date: Date.today)
|
|
7
|
+
class PrepaymentInvoice
|
|
8
|
+
include BillingDocument
|
|
9
|
+
|
|
10
|
+
TYPE_CODE = "386"
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module Zugpferd
|
|
2
|
+
module Model
|
|
3
|
+
# Self-billed Invoice (BG-0), type code 389.
|
|
4
|
+
#
|
|
5
|
+
# @example
|
|
6
|
+
# inv = SelfBilledInvoice.new(number: "SB-001", issue_date: Date.today)
|
|
7
|
+
class SelfBilledInvoice
|
|
8
|
+
include BillingDocument
|
|
9
|
+
|
|
10
|
+
TYPE_CODE = "389"
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
data/lib/zugpferd/ubl/mapping.rb
CHANGED
|
@@ -7,6 +7,12 @@ module Zugpferd
|
|
|
7
7
|
"cbc" => "urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2",
|
|
8
8
|
}.freeze
|
|
9
9
|
|
|
10
|
+
CN_NS = {
|
|
11
|
+
"cn" => "urn:oasis:names:specification:ubl:schema:xsd:CreditNote-2",
|
|
12
|
+
"cac" => "urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2",
|
|
13
|
+
"cbc" => "urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2",
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
10
16
|
# Invoice (BG-0)
|
|
11
17
|
INVOICE = {
|
|
12
18
|
number: "cbc:ID",
|
data/lib/zugpferd/ubl/reader.rb
CHANGED
|
@@ -5,43 +5,50 @@ require_relative "mapping"
|
|
|
5
5
|
|
|
6
6
|
module Zugpferd
|
|
7
7
|
module UBL
|
|
8
|
-
# Reads UBL 2.1 Invoice XML into
|
|
8
|
+
# Reads UBL 2.1 Invoice or Credit Note XML into the appropriate model class.
|
|
9
9
|
#
|
|
10
10
|
# @example
|
|
11
|
-
#
|
|
11
|
+
# doc = Zugpferd::UBL::Reader.new.read(File.read("invoice.xml"))
|
|
12
12
|
class Reader
|
|
13
13
|
include Mapping
|
|
14
14
|
|
|
15
|
-
# Parses a UBL 2.1 Invoice XML string.
|
|
15
|
+
# Parses a UBL 2.1 Invoice or Credit Note XML string.
|
|
16
16
|
#
|
|
17
|
-
# @param xml_string [String] valid UBL 2.1 Invoice XML
|
|
18
|
-
# @return [Model::
|
|
17
|
+
# @param xml_string [String] valid UBL 2.1 Invoice or Credit Note XML
|
|
18
|
+
# @return [Model::BillingDocument]
|
|
19
19
|
# @raise [Nokogiri::XML::SyntaxError] if the XML is malformed
|
|
20
20
|
def read(xml_string)
|
|
21
21
|
doc = Nokogiri::XML(xml_string) { |config| config.strict }
|
|
22
22
|
root = doc.root
|
|
23
|
+
@credit_note = root.name == "CreditNote"
|
|
24
|
+
@ns = @credit_note ? CN_NS : NS
|
|
23
25
|
build_invoice(root)
|
|
24
26
|
end
|
|
25
27
|
|
|
26
28
|
private
|
|
27
29
|
|
|
28
30
|
def build_invoice(root)
|
|
29
|
-
|
|
31
|
+
type_code_element = @credit_note ? "cbc:CreditNoteTypeCode" : INVOICE[:type_code]
|
|
32
|
+
line_element = @credit_note ? "cac:CreditNoteLine" : INVOICE_LINE
|
|
33
|
+
|
|
34
|
+
model_class = @credit_note ? Model::CreditNote : Model::Invoice
|
|
35
|
+
|
|
36
|
+
model_class.new(
|
|
30
37
|
number: text(root, INVOICE[:number]),
|
|
31
38
|
issue_date: parse_date(text(root, INVOICE[:issue_date])),
|
|
32
39
|
due_date: parse_date(text(root, INVOICE[:due_date])),
|
|
33
|
-
type_code: text(root,
|
|
40
|
+
type_code: text(root, type_code_element),
|
|
34
41
|
currency_code: text(root, INVOICE[:currency_code]),
|
|
35
42
|
buyer_reference: text(root, INVOICE[:buyer_reference]),
|
|
36
43
|
customization_id: text(root, INVOICE[:customization_id]),
|
|
37
44
|
profile_id: text(root, INVOICE[:profile_id]),
|
|
38
45
|
note: text(root, INVOICE[:note]),
|
|
39
|
-
seller: build_party(root.at_xpath(SELLER,
|
|
40
|
-
buyer: build_party(root.at_xpath(BUYER,
|
|
41
|
-
line_items: root.xpath(
|
|
42
|
-
allowance_charges: root.xpath(ALLOWANCE_CHARGE,
|
|
43
|
-
tax_breakdown: build_tax_breakdown(root.at_xpath(TAX_TOTAL,
|
|
44
|
-
monetary_totals: build_monetary_totals(root.at_xpath(MONETARY_TOTAL,
|
|
46
|
+
seller: build_party(root.at_xpath(SELLER, @ns)),
|
|
47
|
+
buyer: build_party(root.at_xpath(BUYER, @ns)),
|
|
48
|
+
line_items: root.xpath(line_element, @ns).map { |n| build_line_item(n) },
|
|
49
|
+
allowance_charges: root.xpath(ALLOWANCE_CHARGE, @ns).map { |n| build_allowance_charge(n) },
|
|
50
|
+
tax_breakdown: build_tax_breakdown(root.at_xpath(TAX_TOTAL, @ns)),
|
|
51
|
+
monetary_totals: build_monetary_totals(root.at_xpath(MONETARY_TOTAL, @ns)),
|
|
45
52
|
payment_instructions: build_payment_instructions(root),
|
|
46
53
|
)
|
|
47
54
|
end
|
|
@@ -59,13 +66,13 @@ module Zugpferd
|
|
|
59
66
|
electronic_address: text(node, PARTY[:electronic_address]),
|
|
60
67
|
)
|
|
61
68
|
|
|
62
|
-
endpoint = node.at_xpath(PARTY[:electronic_address],
|
|
69
|
+
endpoint = node.at_xpath(PARTY[:electronic_address], @ns)
|
|
63
70
|
party.electronic_address_scheme = endpoint["schemeID"] if endpoint
|
|
64
71
|
|
|
65
|
-
addr_node = node.at_xpath(POSTAL_ADDRESS,
|
|
72
|
+
addr_node = node.at_xpath(POSTAL_ADDRESS, @ns)
|
|
66
73
|
party.postal_address = build_postal_address(addr_node) if addr_node
|
|
67
74
|
|
|
68
|
-
contact_node = node.at_xpath(CONTACT,
|
|
75
|
+
contact_node = node.at_xpath(CONTACT, @ns)
|
|
69
76
|
party.contact = build_contact(contact_node) if contact_node
|
|
70
77
|
|
|
71
78
|
party
|
|
@@ -89,12 +96,12 @@ module Zugpferd
|
|
|
89
96
|
end
|
|
90
97
|
|
|
91
98
|
def build_payment_instructions(root)
|
|
92
|
-
means_node = root.at_xpath(PAYMENT_MEANS,
|
|
99
|
+
means_node = root.at_xpath(PAYMENT_MEANS, @ns)
|
|
93
100
|
return nil unless means_node
|
|
94
101
|
|
|
95
102
|
# BT-90: In UBL, creditor reference is a PartyIdentification with schemeID="SEPA" on the seller
|
|
96
103
|
creditor_ref = root.at_xpath(
|
|
97
|
-
"#{SELLER}/cac:PartyIdentification/cbc:ID[@schemeID='SEPA']",
|
|
104
|
+
"#{SELLER}/cac:PartyIdentification/cbc:ID[@schemeID='SEPA']", @ns
|
|
98
105
|
)&.text
|
|
99
106
|
|
|
100
107
|
Model::PaymentInstructions.new(
|
|
@@ -114,15 +121,15 @@ module Zugpferd
|
|
|
114
121
|
def build_tax_breakdown(node)
|
|
115
122
|
return nil unless node
|
|
116
123
|
|
|
117
|
-
currency = node.at_xpath("cbc:TaxAmount/@currencyID",
|
|
124
|
+
currency = node.at_xpath("cbc:TaxAmount/@currencyID", @ns)&.text
|
|
118
125
|
|
|
119
126
|
breakdown = Model::TaxBreakdown.new(
|
|
120
127
|
tax_amount: text(node, "cbc:TaxAmount"),
|
|
121
128
|
currency_code: currency,
|
|
122
129
|
)
|
|
123
130
|
|
|
124
|
-
breakdown.subtotals = node.xpath(TAX_SUBTOTAL,
|
|
125
|
-
sub_currency = sub.at_xpath("cbc:TaxableAmount/@currencyID",
|
|
131
|
+
breakdown.subtotals = node.xpath(TAX_SUBTOTAL, @ns).map do |sub|
|
|
132
|
+
sub_currency = sub.at_xpath("cbc:TaxableAmount/@currencyID", @ns)&.text
|
|
126
133
|
Model::TaxSubtotal.new(
|
|
127
134
|
taxable_amount: text(sub, TAX[:taxable_amount]),
|
|
128
135
|
tax_amount: text(sub, TAX[:tax_amount]),
|
|
@@ -153,13 +160,16 @@ module Zugpferd
|
|
|
153
160
|
end
|
|
154
161
|
|
|
155
162
|
def build_line_item(node)
|
|
156
|
-
item_node = node.at_xpath(ITEM,
|
|
157
|
-
price_node = node.at_xpath(PRICE,
|
|
163
|
+
item_node = node.at_xpath(ITEM, @ns)
|
|
164
|
+
price_node = node.at_xpath(PRICE, @ns)
|
|
165
|
+
|
|
166
|
+
quantity_element = @credit_note ? "cbc:CreditedQuantity" : LINE[:invoiced_quantity]
|
|
167
|
+
unit_code_element = @credit_note ? "cbc:CreditedQuantity/@unitCode" : LINE[:unit_code]
|
|
158
168
|
|
|
159
169
|
Model::LineItem.new(
|
|
160
170
|
id: text(node, LINE[:id]),
|
|
161
|
-
invoiced_quantity: text(node,
|
|
162
|
-
unit_code: node.at_xpath(
|
|
171
|
+
invoiced_quantity: text(node, quantity_element),
|
|
172
|
+
unit_code: node.at_xpath(unit_code_element, @ns)&.text,
|
|
163
173
|
line_extension_amount: text(node, LINE[:line_extension_amount]),
|
|
164
174
|
note: text(node, LINE[:note]),
|
|
165
175
|
item: build_item(item_node),
|
|
@@ -188,7 +198,7 @@ module Zugpferd
|
|
|
188
198
|
end
|
|
189
199
|
|
|
190
200
|
def build_allowance_charge(node)
|
|
191
|
-
currency = node.at_xpath("cbc:Amount/@currencyID",
|
|
201
|
+
currency = node.at_xpath("cbc:Amount/@currencyID", @ns)&.text
|
|
192
202
|
Model::AllowanceCharge.new(
|
|
193
203
|
charge_indicator: text(node, ALLOWANCE_CHARGE_FIELDS[:charge_indicator]) == "true",
|
|
194
204
|
reason: text(node, ALLOWANCE_CHARGE_FIELDS[:reason]),
|
|
@@ -203,7 +213,7 @@ module Zugpferd
|
|
|
203
213
|
end
|
|
204
214
|
|
|
205
215
|
def text(node, xpath)
|
|
206
|
-
node.at_xpath(xpath,
|
|
216
|
+
node.at_xpath(xpath, @ns)&.text
|
|
207
217
|
end
|
|
208
218
|
|
|
209
219
|
def parse_date(str)
|
data/lib/zugpferd/ubl/writer.rb
CHANGED
|
@@ -3,23 +3,32 @@ require_relative "mapping"
|
|
|
3
3
|
|
|
4
4
|
module Zugpferd
|
|
5
5
|
module UBL
|
|
6
|
-
# Writes
|
|
6
|
+
# Writes a billing document to UBL 2.1 Invoice or Credit Note XML.
|
|
7
7
|
#
|
|
8
8
|
# @example
|
|
9
|
-
# xml = Zugpferd::UBL::Writer.new.write(
|
|
9
|
+
# xml = Zugpferd::UBL::Writer.new.write(document)
|
|
10
10
|
class Writer
|
|
11
11
|
include Mapping
|
|
12
12
|
|
|
13
|
-
# Serializes
|
|
13
|
+
# Serializes a billing document to UBL 2.1 XML.
|
|
14
14
|
#
|
|
15
|
-
# @param
|
|
15
|
+
# @param document [Model::BillingDocument] the document to serialize
|
|
16
16
|
# @return [String] UTF-8 encoded XML string
|
|
17
|
-
def write(
|
|
17
|
+
def write(document)
|
|
18
|
+
@credit_note = document.type_code == "381"
|
|
18
19
|
builder = Nokogiri::XML::Builder.new(encoding: "UTF-8") do |xml|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
if @credit_note
|
|
21
|
+
xml.CreditNote(xmlns: CN_NS["cn"],
|
|
22
|
+
"xmlns:cac" => CN_NS["cac"],
|
|
23
|
+
"xmlns:cbc" => CN_NS["cbc"]) do
|
|
24
|
+
build_document(xml, document)
|
|
25
|
+
end
|
|
26
|
+
else
|
|
27
|
+
xml.Invoice(xmlns: NS["ubl"],
|
|
28
|
+
"xmlns:cac" => NS["cac"],
|
|
29
|
+
"xmlns:cbc" => NS["cbc"]) do
|
|
30
|
+
build_document(xml, document)
|
|
31
|
+
end
|
|
23
32
|
end
|
|
24
33
|
end
|
|
25
34
|
builder.to_xml
|
|
@@ -27,25 +36,29 @@ module Zugpferd
|
|
|
27
36
|
|
|
28
37
|
private
|
|
29
38
|
|
|
30
|
-
def
|
|
31
|
-
xml["cbc"].CustomizationID
|
|
32
|
-
xml["cbc"].ProfileID
|
|
33
|
-
xml["cbc"].ID
|
|
34
|
-
xml["cbc"].IssueDate
|
|
35
|
-
xml["cbc"].DueDate
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
def build_document(xml, doc)
|
|
40
|
+
xml["cbc"].CustomizationID doc.customization_id if doc.customization_id
|
|
41
|
+
xml["cbc"].ProfileID doc.profile_id if doc.profile_id
|
|
42
|
+
xml["cbc"].ID doc.number
|
|
43
|
+
xml["cbc"].IssueDate doc.issue_date.to_s
|
|
44
|
+
xml["cbc"].DueDate doc.due_date.to_s if doc.due_date
|
|
45
|
+
if @credit_note
|
|
46
|
+
xml["cbc"].CreditNoteTypeCode doc.type_code
|
|
47
|
+
else
|
|
48
|
+
xml["cbc"].InvoiceTypeCode doc.type_code
|
|
49
|
+
end
|
|
50
|
+
xml["cbc"].Note doc.note if doc.note
|
|
51
|
+
xml["cbc"].DocumentCurrencyCode doc.currency_code
|
|
52
|
+
xml["cbc"].BuyerReference doc.buyer_reference if doc.buyer_reference
|
|
40
53
|
|
|
41
|
-
build_supplier(xml,
|
|
42
|
-
build_customer(xml,
|
|
43
|
-
build_payment_means(xml,
|
|
44
|
-
build_payment_terms(xml,
|
|
45
|
-
|
|
46
|
-
build_tax_total(xml,
|
|
47
|
-
build_monetary_total(xml,
|
|
48
|
-
|
|
54
|
+
build_supplier(xml, doc.seller, doc.payment_instructions) if doc.seller
|
|
55
|
+
build_customer(xml, doc.buyer) if doc.buyer
|
|
56
|
+
build_payment_means(xml, doc.payment_instructions) if doc.payment_instructions
|
|
57
|
+
build_payment_terms(xml, doc.payment_instructions) if doc.payment_instructions&.note
|
|
58
|
+
doc.allowance_charges.each { |ac| build_allowance_charge(xml, ac, doc.currency_code) }
|
|
59
|
+
build_tax_total(xml, doc.tax_breakdown) if doc.tax_breakdown
|
|
60
|
+
build_monetary_total(xml, doc.monetary_totals, doc.currency_code) if doc.monetary_totals
|
|
61
|
+
doc.line_items.each { |li| build_line(xml, li, doc.currency_code) }
|
|
49
62
|
end
|
|
50
63
|
|
|
51
64
|
def build_supplier(xml, party, payment_instructions = nil)
|
|
@@ -235,12 +248,15 @@ module Zugpferd
|
|
|
235
248
|
end
|
|
236
249
|
end
|
|
237
250
|
|
|
238
|
-
def
|
|
239
|
-
|
|
251
|
+
def build_line(xml, line, currency_code)
|
|
252
|
+
line_method = @credit_note ? :CreditNoteLine : :InvoiceLine
|
|
253
|
+
quantity_method = @credit_note ? :CreditedQuantity : :InvoicedQuantity
|
|
254
|
+
|
|
255
|
+
xml["cac"].send(line_method) do
|
|
240
256
|
xml["cbc"].ID line.id
|
|
241
257
|
xml["cbc"].Note line.note if line.note
|
|
242
|
-
xml["cbc"].
|
|
243
|
-
|
|
258
|
+
xml["cbc"].send(quantity_method, format_decimal(line.invoiced_quantity),
|
|
259
|
+
unitCode: line.unit_code)
|
|
244
260
|
xml["cbc"].LineExtensionAmount(format_decimal(line.line_extension_amount),
|
|
245
261
|
currencyID: currency_code)
|
|
246
262
|
build_item(xml, line.item) if line.item
|
data/lib/zugpferd.rb
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
+
require_relative "zugpferd/model/billing_document"
|
|
1
2
|
require_relative "zugpferd/model/invoice"
|
|
3
|
+
require_relative "zugpferd/model/credit_note"
|
|
4
|
+
require_relative "zugpferd/model/corrected_invoice"
|
|
5
|
+
require_relative "zugpferd/model/self_billed_invoice"
|
|
6
|
+
require_relative "zugpferd/model/partial_invoice"
|
|
7
|
+
require_relative "zugpferd/model/prepayment_invoice"
|
|
2
8
|
require_relative "zugpferd/model/trade_party"
|
|
3
9
|
require_relative "zugpferd/model/postal_address"
|
|
4
10
|
require_relative "zugpferd/model/contact"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: zugpferd
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alexander Zeitler
|
|
@@ -77,14 +77,20 @@ files:
|
|
|
77
77
|
- lib/zugpferd/cii/reader.rb
|
|
78
78
|
- lib/zugpferd/cii/writer.rb
|
|
79
79
|
- lib/zugpferd/model/allowance_charge.rb
|
|
80
|
+
- lib/zugpferd/model/billing_document.rb
|
|
80
81
|
- lib/zugpferd/model/contact.rb
|
|
82
|
+
- lib/zugpferd/model/corrected_invoice.rb
|
|
83
|
+
- lib/zugpferd/model/credit_note.rb
|
|
81
84
|
- lib/zugpferd/model/invoice.rb
|
|
82
85
|
- lib/zugpferd/model/item.rb
|
|
83
86
|
- lib/zugpferd/model/line_item.rb
|
|
84
87
|
- lib/zugpferd/model/monetary_totals.rb
|
|
88
|
+
- lib/zugpferd/model/partial_invoice.rb
|
|
85
89
|
- lib/zugpferd/model/payment_instructions.rb
|
|
86
90
|
- lib/zugpferd/model/postal_address.rb
|
|
91
|
+
- lib/zugpferd/model/prepayment_invoice.rb
|
|
87
92
|
- lib/zugpferd/model/price.rb
|
|
93
|
+
- lib/zugpferd/model/self_billed_invoice.rb
|
|
88
94
|
- lib/zugpferd/model/tax_breakdown.rb
|
|
89
95
|
- lib/zugpferd/model/tax_subtotal.rb
|
|
90
96
|
- lib/zugpferd/model/trade_party.rb
|