schof / spree forked from railsdog/spree

This is my personal fork of the Spree project. You want railsdog/spree for the official Spree repository.

This URL has Read+Write access

David North (author)
Wed Dec 09 09:01:38 -0800 2009
Sean Schofield (committer)
Fri Dec 11 10:50:25 -0800 2009
spree / app / models / order.rb
100644 297 lines (244 sloc) 9.191 kb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
class Order < ActiveRecord::Base
  module Totaling
    def total
      map(&:amount).sum
    end
  end
 
  before_create :generate_token
  before_save :update_line_items, :update_totals
  after_create :create_checkout_and_shippment, :create_tax_charge
 
  belongs_to :user
  has_many :state_events, :as => :stateful
 
  has_many :line_items, :extend => Totaling, :dependent => :destroy
  has_many :inventory_units
 
  has_many :payments, :extend => Totaling
  has_many :creditcard_payments, :extend => Totaling
 
  has_one :checkout
  has_one :bill_address, :through => :checkout
  has_many :shipments, :dependent => :destroy
 
  has_many :adjustments, :extend => Totaling, :order => :position
  has_many :charges, :extend => Totaling, :order => :position
  has_many :credits, :extend => Totaling, :order => :position
  has_many :shipping_charges, :extend => Totaling, :order => :position
  has_many :tax_charges, :extend => Totaling, :order => :position
  has_many :coupon_credits, :extend => Totaling, :order => :position
  has_many :non_zero_charges, :extend => Totaling, :order => :position,
           :class_name => "Charge", :conditions => ["amount != 0"]
 
  accepts_nested_attributes_for :checkout
  accepts_nested_attributes_for :line_items
 
  def ship_address; shipment.address; end
  delegate :shipping_method, :to =>:shipment
  delegate :email, :to => :checkout
  delegate :ip_address, :to => :checkout
  delegate :special_instructions, :to => :checkout
 
  validates_numericality_of :item_total
  validates_numericality_of :total
 
  named_scope :by_number, lambda {|number| {:conditions => ["orders.number = ?", number]}}
  named_scope :between, lambda {|*dates| {:conditions => ["orders.created_at between :start and :stop", {:start => dates.first.to_date, :stop => dates.last.to_date}]}}
  named_scope :by_customer, lambda {|customer| {:include => :user, :conditions => ["users.email = ?", customer]}}
  named_scope :by_state, lambda {|state| {:conditions => ["state = ?", state]}}
  named_scope :checkout_complete, {:include => :checkout, :conditions => ["orders.completed_at IS NOT NULL"]}
  make_permalink :field => :number
 
  # attr_accessible is a nightmare with attachment_fu, so use attr_protected instead.
  attr_protected :charge_total, :item_total, :total, :user, :number, :state, :token
 
  def checkout_complete; !!completed_at; end
 
  def to_param
    self.number if self.number
    generate_order_number unless self.number
    self.number.parameterize.to_s.upcase
  end
 
  # order state machine (see http://github.com/pluginaweek/state_machine/tree/master for details)
  state_machine :initial => 'in_progress' do
    after_transition :to => 'in_progress', :do => lambda {|order| order.update_attribute(:checkout_complete, false)}
    after_transition :to => 'new', :do => :complete_order
    after_transition :to => 'canceled', :do => :cancel_order
    after_transition :to => 'returned', :do => :restock_inventory
    after_transition :to => 'resumed', :do => :restore_state
    after_transition :to => 'shipped', :do => :make_shipments_shipped
 
    event :complete do
      transition :to => 'new', :from => 'in_progress'
    end
    event :cancel do
      transition :to => 'canceled', :if => :allow_cancel?
    end
    event :return do
      transition :to => 'returned', :from => 'shipped'
    end
    event :resume do
      transition :to => 'resumed', :from => 'canceled', :if => :allow_resume?
    end
    event :pay do
      transition :to => 'paid', :if => :allow_pay?
    end
    event :under_paid do
      transition :to => 'balance_due', :from => ['paid', 'new', 'credit_owed']
    end
    event :over_paid do
      transition :to => 'credit_owed', :from => ['paid', 'new', 'balance_due']
    end
    event :ship do
      transition :to => 'shipped', :from => 'paid'
    end
  end
 
  def restore_state
    # pop the resume event so we can see what the event before that was
    state_events.pop if state_events.last.name == "resume"
    update_attribute("state", state_events.last.previous_state)
  end
  
  def make_shipments_shipped
    shipments.reject(&:shipped?).each do |shipment|
      shipment.update_attributes(:state => 'shipped', :shipped_at => Time.now)
    end
  end
  
 
  def allow_cancel?
    self.state != 'canceled'
  end
 
  def allow_resume?
    # we shouldn't allow resume for legacy orders b/c we lack the information necessary to restore to a previous state
    return false if state_events.empty? || state_events.last.previous_state.nil?
    true
  end
 
  def allow_pay?
    checkout_complete
  end
 
  def add_variant(variant, quantity=1)
    current_item = contains?(variant)
    if current_item
      current_item.increment_quantity unless quantity > 1
      current_item.quantity = (current_item.quantity + quantity) if quantity > 1
      current_item.save
    else
      current_item = LineItem.new(:quantity => quantity)
      current_item.variant = variant
      current_item.price = variant.price
      self.line_items << current_item
    end
 
    # populate line_items attributes for additional_fields entries
    # that have populate => [:line_item]
    Variant.additional_fields.select{|f| !f[:populate].nil? && f[:populate].include?(:line_item) }.each do |field|
      value = ""
 
      if field[:only].nil? || field[:only].include?(:variant)
        value = variant.send(field[:name].gsub(" ", "_").downcase)
      elsif field[:only].include?(:product)
        value = variant.product.send(field[:name].gsub(" ", "_").downcase)
      end
      current_item.update_attribute(field[:name].gsub(" ", "_").downcase, value)
    end
 
    current_item
  end
 
  def generate_order_number
    record = true
    while record
      random = "R#{Array.new(9){rand(9)}.join}"
      record = Order.find(:first, :conditions => ["number = ?", random])
    end
    self.number = random
  end
 
  # convenience method since many stores will not allow user to create multiple shipments
  def shipment
    shipments.last
  end
 
  def contains?(variant)
    line_items.select { |line_item| line_item.variant == variant }.first
  end
 
  def grant_access?(token=nil)
    return true if token && token == self.token
    return false unless current_user_session = UserSession.find
    return current_user_session.user == self.user
  end
  def mark_shipped
    inventory_units.each do |inventory_unit|
      inventory_unit.ship!
    end
  end
 
  # collection of available shipping countries
  def shipping_countries
    ShippingMethod.all.collect { |method| method.zone.country_list }.flatten.uniq.sort_by {|item| item.send 'name'}
  end
 
  def shipping_methods
    return [] unless ship_address
    ShippingMethod.all.select { |method| method.zone.include?(ship_address) && method.available?(self) }
  end
 
  def payment_total
    payments.reload.total
  end
 
  def ship_total
    shipping_charges.reload.total
  end
 
  def tax_total
    tax_charges.reload.total
  end
 
  def credit_total
    credits.reload.total.abs
  end
 
  def charge_total
    charges.reload.total
  end
 
  def create_tax_charge
    if tax_charges.empty?
      tax_charges.create({
          :order => self,
          :description => I18n.t(:tax),
          :adjustment_source => self
      })
    end
  end
 
  def update_totals(force_adjustment_recalculation=false)
    self.item_total = self.line_items.total
 
    # save the items which might be changed by an order update, so that
    # charges can be recalculated accurately.
    self.line_items.map(&:save)
 
    if !self.checkout_complete || force_adjustment_recalculation
      applicable_adjustments, adjustments_to_destroy = adjustments.partition{|a| a.applicable?}
      self.adjustments = applicable_adjustments
      adjustments_to_destroy.each(&:destroy)
    end
 
    self.adjustment_total = self.charge_total - self.credit_total
 
    self.total = self.item_total + self.adjustment_total
  end
 
  def update_totals!
    update_totals
    save!
  end
 
  def update_adjustments
    self.adjustments.each(&:update_amount)
    update_totals(:force_adjustment_update)
    self
  end
 
  private
 
  def complete_order
    self.adjustments.each(&:update_amount)
    update_attribute(:completed_at, Time.now)
 
    if email
      OrderMailer.deliver_confirm(self)
    end
    begin
      InventoryUnit.sell_units(self)
      save!
    rescue Exception => e
      logger.error "Problem saving authorized order: #{e.message}"
      logger.error self.to_yaml
    end
  end
 
  def cancel_order
    restock_inventory
    OrderMailer.deliver_cancel(self)
  end
 
  def restock_inventory
    inventory_units.each do |inventory_unit|
      inventory_unit.restock! if inventory_unit.can_restock?
    end
  end
 
  def update_line_items
    to_wipe = self.line_items.select {|li| 0 == li.quantity || li.quantity.nil? }
    LineItem.destroy(to_wipe)
    self.line_items -= to_wipe # important: remove defunct items, avoid a reload
  end
 
  def generate_token
    self.token = Authlogic::Random.friendly_token
  end
 
  def create_checkout_and_shippment
    self.shipments << Shipment.create(:order => self)
    self.checkout ||= Checkout.create(:order => self)
  end
end