Merge pull request #387 from foodcoops/feature/boxfills
Add optional boxfill phase to orders (+ bootstrap buttons)
This commit is contained in:
commit
227ca0dd84
16 changed files with 271 additions and 76 deletions
|
@ -5,11 +5,11 @@
|
||||||
//
|
//
|
||||||
// Call setDecimalSeparator(char) to overwrite the default character "." with a localized value.
|
// Call setDecimalSeparator(char) to overwrite the default character "." with a localized value.
|
||||||
|
|
||||||
var modified = false // indicates if anything has been clicked on this page
|
var modified = false; // indicates if anything has been clicked on this page
|
||||||
var groupBalance = 0; // available group money
|
var groupBalance = 0; // available group money
|
||||||
var minimumBalance = 0; // minimum group balance for the order to be succesful
|
var minimumBalance = 0; // minimum group balance for the order to be succesful
|
||||||
var toleranceIsCostly = true; // default tolerance behaviour
|
var toleranceIsCostly = true; // default tolerance behaviour
|
||||||
var isStockit = false; // Wheter the order is from stock oder normal supplier
|
var isStockit = false; // Whether the order is from stock oder normal supplier
|
||||||
|
|
||||||
// Article data arrays:
|
// Article data arrays:
|
||||||
var price = new Array();
|
var price = new Array();
|
||||||
|
@ -48,27 +48,37 @@ function addData(orderArticleId, itemPrice, itemUnit, itemSubtotal, itemQuantity
|
||||||
}
|
}
|
||||||
|
|
||||||
function increaseQuantity(item) {
|
function increaseQuantity(item) {
|
||||||
var value = Number($('#q_' + item).val()) + 1;
|
var $el = $('#q_' + item),
|
||||||
|
value = Number($el.val()) + 1,
|
||||||
|
max = $el.data('max');
|
||||||
|
if (value > max) { value = max; }
|
||||||
if (!isStockit || (value <= (quantityAvailable[item] + itemsAllocated[item]))) {
|
if (!isStockit || (value <= (quantityAvailable[item] + itemsAllocated[item]))) {
|
||||||
update(item, value, $('#t_' + item).val());
|
update(item, value, $('#t_' + item).val());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function decreaseQuantity(item) {
|
function decreaseQuantity(item) {
|
||||||
var value = Number($('#q_' + item).val()) - 1;
|
var $el = $('#q_' + item),
|
||||||
if (value >= 0) {
|
value = Number($el.val()) - 1,
|
||||||
|
min = $el.data('min') || 0;
|
||||||
|
if (value >= min) {
|
||||||
update(item, value, $('#t_' + item).val());
|
update(item, value, $('#t_' + item).val());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function increaseTolerance(item) {
|
function increaseTolerance(item) {
|
||||||
var value = Number($('#t_' + item).val()) + 1;
|
var $el = $('#t_' + item),
|
||||||
|
value = Number($el.val()) + 1;
|
||||||
|
max = $el.data('max');
|
||||||
|
if (value > max) { value = max; }
|
||||||
update(item, $('#q_' + item).val(), value);
|
update(item, $('#q_' + item).val(), value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function decreaseTolerance(item) {
|
function decreaseTolerance(item) {
|
||||||
var value = Number($('#t_' + item).val()) - 1;
|
var $el = $('#t_' + item),
|
||||||
if (value >= 0) {
|
value = Number($el.val()) - 1,
|
||||||
|
min = $el.data('min') || 0;
|
||||||
|
if (value >= min) {
|
||||||
update(item, $('#q_' + item).val(), value);
|
update(item, $('#q_' + item).val(), value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -139,9 +149,8 @@ function update(item, quantity, tolerance) {
|
||||||
.removeClass('missing-many missing-few missing-none')
|
.removeClass('missing-many missing-few missing-none')
|
||||||
.addClass(missing_units_css);
|
.addClass(missing_units_css);
|
||||||
|
|
||||||
|
|
||||||
// update balance
|
|
||||||
updateBalance();
|
updateBalance();
|
||||||
|
updateButtons($('#q_'+item).closest('tr'));
|
||||||
}
|
}
|
||||||
|
|
||||||
function calcUnits(unitSize, quantity, tolerance) {
|
function calcUnits(unitSize, quantity, tolerance) {
|
||||||
|
@ -184,21 +193,43 @@ function updateBalance() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateButtons($el) {
|
||||||
|
// enable/disable buttons depending on min/max vs. value
|
||||||
|
$el.find('a[data-increase_quantity]').each(function() {
|
||||||
|
var $q = $el.find('#q_'+$(this).data('increase_quantity'));
|
||||||
|
$(this).toggleClass('disabled', $q.val() >= $q.data('max'));
|
||||||
|
});
|
||||||
|
$el.find('a[data-decrease_quantity]').each(function() {
|
||||||
|
var $q = $el.find('#q_'+$(this).data('decrease_quantity'));
|
||||||
|
$(this).toggleClass('disabled', $q.val() <= ($q.data('min')||0));
|
||||||
|
});
|
||||||
|
$el.find('a[data-increase_tolerance]').each(function() {
|
||||||
|
var $t = $el.find('#t_'+$(this).data('increase_tolerance'));
|
||||||
|
$(this).toggleClass('disabled', $t.val() >= $t.data('max'));
|
||||||
|
});
|
||||||
|
$el.find('a[data-decrease_tolerance]').each(function() {
|
||||||
|
var $t = $el.find('#t_'+$(this).data('decrease_tolerance'));
|
||||||
|
$(this).toggleClass('disabled', $t.val() <= ($t.data('min')||0));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
$(function() {
|
$(function() {
|
||||||
$('input[data-increase_quantity]').on('touchclick', function() {
|
$('a[data-increase_quantity]').on('touchclick', function() {
|
||||||
increaseQuantity($(this).data('increase_quantity'));
|
increaseQuantity($(this).data('increase_quantity'));
|
||||||
});
|
});
|
||||||
$('input[data-decrease_quantity]').on('touchclick', function() {
|
$('a[data-decrease_quantity]').on('touchclick', function() {
|
||||||
decreaseQuantity($(this).data('decrease_quantity'));
|
decreaseQuantity($(this).data('decrease_quantity'));
|
||||||
});
|
});
|
||||||
$('input[data-increase_tolerance]').on('touchclick', function() {
|
$('a[data-increase_tolerance]').on('touchclick', function() {
|
||||||
increaseTolerance($(this).data('increase_tolerance'));
|
increaseTolerance($(this).data('increase_tolerance'));
|
||||||
});
|
});
|
||||||
$('input[data-decrease_tolerance]').on('touchclick', function() {
|
$('a[data-decrease_tolerance]').on('touchclick', function() {
|
||||||
decreaseTolerance($(this).data('decrease_tolerance'));
|
decreaseTolerance($(this).data('decrease_tolerance'));
|
||||||
});
|
});
|
||||||
|
|
||||||
$('a[data-confirm_switch_order]').on('touchclick', function() {
|
$('a[data-confirm_switch_order]').on('touchclick', function() {
|
||||||
return (!modified || confirm(I18n.t('js.ordering.confirm_change')));
|
return (!modified || confirm(I18n.t('js.ordering.confirm_change')));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
updateButtons($(document));
|
||||||
});
|
});
|
||||||
|
|
|
@ -197,10 +197,17 @@ table {
|
||||||
}
|
}
|
||||||
.unused {
|
.unused {
|
||||||
color: @articleUnusedColor;
|
color: @articleUnusedColor;
|
||||||
|
margin-right: 0.5em;
|
||||||
}
|
}
|
||||||
.partused {
|
.partused {
|
||||||
color: @articlePartusedColor;
|
color: @articlePartusedColor;
|
||||||
}
|
}
|
||||||
|
.btn-ordering, .btn-group .btn-ordering {
|
||||||
|
font-size: 10px;
|
||||||
|
line-height: 14px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
margin-top: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
#order-footer, .article-info {
|
#order-footer, .article-info {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
|
|
@ -40,7 +40,7 @@ class Admin::ConfigsController < Admin::BaseController
|
||||||
# turn recurring rules into something palatable
|
# turn recurring rules into something palatable
|
||||||
def parse_recurring_selects!(config)
|
def parse_recurring_selects!(config)
|
||||||
if config
|
if config
|
||||||
for k in [:pickup, :ends] do
|
for k in [:pickup, :boxfill, :ends] do
|
||||||
if config[k]
|
if config[k]
|
||||||
# allow clearing it using dummy value '{}' ('' would break recurring_select)
|
# allow clearing it using dummy value '{}' ('' would break recurring_select)
|
||||||
if config[k][:recurr].present? && config[k][:recurr] != '{}'
|
if config[k][:recurr].present? && config[k][:recurr] != '{}'
|
||||||
|
|
|
@ -54,7 +54,8 @@ module Admin::ConfigsHelper
|
||||||
checked_value = options.delete(:checked_value) || 'true'
|
checked_value = options.delete(:checked_value) || 'true'
|
||||||
unchecked_value = options.delete(:unchecked_value) || 'false'
|
unchecked_value = options.delete(:unchecked_value) || 'false'
|
||||||
options[:checked] = 'checked' if v=options.delete(:value) && v!='false'
|
options[:checked] = 'checked' if v=options.delete(:value) && v!='false'
|
||||||
form.hidden_field(key, value: unchecked_value, as: :hidden) + form.check_box(key, options, checked_value, false)
|
# different key for hidden field so that allow clocking on label focuses the control
|
||||||
|
form.hidden_field(key, id: "#{key}_", value: unchecked_value, as: :hidden) + form.check_box(key, options, checked_value, false)
|
||||||
elsif options[:as] == :select_recurring
|
elsif options[:as] == :select_recurring
|
||||||
options[:value] = FoodsoftDateUtil.rule_from(options[:value])
|
options[:value] = FoodsoftDateUtil.rule_from(options[:value])
|
||||||
options[:rules] ||= []
|
options[:rules] ||= []
|
||||||
|
@ -111,6 +112,13 @@ module Admin::ConfigsHelper
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# @return [String] Tooltip element (span)
|
||||||
|
# @param form [ActionView::Helpers::FormBuilder] Form object.
|
||||||
|
# @param key [Symbol, String] Configuration key of a boolean (e.g. +use_messages+).
|
||||||
|
def config_tooltip(form, key, options={}, &block)
|
||||||
|
content_tag :span, config_input_tooltip_options(form, key, options), &block
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def config_input_tooltip_options(form, key, options)
|
def config_input_tooltip_options(form, key, options)
|
||||||
|
|
|
@ -58,8 +58,10 @@ class GroupOrder < ActiveRecord::Base
|
||||||
group_order_article = group_order_articles.where(order_article_id: order_article.id).first_or_create
|
group_order_article = group_order_articles.where(order_article_id: order_article.id).first_or_create
|
||||||
|
|
||||||
# Get ordered quantities and update group_order_articles/_quantities...
|
# Get ordered quantities and update group_order_articles/_quantities...
|
||||||
|
if group_order_articles_attributes
|
||||||
quantities = group_order_articles_attributes.fetch(order_article.id.to_s, {:quantity => 0, :tolerance => 0})
|
quantities = group_order_articles_attributes.fetch(order_article.id.to_s, {:quantity => 0, :tolerance => 0})
|
||||||
group_order_article.update_quantities(quantities[:quantity].to_i, quantities[:tolerance].to_i)
|
group_order_article.update_quantities(quantities[:quantity].to_i, quantities[:tolerance].to_i)
|
||||||
|
end
|
||||||
|
|
||||||
# Also update results for the order_article
|
# Also update results for the order_article
|
||||||
logger.debug "[save_group_order_articles] update order_article.results!"
|
logger.debug "[save_group_order_articles] update order_article.results!"
|
||||||
|
@ -86,4 +88,3 @@ class GroupOrder < ActiveRecord::Base
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -199,5 +199,3 @@ class GroupOrderArticle < ActiveRecord::Base
|
||||||
result != result_computed unless result.nil?
|
result != result_computed unless result.nil?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -35,7 +35,7 @@ class Order < ActiveRecord::Base
|
||||||
# Allow separate inputs for date and time
|
# Allow separate inputs for date and time
|
||||||
# with workaround for https://github.com/einzige/date_time_attribute/issues/14
|
# with workaround for https://github.com/einzige/date_time_attribute/issues/14
|
||||||
include DateTimeAttributeValidate
|
include DateTimeAttributeValidate
|
||||||
date_time_attribute :starts, :ends
|
date_time_attribute :starts, :boxfill, :ends
|
||||||
|
|
||||||
def stockit?
|
def stockit?
|
||||||
supplier_id == 0
|
supplier_id == 0
|
||||||
|
@ -92,8 +92,16 @@ class Order < ActiveRecord::Base
|
||||||
state == "closed"
|
state == "closed"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def boxfill?
|
||||||
|
FoodsoftConfig[:use_boxfill] && open? && boxfill.present? && boxfill < Time.now
|
||||||
|
end
|
||||||
|
|
||||||
|
def is_boxfill_useful?
|
||||||
|
FoodsoftConfig[:use_boxfill] && supplier.try(:has_tolerance?)
|
||||||
|
end
|
||||||
|
|
||||||
def expired?
|
def expired?
|
||||||
!ends.nil? && ends < Time.now
|
ends.present? && ends < Time.now
|
||||||
end
|
end
|
||||||
|
|
||||||
# sets up first guess of dates when initializing a new object
|
# sets up first guess of dates when initializing a new object
|
||||||
|
@ -105,7 +113,8 @@ class Order < ActiveRecord::Base
|
||||||
last = (DateTime.parse(FoodsoftConfig[:order_schedule][:initial]) rescue nil)
|
last = (DateTime.parse(FoodsoftConfig[:order_schedule][:initial]) rescue nil)
|
||||||
last ||= Order.finished.reorder(:starts).first.try(:starts)
|
last ||= Order.finished.reorder(:starts).first.try(:starts)
|
||||||
last ||= self.starts
|
last ||= self.starts
|
||||||
# adjust end date
|
# adjust boxfill and end date
|
||||||
|
self.boxfill ||= FoodsoftDateUtil.next_occurrence last, self.starts, FoodsoftConfig[:order_schedule][:boxfill] if is_boxfill_useful?
|
||||||
self.ends ||= FoodsoftDateUtil.next_occurrence last, self.starts, FoodsoftConfig[:order_schedule][:ends]
|
self.ends ||= FoodsoftDateUtil.next_occurrence last, self.starts, FoodsoftConfig[:order_schedule][:ends]
|
||||||
end
|
end
|
||||||
self
|
self
|
||||||
|
@ -251,7 +260,9 @@ class Order < ActiveRecord::Base
|
||||||
|
|
||||||
def starts_before_ends
|
def starts_before_ends
|
||||||
delta = Rails.env.test? ? 1 : 0 # since Rails 4.2 tests appear to have time differences, with this validation failing
|
delta = Rails.env.test? ? 1 : 0 # since Rails 4.2 tests appear to have time differences, with this validation failing
|
||||||
errors.add(:ends, I18n.t('orders.model.error_starts_before_ends')) if (ends && starts && ends <= (starts-delta))
|
errors.add(:ends, I18n.t('orders.model.error_starts_before_ends')) if ends && starts && ends <= (starts-delta)
|
||||||
|
errors.add(:ends, I18n.t('orders.model.error_boxfill_before_ends')) if ends && boxfill && ends <= (boxfill-delta)
|
||||||
|
errors.add(:boxfill, I18n.t('orders.model.error_starts_before_boxfill')) if boxfill && starts && boxfill <= (starts-delta)
|
||||||
end
|
end
|
||||||
|
|
||||||
def include_articles
|
def include_articles
|
||||||
|
@ -288,4 +299,3 @@ class Order < ActiveRecord::Base
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -42,10 +42,11 @@ class OrderArticle < ActiveRecord::Base
|
||||||
# Update quantity/tolerance/units_to_order from group_order_articles
|
# Update quantity/tolerance/units_to_order from group_order_articles
|
||||||
def update_results!
|
def update_results!
|
||||||
if order.open?
|
if order.open?
|
||||||
quantity = group_order_articles.collect(&:quantity).sum
|
self.quantity = group_order_articles.collect(&:quantity).sum
|
||||||
tolerance = group_order_articles.collect(&:tolerance).sum
|
self.tolerance = group_order_articles.collect(&:tolerance).sum
|
||||||
update_attributes(:quantity => quantity, :tolerance => tolerance,
|
self.units_to_order = calculate_units_to_order(quantity, tolerance)
|
||||||
:units_to_order => calculate_units_to_order(quantity, tolerance))
|
enforce_boxfill if order.boxfill?
|
||||||
|
save!
|
||||||
elsif order.finished?
|
elsif order.finished?
|
||||||
update_attribute(:units_to_order, group_order_articles.collect(&:result).sum)
|
update_attribute(:units_to_order, group_order_articles.collect(&:result).sum)
|
||||||
end
|
end
|
||||||
|
@ -186,10 +187,11 @@ class OrderArticle < ActiveRecord::Base
|
||||||
|
|
||||||
# @return [Number] Units missing for the last +unit_quantity+ of the article.
|
# @return [Number] Units missing for the last +unit_quantity+ of the article.
|
||||||
def missing_units
|
def missing_units
|
||||||
units = price.unit_quantity - ((quantity % price.unit_quantity) + tolerance)
|
_missing_units(price.unit_quantity, quantity, tolerance)
|
||||||
units = 0 if units < 0
|
end
|
||||||
units = 0 if units == price.unit_quantity
|
|
||||||
units
|
def missing_units_was
|
||||||
|
_missing_units(price.unit_quantity, quantity_was, tolerance_was)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Check if the result of any associated GroupOrderArticle was overridden manually
|
# Check if the result of any associated GroupOrderArticle was overridden manually
|
||||||
|
@ -219,5 +221,26 @@ class OrderArticle < ActiveRecord::Base
|
||||||
order.group_orders.each(&:update_price!)
|
order.group_orders.each(&:update_price!)
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
# Throws an exception when the changed article decreases the amount of filled boxes.
|
||||||
|
def enforce_boxfill
|
||||||
|
# Either nothing changes, or the tolerance increases,
|
||||||
|
# missing_units decreases and the amount doesn't decrease, or
|
||||||
|
# tolerance was moved to quantity. Only then are changes allowed in the boxfill phase.
|
||||||
|
delta_q = quantity - quantity_was
|
||||||
|
delta_t = tolerance - tolerance_was
|
||||||
|
delta_mis = missing_units - missing_units_was
|
||||||
|
delta_box = units_to_order - units_to_order_was
|
||||||
|
unless (delta_q == 0 && delta_t >= 0) ||
|
||||||
|
(delta_mis < 0 && delta_box >= 0 && delta_t >= 0) ||
|
||||||
|
(delta_q > 0 && delta_q == -delta_t)
|
||||||
|
raise ActiveRecord::RecordNotSaved.new("Change not acceptable in boxfill phase for '#{article.name}', sorry.", self)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def _missing_units(unit_quantity, quantity, tolerance)
|
||||||
|
units = unit_quantity - ((quantity % unit_quantity) + tolerance)
|
||||||
|
units = 0 if units < 0
|
||||||
|
units = 0 if units == unit_quantity
|
||||||
|
units
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
|
@ -116,6 +116,11 @@ class Supplier < ActiveRecord::Base
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# @return [Boolean] Whether there are articles that would use tolerance (unit_quantity > 1)
|
||||||
|
def has_tolerance?
|
||||||
|
articles.where('articles.unit_quantity > 1').any?
|
||||||
|
end
|
||||||
|
|
||||||
protected
|
protected
|
||||||
|
|
||||||
# make sure the shared_sync_method is allowed for the shared supplier
|
# make sure the shared_sync_method is allowed for the shared supplier
|
||||||
|
|
|
@ -11,8 +11,18 @@
|
||||||
%span.add-on= t 'number.currency.format.unit'
|
%span.add-on= t 'number.currency.format.unit'
|
||||||
= config_input_field form, :minimum_balance, as: :decimal, class: 'input-small'
|
= config_input_field form, :minimum_balance, as: :decimal, class: 'input-small'
|
||||||
|
|
||||||
|
%h4= t '.schedule_title'
|
||||||
= form.simple_fields_for :order_schedule do |fields|
|
= form.simple_fields_for :order_schedule do |fields|
|
||||||
|
#boxfill-schedule.collapse{class: ('in' if FoodsoftConfig[:use_boxfill])}
|
||||||
|
= fields.simple_fields_for 'boxfill' do |fields|
|
||||||
|
.fold-line
|
||||||
|
= config_input fields, 'recurr', as: :select_recurring, input_html: {class: 'input-xlarge'}
|
||||||
|
= config_input fields, 'time', input_html: {class: 'input-mini'}
|
||||||
= fields.simple_fields_for 'ends' do |fields|
|
= fields.simple_fields_for 'ends' do |fields|
|
||||||
.fold-line
|
.fold-line
|
||||||
= config_input fields, 'recurr', as: :select_recurring, input_html: {class: 'input-xlarge'}, allow_blank: true
|
= config_input fields, 'recurr', as: :select_recurring, input_html: {class: 'input-xlarge'}, allow_blank: true
|
||||||
= config_input fields, 'time', input_html: {class: 'input-mini'}
|
= config_input fields, 'time', input_html: {class: 'input-mini'}
|
||||||
|
-# can't use collapse and tooltip on same element :/
|
||||||
|
= config_input form, :use_boxfill, as: :boolean do
|
||||||
|
= config_tooltip form, :use_boxfill do
|
||||||
|
= config_input_field form, :use_boxfill, as: :boolean, title: '', data: {toggle: 'collapse', target: '#boxfill-schedule'}
|
||||||
|
|
|
@ -102,21 +102,27 @@
|
||||||
%span{id: "missing_units_#{order_article.id}"}= @ordering_data[:order_articles][order_article.id][:missing_units]
|
%span{id: "missing_units_#{order_article.id}"}= @ordering_data[:order_articles][order_article.id][:missing_units]
|
||||||
|
|
||||||
%td.quantity
|
%td.quantity
|
||||||
%input{id: "q_#{order_article.id}", name: "group_order[group_order_articles_attributes][#{order_article.id}][quantity]", size: "2", type: "hidden", value: @ordering_data[:order_articles][order_article.id][:quantity]}/
|
%input{id: "q_#{order_article.id}", name: "group_order[group_order_articles_attributes][#{order_article.id}][quantity]", type: "hidden", value: @ordering_data[:order_articles][order_article.id][:quantity], 'data-min' => (@ordering_data[:order_articles][order_article.id][:quantity] if @order.boxfill?), 'data-max' => (@ordering_data[:order_articles][order_article.id][:quantity]+@ordering_data[:order_articles][order_article.id][:missing_units] if @order.boxfill?)}/
|
||||||
%span.used{id: "q_used_#{order_article.id}"}= @ordering_data[:order_articles][order_article.id][:used_quantity]
|
%span.used{id: "q_used_#{order_article.id}"}= @ordering_data[:order_articles][order_article.id][:used_quantity]
|
||||||
+
|
+
|
||||||
%span.unused{id: "q_unused_#{order_article.id}"}= @ordering_data[:order_articles][order_article.id][:quantity] - @ordering_data[:order_articles][order_article.id][:used_quantity]
|
%span.unused{id: "q_unused_#{order_article.id}"}= @ordering_data[:order_articles][order_article.id][:quantity] - @ordering_data[:order_articles][order_article.id][:used_quantity]
|
||||||
%input{type: 'button', value: '+', 'data-increase_quantity' => order_article.id}
|
.btn-group
|
||||||
%input{type: 'button', value: '-', 'data-decrease_quantity' => order_article.id}
|
%a.btn.btn-ordering{'data-increase_quantity' => order_article.id}
|
||||||
|
%i.icon-plus
|
||||||
|
%a.btn.btn-ordering{'data-decrease_quantity' => order_article.id}
|
||||||
|
%i.icon-minus
|
||||||
|
|
||||||
%td.tolerance{style: ('display:none' if @order.stockit?)}
|
%td.tolerance{style: ('display:none' if @order.stockit?)}
|
||||||
%input{id: "t_#{order_article.id}", name: "group_order[group_order_articles_attributes][#{order_article.id}][tolerance]", size: "2", type: "hidden", value: @ordering_data[:order_articles][order_article.id][:tolerance]}/
|
%input{id: "t_#{order_article.id}", name: "group_order[group_order_articles_attributes][#{order_article.id}][tolerance]", type: "hidden", value: @ordering_data[:order_articles][order_article.id][:tolerance], 'data-min' => (@ordering_data[:order_articles][order_article.id][:tolerance] if @order.boxfill?)}/
|
||||||
- if (@ordering_data[:order_articles][order_article.id][:unit] > 1)
|
- if (@ordering_data[:order_articles][order_article.id][:unit] > 1)
|
||||||
%span.used{id: "t_used_#{order_article.id}"}= @ordering_data[:order_articles][order_article.id][:used_tolerance]
|
%span.used{id: "t_used_#{order_article.id}"}= @ordering_data[:order_articles][order_article.id][:used_tolerance]
|
||||||
+
|
+
|
||||||
%span.unused{id: "t_unused_#{order_article.id}"}= @ordering_data[:order_articles][order_article.id][:tolerance] - @ordering_data[:order_articles][order_article.id][:used_tolerance]
|
%span.unused{id: "t_unused_#{order_article.id}"}= @ordering_data[:order_articles][order_article.id][:tolerance] - @ordering_data[:order_articles][order_article.id][:used_tolerance]
|
||||||
%input{type: 'button', value: '+', 'data-increase_tolerance' => order_article.id}
|
.btn-group
|
||||||
%input{type: 'button', value: '-', 'data-decrease_tolerance' => order_article.id}
|
%a.btn.btn-ordering{'data-increase_tolerance' => order_article.id}
|
||||||
|
%i.icon-plus
|
||||||
|
%a.btn.btn-ordering{'data-decrease_tolerance' => order_article.id}
|
||||||
|
%i.icon-minus
|
||||||
|
|
||||||
%td{id: "td_price_#{order_article.id}", style: "text-align:right; padding-right:10px; width:4em"}
|
%td{id: "td_price_#{order_article.id}", style: "text-align:right; padding-right:10px; width:4em"}
|
||||||
%span{id: "price_#{order_article.id}_display"}= number_to_currency(@ordering_data[:order_articles][order_article.id][:total_price])
|
%span{id: "price_#{order_article.id}_display"}= number_to_currency(@ordering_data[:order_articles][order_article.id][:total_price])
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
= f.hidden_field :supplier_id
|
= f.hidden_field :supplier_id
|
||||||
.fold-line
|
.fold-line
|
||||||
= f.input :starts, as: :date_picker_time
|
= f.input :starts, as: :date_picker_time
|
||||||
|
= f.input :boxfill, as: :date_picker_time if @order.is_boxfill_useful?
|
||||||
= f.input :ends, as: :date_picker_time
|
= f.input :ends, as: :date_picker_time
|
||||||
= f.input :note, input_html: {rows: 2, class: 'input-xxlarge'}
|
= f.input :note, input_html: {rows: 2, class: 'input-xxlarge'}
|
||||||
|
|
||||||
|
|
|
@ -71,6 +71,7 @@ en:
|
||||||
sent_to_all: Send to all members
|
sent_to_all: Send to all members
|
||||||
subject: Subject
|
subject: Subject
|
||||||
order:
|
order:
|
||||||
|
boxfill: Fill boxes after
|
||||||
closed_by: Settled by
|
closed_by: Settled by
|
||||||
created_by: Created by
|
created_by: Created by
|
||||||
ends: Ends at
|
ends: Ends at
|
||||||
|
@ -237,6 +238,8 @@ en:
|
||||||
pdf_title: PDF documents
|
pdf_title: PDF documents
|
||||||
tab_messages:
|
tab_messages:
|
||||||
emails_title: Sending email
|
emails_title: Sending email
|
||||||
|
tab_payment:
|
||||||
|
schedule_title: Ordering schedule
|
||||||
tab_tasks:
|
tab_tasks:
|
||||||
periodic_title: Periodic tasks
|
periodic_title: Periodic tasks
|
||||||
tabs:
|
tabs:
|
||||||
|
@ -478,6 +481,9 @@ en:
|
||||||
ends:
|
ends:
|
||||||
recurr: Schedule for default order closing date.
|
recurr: Schedule for default order closing date.
|
||||||
time: Default time when orders are closed.
|
time: Default time when orders are closed.
|
||||||
|
boxfill:
|
||||||
|
recurr: Schedule for when the box-fill phase starts by default.
|
||||||
|
time: Default time when the box-fill phase of the ordering starts.
|
||||||
initial: Schedule starts at this date.
|
initial: Schedule starts at this date.
|
||||||
page_footer: Shown on each page at the bottom. Enter "blank" to disable the footer completely.
|
page_footer: Shown on each page at the bottom. Enter "blank" to disable the footer completely.
|
||||||
pdf_add_page_breaks:
|
pdf_add_page_breaks:
|
||||||
|
@ -492,6 +498,7 @@ en:
|
||||||
tax_default: Default VAT percentage for new articles.
|
tax_default: Default VAT percentage for new articles.
|
||||||
tolerance_is_costly: Order as much of the member tolerance as possible (compared to only as much needed to fill the last box). Enabling this also includes the tolerance in the total price of the open member order.
|
tolerance_is_costly: Order as much of the member tolerance as possible (compared to only as much needed to fill the last box). Enabling this also includes the tolerance in the total price of the open member order.
|
||||||
use_apple_points: When the apple point system is enabled, members are required to do some tasks to be able to keep ordering.
|
use_apple_points: When the apple point system is enabled, members are required to do some tasks to be able to keep ordering.
|
||||||
|
use_boxfill: When enabled, near end of an order, members are only able to change their order when increases the total amount ordered. This helps to fill any remaining boxes. You still need to set a box-fill date for the orders.
|
||||||
use_messages: Allow members to communicate with each other within Foodsoft.
|
use_messages: Allow members to communicate with each other within Foodsoft.
|
||||||
use_nick: Show and use nicknames instead of real names. When enabling this, please check that each user has a nickname.
|
use_nick: Show and use nicknames instead of real names. When enabling this, please check that each user has a nickname.
|
||||||
use_wiki: Enable editable wiki pages.
|
use_wiki: Enable editable wiki pages.
|
||||||
|
@ -523,6 +530,9 @@ en:
|
||||||
ends:
|
ends:
|
||||||
recurr: Order ends
|
recurr: Order ends
|
||||||
time: time
|
time: time
|
||||||
|
boxfill:
|
||||||
|
recurr: Box fill after
|
||||||
|
time: time
|
||||||
initial: Schedule start
|
initial: Schedule start
|
||||||
page_footer: Page footer
|
page_footer: Page footer
|
||||||
pdf_add_page_breaks: Page breaks
|
pdf_add_page_breaks: Page breaks
|
||||||
|
@ -536,6 +546,7 @@ en:
|
||||||
time_zone: Time zone
|
time_zone: Time zone
|
||||||
tolerance_is_costly: Tolerance is costly
|
tolerance_is_costly: Tolerance is costly
|
||||||
use_apple_points: Apple points
|
use_apple_points: Apple points
|
||||||
|
use_boxfill: Box-fill phase
|
||||||
use_messages: Messages
|
use_messages: Messages
|
||||||
use_nick: Use nicknames
|
use_nick: Use nicknames
|
||||||
use_wiki: Enable wiki
|
use_wiki: Enable wiki
|
||||||
|
@ -1279,6 +1290,8 @@ en:
|
||||||
close_direct_message: Order settled without charging member accounts.
|
close_direct_message: Order settled without charging member accounts.
|
||||||
error_closed: Order was already settled
|
error_closed: Order was already settled
|
||||||
error_nosel: At least one article must be selected. You may want to delete the order instead?
|
error_nosel: At least one article must be selected. You may want to delete the order instead?
|
||||||
|
error_boxfill_before_ends: must be after the box-fill date (or remain empty)
|
||||||
|
error_starts_before_boxfill: must be after the start date (or remain empty)
|
||||||
error_starts_before_ends: must be after the start date (or remain empty)
|
error_starts_before_ends: must be after the start date (or remain empty)
|
||||||
notice_close: 'Order: %{name}, until %{ends}'
|
notice_close: 'Order: %{name}, until %{ends}'
|
||||||
stock: Stock
|
stock: Stock
|
||||||
|
|
5
db/migrate/20150923190747_add_boxfill_to_order.rb
Normal file
5
db/migrate/20150923190747_add_boxfill_to_order.rb
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
class AddBoxfillToOrder < ActiveRecord::Migration
|
||||||
|
def change
|
||||||
|
add_column :orders, :boxfill, :datetime
|
||||||
|
end
|
||||||
|
end
|
23
db/schema.rb
23
db/schema.rb
|
@ -11,7 +11,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema.define(version: 20150301000000) do
|
ActiveRecord::Schema.define(version: 20150923190747) do
|
||||||
|
|
||||||
create_table "article_categories", force: :cascade do |t|
|
create_table "article_categories", force: :cascade do |t|
|
||||||
t.string "name", limit: 255, default: "", null: false
|
t.string "name", limit: 255, default: "", null: false
|
||||||
|
@ -37,7 +37,7 @@ ActiveRecord::Schema.define(version: 20150301000000) do
|
||||||
t.integer "article_category_id", limit: 4, default: 0, null: false
|
t.integer "article_category_id", limit: 4, default: 0, null: false
|
||||||
t.string "unit", limit: 255, default: "", null: false
|
t.string "unit", limit: 255, default: "", null: false
|
||||||
t.string "note", limit: 255
|
t.string "note", limit: 255
|
||||||
t.boolean "availability", limit: 1, default: true, null: false
|
t.boolean "availability", default: true, null: false
|
||||||
t.string "manufacturer", limit: 255
|
t.string "manufacturer", limit: 255
|
||||||
t.string "origin", limit: 255
|
t.string "origin", limit: 255
|
||||||
t.datetime "shared_updated_on"
|
t.datetime "shared_updated_on"
|
||||||
|
@ -61,7 +61,7 @@ ActiveRecord::Schema.define(version: 20150301000000) do
|
||||||
create_table "assignments", force: :cascade do |t|
|
create_table "assignments", force: :cascade do |t|
|
||||||
t.integer "user_id", limit: 4, default: 0, null: false
|
t.integer "user_id", limit: 4, default: 0, null: false
|
||||||
t.integer "task_id", limit: 4, default: 0, null: false
|
t.integer "task_id", limit: 4, default: 0, null: false
|
||||||
t.boolean "accepted", limit: 1, default: false
|
t.boolean "accepted", default: false
|
||||||
end
|
end
|
||||||
|
|
||||||
add_index "assignments", ["user_id", "task_id"], name: "index_assignments_on_user_id_and_task_id", unique: true, using: :btree
|
add_index "assignments", ["user_id", "task_id"], name: "index_assignments_on_user_id_and_task_id", unique: true, using: :btree
|
||||||
|
@ -127,18 +127,18 @@ ActiveRecord::Schema.define(version: 20150301000000) do
|
||||||
t.string "description", limit: 255
|
t.string "description", limit: 255
|
||||||
t.decimal "account_balance", precision: 12, scale: 2, default: 0, null: false
|
t.decimal "account_balance", precision: 12, scale: 2, default: 0, null: false
|
||||||
t.datetime "created_on", null: false
|
t.datetime "created_on", null: false
|
||||||
t.boolean "role_admin", limit: 1, default: false, null: false
|
t.boolean "role_admin", default: false, null: false
|
||||||
t.boolean "role_suppliers", limit: 1, default: false, null: false
|
t.boolean "role_suppliers", default: false, null: false
|
||||||
t.boolean "role_article_meta", limit: 1, default: false, null: false
|
t.boolean "role_article_meta", default: false, null: false
|
||||||
t.boolean "role_finance", limit: 1, default: false, null: false
|
t.boolean "role_finance", default: false, null: false
|
||||||
t.boolean "role_orders", limit: 1, default: false, null: false
|
t.boolean "role_orders", default: false, null: false
|
||||||
t.datetime "deleted_at"
|
t.datetime "deleted_at"
|
||||||
t.string "contact_person", limit: 255
|
t.string "contact_person", limit: 255
|
||||||
t.string "contact_phone", limit: 255
|
t.string "contact_phone", limit: 255
|
||||||
t.string "contact_address", limit: 255
|
t.string "contact_address", limit: 255
|
||||||
t.text "stats", limit: 65535
|
t.text "stats", limit: 65535
|
||||||
t.integer "next_weekly_tasks_number", limit: 4, default: 8
|
t.integer "next_weekly_tasks_number", limit: 4, default: 8
|
||||||
t.boolean "ignore_apple_restriction", limit: 1, default: false
|
t.boolean "ignore_apple_restriction", default: false
|
||||||
end
|
end
|
||||||
|
|
||||||
add_index "groups", ["name"], name: "index_groups_on_name", unique: true, using: :btree
|
add_index "groups", ["name"], name: "index_groups_on_name", unique: true, using: :btree
|
||||||
|
@ -184,7 +184,7 @@ ActiveRecord::Schema.define(version: 20150301000000) do
|
||||||
t.string "subject", limit: 255, null: false
|
t.string "subject", limit: 255, null: false
|
||||||
t.text "body", limit: 65535
|
t.text "body", limit: 65535
|
||||||
t.integer "email_state", limit: 4, default: 0, null: false
|
t.integer "email_state", limit: 4, default: 0, null: false
|
||||||
t.boolean "private", limit: 1, default: false
|
t.boolean "private", default: false
|
||||||
t.datetime "created_at"
|
t.datetime "created_at"
|
||||||
t.integer "reply_to", limit: 4
|
t.integer "reply_to", limit: 4
|
||||||
t.integer "group_id", limit: 4
|
t.integer "group_id", limit: 4
|
||||||
|
@ -224,6 +224,7 @@ ActiveRecord::Schema.define(version: 20150301000000) do
|
||||||
t.integer "updated_by_user_id", limit: 4
|
t.integer "updated_by_user_id", limit: 4
|
||||||
t.decimal "foodcoop_result", precision: 8, scale: 2
|
t.decimal "foodcoop_result", precision: 8, scale: 2
|
||||||
t.integer "created_by_user_id", limit: 4
|
t.integer "created_by_user_id", limit: 4
|
||||||
|
t.datetime "boxfill"
|
||||||
end
|
end
|
||||||
|
|
||||||
add_index "orders", ["state"], name: "index_orders_on_state", using: :btree
|
add_index "orders", ["state"], name: "index_orders_on_state", using: :btree
|
||||||
|
@ -316,7 +317,7 @@ ActiveRecord::Schema.define(version: 20150301000000) do
|
||||||
t.string "name", limit: 255, default: "", null: false
|
t.string "name", limit: 255, default: "", null: false
|
||||||
t.string "description", limit: 255
|
t.string "description", limit: 255
|
||||||
t.date "due_date"
|
t.date "due_date"
|
||||||
t.boolean "done", limit: 1, default: false
|
t.boolean "done", default: false
|
||||||
t.integer "workgroup_id", limit: 4
|
t.integer "workgroup_id", limit: 4
|
||||||
t.datetime "created_on", null: false
|
t.datetime "created_on", null: false
|
||||||
t.datetime "updated_on", null: false
|
t.datetime "updated_on", null: false
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
describe OrderArticle do
|
describe OrderArticle do
|
||||||
let(:order) { FactoryGirl.create :order, article_count: 1 }
|
let(:order) { create :order, article_count: 1 }
|
||||||
let(:oa) { order.order_articles.first }
|
let(:oa) { order.order_articles.first }
|
||||||
|
|
||||||
it 'is not ordered by default' do
|
it 'is not ordered by default' do
|
||||||
|
@ -34,15 +34,15 @@ describe OrderArticle do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'redistribution' do
|
describe 'redistribution' do
|
||||||
let(:admin) { FactoryGirl.create :user, groups:[FactoryGirl.create(:workgroup, role_finance: true)] }
|
let(:admin) { create :user, groups:[create(:workgroup, role_finance: true)] }
|
||||||
let(:article) { FactoryGirl.create :article, unit_quantity: 3 }
|
let(:article) { create :article, unit_quantity: 3 }
|
||||||
let(:order) { FactoryGirl.create :order, article_ids: [article.id] }
|
let(:order) { create :order, article_ids: [article.id] }
|
||||||
let(:go1) { FactoryGirl.create :group_order, order: order }
|
let(:go1) { create :group_order, order: order }
|
||||||
let(:go2) { FactoryGirl.create :group_order, order: order }
|
let(:go2) { create :group_order, order: order }
|
||||||
let(:go3) { FactoryGirl.create :group_order, order: order }
|
let(:go3) { create :group_order, order: order }
|
||||||
let(:goa1) { FactoryGirl.create :group_order_article, group_order: go1, order_article: oa }
|
let(:goa1) { create :group_order_article, group_order: go1, order_article: oa }
|
||||||
let(:goa2) { FactoryGirl.create :group_order_article, group_order: go2, order_article: oa }
|
let(:goa2) { create :group_order_article, group_order: go2, order_article: oa }
|
||||||
let(:goa3) { FactoryGirl.create :group_order_article, group_order: go3, order_article: oa }
|
let(:goa3) { create :group_order_article, group_order: go3, order_article: oa }
|
||||||
|
|
||||||
# set quantities of group_order_articles
|
# set quantities of group_order_articles
|
||||||
def set_quantities(q1, q2, q3)
|
def set_quantities(q1, q2, q3)
|
||||||
|
@ -117,4 +117,80 @@ describe OrderArticle do
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'boxfill' do
|
||||||
|
before { FoodsoftConfig[:use_boxfill] = true }
|
||||||
|
let(:article) { create :article, unit_quantity: 6 }
|
||||||
|
let(:order) { create :order, article_ids: [article.id], starts: 1.week.ago }
|
||||||
|
let(:oa) { order.order_articles.first }
|
||||||
|
let(:go) { create :group_order, order: order }
|
||||||
|
let(:goa) { create :group_order_article, group_order: go, order_article: oa }
|
||||||
|
|
||||||
|
shared_examples "boxfill" do |success, q|
|
||||||
|
# initial situation
|
||||||
|
before do
|
||||||
|
goa.update_quantities *q.keys[0]
|
||||||
|
oa.update_results!; oa.reload
|
||||||
|
end
|
||||||
|
|
||||||
|
# check starting condition
|
||||||
|
it '(before)' do
|
||||||
|
expect([oa.quantity, oa.tolerance, oa.missing_units]).to eq q.keys[1]
|
||||||
|
end
|
||||||
|
|
||||||
|
# actual test
|
||||||
|
it (success ? 'succeeds' : 'fails') do
|
||||||
|
order.update_attributes(boxfill: boxfill_from)
|
||||||
|
|
||||||
|
r = proc {
|
||||||
|
goa.update_quantities *q.values[0]
|
||||||
|
oa.update_results!
|
||||||
|
}
|
||||||
|
if success
|
||||||
|
r.call
|
||||||
|
else
|
||||||
|
expect(r).to raise_error(ActiveRecord::RecordNotSaved)
|
||||||
|
end
|
||||||
|
|
||||||
|
oa.reload
|
||||||
|
expect([oa.quantity, oa.tolerance, oa.missing_units]).to eq q.values[1]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'before the date' do
|
||||||
|
let(:boxfill_from) { 1.hour.from_now }
|
||||||
|
context 'decreasing the missing units' do
|
||||||
|
include_examples "boxfill", true, [6,0]=>[5,0], [6,0,0]=>[5,0,1]
|
||||||
|
end
|
||||||
|
context 'decreasing the tolerance' do
|
||||||
|
include_examples "boxfill", true, [1,2]=>[1,1], [1,2,3]=>[1,1,4]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'after the date' do
|
||||||
|
let(:boxfill_from) { 1.second.ago }
|
||||||
|
context 'changing nothing in particular' do
|
||||||
|
include_examples "boxfill", true, [4,1]=>[4,1], [4,1,1]=>[4,1,1]
|
||||||
|
end
|
||||||
|
context 'increasing missing units' do
|
||||||
|
include_examples "boxfill", false, [3,0]=>[2,0], [3,0,3]=>[3,0,3]
|
||||||
|
end
|
||||||
|
context 'increasing tolerance' do
|
||||||
|
include_examples "boxfill", true, [2,1]=>[2,2], [2,1,3]=>[2,2,2]
|
||||||
|
end
|
||||||
|
context 'decreasing quantity to fix missing units' do
|
||||||
|
include_examples "boxfill", true, [7,0]=>[6,0], [7,0,5]=>[6,0,0]
|
||||||
|
end
|
||||||
|
context 'decreasing quantity keeping missing units equal' do
|
||||||
|
include_examples "boxfill", false, [7,0]=>[1,0], [7,0,5]=>[7,0,5]
|
||||||
|
end
|
||||||
|
context 'moving tolerance to quantity' do
|
||||||
|
include_examples "boxfill", true, [4,2]=>[6,0], [4,2,0]=>[6,0,0]
|
||||||
|
end
|
||||||
|
# @todo enable test when tolerance doesn't count in missing_units
|
||||||
|
#context 'decreasing tolerance' do
|
||||||
|
# include_examples "boxfill", false, [0,2]=>[0,0], [0,2,0]=>[0,2,0]
|
||||||
|
#end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue