├── .hound.yml ├── app ├── views │ └── spree │ │ ├── shared │ │ └── unauthorized.erb │ │ ├── admin │ │ ├── suppliers │ │ │ ├── new.html.erb │ │ │ ├── edit.html.erb │ │ │ ├── index.html.erb │ │ │ └── _form.html.erb │ │ ├── shared │ │ │ └── _header.html.erb │ │ ├── drop_ship_settings │ │ │ └── edit.html.erb │ │ └── shipments │ │ │ ├── edit.html.erb │ │ │ └── index.html.erb │ │ ├── supplier_mailer │ │ └── welcome.html.erb │ │ └── drop_ship_order_mailer │ │ └── supplier_order.html.erb ├── assets │ ├── javascripts │ │ └── spree │ │ │ ├── frontend │ │ │ └── spree_drop_ship.js │ │ │ └── backend │ │ │ └── spree_drop_ship.js │ └── stylesheets │ │ └── spree │ │ ├── frontend │ │ └── spree_drop_ship.css │ │ └── backend │ │ └── spree_drop_ship.scss ├── models │ ├── spree.rb │ ├── spree │ │ ├── supplier_variant.rb │ │ ├── payment_decorator.rb │ │ ├── user_decorator.rb │ │ ├── drop_ship_configuration.rb │ │ ├── stock_location_decorator.rb │ │ ├── variant_decorator.rb │ │ ├── product_decorator.rb │ │ ├── order_decorator.rb │ │ ├── shipment_decorator.rb │ │ ├── supplier_ability.rb │ │ ├── stock │ │ │ └── splitter │ │ │ │ └── drop_ship.rb │ │ └── supplier.rb │ └── ckeditor │ │ └── asset_decorator.rb ├── overrides │ └── spree │ │ ├── admin │ │ ├── products │ │ │ ├── _form │ │ │ │ └── converted_admin_product_form_right.html.erb.deface │ │ │ └── index │ │ │ │ └── override_rows.html.erb.deface │ │ ├── shared │ │ │ └── sub_menu │ │ │ │ └── _configuration │ │ │ │ └── add_drop_ship_settings.html.erb.deface │ │ ├── stock_locations │ │ │ └── _form │ │ │ │ └── add_supplier.html.erb.deface │ │ └── stock_movements │ │ │ └── _form │ │ │ └── fix_bootstrap_form.html.erb.deface │ │ └── layouts │ │ └── admin │ │ ├── add_profile_admin_tabs.html.erb.deface │ │ └── converted_admin_tabs.html.erb.deface ├── controllers │ ├── spree │ │ ├── admin │ │ │ ├── stock_locations_controller_decorator.rb │ │ │ ├── drop_ship_settings_controller.rb │ │ │ ├── products_controller_decorator.rb │ │ │ ├── suppliers_controller.rb │ │ │ └── shipments_controller.rb │ │ ├── base_controller_decorator.rb │ │ └── api │ │ │ └── v1 │ │ │ └── stock_locations_controller_decorator.rb │ └── ckeditor │ │ ├── pictures_controller_decorator.rb │ │ └── attachment_files_controller_decorator.rb └── mailers │ └── spree │ ├── supplier_mailer.rb │ └── drop_ship_order_mailer.rb ├── spec ├── models │ └── spree │ │ ├── payment_decorator_spec.rb │ │ ├── supplier_variant_spec.rb │ │ ├── variant_decorator_spec.rb │ │ ├── product_decorator_spec.rb │ │ ├── user_decorator_spec.rb │ │ ├── stock_location_decorator_spec.rb │ │ ├── shipment_decorator_spec.rb │ │ ├── order_decorator_spec.rb │ │ ├── stock │ │ └── splitter │ │ │ └── drop_ship_spec.rb │ │ ├── supplier_spec.rb │ │ └── supplier_ability_spec.rb ├── features │ └── admin │ │ ├── return_authorizations_spec.rb │ │ ├── orders_spec.rb │ │ ├── products_spec.rb │ │ ├── settings_spec.rb │ │ ├── shipments_spec.rb │ │ ├── stock_spec.rb │ │ ├── suppliers_spec.rb │ │ └── stock_management_spec.rb ├── support │ └── integration_helpers.rb └── spec_helper.rb ├── config ├── routes.rb └── locales │ ├── en.yml │ └── es.yml ├── lib ├── spree_drop_ship.rb ├── spree_drop_ship │ ├── engine.rb │ └── factories.rb ├── generators │ └── spree_drop_ship │ │ └── install │ │ └── install_generator.rb └── tasks │ └── spree_sample.rake ├── Gemfile ├── gemfiles ├── spree_3_2.gemfile ├── spree_3_3.gemfile ├── spree_3_4.gemfile ├── spree_3_1.gemfile └── spree_master.gemfile ├── db └── migrate │ ├── 20130216070944_product_belongs_to_supplier.rb │ ├── 20130606220913_add_permalink_to_suppliers.rb │ ├── 20130405011127_user_belongs_to_supplier.rb │ ├── 20130405005502_stock_locations_belongs_to_supplier.rb │ ├── 20140323170638_add_supplier_commission_to_shipments.rb │ ├── 20130428063053_add_balanced_token_to_suppliers.rb │ ├── 20130510181443_add_supplier_id_to_ckeditor_assets.rb │ ├── 20140416184616_migrate_payment_and_commission.rb │ ├── 20140529041325_create_spree_supplier_variants.rb │ └── 20121006073854_create_suppliers.rb ├── .gitignore ├── script └── rails ├── Rakefile ├── Versionfile ├── .rubocop.yml ├── Appraisals ├── .travis.yml ├── LICENSE ├── spree_drop_ship.gemspec └── README.md /.hound.yml: -------------------------------------------------------------------------------- 1 | --- 2 | rubocop: 3 | config_file: .rubocop.yml 4 | -------------------------------------------------------------------------------- /app/views/spree/shared/unauthorized.erb: -------------------------------------------------------------------------------- 1 | <%= Spree.t(:authorization_failure) %> 2 | -------------------------------------------------------------------------------- /app/assets/javascripts/spree/frontend/spree_drop_ship.js: -------------------------------------------------------------------------------- 1 | //= require spree/frontend 2 | -------------------------------------------------------------------------------- /app/assets/stylesheets/spree/frontend/spree_drop_ship.css: -------------------------------------------------------------------------------- 1 | /* 2 | *= require spree/frontend 3 | */ 4 | -------------------------------------------------------------------------------- /app/models/spree.rb: -------------------------------------------------------------------------------- 1 | module Spree 2 | def self.table_name_prefix 3 | 'spree_' 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/assets/javascripts/spree/backend/spree_drop_ship.js: -------------------------------------------------------------------------------- 1 | //= require spree/backend 2 | // Shipments AJAX API 3 | -------------------------------------------------------------------------------- /spec/models/spree/payment_decorator_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Spree::Payment do 4 | 5 | it { should belong_to(:payable) } 6 | 7 | end 8 | -------------------------------------------------------------------------------- /app/models/spree/supplier_variant.rb: -------------------------------------------------------------------------------- 1 | module Spree 2 | class SupplierVariant < Spree::Base 3 | belongs_to :supplier 4 | belongs_to :variant 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/models/ckeditor/asset_decorator.rb: -------------------------------------------------------------------------------- 1 | if defined?(Ckeditor) 2 | Ckeditor::Asset.class_eval do 3 | belongs_to :supplier, class_name: 'Spree::Supplier' 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/models/spree/supplier_variant_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Spree::SupplierVariant do 4 | skip "add some examples to (or delete) #{__FILE__}" 5 | end 6 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Spree::Core::Engine.add_routes do 2 | namespace :admin do 3 | resource :drop_ship_settings 4 | resources :shipments 5 | resources :suppliers 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/spree_drop_ship.rb: -------------------------------------------------------------------------------- 1 | require 'sass/rails' 2 | require 'spree_api' 3 | require 'spree_backend' 4 | require 'spree_core' 5 | require 'spree_extension' 6 | require 'spree_drop_ship/engine' 7 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'spree', github: 'spree/spree' 4 | gem 'spree_auth_devise', github: 'spree/spree' 5 | gem 'rails-controller-testing' 6 | 7 | gemspec 8 | -------------------------------------------------------------------------------- /spec/models/spree/variant_decorator_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Spree::Variant do 4 | it 'should populate stock item for each of the master variant suppliers' do 5 | skip 'todo' 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /gemfiles/spree_3_2.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "spree", "~> 3.2.0" 6 | gem "spree_auth_devise", "~> 3.3.0" 7 | gem "rails-controller-testing" 8 | 9 | gemspec path: "../" 10 | -------------------------------------------------------------------------------- /gemfiles/spree_3_3.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "spree", "~> 3.3.0" 6 | gem "spree_auth_devise", "~> 3.3.0" 7 | gem "rails-controller-testing" 8 | 9 | gemspec path: "../" 10 | -------------------------------------------------------------------------------- /gemfiles/spree_3_4.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "spree", "~> 3.4.0" 6 | gem "spree_auth_devise", "~> 3.3.0" 7 | gem "rails-controller-testing" 8 | 9 | gemspec path: "../" 10 | -------------------------------------------------------------------------------- /spec/features/admin/return_authorizations_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "Admin - Return Authorizations" do 4 | 5 | skip 'TODO: need to write, but should likely wait until after https://github.com/spree/spree/issues/4026' 6 | 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20130216070944_product_belongs_to_supplier.rb: -------------------------------------------------------------------------------- 1 | class ProductBelongsToSupplier < SpreeExtension::Migration[4.2] 2 | def change 3 | add_column :spree_products, :supplier_id, :integer 4 | add_index :spree_products, :supplier_id 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20130606220913_add_permalink_to_suppliers.rb: -------------------------------------------------------------------------------- 1 | class AddPermalinkToSuppliers < SpreeExtension::Migration[4.2] 2 | def change 3 | add_column :spree_suppliers, :slug, :string 4 | add_index :spree_suppliers, :slug, unique: true 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | \#* 2 | *~ 3 | .#* 4 | .bundle 5 | .DS_Store 6 | .idea 7 | .project 8 | .ruby-gemset 9 | .ruby-version 10 | .sass-cache 11 | coverage 12 | Gemfile.lock 13 | gemfiles/*.lock 14 | tmp 15 | nbproject 16 | pkg 17 | *.swp 18 | spec/dummy 19 | spec/vcr_cassettes 20 | -------------------------------------------------------------------------------- /db/migrate/20130405011127_user_belongs_to_supplier.rb: -------------------------------------------------------------------------------- 1 | class UserBelongsToSupplier < SpreeExtension::Migration[4.2] 2 | def change 3 | add_column Spree.user_class.table_name, :supplier_id, :integer 4 | add_index Spree.user_class.table_name, :supplier_id 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /gemfiles/spree_3_1.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "spree", "~> 3.1.0" 6 | gem "spree_auth_devise", "~> 3.3.0" 7 | gem "rails-controller-testing" 8 | gem "rails", "~> 4.2.10" 9 | 10 | gemspec path: "../" 11 | -------------------------------------------------------------------------------- /db/migrate/20130405005502_stock_locations_belongs_to_supplier.rb: -------------------------------------------------------------------------------- 1 | class StockLocationsBelongsToSupplier < SpreeExtension::Migration[4.2] 2 | def change 3 | add_column :spree_stock_locations, :supplier_id, :integer 4 | add_index :spree_stock_locations, :supplier_id 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20140323170638_add_supplier_commission_to_shipments.rb: -------------------------------------------------------------------------------- 1 | class AddSupplierCommissionToShipments < SpreeExtension::Migration[4.2] 2 | def change 3 | add_column :spree_shipments, :supplier_commission, :decimal, precision: 8, scale: 2, default: 0.0, null: false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/overrides/spree/admin/products/_form/converted_admin_product_form_right.html.erb.deface: -------------------------------------------------------------------------------- 1 | 2 | 3 | <% if can? :manage, Spree::Supplier %> 4 | 5 | <% end %> 6 | -------------------------------------------------------------------------------- /db/migrate/20130428063053_add_balanced_token_to_suppliers.rb: -------------------------------------------------------------------------------- 1 | class AddBalancedTokenToSuppliers < SpreeExtension::Migration[4.2] 2 | def change 3 | add_column :spree_suppliers, :tax_id, :string 4 | add_column :spree_suppliers, :token, :string 5 | add_index :spree_suppliers, :token 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/models/spree/payment_decorator.rb: -------------------------------------------------------------------------------- 1 | module Dropship 2 | module Spree 3 | module PaymentDecorator 4 | def self.prepended(base) 5 | base.belongs_to :payable, polymorphic: true 6 | end 7 | end 8 | end 9 | end 10 | 11 | Spree::Payment.prepend Dropship::Spree::PaymentDecorator 12 | -------------------------------------------------------------------------------- /app/overrides/spree/admin/shared/sub_menu/_configuration/add_drop_ship_settings.html.erb.deface: -------------------------------------------------------------------------------- 1 | 2 | 3 | <%= configurations_sidebar_menu_item Spree.t(:drop_ship_settings), spree.edit_admin_drop_ship_settings_path %> 4 | -------------------------------------------------------------------------------- /db/migrate/20130510181443_add_supplier_id_to_ckeditor_assets.rb: -------------------------------------------------------------------------------- 1 | class AddSupplierIdToCkeditorAssets < SpreeExtension::Migration[4.2] 2 | if table_exists?(:ckeditor_assets) 3 | def change 4 | add_column :ckeditor_assets, :supplier_id, :integer 5 | add_index :ckeditor_assets, :supplier_id 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20140416184616_migrate_payment_and_commission.rb: -------------------------------------------------------------------------------- 1 | class MigratePaymentAndCommission < SpreeExtension::Migration[4.2] 2 | def change 3 | add_column :spree_payments, :payable_id, :integer 4 | add_column :spree_payments, :payable_type, :string 5 | add_index :spree_payments, [:payable_id, :payable_type] 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /script/rails: -------------------------------------------------------------------------------- 1 | # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. 2 | 3 | ENGINE_ROOT = File.expand_path('../..', __FILE__) 4 | ENGINE_PATH = File.expand_path('../../lib/spree_drop_ship/engine', __FILE__) 5 | 6 | require 'rails/all' 7 | require 'rails/engine/commands' 8 | -------------------------------------------------------------------------------- /gemfiles/spree_master.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "spree", github: "spree/spree", branch: "master" 6 | gem "spree_auth_devise", github: "spree/spree_auth_devise", branch: "master" 7 | gem "mysql2", "~> 0.5.1" 8 | gem "rails-controller-testing" 9 | 10 | gemspec path: "../" 11 | -------------------------------------------------------------------------------- /spec/models/spree/product_decorator_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Spree::Product do 4 | 5 | let(:product) { create :product } 6 | 7 | it '#supplier?' do 8 | product.supplier?.should eq false 9 | product.add_supplier! create(:supplier) 10 | product.reload.supplier?.should eq true 11 | end 12 | 13 | end 14 | -------------------------------------------------------------------------------- /app/controllers/spree/admin/stock_locations_controller_decorator.rb: -------------------------------------------------------------------------------- 1 | Spree::Admin::StockLocationsController.class_eval do 2 | create.after :set_supplier 3 | 4 | private 5 | 6 | def set_supplier 7 | if try_spree_current_user.supplier? 8 | @object.supplier = try_spree_current_user.supplier 9 | @object.save 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /app/mailers/spree/supplier_mailer.rb: -------------------------------------------------------------------------------- 1 | module Spree 2 | class SupplierMailer < Spree::BaseMailer 3 | default from: Spree::Store.current.mail_from_address 4 | 5 | def welcome(supplier_id) 6 | @supplier = Supplier.find supplier_id 7 | mail to: @supplier.email, subject: Spree.t('supplier_mailer.welcome.subject') 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/controllers/spree/base_controller_decorator.rb: -------------------------------------------------------------------------------- 1 | Spree::BaseController.class_eval do 2 | prepend_before_action :redirect_supplier 3 | 4 | private 5 | 6 | def redirect_supplier 7 | if ['/admin', '/admin/authorization_failure'].include?(request.path) && try_spree_current_user.try(:supplier) 8 | redirect_to '/admin/shipments' and return false 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | Bundler::GemHelper.install_tasks 3 | 4 | require 'rspec/core/rake_task' 5 | require 'spree/testing_support/common_rake' 6 | 7 | RSpec::Core::RakeTask.new 8 | 9 | task default: [:spec] 10 | 11 | desc 'Generates a dummy app for testing' 12 | task :test_app do 13 | ENV['LIB_NAME'] = 'spree_drop_ship' 14 | Rake::Task['common:test_app'].invoke 'Spree::User' 15 | end 16 | -------------------------------------------------------------------------------- /spec/features/admin/orders_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Admin - Orders', js: true do 4 | 5 | it 'Supplier should not be authorized' do 6 | create(:user) # create extra user so admin role isnt assigned to the user we login as 7 | login_user create(:supplier_user) 8 | visit spree.admin_orders_path 9 | page.should have_content('Authorization Failure') 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /Versionfile: -------------------------------------------------------------------------------- 1 | # This file is used to designate compatibilty with different versions of Spree 2 | # Please see http://spreecommerce.com/documentation/extensions.html#versionfile for details 3 | # '1.3.x' => { :ref => 'e5624f7' } # Not going to be maintained but most of the works there if your looking for a starting point to branch off of. 4 | 5 | '2.4.x' => { :branch => 'master' } 6 | '2.3.x' => { :branch => '2-3-stable' } 7 | -------------------------------------------------------------------------------- /app/overrides/spree/layouts/admin/add_profile_admin_tabs.html.erb.deface: -------------------------------------------------------------------------------- 1 | 2 | 3 | <%# Add tab "profile to admin tabs" %> 4 | <% if try_spree_current_user && try_spree_current_user.supplier? %> 5 | 8 | <% end %> 9 | -------------------------------------------------------------------------------- /spec/models/spree/user_decorator_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Spree.user_class do 4 | 5 | it { should belong_to(:supplier) } 6 | 7 | it { should have_many(:variants).through(:supplier) } 8 | 9 | let(:user) { build :user } 10 | 11 | it '#supplier?' do 12 | user.supplier?.should be false 13 | user.supplier = build :supplier 14 | user.supplier?.should be true 15 | end 16 | 17 | end 18 | -------------------------------------------------------------------------------- /app/assets/stylesheets/spree/backend/spree_drop_ship.scss: -------------------------------------------------------------------------------- 1 | /* 2 | *= require spree/backend 3 | */ 4 | 5 | .center { 6 | text-align: center; 7 | } 8 | 9 | .state.delivered:before, 10 | .state.confirmed:before { 11 | background-color: #ff9300; 12 | } 13 | 14 | .supplier-banks ol li { 15 | margin-left: 20px; 16 | margin-bottom: 5px; 17 | text-align: left; 18 | } 19 | 20 | a .without-textdecor{ 21 | text-decoration: none; 22 | } -------------------------------------------------------------------------------- /app/models/spree/user_decorator.rb: -------------------------------------------------------------------------------- 1 | module Dropship 2 | module Spree 3 | module UserDecorator 4 | def self.prepended(base) 5 | base.belongs_to :supplier, class_name: 'Spree::Supplier' 6 | 7 | base.has_many :variants, through: :supplier 8 | end 9 | 10 | def supplier? 11 | supplier.present? 12 | end 13 | end 14 | end 15 | end 16 | 17 | Spree.user_class.prepend Dropship::Spree::UserDecorator 18 | -------------------------------------------------------------------------------- /spec/support/integration_helpers.rb: -------------------------------------------------------------------------------- 1 | module IntegrationHelpers 2 | 3 | def login_user(user = nil, options = {}) 4 | options[:password] ||= 'secret' 5 | user ||= create(:user, password: options[:password]) 6 | 7 | visit spree.login_path 8 | fill_in 'spree_user[email]', with: user.email 9 | fill_in 'spree_user[password]', with: options[:password] 10 | click_button 'Login' 11 | page.should_not have_content 'Login' 12 | end 13 | 14 | end 15 | -------------------------------------------------------------------------------- /app/mailers/spree/drop_ship_order_mailer.rb: -------------------------------------------------------------------------------- 1 | module Spree 2 | class DropShipOrderMailer < Spree::BaseMailer 3 | default from: Spree::Store.current.mail_from_address 4 | 5 | def supplier_order(shipment_id) 6 | @shipment = Shipment.find shipment_id 7 | @supplier = @shipment.supplier 8 | mail to: @supplier.email, subject: Spree.t('drop_ship_order_mailer.supplier_order.subject', name: Spree::Store.current.name, number: @shipment.number) 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | --- 2 | AllCops: 3 | Exclude: 4 | - 'spec/**/*' 5 | - 'spec/dummy/**/*' 6 | - 'db/**/*' 7 | 8 | # Too picky. 9 | Metrics/LineLength: 10 | Enabled: false 11 | 12 | # This should truly be on for well documented gems. 13 | Style/Documentation: 14 | Enabled: false 15 | 16 | # Neatly aligned code is to swell. 17 | Layout/SpaceBeforeFirstArg: 18 | Enabled: false 19 | 20 | # Avoid contradictory style rules by enforce single quotes. 21 | Style/StringLiterals: 22 | EnforcedStyle: single_quotes 23 | -------------------------------------------------------------------------------- /app/controllers/ckeditor/pictures_controller_decorator.rb: -------------------------------------------------------------------------------- 1 | if defined?(Ckeditor::PicturesController) 2 | Ckeditor::PicturesController.class_eval do 3 | load_and_authorize_resource class: 'Ckeditor::Picture' 4 | after_action :set_supplier, only: [:create] 5 | 6 | def index; end 7 | 8 | private 9 | 10 | def set_supplier 11 | if spree_current_user.supplier? && @picture 12 | @picture.supplier = spree_current_user.supplier 13 | @picture.save 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/controllers/spree/api/v1/stock_locations_controller_decorator.rb: -------------------------------------------------------------------------------- 1 | Spree::Api::V1::StockLocationsController.class_eval do 2 | before_action :supplier_locations, only: [:index] 3 | before_action :supplier_transfers, only: [:index] 4 | 5 | private 6 | 7 | def supplier_locations 8 | params[:q] ||= {} 9 | params[:q][:supplier_id_eq] = spree_current_user.supplier_id 10 | end 11 | 12 | def supplier_transfers 13 | params[:q] ||= {} 14 | params[:q][:supplier_id_eq] = spree_current_user.supplier_id 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /db/migrate/20140529041325_create_spree_supplier_variants.rb: -------------------------------------------------------------------------------- 1 | class CreateSpreeSupplierVariants < SpreeExtension::Migration[4.2] 2 | def change 3 | create_table :spree_supplier_variants do |t| 4 | t.belongs_to :supplier, index: true 5 | t.belongs_to :variant, index: true 6 | t.decimal :cost 7 | 8 | t.timestamps 9 | end 10 | Spree::Product.where.not(supplier_id: nil).each do |product| 11 | product.add_supplier! product.supplier_id 12 | end 13 | remove_column :spree_products, :supplier_id 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/controllers/ckeditor/attachment_files_controller_decorator.rb: -------------------------------------------------------------------------------- 1 | if defined?(Ckeditor::AttachmentFilesController) 2 | Ckeditor::AttachmentFilesController.class_eval do 3 | load_and_authorize_resource class: 'Ckeditor::AttachmentFile' 4 | after_action :set_supplier, only: [:create] 5 | 6 | def index; end 7 | 8 | private 9 | 10 | def set_supplier 11 | if try_spree_current_user.supplier? && @attachment 12 | @attachment.supplier = try_spree_current_user.supplier 13 | @attachment.save 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/controllers/spree/admin/drop_ship_settings_controller.rb: -------------------------------------------------------------------------------- 1 | class Spree::Admin::DropShipSettingsController < Spree::Admin::BaseController 2 | def edit 3 | @config = Spree::DropShipConfiguration.new 4 | end 5 | 6 | def update 7 | config = Spree::DropShipConfiguration.new 8 | 9 | params.each do |name, value| 10 | next unless config.has_preference? name 11 | config[name] = value 12 | end 13 | 14 | flash[:success] = Spree.t('admin.drop_ship_settings.update.success') 15 | redirect_to spree.edit_admin_drop_ship_settings_path 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/overrides/spree/admin/stock_locations/_form/add_supplier.html.erb.deface: -------------------------------------------------------------------------------- 1 | 2 | 3 | <%# Remove extra div and change classes %> 4 | <% if spree_current_user.admin? %> 5 |
6 | <%= f.field_container :supplier do %> 7 | <%= f.label :supplier_id, Spree.t(:supplier) %> 8 | <%= f.collection_select(:supplier_id, Spree::Supplier.all, :id, :name, { :include_blank => 'None' }, { :class => 'select2' }) %> 9 | <%= f.error_message_on :supplier %> 10 | <% end %> 11 |
12 | <% end %> 13 | -------------------------------------------------------------------------------- /app/views/spree/admin/suppliers/new.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :page_actions do %> 2 | <%= button_link_to Spree.t('back_to_suppliers'), spree.admin_suppliers_path, icon: 'arrow-left' %> 3 | <% end %> 4 | 5 | <% content_for :page_title do %> 6 | <%= Spree.t('new_supplier') %> 7 | <% end %> 8 | 9 | <%= render 'spree/shared/error_messages', target: @object%> 10 | 11 | <%= form_for [:admin, @object] do |f| %> 12 | <%= render "form", form: f %> 13 | <%# render 'create' and 'cancel' buttons %> 14 |
15 | <%= render :partial => 'spree/admin/shared/new_resource_links' %> 16 |
17 | <% end %> 18 | -------------------------------------------------------------------------------- /app/overrides/spree/admin/stock_movements/_form/fix_bootstrap_form.html.erb.deface: -------------------------------------------------------------------------------- 1 | 2 | 3 | <%# Fix markup form bug %> 4 | <%= f.field_container :quantity, class: ['form-group'] do %> 5 | <%= f.label :quantity, Spree.t(:quantity) %> 6 | <%= f.text_field :quantity , class: 'form-control'%> 7 | <% end %> 8 | <%= f.field_container :stock_item_id, class: ['form-group'] do %> 9 | <%= f.label :stock_item_id, Spree.t(:stock_item_id) %> 10 | <%= f.text_field 'stock_item_id', :class => 'select2', :'data-stock-location-id' => params[:stock_location_id] %> 11 | <% end %> -------------------------------------------------------------------------------- /app/controllers/spree/admin/products_controller_decorator.rb: -------------------------------------------------------------------------------- 1 | Spree::Admin::ProductsController.class_eval do 2 | before_action :get_suppliers, only: %i[edit update] 3 | before_action :supplier_collection, only: [:index] 4 | 5 | private 6 | 7 | def get_suppliers 8 | @suppliers = Spree::Supplier.order(:name) 9 | end 10 | 11 | # Scopes the collection to the Supplier. 12 | def supplier_collection 13 | if try_spree_current_user && !try_spree_current_user.admin? && try_spree_current_user.supplier? 14 | @collection = @collection.joins(:suppliers).where('spree_suppliers.id = ?', try_spree_current_user.supplier_id) 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/features/admin/products_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Admin - Products', js: true do 4 | 5 | context 'as Admin' do 6 | 7 | xit 'should be able to change supplier' do 8 | s1 = create(:supplier) 9 | s2 = create(:supplier) 10 | product = create :product 11 | product.add_supplier! s1 12 | 13 | login_user create(:admin_user) 14 | visit spree.admin_product_path(product) 15 | 16 | select2 s2.name, from: 'Supplier' 17 | click_button 'Update' 18 | 19 | expect(page).to have_content("Product \"#{product.name}\" has been successfully updated!") 20 | expect(product.reload.suppliers.first.id).to eql(s2.id) 21 | end 22 | 23 | end 24 | 25 | end 26 | -------------------------------------------------------------------------------- /app/models/spree/drop_ship_configuration.rb: -------------------------------------------------------------------------------- 1 | module Spree 2 | class DropShipConfiguration < Preferences::Configuration 3 | # Automatically deliver drop ship orders by default. 4 | preference :automatically_deliver_orders_to_supplier, :boolean, default: true 5 | 6 | # Default flat rate to charge suppliers per order for commission. 7 | preference :default_commission_flat_rate, :float, default: 0.0 8 | 9 | # Default percentage to charge suppliers per order for commission. 10 | preference :default_commission_percentage, :float, default: 0.0 11 | 12 | # Determines whether or not to email a new supplier their welcome email. 13 | preference :send_supplier_email, :boolean, default: true 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /db/migrate/20121006073854_create_suppliers.rb: -------------------------------------------------------------------------------- 1 | class CreateSuppliers < SpreeExtension::Migration[4.2] 2 | 3 | def change 4 | create_table :spree_suppliers do |t| 5 | t.boolean :active, default: false, null: false 6 | t.references :address 7 | t.decimal :commission_flat_rate, :precision => 8, :scale => 2, :default => 0.0, :null => false 8 | t.float :commission_percentage, default: 0.0, null: false 9 | t.string :email 10 | t.string :name 11 | t.string :url 12 | t.datetime :deleted_at 13 | t.timestamps 14 | end 15 | add_index :spree_suppliers, :address_id 16 | add_index :spree_suppliers, :deleted_at 17 | add_index :spree_suppliers, :active 18 | end 19 | 20 | end 21 | -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | appraise 'spree-3-1' do 2 | gem 'spree', '~> 3.1.0' 3 | gem 'spree_auth_devise', '~> 3.1.0' 4 | end 5 | 6 | appraise 'spree-3-2' do 7 | gem 'spree', '~> 3.2.0' 8 | gem 'spree_auth_devise', '~> 3.2.0' 9 | gem 'rails-controller-testing' 10 | end 11 | 12 | appraise 'spree-3-3' do 13 | gem 'spree', '~> 3.3.0' 14 | gem 'spree_auth_devise', '~> 3.3.0' 15 | gem 'rails-controller-testing' 16 | end 17 | 18 | appraise 'spree-3-4' do 19 | gem 'spree', '~> 3.4.0' 20 | gem 'spree_auth_devise', '~> 3.4.0' 21 | gem 'rails-controller-testing' 22 | end 23 | 24 | appraise 'spree-master' do 25 | gem 'spree', github: 'spree/spree', branch: 'master' 26 | gem 'spree_auth_devise', github: 'spree/spree_auth_devise', branch: 'master' 27 | gem 'rails-controller-testing' 28 | end 29 | -------------------------------------------------------------------------------- /app/overrides/spree/layouts/admin/converted_admin_tabs.html.erb.deface: -------------------------------------------------------------------------------- 1 | 2 | 3 | <%# Add sidebar's items for supplier %> 4 | <% if can? :index, Spree::Supplier %> 5 | 8 | <% end %> 9 | <% if can? :index, Spree::Shipment %> 10 | 13 | <% end %> 14 | <% if try_spree_current_user.try(:supplier?) and can? :index, Spree::StockLocation %> 15 | 18 | <% end %> 19 | -------------------------------------------------------------------------------- /app/views/spree/admin/suppliers/edit.html.erb: -------------------------------------------------------------------------------- 1 | <% if can? :index, Spree::Supplier %> 2 | <% content_for :page_actions do %> 3 | <%= button_link_to Spree.t('back_to_suppliers'), spree.admin_suppliers_path, icon: 'arrow-left' %> 4 | <% end %> 5 | <% end %> 6 | 7 | <% content_for :page_title do %> 8 | <%== Spree.t(:editing_supplier) + " “#{@object.name}”".html_safe %> 9 | <% end %> 10 | 11 | <%= render 'spree/shared/error_messages', target: @object %> 12 | 13 | <%= form_for [:admin, @object] do |f| %> 14 | <%= render "form", form: f %> 15 |
16 | <%# render 'update' and 'cancel' buttons %> 17 |
18 | <%= render :partial => 'spree/admin/shared/edit_resource_links' %> 19 |
20 | <% end %> 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/models/spree/stock_location_decorator.rb: -------------------------------------------------------------------------------- 1 | module Dropship 2 | module Spree 3 | module StockLocationDecorator 4 | def self.prepended(base) 5 | base.belongs_to :supplier, class_name: 'Spree::Supplier' 6 | 7 | base.scope :by_supplier, ->(supplier_id) { where(supplier_id: supplier_id) } 8 | end 9 | 10 | # Wrapper for creating a new stock item respecting the backorderable config and supplier 11 | def propagate_variant(variant) 12 | if supplier_id.blank? || variant.suppliers.pluck(:id).include?(supplier_id) 13 | stock_items.create!(variant: variant, backorderable: backorderable_default) 14 | end 15 | end 16 | 17 | def available?(variant) 18 | stock_item(variant).try(:available?) 19 | end 20 | end 21 | end 22 | end 23 | 24 | Spree::StockLocation.prepend Dropship::Spree::StockLocationDecorator 25 | -------------------------------------------------------------------------------- /app/models/spree/variant_decorator.rb: -------------------------------------------------------------------------------- 1 | module Dropship 2 | module Spree 3 | module VariantDecorator 4 | def self.prepended(base) 5 | base.has_many :supplier_variants 6 | base.has_many :suppliers, through: :supplier_variants 7 | 8 | base.before_create :populate_for_suppliers 9 | end 10 | 11 | private 12 | 13 | def create_stock_items 14 | ::Spree::StockLocation.all.find_each do |stock_location| 15 | if stock_location.supplier_id.blank? || suppliers.pluck(:id).include?(stock_location.supplier_id) 16 | stock_location.propagate_variant(self) if stock_location.propagate_all_variants? 17 | end 18 | end 19 | end 20 | 21 | def populate_for_suppliers 22 | self.suppliers = product.suppliers 23 | end 24 | end 25 | end 26 | end 27 | 28 | Spree::Variant.prepend Dropship::Spree::VariantDecorator 29 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | dist: trusty 3 | 4 | before_script: 5 | - export DISPLAY=:99.0 && sh -e /etc/init.d/xvfb start && sleep 3 6 | 7 | script: 8 | - bundle exec rake test_app 9 | - bundle exec rake spec 10 | 11 | addons: 12 | chrome: stable 13 | 14 | env: 15 | - DB=mysql 16 | - DB=postgres 17 | 18 | language: ruby 19 | 20 | rvm: 21 | - 2.5.1 22 | - 2.2.7 23 | 24 | matrix: 25 | allow_failures: 26 | - gemfile: gemfiles/spree_master.gemfile 27 | 28 | gemfile: 29 | - gemfiles/spree_3_1.gemfile 30 | - gemfiles/spree_3_2.gemfile 31 | - gemfiles/spree_3_3.gemfile 32 | - gemfiles/spree_3_4.gemfile 33 | - gemfiles/spree_master.gemfile 34 | 35 | before_install: 36 | - mysql -u root -e "GRANT ALL ON *.* TO 'travis'@'%';" 37 | - wget http://chromedriver.storage.googleapis.com/2.38/chromedriver_linux64.zip 38 | - unzip chromedriver_linux64.zip 39 | - sudo apt-get install libnss3 40 | - sudo cp chromedriver /usr/local/bin/ 41 | - sudo chmod +x /usr/local/bin/chromedriver 42 | -------------------------------------------------------------------------------- /spec/models/spree/stock_location_decorator_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Spree::StockLocation do 4 | 5 | it { should belong_to(:supplier) } 6 | 7 | subject { create(:stock_location, backorderable_default: true) } 8 | 9 | context "propagate variants" do 10 | 11 | let(:variant) { build(:variant) } 12 | let(:stock_item) { subject.propagate_variant(variant) } 13 | 14 | context "passes backorderable default config" do 15 | context "true" do 16 | before { subject.backorderable_default = true } 17 | it { stock_item.backorderable.should be true } 18 | end 19 | 20 | context "false" do 21 | before { subject.backorderable_default = false } 22 | it { stock_item.backorderable.should be false } 23 | end 24 | end 25 | 26 | context 'does not propagate for non supplier variants' do 27 | before { subject.supplier_id = create(:supplier).id } 28 | it { stock_item.should be_nil } 29 | end 30 | 31 | end 32 | 33 | end 34 | -------------------------------------------------------------------------------- /app/views/spree/admin/shared/_header.html.erb: -------------------------------------------------------------------------------- 1 | <%# Override header for supplier %> 2 | <% admin = try_spree_current_user.try(:supplier?) || try_spree_current_user.try(:has_spree_role?, "admin") %> 3 | 4 |
"> 5 | 6 | 23 | 24 |
-------------------------------------------------------------------------------- /lib/spree_drop_ship/engine.rb: -------------------------------------------------------------------------------- 1 | module SpreeDropShip 2 | class Engine < Rails::Engine 3 | require 'spree/core' 4 | isolate_namespace Spree 5 | engine_name 'spree_drop_ship' 6 | 7 | config.autoload_paths += %W[#{config.root}/lib] 8 | 9 | # use rspec for tests 10 | config.generators do |g| 11 | g.test_framework :rspec 12 | end 13 | 14 | initializer 'spree_drop_ship.custom_splitters', after: 'spree.register.stock_splitters' do |app| 15 | app.config.spree.stock_splitters << Spree::Stock::Splitter::DropShip 16 | end 17 | 18 | initializer 'spree_drop_ship.preferences', before: :load_config_initializers do |_app| 19 | SpreeDropShip::Config = Spree::DropShipConfiguration.new 20 | end 21 | 22 | def self.activate 23 | Dir.glob(File.join(File.dirname(__FILE__), '../../app/**/*_decorator*.rb')) do |c| 24 | Rails.configuration.cache_classes ? require(c) : load(c) 25 | end 26 | Spree::Ability.register_ability(Spree::SupplierAbility) 27 | end 28 | 29 | config.to_prepare &method(:activate).to_proc 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /app/views/spree/admin/drop_ship_settings/edit.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :page_title do %> 2 | <%= Spree.t("drop_ship_settings") %> 3 | <% end %> 4 | 5 | <%= form_tag(spree.admin_drop_ship_settings_path, :method => :put) do %> 6 |
7 | <% %w( default_commission_flat_rate default_commission_percentage automatically_deliver_orders_to_supplier send_supplier_email).each do |key| 8 | type = SpreeDropShip::Config.preference_type(key) %> 9 |
10 | <%= label_tag(key, Spree.t(key) + ': ') + tag(:br) if type != :boolean %> 11 | <%= preference_field_tag(key, SpreeDropShip::Config[key], :type => type ,:class => 'form-control')%> 12 | <%= label_tag(key, Spree.t(key)) + tag(:br) if type == :boolean %> 13 |
14 | <% end %> 15 |
16 | <%= button Spree.t(:update), 'update' %> 17 | <%= Spree.t(:or) %> 18 | <%= button_link_to Spree.t(:cancel), spree.edit_admin_drop_ship_settings_url, :icon => 'cancel' %> 19 |
20 |
21 | <% end %> -------------------------------------------------------------------------------- /spec/features/admin/settings_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Admin - DropShip Settings', js: true do 4 | 5 | before do 6 | login_user create(:admin_user) 7 | 8 | visit spree.admin_path 9 | within '[data-hook=admin_tabs]' do 10 | click_link 'Configuration' 11 | end 12 | within 'ul[data-hook=admin_configurations_sidebar_menu]' do 13 | click_link 'Drop Ship Settings' 14 | end 15 | end 16 | 17 | it 'should be able to be updated' do 18 | # Change settings 19 | uncheck 'send_supplier_email' 20 | fill_in 'default_commission_flat_rate', with: 0.30 21 | fill_in 'default_commission_percentage', with: 10 22 | click_button 'Update' 23 | expect(page).to have_content('Drop ship settings successfully updated.') 24 | 25 | # Verify update saved properly by reversing checkboxes or checking field values. 26 | check 'send_supplier_email' 27 | click_button 'Update' 28 | find_field('default_commission_flat_rate').value.to_f.should eql(0.3) 29 | find_field('default_commission_percentage').value.to_f.should eql(10.0) 30 | expect(page).to have_content('Drop ship settings successfully updated.') 31 | end 32 | 33 | end 34 | -------------------------------------------------------------------------------- /app/models/spree/product_decorator.rb: -------------------------------------------------------------------------------- 1 | module Dropship 2 | module Spree 3 | module ProductDecorator 4 | def self.prepended(base) 5 | base.has_many :suppliers, through: :master 6 | end 7 | 8 | def add_supplier!(supplier_or_id) 9 | supplier = supplier_or_id.is_a?(::Spree::Supplier) ? supplier_or_id : ::Spree::Supplier.find(supplier_or_id) 10 | populate_for_supplier! supplier if supplier 11 | end 12 | 13 | def add_suppliers!(supplier_ids) 14 | ::Spree::Supplier.where(id: supplier_ids).find_each do |supplier| 15 | populate_for_supplier! supplier 16 | end 17 | end 18 | 19 | # Returns true if the product has a drop shipping supplier. 20 | def supplier? 21 | suppliers.present? 22 | end 23 | 24 | private 25 | 26 | def populate_for_supplier!(supplier) 27 | variants_including_master.each do |variant| 28 | unless variant.suppliers.pluck(:id).include?(supplier.id) 29 | variant.suppliers << supplier 30 | supplier.stock_locations.each { |location| location.propagate_variant(variant) } 31 | end 32 | end 33 | end 34 | end 35 | end 36 | end 37 | 38 | Spree::Product.prepend Dropship::Spree::ProductDecorator 39 | -------------------------------------------------------------------------------- /app/views/spree/admin/shipments/edit.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :page_actions do %> 2 | <%= button_link_to Spree.t(:back_to_shipments_list), spree.admin_shipments_path, icon: 'arrow-left' %> 3 | <% end %> 4 | 5 | <% if spree_current_user.admin? && @shipment.supplier.present? %> 6 |
7 |
8 | <%= Spree.t(:supplier_information) %> 9 |
10 | <%= Spree::Supplier.human_attribute_name(:name) %>: <%= @shipment.supplier.name %>
11 | <%= Spree::Supplier.human_attribute_name(:email) %>: <%= @shipment.supplier.email %>
12 | <%= Spree::Supplier.human_attribute_name(:url) %>: <%= link_to @shipment.supplier.url, @shipment.supplier.url if @shipment.supplier.url.present? %>
13 |
14 |
15 | <%= Spree.t('contact_information') %>: 16 |
17 | <%= render partial: 'spree/shared/address', locals: { address: @shipment.supplier.address } %> 18 |
19 |
20 | <% end %> 21 | 22 |
23 | <%= render partial: "spree/admin/orders/shipment", locals: { order: @shipment.order, shipment: @shipment } %> 24 |
25 | -------------------------------------------------------------------------------- /app/models/spree/order_decorator.rb: -------------------------------------------------------------------------------- 1 | module Dropship 2 | module Spree 3 | module OrderDecorator 4 | def self.prepended(base) 5 | base.has_many :stock_locations, through: :shipments 6 | base.has_many :suppliers, through: :stock_locations 7 | end 8 | 9 | # Once order is finalized we want to notify the suppliers of their drop ship orders. 10 | # Here we are handling notification by emailing the suppliers. 11 | # If you want to customize this you could override it as a hook for notifying a supplier with a API request instead. 12 | def finalize! 13 | super 14 | 15 | shipments.each do |shipment| 16 | next unless ::SpreeDropShip::Config[:send_supplier_email] && shipment.supplier.present? 17 | begin 18 | ::Spree::DropShipOrderMailer.supplier_order(shipment.id).deliver! 19 | rescue => ex # Errno::ECONNREFUSED => ex 20 | puts ex.message 21 | puts ex.backtrace.join("\n") 22 | Rails.logger.error ex.message 23 | Rails.logger.error ex.backtrace.join("\n") 24 | return true # always return true so that failed email doesn't crash app. 25 | end 26 | end 27 | end 28 | end 29 | end 30 | end 31 | 32 | Spree::Order.prepend Dropship::Spree::OrderDecorator 33 | -------------------------------------------------------------------------------- /app/controllers/spree/admin/suppliers_controller.rb: -------------------------------------------------------------------------------- 1 | class Spree::Admin::SuppliersController < Spree::Admin::ResourceController 2 | def edit 3 | @object.address = Spree::Address.default if @object.address.blank? 4 | respond_with(@object) do |format| 5 | format.html { render layout: !request.xhr? } 6 | format.js { render layout: false } 7 | end 8 | end 9 | 10 | def new 11 | @object = Spree::Supplier.new(address_attributes: {country_id: Spree::Address.default.country_id}) 12 | end 13 | 14 | private 15 | 16 | def collection 17 | params[:q] ||= {} 18 | params[:q][:meta_sort] ||= 'name.asc' 19 | @search = Spree::Supplier.search(params[:q]) 20 | @collection = @search.result.page(params[:page]).per(per_page) 21 | end 22 | 23 | def per_page 24 | params[:per_page] || admin_orders_per_page || orders_per_page 25 | end 26 | 27 | def admin_orders_per_page 28 | Spree::Config[:admin_orders_per_page] if Spree::Config.has_preference?(:admin_orders_per_page) 29 | end 30 | 31 | def orders_per_page 32 | Spree::Config[:orders_per_page] if Spree::Config.has_preference?(:orders_per_page) 33 | end 34 | 35 | def find_resource 36 | Spree::Supplier.friendly.find(params[:id]) 37 | end 38 | 39 | def location_after_save 40 | spree.edit_admin_supplier_path(@object) 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /app/models/spree/shipment_decorator.rb: -------------------------------------------------------------------------------- 1 | module Dropship 2 | module Spree 3 | module ShipmentDecorator 4 | def self.prepended(base) 5 | # TODO: here to fix cancan issue thinking its just Order 6 | base.belongs_to :order, class_name: 'Spree::Order', touch: true, inverse_of: :shipments 7 | 8 | base.has_many :payments, as: :payable 9 | 10 | base.scope :by_supplier, ->(supplier_id) { base.joins(:stock_location).where(spree_stock_locations: { supplier_id: supplier_id }) } 11 | 12 | base.delegate :supplier, to: :stock_location 13 | end 14 | 15 | def display_final_price_with_items 16 | ::Spree::Money.new final_price_with_items 17 | end 18 | 19 | def final_price_with_items 20 | item_cost + final_price 21 | end 22 | 23 | # TODO: move commission to spree_marketplace? 24 | def supplier_commission_total 25 | ((final_price_with_items * supplier.commission_percentage / 100) + supplier.commission_flat_rate) 26 | end 27 | 28 | private 29 | 30 | def after_ship 31 | super 32 | 33 | update_commission if supplier.present? 34 | end 35 | 36 | def update_commission 37 | update_column :supplier_commission, supplier_commission_total 38 | end 39 | end 40 | end 41 | end 42 | 43 | Spree::Shipment.prepend Dropship::Spree::ShipmentDecorator 44 | -------------------------------------------------------------------------------- /app/models/spree/supplier_ability.rb: -------------------------------------------------------------------------------- 1 | module Spree 2 | class SupplierAbility 3 | include CanCan::Ability 4 | 5 | def initialize(user) 6 | user ||= Spree.user_class.new 7 | 8 | if user.supplier 9 | # TODO: Want this to be inline like: 10 | # can [:admin, :read, :stock], Spree::Product, suppliers: { id: user.supplier_id } 11 | # can [:admin, :read, :stock], Spree::Product, supplier_ids: user.supplier_id 12 | can [:admin, :read, :stock], Spree::Product do |product| 13 | product.supplier_ids.include?(user.supplier_id) 14 | end 15 | can %i[admin index], Spree::Product 16 | can %i[admin index], Spree::Stock 17 | can %i[admin manage read ready ship], Spree::Shipment, order: { state: 'complete' }, stock_location: { supplier_id: user.supplier_id } 18 | can %i[admin create update], :stock_items 19 | can %i[admin manage], Spree::StockItem, stock_location_id: user.supplier.stock_locations.pluck(:id) 20 | can %i[admin manage], Spree::StockLocation, supplier_id: user.supplier_id 21 | can :create, Spree::StockLocation 22 | can %i[admin manage], Spree::StockMovement, stock_item: { stock_location_id: user.supplier.stock_locations.pluck(:id) } 23 | can :create, Spree::StockMovement 24 | can %i[admin update], Spree::Supplier, id: user.supplier_id 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/generators/spree_drop_ship/install/install_generator.rb: -------------------------------------------------------------------------------- 1 | module SpreeDropShip 2 | module Generators 3 | class InstallGenerator < Rails::Generators::Base 4 | class_option :auto_run_migrations, type: :boolean, default: false 5 | 6 | def add_javascripts 7 | append_file 'vendor/assets/javascripts/spree/frontend/all.js', "//= require spree/frontend/spree_drop_ship\n" 8 | append_file 'vendor/assets/javascripts/spree/backend/all.js', "//= require spree/backend/spree_drop_ship\n" 9 | end 10 | 11 | def add_stylesheets 12 | inject_into_file 'vendor/assets/stylesheets/spree/frontend/all.css', " *= require spree/frontend/spree_drop_ship\n", before: /\*\//, verbose: true 13 | inject_into_file 'vendor/assets/stylesheets/spree/backend/all.css', " *= require spree/backend/spree_drop_ship\n", before: /\*\//, verbose: true 14 | end 15 | 16 | def add_migrations 17 | run 'bundle exec rake railties:install:migrations FROM=spree_drop_ship' 18 | end 19 | 20 | def run_migrations 21 | run_migrations = options[:auto_run_migrations] || ['', 'y', 'Y'].include?(ask('Would you like to run the migrations now? [Y/n]')) 22 | if run_migrations 23 | run 'bundle exec rake db:migrate' 24 | else 25 | puts 'Skipping rake db:migrate, don\'t forget to run it!' 26 | end 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /app/overrides/spree/admin/products/index/override_rows.html.erb.deface: -------------------------------------------------------------------------------- 1 | 2 | 3 | <%# Changed classes to new like https://github.com/spree/spree/blob/master/backend/app/views/spree/admin/stock_locations/_form.html.erb %> 4 | <%= product.sku rescue '' %> 5 | <%= mini_image product, class: "thumbnail" %> 6 | 7 | <% if try_spree_current_user && try_spree_current_user.supplier? %> 8 | <%= link_to product.try(:name), stock_admin_product_path(product)%> 9 | <% else %> 10 | <%= link_to product.try(:name), edit_admin_product_path(product) %> 11 | <% end %> 12 | 13 | <%= product.display_price.to_html rescue '' %> 14 | 15 | <% if try_spree_current_user && try_spree_current_user.supplier? %> 16 | <%= link_to 'SM', stock_admin_product_path(product), title:'Stock Management', class: 'btn btn-default btn-sm with-tip without-textdecor' if can?(:stock, product) && !product.deleted? %> 17 | <% else %> 18 | <%= link_to_edit product, :no_text => true, :class => 'edit' if can?(:edit, product) && !product.deleted? %> 19 | <% end %> 20 | <%= link_to_clone product, :no_text => true, :class => 'clone' if can?(:clone, product) %> 21 | <%= link_to_delete product, :no_text => true if can?(:delete, product) && !product.deleted? %> 22 | 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2013 Jeff Dutil 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 Spree 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 | -------------------------------------------------------------------------------- /spec/models/spree/shipment_decorator_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Spree::Shipment do 4 | 5 | describe 'Scopes' do 6 | 7 | it '#by_supplier' do 8 | supplier = create(:supplier) 9 | stock_location_1 = supplier.stock_locations.first 10 | stock_location_2 = create(:stock_location, supplier: supplier) 11 | shipment_1 = create(:shipment) 12 | shipment_2 = create(:shipment, stock_location: stock_location_1) 13 | shipment_3 = create(:shipment) 14 | shipment_4 = create(:shipment, stock_location: stock_location_2) 15 | shipment_5 = create(:shipment) 16 | shipment_6 = create(:shipment, stock_location: stock_location_1) 17 | 18 | expect(subject.class.by_supplier(supplier.id)).to match_array([shipment_2, shipment_4, shipment_6]) 19 | end 20 | 21 | end 22 | 23 | describe '#after_ship' do 24 | before do 25 | allow_any_instance_of(Spree::ShipmentHandler).to receive(:send_shipped_email) 26 | end 27 | 28 | it 'should capture payment if balance due' do 29 | skip 'TODO make it so!' 30 | end 31 | 32 | it 'should track commission for shipment' do 33 | supplier = create(:supplier_with_commission) 34 | shipment = create(:shipment, stock_location: supplier.stock_locations.first) 35 | 36 | expect(shipment.supplier_commission.to_f).to eql(0.0) 37 | shipment.stub final_price_with_items: 10.0 38 | shipment.send(:after_ship) 39 | expect(shipment.reload.supplier_commission.to_f).to eql(1.5) 40 | end 41 | 42 | end 43 | 44 | it '#final_price_with_items' do 45 | shipment = build :shipment 46 | shipment.stub item_cost: 50.0, final_price: 5.5 47 | expect(shipment.final_price_with_items.to_f).to eql(55.5) 48 | end 49 | 50 | end 51 | -------------------------------------------------------------------------------- /spree_drop_ship.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.platform = Gem::Platform::RUBY 3 | s.name = 'spree_drop_ship' 4 | s.version = '3.0.1.beta' 5 | s.summary = 'Spree Drop Shipping Extension' 6 | s.description = 'Adds drop shipping functionality to Spree stores.' 7 | s.required_ruby_version = '>= 2.0.0' 8 | 9 | s.author = 'Jeff Dutil' 10 | s.email = 'JDutil@BurlingtonWebApps.com' 11 | s.homepage = 'http://github.com/JDutil/spree_drop_ship' 12 | 13 | s.files = `git ls-files`.split("\n") 14 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 15 | s.require_path = 'lib' 16 | s.requirements << 'none' 17 | 18 | 19 | s.add_dependency 'spree_api', '>= 3.1.0', '< 4.0' 20 | s.add_dependency 'spree_backend', '>= 3.1.0', '< 4.0' 21 | s.add_dependency 'spree_core', '>= 3.1.0', '< 4.0' 22 | s.add_dependency 'spree_extension' 23 | 24 | s.add_development_dependency 'appraisal' 25 | s.add_development_dependency 'capybara' 26 | s.add_development_dependency 'coffee-rails' 27 | s.add_development_dependency 'coveralls' 28 | s.add_development_dependency 'database_cleaner' 29 | s.add_development_dependency 'factory_bot_rails' 30 | s.add_development_dependency 'ffaker' 31 | s.add_development_dependency 'launchy' 32 | s.add_development_dependency 'mysql2', '~> 0.3.18' 33 | s.add_development_dependency 'pg' , '~> 0.18' 34 | s.add_development_dependency 'puma' 35 | s.add_development_dependency 'rspec-rails' 36 | s.add_development_dependency 'sass-rails' 37 | s.add_development_dependency 'selenium-webdriver' 38 | s.add_development_dependency 'shoulda-matchers' 39 | s.add_development_dependency 'spree_auth_devise' 40 | s.add_development_dependency 'spree_sample' 41 | s.add_development_dependency 'sqlite3' 42 | end 43 | -------------------------------------------------------------------------------- /app/models/spree/stock/splitter/drop_ship.rb: -------------------------------------------------------------------------------- 1 | module Spree 2 | module Stock 3 | module Splitter 4 | class DropShip < Spree::Stock::Splitter::Base 5 | def split(packages) 6 | split_packages = [] 7 | packages.each do |package| 8 | # Package fulfilled items together. 9 | fulfilled = package.contents.select { |content| content.variant.suppliers.count == 0 } 10 | split_packages << build_package(fulfilled) 11 | # Determine which supplier to package drop shipped items. 12 | drop_ship = package.contents.select { |content| content.variant.suppliers.count > 0 } 13 | drop_ship.each do |content| 14 | # Select the related variant 15 | variant = content.variant 16 | # Select suppliers ordering ascending according to cost. 17 | suppliers = variant.supplier_variants.order('spree_supplier_variants.cost ASC').map(&:supplier) 18 | # Select first supplier that has stock location with avialable stock item. 19 | available_supplier = suppliers.find do |supplier| 20 | supplier.stock_locations_with_available_stock_items(variant).any? 21 | end 22 | # Select the first available stock location with in the available_supplier stock locations. 23 | stock_location = available_supplier.stock_locations_with_available_stock_items(variant).first 24 | # Add to any existing packages or create a new one. 25 | if existing_package = split_packages.find { |p| p.stock_location == stock_location } 26 | existing_package.contents << content 27 | else 28 | split_packages << Spree::Stock::Package.new(stock_location, [content]) 29 | end 30 | end 31 | end 32 | return_next split_packages 33 | end 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/models/spree/order_decorator_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Spree::Order do 4 | 5 | context '#finalize_with_drop_ship!' do 6 | 7 | after do 8 | SpreeDropShip::Config[:send_supplier_email] = true 9 | end 10 | 11 | it 'should deliver drop ship orders when Spree::DropShipConfig[:send_supplier_email] == true' do 12 | order = create(:order_with_totals, ship_address: create(:address)) 13 | order.line_items = [create(:line_item, variant: create(:variant_with_supplier)), create(:line_item, variant: create(:variant_with_supplier))] 14 | order.create_proposed_shipments 15 | 16 | order.shipments.each do |shipment| 17 | Spree::DropShipOrderMailer.should_receive(:supplier_order).with(shipment.id).and_return(double(Mail, :deliver! => true)) 18 | end 19 | 20 | order.finalize! 21 | order.reload 22 | 23 | # Check orders are properly split. 24 | order.shipments.size.should eql(2) 25 | order.shipments.each do |shipment| 26 | shipment.line_items.size.should eql(1) 27 | shipment.line_items.first.variant.suppliers.first.should eql(shipment.supplier) 28 | end 29 | end 30 | 31 | it 'should NOT deliver drop ship orders when Spree::DropShipConfig[:send_supplier_email] == false' do 32 | SpreeDropShip::Config[:send_supplier_email] = false 33 | order = create(:order_with_totals, ship_address: create(:address)) 34 | order.line_items = [create(:line_item, variant: create(:variant_with_supplier)), create(:line_item, variant: create(:variant_with_supplier))] 35 | order.create_proposed_shipments 36 | 37 | order.shipments.each do |shipment| 38 | Spree::DropShipOrderMailer.should_not_receive(:supplier_order).with(shipment.id) 39 | end 40 | 41 | order.finalize! 42 | order.reload 43 | 44 | # Check orders are properly split. 45 | order.shipments.size.should eql(2) 46 | order.shipments.each do |shipment| 47 | shipment.line_items.size.should eql(1) 48 | shipment.line_items.first.variant.suppliers.first.should eql(shipment.supplier) 49 | end 50 | end 51 | 52 | end 53 | 54 | end 55 | -------------------------------------------------------------------------------- /spec/models/spree/stock/splitter/drop_ship_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Spree 4 | module Stock 5 | module Splitter 6 | describe DropShip do 7 | 8 | let(:supplier_1) { create(:supplier) } 9 | let(:supplier_2) { create(:supplier) } 10 | 11 | let(:variant_1) { 12 | v = create(:variant) 13 | v.product.add_supplier! supplier_1 14 | v.reload.supplier_variants.find_by_supplier_id(supplier_1.id).update_column(:cost, 5) 15 | v.product.add_supplier! supplier_2 16 | v.reload.supplier_variants.find_by_supplier_id(supplier_2.id).update_column(:cost, 6) 17 | v 18 | } 19 | let(:variant_2) { 20 | v = create(:variant) 21 | v.product.add_supplier! supplier_1 22 | v.reload.supplier_variants.find_by_supplier_id(supplier_1.id).update_column(:cost, 5) 23 | v.product.add_supplier! supplier_2 24 | v.reload.supplier_variants.find_by_supplier_id(supplier_2.id).update_column(:cost, 4) 25 | v 26 | } 27 | let(:variant_3) { 28 | v = create(:variant) 29 | v.product.add_supplier! supplier_1 30 | v.product.add_supplier! supplier_2 31 | v.reload 32 | } 33 | let(:variant_4) { create(:variant) } 34 | 35 | let(:variants){ 36 | [variant_1, variant_2, variant_3, variant_4] 37 | } 38 | 39 | let(:packer) { build(:stock_packer) } 40 | 41 | subject { DropShip.new(packer) } 42 | 43 | it 'splits packages for drop ship' do 44 | package = Package.new(packer.stock_location) 45 | package = Package.new(packer.stock_location) 46 | 4.times { |i| package.add build(:inventory_unit, variant: variants[i]) } 47 | 48 | packages = subject.split([package]) 49 | packages.count.should eq 3 50 | 51 | expect(packages[0].stock_location).to eq(packer.stock_location) 52 | expect(packages[0].contents.count).to eq(1) 53 | expect(packages[1].stock_location).to eq(supplier_1.stock_locations.first) 54 | expect(packages[1].contents.count).to eq(2) 55 | expect(packages[2].stock_location).to eq(supplier_2.stock_locations.first) 56 | expect(packages[2].contents.count).to eq(1) 57 | end 58 | 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /app/controllers/spree/admin/shipments_controller.rb: -------------------------------------------------------------------------------- 1 | module Spree 2 | module Admin 3 | class ShipmentsController < Spree::Admin::ResourceController 4 | def index 5 | params[:q] ||= {} 6 | # params[:q][:completed_at_null] ||= '1' 7 | # @show_only_incomplete = params[:q][:completed_at_null].present? 8 | # params[:q][:s] ||= @show_only_incomplete ? 'created_at desc' : 'completed_at desc' 9 | 10 | # As date params are deleted if @show_only_incomplete, store 11 | # the original date so we can restore them into the params 12 | # after the search 13 | created_at_gt = params[:q][:created_at_gt] 14 | created_at_lt = params[:q][:created_at_lt] 15 | 16 | if params[:q][:created_at_gt].present? 17 | params[:q][:created_at_gt] = begin 18 | Time.zone.parse(params[:q][:created_at_gt]).beginning_of_day 19 | rescue 20 | '' 21 | end 22 | end 23 | 24 | if params[:q][:created_at_lt].present? 25 | params[:q][:created_at_lt] = begin 26 | Time.zone.parse(params[:q][:created_at_lt]).end_of_day 27 | rescue 28 | '' 29 | end 30 | end 31 | 32 | @search = Spree::Shipment.accessible_by(current_ability, :index).ransack(params[:q]) 33 | @shipments = @search.result 34 | .page(params[:page]) 35 | .per(per_page) 36 | 37 | # Restore dates 38 | params[:q][:created_at_gt] = created_at_gt 39 | params[:q][:created_at_lt] = created_at_lt 40 | end 41 | 42 | private 43 | 44 | def per_page 45 | params[:per_page] || admin_orders_per_page || orders_per_page 46 | end 47 | 48 | def admin_orders_per_page 49 | Spree::Config[:admin_orders_per_page] if Spree::Config.has_preference?(:admin_orders_per_page) 50 | end 51 | 52 | def orders_per_page 53 | Spree::Config[:orders_per_page] if Spree::Config.has_preference?(:orders_per_page) 54 | end 55 | 56 | def find_resource 57 | if parent_data.present? 58 | parent.send(controller_name).find_by!(number: params[:id]) 59 | else 60 | model_class.find_by!(number: params[:id]) 61 | end 62 | end 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /lib/tasks/spree_sample.rake: -------------------------------------------------------------------------------- 1 | namespace :spree_sample do 2 | desc 'Create sample drop ship orders' 3 | task drop_ship_orders: :environment do 4 | if Spree::Order.count == 0 5 | puts 'Please run `rake spree_sample:load` first to create products and orders' 6 | exit 7 | end 8 | 9 | if Spree::Supplier.count == 0 10 | puts 'Please run `rake spree_sample:suppliers` first to create suppliers' 11 | exit 12 | end 13 | 14 | count = 0 15 | @orders = Spree::Order.complete.includes(:line_items).all 16 | @suppliers = Spree::Supplier.all 17 | 18 | puts 'Linking existing line items to suppliers' 19 | Spree::LineItem.all.each do |li| 20 | print '*' if li.product.add_supplier!(@suppliers.sample.id) && li.save 21 | end 22 | puts 23 | 24 | puts 'Creating drop ship orders for existing orders' 25 | Spree::Order.all.each do |order| 26 | print '*' if order.finalize! 27 | end 28 | puts 29 | end 30 | 31 | desc 'Create sample suppliers and randomly link to products' 32 | task suppliers: :environment do 33 | old_send_value = SpreeDropShip::Config[:send_supplier_email] 34 | SpreeDropShip::Config[:send_supplier_email] = false 35 | 36 | @usa = Spree::Country.find_by(iso: 'US') 37 | @ca = @usa.states.find_by(abbr: 'CA') 38 | 39 | count = Spree::Supplier.count 40 | puts 'Creating Suppliers...' 41 | 5.times do |i| 42 | name = "Supplier #{count + i + 1}" 43 | supplier = Spree::Supplier.new(name: name, 44 | email: "#{name.parameterize}@example.com", 45 | url: 'http://example.com') 46 | supplier.build_address(firstname: name, lastname: name, 47 | address1: '100 State St', 48 | city: 'Santa Barbara', zipcode: '93101', 49 | state_id: @ca.id, country_id: @usa.id, 50 | phone: '1234567890') 51 | print '*' if supplier.save 52 | end 53 | puts 54 | puts "#{Spree::Supplier.count - count} suppliers created" 55 | 56 | puts 'Randomly linking Products & Suppliers...' 57 | 58 | @supplier_ids = Spree::Supplier.pluck(:id).shuffle 59 | @products = Spree::Product.all 60 | count = 0 61 | 62 | @products.each do |product| 63 | product.add_supplier! Spree::Supplier.find(@supplier_ids[rand(@supplier_ids.length)]) 64 | product.save 65 | count += 1 66 | print '*' 67 | end 68 | puts 69 | puts "#{count} products linked." 70 | SpreeDropShip::Config[:send_supplier_email] = old_send_value 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /spec/features/admin/shipments_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Admin - Shipments', js: true do 4 | 5 | context 'as Supplier' do 6 | 7 | let!(:order) { build(:order_ready_for_drop_ship, state: 'complete', completed_at: "2011-02-01 12:36:15", number: "R100") } 8 | let!(:supplier) { create(:supplier) } 9 | 10 | let!(:product) { 11 | p = create(:product, name: 'spree t-shirt', price: 20.00) 12 | p.add_supplier! supplier.id 13 | p 14 | } 15 | 16 | let!(:shipment) { create(:shipment, order: order, stock_location: supplier.stock_locations.first) } 17 | let!(:shipping_method) { create(:shipping_method, name: "Default") } 18 | 19 | before do 20 | # Adjust qoh so shipment will be ready 21 | shipment.stock_location.stock_items.where(variant_id: product.master.id).first.adjust_count_on_hand(10) 22 | # Add product and update shipment 23 | order.contents.add(product.master, 2) 24 | shipment.refresh_rates 25 | shipment.update!(order) 26 | shipment.update_amounts 27 | 28 | # TODO this is a hack until capture_on_dispatch finished https://github.com/spree/spree/issues/4727 29 | shipment.update_attribute :state, 'ready' 30 | 31 | user = create(:user, supplier: supplier) 32 | user.generate_spree_api_key! 33 | login_user user 34 | 35 | visit spree.edit_admin_shipment_path(shipment) 36 | end 37 | 38 | context 'edit page' do 39 | 40 | it "can add tracking information" do 41 | within '.table tr.show-tracking' do 42 | click_icon :edit 43 | end 44 | within '.table tr.edit-tracking' do 45 | fill_in "tracking", with: "FOOBAR" 46 | click_icon :save 47 | end 48 | wait_for_ajax 49 | within '.table tr.show-tracking' do 50 | page.should have_content("Tracking: FOOBAR") 51 | end 52 | end 53 | 54 | it "can change the shipping method" do 55 | within(".table tr.show-method") do 56 | click_icon :edit 57 | end 58 | select2 "Default", from: "Shipping Method" 59 | click_icon :save 60 | wait_for_ajax 61 | 62 | page.should have_content("Default $0.00") 63 | end 64 | 65 | it "can ship a completed order" do 66 | click_on "Ship" 67 | wait_for_ajax 68 | 69 | page.should have_content("shipped package") 70 | order.reload.shipment_state.should == "shipped" 71 | end 72 | end 73 | 74 | it 'should render unauthorized visiting another suppliers shipment' do 75 | visit spree.edit_admin_shipment_path(create(:shipment)) 76 | page.should have_content('Authorization Failure') 77 | end 78 | end 79 | 80 | end 81 | -------------------------------------------------------------------------------- /app/views/spree/supplier_mailer/welcome.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 51 | 52 | 53 |
5 | 6 | 7 | 8 | 11 | 12 | 13 | 20 | 21 | 22 | 40 | 41 | 42 | 47 | 48 | 49 |
9 |

<%= link_to Spree::Store.current.name, spree.root_url %>

10 |
14 |

<%= Spree.t('supplier_mailer.welcome.hello', name: @supplier.name ) %>

15 |

16 | <%= Spree.t('supplier_mailer.welcome.manage_your_account' ) %> 17 | <%= link_to Spree.t('supplier_mailer.welcome.logging_into_your_account'), spree.login_url, target: '_blank' %> 18 |

19 |
23 |

<%= Spree.t('supplier_mailer.welcome.please_verify_your_information') %>

24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 36 | 37 | 38 |
<%= Spree.t('supplier_mailer.welcome.supplier_information') %>
33 | <%= Spree.t(:name) %>: <%= @supplier.name %>
34 | <%= Spree.t(:email) %>: <%= mail_to @supplier.email %>
35 |
39 |
43 |
44 |

<%= raw Spree.t('supplier_mailer.welcome.thank_you_again', name: Spree::Store.current.name) %>

45 |
46 |
50 |
54 | -------------------------------------------------------------------------------- /lib/spree_drop_ship/factories.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :order_for_drop_ship, parent: :order do 3 | bill_address 4 | ship_address 5 | 6 | transient do 7 | line_items_count 5 8 | end 9 | 10 | after(:create) do |order, evaluator| 11 | supplier = create(:supplier) 12 | product = create(:product) 13 | product.add_supplier! supplier 14 | # product.stock_items.where(variant_id: product.master.id).first.adjust_count_on_hand(10) 15 | 16 | product_2 = create(:product) 17 | product_2.add_supplier! create(:supplier) 18 | 19 | create_list(:line_item, evaluator.line_items_count, 20 | order: order, 21 | variant: product_2.master) 22 | order.line_items.reload 23 | 24 | create(:shipment, order: order, stock_location: supplier.stock_locations.first) 25 | order.shipments.reload 26 | 27 | order.update_with_updater! 28 | end 29 | 30 | factory :completed_order_for_drop_ship_with_totals do 31 | state 'complete' 32 | 33 | after(:create) do |order| 34 | order.refresh_shipment_rates 35 | order.update_column(:completed_at, Time.now) 36 | end 37 | 38 | factory :order_ready_for_drop_ship do 39 | payment_state 'paid' 40 | shipment_state 'ready' 41 | 42 | after(:create) do |order| 43 | create(:payment, amount: order.total, order: order, state: 'completed') 44 | order.shipments.each do |shipment| 45 | shipment.inventory_units.each { |u| u.update_column('state', 'on_hand') } 46 | shipment.update_column('state', 'ready') 47 | end 48 | order.reload 49 | end 50 | 51 | factory :shipped_order_for_drop_ship do 52 | after(:create) do |order| 53 | order.shipments.each do |shipment| 54 | shipment.inventory_units.each { |u| u.update_column('state', 'shipped') } 55 | shipment.update_column('state', 'shipped') 56 | end 57 | order.reload 58 | end 59 | end 60 | end 61 | end 62 | end 63 | 64 | factory :supplier, class: Spree::Supplier do 65 | sequence(:name) { |i| "Big Store #{i}" } 66 | email { FFaker::Internet.email } 67 | url 'http://example.com' 68 | address 69 | # Creating a stock location with a factory instead of letting the model handle it 70 | # so that we can run tests with backorderable defaulting to true. 71 | before :create do |supplier| 72 | supplier.stock_locations << build(:stock_location, name: supplier.name, supplier: supplier) 73 | end 74 | 75 | factory :supplier_with_commission do 76 | commission_flat_rate 0.5 77 | commission_percentage 10 78 | end 79 | end 80 | 81 | factory :supplier_user, parent: :user do 82 | supplier 83 | end 84 | 85 | factory :variant_with_supplier, parent: :variant do 86 | after :create do |variant| 87 | variant.product.add_supplier! create(:supplier) 88 | end 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /app/views/spree/admin/suppliers/index.html.erb: -------------------------------------------------------------------------------- 1 | <%# Updated icon %> 2 | <% content_for :page_actions do %> 3 | <%= button_link_to Spree.t('new_supplier'), new_object_url, icon: 'add', class: 'btn-success' %> 4 | <% end %> 5 | 6 | <% content_for :page_title do %> 7 | <%= Spree.t(:listing_suppliers) %> 8 | <% end %> 9 | 10 | <% content_for :table_filter_title do %> 11 | <%= Spree.t(:search) %> 12 | <% end %> 13 | 14 | <% content_for :table_filter do %> 15 |
16 | <%= search_form_for [:admin, @search] do |f| %> 17 |
18 | <%= f.label :name_cont, Spree.t(:name) %> 19 | <%= f.text_field :name_cont, :size => 15, class: "form-control" %> 20 |
21 |
22 |
23 | <%= button Spree.t(:search), 'search' %> 24 |
25 | <% end %> 26 |
27 | <% end %> 28 | 29 | <%# This is a new paginate! %> 30 | <%= render :partial => 'spree/admin/shared/index_table_options', :locals => { :collection => @collection } %> 31 | 32 | <% if @collection.any? %> 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | <%# Updated classes! %> 45 | <%- @collection.each do |supplier| %> 46 | id="<%= spree_dom_id supplier %>" data-hook="admin_suppliers_index_rows" class="<%= cycle('odd', 'even') %>"> 47 | 48 | 49 | 50 | 51 | 52 | 58 | 59 | <% end %> 60 | 61 |
<%= sort_link @search, :name, Spree::Supplier.human_attribute_name(:name) %><%= sort_link @search, :email, Spree::Supplier.human_attribute_name(:email) %><%= Spree.t(:users) %><%= sort_link @search, :active, Spree::Supplier.human_attribute_name(:active) %>
<%= link_to supplier.name, edit_object_url(supplier) %><%= mail_to supplier.email %><%= supplier.users.pluck(:email).join(', ') %><%= supplier.active %> 53 | <% unless supplier.deleted? %> 54 | <%= link_to_edit supplier, :no_text => true, :class => 'edit' %> 55 | <%= link_to_delete supplier, :no_text => true %> 56 | <% end %> 57 |
62 | <% else %> 63 | <%# New alert with link to add new supplier %> 64 |
65 | <%= Spree.t(:no_resource_found, resource: Spree::Supplier.model_name.human(count: :many)) %>, 66 | <%= link_to Spree.t(:add_one), spree.new_admin_supplier_path %>! 67 |
68 | <% end %> 69 | 70 | <%# This is a new paginate! %> 71 | <%= render :partial => 'spree/admin/shared/index_table_options', :locals => { :collection => @collection } %> 72 | -------------------------------------------------------------------------------- /spec/features/admin/stock_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | feature 'Admin - Product Stock Management', js: true do 4 | 5 | before do 6 | @user = create(:supplier_user) 7 | @supplier1 = @user.supplier 8 | @supplier2 = create(:supplier) 9 | @product = create :product 10 | @product.add_supplier! @supplier1 11 | end 12 | 13 | context 'as Admin' do 14 | 15 | scenario 'should display all existing stock item locations' do 16 | login_user create(:admin_user) 17 | visit spree.stock_admin_product_path(@product) 18 | 19 | within '.stock_location_info' do 20 | page.should have_content(@supplier1.name) 21 | # Stock item doesn't exist 22 | page.should_not have_content(@supplier2.name) 23 | end 24 | end 25 | 26 | end 27 | 28 | context 'as Supplier' do 29 | 30 | before(:each) do 31 | login_user @user 32 | visit '/admin/products' 33 | click_link "Stock Locations" 34 | end 35 | 36 | scenario 'should only display suppliers stock locations' do 37 | visit spree.stock_admin_product_path(@product) 38 | 39 | within '.stock_location_info' do 40 | page.should have_content(@supplier1.name) 41 | page.should_not have_content(@supplier2.name) 42 | end 43 | end 44 | 45 | scenario "can create a new stock location" do 46 | visit spree.new_admin_stock_location_path 47 | fill_in "Name", with: "London" 48 | check "Active" 49 | click_button "Create" 50 | 51 | page.should have_content("successfully created") 52 | page.should have_content("London") 53 | end 54 | 55 | scenario "can delete an existing stock location", js: true do 56 | stock_location = create(:stock_location, supplier: @user.supplier) 57 | visit current_path 58 | 59 | find('#listing_stock_locations').should have_content("NY Warehouse") 60 | within("#spree_stock_location_#{stock_location.id}") { click_icon :delete } 61 | page.driver.browser.switch_to.alert.accept 62 | # Wait for API request to complete. 63 | sleep(1) 64 | 65 | visit current_path 66 | 67 | find('#listing_stock_locations').should_not have_content("NY Warehouse") 68 | end 69 | 70 | scenario "can update an existing stock location" do 71 | create(:stock_location, supplier: @user.supplier) 72 | visit current_path 73 | 74 | page.should have_content("Big Store") 75 | 76 | within_row(1) { click_icon :edit } 77 | fill_in "Name", with: "London" 78 | click_button "Update" 79 | 80 | page.should have_content("successfully updated") 81 | page.should have_content("London") 82 | end 83 | 84 | scenario "can deactivate an existing stock location" do 85 | create(:stock_location, supplier: @user.supplier) 86 | visit current_path 87 | 88 | page.should have_content("Big Store") 89 | 90 | within_row(1) { click_icon :edit } 91 | uncheck "Active" 92 | click_button "Update" 93 | 94 | find('#listing_stock_locations').should have_content("Inactive") 95 | end 96 | 97 | end 98 | 99 | end 100 | -------------------------------------------------------------------------------- /app/views/spree/admin/suppliers/_form.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 | <%= Spree.t('contact_address') %> 4 | <%= form.fields_for :address do |contact_form| %> 5 | <%= render :partial => 'spree/admin/shared/address_form', :locals => { :f => contact_form, :name => Spree.t(:contact_address), :type => "shipping" } %> 6 | <% end %> 7 |
8 |
9 | 10 | <%# added bootstrap classes for 'field_container' and '' %> 11 |
12 |
13 | <%= Spree.t('supplier_details') %> 14 | <%= form.field_container :name, class: ['form-group'] do %> 15 | <%= form.label :name, Spree::Supplier.human_attribute_name(:name) %>:
16 | <%= form.text_field :name, :class => 'form-control' %> 17 | <% end %> 18 | <%= form.field_container :email, class: ['form-group'] do %> 19 | <%= form.label :email, Spree::Supplier.human_attribute_name(:email) %>:
20 | <%= form.email_field :email, :class => 'form-control' %> 21 | <%# email validation %> 22 | <%= error_message_on :user, :email %> 23 | <% end %> 24 | <%= form.field_container :url, class: ['form-group'] do %> 25 | <%= form.label :url, Spree::Supplier.human_attribute_name(:url) %>:
26 | <%= form.text_field :url, :class => 'form-control' %> 27 | <% end %> 28 | <%= form.field_container :tax_id, class: ['form-group'] do %> 29 | <%= form.label :tax_id, Spree::Supplier.human_attribute_name(:tax_id) %>:
30 | <%= form.text_field :tax_id, :class => 'form-control' %> 31 | <% end %> 32 | <% if spree_current_user.admin? %> 33 | <%= form.field_container :commission_flat_rate, class: ['form-group'] do %> 34 | <%= form.label :commission_flat_rate, Spree::Supplier.human_attribute_name(:commission_flat_rate) %>:
35 | <%= form.text_field :commission_flat_rate, :class => 'form-control' %> 36 | <% end %> 37 | <%= form.field_container :commission_percentage , class: ['form-group'] do %> 38 | <%= form.label :commission_percentage, Spree::Supplier.human_attribute_name(:commission_percentage) %>:
39 | <%= form.text_field :commission_percentage, :class => 'form-control' %> 40 | <% end %> 41 | <%= form.field_container :users, class: ['form-group'] do %> 42 | <%= form.label :users %>
43 | <%= hidden_field_tag 'supplier[user_ids_string]', form.object.user_ids.join(','), class: 'user_picker' %> 44 | <% end %> 45 | <%= form.field_container :active, class: ['form-group'] do %> 46 |
47 | <%= label_tag :active do %> 48 | <%= form.check_box :active %> 49 | <%= Spree::Supplier.human_attribute_name(:active) %> 50 | <% end %> 51 |
52 | <% end %> 53 | <% end %> 54 | <%# This moved to _edit(new)_resource_links.html.erb %> 55 | 56 | <%# if form.object.new_record? %> 57 | <%#= button Spree.t('create') %> 58 | <%# else %> 59 | <%#= button Spree.t('update') %> 60 | <%# end %> 61 | <%#= Spree.t('or') %> 62 | <%# if can? :index, Spree::Supplier %> 63 | <%#= link_to Spree.t('cancel'), collection_url %> 64 | <%# else %> 65 | <%#= link_to Spree.t('cancel'), spree.account_path %> 66 | <%# end %> 67 | 68 |
69 |
70 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | activerecord: 3 | attributes: 4 | spree/supplier: 5 | commission_flat_rate: Commission Flat Rate 6 | commission_percentage: Commission Percentage 7 | name: Name 8 | tax_id: TAX ID 9 | url: URL 10 | errors: 11 | models: 12 | spree/supplier: 13 | attributes: 14 | url: 15 | invalid: is invalid. Please be sure to include include 'http://' 16 | not_responding: 'is invalid, not responding or redirecting to another location' 17 | spree: 18 | admin: 19 | drop_ship_settings: 20 | update: 21 | success: Drop ship settings successfully updated. 22 | orders: 23 | edit: 24 | approve_drop_ship_orders: Approve Drop Ship Orders 25 | resend_drop_ship_orders: Resend Drop Ship Orders 26 | drop_ship_order_approval_confirmation: This will send an email notification to each supplier. Continue? 27 | show: 28 | approve_drop_ship_orders: Approve Drop Ship Orders 29 | resend_drop_ship_orders: Resend Drop Ship Orders 30 | drop_ship_order_approval_confirmation: This will send an email notification to each supplier. Continue? 31 | back_to_orders_list: Back To Orders List 32 | back_to_suppliers: Back To Suppliers 33 | business: Business 34 | cancel: Cancel 35 | company_info: Company Info 36 | confirm_order: Confirm Order 37 | contact_address: Contact Address 38 | drop_ship: Drop Ship 39 | drop_ship_order_mailer: 40 | supplier_order: 41 | hello: "Hello %{name}," 42 | subject: "%{name} Drop Shipment #%{number}" 43 | thank_you_again: "Thank you again for your business, %{name}" 44 | drop_ship_order_number: "Drop Ship Order #%{number}" 45 | drop_ship_orders: Drop Ship Orders 46 | drop_ship_settings: Drop Ship Settings 47 | editing_supplier: Editing Supplier 48 | individual: Individual 49 | listing_suppliers: Listing Suppliers 50 | manage: Manage 51 | must_be_logged_in: 'Must Be Logged In' 52 | name: Name 53 | new_supplier: New Supplier 54 | or: or 55 | order_state: 56 | active: Active 57 | completed: Completed 58 | confirmed: Confirmed 59 | delivered: Delivered 60 | resend_order_to_supplier: Resend Order To Supplier 61 | save: Save 62 | search: Search 63 | send_order_to_supplier: Send Order To Supplier 64 | shared: 65 | unauthorized: 66 | explained: You are not authorized to access this page. 67 | unauthorized: Unauthorized 68 | show_only_incomplete_orders: Show Only Incomplete Orders 69 | signup: Sign Up 70 | signup_to_become_a_supplier: 'Sign Up To Become A Supplier' 71 | supplier: Supplier 72 | supplier_details: Supplier Details 73 | supplier_info: Supplier Info 74 | supplier_information: Supplier Information 75 | supplier_mailer: 76 | welcome: 77 | hello: "Hello %{name}," 78 | logging_into_your_account: logging into your account. 79 | manage_your_account: You may now manage your profile and inventory by 80 | subject: 'Thank you for signing up. Please verify your information.' 81 | thank_you_again: "Thank you again for your business, %{name}" 82 | thank_you_for_signing_up: Thank you for signing up to be a drop ship supplier. 83 | supplier_registration: 84 | already_signed_up: "You've already signed up to become a supplier." 85 | create: 86 | invalid_password: Invalid password please sign in or sign up. 87 | success: Thank you for signing up! 88 | supplier_signup: Supplier Sign Up 89 | suppliers: Suppliers 90 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'coveralls' 2 | Coveralls.wear! 3 | 4 | # Configure Rails Environment 5 | ENV['RAILS_ENV'] = 'test' 6 | 7 | require File.expand_path('../dummy/config/environment.rb', __FILE__) 8 | 9 | require 'rspec/rails' 10 | require 'database_cleaner' 11 | require 'factory_bot' 12 | require 'selenium/webdriver' 13 | FactoryBot.find_definitions 14 | require 'ffaker' 15 | require 'shoulda-matchers' 16 | 17 | # Requires supporting ruby files with custom matchers and macros, etc, 18 | # in spec/support/ and its subdirectories. 19 | Dir[File.join(File.dirname(__FILE__), 'support/**/*.rb')].each { |f| require f } 20 | 21 | require 'spree/testing_support/authorization_helpers' 22 | require 'spree/testing_support/capybara_ext' 23 | require 'spree/testing_support/controller_requests' 24 | require 'spree/testing_support/factories' 25 | require 'spree/testing_support/preferences' 26 | require 'spree/testing_support/url_helpers' 27 | 28 | require 'spree_drop_ship/factories' 29 | 30 | capabilities = Selenium::WebDriver::Remote::Capabilities.chrome( 31 | 'chromeOptions' => { 32 | 'args' => ['--headless', '--disable-gpu'] 33 | } 34 | ) 35 | 36 | Capybara.register_driver :chrome do |app| 37 | Capybara::Selenium::Driver.new(app, browser: :chrome, desired_capabilities: capabilities) 38 | end 39 | 40 | Capybara.javascript_driver = :chrome 41 | 42 | Shoulda::Matchers.configure do |config| 43 | config.integrate do |with| 44 | with.test_framework :rspec 45 | with.library :active_model 46 | with.library :active_record 47 | end 48 | end 49 | 50 | RSpec.configure do |config| 51 | config.include FactoryBot::Syntax::Methods 52 | config.include IntegrationHelpers 53 | config.include Spree::TestingSupport::Preferences 54 | 55 | # == URL Helpers 56 | # 57 | # Allows access to Spree's routes in specs: 58 | # 59 | # visit spree.admin_path 60 | # current_path.should eql(spree.products_path) 61 | config.include Spree::TestingSupport::UrlHelpers 62 | config.include Spree::TestingSupport::ControllerRequests, type: :controller 63 | 64 | # Capybara javascript drivers require transactional fixtures set to false, and we use DatabaseCleaner 65 | # to cleanup after each test instead. Without transactional fixtures set to false the records created 66 | # to setup a test will be unavailable to the browser, which runs under a seperate server instance. 67 | config.use_transactional_fixtures = false 68 | 69 | # Ensure Suite is set to use transactions for speed. 70 | config.before :suite do 71 | DatabaseCleaner.strategy = :transaction 72 | DatabaseCleaner.clean_with :truncation 73 | end 74 | 75 | # Before each spec check if it is a Javascript test and switch between using database transactions or not where necessary. 76 | config.before :each do 77 | DatabaseCleaner.strategy = RSpec.current_example.metadata[:js] ? :truncation : :transaction 78 | DatabaseCleaner.start 79 | reset_spree_preferences 80 | end 81 | 82 | # After each spec clean the database. 83 | config.after :each do 84 | DatabaseCleaner.clean 85 | end 86 | 87 | # If true, the base class of anonymous controllers will be inferred 88 | # automatically. This will be the default behavior in future versions of 89 | # rspec-rails. 90 | config.infer_base_class_for_anonymous_controllers = false 91 | 92 | config.infer_spec_type_from_file_location! 93 | # Run specs in random order to surface order dependencies. If you find an 94 | # order dependency and want to debug it, you can fix the order by providing 95 | # the seed, which is printed after each run. 96 | # --seed 1234 97 | # config.order = "random" 98 | config.color = true 99 | config.infer_spec_type_from_file_location! 100 | end 101 | -------------------------------------------------------------------------------- /app/models/spree/supplier.rb: -------------------------------------------------------------------------------- 1 | class Spree::Supplier < Spree::Base 2 | extend FriendlyId 3 | friendly_id :name, use: :slugged 4 | 5 | attr_accessor :password, :password_confirmation 6 | 7 | #========================================== 8 | # Associations 9 | 10 | belongs_to :address, class_name: 'Spree::Address' 11 | accepts_nested_attributes_for :address 12 | 13 | if defined?(Ckeditor::Asset) 14 | has_many :ckeditor_pictures 15 | has_many :ckeditor_attachment_files 16 | end 17 | has_many :products, through: :variants 18 | has_many :stock_locations 19 | has_many :shipments, through: :stock_locations 20 | has_many :supplier_variants 21 | has_many :users, class_name: Spree.user_class.to_s 22 | has_many :variants, through: :supplier_variants 23 | 24 | #========================================== 25 | # Validations 26 | 27 | validates :commission_flat_rate, presence: true 28 | validates :commission_percentage, presence: true 29 | validates :email, presence: true, email: true, uniqueness: true 30 | validates :name, presence: true, uniqueness: true 31 | validates :url, format: { with: URI.regexp(%w[http https]), allow_blank: true } 32 | 33 | #========================================== 34 | # Callbacks 35 | 36 | after_create :assign_user 37 | after_create :create_stock_location 38 | after_create :send_welcome, if: -> { SpreeDropShip::Config[:send_supplier_email] } 39 | before_create :set_commission 40 | before_validation :check_url 41 | 42 | #========================================== 43 | # Instance Methods 44 | scope :active, -> { where(active: true) } 45 | 46 | def deleted? 47 | deleted_at.present? 48 | end 49 | 50 | def user_ids_string 51 | user_ids.join(',') 52 | end 53 | 54 | def user_ids_string=(s) 55 | self.user_ids = s.to_s.split(',').map(&:strip) 56 | end 57 | 58 | # Retreive the stock locations that has available 59 | # stock items of the given variant 60 | def stock_locations_with_available_stock_items(variant) 61 | stock_locations.select { |sl| sl.available?(variant) } 62 | end 63 | 64 | #========================================== 65 | # Protected Methods 66 | 67 | protected 68 | 69 | def assign_user 70 | if users.empty? 71 | if user = Spree.user_class.find_by(email: email) 72 | users << user 73 | save 74 | end 75 | end 76 | end 77 | 78 | def check_url 79 | unless url.blank? || url =~URI.regexp(%w[http https]) 80 | self.url = "http://#{url}" 81 | end 82 | end 83 | 84 | def create_stock_location 85 | if stock_locations.empty? 86 | location = stock_locations.build( 87 | active: true, 88 | country_id: address.try(:country_id), 89 | name: name, 90 | state_id: address.try(:state_id) 91 | ) 92 | # It's important location is always created. Some apps add validations that shouldn't break this. 93 | location.save validate: false 94 | end 95 | end 96 | 97 | def send_welcome 98 | Spree::SupplierMailer.welcome(id).deliver_later! 99 | # Specs raise error for not being able to set default_url_options[:host] 100 | rescue => ex # Errno::ECONNREFUSED => ex 101 | Rails.logger.error ex.message 102 | Rails.logger.error ex.backtrace.join("\n") 103 | return true # always return true so that failed email doesn't crash app. 104 | end 105 | 106 | def set_commission 107 | unless changes.key?(:commission_flat_rate) 108 | self.commission_flat_rate = SpreeDropShip::Config[:default_commission_flat_rate] 109 | end 110 | unless changes.key?(:commission_percentage) 111 | self.commission_percentage = SpreeDropShip::Config[:default_commission_percentage] 112 | end 113 | end 114 | end 115 | -------------------------------------------------------------------------------- /spec/models/spree/supplier_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Spree::Supplier do 4 | it { should belong_to(:address) } 5 | 6 | it { should have_many(:products).through(:variants) } 7 | it { should have_many(:stock_locations) } 8 | it { should have_many(:users) } 9 | it { should have_many(:variants).through(:supplier_variants) } 10 | 11 | it { should validate_presence_of(:email) } 12 | it { should validate_presence_of(:name) } 13 | 14 | it '#deleted?' do 15 | subject.deleted_at = nil 16 | subject.deleted_at?.should eql(false) 17 | subject.deleted_at = Time.now 18 | subject.deleted_at?.should eql(true) 19 | end 20 | 21 | context '#assign_user' do 22 | let(:supplier) { build(:supplier) } 23 | 24 | it 'with user' do 25 | Spree.user_class.should_not_receive :find_by_email 26 | supplier.email = 'test@test.com' 27 | supplier.users << create(:user) 28 | supplier.save 29 | end 30 | 31 | it 'with existing user email' do 32 | user = create(:user, email: 'test@test.com') 33 | Spree.user_class.should_receive(:find_by).with(email: user.email).and_return(user) 34 | supplier.email = user.email 35 | supplier.save 36 | supplier.reload.users.first.should eql(user) 37 | end 38 | 39 | end 40 | 41 | it '#create_stock_location' do 42 | Spree::StockLocation.count.should eql(0) 43 | supplier = create :supplier 44 | Spree::StockLocation.first.active.should be true 45 | Spree::StockLocation.first.country.should eql(supplier.address.country) 46 | Spree::StockLocation.first.supplier.should eql(supplier) 47 | end 48 | 49 | context '#send_welcome' do 50 | 51 | after do 52 | SpreeDropShip::Config[:send_supplier_email] = true 53 | end 54 | 55 | before do 56 | @instance = build(:supplier) 57 | @mail_message = double('Mail::Message') 58 | end 59 | 60 | context 'with Spree::DropShipConfig[:send_supplier_email] == false' do 61 | 62 | it 'should not send' do 63 | SpreeDropShip::Config[:send_supplier_email] = false 64 | expect { 65 | Spree::SupplierMailer.should_not_receive(:welcome).with(an_instance_of(Integer)) 66 | } 67 | @instance.save 68 | end 69 | 70 | end 71 | 72 | context 'with Spree::DropShipConfig[:send_supplier_email] == true' do 73 | 74 | it 'should send welcome email' do 75 | expect { 76 | Spree::SupplierMailer.should_receive(:welcome).with(an_instance_of(Integer)) 77 | } 78 | @instance.save 79 | end 80 | 81 | end 82 | 83 | end 84 | 85 | it '#set_commission' do 86 | SpreeDropShip::Config.set default_commission_flat_rate: 1 87 | SpreeDropShip::Config.set default_commission_percentage: 1 88 | supplier = create :supplier 89 | SpreeDropShip::Config.set default_commission_flat_rate: 0 90 | SpreeDropShip::Config.set default_commission_percentage: 0 91 | # Default configuration is 0.0 for each. 92 | supplier.commission_flat_rate.to_f.should eql(1.0) 93 | supplier.commission_percentage.to_f.should eql(1.0) 94 | # With custom commission applied. 95 | supplier = create :supplier, commission_flat_rate: 123, commission_percentage: 25 96 | supplier.commission_flat_rate.should eql(123.0) 97 | supplier.commission_percentage.should eql(25.0) 98 | end 99 | 100 | describe '#shipments' do 101 | let!(:supplier) { create(:supplier) } 102 | 103 | it 'should return shipments for suppliers stock locations' do 104 | stock_location_1 = supplier.stock_locations.first 105 | stock_location_2 = create(:stock_location, supplier: supplier) 106 | shipment_1 = create(:shipment) 107 | shipment_2 = create(:shipment, stock_location: stock_location_1) 108 | shipment_3 = create(:shipment) 109 | shipment_4 = create(:shipment, stock_location: stock_location_2) 110 | shipment_5 = create(:shipment) 111 | shipment_6 = create(:shipment, stock_location: stock_location_1) 112 | 113 | expect(supplier.shipments).to match_array([shipment_2, shipment_4, shipment_6]) 114 | end 115 | end 116 | end 117 | -------------------------------------------------------------------------------- /app/views/spree/admin/shipments/index.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :page_title do %> 2 | <%= Spree.t(:listing_shipments) %> 3 | <% end %> 4 | 5 | <% content_for :table_filter_title do %> 6 | <%= Spree.t(:search) %> 7 | <% end %> 8 | 9 | <%# Update search form with new markup %> 10 | <% content_for :table_filter do %> 11 |
12 | <%= search_form_for [:admin, @search] do |f| %> 13 |
14 |
15 |
16 | <%= label_tag :q_created_at_gt, Spree.t(:date_range) %> 17 |
18 |
19 | <%= f.text_field :created_at_gt, :class => 'datepicker datepicker-from form-control', :value => params[:q][:created_at_gt], :placeholder => Spree.t(:start) %> 20 |
21 |
22 | <%= f.text_field :created_at_lt, :class => 'datepicker datepicker-to form-control', :value => params[:q][:created_at_lt], :placeholder => Spree.t(:stop) %> 23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | <%= label_tag nil, Spree.t(:shipment_number) %> 32 | <%= f.text_field :number_cont , class: 'form-control js-quick-search-target'%> 33 |
34 |
35 | 36 |
37 | <%= button Spree.t(:filter_results), 'search' %> 38 |
39 | <% end %> 40 |
41 | <% end %> 42 | 43 | <%# This is a new paginate! %> 44 | <%= render :partial => 'spree/admin/shared/index_table_options', :locals => { :collection => @shipments } %> 45 | 46 | <% unless @shipments.empty? %> 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | <% @shipments.each do |shipment| %> 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 75 | 76 | <% end %> 77 | 78 |
<%= sort_link @search, :created_at, Spree::Shipment.human_attribute_name(:created_at) %><%= sort_link @search, :number, Spree::Shipment.human_attribute_name(:number) %><%= sort_link @search, :state, Spree::Shipment.human_attribute_name(:state) %><%= sort_link @search, :item_cost, Spree::Shipment.human_attribute_name(:item_cost) %><%= sort_link @search, :shipping_total, Spree::Shipment.human_attribute_name(:cost) %><%= sort_link @search, :tax_total, Spree::Shipment.human_attribute_name(:tax_total) %><%= sort_link @search, :total, Spree::Shipment.human_attribute_name(:final_price) %><%= sort_link @search, :supplier_commission, Spree::Shipment.human_attribute_name(:supplier_commission) %>
<%= l shipment.created_at.to_date %><%= link_to shipment.number, spree.edit_admin_shipment_path(shipment) %><%= Spree.t("shipment_state.#{shipment.state.downcase}") %> <%= shipment.display_item_cost.to_html %><%= shipment.display_cost.to_html %><%= Spree::Money.new(shipment.tax_total, currency: shipment.currency).to_html %><%= Spree::Money.new(shipment.final_price_with_items, currency: shipment.currency).to_html %><%= Spree::Money.new(shipment.supplier_commission, currency: shipment.currency).to_html %> 73 | <%= link_to_edit_url spree.edit_admin_shipment_path(shipment), :title => "admin_edit_#{dom_id(shipment)}", :no_text => true %> 74 |
79 | <% else %> 80 | <%# New alert 'Not Found' with i18n %> 81 |
82 | <%= Spree.t(:no_resource_found, resource: Spree::Shipment.model_name.human(count: :many)) %>, 83 |
84 | <% end %> 85 | 86 | <%# This is a new paginate! %> 87 | <%= render :partial => 'spree/admin/shared/index_table_options', :locals => { :collection => @shipments } %> 88 | -------------------------------------------------------------------------------- /spec/features/admin/suppliers_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | feature 'Admin - Suppliers', js: true do 4 | 5 | before do 6 | country = create(:country, name: "United States") 7 | create(:state, name: "Vermont", country: country) 8 | @supplier = create :supplier 9 | end 10 | 11 | context 'as an Admin' do 12 | 13 | before do 14 | login_user create(:admin_user) 15 | visit spree.admin_path 16 | within '[data-hook=admin_tabs]' do 17 | click_link 'Suppliers' 18 | end 19 | page.should have_content('Listing Suppliers') 20 | end 21 | 22 | scenario 'should be able to create new supplier' do 23 | click_link 'New Supplier' 24 | check 'supplier_active' 25 | fill_in 'supplier[name]', with: 'Test Supplier' 26 | fill_in 'supplier[email]', with: 'spree@example.com' 27 | fill_in 'supplier[url]', with: 'http://www.test.com' 28 | fill_in 'supplier[commission_flat_rate]', with: '0' 29 | fill_in 'supplier[commission_percentage]', with: '0' 30 | fill_in 'supplier[address_attributes][firstname]', with: 'First' 31 | fill_in 'supplier[address_attributes][lastname]', with: 'Last' 32 | fill_in 'supplier[address_attributes][address1]', with: '1 Test Drive' 33 | fill_in 'supplier[address_attributes][city]', with: 'Test City' 34 | fill_in 'supplier[address_attributes][zipcode]', with: '55555' 35 | select2 'United States', from: 'Country' 36 | select2 'Vermont', from: 'State' 37 | fill_in 'supplier[address_attributes][phone]', with: '555-555-5555' 38 | click_button 'Create' 39 | page.should have_content('Supplier "Test Supplier" has been successfully created!') 40 | end 41 | 42 | scenario 'should be able to delete supplier' do 43 | click_icon 'delete' 44 | page.driver.browser.switch_to.alert.accept 45 | within 'table' do 46 | page.should_not have_content(@supplier.name) 47 | end 48 | end 49 | 50 | scenario 'should be able to edit supplier' do 51 | click_icon 'edit' 52 | check 'supplier_active' 53 | fill_in 'supplier[name]', with: 'Test Supplier' 54 | fill_in 'supplier[email]', with: 'spree@example.com' 55 | fill_in 'supplier[url]', with: 'http://www.test.com' 56 | fill_in 'supplier[commission_flat_rate]', with: '0' 57 | fill_in 'supplier[commission_percentage]', with: '0' 58 | fill_in 'supplier[address_attributes][firstname]', with: 'First' 59 | fill_in 'supplier[address_attributes][lastname]', with: 'Last' 60 | fill_in 'supplier[address_attributes][address1]', with: '1 Test Drive' 61 | fill_in 'supplier[address_attributes][city]', with: 'Test City' 62 | fill_in 'supplier[address_attributes][zipcode]', with: '55555' 63 | select2 'United States', from: 'Country' 64 | select2 'Vermont', from: 'State' 65 | fill_in 'supplier[address_attributes][phone]', with: '555-555-5555' 66 | click_button 'Update' 67 | page.should have_content('Supplier "Test Supplier" has been successfully updated!') 68 | end 69 | 70 | end 71 | 72 | context 'as a Supplier' do 73 | before do 74 | @user = create(:supplier_user) 75 | login_user @user 76 | visit spree.edit_admin_supplier_path(@user.supplier) 77 | end 78 | 79 | scenario 'should only see tabs they have access to' do 80 | within '[data-hook=admin_tabs]' do 81 | page.should_not have_link('Overview') 82 | page.should have_link('Products') 83 | page.should_not have_link('Reports') 84 | page.should_not have_link('Configuration') 85 | page.should_not have_link('Promotions') 86 | page.should_not have_link('Suppliers') 87 | page.should have_link('Shipments') 88 | end 89 | end 90 | 91 | scenario 'should be able to update supplier' do 92 | fill_in 'supplier[name]', with: 'Test Supplier' 93 | fill_in 'supplier[email]', with: @user.email 94 | fill_in 'supplier[url]', with: 'http://www.test.com' 95 | fill_in 'supplier[address_attributes][firstname]', with: 'First' 96 | fill_in 'supplier[address_attributes][lastname]', with: 'Last' 97 | fill_in 'supplier[address_attributes][address1]', with: '1 Test Drive' 98 | fill_in 'supplier[address_attributes][city]', with: 'Test City' 99 | fill_in 'supplier[address_attributes][zipcode]', with: '55555' 100 | select2 'United States', from: 'Country' 101 | select2 'Vermont', from: 'State' 102 | fill_in 'supplier[address_attributes][phone]', with: '555-555-5555' 103 | page.should_not have_css('#supplier_active') # cannot edit active 104 | page.should_not have_css('#supplier_featured') # cannot edit featured 105 | page.should_not have_css('#s2id_supplier_user_ids') # cannot edit assigned users 106 | page.should_not have_css('#supplier_commission_flat_rate') # cannot edit flat rate commission 107 | page.should_not have_css('#supplier_commission_percentage') # cannot edit comission percentage 108 | click_button 'Update' 109 | page.should have_content('Supplier "Test Supplier" has been successfully updated!') 110 | page.current_path.should eql(spree.edit_admin_supplier_path(@user.reload.supplier)) 111 | end 112 | 113 | end 114 | 115 | context 'as a User other than the suppliers' do 116 | 117 | scenario 'should be unauthorized' do 118 | supplier = create(:supplier) 119 | login_user create(:user) 120 | visit spree.edit_admin_supplier_path(supplier) 121 | page.should have_content('Authorization Failure') 122 | end 123 | 124 | end 125 | 126 | end 127 | -------------------------------------------------------------------------------- /config/locales/es.yml: -------------------------------------------------------------------------------- 1 | es: 2 | activerecord: 3 | attributes: 4 | spree/supplier: 5 | commission_flat_rate: Tarifa fija de comisión 6 | commission_percentage: Porcentaje de comisión 7 | name: Nombre 8 | tax_id: TAX ID 9 | url: URL 10 | address1: Dirección 11 | address2: Dirección (continuación) 12 | city: Ciudad 13 | commission: Comisión 14 | country: País 15 | email: Email 16 | state: Estado 17 | active: Activo 18 | url: URL 19 | zipcode: Código Postal(ZipCode) 20 | supplier_info: Información de proveedor 21 | supplier_signup: Registro de proveedor 22 | suppliers: Proveedores 23 | spree/shipment: 24 | created_at: Creado 25 | number: Número 26 | state: Estatus 27 | item_cost: Costo ítem 28 | cost: Costo 29 | total_tax: Total impuesto 30 | final_price: Precio final 31 | supplier_commission: Comisión Proveedor 32 | errors: 33 | models: 34 | spree/supplier: 35 | attributes: 36 | url: 37 | invalid: es inválido. Por favor asegúrece que 'http://' esté incluido 38 | not_responding: 'es inválido, no responde o ha redireccionado a otra url' 39 | drop_ship_order_number: "Drop Ship Orden #%{number}" 40 | drop_ship_orders: Drop Ship Ordenes 41 | drop_ship_settings: Drop Ship Configuración 42 | must_be_logged_in: 'Debe Estar Autenticado' 43 | signup: Registro 44 | signup_to_become_a_supplier: 'Autentíquese para ser un Proveedor' 45 | spree: 46 | admin: 47 | drop_ship_settings: 48 | update: 49 | success: Configuración Drop ship actualizada correctamente 50 | orders: 51 | drop_ship_order_order_header: 52 | drop_ship_order_ids: DSO IDs 53 | edit: 54 | approve_drop_ship_orders: Ordenes aprobadas por el proveedor 55 | resend_drop_ship_orders: Reenviar ordenes al proveedor 56 | drop_ship_order_approval_confirmation: Se le enviará una notificación por correo electrónico a cada proveedor. ¿Desea continuar? 57 | show: 58 | approve_drop_ship_orders: Ordene aprobadas por el proveedor 59 | resend_drop_ship_orders: Reenviar ordenes al proveedor 60 | drop_ship_order_approval_confirmation: Se le enviará una notificación por correo electrónico a cada proveedor. ¿Desea continuar? 61 | suppliers: 62 | edit: 63 | back_to_suppliers: Regresar a los proveedores 64 | new: 65 | new_supplier: Nuevo Proveedor 66 | form: 67 | supplier_details: Detalles del Proveedor 68 | supplier_address: Dirección del Proveedor 69 | drop_ship_orders: 70 | deliver: 71 | error: "La orden %{number} no pudo ser enviada al proveedor." 72 | success: "La orden %{number} fue enviada al proveedor." 73 | orders_sent: Todas las ordenes fueron enviadas a los proveedores 74 | orders_not_sent: Algunas o todas las ordenes no pudieron ser enviadas. Por favor contacte directamente al proveedor. 75 | back_to_orders_list: Regresar a las ordenes 76 | back_to_suppliers: Regresar a los proveedores 77 | business: Negocio 78 | cancel: Cancelar 79 | company_info: Informacion de la compañia 80 | confirm_order: Confirmar Orden 81 | contact_address: Dirección de contacto 82 | drop_ship: Drop Ship 83 | editing_supplier: Editar proveedor 84 | default_commission_flat_rate: Comision por defecto 85 | default_commission_percentage: Porcentaje de comision por defecto 86 | automatically_deliver_orders_to_supplier: Enviar orden automaticamente a los proveedores 87 | send_supplier_email: Enviar correo al proveedor 88 | individual: Individual 89 | listing_suppliers: Lista de proveedores 90 | manage: Gestionar 91 | name: Nombre 92 | or: ó 93 | suppliers: Proveedores 94 | listing_shipments: 'Lista de envíos' 95 | order_state: 96 | active: Activo 97 | completed: Completado 98 | confirmed: Confirmado 99 | delivered: Entregado 100 | resend_order_to_supplier: Reenviar orden al proveedor. 101 | save: Guardar 102 | search: Buscar 103 | send_order_to_supplier: Enviar orden al proveedor. 104 | show_only_incomplete_orders: Solo mostrar ordenes incompletas 105 | supplier: Proveedor 106 | supplier_information: Información del proveedor 107 | drop_ship_order_mailer: 108 | supplier_order: 109 | hello: "Hola %{name}," 110 | subject: "%{name} Orden al proveedor #%{number}" 111 | thank_you_again: "Gracias nuevamente por su negocio, %{name}" 112 | shared: 113 | unauthorized: 114 | explained: Usted no está autorizado a acceder a esta página. 115 | unauthorized: No autorizado 116 | supplier_mailer: 117 | welcome: 118 | hello: "Hola %{name}," 119 | check_your_status: Puede revisar el estado de su pedido por 120 | logging_into_your_account: Autentíquese. 121 | once_your_request_is_approved: Una vez que su pedido sea aprobado podrá gestionar su inventario. 122 | subject: 'Gracias por autenticarse. Por favor verifique la información.' 123 | thank_you_again: "Gracias nuevamente por su negocio, %{name}" 124 | thank_you_for_signing_up: Gracias por autenticarse para ser un proveedor. 125 | suppliers: 126 | already_signed_up: "Ya está autenticado para convertirse en un proveedor." 127 | create: 128 | success: Gracias por autenticarse! 129 | supplier_registration: 130 | already_signed_up: "Ya inicio sesión como proveedor." 131 | create: 132 | invalid_password: Clave incorrecta por favor inicie sesion. 133 | success: Gracias por registrarse! -------------------------------------------------------------------------------- /spec/features/admin/stock_management_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "Stock Management", js: true do 4 | 5 | before do 6 | @user = create(:supplier_user) 7 | login_user @user 8 | visit spree.admin_shipments_path 9 | end 10 | 11 | context "as supplier user" do 12 | 13 | context "given a product with a variant and a stock location" do 14 | before do 15 | @secondary = create(:stock_location, name: 'Secondary', supplier: @user.supplier) 16 | @product = create(:product, name: 'apache baseball cap', price: 10) 17 | @v = @product.variants.create!(attributes_for(:variant)) 18 | @user.supplier.reload.stock_locations.update_all(backorderable_default: false) # True database default is false. 19 | end 20 | 21 | context 'with single variant' do 22 | before do 23 | @product.add_supplier! @user.supplier 24 | @v.stock_items.first.update_column(:count_on_hand, 10) 25 | @secondary.stock_item(@v).destroy 26 | click_link "Products" 27 | within '#sidebar-product' do 28 | click_link 'Products' 29 | end 30 | click_link @product.name 31 | within '[data-hook=admin_product_tabs]' do 32 | click_link "Stock" 33 | end 34 | end 35 | 36 | it "should not show deleted stock_items" do 37 | within(:css, '.stock_location_info') do 38 | page.should have_content(@user.supplier.name) 39 | page.should_not have_content('Secondary') 40 | end 41 | end 42 | 43 | it "can toggle backorderable for a variant's stock item", js: true do 44 | backorderable = find(".stock_item_backorderable") 45 | backorderable.should_not be_checked 46 | 47 | backorderable.set(true) 48 | 49 | sleep(1) 50 | 51 | visit current_path 52 | 53 | backorderable = find(".stock_item_backorderable") 54 | backorderable.should be_checked 55 | end 56 | 57 | # Regression test for #2896 58 | # The regression was that unchecking the last checkbox caused a redirect 59 | # to happen. By ensuring that we're still on an /admin/products URL, we 60 | # assert that the redirect is *not* happening. 61 | it "can toggle backorderable for the second variant stock item", js: true do 62 | new_location = create(:stock_location, name: "Another Location", supplier: @user.supplier) 63 | visit page.current_path 64 | 65 | new_location_backorderable = find "#stock_item_backorderable_#{new_location.id}" 66 | new_location_backorderable.set(false) 67 | # Wait for API request to complete. 68 | sleep(1) 69 | 70 | page.current_url.should include("/admin/products") 71 | end 72 | 73 | it "can create a new stock movement", js: true do 74 | fill_in "stock_movement_quantity", with: 5 75 | select2 @user.supplier.name, from: "Stock Location" 76 | click_button "Add Stock" 77 | 78 | page.should have_content('successfully created') 79 | within(:css, '.stock_location_info table') do 80 | column_text(2).should eq '15' 81 | end 82 | end 83 | 84 | it "can create a new negative stock movement", js: true do 85 | fill_in "stock_movement_quantity", with: -5 86 | select2 @user.supplier.name, from: "Stock Location" 87 | click_button "Add Stock" 88 | 89 | page.should have_content('successfully created') 90 | 91 | within(:css, '.stock_location_info table') do 92 | column_text(2).should eq '5' 93 | end 94 | end 95 | 96 | it "can create a new negative stock movement", js: true do 97 | fill_in "stock_movement_quantity", with: -5 98 | select2 @user.supplier.name, from: "Stock Location" 99 | click_button "Add Stock" 100 | 101 | page.should have_content('successfully created') 102 | 103 | within(:css, '.stock_location_info table') do 104 | column_text(2).should eq '5' 105 | end 106 | end 107 | end 108 | 109 | context "with multiple variants" do 110 | before do 111 | v = @product.variants.create!(attributes_for(:variant)) 112 | @product.add_supplier! @user.supplier 113 | v.stock_items.first.update_column(:count_on_hand, 30) 114 | 115 | click_link "Products" 116 | within '#sidebar-product' do 117 | click_link 'Products' 118 | end 119 | click_link @product.name 120 | within '[data-hook=admin_product_tabs]' do 121 | click_link "Stock" 122 | end 123 | end 124 | 125 | it "can create a new stock movement for the specified variant", js: true do 126 | fill_in "stock_movement_quantity", with: 10 127 | select2 @v.sku, from: "Variant" 128 | click_button "Add Stock" 129 | 130 | page.should have_content('successfully created') 131 | end 132 | end 133 | end 134 | 135 | # Regression test for #3304 136 | context "with no stock location" do 137 | before do 138 | @product = create(:product, name: 'apache baseball cap', price: 10) 139 | @product.add_supplier! @user.supplier 140 | @product.variants.create!(attributes_for(:variant)) 141 | Spree::StockLocation.delete_all 142 | click_link "Products" 143 | within '#sidebar-product' do 144 | click_link 'Products' 145 | end 146 | click_link @product.name 147 | end 148 | 149 | it "redirects to stock locations page" do 150 | page.should have_content(Spree.t(:stock_management_requires_a_stock_location)) 151 | page.current_url.should include("admin/stock_locations") 152 | end 153 | end 154 | end 155 | end 156 | -------------------------------------------------------------------------------- /spec/models/spree/supplier_ability_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'cancan/matchers' 3 | require 'spree/testing_support/ability_helpers' 4 | 5 | describe Spree::SupplierAbility do 6 | 7 | let(:user) { create(:user, supplier: create(:supplier)) } 8 | let(:ability) { Spree::SupplierAbility.new(user) } 9 | let(:token) { nil } 10 | 11 | context 'for Dash' do 12 | let(:resource) { Spree::Admin::BaseController } 13 | 14 | context 'requested by supplier' do 15 | it_should_behave_like 'access denied' 16 | it_should_behave_like 'no index allowed' 17 | it_should_behave_like 'admin denied' 18 | end 19 | end 20 | 21 | context 'for Product' do 22 | let(:resource) { create(:product) } 23 | 24 | it_should_behave_like 'index allowed' 25 | it_should_behave_like 'admin granted' 26 | 27 | context 'requested by another suppliers user' do 28 | let(:resource) { 29 | product = create(:product) 30 | product.add_supplier!(create(:supplier)) 31 | product 32 | } 33 | it_should_behave_like 'access denied' 34 | end 35 | 36 | context 'requested by suppliers user' do 37 | let(:resource) { 38 | product = create(:product) 39 | product.add_supplier!(user.supplier) 40 | product.reload 41 | } 42 | # it_should_behave_like 'access granted' 43 | it { ability.should be_able_to :read, resource } 44 | it { ability.should be_able_to :stock, resource } 45 | end 46 | end 47 | 48 | context 'for Shipment' do 49 | context 'requested by another suppliers user' do 50 | let(:resource) { Spree::Shipment.new(stock_location: create(:stock_location, supplier: create(:supplier))) } 51 | it_should_behave_like 'access denied' 52 | it_should_behave_like 'no index allowed' 53 | it_should_behave_like 'admin denied' 54 | it { ability.should_not be_able_to :ready, resource } 55 | it { ability.should_not be_able_to :ship, resource } 56 | end 57 | 58 | context 'requested by suppliers user' do 59 | context 'when order is complete' do 60 | let(:resource) { 61 | order = create(:completed_order_for_drop_ship_with_totals) 62 | order.stock_locations.first.update_attribute :supplier, user.supplier 63 | Spree::Shipment.new(order: order, stock_location: order.stock_locations.first) 64 | } 65 | it_should_behave_like 'access granted' 66 | it_should_behave_like 'index allowed' 67 | it_should_behave_like 'admin granted' 68 | it { ability.should be_able_to :ready, resource } 69 | it { ability.should be_able_to :ship, resource } 70 | end 71 | 72 | context 'when order is incomplete' do 73 | let(:resource) { Spree::Shipment.new(stock_location: create(:stock_location, supplier: user.supplier)) } 74 | it_should_behave_like 'access denied' 75 | it { ability.should_not be_able_to :ready, resource } 76 | it { ability.should_not be_able_to :ship, resource } 77 | end 78 | end 79 | end 80 | 81 | context 'for StockItem' do 82 | let(:resource) { Spree::StockItem } 83 | 84 | it_should_behave_like 'index allowed' 85 | it_should_behave_like 'admin granted' 86 | 87 | context 'requested by another suppliers user' do 88 | let(:resource) { 89 | supplier = create(:supplier) 90 | variant = create(:product).master 91 | variant.product.add_supplier! supplier 92 | supplier.stock_locations.first.stock_items.first 93 | } 94 | it_should_behave_like 'access denied' 95 | end 96 | 97 | context 'requested by suppliers user' do 98 | let(:resource) { 99 | variant = create(:product).master 100 | variant.product.add_supplier! user.supplier 101 | user.supplier.stock_locations.first.stock_items.first 102 | } 103 | it_should_behave_like 'access granted' 104 | end 105 | end 106 | 107 | context 'for StockLocation' do 108 | context 'requsted by another suppliers user' do 109 | let(:resource) { 110 | supplier = create(:supplier) 111 | variant = create(:product).master 112 | variant.product.add_supplier! supplier 113 | supplier.stock_locations.first 114 | } 115 | it_should_behave_like 'create only' 116 | end 117 | 118 | context 'requested by suppliers user' do 119 | let(:resource) { 120 | variant = create(:product).master 121 | variant.product.add_supplier! user.supplier 122 | user.supplier.stock_locations.first 123 | } 124 | it_should_behave_like 'access granted' 125 | it_should_behave_like 'admin granted' 126 | it_should_behave_like 'index allowed' 127 | end 128 | end 129 | 130 | context 'for StockMovement' do 131 | let(:resource) { Spree::StockMovement } 132 | 133 | it_should_behave_like 'index allowed' 134 | it_should_behave_like 'admin granted' 135 | 136 | context 'requested by another suppliers user' do 137 | let(:resource) { 138 | supplier = create(:supplier) 139 | variant = create(:product).master 140 | variant.product.add_supplier! supplier 141 | Spree::StockMovement.new(stock_item: supplier.stock_locations.first.stock_items.first) 142 | } 143 | it_should_behave_like 'create only' 144 | end 145 | 146 | context 'requested by suppliers user' do 147 | let(:resource) { 148 | variant = create(:product).master 149 | variant.product.add_supplier! user.supplier 150 | Spree::StockMovement.new(stock_item: user.supplier.stock_locations.first.stock_items.first) 151 | } 152 | it_should_behave_like 'access granted' 153 | end 154 | end 155 | 156 | context 'for Supplier' do 157 | context 'requested by any user' do 158 | let(:ability) { Spree::SupplierAbility.new(create(:user)) } 159 | let(:resource) { Spree::Supplier } 160 | 161 | it_should_behave_like 'admin denied' 162 | it_should_behave_like 'access denied' 163 | end 164 | 165 | context 'requested by suppliers user' do 166 | let(:resource) { user.supplier } 167 | it_should_behave_like 'admin granted' 168 | it_should_behave_like 'update only' 169 | end 170 | end 171 | 172 | end 173 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ### Check out the latest, officially maintained [Spree Multi Vendor marketplace](https://github.com/spree-contrib/spree_multi_vendor) extension 3 | 4 | # Spree Drop Ship 5 | 6 | [![Build Status](https://travis-ci.org/spree-contrib/spree_drop_ship.png)](https://travis-ci.org/spree-contrib/spree_drop_ship) 7 | [![Code Climate](https://codeclimate.com/github/spree-contrib/spree_drop_ship.png)](https://codeclimate.com/github/spree-contrib/spree_drop_ship) 8 | [![Coverage Status](https://coveralls.io/repos/spree-contrib/spree_drop_ship/badge.png?branch=master)](https://coveralls.io/r/spree-contrib/spree_drop_ship) 9 | [![Dependency Status](https://gemnasium.com/spree-contrib/spree_drop_ship.png?travis)](https://gemnasium.com/spree-contrib/spree_drop_ship) 10 | 11 | What is drop shipping? 12 | 13 | "Drop shipping is a supply chain management technique in which the retailer does not keep goods in stock, but instead transfers customer orders 14 | and shipment details to either the manufacturer or a wholesaler, who then ships the goods directly to the customer." - [wikipedia](http://en.wikipedia.org/wiki/Drop_shipping) 15 | 16 | So the main goal with spree_drop_ship is to link products to suppliers and forward orders to the appropriate suppliers. 17 | 18 | Once an order is placed for a product that belongs to a supplier a shipment is created for the product's supplier. 19 | This shipment is then sent to the supplier (via Email by default). The supplier then follows a link to the shipment 20 | within the email where they are prompted to confirm the shipment. 21 | 22 | Spree Drop Ship used with [Spree Marketplace](https://github.com/jdutil/spree_marketplace) allows handling payments to your suppliers via ACH direct deposits. 23 | This is still currently a work in progress, and any input is welcome. 24 | . 25 | 26 | Upgrading 27 | --------- 28 | 29 | **Warning: Upgrading to Spree 2.2.x & 2.3.x when using this extension is not backwards compatible. 30 | I have removed the notion of drop ship orders which payments & commission were previously tracked to. 31 | Now suppliers simply manage their shipments, and payments & commission are now linked to a payable object i.e. shipment in this case. 32 | This means the previous method of determining a suppliers commission is no longer valid, and you will need to migrate your data accordingly.** 33 | 34 | I'm sorry for the inconvenience this may cause, but I've determined for this extension to meet it's most potential I needed to drastically alter the approach 35 | it was taking. I'm still undergoing several more radical changes for Spree 2.3.x that involve moving product management from this extension into the spree_marketplace 36 | extension. The goal from the beginning of this extension has been for it to be a very light weight and extensible drop shipping solution. Much of this extension 37 | has been made obsolete by split shipping, and line item adjustments within Spree Core itself. Now I feel I can really streamline this extension to take advantage 38 | of the recent Spree Core changes, and also move the product management into the marketplace extension as that is really more of what product management is inteded for. 39 | The typical drop shipping scenario would simply be a supplier being able to update their shipments they need to fulfill and nothing more. 40 | 41 | Installation 42 | ------------ 43 | 44 | Here's how to install spree_drop_ship into your existing spree site AFTER you've installed Spree: 45 | 46 | Add the following to your Gemfile: 47 | 48 | gem 'spree_drop_ship', github: 'spree-contrib/spree_drop_ship' 49 | 50 | Make your bundle happy: 51 | 52 | bundle install 53 | 54 | Now run the generator: 55 | 56 | rails g spree_drop_ship:install 57 | 58 | Then migrate your database if you did not run during installation generator: 59 | 60 | bundle exec rake db:migrate 61 | 62 | And reboot your server: 63 | 64 | rails s 65 | 66 | You should be up and running now! 67 | 68 | Sample Data 69 | ----------- 70 | 71 | If you'd like to generate sample data, use the included rake tasks: 72 | 73 | ```shell 74 | rake spree_sample:load # Loads sample data into the store 75 | rake spree_sample:suppliers # Create sample suppliers and randomly link to products 76 | rake spree_sample:drop_ship_orders # Create sample drop ship orders 77 | ``` 78 | 79 | Demo 80 | ---- 81 | 82 | You can easily use the spec/dummy app as a demo of spree_drop_ship. Just `cd` to where you develop and run: 83 | 84 | ```shell 85 | git clone git://github.com/spree-contrib/spree_drop_ship.git 86 | cd spree_drop_ship 87 | bundle install 88 | bundle exec rake test_app 89 | cd spec/dummy 90 | rake db:migrate db:seed spree_sample:load spree_sample:suppliers spree_sample:drop_ship_orders 91 | rails s 92 | ``` 93 | 94 | Testing 95 | ------- 96 | 97 | Be sure to bundle your dependencies and then create a dummy test app for the specs to run against. 98 | 99 | ```shell 100 | bundle 101 | bundle exec rake test_app 102 | bundle exec rspec spec 103 | ``` 104 | 105 | Todo 106 | ---- 107 | 108 | - Stock Items should automatically be set to backorderable false if the variant doesnt belong to the stock locations supplier. 109 | - Must allow suppliers to edit their stock location addresses & require it. 110 | - Return Authorization UI 111 | - Better documentation 112 | - related products should only allow suppliers own products to be related 113 | 114 | Contributing 115 | ------------ 116 | 117 | In the spirit of [free software](http://www.fsf.org/licensing/essays/free-sw.html), **everyone** is encouraged to help improve this project. 118 | 119 | Here are some ways *you* can contribute: 120 | 121 | * by using prerelease versions 122 | * by reporting [bugs](https://github.com/spree-contrib/spree_drop_ship/issues) 123 | * by suggesting new features 124 | * by [translating to a new language](https://github.com/spree-contrib/spree_drop_ship/tree/master/config/locales) 125 | * by writing or editing documentation 126 | * by writing specifications 127 | * by writing code (*no patch is too small*: fix typos, add comments, clean up inconsistent whitespace) 128 | * by refactoring code 129 | * by resolving [issues](https://github.com/spree-contrib/spree_drop_ship/issues) 130 | * by reviewing patches 131 | 132 | Donating 133 | -------- 134 | 135 | Bitcoin donations may be sent to: 1L6akT6Aus9r6Ashw1wDtLg7D8zJCVVZac 136 | 137 | Copyright (c) 2012-2014 Jeff Dutil, released under the [New BSD License](https://github.com/spree-contrib/spree_drop_ship/tree/master/LICENSE). 138 | -------------------------------------------------------------------------------- /app/views/spree/drop_ship_order_mailer/supplier_order.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 118 | 119 |
5 | 6 | 7 | 8 | 11 | 12 | 13 | 20 | 21 | 22 | 26 | 27 | 28 | 106 | 107 | 108 | 113 | 114 | 115 |
9 | 10 |
14 |

<%= Spree.t('drop_ship_order_mailer. 15 | supplier_order.hello', name: @supplier.name ) %>

16 |

17 | An order has been placed. Please confirm the shipment details, and <%= link_to 'update the shipment', spree.edit_admin_shipment_url(@shipment) %> once it has shipped. 18 |

19 |
23 |

Reference Number: <%= @shipment.number %>

24 |

Placed on: <%= @shipment.created_at.strftime('%A %b %e, %Y at %l:%M%p %Z') %>

25 |
29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 47 | 48 | 51 | 52 | 53 |
Shipping Information:Shipping Method:
40 | <%= @shipment.order.ship_address.firstname %> <%= @shipment.order.ship_address.lastname %>
41 | <%= @shipment.order.ship_address.address1 %>
42 | <% unless @shipment.order.ship_address.address2.blank? %><%= @shipment.order.ship_address.address2 %>
<% end %> 43 | <%= @shipment.order.ship_address.city %>, <%= @shipment.order.ship_address.state.name %> <%= @shipment.order.ship_address.zipcode %>
44 | <%= @shipment.order.ship_address.country.name %>
45 | T: <%= @shipment.order.ship_address.phone %> 46 |
  49 | <%= @shipment.shipping_method.name %>
50 |
54 |
55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | <% @shipment.line_items.each do |line_item| %> 66 | 67 | 70 | 71 | 72 | 75 | 76 | <% end %> 77 | 78 | 79 | 80 | 83 | 86 | 87 | 88 | 91 | 94 | 95 | 96 | 99 | 102 | 103 | 104 |
ItemSkuQtySubtotal
68 | <%= line_item.variant.name %> 69 | <%= line_item.variant.sku %><%= line_item.quantity %> 73 | <%= line_item.display_amount %> 74 |
81 | Subtotal 82 | 84 | <%= @shipment.display_item_cost %> 85 |
89 | Shipping & Handling 90 | 92 | <%= @shipment.display_final_price %> 93 |
97 | Grand Total 98 | 100 | <%= @shipment.display_final_price_with_items %> 101 |
105 |
109 |
110 |

<%= raw Spree.t('drop_ship_order_mailer.supplier_order.thank_you_again', name: Spree::Store.current.name) %>

111 |
112 |
116 |
120 | --------------------------------------------------------------------------------