foodsoft/app/models/ordergroup.rb
Patrick Gansterer e7657b987f Update model to support financial transactions #367
This change introduces two new data types to group the financial
transactions. Now every transaction has a "type", which itself belongs
to a "class".
Types should be used add structured information to an transaction, instead
of writing it into the notice textfield. E.g. this could be used to have
different types depending on the source of money (cash vs. bank transfer).
Classes are shown as different columns in the tables and will be uses to
group transactions of specific types. They should be used if not the whole
amount of ordergroup should be used to order food. E.g. if there is a
deposit or membership fee, which is independent of the normal credit.
This will allow us to implement additional features based on classes in
the future. E.g. the sum of transactions in the "membership fee" class
must be positive to allow food orders or show a big warning if it is bellow
a certain value.
2017-12-09 11:51:23 +01:00

134 lines
5 KiB
Ruby

# encoding: utf-8
#
# Ordergroups can order, they are "children" of the class Group
#
# Ordergroup have the following attributes, in addition to Group
# * account_balance (decimal)
class Ordergroup < Group
include CustomFields
APPLE_MONTH_AGO = 6 # How many month back we will count tasks and orders sum
serialize :stats
has_many :financial_transactions
has_many :group_orders
has_many :orders, :through => :group_orders
validates_numericality_of :account_balance, :message => I18n.t('ordergroups.model.invalid_balance')
validate :uniqueness_of_name, :uniqueness_of_members
after_create :update_stats!
def contact
"#{contact_phone} (#{contact_person})"
end
def non_members
User.natural_order.all.reject { |u| (users.include?(u) || u.ordergroup) }
end
def last_user_activity
last_active_user = users.order('users.last_activity DESC').first
if last_active_user
last_active_user.last_activity
end
end
# the most recent order this ordergroup was participating in
def last_order
orders.order('orders.starts DESC').first
end
def value_of_open_orders(exclude = nil)
group_orders.in_open_orders.reject{|go| go == exclude}.collect(&:price).sum
end
def value_of_finished_orders(exclude = nil)
group_orders.in_finished_orders.reject{|go| go == exclude}.collect(&:price).sum
end
# Returns the available funds for this order group (the account_balance minus price of all non-closed GroupOrders of this group).
# * exclude (GroupOrder): exclude this GroupOrder from the calculation
def get_available_funds(exclude = nil)
account_balance - value_of_open_orders(exclude) - value_of_finished_orders(exclude)
end
# Creates a new FinancialTransaction for this Ordergroup and updates the account_balance accordingly.
# Throws an exception if it fails.
def add_financial_transaction!(amount, note, user, transaction_type, link = nil)
transaction do
t = FinancialTransaction.new(ordergroup: self, amount: amount, note: note, user: user, financial_transaction_type: transaction_type, financial_link: link)
t.save!
self.account_balance = financial_transactions.sum('amount')
save!
# Notify only when order group had a positive balance before the last transaction:
if t.amount < 0 && self.account_balance < 0 && self.account_balance - t.amount >= 0
Resque.enqueue(UserNotifier, FoodsoftConfig.scope, 'negative_balance', self.id, t.id)
end
end
end
def update_stats!
# Get hours for every job of each user in period
jobs = users.to_a.sum { |u| u.tasks.done.where('updated_on > ?', APPLE_MONTH_AGO.month.ago).sum(:duration) }
# Get group_order.price for every finished order in this period
orders_sum = group_orders.includes(:order).merge(Order.finished).where('orders.ends >= ?', APPLE_MONTH_AGO.month.ago).references(:orders).sum(:price)
@readonly = false # Dirty hack, avoid getting RecordReadOnly exception when called in task after_save callback. A rails bug?
update_attribute(:stats, {:jobs_size => jobs, :orders_sum => orders_sum})
end
def avg_jobs_per_euro
stats[:jobs_size].to_f / stats[:orders_sum].to_f rescue 0
end
# This is the ordergroup job per euro performance
# in comparison to the hole foodcoop average
def apples
((avg_jobs_per_euro / Ordergroup.avg_jobs_per_euro) * 100).to_i rescue 0
end
# If the the option stop_ordering_under is set, the ordergroup is only allowed to participate in an order,
# when the apples value is above the configured amount.
# The restriction can be deactivated for each ordergroup.
# Only ordergroups, which have participated in more than 5 orders in total and more than 2 orders in apple time period
def not_enough_apples?
FoodsoftConfig[:use_apple_points] &&
FoodsoftConfig[:stop_ordering_under].present? &&
!ignore_apple_restriction &&
apples < FoodsoftConfig[:stop_ordering_under] &&
group_orders.count > 5 &&
group_orders.joins(:order).merge(Order.finished).where('orders.ends >= ?', APPLE_MONTH_AGO.month.ago).count > 2
end
# Global average
def self.avg_jobs_per_euro
stats = Ordergroup.pluck(:stats)
stats.sum {|s| s[:jobs_size].to_f } / stats.sum {|s| s[:orders_sum].to_f } rescue 0
end
def account_updated
financial_transactions.last.try(:created_on) || created_on
end
private
# Make sure, that a user can only be in one ordergroup
def uniqueness_of_members
users.each do |user|
errors.add :user_tokens, I18n.t('ordergroups.model.error_single_group', :user => user.display) if user.groups.where(:type => 'Ordergroup').size > 1
end
end
# Make sure, the name is uniq, add usefull message if uniq group is already deleted
def uniqueness_of_name
group = Ordergroup.where(name: name)
group = group.where.not(id: self.id) unless new_record?
if group.exists?
message = group.first.deleted? ? :taken_with_deleted : :taken
errors.add :name, message
end
end
end