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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1bafe4631b8105289e0f6c6efba696b91e0502cfbf99173918dc0d57de096263
4
- data.tar.gz: 3afe5b0d992a4622f3eadfabfbc1b4495ff5deca092f15eb659b5a8b4b529f23
3
+ metadata.gz: 132674bb432d021ca01155c3289c34abd9a0f9444242e74b79871aaa155e5dac
4
+ data.tar.gz: 0e03c97fedb053a5a3fce52216c92a64bfbd5671a9bc354fb629c1eae644fc23
5
5
  SHA512:
6
- metadata.gz: 8c50bedc1c561b09d28fb69c808dcc4b0e80b655e9a2c69a138229f05752ff4898ac64fd6bf144a4eb3d0cfdcfa84742576a64565097a7ae9f4e666c686fb06e
7
- data.tar.gz: ad2b0b0bc923f4debd510f40a9e1b296212af4fcbff0df9af0df5c295b2608e01430c80993000257de81d8d247c30939143567d9a8efa80805f02028dd36c99e
6
+ metadata.gz: f9f3e87cd9e52a517724829498ed4063aba54b97c9ea78c5e305e99d6ae9a6cd5a7f393836e2c73aa4a1369dbd35b32ff5a7ad053723b616282f207f62447d30
7
+ data.tar.gz: bfa2cd8c8ef42b94f89b21c8482418284682217be802312a659bc4672413cb1a8032e40b163ad874caae5a6da63410f201ee5467ec66a29bafc2011b101ea24d
@@ -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 {Model::Invoice}.
8
+ # Reads UN/CEFACT CII CrossIndustryInvoice XML into the appropriate model class.
9
9
  #
10
10
  # @example
11
- # invoice = Zugpferd::CII::Reader.new.read(File.read("invoice.xml"))
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::Invoice]
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
- Model::Invoice.new(
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),
@@ -3,18 +3,18 @@ require_relative "mapping"
3
3
 
4
4
  module Zugpferd
5
5
  module CII
6
- # Writes {Model::Invoice} to UN/CEFACT CII CrossIndustryInvoice XML.
6
+ # Writes a billing document to UN/CEFACT CII CrossIndustryInvoice XML.
7
7
  #
8
8
  # @example
9
- # xml = Zugpferd::CII::Writer.new.write(invoice)
9
+ # xml = Zugpferd::CII::Writer.new.write(document)
10
10
  class Writer
11
11
  include Mapping
12
12
 
13
- # Serializes an invoice to CII D16B XML.
13
+ # Serializes a billing document to CII D16B XML.
14
14
  #
15
- # @param invoice [Model::Invoice] the invoice to serialize
15
+ # @param document [Model::BillingDocument] the document to serialize
16
16
  # @return [String] UTF-8 encoded XML string
17
- def write(invoice)
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, invoice)
26
- build_exchanged_document(xml, invoice)
27
- build_transaction(xml, invoice)
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, inv)
35
+ def build_document_context(xml, doc)
36
36
  xml["rsm"].ExchangedDocumentContext do
37
- if inv.profile_id
37
+ if doc.profile_id
38
38
  xml["ram"].BusinessProcessSpecifiedDocumentContextParameter do
39
- xml["ram"].ID inv.profile_id
39
+ xml["ram"].ID doc.profile_id
40
40
  end
41
41
  end
42
- if inv.customization_id
42
+ if doc.customization_id
43
43
  xml["ram"].GuidelineSpecifiedDocumentContextParameter do
44
- xml["ram"].ID inv.customization_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, inv)
50
+ def build_exchanged_document(xml, doc)
51
51
  xml["rsm"].ExchangedDocument do
52
- xml["ram"].ID inv.number
53
- xml["ram"].TypeCode inv.type_code
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(inv.issue_date), format: "102")
55
+ xml["udt"].DateTimeString(format_cii_date(doc.issue_date), format: "102")
56
56
  end
57
- if inv.note
57
+ if doc.note
58
58
  xml["ram"].IncludedNote do
59
- xml["ram"].Content inv.note
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, inv)
65
+ def build_transaction(xml, doc)
66
66
  xml["rsm"].SupplyChainTradeTransaction do
67
- inv.line_items.each { |li| build_line_item(xml, li) }
68
- build_agreement(xml, inv)
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, inv)
70
+ build_settlement(xml, doc)
71
71
  end
72
72
  end
73
73
 
74
- def build_agreement(xml, inv)
74
+ def build_agreement(xml, doc)
75
75
  xml["ram"].ApplicableHeaderTradeAgreement do
76
- xml["ram"].BuyerReference inv.buyer_reference if inv.buyer_reference
77
- build_party(xml, "SellerTradeParty", inv.seller) if inv.seller
78
- build_party(xml, "BuyerTradeParty", inv.buyer) if inv.buyer
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, inv)
144
+ def build_settlement(xml, doc)
145
145
  xml["ram"].ApplicableHeaderTradeSettlement do
146
- if inv.payment_instructions&.creditor_reference_id
147
- xml["ram"].CreditorReferenceID inv.payment_instructions.creditor_reference_id
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 inv.payment_instructions&.payment_id
151
- xml["ram"].PaymentReference inv.payment_instructions.payment_id
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 inv.currency_code
154
+ xml["ram"].InvoiceCurrencyCode doc.currency_code
155
155
 
156
- build_payment_means(xml, inv.payment_instructions) if inv.payment_instructions
156
+ build_payment_means(xml, doc.payment_instructions) if doc.payment_instructions
157
157
 
158
- if inv.tax_breakdown
159
- inv.tax_breakdown.subtotals.each do |sub|
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
- inv.allowance_charges.each { |ac| build_allowance_charge(xml, ac) }
164
+ doc.allowance_charges.each { |ac| build_allowance_charge(xml, ac) }
165
165
 
166
- if inv.payment_instructions&.note || inv.due_date || inv.payment_instructions&.mandate_reference
166
+ if doc.payment_instructions&.note || doc.due_date || doc.payment_instructions&.mandate_reference
167
167
  xml["ram"].SpecifiedTradePaymentTerms do
168
- xml["ram"].Description inv.payment_instructions.note if inv.payment_instructions&.note
169
- if inv.due_date
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(inv.due_date), format: "102")
171
+ xml["udt"].DateTimeString(format_cii_date(doc.due_date), format: "102")
172
172
  end
173
173
  end
174
- xml["ram"].DirectDebitMandateID inv.payment_instructions.mandate_reference if inv.payment_instructions&.mandate_reference
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, inv.monetary_totals, inv.tax_breakdown) if inv.monetary_totals
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
@@ -0,0 +1,13 @@
1
+ module Zugpferd
2
+ module Model
3
+ # Credit Note (BG-0), type code 381.
4
+ #
5
+ # @example
6
+ # cn = CreditNote.new(number: "CN-001", issue_date: Date.today)
7
+ class CreditNote
8
+ include BillingDocument
9
+
10
+ TYPE_CODE = "381"
11
+ end
12
+ end
13
+ end
@@ -1,51 +1,14 @@
1
- require "bigdecimal"
2
-
3
1
  module Zugpferd
4
2
  module Model
5
- # The root invoice document (BG-0).
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
- # @return [String] BT-1 Invoice number
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
- # @param number [String] BT-1 Invoice number
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
+ # Partial Invoice (BG-0), type code 326.
4
+ #
5
+ # @example
6
+ # inv = PartialInvoice.new(number: "P-001", issue_date: Date.today)
7
+ class PartialInvoice
8
+ include BillingDocument
9
+
10
+ TYPE_CODE = "326"
11
+ end
12
+ end
13
+ 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
@@ -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",
@@ -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 {Model::Invoice}.
8
+ # Reads UBL 2.1 Invoice or Credit Note XML into the appropriate model class.
9
9
  #
10
10
  # @example
11
- # invoice = Zugpferd::UBL::Reader.new.read(File.read("invoice.xml"))
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::Invoice]
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
- Model::Invoice.new(
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, INVOICE[:type_code]),
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, 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)),
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], NS)
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, NS)
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, NS)
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, NS)
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']", NS
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", NS)&.text
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, NS).map do |sub|
125
- sub_currency = sub.at_xpath("cbc:TaxableAmount/@currencyID", NS)&.text
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, NS)
157
- price_node = node.at_xpath(PRICE, NS)
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, LINE[:invoiced_quantity]),
162
- unit_code: node.at_xpath(LINE[:unit_code], NS)&.text,
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", NS)&.text
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, NS)&.text
216
+ node.at_xpath(xpath, @ns)&.text
207
217
  end
208
218
 
209
219
  def parse_date(str)
@@ -3,23 +3,32 @@ require_relative "mapping"
3
3
 
4
4
  module Zugpferd
5
5
  module UBL
6
- # Writes {Model::Invoice} to UBL 2.1 Invoice XML.
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(invoice)
9
+ # xml = Zugpferd::UBL::Writer.new.write(document)
10
10
  class Writer
11
11
  include Mapping
12
12
 
13
- # Serializes an invoice to UBL 2.1 XML.
13
+ # Serializes a billing document to UBL 2.1 XML.
14
14
  #
15
- # @param invoice [Model::Invoice] the invoice to serialize
15
+ # @param document [Model::BillingDocument] the document to serialize
16
16
  # @return [String] UTF-8 encoded XML string
17
- def write(invoice)
17
+ def write(document)
18
+ @credit_note = document.type_code == "381"
18
19
  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)
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 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
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, 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) }
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 build_invoice_line(xml, line, currency_code)
239
- xml["cac"].InvoiceLine do
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"].InvoicedQuantity(format_decimal(line.invoiced_quantity),
243
- unitCode: line.unit_code)
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.1.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