Merge decorators with original spree files

This commit is contained in:
Luis Ramos
2020-08-07 20:14:03 +01:00
parent 8cb75fc6d8
commit 751beceb34
12 changed files with 404 additions and 471 deletions

View File

@@ -1,5 +1,6 @@
module Spree
class OptionType < ActiveRecord::Base
has_many :products, through: :product_option_types
has_many :option_values, -> { order(:position) }, dependent: :destroy
has_many :product_option_types, dependent: :destroy
has_and_belongs_to_many :prototypes, join_table: 'spree_option_types_prototypes'

View File

@@ -1,5 +0,0 @@
module Spree
OptionType.class_eval do
has_many :products, through: :product_option_types
end
end

View File

@@ -1,5 +1,7 @@
module Spree
class Price < ActiveRecord::Base
acts_as_paranoid without_default_scope: true
belongs_to :variant, class_name: 'Spree::Variant'
validate :check_price
@@ -22,10 +24,14 @@ module Spree
self[:amount] = parse_price(price)
end
private
def check_price
raise "Price must belong to a variant" if variant.nil?
# Allow prices to access associated soft-deleted variants.
def variant
Spree::Variant.unscoped { super }
end
private
def check_price
if currency.nil?
self.currency = Spree::Config[:currency]
end

View File

@@ -1,18 +0,0 @@
module Spree
Price.class_eval do
acts_as_paranoid without_default_scope: true
# Allow prices to access associated soft-deleted variants.
def variant
Spree::Variant.unscoped { super }
end
private
def check_price
if currency.nil?
self.currency = Spree::Config[:currency]
end
end
end
end

View File

