solidus_product_assembly 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 (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