Commit 2b56f641a244024b17b41c3c2b7d93c37a960145

Authored by Braulio Bhavamitra
1 parent 74e81649

products: fix dependent plugins

plugins/metadata/install.rb 0 → 100644
@@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
  1 +if ENV['CI']
  2 + system 'script/noosfero-plugins -q enable products'
  3 + exit $?.exitstatus
  4 +end
  5 +
plugins/metadata/lib/ext/product.rb
@@ -1,34 +0,0 @@ @@ -1,34 +0,0 @@
1 -require_dependency 'product'  
2 -  
3 -class Product  
4 -  
5 - metadata_spec namespace: :og, tags: {  
6 - type: proc{ |p, plugin| plugin.context.params[:og_type] || MetadataPlugin.og_types[:product] || :product },  
7 - url: proc do |p, plugin|  
8 - url = p.url.merge! profile: p.profile.identifier, og_type: plugin.context.params[:og_type]  
9 - plugin.og_url_for url  
10 - end,  
11 - gr_hascurrencyvalue: proc{ |p, plugin| p.price.to_f },  
12 - gr_hascurrency: proc{ |p, plugin| p.environment.currency_unit },  
13 - title: proc{ |p, plugin| "#{p.name} - #{p.profile.name}" if p },  
14 - description: proc{ |p, plugin| ActionView::Base.full_sanitizer.sanitize p.description },  
15 -  
16 - image: proc do |p, plugin|  
17 - img = "#{p.environment.top_url}#{p.image.public_filename}".html_safe if p.image  
18 - img = "#{p.environment.top_url}#{p.profile.image.public_filename}".html_safe if img.blank? and p.profile.image  
19 - img ||= MetadataPlugin.config[:open_graph][:environment_logo] rescue nil if img.blank?  
20 - img  
21 - end,  
22 - 'image:type' => proc{ |p, plugin| p.image.content_type if p.image },  
23 - 'image:height' => proc{ |p, plugin| p.image.height if p.image },  
24 - 'image:width' => proc{ |p, plugin| p.image.width if p.image },  
25 -  
26 - see_also: [],  
27 - site_name: proc{ |p, plugin| plugin.og_url_for p.profile.url },  
28 - updated_time: proc{ |p, plugin| p.updated_at.iso8601 if p.updated_at },  
29 -  
30 - 'locale:locale' => proc{ |p, plugin| p.environment.default_language },  
31 - 'locale:alternate' => proc{ |p, plugin| p.environment.languages - [p.environment.default_language] if p.environment.languages },  
32 - }  
33 -  
34 -end  
plugins/metadata/lib/ext/products_plugin/product.rb 0 → 100644
@@ -0,0 +1,38 @@ @@ -0,0 +1,38 @@
  1 +if defined? ProductsPlugin
  2 + require_dependency 'products_plugin/product'
  3 +
  4 + module ProductsPlugin
  5 + class Product
  6 +
  7 + metadata_spec namespace: :og, tags: {
  8 + type: proc{ |p, plugin| plugin.context.params[:og_type] || MetadataPlugin.og_types[:product] || :product },
  9 + url: proc do |p, plugin|
  10 + url = p.url.merge! profile: p.profile.identifier, og_type: plugin.context.params[:og_type]
  11 + plugin.og_url_for url
  12 + end,
  13 + gr_hascurrencyvalue: proc{ |p, plugin| p.price.to_f },
  14 + gr_hascurrency: proc{ |p, plugin| p.environment.currency_unit },
  15 + title: proc{ |p, plugin| "#{p.name} - #{p.profile.name}" if p },
  16 + description: proc{ |p, plugin| ActionView::Base.full_sanitizer.sanitize p.description },
  17 +
  18 + image: proc do |p, plugin|
  19 + img = "#{p.environment.top_url}#{p.image.public_filename}".html_safe if p.image
  20 + img = "#{p.environment.top_url}#{p.profile.image.public_filename}".html_safe if img.blank? and p.profile.image
  21 + img ||= MetadataPlugin.config[:open_graph][:environment_logo] rescue nil if img.blank?
  22 + img
  23 + end,
  24 + 'image:type' => proc{ |p, plugin| p.image.content_type if p.image },
  25 + 'image:height' => proc{ |p, plugin| p.image.height if p.image },
  26 + 'image:width' => proc{ |p, plugin| p.image.width if p.image },
  27 +
  28 + see_also: [],
  29 + site_name: proc{ |p, plugin| plugin.og_url_for p.profile.url },
  30 + updated_time: proc{ |p, plugin| p.updated_at.iso8601 if p.updated_at },
  31 +
  32 + 'locale:locale' => proc{ |p, plugin| p.environment.default_language },
  33 + 'locale:alternate' => proc{ |p, plugin| p.environment.languages - [p.environment.default_language] if p.environment.languages },
  34 + }
  35 +
  36 + end
  37 + end
  38 +end
plugins/metadata/test/functional/manage_products_controller_test.rb
@@ -1,24 +0,0 @@ @@ -1,24 +0,0 @@
1 -require 'test_helper'  
2 -require 'home_controller'  
3 -  
4 -class ManageProductsControllerTest < ActionController::TestCase  
5 -  
6 - def setup  
7 - @controller = ManageProductsController.new  
8 - @request = ActionController::TestRequest.new  
9 - @response = ActionController::TestResponse.new  
10 - @enterprise = fast_create(Enterprise, name: 'test', identifier: 'test_ent')  
11 - @user = create_user_with_permission('test_user', 'manage_products', @enterprise)  
12 - @environment = @enterprise.environment  
13 - @environment.enable('products_for_enterprises')  
14 - login_as :test_user  
15 -  
16 - @environment.enabled_plugins += ['MetadataPlugin']  
17 - @environment.save!  
18 - end  
19 -  
20 - should "not crash on new products" do  
21 - get :new, profile: @enterprise.identifier  
22 - end  
23 -  
24 -end  
plugins/metadata/test/functional/products_plugin/page_controller_test.rb 0 → 100644
@@ -0,0 +1,24 @@ @@ -0,0 +1,24 @@
  1 +require 'test_helper'
  2 +
  3 +if defined? ProductsPlugin
  4 + module ProductsPlugin
  5 + class ManageProductsControllerTest < ActionController::TestCase
  6 +
  7 + def setup
  8 + @controller = PageController.new
  9 + @request = ActionController::TestRequest.new
  10 + @response = ActionController::TestResponse.new
  11 + @enterprise = fast_create(Enterprise, name: 'test', identifier: 'test_ent')
  12 + @user = create_user_with_permission('test_user', 'manage_products', @enterprise)
  13 + login_as :test_user
  14 +
  15 + @enterprise.environment.enable_plugin 'MetadataPlugin'
  16 + end
  17 +
  18 + should "not crash on new products" do
  19 + get :new, profile: @enterprise.identifier
  20 + end
  21 +
  22 + end
  23 + end
  24 +end
plugins/orders/install.rb
1 -system "script/noosfero-plugins -q enable delivery" 1 +system 'script/noosfero-plugins -q enable products delivery'
2 exit $?.exitstatus 2 exit $?.exitstatus
3 3
plugins/orders/lib/ext/product.rb
@@ -1,16 +0,0 @@ @@ -1,16 +0,0 @@
1 -require_dependency 'product'  
2 -  
3 -class Product  
4 -  
5 - has_many :items, class_name: 'OrdersPlugin::Item', foreign_key: :product_id, dependent: :destroy  
6 -  
7 - has_many :orders, through: :items  
8 - has_many :sales, through: :items  
9 - has_many :purchases, through: :items  
10 -  
11 - attr_accessor :quantity_ordered  
12 -  
13 - extend CurrencyHelper::ClassMethods  
14 - instance_exec &OrdersPlugin::Item::DefineTotals  
15 -  
16 -end  
plugins/orders/lib/ext/products_plugin/product.rb 0 → 100644
@@ -0,0 +1,18 @@ @@ -0,0 +1,18 @@
  1 +require_dependency 'products_plugin/product'
  2 +
  3 +module ProductsPlugin
  4 + class Product
  5 +
  6 + has_many :items, class_name: 'OrdersPlugin::Item', foreign_key: :product_id, dependent: :destroy
  7 +
  8 + has_many :orders, through: :items
  9 + has_many :sales, through: :items
  10 + has_many :purchases, through: :items
  11 +
  12 + attr_accessor :quantity_ordered
  13 +
  14 + extend CurrencyHelper::ClassMethods
  15 + instance_exec &OrdersPlugin::Item::DefineTotals
  16 +
  17 + end
  18 +end
plugins/orders/models/orders_plugin/item.rb
@@ -39,7 +39,7 @@ class OrdersPlugin::Item &lt; ApplicationRecord @@ -39,7 +39,7 @@ class OrdersPlugin::Item &lt; ApplicationRecord
39 belongs_to :sale, class_name: '::OrdersPlugin::Sale', foreign_key: :order_id, touch: true 39 belongs_to :sale, class_name: '::OrdersPlugin::Sale', foreign_key: :order_id, touch: true
40 belongs_to :purchase, class_name: '::OrdersPlugin::Purchase', foreign_key: :order_id, touch: true 40 belongs_to :purchase, class_name: '::OrdersPlugin::Purchase', foreign_key: :order_id, touch: true
41 41
42 - belongs_to :product 42 + belongs_to :product, class_name: '::ProductsPlugin::Product'
43 has_one :supplier, through: :product 43 has_one :supplier, through: :product
44 44
45 has_one :profile, through: :order 45 has_one :profile, through: :order
plugins/orders_cycle/lib/ext/product.rb
@@ -1,12 +0,0 @@ @@ -1,12 +0,0 @@
1 -require_dependency 'product'  
2 -  
3 -# based on orders/lib/ext/product.rb  
4 -class Product  
5 -  
6 - has_many :orders_cycles_items, class_name: 'OrdersCyclePlugin::Item', foreign_key: :product_id  
7 -  
8 - has_many :orders_cycles_orders, through: :orders_cycles_items, source: :order  
9 - has_many :orders_cycles_sales, through: :orders_cycles_items, source: :sale  
10 - has_many :orders_cycles_purchases, through: :orders_cycles_items, source: :purchase  
11 -  
12 -end  
plugins/orders_cycle/lib/ext/products_plugin/product.rb 0 → 100644
@@ -0,0 +1,14 @@ @@ -0,0 +1,14 @@
  1 +require_dependency 'products_plugin/product'
  2 +
  3 +# based on orders/lib/ext/product.rb
  4 +module ProductsPlugin
  5 + class Product
  6 +
  7 + has_many :orders_cycles_items, class_name: 'OrdersCyclePlugin::Item', foreign_key: :product_id
  8 +
  9 + has_many :orders_cycles_orders, through: :orders_cycles_items, source: :order
  10 + has_many :orders_cycles_sales, through: :orders_cycles_items, source: :sale
  11 + has_many :orders_cycles_purchases, through: :orders_cycles_items, source: :purchase
  12 +
  13 + end
  14 +end
