Class: Receipt

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

Overview

== Schema Information

Table name: receipts
Database name: primary

id :integer not null, primary key
amount :decimal(8, 2)
approved_for_refund :boolean
auth_code :string(255)
card_type :string(255)
category :string(255)
currency :string(255)
email :string
exchange_rate :float
exported :boolean default(FALSE), not null
gl_date :date
hold_for_review :boolean default(FALSE), not null
jde_number :string(255)
receipt_date :date
reference :string(255)
remark :string(255)
state :string(255)
created_at :datetime
updated_at :datetime
authorization_id :integer
bank_account_id :integer
company_id :integer
creator_id :integer
customer_id :integer
deleter_id :integer
edi_transaction_id :string
order_id :integer
payment_id :integer
updater_id :integer

Indexes

idx_customer_id_receipt_date (customer_id,receipt_date)
index_receipts_on_bank_account_id (bank_account_id)
index_receipts_on_company_id (company_id)
index_receipts_on_edi_transaction_id (edi_transaction_id) UNIQUE
index_receipts_on_hold_for_review (hold_for_review)
index_receipts_on_payment_id (payment_id)
index_receipts_on_state (state)
receipts_authorization_id_index (authorization_id)

Foreign Keys

fk_rails_... (authorization_id => legacy_authorizations.id)
fk_rails_... (bank_account_id => bank_accounts.id)
fk_rails_... (company_id => companies.id)
fk_rails_... (customer_id => parties.id)

Defined Under Namespace

Classes: SubmitToTaxjar

Constant Summary collapse

CATEGORIES =
['ACH', 'Cash', 'Check', 'Credit Card', 'Echeck', 'Wire Transfer', 'Amazon Pay', 'Non-Cash', 'PayPal', 'Payment Link'].freeze
CARD_TYPES =
%w[american_express discover master visa vault].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 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

#amountObject (readonly)



72
# File 'app/models/receipt.rb', line 72

validates :company_id, :customer_id, :category, :amount, :currency, :gl_date, :receipt_date, :state, presence: true

#bank_account_idObject (readonly)



73
# File 'app/models/receipt.rb', line 73

validates :bank_account_id, presence: { unless: proc { |r| r.category == 'Non-Cash' } }

#categoryObject (readonly)



72
# File 'app/models/receipt.rb', line 72

validates :company_id, :customer_id, :category, :amount, :currency, :gl_date, :receipt_date, :state, presence: true

#company_idObject (readonly)



72
# File 'app/models/receipt.rb', line 72

validates :company_id, :customer_id, :category, :amount, :currency, :gl_date, :receipt_date, :state, presence: true

#currencyObject (readonly)



72
# File 'app/models/receipt.rb', line 72

validates :company_id, :customer_id, :category, :amount, :currency, :gl_date, :receipt_date, :state, presence: true

#customer_idObject (readonly)



72
# File 'app/models/receipt.rb', line 72

validates :company_id, :customer_id, :category, :amount, :currency, :gl_date, :receipt_date, :state, presence: true

#customer_nameObject



272
273
274
# File 'app/models/receipt.rb', line 272

def customer_name
  customer.try(:full_name)
end

#customer_typeObject



210
211
212
# File 'app/models/receipt.rb', line 210

def customer_type
  customer.try(:type)
end

#gl_dateObject (readonly)



72
# File 'app/models/receipt.rb', line 72

validates :company_id, :customer_id, :category, :amount, :currency, :gl_date, :receipt_date, :state, presence: true

#new_gl_dateObject



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

validates :new_gl_date, presence: { on: :update, unless: proc { |r| r.state_changed? or r.approved_for_refund? or r.only_remarks_changed? } }

#process_cc_paymentObject

Returns the value of attribute process_cc_payment.



110
111
112
# File 'app/models/receipt.rb', line 110

def process_cc_payment
  @process_cc_payment
end

#receipt_dateObject (readonly)



72
# File 'app/models/receipt.rb', line 72

validates :company_id, :customer_id, :category, :amount, :currency, :gl_date, :receipt_date, :state, presence: true

#stateObject (readonly)



72
# File 'app/models/receipt.rb', line 72

validates :company_id, :customer_id, :category, :amount, :currency, :gl_date, :receipt_date, :state, presence: true

Class Method Details

.available_to_applyActiveRecord::Relation<Receipt>

A relation of Receipts that are available to apply. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Receipt>)

See Also:



106
# File 'app/models/receipt.rb', line 106

scope :available_to_apply, -> { where(state: %w[unapplied partially_applied], approved_for_refund: true) }

.create_receipts_from_xlsx(xlsx_file) {|1, total_steps, 'Reading Excel file'| ... } ⇒ Object

Yields:

  • (1, total_steps, 'Reading Excel file')


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

