@@ -1,21 +1,13 @@
<div data-hook="admin_shipping_method_form_fields" class="alpha twelve columns">
<div class="alpha four columns">
<div class="alpha six columns">
<%= f.field_container :name do %>
<%= f.label :name, t(:name) %><br />
<%= f.text_field :name, :class => 'fullwidth' %>
<%= error_message_on :shipping_method, :name %>
<% end %>
</div>

<div class="four columns">
<%= f.field_container :zone_id do %>
<%= f.label :zone_id, t(:zone) %><br />
<%= f.collection_select(:zone_id, @available_zones, :id, :name, {}, {:class => 'select2 fullwidth'}) %>
<%= error_message_on :shipping_method, :zone_id %>
<% end %>
</div>

<div class="omega four columns">
<div class="omega six columns">
<%= f.field_container :display_on do %>
<%= f.label :display_on, t(:display) %><br />
<%= select(:shipping_method, :display_on, Spree::ShippingMethod::DISPLAY.collect { |display| [t(display), display == :both ? nil : display.to_s] }, {}, {:class => 'select2 fullwidth'}) %>
@@ -32,25 +24,32 @@
</div>
</div>

<div data-hook="admin_shipping_method_form_availability_fields" class="alpha six columns">
<fieldset class="categories no-border-bottom">
<legend align="center"><%= t(:availability) %></legend>

<%= f.field_container :shipping_category do %>
<%= f.label :shipping_category, t(:shipping_category_choose) %>
<%= select(:shipping_method, :shipping_category_id, Spree::ShippingCategory.all.collect { |s| [s.name, s.id] }, { :include_blank => "None" }, {:class => 'select2 fullwidth'}) %>
<% end %>

<div class="field">
<%= label_tag t(:match_rule) %>
<ul>
<li><%= f.check_box :match_none %> <%= f.label :match_none, t('match_choices.none') %></li>
<li><%= f.check_box :match_one %> <%= f.label :match_one, t('match_choices.one') %></li>
<li><%= f.check_box :match_all %> <%= f.label :match_all, t('match_choices.all') %></li>
</ul>
</div>
<div class="alpha six columns">
<div data-hook="admin_shipping_method_form_availability_fields" class="alpha six columns">
<fieldset class="categories no-border-bottom">
<legend align="center"><%= t(:categories) %></legend>
<%= f.field_container :categories do %>
<% Spree::ShippingCategory.all.each do |category| %>
<%= f.label :shipping_category_id, category.name %>
<%= check_box_tag('shipping_method[shipping_categories][]', category.id, @shipping_method.shipping_categories.include?(category)) %><br>
<% end %>
<%= error_message_on :shipping_method, :shipping_category_id %>
<% end %>
</fieldset>
</div>

</fieldset>
<div class="alpha six columns">
<fieldset class="no-border-bottom">
<legend align="center"><%= t(:zones) %></legend>
<%= f.field_container :zones do %>
<% Spree::Zone.all.each do |zone| %>
<%= f.label :zone_id, zone.name %>
<%= check_box_tag('shipping_method[zones][]', zone.id, @shipping_method.zones.include?(zone)) %><br>
<% end %>
<%= error_message_on :shipping_method, :zone_id %>
<% end %>
</fieldset>
</div>
</div>

<div data-hook="admin_shipping_method_form_calculator_fields" class="omega six columns">
@@ -9,7 +9,6 @@
<%= button_link_to t(:new_shipping_method), new_object_url, :icon => 'icon-plus', :id => 'admin_new_shipping_method_link' %>
</li>
<% end %>
<% if @shipping_methods.any? %>
<table class="index" id='listing_shipping_methods'>
<colgroup>
@@ -32,7 +31,7 @@
<% @shipping_methods.each do |shipping_method|%>
<tr id="<%= spree_dom_id shipping_method %>" data-hook="admin_shipping_methods_index_rows" class="<%= cycle('odd', 'even')%>">
<td><%= shipping_method.name %></td>
<td><%= shipping_method.zone.name if shipping_method.zone %></td>
<td><%= shipping_method.zones.collect(&:name).join(",") if shipping_method.zones %></td>
<td><%= shipping_method.calculator.description %></td>
<td class="align-center"><%= shipping_method.display_on.blank? ? t(:both) : t(shipping_method.display_on) %></td>
<td data-hook="admin_shipping_methods_index_row_actions" class="actions">
@@ -41,8 +40,22 @@
</td>
</tr>
<% end %>
</tbody>
</table>
</thead>
<tbody>
<% @shipping_methods.each do |shipping_method|%>
<tr id="<%= spree_dom_id shipping_method %>" data-hook="admin_shipping_methods_index_rows" class="<%= cycle('odd', 'even')%>">
<td><%= shipping_method.name %></td>
<td><%= shipping_method.zones.collect(&:name).join(", ") if shipping_method.zones %></td>
<td><%= shipping_method.calculator.description %></td>
<td class="align-center"><%= shipping_method.display_on.blank? ? t(:both) : t(shipping_method.display_on) %></td>
<td data-hook="admin_shipping_methods_index_row_actions" class="actions">
<%= link_to_edit shipping_method, :no_text => true %>
<%= link_to_delete shipping_method, :no_text => true %>
</td>
</tr>
<% end %>
</tbody>
</table>
<% else %>
<div class="alpha twelve columns no-objects-found"><%= t(:no_shipping_methods_found) %></div>
<% end %>
@@ -0,0 +1,85 @@
<div data-hook="admin_stock_locations_form_fields" class="row">
<div class="row">
<div class="alpha twelve columns">
<%= f.field_container :name do %>
<%= f.label :name, t(:name) %>
<%= f.text_field :name, :class => 'fullwidth' %>
<% end %>
</div>
<div class="omega four columns">
<%= f.field_container :active do %>
<label for="active"><%= t(:state) %></label>
<ul>
<li>
<%= f.label :active, t(:active) + ':' %>
<%= f.check_box :active %>
</li>
</ul>
<% end %>
</div>
</div>

<div class="row">
<div class="alpha four columns">
<div class="field ">
<%= f.label :address1, t(:street_address) + ':' %>
<%= f.text_field :address1, :class => 'fullwidth' %>
</div>
</div>

<div class="four columns">
<div class="field ">
<%= f.label :address2, t(:street_address_2) + ':' %>
<%= f.text_field :address2, :class => 'fullwidth' %>
</div>
</div>
<div class="four columns">
<div class="field ">
<%= f.label :city, t(:city) + ':' %>
<%= f.text_field :city, :class => 'fullwidth' %>
</div>
</div>

<div class="omega four columns">
<div class="field ">
<%= f.label :zipcode, t(:zip) + ':' %>
<%= f.text_field :zipcode, :class => 'fullwidth' %>
</div>
</div>
</div>

<div class="row">
<div class="alpha eight columns">
<div class="field ">
<%= f.label :country_id, t(:country) + ':' %>
<span id="country"><%= f.collection_select :country_id, available_countries, :id, :name, {}, {:class => 'select2 fullwidth'} %></span>
</div>
</div>

<div class="four columns">
<div class="field ">
<%= f.label :state_id, t(:state) + ':' %>
<span id="state" class="region">
<%= f.text_field :state_name, :style => "display: #{f.object.country.states.empty? ? 'block' : 'none' };", :disabled => !f.object.country.states.empty?, :class => 'fullwidth state_name' %>
<%= f.collection_select :state_id, f.object.country.states.sort, :id, :name, {:include_blank => true}, {:class => 'select2 fullwidth', :style => "display: #{f.object.country.states.empty? ? 'none' : 'block' };", :disabled => f.object.country.states.empty?} %>
</span>
</div>
</div>

<div class="omega four columns">
<div class="field ">
<%= f.label :phone, t(:phone) + ':' %>
<%= f.phone_field :phone, :class => 'fullwidth' %>
</div>
</div>
</div>
</div>

<% content_for :head do %>
<%= javascript_include_tag 'admin/address_states.js' %>
<%= javascript_tag do -%>
$(document).ready(function(){
$('span#country .select2').on('change', function() { update_state(''); });
});
<% end -%>
<% end %>
@@ -0,0 +1,16 @@
<% content_for :page_title do %>
<%= t(:editing_stock_location) %> <i class="icon-arrow-right"></i> <%= @stock_location.name %>
<% end %>
<% content_for :page_actions do %>
<li><%= link_to_with_icon 'icon-arrow-left', t(:back_to_stock_locations_list), admin_stock_locations_path, :class => 'button' %></li>
<% end %>
<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @stock_location } %>
<%= form_for [:admin, @stock_location] do |f| %>
<fieldset class="no-border-top">
<%= render :partial => 'form', :locals => { :f => f } %>
<%= render :partial => 'spree/admin/shared/edit_resource_links' %>
</fieldset>
<% end %>
@@ -0,0 +1,46 @@
<%= render :partial => 'spree/admin/shared/configuration_menu' %>
<% content_for :page_title do %>
<%= t(:stock_locations) %>
<% end %>
<% content_for :page_actions do %>
<ul class="actions inline-menu">
<li>
<%= button_link_to t(:new_stock_location), new_object_url, { :icon => 'icon-plus', :id => 'admin_new_stock_location' } %>
</li>
</ul>
<% end %>

<table class="index" id='listing_stock_locations' data-hook>
<colgroup>
<col style="width: 50%" />
<col style="width: 15%" />
<col style="width: 20%" />
<col style="width: 15%" />
</colgroup>
<thead>
<tr data-hook="stock_locations_header">
<th><%= t(:name) %></th>
<th><%= t(:state) %></th>
<th><%= t(:stock_movements) %></th>
<th class="actions"></th>
</tr>
</thead>
<tbody>
<% @stock_locations.each do |stock_location|
@edit_url = edit_admin_stock_location_path(stock_location)
@delete_url = admin_stock_location_path(stock_location)
%>
<tr id="<%= spree_dom_id stock_location %>" data-hook="stock_location_row" class="<%= cycle('odd', 'even')%>">
<td><%= stock_location.name %></td>
<td><span class="state <%= stock_location.active? ? 'active' : 'inactive' %>"><%= stock_location.active? ? 'active' : 'inactive' %></span></td>
<td><%= link_to t(:stock_movements), admin_stock_location_stock_movements_path(stock_location.id) %> </td>
<td class="actions">
<%= link_to_edit stock_location, :no_text => true %>
<%= link_to_delete stock_location, :no_text => true %>
</td>
</tr>
<% end %>
</tbody>
</table>
@@ -0,0 +1,16 @@
<% content_for :page_title do %>
<%= t(:new_stock_location) %>
<% end %>
<% content_for :page_actions do %>
<li><%= link_to_with_icon 'icon-arrow-left', t(:back_to_stock_locations_list), admin_stock_locations_path, :class => 'button' %></li>
<% end %>
<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @stock_locations } %>
<% @stock_location.country = Spree::Country.find(Spree::Config[:default_country_id]) %>
<%= form_for [:admin, @stock_location] do |f| %>
<fieldset class="no-border-top">
<%= render :partial => 'form', :locals => { :f => f } %>
<%= render :partial => 'spree/admin/shared/new_resource_links' %>
</fieldset>
<% end %>
@@ -0,0 +1,13 @@
<div data-hook="admin_stock_movements_form_fields" class="row">
<div class="alpha four columns">
<%= f.field_container :quantity do %>
<%= f.label :quantity, t(:quantity) %>
<%= f.text_field :quantity, :class => 'fullwidth' %>
<% end %>
<%= f.field_container :stock_item_id do %>
<%= f.label :stock_item_id, t(:stock_item_id) %>
<%= collection_select 'stock_movement', 'stock_item_id', stock_location.stock_items, :id, :variant_name,
{ selected: @stock_movement.stock_item_id }, class: 'select2 fullwidth' %>
<% end %>
</div>
</div>
@@ -0,0 +1,16 @@
<% content_for :page_title do %>
<%= t(:editing_stock_movement) %>
<% end %>
<% content_for :page_actions do %>
<li><%= link_to_with_icon 'icon-arrow-left', t(:back_to_stock_movements_list), admin_stock_location_stock_movements_path(stock_location), :class => 'button' %></li>
<% end %>
<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @stock_movement } %>
<%= form_for [:admin, stock_location, @stock_movement] do |f| %>
<fieldset class="no-border-top">
<%= render :partial => 'form', :locals => { :f => f } %>
<%= render :partial => 'spree/admin/shared/new_resource_links', locals: { collection_url: admin_stock_location_stock_movements_path(stock_location) } %>
</fieldset>
<% end %>
@@ -0,0 +1,41 @@
<%= render :partial => 'spree/admin/shared/configuration_menu' %>
<% content_for :page_title do %>
<%= t(:stock_movements_for_stock_location, stock_location_name: @stock_location.name) %>
<% end %>
<% content_for :page_actions do %>
<li>
<%= button_link_to t(:new_stock_movement), new_admin_stock_location_stock_movement_path(@stock_location), icon: 'icon-plus', id: 'admin_new_stock_movement_link' %>
</li>
<% end %>

<table class="index" id='listing_stock_movements'>
<colgroup>
<col style="width: 35%">
<col style="width: 20%">
<col style="width: 45%">
</colgroup>
<thead>
<tr data-hook="admin_stock_movements_index_headers">
<th><%= t(:stock_item) %>
<th><%= t(:quantity) %></th>
<th><%= t(:action) %></th>
<th data-hook="admin_stock_movements_index_header_actions" class="actions"></th>
</tr>
</thead>
<tbody>
<% @stock_movements.each do |stock_movement|%>
<tr id="<%= spree_dom_id stock_movement %>" data-hook="admin_stock_movements_index_rows" class="<%= cycle('odd', 'even')%>">
<td class="align-center"><%= stock_movement.stock_item.variant_name %></td>
<td class="align-center"><%= stock_movement.quantity %></td>
<td class="align-center"><%= stock_movement.action %></td>
<td data-hook="admin_stock_movements_index_row_actions" class="actions">
<%= link_to_with_icon 'icon-edit', t(:edit), edit_admin_stock_location_stock_movement_path(@stock_location, stock_movement), data: { action: 'edit' }, no_text: true %>
<%= link_to_with_icon 'icon-trash', t(:delete), admin_stock_location_stock_movement_path(@stock_location, stock_movement), method: :delete,
data: { confirm: t(:are_you_sure), action: 'remove' }, no_text: true %>
</td>
</tr>
<% end %>
</tbody>
</table>
@@ -0,0 +1,16 @@
<% content_for :page_title do %>
<%= t(:new_stock_movement) %>
<% end %>
<% content_for :page_actions do %>
<li><%= link_to_with_icon 'icon-arrow-left', t(:back_to_stock_movements_list), admin_stock_location_stock_movements_path(stock_location), :class => 'button' %></li>
<% end %>
<%= render :partial => 'spree/shared/error_messages', :locals => { :target => @stock_movement } %>
<%= form_for [:admin, stock_location, @stock_movement] do |f| %>
<fieldset class="no-border-top">
<%= render :partial => 'form', :locals => { :f => f } %>
<%= render :partial => 'spree/admin/shared/new_resource_links', locals: { collection_url: admin_stock_location_stock_movements_path(stock_location) } %>
</fieldset>
<% end %>
@@ -14,7 +14,6 @@

<ul class='variant-data'>
<li class='variant-sku'><strong>{{t 'sku'}}:</strong> {{variant.sku}}</li>
<li class='variant-on_hand'><strong>{{t 'on_hand'}}:</strong> {{variant.count_on_hand}}</li>
</ul>

