base_product.rb 6.65 KB
# for some unknown reason, if this is named SuppliersPlugin::Product then
# cycle.products will go to an infinite loop
class SuppliersPlugin::BaseProduct < Product

  attr_accessible :default_margin_percentage, :margin_percentage, :default_unit, :unit_detail,
    :supplier_product_attributes

  accepts_nested_attributes_for :supplier_product

  default_scope -> {
    includes(
      # from_products is required for products.available
      :from_products,
      # FIXME: move use cases to a scope called 'includes_for_links'
      {suppliers: [{ profile: [:domains, {environment: :domains}] }]},
      {profile: [:domains, {environment: :domains}]}
    )
  }

  # if abstract_class is true then it will trigger https://github.com/rails/rails/issues/20871
  #self.abstract_class = true

  settings_items :minimum_selleable, type: Float, default: nil
  settings_items :margin_percentage, type: Float, default: nil
  settings_items :quantity, type: Float, default: nil
  settings_items :unit_detail, type: String, default: nil

  CORE_DEFAULT_ATTRIBUTES = [
    :name, :description, :price, :unit_id, :product_category_id, :image_id,
  ]
  DEFAULT_ATTRIBUTES = CORE_DEFAULT_ATTRIBUTES + [
    :margin_percentage, :stored, :minimum_selleable, :unit_detail,
  ]

  extend DefaultDelegate::ClassMethods
  default_delegate_setting :name, to: :supplier_product
  default_delegate_setting :description, to: :supplier_product

  default_delegate_setting :qualifiers, to: :supplier_product
  default_delegate :product_qualifiers, default_setting: :default_qualifiers, to: :supplier_product

  default_delegate_setting :product_category, to: :supplier_product
  default_delegate :product_category_id, default_setting: :default_product_category, to: :supplier_product

  default_delegate_setting :image, to: :supplier_product, prefix: :_default
  default_delegate :image_id, default_setting: :_default_image, to: :supplier_product

  default_delegate_setting :unit, to: :supplier_product
  default_delegate :unit_id, default_setting: :default_unit, to: :supplier_product

  default_delegate_setting :margin_percentage, to: :profile,
    default_if: -> { self.own_margin_percentage.blank? or self.own_margin_percentage.zero? }
  default_delegate :price, default_setting: :default_margin_percentage, default_if: :equal?,
    to: -> { self.supplier_product.price_with_discount if self.supplier_product }

  default_delegate :unit_detail, default_setting: :default_unit, to: :supplier_product
  default_delegate_setting :minimum_selleable, to: :supplier_product

  extend CurrencyHelper::ClassMethods
  has_currency :own_price
  has_currency :original_price
  has_number_with_locale :minimum_selleable
  has_number_with_locale :own_minimum_selleable
  has_number_with_locale :original_minimum_selleable
  has_number_with_locale :quantity
  has_number_with_locale :margin_percentage
  has_number_with_locale :own_margin_percentage
  has_number_with_locale :original_margin_percentage

  def self.default_product_category environment
    ProductCategory.top_level_for(environment).order('name ASC').first
  end
  def self.default_unit
    Unit.new(singular: I18n.t('suppliers_plugin.models.product.unit'), plural: I18n.t('suppliers_plugin.models.product.units'))
  end

  # override SuppliersPlugin::BaseProduct
  def self.search_scope scope, params
    scope = scope.from_supplier_id params[:supplier_id] if params[:supplier_id].present?
    scope = scope.with_available(if params[:available] == 'true' then true else false end) if params[:available].present?
    scope = scope.fp_name_like params[:name] if params[:name].present?
    scope = scope.fp_with_product_category_id params[:category_id] if params[:category_id].present?
    scope
  end

  def self.orphans_ids
    # FIXME: need references from rails4 to do it without raw query
    result = self.connection.execute <<-SQL
SELECT products.id FROM products
LEFT OUTER JOIN suppliers_plugin_source_products ON suppliers_plugin_source_products.to_product_id = products.id
LEFT OUTER JOIN products from_products_products ON from_products_products.id = suppliers_plugin_source_products.from_product_id
WHERE products.type IN (#{(self.descendants << self).map{ |d| "'#{d}'" }.join(',')})
GROUP BY products.id HAVING count(from_products_products.id) = 0;
SQL
    result.values
  end

  def self.archive_orphans
    self.where(id: self.orphans_ids).find_each batch_size: 50 do |product|
      # need full save to trigger search index
      product.update archived: true
    end
  end

  def buy_price
    self.supplier_products.inject(0){ |sum, p| sum += p.price || 0 }
  end
  def buy_unit
    #TODO: handle multiple products
    (self.supplier_product.unit rescue nil) || self.class.default_unit
  end

  def available
    self[:available]
  end

  def available_with_supplier
    return self.available_without_supplier unless self.supplier
    self.available_without_supplier and self.supplier.active rescue false
  end
  def chained_available
    return self.available_without_supplier unless self.supplier_product
    self.available_without_supplier and self.supplier_product.available and self.supplier.active rescue false
  end
  alias_method_chain :available, :supplier

  def dependent?
    self.from_products.length >= 1
  end
  def orphan?
    !self.dependent?
  end

  def minimum_selleable
    self[:minimum_selleable] || 0.1
  end

  def price_with_margins base_price = nil, margin_source = nil
    margin_source ||= self
    margin_percentage = margin_source.margin_percentage
    margin_percentage ||= self.profile.margin_percentage if self.profile

    base_price ||= 0
    price = if margin_percentage and not base_price.zero?
      base_price.to_f + (margin_percentage.to_f / 100) * base_price.to_f
    else
      self.price_with_default
    end

    price
  end

  def price_without_margins
    self[:price] / (1 + self.margin_percentage/100)
  end

  # FIXME: move to core
  # just in case the from_products is nil
  def product_category_with_default
    self.product_category_without_default or self.class.default_product_category(self.environment)
  end
  def product_category_id_with_default
    self.product_category_id_without_default or self.product_category_with_default.id
  end
  alias_method_chain :product_category, :default
  alias_method_chain :product_category_id, :default

  # FIXME: move to core
  def unit_with_default
    self.unit_without_default or self.class.default_unit
  end
  alias_method_chain :unit, :default

  # FIXME: move to core
  def archive
    self.update! archived: true
  end
  def unarchive
    self.update! archived: false
  end

  protected

  def validate_uniqueness_of_column_name?
    false
  end

  # overhide Product's after_create callback to avoid infinite loop
  def distribute_to_consumers
  end

end