Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Allow volume as sum of product's variants.

  • Loading branch information...
commit 07fe92e4316e744ea364e72f860929fd181cb788 1 parent d66d65a
Adam Wróbel authored April 12, 2011
2  app/controllers/admin/base_controller_decorator.rb
... ...
@@ -1,4 +1,4 @@
1 1
 require "render_inheritable"
2 2
 Admin::BaseController.class_eval do
3 3
   render_inheritable
4  
-end
  4
+end unless Admin::BaseController.included_modules.include? RenderInheritable
2  app/controllers/admin/products_controller_decorator.rb
@@ -10,4 +10,4 @@ def volume_prices
10 10
       render :edit
11 11
     end
12 12
   end
13  
-end
  13
+end unless Admin::ProductsController.instance_methods.include? :volume_prices
8  app/helpers/products_helper_decorator.rb
... ...
@@ -0,0 +1,8 @@
  1
+ProductsHelper.module_eval do
  2
+  def variant_price_diff_with_volume_discount variant
  3
+    return if variant.product.variants_use_master_discount
  4
+    variant_price_diff_without_volume_discount variant
  5
+  end
  6
+  alias_method_chain :variant_price_diff, :volume_discount
  7
+end unless ProductsHelper.instance_methods.include? \
  8
+             :variant_price_diff_with_volume_discount
66  app/models/line_item_decorator.rb
... ...
@@ -1,18 +1,20 @@
1 1
 LineItem.class_eval do
2  
-  before_save :check_update_volume_discount
3  
-
4 2
   def update_volume_discount updated_order = nil
5 3
     self.volume_discount = 0
  4
+    self.order = updated_order if updated_order
6 5
 
7  
-    return if self.quantity < 1 || variant.volume_prices.empty?
  6
+    if variant.uses_master_discount? && new_record?
  7
+      self.price = variant.volume_prices_source.price
  8
+    end
8 9
 
9  
-    self.order = updated_order if updated_order
10  
-    starting_quantity = order.variant_starting_quantity(variant)
  10
+    return if quantity < 1 || !variant.uses_volume_pricing?
  11
+
  12
+    unless variant.requires_per_product_discount?
  13
+      starting_quantity = order.variants_starting_quantity(variant_id)
11 14
 
12  
-    self.volume_discount = if variant.progressive_volume_discount
13  
-      progressive_price_strategy starting_quantity
14  
-    else
15  
-      uniform_price_strategy starting_quantity
  15
+      volume_cost = variant.volume_prices_source.total_volume_cost \
  16
+                      starting_quantity, quantity
  17
+      self.volume_discount = volume_cost - quantity * self.price
16 18
     end
17 19
   end
18 20
 
@@ -23,44 +25,18 @@ def amount_with_volume_discount
23 25
   alias_method_chain :amount, :volume_discount
24 26
   alias total amount
25 27
 
26  
-  private
27  
-  def check_update_volume_discount
28  
-    update_volume_discount if price_changed? || quantity_changed?
29  
-  end
30  
-
31  
-  def uniform_price_strategy starting_quantity
32  
-    total_quantity = self.quantity + starting_quantity
33  
-    final_price = default_price = self.price
34  
-
35  
-    variant.volume_prices.each do |vp|
36  
-      break if vp.starting_quantity > total_quantity
37  
-      final_price = vp.price
  28
+  def update_order_with_volume_discount
  29
+    if quantity > 0 && variant.requires_per_product_discount?
  30
+      order.update_product_volume_discount variant.product
38 31
     end
39 32
 
40  
-    self.quantity * (final_price - default_price)
  33
+    update_order_without_volume_discount
41 34
   end
  35
+  alias_method_chain :update_order, :volume_discount
42 36
 
43  
-  def progressive_price_strategy units_processed
44  
-    discount = 0
45  
-    total_quantity = self.quantity + units_processed
46  
-    current_price = default_price = self.price
47  
-
48  
-    variant.volume_prices.each do |vp|
49  
-      if vp.starting_quantity - 1 > units_processed
50  
-        last_unit_for_this_price = [total_quantity, vp.starting_quantity - 1].min
51  
-        items_count = last_unit_for_this_price - units_processed
52  
-        discount += items_count * (current_price - default_price)
53  
-        units_processed = last_unit_for_this_price
54  
-      end
55  
-      break if vp.starting_quantity > total_quantity
56  
-      current_price = vp.price
57  
-    end
58  
-
59  
-    if total_quantity > units_processed
60  
-      items_count = total_quantity - units_processed
61  
-      discount += items_count * (current_price - default_price)
62  
-    end
63  
-
64  
-    discount
  37
+  private
  38
+  before_save :check_update_volume_discount
  39
+  def check_update_volume_discount
  40
+    update_volume_discount if price_changed? || quantity_changed?
65 41
   end
66  
-end
  42
+end unless LineItem.instance_methods.include? :amount_with_volume_discount
52  app/models/order_decorator.rb
@@ -3,24 +3,68 @@ def volume_discount
3 3
     line_items.map(&:volume_discount).sum
4 4
   end
5 5
 
  6
+  def products_line_items product
  7
+    line_items.all.select {|i| product.all_variant_ids.include? i.variant_id}
  8
+  end
  9
+
6 10
   # By default volume price is calculated based only on quantity of the current
7 11
   # order. If you want to have "Volume Customers" - people who purchase at
8 12
   # volume prices, but making multiple orders of smaller quantities you can
9  
-  # overwrite this method. You could return the total quantity of given variant
  13
+  # overwrite this method. You could return the total quantity of given variants
10 14
   # the customer bought in the last month. The line items volume price
11 15
   # calculation will be adjusted by that number. Like this:
12 16
   # variant_starting_quantity + current order quantity
13  
-  def variant_starting_quantity variant
  17
+  def variants_starting_quantity *variant_ids
14 18
     0
15 19
   end
16 20
 
  21
+  def update_totals_with_volume_discount
  22
+    # we need to refresh the items
  23
+    line_items true
  24
+    update_totals_without_volume_discount
  25
+  end
  26
+  alias_method_chain :update_totals, :volume_discount
  27
+
17 28
   # This is required for Volume Customers
18 29
   # It updates item_total when user logs in
19 30
   def update_totals_on_user_association
20 31
     return unless user_id_changed? || email_changed?
21 32
 
22  
-    line_items.each {|li| li.update_volume_discount self}
  33
+    products_to_update = []
  34
+
  35
+    line_items.each do |li|
  36
+      if li.variant.requires_per_product_discount?
  37
+        products_to_update << li.variant.product
  38
+      else
  39
+        li.update_volume_discount self
  40
+        li.save
  41
+      end
  42
+    end
  43
+
  44
+    products_to_update.uniq.each {|p| update_product_volume_discount p}
  45
+
23 46
     update_totals
24 47
   end
25 48
   before_save :update_totals_on_user_association
26  
-end
  49
+
  50
+  def update_product_volume_discount product
  51
+    line_items true
  52
+    items = products_line_items(product).sort_by &:variant_id
  53
+
  54
+    return if items.blank?
  55
+
  56
+    starting_quantity = variants_starting_quantity *product.all_variant_ids
  57
+    quantity = items.map(&:quantity).sum
  58
+    volume_cost = product.master.total_volume_cost starting_quantity, quantity
  59
+    regular_cost = items.map(&:amount_without_volume_discount).sum
  60
+    volume_discount = volume_cost - regular_cost
  61
+
  62
+    # only one item stores the discount to avoid division problems
  63
+    items.shift.update_attributes_without_callbacks \
  64
+      :volume_discount => volume_discount
  65
+
  66
+    items.each do |i|
  67
+      i.update_attributes_without_callbacks :volume_discount => 0.0
  68
+    end
  69
+  end
  70
+end unless Order.instance_methods.include? :update_totals_on_user_association
14  app/models/product_decorator.rb
@@ -3,11 +3,23 @@
3 3
     :volume_prices_attributes=,
4 4
     :progressive_volume_discount
5 5
 
  6
+  def uses_volume_pricing?
  7
+    if variants_use_master_discount
  8
