Class: CreditMemo

Overview

== Schema Information

Table name: credit_memos
Database name: primary

id :integer not null, primary key
category :string(255)
currency :string(255)
discount :decimal(8, 2)
document_date :date
gl_date :date
line_offset :decimal(8, 2)
line_total :decimal(10, 2)
line_total_discounted :decimal(8, 2)
old_line_total :decimal(8, 2)
old_tax_total :decimal(8, 2)
old_total :decimal(8, 2)
order_type :string(255)
original_order_missing_reason :string(255)
original_po_number :string(255)
reference_number :string(255) not null
remark :text
report_grouping :string
request_date :date
shipping_cost :decimal(8, 2)
shipping_coupon :decimal(8, 2)
state :string(255)
tax_date :date
tax_exempt :boolean
tax_offset :decimal(8, 2)
tax_total :decimal(8, 2)
taxable_total :decimal(12, 2)
taxjar_state :string
total :decimal(10, 2)
transmission_state :string(255)
uploads_count :integer
created_at :datetime
updated_at :datetime
account_specialist_id :integer
billing_address_id :integer
billing_customer_id :integer
company_id :integer
creator_id :integer
credit_order_id :integer
customer_id :integer
local_sales_rep_id :integer
original_invoice_id :integer
original_order_id :integer
primary_sales_rep_id :integer
purchase_order_id :integer
resource_tax_rate_id :integer
rma_id :integer
secondary_sales_rep_id :integer
shipping_address_id :integer
support_case_id :integer
updater_id :integer

Indexes

idx_billing_address_id (billing_address_id)
idx_company_id_customer_id_gl_date (company_id,customer_id,gl_date)
idx_credit_order_id (credit_order_id)
idx_customer_id_state (customer_id,state)
idx_original_order_id (original_order_id)
idx_rma_id (rma_id)
idx_shipping_address_id (shipping_address_id)
index_credit_memos_on_billing_customer_id (billing_customer_id)
index_credit_memos_on_local_sales_rep_id (local_sales_rep_id)
index_credit_memos_on_original_invoice_id (original_invoice_id)
index_credit_memos_on_reference_number (reference_number) UNIQUE
index_credit_memos_on_report_grouping (report_grouping)
index_credit_memos_on_resource_tax_rate_id (resource_tax_rate_id)
index_credit_memos_on_state_and_transmission_state (state,transmission_state)
index_credit_memos_on_support_case_id (support_case_id)

Foreign Keys

fk_credit_memos_resource_tax_rates (resource_tax_rate_id => resource_tax_rates.id) ON DELETE => cascade
fk_rails_... (billing_address_id => addresses.id)
fk_rails_... (billing_customer_id => parties.id)
fk_rails_... (customer_id => parties.id)
fk_rails_... (local_sales_rep_id => parties.id)
fk_rails_... (shipping_address_id => addresses.id)
fk_rails_... (support_case_id => support_cases.id)

Defined Under Namespace

Classes: PdfGenerator, SubmitToTaxjar

Constant Summary collapse

REFERENCE_NUMBER_PATTERN =

Regex matching the canonical CM… reference-number format (case-insensitive).

/^CM\d+/i
CATEGORIES =

Allowed categories: rma (issued against an existing return / RMA) or
standalone (issued without an originating invoice — warranties, rebates).

%w[rma standalone]
LINE_ITEM_CATEGORIES =

Mapping from CRM-facing line-item category labels to the JDE return /
warranty GL account they post against and the business unit on the
offset side. Drives the "Add line item" dropdown on credit memos and
the GL split when Models::AccountingDocumentTransmittable transmits.

[{ name: 'Warranty',       account_number: WARRANTY_CLAIMS_ACCOUNT, business_unit: 'default' },
{ name: 'Rebate',         account_number: REBATES_STANDALONE_ACCOUNT, business_unit: 'default' },
{ name: 'Lead Protection', account_number: RETURN_COUPONS_ACCOUNT, business_unit: 'default' },
{ name: 'Trade Discount', account_number: RETURN_COUPONS_ACCOUNT,  business_unit: 'default' },
{ name: 'Coupon (Goods)', account_number: RETURN_COUPONS_ACCOUNT,  business_unit: 'default' },
{ name: 'Coupon (Freight)', account_number: RETURN_FREIGHT_COUPONS_ACCOUNT,  business_unit: 'sales' },
{ name: 'Freight',        account_number: RETURN_FREIGHT_COUPONS_ACCOUNT,    business_unit: 'sales' },
{ name: 'Misc',           account_number: RETURN_COUPONS_ACCOUNT,  business_unit: 'default' },
{ name: 'Item',           account_number: CUSTOMER_RETURNS,        business_unit: 'default' }]

Constants included from Models::Auditable

Models::Auditable::ALWAYS_IGNORED

Constants included from Schedulable

Schedulable::SIMPLE_FORM_OPTIONS

Instance Attribute Summary collapse

Attributes included from Models::Itemizable

#force_total_reset, #total_reset

Belongs to collapse

Methods included from Models::SupportCaseLinkable

#support_case

Methods included from Models::Auditable

#creator, #updater

Methods included from Models::TaxableResource

#resource_tax_rate

Methods included from Models::Itemizable

#account_specialist, #local_sales_rep, #primary_sales_rep, #secondary_sales_rep

Has one collapse

Has many collapse

Methods included from Models::Itemizable

#coupons, #discounts

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Models::TaxjarSubmittable

#customer_sync_instance, #delete_from_taxjar, #evaluate_taxjar_submission, #record_already_exists_on_taxjar?, #resubmit_to_taxjar, #should_be_submitted_to_taxjar?, #should_sync_customer_with_taxjar?, #submit_to_taxjar, #sync_customer_with_taxjar, #taxjar_customer_id, #taxjar_submission_instance

Methods included from Models::SupportCaseLinkable

#support_case_ref, #support_case_ref=

Methods included from Models::AccountingDocumentTransmittable

#can_be_transmitted?, #fallback_notification_channel_type, #notification_channel_sort_order, #notification_channel_types, #notification_channels, #own_notification_channel_type, #post_communication_exception_hook, #post_communication_sent_hook, #primary_transmission_contact, #primary_transmission_contact_point_id, #transmission_contact_points

Methods included from Models::Auditable

#all_skipped_columns, #audit_reference_data, #should_not_save_version, #stamp_record

Methods included from Models::TaxableResource

#apply_tax_rate_to_line_items, #build_tax_params, #calculate_tax_for_all_lines, #copy_tax_rate, #effective_date, #get_rates_for_line, #get_tax_rate, #manual_rate_goods, #manual_rate_services, #manual_rate_shipping, #origin_address, #refresh_tax_rate, #resource_not_taxable?, #set_initial_tax_rate, #should_refresh_tax_rate?, #state_code, #state_code_sym, #taxes_grouped_by_rate, #taxes_grouped_by_type

Methods included from Models::Itemizable

#add_line_item, #additional_items, #assign_sequence, #billing_entity, #breakdown_of_prices, #calculate_actual_insured_value, #calculate_discounts, #calculate_shipping_cost, #coupon_search, #customer_applied_coupons, #customer_can_apply_coupon?, #discounts_changed?, #discounts_grouped_by_coupon, #discounts_subtotal, #effective_discount, #effective_shipping_discount, #has_kits?, #has_kits_or_serial_numbers?, #has_serial_numbers?, #is_credit_order?, #line_items_requiring_serial_number, #line_items_with_counters, #line_total_plus_tax, #main_rep, #perform_db_total, #purge_empty_quoting_deliveries, #purge_shipping_when_no_other_lines, #remove_line_item, #require_total_reset?, #reset_discount, #set_for_recalc, #set_signature_confirmation_on_shipping_address_change, #set_totals, #shipping_conditions_changed?, #shipping_discounted, #shipping_method_changed?, #should_recalculate_shipping?, #smartinstall_data, #smartsupport_data, #store, #subtotal_cogs, #sync_shipping_line, #total_cogs

Methods inherited from ApplicationRecord

ransackable_associations, ransackable_attributes, ransackable_scopes, ransortable_attributes, #to_relation

Methods included from Schedulable

config

Methods included from Models::AfterCommittable

#after_commit

Methods included from Models::EventPublishable

#publish_event

Instance Attribute Details

#billing_address_idObject (readonly)



143
# File 'app/models/credit_memo.rb', line 143

validates :category, :customer_id, :company_id, :billing_address_id, :shipping_address_id, :request_date, :tax_date, :document_date, :gl_date, :order_type, presence: true

#categoryObject (readonly)



143
# File 'app/models/credit_memo.rb', line 143

validates :category, :customer_id, :company_id, :billing_address_id, :shipping_address_id, :request_date, :tax_date, :document_date, :gl_date, :order_type, presence: true

#company_idObject (readonly)



143
# File 'app/models/credit_memo.rb', line 143

validates :category, :customer_id, :company_id, :billing_address_id, :shipping_address_id, :request_date, :tax_date, :document_date, :gl_date, :order_type, presence: true

#credit_order_idObject (readonly)



144
# File 'app/models/credit_memo.rb', line 144

validates :credit_order_id, presence: { if: proc { |cm| cm.category == 'rma' } }

#customer_companyObject

Returns the value of attribute customer_company.



171
172
173
# File 'app/models/credit_memo.rb', line 171

def customer_company
  @customer_company
end

#customer_idObject (readonly)



143
# File 'app/models/credit_memo.rb', line 143

validates :category, :customer_id, :company_id, :billing_address_id, :shipping_address_id, :request_date, :tax_date, :document_date, :gl_date, :order_type, presence: true

#customer_nameObject

Returns the value of attribute customer_name.



171
172
173
# File 'app/models/credit_memo.rb', line 171

def customer_name
  @customer_name
end

#do_not_set_totalsObject

Returns the value of attribute do_not_set_totals.



171
172
173
# File 'app/models/credit_memo.rb', line 171

def do_not_set_totals
  @do_not_set_totals
end

#document_dateObject (readonly)



143
# File 'app/models/credit_memo.rb', line 143

validates :category, :customer_id, :company_id, :billing_address_id, :shipping_address_id, :request_date, :tax_date, :document_date, :gl_date, :order_type, presence: true

#gl_dateObject (readonly)



143
# File 'app/models/credit_memo.rb', line 143

validates :category, :customer_id, :company_id, :billing_address_id, :shipping_address_id, :request_date, :tax_date, :document_date, :gl_date, :order_type, presence: true

#order_typeObject (readonly)



143
# File 'app/models/credit_memo.rb', line 143

validates :category, :customer_id, :company_id, :billing_address_id, :shipping_address_id, :request_date, :tax_date, :document_date, :gl_date, :order_type, presence: true

#original_invoice_idObject (readonly)



147
# File 'app/models/credit_memo.rb', line 147

validates :original_invoice_id, numericality: { allow_nil: true }

#original_invoice_refObject

Returns the value of attribute original_invoice_ref.



171
172
173
# File 'app/models/credit_memo.rb', line 171

def original_invoice_ref
  @original_invoice_ref
end

#original_order_idObject (readonly)



146
# File 'app/models/credit_memo.rb', line 146

validates :original_order_id, numericality: { allow_nil: true }

#original_order_missing_reasonObject (readonly)



148
# File 'app/models/credit_memo.rb', line 148

validates :original_order_missing_reason, presence: { if: proc { |cm| cm.original_invoice_id.nil? && !cm.fully_offset? } }

#original_order_refObject

Returns the value of attribute original_order_ref.



171
172
173
# File 'app/models/credit_memo.rb', line 171

def original_order_ref
  @original_order_ref
end

#request_dateObject (readonly)



143
# File 'app/models/credit_memo.rb', line 143

validates :category, :customer_id, :company_id, :billing_address_id, :shipping_address_id, :request_date, :tax_date, :document_date, :gl_date, :order_type, presence: true

#shipping_address_idObject (readonly)



143
# File 'app/models/credit_memo.rb', line 143

validates :category, :customer_id, :company_id, :billing_address_id, :shipping_address_id, :request_date, :tax_date, :document_date, :gl_date, :order_type, presence: true

#tax_dateObject (readonly)



143
# File 'app/models/credit_memo.rb', line 143

validates :category, :customer_id, :company_id, :billing_address_id, :shipping_address_id, :request_date, :tax_date, :document_date, :gl_date, :order_type, presence: true

Class Method Details

.available_to_applyActiveRecord::Relation<CreditMemo>

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

Returns:

See Also:



167
# File 'app/models/credit_memo.rb', line 167

scope :available_to_apply, -> { where(state: %w[printed partially_offset]) }

.credit_memo_count(company_id = nil, where_conditions = nil, where_not_conditions = nil) ⇒ Integer

Count credit memos optionally scoped to a company plus arbitrary
where/where.not clauses. Used by dashboard widgets that show
"open RMAs" / "fully offset this month" tallies without needing to
re-implement the filter chain.

Parameters:

  • company_id (Integer, nil) (defaults to: nil)
  • where_conditions (Hash, String, nil) (defaults to: nil)
  • where_not_conditions (Hash, String, nil) (defaults to: nil)

Returns:

  • (Integer)


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

def self.credit_memo_count(company_id = nil, where_conditions = nil, where_not_conditions = nil)
  c = CreditMemo.order('id')
  c = c.where(company_id: company_id) unless company_id.nil?
  c = c.where(where_conditions) unless where_conditions.nil?
  c = c.where.not(where_not_conditions) unless where_not_conditions.nil?
  c.count
