Class: Secretariat::Invoice

Inherits:
Struct
  • Object
show all
Includes:
Versioner
Defined in:
lib/secretariat/invoice.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Versioner

#by_version

Instance Attribute Details

#attachmentsObject

Returns the value of attribute attachments

Returns:

  • (Object)

    the current value of attachments



20
21
22
# File 'lib/secretariat/invoice.rb', line 20

def attachments
  @attachments
end

#basis_amountObject

Returns the value of attribute basis_amount

Returns:

  • (Object)

    the current value of basis_amount



20
21
22
# File 'lib/secretariat/invoice.rb', line 20

def basis_amount
  @basis_amount
end

#buyerObject

Returns the value of attribute buyer

Returns:

  • (Object)

    the current value of buyer



20
21
22
# File 'lib/secretariat/invoice.rb', line 20

def buyer
  @buyer
end

#buyer_referenceObject

Returns the value of attribute buyer_reference

Returns:

  • (Object)

    the current value of buyer_reference



20
21
22
# File 'lib/secretariat/invoice.rb', line 20

def buyer_reference
  @buyer_reference
end

#currency_codeObject

Returns the value of attribute currency_code

Returns:

  • (Object)

    the current value of currency_code



20
21
22
# File 'lib/secretariat/invoice.rb', line 20

def currency_code
  @currency_code
end

#due_amountObject

Returns the value of attribute due_amount

Returns:

  • (Object)

    the current value of due_amount



20
21
22
# File 'lib/secretariat/invoice.rb', line 20

def due_amount
  @due_amount
end

#grand_total_amountObject

Returns the value of attribute grand_total_amount

Returns:

  • (Object)

    the current value of grand_total_amount



20
21
22
# File 'lib/secretariat/invoice.rb', line 20

def grand_total_amount
  @grand_total_amount
end

#idObject

Returns the value of attribute id

Returns:

  • (Object)

    the current value of id



20
21
22
# File 'lib/secretariat/invoice.rb', line 20

def id
  @id
end

#issue_dateObject

Returns the value of attribute issue_date

Returns:

  • (Object)

    the current value of issue_date



20
21
22
# File 'lib/secretariat/invoice.rb', line 20

def issue_date
  @issue_date
end

#line_itemsObject

Returns the value of attribute line_items

Returns:

  • (Object)

    the current value of line_items



20
21
22
# File 'lib/secretariat/invoice.rb', line 20

def line_items
  @line_items
end

Returns the value of attribute paid_amount

Returns:

  • (Object)

    the current value of paid_amount



20
21
22
# File 'lib/secretariat/invoice.rb', line 20

def paid_amount
  @paid_amount
end

#payment_due_dateObject

Returns the value of attribute payment_due_date

Returns:

  • (Object)

    the current value of payment_due_date



20
21
22
# File 'lib/secretariat/invoice.rb', line 20

def payment_due_date
  @payment_due_date
end

#payment_ibanObject

Returns the value of attribute payment_iban

Returns:

  • (Object)

    the current value of payment_iban



20
21
22
# File 'lib/secretariat/invoice.rb', line 20

def payment_iban
  @payment_iban
end

#payment_referenceObject

Returns the value of attribute payment_reference

Returns:

  • (Object)

    the current value of payment_reference



20
21
22
# File 'lib/secretariat/invoice.rb', line 20

def payment_reference
  @payment_reference
end

#payment_terms_textObject

Returns the value of attribute payment_terms_text

Returns:

  • (Object)

    the current value of payment_terms_text



20
21
22
# File 'lib/secretariat/invoice.rb', line 20

def payment_terms_text
  @payment_terms_text
end

#payment_textObject

Returns the value of attribute payment_text

Returns:

  • (Object)

    the current value of payment_text



20
21
22
# File 'lib/secretariat/invoice.rb', line 20

def payment_text
  @payment_text
end

#payment_typeObject

Returns the value of attribute payment_type

Returns:

  • (Object)

    the current value of payment_type



20
21
22
# File 'lib/secretariat/invoice.rb', line 20

def payment_type
  @payment_type
end

#sellerObject

Returns the value of attribute seller

Returns:

  • (Object)

    the current value of seller



20
21
22
# File 'lib/secretariat/invoice.rb', line 20

def seller
  @seller
end

#service_period_endObject

Returns the value of attribute service_period_end

Returns:

  • (Object)

    the current value of service_period_end



