solidus_reserved_stock 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.rubocop.yml +21 -0
- data/.ruby-gemset +1 -0
- data/.travis.yml +4 -0
- data/CODE_OF_CONDUCT.md +13 -0
- data/Gemfile +21 -0
- data/LICENSE.txt +21 -0
- data/README.md +74 -0
- data/Rakefile +26 -0
- data/app/controllers/spree/admin/reserved_stock_items_controller.rb +88 -0
- data/app/controllers/spree/admin/stock_items_controller_decorator.rb +7 -0
- data/app/controllers/spree/admin/users_controller_decorator.rb +16 -0
- data/app/controllers/spree/api/products_controller_decorator.rb +12 -0
- data/app/controllers/spree/api/v1/reserved_stock_items_controller.rb +88 -0
- data/app/controllers/spree/api/v1/validators/reserve_stock_params_validator.rb +37 -0
- data/app/controllers/spree/api/variants_controller_decorator.rb +12 -0
- data/app/helpers/spree/admin/reserved_stock_items_helper.rb +13 -0
- data/app/helpers/spree/admin/stock_locations_helper_decorator.rb +14 -0
- data/app/helpers/spree/api/api_helpers_decorator.rb +7 -0
- data/app/models/spree/product_decorator.rb +18 -0
- data/app/models/spree/reserved_stock_item.rb +48 -0
- data/app/models/spree/stock/coordinator_decorator.rb +28 -0
- data/app/models/spree/stock/prioritizer_decorator.rb +16 -0
- data/app/models/spree/stock/quantifier_decorator.rb +29 -0
- data/app/models/spree/stock/reserver.rb +60 -0
- data/app/models/spree/stock_location_decorator.rb +46 -0
- data/app/models/spree/stock_reservation_ability.rb +11 -0
- data/app/models/spree/user_decorator.rb +42 -0
- data/app/models/spree/variant_decorator.rb +6 -0
- data/app/overrides/spree/admin/stock_items/_stock_management/adjust_number_of_rows.html.erb.deface +2 -0
- data/app/overrides/spree/admin/stock_items/_stock_management/show_user_for_reserved_stock.html.erb.deface +2 -0
- data/app/overrides/spree/admin/users/_sidebar/add_reserved_stock_menu_item.html.erb.deface +6 -0
- data/app/views/spree/admin/reserved_stock_items/_form.html.erb +32 -0
- data/app/views/spree/admin/reserved_stock_items/index.html.erb +104 -0
- data/app/views/spree/admin/reserved_stock_items/new.html.erb +19 -0
- data/app/views/spree/api/products/show.v1.rabl +35 -0
- data/app/views/spree/api/v1/reserved_stock_items/index.v1.rabl +7 -0
- data/app/views/spree/api/v1/reserved_stock_items/show.v1.rabl +5 -0
- data/app/views/spree/api/variants/big.v1.rabl +20 -0
- data/app/views/spree/api/variants/small.v1.rabl +17 -0
- data/bin/console +14 -0
- data/bin/rails +12 -0
- data/bin/setup +7 -0
- data/config/i18n-tasks.yml +103 -0
- data/config/locales/en.yml +23 -0
- data/config/routes.rb +27 -0
- data/db/migrate/20160105203812_add_reserved_items_to_stock_location.rb +6 -0
- data/db/migrate/20160105222821_add_type_to_spree_stock_items.rb +7 -0
- data/db/migrate/20160106215753_add_user_id_to_spree_stock_items.rb +5 -0
- data/db/migrate/20160229223744_add_original_stock_location_id_to_spree_stock_items.rb +5 -0
- data/db/migrate/20160301003702_add_expires_at_to_spree_stock_items.rb +5 -0
- data/db/migrate/20160309025334_modify_stock_item_unique_index.rb +14 -0
- data/lib/generators/solidus_reserved_stock/install/install_generator.rb +22 -0
- data/lib/solidus_reserved_stock/ability_initializer.rb +5 -0
- data/lib/solidus_reserved_stock/engine.rb +15 -0
- data/lib/solidus_reserved_stock/version.rb +18 -0
- data/lib/solidus_reserved_stock.rb +7 -0
- data/lib/tasks/solidus_reserved_stock_tasks.rake +4 -0
- data/solidus_reserved_stock.gemspec +56 -0
- data/spec/controllers/spree/api/stock_locations_controller_spec.rb +24 -0
- data/spec/controllers/spree/api/v1/reserved_stock_items_controller_spec.rb +196 -0
- data/spec/controllers/spree/api/v1/validators/reserve_stock_params_validator_spec.rb +41 -0
- data/spec/controllers/spree/api/variants_controller_spec.rb +62 -0
- data/spec/factories/reserved_stock_item_factory.rb +8 -0
- data/spec/models/spree/reserved_stock_item_spec.rb +101 -0
- data/spec/models/spree/stock/coordinator_decorator_spec.rb +68 -0
- data/spec/models/spree/stock/prioritizer_decorator_spec.rb +29 -0
- data/spec/models/spree/stock/quantifier_decorator_spec.rb +42 -0
- data/spec/models/spree/stock/reserver_spec.rb +103 -0
- data/spec/models/spree/stock_location_decorator_spec.rb +47 -0
- data/spec/models/spree/user_decorator_spec.rb +65 -0
- data/spec/spec_helper.rb +56 -0
- data/spec/support/api_helpers.rb +35 -0
- data/spec/support/database_cleaner.rb +14 -0
- data/spec/support/have_attributes_matcher.rb +10 -0
- 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
data/.rspec
ADDED
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
data/CODE_OF_CONDUCT.md
ADDED
@@ -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,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
|