Noosfero/noosfero

View on GitHub
plugins/orders/models/orders_plugin/item.rb

Summary

Maintainability
D
2 days
Test Coverage
class OrdersPlugin::Item < ApplicationRecord
  attr_accessible :order, :sale, :purchase,
                  :product, :product_id,
                  :price, :name

  # flag used by items to compare them with products
  attr_accessor :product_diff

  Statuses = %w[ordered accepted separated delivered received]
  DbStatuses = %w[draft planned cancelled] + Statuses
  UserStatuses = %w[open forgotten planned cancelled] + Statuses
  StatusText = {}; UserStatuses.map do |status|
    StatusText[status] = "orders_plugin.models.order.statuses.#{status}"
  end

  # should be Order, but can't reference it here so it would create a cyclic reference
  StatusAccessMap = {
    "ordered" => :consumer,
    "accepted" => :supplier,
    "separated" => :supplier,
    "delivered" => :supplier,
    "received" => :consumer,
  }
  StatusDataMap = {}; StatusAccessMap.each do |status, access|
    StatusDataMap[status] = "#{access}_#{status}"
  end
  StatusDataMap.each do |status, data|
    quantity = "quantity_#{data}".to_sym
    price = "price_#{data}".to_sym

    attr_accessible quantity
    attr_accessible price
  end

  serialize :data

  belongs_to :order, class_name: "::OrdersPlugin::Order", foreign_key: :order_id, touch: true, optional: true
  belongs_to :sale, class_name: "::OrdersPlugin::Sale", foreign_key: :order_id, touch: true, optional: true
  belongs_to :purchase, class_name: "::OrdersPlugin::Purchase", foreign_key: :order_id, touch: true, optional: true

  belongs_to :product, class_name: "::ProductsPlugin::Product", optional: true
  has_one :supplier, through: :product

  has_one :profile, through: :order
  has_one :consumer, through: :order

  # FIXME: don't work because of load order
  # if defined? SuppliersPlugin
  has_many :from_products, through: :product
  has_one :from_product, through: :product
  has_many :to_products, through: :product
  has_one :to_product, through: :product
  has_many :sources_supplier_products, through: :product
  has_one :sources_supplier_product, through: :product
  has_many :supplier_products, through: :product, class_name: "ProductsPlugin::Product"
  has_one :supplier_product, through: :product
  has_many :suppliers, through: :product
  has_one :supplier, through: :product
  # end

  scope :ordered, -> { joins(:order).where "orders_plugin_orders.status = ?", "ordered" }
  scope :for_product, ->(product) { where product_id: product.id }

  default_scope -> { includes :product }

  validate :has_order
  validates_presence_of :product
  validates_inclusion_of :status, in: DbStatuses

  before_validation :set_defaults
  before_save :save_calculated_prices
  before_save :step_status
  before_create :sync_fields

  # utility for other classes
  DefineTotals = proc do
    StatusDataMap.each do |status, data|
      quantity = "quantity_#{data}".to_sym
      price = "price_#{data}".to_sym

      self.send :define_method, "total_#{quantity}" do |items = nil|
        items ||= (self.ordered_items rescue nil) || self.items
        items.collect(&quantity).inject(0) { |sum, q| sum + q.to_f }
      end
      self.send :define_method, "total_#{price}" do |items = nil|
        items ||= (self.ordered_items rescue nil) || self.items
        items.collect(&price).inject(0) { |sum, p| sum + p.to_f }
      end

      has_number_with_locale "total_#{quantity}"
      has_currency "total_#{price}"
    end
  end

  extend CurrencyFields::ClassMethods
  has_currency :price
  StatusDataMap.each do |status, data|
    quantity = "quantity_#{data}"
    price = "price_#{data}"

    has_number_with_locale quantity
    has_currency price

    validates_numericality_of quantity, allow_nil: true
    validates_numericality_of price, allow_nil: true
  end

  # Attributes cached from product
  def name
    self[:name] || (self.product.name rescue nil)
  end

  def price
    self[:price] || (self.product.price_with_discount || 0 rescue nil)
  end

  def price_without_margins
    self.product.price_without_margins rescue self.price
  end

  def unit
    self.product.unit
  end

  def unit_name
    self.unit.singular if self.unit
  end

  def supplier
    self.product.supplier rescue self.order.profile.self_supplier
  end

  def supplier_name
    if self.product.supplier
      self.product.supplier.abbreviation_or_name
    else
      self.order.profile.short_name
    end
  end

  def calculated_status
    status = self.order.status
    index = Statuses.index status
    next_status = Statuses[index + 1] if index
    next_quantity = "quantity_#{StatusDataMap[next_status]}" if next_status
    if next_status && self.send(next_quantity) then next_status else status end
  end

  def on_next_status?
    self.order.status != self.calculated_status
  end

  # product used for comparizon when repeating an order
  # override on subclasses
  def repeat_product
    self.product
  end

  def next_status_quantity_field(actor_name)
    status = StatusDataMap[self.order.next_status actor_name] || "consumer_ordered"
    "quantity_#{status}"
  end

  def next_status_quantity(actor_name)
    self.send self.next_status_quantity_field(actor_name)
  end

  def next_status_quantity_set(actor_name, value)
    self.send "#{self.next_status_quantity_field actor_name}=", value
  end

  def status_quantity_field
    @status_quantity_field ||= begin
      status = StatusDataMap[self.status] || "consumer_ordered"
      "quantity_#{status}"
    end
  end

  def status_price_field
    @status_price_field ||= begin
      status = StatusDataMap[self.status] || "consumer_ordered"
      "price_#{status}"
    end
  end

  def status_quantity
    self.send self.status_quantity_field
  end

  def status_quantity=(value)
    self.send "#{self.status_quantity_field}=", value
  end

  def status_price
    self.send self.status_price_field
  end

  def status_price=(value)
    self.send "#{self.status_price_field}=", value
  end

  StatusDataMap.each do |status, data|
    quantity = "quantity_#{data}".to_sym
    price = "price_#{data}".to_sym

    define_method "calculated_#{price}" do
      self.price * self.send(quantity) rescue nil
    end

    define_method price do
      self[price] || self.send("calculated_#{price}")
    end
  end

  def quantity_price_data(actor_name)
    data = { flags: {} }
    statuses = ::OrdersPlugin::Order::Statuses
    statuses_data = data[:statuses] = {}

    current = statuses.index(self.status) || 0
    next_status = self.order.next_status actor_name
    next_index = statuses.index(next_status) || current + 1
    goto_next = actor_name == StatusAccessMap[next_status]

    new_price = nil
    # compare with product
    if self.product_diff
      if self.repeat_product && self.repeat_product.available
        if self.price != self.repeat_product.price
          new_price = self.repeat_product.price
          data[:new_price] = self.repeat_product.price_as_currency_number
        end
      else
        data[:flags][:unavailable] = true
      end
    end

    # Fetch data
    statuses.each.with_index do |status, i|
      data_field = StatusDataMap[status]
      access = StatusAccessMap[status]

      status_data = statuses_data[status] = {
        flags: {},
        field: data_field,
        access: access,
      }

      quantity = self.send "quantity_#{data_field}"
      if quantity.present?
        # quantity is used on <input type=number> so it should not be localized
        status_data[:quantity] = quantity
        status_data[:flags][:removed] = true if status_data[:quantity].zero?
        status_data[:price] = self.send "price_#{data_field}_as_currency_number"
        status_data[:new_price] = quantity * new_price if new_price
        status_data[:flags][:filled] = true
      else
        status_data[:flags][:empty] = true
      end

      if i == current
        status_data[:flags][:current] = true
      elsif (i == next_index) && goto_next
        status_data[:flags][:admin] = true
      end

      break if (if goto_next then i == next_index else i < next_index end)
    end

    # Set flags according to past/future data
    # Present flags are used as classes
    statuses_data.each.with_index do |(status, status_data), i|
      prev_status_data = statuses_data[statuses[i - 1]] unless i.zero?

      if prev_status_data
        if status_data[:quantity] == prev_status_data[:quantity]
          status_data[:flags][:not_modified] = true
        elsif status_data[:flags][:empty]
          # fill with previous status data
          status_data[:quantity] = prev_status_data[:quantity]
          status_data[:price] = prev_status_data[:price]
          status_data[:flags][:filled] = status_data[:flags].delete :empty
          status_data[:flags][:not_modified] = true
        end
      end
    end

    # reverse_each is necessary to set overwritten with intermediate not_modified
    statuses_data.reverse_each.with_index do |(status, status_data), i|
      prev_status_data = statuses_data[statuses[-i - 1]]
      if status_data[:not_modified] ||
         (prev_status_data && prev_status_data[:flags][:filled] && (status_data[:quantity] != prev_status_data[:quantity]))
        status_data[:flags][:overwritten] = true
      end
    end

    # Set access
    statuses_data.each.with_index do |(status, status_data), i|
      # consumer_may_edit = actor_name == :consumer and status == 'ordered' and self.order.open?
      if StatusAccessMap[status] == actor_name
        status_data[:flags][:editable] = true
      end
      # only allow last status
      # status_data[:flags][:editable] = true if status_data[:access] == actor_name and (status_data[:flags][:admin] or self.order.open?)
    end

    data
  end

  def calculate_prices(price)
    self.price = price
    self.save_calculated_prices
  end

  # used by db/migrate/20150627232432_add_status_to_orders_plugin_item.rb
  def fill_status
    status = self.calculated_status
    return if self.status == status

    self.update_column :status, status
    self.order.update_column :building_next_status, true if (self.order.status != status) && (not self.order.building_next_status)
  end

  protected

    def save_calculated_prices
      StatusDataMap.each do |status, data|
        price = "price_#{data}".to_sym
        self.send "#{price}=", self.send("calculated_#{price}")
      end
    end

    def set_defaults
      self.status ||= Statuses.first
    end

    def step_status
      status = self.calculated_status
      return if self.status == status

      self.status = status
      self.order.update_column :building_next_status, true if (self.order.status != status) && (not self.order.building_next_status)
    end

    def has_order
      self.order || self.sale || self.purchase
    end

    def sync_fields
      self.name = self.product.name
      self.price = self.product.price
    end
end