Class: Quote
- Inherits:
-
ApplicationRecord
- Object
- ActiveRecord::Base
- ApplicationRecord
- Quote
- Includes:
- Memery, Models::Auditable, Models::CouponDateOverridable, Models::Itemizable, Models::LegacyRateRequest, Models::Lineage, Models::MultiRoom, Models::Notable, Models::Pickable, Models::Profitable, Models::ShipQuotable, Models::TaxableResource, PgSearch::Model
- Defined in:
- app/models/quote.rb
Overview
== Schema Information
Table name: quotes
Database name: primary
id :integer not null, primary key
ancestors_count :integer
bill_shipping_to_customer :boolean
children_count :integer
complete_datetime :datetime
currency :string(255)
descendants_count :integer
disable_auto_coupon :boolean default(FALSE), not null
discount :decimal(10, 2)
expiration_date :date
expiration_notice_sent :boolean default(FALSE), not null
first_view_date :datetime
hidden :boolean default(FALSE), not null
hold_for_transmission :boolean default(FALSE), not null
jde_b_ab :integer
jde_s_ab :integer
line_offset :decimal(10, 2)
line_total :decimal(10, 2)
line_total_discounted :decimal(10, 2)
ltl_freight :boolean
ltl_freight_guaranteed :boolean
max_discount_override :decimal(4, 2)
min_profit_markup :integer default(0)
override_coupon_date :date
override_coupon_date_without_limits :boolean default(FALSE), not null
override_line_lock :boolean default(FALSE), not null
position :integer default(100)
pricing_program_description :string(255)
pricing_program_discount :integer
priority :string(255)
quote_type :string(2)
recalculate_discounts :boolean default(FALSE), not null
recalculate_shipping :boolean default(TRUE), not null
reference_number :string(255) not null
requires_upgrade :boolean
retrieving_shipping_costs :boolean
saturday_delivery :boolean default(FALSE), not null
shipping_cost :decimal(10, 2)
shipping_coupon :decimal(10, 2)
shipping_issue_alerted :boolean
shipping_method :string(255)
ships_economy :boolean default(FALSE), not null
signature_confirmation :boolean default(FALSE), not null
single_origin :boolean default(FALSE), not null
state :string(255)
suffix :string(255)
tax_exempt :boolean default(FALSE), not null
tax_offset :decimal(10, 2)
tax_total :decimal(10, 2)
taxable_total :decimal(12, 2)
total :decimal(10, 2)
uploads_count :integer default(0)
created_at :datetime
updated_at :datetime
billing_address_id :integer
buying_group_id :integer
creator_id :integer
legacy_proto_iq_room_id :integer
opportunity_id :integer
parent_id :integer
resource_tax_rate_id :integer
rma_id :integer
shipping_address_id :integer
updater_id :integer
visit_id :bigint
Indexes
idx_quotes_reference_number_trgm_gist (reference_number) USING gist
idx_quotes_rma_id (rma_id)
index_quotes_on_legacy_proto_iq_room_id (legacy_proto_iq_room_id)
index_quotes_on_parent_id (parent_id)
index_quotes_on_reference_number (reference_number) UNIQUE
index_quotes_on_suffix (suffix) USING gin
index_quotes_on_visit_id (visit_id) WHERE (visit_id IS NOT NULL) USING hash
quotes_aasm_state_index (state)
quotes_opportunity_id_index (opportunity_id)
quotes_shipping_address_id_idx (shipping_address_id)
Foreign Keys
fk_rails_... (shipping_address_id => addresses.id)
fk_rails_... (visit_id => visits.id) ON DELETE => nullify
Defined Under Namespace
Classes: AssignLargeOppActivity, CombinedPdfGenerator, ConvertToOrder, Copier, LetterPdfGenerator, LetterPdfGeneratorLegacy, Mover, QuoteCompletedHandler, RepPreviewTokenService, Reviser
Constant Summary collapse
- SALES_QUOTE =
Sales quote.
'SQ'- MARKETING_QUOTE =
Marketing quote.
'MQ'- TECH_QUOTE =
Tech quote.
'TQ'- INSTANT_QUOTE =
Instant quote.
'IQ'- QUOTE_TYPES =
Recognised quote types.
%w[SQ MQ TQ IQ].freeze
- REFERENCE_NUMBER_PATTERN =
Regex pattern matching reference number.
Regexp.new("^(#{QUOTE_TYPES.join('|')})(\\d+)(-R\\d+)?$", Regexp::IGNORECASE)
- CORK_MIN_QTY =
Cork min qty.
1- MIN_INSTALL_SQ_FT =
Minimum install sq ft.
88- CUSTOMER_CANCELABLE_STATES =
Recognised customer cancelable states.
%i[pending].freeze
- CANCELABLE_STATES =
Recognised cancelable states.
CUSTOMER_CANCELABLE_STATES + %i[pending_project_details awaiting_completed_installation_plans awaiting_transmission]
- CLOSED_STATES =
Recognised closed states.
%i[complete cancelled].freeze
- CAN_REQUEST_PRE_PACK_STATES =
Recognised can request pre pack states.
%i[pending pre_pack awaiting_transmission profit_review].freeze
- ONLINE_QUOTE_OPTION_KEYS =
Rep-configurable knobs that drive the customer-facing
/ql/<token>
quote view: header style + which auto-generated reference docs the
customer should see in the "Forms & guides" section. Stored as
JSONB so we can add more knobs without per-flag migrations. Each
accessor returns a string ('1'/'0' from the form) coerced to bool
by the matching predicate (#online_quote_proforma_invoice?etc.)
viaActiveRecord::Type::Boolean. %i[ proforma_invoice materials_only show_suggested_services show_suggested_add_ons include_custom_order_agreement include_tempzone_comparison check_list_fh check_list_sm ].freeze
- CUSTOMER_QUOTE_INTENT_KEYS =
Customer-side intent flags. Live in the same JSONB so they ride along
with the rep's config without a second column, but the rep-side
QuotesController#update_online_quote_optionsmerges rather than
replaces so a rep save never wipes customer state. %i[ customer_wants_smart_install customer_wants_smart_support ].freeze
- PRE_PLANNING_MEETING_MIN_TOTAL =
Quote total (dollars) above which the customer is offered a pre-project
technical meeting. Single source of truth so the transmission email (via
Liquid::QuoteDrop) and the /ql/ online-quote viewer show/hide the invite
on identical terms. 500- UNPAID_ORDER_STATES =
Order states where the customer is still inside their own checkout and
has NOT completed payment: Buy Now leaves apendingorder, Add to Cart
acartone, and a half-finished card entry apending_paymentone
(in_shipping_estimateis a transient rating state). While an order sits
in any of these the customer must still be able to reopen and resume the
/ql/ quote; only once it moves past them (payment captured / order placed)
does /ql/ swap to the "already ordered" view. %w[cart pending pending_payment in_shipping_estimate].freeze
Constants included from Models::Auditable
Models::Auditable::ALWAYS_IGNORED
Constants included from Models::Schedulable
Models::Schedulable::SIMPLE_FORM_OPTIONS
Instance Attribute Summary collapse
-
#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.
-
#max_discount_override ⇒ Object
readonly
Validates the max discount override is a 0–100 percentage when set.
-
#opportunity_id ⇒ Object
readonly
Validates an opportunity is linked when required by the quote's validation context.
-
#suffix ⇒ Object
readonly
Validates the suffix is at most 150 characters.
-
#validate_opportunity_and_prepared_for ⇒ Object
Returns the value of attribute validate_opportunity_and_prepared_for.
Attributes included from Models::Profitable
Attributes included from Models::Itemizable
#account_specialist, #coupons, #discounts, #force_total_reset, #total_reset
Delegated Instance Attributes collapse
-
#all_participants ⇒ Object
Alias for Opportunity#all_participants.
-
#billing_address ⇒ Object
Alias for Customer#billing_address.
-
#can_convert? ⇒ Object
Alias for Convert_to_order_service#can_convert?.
-
#catalog ⇒ Object
Alias for Customer#catalog.
-
#company ⇒ Object
Alias for Customer#company.
-
#contact ⇒ Object
Alias for Opportunity#contact.
-
#customer ⇒ Object
Alias for Opportunity#customer.
-
#installation_country_iso ⇒ Object
Alias for Opportunity#installation_country_iso.
-
#installation_country_iso3 ⇒ Object
Alias for Opportunity#installation_country_iso3.
-
#installation_postal_code ⇒ Object
Alias for Opportunity#installation_postal_code.
-
#installation_state_code ⇒ Object
Alias for Opportunity#installation_state_code.
-
#local_sales_rep ⇒ Object
Alias for Customer#local_sales_rep.
-
#primary_sales_rep ⇒ Object
Alias for Customer#primary_sales_rep.
-
#secondary_sales_rep ⇒ Object
Alias for Customer#secondary_sales_rep.
-
#store ⇒ Object
Alias for Customer#store.
Belongs to collapse
-
#buying_group ⇒ BuyingGroup
Buying group the quote is attributed to.
-
#opportunity ⇒ Opportunity
Opportunity this quote belongs to.
-
#rma ⇒ Rma
RMA this quote was created from (advance replacements).
-
#visit ⇒ Visit
Website visit that originated the quote.
Methods included from Models::Auditable
Methods included from Models::ShipQuotable
Methods included from Models::TaxableResource
Has many collapse
-
#active_orders ⇒ ActiveRecord::Relation<Order>
Orders not cancelled or fraudulent.
-
#activities ⇒ ActiveRecord::Relation<Activity>
Activity log entries for this quote.
-
#communications ⇒ ActiveRecord::Relation<Communication>
Communications (emails, calls) logged against this quote.
-
#linked_support_cases ⇒ ActiveRecord::Relation<LinkedSupportCase>
Support cases linked via the quote's room configurations.
-
#material_alerts ⇒ ActiveRecord::Relation<MaterialAlert>
Material alerts raised for this quote.
-
#messaging_logs ⇒ ActiveRecord::Relation<MessagingLog>
Messaging logs for this quote.
-
#orders ⇒ ActiveRecord::Relation<Order>
Orders placed from this quote.
-
#uploads ⇒ ActiveRecord::Relation<Upload>
Uploaded files attached to this quote.
Methods included from Models::ShipQuotable
Methods included from Models::Pickable
Has and belongs to many collapse
-
#contact_points ⇒ ActiveRecord::Relation<ContactPoint>
Contact points attached to this quote.
-
#support_cases ⇒ ActiveRecord::Relation<SupportCase>
Support cases attached to this quote.
Methods included from Models::MultiRoom
Class Method Summary collapse
-
.active ⇒ ActiveRecord::Relation<Quote>
A relation of Quotes that are active.
-
.auto_quote_from_room(room, options = {}) ⇒ Quote
One-shot factory used by the legacy "quote-from-room" entry point — constructs a single-origin quote attached to the given RoomConfiguration's opportunity, saves it twice (once to get an id so discounts can attach, once to recompute), then fires the requested state event (defaulting to
complete). -
.by_reference_number ⇒ ActiveRecord::Relation<Quote>
A relation of Quotes that are by reference number.
-
.completed_quotes ⇒ ActiveRecord::Relation<Quote>
A relation of Quotes that are completed quotes.
-
.contains_coupon_ids ⇒ ActiveRecord::Relation<Quote>
A relation of Quotes that are contains coupon ids.
-
.contains_item_ids ⇒ ActiveRecord::Relation<Quote>
A relation of Quotes that are contains item ids.
-
.count_by_incomplete_state_by_rep(rep_id = nil, sales_rep_type_id_label = 'primary_sales_rep_id') ⇒ Hash{String=>Integer}
Counts open quotes grouped by state, optionally constrained to one rep.
-
.formatted_resources_query(quotes) ⇒ ActiveRecord::Relation<Quote>
Lightweight projection used by the global "resource picker" search — returns just the columns the autocomplete needs (ref, id, created_at, state, opportunity name) to keep the payload small.
-
.ignore_fixture_attributes ⇒ Array<String>
Columns to skip when comparing fixture-loaded quotes — the cached rate-request XML blobs are timestamped and would otherwise cause spurious test diffs.
-
.in_quoting ⇒ ActiveRecord::Relation<Quote>
A relation of Quotes that are in quoting.
-
.last_quote_update_cache_key ⇒ String
Cache key derived from the most-recently-updated quote in the whole table — used to invalidate index-page fragment caches the moment any quote changes.
-
.last_revisions ⇒ ActiveRecord::Relation<Quote>
A relation of Quotes that are last revisions.
-
.like_lookup ⇒ ActiveRecord::Relation<Quote>
A relation of Quotes that are like lookup.
-
.lookup ⇒ ActiveRecord::Relation<Quote>
A relation of Quotes that are lookup.
-
.most_recent_first ⇒ ActiveRecord::Relation<Quote>
A relation of Quotes that are most recent first.
-
.not_converted ⇒ ActiveRecord::Relation<Quote>
A relation of Quotes that are not converted.
-
.open_quotes ⇒ ActiveRecord::Relation<Quote>
A relation of Quotes that are open quotes.
-
.pending ⇒ ActiveRecord::Relation<Quote>
A relation of Quotes that are pending.
-
.sales_quotes ⇒ ActiveRecord::Relation<Quote>
A relation of Quotes that are sales quotes.
-
.states_for_select ⇒ Array<Array(String, String)>
State-machine states formatted for a Rails
selecthelper —[[human_name, machine_value], …]. -
.with_contact_point_category ⇒ ActiveRecord::Relation<Quote>
A relation of Quotes that are with contact point category.
-
.with_email ⇒ ActiveRecord::Relation<Quote>
A relation of Quotes that are with email.
-
.with_line_items ⇒ ActiveRecord::Relation<Quote>
A relation of Quotes that are with line items.
-
.without_quofus ⇒ ActiveRecord::Relation<Quote>
A relation of Quotes that are without quofus.
Instance Method Summary collapse
-
#addendums ⇒ Array<Upload>
PDF uploads that should be appended to the customer-facing quote bundle — e.g.
-
#address_book ⇒ Array<Address>
Address picker source for the quote's "ship to" field — the customer's home store warehouse first (for will-call quotes), then every address on the customer record.
-
#adjust_status_based_on_room_status(rc) ⇒ void
Synchronises the quote's state machine with a child RoomConfiguration that just changed status.
-
#all_activities ⇒ ActiveRecord::Relation<Activity>
Every Activity linked to this quote or its parent Opportunity — what the CRM "activities" tab shows.
-
#all_orders_sold? ⇒ Boolean
Whether every order spawned from this quote has been invoiced — used as the guard for closing out the quote once shipping is done.
-
#all_suggested_items ⇒ Hash{Symbol=>Object}
Aggregate of recommended add-ons for this quote: goods grouped by room configuration, services, a deduplicated goods list, and the SKUs of those unique goods.
-
#all_support_cases ⇒ ActiveRecord::Relation<SupportCase>
Support cases linked to this quote (directly via the HABTM and transitively through its room configurations), ordered newest case-number first for the CRM sidebar.
-
#applies_for_smartinstall ⇒ Boolean
Whether this quote qualifies for SmartInstall — WarmlyYours's in-house installation service for homeowners.
-
#applies_for_smartsupport ⇒ Boolean
Whether this quote qualifies for SmartSupport — the dealer/trade-pro version of the in-house service program (we send an installer to support the trade pro on site).
-
#apply_tier2_pricing? ⇒ Boolean
Whether or not to apply the tier2 pricing (customer discount) by default.
-
#available_contact_points ⇒ Array<ContactPoint>
Every ContactPoint reachable through this quote's opportunity (primary party, customer, and every opportunity participant), deduped — used to populate the recipient picker on the quote transmission form.
-
#available_contact_points_grouped_for_select ⇒ Hash{String=>Array<Array(String, Integer)>}
#available_contact_points reshaped into Rails grouped-select input:
{ "Person Full Name" => [["detail (category)", id], …] }so the transmission form renders an<optgroup>per party. -
#best_contact_point ⇒ ContactPoint?
Best contact point we can send the quote to — falls through quote → contact → customer until something transmittable (email/fax) turns up.
-
#build_activity ⇒ Activity
Builds (but does not save) a new Activity attached to this quote with the quote's primary party already populated.
-
#calculate_expiration_date ⇒ Date
Computed expiration date for the quote — the soonest of every attached coupon's expiration and a default 60-day window from
created_at. -
#can_be_moved? ⇒ Boolean
Whether this quote can be reassigned to a different opportunity.
-
#can_be_transmitted? ⇒ Boolean
Whether the customer-facing transmission flow is allowed in this state.
-
#cancelable? ⇒ Boolean
State-machine guard for the
cancelevent — the quote can be cancelled as long as no surviving order points at it. -
#check_pre_pack ⇒ void
after_savecallback that auto-transitions the quote intopre_packwhen it's awaiting transmission and the warehouse signals that estimated packaging needs to happen first (see #stop_for_pre_pack?). -
#convert_to_order_service ⇒ Quote::ConvertToOrder
Memoised ConvertToOrder service for this quote.
-
#country ⇒ String?
ISO country of the Store this quote was placed against — used by tax / shipping / addendum logic to branch on US vs CA vs other.
-
#crm_link ⇒ String
Internal CRM URL for this quote's show page.
-
#currency_symbol ⇒ String
Currency glyph (
$,€,£, …) for this quote, derived from the quote'scurrencyISO code via themoneygem. -
#deep_dup ⇒ Quote
Deep-clones the quote and its non-shipping parent LineItems, plus the contact-points join, but resets identity / lifecycle columns (state goes back to
pending, tax fields drop, parent_id and delivery_id are stripped) so the duplicate can be saved as a fresh draft. -
#dependents(limit: 5) ⇒ Hash{Symbol=>ActiveRecord::Relation}
Quote dependents that block destruction — orders, child revisions, and customer communications.
-
#editing_locked? ⇒ Boolean
True once the quote is in a terminal state — used by the form/controller layer to read-only the line item rows so a finalised or cancelled quote can't be edited in place.
-
#effective_date_for_coupon ⇒ Date
Date used by the coupon engine to evaluate "is this coupon currently valid?" against
start_date/end_datewindows. -
#exclude_manually_initiated_event?(_event) ⇒ false
State-machine helper —
Models::Auditablehook used to suppress certain manually-initiated events from PaperTrail's audit trail. -
#extra_plans_file_name(with_extension = true) ⇒ String
Filename for the secondary "installation plans" PDF that ships alongside the main quote — same naming convention as #file_name with a
_planssuffix so attachments don't collide. -
#file_name(with_extension = true) ⇒ String
Filename used when the customer-facing quote PDF is downloaded or attached to an email — reference-number-stamped so a customer with multiple revisions can tell them apart.
-
#get_next_reference_number ⇒ String
Pulls the next quote reference number from the
quotes_reference_numbers_seqPostgres sequence and prefixes it with the quote-type code (e.g.SQ,MQ,TQ). -
#goods_product_line_ids ⇒ Array<Integer>
Distinct primary product-line ids represented by the goods on this quote.
-
#has_discounts? ⇒ Boolean
True when at least one line item carries a discount — drives the "savings" badge on the customer-facing PDF and unlocks the discount-detail rows.
-
#has_floor_heating_items? ⇒ Boolean
Auto-default predicates for the four "include X document" checkboxes on the rep's online-quote configuration form.
-
#has_no_instant_quoting_rooms ⇒ true
Validation stub left over from the deprecated Instant Quote (IQ) system — kept registered as a
validate :has_no_instant_quoting_roomsso removing it is a separate, easy-to-revert change. -
#has_onsite_smartsupport? ⇒ Boolean
Whether on-site SmartSupport is available — install is within the service-distance radius from the WarmlyYours HQ.
-
#has_pre_pack_deliveries? ⇒ Boolean
True when the quote itself is in
pre_packor any of its deliveries is in pre-pack. -
#has_selected_heated_items? ⇒ Boolean
Whether the quote actually contains items eligible for the smart-services program — branches on customer type so we look at the right side of the eligibility data.
-
#has_snow_melting_items? ⇒ Boolean
Whether the quote contains snow-melting items.
-
#has_suggested_add_ons? ⇒ Boolean
True when the rep has opted into showing suggested add-ons AND there is at least one actual suggestion to surface.
-
#installation_is_within_range? ⇒ Boolean
True when #service_distance is non-nil and inside the
SMART_SERVICES_MAX_DISTANCEradius (currently 100 mi from HQ). -
#insulation_sq_ft ⇒ Integer
Total square footage of underlayment / insulation across every indoor room on the quote — used by the cork/insulation cross-sell logic and by the catalog to suggest the right underlayment quantity.
-
#is_first_quote? ⇒ Boolean
True when this is the original quote rather than a revision — i.e.
-
#is_revision? ⇒ Boolean
True when this quote was spawned via #deep_dup from an earlier quote — the inverse of #is_first_quote?.
-
#is_sales_quote? ⇒ Boolean
True for the standard customer-facing sales quote (
SQ) — the main quote-type in production. -
#is_smart_service_quote? ⇒ Boolean
True when every non-shipping line on the quote is a smart-service SKU — used to switch the PDF / fulfillment path to the services-only template.
-
#lookup_link ⇒ String
Public-facing URL for the customer's "look up your quote" page (warmlyyours.com /quote-lookup).
-
#main_rep ⇒ Employee?
Primary rep this quote "belongs to" for commission and CRM ownership.
-
#name ⇒ String
Short human label like
Quote #SQ12345. -
#ok_to_customer_cancel? ⇒ Boolean
Whether the customer is allowed to self-cancel from the public quote-lookup page — only when the quote is still pending and has no surviving orders attached.
-
#ok_to_delete? ⇒ Boolean
True when #dependents is empty across the board — the precondition for the destroy button.
-
#open_activities_counter ⇒ Integer
Count of CRM follow-ups still open against this quote (or its opportunity), filtered to the activities that show by default in the rep's queue.
-
#participants_options_for_select ⇒ Array<Array(String, Integer)>?
Opportunity participants formatted for a Rails
selecthelper —[[full_name, party_id], …]. -
#placed_order ⇒ Order?
The placed (payment-completed) order for this quote, if any — drives the "already ordered" gate on the customer-facing /ql/ page.
-
#post_communication_queued_hook(_params = {}) ⇒ void
Hook fired by the communication subsystem the moment a transmission email/fax is enqueued — finalises the quote if every other gate has already cleared.
-
#post_communication_sent_hook(_params = {}) ⇒ void
Hook fired once the transmission has actually been sent by the provider — finalises the quote if every other gate has cleared.
-
#pre_packable? ⇒ Boolean
Whether the warehouse can run an estimated-packaging pre-pack on this quote — at least one delivery must have a shipment that isn't already packed or pre-packed.
-
#pre_planning_invite_url ⇒ String
Booking link for the pre-project technical meeting, pre-filled with the recipient's name/email and this quote's reference so the scheduling form arrives populated.
-
#prefix_for_reference_number ⇒ String
Fallback prefix used when
#quote_typehasn't been chosen yet — derives the two-letter code from the parent Opportunity's type (so a sales opp givesSQ, marketing oppMQ, etc.). - #prepared_for_name ⇒ String
-
#prevent_recalculate_shipping? ⇒ Boolean
Guard used by Models::ShipQuotable to skip live ShipEngine rate quoting — already-finalised quotes don't requote, and the flag also blocks re-entrancy while a rate request is in flight.
-
#pricing_program_description ⇒ String
Human label for the pricing program in effect on this quote — the tier2 coupon title when the customer is on a dealer/contractor tier, otherwise
"MSRP/Catalog". -
#pricing_program_discount ⇒ Integer
Percentage discount the active tier2 pricing program applies to goods, or
0when the quote is on plain MSRP. - #primary_party ⇒ Party
-
#ready_to_complete? ⇒ Boolean
Coarser readiness check than #will_complete? — only requires that no room is in a half-resolved state.
-
#recipient_email ⇒ String?
Email address the quote should be sent to — prefers an explicit quote-level contact-point email, then the primary party's email, then the customer's.
-
#recipient_name ⇒ String?
Best display name for the person this quote is addressed to — walks #primary_party → shipping address → customer until it finds one.
-
#reference_number_with_name ⇒ String
Reference number plus the rep-supplied free-text suffix (the rep uses this slot for the project name).
-
#reference_number_with_opp_name ⇒ String
Reference number with the parent Opportunity's name appended — used in dropdowns where the rep needs to disambiguate between multiple quotes on the same opportunity.
-
#reference_number_with_opp_name_when_specified ⇒ String
#reference_number_with_opp_name but skips the appended job name when the opportunity has a placeholder/unknown name (so we don't render "SQ12345 Job for Customer XYZ" — just
SQ12345). -
#request_plans_or_complete ⇒ void
State-machine driver that nudges the quote forward (or back) based on its current room-configuration mix and line-item presence.
-
#reset_discount_on_ships_economy_change ⇒ Boolean?
protected
after_savecallback: when the rep flips a quote between economy and standard ground shipping, drop any previously-applied shipping discount so the next pricing pass recalculates against the new shipping basis. -
#schedule_follow_up(follow_up_task_type: nil) ⇒ Activity?
Creates (or replaces) a quote-centric follow-up activity on the opportunity so a rep is reminded to chase the customer.
-
#selection_name ⇒ String
Label used by the global Ransack/select2 lookup widget when this quote is offered as a search result.
-
#send_profit_review_notification ⇒ void
Notifies internal staff (margin desk / pricing) that this quote has entered
profit_reviewso they can review the pricing exception. -
#sender ⇒ Employee?
Rep treated as the "from" on outbound communications and notifications about this quote.
-
#service_distance ⇒ Float?
Driving distance (in miles) from the WarmlyYours Lake Zurich HQ to the installation postal code on this quote — the gating measure for SmartInstall / SmartSupport eligibility (see
SMART_SERVICES_MAX_DISTANCE). -
#set_currency ⇒ String?
Pulls the currency from the customer's home Store and writes it onto the quote so prices print in the right denomination.
-
#set_default_quote_type ⇒ void
protected
Defaults the quote type from the reference-number prefix and re-prefixes the reference number when the type changes.
-
#set_expiration_date ⇒ void
protected
Recalculates the expiration date on update (skipped once complete).
-
#set_min_profit_markup ⇒ void
protected
Sets the minimum profit markup from the quote type's default sales markup.
-
#set_opportunity_value ⇒ void
after_commithook that recomputes the parent Opportunity's rolled-up dollar value when this quote's totals change. -
#set_priority(save = true) ⇒ String, true
Computes the highest urgency among the quote's room configurations and writes it onto
priority(urgent,rush,standard, etc.). -
#set_reference_number ⇒ String
before_createcallback that stamps#reference_numberfrom the Postgres sequence unless the caller (e.g. data import) has already supplied one. -
#shipping_address_nil_or_valid? ⇒ Boolean
True when the quote either has no shipping address or has one that passes its own validations — used as a precondition before transitioning to states that require shippable goods.
-
#short_lookup_link ⇒ String
Short URL printed on quote PDFs / encoded in their QR code.
-
#show_pre_planning_meeting? ⇒ Boolean
Whether to offer the customer a pre-project technical meeting.
-
#smartsupport_info ⇒ Hash?
SmartSupport metadata bundle (line items eligible for the program, estimated visit time, etc.) when the install location is within service range; nil otherwise.
-
#sms_enabled_numbers ⇒ Array<String>
Distinct SMS-capable numbers for every participant on this quote's opportunity, formatted for the SMS gateway.
-
#sms_messages ⇒ ActiveRecord::Relation<SmsMessage>
Two-way SMS history for #sms_enabled_numbers — the message thread shown on the quote sidebar.
-
#sold? ⇒ Boolean
True once at least one non-cart, non-cancelled Order exists against this quote — i.e.
-
#special_ordering_instructions ⇒ String?
Free-text "special instructions" stored on the billing-address party — printed on the warehouse pick ticket when present (e.g. "no liftgate", "call before delivery").
-
#stop_for_pre_pack? ⇒ Boolean
State-machine guard — true when transmission must wait for the warehouse to provide an estimated-packaging quote.
-
#stop_for_profit_review? ⇒ Boolean
State-machine guard — true when transmission must pause for margin desk review.
-
#tier2_program_pricing ⇒ Discount?
The tier2 (Discount) row representing the customer's contractor/dealer pricing program on this quote.
-
#tier2_program_pricing_coupon ⇒ Coupon?
The Coupon that powers #tier2_program_pricing, when there is one — used by #pricing_program_description and #pricing_program_discount to label the program.
-
#to_liquid ⇒ Liquid::QuoteDrop
Liquid drop wrapper used when this quote is rendered into the transmission email/SMS templates — exposes only the safe-for-template accessors via Liquid::QuoteDrop.
-
#to_order(order = nil, txid = nil) ⇒ Array<Order>
Convert this quote into one or more Orders via the ConvertToOrder service.
-
#to_s ⇒ String
Display string used by Rails helpers and inspectors —
"Quote # SQ12345 - Smith Master Bath". -
#total_amps ⇒ Float
Sum of every room's calculated electrical load, in amps.
-
#track_profit? ⇒ Boolean
Whether this quote should run through the profit-margin gate — only sales quotes with at least one profitable line item are subject to the profit-review state.
-
#tracking_email_address ⇒ String
Encrypted plus-addressed inbox alias used as the
Reply-Toon quote emails — replies bounce back into Heatwave and are auto-attached to this quote's communication thread. -
#versions_for_audit_trail(_params = {}) ⇒ ActiveRecord::Relation<RecordVersion>
PaperTrail versions for the quote and every record that hangs off it — its line items, deliveries, and discounts — pulled in a single query via JSONB
reference_datalookup. -
#will_complete? ⇒ Boolean
State-machine guard — true when the quote has every gate cleared for a
completetransition: at least one line item, valid shipping address (or none), all rooms settled, and profit margins met. -
#will_installation_plans_complete? ⇒ Boolean
State-machine guard for the
installation_plans_completeevent — rooms must exist and all be settled, and at least one line item must be on the quote. -
#will_pending_project_details? ⇒ Boolean
State-machine guard for the
pending_project_detailsevent — we're either coming from awaiting-design, or there are no line items but at least one draft room. -
#will_ready_for_design? ⇒ Boolean
State-machine guard for the
ready_for_designevent — at least one room exists and at least one is currently in design. -
#will_ready_to_transmit? ⇒ Boolean
State-machine guard for the
ready_to_transmitevent — line items exist, shipping is valid, margins are met, and the warehouse isn't asking for a pre-pack first. -
#zones ⇒ nil
Placeholder for a planned "zones" concept (groups of rooms controlled by a single thermostat).
Methods included from Models::CouponDateOverridable
Methods included from Models::LegacyRateRequest
#last_shipping_rate_request_result, #last_shipping_rate_request_result=
Methods included from Models::Auditable
#all_skipped_columns, #audit_reference_data, #should_not_save_version, #stamp_record
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, #validate_min_profit_markup?
Methods included from Models::Lineage
#ancestors, #ancestors_ids, #children_and_roots, #descendants, #descendants_ids, #ensure_non_recursive_lineage, #family_members, #generate_full_name, #generate_full_name_array, #lineage, #lineage_array, #lineage_simple, #root, #root_id, #self_ancestors_and_descendants, #self_ancestors_and_descendants_ids, #self_and_ancestors, #self_and_ancestors_ids, #self_and_children, #self_and_descendants, #self_and_descendants_ids, #self_and_siblings, #self_and_siblings_ids, #siblings, #siblings_ids
Methods included from Models::MultiRoom
#add_line_items_to_all_rooms, #add_lines_for_room_configuration, #all_rooms_complete?, #all_rooms_complete_or_cancelled?, #all_rooms_complete_or_cancelled_or_draft?, #all_rooms_in_design?, #all_rooms_ppd?, #any_room_ppd?, #any_rooms_in_design?, #get_operating_costs, #get_recommended_materials, #heated_sq_ft, #prioritize_room, #remove_line_items_from_all_rooms, #remove_lines_for_room_configuration, #replace_line_items_in_all_rooms, #suggested_items, #suggested_services, #synchronization_targets
Methods included from Models::ShipQuotable
#carrier, #chosen_shipping_method, #days_commitment, #determine_origin_address, #everything_in_stock?, #friendly_shipping_method, #is_drop_ship?, #is_override?, #is_warehouse_pickup?, #line_items_grouped_by_deliveries_quoting, #line_items_match_deliveries_if_any, #need_to_pre_pack_reasons, #need_to_recalculate_shipping, #one_time_shipping_address, #one_time_shipping_address=, #qualifies_for_cod?, #refresh_deliveries_quoting, #reset_deliveries_shipping_option, #reset_shipping, #retrieve_friendly_shipping_method, #retrieve_shipping_costs, #ship_quoted, #ship_weight, #shipping_non_db_default_but_db_customer?, #shipping_signature_confirmation_non_db_default_but_db_customer?, #shipping_via_wy_but_has_shipping_account?, #ships_economy_package?, #ships_freight?, #ships_freight_but_address_not_freight_ready?, #should_ship_freight?, #should_ship_freight_but_address_not_freight_ready?, #validate_deliveries
Methods included from Models::ShipMeasurable
#cartons_total, #crates_total, #pallets_total, #ship_freight_class_from_shipments, #ship_volume_from_shipments, #ship_volume_from_shipments_in_cubic_feet, #ship_weight_from_shipments, #shipment_set, #shipments_for_measure
Methods included from Models::Pickable
#all_my_publications, #append_suggested_items, #append_suggested_materials, #control_capacity, #determine_catalog_for_picking, #determine_skus_to_filter, #discounted_shipping_total, #electrical_heating_elements, #fix_catalog, #get_material_alerts, #has_custom_products?, #invalidate_material_alerts!, #line_items_grouped_by_room_configuration, #lines_match_pick_catalog_on_flip, #meets_custom_products_threshold?, #pricing_program_discount_factor, #purge_empty_line_items, #soft_recalc, #subtotal, #subtotal_after_trade_discount_without_shipping, #subtotal_discounted_without_shipping, #subtotal_msrp_without_shipping, #switching_pick_catalog?, #synchronize_lines, #total_amps_by_product_line, #total_coverage_by_product_line, #total_linear_feet_by_product_line, #total_spec_by_product_line, #total_watts_by_product_line, #underlayments
Methods included from Models::TaxableResource
#apply_tax_rate_to_line_items, #build_tax_params, #calculate_tax_for_all_lines, #copy_tax_rate, #effective_date, #get_rates_for_line, #get_tax_rate, #manual_rate_goods, #manual_rate_services, #manual_rate_shipping, #origin_address, #refresh_tax_rate, #resource_not_taxable?, #set_initial_tax_rate, #should_refresh_tax_rate?, #state_code, #state_code_sym, #taxes_grouped_by_rate, #taxes_grouped_by_type
Methods included from Models::Itemizable
#add_line_item, #additional_items, #assign_sequence, #billing_entity, #breakdown_of_prices, #calculate_actual_insured_value, #calculate_discounts, #calculate_shipping_cost, #coupon_search, #customer_applied_coupons, #customer_can_apply_coupon?, #discounts_changed?, #discounts_grouped_by_coupon, #discounts_subtotal, #effective_discount, #effective_shipping_discount, #has_kits?, #has_kits_or_serial_numbers?, #has_serial_numbers?, #is_credit_order?, #line_items_requiring_serial_number, #line_items_with_counters, #line_total_plus_tax, #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::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
#do_not_detect_shipping ⇒ Object
Returns the value of attribute do_not_detect_shipping.
164 165 166 |
# File 'app/models/quote.rb', line 164 def do_not_detect_shipping @do_not_detect_shipping end |
#do_not_set_totals ⇒ Object
Returns the value of attribute do_not_set_totals.
164 165 166 |
# File 'app/models/quote.rb', line 164 def do_not_set_totals @do_not_set_totals end |
#max_discount_override ⇒ Object (readonly)
Validates the max discount override is a 0–100 percentage when set.
Validations:
- Numericality ({ greater_than_or_equal_to: 0, less_than_or_equal_to: 100, allow_nil: true })
274 |
# File 'app/models/quote.rb', line 274 validates :max_discount_override, numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: 100, allow_nil: true } |
#opportunity_id ⇒ Object (readonly)
Validates an opportunity is linked when required by the quote's validation context.
Validations:
- Presence ({ if: :validate_opportunity_and_prepared_for })
269 |
# File 'app/models/quote.rb', line 269 validates :opportunity_id, presence: { if: :validate_opportunity_and_prepared_for } |
#suffix ⇒ Object (readonly)
Validates the suffix is at most 150 characters.
Validations:
- Length ({ maximum: 150 })
271 |
# File 'app/models/quote.rb', line 271 validates :suffix, length: { maximum: 150 } |
#validate_opportunity_and_prepared_for ⇒ Object
Returns the value of attribute validate_opportunity_and_prepared_for.
164 165 166 |
# File 'app/models/quote.rb', line 164 def validate_opportunity_and_prepared_for @validate_opportunity_and_prepared_for end |
Class Method Details
.active ⇒ ActiveRecord::Relation<Quote>
A relation of Quotes that are active. Active Record Scope
285 |
# File 'app/models/quote.rb', line 285 scope :active, -> { where.not(state: 'cancelled') } |
.auto_quote_from_room(room, options = {}) ⇒ Quote
One-shot factory used by the legacy "quote-from-room" entry
point — constructs a single-origin quote attached to the given
RoomConfiguration's opportunity, saves it twice (once to get
an id so discounts can attach, once to recompute), then fires
the requested state event (defaulting to complete).
1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 |
# File 'app/models/quote.rb', line 1427 def self.auto_quote_from_room(room, = {}) force_single_origin = [:force_single_origin] || true do_not_set_shipping_address = [:do_not_set_shipping_address] || false target_state_event = [:state_event] || 'complete' q = Quote.new(opportunity_id: room.opportunity_id) q.shipping_address_id = room.customer.shipping_address_id if !room.installation_postal_code && (do_not_set_shipping_address == false) && room.customer && room.customer.shipping_address.present? q.single_origin = true if force_single_origin q.room_configuration_ids = [room.id] q.do_not_detect_shipping = true # First save the quote WITHOUT calculating discounts to get an ID # Discounts require itemizable_id to be set, which only exists after the quote is persisted q.save # Now that the quote has an ID, trigger discount calculation (tier2 pricing, etc.) # The quote must be persisted before discounts can be created if q.persisted? q.recalculate_discounts = true q.force_total_reset = true q.do_not_detect_shipping = true # Still skip shipping detection q.save end # Now apply the target state event (typically 'complete') if q.persisted? && target_state_event.present? && q.respond_to?("#{target_state_event}!") q.do_not_set_totals = true # Discounts already calculated, don't recalculate q.send("#{target_state_event}!") end q end |
.by_reference_number ⇒ ActiveRecord::Relation<Quote>
A relation of Quotes that are by reference number. Active Record Scope
281 |
# File 'app/models/quote.rb', line 281 scope :by_reference_number, -> { order(:reference_number).reverse_order } |
.completed_quotes ⇒ ActiveRecord::Relation<Quote>
A relation of Quotes that are completed quotes. Active Record Scope
277 |
# File 'app/models/quote.rb', line 277 scope :completed_quotes, -> { where(state: 'complete') } |
.contains_coupon_ids ⇒ ActiveRecord::Relation<Quote>
A relation of Quotes that are contains coupon ids. Active Record Scope
284 |
# File 'app/models/quote.rb', line 284 scope :contains_coupon_ids, ->(coupon_ids) { where("EXISTS (select discounts.id from discounts where discounts.coupon_id IN (?) and discounts.itemizable_type = 'Quote' and discounts.itemizable_id = quotes.id)", coupon_ids) } |
.contains_item_ids ⇒ ActiveRecord::Relation<Quote>
A relation of Quotes that are contains item ids. Active Record Scope
283 |
# File 'app/models/quote.rb', line 283 scope :contains_item_ids, ->(item_ids) { where("EXISTS (select 1 from line_items li where li.resource_id = quotes.id and li.resource_type = 'Quote' and li.item_id IN (?))", item_ids) } |
.count_by_incomplete_state_by_rep(rep_id = nil, sales_rep_type_id_label = 'primary_sales_rep_id') ⇒ Hash{String=>Integer}
Counts open quotes grouped by state, optionally constrained to
one rep. The sales_rep_type_id_label parameter lets the caller
ask for primary, secondary, or local rep counts.
1303 1304 1305 1306 1307 1308 1309 |
# File 'app/models/quote.rb', line 1303 def self.count_by_incomplete_state_by_rep(rep_id = nil, sales_rep_type_id_label = 'primary_sales_rep_id') if rep_id.nil? count(:state, conditions: ["state not in ('complete','cancelled')"], group: :state) else count(:state, conditions: ["state not in ('complete','cancelled') and #{sales_rep_type_id_label} = ?", rep_id], group: :state) end end |
.formatted_resources_query(quotes) ⇒ ActiveRecord::Relation<Quote>
Lightweight projection used by the global "resource picker"
search — returns just the columns the autocomplete needs (ref,
id, created_at, state, opportunity name) to keep the payload
small.
626 627 628 |
# File 'app/models/quote.rb', line 626 def self.formatted_resources_query(quotes) quotes.select('quotes.reference_number,quotes.id,quotes.created_at,quotes.state,opportunities.name as opportunity_name').joins(:opportunity).order(reference_number: :desc) end |
.ignore_fixture_attributes ⇒ Array<String>
Columns to skip when comparing fixture-loaded quotes — the cached
rate-request XML blobs are timestamped and would otherwise cause
spurious test diffs.
605 606 607 |
# File 'app/models/quote.rb', line 605 def self.ignore_fixture_attributes %w[last_rate_request_xml last_rate_response_xml] end |
.in_quoting ⇒ ActiveRecord::Relation<Quote>
A relation of Quotes that are in quoting. Active Record Scope
287 |
# File 'app/models/quote.rb', line 287 scope :in_quoting, -> { where(state: %w[awaiting_completed_installation_plans pre_pack awaiting_transmission]) } |
.last_quote_update_cache_key ⇒ String
Cache key derived from the most-recently-updated quote in the
whole table — used to invalidate index-page fragment caches the
moment any quote changes.
614 615 616 |
# File 'app/models/quote.rb', line 614 def self.last_quote_update_cache_key Quote.select('id,updated_at').order(:updated_at).reverse_order.first.cache_key end |
.last_revisions ⇒ ActiveRecord::Relation<Quote>
A relation of Quotes that are last revisions. Active Record Scope
282 |
# File 'app/models/quote.rb', line 282 scope :last_revisions, -> { where("NOT EXISTS (select id from quotes q2 where q2.parent_id = quotes.id and quotes.state <> 'cancelled')") } |
.like_lookup ⇒ ActiveRecord::Relation<Quote>
A relation of Quotes that are like lookup. Active Record Scope
289 |
# File 'app/models/quote.rb', line 289 scope :like_lookup, ->(q) { where('quotes.reference_number ILIKE :term', { term: "%#{q}%" }) } |
.lookup ⇒ ActiveRecord::Relation<Quote>
A relation of Quotes that are lookup. Active Record Scope
288 |
# File 'app/models/quote.rb', line 288 scope :lookup, ->(q) { where('quotes.reference_number ILIKE :term', { term: q }) } |
.most_recent_first ⇒ ActiveRecord::Relation<Quote>
A relation of Quotes that are most recent first. Active Record Scope
280 |
# File 'app/models/quote.rb', line 280 scope :most_recent_first, -> { order(:created_at).reverse_order } |
.not_converted ⇒ ActiveRecord::Relation<Quote>
A relation of Quotes that are not converted. Active Record Scope
286 |
# File 'app/models/quote.rb', line 286 scope :not_converted, -> { where("not exists(select 1 from orders where quote_id = quotes.id and orders.state = 'invoiced')") } |
.open_quotes ⇒ ActiveRecord::Relation<Quote>
A relation of Quotes that are open quotes. Active Record Scope
276 |
# File 'app/models/quote.rb', line 276 scope :open_quotes, -> { where.not(state: %w[complete cancelled]) } |
.pending ⇒ ActiveRecord::Relation<Quote>
A relation of Quotes that are pending. Active Record Scope
278 |
# File 'app/models/quote.rb', line 278 scope :pending, -> { where(state: 'pending') } |
.sales_quotes ⇒ ActiveRecord::Relation<Quote>
A relation of Quotes that are sales quotes. Active Record Scope
292 |
# File 'app/models/quote.rb', line 292 scope :sales_quotes, -> { where(quote_type: SALES_QUOTE) } |
.states_for_select ⇒ Array<Array(String, String)>
State-machine states formatted for a Rails select helper —
[[human_name, machine_value], …]. Used by the CRM filter
dropdowns.
596 597 598 |
# File 'app/models/quote.rb', line 596 def self.states_for_select state_machines[:state].states.map { |s| [s.human_name, s.value] } end |
.with_contact_point_category ⇒ ActiveRecord::Relation<Quote>
A relation of Quotes that are with contact point category. Active Record Scope
290 |
# File 'app/models/quote.rb', line 290 scope :with_contact_point_category, ->(category) { where('exists(select 1 from contact_points_quotes cq inner join contact_points cp on cp.id = cq.contact_point_id and cq.quote_id = quotes.id where cp.category = ?)', category) } |
.with_email ⇒ ActiveRecord::Relation<Quote>
A relation of Quotes that are with email. Active Record Scope
291 |
# File 'app/models/quote.rb', line 291 scope :with_email, -> { with_contact_point_category(ContactPoint::EMAIL) } |
.with_line_items ⇒ ActiveRecord::Relation<Quote>
A relation of Quotes that are with line items. Active Record Scope
279 |
# File 'app/models/quote.rb', line 279 scope :with_line_items, -> { includes(line_items: { catalog_item: { store_item: :item } }) } |
.without_quofus ⇒ ActiveRecord::Relation<Quote>
A relation of Quotes that are without quofus. Active Record Scope
293 294 295 296 297 298 299 300 301 302 |
# File 'app/models/quote.rb', line 293 scope :without_quofus, -> { where(%{ not exists( select 1 from activities a where ( (a.resource_id = quotes.opportunity_id and a.resource_type = 'Opportunity') or (a.resource_id = quotes.id and a.resource_type = 'Quote') ) and a.activity_type_id IN (?) ) }, ActivityTypeConstants::QUOFUS_IDS) } |
Instance Method Details
#active_orders ⇒ ActiveRecord::Relation<Order>
Orders not cancelled or fraudulent.
238 |
# File 'app/models/quote.rb', line 238 has_many :active_orders, -> { where(Order[:state].not_in(%w[cancelled fraudulent])) }, class_name: 'Order' |
#activities ⇒ ActiveRecord::Relation<Activity>
Activity log entries for this quote.
242 |
# File 'app/models/quote.rb', line 242 has_many :activities, as: :resource, dependent: :nullify |
#addendums ⇒ Array<Upload>
PDF uploads that should be appended to the customer-facing quote
bundle — e.g. seasonal promotions, terms-and-conditions revisions,
warranty inserts. Only publications tagged for-quote AND either
the current Month-YYYY tag or permanent are returned, so an
April quote doesn't ship March's flyer.
921 922 923 924 925 926 927 928 |
# File 'app/models/quote.rb', line 921 def addendums current_month = Date.current.strftime('%B-%Y') # Use with_all_tags to require BOTH 'for-quote' AND the time-based tag (AND logic) publications = Item.publications.('for-quote', current_month.downcase) .or(Item.publications.('for-quote', 'permanent')) list = publications.map(&:upload) list.uniq.compact end |
#address_book ⇒ Array<Address>
Address picker source for the quote's "ship to" field — the
customer's home store warehouse first (for will-call quotes),
then every address on the customer record.
1584 1585 1586 |
# File 'app/models/quote.rb', line 1584 def address_book [customer.store.warehouse_address] + customer.addresses end |
#adjust_status_based_on_room_status(rc) ⇒ void
This method returns an undefined value.
Synchronises the quote's state machine with a child
RoomConfiguration that just changed status. Invoked from the
room state-machine after_transition so a room going
complete/cancelled marches the quote toward
installation_plans_complete, a room dropping back to draft
parks the quote in pending_project_details, and a room entering
design pushes the quote to ready_for_design.
1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 |
# File 'app/models/quote.rb', line 1123 def adjust_status_based_on_room_status(rc) Rails.logger.info "Quote Ref and ID #{reference_number}, #{id}, quote.adjust_status_based_on_room_status: self.state: #{state}, rc: #{rc.reference_number}, #{rc.id}, rc state: #{rc.state}, DateTime: #{Time.current}" self.do_not_set_totals = true if rc.complete? || rc.cancelled? Rails.logger.info "Quote Ref and ID #{reference_number}, #{id}, quote.adjust_status_based_on_room_status, BEFORE installation_plans_complete: self.state: #{state}, rc: #{rc.reference_number}, rc state: #{rc.state}" res = reload.installation_plans_complete unless complete? Rails.logger.info "Quote Ref and ID #{reference_number}, #{id}, quote.adjust_status_based_on_room_status, AFTER installation_plans_complete: self.state: #{state}, rc: #{rc.reference_number}, rc state: #{rc.state}, result of installation_plans_complete: #{res}" elsif rc.draft? pending_project_details if can_pending_project_details? && awaiting_completed_installation_plans? elsif rc.in_design? Rails.logger.info "Quote Ref and ID #{reference_number}, #{id}, quote.adjust_status_based_on_room_status, BEFORE ready_for_design: self.state: #{state}, rc: #{rc.reference_number}, rc state: #{rc.state}" res = ready_for_design Rails.logger.info "Quote Ref and ID #{reference_number}, #{id}, quote.adjust_status_based_on_room_status, AFTER ready_for_design: self.state: #{state}, rc: #{rc.reference_number}, rc state: #{rc.state}, result of ready_for_design: #{res}" end end |
#all_activities ⇒ ActiveRecord::Relation<Activity>
Every Activity linked to this quote or its parent
Opportunity — what the CRM "activities" tab shows. Reaches
through opportunity so quote and opp activities aren't split
across two tabs.
1575 1576 1577 |
# File 'app/models/quote.rb', line 1575 def all_activities opportunity.linked_activities end |
#all_orders_sold? ⇒ Boolean
Whether every order spawned from this quote has been invoiced —
used as the guard for closing out the quote once shipping is
done.
784 785 786 |
# File 'app/models/quote.rb', line 784 def all_orders_sold? orders.any? && orders.all?(&:invoiced?) end |
#all_participants ⇒ Object
Alias for Opportunity#all_participants
812 |
# File 'app/models/quote.rb', line 812 delegate :all_participants, to: :opportunity |
#all_suggested_items ⇒ Hash{Symbol=>Object}
Aggregate of recommended add-ons for this quote: goods grouped by
room configuration, services, a deduplicated goods list, and the
SKUs of those unique goods. Drives the "you might also need…"
cross-sell panel on the quote builder and quote PDF.
838 839 840 841 842 843 844 845 846 847 848 849 |
# File 'app/models/quote.rb', line 838 def all_suggested_items goods = suggested_items services = suggested_services unique_goods = [] goods.each do |_rc, lines| lines.each do |li| unique_goods << li unless unique_goods.any? { |si| si.item == li.item } end end unique_goods_skus = unique_goods.compact.uniq.map(&:sku) { goods:, services:, unique_goods:, unique_goods_skus: } end |
#all_support_cases ⇒ ActiveRecord::Relation<SupportCase>
Support cases linked to this quote (directly via the HABTM and
transitively through its room configurations), ordered newest
case-number first for the CRM sidebar.
556 557 558 559 560 |
# File 'app/models/quote.rb', line 556 def all_support_cases support_case_ids linked_support_cases.ids SupportCase.where(id: support_case_ids).order(Arel.sql('support_cases.case_number desc')) end |
#applies_for_smartinstall ⇒ Boolean
Whether this quote qualifies for SmartInstall — WarmlyYours's
in-house installation service for homeowners. Currently disabled
(return false) pending the smart-services refactor; the
original geofence/role/heated-items check is preserved below
the early return for when it's reactivated.
1614 1615 1616 1617 1618 1619 |
# File 'app/models/quote.rb', line 1614 def applies_for_smartinstall return false # added by Roman Aug 19th until service refactor return true if installation_is_within_range? && customer.is_homeowner? && has_selected_heated_items? false end |
#applies_for_smartsupport ⇒ Boolean
Whether this quote qualifies for SmartSupport — the
dealer/trade-pro version of the in-house service program (we send
an installer to support the trade pro on site).
1626 1627 1628 1629 1630 |
# File 'app/models/quote.rb', line 1626 def applies_for_smartsupport return true if customer.is_dealer_or_trade_pro_for_locator? && has_selected_heated_items? false end |
#apply_tier2_pricing? ⇒ Boolean
Whether or not to apply the tier2 pricing (customer discount) by default
537 538 539 |
# File 'app/models/quote.rb', line 537 def apply_tier2_pricing? is_sales_quote? end |
#available_contact_points ⇒ Array<ContactPoint>
Every ContactPoint reachable through this quote's opportunity
(primary party, customer, and every opportunity participant),
deduped — used to populate the recipient picker on the quote
transmission form.
954 955 956 957 958 959 |
# File 'app/models/quote.rb', line 954 def available_contact_points cps = opportunity.primary_party.contact_points.order(:position).to_a cps += opportunity.customer.contact_points.order(:position).to_a cps += opportunity.opportunity_participants.map { |op| op.party.contact_points.order(:position).to_a }.flatten cps.uniq end |
#available_contact_points_grouped_for_select ⇒ Hash{String=>Array<Array(String, Integer)>}
#available_contact_points reshaped into Rails grouped-select
input: { "Person Full Name" => [["detail (category)", id], …] }
so the transmission form renders an <optgroup> per party.
966 967 968 969 970 971 |
# File 'app/models/quote.rb', line 966 def available_contact_points_grouped_for_select available_contact_points.each_with_object({}) do |cp, hsh| hsh[cp.party.full_name] ||= [] hsh[cp.party.full_name] << ["#{cp.detail} (#{cp.category})", cp.id] end end |
#best_contact_point ⇒ ContactPoint?
Best contact point we can send the quote to — falls through quote
→ contact → customer until something transmittable (email/fax)
turns up. Used as the default to: when an agent clicks "Send"
without picking a recipient.
936 937 938 939 940 941 942 943 944 945 946 |
# File 'app/models/quote.rb', line 936 def best_contact_point contact_points.transmittable.first || (begin contact.contact_points.transmittable.first rescue StandardError nil end) || (begin customer.contact_points.transmittable.first rescue StandardError nil end) end |
#billing_address ⇒ Object
Alias for Customer#billing_address
171 |
# File 'app/models/quote.rb', line 171 delegate :billing_address, :company, to: :customer |
#build_activity ⇒ Activity
Builds (but does not save) a new Activity attached to this quote
with the quote's primary party already populated. Used by the CRM
activity-form helpers to seed a new activity without round-tripping
through nested form params.
547 548 549 |
# File 'app/models/quote.rb', line 547 def build_activity activities.build resource: self, party: primary_party end |
#buying_group ⇒ BuyingGroup
Buying group the quote is attributed to.
229 |
# File 'app/models/quote.rb', line 229 belongs_to :buying_group, optional: true |
#calculate_expiration_date ⇒ Date
Computed expiration date for the quote — the soonest of every
attached coupon's expiration and a default 60-day window from
created_at. Persisted by the #set_expiration_date before-update
callback so the customer sees a stable "valid through" date on
the PDF.
1054 1055 1056 1057 1058 1059 1060 1061 |
# File 'app/models/quote.rb', line 1054 def calculate_expiration_date expirations = [] + (begin coupons.filter_map(&:expiration_date) rescue StandardError [] end) + [created_at + 60.days] expirations.min.to_date end |
#can_be_moved? ⇒ Boolean
Whether this quote can be reassigned to a different opportunity.
Locked once the quote has at least one non-cart, non-cancelled
Order so we don't orphan invoiced sales from their opportunity.
567 568 569 |
# File 'app/models/quote.rb', line 567 def can_be_moved? orders.active.non_carts.blank? end |
#can_be_transmitted? ⇒ Boolean
Whether the customer-facing transmission flow is allowed in this
state. Awaiting-transmission and pending-project-details quotes
can be sent for review; complete quotes can be re-sent for the
confirmation copy.
765 766 767 |
# File 'app/models/quote.rb', line 765 def can_be_transmitted? awaiting_transmission? || pending_project_details? || complete? end |
#can_convert? ⇒ Object
Alias for Convert_to_order_service#can_convert?
1198 |
# File 'app/models/quote.rb', line 1198 delegate :can_convert?, to: :convert_to_order_service |
#cancelable? ⇒ Boolean
State-machine guard for the cancel event — the quote can be
cancelled as long as no surviving order points at it.
1282 1283 1284 |
# File 'app/models/quote.rb', line 1282 def cancelable? orders.not_cancelled.empty? end |
#catalog ⇒ Object
Alias for Customer#catalog
170 |
# File 'app/models/quote.rb', line 170 delegate :catalog, to: :customer |
#check_pre_pack ⇒ void
This method returns an undefined value.
after_save callback that auto-transitions the quote into
pre_pack when it's awaiting transmission and the warehouse
signals that estimated packaging needs to happen first (see
#stop_for_pre_pack?). Skipped when every delivery is already
in pre-pack.
1771 1772 1773 |
# File 'app/models/quote.rb', line 1771 def check_pre_pack request_estimated_packaging if awaiting_transmission? && can_request_estimated_packaging? && !pre_pack? && !deliveries.all?(&:pre_pack?) && stop_for_pre_pack? end |
#communications ⇒ ActiveRecord::Relation<Communication>
Communications (emails, calls) logged against this quote.
244 |
# File 'app/models/quote.rb', line 244 has_many :communications, as: :resource, dependent: :nullify |
#company ⇒ Object
Alias for Customer#company
171 |
# File 'app/models/quote.rb', line 171 delegate :billing_address, :company, to: :customer |
#contact ⇒ Object
Alias for Opportunity#contact
168 |
# File 'app/models/quote.rb', line 168 delegate :customer, :contact, to: :opportunity |
#contact_points ⇒ ActiveRecord::Relation<ContactPoint>
Contact points attached to this quote.
253 |
# File 'app/models/quote.rb', line 253 has_and_belongs_to_many :contact_points, inverse_of: :quotes |
#convert_to_order_service ⇒ Quote::ConvertToOrder
Memoised ConvertToOrder service for this quote. The
service encapsulates the logic to turn a complete quote into one
or more Orders (one per delivery / origin) — view and controller
code calls quote.convert_to_order_service.can_convert? and
quote.to_order rather than instantiating the service themselves.
1194 1195 1196 |
# File 'app/models/quote.rb', line 1194 def convert_to_order_service @convert_to_order_service ||= Quote::ConvertToOrder.new(self) end |
#country ⇒ String?
ISO country of the Store this quote was placed against — used
by tax / shipping / addendum logic to branch on US vs CA vs other.
Swallows nil-chain errors and returns nil rather than raising.
1800 1801 1802 1803 1804 |
# File 'app/models/quote.rb', line 1800 def country customer.catalog.store.country rescue StandardError nil end |
#crm_link ⇒ String
Internal CRM URL for this quote's show page. Shared Activity,
Communication, and notification helpers call this to produce the
"open in CRM" link without each having to know the routes module.
1183 1184 1185 |
# File 'app/models/quote.rb', line 1183 def crm_link UrlHelper.instance.quote_path(self) end |
#currency_symbol ⇒ String
Currency glyph ($, €, £, …) for this quote, derived from the
quote's currency ISO code via the money gem. Used wherever the
quote PDF / CRM views need to print prices.
1043 1044 1045 |
# File 'app/models/quote.rb', line 1043 def currency_symbol Money::Currency.new(currency).symbol end |
#customer ⇒ Object
Alias for Opportunity#customer
168 |
# File 'app/models/quote.rb', line 168 delegate :customer, :contact, to: :opportunity |
#deep_dup ⇒ Quote
Deep-clones the quote and its non-shipping parent LineItems, plus
the contact-points join, but resets identity / lifecycle columns
(state goes back to pending, tax fields drop, parent_id and
delivery_id are stripped) so the duplicate can be saved as a
fresh draft. Used by "Revise quote" CRM action and by the
quote-revision workflow.
129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 |
# File 'app/models/quote.rb', line 129 def deep_dup deep_clone(include: :contact_points) do |original, copy| next unless copy.is_a?(Quote) copy.state = 'pending' original.line_items.parents_only.non_shipping.each do |li| li_attrs = li.attributes.dup.symbolize_keys li_attrs = li_attrs.except(:id, :resource_type, :resource_id, :delivery_id, :tax_total, :tax_rate, :tax_type, :resource_tax_rate_id, :original_delivery_id, :children_count, :parent_id) li_attrs[:original_delivery_id] = li[:delivery_id] li_attrs[:discounted_price] = li_attrs[:price] if li_attrs[:discounted_price].to_f.zero? && li_attrs[:price].to_f.positive? copy.line_items.new(li_attrs) end end end |
#dependents(limit: 5) ⇒ Hash{Symbol=>ActiveRecord::Relation}
Quote dependents that block destruction — orders, child
revisions, and customer communications. Returned as a hash for
the "cannot delete" UI to enumerate why.
A quote with communications to a customer cannot be deleted.
A quote used as a parent of another quote is also restricted
(clear the children first). A quote with orders cannot be
deleted.
1253 1254 1255 1256 1257 1258 1259 |
# File 'app/models/quote.rb', line 1253 def dependents(limit: 5) { orders: orders.limit(limit), children: children.limit(limit), communications: communications.limit(limit) } end |
#editing_locked? ⇒ Boolean
True once the quote is in a terminal state — used by the
form/controller layer to read-only the line item rows so a
finalised or cancelled quote can't be edited in place.
1034 1035 1036 |
# File 'app/models/quote.rb', line 1034 def editing_locked? cancelled? || complete? end |
#effective_date_for_coupon ⇒ Date
Date used by the coupon engine to evaluate "is this coupon
currently valid?" against start_date/end_date windows. Prefers
an explicit override_coupon_date, then the date the quote was
completed, then created — falling back to today for brand-new
in-memory quotes. Keeps coupon evaluation stable after the quote
is finalised even if the coupon's window later shifts.
638 639 640 |
# File 'app/models/quote.rb', line 638 def effective_date_for_coupon override_coupon_date.presence || complete_datetime.try(:to_date) || created_at.try(:to_date) || Date.current end |
#exclude_manually_initiated_event?(_event) ⇒ false
State-machine helper — Models::Auditable hook used to suppress
certain manually-initiated events from PaperTrail's audit trail.
Quotes don't suppress anything, so this always returns false.
587 588 589 |
# File 'app/models/quote.rb', line 587 def exclude_manually_initiated_event?(_event) false end |
#extra_plans_file_name(with_extension = true) ⇒ String
Filename for the secondary "installation plans" PDF that ships
alongside the main quote — same naming convention as #file_name
with a _plans suffix so attachments don't collide.
880 881 882 |
# File 'app/models/quote.rb', line 880 def extra_plans_file_name(with_extension = true) "quote_#{reference_number}_plans#{'.pdf' if with_extension}" end |
#file_name(with_extension = true) ⇒ String
Filename used when the customer-facing quote PDF is downloaded or
attached to an email — reference-number-stamped so a customer with
multiple revisions can tell them apart.
870 871 872 |
# File 'app/models/quote.rb', line 870 def file_name(with_extension = true) "quote_#{reference_number}#{'.pdf' if with_extension}" end |
#get_next_reference_number ⇒ String
Pulls the next quote reference number from the
quotes_reference_numbers_seq Postgres sequence and prefixes it
with the quote-type code (e.g. SQ, MQ, TQ). Sequence-based
so concurrent quote creates can't collide.
1487 1488 1489 1490 1491 1492 1493 1494 |
# File 'app/models/quote.rb', line 1487 def get_next_reference_number ref_base = begin self.class.lease_connection.execute("SELECT nextval('quotes_reference_numbers_seq') AS reference_number").first['reference_number'] rescue StandardError nil end "#{quote_type}#{ref_base}" end |
#goods_product_line_ids ⇒ Array<Integer>
Distinct primary product-line ids represented by the goods on this
quote. Drives the "what kind of project is this?" routing in the
CRM (heated floors vs snow melt vs towel warmers) and the addendum
selection for the customer-facing PDF.
532 533 534 |
# File 'app/models/quote.rb', line 532 def goods_product_line_ids line_items.goods.joins(:item).where.not(Item[:primary_product_line_id].eq(nil)).pluck(Arel.sql('distinct items.primary_product_line_id')) end |
#has_discounts? ⇒ Boolean
True when at least one line item carries a discount — drives the
"savings" badge on the customer-facing PDF and unlocks the
discount-detail rows.
910 911 912 |
# File 'app/models/quote.rb', line 910 def has_discounts? line_items.any?(&:is_discounted?) end |
#has_floor_heating_items? ⇒ Boolean
Auto-default predicates for the four "include X document" checkboxes
on the rep's online-quote configuration form. Each returns true
when the quote contents indicate the document is relevant; the form
uses these to pre-check the boxes so the rep just confirms what
they'd have selected anyway.
216 217 218 |
# File 'app/models/quote.rb', line 216 def has_floor_heating_items? line_items.floor_heating_elements.exists? end |
#has_no_instant_quoting_rooms ⇒ true
Validation stub left over from the deprecated Instant Quote (IQ)
system — kept registered as a validate :has_no_instant_quoting_rooms
so removing it is a separate, easy-to-revert change.
1519 1520 1521 1522 |
# File 'app/models/quote.rb', line 1519 def has_no_instant_quoting_rooms # Legacy IQ validation - no longer needed after IQ system deprecated true end |
#has_onsite_smartsupport? ⇒ Boolean
Whether on-site SmartSupport is available — install is within the
service-distance radius from the WarmlyYours HQ.
1647 1648 1649 1650 1651 |
# File 'app/models/quote.rb', line 1647 def has_onsite_smartsupport? return true if service_distance.present? && (service_distance < SMART_SERVICES_MAX_DISTANCE) false end |
#has_pre_pack_deliveries? ⇒ Boolean
True when the quote itself is in pre_pack or any of its
deliveries is in pre-pack. Used by the warehouse dashboard.
1742 1743 1744 |
# File 'app/models/quote.rb', line 1742 def has_pre_pack_deliveries? pre_pack? || deliveries.any?(&:pre_pack?) end |
#has_selected_heated_items? ⇒ Boolean
Whether the quote actually contains items eligible for the
smart-services program — branches on customer type so we look at
the right side of the eligibility data.
1658 1659 1660 1661 1662 1663 1664 1665 1666 |
# File 'app/models/quote.rb', line 1658 def has_selected_heated_items? if customer.is_homeowner? smartinstall_data.present? elsif customer.is_dealer_or_trade_pro_for_locator? smartsupport_info.present? else false end end |
#has_snow_melting_items? ⇒ Boolean
Whether the quote contains snow-melting items.
222 223 224 |
# File 'app/models/quote.rb', line 222 def has_snow_melting_items? line_items.snow_melting_elements.exists? end |
#has_suggested_add_ons? ⇒ Boolean
True when the rep has opted into showing suggested add-ons AND
there is at least one actual suggestion to surface. Used by both
show.html.erb (to gate the section render) and the sidebar nav
(to gate the link) so the two stay in sync — no more "nav says
there's a section but the page has nothing." Memoized because
all_suggested_items walks every line item.
857 858 859 860 861 862 |
# File 'app/models/quote.rb', line 857 def has_suggested_add_ons? return @has_suggested_add_ons unless @has_suggested_add_ons.nil? @has_suggested_add_ons = show_suggested_add_ons? && Array(all_suggested_items[:unique_goods]).any? { |li| li.item.present? } end |
#installation_country_iso ⇒ Object
Alias for Opportunity#installation_country_iso
173 |
# File 'app/models/quote.rb', line 173 delegate :installation_postal_code, :installation_state_code, :installation_country_iso, :installation_country_iso3, to: :opportunity |
#installation_country_iso3 ⇒ Object
Alias for Opportunity#installation_country_iso3
173 |
# File 'app/models/quote.rb', line 173 delegate :installation_postal_code, :installation_state_code, :installation_country_iso, :installation_country_iso3, to: :opportunity |
#installation_is_within_range? ⇒ Boolean
True when #service_distance is non-nil and inside the
SMART_SERVICES_MAX_DISTANCE radius (currently 100 mi from HQ).
1672 1673 1674 |
# File 'app/models/quote.rb', line 1672 def installation_is_within_range? service_distance.present? && (service_distance <= SMART_SERVICES_MAX_DISTANCE) # within 100 miles from office end |
#installation_postal_code ⇒ Object
Alias for Opportunity#installation_postal_code
173 |
# File 'app/models/quote.rb', line 173 delegate :installation_postal_code, :installation_state_code, :installation_country_iso, :installation_country_iso3, to: :opportunity |
#installation_state_code ⇒ Object
Alias for Opportunity#installation_state_code
173 |
# File 'app/models/quote.rb', line 173 delegate :installation_postal_code, :installation_state_code, :installation_country_iso, :installation_country_iso3, to: :opportunity |
#insulation_sq_ft ⇒ Integer
Total square footage of underlayment / insulation across every
indoor room on the quote — used by the cork/insulation
cross-sell logic and by the catalog to suggest the right
underlayment quantity. Outdoor rooms (snow-melt) are excluded
because they don't take indoor underlayment.
1369 1370 1371 1372 1373 1374 1375 |
# File 'app/models/quote.rb', line 1369 def insulation_sq_ft sqft = 0 room_configurations.each do |rc| sqft += rc.insulation_surface.to_i if rc.room_type&.is_indoor? && (rc.insulation_surface.to_i > 0) end sqft end |
#is_first_quote? ⇒ Boolean
True when this is the original quote rather than a revision —
i.e. nothing pointed at as parent_id.
1350 1351 1352 |
# File 'app/models/quote.rb', line 1350 def is_first_quote? parent_id.nil? end |
#is_revision? ⇒ Boolean
True when this quote was spawned via #deep_dup from an earlier
quote — the inverse of #is_first_quote?.
1358 1359 1360 |
# File 'app/models/quote.rb', line 1358 def is_revision? parent_id.present? end |
#is_sales_quote? ⇒ Boolean
True for the standard customer-facing sales quote (SQ) —
the main quote-type in production. Marketing (MQ) and tech
(TQ) quotes return false.
1342 1343 1344 |
# File 'app/models/quote.rb', line 1342 def is_sales_quote? quote_type == SALES_QUOTE end |
#is_smart_service_quote? ⇒ Boolean
True when every non-shipping line on the quote is a smart-service
SKU — used to switch the PDF / fulfillment path to the
services-only template.
1681 1682 1683 |
# File 'app/models/quote.rb', line 1681 def is_smart_service_quote? line_items.non_shipping.all?(&:is_smart_service?) end |
#linked_support_cases ⇒ ActiveRecord::Relation<LinkedSupportCase>
Support cases linked via the quote's room configurations.
248 |
# File 'app/models/quote.rb', line 248 has_many :linked_support_cases, through: :room_configurations, source: :support_cases |
#local_sales_rep ⇒ Object
Alias for Customer#local_sales_rep
172 |
# File 'app/models/quote.rb', line 172 delegate :primary_sales_rep, :secondary_sales_rep, :local_sales_rep, to: :customer |
#lookup_link ⇒ String
Public-facing URL for the customer's "look up your quote" page
(warmlyyours.com /quote-lookup). Embedded in transmission emails
so the customer can pull up their quote without an account.
1780 1781 1782 |
# File 'app/models/quote.rb', line 1780 def lookup_link "https://#{WEB_HOSTNAME}/quote-lookup?quote_id=#{id}" end |
#main_rep ⇒ Employee?
Primary rep this quote "belongs to" for commission and CRM
ownership. Aliases #primary_sales_rep so view code can call a
consistent name across Quote, Order, and Opportunity.
988 989 990 |
# File 'app/models/quote.rb', line 988 def main_rep primary_sales_rep end |
#material_alerts ⇒ ActiveRecord::Relation<MaterialAlert>
Material alerts raised for this quote.
250 |
# File 'app/models/quote.rb', line 250 has_many :material_alerts, dependent: :destroy |
#messaging_logs ⇒ ActiveRecord::Relation<MessagingLog>
Messaging logs for this quote.
246 |
# File 'app/models/quote.rb', line 246 has_many :messaging_logs, dependent: :destroy, as: :resource |
#name ⇒ String
Short human label like Quote #SQ12345. Used in notifications,
mailers, and audit-trail rows where reference number alone needs
the word "Quote" in front of it.
1154 1155 1156 |
# File 'app/models/quote.rb', line 1154 def name "Quote ##{reference_number}" end |
#ok_to_customer_cancel? ⇒ Boolean
Whether the customer is allowed to self-cancel from the public
quote-lookup page — only when the quote is still pending and has
no surviving orders attached.
1274 1275 1276 |
# File 'app/models/quote.rb', line 1274 def ok_to_customer_cancel? CUSTOMER_CANCELABLE_STATES.include?(state.to_sym) && orders.not_cancelled.empty? end |
#ok_to_delete? ⇒ Boolean
True when #dependents is empty across the board — the precondition
for the destroy button.
1265 1266 1267 |
# File 'app/models/quote.rb', line 1265 def ok_to_delete? dependents.values.flatten.empty? end |
#open_activities_counter ⇒ Integer
Count of CRM follow-ups still open against this quote (or its
opportunity), filtered to the activities that show by default in
the rep's queue. Drives the badge on the quote sidebar.
1719 1720 1721 |
# File 'app/models/quote.rb', line 1719 def open_activities_counter all_activities.open_activities.visible_by_default.size end |
#opportunity ⇒ Opportunity
Opportunity this quote belongs to.
227 |
# File 'app/models/quote.rb', line 227 belongs_to :opportunity, optional: true |
#orders ⇒ ActiveRecord::Relation<Order>
Orders placed from this quote.
236 |
# File 'app/models/quote.rb', line 236 has_many :orders |
#participants_options_for_select ⇒ Array<Array(String, Integer)>?
Opportunity participants formatted for a Rails select helper —
[[full_name, party_id], …]. Drives the "additional participants"
multi-select on the quote form.
1593 1594 1595 |
# File 'app/models/quote.rb', line 1593 def opportunity&.opportunity_participants&.map { |scp| [scp.party.full_name, scp.party_id] } end |
#placed_order ⇒ Order?
The placed (payment-completed) order for this quote, if any — drives the
"already ordered" gate on the customer-facing /ql/ page. nil while
the customer is still in checkout, so they can revisit the quote freely.
1237 1238 1239 1240 |
# File 'app/models/quote.rb', line 1237 def placed_order orders.where.not(state: UNPAID_ORDER_STATES + %w[cancelled fraudulent]) .order(created_at: :desc).first end |
#post_communication_queued_hook(_params = {}) ⇒ void
This method returns an undefined value.
Hook fired by the communication subsystem the moment a transmission
email/fax is enqueued — finalises the quote if every other gate has
already cleared. Called by the Communication observer after the
outbound message is queued for delivery.
795 796 797 798 |
# File 'app/models/quote.rb', line 795 def post_communication_queued_hook(_params = {}) # Handles post transmission complete if can_complete? end |
#post_communication_sent_hook(_params = {}) ⇒ void
This method returns an undefined value.
Hook fired once the transmission has actually been sent by the
provider — finalises the quote if every other gate has cleared.
Mirror of #post_communication_queued_hook for providers that
emit a separate "sent" callback.
807 808 809 810 |
# File 'app/models/quote.rb', line 807 def post_communication_sent_hook(_params = {}) # Handles post transmission complete if can_complete? end |
#pre_packable? ⇒ Boolean
Whether the warehouse can run an estimated-packaging pre-pack on
this quote — at least one delivery must have a shipment that
isn't already packed or pre-packed.
1291 1292 1293 |
# File 'app/models/quote.rb', line 1291 def pre_packable? deliveries.any? && deliveries.any? { |d| d.shipments.any? { |s| !s.packed_or_pre_packed? } } end |
#pre_planning_invite_url ⇒ String
Booking link for the pre-project technical meeting, pre-filled with the
recipient's name/email and this quote's reference so the scheduling form
arrives populated.
710 711 712 713 714 |
# File 'app/models/quote.rb', line 710 def pre_planning_invite_url uri = Addressable::URI.parse(ContactConstants::ZOOM_PRE_PLANNING) uri.query_values = { name: recipient_name, email: recipient_email, a1: reference_number } uri.to_s end |
#prefix_for_reference_number ⇒ String
Fallback prefix used when #quote_type hasn't been chosen yet —
derives the two-letter code from the parent Opportunity's type
(so a sales opp gives SQ, marketing opp MQ, etc.).
1510 1511 1512 |
# File 'app/models/quote.rb', line 1510 def prefix_for_reference_number "#{opportunity.opportunity_type}Q" end |
#prepared_for_name ⇒ String
1316 1317 1318 |
# File 'app/models/quote.rb', line 1316 def prepared_for_name (contact || customer).name end |
#prevent_recalculate_shipping? ⇒ Boolean
Guard used by Models::ShipQuotable to skip live ShipEngine rate
quoting — already-finalised quotes don't requote, and the flag
also blocks re-entrancy while a rate request is in flight.
1529 1530 1531 |
# File 'app/models/quote.rb', line 1529 def prevent_recalculate_shipping? %i[complete cancelled].include?(state.to_sym) || retrieving_shipping_costs? end |
#pricing_program_description ⇒ String
Human label for the pricing program in effect on this quote — the
tier2 coupon title when the customer is on a dealer/contractor
tier, otherwise "MSRP/Catalog". Surfaced on the quote PDF and
on the CRM "pricing" badge.
1556 1557 1558 |
# File 'app/models/quote.rb', line 1556 def pricing_program_description tier2_program_pricing_coupon.try(:title) || 'MSRP/Catalog' end |
#pricing_program_discount ⇒ Integer
Percentage discount the active tier2 pricing program applies to
goods, or 0 when the quote is on plain MSRP. Companion to
#pricing_program_description.
1565 1566 1567 |
# File 'app/models/quote.rb', line 1565 def pricing_program_discount (tier2_program_pricing_coupon.try(:amount_goods).presence || 0).to_i end |
#primary_party ⇒ Party
577 578 579 |
# File 'app/models/quote.rb', line 577 def primary_party contact || customer end |
#primary_sales_rep ⇒ Object
Alias for Customer#primary_sales_rep
172 |
# File 'app/models/quote.rb', line 172 delegate :primary_sales_rep, :secondary_sales_rep, :local_sales_rep, to: :customer |
#ready_to_complete? ⇒ Boolean
Coarser readiness check than #will_complete? — only requires
that no room is in a half-resolved state. Used by the UI to grey
out the "Send to Customer" button before margins/etc are
validated.
890 891 892 893 894 |
# File 'app/models/quote.rb', line 890 def ready_to_complete? # here we want good shipping address (allow nil if instant_quoting), and any cancelled rooms to be deleted or resolved before we can get a complete and convertable quote # shipping_address_nil_or_valid? && all_rooms_complete_or_cancelled_or_draft? end |
#recipient_email ⇒ String?
Email address the quote should be sent to — prefers an explicit
quote-level contact-point email, then the primary party's email,
then the customer's. Used as the default to: on transmission.
690 691 692 693 694 |
# File 'app/models/quote.rb', line 690 def recipient_email contact_points.emails.first&.detail || primary_party&.email || customer&.email end |
#recipient_name ⇒ String?
Best display name for the person this quote is addressed to —
walks #primary_party → shipping address → customer until it
finds one. Used as the salutation on the quote PDF and in email.
681 682 683 |
# File 'app/models/quote.rb', line 681 def recipient_name primary_party&.name || shipping_address&.person_name || customer&.name end |
#reference_number_with_name ⇒ String
Reference number plus the rep-supplied free-text suffix (the rep
uses this slot for the project name). Format: SQ12345 - Smith Master Bath. Used as the display label in CRM lists and on the
quote PDF header.
1145 1146 1147 |
# File 'app/models/quote.rb', line 1145 def reference_number_with_name [reference_number, suffix].compact.join(' - ') end |
#reference_number_with_opp_name ⇒ String
Reference number with the parent Opportunity's name appended —
used in dropdowns where the rep needs to disambiguate between
multiple quotes on the same opportunity.
1163 1164 1165 |
# File 'app/models/quote.rb', line 1163 def reference_number_with_opp_name "#{reference_number} #{opportunity.try(:name)}" end |
#reference_number_with_opp_name_when_specified ⇒ String
#reference_number_with_opp_name but skips the appended job name
when the opportunity has a placeholder/unknown name (so we don't
render "SQ12345 Job for Customer XYZ" — just SQ12345).
1172 1173 1174 1175 1176 |
# File 'app/models/quote.rb', line 1172 def reference_number_with_opp_name_when_specified s = reference_number.to_s s += " #{opportunity.name}" unless opportunity.nil? || opportunity.unknown_job_name? s end |
#request_plans_or_complete ⇒ void
This method returns an undefined value.
State-machine driver that nudges the quote forward (or back) based
on its current room-configuration mix and line-item presence.
Called from the rooms controller after a room edit/destroy so the
quote auto-advances to awaiting_transmission,
awaiting_completed_installation_plans, or
pending_project_details without the rep having to click a state
button. No-op once the quote is complete or cancelled.
1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 |
# File 'app/models/quote.rb', line 1001 def request_plans_or_complete return if complete? || cancelled? # Let's see if this is still necessary, as far as i can find it's only on quotes_controller # enter_rooms and room destroy that # this is called and the room association is already up to date at that point # room_configurations.each(&:reload) self.do_not_set_totals = true log_prefix = "Quote(#{id}):request_plans_or_complete" # If no room but line items, let's go straight to awaiting transmission if room_configurations.empty? && line_items.present? logger.info "#{log_prefix} no rooms but line present. attempting ready to transmit" ready_to_transmit! if can_ready_to_transmit? # If all rooms are complete then set quote to be awaiting_transmission elsif can_installation_plans_complete? logger.info "#{log_prefix} installation plan can complete, trying" installation_plans_complete! elsif can_ready_for_design? logger.info "#{log_prefix} can ready for design, trying" ready_for_design! elsif can_pending_project_details? logger.info "#{log_prefix} no line items and nothing else happening, go back to draft" pending_project_details! else logger.info "#{log_prefix} reverting to draft instant quoting rooms" # Legacy IQ cleanup removed - no longer needed after IQ system deprecated end end |
#reset_discount_on_ships_economy_change ⇒ Boolean? (protected)
after_save callback: when the rep flips a quote between economy
and standard ground shipping, drop any previously-applied
shipping discount so the next pricing pass recalculates against
the new shipping basis. Item-level pricing is left alone.
1842 1843 1844 1845 1846 1847 |
# File 'app/models/quote.rb', line 1842 def reset_discount_on_ships_economy_change return unless saved_change_to_ships_economy? reset_discount(reset_item_pricing: false) true end |
#rma ⇒ Rma
RMA this quote was created from (advance replacements).
231 |
# File 'app/models/quote.rb', line 231 belongs_to :rma, optional: true |
#schedule_follow_up(follow_up_task_type: nil) ⇒ Activity?
Creates (or replaces) a quote-centric follow-up activity on the
opportunity so a rep is reminded to chase the customer. Cancels
any existing opp-level follow-ups first, and skips entirely for
anonymous Quote-Builder guests with no email/phone.
1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 |
# File 'app/models/quote.rb', line 1468 def schedule_follow_up(follow_up_task_type: nil) # Don't create follow-up activities for guest customers without contact info # These are anonymous Quote Builder users who haven't provided email/phone return if customer.guest? && !opportunity.emailable? && !opportunity.voice_callable? return if opportunity.activities.quofu.open_activities.present? # cancel oppfu follow ups opportunity.cancel_oppfu # recreate a quofu centric follow up opportunity.create_follow_up_activity(follow_up_task_type:) end |
#secondary_sales_rep ⇒ Object
Alias for Customer#secondary_sales_rep
172 |
# File 'app/models/quote.rb', line 172 delegate :primary_sales_rep, :secondary_sales_rep, :local_sales_rep, to: :customer |
#selection_name ⇒ String
Label used by the global Ransack/select2 lookup widget when this
quote is offered as a search result. Aliases
#reference_number_with_name.
1398 1399 1400 |
# File 'app/models/quote.rb', line 1398 def selection_name reference_number_with_name end |
#send_profit_review_notification ⇒ void
This method returns an undefined value.
Notifies internal staff (margin desk / pricing) that this quote
has entered profit_review so they can review the pricing
exception. Wired up as an after_transition from the state
machine.
1603 1604 1605 |
# File 'app/models/quote.rb', line 1603 def send_profit_review_notification OrdersMailer.quote_profit_review_notification(self).deliver_later end |
#sender ⇒ Employee?
Rep treated as the "from" on outbound communications and
notifications about this quote. Falls through customer-attached
primary rep → opportunity-attached primary rep → customer's
primary rep so an orphan quote without a directly-assigned rep
still has a sender.
1327 1328 1329 1330 1331 1332 1333 1334 1335 |
# File 'app/models/quote.rb', line 1327 def sender if primary_sales_rep primary_sales_rep elsif opportunity.primary_sales_rep opportunity.primary_sales_rep elsif customer.primary_sales_rep customer.primary_sales_rep end end |
#service_distance ⇒ Float?
Driving distance (in miles) from the WarmlyYours Lake Zurich HQ
to the installation postal code on this quote — the gating
measure for SmartInstall / SmartSupport eligibility (see
SMART_SERVICES_MAX_DISTANCE).
1691 1692 1693 1694 |
# File 'app/models/quote.rb', line 1691 def service_distance zip_code = opportunity.installation_postal_code SmartServicesController.helpers.calculate_distance_from_lz(zip_code) end |
#set_currency ⇒ String?
Pulls the currency from the customer's home Store and writes it
onto the quote so prices print in the right denomination. Used
when a quote is created on behalf of a customer whose store
differs from the agent's default.
1082 1083 1084 1085 1086 1087 1088 1089 |
# File 'app/models/quote.rb', line 1082 def set_currency store_currency = begin opportunity.customer.store.currency rescue StandardError nil end self.currency = store_currency if store_currency end |
#set_default_quote_type ⇒ void (protected)
This method returns an undefined value.
Defaults the quote type from the reference-number prefix and re-prefixes
the reference number when the type changes.
1829 1830 1831 1832 1833 1834 |
# File 'app/models/quote.rb', line 1829 def set_default_quote_type self.quote_type ||= prefix_for_reference_number return unless quote_type_changed? && reference_number.present? self.reference_number = "#{quote_type}#{reference_number[2..50]}" end |
#set_expiration_date ⇒ void (protected)
This method returns an undefined value.
Recalculates the expiration date on update (skipped once complete).
1810 1811 1812 1813 1814 |
# File 'app/models/quote.rb', line 1810 def set_expiration_date return if complete? self.expiration_date = calculate_expiration_date end |
#set_min_profit_markup ⇒ void (protected)
This method returns an undefined value.
Sets the minimum profit markup from the quote type's default sales markup.
1818 1819 1820 1821 1822 1823 1824 |
# File 'app/models/quote.rb', line 1818 def set_min_profit_markup self.min_profit_markup = if quote_type == 'SQ' default_sales_markup else 0 end end |
#set_opportunity_value ⇒ void
This method returns an undefined value.
after_commit hook that recomputes the parent Opportunity's
rolled-up dollar value when this quote's totals change.
1381 1382 1383 |
# File 'app/models/quote.rb', line 1381 def set_opportunity_value opportunity&.calculate_value end |
#set_priority(save = true) ⇒ String, true
Computes the highest urgency among the quote's room
configurations and writes it onto priority (urgent, rush,
standard, etc.). Drives badge colour in the rep queue.
1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 |
# File 'app/models/quote.rb', line 1100 def set_priority(save = true) highest_priority = begin RoomConfiguration::PRIORITIES[room_configurations.map(&:priority_urgency_factor).min] rescue StandardError 'standard' end if save update_attribute(:priority, highest_priority) else self.priority = highest_priority end end |
#set_reference_number ⇒ String
before_create callback that stamps #reference_number from the
Postgres sequence unless the caller (e.g. data import) has already
supplied one.
1501 1502 1503 |
# File 'app/models/quote.rb', line 1501 def set_reference_number self.reference_number ||= get_next_reference_number end |
#shipping_address_nil_or_valid? ⇒ Boolean
True when the quote either has no shipping address or has one
that passes its own validations — used as a precondition before
transitioning to states that require shippable goods.
901 902 903 |
# File 'app/models/quote.rb', line 901 def shipping_address_nil_or_valid? shipping_address.nil? || shipping_address.valid? end |
#short_lookup_link ⇒ String
Short URL printed on quote PDFs / encoded in their QR code. Resolves to
an anonymous read-only view of the quote on www. The id is wrapped in
an encrypted token (see Encryption.encrypt_string) so the on-the-wire
path is non-enumerable — anyone who knows the format can't get to a
neighbouring quote by tweaking the digits.
1791 1792 1793 |
# File 'app/models/quote.rb', line 1791 def short_lookup_link "https://#{WEB_HOSTNAME}/ql/#{Encryption.encrypt_string(id.to_s)}" end |
#show_pre_planning_meeting? ⇒ Boolean
Whether to offer the customer a pre-project technical meeting. Starts
simply: any quote above PRE_PLANNING_MEETING_MIN_TOTAL. Shared by the
transmission email (Liquid::QuoteDrop) and the /ql/ viewer.
701 702 703 |
# File 'app/models/quote.rb', line 701 def show_pre_planning_meeting? total.to_f > PRE_PLANNING_MEETING_MIN_TOTAL end |
#smartsupport_info ⇒ Hash?
SmartSupport metadata bundle (line items eligible for the program,
estimated visit time, etc.) when the install location is within
service range; nil otherwise.
1637 1638 1639 1640 1641 |
# File 'app/models/quote.rb', line 1637 def smartsupport_info return nil if service_distance.nil? smartsupport_data(service_distance) end |
#sms_enabled_numbers ⇒ Array<String>
Distinct SMS-capable numbers for every participant on this quote's
opportunity, formatted for the SMS gateway. Drives the recipient
list when the rep texts an update from the quote page.
819 820 821 |
# File 'app/models/quote.rb', line 819 def sms_enabled_numbers ContactPoint.joins(:party).merge(all_participants).sms_numbers.order(:detail).map(&:formatted_for_sms).uniq end |
#sms_messages ⇒ ActiveRecord::Relation<SmsMessage>
Two-way SMS history for #sms_enabled_numbers — the message
thread shown on the quote sidebar.
827 828 829 |
# File 'app/models/quote.rb', line 827 def SmsMessage.for_numbers(sms_enabled_numbers) end |
#sold? ⇒ Boolean
True once at least one non-cart, non-cancelled Order exists
against this quote — i.e. the customer accepted and we
transmitted.
1219 1220 1221 |
# File 'app/models/quote.rb', line 1219 def sold? orders.non_carts.active.present? end |
#special_ordering_instructions ⇒ String?
Free-text "special instructions" stored on the billing-address
party — printed on the warehouse pick ticket when present (e.g.
"no liftgate", "call before delivery"). Swallows nil-chain errors
for headless test fixtures.
1069 1070 1071 1072 1073 |
# File 'app/models/quote.rb', line 1069 def special_ordering_instructions customer.billing_address.party.ordering_instructions rescue StandardError nil end |
#stop_for_pre_pack? ⇒ Boolean
State-machine guard — true when transmission must wait for the
warehouse to provide an estimated-packaging quote. Skipped for
e-commerce, no-shipping-address, empty-line, or already-packed
cases. The list of reasons (need_to_pre_pack_reasons) keeps the
rep apprised of why the quote is parked.
1753 1754 1755 1756 1757 1758 1759 1760 1761 1762 |
# File 'app/models/quote.rb', line 1753 def stop_for_pre_pack? return false unless can_request_estimated_packaging? return false if shipping_address.nil? return false if customer.is_e_commerce_misc? return false if line_items.empty? return false if room_configurations.present? && !room_configurations.all?(&:transmittable_and_settled?) return false if deliveries.all? { |d| (d.shipments.packed.any? && d.all_lines_allocated_to_shipments?) || d.is_smart_service? } need_to_pre_pack_reasons.any? end |
#stop_for_profit_review? ⇒ Boolean
State-machine guard — true when transmission must pause for
margin desk review. E-commerce miscellaneous customers are
exempt; rooms-mid-design and empty-line-item quotes don't trigger
it either.
1729 1730 1731 1732 1733 1734 1735 1736 |
# File 'app/models/quote.rb', line 1729 def stop_for_profit_review? # Etailer orders are immune return false if customer.is_e_commerce_misc? return false if line_items.empty? return false if room_configurations.present? && !room_configurations.all?(&:transmittable?) !profit_margins_met? end |
#store ⇒ Object
Alias for Customer#store
169 |
# File 'app/models/quote.rb', line 169 delegate :store, to: :customer |
#support_cases ⇒ ActiveRecord::Relation<SupportCase>
Support cases attached to this quote.
255 |
# File 'app/models/quote.rb', line 255 has_and_belongs_to_many :support_cases |
#tier2_program_pricing ⇒ Discount?
The tier2 (Discount) row representing the customer's
contractor/dealer pricing program on this quote.
1537 1538 1539 |
# File 'app/models/quote.rb', line 1537 def tier2_program_pricing discounts.tier2.first end |
#tier2_program_pricing_coupon ⇒ Coupon?
The Coupon that powers #tier2_program_pricing, when there is
one — used by #pricing_program_description and
#pricing_program_discount to label the program.
1546 1547 1548 |
# File 'app/models/quote.rb', line 1546 def tier2_program_pricing_coupon tier2_program_pricing&.coupon end |
#to_liquid ⇒ Liquid::QuoteDrop
Liquid drop wrapper used when this quote is rendered into the
transmission email/SMS templates — exposes only the safe-for-template
accessors via Liquid::QuoteDrop.
522 523 524 |
# File 'app/models/quote.rb', line 522 def to_liquid Liquid::QuoteDrop.new self end |
#to_order(order = nil, txid = nil) ⇒ Array<Order>
Convert this quote into one or more Orders via the
ConvertToOrder service. Optionally seeds with an
existing order (e.g. a cart already created in the e-commerce
flow) and a transaction id used to dedupe webhook retries.
1210 1211 1212 |
# File 'app/models/quote.rb', line 1210 def to_order(order = nil, txid = nil) convert_to_order_service.convert(order, txid) end |
#to_s ⇒ String
Display string used by Rails helpers and inspectors —
"Quote # SQ12345 - Smith Master Bath".
1389 1390 1391 |
# File 'app/models/quote.rb', line 1389 def to_s "Quote # #{reference_number_with_name}" end |
#total_amps ⇒ Float
Sum of every room's calculated electrical load, in amps. Used by
the controls/breaker recommendation engine.
1709 1710 1711 |
# File 'app/models/quote.rb', line 1709 def total_amps room_configurations.to_a.sum { |rc| rc.calculate_total_amps.round(2) }&.round(2) end |
#track_profit? ⇒ Boolean
Whether this quote should run through the profit-margin gate —
only sales quotes with at least one profitable line item are
subject to the profit-review state.
1701 1702 1703 |
# File 'app/models/quote.rb', line 1701 def track_profit? profitable_line_items.present? && is_sales_quote? end |
#tracking_email_address ⇒ String
Encrypted plus-addressed inbox alias used as the Reply-To on
quote emails — replies bounce back into Heatwave and are
auto-attached to this quote's communication thread.
1407 1408 1409 1410 1411 1412 |
# File 'app/models/quote.rb', line 1407 def tracking_email_address domain = Rails.application.config.x.email_domain prefix = 'quo' encrypted_id = Encryption.encrypt_string(id.to_s) "#{prefix}+id#{encrypted_id}@#{domain}" end |
#uploads ⇒ ActiveRecord::Relation<Upload>
Uploaded files attached to this quote.
240 |
# File 'app/models/quote.rb', line 240 has_many :uploads, as: :resource, dependent: :destroy |
#versions_for_audit_trail(_params = {}) ⇒ ActiveRecord::Relation<RecordVersion>
PaperTrail versions for the quote and every record that hangs off
it — its line items, deliveries, and discounts — pulled in a
single query via JSONB reference_data lookup. Powers the
quote's "audit trail" tab in the CRM.
649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 |
# File 'app/models/quote.rb', line 649 def versions_for_audit_trail(_params = {}) query_sql = %q{ (item_type = 'Quote' AND item_id = :id) OR ( item_type = 'LineItem' AND reference_data @> '{"resource_type": "Quote"}' AND reference_data @> :resource_id_json ) OR ( item_type = 'Delivery' AND reference_data @> :quote_id_json ) OR ( item_type = 'Discount' AND reference_data @> :itemizable_id_json AND reference_data @> '{"itemizable_type": "Quote"}' ) } RecordVersion.where( query_sql, id:, resource_id_json: { resource_id: id }.to_json, quote_id_json: { quote_id: id }.to_json, itemizable_id_json: { itemizable_id: id }.to_json ) end |
#visit ⇒ Visit
Website visit that originated the quote.
233 |
# File 'app/models/quote.rb', line 233 belongs_to :visit, optional: true |
#will_complete? ⇒ Boolean
State-machine guard — true when the quote has every gate cleared
for a complete transition: at least one line item, valid
shipping address (or none), all rooms settled, and profit margins
met.
722 723 724 725 726 727 |
# File 'app/models/quote.rb', line 722 def will_complete? line_items.any? && shipping_address_nil_or_valid? && all_rooms_complete_or_cancelled_or_draft? && profit_margins_met? end |
#will_installation_plans_complete? ⇒ Boolean
State-machine guard for the installation_plans_complete event —
rooms must exist and all be settled, and at least one line item
must be on the quote.
734 735 736 |
# File 'app/models/quote.rb', line 734 def will_installation_plans_complete? room_configurations.exists? && all_rooms_complete_or_cancelled_or_draft? && line_items.present? end |
#will_pending_project_details? ⇒ Boolean
State-machine guard for the pending_project_details event —
we're either coming from awaiting-design, or there are no line
items but at least one draft room.
774 775 776 777 |
# File 'app/models/quote.rb', line 774 def will_pending_project_details? awaiting_completed_installation_plans? || (line_items.empty? && room_configurations.any?(&:draft?)) end |
#will_ready_for_design? ⇒ Boolean
State-machine guard for the ready_for_design event — at least
one room exists and at least one is currently in design.
742 743 744 |
# File 'app/models/quote.rb', line 742 def will_ready_for_design? room_configurations.exists? && any_rooms_in_design? end |
#will_ready_to_transmit? ⇒ Boolean
State-machine guard for the ready_to_transmit event — line
items exist, shipping is valid, margins are met, and the warehouse
isn't asking for a pre-pack first.
751 752 753 754 755 756 757 |
# File 'app/models/quote.rb', line 751 def will_ready_to_transmit? line_items.any? && shipping_address_nil_or_valid? && profit_margins_met? && !stop_for_pre_pack? && (room_configurations.empty? || room_configurations.all?(&:transmittable?)) end |
#zones ⇒ nil
Placeholder for a planned "zones" concept (groups of rooms
controlled by a single thermostat). Not implemented — left as a
stub because the schema and view code expect the method to exist.
978 979 980 981 |
# File 'app/models/quote.rb', line 978 def zones # Zones are combinations of room configurations which are controlled together and regular rooms # Ramie: I'd say this is an unfortunate choice of name. we already have a well developed concept of room zones, rectangular areas within a room that are heateded areas, along with the model, methods and etc. end |