end

.for_company_idActiveRecord::Relation<CreditMemo>

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

Returns:

See Also:



169
# File 'app/models/credit_memo.rb', line 169

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

.new_credit_memo_from_order(order) ⇒ void

This method returns an undefined value.

Builds a complete RMA-category credit memo from a credit-Order
copies the line items, applies the source order's tax (preserving
marketplace-set rates from CI invoices), copies discounts and
line-discount allocations, then transitions the memo through
approve! and printed! to post the ledger transactions. Wrapped
in a single transaction so a partial failure rolls back. Also
re-points refundable Payments to the new memo so refunds can
process against original authorisations.

Parameters:

  • order (Order)

    the credit-RMA order



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
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
642
643
644
# File 'app/models/credit_memo.rb', line 519

def self.new_credit_memo_from_order(order)
  CreditMemo.transaction do
    memo = CreditMemo.new
    memo.category = 'rma'
    memo.credit_order = order
    if order.rma.original_invoice.present?
      memo.original_invoice = order.rma.original_invoice
      memo.report_grouping = order.rma.original_invoice.report_grouping
      memo.original_order = order.rma.original_order if order.rma.original_order.present?
      memo.order_type = order.order_type
    else
      memo.original_order_missing_reason = 'Not provided on RMA'
      memo.order_type = Order::SALES_ORDER
    end
    invoice = memo.original_invoice.nil? ? nil : memo.original_invoice
    memo.rma = order.rma
    memo.billing_address = order.billing_address
    memo.shipping_address = order.shipping_address
    unless invoice.nil?
      memo.primary_sales_rep = invoice.primary_sales_rep
      memo.secondary_sales_rep = invoice.secondary_sales_rep
      memo.local_sales_rep = invoice.local_sales_rep
    end
    # memo.account_specialist = order.account_specialist
    memo.currency = order.currency
    memo.tax_exempt = order.tax_exempt

    # Build a map between source order line items and their credit memo copies
    # to later reassign line_discounts properly
    line_item_map = {}
    from_ci_invoice = order.ci_invoice_credit_order?
    order.line_items.each do |li|
      copy = li.deep_dup
      # Clear duplicated line_discounts - they reference order's discount records
      # We'll recreate them after creating credit memo's discount records
      copy.line_discounts.clear
      copy.resource = nil
      copy.credit_rma_item_id = nil
      copy.delivery_id = nil
      if from_ci_invoice
        # Inherit the exact per-line tax from the source credit order. The
        # line item's `before_create :set_initial_tax_rate` will still
        # reassign tax_rate / tax_type / resource_tax_rate_id to the credit
        # memo's own (matching) rate; `do_not_calculate_tax = true` prevents
        # tax_total from being recomputed from rate × discounted_price, which
        # would drop marketplace-set (CI invoice) amounts that do not align
        # with our rate.
        copy.tax_rate = li.tax_rate
        copy.tax_type = li.tax_type
        copy.resource_tax_rate_id = li.resource_tax_rate_id
        copy.tax_total = li.tax_total
        copy.do_not_calculate_tax = true
      else
        copy.tax_rate = nil
        copy.tax_type = nil
        copy.resource_tax_rate_id = nil
        copy.tax_total = 0
      end
      copy.cm_category = (li.is_shipping? ? 'Freight' : 'Item')
      copy.credit_order_line_item_id = li.id
      memo.line_items << copy
      line_item_map[li.id] = copy
    end
    memo.tax_total = order.tax_total
    memo.line_total = order.line_total
    memo.total = order.total
    memo.discount = order.line_items.to_a.sum(&:coupon_amount)
    memo.shipping_cost = 0
    memo.customer = order.customer
    memo.company = order.customer.store.company
    memo.request_date = order.rma.created_at.to_datetime.to_date
    memo.tax_date = order.tax_date || order.shipped_date || Date.current
    memo.gl_date = Date.current
    memo.document_date = Date.current
    memo.original_po_number = order.rma.original_invoice.po_number if order.rma.original_invoice.present?
    memo.save!

    # Create discount records for the credit memo and build a map
    # from original discount_id => new credit memo discount_id
    discount_map = {}
    order.discounts.each do |discount|
      copy_discount = discount.dup
      copy_discount.itemizable = memo
      copy_discount.save!
      discount_map[discount.id] = copy_discount.id
    end

    # Recreate line_discounts on the copied credit memo line items with valid
    # credit memo-scoped discount_ids. If a referenced discount is missing on the
    # order (data drift), fall back to finding/creating a credit memo-level
    # discount by coupon_id to preserve amounts and avoid FK violations.
    line_item_map.each do |original_li_id, memo_li|
      original_li = order.line_items.find { |li| li.id == original_li_id }
      next unless original_li

      original_li.line_discounts.each do |ld|
        new_discount_id = discount_map[ld.discount_id]
        if new_discount_id.blank?
          # Fallback: find or create discount on credit memo by coupon_id
          memo_discount = memo.discounts.find_by(coupon_id: ld.coupon_id)
          memo_discount ||= Discount.create!(itemizable: memo, coupon_id: ld.coupon_id, amount: 0)
          new_discount_id = memo_discount.id
        end
        memo_li.line_discounts.create!(amount: ld.amount,
                                       coupon_id: ld.coupon_id,
                                       discount_id: new_discount_id)
      end
      memo_li.save!
    end

    # Recalculate discount total from line discounts
    memo.discount = memo.line_items.to_a.sum(&:coupon_amount)
    memo.approve!
    memo.printed!

    order.line_items.collect do |li|
      li.credit_rma_item.returned_line_item.resource
    rescue StandardError
      nil
    end.compact.uniq.each do |oo|
      oo.payments.each do |pp|
        pp.update(credit_memo_id: memo.id) if pp.authorized?
      end
    end
  end