@@ -1,3 +1,7 @@
require 'open_food_network/permalink_generator'
require 'open_food_network/property_merge'
require 'concerns/product_stock'
# PRODUCTS
# Products represent an entity for sale in a store.
# Products can have variations, called variants
@@ -17,12 +21,18 @@
# All other variants have option values and may have inventory units.
# Sum of on_hand each variant's inventory level determine "on_hand" level for the product.
#
module Spree
class Product < ActiveRecord::Base
include PermalinkGenerator
include ProductStock
acts_as_paranoid
has_many :product_option_types, dependent: :destroy
has_many :option_types, through: :product_option_types
# We have an after_destroy callback on Spree::ProductOptionType. However, if we
# don't specify dependent => destroy on this association, it is not called. See:
# https://github.com/rails/rails/issues/7618
has_many :option_types, through: :product_option_types, dependent: :destroy
has_many :product_properties, dependent: :destroy
has_many :properties, through: :product_properties
@@ -32,6 +42,8 @@ module Spree
belongs_to :tax_category, class_name: 'Spree::TaxCategory'
belongs_to :shipping_category, class_name: 'Spree::ShippingCategory'
belongs_to :supplier, class_name: 'Enterprise', touch: true
belongs_to :primary_taxon, class_name: 'Spree::Taxon', touch: true
has_one :master,
-> { where is_master: true },
@@ -51,8 +63,9 @@ module Spree
has_many :stock_items, through: :variants
delegate_belongs_to :master, :sku, :price, :currency, :display_amount, :display_price, :weight, :height, :width, :depth, :is_master, :has_default_price?, :cost_currency, :price_in, :amount_in
delegate_belongs_to :master, :sku, :price, :currency, :display_amount, :display_price, :weight, :height, :width, :depth, :is_master, :has_default_price?, :cost_currency, :price_in, :amount_in, :unit_value, :unit_description
delegate_belongs_to :master, :cost_price if Variant.table_exists? && Variant.column_names.include?('cost_price')
delegate :images_attributes=, :display_as=, to: :master
after_create :set_master_variant_defaults
after_create :add_properties_and_option_types_from_prototype
@@ -71,6 +84,16 @@ module Spree
validates :price, presence: true, if: proc { Spree::Config[:require_master_price] }
validates :shipping_category_id, presence: true
validates :supplier, presence: true
validates :primary_taxon, presence: true
validates :tax_category_id, presence: true, if: "Spree::Config.products_require_tax_category"
validates :variant_unit, presence: true
validates :variant_unit_scale,
presence: { if: ->(p) { %w(weight volume).include? p.variant_unit } }
validates :variant_unit_name,
presence: { if: ->(p) { p.variant_unit == 'items' } }
attr_accessor :option_values_hash
accepts_nested_attributes_for :product_properties, allow_destroy: true, reject_if: lambda { |pp| pp[:property_name].blank? }
@@ -80,9 +103,111 @@ module Spree
alias :options :product_option_types
after_initialize :ensure_master
after_initialize :set_available_on_to_now, if: :new_record?
before_validation :sanitize_permalink
before_save :add_primary_taxon_to_taxons
after_save :remove_previous_primary_taxon_from_taxons
after_save :ensure_standard_variant
after_save :update_units
before_destroy :punch_permalink
# -- Joins
scope :with_order_cycles_outer, -> {
joins("
LEFT OUTER JOIN spree_variants AS o_spree_variants
ON (o_spree_variants.product_id = spree_products.id)").
joins("
LEFT OUTER JOIN exchange_variants AS o_exchange_variants
ON (o_exchange_variants.variant_id = o_spree_variants.id)").
joins("
LEFT OUTER JOIN exchanges AS o_exchanges
ON (o_exchanges.id = o_exchange_variants.exchange_id)").
joins("
LEFT OUTER JOIN order_cycles AS o_order_cycles
ON (o_order_cycles.id = o_exchanges.order_cycle_id)")
}
scope :imported_on, lambda { |import_date|
import_date = Time.zone.parse import_date if import_date.is_a? String
import_date = import_date.to_date
joins(:variants).merge(Spree::Variant.where(import_date: import_date.beginning_of_day..import_date.end_of_day))
}
scope :with_order_cycles_inner, -> {
joins(variants_including_master: { exchanges: :order_cycle })
}
scope :visible_for, lambda { |enterprise|
joins('LEFT OUTER JOIN spree_variants AS o_spree_variants ON (o_spree_variants.product_id = spree_products.id)').
joins('LEFT OUTER JOIN inventory_items AS o_inventory_items ON (o_spree_variants.id = o_inventory_items.variant_id)').
where('o_inventory_items.enterprise_id = (?) AND visible = (?)', enterprise, true)
}
# -- Scopes
scope :in_supplier, lambda { |supplier| where(supplier_id: supplier) }
# Products distributed via the given distributor through an OC
scope :in_distributor, lambda { |distributor|
distributor = distributor.respond_to?(:id) ? distributor.id : distributor.to_i
with_order_cycles_outer.
where('(o_exchanges.incoming = ? AND o_exchanges.receiver_id = ?)', false, distributor).
select('distinct spree_products.*')
}
scope :in_distributors, lambda { |distributors|
with_order_cycles_outer.
where('(o_exchanges.incoming = ? AND o_exchanges.receiver_id IN (?))', false, distributors).
uniq
}
# Products supplied by a given enterprise or distributed via that enterprise through an OC
scope :in_supplier_or_distributor, lambda { |enterprise|
enterprise = enterprise.respond_to?(:id) ? enterprise.id : enterprise.to_i
with_order_cycles_outer.
where("
spree_products.supplier_id = ?
OR (o_exchanges.incoming = ? AND o_exchanges.receiver_id = ?)
", enterprise, false, enterprise).
select('distinct spree_products.*')
}
# Products distributed by the given order cycle
scope :in_order_cycle, lambda { |order_cycle|
with_order_cycles_inner.
merge(Exchange.outgoing).
where('order_cycles.id = ?', order_cycle)
}
scope :in_an_active_order_cycle, lambda {
with_order_cycles_inner.
merge(OrderCycle.active).
merge(Exchange.outgoing).
where('order_cycles.id IS NOT NULL')
}
scope :by_producer, -> { joins(:supplier).order('enterprises.name') }
scope :by_name, -> { order('name') }
scope :managed_by, lambda { |user|
if user.has_spree_role?('admin')
where(nil)
else
where('supplier_id IN (?)', user.enterprises.select("enterprises.id"))
end
}
scope :stockable_by, lambda { |enterprise|
return where('1=0') if enterprise.blank?
permitted_producer_ids = EnterpriseRelationship.joins(:parent).permitting(enterprise.id)
.with_permission(:add_to_order_cycle).where(enterprises: { is_primary_producer: true }).pluck(:parent_id)
return where('spree_products.supplier_id IN (?)', [enterprise.id] | permitted_producer_ids)
}
def to_param
permalink.present? ? permalink : (permalink_was || name.to_s.to_url)
end
@@ -122,6 +247,12 @@ module Spree
duplicator.duplicate
end
# Called by Spree::Product::duplicate before saving.
def duplicate_extra(_parent)
# Spree sets the SKU to "COPY OF #{parent sku}".
master.sku = ''
end
# use deleted? rather than checking the attribute directly. this
# allows extensions to override deleted? if they want to provide
# their own definition.
@@ -199,6 +330,60 @@ module Spree
super || variants_including_master.with_deleted.where(is_master: true).first
end
def properties_including_inherited
# Product properties override producer properties
ps = product_properties.all
if inherits_properties
ps = OpenFoodNetwork::PropertyMerge.merge(ps, supplier.producer_properties)
end
ps.
sort_by(&:position).
map { |pp| { id: pp.property.id, name: pp.property.presentation, value: pp.value } }
end
def in_distributor?(distributor)
self.class.in_distributor(distributor).include? self
end
def in_order_cycle?(order_cycle)
self.class.in_order_cycle(order_cycle).include? self
end
def variants_distributed_by(order_cycle, distributor)
order_cycle.variants_distributed_by(distributor).where(product_id: self)
end
# Get the most recent import_date of a product's variants
def import_date
variants.map(&:import_date).compact.max
end
def variant_unit_option_type
if variant_unit.present?
option_type_name = "unit_#{variant_unit}"
option_type_presentation = variant_unit.capitalize
Spree::OptionType.find_by(name: option_type_name) ||
Spree::OptionType.create!(name: option_type_name,
presentation: option_type_presentation)
end
end
def destroy_with_delete_from_order_cycles
transaction do
touch_distributors
ExchangeVariant.
where('exchange_variants.variant_id IN (?)', variants_including_master.with_deleted.
select(:id)).destroy_all
destroy_without_delete_from_order_cycles
end
end
alias_method_chain :destroy, :delete_from_order_cycles
private
# Builds variants from a hash of option types & values
@@ -230,10 +415,26 @@ module Spree
master.is_master = true
end
# there's a weird quirk with the delegate stuff that does not automatically save the delegate object
# when saving so we force a save using a hook.
# This fixes any problems arising from failing master saves, without the need for a validates_associated on
# master, while giving us more specific errors as to why saving failed
def save_master
master.save if master && (master.changed? || master.new_record? || (master.default_price && (master.default_price.changed || master.default_price.new_record)))
if master && (
master.changed? || master.new_record? || (
master.default_price && (
master.default_price.changed? || master.default_price.new_record?
)
)
)
master.save!
end
# If the master cannot be saved, the Product object will get its errors
# and will be destroyed
rescue ActiveRecord::RecordInvalid
master.errors.each do |att, error|
errors.add(att, error)
end
raise
end
def ensure_master
@@ -244,6 +445,53 @@ module Spree
def punch_permalink
update_attribute :permalink, "#{Time.now.to_i}_#{permalink}" # punch permalink with date prefix
end
def set_available_on_to_now
self.available_on ||= Time.zone.now
end
def update_units
if variant_unit_changed?
option_types.delete self.class.all_variant_unit_option_types
option_types << variant_unit_option_type if variant_unit.present?
variants_including_master.each(&:update_units)
end
end
def touch_distributors
Enterprise.distributing_products(id).each(&:touch)
end
def add_primary_taxon_to_taxons
taxons << primary_taxon unless taxons.include? primary_taxon
end
def remove_previous_primary_taxon_from_taxons
return unless primary_taxon_id_changed? && primary_taxon_id_was
taxons.destroy(primary_taxon_id_was)
end
def self.all_variant_unit_option_types
Spree::OptionType.where('name LIKE ?', 'unit_%%')
end
def ensure_standard_variant
if master.valid? && variants.empty?
variant = master.dup
variant.product = self
variant.is_master = false
variants << variant
end
end
# Spree creates a permalink already but our implementation fixes an edge case.
def sanitize_permalink
if permalink.blank? || permalink_changed?
requested = permalink.presence || permalink_was.presence || name.presence || 'product'
self.permalink = create_unique_permalink(requested.parameterize)
end
end
end
end

View File

@@ -1,265 +0,0 @@
require 'open_food_network/permalink_generator'
require 'open_food_network/property_merge'
require 'concerns/product_stock'
Spree::Product.class_eval do
include PermalinkGenerator
include ProductStock
# We have an after_destroy callback on Spree::ProductOptionType. However, if we
# don't specify dependent => destroy on this association, it is not called. See:
# https://github.com/rails/rails/issues/7618
has_many :option_types, through: :product_option_types, dependent: :destroy
belongs_to :supplier, class_name: 'Enterprise', touch: true
belongs_to :primary_taxon, class_name: 'Spree::Taxon', touch: true
delegate_belongs_to :master, :unit_value, :unit_description
delegate :images_attributes=, :display_as=, to: :master
validates :supplier, presence: true
validates :primary_taxon, presence: true
validates :tax_category_id, presence: true, if: "Spree::Config.products_require_tax_category"
validates :variant_unit, presence: true
validates :variant_unit_scale,
presence: { if: ->(p) { %w(weight volume).include? p.variant_unit } }
validates :variant_unit_name,
presence: { if: ->(p) { p.variant_unit == 'items' } }
after_initialize :set_available_on_to_now, if: :new_record?
before_validation :sanitize_permalink
before_save :add_primary_taxon_to_taxons
after_save :remove_previous_primary_taxon_from_taxons
after_save :ensure_standard_variant
after_save :update_units
# -- Joins
scope :with_order_cycles_outer, -> {
joins("
LEFT OUTER JOIN spree_variants AS o_spree_variants
ON (o_spree_variants.product_id = spree_products.id)").
joins("
LEFT OUTER JOIN exchange_variants AS o_exchange_variants
ON (o_exchange_variants.variant_id = o_spree_variants.id)").
joins("
LEFT OUTER JOIN exchanges AS o_exchanges
ON (o_exchanges.id = o_exchange_variants.exchange_id)").
joins("
LEFT OUTER JOIN order_cycles AS o_order_cycles
ON (o_order_cycles.id = o_exchanges.order_cycle_id)")
}
scope :imported_on, lambda { |import_date|
import_date = Time.zone.parse import_date if import_date.is_a? String
import_date = import_date.to_date
joins(:variants).merge(Spree::Variant.where(import_date: import_date.beginning_of_day..import_date.end_of_day))
}
scope :with_order_cycles_inner, -> {
joins(variants_including_master: { exchanges: :order_cycle })
}
scope :visible_for, lambda { |enterprise|
joins('LEFT OUTER JOIN spree_variants AS o_spree_variants ON (o_spree_variants.product_id = spree_products.id)').
joins('LEFT OUTER JOIN inventory_items AS o_inventory_items ON (o_spree_variants.id = o_inventory_items.variant_id)').
where('o_inventory_items.enterprise_id = (?) AND visible = (?)', enterprise, true)
}
# -- Scopes
scope :in_supplier, lambda { |supplier| where(supplier_id: supplier) }
# Products distributed via the given distributor through an OC
scope :in_distributor, lambda { |distributor|
distributor = distributor.respond_to?(:id) ? distributor.id : distributor.to_i
with_order_cycles_outer.
where('(o_exchanges.incoming = ? AND o_exchanges.receiver_id = ?)', false, distributor).
select('distinct spree_products.*')
}
scope :in_distributors, lambda { |distributors|
with_order_cycles_outer.
where('(o_exchanges.incoming = ? AND o_exchanges.receiver_id IN (?))', false, distributors).
uniq
}
# Products supplied by a given enterprise or distributed via that enterprise through an OC
scope :in_supplier_or_distributor, lambda { |enterprise|
enterprise = enterprise.respond_to?(:id) ? enterprise.id : enterprise.to_i
with_order_cycles_outer.
where("
spree_products.supplier_id = ?
OR (o_exchanges.incoming = ? AND o_exchanges.receiver_id = ?)
", enterprise, false, enterprise).
select('distinct spree_products.*')
}
# Products distributed by the given order cycle
scope :in_order_cycle, lambda { |order_cycle|
with_order_cycles_inner.
merge(Exchange.outgoing).
where('order_cycles.id = ?', order_cycle)
}
scope :in_an_active_order_cycle, lambda {
with_order_cycles_inner.
merge(OrderCycle.active).
merge(Exchange.outgoing).
where('order_cycles.id IS NOT NULL')
}
scope :by_producer, -> { joins(:supplier).order('enterprises.name') }
scope :by_name, -> { order('name') }
scope :managed_by, lambda { |user|
if user.has_spree_role?('admin')
where(nil)
else
where('supplier_id IN (?)', user.enterprises.select("enterprises.id"))
end
}
scope :stockable_by, lambda { |enterprise|
return where('1=0') if enterprise.blank?
permitted_producer_ids = EnterpriseRelationship.joins(:parent).permitting(enterprise.id)
.with_permission(:add_to_order_cycle).where(enterprises: { is_primary_producer: true }).pluck(:parent_id)
return where('spree_products.supplier_id IN (?)', [enterprise.id] | permitted_producer_ids)
}
# -- Methods
# Called by Spree::Product::duplicate before saving.
def duplicate_extra(_parent)
# Spree sets the SKU to "COPY OF #{parent sku}".
master.sku = ''
end
def properties_including_inherited
# Product properties override producer properties
ps = product_properties.all
if inherits_properties
ps = OpenFoodNetwork::PropertyMerge.merge(ps, supplier.producer_properties)
end
ps.
sort_by(&:position).
map { |pp| { id: pp.property.id, name: pp.property.presentation, value: pp.value } }
end
def in_distributor?(distributor)
self.class.in_distributor(distributor).include? self
end
def in_order_cycle?(order_cycle)
self.class.in_order_cycle(order_cycle).include? self
end
def variants_distributed_by(order_cycle, distributor)
order_cycle.variants_distributed_by(distributor).where(product_id: self)
end
# Get the most recent import_date of a product's variants
def import_date
variants.map(&:import_date).compact.max
end
def variant_unit_option_type
if variant_unit.present?
option_type_name = "unit_#{variant_unit}"
option_type_presentation = variant_unit.capitalize
Spree::OptionType.find_by(name: option_type_name) ||
Spree::OptionType.create!(name: option_type_name,
presentation: option_type_presentation)
end
end
def destroy_with_delete_from_order_cycles
transaction do
touch_distributors
ExchangeVariant.
where('exchange_variants.variant_id IN (?)', variants_including_master.with_deleted.
select(:id)).destroy_all
destroy_without_delete_from_order_cycles
end
end
alias_method_chain :destroy, :delete_from_order_cycles
private
def set_available_on_to_now
self.available_on ||= Time.zone.now
end
def update_units
if variant_unit_changed?
option_types.delete self.class.all_variant_unit_option_types
option_types << variant_unit_option_type if variant_unit.present?
variants_including_master.each(&:update_units)
end
end
def touch_distributors
Enterprise.distributing_products(id).each(&:touch)
end
def add_primary_taxon_to_taxons
taxons << primary_taxon unless taxons.include? primary_taxon
end
def remove_previous_primary_taxon_from_taxons
return unless primary_taxon_id_changed? && primary_taxon_id_was
taxons.destroy(primary_taxon_id_was)
end
def self.all_variant_unit_option_types
Spree::OptionType.where('name LIKE ?', 'unit_%%')
end
def ensure_standard_variant
if master.valid? && variants.empty?
variant = master.dup
variant.product = self
variant.is_master = false
variants << variant
end
end
# Override Spree's old save_master method and replace it with the most recent method from spree repository
# This fixes any problems arising from failing master saves, without the need for a validates_associated on
# master, while giving us more specific errors as to why saving failed
def save_master
if master && (
master.changed? || master.new_record? || (
master.default_price && (
master.default_price.changed? || master.default_price.new_record?
)
)
)
master.save!
end
# If the master cannot be saved, the Product object will get its errors
# and will be destroyed
rescue ActiveRecord::RecordInvalid
master.errors.each do |att, error|
errors.add(att, error)
end
raise
end
# Spree creates a permalink already but our implementation fixes an edge case.
def sanitize_permalink
if permalink.blank? || permalink_changed?
requested = permalink.presence || permalink_was.presence || name.presence || 'product'
self.permalink = create_unique_permalink(requested.parameterize)
end
end
end

View File

@@ -1,7 +1,16 @@
module Spree
class ProductOptionType < ActiveRecord::Base
after_destroy :remove_option_values
belongs_to :product, class_name: 'Spree::Product'
belongs_to :option_type, class_name: 'Spree::OptionType'
acts_as_list scope: :product
def remove_option_values
product.variants_including_master.each do |variant|
option_values = variant.option_values.where(option_type_id: option_type)
variant.option_values.destroy(*option_values)
end
end
end
end

View File

@@ -1,10 +0,0 @@
Spree::ProductOptionType.class_eval do
after_destroy :remove_option_values
def remove_option_values
product.variants_including_master.each do |variant|
option_values = variant.option_values.where(option_type_id: option_type)
variant.option_values.destroy(*option_values)
end
end
end

View File

@@ -1,6 +1,6 @@
module Spree
class ProductProperty < ActiveRecord::Base
belongs_to :product, class_name: 'Spree::Product'
belongs_to :product, class_name: "Spree::Product", touch: true
belongs_to :property, class_name: 'Spree::Property'
validates :property, presence: true

View File

@@ -1,5 +0,0 @@
module Spree
ProductProperty.class_eval do
belongs_to :product, class_name: "Spree::Product", touch: true
end
end

View File

@@ -1,9 +1,16 @@
require 'open_food_network/enterprise_fee_calculator'
require 'variant_units/variant_and_line_item_naming'
require 'concerns/variant_stock'
module Spree
class Variant < ActiveRecord::Base
extend Spree::LocalizedNumber
include VariantUnits::VariantAndLineItemNaming
include VariantStock
acts_as_paranoid
belongs_to :product, 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
@@ -16,54 +23,150 @@ module Spree
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,
-> { where currency: Spree::Config[:currency] },
class_name: 'Spree::Price',
dependent: :destroy
delegate_belongs_to :default_price, :display_price, :display_amount, :price, :price=, :currency if Spree::Price.table_exists?
has_many :prices,
class_name: 'Spree::Price',
dependent: :destroy
delegate_belongs_to :default_price, :display_price, :display_amount, :price, :price=, :currency if Spree::Price.table_exists?
has_many :exchange_variants
has_many :exchanges, through: :exchange_variants
has_many :variant_overrides
has_many :inventory_items
localize_number :price, :cost_price, :weight
validate :check_price
validates :price, numericality: { greater_than_or_equal_to: 0 }, presence: true, if: proc { Spree::Config[:require_master_price] }
validates :cost_price, numericality: { greater_than_or_equal_to: 0, allow_nil: true } if self.table_exists? && self.column_names.include?('cost_price')
validates :unit_value, presence: true, if: ->(variant) {
%w(weight volume).include?(variant.product.andand.variant_unit)
}
validates :unit_description, presence: true, if: ->(variant) {
variant.product.andand.variant_unit.present? && variant.unit_value.nil?
}
before_validation :set_cost_currency
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|
return where("1=0") if enterprise.blank?
joins("
LEFT OUTER JOIN (SELECT *
FROM inventory_items
WHERE enterprise_id = #{sanitize enterprise.andand.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
scoped.index_by(&:id)
end
def self.active(currency = nil)
joins(:prices).where(deleted_at: nil).where('spree_prices.currency' => currency || Spree::Config[:currency]).where('spree_prices.amount IS NOT NULL')
# "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 cost_price=(price)
self[:cost_price] = parse_price(price) if price.present?
end
# Allow variant to access associated soft-deleted prices.
def default_price
Spree::Price.unscoped { super }
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
# returns number of units currently on backorder for this variant.
def on_backorder
inventory_units.with_state('backordered').size
end
def options_text
values = self.option_values.joins(:option_type).order("#{Spree::OptionType.table_name}.position asc")
values.map! do |ov|
"#{ov.option_type.presentation}: #{ov.presentation}"
end
values.to_sentence({ words_connector: ", ", two_words_connector: ", " })
end
def gross_profit
cost_price.nil? ? 0 : (price - cost_price)
end
@@ -133,8 +236,9 @@ module Spree
Spree::Product.unscoped { super }
end
def in_stock?(quantity=1)
Spree::Stock::Quantifier.new(self).can_supply?(quantity)
# can_supply? is implemented in VariantStock
def in_stock?(quantity = 1)
can_supply?(quantity)
end
def total_on_hand
@@ -183,6 +287,15 @@ module Spree
def set_position
self.update_column(:position, product.variants.maximum(:position).to_i + 1)
end
def update_weight_from_unit_value
self.weight = weight_from_unit_value if product.variant_unit == 'weight' && unit_value.present?
end
def destruction
exchange_variants(:reload).destroy_all
yield
end
end
end

View File

@@ -1,141 +0,0 @@
require 'open_food_network/enterprise_fee_calculator'
require 'variant_units/variant_and_line_item_naming'
require 'concerns/variant_stock'
Spree::Variant.class_eval do
extend Spree::LocalizedNumber
# Remove method From Spree, so method from the naming module is used instead
# This file may be double-loaded in delayed job environment, so we check before
# removing the Spree method to prevent error.
remove_method :options_text if instance_methods(false).include? :options_text
include VariantUnits::VariantAndLineItemNaming
include VariantStock
has_many :exchange_variants
has_many :exchanges, through: :exchange_variants
has_many :variant_overrides
has_many :inventory_items
accepts_nested_attributes_for :images
validates :unit_value, presence: true, if: ->(variant) {
%w(weight volume).include?(variant.product.andand.variant_unit)
}
validates :unit_description, presence: true, if: ->(variant) {
variant.product.andand.variant_unit.present? && variant.unit_value.nil?
}
before_validation :update_weight_from_unit_value, if: ->(v) { v.product.present? }
after_save :update_units
around_destroy :destruction
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|
return where("1=0") if enterprise.blank?
joins("
LEFT OUTER JOIN (SELECT *
FROM inventory_items
WHERE enterprise_id = #{sanitize enterprise.andand.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)
}
localize_number :price, :cost_price, :weight
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
scoped.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
# We override in_stock? to avoid depending
# on the non-overridable method Spree::Stock::Quantifier.can_supply?
# VariantStock implements can_supply? itself which depends on overridable methods
def in_stock?(quantity = 1)
can_supply?(quantity)
end
# Allow variant to access associated soft-deleted prices.
def default_price
Spree::Price.unscoped { super }
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
private
def update_weight_from_unit_value
self.weight = weight_from_unit_value if product.variant_unit == 'weight' && unit_value.present?
end
def destruction
exchange_variants(:reload).destroy_all
yield
end
end