Merge pull request #222 from wvengen/feature-receive
New receive screen
This commit is contained in:
commit
8b4c292ea0
52 changed files with 1168 additions and 182 deletions
BIN
app/assets/images/package-bg.png
Normal file
BIN
app/assets/images/package-bg.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 941 B |
BIN
app/assets/images/package.png
Normal file
BIN
app/assets/images/package.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
module Finance::OrderArticlesHelper
|
||||
module OrderArticlesHelper
|
||||
|
||||
def new_order_articles_collection
|
||||
if @order.stockit?
|
||||
|
|
@ -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 = "× #{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 = " ".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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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} × #{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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
@ -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);
|
||||
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
$('#modalContainer').modal('hide');
|
||||
$('#result_table').prepend('#{j(render('finance/balancing/order_article_result', order_article: @order_article))}');
|
||||
$('#summaryChangedWarning').show();
|
||||
|
|
@ -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();
|
||||
|
|
@ -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
|
||||
|
|
@ -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'
|
||||
9
app/views/order_articles/create.js.erb
Normal file
9
app/views/order_articles/create.js.erb
Normal 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');
|
||||
|
||||
9
app/views/order_articles/update.js.erb
Normal file
9
app/views/order_articles/update.js.erb
Normal 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');
|
||||
|
||||
|
|
@ -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)}"
|
||||
|
|
|
|||
27
app/views/orders/_edit_amount.html.haml
Normal file
27
app/views/orders/_edit_amount.html.haml
Normal 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'
|
||||
99
app/views/orders/_edit_amounts.html.haml
Normal file
99
app/views/orders/_edit_amounts.html.haml
Normal 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]
|
||||
|
||||
16
app/views/orders/add_article.js.erb
Normal file
16
app/views/orders/add_article.js.erb
Normal 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();
|
||||
})();
|
||||
|
|
@ -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'
|
||||
|
|
|
|||
45
app/views/orders/receive.html.haml
Normal file
45
app/views/orders/receive.html.haml
Normal 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
|
||||
17
app/views/orders/receive_on_order_article_create.js.erb
Normal file
17
app/views/orders/receive_on_order_article_create.js.erb
Normal 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);
|
||||
|
||||
35
app/views/orders/receive_on_order_article_update.js.erb
Normal file
35
app/views/orders/receive_on_order_article_update.js.erb
Normal 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);
|
||||
|
||||
|
|
@ -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'
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue