spree_related_products 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
+
|