Class: Secretariat::LineItem

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

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Versioner

#by_version

Constructor Details

#initialize(**kwargs) ⇒ LineItem



43
44
45
46
47
48
# File 'lib/secretariat/line_item.rb', line 43

def initialize(**kwargs)
  if kwargs.key?(:quantity) && !kwargs.key?(:billed_quantity)
    kwargs[:billed_quantity] = kwargs.delete(:quantity)
  end
  super(**kwargs)
end

Instance Attribute Details

#basis_quantityObject

Returns the value of attribute basis_quantity



21
22
23
# File 'lib/secretariat/line_item.rb', line 21

def basis_quantity
  @basis_quantity
end

#billed_quantityObject

Returns the value of attribute billed_quantity



21
22
23
# File 'lib/secretariat/line_item.rb', line 21

def billed_quantity
  @billed_quantity
end

#charge_amountObject

Returns the value of attribute charge_amount



21
22
23
# File 'lib/secretariat/line_item.rb', line 21

def charge_amount
  @charge_amount
end

#currency_codeObject

Returns the value of attribute currency_code



21
22
23
# File 'lib/secretariat/line_item.rb', line 21

def currency_code
  @currency_code
end

#discount_amountObject

Returns the value of attribute discount_amount



21
22
23
# File 'lib/secretariat/line_item.rb', line 21

def discount_amount
  @discount_amount
end

#discount_reasonObject

Returns the value of attribute discount_reason



21
22
23
# File 'lib/secretariat/line_item.rb', line 21

def discount_reason
  @discount_reason
end

#gross_amountObject

Returns the value of attribute gross_amount



21
22
23
# File 'lib/secretariat/line_item.rb', line 21

def gross_amount
  @gross_amount
end

#nameObject

Returns the value of attribute name



21
22
23
# File 'lib/secretariat/line_item.rb', line 21

def name
  @name
end

#net_amountObject

Returns the value of attribute net_amount



21
22
23
# File 'lib/secretariat/line_item.rb', line 21

def net_amount
  @net_amount
end

#origin_country_codeObject

Returns the value of attribute origin_country_code



21
22
23
# File 'lib/secretariat/line_item.rb', line 21

def origin_country_code
  @origin_country_code
end

#service_period_endObject

Returns the value of attribute service_period_end



21
22
23
# File 'lib/secretariat/line_item.rb', line 21

def service_period_end
  @service_period_end
end

#service_period_startObject

Returns the value of attribute service_period_start



21
22
23
# File 'lib/secretariat/line_item.rb', line 21

def service_period_start
  @service_period_start
end

#tax_amountObject

Returns the value of attribute tax_amount



21
22
23
# File 'lib/secretariat/line_item.rb', line 21

def tax_amount
  @tax_amount
end

#tax_categoryObject

Returns the value of attribute tax_category



21
22
23
# File 'lib/secretariat/line_item.rb', line 21

def tax_category
  @tax_category
end

#tax_percentObject

Returns the value of attribute tax_percent



21
22
23
# File 'lib/secretariat/line_item.rb', line 21

def tax_percent
  @tax_percent
end

#unitObject

Returns the value of attribute unit



21
22
23
# File 'lib/secretariat/line_item.rb', line 21

def unit
  @unit
end

Instance Method Details

#effective_basis_quantityObject

If not provided, BasisQuantity should be 1.0 (price per 1 unit)

Raises:

  • (ArgumentError)


109
110
111
112
113
# File 'lib/secretariat/line_item.rb', line 109

def effective_basis_quantity
  q = basis_quantity.nil? ? BigDecimal("1.0") : BigDecimal(basis_quantity.to_s)
  raise ArgumentError, "basis_quantity must be > 0" if q <= 0
  q
end

#errorsObject



67
68
69
# File 'lib/secretariat/line_item.rb', line 67

def errors
  @errors
end

#quantityObject



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

def quantity
  warn_once_quantity
  billed_quantity
end

#quantity=(val) ⇒ Object



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

def quantity=(val)
  warn_once_quantity
  self.billed_quantity = val
end

#tax_category_code(version: 2) ⇒ Object



115
116
117
118
119
120
# File 'lib/secretariat/line_item.rb', line 115

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

#to_xml(xml, line_item_index, version: 2, validate: true) ⇒ Object



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
151
152
153
154
155
156
157
158
159
160
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
# File 'lib/secretariat/line_item.rb', line 126

