Merge pull request #361 from foodcoops/feature/spreadsheets

Let upload provide same functionality as shared database sync
This commit is contained in:
wvengen 2015-04-17 19:18:44 +02:00
commit b028431bf0
21 changed files with 490 additions and 343 deletions

View file

@ -35,7 +35,7 @@ class ArticlesController < ApplicationController
@article = @supplier.articles.build(:tax => FoodsoftConfig[:tax_default])
render :layout => false
end
def create
@article = Article.new(params[:article])
if @article.valid? && @article.save
@ -44,12 +44,12 @@ class ArticlesController < ApplicationController
render :action => 'new', :layout => false
end
end
def edit
@article = Article.find(params[:id])
render :action => 'new', :layout => false
end
# Updates one Article and highlights the line if succeded
def update
@article = Article.find(params[:id])
@ -66,8 +66,8 @@ class ArticlesController < ApplicationController
@article = Article.find(params[:id])
@article.mark_as_deleted unless @order = @article.in_open_order # If article is in an active Order, the Order will be returned
render :layout => false
end
end
# Renders a form for editing all articles from a supplier
def edit_all
@articles = @supplier.articles.undeleted
@ -102,7 +102,7 @@ class ArticlesController < ApplicationController
redirect_to supplier_articles_path(@supplier), notice: I18n.t('articles.controller.update_all.notice')
end
end
# makes different actions on selected articles
def update_selected
raise I18n.t('articles.controller.error_nosel') if params[:selected_articles].nil?
@ -129,75 +129,83 @@ class ArticlesController < ApplicationController
redirect_to supplier_articles_url(@supplier, :per_page => params[:per_page]),
:alert => I18n.t('errors.general_msg', :msg => error)
end
# lets start with parsing articles from uploaded file, yeah
# Renders the upload form
def upload
end
# parses the articles from a csv and creates a form-table with the parsed data.
# the csv must have the following format:
# status | number | name | note | manufacturer | origin | unit | clear price | unit_quantity | tax | deposit | scale quantity | scale price | category
# the first line will be ignored.
# field-seperator: ";"
# text-seperator: ""
# Update articles from a spreadsheet
def parse_upload
begin
@articles = Array.new
articles, outlisted_articles = FoodsoftFile::parse(params[:articles]["file"])
no_category = ArticleCategory.new
articles.each do |row|
# fallback to Others category
category = (ArticleCategory.find_match(row[:category]) || no_category)
# creates a new article and price
article = @supplier.articles.build(:name => row[:name],
:note => row[:note],
:manufacturer => row[:manufacturer],
:origin => row[:origin],
:unit => row[:unit],
:article_category => category,
:price => row[:price],
:unit_quantity => row[:unit_quantity],
:order_number => row[:number],
:deposit => row[:deposit],
:tax => (row[:tax] || FoodsoftConfig[:tax_default]))
# stop parsing, when an article isn't valid
unless article.valid?
raise I18n.t('articles.controller.error_parse', :msg => article.errors.full_messages.join(", "), :line => (articles.index(row) + 2).to_s)
end
@articles << article
end
flash.now[:notice] = I18n.t('articles.controller.parse_upload.notice', :count => @articles.size)
rescue => error
redirect_to upload_supplier_articles_path(@supplier), :alert => I18n.t('errors.general_msg', :msg => error.message)
uploaded_file = params[:articles]['file'] or raise I18n.t('articles.controller.parse_upload.no_file')
options = {filename: uploaded_file.original_filename}
options[:outlist_absent] = (params[:articles]['outlist_absent'] == '1')
options[:convert_units] = (params[:articles]['convert_units'] == '1')
@updated_article_pairs, @outlisted_articles, @new_articles = @supplier.sync_from_file uploaded_file.tempfile, options
if @updated_article_pairs.empty? && @outlisted_articles.empty? && @new_articles.empty?
redirect_to supplier_articles_path(@supplier), :notice => I18n.t('articles.controller.parse_upload.notice')
end
@ignored_article_count = 0
rescue => error
redirect_to upload_supplier_articles_path(@supplier), :alert => I18n.t('errors.general_msg', :msg => error.message)
end
# sync all articles with the external database
# renders a form with articles, which should be updated
def sync
# check if there is an shared_supplier
unless @supplier.shared_supplier
redirect_to supplier_articles_url(@supplier), :alert => I18n.t('articles.controller.sync.shared_alert', :supplier => @supplier.name)
end
# sync articles against external database
@updated_article_pairs, @outlisted_articles, @new_articles = @supplier.sync_all
if @updated_article_pairs.empty? && @outlisted_articles.empty? && @new_articles.empty?
redirect_to supplier_articles_path(@supplier), :notice => I18n.t('articles.controller.sync.notice')
end
end
# creates articles from form
def create_from_upload
begin
Article.transaction do
invalid_articles = false
@articles = []
params[:articles].each do |_key, article_attributes|
@articles << (article = @supplier.articles.build(article_attributes))
invalid_articles = true unless article.save
end
raise I18n.t('articles.controller.error_invalid') if invalid_articles
# Updates, deletes articles when upload or sync form is submitted
def update_synchronized
@outlisted_articles = Article.find(params[:outlisted_articles].try(:keys)||[])
@updated_articles = Article.find(params[:articles].try(:keys)||[])
@updated_articles.map{|a| a.assign_attributes(params[:articles][a.id.to_s]) }
@new_articles = (params[:new_articles]||[]).map{|a| @supplier.articles.build(a) }
has_error = false
Article.transaction do
# delete articles
begin
@outlisted_articles.each(&:mark_as_deleted)
rescue
# raises an exception when used in current order
has_error = true
end
# Update articles
@updated_articles.map{|a| a.save or has_error=true }
# Add new articles
@new_articles.each do |article|
article.availability = true if @supplier.shared_sync_method == 'all_available'
article.availability = false if @supplier.shared_sync_method == 'all_unavailable'
article.save or has_error=true
end
# Successfully done.
redirect_to supplier_articles_path(@supplier), notice: I18n.t('articles.controller.create_from_upload.notice', :count => @articles.size)
rescue => error
# An error has occurred, transaction has been rolled back.
flash.now[:error] = I18n.t('errors.general_msg', :msg => error.message)
render :parse_upload
raise ActiveRecord::Rollback if has_error
end
if !has_error
redirect_to supplier_articles_path(@supplier), notice: I18n.t('articles.controller.update_sync.notice')
else
@updated_article_pairs = @updated_articles.map do |article|
orig_article = Article.find(article.id)
[article, orig_article.unequal_attributes(article)]
end
flash.now.alert = I18n.t('articles.controller.error_invalid')
render params[:from_action] == 'sync' ? :sync : :parse_upload
end
end
# renders a view to import articles in local database
#
#
def shared
# build array of keywords, required for ransack _all suffix
params[:q][:name_cont_all] = params[:q][:name_cont_all].split(' ') if params[:q]
@ -206,7 +214,7 @@ class ArticlesController < ApplicationController
@articles = @search.result.page(params[:page]).per(10)
render :layout => false
end
# fills a form whith values of the selected shared_article
# when the direct parameter is set and the article is valid, it is imported directly
def import
@ -218,63 +226,16 @@ class ArticlesController < ApplicationController
render :action => 'new', :layout => false
end
end
# sync all articles with the external database
# renders a form with articles, which should be updated
def sync
# check if there is an shared_supplier
unless @supplier.shared_supplier
redirect_to supplier_articles_url(@supplier), :alert => I18n.t('articles.controller.sync.shared_alert', :supplier => @supplier.name)
end
# sync articles against external database
@updated_articles, @outlisted_articles, @new_articles = @supplier.sync_all
# convert to db-compatible-string
@updated_articles.each {|a, b| a.shared_updated_on = a.shared_updated_on.to_formatted_s(:db)}
if @updated_articles.empty? && @outlisted_articles.empty? && @new_articles.empty?
redirect_to supplier_articles_path(@supplier), :notice => I18n.t('articles.controller.sync.notice')
end
@ignored_article_count = @supplier.articles.where(order_number: [nil, '']).count
end
# Updates, deletes articles when sync form is submitted
def update_synchronized
begin
Article.transaction do
# delete articles
if params[:outlisted_articles]
Article.find(params[:outlisted_articles].keys).each(&:mark_as_deleted)
end
private
# Update articles
if params[:articles]
params[:articles].each do |id, attrs|
Article.find(id).update_attributes! attrs
end
end
# Add new articles
if params[:new_articles]
params[:new_articles].each do |attrs|
article = Article.new attrs
article.supplier = @supplier
article.availability = true if @supplier.shared_sync_method == 'all_available'
article.availability = false if @supplier.shared_sync_method == 'all_unavailable'
article.save!
end
end
end
# Successfully done.
redirect_to supplier_articles_path(@supplier), notice: I18n.t('articles.controller.update_sync.notice')
rescue ActiveRecord::RecordInvalid => invalid
# An error has occurred, transaction has been rolled back.
redirect_to supplier_articles_path(@supplier),
alert: I18n.t('articles.controller.error_update', :article => invalid.record.name, :msg => invalid.record.errors.full_messages)
rescue => error
redirect_to supplier_articles_path(@supplier),
alert: I18n.t('errors.general_msg', :msg => error.message)
# @return [Number] Number of articles not taken into account when syncing (having no number)
helper_method \
def ignored_article_count
if action_name == 'sync' || params[:from_action] == 'sync'
@ignored_article_count ||= @supplier.articles.where(order_number: [nil, '']).count
else
0
end
end
end

View file

@ -3,7 +3,7 @@ module ArticlesHelper
# useful for highlighting attributes, when synchronizing articles
def highlight_new(unequal_attributes, attribute)
return unless unequal_attributes
unequal_attributes.detect {|a| a == attribute} ? "background-color: yellow" : ""
unequal_attributes.has_key?(attribute) ? "background-color: yellow" : ""
end
def row_classes(article)

View file

@ -62,11 +62,11 @@ class Article < ActiveRecord::Base
#validates_uniqueness_of :name, :scope => [:supplier_id, :deleted_at, :type], if: Proc.new {|a| a.supplier.shared_sync_method.blank? or a.supplier.shared_sync_method == 'import' }
#validates_uniqueness_of :name, :scope => [:supplier_id, :deleted_at, :type, :unit, :unit_quantity]
validate :uniqueness_of_name
# Callbacks
before_save :update_price_history
before_destroy :check_article_in_use
# The financial gross, net plus tax and deposti
def gross_price
((price + deposit) * (tax / 100 + 1)).round(2)
@ -76,12 +76,12 @@ class Article < ActiveRecord::Base
def fc_price
(gross_price * (FoodsoftConfig[:price_markup] / 100 + 1)).round(2)
end
# Returns true if article has been updated at least 2 days ago
def recently_updated
updated_at > 2.days.ago
end
# If the article is used in an open Order, the Order will be returned.
def in_open_order
@in_open_order ||= begin
@ -90,92 +90,106 @@ class Article < ActiveRecord::Base
order_article ? order_article.order : nil
end
end
# Returns true if the article has been ordered in the given order at least once
def ordered_in_order?(order)
order.order_articles.where(article_id: id).where('quantity > 0').one?
end
# this method checks, if the shared_article has been changed
# unequal attributes will returned in array
# if only the timestamps differ and the attributes are equal,
# if only the timestamps differ and the attributes are equal,
# false will returned and self.shared_updated_on will be updated
def shared_article_changed?(supplier = self.supplier)
# skip early if the timestamp hasn't changed
shared_article = self.shared_article(supplier)
unless shared_article.nil? || self.shared_updated_on == shared_article.updated_on
# try to convert units
# convert supplier's price and unit_quantity into fc-size
new_price, new_unit_quantity = self.convert_units
new_unit = self.unit
unless new_price && new_unit_quantity
# if convertion isn't possible, take shared_article-price/unit_quantity
new_price, new_unit_quantity, new_unit = shared_article.price, shared_article.unit_quantity, shared_article.unit
end
# check if all attributes differ
unequal_attributes = Article.compare_attributes(
{
:name => [self.name, shared_article.name],
:manufacturer => [self.manufacturer, shared_article.manufacturer.to_s],
:origin => [self.origin, shared_article.origin],
:unit => [self.unit, new_unit],
:price => [self.price.to_f.round(2), new_price.to_f.round(2)],
:tax => [self.tax, shared_article.tax],
:deposit => [self.deposit.to_f.round(2), shared_article.deposit.to_f.round(2)],
# take care of different num-objects.
:unit_quantity => [self.unit_quantity.to_s.to_f, new_unit_quantity.to_s.to_f],
:note => [self.note.to_s, shared_article.note.to_s]
}
)
if unequal_attributes.empty?
attrs = unequal_attributes(shared_article)
if attrs.empty?
# when attributes not changed, update timestamp of article
self.update_attribute(:shared_updated_on, shared_article.updated_on)
false
else
unequal_attributes
attrs
end
end
end
# compare attributes from different articles. used for auto-synchronization
# returns array of symbolized unequal attributes
# Return article attributes that were changed (incl. unit conversion)
# @param new_article [Article] New article to update self
# @option options [Boolean] :convert_units Omit or set to +true+ to keep current unit and recompute unit quantity and price.
# @return [Hash<Symbol, Object>] Attributes with new values
def unequal_attributes(new_article, options={})
# try to convert different units when desired
if options[:convert_units] == false
new_price, new_unit_quantity = nil, nil
else
new_price, new_unit_quantity = convert_units(new_article)
end
if new_price && new_unit_quantity
new_unit = self.unit
else
new_price = new_article.price
new_unit_quantity = new_article.unit_quantity
new_unit = new_article.unit
end
return Article.compare_attributes(
{
:name => [self.name, new_article.name],
:manufacturer => [self.manufacturer, new_article.manufacturer.to_s],
:origin => [self.origin, new_article.origin],
:unit => [self.unit, new_unit],
:price => [self.price.to_f.round(2), new_price.to_f.round(2)],
:tax => [self.tax, new_article.tax],
:deposit => [self.deposit.to_f.round(2), new_article.deposit.to_f.round(2)],
# take care of different num-objects.
:unit_quantity => [self.unit_quantity.to_s.to_f, new_unit_quantity.to_s.to_f],
:note => [self.note.to_s, new_article.note.to_s]
}
)
end
# Compare attributes from two different articles.
#
# This is used for auto-synchronization
# @param attributes [Hash<Symbol, Array>] Attributes with old and new values
# @return [Hash<Symbol, Object>] Changed attributes with new values
def self.compare_attributes(attributes)
unequal_attributes = attributes.select { |name, values| values[0] != values[1] && !(values[0].blank? && values[1].blank?) }
unequal_attributes.collect { |pair| pair[0] }
Hash[unequal_attributes.to_a.map {|a| [a[0], a[1].last]}]
end
# to get the correspondent shared article
def shared_article(supplier = self.supplier)
self.order_number.blank? and return nil
@shared_article ||= supplier.shared_supplier.shared_articles.find_by_number(self.order_number) rescue nil
end
# convert units in foodcoop-size
# uses unit factors in app_config.yml to calc the price/unit_quantity
# returns new price and unit_quantity in array, when calc is possible => [price, unit_quanity]
# returns false if units aren't foodsoft-compatible
# returns nil if units are eqal
def convert_units
if unit != shared_article.unit
def convert_units(new_article = shared_article)
if unit != new_article.unit
# legacy, used by foodcoops in Germany
if shared_article.unit == "KI" && unit == "ST" # 'KI' means a box, with a different amount of items in it
if new_article.unit == "KI" && unit == "ST" # 'KI' means a box, with a different amount of items in it
# try to match the size out of its name, e.g. "banana 10-12 St" => 10
new_unit_quantity = /[0-9\-\s]+(St)/.match(shared_article.name).to_s.to_i
new_unit_quantity = /[0-9\-\s]+(St)/.match(new_article.name).to_s.to_i
if new_unit_quantity && new_unit_quantity > 0
new_price = (shared_article.price/new_unit_quantity.to_f).round(2)
new_price = (new_article.price/new_unit_quantity.to_f).round(2)
[new_price, new_unit_quantity]
else
false
end
else # use ruby-units to convert
fc_unit = (::Unit.new(unit) rescue nil)
supplier_unit = (::Unit.new(shared_article.unit) rescue nil)
supplier_unit = (::Unit.new(new_article.unit) rescue nil)
if fc_unit && supplier_unit && fc_unit =~ supplier_unit
conversion_factor = (supplier_unit / fc_unit).to_base.to_r
new_price = shared_article.price / conversion_factor
new_unit_quantity = shared_article.unit_quantity * conversion_factor
new_price = new_article.price / conversion_factor
new_unit_quantity = new_article.unit_quantity * conversion_factor
[new_price, new_unit_quantity]
else
false
@ -196,7 +210,7 @@ class Article < ActiveRecord::Base
end
protected
# Checks if the article is in use before it will deleted
def check_article_in_use
raise I18n.t('articles.model.error_in_use', :article => self.name.to_s) if self.in_open_order

