e9_crm 0.1.1 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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)
|