Merge pull request #255 from wvengen/feature-orders_group_edit

Edit order_group_article result in orders screen
This commit is contained in:
wvengen 2014-02-12 11:49:48 +01:00
commit 0128f72103
38 changed files with 421 additions and 225 deletions

View file

@ -86,6 +86,7 @@ group :test do
# webkit and poltergeist don't seem to work yet # webkit and poltergeist don't seem to work yet
gem 'selenium-webdriver' gem 'selenium-webdriver'
gem 'database_cleaner' gem 'database_cleaner'
gem 'connection_pool'
# need to include rspec components before i18n-spec or rake fails in test environment # need to include rspec components before i18n-spec or rake fails in test environment
gem 'rspec-core' gem 'rspec-core'
gem 'rspec-expectations' gem 'rspec-expectations'

View file

@ -114,6 +114,7 @@ GEM
execjs execjs
coffee-script-source (1.6.3) coffee-script-source (1.6.3)
commonjs (0.2.7) commonjs (0.2.7)
connection_pool (1.2.0)
content_for_in_controllers (0.0.2) content_for_in_controllers (0.0.2)
coveralls (0.7.0) coveralls (0.7.0)
multi_json (~> 1.3) multi_json (~> 1.3)
@ -387,6 +388,7 @@ DEPENDENCIES
client_side_validations client_side_validations
client_side_validations-simple_form client_side_validations-simple_form
coffee-rails (~> 3.2.1) coffee-rails (~> 3.2.1)
connection_pool
coveralls coveralls
daemons daemons
database_cleaner database_cleaner

View file

