Merge pull request #222 from wvengen/feature-receive

New receive screen
This commit is contained in:
wvengen 2014-01-20 03:02:27 -08:00
commit 8b4c292ea0
52 changed files with 1168 additions and 182 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 941 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -107,6 +107,11 @@ $(function() {
return false; return false;
}); });
// Disable action of disabled buttons
$(document).on('click', 'a.disabled', function() {
return false;
});
// Show and hide loader on ajax callbacks // Show and hide loader on ajax callbacks
$('*[data-remote]').bind('ajax:beforeSend', function() { $('*[data-remote]').bind('ajax:beforeSend', function() {
$('#loader').show(); $('#loader').show();

View file

@ -30,7 +30,19 @@ body {
// Example: // Example:
// @linkColor: #ff0000; // @linkColor: #ff0000;
// Custom styles
// main ui colours
@mainRedColor: #ED0606;
// article status
@articleUsedColor: green;
@articleUnusedColor: red;
@articleUnavailColor: #999;
@articleUpdatedColor: #468847;
// dim colors by this amount when the information is less important
@nonessentialDim: 35%;
// Fix empty dd tags in horizontal dl, see https://github.com/twitter/bootstrap/issues/4062 // Fix empty dd tags in horizontal dl, see https://github.com/twitter/bootstrap/issues/4062
.dl-horizontal { .dl-horizontal {
@ -39,7 +51,8 @@ body {
// Do not use additional margin for input in table // Do not use additional margin for input in table
.form-horizontal .control-group.control-group-intable, .form-horizontal .control-group.control-group-intable,
.form-horizontal .controls.controls-intable { .form-horizontal .controls.controls-intable,
.input-prepend.intable {
margin: 0; margin: 0;
} }
@ -53,7 +66,6 @@ body {
margin-bottom: 0; margin-bottom: 0;
} }
@mainRedColor: #ED0606;
.logo { .logo {
margin: 10px 0 0 30px; margin: 10px 0 0 30px;
@ -134,11 +146,11 @@ table {
} }
// ordering // ordering
span.used { .used {
color: green; color: @articleUsedColor;
} }
span.unused { .unused {
color: red; color: @articleUnusedColor;
} }
#order-footer, .article-info { #order-footer, .article-info {
@ -202,11 +214,11 @@ tr.order-article:hover .article-info {
// ********* Articles // ********* Articles
tr.just-updated { tr.just-updated {
color: #468847; color: @articleUpdatedColor;
} }
tr.unavailable { tr.unavailable {
color: #999; color: @articleUnavailColor;
} }
// articles edit all // articles edit all
@ -216,6 +228,16 @@ tr.unavailable {
} }
} }
// editable article list can be more compact
.ordered-articles input {
margin-bottom: 0;
}
// allow content to appear without sudden table change (receive)
.units_delta {
min-width: 3.5em;
}
// ********* Tweaks & fixes // ********* Tweaks & fixes
// Fix bootstrap dropdown menu on mobile // Fix bootstrap dropdown menu on mobile
@ -258,11 +280,60 @@ table.table {
} }
} }
// it's a bit distracting
.icon-asterisk {
font-size: 80%;
vertical-align: middle;
padding-bottom: 0.4ex;
}
// allow buttons as input add-on (with proper height) // allow buttons as input add-on (with proper height)
.input-append button.add-on { .input-append button.add-on {
height: inherit; height: inherit;
} }
// inline form elements
.inline {
display: inline;
}
// show package icon after amount of wholesale units
.package-image (@align) {
background-image: url(package-bg.png);
background-repeat: no-repeat;
background-position: @align center;
}
input.package {
.package-image(right);
// disabled and readonly definitions though
&[disabled], &[readonly] {
background-color: @inputDisabledBackground;
}
}
i.package {
.package-image(left);
min-width: 18px;
min-height: 18px;
vertical-align: baseline;
font-style: normal;
padding-left: 20px;
@media (max-width: 979px) { padding-left: 0; }
}
i.package.icon-only {
padding-left: 6px;
background-position: right;
display: inline-block;
}
.package { color: tint(@textColor, @nonessentialDim); }
.used .package { color: tint(@articleUsedColor, @nonessentialDim); }
.unused .package { color: tint(@articleUnusedColor, @nonessentialDim); }
.unavailable .package { color: @articleUnavailColor; }
// very small inputs - need !important for responsive selectors
.input-nano {
width: 30px !important;
}
// get rid of extra space on bottom of dialog with form // get rid of extra space on bottom of dialog with form
.modal form { .modal form {
margin: 0; margin: 0;
@ -297,10 +368,22 @@ table.table {
float: none; float: none;
} }
} }
// allow to add a hint for the whole line
> .help-block {
clear: both;
margin-left: 180px;
position: relative;
top: -2.5ex;
}
} }
} }
// allow to have indicator text instead of input with same markup // allow to have indicator text instead of input with same markup
.control-text { .control-text {
margin-top: 5px; margin-top: 5px;
} }
// unlock button same size as warning sign
.input-prepend button.unlocker {
padding-right: 6px;
padding-left: 7px;
}

View file

@ -47,6 +47,7 @@ class ApplicationController < ActionController::Base
when "article_meta" then current_user.role_article_meta? when "article_meta" then current_user.role_article_meta?
when "suppliers" then current_user.role_suppliers? when "suppliers" then current_user.role_suppliers?
when "orders" then current_user.role_orders? when "orders" then current_user.role_orders?
when "finance_or_orders" then (current_user.role_finance? || current_user.role_orders?)
when "any" then true # no role required when "any" then true # no role required
else false # any unknown role will always fail else false # any unknown role will always fail
end end
@ -78,6 +79,10 @@ class ApplicationController < ActionController::Base
authenticate('orders') authenticate('orders')
end end
def authenticate_finance_or_orders
authenticate('finance_or_orders')
end
# checks if the current_user is member of given group. # checks if the current_user is member of given group.
# if fails the user will redirected to startpage # if fails the user will redirected to startpage
def authenticate_membership_or_admin(group_id = params[:id]) def authenticate_membership_or_admin(group_id = params[:id])

View file

@ -29,6 +29,18 @@ class Finance::BalancingController < Finance::BaseController
render layout: false if request.xhr? render layout: false if request.xhr?
end end
def new_on_order_article_create # See publish/subscribe design pattern in /doc.
@order_article = OrderArticle.find(params[:order_article_id])
render :layout => false
end
def new_on_order_article_update # See publish/subscribe design pattern in /doc.
@order_article = OrderArticle.find(params[:order_article_id])
render :layout => false
end
def update_summary def update_summary
@order = Order.find(params[:id]) @order = Order.find(params[:id])

View file

@ -1,28 +1,26 @@
class Finance::OrderArticlesController < ApplicationController class OrderArticlesController < ApplicationController
before_filter :authenticate_finance before_filter :authenticate_finance_or_orders
layout false # We only use this controller to serve js snippets, no need for layout rendering layout false # We only use this controller to serve js snippets, no need for layout rendering
def new def new
@order = Order.find(params[:order_id]) @order = Order.find(params[:order_id])
@order_article = @order.order_articles.build @order_article = @order.order_articles.build(params[:order_article])
end end
def create def create
@order = Order.find(params[:order_id]) @order = Order.find(params[:order_id])
# The article may with zero units ordered - in that case find and set amount to nonzero. # The article may be ordered with zero units - in that case do not complain.
# If order_article is ordered and a new order_article is created, an error message will be # If order_article is ordered and a new order_article is created, an error message will be
# given mentioning that the article already exists, which is desired. # given mentioning that the article already exists, which is desired.
@order_article = @order.order_articles.where(:article_id => params[:order_article][:article_id]).first @order_article = @order.order_articles.where(:article_id => params[:order_article][:article_id]).first
if @order_article and @order_article.units_to_order == 0 unless (@order_article and @order_article.units_to_order == 0)
@order_article.units_to_order = 1
else
@order_article = @order.order_articles.build(params[:order_article]) @order_article = @order.order_articles.build(params[:order_article])
end end
unless @order_article.save @order_article.save!
render action: :new rescue
end render action: :new
end end
def edit def edit

View file

@ -8,7 +8,8 @@ class OrdersController < ApplicationController
# List orders # List orders
def index def index
@open_orders = Order.open @open_orders = Order.open.includes(:supplier)
@finished_orders = Order.finished_not_closed.includes(:supplier)
@per_page = 15 @per_page = 15
if params['sort'] if params['sort']
sort = case params['sort'] sort = case params['sort']
@ -20,7 +21,7 @@ class OrdersController < ApplicationController
else else
sort = "ends DESC" sort = "ends DESC"
end end
@orders = Order.page(params[:page]).per(@per_page).order(sort).where("state != 'open'").includes(:supplier) @orders = Order.closed.page(params[:page]).per(@per_page).includes(:supplier).order(sort)
end end
# Gives a view for the results to a specific order # Gives a view for the results to a specific order
@ -105,6 +106,29 @@ class OrdersController < ApplicationController
redirect_to orders_url, alert: I18n.t('errors.general_msg', :msg => error.message) redirect_to orders_url, alert: I18n.t('errors.general_msg', :msg => error.message)
end end
def receive
@order = Order.find(params[:id])
unless request.post?
@order_articles = @order.order_articles.ordered.includes(:article)
else
s = update_order_amounts
flash[:notice] = (s ? I18n.t('orders.receive.notice', :msg => s) : I18n.t('orders.receive.notice_none'))
redirect_to @order
end
end
def receive_on_order_article_create # See publish/subscribe design pattern in /doc.
@order_article = OrderArticle.find(params[:order_article_id])
render :layout => false
end
def receive_on_order_article_update # See publish/subscribe design pattern in /doc.
@order_article = OrderArticle.find(params[:order_article_id])
render :layout => false
end
protected protected
# Renders the fax-text-file # Renders the fax-text-file
@ -131,4 +155,46 @@ class OrdersController < ApplicationController
end end
text text
end end
def update_order_amounts
return if not params[:order_articles]
# where to leave remainder during redistribution
rest_to = []
rest_to << :tolerance if params[:rest_to_tolerance]
rest_to << :stock if params[:rest_to_stock]
rest_to << nil
# count what happens to the articles:
# changed, rest_to_tolerance, rest_to_stock, left_over
counts = [0] * 4
cunits = [0] * 4
OrderArticle.transaction do
params[:order_articles].each do |oa_id, oa_params|
unless oa_params.blank?
oa = OrderArticle.find(oa_id)
# update attributes; don't use update_attribute because it calls save
# which makes received_changed? not work anymore
oa.attributes = oa_params
if oa.units_received_changed?
counts[0] += 1
unless oa.units_received.blank?
cunits[0] += oa.units_received * oa.article.unit_quantity
oacounts = oa.redistribute oa.units_received * oa.price.unit_quantity, rest_to
oacounts.each_with_index {|c,i| cunits[i+1]+=c; counts[i+1]+=1 if c>0 }
end
end
oa.save!
end
end
end
return nil if counts[0] == 0
notice = []
notice << I18n.t('orders.update_order_amounts.msg1', count: counts[0], units: cunits[0])
notice << I18n.t('orders.update_order_amounts.msg2', count: counts[1], units: cunits[1]) if params[:rest_to_tolerance]
notice << I18n.t('orders.update_order_amounts.msg3', count: counts[2], units: cunits[2]) if params[:rest_to_stock]
if counts[3]>0 or cunits[3]>0
notice << I18n.t('orders.update_order_amounts.msg4', count: counts[3], units: cunits[3])
end
notice.join(', ')
end
end end

