spree_related_products 3.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.markdown +45 -0
- data/app/controllers/admin/relation_types_controller.rb +7 -0
- data/app/controllers/admin/relations_controller.rb +21 -0
- data/app/helpers/admin/relation_types_helper.rb +2 -0
- data/app/helpers/admin/relations_helper.rb +2 -0
- data/app/models/calculator/related_product_discount.rb +49 -0
- data/app/models/relation.rb +7 -0
- data/app/models/relation_type.rb +3 -0
- data/app/views/admin/products/_related_products.html.erb +5 -0
- data/app/views/admin/products/_related_products_table.html.erb +26 -0
- data/app/views/admin/products/related.html.erb +50 -0
- data/app/views/admin/relation_types/_form.html.erb +22 -0
- data/app/views/admin/relation_types/edit.html.erb +16 -0
- data/app/views/admin/relation_types/index.html.erb +46 -0
- data/app/views/admin/relation_types/new.html.erb +17 -0
- data/lib/spree_related_products.rb +43 -0
- data/lib/spree_related_products_hooks.rb +9 -0
- data/lib/tasks/related_products_extension_tasks.rake +17 -0
- metadata +102 -0
data/README.markdown
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
Related Products
|
2
|
+
================
|
3
|
+
|
4
|
+
This extension provides a generic way for you to define different types of relationships between your products, by defining a RelationType for each type of relationship you'd like to maintain.
|
5
|
+
|
6
|
+
You can manage RelationTypes via the admin configuration menu, and you can maintain product relationships via __Related Products__ tab on the edit product UI.
|
7
|
+
|
8
|
+
Possible uses
|
9
|
+
-------------
|
10
|
+
|
11
|
+
* Accessories
|
12
|
+
|
13
|
+
* Cross Sells
|
14
|
+
|
15
|
+
* Up Sells
|
16
|
+
|
17
|
+
* Compatible Products
|
18
|
+
|
19
|
+
* Replacement Products
|
20
|
+
|
21
|
+
* Warranty & Support Products
|
22
|
+
|
23
|
+
|
24
|
+
|
25
|
+
Relation Types
|
26
|
+
--------------
|
27
|
+
When you create a RelationType you can access that set of related products by referencing the relation_type name, see below for an example:
|
28
|
+
|
29
|
+
rt = RelationType.create(:name => "Accessories", :applies_to => "Product")
|
30
|
+
=> #<RelationType id: 4, name: "Accessories" ...>
|
31
|
+
product = Product.last
|
32
|
+
=> #<Product id: 1060500592 ...>
|
33
|
+
product.accessories
|
34
|
+
=> []
|
35
|
+
|
36
|
+
You can access all related products regardless of RelationType by:
|
37
|
+
|
38
|
+
product.relations
|
39
|
+
=> []
|
40
|
+
|
41
|
+
|
42
|
+
Discounts
|
43
|
+
---------
|
44
|
+
If you install the spree-automatic-coupons extension you can also specify a discount amount to be applied if a customer purchases both products. Note: In order for the coupon to be automatically applied, you must create a coupon of type: __RelatedProductDiscount__ and leave the __code__ value empty (blank codes are required for coupons to be automatically applied).
|
45
|
+
|
@@ -0,0 +1,21 @@
|
|
1
|
+
class Admin::RelationsController < Admin::BaseController
|
2
|
+
resource_controller
|
3
|
+
belongs_to :product
|
4
|
+
|
5
|
+
actions :create, :destroy
|
6
|
+
|
7
|
+
create do
|
8
|
+
flash nil
|
9
|
+
end
|
10
|
+
|
11
|
+
create.before do
|
12
|
+
object.related_to = Variant.find(params[:relation][:related_to_id]).product
|
13
|
+
end
|
14
|
+
|
15
|
+
create.response do |wants|
|
16
|
+
wants.html { render :partial => "admin/products/related_products_table", :locals => {:product => @product}, :layout => false}
|
17
|
+
end
|
18
|
+
|
19
|
+
destroy.success.wants.js { render_js_for_destroy }
|
20
|
+
|
21
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
class Calculator::RelatedProductDiscount < Calculator
|
2
|
+
preference :item_total_threshold, :decimal, :default => 5
|
3
|
+
|
4
|
+
def self.description
|
5
|
+
I18n.t("related_product_discount")
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.register
|
9
|
+
super
|
10
|
+
Coupon.register_calculator(self)
|
11
|
+
end
|
12
|
+
|
13
|
+
def compute(object)
|
14
|
+
if object.is_a?(Array)
|
15
|
+
return if object.empty?
|
16
|
+
order = object.first.order
|
17
|
+
else
|
18
|
+
order = object
|
19
|
+
end
|
20
|
+
|
21
|
+
return unless eligible?(order)
|
22
|
+
total = order.line_items.inject(0) do |total, line_item|
|
23
|
+
relations = Relation.find(:all, :conditions => ["discount_amount <> 0.0 AND relatable_type = ? AND relatable_id = ?", "Product", line_item.variant.product.id])
|
24
|
+
discount_applies_to = relations.map {|rel| rel.related_to.master }
|
25
|
+
|
26
|
+
order.line_items.each do |li|
|
27
|
+
if discount_applies_to.include? li.variant
|
28
|
+
discount = relations.detect {|rel| rel.related_to.variant == li.variant}.discount_amount
|
29
|
+
|
30
|
+
total += if li.quantity < line_item.quantity
|
31
|
+
(discount * li.quantity)
|
32
|
+
else
|
33
|
+
(discount * line_item.quantity)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
total
|
39
|
+
end
|
40
|
+
|
41
|
+
total == 0 ? nil : total
|
42
|
+
end
|
43
|
+
|
44
|
+
def eligible?(order)
|
45
|
+
order.line_items.any? { |line_item| Relation.exists?(["discount_amount <> 0.0 AND relatable_type = ? AND relatable_id = ?", "Product", line_item.variant.product.id])}
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
|
@@ -0,0 +1,26 @@
|
|
1
|
+
<table class="index">
|
2
|
+
<thead>
|
3
|
+
<tr>
|
4
|
+
<th><%= t('name') %></th>
|
5
|
+
<% if Spree::Extension.descendants.map(&:to_s).include? "AutomaticCouponsExtension" %>
|
6
|
+
<th><%= t('discount_amount') %></th>
|
7
|
+
<% end %>
|
8
|
+
<th><%= t('type') %></th>
|
9
|
+
<th></th>
|
10
|
+
</tr>
|
11
|
+
</thead>
|
12
|
+
<tbody>
|
13
|
+
<% product.relations.each do |relation| %>
|
14
|
+
<tr id="<%= dom_id relation %>">
|
15
|
+
<td><%= relation.related_to.name %></td>
|
16
|
+
<% if Spree::Extension.descendants.map(&:to_s).include? "AutomaticCouponsExtension" %>
|
17
|
+
<td><%= relation.discount_amount != 0 ? number_to_currency(relation.discount_amount) : "-" %></td>
|
18
|
+
<% end %>
|
19
|
+
<td><%= relation.relation_type.name %></td>
|
20
|
+
<td width="70px">
|
21
|
+
<%= link_to_delete relation, {:url => admin_product_relation_url(relation.relatable, relation)} %>
|
22
|
+
</td>
|
23
|
+
</tr>
|
24
|
+
<% end %>
|
25
|
+
</tbody>
|
26
|
+
</table>
|
@@ -0,0 +1,50 @@
|
|
1
|
+
<%= render :partial => 'admin/shared/product_sub_menu' %>
|
2
|
+
|
3
|
+
<%= render :partial => 'admin/shared/product_tabs', :locals => {:current => "Related Products"} %>
|
4
|
+
|
5
|
+
|
6
|
+
<% if @relation_types.empty? %>
|
7
|
+
<%= t("no_relation_types") %>
|
8
|
+
<% else %>
|
9
|
+
<div id="add-line-item">
|
10
|
+
<fieldset>
|
11
|
+
<legend><%= t('add_related_product') %></legend>
|
12
|
+
<div style="float:left;width:<%= Spree::Extension.descendants.map(&:to_s).include?("AutomaticCouponsExtension") ? "40" : "55" %>%;margin-right:5%;">
|
13
|
+
<%= label_tag :add_product_name, t("name_or_sku") %>
|
14
|
+
<%= text_field_tag :add_product_name, {}, :class => 'fullwidth title' %>
|
15
|
+
<%= hidden_field_tag :add_variant_id %>
|
16
|
+
</div>
|
17
|
+
<% if Spree::Extension.descendants.map(&:to_s).include? "AutomaticCouponsExtension" %>
|
18
|
+
<div style="float:left;width:10%;margin-right:5%;">
|
19
|
+
<%= label_tag :add_discount, t("discount_amount") %>
|
20
|
+
<%= text_field_tag :add_discount, 0.0, :style => 'margin: 0pt; padding: 4px; font-size: 1.5em; width: 98%;' %>
|
21
|
+
</div>
|
22
|
+
<% end %>
|
23
|
+
<div style="float:left;width:20%;margin-right:5%;">
|
24
|
+
<%= label_tag :add_type, t("type") %>
|
25
|
+
<%= select_tag :add_type, options_for_select(@relation_types.map{|rt| [rt.name, rt.id]}), :style => 'margin: 0pt; padding: 4px; font-size: 1.5em; width: 98%;' %>
|
26
|
+
</div>
|
27
|
+
<div style="float: left; width: 15%; padding-top: 17px;">
|
28
|
+
<%= button_link_to_remote t("add"),
|
29
|
+
{:url => admin_product_relations_url(@product),
|
30
|
+
:method => :post,
|
31
|
+
:with => "'relation[related_to_type]=Product&relation[related_to_id]=' + $('#add_variant_id').val() + '&relation[relation_type_id]=' + $('#add_type').val() + '&relation[discount_amount]=' + $('#add_discount').val()",
|
32
|
+
:before => "if($('#add_variant_id').val()==''){ return false; }",
|
33
|
+
:after => "$('#add_product_name').val('');$('#add_variant_id').val('');$('#add_quantity').val(1)",
|
34
|
+
:update => "products-table-wrapper"}, :icon => 'add' %>
|
35
|
+
</div>
|
36
|
+
|
37
|
+
</fieldset>
|
38
|
+
</div>
|
39
|
+
|
40
|
+
<div id="products-table-wrapper">
|
41
|
+
<%= render :partial => "related_products_table", :locals => {:product => @product} %>
|
42
|
+
</div>
|
43
|
+
|
44
|
+
<% content_for :head do %>
|
45
|
+
<%= javascript_tag "var expand_variants = false;" %>
|
46
|
+
<%= javascript_include_tag 'admin/orders/edit.js' %>
|
47
|
+
<%= stylesheet_link_tag 'admin/edit_orders.css' %>
|
48
|
+
<% end %>
|
49
|
+
|
50
|
+
<% end %>
|
@@ -0,0 +1,22 @@
|
|
1
|
+
<%- locals = {:f => f} -%>
|
2
|
+
<% hook :admin_relation_type_form_fields, locals do %>
|
3
|
+
<% f.field_container :name do %>
|
4
|
+
<%= f.label :name, t("name") %><br />
|
5
|
+
<%= f.text_field :name, :class => 'fullwidth title' %>
|
6
|
+
<%= error_message_on :relation_type, :name %>
|
7
|
+
<% end %>
|
8
|
+
|
9
|
+
<% f.field_container :applies_to do %>
|
10
|
+
<%= f.label :applies_to, t("applies_to") %><br />
|
11
|
+
<%= f.text_field :applies_to, :value => f.object.nil? ? "Product" : f.object.applies_to, :class => 'fullwidth title' %>
|
12
|
+
<%= error_message_on :relation_type, :applies_to%>
|
13
|
+
<% end %>
|
14
|
+
|
15
|
+
<% f.field_container :description do %>
|
16
|
+
<%= f.label :description, t("description") %><br />
|
17
|
+
<%= f.text_area :description, {:cols => 60, :rows => 2, :class => 'fullwidth'} %>
|
18
|
+
<%= error_message_on :relation_type, :description %>
|
19
|
+
<% end %>
|
20
|
+
|
21
|
+
<% end %>
|
22
|
+
|
@@ -0,0 +1,16 @@
|
|
1
|
+
<%= render :partial => 'admin/shared/configuration_menu' %>
|
2
|
+
|
3
|
+
<% hook :admin_relation_type_edit_form_header do %>
|
4
|
+
<h1><%= t("editing_relation_type") %></h1>
|
5
|
+
<%= error_messages_for :relation_type %>
|
6
|
+
<% end %>
|
7
|
+
|
8
|
+
<% hook :admin_relation_type_edit_form do %>
|
9
|
+
<% form_for(@relation_type, :url => object_url, :html => { :method => :put }) do |f| %>
|
10
|
+
<%= render :partial => "form", :locals => { :f => f } %>
|
11
|
+
|
12
|
+
<% hook :admin_relation_type_edit_form_buttons do %>
|
13
|
+
<%= render :partial => "admin/shared/edit_resource_links" %>
|
14
|
+
<% end %>
|
15
|
+
<% end %>
|
16
|
+
<% end %>
|
@@ -0,0 +1,46 @@
|
|
1
|
+
<%= render :partial => 'admin/shared/configuration_menu' %>
|
2
|
+
|
3
|
+
<div class='toolbar'>
|
4
|
+
<ul class='actions'>
|
5
|
+
<li>
|
6
|
+
<%= button_link_to t("new_relation_type"), new_object_url, :icon => 'add' %>
|
7
|
+
</li>
|
8
|
+
</ul>
|
9
|
+
<br class='clear' />
|
10
|
+
</div>
|
11
|
+
|
12
|
+
<h1><%= t("relation_types") %></h1>
|
13
|
+
|
14
|
+
<table class="index">
|
15
|
+
<thead>
|
16
|
+
<tr>
|
17
|
+
<% hook :admin_relation_types_index_headers do %>
|
18
|
+
<th><%= t("name") %></th>
|
19
|
+
<th><%= t("applies_to") %></th>
|
20
|
+
<th><%= t("description") %></th>
|
21
|
+
<% end %>
|
22
|
+
<th>
|
23
|
+
<% hook :admin_relation_types_index_header_actions %>
|
24
|
+
</th>
|
25
|
+
</tr>
|
26
|
+
</thead>
|
27
|
+
<tbody>
|
28
|
+
<% @relation_types.each do |relation_type| %>
|
29
|
+
<tr id="<%= dom_id relation_type %>">
|
30
|
+
<%- locals = {:relation_type => relation_type} -%>
|
31
|
+
<% hook :admin_relation_types_index_rows, locals do %>
|
32
|
+
<td><%= relation_type.name %></td>
|
33
|
+
<td><%= relation_type.applies_to %></td>
|
34
|
+
<td><%= relation_type.description %></td>
|
35
|
+
<% end %>
|
36
|
+
<td width="140px">
|
37
|
+
<% hook :admin_relation_types_index_row_actions, locals do %>
|
38
|
+
<%= link_to_edit relation_type %>
|
39
|
+
<%= link_to_delete relation_type %>
|
40
|
+
<% end %>
|
41
|
+
</td>
|
42
|
+
</tr>
|
43
|
+
<% end %>
|
44
|
+
</tbody>
|
45
|
+
</table>
|
46
|
+
|
@@ -0,0 +1,17 @@
|
|
1
|
+
<%= render :partial => 'admin/shared/configuration_menu' %>
|
2
|
+
|
3
|
+
<% hook :admin_relation_type_new_form_header do %>
|
4
|
+
<h1><%= t("new_relation_type") %></h1>
|
5
|
+
|
6
|
+
<%= error_messages_for :relation_type %>
|
7
|
+
<% end %>
|
8
|
+
|
9
|
+
<% hook :admin_relation_type_new_form do %>
|
10
|
+
<% form_for(:relation_type, :url => collection_url) do |f| %>
|
11
|
+
<%= render :partial => "form", :locals => { :f => f } %>
|
12
|
+
|
13
|
+
<% hook :admin_relation_type_new_form_buttons do %>
|
14
|
+
<%= render :partial => "admin/shared/new_resource_links" %>
|
15
|
+
<% end %>
|
16
|
+
<% end %>
|
17
|
+
<% end %>
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'spree_core'
|
2
|
+
|
3
|
+
module SpreeRelatedProducts
|
4
|
+
class Engine < Rails::Engine
|
5
|
+
|
6
|
+
def self.activate
|
7
|
+
|
8
|
+
# Calculator::RelatedProductDiscount.register
|
9
|
+
|
10
|
+
Product.class_eval do
|
11
|
+
has_many :relations, :as => :relatable
|
12
|
+
|
13
|
+
def self.relation_types
|
14
|
+
RelationType.find_all_by_applies_to(self.to_s, :order => :name)
|
15
|
+
end
|
16
|
+
|
17
|
+
def method_missing(method, *args)
|
18
|
+
relation_type = self.class.relation_types.detect { |rt| rt.name.downcase.gsub(" ", "_").pluralize == method.to_s.downcase }
|
19
|
+
|
20
|
+
if relation_type.nil?
|
21
|
+
super
|
22
|
+
else
|
23
|
+
relations.find_all_by_relation_type_id(relation_type.id).map(&:related_to).select {|product| product.deleted_at.nil? && product.available_on <= Time.now()}
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
Admin::ProductsController.class_eval do
|
30
|
+
def related
|
31
|
+
load_object
|
32
|
+
@relation_types = Product.relation_types
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
config.autoload_paths += %W(#{config.root}/lib #{config.root}/app/models/calculator)
|
39
|
+
config.to_prepare &method(:activate).to_proc
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
@@ -0,0 +1,9 @@
|
|
1
|
+
class SpreeRelatedProductsHooks < Spree::ThemeSupport::HookListener
|
2
|
+
|
3
|
+
insert_after :admin_product_tabs, "admin/products/related_products"
|
4
|
+
|
5
|
+
insert_after :admin_configurations_menu do
|
6
|
+
"<%= configurations_menu_item(I18n.t('relation_types'), admin_relation_types_url, I18n.t('manage_relation_types')) %>"
|
7
|
+
end
|
8
|
+
|
9
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
namespace :spree do
|
2
|
+
namespace :extensions do
|
3
|
+
namespace :related_products do
|
4
|
+
desc "Copies public assets of the Related Products to the instance public/ directory."
|
5
|
+
task :update => :environment do
|
6
|
+
is_svn_git_or_dir = proc {|path| path =~ /\.svn/ || path =~ /\.git/ || File.directory?(path) }
|
7
|
+
Dir[RelationsExtension.root + "/public/**/*"].reject(&is_svn_git_or_dir).each do |file|
|
8
|
+
path = file.sub(RelationsExtension.root, '')
|
9
|
+
directory = File.dirname(path)
|
10
|
+
puts "Copying #{path}..."
|
11
|
+
mkdir_p RAILS_ROOT + directory
|
12
|
+
cp file, RAILS_ROOT + path
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
metadata
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: spree_related_products
|
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_related_products.rb
|
49
|
+
- lib/spree_related_products_hooks.rb
|
50
|
+
- lib/tasks/related_products_extension_tasks.rake
|
51
|
+
- app/controllers/admin/relation_types_controller.rb
|
52
|
+
- app/controllers/admin/relations_controller.rb
|
53
|
+
- app/helpers/admin/relation_types_helper.rb
|
54
|
+
- app/helpers/admin/relations_helper.rb
|
55
|
+
- app/models/calculator/related_product_discount.rb
|
56
|
+
- app/models/relation.rb
|
57
|
+
- app/models/relation_type.rb
|
58
|
+
- app/views/admin/products/_related_products.html.erb
|
59
|
+
- app/views/admin/products/_related_products_table.html.erb
|
60
|
+
- app/views/admin/products/related.html.erb
|
61
|
+
- app/views/admin/relation_types/_form.html.erb
|
62
|
+
- app/views/admin/relation_types/edit.html.erb
|
63
|
+
- app/views/admin/relation_types/index.html.erb
|
64
|
+
- app/views/admin/relation_types/new.html.erb
|
65
|
+
has_rdoc: true
|
66
|
+
homepage:
|
67
|
+
licenses: []
|
68
|
+
|
69
|
+
post_install_message:
|
70
|
+
rdoc_options: []
|
71
|
+
|
72
|
+
require_paths:
|
73
|
+
- lib
|
74
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
75
|
+
none: false
|
76
|
+
requirements:
|
77
|
+
- - ">="
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
hash: 57
|
80
|
+
segments:
|
81
|
+
- 1
|
82
|
+
- 8
|
83
|
+
- 7
|
84
|
+
version: 1.8.7
|
85
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
86
|
+
none: false
|
87
|
+
requirements:
|
88
|
+
- - ">="
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
hash: 3
|
91
|
+
segments:
|
92
|
+
- 0
|
93
|
+
version: "0"
|
94
|
+
requirements:
|
95
|
+
- none
|
96
|
+
rubyforge_project:
|
97
|
+
rubygems_version: 1.3.7
|
98
|
+
signing_key:
|
99
|
+
specification_version: 3
|
100
|
+
summary: Allows multiple types of relationships between products to be defined
|
101
|
+
test_files: []
|
102
|
+
|