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;
});
// Disable action of disabled buttons
$(document).on('click', 'a.disabled', function() {
return false;
});
// Show and hide loader on ajax callbacks
$('*[data-remote]').bind('ajax:beforeSend', function() {
$('#loader').show();

View File

@ -30,7 +30,19 @@ body {
// Example:
// @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
.dl-horizontal {
@ -39,7 +51,8 @@ body {
// Do not use additional margin for input in table
.form-horizontal .control-group.control-group-intable,
.form-horizontal .controls.controls-intable {
.form-horizontal .controls.controls-intable,
.input-prepend.intable {
margin: 0;
}
@ -53,7 +66,6 @@ body {
margin-bottom: 0;
}
@mainRedColor: #ED0606;
.logo {
margin: 10px 0 0 30px;
@ -134,11 +146,11 @@ table {
}
// ordering
span.used {
color: green;
.used {
color: @articleUsedColor;
}
span.unused {
color: red;
.unused {
color: @articleUnusedColor;
}
#order-footer, .article-info {
@ -202,11 +214,11 @@ tr.order-article:hover .article-info {
// ********* Articles
tr.just-updated {
color: #468847;
color: @articleUpdatedColor;
}
tr.unavailable {
color: #999;
color: @articleUnavailColor;
}
// 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
// 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)
.input-append button.add-on {
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
.modal form {
margin: 0;
@ -297,10 +368,22 @@ table.table {
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
.control-text {
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 "suppliers" then current_user.role_suppliers?
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
else false # any unknown role will always fail
end
@ -78,6 +79,10 @@ class ApplicationController < ActionController::Base
authenticate('orders')
end
def authenticate_finance_or_orders
authenticate('finance_or_orders')
end
# checks if the current_user is member of given group.
# if fails the user will redirected to startpage
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?
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
@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
def new
@order = Order.find(params[:order_id])
@order_article = @order.order_articles.build
@order_article = @order.order_articles.build(params[:order_article])
end
def create
@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
# given mentioning that the article already exists, which is desired.
@order_article = @order.order_articles.where(:article_id => params[:order_article][:article_id]).first
if @order_article and @order_article.units_to_order == 0
@order_article.units_to_order = 1
else
unless (@order_article and @order_article.units_to_order == 0)
@order_article = @order.order_articles.build(params[:order_article])
end
unless @order_article.save
render action: :new
end
@order_article.save!
rescue
render action: :new
end
def edit

View File

@ -8,7 +8,8 @@ class OrdersController < ApplicationController
# List orders
def index
@open_orders = Order.open
@open_orders = Order.open.includes(:supplier)
@finished_orders = Order.finished_not_closed.includes(:supplier)
@per_page = 15
if params['sort']
sort = case params['sort']
@ -20,7 +21,7 @@ class OrdersController < ApplicationController
else
sort = "ends DESC"
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
# 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)
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
# Renders the fax-text-file
@ -131,4 +155,46 @@ class OrdersController < ApplicationController
end
text
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

View File

@ -81,10 +81,10 @@ module ApplicationHelper
# heading, with an abbreviation title of 'foo'.
# Other options are passed through to I18n.
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)
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?
end
s

View File

@ -10,12 +10,17 @@ module DeliveriesHelper
end
end
def articles_for_select2(supplier)
supplier.articles.undeleted.reorder('articles.name ASC').map {|a| {:id => a.id, :text => "#{a.name} (#{number_to_currency a.price}/#{a.unit})"} }
def articles_for_select2(articles, except = [], &block)
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
def stock_articles_for_table(supplier)
supplier.stock_articles.undeleted.reorder('articles.name ASC')
def articles_for_table(articles)
articles.undeleted.reorder('articles.name ASC')
end
def stock_change_remove_link(stock_change_form)

View File

@ -1,4 +1,4 @@
module Finance::OrderArticlesHelper
module OrderArticlesHelper
def new_order_articles_collection
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_for_select(options)
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

View File

@ -99,57 +99,63 @@ class GroupOrderArticle < ActiveRecord::Base
# Returns a hash with three keys: :quantity / :tolerance / :total
#
# See description of the ordering algorithm in the general application documentation for details.
def calculate_result
@calculate_result ||= begin
quantity = tolerance = total_quantity = 0
def calculate_result(total = nil)
# return memoized result unless a total is given
return @calculate_result if total.nil? and not @calculate_result.nil?
# Get total
if order_article.article.is_a?(StockArticle)
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}"
quantity = tolerance = total_quantity = 0
# Get total
if not total.nil?
logger.debug "<#{order_article.article.name}> => #{total} (given)"
elsif order_article.article.is_a?(StockArticle)
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
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...
# 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.quantity, total - total_quantity].min
q = [goaq.tolerance, total - total_quantity].min
total_quantity += q
if goaq.group_order_article_id == self.id
logger.debug "increasing quantity by #{q}"
quantity += q
logger.debug "increasing tolerance by #{q}"
tolerance += q
end
break if total_quantity >= total
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
{:quantity => quantity, :tolerance => tolerance, :total => quantity + tolerance}
logger.debug "determined quantity/tolerance/total: #{quantity} / #{tolerance} / #{quantity + tolerance}"
end
# memoize result unless a total is given
r = {:quantity => quantity, :tolerance => tolerance, :total => quantity + tolerance}
@calculate_result = r if total.nil?
r
end
# Returns order result,
@ -159,9 +165,11 @@ class GroupOrderArticle < ActiveRecord::Base
self[:result] || calculate_result[type]
end
# This is used during order.finish!.
def save_results!
self.update_attribute(:result, calculate_result[:total])
# This is used for automatic distribution, e.g., in order.finish! or when receiving orders
def save_results!(article_total = nil)
new_result = calculate_result(article_total)[:total]
self.update_attribute(:result_computed, new_result)
self.update_attribute(:result, new_result)
end
# Returns total price for this individual article
@ -180,6 +188,10 @@ class GroupOrderArticle < ActiveRecord::Base
end
end
# Check if the result deviates from the result_computed
def result_manually_changed?
result != result_computed unless result.nil?
end
end

View File

@ -166,6 +166,9 @@ class Order < ActiveRecord::Base
goa.save_results!
# Delete no longer required order-history (group_order_article_quantities) and
# 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
end
end

View File

@ -12,8 +12,9 @@ class OrderArticle < ActiveRecord::Base
validate :article_and_price_exist
validates_uniqueness_of :article_id, scope: :order_id
scope :ordered, :conditions => "units_to_order > 0"
scope :ordered_or_member, -> { includes(:group_order_articles).where("units_to_order > 0 OR group_order_articles.result > 0") }
_ordered_sql = "units_to_order > 0 OR units_billed > 0 OR units_received > 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
after_destroy :update_ordergroup_prices
@ -34,7 +35,14 @@ class OrderArticle < ActiveRecord::Base
def price
article_price || article
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.
# In balancing this can differ from ordered (by supplier) quantity for this article.
def group_orders_sum
@ -81,17 +89,70 @@ class OrderArticle < ActiveRecord::Base
# Calculate price for ordered quantity.
def total_price
units_to_order * price.unit_quantity * price.price
units * price.unit_quantity * price.price
end
# Calculate gross price for ordered qunatity.
def total_gross_price
units_to_order * price.unit_quantity * price.gross_price
units * price.unit_quantity * price.gross_price
end
def ordered_quantities_equal_to_group_orders?
# the rescue is a workaround for units_to_order not being defined in integration tests
(units_to_order * price.unit_quantity) == group_orders_sum[:quantity] rescue false
def ordered_quantities_different_from_group_orders?(ordered_mark="!", billed_mark="?", received_mark="?")
if not units_received.nil?
((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
# Updates order_article and belongings during balancing process
@ -134,18 +195,24 @@ class OrderArticle < ActiveRecord::Base
units = 0 if units < 0
units
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
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
# Associate with current article price if created in a finished order
def init_from_balancing
if order.present? and order.finished?
self.article_price = article.article_prices.first
self.units_to_order = 1
end
end
@ -153,7 +220,7 @@ class OrderArticle < ActiveRecord::Base
# updates prices of ALL ordergroups - these are actually too many
# 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
order.group_orders.each { |go| go.update_price! }
order.group_orders.each(&:update_price!)
end
end

View File

@ -20,7 +20,7 @@
$('#new_stock_article').removeAttr('disabled').select2({
placeholder: '#{t '.create_stock_article'}',
data: #{articles_for_select2(@supplier).to_json},
data: #{articles_for_select2(@supplier.articles).to_json},
createSearchChoice: function(term) {
return {
id: 'new',
@ -115,12 +115,12 @@
%tfoot
%tr
%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'
- else
%input#new_stock_article{:style => 'width: 500px;'}
%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}
%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.human_attribute_name(:order_number_short), "order_number"
%th= t('.amount')
%th= heading_helper Article, :units
%th= heading_helper Article, :unit
%th= t('.net')
%th= t('.gross')
%th= heading_helper Article, :tax
%th= heading_helper Article, :deposit
%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'
%tbody#result_table
- for order_article in @articles

View File

@ -1,11 +1,12 @@
%td.closed
= link_to order_article.article.name, '#', 'data-toggle-this' => "#group_order_articles_#{order_article.id}"
%td= order_article.article.order_number
%td
= order_article.units_to_order
- unless order_article.ordered_quantities_equal_to_group_orders?
%span{:style => "color:red;font-weight: bold"} !
%td #{order_article.price.unit_quantity} &times; #{order_article.article.unit}
%td{title: units_history_line(order_article)}
= order_article.units
= pkg_helper order_article.article_price
- if s=order_article.ordered_quantities_different_from_group_orders?
%span{:style => "color:red;font-weight: bold"}= s
%td #{order_article.article.unit}
%td
= number_to_currency(order_article.price.price, :unit => "")
:plain
@ -19,8 +20,8 @@
%td #{order_article.price.tax}%
%td= order_article.price.deposit
%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'
%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'

View File

@ -19,6 +19,11 @@
%td= show_user(order.updated_by)
%td
- 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('.close'), close_direct_finance_order_path(order),
: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)
- 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
= link_to t('ui.marks.close').html_safe, '#', class: 'close', data: {dismiss: 'modal'}
%h3= t '.title'
.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|
= 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
= link_to t('ui.marks.close').html_safe, '#', class: 'close', data: {dismiss: 'modal'}
%h3= t '.title'
.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
= link_to t('ui.close'), '#', class: 'btn', data: {dismiss: 'modal'}
= 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
%tr
%th= heading_helper Article, :name
%th= heading_helper Article, :unit_quantity
%th= heading_helper Article, :unit
%th= t '.prices'
%th= t '.units_ordered'
- unless order.stockit?
- if order.stockit?
%th= t '.units_ordered'
- else
%th= 'Members'
%th= t '.units_full'
- total_net, total_gross, counter = 0, 0, 0
%tbody
@ -18,13 +20,14 @@
- order_articles.each do |order_article|
- net_price = order_article.price.price
- gross_price = order_article.price.gross_price
- units = order_article.units_to_order
- unit_quantity = order_article.price.unit_quantity
- units = order_article.units
- total_net += units * unit_quantity * net_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= "#{unit_quantity} x #{order_article.article.unit}"
%td= order_article.article.unit
%td= "#{number_to_currency(net_price)} / #{number_to_currency(gross_price)}"
- if order.stockit?
%td= units
@ -33,7 +36,9 @@
%td= "#{order_article.quantity} + #{order_article.tolerance}"
- else
%td= "#{order_article.quantity}"
%td= units
%td{title: units_history_line(order_article)}
= units
= pkg_helper order_article.price
%p
= t '.prices_sum'
= "#{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
.well
%h2= t '.open_orders'
- unless @open_orders.empty?
- if not @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
%thead
%tr
@ -20,24 +25,44 @@
%th= heading_helper Order, :note
%th{colspan: "2"}
%tbody
- for order in @open_orders
- tr_class = " active" if order.expired?
%tr{class: tr_class}
%td= order.name
%td= format_time(order.ends) unless order.ends.nil?
%td= truncate(order.note)
%td= link_to t('.action_end'), finish_order_path(order),
confirm: t('.confirm_end', order: order.name), method: :post,
class: 'btn btn-small btn-success'
- unless @open_orders.empty?
- for order in @open_orders
- tr_class = " active" if order.expired?
%tr{class: tr_class}
%td= order.name
%td= format_time(order.ends) unless order.ends.nil?
%td= truncate(order.note)
%td= link_to t('.action_end'), finish_order_path(order),
confirm: t('.confirm_end', order: order.name), method: :post,
class: 'btn btn-small btn-success'
%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.delete'), order, confirm: t('.confirm_delete'), method: :delete,
class: 'btn btn-small btn-danger'
- else
= t '.no_open_orders'
%td
= 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,
class: 'btn btn-small btn-danger'
%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
= 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?
.alert.alert-warning
= t '.warn_not_closed'
= t '.warn_not_closed'
// Order summary
.well
@ -31,6 +31,9 @@
= 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',
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?
= link_to t('ui.delete'), @order, confirm: t('.confirm_delete'), method: :delete,
class: 'btn btn-danger'

View File

@ -1,4 +1,10 @@
# only serve selected strings for i18n-js to keep filesize down
translations:
- 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
starts: Läuft vom
status: Status
supplier: Lieferant
order_article:
article: Artikel
missing_units: Fehlende Einheiten
missing_units_short: Fehlende
units_to_order: Menge
missing_units_short: Fehlend
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
order_comment:
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_sign_in: als ein anderer Benutzer anmelden
error_members_only: Diese Aktion ist nur für Mitglieder der Gruppe erlaubt!
error_token:
error_token: Zugriff verweigert (ungültiger Token)!
article_categories:
create:
notice: Die Kategorie wurde gespeichert
@ -377,7 +384,9 @@ de:
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.'
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:
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:
@ -469,7 +478,9 @@ de:
- FC-Preis
- Menge
title: ! 'Sortiermatrix der Bestellung: %{name}, beendet am %{date}'
total: Insgesamt %{count} Artikel
total:
one: Insgesamt ein Artikel
other: ! 'Insgesamt %{count} Artikel'
errors:
general: Ein Problem ist aufgetreten.
general_again: Ein Fehler ist aufgetreten. Bitte erneut versuchen.
@ -613,12 +624,6 @@ de:
show:
back: Züruck
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:
index:
new_transaction: Neue Überweisungen eingeben
@ -741,6 +746,7 @@ de:
new_invoice: Rechnung anlegen
show_invoice: Rechnung anzeigen
orders:
old_price: Alter Preis
option_choose: Lieferantin/Lager auswählen
option_stock: Lager
order_pdf: PDF erstellen
@ -1085,6 +1091,12 @@ de:
model:
error_single_group: ! '%{user} ist schon in einer anderen Bestellgruppe'
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:
articles:
article_count: ! 'Bestellte Artikel:'
@ -1096,6 +1108,9 @@ de:
notice: Die Bestellung wurde erstellt.
edit:
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:
amount: Menge
articles: Artikel
@ -1114,12 +1129,14 @@ de:
title: Artikel
index:
action_end: Beenden
action_receive: In Empfang nehmen
confirm_delete: Willst Du wirklich die Bestellung löschen?
confirm_end: Willst Du wirklich die Bestellung %{order} beenden? Es gibt kein zurück.
ended_orders: Beendete Bestellungen
new_order: Neue Bestellung anlegen
no_open_orders: Derzeit gibt es keine laufende Bestellungen.
open_orders: Laufende Bestellungen
no_open_or_finished_orders: Derzeit gibt es keine laufende oder beendete Bestellungen.
orders_finished: Beendet
orders_open: Laufend
orders_settled: Abgerechnet
title: Bestellungen verwalten
model:
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!'
new:
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:
action_end: Beenden!
amounts: ! 'Netto/Bruttosumme:'
@ -1162,6 +1188,11 @@ de:
open: laufend
update:
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:
all:
new_page: Neue Seite anlegen

View File

@ -77,10 +77,17 @@ en:
note: Note
starts: Starts at
status: Status
supplier: Supplier
order_article:
article: Article
missing_units: Missing units
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
order_comment:
text: Add comment to this order ...
@ -617,12 +624,6 @@ en:
show:
back: Back
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:
index:
new_transaction: Add new transactions
@ -745,6 +746,7 @@ en:
new_invoice: New invoice
show_invoice: Show invoice
orders:
old_price: Old price
option_choose: Choose supplier/stock
option_stock: Stock
order_pdf: Create PDF
@ -1089,6 +1091,12 @@ en:
model:
error_single_group: ! '%{user} is already a member of another ordergroup'
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:
articles:
article_count: ! 'Ordered articles:'
@ -1100,6 +1108,9 @@ en:
notice: The order was created.
edit:
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:
amount: Amount
articles: Articles
@ -1118,12 +1129,14 @@ en:
title: Article
index:
action_end: Close
action_receive: Receive
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.
ended_orders: Closed orders
new_order: Create new order
no_open_orders: There are currently no open orders.
open_orders: Current orders
no_open_or_finished_orders: There are currently no open or closed orders.
orders_finished: Closed
orders_open: Open
orders_settled: Settled
title: Manage orders
model:
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.'
new:
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:
action_end: Close!
amounts: ! 'Net/gross sum:'
@ -1166,6 +1188,11 @@ en:
open: open
update:
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:
all:
new_page: Create new page

View File

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

View File

@ -78,6 +78,7 @@ nl:
starts: Start op
status: Status
order_article:
article: Artikel
missing_units: Missende eenheden
missing_units_short: Nodig
units_to_order: Aantal eenheden
@ -617,12 +618,6 @@ nl:
show:
back: Terug
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:
index:
new_transaction: Nieuwe transacties toevoegen
@ -1069,6 +1064,12 @@ nl:
model:
error_single_group: ! '%{user} behoort al tot een ander huishouden'
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:
articles:
article_count: ! 'Bestelde artikelen:'
@ -1100,10 +1101,11 @@ nl:
action_end: Sluiten
confirm_delete: Wil je de bestelling werkelijk verwijderen?
confirm_end: Wil je de bestelling %{order} werkelijk sluiten? Dit kun je niet ongedaan maken.
ended_orders: Gesloten bestellingen
new_order: Nieuwe bestelling openen
no_open_orders: Er zijn momenteel geen lopende bestellingen.
open_orders: Lopende bestellingen
no_open_or_finished_orders: Er zijn momenteel geen open of gesloten bestellingen.
orders_finished: Gesloten
orders_open: Open
orders_settled: Afgerekend
title: Bestellingen beheren
model:
error_closed: Bestelling was al afgerekend

View File

@ -38,7 +38,15 @@ Foodsoft::Application.routes.draw do
member do
post :finish
post :add_comment
get :receive
post :receive
get :receive_on_order_article_create
get :receive_on_order_article_update
end
resources :order_articles
end
resources :group_orders do
@ -141,9 +149,10 @@ Foodsoft::Application.routes.draw do
get :confirm
put :close
put :close_direct
get :new_on_order_article_create
get :new_on_order_article_update
end
resources :order_articles
end
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.
ActiveRecord::Schema.define(:version => 20130920201529) do
ActiveRecord::Schema.define(:version => 20140102170431) do
create_table "article_categories", :force => true do |t|
t.string "name", :default => "", :null => false
@ -101,6 +101,7 @@ ActiveRecord::Schema.define(:version => 20130920201529) do
t.integer "tolerance", :default => 0, :null => false
t.datetime "updated_on", :null => false
t.decimal "result", :precision => 8, :scale => 3
t.decimal "result_computed", :precision => 8, :scale => 3
end
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 "lock_version", :default => 0, :null => false
t.integer "article_price_id"
t.integer "units_billed"
t.integer "units_received"
end
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
# requires order and article
factory :order_article do
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