spree_volume_pricing 3.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.markdown +80 -0
- data/app/models/volume_price.rb +27 -0
- data/app/views/admin/shared/_vp_product_tab.html.erb +5 -0
- data/app/views/admin/variants/_edit_fields.html.erb +17 -0
- data/app/views/admin/variants/_volume_price.html.erb +19 -0
- data/app/views/admin/variants/volume_prices.html.erb +28 -0
- data/lib/spree_volume_pricing.rb +5 -0
- data/lib/spree_volume_pricing/engine.rb +109 -0
- data/lib/spree_volume_pricing_hooks.rb +7 -0
- data/lib/tasks/volume_pricing_extension_tasks.rake +29 -0
- metadata +94 -0
data/README.markdown
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
Volume Pricing
|
2
|
+
==============
|
3
|
+
|
4
|
+
Volume Pricing is an extension to Spree (a complete open source commerce solution for Ruby on Rails) that uses predefined ranges of quantities to determine the price for a particular product variant. For instance, this allows you to set a price for quantities between 1-10, another price for quantities between (10-100) and another for quantities of 100 or more. If no volume price is defined for a variant, then the standard price is used.
|
5
|
+
|
6
|
+
Each VolumePrice contains the following values:
|
7
|
+
|
8
|
+
1. **Variant:** Each VolumePrice is associated with a _Variant_, which is used to link products to particular prices.
|
9
|
+
2. **Display:** The human readable reprentation of the quantity range (Ex. 10-100). (Optional)
|
10
|
+
3. **Range:** The quantity range for which the price is valid (See Below for Examples of Valid Ranges.)
|
11
|
+
4. **Amount:** The price of the product if the line item quantity falls within the specified range.
|
12
|
+
5. **Position:** Integer value for `acts_as_list` (Helps keep the volume prices in a defined order.)
|
13
|
+
|
14
|
+
Ranges
|
15
|
+
======
|
16
|
+
|
17
|
+
Ranges are expressed as Strings and are similar to the format of a Range object in Ruby. The lower numeber of the range is always inclusive. If the range is defined with '..' then it also includes the upper end of the range. If the range is defined with '...' then the upper end of the range is not inclusive.
|
18
|
+
|
19
|
+
Ranges can also be defined as "open ended." Open ended ranges are defined with an integer followed by a '+' character. These ranges are inclusive of the integer and any value higher then the integer.
|
20
|
+
|
21
|
+
All ranges need to be expressed as Strings and must include parentheses. "(1..10)" is considered to be a valid range. "1..10" is not considered to be a valid range (missing the parentheses.)
|
22
|
+
|
23
|
+
Examples
|
24
|
+
========
|
25
|
+
Consider the following examples of volume prices:
|
26
|
+
|
27
|
+
Variant Display Range Amount Position
|
28
|
+
-------------------------------------------------------------------------------
|
29
|
+
Rails T-Shirt 1-5 (1..5) 19.99 1
|
30
|
+
Rails T-Shirt 6-9 (6...10) 18.99 2
|
31
|
+
Rails T-Shirt 10 or more (10+) 17.99 3
|
32
|
+
|
33
|
+
## Example 1
|
34
|
+
|
35
|
+
Cart Contents:
|
36
|
+
|
37
|
+
Product Quantity Price Total
|
38
|
+
----------------------------------------------------------------
|
39
|
+
Rails T-Shirt 1 19.99 19.99
|
40
|
+
|
41
|
+
## Example 2
|
42
|
+
|
43
|
+
Cart Contents:
|
44
|
+
|
45
|
+
Product Quantity Price Total
|
46
|
+
----------------------------------------------------------------
|
47
|
+
Rails T-Shirt 5 19.99 99.95
|
48
|
+
|
49
|
+
## Example 3
|
50
|
+
|
51
|
+
Cart Contents:
|
52
|
+
|
53
|
+
Product Quantity Price Total
|
54
|
+
----------------------------------------------------------------
|
55
|
+
Rails T-Shirt 6 18.99 113.94
|
56
|
+
|
57
|
+
## Example 4
|
58
|
+
|
59
|
+
Cart Contents:
|
60
|
+
|
61
|
+
Product Quantity Price Total
|
62
|
+
----------------------------------------------------------------
|
63
|
+
Rails T-Shirt 10 17.99 179.90
|
64
|
+
|
65
|
+
## Example 5
|
66
|
+
|
67
|
+
Cart Contents:
|
68
|
+
|
69
|
+
Product Quantity Price Total
|
70
|
+
----------------------------------------------------------------
|
71
|
+
Rails T-Shirt 20 17.99 359.80
|
72
|
+
|
73
|
+
|
74
|
+
Additional Notes
|
75
|
+
================
|
76
|
+
|
77
|
+
* The volume price is applied based on the total quantity ordered for a particular variant. It does not apply different prices for the portion of the quantity that falls within a particular range. Only the one price is used (although this would be an interesting configurable option if someone wanted to write a patch.)
|
78
|
+
|
79
|
+
|
80
|
+
|
@@ -0,0 +1,27 @@
|
|
1
|
+
class VolumePrice < ActiveRecord::Base
|
2
|
+
belongs_to :variant
|
3
|
+
acts_as_list :scope => :variant
|
4
|
+
validates_presence_of :variant
|
5
|
+
validates_presence_of :amount
|
6
|
+
|
7
|
+
OPEN_ENDED = /\([0-9]+\+\)/
|
8
|
+
|
9
|
+
def validate
|
10
|
+
return if open_ended?
|
11
|
+
errors.add(:range, "must be in one of the following formats: (a..b), (a...b), (a+)") unless /\([0-9]+\.{2,3}[0-9]+\)/ =~ range
|
12
|
+
end
|
13
|
+
|
14
|
+
def include?(quantity)
|
15
|
+
if open_ended?
|
16
|
+
bound = /\d+/.match(range)[0].to_i
|
17
|
+
return quantity >= bound
|
18
|
+
else
|
19
|
+
range.to_range === quantity
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# indicates whether or not the range is a true Ruby range or an open ended range with no upper bound
|
24
|
+
def open_ended?
|
25
|
+
OPEN_ENDED =~ range
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
<h3><%= t("volume_prices") %></h3>
|
2
|
+
<table class="index">
|
3
|
+
<thead>
|
4
|
+
<tr>
|
5
|
+
<th><%= t("display") %></th>
|
6
|
+
<th><%= t("range") %></th>
|
7
|
+
<th><%= t("amount") %></th>
|
8
|
+
<th><%= t("position") %></th>
|
9
|
+
<th><%= t("action") %></th>
|
10
|
+
</tr>
|
11
|
+
</thead>
|
12
|
+
<tbody id="volume_prices">
|
13
|
+
<%= f.render_associated_form(@variant.volume_prices) %>
|
14
|
+
</tbody>
|
15
|
+
</table>
|
16
|
+
<%= f.add_associated_link(icon('add') + ' ' + t("add_volume_price"), @variant.volume_prices.build) %>
|
17
|
+
<br/><br/>
|
@@ -0,0 +1,19 @@
|
|
1
|
+
<tr class="volume_price">
|
2
|
+
<td valign="top">
|
3
|
+
<%= error_message_on(f.object, :display) %>
|
4
|
+
<%= f.text_field(:display, :size => 10) %>
|
5
|
+
</td>
|
6
|
+
<td valign="top">
|
7
|
+
<%= error_message_on(f.object, :range) %>
|
8
|
+
<%= f.text_field(:range, :size => 10) %>
|
9
|
+
</td>
|
10
|
+
<td valign="top">
|
11
|
+
<%= error_message_on(f.object, :amount) %>
|
12
|
+
<%= f.text_field(:amount, :size => 10) %>
|
13
|
+
</td>
|
14
|
+
<td valign="top">
|
15
|
+
<%= error_message_on(f.object, :position) %>
|
16
|
+
<%= f.text_field(:position, :size => 3) %>
|
17
|
+
</td>
|
18
|
+
<td valign="top"><%= f.remove_link icon('delete') + ' ' + t("remove") %></td>
|
19
|
+
</tr>
|
@@ -0,0 +1,28 @@
|
|
1
|
+
<%= render :partial => 'admin/shared/product_sub_menu' %>
|
2
|
+
|
3
|
+
<%= render :partial => 'admin/shared/product_tabs', :locals => {:current => "Volume Pricing"} %>
|
4
|
+
|
5
|
+
<%= error_messages_for :variant %>
|
6
|
+
|
7
|
+
<% form_for(:variant, :url => admin_product_variant_url(@product, @variant), :html => { :method => :put }) do |f| %>
|
8
|
+
|
9
|
+
<h3><%= t("volume_prices") %></h3>
|
10
|
+
<table class="index">
|
11
|
+
<thead>
|
12
|
+
<tr>
|
13
|
+
<th><%= t("display") %></th>
|
14
|
+
<th><%= t("range") %></th>
|
15
|
+
<th><%= t("amount") %></th>
|
16
|
+
<th><%= t("position") %></th>
|
17
|
+
<th><%= t("action") %></th>
|
18
|
+
</tr>
|
19
|
+
</thead>
|
20
|
+
<tbody id="volume_prices">
|
21
|
+
<%= f.render_associated_form(@variant.volume_prices) %>
|
22
|
+
</tbody>
|
23
|
+
</table>
|
24
|
+
<%= f.add_associated_link(icon('add') + ' ' + t("add_volume_price"), @variant.volume_prices.build) %>
|
25
|
+
<br/><br/>
|
26
|
+
|
27
|
+
<%= render :partial => 'admin/shared/edit_resource_links' %>
|
28
|
+
<% end %>
|
@@ -0,0 +1,109 @@
|
|
1
|
+
require "spree_volume_pricing"
|
2
|
+
|
3
|
+
module SpreeVolumePricing
|
4
|
+
|
5
|
+
class Engine < Rails::Engine
|
6
|
+
|
7
|
+
def self.activate
|
8
|
+
|
9
|
+
Variant.class_eval do
|
10
|
+
has_many :volume_prices, :order => :position, :dependent => :destroy
|
11
|
+
accepts_nested_attributes_for :volume_prices
|
12
|
+
|
13
|
+
# calculates the price based on quantity
|
14
|
+
def volume_price(quantity)
|
15
|
+
volume_prices.each do |price|
|
16
|
+
return price.amount if price.include?(quantity)
|
17
|
+
end
|
18
|
+
self.price
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
Order.class_eval do
|
24
|
+
# override the add_variant functionality so that we can adjust the price based on possible volume adjustment
|
25
|
+
def add_variant(variant, quantity=1)
|
26
|
+
current_item = contains?(variant)
|
27
|
+
price = variant.volume_price(quantity) # Added
|
28
|
+
if current_item
|
29
|
+
current_item.increment_quantity unless quantity > 1
|
30
|
+
current_item.quantity = (current_item.quantity + quantity) if quantity > 1
|
31
|
+
current_item.price = price # Added
|
32
|
+
current_item.save
|
33
|
+
else
|
34
|
+
current_item = line_items.create(:quantity => quantity)
|
35
|
+
current_item.variant = variant
|
36
|
+
current_item.price = price
|
37
|
+
current_item.save
|
38
|
+
end
|
39
|
+
|
40
|
+
# populate line_items attributes for additional_fields entries
|
41
|
+
# that have populate => [:line_item]
|
42
|
+
Variant.additional_fields.select{|f| !f[:populate].nil? && f[:populate].include?(:line_item) }.each do |field|
|
43
|
+
value = ""
|
44
|
+
|
45
|
+
if field[:only].nil? || field[:only].include?(:variant)
|
46
|
+
value = variant.send(field[:name].gsub(" ", "_").downcase)
|
47
|
+
elsif field[:only].include?(:product)
|
48
|
+
value = variant.product.send(field[:name].gsub(" ", "_").downcase)
|
49
|
+
end
|
50
|
+
current_item.update_attribute(field[:name].gsub(" ", "_").downcase, value)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
LineItem.class_eval do
|
56
|
+
before_update :check_volume_pricing
|
57
|
+
|
58
|
+
private
|
59
|
+
def check_volume_pricing
|
60
|
+
if changed? && changes.keys.include?("quantity")
|
61
|
+
self.price = variant.volume_price(quantity)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
String.class_eval do
|
67
|
+
def to_range
|
68
|
+
case self.count('.')
|
69
|
+
when 2
|
70
|
+
elements = self.split('..')
|
71
|
+
return Range.new(elements[0].from(1).to_i, elements[1].to_i)
|
72
|
+
when 3
|
73
|
+
elements = self.split('...')
|
74
|
+
return Range.new(elements[0].from(1).to_i, elements[1].to_i-1)
|
75
|
+
else
|
76
|
+
raise ArgumentError.new("Couldn't convert to Range: #{self}")
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
Admin::VariantsController.class_eval do
|
82
|
+
update.before do
|
83
|
+
params[:variant][:volume_price_attributes] ||= {}
|
84
|
+
end
|
85
|
+
|
86
|
+
update.response do |wants|
|
87
|
+
wants.html do
|
88
|
+
redirect_to object.is_master ? volume_prices_admin_product_variant_url(object.product, object) : collection_url
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def object
|
93
|
+
@object ||= Variant.find(params[:id])
|
94
|
+
end
|
95
|
+
|
96
|
+
def volume_prices
|
97
|
+
@variant = object
|
98
|
+
@product = @variant.product
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
end
|
103
|
+
|
104
|
+
config.autoload_paths += %W(#{config.root}/lib)
|
105
|
+
config.to_prepare &method(:activate).to_proc
|
106
|
+
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
@@ -0,0 +1,29 @@
|
|
1
|
+
namespace :db do
|
2
|
+
desc "Bootstrap your database for Spree."
|
3
|
+
task :bootstrap => :environment do
|
4
|
+
# load initial database fixtures (in db/sample/*.yml) into the current environment's database
|
5
|
+
ActiveRecord::Base.establish_connection(RAILS_ENV.to_sym)
|
6
|
+
Dir.glob(File.join(VolumePricingExtension.root, "db", 'sample', '*.{yml,csv}')).each do |fixture_file|
|
7
|
+
Fixtures.create_fixtures("#{VolumePricingExtension.root}/db/sample", File.basename(fixture_file, '.*'))
|
8
|
+
end
|
9
|
+
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
namespace :spree do
|
14
|
+
namespace :extensions do
|
15
|
+
namespace :volume_pricing do
|
16
|
+
desc "Copies public assets of the Volume Pricing to the instance public/ directory."
|
17
|
+
task :update => :environment do
|
18
|
+
is_svn_or_dir = proc {|path| path =~ /\.svn/ || File.directory?(path) }
|
19
|
+
Dir[VolumePricingExtension.root + "/public/**/*"].reject(&is_svn_or_dir).each do |file|
|
20
|
+
path = file.sub(VolumePricingExtension.root, '')
|
21
|
+
directory = File.dirname(path)
|
22
|
+
puts "Copying #{path}..."
|
23
|
+
mkdir_p RAILS_ROOT + directory
|
24
|
+
cp file, RAILS_ROOT + path
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
metadata
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: spree_volume_pricing
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 5
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 3
|
8
|
+
- 0
|
9
|
+
- 1
|
10
|
+
version: 3.0.1
|
11
|
+
platform: ruby
|
12
|
+
authors: []
|
13
|
+
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2010-09-03 00:00:00 -05:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: spree_core
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - "="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
hash: -1848229955
|
30
|
+
segments:
|
31
|
+
- 0
|
32
|
+
- 30
|
33
|
+
- 0
|
34
|
+
- beta1
|
35
|
+
version: 0.30.0.beta1
|
36
|
+
type: :runtime
|
37
|
+
version_requirements: *id001
|
38
|
+
description:
|
39
|
+
email:
|
40
|
+
executables: []
|
41
|
+
|
42
|
+
extensions: []
|
43
|
+
|
44
|
+
extra_rdoc_files: []
|
45
|
+
|
46
|
+
files:
|
47
|
+
- README.markdown
|
48
|
+
- lib/spree_volume_pricing/engine.rb
|
49
|
+
- lib/spree_volume_pricing.rb
|
50
|
+
- lib/spree_volume_pricing_hooks.rb
|
51
|
+
- lib/tasks/volume_pricing_extension_tasks.rake
|
52
|
+
- app/models/volume_price.rb
|
53
|
+
- app/views/admin/shared/_vp_product_tab.html.erb
|
54
|
+
- app/views/admin/variants/_edit_fields.html.erb
|
55
|
+
- app/views/admin/variants/_volume_price.html.erb
|
56
|
+
- app/views/admin/variants/volume_prices.html.erb
|
57
|
+
has_rdoc: true
|
58
|
+
homepage:
|
59
|
+
licenses: []
|
60
|
+
|
61
|
+
post_install_message:
|
62
|
+
rdoc_options: []
|
63
|
+
|
64
|
+
require_paths:
|
65
|
+
- lib
|
66
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
67
|
+
none: false
|
68
|
+
requirements:
|
69
|
+
- - ">="
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
hash: 57
|
72
|
+
segments:
|
73
|
+
- 1
|
74
|
+
- 8
|
75
|
+
- 7
|
76
|
+
version: 1.8.7
|
77
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
78
|
+
none: false
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
hash: 3
|
83
|
+
segments:
|
84
|
+
- 0
|
85
|
+
version: "0"
|
86
|
+
requirements:
|
87
|
+
- none
|
88
|
+
rubyforge_project:
|
89
|
+
rubygems_version: 1.3.7
|
90
|
+
signing_key:
|
91
|
+
specification_version: 3
|
92
|
+
summary: Allow prices to be configured in quantity ranges for each variant
|
93
|
+
test_files: []
|
94
|
+
|