-
-
Notifications
You must be signed in to change notification settings - Fork 725
/
shipment.rb
360 lines (287 loc) · 10.4 KB
/
shipment.rb
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
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
# frozen_string_literal: true
require 'ostruct'
module Spree
class Shipment < ApplicationRecord
belongs_to :order, class_name: 'Spree::Order'
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_many :adjustments, as: :adjustable, dependent: :destroy
before_create :generate_shipment_number
after_save :ensure_correct_adjustment, :update_adjustments
attr_accessor :special_instructions
alias_attribute :amount, :cost
accepts_nested_attributes_for :address
accepts_nested_attributes_for :inventory_units
make_permalink field: :number
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
generate_shipment_number unless number
number.parameterize.upcase
end
def backordered?
inventory_units.any?(&:backordered?)
end
def shipped=(value)
return unless value == '1' && shipped_at.nil?
self.shipped_at = Time.zone.now
end
def shipping_method
method = selected_shipping_rate.try(:shipping_method)
method ||= shipping_rates.first.try(:shipping_method) unless order.manual_shipping_selection
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.find_by(selected: true)
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)
save!
end
def tax_category
selected_shipping_rate.try(:shipping_method).try(:tax_category)
end
def refresh_rates
return shipping_rates if shipped?
# The call to Stock::Estimator below will replace the current shipping_method
original_shipping_method_id = shipping_method.try(:id)
self.shipping_rates = OrderManagement::Stock::Estimator.new(order).shipping_rates(to_package)
keep_original_shipping_method_selection(original_shipping_method_id)
shipping_rates
end
def keep_original_shipping_method_selection(original_shipping_method_id)
return if shipping_method&.id == original_shipping_method_id
rate_for_original_shipping_method = find_shipping_rate_for(original_shipping_method_id)
if rate_for_original_shipping_method.present?
self.selected_shipping_rate_id = rate_for_original_shipping_method.id
else
# If there's no original ship method to keep, or if it cannot be found on the ship rates
# But there's a new ship method selected (first if clause in this method)
# We need to save the shipment so that callbacks are triggered
save!
end
end
def find_shipping_rate_for(shipping_method_id)
return unless shipping_method_id
shipping_rates.detect { |rate|
rate.shipping_method_id == shipping_method_id
}
end
def currency
order ? order.currency : Spree::Config[:currency]
end
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 update_amounts
return unless fee_adjustment&.amount != cost
update_columns(
cost: fee_adjustment&.amount || 0.0,
updated_at: Time.zone.now
)
recalculate_adjustments
end
def manifest
inventory_units.group_by(&:variant).map do |variant, units|
states = {}
units.group_by(&:state).each { |state, iu| states[state] = iu.count }
scoper.scope(variant)
OpenStruct.new(variant: variant, quantity: units.length, states: states)
end
end
def scoper
@scoper ||= OpenFoodNetwork::ScopeVariantToHub.new(order.distributor)
end
def finalize!
InventoryUnit.finalize_units!(inventory_units)
manifest.each { |item| manifest_unstock(item) }
end
def after_cancel
manifest.each { |item| manifest_restock(item) } if order.restock_items
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_columns(
state: new_state,
updated_at: Time.zone.now
)
after_ship if new_state == 'shipped' && 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 = OrderManagement::Stock::Package.new(stock_location, order)
grouped_inventory_units = inventory_units.includes(:variant).group_by do |iu|
[iu.variant, iu.state_name]
end
grouped_inventory_units.each do |(variant, state_name), inventory_units|
package.add variant, inventory_units.count, state_name
end
package
end
def set_up_inventory(state, variant, order)
inventory_units.create(variant_id: variant.id, state: state, order_id: order.id)
end
def fee_adjustment
@fee_adjustment ||= adjustments.shipping.first
end
def ensure_correct_adjustment
if fee_adjustment
fee_adjustment.originator = shipping_method
fee_adjustment.label = adjustment_label
fee_adjustment.amount = selected_shipping_rate.cost if fee_adjustment.open?
fee_adjustment.save!
fee_adjustment.reload
elsif shipping_method
shipping_method.create_adjustment(adjustment_label,
self,
true,
"open")
reload # ensure adjustment is present on later saves
end
update_amounts
end
def adjustment_label
I18n.t('shipping')
end
def can_modify?
!shipped? && !order.canceled?
end
private
def line_items
if order.complete?
inventory_unit_ids = inventory_units.pluck(:variant_id)
order.line_items.select { |li| inventory_unit_ids.include?(li.variant_id) }
else
order.line_items
end
end
def manifest_unstock(item)
stock_location.unstock item.variant, item.quantity, self
end
def manifest_restock(item)
stock_location.restock item.variant, item.quantity, self
end
def generate_shipment_number
return number if number.present?
record = true
while record
random = "H#{Array.new(11) { rand(9) }.join}"
record = self.class.default_scoped.find_by(number: random)
end
self.number = random
end
def description_for_shipping_charge
"#{Spree.t(:shipping)} (#{shipping_method.name})"
end
def validate_shipping_method
return if shipping_method.nil?
return if shipping_method.include?(address)
errors.add :shipping_method, Spree.t(:is_not_available_to_shipment_address)
end
def after_ship
inventory_units.each(&:ship!)
fee_adjustment.finalize!
send_shipped_email
touch :shipped_at
update_order_shipment_state
end
def update_order_shipment_state
new_state = order.updater.update_shipment_state
order.update_columns(
shipment_state: new_state,
updated_at: Time.zone.now,
)
end
def send_shipped_email
delivery = !!shipping_method.require_ship_address
ShipmentMailer.shipped_email(id, delivery: delivery).deliver_later
end
def update_adjustments
return unless cost_changed? && state != 'shipped'
recalculate_adjustments
end
def recalculate_adjustments
Spree::ItemAdjustments.new(self).update
end
end
end