Initial commit of foodsoft 2
This commit is contained in:
commit
5b9a7e05df
657 changed files with 70444 additions and 0 deletions
201
app/models/article.rb
Normal file
201
app/models/article.rb
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
# articles are the internal products which can ordered by ordergroups
|
||||
#
|
||||
# articles have the following attributes:
|
||||
# * name
|
||||
# * supplier_id
|
||||
# * article_category_id
|
||||
# * unit (string, e.g. 500gr, 1liter)
|
||||
# * note
|
||||
# * availability (boolean)
|
||||
# * net_price, decimal (net price, which will be edited by the user)
|
||||
# * gross_price, decimal (gross price (or long price), incl. tax, deposit, price markup ... see environment.rb)
|
||||
# * tax, float (the VAT, value added tax. default is 7.00 which means 7.00%)
|
||||
# * deposit, decimal (deposit, e.g. for bottles)
|
||||
# * unit_quantity, int (the internal(FC) size of trading unit)
|
||||
# * order_number, varchar (for the supplier)
|
||||
# * created_at, timestamp
|
||||
# * updated_at, timestamp
|
||||
#
|
||||
class Article < ActiveRecord::Base
|
||||
belongs_to :supplier
|
||||
belongs_to :article_category
|
||||
|
||||
validates_presence_of :name, :unit, :net_price, :tax, :deposit, :unit_quantity, :supplier_id
|
||||
validates_length_of :name, :in => 4..60
|
||||
validates_length_of :unit, :in => 2..15
|
||||
validates_numericality_of :net_price, :greater_than => 0
|
||||
validates_numericality_of :deposit, :tax
|
||||
|
||||
# calculate the gross_price
|
||||
before_save :calc_gross_price
|
||||
|
||||
# Custom attribute setter that accepts decimal numbers using localized decimal separator.
|
||||
def net_price=(net_price)
|
||||
self[:net_price] = FoodSoft::delocalizeDecimalString(net_price)
|
||||
end
|
||||
|
||||
# Custom attribute setter that accepts decimal numbers using localized decimal separator.
|
||||
def tax=(tax)
|
||||
self[:tax] = FoodSoft::delocalizeDecimalString(tax)
|
||||
end
|
||||
|
||||
# Custom attribute setter that accepts decimal numbers using localized decimal separator.
|
||||
def deposit=(deposit)
|
||||
self[:deposit] = FoodSoft::delocalizeDecimalString(deposit)
|
||||
end
|
||||
|
||||
# calculate the fc price and sets the attribute
|
||||
def calc_gross_price
|
||||
self.gross_price = ((net_price + deposit) * (tax / 100 + 1)) * (FoodSoft::getPriceMarkup / 100 + 1)
|
||||
end
|
||||
|
||||
# Returns true if article has been updated at least 2 days ago
|
||||
def recently_updated
|
||||
updated_at > 2.days.ago
|
||||
end
|
||||
|
||||
# Returns how many units of this article need to be ordered given the specified order quantity and tolerance.
|
||||
# This is determined by calculating how many units can be ordered from the given order quantity, using
|
||||
# the tolerance to order an additional unit if the order quantity is not quiet sufficient.
|
||||
# There must always be at least one item in a unit that is an ordered quantity (no units are ever entirely
|
||||
# filled by tolerance items only).
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# unit_quantity | quantity | tolerance | calculateOrderQuantity
|
||||
# --------------+----------+-----------+-----------------------
|
||||
# 4 | 0 | 2 | 0
|
||||
# 4 | 0 | 5 | 0
|
||||
# 4 | 2 | 2 | 1
|
||||
# 4 | 4 | 2 | 1
|
||||
# 4 | 4 | 4 | 1
|
||||
# 4 | 5 | 3 | 2
|
||||
# 4 | 5 | 4 | 2
|
||||
#
|
||||
def calculateOrderQuantity(quantity, tolerance = 0)
|
||||
unitSize = unit_quantity
|
||||
units = quantity / unitSize
|
||||
remainder = quantity % unitSize
|
||||
units += ((remainder > 0) && (remainder + tolerance >= unitSize) ? 1 : 0)
|
||||
end
|
||||
|
||||
# If the article is used in an active Order, the Order will returned.
|
||||
def inUse
|
||||
Order.find(:all, :conditions => 'finished = 0').each do |order|
|
||||
if order.articles.find_by_id(self)
|
||||
@order = order
|
||||
break
|
||||
end
|
||||
end
|
||||
return @order if @order
|
||||
end
|
||||
|
||||
# Checks if the article is in use before it will deleted
|
||||
def before_destroy
|
||||
raise self.name.to_s + _(" cannot be deleted. The article is used in a current order!") if self.inUse
|
||||
end
|
||||
|
||||
# this method checks, if the shared_article has been changed
|
||||
# unequal attributes will returned in array
|
||||
# if only the timestamps differ and the attributes are equal,
|
||||
# false will returned and self.shared_updated_on will be updated
|
||||
def shared_article_changed?
|
||||
# skip early if the timestamp hasn't changed
|
||||
unless self.shared_updated_on == self.shared_article.updated_on
|
||||
|
||||
# try to convert units
|
||||
# convert supplier's price and unit_quantity into fc-size
|
||||
new_price, new_unit_quantity = self.convert_units
|
||||
new_unit = self.unit
|
||||
unless new_price and new_unit_quantity
|
||||
# if convertion isn't possible, take shared_article-price/unit_quantity
|
||||
new_price, new_unit_quantity, new_unit = self.shared_article.price, self.shared_article.unit_quantity, self.shared_article.unit
|
||||
end
|
||||
|
||||
# check if all attributes differ
|
||||
unequal_attributes = Article.compare_attributes(
|
||||
{
|
||||
:name => [self.name, self.shared_article.name],
|
||||
:manufacturer => [self.manufacturer, self.shared_article.manufacturer.to_s],
|
||||
:origin => [self.origin, self.shared_article.origin],
|
||||
:unit => [self.unit, new_unit],
|
||||
:net_price => [self.net_price, new_price],
|
||||
:tax => [self.tax, self.shared_article.tax],
|
||||
:deposit => [self.deposit, self.shared_article.deposit],
|
||||
# take care of different num-objects.
|
||||
:unit_quantity => [self.unit_quantity.to_s.to_f, new_unit_quantity.to_s.to_f],
|
||||
:note => [self.note.to_s, self.shared_article.note.to_s]
|
||||
}
|
||||
)
|
||||
if unequal_attributes.empty?
|
||||
# when attributes not changed, update timestamp of article
|
||||
self.update_attribute(:shared_updated_on, self.shared_article.updated_on)
|
||||
false
|
||||
else
|
||||
unequal_attributes
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# compare attributes from different articles. used for auto-synchronization
|
||||
# returns array of symbolized unequal attributes
|
||||
def self.compare_attributes(attributes)
|
||||
unequal_attributes = attributes.select { |name, values| values[0] != values[1] }
|
||||
unequal_attributes.collect { |pair| pair[0] }
|
||||
end
|
||||
|
||||
# to get the correspondent shared article
|
||||
def shared_article
|
||||
@shared_article ||= self.supplier.shared_supplier.shared_articles.find_by_number(self.order_number)
|
||||
end
|
||||
|
||||
# convert units in foodcoop-size
|
||||
# uses FoodSoft.get_units_factors to calc the price/unit_quantity
|
||||
# returns new price and unit_quantity in array, when calc is possible => [price, unit_quanity]
|
||||
# returns false if units aren't foodsoft-compatible
|
||||
# returns nil if units are eqal
|
||||
def convert_units
|
||||
if unit != shared_article.unit
|
||||
if shared_article.unit == "KI" and unit == "ST" # 'KI' means a box, with a different amount of items in it
|
||||
# try to match the size out of its name, e.g. "banana 10-12 St" => 10
|
||||
new_unit_quantity = /[0-9\-\s]+(St)/.match(shared_article.name).to_s.to_i
|
||||
if new_unit_quantity and new_unit_quantity > 0
|
||||
new_price = (shared_article.price/new_unit_quantity.to_f).round(2)
|
||||
[new_price, new_unit_quantity]
|
||||
else
|
||||
false
|
||||
end
|
||||
else # get factors for fc and supplier
|
||||
fc_unit_factor, supplier_unit_factor = FoodSoft.get_units_factors[self.unit], FoodSoft.get_units_factors[self.shared_article.unit]
|
||||
if fc_unit_factor and supplier_unit_factor
|
||||
convertion_factor = fc_unit_factor / supplier_unit_factor
|
||||
new_price = BigDecimal((convertion_factor * shared_article.price).to_s).round(2)
|
||||
new_unit_quantity = ( 1 / convertion_factor) * shared_article.unit_quantity
|
||||
[new_price, new_unit_quantity]
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
# Returns Articles in a nested Array, grouped by category and ordered by article name.
|
||||
# The array has the following form:
|
||||
# e.g: [["drugs",[teethpaste, toiletpaper]], ["fruits" => [apple, banana, lemon]]]
|
||||
# TODO: force article to belong to a category and remove this complicated implementation!
|
||||
def self.group_by_category(articles)
|
||||
articles_by_category = {}
|
||||
ArticleCategory.find(:all).each do |category|
|
||||
articles_by_category.merge!(category.name.to_s => articles.select {|article| article.article_category and article.article_category.id == category.id })
|
||||
end
|
||||
# add articles without a category
|
||||
articles_by_category.merge!( "--" => articles.select {|article| article.article_category == nil})
|
||||
# return "clean" hash, sorted by category.name
|
||||
return articles_by_category.reject {|category, array| array.empty?}.sort
|
||||
|
||||
# it could be so easy ... but that doesn't work for empty category-ids...
|
||||
# articles.group_by {|a| a.article_category}.sort {|a, b| a[0].name <=> b[0].name}
|
||||
end
|
||||
end
|
||||
7
app/models/article_category.rb
Normal file
7
app/models/article_category.rb
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
class ArticleCategory < ActiveRecord::Base
|
||||
has_many :articles
|
||||
|
||||
validates_length_of :name, :in => 2..20
|
||||
validates_uniqueness_of :name
|
||||
|
||||
end
|
||||
17
app/models/assignment.rb
Normal file
17
app/models/assignment.rb
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
class Assignment < ActiveRecord::Base
|
||||
# gettext-option
|
||||
untranslate_all
|
||||
|
||||
belongs_to :user
|
||||
belongs_to :task
|
||||
|
||||
# after user is assigned mark task as assigned
|
||||
def after_create
|
||||
self.task.update_attribute(:assigned, true)
|
||||
end
|
||||
|
||||
# update assigned-attribute
|
||||
def after_destroy
|
||||
self.task.update_attribute(:assigned, false) if self.task.assignments.empty?
|
||||
end
|
||||
end
|
||||
21
app/models/financial_transaction.rb
Normal file
21
app/models/financial_transaction.rb
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# financial transactions are the foodcoop internal financial transactions
|
||||
# only order_groups have an account balance and are happy to transfer money
|
||||
#
|
||||
# financial transaction have the following attributes:
|
||||
# * order_group_id (int)
|
||||
# * amount (decimal)
|
||||
# * note (text)
|
||||
# * created_on (datetime)
|
||||
class FinancialTransaction < ActiveRecord::Base
|
||||
belongs_to :order_group
|
||||
belongs_to :user
|
||||
|
||||
validates_presence_of :note, :user_id, :order_group_id
|
||||
validates_numericality_of :amount
|
||||
|
||||
# Custom attribute setter that accepts decimal numbers using localized decimal separator.
|
||||
def amount=(amount)
|
||||
self[:amount] = FoodSoft::delocalizeDecimalString(amount)
|
||||
end
|
||||
|
||||
end
|
||||
90
app/models/group.rb
Normal file
90
app/models/group.rb
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
# Groups organize the User.
|
||||
#
|
||||
# Group have the following attributes
|
||||
# * name
|
||||
# * description
|
||||
# * type (to specify, if it is a OrderGroup)
|
||||
# * role_admin, role_suppliers, role_article_eta, role_finance, role_orders
|
||||
# * weekly_task (if the group should do a job ervery week)
|
||||
# * weekday (on which weekday should the job be done? 1 means monday and so on)
|
||||
#
|
||||
# A Member gets the roles from the Group
|
||||
class Group < ActiveRecord::Base
|
||||
has_many :memberships, :dependent => :destroy
|
||||
has_many :users, :through => :memberships
|
||||
has_many :tasks
|
||||
# returns all non-finished tasks
|
||||
has_many :open_tasks, :class_name => 'Task', :conditions => ['done = ?', false], :order => 'due_date ASC'
|
||||
|
||||
attr_accessible :name, :description, :role_admin, :role_suppliers, :role_article_meta, :role_finance, :role_orders,
|
||||
:weekly_task, :weekday, :task_name, :task_description, :task_required_users
|
||||
|
||||
validates_length_of :name, :in => 1..25
|
||||
validates_uniqueness_of :name
|
||||
|
||||
# messages
|
||||
ERR_LAST_ADMIN_GROUP_UPDATE = "Der letzten Gruppe mit Admin-Rechten darf die Admin-Rolle nicht entzogen werden"
|
||||
ERR_LAST_ADMIN_GROUP_DELETE = "Die letzte Gruppe mit Admin-Rechten darf nicht gelöscht werden"
|
||||
|
||||
# Returns true if the given user if is an member of this group.
|
||||
def member?(user)
|
||||
memberships.find_by_user_id(user.id)
|
||||
end
|
||||
|
||||
# Returns all NONmembers and a checks for possible multiple OrderGroup-Memberships
|
||||
def non_members
|
||||
nonMembers = Array.new
|
||||
for user in User.find(:all, :order => "nick")
|
||||
unless self.users.include?(user) || ( self.is_a?(OrderGroup) && user.find_ordergroup )
|
||||
nonMembers << user
|
||||
end
|
||||
end
|
||||
return nonMembers
|
||||
end
|
||||
|
||||
# Check before destroy a group, if this is the last group with admin role
|
||||
def before_destroy
|
||||
raise ERR_LAST_ADMIN_GROUP_DELETE if self.role_admin == true && Group.find_all_by_role_admin(true).size == 1
|
||||
end
|
||||
|
||||
# Returns an Array with date-objects to represent the next weekly-tasks
|
||||
def next_weekly_tasks(number = 8)
|
||||
# our system starts from 0 (sunday) to 6 (saturday)
|
||||
# get difference between groups weekday and now
|
||||
diff = self.weekday - Time.now.wday
|
||||
if diff >= 0
|
||||
# weektask is in current week
|
||||
nextTask = diff.day.from_now
|
||||
else
|
||||
# weektask is in the next week
|
||||
nextTask = (diff + 7).day.from_now
|
||||
end
|
||||
# now generate the Array
|
||||
nextTasks = Array.new
|
||||
number.times do
|
||||
nextTasks << nextTask
|
||||
nextTask = 1.week.from_now(nextTask)
|
||||
end
|
||||
return nextTasks
|
||||
end
|
||||
|
||||
# get all groups, which are NOT OrderGroups
|
||||
#TODO: better implement a new model, which inherits from Group, e.g. WorkGroup
|
||||
def self.workgroups
|
||||
Group.find :all, :conditions => "type IS NULL"
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# validates uniqueness of the Group.name. Checks groups and order_groups
|
||||
def validate
|
||||
errors.add(:name, "ist schon vergeben") if (group = Group.find_by_name(name) || group = OrderGroup.find_by_name(name)) && self != group
|
||||
end
|
||||
|
||||
# add validation check on update
|
||||
def validate_on_update
|
||||
# error if this is the last group with admin role and role_admin should set to false
|
||||
errors.add(:role_admin, ERR_LAST_ADMIN_GROUP_UPDATE) if self.role_admin == false && Group.find_all_by_role_admin(true).size == 1 && self == Group.find(:first, :conditions => "role_admin = 1")
|
||||
end
|
||||
|
||||
end
|
||||
39
app/models/group_order.rb
Normal file
39
app/models/group_order.rb
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
# A GroupOrder represents an Order placed by an OrderGroup.
|
||||
#
|
||||
# Properties:
|
||||
# * order_id (int): association to the Order
|
||||
# * order_group_id (int): association to the OrderGroup
|
||||
# * group_order_articles: collection of associated GroupOrderArticles
|
||||
# * order_articles: collection of associated OrderArticles (through GroupOrderArticles)
|
||||
# * price (decimal): the price of this GroupOrder (either maximum price if current order or the actual price if finished order)
|
||||
# * lock_version (int): ActiveRecord optimistic locking column
|
||||
# * updated_by (User): the user who last updated this order
|
||||
#
|
||||
class GroupOrder < ActiveRecord::Base
|
||||
# gettext-option
|
||||
untranslate_all
|
||||
|
||||
belongs_to :order
|
||||
belongs_to :order_group
|
||||
has_many :group_order_articles, :dependent => :destroy
|
||||
has_many :order_articles, :through => :group_order_articles
|
||||
has_many :group_order_article_results
|
||||
belongs_to :updated_by, :class_name => "User", :foreign_key => "updated_by_user_id"
|
||||
|
||||
validates_presence_of :order_id
|
||||
validates_presence_of :order_group_id
|
||||
validates_presence_of :updated_by
|
||||
validates_numericality_of :price
|
||||
validates_uniqueness_of :order_group_id, :scope => :order_id # order groups can only order once per order
|
||||
|
||||
# Updates the "price" attribute.
|
||||
# This will be the maximum value of a current order
|
||||
def updatePrice
|
||||
total = 0
|
||||
for article in group_order_articles.find(:all, :include => :order_article)
|
||||
total += article.order_article.article.gross_price * (article.quantity + article.tolerance)
|
||||
end
|
||||
self.price = total
|
||||
end
|
||||
|
||||
end
|
||||
146
app/models/group_order_article.rb
Normal file
146
app/models/group_order_article.rb
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
# A GroupOrderArticle stores the sum of how many items of an OrderArticle are ordered as part of a GroupOrder.
|
||||
# The chronologically order of the OrderGroup - activity are stored in GroupOrderArticleQuantity
|
||||
#
|
||||
# Properties:
|
||||
# * group_order_id (int): association to the GroupOrder
|
||||
# * order_article_id (int): association to the OrderArticle
|
||||
# * quantity (int): number of items ordered
|
||||
# * tolerance (int): number of items ordered as tolerance
|
||||
# * updated_on (timestamp): updated automatically by ActiveRecord
|
||||
#
|
||||
class GroupOrderArticle < ActiveRecord::Base
|
||||
# gettext-option
|
||||
untranslate_all
|
||||
|
||||
belongs_to :group_order
|
||||
belongs_to :order_article
|
||||
has_many :group_order_article_quantities, :dependent => :destroy
|
||||
|
||||
validates_presence_of :group_order_id
|
||||
validates_presence_of :order_article_id
|
||||
validates_inclusion_of :quantity, :in => 0..99
|
||||
validates_inclusion_of :tolerance, :in => 0..99
|
||||
validates_uniqueness_of :order_article_id, :scope => :group_order_id # just once an article per group order
|
||||
|
||||
# Updates the quantity/tolerance for this GroupOrderArticle by updating both GroupOrderArticle properties
|
||||
# and the associated GroupOrderArticleQuantities chronologically.
|
||||
#
|
||||
# See description of the ordering algorithm in the general application documentation for details.
|
||||
def updateQuantities(quantity, tolerance)
|
||||
logger.debug("GroupOrderArticle[#{id}].updateQuantities(#{quantity}, #{tolerance})")
|
||||
logger.debug("Current quantity = #{self.quantity}, tolerance = #{self.tolerance}")
|
||||
|
||||
# Get quantities ordered with the newest item first.
|
||||
quantities = group_order_article_quantities.find(:all, :order => 'created_on desc')
|
||||
logger.debug("GroupOrderArticleQuantity items found: #{quantities.size}")
|
||||
|
||||
if (quantities.size == 0)
|
||||
# There is no GroupOrderArticleQuantity item yet, just insert with desired quantities...
|
||||
logger.debug("No quantities entry at all, inserting a new one with the desired quantities")
|
||||
quantities.push(GroupOrderArticleQuantity.new(:group_order_article => self, :quantity => quantity, :tolerance => tolerance))
|
||||
self.quantity, self.tolerance = quantity, tolerance
|
||||
else
|
||||
# Decrease quantity/tolerance if necessary by going through the existing items and decreasing their values...
|
||||
i = 0
|
||||
while (i < quantities.size && (quantity < self.quantity || tolerance < self.tolerance))
|
||||
logger.debug("Need to decrease quantities for GroupOrderArticleQuantity[#{quantities[i].id}]")
|
||||
if (quantity < self.quantity && quantities[i].quantity > 0)
|
||||
delta = self.quantity - quantity
|
||||
delta = (delta > quantities[i].quantity ? quantities[i].quantity : delta)
|
||||
logger.debug("Decreasing quantity by #{delta}")
|
||||
quantities[i].quantity -= delta
|
||||
self.quantity -= delta
|
||||
end
|
||||
if (tolerance < self.tolerance && quantities[i].tolerance > 0)
|
||||
delta = self.tolerance - tolerance
|
||||
delta = (delta > quantities[i].tolerance ? quantities[i].tolerance : delta)
|
||||
logger.debug("Decreasing tolerance by #{delta}")
|
||||
quantities[i].tolerance -= delta
|
||||
self.tolerance -= delta
|
||||
end
|
||||
i += 1
|
||||
end
|
||||
# If there is at least one increased value: insert a new GroupOrderArticleQuantity object
|
||||
if (quantity > self.quantity || tolerance > self.tolerance)
|
||||
logger.debug("Inserting a new GroupOrderArticleQuantity")
|
||||
quantities.insert(0, GroupOrderArticleQuantity.new(
|
||||
:group_order_article => self,
|
||||
:quantity => (quantity > self.quantity ? quantity - self.quantity : 0),
|
||||
:tolerance => (tolerance > self.tolerance ? tolerance - self.tolerance : 0)
|
||||
))
|
||||
# Recalc totals:
|
||||
self.quantity += quantities[0].quantity
|
||||
self.tolerance += quantities[0].tolerance
|
||||
end
|
||||
end
|
||||
|
||||
# Check if something went terribly wrong and quantites have not been adjusted as desired.
|
||||
if (self.quantity != quantity || self.tolerance != tolerance)
|
||||
raise 'Invalid state: unable to update GroupOrderArticle/-Quantities to desired quantities!'
|
||||
end
|
||||
|
||||
# Remove zero-only items.
|
||||
quantities = quantities.reject { | q | q.quantity == 0 && q.tolerance == 0}
|
||||
|
||||
# Save
|
||||
transaction do
|
||||
quantities.each { | i | i.save! }
|
||||
self.group_order_article_quantities = quantities
|
||||
save!
|
||||
end
|
||||
end
|
||||
|
||||
# Determines how many items of this article the OrderGroup receives.
|
||||
# Returns a hash with three keys: :quantity / :tolerance / :total
|
||||
#
|
||||
# See description of the ordering algorithm in the general application documentation for details.
|
||||
def orderResult
|
||||
quantity = tolerance = 0
|
||||
|
||||
# Get total
|
||||
total = order_article.units_to_order * order_article.article.unit_quantity
|
||||
logger.debug("unitsToOrder => items ordered: #{order_article.units_to_order} => #{total}")
|
||||
|
||||
if (total > 0)
|
||||
# Get all GroupOrderArticleQuantities for this OrderArticle...
|
||||
orderArticles = GroupOrderArticle.find(:all, :conditions => ['order_article_id = ? AND group_order_id IN (?)', order_article.id, group_order.order.group_orders.collect { | o | o.id }])
|
||||
orderQuantities = GroupOrderArticleQuantity.find(:all, :conditions => ['group_order_article_id IN (?)', orderArticles.collect { | i | i.id }], :order => 'created_on')
|
||||
logger.debug("GroupOrderArticleQuantity records found: #{orderQuantities.size}")
|
||||
|
||||
# Determine quantities to be ordered...
|
||||
totalQuantity = i = 0
|
||||
while (i < orderQuantities.size && totalQuantity < total)
|
||||
q = (orderQuantities[i].quantity <= total - totalQuantity ? orderQuantities[i].quantity : total - totalQuantity)
|
||||
totalQuantity += q
|
||||
if (orderQuantities[i].group_order_article_id == self.id)
|
||||
logger.debug("increasing quantity by #{q}")
|
||||
quantity += q
|
||||
end
|
||||
i += 1
|
||||
end
|
||||
|
||||
# Determine tolerance to be ordered...
|
||||
if (totalQuantity < total)
|
||||
logger.debug("determining additional items to be ordered from tolerance")
|
||||
i = 0
|
||||
while (i < orderQuantities.size && totalQuantity < total)
|
||||
q = (orderQuantities[i].tolerance <= total - totalQuantity ? orderQuantities[i].tolerance : total - totalQuantity)
|
||||
totalQuantity += q
|
||||
if (orderQuantities[i].group_order_article_id == self.id)
|
||||
logger.debug("increasing tolerance by #{q}")
|
||||
tolerance += q
|
||||
end
|
||||
i += 1
|
||||
end
|
||||
end
|
||||
|
||||
# calculate the sum of quantity and tolerance:
|
||||
sum = quantity + tolerance
|
||||
|
||||
logger.debug("determined quantity/tolerance/total: #{quantity} / #{tolerance} / #{sum}")
|
||||
end
|
||||
|
||||
{:quantity => quantity, :tolerance => tolerance, :total => sum}
|
||||
end
|
||||
|
||||
end
|
||||
20
app/models/group_order_article_quantity.rb
Normal file
20
app/models/group_order_article_quantity.rb
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# stores the quantity, tolerance and timestamp of an GroupOrderArticle
|
||||
# Considers every update of an article-order, so may rows for one group_order_article ar possible.
|
||||
#
|
||||
# properties:
|
||||
# * group_order_article_id (int)
|
||||
# * quantity (int)
|
||||
# * tolerance (in)
|
||||
# * created_on (timestamp)
|
||||
|
||||
class GroupOrderArticleQuantity < ActiveRecord::Base
|
||||
# gettext-option
|
||||
untranslate_all
|
||||
|
||||
belongs_to :group_order_article
|
||||
|
||||
validates_presence_of :group_order_article_id
|
||||
validates_inclusion_of :quantity, :in => 0..99
|
||||
validates_inclusion_of :tolerance, :in => 0..99
|
||||
|
||||
end
|
||||
27
app/models/group_order_article_result.rb
Normal file
27
app/models/group_order_article_result.rb
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# An GroupOrderArticleResult represents a group-order for a single Article and its quantities,
|
||||
# according to the order quantity/tolerance.
|
||||
# The GroupOrderArticleResult is part of a finished Order, see OrderArticleResult.
|
||||
#
|
||||
# Properties:
|
||||
# * order_article_result_id (int)
|
||||
# * group_order_result_id (int): associated with OrderGroup through GroupOrderResult.group_name
|
||||
# * quantity (int)
|
||||
#
|
||||
class GroupOrderArticleResult < ActiveRecord::Base
|
||||
|
||||
belongs_to :order_article_result
|
||||
belongs_to :group_order_result
|
||||
|
||||
validates_presence_of :order_article_result, :group_order_result, :quantity
|
||||
validates_numericality_of :quantity, :minimum => 0
|
||||
|
||||
# updates the price attribute for the appropriate GroupOrderResult
|
||||
after_update {|result| result.group_order_result.updatePrice }
|
||||
after_destroy {|result| result.group_order_result.updatePrice }
|
||||
|
||||
# Custom attribute setter that accepts decimal numbers using localized decimal separator.
|
||||
def quantity=(quantity)
|
||||
self[:quantity] = FoodSoft::delocalizeDecimalString(quantity)
|
||||
end
|
||||
|
||||
end
|
||||
23
app/models/group_order_result.rb
Normal file
23
app/models/group_order_result.rb
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
# OrderGroups, which participate on a specific order will have a line
|
||||
# Properties:
|
||||
# * order_id, int
|
||||
# * group_name, the name of the group
|
||||
# * price, decimal
|
||||
# * group_order_article_results: collection of associated GroupOrderArticleResults
|
||||
#
|
||||
class GroupOrderResult < ActiveRecord::Base
|
||||
# gettext-option
|
||||
untranslate_all
|
||||
|
||||
belongs_to :order
|
||||
has_many :group_order_article_results, :dependent => :destroy
|
||||
|
||||
# Calculates the Order-Price for the OrderGroup and updates the price-attribute
|
||||
def updatePrice
|
||||
total = 0
|
||||
group_order_article_results.each do |result|
|
||||
total += result.order_article_result.gross_price * result.quantity
|
||||
end
|
||||
update_attribute(:price, total)
|
||||
end
|
||||
end
|
||||
46
app/models/invite.rb
Normal file
46
app/models/invite.rb
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
require 'digest/sha1'
|
||||
|
||||
# Invites are created by foodcoop users to invite a new user into the foodcoop and their order group.
|
||||
#
|
||||
# Attributes:
|
||||
# * token - the authentication token for this invite
|
||||
# * group - the group the new user is to be made a member of
|
||||
# * user - the inviting user
|
||||
# * expires_at - the time this invite expires
|
||||
# * email - the recipient's email address
|
||||
class Invite < ActiveRecord::Base
|
||||
belongs_to :user
|
||||
belongs_to :group
|
||||
|
||||
validates_format_of :email, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i, :message => 'ist keine gültige Email-Adresse'
|
||||
validates_presence_of :user
|
||||
validates_presence_of :group
|
||||
validates_presence_of :token
|
||||
validates_presence_of :expires_at
|
||||
|
||||
attr_accessible :email, :user, :group
|
||||
|
||||
# messages
|
||||
ERR_EMAIL_IN_USE = 'ist bereits in Verwendung'
|
||||
|
||||
protected
|
||||
|
||||
# Before validation, set token and expires_at.
|
||||
def before_validation
|
||||
self.token = Digest::SHA1.hexdigest(Time.now.to_s + rand(100).to_s)
|
||||
self.expires_at = Time.now.advance(:days => 2)
|
||||
end
|
||||
|
||||
# Sends an email to the invited user.
|
||||
def after_create
|
||||
Mailer.deliver_invite(self)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Custom validation: check that email does not already belong to a registered user.
|
||||
def validate_on_create
|
||||
errors.add(:email, ERR_EMAIL_IN_USE) unless User.find_by_email(self.email).nil?
|
||||
end
|
||||
|
||||
end
|
||||
41
app/models/mailer.rb
Normal file
41
app/models/mailer.rb
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# ActionMailer class that handles all emails for the FoodSoft.
|
||||
class Mailer < ActionMailer::Base
|
||||
|
||||
# Sends an email with instructions on how to reset the password.
|
||||
# Assumes user.setResetPasswordToken has been successfully called already.
|
||||
def password(user)
|
||||
request = ApplicationController.current.request
|
||||
subject "[#{FoodSoft::getFoodcoopName}] Neues Passwort für/ New password for " + user.nick
|
||||
recipients user.email
|
||||
from "FoodSoft <#{FoodSoft::getEmailSender}>"
|
||||
body :user => user,
|
||||
:link => url_for(:host => FoodSoft::getHost || request.host, :controller => "login", :action => "password", :id => user.id, :token => user.reset_password_token),
|
||||
:foodsoftUrl => url_for(:host => FoodSoft::getHost || request.host, :controller => "index")
|
||||
end
|
||||
|
||||
# Sends an email copy of the given internal foodsoft message.
|
||||
def message(message)
|
||||
request = ApplicationController.current.request
|
||||
subject "[#{FoodSoft::getFoodcoopName}] " + message.subject
|
||||
recipients message.recipient.email
|
||||
from (message.system_message? ? "FoodSoft <#{FoodSoft::getEmailSender}>" : "#{message.sender.nick} <#{message.sender.email}>")
|
||||
body :body => message.body, :sender => (message.system_message? ? 'Foodsoft' : message.sender.nick),
|
||||
:recipients => message.recipients,
|
||||
:reply => url_for(:host => FoodSoft::getHost || request.host, :controller => "messages", :action => "reply", :id => message),
|
||||
:profile => url_for(:host => FoodSoft::getHost || request.host, :controller => "index", :action => "myProfile", :id => message.recipient),
|
||||
:link => url_for(:host => FoodSoft::getHost || request.host, :controller => "messages", :action => "show", :id => message),
|
||||
:foodsoftUrl => url_for(:host => FoodSoft::getHost || request.host, :controller => "index")
|
||||
end
|
||||
|
||||
# Sends an invite email.
|
||||
def invite(invite)
|
||||
request = ApplicationController.current.request
|
||||
subject "Einladung in die Foodcoop #{FoodSoft::getFoodcoopName} - Invitation to the Foodcoop"
|
||||
recipients invite.email
|
||||
from "FoodSoft <#{FoodSoft::getEmailSender}>"
|
||||
body :invite => invite,
|
||||
:link => url_for(:host => FoodSoft::getHost || request.host, :controller => "login", :action => "invite", :id => invite.token),
|
||||
:foodsoftUrl => url_for(:host => FoodSoft::getHost || request.host, :controller => "index")
|
||||
end
|
||||
|
||||
end
|
||||
16
app/models/membership.rb
Normal file
16
app/models/membership.rb
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
class Membership < ActiveRecord::Base
|
||||
|
||||
# gettext-option
|
||||
untranslate_all
|
||||
|
||||
belongs_to :user
|
||||
belongs_to :group
|
||||
|
||||
# messages
|
||||
ERR_NO_ADMIN_MEMBER_DELETE = "Mitgliedschaft kann nicht beendet werden. Du bist die letzte Administratorin"
|
||||
|
||||
# check if this is the last admin-membership and deny
|
||||
def before_destroy
|
||||
raise ERR_NO_ADMIN_MEMBER_DELETE if self.group.role_admin? && self.group.memberships.size == 1 && Group.find_all_by_role_admin(true).size == 1
|
||||
end
|
||||
end
|
||||
101
app/models/message.rb
Normal file
101
app/models/message.rb
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
# A message within the foodsoft.
|
||||
#
|
||||
# * sender (User) - the sending User (might be nil if it is a system message)
|
||||
# * recipient (User) - the receiving User
|
||||
# * recipients (String) - list of all recipients of this message as User.nick/Group.name
|
||||
# * subject (string) - message subject
|
||||
# * body (string) - message body
|
||||
# * read? (boolean) - message read status
|
||||
# * email_state (integer) - email state, one of EMAIL_STATE.values
|
||||
# * created_on (timestamp) - creation timestamp
|
||||
class Message < ActiveRecord::Base
|
||||
belongs_to :sender, :class_name => "User", :foreign_key => "sender_id"
|
||||
belongs_to :recipient, :class_name => "User", :foreign_key => "recipient_id"
|
||||
|
||||
attr_accessible :recipient_id, :recipient, :subject, :body, :recipients
|
||||
|
||||
# needed for method 'from_template'
|
||||
include GetText::Rails
|
||||
|
||||
# Values for the email_state attribute: :none, :pending, :sent, :failed
|
||||
EMAIL_STATE = {
|
||||
:none => 0,
|
||||
:pending => 1,
|
||||
:sent => 2,
|
||||
:failed => 3
|
||||
}
|
||||
|
||||
validates_presence_of :recipient_id
|
||||
validates_length_of :subject, :in => 1..255
|
||||
validates_presence_of :recipients
|
||||
validates_presence_of :body
|
||||
validates_inclusion_of :email_state, :in => EMAIL_STATE.values
|
||||
|
||||
@@pending = false
|
||||
|
||||
# Automatically determine if this message should be send as an email.
|
||||
def before_validation_on_create
|
||||
if (recipient && recipient.settings["messages.sendAsEmail"] == '1')
|
||||
self.email_state = EMAIL_STATE[:pending]
|
||||
else
|
||||
self.email_state = EMAIL_STATE[:none]
|
||||
end
|
||||
end
|
||||
|
||||
# Determines if this new message is a pending email.
|
||||
def after_create
|
||||
@@pending = @@pending || self.email_state == EMAIL_STATE[:pending]
|
||||
end
|
||||
|
||||
# Returns true if there might be pending emails.
|
||||
def self.pending?
|
||||
@@pending
|
||||
end
|
||||
|
||||
# Returns true if this message is a system message, i.e. was sent automatically by the FoodSoft itself.
|
||||
def system_message?
|
||||
self.sender_id.nil?
|
||||
end
|
||||
|
||||
# Sends all pending messages that are to be send as emails.
|
||||
def self.send_emails
|
||||
transaction do
|
||||
messages = find(:all, :conditions => ["email_state = ?", EMAIL_STATE[:pending]], :lock => true)
|
||||
logger.debug("Sending #{messages.size} pending messages as emails...") unless messages.empty?
|
||||
for message in messages
|
||||
if (message.recipient && message.recipient.email && !message.recipient.email.empty?)
|
||||
begin
|
||||
Mailer.deliver_message(message)
|
||||
message.update_attribute(:email_state, EMAIL_STATE[:sent])
|
||||
logger.debug("Delivered message as email: id = #{message.id}, recipient = #{message.recipient.nick}, subject = \"#{message.subject}\"")
|
||||
rescue => exception
|
||||
message.update_attribute(:email_state, EMAIL_STATE[:failed])
|
||||
logger.warn("Failed to deliver message as email: id = #{message.id}, recipient = #{message.recipient.nick}, subject = \"#{message.subject}\", exception = #{exception.message}")
|
||||
end
|
||||
else
|
||||
message.update_attribute(:email_state, EMAIL_STATE[:failed])
|
||||
logger.warn("Cannot deliver message as email (no user email): id = #{message.id}, recipient = #{message.recipient.nick}, subject = \"#{message.subject}\"")
|
||||
end
|
||||
end
|
||||
logger.debug("Done sending emails.") unless messages.empty?
|
||||
@@pending = false
|
||||
end
|
||||
end
|
||||
|
||||
# Returns a new message object created from the attributes specified (recipient, recipients, subject)
|
||||
# and the body from the given template that can make use of the variables specified.
|
||||
# The templates are to be stored in app/views/messages, i.e. the template name
|
||||
# "order_finished" would invoke template file "app/views/messages/order_finished.rhtml".
|
||||
# Note: you need to set the sender afterwards if this should not be a system message.
|
||||
#
|
||||
# Example:
|
||||
# Message.from_template(
|
||||
# 'order_finished',
|
||||
# {:user => user, :group => order_group, :order => self, :results => results, :total => group_order.price},
|
||||
# {:recipient_id => user.id, :recipients => recipients, :subject => "Bestellung beendet: #{self.name}"}
|
||||
# ).save!
|
||||
def self.from_template(template, vars, attributes)
|
||||
view = ActionView::Base.new(Rails::Configuration.new.view_path, {}, MessagesController.new)
|
||||
new(attributes.merge(:body => view.render(:file => "messages/#{template}.rhtml", :locals => vars)))
|
||||
end
|
||||
end
|
||||
316
app/models/order.rb
Normal file
316
app/models/order.rb
Normal file
|
|
@ -0,0 +1,316 @@
|
|||
class Order < ActiveRecord::Base
|
||||
has_many :order_articles, :dependent => :destroy
|
||||
has_many :articles, :through => :order_articles
|
||||
has_many :group_orders, :dependent => :destroy
|
||||
has_many :order_groups, :through => :group_orders
|
||||
has_many :order_article_results, :dependent => :destroy
|
||||
has_many :group_order_results, :dependent => :destroy
|
||||
belongs_to :supplier
|
||||
belongs_to :updated_by, :class_name => "User", :foreign_key => "updated_by_user_id"
|
||||
|
||||
validates_length_of :name, :in => 2..50
|
||||
validates_presence_of :starts
|
||||
validates_presence_of :supplier_id
|
||||
validates_inclusion_of :finished, :in => [true, false]
|
||||
validates_numericality_of :invoice_amount, :deposit, :deposit_credit
|
||||
|
||||
validate_on_create :include_articles
|
||||
|
||||
# attr_accessible :name, :supplier, :starts, :ends, :note, :invoice_amount, :deposit, :deposit_credit, :invoice_number, :invoice_date
|
||||
|
||||
#use plugin to make Order commentable
|
||||
acts_as_commentable
|
||||
|
||||
# easyier find of next or previous model
|
||||
acts_as_ordered :order => "ends"
|
||||
|
||||
# Custom attribute setter that accepts decimal numbers using localized decimal separator.
|
||||
def invoice_amount=(amount)
|
||||
self[:invoice_amount] = FoodSoft::delocalizeDecimalString(amount)
|
||||
end
|
||||
|
||||
# Custom attribute setter that accepts decimal numbers using localized decimal separator.
|
||||
def deposit=(deposit)
|
||||
self[:deposit] = FoodSoft::delocalizeDecimalString(deposit)
|
||||
end
|
||||
|
||||
# Custom attribute setter that accepts decimal numbers using localized decimal separator.
|
||||
def deposit_credit=(deposit)
|
||||
self[:deposit_credit] = FoodSoft::delocalizeDecimalString(deposit)
|
||||
end
|
||||
|
||||
# Create or destroy OrderArticle associations on create/update
|
||||
def article_ids=(ids)
|
||||
# fetch selected articles
|
||||
articles_list = Article.find(ids)
|
||||
# create new order_articles
|
||||
(articles_list - articles).each { |article| order_articles.build(:article => article) }
|
||||
# delete old order_articles
|
||||
articles.reject { |article| articles_list.include?(article) }.each do |article|
|
||||
order_articles.detect { |order_article| order_article.article_id == article.id }.destroy
|
||||
end
|
||||
end
|
||||
|
||||
# Returns all current orders, i.e. orders that are not finished and the current time is between the order's start and end time.
|
||||
def self.find_current
|
||||
find(:all, :conditions => ['finished = ? AND starts < ? AND (ends IS NULL OR ends > ?)', false, Time.now, Time.now], :order => 'ends desc', :include => :supplier)
|
||||
end
|
||||
|
||||
# Returns true if this is a current order (not finished and current time matches starts/ends).
|
||||
def current?
|
||||
!finished? && starts < Time.now && (!ends || ends > Time.now)
|
||||
end
|
||||
|
||||
# Returns all finished or expired orders, exclude booked orders
|
||||
def self.find_finished
|
||||
find(:all, :conditions => ['(finished = ? OR ends < ?) AND booked = ?', true, Time.now, false], :order => 'ends desc', :include => :supplier)
|
||||
end
|
||||
|
||||
# Return all booked Orders
|
||||
def self.find_booked
|
||||
find :all, :conditions => ['booked = ?', true], :order => 'ends desc', :include => :supplier
|
||||
end
|
||||
|
||||
# search GroupOrder of given OrderGroup
|
||||
def group_order(ordergroup)
|
||||
unless finished
|
||||
return group_orders.detect {|o| o.order_group_id == ordergroup.id}
|
||||
else
|
||||
return group_order_results.detect {|o| o.group_name == ordergroup.name}
|
||||
end
|
||||
end
|
||||
|
||||
# Returns OrderArticles in a nested Array, grouped by category and ordered by article name.
|
||||
# The array has the following form:
|
||||
# e.g: [["drugs",[teethpaste, toiletpaper]], ["fruits" => [apple, banana, lemon]]]
|
||||
def get_articles
|
||||
articles= order_articles.find :all, :include => :article, :order => "articles.name"
|
||||
articles_by_category= Hash.new
|
||||
ArticleCategory.find(:all).each do |category|
|
||||
articles_by_category.merge!(category.name.to_s => articles.select {|order_article| order_article.article.article_category == category})
|
||||
end
|
||||
# add articles without a category
|
||||
articles_by_category.merge!( "--" => articles.select {|order_article| order_article.article.article_category == nil})
|
||||
# return "clean" hash, sorted by category.name
|
||||
return articles_by_category.reject {|category, order_articles| order_articles.empty?}.sort
|
||||
|
||||
# it could be so easy ... but that doesn't work for empty category-ids...
|
||||
# order_articles.group_by {|a| a.article.article_category}.sort {|a, b| a[0].name <=> b[0].name}
|
||||
end
|
||||
|
||||
# Returns the defecit/benefit for the foodcoop
|
||||
def fcProfit(with_markup = true)
|
||||
groups_sum = with_markup ? sumPrice("groups") : sumPrice("groups_without_markup")
|
||||
groups_sum - invoice_amount + deposit - deposit_credit
|
||||
end
|
||||
|
||||
# Returns the all round price of a finished order
|
||||
# "groups" returns the sum of all GroupOrderResults
|
||||
# "clear" returns the price without tax, deposit and markup
|
||||
# "gross" includes tax and deposit. this amount should be equal to suppliers bill
|
||||
# "fc", guess what...
|
||||
# for unfinished orders it returns the gross price
|
||||
def sumPrice(type = "gross")
|
||||
sum = 0
|
||||
if finished?
|
||||
if type == "groups"
|
||||
for groupResult in group_order_results
|
||||
for result in groupResult.group_order_article_results
|
||||
sum += result.order_article_result.gross_price * result.quantity
|
||||
end
|
||||
end
|
||||
elsif type == "groups_without_markup"
|
||||
for groupResult in group_order_results
|
||||
for result in groupResult.group_order_article_results
|
||||
oar = result.order_article_result
|
||||
sum += (oar.net_price + oar.deposit) * (1 + oar.tax/100) * result.quantity
|
||||
end
|
||||
end
|
||||
else
|
||||
for article in order_article_results
|
||||
case type
|
||||
when 'clear'
|
||||
sum += article.units_to_order * article.unit_quantity * article.net_price
|
||||
when 'gross'
|
||||
sum += article.units_to_order * article.unit_quantity * (article.net_price + article.deposit) * (article.tax / 100 + 1)
|
||||
when "fc"
|
||||
sum += article.units_to_order * article.unit_quantity * article.gross_price
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
for article in order_articles
|
||||
sum += article.units_to_order * article.article.gross_price * article.article.unit_quantity
|
||||
end
|
||||
end
|
||||
sum
|
||||
end
|
||||
|
||||
# Finishes this order. This will set the finish property to "true" and the end property to the current time.
|
||||
# Ignored if the order is already finished.
|
||||
# this will also copied the results into OrderArticleResult, GroupOrderArticleResult and GroupOrderResult
|
||||
def finish(user)
|
||||
unless finished?
|
||||
transaction do
|
||||
#saves ordergroups, which take part in this order
|
||||
self.group_orders.each do |go|
|
||||
group_order_result = GroupOrderResult.create!(:order => self,
|
||||
:group_name => go.order_group.name,
|
||||
:price => go.price)
|
||||
end
|
||||
# saves every article of the order
|
||||
self.get_articles.each do |category, articles|
|
||||
articles.each do |oa|
|
||||
if oa.units_to_order >= 1 # save only successful ordered articles!
|
||||
article_result = OrderArticleResult.new(:order => self,
|
||||
:name => oa.article.name,
|
||||
:unit => oa.article.unit,
|
||||
:net_price => oa.article.net_price,
|
||||
:gross_price => oa.article.gross_price,
|
||||
:tax => oa.article.tax,
|
||||
:deposit => oa.article.deposit,
|
||||
:fc_markup => FoodSoft::getPriceMarkup,
|
||||
:order_number => oa.article.order_number,
|
||||
:unit_quantity => oa.article.unit_quantity,
|
||||
:units_to_order => oa.units_to_order)
|
||||
article_result.save
|
||||
# saves the ordergroup results, belonging to the saved orderd article
|
||||
oa.group_order_articles.each do |goa|
|
||||
result = goa.orderResult
|
||||
# find appropriate GroupOrderResult
|
||||
group_order_result = GroupOrderResult.find(:first, :conditions => ['order_id = ? AND group_name = ?', self.id, goa.group_order.order_group.name])
|
||||
group_order_article_result = GroupOrderArticleResult.new(:order_article_result => article_result,
|
||||
:group_order_result => group_order_result,
|
||||
:quantity => result[:total],
|
||||
:tolerance => result[:tolerance])
|
||||
group_order_article_result.save! if (group_order_article_result.quantity > 0)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
# set new order state (needed by notifyOrderFinished)
|
||||
self.finished = true
|
||||
self.ends = Time.now
|
||||
self.updated_by = user
|
||||
# delete data, which is no longer required, because everything is now in the result-tables
|
||||
self.group_orders.each do |go|
|
||||
go.destroy
|
||||
end
|
||||
self.order_articles.each do |oa|
|
||||
oa.destroy
|
||||
end
|
||||
self.save!
|
||||
# Update all GroupOrder.price
|
||||
self.updateAllGroupOrders
|
||||
# notify order groups
|
||||
notifyOrderFinished
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Updates the ordered quantites of all OrderArticles from the GroupOrderArticles.
|
||||
def updateQuantities
|
||||
orderArticles = Hash.new # holds the list of updated OrderArticles indexed by their id
|
||||
# Get all GroupOrderArticles for this order and update OrderArticle.quantity/.tolerance/.units_to_order from them...
|
||||
articles = GroupOrderArticle.find(:all, :conditions => ['group_order_id IN (?)', group_orders.collect { | o | o.id }], :include => [:order_article])
|
||||
for article in articles
|
||||
if (orderArticle = orderArticles[article.order_article.id.to_s])
|
||||
# OrderArticle has already been fetched, just update...
|
||||
orderArticle.quantity = orderArticle.quantity + article.quantity
|
||||
orderArticle.tolerance = orderArticle.tolerance + article.tolerance
|
||||
orderArticle.units_to_order = orderArticle.article.calculateOrderQuantity(orderArticle.quantity, orderArticle.tolerance)
|
||||
else
|
||||
# First update to OrderArticle, need to store in orderArticle hash...
|
||||
orderArticle = article.order_article
|
||||
orderArticle.quantity = article.quantity
|
||||
orderArticle.tolerance = article.tolerance
|
||||
orderArticle.units_to_order = orderArticle.article.calculateOrderQuantity(orderArticle.quantity, orderArticle.tolerance)
|
||||
orderArticles[orderArticle.id.to_s] = orderArticle
|
||||
end
|
||||
end
|
||||
# Commit changes to database...
|
||||
OrderArticle.transaction do
|
||||
orderArticles.each_value { | value | value.save! }
|
||||
end
|
||||
end
|
||||
|
||||
# Updates the "price" attribute of GroupOrders or GroupOrderResults
|
||||
# This will be either the maximum value of a current order or the actual order value of a finished order.
|
||||
def updateAllGroupOrders
|
||||
unless finished?
|
||||
group_orders.each do |groupOrder|
|
||||
groupOrder.updatePrice
|
||||
groupOrder.save
|
||||
end
|
||||
else #for finished orders
|
||||
group_order_results.each do |groupOrderResult|
|
||||
groupOrderResult.updatePrice
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Sets "booked"-attribute to true and updates all OrderGroup_account_balances
|
||||
def balance(user)
|
||||
raise "Bestellung wurde schon abgerechnet" if self.booked
|
||||
transaction_note = "Bestellung: #{name}, von #{starts.strftime('%d.%m.%Y')} bis #{ends.strftime('%d.%m.%Y')}"
|
||||
transaction do
|
||||
# update OrderGroups
|
||||
group_order_results.each do |result|
|
||||
price = result.price * -1 # decrease! account balance
|
||||
OrderGroup.find_by_name(result.group_name).addFinancialTransaction(price, transaction_note, user)
|
||||
end
|
||||
self.booked = true
|
||||
self.updated_by = user
|
||||
self.save!
|
||||
end
|
||||
end
|
||||
|
||||
# returns the corresponding message for the status
|
||||
def status
|
||||
if !self.finished? && self.ends > Time.now
|
||||
_("running")
|
||||
elsif !self.finished? && self.ends < Time.now
|
||||
_("expired")
|
||||
elsif self.finished? && !self.booked?
|
||||
_("finished")
|
||||
else
|
||||
_("balanced")
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def validate
|
||||
errors.add(:ends, "muss nach dem Bestellstart liegen (oder leer bleiben)") if (ends && starts && ends <= starts)
|
||||
end
|
||||
|
||||
def include_articles
|
||||
errors.add(:order_articles, _("There must be at least one article selected")) if order_articles.empty?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Sends "order finished" messages to users who have participated in this order.
|
||||
def notifyOrderFinished
|
||||
# Loop through GroupOrderResults for this order:
|
||||
for group_order in self.group_order_results
|
||||
order_group = OrderGroup.find_by_name(group_order.group_name)
|
||||
# Determine group users that want a notification message:
|
||||
users = order_group.users.reject{|u| u.settings["notify.orderFinished"] != '1'}
|
||||
unless (users.empty?)
|
||||
# Assemble the order message text:
|
||||
results = group_order.group_order_article_results.find(:all, :include => [:order_article_result])
|
||||
# Create user notification messages:
|
||||
recipients = users.collect{|u| u.nick}.join(', ')
|
||||
for user in users
|
||||
Message.from_template(
|
||||
'order_finished',
|
||||
{:user => user, :group => order_group, :order => self, :results => results, :total => group_order.price},
|
||||
{:recipient_id => user.id, :recipients => recipients, :subject => "Bestellung beendet: #{self.name}"}
|
||||
).save!
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
29
app/models/order_article.rb
Normal file
29
app/models/order_article.rb
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# An OrderArticle represents a single Article that is part of an Order.
|
||||
#
|
||||
# Properties:
|
||||
# * order_id (int): association to the Order
|
||||
# * article_id (int): association to the Article
|
||||
# * quantity (int): number of items ordered by all OrderGroups for this order
|
||||
# * tolerance (int): number of items ordered as tolerance by all OrderGroups for this order
|
||||
# * units_to_order (int): number of packaging units to be ordered according to the order quantity/tolerance
|
||||
#
|
||||
class OrderArticle < ActiveRecord::Base
|
||||
|
||||
# gettext-option
|
||||
untranslate_all
|
||||
|
||||
belongs_to :order
|
||||
belongs_to :article
|
||||
has_many :group_order_articles, :dependent => :destroy
|
||||
|
||||
validates_presence_of :order_id
|
||||
validates_presence_of :article_id
|
||||
validates_uniqueness_of :article_id, :scope => :order_id # an article can only have one record per order
|
||||
|
||||
private
|
||||
|
||||
def validate
|
||||
errors.add(:article, "muss angegeben sein und einen aktuellen Preis haben") if !(article = Article.find(article_id)) || article.gross_price.nil?
|
||||
end
|
||||
|
||||
end
|
||||
73
app/models/order_article_result.rb
Normal file
73
app/models/order_article_result.rb
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
# An OrderArticleResult represents a single Article that is part of a *finished* Order.
|
||||
#
|
||||
# Properties:
|
||||
# * order_id (int): association to the Order
|
||||
# * name (string): article name
|
||||
# * unit (string)
|
||||
# * note (string): for post-editing the ordered article. informations like "new tax is ..."
|
||||
# * net_price (decimal): the net price
|
||||
# * gross_price (decimal): incl tax, deposit, fc-markup
|
||||
# * tax (int)
|
||||
# * deposit (decimal)
|
||||
# * fc_markup (float)
|
||||
# * order_number (string)
|
||||
# * unit_quantity (int): the internal(FC) size of trading unit
|
||||
# * units_to_order (int): number of packaging units to be ordered according to the order quantity/tolerance
|
||||
#
|
||||
class OrderArticleResult < ActiveRecord::Base
|
||||
belongs_to :order
|
||||
has_many :group_order_article_results, :dependent => :destroy
|
||||
|
||||
validates_presence_of :name, :unit, :net_price, :gross_price, :tax, :deposit, :fc_markup, :unit_quantity, :units_to_order
|
||||
validates_numericality_of :net_price, :gross_price, :deposit, :unit_quantity, :units_to_order
|
||||
validates_length_of :name, :minimum => 4
|
||||
|
||||
def make_gross # calculate the gross price and sets the attribute
|
||||
self.gross_price = ((net_price + deposit) * (tax / 100 + 1) * (fc_markup / 100 + 1))
|
||||
end
|
||||
|
||||
# Custom attribute setter that accepts decimal numbers using localized decimal separator.
|
||||
def net_price=(net_price)
|
||||
self[:net_price] = FoodSoft::delocalizeDecimalString(net_price)
|
||||
end
|
||||
|
||||
# Custom attribute setter that accepts decimal numbers using localized decimal separator.
|
||||
def tax=(tax)
|
||||
self[:tax] = FoodSoft::delocalizeDecimalString(tax)
|
||||
end
|
||||
|
||||
# Custom attribute setter that accepts decimal numbers using localized decimal separator.
|
||||
def deposit=(deposit)
|
||||
self[:deposit] = FoodSoft::delocalizeDecimalString(deposit)
|
||||
end
|
||||
|
||||
# Custom attribute setter that accepts decimal numbers using localized decimal separator.
|
||||
def units_to_order=(units_to_order)
|
||||
self[:units_to_order] = FoodSoft::delocalizeDecimalString(units_to_order)
|
||||
end
|
||||
|
||||
# counts from every GroupOrderArticleResult for this ArticleResult
|
||||
# Return a hash with the total quantity (in Article-units) and the total (FC) price
|
||||
def total
|
||||
quantity = 0
|
||||
price = 0
|
||||
for result in self.group_order_article_results
|
||||
quantity += result.quantity
|
||||
price += result.quantity * self.gross_price
|
||||
end
|
||||
return {:quantity => quantity, :price => price}
|
||||
end
|
||||
|
||||
|
||||
# updates the price attribute for all appropriate GroupOrderResults
|
||||
def after_update
|
||||
group_order_article_results.each {|result| result.group_order_result.updatePrice}
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def validate
|
||||
errors.add(:net_price, "should be positive") unless net_price.nil? || net_price > 0
|
||||
end
|
||||
|
||||
end
|
||||
119
app/models/order_group.rb
Normal file
119
app/models/order_group.rb
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
# OrderGroups can order, they are "children" of the class Group
|
||||
#
|
||||
# OrderGroup have the following attributes, in addition to Group
|
||||
# * account_balance (decimal)
|
||||
# * account_updated (datetime)
|
||||
# * actual_size (int) : how many persons are ordering through the OrderGroup
|
||||
class OrderGroup < Group
|
||||
has_many :financial_transactions, :dependent => :destroy
|
||||
has_many :group_orders, :dependent => :destroy
|
||||
has_many :orders, :through => :group_orders
|
||||
has_many :group_order_article_results, :through => :group_orders # TODO: whats this???
|
||||
has_many :group_order_results, :finder_sql => 'SELECT * FROM group_order_results as r WHERE r.group_name = "#{name}"'
|
||||
|
||||
validates_inclusion_of :actual_size, :in => 1..99
|
||||
validates_numericality_of :account_balance, :message => 'ist keine gültige Zahl'
|
||||
|
||||
attr_accessible :actual_size, :account_updated
|
||||
|
||||
# messages
|
||||
ERR_NAME_IS_USED_IN_ARCHIVE = "Der Name ist von einer ehemaligen Gruppe verwendet worden."
|
||||
|
||||
# if the order_group.name is changed, group_order_result.name has to be adapted
|
||||
def before_update
|
||||
ordergroup = OrderGroup.find(self.id)
|
||||
unless (ordergroup.name == self.name) || ordergroup.group_order_results.empty?
|
||||
# rename all finished orders
|
||||
for result in ordergroup.group_order_results
|
||||
result.update_attribute(:group_name, self.name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Returns the available funds for this order group (the account_balance minus price of all non-booked GroupOrders of this group).
|
||||
# * excludeGroupOrder (GroupOrder): exclude this GroupOrder from the calculation
|
||||
def getAvailableFunds(excludeGroupOrder = nil)
|
||||
funds = account_balance
|
||||
for order in GroupOrder.find_all_by_order_group_id(self.id)
|
||||
unless order == excludeGroupOrder
|
||||
funds -= order.price
|
||||
end
|
||||
end
|
||||
for order_result in self.findFinishedNotBooked
|
||||
funds -= order_result.price
|
||||
end
|
||||
return funds
|
||||
end
|
||||
|
||||
# Creates a new FinancialTransaction for this OrderGroup and updates the account_balance accordingly.
|
||||
# Throws an exception if it fails.
|
||||
def addFinancialTransaction(amount, note, user)
|
||||
transaction do
|
||||
trans = FinancialTransaction.new(:order_group => self, :amount => amount, :note => note, :user => user)
|
||||
trans.save!
|
||||
self.account_balance += trans.amount
|
||||
self.account_updated = trans.created_on
|
||||
save!
|
||||
notifyNegativeBalance(trans)
|
||||
end
|
||||
end
|
||||
|
||||
# Returns all GroupOrders by this group that are currently running.
|
||||
def findCurrent
|
||||
group_orders.find(:all, :conditions => ["orders.finished = ? AND orders.starts < ? AND (orders.ends IS NULL OR orders.ends > ?)", false, Time.now, Time.now], :include => :order)
|
||||
end
|
||||
|
||||
#find expired (lapsed) but not manually finished orders
|
||||
def findExpiredOrders
|
||||
group_orders.find(:all, :conditions => ["orders.ends < ?", Time.now], :include => :order, :order => 'orders.ends DESC')
|
||||
end
|
||||
|
||||
# Returns all GroupOrderResults by this group that are finished but not booked yet.
|
||||
def findFinishedNotBooked
|
||||
GroupOrderResult.find(:all,
|
||||
:conditions => ["group_order_results.group_name = ? AND group_order_results.order_id = orders.id AND orders.finished = ? AND orders.booked = ? ", self.name, true, false],
|
||||
:include => :order,
|
||||
:order => 'orders.ends DESC')
|
||||
end
|
||||
|
||||
# Returns all GroupOrderResults for booked orders
|
||||
def findBookedOrders(limit = false, offset = 0)
|
||||
GroupOrderResult.find(:all,
|
||||
:conditions => ["group_order_results.group_name = ? AND group_order_results.order_id = orders.id AND orders.finished = ? AND orders.booked = ? ", self.name, true, true],
|
||||
:include => :order,
|
||||
:order => "orders.ends DESC",
|
||||
:limit => limit,
|
||||
:offset => offset)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# If this order group's account balance is made negative by the given/last transaction,
|
||||
# a message is sent to all users who have enabled notification.
|
||||
def notifyNegativeBalance(transaction)
|
||||
# Notify only when order group had a positive balance before the last transaction:
|
||||
if (transaction.amount < 0 && self.account_balance < 0 && self.account_balance - transaction.amount >= 0)
|
||||
users = self.users.reject{|u| u.settings["notify.negativeBalance"] != '1'}
|
||||
unless (users.empty?)
|
||||
recipients = users.collect{|u| u.nick}.join(', ')
|
||||
for user in users
|
||||
Message.from_template(
|
||||
'negative_balance',
|
||||
{:user => user, :group => self, :transaction => transaction},
|
||||
{:recipient_id => user.id, :recipients => recipients, :subject => "Gruppenkonto im Minus"}
|
||||
).save!
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# before create or update, check if the name is already used in GroupOrderResults
|
||||
def validate_on_create
|
||||
errors.add(:name, ERR_NAME_IS_USED_IN_ARCHIVE) unless GroupOrderResult.find_all_by_group_name(self.name).empty?
|
||||
end
|
||||
def validate_on_update
|
||||
ordergroup = OrderGroup.find(self.id)
|
||||
errors.add(:name, ERR_NAME_IS_USED_IN_ARCHIVE) unless ordergroup.name == self.name || GroupOrderResult.find_all_by_group_name(self.name).empty?
|
||||
end
|
||||
|
||||
end
|
||||
12
app/models/shared_article.rb
Normal file
12
app/models/shared_article.rb
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
class SharedArticle < ActiveRecord::Base
|
||||
|
||||
# gettext-option
|
||||
untranslate_all
|
||||
|
||||
# connect to database from sharedLists-Application
|
||||
SharedArticle.establish_connection(FoodSoft::get_shared_lists_config)
|
||||
# set correct table_name in external DB
|
||||
set_table_name :articles
|
||||
|
||||
belongs_to :shared_supplier, :foreign_key => :supplier_id
|
||||
end
|
||||
17
app/models/shared_supplier.rb
Normal file
17
app/models/shared_supplier.rb
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
class SharedSupplier < ActiveRecord::Base
|
||||
# used for gettext
|
||||
untranslate_all
|
||||
|
||||
# connect to database from sharedLists-Application
|
||||
SharedSupplier.establish_connection(FoodSoft::get_shared_lists_config)
|
||||
# set correct table_name in external DB
|
||||
set_table_name :suppliers
|
||||
|
||||
|
||||
has_one :supplier
|
||||
has_many :shared_articles, :foreign_key => :supplier_id
|
||||
|
||||
# save the lists as an array
|
||||
serialize :lists
|
||||
|
||||
end
|
||||
67
app/models/supplier.rb
Normal file
67
app/models/supplier.rb
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
class Supplier < ActiveRecord::Base
|
||||
has_many :articles, :dependent => :destroy
|
||||
has_many :orders
|
||||
attr_accessible :name, :address, :phone, :phone2, :fax, :email, :url, :contact_person, :customer_number, :delivery_days, :order_howto, :note, :shared_supplier_id, :min_order_quantity
|
||||
|
||||
validates_length_of :name, :in => 4..30
|
||||
validates_uniqueness_of :name
|
||||
|
||||
validates_length_of :phone, :in => 8..20
|
||||
validates_length_of :address, :in => 8..50
|
||||
|
||||
# for the sharedLists-App
|
||||
belongs_to :shared_supplier
|
||||
|
||||
# Returns all articles for this supplier that are available and have a valid price, grouped by article category and ordered by name.
|
||||
def getArticlesAvailableForOrdering
|
||||
articles = Article.find(:all, :conditions => ['supplier_id = ? AND availability = ?', self.id, true], :order => 'article_categories.name, articles.name', :include => :article_category)
|
||||
articles.select {|article| article.gross_price}
|
||||
end
|
||||
|
||||
# sync all articles with the external database
|
||||
# returns an array with articles(and prices), which should be updated (to use in a form)
|
||||
# also returns an array with outlisted_articles, which should be deleted
|
||||
def sync_all
|
||||
updated_articles = Array.new
|
||||
outlisted_articles = Array.new
|
||||
for article in articles.find(:all, :order => "article_categories.name", :include => :article_category)
|
||||
# try to find the associated shared_article
|
||||
shared_article = article.shared_article
|
||||
if shared_article
|
||||
# article will be updated
|
||||
|
||||
# skip if shared_article has not been changed
|
||||
unequal_attributes = article.shared_article_changed?
|
||||
unless unequal_attributes.blank?
|
||||
# update objekt but don't save it
|
||||
|
||||
# try to convert different units
|
||||
new_price, new_unit_quantity = article.convert_units
|
||||
if new_price and new_unit_quantity
|
||||
article.net_price = new_price
|
||||
article.unit_quantity = new_unit_quantity
|
||||
else
|
||||
article.net_price = shared_article.price
|
||||
article.unit_quantity = shared_article.unit_quantity
|
||||
article.unit = shared_article.unit
|
||||
end
|
||||
# update other attributes
|
||||
article.attributes = {
|
||||
:name => shared_article.name,
|
||||
:manufacturer => shared_article.manufacturer,
|
||||
:origin => shared_article.origin,
|
||||
:shared_updated_on => shared_article.updated_on,
|
||||
:tax => shared_article.tax,
|
||||
:deposit => shared_article.deposit,
|
||||
:note => shared_article.note
|
||||
}
|
||||
updated_articles << [article, unequal_attributes]
|
||||
end
|
||||
else
|
||||
# article isn't in external database anymore
|
||||
outlisted_articles << article
|
||||
end
|
||||
end
|
||||
return [updated_articles, outlisted_articles]
|
||||
end
|
||||
end
|
||||
60
app/models/task.rb
Normal file
60
app/models/task.rb
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
class Task < ActiveRecord::Base
|
||||
has_many :assignments, :dependent => :destroy
|
||||
has_many :users, :through => :assignments
|
||||
belongs_to :group
|
||||
|
||||
# form will send user in string. responsibilities will added later
|
||||
attr_protected :users
|
||||
|
||||
validates_length_of :name, :minimum => 3
|
||||
|
||||
|
||||
def is_assigned?(user)
|
||||
self.assignments.detect {|ass| ass.user_id == user.id }
|
||||
end
|
||||
|
||||
def is_accepted?(user)
|
||||
self.assignments.detect {|ass| ass.user_id == user.id && ass.accepted }
|
||||
end
|
||||
|
||||
def enough_users_assigned?
|
||||
assignments.find_all_by_accepted(true).size >= required_users ? true : false
|
||||
end
|
||||
|
||||
# extracts nicknames from a comma seperated string
|
||||
# and makes the users responsible for the task
|
||||
def user_list=(string)
|
||||
@user_list = string.split(%r{,\s*})
|
||||
new_users = @user_list - users.collect(&:nick)
|
||||
old_users = users.reject { |user| @user_list.include?(user.nick) }
|
||||
|
||||
logger.debug "New users: #{new_users}"
|
||||
logger.debug "Old users: #{old_users}"
|
||||
|
||||
self.class.transaction do
|
||||
# delete old assignments
|
||||
if old_users.any?
|
||||
assignments.find(:all, :conditions => ["user_id IN (?)", old_users.collect(&:id)]).each(&:destroy)
|
||||
end
|
||||
# create new assignments
|
||||
new_users.each do |nick|
|
||||
user = User.find_by_nick(nick)
|
||||
if user.blank?
|
||||
errors.add(:user_list)
|
||||
else
|
||||
if user == User.current_user
|
||||
# current_user will accept, when he puts himself to the list of users
|
||||
self.assignments.build :user => user, :accepted => true
|
||||
else
|
||||
# normal assignement
|
||||
self.assignments.build :user => user
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def user_list
|
||||
@user_list ||= users.collect(&:nick).join(", ")
|
||||
end
|
||||
end
|
||||
174
app/models/user.rb
Normal file
174
app/models/user.rb
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
require 'digest/sha1'
|
||||
|
||||
# A foodsoft user.
|
||||
#
|
||||
# * memberships
|
||||
# * groups
|
||||
# * first_name, last_name, email, phone, address
|
||||
# * nick
|
||||
# * password (stored as a hash)
|
||||
# * settings (user properties via acts_as_configurable plugin)
|
||||
# specific user rights through memberships (see Group)
|
||||
class User < ActiveRecord::Base
|
||||
has_many :memberships, :dependent => :destroy
|
||||
has_many :groups, :through => :memberships
|
||||
has_many :assignments, :dependent => :destroy
|
||||
has_many :tasks, :through => :assignments
|
||||
|
||||
attr_accessible :nick, :first_name, :last_name, :email, :phone, :address
|
||||
|
||||
validates_length_of :nick, :in => 2..25
|
||||
validates_uniqueness_of :nick
|
||||
validates_format_of :email, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i
|
||||
validates_uniqueness_of :email
|
||||
validates_length_of :first_name, :in => 2..50
|
||||
|
||||
# Adds support for configuration settings (through "settings" attribute).
|
||||
acts_as_configurable
|
||||
|
||||
# makes the current_user (logged-in-user) available in models
|
||||
cattr_accessor :current_user
|
||||
|
||||
# User settings keys
|
||||
# returns the User-settings and the translated description
|
||||
def self.setting_keys
|
||||
settings_hash = {
|
||||
"notify.orderFinished" => _('Get message with order result'),
|
||||
"notify.negativeBalance" => _('Get message if negative account balance'),
|
||||
"messages.sendAsEmail" => _('Get messages as emails'),
|
||||
"profile.phoneIsPublic" => _('Phone is visible for foodcoop members'),
|
||||
"profile.emailIsPublic" => _('Email is visible for foodcoop members'),
|
||||
"profile.nameIsPublic" => _('Name is visible for foodcoop members')
|
||||
}
|
||||
return settings_hash
|
||||
end
|
||||
# retuns the default setting for a NEW user
|
||||
# for old records nil will returned
|
||||
# TODO: integrate default behaviour in acts_as_configurable plugin
|
||||
def settings_default(setting)
|
||||
# define a default for the settings
|
||||
defaults = {
|
||||
"messages.sendAsEmail" => true
|
||||
}
|
||||
return true if self.new_record? && defaults[setting]
|
||||
end
|
||||
|
||||
|
||||
# Sets the user's password. It will be stored encrypted along with a random salt.
|
||||
def password=(password)
|
||||
salt = [Array.new(6){rand(256).chr}.join].pack("m").chomp
|
||||
self.password_hash, self.password_salt = Digest::SHA1.hexdigest(password + salt), salt
|
||||
end
|
||||
|
||||
# Returns true if the password argument matches the user's password.
|
||||
def has_password(password)
|
||||
Digest::SHA1.hexdigest(password + self.password_salt) == self.password_hash
|
||||
end
|
||||
|
||||
#Sets the passwort, and if fails it returns error-messages (see above)
|
||||
def set_password(options = {:required => false}, password = nil, confirmation = nil)
|
||||
required = options[:required]
|
||||
if required && (password.nil? || password.empty?)
|
||||
self.errors.add_to_base _('Password is required')
|
||||
elsif !password.nil? && !password.empty?
|
||||
if password != confirmation
|
||||
self.errors.add_to_base _("Passwords doesn't match")
|
||||
elsif password.length < 5 || password.length > 25
|
||||
self.errors.add_to_base _('Password-length has to be between 5 and 25 characters')
|
||||
else
|
||||
self.password = password
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Returns a random password.
|
||||
def new_random_password(size = 3)
|
||||
c = %w(b c d f g h j k l m n p qu r s t v w x z ch cr fr nd ng nk nt ph pr rd sh sl sp st th tr)
|
||||
v = %w(a e i o u y)
|
||||
f, r = true, ''
|
||||
(size * 2).times do
|
||||
r << (f ? c[rand * c.size] : v[rand * v.size])
|
||||
f = !f
|
||||
end
|
||||
r
|
||||
end
|
||||
|
||||
# Checks the admin role
|
||||
def role_admin?
|
||||
groups.detect {|group| group.role_admin?}
|
||||
end
|
||||
|
||||
# Checks the finance role
|
||||
def role_finance?
|
||||
groups.detect {|group| group.role_finance?}
|
||||
end
|
||||
|
||||
# Checks the article_meta role
|
||||
def role_article_meta?
|
||||
groups.detect {|group| group.role_article_meta?}
|
||||
end
|
||||
|
||||
# Checks the suppliers role
|
||||
def role_suppliers?
|
||||
groups.detect {|group| group.role_suppliers?}
|
||||
end
|
||||
|
||||
# Checks the orders role
|
||||
def role_orders?
|
||||
groups.detect {|group| group.role_orders?}
|
||||
end
|
||||
|
||||
# Returns the user's OrderGroup or nil if none found.
|
||||
def find_ordergroup
|
||||
groups.find(:first, :conditions => "type = 'OrderGroup'")
|
||||
end
|
||||
|
||||
# Find all tasks, for which the current user should be responsible
|
||||
# but which aren't accepted yet
|
||||
def unaccepted_tasks
|
||||
# this doesn't work. Produces "undefined method", when later use task.users... Rails Bug?
|
||||
# self.tasks.find :all, :conditions => ["accepted = ?", false], :order => "due_date DESC"
|
||||
Task.find_by_sql ["SELECT t.* FROM tasks t, assignments a, users u
|
||||
WHERE u.id = a.user_id
|
||||
AND t.id = a.task_id
|
||||
AND u.id = ?
|
||||
AND a.accepted = ?
|
||||
AND t.done = ?
|
||||
ORDER BY t.due_date ASC", self.id, false, false]
|
||||
end
|
||||
|
||||
# Find all accepted tasks, which aren't done
|
||||
def accepted_tasks
|
||||
Task.find_by_sql ["SELECT t.* FROM tasks t, assignments a, users u
|
||||
WHERE u.id = a.user_id
|
||||
AND t.id = a.task_id
|
||||
AND u.id = ?
|
||||
AND a.accepted = ?
|
||||
AND t.done = ?
|
||||
ORDER BY t.due_date ASC", self.id, true, false]
|
||||
end
|
||||
|
||||
# find all tasks in the next week (or another number of days)
|
||||
def next_tasks(number = 7)
|
||||
Task.find_by_sql ["SELECT t.* FROM tasks t, assignments a, users u
|
||||
WHERE u.id = a.user_id
|
||||
AND t.id = a.task_id
|
||||
AND u.id = ?
|
||||
AND t.due_date >= ?
|
||||
AND t.due_date <= ?
|
||||
AND t.done = ?
|
||||
AND a.accepted = ?
|
||||
ORDER BY t.due_date ASC", self.id, Time.now, number.days.from_now, false, true]
|
||||
end
|
||||
|
||||
# returns true if user is a member of a given group
|
||||
def is_member_of(group)
|
||||
return true if group.users.detect {|user| user == self}
|
||||
end
|
||||
|
||||
#Returns an array with the users groups (but without the OrderGroups -> because tpye=>"")
|
||||
def member_of_groups()
|
||||
self.groups.find(:all, :conditions => {:type => ""})
|
||||
end
|
||||
|
||||
end
|
||||
Loading…
Add table
Add a link
Reference in a new issue