View file

@ -81,10 +81,10 @@ module ApplicationHelper
# heading, with an abbreviation title of 'foo'. # heading, with an abbreviation title of 'foo'.
# 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) } i18nopts = options.select {|a| !['short'].include?(a) }.merge({count: 2})
s = model.human_attribute_name(attribute, i18nopts) s = model.human_attribute_name(attribute, i18nopts)
if options[:short] if options[:short]
sshort = model.human_attribute_name("#{attribute}_short".to_sym, options.merge({fallback: true, default: ''})) 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='#{s}'>#{sshort}</abbr>" unless sshort.blank?
end end
s s

View file

@ -10,12 +10,17 @@ module DeliveriesHelper
end end
end end
def articles_for_select2(supplier) def articles_for_select2(articles, except = [], &block)
supplier.articles.undeleted.reorder('articles.name ASC').map {|a| {:id => a.id, :text => "#{a.name} (#{number_to_currency a.price}/#{a.unit})"} } articles = articles.reorder('articles.name ASC')
articles.reject! {|a| not except.index(a.id).nil? } if except
block_given? or block = Proc.new {|a| "#{a.name} (#{number_to_currency a.price}/#{a.unit})" }
articles.map do |a|
{:id => a.id, :text => block.call(a)}
end.unshift({:id => '', :text => ''})
end end
def stock_articles_for_table(supplier) def articles_for_table(articles)
supplier.stock_articles.undeleted.reorder('articles.name ASC') articles.undeleted.reorder('articles.name ASC')
end end
def stock_change_remove_link(stock_change_form) def stock_change_remove_link(stock_change_form)

View file

@ -1,4 +1,4 @@
module Finance::OrderArticlesHelper module OrderArticlesHelper
def new_order_articles_collection def new_order_articles_collection
if @order.stockit? if @order.stockit?

View file

@ -15,4 +15,67 @@ module OrdersHelper
options += [[I18n.t('helpers.orders.option_stock'), url_for(action: 'new', supplier_id: 0)]] options += [[I18n.t('helpers.orders.option_stock'), url_for(action: 'new', supplier_id: 0)]]
options_for_select(options) options_for_select(options)
end end
def units_history_line(order_article)
if order_article.order.open?
nil
else
units_info = "#{order_article.units_to_order} #{OrderArticle.human_attribute_name :units_to_order, count: order_article.units_to_order}"
units_info += ", #{order_article.units_billed} #{OrderArticle.human_attribute_name :units_billed_short, count: order_article.units_billed}" unless order_article.units_billed.nil?
units_info += ", #{order_article.units_received} #{OrderArticle.human_attribute_name :units_received_short, count: order_article.units_received}" unless order_article.units_received.nil?
end
end
# can be article or article_price
# icon: `false` to not show the icon
# soft_uq: `true` to hide unit quantity specifier on small screens
# sensible in tables with multiple columns calling `pkg_helper`
def pkg_helper(article, options={})
return nil if article.unit_quantity == 1
uq_text = "&times; #{article.unit_quantity}".html_safe
uq_text = content_tag(:span, uq_text, class: 'hidden-phone') if options[:soft_uq]
if options[:icon].nil? or options[:icon]
pkg_helper_icon(uq_text)
else
pkg_helper_icon(uq_text, tag: :span)
end
end
def pkg_helper_icon(c=nil, options={})
options = {tag: 'i', class: ''}.merge(options)
if c.nil?
c = "&nbsp;".html_safe
options[:class] += " icon-only"
end
content_tag(options[:tag], c, class: "package #{options[:class]}").html_safe
end
def article_price_change_hint(order_article, gross=false)
return nil if order_article.article.price == order_article.price.price
title = "#{t('helpers.orders.old_price')}: #{number_to_currency order_article.article.price}"
title += " / #{number_to_currency order_article.article.gross_price}" if gross
content_tag(:i, nil, class: 'icon-asterisk', title: j(title)).html_safe
end
def receive_input_field(form)
order_article = form.object
units_expected = (order_article.units_billed or order_article.units_to_order) *
1.0 * order_article.article.unit_quantity / order_article.article_price.unit_quantity
input_classes = 'input input-nano units_received'
input_classes += ' package' unless order_article.article_price.unit_quantity == 1
input_html = form.text_field :units_received, class: input_classes,
data: {'units-expected' => units_expected},
disabled: order_article.result_manually_changed?,
autocomplete: 'off'
if order_article.result_manually_changed?
input_html = content_tag(:span, class: 'input-prepend intable', title: t('.field_locked_title', default: '')) {
button_tag(nil, type: :button, class: 'btn unlocker') {
content_tag(:i, nil, class: 'icon icon-unlock')
} + input_html
}
end
input_html.html_safe
end
end end

View file

@ -99,57 +99,63 @@ class GroupOrderArticle < ActiveRecord::Base
# Returns a hash with three keys: :quantity / :tolerance / :total # Returns a hash with three keys: :quantity / :tolerance / :total
# #
# See description of the ordering algorithm in the general application documentation for details. # See description of the ordering algorithm in the general application documentation for details.
def calculate_result def calculate_result(total = nil)
@calculate_result ||= begin # return memoized result unless a total is given
quantity = tolerance = total_quantity = 0 return @calculate_result if total.nil? and not @calculate_result.nil?
# Get total quantity = tolerance = total_quantity = 0
if order_article.article.is_a?(StockArticle)
total = order_article.article.quantity # Get total
logger.debug "<#{order_article.article.name}> (stock) => #{total}" if not total.nil?
else logger.debug "<#{order_article.article.name}> => #{total} (given)"
total = order_article.units_to_order * order_article.price.unit_quantity elsif order_article.article.is_a?(StockArticle)
logger.debug "<#{order_article.article.name}> units_to_order #{order_article.units_to_order} => #{total}" total = order_article.article.quantity
logger.debug "<#{order_article.article.name}> (stock) => #{total}"
else
total = order_article.units_to_order * order_article.price.unit_quantity
logger.debug "<#{order_article.article.name}> units_to_order #{order_article.units_to_order} => #{total}"
end
if total > 0
# In total there are enough units ordered. Now check the individual result for the ordergroup (group_order).
#
# Get all GroupOrderArticleQuantities for this OrderArticle...
order_quantities = GroupOrderArticleQuantity.all(
:conditions => ["group_order_article_id IN (?)", order_article.group_order_article_ids], :order => 'created_on')
logger.debug "GroupOrderArticleQuantity records found: #{order_quantities.size}"
# Determine quantities to be ordered...
order_quantities.each do |goaq|
q = [goaq.quantity, total - total_quantity].min
total_quantity += q
if goaq.group_order_article_id == self.id
logger.debug "increasing quantity by #{q}"
quantity += q
end
break if total_quantity >= total
end end
if total > 0 # Determine tolerance to be ordered...
# In total there are enough units ordered. Now check the individual result for the ordergroup (group_order). if total_quantity < total
# logger.debug "determining additional items to be ordered from tolerance"
# Get all GroupOrderArticleQuantities for this OrderArticle...
order_quantities = GroupOrderArticleQuantity.all(
:conditions => ["group_order_article_id IN (?)", order_article.group_order_article_ids], :order => 'created_on')
logger.debug "GroupOrderArticleQuantity records found: #{order_quantities.size}"
# Determine quantities to be ordered...
order_quantities.each do |goaq| order_quantities.each do |goaq|
q = [goaq.quantity, total - total_quantity].min q = [goaq.tolerance, total - total_quantity].min
total_quantity += q total_quantity += q
if goaq.group_order_article_id == self.id if goaq.group_order_article_id == self.id
logger.debug "increasing quantity by #{q}" logger.debug "increasing tolerance by #{q}"
quantity += q tolerance += q
end end
break if total_quantity >= total break if total_quantity >= total
end end
# Determine tolerance to be ordered...
if total_quantity < total
logger.debug "determining additional items to be ordered from tolerance"
order_quantities.each do |goaq|
q = [goaq.tolerance, total - total_quantity].min
total_quantity += q
if goaq.group_order_article_id == self.id
logger.debug "increasing tolerance by #{q}"
tolerance += q
end
break if total_quantity >= total
end
end
logger.debug "determined quantity/tolerance/total: #{quantity} / #{tolerance} / #{quantity + tolerance}"
end end
{:quantity => quantity, :tolerance => tolerance, :total => quantity + tolerance} logger.debug "determined quantity/tolerance/total: #{quantity} / #{tolerance} / #{quantity + tolerance}"
end end
# memoize result unless a total is given
r = {:quantity => quantity, :tolerance => tolerance, :total => quantity + tolerance}
@calculate_result = r if total.nil?
r
end end
# Returns order result, # Returns order result,
@ -159,9 +165,11 @@ class GroupOrderArticle < ActiveRecord::Base
self[:result] || calculate_result[type] self[:result] || calculate_result[type]
end end
# This is used during order.finish!. # This is used for automatic distribution, e.g., in order.finish! or when receiving orders
def save_results! def save_results!(article_total = nil)
self.update_attribute(:result, calculate_result[:total]) new_result = calculate_result(article_total)[:total]
self.update_attribute(:result_computed, new_result)
self.update_attribute(:result, new_result)
end end
# Returns total price for this individual article # Returns total price for this individual article
@ -180,6 +188,10 @@ class GroupOrderArticle < ActiveRecord::Base
end end
end end
# Check if the result deviates from the result_computed
def result_manually_changed?
result != result_computed unless result.nil?
end
end end

View file

@ -166,6 +166,9 @@ class Order < ActiveRecord::Base
goa.save_results! goa.save_results!
# Delete no longer required order-history (group_order_article_quantities) and # Delete no longer required order-history (group_order_article_quantities) and
# TODO: Do we need articles, which aren't ordered? (units_to_order == 0 ?) # TODO: Do we need articles, which aren't ordered? (units_to_order == 0 ?)
# A: Yes, we do - for redistributing articles when the number of articles
# delivered changes, and for statistics on popular articles. Records
# with both tolerance and quantity zero can be deleted.
#goa.group_order_article_quantities.clear #goa.group_order_article_quantities.clear
end end
end end

View file

@ -12,8 +12,9 @@ class OrderArticle < ActiveRecord::Base
validate :article_and_price_exist validate :article_and_price_exist
validates_uniqueness_of :article_id, scope: :order_id validates_uniqueness_of :article_id, scope: :order_id
scope :ordered, :conditions => "units_to_order > 0" _ordered_sql = "units_to_order > 0 OR units_billed > 0 OR units_received > 0"
scope :ordered_or_member, -> { includes(:group_order_articles).where("units_to_order > 0 OR group_order_articles.result > 0") } scope :ordered, -> { where(_ordered_sql) }
scope :ordered_or_member, -> { includes(:group_order_articles).where("#{_ordered_sql} OR group_order_articles.result > 0") }
before_create :init_from_balancing before_create :init_from_balancing
after_destroy :update_ordergroup_prices after_destroy :update_ordergroup_prices
@ -34,7 +35,14 @@ class OrderArticle < ActiveRecord::Base
def price def price
article_price || article article_price || article
end end
# latest information on available units
def units
return units_received unless units_received.nil?
return units_billed unless units_billed.nil?
units_to_order
end
# Count quantities of belonging group_orders. # Count quantities of belonging group_orders.
# In balancing this can differ from ordered (by supplier) quantity for this article. # In balancing this can differ from ordered (by supplier) quantity for this article.
def group_orders_sum def group_orders_sum
@ -81,17 +89,70 @@ class OrderArticle < ActiveRecord::Base
# Calculate price for ordered quantity. # Calculate price for ordered quantity.
def total_price def total_price
units_to_order * price.unit_quantity * price.price units * price.unit_quantity * price.price
end end
# Calculate gross price for ordered qunatity. # Calculate gross price for ordered qunatity.
def total_gross_price def total_gross_price
units_to_order * price.unit_quantity * price.gross_price units * price.unit_quantity * price.gross_price
end end
def ordered_quantities_equal_to_group_orders? def ordered_quantities_different_from_group_orders?(ordered_mark="!", billed_mark="?", received_mark="?")
# the rescue is a workaround for units_to_order not being defined in integration tests if not units_received.nil?
(units_to_order * price.unit_quantity) == group_orders_sum[:quantity] rescue false ((units_received * price.unit_quantity) == group_orders_sum[:quantity]) ? false : received_mark
elsif not units_billed.nil?
((units_billed * price.unit_quantity) == group_orders_sum[:quantity]) ? false : billed_mark
elsif not units_to_order.nil?
((units_to_order * price.unit_quantity) == group_orders_sum[:quantity]) ? false : ordered_mark
else
nil # can happen in integration tests
end
end
# redistribute articles over ordergroups
# quantity Number of units to distribute
# surplus What to do when there are more articles than ordered quantity
# :tolerance fill member orders' tolerance
# :stock move to stock
# nil nothing; for catching the remaining count
# update_totals Whether to update group_order and ordergroup totals
# returns array with counts for each surplus method
def redistribute(quantity, surplus = [:tolerance], update_totals = true)
qty_left = quantity
counts = [0] * surplus.length
if surplus.index(:tolerance).nil?
qty_for_members = [qty_left, self.quantity].min
else
qty_for_members = [qty_left, self.quantity+self.tolerance].min
counts[surplus.index(:tolerance)] = [0, qty_for_members-self.quantity].max
end
# Recompute
group_order_articles.each {|goa| goa.save_results! qty_for_members }
qty_left -= qty_for_members
# if there's anything left, move to stock if wanted
if qty_left > 0 and surplus.index(:stock)
counts[surplus.index(:stock)] = qty_left
# 1) find existing stock article with same name, unit, price
# 2) if not found, create new stock article
# avoiding duplicate stock article names
end
if qty_left > 0 and surplus.index(nil)
counts[surplus.index(nil)] = qty_left
end
# Update GroupOrder prices & Ordergroup stats
# TODO only affected group_orders, and once after redistributing all articles
if update_totals
update_ordergroup_prices
order.ordergroups.each(&:update_stats!)
end
# TODO notifications
counts
end end
# Updates order_article and belongings during balancing process # Updates order_article and belongings during balancing process
@ -134,18 +195,24 @@ class OrderArticle < ActiveRecord::Base
units = 0 if units < 0 units = 0 if units < 0
units units
end end
# Check if the result of any associated GroupOrderArticle was overridden manually
def result_manually_changed?
group_order_articles.any? {|goa| goa.result_manually_changed?}
end
private private
def article_and_price_exist def article_and_price_exist
errors.add(:article, I18n.t('model.order_article.error_price')) if !(article = Article.find(article_id)) || article.fc_price.nil? errors.add(:article, I18n.t('model.order_article.error_price')) if !(article = Article.find(article_id)) || article.fc_price.nil?
rescue
errors.add(:article, I18n.t('model.order_article.error_price'))
end end
# Associate with current article price if created in a finished order # Associate with current article price if created in a finished order
def init_from_balancing def init_from_balancing
if order.present? and order.finished? if order.present? and order.finished?
self.article_price = article.article_prices.first self.article_price = article.article_prices.first
self.units_to_order = 1
end end
end end
@ -153,7 +220,7 @@ class OrderArticle < ActiveRecord::Base
# updates prices of ALL ordergroups - these are actually too many # updates prices of ALL ordergroups - these are actually too many
# in case of performance issues, update only ordergroups, which ordered this article # in case of performance issues, update only ordergroups, which ordered this article
# CAUTION: in after_destroy callback related records (e.g. group_order_articles) are already non-existent # CAUTION: in after_destroy callback related records (e.g. group_order_articles) are already non-existent
order.group_orders.each { |go| go.update_price! } order.group_orders.each(&:update_price!)
end end
end end

View file

@ -20,7 +20,7 @@
$('#new_stock_article').removeAttr('disabled').select2({ $('#new_stock_article').removeAttr('disabled').select2({
placeholder: '#{t '.create_stock_article'}', placeholder: '#{t '.create_stock_article'}',
data: #{articles_for_select2(@supplier).to_json}, data: #{articles_for_select2(@supplier.articles).to_json},
createSearchChoice: function(term) { createSearchChoice: function(term) {
return { return {
id: 'new', id: 'new',
@ -115,12 +115,12 @@
%tfoot %tfoot
%tr %tr
%th{:colspan => 5} %th{:colspan => 5}
- if articles_for_select2(@supplier).empty? - if @supplier.articles.empty?
= link_to t('.create_stock_article'), new_stock_article_path, :remote => true, :class => 'btn' = link_to t('.create_stock_article'), new_stock_article_path, :remote => true, :class => 'btn'
- else - else
%input#new_stock_article{:style => 'width: 500px;'} %input#new_stock_article{:style => 'width: 500px;'}
%tbody %tbody
- for article in stock_articles_for_table(@supplier) - for article in articles_for_table(@supplier.stock_articles)
= render :partial => 'stock_article_for_adding', :locals => {:article => article} = render :partial => 'stock_article_for_adding', :locals => {:article => article}
%h2= t '.title_fill_quantities' %h2= t '.title_fill_quantities'

View file

@ -4,13 +4,13 @@
%th= sort_link_helper Article.model_name.human, "name" %th= sort_link_helper Article.model_name.human, "name"
%th= sort_link_helper Article.human_attribute_name(:order_number_short), "order_number" %th= sort_link_helper Article.human_attribute_name(:order_number_short), "order_number"
%th= t('.amount') %th= t('.amount')
%th= heading_helper Article, :units %th= heading_helper Article, :unit
%th= t('.net') %th= t('.net')
%th= t('.gross') %th= t('.gross')
%th= heading_helper Article, :tax %th= heading_helper Article, :tax
%th= heading_helper Article, :deposit %th= heading_helper Article, :deposit
%th{:colspan => "2"} %th{:colspan => "2"}
= link_to t('.add_article'), new_finance_order_order_article_path(@order), remote: true, = link_to t('.add_article'), new_order_order_article_path(@order), remote: true,
class: 'btn btn-small' class: 'btn btn-small'
%tbody#result_table %tbody#result_table
- for order_article in @articles - for order_article in @articles

View file

@ -1,11 +1,12 @@
%td.closed %td.closed
= link_to order_article.article.name, '#', 'data-toggle-this' => "#group_order_articles_#{order_article.id}" = link_to order_article.article.name, '#', 'data-toggle-this' => "#group_order_articles_#{order_article.id}"
%td= order_article.article.order_number %td= order_article.article.order_number
%td %td{title: units_history_line(order_article)}
= order_article.units_to_order = order_article.units
- unless order_article.ordered_quantities_equal_to_group_orders? = pkg_helper order_article.article_price
%span{:style => "color:red;font-weight: bold"} ! - if s=order_article.ordered_quantities_different_from_group_orders?
%td #{order_article.price.unit_quantity} &times; #{order_article.article.unit} %span{:style => "color:red;font-weight: bold"}= s
%td #{order_article.article.unit}
%td %td
= number_to_currency(order_article.price.price, :unit => "") = number_to_currency(order_article.price.price, :unit => "")
:plain :plain
@ -19,8 +20,8 @@
%td #{order_article.price.tax}% %td #{order_article.price.tax}%
%td= order_article.price.deposit %td= order_article.price.deposit
%td %td
= link_to t('ui.edit'), edit_finance_order_order_article_path(order_article.order, order_article), remote: true, = link_to t('ui.edit'), edit_order_order_article_path(order_article.order, order_article), remote: true,
class: 'btn btn-mini' class: 'btn btn-mini'
%td %td
= link_to t('ui.delete'), finance_order_order_article_path(order_article.order, order_article), method: :delete, = link_to t('ui.delete'), order_order_article_path(order_article.order, order_article), method: :delete,
remote: true, confirm: t('.confirm'), class: 'btn btn-danger btn-mini' remote: true, confirm: t('.confirm'), class: 'btn btn-danger btn-mini'

View file

@ -19,6 +19,11 @@
%td= show_user(order.updated_by) %td= show_user(order.updated_by)
%td %td
- unless order.closed? - unless order.closed?
- if current_user.role_orders?
- unless order.stockit?
= link_to t('orders.index.action_receive'), receive_order_path(order), class: 'btn btn-mini'
- else
= link_to t('orders.index.action_receive'), '#', class: 'btn btn-mini disabled'
= link_to t('.clear'), new_finance_order_path(order_id: order.id), class: 'btn btn-mini btn-primary' = link_to t('.clear'), new_finance_order_path(order_id: order.id), class: 'btn btn-mini btn-primary'
= link_to t('.close'), close_direct_finance_order_path(order), = link_to t('.close'), close_direct_finance_order_path(order),
:confirm => t('.confirm'), :method => :put, class: 'btn btn-mini' :confirm => t('.confirm'), :method => :put, class: 'btn btn-mini'

View file

@ -1,3 +1,27 @@
- content_for :javascript do
:javascript
$(function() {
// Subscribe to database changes.
// See publish/subscribe design pattern in /doc.
$(document).on('OrderArticle#update', function(e) {
$.ajax({
url: '#{new_on_order_article_update_finance_order_path(@order)}',
type: 'get',
data: {order_article_id: e.order_article_id},
contentType: 'application/json; charset=UTF-8'
});
});
$(document).on('OrderArticle#create', function(e) {
$.ajax({
url: '#{new_on_order_article_create_finance_order_path(@order)}',
type: 'get',
data: {order_article_id: e.order_article_id},
contentType: 'application/json; charset=UTF-8'
});
});
});
- title t('.title', name: @order.name) - title t('.title', name: @order.name)
- content_for :sidebar do - content_for :sidebar do

View file

@ -0,0 +1,16 @@
// Handle more advanced DOM update after AJAX database manipulation.
// See publish/subscribe design pattern in /doc.
(function(w) {
$('#order_article_<%= @order_article.id %>').remove(); // just to be sure: remove table row which is added below
$('#ordered-articles tr').removeClass('success');
var order_article_entry = $(
'<%= j render('finance/balancing/order_article_result', order_article: @order_article) %>'
).addClass('success');
$('#result_table').prepend(order_article_entry);
$('#summaryChangedWarning').show();
})(window);

View file

@ -0,0 +1,14 @@
// Handle more advanced DOM update after AJAX database manipulation.
// See publish/subscribe design pattern in /doc.
(function(w) {
$('#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();
})(window);

View file

@ -1,3 +0,0 @@
$('#modalContainer').modal('hide');
$('#result_table').prepend('#{j(render('finance/balancing/order_article_result', order_article: @order_article))}');
$('#summaryChangedWarning').show();

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,9 +1,19 @@
= simple_form_for [:finance, @order, @order_article], remote: true do |form| = simple_form_for [@order, @order_article], remote: true do |form|
.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 '.title' %h3= t '.title'
.modal-body .modal-body
= form.input :units_to_order - if params[:without_units]
= hidden_field_tag :without_units, true
- else
.fold-line
= form.input :units_to_order, hint: '', input_html: {class: 'input-nano'}
-#= form.input :units_billed, label: 'invoice', input_html: {class: 'input-nano'}
= form.input :units_received, input_html: {class: 'input-nano'},
label: t('activerecord.attributes.order_article.units_received_short')
%p.help-block= t 'simple_form.hints.order_article.units_to_order'
.foo{style: 'clear:both'}
= simple_fields_for :article, @order_article.article do |f| = simple_fields_for :article, @order_article.article do |f|
= f.input :name = f.input :name

View file

@ -1,9 +1,9 @@
= simple_form_for [:finance, @order, @order_article], remote: true do |form| = simple_form_for [@order, @order_article], remote: true do |form|
.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 '.title' %h3= t '.title'
.modal-body .modal-body
= form.input :article_id, as: :select, collection: new_order_articles_collection, :label => Article.model_name.human # Why do we need the label? = form.association :article, collection: new_order_articles_collection
.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 class: 'btn btn-primary' = form.submit class: 'btn btn-primary'

View file

@ -0,0 +1,9 @@
// Publish database changes.
// See publish/subscribe design pattern in /doc.
$(document).trigger({
type: 'OrderArticle#create',
order_article_id: <%= @order_article.id %>
});
$('#modalContainer').modal('hide');

View file

@ -0,0 +1,9 @@
// Publish database changes.
// See publish/subscribe design pattern in /doc.
$(document).trigger({
type: 'OrderArticle#update',
order_article_id: <%= @order_article.id %>
});
$('#modalContainer').modal('hide');

View file

@ -2,10 +2,12 @@
%thead %thead
%tr %tr
%th= heading_helper Article, :name %th= heading_helper Article, :name
%th= heading_helper Article, :unit_quantity %th= heading_helper Article, :unit
%th= t '.prices' %th= t '.prices'
%th= t '.units_ordered' - if order.stockit?
- unless order.stockit? %th= t '.units_ordered'
- else
%th= 'Members'
%th= t '.units_full' %th= t '.units_full'
- total_net, total_gross, counter = 0, 0, 0 - total_net, total_gross, counter = 0, 0, 0
%tbody %tbody
@ -18,13 +20,14 @@
- order_articles.each do |order_article| - order_articles.each do |order_article|
- net_price = order_article.price.price - net_price = order_article.price.price
- gross_price = order_article.price.gross_price - gross_price = order_article.price.gross_price
- units = order_article.units_to_order
- unit_quantity = order_article.price.unit_quantity - unit_quantity = order_article.price.unit_quantity
- units = order_article.units
- total_net += units * unit_quantity * net_price - total_net += units * unit_quantity * net_price
- total_gross += units * unit_quantity * gross_price - total_gross += units * unit_quantity * gross_price
%tr{:class => cycle('even', 'odd', :name => 'articles'), :style => "color: #{units > 0 ? 'green' : 'red'}"} - cssclass = (units > 0 ? 'used' : (order_article.quantity > 0 ? 'unused' : 'unavailable'))
%tr{:class => cycle('even', 'odd', :name => 'articles') + ' ' + cssclass}
%td=h order_article.article.name %td=h order_article.article.name
%td= "#{unit_quantity} x #{order_article.article.unit}" %td= order_article.article.unit
%td= "#{number_to_currency(net_price)} / #{number_to_currency(gross_price)}" %td= "#{number_to_currency(net_price)} / #{number_to_currency(gross_price)}"
- if order.stockit? - if order.stockit?
%td= units %td= units
@ -33,7 +36,9 @@
%td= "#{order_article.quantity} + #{order_article.tolerance}" %td= "#{order_article.quantity} + #{order_article.tolerance}"
- else - else
%td= "#{order_article.quantity}" %td= "#{order_article.quantity}"
%td= units %td{title: units_history_line(order_article)}
= units
= pkg_helper order_article.price
%p %p
= t '.prices_sum' = t '.prices_sum'
= "#{number_to_currency(total_net)} / #{number_to_currency(total_gross)}" = "#{number_to_currency(total_net)} / #{number_to_currency(total_gross)}"

View file

@ -0,0 +1,27 @@
-# NOTE: if you modify tiny details here you must also change them in `receive_on_order_article_update.js.erb`
= fields_for 'order_articles', order_article, index: order_article.id do |form|
%tr{id: "order_article_#{order_article.id}", class: "#{cycle('even', 'odd', name: 'articles')} order-article", valign: "top"}
- order_title = []
- order_title.append Article.human_attribute_name(:manufacturer)+': ' + order_article.article.manufacturer unless order_article.article.manufacturer.to_s.empty?
- order_title.append Article.human_attribute_name(:note)+': ' + order_article.article.note unless order_article.article.note.to_s.empty?
%td= order_article.article.order_number
%td.name{title: order_title.join("\n")}= order_article.article.name
%td.unit= order_article.article.unit
%td.article_price
= number_to_currency order_article.article_price.price
= article_price_change_hint(order_article)
%td #{order_article.quantity} + #{order_article.tolerance}
%td
= order_article.units_to_order
= pkg_helper order_article.article
-#%td # TODO implement invoice screen
- unless order_article.units_billed.nil?
= order_article.units_billed
= pkg_helper order_article.article, soft_uq: true
%td.units_received_cell
= receive_input_field(form)
= pkg_helper order_article.article_price, icon: false, soft_uq: true
/ TODO add almost invisible text_field for entering single units
%td.units_delta
%td
= link_to t('ui.edit'), edit_order_order_article_path(order_article.order, order_article, without_units: true), remote: true, class: 'btn btn-small'

View file

@ -0,0 +1,99 @@
- new_articles = (@order.supplier.articles rescue @order.articles)
- new_article_data = articles_for_select2(new_articles, @order_articles.map(&:article_id)) {|a| "#{a.name} (#{a.unit_quantity}#{a.unit})"}
- content_for :javascript do
:javascript
function update_delta(input) {
var units = $(input).val();
var expected = $(input).data('units-expected');
var delta = Math.round((units-expected)*100)/100.0;
var html;
if (units.replace(/\s/g,"")=="") {
// no value
html = '';
} else if (isNaN(units)) {
html = '<i class="icon-remove" style="color: red"></i>';
} else if (delta == 0) {
// equal value
html = '<i class="icon-ok" style="color: green"></i>';
} else {
if (delta < 0) {
html = '<span style="color: red">- '+(-delta)+'</span>';
} else /*if (units> expected)*/ {
html = '<span style="color: green">+ '+(delta)+'</span>';
}
// show package icon only if the receive field has one
if ($(input).hasClass('package')) {
html += '#{j pkg_helper_icon}';
}
}
$(input).closest('tr').find('.units_delta').html(html);
}
$(document).on('change keyup', 'input[data-units-expected]', function() {
update_delta(this);
});
$(document).on('click', '#order_articles .unlocker', unlock_receive_input_field);
$(function() {
$('input[data-units-expected]').each(function() {
update_delta(this);
});
init_add_article('#add_article');
});
function init_add_article(sel) {
$(sel).removeAttr('disabled').select2({
placeholder: '#{j t('orders.receive.add_article')}',
formatNoMatches: function(term) { return '#{j t('.no_articles_available')}';}
// TODO implement adding a new article, like in deliveries
}).on('change', function(e) {
var selectedArticle = $(e.currentTarget).select2('data');
if(!selectedArticle) {
return false;
}
$.ajax({
url: '#{order_order_articles_path(@order)}',
type: 'post',
data: JSON.stringify({order_article: {article_id: selectedArticle.id}}),
contentType: 'application/json; charset=UTF-8'
});
$('#add_article').select2('data', null);
}).select2('data', null);
}
function unlock_receive_input_field() {
$('.units_received', $(this).closest('tr')).prop('disabled', false).focus();
$(this).closest('.input-prepend').prop('title', I18n.t('orders.edit_amount.field_unlocked_title'));
$(this).replaceWith('<i class="icon icon-warning-sign add-on"></i>');
}
%table#order_articles.ordered-articles.table.table-striped.stupidtable{style: 'margin-bottom: 0'}
%thead
%tr
%th.sort{:data => {:sort => 'string'}}= heading_helper Article, :order_number, short: true
%th.default-sort.sort{:data => {:sort => 'string'}}= heading_helper Article, :name
%th= heading_helper Article, :unit
%th= heading_helper Article, :price
%th= heading_helper OrderArticle, :quantity, short: true
%th= heading_helper OrderArticle, :units_to_order, short: true
-#%th Invoice # TODO implement invoice screen
%th= heading_helper OrderArticle, :units_received, short: true
%th
%th= t 'ui.actions'
%tbody#result_table
- @order_articles.each do |order_article|
= render :partial => 'edit_amount', :locals => {:order_article => order_article}
%tfoot
%tr
%th{:colspan => 10}
%select#add_article{:style => 'width: 500px;'}
- new_article_data.each do |option|
%option{id: "add_article_#{option[:id]}", value: option[:id]}= option[:text]

View file

@ -0,0 +1,16 @@
$('div.container-fluid').prepend(
'<%= j(render(:partial => 'shared/alert_success', :locals => {:alert_message => t('.notice', :name => @order_article.article.name)})) %>'
);
(function() {
$('.ordered-articles tr').removeClass('success');
var article_for_adding = $(
'<%= j(render(:partial => 'edit_amount', :locals => {:order_article => @order_article})) %>'
).addClass('success');
$('.ordered-articles tbody').append(article_for_adding);
updateSort('.ordered-articles');
$('#order_articles_<%= @order_article.id %>_units_received').focus();
})();

View file

@ -10,8 +10,13 @@
%li= link_to supplier.name, new_order_path(supplier_id: supplier.id), tabindex: -1 %li= link_to supplier.name, new_order_path(supplier_id: supplier.id), tabindex: -1
.well .well
%h2= t '.open_orders' - if not @open_orders.empty?
- unless @open_orders.empty? %h2= t '.orders_open'
- elsif not @finished_orders.empty?
%h2= t '.orders_finished'
- else
= t '.no_open_or_finished_orders'
- unless @open_orders.empty? and @finished_orders.empty?
%table.table.table-striped %table.table.table-striped
%thead %thead
%tr %tr
@ -20,24 +25,44 @@
%th= heading_helper Order, :note %th= heading_helper Order, :note
%th{colspan: "2"} %th{colspan: "2"}
%tbody %tbody
- for order in @open_orders - unless @open_orders.empty?
- tr_class = " active" if order.expired? - for order in @open_orders
%tr{class: tr_class} - tr_class = " active" if order.expired?
%td= order.name %tr{class: tr_class}
%td= format_time(order.ends) unless order.ends.nil? %td= order.name
%td= truncate(order.note) %td= format_time(order.ends) unless order.ends.nil?
%td= link_to t('.action_end'), finish_order_path(order), %td= truncate(order.note)
confirm: t('.confirm_end', order: order.name), method: :post, %td= link_to t('.action_end'), finish_order_path(order),
class: 'btn btn-small btn-success' confirm: t('.confirm_end', order: order.name), method: :post,
class: 'btn btn-small btn-success'
%td %td
= link_to t('ui.show'), order, class: 'btn btn-small' = link_to t('ui.edit'), edit_order_path(order), class: 'btn btn-small'
= link_to t('ui.edit'), edit_order_path(order), class: 'btn btn-small' = link_to t('ui.show'), order, class: 'btn btn-small'
= link_to t('ui.delete'), order, confirm: t('.confirm_delete'), method: :delete, = link_to t('ui.delete'), order, confirm: t('.confirm_delete'), method: :delete,
class: 'btn btn-small btn-danger' class: 'btn btn-small btn-danger'
- else
= t '.no_open_orders'
%h2= t '.ended_orders' - unless @finished_orders.empty?
- unless @open_orders.empty?
%tr
%td{colspan: 6}
%h2= t '.orders_finished'
- for order in @finished_orders
%tr
%td= order.name
%td= format_time(order.ends)
%td= truncate(order.note)
%td
- unless order.stockit?
-# TODO btn-success class only if not received before
= link_to t('.action_receive'), receive_order_path(order), class: 'btn btn-small btn-success'
%td
= link_to t('ui.edit'), '#', class: 'btn btn-small disabled', tabindex: -1
= link_to t('ui.show'), order, class: 'btn btn-small'
= link_to t('ui.delete'), order, confirm: t('.confirm_delete'), method: :delete,
class: 'btn btn-small btn-danger'
%h2= t '.orders_settled'
#orders_table #orders_table
= render partial: 'orders' = render partial: 'orders'

View file

@ -0,0 +1,45 @@
- content_for :javascript do
:javascript
$(function() {
// Subscribe to database changes.
// See publish/subscribe design pattern in /doc.
$(document).on('OrderArticle#update', function(e) {
$.ajax({
url: '#{receive_on_order_article_update_order_path(@order)}',
type: 'get',
data: {order_article_id: e.order_article_id},
contentType: 'application/json; charset=UTF-8'
});
});
$(document).on('OrderArticle#create', function(e) {
$.ajax({
url: '#{receive_on_order_article_create_order_path(@order)}',
type: 'get',
data: {order_article_id: e.order_article_id},
contentType: 'application/json; charset=UTF-8'
});
});
});
- title t('.title', order: @order.name)
= form_tag(receive_order_path(@order)) do
%fieldset#results
= render 'edit_amounts'
.form-actions
.pull-left
%b.checkbox.inline
= t '.surplus_options'
= label_tag :rest_to_tolerance, class: 'checkbox inline' do
= check_box_tag :rest_to_tolerance, 1, true
= t '.consider_member_tolerance'
= label_tag :rest_to_stock, class: 'checkbox inline' do
= check_box_tag :rest_to_stock, 1, false, disabled: true
%span{style: 'color: grey'}= t '.rest_to_stock'
.pull-right
= submit_tag t('.submit'), class: 'btn btn-primary'
= link_to t('ui.or_cancel'), order_path(@order)
%p= link_to_top

View file

@ -0,0 +1,17 @@
// Handle more advanced DOM update after AJAX database manipulation.
// See publish/subscribe design pattern in /doc.
(function(w) {
$('#order_article_<%= @order_article.id %>').remove(); // just to be sure: remove table row which is added below
$('#order_articles tr').removeClass('success');
var order_article_entry = $(
'<%= j render(partial: 'edit_amount', locals: {order_article: @order_article}) %>'
).addClass('success');
$('#order_articles tbody').append(order_article_entry);
updateSort('#order_articles');
$('#add_article_<%= @order_article.article.id %>').remove(); // remove option to add this article
})(window);

View file

@ -0,0 +1,35 @@
// Handle more advanced DOM update after AJAX database manipulation.
// See publish/subscribe design pattern in /doc.
(function(w) {
// get old element and update the cell which is reused
var old_order_article_entry = $('#order_article_<%= @order_article.id %>');
// update package info after input
$('td.units_received_cell span.package', old_order_article_entry).remove();
$('<%= j pkg_helper(@order_article.article_price, icon: false) %>')
.appendTo($('td.units_received_cell', old_order_article_entry));
// update package icon on input too
$('input', old_order_article_entry).toggleClass('package', <%= @order_article.article_price.unit_quantity == 1 ? 'false' : 'true' %>);
// update expected units, since unit_quantity may have been changed
$('input', old_order_article_entry).data('units-expected', <%=
(@order_article.units_billed or @order_article.units_to_order) *
1.0 * @order_article.article.unit_quantity / @order_article.article_price.unit_quantity
%>);
// render new element and inject dynamic cell
var new_order_article_entry = $(
'<%= j render(partial: 'edit_amount', locals: {order_article: @order_article}) %>'
);
$('td.units_received_cell', new_order_article_entry).replaceWith(
$('td.units_received_cell', old_order_article_entry)
);
// finally replace the OrderArticle entry
old_order_article_entry.replaceWith(new_order_article_entry);
update_delta($('input.units_received', new_order_article_entry));
})(window);

View file

@ -2,7 +2,7 @@
- if @order.finished? and !@order.closed? - if @order.finished? and !@order.closed?
.alert.alert-warning .alert.alert-warning
= t '.warn_not_closed' = t '.warn_not_closed'
// Order summary // Order summary
.well .well
@ -31,6 +31,9 @@
= link_to t('ui.edit'), edit_order_path(@order), class: 'btn' = link_to t('ui.edit'), edit_order_path(@order), class: 'btn'
= link_to t('.action_end'), finish_order_path(@order), method: :post, class: 'btn btn-success', = link_to t('.action_end'), finish_order_path(@order), method: :post, class: 'btn btn-success',
confirm: t('.confirm_end', order: @order.name) confirm: t('.confirm_end', order: @order.name)
- elsif not @order.closed? and not @order.stockit?
-# TODO btn-success class only if not received before
= link_to t('orders.index.action_receive'), receive_order_path(@order), class: 'btn btn-success'
- unless @order.closed? - unless @order.closed?
= link_to t('ui.delete'), @order, confirm: t('.confirm_delete'), method: :delete, = link_to t('ui.delete'), @order, confirm: t('.confirm_delete'), method: :delete,
class: 'btn btn-danger' class: 'btn btn-danger'

View file

@ -1,4 +1,10 @@
# only serve selected strings for i18n-js to keep filesize down # only serve selected strings for i18n-js to keep filesize down
translations: translations:
- file: 'app/assets/javascripts/i18n/translations.js' - file: 'app/assets/javascripts/i18n/translations.js'
only: ['*.js.*', '*.number.*', '*.date.formats.*'] only: [
'*.js.*',
'*.number.*',
'*.date.formats.*',
# foodsoft-specific texts to keep js with normal translations
'*.orders.edit_amount.*'
]

View file

@ -77,10 +77,17 @@ de:
note: Notiz note: Notiz
starts: Läuft vom starts: Läuft vom
status: Status status: Status
supplier: Lieferant
order_article: order_article:
article: Artikel
missing_units: Fehlende Einheiten missing_units: Fehlende Einheiten
missing_units_short: Fehlende missing_units_short: Fehlend
units_to_order: Menge quantity: Gewünschte Einheiten
quantity_short: Gewünscht
units_received: Gelieferte Gebinde
units_received_short: Geliefert
units_to_order: Bestellte Gebinde
units_to_order_short: Bestellt
update_current_price: Globalen Preis aktualisieren update_current_price: Globalen Preis aktualisieren
order_comment: order_comment:
text: Kommentiere diese Bestellung ... text: Kommentiere diese Bestellung ...
@ -277,7 +284,7 @@ de:
error_denied: Du darfst die gewünschte Seite nicht sehen. Wenn Du denkst, dass Du dürfen solltest, frage eine Administratorin, dass sie Dir die entsprechenden Rechte einräumt. Falls Du Zugang zu mehreren Benutzerkonten hast, möchtest Du Dich vielleicht %{sign_in}. error_denied: Du darfst die gewünschte Seite nicht sehen. Wenn Du denkst, dass Du dürfen solltest, frage eine Administratorin, dass sie Dir die entsprechenden Rechte einräumt. Falls Du Zugang zu mehreren Benutzerkonten hast, möchtest Du Dich vielleicht %{sign_in}.
error_denied_sign_in: als ein anderer Benutzer anmelden error_denied_sign_in: als ein anderer Benutzer anmelden
error_members_only: Diese Aktion ist nur für Mitglieder der Gruppe erlaubt! error_members_only: Diese Aktion ist nur für Mitglieder der Gruppe erlaubt!
error_token: error_token: Zugriff verweigert (ungültiger Token)!
article_categories: article_categories:
create: create:
notice: Die Kategorie wurde gespeichert notice: Die Kategorie wurde gespeichert
@ -377,7 +384,9 @@ de:
update: update:
body: ! 'Jeder Artikel wird doppelt angezeigt: die alten Werte sind grau und die Textfelder sind mit den aktuellen Werten vorausgefüllt. Abweichungen zu den alten Artikeln sind gelb markiert.' body: ! 'Jeder Artikel wird doppelt angezeigt: die alten Werte sind grau und die Textfelder sind mit den aktuellen Werten vorausgefüllt. Abweichungen zu den alten Artikeln sind gelb markiert.'
title: Aktualisieren ... title: Aktualisieren ...
update_msg: ! '%{count} Artikel müssen aktualisiert werden.' update_msg:
one: Ein Artikel muss aktualisiert werden.
other: ! '%{count} Artikel müssen aktualisiert werden.'
upload: upload:
body: <p>Die Datei muss eine Textdatei mit der Endung '.csv' sein. Die erste Zeile wird beim Einlesen ignoriert.</p> <p>Die Felder müssen mit einem Semikolon (';') getrennt und der Text mit doppelten Anführungszeichen ("Text...") umklammert werden.</p> <p>Als Zeichensatz wird UTF-8 erwartet. Korrekte Reihenfolge der Spalten:</p> body: <p>Die Datei muss eine Textdatei mit der Endung '.csv' sein. Die erste Zeile wird beim Einlesen ignoriert.</p> <p>Die Felder müssen mit einem Semikolon (';') getrennt und der Text mit doppelten Anführungszeichen ("Text...") umklammert werden.</p> <p>Als Zeichensatz wird UTF-8 erwartet. Korrekte Reihenfolge der Spalten:</p>
fields: fields:
@ -469,7 +478,9 @@ de:
- FC-Preis - FC-Preis
- Menge - Menge
title: ! 'Sortiermatrix der Bestellung: %{name}, beendet am %{date}' title: ! 'Sortiermatrix der Bestellung: %{name}, beendet am %{date}'
total: Insgesamt %{count} Artikel total:
one: Insgesamt ein Artikel
other: ! 'Insgesamt %{count} Artikel'
errors: errors:
general: Ein Problem ist aufgetreten. general: Ein Problem ist aufgetreten.
general_again: Ein Fehler ist aufgetreten. Bitte erneut versuchen. general_again: Ein Fehler ist aufgetreten. Bitte erneut versuchen.
@ -613,12 +624,6 @@ de:
show: show:
back: Züruck back: Züruck
title: Rechnung %{number} title: Rechnung %{number}
order_articles:
edit:
stock_alert: Preise von Lagerartikeln können nicht geändert werden!
title: Artikel aktualisieren
new:
title: Neuer gelieferter Artikel die Bestellung
ordergroups: ordergroups:
index: index:
new_transaction: Neue Überweisungen eingeben new_transaction: Neue Überweisungen eingeben
@ -741,6 +746,7 @@ de:
new_invoice: Rechnung anlegen new_invoice: Rechnung anlegen
show_invoice: Rechnung anzeigen show_invoice: Rechnung anzeigen
orders: orders:
old_price: Alter Preis
option_choose: Lieferantin/Lager auswählen option_choose: Lieferantin/Lager auswählen
option_stock: Lager option_stock: Lager
order_pdf: PDF erstellen order_pdf: PDF erstellen
@ -1085,6 +1091,12 @@ de:
model: model:
error_single_group: ! '%{user} ist schon in einer anderen Bestellgruppe' error_single_group: ! '%{user} ist schon in einer anderen Bestellgruppe'
invalid_balance: ist keine gültige Zahl invalid_balance: ist keine gültige Zahl
order_articles:
edit:
stock_alert: Preise von Lagerartikeln können nicht geändert werden!
title: Artikel aktualisieren
new:
title: Neuer gelieferter Artikel der Bestellung
orders: orders:
articles: articles:
article_count: ! 'Bestellte Artikel:' article_count: ! 'Bestellte Artikel:'
@ -1096,6 +1108,9 @@ de:
notice: Die Bestellung wurde erstellt. notice: Die Bestellung wurde erstellt.
edit: edit:
title: Bestellung bearbeiten title: Bestellung bearbeiten
edit_amount:
field_locked_title: Die Verteilung dieses Artikels auf die einzelnen Bestellgruppen wurde manuell angepasst. Das Eingabefeld wurde gesperrt, um die manuellen Änderungen zu bewahren. Um den Artikel neu zu verteilen, drücke den Entsperrknopf und ändere die gelieferte Menge.
field_unlocked_title: Die Verteilung dieses Artikels auf die einzelnen Bestellgruppen wurde manuell angepasst. Wenn du die gelieferte Menge änderst, werden die vorherigen manuellen Änderungen überschrieben.
fax: fax:
amount: Menge amount: Menge
articles: Artikel articles: Artikel
@ -1114,12 +1129,14 @@ de:
title: Artikel title: Artikel
index: index:
action_end: Beenden action_end: Beenden
action_receive: In Empfang nehmen
confirm_delete: Willst Du wirklich die Bestellung löschen? confirm_delete: Willst Du wirklich die Bestellung löschen?
confirm_end: Willst Du wirklich die Bestellung %{order} beenden? Es gibt kein zurück. confirm_end: Willst Du wirklich die Bestellung %{order} beenden? Es gibt kein zurück.
ended_orders: Beendete Bestellungen
new_order: Neue Bestellung anlegen new_order: Neue Bestellung anlegen
no_open_orders: Derzeit gibt es keine laufende Bestellungen. no_open_or_finished_orders: Derzeit gibt es keine laufende oder beendete Bestellungen.
open_orders: Laufende Bestellungen orders_finished: Beendet
orders_open: Laufend
orders_settled: Abgerechnet
title: Bestellungen verwalten title: Bestellungen verwalten
model: model:
error_closed: Bestellung wurde schon abgerechnet error_closed: Bestellung wurde schon abgerechnet
@ -1131,6 +1148,15 @@ de:
warning_ordered_stock: ! 'Warnung: Die rot markierten Artikel wurden in der laufenden Lagerbestellung bereits bestellt bzw. gekauft. Wenn Du sie hier abwählst, werden alle bestehenden Bestellungen bzw. Käufe dieses Artikels gelöscht und nicht abgerechnet!' warning_ordered_stock: ! 'Warnung: Die rot markierten Artikel wurden in der laufenden Lagerbestellung bereits bestellt bzw. gekauft. Wenn Du sie hier abwählst, werden alle bestehenden Bestellungen bzw. Käufe dieses Artikels gelöscht und nicht abgerechnet!'
new: new:
title: Neue Bestellung anlegen title: Neue Bestellung anlegen
receive:
add_article: Artikel hinzufügen
consider_member_tolerance: Toleranz berücksichtigen
notice: ! 'Bestellung in Empfang genommen: %{msg}'
notice_none: Keine neuen Artikel für den Empfang ausgewählt.
rest_to_stock: Rest ins Lager
submit: Bestellung in Empfang nehmen
surplus_options: 'Verteilungsoptionen:'
title: »%{order}« in Empfang nehmen
show: show:
action_end: Beenden! action_end: Beenden!
amounts: ! 'Netto/Bruttosumme:' amounts: ! 'Netto/Bruttosumme:'
@ -1162,6 +1188,11 @@ de:
open: laufend open: laufend
update: update:
notice: Die Bestellung wurde aktualisiert. notice: Die Bestellung wurde aktualisiert.
update_order_amounts:
update_order_amounts:
msg1: "%{count} Artikel (%{units} Einheiten) aktualisiert"
msg2: "%{count} (%{units}) Toleranzmenge"
msg4: "%{count} (%{units}) übrig"
pages: pages:
all: all:
new_page: Neue Seite anlegen new_page: Neue Seite anlegen

View file

@ -77,10 +77,17 @@ en:
note: Note note: Note
starts: Starts at starts: Starts at
status: Status status: Status
supplier: Supplier
order_article: order_article:
article: Article
missing_units: Missing units missing_units: Missing units
missing_units_short: Missing missing_units_short: Missing
units_to_order: Amount of units quantity: Desired amount
quantity_short: Desired
units_received: Received units
units_received_short: Received
units_to_order: Ordered units
units_to_order_short: Ordered
update_current_price: Globally update current price update_current_price: Globally update current price
order_comment: order_comment:
text: Add comment to this order ... text: Add comment to this order ...
@ -617,12 +624,6 @@ en:
show: show:
back: Back back: Back
title: Invoice %{number} title: Invoice %{number}
order_articles:
edit:
stock_alert: The price of stock articles cannot be changed!
title: Update article
new:
title: Add delivered article to order
ordergroups: ordergroups:
index: index:
new_transaction: Add new transactions new_transaction: Add new transactions
@ -745,6 +746,7 @@ en:
new_invoice: New invoice new_invoice: New invoice
show_invoice: Show invoice show_invoice: Show invoice
orders: orders:
old_price: Old price
option_choose: Choose supplier/stock option_choose: Choose supplier/stock
option_stock: Stock option_stock: Stock
order_pdf: Create PDF order_pdf: Create PDF
@ -1089,6 +1091,12 @@ en:
model: model:
error_single_group: ! '%{user} is already a member of another ordergroup' error_single_group: ! '%{user} is already a member of another ordergroup'
invalid_balance: is not a valid number invalid_balance: is not a valid number
order_articles:
edit:
stock_alert: The price of stock articles cannot be changed!
title: Update article
new:
title: Add delivered article to order
orders: orders:
articles: articles:
article_count: ! 'Ordered articles:' article_count: ! 'Ordered articles:'
@ -1100,6 +1108,9 @@ en:
notice: The order was created. notice: The order was created.
edit: edit:
title: Edit order title: Edit order
edit_amount:
field_locked_title: The distribution of this article among the ordergroups was changed manually. This field is locked to protect those changes. To redistribute and overwrite those changes, press the unlock button and change the amount.
field_unlocked_title: The distribution of this article among the ordergroups was changed manually. When you change the amount, those manual changes will be overwritten.
fax: fax:
amount: Amount amount: Amount
articles: Articles articles: Articles
@ -1118,12 +1129,14 @@ en:
title: Article title: Article
index: index:
action_end: Close action_end: Close
action_receive: Receive
confirm_delete: Do you really want to delete the order? confirm_delete: Do you really want to delete the order?
confirm_end: Do you really want to close the order %{order}? There is no going back. confirm_end: Do you really want to close the order %{order}? There is no going back.
ended_orders: Closed orders
new_order: Create new order new_order: Create new order
no_open_orders: There are currently no open orders. no_open_or_finished_orders: There are currently no open or closed orders.
open_orders: Current orders orders_finished: Closed
orders_open: Open
orders_settled: Settled
title: Manage orders title: Manage orders
model: model:
error_closed: Order was already settled error_closed: Order was already settled
@ -1135,6 +1148,15 @@ en:
warning_ordered_stock: ! 'Warning: Articles marked red have already been ordered/purchased within this open stock order. If you uncheck them here, all existing orders/purchases of these articles will be deleted and it will not be accounted for them.' warning_ordered_stock: ! 'Warning: Articles marked red have already been ordered/purchased within this open stock order. If you uncheck them here, all existing orders/purchases of these articles will be deleted and it will not be accounted for them.'
new: new:
title: Create new order title: Create new order
receive:
add_article: Add article
consider_member_tolerance: consider tolerance
notice: ! 'Order received: %{msg}'
notice_none: No new articles to receive
rest_to_stock: rest to stock
submit: Receive order
surplus_options: 'Distribution options:'
title: Receiving %{order}
show: show:
action_end: Close! action_end: Close!
amounts: ! 'Net/gross sum:' amounts: ! 'Net/gross sum:'
@ -1166,6 +1188,11 @@ en:
open: open open: open
update: update:
notice: The order was updated. notice: The order was updated.
update_order_amounts:
msg1: "%{count} articles (%{units} units) updated"
msg2: "%{count} (%{units}) using tolerance"
msg3: "%{count} (%{units}) go to stock if foodsoft would support that [don't translate]"
msg4: "%{count} (%{units}) left over"
pages: pages:
all: all:
new_page: Create new page new_page: Create new page

View file

@ -78,6 +78,7 @@ fr:
starts: Ouverture le starts: Ouverture le
status: status:
order_article: order_article:
article: Article
missing_units: Unités manquantes missing_units: Unités manquantes
missing_units_short: missing_units_short:
units_to_order: Quantité units_to_order: Quantité
@ -627,12 +628,6 @@ fr:
show: show:
back: Retour back: Retour
title: Facture %{number} title: Facture %{number}
order_articles:
edit:
stock_alert:
title: Mettre à jour la liste des article
new:
title:
ordergroups: ordergroups:
index: index:
new_transaction: Saisir une nouvelle transaction new_transaction: Saisir une nouvelle transaction
@ -1094,6 +1089,12 @@ fr:
model: model:
error_single_group: ! '%{user} fait déjà partie d''une autre cellule' error_single_group: ! '%{user} fait déjà partie d''une autre cellule'
invalid_balance: n'est pas un nombre valide invalid_balance: n'est pas un nombre valide
order_articles:
edit:
stock_alert:
title: Mettre à jour la liste des article
new:
title:
orders: orders:
articles: articles:
article_count: ! 'Articles commandés:' article_count: ! 'Articles commandés:'
@ -1125,10 +1126,11 @@ fr:
action_end: Terminer action_end: Terminer
confirm_delete: Vraiment supprimer la commande? confirm_delete: Vraiment supprimer la commande?
confirm_end: Veux tu vraiment mettre fin à la commande %{order}? Attention, il n'y aura pas d'annulation possible. confirm_end: Veux tu vraiment mettre fin à la commande %{order}? Attention, il n'y aura pas d'annulation possible.
ended_orders: Commandes closes
new_order: Définir une nouvelle commande new_order: Définir une nouvelle commande
no_open_orders: Il n'y a aucune commande en cours en ce moment. no_open_or_finished_orders: Il n'y a aucune commande en cours en ce moment.
open_orders: Commandes en cours orders_finished: Close
orders_open: En cours
orders_settled: Décomptée
title: Gestion des commandes title: Gestion des commandes
model: model:
error_closed: Cette commande a déjà été décomptée error_closed: Cette commande a déjà été décomptée

View file

@ -78,6 +78,7 @@ nl:
starts: Start op starts: Start op
status: Status status: Status
order_article: order_article:
article: Artikel
missing_units: Missende eenheden missing_units: Missende eenheden
missing_units_short: Nodig missing_units_short: Nodig
units_to_order: Aantal eenheden units_to_order: Aantal eenheden
@ -617,12 +618,6 @@ nl:
show: show:
back: Terug back: Terug
title: Factuur %{number} title: Factuur %{number}
order_articles:
edit:
stock_alert: De prijs van voorraadartikelen kan niet aangepast worden!
title: Artikel bijwerken
new:
title: Geleverd artikel aan bestelling toevoegen
ordergroups: ordergroups:
index: index:
new_transaction: Nieuwe transacties toevoegen new_transaction: Nieuwe transacties toevoegen
@ -1069,6 +1064,12 @@ nl:
model: model:
error_single_group: ! '%{user} behoort al tot een ander huishouden' error_single_group: ! '%{user} behoort al tot een ander huishouden'
invalid_balance: is geen geldig nummer invalid_balance: is geen geldig nummer
order_articles:
edit:
stock_alert: De prijs van voorraadartikelen kan niet aangepast worden!
title: Artikel bijwerken
new:
title: Geleverd artikel aan bestelling toevoegen
orders: orders:
articles: articles:
article_count: ! 'Bestelde artikelen:' article_count: ! 'Bestelde artikelen:'
@ -1100,10 +1101,11 @@ nl:
action_end: Sluiten action_end: Sluiten
confirm_delete: Wil je de bestelling werkelijk verwijderen? confirm_delete: Wil je de bestelling werkelijk verwijderen?
confirm_end: Wil je de bestelling %{order} werkelijk sluiten? Dit kun je niet ongedaan maken. confirm_end: Wil je de bestelling %{order} werkelijk sluiten? Dit kun je niet ongedaan maken.
ended_orders: Gesloten bestellingen
new_order: Nieuwe bestelling openen new_order: Nieuwe bestelling openen
no_open_orders: Er zijn momenteel geen lopende bestellingen. no_open_or_finished_orders: Er zijn momenteel geen open of gesloten bestellingen.
open_orders: Lopende bestellingen orders_finished: Gesloten
orders_open: Open
orders_settled: Afgerekend
title: Bestellingen beheren title: Bestellingen beheren
model: model:
error_closed: Bestelling was al afgerekend error_closed: Bestelling was al afgerekend

View file

@ -38,7 +38,15 @@ Foodsoft::Application.routes.draw do
member do member do
post :finish post :finish
post :add_comment post :add_comment
get :receive
post :receive
get :receive_on_order_article_create
get :receive_on_order_article_update
end end
resources :order_articles
end end
resources :group_orders do resources :group_orders do
@ -141,9 +149,10 @@ Foodsoft::Application.routes.draw do
get :confirm get :confirm
put :close put :close
put :close_direct put :close_direct
get :new_on_order_article_create
get :new_on_order_article_update
end end
resources :order_articles
end end
resources :group_order_articles do resources :group_order_articles do

View file

@ -0,0 +1,6 @@
class AddQuantitiesToOrderArticle < ActiveRecord::Migration
def change
add_column :order_articles, :units_billed, :integer
add_column :order_articles, :units_received, :integer
end
end

View file

@ -0,0 +1,6 @@
class AddResultComputedToGroupOrderArticles < ActiveRecord::Migration
def change
add_column :group_order_articles, :result_computed,
:decimal, :precision => 8, :scale => 3
end
end

View file

@ -11,7 +11,7 @@
# #
# It's strongly recommended to check this file into your version control system. # It's strongly recommended to check this file into your version control system.
ActiveRecord::Schema.define(:version => 20130920201529) do ActiveRecord::Schema.define(:version => 20140102170431) do
create_table "article_categories", :force => true do |t| create_table "article_categories", :force => true do |t|
t.string "name", :default => "", :null => false t.string "name", :default => "", :null => false
@ -101,6 +101,7 @@ ActiveRecord::Schema.define(:version => 20130920201529) do
t.integer "tolerance", :default => 0, :null => false t.integer "tolerance", :default => 0, :null => false
t.datetime "updated_on", :null => false t.datetime "updated_on", :null => false
t.decimal "result", :precision => 8, :scale => 3 t.decimal "result", :precision => 8, :scale => 3
t.decimal "result_computed", :precision => 8, :scale => 3
end end
add_index "group_order_articles", ["group_order_id", "order_article_id"], :name => "goa_index", :unique => true add_index "group_order_articles", ["group_order_id", "order_article_id"], :name => "goa_index", :unique => true
@ -195,6 +196,8 @@ ActiveRecord::Schema.define(:version => 20130920201529) do
t.integer "units_to_order", :default => 0, :null => false t.integer "units_to_order", :default => 0, :null => false
t.integer "lock_version", :default => 0, :null => false t.integer "lock_version", :default => 0, :null => false
t.integer "article_price_id" t.integer "article_price_id"
t.integer "units_billed"
t.integer "units_received"
end end
add_index "order_articles", ["order_id", "article_id"], :name => "index_order_articles_on_order_id_and_article_id", :unique => true add_index "order_articles", ["order_id", "article_id"], :name => "index_order_articles_on_order_id_and_article_id", :unique => true

View file

@ -24,8 +24,4 @@ FactoryGirl.define do
end end
end end
# requires order and article
factory :order_article do
end
end end

View file

@ -0,0 +1,106 @@
require_relative '../spec_helper'
describe 'receiving an order', :type => :feature do
let(:admin) { create :user, groups:[create(:workgroup, role_orders: true)] }
let(:supplier) { create :supplier }
let(:article) { create :article, supplier: supplier, unit_quantity: 3 }
let(:order) { create :order, supplier: supplier, article_ids: [article.id] } # need to ref article
let(:go1) { create :group_order, order: order }
let(:go2) { create :group_order, order: order }
let(:oa) { order.order_articles.find_by_article_id(article.id) }
let(:goa1) { create :group_order_article, group_order: go1, order_article: oa }
let(:goa2) { create :group_order_article, group_order: go2, order_article: oa }
# set quantities of group_order_articles
def set_quantities(q1, q2)
goa1.update_quantities(*q1)
goa2.update_quantities(*q2)
oa.update_results!
order.finish!(admin)
reload_articles
end
# reload all group_order_articles
def reload_articles
[goa1, goa2].map(&:reload)
oa.reload
end
def check_quantities(units, q1, q2)
reload_articles
expect(oa.units).to eq units
expect(goa1.result).to be_within(1e-3).of q1
expect(goa2.result).to be_within(1e-3).of q2
end
describe :type => :feature, :js => true do
before { login admin }
it 'has product ordered visible' do
set_quantities [3,0], [0,0]
visit receive_order_path(order)
expect(page).to have_content(article.name)
expect(page).to have_selector("#order_article_#{oa.id}")
end
it 'has product not ordered invisible' do
set_quantities [0,0], [0,0]
visit receive_order_path(order)
expect(page).to_not have_selector("#order_article_#{oa.id}")
end
it 'is not received by default' do
set_quantities [3,0], [0,0]
visit receive_order_path(order)
expect(find("#order_articles_#{oa.id}_units_received").value).to eq ''
end
it 'does not change anything when received is ordered' do
set_quantities [2,0], [3,2]
visit receive_order_path(order)
fill_in "order_articles_#{oa.id}_units_received", :with => oa.units_to_order
find('input[type="submit"]').click
expect(page).to have_selector('body')
check_quantities 2, 2, 4
end
it 'redistributes properly when received is more' do
set_quantities [2,0], [3,2]
visit receive_order_path(order)
fill_in "order_articles_#{oa.id}_units_received", :with => 3
find('input[type="submit"]').click
expect(page).to have_selector('body')
check_quantities 3, 2, 5
end
it 'redistributes properly when received is less' do
set_quantities [2,0], [3,2]
visit receive_order_path(order)
fill_in "order_articles_#{oa.id}_units_received", :with => 1
find('input[type="submit"]').click
expect(page).to have_selector('body')
check_quantities 1, 2, 1
end
it 'has a locked field when edited elsewhere' do
set_quantities [2,0], [3,2]
goa1.result = goa1.result + 1
goa1.save!
visit receive_order_path(order)
expect(find("#order_articles_#{oa.id}_units_received")).to be_disabled
end
it 'leaves locked rows alone when submitted' do
set_quantities [2,0], [3,2]
goa1.result = goa1.result + 1
goa1.save!
visit receive_order_path(order)
find('input[type="submit"]').click
expect(page).to have_selector('body')
check_quantities 2, 3, 4
end
end
end

View file

@ -0,0 +1,120 @@
require 'spec_helper'
describe OrderArticle do
let(:order) { FactoryGirl.create :order, article_count: 1 }
let(:oa) { order.order_articles.first }
it 'is not ordered by default' do
expect(OrderArticle.ordered.count).to eq 0
end
[:units_to_order, :units_billed, :units_received].each do |units|
it "is ordered when there are #{units.to_s.gsub '_', ' '}" do
oa.update_attribute units, rand(1..99)
expect(OrderArticle.ordered.count).to eq 1
end
end
it 'knows how many items there are' do
oa.units_to_order = rand(1..99)
expect(oa.units).to eq oa.units_to_order
oa.units_billed = rand(1..99)
expect(oa.units).to eq oa.units_billed
oa.units_received = rand(1..99)
expect(oa.units).to eq oa.units_received
oa.units_billed = rand(1..99)
expect(oa.units).to eq oa.units_received
oa.units_to_order = rand(1..99)
expect(oa.units).to eq oa.units_received
oa.units_received = rand(1..99)
expect(oa.units).to eq oa.units_received
end
describe 'redistribution' do
let(:admin) { FactoryGirl.create :user, groups:[FactoryGirl.create(:workgroup, role_finance: true)] }
let(:article) { FactoryGirl.create :article, unit_quantity: 3 }
let(:order) { FactoryGirl.create :order, article_ids: [article.id] }
let(:go1) { FactoryGirl.create :group_order, order: order }
let(:go2) { FactoryGirl.create :group_order, order: order }
let(:go3) { FactoryGirl.create :group_order, order: order }
let(:goa1) { FactoryGirl.create :group_order_article, group_order: go1, order_article: oa }
let(:goa2) { FactoryGirl.create :group_order_article, group_order: go2, order_article: oa }
let(:goa3) { FactoryGirl.create :group_order_article, group_order: go3, order_article: oa }
# set quantities of group_order_articles
def set_quantities(q1, q2, q3)
goa1.update_quantities(*q1)
goa2.update_quantities(*q2)
goa3.update_quantities(*q3)
oa.update_results!
order.finish!(admin)
goa_reload
end
# reload all group_order_articles
def goa_reload
[goa1, goa2, goa3].map(&:reload)
end
it 'has expected units_to_order' do
set_quantities [3,2], [1,3], [1,0]
expect(oa.units*oa.article.unit_quantity).to eq 6
expect([goa1, goa2, goa3].map(&:result)).to eq [4, 1, 1]
end
it 'does nothing when nothing has changed' do
set_quantities [3,2], [1,3], [1,0]
expect(oa.redistribute 6, [:tolerance, nil]).to eq [1, 0]
goa_reload
expect([goa1, goa2, goa3].map(&:result).map(&:to_i)).to eq [4, 1, 1]
end
it 'works when there is nothing to distribute' do
set_quantities [3,2], [1,3], [1,0]
expect(oa.redistribute 0, [:tolerance, nil]).to eq [0, 0]
goa_reload
expect([goa1, goa2, goa3].map(&:result)).to eq [0, 0, 0]
end
it 'works when quantity needs to be reduced' do
set_quantities [3,2], [1,3], [1,0]
expect(oa.redistribute 4, [:tolerance, nil]).to eq [0, 0]
goa_reload
expect([goa1, goa2, goa3].map(&:result)).to eq [3, 1, 0]
end
it 'works when quantity is increased within quantity' do
set_quantities [3,0], [2,0], [2,0]
expect([goa1, goa2, goa3].map(&:result)).to eq [3, 2, 1]
expect(oa.redistribute 7, [:tolerance, nil]).to eq [0, 0]
goa_reload
expect([goa1, goa2, goa3].map(&:result).map(&:to_i)).to eq [3, 2, 2]
end
it 'works when there is just one for the first' do
set_quantities [3,2], [1,3], [1,0]
expect(oa.redistribute 1, [:tolerance, nil]).to eq [0, 0]
goa_reload
expect([goa1, goa2, goa3].map(&:result)).to eq [1, 0, 0]
end
it 'works when there is tolerance and left-over' do
set_quantities [3,2], [1,1], [1,0]
expect(oa.redistribute 10, [:tolerance, nil]).to eq [3, 2]
goa_reload
expect([goa1, goa2, goa3].map(&:result)).to eq [5, 2, 1]
end
it 'works when redistributing without tolerance' do
set_quantities [3,2], [1,3], [1,0]
expect(oa.redistribute 8, [nil]).to eq [3]
goa_reload
expect([goa1, goa2, goa3].map(&:result)).to eq [3, 1, 1]
end
end
end