solidus_marketplace 0.1.0

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.
Files changed (116) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +18 -0
  3. data/.rspec +1 -0
  4. data/.travis.yml +11 -0
  5. data/Gemfile +8 -0
  6. data/LICENSE +26 -0
  7. data/README.md +123 -0
  8. data/Rakefile +15 -0
  9. data/Versionfile +6 -0
  10. data/app/assets/javascripts/spree/backend/solidus_marketplace.js +5 -0
  11. data/app/assets/javascripts/spree/backend/solidus_marketplace_routes.js +1 -0
  12. data/app/assets/javascripts/spree/backend/suppliers_autocomplete.js +55 -0
  13. data/app/assets/javascripts/spree/frontend/solidus_marketplace.js +1 -0
  14. data/app/assets/stylesheets/spree/backend/solidus_marketplace.scss +22 -0
  15. data/app/assets/stylesheets/spree/frontend/solidus_marketplace.css +3 -0
  16. data/app/controllers/ckeditor/attachment_files_controller_decorator.rb +20 -0
  17. data/app/controllers/ckeditor/pictures_controller_decorator.rb +17 -0
  18. data/app/controllers/spree/admin/marketplace_settings_controller.rb +19 -0
  19. data/app/controllers/spree/admin/products_controller_decorator.rb +79 -0
  20. data/app/controllers/spree/admin/reports_controller_decorator.rb +68 -0
  21. data/app/controllers/spree/admin/shipments_controller.rb +47 -0
  22. data/app/controllers/spree/admin/stock_items_controller_decorator.rb +10 -0
  23. data/app/controllers/spree/admin/stock_locations_controller_decorator.rb +23 -0
  24. data/app/controllers/spree/admin/suppliers_controller.rb +54 -0
  25. data/app/controllers/spree/admin/users_controller_decorator.rb +34 -0
  26. data/app/controllers/spree/api/suppliers_controller.rb +16 -0
  27. data/app/controllers/spree/api/v1/stock_locations_controller_decorator.rb +18 -0
  28. data/app/controllers/spree/base_controller_decorator.rb +13 -0
  29. data/app/helpers/spree/api/api_helpers_decorator.rb +22 -0
  30. data/app/mailers/spree/marketplace_order_mailer.rb +13 -0
  31. data/app/mailers/spree/supplier_mailer.rb +12 -0
  32. data/app/models/ckeditor/asset_decorator.rb +5 -0
  33. data/app/models/spree.rb +5 -0
  34. data/app/models/spree/marketplace_configuration.rb +14 -0
  35. data/app/models/spree/order_decorator.rb +44 -0
  36. data/app/models/spree/payment_decorator.rb +7 -0
  37. data/app/models/spree/product_decorator.rb +52 -0
  38. data/app/models/spree/shipment_decorator.rb +40 -0
  39. data/app/models/spree/stock/splitter/marketplace.rb +47 -0
  40. data/app/models/spree/stock_location_decorator.rb +23 -0
  41. data/app/models/spree/supplier.rb +120 -0
  42. data/app/models/spree/supplier_ability.rb +35 -0
  43. data/app/models/spree/supplier_variant.rb +6 -0
  44. data/app/models/spree/user_decorator.rb +23 -0
  45. data/app/models/spree/variant_decorator.rb +24 -0
  46. data/app/overrides/spree/admin/menus.rb +15 -0
  47. data/app/overrides/spree/admin/products/_form/converted_admin_product_form_right.html.erb.deface +5 -0
  48. data/app/overrides/spree/admin/products/edit/override_rows.html.erb.deface +22 -0
  49. data/app/overrides/spree/admin/stock_locations/_form/add_supplier.html.erb.deface +12 -0
  50. data/app/views/spree/admin/marketplace_settings/edit.html.erb +21 -0
  51. data/app/views/spree/admin/orders/index.html.erb +187 -0
  52. data/app/views/spree/admin/products/_form.html.erb +198 -0
  53. data/app/views/spree/admin/reports/earnings.html.erb +34 -0
  54. data/app/views/spree/admin/shared/_marketplace_settings.html.erb +1 -0
  55. data/app/views/spree/admin/shared/_marketplace_tabs.html.erb +4 -0
  56. data/app/views/spree/admin/shipments/edit.html.erb +24 -0
  57. data/app/views/spree/admin/shipments/index.html.erb +93 -0
  58. data/app/views/spree/admin/suppliers/_form.html.erb +75 -0
  59. data/app/views/spree/admin/suppliers/edit.html.erb +22 -0
  60. data/app/views/spree/admin/suppliers/index.html.erb +68 -0
  61. data/app/views/spree/admin/suppliers/new.html.erb +17 -0
  62. data/app/views/spree/api/shared/_pagination.json.jbuilder +5 -0
  63. data/app/views/spree/api/suppliers/_supplier.json.jbuilder +1 -0
  64. data/app/views/spree/api/suppliers/index.json.jbuilder +4 -0
  65. data/app/views/spree/marketplace_order_mailer/supplier_order.html.erb +119 -0
  66. data/app/views/spree/shared/unauthorized.erb +1 -0
  67. data/app/views/spree/supplier_mailer/welcome.html.erb +53 -0
  68. data/config/locales/en.yml +107 -0
  69. data/config/locales/es.yml +133 -0
  70. data/config/routes.rb +24 -0
  71. data/db/default/spree/marketplace_roles.rb +14 -0
  72. data/db/migrate/20121006073854_create_suppliers.rb +20 -0
  73. data/db/migrate/20130216070944_product_belongs_to_supplier.rb +6 -0
  74. data/db/migrate/20130405005502_stock_locations_belongs_to_supplier.rb +6 -0
  75. data/db/migrate/20130405011127_user_belongs_to_supplier.rb +6 -0
  76. data/db/migrate/20130428063053_add_balanced_token_to_suppliers.rb +7 -0
  77. data/db/migrate/20130510181443_add_supplier_id_to_ckeditor_assets.rb +8 -0
  78. data/db/migrate/20130606220913_add_permalink_to_suppliers.rb +6 -0
  79. data/db/migrate/20140323170638_add_supplier_commission_to_shipments.rb +5 -0
  80. data/db/migrate/20140416184616_migrate_payment_and_commission.rb +7 -0
  81. data/db/migrate/20140529041325_create_spree_supplier_variants.rb +15 -0
  82. data/db/migrate/20171027180043_add_paypal_email_to_suppliers.rb +5 -0
  83. data/db/seeds.rb +8 -0
  84. data/lib/generators/solidus_marketplace/install/install_generator.rb +36 -0
  85. data/lib/solidus_marketplace.rb +6 -0
  86. data/lib/solidus_marketplace/engine.rb +53 -0
  87. data/lib/solidus_marketplace/factories.rb +107 -0
  88. data/lib/solidus_marketplace/version.rb +3 -0
  89. data/lib/spree/permitted_attributes_decorator.rb +18 -0
  90. data/lib/tasks/spree_sample.rake +72 -0
  91. data/log/awesome.log +1 -0
  92. data/script/rails +7 -0
  93. data/solidus_marketplace.gemspec +46 -0
  94. data/spec/features/admin/orders_spec.rb +11 -0
  95. data/spec/features/admin/products_spec.rb +25 -0
  96. data/spec/features/admin/return_authorizations_spec.rb +7 -0
  97. data/spec/features/admin/settings_spec.rb +33 -0
  98. data/spec/features/admin/shipments_spec.rb +80 -0
  99. data/spec/features/admin/stock_management_spec.rb +156 -0
  100. data/spec/features/admin/stock_spec.rb +98 -0
  101. data/spec/features/admin/suppliers_spec.rb +130 -0
  102. data/spec/models/spree/order_decorator_spec.rb +80 -0
  103. data/spec/models/spree/payment_decorator_spec.rb +7 -0
  104. data/spec/models/spree/product_decorator_spec.rb +56 -0
  105. data/spec/models/spree/shipment_decorator_spec.rb +48 -0
  106. data/spec/models/spree/stock/splitter/marketplace_spec.rb +62 -0
  107. data/spec/models/spree/stock_location_decorator_spec.rb +33 -0
  108. data/spec/models/spree/supplier_ability_spec.rb +182 -0
  109. data/spec/models/spree/supplier_spec.rb +123 -0
  110. data/spec/models/spree/supplier_variant_spec.rb +5 -0
  111. data/spec/models/spree/user_decorator_spec.rb +57 -0
  112. data/spec/models/spree/variant_decorator_spec.rb +7 -0
  113. data/spec/spec_helper.rb +91 -0
  114. data/spec/support/ability_helpers.rb +11 -0
  115. data/spec/support/integration_helpers.rb +15 -0
  116. metadata +473 -0
