├── app ├── models │ ├── .keep │ └── spree │ │ ├── order_decorator.rb │ │ ├── shipping_method_decorator.rb │ │ ├── shipping_category_decorator.rb │ │ ├── shipping │ │ ├── box.rb │ │ └── package.rb │ │ ├── address_decorator.rb │ │ ├── stock_location_decorator.rb │ │ └── shipment_decorator.rb ├── assets │ └── javascripts │ │ └── admin │ │ └── working_title │ │ ├── namespace.js.coffee │ │ └── order_shipments.js.coffee ├── views │ └── spree │ │ ├── user_sessions │ │ └── new.html.haml │ │ ├── shared │ │ ├── _js_onready.html.haml │ │ └── _user_form.html.erb │ │ └── admin │ │ └── orders │ │ └── shipments │ │ └── edit.haml ├── overrides │ └── spree │ │ ├── admin │ │ └── shared │ │ │ └── _order_tabs │ │ │ └── add_shipment_link.html.erb.deface │ │ └── layouts │ │ └── admin │ │ └── add_js_onready_yielder.html.erb.deface └── controllers │ └── spree │ └── admin │ └── orders │ └── shipments_controller.rb ├── config ├── spree.yml ├── initializers │ ├── devise.rb │ ├── session_store.rb │ ├── filter_parameter_logging.rb │ ├── mime_types.rb │ ├── backtrace_silencers.rb │ ├── wrap_parameters.rb │ ├── secret_token.rb │ ├── inflections.rb │ └── spree.rb ├── locales │ └── en.yml ├── environment.rb ├── boot.rb ├── database.yml ├── application.rb └── routes.rb ├── db ├── seeds.rb ├── seeds │ ├── shipping.rb │ └── shipping │ │ └── boxes.rb └── migrate │ ├── 20140123204218_add_label_printer_name_to_user.rb │ ├── 20140121231143_add_label_print_jobs.rb │ ├── 20140121212137_add_shipping_boxes.rb │ └── 20140121211116_add_shipment_packages.rb ├── lib ├── spree │ └── permitted_attributes_decorator.rb └── utilities │ └── labeler.rb ├── config.ru ├── spec ├── models │ └── spree │ │ ├── order_spec.rb │ │ ├── stock_location_spec.rb │ │ ├── shipment_spec.rb │ │ ├── shipping_method_spec.rb │ │ └── shipping │ │ ├── box_spec.rb │ │ └── package_spec.rb └── spec_helper.rb ├── Rakefile ├── README.md ├── LICENSE.txt ├── data └── test_label.zpl └── Gemfile /app/models/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /config/spree.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2.1.3 3 | -------------------------------------------------------------------------------- /db/seeds.rb: -------------------------------------------------------------------------------- 1 | load "db/seeds/shipping.rb" 2 | -------------------------------------------------------------------------------- /config/initializers/devise.rb: -------------------------------------------------------------------------------- 1 | Devise.secret_key = ENV['DEVISE_SECRET_KEY'] 2 | -------------------------------------------------------------------------------- /app/assets/javascripts/admin/working_title/namespace.js.coffee: -------------------------------------------------------------------------------- 1 | window.WorkingTitle ||= {} 2 | -------------------------------------------------------------------------------- /db/seeds/shipping.rb: -------------------------------------------------------------------------------- 1 | puts "** SHIPPING SEEDS **" 2 | 3 | load "db/seeds/shipping/boxes.rb" 4 | -------------------------------------------------------------------------------- /app/models/spree/order_decorator.rb: -------------------------------------------------------------------------------- 1 | Spree::Order.class_eval do 2 | has_many :packages, through: :shipments 3 | end 4 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | spree: 3 | order_number: "Order Number" 4 | recently_viewed: 5 | header: "Recently Viewed" 6 | -------------------------------------------------------------------------------- /lib/spree/permitted_attributes_decorator.rb: -------------------------------------------------------------------------------- 1 | Spree::PermittedAttributes.class_eval do 2 | user_attributes << :label_printer_name 3 | end 4 | -------------------------------------------------------------------------------- /app/models/spree/shipping_method_decorator.rb: -------------------------------------------------------------------------------- 1 | Spree::ShippingMethod.class_eval do 2 | def fedex? 3 | name.to_s.downcase.include? 'fedex' 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/models/spree/shipping_category_decorator.rb: -------------------------------------------------------------------------------- 1 | Spree::ShippingCategory.class_eval do 2 | def self.default 3 | where("name ilike 'default'").first 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require ::File.expand_path('../config/environment', __FILE__) 4 | run Rails.application 5 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require File.expand_path('../application', __FILE__) 3 | 4 | # Initialize the Rails application. 5 | WorkingTitle::Application.initialize! 6 | -------------------------------------------------------------------------------- /config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | WorkingTitle::Application.config.session_store :cookie_store, key: '_working-title_session' 4 | -------------------------------------------------------------------------------- /db/migrate/20140123204218_add_label_printer_name_to_user.rb: -------------------------------------------------------------------------------- 1 | class AddLabelPrinterNameToUser < ActiveRecord::Migration 2 | def change 3 | add_column :spree_users, :label_printer_name, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [:password] 5 | -------------------------------------------------------------------------------- /config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | # Mime::Type.register_alias "text/html", :iphone 6 | -------------------------------------------------------------------------------- /spec/models/spree/order_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Spree::Order do 4 | 5 | describe "configuration" do 6 | describe "associations" do 7 | it { should have_many(:packages).through(:shipments) } 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require File.expand_path('../config/application', __FILE__) 5 | 6 | WorkingTitle::Application.load_tasks 7 | -------------------------------------------------------------------------------- /app/models/spree/shipping/box.rb: -------------------------------------------------------------------------------- 1 | module Spree 2 | module Shipping 3 | class Box < ActiveRecord::Base 4 | self.table_name = "spree_shipping_boxes" 5 | 6 | has_many :packages 7 | 8 | validates_uniqueness_of :description, case_sensitive: false 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/views/spree/user_sessions/new.html.haml: -------------------------------------------------------------------------------- 1 | - if flash[:alert] 2 | .flash.errors= flash[:alert] 3 | 4 | #existing-customer.row 5 | .col-md-6 6 | %h1.la-heading.margin-40= Spree.t(:login_as_existing) 7 | %div{ data: { hook: 'login' } } 8 | = render partial: 'spree/shared/login' 9 | 10 | .col.md-6 -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | # Set up gems listed in the Gemfile. 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | 4 | unless %w[staging production].include?(ENV["RAILS_ENV"].to_s.strip) 5 | require 'dotenv' 6 | Dotenv.load 7 | end 8 | 9 | require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE']) 10 | -------------------------------------------------------------------------------- /app/overrides/spree/admin/shared/_order_tabs/add_shipment_link.html.erb.deface: -------------------------------------------------------------------------------- 1 | 2 | > 3 | 4 | Shipping & Labels 5 | 6 | 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # What 2 | This is intended to be a [Spree](https://github.com/spree/spree) extension that pairs with `spree_active_shipping`. I'm not ready to set aside the time to build out a full extension at the moment but I did take a few minutes to extract the relevant code from my existing apps so that I'd have something to point to the next time I bring this up in #spree. 3 | 4 | ## Screenshot 5 | ![screenshot](http://i.imgur.com/jR54udv.png) 6 | -------------------------------------------------------------------------------- /app/overrides/spree/layouts/admin/add_js_onready_yielder.html.erb.deface: -------------------------------------------------------------------------------- 1 | 3 | 4 | <% content_for :js_onready do %> 5 | console.log("Brutishly shortening admin menu headings"); 6 | $("#admin-menu span:contains('onfiguration')").text('Config'); 7 | $("#admin-menu span:contains('romotions')").text('Promos'); 8 | <% end %> 9 | 10 | <%= render partial: 'spree/shared/js_onready' %> 11 | -------------------------------------------------------------------------------- /config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /app/models/spree/address_decorator.rb: -------------------------------------------------------------------------------- 1 | Spree::Address.class_eval do 2 | def fedex_formatted 3 | { 4 | name: full_name, 5 | company: company, 6 | phone_number: phone, 7 | address: [address1, address2].compact.join(' '), 8 | city: city, 9 | state: state && state.abbr, 10 | postal_code: zipcode, 11 | country_code: country && country.iso, 12 | residential: false, # going commercial to get access to FedEx Ground 13 | } 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/models/spree/stock_location_decorator.rb: -------------------------------------------------------------------------------- 1 | Spree::StockLocation.class_eval do 2 | def company 3 | "Working Title" 4 | end 5 | 6 | def fedex_formatted 7 | { 8 | company: company, 9 | phone_number: phone, 10 | address: [address1, address2].compact.join(' '), 11 | city: city, 12 | state: state && state.abbr, 13 | postal_code: zipcode, 14 | country_code: country && country.iso, 15 | residential: !company.blank?, 16 | } 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /app/models/spree/shipment_decorator.rb: -------------------------------------------------------------------------------- 1 | Spree::Shipment.class_eval do 2 | has_many :packages, class_name: "Spree::Shipping::Package" 3 | 4 | accepts_nested_attributes_for :packages, allow_destroy: true, 5 | :reject_if => proc { |attributes| attributes['weight'].to_f <= 0 } 6 | 7 | delegate :fedex?, :to => :shipping_method 8 | 9 | scope :not_shipped, -> { where(arel_table[:state].eq('shipped').not) } 10 | scope :backordered, -> { joins(:inventory_units). 11 | merge(Spree::InventoryUnit.backordered) } 12 | end 13 | -------------------------------------------------------------------------------- /app/views/spree/shared/_js_onready.html.haml: -------------------------------------------------------------------------------- 1 | :javascript 2 | var console = window.console || { log: function(){} }; 3 | 4 | - if content_for?(:js_onready) 5 | - escaped_js = content_for(:js_onready) 6 | - unescaped_js = escaped_js.gsub("&&", "&&") 7 | 8 | :javascript 9 | $(function() { 10 | console.log("** Beginning sitewide :js_onready **"); 11 | #{unescaped_js} 12 | console.log("** Sitewide :js_onready ended. **"); 13 | }); 14 | - else 15 | :javascript 16 | console.log("** No :js_onready code received **") 17 | -------------------------------------------------------------------------------- /db/migrate/20140121231143_add_label_print_jobs.rb: -------------------------------------------------------------------------------- 1 | class AddLabelPrintJobs < ActiveRecord::Migration 2 | def change 3 | create_table "label_print_jobs", :force => true do |t| 4 | t.string "printer_name", :null => false 5 | t.text "label_plaintext", :null => false 6 | t.datetime "processed_at" 7 | t.datetime "created_at", :null => false 8 | t.datetime "updated_at", :null => false 9 | end 10 | 11 | add_index "label_print_jobs", ["processed_at"], :name => "idx_label_print_jobs_by_processed_at" 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/seeds/shipping/boxes.rb: -------------------------------------------------------------------------------- 1 | dimensions = [ 2 | ['(7x7x7)', '7x7x7'], 3 | ['(12x12x8)', '12x12x8'], 4 | ['(15x15x8)', '15x15x8'], 5 | ['(24x12x12)', '24x12x12'], 6 | ['(36x12x12)', '36x12x12'], 7 | ['(24x24x12)', '24x24x12'], 8 | ['(48x24x12)', '48x24x12']] 9 | 10 | dimensions.each do |desc, lwh| 11 | l,w,h = lwh.split('x') 12 | 13 | box = Spree::Shipping::Box.find_or_create_by( 14 | description: desc, 15 | length: l, 16 | width: w, 17 | height: h) 18 | 19 | puts "Ensured existence of shipping box #{box.description}" 20 | end 21 | -------------------------------------------------------------------------------- /spec/models/spree/stock_location_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Spree::StockLocation do 4 | let(:klass) { Spree::StockLocation } 5 | 6 | describe "public instance methods" do 7 | it "should have a :company method returning a string" do 8 | subject.should respond_to(:company) 9 | subject.company.is_a?(String).should be_true 10 | end 11 | 12 | it "should have a :fedex_formatted method that returns a hash" do 13 | subject.should respond_to(:fedex_formatted) 14 | subject.fedex_formatted.is_a?(Hash).should be_true 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] if respond_to?(:wrap_parameters) 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /config/initializers/secret_token.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key is used for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | 6 | # Make sure the secret is at least 30 characters and all random, 7 | # no regular words or you'll be exposed to dictionary attacks. 8 | # You can use `rake secret` to generate a secure secret key. 9 | 10 | # Make sure your secret_key_base is kept private 11 | # if you're sharing your code publicly. 12 | WorkingTitle::Application.config.secret_key_base = 'bananas' 13 | -------------------------------------------------------------------------------- /db/migrate/20140121212137_add_shipping_boxes.rb: -------------------------------------------------------------------------------- 1 | class AddShippingBoxes < ActiveRecord::Migration 2 | def change 3 | create_table "spree_shipping_boxes", :force => true do |t| 4 | t.string "description", :null => false 5 | t.integer "length", :default => 0, :null => false 6 | t.integer "width", :default => 0, :null => false 7 | t.integer "height", :default => 0, :null => false 8 | t.datetime "created_at", :null => false 9 | t.datetime "updated_at", :null => false 10 | end 11 | 12 | add_index "spree_shipping_boxes", ["description"], :name => "udx_shipping_boxes_on_description", :unique => true 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, '\1en' 8 | # inflect.singular /^(ox)en/i, '\1' 9 | # inflect.irregular 'person', 'people' 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym 'RESTful' 16 | # end 17 | -------------------------------------------------------------------------------- /config/initializers/spree.rb: -------------------------------------------------------------------------------- 1 | # Configure Spree Preferences 2 | # 3 | # Note: Initializing preferences available within the Admin will overwrite any changes that were made through the user interface when you restart. 4 | # If you would like users to be able to update a setting with the Admin it should NOT be set here. 5 | # 6 | # In order to initialize a setting do: 7 | # config.setting_name = 'new value' 8 | Spree.config do |config| 9 | config.currency = "USD" 10 | config.site_name = "Working Title" 11 | config.track_inventory_levels = true 12 | #config.products_per_page = 48 # prefer multiples of 3 13 | config.orders_per_page = 15 # admin page 14 | end 15 | 16 | Spree.user_class = "Spree::User" 17 | -------------------------------------------------------------------------------- /db/migrate/20140121211116_add_shipment_packages.rb: -------------------------------------------------------------------------------- 1 | class AddShipmentPackages < ActiveRecord::Migration 2 | def change 3 | create_table "spree_shipping_packages", :force => true do |t| 4 | t.integer "shipment_id" 5 | t.integer "box_id" 6 | t.decimal "weight", :precision => 8, :scale => 2 7 | t.datetime "created_at", :null => false 8 | t.datetime "updated_at", :null => false 9 | t.text "label_zpl" 10 | t.string "tracking_number" 11 | t.integer "shipping_method_id" 12 | end 13 | 14 | add_index :spree_shipping_packages, [:shipment_id], name: :idx_spree_shipping_packages_on_shipment 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/models/spree/shipment_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Spree::Shipment do 4 | describe "decorations" do 5 | describe 'configuration' do 6 | context "associations" do 7 | it { should have_many(:packages) } 8 | end 9 | 10 | context "delegates" do 11 | let(:shipping_method) { double("shipping_method") } 12 | 13 | before { subject.stub(:shipping_method).and_return shipping_method } 14 | 15 | it { should accept_nested_attributes_for(:packages).allow_destroy(true) } 16 | 17 | it "should delegate :fedex? to :shipping_method" do 18 | shipping_method.should_receive :fedex? 19 | subject.fedex? 20 | end 21 | 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/models/spree/shipping_method_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Spree::ShippingMethod do 4 | context "public instance methods" do 5 | describe ".fedex?" do 6 | it "should be true if the downcased name includes 'fedex'" do 7 | subject.stub(:name).and_return('123123fedex324') 8 | subject.fedex?.should be_true 9 | 10 | subject.stub(:name).and_return('FEDEX!!') 11 | subject.fedex?.should be_true 12 | end 13 | 14 | it "should not be true if the downcased name doesn't include 'fedex'" do 15 | subject.stub(:name).and_return('123123f') 16 | subject.fedex?.should be_false 17 | 18 | subject.stub(:name).and_return(nil) 19 | subject.fedex?.should be_false 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /config/database.yml: -------------------------------------------------------------------------------- 1 | # PG setup: 2 | # createuser working_title --createdb 3 | # rake db:create db:migrate db:seed 4 | # env RAILS_ENV=test rake db:create db:migrate db:seed 5 | 6 | #------------------------------------------------------------ 7 | # common 8 | #------------------------------------------------------------ 9 | 10 | pg: &pg 11 | adapter: postgresql 12 | encoding: utf8 13 | template: template0 14 | port: 5432 15 | pool: 25 16 | 17 | #------------------------------------------------------------ 18 | # environments 19 | #------------------------------------------------------------ 20 | 21 | development: 22 | <<: *pg 23 | host: 127.0.0.1 24 | username: working_title 25 | password: working_title 26 | database: working_title_development 27 | 28 | test: &test 29 | <<: *pg 30 | host: 127.0.0.1 31 | username: working_title 32 | password: working_title 33 | database: working_title_test 34 | -------------------------------------------------------------------------------- /spec/models/spree/shipping/box_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Spree::Shipping::Box do 4 | context "database fields" do 5 | it { should have_db_column(:id).of_type(:integer) } 6 | it { should have_db_column(:description).of_type(:string).with_options(null: false) } 7 | it { should have_db_column(:length).of_type(:integer).with_options(default: 0, null: false) } 8 | it { should have_db_column(:width).of_type(:integer).with_options(default: 0, null: false) } 9 | it { should have_db_column(:height).of_type(:integer).with_options(default: 0, null: false) } 10 | end 11 | 12 | context "database indexes" do 13 | it { should have_db_index(:description).unique(true) } 14 | end 15 | 16 | context "configuration" do 17 | context "validations" do 18 | it { should validate_uniqueness_of(:description).case_insensitive } 19 | end 20 | 21 | describe "associations" do 22 | it { should have_many(:packages) } 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Coroutine, LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /app/views/spree/shared/_user_form.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 | General User Settings 4 |
5 | <%= f.label :email, Spree.t(:email) %> 6 | <%= f.email_field :email, :class => 'title' %> 7 |
8 |
9 |

