Files
openfoodnetwork/app/models/spree/variant.rb
Maikel Linke 8ef6966891 Declare old belongs_to default on remaining models
It would take ages to go through all files now and assess all belongs_to
associations. So I just declare the old default and then we can move on
and apply the new default for the application while these classes still
use the old one. All new models will then use the new default which is
the goal of this excercise and we can refactor old classes when we touch
them anyway.
2023-08-11 10:14:43 +10:00

256 lines
8.4 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
self.belongs_to_required_by_default = false
acts_as_paranoid
searchable_attributes :sku, :display_as, :display_name
searchable_associations :product, :default_price
searchable_scopes :active, :deleted
NAME_FIELDS = ["display_name", "display_as", "weight", "unit_value", "unit_description"].freeze
SEARCH_KEY = "#{%w(name
meta_keywords
variants_display_as
variants_display_name
supplier_name).join('_or_')}_cont".freeze
belongs_to :product, -> { with_deleted }, touch: true, class_name: 'Spree::Product'
belongs_to :tax_category, class_name: 'Spree::TaxCategory'
delegate_belongs_to :product, :name, :description, :shipping_category_id,
:meta_keywords, :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_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_currency
validates :price, numericality: { greater_than_or_equal_to: 0 }, presence: true
validates :tax_category, presence: true,
if: proc { Spree::Config[:products_require_tax_category] }
validates :unit_value, presence: true, if: ->(variant) {
%w(weight volume).include?(variant.product&.variant_unit)
}
validates :unit_value, numericality: { greater_than: 0 }
validates :unit_description, presence: true, if: ->(variant) {
variant.product&.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? }
before_save :convert_variant_weight_to_decimal
before_save :assign_units, if: ->(variant) {
variant.new_record? || variant.changed_attributes.keys.intersection(NAME_FIELDS).any?
}
after_create :create_stock_items
around_destroy :destruction
after_save :save_default_price
# 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 :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 tax_category
if self[:tax_category_id].nil?
TaxCategory.find_by(is_default: true)
else
TaxCategory.find(self[:tax_category_id])
end
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 fees_name_by_type_for(distributor, order_cycle)
OpenFoodNetwork::EnterpriseFeeCalculator.new(distributor,
order_cycle).fees_name_by_type_for self
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
def check_currency
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 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
def convert_variant_weight_to_decimal
self.weight = weight.to_d
end
end
end