Merge pull request #387 from foodcoops/feature/boxfills

Add optional boxfill phase to orders (+ bootstrap buttons)
This commit is contained in:
wvengen 2015-11-20 14:25:41 +01:00
commit 227ca0dd84
16 changed files with 271 additions and 76 deletions

View file

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

View file

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

View file

@ -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] != '{}'

View file

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

View file

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

View file

@ -199,5 +199,3 @@ class GroupOrderArticle < ActiveRecord::Base
result != result_computed unless result.nil? result != result_computed unless result.nil?
end end
end end

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,5 @@
class AddBoxfillToOrder < ActiveRecord::Migration
def change
add_column :orders, :boxfill, :datetime
end
end

View file

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

View file

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