+      !master.volume_prices.empty?
  9
+    else
  10
+      !Product.where(:id => id).joins(:variants => :volume_prices).empty?
  11
+    end
  12
+  end
  13
+
6 14
   def save_master
7 15
     return unless master && (master.changed? || master.new_record? || master.changed_for_autosave?)
8 16
     raise ActiveRecord::Rollback unless master.save
9 17
   end
10 18
 
  19
+  def all_variant_ids
  20
+    @all_variant_ids ||= Variant.where(:product_id => id).map &:id
  21
+  end
  22
+
11 23
   private
12 24
   def duplicate_extra original
13 25
     return unless original
@@ -15,4 +27,4 @@ def duplicate_extra original
15 27
       VolumePrice.new vp.attributes.slice('starting_quantity', 'price')
16 28
     end
17 29
   end
18  
-end
  30
+end unless Product.instance_methods.include? :all_variant_ids
66  app/models/variant_decorator.rb
@@ -9,6 +9,34 @@
9 9
 
10 10
   after_create :copy_master_volume_prices
11 11
 
  12
+  def volume_prices_source
  13
+    if !is_master && product.variants_use_master_discount
  14
+      product.master
  15
+    else
  16
+      self
  17
+    end
  18
+  end
  19
+
  20
+  def uses_volume_pricing?
  21
+    volume_prices_source.volume_prices.present?
  22
+  end
  23
+
  24
+  def uses_master_discount?
  25
+    product.variants_use_master_discount
  26
+  end
  27
+
  28
+  def requires_per_product_discount?
  29
+    uses_master_discount? && product.variants.present?
  30
+  end
  31
+
  32
+  def total_volume_cost starting_quantity, quantity
  33
+    if progressive_volume_discount
  34
+      progressive_total_cost starting_quantity, quantity
  35
+    else
  36
+      uniform_total_cost starting_quantity, quantity
  37
+    end
  38
+  end
  39
+
12 40
   def blank_volume_price attributes
13 41
     attributes['starting_quantity'].blank? && attributes['price'].blank?
14 42
   end
@@ -21,4 +49,40 @@ def copy_master_volume_prices
21 49
       VolumePrice.new vp.attributes.slice('starting_quantity', 'price')
22 50
     end
23 51
   end
24  
-end
  52
+
  53
+  def uniform_total_cost starting_quantity, quantity
  54
+    total_quantity = quantity + starting_quantity
  55
+    final_price = default_price = self.price
  56
+
  57
+    volume_prices.each do |vp|
  58
+      break if vp.starting_quantity > total_quantity
  59
+      final_price = vp.price
  60
+    end
  61
+
  62
+    quantity * final_price
  63
+  end
  64
+
  65
+  def progressive_total_cost units_processed, quantity
  66
+    total_cost = 0
  67
+    total_quantity = quantity + units_processed
  68
+    current_price = default_price = self.price
  69
+
  70
+    volume_prices.each do |vp|
  71
+      if vp.starting_quantity - 1 > units_processed
  72
+        last_unit_for_this_price = [total_quantity, vp.starting_quantity - 1].min
  73
+        items_count = last_unit_for_this_price - units_processed
  74
+        total_cost += items_count * current_price
  75
+        units_processed = last_unit_for_this_price
  76
+      end
  77
+      break if vp.starting_quantity > total_quantity
  78
+      current_price = vp.price
  79
+    end
  80
+
  81
+    if total_quantity > units_processed
  82
+      items_count = total_quantity - units_processed
  83
+      total_cost += items_count * current_price
  84
+    end
  85
+
  86
+    total_cost
  87
+  end
  88
+end unless Variant.instance_methods.include? :volume_prices_source
9  app/views/admin/orders/_form_volume_discount.html.erb
... ...
@@ -0,0 +1,9 @@
  1
+<% if @order.volume_discount != 0.0 %>
  2
+  <tbody id='volume-discount'>
  3
+    <tr class="total" id="volume-discount-row">
  4
+      <td colspan="3"><b><%= t('volume_discount') %>:</b></td>
  5
+      <td class="total"><span><%= number_to_currency @order.volume_discount %></span></td>
  6
+      <td></td>
  7
+    </tr>
  8
+  </tbody>
  9
+<% end %>
12  app/views/admin/products/volume_prices.html.erb
@@ -8,12 +8,22 @@
8 8
              :html => { :method => :put } do |f| %>
9 9
   <h3><%= t("volume_prices") %></h3>
10 10
   <style>
11  
-    #progressive_discount_info {
  11
+    #progressive_discount_info,
  12
+    #variants_use_master_discount_info {
12 13
       display: inline-block;
13 14
       margin-left: 20pt;
14 15
       font-size: 0.8em;
15 16
     }
16 17
   </style>
  18
+  <%= fields_for @product do |pf| %>
  19
+    <p>
  20
+      <%= pf.check_box :variants_use_master_discount %>
  21
+      <%= pf.label :variants_use_master_discount %>
  22
+      <span id="variants_use_master_discount_info">
  23
+        <%= t 'variants_use_master_discount_info' %>
  24
+      </span>
  25
+    </p>
  26
+  <% end %>
17 27
   <p>
18 28
     <%= f.check_box :progressive_volume_discount %>
19 29
     <%= f.label :progressive_volume_discount %>
71  app/views/admin/variants/_volume_prices.html.erb
... ...
@@ -1,33 +1,40 @@
1  
-<%= fields_for @variant do |f| %>
2  
-  <h3><%= t("volume_prices") %></h3>
3  
-  <style>
4  
-    #progressive_discount_info {
5  
-      display: inline-block;
6  
-      margin-left: 20pt;
7  
-      font-size: 0.8em;
8  
-    }
9  
-  </style>
10  
-  <p>
11  
-    <%= f.check_box :progressive_volume_discount %>
12  
-    <%= f.label :progressive_volume_discount %>
13  
-    <span id="progressive_discount_info">
14  
-      <%= t 'progressive_discount_info' %>
15  
-    </span>
16  
-  </p>
17  
-  <table class="index">
18  
-    <thead>
19  
-      <tr>
20  
-        <th><%= t("starting_from") %></th>
21  
-        <th><%= t("price") %></th>
22  
-        <th><%= t("action") %></th>
23  
-      </tr>
24  
-    </thead>
25  
-    <tbody id="volume_prices">
26  
-      <%= f.fields_for :volume_prices do |vp_form| %>
27  
-        <%= render "volume_price_fields", :f => vp_form -%>
28  
-      <% end %>
29  
-    </tbody>
30  
-  </table>
31  
-  <%= link_to_add_fields icon('add') + ' ' + t("add_volume_price"), "tbody#volume_prices", f, :volume_prices %>
32  
-  <br/><br/>
  1
+<h3><%= t("volume_prices") %></h3>
  2
+<% if @variant.product.variants_use_master_discount %>
  3
+  <script>
  4
+    $('p:has(input[id=variant_price])').hide();
  5
+  </script>
  6
+  <%= t :variant_prices_ignored_info %>
  7
+<% else %>
  8
+  <%= fields_for @variant do |f| %>
  9
+    <style>
  10
+      #progressive_discount_info {
  11
+        display: inline-block;
  12
+        margin-left: 20pt;
  13
+        font-size: 0.8em;
  14
+      }
  15
+    </style>
  16
+    <p>
  17
+      <%= f.check_box :progressive_volume_discount %>
  18
+      <%= f.label :progressive_volume_discount %>
  19
+      <span id="progressive_discount_info">
  20
+        <%= t 'progressive_discount_info' %>
  21
+      </span>
  22
+    </p>
  23
+    <table class="index">
  24
+      <thead>
  25
+        <tr>
  26
+          <th><%= t("starting_from") %></th>
  27
+          <th><%= t("price") %></th>
  28
+          <th><%= t("action") %></th>
  29
+        </tr>
  30
+      </thead>
  31
+      <tbody id="volume_prices">
  32
+        <%= f.fields_for :volume_prices do |vp_form| %>
  33
+          <%= render "volume_price_fields", :f => vp_form -%>
  34