20
21
22
# File 'lib/secretariat/invoice.rb', line 20

def service_period_end
  @service_period_end
end

#service_period_startObject

Returns the value of attribute service_period_start

Returns:

  • (Object)

    the current value of service_period_start



20
21
22
# File 'lib/secretariat/invoice.rb', line 20

def service_period_start
  @service_period_start
end

#tax_amountObject

Returns the value of attribute tax_amount

Returns:

  • (Object)

    the current value of tax_amount



20
21
22
# File 'lib/secretariat/invoice.rb', line 20

def tax_amount
  @tax_amount
end

#tax_calculation_methodObject

Returns the value of attribute tax_calculation_method

Returns:

  • (Object)

    the current value of tax_calculation_method



20
21
22
# File 'lib/secretariat/invoice.rb', line 20

def tax_calculation_method
  @tax_calculation_method
end

#tax_categoryObject

Returns the value of attribute tax_category

Returns:

  • (Object)

    the current value of tax_category



20
21
22
# File 'lib/secretariat/invoice.rb', line 20

def tax_category
  @tax_category
end

#tax_percentObject

Returns the value of attribute tax_percent

Returns:

  • (Object)

    the current value of tax_percent



20
21
22
# File 'lib/secretariat/invoice.rb', line 20

def tax_percent
  @tax_percent
end

#tax_reasonObject

Returns the value of attribute tax_reason

Returns:

  • (Object)

    the current value of tax_reason



20
21
22
# File 'lib/secretariat/invoice.rb', line 20

def tax_reason
  @tax_reason
end

Instance Method Details

#errorsObject



51
52
53
# File 'lib/secretariat/invoice.rb', line 51

def errors
  @errors
end

#namespaces(version: 1) ⇒ Object



143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# File 'lib/secretariat/invoice.rb', line 143

def namespaces(version: 1)
  by_version(version,
    {
      'xmlns:ram' => 'urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:12',
      'xmlns:udt' => 'urn:un:unece:uncefact:data:standard:UnqualifiedDataType:15',
      'xmlns:rsm' => 'urn:ferd:CrossIndustryDocument:invoice:1p0',
      'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance'
    },
    {
      'xmlns:qdt' => 'urn:un:unece:uncefact:data:standard:QualifiedDataType:100',
      'xmlns:ram' => 'urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100',
      'xmlns:udt' => 'urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100',
      'xmlns:rsm' => 'urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100',
      'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance'
    }
  )
end

#payment_codeObject



98
99
100
# File 'lib/secretariat/invoice.rb', line 98

def payment_code
  PAYMENT_CODES[payment_type] || '1'
end

#tax_category_code(tax, version: 2) ⇒ Object



59
60
61
62
63
64
# File 'lib/secretariat/invoice.rb', line 59

def tax_category_code(tax, version: 2)
  if version == 1
    return TAX_CATEGORY_CODES_1[tax.tax_category || tax_category] || 'S'
  end
  TAX_CATEGORY_CODES[tax.tax_category || tax_category] || 'S'
end

#tax_reason_textObject



55
56
57
# File 'lib/secretariat/invoice.rb', line 55

def tax_reason_text
  tax_reason || TAX_EXEMPTION_REASONS[tax_category]
end

#taxesObject



66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/secretariat/invoice.rb', line 66

def taxes
  taxes = {}
  # Shortcut for cases where invoices only have one tax and the calculation is off by a cent because of rounding errors
  # (This can happen if the VAT and the net amount is calculated backwards from a round gross amount)
  if tax_calculation_method == :NONE
    tax = Tax.new(tax_percent: BigDecimal(tax_percent || BigDecimal(0)), tax_category: tax_category)
    tax.base_amount = BigDecimal(basis_amount)
    tax.tax_amount = BigDecimal(tax_amount || 0)
    return [tax]
  end

  line_items.each do |line_item|
    if line_item.tax_percent.nil?
      taxes['0'] = Tax.new(tax_percent: BigDecimal(0), tax_category: line_item.tax_category, tax_amount: BigDecimal(0)) if taxes['0'].nil?
      taxes['0'].base_amount += BigDecimal(line_item.net_amount) * line_item.quantity
    else
      taxes[line_item.tax_percent] = Tax.new(tax_percent: BigDecimal(line_item.tax_percent), tax_category: line_item.tax_category) if taxes[line_item.tax_percent].nil?
      taxes[line_item.tax_percent].tax_amount += BigDecimal(line_item.tax_amount)
      taxes[line_item.tax_percent].base_amount += BigDecimal(line_item.net_amount) * line_item.quantity
    end
  end
  
  if tax_calculation_method == :VERTICAL
    taxes.values.map do |tax|
      tax.tax_amount = (tax.base_amount * tax.tax_percent / 100).round(2)
      tax
    end
  else
    taxes.values
  end