end

.printedActiveRecord::Relation<CreditMemo>

A relation of CreditMemos that are printed. Active Record Scope

Returns:

See Also:



168
# File 'app/models/credit_memo.rb', line 168

scope :printed, -> { where(state: %w[printed]) }

.states_for_selectArray<Array(String, Symbol)>

State-machine states formatted for a Rails select helper —
[[human_name, machine_value], …], sorted alphabetically. Used by
the CRM credit-memo filter dropdown.

Returns:

  • (Array<Array(String, Symbol)>)


235
236
237
# File 'app/models/credit_memo.rb', line 235

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

Instance Method Details

#activitiesActiveRecord::Relation<Activity>

Returns:

See Also:



134
# File 'app/models/credit_memo.rb', line 134

has_many   :activities, as: :resource, dependent: :nullify

#available_to_refundBigDecimal

Dollar amount that can still be refunded back to the original
payment instrument(s) — sum of available_to_refund across the
original invoice's refundable Payments. Returns 0 when the RMA
specifies a non-original refund method (e.g. check, store credit).

Returns:

  • (BigDecimal)


383
384
385
386
387
388
389
390
391
392
393
# File 'app/models/credit_memo.rb', line 383

def available_to_refund
  if original_invoice.nil? or (rma.present? and rma.payment_method != 'Original Payment Method')
    available = BigDecimal('0.00')
  else
    available = BigDecimal('0.00')
    original_invoice.payments.can_be_refunded.each do |payment|
      available += payment.available_to_refund
    end
  end
  available
end

#balanceBigDecimal

Outstanding balance — credit-memo total (a negative amount) minus
what's already been offset/refunded. Reaches zero once fully offset.

Returns:

  • (BigDecimal)


345
346
347
# File 'app/models/credit_memo.rb', line 345

def balance
  total - funds_offset
end

#billing_addressAddress

Returns:

See Also:



126
# File 'app/models/credit_memo.rb', line 126

belongs_to :billing_address, class_name: 'Address', optional: true

#billing_customerCustomer

Returns:

See Also:



124
# File 'app/models/credit_memo.rb', line 124

belongs_to :billing_customer, class_name: 'Customer', inverse_of: :billing_credit_memos, optional: true

#build_activityActivity

Builds (but does not save) a new Activity attached to this credit
memo with the customer party pre-populated.

Returns:



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

def build_activity
  activities.new(resource: self, party: customer)
end

#can_be_refunded?Boolean

Returns:

  • (Boolean)


337
338
339
# File 'app/models/credit_memo.rb', line 337

def can_be_refunded?
  (printed? or partially_offset?) and available_to_refund > 0
end

#communicationsActiveRecord::Relation<Communication>

Returns:

See Also:



141
# File 'app/models/credit_memo.rb', line 141

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

#companyCompany

Returns:

See Also:



125
# File 'app/models/credit_memo.rb', line 125

belongs_to :company

#credit_orderOrder

Returns:

See Also:



119
# File 'app/models/credit_memo.rb', line 119

belongs_to :credit_order, class_name: 'Order', optional: true

CRM URL for the credit-memo show page.

Returns:

  • (String)


460
461
462
# File 'app/models/credit_memo.rb', line 460