def self.create_receipts_from_xlsx(xlsx_file)
  report = { errors: [], successes: [], new_receipt_ids: [] }
  new_receipt = nil
  new_receipts = []
  total_steps = 5

  require 'roo'
  yield(1, total_steps, 'Reading Excel file') if block_given?
  sleep 2
  receipts_xlsx = Roo::Spreadsheet.open(xlsx_file.to_file.to_s)

  yield(2, total_steps, 'Building receipts') if block_given?
  sleep 4
  yield(3, total_steps, 'Adding receipt details') if block_given?
  sleep 4
  receipts_xlsx.parse(headers: true, clean: true).each_with_index do |row, index|
    next unless index >= 1

    if row.map { |_k, v| v }.uniq.compact.present?
      # get the components of receipt
      if row['company_id'].present?
        company = Company.find(row['company_id'])
         = company. || LedgerAccount.find_by(number: row['bank_account'].to_i).ledger_company_accounts.first.
        customer = Party.find_by(id: row['payor_id'].to_i)
        receipt_date = row['receipt_date']
        new_receipt = Receipt.new(company: company, bank_account: , customer: customer, category: row['receipt_category'], reference: row['reference'],
                                  currency: row['currency'], amount: row['receipt_amount'], gl_date: row['gl_date'], receipt_date: receipt_date,
                                  card_type: row['card_type'], auth_code: row['authorization_code'])
      end

      # Add all the necesary receipt details to link to its receipt
      if new_receipt.present? && row['receipt_detail_category'].present?
        if row['receipt_detail_category'] == 'Invoice'
          invoice = Invoice.find_by(reference_number: row['invoice_credit_memo_number'])
          write_off = row['write_off_amount'].present? ? row['write_off_amount'].to_f : 0
          new_receipt.receipt_details << ReceiptDetail.new(category: row['receipt_detail_category'], invoice: invoice, gl_date: receipt_date, amount: row['receipt_detail_amount'].to_f,
                                                           write_off: write_off, write_off_code: row['write_off_code'], remark: row['receipt_detail_remark'])
        elsif row['receipt_detail_category'] == 'Credit Memo'
          credit_memo = CreditMemo.find_by(reference_number: row['invoice_credit_memo_number'])
          new_receipt.receipt_details << ReceiptDetail.new(category: row['receipt_detail_category'], credit_memo: credit_memo, gl_date: receipt_date,
                                                           amount: row['receipt_detail_amount'], remark: row['receipt_detail_remark'])
        elsif row['receipt_detail_category'] == 'GL Account'
          new_receipt.receipt_details << ReceiptDetail.new(category: row['receipt_detail_category'], amount: row['receipt_detail_amount'],
                                                           ledger_company_account: LedgerCompanyAccount.joins(:company, :ledger_detail_account).where(companies: { number: row['gl_company'] }, ledger_accounts: { number: row['gl_account'] }).first,
                                                           business_unit: BusinessUnit.where(number: row['business_unit']).first,
                                                           ledger_detail_project: LedgerDetailProject.where(project_number: row['project']).first, remark: row['receipt_detail_remark'])
        end
      end

    end

    if new_receipt.present? && (row.map { |_k, v| v }.uniq.compact.empty? || receipts_xlsx.last_row == (index + 1))
      new_receipts << new_receipt unless new_receipt.nil?
      new_receipt = nil
    end
  end

  yield(4, total_steps, 'Recording receipts in the database') if block_given?
  begin
    Receipt.transaction do
      new_receipts.each do |new_receipt|
        new_receipt.save!
        report[:new_receipt_ids] << new_receipt.id
        msg = "#{Time.current}: Created new receipt id: #{new_receipt.id}"
        logger.info msg
        report[:successes] << msg
      end
      msg = 'Receipts have been created successfully.'
      report[:successes] << msg
    end
  rescue StandardError => e
    msg = "Unable to create receipt, message : #{e}."
    logger.error msg
    report[:errors] << msg
  end
  yield(5, total_steps, 'Finishing...') if block_given?
  sleep 3
  report
end

.edit_receipts_from_xlsx(xlsx_file) {|1, total_steps, 'Reading Excel file'| ... } ⇒ Object

Yields:

  • (1, total_steps, 'Reading Excel file')


643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
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
704
705
706
707
708
709
710
711
712
713
714
# File 'app/models/receipt.rb', line 643