@ -7,7 +7,6 @@
//= require bootstrap-datepicker/locales/bootstrap-datepicker.de //= require bootstrap-datepicker/locales/bootstrap-datepicker.de
//= require bootstrap-datepicker/locales/bootstrap-datepicker.nl //= require bootstrap-datepicker/locales/bootstrap-datepicker.nl
//= require bootstrap-datepicker/locales/bootstrap-datepicker.fr //= require bootstrap-datepicker/locales/bootstrap-datepicker.fr
//= require jquery.observe_field
//= require list //= require list
//= require list.unlist //= require list.unlist
//= require list.delay //= require list.delay
@ -20,6 +19,7 @@
//= require ordering //= require ordering
//= require stupidtable //= require stupidtable
//= require touchclick //= require touchclick
//= require delta_input
// Load following statements, when DOM is ready // Load following statements, when DOM is ready
$(function() { $(function() {
@ -69,17 +69,32 @@ $(function() {
return false; return false;
}); });
// Submit form when changing text of an input field // Submit form when clicking on checkbox
// Use jquery observe_field plugin $(document).on('click', 'form[data-submit-onchange] input[type=checkbox]:not(input[data-ignore-onchange])', function() {
$('form[data-submit-onchange] input[type=text]').each(function() { $(this).parents('form').submit();
$(this).observe_field(1, function() {
$(this).parents('form').submit();
});
}); });
// Submit form when clicking on checkbox // Submit form when changing text of an input field.
$('form[data-submit-onchange] input[type=checkbox]:not(input[data-ignore-onchange])').click(function() { // Wubmission will be done after 500ms of not typed, unless data-submit-onchange=changed,
$(this).parents('form').submit(); // 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() { $('[data-redirect-to]').bind('change', function() {

View file

@ -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;
}

View file

@ -1,5 +1,7 @@
@import "twitter/bootstrap/bootstrap"; @import "twitter/bootstrap/bootstrap";
@import "twitter/bootstrap/responsive"; @import "twitter/bootstrap/responsive";
@import "delta_input";
body { body {
padding-top: 10px; padding-top: 10px;
} }
@ -104,7 +106,7 @@ table {
content: ' \25B2'; content: ' \25B2';
} }
tr.article-category { tr.list-heading {
background-color: #efefef; background-color: #efefef;
td:first-child { td:first-child {
text-align: left; text-align: left;
@ -126,6 +128,10 @@ table {
} }
} }
.center, td.center, th.center {
text-align: center;
}
// Tasks .. // Tasks ..
.accepted { .accepted {
color: #468847; color: #468847;
@ -238,6 +244,27 @@ tr.unavailable {
min-width: 3.5em; 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 // ********* Tweaks & fixes
// Fix bootstrap dropdown menu on mobile // Fix bootstrap dropdown menu on mobile

View file

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

View file

@ -1,6 +1,7 @@
class Finance::GroupOrderArticlesController < ApplicationController class GroupOrderArticlesController < ApplicationController
before_filter :authenticate_finance 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 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 end
def create 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]) @group_order_article = GroupOrderArticle.new(params[:group_order_article])
@order_article = @group_order_article.order_article @order_article = @group_order_article.order_article
@ -21,59 +24,38 @@ class Finance::GroupOrderArticlesController < ApplicationController
@group_order_article = goa @group_order_article = goa
update_summaries(@group_order_article) update_summaries(@group_order_article)
render :update render :create
elsif @group_order_article.save elsif @group_order_article.save
update_summaries(@group_order_article) update_summaries(@group_order_article)
render :update render :create
else # Validation failed, show form else # Validation failed, show form
render :new render :new
end end
end end
def edit
@group_order_article = GroupOrderArticle.find(params[:id])
@order_article = @group_order_article.order_article
end
def update def update
@group_order_article = GroupOrderArticle.find(params[:id]) if params[:delta]
@order_article = @group_order_article.order_article @group_order_article.update_attribute :result, [@group_order_article.result + params[:delta].to_f, 0].max
if @group_order_article.update_attributes(params[:group_order_article])
update_summaries(@group_order_article)
else else
render :edit @group_order_article.update_attributes(params[:group_order_article])
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
end end
update_summaries(group_order_article) update_summaries(@group_order_article)
render :update render :update
end end
def destroy def destroy
group_order_article = GroupOrderArticle.find(params[:id])
# only destroy if quantity and tolerance was zero already, so that we don't # only destroy if quantity and tolerance was zero already, so that we don't
# lose what the user ordered, if any # lose what the user ordered, if any
if group_order_article.quantity > 0 or group_order_article.tolerance >0 if @group_order_article.quantity > 0 or @group_order_article.tolerance >0
group_order_article.update_attribute(:result, 0) @group_order_article.update_attribute(:result, 0)
else else
group_order_article.destroy @group_order_article.destroy
end end
update_summaries(group_order_article) update_summaries(@group_order_article)
@order_article = group_order_article.order_article
render :update render :update
end end
@ -86,4 +68,8 @@ class Finance::GroupOrderArticlesController < ApplicationController
# Update units_to_order of order_article # Update units_to_order of order_article
group_order_article.order_article.update_results! if group_order_article.order_article.article.is_a?(StockArticle) group_order_article.order_article.update_results! if group_order_article.order_article.article.is_a?(StockArticle)
end end
def find_group_order_article
@group_order_article = GroupOrderArticle.find(params[:id])
end
end end

View file

@ -13,10 +13,10 @@ class OrdersController < ApplicationController
@per_page = 15 @per_page = 15
if params['sort'] if params['sort']
sort = case params['sort'] sort = case params['sort']
when "supplier" then "suppliers.name, ends DESC" when "supplier" then "suppliers.name, ends DESC"
when "ends" then "ends DESC" when "ends" then "ends DESC"
when "supplier_reverse" then "suppliers.name DESC" when "supplier_reverse" then "suppliers.name DESC"
when "ends_reverse" then "ends" when "ends_reverse" then "ends"
end end
else else
sort = "ends DESC" sort = "ends DESC"
@ -30,9 +30,9 @@ class OrdersController < ApplicationController
@order= Order.find(params[:id]) @order= Order.find(params[:id])
@view = (params[:view] or 'default').gsub(/[^-_a-zA-Z0-9]/, '') @view = (params[:view] or 'default').gsub(/[^-_a-zA-Z0-9]/, '')
@partial = case @view @partial = case @view
when 'default' then 'articles' when 'default' then 'articles'
when 'groups'then 'shared/articles_by_groups' when 'groups' then 'shared/articles_by/groups'
when 'articles'then 'shared/articles_by_articles' when 'articles' then 'shared/articles_by/articles'
else 'articles' else 'articles'
end end
@ -43,10 +43,10 @@ class OrdersController < ApplicationController
end end
format.pdf do format.pdf do
pdf = case params[:document] pdf = case params[:document]
when 'groups' then OrderByGroups.new(@order) when 'groups' then OrderByGroups.new(@order)
when 'articles' then OrderByArticles.new(@order) when 'articles' then OrderByArticles.new(@order)
when 'fax' then OrderFax.new(@order) when 'fax' then OrderFax.new(@order)
when 'matrix' then OrderMatrix.new(@order) when 'matrix' then OrderMatrix.new(@order)
end end
send_data pdf.to_pdf, filename: pdf.filename, type: 'application/pdf' send_data pdf.to_pdf, filename: pdf.filename, type: 'application/pdf'
end end
@ -120,13 +120,11 @@ class OrdersController < ApplicationController
def receive_on_order_article_create # See publish/subscribe design pattern in /doc. def receive_on_order_article_create # See publish/subscribe design pattern in /doc.
@order_article = OrderArticle.find(params[:order_article_id]) @order_article = OrderArticle.find(params[:order_article_id])
render :layout => false render :layout => false
end end
def receive_on_order_article_update # See publish/subscribe design pattern in /doc. def receive_on_order_article_update # See publish/subscribe design pattern in /doc.
@order_article = OrderArticle.find(params[:order_article_id]) @order_article = OrderArticle.find(params[:order_article_id])
render :layout => false render :layout => false
end end

View file

@ -78,14 +78,19 @@ module ApplicationHelper
# When the 'short' option is true, abbreviations will be used: # When the 'short' option is true, abbreviations will be used:
# When there is a non-empty model attribute 'foo', it looks for # When there is a non-empty model attribute 'foo', it looks for
# the model attribute translation 'foo_short' and use that as # 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. # Other options are passed through to I18n.
def heading_helper(model, attribute, options = {}) 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) s = model.human_attribute_name(attribute, i18nopts)
if options[:short] 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})) sshort = model.human_attribute_name("#{attribute}_short".to_sym, options.merge({fallback: true, default: '', count: 2}))
s = raw "<abbr title='#{s}'>#{sshort}</abbr>" unless sshort.blank? s = raw "<abbr title='#{desc}'>#{sshort}</abbr>" unless sshort.blank?
end end
s s
end end

View file

@ -5,9 +5,9 @@ module Finance::BalancingHelper
when 'edit_results' then when 'edit_results' then
'edit_results_by_articles' 'edit_results_by_articles'
when 'groups_overview' then when 'groups_overview' then
'shared/articles_by_groups' 'shared/articles_by/groups'
when 'articles_overview' then when 'articles_overview' then
'shared/articles_by_articles' 'shared/articles_by/articles'
end end
end end
end end

View file

@ -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

View file

@ -18,7 +18,7 @@ module OrdersHelper
options_for_select(options) options_for_select(options)
end end
# "1 ordered units, 2 billed, 2 received" # "1×2 ordered, 2×2 billed, 2×2 received"
def units_history_line(order_article, options={}) def units_history_line(order_article, options={})
if order_article.order.open? if order_article.order.open?
nil nil
@ -26,10 +26,9 @@ module OrdersHelper
units_info = [] units_info = []
[:units_to_order, :units_billed, :units_received].map do |unit| [:units_to_order, :units_billed, :units_received].map do |unit|
if n = order_article.send(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 = n.to_s + ' '
line += pkg_helper(order_article.price) + ' ' unless options[:plain] or n == 0 line += pkg_helper(order_article.price, options) + ' ' unless n == 0
line += OrderArticle.human_attribute_name(i18nkey, count: n) line += OrderArticle.human_attribute_name("#{unit}_short", count: n)
units_info << line units_info << line
end end
end end
@ -39,13 +38,16 @@ module OrdersHelper
# can be article or article_price # can be article or article_price
# icon: `false` to not show the icon # 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 # soft_uq: `true` to hide unit quantity specifier on small screens
# sensible in tables with multiple columns calling `pkg_helper` # sensible in tables with multiple columns calling `pkg_helper`
def pkg_helper(article, options={}) def pkg_helper(article, options={})
return '' if not article or article.unit_quantity == 1 return '' if not article or article.unit_quantity == 1
uq_text = "&times; #{article.unit_quantity}".html_safe uq_text = "× #{article.unit_quantity}"
uq_text = content_tag(:span, uq_text, class: 'hidden-phone') if options[:soft_uq] 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) pkg_helper_icon(uq_text)
else else
pkg_helper_icon(uq_text, tag: :span) pkg_helper_icon(uq_text, tag: :span)

