diff --git a/Gemfile b/Gemfile
index c49bfd14..90d804a9 100644
--- a/Gemfile
+++ b/Gemfile
@@ -86,6 +86,7 @@ group :test do
# webkit and poltergeist don't seem to work yet
gem 'selenium-webdriver'
gem 'database_cleaner'
+ gem 'connection_pool'
# need to include rspec components before i18n-spec or rake fails in test environment
gem 'rspec-core'
gem 'rspec-expectations'
diff --git a/Gemfile.lock b/Gemfile.lock
index f4dadb98..052ef358 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -114,6 +114,7 @@ GEM
execjs
coffee-script-source (1.6.3)
commonjs (0.2.7)
+ connection_pool (1.2.0)
content_for_in_controllers (0.0.2)
coveralls (0.7.0)
multi_json (~> 1.3)
@@ -387,6 +388,7 @@ DEPENDENCIES
client_side_validations
client_side_validations-simple_form
coffee-rails (~> 3.2.1)
+ connection_pool
coveralls
daemons
database_cleaner
diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
index c05b4474..57aaa64e 100644
--- a/app/assets/javascripts/application.js
+++ b/app/assets/javascripts/application.js
@@ -7,7 +7,6 @@
//= require bootstrap-datepicker/locales/bootstrap-datepicker.de
//= require bootstrap-datepicker/locales/bootstrap-datepicker.nl
//= require bootstrap-datepicker/locales/bootstrap-datepicker.fr
-//= require jquery.observe_field
//= require list
//= require list.unlist
//= require list.delay
@@ -20,6 +19,7 @@
//= require ordering
//= require stupidtable
//= require touchclick
+//= require delta_input
// Load following statements, when DOM is ready
$(function() {
@@ -69,17 +69,32 @@ $(function() {
return false;
});
- // Submit form when changing text of an input field
- // Use jquery observe_field plugin
- $('form[data-submit-onchange] input[type=text]').each(function() {
- $(this).observe_field(1, function() {
- $(this).parents('form').submit();
- });
+ // Submit form when clicking on checkbox
+ $(document).on('click', 'form[data-submit-onchange] input[type=checkbox]:not(input[data-ignore-onchange])', function() {
+ $(this).parents('form').submit();
});
- // Submit form when clicking on checkbox
- $('form[data-submit-onchange] input[type=checkbox]:not(input[data-ignore-onchange])').click(function() {
- $(this).parents('form').submit();
+ // Submit form when changing text of an input field.
+ // Wubmission will be done after 500ms of not typed, unless data-submit-onchange=changed,
+ // in which case it happens when the input box loses its focus ('changed' event).
+ $(document).on('changed keyup focusin', 'form[data-submit-onchange] input[type=text]', function(e) {
+ var input = $(this);
+ // when form has data-submit-onchange=changed, don't do updates while typing
+ if (e.type!='changed' && input.parents('form[data-submit-onchange=changed]')) {
+ return true;
+ }
+ // remember old value when it's getting the focus
+ if (e.type=='focusin') {
+ input.data('old-value', input.val());
+ return true;
+ }
+ // trigger timeout to submit form when value was changed
+ clearTimeout(input.data('submit-timeout-id'));
+ input.data('submit-timeout-id', setTimeout(function() {
+ if (input.val() != input.data('old-value')) input.parents('form').submit();
+ input.removeData('submit-timeout-id');
+ input.removeData('old-value');
+ }, 500));
});
$('[data-redirect-to]').bind('change', function() {
diff --git a/app/assets/javascripts/delta_input.js b/app/assets/javascripts/delta_input.js
new file mode 100644
index 00000000..0ae7f4a5
--- /dev/null
+++ b/app/assets/javascripts/delta_input.js
@@ -0,0 +1,49 @@
+
+$(function() {
+ $(document).on('click', 'button[data-increment]', function() {
+ data_delta_update($('#'+$(this).data('increment')), +1);
+ });
+ $(document).on('click', 'button[data-decrement]', function() {
+ data_delta_update($('#'+$(this).data('decrement')), -1);
+ });
+ $(document).on('change keyup', 'input[type="text"][data-delta]', function() {
+ data_delta_update(this, 0);
+ });
+});
+
+function data_delta_update(el, direction) {
+ var id = $(el).attr('id');
+
+ var min = $(el).data('min');
+ var max = $(el).data('max');
+ var delta = $(el).data('delta');
+ var granularity = $(el).data('granularity');
+
+ var val = $(el).val();
+ var oldval = $.isNumeric(val) ? Number(val) : 0;
+ var newval = oldval + delta*direction;
+
+ if (newval < min) newval = min;
+ if (newval > max) newval = max;
+
+ // disable buttons when min/max reached
+ $('button[data-decrement='+id+']').attr('disabled', newval<=min ? 'disabled' : null);
+ $('button[data-increment='+id+']').attr('disabled', newval>=max ? 'disabled' : null);
+
+ // warn when what was entered is not a number
+ $(el).toggleClass('error', val!='' && val!='.' && (!$.isNumeric(val) || val < 0));
+
+ // update field, unless the user is typing
+ if (!$(el).is(':focus')) {
+ $(el).val(round_float(newval, granularity));
+ $(el).trigger('changed');
+ }
+}
+
+// truncate numbers because of tiny floating point deviations
+// if we don't do this, 1.0 might be shown as 0.99999999
+function round_float(s, granularity) {
+ var e = granularity ? 1/granularity : 1000;
+ return Math.round(Number(s)*e) / e;
+}
+
diff --git a/app/assets/stylesheets/bootstrap_and_overrides.css.less b/app/assets/stylesheets/bootstrap_and_overrides.css.less
index b87d7185..463dd2d3 100644
--- a/app/assets/stylesheets/bootstrap_and_overrides.css.less
+++ b/app/assets/stylesheets/bootstrap_and_overrides.css.less
@@ -1,5 +1,7 @@
@import "twitter/bootstrap/bootstrap";
@import "twitter/bootstrap/responsive";
+@import "delta_input";
+
body {
padding-top: 10px;
}
@@ -104,7 +106,7 @@ table {
content: ' \25B2';
}
- tr.article-category {
+ tr.list-heading {
background-color: #efefef;
td:first-child {
text-align: left;
@@ -126,6 +128,10 @@ table {
}
}
+.center, td.center, th.center {
+ text-align: center;
+}
+
// Tasks ..
.accepted {
color: #468847;
@@ -238,6 +244,27 @@ tr.unavailable {
min-width: 3.5em;
}
+// small cells with just a 'x' or '='
+td.symbol, th.symbol {
+ padding-left: 0;
+ padding-right: 0;
+ text-align: center;
+}
+.symbol { color: tint(@textColor, @nonessentialDim); }
+.used .symbol { color: tint(@articleUsedColor, @nonessentialDim); }
+.unused .symbol { color: tint(@articleUnusedColor, @nonessentialDim); }
+.unavailable .symbol { color: @articleUnavailColor; }
+
+// hide symbols completely on small screens to save space
+@media (max-width: 768px) {
+ .symbol {
+ font-size: 0;
+ padding: 0;
+ margin: 0;
+ }
+}
+
+
// ********* Tweaks & fixes
// Fix bootstrap dropdown menu on mobile
diff --git a/app/assets/stylesheets/delta_input.less b/app/assets/stylesheets/delta_input.less
new file mode 100644
index 00000000..c32ac5ec
--- /dev/null
+++ b/app/assets/stylesheets/delta_input.less
@@ -0,0 +1,33 @@
+// needs @import "twitter/bootstrap/bootstrap";
+
+form.delta-input, .delta-input form {
+ margin: 0;
+ padding: 0;
+}
+
+.delta-input {
+
+ .btn {
+ padding: 0;
+ width: 24px;
+ height: 28px;
+ vertical-align: middle;
+ }
+ input[data-delta] {
+ text-align: center;
+ height: 18px;
+ }
+
+ // handle error class outside of bootstrap controls
+ input[data-delta].error {
+ // relevant bootstrap portion of: .formFieldState(@errorText, @errorText, @errorBackground);
+ border-color: @errorText;
+ .box-shadow(inset 0 1px 1px rgba(0,0,0,.075)); // Redeclare so transitions work
+ &:focus {
+ border-color: darken(@errorText, 10%);
+ @shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 6px lighten(@errorText, 20%);
+ .box-shadow(@shadow);
+ }
+ }
+}
+
diff --git a/app/controllers/finance/group_order_articles_controller.rb b/app/controllers/group_order_articles_controller.rb
similarity index 58%
rename from app/controllers/finance/group_order_articles_controller.rb
rename to app/controllers/group_order_articles_controller.rb
index 3596ec40..dbc40274 100644
--- a/app/controllers/finance/group_order_articles_controller.rb
+++ b/app/controllers/group_order_articles_controller.rb
@@ -1,6 +1,7 @@
-class Finance::GroupOrderArticlesController < ApplicationController
+class GroupOrderArticlesController < ApplicationController
before_filter :authenticate_finance
+ before_filter :find_group_order_article, except: [:new, :create]
layout false # We only use this controller to server js snippets, no need for layout rendering
@@ -10,6 +11,8 @@ class Finance::GroupOrderArticlesController < ApplicationController
end
def create
+ # XXX when ordergroup_id appears before order_article_id in the parameters, you
+ # can get `NoMethodError - undefined method 'order_id' for nil:NilClass`
@group_order_article = GroupOrderArticle.new(params[:group_order_article])
@order_article = @group_order_article.order_article
@@ -21,59 +24,38 @@ class Finance::GroupOrderArticlesController < ApplicationController
@group_order_article = goa
update_summaries(@group_order_article)
- render :update
+ render :create
elsif @group_order_article.save
update_summaries(@group_order_article)
- render :update
+ render :create
else # Validation failed, show form
render :new
end
end
- def edit
- @group_order_article = GroupOrderArticle.find(params[:id])
- @order_article = @group_order_article.order_article
- end
-
def update
- @group_order_article = GroupOrderArticle.find(params[:id])
- @order_article = @group_order_article.order_article
-
- if @group_order_article.update_attributes(params[:group_order_article])
- update_summaries(@group_order_article)
+ if params[:delta]
+ @group_order_article.update_attribute :result, [@group_order_article.result + params[:delta].to_f, 0].max
else
- render :edit
- end
- end
-
- def update_result
- group_order_article = GroupOrderArticle.find(params[:id])
- @order_article = group_order_article.order_article
-
- if params[:modifier] == '-'
- group_order_article.update_attribute :result, group_order_article.result - 1
- elsif params[:modifier] == '+'
- group_order_article.update_attribute :result, group_order_article.result + 1
+ @group_order_article.update_attributes(params[:group_order_article])
end
- update_summaries(group_order_article)
+ update_summaries(@group_order_article)
render :update
end
def destroy
- group_order_article = GroupOrderArticle.find(params[:id])
# only destroy if quantity and tolerance was zero already, so that we don't
# lose what the user ordered, if any
- if group_order_article.quantity > 0 or group_order_article.tolerance >0
- group_order_article.update_attribute(:result, 0)
+ if @group_order_article.quantity > 0 or @group_order_article.tolerance >0
+ @group_order_article.update_attribute(:result, 0)
else
- group_order_article.destroy
+ @group_order_article.destroy
end
- update_summaries(group_order_article)
- @order_article = group_order_article.order_article
+ update_summaries(@group_order_article)
render :update
end
@@ -86,4 +68,8 @@ class Finance::GroupOrderArticlesController < ApplicationController
# Update units_to_order of order_article
group_order_article.order_article.update_results! if group_order_article.order_article.article.is_a?(StockArticle)
end
+
+ def find_group_order_article
+ @group_order_article = GroupOrderArticle.find(params[:id])
+ end
end
diff --git a/app/controllers/orders_controller.rb b/app/controllers/orders_controller.rb
index d3c1590d..f390163e 100644
--- a/app/controllers/orders_controller.rb
+++ b/app/controllers/orders_controller.rb
@@ -13,10 +13,10 @@ class OrdersController < ApplicationController
@per_page = 15
if params['sort']
sort = case params['sort']
- when "supplier" then "suppliers.name, ends DESC"
- when "ends" then "ends DESC"
- when "supplier_reverse" then "suppliers.name DESC"
- when "ends_reverse" then "ends"
+ when "supplier" then "suppliers.name, ends DESC"
+ when "ends" then "ends DESC"
+ when "supplier_reverse" then "suppliers.name DESC"
+ when "ends_reverse" then "ends"
end
else
sort = "ends DESC"
@@ -30,9 +30,9 @@ class OrdersController < ApplicationController
@order= Order.find(params[:id])
@view = (params[:view] or 'default').gsub(/[^-_a-zA-Z0-9]/, '')
@partial = case @view
- when 'default' then 'articles'
- when 'groups'then 'shared/articles_by_groups'
- when 'articles'then 'shared/articles_by_articles'
+ when 'default' then 'articles'
+ when 'groups' then 'shared/articles_by/groups'
+ when 'articles' then 'shared/articles_by/articles'
else 'articles'
end
@@ -43,10 +43,10 @@ class OrdersController < ApplicationController
end
format.pdf do
pdf = case params[:document]
- when 'groups' then OrderByGroups.new(@order)
+ when 'groups' then OrderByGroups.new(@order)
when 'articles' then OrderByArticles.new(@order)
- when 'fax' then OrderFax.new(@order)
- when 'matrix' then OrderMatrix.new(@order)
+ when 'fax' then OrderFax.new(@order)
+ when 'matrix' then OrderMatrix.new(@order)
end
send_data pdf.to_pdf, filename: pdf.filename, type: 'application/pdf'
end
@@ -120,13 +120,11 @@ class OrdersController < ApplicationController
def receive_on_order_article_create # See publish/subscribe design pattern in /doc.
@order_article = OrderArticle.find(params[:order_article_id])
-
render :layout => false
end
def receive_on_order_article_update # See publish/subscribe design pattern in /doc.
@order_article = OrderArticle.find(params[:order_article_id])
-
render :layout => false
end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 4a266ece..d826a6b4 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -78,14 +78,19 @@ module ApplicationHelper
# When the 'short' option is true, abbreviations will be used:
# When there is a non-empty model attribute 'foo', it looks for
# the model attribute translation 'foo_short' and use that as
- # heading, with an abbreviation title of 'foo'.
+ # heading, with an abbreviation title of 'foo'. If a translation
+ # 'foo_desc' is present, that is used instead, but that can be
+ # be overridden by the option 'desc'.
# Other options are passed through to I18n.
def heading_helper(model, attribute, options = {})
- i18nopts = options.select {|a| !['short'].include?(a) }.merge({count: 2})
+ i18nopts = {count: 2}.merge(options.select {|a| !['short', 'desc'].include?(a) })
s = model.human_attribute_name(attribute, i18nopts)
if options[:short]
+ desc = options[:desc]
+ desc ||= model.human_attribute_name("#{attribute}_desc".to_sym, options.merge({fallback: true, default: '', count: 2}))
+ desc.blank? and desc = s
sshort = model.human_attribute_name("#{attribute}_short".to_sym, options.merge({fallback: true, default: '', count: 2}))
- s = raw "#{sshort}" unless sshort.blank?
+ s = raw "#{sshort}" unless sshort.blank?
end
s
end
diff --git a/app/helpers/finance/balancing_helper.rb b/app/helpers/finance/balancing_helper.rb
index 2daa44b2..e81a9f98 100644
--- a/app/helpers/finance/balancing_helper.rb
+++ b/app/helpers/finance/balancing_helper.rb
@@ -5,9 +5,9 @@ module Finance::BalancingHelper
when 'edit_results' then
'edit_results_by_articles'
when 'groups_overview' then
- 'shared/articles_by_groups'
+ 'shared/articles_by/groups'
when 'articles_overview' then
- 'shared/articles_by_articles'
+ 'shared/articles_by/articles'
end
end
end
diff --git a/app/helpers/group_order_articles_helper.rb b/app/helpers/group_order_articles_helper.rb
new file mode 100644
index 00000000..46f2a8cb
--- /dev/null
+++ b/app/helpers/group_order_articles_helper.rb
@@ -0,0 +1,14 @@
+module GroupOrderArticlesHelper
+
+ # return an edit field for a GroupOrderArticle result
+ def group_order_article_edit_result(goa)
+ unless goa.group_order.order.finished? and current_user.role_finance?
+ goa.result
+ else
+ simple_form_for goa, remote: true, html: {'data-submit-onchange' => 'changed', class: 'delta-input'} do |f|
+ f.input_field :result, as: :delta, class: 'input-nano', data: {min: 0}, id: "r_#{goa.id}"
+ end
+ end
+ end
+
+end
diff --git a/app/helpers/orders_helper.rb b/app/helpers/orders_helper.rb
index fffeba44..14131732 100644
--- a/app/helpers/orders_helper.rb
+++ b/app/helpers/orders_helper.rb
@@ -18,7 +18,7 @@ module OrdersHelper
options_for_select(options)
end
- # "1 ordered units, 2 billed, 2 received"
+ # "1×2 ordered, 2×2 billed, 2×2 received"
def units_history_line(order_article, options={})
if order_article.order.open?
nil
@@ -26,10 +26,9 @@ module OrdersHelper
units_info = []
[:units_to_order, :units_billed, :units_received].map do |unit|
if n = order_article.send(unit)
- i18nkey = if units_info.empty? and options[:plain] then unit else "#{unit}_short" end
line = n.to_s + ' '
- line += pkg_helper(order_article.price) + ' ' unless options[:plain] or n == 0
- line += OrderArticle.human_attribute_name(i18nkey, count: n)
+ line += pkg_helper(order_article.price, options) + ' ' unless n == 0
+ line += OrderArticle.human_attribute_name("#{unit}_short", count: n)
units_info << line
end
end
@@ -39,13 +38,16 @@ module OrdersHelper
# can be article or article_price
# icon: `false` to not show the icon
+ # plain: `true` to not use html (implies icon: false)
# soft_uq: `true` to hide unit quantity specifier on small screens
# sensible in tables with multiple columns calling `pkg_helper`
def pkg_helper(article, options={})
return '' if not article or article.unit_quantity == 1
- uq_text = "× #{article.unit_quantity}".html_safe
+ uq_text = "× #{article.unit_quantity}"
uq_text = content_tag(:span, uq_text, class: 'hidden-phone') if options[:soft_uq]
- if options[:icon].nil? or options[:icon]
+ if options[:plain]
+ uq_text
+ elsif options[:icon].nil? or options[:icon]
pkg_helper_icon(uq_text)
else
pkg_helper_icon(uq_text, tag: :span)
diff --git a/app/inputs/delta_input.rb b/app/inputs/delta_input.rb
new file mode 100644
index 00000000..20a10805
--- /dev/null
+++ b/app/inputs/delta_input.rb
@@ -0,0 +1,24 @@
+# encoding: utf-8
+
+class DeltaInput < SimpleForm::Inputs::StringInput
+ # for now, need to pass id or it won't work
+ def input
+ @input_html_options[:data] ||= {}
+ @input_html_options[:data][:delta] ||= 1
+ @input_html_options[:autocomplete] ||= 'off'
+ # TODO get generated id, don't know how yet - `add_default_name_and_id_for_value` might be an option
+
+ template.content_tag :div, class: 'delta-input input-prepend input-append' do
+ delta_button('−', -1) + super + delta_button('+', 1)
+ end
+ end
+ #template.button_tag('−', type: :submit, data: {decrement: @input_html_options[:id]}, tabindex: -1, class: 'btn') +
+
+ private
+
+ def delta_button(title, direction)
+ data = { (direction>0 ? 'increment' : 'decrement') => @input_html_options[:id] }
+ delta = direction * @input_html_options[:data][:delta]
+ template.button_tag(title, type: :button, name: 'delta', value: delta, data: data, tabindex: -1, class: 'btn')
+ end
+end
diff --git a/app/views/finance/balancing/_group_order_articles.html.haml b/app/views/finance/balancing/_group_order_articles.html.haml
index 2658878b..a0fb3134 100644
--- a/app/views/finance/balancing/_group_order_articles.html.haml
+++ b/app/views/finance/balancing/_group_order_articles.html.haml
@@ -4,38 +4,31 @@
%tr
%td
%td{:style => "width:8em"}= Ordergroup.model_name.human
- %td= t('.units')
+ -#%td.center= t('.units')
+ %td.center
+ %acronym{:title => t('shared.articles.received_desc')}= t 'shared.articles.received'
%td= t('.total')
%td{:colspan => "3",:style => "width:14em"}
- = link_to t('.add_group'), new_finance_group_order_article_path(order_article_id: order_article.id),
+ = link_to t('.add_group'), new_group_order_article_path(order_article_id: order_article.id),
remote: true, class: 'btn btn-mini'
%tbody
+ - totals = {result: 0}
- for group_order_article in order_article.group_order_articles.select { |goa| goa.result > 0 }
%tr[group_order_article]
%td
%td{:style=>"width:50%"}
= group_order_article.group_order.ordergroup.name
- %td{:id => "group_order_article_#{group_order_article.id}_quantity", :style => "white-space:nowrap"}
- = group_order_article.result
- = link_to "+", update_result_finance_group_order_article_path(group_order_article, modifier: '+'),
- method: :put, remote: true, class: 'btn btn-mini'
- = link_to "-", update_result_finance_group_order_article_path(group_order_article, modifier: '-'),
- method: :put, remote: true, class: 'btn btn-mini'
- %td.numeric
- = number_to_currency(group_order_article.order_article.price.fc_price * group_order_article.result)
+ %td.center= group_order_article_edit_result(group_order_article)
+ %td.numeric= number_to_currency(group_order_article.order_article.price.fc_price * group_order_article.result)
%td.actions{:style=>"width:1em"}
- = link_to t('ui.edit'), edit_finance_group_order_article_path(group_order_article), remote: true,
- class: 'btn btn-mini'
- %td.actions{:style=>"width:1em"}
- = link_to t('ui.delete'), finance_group_order_article_path(group_order_article), method: :delete,
+ = link_to t('ui.delete'), group_order_article_path(group_order_article), method: :delete,
remote: true, class: 'btn btn-mini btn-danger'
%td
+ - totals[:result] += group_order_article.result
%tfoot
%tr
%td
%td{:style => "width:8em"}= t('.total_fc')
- %td{:id => "group_orders_sum_quantity_#{order_article.id}"}
- = order_article.group_orders_sum[:quantity]
- %td.numeric{:id => "group_orders_sum_price_#{order_article.id}"}
- = number_to_currency(order_article.group_orders_sum[:price])
+ %td.center= totals[:result]
+ %td.numeric= number_to_currency(order_article.group_orders_sum[:price])
%td{:colspan => "3"}
diff --git a/app/views/finance/balancing/new.html.haml b/app/views/finance/balancing/new.html.haml
index f98840d1..5395e7a4 100644
--- a/app/views/finance/balancing/new.html.haml
+++ b/app/views/finance/balancing/new.html.haml
@@ -3,7 +3,7 @@
$(function() {
// Subscribe to database changes.
// See publish/subscribe design pattern in /doc.
- $(document).on('OrderArticle#update', function(e) {
+ $(document).on('OrderArticle#update GroupOrderArticle#create GroupOrderArticle#update', function(e) {
$.ajax({
url: '#{new_on_order_article_update_finance_order_path(@order)}',
type: 'get',
@@ -21,6 +21,7 @@
});
});
});
+= render 'shared/articles_by/common', order: @order
- title t('.title', name: @order.name)
diff --git a/app/views/finance/group_order_articles/edit.js.haml b/app/views/finance/group_order_articles/edit.js.haml
deleted file mode 100644
index 35b0e7c2..00000000
--- a/app/views/finance/group_order_articles/edit.js.haml
+++ /dev/null
@@ -1,2 +0,0 @@
-$('#modalContainer').html('#{j(render("form"))}');
-$('#modalContainer').modal();
\ No newline at end of file
diff --git a/app/views/finance/group_order_articles/new.js.haml b/app/views/finance/group_order_articles/new.js.haml
deleted file mode 100644
index 35b0e7c2..00000000
--- a/app/views/finance/group_order_articles/new.js.haml
+++ /dev/null
@@ -1,2 +0,0 @@
-$('#modalContainer').html('#{j(render("form"))}');
-$('#modalContainer').modal();
\ No newline at end of file
diff --git a/app/views/finance/group_order_articles/update.js.haml b/app/views/finance/group_order_articles/update.js.haml
deleted file mode 100644
index 36e66ccd..00000000
--- a/app/views/finance/group_order_articles/update.js.haml
+++ /dev/null
@@ -1,4 +0,0 @@
-$('#modalContainer').modal('hide');
-$('#order_article_#{@order_article.id}').html('#{j(render('finance/balancing/order_article', order_article: @order_article))}');
-$('#group_order_articles_#{@order_article.id}').html('#{j(render('finance/balancing/group_order_articles', order_article: @order_article))}');
-$('#summaryChangedWarning').show();
\ No newline at end of file
diff --git a/app/views/finance/group_order_articles/_form.html.haml b/app/views/group_order_articles/_form.html.haml
similarity index 67%
rename from app/views/finance/group_order_articles/_form.html.haml
rename to app/views/group_order_articles/_form.html.haml
index 082a3fba..f41dbfdc 100644
--- a/app/views/finance/group_order_articles/_form.html.haml
+++ b/app/views/group_order_articles/_form.html.haml
@@ -1,11 +1,11 @@
-= simple_form_for [:finance, @group_order_article], remote: true do |form|
+= simple_form_for @group_order_article, remote: true do |form|
= form.hidden_field :order_article_id
.modal-header
= link_to t('ui.marks.close').html_safe, '#', class: 'close', data: {dismiss: 'modal'}
%h3= t('.amount_change_for', article: @order_article.article.name)
.modal-body
= form.input :ordergroup_id, as: :select, collection: Ordergroup.all.map { |g| [g.name, g.id] }
- = form.input :result, hint: I18n.t('finance.group_order_articles.form.result_hint', unit: @order_article.article.unit) # Why do we need the full prefix?
+ = form.input :result, hint: I18n.t('group_order_articles.form.result_hint', unit: @order_article.article.unit) # Why do we need the full prefix?
.modal-footer
= link_to t('ui.close'), '#', class: 'btn', data: {dismiss: 'modal'}
= form.submit t('ui.save'), class: 'btn btn-primary'
diff --git a/app/views/group_order_articles/create.js.erb b/app/views/group_order_articles/create.js.erb
new file mode 100644
index 00000000..c0168fb3
--- /dev/null
+++ b/app/views/group_order_articles/create.js.erb
@@ -0,0 +1,10 @@
+$('#modalContainer').modal('hide');
+
+// trigger hooks for views
+$(document).trigger({
+ type: 'GroupOrderArticle#create',
+ order_article_id: <%= @group_order_article.order_article_id %>,
+ group_order_id: <%= @group_order_article.group_order_id %>,
+ group_order_article_id: <%= @group_order_article.id %>,
+ group_order_article_price: <%= @group_order_article.total_price %>
+});
diff --git a/app/views/group_order_articles/new.js.erb b/app/views/group_order_articles/new.js.erb
new file mode 100644
index 00000000..43bcee7e
--- /dev/null
+++ b/app/views/group_order_articles/new.js.erb
@@ -0,0 +1,2 @@
+$('#modalContainer').html('<%= j render("form") %>');
+$('#modalContainer').modal();
diff --git a/app/views/group_order_articles/update.js.erb b/app/views/group_order_articles/update.js.erb
new file mode 100644
index 00000000..bc2c7214
--- /dev/null
+++ b/app/views/group_order_articles/update.js.erb
@@ -0,0 +1,8 @@
+// and trigger hooks for views including this
+$(document).trigger({
+ type: 'GroupOrderArticle#update',
+ order_article_id: <%= @group_order_article.order_article_id %>,
+ group_order_id: <%= @group_order_article.group_order_id %>,
+ group_order_article_id: <%= @group_order_article.id %>,
+ group_order_article_price: <%= @group_order_article.total_price %>
+});
diff --git a/app/views/orders/show.html.haml b/app/views/orders/show.html.haml
index 24006336..a3282fbf 100644
--- a/app/views/orders/show.html.haml
+++ b/app/views/orders/show.html.haml
@@ -98,3 +98,5 @@
$(function() {
activate_search('#{j @view}', '#{j t(".search_placeholder.#{@view}")}');
});
+
+= render 'shared/articles_by/common', order: @order
diff --git a/app/views/shared/_articles_by_articles.html.haml b/app/views/shared/_articles_by_articles.html.haml
deleted file mode 100644
index 9a6132f6..00000000
--- a/app/views/shared/_articles_by_articles.html.haml
+++ /dev/null
@@ -1,26 +0,0 @@
-%table.table.table-hover.list
- %thead.list-heading
- %tr
- %th{:style => 'width:70%'}= t '.ordergroup'
- %th
- %acronym{:title => t('shared.articles.ordered_desc')}= t 'shared.articles.ordered'
- %th
- %acronym{:title => t('shared.articles.received_desc')}= t 'shared.articles.received'
- %th= t '.price'
-
- - for order_article in order.order_articles.ordered.all(:include => [:article, :article_price])
- %tbody
- %tr
- %th.name{:colspan => "4"}
- = order_article.article.name
- = "(#{order_article.article.unit} | #{order_article.price.unit_quantity} | #{number_to_currency(order_article.price.gross_price)})"
- - for goa in order_article.group_order_articles.ordered
- %tr{:class => [cycle('even', 'odd', :name => 'groups'), if goa.result == 0 then 'unavailable' end]}
- %td{:style => "width:70%"}=h goa.group_order.ordergroup.name
- %td= "#{goa.quantity} + #{goa.tolerance}"
- %td
- %b= goa.result
- %td= number_to_currency(order_article.price.fc_price * goa.result)
- %tr
- %td(colspan="4" )
- - reset_cycle('groups')
diff --git a/app/views/shared/_articles_by_groups.html.haml b/app/views/shared/_articles_by_groups.html.haml
deleted file mode 100644
index e117dcc3..00000000
--- a/app/views/shared/_articles_by_groups.html.haml
+++ /dev/null
@@ -1,40 +0,0 @@
-%table.table.table-hover.list
- %thead.list-heading
- %tr
- %th{:style => "width:40%"}= heading_helper Article, :name
- %th
- %acronym{:title => t('shared.articles.ordered_desc')}= t 'shared.articles.ordered'
- %th
- %acronym{:title => t('shared.articles.received_desc')}= t 'shared.articles.received'
- %th
- %acronym{:title => t('.fc_price_desc')}= t '.fc_price'
- %th
- %acronym{:title => t('.unit_quantity_desc')}= t '.unit_quantity'
- %th= heading_helper Article, :unit
- %th= t '.price'
-
- - for group_order in order.group_orders.ordered
- %tbody
- %tr
- %th{:colspan => "7"}
- %h4.name= group_order.ordergroup.name
- - total = 0
- - for goa in group_order.group_order_articles.ordered.all(:include => :order_article)
- - fc_price = goa.order_article.price.fc_price
- - subTotal = fc_price * goa.result
- - total += subTotal
- %tr{:class => [cycle('even', 'odd', :name => 'articles'), if goa.result == 0 then 'unavailable' end]}
- %td.name{:style => "width:40%"}=h goa.order_article.article.name
- %td= "#{goa.quantity} + #{goa.tolerance}"
- %td
- %b= goa.result
- %td= number_to_currency(fc_price)
- %td= goa.order_article.price.unit_quantity
- %td= goa.order_article.article.unit
- %td= number_to_currency(subTotal)
- %tr{:class => cycle('even', 'odd', :name => 'articles')}
- %th{:colspan => "6"} Summe
- %th= number_to_currency(total)
- %tr
- %th(colspan="7")
- - reset_cycle("articles")
diff --git a/app/views/shared/articles_by/_article_single.html.haml b/app/views/shared/articles_by/_article_single.html.haml
new file mode 100644
index 00000000..7b508029
--- /dev/null
+++ b/app/views/shared/articles_by/_article_single.html.haml
@@ -0,0 +1,11 @@
+%tbody{id: "oa_#{order_article.id}"}
+ - if not defined?(heading) or heading
+ %tr.list-heading
+ %th.name{:colspan => "4"}>
+ = order_article.article.name + ' '
+ = "(#{order_article.article.unit}, #{number_to_currency order_article.price.fc_price}"
+ - pkg_info = pkg_helper(order_article.price)
+ = ", #{pkg_info}".html_safe unless pkg_info.blank?
+ )
+ - for goa in order_article.group_order_articles.ordered
+ = render 'shared/articles_by/article_single_goa', goa: goa, edit: (edit rescue nil)
diff --git a/app/views/shared/articles_by/_article_single_goa.html.haml b/app/views/shared/articles_by/_article_single_goa.html.haml
new file mode 100644
index 00000000..b34178d9
--- /dev/null
+++ b/app/views/shared/articles_by/_article_single_goa.html.haml
@@ -0,0 +1,5 @@
+%tr{class: if goa.result == 0 then 'unavailable' end, id: "goa_#{goa.id}"}
+ %td{:style => "width:70%"}= goa.group_order.ordergroup.name
+ %td.center= "#{goa.quantity} + #{goa.tolerance}"
+ %td.center.input-delta= (edit or true rescue true) ? group_order_article_edit_result(goa) : goa.result
+ %td.price{data: {value: goa.total_price}}= number_to_currency(goa.total_price)
diff --git a/app/views/shared/articles_by/_articles.html.haml b/app/views/shared/articles_by/_articles.html.haml
new file mode 100644
index 00000000..be3ce1e9
--- /dev/null
+++ b/app/views/shared/articles_by/_articles.html.haml
@@ -0,0 +1,14 @@
+%table.table.table-hover.list#articles_by_articles
+ %thead.list-heading
+ %tr
+ %th{:style => 'width:70%'}= Ordergroup.model_name.human
+ %th.center
+ %acronym{:title => t('shared.articles.ordered_desc')}= t 'shared.articles.ordered'
+ %th.center
+ %acronym{:title => t('shared.articles.received_desc')}= t 'shared.articles.received'
+ %th= t 'shared.articles_by.price'
+
+ - for order_article in order.order_articles.ordered.all(:include => [:article, :article_price])
+ = render 'shared/articles_by/article_single', order_article: order_article, edit: (edit rescue nil)
+ %tr
+ %td{colspan: 4}
diff --git a/app/views/shared/articles_by/_common.html.haml b/app/views/shared/articles_by/_common.html.haml
new file mode 100644
index 00000000..e75560ce
--- /dev/null
+++ b/app/views/shared/articles_by/_common.html.haml
@@ -0,0 +1,30 @@
+-# common javascript for updating articles_by views
+-# include this in all pages that use articles_by views (directly or via ajax)
+= content_for :javascript do
+ :javascript
+ $(document).on('GroupOrderArticle#update', function(e) {
+
+ var el_goa = $('#goa_'+e.group_order_article_id);
+
+ // update total price of group_order_article
+ // show localised value, store raw number in data attribute
+ var el_price = $('.price', el_goa);
+ var old_price = el_price.data('value');
+ if (el_price.length) {
+ el_price.text(I18n.l('currency', e.group_order_article_price));
+ el_price.data('value', e.group_order_article_price);
+ }
+
+ // group_order_article is greyed when result==0
+ el_goa.toggleClass('unavailable', $('input#r_'+e.group_order_article_id, el_goa).val()==0);
+
+ // update total price of group_order, order_article and/or ordergroup, when present
+ var el_sum = $('#group_order_'+e.group_order_id+', #single_ordergroup_total, #single_order_article_total');
+ var el_price_sum = $('.price_sum', el_sum);
+ if (el_price_sum.length) {
+ var old_price_sum = el_price_sum.data('value');
+ var new_price_sum = old_price_sum - old_price + e.group_order_article_price;
+ el_price_sum.text(I18n.l('currency', new_price_sum));
+ el_price_sum.data('value', new_price_sum);
+ }
+ });
diff --git a/app/views/shared/articles_by/_group_single.html.haml b/app/views/shared/articles_by/_group_single.html.haml
new file mode 100644
index 00000000..c1bf422e
--- /dev/null
+++ b/app/views/shared/articles_by/_group_single.html.haml
@@ -0,0 +1,15 @@
+%tbody{id: "group_order_#{group_order.id}"}
+ - if not defined?(heading) or heading
+ %tr.list-heading
+ %th{colspan: 9}
+ %h4.name= group_order.ordergroup.name
+ - total = 0
+ - for goa in group_order.group_order_articles.ordered.all(:include => :order_article)
+ - total += goa.total_price
+ = render 'shared/articles_by/group_single_goa', goa: goa, edit: (edit rescue nil)
+ %tr{class: cycle('even', 'odd', :name => 'articles')}
+ %th{colspan: 7}= t 'shared.articles_by.price_sum'
+ %th.price_sum{colspan: 2, data: {value: total}}= number_to_currency(total)
+ %tr
+ %th{colspan: 9}
+ - reset_cycle("articles")
diff --git a/app/views/shared/articles_by/_group_single_goa.html.haml b/app/views/shared/articles_by/_group_single_goa.html.haml
new file mode 100644
index 00000000..bf8ca178
--- /dev/null
+++ b/app/views/shared/articles_by/_group_single_goa.html.haml
@@ -0,0 +1,11 @@
+%tr{class: [cycle('even', 'odd', :name => 'articles'), if goa.result == 0 then 'unavailable' end], id: "goa_#{goa.id}"}
+ %td.name= goa.order_article.article.name
+ %td= goa.order_article.article.unit
+ %td.center= "#{goa.quantity} + #{goa.tolerance}"
+ %td.center.input-delta= (edit or true rescue true) ? group_order_article_edit_result(goa) : goa.result
+ %td.symbol ×
+ %td= number_to_currency(goa.order_article.price.fc_price)
+ %td.symbol =
+ %td.price{data: {value: goa.total_price}}= number_to_currency(goa.total_price)
+ %td= pkg_helper goa.order_article.price
+
diff --git a/app/views/shared/articles_by/_groups.html.haml b/app/views/shared/articles_by/_groups.html.haml
new file mode 100644
index 00000000..597b2d0e
--- /dev/null
+++ b/app/views/shared/articles_by/_groups.html.haml
@@ -0,0 +1,17 @@
+%table.table.table-hover.list#articles_by_groups
+ %thead.list-heading
+ %tr
+ %th{:style => "width:40%"}= heading_helper Article, :name
+ %th= heading_helper Article, :unit
+ %th.center
+ %acronym{:title => t('shared.articles.ordered_desc')}= t 'shared.articles.ordered'
+ %th.center
+ %acronym{:title => t('shared.articles.received_desc')}= t 'shared.articles.received'
+ %th.symbol
+ %th= heading_helper Article, :fc_price, short: true
+ %th.symbol
+ %th= t 'shared.articles_by.price'
+ %th= #heading_helper Article, :unit_quantity, short: true
+
+ - for group_order in order.group_orders.ordered
+ = render 'shared/articles_by/group_single', group_order: group_order, edit: (edit rescue nil)
diff --git a/config/locales/de.yml b/config/locales/de.yml
index cefa289f..3d522af0 100644
--- a/config/locales/de.yml
+++ b/config/locales/de.yml
@@ -7,6 +7,7 @@ de:
availability_short: verf.
deposit: Pfand
fc_price: Endpreis
+ fc_price_desc: Preis incl. MwSt, Pfand und Foodcoop-Aufschlag
fc_price_short: FC-Preis
fc_share: FoodCoop-Aufschlag
fc_share_short: FC-Aufschlag
@@ -1285,9 +1286,8 @@ de:
ordergroup: Bestellgruppe
price: Gesamtpreis
articles_by_groups:
- fc_price: FC-Preis
- fc_price_desc: Preis incl. MwSt, Pfand und Foodcoop-Aufschlag
price: Gesamtpreis
+ price_sum: Summe
unit_quantity: GebGr
unit_quantity_desc: Gebindegröße
group:
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 7403cbaf..9336ff4d 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -7,6 +7,7 @@ en:
availability_short: avail.
deposit: Deposit
fc_price: FoodCoop price
+ fc_price_desc: Price including taxes, deposit and Foodcoop-charge
fc_price_short: FC price
fc_share: FoodCoop margin
fc_share_short: FC margin
@@ -599,10 +600,6 @@ en:
ordergroup:
remove: Remove
remove_group: Remove group
- group_order_articles:
- form:
- amount_change_for: Change amount for %{article}
- result_hint: ! 'Unit: %{unit}'
index:
amount_fc: Amount(FC)
end: End
@@ -736,6 +733,10 @@ en:
error_general: The order couldn’t be updated due to a bug.
error_stale: Someone else has ordered in the meantime, couldn't update the order.
notice: The order was saved.
+ group_order_articles:
+ form:
+ amount_change_for: Change amount for %{article}
+ result_hint: ! 'Unit: %{unit}'
helpers:
application:
edit_user: Edit user
@@ -1293,15 +1294,9 @@ en:
ordered_desc: Number of articles as ordered by member (amount + tolerance)
received: Received
received_desc: Number of articles that (will be) received by member
- articles_by_articles:
- ordergroup: Ordergroup
+ articles_by:
price: Total price
- articles_by_groups:
- fc_price: FC-Price
- fc_price_desc: Price including taxes, deposit and Foodcoop-charge
- price: Total price
- unit_quantity: Lot quantity
- unit_quantity_desc: How many units per lot.
+ price_sum: Sum
group:
access: Access to
activated: activated
diff --git a/config/routes.rb b/config/routes.rb
index 67497f9e..86e49d23 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -53,6 +53,8 @@ Foodsoft::Application.routes.draw do
get :archive, :on => :collection
end
+ resources :group_order_articles
+
resources :order_comments, :only => [:new, :create]
############ Foodcoop orga
@@ -155,12 +157,6 @@ Foodsoft::Application.routes.draw do
end
end
- resources :group_order_articles do
- member do
- put :update_result
- end
- end
-
resources :invoices
resources :ordergroups, :only => [:index] do
diff --git a/spec/integration/balancing_spec.rb b/spec/integration/balancing_spec.rb
index eba7973e..05c7bfd3 100644
--- a/spec/integration/balancing_spec.rb
+++ b/spec/integration/balancing_spec.rb
@@ -2,6 +2,7 @@ require_relative '../spec_helper'
describe 'settling an order', :type => :feature do
let(:admin) { create :user, groups:[create(:workgroup, role_finance: true)] }
+ let(:user) { create :user, groups:[create(:ordergroup)] }
let(:supplier) { create :supplier }
let(:article) { create :article, supplier: supplier, unit_quantity: 1 }
let(:order) { create :order, supplier: supplier, article_ids: [article.id] } # need to ref article
@@ -43,10 +44,10 @@ describe 'settling an order', :type => :feature do
expect(page).to have_content(go1.ordergroup.name)
expect(page).to have_content(go2.ordergroup.name)
# and that their order results match what we expect
- expect(page).to have_selector("#group_order_article_#{goa1.id}_quantity")
- expect(find("#group_order_article_#{goa1.id}_quantity").text.to_f).to eq(3)
- expect(page).to have_selector("#group_order_article_#{goa2.id}_quantity")
- expect(find("#group_order_article_#{goa2.id}_quantity").text.to_f).to eq(1)
+ expect(page).to have_selector("#r_#{goa1.id}")
+ expect(find("#r_#{goa1.id}").value.to_f).to eq(3)
+ expect(page).to have_selector("#r_#{goa2.id}")
+ expect(find("#r_#{goa2.id}").value.to_f).to eq(1)
end
end
@@ -120,6 +121,48 @@ describe 'settling an order', :type => :feature do
expect(oa.units_to_order).to eq(0)
end
+ it 'can add an ordergroup to an order article' do
+ user # need to reference user before "new article" dialog is loaded
+ click_link article.name
+ within("#group_order_articles_#{oa.id}") do
+ click_link I18n.t('finance.balancing.group_order_articles.add_group')
+ end
+ expect(page).to have_selector('form#new_group_order_article')
+ within('#new_group_order_article') do
+ select user.ordergroup.name, :from => 'group_order_article_ordergroup_id'
+ fill_in 'group_order_article_result', :with => 8
+ find('input[type="submit"]').click
+ end
+ expect(page).to have_content(user.ordergroup.name)
+ goa = GroupOrderArticle.last
+ expect(goa).to_not be_nil
+ expect(goa.result).to eq 8
+ expect(page).to have_selector("#group_order_article_#{goa.id}")
+ expect(find("#r_#{goa.id}").value.to_f).to eq 8
+ end
+
+ it 'can modify an ordergroup result' do
+ click_link article.name
+ within("#group_order_articles_#{oa.id}") do
+ fill_in "r_#{goa1.id}", :with => 5
+ # leave input box and wait a bit so that update is sent using ajax
+ find("#r_#{goa1.id}").native.send_keys :tab
+ sleep 1
+ end
+ expect(goa1.reload.result).to eq 5
+ expect(find("#group_order_articles_#{oa.id} tfoot td:nth-child(3)").text.to_f).to eq 6
+ end
+
+ it 'can modify an ordergroup result using the + button' do
+ click_link article.name
+ within("#group_order_article_#{goa1.id}") do
+ 4.times { find('button[data-increment]').click }
+ sleep 1
+ end
+ expect(goa1.reload.result).to eq 7
+ expect(find("#group_order_articles_#{oa.id} tfoot td:nth-child(3)").text.to_f).to eq 8
+ end
+
end
end
diff --git a/spec/support/shared_database.rb b/spec/support/shared_database.rb
index 55f8f2b3..5441c78d 100644
--- a/spec/support/shared_database.rb
+++ b/spec/support/shared_database.rb
@@ -5,7 +5,7 @@ class ActiveRecord::Base
@@shared_connection = nil
def self.connection
- @@shared_connection || retrieve_connection
+ @@shared_connection || ConnectionPool::Wrapper.new(:size => 1) { retrieve_connection }
end
end
# Forces all threads to share the same connection. This works on
diff --git a/vendor/assets/javascripts/jquery.observe_field.js b/vendor/assets/javascripts/jquery.observe_field.js
deleted file mode 100644
index b2d72e58..00000000
--- a/vendor/assets/javascripts/jquery.observe_field.js
+++ /dev/null
@@ -1,39 +0,0 @@
-// jquery.observe_field.js
-
-
-(function( $ ){
-
- jQuery.fn.observe_field = function(frequency, callback) {
-
- frequency = frequency * 1000; // translate to milliseconds
-
- return this.each(function(){
- var $this = $(this);
- var prev = $this.val();
-
- var check = function() {
- var val = $this.val();
- if(prev != val){
- prev = val;
- $this.map(callback); // invokes the callback on $this
- }
- };
-
- var reset = function() {
- if(ti){
- clearInterval(ti);
- ti = setInterval(check, frequency);
- }
- };
-
- check();
- var ti = setInterval(check, frequency); // invoke check periodically
-
- // reset counter after user interaction
- $this.bind('keyup click mousemove', reset); //mousemove is for selects
- });
-
- };
-
-})( jQuery );
-