def self.edit_receipts_from_xlsx(xlsx_file)
  report = { errors: [], successes: [], edited_receipt_ids: [] }
  receipt = nil
  receipts = []
  total_steps = 4

  require 'roo'
  yield(1, total_steps, 'Reading Excel file') if block_given?
  sleep 2
  receipts_xlsx = Roo::Spreadsheet.open(xlsx_file.to_file.to_s)

  yield(2, total_steps, 'Adding receipt details') if block_given?
  sleep 4
  receipts_xlsx.parse(headers: true, clean: true).each_with_index do |row, index|
    next unless index >= 1

    if row.map { |_k, v| v }.uniq.compact.present?
      # update data of existing receipt
      if row['receipt_number'].present?
        receipt = Receipt.find_by(id: row['receipt_number'])
        remark = row['receipt_remark'] || receipt.remark
        receipt.remark = remark
        receipt.new_gl_date = row['new_gl_date']
      end

      # add new receipt details to existing receipt
      if receipt.present? && row['receipt_detail_category'].present?
        if row['receipt_detail_category'] == 'Invoice' && row['invoice_credit_memo_number'].present?
          invoice = Invoice.find_by(reference_number: row['invoice_credit_memo_number'])
          write_off = row['write_off_amount'].present? ? row['write_off_amount'].to_f : 0
          receipt.receipt_details << ReceiptDetail.new(category: row['receipt_detail_category'], invoice: invoice, gl_date: receipt.new_gl_date, amount: row['receipt_detail_amount'].to_f,
                                                       write_off: write_off, write_off_code: row['write_off_code'], remark: row['receipt_detail_remark'])
        elsif row['receipt_detail_category'] == 'Credit Memo' && row['invoice_credit_memo_number'].present?
          credit_memo = CreditMemo.find_by(reference_number: row['invoice_credit_memo_number'])
          receipt.receipt_details << ReceiptDetail.new(category: row['receipt_detail_category'], credit_memo: credit_memo, gl_date: receipt.new_gl_date,
                                                       amount: row['receipt_detail_amount'], remark: row['receipt_detail_remark'])
        elsif row['receipt_detail_category'] == 'GL Account'
          receipt.receipt_details << ReceiptDetail.new(category: row['receipt_detail_category'], amount: row['receipt_detail_amount'],
                                                       ledger_company_account: LedgerCompanyAccount.joins(:company, :ledger_detail_account).where(companies: { number: row['gl_company'] }, ledger_accounts: { number: row['gl_account'] }).first,
                                                       business_unit: BusinessUnit.where(number: row['business_unit']).first,
                                                       ledger_detail_project: LedgerDetailProject.where(project_number: row['project']).first, remark: row['receipt_detail_remark'])
        end
      end
    end

    if receipt.present? && (row.map { |_k, v| v }.uniq.compact.empty? || receipts_xlsx.last_row == (index + 1))
      receipts << receipt unless receipt.nil?
      receipt = nil
    end
  end

  yield(3, total_steps, 'Updating receipts in the database') if block_given?
  begin
    Receipt.transaction do
      receipts.each do |receipt|
        receipt.save!
        report[:edited_receipt_ids] << receipt.id
        msg = "#{Time.current}: Updated receipt id: #{receipt.id}"
        logger.info msg
        report[:successes] << msg
      end
      # redirect_to edited_receipts_path(edited_receipt_ids: receipt_ids)
    end
  rescue StandardError => e
    msg = "Unable to edit receipt, message : #{e}."
    logger.error msg
    report[:errors] << msg
  end
  yield(4, total_steps, 'Finishing...') if block_given?
  sleep 3
  report
end

.for_company_idActiveRecord::Relation<Receipt>

A relation of Receipts that are for company id. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Receipt>)

See Also:



108
# File 'app/models/receipt.rb', line 108

scope :for_company_id, ->(company_id) { where(company_id: company_id) }

.fully_appliedActiveRecord::Relation<Receipt>

A relation of Receipts that are fully applied. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Receipt>)

See Also:



107
# File 'app/models/receipt.rb', line 107

scope :fully_applied, -> { where(state: 'fully_applied') }

.states_for_selectObject



276
277
278
# File 'app/models/receipt.rb', line 276

def self.states_for_select
  Receipt.state_machines[:state].states.map { |s| [s.human_name.titleize, s.name] }.sort
end

.validate_data_create_receipts_from_xlsx(xlsx_file) ⇒ Object



361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
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
414
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
443
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
# File 'app/models/receipt.rb', line 361