24
app/inputs/delta_input.rb Normal file
View file

@ -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

View file

@ -4,38 +4,31 @@
%tr %tr
%td %td
%td{:style => "width:8em"}= Ordergroup.model_name.human %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= t('.total')
%td{:colspan => "3",:style => "width:14em"} %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' remote: true, class: 'btn btn-mini'
%tbody %tbody
- totals = {result: 0}
- for group_order_article in order_article.group_order_articles.select { |goa| goa.result > 0 } - for group_order_article in order_article.group_order_articles.select { |goa| goa.result > 0 }
%tr[group_order_article] %tr[group_order_article]
%td %td
%td{:style=>"width:50%"} %td{:style=>"width:50%"}
= group_order_article.group_order.ordergroup.name = group_order_article.group_order.ordergroup.name
%td{:id => "group_order_article_#{group_order_article.id}_quantity", :style => "white-space:nowrap"} %td.center= group_order_article_edit_result(group_order_article)
= group_order_article.result %td.numeric= number_to_currency(group_order_article.order_article.price.fc_price * 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.actions{:style=>"width:1em"} %td.actions{:style=>"width:1em"}
= link_to t('ui.edit'), edit_finance_group_order_article_path(group_order_article), remote: true, = link_to t('ui.delete'), group_order_article_path(group_order_article), method: :delete,
class: 'btn btn-mini'
%td.actions{:style=>"width:1em"}
= link_to t('ui.delete'), finance_group_order_article_path(group_order_article), method: :delete,
remote: true, class: 'btn btn-mini btn-danger' remote: true, class: 'btn btn-mini btn-danger'
%td %td
- totals[:result] += group_order_article.result
%tfoot %tfoot
%tr %tr
%td %td
%td{:style => "width:8em"}= t('.total_fc') %td{:style => "width:8em"}= t('.total_fc')
%td{:id => "group_orders_sum_quantity_#{order_article.id}"} %td.center= totals[:result]
= order_article.group_orders_sum[:quantity] %td.numeric= number_to_currency(order_article.group_orders_sum[:price])
%td.numeric{:id => "group_orders_sum_price_#{order_article.id}"}
= number_to_currency(order_article.group_orders_sum[:price])
%td{:colspan => "3"} %td{:colspan => "3"}

View file

@ -3,7 +3,7 @@
$(function() { $(function() {
// Subscribe to database changes. // Subscribe to database changes.
// See publish/subscribe design pattern in /doc. // See publish/subscribe design pattern in /doc.
$(document).on('OrderArticle#update', function(e) { $(document).on('OrderArticle#update GroupOrderArticle#create GroupOrderArticle#update', function(e) {
$.ajax({ $.ajax({
url: '#{new_on_order_article_update_finance_order_path(@order)}', url: '#{new_on_order_article_update_finance_order_path(@order)}',
type: 'get', type: 'get',
@ -21,6 +21,7 @@
}); });
}); });
}); });
= render 'shared/articles_by/common', order: @order
- title t('.title', name: @order.name) - title t('.title', name: @order.name)

View file

@ -1,2 +0,0 @@
$('#modalContainer').html('#{j(render("form"))}');
$('#modalContainer').modal();

View file

@ -1,2 +0,0 @@
$('#modalContainer').html('#{j(render("form"))}');
$('#modalContainer').modal();

View file

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

View file

@ -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 = form.hidden_field :order_article_id
.modal-header .modal-header
= link_to t('ui.marks.close').html_safe, '#', class: 'close', data: {dismiss: 'modal'} = link_to t('ui.marks.close').html_safe, '#', class: 'close', data: {dismiss: 'modal'}
%h3= t('.amount_change_for', article: @order_article.article.name) %h3= t('.amount_change_for', article: @order_article.article.name)
.modal-body .modal-body
= form.input :ordergroup_id, as: :select, collection: Ordergroup.all.map { |g| [g.name, g.id] } = 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 .modal-footer
= link_to t('ui.close'), '#', class: 'btn', data: {dismiss: 'modal'} = link_to t('ui.close'), '#', class: 'btn', data: {dismiss: 'modal'}
= form.submit t('ui.save'), class: 'btn btn-primary' = form.submit t('ui.save'), class: 'btn btn-primary'

View file

@ -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 %>
});

