solidus_product_bundle 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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
@@ -0,0 +1,12 @@
1
+ ---
2
+ fr:
3
+ spree:
4
+ available_parts: Parties disponibles
5
+ can_be_part: Peut faire partie d'un package
6
+ individual_sale: Vente individuelle
7
+ no_variants: Pas de variants
8
+ parts: Parties
9
+ assembly_cannot_be_part: ensemble ne peut pas être partie
10
+ product_bundles: Ensembles du produit
11
+ parts_included: Les pièces comprises
12
+ part_of_bundle: 'Une partie du %{sku}'
@@ -0,0 +1,12 @@
1
+ ---
2
+ ru:
3
+ spree:
4
+ available_parts: Доступные составные части
5
+ can_be_part: Может входить в состав других продуктов
6
+ individual_sale: Может продаваться отдельно
7
+ no_variants: Нет вариантов
8
+ parts: Составные части
9
+ assembly_cannot_be_part: сборка не может быть частью
10
+ product_bundles: Продукт cвязки
11
+ parts_included: 'Детали, входящие'
12
+ part_of_bundle: 'Часть пучка %{sku}'
@@ -0,0 +1,12 @@
1
+ ---
2
+ sv:
3
+ spree:
4
+ available_parts: Tillgängliga delar
5
+ can_be_part: Kan vara del
6
+ individual_sale: Individuell försäljning
7
+ no_variants: Inga varianter
8
+ parts: Delar
9
+ assembly_cannot_be_part: enheten kan inte vara en del
10
+ product_bundles: Produktpaket
11
+ parts_included: Delar som ingår
12
+ part_of_bundle: 'Del av %{sku}'
data/config/routes.rb ADDED
@@ -0,0 +1,19 @@
1
+ Spree::Core::Engine.add_routes do
2
+
3
+ namespace :admin do
4
+ resources :products do
5
+ resources :parts do
6
+ member do
7
+ post :select
8
+ post :remove
9
+ post :set_count
10
+ end
11
+ collection do
12
+ post :available
13
+ get :selected
14
+ end
15
+ end
16
+ end
17
+ end
18
+
19
+ end
@@ -0,0 +1,13 @@
1
+ class AddManyToManyRelationToProducts < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :assemblies_parts, :id => false do |t|
4
+ t.integer "assembly_id", :null => false
5
+ t.integer "part_id", :null => false
6
+ t.integer "count", :null => false, :default => 1
7
+ end
8
+ end
9
+
10
+ def self.down
11
+ drop_table :assemblies_parts
12
+ end
13
+ end
@@ -0,0 +1,27 @@
1
+ class AddPartsFieldsToProducts < ActiveRecord::Migration
2
+ def self.up
3
+ table = if table_exists?(:products)
4
+ 'products'
5
+ elsif table_exists?(:spree_products)
6
+ 'spree_products'
7
+ end
8
+
9
+ change_table(table) do |t|
10
+ t.column :can_be_part, :boolean, :default => false, :null => false
11
+ t.column :individual_sale, :boolean, :default => true, :null => false
12
+ end
13
+ end
14
+
15
+ def self.down
16
+ table = if table_exists?(:products)
17
+ 'products'
18
+ elsif table_exists?(:spree_products)
19
+ 'spree_products'
20
+ end
21
+
22
+ change_table(table) do |t|
23
+ t.remove :can_be_part
24
+ t.remove :individual_sale
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,9 @@
1
+ class NamespaceProductAssemblyForSpreeOne < ActiveRecord::Migration
2
+ def up
3
+ rename_table :assemblies_parts, :spree_assemblies_parts
4
+ end
5
+
6
+ def down
7
+ rename_table :spree_assemblies_parts, :assemblies_parts
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ class AddIdToSpreeAssembliesParts < ActiveRecord::Migration
2
+ def up
3
+ add_column :spree_assemblies_parts, :id, :primary_key
4
+ end
5
+
6
+ def down
7
+ add_column :spree_assemblies_parts, :id
8
+ end
9
+ end
@@ -0,0 +1,5 @@
1
+ class AddVariantSelectionDeferredToAssembliesParts < ActiveRecord::Migration
2
+ def change
3
+ add_column :spree_assemblies_parts, :variant_selection_deferred, :boolean
4
+ end
5
+ end
@@ -0,0 +1,9 @@
1
+ class CreatePartLineItems < ActiveRecord::Migration
2
+ def change
3
+ create_table :spree_part_line_items do |t|
4
+ t.belongs_to :line_item, null: false
5
+ t.belongs_to :variant, null: false
6
+ t.integer :quantity, default: 1
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,24 @@
1
+ module SpreeProductAssembly
2
+ module Generators
3
+ class InstallGenerator < Rails::Generators::Base
4
+ class_option :auto_run_migrations, :type => :boolean, :default => false
5
+
6
+ def add_migrations
7
+ run 'rake railties:install:migrations FROM=spree_product_assembly'
8
+ end
9
+
10
+ def add_javascripts
11
+ append_file "vendor/assets/javascripts/spree/backend/all.js", "//= require spree/backend/spree_product_assembly\n"
12
+ end
13
+
14
+ def run_migrations
15
+ run_migrations = options[:auto_run_migrations] || ['', 'y', 'Y'].include?(ask 'Would you like to run the migrations now? [Y/n]')
16
+ if run_migrations
17
+ run 'rake db:migrate'
18
+ else
19
+ puts "Skiping rake db:migrate, don't forget to run it!"
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,21 @@
1
+ module SpreeProductAssembly
2
+ class Engine < Rails::Engine
3
+ engine_name 'spree_product_assembly'
4
+
5
+ config.autoload_paths += %W(#{config.root}/lib)
6
+
7
+ def self.activate
8
+ Dir.glob(File.join(File.dirname(__FILE__), "../../app/**/*_decorator.rb")) do |c|
9
+ Rails.configuration.cache_classes ? require(c) : load(c)
10
+ end
11
+
12
+ if ::Rails::Engine.subclasses.map(&:name).include? "Spree::Wombat::Engine"
13
+ Dir.glob(File.join(File.dirname(__FILE__), "../../lib/**/*_serializer.rb")) do |serializer|
14
+ Rails.env.production? ? require(serializer) : load(serializer)
15
+ end
16
+ end
17
+ end
18
+
19
+ config.to_prepare &method(:activate).to_proc
20
+ end
21
+ end
@@ -0,0 +1,4 @@
1
+ require 'spree_core'
2
+ require 'spree_product_assembly/engine'
3
+ require 'coffee_script'
4
+ require 'sass/rails'
@@ -0,0 +1,29 @@
1
+ namespace :spree_product_assembly do
2
+ desc 'Link legacy inventory units to an order line item'
3
+ task :upgrade => :environment do
4
+ shipments = Spree::Shipment.includes(:inventory_units).where("spree_inventory_units.line_item_id IS NULL")
5
+
6
+ shipments.each do |shipment|
7
+ shipment.inventory_units.includes(:variant).group_by(&:variant).each do |variant, units|
8
+
9
+ line_item = shipment.order.line_items.detect { |line_item| line_item.variant_id == variant.id }
10
+
11
+ unless line_item
12
+
13
+ begin
14
+ master = shipment.order.products.detect { |p| variant.assemblies.include? p }.master
15
+ supposed_line_item = shipment.order.line_items.detect { |line_item| line_item.variant_id == master.id }
16
+
17
+ if supposed_line_item
18
+ Spree::InventoryUnit.where(id: units.map(&:id)).update_all "line_item_id = #{supposed_line_item.id}"
19
+ else
20
+ puts "Couldn't find a matching line item for #{variant.name}"
21
+ end
22
+ rescue
23
+ puts "Couldn't find a matching line item for #{variant.name}"
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,33 @@
1
+ Gem::Specification.new do |s|
2
+ s.platform = Gem::Platform::RUBY
3
+ s.name = 'solidus_product_bundle'
4
+ s.version = '1.0.0'
5
+ s.summary = 'Adds oportunity to make bundle of products to your Spree store'
6
+ s.description = s.summary
7
+ s.required_ruby_version = '>= 1.9.3'
8
+
9
+ s.author = 'Sapna Tomar'
10
+ s.email = 'ystomar12488@gmail.com'
11
+ s.homepage = 'https://github.com/YSTomar/solidus_product_bundle'
12
+
13
+ s.files = `git ls-files`.split("\n")
14
+ s.test_files = `git ls-files -- spec/*`.split("\n")
15
+ s.require_path = 'lib'
16
+ s.requirements << 'none'
17
+
18
+ s.add_dependency "solidus_api", [">= 1.0.0.pre", "< 2"]
19
+ s.add_dependency "solidus_backend", [">= 1.0.0.pre", "< 2"]
20
+ s.add_dependency "solidus_core", [">= 1.0.0.pre", "< 2"]
21
+
22
+ s.add_development_dependency 'rspec-rails', '~> 3.1.0'
23
+ s.add_development_dependency 'sqlite3'
24
+ s.add_development_dependency 'ffaker'
25
+ s.add_development_dependency 'factory_girl', '~> 4.4'
26
+ s.add_development_dependency 'coffee-rails', '~> 4.0.0'
27
+ s.add_development_dependency 'sass-rails', '~> 4.0.0'
28
+ s.add_development_dependency 'capybara', '~> 2.4'
29
+ s.add_development_dependency 'poltergeist', '~> 1.6'
30
+ s.add_development_dependency 'database_cleaner', '~> 1.3'
31
+ s.add_development_dependency 'simplecov'
32
+ s.add_development_dependency 'pg'
33
+ end
@@ -0,0 +1,203 @@
1
+ require "spec_helper"
2
+
3
+ describe "Adding items to the cart", type: :feature do
4
+ context "when adding a bundle to the cart" do
5
+ context "when none of the bundle items are packs or have options" do
6
+ specify "the cart lists the contents of the bundle" do
7
+ bundle = create(:product_in_stock, name: "Bundle", sku: "BUNDLE")
8
+
9
+ keychain = create(:product_in_stock, name: "Keychain",
10
+ sku: "KEYCHAIN",
11
+ can_be_part: true)
12
+ shirt = create(:product_in_stock, name: "Shirt",
13
+ sku: "SHIRT",
14
+ can_be_part: true)
15
+
16
+ add_part_to_bundle(bundle, keychain.master)
17
+ add_part_to_bundle(bundle, shirt.master)
18
+
19
+ visit spree.product_path(bundle)
20
+
21
+ click_button "add-to-cart-button"
22
+
23
+ within("#cart-detail") do
24
+ within("tbody tr:first-child") do
25
+ expect(page).to have_content(bundle.name)
26
+ expect(page).to have_css("input[value='1']")
27
+ expect(page).to have_content("(1) Keychain (KEYCHAIN)")
28
+ expect(page).to have_content("(1) Shirt (SHIRT)")
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ context "when one of the variants is a pack" do
35
+ specify "the cart displays the same quantity for part line items" do
36
+ bundle = create(:product_in_stock, name: "Bundle", sku: "BUNDLE")
37
+
38
+ keychain = create(:product_in_stock, name: "Keychain",
39
+ sku: "KEYCHAIN",
40
+ can_be_part: true)
41
+ _shirt, shirts_by_size = create_bundle_product_with_options(
42
+ name: "Shirt",
43
+ sku: "SHIRT",
44
+ option_type: "Size",
45
+ option_values: ["Small"]
46
+ )
47
+
48
+ add_part_to_bundle(bundle, keychain.master, count: 2)
49
+ add_part_to_bundle(bundle, shirts_by_size["small"])
50
+
51
+ visit spree.product_path(bundle)
52
+
53
+ click_button "add-to-cart-button"
54
+
55
+ within("#cart-detail tbody tr:first-child") do
56
+ expect(page).to have_content(bundle.name)
57
+ expect(page).to have_css("input[value='1']")
58
+ expect(page).to have_content("(2) Keychain (KEYCHAIN)")
59
+ expect(page).to have_content("(1) Shirt (Size: Small) (SHIRT-SMALL)")
60
+ end
61
+ end
62
+
63
+ context "when ordering more than one of the bundle" do
64
+ specify "the part quantity is multiplied by the bundle quantity" do
65
+ bundle = create(:product_in_stock, name: "Bundle", sku: "BUNDLE")
66
+
67
+ keychain = create(:product_in_stock, name: "Keychain",
68
+ sku: "KEYCHAIN",
69
+ can_be_part: true)
70
+ shirt = create(:product_in_stock, name: "Shirt",
71
+ sku: "SHIRT",
72
+ can_be_part: true)
73
+
74
+ add_part_to_bundle(bundle, keychain.master, count: 2)
75
+ add_part_to_bundle(bundle, shirt.master)
76
+
77
+ visit spree.product_path(bundle)
78
+
79
+ fill_in "quantity", with: 2
80
+
81
+ click_button "add-to-cart-button"
82
+
83
+ within("#cart-detail tbody tr:first-child") do
84
+ expect(page).to have_content(bundle.name)
85
+ expect(page).to have_css("input[value='2']")
86
+ expect(page).to have_content("(4) Keychain (KEYCHAIN)")
87
+ expect(page).to have_content("(2) Shirt (SHIRT)")
88
+ end
89
+ end
90
+ end
91
+ end
92
+
93
+ context "when a bundle items has a variant (that is not user-selectable)" do
94
+ specify "the cart includes the variant when listing items bundle items" do
95
+ bundle = create(:product_in_stock, name: "Bundle", sku: "BUNDLE")
96
+
97
+ keychain = create(:product_in_stock, name: "Keychain",
98
+ sku: "KEYCHAIN",
99
+ can_be_part: true)
100
+ _shirt, shirts_by_size = create_bundle_product_with_options(
101
+ name: "Shirt",
102
+ sku: "SHIRT",
103
+ option_type: "Size",
104
+ option_values: ["Small"]
105
+ )
106
+
107
+ add_part_to_bundle(bundle, keychain.master)
108
+ add_part_to_bundle(bundle, shirts_by_size["small"])
109
+
110
+ visit spree.product_path(bundle)
111
+
112
+ click_button "add-to-cart-button"
113
+
114
+ within("#cart-detail tbody tr:first-child") do
115
+ expect(page).to have_content(bundle.name)
116
+ expect(page).to have_css("input[value='1']")
117
+ expect(page).to have_content("(1) Keychain (KEYCHAIN)")
118
+ expect(page).to have_content("(1) Shirt (Size: Small) (SHIRT-SMALL)")
119
+ end
120
+ end
121
+ end
122
+
123
+ context "when one of the bundle items has a user-selectable variant" do
124
+ specify "the cart includes the variant when listing bundle items" do
125
+ bundle = create(:product_in_stock, name: "Bundle", sku: "BUNDLE")
126
+
127
+ keychain = create(:product_in_stock, name: "Keychain",
128
+ sku: "KEYCHAIN",
129
+ can_be_part: true)
130
+ shirt, _shirts_by_size = create_bundle_product_with_options(
131
+ name: "Shirt",
132
+ sku: "SHIRT",
133
+ option_type: "Size",
134
+ option_values: ["Small", "Medium"]
135
+ )
136
+
137
+ add_part_to_bundle(bundle, keychain.master, count: 1)
138
+ add_part_to_bundle(
139
+ bundle,
140
+ shirt.master,
141
+ variant_selection_deferred: true
142
+ )
143
+
144
+ visit spree.product_path(bundle)
145
+
146
+ select "Size: Medium", from: "Variant"
147
+
148
+ click_button "add-to-cart-button"
149
+
150
+ within("#cart-detail tbody tr:first-child") do
151
+ expect(page).to have_content(bundle.name)
152
+ expect(page).to have_css("input[value='1']")
153
+ expect(page).to have_content("(1) Keychain (KEYCHAIN)")
154
+ expect(page).to(
155
+ have_content("(1) Shirt (Size: Medium) (SHIRT-MEDIUM)")
156
+ )
157
+ end
158
+ end
159
+ end
160
+ end
161
+
162
+ def create_bundle_product_with_options(args)
163
+ option_type_presentation = args.fetch(:option_type)
164
+ option_value_presentations = args.fetch(:option_values)
165
+ option_values = option_value_presentations.map do |presentation|
166
+ create(:option_value, presentation: presentation)
167
+ end
168
+ option_type = create(:option_type,
169
+ presentation: option_type_presentation,
170
+ name: option_type_presentation.downcase,
171
+ option_values: option_values)
172
+ product_attributes = args.slice(:name, :sku).merge(
173
+ option_types: [option_type],
174
+ can_be_part: true
175
+ )
176
+ product = create(:product, product_attributes)
177
+
178
+ variants = variants_by_option(product, option_values)
179
+
180
+ [product, variants]
181
+ end
182
+
183
+ def variants_by_option(product, option_values)
184
+ option_values.each_with_object({}) do |value, hash|
185
+ hash[value.presentation.downcase] = create(
186
+ :variant_in_stock,
187
+ product: product,
188
+ sku: "#{product.sku}-#{value.presentation.upcase}",
189
+ option_values: [value]
190
+ )
191
+ end
192
+ end
193
+
194
+ def add_part_to_bundle(bundle, variant, options = {})
195
+ attributes = options.reverse_merge(
196
+ assembly_id: bundle.id,
197
+ part_id: variant.id,
198
+ )
199
+ create(:assemblies_part, attributes).tap do |_part|
200
+ bundle.reload
201
+ end
202
+ end
203
+ end
@@ -0,0 +1,29 @@
1
+ require 'spec_helper'
2
+
3
+ describe "Orders", type: :feature, js: true do
4
+ stub_authorization!
5
+
6
+ let(:order) { create(:order_with_line_items) }
7
+ let(:line_item) { order.line_items.first }
8
+ let(:bundle) { line_item.product }
9
+ let(:parts) { (1..3).map { create(:variant) } }
10
+
11
+ before do
12
+ bundle.parts << [parts]
13
+ line_item.update_attributes!(quantity: 3)
14
+ order.reload.create_proposed_shipments
15
+ order.finalize!
16
+ end
17
+
18
+ it "allows admin to edit product bundle" do
19
+ visit spree.edit_admin_order_path(order)
20
+
21
+ within("table.product-bundles") do
22
+ find(".edit-line-item").click
23
+ fill_in "quantity", :with => "2"
24
+ find(".save-line-item").click
25
+
26
+ sleep(1) # avoid odd "cannot rollback - no transaction is active: rollback transaction"
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,183 @@
1
+ require 'spec_helper'
2
+
3
+ describe "Managing parts for a product bundle", type: :feature, js: true do
4
+ stub_authorization!
5
+
6
+ let!(:tshirt) { create(:product, :name => "T-Shirt") }
7
+ let!(:mug) { create(:product, :name => "Mug", can_be_part: true) }
8
+
9
+ context "when searching for parts" do
10
+ before do
11
+ visit spree.admin_product_path(tshirt)
12
+ click_on "Parts"
13
+ end
14
+
15
+ it "returns empty results when there is no query" do
16
+ fill_in "searchtext", with: ""
17
+ click_on "Search"
18
+
19
+ page.should have_content("No Match Found.")
20
+ end
21
+
22
+ it "displays no-match feedback when it does not find any products" do
23
+ fill_in "searchtext", with: "Foo"
24
+ click_on "Search"
25
+
26
+ page.should have_content("No Match Found.")
27
+ end
28
+
29
+ it "shows any products that were found" do
30
+ fill_in "searchtext", with: mug.name
31
+ click_on "Search"
32
+
33
+ page.should have_content(mug.name)
34
+ end
35
+ end
36
+
37
+ context "when adding parts to a bundle" do
38
+ it "allows adding a product with no variants" do
39
+ visit spree.admin_product_path(tshirt)
40
+ click_on "Parts"
41
+ fill_in "searchtext", with: mug.name
42
+ click_on "Search"
43
+
44
+ within("#search_hits") { click_on "Select" }
45
+ page.should have_content(mug.sku)
46
+ end
47
+
48
+ context "when a part has multiple variants" do
49
+ def build_option(options)
50
+ option_type_name = options.fetch(:type)
51
+ option_type = create(:option_type,
52
+ presentation: option_type_name,
53
+ name: option_type_name
54
+ )
55
+ option_value = options.fetch(:value)
56
+ option_type.option_values.create(
57
+ name: option_value.downcase,
58
+ presentation: option_value
59
+ )
60
+
61
+ option_type
62
+ end
63
+
64
+ def build_part_with_options(product_name, option_type)
65
+ product = create(:product,
66
+ can_be_part: true,
67
+ name: product_name,
68
+ option_types: [option_type]
69
+ )
70
+ create(:variant,
71
+ product: product,
72
+ option_values: option_type.option_values
73
+ )
74
+ end
75
+
76
+ it "allows a specific variant to be selected as part of the bundle" do
77
+ bundle = create(:product)
78
+ option = build_option(type: "Color", value: "Red")
79
+ part = build_part_with_options("Shirt", option)
80
+
81
+ visit spree.admin_product_path(bundle)
82
+ click_on "Parts"
83
+ fill_in "searchtext", with: "Shirt"
84
+ click_on "Search"
85
+
86
+ within("#search_hits") do
87
+ select "Color: Red", from: "part_id"
88
+ click_on "Select"
89
+ end
90
+
91
+ page.should have_content(part.sku)
92
+ end
93
+
94
+ it "allows admin to specify that user can select any variant" do
95
+ bundle = create(:product)
96
+ option = build_option(type: "Color", value: "Red")
97
+ part = build_part_with_options("Shirt", option)
98
+
99
+ visit spree.admin_product_path(bundle)
100
+ click_on "Parts"
101
+ fill_in "searchtext", with: "Shirt"
102
+ click_on "Search"
103
+
104
+ within("#search_hits") do
105
+ select Spree.t(:user_selectable), from: "part_id"
106
+ fill_in "part_count", with: 666
107
+ click_on "Select"
108
+ end
109
+
110
+ within("#product_parts") do
111
+ page.should have_content("Shirt")
112
+ page.should have_content(part.product.sku)
113
+ page.should have_content(Spree.t(:user_selectable))
114
+
115
+ input = find_field("count")
116
+ input[:value].should eq("666")
117
+ end
118
+ end
119
+ end
120
+ end
121
+
122
+ it "allows parts to be removed from the bundle" do
123
+ visit spree.admin_product_path(tshirt)
124
+ click_on "Parts"
125
+ fill_in "searchtext", with: mug.name
126
+ click_on "Search"
127
+
128
+ within("#search_hits") { click_on "Select" }
129
+ page.should have_content(mug.sku)
130
+
131
+ within("#product_parts") do
132
+ find(".remove_admin_product_part_link").click
133
+
134
+ page.should_not have_content(mug.sku)
135
+ end
136
+ end
137
+
138
+ context "when updating part quantities" do
139
+ before do
140
+ visit spree.admin_product_path(tshirt)
141
+ click_on "Parts"
142
+ fill_in "searchtext", with: mug.name
143
+ click_on "Search"
144
+ within("#search_hits") { click_on "Select" }
145
+ end
146
+
147
+ it "updates the quantity to match the newly-supplied value" do
148
+ within("#product_parts") do
149
+ fill_in "count", with: "5"
150
+ find(".set_count_admin_product_part_link").click
151
+
152
+ expect(find_field('count').value).to eq "5"
153
+ end
154
+ end
155
+
156
+ it "rejects a negative quantity" do
157
+ within("#product_parts") do
158
+ fill_in "count", with: "-1"
159
+ find(".set_count_admin_product_part_link").click
160
+ end
161
+
162
+ expect(page).to have_content("Quantity must be greater than 0")
163
+ end
164
+
165
+ it "rejects a part quantity of `0`" do
166
+ within("#product_parts") do
167
+ fill_in "count", with: "0"
168
+ find(".set_count_admin_product_part_link").click
169
+ end
170
+
171
+ expect(page).to have_content("Quantity must be greater than 0")
172
+ end
173
+
174
+ it "rejects a non-numeric part quantity" do
175
+ within("#product_parts") do
176
+ fill_in "count", with: "non-numeric"
177
+ find(".set_count_admin_product_part_link").click
178
+ end
179
+
180
+ expect(page).to have_content("Quantity must be greater than 0")
181
+ end
182
+ end
183
+ end