def self.validate_data_create_receipts_from_xlsx(xlsx_file)
  errors = []
  new_receipt = nil
  require 'roo'
  receipts_xlsx = Roo::Spreadsheet.open(xlsx_file.to_file.to_s)
  receipt_ref = 0
  receipts_xlsx.parse(headers: true, clean: true).each_with_index do |row, index|
    next unless index >= 1

    if row.map { |_k, v| v }.uniq.compact.present?
      # get the data to validate the components of receipt
      if row['company_id'].present?
        receipt_ref = (index + 1)
        company = Company.find_by(id: row['company_id'])
        if company.nil?
          errors << "-<b> Receipt located on row #{receipt_ref}:</b> Company with ID #{row['company_id']} not found. <br>"
          new_receipt = nil # Clear stale receipt to prevent detail rows from using wrong receipt
          next
        end
         = nil
        if row['bank_account'].nil?
          errors << "-<b> Receipt located on row #{receipt_ref}:</b> Column bank_account can't be blank. <br>"
        else
           = company.
          if .nil?
             = LedgerAccount.find_by(number: row['bank_account'].to_i)
            if .nil? || .ledger_company_accounts.first.nil?
              errors << "-<b> Receipt located on row #{receipt_ref}:</b> Bank account #{row['bank_account']} not found. <br>"
            else
               = .ledger_company_accounts.first.
            end
          end
        end
        customer = nil
        if row['payor_type'].nil? || row['payor_id'].nil?
          errors << "-<b> Receipt located on row #{receipt_ref}:</b> Columns payor_type and payor_id can't be blank. <br>"
        else
          customer = Party.find_by(id: row['payor_id'].to_i)
          errors << "-<b> Receipt located on row #{receipt_ref}:</b> Customer with ID #{row['payor_id']} not found. <br>" if customer.nil?
        end
        if row['receipt_category'].nil? || row['reference'].nil? || row['currency'].nil? || row['receipt_amount'].nil? || row['receipt_date'].nil? || row['gl_date'].nil?
          errors << "-<b> Receipt located on row #{receipt_ref} (#{row['payor_type']}: #{row['payor_id']}):</b> Columns receipt_category, reference, currency, receipt_amount, receipt_date and gl_date can't be blank. <br>"
        end

        new_receipt = Receipt.new(company: company, bank_account: , customer: customer, category: row['receipt_category'], reference: row['reference'],
                                  currency: row['currency'], amount: row['receipt_amount'], gl_date: row['gl_date'], receipt_date: row['receipt_date'],
                                  card_type: row['card_type'], auth_code: row['authorization_code'])
      end

      # validate all the necesary receipt details to link to its receipt
      if new_receipt.present? && row['receipt_detail_category'].present?
        receipt_ref = (index + 1)
        valid_categories = ['Invoice', 'Credit Memo', 'GL Account']
        errors << "-<b> Receipt detail located on row #{receipt_ref}:</b> Invalid category '#{row['receipt_detail_category']}'. Must be one of: #{valid_categories.join(', ')}. <br>" unless valid_categories.include?(row['receipt_detail_category'])
        if ['Invoice', 'Credit Memo'].include?(row['receipt_detail_category']) && row['invoice_credit_memo_number'].nil?
          errors << "-<b> Receipt detail located on row #{receipt_ref}:</b> Column invoice_credit_memo_number can't be blank when an invoice or credit is present. <br>"
        end
        if row['receipt_detail_category'].present? && row['receipt_detail_amount'].nil?
          errors << "-<b> Receipt detail located on row #{receipt_ref} (#{row['invoice_credit_memo_number']}):</b> Column amount can't be blank when receipt_detail_category is present. <br>"
        end
        if row['write_off_amount'].present? && (row['write_off_amount'].to_f != 0) && row['write_off_code'].nil?
          errors << "-<b> Receipt detail located on row #{receipt_ref} (#{row['invoice_credit_memo_number']}):</b> Column write_off_code can't be blank when write_off_amount is present. <br>"
        end
        if row['receipt_detail_category'] == 'GL Account' && (row['gl_company'].nil? || row['gl_account'].nil? || row['business_unit'].nil? || row['receipt_detail_remark'].nil?)
          errors << "-<b> Receipt detail located on row #{receipt_ref}:</b> Columns gl_company, gl_account, business_unit and receipt_detail_remark can't be blank when an G/L is present. <br>"
        elsif row['receipt_detail_category'] == 'GL Account'
           = LedgerCompanyAccount.joins(:company, :ledger_detail_account).where(companies: { number: row['gl_company'] }, ledger_accounts: { number: row['gl_account'] }).first
          if .nil?
            errors << "-<b> Receipt detail located on row #{receipt_ref}:</b> No GL account found for company #{row['gl_company']} and account #{row['gl_account']}. Please verify these values exist. <br>"
          elsif new_receipt.company.present? && new_receipt.company != .company
            errors << "-<b> Receipt detail located on row #{receipt_ref} (#{row['gl_company']}.#{row['gl_account']}):</b> Receipt and GL account must belong to the same company. <br>"
          end
          business_unit = BusinessUnit.find_by(number: row['business_unit'])
          errors << "-<b> Receipt detail located on row #{receipt_ref}:</b> Business unit #{row['business_unit']} not found. <br>" if business_unit.nil?
        end

        if row['receipt_detail_category'] == 'Invoice' && row['invoice_credit_memo_number'].present?
          invoice = Invoice.find_by(reference_number: row['invoice_credit_memo_number'])
          if invoice.nil?
            errors << "-<b> Receipt detail located on row #{receipt_ref} (#{row['invoice_credit_memo_number']}):</b> Invoice not found, or reference number is not an invoice. <br>"
          else
            if new_receipt.company.present? && new_receipt.company != invoice.company
              errors << "-<b> Receipt detail located on row #{receipt_ref} (#{row['invoice_credit_memo_number']}):</b> Receipt and invoice must belong to the same company. <br>"
            end
            if new_receipt.receipt_date.present? && invoice.gl_date.present? && invoice.gl_date > new_receipt.receipt_date
              errors << "-<b> Receipt detail located on row #{receipt_ref} (#{row['invoice_credit_memo_number']}):</b> Invoice date can't be greater than receipt date. <br>"
            end
            errors << "-<b> Receipt detail located on row #{receipt_ref} (#{row['invoice_credit_memo_number']}):</b> Invoice must be under an approved status to apply the receipt. <br>" if invoice.draft?
          end

        elsif row['receipt_detail_category'] == 'Credit Memo' && row['invoice_credit_memo_number'].present?
          credit_memo = CreditMemo.find_by(reference_number: row['invoice_credit_memo_number'])
          if credit_memo.nil?
            errors << "-<b> Receipt detail located on row #{receipt_ref} (#{row['invoice_credit_memo_number']}):</b> Credit memo not found, or reference number is not a credit memo. <br>"
          else
            if new_receipt.company.present? && new_receipt.company != credit_memo.company
              errors << "-<b> Receipt detail located on row #{receipt_ref} (#{row['invoice_credit_memo_number']}):</b> Receipt and credit memo must belong to the same company. <br>"
            end
            if new_receipt.receipt_date.present? && credit_memo.gl_date.present? && credit_memo.gl_date > new_receipt.receipt_date
              errors << "-<b> Receipt detail located on row #{receipt_ref} (#{row['invoice_credit_memo_number']}):</b> Credit memo date can't be greater than receipt date. <br>"
            end
            if credit_memo.requested? || credit_memo.approved? || credit_memo.fully_offset?
              errors << "-<b> Receipt detail located on row #{receipt_ref} (#{row['invoice_credit_memo_number']}):</b> Credit memo must be under printed, processing refund or partially offset state to apply the receipt. <br>"
            end
          end
        end
      end

    end

    new_receipt = nil if errors.empty? && new_receipt.present? && (row.map { |_k, v| v }.uniq.compact.empty? || receipts_xlsx.last_row == (index + 1))
  end
  errors
