Class: Invoice

Overview

Invoice model representing a financial document for goods or services.
Handles billing, payments, line items, and various invoice types including
sales orders, credit memos, and consignment invoices.

Defined Under Namespace

Classes: CaptureFundsHandler, TaxjarSubmissionHandler

Constant Summary collapse

SO =

Sales Order — invoice generated when a customer Order's Delivery ships.

'SO'
ST =

Sales Tax — placeholder type used for tax-only adjustment invoices.

'ST'
MI =

Miscellaneous Invoice — manually entered, not tied to an order.

'MI'
MO =

Manual Order — legacy manual invoice type, predates Order entry workflow.

'MO'
TO =

Trade Order — internal trade between companies in the JDE multi-company ledger.

'TO'
CI =

Consignment Invoice — issued when consignment stock is invoiced to the consignee.

'CI'
SS =

Service / Smart Service — invoice for installation or smart-service labour, not goods.

'SS'
INVOICE_TYPES =

All recognised JDE invoice-type codes accepted by validators and dropdowns.

[SO, ST, MI, MO, TO, CI, SS].freeze
REFERENCE_NUMBER_PATTERN =

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

/^INV\d+$/i
LINE_ITEM_CATEGORIES =

Mapping from CRM-facing line-item category labels to the JDE GL account
they post against. Drives the "Add line item" dropdown on miscellaneous
(MI) and consignment (CI) invoices and the GL split when transmitted.

[{ name: 'Coupon (Goods)', account_number: COUPONS_ACCOUNT },
{ name: 'Coupon (Freight)', account_number: FREIGHT_COUPONS_ACCOUNT },
{ name: 'Freight', account_number: FREIGHT_ACCOUNT },
{ name: 'Misc', account_number: PRODUCT_SALES_ACCOUNT },
{ name: 'Item', account_number: PRODUCT_SALES_ACCOUNT },
{ name: 'Fee', account_number: nil }].freeze

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::Profitable

#min_profit_markup

Attributes included from Models::Itemizable

#force_total_reset, #total_reset

Belongs to collapse

Methods included from Models::TaxableResource

#resource_tax_rate

Methods included from Models::Itemizable

#account_specialist, #local_sales_rep, #primary_sales_rep, #secondary_sales_rep

Methods included from Models::Auditable

#creator, #updater

Has many collapse

Methods included from Models::Itemizable

#coupons, #discounts

Delegated Instance Attributes collapse

Class Method Summary collapse

Instance Method Summary collapse

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::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::Profitable

#default_sales_markup, #profit_margins_met?, #profitable_line_items, #profitable_status, #profitable_total_discounted, #profitable_total_estimated_cost, #profitable_total_estimated_line_cost, #profitable_total_profit, #profitable_total_profit_margin, #profitable_total_profit_markup, #track_profit?, #validate_min_profit_markup?

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, #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, #subtotal_cogs, #sync_shipping_line, #total_cogs

Methods included from Models::Auditable

#all_skipped_columns, #audit_reference_data, #should_not_save_version, #stamp_record

Methods included from Models::Notable

#quick_note

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

#allow_duplicate_delivery_for_testingObject

Returns the value of attribute allow_duplicate_delivery_for_testing.



175
176
177
# File 'app/models/invoice.rb', line 175

def allow_duplicate_delivery_for_testing
  @allow_duplicate_delivery_for_testing
end

#billing_address_idObject (readonly)



210
# File 'app/models/invoice.rb', line 210

validates :due_date, :terms, :invoice_type, :billing_address_id, :customer_id, :document_date, :gl_date, :gl_offset_account_id, presence: true

#customer_idObject (readonly)



210
# File 'app/models/invoice.rb', line 210

validates :due_date, :terms, :invoice_type, :billing_address_id, :customer_id, :document_date, :gl_date, :gl_offset_account_id, presence: true

#delivery_idObject (readonly)



215
# File 'app/models/invoice.rb', line 215

validates :delivery_id, uniqueness: { allow_nil: true }, unless: :allow_duplicate_delivery_for_testing?

#disable_auto_couponObject

Returns the value of attribute disable_auto_coupon.



175
176
177
# File 'app/models/invoice.rb', line 175

def disable_auto_coupon
  @disable_auto_coupon
end

#do_not_detect_shippingObject

Returns the value of attribute do_not_detect_shipping.



175
176
177
# File 'app/models/invoice.rb', line 175

def do_not_detect_shipping
  @do_not_detect_shipping
end

#do_not_set_totalsObject

Returns the value of attribute do_not_set_totals.



175
176
177
# File 'app/models/invoice.rb', line 175

def do_not_set_totals
  @do_not_set_totals
end

#document_dateObject (readonly)



210
# File 'app/models/invoice.rb', line 210

validates :due_date, :terms, :invoice_type, :billing_address_id, :customer_id, :document_date, :gl_date, :gl_offset_account_id, presence: true

#due_dateObject (readonly)



210
# File 'app/models/invoice.rb', line 210

validates :due_date, :terms, :invoice_type, :billing_address_id, :customer_id, :document_date, :gl_date, :gl_offset_account_id, presence: true

#enter_new_addressObject

Returns the value of attribute enter_new_address.



175
176
177
# File 'app/models/invoice.rb', line 175

def enter_new_address
  @enter_new_address
end

#gl_dateObject (readonly)



210
# File 'app/models/invoice.rb', line 210

validates :due_date, :terms, :invoice_type, :billing_address_id, :customer_id, :document_date, :gl_date, :gl_offset_account_id, presence: true

#gl_offset_account_idObject (readonly)



210
# File 'app/models/invoice.rb', line 210

validates :due_date, :terms, :invoice_type, :billing_address_id, :customer_id, :document_date, :gl_date, :gl_offset_account_id, presence: true

#gl_offset_account_refObject

Returns the value of attribute gl_offset_account_ref.



175
176
177
# File 'app/models/invoice.rb', line 175

def 
  @gl_offset_account_ref
end

#invoice_typeObject (readonly)



210
# File 'app/models/invoice.rb', line 210

validates :due_date, :terms, :invoice_type, :billing_address_id, :customer_id, :document_date, :gl_date, :gl_offset_account_id, presence: true

#order_idObject (readonly)



211
# File 'app/models/invoice.rb', line 211

validates :order_id, presence: { if: proc { |i| [MI, CI].exclude?(i.invoice_type) } }

#original_order_refObject

Returns the value of attribute original_order_ref.



175
176
177
# File 'app/models/invoice.rb', line 175

def original_order_ref
  @original_order_ref
end

#skip_initial_state_checkObject

Flag to bypass the initial state check (use only if you have a legitimate reason to create
an invoice in a non-draft state, which should be extremely rare)



252
253
254
# File 'app/models/invoice.rb', line 252

def skip_initial_state_check
  @skip_initial_state_check
end

#skip_line_item_integrity_checkObject

Flag to skip integrity check during initial creation (set by CreateInvoiceFromDelivery)



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

def skip_line_item_integrity_check
  @skip_line_item_integrity_check
end

#skip_pdfObject

Returns the value of attribute skip_pdf.



175
176
177
# File 'app/models/invoice.rb', line 175

def skip_pdf
  @skip_pdf
end

#store_idObject (readonly)



212
# File 'app/models/invoice.rb', line 212

validates :store_id, presence: { if: proc { |i| [MI, CI].include?(i.invoice_type) } }

#tax_dateObject (readonly)



213
# File 'app/models/invoice.rb', line 213

validates :tax_date, :store_id, presence: { if: proc { |i| [MI, CI].include?(i.invoice_type) } }

#termsObject (readonly)



210
# File 'app/models/invoice.rb', line 210

validates :due_date, :terms, :invoice_type, :billing_address_id, :customer_id, :document_date, :gl_date, :gl_offset_account_id, presence: true

Class Method Details

.awaiting_transmissionActiveRecord::Relation<Invoice>

A relation of Invoices that are awaiting transmission. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Invoice>)

See Also:



259
# File 'app/models/invoice.rb', line 259

scope :awaiting_transmission, -> { where(state: %w[unpaid paid], transmission_state: %w[awaiting_transmission in_transmission_queue]) }

.calculate_due_date(order, delivery) ⇒ Date

Computes the invoice due date as delivery.shipped_date + billing_entity.terms_in_days. Called once at invoice creation;
the result is persisted onto due_date.

Parameters:

Returns:

  • (Date)


551
552
553
554
# File 'app/models/invoice.rb', line 551

def self.calculate_due_date(order, delivery)
  shipped_date = delivery.shipped_date.to_datetime.to_date
  shipped_date + order.billing_entity.terms_in_days.days
end

.calculate_terms(order) ⇒ String

Resolves the textual payment-terms string from an Order, appending
(COD) when the order is COD-funded so it prints on the PDF.

Parameters:

Returns:

  • (String)


578
579
580
581
582
# File 'app/models/invoice.rb', line 578

def self.calculate_terms(order)
  terms = order.billing_entity.terms
  terms += ' (COD)' if order.funded_by_cod?
  terms
end

.included_in_notificationsActiveRecord::Relation<Invoice>

A relation of Invoices that are included in notifications. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Invoice>)

See Also:



262
# File 'app/models/invoice.rb', line 262

scope :included_in_notifications, -> { where(exclude_fund_capture_notification: false) }

.like_lookupActiveRecord::Relation<Invoice>

A relation of Invoices that are like lookup. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Invoice>)

See Also:



265
# File 'app/models/invoice.rb', line 265

scope :like_lookup, ->(q) { left_joins(:order).where(Invoice[:reference_number].matches("%#{q}%")).or(Order.where(Order[:reference_number].matches("%#{q}%"))) }

.lookupActiveRecord::Relation<Invoice>

A relation of Invoices that are lookup. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Invoice>)

See Also:



264
# File 'app/models/invoice.rb', line 264

scope :lookup, ->(q) { where(reference_number: q) }

.missing_edi_810ActiveRecord::Relation<Invoice>

A relation of Invoices that are missing edi 810. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Invoice>)

See Also:



266
267
268
269
270
# File 'app/models/invoice.rb', line 266

scope :missing_edi_810, -> {
  joins(customer: :notification_channels)
    .where(notification_channels: { notification_type: NotificationChannel::INVOICES, transmission_type: NotificationChannel::EDI })
    .where(transmission_state: 'awaiting_transmission')
}

.overdueActiveRecord::Relation<Invoice>

A relation of Invoices that are overdue. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Invoice>)

See Also:



263
# File 'app/models/invoice.rb', line 263

scope :overdue, -> { unpaid.where(due_date: ...Date.current) }

.sales_ordersActiveRecord::Relation<Invoice>

A relation of Invoices that are sales orders. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Invoice>)

See Also:



260
# File 'app/models/invoice.rb', line 260

scope :sales_orders, -> { where(invoice_type: 'SO') }

.unpaidActiveRecord::Relation<Invoice>

A relation of Invoices that are unpaid. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Invoice>)

See Also:



261
# File 'app/models/invoice.rb', line 261

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

Instance Method Details

#activitiesActiveRecord::Relation<Activity>

Returns:

See Also:



201
# File 'app/models/invoice.rb', line 201

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

#allow_duplicate_delivery_for_testing?Boolean

Returns:

  • (Boolean)


217
218
219
# File 'app/models/invoice.rb', line 217

def allow_duplicate_delivery_for_testing?
  allow_duplicate_delivery_for_testing == true
end

#amount_dueBigDecimal

Liquid-template alias for #total; kept distinct from #balance so
the public pay-link template can show the original amount even after
partial payments.

Returns:

  • (BigDecimal)


527
528
529
# File 'app/models/invoice.rb', line 527

def amount_due
  total
end

#balanceBigDecimal

Outstanding balance — invoice total minus the sum of applied
ReceiptDetails (cash receipts, write-offs, applied discounts).
Drives balance_is_zero? / balance_positive? and the AR aging report.

Returns:

  • (BigDecimal)


494
495
496
# File 'app/models/invoice.rb', line 494

def balance
  total - receipts_total
end

#balance_is_zero?Boolean

Returns:

  • (Boolean)


485
486
487
# File 'app/models/invoice.rb', line 485

def balance_is_zero?
  balance.zero?
end

#balance_positive?Object

Alias for Balance#positive?

Returns:

  • (Object)

    Balance#balance_positive?

See Also:



281
# File 'app/models/invoice.rb', line 281

delegate :positive?, to: :balance, prefix: true, allow_nil: true

#billing_addressAddress

Returns:

See Also:



178
# File 'app/models/invoice.rb', line 178

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

#billing_customerCustomer

Returns:

See Also:



182
# File 'app/models/invoice.rb', line 182

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

#billing_entityParty

The Party (Customer or sub-account) who is actually being billed,
which can differ from #customer when a buying-group or parent
account is on the billing address.

Returns:



1021
1022
1023
# File 'app/models/invoice.rb', line 1021

def billing_entity
  billing_address.party
end

#build_activityActivity

Builds (but does not save) a new Activity attached to this invoice
with the customer party pre-populated, mirroring the CRM activity-form
helper used by Order and Quote.

Returns:



390
391
392
# File 'app/models/invoice.rb', line 390

def build_activity
  activities.build resource: self, party: primary_party
end

#business_unitBusinessUnit



188
# File 'app/models/invoice.rb', line 188

belongs_to :business_unit, optional: true, inverse_of: :invoices

#buying_groupBuyingGroup



184
# File 'app/models/invoice.rb', line 184

belongs_to :buying_group, optional: true, inverse_of: :invoices

#calculate_all_cogsBigDecimal

Cost-of-goods total computed in SQL across every Item-category
LineItem, ignoring tax-class filtering. Used by the BoB / margin
report. Casts to BigDecimal to avoid float drift downstream.

Returns:

  • (BigDecimal)


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

def calculate_all_cogs
  BigDecimal(line_items.where(cm_category: 'Item').sum('unit_cogs * quantity'))
end

#calculate_cogs(tax_class = %w[g svc shp])) ⇒ BigDecimal

Cost-of-goods total for this invoice restricted to the supplied tax
classes — defaults to goods (g), services (svc) and shipping (shp).
Walks the in-memory Item-category LineItems so unsaved edits
are reflected; pair with #calculate_all_cogs for the full-set sum.

Parameters:

  • tax_class (Array<String>) (defaults to: %w[g svc shp]))

    tax-class codes to include

Returns:

  • (BigDecimal)


401
402
403
# File 'app/models/invoice.rb', line 401

def calculate_cogs(tax_class = %w[g svc shp])
  line_items.where(cm_category: 'Item').select { |li| tax_class.include?(li.calculated_tax_class) }.sum { |li| li.unit_cogs * li.quantity }
end

#capture_funds?Boolean

Returns:

  • (Boolean)


740
741
742
743
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
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
# File 'app/models/invoice.rb', line 740

def capture_funds?
  logger.info("#{Time.current}: Capturing funds for invoice id: #{id}, ref: #{reference_number}, delivery id: #{delivery_id}")
  copy_payments unless delivery.nil?
  invoice_total = total
  captured_balance = payments.all_captured.sum(:amount)
  pending_balance = invoice_total - captured_balance
  payments_total = payments.all_authorized.sum(:amount)
  check_total = payments.all_check_captured.sum(:amount)
  applied_store_credit_total = delivery.nil? ? 0 : delivery.payments.where(state: 'authorized', category: Payment::STORE_CREDIT, currency: currency).sum(:amount)
  unapplied_credit_memos = billing_customer.credit_memos.available_to_apply.order(:document_date)

  logger.info("#{Time.current}: Invoice total: #{invoice_total}")
  logger.info("#{Time.current}: Payments not captured available: #{payments_total}")
  logger.info("#{Time.current}: Checks already captured but receipt needed: #{check_total}")

  capture_problem = false
  if pending_balance <= 0
    create_receipts_for_captured_payments
    paid! if can_paid?

    if pending_balance.negative?
      overpaid_amount = pending_balance.abs
      currency_sym = Money::Currency.new(currency).symbol
      Mailer.generic_mailer(
        from: ADMINISTRATOR_EMAIL,
        to: "#{ADMINISTRATOR_EMAIL},#{ACCOUNTS_RECEIVABLE_EMAIL}",
        subject: "ORDER OVERPAYMENT — Invoice ##{reference_number} (Order ##{order&.reference_number})",
        message: "Invoice ##{reference_number} (Order ##{order&.reference_number}) has been overpaid by #{currency_sym}#{'%.2f' % overpaid_amount}.\n\n" \
                 "Invoice total: #{currency_sym}#{'%.2f' % invoice_total}\n" \
                 "Total captured: #{currency_sym}#{'%.2f' % captured_balance}\n" \
                 "Overpaid amount: #{currency_sym}#{'%.2f' % overpaid_amount}\n\n" \
                 "Customer: #{customer&.full_name} (ID: #{customer_id})\n" \
                 "CRM link: #{crm_link}\n\n" \
                 "A manual refund or credit memo needs to be issued for the excess amount.",
        no_verbage: true
      ).deliver
      logger.warn("#{Time.current}: OVERPAYMENT on invoice #{id}: captured #{captured_balance} exceeds total #{invoice_total} by #{overpaid_amount}")
    elsif invoice_type == 'SO'
      Mailer.generic_mailer(from: ADMINISTRATOR_EMAIL,
                            to: "#{ADMINISTRATOR_EMAIL},#{ACCOUNTS_RECEIVABLE_EMAIL}",
                            subject: "BALANCE ALREADY PAID FOR INVOICE ID ##{id}",
                            message: "Invoice ##{id} had all the payments already captured before starting the invoicing process. This might be ok, but check all payments are correctly captured.",
                            no_verbage: true).deliver
    end
  else
    # WE SHOULD ALWAYS USE PAYMENT.AMOUNT IN THE RECEIPTS CREATED SINCE THE PAYMENT HAS ALRADY BEEN CAPTURED. THERE IS NO POINT IN USING
    # THE BALANCE IF THAT NUMBER IS DIFFERENT THAN THE ALREADY CAPTURED PAYMENT
    # SO WE SHOULD USE A METHOD LIKE create_receipts_for_captured_payments

    # First let's create the receipts for the payments that have been captured already.
    create_receipts_for_captured_payments

    # Second let's add receipts for any store credit used
    if applied_store_credit_total.positive?
      unapplied_credit_memos.each do |cm|
        next if pending_balance.zero?

        cm_balance = cm.balance * -1
        amount = [pending_balance, cm_balance].min
        new_receipt = Receipt.new(company: company,
                                  customer: customer,
                                  category: 'Non-Cash',
                                  amount: 0,
                                  reference: cm.reference_number,
                                  currency: currency,
                                  gl_date: Date.current,
                                  receipt_date: Date.current)
        new_receipt.receipt_details << ReceiptDetail.new(category: 'Invoice', invoice: self, amount: amount, gl_date: Date.current)
        new_receipt.receipt_details << ReceiptDetail.new(category: 'Credit Memo', credit_memo: cm, amount: amount * -1, gl_date: Date.current)
        begin
          new_receipt.save!
          logger.info("#{Time.current}: Created new receipt id: #{new_receipt.id}")
          pending_balance -= amount
        rescue StandardError => e
          msg = "#{Time.current}: Unable to create new receipt for Credit Memo ID: #{cm.id} (store credit), Exception: #{e}"
          logger.error(msg)
          ErrorReporting.error(e, credit_memo_id: cm.id)
          capture_problem = true
        end
      end
    end

    # Finally let's capture the remaining balance from existing authorizations
    payments.all_authorized.cc_paypal_bread_amazon.each do |payment|
      next if pending_balance.zero? # If the balance is already zero then we don't need to capture more

      amount = [pending_balance, payment.amount].min
      res = payment.gateway_class.new(payment).capture(amount, { order_id: reference_number, currency: payment.currency })
      if res.success
        pending_balance -= amount
        capture_problem = true if payment.receipts.empty?
      else
        capture_problem = true
      end
    end
  end

  # The rest of payment methods cannot be captured through a gateway and need manual processing.
  # For example, POs or VPOs. We generate the invoice PDF for those and then accounting has a special
  # report to find the unpaid invoices and apply a voucher, credit memo, or any other payment type to
  # mark the invoice as paid

  # Check if capture_problem is due to legitimate issues or just manual-processing payment types
  # Don't flag PO/VPO/ECHECK/WIRE payments as problems since they require manual intervention by design
  if capture_problem
    authorized_without_receipts = payments.all_authorized.select { |p| p.receipts.empty? }
    only_manual_payments = authorized_without_receipts.all? { |p| Payment::CATEGORIES_NOT_ALLOWING_CAPTURE.include?(p.category) }

    if only_manual_payments && authorized_without_receipts.any?
      logger.info("#{Time.current}: Authorized payments exist that require manual processing (#{authorized_without_receipts.map(&:category).uniq.join(', ')}). This is expected.")
      capture_problem = false
    end
  end

  # Let's do a security check to make sure the capture balance is the same as the invoice balance
  # final_captured_balance = payments.all_captured.sum(:amount)
  # invoice_total = total
  # capture_problem = true if invoice_total != final_captured_balance

  if capture_problem == true
    Mailer.generic_mailer(
      from: ADMINISTRATOR_EMAIL,
      to: "#{ADMINISTRATOR_EMAIL},#{ACCOUNTS_RECEIVABLE_EMAIL}",
      subject: "INVOICE ##{reference_number} FUNDS CAPTURE ERROR",
      message: "There has been a problem with the funds capture on invoice id #{id}, ref #{reference_number}, delivery id: #{delivery_id}. Please take action to ensure all funds are captured or applied.",
      no_verbage: true
    ).deliver
    logger.error("#{Time.current}: CAPTURE ERROR: Problem with funds capture")
  else
    if pending_balance.zero?
      logger.info("#{Time.current}: Funds captured successfully.")
    else
      logger.info("#{Time.current}: Funds captured successfully, but balance has not been completely paid.")
    end
    # enqueue pdf generation process
    InvoicePdfGenerationWorker.perform_async(id)
  end

  true
end

#chosen_shipping_costBigDecimal

Numeric cost of #chosen_shipping_method, or 0.00 when the carrier
uses a customer-supplied shipping account number (no charge to bill).
Wrapped in rescue because legacy data has nil shipping methods.

Returns:

  • (BigDecimal)


597
598
599
600
601
602
603
604
605
# File 'app/models/invoice.rb', line 597

def chosen_shipping_cost
  cost = BigDecimal('0.00')
  begin
    cost = chosen_shipping_method.cost unless chosen_shipping_method.
  rescue StandardError => e
    Rails.logger.warn "Could not get shipping cost for invoice #{id}: #{e.message}"
  end
  cost
end

#chosen_shipping_methodShippingCost?

Cheapest available ShippingCost on the parent Delivery — what
the customer is actually being charged for shipping on this invoice.

Returns:



588
589
590
# File 'app/models/invoice.rb', line 588

def chosen_shipping_method
  shipping_costs.first
end

#combined_termsString

Terms label augmented with the early-payment offer in the standard
"Net X - Y%/Z" notation (e.g. Net 30 - 2%/10). Falls back to plain
#terms when no early-payment offer applies.

Returns:

  • (String)


971
972
973
974
975
976
977
# File 'app/models/invoice.rb', line 971

def combined_terms
  if early_payment_discount && early_payment_timescale
    "#{terms} - #{early_payment_discount}%/#{early_payment_timescale}"
  else
    terms
  end
end

#communicationsActiveRecord::Relation<Communication>

Returns:

See Also:



203
# File 'app/models/invoice.rb', line 203

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

#companyCompany

Returns:

See Also:



183
# File 'app/models/invoice.rb', line 183

belongs_to :company, inverse_of: :invoices

#copy_paymentsvoid

This method returns an undefined value.

Re-points Payments from the originating Delivery onto this invoice
so that authorised cards / captured echecks are available for capture
against the new invoice id. Skips foreign-currency payments and
payments not yet authorised.



479
480
481
482
483
# File 'app/models/invoice.rb', line 479

def copy_payments
  delivery.payments.where(currency: currency).find_each do |pp|
    pp.update!(invoice_id: id) if pp.authorized? || (pp.authorization_type.in?(%w[credit_card check paypal_invoice amazon_pay]) && pp.captured?)
  end
end

#create_receipts_for_captured_paymentsvoid

This method returns an undefined value.

Creates Receipts for Payments that have already been captured
at the gateway but don't yet have receipts on this invoice. Used
both by funds-capture and as the catch-up step for CC/PayPal
captures whose webhook receipt creation didn't fire. Idempotent —
skips payments already linked to receipts.



888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
# File 'app/models/invoice.rb', line 888

def create_receipts_for_captured_payments
  payments.all_captured.each do |payment|
    next if payment.receipts.present?
    next if payment.skip_auto_receipt

    # Extends the AppSignal #4120 fix (31f69165d3, which guarded the
    # create_receipt_details path) to this captured-payments path. If the
    # invoice was already settled by another receipt before this async
    # catch-up runs, applying payment.amount over-applies; the receipt-detail
    # balance validation correctly rejects it and the RecordInvalid re-raised
    # into CaptureFundsHandler for a futile Sidekiq retry. Skip instead —
    # same net result (no over-applied detail) without the spurious error.
    if balance.present? && payment.amount > balance
      Rails.logger.info "[Invoice##{id}] skipping captured-payment receipt for payment ##{payment.id}: amount #{payment.amount} exceeds balance #{balance}"
      next
    end

    begin
      res = payment.gateway_class.new(payment).create_receipt(self, payment.amount, payment.amount)
      res.receipt.apply
    rescue ActiveRecord::RecordInvalid => e
      # Belt-and-suspenders for the check-then-act race: if a concurrent receipt
      # settles the invoice between the balance check above and #apply, the
      # receipt-detail balance validation rejects the over-application. Skip this
      # one payment and keep going rather than re-raising the deterministic
      # failure into a futile CaptureFundsHandler Sidekiq retry (#4120).
      Rails.logger.info "[Invoice##{id}] skipping over-applying receipt for payment ##{payment.id}: #{e.message}"
    end
  end

  # create receipt details for already cc and paypal payments with an unapplied receipt
  payments.all_cc_captured.each do |payment|
    receipts_with_no_details = payment.receipts.where.missing(:receipt_details)
    receipts_with_no_details.each do |receipt|
      receipt.create_receipt_details(payment.invoice, payment.amount) if payment.invoice.present?
      receipt.apply
    end
  end
end

#credit_memosActiveRecord::Relation<CreditMemo>

Returns:

See Also:



198
# File 'app/models/invoice.rb', line 198

has_many   :credit_memos, foreign_key: 'original_invoice_id', dependent: :destroy, inverse_of: :original_invoice

CRM URL for the invoice show page. Used by activity/comm logs and
admin notification emails so reps can jump straight to the record.

Returns:

  • (String)


700
701
702
# File 'app/models/invoice.rb', line 700

def crm_link
  UrlHelper.instance.invoice_path(self)
end

#currency_symbolString

Currency symbol (e.g. $, ) for this invoice's #currency, used
in the PDF and overpayment notification templates.

Returns:

  • (String)


502
503
504
# File 'app/models/invoice.rb', line 502

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

#customerCustomer

Returns:

See Also:



181
# File 'app/models/invoice.rb', line 181

belongs_to :customer, optional: true, inverse_of: :invoices

#customer_nameString?

Display name for the Customer this invoice bills, falling back to
nil for headless data (rare — most invoices have a customer).

Returns:

  • (String, nil)


461
462
463
# File 'app/models/invoice.rb', line 461

def customer_name
  customer.try(:full_name)
end

#deliveryDelivery

Returns:

See Also:



189
# File 'app/models/invoice.rb', line 189

belongs_to :delivery, optional: true, inverse_of: :invoices

#disable_auto_coupon?Boolean

Returns:

  • (Boolean)


1012
1013
1014
# File 'app/models/invoice.rb', line 1012

def disable_auto_coupon?
  true
end

#discount_appliedBigDecimal

Sum of discount written off via ReceiptDetails, e.g. early-payment
discounts or accounting write-downs. Reported on the invoice ledger tab.

Returns:

  • (BigDecimal)


1004
1005
1006
# File 'app/models/invoice.rb', line 1004

def discount_applied
  receipt_details.sum(:discount)
end

#discount_days_dueInteger?

Days between #gl_date and #early_payment_due_date; used by the
PDF "early-payment discount" line. Nil when no early-payment offer applies.

Returns:

  • (Integer, nil)


567
568
569
570
571
# File 'app/models/invoice.rb', line 567

def discount_days_due
  return unless early_payment_due_date

  (early_payment_due_date - gl_date).to_i
end

#do_not_detect_shipping?Boolean

Returns:

  • (Boolean)


1008
1009
1010
# File 'app/models/invoice.rb', line 1008

def do_not_detect_shipping?
  true
end

#drop_ship_purchase_ordersActiveRecord::Relation<DropShipPurchaseOrder>

Returns:

  • (ActiveRecord::Relation<DropShipPurchaseOrder>)

See Also:



204
# File 'app/models/invoice.rb', line 204

has_many   :drop_ship_purchase_orders, -> { order(:id) }, through: :delivery, dependent: :destroy

#early_payment_amountBigDecimal

Dollar value of the early-payment discount — early_payment_discount%
of the invoice total, rounded to 2dp. Zero when no early-payment
offer is configured.

Returns:

  • (BigDecimal)


933
934
935
936
937
938
939
# File 'app/models/invoice.rb', line 933

def early_payment_amount
  if early_payment_discount.blank?
    BigDecimal(0)
  else
    ((early_payment_discount * total) / 100).round(2)
  end
end

#early_payment_due_dateDate?

Date by which the customer must pay to qualify for the
early-payment discount (shipped_date + early_payment_timescale days).
Nil when no early-payment offer applies.

Returns:

  • (Date, nil)


946
947
948
949
950
951
952
# File 'app/models/invoice.rb', line 946

def early_payment_due_date
  if early_payment_timescale.blank?
    nil
  else
    shipped_date + early_payment_timescale.days
  end
end

#early_payment_totalBigDecimal

Discounted total the customer would owe if they pay by
#early_payment_due_date — total minus #early_payment_amount.

Returns:

  • (BigDecimal)


958
959
960
961
962
963
964
# File 'app/models/invoice.rb', line 958

def early_payment_total
  if early_payment_discount.zero?
    total
  else
    total - early_payment_amount
  end
end

#edi_communication_logsActiveRecord::Relation<EdiCommunicationLog>

Returns:

See Also:



206
# File 'app/models/invoice.rb', line 206

has_many   :edi_communication_logs, through: :edi_documents, dependent: :destroy

#edi_documentsActiveRecord::Relation<EdiDocument>

Returns:

See Also:



205
# File 'app/models/invoice.rb', line 205

has_many   :edi_documents, dependent: :destroy, inverse_of: :invoice

#editing_locked?Boolean

Returns:

  • (Boolean)


435
436
437
# File 'app/models/invoice.rb', line 435

def editing_locked?
  unpaid? || paid?
end

#effective_storeStore?

Resolves the Store that should own this invoice for inventory and
ledger purposes — the explicit store, falling back to the parent
Order's store, then the company's first store. Used by report
grouping and the consignment-offset GL lookup.

Returns:



364
365
366
# File 'app/models/invoice.rb', line 364

def effective_store
  store || order&.store || company.stores.first
end

#file_name(with_extension: true) ⇒ String

Filename used when attaching the invoice PDF to email or storing in S3.

Parameters:

  • with_extension (Boolean) (defaults to: true)

    include .pdf

Returns:

  • (String)


668
669
670
# File 'app/models/invoice.rb', line 668

def file_name(with_extension: true)
  "invoice_#{reference_number}#{'.pdf' if with_extension}"
end

#friendly_shipping_method(show_customer_pays_info: false) ⇒ String

Human-readable shipping-method label for the PDF and CRM —
"Warehouse Pickup" for warehouse addresses, otherwise the carrier
description with an optional COD-charge note.

Parameters:

  • show_customer_pays_info (Boolean) (defaults to: false)

    kept for signature compatibility

Returns:

  • (String)


629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
# File 'app/models/invoice.rb', line 629

def friendly_shipping_method(show_customer_pays_info: false) # rubocop:disable Lint/UnusedMethodArgument
  shipping_method_name = ''
  method_cod = ''
  if begin
    shipping_address.is_warehouse
  rescue StandardError => e
    Rails.logger.warn "Could not check if shipping address is warehouse for invoice #{id}: #{e.message}"
    false
  end
    shipping_method_name = 'Warehouse Pickup'
  elsif chosen_shipping_method
    shipping_method_name = chosen_shipping_method.description
    method_cod = ' (inc. COD charge)' if chosen_shipping_method.cod
  end
  "#{shipping_method_name} #{method_cod}".strip
end

#fully_funded_by_rma?Boolean

Returns:

  • (Boolean)


732
733
734
# File 'app/models/invoice.rb', line 732

def fully_funded_by_rma?
  order.present? && order.fully_funded_by_advance_replacement?
end

#funded_by_cod?Boolean

Returns:

  • (Boolean)


736
737
738
# File 'app/models/invoice.rb', line 736

def funded_by_cod?
  terms.include?('COD')
end

#funded_by_rma?Boolean

Returns:

  • (Boolean)


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

def funded_by_rma?
  order.present? && order.funded_by_advance_replacement?
end

#generate_pdfUpload

Renders a fresh combined invoice PDF (cover + line-item pages + any
addendums) via Invoicing::CombinedPdfGenerator, uploads it to S3
under the invoice_pdf category and attaches the Upload.

Returns:

  • (Upload)

    the newly-created upload



677
678
679
680
681
682
683
684
685
# File 'app/models/invoice.rb', line 677

def generate_pdf
  combined_pdf_result = Invoicing::CombinedPdfGenerator.new.process(self, output_to_file: true)
  upload = Upload.uploadify(combined_pdf_result.pdf_file_path,
                            'invoice_pdf',
                            self,
                            combined_pdf_result.file_name)
  uploads << upload
  upload
end

#get_or_regen_pdf(logger = nil) ⇒ Upload

Returns the persisted invoice-PDF Upload, regenerating it via
#generate_pdf if the upload row is missing or its file is gone
from S3. Used by transmission and the CRM "Download PDF" action.

Parameters:

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

Returns:



652
653
654
655
656
657
658
659
660
661
662
# File 'app/models/invoice.rb', line 652

def get_or_regen_pdf(logger = nil)
  logger ||= Rails.logger
  pdf = uploads.in_category('invoice_pdf').first
  logger.info "Retrieving Invoice #{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

#gl_offset_accountLedgerCompanyAccount



187
# File 'app/models/invoice.rb', line 187

belongs_to :gl_offset_account, class_name: 'LedgerCompanyAccount', optional: true, inverse_of: :invoices

#item_ledger_entriesActiveRecord::Relation<ItemLedgerEntry>

Returns:

See Also:



196
# File 'app/models/invoice.rb', line 196

has_many   :item_ledger_entries, dependent: :destroy, inverse_of: :invoice

#ledger_transactionsActiveRecord::Relation<LedgerTransaction>

Returns:

See Also:



195
# File 'app/models/invoice.rb', line 195

has_many   :ledger_transactions, dependent: :destroy, inverse_of: :invoice

#line_discountsActiveRecord::Relation<LineDiscount>

Returns:

See Also:



193
# File 'app/models/invoice.rb', line 193

has_many   :line_discounts, through: :line_items

#line_itemsActiveRecord::Relation<LineItem>

Returns:

See Also:



192
# File 'app/models/invoice.rb', line 192

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

#marketplace_invoice_formatObject

Alias for Customer#marketplace_invoice_format

Returns:

  • (Object)

    Customer#marketplace_invoice_format

See Also:



280
# File 'app/models/invoice.rb', line 280

delegate :marketplace_invoice_format, to: :customer, allow_nil: true

#nameString

Short display name for selection lists and links — just the
reference number.

Returns:

  • (String)


611
612
613
# File 'app/models/invoice.rb', line 611

def name
  reference_number
end

#non_service_line_itemsArray<LineItem>

Goods/shipping LineItems only — services (tax_class 'svc') excluded.
Used for ship-confirmation logic where service lines should not affect
what's physically shipped.

Returns:



419
420
421
# File 'app/models/invoice.rb', line 419

def non_service_line_items
  line_items.where(cm_category: 'Item').reject { |li| li.item.tax_class == 'svc' }
end

#non_voided_receipt_detailsActiveRecord::Relation<ReceiptDetail>

All ReceiptDetails posted against this invoice excluding voided ones.

Returns:



509
510
511
# File 'app/models/invoice.rb', line 509

def non_voided_receipt_details
  receipt_details.non_voided
end

#not_rma?Boolean

Returns:

  • (Boolean)


724
725
726
# File 'app/models/invoice.rb', line 724

def not_rma?
  !(order.present? && order.funded_by_advance_replacement?)
end

#online_payment_optionsArray<String>

Payment options offered on the public pay-online page. Currently the
only gateway-driven option is credit card; checks/POs are out-of-band.

Returns:

  • (Array<String>)


469
470
471
# File 'app/models/invoice.rb', line 469

def online_payment_options
  [Payment::CREDIT_CARD]
end

#orderOrder

Returns:

See Also:



177
# File 'app/models/invoice.rb', line 177

belongs_to :order, optional: true, inverse_of: :invoices

#order_refString?

Reference number of the parent Order (or nil for MI/CI invoices
with no order). Used by EDI templates and the public pay link.

Returns:

  • (String, nil)


443
444
445
# File 'app/models/invoice.rb', line 443

def order_ref
  order.try(:reference_number)
end

#order_ref=(ref) ⇒ Order?

Setter pairing with #order_ref — looks up the Order by its
reference number so manual-entry forms can attach an invoice to an
existing order without exposing the integer primary key.

Parameters:

  • ref (String)

Returns:



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

def order_ref=(ref)
  self.order = Order.find_by(reference_number: ref) if ref.present?
end

#paymentsActiveRecord::Relation<Payment>

Returns:

  • (ActiveRecord::Relation<Payment>)

See Also:



202
# File 'app/models/invoice.rb', line 202

has_many   :payments, dependent: :nullify, inverse_of: :invoice

#po_numbersArray<String>

Distinct customer purchase-order numbers attached to Payments on
this invoice. Surfaced in the PDF header and EDI 810 PO segment.

Returns:

  • (Array<String>)


619
620
621
# File 'app/models/invoice.rb', line 619

def po_numbers
  payments.where.not(po_number: nil).distinct.pluck(:po_number)
end

#prevent_recalculate_shipping?Boolean

Returns:

  • (Boolean)


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

def prevent_recalculate_shipping?
  true
end

#pricing_program_discount_factorBigDecimal

Pricing-program discount multiplier — pulled from the parent Order
when one exists, otherwise from the Customer's tier. Used by
Models::Itemizable when re-evaluating discounts on edit.

Returns:

  • (BigDecimal)


536
537
538
539
540
541
542
# File 'app/models/invoice.rb', line 536

def pricing_program_discount_factor
  if order.present?
    order.pricing_program_discount_factor
  else
    customer.pricing_program_discount
  end
end

#primary_partyParty

Person/company this invoice is "addressed to" for activity attribution
and notifications — the parent Order's primary party when present,
otherwise the Customer.

Returns:



692
693
694
# File 'app/models/invoice.rb', line 692

def primary_party
  order&.primary_party || customer
end

#profileProfile

Returns:

See Also:



185
# File 'app/models/invoice.rb', line 185

belongs_to :profile, optional: true, inverse_of: :invoices

