├── .rspec
├── .github_changelog_generator
├── app
├── assets
│ ├── javascripts
│ │ └── spree
│ │ │ └── backend
│ │ │ └── solidus_product_assembly.js
│ ├── stylesheets
│ │ └── spree
│ │ │ └── backend
│ │ │ └── solidus_product_assembly.css
│ └── images
│ │ └── spinner.gif
├── overrides
│ └── spree
│ │ ├── checkout
│ │ └── _delivery
│ │ │ ├── remove_unshippable_markup.html.erb.deface
│ │ │ └── render_line_item_manifest.html.erb.deface
│ │ ├── orders
│ │ └── _line_item
│ │ │ └── product_assembly_cart_item_description.deface
│ │ ├── admin
│ │ ├── shared
│ │ │ └── _product_tabs
│ │ │ │ └── product_assembly_admin_product_tabs.deface
│ │ ├── orders
│ │ │ ├── _form
│ │ │ │ └── inject_product_assemblies.html.erb.deface
│ │ │ ├── _carton_manifest
│ │ │ │ ├── _assembly_parts_price.html.erb.deface
│ │ │ │ └── _assembly_parts_total_price.html.erb.deface
│ │ │ └── _shipment
│ │ │ │ └── stock_contents.html.erb.deface
│ │ └── products
│ │ │ └── _form
│ │ │ └── product_assembly_product_form_right.deface
│ │ ├── products
│ │ └── show
│ │ │ ├── remove_add_to_cart_button_for_non_individual_sale_products.html.erb.deface
│ │ │ └── add_links_to_parts.html.erb.deface
│ │ └── shared
│ │ └── _order_details
│ │ └── part_description.html.erb.deface
├── views
│ └── spree
│ │ ├── admin
│ │ ├── parts
│ │ │ ├── update_parts_table.js.erb
│ │ │ ├── _parts_table.html.erb
│ │ │ ├── available.html.erb
│ │ │ ├── available.js.erb
│ │ │ └── index.html.erb
│ │ ├── shared
│ │ │ └── _product_assembly_product_tabs.html.erb
│ │ ├── orders
│ │ │ ├── _stock_contents.html.erb
│ │ │ ├── _assemblies.html.erb
│ │ │ ├── _stock_item.html.erb
│ │ │ ├── _stock_contents_2_3.html.erb
│ │ │ └── _stock_contents_2_4.html.erb
│ │ └── products
│ │ │ └── _product_assembly_fields.html.erb
│ │ ├── orders
│ │ └── _cart_description.html.erb
│ │ ├── api
│ │ └── line_items
│ │ │ └── show.v1.rabl
│ │ └── checkout
│ │ └── _line_item_manifest.html.erb
├── decorators
│ ├── models
│ │ └── solidus_product_assembly
│ │ │ └── spree
│ │ │ ├── return_item_decorator.rb
│ │ │ ├── stock
│ │ │ ├── differentiator_decorator.rb
│ │ │ ├── inventory_validator_decorator.rb
│ │ │ ├── inventory_units_finalizer_decorator.rb
│ │ │ ├── inventory_unit_builder_decorator.rb
│ │ │ └── availability_validator_decorator.rb
│ │ │ ├── inventory_unit_decorator.rb
│ │ │ ├── variant_decorator.rb
│ │ │ ├── line_item_decorator.rb
│ │ │ ├── shipment_decorator.rb
│ │ │ └── product_decorator.rb
│ └── helpers
│ │ └── solidus_product_assembly
│ │ └── spree
│ │ └── admin
│ │ └── orders_helper_decorator.rb
├── models
│ └── spree
│ │ ├── assemblies_part.rb
│ │ ├── calculator
│ │ └── returns
│ │ │ └── assemblies_default_refund_amount.rb
│ │ └── order_inventory_assembly.rb
└── controllers
│ └── spree
│ └── admin
│ └── parts_controller.rb
├── lib
├── solidus_product_assembly
│ ├── testing_support
│ │ └── factories.rb
│ ├── version.rb
│ ├── configuration.rb
│ └── engine.rb
├── solidus_product_assembly.rb
├── generators
│ └── solidus_product_assembly
│ │ └── install
│ │ ├── templates
│ │ └── initializer.rb
│ │ └── install_generator.rb
└── tasks
│ └── spree2_upgrade.rake
├── .rubocop.yml
├── bin
├── rake
├── setup
├── rails
├── rails-sandbox
├── console
├── rails-engine
└── sandbox
├── CHANGELOG.md
├── .gem_release.yml
├── .github
├── dependabot.yml
└── stale.yml
├── Rakefile
├── .gitignore
├── db
└── migrate
│ ├── 20140620223938_add_id_to_spree_assemblies_parts.rb
│ ├── 20120316141830_namespace_product_assembly_for_spree_one.rb
│ ├── 20091028152124_add_many_to_many_relation_to_products.rb
│ └── 20091029165620_add_parts_fields_to_products.rb
├── config
├── locales
│ ├── sv.yml
│ ├── en.yml
│ ├── fr.yml
│ └── ru.yml
└── routes.rb
├── spec
├── support
│ └── shared_contexts
│ │ └── order_with_bundle.rb
├── models
│ └── spree
│ │ ├── assemblies_part_spec.rb
│ │ ├── variant_spec.rb
│ │ ├── order_inventory_spec.rb
│ │ ├── order_contents_spec.rb
│ │ ├── stock
│ │ ├── inventory_unit_builder_spec.rb
│ │ ├── coordinator_spec.rb
│ │ └── availability_validator_spec.rb
│ │ ├── inventory_unit_spec.rb
│ │ ├── product_spec.rb
│ │ ├── order_inventory_assembly_spec.rb
│ │ ├── line_item_spec.rb
│ │ └── shipment_spec.rb
├── features
│ ├── admin
│ │ ├── parts_spec.rb
│ │ ├── orders_spec.rb
│ │ └── return_items_spec.rb
│ └── checkout_spec.rb
└── spec_helper.rb
├── LICENSE
├── solidus_product_assembly.gemspec
├── Gemfile
├── .circleci
└── config.yml
├── README.md
├── .rubocop_todo.yml
└── OLD_CHANGELOG.md
/.rspec:
--------------------------------------------------------------------------------
1 | --color
2 | --require spec_helper
3 |
--------------------------------------------------------------------------------
/.github_changelog_generator:
--------------------------------------------------------------------------------
1 | issues=false
2 | exclude-labels=infrastructure
3 |
--------------------------------------------------------------------------------
/app/assets/javascripts/spree/backend/solidus_product_assembly.js:
--------------------------------------------------------------------------------
1 | //= require_tree .
2 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/spree/backend/solidus_product_assembly.css:
--------------------------------------------------------------------------------
1 | /*
2 | *= require spree/backend
3 | */
4 |
--------------------------------------------------------------------------------
/app/overrides/spree/checkout/_delivery/remove_unshippable_markup.html.erb.deface:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/lib/solidus_product_assembly/testing_support/factories.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | FactoryBot.define do
4 | end
5 |
--------------------------------------------------------------------------------
/app/assets/images/spinner.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solidusio-contrib/solidus_product_assembly/HEAD/app/assets/images/spinner.gif
--------------------------------------------------------------------------------
/.rubocop.yml:
--------------------------------------------------------------------------------
1 | inherit_from: .rubocop_todo.yml
2 |
3 | require:
4 | - solidus_dev_support/rubocop
5 |
6 | AllCops:
7 | NewCops: disable
8 |
--------------------------------------------------------------------------------
/lib/solidus_product_assembly/version.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module SolidusProductAssembly
4 | VERSION = '1.4.0'
5 | end
6 |
--------------------------------------------------------------------------------
/bin/rake:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | require "rubygems"
5 | require "bundler/setup"
6 |
7 | load Gem.bin_path("rake", "rake")
8 |
--------------------------------------------------------------------------------
/bin/setup:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -euo pipefail
3 | IFS=$'\n\t'
4 | set -vx
5 |
6 | gem install bundler --conservative
7 | bundle update
8 | bin/rake clobber
9 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | See https://github.com/solidusio-contrib/solidus_product_assembly/releases or [OLD_CHANGELOG.md](OLD_CHANGELOG.md) for older versions.
4 |
--------------------------------------------------------------------------------
/.gem_release.yml:
--------------------------------------------------------------------------------
1 | bump:
2 | recurse: false
3 | file: 'lib/solidus_product_assembly/version.rb'
4 | message: Bump SolidusProductAssembly to %{version}
5 | tag: true
6 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: bundler
4 | directory: "/"
5 | schedule:
6 | interval: daily
7 | open-pull-requests-limit: 10
8 |
--------------------------------------------------------------------------------
/app/overrides/spree/orders/_line_item/product_assembly_cart_item_description.deface:
--------------------------------------------------------------------------------
1 | insert_bottom "[data-hook='cart_item_description']"
2 | partial "spree/orders/cart_description"
3 |
--------------------------------------------------------------------------------
/bin/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | if %w[g generate].include? ARGV.first
4 | exec "#{__dir__}/rails-engine", *ARGV
5 | else
6 | exec "#{__dir__}/rails-sandbox", *ARGV
7 | end
8 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'bundler/gem_tasks'
4 | require 'solidus_dev_support/rake_tasks'
5 | SolidusDevSupport::RakeTasks.install
6 |
7 | task default: 'extension:specs'
8 |
--------------------------------------------------------------------------------
/app/overrides/spree/admin/shared/_product_tabs/product_assembly_admin_product_tabs.deface:
--------------------------------------------------------------------------------
1 | insert_bottom "[data-hook='admin_product_tabs']"
2 | partial "spree/admin/shared/product_assembly_product_tabs"
3 |
--------------------------------------------------------------------------------
/app/overrides/spree/checkout/_delivery/render_line_item_manifest.html.erb.deface:
--------------------------------------------------------------------------------
1 |
2 |
3 | <%= render 'line_item_manifest', ship_form: ship_form %>
4 |
--------------------------------------------------------------------------------
/app/views/spree/admin/parts/update_parts_table.js.erb:
--------------------------------------------------------------------------------
1 | $("#product_parts").html("<%= escape_javascript(render(:partial => "parts_table", :locals => {:parts => @product.parts})) %>");
2 | $("#search_hits").hide();
3 |
--------------------------------------------------------------------------------
/lib/solidus_product_assembly.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'solidus_product_assembly/configuration'
4 | require 'solidus_product_assembly/version'
5 | require 'solidus_product_assembly/engine'
6 |
--------------------------------------------------------------------------------
/app/overrides/spree/admin/orders/_form/inject_product_assemblies.html.erb.deface:
--------------------------------------------------------------------------------
1 |
2 |
3 | <%= render partial: 'spree/admin/orders/assemblies', locals: { order: order } %>
4 |
--------------------------------------------------------------------------------
/app/overrides/spree/admin/products/_form/product_assembly_product_form_right.deface:
--------------------------------------------------------------------------------
1 | insert_after "[data-hook='admin_product_form_right'], #admin_product_form_right[data-hook]"
2 | partial "spree/admin/products/product_assembly_fields"
3 |
--------------------------------------------------------------------------------
/app/overrides/spree/products/show/remove_add_to_cart_button_for_non_individual_sale_products.html.erb.deface:
--------------------------------------------------------------------------------
1 |
2 | <% if @product.individual_sale? %>
3 | <%= render_original %>
4 | <% end %>
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.gem
2 | \#*
3 | *~
4 | .#*
5 | .DS_Store
6 | .idea
7 | .project
8 | .sass-cache
9 | coverage
10 | Gemfile.lock
11 | tmp
12 | nbproject
13 | pkg
14 | *.swp
15 | spec/dummy
16 | spec/examples.txt
17 | /sandbox
18 | .rvmrc
19 | .ruby-version
20 | .ruby-gemset
21 |
--------------------------------------------------------------------------------
/app/views/spree/admin/shared/_product_assembly_product_tabs.html.erb:
--------------------------------------------------------------------------------
1 | <%= content_tag :li, class: ('active' if current == 'Parts') do %>
2 | <%= link_to t('spree.parts'), admin_product_parts_url(@product) %>
3 | <% end if can?(:admin, Spree::AssembliesPart) && !@product.deleted? %>
4 |
--------------------------------------------------------------------------------
/app/views/spree/admin/orders/_stock_contents.html.erb:
--------------------------------------------------------------------------------
1 | <%= render 'stock_item', shipment: shipment %>
2 |
3 | <% unless shipment.shipped? %>
4 |
5 |
6 | <% end %>
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/lib/generators/solidus_product_assembly/install/templates/initializer.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | SolidusProductAssembly.configure do |config|
4 | # TODO: Remember to change this with the actual preferences you have implemented!
5 | # config.sample_preference = 'sample_value'
6 | end
7 |
--------------------------------------------------------------------------------
/app/views/spree/orders/_cart_description.html.erb:
--------------------------------------------------------------------------------
1 | <% product = variant.product
2 | if product.assembly? %>
3 |
4 | <% line_item.parts.each do |v| %>
5 | (<%= line_item.count_of(v) %>) <%= link_to v.name, product_path(v.product) %> (<%= v.sku %>)
6 | <% end %>
7 |
8 | <% end %>
9 |
--------------------------------------------------------------------------------
/app/overrides/spree/admin/orders/_carton_manifest/_assembly_parts_price.html.erb.deface:
--------------------------------------------------------------------------------
1 |
2 |
3 | <% if item.variant.part? %>
4 | ---
5 | <% else %>
6 |
7 | <%= line_item_shipment_price(item.line_item, item.quantity) %>
8 |
9 | <% end %>
10 |
--------------------------------------------------------------------------------
/db/migrate/20140620223938_add_id_to_spree_assemblies_parts.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class AddIdToSpreeAssembliesParts < SolidusSupport::Migration[4.2]
4 | def up
5 | add_column :spree_assemblies_parts, :id, :primary_key
6 | end
7 |
8 | def down
9 | remove_column :spree_assemblies_parts, :id
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/app/overrides/spree/admin/orders/_carton_manifest/_assembly_parts_total_price.html.erb.deface:
--------------------------------------------------------------------------------
1 |
2 |
3 | <% if item.variant.part? %>
4 | ---
5 | <% else %>
6 |
7 | <%= line_item_shipment_price(item.line_item, item.quantity) %>
8 |
9 | <% end %>
10 |
--------------------------------------------------------------------------------
/db/migrate/20120316141830_namespace_product_assembly_for_spree_one.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class NamespaceProductAssemblyForSpreeOne < SolidusSupport::Migration[4.2]
4 | def up
5 | rename_table :assemblies_parts, :spree_assemblies_parts
6 | end
7 |
8 | def down
9 | rename_table :spree_assemblies_parts, :assemblies_parts
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/app/overrides/spree/shared/_order_details/part_description.html.erb.deface:
--------------------------------------------------------------------------------
1 |
2 | <% if item.product.assembly? %>
3 |
4 | <% item.parts.each do |v| %>
5 | (<%= item.count_of(v) %>) <%= link_to v.name, product_path(v.product) %> (<%= v.sku %>)
6 | <% end %>
7 |
8 | <% end %>
9 |
--------------------------------------------------------------------------------
/config/locales/sv.yml:
--------------------------------------------------------------------------------
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}'
--------------------------------------------------------------------------------
/bin/rails-sandbox:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | app_root = 'sandbox'
4 |
5 | unless File.exist? "#{app_root}/bin/rails"
6 | warn 'Creating the sandbox app...'
7 | Dir.chdir "#{__dir__}/.." do
8 | system "#{__dir__}/sandbox" or begin
9 | warn 'Automatic creation of the sandbox app failed'
10 | exit 1
11 | end
12 | end
13 | end
14 |
15 | Dir.chdir app_root
16 | exec 'bin/rails', *ARGV
17 |
--------------------------------------------------------------------------------
/config/locales/en.yml:
--------------------------------------------------------------------------------
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'
15 |
--------------------------------------------------------------------------------
/config/locales/fr.yml:
--------------------------------------------------------------------------------
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}'
13 |
--------------------------------------------------------------------------------
/config/routes.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | Spree::Core::Engine.routes.draw do
4 | namespace :admin do
5 | resources :products do
6 | resources :parts do
7 | member do
8 | post :select
9 | post :remove
10 | post :set_count
11 | end
12 | collection do
13 | post :available
14 | get :selected
15 | end
16 | end
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/app/overrides/spree/admin/orders/_shipment/stock_contents.html.erb.deface:
--------------------------------------------------------------------------------
1 |
2 | <% if Spree.solidus_gem_version < Gem::Version.new('2.4') %>
3 | <%= render "stock_contents_2_3", shipment: shipment %>
4 | <% elsif Spree.solidus_gem_version < Gem::Version.new('2.5') %>
5 | <%= render "stock_contents_2_4", shipment: shipment %>
6 | <% else %>
7 | <%= render "stock_contents", shipment: shipment %>
8 | <% end %>
9 |
--------------------------------------------------------------------------------
/config/locales/ru.yml:
--------------------------------------------------------------------------------
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}'
--------------------------------------------------------------------------------
/app/decorators/models/solidus_product_assembly/spree/return_item_decorator.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module SolidusProductAssembly
4 | module Spree
5 | module ReturnItemDecorator
6 | def self.prepended(base)
7 | base.class_eval do
8 | self.refund_amount_calculator = ::Spree::Calculator::Returns::AssembliesDefaultRefundAmount
9 | end
10 | end
11 |
12 | ::Spree::ReturnItem.prepend self
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/db/migrate/20091028152124_add_many_to_many_relation_to_products.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class AddManyToManyRelationToProducts < SolidusSupport::Migration[4.2]
4 | def self.up
5 | create_table :assemblies_parts, id: false do |t|
6 | t.integer "assembly_id", null: false
7 | t.integer "part_id", null: false
8 | t.integer "count", null: false, default: 1
9 | end
10 | end
11 |
12 | def self.down
13 | drop_table :assemblies_parts
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/app/decorators/models/solidus_product_assembly/spree/stock/differentiator_decorator.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module SolidusProductAssembly
4 | module Spree
5 | module Stock
6 | module DifferentiatorDecorator
7 | def build_missing
8 | super.tap do
9 | @missing.delete_if { |k, _v| k.product.assembly? }
10 | end
11 | end
12 |
13 | ::Spree::Stock::Differentiator.prepend self
14 | end
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/app/models/spree/assemblies_part.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Spree
4 | class AssembliesPart < ApplicationRecord
5 | belongs_to :assembly, class_name: "Spree::Product",
6 | foreign_key: "assembly_id", touch: true
7 |
8 | belongs_to :part, class_name: "Spree::Variant", foreign_key: "part_id"
9 |
10 | def self.get(assembly_id, part_id)
11 | find_or_initialize_by(assembly_id: assembly_id, part_id: part_id)
12 | end
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/lib/solidus_product_assembly/configuration.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module SolidusProductAssembly
4 | class Configuration
5 | # Define here the settings for this extension, e.g.:
6 | #
7 | # attr_accessor :my_setting
8 | end
9 |
10 | class << self
11 | def configuration
12 | @configuration ||= Configuration.new
13 | end
14 |
15 | alias config configuration
16 |
17 | def configure
18 | yield configuration
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/lib/solidus_product_assembly/engine.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'solidus_core'
4 | require 'solidus_support'
5 | require 'deface'
6 |
7 | module SolidusProductAssembly
8 | class Engine < Rails::Engine
9 | include SolidusSupport::EngineExtensions
10 |
11 | isolate_namespace ::Spree
12 |
13 | engine_name 'solidus_product_assembly'
14 |
15 | # use rspec for tests
16 | config.generators do |g|
17 | g.test_framework :rspec
18 | end
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/bin/console:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | # frozen_string_literal: true
4 |
5 | require "bundler/setup"
6 | require "solidus_product_assembly"
7 |
8 | # You can add fixtures and/or initialization code here to make experimenting
9 | # with your gem easier. You can also use a different console, if you like.
10 | $LOAD_PATH.unshift(*Dir["#{__dir__}/../app/*"])
11 |
12 | # (If you use this, don't forget to add pry to your Gemfile!)
13 | # require "pry"
14 | # Pry.start
15 |
16 | require "irb"
17 | IRB.start(__FILE__)
18 |
--------------------------------------------------------------------------------
/spec/support/shared_contexts/order_with_bundle.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | shared_context "product is ordered as individual and within a bundle" do
4 | let(:order) { create(:order_with_line_items) }
5 | let(:parts) { (1..3).map { create(:variant) } }
6 |
7 | let(:bundle_variant) { order.variants.first }
8 | let(:bundle) { bundle_variant.product }
9 |
10 | let(:common_product) { order.variants.last }
11 |
12 | before do
13 | bundle.parts << [parts, common_product]
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/spec/models/spree/assemblies_part_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'spec_helper'
4 |
5 | module Spree
6 | describe AssembliesPart do
7 | let(:product) { create(:product) }
8 | let(:variant) { create(:variant) }
9 |
10 | before do
11 | product.parts.push variant
12 | end
13 |
14 | context "get" do
15 | it "brings part by product and variant id" do
16 | expect(subject.class.get(product.id, variant.id).part).to eq variant
17 | end
18 | end
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/bin/rails-engine:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # This command will automatically be run when you run "rails" with Rails gems
3 | # installed from the root of your application.
4 |
5 | ENGINE_ROOT = File.expand_path('..', __dir__)
6 | ENGINE_PATH = File.expand_path('../lib/solidus_product_assembly/engine', __dir__)
7 |
8 | # Set up gems listed in the Gemfile.
9 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
10 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
11 |
12 | require 'rails/all'
13 | require 'rails/engine/commands'
14 |
--------------------------------------------------------------------------------
/app/models/spree/calculator/returns/assemblies_default_refund_amount.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Spree
4 | module Calculator::Returns
5 | class AssembliesDefaultRefundAmount < DefaultRefundAmount
6 | def compute(return_item)
7 | percentage = return_item.inventory_unit.percentage_of_line_item
8 | if percentage < 1 && return_item.variant.part?
9 | line_item = return_item.inventory_unit.line_item
10 | (super * percentage * line_item.quantity).round 4, :up
11 | else
12 | super
13 | end
14 | end
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/app/decorators/helpers/solidus_product_assembly/spree/admin/orders_helper_decorator.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module SolidusProductAssembly
4 | module Spree
5 | module Admin
6 | module OrdersHelperDecorator
7 | def self.prepended(base)
8 | base.module_eval do
9 | def line_item_shipment_price(line_item, quantity)
10 | ::Spree::Money.new(line_item.price * quantity, currency: line_item.currency)
11 | end
12 | end
13 | end
14 |
15 | ::Spree::Admin::OrdersHelper.prepend self
16 | end
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/app/decorators/models/solidus_product_assembly/spree/inventory_unit_decorator.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module SolidusProductAssembly
4 | module Spree
5 | module InventoryUnitDecorator
6 | def percentage_of_line_item
7 | product = line_item.product
8 | if product.assembly?
9 | total_value = line_item.quantity_by_variant.map { |part, quantity| part.price * quantity }.sum
10 | variant.price / total_value
11 | else
12 | 1 / BigDecimal(line_item.quantity)
13 | end
14 | end
15 |
16 | ::Spree::InventoryUnit.prepend self
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/app/decorators/models/solidus_product_assembly/spree/stock/inventory_validator_decorator.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module SolidusProductAssembly
4 | module Spree
5 | module Stock
6 | module InventoryValidatorDecorator
7 | def validate(line_item)
8 | total_quantity = line_item.quantity_by_variant.values.sum
9 |
10 | if line_item.inventory_units.count != total_quantity
11 | line_item.errors[:inventory] << I18n.t(
12 | 'spree.inventory_not_available',
13 | item: line_item.variant.name
14 | )
15 | end
16 | end
17 |
18 | ::Spree::Stock::InventoryValidator.prepend self
19 | end
20 | end
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/.github/stale.yml:
--------------------------------------------------------------------------------
1 | # Number of days of inactivity before an issue becomes stale
2 | daysUntilStale: 60
3 | # Number of days of inactivity before a stale issue is closed
4 | daysUntilClose: false
5 | # Issues with these labels will never be considered stale
6 | exemptLabels:
7 | - pinned
8 | - security
9 | # Label to use when marking an issue as stale
10 | staleLabel: stale
11 | # Comment to post when marking an issue as stale. Set to `false` to disable
12 | markComment: >
13 | This issue has been automatically marked as stale because it has not had
14 | recent activity. It might be closed if no further activity occurs. Thank you
15 | for your contributions.
16 | # Comment to post when closing a stale issue. Set to `false` to disable
17 | closeComment: false
18 |
--------------------------------------------------------------------------------
/app/decorators/models/solidus_product_assembly/spree/variant_decorator.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module SolidusProductAssembly
4 | module Spree
5 | module VariantDecorator
6 | def self.prepended(base)
7 | base.class_eval do
8 | has_and_belongs_to_many :assemblies,
9 | class_name: "Spree::Product",
10 | join_table: "spree_assemblies_parts",
11 | foreign_key: "part_id", association_foreign_key: "assembly_id"
12 | end
13 | end
14 |
15 | def assemblies_for(products)
16 | assemblies.where(id: products)
17 | end
18 |
19 | def part?
20 | assemblies.exists?
21 | end
22 |
23 | ::Spree::Variant.prepend self
24 | end
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/app/views/spree/admin/products/_product_assembly_fields.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 | <%= f.label :can_be_part, t('spree.can_be_part')%>
4 | <%= f.check_box(:can_be_part) %>
5 |
6 |
7 |
8 |
9 | <%= f.label :individual_sale, t('spree.individual_sale')%>
10 | <%= f.check_box(:individual_sale) %>
11 |
12 |
13 |
14 | <% if @product.assembly? %>
15 | <% content_for :head do %>
16 |
22 | <% end %>
23 | <% end %>
24 |
--------------------------------------------------------------------------------
/app/views/spree/api/line_items/show.v1.rabl:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | object @line_item
4 | cache [I18n.locale, root_object]
5 | attributes *line_item_attributes
6 | node(:single_display_amount) { |li| li.single_display_amount.to_s }
7 | node(:display_amount) { |li| li.display_amount.to_s }
8 | node(:total, &:total)
9 | glue variant: :parts do
10 | glue :product do
11 | child parts: :parts do
12 | extends "spree/api/variants/small"
13 | attributes :product_id
14 |
15 | child(images: :images) { extends "spree/api/images/show" }
16 | end
17 | end
18 | end
19 | child :variant do
20 | extends "spree/api/variants/small"
21 | attributes :product_id
22 | child(images: :images) { extends "spree/api/images/show" }
23 | end
24 |
25 | child adjustments: :adjustments do
26 | extends "spree/api/adjustments/show"
27 | end
28 |
--------------------------------------------------------------------------------
/app/views/spree/checkout/_line_item_manifest.html.erb:
--------------------------------------------------------------------------------
1 | <% ship_form.object.line_item_manifest.each do |item| %>
2 |
3 | <%= render 'spree/shared/image',
4 | image: (item.variant.gallery.images.first || item.variant.product.gallery.images.first),
5 | size: :mini %>
6 |
7 | <%= item.variant.name %>
8 | <%= render 'spree/orders/cart_description', variant: item.variant, line_item: item.line_item %>
9 |
10 |
11 | <% if item.line_item.product.assembly? %>
12 | <%= item.line_item.quantity %>
13 | <% else %>
14 | <%= item.quantity %>
15 | <% end %>
16 |
17 | <%= item.line_item.single_money %>
18 |
19 | <% end %>
20 |
--------------------------------------------------------------------------------
/spec/features/admin/parts_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'spec_helper'
4 |
5 | describe "Parts", type: :feature, js: true do
6 | stub_authorization!
7 |
8 | let!(:tshirt) { create(:product, name: "T-Shirt") }
9 | let!(:mug) { create(:product, name: "Mug") }
10 |
11 | before do
12 | visit spree.admin_product_path(mug)
13 | check "product_can_be_part"
14 | click_on "Update"
15 | end
16 |
17 | it "can add and remove parts" do
18 | visit spree.admin_product_path(tshirt)
19 | click_on "Parts"
20 | fill_in "searchtext", with: mug.name
21 | click_on "Search"
22 | click_on "Select"
23 | expect(page).to have_link('Delete')
24 |
25 | expect(tshirt.reload.parts).to eq([mug.master])
26 |
27 | click_on 'Delete', wait: 30
28 | expect(page).not_to have_link('Delete')
29 | expect(tshirt.reload.parts).to eq([])
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/db/migrate/20091029165620_add_parts_fields_to_products.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class AddPartsFieldsToProducts < SolidusSupport::Migration[4.2]
4 | def self.up
5 | table = if data_source_exists?(:products)
6 | 'products'
7 | elsif data_source_exists?(:spree_products)
8 | 'spree_products'
9 | end
10 |
11 | change_table(table) do |t|
12 | t.column :can_be_part, :boolean, default: false, null: false
13 | t.column :individual_sale, :boolean, default: true, null: false
14 | end
15 | end
16 |
17 | def self.down
18 | table = if data_source_exists?(:products)
19 | 'products'
20 | elsif data_source_exists?(:spree_products)
21 | 'spree_products'
22 | end
23 |
24 | change_table(table) do |t|
25 | t.remove :can_be_part
26 | t.remove :individual_sale
27 | end
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/spec/models/spree/variant_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'spec_helper'
4 |
5 | module Spree
6 | describe Variant do
7 | context "filter assemblies" do
8 | let(:mug) { create(:product) }
9 | let(:tshirt) { create(:product) }
10 | let(:variant) { create(:variant) }
11 |
12 | context "variant has more than one assembly" do
13 | before { variant.assemblies.push [mug, tshirt] }
14 |
15 | it "returns both products" do
16 | expect(variant.assemblies_for([mug, tshirt])).to include mug
17 | expect(variant.assemblies_for([mug, tshirt])).to include tshirt
18 | end
19 |
20 | it { expect(variant).to be_a_part }
21 | end
22 |
23 | context "variant no assembly" do
24 | it "returns both products" do
25 | expect(variant.assemblies_for([mug, tshirt])).to be_empty
26 | end
27 | end
28 | end
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/app/overrides/spree/products/show/add_links_to_parts.html.erb.deface:
--------------------------------------------------------------------------------
1 |
2 | <% if @product.parts.any?(&:in_stock?) %>
3 |
4 | <%= t('spree.parts_included') %>
5 |
6 |
21 | <% end %>
22 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Configure Rails Environment
4 | ENV['RAILS_ENV'] = 'test'
5 |
6 | # Run Coverage report
7 | require 'solidus_dev_support/rspec/coverage'
8 |
9 | # Create the dummy app if it's still missing.
10 | dummy_env = "#{__dir__}/dummy/config/environment.rb"
11 | system 'bin/rake extension:test_app' unless File.exist? dummy_env
12 | require dummy_env
13 |
14 | # Requires factories and other useful helpers defined in spree_core.
15 | require 'solidus_dev_support/rspec/feature_helper'
16 |
17 | # Requires supporting ruby files with custom matchers and macros, etc,
18 | # in spec/support/ and its subdirectories.
19 | Dir["#{__dir__}/support/**/*.rb"].sort.each { |f| require f }
20 |
21 | # Requires factories defined in lib/solidus_product_assembly/testing_support/factories.rb
22 | SolidusDevSupport::TestingSupport::Factories.load_for(SolidusProductAssembly::Engine)
23 |
24 | RSpec.configure do |config|
25 | config.infer_spec_type_from_file_location!
26 | config.use_transactional_fixtures = false
27 |
28 | if Spree.solidus_gem_version < Gem::Version.new('2.11')
29 | config.extend Spree::TestingSupport::AuthorizationHelpers::Request, type: :system
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/spec/models/spree/order_inventory_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'spec_helper'
4 |
5 | module Spree
6 | describe OrderInventory do
7 | subject { described_class.new(order, order.line_items.first) }
8 |
9 | let!(:store) { create :store }
10 | let(:order) { Order.create }
11 |
12 | context "same variant within bundle and as regular product" do
13 | let(:contents) { OrderContents.new(order) }
14 | let(:guitar) { create(:variant) }
15 | let(:bass) { create(:variant) }
16 |
17 | let(:bundle) { create(:product) }
18 |
19 | before { bundle.parts.push [guitar, bass] }
20 |
21 | let!(:bundle_item) { contents.add(bundle.master, 5) }
22 | let!(:guitar_item) { contents.add(guitar, 3) }
23 |
24 | let!(:shipment) { order.create_proposed_shipments.first }
25 |
26 | context "completed order" do
27 | before do
28 | order.touch :completed_at
29 | end
30 |
31 | it "removes only units associated with provided line item" do
32 | expect {
33 | subject.send(:remove_from_shipment, shipment, 5)
34 | }.not_to change { bundle_item.inventory_units.count }
35 | end
36 | end
37 | end
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/spec/models/spree/order_contents_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'spec_helper'
4 |
5 | module Spree
6 | describe OrderContents do
7 | subject { described_class.new(order) }
8 |
9 | let!(:store) { create :store }
10 | let(:order) { Order.create }
11 |
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 | context "same variant within bundle and as regular product" do
20 | let!(:guitar_item) { subject.add(guitar, 3) }
21 | let!(:bundle_item) { subject.add(bundle.master, 5) }
22 |
23 | it "destroys the variant as regular product only" do
24 | subject.remove(guitar, 3)
25 | expect(order.reload.line_items.to_a).to eq [bundle_item]
26 | end
27 |
28 | context "completed order" do
29 | before do
30 | order.create_proposed_shipments
31 | order.touch :completed_at
32 | end
33 |
34 | it "destroys accurate number of inventory units" do
35 | expect { subject.remove(guitar, 3) }.
36 | to change(InventoryUnit, :count).by(-3)
37 | end
38 | end
39 | end
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/lib/tasks/spree2_upgrade.rake:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | namespace :solidus_product_assembly do
4 | desc 'Link legacy inventory units to an order line item'
5 | task upgrade: :environment do
6 | shipments = Spree::Shipment.includes(:inventory_units).where("spree_inventory_units.line_item_id IS NULL")
7 |
8 | shipments.each do |shipment|
9 | shipment.inventory_units.includes(:variant).group_by(&:variant).each do |variant, units|
10 | line_item = shipment.order.line_items.detect { |line_item| line_item.variant_id == variant.id }
11 |
12 | next if line_item
13 |
14 | begin
15 | master = shipment.order.products.detect { |p| variant.assemblies.include? p }.master
16 | supposed_line_item = shipment.order.line_items.detect { |line_item| line_item.variant_id == master.id }
17 |
18 | if supposed_line_item
19 | Spree::InventoryUnit.where(id: units.map(&:id)).update_all "line_item_id = #{supposed_line_item.id}"
20 | else
21 | puts "Couldn't find a matching line item for #{variant.name}"
22 | end
23 | rescue StandardError
24 | puts "Couldn't find a matching line item for #{variant.name}"
25 | end
26 | end
27 | end
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/app/decorators/models/solidus_product_assembly/spree/stock/inventory_units_finalizer_decorator.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module SolidusProductAssembly
4 | module Spree
5 | module Stock
6 | module InventoryUnitsFinalizerDecorator
7 | private
8 |
9 | def unstock_inventory_units
10 | inventory_units.group_by(&:shipment_id).each_value do |inventory_units_for_shipment|
11 | inventory_units_for_shipment.group_by(&:line_item_id).each_value do |units|
12 | shipment = units.first.shipment
13 | line_item = units.first.line_item
14 |
15 | if line_item.product.assembly?
16 | units.group_by(&:variant_id).each_value do |units_for_part|
17 | part = units_for_part.first.variant
18 | shipment.stock_location.unstock part, units_for_part.count, shipment
19 | end
20 | else
21 | shipment.stock_location.unstock line_item.variant, units.count, shipment
22 | end
23 | end
24 | end
25 | end
26 |
27 | if ::Spree.solidus_gem_version >= Gem::Version.new('2.8')
28 | ::Spree::Stock::InventoryUnitsFinalizer.prepend self
29 | end
30 | end
31 | end
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/spec/models/spree/stock/inventory_unit_builder_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'spec_helper'
4 |
5 | module Spree
6 | module Stock
7 | describe InventoryUnitBuilder, type: :model do
8 | subject { described_class.new(order) }
9 |
10 | context "order shares variant as individual and within bundle" do
11 | include_context "product is ordered as individual and within a bundle" do
12 | let(:bundle_item_quantity) { order.find_line_item_by_variant(bundle_variant).quantity }
13 |
14 | describe "#units" do
15 | it "returns an inventory unit for each part of each quantity for the order's line items" do
16 | units = subject.units
17 | expect(units.count).to eq 4
18 | expect(units[0].line_item.quantity).to eq order.line_items.first.quantity
19 | expect(units[0].line_item.quantity).to eq bundle_item_quantity
20 |
21 | line_item = order.line_items.first
22 |
23 | expect(units.map(&:variant)).to match_array line_item.parts
24 | end
25 |
26 | it "builds the inventory units as pending" do
27 | expect(subject.units.map(&:pending).uniq).to eq [true]
28 | end
29 | end
30 | end
31 | end
32 | end
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/app/decorators/models/solidus_product_assembly/spree/stock/inventory_unit_builder_decorator.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module SolidusProductAssembly
4 | module Spree
5 | module Stock
6 | module InventoryUnitBuilderDecorator
7 | def units
8 | @order.line_items.flat_map do |line_item|
9 | line_item.quantity_by_variant.flat_map do |variant, quantity|
10 | quantity.times.map { build_inventory_unit(variant, line_item) }
11 | end
12 | end
13 | end
14 |
15 | def build_inventory_unit(variant, line_item)
16 | inventory_unit_attributes = {
17 | pending: true,
18 | variant: variant,
19 | line_item: line_item,
20 | }
21 | if ::Spree.solidus_gem_version < Gem::Version.new('2.5.x')
22 | inventory_unit_attributes[:order] = @order
23 | end
24 | @order.inventory_units.includes(
25 | variant: {
26 | product: {
27 | shipping_category: {
28 | shipping_methods: [:calculator, { zones: :zone_members }]
29 | }
30 | }
31 | }
32 | ).build(inventory_unit_attributes)
33 | end
34 |
35 | ::Spree::Stock::InventoryUnitBuilder.prepend self
36 | end
37 | end
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/app/decorators/models/solidus_product_assembly/spree/stock/availability_validator_decorator.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module SolidusProductAssembly
4 | module Spree
5 | module Stock
6 | # Overridden from spree core to make it also check for assembly parts stock
7 | module AvailabilityValidatorDecorator
8 | def validate(line_item)
9 | if line_item.product.assembly?
10 | line_item.quantity_by_variant.each do |variant, variant_quantity|
11 | inventory_units = line_item.inventory_units.where(variant: variant).count
12 | quantity = variant_quantity - inventory_units
13 | next if quantity <= 0
14 | next unless variant
15 |
16 | quantifier = ::Spree::Stock::Quantifier.new(variant)
17 |
18 | next if quantifier.can_supply? quantity
19 |
20 | display_name = variant.name.to_s
21 | display_name += %{ (#{variant.options_text})} if variant.options_text.present?
22 |
23 | line_item.errors[:quantity] << I18n.t(
24 | 'spree.selected_quantity_not_available',
25 | item: display_name.inspect
26 | )
27 | end
28 | else
29 | super
30 | end
31 | end
32 |
33 | ::Spree::Stock::AvailabilityValidator.prepend self
34 | end
35 | end
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/spec/models/spree/inventory_unit_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'spec_helper'
4 |
5 | module Spree
6 | describe InventoryUnit do
7 | subject { described_class.create!(attributes) }
8 |
9 | let!(:order) { create(:order_with_line_items) }
10 | let(:line_item) { order.line_items.first }
11 | let(:product) { line_item.product }
12 | let(:shipment) { create(:shipment, order: order) }
13 |
14 | if Spree.solidus_gem_version < Gem::Version.new('2.5.x')
15 | let(:attributes) { { shipment: shipment, line_item: line_item, variant: line_item.variant, order: order } }
16 | else
17 | let(:attributes) { { shipment: shipment, line_item: line_item, variant: line_item.variant } }
18 | end
19 |
20 | context 'if the unit is not part of an assembly' do
21 | it 'will return the percentage of a line item' do
22 | expect(subject.percentage_of_line_item).to eql(BigDecimal(1))
23 | end
24 | end
25 |
26 | context 'if part of an assembly' do
27 | let(:parts) { (1..2).map { create(:variant) } }
28 |
29 | before do
30 | product.parts << parts
31 | order.create_proposed_shipments
32 | end
33 |
34 | it 'will return the percentage of a line item' do
35 | subject.line_item = line_item
36 | expect(subject.percentage_of_line_item).to eql(BigDecimal(0.5, 2))
37 | end
38 | end
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/app/controllers/spree/admin/parts_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class Spree::Admin::PartsController < Spree::Admin::BaseController
4 | before_action :find_product
5 |
6 | def index
7 | @parts = @product.parts
8 | end
9 |
10 | def remove
11 | @part = Spree::Variant.find(params[:id])
12 | @product.remove_part(@part)
13 | render 'spree/admin/parts/update_parts_table'
14 | end
15 |
16 | def set_count
17 | @part = Spree::Variant.find(params[:id])
18 | @product.set_part_count(@part, params[:count].to_i)
19 | render 'spree/admin/parts/update_parts_table'
20 | end
21 |
22 | def available
23 | if params[:q].blank?
24 | @available_products = []
25 | else
26 | query = "%#{params[:q]}%"
27 | @available_products = Spree::Product.search_can_be_part(query).distinct
28 | end
29 | respond_to do |format|
30 | format.html { render layout: false }
31 | format.js { render layout: false }
32 | end
33 | end
34 |
35 | def create
36 | @part = Spree::Variant.find(params[:part_id])
37 | qty = params[:part_count].to_i
38 | @product.add_part(@part, qty) if qty > 0
39 | render 'spree/admin/parts/update_parts_table'
40 | end
41 |
42 | private
43 |
44 | def find_product
45 | @product = Spree::Product.find_by(slug: params[:product_id])
46 | end
47 |
48 | def model_class
49 | Spree::AssembliesPart
50 | end
51 | end
52 |
--------------------------------------------------------------------------------
/app/views/spree/admin/parts/_parts_table.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%= t('spree.sku') %>
5 | <%= t('spree.name') %>
6 | <%= t('spree.available_on') %>
7 | <%= t('spree.options') %>
8 | <%= t('spree.qty') %>
9 |
10 |
11 |
12 | <% parts.each do |part| %>
13 |
14 | <%= part.sku %>
15 | <%= part.product.name %>
16 | <%= part.product.available_on %>
17 | <%= variant_options part %>
18 | <%= text_field_tag :count, @product.count_of(part) %>
19 |
20 | <%= link_to_with_icon(
21 | 'edit',
22 | t('spree.actions.edit'),
23 | set_count_admin_product_part_url(@product, part),
24 | :class => "set_count_admin_product_part_link save-line-item") %>
25 |
26 | <%= link_to_with_icon(
27 | 'trash',
28 | t('spree.actions.delete'),
29 | remove_admin_product_part_url(@product, part),
30 | class: "remove_admin_product_part_link delete-line-item") %>
31 |
32 |
33 | <% end %>
34 | <% if parts.empty? %>
35 | <%= t('spree.none') %>.
36 | <% end %>
37 |
38 |
39 | <%= javascript_tag("subscribe_product_part_links();") if request.xhr? %>
40 |
--------------------------------------------------------------------------------
/lib/generators/solidus_product_assembly/install/install_generator.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module SolidusProductAssembly
4 | module Generators
5 | class InstallGenerator < Rails::Generators::Base
6 | class_option :auto_run_migrations, type: :boolean, default: false
7 | source_root File.expand_path('templates', __dir__)
8 |
9 | def copy_initializer
10 | template 'initializer.rb', 'config/initializers/solidus_product_assembly.rb'
11 | end
12 |
13 | def add_javascripts
14 | append_file 'vendor/assets/javascripts/spree/backend/all.js', "//= require spree/backend/solidus_product_assembly\n"
15 | end
16 |
17 | def add_stylesheets
18 | inject_into_file 'vendor/assets/stylesheets/spree/backend/all.css', " *= require spree/backend/solidus_product_assembly\n", before: %r{\*/}, verbose: true # rubocop:disable Layout/LineLength
19 | end
20 |
21 | def add_migrations
22 | run 'bin/rails railties:install:migrations FROM=solidus_product_assembly'
23 | end
24 |
25 | def run_migrations
26 | run_migrations = options[:auto_run_migrations] || ['', 'y', 'Y'].include?(ask('Would you like to run the migrations now? [Y/n]')) # rubocop:disable Layout/LineLength
27 | if run_migrations
28 | run 'bin/rails db:migrate'
29 | else
30 | puts 'Skipping bin/rails db:migrate, don\'t forget to run it!' # rubocop:disable Rails/Output
31 | end
32 | end
33 | end
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2020 Spree Commerce Inc. and other contributors.
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without modification,
5 | are permitted provided that the following conditions are met:
6 |
7 | * Redistributions of source code must retain the above copyright notice,
8 | this list of conditions and the following disclaimer.
9 | * Redistributions in binary form must reproduce the above copyright notice,
10 | this list of conditions and the following disclaimer in the documentation
11 | and/or other materials provided with the distribution.
12 | * Neither the name Solidus nor the names of its contributors may be used to
13 | endorse or promote products derived from this software without specific
14 | prior written permission.
15 |
16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
20 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
21 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
22 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
23 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
24 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
25 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
26 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27 |
--------------------------------------------------------------------------------
/solidus_product_assembly.gemspec:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_relative 'lib/solidus_product_assembly/version'
4 |
5 | Gem::Specification.new do |spec|
6 | spec.name = 'solidus_product_assembly'
7 | spec.version = SolidusProductAssembly::VERSION
8 | spec.authors = ['Roman Smirnov']
9 | spec.email = 'roman@railsdog.com'
10 |
11 | spec.summary = 'Make bundle of products to your Solidus store'
12 | spec.description = 'Make bundle of products to your Solidus store'
13 | spec.homepage = 'https://github.com/solidusio-contrib/solidus_product_assembly'
14 | spec.license = 'BSD-3-Clause'
15 |
16 | spec.metadata['homepage_uri'] = spec.homepage
17 | spec.metadata['source_code_uri'] = 'https://github.com/solidusio-contrib/solidus_product_assembly'
18 | spec.metadata['changelog_uri'] = 'https://github.com/solidusio-contrib/solidus_product_assembly/releases'
19 |
20 | spec.required_ruby_version = Gem::Requirement.new('>= 2.5', '< 4')
21 |
22 | # Specify which files should be added to the gem when it is released.
23 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
24 | files = Dir.chdir(__dir__) { `git ls-files -z`.split("\x0") }
25 |
26 | spec.files = files.grep_v(%r{^(test|spec|features)/})
27 | spec.test_files = files.grep(%r{^(test|spec|features)/})
28 | spec.bindir = "exe"
29 | spec.executables = files.grep(%r{^exe/}) { |f| File.basename(f) }
30 | spec.require_paths = ["lib"]
31 |
32 | spec.add_dependency 'solidus_core', '>= 3.2'
33 | spec.add_dependency 'solidus_support', '~> 0.8'
34 | spec.add_dependency 'deface'
35 |
36 | spec.add_development_dependency 'solidus_dev_support', '~> 2.5'
37 | end
38 |
--------------------------------------------------------------------------------
/app/models/spree/order_inventory_assembly.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Spree
4 | # This class has basically the same functionality of Spree core OrderInventory
5 | # except that it takes account of bundle parts and properly creates and removes
6 | # inventory unit for each parts of a bundle
7 | class OrderInventoryAssembly < OrderInventory
8 | attr_reader :product
9 |
10 | def initialize(line_item)
11 | @order = line_item.order
12 | @line_item = line_item
13 | @product = line_item.product
14 | end
15 |
16 | def verify(shipment = nil)
17 | if order.completed? || shipment.present?
18 | line_item.quantity_by_variant.each do |part, total_parts|
19 | existing_parts = line_item.inventory_units.where(variant: part).count
20 |
21 | self.variant = part
22 |
23 | if existing_parts < total_parts
24 | if method(:determine_target_shipment).arity == 1
25 | quantity = total_parts - existing_parts
26 | shipment = determine_target_shipment(quantity) unless shipment
27 | add_to_shipment(shipment, quantity)
28 | else
29 | shipment = determine_target_shipment unless shipment
30 | add_to_shipment(shipment, total_parts - existing_parts)
31 | end
32 | elsif existing_parts > total_parts
33 | quantity = existing_parts - total_parts
34 | if shipment.present?
35 | remove_from_shipment(shipment, quantity)
36 | else
37 | order.shipments.each do |shipment|
38 | break if quantity == 0
39 |
40 | quantity -= remove_from_shipment(shipment, quantity)
41 | end
42 | end
43 | end
44 | end
45 | end
46 | end
47 | end
48 | end
49 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | source 'https://rubygems.org'
4 | git_source(:github) { |repo| "https://github.com/#{repo}.git" }
5 |
6 | branch = ENV.fetch('SOLIDUS_BRANCH', 'main')
7 | gem 'solidus', github: 'solidusio/solidus', branch: branch
8 |
9 | # The solidus_frontend gem has been pulled out since v3.2
10 | if branch >= 'v3.2'
11 | gem 'solidus_frontend'
12 | elsif branch == 'main'
13 | gem 'solidus_frontend', github: 'solidusio/solidus_frontend'
14 | else
15 | gem 'solidus_frontend', github: 'solidusio/solidus', branch: branch
16 | end
17 |
18 | # Needed to help Bundler figure out how to resolve dependencies,
19 | # otherwise it takes forever to resolve them.
20 | # See https://github.com/bundler/bundler/issues/6677
21 | gem 'rails', '>0.a'
22 |
23 | # Provides basic authentication functionality for testing parts of your engine
24 | gem 'solidus_auth_devise'
25 |
26 | gem 'friendly_id-globalize', github: 'norman/friendly_id-globalize', branch: "master"
27 | gem 'solidus_globalize', github: 'solidusio-contrib/solidus_globalize'
28 |
29 | case ENV.fetch('DB', nil)
30 | when 'mysql'
31 | gem 'mysql2'
32 | when 'postgresql'
33 | gem 'pg'
34 | else
35 | gem 'sqlite3'
36 | end
37 |
38 | # While we still support Ruby < 3 we need to workaround a limitation in
39 | # the 'async' gem that relies on the latest ruby, since RubyGems doesn't
40 | # resolve gems based on the required ruby version.
41 | gem 'async', '< 3' if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('3')
42 |
43 | gemspec
44 |
45 | # Use a local Gemfile to include development dependencies that might not be
46 | # relevant for the project or for other contributors, e.g. pry-byebug.
47 | #
48 | # We use `send` instead of calling `eval_gemfile` to work around an issue with
49 | # how Dependabot parses projects: https://github.com/dependabot/dependabot-core/issues/1658.
50 | send(:eval_gemfile, 'Gemfile-local') if File.exist? 'Gemfile-local'
51 |
--------------------------------------------------------------------------------
/spec/models/spree/product_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'spec_helper'
4 |
5 | describe Spree::Product do
6 | before do
7 | @product = FactoryBot.create(:product, name: "Foo Bar")
8 | @master_variant = Spree::Variant.where(is_master: true).find_by(product_id: @product.id)
9 | end
10 |
11 | describe ".search_can_be_part" do
12 | subject { described_class.search_can_be_part("matching") }
13 |
14 | let!(:name_matching_product) { create :product, can_be_part: true, name: "matching" }
15 | let!(:sku_matching_product) { create :product, can_be_part: true, master: variant }
16 | let(:variant) { create :master_variant, sku: "matching" }
17 |
18 | before do
19 | create :product, can_be_part: false, name: "matching"
20 | create :product, deleted_at: 1.day.ago, can_be_part: true, name: "matching"
21 | create :product, can_be_part: true, name: "Something else"
22 | end
23 |
24 | it "returns non-deleted products matching the search that can be parts" do
25 | expect(subject).to contain_exactly(name_matching_product, sku_matching_product)
26 | end
27 | end
28 |
29 | describe "Spree::Product Assembly" do
30 | before do
31 | @product = create(:product)
32 | @part1 = create(:product, can_be_part: true)
33 | @part2 = create(:product, can_be_part: true)
34 | @product.add_part @part1.master, 1
35 | @product.add_part @part2.master, 4
36 | end
37 |
38 | it "is an assembly" do
39 | expect(@product).to be_assembly
40 | end
41 |
42 | it "cannot be part" do
43 | expect(@product).to be_assembly
44 | @product.can_be_part = true
45 | expect(@product).not_to be_valid
46 | expect(@product.errors[:can_be_part]).to eq ["assembly can't be part"]
47 | end
48 |
49 | it 'changing part qty changes count on_hand' do
50 | @product.set_part_count(@part2.master, 2)
51 | expect(@product.count_of(@part2.master)).to eq 2
52 | end
53 | end
54 | end
55 |
--------------------------------------------------------------------------------
/app/decorators/models/solidus_product_assembly/spree/line_item_decorator.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module SolidusProductAssembly
4 | module Spree
5 | module LineItemDecorator
6 | def self.prepended(base)
7 | base.class_eval do
8 | scope :assemblies, -> { joins(product: :parts).distinct }
9 | end
10 | end
11 |
12 | def any_units_shipped?
13 | inventory_units.any?(&:shipped?)
14 | end
15 |
16 | # The parts that apply to this particular LineItem. Usually `product#parts`, but
17 | # provided as a hook if you want to override and customize the parts for a specific
18 | # LineItem.
19 | delegate :parts, to: :product
20 |
21 | # The number of the specified variant that make up this LineItem. By default, calls
22 | # `product#count_of`, but provided as a hook if you want to override and customize
23 | # the parts available for a specific LineItem. Note that if you only customize whether
24 | # a variant is included in the LineItem, and don't customize the quantity of that part
25 | # per LineItem, you shouldn't need to override this method.
26 | delegate :count_of, to: :product
27 |
28 | def quantity_by_variant
29 | if product.assembly?
30 | {}.tap { |hash| product.assemblies_parts.each { |ap| hash[ap.part] = ap.count * quantity } }
31 | else
32 | { variant => quantity }
33 | end
34 | end
35 |
36 | private
37 |
38 | def update_inventory
39 | saved_changes = respond_to?(:saved_changes?) ? saved_changes? : changed?
40 | if (saved_changes || target_shipment.present?) && order.has_checkout_step?("delivery")
41 | if product.assembly?
42 | ::Spree::OrderInventoryAssembly.new(self).verify(target_shipment)
43 | else
44 | ::Spree::OrderInventory.new(order, self).verify(target_shipment)
45 | end
46 | end
47 | end
48 |
49 | ::Spree::LineItem.prepend self
50 | end
51 | end
52 | end
53 |
--------------------------------------------------------------------------------
/app/views/spree/admin/parts/available.html.erb:
--------------------------------------------------------------------------------
1 |
8 |
9 | <%= t('spree.available_parts') %>
10 |
11 |
12 |
13 | <%= t('spree.name') %>
14 | <%= t('spree.available_on') %>
15 | <%= t('spree.options') %>
16 | <%= t('spree.qty') %>
17 |
18 |
19 |
20 |
21 | <% @available_products.each do |product| %>
22 |
23 |
24 | <%= product.name %>
25 | <%= product.available_on %>
26 |
27 | <% if product.has_variants? %>
28 | <%= select_tag "part[id]",
29 | options_for_select(product.variants.map { |v| [variant_options(v), v.id] }) %>
30 | <% else %>
31 | <%= hidden_field_tag "part[id]", product.master.id %>
32 | <%= t(:no_variants) %>
33 | <% end %>
34 |
35 | <%= text_field_tag "part[count]", 1 %>
36 |
37 | <%= link_to(t('spree.select'),
38 | admin_product_parts_path(@product),
39 | :class => "add_product_part_link btn btn-primary") %>
40 |
41 |
42 | <% end %>
43 | <% if @available_products.empty? %>
44 | <%= t('spree.no_match_found') %>.
45 | <% end %>
46 |
47 |
48 |
49 | <%= javascript_tag do %>
50 | $("a.add_product_part_link").click(function(){
51 | part_id_val = $('select option:selected', $(this).parent().parent()).val() ||
52 | $('input:first', $(this).parent().parent()).val();
53 | params = { part_count : $('input:last', $(this).parent().parent()).val(),
54 | part_id : part_id_val};
55 | return make_post_request($(this), params);
56 | });
57 | <% end %>
58 |
--------------------------------------------------------------------------------
/app/views/spree/admin/parts/available.js.erb:
--------------------------------------------------------------------------------
1 |
8 | <%= t('spree.available_parts') %>
9 |
10 |
11 |
12 | <%= t('spree.name') %>
13 | <%= t('spree.available_on') %>
14 | <%= t('spree.options') %>
15 | <%= t('spree.qty') %>
16 |
17 |
18 |
19 |
20 | <% @available_products.each do |product| %>
21 |
22 |
23 | <%= product.name %>
24 | <%= product.available_on %>
25 |
26 | <% if product.has_variants? %>
27 | <%= select_tag "part[id]",
28 | options_for_select(product.variants.map { |v| [variant_options(v), v.id] }) %>
29 | <% else %>
30 | <%= hidden_field_tag "part[id]", product.master.id %>
31 | <%= t('spree.no_variants') %>
32 | <% end %>
33 |
34 | <%= text_field_tag "part[count]", 1 %>
35 |
36 | <%= link_to(icon('add') + ' ' + t('spree.select'),
37 | admin_product_parts_path(@product),
38 | :class => "add_product_part_link") %>
39 |
40 |
41 | <% end %>
42 | <% if @available_products.empty? %>
43 | <%= t('spree.no_match_found') %>.
44 | <% end %>
45 |
46 |
47 |
48 | <%= javascript_tag do %>
49 | $("a.add_product_part_link").click(function(){
50 | part_id_val = $('select option:selected', $(this).parent().parent()).val() ||
51 | $('input:first', $(this).parent().parent()).val();
52 | params = { part_count : $('input:last', $(this).parent().parent()).val(),
53 | part_id : part_id_val};
54 | return make_post_request($(this), params);
55 | });
56 | <% end %>
57 |
--------------------------------------------------------------------------------
/app/views/spree/admin/orders/_assemblies.html.erb:
--------------------------------------------------------------------------------
1 | <% if order.line_items.assemblies.any? %>
2 |
3 |
4 | <%= t('spree.product_bundles') %>
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | <%= Spree::Product.human_attribute_name(:name) %>
20 | <%= Spree::LineItem.human_attribute_name(:price) %>
21 | <%= Spree::LineItem.human_attribute_name(:quantity) %>
22 | <%= Spree::LineItem.human_attribute_name(:total) %>
23 |
24 |
25 |
26 |
27 | <% order.line_items.assemblies.each do |item| %>
28 | <%= content_tag :tr, class: 'line-item', id: "line-item-#{item.id}", data: { line_item_id: item.id } do %>
29 | <%= render 'spree/shared/image',
30 | image: (item.variant.gallery.images.first || item.variant.product.gallery.images.first),
31 | size: :mini %>
32 |
33 | <%= item.variant.product.name %> <%= "(" + variant_options(item.variant) + ")" unless item.variant.option_values.empty? %>
34 | <% if item.variant.sku.present? %>
35 | <%= Spree::Variant.human_attribute_name(:sku) %>: <%= item.variant.sku %>
36 | <% end %>
37 |
38 | <%= item.single_money.to_html %>
39 |
40 | <%= item.quantity %>
41 |
42 | <%= line_item_shipment_price(item, item.quantity) %>
43 | <% end %>
44 | <% end %>
45 |
46 |
47 | <% end %>
48 |
--------------------------------------------------------------------------------
/app/views/spree/admin/parts/index.html.erb:
--------------------------------------------------------------------------------
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 |
6 | <%= render :partial => "parts_table", :locals => {:parts => @parts} %>
7 |
8 |
9 | <%= form_tag('#') do %>
10 | <%= t('spree.search') %>:
11 |
12 | Search
13 | <% end %>
14 |
15 |
16 |
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 -%>
71 |
--------------------------------------------------------------------------------
/spec/models/spree/stock/coordinator_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'spec_helper'
4 |
5 | # Spree::Stock::Coordinator was refactored in Solidus to hide keep private
6 | # information about packages, we have to be sneaky to reach inside
7 | def inventory_units(coordinator)
8 | coordinator.shipments.flat_map(&:inventory_units)
9 | end
10 |
11 | module Spree
12 | module Stock
13 | coordinator_class =
14 | if Spree.solidus_gem_version < Gem::Version.new('2.4.x')
15 | Coordinator
16 | else
17 | SimpleCoordinator
18 | end
19 | describe coordinator_class do
20 | subject { described_class.new(order) }
21 |
22 | context "order shares variant as individual and within bundle" do
23 | include_context "product is ordered as individual and within a bundle"
24 |
25 | before { StockItem.update_all 'count_on_hand = 10' }
26 |
27 | context "bundle part requires more units than individual product" do
28 | before { order.contents.add(bundle_variant, 5) }
29 |
30 | let(:bundle_item_quantity) { order.find_line_item_by_variant(bundle_variant).quantity }
31 |
32 | it "calculates items quantity properly" do
33 | expected_units_on_package = order.line_items.to_a.sum(&:quantity) - bundle_item_quantity + (bundle.parts.count * bundle_item_quantity)
34 |
35 | expect(inventory_units(subject).size).to eql expected_units_on_package
36 | end
37 | end
38 | end
39 |
40 | context "multiple stock locations" do
41 | let!(:stock_locations) { (1..3).map { create(:stock_location) } }
42 |
43 | let(:order) { create(:order_with_line_items) }
44 | let(:parts) { (1..3).map { create(:variant) } }
45 |
46 | let(:bundle_variant) { order.variants.first }
47 | let(:bundle) { bundle_variant.product }
48 |
49 | let(:bundle_item_quantity) { order.find_line_item_by_variant(bundle_variant).quantity }
50 |
51 | before { bundle.parts << parts }
52 |
53 | it "haha" do
54 | expected_units_on_package = order.line_items.to_a.sum(&:quantity) - bundle_item_quantity + (bundle.parts.count * bundle_item_quantity)
55 | expect(inventory_units(subject).size).to eql expected_units_on_package
56 | end
57 | end
58 | end
59 | end
60 | end
61 |
--------------------------------------------------------------------------------
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | version: 2.1
2 |
3 | orbs:
4 | # Required for feature specs.
5 | browser-tools: circleci/browser-tools@1.1
6 |
7 | # Always take the latest version of the orb, this allows us to
8 | # run specs against Solidus supported versions only without the need
9 | # to change this configuration every time a Solidus version is released
10 | # or goes EOL.
11 | solidusio_extensions: solidusio/extensions@volatile
12 |
13 | jobs:
14 | run-specs:
15 | parameters:
16 | solidus:
17 | type: string
18 | default: main
19 | db:
20 | type: string
21 | default: "postgres"
22 | ruby:
23 | type: string
24 | default: "3.2"
25 | executor:
26 | name: solidusio_extensions/<< parameters.db >>
27 | ruby_version: << parameters.ruby >>
28 | steps:
29 | - checkout
30 | - browser-tools/install-chrome
31 | - solidusio_extensions/run-tests-solidus-<< parameters.solidus >>
32 |
33 | lint-code:
34 | executor:
35 | name: solidusio_extensions/sqlite
36 | ruby_version: "3.0"
37 | steps:
38 | - solidusio_extensions/lint-code
39 |
40 | workflows:
41 | "Run specs on supported Solidus versions":
42 | jobs:
43 | - run-specs:
44 | name: &name "run-specs-solidus-<< matrix.solidus >>-ruby-<< matrix.ruby >>-db-<< matrix.db >>"
45 | matrix:
46 | parameters: { solidus: ["main"], ruby: ["3.2"], db: ["postgres"] }
47 | - run-specs:
48 | name: *name
49 | matrix:
50 | parameters: { solidus: ["current"], ruby: ["3.1"], db: ["mysql"] }
51 | - run-specs:
52 | name: *name
53 | matrix:
54 | parameters: { solidus: ["older"], ruby: ["3.0"], db: ["sqlite"] }
55 | - lint-code
56 |
57 | "Weekly run specs against master":
58 | triggers:
59 | - schedule:
60 | cron: "0 0 * * 4" # every Thursday
61 | filters:
62 | branches:
63 | only:
64 | - master
65 | jobs:
66 | - run-specs:
67 | name: *name
68 | matrix:
69 | parameters: { solidus: ["main"], ruby: ["3.2"], db: ["postgres"] }
70 | - run-specs:
71 | name: *name
72 | matrix:
73 | parameters: { solidus: ["current"], ruby: ["3.1"], db: ["mysql"] }
74 |
--------------------------------------------------------------------------------
/spec/models/spree/order_inventory_assembly_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'spec_helper'
4 |
5 | module Spree
6 | describe OrderInventoryAssembly do
7 | subject { described_class.new(line_item) }
8 |
9 | let(:order) { create(:order_with_line_items) }
10 | let(:line_item) { order.line_items.first }
11 | let(:bundle) { line_item.product }
12 | let(:parts) { (1..3).map { create(:variant) } }
13 |
14 | before do
15 | bundle.parts << [parts]
16 | bundle.set_part_count(parts.first, 3)
17 |
18 | line_item.update!(quantity: 3)
19 | order.reload.create_proposed_shipments
20 | allow(order).to receive(:completed?).and_return(true)
21 | end
22 |
23 | context "inventory units count" do
24 | it "calculates the proper value for the bundle" do
25 | expected_units_count = line_item.quantity * bundle.assemblies_parts.to_a.sum(&:count)
26 | expect(subject.inventory_units.count).to eql(expected_units_count)
27 | end
28 | end
29 |
30 | context "verify line item units" do
31 | let!(:original_units_count) { subject.inventory_units.count }
32 |
33 | context "quantity increases" do
34 | before { subject.line_item.quantity += 1 }
35 |
36 | it "inserts new inventory units for every bundle part" do
37 | expected_units_count = original_units_count + bundle.assemblies_parts.to_a.sum(&:count)
38 | subject.verify
39 |
40 | # needs to reload so that inventory units are fetched from the updated order.shipments
41 | updated_units_count = described_class.new(line_item.reload).inventory_units.count
42 | expect(updated_units_count).to eql(expected_units_count)
43 | end
44 | end
45 |
46 | context "quantity decreases" do
47 | before { subject.line_item.quantity -= 1 }
48 |
49 | it "remove inventory units for every bundle part" do
50 | expected_units_count = original_units_count - bundle.assemblies_parts.to_a.sum(&:count)
51 | subject.verify
52 |
53 | # needs to reload so that inventory units are fetched from the updated order.shipments
54 | updated_units_count = described_class.new(line_item.reload).inventory_units.count
55 | expect(updated_units_count).to eql(expected_units_count)
56 | end
57 | end
58 | end
59 | end
60 | end
61 |
--------------------------------------------------------------------------------
/spec/models/spree/line_item_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'spec_helper'
4 |
5 | module Spree
6 | describe LineItem do
7 | let!(:order) { create(:order_with_line_items) }
8 | let(:line_item) { order.line_items.first }
9 | let(:product) { line_item.product }
10 | let(:variant) { line_item.variant }
11 | let(:inventory) { double('order_inventory') }
12 |
13 | context "bundle parts stock" do
14 | let(:parts) { (1..2).map { create(:variant) } }
15 |
16 | before { product.parts << parts }
17 |
18 | context "one of them not in stock" do
19 | before do
20 | part = product.parts.first
21 | part.stock_items.update_all backorderable: false
22 |
23 | expect(part).not_to be_in_stock
24 | end
25 |
26 | it "doesn't save line item quantity" do
27 | order.contents.add(variant, 10)
28 | expect { order.contents.advance }.to raise_error Spree::Order::InsufficientStock
29 | end
30 | end
31 |
32 | context "in stock" do
33 | before do
34 | parts.each do |part|
35 | part.stock_items.first.set_count_on_hand(10)
36 | end
37 | expect(parts[0]).to be_in_stock
38 | expect(parts[1]).to be_in_stock
39 | end
40 |
41 | it "saves line item quantity" do
42 | line_item = order.contents.add(variant, 10)
43 | expect(line_item).to be_valid
44 | end
45 | end
46 | end
47 |
48 | context "updates bundle product line item" do
49 | let(:parts) { (1..2).map { create(:variant) } }
50 |
51 | before do
52 | product.parts << parts
53 | order.create_proposed_shipments
54 | end
55 |
56 | it "verifies inventory units via OrderInventoryAssembly" do
57 | expect(OrderInventoryAssembly).to receive(:new).with(line_item).and_return(inventory)
58 | expect(inventory).to receive(:verify).with(line_item.target_shipment)
59 | line_item.quantity = 2
60 | line_item.save
61 | end
62 | end
63 |
64 | context "updates regular line item" do
65 | it "verifies inventory units via OrderInventory" do
66 | expect(OrderInventory).to receive(:new).with(line_item.order, line_item).and_return(inventory)
67 | expect(inventory).to receive(:verify).with(line_item.target_shipment)
68 | line_item.quantity = 2
69 | line_item.save
70 | end
71 | end
72 | end
73 | end
74 |
--------------------------------------------------------------------------------
/spec/models/spree/stock/availability_validator_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'spec_helper'
4 |
5 | module Spree
6 | module Stock
7 | describe AvailabilityValidator, type: :model do
8 | context "line item has no parts" do
9 | subject { described_class.new }
10 |
11 | let!(:line_item) { create(:line_item, quantity: 5) }
12 | let!(:product) { line_item.product }
13 |
14 | it 'delegates to the original Solidus implementation' do
15 | expect(subject).to receive(:is_valid?).with(line_item)
16 | subject.validate(line_item)
17 | end
18 | end
19 |
20 | context "line item has parts" do
21 | subject { described_class.new }
22 |
23 | let!(:order) { create(:order_with_line_items) }
24 | let(:line_item) { order.line_items.first }
25 | let(:product) { line_item.product }
26 | let(:variant) { line_item.variant }
27 | let(:parts) { (1..2).map { create(:variant) } }
28 |
29 | before { product.parts << parts }
30 |
31 | it 'is valid when supply of all parts is sufficient' do
32 | allow_any_instance_of(Stock::Quantifier).to receive(:can_supply?).and_return(true)
33 | expect(line_item).not_to receive(:errors)
34 | subject.validate(line_item)
35 | end
36 |
37 | it 'is invalid when supplies of all parts are insufficent' do
38 | allow_any_instance_of(Stock::Quantifier).to receive(:can_supply?).and_return(false)
39 | expect(line_item.errors).to receive(:[]).exactly(line_item.parts.size).times.with(:quantity).and_return([])
40 | subject.validate(line_item)
41 | end
42 |
43 | it 'is invalid when supply of 1 part is insufficient' do
44 | allow_any_instance_of(Stock::Quantifier).to receive(:can_supply?).and_return(false)
45 | create_list(:inventory_unit, 5, line_item: line_item, variant: line_item.parts.first, order: order, shipment: order.shipments.first)
46 | expect(line_item.errors).to receive(:[]).once.with(:quantity).and_return([])
47 | subject.validate(line_item)
48 | end
49 |
50 | it 'is valid when supply of each part is sufficient' do
51 | line_item.parts.each do |part|
52 | allow(part).to receive(:inventory_units).and_return([double(variant: part)] * 5)
53 | end
54 | expect(line_item).not_to receive(:errors)
55 | subject.validate(line_item)
56 | end
57 | end
58 | end
59 | end
60 | end
61 |
--------------------------------------------------------------------------------
/app/decorators/models/solidus_product_assembly/spree/shipment_decorator.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module SolidusProductAssembly
4 | module Spree
5 | module ShipmentDecorator
6 | # Overriden from Spree core as a product bundle part should not be put
7 | # together with an individual product purchased (even though they're the
8 | # very same variant) That is so we can tell the store admin which units
9 | # were purchased individually and which ones as parts of the bundle
10 | #
11 | # Account for situations where we can't track the line_item for a variant.
12 | # This should avoid exceptions when users upgrade from spree 1.3
13 | #
14 | # TODO Can possibly be removed as well. We already override the manifest
15 | # partial so we can get the product there
16 | def manifest
17 | items = []
18 | inventory_units.joins(:variant).includes(:variant, :line_item).group_by(&:variant).each do |variant, units|
19 | units.group_by(&:line_item).each do |line_item, units|
20 | states = {}
21 | units.group_by(&:state).each { |state, iu| states[state] = iu.count }
22 | line_item ||= order.find_line_item_by_variant(variant)
23 |
24 | part = line_item ? line_item.product.assembly? : false
25 | items << OpenStruct.new(part: part,
26 | product: line_item.try(:product),
27 | line_item: line_item,
28 | variant: variant,
29 | quantity: units.length,
30 | states: states)
31 | end
32 | end
33 | items
34 | end
35 |
36 | # There might be scenarios where we don't want to display every single
37 | # variant on the shipment. e.g. when ordering a product bundle that includes
38 | # 5 other parts. Frontend users should only see the product bundle as a
39 | # single item to ship
40 | def line_item_manifest
41 | inventory_units.includes(:line_item, :variant).group_by(&:line_item).map do |line_item, units|
42 | states = {}
43 | units.group_by(&:state).each { |state, iu| states[state] = iu.count }
44 | OpenStruct.new(line_item: line_item, variant: line_item.variant, quantity: units.length, states: states)
45 | end
46 | end
47 |
48 | def inventory_units_for_item(line_item, variant)
49 | inventory_units.where(line_item_id: line_item.id, variant_id: variant.id)
50 | end
51 |
52 | ::Spree::Shipment.prepend self
53 | end
54 | end
55 | end
56 |
--------------------------------------------------------------------------------
/app/decorators/models/solidus_product_assembly/spree/product_decorator.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module SolidusProductAssembly
4 | module Spree
5 | module ProductDecorator
6 | def self.prepended(base)
7 | base.class_eval do
8 | has_and_belongs_to_many :parts, class_name: "Spree::Variant",
9 | join_table: "spree_assemblies_parts",
10 | foreign_key: "assembly_id", association_foreign_key: "part_id"
11 |
12 | has_many :assemblies_parts, class_name: "Spree::AssembliesPart",
13 | foreign_key: "assembly_id"
14 |
15 | scope :individual_saled, -> { where(individual_sale: true) }
16 |
17 | if defined?(SolidusGlobalize)
18 | scope :search_can_be_part, ->(query){
19 | not_deleted.available.joins(:master)
20 | .joins(:translations)
21 | .where(
22 | ::Spree::Product::Translation.arel_table["name"].matches(query)
23 | .or(::Spree::Variant.arel_table["sku"].matches(query))
24 | )
25 | .where(can_be_part: true)
26 | .limit(30)
27 | }
28 | else
29 | scope :search_can_be_part, ->(query){
30 | not_deleted.available.joins(:master)
31 | .where(arel_table["name"].matches(query).or(::Spree::Variant.arel_table["sku"].matches(query)))
32 | .where(can_be_part: true)
33 | .limit(30)
34 | }
35 | end
36 |
37 | validate :assembly_cannot_be_part, if: :assembly?
38 | end
39 | end
40 |
41 | def add_part(variant, count = 1)
42 | set_part_count(variant, count_of(variant) + count)
43 | end
44 |
45 | def remove_part(variant)
46 | set_part_count(variant, 0)
47 | end
48 |
49 | def set_part_count(variant, count)
50 | ap = assemblies_part(variant)
51 | if count > 0
52 | ap.count = count
53 | ap.save
54 | else
55 | ap.destroy
56 | end
57 | reload
58 | end
59 |
60 | def assembly?
61 | parts.present?
62 | end
63 |
64 | def count_of(variant)
65 | ap = assemblies_part(variant)
66 | # This checks persisted because the default count is 1
67 | ap.persisted? ? ap.count : 0
68 | end
69 |
70 | def assembly_cannot_be_part
71 | errors.add(:can_be_part, I18n.t('spree.assembly_cannot_be_part')) if can_be_part
72 | end
73 |
74 | private
75 |
76 | def assemblies_part(variant)
77 | ::Spree::AssembliesPart.get(id, variant.id)
78 | end
79 |
80 | ::Spree::Product.prepend self
81 | end
82 | end
83 | end
84 |
--------------------------------------------------------------------------------
/app/views/spree/admin/orders/_stock_item.html.erb:
--------------------------------------------------------------------------------
1 | <% shipment.manifest.each do |item| %>
2 |
3 | <%= render 'spree/shared/image',
4 | image: (item.variant.gallery.images.first || item.variant.product.gallery.images.first),
5 | size: :mini %>
6 |
7 | <%= link_to item.variant.product.name, edit_admin_product_path(item.variant.product) %> <%= "(" + variant_options(item.variant) + ")" unless item.variant.option_values.empty? %>
8 | <% if item.part && item.line_item %>
9 | <%= t('spree.part_of_bundle', sku: item.product.sku) %>
10 | <% elsif item.variant.sku.present? %>
11 | <%= Spree::Variant.human_attribute_name(:sku) %>: <%= item.variant.sku %>
12 | <% end %>
13 |
14 |
15 | <% if item.part %>
16 | ---
17 | <% else %>
18 | <%= item.line_item.single_money.to_html if item.line_item %>
19 | <% end %>
20 |
21 |
22 | <% item.states.each do |state,count| %>
23 | <%= count %> x <%= state.humanize.downcase %>
24 | <% end %>
25 |
26 | <% unless shipment.shipped? %>
27 |
28 | <%= number_field_tag :quantity, item.quantity, :min => 0, :class => "line_item_quantity", :size => 5 %>
29 |
30 | <% end %>
31 |
32 | <% if item.part %>
33 | ---
34 | <% else %>
35 | <%= line_item_shipment_price(item.line_item, item.quantity) if item.line_item %>
36 | <% end %>
37 |
38 |
39 | <% if !shipment.shipped? %>
40 | <% if can? :update, item %>
41 | <%= button_tag '', :class => 'save-item fa fa-ok no-text with-tip', :data => {'shipment-number' => shipment.number, 'variant-id' => item.variant.id, :action => 'save'}, :title => t('spree.actions.save'), :style => 'display: none' %>
42 | <%= link_to '', :class => 'cancel-item fa fa-cancel no-text with-tip', :data => {:action => 'cancel'}, :title => t('spree.actions.cancel'), :style => 'display: none' %>
43 | <%= button_tag '', :class => 'split-item fa fa-arrows-h no-text with-tip', :data => {:action => 'split', 'variant-id' => item.variant.id}, :title => t('spree.actions.split') %>
44 | <%= button_tag '', :class => 'delete-item fa fa-trash no-text with-tip', :data => {'shipment-number' => shipment.number, 'variant-id' => item.variant.id, :action => 'remove'}, :title => t('spree.actions.delete') %>
45 | <% end %>
46 | <% end %>
47 |
48 |
49 | <% end %>
50 |
--------------------------------------------------------------------------------
/bin/sandbox:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -e
4 | if [ ! -z $DEBUG ]
5 | then
6 | set -x
7 | fi
8 |
9 | case "$DB" in
10 | postgres|postgresql)
11 | RAILSDB="postgresql"
12 | ;;
13 | mysql)
14 | RAILSDB="mysql"
15 | ;;
16 | sqlite3|sqlite)
17 | RAILSDB="sqlite3"
18 | ;;
19 | '')
20 | echo "~~> Use 'export DB=[postgres|mysql|sqlite]' to control the DB adapter"
21 | RAILSDB="sqlite3"
22 | ;;
23 | *)
24 | echo "Invalid value specified for the Solidus sandbox: DB=\"$DB\"."
25 | echo "Please use 'postgres', 'mysql', or 'sqlite' instead."
26 | exit 1
27 | ;;
28 | esac
29 | echo "~~> Using $RAILSDB as the database engine"
30 |
31 | if [ -n $SOLIDUS_BRANCH ]
32 | then
33 | BRANCH=$SOLIDUS_BRANCH
34 | else
35 | echo "~~> Use 'export SOLIDUS_BRANCH=[master|v3.2|...]' to control the Solidus branch"
36 | BRANCH="master"
37 | fi
38 | echo "~~> Using branch $BRANCH of solidus"
39 |
40 | if [ -z $SOLIDUS_FRONTEND ]
41 | then
42 | echo "~~> Use 'export SOLIDUS_FRONTEND=[solidus_frontend|solidus_starter_frontend]' to control the Solidus frontend"
43 | SOLIDUS_FRONTEND="solidus_frontend"
44 | fi
45 | echo "~~> Using branch $SOLIDUS_FRONTEND as the solidus frontend"
46 |
47 | extension_name="solidus_product_assembly"
48 |
49 | # Stay away from the bundler env of the containing extension.
50 | function unbundled {
51 | ruby -rbundler -e'b = proc {system *ARGV}; Bundler.respond_to?(:with_unbundled_env) ? Bundler.with_unbundled_env(&b) : Bundler.with_clean_env(&b)' -- $@
52 | }
53 |
54 | rm -rf ./sandbox
55 | unbundled bundle exec rails new sandbox --database="$RAILSDB" \
56 | --skip-bundle \
57 | --skip-git \
58 | --skip-keeps \
59 | --skip-rc \
60 | --skip-spring \
61 | --skip-test \
62 | --skip-javascript
63 |
64 | if [ ! -d "sandbox" ]; then
65 | echo 'sandbox rails application failed'
66 | exit 1
67 | fi
68 |
69 | cd ./sandbox
70 | cat <> Gemfile
71 | gem 'solidus', github: 'solidusio/solidus', branch: '$BRANCH'
72 | gem 'rails-i18n'
73 | gem 'solidus_i18n'
74 |
75 | gem '$extension_name', path: '..'
76 |
77 | group :test, :development do
78 | platforms :mri do
79 | gem 'pry-byebug'
80 | end
81 | end
82 | RUBY
83 |
84 | unbundled bundle install --gemfile Gemfile
85 |
86 | unbundled bundle exec rake db:drop db:create
87 |
88 | unbundled bundle exec rails generate solidus:install \
89 | --auto-accept \
90 | --user_class=Spree::User \
91 | --enforce_available_locales=true \
92 | --with-authentication=true \
93 | --payment-method=none \
94 | --frontend=${SOLIDUS_FRONTEND} \
95 | $@
96 |
97 | unbundled bundle exec rails generate solidus:auth:install --auto-run-migrations
98 | unbundled bundle exec rails generate ${extension_name}:install --auto-run-migrations
99 |
100 | echo
101 | echo "🚀 Sandbox app successfully created for $extension_name!"
102 | echo "🧪 This app is intended for test purposes."
103 |
--------------------------------------------------------------------------------
/app/views/spree/admin/orders/_stock_contents_2_3.html.erb:
--------------------------------------------------------------------------------
1 | <%= render 'stock_item', shipment: shipment %>
2 |
3 | <% unless shipment.shipped? %>
4 |
5 |
6 |
7 | <%= label_tag 'selected_shipping_rate_id', t('spree.shipping_method') %>
8 | <%= select_tag :selected_shipping_rate_id,
9 | options_for_select(shipment.shipping_rates.map {|sr| ["#{sr.name} #{sr.display_price}", sr.id] }, shipment.selected_shipping_rate_id),
10 | {:class => 'select2 fullwidth', :data => {'shipment-number' => shipment.number } } %>
11 |
12 |
13 |
14 | <% if can? :update, shipment %>
15 | <%= link_to '', '#', :class => 'save-method fa fa-ok no-text with-tip',
16 | :data => {'shipment-number' => shipment.number, :action => 'save'}, title: t('spree.actions.save') %>
17 | <%= link_to '', '#', :class => 'cancel-method fa fa-cancel no-text with-tip',
18 | :data => {:action => 'cancel'}, :title => t('spree.actions.cancel') %>
19 | <% end %>
20 |
21 |
22 | <% end %>
23 |
24 |
25 | <% if shipment.shipping_method %>
26 |
27 | <%= shipment.shipping_method.name %>
28 |
29 |
30 | <%= shipment.display_cost %>
31 |
32 | <% else %>
33 | <%= t('spree.no_shipping_method_selected') %>
34 | <% end %>
35 |
36 |
37 | <% if can? :update, shipment %>
38 | <%= link_to '', '#', :class => 'edit-method fa fa-edit no-text with-tip', :data => {:action => 'edit'}, :title => t('spree.actions.edit') %>
39 | <% end %>
40 |
41 |
42 |
43 |
44 |
45 | <%= t('spree.tracking_number') %>:
46 | <%= text_field_tag :tracking, shipment.tracking %>
47 |
48 |
49 | <% if can? :update, shipment %>
50 | <%= link_to '', '#', :class => 'save-tracking fa fa-ok no-text with-tip', :data => {'shipment-number' => shipment.number, :action => 'save'}, :title => t('spree.actions.save') %>
51 | <%= link_to '', '#', :class => 'cancel-tracking fa fa-cancel no-text with-tip', :data => {:action => 'cancel'}, :title => t('spree.actions.cancel') %>
52 | <% end %>
53 |
54 |
55 |
56 |
57 |
58 | <% if shipment.tracking.present? %>
59 | <%= shipment.tracking %>
60 | <% else %>
61 | <%= t('spree.no_tracking_present') %>
62 | <% end %>
63 |
64 |
65 | <% if can? :update, shipment %>
66 | <%= link_to '', '#', :class => 'edit-tracking fa fa-edit no-text with-tip', :data => {:action => 'edit'}, :title => t('spree.actions.edit') %>
67 | <% end %>
68 |
69 |
70 |
--------------------------------------------------------------------------------
/app/views/spree/admin/orders/_stock_contents_2_4.html.erb:
--------------------------------------------------------------------------------
1 | <%= render 'stock_item', shipment: shipment %>
2 |
3 | <% unless shipment.shipped? %>
4 |
5 |
6 |
7 | <%= label_tag 'selected_shipping_rate_id', t('spree.shipping_method') %>
8 | <%= select_tag :selected_shipping_rate_id,
9 | options_for_select(shipment.shipping_rates.map {|sr| ["#{sr.name} #{sr.display_price}", sr.id] }, shipment.selected_shipping_rate_id),
10 | {:class => 'select2 fullwidth', :data => {'shipment-number' => shipment.number } } %>
11 |
12 |
13 |
14 | <% if can? :update, shipment %>
15 | <%= button_tag '', :class => 'save-method fa fa-ok no-text with-tip',
16 | :data => {'shipment-number' => shipment.number, :action => 'save'}, title: t('spree.actions.save') %>
17 | <%= button_tag '', :class => 'cancel-method fa fa-cancel no-text with-tip',
18 | :data => {:action => 'cancel'}, :title => t('spree.actions.cancel') %>
19 | <% end %>
20 |
21 |
22 | <% end %>
23 |
24 |
25 | <% if shipment.shipping_method %>
26 |
27 | <%= shipment.shipping_method.name %>
28 |
29 |
30 | <%= shipment.display_cost %>
31 |
32 | <% else %>
33 | <%= t('spree.no_shipping_method_selected') %>
34 | <% end %>
35 |
36 |
37 | <% if can? :update, shipment %>
38 | <%= button_tag '', :class => 'edit-method fa fa-edit no-text with-tip', :data => {:action => 'edit'}, :title => t('spree.actions.edit') %>
39 | <% end %>
40 |
41 |
42 |
43 |
44 |
45 | <%= t('spree.tracking_number') %>:
46 | <%= text_field_tag :tracking, shipment.tracking %>
47 |
48 |
49 | <% if can? :update, shipment %>
50 | <%= button_tag '', :class => 'save-tracking fa fa-ok no-text with-tip', :data => {'shipment-number' => shipment.number, :action => 'save'}, :title => t('spree.actions.save') %>
51 | <%= button_tag '', :class => 'cancel-tracking fa fa-cancel no-text with-tip', :data => {:action => 'cancel'}, :title => t('spree.actions.cancel') %>
52 | <% end %>
53 |
54 |
55 |
56 |
57 | <%= Spree::Shipment.human_attribute_name(:tracking) %>
58 |
59 | <% if shipment.tracking.present? %>
60 | <%= shipment.tracking %>
61 | <% else %>
62 | <%= t('spree.no_tracking_present') %>
63 | <% end %>
64 |
65 |
66 | <% if can? :update, shipment %>
67 | <%= button_tag '', class: 'edit-tracking fa fa-edit no-text with-tip', data: {action: 'edit'}, title: t('spree.actions.edit'), type: :button %>
68 | <% end %>
69 |
70 |
71 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Solidus Product Assembly
2 |
3 | [](https://circleci.com/gh/solidusio-contrib/solidus_product_assembly)
4 | [](https://codecov.io/gh/solidusio-contrib/solidus_product_assembly)
5 |
6 | Create a product which is composed of other products.
7 |
8 | ## Installation
9 |
10 | Add solidus_product_assembly to your Gemfile:
11 |
12 | ```ruby
13 | gem 'solidus_product_assembly'
14 | ```
15 |
16 | Bundle your dependencies and run the installation generator:
17 |
18 | ```shell
19 | bin/rails generate solidus_product_assembly:install
20 | ```
21 |
22 | ## Usage
23 |
24 | To build a bundle (assembly product) you'd need to first check the "Can be part"
25 | flag on each product you want to be part of the bundle. Then create a product
26 | and add parts to it. By doing that you're making that product an assembly.
27 |
28 | The store will treat assemblies a bit different than regular products on checkout.
29 | Spree will create and track inventory units for its parts rather than for the product itself.
30 | That means you essentially have a product composed of other products. From a
31 | customer perspective it's like they are paying a single amount for a collection
32 | of products.
33 |
34 | ## Development
35 |
36 | ### Testing the extension
37 |
38 | First bundle your dependencies, then run `bin/rake`. `bin/rake` will default to building the dummy
39 | app if it does not exist, then it will run specs. The dummy app can be regenerated by using
40 | `bin/rake extension:test_app`.
41 |
42 | ```shell
43 | bin/rake
44 | ```
45 |
46 | To run [Rubocop](https://github.com/bbatsov/rubocop) static code analysis run
47 |
48 | ```shell
49 | bundle exec rubocop
50 | ```
51 |
52 | When testing your application's integration with this extension you may use its factories.
53 | Simply add this require statement to your `spec/spec_helper.rb`:
54 |
55 | ```ruby
56 | require 'solidus_product_assembly/testing_support/factories'
57 | ```
58 |
59 | Or, if you are using `FactoryBot.definition_file_paths`, you can load Solidus core
60 | factories along with this extension's factories using this statement:
61 |
62 | ```ruby
63 | SolidusDevSupport::TestingSupport::Factories.load_for(SolidusProductAssembly::Engine)
64 | ```
65 |
66 | ### Running the sandbox
67 |
68 | To run this extension in a sandboxed Solidus application, you can run `bin/sandbox`. The path for
69 | the sandbox app is `./sandbox` and `bin/rails` will forward any Rails commands to
70 | `sandbox/bin/rails`.
71 |
72 | Here's an example:
73 |
74 | ```
75 | $ bin/rails server
76 | => Booting Puma
77 | => Rails 6.0.2.1 application starting in development
78 | * Listening on tcp://127.0.0.1:3000
79 | Use Ctrl-C to stop
80 | ```
81 |
82 | ### Updating the changelog
83 |
84 | Before and after releases the changelog should be updated to reflect the up-to-date status of
85 | the project:
86 |
87 | ```shell
88 | bin/rake changelog
89 | git add CHANGELOG.md
90 | git commit -m "Update the changelog"
91 | ```
92 |
93 | ### Releasing new versions
94 |
95 | Please refer to the dedicated [page](https://github.com/solidusio/solidus/wiki/How-to-release-extensions) on Solidus wiki.
96 |
97 | ## Contributing
98 |
99 | Spree is an open source project and we encourage contributions. Please see the [Community Guidelines](https://solidus.io/community-guidelines/) before contributing.
100 |
101 | In the spirit of [free software](http://www.fsf.org/licensing/essays/free-sw.html), **everyone** is encouraged to help improve this project.
102 |
103 | ## License
104 |
105 | Copyright (c) 2014 [Spree Commerce Inc.](https://github.com/spree) and [contributors](https://github.com/spree/spree-product-assembly/graphs/contributors), released under the [New BSD License](LICENSE)
106 |
--------------------------------------------------------------------------------
/spec/models/spree/shipment_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'spec_helper'
4 |
5 | module Spree
6 | describe Shipment do
7 | context "order has one product assembly" do
8 | let!(:store) { create(:store) }
9 | let(:order) { Order.create }
10 | let(:bundle) { create(:variant) }
11 | let!(:parts) { (1..2).map { create(:variant) } }
12 | let!(:bundle_parts) { bundle.product.parts << parts }
13 |
14 | let!(:line_item) { order.contents.add(bundle, 1) }
15 | let!(:shipment) { order.create_proposed_shipments.first }
16 |
17 | before do
18 | order.update_column :state, 'complete'
19 | end
20 |
21 | it "shipment item cost equals line item amount" do
22 | expect(shipment.item_cost).to eq line_item.amount
23 | end
24 | end
25 |
26 | context "manifests" do
27 | include_context "product is ordered as individual and within a bundle"
28 |
29 | let(:shipments) { order.create_proposed_shipments }
30 |
31 | context "default" do
32 | let(:expected_variants) { order.variants - [bundle_variant] + bundle.parts }
33 | before {shipments.first.save }
34 |
35 | it "separates variant purchased individually from the bundle one" do
36 | expect(shipments.count).to be 1
37 | expect(shipments.first.manifest.map(&:variant)).to match_array expected_variants
38 | end
39 | end
40 |
41 | context "line items manifest" do
42 | let(:expected_variants) { order.variants }
43 | before { shipments.first.save }
44 |
45 | it "groups units by line_item only" do
46 | expect(shipments.count).to be 1
47 | expect(shipments.first.line_item_manifest.map(&:variant)).to match_array expected_variants
48 | end
49 | end
50 |
51 | context "units are not associated with a line item" do
52 | let(:order) { create(:shipped_order) }
53 | let(:shipment) { order.shipments.first }
54 |
55 | it "searches for line item if inventory unit doesn't have one" do
56 | expect(shipment.manifest.last.line_item).not_to be_blank
57 | end
58 | end
59 | end
60 |
61 | context "set up new inventory units" do
62 | let(:line_item) { create(:line_item) }
63 | let(:variant) { line_item.variant }
64 | let(:order) { line_item.order }
65 | let(:shipment) { create(:shipment, order: order) }
66 |
67 | it "assigns variant, order and line_item" do
68 | unit = shipment.set_up_inventory('on_hand', variant, order, line_item)
69 |
70 | expect(unit.line_item).to eq line_item
71 | expect(unit.variant).to eq variant
72 | expect(unit.order).to eq order
73 | expect(unit.state).to eq 'on_hand'
74 | end
75 | end
76 |
77 | context "unit states for variant sold as part of an assembly and separately" do
78 | let(:assembly_line_item) { create(:line_item) }
79 | let(:shirt) { create(:variant) }
80 |
81 | let(:assembly_shirts) do
82 | 5.times.map {
83 | create(:inventory_unit,
84 | variant: shirt,
85 | line_item: assembly_line_item,
86 | state: :on_hand)
87 | }
88 | end
89 |
90 | let(:standalone_line_item) { create(:line_item, variant: shirt) }
91 |
92 | let(:standalone_shirts) do
93 | 2.times.map {
94 | create(:inventory_unit,
95 | variant: shirt,
96 | line_item: standalone_line_item,
97 | state: :on_hand)
98 | }
99 | end
100 |
101 | let(:shipment) { create(:shipment) }
102 |
103 | before do
104 | shipment.inventory_units << assembly_shirts
105 | shipment.inventory_units << standalone_shirts
106 | end
107 |
108 | it "set states numbers properly for all items" do
109 | shipment.manifest.each do |item|
110 | if item.line_item.id == standalone_line_item.id
111 | expect(item.states["on_hand"]).to eq standalone_shirts.count
112 | else
113 | expect(item.states["on_hand"]).to eq assembly_shirts.count
114 | end
115 | end
116 | end
117 | end
118 | end
119 | end
120 |
--------------------------------------------------------------------------------
/spec/features/admin/orders_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'spec_helper'
4 |
5 | describe "Orders", type: :feature, js: true do
6 | stub_authorization!
7 |
8 | context 'when the order product is a bundle' do
9 | let(:order) { create(:order_with_line_items) }
10 | let(:line_item) { order.line_items.first }
11 | let(:bundle) { line_item.product }
12 | let(:parts) { (1..3).map { create(:variant) } }
13 |
14 | before do
15 | bundle.parts << [parts]
16 | line_item.update!(quantity: 3)
17 | order.reload.create_proposed_shipments
18 | end
19 |
20 | if Spree.solidus_gem_version < Gem::Version.new('2.5')
21 | context 'Adding tracking number' do
22 | let(:tracking_number) { 'AA123' }
23 |
24 | before { visit spree.edit_admin_order_path(order) }
25 |
26 | it 'allows admin to edit tracking' do
27 | expect(page).not_to have_content tracking_number
28 | within '.show-tracking' do
29 | find('.fa-edit').click
30 | end
31 | within '.edit-tracking' do
32 | expect(page).to have_selector 'input[name="tracking"]'
33 | fill_in :tracking, with: tracking_number
34 | find('.fa-ok').click
35 | expect(page).not_to have_selector 'input[name="tracking"]'
36 | end
37 | expect(page).to have_content tracking_number
38 | end
39 | end
40 |
41 | context 'Changing carrier/shipping costs' do
42 | before do
43 | visit spree.edit_admin_order_path(order)
44 | end
45 |
46 | it 'allows admin to edit shipping costs' do
47 | expect(page).not_to have_content 'UPS Ground $100.00'
48 | within '.show-method' do
49 | find('.fa-edit').click
50 | end
51 | within '.edit-method' do
52 | find('.fa-ok').click
53 | end
54 | expect(page).to have_content 'UPS Ground $100.00'
55 | end
56 | end
57 | else
58 | context 'Adding tracking number' do
59 | let(:tracking_number) { 'AA123' }
60 |
61 | before { visit spree.edit_admin_order_path(order) }
62 |
63 | it 'allows admin to edit tracking' do
64 | within '.edit-tracking' do
65 | expect(page).not_to have_content tracking_number
66 | find('.js-edit').click
67 | expect(page).to have_selector 'input[name="tracking"]'
68 | fill_in :tracking, with: tracking_number
69 | find('.js-save').click
70 | expect(page).not_to have_selector 'input[name="tracking"]'
71 | expect(page).to have_content tracking_number
72 | end
73 | end
74 | end
75 |
76 | context 'Changing carrier/shipping costs' do
77 | let(:carrier_name) { 'Fedex' }
78 | let(:select_name) { 'selected_shipping_method_id' }
79 |
80 | before do
81 | create :shipping_method, name: carrier_name
82 | visit spree.edit_admin_order_path(order)
83 | end
84 |
85 | it 'allows admin to edit shipping costs' do
86 | within '.edit-shipping-method' do
87 | expect(page).not_to have_content carrier_name
88 | find('.js-edit').click
89 | expect(page).to have_selector "select[name='#{select_name}']"
90 | select "#{carrier_name} $10.00", from: select_name
91 | find('.js-save').click
92 | expect(page).not_to have_selector "select[name='#{select_name}']"
93 | expect(page).to have_content carrier_name
94 | end
95 | end
96 | end
97 | end
98 | end
99 |
100 | context 'when the order product is not a bundle' do
101 | let(:order) { create(:order_ready_to_ship, state: 'complete', line_items_count: 1) }
102 | let(:product) { order.products.first }
103 | let(:location_name) { 'Foobar warehouse' }
104 |
105 | before { create :stock_location, name: location_name }
106 |
107 | context 'Splitting items' do
108 | before { visit spree.edit_admin_order_path(order) }
109 |
110 | it 'allows admin to split items to another stock location' do
111 | find('.split-item').click
112 | within '.stock-item-split' do
113 | expect(page).to have_content "Move #{product.name} to"
114 | find('.save-split').click
115 | alert_text = page.driver.browser.switch_to.alert.text
116 | expect(alert_text).to include 'Please select the split destination'
117 | page.driver.browser.switch_to.alert.accept
118 | find('.select2-container').click
119 | find(:xpath, '//body').all('.select2-drop li.select2-result', text: "#{location_name} (0 on hand)")[0].click
120 | find('.save-split').click
121 | end
122 | expect(page).to have_content /Pending package from '#{location_name}'/i
123 | end
124 | end
125 | end
126 | end
127 |
--------------------------------------------------------------------------------
/spec/features/admin/return_items_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'spec_helper'
4 |
5 | describe "Return Items", type: :feature, js: true do
6 | stub_authorization!
7 |
8 | let(:line_item) { order.line_items.first }
9 |
10 | before do
11 | allow_any_instance_of(Spree::Admin::ReimbursementsController).to receive(:try_spree_current_user) { Spree.user_class.new }
12 | end
13 |
14 | context 'when the order product is a bundle' do
15 | let(:bundle) { line_item.product }
16 | let(:parts) { (1..3).map { create(:variant) } }
17 |
18 | before do
19 | create :refund_reason, name: Spree::RefundReason::RETURN_PROCESSING_REASON, mutable: false
20 | bundle.parts << [parts]
21 | bundle.set_part_count(parts.first, 2)
22 | parts.each { |p| p.stock_items.first.set_count_on_hand 4 }
23 | 3.times { order.next }
24 | create :payment, order: order, amount: order.amount
25 | order.next
26 | order.complete!
27 | order.payments.each(&:capture!)
28 | order.shipments.each { |s| s.update state: :ready }
29 | order.shipments.each(&:ship!)
30 | end
31 |
32 | context 'with a single item cart' do
33 | let(:order) { create(:order_with_line_items) }
34 |
35 | it 'successfully generates a RMA, a reimbursement and a refund' do
36 | visit spree.edit_admin_order_path(order)
37 |
38 | expect(page).to have_selector '.line-item-total', text: '$10.00'
39 |
40 | click_link 'RMA'
41 | click_link 'New RMA'
42 |
43 | expect(page).to have_selector '.return-item-charged', text: '$2.50', count: 4
44 |
45 | find('#select-all').click
46 | select Spree::StockLocation.first.name, from: 'Stock Location'
47 | click_button 'Create'
48 |
49 | expect(page).to have_content 'Return Authorization has been successfully created!'
50 |
51 | click_link 'Customer Returns'
52 | click_link 'New Customer Return'
53 | find('#select-all').click
54 | page.execute_script <<~JS
55 | $('option[value="receive"]').attr('selected', 'selected');
56 | JS
57 | select Spree::StockLocation.first.name, from: 'Stock Location'
58 | click_button 'Create'
59 |
60 | expect(page).to have_content 'Customer Return has been successfully created!'
61 |
62 | click_button 'Create reimbursement'
63 | click_button 'Reimburse'
64 |
65 | within 'table.reimbursement-refunds' do
66 | expect(page).to have_content '$10.00'
67 | end
68 | end
69 | end
70 |
71 | context 'with a multiple items cart' do
72 | let(:order) { create(:order_with_line_items, line_items_attributes: [{ quantity: 2 }]) }
73 |
74 | it 'successfully generates a RMA, a reimbursement and a refund' do
75 | visit spree.edit_admin_order_path(order)
76 |
77 | expect(page).to have_selector '.line-item-total', text: '$20.00'
78 |
79 | click_link 'RMA'
80 | click_link 'New RMA'
81 |
82 | expect(page).to have_selector '.return-item-charged', text: '$2.50', count: 8
83 |
84 | find('#select-all').click
85 | select Spree::StockLocation.first.name, from: 'Stock Location'
86 | click_button 'Create'
87 |
88 | expect(page).to have_content 'Return Authorization has been successfully created!'
89 |
90 | click_link 'Customer Returns'
91 | click_link 'New Customer Return'
92 | find('#select-all').click
93 | page.execute_script <<~JS
94 | $('option[value="receive"]').attr('selected', 'selected');
95 | JS
96 | select Spree::StockLocation.first.name, from: 'Stock Location'
97 | click_button 'Create'
98 |
99 | expect(page).to have_content 'Customer Return has been successfully created!'
100 |
101 | click_button 'Create reimbursement'
102 | click_button 'Reimburse'
103 |
104 | within 'table.reimbursement-refunds' do
105 | expect(page).to have_content '$20.00'
106 | end
107 | end
108 | end
109 | end
110 |
111 | context 'when the product is not a bundle' do
112 | before do
113 | create :refund_reason, name: Spree::RefundReason::RETURN_PROCESSING_REASON, mutable: false
114 | order.line_items.each { |li| li.variant.stock_items.first.set_count_on_hand 4 }
115 | 3.times { order.next }
116 | create :payment, order: order, amount: order.amount
117 | order.next
118 | order.complete!
119 | order.payments.each(&:capture!)
120 | order.shipments.each { |s| s.update state: :ready }
121 | order.shipments.each(&:ship!)
122 | end
123 |
124 | context 'with a single item cart' do
125 | let(:order) { create(:order_with_line_items) }
126 |
127 | it 'builds one return item for each product' do
128 | visit spree.edit_admin_order_path(order)
129 |
130 | expect(page).to have_selector '.item-total', text: '$10.00'
131 |
132 | click_link 'RMA'
133 | click_link 'New RMA'
134 |
135 | within '.return-items-table tbody' do
136 | expect(page).to have_selector 'tr', count: 1
137 | expect(page).to have_selector :field, class: 'refund-amount-input', with: '10.0', count: 1
138 | end
139 | end
140 | end
141 |
142 | context 'with a multiple items cart' do
143 | let(:order) { create(:order_with_line_items, line_items_attributes: [{ quantity: 2 }]) }
144 |
145 | it 'builds one return item for each product' do
146 | visit spree.edit_admin_order_path(order)
147 |
148 | expect(page).to have_selector '.item-total', text: '$20.00'
149 |
150 | click_link 'RMA'
151 | click_link 'New RMA'
152 |
153 | within '.return-items-table tbody' do
154 | expect(page).to have_selector 'tr', count: 2
155 | expect(page).to have_selector :field, class: 'refund-amount-input', with: '10.0', count: 2
156 | end
157 | end
158 | end
159 | end
160 | end
161 |
--------------------------------------------------------------------------------
/spec/features/checkout_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require 'spec_helper'
4 |
5 | describe "Checkout", type: :feature do
6 | let!(:store) { create :store, default: true }
7 | let!(:country) { create(:country, name: "United States", states_required: true) }
8 | let!(:state) { create(:state, name: "Ohio", country: country) }
9 | let!(:shipping_method) { create(:shipping_method) }
10 | let!(:stock_location) { create(:stock_location) }
11 | let!(:payment_method) { create(:check_payment_method) }
12 | let!(:zone) { create(:zone) }
13 |
14 | let(:product) { create(:product, name: "RoR Mug") }
15 | let(:variant) { create(:variant) }
16 | let(:other_variant) { create(:variant) }
17 |
18 | stub_authorization!
19 |
20 | before { product.parts << variant << other_variant }
21 |
22 | shared_context "purchases product with part included" do
23 | before do
24 | visit spree.root_path
25 | add_product_to_cart
26 | click_button "Checkout"
27 |
28 | fill_in "order_email", with: "ryan@spreecommerce.com"
29 | click_button "Continue"
30 | fill_in_address
31 |
32 | click_button "Save and Continue"
33 | expect(current_path).to eql(spree.checkout_state_path("delivery"))
34 | expect(page).to have_content(variant.product.name)
35 |
36 | click_button "Save and Continue"
37 | expect(current_path).to eql(spree.checkout_state_path("payment"))
38 |
39 | click_button "Save and Continue"
40 | expect(page).to have_current_path spree.checkout_state_path('confirm'), ignore_query: true
41 | expect(page).to have_content(variant.product.name)
42 |
43 | click_button "Place Order"
44 | expect(page).to have_content('Your order has been processed successfully')
45 | end
46 | end
47 |
48 | shared_examples 'purchasable assembly' do
49 | context "ordering only the product assembly" do
50 | include_context "purchases product with part included"
51 |
52 | it "views parts bundled as well" do
53 | visit spree.admin_orders_path
54 | click_on Spree::Order.last.number
55 |
56 | expect(page).to have_content(variant.product.name)
57 | end
58 | end
59 |
60 | context "ordering assembly and the part as individual sale" do
61 | before do
62 | visit spree.root_path
63 | click_link variant.product.name
64 | click_button "add-to-cart-button"
65 | end
66 |
67 | include_context "purchases product with part included"
68 |
69 | it "views parts bundled and not" do
70 | visit spree.admin_orders_path
71 | click_on Spree::Order.last.number
72 |
73 | expect(page).to have_content(variant.product.name)
74 | end
75 | end
76 | end
77 |
78 | context "backend order shipments UI", js: true do
79 | context 'when the product and parts are backorderable' do
80 | it_behaves_like 'purchasable assembly'
81 | end
82 |
83 | context 'when the product and parts are not backorderable' do
84 | context 'when there is enough quantity available' do
85 | before do
86 | Spree::StockItem.update_all backorderable: false
87 | variant.stock_items.each { |stock_item| stock_item.set_count_on_hand 2 }
88 | other_variant.stock_items.each { |stock_item| stock_item.set_count_on_hand 1 }
89 | product.stock_items.each { |stock_item| stock_item.set_count_on_hand 1 }
90 | end
91 |
92 | it_behaves_like 'purchasable assembly'
93 |
94 | context 'when the order can be processed' do
95 | include_context "purchases product with part included"
96 |
97 | before do
98 | visit spree.admin_orders_path
99 | click_on Spree::Order.last.number
100 | end
101 |
102 | it 'marks the order as paid and shipped showing proper parts information' do
103 | within '#order_tab_summary' do
104 | expect(page).to have_selector '#shipment_status', text: 'Pending'
105 | expect(page).to have_selector '#payment_status', text: 'Balance due'
106 | end
107 |
108 | click_link 'Payments'
109 | find('.fa-capture').click
110 |
111 | within '#order_tab_summary' do
112 | expect(page).to have_selector '#shipment_status', text: 'Ready'
113 | expect(page).to have_selector '#payment_status', text: 'Paid'
114 | end
115 |
116 | click_link 'Shipments'
117 |
118 | within '.stock-contents' do
119 | expect(page).to have_selector '.item-price', text: '---'
120 | expect(page).to have_selector '.item-total', text: '---'
121 | expect(page).to have_selector '.item-qty-show', count: 2, text: '1 x on hand'
122 | end
123 |
124 | expect(page).not_to have_selector '.stock-contents.carton'
125 |
126 | click_button 'Ship'
127 |
128 | within '.stock-contents' do
129 | expect(page).to have_selector '.item-qty-show', count: 2, text: '1 x Shipped'
130 | end
131 |
132 | within '.stock-contents.carton' do
133 | expect(page).to have_selector '.item-qty-show', count: 2, text: '1 x Shipped'
134 | expect(page).to have_selector '.item-price', text: '---'
135 | expect(page).to have_selector '.item-total', text: '---'
136 | end
137 |
138 | within '#order_tab_summary' do
139 | expect(page).to have_selector '#shipment_status', text: 'Shipped'
140 | expect(page).to have_selector '#payment_status', text: 'Paid'
141 | end
142 | end
143 | end
144 | end
145 | end
146 | end
147 |
148 | def fill_in_address
149 | address = "order_bill_address_attributes"
150 |
151 | if use_address_full_name?
152 | fill_in "#{address}_name", with: "Ryan Bigg"
153 | else
154 | fill_in "#{address}_firstname", with: "Ryan"
155 | fill_in "#{address}_lastname", with: "Bigg"
156 | end
157 |
158 | fill_in "#{address}_address1", with: "143 Swan Street"
159 | fill_in "#{address}_city", with: "Richmond"
160 | select "Ohio", from: "#{address}_state_id"
161 | fill_in "#{address}_zipcode", with: "12345"
162 | fill_in "#{address}_phone", with: "(555) 555-5555"
163 | end
164 |
165 | def use_address_full_name?
166 | SolidusSupport.combined_first_and_last_name_in_address?
167 | end
168 |
169 | def add_product_to_cart
170 | visit spree.root_path
171 | click_link product.name
172 | click_button "add-to-cart-button"
173 | end
174 | end
175 |
--------------------------------------------------------------------------------
/.rubocop_todo.yml:
--------------------------------------------------------------------------------
1 | # This configuration was generated by
2 | # `rubocop --auto-gen-config`
3 | # on 2022-09-14 16:19:15 UTC using RuboCop version 1.36.0.
4 | # The point is for the user to remove these configuration records
5 | # one by one as the offenses are removed from the code base.
6 | # Note that changes in the inspected code, or installation of new
7 | # versions of RuboCop, may require this file to be generated again.
8 |
9 | # Offense count: 1
10 | # This cop supports safe autocorrection (--autocorrect).
11 | # Configuration parameters: TreatCommentsAsGroupSeparators, ConsiderPunctuation, Include.
12 | # Include: **/*.gemfile, **/Gemfile, **/gems.rb
13 | Bundler/OrderedGems:
14 | Exclude:
15 | - 'Gemfile'
16 |
17 | # Offense count: 2
18 | # This cop supports safe autocorrection (--autocorrect).
19 | # Configuration parameters: TreatCommentsAsGroupSeparators, ConsiderPunctuation, Include.
20 | # Include: **/*.gemspec
21 | Gemspec/OrderedDependencies:
22 | Exclude:
23 | - 'solidus_product_assembly.gemspec'
24 |
25 | # Offense count: 1
26 | # Configuration parameters: Include.
27 | # Include: **/*.gemspec
28 | Gemspec/RequiredRubyVersion:
29 | Exclude:
30 | - 'solidus_product_assembly.gemspec'
31 |
32 | # Offense count: 21
33 | # This cop supports safe autocorrection (--autocorrect).
34 | # Configuration parameters: EnforcedStyle, IndentationWidth.
35 | # SupportedStyles: with_first_argument, with_fixed_indentation
36 | Layout/ArgumentAlignment:
37 | Exclude:
38 | - 'app/decorators/models/solidus_product_assembly/spree/product_decorator.rb'
39 | - 'app/decorators/models/solidus_product_assembly/spree/shipment_decorator.rb'
40 | - 'app/models/spree/assemblies_part.rb'
41 | - 'app/overrides/add_admin_product_form_fields.rb'
42 | - 'app/overrides/add_admin_tabs.rb'
43 | - 'app/overrides/add_line_item_description.rb'
44 |
45 | # Offense count: 2
46 | # This cop supports safe autocorrection (--autocorrect).
47 | # Configuration parameters: EnforcedStyleAlignWith, Severity.
48 | # SupportedStylesAlignWith: keyword, variable, start_of_line
49 | Layout/EndAlignment:
50 | Exclude:
51 | - 'db/migrate/20091029165620_add_parts_fields_to_products.rb'
52 |
53 | # Offense count: 1
54 | # This cop supports safe autocorrection (--autocorrect).
55 | # Configuration parameters: AllowForAlignment, AllowBeforeTrailingComments, ForceEqualSignAlignment.
56 | Layout/ExtraSpacing:
57 | Exclude:
58 | - 'solidus_product_assembly.gemspec'
59 |
60 | # Offense count: 1
61 | # This cop supports safe autocorrection (--autocorrect).
62 | # Configuration parameters: Width, AllowedPatterns, IgnoredPatterns.
63 | Layout/IndentationWidth:
64 | Exclude:
65 | - 'spec/models/spree/shipment_spec.rb'
66 |
67 | # Offense count: 8
68 | # This cop supports safe autocorrection (--autocorrect).
69 | # Configuration parameters: EnforcedStyle, IndentationWidth.
70 | # SupportedStyles: aligned, indented, indented_relative_to_receiver
71 | Layout/MultilineMethodCallIndentation:
72 | Exclude:
73 | - 'app/decorators/models/solidus_product_assembly/spree/product_decorator.rb'
74 | - 'spec/models/spree/order_contents_spec.rb'
75 |
76 | # Offense count: 1
77 | # This cop supports safe autocorrection (--autocorrect).
78 | # Configuration parameters: AllowForAlignment, EnforcedStyleForExponentOperator.
79 | # SupportedStylesForExponentOperator: space, no_space
80 | Layout/SpaceAroundOperators:
81 | Exclude:
82 | - 'solidus_product_assembly.gemspec'
83 |
84 | # Offense count: 1
85 | # This cop supports safe autocorrection (--autocorrect).
86 | # Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces, SpaceBeforeBlockParameters.
87 | # SupportedStyles: space, no_space
88 | # SupportedStylesForEmptyBraces: space, no_space
89 | Layout/SpaceInsideBlockBraces:
90 | Exclude:
91 | - 'spec/models/spree/shipment_spec.rb'
92 |
93 | # Offense count: 1
94 | # This cop supports safe autocorrection (--autocorrect).
95 | # Configuration parameters: AllowInHeredoc.
96 | Layout/TrailingWhitespace:
97 | Exclude:
98 | - 'spec/features/checkout_spec.rb'
99 |
100 | # Offense count: 1
101 | # Configuration parameters: AllowedMethods, AllowedPatterns, IgnoredMethods.
102 | Lint/AmbiguousBlockAssociation:
103 | Exclude:
104 | - 'spec/models/spree/order_inventory_spec.rb'
105 |
106 | # Offense count: 1
107 | # This cop supports safe autocorrection (--autocorrect).
108 | Lint/AmbiguousOperator:
109 | Exclude:
110 | - 'app/views/spree/api/line_items/show.v1.rabl'
111 |
112 | # Offense count: 1
113 | Lint/MissingSuper:
114 | Exclude:
115 | - 'app/models/spree/order_inventory_assembly.rb'
116 |
117 | # Offense count: 1
118 | # This cop supports safe autocorrection (--autocorrect).
119 | Lint/RedundantCopDisableDirective:
120 | Exclude:
121 | - 'lib/generators/solidus_product_assembly/install/install_generator.rb'
122 |
123 | # Offense count: 3
124 | Lint/ShadowingOuterLocalVariable:
125 | Exclude:
126 | - 'app/decorators/models/solidus_product_assembly/spree/shipment_decorator.rb'
127 | - 'lib/tasks/spree2_upgrade.rake'
128 |
129 | # Offense count: 2
130 | # This cop supports safe autocorrection (--autocorrect).
131 | Performance/BigDecimalWithNumericArgument:
132 | Exclude:
133 | - 'spec/models/spree/inventory_unit_spec.rb'
134 |
135 | # Offense count: 3
136 | # This cop supports unsafe autocorrection (--autocorrect-all).
137 | Performance/TimesMap:
138 | Exclude:
139 | - 'app/decorators/models/solidus_product_assembly/spree/stock/inventory_unit_builder_decorator.rb'
140 | - 'spec/models/spree/shipment_spec.rb'
141 |
142 | # Offense count: 4
143 | RSpec/AnyInstance:
144 | Exclude:
145 | - 'spec/features/admin/return_items_spec.rb'
146 | - 'spec/models/spree/stock/availability_validator_spec.rb'
147 |
148 | # Offense count: 2
149 | # This cop supports safe autocorrection (--autocorrect).
150 | RSpec/Capybara/CurrentPathExpectation:
151 | Exclude:
152 | - 'spec/features/checkout_spec.rb'
153 |
154 | # Offense count: 42
155 | # Configuration parameters: Prefixes, AllowedPatterns.
156 | # Prefixes: when, with, without
157 | RSpec/ContextWording:
158 | Exclude:
159 | - 'spec/features/admin/orders_spec.rb'
160 | - 'spec/features/checkout_spec.rb'
161 | - 'spec/models/spree/assemblies_part_spec.rb'
162 | - 'spec/models/spree/inventory_unit_spec.rb'
163 | - 'spec/models/spree/line_item_spec.rb'
164 | - 'spec/models/spree/order_contents_spec.rb'
165 | - 'spec/models/spree/order_inventory_assembly_spec.rb'
166 | - 'spec/models/spree/order_inventory_spec.rb'
167 | - 'spec/models/spree/shipment_spec.rb'
168 | - 'spec/models/spree/stock/availability_validator_spec.rb'
169 | - 'spec/models/spree/stock/coordinator_spec.rb'
170 | - 'spec/models/spree/stock/inventory_unit_builder_spec.rb'
171 | - 'spec/models/spree/variant_spec.rb'
172 | - 'spec/support/shared_contexts/order_with_bundle.rb'
173 |
174 | # Offense count: 2
175 | # This cop supports safe autocorrection (--autocorrect).
176 | RSpec/EmptyLineAfterFinalLet:
177 | Exclude:
178 | - 'spec/models/spree/shipment_spec.rb'
179 |
180 | # Offense count: 9
181 | RSpec/ExpectInHook:
182 | Exclude:
183 | - 'spec/features/checkout_spec.rb'
184 | - 'spec/models/spree/line_item_spec.rb'
185 |
186 | # Offense count: 14
187 | # Configuration parameters: AssignmentOnly.
188 | RSpec/InstanceVariable:
189 | Exclude:
190 | - 'spec/models/spree/product_spec.rb'
191 |
192 | # Offense count: 14
193 | RSpec/LetSetup:
194 | Exclude:
195 | - 'spec/features/checkout_spec.rb'
196 | - 'spec/models/spree/order_contents_spec.rb'
197 | - 'spec/models/spree/order_inventory_spec.rb'
198 | - 'spec/models/spree/shipment_spec.rb'
199 | - 'spec/models/spree/stock/availability_validator_spec.rb'
200 | - 'spec/models/spree/stock/coordinator_spec.rb'
201 |
202 | # Offense count: 9
203 | # Configuration parameters: .
204 | # SupportedStyles: have_received, receive
205 | RSpec/MessageSpies:
206 | EnforcedStyle: receive
207 |
208 | # Offense count: 19
209 | RSpec/MultipleExpectations:
210 | Max: 14
211 |
212 | # Offense count: 23
213 | # Configuration parameters: AllowSubject.
214 | RSpec/MultipleMemoizedHelpers:
215 | Max: 10
216 |
217 | # Offense count: 23
218 | # Configuration parameters: IgnoreSharedExamples.
219 | RSpec/NamedSubject:
220 | Exclude:
221 | - 'spec/models/spree/assemblies_part_spec.rb'
222 | - 'spec/models/spree/inventory_unit_spec.rb'
223 | - 'spec/models/spree/order_contents_spec.rb'
224 | - 'spec/models/spree/order_inventory_assembly_spec.rb'
225 | - 'spec/models/spree/order_inventory_spec.rb'
226 | - 'spec/models/spree/product_spec.rb'
227 | - 'spec/models/spree/stock/availability_validator_spec.rb'
228 | - 'spec/models/spree/stock/coordinator_spec.rb'
229 | - 'spec/models/spree/stock/inventory_unit_builder_spec.rb'
230 |
231 | # Offense count: 2
232 | # Configuration parameters: AllowedGroups.
233 | RSpec/NestedGroups:
234 | Max: 5
235 |
236 | # Offense count: 3
237 | # This cop supports safe autocorrection (--autocorrect).
238 | RSpec/ScatteredLet:
239 | Exclude:
240 | - 'spec/models/spree/order_inventory_spec.rb'
241 |
242 | # Offense count: 2
243 | RSpec/StubbedMock:
244 | Exclude:
245 | - 'spec/models/spree/line_item_spec.rb'
246 |
247 | # Offense count: 2
248 | # Configuration parameters: IgnoreNameless, IgnoreSymbolicNames.
249 | RSpec/VerifiedDoubles:
250 | Exclude:
251 | - 'spec/models/spree/line_item_spec.rb'
252 | - 'spec/models/spree/stock/availability_validator_spec.rb'
253 |
254 | # Offense count: 1
255 | # Configuration parameters: Include.
256 | # Include: db/migrate/*.rb
257 | Rails/CreateTableWithTimestamps:
258 | Exclude:
259 | - 'db/migrate/20091028152124_add_many_to_many_relation_to_products.rb'
260 |
261 | # Offense count: 2
262 | # Configuration parameters: IgnoreScopes, Include.
263 | # Include: app/models/**/*.rb
264 | Rails/InverseOf:
265 | Exclude:
266 | - 'app/models/spree/assemblies_part.rb'
267 |
268 | # Offense count: 2
269 | # This cop supports safe autocorrection (--autocorrect).
270 | Rails/RedundantForeignKey:
271 | Exclude:
272 | - 'app/models/spree/assemblies_part.rb'
273 |
274 | # Offense count: 8
275 | # Configuration parameters: ForbiddenMethods, AllowedMethods.
276 | # ForbiddenMethods: decrement!, decrement_counter, increment!, increment_counter, insert, insert!, insert_all, insert_all!, toggle!, touch, touch_all, update_all, update_attribute, update_column, update_columns, update_counters, upsert, upsert_all
277 | Rails/SkipsModelValidations:
278 | Exclude:
279 | - 'lib/tasks/spree2_upgrade.rake'
280 | - 'spec/features/admin/return_items_spec.rb'
281 | - 'spec/features/checkout_spec.rb'
282 | - 'spec/models/spree/line_item_spec.rb'
283 | - 'spec/models/spree/order_contents_spec.rb'
284 | - 'spec/models/spree/order_inventory_spec.rb'
285 | - 'spec/models/spree/shipment_spec.rb'
286 | - 'spec/models/spree/stock/coordinator_spec.rb'
287 |
288 | # Offense count: 2
289 | # This cop supports unsafe autocorrection (--autocorrect-all).
290 | # Configuration parameters: EnforcedStyle.
291 | # SupportedStyles: nested, compact
292 | Style/ClassAndModuleChildren:
293 | Exclude:
294 | - 'app/controllers/spree/admin/parts_controller.rb'
295 | - 'app/models/spree/calculator/returns/assemblies_default_refund_amount.rb'
296 |
297 | # Offense count: 3
298 | # Configuration parameters: MinBodyLength, AllowConsecutiveConditionals.
299 | Style/GuardClause:
300 | Exclude:
301 | - 'app/decorators/models/solidus_product_assembly/spree/line_item_decorator.rb'
302 | - 'app/decorators/models/solidus_product_assembly/spree/stock/inventory_validator_decorator.rb'
303 | - 'app/models/spree/order_inventory_assembly.rb'
304 |
305 | # Offense count: 2
306 | # This cop supports safe autocorrection (--autocorrect).
307 | Style/OrAssignment:
308 | Exclude:
309 | - 'app/models/spree/order_inventory_assembly.rb'
310 |
311 | # Offense count: 6
312 | # This cop supports safe autocorrection (--autocorrect).
313 | # Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns, IgnoredPatterns.
314 | # URISchemes: http, https
315 | Layout/LineLength:
316 | Max: 146
317 |
--------------------------------------------------------------------------------
/OLD_CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## [v1.2.0](https://github.com/solidusio-contrib/solidus_product_assembly/tree/v1.2.0) (2022-11-02)
4 |
5 | [Full Changelog](https://github.com/solidusio-contrib/solidus_product_assembly/compare/v1.1.0...v1.2.0)
6 |
7 | **Merged pull requests:**
8 |
9 | - Fix before payment override [\#112](https://github.com/solidusio-contrib/solidus_product_assembly/pull/112) ([cpfergus1](https://github.com/cpfergus1))
10 | - Update to use forked solidus\_frontend when needed [\#111](https://github.com/solidusio-contrib/solidus_product_assembly/pull/111) ([waiting-for-dev](https://github.com/waiting-for-dev))
11 | - Search part globalize [\#109](https://github.com/solidusio-contrib/solidus_product_assembly/pull/109) ([AlistairNorman](https://github.com/AlistairNorman))
12 |
13 | ## [v1.1.0](https://github.com/solidusio-contrib/solidus_product_assembly/tree/v1.1.0) (2021-10-19)
14 |
15 | [Full Changelog](https://github.com/solidusio-contrib/solidus_product_assembly/compare/v1.0.0...v1.1.0)
16 |
17 | **Closed issues:**
18 |
19 | - Dependabot couldn't find a Gemfile-local for this project [\#107](https://github.com/solidusio-contrib/solidus_product_assembly/issues/107)
20 | - Prepare Solidus Product Assembly for Solidus 3.0 [\#102](https://github.com/solidusio-contrib/solidus_product_assembly/issues/102)
21 | - Error on running specs locally [\#91](https://github.com/solidusio-contrib/solidus_product_assembly/issues/91)
22 | - Dependabot can't resolve your Ruby dependency files [\#80](https://github.com/solidusio-contrib/solidus_product_assembly/issues/80)
23 | - Dependabot can't resolve your Ruby dependency files [\#79](https://github.com/solidusio-contrib/solidus_product_assembly/issues/79)
24 | - Dependabot can't resolve your Ruby dependency files [\#78](https://github.com/solidusio-contrib/solidus_product_assembly/issues/78)
25 | - Dependabot can't resolve your Ruby dependency files [\#77](https://github.com/solidusio-contrib/solidus_product_assembly/issues/77)
26 | - Dependabot can't resolve your Ruby dependency files [\#76](https://github.com/solidusio-contrib/solidus_product_assembly/issues/76)
27 | - Dependabot can't resolve your Ruby dependency files [\#75](https://github.com/solidusio-contrib/solidus_product_assembly/issues/75)
28 | - Dependabot can't resolve your Ruby dependency files [\#74](https://github.com/solidusio-contrib/solidus_product_assembly/issues/74)
29 | - Dependabot can't resolve your Ruby dependency files [\#73](https://github.com/solidusio-contrib/solidus_product_assembly/issues/73)
30 | - Action links for assembly don't work [\#62](https://github.com/solidusio-contrib/solidus_product_assembly/issues/62)
31 | - Wrong return item amounts when returning orders with line items with quantity \> 1 [\#46](https://github.com/solidusio-contrib/solidus_product_assembly/issues/46)
32 | - `method_missing': undefined method `add\_routes' [\#39](https://github.com/solidusio-contrib/solidus_product_assembly/issues/39)
33 | - Deface is required [\#37](https://github.com/solidusio-contrib/solidus_product_assembly/issues/37)
34 | - Javascript not registering Events [\#23](https://github.com/solidusio-contrib/solidus_product_assembly/issues/23)
35 | - Cannot select parts [\#18](https://github.com/solidusio-contrib/solidus_product_assembly/issues/18)
36 |
37 | **Merged pull requests:**
38 |
39 | - Solidus 3 preparation [\#106](https://github.com/solidusio-contrib/solidus_product_assembly/pull/106) ([cpfergus1](https://github.com/cpfergus1))
40 | - Upgrade to GitHub-native Dependabot [\#104](https://github.com/solidusio-contrib/solidus_product_assembly/pull/104) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
41 | - Bump solidus\_support dependency to \(~\> 0.5\) [\#96](https://github.com/solidusio-contrib/solidus_product_assembly/pull/96) ([jcsanti](https://github.com/jcsanti))
42 | - Display parts' available\_on in the admin's "parts" tab \(feature proposal\) [\#95](https://github.com/solidusio-contrib/solidus_product_assembly/pull/95) ([jcsanti](https://github.com/jcsanti))
43 | - Fix Community Guidelines link in Readme [\#94](https://github.com/solidusio-contrib/solidus_product_assembly/pull/94) ([jcsanti](https://github.com/jcsanti))
44 | - Fix failing test in shipment\_spec due to missing store [\#93](https://github.com/solidusio-contrib/solidus_product_assembly/pull/93) ([jcsanti](https://github.com/jcsanti))
45 | - Fix deprecation warning about SolidusSupport.solidus\_gem\_version [\#84](https://github.com/solidusio-contrib/solidus_product_assembly/pull/84) ([stem](https://github.com/stem))
46 | - Upgrade the extension using solidus\_dev\_support [\#81](https://github.com/solidusio-contrib/solidus_product_assembly/pull/81) ([blocknotes](https://github.com/blocknotes))
47 | - Adopt solidus\_extension\_dev\_tools [\#71](https://github.com/solidusio-contrib/solidus_product_assembly/pull/71) ([aldesantis](https://github.com/aldesantis))
48 | - Remove deprecation for Reimbursement\#simulate [\#69](https://github.com/solidusio-contrib/solidus_product_assembly/pull/69) ([spaghetticode](https://github.com/spaghetticode))
49 | - Remove broken buttons in Shipments page [\#68](https://github.com/solidusio-contrib/solidus_product_assembly/pull/68) ([spaghetticode](https://github.com/spaghetticode))
50 | - Update factory\_bot requirement from 5.1.0 to 5.1.1 [\#67](https://github.com/solidusio-contrib/solidus_product_assembly/pull/67) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
51 | - Update puma requirement from ~\> 3.12 to ~\> 4.2 [\#66](https://github.com/solidusio-contrib/solidus_product_assembly/pull/66) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
52 | - Update factory\_bot requirement from 4.10.0 to 5.1.0 [\#65](https://github.com/solidusio-contrib/solidus_product_assembly/pull/65) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
53 | - Update pg requirement from ~\> 0.21 to ~\> 1.1 [\#64](https://github.com/solidusio-contrib/solidus_product_assembly/pull/64) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
54 | - Update sqlite3 requirement from ~\> 1.3.6 to ~\> 1.4.1 [\#63](https://github.com/solidusio-contrib/solidus_product_assembly/pull/63) ([dependabot-preview[bot]](https://github.com/apps/dependabot-preview))
55 | - Use `ActionView::Helpers::TranslationHelper.t` [\#60](https://github.com/solidusio-contrib/solidus_product_assembly/pull/60) ([spaghetticode](https://github.com/spaghetticode))
56 | - Releasing new gem version to RubyGems, changelog generation [\#57](https://github.com/solidusio-contrib/solidus_product_assembly/pull/57) ([spaghetticode](https://github.com/spaghetticode))
57 | - Remove Travis CI [\#56](https://github.com/solidusio-contrib/solidus_product_assembly/pull/56) ([spaghetticode](https://github.com/spaghetticode))
58 | - Add SKU to \_stock\_item [\#55](https://github.com/solidusio-contrib/solidus_product_assembly/pull/55) ([ericsaupe](https://github.com/ericsaupe))
59 | - Add CircleCI build [\#54](https://github.com/solidusio-contrib/solidus_product_assembly/pull/54) ([spaghetticode](https://github.com/spaghetticode))
60 | - Load Capybara config from `solidus_support` gem [\#53](https://github.com/solidusio-contrib/solidus_product_assembly/pull/53) ([spaghetticode](https://github.com/spaghetticode))
61 | - Fix build on Travis [\#52](https://github.com/solidusio-contrib/solidus_product_assembly/pull/52) ([spaghetticode](https://github.com/spaghetticode))
62 | - Update `#determine_target_shipment` after Solidus \#3197 [\#50](https://github.com/solidusio-contrib/solidus_product_assembly/pull/50) ([spaghetticode](https://github.com/spaghetticode))
63 | - Show split/delete actions for all items \(admin/orders/shipment\) [\#49](https://github.com/solidusio-contrib/solidus_product_assembly/pull/49) ([MinasMazar](https://github.com/MinasMazar))
64 | - Show correct RMA amounts when assembly has more than one piece for var… [\#48](https://github.com/solidusio-contrib/solidus_product_assembly/pull/48) ([spaghetticode](https://github.com/spaghetticode))
65 | - Fix build on Travis [\#47](https://github.com/solidusio-contrib/solidus_product_assembly/pull/47) ([spaghetticode](https://github.com/spaghetticode))
66 | - Fix return item amounts with multiple items [\#45](https://github.com/solidusio-contrib/solidus_product_assembly/pull/45) ([spaghetticode](https://github.com/spaghetticode))
67 | - add deface as a development dependency in gemspec [\#44](https://github.com/solidusio-contrib/solidus_product_assembly/pull/44) ([kevinjbayer](https://github.com/kevinjbayer))
68 | - Round up default refund amount for assembly parts [\#43](https://github.com/solidusio-contrib/solidus_product_assembly/pull/43) ([spaghetticode](https://github.com/spaghetticode))
69 | - Remove assembly part prices from carton manifest [\#42](https://github.com/solidusio-contrib/solidus_product_assembly/pull/42) ([spaghetticode](https://github.com/spaghetticode))
70 | - Split refund amount proportionally on each assembly part [\#41](https://github.com/solidusio-contrib/solidus_product_assembly/pull/41) ([spaghetticode](https://github.com/spaghetticode))
71 | - Fix stock finalization with non-backorderable stock and Solidus \>= 2.8 [\#40](https://github.com/solidusio-contrib/solidus_product_assembly/pull/40) ([spaghetticode](https://github.com/spaghetticode))
72 | - Replace `Spree.t` with `I18n.t` [\#38](https://github.com/solidusio-contrib/solidus_product_assembly/pull/38) ([spaghetticode](https://github.com/spaghetticode))
73 | - Lock SQLite3 to version 1.3 [\#36](https://github.com/solidusio-contrib/solidus_product_assembly/pull/36) ([aitbw](https://github.com/aitbw))
74 | - Add Solidus v2.8 to Travis config [\#35](https://github.com/solidusio-contrib/solidus_product_assembly/pull/35) ([aitbw](https://github.com/aitbw))
75 | - Remove stale edit button [\#34](https://github.com/solidusio-contrib/solidus_product_assembly/pull/34) ([spaghetticode](https://github.com/spaghetticode))
76 | - Use Selenium with Chrome headless instead of Poltergeist [\#33](https://github.com/solidusio-contrib/solidus_product_assembly/pull/33) ([spaghetticode](https://github.com/spaghetticode))
77 | - Fix `stock_item` partial for splitting items [\#32](https://github.com/solidusio-contrib/solidus_product_assembly/pull/32) ([spaghetticode](https://github.com/spaghetticode))
78 | - Fix admin edit for shipping costs and tracking [\#31](https://github.com/solidusio-contrib/solidus_product_assembly/pull/31) ([spaghetticode](https://github.com/spaghetticode))
79 | - AvailabilityValidator delegates to Solidus implementation when product is not an assembly [\#30](https://github.com/solidusio-contrib/solidus_product_assembly/pull/30) ([spaghetticode](https://github.com/spaghetticode))
80 | - Test suite maintenance [\#29](https://github.com/solidusio-contrib/solidus_product_assembly/pull/29) ([aitbw](https://github.com/aitbw))
81 | - Remove 2.2 from CI \(EOL\) [\#28](https://github.com/solidusio-contrib/solidus_product_assembly/pull/28) ([jacobherrington](https://github.com/jacobherrington))
82 | - Remove versions past EOL from .travis.yml [\#27](https://github.com/solidusio-contrib/solidus_product_assembly/pull/27) ([jacobherrington](https://github.com/jacobherrington))
83 | - Add Solidus 2.7 to .travis.yml [\#26](https://github.com/solidusio-contrib/solidus_product_assembly/pull/26) ([jacobherrington](https://github.com/jacobherrington))
84 | - Update for Solidus 2.5 [\#21](https://github.com/solidusio-contrib/solidus_product_assembly/pull/21) ([jhawthorn](https://github.com/jhawthorn))
85 | - Fix warnings [\#20](https://github.com/solidusio-contrib/solidus_product_assembly/pull/20) ([jhawthorn](https://github.com/jhawthorn))
86 | - 2.4 fixes [\#19](https://github.com/solidusio-contrib/solidus_product_assembly/pull/19) ([jhawthorn](https://github.com/jhawthorn))
87 | - Avoid stubbing in spec [\#17](https://github.com/solidusio-contrib/solidus_product_assembly/pull/17) ([jhawthorn](https://github.com/jhawthorn))
88 | - Remove old Spree information from README [\#16](https://github.com/solidusio-contrib/solidus_product_assembly/pull/16) ([brchristian](https://github.com/brchristian))
89 | - Removing small\_image Method Call. Replacing It To Avoid Error. [\#15](https://github.com/solidusio-contrib/solidus_product_assembly/pull/15) ([hugohernani](https://github.com/hugohernani))
90 | - Fix deprecations and removals [\#14](https://github.com/solidusio-contrib/solidus_product_assembly/pull/14) ([jhawthorn](https://github.com/jhawthorn))
91 | - Remove add\_routes fo routes.draw [\#13](https://github.com/solidusio-contrib/solidus_product_assembly/pull/13) ([denissellu](https://github.com/denissellu))
92 | - Fix migration to allow rollback [\#12](https://github.com/solidusio-contrib/solidus_product_assembly/pull/12) ([tudorpavel](https://github.com/tudorpavel))
93 | - Parts informations in API [\#11](https://github.com/solidusio-contrib/solidus_product_assembly/pull/11) ([DanielePalombo](https://github.com/DanielePalombo))
94 |
95 | ## [v1.0.0](https://github.com/solidusio-contrib/solidus_product_assembly/tree/v1.0.0) (2016-09-06)
96 |
97 | [Full Changelog](https://github.com/solidusio-contrib/solidus_product_assembly/compare/7a6d85258735c0035d675364fc9000cdd7355e0e...v1.0.0)
98 |
99 | **Closed issues:**
100 |
101 | - Inventory Validation fail upon checkout, for the assembly product [\#3](https://github.com/solidusio-contrib/solidus_product_assembly/issues/3)
102 |
103 | **Merged pull requests:**
104 |
105 | - Add support for Solidus 2.0 and Rails 5 [\#10](https://github.com/solidusio-contrib/solidus_product_assembly/pull/10) ([jhawthorn](https://github.com/jhawthorn))
106 | - Fix installation instructions in README [\#9](https://github.com/solidusio-contrib/solidus_product_assembly/pull/9) ([alexblackie](https://github.com/alexblackie))
107 | - Add missing translation for actions.delete [\#8](https://github.com/solidusio-contrib/solidus_product_assembly/pull/8) ([Sinetheta](https://github.com/Sinetheta))
108 | - Fix a load order issue when redefining inventory validator. [\#7](https://github.com/solidusio-contrib/solidus_product_assembly/pull/7) ([Senjai](https://github.com/Senjai))
109 | - Fix product sub\_menu display on new Solidus [\#6](https://github.com/solidusio-contrib/solidus_product_assembly/pull/6) ([Sinetheta](https://github.com/Sinetheta))
110 | - Inventory validation [\#5](https://github.com/solidusio-contrib/solidus_product_assembly/pull/5) ([Sinetheta](https://github.com/Sinetheta))
111 | - Rename to solidus\_product\_assembly [\#2](https://github.com/solidusio-contrib/solidus_product_assembly/pull/2) ([Sinetheta](https://github.com/Sinetheta))
112 | - Upgrade from spree 2.4 to solidus 1.1-1.3 [\#1](https://github.com/solidusio-contrib/solidus_product_assembly/pull/1) ([Sinetheta](https://github.com/Sinetheta))
113 |
114 |
115 |
116 | \* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)*
117 |
--------------------------------------------------------------------------------