Merge pull request #361 from foodcoops/feature/spreadsheets
Let upload provide same functionality as shared database sync
This commit is contained in:
commit
b028431bf0
21 changed files with 490 additions and 343 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
32
app/views/articles/_sync.html.haml
Normal file
32
app/views/articles/_sync.html.haml
Normal 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
|
||||
|
|
@ -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(', ')
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue