Class: Invoice
- Inherits:
-
ApplicationRecord
- Object
- ActiveRecord::Base
- ApplicationRecord
- Invoice
- Includes:
- Models::AccountingDocumentTransmittable, Models::Auditable, Models::Itemizable, Models::LiquidMethods, Models::Notable, Models::Profitable, Models::TaxableResource, Models::TaxjarSubmittable, PgSearch::Model
- Defined in:
- app/models/invoice.rb
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 =
'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 =
[{ 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 Models::Schedulable
Models::Schedulable::SIMPLE_FORM_OPTIONS
Instance Attribute Summary collapse
-
#allow_duplicate_delivery_for_testing ⇒ Object
Returns the value of attribute allow_duplicate_delivery_for_testing.
-
#billing_address_id ⇒ Object
readonly
Validates presence of the core invoicing fields.
-
#customer_id ⇒ Object
readonly
Validates presence of the core invoicing fields.
-
#delivery_id ⇒ Object
readonly
Validates a delivery is invoiced at most once (unless explicitly allowed for testing).
-
#disable_auto_coupon ⇒ Object
Returns the value of attribute disable_auto_coupon.
-
#do_not_detect_shipping ⇒ Object
Returns the value of attribute do_not_detect_shipping.
-
#do_not_set_totals ⇒ Object
Returns the value of attribute do_not_set_totals.
-
#document_date ⇒ Object
readonly
Validates presence of the core invoicing fields.
-
#due_date ⇒ Object
readonly
Validates presence of the core invoicing fields.
-
#enter_new_address ⇒ Object
Returns the value of attribute enter_new_address.
-
#gl_date ⇒ Object
readonly
Validates presence of the core invoicing fields.
-
#gl_offset_account_id ⇒ Object
readonly
Validates presence of the core invoicing fields.
-
#gl_offset_account_ref ⇒ Object
Returns the value of attribute gl_offset_account_ref.
-
#invoice_type ⇒ Object
readonly
Validates presence of the core invoicing fields.
-
#order_id ⇒ Object
readonly
Validates an order is present for non-misc/non-counter invoice types.
-
#original_order_ref ⇒ Object
Returns the value of attribute original_order_ref.
-
#skip_initial_state_check ⇒ Object
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).
-
#skip_line_item_integrity_check ⇒ Object
Flag to skip integrity check during initial creation (set by CreateInvoiceFromDelivery).
-
#skip_pdf ⇒ Object
Returns the value of attribute skip_pdf.
-
#store_id ⇒ Object
readonly
Validates a store is present for misc/counter invoice types.
-
#tax_date ⇒ Object
readonly
Validates tax date and store are present for misc/counter invoice types.
-
#terms ⇒ Object
readonly
Validates presence of the core invoicing fields.
Attributes included from Models::Profitable
Attributes included from Models::Itemizable
#account_specialist, #coupons, #discounts, #force_total_reset, #local_sales_rep, #primary_sales_rep, #secondary_sales_rep, #total_reset
Belongs to collapse
-
#billing_address ⇒ Address
Address the invoice is billed to.
-
#billing_customer ⇒ Customer
Customer responsible for payment, when different from #customer.
-
#business_unit ⇒ BusinessUnit
Business unit the invoice is attributed to.
-
#buying_group ⇒ BuyingGroup
Buying group the invoice is attributed to.
-
#company ⇒ Company
Company (brand/entity) issuing the invoice.
-
#customer ⇒ Customer
Customer the invoice is issued to.
-
#delivery ⇒ Delivery
Delivery fulfilling this invoice.
-
#gl_offset_account ⇒ LedgerCompanyAccount
GL account used as the offset for this invoice's ledger postings.
-
#order ⇒ Order
Order this invoice bills.
-
#profile ⇒ Profile
Customer profile the invoice is attributed to.
-
#shipping_address ⇒ Address
Address the invoiced goods ship to.
-
#sold_to_billing_address ⇒ Address
Sold-to party's billing address, when different from #billing_address.
-
#source ⇒ Source
Source (marketing/order origin) of the invoice.
-
#store ⇒ Store
Store the invoice was issued by (misc/counter invoices).
-
#technical_support_rep ⇒ Employee
Technical support rep credited on the invoice.
Methods included from Models::TaxableResource
Methods included from Models::Auditable
Has many collapse
-
#activities ⇒ ActiveRecord::Relation<Activity>
Activity log entries for this invoice.
-
#attributed_opportunities ⇒ ActiveRecord::Relation<AttributedOpportunity>
Opportunities attributed through #outlet_purchases.
-
#communications ⇒ ActiveRecord::Relation<Communication>
Communications (emails, calls) logged against this invoice, most recent first.
-
#credit_memos ⇒ ActiveRecord::Relation<CreditMemo>
Credit memos issued against this invoice.
-
#drop_ship_purchase_orders ⇒ ActiveRecord::Relation<DropShipPurchaseOrder>
Drop-ship purchase orders raised from this invoice's delivery.
-
#edi_communication_logs ⇒ ActiveRecord::Relation<EdiCommunicationLog>
Communication logs from this invoice's EDI documents.
-
#edi_documents ⇒ ActiveRecord::Relation<EdiDocument>
EDI documents exchanged for this invoice.
-
#item_ledger_entries ⇒ ActiveRecord::Relation<ItemLedgerEntry>
Inventory ledger entries posted for this invoice.
-
#ledger_transactions ⇒ ActiveRecord::Relation<LedgerTransaction>
GL ledger transactions posted for this invoice.
-
#line_discounts ⇒ ActiveRecord::Relation<LineDiscount>
Discounts applied to individual line items.
-
#line_items ⇒ ActiveRecord::Relation<LineItem>
Invoice line items.
-
#outlet_purchases ⇒ ActiveRecord::Relation<CustomerOutletPurchase>
Opportunities a rep worked that this outlet invoice satisfied — see doc/tasks/202608081330_OUTLET_PURCHASE_ATTRIBUTION.md.
-
#payments ⇒ ActiveRecord::Relation<Payment>
Payments applied to this invoice.
-
#receipt_details ⇒ ActiveRecord::Relation<ReceiptDetail>
Payment receipt details applied to this invoice.
-
#rmas ⇒ ActiveRecord::Relation<Rma>
RMAs issued against this invoice.
-
#shipping_costs ⇒ ActiveRecord::Relation<ShippingCost>
Shipping costs from the delivery, cheapest first.
-
#uploads ⇒ ActiveRecord::Relation<Upload>
Uploaded files attached to this invoice, most recent first.
Delegated Instance Attributes collapse
-
#balance_positive? ⇒ Object
Alias for Balance#positive?.
-
#marketplace_invoice_format ⇒ Object
Alias for Customer#marketplace_invoice_format.
Class Method Summary collapse
-
.awaiting_transmission ⇒ ActiveRecord::Relation<Invoice>
A relation of Invoices that are awaiting transmission.
-
.calculate_due_date(order, delivery) ⇒ Date
Computes the invoice due date as
delivery.shipped_date + billing_entity.terms_in_days. -
.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. -
.included_in_notifications ⇒ ActiveRecord::Relation<Invoice>
A relation of Invoices that are included in notifications.
-
.like_lookup ⇒ ActiveRecord::Relation<Invoice>
A relation of Invoices that are like lookup.
-
.lookup ⇒ ActiveRecord::Relation<Invoice>
A relation of Invoices that are lookup.
-
.missing_edi_810 ⇒ ActiveRecord::Relation<Invoice>
A relation of Invoices that are missing edi 810.
-
.overdue ⇒ ActiveRecord::Relation<Invoice>
A relation of Invoices that are overdue.
-
.sales_orders ⇒ ActiveRecord::Relation<Invoice>
A relation of Invoices that are sales orders.
-
.unpaid ⇒ ActiveRecord::Relation<Invoice>
A relation of Invoices that are unpaid.
Instance Method Summary collapse
-
#alert_unless_prepaid_balance_reconciles(invoice_total) ⇒ void
Everything on this SO invoice was captured before invoicing started — routine for split orders, where an earlier delivery's capture precedes this delivery's invoice (INV012607425 / PI pi_3TwiaK, 2026-07-28: one PI multicaptured across two deliveries, both sides exact).
-
#allow_duplicate_delivery_for_testing? ⇒ Boolean
Whether duplicate deliveries are allowed (test-only escape hatch).
-
#amount_due ⇒ BigDecimal
Liquid-template alias for
#total; kept distinct from #balance so the public pay-link template can show the original amount even after partial payments. -
#attributed_captured_total ⇒ Float
Captured money attributable to THIS invoice through receipts: what each captured payment's receipts applied here, plus any captured money its receipts do NOT explain (unapplied remainders and captures never receipted) — unexplained money is exactly what a real overcharge looks like (payment 288279 / invoice 275115: $478.90 sat unapplied on a partially_applied receipt).
-
#balance ⇒ BigDecimal
Outstanding balance — invoice total minus the sum of applied ReceiptDetails (cash receipts, write-offs, applied discounts).
-
#balance_is_zero? ⇒ Boolean
Whether the invoice balance has been fully settled.
- #billing_entity ⇒ Party
- #build_activity ⇒ Activity
-
#calculate_all_cogs ⇒ BigDecimal
Cost-of-goods total computed in SQL across every
Item-category LineItem, ignoring tax-class filtering. -
#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). -
#capture_funds? ⇒ Boolean
Captures outstanding authorized payments for this invoice; logs progress.
-
#chosen_shipping_cost ⇒ BigDecimal
Numeric cost of #chosen_shipping_method, or
0.00when the carrier uses a customer-supplied shipping account number (no charge to bill). -
#chosen_shipping_method ⇒ ShippingCost?
Cheapest available ShippingCost on the parent Delivery — what the customer is actually being charged for shipping on this invoice.
-
#combined_terms ⇒ String
Terms label augmented with the early-payment offer in the standard "Net X - Y%/Z" notation (e.g.
Net 30 - 2%/10). - #copy_payments ⇒ void
- #create_receipts_for_captured_payments ⇒ void
-
#crm_link ⇒ String
CRM URL for the invoice show page.
-
#currency_symbol ⇒ String
Currency symbol (e.g.
$,€) for this invoice's#currency, used in the PDF and overpayment notification templates. -
#customer_name ⇒ String?
Display name for the Customer this invoice bills, falling back to nil for headless data (rare — most invoices have a customer).
-
#destination_address ⇒ Object
(An explicit reader rather than
alias destination_address shipping_address— YARD's alias handler crashes copying the yard-activerecord association docstring during the docs build.). -
#disable_auto_coupon? ⇒ Boolean
Whether automatic coupon application is suppressed.
-
#discount_applied ⇒ BigDecimal
Sum of
discountwritten off via ReceiptDetails, e.g. -
#discount_days_due ⇒ Integer?
Days between #gl_date and #early_payment_due_date; used by the PDF "early-payment discount" line.
-
#do_not_detect_shipping? ⇒ Boolean
Whether automatic shipping detection is suppressed.
-
#early_payment_amount ⇒ BigDecimal
Dollar value of the early-payment discount —
early_payment_discount% of the invoice total, rounded to 2dp. -
#early_payment_due_date ⇒ Date?
Date by which the customer must pay to qualify for the early-payment discount (
shipped_date + early_payment_timescaledays). -
#early_payment_total ⇒ BigDecimal
Discounted total the customer would owe if they pay by #early_payment_due_date — total minus #early_payment_amount.
-
#editing_locked? ⇒ Boolean
Whether the invoice is locked from editing (already unpaid-out or paid).
- #effective_store ⇒ Store?
-
#file_name(with_extension: true) ⇒ String
Filename used when attaching the invoice PDF to email or storing in S3.
-
#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.
-
#fully_funded_by_rma? ⇒ Boolean
Whether this invoice is fully funded by an advance-replacement RMA.
-
#funded_by_cod? ⇒ Boolean
Whether the invoice terms are cash-on-delivery.
-
#funded_by_rma? ⇒ Boolean
Whether this invoice is funded by an advance-replacement RMA.
-
#generate_pdf ⇒ Upload
Renders a fresh combined invoice PDF (cover + line-item pages + any addendums) via Invoicing::CombinedPdfGenerator, uploads it to S3 under the
invoice_pdfcategory and attaches the Upload. -
#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.
-
#name ⇒ String
Short display name for selection lists and links — just the reference number.
-
#non_service_line_items ⇒ Array<LineItem>
Goods/shipping LineItems only — services (tax_class 'svc') excluded.
-
#non_voided_receipt_details ⇒ ActiveRecord::Relation<ReceiptDetail>
All ReceiptDetails posted against this invoice excluding voided ones.
-
#not_rma? ⇒ Boolean
Whether this invoice is NOT funded by an advance-replacement RMA.
-
#online_payment_options ⇒ Array<String>
Payment options offered on the public pay-online page.
- #order_ref ⇒ String?
-
#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.
-
#po_numbers ⇒ Array<String>
Distinct customer purchase-order numbers attached to Payments on this invoice.
-
#prevent_recalculate_shipping? ⇒ Boolean
Whether shipping recalculation is suppressed on save.
- #pricing_program_discount_factor ⇒ BigDecimal
- #primary_party ⇒ Party
-
#public_pay_link ⇒ String?
Public-facing pay-online URL for self-serve payment by the customer (delegates to the parent Order's public-payment link).
-
#public_pay_link_has_auth_token? ⇒ Boolean
Whether the public pay link carries an auth token (currently always false).
-
#receipts_total ⇒ BigDecimal
Sum of all credit applied to this invoice — cash Receipt amount plus write-offs plus discount applied.
-
#rma_awaiting_return? ⇒ Boolean
Whether the linked RMA is still awaiting the returned goods.
-
#rma_number ⇒ String
RMA reference for this invoice — the linked Rma's number when one exists, otherwise a synthesised "RMA # …" label from the order's
rma_reference. -
#selection_name ⇒ String
Display label for resource pickers / invoice dropdowns —
INV… <Customer Name>. -
#selection_name_for_rmas ⇒ String
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.
-
#service_line_items ⇒ Array<LineItem>
Service-class LineItems only (tax_class 'svc'), e.g.
-
#set_consolidated_amount ⇒ void
Caches the consolidated-currency exchange rate for
gl_dateonto#consolidated_exchange_rate, so downstream financial reports can express the invoice inCONSOLIDATED_CURRENCYwithout re-querying ExchangeRate. -
#show_tax_info? ⇒ Boolean
Whether tax info (VAT number / company tax info) should be rendered.
-
#tax_info ⇒ String?
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.
-
#terms_in_days ⇒ Object
calculate net due date in days.
-
#to_liquid ⇒ Liquid::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.
-
#to_s ⇒ String
Human-readable identifier used in audit logs and error messages.
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
Methods inherited from ApplicationRecord
ransackable_associations, ransackable_attributes, ransackable_scopes, ransortable_attributes, #to_relation
Methods included from Models::Schedulable
Methods included from Models::AfterCommittable
Methods included from Models::EventPublishable
Instance Attribute Details
#allow_duplicate_delivery_for_testing ⇒ Object
Returns the value of attribute allow_duplicate_delivery_for_testing.
174 175 176 |
# File 'app/models/invoice.rb', line 174 def allow_duplicate_delivery_for_testing @allow_duplicate_delivery_for_testing end |
#billing_address_id ⇒ Object (readonly)
Validates presence of the core invoicing fields.
Validations:
246 |
# File 'app/models/invoice.rb', line 246 validates :due_date, :terms, :invoice_type, :billing_address_id, :customer_id, :document_date, :gl_date, :gl_offset_account_id, presence: true |
#customer_id ⇒ Object (readonly)
Validates presence of the core invoicing fields.
Validations:
246 |
# File 'app/models/invoice.rb', line 246 validates :due_date, :terms, :invoice_type, :billing_address_id, :customer_id, :document_date, :gl_date, :gl_offset_account_id, presence: true |
#delivery_id ⇒ Object (readonly)
Validates a delivery is invoiced at most once (unless explicitly allowed for testing).
Validations (unless => #allow_duplicate_delivery_for_testing? ):
- Uniqueness ({ allow_nil: true })
256 |
# File 'app/models/invoice.rb', line 256 validates :delivery_id, uniqueness: { allow_nil: true }, unless: :allow_duplicate_delivery_for_testing? |
#disable_auto_coupon ⇒ Object
Returns the value of attribute disable_auto_coupon.
174 175 176 |
# File 'app/models/invoice.rb', line 174 def disable_auto_coupon @disable_auto_coupon end |
#do_not_detect_shipping ⇒ Object
Returns the value of attribute do_not_detect_shipping.
174 175 176 |
# File 'app/models/invoice.rb', line 174 def do_not_detect_shipping @do_not_detect_shipping end |
#do_not_set_totals ⇒ Object
Returns the value of attribute do_not_set_totals.
174 175 176 |
# File 'app/models/invoice.rb', line 174 def do_not_set_totals @do_not_set_totals end |
#document_date ⇒ Object (readonly)
Validates presence of the core invoicing fields.
Validations:
246 |
# File 'app/models/invoice.rb', line 246 validates :due_date, :terms, :invoice_type, :billing_address_id, :customer_id, :document_date, :gl_date, :gl_offset_account_id, presence: true |
#due_date ⇒ Object (readonly)
Validates presence of the core invoicing fields.
Validations:
246 |
# File 'app/models/invoice.rb', line 246 validates :due_date, :terms, :invoice_type, :billing_address_id, :customer_id, :document_date, :gl_date, :gl_offset_account_id, presence: true |
#enter_new_address ⇒ Object
Returns the value of attribute enter_new_address.
174 175 176 |
# File 'app/models/invoice.rb', line 174 def enter_new_address @enter_new_address end |
#gl_date ⇒ Object (readonly)
Validates presence of the core invoicing fields.
Validations:
246 |
# File 'app/models/invoice.rb', line 246 validates :due_date, :terms, :invoice_type, :billing_address_id, :customer_id, :document_date, :gl_date, :gl_offset_account_id, presence: true |
#gl_offset_account_id ⇒ Object (readonly)
Validates presence of the core invoicing fields.
Validations:
246 |
# File 'app/models/invoice.rb', line 246 validates :due_date, :terms, :invoice_type, :billing_address_id, :customer_id, :document_date, :gl_date, :gl_offset_account_id, presence: true |
#gl_offset_account_ref ⇒ Object
Returns the value of attribute gl_offset_account_ref.
174 175 176 |
# File 'app/models/invoice.rb', line 174 def gl_offset_account_ref @gl_offset_account_ref end |
#invoice_type ⇒ Object (readonly)
Validates presence of the core invoicing fields.
Validations:
246 |
# File 'app/models/invoice.rb', line 246 validates :due_date, :terms, :invoice_type, :billing_address_id, :customer_id, :document_date, :gl_date, :gl_offset_account_id, presence: true |
#order_id ⇒ Object (readonly)
Validates an order is present for non-misc/non-counter invoice types.
Validations:
- Presence ({ if: proc { |i| [MI, CI].exclude?(i.invoice_type) } })
- Numericality ({ allow_nil: true })
248 |
# File 'app/models/invoice.rb', line 248 validates :order_id, presence: { if: proc { |i| [MI, CI].exclude?(i.invoice_type) } } |
#original_order_ref ⇒ Object
Returns the value of attribute original_order_ref.
174 175 176 |
# File 'app/models/invoice.rb', line 174 def original_order_ref @original_order_ref end |
#skip_initial_state_check ⇒ Object
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)
295 296 297 |
# File 'app/models/invoice.rb', line 295 def skip_initial_state_check @skip_initial_state_check end |
#skip_line_item_integrity_check ⇒ Object
Flag to skip integrity check during initial creation (set by CreateInvoiceFromDelivery)
291 292 293 |
# File 'app/models/invoice.rb', line 291 def skip_line_item_integrity_check @skip_line_item_integrity_check end |
#skip_pdf ⇒ Object
Returns the value of attribute skip_pdf.
174 175 176 |
# File 'app/models/invoice.rb', line 174 def skip_pdf @skip_pdf end |
#store_id ⇒ Object (readonly)
Validates a store is present for misc/counter invoice types.
Validations:
- Presence ({ if: proc { |i| [MI, CI].include?(i.invoice_type) } })
- Presence ({ if: proc { |i| [MI, CI].include?(i.invoice_type) } })
250 |
# File 'app/models/invoice.rb', line 250 validates :store_id, presence: { if: proc { |i| [MI, CI].include?(i.invoice_type) } } |
#tax_date ⇒ Object (readonly)
Validates tax date and store are present for misc/counter invoice types.
Validations:
- Presence ({ if: proc { |i| [MI, CI].include?(i.invoice_type) } })
252 |
# File 'app/models/invoice.rb', line 252 validates :tax_date, :store_id, presence: { if: proc { |i| [MI, CI].include?(i.invoice_type) } } |
#terms ⇒ Object (readonly)
Validates presence of the core invoicing fields.
Validations:
246 |
# File 'app/models/invoice.rb', line 246 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_transmission ⇒ ActiveRecord::Relation<Invoice>
A relation of Invoices that are awaiting transmission. Active Record Scope
305 |
# File 'app/models/invoice.rb', line 305 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.
607 608 609 610 |
# File 'app/models/invoice.rb', line 607 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.
634 635 636 637 638 |
# File 'app/models/invoice.rb', line 634 def self.calculate_terms(order) terms = order.billing_entity.terms terms += ' (COD)' if order.funded_by_cod? terms end |
.included_in_notifications ⇒ ActiveRecord::Relation<Invoice>
A relation of Invoices that are included in notifications. Active Record Scope
308 |
# File 'app/models/invoice.rb', line 308 scope :included_in_notifications, -> { where(exclude_fund_capture_notification: false) } |
.like_lookup ⇒ ActiveRecord::Relation<Invoice>
A relation of Invoices that are like lookup. Active Record Scope
311 |
# File 'app/models/invoice.rb', line 311 scope :like_lookup, ->(q) { left_joins(:order).where(Invoice[:reference_number].matches("%#{q}%")).or(Order.where(Order[:reference_number].matches("%#{q}%"))) } |
.lookup ⇒ ActiveRecord::Relation<Invoice>
A relation of Invoices that are lookup. Active Record Scope
310 |
# File 'app/models/invoice.rb', line 310 scope :lookup, ->(q) { where(reference_number: q) } |
.missing_edi_810 ⇒ ActiveRecord::Relation<Invoice>
A relation of Invoices that are missing edi 810. Active Record Scope
312 313 314 315 316 |
# File 'app/models/invoice.rb', line 312 scope :missing_edi_810, -> { joins(customer: :notification_channels) .where(notification_channels: { notification_type: NotificationChannel::INVOICES, transmission_type: NotificationChannel::EDI }) .where(transmission_state: 'awaiting_transmission') } |
.overdue ⇒ ActiveRecord::Relation<Invoice>
A relation of Invoices that are overdue. Active Record Scope
309 |
# File 'app/models/invoice.rb', line 309 scope :overdue, -> { unpaid.where(due_date: ...Date.current) } |
.sales_orders ⇒ ActiveRecord::Relation<Invoice>
A relation of Invoices that are sales orders. Active Record Scope
306 |
# File 'app/models/invoice.rb', line 306 scope :sales_orders, -> { where(invoice_type: 'SO') } |
.unpaid ⇒ ActiveRecord::Relation<Invoice>
A relation of Invoices that are unpaid. Active Record Scope
307 |
# File 'app/models/invoice.rb', line 307 scope :unpaid, -> { where(state: 'unpaid') } |
Instance Method Details
#activities ⇒ ActiveRecord::Relation<Activity>
Activity log entries for this invoice.
225 |
# File 'app/models/invoice.rb', line 225 has_many :activities, as: :resource, dependent: :nullify, inverse_of: :resource |
#alert_unless_prepaid_balance_reconciles(invoice_total) ⇒ void
This method returns an undefined value.
Everything on this SO invoice was captured before invoicing started —
routine for split orders, where an earlier delivery's capture precedes
this delivery's invoice (INV012607425 / PI pi_3TwiaK, 2026-07-28: one PI
multicaptured across two deliveries, both sides exact). Asking AR to
"check all payments are correctly captured" fired ~5x a week for
reconciliations the system can verify itself, so only email when the
receipts genuinely don't account for the invoice total.
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 |
# File 'app/models/invoice.rb', line 839 def alert_unless_prepaid_balance_reconciles(invoice_total) attributed_total = attributed_captured_total unaccounted = (invoice_total - attributed_total).round(2) if unaccounted.zero? logger.info("#{Time.current}: Invoice #{id}: balance already paid before invoicing; receipts attribute #{attributed_total} — reconciled, no alert") return end currency_sym = Money::Currency.new(currency).symbol Mailer.generic_mailer( from: ADMINISTRATOR_EMAIL, to: "#{ADMINISTRATOR_EMAIL},#{ACCOUNTS_RECEIVABLE_EMAIL}", subject: "BALANCE ALREADY PAID FOR INVOICE ID ##{id}", message: "Invoice ##{reference_number} (ID ##{id}) was fully covered by payments captured before invoicing, " \ "but its receipts don't account for the whole total.\n\n" \ "Invoice total: #{currency_sym}#{'%.2f' % invoice_total}\n" \ "Attributed by receipts: #{currency_sym}#{'%.2f' % attributed_total}\n" \ "Unaccounted: #{currency_sym}#{'%.2f' % unaccounted}\n\n" \ "Customer: #{customer&.full_name} (ID: #{customer_id})\n" \ "CRM link: #{crm_link}\n\n" \ 'Check that every payment on this order is captured and applied to the right invoice.', no_verbage: true ).deliver logger.warn("#{Time.current}: Invoice #{id}: balance already paid but receipts attribute #{attributed_total} of #{invoice_total} (unaccounted #{unaccounted})") end |
#allow_duplicate_delivery_for_testing? ⇒ Boolean
Whether duplicate deliveries are allowed (test-only escape hatch).
260 261 262 |
# File 'app/models/invoice.rb', line 260 def allow_duplicate_delivery_for_testing? allow_duplicate_delivery_for_testing == true end |
#amount_due ⇒ BigDecimal
Liquid-template alias for #total; kept distinct from #balance so
the public pay-link template can show the original amount even after
partial payments.
583 584 585 |
# File 'app/models/invoice.rb', line 583 def amount_due total end |
#attributed_captured_total ⇒ Float
Captured money attributable to THIS invoice through receipts: what each
captured payment's receipts applied here, plus any captured money its
receipts do NOT explain (unapplied remainders and captures never
receipted) — unexplained money is exactly what a real overcharge looks
like (payment 288279 / invoice 275115: $478.90 sat unapplied on a
partially_applied receipt). A payment legitimately split across sibling
invoices attributes only this invoice's share, so split applications no
longer read as overpayment.
816 817 818 819 820 821 822 823 824 825 826 827 |
# File 'app/models/invoice.rb', line 816 def attributed_captured_total candidates = payments.all_captured.or(payments.partially_captured) candidates.sum do |payment| receipt_ids = payment.receipts.where.not(state: 'voided').select(:id) details = ReceiptDetail.non_voided.where(receipt_id: receipt_ids) detail_amounts = details.pluck(:invoice_id, :amount) applied_here = detail_amounts.sum { |invoice_id, amount| invoice_id == id ? amount.to_f : 0.0 } applied_anywhere = detail_amounts.sum { |_invoice_id, amount| amount.to_f } unexplained = [(payment.total_captured - payment.total_refunded) - applied_anywhere, 0].max applied_here + unexplained end.round(2) end |
#attributed_opportunities ⇒ ActiveRecord::Relation<AttributedOpportunity>
Opportunities attributed through #outlet_purchases.
234 |
# File 'app/models/invoice.rb', line 234 has_many :attributed_opportunities, through: :outlet_purchases, source: :opportunity |
#balance ⇒ BigDecimal
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.
550 551 552 |
# File 'app/models/invoice.rb', line 550 def balance total - receipts_total end |
#balance_is_zero? ⇒ Boolean
Whether the invoice balance has been fully settled.
541 542 543 |
# File 'app/models/invoice.rb', line 541 def balance_is_zero? balance.zero? end |
#balance_positive? ⇒ Object
Alias for Balance#positive?
327 |
# File 'app/models/invoice.rb', line 327 delegate :positive?, to: :balance, prefix: true, allow_nil: true |
#billing_address ⇒ Address
Address the invoice is billed to.
179 |
# File 'app/models/invoice.rb', line 179 belongs_to :billing_address, class_name: 'Address', optional: true, inverse_of: :billing_invoices |
#billing_customer ⇒ Customer
Customer responsible for payment, when different from #customer.
187 |
# File 'app/models/invoice.rb', line 187 belongs_to :billing_customer, class_name: 'Customer', optional: true, inverse_of: :invoices |
#billing_entity ⇒ Party
1169 1170 1171 |
# File 'app/models/invoice.rb', line 1169 def billing_entity billing_address.party end |
#build_activity ⇒ Activity
440 441 442 |
# File 'app/models/invoice.rb', line 440 def build_activity activities.build resource: self, party: primary_party end |
#business_unit ⇒ BusinessUnit
Business unit the invoice is attributed to.
199 |
# File 'app/models/invoice.rb', line 199 belongs_to :business_unit, optional: true, inverse_of: :invoices |
#buying_group ⇒ BuyingGroup
Buying group the invoice is attributed to.
191 |
# File 'app/models/invoice.rb', line 191 belongs_to :buying_group, optional: true, inverse_of: :invoices |
#calculate_all_cogs ⇒ BigDecimal
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.
460 461 462 |
# File 'app/models/invoice.rb', line 460 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.
451 452 453 |
# File 'app/models/invoice.rb', line 451 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
Captures outstanding authorized payments for this invoice; logs progress.
867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 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 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 |
# File 'app/models/invoice.rb', line 867 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 # State-captured payments count by amount (kept equal to captured funds # by the final-capture-short path); partially captured ones are still # `authorized`, so their real captured money must be summed from their # capture transactions or the invoice looks unpaid and gets re-captured. captured_balance = payments.all_captured.sum(:amount) + payments.partially_captured.sum(&:total_captured) pending_balance = invoice_total - captured_balance payments_total = payments..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? # A payment-level negative balance is only a REAL overpayment when the # receipt-level attribution agrees — a payment legitimately split # across sibling invoices (one receipt, two invoice details) carries # its full amount into captured_balance here but only this invoice's # share in attributed_captured_total (false alarm: INV012607350 / # payment 288251, 2026-07-24 — $724.68 was applied to the sibling). attributed_total = attributed_captured_total overpaid_amount = (attributed_total - invoice_total).round(2) if overpaid_amount.positive? 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}: attributed #{attributed_total} exceeds total #{invoice_total} by #{overpaid_amount}") else logger.info("#{Time.current}: Invoice #{id}: payment-level captured #{captured_balance} exceeds total #{invoice_total}, but receipts attribute only #{attributed_total} here (split application) — no overpayment") end elsif invoice_type == 'SO' alert_unless_prepaid_balance_reconciles(invoice_total) 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..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 # Cap at the payment's uncaptured remainder — a partially captured # (multicapture) payment is still `authorized`, and requesting its # full amount would exceed what Stripe allows and re-request money # already captured. amount = [pending_balance, payment.amount - payment.total_captured].min next unless amount.positive? 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 = payments..select { |p| p.receipts.empty? } only_manual_payments = .all? { |p| Payment::CATEGORIES_NOT_ALLOWING_CAPTURE.include?(p.category) } if only_manual_payments && .any? logger.info("#{Time.current}: Authorized payments exist that require manual processing (#{.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_cost ⇒ BigDecimal
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.
653 654 655 656 657 658 659 660 661 |
# File 'app/models/invoice.rb', line 653 def chosen_shipping_cost cost = BigDecimal('0.00') begin cost = chosen_shipping_method.cost unless chosen_shipping_method.shipping_account_number rescue StandardError => e Rails.logger.warn "Could not get shipping cost for invoice #{id}: #{e.}" end cost end |
#chosen_shipping_method ⇒ ShippingCost?
Cheapest available ShippingCost on the parent Delivery — what
the customer is actually being charged for shipping on this invoice.
644 645 646 |
# File 'app/models/invoice.rb', line 644 def chosen_shipping_method shipping_costs.first end |
#combined_terms ⇒ String
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.
1115 1116 1117 1118 1119 1120 1121 |
# File 'app/models/invoice.rb', line 1115 def combined_terms if early_payment_discount && early_payment_timescale "#{terms} - #{early_payment_discount}%/#{early_payment_timescale}" else terms end end |
#communications ⇒ ActiveRecord::Relation<Communication>
Communications (emails, calls) logged against this invoice, most recent first.
229 |
# File 'app/models/invoice.rb', line 229 has_many :communications, -> { order(:id).reverse_order }, as: :resource, dependent: :nullify, inverse_of: :resource |
#company ⇒ Company
Company (brand/entity) issuing the invoice.
189 |
# File 'app/models/invoice.rb', line 189 belongs_to :company, inverse_of: :invoices |
#copy_payments ⇒ void
533 534 535 536 537 |
# File 'app/models/invoice.rb', line 533 def copy_payments delivery.payments.where(currency: currency).find_each do |pp| pp.update!(invoice_id: id) if pp. || (pp..in?(%w[credit_card check paypal_invoice amazon_pay]) && pp.captured?) end end |
#create_receipts_for_captured_payments ⇒ void
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.
1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 |
# File 'app/models/invoice.rb', line 1032 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.}" 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_memos ⇒ ActiveRecord::Relation<CreditMemo>
Credit memos issued against this invoice.
219 |
# File 'app/models/invoice.rb', line 219 has_many :credit_memos, foreign_key: 'original_invoice_id', dependent: :destroy, inverse_of: :original_invoice |
#crm_link ⇒ String
CRM URL for the invoice show page. Used by activity/comm logs and
admin notification emails so reps can jump straight to the record.
756 757 758 |
# File 'app/models/invoice.rb', line 756 def crm_link UrlHelper.instance.invoice_path(self) end |
#currency_symbol ⇒ String
Currency symbol (e.g. $, €) for this invoice's #currency, used
in the PDF and overpayment notification templates.
558 559 560 |
# File 'app/models/invoice.rb', line 558 def currency_symbol Money::Currency.new(currency).symbol end |
#customer ⇒ Customer
Customer the invoice is issued to.
185 |
# File 'app/models/invoice.rb', line 185 belongs_to :customer, optional: true, inverse_of: :invoices |
#customer_name ⇒ String?
Display name for the Customer this invoice bills, falling back to
nil for headless data (rare — most invoices have a customer).
515 516 517 |
# File 'app/models/invoice.rb', line 515 def customer_name customer.try(:full_name) end |
#delivery ⇒ Delivery
Delivery fulfilling this invoice.
201 |
# File 'app/models/invoice.rb', line 201 belongs_to :delivery, optional: true, inverse_of: :invoices |
#destination_address ⇒ Object
(An explicit reader rather than alias destination_address shipping_address —
YARD's alias handler crashes copying the yard-activerecord association
docstring during the docs build.)
303 |
# File 'app/models/invoice.rb', line 303 def destination_address = shipping_address |
#disable_auto_coupon? ⇒ Boolean
Whether automatic coupon application is suppressed.
1160 1161 1162 |
# File 'app/models/invoice.rb', line 1160 def disable_auto_coupon? true end |
#discount_applied ⇒ BigDecimal
Sum of discount written off via ReceiptDetails, e.g. early-payment
discounts or accounting write-downs. Reported on the invoice ledger tab.
1148 1149 1150 |
# File 'app/models/invoice.rb', line 1148 def discount_applied receipt_details.sum(:discount) end |
#discount_days_due ⇒ Integer?
Days between #gl_date and #early_payment_due_date; used by the
PDF "early-payment discount" line. Nil when no early-payment offer applies.
623 624 625 626 627 |
# File 'app/models/invoice.rb', line 623 def discount_days_due return unless early_payment_due_date (early_payment_due_date - gl_date).to_i end |
#do_not_detect_shipping? ⇒ Boolean
Whether automatic shipping detection is suppressed.
1154 1155 1156 |
# File 'app/models/invoice.rb', line 1154 def do_not_detect_shipping? true end |
#drop_ship_purchase_orders ⇒ ActiveRecord::Relation<DropShipPurchaseOrder>
Drop-ship purchase orders raised from this invoice's delivery.
236 |
# File 'app/models/invoice.rb', line 236 has_many :drop_ship_purchase_orders, -> { order(:id) }, through: :delivery, dependent: :destroy |
#early_payment_amount ⇒ BigDecimal
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.
1077 1078 1079 1080 1081 1082 1083 |
# File 'app/models/invoice.rb', line 1077 def early_payment_amount if early_payment_discount.blank? BigDecimal(0) else ((early_payment_discount * total) / 100).round(2) end end |
#early_payment_due_date ⇒ Date?
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.
1090 1091 1092 1093 1094 1095 1096 |
# File 'app/models/invoice.rb', line 1090 def early_payment_due_date if early_payment_timescale.blank? nil else shipped_date + early_payment_timescale.days end end |
#early_payment_total ⇒ BigDecimal
Discounted total the customer would owe if they pay by
#early_payment_due_date — total minus #early_payment_amount.
1102 1103 1104 1105 1106 1107 1108 |
# File 'app/models/invoice.rb', line 1102 def early_payment_total if early_payment_discount.zero? total else total - early_payment_amount end end |
#edi_communication_logs ⇒ ActiveRecord::Relation<EdiCommunicationLog>
Communication logs from this invoice's EDI documents.
240 |
# File 'app/models/invoice.rb', line 240 has_many :edi_communication_logs, through: :edi_documents, dependent: :destroy |
#edi_documents ⇒ ActiveRecord::Relation<EdiDocument>
EDI documents exchanged for this invoice.
238 |
# File 'app/models/invoice.rb', line 238 has_many :edi_documents, dependent: :destroy, inverse_of: :invoice |
#editing_locked? ⇒ Boolean
Whether the invoice is locked from editing (already unpaid-out or paid).
489 490 491 |
# File 'app/models/invoice.rb', line 489 def editing_locked? unpaid? || paid? end |
#effective_store ⇒ Store?
410 411 412 |
# File 'app/models/invoice.rb', line 410 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.
724 725 726 |
# File 'app/models/invoice.rb', line 724 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.
685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 |
# File 'app/models/invoice.rb', line 685 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.}" 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
Whether this invoice is fully funded by an advance-replacement RMA.
796 797 798 |
# File 'app/models/invoice.rb', line 796 def fully_funded_by_rma? order.present? && order.fully_funded_by_advance_replacement? end |
#funded_by_cod? ⇒ Boolean
Whether the invoice terms are cash-on-delivery.
802 803 804 |
# File 'app/models/invoice.rb', line 802 def funded_by_cod? terms.include?('COD') end |
#funded_by_rma? ⇒ Boolean
Whether this invoice is funded by an advance-replacement RMA.
790 791 792 |
# File 'app/models/invoice.rb', line 790 def funded_by_rma? order.present? && order.funded_by_advance_replacement? end |
#generate_pdf ⇒ Upload
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.
733 734 735 736 737 738 739 740 741 |
# File 'app/models/invoice.rb', line 733 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.
708 709 710 711 712 713 714 715 716 717 718 |
# File 'app/models/invoice.rb', line 708 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_account ⇒ LedgerCompanyAccount
GL account used as the offset for this invoice's ledger postings.
197 |
# File 'app/models/invoice.rb', line 197 belongs_to :gl_offset_account, class_name: 'LedgerCompanyAccount', optional: true, inverse_of: :invoices |
#item_ledger_entries ⇒ ActiveRecord::Relation<ItemLedgerEntry>
Inventory ledger entries posted for this invoice.
215 |
# File 'app/models/invoice.rb', line 215 has_many :item_ledger_entries, dependent: :destroy, inverse_of: :invoice |
#ledger_transactions ⇒ ActiveRecord::Relation<LedgerTransaction>
GL ledger transactions posted for this invoice.
213 |
# File 'app/models/invoice.rb', line 213 has_many :ledger_transactions, dependent: :destroy, inverse_of: :invoice |
#line_discounts ⇒ ActiveRecord::Relation<LineDiscount>
Discounts applied to individual line items.
209 |
# File 'app/models/invoice.rb', line 209 has_many :line_discounts, through: :line_items |
#line_items ⇒ ActiveRecord::Relation<LineItem>
Invoice line items.
207 |
# File 'app/models/invoice.rb', line 207 has_many :line_items, as: :resource, inverse_of: :resource, dependent: :destroy, extend: LineItemExtension, autosave: true |
#marketplace_invoice_format ⇒ Object
Alias for Customer#marketplace_invoice_format
326 |
# File 'app/models/invoice.rb', line 326 delegate :marketplace_invoice_format, to: :customer, allow_nil: true |
#name ⇒ String
Short display name for selection lists and links — just the
reference number.
667 668 669 |
# File 'app/models/invoice.rb', line 667 def name reference_number end |
#non_service_line_items ⇒ Array<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.
469 470 471 |
# File 'app/models/invoice.rb', line 469 def non_service_line_items line_items.where(cm_category: 'Item').reject { |li| li.item.tax_class == 'svc' } end |
#non_voided_receipt_details ⇒ ActiveRecord::Relation<ReceiptDetail>
All ReceiptDetails posted against this invoice excluding voided ones.
565 566 567 |
# File 'app/models/invoice.rb', line 565 def non_voided_receipt_details receipt_details.non_voided end |
#not_rma? ⇒ Boolean
Whether this invoice is NOT funded by an advance-replacement RMA.
784 785 786 |
# File 'app/models/invoice.rb', line 784 def not_rma? !(order.present? && order.funded_by_advance_replacement?) end |
#online_payment_options ⇒ Array<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.
523 524 525 |
# File 'app/models/invoice.rb', line 523 def [Payment::CREDIT_CARD] end |
#order ⇒ Order
Order this invoice bills.
177 |
# File 'app/models/invoice.rb', line 177 belongs_to :order, optional: true, inverse_of: :invoices |
#order_ref ⇒ String?
497 498 499 |
# File 'app/models/invoice.rb', line 497 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.
507 508 509 |
# File 'app/models/invoice.rb', line 507 def order_ref=(ref) self.order = Order.find_by(reference_number: ref) if ref.present? end |
#outlet_purchases ⇒ ActiveRecord::Relation<CustomerOutletPurchase>
Opportunities a rep worked that this outlet invoice satisfied — see
doc/tasks/202608081330_OUTLET_PURCHASE_ATTRIBUTION.md.
232 |
# File 'app/models/invoice.rb', line 232 has_many :outlet_purchases, class_name: 'CustomerOutletPurchase', inverse_of: :invoice, dependent: :destroy |
#payments ⇒ ActiveRecord::Relation<Payment>
Payments applied to this invoice.
227 |
# File 'app/models/invoice.rb', line 227 has_many :payments, dependent: :nullify, inverse_of: :invoice |
#po_numbers ⇒ Array<String>
Distinct customer purchase-order numbers attached to Payments on
this invoice. Surfaced in the PDF header and EDI 810 PO segment.
675 676 677 |
# File 'app/models/invoice.rb', line 675 def po_numbers payments.where.not(po_number: nil).distinct.pluck(:po_number) end |
#prevent_recalculate_shipping? ⇒ Boolean
Whether shipping recalculation is suppressed on save.
483 484 485 |
# File 'app/models/invoice.rb', line 483 def prevent_recalculate_shipping? true end |
#pricing_program_discount_factor ⇒ BigDecimal
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.
592 593 594 595 596 597 598 |
# File 'app/models/invoice.rb', line 592 def pricing_program_discount_factor if order.present? order.pricing_program_discount_factor else customer.pricing_program_discount end end |
#primary_party ⇒ Party
748 749 750 |
# File 'app/models/invoice.rb', line 748 def primary_party order&.primary_party || customer end |
#profile ⇒ Profile
Customer profile the invoice is attributed to.
193 |
# File 'app/models/invoice.rb', line 193 belongs_to :profile, optional: true, inverse_of: :invoices |
#public_pay_link ⇒ String?
765 766 767 768 769 770 771 772 773 774 |
# File 'app/models/invoice.rb', line 765 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 |
#public_pay_link_has_auth_token? ⇒ Boolean
Whether the public pay link carries an auth token (currently always false).
778 779 780 |
# File 'app/models/invoice.rb', line 778 def public_pay_link_has_auth_token? false end |
#receipt_details ⇒ ActiveRecord::Relation<ReceiptDetail>
Payment receipt details applied to this invoice.
211 |
# File 'app/models/invoice.rb', line 211 has_many :receipt_details, dependent: :nullify, inverse_of: :invoice |
#receipts_total ⇒ BigDecimal
574 575 576 |
# File 'app/models/invoice.rb', line 574 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
Whether the linked RMA is still awaiting the returned goods.
431 432 433 |
# File 'app/models/invoice.rb', line 431 def rma_awaiting_return? order.try(:rma).try(:state) == 'awaiting_return' end |
#rma_number ⇒ String
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.
425 426 427 |
# File 'app/models/invoice.rb', line 425 def rma_number order.try(:rma).try(:rma_number) || "RMA # #{order.rma_reference}" end |
#rmas ⇒ ActiveRecord::Relation<Rma>
RMAs issued against this invoice.
217 |
# File 'app/models/invoice.rb', line 217 has_many :rmas, foreign_key: 'original_invoice_id', dependent: :destroy, inverse_of: :original_invoice |
#selection_name ⇒ String
Display label for resource pickers / invoice dropdowns —
INV… <Customer Name>.
1127 1128 1129 |
# File 'app/models/invoice.rb', line 1127 def selection_name "#{reference_number} #{customer.full_name}" end |
#selection_name_for_rmas ⇒ String
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.
1136 1137 1138 1139 1140 1141 1142 |
# File 'app/models/invoice.rb', line 1136 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_items ⇒ Array<LineItem>
Service-class LineItems only (tax_class 'svc'), e.g. SmartInstall
labour. Used by the consignment / service-only invoice rendering path.
477 478 479 |
# File 'app/models/invoice.rb', line 477 def service_line_items line_items.where(cm_category: 'Item').select { |li| li.item.tax_class == 'svc' } end |
#set_consolidated_amount ⇒ void
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.
1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 |
# File 'app/models/invoice.rb', line 1181 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_address ⇒ Address
Address the invoiced goods ship to.
Validations:
183 |
# File 'app/models/invoice.rb', line 183 belongs_to :shipping_address, class_name: 'Address', optional: true, inverse_of: :shipping_invoices |
#shipping_costs ⇒ ActiveRecord::Relation<ShippingCost>
Shipping costs from the delivery, cheapest first.
221 |
# File 'app/models/invoice.rb', line 221 has_many :shipping_costs, -> { order(:cost) }, through: :delivery |
#show_tax_info? ⇒ Boolean
Whether tax info (VAT number / company tax info) should be rendered.
416 417 418 |
# File 'app/models/invoice.rb', line 416 def show_tax_info? tax_info.present? end |
#sold_to_billing_address ⇒ Address
Sold-to party's billing address, when different from #billing_address.
181 |
# File 'app/models/invoice.rb', line 181 belongs_to :sold_to_billing_address, class_name: 'Address', foreign_key: 'sold_to_billing_address', optional: true, inverse_of: :billing_invoices |
#source ⇒ Source
Source (marketing/order origin) of the invoice.
203 |
# File 'app/models/invoice.rb', line 203 belongs_to :source, optional: true, inverse_of: :invoices |
#store ⇒ Store
Store the invoice was issued by (misc/counter invoices).
195 |
# File 'app/models/invoice.rb', line 195 belongs_to :store, optional: true, inverse_of: :invoices |
#tax_info ⇒ String?
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).
389 390 391 392 393 394 395 396 397 398 399 400 401 402 |
# File 'app/models/invoice.rb', line 389 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_rep ⇒ Employee
Technical support rep credited on the invoice.
205 |
# File 'app/models/invoice.rb', line 205 belongs_to :technical_support_rep, class_name: 'Employee', optional: true, inverse_of: :technical_support_invoices |
#terms_in_days ⇒ Object
calculate net due date in days
613 614 615 616 617 |
# File 'app/models/invoice.rb', line 613 def terms_in_days return unless due_date (due_date - gl_date).to_i end |
#to_liquid ⇒ Liquid::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.
1210 1211 1212 |
# File 'app/models/invoice.rb', line 1210 def to_liquid Liquid::InvoiceDrop.new self end |
#to_s ⇒ String
Human-readable identifier used in audit logs and error messages.
1197 1198 1199 1200 1201 1202 1203 |
# File 'app/models/invoice.rb', line 1197 def to_s if respond_to?(:reference_number) "Invoice # #{reference_number}" else "Invoice ID #{id}" end end |
#uploads ⇒ ActiveRecord::Relation<Upload>
Uploaded files attached to this invoice, most recent first.
223 |
# File 'app/models/invoice.rb', line 223 has_many :uploads, -> { order(:updated_at).reverse_order }, as: :resource, dependent: :destroy, inverse_of: :resource |