@@ -0,0 +1,68 @@
1
+ require 'csv'
2
+
3
+ Spree::Admin::ReportsController.class_eval do
4
+ before_action :add_marketplace_reports, only: [:index]
5
+
6
+ def earnings
7
+ @supplier_earnings = get_supplier_earnings
8
+ respond_to do |format|
9
+ format.html
10
+ format.csv { send_data earnings_csv }
11
+ end
12
+ end
13
+
14
+ def earnings_csv
15
+ header1 = ["Supplier Earnings"]
16
+ header2 = ["Supplier", "Earnings", "Paypal Email"]
17
+
18
+ CSV.generate do |csv|
19
+ csv << header1
20
+ csv << []
21
+ csv << header2
22
+ @supplier_earnings.each do |se|
23
+ csv << ["#{se[:name]}", "#{se[:earnings].to_html}", "#{se[:paypal_email]}"]
24
+ end
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def add_marketplace_reports
31
+ marketplace_reports.each do |report|
32
+ Spree::Admin::ReportsController.add_available_report! report
33
+ end
34
+ end
35
+
36
+ def marketplace_reports
37
+ [:earnings]
38
+ end
39
+
40
+ def get_supplier_earnings
41
+ grouped_supplier_earnings.each do |se|
42
+ se[:earnings] = se[:earnings].inject(Spree::Money.new(0)){ |e, c| c + e }
43
+ end
44
+ end
45
+
46
+ def grouped_supplier_earnings
47
+ params[:q] = search_params
48
+ @search = Spree::Order.complete.ransack(params[:q])
49
+ @orders = @search.result
50
+
51
+ supplier_earnings_map = @orders.map { |o| o.supplier_earnings_map }
52
+ grouped_suppliers_map = supplier_earnings_map.flatten.group_by { |e| e[:name] }.values
53
+ grouped_earnings = grouped_suppliers_map.map do |gs|
54
+ h = {}
55
+ h[:name] = nil
56
+ h[:paypal_email] = nil
57
+ h[:earnings] = []
58
+ gs.each do |s|
59
+ h[:name] = s[:name] if h[:name].nil?
60
+ h[:paypal_email] = s[:paypal_email] if h[:paypal_email].nil?
61
+ h[:earnings] << s[:earnings]
62
+ end
63
+ h
64
+ end
65
+ grouped_earnings
66
+ end
67
+
68
+ end
@@ -0,0 +1,47 @@
1
+ module Spree
2
+ module Admin
3
+ class ShipmentsController < Spree::Admin::ResourceController
4
+
5
+ def index
6
+ params[:q] ||= {}
7
+ # params[:q][:completed_at_null] ||= '1'
8
+ # @show_only_incomplete = params[:q][:completed_at_null].present?
9
+ # params[:q][:s] ||= @show_only_incomplete ? 'created_at desc' : 'completed_at desc'
10
+
11
+ # As date params are deleted if @show_only_incomplete, store
12
+ # the original date so we can restore them into the params
13
+ # after the search
14
+ created_at_gt = params[:q][:created_at_gt]
15
+ created_at_lt = params[:q][:created_at_lt]
16
+
17
+ if !params[:q][:created_at_gt].blank?
18
+ params[:q][:created_at_gt] = Time.zone.parse(params[:q][:created_at_gt]).beginning_of_day rescue ""
19
+ end
20
+
21
+ if !params[:q][:created_at_lt].blank?
22
+ params[:q][:created_at_lt] = Time.zone.parse(params[:q][:created_at_lt]).end_of_day rescue ""
23
+ end
24
+
25
+ @search = Spree::Shipment.accessible_by(current_ability, :index).ransack(params[:q])
26
+ @shipments = @search.result.
27
+ page(params[:page]).
28
+ per(params[:per_page] || Spree::Config[:orders_per_page])
29
+
30
+ # Restore dates
31
+ params[:q][:created_at_gt] = created_at_gt
32
+ params[:q][:created_at_lt] = created_at_lt
33
+ end
34
+
35
+ private
36
+
37
+ def find_resource
38
+ if parent_data.present?
39
+ parent.send(controller_name).find_by!(number: params[:id])
40
+ else
41
+ model_class.find_by!(number: params[:id])
42
+ end
43
+ end
44
+
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,10 @@
1
+ Spree::Admin::StockItemsController.class_eval do
2
+ before_action :load_supplier_stock_location, only: :index
3
+
4
+ def load_supplier_stock_location
5
+ if try_spree_current_user.supplier
6
+ @stock_locations = Spree::StockLocation.by_supplier(try_spree_current_user.supplier).accessible_by(current_ability, :read)
7
+ @stock_item_stock_locations = params[:stock_location_id].present? ? @stock_locations.where(id: params[:stock_location_id]) : @stock_locations
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,23 @@
1
+ Spree::Admin::StockLocationsController.class_eval do
2
+
3
+ create.after :set_supplier
4
+
5
+ def index
6
+ @stock_locations = Spree::StockLocation.accessible_by(current_ability, :read)
7
+ .order('name ASC')
8
+ .ransack(params[:q])
9
+ .result
10
+ .page(params[:page])
11
+ .per(params[:per_page])
12
+ end
13
+
14
+ private
15
+
16
+ def set_supplier
17
+ if try_spree_current_user.supplier?
18
+ @object.supplier = try_spree_current_user.supplier
19
+ @object.save
20
+ end
21
+ end
22
+
23
+ end
@@ -0,0 +1,54 @@
1
+ class Spree::Admin::SuppliersController < Spree::Admin::ResourceController
2
+
3
+ def update
4
+ @object.address = Spree::Address.immutable_merge(@object.address, permitted_resource_params[:address_attributes])
5
+
6
+ if @object.update_attributes(permitted_resource_params.except(:address_attributes))
7
+ respond_with(@object) do |format|
8
+ format.html do
9
+ flash[:success] = flash_message_for(@object, :successfully_updated)
10
+ redirect_to location_after_save
11
+ end
12
+ format.js { render layout: false }
13
+ end
14
+ else
15
+ respond_with(@object) do |format|
16
+ format.html do
17
+ flash.now[:error] = @object.errors.full_messages.join(", ")
18
+ render_after_update_error
19
+ end
20
+ format.js { render layout: false }
21
+ end
22
+ end
23
+ end
24
+
25
+ def edit
26
+ @object.address = Spree::Address.build_default unless @object.address.present?
27
+ respond_with(@object) do |format|
28
+ format.html { render :layout => !request.xhr? }
29
+ format.js { render :layout => false }
30
+ end
31
+ end
32
+
33
+ def new
34
+ @object.address = Spree::Address.build_default
35
+ end
36
+
37
+ private
38
+
39
+ def collection
40
+ params[:q] ||= {}
41
+ params[:q][:meta_sort] ||= "name.asc"
42
+ @search = Spree::Supplier.search(params[:q])
43
+ @collection = @search.result.page(params[:page]).per(Spree::Config[:orders_per_page])
44
+ end
45
+
46
+ def find_resource
47
+ Spree::Supplier.friendly.find(params[:id])
48
+ end
49
+
50
+ def location_after_save
51
+ spree.edit_admin_supplier_path(@object)
52
+ end
53
+
54
+ end
@@ -0,0 +1,34 @@
1
+ Spree::Admin::UsersController.class_eval do
2
+
3
+ def create
4
+ current_supplier = try_spree_current_user.supplier
5
+ @user = Spree.user_class.new(user_params)
6
+ @user.supplier = current_supplier
7
+ if @user.save
8
+ set_roles
9
+ set_stock_locations
10
+
11
+ flash[:success] = Spree.t(:created_successfully)
12
+ redirect_to edit_admin_user_url(@user)
13
+ else
14
+ load_roles
15
+ load_stock_locations
16
+
17
+ flash.now[:error] = @user.errors.full_messages.join(", ")
18
+ render :new, status: :unprocessable_entity
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def build_resource
25
+ current_supplier = try_spree_current_user.supplier
26
+
27
+ if parent?
28
+ parent.send(controller_name).build
29
+ else
30
+ model_class.new(supplier: current_supplier)
31
+ end
32
+ end
33
+
34
+ end
@@ -0,0 +1,16 @@
1
+ module Spree
2
+ module Api
3
+ class SuppliersController < Spree::Api::BaseController
4
+ def index
5
+ if params[:ids]
6
+ @suppliers = Spree::Supplier.accessible_by(current_ability, :read).where(id: params[:ids].split(',')).order(:name)
7
+ else
8
+ @suppliers = Spree::Supplier.accessible_by(current_ability, :read).order(:name).ransack(params[:q]).result
9
+ end
10
+
11
+ @suppliers = paginate(@suppliers)
12
+ respond_with(@suppliers)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,18 @@
1
+ Spree::Api::StockLocationsController.class_eval do
2
+
3
+ before_action :supplier_locations, only: [:index]
4
+ before_action :supplier_transfers, only: [:index]
5
+
6
+ private
7
+
8
+ def supplier_locations
9
+ params[:q] ||= {}
10
+ params[:q][:supplier_id_eq] = spree_current_user.supplier_id
11
+ end
12
+
13
+ def supplier_transfers
14
+ params[:q] ||= {}
15
+ params[:q][:supplier_id_eq] = spree_current_user.supplier_id
16
+ end
17
+
18
+ end
@@ -0,0 +1,13 @@
1
+ Spree::BaseController.class_eval do
2
+
3
+ prepend_before_action :redirect_supplier
4
+
5
+ private
6
+
7
+ def redirect_supplier
8
+ if ['/admin', '/admin/authorization_failure'].include?(request.path) && try_spree_current_user.try(:supplier)
9
+ redirect_to '/admin/shipments' and return false
10
+ end
11
+ end
12
+
13
+ end
@@ -0,0 +1,22 @@
1
+ module Spree
2
+ module Api
3
+ module ApiHelpers
4
+
5
+ @@supplier_attributes = [
6
+ :id,
7
+ :address_id,
8
+ :commission_flat_rate,
9
+ :commission_percentage,
10
+ :email,
11
+ :name,
12
+ :url,
13
+ :deleted_at,
14
+ :tax_id,
15
+ :token,
16
+ :slug
17
+ ]
18
+
19
+ mattr_reader(:supplier_attributes)
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,13 @@
1
+ module Spree
2
+ class MarketplaceOrderMailer < Spree::BaseMailer
3
+
4
+ default from: Spree::Store.default.mail_from_address
5
+
6
+ def supplier_order(shipment_id)
7
+ @shipment = Shipment.find shipment_id
8
+ @supplier = @shipment.supplier
9
+ mail to: @supplier.email, subject: Spree.t('marketplace_order_mailer.supplier_order.subject', name: Spree::Store.current.name, number: @shipment.number)
10
+ end
11
+
12
+ end
13
+ end
@@ -0,0 +1,12 @@
1
+ module Spree
2
+ class SupplierMailer < Spree::BaseMailer
3
+
4
+ default from: Spree::Store.default.mail_from_address
5
+
6
+ def welcome(supplier_id)
7
+ @supplier = Supplier.find supplier_id
8
+ mail to: @supplier.email, subject: Spree.t('supplier_mailer.welcome.subject')
9
+ end
10
+
11
+ end
12
+ end
@@ -0,0 +1,5 @@
1
+ if defined?(Ckeditor)
2
+ Ckeditor::Asset.class_eval do
3
+ belongs_to :supplier, class_name: 'Spree::Supplier'
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ module Spree
2
+ def self.table_name_prefix
3
+ 'spree_'
4
+ end
5
+ end
@@ -0,0 +1,14 @@
1
+ module Spree
2
+ class MarketplaceConfiguration < Preferences::Configuration
3
+
4
+ # Default flat rate to charge suppliers per order for commission.
5
+ preference :default_commission_flat_rate, :float, default: 0.0
6
+
7
+ # Default percentage to charge suppliers per order for commission.
8
+ preference :default_commission_percentage, :float, default: 0.0
9
+
10
+ # Determines whether or not to email a new supplier their welcome email.
11
+ preference :send_supplier_email, :boolean, default: true
12
+
13
+ end
14
+ end
@@ -0,0 +1,44 @@
1
+ Spree::Order.class_eval do
2
+
3
+ has_many :stock_locations, through: :shipments
4
+ has_many :suppliers, through: :stock_locations
5
+
6
+ def supplier_total(user_or_supplier)
7
+ supplier = user_or_supplier.is_a?(Spree::Supplier) ? user_or_supplier : user_or_supplier.supplier
8
+ shipments = self.shipments.by_supplier(supplier)
9
+ commissions = shipments.map(&:supplier_commission_total)
10
+ Spree::Money.new(commissions.sum)
11
+ end
12
+
13
+ def supplier_earnings_map
14
+ suppliers.map do |s|
15
+ {
16
+ name: s.name,
17
+ earnings: self.supplier_total(s),
18
+ paypal_email: s.paypal_email
19
+ }
20
+ end
21
+ end
22
+
23
+ # Once order is finalized we want to notify the suppliers of their drop ship orders.
24
+ # Here we are handling notification by emailing the suppliers.
25
+ # If you want to customize this you could override it as a hook for notifying a supplier with a API request instead.
26
+ def finalize_with_supplier!
27
+ finalize_without_supplier!
28
+ shipments.each do |shipment|
29
+ if SolidusMarketplace::Config[:send_supplier_email] && shipment.supplier.present?
30
+ begin
31
+ Spree::MarketplaceOrderMailer.supplier_order(shipment.id).deliver!
32
+ rescue => ex #Errno::ECONNREFUSED => ex
33
+ puts ex.message
34
+ puts ex.backtrace.join("\n")
35
+ Rails.logger.error ex.message
36
+ Rails.logger.error ex.backtrace.join("\n")
37
+ return true # always return true so that failed email doesn't crash app.
38
+ end
39
+ end
40
+ end
41
+ end
42
+ # alias_method :finalize!, :drop_ship
43
+
44
+ end
@@ -0,0 +1,7 @@
1
+ module Spree
2
+ Payment.class_eval do
3
+
4
+ belongs_to :payable, polymorphic: true
5
+
6
+ end
7
+ end
@@ -0,0 +1,52 @@
1
+ Spree::Product.class_eval do
2
+
3
+ has_many :suppliers, -> { readonly }, through: :master
4
+
5
+ scope :of_supplier, -> (supplier_id) { joins(:suppliers).where('spree_suppliers.id = ?', supplier_id) }
6
+
7
+ def add_supplier!(supplier_or_id)
8
+ supplier = supplier_or_id.is_a?(Spree::Supplier) ? supplier_or_id : Spree::Supplier.find(supplier_or_id)
9
+ populate_for_supplier! supplier if supplier
10
+ end
11
+
12
+ def add_suppliers!(supplier_ids)
13
+ Spree::Supplier.where(id: supplier_ids).each do |supplier|
14
+ populate_for_supplier! supplier
15
+ end
16
+ end
17
+
18
+ def remove_suppliers!(supplier_ids)
19
+ Spree::Supplier.where(id: supplier_ids).each do |supplier|
20
+ unpopulate_for_supplier! supplier
21
+ end
22
+ end
23
+
24
+ # Returns true if the product has one or more suppliers.
25
+ def supplier?
26
+ suppliers.present?
27
+ end
28
+
29
+ private
30
+
31
+ def populate_for_supplier!(supplier)
32
+ variants_including_master.each do |variant|
33
+ unless variant.suppliers.pluck(:id).include?(supplier.id)
34
+ variant.suppliers << supplier
35
+ supplier.stock_locations.each { |location| location.propagate_variant(variant) unless location.stock_item(variant) }
36
+ end
37
+ end
38
+ end
39
+
40
+ def unpopulate_for_supplier!(supplier)
41
+ variants_including_master.each do |variant|
42
+ if variant.suppliers.pluck(:id).include?(supplier.id)
43
+ variant.suppliers.delete(supplier)
44
+
45
+ supplier.stock_locations.each do |location|
46
+ location.unpropagate_variant(variant)
47
+ end
48
+ end
49
+ end
50
+ end
51
+
52
+ end