def to_xml(xml, line_item_index, version: 2, validate: true)
  net_price = net_amount && BigDecimal(net_amount)
  gross_price = gross_amount && BigDecimal(gross_amount)
  charge_price = charge_amount && BigDecimal(charge_amount)

  self.tax_percent ||= BigDecimal(0)

  if net_price&.zero?
    self.tax_percent = 0
  end
  
  if net_price&.negative?
    # Zugferd doesn't allow negative amounts at the item level.
    # Instead, a negative quantity is used.
    self.billed_quantity = -billed_quantity
    self.gross_amount = gross_price&.abs
    self.net_amount = net_price&.abs
    self.charge_amount = charge_price&.abs
  end

  if validate && !valid?
    raise ValidationError.new("LineItem #{line_item_index} is invalid", errors)
  end

  xml['ram'].IncludedSupplyChainTradeLineItem do
    xml['ram'].AssociatedDocumentLineDocument do
      xml['ram'].LineID line_item_index
    end
    if (version == 2)
      xml['ram'].SpecifiedTradeProduct do
        xml['ram'].Name name
        xml['ram'].OriginTradeCountry do
          xml['ram'].ID origin_country_code
        end
      end
    end
    agreement = by_version(version, 'SpecifiedSupplyChainTradeAgreement', 'SpecifiedLineTradeAgreement')

    xml['ram'].send(agreement) do
      xml['ram'].GrossPriceProductTradePrice do
        Helpers.currency_element(xml, 'ram', 'ChargeAmount', gross_amount, currency_code, add_currency: version == 1, digits: 4)
        if version == 2 && discount_amount
          xml['ram'].BasisQuantity(unitCode: unit_code) do
            xml.text(Helpers.format(effective_basis_quantity, digits: 4))
          end
          xml['ram'].AppliedTradeAllowanceCharge do
            xml['ram'].ChargeIndicator do
              xml['udt'].Indicator 'false'
            end
            Helpers.currency_element(xml, 'ram', 'ActualAmount', discount_amount, currency_code, add_currency: version == 1)
            xml['ram'].Reason discount_reason
          end
        end
        if version == 1 && discount_amount
          xml['ram'].AppliedTradeAllowanceCharge do
            xml['ram'].ChargeIndicator do
              xml['udt'].Indicator 'false'
            end
            Helpers.currency_element(xml, 'ram', 'ActualAmount', discount_amount, currency_code, add_currency: version == 1)
            xml['ram'].Reason discount_reason
          end
        end
      end
      xml['ram'].NetPriceProductTradePrice do
        Helpers.currency_element(xml, 'ram', 'ChargeAmount', net_amount, currency_code, add_currency: version == 1, digits: 4)
        if version == 2
          xml['ram'].BasisQuantity(unitCode: unit_code) do
            xml.text(Helpers.format(effective_basis_quantity, digits: 4))
          end
        end
      end
    end

    delivery = by_version(version, 'SpecifiedSupplyChainTradeDelivery', 'SpecifiedLineTradeDelivery')

    xml['ram'].send(delivery) do
      xml['ram'].BilledQuantity(unitCode: unit_code) do
        xml.text(Helpers.format(billed_quantity, digits: 4))
      end
    end

    settlement = by_version(version, 'SpecifiedSupplyChainTradeSettlement', 'SpecifiedLineTradeSettlement')

    xml['ram'].send(settlement) do
      xml['ram'].ApplicableTradeTax do
        xml['ram'].TypeCode 'VAT'
        xml['ram'].CategoryCode tax_category_code(version: version)
        unless untaxable?
          percent = by_version(version, 'ApplicablePercent', 'RateApplicablePercent')
          xml['ram'].send(percent,Helpers.format(tax_percent))            
        end
      end

      if version == 2 && self.service_period_start && self.service_period_end
        xml['ram'].BillingSpecifiedPeriod do
          xml['ram'].StartDateTime do
            xml['udt'].DateTimeString(format: '102') do
              xml.text(service_period_start.strftime("%Y%m%d"))
            end
          end
          xml['ram'].EndDateTime do
            xml['udt'].DateTimeString(format: '102') do
              xml.text(service_period_end.strftime("%Y%m%d"))
            end
          end
        end
      end

      monetary_summation = by_version(version, 'SpecifiedTradeSettlementMonetarySummation', 'SpecifiedTradeSettlementLineMonetarySummation')
      xml['ram'].send(monetary_summation) do
        Helpers.currency_element(xml, 'ram', 'LineTotalAmount', (billed_quantity.negative? ? -charge_amount  : charge_amount), currency_code, add_currency: version == 1)
      end
    end

    if version == 1
      xml['ram'].SpecifiedTradeProduct do
        xml['ram'].Name name
      end
    end
  end
end

#unit_codeObject



104
105
106
# File 'lib/secretariat/line_item.rb', line 104

def unit_code
  UNIT_CODES[unit] || 'C62'
end

#untaxable?Boolean



122
123
124
# File 'lib/secretariat/line_item.rb', line 122

def untaxable?
  tax_category == :UNTAXEDSERVICE
end

#valid?Boolean



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
102
# File 'lib/secretariat/line_item.rb', line 71

def valid?
  @errors = []
  net_price = BigDecimal(net_amount)
  gross_price = BigDecimal(gross_amount)
  charge_price = BigDecimal(charge_amount)
  tax = BigDecimal(tax_amount)
  unit_price = net_price * BigDecimal(billed_quantity.abs)

  if charge_price != unit_price
    @errors << "charge price and gross price times quantity deviate: #{charge_price} / #{unit_price}"
    return false
  end
  if discount_amount
    discount = BigDecimal(discount_amount)
    calculated_net_price = (gross_price - discount).round(2, :down)
    if calculated_net_price != net_price
      @errors = "Calculated net price and net price deviate: #{calculated_net_price} / #{net_price}"
      return false
    end
  end
  if tax_category != :UNTAXEDSERVICE
    self.tax_percent ||= BigDecimal(0)
    calculated_tax = charge_price * BigDecimal(tax_percent) / BigDecimal(100)
    calculated_tax = calculated_tax.round(2)
    calculated_tax = -calculated_tax if billed_quantity.negative?
    if calculated_tax != tax
      @errors << "Tax and calculated tax deviate: #{tax} / #{calculated_tax}"
      return false
    end
  end
  return true
end

#warn_once_quantityObject



60
61
62
63
64
65
# File 'lib/secretariat/line_item.rb', line 60

def warn_once_quantity
  unless @__warned_quantity
    Kernel.warn("[secretariat] LineItem#quantity is deprecated; use #billed_quantity")
    @__warned_quantity = true
  end
end