Files
openfoodnetwork/app/models/spree/variant.rb
Matt-Yorkley 7948c5943d Remove #deleted? method from Product and Variant
This method is already supplied via the paranoia gem, there's no need to re-implement it here.
2021-05-01 12:54:33 +01:00

244 lines
8.0 KiB
Ruby

# frozen_string_literal: true
require 'open_food_network/enterprise_fee_calculator'
require 'variant_units/variant_and_line_item_naming'
require 'concerns/variant_stock'
require 'spree/localized_number'
module Spree
class Variant < ApplicationRecord
extend Spree::LocalizedNumber
include VariantUnits::VariantAndLineItemNaming
include VariantStock
acts_as_paranoid
belongs_to :product, -> { with_deleted }, touch: true, class_name: 'Spree::Product'
delegate_belongs_to :product, :name, :description, :permalink, :available_on,
:tax_category_id, :shipping_category_id, :meta_description,
:meta_keywords, :tax_category, :shipping_category
has_many :inventory_units, inverse_of: :variant
has_many :line_items, inverse_of: :variant
has_many :stock_items, dependent: :destroy, inverse_of: :variant
has_many :stock_locations, through: :stock_items
has_many :stock_movements
has_and_belongs_to_many :option_values, join_table: :spree_option_values_variants
has_many :images, -> { order(:position) }, as: :viewable,
dependent: :destroy,
class_name: "Spree::Image"
accepts_nested_attributes_for :images
has_one :default_price,
-> { with_deleted.where(currency: Spree::Config[:currency]) },
class_name: 'Spree::Price',
dependent: :destroy
has_many :prices,
class_name: 'Spree::Price',
dependent: :destroy
delegate_belongs_to :default_price, :display_price, :display_amount,
:price, :price=, :currency
has_many :exchange_variants
has_many :exchanges, through: :exchange_variants
has_many :variant_overrides
has_many :inventory_items
localize_number :price, :weight
validate :check_price
validates :price, numericality: { greater_than_or_equal_to: 0 },
presence: true,
if: proc { Spree::Config[:require_master_price] }
validates :unit_value, presence: true, if: ->(variant) {
%w(weight volume).include?(variant.product.andand.variant_unit)
}
validates :unit_value, numericality: { greater_than: 0 }
validates :unit_description, presence: true, if: ->(variant) {
variant.product.andand.variant_unit.present? && variant.unit_value.nil?
}
before_validation :set_cost_currency
before_validation :ensure_unit_value
before_validation :update_weight_from_unit_value, if: ->(v) { v.product.present? }
after_save :save_default_price
after_save :update_units
after_create :create_stock_items
after_create :set_position
around_destroy :destruction
# default variant scope only lists non-deleted variants
scope :deleted, lambda { where('deleted_at IS NOT NULL') }
scope :with_order_cycles_inner, -> { joins(exchanges: :order_cycle) }
scope :not_master, -> { where(is_master: false) }
scope :in_order_cycle, lambda { |order_cycle|
with_order_cycles_inner.
merge(Exchange.outgoing).
where('order_cycles.id = ?', order_cycle).
select('DISTINCT spree_variants.*')
}
scope :in_schedule, lambda { |schedule|
joins(exchanges: { order_cycle: :schedules }).
merge(Exchange.outgoing).
where(schedules: { id: schedule }).
select('DISTINCT spree_variants.*')
}
scope :for_distribution, lambda { |order_cycle, distributor|
where('spree_variants.id IN (?)', order_cycle.variants_distributed_by(distributor).
select(&:id))
}
scope :visible_for, lambda { |enterprise|
joins(:inventory_items).
where(
'inventory_items.enterprise_id = (?) AND inventory_items.visible = (?)',
enterprise,
true
)
}
scope :not_hidden_for, lambda { |enterprise|
enterprise_id = enterprise&.id.to_i
return none if enterprise_id < 1
joins("
LEFT OUTER JOIN (SELECT *
FROM inventory_items
WHERE enterprise_id = #{enterprise_id})
AS o_inventory_items
ON o_inventory_items.variant_id = spree_variants.id")
.where("o_inventory_items.id IS NULL OR o_inventory_items.visible = (?)", true)
}
scope :stockable_by, lambda { |enterprise|
return where("1=0") if enterprise.blank?
joins(:product).
where(spree_products: { id: Spree::Product.stockable_by(enterprise).pluck(:id) })
}
# Define sope as class method to allow chaining with other scopes filtering id.
# In Rails 3, merging two scopes on the same column will consider only the last scope.
def self.in_distributor(distributor)
where(id: ExchangeVariant.select(:variant_id).
joins(:exchange).
where('exchanges.incoming = ? AND exchanges.receiver_id = ?', false, distributor))
end
def self.indexed
where(nil).index_by(&:id)
end
def self.active(currency = nil)
# "where(id:" is necessary so that the returned relation has no includes
# The relation without includes will not be readonly and allow updates on it
where("spree_variants.id in (?)", joins(:prices).
where(deleted_at: nil).
where('spree_prices.currency' =>
currency || Spree::Config[:currency]).
where('spree_prices.amount IS NOT NULL').
select("spree_variants.id"))
end
def price_with_fees(distributor, order_cycle)
price + fees_for(distributor, order_cycle)
end
def fees_for(distributor, order_cycle)
OpenFoodNetwork::EnterpriseFeeCalculator.new(distributor, order_cycle).fees_for self
end
def fees_by_type_for(distributor, order_cycle)
OpenFoodNetwork::EnterpriseFeeCalculator.new(distributor, order_cycle).fees_by_type_for self
end
def option_value(opt_name)
option_values.detect { |o| o.option_type.name == opt_name }.try(:presentation)
end
def price_in(currency)
prices.select{ |price| price.currency == currency }.first ||
Spree::Price.new(variant_id: id, currency: currency)
end
def amount_in(currency)
price_in(currency).try(:amount)
end
# can_supply? is implemented in VariantStock
def in_stock?(quantity = 1)
can_supply?(quantity)
end
def total_on_hand
Spree::Stock::Quantifier.new(self).total_on_hand
end
private
# Ensures a new variant takes the product master price when price is not supplied
def check_price
if price.nil? && Spree::Config[:require_master_price]
raise 'No master variant found to infer price' unless product&.master
raise 'Must supply price for variant or master.price for product.' if self == product.master
self.price = product.master.price
end
return unless currency.nil?
self.currency = Spree::Config[:currency]
end
def save_default_price
default_price.save if default_price && (default_price.changed? || default_price.new_record?)
end
def set_cost_currency
self.cost_currency = Spree::Config[:currency] if cost_currency.blank?
end
def create_stock_items
StockLocation.all.find_each do |stock_location|
stock_location.propagate_variant(self)
end
end
def set_position
update_column(:position, product.variants.maximum(:position).to_i + 1)
end
def update_weight_from_unit_value
return unless product.variant_unit == 'weight' && unit_value.present?
self.weight = weight_from_unit_value
end
def destruction
exchange_variants.reload.destroy_all
yield
end
def ensure_unit_value
Bugsnag.notify("Trying to set unit_value to NaN") if unit_value&.nan?
return unless (product&.variant_unit == "items" && unit_value.nil?) || unit_value&.nan?
self.unit_value = 1.0
end
end
end