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 third's one.

Deal      |  Debit  |  Credit |

Sale | X | | SaleCredit | | X | SaleGap | Profit! | Loss! | Payslip | | X | Purchase | | X | PurchaseCredit | | | PurchaseGap | Profit! | Loss! | OutgoingPayment | X | | IncomingPayment | | X | Regularization | ? | ? |

Direct Known Subclasses

PayslipAffair, PurchaseAffair, SaleAffair

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Ekylibre::Record::Base

#already_updated?, #check_if_destroyable?, #check_if_updateable?, columns_definition, #customizable?, customizable?, #customized?, #destroyable?, #editable?, has_picture, #human_attribute_name, nomenclature_reflections, #old_record, #others, refers_to, #unsuppress, #updateable?

Methods included from Userstamp::Stampable

included

Methods included from Userstamp::Stamper

included

Class Method Details

.affairable_typesObject

Returns types of accepted deals


153
154
155
# File 'app/models/affair.rb', line 153

def affairable_types
  @affairable_types ||= %w[SaleGap PurchaseGap Sale PurchaseInvoice Payslip IncomingPayment OutgoingPayment Regularization DebtTransfer].freeze
end

.clean_deadsObject

Removes empty affairs in the whole table


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

def clean_deads
  query = "journal_entry_id NOT IN (SELECT id FROM #{connection.quote_table_name(:journal_entries)})"
  query << 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
  where(query).delete_all
end

.deal_classObject


357
358
359
# File 'app/models/affair.rb', line 357

def self.deal_class
  name.gsub(/Affair$/, '').constantize
end

.generate_deals_methodObject

Returns heterogen list of deals of the affair


168
169
170
171
172
173
174
175
176
177
178
# File 'app/models/affair.rb', line 168

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


141
142
143
144
145
146
147
148
149
150
# File 'app/models/affair.rb', line 141

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

#absorb!(other) ⇒ Object


190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
# File 'app/models/affair.rb', line 190

def absorb!(other)
  unless other.is_a?(Affair)
    raise "#{other.class.name} (ID=#{other.id}) cannot be merged in Affair"
  end
  return self if self == other
  if other.currency != currency
    raise ArgumentError, "The currency (#{currency}) is different of the affair currency(#{other.currency})"
  end
  Ekylibre::Record::Base.transaction do
    other.deals.each do |deal|
      deal.update_columns(affair_id: id)
      deal.reload
    end
    refresh!
    other.destroy!
  end
  self
end

#attach(deal) ⇒ Object

Permit to attach a deal from affair


386
387
388
# File 'app/models/affair.rb', line 386

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


186
187
188
# File 'app/models/affair.rb', line 186

def balance
  self.credit - self.debit
end

#balanced?Boolean

Check if debit is equal to credit

Returns:

  • (Boolean)

232
233
234
# File 'app/models/affair.rb', line 232

def balanced?
  !unbalanced?
end

#currency_precision(default = 2) ⇒ Object

Returns the currency precision to use in affair


402
403
404
# File 'app/models/affair.rb', line 402

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

#deal_work_nameObject

Returns the first deal number for the given type as deal work name. FIXME: Not sure that's a good method. Why the first deal number is used as the

"deal work name".

133
134
135
136
137
# File 'app/models/affair.rb', line 133

def deal_work_name
  d = deals_of_type(self.class.deal_class).first
  return d.number if d
  nil
end

#deals_of(third) ⇒ Object

Returns deals of the given third


366
367
368
369
370
# File 'app/models/affair.rb', line 366

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

#deals_of_type(klass) ⇒ Object


372
373
374
375
376
377
378
# File 'app/models/affair.rb', line 372

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

#debt_transferable?Boolean

Returns:

  • (Boolean)

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

def debt_transferable?
  # unbalanced
  !closed && unbalanced?
end

#detach(deal) ⇒ Object

Permit to detach a deal from affair


391
392
393
# File 'app/models/affair.rb', line 391

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

#extract!(deal) ⇒ Object


209
210
211
212
213
214
215
216
217
218
219
220
# File 'app/models/affair.rb', line 209

def extract!(deal)
  unless deals.include?(deal)
    raise ArgumentError, 'Given deal is not one of the affair'
  end
  Ekylibre::Record::Base.transaction do
    affair = self.class.create!(currency: deal.currency, third: deal.deal_third)
    update_column(:affair_id, affair.id)
    affair.refresh!
    refresh!
    destroy! if deals_count.zero?
  end
end

#finishObject

Adds a gap to close the affair

Basically we calculate the gap between the debit and credit for each third then we create GapItems for each VAT % present in the biggest value between debit and credit. Each of those holds a value equal to (VATed amount / total) * gap so the amounts amounts taxed at each VAT %s in the gap are proportional to the VAT %s amounts in the debit/credit.


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
340
341
342
343
344
345
346
347
348
349
350
351
# File 'app/models/affair.rb', line 274

def finish
  return false if balance.zero?
  raise 'Cannot finish anymore multi-thirds affairs' if multi_thirds?
  precision = Nomen::Currency.find(currency).precision
  self.class.transaction do
    # Get all VAT-specified deals
    deals_amount = deals.map do |deal|
      %i[debit credit].map do |mode|
        # Get the items of the deal with their VAT %
        # then add 0% VAT to untaxed deals
        deal.deal_taxes(mode)
            .each { |am| am[:tax] ||= Tax.used_for_untaxed_deals }
      end
    end

    # Extract the debit ones from the credit ones / vice versa
    debit_deals = deals_amount.map(&:first).flatten
    credit_deals = deals_amount.map(&:last).flatten

    # Group the same-VAT-ed amounts.
    # Groups amounts by tax, sums the amounts, and converts back to hash
    grouped_debit = debit_deals
                    .group_by { |d| d[:tax] }
                    .map { |tax, pairs| [tax, pairs.map { |p| p[:amount] }.sum] }
                    .to_h

    # Groups amounts by tax, sums the amounts, and converts back to hash
    grouped_credit = credit_deals
                     .group_by { |c| c[:tax] }
                     .map { |tax, pairs| [tax, pairs.map { |p| p[:amount] }.sum] }
                     .to_h

    total_debit = grouped_debit.values.sum
    total_credit = grouped_credit.values.sum

    gap_amount = (total_debit - total_credit).abs

    # Select which will be used as a reference for VAT % ratios on gap
    bigger_total = [total_debit, total_credit].max

    # Gap is always on the lesser column.
    gap_is_credit = bigger_total == total_debit

    bigger_deal_set = gap_is_credit ? grouped_debit : grouped_credit

    # Construct a GapItem per VAT % in the debit/credit
    gap_items = bigger_deal_set.map do |tax, taxed_amount|
      # Calculate percentage of the column taxed at `tax`
      percentage_at_vat = taxed_amount / bigger_total
      # Apply that percentage to the gap to get a proportional amount
      taxed_amount_in_gap = percentage_at_vat * gap_amount
      # Get that amount +/- depending if we're crediting or debiting
      # taxed_amount_in_gap *= -1 unless gap_is_credit
      # Get the pre-tax value
      pretaxed_amount_in_gap = tax.pretax_amount_of(taxed_amount_in_gap)

      GapItem.new(
        currency: currency,
        tax: tax,
        amount: taxed_amount_in_gap.round(precision),
        pretax_amount: pretaxed_amount_in_gap.round(precision)
      )
    end

    # TODO: Check that rounds fit exactly wanted amount

    gap_class.create!(
      affair: self,
      amount: gap_amount,
      currency: currency,
      entity: third,
      direction: (!gap_is_credit ? :profit : :loss),
      items: gap_items
    )
    refresh!
  end
  true
end

#finishable?Boolean

Returns:

  • (Boolean)

257
258
259
# File 'app/models/affair.rb', line 257

def finishable?
  unbalanced? && gap_class && !multi_thirds?
end

#gap_classObject


353
354
355
# File 'app/models/affair.rb', line 353

def gap_class
  nil
end

#journal_entry_items_already_lettered?Boolean

Returns true if a part of items are already lettered by outside

Returns:

  • (Boolean)

436
437
438
439
440
441
442
443
# File 'app/models/affair.rb', line 436

def journal_entry_items_already_lettered?
  letters = letterable_journal_entry_items.pluck(:letter).map { |letter| letter.delete('*') }
  if (letter? && letters.detect { |x| x != letter }) ||
     (!letter? && letters.detect(&:present?))
    return true
  end
  false
end

#journal_entry_items_balanced?Boolean

Returns true if a part of items are already lettered by outside

Returns:

  • (Boolean)

446
447
448
# File 'app/models/affair.rb', line 446

def journal_entry_items_balanced?
  letterable_journal_entry_items.sum('debit - credit').zero?
end

#journal_entry_items_unbalanced?Boolean

Returns true if a part of items are already lettered by outside

Returns:

  • (Boolean)

451
452
453
# File 'app/models/affair.rb', line 451

def journal_entry_items_unbalanced?
  !journal_entry_items_balanced?
end

#letter_journal_entriesObject


422
423
424
# File 'app/models/affair.rb', line 422

def letter_journal_entries
  letter_journal_entries! if letterable?
end

#letter_journal_entries!Object

Adds a letter on journal


467
468
469
470
471
472
473
474
475
476
# File 'app/models/affair.rb', line 467

def letter_journal_entries!
  journal_entry_items = letterable_journal_entry_items
   = 

  # Update letters
  .unmark(letter) if journal_entry_items.any?
  self.letter = nil if letter.blank?
  self.letter = .mark!(journal_entry_items.pluck(:id), letter)
  true
end

#letterable?Boolean

Returns:

  • (Boolean)

410
411
412
# File 'app/models/affair.rb', line 410

def letterable?
  !unletterable?
end

#letterable_journal_entry_itemsObject


431
432
433
# File 'app/models/affair.rb', line 431

def letterable_journal_entry_items
  JournalEntryItem.where(account: , entry: deals.map(&:journal_entry))
end

#lettered?Boolean

Returns:

  • (Boolean)

418
419
420
# File 'app/models/affair.rb', line 418

def lettered?
  letter?
end

#losing?Boolean

Returns if the affair is bad for us…

Returns:

  • (Boolean)

253
254
255
# File 'app/models/affair.rb', line 253

def losing?
  self.debit > self.credit
end

#match_with_accountancy?Boolean

Returns true if debit/credit are the same for third in journal entry items

Returns:

  • (Boolean)

456
457
458
459
# File 'app/models/affair.rb', line 456

def match_with_accountancy?
  letterable_journal_entry_items.sum(:real_debit) == credit &&
    letterable_journal_entry_items.sum(:real_credit) == debit
end

#multi_thirds?Boolean

Returns true if many thirds are involved in this affair

Returns:

  • (Boolean)

462
463
464
# File 'app/models/affair.rb', line 462

def multi_thirds?
  thirds.count > 1
end

#numberObject


122
123
124
# File 'app/models/affair.rb', line 122

def number
  "A#{id.to_s.rjust(7, '0')}"
end

#originatorObject


361
362
363
# File 'app/models/affair.rb', line 361

def originator
  deals.first
end

#refresh!Object

Reload and save! affair to force counts and sums computation


247
248
249
250
# File 'app/models/affair.rb', line 247

def refresh!
  reload
  save!
end

#reload_gapsObject


395
396
397
398
399
# File 'app/models/affair.rb', line 395

def reload_gaps
  return if gaps.none?
  gaps.each { |g| g.undeal! self }
  finish
end

#statusObject


236
237
238
239
240
241
242
243
244
# File 'app/models/affair.rb', line 236

def status
  if closed?
    :go
  elsif deals_count > 1
    :caution
  else
    :stop
  end
end

#third_accountObject Also known as: letterable_account


426
427
428
# File 'app/models/affair.rb', line 426

def 
  third.(third_role)
end

#third_credit_balanceObject


222
223
224
# File 'app/models/affair.rb', line 222

def third_credit_balance
  JournalEntryItem.where(entry: deals.map(&:journal_entry), account: ).sum('real_credit - real_debit')
end

#third_roleObject

Raises:

  • (NotImplementedError)

406
407
408
# File 'app/models/affair.rb', line 406

def third_role
  raise NotImplementedError
end

#thirdsObject

Returns all associated thirds of the affair


381
382
383
# File 'app/models/affair.rb', line 381

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

#unbalanced?Boolean

Check if debit is equal to credit

Returns:

  • (Boolean)

227
228
229
# File 'app/models/affair.rb', line 227

def unbalanced?
  self.debit != self.credit
end

#unletterable?Boolean

Returns:

  • (Boolean)

414
415
416
# File 'app/models/affair.rb', line 414

def unletterable?
  multi_thirds?
end

#work_nameObject


126
127
128
# File 'app/models/affair.rb', line 126

def work_name
  number.to_s
end