diff --git a/app/assets/stylesheets/bootstrap_and_overrides.css.less b/app/assets/stylesheets/bootstrap_and_overrides.css.less
index 6ad25972..1a514396 100644
--- a/app/assets/stylesheets/bootstrap_and_overrides.css.less
+++ b/app/assets/stylesheets/bootstrap_and_overrides.css.less
@@ -212,6 +212,16 @@ tr.unavailable {
}
}
+// editable article list can be more compact
+.ordered-articles input {
+ margin-bottom: 0;
+}
+
+// entering units
+.units_delta {
+ width: 2em;
+}
+
// ********* Tweaks & fixes
// need more space for supplier&order information (in German, at least)
diff --git a/app/controllers/finance/receive_controller.rb b/app/controllers/finance/receive_controller.rb
new file mode 100644
index 00000000..2ecbd059
--- /dev/null
+++ b/app/controllers/finance/receive_controller.rb
@@ -0,0 +1,37 @@
+class Finance::ReceiveController < Finance::BaseController
+
+ def edit
+ @order = Order.find(params[:id])
+ @order_articles = @order.order_articles.ordered.includes(:article)
+ end
+
+ def update
+ OrderArticle.transaction do
+ params[:order_articles].each do |oa_id, oa_params|
+ unless oa_params.blank?
+ oa = OrderArticle.find(oa_id)
+ # update attributes
+ oa.update_attributes!(oa_params)
+ # and process consequences
+ oa.redistribute(oa.units_received * oa.price.unit_quantity) unless oa.units_received.blank?
+ oa.save!
+ end
+ end
+
+ flash[:notice] = I18n.t('receive.update.notice')
+ redirect_to finance_order_index_path
+ end
+ end
+
+ # ajax add article
+ def add_article
+ @order = Order.find(params[:receive_id])
+ @order_article = @order.order_articles.where(:article_id => params[:article_id]).includes(:article).first
+ # we need to create the order article if it's not part of the current order
+ if @order_article.nil?
+ @order_article = @order.order_articles.build({order: @order, article_id: params[:article_id]})
+ @order_article.save!
+ end
+ end
+
+end
diff --git a/app/helpers/finance/receive_helper.rb b/app/helpers/finance/receive_helper.rb
new file mode 100644
index 00000000..15b28a66
--- /dev/null
+++ b/app/helpers/finance/receive_helper.rb
@@ -0,0 +1,9 @@
+# :encoding:utf-8:
+module Finance::ReceiveHelper
+ # TODO currently duplicate a bit of DeliveriesHelper.articles_for_select2
+ def articles_for_select2(supplier)
+ supplier.articles.undeleted.reorder('articles.name ASC').map do |a|
+ {:id => a.id, :text => "#{a.name} (#{a.unit_quantity}тип#{a.unit})"}
+ end
+ end
+end
diff --git a/app/models/group_order_article.rb b/app/models/group_order_article.rb
index 9d4b8c82..4d7bff09 100644
--- a/app/models/group_order_article.rb
+++ b/app/models/group_order_article.rb
@@ -99,57 +99,63 @@ 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 calculate_result
- @calculate_result ||= begin
- quantity = tolerance = total_quantity = 0
+ def calculate_result(total = nil)
+ # return memoized result unless a total is given
+ return @calculate_result if total.nil? and not @calculate_result.nil?
- # Get total
- if order_article.article.is_a?(StockArticle)
- total = order_article.article.quantity
- logger.debug "<#{order_article.article.name}> (stock) => #{total}"
- else
- total = order_article.units_to_order * order_article.price.unit_quantity
- logger.debug "<#{order_article.article.name}> units_to_order #{order_article.units_to_order} => #{total}"
+ quantity = tolerance = total_quantity = 0
+
+ # Get total
+ if not total.nil?
+ logger.debug "<#{order_article.article.name}> => #{total} (given)"
+ elsif order_article.article.is_a?(StockArticle)
+ total = order_article.article.quantity
+ logger.debug "<#{order_article.article.name}> (stock) => #{total}"
+ else
+ total = order_article.units_to_order * order_article.price.unit_quantity
+ logger.debug "<#{order_article.article.name}> units_to_order #{order_article.units_to_order} => #{total}"
+ end
+
+ 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...
+ 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...
+ order_quantities.each do |goaq|
+ q = [goaq.quantity, total - total_quantity].min
+ total_quantity += q
+ if goaq.group_order_article_id == self.id
+ logger.debug "increasing quantity by #{q}"
+ quantity += q
+ end
+ break if total_quantity >= total
end
- 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...
- 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...
+ # Determine tolerance to be ordered...
+ if total_quantity < total
+ logger.debug "determining additional items to be ordered from tolerance"
order_quantities.each do |goaq|
- q = [goaq.quantity, total - total_quantity].min
+ q = [goaq.tolerance, total - total_quantity].min
total_quantity += q
if goaq.group_order_article_id == self.id
- logger.debug "increasing quantity by #{q}"
- quantity += q
+ logger.debug "increasing tolerance by #{q}"
+ tolerance += q
end
break if total_quantity >= total
end
-
- # Determine tolerance to be ordered...
- if total_quantity < total
- logger.debug "determining additional items to be ordered from tolerance"
- order_quantities.each do |goaq|
- q = [goaq.tolerance, total - total_quantity].min
- total_quantity += q
- if goaq.group_order_article_id == self.id
- logger.debug "increasing tolerance by #{q}"
- tolerance += q
- end
- break if total_quantity >= total
- end
- end
-
- logger.debug "determined quantity/tolerance/total: #{quantity} / #{tolerance} / #{quantity + tolerance}"
end
- {:quantity => quantity, :tolerance => tolerance, :total => quantity + tolerance}
+ logger.debug "determined quantity/tolerance/total: #{quantity} / #{tolerance} / #{quantity + tolerance}"
end
+
+ # memoize result unless a total is given
+ r = {:quantity => quantity, :tolerance => tolerance, :total => quantity + tolerance}
+ @calculate_result = r if total.nil?
+ r
end
# Returns order result,
@@ -160,8 +166,8 @@ class GroupOrderArticle < ActiveRecord::Base
end
# This is used during order.finish!.
- def save_results!
- self.update_attribute(:result, calculate_result[:total])
+ def save_results!(article_total = nil)
+ self.update_attribute(:result, calculate_result(article_total)[:total])
end
# Returns total price for this individual article
diff --git a/app/models/order.rb b/app/models/order.rb
index 057c0768..087bf450 100644
--- a/app/models/order.rb
+++ b/app/models/order.rb
@@ -166,6 +166,9 @@ class Order < ActiveRecord::Base
goa.save_results!
# Delete no longer required order-history (group_order_article_quantities) and
# TODO: Do we need articles, which aren't ordered? (units_to_order == 0 ?)
+ # A: Yes, we do - for redistributing articles when the number of articles
+ # delivered changes, and for statistics on popular articles. Records
+ # with both tolerance and quantity zero can be deleted.
#goa.group_order_article_quantities.clear
end
end
diff --git a/app/models/order_article.rb b/app/models/order_article.rb
index 3eb97aea..32211f24 100644
--- a/app/models/order_article.rb
+++ b/app/models/order_article.rb
@@ -12,8 +12,8 @@ class OrderArticle < ActiveRecord::Base
validate :article_and_price_exist
validates_uniqueness_of :article_id, scope: :order_id
- scope :ordered, :conditions => "units_to_order > 0"
- scope :ordered_or_member, -> { includes(:group_order_articles).where("units_to_order > 0 OR group_order_articles.result > 0") }
+ scope :ordered, -> { where("units_to_order > 0 OR units_billed > 0 OR units_received > 0") }
+ scope :ordered_or_member, -> { includes(:group_order_articles).where("units_to_order > 0 OR units_billed > 0 OR units_received > 0 OR group_order_articles.result > 0") }
before_create :init_from_balancing
after_destroy :update_ordergroup_prices
@@ -34,7 +34,14 @@ class OrderArticle < ActiveRecord::Base
def price
article_price || article
end
-
+
+ # latest information on available units
+ def units
+ return units_received unless units_received.nil?
+ return units_billed unless units_billed.nil?
+ units_to_order
+ end
+
# Count quantities of belonging group_orders.
# In balancing this can differ from ordered (by supplier) quantity for this article.
def group_orders_sum
@@ -94,6 +101,18 @@ class OrderArticle < ActiveRecord::Base
(units_to_order * price.unit_quantity) == group_orders_sum[:quantity] rescue false
end
+ def redistribute(quantity)
+ # recompute
+ group_order_articles.each {|goa| goa.save_results! quantity }
+
+ # Update GroupOrder prices & Ordergroup stats
+ # TODO only affected group_orders, and once after redistributing all articles
+ order.group_orders.each(&:update_price!)
+ order.ordergroups.each(&:update_stats!)
+
+ # TODO notifications
+ end
+
# Updates order_article and belongings during balancing process
def update_article_and_price!(order_article_attributes, article_attributes, price_attributes = nil)
OrderArticle.transaction do
diff --git a/app/views/finance/balancing/_orders.html.haml b/app/views/finance/balancing/_orders.html.haml
index 6973aa32..3c54e340 100644
--- a/app/views/finance/balancing/_orders.html.haml
+++ b/app/views/finance/balancing/_orders.html.haml
@@ -19,6 +19,7 @@
%td= show_user(order.updated_by)
%td
- unless order.closed?
+ = link_to t('.receive'), edit_finance_receive_path(order), class: 'btn btn-mini'
= link_to t('.clear'), new_finance_order_path(order_id: order.id), class: 'btn btn-mini btn-primary'
= link_to t('.close'), close_direct_finance_order_path(order),
:confirm => t('.confirm'), :method => :put, class: 'btn btn-mini'
diff --git a/app/views/finance/receive/_edit_article.html.haml b/app/views/finance/receive/_edit_article.html.haml
new file mode 100644
index 00000000..8be17a37
--- /dev/null
+++ b/app/views/finance/receive/_edit_article.html.haml
@@ -0,0 +1,16 @@
+= fields_for 'order_articles', order_article, index: order_article.id do |form|
+ %tr{class: "#{cycle('even', 'odd', name: 'articles')} order-article", valign: "top"}
+ - order_title = []
+ - order_title.append t('.manufacturer')+': ' + order_article.article.manufacturer unless order_article.article.manufacturer.to_s.empty?
+ - order_title.append t('.note')+': ' + order_article.article.note unless order_article.article.note.to_s.empty?
+ - units_expected = (order_article.units_billed or order_article.units_to_order)
+ %td= order_article.article.order_number
+ %td.name{title: order_title.join("\n")}= order_article.article.name
+ %td #{order_article.article.unit_quantity} × #{order_article.article.unit}
+ %td #{order_article.quantity} + #{order_article.tolerance}
+ %td= order_article.units_to_order
+ %td= order_article.units_billed
+ %td
+ = form.text_field :units_received, class: 'input-mini', data: {'units-expected' => units_expected}
+ / TODO add almost invisible text_field for entering single units
+ %td.units_delta
diff --git a/app/views/finance/receive/_edit_articles.html.haml b/app/views/finance/receive/_edit_articles.html.haml
new file mode 100644
index 00000000..afc71f12
--- /dev/null
+++ b/app/views/finance/receive/_edit_articles.html.haml
@@ -0,0 +1,76 @@
+- content_for :javascript do
+ :javascript
+
+ function update_delta(input) {
+ var units = $(input).val();
+ var expected = $(input).data('units-expected');
+ var html;
+
+ if (units.replace(/\s/g,"")=="") {
+ // no value
+ html = '';
+ } else if (isNaN(units)) {
+ html = '';
+ } else if (units == expected) {
+ // equal value
+ html = '';
+ } else if (units < expected) {
+ html = '- '+(expected-units)+'';
+ } else /*if (units> expected)*/ {
+ html = '+ '+(units-expected)+'';
+ }
+
+ $(input).closest('tr').find('.units_delta').html(html);
+ }
+
+ $(document).on('change', 'input[data-units-expected]', function() {
+ update_delta(this);
+ });
+
+ $(function() {
+ $('input[data-units-expected]').each(function() {
+ update_delta(this);
+ });
+
+ $('#add_article').removeAttr('disabled').select2({
+ placeholder: '#{t '.add_article'}',
+ data: #{articles_for_select2(@order).to_json},
+ // TODO implement adding a new article, like in deliveries
+ }).on('change', function(e) {
+ var selectedArticle = $(e.currentTarget).select2('data');
+ if(!selectedArticle) {
+ return false;
+ }
+
+ $.ajax({
+ url: '#{finance_receive_add_article_path(@order)}',
+ type: 'get',
+ data: {article_id: selectedArticle.id},
+ contentType: 'application/json; charset=UTF-8'
+ });
+ $('#add_article').select2('data', null);
+ return true;
+ });
+
+ });
+
+
+%table.ordered-articles.table.table-striped.stupidtable
+ %thead
+ %tr
+ %th.sort{:data => {:sort => 'string'}}= t('.number')
+ %th.sort{:data => {:sort => 'string'}}= t('.article')
+ %th= heading_helper GroupOrderArticle, :units
+ %th Members
+ %th Ordered
+ %th Invoice
+ %th Received
+ %th
+ %tbody#result_table
+ - @order_articles.each do |order_article|
+ = render :partial => 'edit_article', :locals => {:order_article => order_article}
+ %tfoot
+ %tr
+ %th{:colspan => 8}
+ %input#add_article{:style => 'width: 500px;'}
+
diff --git a/app/views/finance/receive/add_article.js.erb b/app/views/finance/receive/add_article.js.erb
new file mode 100644
index 00000000..b73b6719
--- /dev/null
+++ b/app/views/finance/receive/add_article.js.erb
@@ -0,0 +1,14 @@
+$('div.container-fluid').prepend(
+ '<%= j(render(:partial => 'shared/alert_success', :locals => {:alert_message => t('.notice', :name => @order_article.article.name)})) %>'
+);
+
+(function() {
+ $('.ordered-articles tr').removeClass('success');
+
+ var article_for_adding = $(
+ '<%= j(render(:partial => 'edit_article', :locals => {:order_article => @order_article})) %>'
+ ).addClass('success');
+
+ $('.ordered-articles tbody').append(article_for_adding);
+ updateSort('.ordered-articles');
+})();
diff --git a/app/views/finance/receive/edit.html.haml b/app/views/finance/receive/edit.html.haml
new file mode 100644
index 00000000..4be57743
--- /dev/null
+++ b/app/views/finance/receive/edit.html.haml
@@ -0,0 +1,10 @@
+- title "Receiving #{@order.name}"
+
+= form_tag(finance_receive_path(@order), :method => :put) do
+ %section#results
+ = render 'edit_articles'
+ .form-actions
+ = submit_tag t('.submit'), class: 'btn btn-primary'
+ = link_to t('ui.or_cancel'), finance_order_index_path
+
+%p= link_to_top
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 5d3b1e06..efd2961e 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -560,6 +560,7 @@ en:
last_edited_by: Last edited by
name: Supplier
no_closed_orders: At the moment there are no closed orders.
+ receive: receive
state: State
summary:
changed: Data was changed!
diff --git a/config/routes.rb b/config/routes.rb
index 655a1038..3a0b0608 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -144,6 +144,10 @@ Foodsoft::Application.routes.draw do
resources :order_articles
end
+ resources :receive do
+ get :add_article
+ end
+
resources :group_order_articles do
member do
put :update_result
diff --git a/db/migrate/20130930132511_add_quantities_to_order_article.rb b/db/migrate/20130930132511_add_quantities_to_order_article.rb
new file mode 100644
index 00000000..1477b194
--- /dev/null
+++ b/db/migrate/20130930132511_add_quantities_to_order_article.rb
@@ -0,0 +1,6 @@
+class AddQuantitiesToOrderArticle < ActiveRecord::Migration
+ def change
+ add_column :order_articles, :units_billed, :integer
+ add_column :order_articles, :units_received, :integer
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 4170d918..3093e9a1 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended to check this file into your version control system.
-ActiveRecord::Schema.define(:version => 20130920201529) do
+ActiveRecord::Schema.define(:version => 20130930132511) do
create_table "article_categories", :force => true do |t|
t.string "name", :default => "", :null => false
@@ -195,6 +195,8 @@ ActiveRecord::Schema.define(:version => 20130920201529) do
t.integer "units_to_order", :default => 0, :null => false
t.integer "lock_version", :default => 0, :null => false
t.integer "article_price_id"
+ t.integer "units_billed"
+ t.integer "units_received"
end
add_index "order_articles", ["order_id", "article_id"], :name => "index_order_articles_on_order_id_and_article_id", :unique => true
diff --git a/spec/factories/order.rb b/spec/factories/order.rb
index 4bf63b00..ab89ff6e 100644
--- a/spec/factories/order.rb
+++ b/spec/factories/order.rb
@@ -24,8 +24,4 @@ FactoryGirl.define do
end
end
- # requires order and article
- factory :order_article do
- end
-
end
diff --git a/spec/models/order_article_spec.rb b/spec/models/order_article_spec.rb
new file mode 100644
index 00000000..bc6dcc5c
--- /dev/null
+++ b/spec/models/order_article_spec.rb
@@ -0,0 +1,106 @@
+require 'spec_helper'
+
+describe OrderArticle do
+ let(:order) { FactoryGirl.create :order, article_count: 1 }
+ let(:oa) { order.order_articles.first }
+
+ it 'is not ordered by default' do
+ expect(OrderArticle.ordered.count).to eq 0
+ end
+
+ [:units_to_order, :units_billed, :units_received].each do |units|
+
+ it "is ordered when there are #{units.to_s.gsub '_', ' '}" do
+ oa.update_attribute units, rand(1..99)
+ expect(OrderArticle.ordered.count).to eq 1
+ end
+
+ end
+
+ it 'knows how many items there are' do
+ oa.units_to_order = rand(1..99)
+ expect(oa.units).to eq oa.units_to_order
+ oa.units_billed = rand(1..99)
+ expect(oa.units).to eq oa.units_billed
+ oa.units_received = rand(1..99)
+ expect(oa.units).to eq oa.units_received
+
+ oa.units_billed = rand(1..99)
+ expect(oa.units).to eq oa.units_received
+ oa.units_to_order = rand(1..99)
+ expect(oa.units).to eq oa.units_received
+ oa.units_received = rand(1..99)
+ expect(oa.units).to eq oa.units_received
+ end
+
+ describe 'redistribution' do
+ let(:admin) { FactoryGirl.create :user, groups:[FactoryGirl.create(:workgroup, role_finance: true)] }
+ let(:article) { FactoryGirl.create :article, unit_quantity: 3 }
+ let(:order) { FactoryGirl.create :order, article_ids: [article.id] }
+ let(:go1) { FactoryGirl.create :group_order, order: order }
+ let(:go2) { FactoryGirl.create :group_order, order: order }
+ let(:go3) { FactoryGirl.create :group_order, order: order }
+ let(:goa1) { FactoryGirl.create :group_order_article, group_order: go1, order_article: oa }
+ let(:goa2) { FactoryGirl.create :group_order_article, group_order: go2, order_article: oa }
+ let(:goa3) { FactoryGirl.create :group_order_article, group_order: go3, order_article: oa }
+
+ # set quantities of group_order_articles
+ def set_quantities(q1, q2, q3)
+ goa1.update_quantities(*q1)
+ goa2.update_quantities(*q2)
+ goa3.update_quantities(*q3)
+ oa.update_results!
+ order.finish!(admin)
+ goa_reload
+ end
+
+ # reload all group_order_articles
+ def goa_reload
+ [goa1, goa2, goa3].map(&:reload)
+ end
+
+ it 'has expected units_to_order' do
+ set_quantities [3,2], [1,3], [1,0]
+ expect(oa.units*oa.article.unit_quantity).to eq 6
+ expect([goa1, goa2, goa3].map(&:result)).to eq [4, 1, 1]
+ end
+
+ it 'does nothing when nothing has changed' do
+ set_quantities [3,2], [1,3], [1,0]
+ oa.redistribute 6
+ goa_reload
+ expect([goa1, goa2, goa3].map(&:result).map(&:to_i)).to eq [4, 1, 1]
+ end
+
+ it 'works when there is nothing to distribute' do
+ set_quantities [3,2], [1,3], [1,0]
+ oa.redistribute 0
+ goa_reload
+ expect([goa1, goa2, goa3].map(&:result)).to eq [0, 0, 0]
+ end
+
+ it 'works when quantity needs to be reduced' do
+ set_quantities [3,2], [1,3], [1,0]
+ oa.redistribute 4
+ goa_reload
+ expect([goa1, goa2, goa3].map(&:result)).to eq [3, 1, 0]
+ end
+
+ it 'works when quantity is increased within quantity' do
+ set_quantities [3,0], [2,0], [2,0]
+ expect([goa1, goa2, goa3].map(&:result)).to eq [3, 2, 1]
+ oa.redistribute 7
+ goa_reload
+ expect([goa1, goa2, goa3].map(&:result).map(&:to_i)).to eq [3, 2, 2]
+ end
+
+ it 'works when there is just one for the first' do
+ set_quantities [3,2], [1,3], [1,0]
+ oa.redistribute 1
+ goa_reload
+ expect([goa1, goa2, goa3].map(&:result)).to eq [1, 0, 0]
+ end
+
+ end
+
+end