e9_crm 0.1.1 → 0.1.4
Sign up to get free protection for your applications and to get access to all the features.
- data/app/controllers/e9_crm/advertising_campaigns_controller.rb +1 -0
- data/app/controllers/e9_crm/affiliate_campaigns_controller.rb +1 -0
- data/app/controllers/e9_crm/campaign_groups_controller.rb +8 -0
- data/app/controllers/e9_crm/campaigns_controller.rb +38 -1
- data/app/controllers/e9_crm/companies_controller.rb +1 -0
- data/app/controllers/e9_crm/contact_emails_controller.rb +3 -7
- data/app/controllers/e9_crm/contacts_controller.rb +6 -4
- data/app/controllers/e9_crm/dated_costs_controller.rb +1 -0
- data/app/controllers/e9_crm/deals_controller.rb +66 -2
- data/app/controllers/e9_crm/email_campaigns_controller.rb +1 -0
- data/app/controllers/e9_crm/email_templates.controller.rb +1 -0
- data/app/controllers/e9_crm/offers_controller.rb +1 -0
- data/app/controllers/e9_crm/page_views_controller.rb +1 -0
- data/app/controllers/e9_crm/resources_controller.rb +2 -1
- data/app/controllers/e9_crm/sales_campaigns_controller.rb +1 -0
- data/app/helpers/e9_crm/campaign_groups_helper.rb +8 -0
- data/app/helpers/e9_crm/campaigns_helper.rb +42 -11
- data/app/helpers/e9_crm/contacts_helper.rb +1 -1
- data/app/helpers/e9_crm/deals_helper.rb +25 -0
- data/app/models/advertising_campaign.rb +0 -1
- data/app/models/campaign.rb +40 -8
- data/app/models/campaign_group.rb +6 -0
- data/app/models/contact.rb +22 -0
- data/app/models/contact_email.rb +29 -18
- data/app/models/deal.rb +93 -14
- data/app/models/no_campaign.rb +5 -0
- data/app/models/page_view.rb +13 -44
- data/app/models/tracking_cookie.rb +0 -6
- data/app/observers/deal_observer.rb +3 -0
- data/app/views/e9_crm/advertising_campaigns/_form_inner.html.haml +1 -0
- data/app/views/e9_crm/affiliate_campaigns/_form_inner.html.haml +10 -0
- data/app/views/e9_crm/campaign_groups/_footer.html.haml +0 -0
- data/app/views/e9_crm/campaign_groups/_header.html.haml +3 -0
- data/app/views/e9_crm/campaigns/_footer.html.haml +0 -0
- data/app/views/e9_crm/campaigns/_form_inner.html.haml +20 -4
- data/app/views/e9_crm/campaigns/_header.html.haml +16 -0
- data/app/views/e9_crm/campaigns/_reports_table.html.haml +31 -0
- data/app/views/e9_crm/campaigns/_table.html.haml +31 -0
- data/app/views/e9_crm/campaigns/reports.html.haml +13 -0
- data/app/views/e9_crm/contact_emails/_form_inner.html.haml +1 -1
- data/app/views/e9_crm/contacts/_header.html.haml +5 -4
- data/app/views/e9_crm/deals/_reports_table.html.haml +86 -0
- data/app/views/e9_crm/deals/reports.html.haml +19 -0
- data/app/views/e9_crm/deals/reports.js.erb +1 -0
- data/app/views/e9_crm/email_campaigns/_form_inner.html.haml +1 -0
- data/app/views/e9_crm/page_views/_table.html.haml +7 -7
- data/app/views/e9_crm/resources/_table.html.haml +1 -1
- data/app/views/e9_crm/sales_campaigns/_form_inner.html.haml +11 -0
- data/config/locales/e9.en.yml +11 -1
- data/config/locales/en.yml +16 -5
- data/config/routes.rb +15 -12
- data/e9_crm.gemspec +1 -1
- data/lib/e9_crm/rails_extensions.rb +7 -0
- data/lib/e9_crm/tracking_controller.rb +69 -52
- data/lib/e9_crm/version.rb +1 -1
- data/lib/generators/e9_crm/install_generator.rb +1 -1
- data/lib/generators/e9_crm/templates/{create_e9_crm_tables.rb → migration.rb} +6 -7
- metadata +20 -6
- data/app/controllers/e9_crm/record_attributes_controller.rb +0 -3
- data/app/controllers/e9_crm/reports_controller.rb +0 -2
data/app/models/contact.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
class Contact < ActiveRecord::Base
|
2
2
|
include E9Tags::Model
|
3
3
|
include E9Rails::ActiveRecord::AttributeSearchable
|
4
|
+
include E9Rails::ActiveRecord::Initialization
|
4
5
|
|
5
6
|
# necessary so contact knows its merge path
|
6
7
|
# NOTE in the future we'll probably want to give contacts public urls and make them 'Linkable'
|
@@ -128,6 +129,11 @@ class Contact < ActiveRecord::Base
|
|
128
129
|
.or(User.attr_like_scope_condition(:email, query))
|
129
130
|
)
|
130
131
|
}
|
132
|
+
|
133
|
+
scope :sales_persons, lambda { where(:status => Status::SalesPerson) }
|
134
|
+
scope :affiliates, lambda { where(:status => Status::Affiliate) }
|
135
|
+
scope :contacts, lambda { where(:status => Status::Contact) }
|
136
|
+
|
131
137
|
scope :by_title, lambda {|val| where(:title => val) }
|
132
138
|
scope :by_company, lambda {|val| where(:company_id => val) }
|
133
139
|
scope :tagged, lambda {|tags|
|
@@ -138,6 +144,12 @@ class Contact < ActiveRecord::Base
|
|
138
144
|
end
|
139
145
|
}
|
140
146
|
|
147
|
+
#
|
148
|
+
# Carrierwave
|
149
|
+
#
|
150
|
+
mount_uploader :avatar, AvatarUploader
|
151
|
+
def thumb(options = {}); self.avatar end
|
152
|
+
|
141
153
|
# The parameters for building the JS template for associated users
|
142
154
|
def self.users_build_parameters # :nodoc:
|
143
155
|
{ :status => User::Status::PROSPECT }
|
@@ -242,6 +254,10 @@ class Contact < ActiveRecord::Base
|
|
242
254
|
|
243
255
|
protected
|
244
256
|
|
257
|
+
def _assign_initialization_defaults
|
258
|
+
self.status ||= Status::Contact
|
259
|
+
end
|
260
|
+
|
245
261
|
def ensure_user_references
|
246
262
|
users.each {|u| u.contact = self }
|
247
263
|
end
|
@@ -255,4 +271,10 @@ class Contact < ActiveRecord::Base
|
|
255
271
|
attributes.keys.member?('value') && attributes['value'].blank?
|
256
272
|
end
|
257
273
|
|
274
|
+
module Status
|
275
|
+
VALUES = %w(contact sales_person affiliate)
|
276
|
+
Contact = VALUES[0]
|
277
|
+
SalesPerson = VALUES[1]
|
278
|
+
Affiliate = VALUES[2]
|
279
|
+
end
|
258
280
|
end
|
data/app/models/contact_email.rb
CHANGED
@@ -1,14 +1,12 @@
|
|
1
1
|
# A ContactEmail is a one time mail generated from an EmailTemplate, sent not to a
|
2
|
-
# list, but to a set of
|
2
|
+
# list, but to a set of contact_ids.
|
3
3
|
#
|
4
4
|
class ContactEmail < Email
|
5
5
|
include ScheduledEmail
|
6
6
|
before_save :generate_html_body_from_text_body
|
7
7
|
|
8
|
-
|
9
|
-
|
10
|
-
# addresses
|
11
|
-
validates :user_ids, :presence => true
|
8
|
+
validates :contact_ids, :presence => true
|
9
|
+
validates :user_ids, :presence => { :unless => lambda {|r| r.contact_ids.blank? } }
|
12
10
|
|
13
11
|
after_create :send_to_user_ids
|
14
12
|
|
@@ -23,23 +21,36 @@ class ContactEmail < Email
|
|
23
21
|
end
|
24
22
|
end
|
25
23
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
24
|
+
# contact_ids only gets set if it's an array or a properly formatted string
|
25
|
+
def contact_ids=(val)
|
26
|
+
@contact_ids = case val
|
27
|
+
when /^\[?((\d+,?\s?)+)\]?$/
|
28
|
+
$1.split(',')
|
29
|
+
when Array
|
30
|
+
val
|
31
|
+
else
|
32
|
+
[]
|
33
|
+
end
|
34
|
+
|
35
|
+
# clear user_ids cache
|
36
|
+
@user_ids = nil
|
37
|
+
end
|
38
38
|
|
39
|
-
|
39
|
+
def contact_ids
|
40
|
+
(@contact_ids ||= []).map(&:to_i)
|
40
41
|
end
|
41
42
|
|
42
43
|
|
44
|
+
def user_ids
|
45
|
+
@user_ids ||= if @contact_ids.present?
|
46
|
+
user_scope = (User.primary.joins(:contact) & Contact.where(:id => @contact_ids))
|
47
|
+
user_id_sql = user_scope.select('users.id').to_sql
|
48
|
+
User.connection.send(:select_values, user_id_sql, 'User ID Load')
|
49
|
+
else
|
50
|
+
[]
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
43
54
|
protected
|
44
55
|
|
45
56
|
def send_to_user_ids
|
data/app/models/deal.rb
CHANGED
@@ -3,25 +3,80 @@
|
|
3
3
|
#
|
4
4
|
class Deal < ActiveRecord::Base
|
5
5
|
include E9Rails::ActiveRecord::Initialization
|
6
|
+
include E9Rails::ActiveRecord::Scopes::Times
|
6
7
|
|
7
|
-
belongs_to :campaign, :inverse_of => :
|
8
|
-
belongs_to :tracking_cookie, :inverse_of => :
|
8
|
+
belongs_to :campaign, :inverse_of => :deals
|
9
|
+
belongs_to :tracking_cookie, :inverse_of => :deals
|
9
10
|
belongs_to :offer, :inverse_of => :deals
|
10
11
|
|
11
|
-
|
12
|
-
|
13
|
-
conditions = conditions.not if reverse
|
14
|
-
where(conditions)
|
15
|
-
}
|
12
|
+
money_columns :value
|
13
|
+
validates :value, :numericality => true
|
16
14
|
|
17
|
-
|
18
|
-
|
15
|
+
%w(total_value average_value total_cost average_cost).each do |money_column|
|
16
|
+
class_eval("def #{money_column}; (r = read_attribute(:#{money_column})) && Money.new(r) end")
|
17
|
+
end
|
18
|
+
|
19
|
+
scope :reports, lambda {
|
20
|
+
select_sql = <<-SELECT.gsub(/\s+/, ' ')
|
21
|
+
campaigns.id campaign_id,
|
22
|
+
campaigns.type campaign_type,
|
23
|
+
campaigns.name campaign_name,
|
24
|
+
campaign_groups.name campaign_group,
|
25
|
+
SUM(IF(deals.status != 'lead',1,0)) deal_count,
|
26
|
+
COUNT(deals.id) lead_count,
|
27
|
+
SUM(IF(deals.status='won',1,0)) won_deal_count,
|
28
|
+
campaigns.new_visits new_visits,
|
29
|
+
campaigns.repeat_visits repeat_visits,
|
30
|
+
|
31
|
+
SUM(IF(deals.status='won',value,0)) total_value,
|
32
|
+
AVG(IF(deals.status='won',value,NULL)) average_value,
|
33
|
+
|
34
|
+
(CASE campaigns.type
|
35
|
+
WHEN "SalesCampaign"
|
36
|
+
THEN SUM(campaigns.sales_fee)
|
37
|
+
WHEN "AdvertisingCampaign"
|
38
|
+
THEN dated_costs.cost
|
39
|
+
WHEN "AffiliateCampaign"
|
40
|
+
THEN SUM(campaigns.sales_fee +
|
41
|
+
campaigns.affiliate_fee)
|
42
|
+
ELSE
|
43
|
+
0
|
44
|
+
END) total_cost,
|
45
|
+
|
46
|
+
(CASE campaigns.type
|
47
|
+
WHEN "SalesCampaign"
|
48
|
+
THEN campaigns.sales_fee
|
49
|
+
WHEN "AdvertisingCampaign"
|
50
|
+
THEN dated_costs.cost
|
51
|
+
WHEN "AffiliateCampaign"
|
52
|
+
THEN campaigns.sales_fee +
|
53
|
+
campaigns.affiliate_fee
|
54
|
+
ELSE
|
55
|
+
0
|
56
|
+
END / COUNT(deals.id)) average_cost,
|
57
|
+
|
58
|
+
FLOOR(AVG(
|
59
|
+
DATEDIFF(
|
60
|
+
deals.closed_at,
|
61
|
+
deals.created_at))) average_elapsed
|
62
|
+
|
63
|
+
SELECT
|
64
|
+
|
65
|
+
join_sql = <<-JOINS.gsub(/\s+/, ' ')
|
66
|
+
LEFT OUTER JOIN dated_costs
|
67
|
+
ON deals.campaign_id = dated_costs.costable_id
|
68
|
+
AND dated_costs.costable_type = "Campaign"
|
69
|
+
|
70
|
+
LEFT OUTER JOIN campaigns
|
71
|
+
ON campaigns.id = deals.campaign_id
|
72
|
+
|
73
|
+
LEFT OUTER JOIN campaign_groups
|
74
|
+
ON campaign_groups.id = campaigns.campaign_group_id
|
75
|
+
JOINS
|
76
|
+
|
77
|
+
select(select_sql).joins(join_sql).group(:campaign_id)
|
19
78
|
}
|
20
79
|
|
21
|
-
scope :leads, lambda {|reverse=true| column_eq(:status, Status::Lead, !reverse) }
|
22
|
-
scope :pending, lambda {|reverse=true| column_eq(:status, Status::Pending, !reverse) }
|
23
|
-
scope :won, lambda {|reverse=true| column_eq(:status, Status::Won, !reverse) }
|
24
|
-
scope :lost, lambda {|reverse=true| column_eq(:status, Status::Lost, !reverse) }
|
25
80
|
|
26
81
|
validate do |record|
|
27
82
|
return unless record.status_changed?
|
@@ -41,12 +96,31 @@ class Deal < ActiveRecord::Base
|
|
41
96
|
when Status::Won, Status::Lost
|
42
97
|
if record.status_was == Status::Lead
|
43
98
|
record.errors.add(:status, :illegal_conversion)
|
99
|
+
elsif record.persisted?
|
100
|
+
# "close" isn't happening on new records
|
101
|
+
record.send :_do_close
|
44
102
|
end
|
45
103
|
else
|
46
104
|
record.errors.add(:status, :invalid, :options => Status::OPTIONS.join(', '))
|
47
105
|
end
|
48
106
|
end
|
49
107
|
|
108
|
+
scope :column_op, lambda {|op, column, value, reverse=false|
|
109
|
+
conditions = arel_table[column].send(op, value)
|
110
|
+
conditions = conditions.not if reverse
|
111
|
+
where(conditions)
|
112
|
+
}
|
113
|
+
|
114
|
+
scope :column_eq, lambda {|column, value, reverse=false|
|
115
|
+
column_op(:eq, column, value, reverse)
|
116
|
+
}
|
117
|
+
|
118
|
+
scope :leads, lambda {|reverse=true| column_eq(:status, Status::Lead, !reverse) }
|
119
|
+
scope :pending, lambda {|reverse=true| column_eq(:status, Status::Pending, !reverse) }
|
120
|
+
scope :won, lambda {|reverse=true| column_eq(:status, Status::Won, !reverse) }
|
121
|
+
scope :lost, lambda {|reverse=true| column_eq(:status, Status::Lost, !reverse) }
|
122
|
+
|
123
|
+
|
50
124
|
protected
|
51
125
|
|
52
126
|
def method_missing(method_name, *args)
|
@@ -66,11 +140,16 @@ class Deal < ActiveRecord::Base
|
|
66
140
|
self.converted_at = nil
|
67
141
|
notify_observers :before_revert
|
68
142
|
end
|
143
|
+
|
144
|
+
def _do_close
|
145
|
+
self.closed_at = nil
|
146
|
+
notify_observers :before_close
|
147
|
+
end
|
69
148
|
|
70
149
|
# Typically, new Deals are 'pending', with the assumption that offers
|
71
150
|
# explicitly create Deals as 'lead' when they convert.
|
72
151
|
def _assign_initialization_defaults
|
73
|
-
self.status
|
152
|
+
self.status ||= Status::Pending
|
74
153
|
end
|
75
154
|
|
76
155
|
module Status
|
data/app/models/page_view.rb
CHANGED
@@ -13,56 +13,16 @@
|
|
13
13
|
# [session] The session id of the request
|
14
14
|
#
|
15
15
|
class PageView < ActiveRecord::Base
|
16
|
+
include E9Rails::ActiveRecord::Scopes::Times
|
17
|
+
|
16
18
|
belongs_to :tracking_cookie
|
17
19
|
|
18
20
|
belongs_to :campaign, :inverse_of => :page_views
|
19
21
|
has_one :user, :through => :tracking_cookie
|
20
22
|
|
21
|
-
|
22
|
-
scope :repeat_visits, lambda { new_visits(false) }
|
23
|
-
|
24
|
-
scope :from_time, lambda {|*args|
|
25
|
-
args.flatten!
|
26
|
-
for_time_range(args.shift, nil, args.extract_options!)
|
27
|
-
}
|
28
|
-
|
29
|
-
scope :until_time, lambda {|*args|
|
30
|
-
args.flatten!
|
31
|
-
for_time_range(nil, args.shift, args.extract_options!)
|
32
|
-
}
|
33
|
-
|
34
|
-
scope :for_time_range, lambda {|*args|
|
35
|
-
opts = args.extract_options!
|
36
|
-
|
37
|
-
args.flatten!
|
38
|
-
|
39
|
-
# try to determine a datetime from each arg, skipping #to_time on passed strings because
|
40
|
-
# it doesn't handle everything DateTime.parse can, e.g. 'yyyy/mm'
|
41
|
-
args.map! do |t|
|
42
|
-
t.presence and
|
23
|
+
attr_accessor :should_cache
|
43
24
|
|
44
|
-
|
45
|
-
t.is_a?(String) && /^\d{4}$/.match(t) && Date.civil(t.to_i) ||
|
46
|
-
|
47
|
-
# handle Time etc. (String#to_time doesn't handle yyyy/mm properly)
|
48
|
-
!t.is_a?(String) && t.respond_to?(:to_time) && t.to_time ||
|
49
|
-
|
50
|
-
# try to parse it
|
51
|
-
DateTime.parse(t) rescue nil
|
52
|
-
end
|
53
|
-
|
54
|
-
time_column = opts[:column] || :created_at
|
55
|
-
|
56
|
-
if !args.any?
|
57
|
-
where('1=0')
|
58
|
-
elsif args.all?
|
59
|
-
where(time_column => args[0]..args[1])
|
60
|
-
elsif args[0]
|
61
|
-
where(arel_table[time_column].gteq(args[0]))
|
62
|
-
else
|
63
|
-
where(arel_table[time_column].lteq(args[1]))
|
64
|
-
end
|
65
|
-
}
|
25
|
+
after_create :increment_campaign_visit_cache, :if => '!!should_cache'
|
66
26
|
|
67
27
|
scope :by_user, lambda {|*users|
|
68
28
|
users.flatten!
|
@@ -76,5 +36,14 @@ class PageView < ActiveRecord::Base
|
|
76
36
|
where(arel_table[:campaign_id].send *(campaigns.length == 1 ? [:eq, campaigns.pop] : [:in, campaigns]))
|
77
37
|
}
|
78
38
|
|
39
|
+
scope :new_visits, lambda {|v=true| where(:new_visit => v) }
|
40
|
+
scope :repeat_visits, lambda { new_visits(false) }
|
41
|
+
|
79
42
|
delegate :name, :code, :to => :campaign, :prefix => true, :allow_nil => true
|
43
|
+
|
44
|
+
protected
|
45
|
+
|
46
|
+
def increment_campaign_visit_cache
|
47
|
+
Campaign.increment_counter(new_visit ? :new_visits : :repeat_visits, campaign_id)
|
48
|
+
end
|
80
49
|
end
|
@@ -2,6 +2,9 @@ class DealObserver < ActiveRecord::Observer
|
|
2
2
|
def before_revert(record)
|
3
3
|
end
|
4
4
|
|
5
|
+
def before_close(record)
|
6
|
+
end
|
7
|
+
|
5
8
|
def before_convert(record)
|
6
9
|
if email = record.offer && record.offer.conversion_alert_email.presence
|
7
10
|
Rails.logger.debug("Sending Offer Conversion Alert to [#{email}]")
|
@@ -0,0 +1 @@
|
|
1
|
+
= render 'e9_crm/campaigns/form_inner', :f => f
|
@@ -0,0 +1,10 @@
|
|
1
|
+
= render 'e9_crm/sales_campaigns/form_inner', :f => f
|
2
|
+
|
3
|
+
%fieldset
|
4
|
+
%legend= e9_t(:affiliate_information_legend, :scope => 'e9_crm.campaigns')
|
5
|
+
.field
|
6
|
+
= f.label :affilate_id
|
7
|
+
= f.collection_select :affiliate_id, Contact.affiliates.all, :id, :name, :prompt => true
|
8
|
+
.field
|
9
|
+
= f.label :affiliate_fee
|
10
|
+
= f.text_field :affiliate_fee
|
File without changes
|
File without changes
|
@@ -1,5 +1,21 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
%fieldset
|
2
|
+
%legend= e9_t(:campaign_information_legend, :scope => 'e9_crm.campaigns')
|
3
3
|
.field
|
4
|
-
= f.label
|
5
|
-
= f.
|
4
|
+
= f.label :campaign_group_id
|
5
|
+
= f.collection_select :campaign_group_id, CampaignGroup.all, :id, :name, :include_blank => 'No Group'
|
6
|
+
.field
|
7
|
+
= f.label :name
|
8
|
+
= f.text_field :name
|
9
|
+
- if f.object.new_record?
|
10
|
+
.field#campaign_code_field
|
11
|
+
= f.label :code
|
12
|
+
= f.text_field :code
|
13
|
+
.field
|
14
|
+
%label{:for => 'campaign_code_hint'}
|
15
|
+
= resource_class.human_attribute_name(:code_hint)
|
16
|
+
%span.help{:title => e9_t(:code_help, :scope => 'e9_crm.campaigns', :code => E9Crm.query_param)}= t(:inline_help_link)
|
17
|
+
#campaign_code_hint= "?#{E9Crm.query_param}=#{f.object.code}"
|
18
|
+
.field
|
19
|
+
= f.label :active
|
20
|
+
= f.check_box :active
|
21
|
+
|
@@ -0,0 +1,16 @@
|
|
1
|
+
.toolbar
|
2
|
+
.toolbar-left
|
3
|
+
= form_tag(resource_class, :method => :get, :id => 'campaign_search_form') do
|
4
|
+
%select{:name => 'type'}
|
5
|
+
= campaign_type_select_options
|
6
|
+
%select{:name => 'group'}
|
7
|
+
= campaign_group_select_options
|
8
|
+
%select{:name => 'active'}
|
9
|
+
= campaign_active_select_options
|
10
|
+
|
11
|
+
.toolbar-right
|
12
|
+
= form_tag new_advertising_campaign_path, :method => :get, :id => 'new_campaign_form' do
|
13
|
+
= label_tag 'new_campaign_type_select', t('activerecord.links.new', :model => Campaign.model_name.human)
|
14
|
+
%select{:name => 'type', :id => 'new_campaign_type_select'}
|
15
|
+
= campaign_type_select_options(false)
|
16
|
+
= submit_tag t(:go)
|