Commit iniziale di eminuxCRM: CRM Rails con pipeline, campagne email e Docker.
Include autenticazione, progetti isolati, mail marketing HTML con SMTP, test A/B e editor WYSIWYG. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
class Opportunity < ApplicationRecord
|
||||
include Auditable
|
||||
|
||||
belongs_to :organization
|
||||
belongs_to :project
|
||||
belongs_to :assigned_user, class_name: "User", optional: true
|
||||
|
||||
has_many :activities, dependent: :nullify
|
||||
has_many :tasks, dependent: :nullify
|
||||
|
||||
validates :name, presence: true
|
||||
validates :pipeline_stage, inclusion: { in: Catalog::PIPELINE_STAGES.keys }
|
||||
validates :lost_reason, inclusion: { in: Catalog::LOST_REASONS.keys }, allow_blank: true
|
||||
validates :ab_variant, inclusion: { in: Catalog::AB_VARIANTS.keys }, allow_blank: true
|
||||
validates :send_status, inclusion: { in: Catalog::SEND_STATUSES.keys }, allow_blank: true
|
||||
validates :probability, numericality: { in: 0..100 }, allow_nil: true
|
||||
validates :estimated_value, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true
|
||||
validate :lost_reason_required_when_lost
|
||||
|
||||
before_validation :set_default_probability, on: :create
|
||||
before_validation :default_project_from_current, on: :create
|
||||
before_save :track_stage_change
|
||||
after_save :handle_stage_side_effects
|
||||
after_save :ensure_organization_in_project
|
||||
|
||||
scope :open_stage, -> { where(pipeline_stage: Catalog::OPEN_PIPELINE_STAGES) }
|
||||
scope :won, -> { where(pipeline_stage: "won") }
|
||||
scope :lost, -> { where(pipeline_stage: "lost") }
|
||||
scope :in_stage, ->(stage) { where(pipeline_stage: stage) }
|
||||
scope :for_project, ->(project) {
|
||||
return none if project.nil?
|
||||
|
||||
where(project_id: project.id)
|
||||
}
|
||||
|
||||
def pipeline_stage_label
|
||||
Catalog.label_for(Catalog::PIPELINE_STAGES, pipeline_stage)
|
||||
end
|
||||
|
||||
def lost_reason_label
|
||||
Catalog.label_for(Catalog::LOST_REASONS, lost_reason)
|
||||
end
|
||||
|
||||
def ab_variant_label
|
||||
Catalog.label_for(Catalog::AB_VARIANTS, ab_variant)
|
||||
end
|
||||
|
||||
def send_status_label
|
||||
Catalog.label_for(Catalog::SEND_STATUSES, send_status)
|
||||
end
|
||||
|
||||
def open?
|
||||
Catalog::OPEN_PIPELINE_STAGES.include?(pipeline_stage)
|
||||
end
|
||||
|
||||
def won?
|
||||
pipeline_stage == "won"
|
||||
end
|
||||
|
||||
def lost?
|
||||
pipeline_stage == "lost"
|
||||
end
|
||||
|
||||
def next_pending_task
|
||||
tasks.pending.order(:due_at).first
|
||||
end
|
||||
|
||||
def days_in_current_stage
|
||||
anchor = stage_changed_at || created_at
|
||||
return 0 unless anchor
|
||||
|
||||
((Time.current - anchor) / 1.day).floor
|
||||
end
|
||||
|
||||
def move_to_stage!(new_stage, lost_reason: nil, notes: nil, user: Current.user)
|
||||
attrs = { pipeline_stage: new_stage }
|
||||
attrs[:lost_reason] = lost_reason if lost_reason.present?
|
||||
attrs[:notes] = [self.notes, notes].compact_blank.join("\n") if notes.present?
|
||||
attrs[:probability] = Catalog::STAGE_PROBABILITIES.fetch(new_stage.to_s, probability)
|
||||
update!(attrs)
|
||||
|
||||
create_stage_activity!(user) if saved_change_to_pipeline_stage?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_default_probability
|
||||
self.probability ||= Catalog::STAGE_PROBABILITIES.fetch(pipeline_stage, 0)
|
||||
end
|
||||
|
||||
def track_stage_change
|
||||
return unless pipeline_stage_changed?
|
||||
|
||||
self.stage_changed_at = Time.current
|
||||
self.first_contacted_at ||= Time.current if pipeline_stage != "to_contact"
|
||||
|
||||
case pipeline_stage
|
||||
when "won"
|
||||
self.won_at ||= Time.current
|
||||
self.lost_at = nil
|
||||
self.probability = 100
|
||||
when "lost"
|
||||
self.lost_at ||= Time.current
|
||||
self.won_at = nil
|
||||
self.probability = 0
|
||||
else
|
||||
self.won_at = nil
|
||||
self.lost_at = nil
|
||||
end
|
||||
end
|
||||
|
||||
def handle_stage_side_effects
|
||||
return unless saved_change_to_pipeline_stage?
|
||||
|
||||
organization.mark_as_active_customer! if won?
|
||||
end
|
||||
|
||||
def lost_reason_required_when_lost
|
||||
return unless pipeline_stage == "lost"
|
||||
return if lost_reason.present?
|
||||
|
||||
errors.add(:lost_reason, "è obbligatorio quando l'opportunità è persa")
|
||||
end
|
||||
|
||||
def create_stage_activity!(user)
|
||||
return unless user
|
||||
|
||||
type = won? ? "won" : lost? ? "lost" : "other"
|
||||
subject = if won?
|
||||
"Cliente #{product.presence || name}"
|
||||
elsif lost?
|
||||
"Perso: #{lost_reason_label}"
|
||||
else
|
||||
"Stage: #{pipeline_stage_label}"
|
||||
end
|
||||
|
||||
activities.create!(
|
||||
activity_type: type,
|
||||
subject: subject,
|
||||
description: notes,
|
||||
happened_at: Time.current,
|
||||
user: user,
|
||||
organization: organization,
|
||||
created_by: user,
|
||||
updated_by: user
|
||||
)
|
||||
end
|
||||
|
||||
def default_project_from_current
|
||||
self.project ||= Current.project
|
||||
end
|
||||
|
||||
def ensure_organization_in_project
|
||||
return unless project && organization
|
||||
|
||||
organization.ensure_in_project!(project)
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user