spree_promo 0.60.6 → 0.70.RC1
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +26 -0
- data/app/assets/javascripts/admin/promotions.js +131 -0
- data/app/assets/javascripts/admin/spree_promo.js +3 -0
- data/app/assets/javascripts/store/spree_promo.js +2 -0
- data/{public → app/assets}/stylesheets/admin/promotions.css +19 -29
- data/app/assets/stylesheets/admin/spree_promo.css +5 -0
- data/app/assets/stylesheets/store/spree_promo.css +4 -0
- data/app/controllers/admin/promotion_actions_controller.rb +28 -0
- data/app/controllers/admin/promotions_controller.rb +2 -2
- data/app/controllers/content_controller_decorator.rb +8 -0
- data/app/controllers/orders_controller_decorator.rb +14 -5
- data/app/models/adjustment_decorator.rb +3 -0
- data/app/models/order_decorator.rb +24 -0
- data/app/models/product_decorator.rb +10 -0
- data/app/models/product_group_decorator.rb +3 -0
- data/app/models/promotion.rb +61 -41
- data/app/models/promotion/actions/create_adjustment.rb +28 -0
- data/app/models/promotion/actions/create_line_items.rb +29 -0
- data/app/models/promotion/rules/first_order.rb +1 -1
- data/app/models/promotion/rules/item_total.rb +3 -2
- data/app/models/promotion/rules/landing_page.rb +17 -0
- data/app/models/promotion/rules/product.rb +1 -1
- data/app/models/promotion/rules/user.rb +1 -1
- data/app/models/promotion/rules/user_logged_in.rb +13 -0
- data/app/models/promotion_action.rb +19 -0
- data/app/models/promotion_action_line_item.rb +6 -0
- data/app/models/promotion_rule.rb +3 -16
- data/app/overrides/promo_admin_tabs.rb +5 -0
- data/app/overrides/promo_coupon_code_field.rb +5 -0
- data/app/overrides/promo_product_properties.rb +5 -0
- data/app/views/admin/promotion_actions/create.js.erb +7 -0
- data/app/views/admin/promotion_actions/destroy.js.erb +2 -0
- data/app/views/admin/promotions/_actions.html.erb +32 -0
- data/app/views/admin/promotions/_form.html.erb +17 -17
- data/app/views/admin/promotions/_promotion_action.html.erb +12 -0
- data/app/views/admin/promotions/_promotion_rule.html.erb +2 -2
- data/app/views/admin/promotions/_rules.html.erb +32 -0
- data/app/views/admin/promotions/actions/_create_adjustment.html.erb +19 -0
- data/app/views/admin/promotions/actions/_create_line_items.html.erb +52 -0
- data/app/views/admin/promotions/actions/_give_store_credit.html.erb +6 -0
- data/app/views/admin/promotions/edit.html.erb +4 -38
- data/app/views/admin/promotions/index.html.erb +5 -7
- data/app/views/admin/promotions/rules/_landing_page.html.erb +8 -0
- data/app/views/admin/promotions/rules/_user_logged_in.html.erb +3 -0
- data/app/views/checkout/_coupon_code_field.html.erb +1 -1
- data/config/locales/en.yml +27 -5
- data/config/routes.rb +1 -0
- data/db/migrate/20101026184833_migrate_adjustments.rb +9 -0
- data/db/migrate/20110331094351_promotion_changes_to_subclass_of_activator.rb +22 -0
- data/db/migrate/20110404123358_create_promotion_actions.rb +13 -0
- data/db/migrate/20110601202923_create_promotion_action_line_items.rb +12 -0
- data/lib/spree_promo.rb +24 -100
- metadata +77 -26
- data/app/models/promotion_credit.rb +0 -9
- data/db/migrate/20100419194457_fix_existing_coupon_credits.rb +0 -9
- data/lib/spree_promo_hooks.rb +0 -11
- data/lib/tasks/install.rake +0 -26
- data/public/javascripts/admin/promotions.js +0 -26
@@ -0,0 +1,28 @@
|
|
1
|
+
class Promotion::Actions::CreateAdjustment < PromotionAction
|
2
|
+
|
3
|
+
calculated_adjustments
|
4
|
+
|
5
|
+
before_create do |a|
|
6
|
+
c = a.build_calculator
|
7
|
+
c.type = Promotion::Actions::CreateAdjustment.calculators.first.to_s
|
8
|
+
end
|
9
|
+
|
10
|
+
def perform(options = {})
|
11
|
+
return unless order = options[:order]
|
12
|
+
return if order.promotion_credit_exists?(promotion)
|
13
|
+
if amount = calculator.compute(order)
|
14
|
+
amount = order.item_total if amount > order.item_total
|
15
|
+
order.adjustments.promotion.reload.clear
|
16
|
+
order.update!
|
17
|
+
create_adjustment("#{I18n.t(:promotion)} (#{promotion.name})", order, order)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Ensure a negative amount which does not exceed the sum of the order's item_total and ship_total
|
22
|
+
def compute_amount(calculable)
|
23
|
+
[(calculable.item_total + calculable.ship_total), super.to_f.abs].min * -1
|
24
|
+
end
|
25
|
+
|
26
|
+
delegate :eligible?, :to => :promotion
|
27
|
+
|
28
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
class Promotion::Actions::CreateLineItems < PromotionAction
|
2
|
+
|
3
|
+
has_many :promotion_action_line_items, :foreign_key => 'promotion_action_id'
|
4
|
+
|
5
|
+
attr_accessor :line_items_string
|
6
|
+
|
7
|
+
def perform(options = {})
|
8
|
+
return unless order = options[:order]
|
9
|
+
promotion_action_line_items.each do |item|
|
10
|
+
order.add_variant(item.variant, item.quantity)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def line_items_string
|
15
|
+
promotion_action_line_items.map { |li| "#{li.variant_id}x#{li.quantity}" }.join(',')
|
16
|
+
end
|
17
|
+
|
18
|
+
def line_items_string=(value)
|
19
|
+
promotion_action_line_items.destroy_all
|
20
|
+
value.to_s.split(',').each do |str|
|
21
|
+
variant_id, quantity = str.split('x')
|
22
|
+
if variant_id && quantity && variant = Variant.find_by_id(variant_id)
|
23
|
+
promotion_action_line_items.create(:variant => variant, :quantity => quantity.to_i)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
@@ -6,7 +6,8 @@ class Promotion::Rules::ItemTotal < PromotionRule
|
|
6
6
|
|
7
7
|
OPERATORS = ['gt', 'gte']
|
8
8
|
|
9
|
-
def eligible?(order)
|
10
|
-
order.
|
9
|
+
def eligible?(order, options = {})
|
10
|
+
item_total = order.line_items.map(&:amount).sum
|
11
|
+
item_total.send(preferred_operator == 'gte' ? :>= : :>, preferred_amount)
|
11
12
|
end
|
12
13
|
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# A rule to limit a promotion to a specific user.
|
2
|
+
class Promotion::Rules::LandingPage < PromotionRule
|
3
|
+
|
4
|
+
preference :path, :string
|
5
|
+
|
6
|
+
def eligible?(order, options = {})
|
7
|
+
if options.has_key?(:visited_paths)
|
8
|
+
options[:visited_paths].to_a.any? do |path|
|
9
|
+
path.gsub(/^\//, '') == preferred_path.gsub(/^\//, '')
|
10
|
+
end
|
11
|
+
else
|
12
|
+
true
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
|
@@ -14,7 +14,7 @@ class Promotion::Rules::Product < PromotionRule
|
|
14
14
|
product_group ? product_group.products : products
|
15
15
|
end
|
16
16
|
|
17
|
-
def eligible?(order)
|
17
|
+
def eligible?(order, options = {})
|
18
18
|
return true if eligible_products.empty?
|
19
19
|
if preferred_match_policy == 'all'
|
20
20
|
eligible_products.all? {|p| order.products.include?(p) }
|
@@ -2,7 +2,7 @@ class Promotion::Rules::User < PromotionRule
|
|
2
2
|
belongs_to :user
|
3
3
|
has_and_belongs_to_many :users, :class_name => '::User', :join_table => 'promotion_rules_users', :foreign_key => 'promotion_rule_id'
|
4
4
|
|
5
|
-
def eligible?(order)
|
5
|
+
def eligible?(order, options = {})
|
6
6
|
users.none? or users.include?(order.user)
|
7
7
|
end
|
8
8
|
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class Promotion::Rules::UserLoggedIn < PromotionRule
|
2
|
+
def eligible?(order, options = {})
|
3
|
+
|
4
|
+
# this is tricky. We couldn't use any of the devise methods since we aren't in the controller.
|
5
|
+
# we need to rely on the controller already having done this for us.
|
6
|
+
|
7
|
+
# The thinking is that the controller should have some sense of what state
|
8
|
+
# we should be in before firing events,
|
9
|
+
# so the controller will have to set this field.
|
10
|
+
|
11
|
+
return options && options[:user_signed_in]
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# Base class for all types of promotion action.
|
2
|
+
# PromotionActions perform the necessary tasks when a promotion is activated by an event and determined to be eligible.
|
3
|
+
class PromotionAction < ActiveRecord::Base
|
4
|
+
include SubclassRegistration
|
5
|
+
|
6
|
+
belongs_to :promotion, :foreign_key => 'activator_id'
|
7
|
+
|
8
|
+
scope :of_type, lambda {|t| {:conditions => {:type => t}}}
|
9
|
+
|
10
|
+
# This method should be overriden in subclass
|
11
|
+
# Updates the state of the order or performs some other action depending on the subclass
|
12
|
+
# options will contain the payload from the event that activated the promotion. This will include
|
13
|
+
# the key :user which allows user based actions to be performed in addition to actions on the order
|
14
|
+
def perform(options = {})
|
15
|
+
raise 'perform should be implemented in a sub-class of PromotionAction'
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
|
@@ -1,26 +1,13 @@
|
|
1
1
|
# Base class for all promotion rules
|
2
2
|
class PromotionRule < ActiveRecord::Base
|
3
|
+
include SubclassRegistration
|
3
4
|
|
4
|
-
belongs_to :promotion
|
5
|
+
belongs_to :promotion, :foreign_key => 'activator_id'
|
5
6
|
|
6
7
|
scope :of_type, lambda {|t| {:conditions => {:type => t}}}
|
7
8
|
|
8
|
-
def eligible?(order)
|
9
|
+
def eligible?(order, options = {})
|
9
10
|
raise 'eligible? should be implemented in a sub-class of Promotion::PromotionRule'
|
10
11
|
end
|
11
12
|
|
12
|
-
@rule_classes = nil
|
13
|
-
@@rule_classes = Set.new
|
14
|
-
def self.register
|
15
|
-
@@rule_classes.add(self)
|
16
|
-
end
|
17
|
-
|
18
|
-
def self.rule_classes
|
19
|
-
@@rule_classes.to_a
|
20
|
-
end
|
21
|
-
|
22
|
-
def self.rule_class_names
|
23
|
-
PromotionRule.rule_classes.map(&:name)
|
24
|
-
end
|
25
|
-
|
26
13
|
end
|
@@ -0,0 +1,7 @@
|
|
1
|
+
jQuery('#actions_container').html('<%= escape_javascript( render(:partial => 'admin/promotions/actions') ) %>');
|
2
|
+
initProductActions();
|
3
|
+
|
4
|
+
jQuery('#<%= dom_id @promotion_action %>').hide();
|
5
|
+
jQuery('#<%= dom_id @promotion_action %>').fadeIn();
|
6
|
+
jQuery('#<%= dom_id @promotion_action %> .tokeninput.products').productPicker();
|
7
|
+
|
@@ -0,0 +1,32 @@
|
|
1
|
+
<fieldset id="action_fields">
|
2
|
+
<legend><%= t(:promotion_actions) %></legend>
|
3
|
+
|
4
|
+
<%= form_for(@promotion, :url => admin_promotion_path(@promotion), :html => { :method => :put }) do |f| %>
|
5
|
+
<div id="actions" class="filter_list">
|
6
|
+
<% if @promotion.actions.any? %>
|
7
|
+
<%#= render :partial => 'promotion_action', :collection => @promotion.actions, :locals => {:f => f} %>
|
8
|
+
|
9
|
+
<%= f.fields_for :promotion_actions do |action_form| %>
|
10
|
+
<%= render 'admin/promotions/promotion_action', :action_form => action_form %>
|
11
|
+
<% end %>
|
12
|
+
|
13
|
+
<% else %>
|
14
|
+
<!-- <p><%= t('no_actions_added') %></p> -->
|
15
|
+
<% end %>
|
16
|
+
</div>
|
17
|
+
<p class="form-buttons">
|
18
|
+
<%= button t("update") %>
|
19
|
+
</p>
|
20
|
+
<% end %>
|
21
|
+
|
22
|
+
<%= form_tag(admin_promotion_promotion_actions_path(@promotion), :remote => true, :id => 'new_promotion_action_form') do %>
|
23
|
+
<% options = options_for_select( PromotionAction.registered_class_names.map {|name| [ t("promotion_action_types.#{name.demodulize.underscore}.name"), name] } ) %>
|
24
|
+
<p>
|
25
|
+
<label for="promotion_action_type"><%= t('add_action_of_type') %></label>
|
26
|
+
<%= select_tag("promotion_action[type]", options) %>
|
27
|
+
<input type="submit" value="<%= t('add') %>" />
|
28
|
+
</p>
|
29
|
+
<% end %>
|
30
|
+
|
31
|
+
</fieldset>
|
32
|
+
|
@@ -6,21 +6,24 @@
|
|
6
6
|
<%= f.text_field :name %>
|
7
7
|
<% end %>
|
8
8
|
|
9
|
+
<%= f.field_container :event_name do %>
|
10
|
+
<%= f.label :event_name, t(:event) %><br />
|
11
|
+
<%= f.select :event_name, Activator.event_names.map{|name| [t("events.#{name}"), name] } %>
|
12
|
+
<% end %>
|
13
|
+
|
14
|
+
<%= f.field_container :preferred_code do %>
|
15
|
+
<%= f.label :preferred_code, t(:code) %><br />
|
16
|
+
<%= f.text_field :preferred_code %>
|
17
|
+
<% end %>
|
18
|
+
|
9
19
|
<%= f.field_container :description do %>
|
10
20
|
<%= f.label :description %><br />
|
11
21
|
<%= f.text_area :description, :style => "height:50px" %>
|
12
22
|
<% end %>
|
13
23
|
|
14
|
-
<%= f.field_container :
|
15
|
-
<%= f.
|
16
|
-
<%= f.
|
17
|
-
<% end %>
|
18
|
-
|
19
|
-
<%= f.field_container :combine do %>
|
20
|
-
<label>
|
21
|
-
<%= f.check_box :combine %>
|
22
|
-
<%= t(:may_be_combined_with_other_promotions) %>
|
23
|
-
</label>
|
24
|
+
<%= f.field_container :preferred_advertise do %>
|
25
|
+
<%= f.check_box :preferred_advertise %>
|
26
|
+
<%= f.label :preferred_advertise, t('advertise') %>
|
24
27
|
<% end %>
|
25
28
|
|
26
29
|
</fieldset>
|
@@ -28,19 +31,16 @@
|
|
28
31
|
<fieldset id="expiry_fields">
|
29
32
|
<legend><%= t(:expiry) %></legend>
|
30
33
|
<p>
|
31
|
-
<%= f.label :usage_limit %><br />
|
32
|
-
<%= f.text_field :
|
34
|
+
<%= f.label :preferred_usage_limit, t(:usage_limit) %><br />
|
35
|
+
<%= f.text_field :preferred_usage_limit %>
|
33
36
|
</p>
|
34
37
|
|
35
38
|
<p id="starts_at_field">
|
36
39
|
<%= f.label :starts_at %>
|
37
|
-
<%= f.
|
40
|
+
<%= f.text_field :starts_at, :class => 'datepicker' %>
|
38
41
|
</p>
|
39
42
|
<p id="expires_at_field">
|
40
43
|
<%= f.label :expires_at %>
|
41
|
-
<%= f.
|
44
|
+
<%= f.text_field :expires_at, :class => 'datepicker' %>
|
42
45
|
</p>
|
43
46
|
</fieldset>
|
44
|
-
|
45
|
-
<hr />
|
46
|
-
<%= render "admin/shared/calculator_fields", :f => f %>
|
@@ -0,0 +1,12 @@
|
|
1
|
+
<% promotion_action = action_form.object %>
|
2
|
+
<div class="promotion_action <%= promotion_action.type.demodulize.underscore %>" id="<%= dom_id promotion_action %>">
|
3
|
+
<span class="delete">
|
4
|
+
<%= link_to_with_icon 'cross', '', admin_promotion_promotion_action_path(@promotion, promotion_action), :remote => true, :method => 'delete' %>
|
5
|
+
</span>
|
6
|
+
<% type_name = promotion_action.class.name.demodulize.underscore %>
|
7
|
+
<% param_prefix = "promotion[promotion_actions_attributes][#{promotion_action.id}]" %>
|
8
|
+
<%#= hidden_field_tag "#{param_prefix}[id]", promotion_action.id %>
|
9
|
+
<p><%= t "promotion_action_types.#{type_name}.description" %></p>
|
10
|
+
|
11
|
+
<%= render "admin/promotions/actions/#{type_name}", :action_form => action_form, :promotion_action => promotion_action, :param_prefix => param_prefix %>
|
12
|
+
</div>
|
@@ -1,5 +1,5 @@
|
|
1
|
-
<div class="
|
2
|
-
<span class="delete"><%= link_to_with_icon 'cross', '', admin_promotion_promotion_rule_path(@promotion, promotion_rule),
|
1
|
+
<div class="promotion_rule" id="<%= dom_id promotion_rule %>">
|
2
|
+
<span class="delete"><%= link_to_with_icon 'cross', '', admin_promotion_promotion_rule_path(@promotion, promotion_rule), :remote => true, :method => 'delete' %></span>
|
3
3
|
<% type_name = promotion_rule.class.name.demodulize.underscore %>
|
4
4
|
<% param_prefix = "promotion[promotion_rules_attributes][#{promotion_rule.id}]" %>
|
5
5
|
<%= hidden_field_tag "#{param_prefix}[id]", promotion_rule.id %>
|
@@ -0,0 +1,32 @@
|
|
1
|
+
<fieldset id="rule_fields">
|
2
|
+
<legend><%= t(:rules) %></legend>
|
3
|
+
|
4
|
+
<%= form_for(@promotion, :url => object_url, :html => { :method => :put }) do |f| %>
|
5
|
+
<p>
|
6
|
+
<% for policy in Promotion::MATCH_POLICIES %>
|
7
|
+
<label><%= f.radio_button :preferred_match_policy, policy %> <%= t "promotion_form.match_policies.#{policy}" %></label>
|
8
|
+
<% end %>
|
9
|
+
</p>
|
10
|
+
<div id="rules" class="filter_list">
|
11
|
+
<% if @promotion.rules.any? %>
|
12
|
+
<%= render :partial => 'promotion_rule', :collection => @promotion.rules, :locals => {} %>
|
13
|
+
<% else %>
|
14
|
+
<!-- <p><%= t('no_rules_added') %></p> -->
|
15
|
+
<% end %>
|
16
|
+
</div>
|
17
|
+
<p class="form-buttons">
|
18
|
+
<%= button t("update") %>
|
19
|
+
</p>
|
20
|
+
<% end %>
|
21
|
+
|
22
|
+
<%= form_tag(admin_promotion_promotion_rules_path(@promotion), :remote => true,
|
23
|
+
:id => 'new_product_rule_form') do %>
|
24
|
+
<% options = options_for_select( PromotionRule.registered_class_names.map {|name| [ t("promotion_rule_types.#{name.demodulize.underscore}.name"), name] } ) %>
|
25
|
+
<p>
|
26
|
+
<label for="promotion_rule_type"><%= t('add_rule_of_type') %></label>
|
27
|
+
<%= select_tag("promotion_rule[type]", options) %>
|
28
|
+
<input type="submit" value="<%= t('add') %>" />
|
29
|
+
</p>
|
30
|
+
<% end %>
|
31
|
+
|
32
|
+
</fieldset>
|
@@ -0,0 +1,19 @@
|
|
1
|
+
<div class="calculator-fields">
|
2
|
+
|
3
|
+
<p class="field">
|
4
|
+
<%= action_form.label :calculator_type, t("calculator") %>
|
5
|
+
<%= action_form.collection_select :calculator_type, @calculators, :to_s, :description, {}, {:class => 'type-select'} %>
|
6
|
+
<% if promotion_action.calculator.respond_to?(:preferences) %>
|
7
|
+
<span class="warning"><%= t('calculator_settings_warning')%></span>
|
8
|
+
<% end %>
|
9
|
+
</p>
|
10
|
+
|
11
|
+
<% unless promotion_action.new_record? %>
|
12
|
+
<div class="settings">
|
13
|
+
<%= action_form.fields_for :calculator, promotion_action.calculator do |calculator_form| %>
|
14
|
+
<%= preference_fields(promotion_action.calculator, calculator_form) %>
|
15
|
+
<% end %>
|
16
|
+
</div>
|
17
|
+
<% end %>
|
18
|
+
|
19
|
+
</div>
|
@@ -0,0 +1,52 @@
|
|
1
|
+
<table class="index">
|
2
|
+
<tr>
|
3
|
+
<th>Product</th>
|
4
|
+
<th>Variant</th>
|
5
|
+
<th>Quantity</th>
|
6
|
+
<th> </th>
|
7
|
+
</tr>
|
8
|
+
<% for item in promotion_action.promotion_action_line_items %>
|
9
|
+
<tr>
|
10
|
+
<td>
|
11
|
+
<%= item.variant.product.name %>
|
12
|
+
</td>
|
13
|
+
<td>
|
14
|
+
<%= item.variant.options_text.presence || t("master") %>
|
15
|
+
</td>
|
16
|
+
<td>
|
17
|
+
<%= item.quantity %>
|
18
|
+
</td>
|
19
|
+
<td>
|
20
|
+
<img src='/admin/images/icons/cross.png' />
|
21
|
+
</td>
|
22
|
+
</tr>
|
23
|
+
<% end %>
|
24
|
+
</table>
|
25
|
+
|
26
|
+
|
27
|
+
|
28
|
+
<%= action_form.hidden_field :line_items_string %>
|
29
|
+
|
30
|
+
|
31
|
+
<div class="add-line-item">
|
32
|
+
<fieldset>
|
33
|
+
<legend><%= t('add_product') %></legend>
|
34
|
+
<div style="float:left;width:50%;margin-right:5%;">
|
35
|
+
<%= label_tag :add_product_name, t("name_or_sku") %>
|
36
|
+
<%= text_field_tag :add_product_name, nil, :class => 'fullwidth' %>
|
37
|
+
</div>
|
38
|
+
<div style="float:left;width:10%;margin-right:5%;">
|
39
|
+
<%= label_tag :add_line_item_variant_id, t("variant") %><br />
|
40
|
+
<select name="add_line_item_variant_id" class="fullwidth"></select>
|
41
|
+
</div>
|
42
|
+
<div style="float:left;width:10%;margin-right:5%;">
|
43
|
+
<%= label_tag :add_quantity, t("qty") %>
|
44
|
+
<%= text_field_tag :add_quantity, 1 %>
|
45
|
+
</div>
|
46
|
+
<div style="float:left;width:j0%">
|
47
|
+
<br />
|
48
|
+
<button class="add small"> <%= t("add") %></button>
|
49
|
+
</div>
|
50
|
+
|
51
|
+
</fieldset>
|
52
|
+
</div>
|