solidus_reserved_stock 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (78) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +21 -0
  5. data/.ruby-gemset +1 -0
  6. data/.travis.yml +4 -0
  7. data/CODE_OF_CONDUCT.md +13 -0
  8. data/Gemfile +21 -0
  9. data/LICENSE.txt +21 -0
  10. data/README.md +74 -0
  11. data/Rakefile +26 -0
  12. data/app/controllers/spree/admin/reserved_stock_items_controller.rb +88 -0
  13. data/app/controllers/spree/admin/stock_items_controller_decorator.rb +7 -0
  14. data/app/controllers/spree/admin/users_controller_decorator.rb +16 -0
  15. data/app/controllers/spree/api/products_controller_decorator.rb +12 -0
  16. data/app/controllers/spree/api/v1/reserved_stock_items_controller.rb +88 -0
  17. data/app/controllers/spree/api/v1/validators/reserve_stock_params_validator.rb +37 -0
  18. data/app/controllers/spree/api/variants_controller_decorator.rb +12 -0
  19. data/app/helpers/spree/admin/reserved_stock_items_helper.rb +13 -0
  20. data/app/helpers/spree/admin/stock_locations_helper_decorator.rb +14 -0
  21. data/app/helpers/spree/api/api_helpers_decorator.rb +7 -0
  22. data/app/models/spree/product_decorator.rb +18 -0
  23. data/app/models/spree/reserved_stock_item.rb +48 -0
  24. data/app/models/spree/stock/coordinator_decorator.rb +28 -0
  25. data/app/models/spree/stock/prioritizer_decorator.rb +16 -0
  26. data/app/models/spree/stock/quantifier_decorator.rb +29 -0
  27. data/app/models/spree/stock/reserver.rb +60 -0
  28. data/app/models/spree/stock_location_decorator.rb +46 -0
  29. data/app/models/spree/stock_reservation_ability.rb +11 -0
  30. data/app/models/spree/user_decorator.rb +42 -0
  31. data/app/models/spree/variant_decorator.rb +6 -0
  32. data/app/overrides/spree/admin/stock_items/_stock_management/adjust_number_of_rows.html.erb.deface +2 -0
  33. data/app/overrides/spree/admin/stock_items/_stock_management/show_user_for_reserved_stock.html.erb.deface +2 -0
  34. data/app/overrides/spree/admin/users/_sidebar/add_reserved_stock_menu_item.html.erb.deface +6 -0
  35. data/app/views/spree/admin/reserved_stock_items/_form.html.erb +32 -0
  36. data/app/views/spree/admin/reserved_stock_items/index.html.erb +104 -0
  37. data/app/views/spree/admin/reserved_stock_items/new.html.erb +19 -0
  38. data/app/views/spree/api/products/show.v1.rabl +35 -0
  39. data/app/views/spree/api/v1/reserved_stock_items/index.v1.rabl +7 -0
  40. data/app/views/spree/api/v1/reserved_stock_items/show.v1.rabl +5 -0
  41. data/app/views/spree/api/variants/big.v1.rabl +20 -0
  42. data/app/views/spree/api/variants/small.v1.rabl +17 -0
  43. data/bin/console +14 -0
  44. data/bin/rails +12 -0
  45. data/bin/setup +7 -0
  46. data/config/i18n-tasks.yml +103 -0
  47. data/config/locales/en.yml +23 -0
  48. data/config/routes.rb +27 -0
  49. data/db/migrate/20160105203812_add_reserved_items_to_stock_location.rb +6 -0
  50. data/db/migrate/20160105222821_add_type_to_spree_stock_items.rb +7 -0
  51. data/db/migrate/20160106215753_add_user_id_to_spree_stock_items.rb +5 -0
  52. data/db/migrate/20160229223744_add_original_stock_location_id_to_spree_stock_items.rb +5 -0
  53. data/db/migrate/20160301003702_add_expires_at_to_spree_stock_items.rb +5 -0
  54. data/db/migrate/20160309025334_modify_stock_item_unique_index.rb +14 -0
  55. data/lib/generators/solidus_reserved_stock/install/install_generator.rb +22 -0
  56. data/lib/solidus_reserved_stock/ability_initializer.rb +5 -0
  57. data/lib/solidus_reserved_stock/engine.rb +15 -0
  58. data/lib/solidus_reserved_stock/version.rb +18 -0
  59. data/lib/solidus_reserved_stock.rb +7 -0
  60. data/lib/tasks/solidus_reserved_stock_tasks.rake +4 -0
  61. data/solidus_reserved_stock.gemspec +56 -0
  62. data/spec/controllers/spree/api/stock_locations_controller_spec.rb +24 -0
  63. data/spec/controllers/spree/api/v1/reserved_stock_items_controller_spec.rb +196 -0
  64. data/spec/controllers/spree/api/v1/validators/reserve_stock_params_validator_spec.rb +41 -0
  65. data/spec/controllers/spree/api/variants_controller_spec.rb +62 -0
  66. data/spec/factories/reserved_stock_item_factory.rb +8 -0
  67. data/spec/models/spree/reserved_stock_item_spec.rb +101 -0
  68. data/spec/models/spree/stock/coordinator_decorator_spec.rb +68 -0
  69. data/spec/models/spree/stock/prioritizer_decorator_spec.rb +29 -0
  70. data/spec/models/spree/stock/quantifier_decorator_spec.rb +42 -0
  71. data/spec/models/spree/stock/reserver_spec.rb +103 -0
  72. data/spec/models/spree/stock_location_decorator_spec.rb +47 -0
  73. data/spec/models/spree/user_decorator_spec.rb +65 -0
  74. data/spec/spec_helper.rb +56 -0
  75. data/spec/support/api_helpers.rb +35 -0
  76. data/spec/support/database_cleaner.rb +14 -0
  77. data/spec/support/have_attributes_matcher.rb +10 -0
  78. metadata +396 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: b2e6f8b25eafcc693df3c6a0265b6b77a1933f7b