View file

@ -26,39 +26,16 @@ class Supplier < ActiveRecord::Base
# also returns an array with outlisted_articles, which should be deleted
# also returns an array with new articles, which should be added (depending on shared_sync_method)
def sync_all
updated_articles = Array.new
outlisted_articles = Array.new
new_articles = Array.new
updated_article_pairs, outlisted_articles, new_articles = [], [], []
for article in articles.undeleted
# try to find the associated shared_article
shared_article = article.shared_article(self)
if shared_article # article will be updated
unequal_attributes = article.shared_article_changed?(self)
unless unequal_attributes.blank? # skip if shared_article has not been changed
# try to convert different units
new_price, new_unit_quantity = article.convert_units
if new_price && new_unit_quantity
article.price = new_price
article.unit_quantity = new_unit_quantity
else
article.price = shared_article.price
article.unit_quantity = shared_article.unit_quantity
article.unit = shared_article.unit
end
# update other attributes
article.attributes = {
:name => shared_article.name,
:manufacturer => shared_article.manufacturer,
:origin => shared_article.origin,
:shared_updated_on => shared_article.updated_on,
:tax => shared_article.tax,
:deposit => shared_article.deposit,
:note => shared_article.note
}
updated_articles << [article, unequal_attributes]
article.attributes = unequal_attributes
updated_article_pairs << [article, unequal_attributes]
end
# Articles with no order number can be used to put non-shared articles
# in a shared supplier, with sync keeping them.
@ -75,7 +52,49 @@ class Supplier < ActiveRecord::Base
end
end
end
return [updated_articles, outlisted_articles, new_articles]
return [updated_article_pairs, outlisted_articles, new_articles]
end
# Synchronise articles with spreadsheet.
#
# @param file [File] Spreadsheet file to parse
# @param options [Hash] Options passed to {FoodsoftFile#parse} except when listed here.
# @option options [Boolean] :outlist_absent Set to +true+ to remove articles not in spreadsheet.
# @option options [Boolean] :convert_units Omit or set to +true+ to keep current units, recomputing unit quantity and price.
def sync_from_file(file, options={})
all_order_numbers = []
updated_article_pairs, outlisted_articles, new_articles = [], [], []
FoodsoftFile::parse file, options do |status, new_attrs, line|
article = articles.undeleted.where(order_number: new_attrs[:order_number]).first
new_attrs[:article_category] = ArticleCategory.find_match(new_attrs[:article_category])
new_attrs[:tax] ||= FoodsoftConfig[:tax_default]
new_article = articles.build(new_attrs)
if status.nil?
if article.nil?
new_articles << new_article
else
unequal_attributes = article.unequal_attributes(new_article, options.slice(:convert_units))
unless unequal_attributes.empty?
article.attributes = unequal_attributes
updated_article_pairs << [article, unequal_attributes]
end
end
elsif status == :outlisted && article.present?
outlisted_articles << article
# stop when there is a parsing error
elsif status.is_a? String
# @todo move I18n key to model
raise I18n.t('articles.model.error_parse', :msg => status, :line => line.to_s)
end
all_order_numbers << article.order_number if article
end
if options[:outlist_absent]
outlisted_articles += articles.undeleted.where.not(id: all_order_numbers+[nil])
end
return [updated_article_pairs, outlisted_articles, new_articles]
end
# default value
@ -114,4 +133,3 @@ class Supplier < ActiveRecord::Base
end
end
end

