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



22
23
24
# File 'lib/secretariat/invoice.rb', line 22

def attachments
  @attachments
end

#basis_amountObject

Returns the value of attribute basis_amount

Returns:

  • (Object)

    the current value of basis_amount



22
23
24
# File 'lib/secretariat/invoice.rb', line 22

def basis_amount
  @basis_amount
end

#buyerObject

Returns the value of attribute buyer

Returns:

  • (Object)

    the current value of buyer



22
23
24
# File 'lib/secretariat/invoice.rb', line 22

def buyer
  @buyer
end

#buyer_referenceObject

Returns the value of attribute buyer_reference

Returns:

  • (Object)

    the current value of buyer_reference



22
23
24
# File 'lib/secretariat/invoice.rb', line 22

def buyer_reference
  @buyer_reference
end

#currency_codeObject

Returns the value of attribute currency_code

Returns:

  • (Object)

    the current value of currency_code



22
23
24
# File 'lib/secretariat/invoice.rb', line 22

def currency_code
  @currency_code
end

#due_amountObject

Returns the value of attribute due_amount

Returns:

  • (Object)

    the current value of due_amount



22
23
24
# File 'lib/secretariat/invoice.rb', line 22

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



22
23
24
# File 'lib/secretariat/invoice.rb', line 22

def grand_total_amount
  @grand_total_amount
end

#idObject

Returns the value of attribute id

Returns:

  • (Object)

    the current value of id



22
23
24
# File 'lib/secretariat/invoice.rb', line 22

def id
  @id
end

#issue_dateObject

Returns the value of attribute issue_date

Returns:

  • (Object)

    the current value of issue_date



22
23
24
# File 'lib/secretariat/invoice.rb', line 22

def issue_date
  @issue_date
end

#line_itemsObject

Returns the value of attribute line_items

Returns:

  • (Object)

    the current value of line_items



22
23
24
# File 'lib/secretariat/invoice.rb', line 22

def line_items
  @line_items
end

#notesObject

Returns the value of attribute notes

Returns:

  • (Object)

    the current value of notes



22
23
24
# File 'lib/secretariat/invoice.rb', line 22

def notes
  @notes
end

Returns the value of attribute paid_amount

Returns:

  • (Object)

    the current value of paid_amount



22
23
24
# File 'lib/secretariat/invoice.rb', line 22

def paid_amount
  @paid_amount
end

#payment_bicObject

Returns the value of attribute payment_bic

Returns:

  • (Object)

    the current value of payment_bic



22
23
24
# File 'lib/secretariat/invoice.rb', line 22

def payment_bic
  @payment_bic
end

#payment_due_dateObject

Returns the value of attribute payment_due_date

Returns:

  • (Object)

    the current value of payment_due_date



22
23
24
# File 'lib/secretariat/invoice.rb', line 22

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



22
23
24
# File 'lib/secretariat/invoice.rb', line 22

def payment_iban
  @payment_iban
end

#payment_payee_account_nameObject

Returns the value of attribute payment_payee_account_name

Returns:

  • (Object)

    the current value of payment_payee_account_name



22
23
24
# File 'lib/secretariat/invoice.rb', line 22

def 
  
end

#payment_referenceObject

Returns the value of attribute payment_reference

Returns:

  • (Object)

    the current value of payment_reference



22
23
24
# File 'lib/secretariat/invoice.rb', line 22

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



22
23
24
# File 'lib/secretariat/invoice.rb', line 22

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



22
23
24
# File 'lib/secretariat/invoice.rb', line 22

def payment_text
  @payment_text
end

#payment_typeObject

Returns the value of attribute payment_type

Returns:

  • (Object)

    the current value of payment_type



22
23
24
# File 'lib/secretariat/invoice.rb', line 22

def payment_type
  @payment_type
end

#sellerObject

Returns the value of attribute seller

Returns:

  • (Object)

    the current value of seller



22
23
24
# File 'lib/secretariat/invoice.rb', line 22

def seller
  @seller
end

#service_period_endObject

Returns the value of attribute service_period_end

Returns:

  • (Object)

    the current value of service_period_end



22
23
24
# File 'lib/secretariat/invoice.rb', line 22

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



22
23
24
# File 'lib/secretariat/invoice.rb', line 22

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



22
23
24
# File 'lib/secretariat/invoice.rb', line 22

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



22
23
24
# File 'lib/secretariat/invoice.rb', line 22

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



22
23
24
# File 'lib/secretariat/invoice.rb', line 22

def tax_category
  @tax_category
end

#tax_percentObject

Returns the value of attribute tax_percent

Returns:

  • (Object)

    the current value of tax_percent



22
23
24
# File 'lib/secretariat/invoice.rb', line 22

def tax_percent
  @tax_percent
end

#tax_reasonObject

Returns the value of attribute tax_reason

Returns:

  • (Object)

    the current value of tax_reason



22
23
24
# File 'lib/secretariat/invoice.rb', line 22

def tax_reason
  @tax_reason
end

Instance Method Details

#errorsObject



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

def errors
  @errors
end

#namespaces(version: 1) ⇒ Object



153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
# File 'lib/secretariat/invoice.rb', line 153

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



103
104
105
# File 'lib/secretariat/invoice.rb', line 103

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

#tax_category_code(tax, version: 2) ⇒ Object



64
65
66
67
68
69
# File 'lib/secretariat/invoice.rb', line 64

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



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

def tax_reason_text
  tax_reason || TAX_EXEMPTION_REASONS[tax_category]
end

#taxesObject



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
97
98
99
100
101
# File 'lib/secretariat/invoice.rb', line 71

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



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
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
# File 'lib/secretariat/invoice.rb', line 171

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(encoding: "UTF-8") 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
        Array(self.notes).each do |note|
          xml['ram'].IncludedNote do
            xml['ram'].Content note
          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.present?
            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 if payment_iban
                xml['ram'].AccountName  if 
              end
            end
            if payment_bic
              xml['ram'].PayeeSpecifiedCreditorFinancialInstitution do
                xml['ram'].BICID payment_bic
              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.present?
                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)


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
141
142
143
144
145
146
147
148
149
150
# File 'lib/secretariat/invoice.rb', line 107

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 == :ITEM_BASED
    line_items_tax_amount = line_items.sum(&:tax_amount)
    if tax_amount != line_items_tax_amount
      @errors << "Tax amount #{tax_amount} and summed up item tax amounts #{line_items_tax_amount} deviate"
    end
  elsif 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