Merge branch 'master' into updated-gems
Conflicts: Gemfile Gemfile.lock
This commit is contained in:
commit
bdce1b5872
64 changed files with 1039 additions and 472 deletions
|
|
@ -7,6 +7,10 @@
|
|||
//= require bootstrap-datepicker/locales/bootstrap-datepicker.de
|
||||
//= require bootstrap-datepicker/locales/bootstrap-datepicker.nl
|
||||
//= require jquery.observe_field
|
||||
//= require list
|
||||
//= require list.unlist
|
||||
//= require list.delay
|
||||
//= require list.reset
|
||||
//= require rails.validations
|
||||
//= require rails.validations.simple_form
|
||||
//= require_self
|
||||
|
|
|
|||
50
app/assets/javascripts/list.delay.js
Normal file
50
app/assets/javascripts/list.delay.js
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
// for use with listjs 0.2.0
|
||||
// https://github.com/javve/list.js
|
||||
|
||||
(function(window, undefined) {
|
||||
|
||||
window.List.prototype.plugins.delay = function(locals, options) {
|
||||
var list = this;
|
||||
|
||||
this.searchTimeout = undefined;
|
||||
|
||||
var init = {
|
||||
start: function(options) {
|
||||
this.defaults(options);
|
||||
this.callbacks(options);
|
||||
this.onload(options);
|
||||
},
|
||||
defaults: function(options) {
|
||||
options.delayedSearchClass = options.delayedSearchClass || 'delayed-search';
|
||||
options.delayedSearchTime = options.delayedSearchTime || 500;
|
||||
},
|
||||
callbacks: function(options) {
|
||||
$('.' + options.delayedSearchClass, list.listContainer).keyup(list.searchDelayStart);
|
||||
},
|
||||
onload: function(options) {
|
||||
var initialSearchTerm = $('.' + options.delayedSearchClass, list.listContainer).val();
|
||||
if('' != initialSearchTerm) {
|
||||
list.search(initialSearchTerm);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.searchDelayStart = function(searchString, columns) {
|
||||
// TODO: if keycode corresponds to 'ENTER' ? skip delay
|
||||
clearTimeout(list.searchTimeout);
|
||||
list.searchTimeout = window.setTimeout(
|
||||
function() {list.searchDelayEnd(searchString, columns)},
|
||||
options.delayedSearchTime
|
||||
);
|
||||
|
||||
$(list.listContainer).trigger('updateComing');
|
||||
};
|
||||
|
||||
this.searchDelayEnd = function(searchString, columns) {
|
||||
list.search(searchString, columns);
|
||||
};
|
||||
|
||||
init.start(options);
|
||||
}
|
||||
|
||||
})(window);
|
||||
42
app/assets/javascripts/list.reset.js
Normal file
42
app/assets/javascripts/list.reset.js
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
// for use with listjs 0.2.0
|
||||
// https://github.com/javve/list.js
|
||||
|
||||
(function(window, undefined) {
|
||||
|
||||
window.List.prototype.plugins.reset = function(locals, options) {
|
||||
var list = this;
|
||||
|
||||
var init = {
|
||||
start: function(options) {
|
||||
this.defaults(options);
|
||||
this.callbacks(options);
|
||||
},
|
||||
defaults: function(options) {
|
||||
options.highlightClass = options.highlightClass || 'btn-primary';
|
||||
options.resetSearchClass = options.resetSearchClass || 'reset-search';
|
||||
options.resettableClass = options.resettableClass || 'resettable';
|
||||
},
|
||||
callbacks: function(options) {
|
||||
$('.' + options.resetSearchClass, list.listContainer).click(list.resetSearch);
|
||||
list.on('updated', list.highlightResetButton);
|
||||
|
||||
$(list.listContainer).on('updateComing', function() {
|
||||
list.highlightResetButton(false);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
this.highlightResetButton = function(highlightEnabled) {
|
||||
highlightEnabled = (undefined === highlightEnabled) ? (list.searched) : (highlightEnabled);
|
||||
$('.' + options.resetSearchClass, list.listContainer).toggleClass(options.highlightClass, highlightEnabled);
|
||||
};
|
||||
|
||||
this.resetSearch = function() {
|
||||
$('.' + options.resettableClass, list.listContainer).val('');
|
||||
list.search('');
|
||||
};
|
||||
|
||||
init.start(options);
|
||||
}
|
||||
|
||||
})(window);
|
||||
124
app/assets/javascripts/list.unlist.js
Normal file
124
app/assets/javascripts/list.unlist.js
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
// for use with listjs 0.2.0
|
||||
// https://github.com/javve/list.js
|
||||
|
||||
/*******************************************************************************
|
||||
********************************************************************************
|
||||
|
||||
The following code is a modification of list.js. It was created by copy-pasting
|
||||
the original code with the copyright notice below.
|
||||
|
||||
********************************************************************************
|
||||
*******************************************************************************/
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
Begin copyright notice of the original code
|
||||
*******************************************************************************/
|
||||
|
||||
/*
|
||||
ListJS Beta 0.2.0
|
||||
By Jonny Strömberg (www.jonnystromberg.com, www.listjs.com)
|
||||
|
||||
OBS. The API is not frozen. It MAY change!
|
||||
|
||||
License (MIT)
|
||||
|
||||
Copyright (c) 2011 Jonny Strömberg http://jonnystromberg.com
|
||||
|
||||
Permission is hereby granted, free of charge, to any person
|
||||
obtaining a copy of this software and associated documentation
|
||||
files (the "Software"), to deal in the Software without restriction,
|
||||
including without limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of the Software,
|
||||
and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
/*******************************************************************************
|
||||
End copyright notice of the original code
|
||||
*******************************************************************************/
|
||||
|
||||
|
||||
|
||||
(function(w, undefined) {
|
||||
/*******************************************************************************
|
||||
Begin copy-pasted and modified code
|
||||
*******************************************************************************/
|
||||
|
||||
// * template engine which adds class 'unlisted' instead of removing from DOM
|
||||
// * especially useful in case of formulars
|
||||
// * uses jQuery's $
|
||||
w.List.prototype.templateEngines.unlist = function(list, settings) {
|
||||
var h = w.ListJsHelpers;
|
||||
|
||||
// start with standard engine, override specific methods afterwards
|
||||
this.superClass = w.List.prototype.templateEngines.standard;
|
||||
this.superClass(list, settings);
|
||||
|
||||
// todo refer to listjs code instead of copy-pasting
|
||||
var listSource = h.getByClass(settings.listClass, list.listContainer, true);
|
||||
var templater = this;
|
||||
var ensure = {
|
||||
created: function(item) {
|
||||
if(item.elm === undefined) {
|
||||
templater.create(item);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var init = {
|
||||
start: function(options) {
|
||||
this.defaults(options);
|
||||
this.callbacks(options);
|
||||
},
|
||||
defaults: function(options) {
|
||||
options.listHeadingsClass = options.listHeadingsClass || 'list-heading';
|
||||
},
|
||||
callbacks: function(options) {
|
||||
list.on('updated', templater.updateListHeadings);
|
||||
}
|
||||
};
|
||||
|
||||
this.show = function(item) {
|
||||
ensure.created(item);
|
||||
listSource.appendChild(item.elm); // append item (or move it to the end)
|
||||
$(item.elm).removeClass('unlisted');
|
||||
};
|
||||
this.hide = function(item) {
|
||||
ensure.created(item);
|
||||
$(item.elm).addClass('unlisted');
|
||||
listSource.appendChild(item.elm);
|
||||
};
|
||||
this.clear = function() {
|
||||
$(listSource.childNodes).addClass('unlisted');
|
||||
};
|
||||
|
||||
this.updateListHeadings = function() {
|
||||
var headSel = '.' + settings.listHeadingsClass;
|
||||
|
||||
$(headSel, listSource).each(function() {
|
||||
var listedCount = $(this).nextUntil(headSel, ':not(.unlisted)').length;
|
||||
$(this).toggleClass('unlisted', 0==listedCount);
|
||||
});
|
||||
};
|
||||
|
||||
init.start(settings);
|
||||
};
|
||||
|
||||
/*******************************************************************************
|
||||
End copy-pasted and modified code
|
||||
*******************************************************************************/
|
||||
})(window);
|
||||
|
|
@ -3,4 +3,5 @@
|
|||
*= require select2
|
||||
*= require token-input-bootstrappy
|
||||
*= require bootstrap-datepicker
|
||||
*= require list.unlist
|
||||
*/
|
||||
|
|
@ -237,3 +237,8 @@ tr.unavailable {
|
|||
margin-bottom: 15px
|
||||
}
|
||||
}
|
||||
|
||||
// allow buttons as input add-on (with proper height)
|
||||
.input-append button.add-on {
|
||||
height: inherit;
|
||||
}
|
||||
|
|
|
|||
3
app/assets/stylesheets/list.unlist.css
Normal file
3
app/assets/stylesheets/list.unlist.css
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
.list .unlisted:not(.no-unlist) {
|
||||
display: none;
|
||||
}
|
||||
|
|
@ -26,7 +26,7 @@ class ApplicationController < ActionController::Base
|
|||
|
||||
def deny_access
|
||||
session[:return_to] = request.original_url
|
||||
redirect_to login_url, :alert => 'Access denied!'
|
||||
redirect_to login_url, :alert => I18n.t('application.controller.error_denied')
|
||||
end
|
||||
|
||||
private
|
||||
|
|
@ -37,7 +37,7 @@ class ApplicationController < ActionController::Base
|
|||
# No user at all: redirect to login page.
|
||||
session[:user_id] = nil
|
||||
session[:return_to] = request.original_url
|
||||
redirect_to login_url, :alert => 'Authentication required!'
|
||||
redirect_to login_url, :alert => I18n.t('application.controller.error_authn')
|
||||
else
|
||||
# We have an authenticated user, now check role...
|
||||
# Roles gets the user through his memberships.
|
||||
|
|
@ -83,7 +83,7 @@ class ApplicationController < ActionController::Base
|
|||
def authenticate_membership_or_admin
|
||||
@group = Group.find(params[:id])
|
||||
unless @group.member?(@current_user) or @current_user.role_admin?
|
||||
redirect_to root_path, alert: "Diese Aktion ist nur für Mitglieder der Gruppe erlaubt!"
|
||||
redirect_to root_path, alert: I18n.t('application.controller.error_members_only')
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ class Finance::BalancingController < Finance::BaseController
|
|||
flash.now.alert = t('finance.balancing.new.alert') if @order.closed?
|
||||
@comments = @order.comments
|
||||
|
||||
@articles = @order.order_articles.ordered.includes(:article, :article_price,
|
||||
@articles = @order.order_articles.ordered_or_member.includes(:article, :article_price,
|
||||
group_order_articles: {group_order: :ordergroup})
|
||||
|
||||
sort_param = params['sort'] || 'name'
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ class Finance::FinancialTransactionsController < ApplicationController
|
|||
@financial_transaction = FinancialTransaction.new(params[:financial_transaction])
|
||||
@financial_transaction.user = current_user
|
||||
@financial_transaction.add_transaction!
|
||||
redirect_to finance_ordergroup_transactions_url(@ordergroup), notice: t('finance.financial_transactions.create.notice')
|
||||
redirect_to finance_ordergroup_transactions_url(@ordergroup), notice: I18n.t('finance.financial_transactions.controller.create.notice')
|
||||
rescue ActiveRecord::RecordInvalid => error
|
||||
flash.now[:alert] = error.message
|
||||
render :action => :new
|
||||
|
|
@ -44,16 +44,16 @@ class Finance::FinancialTransactionsController < ApplicationController
|
|||
end
|
||||
|
||||
def create_collection
|
||||
raise "Notiz wird benötigt!" if params[:note].blank?
|
||||
raise I18n.t('finance.financial_transactions.controller.create_collection.error_note_required') if params[:note].blank?
|
||||
params[:financial_transactions].each do |trans|
|
||||
# ignore empty amount fields ...
|
||||
unless trans[:amount].blank?
|
||||
Ordergroup.find(trans[:ordergroup_id]).add_financial_transaction!(trans[:amount], params[:note], @current_user)
|
||||
end
|
||||
end
|
||||
redirect_to finance_ordergroups_url, notice: t('finance.create_collection.create.notice')
|
||||
redirect_to finance_ordergroups_url, notice: I18n.t('finance.financial_transactions.controller.create_collection.notice')
|
||||
rescue => error
|
||||
redirect_to finance_new_transaction_collection_url, alert: t('finance.create_collection.create.alert', error: error.to_s)
|
||||
redirect_to finance_new_transaction_collection_url, alert: I18n.t('finance.financial_transactions.controller.create_collection.alert', error: error.to_s)
|
||||
end
|
||||
|
||||
protected
|
||||
|
|
|
|||
|
|
@ -65,7 +65,13 @@ class Finance::GroupOrderArticlesController < ApplicationController
|
|||
|
||||
def destroy
|
||||
group_order_article = GroupOrderArticle.find(params[:id])
|
||||
group_order_article.destroy
|
||||
# only destroy if quantity and tolerance was zero already, so that we don't
|
||||
# lose what the user ordered, if any
|
||||
if group_order_article.quantity > 0 or group_order_article.tolerance >0
|
||||
group_order_article.update_attribute(:result, 0)
|
||||
else
|
||||
group_order_article.destroy
|
||||
end
|
||||
update_summaries(group_order_article)
|
||||
@order_article = group_order_article.order_article
|
||||
|
||||
|
|
|
|||
|
|
@ -42,6 +42,14 @@ class Finance::OrderArticlesController < ApplicationController
|
|||
|
||||
def destroy
|
||||
@order_article = OrderArticle.find(params[:id])
|
||||
@order_article.destroy
|
||||
# only destroy if there are no associated GroupOrders; if we would, the requested
|
||||
# quantity and tolerance would be gone. Instead of destroying, we set all result
|
||||
# quantities to zero.
|
||||
if @order_article.group_order_articles.count == 0
|
||||
@order_article.destroy
|
||||
else
|
||||
@order_article.group_order_articles.each { |goa| goa.update_attribute(:result, 0) }
|
||||
@order_article.update_results!
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -31,6 +31,11 @@ class StockitController < ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
def show
|
||||
@stock_article = StockArticle.find(params[:id])
|
||||
@stock_changes = @stock_article.stock_changes.order('stock_changes.created_at DESC')
|
||||
end
|
||||
|
||||
def destroy
|
||||
@article = StockArticle.find(params[:id])
|
||||
@article.mark_as_deleted
|
||||
|
|
@ -55,9 +60,4 @@ class StockitController < ApplicationController
|
|||
|
||||
render :partial => 'form', :locals => {:stock_article => stock_article}
|
||||
end
|
||||
|
||||
def history
|
||||
@stock_article = StockArticle.undeleted.find(params[:stock_article_id])
|
||||
@stock_changes = @stock_article.stock_changes.order('stock_changes.created_at DESC').each {|s| s.readonly!}
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -12,20 +12,29 @@ class OrderByArticles < OrderPdf
|
|||
|
||||
def body
|
||||
@order.order_articles.ordered.each do |order_article|
|
||||
text "#{order_article.article.name} (#{order_article.article.unit} | #{order_article.price.unit_quantity.to_s} | #{number_with_precision(order_article.price.fc_price, precision: 2)})",
|
||||
style: :bold, size: 10
|
||||
rows = []
|
||||
rows << I18n.t('documents.order_by_articles.rows')
|
||||
for goa in order_article.group_order_articles
|
||||
dimrows = []
|
||||
for goa in order_article.group_order_articles.ordered
|
||||
rows << [goa.group_order.ordergroup.name,
|
||||
"#{goa.quantity} + #{goa.tolerance}",
|
||||
goa.result,
|
||||
number_with_precision(order_article.price.fc_price * goa.result, precision: 2)]
|
||||
dimrows << rows.length if goa.result == 0
|
||||
end
|
||||
next if rows.length == 0
|
||||
rows.unshift I18n.t('documents.order_by_articles.rows') # table header
|
||||
|
||||
table rows, column_widths: [200,40,40], cell_style: {size: 8, overflow: :shrink_to_fit} do |table|
|
||||
table.columns(1..2).align = :right
|
||||
text "#{order_article.article.name} (#{order_article.article.unit} | #{order_article.price.unit_quantity.to_s} | #{number_with_precision(order_article.price.fc_price, precision: 2)})",
|
||||
style: :bold, size: 10
|
||||
table rows, cell_style: {size: 8, overflow: :shrink_to_fit} do |table|
|
||||
table.column(0).width = 200
|
||||
table.columns(1..3).align = :right
|
||||
table.column(2).font_style = :bold
|
||||
table.cells.border_width = 1
|
||||
table.cells.border_color = '666666'
|
||||
table.rows(0).border_bottom_width = 2
|
||||
# dim rows which were ordered but not received
|
||||
dimrows.each { |ri| table.row(ri).text_color = '999999' }
|
||||
end
|
||||
move_down 10
|
||||
end
|
||||
|
|
|
|||
|
|
@ -12,12 +12,10 @@ class OrderByGroups < OrderPdf
|
|||
|
||||
def body
|
||||
# Start rendering
|
||||
@order.group_orders.each do |group_order|
|
||||
text group_order.ordergroup.name, size: 9, style: :bold
|
||||
|
||||
@order.group_orders.ordered.each do |group_order|
|
||||
total = 0
|
||||
rows = []
|
||||
rows << I18n.t('documents.order_by_groups.rows') # Table Header
|
||||
dimrows = []
|
||||
|
||||
group_order_articles = group_order.group_order_articles.ordered
|
||||
group_order_articles.each do |goa|
|
||||
|
|
@ -25,15 +23,20 @@ class OrderByGroups < OrderPdf
|
|||
sub_total = price * goa.result
|
||||
total += sub_total
|
||||
rows << [goa.order_article.article.name,
|
||||
"#{goa.quantity} + #{goa.tolerance}",
|
||||
goa.result,
|
||||
number_with_precision(price, precision: 2),
|
||||
goa.order_article.price.unit_quantity,
|
||||
goa.order_article.article.unit,
|
||||
number_with_precision(sub_total, precision: 2)]
|
||||
dimrows << rows.length if goa.result == 0
|
||||
end
|
||||
rows << [ I18n.t('documents.order_by_groups.sum'), nil, nil, nil, nil, number_with_precision(total, precision: 2)]
|
||||
next if rows.length == 0
|
||||
rows << [ I18n.t('documents.order_by_groups.sum'), nil, nil, nil, nil, nil, number_with_precision(total, precision: 2)]
|
||||
rows.unshift I18n.t('documents.order_by_groups.rows') # Table Header
|
||||
|
||||
table rows, column_widths: [250,50,50,50,50,50], cell_style: {size: 8, overflow: :shrink_to_fit} do |table|
|
||||
text group_order.ordergroup.name, size: 9, style: :bold
|
||||
table rows, width: 500, cell_style: {size: 8, overflow: :shrink_to_fit} do |table|
|
||||
# borders
|
||||
table.cells.borders = []
|
||||
table.row(0).borders = [:bottom]
|
||||
|
|
@ -41,8 +44,14 @@ class OrderByGroups < OrderPdf
|
|||
table.cells.border_width = 1
|
||||
table.cells.border_color = '666666'
|
||||
|
||||
table.columns(1..3).align = :right
|
||||
table.columns(5).align = :right
|
||||
table.column(0).width = 240
|
||||
table.column(2).font_style = :bold
|
||||
table.columns(1..4).align = :right
|
||||
table.column(6).align = :right
|
||||
table.column(6).font_style = :bold
|
||||
|
||||
# dim rows which were ordered but not received
|
||||
dimrows.each { |ri| table.row(ri).text_color = '999999' }
|
||||
end
|
||||
|
||||
move_down 15
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
# encoding: utf-8
|
||||
class Article < ActiveRecord::Base
|
||||
extend ActiveSupport::Memoizable # Ability to cache method results. Use memoize :expensive_method
|
||||
|
||||
# Replace numeric seperator with database format
|
||||
localize_input_of :price, :tax, :deposit
|
||||
|
|
@ -44,11 +43,12 @@ class Article < ActiveRecord::Base
|
|||
|
||||
# If the article is used in an open Order, the Order will be returned.
|
||||
def in_open_order
|
||||
order_articles = OrderArticle.all(:conditions => ['order_id IN (?)', Order.open.collect(&:id)])
|
||||
order_article = order_articles.detect {|oa| oa.article_id == id }
|
||||
order_article ? order_article.order : nil
|
||||
@in_open_order ||= begin
|
||||
order_articles = OrderArticle.all(:conditions => ['order_id IN (?)', Order.open.collect(&:id)])
|
||||
order_article = order_articles.detect {|oa| oa.article_id == id }
|
||||
order_article ? order_article.order : nil
|
||||
end
|
||||
end
|
||||
memoize :in_open_order
|
||||
|
||||
# Returns true if the article has been ordered in the given order at least once
|
||||
def ordered_in_order?(order)
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ class GroupOrder < ActiveRecord::Base
|
|||
scope :in_open_orders, joins(:order).merge(Order.open)
|
||||
scope :in_finished_orders, joins(:order).merge(Order.finished_not_closed)
|
||||
|
||||
scope :ordered, :include => :ordergroup, :order => 'groups.name'
|
||||
|
||||
# Generate some data for the javascript methods in ordering view
|
||||
def load_data
|
||||
data = {}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
# The chronologically order of the Ordergroup - activity are stored in GroupOrderArticleQuantity
|
||||
#
|
||||
class GroupOrderArticle < ActiveRecord::Base
|
||||
extend ActiveSupport::Memoizable # Ability to cache method results. Use memoize :expensive_method
|
||||
|
||||
belongs_to :group_order
|
||||
belongs_to :order_article
|
||||
|
|
@ -14,7 +13,7 @@ class GroupOrderArticle < ActiveRecord::Base
|
|||
validates_inclusion_of :tolerance, :in => 0..99
|
||||
validates_uniqueness_of :order_article_id, :scope => :group_order_id # just once an article per group order
|
||||
|
||||
scope :ordered, :conditions => 'result > 0'
|
||||
scope :ordered, :conditions => 'group_order_articles.result > 0 OR group_order_articles.quantity > 0 OR group_order_articles.tolerance > 0', :include => {:group_order => :ordergroup}, :order => 'groups.name'
|
||||
|
||||
localize_input_of :result
|
||||
|
||||
|
|
@ -101,54 +100,55 @@ class GroupOrderArticle < ActiveRecord::Base
|
|||
#
|
||||
# See description of the ordering algorithm in the general application documentation for details.
|
||||
def calculate_result
|
||||
quantity = tolerance = 0
|
||||
stockit = order_article.article.is_a?(StockArticle)
|
||||
@calculate_result ||= begin
|
||||
quantity = tolerance = 0
|
||||
stockit = order_article.article.is_a?(StockArticle)
|
||||
|
||||
# Get total
|
||||
total = stockit ? order_article.article.quantity : order_article.units_to_order * order_article.price.unit_quantity
|
||||
logger.debug("<#{order_article.article.name}>.unitsToOrder => items ordered: #{order_article.units_to_order} => #{total}")
|
||||
# Get total
|
||||
total = stockit ? order_article.article.quantity : order_article.units_to_order * order_article.price.unit_quantity
|
||||
logger.debug("<#{order_article.article.name}>.unitsToOrder => items ordered: #{order_article.units_to_order} => #{total}")
|
||||
|
||||
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}")
|
||||
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...
|
||||
total_quantity = i = 0
|
||||
while (i < order_quantities.size && total_quantity < total)
|
||||
q = (order_quantities[i].quantity <= total - total_quantity ? order_quantities[i].quantity : total - total_quantity)
|
||||
total_quantity += q
|
||||
if (order_quantities[i].group_order_article_id == self.id)
|
||||
logger.debug("increasing quantity by #{q}")
|
||||
quantity += q
|
||||
end
|
||||
i += 1
|
||||
end
|
||||
|
||||
# Determine tolerance to be ordered...
|
||||
if (total_quantity < total)
|
||||
logger.debug("determining additional items to be ordered from tolerance")
|
||||
i = 0
|
||||
# Determine quantities to be ordered...
|
||||
total_quantity = i = 0
|
||||
while (i < order_quantities.size && total_quantity < total)
|
||||
q = (order_quantities[i].tolerance <= total - total_quantity ? order_quantities[i].tolerance : total - total_quantity)
|
||||
q = (order_quantities[i].quantity <= total - total_quantity ? order_quantities[i].quantity : total - total_quantity)
|
||||
total_quantity += q
|
||||
if (order_quantities[i].group_order_article_id == self.id)
|
||||
logger.debug("increasing tolerance by #{q}")
|
||||
tolerance += q
|
||||
logger.debug("increasing quantity by #{q}")
|
||||
quantity += q
|
||||
end
|
||||
i += 1
|
||||
end
|
||||
|
||||
# Determine tolerance to be ordered...
|
||||
if (total_quantity < total)
|
||||
logger.debug("determining additional items to be ordered from tolerance")
|
||||
i = 0
|
||||
while (i < order_quantities.size && total_quantity < total)
|
||||
q = (order_quantities[i].tolerance <= total - total_quantity ? order_quantities[i].tolerance : total - total_quantity)
|
||||
total_quantity += q
|
||||
if (order_quantities[i].group_order_article_id == self.id)
|
||||
logger.debug("increasing tolerance by #{q}")
|
||||
tolerance += q
|
||||
end
|
||||
i += 1
|
||||
end
|
||||
end
|
||||
|
||||
logger.debug("determined quantity/tolerance/total: #{quantity} / #{tolerance} / #{quantity + tolerance}")
|
||||
end
|
||||
|
||||
logger.debug("determined quantity/tolerance/total: #{quantity} / #{tolerance} / #{quantity + tolerance}")
|
||||
{:quantity => quantity, :tolerance => tolerance, :total => quantity + tolerance}
|
||||
end
|
||||
|
||||
{:quantity => quantity, :tolerance => tolerance, :total => quantity + tolerance}
|
||||
end
|
||||
memoize :calculate_result
|
||||
|
||||
# Returns order result,
|
||||
# either calcualted on the fly or fetched from result attribute
|
||||
|
|
@ -167,7 +167,7 @@ class GroupOrderArticle < ActiveRecord::Base
|
|||
# the minimum price depending on configuration. When the order is finished it
|
||||
# will be the value depending of the article results.
|
||||
def total_price(order_article = self.order_article)
|
||||
unless order_article.order.finished?
|
||||
if order_article.order.open?
|
||||
if FoodsoftConfig[:tolerance_is_costly]
|
||||
order_article.article.fc_price * (quantity + tolerance)
|
||||
else
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
#
|
||||
class Order < ActiveRecord::Base
|
||||
|
||||
attr_accessor :ignore_warnings
|
||||
|
||||
# Associations
|
||||
has_many :order_articles, :dependent => :destroy
|
||||
has_many :articles, :through => :order_articles
|
||||
|
|
@ -17,6 +19,7 @@ class Order < ActiveRecord::Base
|
|||
# Validations
|
||||
validates_presence_of :starts
|
||||
validate :starts_before_ends, :include_articles
|
||||
validate :keep_ordered_articles
|
||||
|
||||
# Callbacks
|
||||
after_save :save_order_articles, :update_price_of_group_orders
|
||||
|
|
@ -55,7 +58,12 @@ class Order < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def article_ids
|
||||
@article_ids ||= order_articles.map(&:article_id)
|
||||
@article_ids ||= order_articles.map { |a| a.article_id.to_s }
|
||||
end
|
||||
|
||||
# Returns an array of article ids that lead to a validation error.
|
||||
def erroneous_article_ids
|
||||
@erroneous_article_ids ||= []
|
||||
end
|
||||
|
||||
def open?
|
||||
|
|
@ -209,24 +217,24 @@ class Order < ActiveRecord::Base
|
|||
protected
|
||||
|
||||
def starts_before_ends
|
||||
errors.add(:ends, I18n.t('articles.model.error_starts_before_ends')) if (ends && starts && ends <= starts)
|
||||
errors.add(:ends, I18n.t('orders.model.error_starts_before_ends')) if (ends && starts && ends <= starts)
|
||||
end
|
||||
|
||||
def include_articles
|
||||
errors.add(:articles, I18n.t('articles.model.error_nosel')) if article_ids.empty?
|
||||
errors.add(:articles, I18n.t('orders.model.error_nosel')) if article_ids.empty?
|
||||
end
|
||||
|
||||
def keep_ordered_articles
|
||||
chosen_order_articles = order_articles.find_all_by_article_id(article_ids)
|
||||
to_be_removed = order_articles - chosen_order_articles
|
||||
to_be_removed_but_ordered = to_be_removed.select { |a| a.quantity > 0 or a.tolerance > 0 }
|
||||
unless to_be_removed_but_ordered.empty? or ignore_warnings
|
||||
errors.add(:articles, I18n.t(stockit? ? 'orders.model.warning_ordered_stock' : 'orders.model.warning_ordered'))
|
||||
@erroneous_article_ids = to_be_removed_but_ordered.map { |a| a.article_id }
|
||||
end
|
||||
end
|
||||
|
||||
def save_order_articles
|
||||
#self.articles = Article.find(article_ids) # This doesn't deletes the group_order_articles, belonging to order_articles,
|
||||
# # see http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#method-i-has_many
|
||||
#
|
||||
## Ensure to delete also the group_order_articles, belonging to order_articles
|
||||
## This case is relevant, when removing articles from a running order
|
||||
#goa_ids = GroupOrderArticle.where(group_order_id: group_order_ids).includes(:order_article).
|
||||
# select { |goa| goa.order_article.nil? }.map(&:id)
|
||||
#GroupOrderArticle.delete_all(id: goa_ids) unless goa_ids.empty?
|
||||
|
||||
|
||||
# fetch selected articles
|
||||
articles_list = Article.find(article_ids)
|
||||
# create new order_articles
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@ class OrderArticle < ActiveRecord::Base
|
|||
validate :article_and_price_exist
|
||||
validates_uniqueness_of :article_id, scope: :order_id
|
||||
|
||||
scope :ordered, :conditions => "units_to_order >= 1"
|
||||
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") }
|
||||
|
||||
before_create :init_from_balancing
|
||||
after_destroy :update_ordergroup_prices
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
- title 'Artikel mit externer Datenbank synchronisieren'
|
||||
- title t('.title')
|
||||
|
||||
= form_tag update_synchronized_supplier_articles_path(@supplier) do
|
||||
%h2= t '.outlist.title'
|
||||
|
|
@ -19,9 +19,8 @@
|
|||
%h2= t '.update.title'
|
||||
%p
|
||||
%i
|
||||
%b= @updated_articles.size
|
||||
= t '.update.update_msg'
|
||||
= t('.update.body').html_safe
|
||||
= t '.update.update_msg', count: @updated_articles.size
|
||||
= t '.update.body'
|
||||
%table.table
|
||||
%thead
|
||||
%tr
|
||||
|
|
|
|||
|
|
@ -5,21 +5,19 @@
|
|||
|
||||
$('#stock_changes tr').removeClass('success');
|
||||
|
||||
var quantity = w.prompt('<%= j(t('.how_many_units', :unit => @stock_change.stock_article.unit, :name => @stock_change.stock_article.name)) %>');
|
||||
if(null === quantity) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var stock_change = $(
|
||||
'<%= j(render(:partial => 'stock_change', :locals => {:stock_change => @stock_change})) %>'
|
||||
).addClass('success');
|
||||
enablePriceTooltips(stock_change);
|
||||
$('input.stock-change-quantity', stock_change).val(quantity);
|
||||
|
||||
$('#stock_changes').append(stock_change);
|
||||
mark_article_for_delivery(<%= @stock_change.stock_article.id %>);
|
||||
updateSort('#stock_changes');
|
||||
|
||||
var quantity = w.prompt('<%= j(t('.how_many_units', :unit => @stock_change.stock_article.unit, :name => @stock_change.stock_article.name)) %>'); <%# how to properly escape here? %>
|
||||
if(null === quantity) {
|
||||
stock_change.remove();
|
||||
mark_article_for_delivery(<%= @stock_change.stock_article.id %>);
|
||||
return false;
|
||||
}
|
||||
$('input.stock-change-quantity', stock_change).val(quantity);
|
||||
|
||||
})(window);
|
||||
|
|
|
|||
|
|
@ -7,12 +7,17 @@
|
|||
setMinimumBalance(#{FoodsoftConfig[:minimum_balance] or 0});
|
||||
setToleranceBehaviour(#{FoodsoftConfig[:tolerance_is_costly]});
|
||||
setStockit(#{@order.stockit?});
|
||||
// create List for search-feature (using list.js, http://listjs.com)
|
||||
var listjsResetPlugin = ['reset', {highlightClass: 'btn-primary'}];
|
||||
var listjsDelayPlugin = ['delay', {delayedSearchTime: 500}];
|
||||
new List(document.body, { valueNames: ['name'], engine: 'unlist', plugins: [listjsResetPlugin, listjsDelayPlugin] });
|
||||
});
|
||||
|
||||
- title t('.title'), false
|
||||
|
||||
.row-fluid
|
||||
.well.pull-left
|
||||
%button{type: "button", class: "close", data: {dismiss: 'alert'}}= '×'.html_safe
|
||||
%h2= @order.name
|
||||
%dl.dl-horizontal
|
||||
- unless @order.note.blank?
|
||||
|
|
@ -35,8 +40,17 @@
|
|||
%dd= number_to_currency(@ordering_data[:available_funds])
|
||||
|
||||
.well.pull-right
|
||||
%button{type: "button", class: "close", data: {dismiss: 'alert'}}= '×'.html_safe
|
||||
= render 'switch_order', current_order: @order
|
||||
|
||||
.row-fluid
|
||||
.well.clear
|
||||
.form-search
|
||||
.input-append
|
||||
= text_field_tag :article, params[:article], placeholder: t('.search_article'), class: 'search-query delayed-search resettable'
|
||||
%button.add-on.btn.reset-search{:type => :button, :title => t('.reset_article_search')}
|
||||
%i.icon.icon-remove
|
||||
|
||||
= form_for @group_order do |f|
|
||||
= f.hidden_field :lock_version
|
||||
= f.hidden_field :order_id
|
||||
|
|
@ -59,9 +73,9 @@
|
|||
%th(style="width:20px")= t '.available'
|
||||
%th#col_required= t '.amount'
|
||||
%th{style: "width:15px;"}= t '.sum'
|
||||
%tbody
|
||||
%tbody.list
|
||||
- @order.articles_grouped_by_category.each do |category, order_articles|
|
||||
%tr.article-category
|
||||
%tr.list-heading.article-category
|
||||
%td
|
||||
= category
|
||||
%i.icon-tag
|
||||
|
|
|
|||
|
|
@ -4,4 +4,4 @@
|
|||
= form.hidden_field :group_id
|
||||
= form.input :email
|
||||
= form.submit t('.action')
|
||||
= link_to t('.back'), :back
|
||||
= link_to t('ui.or_cancel'), :back
|
||||
|
|
|
|||
|
|
@ -27,10 +27,14 @@
|
|||
= category_name
|
||||
%i.icon-tag
|
||||
- for article in articles
|
||||
/ check if the article is selected
|
||||
- included = @order.article_ids.include?(article.id)
|
||||
- included_class = included ? ' selected' : ''
|
||||
%tr{:class => included_class, :id => article.id.to_s }
|
||||
/ check if the article is selected or has an error
|
||||
- included = @order.article_ids.include?(article.id.to_s)
|
||||
- row_class = ''
|
||||
- if included
|
||||
- row_class = 'selected'
|
||||
- elsif @order.erroneous_article_ids.include?(article.id)
|
||||
- row_class = 'error'
|
||||
%tr{class: row_class, id: article.id}
|
||||
%td= check_box_tag "order[article_ids][]", article.id, included, :id => "checkbox_#{article.id}"
|
||||
%td.click-me{'data-check-this' => "#checkbox_#{article.id}"}= article.name
|
||||
%td=h truncate article.note, :length => 25
|
||||
|
|
@ -52,3 +56,7 @@
|
|||
.form-actions
|
||||
= f.submit class: 'btn'
|
||||
= link_to t('ui.or_cancel'), orders_path
|
||||
- unless @order.erroneous_article_ids.empty?
|
||||
|
||||
= check_box_tag 'order[ignore_warnings]'
|
||||
= t '.ignore_warnings'
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@
|
|||
%thead
|
||||
%tr
|
||||
%th{:style => 'width:70%'}= t '.ordergroup'
|
||||
%th= t '.ordered'
|
||||
%th= t '.received'
|
||||
%th
|
||||
%acronym{:title => t('shared.articles.ordered_desc')}= t 'shared.articles.ordered'
|
||||
%th
|
||||
%acronym{:title => t('shared.articles.received_desc')}= t 'shared.articles.received'
|
||||
%th= t '.price'
|
||||
|
||||
- for order_article in order.order_articles.ordered.all(:include => [:article, :article_price])
|
||||
|
|
@ -13,8 +15,8 @@
|
|||
= order_article.article.name
|
||||
= "(#{order_article.article.unit} | #{order_article.price.unit_quantity} | #{number_to_currency(order_article.price.gross_price)})"
|
||||
%tbody
|
||||
- for goa in order_article.group_order_articles
|
||||
%tr{:class => cycle('even', 'odd', :name => 'groups')}
|
||||
- for goa in order_article.group_order_articles.ordered
|
||||
%tr{:class => [cycle('even', 'odd', :name => 'groups'), if goa.result == 0 then 'unavailable' end]}
|
||||
%td{:style => "width:70%"}=h goa.group_order.ordergroup.name
|
||||
%td= "#{goa.quantity} + #{goa.tolerance}"
|
||||
%td
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@
|
|||
%tr
|
||||
%th{:style => "width:40%"}= t '.name'
|
||||
%th
|
||||
%acronym{:title => t('.units_desc')}= t '.units'
|
||||
%acronym{:title => t('shared.articles.ordered_desc')}= t 'shared.articles.ordered'
|
||||
%th
|
||||
%acronym{:title => t('shared.articles.received_desc')}= t 'shared.articles.received'
|
||||
%th
|
||||
%acronym{:title => t('.fc_price_desc')}= t '.fc_price'
|
||||
%th
|
||||
|
|
@ -11,10 +13,10 @@
|
|||
%th= t '.unit'
|
||||
%th= t '.price'
|
||||
|
||||
- for group_order in order.group_orders.all
|
||||
- for group_order in order.group_orders.ordered
|
||||
%thead
|
||||
%tr
|
||||
%th{:colspan => "6"}
|
||||
%th{:colspan => "7"}
|
||||
%h4= group_order.ordergroup.name
|
||||
%tbody
|
||||
- total = 0
|
||||
|
|
@ -22,17 +24,19 @@
|
|||
- fc_price = goa.order_article.price.fc_price
|
||||
- subTotal = fc_price * goa.result
|
||||
- total += subTotal
|
||||
%tr{:class => cycle('even', 'odd', :name => 'articles')}
|
||||
%tr{:class => [cycle('even', 'odd', :name => 'articles'), if goa.result == 0 then 'unavailable' end]}
|
||||
%td{:style => "width:40%"}=h goa.order_article.article.name
|
||||
%td= goa.result
|
||||
%td= "#{goa.quantity} + #{goa.tolerance}"
|
||||
%td
|
||||
%b= goa.result
|
||||
%td= number_to_currency(fc_price)
|
||||
%td= goa.order_article.price.unit_quantity
|
||||
%td= goa.order_article.article.unit
|
||||
%td= number_to_currency(subTotal)
|
||||
|
||||
%tr{:class => cycle('even', 'odd', :name => 'articles')}
|
||||
%th{:colspan => "5"} Summe
|
||||
%th{:colspan => "6"} Summe
|
||||
%th= number_to_currency(total)
|
||||
%tr
|
||||
%th(colspan="6")
|
||||
%th(colspan="7")
|
||||
- reset_cycle("articles")
|
||||
|
|
|
|||
|
|
@ -3,5 +3,6 @@
|
|||
= form.hidden_field :stock_article_id
|
||||
= "Menge (#{stock_change.stock_article.quantity_available})"
|
||||
= form.text_field :quantity, :size => 5, :autocomplete => 'off'
|
||||
%b= stock_change.stock_article.name
|
||||
= "(#{number_to_currency(stock_change.stock_article.price)} / #{stock_change.stock_article.unit})"
|
||||
%span{:data => {:toggle => :tooltip, :title => render(:partial => 'shared/article_price_info', :locals => {:article => stock_change.stock_article})}}
|
||||
%b= stock_change.stock_article.name
|
||||
= "(#{number_to_currency(stock_change.stock_article.price)} / #{stock_change.stock_article.unit})"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,20 @@
|
|||
- title t('.title')
|
||||
|
||||
- content_for :javascript do
|
||||
:javascript
|
||||
$(function() {
|
||||
enablePriceTooltips();
|
||||
});
|
||||
|
||||
function enablePriceTooltips(context) {
|
||||
$('[data-toggle~="tooltip"]', context).tooltip({
|
||||
animation: false,
|
||||
html: true,
|
||||
placement: 'left',
|
||||
container: 'body'
|
||||
});
|
||||
}
|
||||
|
||||
- content_for :sidebar do
|
||||
%p
|
||||
%i= t('.text_deviations', inv_link: link_to(t('.temp_inventory'), stock_articles_path)).html_safe
|
||||
|
|
|
|||
|
|
@ -1,17 +0,0 @@
|
|||
- title t('.stock_changes', :article_name => @stock_article.name)
|
||||
|
||||
%table.table.table-hover#stock_changes
|
||||
%thead
|
||||
%tr
|
||||
%th= t '.datetime'
|
||||
%th= t '.reason'
|
||||
%th= t '.change_quantity'
|
||||
%th= t '.new_quantity'
|
||||
%tbody
|
||||
- reversed_history = @stock_article.quantity_history.reverse
|
||||
- @stock_changes.each_with_index do |stock_change, index|
|
||||
%tr
|
||||
%td= l stock_change.created_at
|
||||
%td= link_to_stock_change_reason(stock_change)
|
||||
%td= stock_change.quantity
|
||||
%td= reversed_history[index]
|
||||
|
|
@ -45,7 +45,7 @@
|
|||
%tbody
|
||||
- for article in @stock_articles
|
||||
%tr{:class => stock_article_classes(article), :id => "stockArticle-#{article.id}"}
|
||||
%td=h article.name
|
||||
%td= link_to article.name, article
|
||||
%td= article.quantity
|
||||
%td= article.quantity - article.quantity_available
|
||||
%th= article.quantity_available
|
||||
|
|
@ -56,7 +56,6 @@
|
|||
%td= article.article_category.name
|
||||
%td
|
||||
= link_to t('ui.edit'), edit_stock_article_path(article), class: 'btn btn-mini'
|
||||
= link_to t('ui.history'), stock_article_history_path(article), class: 'btn btn-mini'
|
||||
= link_to t('ui.delete'), article, :method => :delete, :confirm => t('.confirm_delete'),
|
||||
class: 'btn btn-mini btn-danger', :remote => true
|
||||
%p
|
||||
|
|
|
|||
47
app/views/stockit/show.html.haml
Normal file
47
app/views/stockit/show.html.haml
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
- title @stock_article.name
|
||||
|
||||
.row-fluid
|
||||
.span6
|
||||
%dl.dl-horizontal
|
||||
%dt= StockArticle.human_attribute_name 'supplier'
|
||||
%dd= link_to @stock_article.supplier.name, @stock_article.supplier
|
||||
%dt= StockArticle.human_attribute_name 'name'
|
||||
%dd= @stock_article.name
|
||||
%dt= StockArticle.human_attribute_name 'unit'
|
||||
%dd= @stock_article.unit
|
||||
%dt= StockArticle.human_attribute_name 'price'
|
||||
%dd= number_to_currency @stock_article.price
|
||||
%dt= StockArticle.human_attribute_name 'tax'
|
||||
%dd= number_to_percentage @stock_article.tax
|
||||
%dt= StockArticle.human_attribute_name 'deposit'
|
||||
%dd= number_to_currency @stock_article.deposit
|
||||
%dt= StockArticle.human_attribute_name 'fc_price'
|
||||
%dd= number_to_currency @stock_article.fc_price
|
||||
%dt= StockArticle.human_attribute_name 'article_category'
|
||||
%dd= @stock_article.article_category.name
|
||||
%dt= StockArticle.human_attribute_name 'note'
|
||||
%dd= @stock_article.note
|
||||
%dt= StockArticle.human_attribute_name 'quantity'
|
||||
%dd= @stock_article.quantity
|
||||
%dt= StockArticle.human_attribute_name 'quantity_available'
|
||||
%dd= @stock_article.quantity_available
|
||||
.form-actions
|
||||
= link_to t('ui.edit'), edit_stock_article_path(@stock_article), class: 'btn'
|
||||
|
||||
.span6
|
||||
%h2= t('.stock_changes')
|
||||
%table.table.table-hover#stock_changes
|
||||
%thead
|
||||
%tr
|
||||
%th= t '.datetime'
|
||||
%th= t '.reason'
|
||||
%th= t '.change_quantity'
|
||||
%th= t '.new_quantity'
|
||||
%tbody
|
||||
- reversed_history = @stock_article.quantity_history.reverse
|
||||
- @stock_changes.each_with_index do |stock_change, index|
|
||||
%tr
|
||||
%td= l stock_change.created_at
|
||||
%td= link_to_stock_change_reason(stock_change)
|
||||
%td= stock_change.quantity
|
||||
%td= reversed_history[index]
|
||||
Loading…
Add table
Add a link
Reference in a new issue