def crm_link
  UrlHelper.instance.credit_memo_path(self)
end

#currency_symbolString

Currency symbol (e.g. $, ) for this credit memo's #currency.

Returns:

  • (String)


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

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

#customerCustomer

Returns:

See Also:



123
# File 'app/models/credit_memo.rb', line 123

belongs_to :customer, inverse_of: :credit_memos

#disable_auto_coupontrue

Models::Itemizable hook — credit memos never auto-apply customer
coupons (a credit is the customer being made whole, not a new sale).

Returns:

  • (true)


659
660
661
# File 'app/models/credit_memo.rb', line 659

def disable_auto_coupon
  true
end

#do_not_detect_shippingtrue

Models::Itemizable hook — credit memos never re-detect shipping
automatically (the credit is for the original shipping cost, not a
newly-quoted rate).

Returns:

  • (true)


651
652
653
# File 'app/models/credit_memo.rb', line 651

def do_not_detect_shipping
  true
end

#editing_locked?Boolean

Returns:

  • (Boolean)


311
312
313
# File 'app/models/credit_memo.rb', line 311

def editing_locked?
  credit_order.present? or printed? or partially_offset? or fully_offset? or processing_refund?
end

#file_nameString

Filename used when attaching the credit-memo PDF to email or storing in S3.

Returns:

  • (String)


467
468
469
# File 'app/models/credit_memo.rb', line 467

def file_name
  "CreditMemo_#{reference_number}.pdf"
end

#funds_fully_offset?Boolean

Returns:

  • (Boolean)


315
316
317
# File 'app/models/credit_memo.rb', line 315

def funds_fully_offset?
  funds_offset == total
end

#funds_offsetBigDecimal

Net dollars already offset against this credit memo — sum of applied
ReceiptDetails (credit applied to invoices, write-offs) minus any
outgoing payments (refund checks). Drives the
funds_fully_offset? / funds_partially_offset? state-machine guards.

Returns:

  • (BigDecimal)


333
334
335
# File 'app/models/credit_memo.rb', line 333

def funds_offset
  receipt_details.non_voided.sum(:amount) - outgoing_payment_items.applied.sum(:amount)
end

#funds_partially_offset?Boolean

Returns:

  • (Boolean)


319
320
321
# File 'app/models/credit_memo.rb', line 319

def funds_partially_offset?
  !funds_offset.zero? && !(total - funds_offset).zero?
end

#generate_pdfUpload

Renders a fresh credit-memo PDF via PdfGenerator,
writes it to temp storage, uploads it to S3 under
credit_memo_pdf and attaches the Upload.

Returns:

  • (Upload)

    the newly-created upload



494
495
496
497
498
499
500
501
502
503
504
505
506
# File 'app/models/credit_memo.rb', line 494

def generate_pdf
  store = original_invoice.try(:store) || company.stores.first
  file_name = "#{reference_number}_credit_memo.pdf"
  path = Rails.application.config.x.temp_storage_path.join(file_name)

  pdf = CreditMemo::PdfGenerator.new(self, store: store, show_tax_info: true).generate

  File.binwrite(path, pdf)

  upload = Upload.uploadify(path, 'credit_memo_pdf', self, file_name)
  uploads << upload
  upload
end

#get_or_regen_pdf(logger = nil) ⇒ Upload

Returns the persisted credit-memo PDF Upload, regenerating it via
#generate_pdf if the upload row is missing or its file is gone
from S3.

Parameters:

  • logger (Logger, nil) (defaults to: nil)

Returns:



477
478
479
480
481
482
483
484
485
486
487
# File 'app/models/credit_memo.rb', line 477

def get_or_regen_pdf(logger = nil)
  logger ||= Rails.logger
  pdf = uploads.in_category('credit_memo_pdf').first
  logger.info "Retrieving Credit Memo #{id} pdf, record exists: #{!pdf.nil?}"
  unless pdf&.file_exists?
    logger.error ' * Pdf nil or file does not exist, attempting regen'
    pdf = generate_pdf
    logger.info "Pdf regenerated with upload id #{pdf.id}"
  end
  pdf
end

#invoiceInvoice

Returns:

See Also:



129
# File 'app/models/credit_memo.rb', line 129

has_one    :invoice, class_name: 'Invoice', foreign_key: :id, primary_key: :original_invoice_id

#is_build_com?Boolean

Returns:

  • (Boolean)


687
688
689
# File 'app/models/credit_memo.rb', line 687

def is_build_com?
  customer&.is_build_com?
end

#is_edi?Boolean