+        <% end %>
  35
+      </tbody>
  36
+    </table>
  37
+    <%= link_to_add_fields icon('add') + ' ' + t("add_volume_price"), "tbody#volume_prices", f, :volume_prices %>
  38
+    <br/><br/>
  39
+  <% end %>
33 40
 <% end %>
4  app/views/products/_volume_prices.html.erb
... ...
@@ -1,10 +1,10 @@
1  
-<% if @product.master.volume_prices.empty? %>
  1
+<% if !@product.uses_volume_pricing? %>
2 2
   <p class="prices">
3 3
     <%= t("price") -%>
4 4
     <br />
5 5
     <span class="price selling"><%= product_price(@product) %></span><br />
6 6
   </p>
7  
-<% elsif @product.has_variants? %>
  7
+<% elsif !@product.variants_use_master_discount && @product.has_variants? %>
8 8
   <p class="prices">
9 9
     <%= t("price") -%>
10 10
     <br />
5  config/locales/en.yml
@@ -2,6 +2,7 @@ en:
2 2
   activerecord:
3 3
     attributes:
4 4
       product:
  5
+        variants_use_master_discount: Single volume discount for all variants
5 6
         progressive_volume_discount: Progressive volume discount
6 7
       variant:
7 8
         progressive_volume_discount: Progressive volume discount
@@ -14,4 +15,6 @@ en:
14 15
   next: Next
15 16
   additional: Additional
16 17
   at: at
17  
-  progressive_discount_info: By default, a single volume price selected based on the item's quantity is applied to all it's units. Select this option to have a progressively reduced price applied to successive units depending on how many are already in the cart, i.e., the first three at $10 each, the next three at $9 each, and all additional units at $8 apiece.
  18
+  progressive_discount_info: By default, a single volume price selected based on the item's quantity is applied to all it's units. Select this option to have a progressively reduced price applied to successive units depending on how many are already in the cart, e.g., the first three at $10 each, the next three at $9 each, and all additional units at $8 apiece.
  19
+  variants_use_master_discount_info: Calculate this product's volume discount based on the sum of it's variants in the cart instead of calculating it for each variant separately. This implies that the starting price and volume prices that were set for particular variants will be ignored and, instead, the product's own pricing scheme will be used.
  20
+  variant_prices_ignored_info: This variant uses prices set on the product level. If you want to customize pricing scheme for this particular variant you need to disable the "Single volume discount for all variants" option in the product's Volume Pricing screen.
10  db/migrate/20110408162443_add_variants_use_master_discount_to_products.rb
... ...
@@ -0,0 +1,10 @@
  1
+class AddVariantsUseMasterDiscountToProducts < ActiveRecord::Migration
  2
+  def self.up
  3
+    add_column :products, :variants_use_master_discount, :boolean,
  4
+      :null => false, :default => 0
  5
+  end
  6
+
  7
+  def self.down
  8
+    remove_column :products, :variants_use_master_discount
  9
+  end
  10
+end
1  lib/volume_pricing_hooks.rb
@@ -4,4 +4,5 @@ class VolumePricingHooks < Spree::ThemeSupport::HookListener
4 4
   replace :product_price, :partial => "products/volume_prices"
5 5
   insert_after :cart_items, :partial => "orders/cart_volume_discount"
6 6
   insert_before :order_details_subtotal, :partial => "shared/order_details_volume_discount"
  7
+  insert_before :admin_order_form_subtotal, :partial => "admin/orders/form_volume_discount"
7 8
 end
2  spree_simple_volume_pricing.gemspec
@@ -3,7 +3,7 @@
3 3
 Gem::Specification.new do |s|
4 4
   s.platform = Gem::Platform::RUBY
5 5
   s.name     = 'spree_simple_volume_pricing'
6  
-  s.version  = '2.1.1'
  6
+  s.version  = '3.0.0'
7 7
   s.summary  = 'Adds volume pricing capabilities to Spree'
8 8
 
9 9
   s.author   = 'Adam Wróbel'

0 notes on commit 07fe92e

Please sign in to comment.
Something went wrong with that request. Please try again.