end

#to_xml(version: 1, validate: true) ⇒ Object



161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
# File 'lib/secretariat/invoice.rb', line 161

def to_xml(version: 1, validate: true)
  if version < 1 || version > 2
    raise 'Unsupported Document Version'
  end

  if validate && !valid?
    raise ValidationError.new("Invoice is invalid", errors)
  end

  builder = Nokogiri::XML::Builder.new do |xml|

    root = by_version(version, 'CrossIndustryDocument', 'CrossIndustryInvoice')

    xml['rsm'].send(root, namespaces(version: version)) do

      context = by_version(version, 'SpecifiedExchangedDocumentContext', 'ExchangedDocumentContext')

      xml['rsm'].send(context) do
        xml['ram'].GuidelineSpecifiedDocumentContextParameter do
          version_id = by_version(version, 'urn:ferd:CrossIndustryDocument:invoice:1p0:comfort', 'urn:cen.eu:en16931:2017')
          xml['ram'].ID version_id
        end
      end

      header = by_version(version, 'HeaderExchangedDocument', 'ExchangedDocument')

      xml['rsm'].send(header) do
        xml['ram'].ID id
        if version == 1
          xml['ram'].Name "RECHNUNG"
        end
        xml['ram'].TypeCode '380' # TODO: make configurable
        xml['ram'].IssueDateTime do
          xml['udt'].DateTimeString(format: '102') do
            xml.text(issue_date.strftime("%Y%m%d"))
          end
        end

      end
      transaction = by_version(version, 'SpecifiedSupplyChainTradeTransaction', 'SupplyChainTradeTransaction')
      xml['rsm'].send(transaction) do

        if version == 2
          line_items.each_with_index do |item, i|
            item.to_xml(xml, i + 1, version: version, validate: validate) # one indexed
          end
        end

        trade_agreement = by_version(version, 'ApplicableSupplyChainTradeAgreement', 'ApplicableHeaderTradeAgreement')

        xml['ram'].send(trade_agreement) do
          if buyer_reference
            xml['ram'].BuyerReference buyer_reference
          end
          xml['ram'].SellerTradeParty do
            seller.to_xml(xml, version: version)
          end
          xml['ram'].BuyerTradeParty do
            buyer.to_xml(xml, version: version)
          end
          if version == 2
            if Array(attachments).size > 0
              attachments.each_with_index do |attachment, index|
                attachment.to_xml(xml, index, version: version, validate: validate)
              end
            end
          end
        end

        delivery = by_version(version, 'ApplicableSupplyChainTradeDelivery', 'ApplicableHeaderTradeDelivery')

        xml['ram'].send(delivery) do
          if version == 2
            xml['ram'].ShipToTradeParty do
              buyer.to_xml(xml, exclude_tax: true, version: version)
            end
          end
          xml['ram'].ActualDeliverySupplyChainEvent do
            xml['ram'].OccurrenceDateTime do
              xml['udt'].DateTimeString(format: '102') do
                xml.text(issue_date.strftime("%Y%m%d"))
              end
            end
          end
        end
        trade_settlement = by_version(version, 'ApplicableSupplyChainTradeSettlement', 'ApplicableHeaderTradeSettlement')
        xml['ram'].send(trade_settlement) do
          if payment_reference && payment_reference != ''
            xml['ram'].PaymentReference payment_reference
          end
          xml['ram'].InvoiceCurrencyCode currency_code
          xml['ram'].SpecifiedTradeSettlementPaymentMeans do
            xml['ram'].TypeCode payment_code
            xml['ram'].Information payment_text
            if payment_iban
              xml['ram'].PayeePartyCreditorFinancialAccount do
                xml['ram'].IBANID payment_iban
              end
            end
          end
          taxes.each do |tax|
            xml['ram'].ApplicableTradeTax do
              Helpers.currency_element(xml, 'ram', 'CalculatedAmount', tax.tax_amount, currency_code, add_currency: version == 1)
              xml['ram'].TypeCode 'VAT'
              if tax_reason_text && tax_reason_text != ''
                xml['ram'].ExemptionReason tax_reason_text
              end
              Helpers.currency_element(xml, 'ram', 'BasisAmount', tax.base_amount, currency_code, add_currency: version == 1)
              xml['ram'].CategoryCode tax_category_code(tax, version: version)
              # unless tax.untaxable?
                percent = by_version(version, 'ApplicablePercent', 'RateApplicablePercent')
                xml['ram'].send(percent, Helpers.format(tax.tax_percent))
              # end
            end
          end
          if version == 2 && service_period_start && service_period_end
            xml['ram'].BillingSpecifiedPeriod do
              xml['ram'].StartDateTime do
                Helpers.date_element(xml, service_period_start)
              end
              xml['ram'].EndDateTime do
                Helpers.date_element(xml, service_period_end)
              end
            end
          end
          xml['ram'].SpecifiedTradePaymentTerms do
            xml['ram'].Description payment_terms_text || "Paid"
            if payment_due_date
              xml['ram'].DueDateDateTime do
                Helpers.date_element(xml, payment_due_date)
              end
            end
          end

          monetary_summation = by_version(version, 'SpecifiedTradeSettlementMonetarySummation', 'SpecifiedTradeSettlementHeaderMonetarySummation')

          xml['ram'].send(monetary_summation) do
            Helpers.currency_element(xml, 'ram', 'LineTotalAmount', basis_amount, currency_code, add_currency: version == 1)
            # TODO: Fix this!
            Helpers.currency_element(xml, 'ram', 'ChargeTotalAmount', BigDecimal(0), currency_code, add_currency: version == 1)
            Helpers.currency_element(xml, 'ram', 'AllowanceTotalAmount', BigDecimal(0), currency_code, add_currency: version == 1)
            Helpers.currency_element(xml, 'ram', 'TaxBasisTotalAmount', basis_amount, currency_code, add_currency: version == 1)
            Helpers.currency_element(xml, 'ram', 'TaxTotalAmount', tax_amount, currency_code, add_currency: true)
            Helpers.currency_element(xml, 'ram', 'GrandTotalAmount', grand_total_amount, currency_code, add_currency: version == 1)
            Helpers.currency_element(xml, 'ram', 'TotalPrepaidAmount', paid_amount, currency_code, add_currency: version == 1)
            Helpers.currency_element(xml, 'ram', 'DuePayableAmount', due_amount, currency_code, add_currency: version == 1)
          end
        end
        if version == 1
          line_items.each_with_index do |item, i|
            item.to_xml(xml, i + 1, version: version, validate: validate) # one indexed
          end
        end
      end
    end
  end
  builder.to_xml
