solidus_product_assembly 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (82) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/.rspec +3 -0
  4. data/.travis.yml +21 -0
  5. data/Gemfile +18 -0
  6. data/LICENSE.md +13 -0
  7. data/README.md +80 -0
  8. data/Rakefile +21 -0
  9. data/app/assets/images/spinner.gif +0 -0
  10. data/app/assets/javascripts/spree/backend/solidus_product_assembly.js +1 -0
  11. data/app/assets/javascripts/spree/frontend/solidus_product_assembly.js +1 -0
  12. data/app/assets/stylesheets/spree/backend/solidus_product_assembly.css +3 -0
  13. data/app/assets/stylesheets/spree/frontend/solidus_product_assembly.css +3 -0
  14. data/app/controllers/spree/admin/parts_controller.rb +49 -0
  15. data/app/controllers/spree/checkout_controller_decorator.rb +9 -0
  16. data/app/helpers/spree/admin/orders_helper_decorator.rb +9 -0
  17. data/app/models/spree/assemblies_part.rb +12 -0
  18. data/app/models/spree/inventory_unit_decorator.rb +13 -0
  19. data/app/models/spree/line_item_decorator.rb +44 -0
  20. data/app/models/spree/order_inventory_assembly.rb +39 -0
  21. data/app/models/spree/product_decorator.rb +56 -0
  22. data/app/models/spree/shipment_decorator.rb +50 -0
  23. data/app/models/spree/stock/availability_validator.rb +28 -0
  24. data/app/models/spree/stock/inventory_unit_builder_decorator.rb +30 -0
  25. data/app/models/spree/stock/inventory_validator_decorator.rb +12 -0
  26. data/app/models/spree/variant_decorator.rb +13 -0
  27. data/app/overrides/add_admin_product_form_fields.rb +5 -0
  28. data/app/overrides/add_admin_tabs.rb +5 -0
  29. data/app/overrides/add_line_item_description.rb +5 -0
  30. data/app/overrides/spree/admin/orders/_form/inject_product_assemblies.html.erb.deface +3 -0
  31. data/app/overrides/spree/admin/orders/_shipment/stock_contents.html.erb.deface +2 -0
  32. data/app/overrides/spree/checkout/_delivery/remove_unshippable_markup.html.erb.deface +1 -0
  33. data/app/overrides/spree/checkout/_delivery/render_line_item_manifest.html.erb.deface +3 -0
  34. data/app/overrides/spree/products/show/add_links_to_parts.html.erb.deface +21 -0
  35. data/app/overrides/spree/products/show/remove_add_to_cart_button_for_non_individual_sale_products.html.erb.deface +4 -0
  36. data/app/overrides/spree/shared/_order_details/part_description.html.erb.deface +8 -0
  37. data/app/views/spree/admin/orders/_assemblies.html.erb +56 -0
  38. data/app/views/spree/admin/orders/_stock_contents.html.erb +69 -0
  39. data/app/views/spree/admin/orders/_stock_item.html.erb +46 -0
  40. data/app/views/spree/admin/parts/_parts_table.html.erb +33 -0
  41. data/app/views/spree/admin/parts/available.html.erb +55 -0
  42. data/app/views/spree/admin/parts/available.js.erb +54 -0
  43. data/app/views/spree/admin/parts/index.html.erb +70 -0
  44. data/app/views/spree/admin/parts/update_parts_table.js.erb +2 -0
  45. data/app/views/spree/admin/products/_product_assembly_fields.html.erb +23 -0
  46. data/app/views/spree/admin/shared/_product_assembly_product_tabs.html.erb +3 -0
  47. data/app/views/spree/checkout/_line_item_manifest.html.erb +17 -0
  48. data/app/views/spree/orders/_cart_description.html.erb +8 -0
  49. data/bin/rails +7 -0
  50. data/config/locales/en.yml +14 -0
  51. data/config/locales/fr.yml +12 -0
  52. data/config/locales/ru.yml +12 -0
  53. data/config/locales/sv.yml +12 -0
  54. data/config/routes.rb +19 -0
  55. data/db/migrate/20091028152124_add_many_to_many_relation_to_products.rb +13 -0
  56. data/db/migrate/20091029165620_add_parts_fields_to_products.rb +27 -0
  57. data/db/migrate/20120316141830_namespace_product_assembly_for_spree_one.rb +9 -0
  58. data/db/migrate/20140620223938_add_id_to_spree_assemblies_parts.rb +9 -0
  59. data/lib/generators/solidus_product_assembly/install/install_generator.rb +24 -0
  60. data/lib/solidus_product_assembly.rb +3 -0
  61. data/lib/solidus_product_assembly/engine.rb +15 -0
  62. data/lib/solidus_product_assembly/version.rb +3 -0
  63. data/lib/tasks/spree2_upgrade.rake +29 -0
  64. data/solidus_product_assembly.gemspec +35 -0
  65. data/spec/features/admin/orders_spec.rb +29 -0
  66. data/spec/features/admin/parts_spec.rb +28 -0
  67. data/spec/features/checkout_spec.rb +92 -0
  68. data/spec/models/spree/assemblies_part_spec.rb +18 -0
  69. data/spec/models/spree/inventory_unit_spec.rb +32 -0
  70. data/spec/models/spree/line_item_spec.rb +72 -0
  71. data/spec/models/spree/order_contents_spec.rb +40 -0
  72. data/spec/models/spree/order_inventory_assembly_spec.rb +55 -0
  73. data/spec/models/spree/order_inventory_spec.rb +35 -0
  74. data/spec/models/spree/product_spec.rb +35 -0
  75. data/spec/models/spree/shipment_spec.rb +112 -0
  76. data/spec/models/spree/stock/availability_validator_spec.rb +71 -0
  77. data/spec/models/spree/stock/coordinator_spec.rb +52 -0
  78. data/spec/models/spree/stock/inventory_unit_builder_spec.rb +36 -0
  79. data/spec/models/spree/variant_spec.rb +28 -0
  80. data/spec/spec_helper.rb +57 -0
  81. data/spec/support/shared_contexts/order_with_bundle.rb +13 -0
  82. metadata +272 -0
