solidus_product_assembly 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (82) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/.rspec +3 -0
  4. data/.travis.yml +21 -0
  5. data/Gemfile +18 -0
  6. data/LICENSE.md +13 -0
  7. data/README.md +80 -0
  8. data/Rakefile +21 -0
  9. data/app/assets/images/spinner.gif +0 -0
  10. data/app/assets/javascripts/spree/backend/solidus_product_assembly.js +1 -0
  11. data/app/assets/javascripts/spree/frontend/solidus_product_assembly.js +1 -0
  12. data/app/assets/stylesheets/spree/backend/solidus_product_assembly.css +3 -0
  13. data/app/assets/stylesheets/spree/frontend/solidus_product_assembly.css +3 -0
  14. data/app/controllers/spree/admin/parts_controller.rb +49 -0
  15. data/app/controllers/spree/checkout_controller_decorator.rb +9 -0
  16. data/app/helpers/spree/admin/orders_helper_decorator.rb +9 -0
  17. data/app/models/spree/assemblies_part.rb +12 -0
  18. data/app/models/spree/inventory_unit_decorator.rb +13 -0
  19. data/app/models/spree/line_item_decorator.rb +44 -0
  20. data/app/models/spree/order_inventory_assembly.rb +39 -0
  21. data/app/models/spree/product_decorator.rb +56 -0
  22. data/app/models/spree/shipment_decorator.rb +50 -0
  23. data/app/models/spree/stock/availability_validator.rb +28 -0
  24. data/app/models/spree/stock/inventory_unit_builder_decorator.rb +30 -0
  25. data/app/models/spree/stock/inventory_validator_decorator.rb +12 -0
  26. data/app/models/spree/variant_decorator.rb +13 -0
  27. data/app/overrides/add_admin_product_form_fields.rb +5 -0
  28. data/app/overrides/add_admin_tabs.rb +5 -0
  29. data/app/overrides/add_line_item_description.rb +5 -0
  30. data/app/overrides/spree/admin/orders/_form/inject_product_assemblies.html.erb.deface +3 -0
  31. data/app/overrides/spree/admin/orders/_shipment/stock_contents.html.erb.deface +2 -0
  32. data/app/overrides/spree/checkout/_delivery/remove_unshippable_markup.html.erb.deface +1 -0
  33. data/app/overrides/spree/checkout/_delivery/render_line_item_manifest.html.erb.deface +3 -0
  34. data/app/overrides/spree/products/show/add_links_to_parts.html.erb.deface +21 -0
  35. data/app/overrides/spree/products/show/remove_add_to_cart_button_for_non_individual_sale_products.html.erb.deface +4 -0
  36. data/app/overrides/spree/shared/_order_details/part_description.html.erb.deface +8 -0
  37. data/app/views/spree/admin/orders/_assemblies.html.erb +56 -0
  38. data/app/views/spree/admin/orders/_stock_contents.html.erb +69 -0
  39. data/app/views/spree/admin/orders/_stock_item.html.erb +46 -0
  40. data/app/views/spree/admin/parts/_parts_table.html.erb +33 -0
  41. data/app/views/spree/admin/parts/available.html.erb +55 -0
  42. data/app/views/spree/admin/parts/available.js.erb +54 -0
  43. data/app/views/spree/admin/parts/index.html.erb +70 -0
  44. data/app/views/spree/admin/parts/update_parts_table.js.erb +2 -0
  45. data/app/views/spree/admin/products/_product_assembly_fields.html.erb +23 -0
  46. data/app/views/spree/admin/shared/_product_assembly_product_tabs.html.erb +3 -0
  47. data/app/views/spree/checkout/_line_item_manifest.html.erb +17 -0
  48. data/app/views/spree/orders/_cart_description.html.erb +8 -0
  49. data/bin/rails +7 -0
  50. data/config/locales/en.yml +14 -0
  51. data/config/locales/fr.yml +12 -0
  52. data/config/locales/ru.yml +12 -0
  53. data/config/locales/sv.yml +12 -0
  54. data/config/routes.rb +19 -0
  55. data/db/migrate/20091028152124_add_many_to_many_relation_to_products.rb +13 -0
  56. data/db/migrate/20091029165620_add_parts_fields_to_products.rb +27 -0
  57. data/db/migrate/20120316141830_namespace_product_assembly_for_spree_one.rb +9 -0
  58. data/db/migrate/20140620223938_add_id_to_spree_assemblies_parts.rb +9 -0
  59. data/lib/generators/solidus_product_assembly/install/install_generator.rb +24 -0
  60. data/lib/solidus_product_assembly.rb +3 -0
  61. data/lib/solidus_product_assembly/engine.rb +15 -0
  62. data/lib/solidus_product_assembly/version.rb +3 -0
  63. data/lib/tasks/spree2_upgrade.rake +29 -0
  64. data/solidus_product_assembly.gemspec +35 -0
  65. data/spec/features/admin/orders_spec.rb +29 -0
  66. data/spec/features/admin/parts_spec.rb +28 -0
  67. data/spec/features/checkout_spec.rb +92 -0
  68. data/spec/models/spree/assemblies_part_spec.rb +18 -0
  69. data/spec/models/spree/inventory_unit_spec.rb +32 -0
  70. data/spec/models/spree/line_item_spec.rb +72 -0
  71. data/spec/models/spree/order_contents_spec.rb +40 -0
  72. data/spec/models/spree/order_inventory_assembly_spec.rb +55 -0
  73. data/spec/models/spree/order_inventory_spec.rb +35 -0
  74. data/spec/models/spree/product_spec.rb +35 -0
  75. data/spec/models/spree/shipment_spec.rb +112 -0
  76. data/spec/models/spree/stock/availability_validator_spec.rb +71 -0
  77. data/spec/models/spree/stock/coordinator_spec.rb +52 -0
  78. data/spec/models/spree/stock/inventory_unit_builder_spec.rb +36 -0
  79. data/spec/models/spree/variant_spec.rb +28 -0
  80. data/spec/spec_helper.rb +57 -0
  81. data/spec/support/shared_contexts/order_with_bundle.rb +13 -0
  82. metadata +272 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 89f3f4fbf3c8638a1d3a0eb8daeec71ab2e0be2c
