diff --git a/app/controllers/application.rb b/app/controllers/application.rb index 1521e840..6f3c7fe2 100644 --- a/app/controllers/application.rb +++ b/app/controllers/application.rb @@ -53,7 +53,7 @@ class ApplicationController < ActionController::Base def deny_access self.return_to = request.request_uri - redirect_to :controller => 'login', :action => 'denied' + redirect_to :controller => '/login', :action => 'denied' return false end diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index ee24faa6..0ee58259 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -4,9 +4,6 @@ class HomeController < ApplicationController def index @currentOrders = Order.open @ordergroup = @current_user.ordergroup - if @ordergroup - @financial_transactions = @ordergroup.financial_transactions.find(:all, :order => 'created_on desc', :limit => 3) - end # unaccepted tasks @unaccepted_tasks = @current_user.unaccepted_tasks # task in next week diff --git a/app/controllers/ordering_controller.rb b/app/controllers/ordering_controller.rb index f3469888..fe363a05 100644 --- a/app/controllers/ordering_controller.rb +++ b/app/controllers/ordering_controller.rb @@ -5,7 +5,7 @@ class OrderingController < ApplicationController before_filter :ensure_ordergroup_member before_filter :ensure_open_order, :only => [:order, :saveOrder] - verify :method => :post, :only => [:saveOrder], :redirect_to => { :action => :index } + verify :method => :post, :only => [:saveOrder], :redirect_to => {:action => :index} # Index page. def index @@ -23,8 +23,8 @@ class OrderingController < ApplicationController if @group_order # Group has already ordered, so get the results... for article in @group_order.group_order_articles - result = article.orderResult - ordered_articles[article.order_article_id] = { 'quantity' => article.quantity, + result = article.result + ordered_articles[article.order_article_id] = {'quantity' => article.quantity, 'tolerance' => article.tolerance, 'quantity_result' => result[:quantity], 'tolerance_result' => result[:tolerance]} @@ -61,53 +61,53 @@ class OrderingController < ApplicationController # Update changes to a current order. def saveOrder - order = @order # Get the object through before_filter - if (params[:total_balance].to_i < 0) + if (params[:total_balance].to_i < 0) #TODO: Better use a real test on sufficiant funds flash[:error] = 'Der Bestellwert übersteigt das verfügbare Guthaben.' redirect_to :action => 'order' elsif (ordered = params[:ordered]) - begin - Order.transaction do - # Create group order if necessary... - if (groupOrder = order.group_orders.find(:first, :conditions => "ordergroup_id = #{@ordergroup.id}", :include => [:group_order_articles])) - if (params[:version].to_i != groupOrder.lock_version) # check for conflicts well ahead - raise ActiveRecord::StaleObjectError - end - else - groupOrder = GroupOrder.new(:ordergroup => @ordergroup, :order => order, :updated_by => @current_user, :price => 0) - groupOrder.save! - end - # Create/update GroupOrderArticles... - newGroupOrderArticles = Array.new - for article in order.order_articles - # Find the GroupOrderArticle, create a new one if necessary... - groupOrderArticles = groupOrder.group_order_articles.select{ |v| v.order_article_id == article.id } - unless (groupOrderArticle = groupOrderArticles[0]) - groupOrderArticle = GroupOrderArticle.create(:group_order => groupOrder, :order_article_id => article.id, :quantity => 0, :tolerance => 0) - end - # Get ordered quantities and update GroupOrderArticle/-Quantities... - unless (quantities = ordered.delete(article.id.to_s)) && (quantity = quantities['quantity']) && (tolerance = quantities['tolerance']) - quantity = tolerance = 0 - end - groupOrderArticle.update_quantities(quantity.to_i, tolerance.to_i) - # Add to new list of GroupOrderArticles: - newGroupOrderArticles.push(groupOrderArticle) - end - groupOrder.group_order_articles = newGroupOrderArticles - groupOrder.update_price! - groupOrder.updated_by = @current_user - groupOrder.save! - order.update_quantities - order.save! - end - flash[:notice] = 'Die Bestellung wurde gespeichert.' - rescue ActiveRecord::StaleObjectError - flash[:error] = 'In der Zwischenzeit hat jemand anderes auch bestellt, daher konnte die Bestellung nicht aktualisiert werden.' - rescue => exception - logger.error('Failed to update order: ' + exception.message) - flash[:error] = 'Die Bestellung konnte nicht aktualisiert werden, da ein Fehler auftrat.' - end - redirect_to :action => 'my_order_result', :id => order + begin + Order.transaction do + # Try to find group_order + group_order = @order.group_orders.first :conditions => "ordergroup_id = #{@ordergroup.id}", + :include => [:group_order_articles] + # Create group order if necessary... + unless group_order.nil? + # check for conflicts well ahead + if (params[:version].to_i != group_order.lock_version) + raise ActiveRecord::StaleObjectError + end + else + group_order = @ordergroup.group_orders.create!(:order => @order, :updated_by => @current_user, :price => 0) + end + + # Create/update group_order_articles... + for order_article in @order.order_articles + + # Find the group_order_article, create a new one if necessary... + group_order_article = group_order.group_order_articles.detect { |v| v.order_article_id == order_article.id } + if group_order_article.nil? + group_order_article = group_order.group_order_articles.create(:order_article_id => order_article.id) + end + + # Get ordered quantities and update group_order_articles/_quantities... + quantities = ordered.fetch(order_article.id.to_s, {'quantity' => 0, 'tolerance' => 0}) + group_order_article.update_quantities(quantities['quantity'].to_i, quantities['tolerance'].to_i) + + # Also update results for the order_article + order_article.update_results! + end + + group_order.update_price! + group_order.update_attribute(:updated_by, @current_user) + end + flash[:notice] = 'Die Bestellung wurde gespeichert.' + rescue ActiveRecord::StaleObjectError + flash[:error] = 'In der Zwischenzeit hat jemand anderes auch bestellt, daher konnte die Bestellung nicht aktualisiert werden.' + rescue => exception + logger.error('Failed to update order: ' + exception.message) + flash[:error] = 'Die Bestellung konnte nicht aktualisiert werden, da ein Fehler auftrat.' + end + redirect_to :action => 'my_order_result', :id => @order end end diff --git a/app/controllers/orders_controller.rb b/app/controllers/orders_controller.rb index 3b502527..f7c624a9 100644 --- a/app/controllers/orders_controller.rb +++ b/app/controllers/orders_controller.rb @@ -138,7 +138,7 @@ class OrdersController < ApplicationController text += "****** " + "Artikel" + "\n\n" text += "Nummer" + " " + "Menge" + " " + "Name" + "\n" # now display all ordered articles - order.order_articles.all(:include => [:article, :article_price]).each do |oa| + order.order_articles.ordered.all(:include => [:article, :article_price]).each do |oa| number = oa.article.order_number (8 - number.size).times { number += " " } quantity = oa.units_to_order.to_i.to_s diff --git a/app/models/article.rb b/app/models/article.rb index 98e8f49d..85326e7f 100644 --- a/app/models/article.rb +++ b/app/models/article.rb @@ -76,34 +76,9 @@ class Article < ActiveRecord::Base 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 | calculate_order_quantity - # --------------+----------+-----------+----------------------- - # 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 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 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_articles = OrderArticle.all(:conditions => ['order_id IN (?)', Order.open.collect(&:id)]) order_article = order_articles.detect {|oa| oa.article_id == id } order_article ? order_article.order : nil end diff --git a/app/models/group_order_article.rb b/app/models/group_order_article.rb index b5a39e63..22e9fcd7 100644 --- a/app/models/group_order_article.rb +++ b/app/models/group_order_article.rb @@ -1,5 +1,5 @@ # == Schema Information -# Schema version: 20090119155930 +# Schema version: 20090120184410 # # Table name: group_order_articles # @@ -9,13 +9,16 @@ # quantity :integer default(0), not null # tolerance :integer default(0), not null # updated_on :datetime not null +# quantity_result :integer +# tolerance_result :integer # # 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 # class GroupOrderArticle < ActiveRecord::Base - + extend ActiveSupport::Memoizable # Ability to cache method results. Use memoize :expensive_method + belongs_to :group_order belongs_to :order_article has_many :group_order_article_quantities, :dependent => :destroy @@ -27,6 +30,8 @@ class GroupOrderArticle < ActiveRecord::Base attr_accessor :ordergroup_id # To create an new GroupOrder if neccessary + named_scope :ordered, :conditions => 'quantity_result > 0 OR tolerance_result > 0' + # Updates the quantity/tolerance for this GroupOrderArticle by updating both GroupOrderArticle properties # and the associated GroupOrderArticleQuantities chronologically. # @@ -99,25 +104,27 @@ class GroupOrderArticle < ActiveRecord::Base # Returns a hash with three keys: :quantity / :tolerance / :total # # See description of the ordering algorithm in the general application documentation for details. - def orderResult + def calculate_result 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}") + total = order_article.units_to_order * order_article.price.unit_quantity + logger.debug("<#{order_article.article.name}>.unitsToOrder => items ordered: #{order_article.units_to_order} => #{total}") if (total > 0) + # In total there are enough units ordered. Now check the individual result for the ordergroup (group_order). + # # 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}") + order_quantities = GroupOrderArticleQuantity.all( + :conditions => ["group_order_article_id IN (?)", order_article.group_order_article_ids], :order => 'created_on') + logger.debug("GroupOrderArticleQuantity records found: #{order_quantities.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) + total_quantity = i = 0 + while (i < order_quantities.size && total_quantity < total) + q = (order_quantities[i].quantity <= total - total_quantity ? order_quantities[i].quantity : total - total_quantity) + total_quantity += q + if (order_quantities[i].group_order_article_id == self.id) logger.debug("increasing quantity by #{q}") quantity += q end @@ -125,13 +132,13 @@ class GroupOrderArticle < ActiveRecord::Base end # Determine tolerance to be ordered... - if (totalQuantity < total) + if (total_quantity < 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) + while (i < order_quantities.size && total_quantity < total) + q = (order_quantities[i].tolerance <= total - total_quantity ? order_quantities[i].tolerance : total - total_quantity) + total_quantity += q + if (order_quantities[i].group_order_article_id == self.id) logger.debug("increasing tolerance by #{q}") tolerance += q end @@ -139,13 +146,27 @@ class GroupOrderArticle < ActiveRecord::Base end end - # calculate the sum of quantity and tolerance: - sum = quantity + tolerance - - logger.debug("determined quantity/tolerance/total: #{quantity} / #{tolerance} / #{sum}") + logger.debug("determined quantity/tolerance/total: #{quantity} / #{tolerance} / #{quantity + tolerance}") end - {:quantity => quantity, :tolerance => tolerance, :total => sum} + {:quantity => quantity, :tolerance => tolerance, :total => quantity + tolerance} + end + memoize :calculate_result + + # Returns order result, + # either calcualted on the fly or fetched from quantity_/tolerance_result + def result + if quantity_result.nil? + calculate_result + else + {:quantity => quantity_result, :tolerance => tolerance_result, :total => quantity_result + tolerance_result} + end end + # This is used when finishing the order. + def save_results! + self.quantity_result = calculate_result[:quantity] + self.tolerance_result = calculate_result[:tolerance] + save! + end end diff --git a/app/models/order.rb b/app/models/order.rb index 9e72a3ee..ca84b9c8 100644 --- a/app/models/order.rb +++ b/app/models/order.rb @@ -100,11 +100,11 @@ class Order < ActiveRecord::Base # :fc, guess what... def sum(type = :gross) total = 0 - if type == :clear || type == :gross || type == :fc + if type == :net || 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 + when :net total += quantity * oa.price.price when :gross total += quantity * oa.price.gross_price @@ -133,51 +133,26 @@ class Order < ActiveRecord::Base unless finished? Order.transaction do # Update order_articles. Save the current article_price to keep price consistency + # Also save results for each group_order_result order_articles.all(:include => :article).each do |oa| oa.update_attribute(:article_price, oa.article.article_prices.first) + oa.group_order_articles.each { |goa| goa.save_results! } 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 ? ... + # Clean up + # Delete no longer required order-history (group_order_article_quantities) and + # TODO: Do we need articles, which aren't ordered? (units_to_order == 0 ?) + order_articles.each do |oa| + oa.group_order_articles.each { |goa| goa.group_order_article_quantities.clear } + end 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. - # 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... - 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... - 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 - indexed_order_articles.each_value { | value | value.save! } - end - end # Sets order.status to 'close' and updates all Ordergroup.account_balances def close!(user) diff --git a/app/models/order_article.rb b/app/models/order_article.rb index aaaffd56..8be0fc41 100644 --- a/app/models/order_article.rb +++ b/app/models/order_article.rb @@ -34,8 +34,8 @@ class OrderArticle < ActiveRecord::Base # # before_validation_on_create :create_new_article - # This method returns either the Article or the ArticlePrice - # The latter will be set, when the the order is finished + # This method returns either the ArticlePrice or the Article + # The first will be set, when the the order is finished def price article_price || article end @@ -47,6 +47,39 @@ class OrderArticle < ActiveRecord::Base {:quantity => quantity, :price => quantity * price.fc_price} end + # Update quantity/tolerance/units_to_order from group_order_articles + def update_results! + quantity = group_order_articles.collect(&:quantity).sum + tolerance = group_order_articles.collect(&:tolerance).sum + update_attributes(:quantity => quantity, :tolerance => tolerance, + :units_to_order => calculate_units_to_order(quantity, tolerance)) + end + + # Returns how many units of the belonging 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 | calculate_units_to_order + # --------------+----------+-----------+----------------------- + # 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 calculate_units_to_order(quantity, tolerance = 0) + unit_size = price.unit_quantity + units = quantity / unit_size + remainder = quantity % unit_size + units += ((remainder > 0) && (remainder + tolerance >= unit_size) ? 1 : 0) + end + private def article_and_price_exist diff --git a/app/models/ordergroup.rb b/app/models/ordergroup.rb index 56ad433f..618a6bbc 100644 --- a/app/models/ordergroup.rb +++ b/app/models/ordergroup.rb @@ -35,7 +35,7 @@ class Ordergroup < Group 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 :financial_transactions, :order => "created_on DESC" has_many :group_orders has_many :orders, :through => :group_orders diff --git a/app/views/articles/index.haml b/app/views/articles/index.haml index b30d7b16..9a5947e0 100644 --- a/app/views/articles/index.haml +++ b/app/views/articles/index.haml @@ -54,7 +54,7 @@ | = link_to _('Upload articles'), upload_supplier_articles_path(@supplier) | - = link_to_if @current_user.role_orders?, _('Create order'), {:controller => 'orders', :action => 'new', :id => @supplier } + = link_to_if @current_user.role_orders?, _('Create order'), {:controller => 'orders', :action => 'new', :supplier_id => @supplier } #article_filter #article_search_form{:style=>"display:inline;"} diff --git a/app/views/finance/balancing/_summary.haml b/app/views/finance/balancing/_summary.haml index eff935ef..027e6657 100644 --- a/app/views/finance/balancing/_summary.haml +++ b/app/views/finance/balancing/_summary.haml @@ -5,7 +5,7 @@ %table %tr %td Nettobetrag: - %td= number_to_currency(order.sum(:clear)) + %td= number_to_currency(order.sum(:net)) %tr %td Bruttobetrag: %td= number_to_currency(order.sum(:gross)) diff --git a/app/views/home/index.html.haml b/app/views/home/index.html.haml index 2a69ffea..36a51aab 100644 --- a/app/views/home/index.html.haml +++ b/app/views/home/index.html.haml @@ -57,7 +57,7 @@ %th=_ "Who" %th=_ "Note" %th=_ "Amount" - - for ft in @financial_transactions + - for ft in @ordergroup.financial_transactions.all(:limit => 5) %tr{:class => cycle('even','odd')} %td= format_time(ft.created_on) %td= h(ft.user.nil? ? '?' : ft.user.nick) diff --git a/app/views/layouts/_main_tabnav.html.erb b/app/views/layouts/_main_tabnav.html.erb index 48d47a7d..517bbbfc 100644 --- a/app/views/layouts/_main_tabnav.html.erb +++ b/app/views/layouts/_main_tabnav.html.erb @@ -20,22 +20,22 @@ :subnav => [ { :name => "Order", :url => "/ordering" }, { :name => "My orders", :url => "/ordering/myOrders" }, - { :name => "Manage orders", :url => "/orders", :access? => (u.role_orders?) } + { :name => "Manage orders", :url => "/orders", :access_denied? => (!u.role_orders?) } ] }, { :name => "Articles", :url => "/suppliers", :active => ["articles", "suppliers", "deliveries", "article_categories", "stockit"], - :access? => (u.role_article_meta? || u.role_suppliers?), + :access_denied? => (!u.role_article_meta? && !u.role_suppliers?), :subnav => [ { :name => "Artikel", :url => supplier_articles_path(Supplier.first) }, { :name => "Lager", :url => "/stockit" }, - { :name => "Lieferantinnen", :url => suppliers_path, :access? => (u.role_suppliers?) }, + { :name => "Lieferantinnen", :url => suppliers_path, :access_denied? => (!u.role_suppliers?) }, { :name => "Kategorien", :url => "/article_categories"} ] }, { :name => "Finance", :url => "/finance", :active => ["finance/invoices", "finance/transactions", "finance/balancing"], - :access? => (u.role_finance?), + :access_denied? => (!u.role_finance?), :subnav => [ { :name => "Manage accounts", :url => "/finance/transactions" }, { :name => "Balance orders", :url => "/finance/balancing/list" }, @@ -44,7 +44,7 @@ }, { :name => "Administration", :url => "/admin", :active => ["admin", "admin/users", "admin/ordergroups", "admin/workgroups"], - :access? => (u.role_admin?), + :access_denied? => (!u.role_admin?), :subnav => [ { :name => "Users", :url => admin_users_path }, { :name => "Ordergroups", :url => admin_ordergroups_path }, @@ -55,12 +55,12 @@ -%>