View file

@ -0,0 +1,2 @@
$('#modalContainer').html('<%= j render("form") %>');
$('#modalContainer').modal();

View file

@ -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 %>
});

View file

@ -98,3 +98,5 @@
$(function() { $(function() {
activate_search('#{j @view}', '#{j t(".search_placeholder.#{@view}")}'); activate_search('#{j @view}', '#{j t(".search_placeholder.#{@view}")}');
}); });
= render 'shared/articles_by/common', order: @order

View file

@ -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')

View file

@ -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")

View file

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

View file

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

View file

@ -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}

View file

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

View file

@ -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")

View file

@ -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 &times;
%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

View file

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

View file

@ -7,6 +7,7 @@ de:
availability_short: verf. availability_short: verf.
deposit: Pfand deposit: Pfand
fc_price: Endpreis fc_price: Endpreis
fc_price_desc: Preis incl. MwSt, Pfand und Foodcoop-Aufschlag
fc_price_short: FC-Preis fc_price_short: FC-Preis
fc_share: FoodCoop-Aufschlag fc_share: FoodCoop-Aufschlag
fc_share_short: FC-Aufschlag fc_share_short: FC-Aufschlag
@ -1285,9 +1286,8 @@ de:
ordergroup: Bestellgruppe ordergroup: Bestellgruppe
price: Gesamtpreis price: Gesamtpreis
articles_by_groups: articles_by_groups:
fc_price: FC-Preis
fc_price_desc: Preis incl. MwSt, Pfand und Foodcoop-Aufschlag
price: Gesamtpreis price: Gesamtpreis
price_sum: Summe
unit_quantity: GebGr unit_quantity: GebGr
unit_quantity_desc: Gebindegröße unit_quantity_desc: Gebindegröße
group: group:

View file

