mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-03-16 04:24:23 +00:00
Bring models related to Order from spree_core
EPIC COMMIT ALERT :-)
This commit is contained in:
70
app/models/spree/inventory_unit.rb
Normal file
70
app/models/spree/inventory_unit.rb
Normal file
@@ -0,0 +1,70 @@
|
||||
module Spree
|
||||
class InventoryUnit < ActiveRecord::Base
|
||||
belongs_to :variant, class_name: "Spree::Variant"
|
||||
belongs_to :order, class_name: "Spree::Order"
|
||||
belongs_to :shipment, class_name: "Spree::Shipment"
|
||||
belongs_to :return_authorization, class_name: "Spree::ReturnAuthorization"
|
||||
|
||||
scope :backordered, -> { where state: 'backordered' }
|
||||
scope :shipped, -> { where state: 'shipped' }
|
||||
scope :backordered_per_variant, ->(stock_item) do
|
||||
includes(:shipment)
|
||||
.where("spree_shipments.state != 'canceled'").references(:shipment)
|
||||
.where(variant_id: stock_item.variant_id)
|
||||
.backordered.order("#{self.table_name}.created_at ASC")
|
||||
end
|
||||
|
||||
# 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: :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
|
||||
end
|
||||
|
||||
# This was refactored from a simpler query because the previous implementation
|
||||
# lead to issues once users tried to modify the objects returned. That's due
|
||||
# to ActiveRecord `joins(shipment: :stock_location)` only return readonly
|
||||
# objects
|
||||
#
|
||||
# Returns an array of backordered inventory units as per a given stock item
|
||||
def self.backordered_for_stock_item(stock_item)
|
||||
backordered_per_variant(stock_item).select do |unit|
|
||||
unit.shipment.stock_location == stock_item.stock_location
|
||||
end
|
||||
end
|
||||
|
||||
def self.finalize_units!(inventory_units)
|
||||
inventory_units.map { |iu| iu.update_column(:pending, false) }
|
||||
end
|
||||
|
||||
def find_stock_item
|
||||
Spree::StockItem.where(stock_location_id: shipment.stock_location_id,
|
||||
variant_id: variant_id).first
|
||||
end
|
||||
|
||||
# Remove variant default_scope `deleted_at: nil`
|
||||
def variant
|
||||
Spree::Variant.unscoped { super }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def allow_ship?
|
||||
Spree::Config[:allow_backorder_shipping] || self.on_hand?
|
||||
end
|
||||
|
||||
def update_order
|
||||
order.update!
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
102
app/models/spree/line_item.rb
Normal file
102
app/models/spree/line_item.rb
Normal file
@@ -0,0 +1,102 @@
|
||||
module Spree
|
||||
class LineItem < ActiveRecord::Base
|
||||
before_validation :adjust_quantity
|
||||
belongs_to :order, class_name: "Spree::Order"
|
||||
belongs_to :variant, class_name: "Spree::Variant"
|
||||
belongs_to :tax_category, class_name: "Spree::TaxCategory"
|
||||
|
||||
has_one :product, through: :variant
|
||||
has_many :adjustments, as: :adjustable, dependent: :destroy
|
||||
|
||||
before_validation :copy_price
|
||||
before_validation :copy_tax_category
|
||||
|
||||
validates :variant, presence: true
|
||||
validates :quantity, numericality: {
|
||||
only_integer: true,
|
||||
greater_than: -1,
|
||||
message: Spree.t('validation.must_be_int')
|
||||
}
|
||||
validates :price, numericality: true
|
||||
validates_with Stock::AvailabilityValidator
|
||||
|
||||
before_save :update_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?
|
||||
self.cost_price = variant.cost_price if cost_price.nil?
|
||||
self.currency = variant.currency if currency.nil?
|
||||
end
|
||||
end
|
||||
|
||||
def copy_tax_category
|
||||
if variant
|
||||
self.tax_category = variant.product.tax_category
|
||||
end
|
||||
end
|
||||
|
||||
def amount
|
||||
price * quantity
|
||||
end
|
||||
alias total amount
|
||||
|
||||
def single_money
|
||||
Spree::Money.new(price, { currency: currency })
|
||||
end
|
||||
alias single_display_amount single_money
|
||||
|
||||
def money
|
||||
Spree::Money.new(amount, { currency: currency })
|
||||
end
|
||||
alias display_total money
|
||||
alias display_amount money
|
||||
|
||||
def adjust_quantity
|
||||
self.quantity = 0 if quantity.nil? || quantity < 0
|
||||
end
|
||||
|
||||
def sufficient_stock?
|
||||
Stock::Quantifier.new(variant_id).can_supply? quantity
|
||||
end
|
||||
|
||||
def insufficient_stock?
|
||||
!sufficient_stock?
|
||||
end
|
||||
|
||||
def assign_stock_changes_to=(shipment)
|
||||
@preferred_shipment = shipment
|
||||
end
|
||||
|
||||
# Remove product default_scope `deleted_at: nil`
|
||||
def product
|
||||
variant.product
|
||||
end
|
||||
|
||||
# Remove variant default_scope `deleted_at: nil`
|
||||
def variant
|
||||
Spree::Variant.unscoped { super }
|
||||
end
|
||||
|
||||
private
|
||||
def update_inventory
|
||||
if changed?
|
||||
Spree::OrderInventory.new(self.order).verify(self, target_shipment)
|
||||
end
|
||||
end
|
||||
|
||||
def update_order
|
||||
if changed? || destroyed?
|
||||
# update the order totals, etc.
|
||||
order.create_tax_charge!
|
||||
order.update!
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
575
app/models/spree/order.rb
Normal file
575
app/models/spree/order.rb
Normal file
@@ -0,0 +1,575 @@
|
||||
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 :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('completed_at IS NOT NULL')
|
||||
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?
|
||||
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?
|
||||
payments.map(&:payment_method).compact.any?(&:payment_profiles_supported?)
|
||||
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 ||= Spree::Config.order_updater_decorator.new(
|
||||
Spree::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 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_changes.empty? || state_changes.last.previous_state.nil?
|
||||
true
|
||||
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.map(&:amount).sum
|
||||
end
|
||||
|
||||
def tax_total
|
||||
adjustments.tax.map(&:amount).sum
|
||||
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
|
||||
updater.before_save_hook
|
||||
save
|
||||
updater.run_hooks
|
||||
|
||||
deliver_order_confirmation_email
|
||||
|
||||
self.state_changes.create(
|
||||
previous_state: 'cart',
|
||||
next_state: 'complete',
|
||||
name: 'order' ,
|
||||
user_id: self.user_id
|
||||
)
|
||||
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)
|
||||
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)
|
||||
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
|
||||
# So that the destroy doesn't take out line items which may have been re-assigned
|
||||
order.line_items.reload
|
||||
order.destroy
|
||||
end
|
||||
|
||||
def empty!
|
||||
line_items.destroy_all
|
||||
adjustments.destroy_all
|
||||
end
|
||||
|
||||
def clear_adjustments!
|
||||
self.adjustments.destroy_all
|
||||
self.line_item_adjustments.destroy_all
|
||||
end
|
||||
|
||||
def has_step?(step)
|
||||
checkout_steps.include?(step)
|
||||
end
|
||||
|
||||
def state_changed(name)
|
||||
state = "#{name}_state"
|
||||
if persisted?
|
||||
old_state = self.send("#{state}_was")
|
||||
self.state_changes.create(
|
||||
previous_state: old_state,
|
||||
next_state: self.send(state),
|
||||
name: name,
|
||||
user_id: self.user_id
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def coupon_code=(code)
|
||||
@coupon_code = code.strip.downcase rescue nil
|
||||
end
|
||||
|
||||
# Tells us if there if the specified promotion is already associated with the order
|
||||
# regardless of whether or not its currently eligible. Useful because generally
|
||||
# you would only want a promotion action to apply to order no more than once.
|
||||
#
|
||||
# Receives an adjustment +originator+ (here a PromotionAction object) and tells
|
||||
# if the order has adjustments from that already
|
||||
def promotion_credit_exists?(originator)
|
||||
!! adjustments.includes(:originator).promotion.reload.detect { |credit| credit.originator.id == originator.id }
|
||||
end
|
||||
|
||||
def promo_total
|
||||
adjustments.eligible.promotion.map(&:amount).sum
|
||||
end
|
||||
|
||||
def shipped?
|
||||
%w(partial shipped).include?(shipment_state)
|
||||
end
|
||||
|
||||
def create_proposed_shipments
|
||||
adjustments.shipping.delete_all
|
||||
shipments.destroy_all
|
||||
|
||||
packages = Spree::Stock::Coordinator.new(self).packages
|
||||
packages.each do |package|
|
||||
shipments << package.to_shipment
|
||||
end
|
||||
|
||||
shipments
|
||||
end
|
||||
|
||||
# Clean shipments and make order back to address state
|
||||
#
|
||||
# At some point the might need to force the order to transition from address
|
||||
# to delivery again so that proper updated shipments are created.
|
||||
# e.g. customer goes back from payment step and changes order items
|
||||
def ensure_updated_shipments
|
||||
if shipments.any?
|
||||
self.shipments.destroy_all
|
||||
self.update_column(:state, "address")
|
||||
end
|
||||
end
|
||||
|
||||
def refresh_shipment_rates
|
||||
shipments.map &:refresh_rates
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def link_by_email
|
||||
self.email = user.email if self.user
|
||||
end
|
||||
|
||||
# Determine if email is required (we don't want validation errors before we hit the checkout)
|
||||
def require_email
|
||||
return true unless new_record? or state == 'cart'
|
||||
end
|
||||
|
||||
def ensure_line_items_present
|
||||
unless line_items.present?
|
||||
errors.add(:base, Spree.t(:there_are_no_items_for_this_order)) and return false
|
||||
end
|
||||
end
|
||||
|
||||
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?
|
||||
end
|
||||
|
||||
def ensure_available_shipping_rates
|
||||
if shipments.empty? || shipments.any? { |shipment| shipment.shipping_rates.blank? }
|
||||
errors.add(:base, Spree.t(:items_cannot_be_shipped)) and return false
|
||||
end
|
||||
end
|
||||
|
||||
def has_available_payment
|
||||
return unless delivery?
|
||||
# errors.add(:base, :no_payment_methods_available) if available_payment_methods.empty?
|
||||
end
|
||||
|
||||
def after_cancel
|
||||
shipments.each { |shipment| shipment.cancel! }
|
||||
|
||||
OrderMailer.cancel_email(self.id).deliver
|
||||
self.payment_state = 'credit_owed' unless shipped?
|
||||
end
|
||||
|
||||
def after_resume
|
||||
shipments.each { |shipment| shipment.resume! }
|
||||
end
|
||||
|
||||
def use_billing?
|
||||
@use_billing == true || @use_billing == 'true' || @use_billing == '1'
|
||||
end
|
||||
|
||||
def set_currency
|
||||
self.currency = Spree::Config[:currency] if self[:currency].nil?
|
||||
end
|
||||
end
|
||||
end
|
||||
67
app/models/spree/order_contents.rb
Normal file
67
app/models/spree/order_contents.rb
Normal file
@@ -0,0 +1,67 @@
|
||||
module Spree
|
||||
class OrderContents
|
||||
attr_accessor :order, :currency
|
||||
|
||||
def initialize(order)
|
||||
@order = order
|
||||
end
|
||||
|
||||
# Get current line item for variant if exists
|
||||
# Add variant qty to line_item
|
||||
def add(variant, quantity = 1, currency = nil, shipment = nil)
|
||||
line_item = order.find_line_item_by_variant(variant)
|
||||
add_to_line_item(line_item, variant, quantity, currency, shipment)
|
||||
end
|
||||
|
||||
# Get current line item for variant
|
||||
# Remove variant qty from line_item
|
||||
def remove(variant, quantity = 1, shipment = nil)
|
||||
line_item = order.find_line_item_by_variant(variant)
|
||||
|
||||
unless line_item
|
||||
raise ActiveRecord::RecordNotFound, "Line item not found for variant #{variant.sku}"
|
||||
end
|
||||
|
||||
remove_from_line_item(line_item, variant, quantity, shipment)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def add_to_line_item(line_item, variant, quantity, currency=nil, shipment=nil)
|
||||
if line_item
|
||||
line_item.target_shipment = shipment
|
||||
line_item.quantity += quantity.to_i
|
||||
line_item.currency = currency unless currency.nil?
|
||||
else
|
||||
line_item = order.line_items.new(quantity: quantity, variant: variant)
|
||||
line_item.target_shipment = shipment
|
||||
if currency
|
||||
line_item.currency = currency unless currency.nil?
|
||||
line_item.price = variant.price_in(currency).amount
|
||||
else
|
||||
line_item.price = variant.price
|
||||
end
|
||||
end
|
||||
|
||||
line_item.save
|
||||
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
|
||||
106
app/models/spree/order_inventory.rb
Normal file
106
app/models/spree/order_inventory.rb
Normal file
@@ -0,0 +1,106 @@
|
||||
module Spree
|
||||
class OrderInventory
|
||||
attr_accessor :order
|
||||
|
||||
def initialize(order)
|
||||
@order = order
|
||||
end
|
||||
|
||||
# Only verify inventory for completed orders (as orders in frontend checkout
|
||||
# have inventory assigned via +order.create_proposed_shipment+) or when
|
||||
# shipment is explicitly passed
|
||||
#
|
||||
# In case shipment is passed the stock location should only unstock or
|
||||
# restock items if the order is completed. That is so because stock items
|
||||
# are always unstocked when the order is completed through +shipment.finalize+
|
||||
def verify(line_item, shipment = nil)
|
||||
if order.completed? || shipment.present?
|
||||
|
||||
variant_units = inventory_units_for(line_item.variant)
|
||||
|
||||
if variant_units.size < line_item.quantity
|
||||
quantity = line_item.quantity - variant_units.size
|
||||
|
||||
shipment = determine_target_shipment(line_item.variant) unless shipment
|
||||
add_to_shipment(shipment, line_item.variant, quantity)
|
||||
elsif variant_units.size > line_item.quantity
|
||||
remove(line_item, variant_units, shipment)
|
||||
end
|
||||
else
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
def inventory_units_for(variant)
|
||||
units = order.shipments.collect{|s| s.inventory_units.to_a}.flatten
|
||||
units.group_by(&:variant_id)[variant.id] || []
|
||||
end
|
||||
|
||||
private
|
||||
def remove(line_item, variant_units, shipment = nil)
|
||||
quantity = variant_units.size - line_item.quantity
|
||||
|
||||
if shipment.present?
|
||||
remove_from_shipment(shipment, line_item.variant, quantity)
|
||||
else
|
||||
order.shipments.each do |shipment|
|
||||
break if quantity == 0
|
||||
quantity -= remove_from_shipment(shipment, line_item.variant, quantity)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Returns either one of the shipment:
|
||||
#
|
||||
# first unshipped that already includes this variant
|
||||
# first unshipped that's leaving from a stock_location that stocks this variant
|
||||
def determine_target_shipment(variant)
|
||||
shipment = order.shipments.detect do |shipment|
|
||||
(shipment.ready? || shipment.pending?) && shipment.include?(variant)
|
||||
end
|
||||
|
||||
shipment ||= order.shipments.detect do |shipment|
|
||||
(shipment.ready? || shipment.pending?) && variant.stock_location_ids.include?(shipment.stock_location_id)
|
||||
end
|
||||
end
|
||||
|
||||
def add_to_shipment(shipment, variant, quantity)
|
||||
on_hand, back_order = shipment.stock_location.fill_status(variant, quantity)
|
||||
|
||||
on_hand.times { shipment.set_up_inventory('on_hand', variant, order) }
|
||||
back_order.times { shipment.set_up_inventory('backordered', variant, order) }
|
||||
|
||||
# adding to this shipment, and removing from stock_location
|
||||
if order.completed?
|
||||
shipment.stock_location.unstock(variant, quantity, shipment)
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
shipment.destroy if shipment.inventory_units.count == 0
|
||||
|
||||
# removing this from shipment, and adding to stock_location
|
||||
if order.completed?
|
||||
shipment.stock_location.restock variant, removed_quantity, shipment
|
||||
end
|
||||
|
||||
removed_quantity
|
||||
end
|
||||
end
|
||||
end
|
||||
103
app/models/spree/return_authorization.rb
Normal file
103
app/models/spree/return_authorization.rb
Normal file
@@ -0,0 +1,103 @@
|
||||
module Spree
|
||||
class ReturnAuthorization < ActiveRecord::Base
|
||||
belongs_to :order, class_name: 'Spree::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
|
||||
|
||||
state_machine initial: :authorized do
|
||||
after_transition to: :received, do: :process_return
|
||||
|
||||
event :receive do
|
||||
transition to: :received, from: :authorized, if: :allow_receive?
|
||||
end
|
||||
event :cancel do
|
||||
transition to: :canceled, from: :authorized
|
||||
end
|
||||
end
|
||||
|
||||
def currency
|
||||
order.nil? ? Spree::Config[:currency] : order.currency
|
||||
end
|
||||
|
||||
def display_amount
|
||||
Spree::Money.new(amount, { currency: currency })
|
||||
end
|
||||
|
||||
def add_variant(variant_id, quantity)
|
||||
order_units = returnable_inventory.group_by(&:variant_id)
|
||||
returned_units = inventory_units.group_by(&:variant_id)
|
||||
return false if order_units.empty?
|
||||
|
||||
count = 0
|
||||
|
||||
if returned_units[variant_id].nil? || returned_units[variant_id].size < quantity
|
||||
count = returned_units[variant_id].nil? ? 0 : returned_units[variant_id].size
|
||||
|
||||
order_units[variant_id].each do |inventory_unit|
|
||||
next unless inventory_unit.return_authorization.nil? && count < quantity
|
||||
|
||||
inventory_unit.return_authorization = self
|
||||
inventory_unit.save!
|
||||
|
||||
count += 1
|
||||
end
|
||||
elsif returned_units[variant_id].size > quantity
|
||||
(returned_units[variant_id].size - quantity).times do |i|
|
||||
returned_units[variant_id][i].return_authorization_id = nil
|
||||
returned_units[variant_id][i].save!
|
||||
end
|
||||
end
|
||||
|
||||
order.authorize_return! if inventory_units.reload.size > 0 && !order.awaiting_return?
|
||||
end
|
||||
|
||||
def returnable_inventory
|
||||
order.shipped_shipments.collect{|s| s.inventory_units.to_a}.flatten
|
||||
end
|
||||
|
||||
private
|
||||
def must_have_shipped_units
|
||||
errors.add(:order, Spree.t(:has_no_shipped_units)) if order.nil? || !order.shipped_shipments.any?
|
||||
end
|
||||
|
||||
def generate_number
|
||||
return if number
|
||||
|
||||
record = true
|
||||
while record
|
||||
random = "RMA#{Array.new(9){rand(9)}.join}"
|
||||
record = self.class.where(number: random).first
|
||||
end
|
||||
self.number = random
|
||||
end
|
||||
|
||||
def process_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: Spree.t(:rma_credit))
|
||||
credit.source = self
|
||||
credit.adjustable = order
|
||||
credit.save
|
||||
|
||||
order.return if inventory_units.all?(&:returned?)
|
||||
end
|
||||
|
||||
def allow_receive?
|
||||
!inventory_units.empty?
|
||||
end
|
||||
|
||||
def force_positive_amount
|
||||
self.amount = amount.abs
|
||||
end
|
||||
end
|
||||
end
|
||||
15
app/models/spree/state_change.rb
Normal file
15
app/models/spree/state_change.rb
Normal file
@@ -0,0 +1,15 @@
|
||||
module Spree
|
||||
class StateChange < ActiveRecord::Base
|
||||
belongs_to :user
|
||||
belongs_to :stateful, polymorphic: true
|
||||
before_create :assign_user
|
||||
|
||||
def <=>(other)
|
||||
created_at <=> other.created_at
|
||||
end
|
||||
|
||||
def assign_user
|
||||
true # don't stop the filters
|
||||
end
|
||||
end
|
||||
end
|
||||
6
app/models/spree/tokenized_permission.rb
Normal file
6
app/models/spree/tokenized_permission.rb
Normal file
@@ -0,0 +1,6 @@
|
||||
module Spree
|
||||
class TokenizedPermission < ActiveRecord::Base
|
||||
belongs_to :permissable, polymorphic: true
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user