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,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