Complete refactoring of orders-workflow.
OrderResult tables are removed. Data consistency is now possible through new article.price-history (ArticlePrice). Balancing-workflow needs to be updated.
This commit is contained in:
parent
80287aeea4
commit
9eb2125f15
98 changed files with 1121 additions and 1717 deletions
|
|
@ -1,5 +1,5 @@
|
|||
# == Schema Information
|
||||
# Schema version: 20090119155930
|
||||
# Schema version: 20090120184410
|
||||
#
|
||||
# Table name: articles
|
||||
#
|
||||
|
|
@ -13,8 +13,7 @@
|
|||
# manufacturer :string(255)
|
||||
# origin :string(255)
|
||||
# shared_updated_on :datetime
|
||||
# net_price :decimal(8, 2)
|
||||
# gross_price :decimal(8, 2) default(0.0), not null
|
||||
# price :decimal(8, 2)
|
||||
# tax :float
|
||||
# deposit :decimal(8, 2) default(0.0)
|
||||
# unit_quantity :integer(4) default(1), not null
|
||||
|
|
@ -26,25 +25,30 @@
|
|||
#
|
||||
|
||||
class Article < ActiveRecord::Base
|
||||
acts_as_paranoid # Avoid deleting the article for consistency of order-results
|
||||
acts_as_paranoid # Avoid deleting the article for consistency of order-results
|
||||
extend ActiveSupport::Memoizable # Ability to cache method results. Use memoize :expensive_method
|
||||
|
||||
# Associations
|
||||
belongs_to :supplier
|
||||
belongs_to :article_category
|
||||
has_many :article_prices, :order => "created_at"
|
||||
|
||||
named_scope :in_stock, :conditions => "quantity > 0", :order => 'suppliers.name', :include => :supplier
|
||||
|
||||
validates_presence_of :name, :unit, :net_price, :tax, :deposit, :unit_quantity, :supplier_id
|
||||
|
||||
# Validations
|
||||
validates_presence_of :name, :unit, :price, :tax, :deposit, :unit_quantity, :supplier_id, :article_category_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 :price, :greater_than => 0
|
||||
validates_numericality_of :deposit, :tax
|
||||
|
||||
# calculate the gross_price
|
||||
before_save :calc_gross_price
|
||||
|
||||
# Callbacks
|
||||
before_save :update_price_history
|
||||
before_destroy :check_article_in_use
|
||||
|
||||
# Custom attribute setter that accepts decimal numbers using localized decimal separator.
|
||||
def net_price=(net_price)
|
||||
self[:net_price] = String.delocalized_decimal(net_price)
|
||||
def price=(price)
|
||||
self[:price] = String.delocalized_decimal(price)
|
||||
end
|
||||
|
||||
# Custom attribute setter that accepts decimal numbers using localized decimal separator.
|
||||
|
|
@ -57,9 +61,14 @@ class Article < ActiveRecord::Base
|
|||
self[:deposit] = String.delocalized_decimal(deposit)
|
||||
end
|
||||
|
||||
# calculate the fc price and sets the attribute
|
||||
def calc_gross_price
|
||||
self.gross_price = ((net_price + deposit) * (tax / 100 + 1)) * (APP_CONFIG[:price_markup] / 100 + 1)
|
||||
# The financial gross, net plus tax and deposti
|
||||
def gross_price
|
||||
((price + deposit) * (tax / 100 + 1)).round(2)
|
||||
end
|
||||
|
||||
# The price for the foodcoop-member.
|
||||
def fc_price
|
||||
(gross_price * (APP_CONFIG[:price_markup] / 100 + 1)).round(2)
|
||||
end
|
||||
|
||||
# Returns true if article has been updated at least 2 days ago
|
||||
|
|
@ -75,7 +84,7 @@ class Article < ActiveRecord::Base
|
|||
#
|
||||
# Example:
|
||||
#
|
||||
# unit_quantity | quantity | tolerance | calculateOrderQuantity
|
||||
# unit_quantity | quantity | tolerance | calculate_order_quantity
|
||||
# --------------+----------+-----------+-----------------------
|
||||
# 4 | 0 | 2 | 0
|
||||
# 4 | 0 | 5 | 0
|
||||
|
|
@ -85,28 +94,20 @@ class Article < ActiveRecord::Base
|
|||
# 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)
|
||||
def calculate_order_quantity(quantity, tolerance = 0)
|
||||
unit_size = unit_quantity
|
||||
units = quantity / unit_size
|
||||
remainder = quantity % unit_size
|
||||
units += ((remainder > 0) && (remainder + tolerance >= unit_size) ? 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
|
||||
# If the article is used in an open Order, the Order will be returned.
|
||||
def in_open_order
|
||||
order_articles = OrderArticle.all(:conditions => ['order_id IN (?)', Order.open.collect {|o| o.id }])
|
||||
order_article = order_articles.detect {|oa| oa.article_id == id }
|
||||
order_article ? order_article.order : nil
|
||||
end
|
||||
memoize :in_open_order
|
||||
|
||||
# this method checks, if the shared_article has been changed
|
||||
# unequal attributes will returned in array
|
||||
|
|
@ -132,7 +133,7 @@ class Article < ActiveRecord::Base
|
|||
: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],
|
||||
:price => [self.price, new_price],
|
||||
:tax => [self.tax, self.shared_article.tax],
|
||||
:deposit => [self.deposit, self.shared_article.deposit],
|
||||
# take care of different num-objects.
|
||||
|
|
@ -217,4 +218,26 @@ class Article < ActiveRecord::Base
|
|||
update_attribute :quantity, quantity + amount
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# Checks if the article is in use before it will deleted
|
||||
def check_article_in_use
|
||||
raise self.name.to_s + _(" cannot be deleted. The article is used in a current order!") if self.in_open_order
|
||||
end
|
||||
|
||||
# Create an ArticlePrice, when the price-attr are changed.
|
||||
def update_price_history
|
||||
if price_changed?
|
||||
article_prices.create(
|
||||
:price => price,
|
||||
:tax => tax,
|
||||
:deposit => deposit,
|
||||
:unit_quantity => unit_quantity
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def price_changed?
|
||||
changed.detect { |attr| attr == 'price' || 'tax' || 'deposit' || 'unit_quantity' } ? true : false
|
||||
end
|
||||
end
|
||||
|
|
|
|||
29
app/models/article_price.rb
Normal file
29
app/models/article_price.rb
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# == Schema Information
|
||||
# Schema version: 20090120184410
|
||||
#
|
||||
# Table name: article_prices
|
||||
#
|
||||
# id :integer(4) not null, primary key
|
||||
# article_id :integer(4)
|
||||
# price :decimal(8, 2) default(0.0), not null
|
||||
# tax :decimal(8, 2) default(0.0), not null
|
||||
# deposit :decimal(8, 2) default(0.0), not null
|
||||
# unit_quantity :integer(4)
|
||||
# created_at :datetime
|
||||
#
|
||||
|
||||
class ArticlePrice < ActiveRecord::Base
|
||||
|
||||
belongs_to :article
|
||||
has_many :order_articles
|
||||
|
||||
# The financial gross, net plus tax and deposit.
|
||||
def gross_price
|
||||
((price + deposit) * (tax / 100 + 1))
|
||||
end
|
||||
|
||||
# The price for the foodcoop-member.
|
||||
def fc_price
|
||||
(gross_price * (APP_CONFIG[:price_markup] / 100 + 1))
|
||||
end
|
||||
end
|
||||
|
|
@ -19,7 +19,6 @@ class GroupOrder < ActiveRecord::Base
|
|||
belongs_to :ordergroup
|
||||
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
|
||||
|
|
@ -28,14 +27,17 @@ class GroupOrder < ActiveRecord::Base
|
|||
validates_numericality_of :price
|
||||
validates_uniqueness_of :ordergroup_id, :scope => :order_id # order groups can only order once per order
|
||||
|
||||
named_scope :open, lambda { {:conditions => ["order_id IN (?)", Order.open.collect{|o| o.id}]} }
|
||||
named_scope :finished, lambda { {:conditions => ["order_id IN (?)", Order.finished.collect{|o| o.id}]} }
|
||||
|
||||
# Updates the "price" attribute.
|
||||
# This will be the maximum value of a current order
|
||||
def updatePrice
|
||||
def update_price!
|
||||
total = 0
|
||||
for article in group_order_articles.find(:all, :include => :order_article)
|
||||
total += article.order_article.article.gross_price * (article.quantity + article.tolerance)
|
||||
total += article.order_article.article.fc_price * (article.quantity + article.tolerance)
|
||||
end
|
||||
self.price = total
|
||||
update_attribute(:price, total)
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
|||
|
|
@ -30,8 +30,8 @@ class GroupOrderArticle < ActiveRecord::Base
|
|||
# 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})")
|
||||
def update_quantities(quantity, tolerance)
|
||||
logger.debug("GroupOrderArticle[#{id}].update_quantities(#{quantity}, #{tolerance})")
|
||||
logger.debug("Current quantity = #{self.quantity}, tolerance = #{self.tolerance}")
|
||||
|
||||
# Get quantities ordered with the newest item first.
|
||||
|
|
|
|||
|
|
@ -1,34 +0,0 @@
|
|||
# == Schema Information
|
||||
# Schema version: 20090102171850
|
||||
#
|
||||
# Table name: group_order_article_results
|
||||
#
|
||||
# id :integer(4) not null, primary key
|
||||
# order_article_result_id :integer(4) default(0), not null
|
||||
# group_order_result_id :integer(4) default(0), not null
|
||||
# quantity :decimal(6, 3) default(0.0)
|
||||
# tolerance :integer(4)
|
||||
#
|
||||
|
||||
# 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.
|
||||
#
|
||||
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] = String.delocalized_decimal(quantity)
|
||||
end
|
||||
|
||||
end
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
# == Schema Information
|
||||
# Schema version: 20090102171850
|
||||
#
|
||||
# Table name: group_order_results
|
||||
#
|
||||
# id :integer(4) not null, primary key
|
||||
# order_id :integer(4) default(0), not null
|
||||
# group_name :string(255) default(""), not null
|
||||
# price :decimal(8, 2) default(0.0), not null
|
||||
#
|
||||
|
||||
# Ordergroups, which participate on a specific order will have a line
|
||||
class GroupOrderResult < ActiveRecord::Base
|
||||
|
||||
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
|
||||
|
|
@ -1,24 +1,28 @@
|
|||
# == Schema Information
|
||||
# Schema version: 20090113111624
|
||||
# Schema version: 20090120184410
|
||||
#
|
||||
# Table name: invoices
|
||||
#
|
||||
# id :integer(4) not null, primary key
|
||||
# supplier_id :integer(4)
|
||||
# delivery_id :integer(4)
|
||||
# number :string(255)
|
||||
# date :date
|
||||
# paid_on :date
|
||||
# note :text
|
||||
# amount :decimal(8, 2) default(0.0), not null
|
||||
# created_at :datetime
|
||||
# updated_at :datetime
|
||||
# id :integer(4) not null, primary key
|
||||
# supplier_id :integer(4)
|
||||
# delivery_id :integer(4)
|
||||
# number :string(255)
|
||||
# date :date
|
||||
# paid_on :date
|
||||
# note :text
|
||||
# amount :decimal(8, 2) default(0.0), not null
|
||||
# created_at :datetime
|
||||
# updated_at :datetime
|
||||
# order_id :integer(4)
|
||||
# deposit :decimal(8, 2) default(0.0), not null
|
||||
# deposit_credit :decimal(8, 2) default(0.0), not null
|
||||
#
|
||||
|
||||
class Invoice < ActiveRecord::Base
|
||||
|
||||
belongs_to :supplier
|
||||
belongs_to :delivery
|
||||
belongs_to :order
|
||||
|
||||
validates_presence_of :supplier_id
|
||||
validates_uniqueness_of :date, :scope => [:supplier_id]
|
||||
|
|
@ -29,4 +33,14 @@ class Invoice < ActiveRecord::Base
|
|||
def amount=(amount)
|
||||
self[:amount] = String.delocalized_decimal(amount)
|
||||
end
|
||||
|
||||
# Custom attribute setter that accepts decimal numbers using localized decimal separator.
|
||||
def deposit=(deposit)
|
||||
self[:deposit] = String.delocalized_decimal(deposit)
|
||||
end
|
||||
|
||||
# Custom attribute setter that accepts decimal numbers using localized decimal separator.
|
||||
def deposit_credit=(deposit)
|
||||
self[:deposit_credit] = String.delocalized_decimal(deposit)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,69 +1,47 @@
|
|||
# == Schema Information
|
||||
# Schema version: 20090102171850
|
||||
# Schema version: 20090120184410
|
||||
#
|
||||
# Table name: orders
|
||||
#
|
||||
# id :integer(4) not null, primary key
|
||||
# name :string(255) default(""), not null
|
||||
# supplier_id :integer(4) default(0), not null
|
||||
# starts :datetime not null
|
||||
# supplier_id :integer(4)
|
||||
# note :text
|
||||
# starts :datetime
|
||||
# ends :datetime
|
||||
# note :string(255)
|
||||
# finished :boolean(1) not null
|
||||
# booked :boolean(1) not null
|
||||
# state :string(255) default("open")
|
||||
# lock_version :integer(4) default(0), not null
|
||||
# updated_by_user_id :integer(4)
|
||||
# invoice_amount :decimal(8, 2) default(0.0), not null
|
||||
# deposit :decimal(8, 2) default(0.0)
|
||||
# deposit_credit :decimal(8, 2) default(0.0)
|
||||
# invoice_number :string(255)
|
||||
# invoice_date :string(255)
|
||||
#
|
||||
|
||||
class Order < ActiveRecord::Base
|
||||
extend ActiveSupport::Memoizable # Ability to cache method results. Use memoize :expensive_method
|
||||
acts_as_ordered :order => "ends" # easyier find of next or previous model
|
||||
|
||||
# Associations
|
||||
has_many :order_articles, :dependent => :destroy
|
||||
has_many :articles, :through => :order_articles
|
||||
has_many :group_orders, :dependent => :destroy
|
||||
has_many :ordergroups, :through => :group_orders
|
||||
has_many :order_article_results, :dependent => :destroy
|
||||
has_many :group_order_results, :dependent => :destroy
|
||||
has_one :invoice
|
||||
has_many :comments, :class_name => "OrderComment", :order => "created_at"
|
||||
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
|
||||
# Validations
|
||||
validates_presence_of :supplier_id, :starts
|
||||
validate_on_create :starts_before_ends, :include_articles
|
||||
|
||||
# easyier find of next or previous model
|
||||
acts_as_ordered :order => "ends"
|
||||
# Callbacks
|
||||
after_update :update_price_of_group_orders
|
||||
|
||||
# Finders
|
||||
named_scope :finished, :conditions => { :state => 'finished' },
|
||||
:order => 'ends DESC'
|
||||
named_scope :open, :conditions => { :state => 'open' },
|
||||
:order => 'ends DESC'
|
||||
named_scope :closed, :conditions => { :state => 'closed' },
|
||||
:order => 'ends DESC'
|
||||
|
||||
named_scope :finished, :conditions => { :finished => true },
|
||||
:order => 'ends DESC', :include => :supplier
|
||||
|
||||
# Custom attribute setter that accepts decimal numbers using localized decimal separator.
|
||||
def invoice_amount=(amount)
|
||||
self[:invoice_amount] = String.delocalized_decimal(amount)
|
||||
end
|
||||
|
||||
# Custom attribute setter that accepts decimal numbers using localized decimal separator.
|
||||
def deposit=(deposit)
|
||||
self[:deposit] = String.delocalized_decimal(deposit)
|
||||
end
|
||||
|
||||
# Custom attribute setter that accepts decimal numbers using localized decimal separator.
|
||||
def deposit_credit=(deposit)
|
||||
self[:deposit_credit] = String.delocalized_decimal(deposit)
|
||||
end
|
||||
|
||||
# Create or destroy OrderArticle associations on create/update
|
||||
def article_ids=(ids)
|
||||
# fetch selected articles
|
||||
|
|
@ -75,202 +53,123 @@ class Order < ActiveRecord::Base
|
|||
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)
|
||||
|
||||
def open?
|
||||
state == "open"
|
||||
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)
|
||||
|
||||
def finished?
|
||||
state == "finished"
|
||||
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
|
||||
|
||||
def closed?
|
||||
state == "closed"
|
||||
end
|
||||
|
||||
# search GroupOrder of given Ordergroup
|
||||
def group_order(ordergroup)
|
||||
unless finished
|
||||
return group_orders.detect {|o| o.ordergroup_id == ordergroup.id}
|
||||
else
|
||||
return group_order_results.detect {|o| o.group_name == ordergroup.name}
|
||||
end
|
||||
group_orders.first :conditions => { :ordergroup_id => ordergroup.id }
|
||||
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}
|
||||
order_articles.all(:include => [:article, :article_price], :order => 'articles.name').group_by { |a|
|
||||
a.article.article_category.name
|
||||
}.sort { |a, b| a[0] <=> b[0] }
|
||||
end
|
||||
memoize :get_articles
|
||||
|
||||
# Returns the defecit/benefit for the foodcoop
|
||||
def fcProfit(with_markup = true)
|
||||
groups_sum = with_markup ? sumPrice("groups") : sumPrice("groups_without_markup")
|
||||
def profit(with_markup = true)
|
||||
groups_sum = with_markup ? sum(:groups) : sum(: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
|
||||
# :groups returns the sum of all GroupOrders
|
||||
# :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...
|
||||
def sum(type = :gross)
|
||||
total = 0
|
||||
if type == :clear || type == :gross || type == :fc
|
||||
for oa in order_articles.ordered.all(:include => [:article,:article_price])
|
||||
quantity = oa.units_to_order * oa.price.unit_quantity
|
||||
case type
|
||||
when :clear
|
||||
total += quantity * oa.price.price
|
||||
when :gross
|
||||
total += quantity * oa.price.gross_price
|
||||
when :fc
|
||||
total += quantity * oa.price.fc_price
|
||||
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
|
||||
end
|
||||
elsif type == :groups || type == :groups_without_markup
|
||||
for go in group_orders
|
||||
for goa in go.group_order_articles
|
||||
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
|
||||
when :groups
|
||||
total += goa.quantity * goa.order_article.price.fc_price
|
||||
when :groups_without_markup
|
||||
total += goa.quantity * goa.order_article.price.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
|
||||
total
|
||||
end
|
||||
|
||||
# Finishes this order. This will set the finish property to "true" and the end property to the current time.
|
||||
# Finishes this order. This will set the order state to "finish" 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.ordergroup.name,
|
||||
:price => go.price)
|
||||
def finish!(user)
|
||||
unless finished?
|
||||
Order.transaction do
|
||||
# Update order_articles. Save the current article_price to keep price consistency
|
||||
order_articles.all(:include => :article).each do |oa|
|
||||
oa.update_attribute(:article_price, oa.article.article_prices.first)
|
||||
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 => APP_CONFIG[:price_markup],
|
||||
: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.ordergroup.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
|
||||
# set new order state (needed by notify_order_finished)
|
||||
update_attributes(:state => 'finished', :ends => Time.now, :updated_by => user)
|
||||
|
||||
# TODO: delete data, which is no longer required ...
|
||||
# group_order_article_quantities... order_articles with units_to_order == 0 ? ...
|
||||
end
|
||||
|
||||
# notify order groups
|
||||
notify_order_finished
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# TODO: I can't understand, why its going out from the group_order_articles perspective.
|
||||
# Why we can't just iterate through the order_articles?
|
||||
#
|
||||
# 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
|
||||
# This method is fired after an ordergroup has saved/updated his order.
|
||||
def update_quantities
|
||||
indexed_order_articles = {} # 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)
|
||||
group_order_articles = GroupOrderArticle.all(:conditions => ['group_order_id IN (?)', group_order_ids],
|
||||
:include => [:order_article])
|
||||
for goa in group_order_articles
|
||||
if (order_article = indexed_order_articles[goa.order_article.id.to_s])
|
||||
# order_article has already been fetched, just update...
|
||||
order_article.quantity = order_article.quantity + goa.quantity
|
||||
order_article.tolerance = order_article.tolerance + goa.tolerance
|
||||
order_article.units_to_order = order_article.article.calculate_order_quantity(order_article.quantity, order_article.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
|
||||
order_article = goa.order_article
|
||||
order_article.quantity = goa.quantity
|
||||
order_article.tolerance = goa.tolerance
|
||||
order_article.units_to_order = order_article.article.calculate_order_quantity(order_article.quantity, order_article.tolerance)
|
||||
indexed_order_articles[order_article.id.to_s] = order_article
|
||||
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
|
||||
indexed_order_articles.each_value { | value | value.save! }
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -290,49 +189,40 @@ class Order < ActiveRecord::Base
|
|||
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
|
||||
def starts_before_ends
|
||||
errors.add(:ends, "muss nach dem Bestellstart liegen (oder leer bleiben)") if (ends && starts && ends <= starts)
|
||||
end
|
||||
|
||||
def include_articles
|
||||
errors.add(:order_articles, "Es muss mindestens ein Artikel ausgewählt sein") 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
|
||||
ordergroup = Ordergroup.find_by_name(group_order.group_name)
|
||||
# Determine group users that want a notification message:
|
||||
users = ordergroup.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:
|
||||
Message.from_template(
|
||||
'order_finished',
|
||||
{:group => ordergroup, :order => self, :results => results, :total => group_order.price},
|
||||
{:recipients_ids => users.collect(&:id), :subject => "Bestellung beendet: #{self.name}"}
|
||||
).save!
|
||||
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 update_price_of_group_orders
|
||||
group_orders.each { |group_order| group_order.update_price! }
|
||||
end
|
||||
|
||||
# Sends "order finished" messages to users who have participated in this order.
|
||||
def notify_order_finished
|
||||
for group_order in self.group_orders
|
||||
ordergroup = group_order.ordergroup
|
||||
logger.debug("Send 'order finished' message to #{ordergroup.name}")
|
||||
# Determine users that want a notification message:
|
||||
users = ordergroup.users.reject{|u| u.settings["notify.orderFinished"] != '1'}
|
||||
unless users.empty?
|
||||
# Create user notification messages:
|
||||
Message.from_template(
|
||||
'order_finished',
|
||||
{:group => ordergroup, :order => self, :group_order => group_order},
|
||||
{:recipients_ids => users.collect(&:id), :subject => "Bestellung beendet: #{supplier.name}"}
|
||||
).save!
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,15 +1,16 @@
|
|||
# == Schema Information
|
||||
# Schema version: 20090102171850
|
||||
# Schema version: 20090120184410
|
||||
#
|
||||
# Table name: order_articles
|
||||
#
|
||||
# id :integer(4) not null, primary key
|
||||
# order_id :integer(4) default(0), not null
|
||||
# article_id :integer(4) default(0), not null
|
||||
# quantity :integer(4) default(0), not null
|
||||
# tolerance :integer(4) default(0), not null
|
||||
# units_to_order :integer(4) default(0), not null
|
||||
# lock_version :integer(4) default(0), not null
|
||||
# id :integer(4) not null, primary key
|
||||
# order_id :integer(4) default(0), not null
|
||||
# article_id :integer(4) default(0), not null
|
||||
# quantity :integer(4) default(0), not null
|
||||
# tolerance :integer(4) default(0), not null
|
||||
# units_to_order :integer(4) default(0), not null
|
||||
# lock_version :integer(4) default(0), not null
|
||||
# article_price_id :integer(4)
|
||||
#
|
||||
|
||||
# An OrderArticle represents a single Article that is part of an Order.
|
||||
|
|
@ -17,16 +18,25 @@ class OrderArticle < ActiveRecord::Base
|
|||
|
||||
belongs_to :order
|
||||
belongs_to :article
|
||||
belongs_to :article_price
|
||||
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
|
||||
|
||||
named_scope :ordered, :conditions => "units_to_order >= 1"
|
||||
|
||||
# This method returns either the Article or the ArticlePrice
|
||||
# The latter will be set, when the the order is finished
|
||||
def price
|
||||
article_price || article
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def validate
|
||||
errors.add(:article, "muss angegeben sein und einen aktuellen Preis haben") if !(article = Article.find(article_id)) || article.gross_price.nil?
|
||||
errors.add(:article, "muss angegeben sein und einen aktuellen Preis haben") if !(article = Article.find(article_id)) || article.fc_price.nil?
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,78 +0,0 @@
|
|||
# == Schema Information
|
||||
# Schema version: 20090102171850
|
||||
#
|
||||
# Table name: order_article_results
|
||||
#
|
||||
# id :integer(4) not null, primary key
|
||||
# order_id :integer(4) default(0), not null
|
||||
# name :string(255) default(""), not null
|
||||
# unit :string(255) default(""), not null
|
||||
# note :string(255)
|
||||
# net_price :decimal(8, 2) default(0.0)
|
||||
# gross_price :decimal(8, 2) default(0.0), not null
|
||||
# tax :float default(0.0), not null
|
||||
# deposit :decimal(8, 2) default(0.0)
|
||||
# fc_markup :float default(0.0), not null
|
||||
# order_number :string(255)
|
||||
# unit_quantity :integer(4) default(0), not null
|
||||
# units_to_order :decimal(6, 3) default(0.0), not null
|
||||
#
|
||||
|
||||
# An OrderArticleResult represents a single Article that is part of a *finished* Order.
|
||||
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] = String.delocalized_decimal(net_price)
|
||||
end
|
||||
|
||||
# Custom attribute setter that accepts decimal numbers using localized decimal separator.
|
||||
def tax=(tax)
|
||||
self[:tax] = String.delocalized_decimal(tax)
|
||||
end
|
||||
|
||||
# Custom attribute setter that accepts decimal numbers using localized decimal separator.
|
||||
def deposit=(deposit)
|
||||
self[:deposit] = String.delocalized_decimal(deposit)
|
||||
end
|
||||
|
||||
# Custom attribute setter that accepts decimal numbers using localized decimal separator.
|
||||
def units_to_order=(units_to_order)
|
||||
self[:units_to_order] = String.delocalized_decimal(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
|
||||
19
app/models/order_comment.rb
Normal file
19
app/models/order_comment.rb
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
# == Schema Information
|
||||
# Schema version: 20090120184410
|
||||
#
|
||||
# Table name: order_comments
|
||||
#
|
||||
# id :integer(4) not null, primary key
|
||||
# order_id :integer(4)
|
||||
# user_id :integer(4)
|
||||
# text :text
|
||||
# created_at :datetime
|
||||
#
|
||||
|
||||
class OrderComment < ActiveRecord::Base
|
||||
|
||||
belongs_to :order
|
||||
belongs_to :user
|
||||
|
||||
validates_presence_of :order_id, :user_id, :text
|
||||
end
|
||||
|
|
@ -30,46 +30,34 @@
|
|||
# * 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
|
||||
acts_as_paranoid # Avoid deleting the ordergroup for consistency of order-results
|
||||
extend ActiveSupport::Memoizable # Ability to cache method results. Use memoize :expensive_method
|
||||
|
||||
has_many :financial_transactions
|
||||
has_many :group_orders
|
||||
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 ordergroup.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_ordergroup_id(self.id)
|
||||
unless order == excludeGroupOrder
|
||||
funds -= order.price
|
||||
end
|
||||
end
|
||||
for order_result in self.findFinishedNotBooked
|
||||
funds -= order_result.price
|
||||
end
|
||||
return funds
|
||||
|
||||
def value_of_open_orders(exclude = nil)
|
||||
group_orders.open.reject{|go| go == exclude}.collect(&:price).sum
|
||||
end
|
||||
|
||||
def value_of_finished_orders(exclude = nil)
|
||||
group_orders.finished.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
|
||||
memoize :get_available_funds
|
||||
|
||||
# Creates a new FinancialTransaction for this Ordergroup and updates the account_balance accordingly.
|
||||
# Throws an exception if it fails.
|
||||
def addFinancialTransaction(amount, note, user)
|
||||
|
|
@ -129,13 +117,4 @@ class Ordergroup < Group
|
|||
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
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ class Supplier < ActiveRecord::Base
|
|||
# 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}
|
||||
articles.select {|article| article.fc_price}
|
||||
end
|
||||
|
||||
# sync all articles with the external database
|
||||
|
|
@ -66,10 +66,10 @@ class Supplier < ActiveRecord::Base
|
|||
# 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.price = new_price
|
||||
article.unit_quantity = new_unit_quantity
|
||||
else
|
||||
article.net_price = shared_article.price
|
||||
article.price = shared_article.price
|
||||
article.unit_quantity = shared_article.unit_quantity
|
||||
article.unit = shared_article.unit
|
||||
end
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue