Class: LedgerTransaction

Inherits:
ApplicationRecord show all
Includes:
Models::Auditable
Defined in:
app/models/ledger_transaction.rb

Overview

== Schema Information

Table name: ledger_transactions
Database name: primary

id :integer not null, primary key
currency :string(3) not null
description :string(255)
exchange_rate :float
transaction_date :date not null
transaction_number :integer not null
transaction_type :string(255) not null
created_at :datetime
updated_at :datetime
company_id :integer
creator_id :integer
credit_memo_id :integer
delivery_id :integer
invoice_id :integer
landed_cost_id :integer
outgoing_payment_id :integer
receipt_id :integer
reversed_transaction_id :integer
shipment_receipt_id :integer
supplier_id :integer
updater_id :integer
voucher_id :integer

Indexes

index_ledger_transactions_on_credit_memo_id (credit_memo_id)
index_ledger_transactions_on_delivery_id (delivery_id)
index_ledger_transactions_on_invoice_id (invoice_id)
index_ledger_transactions_on_landed_cost_id (landed_cost_id)
index_ledger_transactions_on_outgoing_payment_id (outgoing_payment_id)
index_ledger_transactions_on_receipt_id (receipt_id)
index_ledger_transactions_on_reversed_transaction_id (reversed_transaction_id)
index_ledger_transactions_on_shipment_receipt_id (shipment_receipt_id)
index_ledger_transactions_on_supplier_id (supplier_id)
index_ledger_transactions_on_transaction_date_and_id (transaction_date,id)
index_ledger_transactions_on_transaction_number (transaction_number)
index_ledger_transactions_on_voucher_id (voucher_id)

Constant Summary collapse

LOCKED_ATTRIBUTES =
%w[transaction_number transaction_type transaction_date currency exchange_rate company_id
invoice_id receipt_id shipment_receipt_id landed_cost_id voucher_id credit_memo_id outgoing_payment_id reversed_transaction_id].freeze
TYPE_ABBREVIATIONS =
{ 'JOURNAL_ENTRY' => 'JE',
'ISSUE_TO_ACCOUNT' => 'ITA',
'INVENTORY_ADJUSTMENT' => 'IA',
'LOCATION_TRANSFER' => 'LT',
'ITEM_RECLASSIFICATION' => 'IR',
'SALES_ORDER' => 'SO',
'STORE_TRANSFER' => 'ST',
'RECEIPT' => 'RE',
'PO_RECEIPT' => 'POR',
'PO_LANDED_COST' => 'POLC',
'VOUCHER' => 'VCR',
'RMA_RECEIPT' => 'RMA',
'CREDIT_MEMO' => 'CM',
'MISC_INVOICE' => 'MI',
'PAYMENT' => 'PAY',
'PO_SERVICE_FULFILLMENT' => 'POSF',
'TOTAL_COST_ADJUSTMENT' => 'TCA',
'COGS_ADJUSTMENT' => 'COGS',
'CYCLE_COUNT' => 'CC',
'CONSIGNMENT_INVOICE' => 'CI' }.freeze
SUPPORTED_CSV_ENCODINGS =
{
  'UTF-8' => 'bom|utf-8',
  'Windows-1252' => 'windows-1252:utf-8',
  'ISO-8859-1' => 'iso-8859-1:utf-8'
}.freeze

Constants included from Models::Auditable

Models::Auditable::ALWAYS_IGNORED

Instance Attribute Summary collapse

Belongs to collapse

Methods included from Models::Auditable

#creator, #updater

Has one collapse

Has many collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Models::Auditable

#all_skipped_columns, #audit_reference_data, #should_not_save_version, #stamp_record

Methods inherited from ApplicationRecord

ransackable_associations, ransackable_attributes, ransackable_scopes, ransortable_attributes, #to_relation

Methods included from Models::EventPublishable

#publish_event

Instance Attribute Details

#currencyObject (readonly)



75
# File 'app/models/ledger_transaction.rb', line 75

validates :transaction_number, :transaction_type, :transaction_date, :currency, :company, presence: true

#do_not_set_transaction_numberObject

Returns the value of attribute do_not_set_transaction_number.



51
52
53
# File 'app/models/ledger_transaction.rb', line 51

def do_not_set_transaction_number
  @do_not_set_transaction_number
end

#overrideObject

Returns the value of attribute override.



51
52
53
# File 'app/models/ledger_transaction.rb', line 51

def override
  @override
end

#transaction_dateObject (readonly)



75
# File 'app/models/ledger_transaction.rb', line 75

validates :transaction_number, :transaction_type, :transaction_date, :currency, :company, presence: true

#transaction_numberObject (readonly)



75
# File 'app/models/ledger_transaction.rb', line 75

validates :transaction_number, :transaction_type, :transaction_date, :currency, :company, presence: true

#transaction_typeObject (readonly)



75
# File 'app/models/ledger_transaction.rb', line 75

validates :transaction_number, :transaction_type, :transaction_date, :currency, :company, presence: true

Class Method Details

.build_from_csv(file_path) ⇒ Object

Legacy alias for backwards compatibility



135
136
137
# File 'app/models/ledger_transaction.rb', line 135

def self.build_from_csv(file_path)
  build_from_spreadsheet(file_path)
end

.build_from_spreadsheet(file_path, original_filename: nil, encoding: nil) ⇒ Object



110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'app/models/ledger_transaction.rb', line 110

def self.build_from_spreadsheet(file_path, original_filename: nil, encoding: nil)
  lt = LedgerTransaction.new
  companies = []

  each_spreadsheet_row(file_path, original_filename: original_filename, encoding: encoding) do |row|
    companies << row['company']
    lt.ledger_entries.build(
      ledger_company_account: LedgerCompanyAccount.joins(:company, :ledger_detail_account).where(companies: { number: row['company'] }, ledger_accounts: { number: row['account'] }).first,
      ledger_detail_project: LedgerDetailProject.where(project_number: row['project']).first,
      amount: row['amount'],
      business_unit: BusinessUnit.where(number: row['business_unit']).first,
      description: row['description']
    )
  end

  companies.uniq!
  if companies.length == 1
    lt.company = Company.where(number: companies[0]).first
  else
    lt.override = '1'
  end
  lt
end

.each_csv_row(file_path, encoding: nil) ⇒ Object



166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
# File 'app/models/ledger_transaction.rb', line 166

def self.each_csv_row(file_path, encoding: nil)
  require 'csv'

  if encoding.present? && SUPPORTED_CSV_ENCODINGS[encoding]
    # User specified encoding
    CSV.foreach(file_path, headers: true, return_headers: false, encoding: SUPPORTED_CSV_ENCODINGS[encoding]) do |row|
      yield row.to_h
    end
  else
    # Auto-detect: try UTF-8 first, then Windows-1252
    CSV.foreach(file_path, headers: true, return_headers: false, encoding: 'bom|utf-8') do |row|
      yield row.to_h
    end
  end
rescue Encoding::InvalidByteSequenceError, Encoding::UndefinedConversionError
  CSV.foreach(file_path, headers: true, return_headers: false, encoding: 'windows-1252:utf-8') do |row|
    yield row.to_h
  end
end

.each_spreadsheet_row(file_path, original_filename: nil, encoding: nil, &block) ⇒ Object



139
140
141
142
143
144
145
146
147
# File 'app/models/ledger_transaction.rb', line 139

def self.each_spreadsheet_row(file_path, original_filename: nil, encoding: nil, &block)
  extension = original_filename ? File.extname(original_filename).downcase : File.extname(file_path).downcase

  if extension == '.xlsx'
    each_xlsx_row(file_path, &block)
  else
    each_csv_row(file_path, encoding: encoding, &block)
  end
end

.each_xlsx_row(file_path) ⇒ Object



149
150
151
152
153
154
155
156
157
158
# File 'app/models/ledger_transaction.rb', line 149

def self.each_xlsx_row(file_path)
  require 'roo'
  xlsx = Roo::Spreadsheet.open(file_path, extension: :xlsx)
  headers = xlsx.row(1).map(&:to_s).map(&:strip)
  (2..xlsx.last_row).each do |row_idx|
    row_data = xlsx.row(row_idx)
    row_hash = headers.zip(row_data.map { |v| v.to_s.presence }).to_h
    yield row_hash
  end
end

.filter(params) ⇒ Object



576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
# File 'app/models/ledger_transaction.rb', line 576

def self.filter(params)
  params[:transaction_type] = params[:transaction_type].map(&:presence).compact if params[:transaction_type].present?
  params[:reconciled] = params[:reconciled].to_b if params[:reconciled].present?
  params[:payment_type] = params[:payment_type].map(&:presence).compact if params[:payment_type].present?
  params[:company_account_id] = params[:company_account_id].map(&:presence).compact.map!(&:to_i) if params[:company_account_id].present?
  ledger_transactions = LedgerTransaction.joins('left join ledger_entries on ledger_entries.ledger_transaction_id = ledger_transactions.id')
                                         .joins('left join receipts on ledger_transactions.receipt_id = receipts.id')
                                         .joins('left join outgoing_payments on ledger_transactions.outgoing_payment_id = outgoing_payments.id')
                                         .select('ledger_transactions.*')
                                         .order('ledger_transactions.transaction_date desc, ledger_transactions.transaction_number desc, ledger_entries.id desc')
  ledger_transactions = ledger_transactions.where('ledger_transactions.transaction_type in (?)', params[:transaction_type]) if params[:transaction_type].present?
  ledger_transactions = ledger_transactions.where('ledger_entries.reconciled = ?', params[:reconciled]) if params[:reconciled] == false || params[:reconciled] == true
  ledger_transactions = ledger_transactions.where('outgoing_payments.category in (?) or receipts.category in (?)', params[:payment_type].collect { |pt| pt.downcase.underscore }, params[:payment_type]) if params[:payment_type].present?
  if params[:transaction_date_gteq].present? or params[:transaction_date_lteq].present?
    start_date = params[:transaction_date_gteq].presence || '2000-01-01'
    end_date = params[:transaction_date_lteq].presence || Time.current.utc.to_date.to_fs(:db)
    ledger_transactions = ledger_transactions.where('ledger_transactions.transaction_date between ? and ?', start_date.to_date, end_date.to_date)
  end
  if params[:bank_date_gteq].present? or params[:bank_date_lteq].present?
    bank_start_date = params[:bank_date_gteq].presence || '2000-01-01'
    bank_end_date = params[:bank_date_lteq].presence || Time.current.utc.to_date.to_fs(:db)
    ledger_transactions = ledger_transactions.where('ledger_entries.bank_date between ? and ?', bank_start_date.to_date, bank_end_date.to_date)
  end
  if params[:reconciled_date_gteq].present? or params[:reconciled_date_lteq].present?
    reconciled_start_date = params[:reconciled_date_gteq].presence || '2000-01-01'
    reconciled_end_date = params[:reconciled_date_lteq].presence || Time.current.utc.to_date.to_fs(:db)
    ledger_transactions = ledger_transactions.where('ledger_entries.reconciled_at between ? and ?', reconciled_start_date.to_date, reconciled_end_date.to_date)
  end
  ledger_transactions = ledger_transactions.where('ledger_entries.ledger_company_account_id in (?)', params[:company_account_id]) if params[:company_account_id].present?
  ledger_transactions
end

.process_credit_memo(credit_memo) ⇒ Object



444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
# File 'app/models/ledger_transaction.rb', line 444

def self.process_credit_memo(credit_memo)
  company = credit_memo.company

   = LedgerCompanyAccount.(company.id, RETURN_FREIGHT_COUPONS_ACCOUNT)
   = if credit_memo.category == 'standalone'
                             LedgerCompanyAccount.(company.id, RETURN_COUPONS_STANDALONE_ACCOUNT)
                           else
                             LedgerCompanyAccount.(company.id, RETURN_COUPONS_ACCOUNT)
                           end
   = LedgerCompanyAccount.(company.id, RETURN_SERVICE_COUPONS_ACCOUNT)
   = LedgerCompanyAccount.(company.id, TRADE_ACCOUNTS_RECEIVABLE_ACCOUNT)

  raise 'Unable to find all necessary accounts to post to' if .nil? || .nil? || .nil? || .nil?

  sales_business_unit = company.sales_business_unit
  raise 'Unable to find sales business account' if sales_business_unit.nil?

  default_business_unit = company.default_business_unit
  raise 'Unable to find default business account' if default_business_unit.nil?

  # postings should be
  # + Tax accounts (summarised)
  # + for every line_item's linked cm_category account
  # + 6272 (return freight coupons account)
  # + 5310 (return coupons account) for any linked discounts
  # - 1210 (Trade Accounts Receivable)

  LedgerTransaction.transaction do
    transaction = LedgerTransaction.new(company:, transaction_type: 'CREDIT_MEMO', transaction_date: credit_memo.gl_date, currency: credit_memo.currency, credit_memo:, description: "Credit Memo ##{credit_memo.reference_number}")

    # line items
    credit_memo.line_items.parents_only.each do |li|
      if li.cm_category == 'Item'
        line_item_details = if li.item.tax_class == 'svc'
                              { name: 'Item', account_number: SERVICE_CANCELLATIONS_ACCOUNT, business_unit: 'default' }
                            else
                              { name: 'Item', account_number: CUSTOMER_RETURNS, business_unit: 'default' }
                            end
      elsif li.cm_category == 'Misc'
         = li.
        business_unit = li.business_unit
      else
        line_item_details = begin
          CreditMemo::LINE_ITEM_CATEGORIES.select { |cat| cat[:name] == li.cm_category }[0]
        rescue StandardError
          nil
        end
      end
      raise "Can't find details for credit memo category #{li.cm_category}" if (li.cm_category != 'Misc') && line_item_details.nil?

       ||= LedgerCompanyAccount.(company.id, line_item_details[:account_number])
      raise "Unable to find matching account for credit memo category #{li.cm_category}" if .nil?

      business_unit ||= company.business_unit_for(line_item_details[:business_unit])
      raise "Unable to find matching business unit for credit memo category #{li.cm_category}" if business_unit.nil?

      transaction.ledger_entries << LedgerEntry.new(ledger_company_account: , currency: credit_memo.currency, amount: -li.price_total, description: li.ledger_description, business_unit:)
    end

    # taxes
    unless credit_memo.taxes_grouped_by_rate.empty?
      credit_memo.taxes_grouped_by_rate.each do |tax_type, amount|
        if credit_memo.customer.country.eu_country?
           = credit_memo.resource_tax_rate&.tax_rate&.
        else
           = TAXABLE_STATES.select { |_state, details| details[:tax_type] == tax_type }.first[1][:account]
           = LedgerDetailAccount.where(number: ).first
        end

        raise "Unable to find ledger account for tax type: #{tax_type}, account number: #{}" if .nil?

         = LedgerCompanyAccount.where(company_id: company.id, ledger_detail_account_id: .id).first
        raise "Unable to find company account for tax type: #{tax_type}, account number: #{}" if .nil?

        transaction.ledger_entries << LedgerEntry.new(ledger_company_account_id: .id, currency: credit_memo.currency, amount: -amount)
      end
    end

    # Discount entries - calculate coupon_amount for each category
    # Note: Using cm_category-aware filtering rather than tax_class scopes
    # because some line items (e.g., 'Coupon (Goods)') can have tax_class='none'
    # which would be missed by the .goods scope (tax_class='g')
    freight_categories = ['Freight', 'Coupon (Freight)']

    # Goods discount (items that are not freight and not services)
    goods_discount = credit_memo.line_items
                                .where.not(cm_category: freight_categories)
                                .where.not(tax_class: 'svc')
                                .to_a.sum(&:coupon_amount)
    unless goods_discount.zero?
      transaction.ledger_entries << LedgerEntry.new(
        ledger_company_account: ,
        currency: credit_memo.currency,
        amount: -goods_discount,
        business_unit: default_business_unit
      )
    end

    # Services discount
    services_discount = credit_memo.line_items.services.to_a.sum(&:coupon_amount)
    unless services_discount.zero?
      transaction.ledger_entries << LedgerEntry.new(
        ledger_company_account: ,
        currency: credit_memo.currency,
        amount: -services_discount,
        business_unit: default_business_unit
      )
    end

    # Freight/shipping discount
    shipping_discount = credit_memo.line_items
                                   .where(cm_category: freight_categories)
                                   .to_a.sum(&:coupon_amount)
    unless shipping_discount.zero?
      transaction.ledger_entries << LedgerEntry.new(
        ledger_company_account: ,
        currency: credit_memo.currency,
        amount: -shipping_discount,
        business_unit: sales_business_unit
      )
    end

    # AR entry: calculate as balancing amount to ensure ledger always balances
    # This avoids floating-point precision issues between stored totals and calculated sums
    other_entries_sum = transaction.ledger_entries.sum(&:amount)
    ar_amount = -other_entries_sum
    transaction.ledger_entries << LedgerEntry.new(ledger_company_account: , currency: credit_memo.currency, amount: ar_amount)

    transaction.save!
  end
end

.process_intracompany_st_delivery(delivery) ⇒ Object



415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
# File 'app/models/ledger_transaction.rb', line 415

def self.process_intracompany_st_delivery(delivery)
  order = delivery.order

  consignor = order.from_store.consignee_party
  consignee = order.to_store.consignee_party

  company = order.from_store.company
  to_company = order.to_store.company
  raise 'Unable to process intracompany ST delivery as from/to companies are different' if company != to_company

   = LedgerCompanyAccount.(company.id, order.from_store.)

  # we put the cogs value into an intracompany transit account while in transit
   = LedgerCompanyAccount.(company.id, INTRACOMPANY_TRANSIT_ACCOUNT)

  raise 'Unable to find all necessary accounts to post to' if .nil? || .nil?

  default_business_unit = company.default_business_unit
  raise 'Unable to find default business account' if default_business_unit.nil?

  LedgerTransaction.transaction do
    cogs = delivery.calculate_all_cogs
    transaction = LedgerTransaction.new(company:, transaction_type: 'STORE_TRANSFER', transaction_date: delivery.shipped_date, currency: order.currency, delivery:)
    transaction.ledger_entries << LedgerEntry.new(ledger_company_account: , currency: order.currency, amount: cogs, business_unit: default_business_unit)
    transaction.ledger_entries << LedgerEntry.new(ledger_company_account: , currency: order.currency, amount: -cogs, business_unit: default_business_unit)
    transaction.save!
  end
end

.process_invoice(invoice) ⇒ Object



249
250
251
252
253
254
255
# File 'app/models/ledger_transaction.rb', line 249

def self.process_invoice(invoice)
  if invoice.invoice_type == Invoice::ST
    LedgerTransaction.process_st_invoice(invoice)
  else
    LedgerTransaction.process_so_invoice(invoice)
  end
end

.process_payment(payment) ⇒ Object



608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
# File 'app/models/ledger_transaction.rb', line 608

def self.process_payment(payment)
  LedgerTransaction.transaction do
    company = payment.company
    currency = payment.currency

    transaction = LedgerTransaction.new(company:, transaction_type: 'PAYMENT', transaction_date: payment.payment_date, currency:, exchange_rate: payment.exchange_rate, outgoing_payment: payment)

     = payment..
     = LedgerCompanyAccount.(company.id, TRADE_ACCOUNTS_RECEIVABLE_ACCOUNT)

     = if payment.supplier..present?
                               LedgerCompanyAccount.(company.id, payment.supplier..number)
                             else
                               LedgerCompanyAccount.(company.id, TRADE_ACCOUNTS_PAYABLE_ACCOUNT)
                             end

    raise 'Unable to find all necessary accounts to post to' if .nil? || .nil? || .nil?

    transaction.ledger_entries << LedgerEntry.new(ledger_company_account: , currency:, amount: -payment.amount)

    payment.outgoing_payment_items.each do |pi|
      if pi.voucher_item.present?
        transaction.ledger_entries << if pi.voucher_item..present?
                                        LedgerEntry.new(ledger_company_account: pi.voucher_item., currency:, amount: pi.amount, business_unit: pi.voucher_item.business_unit)
                                      else
                                        LedgerEntry.new(ledger_company_account: , currency:, amount: pi.amount)
                                      end
      elsif pi.credit_memo.present? || pi.receipt.present?
        transaction.ledger_entries << LedgerEntry.new(ledger_company_account: , currency:, amount: pi.amount)
      end
    end

    transaction.save!
  end
end

.process_so_invoice(invoice) ⇒ Object



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
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
# File 'app/models/ledger_transaction.rb', line 257

def self.process_so_invoice(invoice)
  company = invoice.company

  # goods and services account dependent on Store selected
  if invoice.store && (invoice.store.owner != 'warmlyyours')
    # it's a consignment order, so use the consignment account
     = LedgerCompanyAccount.(company.id, CONSIGNMENT_ACCOUNT)
     = LedgerCompanyAccount.(company.id, CONSIGNMENT_ACCOUNT)
  else
     = LedgerCompanyAccount.(company.id, PURCHASED_FINISHED_GOODS_ACCOUNT)
     = LedgerCompanyAccount.(company.id, SERVICES_PENDING_FULFILLMENT_DEBIT_ACCOUNT)
  end
   = LedgerCompanyAccount.(company.id, PRODUCT_SALES_ACCOUNT)
   = LedgerCompanyAccount.(company.id, SERVICE_SALES_ACCOUNT)
   = LedgerCompanyAccount.(company.id, COUPONS_ACCOUNT)
   = LedgerCompanyAccount.(company.id, SERVICE_COUPONS_ACCOUNT)
   = LedgerCompanyAccount.(company.id, FREIGHT_COUPONS_ACCOUNT)
   = LedgerCompanyAccount.(company.id, FREIGHT_ACCOUNT)
   = invoice.
   = LedgerCompanyAccount.(company.id, PRODUCT_COGS_ACCOUNT)
   = LedgerCompanyAccount.(company.id, PRODUCT_COSS_ACCOUNT)
   = LedgerCompanyAccount.(company.id, SALES_EXPENSES_ACCOUNT)

  if .nil? || .nil? || .nil? || .nil? || .nil? || .nil? || .nil? || .nil? || .nil? || .nil? || .nil?
    raise 'Unable to find all necessary accounts to post to'
  end

  sales_business_unit = company.sales_business_unit
  raise 'Unable to find sales business account' if sales_business_unit.nil?

  default_business_unit = company.default_business_unit
  raise 'Unable to find default business account' if default_business_unit.nil?

  LedgerTransaction.transaction do
    t_type = case invoice.invoice_type
             when Invoice::MI
               'MISC_INVOICE'
             when Invoice::CI
               'CONSIGNMENT_INVOICE'
             else
               'SALES_ORDER'
             end

    transaction = LedgerTransaction.new(company:, transaction_type: t_type, transaction_date: invoice.gl_date, currency: invoice.currency, invoice:)
    # non service items
    non_service_item_total = invoice.non_service_line_items.to_a.sum(&:price_total)
    transaction.ledger_entries << LedgerEntry.new(ledger_company_account: , currency: invoice.currency, amount: -non_service_item_total, business_unit: default_business_unit) unless non_service_item_total == 0

    # service items
    service_item_total = invoice.service_line_items.to_a.sum(&:price_total)
    transaction.ledger_entries << LedgerEntry.new(ledger_company_account: , currency: invoice.currency, amount: -service_item_total, business_unit: default_business_unit) unless service_item_total == 0

    # misc items
    invoice.line_items.where(cm_category: 'Misc').each do |li|
      transaction.ledger_entries << LedgerEntry.new(ledger_company_account: li., currency: invoice.currency, amount: -li.price_total, business_unit: li.business_unit)
    end

    # fees
    invoice.line_items.where(cm_category: 'Fee').each do |li|
      transaction.ledger_entries << LedgerEntry.new(ledger_company_account: , currency: invoice.currency, amount: -li.price_total, business_unit: sales_business_unit)
    end

    # shipping
    freight_total = BigDecimal(invoice.line_items.where("cm_category = 'Freight'").to_a.sum(&:price_total))
    transaction.ledger_entries << LedgerEntry.new(ledger_company_account: , currency: invoice.currency, amount: -freight_total, business_unit: sales_business_unit) unless freight_total == 0

    # taxes
    customer_ledger_project = invoice&.customer&.ledger_detail_project
    invoice.taxes_grouped_by_rate.each do |tax_type, tax_amount|
      if invoice.customer.country.eu_country?
        # if european, we search for the account number directly in the DB
         = LedgerDetailAccount.find(invoice.resource_tax_rate.tax_rate.)
      else
         = TAXABLE_STATES.select { |_state, details| details[:tax_type] == tax_type }.first[1][:account]
         = LedgerDetailAccount.where(number: ).first
      end

      raise "Unable to find ledger account for tax type: #{tax_type}, account number: #{}" if .nil?

       = LedgerCompanyAccount.where(company_id: company.id, ledger_detail_account_id: .id).first
      raise "Unable to find company account for tax type: #{tax_type}, account number: #{}" if .nil?

      transaction.ledger_entries << LedgerEntry.new(ledger_company_account: , currency: invoice.currency, amount: tax_amount * -1, ledger_detail_project: customer_ledger_project)
    end

    # goods discount
    goods_discount = invoice.line_items.goods.to_a.sum(&:coupon_amount) + BigDecimal(invoice.line_items.where("cm_category = 'Coupon (Goods)'").to_a.sum(&:discounted_total))
    transaction.ledger_entries << LedgerEntry.new(ledger_company_account: , currency: invoice.currency, amount: -goods_discount, business_unit: default_business_unit) unless goods_discount == 0

    # services discount
    services_discount = invoice.line_items.services.to_a.sum(&:coupon_amount) + BigDecimal(invoice.line_items.where("cm_category = 'Coupon (Services)'").to_a.sum(&:discounted_total))
    transaction.ledger_entries << LedgerEntry.new(ledger_company_account: , currency: invoice.currency, amount: -services_discount, business_unit: default_business_unit) unless services_discount == 0

    # shipping discount
    freight_discount = invoice.line_items.where("cm_category = 'Freight'").to_a.sum(&:coupon_amount) + BigDecimal(invoice.line_items.where("cm_category = 'Coupon (Freight)'").to_a.sum(&:discounted_total))
    transaction.ledger_entries << LedgerEntry.new(ledger_company_account: , currency: invoice.currency, amount: -freight_discount, business_unit: sales_business_unit) unless freight_discount == 0

    # total into a/r
    transaction.ledger_entries << LedgerEntry.new(ledger_company_account: , currency: invoice.currency, amount: invoice.total, business_unit: invoice.business_unit)

    # cogs
    non_service_cogs = invoice.calculate_cogs(['g'])
    service_only_cogs = invoice.calculate_cogs(['svc'])

    unless (non_service_cogs == 0) && invoice.non_service_line_items.empty?
      transaction.ledger_entries << LedgerEntry.new(ledger_company_account: , currency: invoice.currency, amount: non_service_cogs, business_unit: default_business_unit)
      transaction.ledger_entries << LedgerEntry.new(ledger_company_account: , currency: invoice.currency, amount: -non_service_cogs)
    end
    unless (service_only_cogs == 0) && invoice.service_line_items.empty?
      transaction.ledger_entries << LedgerEntry.new(ledger_company_account: , currency: invoice.currency, amount: service_only_cogs, business_unit: default_business_unit)
      transaction.ledger_entries << LedgerEntry.new(ledger_company_account: , currency: invoice.currency, amount: -service_only_cogs)
    end

    unless transaction.valid?
      transaction.errors.add :base, "ENTRIES DETAIL: #{transaction.ledger_entries.collect { |le| "#{le..name} #{le.amount}" }.join(', ')}"
      raise "Cannot save ledger transaction, errors: #{transaction.errors.full_messages}"
    end

    transaction.save!
  end
end

.process_st_invoice(invoice) ⇒ Object



379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
# File 'app/models/ledger_transaction.rb', line 379

def self.process_st_invoice(invoice)
  company = invoice.company

   = LedgerCompanyAccount.(company.id, PURCHASED_FINISHED_GOODS_ACCOUNT)
   = LedgerCompanyAccount.(company.id, SERVICES_PENDING_FULFILLMENT_DEBIT_ACCOUNT)
   = LedgerCompanyAccount.(company.id, INTERCOMPANY_SALES_ACCOUNT)
   = invoice.
   = LedgerCompanyAccount.(company.id, INTERCOMPANY_COGS_ACCOUNT)

  raise 'Unable to find all necessary accounts to post to' if .nil? || .nil? || .nil? || .nil? || .nil?

  # raise "Subtotal and Total do not match for this ST invoice - may have coupons, shipping or tax which should not be there" if invoice.subtotal != invoice.total

  sales_business_unit = company.sales_business_unit
  raise 'Unable to find sales business account' if sales_business_unit.nil?

  default_business_unit = company.default_business_unit
  raise 'Unable to find default business account' if default_business_unit.nil?

  LedgerTransaction.transaction do
    transaction = LedgerTransaction.new(company:, transaction_type: 'STORE_TRANSFER', transaction_date: invoice.gl_date, currency: invoice.currency, invoice:)
    transaction.ledger_entries << LedgerEntry.new(ledger_company_account: , currency: invoice.currency, amount: -invoice.total, business_unit: default_business_unit)
    transaction.ledger_entries << LedgerEntry.new(ledger_company_account: , currency: invoice.currency, amount: invoice.total, business_unit: invoice.business_unit)

    non_service_cogs = invoice.calculate_cogs(['g'])
    service_only_cogs = invoice.calculate_cogs(['svc'])

    transaction.ledger_entries << LedgerEntry.new(ledger_company_account: , currency: invoice.currency, amount: invoice.calculate_all_cogs, business_unit: default_business_unit)

    transaction.ledger_entries << LedgerEntry.new(ledger_company_account: , currency: invoice.currency, amount: -non_service_cogs) unless (non_service_cogs == 0) && invoice.non_service_line_items.empty?
    transaction.ledger_entries << LedgerEntry.new(ledger_company_account: , currency: invoice.currency, amount: -service_only_cogs) unless (service_only_cogs == 0) && invoice.service_line_items.empty?

    transaction.save!
  end
end

Instance Method Details

#balanceObject



186
187
188
189
190
191
192
# File 'app/models/ledger_transaction.rb', line 186

def balance
  balance = BigDecimal(0)
  ledger_entries.each do |entry|
    balance += entry.amount unless entry.amount.nil?
  end
  balance
end

#balance_inter_company_transactionObject



666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
# File 'app/models/ledger_transaction.rb', line 666

def balance_inter_company_transaction
  return unless balance == 0

  groups = ledger_entries.group_by do |e|
             e.&.company
           end.delete_if { |k, _v| k.nil? }
  return unless groups.length > 1

  # must be intercompany so need to balance them out, unless they already are balanced
   = LedgerDetailAccount.find_by(number: INTERCOMPANY_TRANSFERS_ACCOUNT)
  unless 
    errors.add(:base, 'Cannot find Inter Company Transfers account')
    return
  end

  groups.each do |company, entries|
    balance = BigDecimal(0)
    entries.each { |e| balance += e.amount }
    next unless balance != 0

    # there is a difference so need to add it to the intercompany transfer account
    diff = -balance
     = LedgerCompanyAccount.where(company_id: company.id, ledger_detail_account_id: .id).first
    unless 
      errors.add(:base, "Cannot find Inter Company Transfers account for company id: #{company.id}")
      return
    end

    entry = LedgerEntry.new(ledger_transaction: self, ledger_company_account: , amount: diff, currency:)
    unless entry.valid?
      entry.errors.full_messages.each do |msg|
        errors.add(:base, "Inter-company transfer entry invalid: #{msg}")
      end
      return
    end
    # ledger_entries << entry
  end