View file

@ -0,0 +1,32 @@
- if @outlisted_articles.any?
%h2= t '.outlist.title'
%p
= t('.outlist.body').html_safe
%ul
- for article in @outlisted_articles
%li
= hidden_field_tag "outlisted_articles[#{article.id}]", '1'
= article.name
- if article.in_open_order
.alert= t '.outlist.alert_used', article: article.name
%hr/
- if @updated_article_pairs.any?
%h2= t '.update.title'
%p
%i
= t '.update.update_msg', count: @updated_article_pairs.size
= t '.update.body'
= render 'sync_table', articles: @updated_article_pairs, field: 'articles', hidden_fields: %w(shared_updated_on)
%hr/
- if @new_articles.any?
%h2= t '.upnew.title'
%p
%i= t '.upnew.body_count', count: @new_articles.length
= render 'sync_table', articles: @new_articles, field: 'new_articles', hidden_fields: %w(shared_updated_on order_number)
%hr/
- if ignored_article_count > 0
%p
%i= t '.outlist.body_ignored', count: ignored_article_count

View file

@ -51,3 +51,6 @@
= form.text_field 'deposit', class: 'input-mini', style: 'width: 45px'
%td= form.select :article_category_id, ArticleCategory.all.map {|a| [ a.name, a.id ] },
{include_blank: true}, class: 'input-small'
- unless changed_article.errors.empty?
%tr.alert
%td(colspan=11)= changed_article.errors.full_messages.join(', ')

