Class: Opportunity
Overview
== Schema Information
Table name: opportunities
Database name: primary
id :integer not null, primary key
close_date :date
discount_percentage :decimal(10, 2)
electricity_rate :decimal(6, 3)
google_conversion_meta :jsonb
installation_postal_code :string(255)
latitude :decimal(9, 6)
legacy_awaiting_iq_transmission :boolean
longitude :decimal(9, 6)
lost_reason_code :string(255)
lost_reason_notes :text
media :string(255)
msrp_value :decimal(10, 2)
name :string(80) not null
openai_ads_conversion_meta :jsonb not null
opportunity_reception_type :string(255) default("CRM"), not null
opportunity_type :string(1) default("S"), not null
pinterest_conversion_meta :jsonb
planned_installation_date :datetime
primary_product_line_name :string(255)
purchase_outlet :string(255)
purchased_competitor :string(255)
purchased_system :string(255)
quote_underlayment_by_room :boolean
reference_number :string
specs_eta :datetime
state :string(255)
state_code :string(2)
summary :string(240)
value :decimal(10, 2)
won_lost_date :datetime
created_at :datetime
updated_at :datetime
account_specialist_id :integer
buying_group_id :integer
contact_id :integer
creator_id :integer
current_heat_loss_room_id :integer
customer_id :integer not null
local_sales_rep_id :integer
merged_into_id :bigint
original_sales_rep_id :integer
parent_id :bigint
primary_sales_rep_id :integer
sales_support_rep_id :integer
secondary_sales_rep_id :integer
source_id :integer
technical_support_rep_id :integer
technical_support_rep_sec_id :integer
tracking_event_id :uuid
updater_id :integer
visit_id :bigint
Indexes
idx_tsearch_opp_name (to_tsvector('hw_name_search'::regconfig, COALESCE((name)::text, ''::text))) USING gin
index_opportunities_created_at (created_at)
index_opportunities_on_contact_id (contact_id)
index_opportunities_on_current_heat_loss_room_id (current_heat_loss_room_id)
index_opportunities_on_google_conversion_meta (google_conversion_meta) USING gin
index_opportunities_on_legacy_awaiting_iq_transmission (legacy_awaiting_iq_transmission)
index_opportunities_on_merged_into_id (merged_into_id)
index_opportunities_on_opportunity_type (opportunity_type)
index_opportunities_on_parent_id (parent_id)
index_opportunities_on_quote_underlayment_by_room (quote_underlayment_by_room)
index_opportunities_on_sales_support_rep_id (sales_support_rep_id)
index_opportunities_on_source_id (source_id)
index_opportunities_on_technical_support_rep_sec_id (technical_support_rep_sec_id)
index_opportunities_on_visit_id (visit_id) WHERE (visit_id IS NOT NULL) USING hash
opportunities_customer_id_index (customer_id)
opportunities_state_index (state)
opportunities_technical_support_rep_id_idx (technical_support_rep_id)
Foreign Keys
fk_rails_... (current_heat_loss_room_id => room_configurations.id) ON DELETE => nullify
fk_rails_... (merged_into_id => opportunities.id) ON DELETE => nullify
fk_rails_... (parent_id => opportunities.id) ON DELETE => nullify
fk_rails_... (source_id => sources.id)
fk_rails_... (technical_support_rep_id => parties.id)
fk_rails_... (visit_id => visits.id) ON DELETE => nullify
Defined Under Namespace
Classes: Copier, Merger, Mover
Constant Summary
collapse
- REFERENCE_NUMBER_PATTERN =
Regex pattern matching reference number.
/^ON\d+$/i
- UNKNOWN_JOB_NAME_PREFIX =
'Job'
- WEB_ORIGINATED_RECEPTION_TYPES =
Reception types where the opportunity was created from a website touch
(quote builder, instant quote, or generic online lead form). Used as the
candidate parent set when a CRM opp is auto-linked on creation.
%w[Online IQ].freeze
- AUTO_LINK_LOOKBACK_DAYS =
Window in days within which a CRM opp will auto-link to an earlier web opp
for the same customer/project. Matches BackfillOpportunityParents.
180
Models::Auditable::ALWAYS_IGNORED
Constants included
from Schedulable
Schedulable::SIMPLE_FORM_OPTIONS
Instance Attribute Summary collapse
#creator, #updater
Has and belongs to many
collapse
Class Method Summary
collapse
Instance Method Summary
collapse
#has_google_ads_attribution?, #source_locked?, #source_locked_reason, #visit_with_google_click_id?
#quick_note
#all_skipped_columns, #audit_reference_data, #should_not_save_version, #stamp_record
ransackable_associations, ransackable_attributes, ransackable_scopes, ransortable_attributes, #to_relation
config
#after_commit
#publish_event
Instance Attribute Details
#create_oppfu ⇒ Object
Returns the value of attribute create_oppfu.
243
244
245
|
# File 'app/models/opportunity.rb', line 243
def create_oppfu
@create_oppfu
end
|
#electricity_rate ⇒ Object
DB column is numeric(6,3) — guard before PG::NumericValueOutOfRange (AppSignal #4498)
Validations:
-
Numericality
({ greater_than_or_equal_to: 0, less_than: 1000, allow_nil: true })
154
155
|
# File 'app/models/opportunity.rb', line 154
validates :electricity_rate,
numericality: { greater_than_or_equal_to: 0, less_than: 1000, allow_nil: true }
|
#msrp_value ⇒ Object
151
152
|
# File 'app/models/opportunity.rb', line 151
validates :msrp_value,
numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: 99_999_999.99, allow_nil: true }
|
#must_have_one_room ⇒ Object
Returns the value of attribute must_have_one_room.
242
243
244
|
# File 'app/models/opportunity.rb', line 242
def must_have_one_room
@must_have_one_room
end
|
#name ⇒ Object
143
|
# File 'app/models/opportunity.rb', line 143
validates :name, presence: true, length: { within: 1..80 }
|
#opportunity_type ⇒ Object
147
|
# File 'app/models/opportunity.rb', line 147
validates :opportunity_type, inclusion: { in: OpportunityConstants::OPPORTUNITY_TYPES.keys }
|
#summary ⇒ Object
145
|
# File 'app/models/opportunity.rb', line 145
validates :summary, length: { within: 1..240 }, if: -> { summary.present? }
|
#value ⇒ Object
DB column is numeric(10,2) — guard before PG::NumericValueOutOfRange (AppSignal #4498)
Validations:
-
Numericality
({ greater_than_or_equal_to: 0, less_than_or_equal_to: 99_999_999.99, allow_nil: true })
149
150
|
# File 'app/models/opportunity.rb', line 149
validates :value,
numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: 99_999_999.99, allow_nil: true }
|
Class Method Details
.active ⇒ ActiveRecord::Relation<Opportunity>
A relation of Opportunities that are active. Active Record Scope
176
|
# File 'app/models/opportunity.rb', line 176
scope :active, -> { open_opportunities.with_quotes_and_rooms }
|
.all_competitors ⇒ Object
646
647
648
|
# File 'app/models/opportunity.rb', line 646
def self.all_competitors
OpportunityConstants::COMPETITOR_SYSTEM.values.flatten.compact.uniq.sort
end
|
.assigned_to_rep ⇒ ActiveRecord::Relation<Opportunity>
A relation of Opportunities that are assigned to rep. Active Record Scope
220
221
222
223
224
225
226
227
228
229
230
231
232
|
# File 'app/models/opportunity.rb', line 220
scope :assigned_to_rep, ->(party_or_ids) {
ids = Array.wrap(party_or_ids).compact
next none if ids.empty?
where.any_of(
{ primary_sales_rep_id: ids },
{ secondary_sales_rep_id: ids },
{ local_sales_rep_id: ids }
)
}
|
.awaiting_iq_transmission ⇒ ActiveRecord::Relation<Opportunity>
A relation of Opportunities that are awaiting iq transmission. Active Record Scope
185
|
# File 'app/models/opportunity.rb', line 185
scope :awaiting_iq_transmission, -> { where(legacy_awaiting_iq_transmission: true) }
|
.cluster_roots ⇒ ActiveRecord::Relation<Opportunity>
A relation of Opportunities that are cluster roots. Active Record Scope
186
|
# File 'app/models/opportunity.rb', line 186
scope :cluster_roots, -> { where(parent_id: nil, merged_into_id: nil) }
|
.fix_postal_codes ⇒ Object
638
639
640
641
642
643
644
|
# File 'app/models/opportunity.rb', line 638
def self.fix_postal_codes
opps = Opportunity.where.not(installation_postal_code: nil).pluck(:id, :installation_postal_code)
opps.each do |r|
pc = r[1].scan(/^\d{5}/)&.first || r[1].scan(/([A-Za-z]\d[A-Za-z]) ?(\d[A-Za-z]\d)/)&.first&.join&.upcase
Opportunity.where(id: r[0]).update_all(installation_postal_code: pc) if pc && pc != r[1]
end
end
|
1119
1120
1121
|
# File 'app/models/opportunity.rb', line 1119
def self.format_address_for_zip_selection(address)
"#{address.zip_compact} - #{address.city} #{address.state}"
end
|
.get_unique_name(party, name) ⇒ Object
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
|
# File 'app/models/opportunity.rb', line 694
def self.get_unique_name(party, name)
logger.info "self.get_unique_name(party,name): party.name: #{party.name}"
logger.info "self.get_unique_name(party,name): name: #{name}"
name_root = name.split(' #').first
same_name_opps = party.opportunities.where('name LIKE ?', "#{name_root}%")
if same_name_opps.any?
same_name_opp_count = same_name_opps.length + 1
new_opp_name = nil
loop do
new_opp_name = "#{name_root} ##{same_name_opp_count}"
same_name_opp_count += 1
break unless party.opportunities.exists?(name: new_opp_name)
end
else
new_opp_name = name
end
logger.info "self.get_unique_name(party,name): new_opp_name: #{new_opp_name}"
new_opp_name
end
|
.google_conversion_reported ⇒ ActiveRecord::Relation<Opportunity>
A relation of Opportunities that are google conversion reported. Active Record Scope
196
197
198
|
# File 'app/models/opportunity.rb', line 196
scope :google_conversion_reported, -> {
jsonb_where_exists(column_name: :google_conversion_meta, key: :reported_at)
}
|
.instant_quoting ⇒ ActiveRecord::Relation<Opportunity>
A relation of Opportunities that are instant quoting. Active Record Scope
182
|
# File 'app/models/opportunity.rb', line 182
scope :instant_quoting, -> { where(state: 'instant_quoting') }
|
.lost ⇒ ActiveRecord::Relation<Opportunity>
A relation of Opportunities that are lost. Active Record Scope
178
|
# File 'app/models/opportunity.rb', line 178
scope :lost, -> { where(state: 'lost') }
|
.lost_or_abandoned ⇒ ActiveRecord::Relation<Opportunity>
A relation of Opportunities that are lost or abandoned. Active Record Scope
179
|
# File 'app/models/opportunity.rb', line 179
scope :lost_or_abandoned, -> { where(state: %w[lost abandoned]) }
|
.most_recent_first ⇒ ActiveRecord::Relation<Opportunity>
A relation of Opportunities that are most recent first. Active Record Scope
172
|
# File 'app/models/opportunity.rb', line 172
scope :most_recent_first, -> { order(:created_at).reverse_order }
|
.not_cancelled ⇒ ActiveRecord::Relation<Opportunity>
A relation of Opportunities that are not cancelled. Active Record Scope
181
|
# File 'app/models/opportunity.rb', line 181
scope :not_cancelled, -> { where.not(state: 'cancelled') }
|
.not_won_or_lost ⇒ ActiveRecord::Relation<Opportunity>
A relation of Opportunities that are not won or lost. Active Record Scope
175
|
# File 'app/models/opportunity.rb', line 175
scope :not_won_or_lost, -> { where.not(state: %w[won lost]) }
|
.open_for_iq ⇒ ActiveRecord::Relation<Opportunity>
A relation of Opportunities that are open for iq. Active Record Scope
183
|
# File 'app/models/opportunity.rb', line 183
scope :open_for_iq, -> { not_cancelled.with_quotes_and_rooms }
|
.open_opportunities ⇒ ActiveRecord::Relation<Opportunity>
A relation of Opportunities that are open opportunities. Active Record Scope
174
|
# File 'app/models/opportunity.rb', line 174
scope :open_opportunities, -> { where.not(state: %w[won lost abandoned cancelled untracked]) }
|
.opportunity_reception_types_for_select ⇒ Object
658
659
660
|
# File 'app/models/opportunity.rb', line 658
def self.opportunity_reception_types_for_select
OpportunityConstants::RECEPTION_TYPES.map { |rt| [rt, rt] }
end
|
.opportunity_types_for_select ⇒ Object
654
655
656
|
# File 'app/models/opportunity.rb', line 654
def self.opportunity_types_for_select
OpportunityConstants::OPPORTUNITY_TYPES.map { |k, v| [v, k] }
end
|
.states_for_select ⇒ Object
650
651
652
|
# File 'app/models/opportunity.rb', line 650
def self.states_for_select
state_machines[:state].states.map { |s| [s.human_name, s.value] }
end
|
.web_originated ⇒ ActiveRecord::Relation<Opportunity>
A relation of Opportunities that are web originated. Active Record Scope
187
|
# File 'app/models/opportunity.rb', line 187
scope :web_originated, -> { where(opportunity_reception_type: %w[Online IQ]) }
|
.with_google_conversion_gclid ⇒ ActiveRecord::Relation<Opportunity>
A relation of Opportunities that are with google conversion gclid. Active Record Scope
200
201
202
203
204
|
# File 'app/models/opportunity.rb', line 200
scope :with_google_conversion_gclid, ->(gclid) {
next none if gclid.blank?
jsonb_where(column_name: :google_conversion_meta, json_keys: %w[gclid], operator: :eq, value: gclid)
}
|
.with_quotes_and_rooms ⇒ ActiveRecord::Relation<Opportunity>
A relation of Opportunities that are with quotes and rooms. Active Record Scope
173
|
# File 'app/models/opportunity.rb', line 173
scope :with_quotes_and_rooms, -> { includes(:quotes, :room_configurations).most_recent_first }
|
.with_value ⇒ ActiveRecord::Relation<Opportunity>
A relation of Opportunities that are with value. Active Record Scope
180
|
# File 'app/models/opportunity.rb', line 180
scope :with_value, -> { where.not(value: nil) }
|
.won ⇒ ActiveRecord::Relation<Opportunity>
A relation of Opportunities that are won. Active Record Scope
177
|
# File 'app/models/opportunity.rb', line 177
scope :won, -> { where(state: 'won') }
|
Instance Method Details
#abandonable? ⇒ Boolean
604
605
606
607
608
|
# File 'app/models/opportunity.rb', line 604
def abandonable?
related_activities.open_activities.blank? &&
(interest? || qualify? || follow_up?) &&
!sales_present?
end
|
#abandonment_date ⇒ Object
618
619
620
|
# File 'app/models/opportunity.rb', line 618
def abandonment_date
last_movement.to_date + 60.days
end
|
#account_specialist ⇒ Employee
114
|
# File 'app/models/opportunity.rb', line 114
belongs_to :account_specialist, class_name: 'Employee', optional: true
|
#activities ⇒ ActiveRecord::Relation<Activity>
128
|
# File 'app/models/opportunity.rb', line 128
has_many :activities, as: :resource, dependent: :nullify
|
#add_customer_to_campaign ⇒ Object
528
529
530
|
# File 'app/models/opportunity.rb', line 528
def add_customer_to_campaign
source.add_customer_to_campaign(customer)
end
|
#all_activities ⇒ Object
554
555
556
|
# File 'app/models/opportunity.rb', line 554
def all_activities
linked_activities
end
|
#all_participants ⇒ Object
1068
1069
1070
|
# File 'app/models/opportunity.rb', line 1068
def all_participants
Party.where(id: [customer_id, contact_id, *opportunity_participants.pluck(:party_id)].compact.uniq)
end
|
541
542
543
544
545
546
547
548
549
550
551
552
|
# File 'app/models/opportunity.rb', line 541
def all_related_communications
order_query = Communication.where(resource_type: 'Order', resource_id: order_ids)
quote_query = Communication.where(resource_type: 'Quote', resource_id: quote_ids)
room_query = Communication.where(resource_type: 'RoomConfiguration', resource_id: room_configuration_ids)
opp_query = Communication.where(resource_type: 'Opportunity', resource_id: id)
Communication.from(
"(#{order_query.to_sql} UNION #{quote_query.to_sql} UNION #{room_query.to_sql} UNION #{opp_query.to_sql}) AS communications"
)
end
|
#all_reps ⇒ Object
1079
1080
1081
|
# File 'app/models/opportunity.rb', line 1079
def all_reps
Employee.where(id: all_reps_ids)
end
|
#all_reps_ids ⇒ Object
1072
1073
1074
1075
1076
1077
|
# File 'app/models/opportunity.rb', line 1072
def all_reps_ids
[
primary_sales_rep_id, secondary_sales_rep_id, local_sales_rep_id, account_specialist_id, technical_support_rep_id,
customer.primary_sales_rep_id, customer.secondary_sales_rep_id, customer.service_rep_id
].compact.uniq
end
|
#all_uploads ⇒ Object
516
517
518
519
520
|
# File 'app/models/opportunity.rb', line 516
def all_uploads
Upload.where("(resource_type = 'Opportunity' and resource_id = ?) OR (resource_type = 'RoomConfiguration' and resource_id IN (?))", id, room_configuration_ids)
.includes(:resource)
.order('uploads.created_at DESC')
end
|
#build_activity ⇒ Object
532
533
534
|
# File 'app/models/opportunity.rb', line 532
def build_activity
activities.build resource: self, party: primary_party
end
|
119
|
# File 'app/models/opportunity.rb', line 119
belongs_to :buying_group, optional: true
|
#calculate_value ⇒ Object
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
|
# File 'app/models/opportunity.rb', line 903
def calculate_value
return if destroyed?
discounted_value = nil
msrp_value = nil
discount_percentage = nil
sales_orders = orders.sales_orders
sales_quotes = quotes.sales_quotes.active
if sales_orders.present? && (discounted_value = sales_orders.sum(:line_total)).positive?
msrp_value = LineItem.where(resource_type: 'Order', resource_id: sales_orders.select(:id))
.non_shipping.sum('quantity * price')
discount_percentage = msrp_value.zero? ? 0.0 : ((1.0 - (discounted_value / msrp_value)) * 100)
elsif (top_quote = sales_quotes.where.not(line_total: nil).order(line_total: :desc).first) &&
(discounted_value = top_quote.line_total).positive?
msrp_value = LineItem.where(resource_type: 'Quote', resource_id: top_quote.id)
.non_shipping.sum('quantity * price')
discount_percentage = msrp_value.zero? ? 0.0 : ((1.0 - (discounted_value / msrp_value)) * 100)
end
Opportunity.with_advisory_lock("opportunity_value_#{id}", timeout_seconds: 10) do
update_columns value: discounted_value, msrp_value:, discount_percentage:
end
end
|
#can_be_moved? ⇒ Boolean
435
436
437
|
# File 'app/models/opportunity.rb', line 435
def can_be_moved?
orders.active.non_carts.blank?
end
|
#cancel_followups(include_large_op_activities: false) ⇒ Object
839
840
841
842
843
844
845
846
|
# File 'app/models/opportunity.rb', line 839
def cancel_followups(include_large_op_activities: false)
sales_activity_ids = ActivityType.sales_activities.ids
sales_activity_ids.delete(ActivityTypeConstants::NEW_LARGE_OP_ID) unless include_large_op_activities
prune_activities nil, sales_activity_ids
end
|
#cancel_open_quotes ⇒ Object
Best-effort cancel of in-progress quotes when the opportunity is closed.
Skips quotes that aren't in a cancelable state (e.g. already complete or
blocked by active orders); the sync_state guard above prevents any
remaining in_quoting quotes from flipping the opportunity back open.
510
511
512
513
514
|
# File 'app/models/opportunity.rb', line 510
def cancel_open_quotes
quotes.in_quoting.find_each do |q|
q.cancel if q.cancelable?
end
end
|
#cancel_oppfu ⇒ Object
1115
1116
1117
|
# File 'app/models/opportunity.rb', line 1115
def cancel_oppfu
activities.where(activity_type_id: oppfu_activity_type.id).find_each(&:cancel)
end
|
#cancelable? ⇒ Boolean
832
833
834
835
836
837
|
# File 'app/models/opportunity.rb', line 832
def cancelable?
OpportunityConstants::CANCELABLE_STATES.include?(state.to_sym) &&
quotes.active.completed_quotes.empty? &&
room_configurations.active.empty? &&
orders.not_cancelled.empty?
end
|
#children ⇒ ActiveRecord::Relation<Opportunity>
126
|
# File 'app/models/opportunity.rb', line 126
has_many :children, class_name: 'Opportunity', foreign_key: :parent_id, inverse_of: :parent, dependent: :nullify
|
#closed? ⇒ Boolean
443
444
445
|
# File 'app/models/opportunity.rb', line 443
def closed?
won? || lost? || cancelled? || abandoned?
end
|
The canonical opp this row should report against. Walks parent_id up
to the root (loop-guarded so a cyclic parent chain returns the deepest
safe ancestor instead of looping forever). Used by reports that want
to dedupe a cluster.
457
458
459
460
461
462
463
464
465
|
# File 'app/models/opportunity.rb', line 457
def cluster_root
seen = Set[id]
current = self
while current.parent_id.present? && seen.exclude?(current.parent_id)
seen << current.parent_id
current = current.parent || break
end
current
end
|
110
|
# File 'app/models/opportunity.rb', line 110
belongs_to :contact, inverse_of: :opportunities, optional: true
|
791
792
793
794
795
|
# File 'app/models/opportunity.rb', line 791
def contact_select_options
contacts_to_use = customer.contacts.active.to_a.map { |c| ["#{c.full_name}#{" (#{c.job_title})" if c.job_title.present?} ", c.id] }
contacts_to_use << [contact.full_name, contact.id] if contact&.inactive
contacts_to_use.sort
end
|
Check if the opportunity can be followed up on.
Returns true if customer is not a guest OR if we have contact info (email/phone).
588
589
590
|
# File 'app/models/opportunity.rb', line 588
def contactable_for_follow_up?
!customer.guest? || emailable? || voice_callable?
end
|
#create_follow_up_activity(follow_up_note: nil, current_user: nil, follow_up_activity_type: nil, follow_up_task_type: nil, target_date: nil) ⇒ Object
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
|
# File 'app/models/opportunity.rb', line 848
def create_follow_up_activity(follow_up_note: nil, current_user: nil, follow_up_activity_type: nil, follow_up_task_type: nil, target_date: nil)
return unless sales_opportunity?
follow_up_activity_type ||= ActivityType.find_by(task_type: follow_up_task_type)
follow_up_activity_type ||= ActivityType.find(ActivityTypeConstants::QUOFU)
current_user ||= Employee.find_by(id: CurrentScope.user_id)
assigned_rep = follow_up_activity_type.determine_assigned_resource(customer) || current_user || customer.sales_manager
target_date ||= assigned_rep.next_business_day_closing_time
follow_up_note ||= "Follow up on opportunity #{name}"
new_activity_values = { activity_type: follow_up_activity_type,
target_datetime: target_date,
party: primary_party,
resource: self,
assigned_resource: assigned_rep,
new_note: follow_up_note }
Activity.create(new_activity_values)
end
|
#create_heat_loss_room ⇒ Object
938
939
940
941
942
943
944
945
946
947
948
949
|
# File 'app/models/opportunity.rb', line 938
def create_heat_loss_room
room = room_configurations.create(
RoomConfiguration.default_attributes(customer.store.id).merge(
name: 'New Room',
reception_type: 'HeatLossCalculator',
state: 'draft'
)
)
update!(current_heat_loss_room: room)
room
end
|
#crm_link ⇒ Object
797
798
799
|
# File 'app/models/opportunity.rb', line 797
def crm_link
UrlHelper.instance.opportunity_path(self)
end
|
#current_heat_loss_room ⇒ RoomConfiguration
121
|
# File 'app/models/opportunity.rb', line 121
belongs_to :current_heat_loss_room, class_name: 'RoomConfiguration', optional: true
|
Validations:
118
|
# File 'app/models/opportunity.rb', line 118
belongs_to :customer, inverse_of: :opportunities, optional: true
|
#deep_dup ⇒ Object
106
107
108
|
# File 'app/models/opportunity.rb', line 106
def deep_dup
deep_clone(except: :reference_number)
end
|
#digital_assets ⇒ ActiveRecord::Relation<DigitalAsset>
141
|
# File 'app/models/opportunity.rb', line 141
has_and_belongs_to_many :digital_assets, inverse_of: :opportunities
|
#editable? ⇒ Boolean
783
784
785
|
# File 'app/models/opportunity.rb', line 783
def editable?
%w[won lost cancelled abandoned].exclude?(state)
end
|
#effective_opportunity_won_date ⇒ Object
536
537
538
539
|
# File 'app/models/opportunity.rb', line 536
def effective_opportunity_won_date
[orders.active.minimum(:shipped_date).try(:end_of_day), Time.current].compact.min
end
|
#emailable? ⇒ Boolean
1056
1057
1058
|
# File 'app/models/opportunity.rb', line 1056
def emailable?
emails.present?
end
|
#emails ⇒ Object
1052
1053
1054
|
# File 'app/models/opportunity.rb', line 1052
def emails
primary_party.contact_points.emails
end
|
#estimate_electricity_rate ⇒ Object
1105
1106
1107
1108
1109
1110
1111
1112
1113
|
# File 'app/models/opportunity.rb', line 1105
def estimate_electricity_rate
res = nil
if (postal_code_to_use = installation_postal_code).present?
rate_data = ElectricityRate.get_from_postal_code(postal_code_to_use)
res = rate_data[:average_rate] if rate_data[:status] == :ok
end
res
end
|
#exclude_manually_initiated_event?(event) ⇒ Boolean
558
559
560
|
# File 'app/models/opportunity.rb', line 558
def exclude_manually_initiated_event?(event)
%i[iq_advanced reset].include?(event.to_sym)
end
|
#find_gbraid ⇒ Object
879
880
881
|
# File 'app/models/opportunity.rb', line 879
def find_gbraid
visit&.marketing_meta_gbraid || customer&.visit&.marketing_meta_gbraid
end
|
#find_gclid ⇒ Object
869
870
871
|
# File 'app/models/opportunity.rb', line 869
def find_gclid
visit&.marketing_meta_gclid || customer&.gclid || customer&.visit&.marketing_meta_gclid
end
|
#find_oppref ⇒ String?
OpenAI Ads (ChatGPT) click identifier, captured by the Tracker into
Visit#marketing_meta['oppref'] (JSONB-only — no scalar column). Used by
the CAPI reporter to populate the event's top-level oppref field, which
OpenAI requires us to forward ourselves on server-side events.
890
891
892
893
|
# File 'app/models/opportunity.rb', line 890
def find_oppref
(visit&.marketing_meta || {})['oppref'].presence ||
(customer&.visit&.marketing_meta || {})['oppref'].presence
end
|
#find_wbraid ⇒ Object
874
875
876
|
# File 'app/models/opportunity.rb', line 874
def find_wbraid
visit&.marketing_meta_wbraid || customer&.visit&.marketing_meta_wbraid
end
|
#heat_loss_room ⇒ Object
934
935
936
|
# File 'app/models/opportunity.rb', line 934
def heat_loss_room
current_heat_loss_room || create_heat_loss_room
end
|
#in_canada? ⇒ Boolean
951
952
953
|
# File 'app/models/opportunity.rb', line 951
def in_canada?
installation_postal_code.present? && installation_country_iso3 == 'CAN'
end
|
#in_usa? ⇒ Boolean
955
956
957
|
# File 'app/models/opportunity.rb', line 955
def in_usa?
installation_postal_code.present? && installation_country_iso3 == 'USA'
end
|
#installation_country_iso ⇒ Object
972
973
974
|
# File 'app/models/opportunity.rb', line 972
def installation_country_iso
installation_country_iso3.first(2)
end
|
#installation_country_iso3 ⇒ Object
965
966
967
968
969
970
|
# File 'app/models/opportunity.rb', line 965
def installation_country_iso3
customer&.store&.country_iso3 ||
PostalCode.find_by(code: installation_postal_code)&.state&.country_iso3 ||
{ 'en-CA': 'CAN', 'en-US': 'USA' }[I18n.locale] ||
'USA'
end
|
#installation_postal_codes_for_select ⇒ Object
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
|
# File 'app/models/opportunity.rb', line 1123
def installation_postal_codes_for_select
postal_options = []
postal_options += customer.all_addresses_including_contacts.map { |a| [Opportunity.format_address_for_zip_selection(a), a.zip_compact] }
postal_options += orders.select(&:shipping_address).map { |o| [Opportunity.format_address_for_zip_selection(o.shipping_address), o.shipping_address.zip_compact] }
postal_options += quotes.select(&:shipping_address).map { |o| [Opportunity.format_address_for_zip_selection(o.shipping_address), o.shipping_address.zip_compact] }
postal_options.uniq.sort
end
|
#installation_state_code ⇒ Object
959
960
961
962
963
|
# File 'app/models/opportunity.rb', line 959
def installation_state_code
return if installation_postal_code.blank?
PostalCode.get_state_code_from_postal_code(installation_postal_code)
end
|
#last_movement ⇒ Object
614
615
616
|
# File 'app/models/opportunity.rb', line 614
def last_movement
[planned_installation_date, last_quote_date, last_room_date, last_sales_activity_completion, created_at].compact.max
end
|
#last_quote_date ⇒ Object
626
627
628
|
# File 'app/models/opportunity.rb', line 626
def last_quote_date
quotes.maximum(:created_at)
end
|
#last_room_date ⇒ Object
630
631
632
|
# File 'app/models/opportunity.rb', line 630
def last_room_date
room_configurations.maximum(:created_at)
end
|
#last_sales_activity_completion ⇒ Object
622
623
624
|
# File 'app/models/opportunity.rb', line 622
def last_sales_activity_completion
related_activities.sales_activities.maximum(:completion_datetime)
end
|
#linked_activities ⇒ ActiveRecord::Relation<Activity>
139
|
# File 'app/models/opportunity.rb', line 139
has_many :linked_activities, class_name: 'Activity'
|
#local_sales_rep ⇒ Employee
113
|
# File 'app/models/opportunity.rb', line 113
belongs_to :local_sales_rep, class_name: 'Employee', optional: true
|
#location ⇒ Object
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
|
# File 'app/models/opportunity.rb', line 981
def location
addr = nil
if (o = orders.order(:id).last) && o.shipping_address
addr = o.shipping_address
elsif (q = quotes.order(:id).last) && q.shipping_address
addr = q.shipping_address
elsif (a = customer.try(:main_address))
addr = a
end
if addr
loc = addr.address_for_geocoder
elsif (postal_code = installation_postal_code.presence)
if (r = Geocoder.search(postal_code).first)
loc = "#{r.city}, #{r.state_code} #{postal_code}"
end
elsif (v = visit || customer&.visit)
loc = begin
[v.city, v.region, v.postal_code].compact_blank.join(' ')
rescue StandardError
nil
end
end
loc
end
|
#merge_into!(target) ⇒ void
This method returns an undefined value.
Mark this opportunity as a duplicate of target and retire it from
active pipelines. Persists merged_into_id and transitions to
cancelled so it stops appearing in open-pipeline reports.
Logic Details
The cancel transition is best-effort — if the opp is in a state that
cancelable? rejects (e.g. already cancelled, or has active orders),
only merged_into_id is persisted. The merged_into_id link is what
drives view_opportunity_conversions exclusion; the cancel is a UX
nicety so the duplicate stops appearing in active-pipeline lists.
483
484
485
486
487
488
489
490
|
# File 'app/models/opportunity.rb', line 483
def merge_into!(target)
raise ArgumentError, 'cannot merge an opportunity into itself' if target.id == id
transaction do
update!(merged_into_id: target.id)
cancel if cancelable?
end
end
|
#merge_with(opp_to_merge, delete_after: false) ⇒ Object
816
817
818
819
820
821
822
|
# File 'app/models/opportunity.rb', line 816
def merge_with(opp_to_merge, delete_after: false)
opp_to_merge.room_configurations.each { |rc| rc.update(opportunity: self) }
opp_to_merge.activities.each { |act| act.update(resource: self) }
opp_to_merge.quotes.each { |quote| quote.update(opportunity: self) }
opp_to_merge.reload
opp_to_merge.destroy if delete_after == true
end
|
#merged_duplicates ⇒ ActiveRecord::Relation<Opportunity>
127
|
# File 'app/models/opportunity.rb', line 127
has_many :merged_duplicates, class_name: 'Opportunity', foreign_key: :merged_into_id, inverse_of: :merged_into, dependent: :nullify
|
124
|
# File 'app/models/opportunity.rb', line 124
belongs_to :merged_into, class_name: 'Opportunity', optional: true, inverse_of: :merged_duplicates
|
#next_room_name(options) ⇒ Object
801
802
803
804
805
806
807
808
809
810
811
812
813
814
|
# File 'app/models/opportunity.rb', line 801
def next_room_name(options)
room_base_name = options[:room_base_name] || options[:room_type]&.name || 'Room'
same_name_rooms = room_configurations.where('name LIKE ?', "#{room_base_name}%")
same_name_room_count = same_name_rooms.length || 0
return room_base_name if same_name_room_count.zero?
new_room_name = nil
loop do
same_name_room_count += 1
new_room_name = "#{room_base_name} ##{same_name_room_count}"
break unless room_configurations.exists?(name: new_room_name)
end
new_room_name
end
|
#ok_to_customer_cancel? ⇒ Boolean
828
829
830
|
# File 'app/models/opportunity.rb', line 828
def ok_to_customer_cancel?
OpportunityConstants::CUSTOMER_CANCELABLE_STATES.include?(state.to_sym) && quotes.active.completed_quotes.empty? && room_configurations.active.empty? && orders.not_cancelled.empty?
end
|
#ok_to_delete? ⇒ Boolean
824
825
826
|
# File 'app/models/opportunity.rb', line 824
def ok_to_delete?
quotes.completed_quotes.empty? && room_configurations.non_drafts.empty? && orders.empty?
end
|
#open_activities_counter ⇒ Object
725
726
727
|
# File 'app/models/opportunity.rb', line 725
def open_activities_counter
related_activities.visible_by_default.open_activities.size
end
|
#open_opp_fu? ⇒ Boolean
439
440
441
|
# File 'app/models/opportunity.rb', line 439
def open_opp_fu?
activities.open_activities.joins(:activity_type).where(activity_types: { task_type: 'OPPFU' }).present?
end
|
#open_opportunity? ⇒ Boolean
634
635
636
|
# File 'app/models/opportunity.rb', line 634
def open_opportunity?
interest? || qualify? || quoting? || follow_up?
end
|
#opportunity_participants ⇒ ActiveRecord::Relation<OpportunityParticipant>
137
|
# File 'app/models/opportunity.rb', line 137
has_many :opportunity_participants, inverse_of: :opportunity, dependent: :destroy, autosave: true
|
#opportunity_type_expanded ⇒ Object
715
716
717
|
# File 'app/models/opportunity.rb', line 715
def opportunity_type_expanded
OpportunityConstants::OPPORTUNITY_TYPES[opportunity_type]
end
|
#order_activities ⇒ ActiveRecord::Relation<Activity>
136
|
# File 'app/models/opportunity.rb', line 136
has_many :order_activities, class_name: 'Activity', through: :orders, source: :activities
|
#orders ⇒ ActiveRecord::Relation<Order>
131
|
# File 'app/models/opportunity.rb', line 131
has_many :orders, before_add: :set_default_for_order
|
123
|
# File 'app/models/opportunity.rb', line 123
belongs_to :parent, class_name: 'Opportunity', optional: true, inverse_of: :children
|
#participants_options_for_select ⇒ Object
1064
1065
1066
|
# File 'app/models/opportunity.rb', line 1064
def participants_options_for_select
opportunity_participants.map { |scp| [scp.party.full_name, scp.party_id] }
end
|
#parties ⇒ ActiveRecord::Relation<Party>
138
|
# File 'app/models/opportunity.rb', line 138
has_many :parties, through: :opportunity_participants
|
#planned_installation_date_time_frame ⇒ Object
1083
1084
1085
1086
1087
1088
1089
1090
1091
|
# File 'app/models/opportunity.rb', line 1083
def planned_installation_date_time_frame
return nil if planned_installation_date.nil?
time_diff = planned_installation_date - Time.current
OpportunityConstants::PLANNED_INSTALLATION_DATE_TIME_FRAMES_IN_WEEKS.each do |k, v|
return k if (time_diff > v.first.weeks) && (time_diff <= v.last.weeks)
end
nil
end
|
#planned_installation_date_time_frame=(val) ⇒ Object
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
|
# File 'app/models/opportunity.rb', line 1093
def planned_installation_date_time_frame=(val)
Rails.logger.debug { "planned_installation_date_time_frame= #{val.inspect}" }
if val.present?
week_range = OpportunityConstants::PLANNED_INSTALLATION_DATE_TIME_FRAMES_IN_WEEKS[val.to_sym]
date = week_range.present? ? week_range.average.weeks.from_now : nil
Rails.logger.debug { "planned_installation_date_time_frame= planned_installation_date: #{planned_installation_date}, week_range: #{week_range}" }
else
date = nil
end
update(planned_installation_date: date)
end
|
#primary_party ⇒ Object
787
788
789
|
# File 'app/models/opportunity.rb', line 787
def primary_party
contact || customer
end
|
#primary_sales_rep ⇒ Employee
111
|
# File 'app/models/opportunity.rb', line 111
belongs_to :primary_sales_rep, class_name: 'Employee', optional: true
|
#product_lines(reviewable_only: true) ⇒ Object
We will gather all product lines either ordered or quoted or in the rooms
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
|
# File 'app/models/opportunity.rb', line 1011
def product_lines(reviewable_only: true)
pls = if (o = orders.last)
LineItem.collect_product_lines(o.line_items.goods)
elsif (q = quotes.last)
LineItem.collect_product_lines(q.line_items.goods)
else
LineItem.collect_product_lines(room_configurations.map(&:line_items).flatten)
end
pls ||= []
pls = pls.map(&:get_first_reviewable) if reviewable_only
pls.compact.uniq
end
|
#prune_activities(exclude_activity_id = nil, activity_type_ids = nil) ⇒ Object
This function looks for a particular activity in the context of the opportunity and cancel any found except the excluded one.
764
765
766
767
768
769
770
771
772
|
# File 'app/models/opportunity.rb', line 764
def prune_activities(exclude_activity_id = nil, activity_type_ids = nil)
activity_type_ids ||= ActivityTypeConstants::QUOFUS_IDS
activities_to_prune = related_activities.open_activities.where(activity_type_id: activity_type_ids)
activities_to_prune = activities_to_prune.where.not(id: exclude_activity_id) if exclude_activity_id.present?
activities_to_prune.each do |a|
logger.info "Pruning and closing activity #{a.id}"
a.cancel
end
end
|
#qualified_opportunity? ⇒ Boolean
596
597
598
|
# File 'app/models/opportunity.rb', line 596
def qualified_opportunity?
customer && !customer.guest? && qualified_room_present?
end
|
#qualified_room_present? ⇒ Boolean
600
601
602
|
# File 'app/models/opportunity.rb', line 600
def qualified_room_present?
room_configurations.where.not(state: %w[draft instant_quoting cancelled]).present?
end
|
#quote_activities ⇒ ActiveRecord::Relation<Activity>
135
|
# File 'app/models/opportunity.rb', line 135
has_many :quote_activities, class_name: 'Activity', through: :quotes, source: :activities
|
#quotes ⇒ ActiveRecord::Relation<Quote>
130
|
# File 'app/models/opportunity.rb', line 130
has_many :quotes, dependent: :destroy, before_add: :set_default_for_quote
|
#quotes_completed? ⇒ Boolean
574
575
576
|
# File 'app/models/opportunity.rb', line 574
def quotes_completed?
quotes.completed_quotes.present?
end
|
#quoting_event? ⇒ Boolean
592
593
594
|
# File 'app/models/opportunity.rb', line 592
def quoting_event?
quotes.in_quoting.present? || room_configurations.in_quoting.present?
end
|
#ready_for_follow_up? ⇒ Boolean
Ready for follow-up requires completed quotes AND a way to contact the customer.
This prevents Quote Builder opportunities from anonymous users (guest customers
with no contact info) from transitioning to follow_up state where they would
create noise in the CRM with no way for sales to action them.
582
583
584
|
# File 'app/models/opportunity.rb', line 582
def ready_for_follow_up?
quotes_completed? && contactable_for_follow_up?
end
|
729
730
731
732
733
|
# File 'app/models/opportunity.rb', line 729
def recalculate_customer_profiling_information
customer.recalculate_profiling_information
true
end
|
759
760
761
|
# File 'app/models/opportunity.rb', line 759
def related_activities
Activity.where(id: related_activities_ids)
end
|
755
756
757
|
# File 'app/models/opportunity.rb', line 755
def related_activities_ids
[activities.ids, room_activities.ids, quote_activities.ids].flatten
end
|
#reset_rooms_last_heat_loss ⇒ Object
976
977
978
979
|
# File 'app/models/opportunity.rb', line 976
def reset_rooms_last_heat_loss
room_configurations.update_all(last_heat_loss: nil)
true
end
|
#room_activities ⇒ ActiveRecord::Relation<Activity>
134
|
# File 'app/models/opportunity.rb', line 134
has_many :room_activities, class_name: 'Activity', through: :room_configurations, source: :activities
|
#room_configurations ⇒ ActiveRecord::Relation<RoomConfiguration>
Validations (if => #must_have_one_room ):
129
|
# File 'app/models/opportunity.rb', line 129
has_many :room_configurations, dependent: :destroy, inverse_of: :opportunity
|
#room_uploads ⇒ ActiveRecord::Relation<Upload>
133
|
# File 'app/models/opportunity.rb', line 133
has_many :room_uploads, class_name: 'Upload', through: :room_configurations, source: :uploads
|
#room_without_installation_record?(room = nil) ⇒ Boolean
719
720
721
722
723
|
# File 'app/models/opportunity.rb', line 719
def room_without_installation_record?(room = nil)
rooms = room_configurations.where(installation_date: nil)
rooms = rooms.excluding(room) if room.present?
rooms.any?
end
|
#sales_opportunity? ⇒ Boolean
566
567
568
|
# File 'app/models/opportunity.rb', line 566
def sales_opportunity?
opportunity_type == 'S'
end
|
#sales_present? ⇒ Boolean
570
571
572
|
# File 'app/models/opportunity.rb', line 570
def sales_present?
orders.non_carts.so_only.active.present?
end
|
#sales_support_rep ⇒ Employee
117
|
# File 'app/models/opportunity.rb', line 117
belongs_to :sales_support_rep, class_name: 'Employee', optional: true
|
#schedule_interest_activity ⇒ Object
This method is responsible for creating the relevant activity based on the
opportunity flow for interest grade type of opportunities. The type of activity
scheduled is based on the contact point presents and the presence of a cart
INTEREST_CALL - If Phone only present
INTEREST_EMAIL - If Email preesnt
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
|
# File 'app/models/opportunity.rb', line 1031
def schedule_interest_activity
if emailable?
task_type = 'INTEREST_EMAIL'
elsif voice_callable? task_type = 'INTEREST_CALL'
else
return false
end
return false if primary_party.activities.open_activities.joins(:activity_type).where(activity_types: { task_type: }).present?
new_activity = primary_party.activities.new(activity_type: ActivityType.find_by(task_type:))
new_activity.resource = self
new_activity.auto_assign_and_schedule
new_activity.save
new_activity
end
|
#secondary_sales_rep ⇒ Employee
112
|
# File 'app/models/opportunity.rb', line 112
belongs_to :secondary_sales_rep, class_name: 'Employee', optional: true
|
#selection_name ⇒ Object
778
779
780
781
|
# File 'app/models/opportunity.rb', line 778
def selection_name
base = "#{reference_number} - #{name}"
customer&.full_name.present? ? "#{base} - #{customer.full_name}" : base
end
|
#send_online_email_confirmation ⇒ Object
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
|
# File 'app/models/opportunity.rb', line 735
def send_online_email_confirmation
return unless opportunity_reception_type == 'Online' && customer&.email.present?
sender = customer.primary_sales_rep
co = CommunicationBuilder.new(
resource: self,
sender_party: sender,
sender: (sender.nil? ? INFO_EMAIL : nil),
recipient_party: customer,
emails: customer.email,
recipient_name: customer.name,
template_system_code: 'OPPORTUNITY_RECEIVED'
).create
if co.draft?
{ status_code: :error, status_message: co.errors_to_s }
else
{ status_code: :ok, status_message: "Opportunity confirmation e-mail sent to #{customer.name} #{customer.email}." }
end
end
|
#set_default_for_order(new_order) ⇒ Object
1162
1163
1164
1165
|
# File 'app/models/opportunity.rb', line 1162
protected def set_default_for_order(new_order)
new_order.order_type = "#{opportunity_type}O"
new_order
end
|
#set_default_for_quote(new_quote) ⇒ Object
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
|
# File 'app/models/opportunity.rb', line 1149
protected def set_default_for_quote(new_quote)
new_quote.validate_opportunity_and_prepared_for = true new_quote.shipping_address = customer.shipping_address
new_quote.shipping_method = customer.default_shipping_option_name
new_quote.bill_shipping_to_customer = customer.bill_shipping_to_customer
new_quote.pricing_program_description = customer.pricing_program_description
new_quote.pricing_program_discount = customer.pricing_program_discount
new_quote.currency = customer.catalog.currency
new_quote.total = nil
new_quote.tax_total = nil
new_quote
end
|
#should_abandon? ⇒ Boolean
610
611
612
|
# File 'app/models/opportunity.rb', line 610
def should_abandon?
abandonable? && Date.current > abandonment_date
end
|
#sms_enabled_numbers ⇒ Object
427
428
429
|
# File 'app/models/opportunity.rb', line 427
def sms_enabled_numbers
ContactPoint.joins(:party).merge(all_participants).sms_numbers.order(:detail).map(&:formatted_for_sms).uniq
end
|
#sms_messages ⇒ Object
431
432
433
|
# File 'app/models/opportunity.rb', line 431
def sms_messages
SmsMessage.for_numbers(sms_enabled_numbers)
end
|
120
|
# File 'app/models/opportunity.rb', line 120
belongs_to :source, optional: true
|
#state_description(describe_state = nil) ⇒ Object
562
563
564
|
# File 'app/models/opportunity.rb', line 562
def state_description(describe_state = nil)
OpportunityConstants::STATE_DESCRIPTION[describe_state || state]
end
|
#sync_state ⇒ Object
492
493
494
495
496
497
498
499
500
501
502
503
504
|
# File 'app/models/opportunity.rb', line 492
def sync_state
return if state.in?(%w[won lost abandoned cancelled])
untrack ||
win ||
follow ||
quote ||
(should_abandon? && abandon) ||
send_to_qualify ||
reset
end
|
#synchronize_reps ⇒ Object
Synchronize the reps on this opportunity with the customer
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
|
# File 'app/models/opportunity.rb', line 1136
def synchronize_reps
return unless customer
self.primary_sales_rep_id = customer.primary_sales_rep_id
self.secondary_sales_rep_id = customer.secondary_sales_rep_id
self.local_sales_rep_id = customer.local_sales_rep_id if customer.local_sales_rep_id.present?
rep_changes = {}
rep_changes = changes.symbolize_keys.slice(:primary_sales_rep_id, :secondary_sales_rep_id, :local_sales_rep_id) if changes.present?
Rails.logger.debug('Changing reps on opportunity', opportunity_id: id, changes: rep_changes) if rep_changes.present?
rep_changes
end
|
#technical_support_rep ⇒ Employee
115
|
# File 'app/models/opportunity.rb', line 115
belongs_to :technical_support_rep, class_name: 'Employee', optional: true
|
#technical_support_rep_sec ⇒ Employee
116
|
# File 'app/models/opportunity.rb', line 116
belongs_to :technical_support_rep_sec, class_name: 'Employee', optional: true
|
#to_s ⇒ Object
774
775
776
|
# File 'app/models/opportunity.rb', line 774
def to_s
"#{name} - #{human_state_name}"
end
|
#tracking_email_address ⇒ Object
896
897
898
899
900
901
|
# File 'app/models/opportunity.rb', line 896
def tracking_email_address
domain = Rails.application.config.x.email_domain
prefix = 'opp'
encrypted_id = Encryption.encrypt_string(id.to_s)
"#{prefix}+id#{encrypted_id}@#{domain}"
end
|
#unattached_orders(**_pagination) ⇒ Object
Retrieve all orphaned orders that are candidate to be attached to this opportunity
523
524
525
526
|
# File 'app/models/opportunity.rb', line 523
def unattached_orders(**)
customer.orders.so_only.where(opportunity_id: nil).order('orders.created_at DESC')
end
|
#unique_room_name(base_name, except: nil) ⇒ Object
Generate a room-configuration name unique within this opportunity.
Strips any existing " #N" suffix from base_name so re-deriving from
the same root doesn't pile up ("Basement #2 #3"), then returns the
bare root if free, else "#Top Level Namespace #2", "#Top Level Namespace #3", ... until unique.
Pass except: (typically the record currently being edited) so its own
current name is not counted as a collision.
Modeled on Opportunity.get_unique_name (which uniquifies opportunities
within a customer); the differences are: scoped to this opportunity's
room_configurations, returns the bare root when free, and supports
excluding a record.
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
|
# File 'app/models/opportunity.rb', line 674
def unique_room_name(base_name, except: nil)
return base_name if base_name.blank?
name_root = base_name.to_s.sub(/\s#\d+\z/, '')
scope = room_configurations
scope = scope.excluding(except) if except&.persisted?
taken = scope.where('name LIKE ?', "#{self.class.sanitize_sql_like(name_root)}%").pluck(:name).to_set
return name_root unless taken.include?(name_root)
n = 2
loop do
candidate = "#{name_root} ##{n}"
return candidate unless taken.include?(candidate)
n += 1
end
end
|
#unknown_job_name? ⇒ Boolean
447
448
449
|
# File 'app/models/opportunity.rb', line 447
def unknown_job_name?
name&.start_with?(UNKNOWN_JOB_NAME_PREFIX)
end
|
#update_primary_product_line_name ⇒ Object
929
930
931
932
|
# File 'app/models/opportunity.rb', line 929
def update_primary_product_line_name
product_line_name = room_configurations.order(:id).map { |a| a.heating_system_product_line.heating_system_type_name }.first
update_column :primary_product_line_name, product_line_name
end
|
#uploads ⇒ ActiveRecord::Relation<Upload>
132
|
# File 'app/models/opportunity.rb', line 132
has_many :uploads, as: :resource, dependent: :destroy
|
122
|
# File 'app/models/opportunity.rb', line 122
belongs_to :visit, optional: true
|
#voice_callable? ⇒ Boolean
1060
1061
1062
|
# File 'app/models/opportunity.rb', line 1060
def voice_callable?
primary_party.contact_points.voice_callable.present?
end
|