end

#balances_are_zeroObject



658
659
660
661
662
663
664
# File 'app/models/ledger_transaction.rb', line 658

def balances_are_zero
  errors.add(:base, "Balance of amounts must equal 0, but is #{balance}. ENTRIES DETAIL: #{ledger_entries.collect { |le| "#{le..try(:name)} #{le.amount}" }.join(', ')}") unless balance.zero?
  errors.add(:base, "Balance of company amounts must equal 0, but is #{company_balance}. ENTRIES DETAIL: #{ledger_entries.collect { |le| "#{le..try(:name)} #{le.company_amount}" }.join(', ')}") unless company_balance.zero?
  return if consolidated_balance.zero?

  errors.add(:base, "Balance of consolidated amounts must equal 0, but is #{consolidated_balance}. ENTRIES DETAIL: #{ledger_entries.collect { |le| "#{le..try(:name)} #{le.consolidated_amount}" }.join(', ')}")
end

#cat_for_reportObject



770
771
772
773
774
775
776
# File 'app/models/ledger_transaction.rb', line 770

def cat_for_report
  if outgoing_payment.present?
    outgoing_payment.category
  elsif receipt.present?
    receipt.category
  end
end

#check_for_discrepanciesObject



705
706
707
708
709
710
711
712
713
714
# File 'app/models/ledger_transaction.rb', line 705

def check_for_discrepancies
  if company_balance != 0 && company_balance.abs <= (BigDecimal('0.05'))
    # correct the difference but only if it is +/- 0.05 or less
    ledger_entries.last.company_amount += -company_balance
  end
  return unless consolidated_balance != 0 && consolidated_balance.abs <= (BigDecimal('0.05'))

  # correct the difference but only if it is +/- 0.05 or less
  ledger_entries.last.consolidated_amount += -consolidated_balance
end

#clear_exchange_ratesObject



716
717
718
719
720
721
722
723
724
725
726
# File 'app/models/ledger_transaction.rb', line 716

def clear_exchange_rates
  return if reversed_transaction_id.present?
  return unless ledger_entries.any? { |le| le.new_record? || le.changed? }

  ledger_entries.each do |le|
    le.consolidated_exchange_rate = nil
    le.consolidated_amount = nil
    le.company_exchange_rate = nil
    le.company_amount = nil
  end
end

#companyCompany

Returns:

See Also:

Validations:



53
# File 'app/models/ledger_transaction.rb', line 53

belongs_to :company, optional: true

#company_balanceObject



194
195
196
197
198
199
200
# File 'app/models/ledger_transaction.rb', line 194

def company_balance
  company_balance = BigDecimal(0)
  ledger_entries.each do |entry|
    company_balance += entry.company_amount unless entry.company_amount.nil?
  end
  company_balance
end

#consolidated_balanceObject



202
203
204
205
206
207
208
# File 'app/models/ledger_transaction.rb', line 202

def consolidated_balance
  consolidated_balance = BigDecimal(0)
  ledger_entries.each do |entry|
    consolidated_balance += entry.consolidated_amount unless entry.consolidated_amount.nil?
  end
  consolidated_balance
end

#credit_memoCreditMemo

Returns:

See Also:



59
# File 'app/models/ledger_transaction.rb', line 59

belongs_to :credit_memo, optional: true

#deliveryDelivery

Returns:

See Also:



61
# File 'app/models/ledger_transaction.rb', line 61

belongs_to :delivery, optional: true

#edit_allowedObject



803
804
805
806
807
808
809
810
811
812
813
814
# File 'app/models/ledger_transaction.rb', line 803

def edit_allowed
  return if company_id.nil? || transaction_type.nil? || transaction_date.nil?
  return unless changed? && changes.keys.any? { |attr| LOCKED_ATTRIBUTES.include?(attr) }

  arel = LedgerClosingPeriod.arel_table
  lcls = LedgerClosingPeriod.where(arel[:companies].overlap([company_id, 'ALL']))
  lcls = lcls.where(arel[:transaction_types].overlap(['ALL', transaction_type]))
  lcls = lcls.where('close_to >= ?', transaction_date.to_fs(:db))
  return unless lcls.any?

  errors.add :base, "Period is closed for #{transaction_type} for #{company.short_name} up to #{lcls.first.close_to.to_fs(:crm_default)}."
end

#explanation_for_reportObject



778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
# File 'app/models/ledger_transaction.rb', line 778

def explanation_for_report
  if outgoing_payment.present?
    if outgoing_payment.category == 'check'
      check = outgoing_payment.checks.last
      check.nil? ? nil : "#{check.check_number} #{check.payee}"
    else
      "#{outgoing_payment.remark} #{outgoing_payment.supplier.full_name}"
    end
  elsif receipt.present?
    if receipt.category == 'Credit Card'
      "#{receipt.card_type} #{receipt.customer.full_name}"
    else
      "#{receipt.reference} #{receipt.customer.full_name}"
    end
  else
    description
  end
end

#has_two_ledger_entriesObject



652
653
654
655
656
# File 'app/models/ledger_transaction.rb', line 652

def has_two_ledger_entries
  return unless ledger_entries.length < 2

  errors.add(:base, 'At least 2 ledger entries are required.')
end

#invoiceInvoice

Returns:

See Also:



54
# File 'app/models/ledger_transaction.rb', line 54

belongs_to :invoice, optional: true

#item_ledger_entriesActiveRecord::Relation<ItemLedgerEntry>

Returns:

See Also:



64
# File 'app/models/ledger_transaction.rb', line 64

has_many :item_ledger_entries

#landed_costLandedCost

Returns:

See Also:



57
# File 'app/models/ledger_transaction.rb', line 57

belongs_to :landed_cost, optional: true

#ledger_entriesActiveRecord::Relation<LedgerEntry>

Returns:

See Also:



65
# File 'app/models/ledger_transaction.rb', line 65

has_many :ledger_entries, inverse_of: :ledger_transaction, dependent: :destroy

#outgoing_paymentOutgoingPayment



60
# File 'app/models/ledger_transaction.rb', line 60

belongs_to :outgoing_payment, optional: true

#receiptReceipt

Returns:

See Also:



55
# File 'app/models/ledger_transaction.rb', line 55

belongs_to :receipt, optional: true

#resource_refObject



744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
# File 'app/models/ledger_transaction.rb', line 744

def resource_ref
  if invoice.present?
    invoice.reference_number
  elsif receipt.present?
    receipt.id
  elsif shipment_receipt.present?
    shipment_receipt_id
  elsif landed_cost.present?
    landed_cost_id
  elsif voucher.present?
    voucher.reference_number
  elsif credit_memo.present?
    credit_memo.reference_number
  elsif outgoing_payment.present?
    outgoing_payment.reference_number
  elsif item_ledger_entries.any?
    refs = []
    item_ledger_entries.each do |entry|
      refs << entry.id
    end
    refs.join(' ')
  else
    id
  end
end

#reverse(reversal_date) ⇒ Object



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

def reverse(reversal_date)
  tran = dup
  tran.description = description.present? ? (description + ' - REVERSAL') : 'REVERSAL'
  tran.transaction_number = nil
  tran.created_at = nil
  tran.updated_at = nil
  tran.creator_id = nil
  tran.updater_id = nil
  tran.transaction_date = reversal_date
  tran.reversed_transaction_id = id
  ledger_entries.each do |le|
    new_le = le.dup
    new_le.ledger_transaction_id = nil
    new_le.created_at = nil
    new_le.updated_at = nil
    new_le.creator_id = nil
    new_le.updater_id = nil
    new_le.amount = -le.amount
    new_le.company_amount = -le.company_amount
    new_le.consolidated_amount = -le.consolidated_amount
    new_le.reconciled = false
    tran.ledger_entries << new_le
  end
  tran.save!
  tran
end

#reversed_transactionLedgerTransaction



62
# File 'app/models/ledger_transaction.rb', line 62

belongs_to :reversed_transaction, class_name: 'LedgerTransaction', optional: true

#set_company_amountObject



732
733
734
735
736
# File 'app/models/ledger_transaction.rb', line 732

def set_company_amount
  return if reversed_transaction_id.present?

  ledger_entries.each(&:set_company_amount)
end

#set_company_and_overrideObject



237
238
239
240
241
242
243
244
245
246
247
# File 'app/models/ledger_transaction.rb', line 237

def set_company_and_override
  company_ids = []
  ledger_entries.each do |e|
    company_ids << e..company_id unless company_ids.include?(e..company_id)
  end
  if company_ids.length == 1
    self.company_id = company_ids[0]
  else
    self.override = true
  end
end

#set_consolidated_amountObject



738
739
740
741
742
# File 'app/models/ledger_transaction.rb', line 738

def set_consolidated_amount
  return if reversed_transaction_id.present?

  ledger_entries.each(&:set_consolidated_amount)
end

#set_currencyObject



728
729
730
# File 'app/models/ledger_transaction.rb', line 728

def set_currency
  ledger_entries.each(&:set_currency)
end

#set_supplier_idObject



816
817
818
819
# File 'app/models/ledger_transaction.rb', line 816

def set_supplier_id
  doc = voucher || outgoing_payment
  self.supplier_id = doc.supplier_id if doc.present?
end

#set_transaction_numberObject



644
645
646
647
648
649
650
# File 'app/models/ledger_transaction.rb', line 644

def set_transaction_number
  return unless (!do_not_set_transaction_number == true) && transaction_number.blank?

  # getting number from a shared sequence now
  seq = LedgerTransaction.find_by_sql("SELECT nextval('transaction_numbers_seq') AS transaction_number")
  self.transaction_number = seq[0].transaction_number.to_s
end

#shipment_receiptShipmentReceipt



56
# File 'app/models/ledger_transaction.rb', line 56

belongs_to :shipment_receipt, optional: true

#to_sObject



106
107
108
# File 'app/models/ledger_transaction.rb', line 106

def to_s
  "Ledger Transaction ##{transaction_number}"
end

#transaction_date_not_before_originalObject



797
798
799
800
801
# File 'app/models/ledger_transaction.rb', line 797

def transaction_date_not_before_original
  return unless transaction_date < reversed_transaction.transaction_date

  errors.add(:transaction_date, 'cannot be before the original transaction date')
end

#transaction_reversalLedgerTransaction



63
# File 'app/models/ledger_transaction.rb', line 63

has_one :transaction_reversal, class_name: 'LedgerTransaction', foreign_key: 'reversed_transaction_id'

#voucherVoucher

Returns:

See Also:



58
# File 'app/models/ledger_transaction.rb', line 58

belongs_to :voucher, optional: true