spree_volume_pricing 3.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,5 @@
1
+ <% if url_options_authenticate?(:controller => 'admin/taxons') %>
2
+ <li <%= ' class="active"' if current == "Volume Pricing" %>>
3
+ <%= link_to("Volume Pricing", volume_prices_admin_product_variant_url(@product, @product.master)) %>
4
+ </li>
5
+ <% 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,5 @@
1
+ require 'spree_core'
2
+ require 'spree_volume_pricing'
3
+ require 'spree_volume_pricing/engine'
4
+
5
+
@@ -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,7 @@
1
+ class VolumePricingHooks < Spree::ThemeSupport::HookListener
2
+
3
+ insert_after :admin_product_tabs, :partial => "admin/shared/vp_product_tab"
4
+
5
+ insert_after :admin_variant_edit_form, :partial => "admin/variants/edit_fields"
6
+
7
+ end
@@ -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
+