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.
Files changed (58) hide show
  1. data/LICENSE +26 -0
  2. data/app/assets/javascripts/admin/promotions.js +131 -0
  3. data/app/assets/javascripts/admin/spree_promo.js +3 -0
  4. data/app/assets/javascripts/store/spree_promo.js +2 -0
  5. data/{public → app/assets}/stylesheets/admin/promotions.css +19 -29
  6. data/app/assets/stylesheets/admin/spree_promo.css +5 -0
  7. data/app/assets/stylesheets/store/spree_promo.css +4 -0
  8. data/app/controllers/admin/promotion_actions_controller.rb +28 -0
  9. data/app/controllers/admin/promotions_controller.rb +2 -2
  10. data/app/controllers/content_controller_decorator.rb +8 -0
  11. data/app/controllers/orders_controller_decorator.rb +14 -5
  12. data/app/models/adjustment_decorator.rb +3 -0
  13. data/app/models/order_decorator.rb +24 -0
  14. data/app/models/product_decorator.rb +10 -0
  15. data/app/models/product_group_decorator.rb +3 -0
  16. data/app/models/promotion.rb +61 -41
  17. data/app/models/promotion/actions/create_adjustment.rb +28 -0
  18. data/app/models/promotion/actions/create_line_items.rb +29 -0
  19. data/app/models/promotion/rules/first_order.rb +1 -1
  20. data/app/models/promotion/rules/item_total.rb +3 -2
  21. data/app/models/promotion/rules/landing_page.rb +17 -0
  22. data/app/models/promotion/rules/product.rb +1 -1
  23. data/app/models/promotion/rules/user.rb +1 -1
  24. data/app/models/promotion/rules/user_logged_in.rb +13 -0
  25. data/app/models/promotion_action.rb +19 -0
  26. data/app/models/promotion_action_line_item.rb +6 -0
  27. data/app/models/promotion_rule.rb +3 -16
  28. data/app/overrides/promo_admin_tabs.rb +5 -0
  29. data/app/overrides/promo_coupon_code_field.rb +5 -0
  30. data/app/overrides/promo_product_properties.rb +5 -0
  31. data/app/views/admin/promotion_actions/create.js.erb +7 -0
  32. data/app/views/admin/promotion_actions/destroy.js.erb +2 -0
  33. data/app/views/admin/promotions/_actions.html.erb +32 -0
  34. data/app/views/admin/promotions/_form.html.erb +17 -17
  35. data/app/views/admin/promotions/_promotion_action.html.erb +12 -0
  36. data/app/views/admin/promotions/_promotion_rule.html.erb +2 -2
  37. data/app/views/admin/promotions/_rules.html.erb +32 -0
  38. data/app/views/admin/promotions/actions/_create_adjustment.html.erb +19 -0
  39. data/app/views/admin/promotions/actions/_create_line_items.html.erb +52 -0
  40. data/app/views/admin/promotions/actions/_give_store_credit.html.erb +6 -0
  41. data/app/views/admin/promotions/edit.html.erb +4 -38
  42. data/app/views/admin/promotions/index.html.erb +5 -7
  43. data/app/views/admin/promotions/rules/_landing_page.html.erb +8 -0
  44. data/app/views/admin/promotions/rules/_user_logged_in.html.erb +3 -0
  45. data/app/views/checkout/_coupon_code_field.html.erb +1 -1
  46. data/config/locales/en.yml +27 -5
  47. data/config/routes.rb +1 -0
  48. data/db/migrate/20101026184833_migrate_adjustments.rb +9 -0
  49. data/db/migrate/20110331094351_promotion_changes_to_subclass_of_activator.rb +22 -0
  50. data/db/migrate/20110404123358_create_promotion_actions.rb +13 -0
  51. data/db/migrate/20110601202923_create_promotion_action_line_items.rb +12 -0
  52. data/lib/spree_promo.rb +24 -100
  53. metadata +77 -26
  54. data/app/models/promotion_credit.rb +0 -9
  55. data/db/migrate/20100419194457_fix_existing_coupon_credits.rb +0 -9
  56. data/lib/spree_promo_hooks.rb +0 -11
  57. data/lib/tasks/install.rake +0 -26
  58. 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
+
@@ -1,6 +1,6 @@
1
1
  class Promotion::Rules::FirstOrder < PromotionRule
2
2
 
3
- def eligible?(order)
3
+ def eligible?(order, options = {})
4
4
  order.user && order.user.orders.complete.count == 0
5
5
  end
6
6
 
@@ -6,7 +6,8 @@ class Promotion::Rules::ItemTotal < PromotionRule
6
6
 
7
7
  OPERATORS = ['gt', 'gte']
8
8
 
9
- def eligible?(order)
10
- order.item_total.send(preferred_operator == 'gte' ? :>= : :>, preferred_amount)
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
+
@@ -0,0 +1,6 @@
1
+ class PromotionActionLineItem < ActiveRecord::Base
2
+
3
+ belongs_to :promotion_action, :class_name => 'Promotion::Actions::CreateLineItems'
4
+ belongs_to :variant
5
+
6
+ end
@@ -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,5 @@
1
+ Deface::Override.new(:virtual_path => "layouts/admin",
2
+ :name => "promo_admin_tabs",
3
+ :insert_bottom => "[data-hook='admin_tabs'], #admin_tabs[data-hook]",
4
+ :text => "<%= tab(:promotions) %>",
5
+ :disabled => false)
@@ -0,0 +1,5 @@
1
+ Deface::Override.new(:virtual_path => "checkout/_payment",
2
+ :name => "promo_coupon_code_field",
3
+ :replace => "[data-hook='coupon_code_field'], #coupon_code_field[data-hook]",
4
+ :partial => "checkout/coupon_code_field",
5
+ :disabled => false)
@@ -0,0 +1,5 @@
1
+ Deface::Override.new(:virtual_path => "products/show",
2
+ :name => "promo_product_properties",
3
+ :insert_after => "[data-hook='product_properties'], #product_properties[data-hook]",
4
+ :partial => "products/promotions",
5
+ :disabled => false)
@@ -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,2 @@
1
+ jQuery('#<%= dom_id @promotion_action %>').fadeOut().remove();
2
+
@@ -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 :code do %>
15
- <%= f.label :code %><br />
16
- <%= f.text_field :code %>
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 :usage_limit %>
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.spree_date_picker :starts_at, :prompt => true, :use_month_numbers => true %>
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.spree_date_picker :expires_at, :prompt => true, :use_month_numbers => true %>
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="promotion-rule" id="<%= dom_id promotion_rule %>">
2
- <span class="delete"><%= link_to_with_icon 'cross', '', admin_promotion_promotion_rule_path(@promotion, promotion_rule), 'data-remote' => true, 'data-method' => 'delete' %></span>
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> &nbsp;
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
+ &nbsp; <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>&nbsp;</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
+ &nbsp;<br />
48
+ <button class="add small"> <%= t("add") %></button>
49
+ </div>
50
+
51
+ </fieldset>
52
+ </div>