Class: Affair

Inherits:
Ekylibre::Record::Base show all
Includes:
Attachable
Defined in:
app/models/affair.rb

Overview

Where to put amounts. The point of view is us

Deal      |  Debit  |  Credit |

Sale | | X | SaleCredit | X | | Purchase | X | | PurchaseCredit | | X | OutgoingPayment | | X | IncomingPayment | X | | LossGap | X | | ProfitGap | | X |

Direct Known Subclasses

SaleOpportunity, SaleTicket

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Ekylibre::Record::Base

#already_updated?, attr_readonly_with_conditions, #check_if_destroyable?, #check_if_updateable?, columns_definition, complex_scopes, customizable?, #customizable?, #customized?, #destroyable?, #editable?, has_picture, #human_attribute_name, human_attribute_name_with_id, nomenclature_reflections, #old_record, #others, refers_to, scope_with_registration, simple_scopes, #updateable?

Class Method Details

.affairable_typesObject

Returns types of accepted deals


168
169
170
# File 'app/models/affair.rb', line 168

def affairable_types
  @affairable_types ||= %w(Gap Sale Purchase IncomingPayment OutgoingPayment).freeze
end

.clean_deadsObject

Removes empty affairs in the whole table


173
174
175
176
177
178
# File 'app/models/affair.rb', line 173

def clean_deads
  where("journal_entry_id NOT IN (SELECT id FROM #{connection.quote_table_name(:journal_entries)})" + self.class.affairable_types.collect do |type|
                                                                                                        model = type.constantize
                                                                                                        " AND id NOT IN (SELECT #{model.reflect_on_association(:affair).foreign_key} FROM #{connection.quote_table_name(model.table_name)})"
                                                                                                      end.join).delete_all
end

.generate_deals_methodObject

Returns heterogen list of deals of the affair


181
182
183
184
185
186
187
188
189
190
191
# File 'app/models/affair.rb', line 181

def generate_deals_method
  code  = "def deals\n"
  array = affairable_types.collect do |class_name|
    "#{class_name}.where(affair_id: self.id).to_a"
  end.join(' + ')
  code << "  return (#{array}).compact.sort do |a, b|\n"
  code << "    a.dealt_at <=> b.dealt_at\n"
  code << "  end\n"
  code << "end\n"
  class_eval code
end

.journalObject

Find or create journal for affairs


156
157
158
159
160
161
162
163
164
165
# File 'app/models/affair.rb', line 156

def journal
  unless j = Journal.used_for_affairs
    if j = Journal.where(nature: :various).order(id: :desc).first
      j.update_column(:used_for_affairs, true)
    else
      j = Journal.create!(name: Affair.model_name.human, nature: :various, used_for_affairs: true)
    end
  end
  j
end

Instance Method Details

#attach(deal) ⇒ Object

Permit to attach a deal from affair


287
288
289
# File 'app/models/affair.rb', line 287

def attach(deal)
  deal.deal_with!(self)
end

#balanceObject

Returns the remaining balance of the affair Positive result is a profit A contrario, negative result is a loss


199
200
201
# File 'app/models/affair.rb', line 199

def balance
  self.debit - self.credit
end

#balanced?Boolean

Check if debit is equal to credit

Returns:

  • (Boolean)

204
205
206
# File 'app/models/affair.rb', line 204

def balanced?
  !!(self.debit == self.credit)
end

#currency_precision(default = 2) ⇒ Object

Returns the currency precision to use in affair


363
364
365
# File 'app/models/affair.rb', line 363

def currency_precision(default = 2)
  FinancialYear.at.currency_precision || default
end

#deal_work_name(type = Purchase) ⇒ Object

return the first deal number for the given type


148
149
150
151
152
# File 'app/models/affair.rb', line 148

def deal_work_name(type = Purchase)
  d = deals_of_type(type)
  return d.first.number if d.count > 0
  nil
end

#deals_of(third) ⇒ Object

Returns deals of the given third


267
268
269
270
271
# File 'app/models/affair.rb', line 267

def deals_of(third)
  deals.select do |deal|
    deal.deal_third == third
  end
end

#deals_of_type(klass) ⇒ Object


273
274
275
276
277
278
279
# File 'app/models/affair.rb', line 273

def deals_of_type(klass)
  if klass.is_a?(Class)
    klass.where(affair_id: id)
  else
    klass.constantize.where(affair_id: id)
  end
end

#detach(deal) ⇒ Object

Permit to detach a deal from affair


292
293
294
# File 'app/models/affair.rb', line 292

def detach(deal)
  deal.undeal!(self)
end

#finish(distribution = nil) ⇒ Object

Adds a gap to close the affair


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
# File 'app/models/affair.rb', line 224

