product.rb 7.2 KB
require_dependency 'product'

# FIXME: The lines bellow should be on the core
class Product

  extend CurrencyHelper::ClassMethods
  has_currency :price
  has_currency :discount

  scope :alphabetically, -> { order 'products.name ASC' }

  scope :available, -> { where available: true }
  scope :unavailable, -> { where 'products.available <> true' }
  scope :archived, -> { where archived: true }
  scope :unarchived, -> { where 'products.archived <> true' }

  scope :with_available, -> (available) { where available: available }
  scope :with_price, -> { where 'products.price > 0' }

  # FIXME: transliterate input and name column
  scope :name_like, -> (name) { where "name ILIKE ?", "%#{name}%" }
  scope :with_product_category_id, -> (id) { where product_category_id: id }

  scope :by_profile, -> (profile) { where profile_id: profile.id }
  scope :by_profile_id, -> (profile_id) { where profile_id: profile_id }

  def self.product_categories_of products
    ProductCategory.find products.collect(&:product_category_id).compact.select{ |id| not id.zero? }
  end

  attr_accessible :external_id
  settings_items :external_id, type: String, default: nil

  # should be on core, used by SuppliersPlugin::Import
  attr_accessible :price_details

end

class Product

  attr_accessible :from_products, :from_product, :supplier_id, :supplier

  has_many :sources_from_products, foreign_key: :to_product_id, class_name: 'SuppliersPlugin::SourceProduct', dependent: :destroy
  has_one  :sources_from_product,  foreign_key: :to_product_id, class_name: 'SuppliersPlugin::SourceProduct'
  has_many :sources_to_products, foreign_key: :from_product_id, class_name: 'SuppliersPlugin::SourceProduct', dependent: :destroy
  has_one  :sources_to_product,  foreign_key: :from_product_id, class_name: 'SuppliersPlugin::SourceProduct'
  has_many :to_products, -> { order 'id ASC' }, through: :sources_to_products
  has_one  :to_product, -> { order 'id ASC' },  through: :sources_to_product,  autosave: true
  has_many :from_products, -> { order 'id ASC' }, through: :sources_from_products
  has_one  :from_product, -> { order 'id ASC' },  through: :sources_from_product, autosave: true

  has_many :sources_from_2x_products, through: :from_products, source: :sources_from_products
  has_one  :sources_from_2x_product,  through: :from_product,  source: :sources_from_product
  has_many :sources_to_2x_products,   through: :to_products,   source: :sources_to_products
  has_one  :sources_to_2x_product,    through: :to_product,    source: :sources_to_product
  has_many :from_2x_products, through: :sources_from_2x_products, source: :from_product
  has_one  :from_2x_product,  through: :sources_from_2x_product,  source: :from_product
  has_many :to_2x_products,   through: :sources_to_2x_products,   source: :to_product
  has_one  :to_2x_product,    through: :sources_to_2x_product,    source: :to_product

  # semantic alias for supplier_from_product(s)
  has_many :sources_supplier_products, foreign_key: :to_product_id, class_name: 'SuppliersPlugin::SourceProduct'
  has_one  :sources_supplier_product,  foreign_key: :to_product_id, class_name: 'SuppliersPlugin::SourceProduct'
  has_many :supplier_products, -> { order 'id ASC' }, through: :sources_supplier_products, source: :from_product
  has_one  :supplier_product, -> { order 'id ASC' },  through: :sources_supplier_product,  source: :from_product, autosave: true
  has_many :suppliers, -> { distinct.order 'id ASC' }, through: :sources_supplier_products
  has_one  :supplier, -> { order 'id ASC' },  through: :sources_supplier_product

  has_many :consumers, -> { distinct.order 'id ASC' }, through: :to_products, source: :profile
  has_one  :consumer, -> { order 'id ASC' },  through: :to_product,  source: :profile

  # overhide original, FIXME: rename to available_and_supplier_active
  scope :available, -> {
    joins(:suppliers).
    where 'products.available = ? AND suppliers_plugin_suppliers.active = ?', true, true
  }
  scope :unavailable, -> {
    where 'products.available <> ? OR suppliers_plugin_suppliers.active <> ?', true, true
  }
  scope :with_available, -> (available) {
    op = if available then '=' else '<>' end
    cond = if available then 'AND' else 'OR' end
    where "products.available #{op} ? #{cond} suppliers_plugin_suppliers.active #{op} ?", true, true
  }

  scope :fp_name_like, -> (name) { where "from_products_products.name ILIKE ?", "%#{name}%" }
  scope :fp_with_product_category_id, -> (id) { where 'from_products_products.product_category_id = ?', id }

  # prefer distributed_products has_many to use DistributedProduct scopes and eager loading
  scope :distributed, -> { where type: 'SuppliersPlugin::DistributedProduct'}
  scope :own, -> { where type: nil }
  scope :supplied, -> {
    # this remove duplicates and allow sorting on the fields, unlike distinct
    group('products.id').
    where type: [nil, 'SuppliersPlugin::DistributedProduct']
  }
  scope :supplied_for_count, -> {
    distinct.
    where type: [nil, 'SuppliersPlugin::DistributedProduct']
  }

  scope :from_supplier, -> (supplier) { joins(:suppliers).where 'suppliers_plugin_suppliers.id = ?', supplier.id }
  scope :from_supplier_id, -> (supplier_id) { joins(:suppliers).where 'suppliers_plugin_suppliers.id = ?', supplier_id }

  after_create :distribute_to_consumers

  def own?
    self.class == Product
  end
  def distributed?
    self.class == SuppliersPlugin::DistributedProduct
  end
  def supplied?
    self.own? or self.distributed?
  end

  def supplier
    # FIXME: use self.suppliers when rails support for nested preload comes
    @supplier ||= self.sources_supplier_product.supplier rescue nil
    @supplier ||= self.profile.self_supplier rescue nil
  end
  def supplier= value
    @supplier = value
  end
  def supplier_id
    self.supplier.id
  end
  def supplier_id= id
    @supplier = profile.environment.profiles.find id
  end

  def supplier_dummy?
    self.supplier ? self.supplier.dummy? : self.profile.dummy?
  end

  def distribute_to_consumer consumer, attrs = {}
    distributed_product = consumer.distributed_products.where(profile_id: consumer.id, from_products_products: {id: self.id}).first
    distributed_product ||= SuppliersPlugin::DistributedProduct.create! profile: consumer, from_product: self
    distributed_product.update! attrs if attrs.present?
    distributed_product
  end

  def destroy_dependent
    self.to_products.each do |to_product|
      to_product.destroy if to_product.respond_to? :dependent? and to_product.dependent?
    end
  end

  # before_destroy and after_destroy don't work,
  # see http://stackoverflow.com/questions/14175330/associations-not-loaded-in-before-destroy-callback
  def destroy
    self.class.transaction do
      self.destroy_dependent
      super
    end
  end

  def diff from = self.from_product
    return @changed_attrs if @changed_attrs
    @changed_attrs = []
    SuppliersPlugin::BaseProduct::CORE_DEFAULT_ATTRIBUTES.each do |attr|
      @changed_attrs << attr if self[attr].present? and self[attr] != from[attr]
    end
    @changed_attrs
  end

  protected

  def distribute_to_consumers
    # shopping_cart creates products without a profile...
    return unless self.profile

    self.profile.consumers.except_people.except_self.each do |consumer|
      self.distribute_to_consumer consumer.profile
    end
  end

end