end

.validate_data_edit_receipts_from_xlsx(xlsx_file) ⇒ Object



556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
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
607
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
# File 'app/models/receipt.rb', line 556

def self.validate_data_edit_receipts_from_xlsx(xlsx_file)
  errors = []
  receipt = nil
  require 'roo'
  receipts_xlsx = Roo::Spreadsheet.open(xlsx_file.to_file.to_s)
  receipt_ref = 0
  receipts_xlsx.parse(headers: true, clean: true).each_with_index do |row, index|
    next unless index >= 1

    if row.map { |_k, v| v }.uniq.compact.present?
      # update data of existing receipt
      if row['receipt_number'].present?
        receipt_ref = (index + 1)
        receipt = Receipt.find_by(id: row['receipt_number'])
        if receipt.present?
          errors << "-<b> Receipt located on row #{receipt_ref} (#{row['receipt_number']}):</b> Column new_gl_date can't be blank. <br>" if row['new_gl_date'].nil?
          remark = row['receipt_remark'] || receipt.remark
          receipt.remark = remark
          receipt.new_gl_date = row['new_gl_date']
        else
          errors << "-<b> Receipt located on row #{receipt_ref} (#{row['receipt_number']}):</b> Receipt not found. <br>"
          receipt = nil # Clear stale receipt to prevent detail rows from using wrong receipt
        end
      end

      # add new receipt details to existing receipt
      if receipt.present? && row['receipt_detail_category'].present?
        receipt_ref = (index + 1)
        valid_categories = ['Invoice', 'Credit Memo', 'GL Account']
        errors << "-<b> Receipt detail located on row #{receipt_ref}:</b> Invalid category '#{row['receipt_detail_category']}'. Must be one of: #{valid_categories.join(', ')}. <br>" unless valid_categories.include?(row['receipt_detail_category'])
        if ['Invoice', 'Credit Memo'].include?(row['receipt_detail_category']) && row['invoice_credit_memo_number'].nil?
          errors << "-<b> Receipt detail located on row #{receipt_ref}:</b> Column invoice_credit_memo_number can't be blank when an invoice or credit is present. <br>"
        end
        if row['receipt_detail_category'].present? && row['receipt_detail_amount'].nil?
          errors << "-<b> Receipt detail located on row #{receipt_ref} (#{row['invoice_credit_memo_number']}):</b> Column amount can't be blank when receipt_detail_category is present. <br>"
        end
        if row['write_off_amount'].present? && (row['write_off_amount'].to_f != 0) && row['write_off_code'].nil?
          errors << "-<b> Receipt detail located on row #{receipt_ref} (#{row['invoice_credit_memo_number']}):</b> Column write_off_code can't be blank when write_off_amount is present. <br>"
        end
        if row['receipt_detail_category'] == 'GL Account' && (row['gl_company'].nil? || row['gl_account'].nil? || row['business_unit'].nil? || row['receipt_detail_remark'].nil?)
          errors << "-<b> Receipt detail located on row #{receipt_ref}:</b> Columns gl_company, gl_account, business_unit and receipt_detail_remark can't be blank when an G/L is present. <br>"
        elsif row['receipt_detail_category'] == 'GL Account'
           = LedgerCompanyAccount.joins(:company, :ledger_detail_account).where(companies: { number: row['gl_company'] }, ledger_accounts: { number: row['gl_account'] }).first
          if .nil?
            errors << "-<b> Receipt detail located on row #{receipt_ref}:</b> No GL account found for company #{row['gl_company']} and account #{row['gl_account']}. Please verify these values exist. <br>"
          elsif receipt.company.present? && receipt.company != .company
            errors << "-<b> Receipt detail located on row #{receipt_ref} (#{row['gl_company']}.#{row['gl_account']}):</b> Receipt and GL account must belong to the same company. <br>"
          end
          business_unit = BusinessUnit.find_by(number: row['business_unit'])
          errors << "-<b> Receipt detail located on row #{receipt_ref}:</b> Business unit #{row['business_unit']} not found. <br>" if business_unit.nil?
        end

        if row['receipt_detail_category'] == 'Invoice' && row['invoice_credit_memo_number'].present?
          invoice = Invoice.find_by(reference_number: row['invoice_credit_memo_number'])
          if invoice.nil?
            errors << "-<b> Receipt detail located on row #{receipt_ref} (#{row['invoice_credit_memo_number']}):</b> Invoice not found, or reference number is not an invoice. <br>"
          else
            errors << "-<b> Receipt detail located on row #{receipt_ref} (#{row['invoice_credit_memo_number']}):</b> Receipt and invoice must belong to the same company. <br>" if receipt.company.present? && receipt.company != invoice.company
            if receipt.new_gl_date.present? && invoice.gl_date.present? && invoice.gl_date > receipt.new_gl_date
              errors << "-<b> Receipt detail located on row #{receipt_ref} (#{row['invoice_credit_memo_number']}):</b> Invoice date can't be greater than receipt date. <br>"
            end
            errors << "-<b> Receipt detail located on row #{receipt_ref} (#{row['invoice_credit_memo_number']}):</b> Invoice must be under an approved status to apply the receipt. <br>" if invoice.draft?
          end
        elsif row['receipt_detail_category'] == 'Credit Memo' && row['invoice_credit_memo_number'].present?
          credit_memo = CreditMemo.find_by(reference_number: row['invoice_credit_memo_number'])
          if credit_memo.nil?
            errors << "-<b> Receipt detail located on row #{receipt_ref} (#{row['invoice_credit_memo_number']}):</b> Credit memo not found, or reference number is not a credit memo. <br>"
          else
            if receipt.company.present? && receipt.company != credit_memo.company
              errors << "-<b> Receipt detail located on row #{receipt_ref} (#{row['invoice_credit_memo_number']}):</b> Receipt and credit memo must belong to the same company. <br>"
            end
            if receipt.new_gl_date.present? && credit_memo.gl_date.present? && credit_memo.gl_date > receipt.new_gl_date
              errors << "-<b> Receipt detail located on row #{receipt_ref} (#{row['invoice_credit_memo_number']}):</b> Credit memo date can't be greater than receipt date. <br>"
            end
            if credit_memo.requested? || credit_memo.approved? || credit_memo.fully_offset?
              errors << "-<b> Receipt detail located on row #{receipt_ref} (#{row['invoice_credit_memo_number']}):</b> Credit memo must be under printed, processing refund or partially offset state to apply the receipt. <br>"
            end
          end
        end
      end
    end

    receipt = nil if errors.empty? && receipt.present? && (row.map { |_k, v| v }.uniq.compact.empty? || receipts_xlsx.last_row == (index + 1))
  end
  errors