@@ -0,0 +1,92 @@
1
+ require 'spec_helper'
2
+
3
+ describe "Checkout", type: :feature do
4
+ let!(:store) { create :store, default: true }
5
+ let!(:country) { create(:country, :name => "United States", :states_required => true) }
6
+ let!(:state) { create(:state, :name => "Ohio", :country => country) }
7
+ let!(:shipping_method) { create(:shipping_method) }
8
+ let!(:stock_location) { create(:stock_location) }
9
+ let!(:payment_method) { create(:check_payment_method) }
10
+ let!(:zone) { create(:zone) }
11
+
12
+ let(:product) { create(:product, :name => "RoR Mug") }
13
+ let(:variant) { create(:variant) }
14
+
15
+ stub_authorization!
16
+
17
+ before { product.parts << variant << create(:variant) }
18
+
19
+ shared_context "purchases product with part included" do
20
+ before do
21
+ add_product_to_cart
22
+ click_button "Checkout"
23
+
24
+ fill_in "order_email", :with => "ryan@spreecommerce.com"
25
+ click_button "Continue"
26
+ fill_in_address
27
+
28
+ click_button "Save and Continue"
29
+ expect(current_path).to eql(spree.checkout_state_path("delivery"))
30
+ page.should have_content(variant.product.name)
31
+
32
+ click_button "Save and Continue"
33
+ expect(current_path).to eql(spree.checkout_state_path("payment"))
34
+
35
+ click_button "Save and Continue"
36
+ expect(current_path).to eq spree.checkout_state_path('confirm')
37
+ page.should have_content(variant.product.name)
38
+
39
+ click_button "Place Order"
40
+ page.should have_content('Your order has been processed successfully')
41
+ end
42
+ end
43
+
44
+ context "backend order shipments UI", js: true do
45
+
46
+ context "ordering only the product assembly" do
47
+ include_context "purchases product with part included"
48
+
49
+ it "views parts bundled as well" do
50
+ visit spree.admin_orders_path
51
+ click_on Spree::Order.last.number
52
+
53
+ page.should have_content(variant.product.name)
54
+ end
55
+ end
56
+
57
+ context "ordering assembly and the part as individual sale" do
58
+ before do
59
+ visit spree.root_path
60
+ click_link variant.product.name
61
+ click_button "add-to-cart-button"
62
+ end
63
+ include_context "purchases product with part included"
64
+
65
+ it "views parts bundled and not" do
66
+ visit spree.admin_orders_path
67
+ click_on Spree::Order.last.number
68
+
69
+ page.should have_content(variant.product.name)
70
+ end
71
+ end
72
+
73
+ end
74
+
75
+
76
+ def fill_in_address
77
+ address = "order_bill_address_attributes"
78
+ fill_in "#{address}_firstname", :with => "Ryan"
79
+ fill_in "#{address}_lastname", :with => "Bigg"
80
+ fill_in "#{address}_address1", :with => "143 Swan Street"
81
+ fill_in "#{address}_city", :with => "Richmond"
82
+ select "Ohio", :from => "#{address}_state_id"
83
+ fill_in "#{address}_zipcode", :with => "12345"
84
+ fill_in "#{address}_phone", :with => "(555) 555-5555"
85
+ end
86
+
87
+ def add_product_to_cart
88
+ visit spree.root_path
89
+ click_link product.name
90
+ click_button "add-to-cart-button"
91
+ end
92
+ end
@@ -0,0 +1,18 @@
1
+ require 'spec_helper'
2
+
3
+ module Spree
4
+ describe AssembliesPart do
5
+ let(:product) { create(:product) }
6
+ let(:variant) { create(:variant) }
7
+
8
+ before do
9
+ product.parts.push variant
10
+ end
11
+
12
+ context "get" do
13
+ it "brings part by product and variant id" do
14
+ subject.class.get(product.id, variant.id).part.should == variant
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,32 @@
1
+ require 'spec_helper'
2
+
3
+ module Spree
4
+ describe InventoryUnit do
5
+ let!(:order) { create(:order_with_line_items) }
6
+ let(:line_item) { order.line_items.first }
7
+ let(:product) { line_item.product }
8
+
9
+ subject { InventoryUnit.create(line_item: line_item, variant: line_item.variant, order: order) }
10
+
11
+ context 'if the unit is not part of an assembly' do
12
+ it 'it will return the percentage of a line item' do
13
+ expect(subject.percentage_of_line_item).to eql(BigDecimal.new(1))
14
+ end
15
+ end
16
+
17
+ context 'if part of an assembly' do
18
+ let(:parts) { (1..2).map { create(:variant) } }
19
+
20
+ before do
21
+ product.parts << parts
22
+ order.create_proposed_shipments
23
+ order.finalize!
24
+ end
25
+
26
+ it 'it will return the percentage of a line item' do
27
+ subject.line_item = line_item
28
+ expect(subject.percentage_of_line_item).to eql(BigDecimal.new(0.5, 2))
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,72 @@
1
+ require 'spec_helper'
2
+
3
+ module Spree
4
+ describe LineItem do
5
+ let!(:order) { create(:order_with_line_items) }
6
+ let(:line_item) { order.line_items.first }
7
+ let(:product) { line_item.product }
8
+ let(:variant) { line_item.variant }
9
+ let(:inventory) { double('order_inventory') }
10
+
11
+ context "bundle parts stock" do
12
+ let(:parts) { (1..2).map { create(:variant) } }
13
+
14
+ before { product.parts << parts }
15
+
16
+ context "one of them not in stock" do
17
+ before do
18
+ part = product.parts.first
19
+ part.stock_items.update_all backorderable: false
20
+
21
+ expect(part).not_to be_in_stock
22
+ end
23
+
24
+ it "doesn't save line item quantity" do
25
+ order.contents.add(variant, 10)
26
+ expect { order.contents.advance }.to raise_error Spree::Order::InsufficientStock
27
+ end
28
+ end
29
+
30
+ context "in stock" do
31
+ before do
32
+ parts.each do |part|
33
+ part.stock_items.first.set_count_on_hand(10)
34
+ end
35
+ expect(parts[0]).to be_in_stock
36
+ expect(parts[1]).to be_in_stock
37
+ end
38
+
39
+ it "saves line item quantity" do
40
+ line_item = order.contents.add(variant, 10)
41
+ expect(line_item).to be_valid
42
+ end
43
+ end
44
+ end
45
+
46
+ context "updates bundle product line item" do
47
+ let(:parts) { (1..2).map { create(:variant) } }
48
+
49
+ before do
50
+ product.parts << parts
51
+ order.create_proposed_shipments
52
+ order.finalize!
53
+ end
54
+
55
+ it "verifies inventory units via OrderInventoryAssembly" do
56
+ OrderInventoryAssembly.should_receive(:new).with(line_item).and_return(inventory)
57
+ inventory.should_receive(:verify).with(line_item.target_shipment)
58
+ line_item.quantity = 2
59
+ line_item.save
60
+ end
61
+ end
62
+
63
+ context "updates regular line item" do
64
+ it "verifies inventory units via OrderInventory" do
65
+ OrderInventory.should_receive(:new).with(line_item.order, line_item).and_return(inventory)
66
+ inventory.should_receive(:verify).with(line_item.target_shipment)
67
+ line_item.quantity = 2
68
+ line_item.save
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,40 @@
1
+ require 'spec_helper'
2
+
3
+ module Spree
4
+ describe OrderContents do
5
+ let!(:store) { create :store }
6
+ let(:order) { Order.create }
7
+
8
+ let(:guitar) { create(:variant) }
9
+ let(:bass) { create(:variant) }
10
+
11
+ let(:bundle) { create(:product) }
12
+
13
+ subject { OrderContents.new(order) }
14
+
15
+ before { bundle.parts.push [guitar, bass] }
16
+
17
+ context "same variant within bundle and as regular product" do
18
+ let!(:guitar_item) { subject.add(guitar, 3) }
19
+ let!(:bundle_item) { subject.add(bundle.master, 5) }
20
+
21
+ it "destroys the variant as regular product only" do
22
+ subject.remove(guitar, 3)
23
+ expect(order.reload.line_items.to_a).to eq [bundle_item]
24
+ end
25
+
26
+ context "completed order" do
27
+ before do
28
+ order.create_proposed_shipments
29
+ order.touch :completed_at
30
+ end
31
+
32
+ it "destroys accurate number of inventory units" do
33
+ expect {
34
+ subject.remove(guitar, 3)
35
+ }.to change { InventoryUnit.count }.by(-3)
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,55 @@
1
+ require 'spec_helper'
2
+
3
+ module Spree
4
+ describe OrderInventoryAssembly do
5
+ let(:order) { create(:order_with_line_items) }
6
+ let(:line_item) { order.line_items.first }
7
+ let(:bundle) { line_item.product }
8
+ let(:parts) { (1..3).map { create(:variant) } }
9
+
10
+ before do
11
+ bundle.parts << [parts]
12
+ bundle.set_part_count(parts.first, 3)
13
+
14
+ line_item.update_attributes!(quantity: 3)
15
+ order.reload.create_proposed_shipments
16
+ order.finalize!
17
+ end
18
+
19
+ subject { OrderInventoryAssembly.new(line_item) }
20
+
21
+ context "inventory units count" do
22
+ it "calculates the proper value for the bundle" do
23
+ expected_units_count = line_item.quantity * bundle.assemblies_parts.to_a.sum(&:count)
24
+ expect(subject.inventory_units.count).to eql(expected_units_count)
25
+ end
26
+ end
27
+
28
+ context "verify line item units" do
29
+ let!(:original_units_count) { subject.inventory_units.count }
30
+
31
+ context "quantity increases" do
32
+ before { subject.line_item.quantity += 1 }
33
+
34
+ it "inserts new inventory units for every bundle part" do
35
+ expected_units_count = original_units_count + bundle.assemblies_parts.to_a.sum(&:count)
36
+ subject.verify
37
+ expect(OrderInventoryAssembly.new(line_item.reload).inventory_units.count).to eql(expected_units_count)
38
+ end
39
+ end
40
+
41
+ context "quantity decreases" do
42
+ before { subject.line_item.quantity -= 1 }
43
+
44
+ it "remove inventory units for every bundle part" do
45
+ expected_units_count = original_units_count - bundle.assemblies_parts.to_a.sum(&:count)
46
+ subject.verify
47
+
48
+ # needs to reload so that inventory units are fetched from updates order.shipments
49
+ updated_units_count = OrderInventoryAssembly.new(line_item.reload).inventory_units.count
50
+ expect(updated_units_count).to eql(expected_units_count)
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,35 @@
1
+ require 'spec_helper'
2
+
3
+ module Spree
4
+ describe OrderInventory do
5
+ let!(:store) { create :store }
6
+ let(:order) { Order.create }
7
+
8
+ subject { OrderInventory.new(order, order.line_items.first) }
9
+
10
+ context "same variant within bundle and as regular product" do
11
+ let(:contents) { OrderContents.new(order) }
12
+ let(:guitar) { create(:variant) }
13
+ let(:bass) { create(:variant) }
14
+
15
+ let(:bundle) { create(:product) }
16
+
17
+ before { bundle.parts.push [guitar, bass] }
18
+
19
+ let!(:bundle_item) { contents.add(bundle.master, 5) }
20
+ let!(:guitar_item) { contents.add(guitar, 3) }
21
+
22
+ let!(:shipment) { order.create_proposed_shipments.first }
23
+
24
+ context "completed order" do
25
+ before { order.touch :completed_at }
26
+
27
+ it "removes only units associated with provided line item" do
28
+ expect {
29
+ subject.send(:remove_from_shipment, shipment, 5)
30
+ }.not_to change { bundle_item.inventory_units.count }
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,35 @@
1
+ require 'spec_helper'
2
+
3
+ describe Spree::Product do
4
+ before(:each) do
5
+ @product = FactoryGirl.create(:product, :name => "Foo Bar")
6
+ @master_variant = Spree::Variant.where(is_master: true).find_by_product_id(@product.id)
7
+ end
8
+
9
+ describe "Spree::Product Assembly" do
10
+ before(:each) do
11
+ @product = create(:product)
12
+ @part1 = create(:product, :can_be_part => true)
13
+ @part2 = create(:product, :can_be_part => true)
14
+ @product.add_part @part1.master, 1
15
+ @product.add_part @part2.master, 4
16
+ end
17
+
18
+ it "is an assembly" do
19
+ @product.should be_assembly
20
+ end
21
+
22
+
23
+ it "cannot be part" do
24
+ @product.should be_assembly
25
+ @product.can_be_part = true
26
+ @product.valid?
27
+ @product.errors[:can_be_part].should == ["assembly can't be part"]
28
+ end
29
+
30
+ it 'changing part qty changes count on_hand' do
31
+ @product.set_part_count(@part2.master, 2)
32
+ @product.count_of(@part2.master).should == 2
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,112 @@
1
+ require 'spec_helper'
2
+
3
+ module Spree
4
+ describe Shipment do
5
+ context "order has one product assembly" do
6
+ let(:order) { Order.create }
7
+ let(:bundle) { create(:variant) }
8
+ let!(:parts) { (1..2).map { create(:variant) } }
9
+ let!(:bundle_parts) { bundle.product.parts << parts }
10
+
11
+ let!(:line_item) { order.contents.add(bundle, 1) }
12
+ let!(:shipment) { order.create_proposed_shipments.first }
13
+
14
+ before { order.update_column :state, 'complete' }
15
+
16
+ it "shipment item cost equals line item amount" do
17
+ expect(shipment.item_cost).to eq line_item.amount
18
+ end
19
+ end
20
+
21
+ context "manifests" do
22
+ include_context "product is ordered as individual and within a bundle"
23
+
24
+ let(:shipments) { order.create_proposed_shipments }
25
+
26
+ context "default" do
27
+ let(:expected_variants) { order.variants - [bundle_variant] + bundle.parts }
28
+
29
+ it "separates variant purchased individually from the bundle one" do
30
+ expect(shipments.count).to eql 1
31
+ shipments.first.manifest.map(&:variant).sort.should == expected_variants.sort
32
+ end
33
+ end
34
+
35
+ context "line items manifest" do
36
+ let(:expected_variants) { order.variants }
37
+
38
+ it "groups units by line_item only" do
39
+ expect(shipments.count).to eql 1
40
+ shipments.first.line_item_manifest.map(&:variant).sort.should == expected_variants.sort
41
+ end
42
+ end
43
+
44
+ context "units are not associated with a line item" do
45
+ let(:order) { create(:shipped_order) }
46
+ let(:shipment) { order.shipments.first }
47
+
48
+ it "searches for line item if inventory unit doesn't have one" do
49
+ shipment.manifest.last.line_item.should_not be_blank
50
+ end
51
+ end
52
+ end
53
+
54
+ context "set up new inventory units" do
55
+ let(:line_item) { create(:line_item) }
56
+ let(:variant) { line_item.variant }
57
+ let(:order) { line_item.order }
58
+ let(:shipment) { create(:shipment) }
59
+
60
+ it "assigns variant, order and line_item" do
61
+ unit = shipment.set_up_inventory('on_hand', variant, order, line_item)
62
+
63
+ expect(unit.line_item).to eq line_item
64
+ expect(unit.variant).to eq variant
65
+ expect(unit.order).to eq order
66
+ expect(unit.state).to eq 'on_hand'
67
+ end
68
+ end
69
+
70
+ context "unit states for variant sold as part of an assembly and separately" do
71
+ let(:assembly_line_item) { create(:line_item) }
72
+ let(:shirt) { create(:variant) }
73
+
74
+ let(:assembly_shirts) do
75
+ 5.times.map {
76
+ create(:inventory_unit,
77
+ variant: shirt,
78
+ line_item: assembly_line_item,
79
+ state: :on_hand)
80
+ }
81
+ end
82
+
83
+ let(:standalone_line_item) { create(:line_item, variant: shirt) }
84
+
85
+ let(:standalone_shirts) do
86
+ 2.times.map {
87
+ create(:inventory_unit,
88
+ variant: shirt,
89
+ line_item: standalone_line_item,
90
+ state: :on_hand)
91
+ }
92
+ end
93
+
94
+ let(:shipment) { create(:shipment) }
95
+
96
+ before do
97
+ shipment.inventory_units << assembly_shirts
98
+ shipment.inventory_units << standalone_shirts
99
+ end
100
+
101
+ it "set states numbers properly for all items" do
102
+ shipment.manifest.each do |item|
103
+ if item.line_item.id == standalone_line_item.id
104
+ expect(item.states["on_hand"]).to eq standalone_shirts.count
105
+ else
106
+ expect(item.states["on_hand"]).to eq assembly_shirts.count
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end