solidus_product_bundle 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|