spree_related_products 3.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,7 @@
1
+ class Admin::RelationTypesController < Admin::BaseController
2
+ resource_controller
3
+
4
+ update.wants.html { redirect_to collection_url }
5
+ create.wants.html { redirect_to collection_url }
6
+ destroy.success.wants.js { render_js_for_destroy }
7
+ end
@@ -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,2 @@
1
+ module Admin::RelationTypesHelper
2
+ end
@@ -0,0 +1,2 @@
1
+ module Admin::RelationsHelper
2
+ 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,7 @@
1
+ class Relation < ActiveRecord::Base
2
+ belongs_to :relation_type
3
+ belongs_to :relatable, :polymorphic => true
4
+ belongs_to :related_to, :polymorphic => true
5
+
6
+ validates_presence_of :relation_type, :relatable, :related_to
7
+ end
@@ -0,0 +1,3 @@
1
+ class RelationType < ActiveRecord::Base
2
+ has_many :relations
3
+ end
@@ -0,0 +1,5 @@
1
+ <% if url_options_authenticate?(:controller => 'admin/products') %>
2
+ <li<%= ' class="active"' if current == "Related Products" %>>
3
+ <%= link_to t("related_products"), related_admin_product_url(@product) %>
4
+ </li>
5
+ <% end %>
@@ -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 %> &nbsp;
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
+