Noosfero/noosfero

View on GitHub
plugins/suppliers/models/suppliers_plugin/base_product.rb

Summary

Maintainability
B
4 hrs
Test Coverage
# 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? || 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 CurrencyFields::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 && self.supplier.active rescue false
  end

  def chained_available
    return self.available_without_supplier unless self.supplier_product

    self.available_without_supplier && self.supplier_product.available && self.supplier.active rescue false
  end
  alias_method :available_without_supplier, :available
  alias_method :available, :available_with_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 && (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 || self.class.default_product_category(self.environment)
  end

  def product_category_id_with_default
    self.product_category_id_without_default || self.product_category_with_default.id
  end
  alias_method :product_category_without_default, :product_category
  alias_method :product_category, :product_category_with_default

  alias_method :product_category_id_without_default, :product_category_id
  alias_method :product_category_id, :product_category_id_with_default

  # FIXME: move to core
  def unit_with_default
    self.unit_without_default || self.class.default_unit
  end
  alias_method :unit_without_default, :unit
  alias_method :unit, :unit_with_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