plugins/orders/models/orders_plugin/order.rb
class OrdersPlugin::Order < ApplicationRecord
# if abstract_class is true then it will trigger https://github.com/rails/rails/issues/20871
# self.abstract_class = true
Statuses = ::OrdersPlugin::Item::Statuses
DbStatuses = ::OrdersPlugin::Item::DbStatuses
UserStatuses = ::OrdersPlugin::Item::UserStatuses
StatusText = ::OrdersPlugin::Item::StatusText
# oh, we need a payments plugin!
PaymentMethods = {
money: proc { _"Money" },
check: proc { _"shopping_cart|Check" },
credit_card: proc { _ "Credit card" },
bank_transfer: proc { _ "Bank transfer" },
boleto: proc { _ "Boleto" },
}
# remember to translate on changes
ActorData = [
:name, :email, :contact_phone,
]
DeliveryData = [
:name, :description,
:address_line1, :address_line2, :reference,
:district, :city, :state,
:postal_code, :zip_code,
]
PaymentData = [
:method, :change,
]
# copy, for easiness. can't be declared to here to avoid cyclic reference
StatusDataMap = ::OrdersPlugin::Item::StatusDataMap
StatusAccessMap = ::OrdersPlugin::Item::StatusAccessMap
StatusesByActor = {
consumer: StatusAccessMap.map { |s, a| s if a == :consumer }.compact,
supplier: StatusAccessMap.map { |s, a| s if a == :supplier }.compact,
}
attr_accessible :status, :consumer, :profile,
:supplier_delivery_id, :consumer_delivery_id, :supplier_delivery_data, :consumer_delivery_data
belongs_to :profile, optional: true
# may be override by subclasses
belongs_to :supplier, foreign_key: :profile_id, class_name: "Profile", optional: true
belongs_to :consumer, class_name: "Profile", optional: true
belongs_to :session, primary_key: :session_id, foreign_key: :session_id, class_name: "Session", optional: true
has_many :items, -> { order "name ASC" }, class_name: "OrdersPlugin::Item", foreign_key: :order_id, dependent: :destroy
has_many :products, through: :items
belongs_to :supplier_delivery, class_name: "DeliveryPlugin::Method", optional: true
belongs_to :consumer_delivery, class_name: "DeliveryPlugin::Method", optional: true
scope :alphabetical, -> { joins(:consumer).reorder "profiles.name ASC" }
scope :latest, -> { reorder "code ASC" }
scope :default_order, -> { reorder "code DESC" }
scope :of_session, ->session_id { where session_id: session_id }
scope :of_user, ->session_id, consumer_id = nil do
orders = OrdersPlugin::Order.arel_table
cond = orders[:session_id].eq(session_id)
cond = cond.or orders[:consumer_id].eq(consumer_id) if consumer_id
where cond
end
scope :latest, -> { order "created_at DESC" }
scope :draft, -> { where status: "draft" }
scope :planned, -> { where status: "planned" }
scope :cancelled, -> { where status: "cancelled" }
scope :not_cancelled, -> { where "status <> 'cancelled'" }
scope :ordered, -> { where "ordered_at IS NOT NULL" }
scope :confirmed, -> { where "ordered_at IS NOT NULL" }
scope :accepted, -> { where "accepted_at IS NOT NULL" }
scope :separated, -> { where "separated_at IS NOT NULL" }
scope :delivered, -> { where "delivered_at IS NOT NULL" }
scope :received, -> { where "received_at IS NOT NULL" }
scope :for_profile, ->(profile) { where profile_id: profile.id }
scope :for_profile_id, ->(profile_id) { where profile_id: profile_id }
scope :for_supplier, ->(profile) { where profile_id: profile.id }
scope :for_supplier_id, ->(profile_id) { where profile_id: profile_id }
scope :for_consumer, ->(consumer) { where consumer_id: (consumer.id rescue nil) }
scope :for_consumer_id, ->(consumer_id) { where consumer_id: consumer_id }
scope :months, -> { select("DISTINCT(EXTRACT(months FROM orders_plugin_orders.created_at)) as month").order("month DESC") }
scope :years, -> { select("DISTINCT(EXTRACT(YEAR FROM orders_plugin_orders.created_at)) as year").order("year DESC") }
scope :by_month, ->(month) {
where "EXTRACT(month FROM orders_plugin_orders.created_at) <= :month AND EXTRACT(month FROM orders_plugin_orders.created_at) >= :month", month: month
}
scope :by_year, ->(year) {
where "EXTRACT(year FROM orders_plugin_orders.created_at) <= :year AND EXTRACT(year FROM orders_plugin_orders.created_at) >= :year", year: year
}
scope :by_range, ->(start_time, end_time) {
where "orders_plugin_orders.created_at >= :start AND orders_plugin_orders.created_at <= :end", start: start_time, end: end_time
}
scope :with_status, ->(status) { where status: status }
scope :with_code, ->(code) { where code: code }
validates_presence_of :profile
# consumer is optional, as orders can be made by unlogged users
validates_inclusion_of :status, in: DbStatuses
before_validation :check_status
before_validation :change_status
after_save :send_notifications
extend CodeNumbering::ClassMethods
code_numbering :code, scope: -> { self.profile.orders }
serialize :data
extend SerializedSyncedData::ClassMethods
sync_serialized_field :profile do |profile|
{ name: profile.name, email: profile.contact_email, contact_phone: profile.contact_phone } if profile
end
sync_serialized_field :consumer do |consumer|
{ name: consumer.name, email: consumer.contact_email, contact_phone: consumer.contact_phone } if consumer
end
sync_serialized_field :supplier_delivery
sync_serialized_field :consumer_delivery do |consumer_delivery_data|
if self.consumer
h = {}; Profile::LOCATION_FIELDS.each do |field|
h[field.to_sym] = self.consumer.send(field)
end
h
end
end
serialize :payment_data, Hash
# Aliases needed for terms use
alias_method :supplier, :profile
alias_method :supplier_data, :profile_data
def self.search_scope(scope, params)
scope = scope.with_status params[:status] if params[:status].present?
scope = scope.for_consumer_id params[:consumer_id] if params[:consumer_id].present?
scope = scope.for_profile_id params[:supplier_id] if params[:supplier_id].present?
scope = scope.with_code params[:code] if params[:code].present?
scope = scope.by_range params[:start_time], params[:end_time] if params[:start_time].present?
scope = scope.where supplier_delivery_id: params[:delivery_method_id] if params[:delivery_method_id].present?
scope = scope.default_order
scope
end
def self.products_of(orders)
# offered products in case of orders inside cycles
Product.join(:items).includes(:suppliers).where(orders_plugin_items: { order_id: orders.map(&:id) })
end
# for cycle we have situation like this
# / Items
# / \ OfferedProduct (column product_id)
# Order / \ SourceProduct from DistributedProduct (quantity=1, always)
# \ SourceProduct from Product* (multiple for each if is an aggregate product)
# for order outside cycle we have
# / Items
# / \ SourceProduct from DistributedProduct (quantity=1, always)
# Order / \ SourceProduct from Product* (multiple for each if is an aggregate product)
#
# *suppliers usually don't distribute using cycles, so they only have Product
#
def self.supplier_products_by_suppliers(orders)
products_by_supplier = {}
items = self.parent::Item.where(order_id: orders.map(&:id)).includes(sources_supplier_products: [:supplier, :from_product])
items.each do |item|
if item.sources_supplier_products.present?
item.sources_supplier_products.each do |source_sp|
sp = source_sp.from_product
supplier = source_sp.supplier
products_by_supplier[supplier] ||= Set.new
products_by_supplier[supplier] << sp
sp.quantity_ordered ||= 0
sp.quantity_ordered += item.status_quantity * source_sp.quantity
end
else
# the case where cycles and offered products are not involved, so item is linked directly to a Product
sp = item.product
supplier = item.order.profile.self_supplier
products_by_supplier[supplier] ||= Set.new
products_by_supplier[supplier] << sp
sp.quantity_ordered ||= 0
sp.quantity_ordered += item.status_quantity
end
end
products_by_supplier
end
# define on subclasses
def orders_name
raise "undefined"
end
# override on subclasses
def delivery_methods
self.profile.delivery_methods
end
# override on subclasses
def available_products
self.profile.products
end
def actor_data(actor_name)
data = {}; self.send("#{actor_name}_data").each do |k, v|
data[k] = v if OrdersPlugin::Order::ActorData.include?(k) && v.present?
end
data = {} if (data.size == 1) && data[:name].present?
data
end
def actor_delivery_data(actor_name)
data = {}; self.send("#{actor_name}_delivery_data").each do |k, v|
data[k] = v if OrdersPlugin::Order::DeliveryData.include?(k) && v.present?
end
data
end
def delivery_data(actor_name = nil)
return actor_delivery_data actor_name if actor_name
supplier_data = actor_delivery_data :supplier
case self.supplier_delivery_data[:delivery_type]
when "delivery"
consumer_data = actor_delivery_data :consumer
data = consumer_data.dup
data[:name] = supplier_data[:name]
data[:description] = supplier_data[:description]
when "pickup"
data = supplier_data.dup
end
data
end
# All products from the order profile?
# FIXME reimplement to be generic for consumer/supplier
def self_supplier?
return @self_supplier if @self_supplier
self.items.each do |item|
return @self_supplier = false unless (item.product.supplier.self? rescue true)
end
@self_supplier = true
end
def draft?
self.status == "draft"
end
alias_method :open?, :draft?
def planned?
self.status == "planned"
end
def cancelled?
self.status == "cancelled"
end
def ordered?
self.ordered_at.present?
end
def pre_order?
not self.ordered?
end
alias_method :confirmed?, :ordered?
def status_on?(status)
UserStatuses.index(self.current_status) >= UserStatuses.index(status) rescue false
end
def current_status
return @current_status if @current_status # cache
return @current_status = "open" if self.open?
@current_status = self["status"]
end
def status_message
I18n.t StatusText[current_status]
end
def next_status(actor_name)
# allow supplier to confirm and receive orders if admin is true
actor_statuses = if actor_name == :supplier then Statuses else StatusesByActor[actor_name] end
# if no status was found go to the first (-1 to 0)
current_index = Statuses.index(self.status) || -1
next_status = Statuses[current_index + 1]
next_status if actor_statuses.index next_status rescue false
end
def step(actor_name)
new_status = self.next_status actor_name
self.status = new_status if new_status
end
def step!(actor_name)
# don't crash on errors as some suppliers may have been deleted and this order may not be valid anymore
self.save if self.step actor_name
end
def situation
current_index = UserStatuses.index self.current_status || 0
statuses = []
UserStatuses.each_with_index do |status, i|
statuses << status if Statuses.include? status
break if i >= current_index
end
statuses << Statuses.first if statuses.empty?
statuses
end
def may_view?(user)
(@may_view ||= self.profile.admins.include?(user)) || ((self.consumer == user)) && self.profile.members.include?(user)
end
# cache is done independent of user as model cache is per request
def may_edit?(user, admin_action = false)
(@may_edit ||= (admin_action && user.in?(self.profile.admins))) || (self.open? && (self.consumer == user) && user.in?(self.profile.members)) rescue false
end
def verify_actor?(profile, actor_name)
((actor_name == :supplier) && (self.profile == profile)) || ((actor_name == :consumer) && (self.consumer == profile))
end
# ShoppingCart format
def products_list
hash = {}; self.items.map do |item|
hash[item.product_id] = { quantity: item.quantity_consumer_ordered, name: item.name, price: item.price }
end
hash
end
def products_list=(hash)
self.items = hash.map do |id, data|
data[:quantity_consumer_ordered] = data.delete(:quantity)
i = OrdersPlugin::Item.new data
i.product_id = id
i.order = self
i
end
end
def items_summary
self.items.map { |item| "#{item.name} (#{item.quantity_consumer_ordered_localized})" }.join ", "
end
extend CurrencyFields::ClassMethods
instance_exec &OrdersPlugin::Item::DefineTotals
# total_price considering last state
def total_price(actor_name = :consumer, admin = false)
# for admins, we want the next_status while we concluded the finish status change
if admin
price = :status_price
else
data = StatusDataMap[self.status] || StatusDataMap[Statuses.first]
price = "price_#{data}".to_sym
end
items ||= (self.ordered_items rescue nil) || self.items
items.collect(&price).inject(0) { |sum, p| sum + p.to_f }
end
has_currency :total_price
def total(actor_name = :consumer, admin = false)
t = self.total_price actor_name, admin
t += self.supplier_delivery.cost t if self.supplier_delivery.present?
t
end
has_currency :total
def fill_items(from_status, to_status, save = false)
# check for status advance
return if (Statuses.index(to_status) <= Statuses.index(from_status) rescue true)
from_data = StatusDataMap[from_status]
to_data = StatusDataMap[to_status]
return unless from_data.present? && to_data.present?
self.items.each do |item|
# already filled?
next if (quantity = item.send "quantity_#{to_data}").present?
item.send "quantity_#{to_data}=", item.send("quantity_#{from_data}")
item.send "price_#{to_data}=", item.send("price_#{from_data}")
item.save if save
end
end
def enable_product_diff
self.items.each { |i| i.product_diff = true }
end
protected
def check_status
self.status ||= "draft"
# backwards compatibility
self.status = "ordered" if self.status == "confirmed"
end
def change_status
return if self.status_was == self.status
self.fill_items self.status_was, self.status, true
self.items.update_all status: self.status
self.building_next_status = false
# fill dates on status advance
if self.status_on? "ordered"
Statuses.each do |status|
self.send "#{self.status}_at=", Time.now if (self.status_was != status) && (self.status == status)
end
else
# status rewind for draft, planned, forgotten, cancelled, etc
Statuses.each do |status|
self.send "#{status}_at=", nil
end
end
end
def send_notifications
# shopping_cart has its notifications
return if source == "shopping_cart_plugin"
# ignore when status is being rewinded
return if (Statuses.index(self.status) <= Statuses.index(self.status_was) rescue false)
if (self.status == "ordered") && (self.status_was != "ordered")
OrdersPlugin::Mailer.order_confirmation(self).deliver
elsif (self.status == "cancelled") && (self.status_was != "cancelled")
OrdersPlugin::Mailer.order_cancellation(self).deliver
end
end
end