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,70 @@
1
+ <%# This partial was remove in solidus 1.2 but that is also when Spree.solidus_version was added %>
2
+ <%= render :partial => 'spree/admin/shared/product_sub_menu' if !Spree.respond_to?(:solidus_version) %>
3
+
4
+ <%= render :partial => 'spree/admin/shared/product_tabs', :locals => {:current => "Parts"} %>
5
+ <div id="product_parts">
6
+ <%= render :partial => "parts_table", :locals => {:parts => @parts} %>
7
+ </div>
8
+
9
+ <%= form_tag('#') do %>
10
+ <label><%= Spree.t(:search) %>:</label>
11
+ <input id="searchtext" size="25">
12
+ <button id="search_parts_button" class="fa fa-search button" name="button">Search</button>
13
+ <% end %>
14
+
15
+ <br/>
16
+ <div id="search_hits"></div>
17
+ <%= javascript_tag do %>
18
+ /*!
19
+ * Spree Product Assembly
20
+ * https://github.com/spree/spree-product-assembly
21
+ *
22
+ */
23
+
24
+ function search_for_parts(){
25
+ $.ajax({
26
+ data: {q: $("#searchtext").val() },
27
+ dataType: 'html',
28
+ success: function(request){
29
+ jQuery('#search_hits').html(request);
30
+ $('#search_hits').show();
31
+ },
32
+ type: 'POST',
33
+ url: '<%= available_admin_product_parts_url(@product) %>'
34
+ });
35
+ }
36
+
37
+ $("#searchtext").keypress(function (e) {
38
+ if ((e.which && e.which == 13) || (e.keyCode && e.keyCode == 13)) {
39
+ search_for_parts();
40
+ return false;
41
+ } else {
42
+ return true;
43
+ }
44
+ });
45
+
46
+ $("#search_parts_button").click(function(e) {
47
+ e.preventDefault();
48
+ search_for_parts();
49
+ });
50
+
51
+ function subscribe_product_part_links()
52
+ {
53
+ $("a.set_count_admin_product_part_link").click(function(){
54
+ params = { count : $("input", $(this).parent().parent()).val() };
55
+ return make_post_request($(this), params);
56
+ });
57
+
58
+ $("a.remove_admin_product_part_link").click(function(){
59
+ return make_post_request($(this), {});
60
+ });
61
+ }
62
+
63
+ function make_post_request(link, post_params)
64
+ {
65
+ $.post(link.attr("href"), post_params, "script");
66
+ return false;
67
+ }
68
+
69
+ subscribe_product_part_links();
70
+ <% end -%>
@@ -0,0 +1,2 @@
1
+ $("#product_parts").html("<%= escape_javascript(render(:partial => "parts_table", :locals => {:parts => @product.parts})) %>");
2
+ $("#search_hits").hide();
@@ -0,0 +1,23 @@
1
+ <div class="left six columns alpha">
2
+ <div class="field">
3
+ <%= f.label :can_be_part, Spree.t(:can_be_part)%>
4
+ <%= f.check_box(:can_be_part) %>
5
+ </div>
6
+ </div>
7
+ <div class="right six columns omega">
8
+ <div class="field">
9
+ <%= f.label :individual_sale, Spree.t(:individual_sale)%>
10
+ <%= f.check_box(:individual_sale) %>
11
+ </div>
12
+ </div>
13
+
14
+ <% if @product.assembly? %>
15
+ <% content_for :head do %>
16
+ <script type="text/javascript">
17
+ jQuery(document).ready(function(){
18
+ $("input[id='product_on_hand']").attr('disabled',true).parent().hide();
19
+ $("input#product_can_be_part").attr('disabled',true);
20
+ });
21
+ </script>
22
+ <% end %>
23
+ <% end %>
@@ -0,0 +1,3 @@
1
+ <li<%= ' class="active"' if current == "Parts" %>>
2
+ <%= link_to_with_icon 'sitemap', Spree.t(:parts), admin_product_parts_url(@product) %>
3
+ </li>
@@ -0,0 +1,17 @@
1
+ <% ship_form.object.line_item_manifest.each do |item| %>
2
+ <tr class="stock-item">
3
+ <td class="item-image"><%= mini_image(item.variant) %></td>
4
+ <td class="item-name">
5
+ <%= item.variant.name %>
6
+ <%= render 'spree/orders/cart_description', variant: item.variant, line_item: item.line_item %>
7
+ </td>
8
+ <td class="item-qty">
9
+ <% if item.line_item.product.assembly? %>
10
+ <%= item.line_item.quantity %>
11
+ <% else %>
12
+ <%= item.quantity %>
13
+ <% end %>
14
+ </td>
15
+ <td class="item-price"><%= item.line_item.single_money %></td>
16
+ </tr>
17
+ <% end %>
@@ -0,0 +1,8 @@
1
+ <% product = variant.product
2
+ if product.assembly? %>
3
+ <ul class='assembly_parts'>
4
+ <% line_item.parts.each do |v| %>
5
+ <li>(<%= line_item.count_of(v) %>) <%= link_to v.name, product_path(v.product) %> (<%= v.sku %>)</li>
6
+ <% end %>
7
+ </ul>
8
+ <% end %>
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ ENGINE_ROOT = File.expand_path('../..', __FILE__)
4
+ ENGINE_PATH = File.expand_path('../../lib/solidus_product_assembly/engine', __FILE__)
5
+
6
+ require 'rails/all'
7
+ require 'rails/engine/commands'
@@ -0,0 +1,14 @@
1
+ ---
2
+ en:
3
+ spree:
4
+ available_parts: Available parts
5
+ can_be_part: Can be part
6
+ individual_sale: Individual sale
7
+ no_variants: No variants
8
+ parts: Parts
9
+ assembly_cannot_be_part: assembly can't be part
10
+ product_bundles: Product Bundles
11
+ parts_included: Parts included
12
+ part_of_bundle: 'Part of bundle %{sku}'
13
+ actions:
14
+ delete: 'Delete'
@@ -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}'
@@ -0,0 +1,19 @@
1
+ Spree::Core::Engine.add_routes do
2
+
3
+ namespace :admin do
4
+ resources :products do
5
+ resources :parts do
6
+ member do
7
+ post :select
8
+ post :remove
9
+ post :set_count
10
+ end
11
+ collection do
12
+ post :available
13
+ get :selected
14
+ end
15
+ end
16
+ end
17
+ end
18
+
19
+ end
@@ -0,0 +1,13 @@
1
+ class AddManyToManyRelationToProducts < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :assemblies_parts, :id => false do |t|
4
+ t.integer "assembly_id", :null => false
5
+ t.integer "part_id", :null => false
6
+ t.integer "count", :null => false, :default => 1
7
+ end
8
+ end
9
+
10
+ def self.down
11
+ drop_table :assemblies_parts
12
+ end
13
+ end
@@ -0,0 +1,27 @@
1
+ class AddPartsFieldsToProducts < ActiveRecord::Migration
2
+ def self.up
3
+ table = if table_exists?(:products)
4
+ 'products'
5
+ elsif table_exists?(:spree_products)
6
+ 'spree_products'
7
+ end
8
+
9
+ change_table(table) do |t|
10
+ t.column :can_be_part, :boolean, :default => false, :null => false
11
+ t.column :individual_sale, :boolean, :default => true, :null => false
12
+ end
13
+ end
14
+
15
+ def self.down
16
+ table = if table_exists?(:products)
17
+ 'products'
18
+ elsif table_exists?(:spree_products)
19
+ 'spree_products'
20
+ end
21
+
22
+ change_table(table) do |t|
23
+ t.remove :can_be_part
24
+ t.remove :individual_sale
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,9 @@
1
+ class NamespaceProductAssemblyForSpreeOne < ActiveRecord::Migration
2
+ def up
3
+ rename_table :assemblies_parts, :spree_assemblies_parts
4
+ end
5
+
6
+ def down
7
+ rename_table :spree_assemblies_parts, :assemblies_parts
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ class AddIdToSpreeAssembliesParts < ActiveRecord::Migration
2
+ def up
3
+ add_column :spree_assemblies_parts, :id, :primary_key
4
+ end
5
+
6
+ def down
7
+ add_column :spree_assemblies_parts, :id
8
+ end
9
+ end
@@ -0,0 +1,24 @@
1
+ module SolidusProductAssembly
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=solidus_product_assembly'
8
+ end
9
+
10
+ def add_javascripts
11
+ append_file "vendor/assets/javascripts/spree/backend/all.js", "//= require spree/backend/solidus_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,3 @@
1
+ require 'solidus_core'
2
+ require 'solidus_product_assembly/engine'
3
+ require 'solidus_product_assembly/version'
@@ -0,0 +1,15 @@
1
+ module SolidusProductAssembly
2
+ class Engine < Rails::Engine
3
+ engine_name 'solidus_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
+ end
12
+
13
+ config.to_prepare &method(:activate).to_proc
14
+ end
15
+ end
@@ -0,0 +1,3 @@
1
+ module SolidusProductAssembly
2
+ VERSION = '1.0.0'
3
+ end
@@ -0,0 +1,29 @@
1
+ namespace :solidus_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,35 @@
1
+ # encoding: UTF-8
2
+
3
+ lib = File.expand_path('../lib/', __FILE__)
4
+ $:.unshift lib unless $:.include?(lib)
5
+
6
+ require 'solidus_product_assembly/version'
7
+
8
+ Gem::Specification.new do |s|
9
+ s.platform = Gem::Platform::RUBY
10
+ s.name = 'solidus_product_assembly'
11
+ s.version = SolidusProductAssembly::VERSION
12
+ s.summary = 'Adds oportunity to make bundle of products to your Spree store'
13
+ s.description = s.summary
14
+ s.required_ruby_version = '>= 1.9.3'
15
+
16
+ s.author = 'Roman Smirnov'
17
+ s.email = 'roman@railsdog.com'
18
+ s.homepage = 'https://solidus.io'
19
+
20
+ s.files = `git ls-files`.split("\n")
21
+ s.test_files = `git ls-files -- spec/*`.split("\n")
22
+ s.require_path = 'lib'
23
+ s.requirements << 'none'
24
+
25
+ s.add_runtime_dependency 'solidus_backend', [">= 1.0", "< 3"]
26
+
27
+ s.add_development_dependency 'rspec-rails', '~> 3.4'
28
+ s.add_development_dependency 'sqlite3'
29
+ s.add_development_dependency 'ffaker'
30
+ s.add_development_dependency 'factory_girl', '~> 4.4'
31
+ s.add_development_dependency 'capybara', '~> 2.7'
32
+ s.add_development_dependency 'poltergeist', '~> 1.9'
33
+ s.add_development_dependency 'database_cleaner', '~> 1.3'
34
+ s.add_development_dependency 'simplecov'
35
+ 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,28 @@
1
+ require 'spec_helper'
2
+
3
+ describe "Parts", type: :feature, js: true do
4
+ stub_authorization!
5
+
6
+ let!(:tshirt) { create(:product, :name => "T-Shirt") }
7
+ let!(:mug) { create(:product, :name => "Mug") }
8
+
9
+ before do
10
+ visit spree.admin_product_path(mug)
11
+ check "product_can_be_part"
12
+ click_on "Update"
13
+ end
14
+
15
+ it "add and remove parts" do
16
+ visit spree.admin_product_path(tshirt)
17
+ click_on "Parts"
18
+ fill_in "searchtext", with: mug.name
19
+ click_on "Search"
20
+
21
+ within("#search_hits") { click_on "Select" }
22
+ page.should have_content(mug.sku)
23
+
24
+ within("#product_parts") do
25
+ find(".remove_admin_product_part_link").click
26
+ end
27
+ end
28
+ end