@ -7,6 +7,7 @@ en:
availability_short: avail. availability_short: avail.
deposit: Deposit deposit: Deposit
fc_price: FoodCoop price fc_price: FoodCoop price
fc_price_desc: Price including taxes, deposit and Foodcoop-charge
fc_price_short: FC price fc_price_short: FC price
fc_share: FoodCoop margin fc_share: FoodCoop margin
fc_share_short: FC margin fc_share_short: FC margin
@ -599,10 +600,6 @@ en:
ordergroup: ordergroup:
remove: Remove remove: Remove
remove_group: Remove group remove_group: Remove group
group_order_articles:
form:
amount_change_for: Change amount for %{article}
result_hint: ! 'Unit: %{unit}'
index: index:
amount_fc: Amount(FC) amount_fc: Amount(FC)
end: End end: End
@ -736,6 +733,10 @@ en:
error_general: The order couldnt be updated due to a bug. error_general: The order couldnt be updated due to a bug.
error_stale: Someone else has ordered in the meantime, couldn't update the order. error_stale: Someone else has ordered in the meantime, couldn't update the order.
notice: The order was saved. notice: The order was saved.
group_order_articles:
form:
amount_change_for: Change amount for %{article}
result_hint: ! 'Unit: %{unit}'
helpers: helpers:
application: application:
edit_user: Edit user edit_user: Edit user
@ -1293,15 +1294,9 @@ en:
ordered_desc: Number of articles as ordered by member (amount + tolerance) ordered_desc: Number of articles as ordered by member (amount + tolerance)
received: Received received: Received
received_desc: Number of articles that (will be) received by member received_desc: Number of articles that (will be) received by member
articles_by_articles: articles_by:
ordergroup: Ordergroup
price: Total price price: Total price
articles_by_groups: price_sum: Sum
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.
group: group:
access: Access to access: Access to
activated: activated activated: activated

View file

@ -53,6 +53,8 @@ Foodsoft::Application.routes.draw do
get :archive, :on => :collection get :archive, :on => :collection
end end
resources :group_order_articles
resources :order_comments, :only => [:new, :create] resources :order_comments, :only => [:new, :create]
############ Foodcoop orga ############ Foodcoop orga
@ -155,12 +157,6 @@ Foodsoft::Application.routes.draw do
end end
end end
resources :group_order_articles do
member do
put :update_result
end
end
resources :invoices resources :invoices
resources :ordergroups, :only => [:index] do resources :ordergroups, :only => [:index] do

View file

@ -2,6 +2,7 @@ require_relative '../spec_helper'
describe 'settling an order', :type => :feature do describe 'settling an order', :type => :feature do
let(:admin) { create :user, groups:[create(:workgroup, role_finance: true)] } let(:admin) { create :user, groups:[create(:workgroup, role_finance: true)] }
let(:user) { create :user, groups:[create(:ordergroup)] }
let(:supplier) { create :supplier } let(:supplier) { create :supplier }
let(:article) { create :article, supplier: supplier, unit_quantity: 1 } let(:article) { create :article, supplier: supplier, unit_quantity: 1 }
let(:order) { create :order, supplier: supplier, article_ids: [article.id] } # need to ref article 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(go1.ordergroup.name)
expect(page).to have_content(go2.ordergroup.name) expect(page).to have_content(go2.ordergroup.name)
# and that their order results match what we expect # and that their order results match what we expect
expect(page).to have_selector("#group_order_article_#{goa1.id}_quantity") expect(page).to have_selector("#r_#{goa1.id}")
expect(find("#group_order_article_#{goa1.id}_quantity").text.to_f).to eq(3) expect(find("#r_#{goa1.id}").value.to_f).to eq(3)
expect(page).to have_selector("#group_order_article_#{goa2.id}_quantity") expect(page).to have_selector("#r_#{goa2.id}")
expect(find("#group_order_article_#{goa2.id}_quantity").text.to_f).to eq(1) expect(find("#r_#{goa2.id}").value.to_f).to eq(1)
end end
end end
@ -120,6 +121,48 @@ describe 'settling an order', :type => :feature do
expect(oa.units_to_order).to eq(0) expect(oa.units_to_order).to eq(0)
end 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
end end

View file

@ -5,7 +5,7 @@ class ActiveRecord::Base
@@shared_connection = nil @@shared_connection = nil
def self.connection def self.connection
@@shared_connection || retrieve_connection @@shared_connection || ConnectionPool::Wrapper.new(:size => 1) { retrieve_connection }
end end
end end
# Forces all threads to share the same connection. This works on # Forces all threads to share the same connection. This works on

View file

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