accepts_nested_attributes_for not triggering callback on children? - ruby-on-rails

I have a model called 'Shift' Which has many child models each called 'Booking'. When I update shift, the children are not being updated (in the development environment), despite the fact that the tests are passing.
More detail:
In the controller, we run this action to process reviews.
def review
#shift = Shift.find(params[:id])
#shift.start_will_change! # this makes the 'shift' object 'dirty', triggering callbacks.
if #shift.update(review_params)
#shift.update(reviewed: true)
redirect_to employers_shift_path #shift
else
flash[:notice] = "Could not leave review. If this problem persists please contact will#get-rota.com"
redirect_to employers_shift_path #shift
end
end
Below is the test that passes. So far so good.
test "accepts reviews that change the time" do
assert_nil #booking.start_actual
post :review, id: #shift.id, shift: {
bookings_attributes: {
"0"=> {
"rating_of_worker"=>"4",
"feedback_for_worker"=>"Great job",
"edit_times"=>"true",
"start_date"=> (#booking.start + 1.day).to_s,
"start_time_hours"=>(#booking.start - 1.hour).to_s,
"start_time_minutes"=>#booking.start.to_s,
"end_time_hours"=>#booking.ending.to_s,
"end_time_minutes"=>#booking.ending.to_s,
"id"=>#booking.id}
}
}
#booking.reload
assert_equal 4, #booking.rating_of_worker
assert_equal "Great job", #booking.feedback_for_worker
assert_not_nil #booking.start_actual
assert_equal (#booking.start - 1.hour + 1.day), #booking.start_actual
assert_not_nil #booking.ending_actual
assert_equal (#booking.ending + 1.day), #booking.ending_actual
assert_redirected_to employers_shift_path #shift
end
Other info:
Here's booking:
class Booking < ActiveRecord::Base
belongs_to :shift
after_validation :update_actual_start_and_end
attr_accessor :start_time_hours,
:start_time_minutes,
:end_time_hours,
:end_time_minutes,
:edit_times,
:start_date
def update_actual_start_and_end
if edit_times == 'true'
date = DateTime.parse(start_date)
self.start_actual = start.change({ year: date.year,
month: date.month,
day: date.day,
hour: DateTime.parse(start_time_hours).hour,
min: DateTime.parse(start_time_minutes).minute })
self.ending_actual = ending.change({ hour: DateTime.parse(end_time_hours).hour,
min: DateTime.parse(end_time_minutes).minute })
if self.start_actual > self.ending_actual
self.ending_actual += 1.day
end
end
end
end
Here's Shift:
class Shift < ActiveRecord::Base
has_many :bookings, dependent: :destroy
accepts_nested_attributes_for :bookings
end

Related

Trouble with Paperclip + Rspec ( Paperclip::AdapterRegistry::NoHandlerError: No handler found for )

I'm having some problems when i'm trying to run some tests with rspec and paperclip.
i'm getting this message:
Failure/Error: #event = Event.new(event_params)
Paperclip::AdapterRegistry::NoHandlerError:
No handler found for "/events/rails.png?1453566649"
Here is my test class:
context "user creates a new event" do
describe "with valid fields" do
before(:each) do
#image = Rack::Test::UploadedFile.new(Rails.root.join('spec/images/rails.png'), 'image/png')
end
it "must persist the event" do
event = build(:event, photo: nil, photo_attachment: nil)
event.photo_attachment = #image
post :create, event: {title: event.title, description: event.description, place: event.place, address: event.address, opening_date: event.opening_date, category: event.category, price: event.price, photo_attachment: event.photo_attachment, hour: event. hour}
expect(assigns(:event).id.nil?).to be false
end
Here is my controller:
class EventsController < ApplicationController
def new
#event = Event.new
end
def create
#event = Event.new(event_params)
if #event.save
redirect_to "success"
end
end
private
def event_params
params.require(:event).permit(:title, :description, :photo, :place, :address, :opening_date, :ending_date, :category, :price, :photo_attachment, :hour)
end
end
And here is my factory ( I'm using factory girl ):
FactoryGirl.define do
factory :event do
title { Faker::Name.name }
description 'A simples description'
photo { Faker::Bitcoin.address }
place 'Anywhere'
address { Faker::Address.street_address }
opening_date Date.today
ending_date Faker::Date.forward(2)
category 'any category'
price 1.99
photo_attachment {File.new("#{Rails.root}/spec/images/rails.png")}
hour '00:00'
end
trait :soccer_game do
title 'Soccer Game'
place 'Maracana Stadium'
opening_date Date.today
hour '14:00'
end
end
Could you help me with this ?

How to get FactoryGirl model attributes with has_and_belongs_to_many association?

I'm trying to get FactoryGirl.attributes_for(:trip) with HABTM association :countries because controller test fails - :countries absent in :trip attributes):
TripsController:
class TripsController < ApplicationController
def create
trip = Trip.new(create_params)
if trip.save
redirect_to trips_path, notice: 'Trip successfully created.'
else
redirect_to :back, alert: trip.errors.full_messages.join('<br>').html_safe
end
end
def create_params
params.require(:trip).permit(:end_date, :description, country_ids: [], countries: [])
end
end
RSpec TripsController test:
describe TripsController do
describe 'POST #create' do
before { post :create, trip: attributes_for(:trip) }
it { is_expected.to redirect_to trips_path }
end
end
Trip model:
class Trip < ActiveRecord::Base
# Associations
has_and_belongs_to_many :countries
#Validations
validate :require_at_least_one_country
private
def require_at_least_one_country
if country_ids.empty? && countries.count == 0
errors.add(:base, 'Please select at least one country')
end
end
end
Trip factory:
FactoryGirl.define do
factory :trip do
description { Faker::Lorem.sentence }
end_date { DateTime.now + 1.day }
after(:build) do |trip, evaluator|
trip.countries << FactoryGirl.create(:country, :with_currencies)
end
end
end
Gemfile:
factory_girl_rails (4.5.0)
Tried this: http://makandracards.com/jan0sch/11111-rails-factorygirl-and-has_and_belongs_to_many, but useless.
Here is the answer with the explanation:
FactoryGirl.define do
factory :trip do
description { Faker::Lorem.sentence }
end_date { DateTime.now + 1.day }
after(:build) do |trip, evaluator|
trip.countries << FactoryGirl.create(:country, :with_currencies)
end
end
end
FactoryGirl.attributes_for(:trip) returns
{
:description=>"Eum alias tenetur odit voluptatibus inventore qui nobis.",
:end_date=>Wed, 16 Sep 2015 11:48:28 +0300
}
.
FactoryGirl.define do
factory :trip do
description { Faker::Lorem.sentence }
end_date { DateTime.now + 1.day }
country_ids { [FactoryGirl.create(:country, :with_currencies).id] }
end
end
FactoryGirl.attributes_for(:trip) returns
{
:description=>"Assumenda sapiente pariatur facilis et architecto in.",
:end_date=>Wed, 16 Sep 2015 11:45:22 +0300,
:country_ids=>[4]
}
Check if you really sending any countries_ids in your request.
Please, update your post with the value of attributes_for(:trip).

RSPEC test NAME ERROR - Undefined Local variable or method

I'm a beginner in ruby on rails and programming in general.
I have an assignment where I have to test my rspec model Vote, and as per instructions the test should pass.
When I run rspec spec/models/vote_spec.rb on the console, I receive the following error:
.F
Failures:
1) Vote after_save calls `Post#update_rank` after save
Failure/Error: post = associated_post
NameError:
undefined local variable or method `associated_post' for #<RSpec::ExampleGroups::Vote::AfterSave:0x007f9416c791e0>
# ./spec/models/vote_spec.rb:22:in `block (3 levels) in <top (required)>'
Finished in 0.28533 seconds (files took 2.55 seconds to load)
2 examples, 1 failure
Failed examples:
rspec ./spec/models/vote_spec.rb:21 # Vote after_save calls `Post#update_rank` after save
Here is my vote_spec code:
require 'rails_helper'
describe Vote do
describe "validations" do
describe "value validation" do
it "only allows -1 or 1 as values" do
up_vote = Vote.new(value: 1)
expect(up_vote.valid?).to eq(true)
down_vote = Vote.new(value: -1)
expect(down_vote.valid?).to eq(true)
invalid_vote = Vote.new(value: 2)
expect(invalid_vote.valid?).to eq(false)
end
end
end
describe 'after_save' do
it "calls `Post#update_rank` after save" do
post = associated_post
vote = Vote.new(value: 1, post: post)
expect(post).to receive(:update_rank)
vote.save
end
end
end
And here is my post_spec code:
require 'rails_helper'
describe Post do
describe "vote method" do
before do
user = User.create
topic = Topic.create
#post = associated_post
3.times { #post.votes.create(value: 1) }
2.times { #post.votes.create(value: -1) }
end
describe '#up_votes' do
it "counts the number of votes with value = 1" do
expect( #post.up_votes ).to eq(3)
end
end
describe '#down_votes' do
it "counts the number of votes with value = -1" do
expect( #post.down_votes ).to eq(2)
end
end
describe '#points' do
it "returns the sum of all down and up votes" do
expect( #post.points).to eq(1) # 3 - 2
end
end
end
describe '#create_vote' do
it "generates an up-vote when explicitly called" do
post = associated_post
expect(post.up_votes ).to eq(0)
post.create_vote
expect( post.up_votes).to eq(1)
end
end
end
def associated_post(options = {})
post_options = {
title: 'Post title',
body: 'Post bodies must be pretty long.',
topic: Topic.create(name: 'Topic name',description: 'the description of a topic must be long'),
user: authenticated_user
}.merge(options)
Post.create(post_options)
end
def authenticated_user(options = {})
user_options = { email: "email#{rand}#fake.com", password: 'password'}.merge(options)
user = User.new( user_options)
user.skip_confirmation!
user.save
user
end
I'm not sure if providing the Post and Vote models code is necessary.
Here is my Post model:
class Post < ActiveRecord::Base
has_many :votes, dependent: :destroy
has_many :comments, dependent: :destroy
belongs_to :user
belongs_to :topic
default_scope { order('rank DESC')}
validates :title, length: { minimum: 5 }, presence: true
validates :body, length: { minimum: 20 }, presence: true
validates :user, presence: true
validates :topic, presence: true
def up_votes
votes.where(value: 1).count
end
def down_votes
votes.where(value: -1).count
end
def points
votes.sum(:value)
end
def update_rank
age_in_days = ( created_at - Time.new(1970,1,1)) / (60 * 60 * 24)
new_rank = points + age_in_days
update_attribute(:rank, new_rank)
end
def create_vote
user.votes.create(value: 1, post: self)
# user.votes.create(value: 1, post: self)
# self.user.votes.create(value: 1, post: self)
# votes.create(value: 1, user: user)
# self.votes.create(value: 1, user: user)
# vote = Vote.create(value: 1, user: user, post: self)
# self.votes << vote
# save
end
end
and the Vote model:
class Vote < ActiveRecord::Base
belongs_to :post
belongs_to :user
validates :value, inclusion: { in: [-1, 1], message: "%{value} is not a valid vote."}
after_save :update_post
def update_post
post.update_rank
end
end
It seems like in the spec vote model, the method assosicated_post can't be retrieved from the post spec model?
You're absolutely right - because you defined the associated post method inside of post_spec.rb, it can't be called from inside vote_spec.rb.
You have a couple options: you can copy your associated post method and put it inside vote_spec.rb, or you can create a spec helper file where you define associated_post once and include it in both vote_spec.rb and post_spec.rb. Hope that helps!

How to Test a Class Method in Rspec that has a Has Many Through Association

How would I test the class method .trending in Rspec considering that it has a has many through association. .trending works but it currently has not been properly vetted in Rspec. Any advice?
class Author < ActiveRecord::Base
has_many :posts
has_many :comments, through: :posts
validates :name, presence: true
validate :name_length
def self.trending
hash = {}
all.each{|x|
hash[x.id] = x.comments.where("comments.created_at >= ?", Time.zone.now - 7.days).count
}
new_hash = hash.sort_by {|k,v| v}.reverse!.to_h
new_hash.delete_if {|k, v| v < 1 }
new_hash.map do |k,v,|
self.find(k)
end
end
private
def name_length
unless name.nil?
if name.length < 2
errors.add(:name, 'must be longer than 1 character')
end
end
end
end
Test I attempted to use (it didn't work)
describe ".trending" do
it "an instance of Author should be able to return trending" do
#author = FactoryGirl.build(:author, name:'drew', created_at: Time.now - 11.years, id: 1)
#post = #author.posts.build(id: 1, body:'hello', subject:'hello agains', created_at: Time.now - 10.years)
#comment1 = #post.comments.build(id: 1, body: 'this is the body', created_at: Time.now - 9.years)
#comment2 = #post.comments.build(id: 2, body: 'this was the body', created_at: Time.now - 8.years)
#comment3 = #post.comments.build(id: 3, body: 'this shall be the body', created_at: Time.now - 7.minutes)
Author.trending.should include(#comment3)
end
end
Neither FactoryGirl.build nor ActiveRecord::Relation#build persists a record to the database—they just return an un-saved instance of the object—but Author.trending is looking for records in the database. You should either call save on the instances to persist them to the database, or use create instead of build.

NameError: uninitialized constant Shipment with Rspec

I'm trying to write rspec test cases with rspec-mock. But, I got an error as follow. order_decorator_spec.rb:149 is let!(:order) { create(:order, shipments: [shipment]) }. It doesn't show any stacktrace perhaps bacause it is executed on RubyMine console. How can I fix the problem??
console
NameError: uninitialized constant Shipment
./spec/models/spree/order_decorator_spec.rb:149:in `block (3 levels) in <top (required)>'
order_decorator_spec.rb
require 'spec_helper'
describe Spree::Order do
context '#after_cancel' do
let!(:shipment) { double(:shipment, :cancel! => '')}
before {shipment}
let!(:order) { create(:order, shipments: [shipment]) }
before do
order.stub(:after_cancel)
end
it "should adsfasdfasdfa" do
shipment.should_receive(:cancel!).once
order.after_cancel
end
end
end
order.rb
require 'spree/core/validators/email'
require 'spree/order/checkout'
module Spree
class Order < ActiveRecord::Base
include Checkout
checkout_flow do
go_to_state :address
go_to_state :delivery
go_to_state :payment, if: ->(order) {
order.update_totals
order.payment_required?
}
go_to_state :confirm, if: ->(order) { order.confirmation_required? }
go_to_state :complete
remove_transition from: :delivery, to: :confirm
end
token_resource
attr_reader :coupon_code
if Spree.user_class
belongs_to :user, class_name: Spree.user_class.to_s
belongs_to :created_by, class_name: Spree.user_class.to_s
else
belongs_to :user
belongs_to :created_by
end
belongs_to :bill_address, foreign_key: :bill_address_id, class_name: 'Spree::Address'
alias_attribute :billing_address, :bill_address
belongs_to :ship_address, foreign_key: :ship_address_id, class_name: 'Spree::Address'
alias_attribute :shipping_address, :ship_address
has_many :state_changes, as: :stateful
has_many :line_items, -> { order('created_at ASC') }, dependent: :destroy
has_many :payments, dependent: :destroy
has_many :return_authorizations, dependent: :destroy
has_many :adjustments, -> { order("#{Adjustment.table_name}.created_at ASC") }, as: :adjustable, dependent: :destroy
has_many :line_item_adjustments, through: :line_items, source: :adjustments
has_many :all_adjustments, class_name: 'Spree::Adjustment'
has_many :inventory_units
has_many :shipments, dependent: :destroy do
def states
pluck(:state).uniq
end
end
accepts_nested_attributes_for :line_items
accepts_nested_attributes_for :bill_address
accepts_nested_attributes_for :ship_address
accepts_nested_attributes_for :payments
accepts_nested_attributes_for :shipments
# Needs to happen before save_permalink is called
before_validation :set_currency
before_validation :generate_order_number, on: :create
before_validation :clone_billing_address, if: :use_billing?
attr_accessor :use_billing
before_create :link_by_email
after_create :create_tax_charge!
validates :email, presence: true, if: :require_email
validates :email, email: true, if: :require_email, allow_blank: true
validate :has_available_shipment
validate :has_available_payment
make_permalink field: :number
class_attribute :update_hooks
self.update_hooks = Set.new
def self.by_number(number)
where(number: number)
end
def self.between(start_date, end_date)
where(created_at: start_date..end_date)
end
def self.by_customer(customer)
joins(:user).where("#{Spree.user_class.table_name}.email" => customer)
end
def self.by_state(state)
where(state: state)
end
def self.complete
where.not(completed_at: nil)
end
def self.incomplete
where(completed_at: nil)
end
# Use this method in other gems that wish to register their own custom logic
# that should be called after Order#update
def self.register_update_hook(hook)
self.update_hooks.add(hook)
end
# For compatiblity with Calculator::PriceSack
def amount
line_items.inject(0.0) { |sum, li| sum + li.amount }
end
def currency
self[:currency] || Spree::Config[:currency]
end
def display_outstanding_balance
Spree::Money.new(outstanding_balance, { currency: currency })
end
def display_item_total
Spree::Money.new(item_total, { currency: currency })
end
def display_adjustment_total
Spree::Money.new(adjustment_total, { currency: currency })
end
def display_tax_total
Spree::Money.new(tax_total, { currency: currency })
end
def display_ship_total
Spree::Money.new(ship_total, { currency: currency })
end
def display_total
Spree::Money.new(total, { currency: currency })
end
def to_param
number.to_s.to_url.upcase
end
def completed?
completed_at.present? || complete?
end
# Indicates whether or not the user is allowed to proceed to checkout.
# Currently this is implemented as a check for whether or not there is at
# least one LineItem in the Order. Feel free to override this logic in your
# own application if you require additional steps before allowing a checkout.
def checkout_allowed?
line_items.count > 0
end
# Is this a free order in which case the payment step should be skipped
def payment_required?
total.to_f > 0.0
end
# If true, causes the confirmation step to happen during the checkout process
def confirmation_required?
Spree::Config[:always_include_confirm_step] ||
payments.valid.map(&:payment_method).compact.any?(&:payment_profiles_supported?) ||
# Little hacky fix for #4117
# If this wasn't here, order would transition to address state on confirm failure
# because there would be no valid payments any more.
state == 'confirm'
end
# Indicates the number of items in the order
def item_count
line_items.inject(0) { |sum, li| sum + li.quantity }
end
def backordered?
shipments.any?(&:backordered?)
end
# Returns the relevant zone (if any) to be used for taxation purposes.
# Uses default tax zone unless there is a specific match
def tax_zone
Zone.match(tax_address) || Zone.default_tax
end
# Indicates whether tax should be backed out of the price calcualtions in
# cases where prices include tax but the customer is not required to pay
# taxes in that case.
def exclude_tax?
return false unless Spree::Config[:prices_inc_tax]
return tax_zone != Zone.default_tax
end
# Returns the address for taxation based on configuration
def tax_address
Spree::Config[:tax_using_ship_address] ? ship_address : bill_address
end
# Array of totals grouped by Adjustment#label. Useful for displaying line item
# adjustments on an invoice. For example, you can display tax breakout for
# cases where tax is included in price.
def line_item_adjustment_totals
Hash[self.line_item_adjustments.eligible.group_by(&:label).map do |label, adjustments|
total = adjustments.sum(&:amount)
[label, Spree::Money.new(total, { currency: currency })]
end]
end
def updater
#updater ||= OrderUpdater.new(self)
end
def update!
updater.update
end
def update_totals
updater.update_totals
end
def clone_billing_address
if bill_address and self.ship_address.nil?
self.ship_address = bill_address.clone
else
self.ship_address.attributes = bill_address.attributes.except('id', 'updated_at', 'created_at')
end
true
end
def allow_cancel?
return false unless completed? and state != 'canceled'
shipment_state.nil? || %w{ready backorder pending}.include?(shipment_state)
end
def awaiting_returns?
return_authorizations.any? { |return_authorization| return_authorization.authorized? }
end
def contents
#contents ||= Spree::OrderContents.new(self)
end
# Associates the specified user with the order.
def associate_user!(user)
self.user = user
self.email = user.email
self.created_by = user if self.created_by.blank?
if persisted?
# immediately persist the changes we just made, but don't use save since we might have an invalid address associated
self.class.unscoped.where(id: id).update_all(email: user.email, user_id: user.id, created_by_id: self.created_by_id)
end
end
# FIXME refactor this method and implement validation using validates_* utilities
def generate_order_number
record = true
while record
random = "R#{Array.new(9){rand(9)}.join}"
record = self.class.where(number: random).first
end
self.number = random if self.number.blank?
self.number
end
def shipped_shipments
shipments.shipped
end
def contains?(variant)
find_line_item_by_variant(variant).present?
end
def quantity_of(variant)
line_item = find_line_item_by_variant(variant)
line_item ? line_item.quantity : 0
end
def find_line_item_by_variant(variant)
line_items.detect { |line_item| line_item.variant_id == variant.id }
end
def ship_total
adjustments.shipping.sum(:amount)
end
# Creates new tax charges if there are any applicable rates. If prices already
# include taxes then price adjustments are created instead.
def create_tax_charge!
Spree::TaxRate.adjust(self)
end
def outstanding_balance
total - payment_total
end
def outstanding_balance?
self.outstanding_balance != 0
end
def name
if (address = bill_address || ship_address)
"#{address.firstname} #{address.lastname}"
end
end
def can_ship?
self.complete? || self.resumed? || self.awaiting_return? || self.returned?
end
def credit_cards
credit_card_ids = payments.from_credit_card.pluck(:source_id).uniq
CreditCard.where(id: credit_card_ids)
end
# Finalizes an in progress order after checkout is complete.
# Called after transition to complete state when payments will have been processed
def finalize!
touch :completed_at
# lock all adjustments (coupon promotions, etc.)
adjustments.update_all state: 'closed'
# update payment and shipment(s) states, and save
updater.update_payment_state
shipments.each do |shipment|
shipment.update!(self)
shipment.finalize!
end
updater.update_shipment_state
save
updater.run_hooks
deliver_order_confirmation_email
end
def deliver_order_confirmation_email
begin
OrderMailer.confirm_email(self.id).deliver
rescue Exception => e
logger.error("#{e.class.name}: #{e.message}")
logger.error(e.backtrace * "\n")
end
end
# Helper methods for checkout steps
def paid?
payment_state == 'paid' || payment_state == 'credit_owed'
end
def available_payment_methods
#available_payment_methods ||= (PaymentMethod.available(:front_end) + PaymentMethod.available(:both)).uniq
end
def pending_payments
payments.select(&:checkout?)
end
# processes any pending payments and must return a boolean as it's
# return value is used by the checkout state_machine to determine
# success or failure of the 'complete' event for the order
#
# Returns:
# - true if all pending_payments processed successfully
# - true if a payment failed, ie. raised a GatewayError
# which gets rescued and converted to TRUE when
# :allow_checkout_gateway_error is set to true
# - false if a payment failed, ie. raised a GatewayError
# which gets rescued and converted to FALSE when
# :allow_checkout_on_gateway_error is set to false
#
def process_payments!
if pending_payments.empty?
raise Core::GatewayError.new Spree.t(:no_pending_payments)
else
pending_payments.each do |payment|
break if payment_total >= total
payment.process!
if payment.completed?
self.payment_total += payment.amount
end
end
end
rescue Core::GatewayError => e
result = !!Spree::Config[:allow_checkout_on_gateway_error]
errors.add(:base, e.message) and return result
end
def billing_firstname
bill_address.try(:firstname)
end
def billing_lastname
bill_address.try(:lastname)
end
def products
line_items.map(&:product)
end
def variants
line_items.map(&:variant)
end
def insufficient_stock_lines
line_items.select &:insufficient_stock?
end
def merge!(order, user = nil)
order.line_items.each do |line_item|
next unless line_item.currency == currency
current_line_item = self.line_items.find_by(variant: line_item.variant)
if current_line_item
current_line_item.quantity += line_item.quantity
current_line_item.save
else
line_item.order_id = self.id
line_item.save
end
end
self.associate_user!(user) if !self.user && !user.blank?
# So that the destroy doesn't take out line items which may have been re-assigned
order.line_items.reload
order.destroy
end
(snip)
(snip)
(snip)
(snip)
(snip)
(snip)
(snip)
(snip)
end
end
shipment_decorator.rb
require 'ostruct'
module Spree
Shipment.class_eval do
def after_ship
inventory_units.each &:ship!
adjustment.finalize!
#send_shipped_email
touch :shipped_at
end
end
end
shipment.rb
require 'ostruct'
module Spree
class Shipment < ActiveRecord::Base
belongs_to :order, class_name: 'Spree::Order', touch: true
belongs_to :address, class_name: 'Spree::Address'
belongs_to :stock_location, class_name: 'Spree::StockLocation'
has_many :shipping_rates, dependent: :delete_all
has_many :shipping_methods, through: :shipping_rates
has_many :state_changes, as: :stateful
has_many :inventory_units, dependent: :delete_all
has_one :adjustment, as: :source, dependent: :destroy
after_save :ensure_correct_adjustment, :update_order
attr_accessor :special_instructions
accepts_nested_attributes_for :address
accepts_nested_attributes_for :inventory_units
make_permalink field: :number, length: 11, prefix: 'H'
scope :shipped, -> { with_state('shipped') }
scope :ready, -> { with_state('ready') }
scope :pending, -> { with_state('pending') }
scope :with_state, ->(*s) { where(state: s) }
scope :trackable, -> { where("tracking IS NOT NULL AND tracking != ''") }
# shipment state machine (see http://github.com/pluginaweek/state_machine/tree/master for details)
state_machine initial: :pending, use_transactions: false do
event :ready do
transition from: :pending, to: :ready, if: lambda { |shipment|
# Fix for #2040
shipment.determine_state(shipment.order) == 'ready'
}
end
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 to_param
number
end
def backordered?
inventory_units.any? { |inventory_unit| inventory_unit.backordered? }
end
def shipped=(value)
return unless value == '1' && shipped_at.nil?
self.shipped_at = Time.now
end
def shipping_method
selected_shipping_rate.try(:shipping_method) || shipping_rates.first.try(:shipping_method)
end
def add_shipping_method(shipping_method, selected = false)
shipping_rates.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 refresh_rates
return shipping_rates if shipped?
return [] unless can_get_rates?
# StockEstimator.new assigment below will replace the current shipping_method
original_shipping_method_id = shipping_method.try(:id)
self.shipping_rates = Stock::Estimator.new(order).shipping_rates(to_package)
if shipping_method
selected_rate = shipping_rates.detect { |rate|
rate.shipping_method_id == original_shipping_method_id
}
self.selected_shipping_rate_id = selected_rate.id if selected_rate
end
shipping_rates
end
def currency
order ? order.currency : Spree::Config[:currency]
end
# The adjustment amount associated with this shipment (if any.) Returns only the first adjustment to match
# the shipment but there should never really be more than one.
def cost
adjustment ? adjustment.amount : 0
end
alias_method :amount, :cost
def display_cost
Spree::Money.new(cost, { currency: currency })
end
alias_method :display_amount, :display_cost
def item_cost
line_items.map(&:amount).sum
end
def display_item_cost
Spree::Money.new(item_cost, { currency: currency })
end
def total_cost
cost + item_cost
end
def display_total_cost
Spree::Money.new(total_cost, { currency: currency })
end
def editable_by?(user)
!shipped?
end
def manifest
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
def line_items
if order.complete? and Spree::Config.track_inventory_levels
order.line_items.select { |li| !li.should_track_inventory? || inventory_units.pluck(:variant_id).include?(li.variant_id) }
else
order.line_items
end
end
def finalize!
InventoryUnit.finalize_units!(inventory_units)
manifest.each { |item| manifest_unstock(item) }
end
def after_cancel
manifest.each { |item| manifest_restock(item) }
end
def after_resume
manifest.each { |item| manifest_unstock(item) }
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.
def update!(order)
old_state = state
new_state = determine_state(order)
update_column :state, new_state
after_ship if new_state == 'shipped' and old_state != 'shipped'
end
# Determines the appropriate +state+ according to the following logic:
#
# pending unless order is complete and +order.payment_state+ is +paid+
# 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'
order.paid? ? 'ready' : 'pending'
end
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.includes(:variant).each do |inventory_unit|
package.add inventory_unit.variant, 1, inventory_unit.state_name
end
package
end
def set_up_inventory(state, variant, order)
self.inventory_units.create(variant_id: variant.id, state: state, order_id: order.id)
end
private
def manifest_unstock(item)
stock_location.unstock item.variant, item.quantity, self
end
def manifest_restock(item)
if item.states["on_hand"].to_i > 0
stock_location.restock item.variant, item.states["on_hand"], self
end
if item.states["backordered"].to_i > 0
stock_location.restock_backordered item.variant, item.states["backordered"]
end
end
def description_for_shipping_charge
"#{Spree.t(:shipping)} (#{shipping_method.name})"
end
def after_ship
inventory_units.each &:ship!
adjustment.finalize!
send_shipped_email
touch :shipped_at
end
def send_shipped_email
ShipmentMailer.shipped_email(self.id).deliver
end
def ensure_correct_adjustment
if adjustment
adjustment.originator = shipping_method
adjustment.label = shipping_method.adjustment_label
adjustment.amount = selected_shipping_rate.cost if adjustment.open?
adjustment.save!
adjustment.reload
elsif selected_shipping_rate_id
shipping_method.create_adjustment shipping_method.adjustment_label, order, self, true, "open"
reload #ensure adjustment is present on later saves
end
end
def update_order
order.update!
end
def can_get_rates?
order.ship_address && order.ship_address.valid?
end
end
end

Resources