Public-facing pay-online URL for self-serve payment by the customer
(delegates to the parent Order's public-payment link). Nil when
the invoice has no order context (e.g. MI/CI).

Returns:

  • (String, nil)


709
710
711
712
713
714
715
716
717
718
# File 'app/models/invoice.rb', line 709

def public_pay_link
  # disabling authenticated links for now
  return nil unless order # && order.customer.present?

  # a = order.customer.account
  # return nil unless a.present?

  order.public_payment_link
  # "https://#{WEB_HOSTNAME}#{public_pay_path}"
end

Returns:

  • (Boolean)


720
721
722
# File 'app/models/invoice.rb', line 720

def public_pay_link_has_auth_token?
  false
end

#receipt_detailsActiveRecord::Relation<ReceiptDetail>

Returns:

See Also:



194
# File 'app/models/invoice.rb', line 194

has_many   :receipt_details, dependent: :nullify, inverse_of: :invoice

#receipts_totalBigDecimal

Sum of all credit applied to this invoice — cash Receipt amount
plus write-offs plus discount applied. Subtracted from #total to
produce #balance.

Returns:

  • (BigDecimal)


518
519
520
# File 'app/models/invoice.rb', line 518

def receipts_total
  non_voided_receipt_details.sum('amount') + non_voided_receipt_details.sum('write_off') + non_voided_receipt_details.sum('discount')
end

#rma_awaiting_return?Boolean

Returns:

  • (Boolean)


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

def rma_awaiting_return?
  order.try(:rma).try(:state) == 'awaiting_return'
end

#rma_numberString

RMA reference for this invoice — the linked Rma's number when one
exists, otherwise a synthesised "RMA # …" label from the order's
rma_reference. Surfaced on the customer-facing PDF for return shipments.

Returns:

  • (String)


377
378
379
# File 'app/models/invoice.rb', line 377

def rma_number
  order.try(:rma).try(:rma_number) || "RMA # #{order.rma_reference}"
end

#rmasActiveRecord::Relation<Rma>

Returns:

  • (ActiveRecord::Relation<Rma>)

See Also:



197
# File 'app/models/invoice.rb', line 197

has_many   :rmas, foreign_key: 'original_invoice_id', dependent: :destroy, inverse_of: :original_invoice

#selection_nameString

Display label for resource pickers / invoice dropdowns —
INV… <Customer Name>.

Returns:

  • (String)


983
984
985
# File 'app/models/invoice.rb', line 983

def selection_name
  "#{reference_number} #{customer.full_name}"
end

#selection_name_for_rmasString

Variant of #selection_name for the RMA picker that appends the
parent Order's reference when present, since RMAs are scoped to
an order, not just a customer.

Returns:

  • (String)


992
993
994
995
996
997
998
# File 'app/models/invoice.rb', line 992

def selection_name_for_rmas
  if order.nil?
    "#{reference_number} #{customer.full_name}"
  else
    "#{reference_number} #{customer.full_name} (#{order&.reference_number})"
  end
end

#service_line_itemsArray<LineItem>

Service-class LineItems only (tax_class 'svc'), e.g. SmartInstall
labour. Used by the consignment / service-only invoice rendering path.

Returns:



427
428
429
# File 'app/models/invoice.rb', line 427

def service_line_items
  line_items.where(cm_category: 'Item').select { |li| li.item.tax_class == 'svc' }
end

#set_consolidated_amountvoid

This method returns an undefined value.

Caches the consolidated-currency exchange rate for gl_date onto
#consolidated_exchange_rate, so downstream financial reports can
express the invoice in CONSOLIDATED_CURRENCY without re-querying
ExchangeRate. Sets the rate to 1.0 when the invoice currency
already matches the consolidated currency, or nil when fields aren't
populated yet.



1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
# File 'app/models/invoice.rb', line 1033

def set_consolidated_amount
  if currency && gl_date
    if currency == CONSOLIDATED_CURRENCY
      self.consolidated_exchange_rate = 1.0
    else
      exchange_rate = ExchangeRate.get_exchange_rate(currency, CONSOLIDATED_CURRENCY, gl_date)
      self.consolidated_exchange_rate = exchange_rate
    end
  else
    self.consolidated_exchange_rate = nil
  end
end

#shipping_addressAddress Also known as: destination_address

Returns:

See Also:

Validations:



180
# File 'app/models/invoice.rb', line 180

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

#shipping_costsActiveRecord::Relation<ShippingCost>

Returns:

See Also:



199
# File 'app/models/invoice.rb', line 199

has_many   :shipping_costs, -> { order(:cost) }, through: :delivery

#show_tax_info?Boolean

Returns:

  • (Boolean)


368
369
370
# File 'app/models/invoice.rb', line 368

def show_tax_info?
  tax_info.present?
end

#sold_to_billing_addressAddress

Returns:

See Also:



179
# File 'app/models/invoice.rb', line 179

belongs_to :sold_to_billing_address, class_name: 'Address', foreign_key: 'sold_to_billing_address', optional: true, inverse_of: :billing_invoices

#sourceSource

Returns:

See Also:



190
# File 'app/models/invoice.rb', line 190

belongs_to :source, optional: true, inverse_of: :invoices

#storeStore

Returns:

See Also:



186
# File 'app/models/invoice.rb', line 186

belongs_to :store, optional: true, inverse_of: :invoices

#tax_infoString?

Tax-identification line printed at the top of the invoice PDF — the
destination country's EU VAT number when shipping into the EU,
otherwise the company's tax info for Canada. Returns nil when no
tax identifier applies (e.g. US domestic).

Returns:

  • (String, nil)


343
344
345
346
347
348
349
350
351
352
353
354
355
356
# File 'app/models/invoice.rb', line 343

def tax_info
  destination_country = shipping_address&.country
  return unless destination_country

  if destination_country.eu_country?
    if destination_country.eu_vat_number.present?
      "VAT: #{destination_country.eu_vat_number}"
    else
      company.tax_info
    end
  elsif company.canada?
    company.tax_info
  end
end

#technical_support_repEmployee

Returns:

See Also:



191
# File 'app/models/invoice.rb', line 191

belongs_to :technical_support_rep, class_name: 'Employee', optional: true, inverse_of: :technical_support_invoices

#terms_in_daysObject

calculate net due date in days



557
558
559
560
561
# File 'app/models/invoice.rb', line 557

def terms_in_days
  return unless due_date

  (due_date - gl_date).to_i
end

#to_liquidLiquid::InvoiceDrop

Liquid drop wrapper used when this invoice is rendered into
transmission email/SMS templates — exposes only the safe-for-template
accessors via Liquid::InvoiceDrop.

Returns:



1062
1063
1064
# File 'app/models/invoice.rb', line 1062

def to_liquid
  Liquid::InvoiceDrop.new self
end

#to_sString

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

Returns:

  • (String)


1049
1050
1051
1052
1053
1054
1055
# File 'app/models/invoice.rb', line 1049

def to_s
  if respond_to?(:reference_number)
    "Invoice # #{reference_number}"
  else
    "Invoice ID #{id}"
  end
end

#uploadsActiveRecord::Relation<Upload>

Returns:

  • (ActiveRecord::Relation<Upload>)

See Also:



200
# File 'app/models/invoice.rb', line 200

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