end

Instance Method Details

#authorization_codeObject



206
207
208
# File 'app/models/receipt.rb', line 206

def authorization_code
  payment.try(:authorization_code)
end

#bank_accountBankAccount



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

belongs_to :bank_account, optional: true

#billing_addressObject



180
181
182
# File 'app/models/receipt.rb', line 180

def billing_address
  customer&.billing_address
end

#card_nameObject



293
294
295
# File 'app/models/receipt.rb', line 293

def card_name
  payment.nil? ? nil : payment.name
end

#category_for_authorization_typeObject



214
215
216
217
218
219
220
# File 'app/models/receipt.rb', line 214

def category_for_authorization_type
  if category == 'Credit Card'
    'credit_card'
  elsif category == 'Echeck'
    'check'
  end
end

#communicationsActiveRecord::Relation<Communication>

Returns:

See Also:



68
# File 'app/models/receipt.rb', line 68

has_many :communications, -> { order(:id).reverse_order }, as: :resource, dependent: :nullify, inverse_of: :resource

#companyCompany

Returns:

See Also:



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

belongs_to :company

#create_receipt_details(invoice, amount) ⇒ Object



257
258
259
260
261
262
263
264
265
266
# File 'app/models/receipt.rb', line 257

def create_receipt_details(invoice, amount)
  effective_gl_date = [gl_date, invoice&.gl_date].compact.max
  self.new_gl_date = effective_gl_date
  receipt_details << ReceiptDetail.new(category: 'Invoice', invoice: invoice, amount: amount, gl_date: effective_gl_date)
  begin
    save!
  rescue StandardError => e
    ErrorReporting.error e, receipt_id: id, message: "Receipt details not created correctly. Receipt ID #{id}"
  end
end

#credit_applied_to_summaryObject



145
146
147
148
149
150
151
152
153
154
# File 'app/models/receipt.rb', line 145

def credit_applied_to_summary
  list = []
  if receipt_details.non_voided.invoices.empty? && receipt_details.non_voided.gl_accounts.empty?
    "Payment issued by #{category} #{reference_summary}"
  else
    receipt_details.non_voided.invoices.each { |rd| list << "Invoice #{rd.invoice.reference_number}" }
    receipt_details.non_voided.gl_accounts.each { |_rd| list << 'GL Account' }
    "Credit applied to #{list.join('/')}"
  end
end

#credit_card?Boolean

Returns:

  • (Boolean)


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

def credit_card?
  category == 'Credit Card'
end

#currency_symbolObject



312
313
314
# File 'app/models/receipt.rb', line 312

def currency_symbol
  Money::Currency.new(currency).symbol
end

#customerParty

Returns:

See Also:



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

belongs_to :customer, class_name: 'Party', inverse_of: :receipts, optional: true

#default_cc_options(payment) ⇒ Object



325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
# File 'app/models/receipt.rb', line 325

def default_cc_options(payment)
  options = {
    description: payment_description(payment) || 'Receipt',
    statement_description: 'WarmlyYours',
    currency: payment.currency,
    metadata: {
      ip: payment.remote_ip_address,
      email: payment.email,
      receipt_id: id,
      payment_id: payment.id
    }
  }
  # only add the customer id if we're charging a stored card, otherwise it will automatically charge the first card on the customer's account
  # and besides ActiveMerchant .purchase will not work right with a payment token
  options[:customer] = customer.stripe_customer_id if payment.vault_id.present?
  options
end

#determine_emailObject



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

def determine_email
  if (payment&.order&.order_reception_type == 'Online') && notification_emails.any?
    notification_emails.join(',')
  else
    email
  end
end

#echeck?Boolean

Returns:

  • (Boolean)


226
227
228
# File 'app/models/receipt.rb', line 226

def echeck?
  category == 'Echeck'
end

#edi_communication_logsActiveRecord::Relation<EdiCommunicationLog>

Returns:

See Also:



70
# File 'app/models/receipt.rb', line 70

has_many :edi_communication_logs, through: :edi_documents

#edi_documentsActiveRecord::Relation<EdiDocument>

Returns:

See Also:



69
# File 'app/models/receipt.rb', line 69

has_many :edi_documents, dependent: :destroy

#evaluate_taxjar_submissionObject



141
142
143
# File 'app/models/receipt.rb', line 141

def evaluate_taxjar_submission
  receipt_details.non_voided.gl_accounts.each(&:evaluate_taxjar_submission)
end

#first_documentObject



200
201
202
203
204
# File 'app/models/receipt.rb', line 200

def first_document
  return nil if receipt_details.empty?

  receipt_details.first.invoice || receipt_details.first.credit_memo
end

#funds_assignedObject



297
298
299
300
301
302
# File 'app/models/receipt.rb', line 297

def funds_assigned
  assigned = BigDecimal(0)
  receipt_details.each { |rd| assigned += rd.amount unless rd.amount.nil? }
  outgoing_payment_items.applied.each { |pi| assigned += pi.amount }
  assigned
end

#has_linked_payment_items?Boolean

Returns:

  • (Boolean)


268
269
270
# File 'app/models/receipt.rb', line 268

def has_linked_payment_items?
  outgoing_payment_items.not_voided.any?
end

#hold_for_review?Boolean

Returns:

  • (Boolean)


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

def hold_for_review?
  hold_for_review
end

#is_not_for_a_customer?Boolean