{{#if variant.option_values}}
@@ -28,3 +27,41 @@
</div>
</div>
</script>

<script type='text/template' id='variant_autocomplete_stock_template'>
<fieldset>
<legend align="center"><%= t(:select_stock) %></legend>
<table class="stock-levels" data-hook="stock-levels">
<colgroup>
<col style="width: 30%;" />
<col style="width: 40%;" />
<col style="width: 20%;" />
<col style="width: 10%;" />
</colgroup>
<thead>
<th><%= t(:location) %></th>
<th><%= t(:count_on_hand) %></th>
<th><%= t(:quantity) %></th>
<th class="actions"></th>
</thead>
<tbody>
{{#each variant.stock_items}}
<tr>
<td>{{this.stock_location_name}}</td>
<td>
{{this.count_on_hand}}
{{#if this.backorderable}}
(backorders allowed)
{{/if}}
</td>
<td>
<input class="quantity" id="stock_item_quantity" data-stock-location-id="{{this.stock_location_id}}" type="number" min="1" value="1">
</td>
<td class="actions">
<button class="add_variant no-text icon-plus icon_link with-tip" data-stock-location-id="{{this.stock_location_id}}" title="Add" data-action="add"></button>
</td>
</tr>
{{/each}}
</tbody>
</fieldset>
</script>
@@ -31,19 +31,6 @@
<%= f.label :cost_price, t(:cost_price) %>
<%= f.text_field :cost_price, :value => number_to_currency(@variant.cost_price, :unit => ''), :class => 'fullwidth' %>
</div>

<% if Spree::Config[:track_inventory_levels] %>
<div class="field checkbox" data-hook="on_hand">
<label>
<%= f.check_box :on_demand %>
<%= t(:on_demand) %>
</label>
</div>
<div class="field" data-hook="on_hand">
<%= f.label :on_hand, t(:on_hand) %>
<%= f.text_field :on_hand, :class => 'fullwidth' %>
</div>
<% end %>
</div>
</div>

@@ -20,7 +20,6 @@
<th colspan="2"><%= t(:options) %></th>
<th><%= t(:price) %></th>
<th><%= t(:sku) %></th>
<th><%= t(:on_hand) %></th>
<th class="actions"></th>
</tr>
</thead>
@@ -35,7 +34,6 @@
<td><%= variant.options_text %></td>
<td class="align-center"><%= variant.display_price %></td>
<td class="align-center"><%= variant.sku %></td>
<td class="align-center"><%= variant.on_hand %></td>
<td class="actions">
<%= link_to_edit(variant, :no_text => true) unless variant.deleted? %>
&nbsp;
@@ -31,8 +31,8 @@
get :shipping_method
end
end

end

get '/cart', :to => 'orders#edit', :as => :cart
put '/cart', :to => 'orders#update', :as => :update_cart
put '/cart/empty', :to => 'orders#empty', :as => :empty_cart
@@ -74,6 +74,7 @@
end
member do
get :clone
get :stock
end
resources :variants do
collection do
@@ -111,10 +112,9 @@
end
end

resource :inventory_settings
resource :image_settings

resources :orders do
resources :orders, :except => [:show] do
member do
put :fire
get :fire
@@ -179,6 +179,11 @@

resources :shipping_methods
resources :shipping_categories
resources :stock_locations do
resources :stock_movements
end
resources :stock_movements
resources :stock_items, :only => :update
resources :tax_rates
resource :tax_settings

This file was deleted.

This file was deleted.

@@ -3,7 +3,7 @@
describe "Shipping Methods" do
stub_authorization!
let!(:zone) { create(:global_zone) }
let!(:shipping_method) { create(:shipping_method, :zone => zone) }
let!(:shipping_method) { create(:shipping_method, :zones => [zone]) }

before(:each) do
# HACK: To work around no email prompting on check out
@@ -48,20 +48,13 @@
click_icon :edit
end

find(:css, ".calculator-settings-warning").should_not be_visible
select('Flexible Rate', :from => 'calc_type')
find(:css, ".calculator-settings-warning").should be_visible

click_button "Update"
page.should_not have_content("Shipping method is not found")
end
end

context "availability", :js => true do
it "can check shipping method match fields" do
click_link "Shipping Methods"
click_link "New Shipping Method"
["none", "one", "all"].each do |type|
field = "shipping_method_match_#{type}"
check field
uncheck field
end
end
end
end
@@ -20,7 +20,7 @@
end

create(:shipping_method, :display_on => "front_end")
create(:order_with_inventory_unit_shipped, :completed_at => "2011-02-01 12:36:15")
create(:order, :state => 'complete', :completed_at => "2011-02-01 12:36:15")
# We need a unique name that will appear for the customer dropdown
ship_address = create(:address, :country => country, :state => state, :first_name => "Rumpelstiltskin")
bill_address = create(:address, :country => country, :state => state, :first_name => "Rumpelstiltskin")
@@ -82,7 +82,7 @@
fill_in "Phone", :with => "123-456-7890"
end

click_button "Continue"
click_button "Update"

click_link "Customer Details"
find_field('order_ship_address_attributes_firstname').value.should == "John 99"
@@ -91,7 +91,7 @@

it "should show validation errors" do
click_link "Customer Details"
click_button "Continue"
click_button "Update"
page.should have_content("Shipping address first name can't be blank")
end

@@ -4,68 +4,83 @@
describe "Order Details" do
stub_authorization!

context "edit order page" do
context "edit order page", :js => true do
after(:each) { I18n.reload! }

let(:product) { create(:product, :name => 'spree t-shirt', :price => 20.00) }
let(:order) { create(:order, :state => 'complete', :completed_at => "2011-02-01 12:36:15", :number => "R100") }
let(:stock_location) { create(:stock_location_with_items) }
let(:shipment) { create(:shipment, :order => order, :stock_location => stock_location) }

before do
configure_spree_preferences do |config|
config.allow_backorders = true
end
create(:country)
create(:shipping_method, :name => "Default")
create(:product, :name => "Tote", :price => 15.00)
order.shipments.create({stock_location_id: stock_location.id}, :without_protection => true)
order.contents.add(product.master, 2)
end

after(:each) { I18n.reload! }

let(:product) { create(:product, :name => 'spree t-shirt', :on_hand => 5, :price => 19.99) }
let(:order) { create(:order, :completed_at => "2011-02-01 12:36:15", :number => "R100") }
it "should allow me to edit order details" do
visit spree.edit_admin_order_path(order)
page.should have_content("spree t-shirt")
page.should have_content("$40.00")

it "should allow me to edit order details", :js => true do
order.add_variant(product.master, 2)
order.inventory_units.each do |iu|
iu.update_attribute_without_callbacks('state', 'sold')
within_row(1) do
click_icon :edit
fill_in "quantity", :with => "1"
end
click_icon :ok
sleep 1
page.should have_content("Total: $20.00")
end

visit spree.admin_path
click_link "Orders"
it "can add an item to a shipment" do
visit spree.edit_admin_order_path(order)

within_row(1) do
click_link "R100"
select2_search "Tote", :from => I18n.t(:name_or_sku)
within("table.stock-levels") do
fill_in "stock_item_quantity", :with => 2
click_icon :plus
end

page.should have_content("spree t-shirt")
page.should have_content("$39.98")
click_link "Edit"
fill_in "order_line_items_attributes_0_quantity", :with => "1"
click_button "Update"
page.should have_content("Total: $19.99")
sleep 1
page.should have_content("Total: $40.00")
end

it "should render details properly" do
order.state = :complete
order.currency = 'GBP'
order.save!

it "can remove an item from a shipment" do
visit spree.edit_admin_order_path(order)
page.should have_content("spree t-shirt")

find(".page-title").text.strip.should == "Order #R100"

within ".additional-info" do
find(".state").text.should == "complete"
find("#shipment_status").text.should == "none"
find("#payment_status").text.should == "none"
within_row(1) do
click_icon :trash
end
sleep 1
page.should_not have_content("spree t-shirt")
end

I18n.backend.store_translations I18n.locale,
:shipment_states => { :missing => 'some text' },
:payment_states => { :missing => 'other text' }

it "can add tracking information" do
visit spree.edit_admin_order_path(order)

within ".additional-info" do
find("#order_total").text.should == "£0.00"
find("#shipment_status").text.should == "some text"
find("#payment_status").text.should == "other text"
within("table.index tr:nth-child(5)") do
click_icon :edit
end
fill_in "tracking", :with => "FOOBAR"
click_icon :ok
sleep 1
page.should have_content("Tracking: FOOBAR")
end

xit "can add change the shipping method" do
order = create(:completed_order_with_totals)
visit spree.edit_admin_order_path(order)
within("table.index tr:nth-child(3)") do
click_icon :edit
end
select2 "Default", :from => "Shipping Method"
click_icon :ok
sleep 1
page.should have_content("Default:")
end
end
end
@@ -3,30 +3,12 @@
describe "Payments" do
stub_authorization!

before(:each) do

configure_spree_preferences do |config|
config.allow_backorders = true
end

@order = create(:completed_order_with_totals, :number => "R100", :state => "complete")
product = create(:product, :name => 'spree t-shirt', :on_hand => 5)
product.master.count_on_hand = 5
product.master.save
@order.add_variant(product.master, 2)
@order.update!

@order.inventory_units.each do |iu|
iu.update_attribute_without_callbacks('state', 'sold')
end
@order.update!

end
let(:order) { create(:completed_order_with_totals, :number => "R100", :state => "complete") }

context "payment methods" do

before(:each) do
create(:payment, :order => @order, :amount => @order.outstanding_balance, :payment_method => create(:bogus_payment_method, :environment => 'test'))
create(:payment, :order => order, :amount => order.outstanding_balance, :payment_method => create(:bogus_payment_method, :environment => 'test'))
visit spree.admin_path
click_link "Orders"
within_row(1) do
@@ -39,7 +21,7 @@
click_link "Payments"
find("#payment_status").text.should == "BALANCE DUE"
within_row(1) do
column_text(2).should == "$49.98"
column_text(2).should == "$50.00"
column_text(3).should == "Credit Card"
column_text(4).should == "CHECKOUT"
end
@@ -49,7 +31,7 @@
page.should have_content("Payment Updated")

within_row(1) do
column_text(2).should == "$49.98"
column_text(2).should == "$50.00"
column_text(3).should == "Credit Card"
column_text(4).should == "VOID"
end
@@ -68,32 +50,32 @@
# Regression test for #1269
it "cannot create a payment for an order with no payment methods" do
Spree::PaymentMethod.delete_all
@order.payments.delete_all
order.payments.delete_all

visit spree.new_admin_order_payment_path(@order)
visit spree.new_admin_order_payment_path(order)
page.should have_content("You cannot create a payment for an order without any payment methods defined.")
page.should have_content("Please define some payment methods first.")
end

# Regression tests for #1453
context "with a check payment" do
before do
@order.payments.delete_all
create(:payment, :order => @order,
order.payments.delete_all
create(:payment, :order => order,
:state => "checkout",
:amount => @order.outstanding_balance,
:amount => order.outstanding_balance,
:payment_method => create(:bogus_payment_method, :environment => 'test'))
end

it "capturing a check payment from a new order" do
visit spree.admin_order_payments_path(@order)
visit spree.admin_order_payments_path(order)
click_icon(:capture)
page.should_not have_content("Cannot perform requested operation")
page.should have_content("Payment Updated")
end

it "voids a check payment from a new order" do
visit spree.admin_order_payments_path(@order)
visit spree.admin_order_payments_path(order)
click_icon(:void)
page.should have_content("Payment Updated")
end
@@ -3,14 +3,13 @@
describe "return authorizations" do
stub_authorization!

let!(:order) { create(:completed_order_with_totals) }
let!(:order) { create(:shipped_order) }

before do
order.inventory_units.update_all("state = 'shipped'")
create(:return_authorization,
:order => order,
:state => 'authorized',
:inventory_units => order.inventory_units)
:inventory_units => order.shipments.first.inventory_units)
end

# Regression test for #1107

This file was deleted.

@@ -59,6 +59,7 @@
create(:option_value)
click_link "Option Types"
within('table#listing_option_types') { click_icon :edit }
page.should have_content("Editing Option Type")
all("tbody#option_values tr").count.should == 1
within("tbody#option_values") do
find('.remove_fields').click
@@ -166,7 +166,6 @@ def build_option_type_with_values(name, values)
fill_in "product_available_on", :with => "2012/01/24"
click_button "Create"
page.should have_content("successfully created!")
fill_in "product_on_hand", :with => "100"
click_button "Update"
page.should have_content("successfully updated!")
end
@@ -182,9 +181,7 @@ def build_option_type_with_values(name, values)
fill_in "product_price", :with => "100"
click_button "Create"
page.should have_content("successfully created!")
fill_in "product_on_hand", :with => ""
click_button "Update"
page.should_not have_content("spree_products.count_on_hand may not be NULL")
page.should have_content("successfully updated!")
end
end
@@ -70,6 +70,7 @@

# Regression test for #2279
specify do
page.should have_content('Editing Product')
fill_in "product_product_properties_attributes_0_property_name", :with => "A Property"
fill_in "product_product_properties_attributes_0_value", :with => "A Value"
click_button "Update"
@@ -0,0 +1,99 @@
require 'spec_helper'

describe "Stock Management" do
stub_authorization!

context "as admin user" do
before(:each) do
visit spree.admin_path
end

context "given a product with a variant and a stock location" do
before do
create(:stock_location, name: 'Default')
@product = create(:product, name: 'apache baseball cap', price: 10)
v = @product.variants.create!(sku: 'FOOBAR')
v.stock_items.first.update_column(:count_on_hand, 10)

click_link "Products"
within_row(1) do
click_icon :edit
end
end

it "can view count on hand for the variant" do
click_link "Stock Management"

within_row(1) do
page.should have_content('Count On Hand')
within(:css, '.stock_location_info') do
column_text(2).should have_content('10')
end
end
end

it "can toggle backorderable for a variant's stock item", js: true do
click_link "Stock Management"

backorderable = find "#stock_item_backorderable"
backorderable.should be_checked

backorderable.set(false)
visit current_path

backorderable.should_not be_checked
end

it "can create a new stock movement", js: true do
click_link "Stock Management"

fill_in "stock_movement_quantity", with: 5
select2 "default", from: "Stock Location"
click_button "Add Stock"

page.should have_content('successfully created')

within(:css, '.stock_location_info table') do
column_text(2).should eq '15'
end
end

it "can create a new negative stock movement", js: true do
click_link "Stock Management"

fill_in "stock_movement_quantity", with: -5
select2 "default", from: "Stock Location"
click_button "Add Stock"

page.should have_content('successfully created')

within(:css, '.stock_location_info table') do
column_text(2).should eq '5'
end
end

context "with multiple variants" do
before do
v = @product.variants.create!(sku: 'SPREEC')
v.stock_items.first.update_column(:count_on_hand, 30)
end

it "can create a new stock movement for the specified variant", js: true do
click_link "Stock Management"
fill_in "stock_movement_quantity", with: 10
select2 "SPREEC", from: "Variant"
click_button "Add Stock"

page.should have_content('successfully created')

within_row(2) do
page.should have_content("SPREEC")
within(:css, '.stock_location_info table') do
column_text(2).should eq '40'
end
end
end
end
end
end
end
@@ -31,19 +31,7 @@ def link_to_cart(text = nil)

# human readable list of variant options
def variant_options(v, options={})
list = v.options_text

# We shouldn't show out of stock if the product is infact in stock
# or when we're not allowing backorders.
unless v.in_stock?
list = if options[:include_style]
content_tag(:span, "(#{t(:out_of_stock)}) #{list}", :class => 'out-of-stock')
else
"#{t(:out_of_stock)} #{list}"
end
end

list
v.options_text
end

def meta_data_tags
@@ -60,7 +60,6 @@ class AppConfiguration < Preferences::Configuration
preference :shipping_instructions, :boolean, :default => false # Request instructions/info for shipping
preference :show_descendents, :boolean, :default => true
preference :show_only_complete_orders_by_default, :boolean, :default => true
preference :show_zero_stock_products, :boolean, :default => true
preference :show_variant_full_price, :boolean, :default => false #Displays variant full price or difference with product price. Default false to be compatible with older behavior
preference :show_products_without_price, :boolean, :default => false
preference :site_name, :string, :default => 'Spree Demo Site'
@@ -0,0 +1,21 @@
require_dependency 'spree/shipping_calculator'

module Spree
module Calculator::Shipping
class FlatPercentItemTotal < ShippingCalculator
preference :flat_percent, :decimal, :default => 0
attr_accessible :preferred_flat_percent

def self.description
I18n.t(:flat_percent)
end

def compute_package(package)
content_items = package.contents
item_total = total(content_items)
value = item_total * BigDecimal(self.preferred_flat_percent.to_s) / 100.0
(value * 100).round.to_f / 100
end
end
end
end
@@ -0,0 +1,20 @@
require_dependency 'spree/shipping_calculator'

module Spree
module Calculator::Shipping
class FlatRate < ShippingCalculator
preference :amount, :decimal, :default => 0
preference :currency, :string, :default => Spree::Config[:currency]

attr_accessible :preferred_amount, :preferred_currency

def self.description
I18n.t(:shipping_flat_rate_per_order)
end

def compute_package(package)
self.preferred_amount
end
end
end
end
@@ -0,0 +1,38 @@
require_dependency 'spree/shipping_calculator'

module Spree
module Calculator::Shipping
class FlexiRate < ShippingCalculator
preference :first_item, :decimal, :default => 0.0
preference :additional_item, :decimal, :default => 0.0
preference :max_items, :integer, :default => 0
preference :currency, :string, :default => Spree::Config[:currency]

attr_accessible :preferred_first_item,
:preferred_additional_item,
:preferred_max_items,
:preferred_currency

def self.description
I18n.t(:shipping_flexible_rate)
end

def compute_package(package)
content_items = package.contents
sum = 0
max = self.preferred_max_items.to_i
items_count = content_items.map(&:quantity).sum
items_count.times do |i|
# check max value to avoid divide by 0 errors
if (max == 0 && i == 0) || (max > 0) && (i % max == 0)
sum += self.preferred_first_item.to_f
else
sum += self.preferred_additional_item.to_f
end
end

sum
end
end
end
end
@@ -0,0 +1,20 @@
require_dependency 'spree/shipping_calculator'

module Spree
module Calculator::Shipping
class PerItem < ShippingCalculator
preference :amount, :decimal, :default => 0
preference :currency, :string, :default => Spree::Config[:currency]
attr_accessible :preferred_amount, :preferred_currency

def self.description
I18n.t(:shipping_flat_rate_per_item)
end

def compute_package(package)
content_items = package.contents
self.preferred_amount * content_items.sum { |item| item.quantity }
end
end
end
end
@@ -0,0 +1,32 @@
require_dependency 'spree/shipping_calculator'
# For #to_d method on Ruby 1.8
require 'bigdecimal/util'

module Spree
module Calculator::Shipping
class PriceSack < ShippingCalculator
preference :minimal_amount, :decimal, :default => 0
preference :normal_amount, :decimal, :default => 0
preference :discount_amount, :decimal, :default => 0
preference :currency, :string, :default => Spree::Config[:currency]

attr_accessible :preferred_minimal_amount,
:preferred_normal_amount,
:preferred_discount_amount,
:preferred_currency

def self.description
I18n.t(:shipping_price_sack)
end

def compute_package(package)
content_items = package.contents
if total(content_items) < self.preferred_minimal_amount
self.preferred_normal_amount
else
self.preferred_discount_amount
end
end
end
end
end
@@ -8,119 +8,47 @@ class InventoryUnit < ActiveRecord::Base
scope :backordered, lambda { where(:state => 'backordered') }
scope :shipped, lambda { where(:state => 'shipped') }

def self.backorder
warn "[SPREE] Spree::InventoryUnit.backorder will be deprecated in Spree 1.3. Please use Spree::Product.backordered instead."
backordered
end

attr_accessible :shipment
attr_accessible :shipment, :variant_id

# state machine (see http://github.com/pluginaweek/state_machine/tree/master for details)
state_machine :initial => 'on_hand' do
event :fill_backorder do
transition :to => 'sold', :from => 'backordered'
transition :to => 'on_hand', :from => 'backordered'
end
after_transition :on => :fill_backorder, :do => :update_order

event :ship do
transition :to => 'shipped', :if => :allow_ship?
end
event :return do
transition :to => 'returned', :from => 'shipped'
end

after_transition :on => :fill_backorder, :do => :update_order
after_transition :to => 'returned', :do => :restock_variant
end

# Assigns inventory to a newly completed order.
# Should only be called once during the life-cycle of an order, on transition to completed.
#
def self.assign_opening_inventory(order)
return [] unless order.completed?

#increase inventory to meet initial requirements
order.line_items.each do |line_item|
increase(order, line_item.variant, line_item.quantity)
end
def self.backordered_for_stock_item(stock_item)
stock_locations_table = Spree::StockLocation.table_name
joins(:shipment => :stock_location).
where("#{stock_locations_table}.id = ?", stock_item.stock_location_id).
where("#{table_name}.variant_id = ?", stock_item.variant_id).
where("spree_shipments.state != 'canceled'").
where(:state => "backordered").order("created_at ASC")
end

# manages both variant.count_on_hand and inventory unit creation
#
def self.increase(order, variant, quantity)
back_order = determine_backorder(order, variant, quantity)
sold = quantity - back_order

#set on_hand if configured
if self.track_levels?(variant)
variant.decrement!(:count_on_hand, quantity)
end

#create units if configured
if Spree::Config[:create_inventory_units]
create_units(order, variant, sold, back_order)
end
def self.finalize_units!(inventory_units)
inventory_units.map { |iu| iu.update_column(:pending, false) }
end

def self.decrease(order, variant, quantity)
if self.track_levels?(variant)
variant.increment!(:count_on_hand, quantity)
end

if Spree::Config[:create_inventory_units]
destroy_units(order, variant, quantity)
end
end

def self.track_levels?(variant)
Spree::Config[:track_inventory_levels] && !variant.on_demand
def find_stock_item
Spree::StockItem.where({:stock_location_id => self.shipment.stock_location_id, :variant_id => variant_id}).first
end

private
def allow_ship?
Spree::Config[:allow_backorder_shipping] || self.sold?
end

def self.determine_backorder(order, variant, quantity)
if variant.on_hand == 0
quantity
elsif variant.on_hand.present? and variant.on_hand < quantity
quantity - (variant.on_hand < 0 ? 0 : variant.on_hand)
else
0
end
end

def self.destroy_units(order, variant, quantity)
variant_units = order.inventory_units.group_by(&:variant_id)
return unless variant_units.include? variant.id

variant_units = variant_units[variant.id].reject do |variant_unit|
variant_unit.state == 'shipped'
end.sort_by(&:state)

quantity.times do
inventory_unit = variant_units.shift
inventory_unit.destroy
end
end

def self.create_units(order, variant, sold, back_order)
return if back_order > 0 && !Spree::Config[:allow_backorders]

shipment = order.shipments.detect { |shipment| !shipment.shipped? }

sold.times { order.inventory_units.create({:variant => variant, :state => 'sold', :shipment => shipment}, :without_protection => true) }
back_order.times { order.inventory_units.create({:variant => variant, :state => 'backordered', :shipment => shipment}, :without_protection => true) }
Spree::Config[:allow_backorder_shipping] || self.on_hand?
end

def update_order
order.update!
end

def restock_variant
if self.class.track_levels?(variant)
variant.on_hand += 1
variant.save
end
end
end
end
@@ -12,17 +12,17 @@ class LineItem < ActiveRecord::Base
validates :variant, :presence => true
validates :quantity, :numericality => { :only_integer => true, :message => I18n.t('validation.must_be_int'), :greater_than => -1 }
validates :price, :numericality => true
validate :stock_availability
validate :quantity_no_less_than_shipped
validates_with Stock::AvailabilityValidator

attr_accessible :quantity, :variant_id

before_save :update_inventory
before_destroy :ensure_not_shipped, :remove_inventory

after_save :update_order
after_destroy :update_order

attr_accessor :target_shipment

def copy_price
if variant
self.price = variant.price if price.nil?
@@ -59,63 +59,27 @@ def adjust_quantity
end

def sufficient_stock?
return true if Spree::Config[:allow_backorders]
if new_record? || !order.completed?
variant.on_hand >= quantity
else
variant.on_hand >= (quantity - self.changed_attributes['quantity'].to_i)
end
Stock::Quantifier.new(variant_id).can_supply? quantity
end

def insufficient_stock?
!sufficient_stock?
end

private
def update_inventory
return true unless order.completed?

if new_record?
InventoryUnit.increase(order, variant, quantity)
elsif old_quantity = self.changed_attributes['quantity']
if old_quantity < quantity
InventoryUnit.increase(order, variant, (quantity - old_quantity))
elsif old_quantity > quantity
InventoryUnit.decrease(order, variant, (old_quantity - quantity))
end
end
end

def remove_inventory
return true unless order.completed?

InventoryUnit.decrease(order, variant, quantity)
end

def update_order
# update the order totals, etc.
order.create_tax_charge!
order.update!
end
def assign_stock_changes_to=(shipment)
@preferred_shipment = shipment
end

def ensure_not_shipped
if order.try(:inventory_units).to_a.any?{ |unit| unit.variant_id == variant_id && unit.shipped? }
errors.add :base, I18n.t('validation.cannot_destory_line_item_as_inventory_units_have_shipped')
return false
end
end
private

# Validation
def stock_availability
return if sufficient_stock?
errors.add(:quantity, I18n.t('validation.exceeds_available_stock'))
end
def update_inventory
Spree::OrderInventory.new(self.order).verify(self, target_shipment)
end

def quantity_no_less_than_shipped
already_shipped = order.shipments.reduce(0) { |acc, s| acc + s.inventory_units.shipped.where(:variant_id => variant_id).count }
unless quantity >= already_shipped
errors.add(:quantity, I18n.t('validation.cannot_be_less_than_shipped_units'))
end
end
def update_order
# update the order totals, etc.
order.create_tax_charge!
order.update!
end
end
end
@@ -17,8 +17,7 @@ class Order < ActiveRecord::Base
go_to_state :delivery
go_to_state :payment, :if => lambda { |order|
# Fix for #2191
if order.shipping_method
order.create_shipment!
if order.shipments
order.update_totals
end
order.payment_required?
@@ -32,7 +31,8 @@ class Order < ActiveRecord::Base

attr_accessible :line_items, :bill_address_attributes, :ship_address_attributes, :payments_attributes,
:ship_address, :bill_address, :payments_attributes, :line_items_attributes, :number,
:shipping_method_id, :email, :use_billing, :special_instructions, :currency, :coupon_code
:email, :use_billing, :special_instructions, :currency, :coupon_code,
:shipments_attributes

attr_reader :coupon_code

@@ -48,11 +48,8 @@ class Order < ActiveRecord::Base
belongs_to :ship_address, :foreign_key => :ship_address_id, :class_name => "Spree::Address"
alias_attribute :shipping_address, :ship_address

belongs_to :shipping_method

has_many :state_changes, :as => :stateful
has_many :line_items, :dependent => :destroy, :order => "created_at ASC"
has_many :inventory_units
has_many :payments, :dependent => :destroy

has_many :shipments, :dependent => :destroy do
@@ -120,7 +117,7 @@ def self.register_update_hook(hook)

# For compatiblity with Calculator::PriceSack
def amount
line_items.sum(&:amount)
line_items.inject(0.0) { |sum, li| sum + li.amount }
end

def currency
@@ -178,13 +175,11 @@ def has_unprocessed_payments?

# Indicates the number of items in the order
def item_count
line_items.sum(:quantity)
line_items.inject(0) { |sum, li| sum + li.quantity }
end

# Indicates whether there are any backordered InventoryUnits associated with the Order.
def backordered?
return false unless Spree::Config[:track_inventory_levels]
inventory_units.backordered.present?
shipments.any? { |shipment| shipment.backordered? }
end

# Returns the relevant zone (if any) to be used for taxation purposes. Uses default tax zone
@@ -258,26 +253,19 @@ def awaiting_returns?
return_authorizations.any? { |return_authorization| return_authorization.authorized? }
end

def contents
@contents ||= Spree::OrderContents.new(self)
end

def add_variant(variant, quantity = 1, currency = nil)
current_item = find_line_item_by_variant(variant)
if current_item
current_item.quantity += quantity
current_item.currency = currency unless currency.nil?
current_item.save
else
current_item = LineItem.new(:quantity => quantity)
current_item.variant = variant
if currency
current_item.currency = currency unless currency.nil?
current_item.price = variant.price_in(currency).amount
else
current_item.price = variant.price
end
self.line_items << current_item
end
# ActiveSupport::Deprecation.warn("[SPREE] Spree::Order#add_variant will be deprecated in Spree 2.1, please use order.contents.add instead.")
contents.currency = currency unless currency.nil?
contents.add(variant, quantity)
end

self.reload
current_item
def remove_variant(variant, quantity = 1)
# ActiveSupport::Deprecation.warn("[SPREE] Spree::Order#remove_variant will be deprecated in Spree 2.1, please use order.contents.remove instead.")
contents.remove(variant, quantity)
end

# Associates the specified user with the order.
@@ -305,6 +293,10 @@ def shipment
@shipment ||= shipments.last
end

def shipped_shipments
shipments.shipped
end

def contains?(variant)
find_line_item_by_variant(variant).present?
end
@@ -338,19 +330,6 @@ def create_tax_charge!
Spree::TaxRate.adjust(self)
end

# Creates a new shipment (adjustment is created by shipment model)
def create_shipment!
shipping_method(true)
if shipment.present?
shipment.update_attributes!(:shipping_method => shipping_method)
else
self.shipments << Shipment.create!({ :order => self,
:shipping_method => shipping_method,
:address => self.ship_address,
:inventory_units => self.inventory_units}, :without_protection => true)
end
end

def outstanding_balance
total - payment_total
end
@@ -378,15 +357,18 @@ def credit_cards
# Called after transition to complete state when payments will have been processed
def finalize!
touch :completed_at
InventoryUnit.assign_opening_inventory(self)

# lock all adjustments (coupon promotions, etc.)
adjustments.each { |adjustment| adjustment.update_column('state', "closed") }

# update payment and shipment(s) states, and save
updater = OrderUpdater.new(self)
updater.update_payment_state
shipments.each { |shipment| shipment.update!(self) }
shipments.each do |shipment|
shipment.update!(self)
shipment.finalize!
end

updater.update_shipment_state
save

@@ -410,23 +392,6 @@ def deliver_order_confirmation_email
end

# Helper methods for checkout steps

def available_shipping_methods
return [] unless ship_address
ShippingMethod.all_available(self)
end

def rate_hash
@rate_hash ||= available_shipping_methods.collect do |ship_method|
next unless cost = ship_method.calculator.compute(self)
ShippingRate.new( :id => ship_method.id,
:shipping_method => ship_method,
:name => ship_method.name,
:cost => cost,
:currency => currency)
end.compact.sort_by { |r| r.cost }
end

def paid?
payment_state == 'paid'
end
@@ -540,6 +505,17 @@ def shipped?
%w(partial shipped).include?(shipment_state)
end

def create_proposed_shipments
shipments.destroy_all

packages = Spree::Stock::Coordinator.new(self).packages
packages.each do |package|
shipments << package.to_shipment
end

shipments
end

private
def link_by_email
self.email = user.email if self.user
@@ -554,36 +530,23 @@ def has_available_shipment
return unless has_step?("delivery")
return unless address?
return unless ship_address && ship_address.valid?
errors.add(:base, :no_shipping_methods_available) if available_shipping_methods.empty?
# errors.add(:base, :no_shipping_methods_available) if available_shipping_methods.empty?
end

def has_available_payment
return unless delivery?
errors.add(:base, :no_payment_methods_available) if available_payment_methods.empty?
# errors.add(:base, :no_payment_methods_available) if available_payment_methods.empty?
end

def after_cancel
restock_items!
shipments.each { |shipment| shipment.cancel! }

#TODO: make_shipments_pending
OrderMailer.cancel_email(self).deliver
self.payment_state = 'credit_owed' unless shipped?
end

def restock_items!
line_items.each do |line_item|
InventoryUnit.decrease(self, line_item.variant, line_item.quantity)
end
end

def after_resume
unstock_items!
end

def unstock_items!
line_items.each do |line_item|
InventoryUnit.increase(self, line_item.variant, line_item.quantity)
end
shipments.each { |shipment| shipment.resume! }
end

def use_billing?
@@ -69,14 +69,12 @@ def self.define_state_machine!
end
end

before_transition :to => :delivery, :do => :remove_invalid_shipments!
before_transition :to => :delivery, :do => :create_proposed_shipments

after_transition :to => :complete, :do => :finalize!
after_transition :to => :delivery, :do => :create_tax_charge!
after_transition :to => :resumed, :do => :after_resume
after_transition :to => :canceled, :do => :after_cancel

after_transition :from => :delivery, :do => :create_shipment!
end
end

@@ -0,0 +1,69 @@
module Spree
class OrderContents
attr_accessor :order, :currency

def initialize(order)
@order = order
end

def add(variant, quantity, shipment=nil)
#get current line item for variant if exists
line_item = order.find_line_item_by_variant(variant)

#add variant qty to line_item
add_to_line_item(line_item, variant, quantity, shipment)
end

def remove(variant, quantity, shipment=nil)
#get current line item for variant
line_item = order.find_line_item_by_variant(variant)

#TODO raise exception if line_item is nil

#remove variant qty from line_item
remove_from_line_item(line_item, variant, quantity, shipment)
end

private

def add_to_line_item(line_item, variant, quantity, shipment=nil)
if line_item
line_item.target_shipment = shipment
line_item.quantity += quantity
line_item.currency = currency unless currency.nil?
line_item.save
else
line_item = LineItem.new(:quantity => quantity)
line_item.target_shipment = shipment
line_item.variant = variant
if currency
line_item.currency = currency unless currency.nil?
line_item.price = variant.price_in(currency).amount
else
line_item.price = variant.price
end
order.line_items << line_item
line_item
end

order.reload
line_item
end

def remove_from_line_item(line_item, variant, quantity, shipment=nil)
line_item.quantity += -quantity
line_item.target_shipment= shipment

if line_item.quantity == 0
Spree::OrderInventory.new(order).verify(line_item, shipment)
line_item.destroy
else
line_item.save!
end

order.reload
line_item
end

end
end
@@ -0,0 +1,96 @@
module Spree
class OrderInventory
attr_accessor :order

def initialize(order)
@order = order
end

def verify(line_item, shipment=nil)
return true unless order.completed?

variant_units = inventory_units_for(line_item.variant)

if variant_units.size < line_item.quantity
#add

quantity = line_item.quantity - variant_units.size

shipment ||= order.shipments.detect { |shipment| (shipment.ready? || shipment.pending?) && shipment.include?(line_item.variant) }
shipment ||= order.shipments.detect { |shipment| (shipment.ready? || shipment.pending?) && line_item.variant.stock_location_ids.include?(shipment.stock_location_id) }

add_to_shipment(shipment,line_item.variant, quantity)

elsif variant_units.size > line_item.quantity
#remove

quantity = variant_units.size - line_item.quantity

order.shipments.each do |shipment|
break if quantity == 0

quantity -= remove_from_shipment(shipment, line_item.variant, quantity)
end

end
end

def inventory_units_for(variant)
return [] unless order.completed?

units = order.shipments.collect{|s| s.inventory_units.all}.flatten
units.group_by(&:variant_id)[variant.id] || []
end

private

def add_to_shipment(shipment, variant, quantity)
#create inventory_units
on_hand, back_order = shipment.stock_location.fill_status(variant, quantity)

on_hand.times do
shipment.inventory_units.create({:variant_id => variant.id,
:state => 'on_hand'}, :without_protection => true)
end

back_order.times do
shipment.inventory_units.create({:variant_id => variant.id,
:state => 'backordered'}, :without_protection => true)
end


# adding to this shipment, and removing from stock_location
shipment.stock_location.unstock variant, quantity, shipment

# return quantity added
quantity
end

def remove_from_shipment(shipment, variant, quantity)
return 0 if quantity == 0 || shipment.shipped?

shipment_units = shipment.inventory_units_for(variant).reject do |variant_unit|
variant_unit.state == 'shipped'
end.sort_by(&:state)

removed_quantity = 0

shipment_units.each do |inventory_unit|
break if removed_quantity == quantity
inventory_unit.destroy
removed_quantity += 1
end

if shipment.inventory_units.count == 0
shipment.destroy
end

# removing this from shipment, and adding to stock_location
shipment.stock_location.restock variant, removed_quantity, shipment

# return quantity removed
removed_quantity
end

end
end
@@ -57,15 +57,8 @@ def check_stock_levels(variant, quantity)
display_name = %Q{#{variant.name}}
display_name += %Q{ (#{variant.options_text})} unless variant.options_text.blank?

if variant.available?
on_hand = variant.on_hand
if on_hand >= quantity || Spree::Config[:allow_backorders]
return true
else
errors.add(:base, %Q{There are only #{on_hand} of #{display_name.inspect} remaining.} +
%Q{ Please select a quantity less than or equal to this value.})
return false
end
if Stock::Quantifier.new(variant).can_supply? quantity
true
else
errors.add(:base, %Q{#{display_name.inspect} is out of stock.})
return false
@@ -72,10 +72,11 @@ def update_shipment_state
else
# will return nil if no shipments are found
order.shipment_state = shipment_states.first
if order.shipment_state && order.inventory_units.where(:shipment_id => nil).exists?
# shipments exist but there are unassigned inventory units
order.shipment_state = 'partial'
end
# TODO inventory unit states?
# if order.shipment_state && order.inventory_units.where(:shipment_id => nil).exists?
# shipments exist but there are unassigned inventory units
# order.shipment_state = 'partial'
# end
end
end

@@ -57,9 +57,7 @@ class Product < ActiveRecord::Base
after_create :set_master_variant_defaults
after_create :add_properties_and_option_types_from_prototype
after_create :build_variants_from_option_values_hash, :if => :option_values_hash
before_save :recalculate_count_on_hand
after_save :save_master
after_save :set_master_on_hand_to_zero_when_product_has_variants

delegate :images, :to => :master, :prefix => true
alias_method :images, :master_images
@@ -75,7 +73,7 @@ class Product < ActiveRecord::Base

attr_accessible :name, :description, :available_on, :permalink, :meta_description,
:meta_keywords, :price, :sku, :deleted_at, :prototype_id,
:option_values_hash, :on_demand, :on_hand, :weight, :height, :width, :depth,
:option_values_hash, :weight, :height, :width, :depth,
:shipping_category_id, :tax_category_id, :product_properties_attributes,
:variants_attributes, :taxon_ids, :option_type_ids, :cost_currency

@@ -103,45 +101,6 @@ def has_variants?
variants.any?
end

# should product be displayed on products pages and search
def on_display?
has_stock? || Spree::Config[:show_zero_stock_products]
end

# is this product actually available for purchase
def on_sale?
has_stock? || Spree::Config[:allow_backorders]
end

# returns the number of inventory units "on_hand" for this product
def on_hand
has_variants? ? variants.sum(&:on_hand) : master.on_hand
end

# adjusts the "on_hand" inventory level for the product up or down to match the given new_level
def on_hand=(new_level)
unless self.on_demand
raise 'cannot set on_hand of product with variants' if has_variants? && Spree::Config[:track_inventory_levels]
master.on_hand = new_level
end
end

def on_demand=(new_on_demand)
raise 'cannot set on_demand of product with variants' if has_variants? && Spree::Config[:track_inventory_levels]
master.on_demand = on_demand
self[:on_demand] = new_on_demand
end

def count_on_hand=(value)
raise I18n.t('exceptions.count_on_hand_setter')
end

# Returns true if there are inventory units (any variant) with "on_hand" state for this product
# Variants take precedence over master
def has_stock?
has_variants? ? variants.any?(&:in_stock?) : master.in_stock?
end

def tax_category
if self[:tax_category_id].nil?
TaxCategory.where(:is_default => true).first
@@ -250,21 +209,6 @@ def add_properties_and_option_types_from_prototype
end
end

def recalculate_count_on_hand
value = if has_variants?
variants.sum(:count_on_hand)
else
(master ? master.count_on_hand : 0)
end
self[:count_on_hand] = value
end

# the master on_hand is meaningless once a product has variants as the inventory
# units are now "contained" within the product variants
def set_master_on_hand_to_zero_when_product_has_variants
master.on_hand = 0 if has_variants? && Spree::Config[:track_inventory_levels] && !self.on_demand
end

# ensures the master variant is flagged as such
def set_master_variant_defaults
master.is_master = true
@@ -50,7 +50,7 @@ def self.simple_scopes
# If you need products only within one taxon use
#
# Spree::Product.taxons_id_eq(x)
#
#
# If you're using count on the result of this scope, you must use the
# `:distinct` option as well:
#
@@ -202,11 +202,6 @@ def self.active(currency = nil)
end
search_scopes << :active

add_search_scope :on_hand do
variants_table = Variant.table_name
where("#{table_name}.id in (select product_id from #{variants_table} where product_id = #{table_name}.id and #{variants_table}.deleted_at IS NULL group by product_id having sum(count_on_hand) > 0)")
end

add_search_scope :taxons_name_eq do |name|
group("spree_products.id").joins(:taxons).where(Taxon.arel_table[:name].eq(name))
end
@@ -3,14 +3,15 @@ class ReturnAuthorization < ActiveRecord::Base
belongs_to :order

has_many :inventory_units
has_one :stock_location
before_create :generate_number
before_save :force_positive_amount

validates :order, :presence => true
validates :amount, :numericality => true
validate :must_have_shipped_units

attr_accessible :amount, :reason
attr_accessible :amount, :reason, :stock_location_id

state_machine :initial => 'authorized' do
after_transition :to => 'received', :do => :process_return
@@ -32,8 +33,9 @@ def display_amount
end

def add_variant(variant_id, quantity)
order_units = order.inventory_units.group_by(&:variant_id)
order_units = returnable_inventory.group_by(&:variant_id)
returned_units = inventory_units.group_by(&:variant_id)
return false if order_units.empty?

count = 0

@@ -58,9 +60,13 @@ def add_variant(variant_id, quantity)
order.authorize_return! if inventory_units.reload.size > 0 && !order.awaiting_return?
end

def returnable_inventory
order.shipped_shipments.collect{|s| s.inventory_units.all}.flatten
end

private
def must_have_shipped_units
errors.add(:order, I18n.t(:has_no_shipped_units)) if order.nil? || !order.inventory_units.any?(&:shipped?)
errors.add(:order, I18n.t(:has_no_shipped_units)) if order.nil? || !order.shipped_shipments.any?
end

def generate_number
@@ -75,11 +81,16 @@ def generate_number
end

def process_return
inventory_units.each &:return!
inventory_units.each do |iu|
iu.return!
Spree::StockMovement.create!(:stock_item_id => iu.find_stock_item.id, :quantity => 1)
end

credit = Adjustment.new(:amount => amount.abs * -1, :label => I18n.t(:rma_credit))
credit.source = self
credit.adjustable = order
credit.save

order.return if inventory_units.all?(&:returned?)
end

@@ -3,27 +3,27 @@
module Spree
class Shipment < ActiveRecord::Base
belongs_to :order
belongs_to :shipping_method

has_many :shipping_rates
has_many :shipping_methods, :through => :shipping_rates

belongs_to :address
belongs_to :stock_location

has_many :state_changes, :as => :stateful
has_many :inventory_units, :dependent => :nullify
has_many :inventory_units, :dependent => :destroy
has_one :adjustment, :as => :source, :dependent => :destroy

before_create :generate_shipment_number
after_save :ensure_correct_adjustment, :update_order
after_save :ensure_selected_shipping_rate, :ensure_correct_adjustment, :update_order

attr_accessor :special_instructions

attr_accessible :order, :shipping_method, :special_instructions,
:shipping_method_id, :tracking, :address, :inventory_units
attr_accessible :order, :special_instructions, :stock_location_id,
:tracking, :address, :inventory_units, :selected_shipping_rate_id

accepts_nested_attributes_for :address
accepts_nested_attributes_for :inventory_units

validates :inventory_units, :presence => true, :if => :require_inventory
validates :shipping_method, :presence => true

make_permalink :field => :number

scope :with_state, lambda { |s| where(:state => s) }
@@ -39,11 +39,60 @@ def to_param
number.to_s.to_url.upcase
end

def backordered?
inventory_units.any? { |iu| iu.backordered? }
end

def shipped=(value)
return unless value == '1' && shipped_at.nil?
self.shipped_at = Time.now
end

def shipping_method
shipping_rates.where(selected: true).first.try(:shipping_method) || shipping_rates.first.try(:shipping_method)
end

def add_shipping_method(shipping_method, selected=false)
shipping_rates << Spree::ShippingRate.create(:shipping_method => shipping_method,
:selected => selected)
end

def selected_shipping_rate
shipping_rates.where(selected: true).first
end

def selected_shipping_rate_id
selected_shipping_rate.try(:id)
end

def selected_shipping_rate_id=(id)
shipping_rates.update_all(selected: false)
shipping_rates.update(id, selected: true)
self.save!
end

def ensure_selected_shipping_rate
shipping_rates.exists?(selected: true) ||
shipping_rates.limit(1).update_all(selected: true)
end

def refresh_rates
return self.shipping_rates if self.shipped?

shipping_method_id = self.shipping_method.try(:id)
self.shipping_rates = Stock::Estimator.new(order).shipping_rates(to_package)


if shipping_method_id
selected_rate = self.shipping_rates.detect{|rate| rate.shipping_method_id == shipping_method_id}
if selected_rate
self.selected_shipping_rate_id = selected_rate.id
end
end

self.shipping_rates
end

def currency
order.nil? ? Spree::Config[:currency] : order.currency
end
@@ -74,20 +123,38 @@ def display_cost
event :pend do
transition :from => 'ready', :to => 'pending'
end

event :ship do
transition :from => 'ready', :to => 'shipped'
end

after_transition :to => 'shipped', :do => :after_ship

event :cancel do
transition :to => 'canceled', :from => ['pending', 'ready']
end
after_transition :to => 'canceled', :do => :after_cancel

event :resume do
transition :from => 'canceled', :to => 'ready', :if => lambda { |shipment|
shipment.determine_state(shipment.order) == 'ready'
}
transition :from => 'canceled', :to => 'pending', :if => lambda { |shipment|
shipment.determine_state(shipment.order) == 'ready'
}
transition :from => 'canceled', :to => 'pending'
end
after_transition :from => 'canceled', :to => ['pending', 'ready'], :do => :after_resume
end

def editable_by?(user)
!shipped?
end

def manifest
inventory_units.group_by(&:variant).map do |i|
OpenStruct.new(:variant => i.first, :quantity => i.last.length)
inventory_units.group_by(&:variant).map do |variant, units|
states = {}
units.group_by(&:state).each { |state, iu| states[state] = iu.count }
OpenStruct.new(:variant => variant, :quantity => units.length, :states => states)
end
end

@@ -99,6 +166,25 @@ def line_items
end
end

def finalize!
InventoryUnit.finalize_units!(inventory_units)
manifest.each do |item|
stock_location.unstock item.variant, item.quantity, self
end
end

def after_cancel
manifest.each do |item|
stock_location.restock item.variant, item.quantity, self
end
end

def after_resume
manifest.each do |item|
stock_location.unstock item.variant, item.quantity, self
end
end

# Updates various aspects of the Shipment while bypassing any callbacks. Note that this method takes an explicit reference to the
# Order object. This is necessary because the association actually has a stale (and unsaved) copy of the Order and so it will not
# yield the correct results.
@@ -115,6 +201,7 @@ def update!(order)
# shipped if already shipped (ie. does not change the state)
# ready all other cases
def determine_state(order)
return 'canceled' if order.canceled?
return 'pending' unless order.can_ship?
return 'pending' if inventory_units.any? &:backordered?
return 'shipped' if state == 'shipped'
@@ -125,6 +212,22 @@ def tracking_url
@tracking_url ||= shipping_method.build_tracking_url(tracking)
end

def include?(variant)
inventory_units_for(variant).present?
end

def inventory_units_for(variant)
inventory_units.group_by(&:variant_id)[variant.id] || []
end

def to_package
package = Stock::Package.new(stock_location, order)
inventory_units.each do |inventory_unit|
package.add inventory_unit.variant, 1, inventory_unit.state
end
package
end

private
def generate_shipment_number
return number unless number.blank?
@@ -142,18 +245,10 @@ def description_for_shipping_charge

def validate_shipping_method
unless shipping_method.nil?
errors.add :shipping_method, I18n.t(:is_not_available_to_shipment_address) unless shipping_method.zone.include?(address)
errors.add :shipping_method, I18n.t(:is_not_available_to_shipment_address) unless shipping_method.include?(address)
end
end

# Determines whether or not inventory units should be associated with the shipment. This is always +false+ when
# +Spree::Config[:track_inventory_levels]+ is set to +false.+ Otherwise its +true+ whenever the order is completed
# (and not canceled.)
def require_inventory
return false unless Spree::Config[:track_inventory_levels]
order.completed? && !order.canceled?
end

def after_ship
inventory_units.each &:ship!
adjustment.finalize!
@@ -168,9 +263,14 @@ def send_shipped_email
def ensure_correct_adjustment
if adjustment
adjustment.originator = shipping_method
adjustment.label = shipping_method.adjustment_label
adjustment.save
else
adjustment.label = shipping_method.name
if adjustment.open?
adjustment.amount = selected_shipping_rate.cost
end
adjustment.save!
adjustment.reload

elsif shipping_method
shipping_method.create_adjustment(shipping_method.adjustment_label, order, self, true)
reload #ensure adjustment is present on later saves
end
@@ -0,0 +1,24 @@
module Spree
class ShippingCalculator < Calculator
belongs_to :calculable, :polymorphic => true

def compute(package_or_shipment)
package = package_or_shipment.respond_to?(:to_package) ?
package_or_shipment.to_package : package_or_shipment
compute_package package
end

def compute_package(package)
raise(NotImplementedError, 'please use concrete calculator')
end

def available?(package)
true
end

private
def total(content_items)
content_items.sum { |item| item.quantity * item.variant.price }
end
end
end
@@ -2,7 +2,8 @@ module Spree
class ShippingCategory < ActiveRecord::Base
validates :name, :presence => true
has_many :products
has_many :shipping_methods
has_many :shipping_method_categories
has_many :shipping_methods, :through => :shipping_method_categories

attr_accessible :name
end
@@ -6,59 +6,39 @@ class ShippingMethod < ActiveRecord::Base
default_scope where(:deleted_at => nil)

has_many :shipments
validates :name, :zone, :presence => true
validates :name, :presence => true

belongs_to :shipping_category
belongs_to :zone
has_many :shipping_method_categories
has_many :shipping_categories, :through => :shipping_method_categories

attr_accessible :name, :zone_id, :display_on, :shipping_category_id,
has_and_belongs_to_many :zones

attr_accessible :name, :zones, :display_on, :shipping_category_id,
:match_none, :match_one, :match_all, :tracking_url

def adjustment_label
I18n.t(:shipping)
end

def available?(order)
calculator.available?(order)
end

def within_zone?(order)
zone && zone.include?(order.ship_address)
def zone
raise "DEPRECATION WARNING: ShippingMethod#zone is no longer correct. Multiple zones need to be supported"
Rails.logger.error "DEPRECATION WARNING: ShippingMethod#zone is no longer correct. Multiple zones need to be supported"
zones.first
end

def available_to_order?(order)
available?(order) &&
within_zone?(order) &&
category_match?(order) &&
currency_match?(order)
def zone=(zone)
p "DEPRECATION WARNING: ShippingMethod#zone is no longer correct. Multiple zones need to be supported"
Rails.logger.error "DEPRECATION WARNING: ShippingMethod#zone= is no longer correct. Multiple zones need to be supported"
zones = zone
end

# Indicates whether or not the category rules for this shipping method
# are satisfied (if applicable)
def category_match?(order)
return true if shipping_category.nil?

if match_all
order.products.all? { |p| p.shipping_category == shipping_category }
elsif match_one
order.products.any? { |p| p.shipping_category == shipping_category }
elsif match_none
order.products.all? { |p| p.shipping_category != shipping_category }
def include?(address)
return false unless address
zones.any? do |zone|
zone.include?(address)
end
end

def currency_match?(order)
calculator_currency.nil? || calculator_currency == order.currency
end

def calculator_currency
calculator.preferences[:currency]
end

def self.all_available(order)
all.select { |method| method.available_to_order?(order) }
end

def build_tracking_url(tracking)
tracking_url.gsub(/:tracking/, tracking) unless tracking.blank? || tracking_url.blank?
end
@@ -0,0 +1,6 @@
module Spree
class ShippingMethodCategory < ActiveRecord::Base
belongs_to :shipping_method
belongs_to :shipping_category
end
end
@@ -1,10 +1,13 @@
module Spree
class ShippingRate < Struct.new(:id, :shipping_method, :name, :cost, :currency)
def initialize(attributes = {})
attributes.each do |k, v|
self.send("#{k}=", v)
end
end
class ShippingRate < ActiveRecord::Base
belongs_to :shipment
belongs_to :shipping_method

attr_accessible :id, :shipping_method, :shipment,
:name, :cost, :selected

delegate :order, :currency, to: :shipment
delegate :name, to: :shipping_method

def display_price
if Spree::Config[:shipment_inc_vat]
@@ -0,0 +1,28 @@
# Used by Prioritizer to adjust item quantities
# see prioritizer_spec for use cases
module Spree
module Stock
class Adjuster
attr_accessor :variant, :need, :status

def initialize(variant, quantity, status)
@variant = variant
@need = quantity
@status = status
end

def adjust(item)
if item.quantity >= need
item.quantity = need
@need = 0
elsif item.quantity < need
@need -= item.quantity
end
end

def fulfilled?
@need == 0
end
end
end
end
@@ -0,0 +1,15 @@
module Spree
module Stock
class AvailabilityValidator < ActiveModel::Validator

def validate(line_item)
quantifier = Stock::Quantifier.new(line_item.variant_id)

unless quantifier.can_supply? line_item.quantity
line_item.errors[:quantity] << I18n.t('validation.exceeds_available_stock')
end

end
end
end
end
@@ -0,0 +1,49 @@
module Spree
module Stock
class Coordinator
attr_reader :order

def initialize(order)
@order = order
end

def packages
packages = Array.new
packages = build_packages(packages)
packages = prioritize_packages(packages)
packages = estimate_packages(packages)
end

private
def build_packages(packages)
StockLocation.active.each do |stock_location|
packer = build_packer(stock_location, order)
packages += packer.packages
end
packages
end

def prioritize_packages(packages)
prioritizer = Prioritizer.new(order, packages)
prioritizer.prioritized_packages
end

def estimate_packages(packages)
estimator = Estimator.new(order)
packages.each do |package|
package.shipping_rates = estimator.shipping_rates(package)
end
packages
end

def build_packer(stock_location, order)
Packer.new(stock_location, order, splitters(stock_location))
end

def splitters(stock_location)
# extension point to return custom splitters for a location
Rails.application.config.spree.stock_splitters
end
end
end
end
@@ -0,0 +1,44 @@
module Spree
module Stock
class Differentiator
attr_reader :missing, :packed, :required, :packages, :order

def initialize(order, packages)
@order = order
@packages = packages
build_packed
build_required
build_missing
end

def missing?
missing.values.any? { |v| v > 0 }
end

private
def build_missing
@missing = Hash.new(0)
required.keys.each do |variant|
missing = required[variant] - packed[variant]
@missing[variant] = missing if missing > 0
end
end

def build_packed
@packed = Hash.new(0)
packages.each do |package|
package.contents.each do |content_item|
@packed[content_item.variant] += content_item.quantity
end
end
end

def build_required
@required = Hash.new(0)
order.line_items.each do |line_item|
@required[line_item.variant] = line_item.quantity
end
end
end
end
end
@@ -0,0 +1,38 @@
module Spree
module Stock
class Estimator
attr_reader :order, :currency

def initialize(order)
@order = order
@currency = order.currency
end

def shipping_rates(package)
shipping_rates = Array.new
shipping_methods = shipping_methods(package)
return [] unless shipping_methods
shipping_methods.each do |shipping_method|
cost = calculate_cost(shipping_method, package)

shipping_rates << ShippingRate.new( :shipping_method => shipping_method,
:cost => cost)
end
shipping_rates
end

private
def shipping_methods(package)
shipping_methods = package.shipping_methods
shipping_methods.delete_if { |ship_method| !ship_method.calculator.available?(package.contents) }
shipping_methods.delete_if { |ship_method| !ship_method.include?(order.ship_address) }
shipping_methods.delete_if { |ship_method| !(ship_method.calculator.preferences[:currency].nil? || ship_method.calculator.preferences[:currency] == currency) }
shipping_methods
end

def calculate_cost(shipping_method, package)
shipping_method.calculator.compute(package)
end
end
end
end
@@ -0,0 +1,54 @@
module Spree
module Stock
class OrderCounter
attr_reader :order

def initialize(order)
@order = order
@ordered_counts = count_line_items
@assigned_counts = count_inventory_units
end

def variants
@ordered_counts.keys
end

def variants_with_remaining
variants.select { |variant| remaining(variant) > 0 }
end

def remaining?
not variants_with_remaining.empty?
end

def ordered(variant)
@ordered_counts[variant]
end

def assigned(variant)
@assigned_counts[variant]
end

def remaining(variant)
@ordered_counts[variant] - @assigned_counts[variant]
end

private
def count_line_items
counts = Hash.new(0)
order.line_items.each do |line_item|
counts[line_item.variant] += line_item.quantity
end
counts
end

def count_inventory_units
counts = Hash.new(0)
order.inventory_units.each do |inventory_unit|
counts[inventory_unit.variant] += 1
end
counts
end
end
end
end
@@ -0,0 +1,116 @@
module Spree
module Stock
class Package
ContentItem = Struct.new(:variant, :quantity, :state)

attr_reader :stock_location, :order, :contents
attr_accessor :shipping_rates

def initialize(stock_location, order, contents=[])
@stock_location = stock_location
@order = order
@contents = contents
@shipping_rates = Array.new
end

def add(variant, quantity, state=:on_hand)
contents << ContentItem.new(variant, quantity, state)
end

def weight
contents.sum { |item| item.variant.weight * item.quantity }
end

def on_hand
contents.select { |item| item.state == :on_hand }
end

def backordered
contents.select { |item| item.state == :backordered }
end

def find_item(variant, state=:on_hand)
contents.select do |item|
item.variant == variant &&
item.state == state
end.first
end

def quantity(state=nil)
case state
when :on_hand
on_hand.sum { |item| item.quantity }
when :backordered
backordered.sum { |item| item.quantity }
else
contents.sum { |item| item.quantity }
end
end

def empty?
quantity == 0
end

def flattened
flat = []
contents.each do |item|
item.quantity.times do
flat << ContentItem.new(item.variant, 1, item.state)
end
end
flat
end

def flattened=(flattened)
contents.clear
flattened.each do |item|
current_item = find_item(item.variant, item.state)
if current_item
current_item.quantity += 1
else
add(item.variant, item.quantity, item.state)
end
end
end

def currency
#TODO calculate from first variant?
end

def shipping_categories
contents.map { |item| item.variant.shipping_category }.uniq
end

def shipping_methods
shipping_categories.map { |sc| sc.shipping_methods }.flatten.uniq
end

def inspect
out = "#{order} - "
out << contents.map do |content_item|
"#{content_item.variant.name} #{content_item.quantity} #{content_item.state}"
end.join('/')
out
end

def to_shipment
shipment = Spree::Shipment.new
shipment.order = order
shipment.stock_location = stock_location
shipment.shipping_rates = shipping_rates

contents.each do |item|
item.quantity.times do |n|
unit = shipment.inventory_units.build
unit.pending = true
unit.order = order
unit.variant = item.variant
unit.state = item.state.to_s
end
end

shipment
end
end
end
end
@@ -0,0 +1,36 @@
module Spree
module Stock
class Packer
attr_reader :stock_location, :order, :splitters

def initialize(stock_location, order, splitters=[Splitter::Base])
@stock_location = stock_location
@order = order
@splitters = splitters
end

def packages
build_splitter.split [default_package]
end

def default_package
package = Package.new(stock_location, order)
order.line_items.each do |line_item|
on_hand, backordered = stock_location.fill_status(line_item.variant, line_item.quantity)
package.add line_item.variant, on_hand, :on_hand if on_hand > 0
package.add line_item.variant, backordered, :backordered if backordered > 0
end
package
end

private
def build_splitter
splitter = nil
splitters.reverse.each do |klass|
splitter = klass.new(self, splitter)
end
splitter
end
end
end
end
@@ -0,0 +1,47 @@
module Spree
module Stock
class Prioritizer
attr_reader :packages, :order

def initialize(order, packages, adjuster_class=Adjuster)
@order = order
@packages = packages
@adjuster_class = adjuster_class
end

def prioritized_packages
sort_packages
adjust_packages
prune_packages
packages
end

private
def adjust_packages
order.line_items.each do |line_item|
adjuster = @adjuster_class.new(line_item.variant, line_item.quantity, :on_hand)

visit_packages(adjuster)

adjuster.status = :backordered
visit_packages(adjuster)
end
end

def visit_packages(adjuster)
packages.each do |package|
item = package.find_item adjuster.variant, adjuster.status
adjuster.adjust(item) if item
end
end

def sort_packages
# order packages by preferred stock_locations
end

def prune_packages
packages.reject! { |pkg| pkg.empty? }
end
end
end
end
@@ -0,0 +1,28 @@
module Spree
module Stock
class Quantifier
attr_reader :stock_items

def initialize(variant)
@variant = variant
@stock_items = Spree::StockItem.joins(:stock_location).where(:variant_id => @variant, Spree::StockLocation.table_name =>{ :active => true})
end

def total_on_hand
if Spree::Config.track_inventory_levels
stock_items.sum(&:count_on_hand)
else
Float::INFINITY
end
end

def backorderable?
stock_items.any?(&:backorderable)
end

def can_supply?(required)
total_on_hand >= required || backorderable?
end
end
end
end
@@ -0,0 +1,22 @@
module Spree
module Stock
class RemainingPacker < Packer
attr_reader :order_counter

def initialize(stock_location, order, order_counter=nil)
super
@order_counter = order_counter || Stock::OrderCounter.new(order)
end

def default_package
package = Package.new(stock_location, order)
order_counter.variants_with_remaining.each do |variant|
on_hand, backordered = stock_status(variant, order_counter.remaining(variant))
package.add variant, on_hand, :on_hand if on_hand > 0
package.add variant, backordered, :backordered if backordered > 0
end
package
end
end
end
end
@@ -0,0 +1,23 @@
module Spree
module Stock
module Splitter
class Backordered < Base

def split(packages)
split_packages = []
packages.each do |package|
if package.on_hand.count > 0
split_packages << build_package(package.on_hand)
end

if package.backordered.count > 0
split_packages << build_package(package.backordered)
end
end
return_next split_packages
end

end
end
end
end