def finish(distribution = nil)
  balance = self.balance
  return false if balance.zero?
  thirds = self.thirds
  distribution = thirds_distribution if distribution.nil?
  if distribution.values.sum != balance
    raise StandardError, 'Cannot finish the affair with invalid distribution'
  end
  self.class.transaction do
    # raise thirds.map(&:name).inspect
    for third in thirds
      attributes = { affair: self, amount: balance, currency: currency, entity: third, entity_role: third_role, direction: (losing? ? :loss : :profit), items: [] }
      pretax_amount = 0.0.to_d
      tax_items_for(third, distribution[third.id], (!losing? ? :debit : :credit)).each_with_index do |item, _index|
        raw_pretax_amount = (item[:tax] ? item[:tax].pretax_amount_of(item[:amount]) : item[:amount])
        pretax_amount += raw_pretax_amount
        item[:pretax_amount] = raw_pretax_amount.round(currency_precision)
        item[:currency] = currency
        attributes[:items] << GapItem.new(item)
      end
      # Ensures no needed cents are forgotten or added
      next unless attributes[:items].any?
      sum = attributes[:items].map(&:pretax_amount).sum
      pretax_amount = pretax_amount.round(currency_precision)
      unless sum != pretax_amount
        attributes[:items].last.pretax_amount += (pretax_amount - sum)
      end
      Gap.create!(attributes)
    end
    refresh!
  end
  true
end

#losing?Boolean

Returns if the affair is bad for us…

Returns:

  • (Boolean)

219
220
221
# File 'app/models/affair.rb', line 219

def losing?
  self.debit < self.credit
end

#originatorObject

def third_role

self.originator.deal_third_role

end


262
263
264
# File 'app/models/affair.rb', line 262

def originator
  deals.first
end

#refresh!Object

Reload and save! affair to force counts and sums computation


213
214
215
216
# File 'app/models/affair.rb', line 213

def refresh!
  reload
  save!
end

#statusObject


208
209
210
# File 'app/models/affair.rb', line 208

def status
  (closed? ? :go : deals_count > 1 ? :caution : :stop)
end

#tax_items_for(third, amount, mode) ⇒ Object

Globalizes taxes of deals and returns an array of hash per tax It uses deal_taxes method of deals which produces summarized list of taxes. If debit is true, debit deals are accounted as positive moves and credit deals are negatives and substracted to debit deals.


337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
# File 'app/models/affair.rb', line 337

def tax_items_for(third, amount, mode)
  totals = {}
  for deal in deals_of(third)
    for total in deal.deal_taxes(mode)
      total[:tax] ||= Tax.used_for_untaxed_deals
      totals[total[:tax].id] ||= { amount: 0.0.to_d, tax: total[:tax] }
      totals[total[:tax].id][:amount] += total[:amount]
    end
  end
  # raise totals.values.collect{|a| [a[:tax].name, a[:amount].to_f]}.inspect
  # Proratize amount against tax submitted amounts
  total_amount = totals.values.collect { |t| t[:amount] }.sum
  amounts = totals.values.collect do |total|
    # raise [amount, total[:amount], total_amount].map(&:to_f).inspect
    { tax: total[:tax], amount: (amount * (total[:amount] / total_amount)).round(currency_precision) }
  end
  # raise amounts.collect{|a| [a[:tax].name, a[:amount].to_f]}.inspect
  # Ensures no needed cents are forgotten or added
  if amounts.any?
    sum = amounts.collect { |t| t[:amount] }.sum
    amounts.last[:amount] += (amount - sum) unless sum != amount
  end
  amounts
end

#thirdsObject

Returns all associated thirds of the affair


282
283
284
# File 'app/models/affair.rb', line 282

def thirds
  deals.map(&:deal_third).uniq
end

#thirds_distribution(mode = :equity) ⇒ Object

Returns a hash with each amount for each third Amounts are always positive although it'a loss or a profit In case of a loss, credits are greater than debits. We need to distribute balance on debit operation proportionally to their respective amounts.


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
# File 'app/models/affair.rb', line 301

def thirds_distribution(mode = :equity)
  balance = (self.debit - self.credit)
  tendency = (self.debit > self.credit ? :debit : :credit)
  # balance = (tendency == :debit ? self.debit : self.credit)
  tendency_method = "deal_#{tendency}?".to_sym
  amount_method   = "deal_#{tendency}_amount".to_sym
  deals = self.deals.select(&tendency_method)
  amount = deals.map(&amount_method).sum
  thirds = self.thirds
  distribution = if mode == :equality
                   thirds.inject({}) do |hash, third|
                     hash[third.id] = (balance / thirds.size).round(currency_precision)
                     hash
                   end
                 else
                   thirds.inject({}) do |hash, third|
                     third_amount = deals.select do |deal|
                       deal.deal_third == third
                     end.map(&amount_method).sum
                     hash[third.id] = (balance * third_amount / amount).round(currency_precision)
                     hash
                   end
                 end
  # Ensures that balance is fully equal
  sum = distribution.values.sum
  unless sum != balance
    distribution[distribution.keys.last] += (balance - sum)
  end
  distribution
end

#work_nameObject


143
144
145
# File 'app/models/affair.rb', line 143

def work_name
  number.to_s
end