Returns:

  • (Boolean)


316
317
318
# File 'app/models/receipt.rb', line 316

def is_not_for_a_customer?
  customer.present? and customer.class != Customer
end

#ledger_transactionsActiveRecord::Relation<LedgerTransaction>

Returns:

See Also:



66
# File 'app/models/receipt.rb', line 66

has_many :ledger_transactions, dependent: :destroy

#no_funds_applied?Boolean

Returns:

  • (Boolean)


304
305
306
# File 'app/models/receipt.rb', line 304

def no_funds_applied?
  funds_assigned == 0
end

#notification_emailsObject



184
185
186
# File 'app/models/receipt.rb', line 184

def notification_emails
  customer.all_notification_channels.email.invoices.distinct.pluck(ContactPoint[:detail])
end

#only_remarks_changed?Boolean

Returns:

  • (Boolean)


156
157
158
159
# File 'app/models/receipt.rb', line 156

def only_remarks_changed?
  all_changes = changes.keys.concat(receipt_details.collect { |rd| rd.changes.keys }).flatten
  all_changes.any? and all_changes.all? { |attr| attr == 'remark' }
end

#outgoing_payment_itemsActiveRecord::Relation<OutgoingPaymentItem>

Returns:

See Also:



67
# File 'app/models/receipt.rb', line 67

has_many :outgoing_payment_items, inverse_of: :receipt, dependent: :destroy

#paymentPayment

Returns:

See Also:



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

belongs_to :payment, optional: true

#payment_description(_auth) ⇒ Object



320
321
322
323
# File 'app/models/receipt.rb', line 320

def payment_description(_auth)
  desc = receipt_details.collect { |rd| rd.description }.compact.join(' / ')
  desc.presence
end

#primary_transmission_contactObject



188
189
190
191
192
# File 'app/models/receipt.rb', line 188

def primary_transmission_contact
  return nil if first_document.nil?

  first_document.primary_transmission_contact
end

#receipt_detailsActiveRecord::Relation<ReceiptDetail>

Returns:

See Also:



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

has_many :receipt_details, inverse_of: :receipt, dependent: :destroy

#reference_summary(with_brackets = false) ⇒ Object



280
281
282
283
284
285
286
287
288
289
290
291
# File 'app/models/receipt.rb', line 280

def reference_summary(with_brackets = false)
  ref = if category == 'Credit Card'
          card_type.blank? && reference.blank? ? nil : "#{card_type} #{reference}"
        else
          reference.presence
        end
  if ref.nil?
    nil
  else
    with_brackets == true ? "(#{ref})" : ref
  end
end

#regenerate_invoice_pdfObject



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

def regenerate_invoice_pdf
  receipt_details.where.not(invoice_id: nil).each { |rd| InvoicePdfGenerationWorker.perform_async(rd.invoice_id) }
end

#send_emailObject



161
162
163
164
165
166
167
168
169
170
# File 'app/models/receipt.rb', line 161

def send_email
  sender = customer.try(:primary_sales_rep)
  CommunicationBuilder.new(
    resource: self,
    sender_party: sender,
    sender: (sender.nil? ? INFO_EMAIL : nil),
    emails: determine_email,
    bcc: (sender.nil? ? nil : sender.email)
  ).create
end

#storeObject



347
348
349
350
351
# File 'app/models/receipt.rb', line 347

def store
  company.stores.first
rescue StandardError
  nil
end

#to_sObject



343
344
345
# File 'app/models/receipt.rb', line 343

def to_s
  "Receipt #{id}"
end

#transmission_contact_pointsObject



194
195
196
197
198
# File 'app/models/receipt.rb', line 194

def transmission_contact_points
  return [] if first_document.nil?

  first_document.transmission_contact_points
end

#unapplied_fundsObject



308
309
310
# File 'app/models/receipt.rb', line 308

def unapplied_funds
  amount - funds_assigned
end

#void_receipt(reversal_dates, use_original_date: false, use_specific_date: nil) ⇒ Object

reversal_dates: a hash of transaction id containing date to use
use_original_date: use the ledger transaction original date
use_specific_date: use a specific date



233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
# File 'app/models/receipt.rb', line 233

def void_receipt(reversal_dates, use_original_date: false, use_specific_date: nil)
  Receipt.transaction do
    # void the receipt details
    receipt_details.each(&:void!)

    # reverse the ledger transactions
    ledger_transactions.each do |lt|
      date_to_use = use_specific_date
      date_to_use ||= lt.transaction_date if use_original_date
      date_to_use ||= reversal_dates[lt.id.to_s]
      lt.reverse(date_to_use)
    end

    # unoffset the credit memos and unpay the receipts
    receipt_details.each do |rd|
      rd.credit_memo.presence&.unoffset!
      rd.invoice.presence&.unpay
    end

    # void the receipt
    trigger_void!
  end
end