Returns:

  • (Boolean)


683
684
685
# File 'app/models/credit_memo.rb', line 683

def is_edi?
  invoice&.order&.is_edi_order?
end

#ledger_transactionsActiveRecord::Relation<LedgerTransaction>

Returns:

See Also:



136
# File 'app/models/credit_memo.rb', line 136

has_many   :ledger_transactions

#line_discountsActiveRecord::Relation<LineDiscount>

Returns:

See Also:



131
# File 'app/models/credit_memo.rb', line 131

has_many   :line_discounts, through: :line_items

#line_itemsActiveRecord::Relation<LineItem>

Returns:

See Also:



130
# File 'app/models/credit_memo.rb', line 130

has_many   :line_items, as: :resource, dependent: :destroy, extend: LineItemExtension, inverse_of: :resource

#nameString

Display name used in lists and links.

Returns:

  • (String)


453
454
455
# File 'app/models/credit_memo.rb', line 453

def name
  "Credit Memo: #{reference_number}"
end

#no_funds_offset?Boolean

Returns:

  • (Boolean)


323
324
325
# File 'app/models/credit_memo.rb', line 323

def no_funds_offset?
  funds_offset == 0
end

#original_invoiceInvoice

Returns:

See Also:



122
# File 'app/models/credit_memo.rb', line 122

belongs_to :original_invoice, class_name: 'Invoice', optional: true

#original_orderOrder

Returns:

See Also:



121
# File 'app/models/credit_memo.rb', line 121

belongs_to :original_order, class_name: 'Order', optional: true

#outgoing_payment_itemsActiveRecord::Relation<OutgoingPaymentItem>

Returns:

See Also:



139
# File 'app/models/credit_memo.rb', line 139

has_many   :outgoing_payment_items

#outgoing_paymentsActiveRecord::Relation<OutgoingPayment>

Returns:

See Also:



140
# File 'app/models/credit_memo.rb', line 140

has_many   :outgoing_payments, through: :outgoing_payment_items

#payment_statusHash

CRM badge describing where this credit memo sits in the
refund/offset flow — checks the linked outgoing Payment (refund
check), then any Receipt that consumed the credit, then state-machine
state. Returns {status:, color:, ref:, preview:?} consumed by the
credit-memo header partial.

Returns:

  • (Hash)


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

def payment_status
  pm = outgoing_payments.applied.first
  ck = pm.try(:checks).try(:first)
  rec = receipts.fully_applied.first
  if printed?
    { status: 'Credit Memo Prepared and Awaiting Payment', color: 'orange', ref: reference_number }
  elsif pm
    if pm.category == 'check'
      if pm.queued? or pm.generated?
        { status: 'Check In Print Queue', color: 'orange', ref: reference_number }
      elsif pm.pending_review?
        { status: 'Check Pending Approval', color: 'orange', ref: reference_number }
      elsif pm.printed? or pm.reprinted?
        if ck
          mailed_out = 1.working.days.since(ck.created_at)
          if mailed_out < Time.current
            { status: "Check Mailed Out #{mailed_out.to_fs(:crm_date_only)}", color: 'green',
              preview: (ck.uploads.empty? ? nil : ck.uploads.first.attachment.encode('png').url), ref: reference_number }
          else
            { status: "Check Mailed Out #{mailed_out.to_fs(:crm_date_only)}", color: 'orange',
              preview: (ck.uploads.empty? ? nil : ck.uploads.first.attachment.encode('png').url), ref: reference_number }
          end
        else
          { status: 'Check Printed', color: 'green', ref: reference_number }
        end
      end
    else
      { status: "Refunded via #{pm.category} #{pm.payment_date.to_fs(:crm_default)}", color: 'green', ref: reference_number }
    end
  elsif rec
    if rec.category == 'Credit Card'
      { status: "Credit Card (#{rec.reference}) Refunded #{rec.receipt_date.to_fs(:crm_default)}", color: 'green', ref: reference_number }
    elsif rec.receipt_details.any? { |rd| rd.invoice_id.present? }
      { status: "Credit Redeemed #{rec.receipt_date.to_fs(:crm_default)}", color: 'green', ref: reference_number }
    else
      { status: "Refunded via #{rec.category} #{rec.receipt_date.to_fs(:crm_default)}", color: 'green', ref: reference_number }
    end
  elsif processing_refund?
    { status: 'Processing Refund', color: 'orange', ref: reference_number }
  elsif partially_offset?
    { status: 'Partially Refunded', color: 'orange', ref: reference_number }
  elsif fully_offset?
    { status: 'Fully Refunded', color: 'orange', ref: reference_number }
  else
    { status: state, color: 'red', ref: reference_number }
  end