4
+ data.tar.gz: 45d79df61fb6aa548b0de3aff91dd3b3ab4b73cb
5
+ SHA512:
6
+ metadata.gz: a69bfc8336e4aec5044698ed7328a7ad9fe06b825b608729bf77db09125e3addf16aa7d041489e9b10f2fcffdcb53b6559f5d924bddd314dece262bd56a0df08
7
+ data.tar.gz: 7be1f324cd678cbcd0119c5f2e103c49cd7783c018807522821f1a4147f73f0ea75c0150b2d68fbda460b970724fe81a5976e32b786ce4990793c7ab20892454
data/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ /_yardoc/
2
+ /.bundle/
3
+ /.gems
4
+ /.yardoc
5
+ /coverage/
6
+ /doc/
7
+ /Gemfile.lock
8
+ /pkg/
9
+ /spec/reports/
10
+ /tmp/
11
+ /spec/dummy/
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --profile
2
+ --color
3
+ --format documentation
data/.rubocop.yml ADDED
@@ -0,0 +1,21 @@
1
+ # This is the configuration used to check the rubocop source code.
2
+ # See https://github.com/bbatsov/rubocop/blob/master/config/default.yml for info
3
+
4
+ AllCops:
5
+ Exclude:
6
+ - 'vendor/**/*'
7
+ - 'spec/fixtures/**/*'
8
+ Rails:
9
+ Enabled: true
10
+
11
+ Style/StringLiterals:
12
+ EnforcedStyle: double_quotes
13
+ SupportedStyles:
14
+ - single_quotes
15
+ - double_quotes
16
+
17
+ Style/StringLiteralsInInterpolation:
18
+ EnforcedStyle: double_quotes
19
+ SupportedStyles:
20
+ - single_quotes
21
+ - double_quotes
data/.ruby-gemset ADDED
@@ -0,0 +1 @@
1
+ .gems
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.2.3
4
+ before_install: gem install bundler -v 1.10.6
@@ -0,0 +1,13 @@
1
+ # Contributor Code of Conduct
2
+
3
+ As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
4
+
5
+ We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion.
6
+
7
+ Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct.
8
+
9
+ Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team.
10
+
11
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers.
12
+
13
+ This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/)
data/Gemfile ADDED
@@ -0,0 +1,21 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "solidus", github: "solidusio/solidus", branch: "v1.2"
4
+
5
+ group :development do
6
+ gem "i18n-tasks"
7
+ end
8
+
9
+ group :test do
10
+ gem "database_cleaner"
11
+ end
12
+
13
+ gemspec
14
+
15
+ if ENV["DB"] == "mysql"
16
+ gem "mysql2", "~> 0.3.20"
17
+ elsif ENV["DB"] == "postgres"
18
+ gem "pg"
19
+ else
20
+ gem "sqlite3", "~> 1.3.10"
21
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Isaac Freeman
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,74 @@
1
+ # SolidusReservedStock
2
+
3
+ Allow stock to be reserved for a given user, so it can't be purchased by other users.
4
+
5
+ When a customer reserves stock, it's moved from its normal stock location to a special reserved stock location. When a customer checks out, their reserved items will be used first to fulfill their order.
6
+
7
+ Reserved stock can be restored to its original stock location at any time, and can be stored with an expiry date for the reservation.
8
+
9
+ ## How it works
10
+ TODO
11
+
12
+ ## Installation
13
+
14
+ Add this line to your application's Gemfile:
15
+
16
+ ```ruby
17
+ gem 'solidus_reserved_stock'
18
+ ```
19
+
20
+ And then execute:
21
+
22
+ $ bundle
23
+
24
+ Or install it yourself as:
25
+
26
+ $ gem install solidus_reserved_stock
27
+
28
+ ## Usage
29
+
30
+ <!-- TODO: Write usage instructions here -->
31
+
32
+ ### API Usage
33
+ `GET /api/v1/reserved_stock_items.json`
34
+ Retrieve all reserved stock items
35
+
36
+ `GET /api/v1/user/:user_id/reserved_stock_items.json`
37
+ Retrieve reserved stock items for a given user
38
+
39
+ `POST /api/v1/reserved_stock_items/reserve.json`
40
+ Reserve stock for a given user.
41
+ Parameters:
42
+ - `variant_id` or `sku` to identify the variant (one of these is required)
43
+ - `original_stock_location_id` to remove stock from (required)
44
+ - `user_id` identifying the user the stock will be reserved for (required)
45
+ - `quantity` of stock to reserve (required)
46
+ - `expires_at` date when the reservation ends and stock can be returned to the original stock location (optional)
47
+
48
+ `POST /api/v1/reserved_stock_items/restock.json`
49
+ Return reserved stock to its original stock location.
50
+ Parameters:
51
+ - `variant_id` or `sku` to identify the variant (one of these is required)
52
+ - `user_id` identifying the user the stock was be reserved for (required)
53
+ - `quantity` of stock to restore (optional – if not present, the full amount of reserved stock will be restored)
54
+
55
+ `POST /api/v1/reserved_stock_items/restock_expired.json`
56
+ Restock all reserved items whose expiry date has passed.
57
+
58
+ `GET /api/stock_locations(.:format)`
59
+ This is the same as the standard Solidus route, but the response is decorated to include a `reserved_items` parameter indicating whether the stock location is for reserved stock items.
60
+
61
+ `GET /api/variants/:id?user_id=:user_id`
62
+ `GET /api/products/:id?user_id=:user_id`
63
+ Same as the standard Solidus routes, but accept an optional `user_id` parameter
64
+ so that `total_on_hand` can include reserved stock for the user.
65
+
66
+
67
+ ## Contributing
68
+
69
+ Bug reports and pull requests are welcome on GitHub at https://github.com/resolve/solidus_reserved_stock. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](contributor-covenant.org) code of conduct.
70
+
71
+
72
+ ## License
73
+
74
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,26 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rubygems'
3
+ require 'rake'
4
+ require 'rake/testtask'
5
+ require 'rake/packagetask'
6
+ require 'rubygems/package_task'
7
+ require 'rspec/core/rake_task'
8
+ require 'spree/testing_support/common_rake'
9
+ require 'solidus_reserved_stock'
10
+
11
+ Bundler::GemHelper.install_tasks
12
+ RSpec::Core::RakeTask.new
13
+
14
+ task default: :spec
15
+
16
+ spec = eval(File.read('solidus_reserved_stock.gemspec'))
17
+
18
+ Gem::PackageTask.new(spec) do |p|
19
+ p.gem_spec = spec
20
+ end
21
+
22
+ desc 'Generates a dummy app for testing'
23
+ task :test_app do
24
+ ENV['LIB_NAME'] = 'solidus_reserved_stock'
25
+ Rake::Task['common:test_app'].invoke
26
+ end
@@ -0,0 +1,88 @@
1
+ module Spree
2
+ module Admin
3
+ class ReservedStockItemsController < ResourceController
4
+ before_action :load_user, only: [:index, :create, :new, :restock]
5
+ before_action :load_variants, only: [:create, :new]
6
+ before_action :load_original_stock_locations, only: [:create, :new]
7
+
8
+ class_attribute :variant_display_attributes
9
+ self.variant_display_attributes = [
10
+ { translation_key: :sku, attr_name: :sku },
11
+ { translation_key: :name, attr_name: :name }
12
+ ]
13
+
14
+ def index
15
+ @reserved_stock_items = @user.reserved_stock_items
16
+ @variant_display_attributes = self.class.variant_display_attributes
17
+ end
18
+
19
+ def create
20
+ validator = Spree::Api::V1::Validators::ReserveStockParamsValidator.new(permitted_resource_params)
21
+
22
+ if validator.validate
23
+ variant = Spree::Variant.find(permitted_resource_params[:variant_id])
24
+ original_stock_location = Spree::StockLocation.find(permitted_resource_params[:original_stock_location_id])
25
+ @reserved_stock_item = Spree::Stock::Reserver.new.reserve(
26
+ variant,
27
+ original_stock_location,
28
+ @user,
29
+ permitted_resource_params[:quantity].to_i,
30
+ Time.zone.parse(permitted_resource_params[:expires_at])
31
+ )
32
+ flash[:success] = flash_message_for(@reserved_stock_item, :successfully_created)
33
+ redirect_to admin_user_reserved_stock_items_path(@user)
34
+ else
35
+ flash[:error] = "#{Spree.t("admin.reserved_stock_item.unable_to_create")}: #{validator.errors}"
36
+ render :new
37
+ end
38
+ rescue => e
39
+ flash[:error] = "#{Spree.t("admin.reserved_stock_item.unable_to_create")}: #{e.message}"
40
+ render :new
41
+ end
42
+
43
+ def restock
44
+ variant = Spree::ReservedStockItem.find(params[:id]).variant
45
+ Spree::Stock::Reserver.new.restock(variant, @user)
46
+ flash[:success] = flash_message_for(@reserved_stock_item, :successfully_restocked)
47
+ redirect_to admin_user_reserved_stock_items_path(@user)
48
+ rescue => e
49
+ flash[:error] = "#{Spree.t("admin.reserved_stock_item.unable_to_restock")}: #{e.message}"
50
+ redirect_to admin_user_reserved_stock_items_path(@user)
51
+ end
52
+
53
+ private
54
+
55
+ def location_after_destroy
56
+ :back
57
+ end
58
+
59
+ def location_after_save
60
+ :back
61
+ end
62
+
63
+ def collection
64
+ []
65
+ end
66
+
67
+ def permitted_resource_params
68
+ params.require(:reserved_stock_item).permit!.
69
+ merge(user_id: @user.id)
70
+ end
71
+
72
+
73
+ def load_user
74
+ @user = Spree.user_class.find(params[:user_id])
75
+ end
76
+
77
+ def load_original_stock_locations
78
+ @original_stock_locations = Spree::StockLocation.not_reserved_items.order_default
79
+ end
80
+
81
+ def load_variants
82
+ @variants = Spree::Variant
83
+ .includes(option_values: :option_type)
84
+ .order(id: :desc)
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,7 @@
1
+ module Spree
2
+ module Admin
3
+ StockItemsController.class_eval do
4
+ helper Spree::Admin::ReservedStockItemsHelper
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,16 @@
1
+ module Spree
2
+ module Admin
3
+ UsersController.class_eval do
4
+ class_attribute :variant_display_attributes
5
+ self.variant_display_attributes = [
6
+ { translation_key: :sku, attr_name: :sku },
7
+ { translation_key: :name, attr_name: :name }
8
+ ]
9
+
10
+ def reserved_stock_items
11
+ @reserved_stock_items = @user.reserved_stock_items
12
+ @variant_display_attributes = self.class.variant_display_attributes
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,12 @@
1
+ Spree::Api::ProductsController.class_eval do
2
+ before_action :load_customer, only: :show
3
+
4
+ private
5
+
6
+ # Retrieve a customer so we can return stock information specific to that
7
+ # customer
8
+ def load_customer
9
+ user_id = params[:user_id]
10
+ @customer = user_id.present? ? Spree.user_class.find(user_id) : nil
11
+ end
12
+ end
@@ -0,0 +1,88 @@
1
+ module Spree
2
+ module Api
3
+ module V1
4
+ # API Controller for reserving, restocking and expiring stock
5
+ class ReservedStockItemsController < Spree::Api::BaseController
6
+ before_action :authorize_reserving_stock
7
+
8
+ def index
9
+ @reserved_stock_items = scope.all.page(params[:page]).per(params[:per_page])
10
+ respond_with(@reserved_stock_items)
11
+ end
12
+
13
+ def reserve
14
+ render_reserve_param_errors(params); return if performed?
15
+ @reserved_stock_item = Spree::Stock::Reserver.new.reserve(
16
+ variant,
17
+ original_stock_location,
18
+ user,
19
+ params[:quantity],
20
+ params[:expires_at]
21
+ )
22
+ respond_with(@reserved_stock_item, status: :created, default_template: :show)
23
+ rescue => e
24
+ # TODO: Appropriate error if @reserved_stock_item not returned
25
+ invalid_resource!(@reserved_stock_item)
26
+ end
27
+
28
+ def restock
29
+ @reserved_stock_item = Spree::Stock::Reserver.new.restock(
30
+ variant, user, params[:quantity]
31
+ )
32
+ respond_with(@reserved_stock_item, status: :created, default_template: :show)
33
+ rescue => e
34
+ invalid_resource!(@reserved_stock_item)
35
+ end
36
+
37
+ def restock_expired
38
+ Spree::Stock::Reserver.new.restock_expired
39
+ head 204 # return success with no body
40
+ rescue => e
41
+ # TODO: error message
42
+ end
43
+
44
+ private
45
+
46
+ def authorize_reserving_stock
47
+ authorize! :manage, ReservedStockItem
48
+ end
49
+
50
+ def user
51
+ Spree.user_class.find(params[:user_id])
52
+ end
53
+
54
+ def variant
55
+ variant_id = params[:variant_id]
56
+ sku = params[:sku]
57
+ return Spree::Variant.find(variant_id) if variant_id.present?
58
+ return Spree::Variant.find_by(sku: sku) if sku.present?
59
+ nil
60
+ end
61
+
62
+ def original_stock_location
63
+ Spree::StockLocation.find(params[:original_stock_location_id])
64
+ end
65
+
66
+ def render_reserve_param_errors(params)
67
+ validator = Validators::ReserveStockParamsValidator.new(params)
68
+ render_errors(validator.errors) unless validator.validate
69
+ end
70
+
71
+ def render_errors(errors)
72
+ return if errors.blank?
73
+ Rails.logger.error errors
74
+ render json: {'errors': errors}.to_json, status: 422
75
+ end
76
+
77
+ def scope
78
+ base_scope = if params[:user_id].present?
79
+ user
80
+ else
81
+ Spree::StockLocation.reserved_items_location
82
+ end
83
+ base_scope.reserved_stock_items.accessible_by(current_ability, :read).includes(:variant)
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,37 @@
1
+ module Spree
2
+ module Api
3
+ module V1
4
+ module Validators
5
+ # Generate useful error messages for API calls to ReservedStockItemsController#reserve
6
+ class ReserveStockParamsValidator
7
+ attr_reader :errors
8
+
9
+ def initialize(params)
10
+ @params = params
11
+ @errors = []
12
+ end
13
+
14
+ def validate
15
+ if @params[:variant_id].blank? && @params[:sku].blank?
16
+ @errors << { status: 422, detail: "Parameters must include either 'variant_id' or 'sku'"}
17
+ end
18
+
19
+ if @params[:original_stock_location_id].blank?
20
+ @errors << { status: 422, detail: "Parameters must include 'original_stock_location_id'"}
21
+ end
22
+
23
+ if @params[:user_id].blank?
24
+ @errors << { status: 422, detail: "Parameters must include 'user_id'"}
25
+ end
26
+
27
+ if @params[:quantity].blank?
28
+ @errors << { status: 422, detail: "Parameters must include 'quantity'"}
29
+ end
30
+
31
+ @errors.empty?
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,12 @@
1
+ Spree::Api::VariantsController.class_eval do
2
+ before_action :load_customer, only: :show
3
+
4
+ private
5
+
6
+ # Retrieve a customer so we can return stock information specific to that
7
+ # customer
8
+ def load_customer
9
+ user_id = params[:user_id]
10
+ @customer = user_id.present? ? Spree.user_class.find(user_id) : nil
11
+ end
12
+ end
@@ -0,0 +1,13 @@
1
+ module Spree
2
+ module Admin
3
+ module ReservedStockItemsHelper
4
+ def stock_location_name(stock_item)
5
+ stock_location = stock_item.stock_location
6
+ return stock_location.name unless stock_location.reserved_items?
7
+
8
+ user_link = link_to(stock_item.user.email, admin_user_path(stock_item.user))
9
+ "#{stock_location.name} (#{user_link})".html_safe
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,14 @@
1
+ module Spree
2
+ module Admin
3
+ module StockLocationsHelper
4
+
5
+ alias_method :original_display_name, :display_name
6
+ def display_name(stock_location)
7
+ name = original_display_name(stock_location)
8
+ return name unless stock_location.reserved_items?
9
+ "#{name} (Reserved Items)"
10
+ end
11
+
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,7 @@
1
+ Spree::Api::ApiHelpers.module_eval do
2
+ def stock_location_attributes_with_reserved_items_decoration
3
+ stock_location_attributes_without_reserved_items_decoration | [:reserved_items]
4
+ end
5
+
6
+ alias_method_chain :stock_location_attributes, :reserved_items_decoration
7
+ end
@@ -0,0 +1,18 @@
1
+ Spree::Product.class_eval do
2
+ # Don't count reserved stock, unless a user argument is given
3
+ def total_on_hand(user = nil)
4
+ if any_variants_not_track_inventory?
5
+ Float::INFINITY
6
+ else
7
+ unreserved_count = stock_items
8
+ .where("type IS NULL OR type <> 'Spree::ReservedStockItem'")
9
+ .sum(:count_on_hand)
10
+ return unreserved_count unless user.present?
11
+ reserved_count = stock_items
12
+ .where("type = 'Spree::ReservedStockItem'")
13
+ .where(user_id: user.id)
14
+ .sum(:count_on_hand)
15
+ unreserved_count + reserved_count
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,48 @@
1
+ # Validates that the StockLocation is specifically for reserved stock
2
+ class ReservedStockLocationValidator < ActiveModel::Validator
3
+ def validate(record)
4
+ return unless record.stock_location # Missing stock location should be caught by other validator
5
+ return if record.stock_location.reserved_items?
6
+ record.errors[:stock_location] << "StockLocation for ReservedStockItems must have reserved_items? == true"
7
+ end
8
+ end
9
+
10
+ # Validates that the reserved stock item is not backorderable
11
+ class NotBackOrderableValidator < ActiveModel::Validator
12
+ def validate(record)
13
+ return unless record.backorderable?
14
+ record.errors[:backorderable] << "ReservedStockItems must not be backorderable"
15
+ end
16
+ end
17
+
18
+ module Spree
19
+ # A StockItem that's been reserved by a customer. It will be stored in a
20
+ # dedicated StockLocation, but rmembers the original StockLocation for the
21
+ # items, so the can be restored if the reservation expiresor is cancelled.
22
+ class ReservedStockItem < Spree::StockItem
23
+ include ActiveModel::Validations
24
+
25
+ belongs_to :user, class_name: Spree::UserClassHandle.new
26
+
27
+ belongs_to :original_stock_location, class_name: Spree::StockLocation
28
+
29
+ # Remove 'variant_id' attribute from its existing uniqueness validator
30
+ # (effectively disabling it) so we can add a new uniqueness validator that
31
+ # is scoped to user_id. We want multiple users to be able to reserve the
32
+ # same variant.
33
+ variant_id_uniqueness_validator = validators_on(:variant_id).detect do |validator|
34
+ validator.is_a? ActiveRecord::Validations::UniquenessValidator
35
+ end
36
+ variant_id_uniqueness_validator.attributes.delete(:variant_id) if variant_id_uniqueness_validator
37
+
38
+ validates_with ReservedStockLocationValidator
39
+ validates_with NotBackOrderableValidator
40
+ validates_uniqueness_of :variant_id,
41
+ scope: [
42
+ :stock_location_id,
43
+ :deleted_at,
44
+ :user_id
45
+ ]
46
+ validates :user_id, presence: true
47
+ end
48
+ end
@@ -0,0 +1,28 @@
1
+ Spree::Stock::Coordinator.class_eval do
2
+ private
3
+
4
+ # Overridden so that users only see their own reserved stock items, and not
5
+ # those reserved by other users
6
+ # TODO: PR into Solidus at https://github.com/solidusio/solidus/pull/1180
7
+ # provides a better way to achieve this than overriding a private
8
+ # method. If it's accepted, we should use that approach.
9
+ def stock_location_variant_ids
10
+ location_variant_ids = Spree::StockItem.
11
+ where(variant_id: unallocated_variant_ids).
12
+ where(user_id: [order.user_id, nil]). # only change is adding this line
13
+ joins(:stock_location).
14
+ merge(Spree::StockLocation.active).
15
+ pluck(:stock_location_id, :variant_id)
16
+
17
+ location_lookup = Spree::StockLocation.
18
+ where(id: location_variant_ids.map(&:first).uniq).
19
+ map { |l| [l.id, l] }.
20
+ to_h
21
+ hash = location_variant_ids.each_with_object({}) do |(location_id, variant_id), hash|
22
+ location = location_lookup[location_id]
23
+ hash[location] ||= Set.new
24
+ hash[location] << variant_id
25
+ end
26
+ hash
27
+ end
28
+ end
@@ -0,0 +1,16 @@
1
+ Spree::Stock::Prioritizer.class_eval do
2
+ private
3
+
4
+ # Use reserved_stock_location first
5
+ alias_method :original_sort_packages, :sort_packages
6
+ def sort_packages
7
+ original_sort_packages
8
+
9
+ reserved_stock_location = Spree::StockLocation.reserved_items_location
10
+ @packages = @packages
11
+ .partition do |package|
12
+ package.stock_location == reserved_stock_location
13
+ end
14
+ .flatten
15
+ end
16
+ end
@@ -0,0 +1,29 @@
1
+ Spree::Stock::Quantifier.class_eval do
2
+ def initialize(variant, stock_location = nil, user = nil)
3
+ @variant = variant
4
+ where_args = { variant_id: @variant }
5
+ if stock_location
6
+ where_args.merge!(stock_location: stock_location)
7
+ else
8
+ where_args.merge!(
9
+ Spree::StockLocation.table_name => {
10
+ active: true,
11
+ reserved_items: false
12
+ }
13
+ )
14
+ end
15
+ @stock_items = Spree::StockItem.joins(:stock_location).where(where_args)
16
+
17
+ if user && user.reserved_stock_item(variant)
18
+ @stock_items.unshift user.reserved_stock_item(variant)
19
+ end
20
+ end
21
+
22
+ def total_on_hand
23
+ if @variant.should_track_inventory?
24
+ stock_items.to_a.sum(&:count_on_hand)
25
+ else
26
+ Float::INFINITY
27
+ end
28
+ end
29
+ end