4
+ data.tar.gz: e67518d457ee0aa60495099b41e058b917f36472
5
+ SHA512:
6
+ metadata.gz: a4dae9aac9db62b23daca17315e84596f8da6421269ba816e8f17b6ac100e0c5916692d09e5f701c69b09972f65afe538fb231bd325bbf6e37690d48e8827c25
7
+ data.tar.gz: ff2051e5a167ef6f1d4d7fce18d302baef6952e94f22df48115242ee5e9d9037170e93b99ab47e91ebd794e115dba79527db152c174ffd6f4659cdabcdb96003
@@ -0,0 +1,4 @@
1
+ spec/dummy
2
+ Gemfile.lock
3
+ coverage
4
+ spec/examples.txt
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --color
2
+ --format
3
+ progress
@@ -0,0 +1,21 @@
1
+ sudo: false
2
+ cache: bundler
3
+ language: ruby
4
+ rvm:
5
+ - 2.3.1
6
+ env:
7
+ matrix:
8
+ - SOLIDUS_BRANCH=v1.0 DB=postgres
9
+ - SOLIDUS_BRANCH=v1.1 DB=postgres
10
+ - SOLIDUS_BRANCH=v1.2 DB=postgres
11
+ - SOLIDUS_BRANCH=v1.3 DB=postgres
12
+ - SOLIDUS_BRANCH=v1.4 DB=postgres
13
+ - SOLIDUS_BRANCH=v2.0 DB=postgres
14
+ - SOLIDUS_BRANCH=master DB=postgres
15
+ - SOLIDUS_BRANCH=v1.0 DB=mysql
16
+ - SOLIDUS_BRANCH=v1.1 DB=mysql
17
+ - SOLIDUS_BRANCH=v1.2 DB=mysql
18
+ - SOLIDUS_BRANCH=v1.3 DB=mysql
19
+ - SOLIDUS_BRANCH=v1.4 DB=mysql
20
+ - SOLIDUS_BRANCH=v2.0 DB=mysql
21
+ - SOLIDUS_BRANCH=master DB=mysql
data/Gemfile ADDED
@@ -0,0 +1,18 @@
1
+ source "https://rubygems.org"
2
+
3
+ branch = ENV.fetch('SOLIDUS_BRANCH', 'master')
4
+ gem "solidus", github: "solidusio/solidus", branch: branch
5
+ gem 'solidus_auth_devise'
6
+
7
+ if branch == 'master' || branch >= "v2.0"
8
+ gem "rails-controller-testing", group: :test
9
+ end
10
+
11
+ gem 'pg'
12
+ gem 'mysql2'
13
+
14
+ group :development, :test do
15
+ gem "pry-rails"
16
+ end
17
+
18
+ gemspec
@@ -0,0 +1,13 @@
1
+ Spree Product Assembly License
2
+ ==============================
3
+
4
+ Copyright © 2007-2014, Spree Commerce Inc. and other contributors.
5
+ All rights reserved.
6
+
7
+ Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
8
+
9
+ * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
10
+ * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
11
+ * Neither the name of Spree Commerce Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
12
+
13
+ _This software is provided by the copyright holders and contributors "as is" and any express or implied warranties, including, but not limited to, the implied warranties of merchantability and fitness for a particular purpose are disclaimed. In no event shall the copyright owner of contributors be liable for any direct, indirect, incidental, special, exemplary, or consequential damages (including, but not limited to, procurement of substitute goods or services; loss of use, data, or profits; or business interruption) however caused and on any theory of liability, whether in contract, strict liability, or tort (including negligence or otherwise) arising in any way out of the use of this software, even if advised of the possibility of such damage._
@@ -0,0 +1,80 @@
1
+ # Product Assembly
2
+
3
+ [![Build Status](https://travis-ci.org/solidusio-contrib/solidus_product_assembly.svg?branch=master)](https://travis-ci.org/solidusio-contrib/solidus_product_assembly)
4
+
5
+ Create a product which is composed of other products.
6
+
7
+ ## Installation
8
+
9
+ Add the following line to your `Gemfile`
10
+ ```ruby
11
+ gem 'solidus_product_assembly', github: 'solidusio-contrib/solidus_product_assembly', branch: 'master'
12
+ ```
13
+
14
+ Run bundle install as well as the extension intall command to copy and run migrations and
15
+ append solidus_product_assembly to your js manifest file
16
+
17
+ bundle install
18
+ rails g solidus_product_assembly:install
19
+
20
+ _master branch is compatible with spree edge and rails 4 only. Please use
21
+ 2-0-stable for Spree 2.0.x or 1-3-stable branch for Spree 1.3.x compatibility_
22
+
23
+ _In case you're upgrading from 1-3-stable of this extension you might want to run a
24
+ rake task which assigns a line item to your previous inventory units from bundle
25
+ products. That is so you have a better view on the current backend UI and avoid
26
+ exceptions. No need to run this task if you're not upgrading from product assembly
27
+ 1-3-stable_
28
+
29
+ rake solidus_product_assembly:upgrade
30
+
31
+ ## Use
32
+
33
+ To build a bundle (assembly product) you'd need to first check the "Can be part"
34
+ flag on each product you want to be part of the bundle. Then create a product
35
+ and add parts to it. By doing that you're making that product an assembly.
36
+
37
+ The store will treat assemblies a bit different than regular products on checkout.
38
+ Spree will create and track inventory units for its parts rather than for the product itself.
39
+ That means you essentially have a product composed of other products. From a
40
+ customer perspective it's like they are paying a single amount for a collection
41
+ of products.
42
+
43
+ Contributing
44
+ ------------
45
+
46
+ Spree is an open source project and we encourage contributions. Please see the [contributors guidelines][1] before contributing.
47
+
48
+ In the spirit of [free software][2], **everyone** is encouraged to help improve this project.
49
+
50
+ Here are some ways *you* can contribute:
51
+
52
+ * by using prerelease versions
53
+ * by reporting [bugs][3]
54
+ * by suggesting new features
55
+ * by writing translations
56
+ * by writing or editing documentation
57
+ * by writing specifications
58
+ * by writing code (*no patch is too small*: fix typos, add comments, clean up inconsistent whitespace)
59
+ * by refactoring code
60
+ * by resolving [issues][3]
61
+ * by reviewing patches
62
+
63
+ Starting point:
64
+
65
+ * Fork the repo
66
+ * Clone your repo
67
+ * Run `bundle install`
68
+ * Run `bundle exec rake test_app` to create the test application in `spec/test_app`
69
+ * Make your changes
70
+ * Ensure specs pass by running `bundle exec rspec spec`
71
+ * Submit your pull request
72
+
73
+ Copyright (c) 2014 [Spree Commerce Inc.][4] and [contributors][5], released under the [New BSD License][6]
74
+
75
+ [1]: http://guides.spreecommerce.com/developer/contributing.html
76
+ [2]: http://www.fsf.org/licensing/essays/free-sw.html
77
+ [3]: https://github.com/spree/spree-product-assembly/issues
78
+ [4]: https://github.com/spree
79
+ [5]: https://github.com/spree/spree-product-assembly/graphs/contributors
80
+ [6]: https://github.com/spree/spree-product-assembly/blob/master/LICENSE.md
@@ -0,0 +1,21 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ require 'rspec/core/rake_task'
5
+ require 'spree/testing_support/common_rake'
6
+
7
+ RSpec::Core::RakeTask.new
8
+
9
+ task :default do
10
+ if Dir["spec/dummy"].empty?
11
+ Rake::Task[:test_app].invoke
12
+ Dir.chdir("../../")
13
+ end
14
+ Rake::Task[:spec].invoke
15
+ end
16
+
17
+ desc 'Generates a dummy app for testing'
18
+ task :test_app do
19
+ ENV['LIB_NAME'] = 'solidus_product_assembly'
20
+ Rake::Task['common:test_app'].invoke("Spree::User")
21
+ end
@@ -0,0 +1 @@
1
+ //= require spree/frontend
@@ -0,0 +1,3 @@
1
+ /*
2
+ *= require spree/backend
3
+ */
@@ -0,0 +1,3 @@
1
+ /*
2
+ *= require spree/frontend
3
+ */
@@ -0,0 +1,49 @@
1
+ class Spree::Admin::PartsController < Spree::Admin::BaseController
2
+ before_filter :find_product
3
+
4
+ def index
5
+ @parts = @product.parts
6
+ end
7
+
8
+ def remove
9
+ @part = Spree::Variant.find(params[:id])
10
+ @product.remove_part(@part)
11
+ render 'spree/admin/parts/update_parts_table'
12
+ end
13
+
14
+ def set_count
15
+ @part = Spree::Variant.find(params[:id])
16
+ @product.set_part_count(@part, params[:count].to_i)
17
+ render 'spree/admin/parts/update_parts_table'
18
+ end
19
+
20
+ def available
21
+ if params[:q].blank?
22
+ @available_products = []
23
+ else
24
+ query = "%#{params[:q]}%"
25
+ @available_products = Spree::Product.search_can_be_part(query)
26
+ @available_products.uniq!
27
+ end
28
+ respond_to do |format|
29
+ format.html {render :layout => false}
30
+ format.js {render :layout => false}
31
+ end
32
+ end
33
+
34
+ def create
35
+ @part = Spree::Variant.find(params[:part_id])
36
+ qty = params[:part_count].to_i
37
+ @product.add_part(@part, qty) if qty > 0
38
+ render 'spree/admin/parts/update_parts_table'
39
+ end
40
+
41
+ private
42
+ def find_product
43
+ @product = Spree::Product.find_by(slug: params[:product_id])
44
+ end
45
+
46
+ def model_class
47
+ Spree::AssembliesPart
48
+ end
49
+ end
@@ -0,0 +1,9 @@
1
+ module Spree
2
+ if defined? Spree::Frontend
3
+ CheckoutController.class_eval do
4
+ # Override because we don't want to remove unshippable items from the order
5
+ # A bundle itself is an unshippable item
6
+ def before_payment; end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Spree
2
+ module Admin
3
+ OrdersHelper.module_eval do
4
+ def line_item_shipment_price(line_item, quantity)
5
+ Spree::Money.new(line_item.price * quantity, { currency: line_item.currency })
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,12 @@
1
+ module Spree
2
+ class AssembliesPart < ActiveRecord::Base
3
+ belongs_to :assembly, :class_name => "Spree::Product",
4
+ :foreign_key => "assembly_id", touch: true
5
+
6
+ belongs_to :part, :class_name => "Spree::Variant", :foreign_key => "part_id"
7
+
8
+ def self.get(assembly_id, part_id)
9
+ find_or_initialize_by(assembly_id: assembly_id, part_id: part_id)
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,13 @@
1
+ module Spree
2
+ InventoryUnit.class_eval do
3
+ def percentage_of_line_item
4
+ product = line_item.product
5
+ if product.assembly?
6
+ total_value = line_item.quantity_by_variant.map { |part, quantity| part.price * quantity }.sum
7
+ variant.price / total_value
8
+ else
9
+ 1 / BigDecimal.new(line_item.quantity)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,44 @@
1
+ module Spree
2
+ LineItem.class_eval do
3
+ scope :assemblies, -> { joins(:product => :parts).uniq }
4
+
5
+ def any_units_shipped?
6
+ inventory_units.any? { |unit| unit.shipped? }
7
+ end
8
+
9
+ # The parts that apply to this particular LineItem. Usually `product#parts`, but
10
+ # provided as a hook if you want to override and customize the parts for a specific
11
+ # LineItem.
12
+ def parts
13
+ product.parts
14
+ end
15
+
16
+ # The number of the specified variant that make up this LineItem. By default, calls
17
+ # `product#count_of`, but provided as a hook if you want to override and customize
18
+ # the parts available for a specific LineItem. Note that if you only customize whether
19
+ # a variant is included in the LineItem, and don't customize the quantity of that part
20
+ # per LineItem, you shouldn't need to override this method.
21
+ def count_of(variant)
22
+ product.count_of(variant)
23
+ end
24
+
25
+ def quantity_by_variant
26
+ if self.product.assembly?
27
+ {}.tap { |hash| self.product.assemblies_parts.each { |ap| hash[ap.part] = ap.count * self.quantity } }
28
+ else
29
+ { self.variant => self.quantity }
30
+ end
31
+ end
32
+
33
+ private
34
+ def update_inventory
35
+ if (changed? || target_shipment.present?) && self.order.has_checkout_step?("delivery")
36
+ if self.product.assembly?
37
+ OrderInventoryAssembly.new(self).verify(target_shipment)
38
+ else
39
+ OrderInventory.new(self.order, self).verify(target_shipment)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,39 @@
1
+ module Spree
2
+ # This class has basically the same functionality of Spree core OrderInventory
3
+ # except that it takes account of bundle parts and properly creates and removes
4
+ # inventory unit for each parts of a bundle
5
+ class OrderInventoryAssembly < OrderInventory
6
+ attr_reader :product
7
+
8
+ def initialize(line_item)
9
+ @order = line_item.order
10
+ @line_item = line_item
11
+ @product = line_item.product
12
+ end
13
+
14
+ def verify(shipment = nil)
15
+ if order.completed? || shipment.present?
16
+ line_item.quantity_by_variant.each do |part, total_parts|
17
+ existing_parts = line_item.inventory_units.where(variant: part).count
18
+
19
+ self.variant = part
20
+
21
+ if existing_parts < total_parts
22
+ shipment = determine_target_shipment unless shipment
23
+ add_to_shipment(shipment, total_parts - existing_parts)
24
+ elsif existing_parts > total_parts
25
+ quantity = existing_parts - total_parts
26
+ if shipment.present?
27
+ remove_from_shipment(shipment, quantity)
28
+ else
29
+ order.shipments.each do |shipment|
30
+ break if quantity == 0
31
+ quantity -= remove_from_shipment(shipment, quantity)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,56 @@
1
+ Spree::Product.class_eval do
2
+ has_and_belongs_to_many :parts, :class_name => "Spree::Variant",
3
+ :join_table => "spree_assemblies_parts",
4
+ :foreign_key => "assembly_id", :association_foreign_key => "part_id"
5
+
6
+ has_many :assemblies_parts, :class_name => "Spree::AssembliesPart",
7
+ :foreign_key => "assembly_id"
8
+
9
+ scope :individual_saled, -> { where(individual_sale: true) }
10
+
11
+ scope :search_can_be_part, ->(query){ not_deleted.available.joins(:master)
12
+ .where(arel_table["name"].matches("%#{query}%").or(Spree::Variant.arel_table["sku"].matches("%#{query}%")))
13
+ .where(can_be_part: true)
14
+ .limit(30)
15
+ }
16
+
17
+ validate :assembly_cannot_be_part, :if => :assembly?
18
+
19
+ def add_part(variant, count = 1)
20
+ set_part_count(variant, count_of(variant) + count)
21
+ end
22
+
23
+ def remove_part(variant)
24
+ set_part_count(variant, 0)
25
+ end
26
+
27
+ def set_part_count(variant, count)
28
+ ap = assemblies_part(variant)
29
+ if count > 0
30
+ ap.count = count
31
+ ap.save
32
+ else
33
+ ap.destroy
34
+ end
35
+ reload
36
+ end
37
+
38
+ def assembly?
39
+ parts.present?
40
+ end
41
+
42
+ def count_of(variant)
43
+ ap = assemblies_part(variant)
44
+ # This checks persisted because the default count is 1
45
+ ap.persisted? ? ap.count : 0
46
+ end
47
+
48
+ def assembly_cannot_be_part
49
+ errors.add(:can_be_part, Spree.t(:assembly_cannot_be_part)) if can_be_part
50
+ end
51
+
52
+ private
53
+ def assemblies_part(variant)
54
+ Spree::AssembliesPart.get(self.id, variant.id)
55
+ end
56
+ end
@@ -0,0 +1,50 @@
1
+ module Spree
2
+ Shipment.class_eval do
3
+ # Overriden from Spree core as a product bundle part should not be put
4
+ # together with an individual product purchased (even though they're the
5
+ # very same variant) That is so we can tell the store admin which units
6
+ # were purchased individually and which ones as parts of the bundle
7
+ #
8
+ # Account for situations where we can't track the line_item for a variant.
9
+ # This should avoid exceptions when users upgrade from spree 1.3
10
+ #
11
+ # TODO Can possibly be removed as well. We already override the manifest
12
+ # partial so we can get the product there
13
+ def manifest
14
+ items = []
15
+ inventory_units.joins(:variant).includes(:variant, :line_item).group_by(&:variant).each do |variant, units|
16
+
17
+ units.group_by(&:line_item).each do |line_item, units|
18
+ states = {}
19
+ units.group_by(&:state).each { |state, iu| states[state] = iu.count }
20
+ line_item ||= order.find_line_item_by_variant(variant)
21
+
22
+ part = line_item ? line_item.product.assembly? : false
23
+ items << OpenStruct.new(part: part,
24
+ product: line_item.try(:product),
25
+ line_item: line_item,
26
+ variant: variant,
27
+ quantity: units.length,
28
+ states: states)
29
+ end
30
+ end
31
+ items
32
+ end
33
+
34
+ # There might be scenarios where we don't want to display every single
35
+ # variant on the shipment. e.g. when ordering a product bundle that includes
36
+ # 5 other parts. Frontend users should only see the product bundle as a
37
+ # single item to ship
38
+ def line_item_manifest
39
+ inventory_units.includes(:line_item, :variant).group_by(&:line_item).map do |line_item, units|
40
+ states = {}
41
+ units.group_by(&:state).each { |state, iu| states[state] = iu.count }
42
+ OpenStruct.new(line_item: line_item, variant: line_item.variant, quantity: units.length, states: states)
43
+ end
44
+ end
45
+
46
+ def inventory_units_for_item(line_item, variant)
47
+ inventory_units.where(line_item_id: line_item.id, variant_id: variant.id)
48
+ end
49
+ end
50
+ end