10 | <%= f.label :password, Spree.t(:password) %> 11 | <%= f.password_field :password, :class => 'title' %> 12 |

13 | 14 |

15 | <%= f.label :password_confirmation, Spree.t(:confirm_password) %> 16 | <%= f.password_field :password_confirmation, :class => 'title' %> 17 |

18 |
19 |
20 | 21 |
22 | <% if spree_current_user && spree_current_user.admin? %> 23 |
24 | Warehouse-Only 25 |

26 | <%= f.label :label_printer_name, Spree.t(:label_printer_name) %>
27 | <%= f.text_field :label_printer_name, :class => 'title' %> 28 |

29 |
30 | <% end %> 31 |
32 | 33 |
34 | 35 |
36 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # load rails and other dependencies 2 | # 3 | ENV["RAILS_ENV"] ||= 'test' 4 | require File.expand_path("../../config/environment", __FILE__) 5 | require 'rspec/rails' 6 | 7 | require 'capybara-screenshot/rspec' 8 | 9 | # spree test helpers 10 | require 'spree/testing_support/factories' 11 | require 'spree/testing_support/controller_requests' 12 | require 'spree/testing_support/authorization_helpers' 13 | 14 | # load supporting code 15 | Dir[Rails.root.join("spec/support/**/*.rb")].each {|f| require f} 16 | 17 | # configure rspec 18 | RSpec.configure do |config| 19 | Capybara.javascript_driver = :webkit 20 | 21 | config.order = 'random' 22 | 23 | # controller test setup 24 | config.include Spree::TestingSupport::ControllerRequests, type: :controller 25 | config.include Devise::TestHelpers, type: :controller 26 | 27 | # mocks 28 | config.mock_with :rspec 29 | 30 | # transactions 31 | config.use_transactional_fixtures = true 32 | config.use_transactional_examples = false 33 | 34 | # database cleaner 35 | config.before(:suite) { DatabaseCleaner.strategy = :transaction } 36 | config.before(:each) { DatabaseCleaner.start } 37 | config.after(:each) { DatabaseCleaner.clean } 38 | 39 | end 40 | -------------------------------------------------------------------------------- /app/assets/javascripts/admin/working_title/order_shipments.js.coffee: -------------------------------------------------------------------------------- 1 | WorkingTitle.OrderShipments ||= 2 | initialize: -> 3 | @selectifyForm() 4 | @bindHandlers() 5 | console.log("OrderShipments initialized.") 6 | 7 | selectifyForm: -> 8 | $(".shipment-package-form select").select2 width: "100%" 9 | 10 | preselectMethod: ($container) -> 11 | window.$container = $container 12 | methodId = $container.data().methodId || null 13 | $container.find(".shipping-method-column option[value=#{methodId}]"). 14 | last().attr('selected', 'selected') 15 | 16 | handleFieldAdded: (event) -> 17 | container = $(event.target).parents(".shipment-package-form") 18 | WorkingTitle.OrderShipments.preselectMethod(container) 19 | WorkingTitle.OrderShipments.selectifyForm() 20 | 21 | # not printable until it's persisted 22 | $(".icon-print", event.field).hide() 23 | 24 | bindHandlers: -> 25 | $(document).on "nested:fieldAdded", WorkingTitle.OrderShipments.handleFieldAdded 26 | 27 | # api ship endpoint returns JSON that we don't want to see 28 | $(".finalize-shipment-button").bind "ajax:success", -> 29 | location.reload() 30 | 31 | $(".finalize-shipment-button").bind "ajax:failure", -> 32 | alert "Failed to finalize shipment." 33 | 34 | $(".icon-print").bind "ajax:error", (e, data, status, xhr) -> 35 | err = JSON.parse(data.responseText).error 36 | window.errs = err 37 | err = err or "Unexpected shipping error! Check server logs for details." 38 | alert err 39 | 40 | $(".icon-print").bind "ajax:success", (e, data, status, xhr) -> 41 | location.reload() 42 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../boot', __FILE__) 2 | 3 | require 'rails/all' 4 | 5 | # Require the gems listed in Gemfile, including any gems 6 | # you've limited to :test, :development, or :production. 7 | Bundler.require(:default, Rails.env) 8 | 9 | module WorkingTitle 10 | class Application < Rails::Application 11 | 12 | config.to_prepare do 13 | # Load libs 14 | Dir.glob(File.join(File.dirname(__FILE__), "../lib/**/*.rb")) do |c| 15 | Rails.configuration.cache_classes ? require(c) : load(c) 16 | end 17 | 18 | # Load application's model / class decorators 19 | Dir.glob(File.join(File.dirname(__FILE__), "../app/**/*_decorator*.rb")) do |c| 20 | Rails.configuration.cache_classes ? require(c) : load(c) 21 | end 22 | 23 | # Load application's view overrides 24 | Dir.glob(File.join(File.dirname(__FILE__), "../app/overrides/*.rb")) do |c| 25 | Rails.configuration.cache_classes ? require(c) : load(c) 26 | end 27 | end 28 | 29 | # Settings in config/environments/* take precedence over those specified here. 30 | # Application configuration should go into files in config/initializers 31 | # -- all .rb files in that directory are automatically loaded. 32 | 33 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 34 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 35 | config.time_zone = 'Central Time (US & Canada)' 36 | 37 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 38 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 39 | # config.i18n.default_locale = :de 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /app/controllers/spree/admin/orders/shipments_controller.rb: -------------------------------------------------------------------------------- 1 | module Spree 2 | module Admin 3 | module Orders 4 | class ShipmentsController < Spree::Admin::BaseController 5 | expose(:order) { @order } 6 | 7 | before_filter :load_ivars 8 | 9 | rescue_from Utilities::LabelError do |err| 10 | render json: { error: "Error:\n< #{err.message} >" }, status: 422 11 | end 12 | 13 | def edit 14 | end 15 | 16 | def update 17 | unless @shipment.update_attributes(shipment_params) 18 | flash[:error] = @shipment.errors.full_messages.join(" ") 19 | end 20 | 21 | redirect_to edit_admin_order_shipments_url 22 | end 23 | 24 | def print 25 | unless @package.persisted? 26 | return render( 27 | status: 200, 28 | json: { errors: [Spree.t(:cannot_print_unsaved_label)] }) 29 | end 30 | 31 | printer_name = spree_current_user.label_printer_name 32 | 33 | @package.print_label!(printer_name: printer_name) 34 | render status: 200, 35 | json: { tracking_code: @package.tracking_number } 36 | end 37 | 38 | def print_product_labels 39 | labels = @order.line_items.map(&:generate_variant_label!) 40 | render json: labels 41 | end 42 | 43 | private 44 | def load_ivars 45 | @order = Order.find_by_number!(params[:order_id], :include => :adjustments) 46 | @shipment = @order.shipments.find_by_number(params[:shipment_number]) 47 | @package = @order.packages.find_by_id(params[:package_id]) 48 | end 49 | 50 | def shipment_params 51 | params.require(:shipment).permit( 52 | packages_attributes: [:shipping_method_id, :box_id, :weight, :_destroy, :id]) 53 | end 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /app/models/spree/shipping/package.rb: -------------------------------------------------------------------------------- 1 | module Spree 2 | module Shipping 3 | class Package < ActiveRecord::Base 4 | after_save :update_shipment_tracking_number 5 | 6 | self.table_name = "spree_shipping_packages" 7 | 8 | belongs_to :shipment, validate: true 9 | belongs_to :shipping_method, validate: true 10 | belongs_to :box, validate: true 11 | has_one :order, through: :shipment 12 | 13 | scope :has_label, -> { where "label_zpl is not null" } 14 | 15 | validates_presence_of :shipping_method_id 16 | validates_presence_of :box_id 17 | validates_presence_of :weight 18 | validates_numericality_of :weight, greater_than: 0 19 | 20 | delegate :fedex?, to: :shipping_method 21 | 22 | delegate :length, to: :box 23 | delegate :width, to: :box 24 | delegate :height, to: :box 25 | 26 | def destination 27 | shipment.address || order.ship_address 28 | end 29 | 30 | def origin 31 | shipment.stock_location 32 | end 33 | 34 | # Endicia requires 4.1 precision 35 | def weight_in_ounces 36 | raw = weight * 16.0 37 | rounded_up = (raw * 10).ceil / 10.0 38 | end 39 | 40 | # Throw away the label ZPL when e.g. shipping method or address has changed 41 | # 42 | def invalidate_label! 43 | self.label_zpl = nil 44 | save! 45 | end 46 | 47 | def print_label!(printer_name: nil) 48 | generate_label! 49 | 50 | # Left out because I didn't write that class: 51 | Utilities::RawPrinter.new(printer_name).print label_zpl 52 | end 53 | 54 | private 55 | def generate_label! 56 | # Don't regenerate a label unless it's nil. Regenerating USPS labels 57 | # costs money. 58 | # 59 | 60 | if label_zpl.nil? 61 | begin 62 | response = ::Utilities::Labeler.new(self).generate 63 | rescue *acceptable_errors => err 64 | handle_label_error! err 65 | end 66 | 67 | self.label_zpl = response[:label] 68 | self.tracking_number = response[:tracking_number] 69 | 70 | save! 71 | end 72 | end 73 | 74 | def handle_label_error!(err) 75 | Rails.logger.warn("** Labeler error: #{err.inspect}") 76 | raise Utilities::LabelError, err.message 77 | end 78 | 79 | def acceptable_errors 80 | [Utilities::LabelError] 81 | end 82 | 83 | def update_shipment_tracking_number 84 | return if tracking_number.blank? 85 | 86 | shipment.tracking = tracking_number 87 | shipment.save! if shipment.changed? 88 | end 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /data/test_label.zpl: -------------------------------------------------------------------------------- 1 | ^XA^CF,0,0,0^PR12^MD30^PW800^POI^CI13^LH0,20 2 | ^FO12,139^GB753,2,2^FS 3 | ^FO12,405^GB777,2,2^FS 4 | ^FO464,8^GB2,129,2^FS 5 | ^FO32,10^AdN,0,0^FWN^FH^FDORIGIN ID:HKAA ^FS 6 | ^FO224,10^AdN,0,0^FWN^FH^FD(901) 754-9100^FS 7 | ^FO32,28^AdN,0,0^FWN^FH^FDSHIPPING DEPT^FS 8 | ^FO32,46^AdN,0,0^FWN^FH^FDLR^FS 9 | ^FO32,64^AdN,0,0^FWN^FH^FD7730 TRINITY RD, SUITE 110^FS 10 | ^FO32,82^AdN,0,0^FWN^FH^FD^FS 11 | ^FO32,100^AdN,0,0^FWN^FH^FDCORDOVA, TN 38018^FS 12 | ^FO32,118^AdN,0,0^FWN^FH^FDUNITED STATES US^FS 13 | ^FO478,46^AdN,0,0^FWN^FH^FDCAD: 8030215/WSXI0100^FS 14 | ^FO8,151^A0N,21,21^FWN^FH^FDTO^FS 15 | ^FO37,233^A0N,38,38^FWN^FH^FD5107 N KENMORE AVE^FS 16 | ^FO37,275^A0N,38,38^FWN^FH^FDAPT 4S^FS 17 | ^FO37,317^A0N,43,40^FWN^FH^FDCHICAGO IL 60640^FS 18 | ^FO28,747^A0N,24,24^FWN^FH^FDTRK#^FS 19 | ^FO28,805^A0N,27,32^FWN^FH^FD^FS 20 | ^FO136,717^A0N,27,36^FWN^FH^FD^FS 21 | ^FO37,149^A0N,38,38^FWN^FH^FDWILL GLYNN^FS 22 | ^FO35,359^A0N,21,21^FWN^FH^FD(773) 968-9811^FS 23 | ^FO37,191^A0N,38,38^FWN^FH^FD^FS 24 | ^FO677,511^GB104,10,10^FS 25 | ^FO677,521^GB10,112,10^FS 26 | ^FO771,521^GB10,112,10^FS 27 | ^FO677,633^GB104,10,10^FS 28 | ^FO652,449^A0N,43,58^FWN^FH^FDFedEx^FS 29 | ^FO708,488^A0N,19,26^FWN^FH^FDExpress^FS 30 | ^FO697,529^A0N,128,137^FWN^FH^FDE^FS 31 | ^FO21,416^BY2,3^BCN,25,N,N,N^FWN^FD75107 N KENMORE AVE^FS 32 | ^FO21,449^BY2,2^B7N,10,5,14^FH^FWN^FH^FD[)>_1E01_1D0260640_1D840_1D05_1D7972760443430201_1DFDE_1D389858480_1D333_1D_1D1/1_1D3.3LB_1DN_1D5107 N Kenmore Ave_1DChicago_1DIL_1DWill Glynn_1E06_1D10ZED006_1D12Z7739689811_1D31Z36064039510025627972760443432019_1D32Z10_1D33Z _1D34Z01_1D14ZApt 4S_1D15Z8030215_1D20Z_1C _1D26Z1518_1C_1D_1E_04^FS 33 | ^FO478,100^AdN,0,0^FWN^FH^FDBILL SENDER^FS 34 | ^FO12,694^GB777,2,2^FS 35 | ^FO494,890^A0N,43,43^FWN^FH^FD^FS 36 | ^FO791,120^AbN,11,7^FWB^FH^FD51AG1/D5E6/1A9E^FS 37 | ^FO95,751^A0N,53,40^FWN^FH^FD7972 7604 4343^FS 38 | ^FO409,700^A0N,51,38^FWN^FH^FB390,,,R,^FD MON - 02 DEC AA^FS 39 | ^FO309,752^A0N,51,38^FWN^FH^FB490,,,R,^FD STANDARD OVERNIGHT^FS 40 | ^FO413,804^A0N,40,40^FWN^FH^FB386,,,R,^FD DSR^FS 41 | ^FO495,846^A0N,44,44^FWN^FH^FB298,,,R,^FD 60640^FS 42 | ^FO574,906^A0N,24,24^FWN^FH^FB120,,,R,^FD IL-US^FS 43 | ^FO695,890^A0N,43,43^FWN^FH^FB100,,,R,^FDORD^FS 44 | ^FO39,932^A0N,27,32^FWN^FH^FD^FS 45 | ^FO75,993^BY3,2^BCN,200,N,N,N,N^FWN^FD>;36064039510025627972760443432019^FS 46 | ^FO28,842^A0N,107,96^FWN^FH^FDSE NBUA ^FS 47 | ^FO790,513^A0N,13,18^FWB^FH^FDJ13201306280126^FS 48 | ^FO478,10^AdN,0,0^FWN^FH^FDSHIP DATE: 29NOV13^FS 49 | ^FO478,28^AdN,0,0^FWN^FH^FDACTWGT: 3.3 LB^FS 50 | ^FO478,64^AdN,0,0^FWN^FH^FDDIMS: 12x12x8 IN^FS 51 | ^FO328,364^AbN,11,7^FWN^FH^FDREF: ^FS 52 | ^FO38,378^AbN,11,7^FWN^FH^FDINV: ^FS 53 | ^FO38,392^AbN,11,7^FWN^FH^FDPO: ORDER 249902^FS 54 | ^FO428,392^AbN,11,7^FWN^FH^FDDEPT: ^FS 55 | ^FO25,768^GB58,1,1^FS 56 | ^FO25,768^GB1,26,1^FS 57 | ^FO83,768^GB1,26,1^FS 58 | ^FO25,794^GB58,1,1^FS 59 | ^FO31,774^AdN,0,0^FWN^FH^FD0201^FS 60 | ^PQ1 61 | ^XZ 62 | 63 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | # Heroku uses latest patch level and blows up if you specify it explicitly 4 | ruby "2.1.1" 5 | 6 | gem "rails", "4.0.3" 7 | 8 | # app server 9 | gem "puma", "~> 2.7.1" 10 | 11 | # database 12 | gem "pg", "~> 0.17.1" 13 | 14 | # View stuff 15 | gem 'bootstrap-sass', "~> 3.0.3.0" 16 | gem "decent_exposure", "~> 2.3.0" 17 | gem "draper", "~> 1.3.0" 18 | gem "font-awesome-sass", "~> 4.0.2" 19 | gem "haml-rails", "~> 0.5.3" 20 | gem "nested_form", "~> 0.3.2" 21 | gem "sass-rails", "~> 4.0.0" 22 | 23 | # Use Uglifier as compressor for JavaScript assets 24 | gem "uglifier", ">= 1.3.0" 25 | 26 | # Use CoffeeScript for .js.coffee assets and views 27 | gem "coffee-rails", "~> 4.0.0" 28 | 29 | # See https://github.com/sstephenson/execjs#readme for more supported runtimes 30 | # gem "therubyracer", platforms: :ruby 31 | 32 | # Turbolinks makes following links in your web application faster. Read more: https://github.com/rails/turbolinks 33 | gem "turbolinks" 34 | 35 | # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder 36 | gem "jbuilder", "~> 1.2" 37 | 38 | # spree 39 | gem "spree", github: "spree/spree", branch: "2-1-stable" 40 | #gem "spree_gateway", github: "spree/spree_gateway", branch: "2-1-stable" 41 | #gem "spree_auth_devise", github: "spree/spree_auth_devise", branch: "2-1-stable" 42 | 43 | # shipping / labels 44 | gem "spree_active_shipping", github: "spree/spree_active_shipping", branch: "2-1-stable" 45 | gem "fedex", "~> 3.4.0" 46 | 47 | # local 48 | group :development, :test do 49 | gem "active_record_sampler_platter", "~> 0.1.0" 50 | gem "better_errors", "~> 0.9.0" 51 | gem "binding_of_caller", "~> 0.7.2" 52 | gem "capybara-webkit", "~> 1.1.1" 53 | gem "capybara-screenshot", "~> 0.3.16" 54 | gem "database_cleaner", "~> 1.0.1" 55 | gem "dotenv", "~> 0.9.0" 56 | gem "factory_girl_rails", "~> 4.2.1" 57 | gem "ffaker", "~> 1.22.1" 58 | gem "guard-rspec", "~> 4.0.3" 59 | gem "growl", "~> 1.0.3" 60 | gem "pry", "~> 0.9.12.3" 61 | gem "pry-doc", "~> 0.4.6" 62 | gem "pry-rails", "~> 0.3.2" 63 | gem "rspec-rails", "~> 2.14.0" 64 | gem "rspec-pride", "~> 2.2.0" 65 | gem "shoulda-matchers", "~> 2.1.0" 66 | gem "spring", "~> 1.1.0" 67 | gem "spring-commands-rspec", "~> 1.0.1" 68 | gem "xray-rails", "~> 0.1.6" 69 | end 70 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | WorkingTitle::Application.routes.draw do 2 | 3 | # This line mounts Spree's routes at the root of your application. 4 | # This means, any requests to URLs such as /products, will go to Spree::ProductsController. 5 | # If you would like to change where this engine is mounted, simply change the :at option to something different. 6 | # 7 | # We ask that you don't use the :as option here, as Spree relies on it being the default of "spree" 8 | mount Spree::Core::Engine, :at => '/' 9 | # The priority is based upon order of creation: first created -> highest priority. 10 | # See how all your routes lay out with "rake routes". 11 | 12 | namespace :admin do 13 | 14 | resource :label_printing_config, controller: "label_printing_config", only: [:edit, :update] do 15 | member do 16 | post :generate_new_key 17 | end 18 | end 19 | 20 | resources :orders do 21 | resource :shipment, :controller => "orders/shipments", :only => [:update, :edit, :show, :print] do 22 | member do 23 | post :print 24 | post :print_product_labels 25 | end 26 | end 27 | end 28 | 29 | resources :products do 30 | member do 31 | post :print_label 32 | end 33 | end 34 | end 35 | 36 | 37 | # You can have the root of your site routed with "root" 38 | # root 'welcome#index' 39 | 40 | # Example of regular route: 41 | # get 'products/:id' => 'catalog#view' 42 | 43 | # Example of named route that can be invoked with purchase_url(id: product.id) 44 | # get 'products/:id/purchase' => 'catalog#purchase', as: :purchase 45 | 46 | # Example resource route (maps HTTP verbs to controller actions automatically): 47 | # resources :products 48 | 49 | # Example resource route with options: 50 | # resources :products do 51 | # member do 52 | # get 'short' 53 | # post 'toggle' 54 | # end 55 | # 56 | # collection do 57 | # get 'sold' 58 | # end 59 | # end 60 | 61 | # Example resource route with sub-resources: 62 | # resources :products do 63 | # resources :comments, :sales 64 | # resource :seller 65 | # end 66 | 67 | # Example resource route with more complex sub-resources: 68 | # resources :products do 69 | # resources :comments 70 | # resources :sales do 71 | # get 'recent', on: :collection 72 | # end 73 | # end 74 | 75 | # Example resource route with concerns: 76 | # concern :toggleable do 77 | # post 'toggle' 78 | # end 79 | # resources :posts, concerns: :toggleable 80 | # resources :photos, concerns: :toggleable 81 | 82 | # Example resource route within a namespace: 83 | # namespace :admin do 84 | # # Directs /admin/products/* to Admin::ProductsController 85 | # # (app/controllers/admin/products_controller.rb) 86 | # resources :products 87 | # end 88 | end 89 | 90 | Spree::Core::Engine.routes.draw do 91 | namespace :admin do 92 | 93 | resource :label_printing_config, controller: "label_printing_config", only: [:edit, :update] do 94 | member do 95 | post :generate_new_key 96 | end 97 | end 98 | 99 | resources :orders do 100 | resource :shipments, :controller => "orders/shipments", :only => [:update, :edit, :index, :print] do 101 | member do 102 | post :print 103 | post :print_product_labels 104 | end 105 | end 106 | end 107 | 108 | resources :products do 109 | member do 110 | post :print_label 111 | end 112 | end 113 | end 114 | end 115 | -------------------------------------------------------------------------------- /lib/utilities/labeler.rb: -------------------------------------------------------------------------------- 1 | # Generate ZPLII shipping labels for thermal printers. 2 | # Connects to FedEx and pulls in a plaintext label. 3 | # 4 | # Expects a Spree::Shipping::Package 5 | # 6 | module Utilities 7 | class LabelError < Exception; end 8 | 9 | class Labeler 10 | attr_reader :package 11 | def initialize(pkg=nil) 12 | @package = pkg 13 | end 14 | 15 | # Create a shipping label for a given package. Supports Fedex 16 | # 17 | def generate 18 | if package.fedex? 19 | fedex_generate 20 | else 21 | raise LabelError, "Shipping method does not have a valid label generator!" 22 | end 23 | end 24 | 25 | def label_service_name_for_calculator(calculator) 26 | name = calculator.type.gsub(/^Spree::Calculator::Shipping::/, '') 27 | 28 | mappings = { 29 | 'Fedex::PriorityOvernight' => 'PRIORITY_OVERNIGHT', 30 | 'Fedex::StandardOvernight' => 'STANDARD_OVERNIGHT', 31 | 'Fedex::TwoDay' => 'FEDEX_2_DAY', 32 | 'Fedex::ExpressSaver' => 'FEDEX_EXPRESS_SAVER', 33 | 'Fedex::Ground' => 'FEDEX_GROUND', 34 | 'Fedex::GroundHomeDelivery' => 'GROUND_HOME_DELIVERY', 35 | 'Fedex::InternationalEconomy' => 'INTERNATIONAL_ECONOMY', 36 | 37 | 'Usps::FirstClassMailParcels' => 'First', 38 | 'Usps::PriorityMail' => 'Priority', 39 | 'Usps::ExpressMailInternational' => 'ExpressMailInternational', 40 | } 41 | 42 | mappings.fetch(name) 43 | end 44 | 45 | ####################### 46 | # Private Instance Methods 47 | ####################### 48 | 49 | private 50 | # Makes recursive labeling a bit easier to test 51 | def companion_labeler 52 | @companion_memo ||= self.class.new 53 | end 54 | 55 | def fedex_generate 56 | begin 57 | result = self.class.fedex_connection.label({ 58 | packages: [ 59 | { 60 | weight: { value: package.weight, units: "LB" }, 61 | dimensions: { 62 | length: package.length, 63 | width: package.width, 64 | height: package.height, 65 | units: "IN" 66 | } 67 | } 68 | ], 69 | recipient: package.destination.fedex_formatted, 70 | shipper: package.origin.fedex_formatted, 71 | label_specification: { 72 | image_type: "ZPLII", 73 | label_stock_type: "STOCK_4X6.75_LEADING_DOC_TAB", 74 | filename: nil 75 | }, 76 | service_type: service_name, 77 | }) 78 | rescue Exception => err 79 | raise Utilities::LabelError, err.message 80 | end 81 | 82 | { 83 | label: result.image, 84 | tracking_number: result.options[:tracking_number] 85 | } 86 | end 87 | 88 | def service_name 89 | label_service_name_for_calculator(package.shipping_method.calculator) 90 | end 91 | 92 | ####################### 93 | # Private Class Methods 94 | ####################### 95 | 96 | def self.fedex_connection 97 | @fedex_conn_memo ||= ::Fedex::Shipment.new({ 98 | key: Spree::ActiveShipping::Config[:fedex_key], 99 | password: Spree::ActiveShipping::Config[:fedex_password], 100 | meter: Spree::ActiveShipping::Config[:fedex_login], 101 | account_number: Spree::ActiveShipping::Config[:fedex_account], 102 | mode: (test_mode? ? 'test' : 'production') 103 | }) 104 | end 105 | 106 | 107 | def self.test_mode? 108 | !!Spree::ActiveShipping::Config[:test_mode] 109 | end 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /app/views/spree/admin/orders/shipments/edit.haml: -------------------------------------------------------------------------------- 1 | -# Header 2 | - content_for :js_onready do 3 | :plain 4 | WorkingTitle.OrderShipments.initialize() 5 | 6 | = render :partial => 'spree/admin/shared/order_tabs', :locals => { :current => 'Shipment' } 7 | 8 | = content_for :page_title do 9 | %i.icon-arrow-right 10 | Print Labels 11 | 12 | -# Body 13 | = content_for :page_actions do 14 | %li 15 | = button_link_to Spree.t(:back_to_orders_list), admin_orders_path, :icon => 'icon-arrow-left' 16 | 17 | - order.shipments.each do |shipment| 18 | - method_id = shipment.shipping_method.id rescue nil 19 | .shipment-package-form{ data: { method_id: method_id} } 20 | %h4{ align: "center" } Shipment #{shipment.number} 21 | %fieldset.no-border-bottom 22 | %legend{ align: "center" } 23 | #{shipment.shipping_method.name rescue "No shipping method"} 24 | - 25 | to 26 | - printer_name = spree_current_user.label_printer_name || "No printer configured" 27 | = link_to "[#{printer_name}]", edit_user_path(spree_current_user) 28 | - if shipment.ready? and can? :update, shipment 29 | = '-' 30 | - token = spree_current_user.spree_api_key 31 | = link_to 'Ship', ship_api_order_shipment_path(order, shipment, token: token), 32 | method: :put, class: %w[button icon-arrow-right], 33 | remote: true, 34 | class: %w[finalize-shipment-button icon-arrow-right button] 35 | 36 | = nested_form_for shipment, url: { action: "update", shipment_number: shipment.number } do |f| 37 | %fieldset.no-border-top 38 | %table{ id: "packages-#{shipment.number}" } 39 | %thead 40 | %tr 41 | %th.shipping-method-column 42 | Method 43 | %th 44 | Box type 45 | %th 46 | Weight (lbs) 47 | %th 48 | Tracking # 49 | 50 | = f.fields_for :packages, wrapper: false do |package_form| 51 | - obj = package_form.object 52 | %tr.fields 53 | %td.shipping-method-column 54 | - suggested_method = obj.shipping_method || shipment.shipping_method 55 | - if suggested_method 56 | - select_options = options_for_select(Spree::ShippingMethod.all.map { |m| [m.name, m.id] }, suggested_method.id ) 57 | - else 58 | - select_options = options_for_select(Spree::ShippingMethod.all.map { |m| [m.name, m.id] }) 59 | 60 | = package_form.select(:shipping_method_id, select_options, include_blank: true) 61 | %td 62 | = package_form.select(:box_id, Spree::Shipping::Box.all.map { |b| [b.description, b.id] }) 63 | %td.weight-column 64 | = package_form.text_field :weight 65 | %td 66 | - if (trk = obj.tracking_number) && (meth = obj.shipping_method) 67 | - url = meth.build_tracking_url(trk) 68 | = label_tag(:tracking_number, link_to(trk, url)) 69 | - else 70 | = label_tag(:tracking_number, 'n/a') 71 | %td.actions 72 | - if obj 73 | - if spree_current_user.label_printer_name.present? 74 | =link_to("", print_admin_order_shipments_path(package_id: obj.id), 75 | method: :post, remote: true, class: %w[icon-print no-text with-tip]) 76 | - else 77 | =link_to("", "javascript:alert('#{Spree.t(:no_label_printer_yet)}')", 78 | class: %w[red icon-print no-text with-tip]) 79 | 80 | = package_form.link_to_remove("", class: ["delete_item", "icon-trash", "no-text", "with-tip"]) 81 | 82 | %span.offset-by-one 83 | = f.link_to_add(" Add a package", 84 | :packages, data: { target: "#packages-#{shipment.number}" }, class: 'icon-plus') 85 | 86 | .form-buttons.filter-actions.actions 87 | %button.button.icon-refresh{ type: 'submit'} 88 | Update 89 | %span.or 90 | =Spree.t(:or) 91 | = link_to_with_icon 'icon-remove', Spree.t('actions.cancel'), 92 | edit_admin_order_path(shipment.order), class: 'button' 93 | -------------------------------------------------------------------------------- /spec/models/spree/shipping/package_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Spree::Shipping::Package do 4 | context "database fields" do 5 | it { should have_db_column(:id).of_type(:integer) } 6 | it { should have_db_column(:shipment_id).of_type(:integer) } 7 | it { should have_db_column(:shipping_method_id).of_type(:integer) } 8 | it { should have_db_column(:box_id).of_type(:integer) } 9 | it { should have_db_column(:tracking_number).of_type(:string) } 10 | end 11 | 12 | context "database indexes" do 13 | it { should have_db_index(:shipment_id).unique(false) } 14 | end 15 | 16 | context "configuration" do 17 | describe "associations" do 18 | it { should belong_to(:shipment) } 19 | it { should belong_to(:box) } 20 | it { should have_one(:order).through(:shipment) } 21 | end 22 | 23 | describe "validations" do 24 | it { should validate_presence_of :box_id } 25 | it { should validate_presence_of :shipping_method_id } 26 | it { should validate_presence_of :weight } 27 | it { should validate_numericality_of(:weight) } 28 | end 29 | end 30 | 31 | context "public instance methods" do 32 | let(:shipment) { double("shipment").as_null_object } 33 | let(:shipping_method) { double("shipping method").as_null_object } 34 | let(:order) { double("order") } 35 | let(:box) { double("box") } 36 | 37 | before { subject.stub(:shipment).and_return(shipment) } 38 | before { subject.stub(:shipping_method).and_return(shipping_method) } 39 | before { subject.stub(:order).and_return(order) } 40 | before { subject.stub(:box).and_return(box) } 41 | 42 | context "delegates" do 43 | it "should delegate :fedex? to :shipping_method" do 44 | shipping_method.should_receive :fedex? 45 | subject.fedex? 46 | end 47 | 48 | %i[length width height].each do |dimension| 49 | it "should delegate :#{dimension} to :box" do 50 | box.should_receive dimension 51 | subject.send dimension 52 | end 53 | end 54 | end 55 | 56 | describe ".destination" do 57 | after { subject.destination } 58 | 59 | context "shipment has an address" do 60 | before { shipment.stub(:address).and_return(Object.new) } 61 | 62 | it "should return the shipment address" do 63 | shipment.should_receive(:address) 64 | end 65 | end 66 | 67 | context "shipment has no address" do 68 | before { shipment.stub(:address).and_return(nil) } 69 | 70 | it "should try the order :ship_address as a fallback" do 71 | order.should_receive(:ship_address) 72 | end 73 | end 74 | end 75 | 76 | describe ".origin" do 77 | after { subject.origin } 78 | 79 | it "should return the shipment's stock location" do 80 | shipment.should_recive :stock_location 81 | end 82 | end 83 | 84 | describe ".weight_in_ounces" do 85 | context "package has a :weight greater than zero" do 86 | let(:weight) { rand(1..1000) / 10.0 } 87 | before { subject.stub(:weight).and_return weight } 88 | 89 | it "should return :weight * 16.0 rounded up to the nearest 10th" do 90 | raw = weight * 16.0 91 | rounded_up_nearest_tenth = (raw * 10).ceil / 10.0 92 | subject.weight_in_ounces.should == (rounded_up_nearest_tenth) 93 | end 94 | end 95 | end 96 | 97 | describe ".print_label!" do 98 | let(:printer_name) { Faker::Company.bs } 99 | let(:zpl_text) { Faker::Company.bs } 100 | let(:raw_printer) { double("raw printer").as_null_object } 101 | 102 | before { subject.stub :save! } 103 | before { Utilities::RawPrinter.stub(:new) { raw_printer } } 104 | before { subject.stub(:label_zpl).and_return zpl_text } 105 | 106 | after { subject.print_label!(printer_name: printer_name) } 107 | 108 | it "should call :generate_label!" do 109 | subject.should_receive(:generate_label!) 110 | end 111 | 112 | it "should create a new print job with the appropriate label text and printer name" do 113 | raw_printer.should_receive(:print).with(zpl_text) 114 | end 115 | end 116 | end 117 | end 118 | --------------------------------------------------------------------------------