Class: Spree::Shipment
- Inherits:
-
Object
- Object
- Spree::Shipment
- Extended by:
- DisplayMoney
- Includes:
- Metadata, Metafields, NumberIdentifier, Spree::Security::Shipments, CustomEvents, Webhooks, VendorConcern
- Defined in:
- app/models/spree/shipment.rb,
app/models/spree/shipment/webhooks.rb,
app/models/spree/shipment/custom_events.rb
Defined Under Namespace
Modules: CustomEvents, Webhooks Classes: ManifestItem
Instance Attribute Summary collapse
-
#special_instructions ⇒ Object
Returns the value of attribute special_instructions.
Instance Method Summary collapse
- #add_shipping_method(shipping_method, selected = false) ⇒ Object
- #after_cancel ⇒ Object
- #after_resume ⇒ Object
- #amount ⇒ Object
-
#backordered? ⇒ Boolean
Returns true if the shipment has any backordered inventory units.
-
#cost=(value) ⇒ Object
Strict decimal bridge for string input — raises ArgumentError on malformed values instead of letting Rails' cast silently truncate ("12 boxes" would otherwise become 12.0).
-
#determine_state(order) ⇒ Object
Determines the appropriate
stateaccording to the following logic:. - #digital? ⇒ Boolean
- #discounted_cost ⇒ Object (also: #discounted_amount)
- #final_price ⇒ Object (also: #total)
- #final_price_with_items ⇒ Object
- #finalize! ⇒ Object
- #free? ⇒ Boolean
- #include?(variant) ⇒ Boolean
- #inventory_units_for(variant) ⇒ Object
- #inventory_units_for_item(line_item, variant = nil) ⇒ Object
-
#item_cost ⇒ BigDecimal
Returns the cost of the shipment.
-
#item_quantity ⇒ Object
Returns the total quantity of all line items in the shipment.
-
#item_weight ⇒ BigDecimal
Returns the weight of the shipment.
- #line_items ⇒ Object
- #manifest ⇒ Object
-
#name ⇒ String
Returns the shipment number and shipping method name.
-
#partial? ⇒ Boolean
Returns true if not all of the shipment's line items are fully shipped.
- #process_order_payments ⇒ Object
- #ready_or_pending? ⇒ Boolean
- #refresh_rates(shipping_method_filter = ShippingMethod::DISPLAY_ON_FRONT_END) ⇒ Object
-
#selected_delivery_rate_id ⇒ String?
Public API v3 name for the selected rate (see docs/plans/6.0-fulfillment-and-delivery.md).
-
#selected_delivery_rate_id=(id) ⇒ Object
Selects a delivery rate by its public prefixed ID (+dr_...+); raw IDs are accepted for internal callers.
- #selected_shipping_rate_id ⇒ Object
- #selected_shipping_rate_id=(id) ⇒ Object
- #set_up_inventory(state, variant, order, line_item, quantity = 1) ⇒ Object
-
#shippable? ⇒ Boolean
Returns true if the shipment is shippable.
- #shipped=(value) ⇒ Object
-
#shipping_method ⇒ Spree::ShippingMethod
Returns the shipping method of the selected shipping rate.
-
#tax_category ⇒ Spree::TaxCategory
Returns the tax category of the selected shipping rate.
-
#tax_category_id ⇒ Integer
Returns the tax category ID of the selected shipping rate.
-
#tax_total ⇒ Object
Only one of either included_tax_total or additional_tax_total is set This method returns the total of the two.
-
#taxable_basis ⇒ BigDecimal
Returns the amount this shipment is taxed on: its discounted cost, never negative (stacked shipping promotions can push discounted_cost below zero).
- #to_package ⇒ Object
-
#tracked? ⇒ Boolean
Returns true if the shipment is tracked.
-
#tracking_url ⇒ String?
External systems (3PLs, courier APIs) often hand over a complete tracking link rather than a bare tracking code — returned as-is instead of being templated into the delivery method's tracking URL.
- #transfer_to_location(variant, quantity, stock_location) ⇒ Object
- #transfer_to_shipment(variant, quantity, shipment_to_transfer_to) ⇒ Object
-
#update!(order) ⇒ Object
Updates various aspects of the Shipment while bypassing any callbacks.
- #update_amounts ⇒ Object
- #update_attributes_and_order(params = {}) ⇒ Object
-
#weight_unit ⇒ String
Returns the weight unit of the shipment.
-
#with_free_shipping_promotion? ⇒ Boolean
Returns true if the shipment has a free shipping promotion applied.
Methods included from DisplayMoney
Methods included from CustomEvents
#publish_shipment_canceled_event, #publish_shipment_resumed_event, #publish_shipment_shipped_event
Methods included from Webhooks
#send_shipment_shipped_webhook
Methods included from Metadata
#metadata, #metadata=, #public_metadata=
Instance Attribute Details
#special_instructions ⇒ Object
Returns the value of attribute special_instructions.
44 45 46 |
# File 'app/models/spree/shipment.rb', line 44 def special_instructions @special_instructions end |
Instance Method Details
#add_shipping_method(shipping_method, selected = false) ⇒ Object
148 149 150 |
# File 'app/models/spree/shipment.rb', line 148 def add_shipping_method(shipping_method, selected = false) shipping_rates.create(shipping_method: shipping_method, selected: selected, cost: cost) end |
#after_cancel ⇒ Object
152 153 154 |
# File 'app/models/spree/shipment.rb', line 152 def after_cancel manifest.each { |item| manifest_restock(item) } end |
#after_resume ⇒ Object
156 157 158 |
# File 'app/models/spree/shipment.rb', line 156 def after_resume manifest.each { |item| manifest_unstock(item) } end |
#amount ⇒ Object
129 130 131 |
# File 'app/models/spree/shipment.rb', line 129 def amount cost end |
#backordered? ⇒ Boolean
Returns true if the shipment has any backordered inventory units
163 164 165 |
# File 'app/models/spree/shipment.rb', line 163 def backordered? inventory_units.any?(&:backordered?) end |
#cost=(value) ⇒ Object
Strict decimal bridge for string input — raises ArgumentError on malformed values instead of letting Rails' cast silently truncate ("12 boxes" would otherwise become 12.0). Blank strings cast to nil, matching the default Rails behavior.
139 140 141 142 |
# File 'app/models/spree/shipment.rb', line 139 def cost=(value) value = value.blank? ? nil : BigDecimal(value.strip) if value.is_a?(String) super end |
#determine_state(order) ⇒ Object
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
196 197 198 199 200 201 202 203 |
# File 'app/models/spree/shipment.rb', line 196 def determine_state(order) return 'canceled' if canceled? || order.canceled? return 'pending' unless order.can_ship? return 'pending' if inventory_units.any?(&:backordered?) return 'shipped' if shipped? order.paid? || Spree::Config[:auto_capture_on_dispatch] ? 'ready' : 'pending' end |
#digital? ⇒ Boolean
144 145 146 |
# File 'app/models/spree/shipment.rb', line 144 def digital? shipping_method&.digital? || false end |
#discounted_cost ⇒ Object Also known as: discounted_amount
205 206 207 |
# File 'app/models/spree/shipment.rb', line 205 def discounted_cost cost + promo_total end |
#final_price ⇒ Object Also known as: total
219 220 221 |
# File 'app/models/spree/shipment.rb', line 219 def final_price cost + adjustment_total end |
#final_price_with_items ⇒ Object
225 226 227 |
# File 'app/models/spree/shipment.rb', line 225 def final_price_with_items item_cost + final_price end |
#finalize! ⇒ Object
243 244 245 246 |
# File 'app/models/spree/shipment.rb', line 243 def finalize! inventory_units.finalize_units! after_resume end |
#free? ⇒ Boolean
229 230 231 232 233 |
# File 'app/models/spree/shipment.rb', line 229 def free? return true if final_price == BigDecimal(0) with_free_shipping_promotion? end |
#include?(variant) ⇒ Boolean
248 249 250 |
# File 'app/models/spree/shipment.rb', line 248 def include?(variant) inventory_units_for(variant).present? end |
#inventory_units_for(variant) ⇒ Object
252 253 254 |
# File 'app/models/spree/shipment.rb', line 252 def inventory_units_for(variant) inventory_units.where(variant_id: variant.id) end |
#inventory_units_for_item(line_item, variant = nil) ⇒ Object
256 257 258 |
# File 'app/models/spree/shipment.rb', line 256 def inventory_units_for_item(line_item, variant = nil) inventory_units.where(line_item_id: line_item.id, variant_id: line_item.variant_id || variant.id) end |
#item_cost ⇒ BigDecimal
Returns the cost of the shipment
268 269 270 |
# File 'app/models/spree/shipment.rb', line 268 def item_cost manifest.map { |m| (m.line_item.price + (m.line_item.adjustment_total / m.line_item.quantity)) * m.quantity }.sum end |
#item_quantity ⇒ Object
Returns the total quantity of all line items in the shipment
261 262 263 |
# File 'app/models/spree/shipment.rb', line 261 def item_quantity manifest.sum(&:quantity) end |
#item_weight ⇒ BigDecimal
Returns the weight of the shipment
275 276 277 |
# File 'app/models/spree/shipment.rb', line 275 def item_weight manifest.map { |m| m.line_item.item_weight }.sum end |
#line_items ⇒ Object
286 287 288 |
# File 'app/models/spree/shipment.rb', line 286 def line_items inventory_units.includes(:line_item).map(&:line_item).uniq end |
#manifest ⇒ Object
292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 |
# File 'app/models/spree/shipment.rb', line 292 def manifest # Grouping by the ID means that we don't have to call out to the association accessor # This makes the grouping by faster because it results in less SQL cache hits. inventory_units.group_by(&:variant_id).flat_map do |_variant_id, units| units.group_by(&:line_item_id).filter_map do |_line_item_id, units| line_item = units.first.line_item # Defensively skip orphaned inventory units (line item destroyed # without cascading) so a single bad row doesn't crash callers that # rely on line_item being present (item_cost, item_weight, the admin # shipment manifest view, etc.). next if line_item.nil? states = {} units.group_by(&:state).each { |state, iu| states[state] = iu.sum(&:quantity) } variant = units.first.variant ManifestItem.new(line_item, variant, units.sum(&:quantity), states) end end end |
#name ⇒ String
Returns the shipment number and shipping method name
125 126 127 |
# File 'app/models/spree/shipment.rb', line 125 def name [number, shipping_method&.name].compact.join(' ').strip end |
#partial? ⇒ Boolean
Returns true if not all of the shipment's line items are fully shipped
184 185 186 187 188 189 |
# File 'app/models/spree/shipment.rb', line 184 def partial? manifest.any? do |manifest_item| line_item = manifest_item.line_item line_item.quantity > manifest_item.quantity end end |
#process_order_payments ⇒ Object
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 |
# File 'app/models/spree/shipment.rb', line 313 def process_order_payments pending_payments = order.pending_payments. sort_by(&:uncaptured_amount).reverse shipment_to_pay = final_price_with_items payments_amount = 0 payments_pool = pending_payments.each_with_object([]) do |payment, pool| break if payments_amount >= shipment_to_pay payments_amount += payment.uncaptured_amount pool << payment end payments_pool.each do |payment| capturable_amount = if payment.amount >= shipment_to_pay shipment_to_pay else payment.amount end cents = (capturable_amount * 100).to_i payment.capture!(cents) shipment_to_pay -= capturable_amount end end |
#ready_or_pending? ⇒ Boolean
340 341 342 |
# File 'app/models/spree/shipment.rb', line 340 def ready_or_pending? ready? || pending? end |
#refresh_rates(shipping_method_filter = ShippingMethod::DISPLAY_ON_FRONT_END) ⇒ Object
344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 |
# File 'app/models/spree/shipment.rb', line 344 def refresh_rates(shipping_method_filter = ShippingMethod::DISPLAY_ON_FRONT_END) return shipping_rates if shipped? return [] unless can_get_rates? # StockEstimator.new assignment 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, shipping_method_filter) if shipping_method selected_rate = shipping_rates.detect do |rate| if original_shipping_method_id rate.shipping_method_id == original_shipping_method_id else rate.selected end end save! self.selected_shipping_rate_id = selected_rate.id if selected_rate reload end shipping_rates end |
#selected_delivery_rate_id ⇒ String?
Public API v3 name for the selected rate (see docs/plans/6.0-fulfillment-and-delivery.md).
373 374 375 |
# File 'app/models/spree/shipment.rb', line 373 def selected_delivery_rate_id selected_shipping_rate&.prefixed_id end |
#selected_delivery_rate_id=(id) ⇒ Object
Selects a delivery rate by its public prefixed ID (+dr_...+); raw IDs are accepted for internal callers. The model owns this naming bridge so API clients never deal with the legacy shipping-rate name.
382 383 384 385 |
# File 'app/models/spree/shipment.rb', line 382 def selected_delivery_rate_id=(id) rate = Spree::PrefixedId.prefixed_id?(id) ? shipping_rates.find_by_prefix_id!(id) : shipping_rates.find(id) self.selected_shipping_rate_id = rate.id end |
#selected_shipping_rate_id ⇒ Object
387 388 389 |
# File 'app/models/spree/shipment.rb', line 387 def selected_shipping_rate_id selected_shipping_rate.try(:id) end |
#selected_shipping_rate_id=(id) ⇒ Object
391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 |
# File 'app/models/spree/shipment.rb', line 391 def selected_shipping_rate_id=(id) # Explicitly updates the timestamp in order to bust cache dependent on "updated_at" shipping_rates.update_all(selected: false, updated_at: Time.current) shipping_rates.update(id, selected: true) save! # Reload associations to pick up the new selected shipping rate shipping_rates.reset association(:selected_shipping_rate).reset # Update shipment cost and order totals only for incomplete orders (during checkout) # For completed orders, totals are managed separately (e.g., in tests or admin adjustments) return if order.completed? update_amounts reload # reload to pick up cost set by update_columns in update_amounts order.set_shipments_cost end |
#set_up_inventory(state, variant, order, line_item, quantity = 1) ⇒ Object
408 409 410 411 412 413 414 415 416 417 418 |
# File 'app/models/spree/shipment.rb', line 408 def set_up_inventory(state, variant, order, line_item, quantity = 1) return if quantity <= 0 inventory_units.create( state: state, variant_id: variant.id, order_id: order.id, line_item_id: line_item.id, quantity: quantity ) end |
#shippable? ⇒ Boolean
Returns true if the shipment is shippable
177 178 179 |
# File 'app/models/spree/shipment.rb', line 177 def shippable? can_ship? && (tracked? || digital?) end |
#shipped=(value) ⇒ Object
420 421 422 423 424 |
# File 'app/models/spree/shipment.rb', line 420 def shipped=(value) return unless value == '1' && shipped_at.nil? self.shipped_at = Time.current end |
#shipping_method ⇒ Spree::ShippingMethod
Returns the shipping method of the selected shipping rate
429 430 431 |
# File 'app/models/spree/shipment.rb', line 429 def shipping_method selected_shipping_rate&.shipping_method || shipping_rates.first&.shipping_method end |
#tax_category ⇒ Spree::TaxCategory
Returns the tax category of the selected shipping rate
436 437 438 |
# File 'app/models/spree/shipment.rb', line 436 def tax_category selected_shipping_rate.try(:tax_rate).try(:tax_category) end |
#tax_category_id ⇒ Integer
Returns the tax category ID of the selected shipping rate
443 444 445 |
# File 'app/models/spree/shipment.rb', line 443 def tax_category_id selected_shipping_rate.try(:tax_rate).try(:tax_category_id) end |
#tax_total ⇒ Object
Only one of either included_tax_total or additional_tax_total is set This method returns the total of the two. Saves having to check if tax is included or additional.
450 451 452 |
# File 'app/models/spree/shipment.rb', line 450 def tax_total included_tax_total + additional_tax_total end |
#taxable_basis ⇒ BigDecimal
Returns the amount this shipment is taxed on: its discounted cost, never negative (stacked shipping promotions can push discounted_cost below zero). Whole-order promotions are not allocated to shipments.
215 216 217 |
# File 'app/models/spree/shipment.rb', line 215 def taxable_basis [discounted_cost, BigDecimal(0)].max end |
#to_package ⇒ Object
454 455 456 457 458 459 460 |
# File 'app/models/spree/shipment.rb', line 454 def to_package package = Stock::Package.new(stock_location) inventory_units.includes(:variant).joins(:variant).group_by(&:state).each do |state, state_inventory_units| package.add_multiple state_inventory_units, state.to_sym end package end |
#tracked? ⇒ Boolean
Returns true if the shipment is tracked
170 171 172 |
# File 'app/models/spree/shipment.rb', line 170 def tracked? tracking.present? || tracking_url.present? end |
#tracking_url ⇒ String?
External systems (3PLs, courier APIs) often hand over a complete tracking link rather than a bare tracking code — returned as-is instead of being templated into the delivery method's tracking URL.
467 468 469 470 471 472 473 |
# File 'app/models/spree/shipment.rb', line 467 def tracking_url @tracking_url ||= if tracking&.start_with?('https://') tracking else shipping_method&.build_tracking_url(tracking) end end |
#transfer_to_location(variant, quantity, stock_location) ⇒ Object
502 503 504 505 506 507 508 |
# File 'app/models/spree/shipment.rb', line 502 def transfer_to_location(variant, quantity, stock_location) transfer_to_shipment( variant, quantity, order.shipments.build(stock_location: stock_location) ) end |
#transfer_to_shipment(variant, quantity, shipment_to_transfer_to) ⇒ Object
510 511 512 513 514 515 516 517 518 519 |
# File 'app/models/spree/shipment.rb', line 510 def transfer_to_shipment(variant, quantity, shipment_to_transfer_to) Spree::FulfilmentChanger.new( current_stock_location: stock_location, desired_stock_location: shipment_to_transfer_to.stock_location, current_shipment: self, desired_shipment: shipment_to_transfer_to, variant: variant, quantity: quantity ) end |
#update!(order) ⇒ Object
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.
492 493 494 495 496 497 498 499 500 |
# File 'app/models/spree/shipment.rb', line 492 def update!(order) old_state = state new_state = determine_state(order) update_columns( state: new_state, updated_at: Time.current ) after_ship if new_state == 'shipped' && old_state != 'shipped' end |
#update_amounts ⇒ Object
475 476 477 478 479 480 481 482 483 |
# File 'app/models/spree/shipment.rb', line 475 def update_amounts if selected_shipping_rate update_columns( cost: selected_shipping_rate.cost, adjustment_total: adjustments.additional.map(&:update!).compact.sum, updated_at: Time.current ) end end |
#update_attributes_and_order(params = {}) ⇒ Object
485 486 487 |
# File 'app/models/spree/shipment.rb', line 485 def update_attributes_and_order(params = {}) Shipments::Update.call(shipment: self, shipment_attributes: params).success? end |
#weight_unit ⇒ String
Returns the weight unit of the shipment
282 283 284 |
# File 'app/models/spree/shipment.rb', line 282 def weight_unit manifest.first.line_item.weight_unit end |
#with_free_shipping_promotion? ⇒ Boolean
Returns true if the shipment has a free shipping promotion applied
238 239 240 241 |
# File 'app/models/spree/shipment.rb', line 238 def with_free_shipping_promotion? adjustments.promotion.joins("INNER JOIN #{Spree::PromotionAction.table_name} ON #{Spree::PromotionAction.table_name}.id = #{Spree::Adjustment.table_name}.source_id"). where("#{Spree::PromotionAction.table_name}.type = 'Spree::Promotion::Actions::FreeShipping'").exists? end |