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'