Permalink
Browse files

Add Product Package functionality

Product Packages are a way for products that ship in there own packages to be defined properly for shipping estimates.  It is extremely useful for certain cases like a 200 lb product that ships in 5x 40lb boxes because 200lbs throws a UPS ship error.  This commit includes the ability to enter product packages through the admin interface.

The calculator now includes product packages in it's base calculation.  The user can now enter packages for a particular product and have those packages be used in the base freight calculation.  The calculator checks the products for over weight limit packages and will raise an error if the packages weigh too much.

Signed-off-by: Nathan Lowrie <nate@finelineautomation.com>

Fixes #55
  • Loading branch information...
1 parent 01b98fd commit 186e5576d6c5d57b52d535b0828de8f073046b4c @FineLineAutomation FineLineAutomation committed with radar Jan 22, 2013
View
@@ -2,4 +2,5 @@ spec/test_app
spec/dummy
*.swp
Gemfile.lock
-.rvmrc
+.rvmrc
+.DS_Store
@@ -0,0 +1,18 @@
+module Spree
+ module Admin
+ class ProductPackagesController < ResourceController
+ belongs_to 'spree/product', :find_by => :permalink
+ before_filter :find_packages
+ before_filter :setup_package, :only => [:index]
+
+ private
+ def find_packages
+ @packages = @product.product_packages
+ end
+
+ def setup_package
+ @product.product_packages.build
+ end
+ end
+ end
+end
@@ -189,12 +189,33 @@ def convert_order_to_weights_array(order)
weights.flatten.sort
end
+ def convert_order_to_item_packages_array(order)
+ multiplier = Spree::ActiveShipping::Config[:unit_multiplier]
+ max_weight = get_max_weight(order)
+ packages = []
+
+ order.line_items.each do |line_item|
+ line_item.product_packages.each do |product_package|
+ if product_package.weight <= max_weight or max_weight == 0
+ line_item.quantity.times do |idx|
+ packages << [product_package.weight * multiplier, product_package.length, product_package.width, product_package.height]
+ end
+ else
+ raise Spree::ShippingError.new("#{I18n.t(:shipping_error)}: The maximum per package weight for the selected service from the selected country is #{max_weight} ounces.")
+ end
+ end
+ end
+
+ packages
+ end
+
# Generates an array of Package objects based on the quantities and weights of the variants in the line items
def packages(order)
units = Spree::ActiveShipping::Config[:units].to_sym
packages = []
weights = convert_order_to_weights_array(order)
max_weight = get_max_weight(order)
+ item_specific_packages = convert_order_to_item_packages_array(order)
if max_weight <= 0
packages << Package.new(weights.sum, [], :units => units)
@@ -211,6 +232,10 @@ def packages(order)
packages << Package.new(package_weight, [], :units => units) if package_weight > 0
end
+ item_specific_packages.each do |package|
+ packages << Package.new(package.at(0), [package.at(1), package.at(2), package.at(3)], :units => :imperial)
+ end
+
packages
end
@@ -0,0 +1,4 @@
+# Add product packages relation
+Spree::LineItem.class_eval do
+ has_many :product_packages, :through => :product
+end
@@ -0,0 +1,7 @@
+# Add product packages relation
+Spree::Product.class_eval do
+ has_many :product_packages, :dependent => :destroy
+
+ attr_accessible :product_packages_attributes
+ accepts_nested_attributes_for :product_packages, :allow_destroy => true, :reject_if => lambda { |pp| pp[:weight].blank? or Integer(pp[:weight]) < 1 }
+end
@@ -0,0 +1,9 @@
+module Spree
+ class ProductPackage < ActiveRecord::Base
+ belongs_to :product
+
+ validates :length, :width, :height, :weight, :numericality => { :only_integer => true, :message => I18n.t('validation.must_be_int'), :greater_than => 0 }
+
+ attr_accessible :length, :width, :height, :weight
+ end
+end
@@ -0,0 +1,6 @@
+Deface::Override.new(:virtual_path => "spree/admin/shared/_product_tabs",
+ :name => "add_product_packages_tab",
+ :insert_bottom => "[data-hook='admin_product_tabs']",
+ :text => "<li<%== ' class=\"active\"' if current == 'Product Packages' %>>
+ <%= link_to t(:product_packages), admin_product_product_packages_url(@product) %>
+ </li>")
@@ -0,0 +1,50 @@
+<%= render :partial => 'spree/admin/shared/product_sub_menu' %>
+
+<%= render :partial => 'spree/admin/shared/product_tabs', :locals => { :current => 'Product Packages' } %>
+
+<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @product } %>
+
+<%= form_for @product, :url => admin_product_url(@product), :method => :put do |f| %>
+ <table class="index">
+ <thead>
+ <tr data-hook="product_packages_header">
+ <th><%= t(:length) %></th>
+ <th><%= t(:width) %></th>
+ <th><%= t(:height) %></th>
+ <th><%= t(:weight) %></th>
+ <th><%= t(:action) %></th>
+ </tr>
+ </thead>
+ <tbody id="product_packages" data-hook>
+ <%= f.fields_for :product_packages do |pp_form| %>
+ <tr class="product_package fields" data-hook="product_package">
+ <td class='length'>
+ <%= pp_form.text_field :length %>
+ </td>
+ <td class='width'>
+ <%= pp_form.text_field :width %>
+ </td>
+ <td class='height'>
+ <%= pp_form.text_field :height %>
+ </td>
+ <td class='weight'>
+ <%= pp_form.text_field :weight %>
+ </td>
+ <td class="actions">
+ <%= link_to_remove_fields t(:remove), pp_form %>
+ </td>
+ </tr>
+ <% end %>
+ </tbody>
+ </table>
+
+ <%= hidden_field_tag 'clear_product_packages', 'true' %>
+
+ <p class="add_product_packages" data-hook="add_product_packages">
+ <%= link_to_add_fields t(:add_product_packages), 'tbody#product_packages' %>
+ </p>
+ <div id="prototypes" data-hook></div>
+ <%= image_tag 'spinner.gif', :plugin => 'spree', :style => 'display:none;', :id => 'busy_indicator' %>
+ <br />
+ <%= render :partial => 'spree/admin/shared/edit_resource_links' %>
+<% end %>
@@ -0,0 +1,12 @@
+class CreateProductPackages < ActiveRecord::Migration
+ def change
+ create_table :spree_product_packages do |t|
+ t.integer "product_id", :null => false
+ t.integer "length", :default => 0, :null => false
+ t.integer "width", :default => 0, :null => false
+ t.integer "height", :default => 0, :null => false
+ t.integer "weight", :default => 0, :null => false
+ t.timestamps
+ end
+ end
+end
View
@@ -58,3 +58,5 @@ en:
parcel_surface: "Canada Post Parcel Surface"
small_packets_surface: "Canada Post Small Packets Surface"
shipping_error: "Shipping Error"
+ product_packages: "Product Packages"
+ add_product_packages: "Add Product Packages"
View
@@ -1,5 +1,9 @@
Spree::Core::Engine.routes.draw do
namespace :admin do
resource :active_shipping_settings, :only => ['show', 'update', 'edit']
+
+ resources :products do
+ resources :product_packages
+ end
end
end
@@ -7,8 +7,8 @@ module ActiveShipping
let(:country) { mock_model Spree::Country, :iso => "US", :state => mock_model(Spree::State, :abbr => "MD") }
let(:address) { mock_model Spree::Address, :country => country, :state => country.state, :city => "Chevy Chase", :zipcode => "20815" }
- let(:line_item_1) { mock_model(Spree::LineItem, :variant_id => 1, :quantity => 2, :variant => mock_model(Spree::Variant, :weight => 10)) }
- let(:line_item_2) { mock_model(Spree::LineItem, :variant_id => 2, :quantity => 1, :variant => mock_model(Spree::Variant, :weight => 5.25)) }
+ let(:line_item_1) { mock_model(Spree::LineItem, :variant_id => 1, :quantity => 2, :variant => mock_model(Spree::Variant, :weight => 10), :product_packages => []) }
+ let(:line_item_2) { mock_model(Spree::LineItem, :variant_id => 2, :quantity => 1, :variant => mock_model(Spree::Variant, :weight => 5.25), :product_packages => []) }
let(:order) { mock_model Spree::Order, :number => "R12345", :ship_address => address, :line_items => [ line_item_1, line_item_2 ] }
let(:carrier) { Spree::ActiveShipping::BogusCarrier.new }
@@ -8,16 +8,20 @@ module ActiveShipping
let(:address) { mock_model Spree::Address, :country => country, :state_name => country.state_name, :city => "Montreal", :zipcode => "H2B", :state => nil }
let(:usa) { mock_model Spree::Country, :iso => "US", :state => mock_model(Spree::State, :abbr => "MD") }
let(:us_address) { mock_model Spree::Address, :country => usa, :state => usa.state, :city => "Chevy Chase", :zipcode => "20815" }
- let(:line_item_1) { mock_model(Spree::LineItem, :variant_id => 1, :quantity => 10, :variant => mock_model(Spree::Variant, :weight => 20.0)) }
- let(:line_item_2) { mock_model(Spree::LineItem, :variant_id => 2, :quantity => 4, :variant => mock_model(Spree::Variant, :weight => 5.25)) }
- let(:line_item_3) { mock_model(Spree::LineItem, :variant_id => 3, :quantity => 1, :variant => mock_model(Spree::Variant, :weight => 29.0)) }
- let(:line_item_4) { mock_model(Spree::LineItem, :variant_id => 4, :quantity => 1, :variant => mock_model(Spree::Variant, :weight => 100.0)) }
- let(:line_item_5) { mock_model(Spree::LineItem, :variant_id => 5, :quantity => 1, :variant => mock_model(Spree::Variant, :weight => 0.0)) }
- let(:line_item_6) { mock_model(Spree::LineItem, :variant_id => 5, :quantity => 1, :variant => mock_model(Spree::Variant, :weight => -1.0)) }
+ let(:line_item_1) { mock_model(Spree::LineItem, :variant_id => 1, :quantity => 10, :variant => mock_model(Spree::Variant, :weight => 20.0), :product_packages => []) }
+ let(:line_item_2) { mock_model(Spree::LineItem, :variant_id => 2, :quantity => 4, :variant => mock_model(Spree::Variant, :weight => 5.25), :product_packages => []) }
+ let(:line_item_3) { mock_model(Spree::LineItem, :variant_id => 3, :quantity => 1, :variant => mock_model(Spree::Variant, :weight => 29.0), :product_packages => []) }
+ let(:line_item_4) { mock_model(Spree::LineItem, :variant_id => 4, :quantity => 1, :variant => mock_model(Spree::Variant, :weight => 100.0), :product_packages => []) }
+ let(:line_item_5) { mock_model(Spree::LineItem, :variant_id => 5, :quantity => 1, :variant => mock_model(Spree::Variant, :weight => 0.0), :product_packages => []) }
+ let(:line_item_6) { mock_model(Spree::LineItem, :variant_id => 5, :quantity => 1, :variant => mock_model(Spree::Variant, :weight => -1.0), :product_packages => []) }
+ let(:package_1) { mock_model(Spree::ProductPackage, :length => 12, :width => 24, :height => 47, :weight => 36) }
+ let(:package_2) { mock_model(Spree::ProductPackage, :length => 6, :width => 6, :height => 51, :weight => 43) }
+ let(:line_item_7) { mock_model(Spree::LineItem, :variant_id => 3, :quantity => 2, :variant => mock_model(Spree::Variant, :weight => 29.0), :product_packages => [ package_1, package_2 ]) }
let(:order) { mock_model Spree::Order, :number => "R12345", :ship_address => address, :line_items => [ line_item_1, line_item_2, line_item_3 ] }
let(:us_order) { mock_model Spree::Order, :number => "R12347", :ship_address => us_address, :line_items => [ line_item_1, line_item_2, line_item_3 ] }
let(:too_heavy_order) { mock_model Spree::Order, :number => "R12349", :ship_address => us_address, :line_items => [ line_item_3, line_item_4 ] }
let(:order_with_invalid_weights) { mock_model Spree::Order, :number => "R12350", :ship_address => us_address, :line_items => [ line_item_5, line_item_6 ] }
+ let(:order_with_packages) { mock_model Spree::Order, :number => "R12345", :ship_address => address, :line_items => [ line_item_2, line_item_7 ] }
let(:international_calculator) { Spree::Calculator::Usps::PriorityMailInternational.new }
@@ -99,5 +103,14 @@ module ActiveShipping
weights.should == [0, 0]
end
end
+
+ describe "adds item packages" do
+ it "should add item packages to weight calculation" do
+ packages = domestic_calculator.send :packages, order_with_packages
+ packages.size.should == 6
+ packages.map{|package| package.weight.amount}.should == [21, 58, 36, 36, 43, 43].map{|x| x*Spree::ActiveShipping::Config[:unit_multiplier]}
+ packages.map{|package| package.weight.unit}.uniq.should == [:ounces]
+ end
+ end
end
end

0 comments on commit 186e557

Please sign in to comment.