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.
- checksums.yaml +7 -0
- data/.gitignore +5 -0
- data/.rspec +3 -0
- data/.travis.yml +11 -0
- data/Gemfile +9 -0
- data/LICENSE.md +13 -0
- data/README.markdown +95 -0
- data/README.md +2 -0
- data/Rakefile +15 -0
- data/app/assets/images/spinner.gif +0 -0
- data/app/assets/javascripts/spree/backend/spree_product_assembly/index.js.coffee +82 -0
- data/app/assets/javascripts/spree/backend/spree_product_assembly/translations.js.erb +3 -0
- data/app/assets/javascripts/spree/frontend/spree_product_assembly.js +1 -0
- data/app/assets/stylesheets/spree/backend/spree_product_assembly.css +3 -0
- data/app/assets/stylesheets/spree/frontend/spree_product_assembly.css +3 -0
- data/app/controllers/spree/admin/parts_controller.rb +63 -0
- data/app/controllers/spree/checkout_controller_decorator.rb +9 -0
- data/app/helpers/spree/admin/orders_helper_decorator.rb +9 -0
- data/app/models/spree/assemblies_part.rb +33 -0
- data/app/models/spree/assign_part_to_bundle_form.rb +64 -0
- data/app/models/spree/inventory_unit_decorator.rb +13 -0
- data/app/models/spree/line_item_decorator.rb +65 -0
- data/app/models/spree/order_contents_decorator.rb +35 -0
- data/app/models/spree/order_inventory_assembly.rb +54 -0
- data/app/models/spree/part_line_item.rb +6 -0
- data/app/models/spree/product_decorator.rb +37 -0
- data/app/models/spree/shipment_decorator.rb +50 -0
- data/app/models/spree/stock/availability_validator.rb +27 -0
- data/app/models/spree/stock/inventory_unit_builder_decorator.rb +30 -0
- data/app/models/spree/variant_decorator.rb +13 -0
- data/app/overrides/add_admin_product_form_fields.rb +5 -0
- data/app/overrides/add_admin_tabs.rb +5 -0
- data/app/overrides/add_line_item_description.rb +5 -0
- data/app/overrides/spree/admin/orders/_form/inject_product_assemblies.html.erb.deface +3 -0
- data/app/overrides/spree/admin/orders/_shipment/stock_contents.html.erb.deface +2 -0
- data/app/overrides/spree/checkout/_delivery/remove_unshippable_markup.html.erb.deface +1 -0
- data/app/overrides/spree/checkout/_delivery/render_line_item_manifest.html.erb.deface +3 -0
- data/app/overrides/spree/products/add_links_to_parts.rb +6 -0
- data/app/overrides/spree/products/show/remove_add_to_cart_button_for_non_individual_sale_products.html.erb.deface +4 -0
- data/app/overrides/spree/shared/_order_details/part_description.html.erb.deface +16 -0
- data/app/serializers/spree/wombat/assembly_shipment_serializer.rb +37 -0
- data/app/views/spree/admin/orders/_assemblies.html.erb +62 -0
- data/app/views/spree/admin/orders/_stock_contents.html.erb +69 -0
- data/app/views/spree/admin/orders/_stock_item.html.erb +46 -0
- data/app/views/spree/admin/parts/_parts_table.html.erb +33 -0
- data/app/views/spree/admin/parts/available.html.erb +43 -0
- data/app/views/spree/admin/parts/index.html.erb +19 -0
- data/app/views/spree/admin/parts/update_parts_table.js.erb +2 -0
- data/app/views/spree/admin/products/_product_assembly_fields.html.erb +23 -0
- data/app/views/spree/admin/shared/_product_assembly_product_tabs.html.erb +3 -0
- data/app/views/spree/checkout/_line_item_manifest.html.erb +17 -0
- data/app/views/spree/orders/_cart_description.html.erb +16 -0
- data/app/views/spree/products/show/_parts.html.erb +38 -0
- data/bin/rails +7 -0
- data/config/locales/en.yml +15 -0
- data/config/locales/fr.yml +12 -0
- data/config/locales/ru.yml +12 -0
- data/config/locales/sv.yml +12 -0
- data/config/routes.rb +19 -0
- data/db/migrate/20091028152124_add_many_to_many_relation_to_products.rb +13 -0
- data/db/migrate/20091029165620_add_parts_fields_to_products.rb +27 -0
- data/db/migrate/20120316141830_namespace_product_assembly_for_spree_one.rb +9 -0
- data/db/migrate/20140620223938_add_id_to_spree_assemblies_parts.rb +9 -0
- data/db/migrate/20150219192418_add_variant_selection_deferred_to_assemblies_parts.rb +5 -0
- data/db/migrate/20150303105615_create_part_line_items.rb +9 -0
- data/lib/generators/spree_product_assembly/install/install_generator.rb +24 -0
- data/lib/spree_product_assembly/engine.rb +21 -0
- data/lib/spree_product_assembly.rb +4 -0
- data/lib/tasks/spree2_upgrade.rake +29 -0
- data/solidus_product_bundle.gemspec +33 -0
- data/spec/features/adding_items_to_cart_spec.rb +203 -0
- data/spec/features/admin/orders_spec.rb +29 -0
- data/spec/features/admin/parts_spec.rb +183 -0
- data/spec/features/checkout_spec.rb +249 -0
- data/spec/features/updating_items_in_cart_spec.rb +199 -0
- data/spec/models/spree/assemblies_part_spec.rb +18 -0
- data/spec/models/spree/assign_part_to_bundle_form_spec.rb +51 -0
- data/spec/models/spree/inventory_unit_spec.rb +32 -0
- data/spec/models/spree/line_item_spec.rb +88 -0
- data/spec/models/spree/order_contents_spec.rb +82 -0
- data/spec/models/spree/order_inventory_assembly_spec.rb +321 -0
- data/spec/models/spree/order_inventory_spec.rb +34 -0
- data/spec/models/spree/product_spec.rb +40 -0
- data/spec/models/spree/shipment_spec.rb +113 -0
- data/spec/models/spree/stock/availability_validator_spec.rb +71 -0
- data/spec/models/spree/stock/coordinator_spec.rb +46 -0
- data/spec/models/spree/stock/inventory_unit_builder_spec.rb +36 -0
- data/spec/models/spree/variant_spec.rb +28 -0
- data/spec/serializers/spree/wombat/assembly_shipment_serializer_spec.rb +36 -0
- data/spec/spec_helper.rb +56 -0
- data/spec/support/factories/assemblies_part_factory.rb +10 -0
- data/spec/support/factories/part_line_item_factory.rb +9 -0
- data/spec/support/factories/variant_factory.rb +15 -0
- data/spec/support/shared_contexts/order_with_bundle.rb +13 -0
- 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,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,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
|