View file

@ -1,11 +1,8 @@
- title t('.title', supplier: @supplier.name)
%p= t('.body').html_safe
= form_tag(create_from_upload_supplier_articles_path(@supplier)) do
= render layout: 'edit_all_table' do |form|
= form.hidden_field :manufacturer
= form.hidden_field :origin
= form_tag update_synchronized_supplier_articles_path(@supplier) do
= hidden_field_tag :from_action, 'parse_upload'
= render 'sync'
.form-actions
= submit_tag t('.submit', supplier: @supplier.name), class: 'btn btn-primary'
= submit_tag t('.submit'), class: 'btn btn-primary'
= link_to t('ui.or_cancel'), upload_supplier_articles_path(@supplier)

View file

@ -1,39 +1,8 @@
- title t('.title')
= form_tag update_synchronized_supplier_articles_path(@supplier) do
- if @outlisted_articles.any?
%h2= t '.outlist.title'
%p
= t('.outlist.body').html_safe
%ul
- for article in @outlisted_articles
%li
= hidden_field_tag "outlisted_articles[#{article.id}]", '1'
= article.name
- if article.in_open_order
.alert= t '.outlist.alert_used', article: article.name
%hr/
- if @updated_articles.any?
%h2= t '.update.title'
%p
%i
= t '.update.update_msg', count: @updated_articles.size
= t '.update.body'
= render 'sync_table', articles: @updated_articles, field: 'articles', hidden_fields: %w(shared_updated_on)
%hr/
- if @new_articles.any?
%h2= t '.upnew.title'
%p
%i= t '.upnew.body_count', count: @new_articles.length
= render 'sync_table', articles: @new_articles, field: 'new_articles', hidden_fields: %w(shared_updated_on order_number)
%hr/
- if @ignored_article_count > 0
%p
%i= t '.outlist.body_ignored', count: @ignored_article_count
= hidden_field 'supplier', 'id'
= submit_tag t('.submit'), class: 'btn btn-primary'
= link_to t('ui.or_cancel'), supplier_articles_path(@supplier)
= hidden_field_tag :from_action, 'sync'
= render 'sync'
.form-actions
= submit_tag t('.submit'), class: 'btn btn-primary'
= link_to t('ui.or_cancel'), supplier_articles_path(@supplier)

View file

@ -1,24 +1,88 @@
- title t('.title', supplier: @supplier.name)
= t('.body').html_safe
%pre
= [t('.fields.status'),
Article.human_attribute_name(:order_number),
Article.human_attribute_name(:name),
Article.human_attribute_name(:note),
Article.human_attribute_name(:manufacturer),
Article.human_attribute_name(:origin),
Article.human_attribute_name(:unit),
Article.human_attribute_name(:price),
Article.human_attribute_name(:tax),
Article.human_attribute_name(:deposit),
Article.human_attribute_name(:unit_quantity),
t('.fields.reserved'),
t('.fields.reserved'),
Article.human_attribute_name(:article_category)].join(" | ")
%p= t '.text_1', supplier: @supplier.name
%table.table.table-bordered
%thead
%tr
%th= t '.field.status'
%th= Article.human_attribute_name(:order_number)
%th= Article.human_attribute_name(:name)
%th= Article.human_attribute_name(:note)
%th= Article.human_attribute_name(:manufacturer)
%th= Article.human_attribute_name(:origin)
%th= Article.human_attribute_name(:unit)
%th= Article.human_attribute_name(:price)
%th= Article.human_attribute_name(:tax)
%th= Article.human_attribute_name(:deposit)
%th= Article.human_attribute_name(:unit_quantity)
%th.muted= t '.fields.reserved'
%th.muted= t '.fields.reserved'
%th= Article.human_attribute_name(:article_category)
%tbody
%tr
%td
%td 1234A
%td= t '.sample.walnuts'
%td
%td= t '.sample.supplier_1'
%td CA
%td 500 gr
%td 8.90
%td= FoodsoftConfig[:tax_default] || 6
%td 0
%td 6
%td
%td
%td= t '.sample.nuts'
%tr
%td x
%td 4321Z
%td= t '.sample.tomato_juice'
%td= t '.sample.organic'
%td= t '.sample.supplier_2'
%td IN
%td 1.5 l
%td 4.35
%td= FoodsoftConfig[:tax_default] || 6
%td 0
%td 1
%td
%td
%td= t '.sample.juices'
%tr
%td
%td 4322Q
%td= t '.sample.tomato_juice'
%td= t '.sample.organic'
%td= t '.sample.supplier_3'
%td TR
%td 1.2 l
%td 4.02
%td= FoodsoftConfig[:tax_default] || 6
%td 0
%td 2
%td
%td
%td= t '.sample.juices'
%p= t '.text_2'
= form_for :articles, :url => parse_upload_supplier_articles_path(@supplier),
:html => { :multipart => true } do |f|
%label(for="articles_file")= t '.file_label'
= f.file_field "file"
:html => { multipart: true, class: "form-horizontal" } do |f|
.control-group
%label(for="articles_file")= t '.file_label'
= f.file_field "file"
.control-group
%label(for="articles_outlist_absent")
= f.check_box "outlist_absent"
= t '.options.outlist_absent'
%label(for="articles_convert_units")
= f.check_box "convert_units"
= t '.options.convert_units'
.form-actions
= submit_tag t('.submit'), class: 'btn'
= submit_tag t('.submit'), class: 'btn btn-primary'
= link_to t('ui.or_cancel'), supplier_articles_path(@supplier)