plugins/pg_search/db/migrate/20130320010063_create_indexes_for_search.rb
1 class CreateIndexesForSearch < ActiveRecord::Migration 1 class CreateIndexesForSearch < ActiveRecord::Migration
2 - SEARCHABLES = %w[ article comment qualifier national_region certifier profile license scrap category ] 2 + SEARCHABLES = %w[ article comment national_region profile license scrap category ]
3 KLASSES = SEARCHABLES.map {|searchable| searchable.camelize.constantize } 3 KLASSES = SEARCHABLES.map {|searchable| searchable.camelize.constantize }
4 def self.up 4 def self.up
5 KLASSES.each do |klass| 5 KLASSES.each do |klass|
plugins/shopping_cart/views/shopping_cart_plugin/send_request.js.erb
1 <% if @success %> 1 <% if @success %>
2 - window.location.href = <%= url_for(controller: :catalog, profile: @profile.identifier).to_json %> 2 + window.location.href = <%= url_for(controller: 'products_plugin/catalog', profile: @profile.identifier).to_json %>
3 <% else %> 3 <% else %>
4 display_notice(<%= "#{_ 'Error'}: #{@error}".to_json %>) 4 display_notice(<%= "#{_ 'Error'}: #{@error}".to_json %>)
5 <% end %> 5 <% end %>
plugins/sniffer/install.rb 0 → 100644
@@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
  1 +system 'script/noosfero-plugins -q enable products'
  2 +exit $?.exitstatus
  3 +
plugins/statistics/install.rb 0 → 100644
@@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
  1 +if ENV['CI']
  2 + system 'script/noosfero-plugins -q enable products'
  3 + exit $?.exitstatus
  4 +end
  5 +
plugins/suppliers/install.rb 0 → 100644
@@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
  1 +system 'script/noosfero-plugins -q enable products'
  2 +exit $?.exitstatus
  3 +
