Bring models related to Order from spree_core

EPIC COMMIT ALERT :-)
This commit is contained in:
Luis Ramos
2020-08-08 14:44:30 +01:00
parent 77eaebc2a7
commit 47d2f698ef
22 changed files with 2893 additions and 2 deletions

View 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

View 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
View 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

View 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

View 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

View 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

View 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

View File

@@ -0,0 +1,6 @@
module Spree
class TokenizedPermission < ActiveRecord::Base
belongs_to :permissable, polymorphic: true
end
end