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 ); -