mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-01-29 21:17:17 +00:00
256 lines
7.7 KiB
Ruby
256 lines
7.7 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require 'open_food_network/scope_variant_to_hub'
|
|
require 'variant_units/variant_and_line_item_naming'
|
|
|
|
module Spree
|
|
class LineItem < ApplicationRecord
|
|
include VariantUnits::VariantAndLineItemNaming
|
|
include LineItemStockChanges
|
|
|
|
belongs_to :order, class_name: "Spree::Order", inverse_of: :line_items
|
|
belongs_to :variant, -> { with_deleted }, class_name: "Spree::Variant"
|
|
belongs_to :tax_category, class_name: "Spree::TaxCategory"
|
|
|
|
has_one :product, through: :variant
|
|
has_many :adjustments, as: :adjustable, dependent: :destroy
|
|
|
|
has_and_belongs_to_many :option_values, join_table: 'spree_option_values_line_items',
|
|
class_name: 'Spree::OptionValue'
|
|
|
|
before_validation :adjust_quantity
|
|
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
|
|
before_save :calculate_final_weight_volume, if: :quantity_changed?,
|
|
unless: :final_weight_volume_changed?
|
|
after_save :update_order
|
|
after_save :update_units
|
|
before_destroy :update_inventory_before_destroy
|
|
after_destroy :update_order
|
|
|
|
delegate :product, :unit_description, :display_name, to: :variant
|
|
|
|
attr_accessor :skip_stock_check, :target_shipment # Allows manual skipping of Stock::AvailabilityValidator
|
|
|
|
# -- Scopes
|
|
scope :managed_by, lambda { |user|
|
|
if user.has_spree_role?('admin')
|
|
where(nil)
|
|
else
|
|
# Find line items that are from orders distributed by the user or supplied by the user
|
|
joins(variant: :product).
|
|
joins(:order).
|
|
where('spree_orders.distributor_id IN (?) OR spree_products.supplier_id IN (?)',
|
|
user.enterprises, user.enterprises).
|
|
select('spree_line_items.*')
|
|
end
|
|
}
|
|
|
|
scope :in_orders, lambda { |orders|
|
|
where(order_id: orders)
|
|
}
|
|
|
|
# Find line items that are from order sorted by variant name and unit value
|
|
scope :sorted_by_name_and_unit_value, -> {
|
|
joins(variant: :product).
|
|
reorder(Arel.sql("
|
|
lower(spree_products.name) asc,
|
|
lower(spree_variants.display_name) asc,
|
|
spree_variants.unit_value asc"))
|
|
}
|
|
|
|
scope :from_order_cycle, lambda { |order_cycle|
|
|
joins(order: :order_cycle).
|
|
where('order_cycles.id = ?', order_cycle)
|
|
}
|
|
|
|
# Here we are simply joining the line item to its variant and product
|
|
# We dont use joins here to avoid the default scopes,
|
|
# and with that, include deleted variants and deleted products
|
|
scope :supplied_by_any, lambda { |enterprises|
|
|
product_ids = Spree::Product.unscoped.where(supplier_id: enterprises).select(:id)
|
|
variant_ids = Spree::Variant.unscoped.where(product_id: product_ids).select(:id)
|
|
where("spree_line_items.variant_id IN (?)", variant_ids)
|
|
}
|
|
|
|
scope :with_tax, -> {
|
|
joins(:adjustments).
|
|
where('spree_adjustments.originator_type = ?', 'Spree::TaxRate').
|
|
select('DISTINCT spree_line_items.*')
|
|
}
|
|
|
|
# Line items without a Spree::TaxRate-originated adjustment
|
|
scope :without_tax, -> {
|
|
joins("
|
|
LEFT OUTER JOIN spree_adjustments
|
|
ON (spree_adjustments.adjustable_id=spree_line_items.id
|
|
AND spree_adjustments.adjustable_type = 'Spree::LineItem'
|
|
AND spree_adjustments.originator_type='Spree::TaxRate')").
|
|
where('spree_adjustments.id IS NULL')
|
|
}
|
|
|
|
def copy_price
|
|
return unless variant
|
|
|
|
self.price = variant.price if price.nil?
|
|
self.currency = variant.currency if currency.nil?
|
|
end
|
|
|
|
def copy_tax_category
|
|
return unless variant
|
|
|
|
self.tax_category = variant.product.tax_category
|
|
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
|
|
|
|
# Here we skip stock check if skip_stock_check flag is active,
|
|
# we skip stock check if requested quantity is zero or negative,
|
|
# and we scope variants to hub and thus acivate variant overrides.
|
|
def sufficient_stock?
|
|
return true if skip_stock_check
|
|
return true if quantity <= 0
|
|
|
|
scoper.scope(variant)
|
|
variant.can_supply?(quantity)
|
|
end
|
|
|
|
def insufficient_stock?
|
|
!sufficient_stock?
|
|
end
|
|
|
|
def assign_stock_changes_to=(shipment)
|
|
@preferred_shipment = shipment
|
|
end
|
|
|
|
def cap_quantity_at_stock!
|
|
scoper.scope(variant)
|
|
return if variant.on_demand
|
|
|
|
update!(quantity: variant.on_hand) if quantity > variant.on_hand
|
|
end
|
|
|
|
def has_tax?
|
|
adjustments.tax.any?
|
|
end
|
|
|
|
def included_tax
|
|
adjustments.tax.inclusive.sum(:amount)
|
|
end
|
|
|
|
def tax_rates
|
|
product.tax_category.andand.tax_rates || []
|
|
end
|
|
|
|
def price_with_adjustments
|
|
# EnterpriseFee#create_adjustment applies adjustments on line items to their parent order,
|
|
# so line_item.adjustments returns an empty array
|
|
return 0 if quantity.zero?
|
|
|
|
fees = adjustments.enterprise_fee.sum(:amount)
|
|
|
|
(price + fees / quantity).round(2)
|
|
end
|
|
|
|
def single_display_amount_with_adjustments
|
|
Spree::Money.new(price_with_adjustments, currency: currency)
|
|
end
|
|
|
|
def amount_with_adjustments
|
|
# We calculate from price_with_adjustments here rather than building our own value because
|
|
# rounding errors can produce discrepencies of $0.01.
|
|
price_with_adjustments * quantity
|
|
end
|
|
|
|
def display_amount_with_adjustments
|
|
Spree::Money.new(amount_with_adjustments, currency: currency)
|
|
end
|
|
|
|
def display_included_tax
|
|
Spree::Money.new(included_tax, currency: currency)
|
|
end
|
|
|
|
def unit_value
|
|
return variant.unit_value if quantity == 0 || !final_weight_volume
|
|
|
|
final_weight_volume / quantity
|
|
end
|
|
|
|
def unit_price_price_and_unit
|
|
unit_price = UnitPrice.new(variant)
|
|
Spree::Money.new(price_with_adjustments / unit_price.denominator).to_html +
|
|
" / ".html_safe + unit_price.unit
|
|
end
|
|
|
|
def scoper
|
|
@scoper ||= OpenFoodNetwork::ScopeVariantToHub.new(order.distributor)
|
|
end
|
|
|
|
private
|
|
|
|
def update_inventory
|
|
return unless changed?
|
|
|
|
scoper.scope(variant)
|
|
Spree::OrderInventory.new(order).verify(self, target_shipment)
|
|
end
|
|
|
|
def update_order
|
|
return unless saved_changes.present? || destroyed?
|
|
|
|
# update the order totals, etc.
|
|
order.create_tax_charge!
|
|
end
|
|
|
|
def update_inventory_before_destroy
|
|
# This is necessary before destroying the line item
|
|
# so that update_inventory will restore stock to the variant
|
|
self.quantity = 0
|
|
|
|
update_inventory
|
|
|
|
# This is necessary after updating inventory
|
|
# because update_inventory may delete the last shipment in the order
|
|
# and that makes update_order fail if we don't reload the shipments
|
|
order.shipments.reload
|
|
end
|
|
|
|
def calculate_final_weight_volume
|
|
if final_weight_volume.present? && quantity_was > 0
|
|
self.final_weight_volume = final_weight_volume * quantity / quantity_was
|
|
elsif variant.andand.unit_value.present?
|
|
self.final_weight_volume = variant.andand.unit_value * quantity
|
|
end
|
|
end
|
|
end
|
|
end
|