solidus_product_bundle 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (95) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +5 -0
  3. data/.rspec +3 -0
  4. data/.travis.yml +11 -0
  5. data/Gemfile +9 -0
  6. data/LICENSE.md +13 -0
  7. data/README.markdown +95 -0
  8. data/README.md +2 -0
  9. data/Rakefile +15 -0
  10. data/app/assets/images/spinner.gif +0 -0
  11. data/app/assets/javascripts/spree/backend/spree_product_assembly/index.js.coffee +82 -0
  12. data/app/assets/javascripts/spree/backend/spree_product_assembly/translations.js.erb +3 -0
  13. data/app/assets/javascripts/spree/frontend/spree_product_assembly.js +1 -0
  14. data/app/assets/stylesheets/spree/backend/spree_product_assembly.css +3 -0
  15. data/app/assets/stylesheets/spree/frontend/spree_product_assembly.css +3 -0
  16. data/app/controllers/spree/admin/parts_controller.rb +63 -0
  17. data/app/controllers/spree/checkout_controller_decorator.rb +9 -0
  18. data/app/helpers/spree/admin/orders_helper_decorator.rb +9 -0
  19. data/app/models/spree/assemblies_part.rb +33 -0
  20. data/app/models/spree/assign_part_to_bundle_form.rb +64 -0
  21. data/app/models/spree/inventory_unit_decorator.rb +13 -0
  22. data/app/models/spree/line_item_decorator.rb +65 -0
  23. data/app/models/spree/order_contents_decorator.rb +35 -0
  24. data/app/models/spree/order_inventory_assembly.rb +54 -0
  25. data/app/models/spree/part_line_item.rb +6 -0
  26. data/app/models/spree/product_decorator.rb +37 -0
  27. data/app/models/spree/shipment_decorator.rb +50 -0
  28. data/app/models/spree/stock/availability_validator.rb +27 -0
  29. data/app/models/spree/stock/inventory_unit_builder_decorator.rb +30 -0
  30. data/app/models/spree/variant_decorator.rb +13 -0
  31. data/app/overrides/add_admin_product_form_fields.rb +5 -0
  32. data/app/overrides/add_admin_tabs.rb +5 -0
  33. data/app/overrides/add_line_item_description.rb +5 -0
  34. data/app/overrides/spree/admin/orders/_form/inject_product_assemblies.html.erb.deface +3 -0
  35. data/app/overrides/spree/admin/orders/_shipment/stock_contents.html.erb.deface +2 -0
  36. data/app/overrides/spree/checkout/_delivery/remove_unshippable_markup.html.erb.deface +1 -0
  37. data/app/overrides/spree/checkout/_delivery/render_line_item_manifest.html.erb.deface +3 -0
  38. data/app/overrides/spree/products/add_links_to_parts.rb +6 -0
  39. data/app/overrides/spree/products/show/remove_add_to_cart_button_for_non_individual_sale_products.html.erb.deface +4 -0
  40. data/app/overrides/spree/shared/_order_details/part_description.html.erb.deface +16 -0
  41. data/app/serializers/spree/wombat/assembly_shipment_serializer.rb +37 -0
  42. data/app/views/spree/admin/orders/_assemblies.html.erb +62 -0
  43. data/app/views/spree/admin/orders/_stock_contents.html.erb +69 -0
  44. data/app/views/spree/admin/orders/_stock_item.html.erb +46 -0
  45. data/app/views/spree/admin/parts/_parts_table.html.erb +33 -0
  46. data/app/views/spree/admin/parts/available.html.erb +43 -0
  47. data/app/views/spree/admin/parts/index.html.erb +19 -0
  48. data/app/views/spree/admin/parts/update_parts_table.js.erb +2 -0
  49. data/app/views/spree/admin/products/_product_assembly_fields.html.erb +23 -0
  50. data/app/views/spree/admin/shared/_product_assembly_product_tabs.html.erb +3 -0
  51. data/app/views/spree/checkout/_line_item_manifest.html.erb +17 -0
  52. data/app/views/spree/orders/_cart_description.html.erb +16 -0
  53. data/app/views/spree/products/show/_parts.html.erb +38 -0
  54. data/bin/rails +7 -0
  55. data/config/locales/en.yml +15 -0
  56. data/config/locales/fr.yml +12 -0
  57. data/config/locales/ru.yml +12 -0
  58. data/config/locales/sv.yml +12 -0
  59. data/config/routes.rb +19 -0
  60. data/db/migrate/20091028152124_add_many_to_many_relation_to_products.rb +13 -0
  61. data/db/migrate/20091029165620_add_parts_fields_to_products.rb +27 -0
  62. data/db/migrate/20120316141830_namespace_product_assembly_for_spree_one.rb +9 -0
  63. data/db/migrate/20140620223938_add_id_to_spree_assemblies_parts.rb +9 -0
  64. data/db/migrate/20150219192418_add_variant_selection_deferred_to_assemblies_parts.rb +5 -0
  65. data/db/migrate/20150303105615_create_part_line_items.rb +9 -0
  66. data/lib/generators/spree_product_assembly/install/install_generator.rb +24 -0
  67. data/lib/spree_product_assembly/engine.rb +21 -0
  68. data/lib/spree_product_assembly.rb +4 -0
  69. data/lib/tasks/spree2_upgrade.rake +29 -0
  70. data/solidus_product_bundle.gemspec +33 -0
  71. data/spec/features/adding_items_to_cart_spec.rb +203 -0
  72. data/spec/features/admin/orders_spec.rb +29 -0
  73. data/spec/features/admin/parts_spec.rb +183 -0
  74. data/spec/features/checkout_spec.rb +249 -0
  75. data/spec/features/updating_items_in_cart_spec.rb +199 -0
  76. data/spec/models/spree/assemblies_part_spec.rb +18 -0
  77. data/spec/models/spree/assign_part_to_bundle_form_spec.rb +51 -0
  78. data/spec/models/spree/inventory_unit_spec.rb +32 -0
  79. data/spec/models/spree/line_item_spec.rb +88 -0
  80. data/spec/models/spree/order_contents_spec.rb +82 -0
  81. data/spec/models/spree/order_inventory_assembly_spec.rb +321 -0
  82. data/spec/models/spree/order_inventory_spec.rb +34 -0
  83. data/spec/models/spree/product_spec.rb +40 -0
  84. data/spec/models/spree/shipment_spec.rb +113 -0
  85. data/spec/models/spree/stock/availability_validator_spec.rb +71 -0
  86. data/spec/models/spree/stock/coordinator_spec.rb +46 -0
  87. data/spec/models/spree/stock/inventory_unit_builder_spec.rb +36 -0
  88. data/spec/models/spree/variant_spec.rb +28 -0
  89. data/spec/serializers/spree/wombat/assembly_shipment_serializer_spec.rb +36 -0
  90. data/spec/spec_helper.rb +56 -0
  91. data/spec/support/factories/assemblies_part_factory.rb +10 -0
  92. data/spec/support/factories/part_line_item_factory.rb +9 -0
  93. data/spec/support/factories/variant_factory.rb +15 -0
  94. data/spec/support/shared_contexts/order_with_bundle.rb +13 -0
  95. metadata +374 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 9a9bb87d164bab435ae5b5dae93c8f25003fbacf
4
+ data.tar.gz: 364c684f10939d8d693b326ebe6f6e006b69f88e
5
+ SHA512:
6
+ metadata.gz: a0dd50bd5b5ead6023050936ac7847512b8a32980d1cbc23071bdf07a7e5f266a77fabdc245956ed506c469fed3591ec9687d15f94a4d7d9de3b21eb7399c9d6
7
+ data.tar.gz: d7c521af96a68c35f539ea7c7fe1250ab6274929c5ec09742d5592ea40f23e4ff80c1cdb1bfe018c70286626a6496dadcfa5a42034401ff9cb2d8a74ba9005fb
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ spec/dummy
2
+ Gemfile.lock
3
+ coverage
4
+ .ruby-gemset
5
+ .ruby-version
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --color
2
+ --format
3
+ progress
data/.travis.yml ADDED
@@ -0,0 +1,11 @@
1
+ language: ruby
2
+ before_script:
3
+ - sh -e /etc/init.d/xvfb start
4
+ - export DISPLAY=:99.0
5
+ - bundle exec rake test_app
6
+ script:
7
+ - bundle exec rspec spec
8
+ rvm:
9
+ - 1.9.3
10
+ - 2.0.0
11
+ - 2.1.0
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ source "https://rubygems.org"
2
+
3
+ branch = '2-4-stable'
4
+ gem "solidus", github: 'solidusio/solidus', branch: 'master'
5
+ # gem 'spree_wombat', github: 'spree/spree_wombat', branch: branch
6
+
7
+ gem 'pry-rails'
8
+
9
+ gemspec
data/LICENSE.md ADDED
@@ -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._
data/README.markdown ADDED
@@ -0,0 +1,95 @@
1
+ # Product Assembly
2
+
3
+ [![Build Status](https://secure.travis-ci.org/spree-contrib/spree-product-assembly.png)](https://travis-ci.org/spree-contrib/spree-product-assembly)
4
+ [![Code Climate](https://codeclimate.com/github/spree-contrib/spree-product-assembly.png)](https://codeclimate.com/github/spree-contrib/spree-product-assembly)
5
+
6
+ Create a product which is composed of other products.
7
+
8
+ ## Installation
9
+
10
+ Add the following line to your `Gemfile`
11
+ ```ruby
12
+ gem 'spree_product_assembly', github: 'spree-contrib/spree-product-assembly', branch: 'master'
13
+ ```
14
+
15
+ Run bundle install as well as the extension intall command to copy and run migrations and
16
+ append spree_product_assembly to your js manifest file
17
+
18
+ bundle install
19
+ rails g spree_product_assembly:install
20
+
21
+ _master branch is compatible with spree edge and rails 4 only. Please use
22
+ 2-0-stable for Spree 2.0.x or 1-3-stable branch for Spree 1.3.x compatibility_
23
+
24
+ _In case you're upgrading from 1-3-stable of this extension you might want to run a
25
+ rake task which assigns a line item to your previous inventory units from bundle
26
+ products. That is so you have a better view on the current backend UI and avoid
27
+ exceptions. No need to run this task if you're not upgrading from product assembly
28
+ 1-3-stable_
29
+
30
+ rake spree_product_assembly:upgrade
31
+
32
+ ## Use
33
+
34
+ To build a bundle (assembly product) you'd need to first check the "Can be part"
35
+ flag on each product you want to be part of the bundle. Then create a product
36
+ and add parts to it. By doing that you're making that product an assembly.
37
+
38
+ The store will treat assemblies a bit different than regular products on checkout.
39
+ Spree will create and track inventory units for its parts rather than for the product itself.
40
+ That means you essentially have a product composed of other products. From a
41
+ customer perspective it's like they are paying a single amount for a collection
42
+ of products.
43
+
44
+ ## Using with spree_wombat
45
+
46
+ If you use this with spree_wombat make sure that you add this extension after
47
+ spree_wombat in your `Gemfile`
48
+
49
+ This extension provides a specific serializer for shipments `assembly_shipment_serializer`, to use this in your Spree storefront make sure
50
+ you configure spree_wombat like this:
51
+
52
+ ```ruby
53
+ config.payload_builder = {
54
+ "Spree::Shipment" => {:serializer => "Spree::Wombat::AssemblyShipmentSerializer", :root => "shipments"}
55
+ }
56
+ ```
57
+
58
+ Contributing
59
+ ------------
60
+
61
+ Spree is an open source project and we encourage contributions. Please see the [contributors guidelines][1] before contributing.
62
+
63
+ In the spirit of [free software][2], **everyone** is encouraged to help improve this project.
64
+
65
+ Here are some ways *you* can contribute:
66
+
67
+ * by using prerelease versions
68
+ * by reporting [bugs][3]
69
+ * by suggesting new features
70
+ * by writing translations
71
+ * by writing or editing documentation
72
+ * by writing specifications
73
+ * by writing code (*no patch is too small*: fix typos, add comments, clean up inconsistent whitespace)
74
+ * by refactoring code
75
+ * by resolving [issues][3]
76
+ * by reviewing patches
77
+
78
+ Starting point:
79
+
80
+ * Fork the repo
81
+ * Clone your repo
82
+ * Run `bundle install`
83
+ * Run `bundle exec rake test_app` to create the test application in `spec/test_app`
84
+ * Make your changes
85
+ * Ensure specs pass by running `bundle exec rspec spec`
86
+ * Submit your pull request
87
+
88
+ Copyright (c) 2014 [Spree Commerce Inc.][4] and [contributors][5], released under the [New BSD License][6]
89
+
90
+ [1]: http://guides.spreecommerce.com/developer/contributing.html
91
+ [2]: http://www.fsf.org/licensing/essays/free-sw.html
92
+ [3]: https://github.com/spree/spree-product-assembly/issues
93
+ [4]: https://github.com/spree
94
+ [5]: https://github.com/spree/spree-product-assembly/graphs/contributors
95
+ [6]: https://github.com/spree/spree-product-assembly/blob/master/LICENSE.md
data/README.md ADDED
@@ -0,0 +1,2 @@
1
+ # solidus_product_assembly
2
+ Completely based on spree_product_assembly
data/Rakefile ADDED
@@ -0,0 +1,15 @@
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 => [:spec]
10
+
11
+ desc "Generates a dummy app for testing"
12
+ task :test_app do
13
+ ENV['LIB_NAME'] = 'spree_product_assembly'
14
+ Rake::Task['common:test_app'].invoke
15
+ end
Binary file
@@ -0,0 +1,82 @@
1
+ #= require ./translations
2
+
3
+ $(document).ready ->
4
+ Spree.routes.available_admin_product_parts = (productSlug) ->
5
+ Spree.pathFor("admin/products/" + productSlug + "/parts/available")
6
+
7
+ showErrorMessages = (xhr) ->
8
+ response = JSON.parse(xhr.responseText)
9
+ show_flash("error", response)
10
+
11
+ partsTable = $("#product_parts")
12
+ searchResults = $("#search_hits")
13
+
14
+ searchForParts = ->
15
+ productSlug = partsTable.data("product-slug")
16
+ searchUrl = Spree.routes.available_admin_product_parts(productSlug)
17
+
18
+ $.ajax
19
+ data:
20
+ q: $("#searchtext").val()
21
+ dataType: 'html'
22
+ success: (request) ->
23
+ searchResults.html(request)
24
+ searchResults.show()
25
+ type: 'POST'
26
+ url: searchUrl
27
+
28
+ $("#searchtext").keypress (e) ->
29
+ if (e.which && e.which == 13) || (e.keyCode && e.keyCode == 13)
30
+ searchForParts()
31
+ false
32
+ else
33
+ true
34
+
35
+ $("#search_parts_button").click (e) ->
36
+ e.preventDefault()
37
+ searchForParts()
38
+
39
+ makePostRequest = (link, post_params = {}) ->
40
+ spinner = $("img.spinner", link.parent())
41
+ spinner.show()
42
+
43
+ request = $.ajax
44
+ type: "POST"
45
+ url: link.attr("href")
46
+ data: post_params
47
+ dateType: "script"
48
+ request.fail showErrorMessages
49
+ request.always -> spinner.hide()
50
+
51
+ false
52
+
53
+ searchResults.on "click", "a.add_product_part_link", (event) ->
54
+ event.preventDefault()
55
+
56
+ part = {}
57
+ link = $(this)
58
+ row = $("#" + link.data("target"))
59
+ loadingIndicator = $("img.spinner", link.parent())
60
+ quantityField = $('input:last', row)
61
+
62
+ part.count = quantityField.val()
63
+
64
+ if row.hasClass("with-variants")
65
+ selectedVariantOption = $('select option:selected', row)
66
+ part.variant_id = selectedVariantOption.val()
67
+
68
+ if selectedVariantOption.text() == Spree.translations.user_selectable
69
+ part.variant_selection_deferred = "t"
70
+ part.variant_id = link.data("master-variant-id")
71
+
72
+ else
73
+ part.variant_id = $('input[name="part[id]"]', row).val()
74
+
75
+ makePostRequest(link, {assemblies_part: part})
76
+
77
+ partsTable.on "click", "a.set_count_admin_product_part_link", ->
78
+ params = { count: $("input", $(this).parent().parent()).val() }
79
+ makePostRequest($(this), params)
80
+
81
+ partsTable.on "click", "a.remove_admin_product_part_link", ->
82
+ makePostRequest($(this))
@@ -0,0 +1,3 @@
1
+ $(document).ready(function() {
2
+ Spree.translations.user_selectable = "<%= Spree.t(:user_selectable) %>";
3
+ });
@@ -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,63 @@
1
+ class Spree::Admin::PartsController < Spree::Admin::BaseController
2
+ helper_method :product
3
+
4
+ def index
5
+ @parts = product.assemblies_parts
6
+ end
7
+
8
+ def remove
9
+ assembly_part = Spree::AssembliesPart.find(params[:id])
10
+ assembly_part.destroy
11
+ render 'spree/admin/parts/update_parts_table'
12
+ end
13
+
14
+ def set_count
15
+ save_part(existing_part_params)
16
+ end
17
+
18
+ def available
19
+ if params[:q].blank?
20
+ @available_products = []
21
+ else
22
+ query = "%#{params[:q]}%"
23
+ @available_products = Spree::Product.search_can_be_part(query)
24
+ @available_products.uniq!
25
+ end
26
+ respond_to do |format|
27
+ format.html {render :layout => false}
28
+ format.js {render :layout => false}
29
+ end
30
+ end
31
+
32
+ def create
33
+ save_part(new_part_params)
34
+ end
35
+
36
+ private
37
+
38
+ def save_part(part_params)
39
+ form = Spree::AssignPartToBundleForm.new(product, part_params)
40
+ if form.submit
41
+ render 'spree/admin/parts/update_parts_table'
42
+ else
43
+ error_message = form.errors.full_messages.to_sentence
44
+ render json: error_message.to_json, status: 422
45
+ end
46
+ end
47
+
48
+ def product
49
+ @product ||= Spree::Product.find_by(slug: params[:product_id])
50
+ end
51
+
52
+ def new_part_params
53
+ params.require(:assemblies_part).permit(
54
+ :count,
55
+ :variant_id,
56
+ :variant_selection_deferred
57
+ )
58
+ end
59
+
60
+ def existing_part_params
61
+ params.permit(:id, :count)
62
+ end
63
+ 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,33 @@
1
+ module Spree
2
+ class AssembliesPart < ActiveRecord::Base
3
+ belongs_to :assembly, class_name: "Spree::Product",
4
+ foreign_key: "assembly_id",
5
+ touch: true
6
+
7
+ belongs_to :part, class_name: "Spree::Variant", foreign_key: "part_id"
8
+
9
+ delegate :name, :sku, to: :part
10
+
11
+ after_create :set_master_unlimited_stock
12
+
13
+ def self.get(assembly_id, part_id)
14
+ find_or_initialize_by(assembly_id: assembly_id, part_id: part_id)
15
+ end
16
+
17
+ def options_text
18
+ if variant_selection_deferred?
19
+ Spree.t(:user_selectable)
20
+ else
21
+ part.options_text
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def set_master_unlimited_stock
28
+ if part.product.variants.any?
29
+ part.product.master.update_attribute :track_inventory, false
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,64 @@
1
+ module Spree
2
+ class AssignPartToBundleForm
3
+ include ActiveModel::Validations
4
+
5
+ validates :quantity, numericality: {greater_than: 0}
6
+
7
+ attr_reader :product, :part_options
8
+
9
+ def initialize(product, part_options)
10
+ @product = product
11
+ @part_options = part_options
12
+ end
13
+
14
+ def submit
15
+ if valid?
16
+ assemblies_part.update_attributes(attributes)
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def attributes
23
+ part_options.reject {|k, v| k.to_sym == :variant_id}
24
+ end
25
+
26
+ def given_id?
27
+ part_options[:id].present?
28
+ end
29
+
30
+ def product_id
31
+ product.id
32
+ end
33
+
34
+ def part_id
35
+ variant.id
36
+ end
37
+
38
+ def variant
39
+ Spree::Variant.find(part_options[:variant_id])
40
+ end
41
+
42
+ def variant_selection_deferred?
43
+ part_options[:variant_selection_deferred]
44
+ end
45
+
46
+ def quantity
47
+ part_options[:count].to_i
48
+ end
49
+
50
+ def assemblies_part
51
+ @assemblies_part ||= begin
52
+ if given_id?
53
+ Spree::AssembliesPart.find(part_options[:id])
54
+ else
55
+ Spree::AssembliesPart.find_or_initialize_by(
56
+ variant_selection_deferred: variant_selection_deferred?,
57
+ assembly_id: product_id,
58
+ part_id: part_id
59
+ )
60
+ end
61
+ end
62
+ end
63
+ end
64
+ 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,65 @@
1
+ module Spree
2
+ LineItem.class_eval do
3
+ scope :assemblies, -> { joins(product: :parts).uniq }
4
+
5
+ has_many :part_line_items, dependent: :destroy
6
+
7
+ def any_units_shipped?
8
+ inventory_units.any? { |unit| unit.shipped? }
9
+ end
10
+
11
+ # The parts that apply to this particular LineItem. Usually `product#parts`,
12
+ # but provided as a hook if you want to override and customize the parts for
13
+ # a specific LineItem.
14
+ def parts
15
+ product.parts
16
+ end
17
+
18
+ # The number of the specified variant that make up this LineItem. By
19
+ # default, calls `product#count_of`, but provided as a hook if you want to
20
+ # override and customize the parts available for a specific LineItem. Note
21
+ # that if you only customize whether a variant is included in the LineItem,
22
+ # and don't customize the quantity of that part per LineItem, you shouldn't
23
+ # need to override this method.
24
+ def count_of(variant)
25
+ product.count_of(variant)
26
+ end
27
+
28
+ def quantity_by_variant
29
+ if product.assembly?
30
+ if part_line_items.any?
31
+ quantity_with_part_line_items(quantity)
32
+ else
33
+ quantity_without_part_line_items(quantity)
34
+ end
35
+ else
36
+ { variant => quantity }
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ def update_inventory
43
+ if (changed? || target_shipment.present?) &&
44
+ order.has_checkout_step?("delivery")
45
+ if product.assembly?
46
+ OrderInventoryAssembly.new(self).verify(target_shipment)
47
+ else
48
+ OrderInventory.new(order, self).verify(target_shipment)
49
+ end
50
+ end
51
+ end
52
+
53
+ def quantity_with_part_line_items(quantity)
54
+ part_line_items.each_with_object({}) do |ap, hash|
55
+ hash[ap.variant] = ap.quantity * quantity
56
+ end
57
+ end
58
+
59
+ def quantity_without_part_line_items(quantity)
60
+ product.assemblies_parts.each_with_object({}) do |ap, hash|
61
+ hash[ap.part] = ap.count * quantity
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,35 @@
1
+ module Spree
2
+ OrderContents.class_eval do
3
+ def add_to_line_item_with_parts(variant, quantity, options = {})
4
+ add_to_line_item_without_parts(variant, quantity, options).
5
+ tap do |line_item|
6
+ populate_part_line_items(
7
+ line_item,
8
+ variant.product.assemblies_parts,
9
+ options["selected_variants"]
10
+ )
11
+ end
12
+ end
13
+ alias_method_chain :add_to_line_item, :parts
14
+
15
+ private
16
+
17
+ def populate_part_line_items(line_item, parts, selected_variants)
18
+ parts.each do |part|
19
+ line_item.part_line_items.create!(
20
+ line_item: line_item,
21
+ variant_id: variant_id_for(part, selected_variants),
22
+ quantity: part.count
23
+ )
24
+ end
25
+ end
26
+
27
+ def variant_id_for(part, selected_variants)
28
+ if part.variant_selection_deferred?
29
+ selected_variants[part.id.to_s]
30
+ else
31
+ part.part.id
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,54 @@
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
4
+ # removes 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
+ verify_parts(shipment, total_parts, existing_parts)
22
+ end
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def verify_parts(shipment, total_parts, existing_parts)
29
+ if existing_parts < total_parts
30
+ verifiy_add_to_shipment(shipment, total_parts, existing_parts)
31
+ elsif existing_parts > total_parts
32
+ verify_remove_from_shipment(shipment, total_parts, existing_parts)
33
+ end
34
+ end
35
+
36
+ def verifiy_add_to_shipment(shipment, total_parts, existing_parts)
37
+ shipment = determine_target_shipment unless shipment
38
+ add_to_shipment(shipment, total_parts - existing_parts)
39
+ end
40
+
41
+ def verify_remove_from_shipment(shipment, total_parts, existing_parts)
42
+ quantity = existing_parts - total_parts
43
+
44
+ if shipment.present?
45
+ remove_from_shipment(shipment, quantity)
46
+ else
47
+ order.shipments.each do |shpment|
48
+ break if quantity == 0
49
+ quantity -= remove_from_shipment(shpment, quantity)
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,6 @@
1
+ module Spree
2
+ class PartLineItem < ActiveRecord::Base
3
+ belongs_to :line_item
4
+ belongs_to :variant, class_name: "Spree::Variant"
5
+ end
6
+ end
@@ -0,0 +1,37 @@
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 assembly?
20
+ parts.present?
21
+ end
22
+
23
+ def count_of(variant)
24
+ ap = assemblies_part(variant)
25
+ # This checks persisted because the default count is 1
26
+ ap.persisted? ? ap.count : 0
27
+ end
28
+
29
+ def assembly_cannot_be_part
30
+ errors.add(:can_be_part, Spree.t(:assembly_cannot_be_part)) if can_be_part
31
+ end
32
+
33
+ private
34
+ def assemblies_part(variant)
35
+ Spree::AssembliesPart.get(self.id, variant.id)
36
+ end
37
+ end