plugins/suppliers/lib/ext/price_detail.rb
@@ -1,8 +0,0 @@ @@ -1,8 +0,0 @@
1 -require_dependency 'price_detail'  
2 -  
3 -class PriceDetail  
4 -  
5 - # should be on core, used by SuppliersPlugin::Import  
6 - attr_accessible :production_cost  
7 -  
8 -end  
plugins/suppliers/lib/ext/product.rb
@@ -1,178 +0,0 @@ @@ -1,178 +0,0 @@
1 -require_dependency 'product'  
2 -  
3 -# FIXME: The lines bellow should be on the core  
4 -class Product  
5 -  
6 - extend CurrencyHelper::ClassMethods  
7 - has_currency :price  
8 - has_currency :discount  
9 -  
10 - scope :alphabetically, -> { order 'products.name ASC' }  
11 -  
12 - scope :available, -> { where available: true }  
13 - scope :unavailable, -> { where 'products.available <> true' }  
14 - scope :archived, -> { where archived: true }  
15 - scope :unarchived, -> { where 'products.archived <> true' }  
16 -  
17 - scope :with_available, -> (available) { where available: available }  
18 - scope :with_price, -> { where 'products.price > 0' }  
19 -  
20 - # FIXME: transliterate input and name column  
21 - scope :name_like, -> (name) { where "name ILIKE ?", "%#{name}%" }  
22 - scope :with_product_category_id, -> (id) { where product_category_id: id }  
23 -  
24 - scope :by_profile, -> (profile) { where profile_id: profile.id }  
25 - scope :by_profile_id, -> (profile_id) { where profile_id: profile_id }  
26 -  
27 - def self.product_categories_of products  
28 - ProductCategory.find products.collect(&:product_category_id).compact.select{ |id| not id.zero? }  
29 - end  
30 -  
31 - attr_accessible :external_id  
32 - settings_items :external_id, type: String, default: nil  
33 -  
34 - # should be on core, used by SuppliersPlugin::Import  
35 - attr_accessible :price_details  
36 -  
37 -end  
38 -  
39 -class Product  
40 -  
41 - attr_accessible :from_products, :from_product, :supplier_id, :supplier  
42 -  
43 - has_many :sources_from_products, foreign_key: :to_product_id, class_name: 'SuppliersPlugin::SourceProduct', dependent: :destroy  
44 - has_one :sources_from_product, foreign_key: :to_product_id, class_name: 'SuppliersPlugin::SourceProduct'  
45 - has_many :sources_to_products, foreign_key: :from_product_id, class_name: 'SuppliersPlugin::SourceProduct', dependent: :destroy  
46 - has_one :sources_to_product, foreign_key: :from_product_id, class_name: 'SuppliersPlugin::SourceProduct'  
47 - has_many :to_products, -> { order 'id ASC' }, through: :sources_to_products  
48 - has_one :to_product, -> { order 'id ASC' }, through: :sources_to_product, autosave: true  
49 - has_many :from_products, -> { order 'id ASC' }, through: :sources_from_products  
50 - has_one :from_product, -> { order 'id ASC' }, through: :sources_from_product, autosave: true  
51 -  
52 - has_many :sources_from_2x_products, through: :from_products, source: :sources_from_products  
53 - has_one :sources_from_2x_product, through: :from_product, source: :sources_from_product  
54 - has_many :sources_to_2x_products, through: :to_products, source: :sources_to_products  
55 - has_one :sources_to_2x_product, through: :to_product, source: :sources_to_product  
56 - has_many :from_2x_products, through: :sources_from_2x_products, source: :from_product  
57 - has_one :from_2x_product, through: :sources_from_2x_product, source: :from_product  
58 - has_many :to_2x_products, through: :sources_to_2x_products, source: :to_product  
59 - has_one :to_2x_product, through: :sources_to_2x_product, source: :to_product  
60 -  
61 - # semantic alias for supplier_from_product(s)  
62 - has_many :sources_supplier_products, foreign_key: :to_product_id, class_name: 'SuppliersPlugin::SourceProduct'  
63 - has_one :sources_supplier_product, foreign_key: :to_product_id, class_name: 'SuppliersPlugin::SourceProduct'  
64 - has_many :supplier_products, -> { order 'id ASC' }, through: :sources_supplier_products, source: :from_product  
65 - has_one :supplier_product, -> { order 'id ASC' }, through: :sources_supplier_product, source: :from_product, autosave: true  
66 - has_many :suppliers, -> { distinct.order 'id ASC' }, through: :sources_supplier_products  
67 - has_one :supplier, -> { order 'id ASC' }, through: :sources_supplier_product  
68 -  
69 - has_many :consumers, -> { distinct.order 'id ASC' }, through: :to_products, source: :profile  
70 - has_one :consumer, -> { order 'id ASC' }, through: :to_product, source: :profile  
71 -  
72 - # overhide original, FIXME: rename to available_and_supplier_active  
73 - scope :available, -> {  
74 - joins(:suppliers).  
75 - where 'products.available = ? AND suppliers_plugin_suppliers.active = ?', true, true  
76 - }  
77 - scope :unavailable, -> {  
78 - where 'products.available <> ? OR suppliers_plugin_suppliers.active <> ?', true, true  
79 - }  
80 - scope :with_available, -> (available) {  
81 - op = if available then '=' else '<>' end  
82 - cond = if available then 'AND' else 'OR' end  
83 - where "products.available #{op} ? #{cond} suppliers_plugin_suppliers.active #{op} ?", true, true  
84 - }  
85 -  
86 - scope :fp_name_like, -> (name) { where "from_products_products.name ILIKE ?", "%#{name}%" }  
87 - scope :fp_with_product_category_id, -> (id) { where 'from_products_products.product_category_id = ?', id }  
88 -  
89 - # prefer distributed_products has_many to use DistributedProduct scopes and eager loading  
90 - scope :distributed, -> { where type: 'SuppliersPlugin::DistributedProduct'}  
91 - scope :own, -> { where type: nil }  
92 - scope :supplied, -> {  
93 - # this remove duplicates and allow sorting on the fields, unlike distinct  
94 - group('products.id').  
95 - where type: [nil, 'SuppliersPlugin::DistributedProduct']  
96 - }  
97 - scope :supplied_for_count, -> {  
98 - distinct.  
99 - where type: [nil, 'SuppliersPlugin::DistributedProduct']  
100 - }  
101 -  
102 - scope :from_supplier, -> (supplier) { joins(:suppliers).where 'suppliers_plugin_suppliers.id = ?', supplier.id }  
103 - scope :from_supplier_id, -> (supplier_id) { joins(:suppliers).where 'suppliers_plugin_suppliers.id = ?', supplier_id }  
104 -  
105 - after_create :distribute_to_consumers  
106 -  
107 - def own?  
108 - self.class == Product  
109 - end  
110 - def distributed?  
111 - self.class == SuppliersPlugin::DistributedProduct  
112 - end  
113 - def supplied?  
114 - self.own? or self.distributed?  
115 - end  
116 -  
117 - def supplier  
118 - # FIXME: use self.suppliers when rails support for nested preload comes  
119 - @supplier ||= self.sources_supplier_product.supplier rescue nil  
120 - @supplier ||= self.profile.self_supplier rescue nil  
121 - end  
122 - def supplier= value  
123 - @supplier = value  
124 - end  
125 - def supplier_id  
126 - self.supplier.id  
127 - end  
128 - def supplier_id= id  
129 - @supplier = profile.environment.profiles.find id  
130 - end  
131 -  
132 - def supplier_dummy?  
133 - self.supplier ? self.supplier.dummy? : self.profile.dummy?  
134 - end  
135 -  
136 - def distribute_to_consumer consumer, attrs = {}  
137 - distributed_product = consumer.distributed_products.where(profile_id: consumer.id, from_products_products: {id: self.id}).first  
138 - distributed_product ||= SuppliersPlugin::DistributedProduct.create! profile: consumer, from_product: self  
139 - distributed_product.update! attrs if attrs.present?  
140 - distributed_product  
141 - end  
142 -  
143 - def destroy_dependent  
144 - self.to_products.each do |to_product|  
145 - to_product.destroy if to_product.respond_to? :dependent? and to_product.dependent?  
146 - end  
147 - end  
148 -  
149 - # before_destroy and after_destroy don't work,  
150 - # see http://stackoverflow.com/questions/14175330/associations-not-loaded-in-before-destroy-callback  
151 - def destroy  
152 - self.class.transaction do  
153 - self.destroy_dependent  
154 - super  
155 - end  
156 - end  
157 -  
158 - def diff from = self.from_product  
159 - return @changed_attrs if @changed_attrs  
160 - @changed_attrs = []  
161 - SuppliersPlugin::BaseProduct::CORE_DEFAULT_ATTRIBUTES.each do |attr|  
162 - @changed_attrs << attr if self[attr].present? and self[attr] != from[attr]  
163 - end  
164 - @changed_attrs  
165 - end  
166 -  
167 - protected  
168 -  
169 - def distribute_to_consumers  
170 - # shopping_cart creates products without a profile...  
171 - return unless self.profile  
172 -  
173 - self.profile.consumers.except_people.except_self.each do |consumer|  
174 - self.distribute_to_consumer consumer.profile  
175 - end  
176 - end  
177 -  
178 -end  
plugins/suppliers/lib/ext/products_plugin/product.rb 0 → 100644
@@ -0,0 +1,178 @@ @@ -0,0 +1,178 @@
  1 +require_dependency 'products_plugin/product'
  2 +
  3 +# FIXME: The lines bellow should be on the core
  4 +class Product
  5 +
  6 + extend CurrencyHelper::ClassMethods
  7 + has_currency :price
  8 + has_currency :discount
  9 +
  10 + scope :alphabetically, -> { order 'products.name ASC' }
  11 +
  12 + scope :available, -> { where available: true }
  13 + scope :unavailable, -> { where 'products.available <> true' }
  14 + scope :archived, -> { where archived: true }
  15 + scope :unarchived, -> { where 'products.archived <> true' }
  16 +
  17 + scope :with_available, -> (available) { where available: available }
  18 + scope :with_price, -> { where 'products.price > 0' }
  19 +
  20 + # FIXME: transliterate input and name column
  21 + scope :name_like, -> (name) { where "name ILIKE ?", "%#{name}%" }
  22 + scope :with_product_category_id, -> (id) { where product_category_id: id }
  23 +
  24 + scope :by_profile, -> (profile) { where profile_id: profile.id }
  25 + scope :by_profile_id, -> (profile_id) { where profile_id: profile_id }
  26 +
  27 + def self.product_categories_of products
  28 + ProductCategory.find products.collect(&:product_category_id).compact.select{ |id| not id.zero? }
  29 + end
  30 +
  31 + attr_accessible :external_id
  32 + settings_items :external_id, type: String, default: nil
  33 +
  34 + # should be on core, used by SuppliersPlugin::Import
  35 + attr_accessible :price_details
  36 +
  37 +end
  38 +
  39 +class Product
  40 +
  41 + attr_accessible :from_products, :from_product, :supplier_id, :supplier
  42 +
  43 + has_many :sources_from_products, foreign_key: :to_product_id, class_name: 'SuppliersPlugin::SourceProduct', dependent: :destroy
  44 + has_one :sources_from_product, foreign_key: :to_product_id, class_name: 'SuppliersPlugin::SourceProduct'
  45 + has_many :sources_to_products, foreign_key: :from_product_id, class_name: 'SuppliersPlugin::SourceProduct', dependent: :destroy
  46 + has_one :sources_to_product, foreign_key: :from_product_id, class_name: 'SuppliersPlugin::SourceProduct'
  47 + has_many :to_products, -> { order 'id ASC' }, through: :sources_to_products
  48 + has_one :to_product, -> { order 'id ASC' }, through: :sources_to_product, autosave: true
  49 + has_many :from_products, -> { order 'id ASC' }, through: :sources_from_products
  50 + has_one :from_product, -> { order 'id ASC' }, through: :sources_from_product, autosave: true
  51 +
  52 + has_many :sources_from_2x_products, through: :from_products, source: :sources_from_products
  53 + has_one :sources_from_2x_product, through: :from_product, source: :sources_from_product
  54 + has_many :sources_to_2x_products, through: :to_products, source: :sources_to_products
  55 + has_one :sources_to_2x_product, through: :to_product, source: :sources_to_product
  56 + has_many :from_2x_products, through: :sources_from_2x_products, source: :from_product
  57 + has_one :from_2x_product, through: :sources_from_2x_product, source: :from_product
  58 + has_many :to_2x_products, through: :sources_to_2x_products, source: :to_product
  59 + has_one :to_2x_product, through: :sources_to_2x_product, source: :to_product
  60 +
  61 + # semantic alias for supplier_from_product(s)
  62 + has_many :sources_supplier_products, foreign_key: :to_product_id, class_name: 'SuppliersPlugin::SourceProduct'
  63 + has_one :sources_supplier_product, foreign_key: :to_product_id, class_name: 'SuppliersPlugin::SourceProduct'
  64 + has_many :supplier_products, -> { order 'id ASC' }, through: :sources_supplier_products, source: :from_product
  65 + has_one :supplier_product, -> { order 'id ASC' }, through: :sources_supplier_product, source: :from_product, autosave: true
  66 + has_many :suppliers, -> { distinct.order 'id ASC' }, through: :sources_supplier_products
  67 + has_one :supplier, -> { order 'id ASC' }, through: :sources_supplier_product
  68 +
  69 + has_many :consumers, -> { distinct.order 'id ASC' }, through: :to_products, source: :profile
  70 + has_one :consumer, -> { order 'id ASC' }, through: :to_product, source: :profile
  71 +
  72 + # overhide original, FIXME: rename to available_and_supplier_active
  73 + scope :available, -> {
  74 + joins(:suppliers).
  75 + where 'products.available = ? AND suppliers_plugin_suppliers.active = ?', true, true
  76 + }
  77 + scope :unavailable, -> {
  78 + where 'products.available <> ? OR suppliers_plugin_suppliers.active <> ?', true, true
  79 + }
  80 + scope :with_available, -> (available) {
  81 + op = if available then '=' else '<>' end
  82 + cond = if available then 'AND' else 'OR' end
  83 + where "products.available #{op} ? #{cond} suppliers_plugin_suppliers.active #{op} ?", true, true
  84 + }
  85 +
  86 + scope :fp_name_like, -> (name) { where "from_products_products.name ILIKE ?", "%#{name}%" }
  87 + scope :fp_with_product_category_id, -> (id) { where 'from_products_products.product_category_id = ?', id }
  88 +
  89 + # prefer distributed_products has_many to use DistributedProduct scopes and eager loading
  90 + scope :distributed, -> { where type: 'SuppliersPlugin::DistributedProduct'}
  91 + scope :own, -> { where type: nil }
  92 + scope :supplied, -> {
  93 + # this remove duplicates and allow sorting on the fields, unlike distinct
  94 + group('products.id').
  95 + where type: [nil, 'SuppliersPlugin::DistributedProduct']
  96 + }
  97 + scope :supplied_for_count, -> {
  98 + distinct.
  99 + where type: [nil, 'SuppliersPlugin::DistributedProduct']
  100 + }
  101 +
  102 + scope :from_supplier, -> (supplier) { joins(:suppliers).where 'suppliers_plugin_suppliers.id = ?', supplier.id }
  103 + scope :from_supplier_id, -> (supplier_id) { joins(:suppliers).where 'suppliers_plugin_suppliers.id = ?', supplier_id }
  104 +
  105 + after_create :distribute_to_consumers
  106 +
  107 + def own?
  108 + self.class == Product
  109 + end
  110 + def distributed?
  111 + self.class == SuppliersPlugin::DistributedProduct
  112 + end
  113 + def supplied?
  114 + self.own? or self.distributed?
  115 + end
  116 +
  117 + def supplier
  118 + # FIXME: use self.suppliers when rails support for nested preload comes
  119 + @supplier ||= self.sources_supplier_product.supplier rescue nil
  120 + @supplier ||= self.profile.self_supplier rescue nil
  121 + end
  122 + def supplier= value
  123 + @supplier = value
  124 + end
  125 + def supplier_id
  126 + self.supplier.id
  127 + end
  128 + def supplier_id= id
  129 + @supplier = profile.environment.profiles.find id
  130 + end
  131 +
  132 + def supplier_dummy?
  133 + self.supplier ? self.supplier.dummy? : self.profile.dummy?
  134 + end
  135 +
  136 + def distribute_to_consumer consumer, attrs = {}
  137 + distributed_product = consumer.distributed_products.where(profile_id: consumer.id, from_products_products: {id: self.id}).first
  138 + distributed_product ||= SuppliersPlugin::DistributedProduct.create! profile: consumer, from_product: self
  139 + distributed_product.update! attrs if attrs.present?
  140 + distributed_product
  141 + end
  142 +
  143 + def destroy_dependent
  144 + self.to_products.each do |to_product|
  145 + to_product.destroy if to_product.respond_to? :dependent? and to_product.dependent?
  146 + end
  147 + end
  148 +
  149 + # before_destroy and after_destroy don't work,
  150 + # see http://stackoverflow.com/questions/14175330/associations-not-loaded-in-before-destroy-callback
  151 + def destroy
  152 + self.class.transaction do
  153 + self.destroy_dependent
  154 + super
  155 + end
  156 + end
  157 +
  158 + def diff from = self.from_product
  159 + return @changed_attrs if @changed_attrs
  160 + @changed_attrs = []
  161 + SuppliersPlugin::BaseProduct::CORE_DEFAULT_ATTRIBUTES.each do |attr|
  162 + @changed_attrs << attr if self[attr].present? and self[attr] != from[attr]
  163 + end
  164 + @changed_attrs
  165 + end
  166 +
  167 + protected
  168 +
  169 + def distribute_to_consumers
  170 + # shopping_cart creates products without a profile...
  171 + return unless self.profile
  172 +
  173 + self.profile.consumers.except_people.except_self.each do |consumer|
  174 + self.distribute_to_consumer consumer.profile
  175 + end
  176 + end
  177 +
  178 +end