end

#valid?Boolean

Returns:

  • (Boolean)


102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# File 'lib/secretariat/invoice.rb', line 102

def valid?
  @errors = []
  tax = BigDecimal(tax_amount)
  basis = BigDecimal(basis_amount)
  summed_tax_amount = taxes.sum(&:tax_amount)
  if tax != summed_tax_amount
    @errors << "Tax amount and summed tax amounts deviate: #{tax_amount} / #{summed_tax_amount}"
    return false
  end
  summed_tax_base_amount = taxes.sum(&:base_amount)
  if basis != summed_tax_base_amount
    @errors << "Base amount and summed tax base amount deviate: #{basis} / #{summed_tax_base_amount}"
    return false
  end
  if tax_calculation_method != :NONE
    taxes.each do |tax|
      calc_tax = tax.base_amount * BigDecimal(tax.tax_percent) / BigDecimal(100)
      calc_tax = calc_tax.round(2)
      if tax.tax_amount != calc_tax
        @errors << "Tax amount and calculated tax amount deviate for rate #{tax.tax_percent}: #{tax.tax_amount} / #{calc_tax}"
        return false
      end
    end
  end
  grand_total = BigDecimal(grand_total_amount)
  calc_grand_total = basis + tax
  if grand_total != calc_grand_total
    @errors << "Grand total amount and calculated grand total amount deviate: #{grand_total} / #{calc_grand_total}"
    return false
  end
  line_item_sum = line_items.inject(BigDecimal(0)) do |m, item|
    m + BigDecimal(item.quantity.negative? ? -item.charge_amount : item.charge_amount)
  end
  if line_item_sum != basis
    @errors << "Line items do not add up to basis amount #{line_item_sum} / #{basis}"
    return false
  end
  return true
end