end

#paymentsActiveRecord::Relation<Payment>

Returns:

  • (ActiveRecord::Relation<Payment>)

See Also:



135
# File 'app/models/credit_memo.rb', line 135

has_many   :payments

#prevent_recalculate_shipping?Boolean

Returns:

  • (Boolean)


373
374
375
# File 'app/models/credit_memo.rb', line 373

def prevent_recalculate_shipping?
  true
end

#process_refund(auth_hash) ⇒ Hash{Symbol => Object}

Refunds the customer through gateway authorisations, splitting the
total across one or more Payment ids per auth_hash. Validates
that no individual amount is negative, that the sum doesn't exceed
the credit-memo balance, and that each amount fits within its
authorisation's refundable amount, then issues gateway refunds.

Parameters:

  • auth_hash (Hash{Integer => BigDecimal})

    payment id => refund amount

Returns:

  • (Hash{Symbol => Object})

    {success:, error_message:}



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

def process_refund(auth_hash)
  # check none are less than $0
  if processing_refund?
    { success: false, error_message: 'A refund is already being processed for this credit memo' }
  elsif auth_hash.any? { |_auth_id, refund_amount| refund_amount < 0 }
    { success: false, error_message: 'All refund amounts must be greater than 0 or left blank' }
  else
    # sum up the total value of the refund
    refund_total = BigDecimal('0.00')
    auth_hash.each { |_auth_id, refund_amount| refund_total += refund_amount }
    # check refund total is not greater than the credit memo balance
    if refund_total > -balance
      { success: false, error_message: 'Total of refund is greater than balance of credit memo' }
    # check individual refund amounts are available on the selected authorization
    elsif auth_hash.any? { |auth_id, refund_amount| Payment.find(auth_id).available_to_refund < refund_amount }
      { success: false, error_message: 'Amount to refund is greater than amount available to refund on one or more payments' }
    else
      success = true
      auth_hash.each do |auth_id, refund_amount|
        payment = Payment.find(auth_id)
        res = payment.gateway_class.new(payment).refund(refund_amount, self)
        success = false unless res.success
      end
      if success == false
        { success: false, error_message: 'One or more refunds could not be processed successfully' }
      else
        { success: true, error_message: 'Refund processed successfully' }
      end
    end
  end
end

#receipt_detailsActiveRecord::Relation<ReceiptDetail>

Returns:

See Also:



137
# File 'app/models/credit_memo.rb', line 137

has_many   :receipt_details, dependent: :nullify, inverse_of: :credit_memo

#receiptsActiveRecord::Relation<Receipt>

Returns:

  • (ActiveRecord::Relation<Receipt>)

See Also:



138
# File 'app/models/credit_memo.rb', line 138

has_many   :receipts, through: :receipt_details

#rmaRma

Returns:

See Also:



120
# File 'app/models/credit_memo.rb', line 120

belongs_to :rma, optional: true

#set_repsvoid

This method returns an undefined value.

Inherits sales-rep attribution from the original invoice when any
rep slot is unset. Fired before save so commission credit follows
the source sale.



668
669
670
671
672
673
674
# File 'app/models/credit_memo.rb', line 668

def set_reps
  return unless (primary_sales_rep_id.nil? or secondary_sales_rep_id.nil? or local_sales_rep_id.nil?) and original_invoice.present?

  self.primary_sales_rep = original_invoice.primary_sales_rep if primary_sales_rep_id.nil?
  self.secondary_sales_rep = original_invoice.secondary_sales_rep if secondary_sales_rep_id.nil?
  self.local_sales_rep = original_invoice.local_sales_rep if local_sales_rep_id.nil?
end

#shipping_addressAddress Also known as: destination_address

Returns:

See Also:



127
# File 'app/models/credit_memo.rb', line 127

belongs_to :shipping_address, class_name: 'Address', optional: true

#to_sString

Human-readable identifier used in audit logs and error messages.

Returns:

  • (String)


679
680
681
# File 'app/models/credit_memo.rb', line 679

def to_s
  "Credit Memo # #{reference_number}"
end

#uploadsActiveRecord::Relation<Upload>

has_many :shipping_costs, -> { order(:cost) }, :as => :resource, :dependent => :destroy

Returns:

  • (ActiveRecord::Relation<Upload>)

See Also:



133
# File 'app/models/credit_memo.rb', line 133

has_many   :uploads, -> { order(:updated_at).reverse_order }, as: :resource, dependent: :destroy