diff --git a/Gemfile b/Gemfile
index 97422021a..f724d5bfd 100644
--- a/Gemfile
+++ b/Gemfile
@@ -64,6 +64,8 @@ gem "terser", "~> 1.1"
# we use the git version of acts_as_versioned, and need to include it in this Gemfile
gem 'acts_as_versioned', git: 'https://github.com/technoweenie/acts_as_versioned.git'
+
+gem 'foodsoft_article_import', path: 'plugins/article_import'
gem 'foodsoft_discourse', path: 'plugins/discourse'
gem 'foodsoft_documents', path: 'plugins/documents'
gem 'foodsoft_links', path: 'plugins/links'
diff --git a/Gemfile.lock b/Gemfile.lock
index c66901cfd..23b0f6722 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -16,6 +16,14 @@ GIT
acts_as_versioned (0.6.0)
activerecord (>= 3.0.9)
+PATH
+ remote: plugins/article_import
+ specs:
+ foodsoft_article_import (0.0.1)
+ deface (~> 1.0)
+ rails
+ roo (~> 2.9.0)
+
PATH
remote: plugins/discourse
specs:
@@ -624,6 +632,7 @@ DEPENDENCIES
exception_notification
factory_bot_rails
faker
+ foodsoft_article_import!
foodsoft_discourse!
foodsoft_documents!
foodsoft_links!
diff --git a/app/models/concerns/localize_input.rb b/app/models/concerns/localize_input.rb
index 296c4c174..7cd26acdc 100644
--- a/app/models/concerns/localize_input.rb
+++ b/app/models/concerns/localize_input.rb
@@ -5,10 +5,10 @@ def self.parse(input)
return input unless input.is_a? String
Rails.logger.debug { "Input: #{input.inspect}" }
- separator = I18n.t('separator', scope: 'number.format')
- delimiter = I18n.t('delimiter', scope: 'number.format')
- input.gsub!(delimiter, '') if input.match(/\d+#{Regexp.escape(delimiter)}+\d+#{Regexp.escape(separator)}+\d+/) # Remove delimiter
- input.gsub!(separator, '.') # Replace separator with db compatible character
+ separator = I18n.t("separator", scope: "number.format")
+ delimiter = I18n.t("delimiter", scope: "number.format")
+ input.gsub!(delimiter, "") if input.match(/\d+#{Regexp.escape(delimiter)}+\d+#{Regexp.escape(separator)}+\d+/) # Remove delimiter
+ input.gsub!(separator, ".") or input.gsub!(",", ".") # Replace separator with db compatible character
input
rescue StandardError
Rails.logger.warn "Can't localize input: #{input}"
diff --git a/app/views/articles/_sync_table.html.haml b/app/views/articles/_sync_table.html.haml
index ac17adfaf..68db9477c 100644
--- a/app/views/articles/_sync_table.html.haml
+++ b/app/views/articles/_sync_table.html.haml
@@ -32,8 +32,8 @@
= form.text_field 'name', :size => 0
- hidden_fields.each do |field|
= form.hidden_field field
- %td{:style => highlight_new(attrs, :note)}= form.text_field 'note', class: 'input-small'
- %td{:style => highlight_new(attrs, :manufacturer)}= form.text_field 'manufacturer', class: 'input-small'
+ %td{:style => highlight_new(attrs, :note)}= form.text_field 'note', class: 'input-medium'
+ %td{:style => highlight_new(attrs, :manufacturer)}= form.text_field 'manufacturer', class: 'input-medium'
%td{:style => highlight_new(attrs, :origin)}= form.text_field 'origin', class: 'input-mini'
%td{:style => highlight_new(attrs, :unit)}= form.text_field 'unit', class: 'input-mini'
%td{:style => highlight_new(attrs, :unit_quantity)}= form.text_field 'unit_quantity', class: 'input-mini'
@@ -49,8 +49,8 @@
.input-prepend
%span.add-on= t 'number.currency.format.unit'
= 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'
+ %td{:style => highlight_new(attrs, :article_category)}
+ = form.select :article_category_id, ArticleCategory.all.map {|a| [ a.name, a.id ] }, {include_blank: true}
- unless changed_article.errors.empty?
%tr.alert
%td(colspan=11)= changed_article.errors.full_messages.join(', ')
diff --git a/plugins/article_import/app/overrides/articles/upload/replace_label_with_file_format_option.html.haml.deface b/plugins/article_import/app/overrides/articles/upload/replace_label_with_file_format_option.html.haml.deface
new file mode 100644
index 000000000..a662d6196
--- /dev/null
+++ b/plugins/article_import/app/overrides/articles/upload/replace_label_with_file_format_option.html.haml.deface
@@ -0,0 +1,10 @@
+/ insert_after 'erb:contains("file_field")'
+- if FoodsoftArticleImport.enabled?
+ %label(for="articles_file")
+ %strong="select the file type you are about to upload"
+ =f.collection_select :type, FoodsoftArticleImport::FORMATS , :to_s, :to_s
+/ insert_before 'erb:contains("articles_outlist_absent")'
+-if FoodsoftArticleImport.enabled?
+ %label(for="articles_update_category")
+ = f.check_box "update_category"
+ = t 'Kategorien aus der Datei übernehmen und erstellen.'
\ No newline at end of file
diff --git a/plugins/article_import/app/overrides/controllers/articles_controller_override.rb b/plugins/article_import/app/overrides/controllers/articles_controller_override.rb
new file mode 100644
index 000000000..7777064e2
--- /dev/null
+++ b/plugins/article_import/app/overrides/controllers/articles_controller_override.rb
@@ -0,0 +1,20 @@
+if FoodsoftArticleImport.enabled?
+ ArticlesController.class_eval do
+ def parse_upload
+ uploaded_file = params[:articles]['file'] or raise I18n.t('articles.controller.parse_upload.no_file')
+ type = params[:articles]['type']
+ options = { filename: uploaded_file.original_filename }
+ options[:outlist_absent] = (params[:articles]['outlist_absent'] == '1')
+ options[:convert_units] = (params[:articles]['convert_units'] == '1')
+ options[:update_category] = (params[:articles]['update_category'] == '1')
+
+ @updated_article_pairs, @outlisted_articles, @new_articles = @supplier.sync_from_file uploaded_file.tempfile, type, 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
+ end
+end
\ No newline at end of file
diff --git a/plugins/article_import/app/overrides/models/article_override.rb b/plugins/article_import/app/overrides/models/article_override.rb
new file mode 100644
index 000000000..5ca177ade
--- /dev/null
+++ b/plugins/article_import/app/overrides/models/article_override.rb
@@ -0,0 +1,39 @@
+if FoodsoftArticleImport.enabled?
+ Article.class_eval do
+ def unequal_attributes(new_article, options = {})
+ # try to convert different units when desired
+ if options[:convert_units] == false
+ new_price = nil
+ new_unit_quantity = 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
+
+ attribute_hash = {
+ :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]
+ }
+ if options[:update_category] == true
+ new_article_category = new_article.article_category
+ attribute_hash[:article_category] = [self.article_category, new_article_category] unless new_article_category.blank?
+ end
+
+ Article.compare_attributes(attribute_hash)
+ end
+ end
+end
\ No newline at end of file
diff --git a/plugins/article_import/app/overrides/models/supplier_override.rb b/plugins/article_import/app/overrides/models/supplier_override.rb
new file mode 100644
index 000000000..62885aac4
--- /dev/null
+++ b/plugins/article_import/app/overrides/models/supplier_override.rb
@@ -0,0 +1,53 @@
+if FoodsoftArticleImport.enabled?
+ Supplier.class_eval do
+ # Synchronise articles with spreadsheet.
+ #
+ # @param file [File] Spreadsheet file to parse
+ # @param options [Hash] Options passed to {FoodsoftArticleImport#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, type, options = {})
+ all_order_numbers = []
+ updated_article_pairs, outlisted_articles, new_articles = [], [], []
+ custom_codes_path = File.join(Rails.root, "config", "custom_codes.yml")
+ opts = options.except(:convert_units, :outlist_absent)
+ custom_codes_file_path = custom_codes_path if File.exist?(custom_codes_path)
+ FoodsoftArticleImport.parse(file, custom_file_path: custom_codes_file_path, type: type, **opts) do |new_attrs, status, line|
+ article = articles.undeleted.where(order_number: new_attrs[:order_number]).first
+
+ if new_attrs[:article_category].present? && options[:update_category]
+ new_attrs[:article_category] = ArticleCategory.find_match(new_attrs[:article_category]) || ArticleCategory.create_or_find_by!(name: new_attrs[:article_category])
+ else
+ new_attrs[:article_category] = nil
+ end
+
+ 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, :update_category))
+ 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(order_number: all_order_numbers + [nil])
+ end
+ [updated_article_pairs, outlisted_articles, new_articles]
+ end
+ end
+end
diff --git a/plugins/article_import/foodsoft_article_import.gemspec b/plugins/article_import/foodsoft_article_import.gemspec
new file mode 100644
index 000000000..b030005f2
--- /dev/null
+++ b/plugins/article_import/foodsoft_article_import.gemspec
@@ -0,0 +1,20 @@
+$:.push File.expand_path("../lib", __FILE__)
+
+# Maintain your gem's version:
+require "foodsoft_article_import/version"
+
+# Describe your gem and declare its dependencies:
+Gem::Specification.new do |s|
+ s.name = "foodsoft_article_import"
+ s.version = FoodsoftArticleImport::VERSION
+ s.authors = ["viehlieb"]
+ s.email = ["foodsoft@local-it.org"]
+ s.summary = "Manages manual article import from file. File Formats supported are: foodsoft file(csv), bnn files (.bnn) and odin files (xml)"
+
+ s.files = Dir["{app,config,db,lib}/**/*"] + ["Rakefile", "README.md"]
+
+ s.add_dependency "rails"
+ s.add_dependency "deface", "~> 1.0"
+ s.add_dependency 'roo', '~> 2.9.0'
+ s.add_development_dependency 'simplecov'
+end
diff --git a/plugins/article_import/lib/foodsoft_article_import.rb b/plugins/article_import/lib/foodsoft_article_import.rb
new file mode 100644
index 000000000..5bd48dd87
--- /dev/null
+++ b/plugins/article_import/lib/foodsoft_article_import.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+require "deface"
+require 'foodsoft_article_import/engine'
+require 'digest/sha1'
+require 'tempfile'
+require 'csv'
+require 'yaml'
+require 'active_support/core_ext/hash/keys'
+require_relative 'foodsoft_article_import/bnn'
+require_relative 'foodsoft_article_import/odin'
+require_relative 'foodsoft_article_import/foodsoft'
+module FoodsoftArticleImport
+ class ConversionFailedException < StandardError; end
+
+ FORMATS = %w(bnn foodsoft odin).freeze
+ def self.enabled?
+ FoodsoftConfig[:use_article_import]
+ end
+
+ def self.file_formats
+ @@file_formats ||= {
+ 'bnn' => FoodsoftArticleImport::Bnn,
+ 'foodsoft' => FoodsoftArticleImport::Foodsoft,
+ 'odin' => FoodsoftArticleImport::Odin,
+ }.freeze
+ end
+
+ # Parse file by type (one of {.file_formats})
+ #
+ # @param file [File, Tempfile]
+ # @option opts [String] type file format (required) (see {.file_formats})
+ # @return [File, Roo::Spreadsheet] file with encoding set if needed
+ def self.parse(file, custom_file_path: nil, type: nil, **opts, &blk)
+ @@filename = opts[:filename] if opts[:filename]
+ custom_file_path ||= nil
+ type ||= 'bnn'
+ parser = file_formats[type]
+ if block_given?
+ parser.parse(file, custom_file_path: custom_file_path, &blk)
+ else
+ data = []
+ parser.parse(file, custom_file_path: custom_file_path) { |a| data << a }
+ data
+ end
+ end
+
+ # Helper method to generate an article number for suppliers that do not have one
+ def self.generate_number(article)
+ # something unique, but not too unique
+ s = "#{article[:name]}-#{article[:unit_quantity]}x#{article[:unit]}"
+ s = s.downcase.gsub(/[^a-z0-9.]/, '')
+ # prefix abbreviated sha1-hash with colon to indicate that it's a generated number
+ article[:order_number] = ":#{Digest::SHA1.hexdigest(s)[-7..]}"
+ article
+ end
+
+ # Helper method for opening a spreadsheet file
+ #
+ # @param file [File] file to open
+ # @param filename [String, NilClass] optional filename for guessing the file format
+ # @param encoding [String, NilClass] optional CSV encoding
+ # @param col_sep [String, NilClass] optional column separator
+ # @return [Roo::Spreadsheet]
+ def self.open_spreadsheet(file, encoding: nil, col_sep: nil, liberal_parsing: nil)
+ opts = { csv_options: {} }
+ opts[:csv_options][:encoding] = encoding if encoding
+ opts[:csv_options][:col_sep] = col_sep if col_sep
+ opts[:csv_options][:liberal_parsing] = true if liberal_parsing
+ opts[:extension] = File.extname(File.basename(file)) if file
+ begin
+ Roo::Spreadsheet.open(file, **opts)
+ rescue StandardError => e
+ raise "Failed to parse foodsoft file. make sure file format is correct: #{e.message}"
+ end
+ end
+end
diff --git a/plugins/article_import/lib/foodsoft_article_import/bnn.rb b/plugins/article_import/lib/foodsoft_article_import/bnn.rb
new file mode 100644
index 000000000..008b60293
--- /dev/null
+++ b/plugins/article_import/lib/foodsoft_article_import/bnn.rb
@@ -0,0 +1,90 @@
+# frozen_string_literal: true
+
+# Module for translation and parsing of BNN-files (www.n-bnn.de)
+#
+module FoodsoftArticleImport
+ module Bnn
+ @@codes = {}
+ @@midgard = {}
+ # Loads the codes_file config/bnn_codes.yml into the class variable @@codes
+ def self.load_codes(custom_file_path = nil)
+ @gem_lib = File.expand_path '..', __dir__
+ dir = File.join @gem_lib, 'foodsoft_article_import'
+ begin
+ @@codes = YAML.safe_load(File.open(File.join(dir, 'bnn_codes.yml'))).symbolize_keys
+ if custom_file_path
+ custom_codes = YAML.safe_load(File.open(custom_file_path)).symbolize_keys
+ custom_codes.each_key do |key|
+ custom_codes[key] = custom_codes[key].merge @@codes[key] if @@codes.keys.include?(key)
+ @@codes = @@codes.merge custom_codes
+ end
+ end
+ @@midgard = YAML.safe_load(File.open(File.join(dir, 'midgard_codes.yml'))).symbolize_keys
+ rescue StandardError => e
+ raise "Failed to load bnn_codes: #{dir}/{bnn,midgard}_codes.yml: #{e.message}"
+ end
+ end
+
+ $missing_bnn_codes = []
+
+ # translates codes from BNN to foodsoft-code
+ def self.translate(key, value)
+ if @@codes[key][value]
+ @@codes[key][value]
+ elsif @@midgard[key]
+ @@midgard[key][value]
+ elsif !value.nil?
+ $missing_bnn_codes << value
+ nil
+ end
+ end
+
+ NAME = 'BNN (CSV)'
+ OUTLIST = false
+ OPTIONS = {
+ encoding: 'IBM850',
+ col_sep: ';'
+ }.freeze
+
+ # parses a bnn-file
+ def self.parse(file, custom_file_path: nil, **opts)
+ custom_file_path ||= nil
+ encoding = opts[:encoding] || OPTIONS[:encoding]
+ col_sep = opts[:col_sep] || OPTIONS[:col_sep]
+ load_codes(custom_file_path)
+ CSV.foreach(file, { col_sep: col_sep, encoding: encoding, headers: true }).with_index(1) do |row, i|
+ # check if the line is empty
+ unless row[0] == '' || row[0].nil?
+ article = {
+ name: row[6],
+ order_number: row[0],
+ note: row[7],
+ manufacturer: translate(:manufacturer, row[10]),
+ origin: row[12],
+ article_category: translate(:category, row[16]),
+ unit: row[23],
+ price: row[37],
+ tax: translate(:tax, row[33]),
+ unit_quantity: row[22]
+ }
+ # TODO: Complete deposit list....
+ article.merge!(deposit: translate(:deposit, row[26])) if translate(:deposit, row[26])
+
+ if !row[62].nil?
+ # consider special prices
+ article[:note] = "Sonderpreis: #{article[:price]} von #{row[62]} bis #{row[63]}"
+ yield article, :special, i
+
+ # Check now for article status, we only consider outlisted articles right now
+ # N=neu, A=Änderung, X=ausgelistet, R=Restbestand,
+ # V=vorübergehend ausgelistet, W=wiedergelistet
+ elsif row[1] == 'X' || row[1] == 'V'
+ yield article, :outlisted, i
+ else
+ yield article, nil, i
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/plugins/article_import/lib/foodsoft_article_import/bnn_codes.yml b/plugins/article_import/lib/foodsoft_article_import/bnn_codes.yml
new file mode 100644
index 000000000..dfb226e72
--- /dev/null
+++ b/plugins/article_import/lib/foodsoft_article_import/bnn_codes.yml
@@ -0,0 +1,1119 @@
+# BNN Codes
+tax:
+ "1": 7.0
+ "2": 19.0
+ "3": 10.7
+
+deposit:
+ "930190": 0.08
+ "930200": 0.08
+ "930205": 0.08
+ "930210": 0.08
+ "930230": 0.08
+ "930260": 0.08
+ "930270": 0.08
+ "930280": 0.08
+ "998010": 0.08
+ "998016": 0.08
+ "998350": 0.08
+ "998360": 0.08
+ "998427": 0.08
+ "998440": 0.08
+ "998460": 0.08
+ "998470": 0.08
+ "998500": 0.08
+ "998510": 0.08
+ "998700": 0.08
+ "998710": 0.08
+ "998725": 0.08
+ "998730": 0.08
+ "998750": 0.08
+ "998760": 0.08
+ "998790": 0.08
+ "998800": 0.08
+ "998810": 0.08
+ "998840": 0.08
+ "998860": 0.08
+ "998880": 0.08
+ "998887": 0.08
+ "900010": 0.15
+ "900020": 0.15
+ "900030": 0.15
+ "900040": 0.15
+ "900050": 0.15
+ "900070": 0.15
+ "900075": 0.15
+ "900085": 0.15
+ "900089": 0.15
+ "900850": 0.15
+ "900860": 0.15
+ "900870": 0.15
+ "900890": 0.15
+ "930010": 0.15
+ "930020": 0.15
+ "930030": 0.15
+ "930035": 0.15
+ "930050": 0.15
+ "930090": 0.15
+ "930110": 0.15
+ "930120": 0.15
+ "930130": 0.15
+ "930320": 0.15
+ "930325": 0.15
+ "955230": 0.15
+ "998000": 0.15
+ "999983": 0.15
+ "999985": 0.15
+ "998040": 0.15
+ "998060": 0.15
+ "998070": 0.15
+ "998080": 0.15
+ "998100": 0.15
+ "998110": 0.15
+ "998300": 0.15
+ "998310": 0.15
+ "998320": 0.15
+ "998330": 0.15
+ "998340": 0.15
+ "998352": 0.15
+ "998370": 0.15
+ "998380": 0.15
+ "998390": 0.15
+ "998405": 0.15
+ "998417": 0.15
+ "998450": 0.15
+ "998480": 0.15
+ "998520": 0.15
+ "999200": 0.15
+ "900892": 0.25
+ "930290": 0.25
+ "999980": 0.25
+ "998020": 0.25
+ "998030": 0.25
+ "998090": 0.25
+ "998366": 0.25
+ "998420": 0.25
+ "998437": 0.25
+ "998740": 0.25
+ "998770": 0.25
+ "930450": 0.50
+ "930440": 1.00
+ "930460": 1.00
+ "930256": 1.50
+ "930257": 1.50
+ "930250": 30.00
+ "930252": 30.00
+ "998720": 30.00
+ "998830": 30.00
+ "998870": 30.00
+
+manufacturer:
+ bro: Brodowin
+ AAB: Azienda Agricola Bettili
+ ABD: Agro Bio Drom, Frankreich
+ ABM: Maintal GmbH
+ ABT: Albtal Naturkost GmbH
+ ACF: Arcada France S.A.
+ ACL: Achleitner Biohof GmbH
+ ADC: Coste Vincent & Francoise
+ ADM: Antersdorfer Mühle GmbH
+ ADR: Faan Zuidhorn
+ AFH: Doris Wallbaum & Co
+ agm: Angermeier Weinimport
+ AGR: Agrano GmbH & Co KG
+ AGX: Agrexco Ltd.
+ akr: Biohof Barnsen
+ ALB: Alber Pilzkonservenfabrik
+ alf: Bioland-Hof Altfeld
+ ALL: Allgäuland Käsereien GmbH
+ ALM: Allmendinger Metzgerei
+ ALN: AL Naturkost Handels GmbH
+ ALO: Allos-Walter Lang GmbH
+ ALR: Allerleirauh GmbH
+ ALS: Alsan Werk
+ ALT: Alberts-Tofuhaus
+ ALV: Alva
+ AMA: Amazonas Naturpr.Handels GmbH
+ AMS: Santa Fe Europe GmbH
+ AMW: AlmaWin GmbH
+ AND: Andechser Molkerei GmbH
+ ANF: Allgäu Natur GmbH
+ ANG: Angelmaier OHG
+ ANI: Anis de l' Abbaye
+ ANJ: Atelier Niedernjesa
+ ANL: Anderlbauer Frasdorf
+ ANN: AN, Ne Bienenwachskerzen
+ ANT: Antico Forno a Legna
+ APR: Apeiron
+ ARC: Arche Naturprodukte GmbH
+ ARG: ARGANDÓR
+ ARI: Aries Umweltprodukte
+ ARO: Aromabalance, Silvia Plum
+ art: Artesia
+ asc: Asch
+ ASS: Assindia - GmbH
+ ATX: Ute Arnswald
+ AUR: Auro Pflanzenchemie AG
+ BAB: Baba-Laffa, W. Maguid
+ BAC: Hof Backensholz
+ BAE: Biofarm A.E.
+ bai: Bioland-Hof Gerhard Baiker
+ bäj: Bärthele, Joachim
+ BAK: Bauckhof demeter Naturkost
+ BAL: Ballybrado, Cahir Co.
+ BAS: Bastiaansen Bio-Kaas B.V.
+ BAU: BioBauernmarkt Chiemgau eG
+ bba: Bäckerei Bahde
+ bbi: Bäckerei Bihn
+ BBN: Bio Bavaria Naturkost
+ BBO: Flemming Naturkost
+ BBR: Mineralbrunnen AG
+ BBW: Bioland Ba-Wü GmbH
+ bdh: Butendiek-Hof
+ bdp: Biodynaminska Produkter
+ BDW: Bio-Dienst Weiss GmbH
+ BEL: Bellenature
+ BER: Bergquell GmbH & Co.KG
+ BEU: Beutelsbacher GmbH
+ BFA: Biofarms s.r.l.
+ BFG: Burkhardt Feinkostwerke GmbH
+ BFR: Bioforce A.Vogel GbmH
+ BFS: Bruno Fischer GmbH Naturkost
+ bgb: Heinz Bursch
+ BGH: Burgunderhof digestif´s GmbH
+ BHÄ: Bäck. Härdtner
+ bhb: Bäckerei Höhenberg
+ bhc: Bioland-Hof Christiansen´s
+ bhk: Bioland-Hof Klauser
+ bhl: Bioland Hof Lesker KG
+ bhm: Bioland-Hof Hubert Merz
+ BHO: Barnhouse Naturprodukte GmbH
+ bhö: Bioland-Hof Hörz
+ bhr: Bioland-Hof Martin Häring
+ BIA: Bio Aras, ELAFOS
+ BIB: Bio Bärchen Vertriebs GmbH
+ bid: Imkerei Binder
+ BIF: Biofrisch GmbH
+ BIH: Markus Bihler GmbH
+ bil: BioHof Laurer
+ bim: Bieringer Mühle
+ BIN: Bingenheimer Saatgut AG
+ BIO: Bio Akademie
+ BIP: Bio Plus GmbH
+ BIR: Käserei H. Birkenstock GmbH
+ BIS: Bio Service SRL
+ BIT: Bioturm
+ BIV: BioVita Naturkost GmbH
+ BIZ: Biozyklische Produkte AG
+ BJS: Josef Schäfers
+ BKB: Bio Korn Biscuits
+ bkf: Bakenhus Biofleisch GmbH
+ bkh: Bauckhof OHG
+ BKM: Biokosma GmbH
+ bko: Bäckerei Kostner
+ BKP: Brava cv
+ BKT: Biokorntakt Vertriebs GmbH
+ BKW: Liane und Roman Wirth
+ BLA: biolare ? Pilzanbau
+ BLB: Blütenland Bienenh.
+ blg: Bollinger, Bernd
+ BLI: Holle Baby Food GmbH
+ BLL: Boller Fruchtsäfte
+ BLM: Binger Lammbräu GmbH
+ BLN: Bioland GmbH Nord
+ BLR: Ökofrost GmbH
+ BLS: Bioland Südtirol
+ BMA: BIOMAS
+ BMK: N.V. Biomilk S.A.
+ BML: Bio Molkerei Lembach
+ bms: Bioland-Milchschafhof
+ bmü: Bannmühle
+ bmw: Barbara Müller Handels GmbH
+ BND: Bionade GHmbH
+ BNE: Bionest
+ BOA: Bio-Obst Augustin KG
+ BOB: Bobalis
+ BOH: Boschenhof GbR
+ BOI: Bois Naturkost GmbH
+ BOK: Bodensee Kelterei GmbH
+ BOL: Bohlsener Mühle
+ böm: BÖMO
+ BON: Allos Walter Lang GmbH
+ bos: Biol-Dyn. Obstbau Seger
+ BOW: Gandha BV
+ BPL: F.J. Moog SARL
+ BPR: Bioprim
+ bra: Brack Kaffee
+ brb: Rüdiger Born
+ BRE: Gewürzmühle Brecht GmbH
+ BRI: Brio Spa
+ BRO: Jules Brocherin S.A.
+ brs: Asgaard
+ BRT: Candy Factory KG
+ bru: Biolandhof Brummer-Bange
+ bsh: Bioland Schleswig-Holstein
+ BSK: Wiggensbach GmbH
+ bsp: Bioland-Hof Speidel
+ bsw: Bioland-Hof Schulte-Walter
+ BTH: Biothek Handels-GmbH
+ btm: EZG Bioland-Südgetreide
+ BTR: Biotropic GmbH
+ BUC: Bucheckchen
+ BUF: Bodin & Fils
+ BÜH: Metzgerei Bühler GmbH
+ BUK: Borghoff & Kötter Gbr
+ bup: Braun U. Partner
+ BUR: Burk's Fränkische Öko-Nudeln
+ BVE: Bio Vegan GmbH
+ bvg: Bioland Vermarktungs GmbH
+ BVI: abacco B.V.
+ BWH: Bornwiesenhof
+ BWL: Haya Lebensmittel GmbH
+ BYO: Byodo Naturkost GmbH
+ CAA: Coop.Agr. ARABIOS.a.r.l.
+ CAB: Erich Boden Delikatessen
+ CAL: Calendula Naturkost Backstube
+ CAM: Campobelle s.r.l.
+ can: Anthal Canadi
+ CAR: Care Naturkost GmbH & Co.
+ CAV: Cal Valls
+ CBR: C. Berger
+ CBU: Weingut Clemens Busch
+ CDO: Cha Do Teehandel
+ CES: Il Cesto
+ CGL: Castiglioni S.P.A.
+ CHE: Chemviron Carbon gmbH
+ CHI: Chiemgauer Naturfleisch GmbH
+ CHR: De Rit Handels GmbH
+ CLO: Clostermann
+ CMD: CMD Naturkosmetik
+ CNH: Chiemgauer Naturkosthandel
+ COL: Chocolat Schönenberger AG
+ COR: C&C Fine Foods, Niederlassung
+ COS: Cosmoveda, G. Eckerle
+ CTE: Castle Tea
+ CUM: Cumnatura
+ CVE: Campina Verde ecosol S.L.
+ DAG: De Dageraad
+ DAK: Die andere Konditorei
+ DAM: Dachswanger Hof
+ DAN: Danival
+ DAV: Davert GmbH
+ DBB: Die Beerenbauern
+ DBH: CW Öko Ei GmbH
+ DDC: Domain de Clairac, Frankreich
+ DDI: Demeter Dienste
+ DDM: Domaine du Midi GmbH
+ DEH: Bio-Hofmolkerei Dehlwes
+ DEN: dennree-Versorgungs GmbH
+ DET: Detmers Getreide GmbH
+ DFE: Demeter Felderzeugnisse GmbH
+ DFR: Daniel Frank
+ DGU: Dal Gustaio
+ DIE: Helmut Arendt
+ DIN: Alfons u. Franz Neumeier GbR
+ dio: Dionisio de Nova Garcia
+ dis: Schulze-Schleppinghoff
+ DIW: Weingut Hans Diwald
+ DKA: Dr. Klaus Karg
+ DKG: Geifertshofen GmbH
+ dko: Disselkoen Organics
+ dma: Demeterhof Massmann
+ dmä: Demeterhof Mäck
+ dnj: Danner, Johann Georg
+ DOT: Dottenfelder Hof
+ dpr: Demeterhof Preller
+ DRM: Dr. Martins da Cunha GmbH
+ DSE: Deutsche See
+ dsw: Dirk Schulze-Wethmar
+ DUN: Dunn's of Dublin
+ DWE: Dwersteg Destillerie
+ DWF: Dworschak-Fleischmann GbR
+ dwp: Dritte Welt Partner
+ DYF: Dynamis France
+ DZW: De Zwalm
+ EBR: Die Regionalen
+ ECO: Ecomel B.V.
+ ECP: ecopan-Naturkost GmbH
+ ecr: Ecoregion
+ ECV: ECOVER Products nv
+ EGG: Eggert´s Tiefkühl-Service
+ EGL: Wilhelm Egle GmbH
+ EGM: Ökoland GmbH
+ EIQ: Ei.Q. GmbH
+ EIS: Eisblümerl Naturkost
+ eit: Eitzinger Franz
+ ELK: Naturkost Elkershausen GmbH
+ EOG: EOS Getränke GmbH
+ EOS: Eosta International BV (NL)
+ EPI: epikouros
+ ERD: ErdmannHauser GmbH
+ ERH: Frank Erhardt
+ ERN: ERNTESEGEN Naturkost GmbH
+ EUH: Eurohealth AG
+ EUN: Euronat ? Bretagne
+ EVS: Evers Naturkost GmbH
+ EWE: Ernst Weber Naturkost
+ fah: FairHandeln
+ FAI: Frucht Agentur Iberia
+ FAL: Breisgaumilch GmbH, Fallers
+ FDO: Fattoria degli Orsi
+ fig: Fattoria degli Orsi
+ FIN: Finck
+ fis: Brauerei Rupert Fisch
+ FIT: Fitne GmbH
+ FKS: Hermann Stiefel
+ FKW: Fruchsaftkelterei Klaus Weber
+ FLA: Flamant Vert S.A.R.L.
+ FLC: Flockenhaus
+ fle: Fleckenbühler Landprodukte
+ FLN: Flemming-Naturkost
+ FLO: Florin, Angelika Trankle
+ FLP: Fleur Products BV
+ FON: Fontaine Nahrungsmittel GmbH
+ FOR: Forte
+ FÖR: Förster
+ FPF: Fahrenzhausen GmbH
+ fps: Franz Baumann
+ FRC: Francia Mozzarella
+ FRE: Hofladen Frey
+ FRH: Freiheithof
+ FRI: Frisetta GmbH
+ FRK: Frischkeim Naturkost GmbH
+ FRO: Fromin GmbH
+ FRT: Florentin-Mediterranean Food
+ FVG: FALA Verkaufsgesellschaft mbH
+ FZI: Feinkäserei Zimmermann
+ FZS: Sommer & Co.KG
+ gah: Gärtnerei Amaranth
+ gäh: Gärtnerei Halmberg
+ gäk: Gärtnerei Kienast
+ gal: Gallung´s Ziegenhof
+ GAR: Gärtnerei Landes
+ GBA: Gerald Bartke GmbH
+ gch: Gärtnerei Christian Hiss
+ gcl: Gut Clarenhof
+ gdi: Gärtnerei Distel
+ GDR: Graindrops
+ GEB: Martina Gebhardt GmbH
+ GEE: Lupina Handels GmbH
+ GEH: Georg Gehrsitz GmbH & Co.KG
+ GEP: GEPA
+ GFH: Mechthild & Andres Klose
+ GFR: Gebr. Franz GmbH
+ ggk: Burkhard Dreckel
+ gha: Gärtnerei Andreas Hankel
+ gho: Gärtnerei Horizont
+ gie: Giegold Hefefabrik
+ gip: Gilchinger Pilzzucht
+ gks: Gärtnerei Klein Sigi
+ GLG: Glafey-Lichte GmbH
+ gli: Glitz Ehringhausen
+ GLM: Gläserne Meierei GmbH
+ GOL: Golden Temple
+ GÖM: Grünsfelder GmbH & Co.KG.
+ gom: Gomille, Torsten
+ GOV: Govinda?s Naturkost GbR
+ gpb: Gerhard Preuschl Biolandhof
+ gpe: Gesa Petersen
+ GPN: Grüner Punkt Naturkost GmbH
+ GRE: C.F. Grell Nachf.
+ GRM: Grindsted Mejeri
+ GRN: Biotropic GmbH
+ GRU: Gruel Biolandhof
+ GSD: Gerhard Schürholz GmbH
+ GSE: GSE Vertrieb
+ gsi: Gregor Sing
+ gsl: Gärtnerei Schmälzle
+ gub: Gärtnerei Ulenburg
+ GUD: Gude GmbH
+ GUL: Gesund & Leben
+ GUR: Gurtmann, Christoph
+ GUT: Gute Zeiten GmbH
+ GUZ: Glahn & Zindl
+ GWF: GWF eG
+ HAA: Haaner Felsenquelle GmbH
+ hag: Hartmann Getränke
+ hal: Haldenhof
+ HAM: Hamfelder Hof
+ HAN: Handelskontor Willmann
+ HÄR: Härle
+ HAU: Dr. Hauschka ? Wala GmbH
+ HAW: hawo´s Getreidemühlen GmbH
+ HBG: Hornberger Lebensquell
+ hdk: Hof Dinkler
+ HEC: Heuck Landbäckerei
+ HEI: Heidelberger Naturfarben
+ hek: Hecker Naturkost
+ HER: Herbaria Kräuterparadies GmbH
+ HES: Weingut Heiner Sauer
+ HEU: Heuschrecke Naturkost GmbH
+ hex: Hexerküche
+ HFA: Hof Farrenau, Deimling GbR
+ hfm: Hofgemeinschaft Fischermühle
+ hfn: Hofgemeinschaft Fischermühle
+ hgo: Imkerei Oswald
+ hha: Hasso Hasbach
+ HIE: Hierl- Der Nudelmacher
+ HIN: Mathias Kloppenborg GmbH
+ HKH: Hofkäserei Heggelbach
+ hkk: Hekking
+ HLE: Holzlehner
+ HLW: Herrmannsdorfer Werkstätten
+ HMB: Humbel Brennerei
+ HMS: Handelsag. Rolf Schekerka
+ HMÜ: Peter & Martina Linxweiler
+ HOC: Hoch GmbH Oblatenfabrik
+ hof: Hofmark Brauerei
+ hoh: Dorfgem. Hohenroth
+ hoi: Henri Willig B.V.
+ HOL: Holle Baby Food GmbH
+ HÖL: Höllensprudel
+ hom: Hoffmeier
+ HOR: Horizon Natuurvoeding B.V.
+ HÖR: Bäckerei Hörtling
+ HOV: H2Ovital oHG
+ HOY: Hoyer GmbH
+ HPG: Horn Papiergroßhandel
+ hse: Herbert Seitz
+ hst: Hof Steinrausch
+ hum: Huber, Martin
+ HUN: hanf & natur
+ HUZ: Huzo
+ hwi: Henri Willig, Kaasm.
+ HWL: Hawlik Pilzbrut GmbH
+ IAB: Imkerei (Reiner) Bienefeld
+ ibe: Imkerei Berrenrath
+ ilk: Imkerei Ludger Klinker
+ ILU: ILUMINA GmbH
+ imb: Imkerei Betz
+ IMF: Hain Celestial Europe BVBA
+ imm: Mohr + Müller Imkerei
+ irf: Imkerei Feldt
+ IRM: Imkerei Roland Maier
+ ISA: ISANA GmbH & Co.KG
+ ISE: Klaus Wolf
+ ISK: Isko Vertriebs GmbH
+ IUM: I&M Inge Stamm GmbH
+ jäh: Jähnke Naturkost
+ JAN: BioFleischerei Jansen
+ JAS: Jasci Donatello
+ JAT: Jatex Handels-GmbH
+ jbe: Beck Schafhof
+ JOP: Jogopur Yogoferm GmbH
+ JOR: Jordan Cereals Ltd.
+ KÄB: (Martin) Bauhofer Käserei
+ KÄL: Inntaler GmbH
+ KAM: KAMUT Association of Europe
+ KAN: Kanne Brottrunk GmbH & Co.KG
+ KAT: Karibu Trade
+ kbh: Kiebitzhof
+ KBW: Klosterbrauerei Weißenohe
+ kei: Keil, Sepp
+ KER: Keramik & Kerzen
+ kgg: Kelterei Gregor Greimel
+ KGÖ: Karl Gröner GmbH
+ KGV: Klostergut Volkenroda
+ KHA: Konrad Halder
+ KIL: Kilian
+ KIP: Kipepeo BIO & FAIR GmbH
+ KKL: KKL-Naturwaren
+ KKV: Vollwertbäckerei König
+ KLA: AlmaWin GmbH
+ KLD: Keimland
+ KLG: Keimling Naturkost GmbH
+ klk: Käserei Schlierbach
+ klo: Klotz, Martin
+ KMF: Käsemanufaktur, Lothar Müller
+ KNE: Getreidemühle Knecht KG
+ KNÖ: Robert Knöbel
+ KON: Engelhard GmbH & Co. KG
+ KOR: Kornblume, F.+ B.Brinkmann
+ KPL: Kräutergarten Pommernland
+ KRA: Kranichhof
+ KRÄ: KAULFUSS
+ KRE: Krämers Ernährung
+ krr: Fachkrankenhaus Ringgenhof
+ KTO: Kato
+ KUC: Kolla & Co
+ KWE: Köhlerei Wengert
+ LAB: Labroco Agrarconsult
+ LAF: Hofgut Algertshausen
+ LAL: La Luna del Rospo
+ läm: Lämmerhof
+ LAN: Landkrone GmbH
+ LAS: Lakhsmi
+ LAV: laverana GmbH
+ LBI: Do-it Dutsch
+ LEB: U. Walter GmbH - Lebensbaum
+ LHQ: St. Leonhardquelle
+ LIL: Legend International Ltd.
+ LIM: Lima NV
+ LIR: Lily Rose
+ LIW: Lichtwurzel, Imton
+ LJL: Johann Langgartner
+ LMC: Lammersiek & CO.
+ LNA: Grabower Süßwaren GmbH
+ LÖC: Löcke Bio-Pilzzucht
+ LOG: Logona Hans Hansel GmbH
+ LSP: La Spinosa ( Weingut)
+ LTA: Lauretana, Wasser Import GmbH
+ LUB: Lubs GmbH
+ lüp: Lübcke Papier GmbH & Co KG
+ LUV: Luvos Heilerde
+ MAB: Bio-Nahrungsmittel GmbH
+ mah: Hof Mahlitzsch
+ MAI: Maisch
+ map: Maple GmbH
+ MAR: Marschland NK GmbH
+ MÄR: Märkisches Landbrot GmbH
+ MAT: Martinshof GmbH
+ MÄU: Gebr. Grund GmbH & Co.KG
+ MAY: Mayka Naturbackwaren GmbH
+ MAZ: Mazer, Bernd
+ MCA: Mustiola International SRL
+ med: Medousa, Griechenland Importe
+ MEK: Weingut Meinklang
+ met: Metsä Tissue GmbH
+ MGN: Naturland-Bauern e.G.
+ mhb: Meyerhof Belm
+ mhh: ?Die Meierei? Hansfelder Hof
+ MID: Midi
+ mig: Migliore
+ mil: Miller GmbH & Co.KG Agnes
+ MKF: merkur frucht Freiburg GmbH
+ MKL: Makulaku Confectionery Ltd
+ MLL: Mollis Kinderprodukte GmbH
+ MMC: MM Cosmetic GmbH
+ MOA: MolenAartje B.V. Natudis
+ MOB: Frisetta GmbH & Co.KG
+ MOC: Wasserprinz; div. Anbieter
+ MOI: Moin BioTK-Bachwaren
+ MOK: Mokobella EU GmbH
+ MOL: Moltex Baby-Hygiene GmbH
+ MOR: EgeSun GmbH
+ MÖR: Mörk Naturkostprodukte
+ MOU: Wertform GmbH & Co
+ MSV: mesa verde
+ MTB: Mont´Albano
+ MUL: Multikost Vertriebs GmbH
+ MWO: Milchwerke Oberfranken
+ MZG: Sieben Zwerge GmbH
+ mzi: Mathias Zipf
+ NAB: Hubert Tempelmann e.K.
+ NAC: BodyWise (UK)Ltd.
+ NAG: Tofumanufaktur Nagel GmbH
+ NAM: Naturmaelk A.m.b.a.
+ NAP: Combu Cha
+ NAT: Naturata e.G.
+ nbm: Nußbaumer, Roman
+ NBO: Nürnberger Bio-Originale
+ NCO: Natur Compagnie GmbH
+ NEE: Neue Erde GmbH
+ NEG: Neu´s GmbH & Co. KG
+ NEU: Gebr. Ehrnsperger e.K.
+ nhb: Naturlandhof Biberger
+ NHU: Natur Hurtig (Himalaya Salz)
+ nlg: Nordland, Lebensgem.
+ NMA: NaturMarkt GmbH
+ nmd: Münzner
+ NNA: Mensch & Natur AG
+ NOK: Noka-Sojamanufaktur GmbH
+ NPG: Nette Papier GmbH
+ NSC: Naturkost Schuchardt
+ NTM: Natumi GmbH Produkte & Ideen
+ NTR: Naturian Ökoweine OHG
+ NTU: Naturion
+ NUR: IL NURAGHE GmbH
+ NUT: Nutrifors AG
+ NWA: Nikolaihof Wachau - Weingut
+ NWR: Öko-Norm GmbH
+ OAT: Ceba Foods AB
+ obb: Obsthof Bruno Brugger
+ obm: Obsthof Bernd Majer
+ OBS: Öko-Bauernhöfe Sachsen GmbH
+ ÖBW: Brodowin Ökodorf
+ OCB: OCB-Vertriebs-GmbH
+ ODE: Odenwald EKO Brood en Banket
+ ODI: ODIN Holland C.V.
+ ÖER: Öko Ernte GmbH
+ ÖFA: Öko Feinkost Andechs Gmbh
+ ÖFR: Ökofrost GmbH
+ ogh: Demeter Gärtnerei Obergrashof
+ ohc: Obsthof Cordes
+ OHE: Obsthof Heinrich
+ ohh: Obsthof Hermann Helde
+ OHL: Ohling, Andreas
+ ÖKB: ÖkoBo
+ ÖKH: Ökohum Vertriebs GmbH
+ ÖKL: Ökoland GmbH Nord
+ ÖKN: Ökonatur
+ ÖKU: Naturkosthandel Ökollus
+ ÖLI: Öko-Line
+ ÖMA: ÖMA- Beer GmbH
+ OML: Ostermühle Naturkost GmbH
+ ÖMS: Ölmühle Solling GmbH
+ ORG: Organix4U GmbH
+ ORH: Obsthof Robert Hartmann
+ ORO: Organic Oils S.P.A.
+ ort: Biofrucht Ortlieb GbR
+ osn: Osning-Getränke GmbH
+ ÖWK: Haus am Goldberg GmbH
+ ÖWR: Weingut Richard Schmidt
+ ÖWS: Öko-Weinimport Schmid
+ pab: Bacchini Roberto & C. S.n.c.
+ PAN: Pasta Nuova GmbH
+ pau: Bioland Gemüse Paul
+ PEM: PEMA Heinrich Leupoldt KG
+ PER: Perger Getränke GmbH
+ PET: Marcel Petite, Frankreich
+ PFH: Gabriele Gersdorf GmbH
+ PFO: Papierf. Oberschmitten GmbH
+ PID: MW BGL Chiemgau eG
+ PIM: Pinzgauer Molkerei
+ PIN: Pinkus Müller GmbH & Co.KG
+ PLA: Käserei Plangger Ges.m.b.H.
+ PLG: Peralge, Marciella Callegarie
+ PLO: div. Anbieter
+ PMI: Peterstaler Mineralquellen
+ PMP: Progeo Mangimi Petfood
+ PNA: Pro Natura S.A.
+ PÖB: Pötzelberger
+ pod: Poder GmbH
+ PÖS: PINGU-Öko-Tiefkühlservice
+ PRG: Provence Regime S.A.
+ PRN: Pro Natur GmbH
+ PRO: Probio Handelsgesellschaft
+ PRV: Provamel
+ PVL: Primavera Life GmbH
+ RAA: Raab
+ RAC: Rachelli Italia s.r.l.
+ rad: Radicula GmbH (Avalon)
+ ran: Randegger Ottilienquell
+ RAP: Rapunzel Naturkost AG
+ RBB: Michael Krieger KG
+ rde: Roman Denis Bioland-Gemüsebau
+ rds: Rudolf Schramm
+ RED: Redecker
+ rei: Reicheneder, Gerhard
+ rha: Hofgut Rengoldshausen
+ RHG: Rheinland-Höfe GmbH
+ RHÖ: Rhöner (Brauerei)
+ RIE: Peter Riegel Weinimport GmbH
+ RIN: Ringenwalde Werkhof
+ RIS: Ristic
+ RIT: De Rit Handels GmbH
+ RLG: Metzgerei Rieblinger
+ ROB: Geflügelhof RoBert?s
+ rog: Söbbeke GmbH & Co. KG
+ ROH: Geflügelhof Rothäusle
+ ROL: C. F. Rolle Mühle GmbH
+ ROM: Rosmarin Ingo Karrasch GbR
+ ROS: Hubmann- Rosengarten
+ RÖS: Georg Rösner Vertriebs GmbH
+ ROT: Rother Bräu
+ rsh: Rösslerhof
+ RTE: Manfred und Christine Rothe
+ RUH: Riensch & Held GmbH & Co. KG
+ RUN: Runge Nahrungsmittel GmbH
+ RUS: Ruschin Makrobiotik GmbH
+ RZO: Rzollhäusle, H.R. Hauser
+ SAB: SANBEAM Gesunde Produkte GmbH
+ SAC: Petersilchen Sanchon GmbH
+ säh: Karla u. Sebastian Schäfer
+ SAL: Salomon
+ SAN: Sante Naturkosmetik GmbH
+ SAO: Gsund & Schön Sanoll
+ SAR: Sanatur GmbH
+ SAS: S´Atra Sardigna Coop A.r.l.
+ SAV: Santaverde GmbH
+ sbä: Steinofen Bäcker
+ SBG: Mol Hohenlohe-Franken e.G.
+ SBH: Matthias Höfflin
+ SBM: Saarpfälzische Bio-Höfe GmbH
+ SCH: Naturkost Schramm GmbH
+ SCK: Walter Rau GmbH & Co. KG
+ SDE: Robert Schindele GesmbH
+ SDL: Siegfried Schedel
+ SEE: Weingut W. Seeber
+ SEG: Sennerei Walchsee GmbH
+ SEK: Sekowa Seibold KG
+ SEL: La Selva Vertriebs-GmbH
+ SFE: Hof Mühlenberg E. Schiffers
+ sfm: Schäfer, Martin (Michaelshof)
+ SFO: Sinfo Naturkost & Naturwaren
+ SHE: Weingut Schäfer-Heinrich
+ shh: Max Fischer
+ SIN: Singer
+ SJF: Sojafarm
+ sjh: Metzgerei Schojohann
+ sjs: Schaut, Josef
+ SKA: Schönegger Käse-Alm GmbH
+ SLC: Svenska LantChips AB
+ sle: Schilling, Erich
+ slm: Salm, Elvira (Limberger)
+ SMA: Sana-Mare
+ SND: Weingut Sander
+ SNF: Sanoflore
+ SNI: Weingut Stortz-Nicolaus
+ snn: Monika u. Thomas Sannmann
+ SNT: Sonett OHG
+ SNZ: Schnitzer Bräu
+ SOB: SOBO Naturkost
+ SÖB: Söbbeke GmbH & Co. KG
+ SOD: Sodasan GmbH
+ SOE: Salamita Soc. Coop. A.r.l.
+ SOF: Soto Feinkost, Oskar Schramm
+ SOJ: Triballat Noyal
+ SON: Sonne GmbH
+ SOT: Allgäuland
+ SPA: Spaichinger Nudelmacher GmbH
+ spe: Speckhan, Rudolf
+ SPH: Spreewälder Hirsemühle
+ SPI: Spielberger KG Naturata e.G.
+ SPL: Silver Plastic GmbH & Co. KG
+ SPR: B & G Sprossenparadies GmbH
+ sqn: St. Nikolaus Quelle
+ SRH: Scharein, Hubert
+ SSC: Sural-Sacicc
+ SSI: Santisi Vollkornnudeln
+ STB: Seitenbacher GmbH Naturkost
+ STE: Steck
+ sth: Scholtenhof
+ STN: Sonnentor GmbH
+ STY: Teebaumöl Kosmetik
+ STZ: Schnitzer OHG
+ SUN: Sunval Nahrungsmittel GmbH
+ SVA: Svadesha Naturkost- Vertrieb
+ SVE: Svenska Lant Chips
+ svm: Hof von der Mehden
+ SWE: Schuldt & Weber
+ swo: Schwollener Sprudel
+ sww: Karin u. Corney Weimeijer
+ SWZ: Schweizer GmbH
+ SYM: Sympakorn
+ tag: Tagwerk
+ TAI: Life Food GmbH
+ TAP: TAPIR Wachswaren GmbH
+ TAR: Tarpa Naturkost
+ TAU: Tautropfen GmbH
+ TDP: Terra di Puglia
+ TEL: Hakle GmbH
+ TER: Terrasana Naturvoeding BV
+ TES: Terra Soleil
+ TEU: Teutoburger Ölmühle
+ TFO: Faan Zuidhorn BV. F.Andringa
+ TIL: Tilouche Fruchtimport GmbH
+ TLI: CV Ter Linde
+ TMJ: Thise Mejeri
+ TOF: Ökofrost GmbH
+ TOP: ToPas GmbH
+ tph: T.Port Hamburg GmbH & Co.
+ TRA: Tradin Organic B.V.
+ tro: Tropenfruchtimport GmbH
+ TUC: Tra Terra e Cielo
+ ubm: Upländer Bauernmolkerei GmbH
+ uhe: Uta Helberg
+ ulh: Ulmenhof
+ una: Uli Natterer
+ uns: Unseld´s Backstube
+ unt: Ulrich u. Monika Unterweger
+ URD: Uni-Vert
+ URT: Urtekram A/S
+ VAV: Vallée-Verte Handelsges. mbH
+ vbr: Vollkornbäckerei Rasche
+ VEN: eco cosmetics GmbH & Co. KG
+ VGB: Bioland Schleswig Holstein
+ VGE: Verlag gesund essen GmbH
+ VGF: Hansen & Koschmieder GmbH
+ VIA: Viana Naturkost GmbH
+ VIB: Fattoria VIB
+ VIV: S.A. Viver, Frankreich
+ VIZ: Vino Zero
+ VLV: VivoLo Vin, Ökoweinhandel
+ vms: Traitteur Villemin GmbH
+ VNI: L. Weinrich GmbH & Co.KG
+ VOE: voelkel GmbH
+ VOL: Volvic, Frankreich
+ VUN: Velazquez Universal s.L.
+ VVE: Viola Verde GmbH
+ vzw: Hüser van Zwoll GmbH & Co. KG
+ waa: Gartenbau Waas
+ wal: Walter, J.
+ WAT: Walter Thies Zellglas
+ WBT: WBT SRL
+ WDL: Milchkoop Wendland GmbH
+ WDM: Windmill Organics Foods Ltd.
+ WDN: Großbäckerei Wendeln
+ web: Weber GmbH
+ WEG: Weingut O. Gottschalk
+ WEH: Peter Werth
+ WEL: Weleda AG
+ WEN: Wilhelm Weber GmbH
+ WER: Werz GmbH & Co. KG
+ WGP: Wagner Tiefkühlprodukte GmbH
+ who: Westhof GmbH
+ WHS: Deutsche Parmalat GmbH
+ WIE: Weingut Stephanshof
+ WLM: Ecover Belgium n.v.
+ wmr: Richard Wirthmüller
+ WOB: Bäckerei Wolfgruber OHG
+ WOL: Verlag Fred Wollner GbR
+ WOO: woodshade organics ApS
+ WPA: Wepa P. Krengel GmbH & Co. KG
+ WRK: Weingut Friedhelm Rinklin
+ WSB: Battenfeld-Spanier
+ wsq: Wittenseer Quelle
+ WTI: WTI GmbH
+ WUN: Wunderland e.V.
+ WUR: Wurzel Fachgroßhandel
+ WÜR: Prima Käse, Jürgen Würth
+ wwb: Westerwald Bio GmbH
+ wwi: Weber, Wilhelm
+ WYS: KAMO, Peter Wyssling
+ wzb: Wenzelburger GbR.
+ YAK: Faan Zuidhorn BV. F. Andringa
+ YAR: Yarrah Food/Vink Sales BV
+ ZAN: Zann Bio-Center
+ ZAP: Zapparoli
+ ZEL: Zellertaler Kellerei GmbH
+ ZIM: E. Zimmermann GmbH & Co
+ ZLN: Pastificio Zanellini spa
+ ZNL: Zagler´s Naturladen
+ zsl: Ziegenhof Schlatt
+ ZWE: Zwergenwiese Naturkost GmbH
+ ZWI: E. Zwicky (Deutschland) GmbH
+ ZWÖ: Weingut im Zwölberich
+
+category:
+ "01": Brot und Backwaren
+ "02": Milch, Milchprodukte, Eier, Tofu
+ "03": Obst, Gemüse, Sprossen, Pilze
+ "04": Fleisch, Wurst, Snacks
+ "05": Getreide, Ölsaaten, Nußkerne
+ "06": Nudeln, Trockenfrüchte, Müsli
+ "07": Brotaufstriche, Honig, Nußmuse
+ "08": Würzmittel, Öle, Fette
+ "09": Süßwaren, Gebäck, Pudding
+ "10": Spezialsortimente
+ "11": Tee, Kaffee, Kakao
+ "12": Getränke
+ "13": Kräuter, Heilmittel, Ätherische Öle
+ "14": Körperpflege und Kosmetik
+ "15": Wasch- und Reinigungsmittel
+ "16": Haushaltsgeräte
+ "17": Bücher und Zeitschriften
+ "18": Papier, Schreibwaren, Spielzeug
+ "19": Textilien und Schuhe
+ "20": Farben, Bau- u. Wohnmaterial
+ "0101": Brot
+ "0102": Brötchen, Semmeln, Brezen
+ "0103": Spezialitäten
+ "0111": Standardgebäck
+ "0112": Saisongebäck
+ "0113": Kuchen, Torten
+ "0121": Pikantes Gebäck
+ "0131": Sonstiges vom Bäcker
+ "0201": Milch
+ "0202": Sauermilchprodukte
+ "0203": Quark
+ "0204": Joghurt
+ "0205": Pudding
+ "0206": Sahne, Butter, Sonstiges
+ "0211": Ziegen-/Schafsmilchprodukte
+ "0221": Frischkäse
+ "0222": Weichkäse
+ "0223": Halbfester Schnittkäse
+ "0224": Schnittkäse
+ "0225": Hartkäse
+ "0231": Ziegen-/Schafskäse
+ "0241": Eier
+ "0251": Tofu, Tempeh
+ "0252": Soja-Frischprodukte
+ "0253": Soja- und Reisgetränke
+ "0254": Sojapudding
+ "0301": Obst, heimisch
+ "0302": Südfrüchte
+ "0303": Beeren
+ "0304": Exoten
+ "0311": Kartoffeln
+ "0312": Wurzelgemüse
+ "0313": Salate
+ "0314": Blatt- und Zwiebelgemüse
+ "0315": Kohlgemüse
+ "0316": Fruchtgemüse und Spezialitäten
+ "0321": Kräuter
+ "0331": Keime und Sprossen
+ "0341": Pilze
+ "0351": Nüsse in Schale
+ "0399": Div. Frischprodukte
+ "0401": Fleisch
+ "0402": Geflügel
+ "0411": Wurst
+ "0421": Fisch
+ "0422": Fischerzeugnisse
+ "0431": Burger, Kroketten
+ "0441": Sonstige Snacks
+ "0501": Getreide
+ "0502": Hülsenfrüchte
+ "0511": Ölsaaten
+ "0521": Nußkerne
+ "0531": Keimsaaten
+ "0601": Getreideprodukte
+ "0602": Flocken
+ "0603": Nudeln
+ "0611": Sojaerzeugnisse
+ "0621": Trockenfrüchte
+ "0631": Müsli
+ "0632": Krunchy
+ "0701": Würzige Aufstriche
+ "0702": Fruchtaufstriche
+ "0711": Honig
+ "0712": Honigprodukte
+ "0721": Nußmuse
+ "0801": Salz und Kräutersalz
+ "0802": Essig
+ "0803": Senf
+ "0804": Suppen und Soßen
+ "0805": Sojasoße und Miso
+ "0806": Würzmittel
+ "0811": Gewürze
+ "0812": Gewürzmischungen
+ "0813": Gewürzöle
+ "0821": Speiseöle
+ "0822": Margarine
+ "0831": Pikante Konserven
+ "0832": Süße Konserven
+ "0841": Fertiggerichte
+ "0842": Halbfertiggerichte
+ "0901": Frucht- und Knusperriegel
+ "0902": Bonbons und Lutscher
+ "0903": Schokolade
+ "0904": Pralinen
+ "0911": Dauergebäck
+ "0912": Waffeln
+ "0913": Kekse
+ "0914": Knabbergebäck
+ "0921": Süßmittel
+ "0922": Obstdicksäfte
+ "0923": Carob
+ "0931": Pudding
+ "0932": Back- und Geliermittel
+ "0933": Kochhilfen, Fermente
+ "1001": Säuglingsbreie
+ "1002": Babykost
+ "1011": Makrobiotische Spezialitäten
+ "1021": 3. Welt-Solidaritätswaren
+ "1031": Tiefkühlkost
+ "1051": Tiernahrung
+ "1101": Früchtetee
+ "1102": Kräutertee
+ "1103": Kräutertee-Mischungen
+ "1104": Rooibos
+ "1105": Gewürztee
+ "1111": Schwarzer Tee
+ "1112": Grüner Tee
+ "1113": Aromatisierter Tee
+ "1121": Bohnenkaffee
+ "1122": Ersatzkaffee
+ "1131": Kakao
+ "1132": Schokoladengetränke
+ "1201": Wasser
+ "1211": Fruchtsäfte
+ "1212": Fruchtnektare, Limonade, Schorle
+ "1213": Gemüsesäfte
+ "1215": Kwaszgetränke, Getreidegetränke, Diätgetränke
+ "1221": Bier
+ "1231": Rotwein
+ "1232": Rosé-Wein
+ "1233": Weißwein
+ "1241": Cidre
+ "1242": Schaumwein
+ "1251": Spirituosen
+ "1301": Heilkräuter
+ "1302": Kräutermischungen
+ "1311": Freiverkäufliche Arzneimittel
+ "1312": Kur- und Heilmittel
+ "1321": Ätherische Öle
+ "1322": Ätherische Ölmischungen
+ "1331": Duftlampen und Rauchgefäße
+ "1332": Zubehör für Duftwerk
+ "1341": Räucherwerk
+ "1401": Seife
+ "1402": Gesichtsreinigung und -pflege
+ "1403": Körperöl und Körperpflege
+ "1404": Haarpflege
+ "1405": Zahn- und Mundpflege
+ "1406": Handcreme
+ "1407": Fußpflege
+ "1411": Badezusätze und Duschpräparate
+ "1412": Deo, Eau de Toilette
+ "1413": Rasierzubehör
+ "1414": Sonnenschutz
+ "1415": Baby- und Kinderpflege
+ "1421": Dekorativkosmetik
+ "1422": Parfum
+ "1423": Sonstige Kosmetik
+ "1431": Zahnbürsten
+ "1432": Bürsten und Kämme
+ "1433": Kosmetikzubehör
+ "1441": Hygiene
+ "1451": Tierpflege
+ "1501": Waschmittel
+ "1502": Spülmittel
+ "1503": Reinigungsmittel
+ "1511": Dosierhilfsmittel
+ "1521": Schuhcreme
+ "1531": Insektenschutz, Düngemittel
+ "1601": Handmühlen
+ "1602": Elektromühlen
+ "1603": Kombi-Maschinen
+ "1604": Zubehör für Kombigeräte
+ "1611": Sonstige Haushaltsgeräte
+ "1612": Keimgeräte, Dörrapparate, Gärtöpfe
+ "1621": Küchenhelfer
+ "1622": Kaffee- und Teefilter
+ "1631": Haushaltswaren
+ "1701": Kochen und Backen
+ "1702": Ernährung und Gesundheit
+ "1703": Landwirtschaft und Garten
+ "1704": Ökologie und Ergänzendes
+ "1705": Baubiologie
+ "1706": Esoterisches
+ "1707": Sonstige Bücher
+ "1711": Zeitschriften
+ "1801": Schmuckpapier
+ "1802": Schulpapier
+ "1803": Neutrales Papier
+ "1804": Formdrucke
+ "1805": Geschenkpapier
+ "1806": Sonstiges Papier
+ "1811": Stifte
+ "1812": Malbedarf
+ "1813": Knetwachs
+ "1821": Kerzen
+ "1831": Spielzeug
+ "1832": Bastelbedarf
+ "1841": Edelsteine
+ "1851": CD's
+ "1852": MC's
+ "1901": Windeln
+ "1902": Baby- und Kinderwäsche
+ "1903": Erwachsenenwäsche
+ "1904": Oberbekleidung
+ "1905": Strümpfe
+ "1911": Schuhe und Einlegesohlen
+ "2001": Imprägnierung, Lasur, Balsame
+ "2002": Lacke
+ "2003": Wandfarben
+ "2004": Kleber
+ "2009": Sonstige Farben, Lösemittel
+ "2011": Tapeten
+ "2012": Bodenbeläge
+ "2013": Dämmstoffe
+ "2019": Sonstige Baumaterialien
+ "2021": Mobiliar
+ "2022": Matratzen
+ "2023": Heimtextilien
+ "2029": Sonstige Wohnmaterialien
+ "2031": Werkzeug, Hilfsmittel
\ No newline at end of file
diff --git a/plugins/article_import/lib/foodsoft_article_import/dnb_codes.yml b/plugins/article_import/lib/foodsoft_article_import/dnb_codes.yml
new file mode 100644
index 000000000..b7cf5b020
--- /dev/null
+++ b/plugins/article_import/lib/foodsoft_article_import/dnb_codes.yml
@@ -0,0 +1,129 @@
+
+# from http://www.nieuweband.nl/producten/groepen/
+indeling:
+ 1: Verswaren
+ 50: Kaas
+ 62: Schapenkaas
+
+ 2: Basisproducten
+ 850: Noten
+ 855: Noten grootverbruik
+ 700: Peulvruchten
+ 705: Peulvruchten grootverbruik
+ 340: Rijst
+ 341: Rijst grootverbruik
+ 450: Vlokken
+ 455: Vlokken grootverbruik
+ 800: Zaden en pitten
+ 805: Zaden en pitten grootverbruik
+ 603: Melen grootverbruik
+
+ 3: Ontbijt en lunch
+ 943: Marmelade
+ 1272: Muesli en poppies
+ 1000: Notenpasta
+ 1276: Ontbijtmelen
+ 1295: Rijstwafels
+ 1290: Roggebrood
+ 1270: Sandwichspread
+ 940: Vruchtenbeleg
+ 942: Vruchtenjam
+ 944: Vruchtenstroop
+ 1300: Knäckebröd, toast en beschuit
+
+ 4: Warme maaltijd
+ 1820: Mosterd
+ 1610: Olijfolie
+ 1600: Olijven
+ 1451: Peulvruchtenconserven
+ 1957: Pindasaus
+ 1960: Sambal, ketjap en pittige smaakmakers
+ 2170: Seitan
+ 2260: Siropen
+ 2248: Smaakmakers
+ 1500: Soepen en bouillon
+ 1515: Soepstengels
+ 2000: Sojasauzen
+ 2250: Suiker
+ 1452: Tafelzuren
+ 1590: Tamme-kastanje-producten
+ 1975: Thaise keuken
+ 1900: Tomatenproducten
+ 1670: Vetten
+ 1930: Visconserven
+ 2175: Vleesvervangers
+ 1360: Vruchtencompote
+ 1400: Vruchtenconserven
+ 1350: Vruchtenmoes en -puree
+ 2249: Zout en kruidenzout
+
+ 5: Sappen en dranken
+ 2605: Rode wijn Oostenrijk
+ 2604: Rode wijn Portugal
+ 2602: Rode wijn Spanje
+ 2608: Rode wijn Zuid-Afrika
+ 2612: Rosé Spanje
+ 2617: Rosé Zuid-Afrika
+ 2420: Smoothies
+ 2455: Sojamelkproducten
+ 2505: Speciaalbieren
+ 2400: Vruchtensappen
+ 2490: Waterijs
+ 2637: Witte wijn Argentinië
+ 2630: Witte wijn Frankrijk
+ 2634: Witte wijn Griekenland
+ 2631: Witte wijn Italië
+ 2635: Witte wijn Oostenrijk
+ 2632: Witte wijn Spanje
+ 2638: Witte wijn Zuid-Afrika
+
+ 6: Warme dranken en theekruiden
+ 3102: Kruidenthee builtjes
+ 3100: Kruidenthee los
+ 3020: Kruidenthee met geneeskrachtige werking
+ 3009: Rooibosthee
+ 3010: Thee grootverpakking
+ 3052: Theekruiden
+ 3008: Witte thee
+ 3011: Yogi spice tea
+ 3012: Yogi tao tea
+ 3000: Zwarte thee
+
+ 7: Versnaperingen
+ 3552: Lollies
+ 3470: Nougat en fudge
+ 3570: Raw Food
+ 3360: Rozijntjes in kinderverpakking
+ 3410: Snijkoek
+ 3555: Snoep met suiker
+ 3550: Snoep zonder suiker
+ 3405: Stroopwafels
+ 3350: Tortillachips en salsa
+ 3358: Zoete chips
+ 3540: Zoethoutstokjes
+ 3365: Zoutjes, hartige bites en popcorn
+ 3530: Laurierdrop
+
+ 8: Persoonlijke verzorging en cosmetica
+ 5036: Lavera
+ 5037: Namaste
+ 5040: Natracare
+ 5042: Odylique
+ 5049: Sonett
+ 5055: Urtekram
+ 5065: Weleda
+
+ 9: Natuurtherapeutisch
+ 5455: Kruidentincturen
+ 5420: Propolis-producten
+ 5245: Zelfzorgmiddelen
+ 5280: Huid- en massage-olie
+
+ 10: Non Food
+ 5517: Luiers en babydoekjes
+ 5510: Maandverband en tampons
+ 5520: Toiletpapier e.d.
+ 5890: Voor kinderen (en volwassenen)
+ 5650: Was- en schoonmaakmiddelen
+ 5515: Watten
+ 5610: Luchtverfrissers
diff --git a/plugins/article_import/lib/foodsoft_article_import/engine.rb b/plugins/article_import/lib/foodsoft_article_import/engine.rb
new file mode 100644
index 000000000..a2eee1181
--- /dev/null
+++ b/plugins/article_import/lib/foodsoft_article_import/engine.rb
@@ -0,0 +1,12 @@
+module FoodsoftArticleImport
+ class Engine < ::Rails::Engine
+ config.to_prepare do
+ Dir.glob(Rails.root + "app/decorators/**/*_decorator*.rb").each do |c|
+ require_dependency(c)
+ end
+ end
+ def default_foodsoft_config(cfg)
+ cfg[:use_article_import] = false
+ end
+ end
+end
diff --git a/plugins/article_import/lib/foodsoft_article_import/foodsoft.rb b/plugins/article_import/lib/foodsoft_article_import/foodsoft.rb
new file mode 100644
index 000000000..25ff4bad8
--- /dev/null
+++ b/plugins/article_import/lib/foodsoft_article_import/foodsoft.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+# Module for Foodsoft-file import
+# The Foodsoft-file is a CSV-file, with semicolon-separated columns, or ODS/XLS/XLSX
+
+require 'roo'
+require 'roo-xls'
+
+module FoodsoftArticleImport
+ module Foodsoft
+ NAME = 'Foodsoft (CSV, ODS, XLS, XLSX)'
+ OUTLIST = false
+ OPTIONS = {
+ encoding: 'UTF-8',
+ col_sep: ';'
+ }.freeze
+
+ # Parses Foodsoft file
+ # the yielded article is a simple hash
+ def self.parse(file, custom_file_path: nil)
+ custom_file_path ||= nil
+ opts = OPTIONS.dup
+
+ ss = FoodsoftArticleImport.open_spreadsheet(file, **opts)
+
+ header_row = true
+ ss.sheet(0).each.with_index(1) do |row, i|
+ # skip first header row
+ if header_row
+ header_row = false
+ next
+ end
+ # skip empty lines
+ if row[2].to_s.strip.empty?
+ # raise no order number given
+ yield nil, nil, i
+ next
+ end
+
+ article = { order_number: row[1],
+ name: row[2],
+ note: row[3],
+ manufacturer: row[4],
+ origin: row[5],
+ unit: row[6],
+ price: row[7],
+ tax: row[8],
+ unit_quantity: row[10],
+ article_category: row[13] }
+ article.merge!(deposit: row[9]) unless row[9].nil?
+ FoodsoftArticleImport.generate_number(article) if article[:order_number].to_s.strip.empty?
+ if row[6].nil? || row[7].nil? || row[8].nil?
+ yield article, 'Error: unit, price and tax must be entered', i
+ else
+ yield article, (row[0] == 'x' ? :outlisted : nil), i
+ end
+ end
+ end
+ end
+end
diff --git a/plugins/article_import/lib/foodsoft_article_import/midgard_codes.yml b/plugins/article_import/lib/foodsoft_article_import/midgard_codes.yml
new file mode 100644
index 000000000..8777e2e7d
--- /dev/null
+++ b/plugins/article_import/lib/foodsoft_article_import/midgard_codes.yml
@@ -0,0 +1,294 @@
+manufacturer:
+ "61": Maintal
+ AB: Agrobioservice
+ AD: Anita Dehnert
+ AH: Phyto Treasures e.K.
+ AO: Arganöl
+ AR: ARIES
+ AS: Abraham Schinken
+ Ad: Molkerei Andechs
+ An: Frans Andringa
+ Ap: Apfeltraum
+ Ar: Provamel über Arche
+ Ay: Aytem
+ BA: BauckHof Amelinghausn
+ BB: Bakenhus Biofleisch GmbH
+ BC: Bio-Bäckerei Bucco
+ BD: Biosa
+ BF: Bruno Fischer
+ BG: Bauers Garten
+ BH: Bauck Hof
+ BHA: Bauck Hof Amelinghausen
+ BI: Biofarben
+ BK: Burger Knäcke
+ BKO: BioKräuterei Oberhavel
+ BL: Beumer & Lutum
+ BM: Bohlsener Mühle
+ BN: Brochenin
+ BOD: Bode Naturkost
+ BR: Luchs Bier
+ BT: Beltane Naturkost GmbH
+ BU: Baumschule am Butzelberg
+ BV: BIO VITA
+ BZ: Biozeit
+ Ba: Bauck demeter Produkte
+ Bb: Beutelsbacher
+ Bd: Biosa Danmark Aps
+ Be: Behncken
+ Bf: Backforum
+ Bg: Butzelberg
+ Bh: Barnhouse
+ Bj: Milchschafhof Brünjes
+ Bk: Blank
+ Bm: Biomax
+ Bn: Bentele
+ Bo: Bobalis
+ Bt: Bretti's
+ Bu: Biogärtnerei Bauer
+ By: Byodo
+ CA: Care
+ CF: CLUB Feinkost
+ CI: CIDRERIE
+ CV: Cosmoveda
+ Ca: Campo
+ Cl: Obsthof Clostermann
+ Co: Obsthof Cordes (Heinrich)
+ Cp: Campobello
+ Cs: Cosmoveda
+ Ct: cbet GmbH
+ DA: Danival
+ DE: DEMETER-Erzeugergemeinschaft
+ DH: Dieter Hein Wurstwaren
+ DM: Dr. Martins
+ DN: Hof Dannwisch
+ DO: Donath-Mühle
+ DR: De Rit
+ DV: Davert
+ DW: Vovic / Evian
+ De: Dennree
+ Dk: Dinkula
+ EB: Erich Boden
+ EH: Engemann Handel
+ EI: Natürlich Eistert
+ ELM: BIONADE
+ EN: Provence Regime
+ EO: Eosta
+ ER: Euresis
+ Eb: Eisblümerl
+ Eh: Erhardt Meerrettichprodukte
+ Ei: Eiland
+ El: Kelterei Elm
+ En: Eichhorn
+ Er: Erdmannhauser Brezelfabrik
+ Es: Erntesegen
+ FB: Flensburger Brauerei
+ FE: Frucht-Express
+ FF: Schiffers
+ FI: Fromi GmbH
+ FL: Florian Kerzen
+ FR: I Frutti del Sole
+ FU: Future 3000
+ Fh: Florahof
+ Fq: Fläming-Quelle
+ Fr: Frunet
+ Ft: Fontaine
+ GA: Bio-Gärtnerei Altglobsow
+ GG: Naturhof Günter Gaßmann
+ GH: Gutshöfe
+ GN: Nesse Gewürze
+ GO: Der Georgshof
+ GS: Gut Schmerwitz
+ GT: Gut Temmen
+ Gb: Grabower
+ Gl: Glaciar
+ Go: Golden Temple
+ Gr: Grützdorfer
+ Gw: Gwidon Zastawa
+ Gä: Gärtnerei am Bauerngut
+ GÖ: Stadtgut Görlitz
+ HA: Haaner Felsenquelle
+ HB: Hof Bockum
+ HF: Hühnerhof Falkenthal
+ HK: Heinz Ketchup
+ HM: Hof Marienhöhe
+ HO: Hoffmann
+ HS: Obstbau H. Schalkau
+ Ha: Hake
+ Hc: Hoch Oblatenfabrik
+ He: Hennicke
+ Hk: Natur Obsthof Hauke
+ Hl: Heidehof
+ Ho: Holle
+ Hu: Humanopolis
+ Hü: Hütterman
+ IC: Japan Grüntee
+ IN: Isola della Natura
+ IOC: IOC
+ IS: Isana
+ Ib: Iberia
+ Il: Il Nuraghe
+ Is: ISANA
+ JH: Beerenobst
+ JS: Juers Fruchtchips
+ Je: Jelitta Käse
+ KD: Kristdyn
+ KG: Kräuter Gut
+ KK: 74271
+ KN: Öko-Gartenbau
+ KP: Kräutergarten Pommerland
+ Ka: Kanne
+ Kg: Karg Brotgenuß
+ Kä: Kärrners
+ Kö: Obsthof König
+ LB: Lammsbräu
+ LE: LEEB Schaf- und Ziegenmolkerei
+ LI: Legend Organics
+ LM: LeMar
+ LS: La Selva
+ La: Lahmann
+ Lb: Lebensbaum
+ Le: Leuchtenberg Sauerkrautfabrik
+ Lh: Lindenhof
+ Li: Lima Belgien
+ Lk: Landkrone
+ Ln: Land in Sicht
+ Ls: Lubs GmbH
+ Lu: Luvos Heilerde
+ Lw: Gärtnerei Löwenzahn
+ MA: Mack
+ MB: Mabutake
+ ME: Martin Evers
+ MH: Märkische Heide
+ MI: Martin Ibele
+ MII: Katal. Olivenöl
+ ML: Märkisches Landbrot
+ MM: Bioland Imkerei
+ MT: Maintal
+ MV: MegaVega Limited
+ MY: Mayka Brezel
+ Ma: Marschland
+ Mg: MIDGARD
+ Mh: Melchhof
+ Mn: Mosna
+ Mo: Mosaikwerkstätten
+ My: MAYKA, Brezelfabrik
+ Mü: Hofmolkerei GmbH Münchehofe
+ MÖ: Märkischer Ökovertrieb
+ NE: Natürlich Eistert
+ NM:
+ NO: Nürnberger Bio Originale
+ NQ: Pineo Wasser
+ Na: NATURATA
+ Nt: Natumi
+ OTC: OTC
+ Od: ODIN Holland
+ PB: Peter Bentele
+ PG: Pilzgarten
+ PH: Biopilzhof
+ PM: Pinkus Müller
+ PN: Pro Natura
+ Pi: Piding
+ Pt: Port International
+ QB: Panettoncino
+ RB: Rother Bräu
+ RP: Rheinsberger Preussenquelle
+ RS: rosmarin BIOBACK
+ RZ: Ranch Zempow
+ Ra: Raab
+ Rb: Rabenhorst
+ Re: Rebgarten
+ Rg: Rosengarten
+ Rh: Rotenhäusler
+ Ro: Geflügelhof Robert
+ RoL: Robert´s LOSE
+ Rt: Rottstock
+ Rö: Römerquelle
+ SB: Sabines Bauernhof
+ SBP: Stiftelsen Bananen
+ SC: Sommer & Co.
+ SF: Sprossen
+ SH: Spreewälder Hirse
+ SI: SINFO
+ SK: Spargelhof Kreienbaum
+ SL: St. Leonhardsquelle
+ SM: Seenlan Müritz
+ SO: Sonett
+ SR: Sprossenmanufaktur GbR
+ STN: Sonnentor
+ SV: SANTAVERDE ALOE VERA
+ Sa: Salamita
+ Sb: Hans Hermann Soetbeer
+ Sc: Schulz-Deetz
+ Sch: Hof Schütte
+ Sd: Savid
+ Se: Sekem, Ägypten
+ Sf: Sauerkonservenfabrik Schweizer
+ Sh: Kombucha
+ Si: Land in Sicht
+ Sk: Schock Ludwigsburg
+ Sm: Schramm
+ So: Sophienhof
+ Sp: Spielberger
+ Sr: Sanmar
+ St: Steck Senf
+ StB: Stralsunder Brauerei
+ Su: Sun,Backwaren aus Norwegen
+ Sv: Sunval demeter-Produkte
+ Sw: Szilleweit
+ Sy: Synanon
+ Sz: Schrozberg
+ Sü: Südasien
+ TB: Team Blue
+ TF: Terra Frischdienst
+ TN: Tofu Nagel
+ TR: Teltower Rübchen
+ Ta: Tarpa
+ Te: Teutoburger Ölmühle
+ Ti: Tiedemann
+ Tm: Tillmann
+ Tr: tri d´Aix
+ Tt: Tautropfen
+ Tö: Töpfer Rohrzucker
+ UK: Udo Kolm Bananen
+ UL: Gärtnerei Ulenburg
+ UV: Uni-Vert
+ Ul: Ulenburg Bioland Gemüse
+ VA: Kleingenossenschaft VENUSTA
+ VD: V & D
+ VE: Vega e.K.
+ VG: Biolog. Vollwertgetränke
+ VT: Vogt
+ VV: Vallé Käse
+ Vi: Viana Tofu
+ VlV: Vivo Lo Vin
+ Vo: Voelkel
+ WB: Weber
+ WD: Werder Feinkost GmbH
+ WH: Weide-Hardebek
+ WK: BioCompany Kaffee
+ WL: Wendland Storchenmilch
+ WP: Plosewasser
+ WR: Speickwerke
+ WS: Weingut Sander
+ Wa: Watzkendorf
+ We: Wendts
+ Wh: Molkerei Weißenhorn
+ Wz: Werz Heidenheim
+ ZA: Bio-Center Zann
+ ZF: Obsthof zum Felde
+ ZG: Zwergenwiese
+ ZI: Biolandhof Zielke
+ ZK: Ziegenkäserei Karolinenhof
+ ZP: Bioland Ranch Zempow
+ ZW: Zellertaler Wein
+ bF: bio Frische
+ bi: biosanica
+ dB: ÖMA-d`Beers, Kisslegg im Algäu
+ eu: felicia
+ fa: familia Müsli
+ ha: Hawlik
+ vL: v.d.Linden
+ öG: Öko-Gartenbau
+ öh: ökohum Blumenerde
+ ÖL: Öko-Line
+ ÖS: Ölmühle Solling
\ No newline at end of file
diff --git a/plugins/article_import/lib/foodsoft_article_import/odin.rb b/plugins/article_import/lib/foodsoft_article_import/odin.rb
new file mode 100644
index 000000000..bdfa605d4
--- /dev/null
+++ b/plugins/article_import/lib/foodsoft_article_import/odin.rb
@@ -0,0 +1,85 @@
+# frozen_string_literal: true
+
+# Article import for De Nieuw Band XML file
+#
+# Always contains full assortment, including recently outlisted articles.
+# To make sure we don't keep old articles when a number of updates was missed,
+# +OUTLIST+ is set to +true+ to remove articles not present in the file.
+#
+require 'nokogiri'
+
+module FoodsoftArticleImport
+ class Odin
+ NAME = 'De Nieuwe Band (XML)'
+ OUTLIST = true
+ OPTIONS = {}.freeze
+
+ # parses a string or file
+ def self.parse(file, custom_file_path: nil, **_opts)
+ custom_file_path ||= nil
+ xml = File.open(file)
+ doc = Nokogiri.XML(xml, nil, nil,
+ Nokogiri::XML::ParseOptions::RECOVER +
+ Nokogiri::XML::ParseOptions::NONET +
+ Nokogiri::XML::ParseOptions::COMPACT) # do not modify doc!
+ load_codes(custom_file_path)
+ doc.search('product').each.with_index(1) do |row, i|
+ # create a new article
+ unit = row.search('eenheid').text
+ unit = case unit.strip
+ when '' then 'st'
+ when 'stuk' then 'st'
+ when 'g' then 'gr' # need at least 2 chars
+ when 'l' then 'ltr'
+ else unit
+ end
+ inhoud = row.search('inhoud').text
+ inhoud.to_s.strip.empty? or (inhoud.to_f - 1).abs > 1e-3 and unit = inhoud.gsub(/\.0+\s*$/, '') + unit
+ deposit = row.search('statiegeld').text
+ deposit.to_s.strip.empty? and deposit = 0
+ category = [
+ @@codes[:indeling][row.search('indeling').text.to_i],
+ @@codes[:indeling][row.search('subindeling').text.to_i]
+ ].compact.join(' - ')
+
+ status = row.search('status').text == 'Actief' ? nil : :outlisted
+ article = {}
+ unless row.search('bestelnummer').text == ''
+ article = { order_number: row.search('bestelnummer').text,
+ # :ean => row.search('eancode').text,
+ name: row.search('omschrijving').text,
+ note: row.search('kwaliteit').text,
+ manufacturer: row.search('merk').text,
+ origin: row.search('herkomst').text,
+ unit: unit,
+ price: row.search('prijs inkoopprijs').text,
+ unit_quantity: row.search('sve').text,
+ tax: row.search('btw').text,
+ deposit: deposit,
+ article_category: category }
+ end
+ yield article, status, i
+ end
+ end
+
+ @@codes = {}
+
+ def self.load_codes(custom_file_path = nil)
+ @gem_lib = File.expand_path '..', __dir__
+ dir = File.join @gem_lib, 'foodsoft_article_import'
+ begin
+ @@codes = YAML.safe_load(File.open(File.join(dir, 'dnb_codes.yml'))).symbolize_keys
+ if custom_file_path
+ custom_codes = YAML.safe_load(File.open(custom_file_path)).symbolize_keys
+ custom_codes.each_key do |key|
+ custom_codes[key] = custom_codes[key].merge @@codes[key] if @@codes.keys.include?(key)
+ @@codes = @@codes.merge custom_codes
+ end
+ end
+ @@codes
+ rescue StandardError => e
+ raise "Failed to load dnb_codes: #{dir}/dnb_codes.yml: #{e.message}"
+ end
+ end
+ end
+end
diff --git a/plugins/article_import/lib/foodsoft_article_import/version.rb b/plugins/article_import/lib/foodsoft_article_import/version.rb
new file mode 100644
index 000000000..5be60bf72
--- /dev/null
+++ b/plugins/article_import/lib/foodsoft_article_import/version.rb
@@ -0,0 +1,3 @@
+module FoodsoftArticleImport
+ VERSION = "0.0.1"
+end
diff --git a/plugins/article_import/spec/app_config.yml b/plugins/article_import/spec/app_config.yml
new file mode 100644
index 000000000..e31af571b
--- /dev/null
+++ b/plugins/article_import/spec/app_config.yml
@@ -0,0 +1,34 @@
+# Minimal Foodsoft configuration
+#
+# Without those settings, Foodsoft may not even work.
+# This file is used when running tests. When plugins would modify foodsoft behaviour
+# and they are enabled in the sample configuration, there is stable base to test with.
+
+default: &defaults
+ multi_coop_install: false
+ use_self_service: true
+ default_scope: 'f'
+
+ name: FC Minimal
+
+ # true by default to keep compat with older installations, but test with false here
+ use_nick: false
+ use_article_import: true
+
+ price_markup: 5
+
+ # do we really need the following ones?
+ tax_default: 6.0
+ email_sender: noreply@minimal.test
+
+ host: localhost
+
+
+development:
+ <<: *defaults
+
+test:
+ <<: *defaults
+
+production:
+ <<: *defaults
diff --git a/plugins/article_import/spec/files/bnn/bnn_bad_encoding.BNN b/plugins/article_import/spec/files/bnn/bnn_bad_encoding.BNN
new file mode 100644
index 000000000..0b7cb14e5
--- /dev/null
+++ b/plugins/article_import/spec/files/bnn/bnn_bad_encoding.BNN
@@ -0,0 +1,3 @@
+BNN;3;0;Naturkost Nord, Hamburg;T;Angebot Nr. 0922;EUR;20220905;20221001;20220825;837;1
+64721;A;;;4280001958081;4280001958203;Greek Dressing - Kräuter Mix;Oregano, Basilikum und Minze;;;med;;GR;C%;DE-ÖKO-001;120;1302;10;55;;1;6 x35g;6;35g;1;N;930190;99260;;1,41;;;;1;;;4,49;2,89;J;;2;3;;;;;;;;;;;;;;;;;;;A;;;;;Kg;28,571;;
+;;99
\ No newline at end of file
diff --git a/plugins/article_import/spec/files/bnn/bnn_flawless.BNN b/plugins/article_import/spec/files/bnn/bnn_flawless.BNN
new file mode 100644
index 000000000..3229196c6
--- /dev/null
+++ b/plugins/article_import/spec/files/bnn/bnn_flawless.BNN
@@ -0,0 +1,3 @@
+BNN;3;0;Naturkost Nord, Hamburg;T;Angebot Nr. 0922;EUR;20220905;20221001;20220825;837;1
+64721;X;;;4280001958081;4280001958203;Greek Dressing - Kr„uter Mix;Oregano, Basilikum und Minze;;;med;;GR;C%;DE-™KO-001;120;1302;10;55;;1;6 x35g;6;35g;1;N;930190;99260;;1,41;;;;1;;;4,49;2,89;J;;2;3;;;;;;;;;;;;;;;;;;;A;;;;;Kg;28,571;;
+;;99
diff --git a/plugins/article_import/spec/files/bnn/bnn_flawless_category.BNN b/plugins/article_import/spec/files/bnn/bnn_flawless_category.BNN
new file mode 100644
index 000000000..78234d92f
--- /dev/null
+++ b/plugins/article_import/spec/files/bnn/bnn_flawless_category.BNN
@@ -0,0 +1,3 @@
+BNN;3;0;Naturkost Nord, Hamburg;T;Angebot Nr. 0922;EUR;20220905;20221001;20220825;837;1
+64721;A;;;4280001958081;4280001958203;Greek Dressing - Kr„uter Mix;Oregano, Basilikum und Minze;;;med;;GR;C%;DE-™KO-001;120;4000;10;55;;1;6 x35g;6;35g;1;N;930190;99260;;1,41;;;;1;;;4,49;2,89;J;;2;3;;;;;;;;;;;;;;;;;;;A;;;;;Kg;28,571;;
+;;99
diff --git a/plugins/article_import/spec/files/bnn/bnn_flawless_special.BNN b/plugins/article_import/spec/files/bnn/bnn_flawless_special.BNN
new file mode 100644
index 000000000..0f285f6bb
--- /dev/null
+++ b/plugins/article_import/spec/files/bnn/bnn_flawless_special.BNN
@@ -0,0 +1,3 @@
+BNN;3;0;Naturkost Nord, Hamburg;T;Angebot Nr. 0922;EUR;20220905;20221001;20220825;837;1
+64721;A;;;4280001958081;4280001958203;Greek Dressing - Kr„uter Mix;Oregano, Basilikum und Minze;;;med;;GR;C%;DE-™KO-001;120;1302;10;55;;1;6 x35g;6;35g;1;N;930190;99260;;1,41;;;;1;;;4,49;2,89;J;;2;3;;;;;;;;;;;;;;;;;;;A;;20230101;20230201;;Kg;28,571;;
+;;99
diff --git a/plugins/article_import/spec/files/bnn/bnn_missing_entries.BNN b/plugins/article_import/spec/files/bnn/bnn_missing_entries.BNN
new file mode 100644
index 000000000..6c8dafe97
--- /dev/null
+++ b/plugins/article_import/spec/files/bnn/bnn_missing_entries.BNN
@@ -0,0 +1,3 @@
+BNN;3;0;Naturkost Nord, Hamburg;T;Angebot Nr. 0922;EUR;20220905;20221001;20220825;837;1
+64721;A;;;4280001958081;4280001958203;Greek Dressing - Kr„uter Mix;Oregano, Basilikum und Minze;;;HDE;;GR;C%;DE-™KO-001;120;1100;10;55;;1;6 x35g;6;35g;1;N;;99260;;1,41;;;;1;;;4,49;2,89;J;;;;;;;;;;;;;;;;;;;;;;A;;;;;Kg;28,571;;
+;;99
diff --git a/plugins/article_import/spec/files/bnn/bnn_missing_order_number.BNN b/plugins/article_import/spec/files/bnn/bnn_missing_order_number.BNN
new file mode 100644
index 000000000..aadcb9b6a
--- /dev/null
+++ b/plugins/article_import/spec/files/bnn/bnn_missing_order_number.BNN
@@ -0,0 +1,3 @@
+BNN;3;0;Naturkost Nord, Hamburg;T;Angebot Nr. 0922;EUR;20220905;20221001;20220825;837;1
+;A;;;4280001958081;4280001958203;Greek Dressing - Kr„uter Mix;Oregano, Basilikum und Minze;;;HDE;;GR;C%;DE-™KO-001;120;1100;10;55;;1;6 x35g;6;35g;1;N;;99260;;1,41;;;;1;;;4,49;2,89;J;;;;;;;;;;;;;;;;;;;;;;A;;;;;Kg;28,571;;
+;;99
diff --git a/plugins/article_import/spec/files/bnn/demo_file.BNN b/plugins/article_import/spec/files/bnn/demo_file.BNN
new file mode 100644
index 000000000..3d9cc23f6
--- /dev/null
+++ b/plugins/article_import/spec/files/bnn/demo_file.BNN
@@ -0,0 +1,7 @@
+BNN;3;0;Naturkost Nord, Hamburg;T;Angebot Nr. 0922;EUR;20220905;20221001;20220825;837;1
+5;;;;4280001958081;4280001958203;Žpfel Elstar;erntefrisch und knackig;;;obb;;D;C%;DE-?KO-001;120;0301;10;55;;1;10 x1kg;10;1kg;1;N;;;;1,41;;;;1;;;4,49;2,89;J;;2;3;;;;;;;;;;;;;;;;;;;A;;;;;Kg;1;;
+6;;;;4280001958081;4280001958203;Brokkoli;gesund und lecker;;;fig;;IT;C%;DE-?KO-001;120;03;10;55;;1;6 x400g;6;400g;1;N;;;;1,41;;;;1;;;4,49;2,99;J;;2;3;;;;;;;;;;;;;;;;;;;A;;;;;Kg;2,5;;
+7;;;;4280001958081;4280001958203;Tomaten;pomodori italiani, demeter;;;TDP;;IT;C%;DE-?KO-001;120;03;10;55;;1;20 x500g;20;500g;1;N;;;;1,41;;;;1;;;4,49;3,19;J;;2;3;;;;;;;;;;;;;;;;;;;A;;;;;Kg;2;;
+8;;;;4280001958081;4280001958203;Reis;Reis im Vorratssack, demeter;;;FIN;;D;C%;DE-?KO-001;120;05;10;55;;1;12 x3k;12;3kg;1;N;;;;1,41;;;;1;;;4,49;3,49;J;;2;3;;;;;;;;;;;;;;;;;;;A;;;;;Kg;0,3;;
+9;;;;4280001958081;4280001958203;Spaghetti;100% italienisches Hartweizengrie?;;;ZLN;;D;C%;DE-?KO-001;120;06;10;55;;1;4 x500g;4;500g;1;N;;;;1,41;;;;1;;;4,49;2,99;J;;2;3;;;;;;;;;;;;;;;;;;;A;;;;;Kg;2;;
+10;;;;4280001958081;4280001958203;Kartoffeln;vorwiegend festkochend;;;rsh;;D;C%;DE-?KO-001;120;0311;10;55;;1;6 x5Kg;6;5Kg;1;N;;;;1,41;;;;1;;;4,49;3,00;J;;2;3;;;;;;;;;;;;;;;;;;;A;;;;;Kg;0.2;;
diff --git a/plugins/article_import/spec/files/custom_codes.yml b/plugins/article_import/spec/files/custom_codes.yml
new file mode 100644
index 000000000..5e9020f3f
--- /dev/null
+++ b/plugins/article_import/spec/files/custom_codes.yml
@@ -0,0 +1,8 @@
+# BNN Codes
+category:
+ "4000": "Schuhe"
+additional:
+ "additional": "value"
+indeling:
+ 11: Test Indeling
+ 111: Test Subindeling
\ No newline at end of file
diff --git a/plugins/article_import/spec/files/foodsoft/foodsoft_flawless.csv b/plugins/article_import/spec/files/foodsoft/foodsoft_flawless.csv
new file mode 100644
index 000000000..a9a94c220
--- /dev/null
+++ b/plugins/article_import/spec/files/foodsoft/foodsoft_flawless.csv
@@ -0,0 +1,3 @@
+status;number;name;note;manufacturer;origin;unit ;clear price;tax;deposit;unit quantity;scale quantity;scale price;category
+;1;product;bio;someone;eu;1 kg;1.23;6;0;10;;;coolstuff
+;12;other product;bio;someone;eu;2 kg;3.45;6;0;10;;;coolstuff
\ No newline at end of file
diff --git a/plugins/article_import/spec/files/foodsoft/foodsoft_generate_order_number.csv b/plugins/article_import/spec/files/foodsoft/foodsoft_generate_order_number.csv
new file mode 100644
index 000000000..a50dde34e
--- /dev/null
+++ b/plugins/article_import/spec/files/foodsoft/foodsoft_generate_order_number.csv
@@ -0,0 +1,3 @@
+status;number;name;note;manufacturer;origin;unit ;clear price;tax;deposit;unit quantity;scale quantity;scale price;category
+;;product;bio;someone;eu;1 kg;1.23;6;0;10;;;coolstuff
+;;other product;bio;someone;eu;2 kg;3.45;6;0;10;;;coolstuff
\ No newline at end of file
diff --git a/plugins/article_import/spec/files/foodsoft/foodsoft_missing_entries.csv b/plugins/article_import/spec/files/foodsoft/foodsoft_missing_entries.csv
new file mode 100644
index 000000000..560c11af5
--- /dev/null
+++ b/plugins/article_import/spec/files/foodsoft/foodsoft_missing_entries.csv
@@ -0,0 +1,2 @@
+status;number;name;note;manufacturer;origin;unit ;clear price;tax;deposit;unit quantity;scale quantity;scale price;category
+;12;product;bio;;eu;1 kg;1.23;;0;10;;;coolstuff
\ No newline at end of file
diff --git a/plugins/article_import/spec/files/odin/odin_flawless.xml b/plugins/article_import/spec/files/odin/odin_flawless.xml
new file mode 100644
index 000000000..5b5a28fc9
--- /dev/null
+++ b/plugins/article_import/spec/files/odin/odin_flawless.xml
@@ -0,0 +1,75 @@
+
+
+
+1039
+1.08
+Estafette Associatie C.V.
+Geldermalsen
+
+
+8719325207668
+nucli rose
+Nucli rose
+
+0
+0
+0
+750
+g
+Stuk
+0
+NELEMAN
+Biologisch
+
+
+ES
+
+21
+1017515
+0109
+6
+Actief
+druiven*
+0
+0
+2
+2
+0
+0
+0
+2
+2
+0
+2
+0
+2
+0
+2
+2
+2
+2
+1
+0
+2
+0
+2
+2
+
+
+
+0
+0
+0
+0
+1
+
+2
+0
+
+adviesprijs
+2022-08-18
+4.52
+7.95
+
+
+
\ No newline at end of file
diff --git a/plugins/article_import/spec/files/odin/odin_flawless_custom_category.xml b/plugins/article_import/spec/files/odin/odin_flawless_custom_category.xml
new file mode 100644
index 000000000..460da24c5
--- /dev/null
+++ b/plugins/article_import/spec/files/odin/odin_flawless_custom_category.xml
@@ -0,0 +1,77 @@
+
+
+
+1039
+1.08
+Estafette Associatie C.V.
+Geldermalsen
+
+
+8719325207668
+nucli rose
+Nucli rose
+
+0
+0
+0
+750
+g
+Stuk
+0
+NELEMAN
+Biologisch
+
+
+ES
+
+21
+1017515
+0109
+11
+111
+6
+Actief
+druiven*
+0
+0
+2
+2
+0
+0
+0
+2
+2
+0
+2
+0
+2
+0
+2
+2
+2
+2
+1
+0
+2
+0
+2
+2
+
+
+
+0
+0
+0
+0
+1
+
+2
+0
+
+adviesprijs
+2022-08-18
+4.52
+7.95
+
+
+
\ No newline at end of file
diff --git a/plugins/article_import/spec/files/odin/odin_missing_entries.xml b/plugins/article_import/spec/files/odin/odin_missing_entries.xml
new file mode 100644
index 000000000..5089b911f
--- /dev/null
+++ b/plugins/article_import/spec/files/odin/odin_missing_entries.xml
@@ -0,0 +1,75 @@
+
+
+
+1039
+1.08
+Estafette Associatie C.V.
+Geldermalsen
+
+
+8719325207668
+nucli rose
+Nucli rose
+
+0
+0
+0
+750
+
+Stuk
+0
+
+Biologisch
+
+
+ES
+
+21
+1017515
+0109
+6
+Non Actief
+druiven*
+0
+0
+2
+2
+0
+0
+0
+2
+2
+0
+2
+0
+2
+0
+2
+2
+2
+2
+1
+0
+2
+0
+2
+2
+
+
+
+0
+0
+0
+0
+1
+
+2
+0
+
+adviesprijs
+2022-08-18
+4.52
+7.95
+
+
+
\ No newline at end of file
diff --git a/plugins/article_import/spec/files/odin/odin_missing_order_number.xml b/plugins/article_import/spec/files/odin/odin_missing_order_number.xml
new file mode 100644
index 000000000..d43a94397
--- /dev/null
+++ b/plugins/article_import/spec/files/odin/odin_missing_order_number.xml
@@ -0,0 +1,75 @@
+
+
+
+1039
+1.08
+Estafette Associatie C.V.
+Geldermalsen
+
+
+8719325207668
+nucli rose
+Nucli rose
+
+0
+0
+0
+750
+g
+Stuk
+0
+NELEMAN
+Biologisch
+
+
+ES
+
+21
+1017515
+
+6
+Actief
+druiven*
+0
+0
+2
+2
+0
+0
+0
+2
+2
+0
+2
+0
+2
+0
+2
+2
+2
+2
+1
+0
+2
+0
+2
+2
+
+
+
+0
+0
+0
+0
+1
+
+2
+0
+
+adviesprijs
+2022-08-18
+4.52
+7.95
+
+
+
\ No newline at end of file
diff --git a/plugins/article_import/spec/fixtures/bnn_file01.bnn b/plugins/article_import/spec/fixtures/bnn_file01.bnn
new file mode 100644
index 000000000..b75b63cfd
--- /dev/null
+++ b/plugins/article_import/spec/fixtures/bnn_file01.bnn
@@ -0,0 +1,5 @@
+BNN;3;0;Naturkost Nord, Hamburg;T;Angebot Nr. 0922;EUR;20220905;20221001;20220825;837;1
+29932;;;;4280001958081;4280001958203;Walnoten (ongeroosterd);bio;;;med;;GR;C%;DE-?KO-001;120;1302;10;55;;1;;1;1 kg;1;N;930190;99260;;1,41;;;;1;;;4,49;2,34;J;;2;3;;;;;;;;;;;;;;;;;;;A;;;;;Kg;28,571;;
+28391;;;;4280001958081;4280001958203;Pijnboompitten;dem;;;med;;GR;C%;DE-?KO-001;120;1302;10;55;;1;;1;100 g;10;N;930190;99260;;1,41;;;;1;;;5,56;2.89;J;;2;3;;;;;;;;;;;;;;;;;;;A;;;;;Kg;28,571;;
+1829;;;;4280001958081;4280001958203;Appelsap (verpakt);;;;med;;GR;C%;DE-?KO-001;120;1302;10;55;;1;4x250 ml;10;4x250 ml;10;N;930190;99260;;3,21;;;;1;;;4,49;2.89;J;;2;3;;;;;;;;;;;;;;;;;;;A;;;;;ml;28,571;;
+177813;;;;4280001958081;4280001958203;Tomaten;bio;;;med;;GR;C%;DE-?KO-001;120;1302;10;55;;1;;1;500 g;20;N;930190;99260;;1,20;;;;1;;;4,49;2.89;J;;2;3;;;;;;;;;;;;;;;;;;;A;;;;;g;28,571;;
\ No newline at end of file
diff --git a/plugins/article_import/spec/fixtures/bnn_file_02.bnn b/plugins/article_import/spec/fixtures/bnn_file_02.bnn
new file mode 100644
index 000000000..e3dba5bbf
--- /dev/null
+++ b/plugins/article_import/spec/fixtures/bnn_file_02.bnn
@@ -0,0 +1,2 @@
+BNN;3;0;Naturkost Nord, Hamburg;T;Angebot Nr. 0922;EUR;20220905;20221001;20220825;837;1
+1;;;;4280001958081;4280001958203;Tomatoes;organic;;;med;;GR;C%;DE-?KO-001;120;1302;10;55;;1;;20;500 g;1;N;930190;99260;;1,41;;;;1;;;4,49;1,20;J;;2;3;;;;;;;;;;;;;;;;;;;A;;;;;g;28,571;;
\ No newline at end of file
diff --git a/plugins/article_import/spec/fixtures/odin_file_01.xml b/plugins/article_import/spec/fixtures/odin_file_01.xml
new file mode 100644
index 000000000..3b60e83e4
--- /dev/null
+++ b/plugins/article_import/spec/fixtures/odin_file_01.xml
@@ -0,0 +1,273 @@
+
+
+
+1039
+1.08
+Estafette Associatie C.V.
+Geldermalsen
+
+
+8719325207668
+Walnoten (ongeroosterd)
+Nucli rose
+
+0
+0
+0
+1
+kg
+Stuk
+0
+Het warme woud
+bio
+
+
+NL
+
+6
+1017515
+29932
+10
+Actief
+druiven*
+0
+0
+2
+2
+0
+0
+0
+2
+2
+0
+2
+0
+2
+0
+2
+2
+2
+2
+1
+0
+2
+0
+2
+2
+
+
+
+0
+0
+0
+0
+1
+
+2
+0
+
+adviesprijs
+2022-08-18
+2.34
+7.95
+
+
+
+8719325207668
+Pijnboompitten
+Nucli rose
+
+0
+0
+0
+100
+g
+Stuk
+0
+NELEMAN
+dem
+
+
+TR
+
+6
+1017515
+28391
+10
+Actief
+druiven*
+0
+0
+2
+2
+0
+0
+0
+2
+2
+0
+2
+0
+2
+0
+2
+2
+2
+2
+1
+0
+2
+0
+2
+2
+
+
+
+0
+0
+0
+0
+1
+
+2
+0
+
+adviesprijs
+2022-08-18
+5.56
+7.95
+
+
+
+8719325207668
+Appelsap (verpakt)
+Nucli rose
+
+0
+0
+0
+4x250
+ml
+Stuk
+0.4
+Appelgaarde
+
+
+
+DE
+
+6
+1017515
+1829
+10
+Actief
+druiven*
+0
+0
+2
+2
+0
+0
+0
+2
+2
+0
+2
+0
+2
+0
+2
+2
+2
+2
+1
+0
+2
+0
+2
+2
+
+
+
+0
+0
+0
+0
+1
+
+2
+0
+
+adviesprijs
+2022-08-18
+3.21
+7.95
+
+
+
+8719325207668
+Tomaten
+Nucli rose
+
+0
+0
+0
+500
+g
+Stuk
+0
+De röde hof
+bio
+
+
+DE
+
+6
+1017515
+177813
+20
+Actief
+druiven*
+0
+0
+2
+2
+0
+0
+0
+2
+2
+0
+2
+0
+2
+0
+2
+2
+2
+2
+1
+0
+2
+0
+2
+2
+
+
+
+0
+0
+0
+0
+1
+
+2
+0
+
+adviesprijs
+2022-08-18
+1.2
+7.95
+
+
+
\ No newline at end of file
diff --git a/plugins/article_import/spec/fixtures/odin_file_02.xml b/plugins/article_import/spec/fixtures/odin_file_02.xml
new file mode 100644
index 000000000..c732b4d57
--- /dev/null
+++ b/plugins/article_import/spec/fixtures/odin_file_02.xml
@@ -0,0 +1,75 @@
+
+
+
+1039
+1.08
+Estafette Associatie C.V.
+Geldermalsen
+
+
+8719325207668
+Tomatoes
+Nucli rose
+
+0
+0
+0
+500
+g
+Stuk
+0
+De röde hof
+organic
+
+
+Somewhere, UK
+
+6
+1017515
+1
+20
+Actief
+druiven*
+0
+0
+2
+2
+0
+0
+0
+2
+2
+0
+2
+0
+2
+0
+2
+2
+2
+2
+1
+0
+2
+0
+2
+2
+
+
+
+0
+0
+0
+0
+1
+
+2
+0
+
+adviesprijs
+2022-08-18
+1.2
+7.95
+
+
+
\ No newline at end of file
diff --git a/plugins/article_import/spec/integration/articles_spec.rb b/plugins/article_import/spec/integration/articles_spec.rb
new file mode 100644
index 000000000..0a5475158
--- /dev/null
+++ b/plugins/article_import/spec/integration/articles_spec.rb
@@ -0,0 +1,169 @@
+require_relative '../test_helper'
+require_relative '../../../../spec/spec_helper'
+
+feature ArticlesController do
+
+ let(:user) { create(:user, groups: [create(:workgroup, role_article_meta: true)]) }
+ let(:supplier) { create(:supplier) }
+ let!(:article_category) { create(:article_category) }
+
+ before { login user }
+
+ describe ':index', js: true do
+ before do
+ login user
+ visit supplier_articles_path(supplier_id: supplier.id)
+ end
+
+ it 'can visit supplier articles path' do
+ expect(page).to have_content(supplier.name)
+ expect(page).to have_content(I18n.t('articles.index.edit_all'))
+ end
+
+ it 'can create a new article' do
+ click_on I18n.t('articles.index.new')
+ expect(page).to have_selector('form#new_article')
+ article = build(:article, supplier: supplier, article_category: article_category)
+ within('#new_article') do
+ fill_in 'article_name', :with => article.name
+ fill_in 'article_unit', :with => article.unit
+ select article.article_category.name, :from => 'article_article_category_id'
+ fill_in 'article_price', :with => article.price
+ fill_in 'article_unit_quantity', :with => article.unit_quantity
+ fill_in 'article_tax', :with => article.tax
+ fill_in 'article_deposit', :with => article.deposit
+ # "Element cannot be scrolled into view" error, js as workaround
+ # find('input[type="submit"]').click
+ page.execute_script('$("form#new_article").submit();')
+ end
+ expect(page).to have_content(article.name)
+ end
+ end
+
+ describe ':upload' do
+ let(:filename) { 'foodsoft_file_02.csv' }
+ let(:file) { Rails.root.join("spec/fixtures/#{filename}") }
+
+ before do
+ visit upload_supplier_articles_path(supplier_id: supplier.id)
+ attach_file 'articles_file', file
+ end
+
+ Dir.glob('spec/fixtures/foodsoft_file_01.*') do |test_file|
+ describe "can import articles from #{test_file}" do
+ let(:file) { Rails.root.join(test_file) }
+
+ it do
+ find("#articles_type option[value='foodsoft']").select_option
+ find('input[type="submit"]').click
+ expect(find("tr:nth-child(1) #new_articles__note").value).to eq "bio â—Ž"
+ expect(find("tr:nth-child(2) #new_articles__name").value).to eq "Pijnboompitten"
+
+ 4.times do |i|
+ all("tr:nth-child(#{i + 1}) select > option")[1].select_option
+ end
+ find('input[type="submit"]').click
+ expect(page).to have_content("Pijnboompitten")
+
+ expect(supplier.articles.count).to eq 4
+ end
+ end
+ end
+
+ Dir.glob('spec/fixtures/bnn_file_01.*') do |test_file|
+ describe "can import articles from #{test_file}" do
+ let(:file) { Rails.root.join(test_file) }
+
+ it do
+ find("#articles_type option[value='bnn']").select_option
+ find('input[type="submit"]').click
+ expect(find("tr:nth-child(1) #new_articles__note").value).to eq "bio"
+ expect(find("tr:nth-child(1) #new_articles__name").value).to eq "Walnoten (ongeroosterd)"
+ # set article category
+ 4.times do |i|
+ all("tr:nth-child(#{i + 1}) select > option")[1].select_option
+ end
+ find('input[type="submit"]').click
+
+ expect(page).to have_content("Pijnboompitten")
+
+ expect(supplier.articles.count).to eq 4
+ end
+ end
+ end
+ end
+
+ describe "updates" do
+ file_paths = ['spec/fixtures/foodsoft_file_02.csv', 'plugins/article_import/spec/fixtures/bnn_file_02.bnn', 'plugins/article_import/spec/fixtures/odin_file_02.xml']
+ let(:filename) { 'foodsoft_file_02.csv' }
+ let(:file) { Rails.root.join("spec/fixtures/#{filename}") }
+ let(:val) { 'foodsoft' }
+ let(:type) { %w[foodsoft bnn odin] }
+
+ before do
+ visit upload_supplier_articles_path(supplier_id: supplier.id)
+ attach_file 'articles_file', file
+ find("#articles_type option[value='#{val}']").select_option
+ end
+
+ file_paths.each_with_index do |test_file, index|
+ describe "updates article for #{test_file}" do
+ let(:article) { create(:article, supplier: supplier, name: 'Foobar', order_number: 1, unit: '250 g') }
+ let(:file) { Rails.root.join(test_file) }
+ let(:val) { type[index] }
+
+ it do
+ article.reload
+ find('input[type="submit"]').click
+ expect(find("#articles_#{article.id}_name").value).to eq 'Tomatoes'
+ find('input[type="submit"]').click
+ article.reload
+ expect(article.name).to eq 'Tomatoes'
+ if type[index] == "odin"
+ expect([article.unit, article.unit_quantity, article.price]).to eq ['500gr', 20, 1.20]
+ else
+ expect([article.unit, article.unit_quantity, article.price]).to eq ['500 g', 20, 1.20]
+ end
+ end
+
+ it "handles missing data" do
+ find('input[type="submit"]').click # to overview
+ find('input[type="submit"]').click # missing category, re-show form
+ expect(find('tr.alert')).to be_present
+ expect(supplier.articles.count).to eq 0
+
+ all("tr select > option")[1].select_option
+ find('input[type="submit"]').click # now it should succeed
+ expect(supplier.articles.count).to eq 1
+ end
+ end
+
+ describe "can remove an existing article" do
+ let!(:article) { create(:article, supplier: supplier, name: 'Foobar', order_number: 99999) }
+
+ it do
+ check('articles_outlist_absent')
+ find('input[type="submit"]').click
+ expect(find("#outlisted_articles_#{article.id}", visible: :all)).to be_present
+
+ all("tr select > option")[1].select_option
+ find('input[type="submit"]').click
+ expect(article.reload.deleted?).to be true
+ end
+ end
+
+ describe "can convert units when updating" do
+ let!(:article) { create(:article, supplier: supplier, order_number: 1, unit: '250 g') }
+
+ it do
+ check('articles_convert_units')
+ find('input[type="submit"]').click
+ expect(find("#articles_#{article.id}_name").value).to eq 'Tomatoes'
+ find('input[type="submit"]').click
+ article.reload
+ expect([article.unit, article.unit_quantity, article.price]).to eq ['250 g', 40, 0.6]
+ end
+ end
+ end
+ end
+end
diff --git a/plugins/article_import/spec/integration/supplier_spec.rb b/plugins/article_import/spec/integration/supplier_spec.rb
new file mode 100644
index 000000000..d80c0d52c
--- /dev/null
+++ b/plugins/article_import/spec/integration/supplier_spec.rb
@@ -0,0 +1,21 @@
+require_relative '../test_helper'
+require_relative '../../../../spec/spec_helper'
+
+describe Supplier do
+ let(:supplier) { create :supplier }
+
+ context 'syncs from file' do
+ it 'imports and updates articles' do
+ article1 = create(:article, supplier: supplier, order_number: 177813, unit: '250 g', price: 0.1)
+ article2 = create(:article, supplier: supplier, order_number: 12345)
+ supplier.articles = [article1, article2]
+ options = { filename: 'foodsoft_file_01.csv' }
+ options[:outlist_absent] = true
+ options[:convert_units] = true
+ updated_article_pairs, outlisted_articles, new_articles = supplier.sync_from_file(Rails.root.join('spec/fixtures/foodsoft_file_01.csv'), 'foodsoft', options)
+ expect(new_articles.length).to be > 0
+ expect(updated_article_pairs.first[1][:name]).to eq 'Tomaten'
+ expect(outlisted_articles.first).to eq article2
+ end
+ end
+end
\ No newline at end of file
diff --git a/plugins/article_import/spec/lib/bnn/foodsoft_article_import_bnn_spec.rb b/plugins/article_import/spec/lib/bnn/foodsoft_article_import_bnn_spec.rb
new file mode 100644
index 000000000..d50aa591a
--- /dev/null
+++ b/plugins/article_import/spec/lib/bnn/foodsoft_article_import_bnn_spec.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_relative '../../../lib/foodsoft_article_import'
+
+describe FoodsoftArticleImport do
+ files_path = File.expand_path '../../files', __dir__
+ bnn_files_path = File.join(files_path, 'bnn')
+
+ dummy_article = { name: 'Greek Dressing - Kräuter Mix', order_number: '64721', note: 'Oregano, Basilikum und Minze',
+ manufacturer: 'Medousa, Griechenland Importe', origin: 'GR', article_category: 'Kräutermischungen', unit: '35g', price: '2,89', tax: 7.0, unit_quantity: '6' }
+
+ article = dummy_article.merge({ deposit: 0.08 })
+ article_special = article.merge(note: 'Sonderpreis: 2,89 von 20230101 bis 20230201')
+
+ article_2 = dummy_article.merge({ manufacturer: nil, article_category: nil })
+
+ article_custom_code = article.merge(article_category: 'Schuhe')
+
+ empty = {}
+
+ context 'bnn' do
+ it 'parses bnn file correctly without type parameter' do
+ FoodsoftArticleImport.parse(File.open(File.join(bnn_files_path, 'bnn_flawless.BNN'))) do |new_attrs, status, _line|
+ expect(new_attrs).to eq article
+ expect(status).to eq :outlisted
+ end
+ end
+ it 'parses file correctly with type parameter' do
+ FoodsoftArticleImport.parse(File.open(File.join(bnn_files_path, 'bnn_flawless.BNN')), type: 'bnn') do |new_attrs, status, _line|
+ expect(new_attrs).to eq article
+ expect(status).to eq :outlisted
+ end
+ end
+ it 'raises error wenn wrong type (except odin) specified' do
+ expect do
+ FoodsoftArticleImport.parse(File.open(File.join(bnn_files_path, 'bnn_flawless.BNN')), type: 'foodsoft')
+ end.to raise_error(RuntimeError)
+
+ expect(FoodsoftArticleImport.parse(File.open(File.join(bnn_files_path, 'bnn_flawless.BNN')), type: 'odin')).to eq []
+ end
+ it 'parses article with special correctly' do
+ FoodsoftArticleImport.parse(File.open(File.join(bnn_files_path, 'bnn_flawless_special.BNN')), type: 'bnn') do |new_attrs, status, _line|
+ expect(new_attrs).to eq article_special
+ expect(status).to eq :special
+ end
+ end
+ it 'parses missing entries correctly' do
+ FoodsoftArticleImport.parse(File.open(File.join(bnn_files_path, 'bnn_missing_entries.BNN')), type: 'bnn') do |new_attrs, status, _line|
+ expect(new_attrs).to eq article_2
+ expect(status).to eq nil
+ end
+ end
+ it 'skips rows without order_number' do
+ FoodsoftArticleImport.parse(File.open(File.join(bnn_files_path, 'bnn_missing_order_number.BNN')), type: 'bnn') do |new_attrs, _status, _line|
+ expect(new_attrs).to eq empty
+ end
+ end
+ it 'joins custom_codes file' do
+ custom_file_path = File.join(files_path, 'custom_codes.yml').to_s
+ FoodsoftArticleImport.parse(File.open(File.join(bnn_files_path, 'bnn_flawless_category.BNN')), custom_file_path: custom_file_path, type: 'bnn') do |new_attrs, _status, _line|
+ expect(new_attrs).to eq article_custom_code
+ end
+ end
+ it 'parses file with different encoding' do
+ # the bnn file is loaded with encoding ibm850. If file is not ibm850 encoded, some characters might look weird
+ FoodsoftArticleImport.parse(File.open(File.join(bnn_files_path, 'bnn_bad_encoding.BNN')), type: 'bnn') do |new_attrs, _status, _line|
+ expect(new_attrs[:order_number]).to eq('64721')
+ expect(new_attrs[:name]).to eq('Greek Dressing - Kräuter Mix')
+ end
+ end
+ end
+end
diff --git a/plugins/article_import/spec/lib/foodsoft/foodsoft_article_import_foodsoft_spec.rb b/plugins/article_import/spec/lib/foodsoft/foodsoft_article_import_foodsoft_spec.rb
new file mode 100644
index 000000000..ce78b12b2
--- /dev/null
+++ b/plugins/article_import/spec/lib/foodsoft/foodsoft_article_import_foodsoft_spec.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require 'foodsoft_article_import'
+
+describe FoodsoftArticleImport do
+ files_path = File.expand_path '../../files', __dir__
+ foodsoft_files_path = File.join(files_path, 'foodsoft')
+
+ dummy_article = { order_number: '1', name: 'product', note: 'bio', manufacturer: 'someone', origin: 'eu',
+ unit: '1 kg', price: '1.23', tax: '6', unit_quantity: '10', article_category: 'coolstuff', deposit: '0' }
+
+ dummy_article_2 = { order_number: '12', name: 'other product', note: 'bio', manufacturer: 'someone',
+ origin: 'eu', unit: '2 kg', price: '3.45', tax: '6', unit_quantity: '10', article_category: 'coolstuff', deposit: '0' }
+
+ articles = [dummy_article, dummy_article_2]
+
+ dummy_article_3 = dummy_article.merge({ order_number: ':d8df298' })
+ dummy_article_4 = dummy_article_2.merge({ order_number: ':1f37e39' })
+ articles_number_generated = [dummy_article_3, dummy_article_4]
+ empty = {}
+
+ context 'foodsoft' do
+ it 'parses file correctly with type parameter foodsoft' do
+ count = 0
+ FoodsoftArticleImport.parse(File.open(File.join(foodsoft_files_path, 'foodsoft_flawless.csv')), type: 'foodsoft') do |new_attrs, status, _line|
+ expect(new_attrs).to eq articles[count]
+ expect(status).to eq nil
+ count += 1
+ end
+ end
+
+ it 'raises error wenn wrong type specified' do
+ expect(FoodsoftArticleImport.parse(File.open(File.join(foodsoft_files_path, 'foodsoft_flawless.csv')), type: 'odin')).to eq []
+
+ expect(FoodsoftArticleImport.parse(File.open(File.join(foodsoft_files_path, 'foodsoft_flawless.csv')), type: 'bnn')).to eq []
+ end
+
+ it 'parses missing entries correctly' do
+ FoodsoftArticleImport.parse(File.open(File.join(foodsoft_files_path, 'foodsoft_missing_entries.csv')), type: 'foodsoft') do |new_attrs, status, _line|
+ expect(status).to eq 'Error: unit, price and tax must be entered'
+ expect(new_attrs[:unit]).to eq '1 kg'
+ expect(new_attrs[:manufacturer]).to eq nil
+ end
+ end
+
+ it 'generates order numbers for articles without order number' do
+ count = 0
+ FoodsoftArticleImport.parse(File.open(File.join(foodsoft_files_path, 'foodsoft_generate_order_number.csv')), type: 'foodsoft') do |new_attrs, _status, _line|
+ expect(new_attrs).to eq articles_number_generated[count]
+ count += 1
+ end
+ end
+
+ xit 'joins custom_codes file' do
+ custom_file_path = File.join(files_path, 'custom_codes.yml').to_s
+ FoodsoftArticleImport.parse(File.open(File.join(foodsoft_files_path, 'foodsoft_flawless_custom_category.csv')), custom_file_path: custom_file_path, type: 'foodsoft') do |new_attrs, _status, _line|
+ expect(new_attrs[:article_category]).to eq 'Test Indeling - Test Subindeling'
+ end
+ end
+ end
+end
diff --git a/plugins/article_import/spec/lib/odin/foodsoft_article_import_odin_spec.rb b/plugins/article_import/spec/lib/odin/foodsoft_article_import_odin_spec.rb
new file mode 100644
index 000000000..af3da3f46
--- /dev/null
+++ b/plugins/article_import/spec/lib/odin/foodsoft_article_import_odin_spec.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_relative '../../../lib/foodsoft_article_import'
+
+describe FoodsoftArticleImport do
+ files_path = File.expand_path '../../files', __dir__
+ odin_files_path = File.join(files_path, 'odin')
+
+ dummy_article = { order_number: '0109', name: 'nucli rose', note: 'Biologisch', manufacturer: 'NELEMAN',
+ origin: 'ES', unit: '750gr', price: '4.52', unit_quantity: '6', tax: '21', deposit: '0', article_category: '' }
+
+ empty = {}
+
+ context 'odin' do
+ it 'parses file correctly with type parameter odin' do
+ FoodsoftArticleImport.parse(File.open(File.join(odin_files_path, 'odin_flawless.xml')), type: 'odin') do |new_attrs, status, _line|
+ expect(new_attrs).to eq dummy_article
+ expect(status).to eq nil
+ end
+ end
+
+ it 'raises error wenn wrong type specified' do
+ expect do
+ FoodsoftArticleImport.parse(File.open(File.join(odin_files_path, 'odin_flawless.xml')), type: 'foodsoft')
+ end.to raise_error(RuntimeError)
+
+ expect do
+ FoodsoftArticleImport.parse(File.open(File.join(odin_files_path, 'odin_flawless.xml')), type: 'bnn')
+ end.to raise_error(CSV::MalformedCSVError)
+ end
+
+ it 'parses missing entries correctly' do
+ FoodsoftArticleImport.parse(File.open(File.join(odin_files_path, 'odin_missing_entries.xml')), type: 'odin') do |new_attrs, status, _line|
+ expect(status).to eq :outlisted
+ expect(new_attrs[:unit]).to eq '750st'
+ expect(new_attrs[:manufacturer]).to eq ''
+ end
+ end
+
+ it 'skips rows without order_number' do
+ FoodsoftArticleImport.parse(File.open(File.join(odin_files_path, 'odin_missing_order_number.xml')), type: 'odin') do |new_attrs, _status, _line|
+ expect(new_attrs).to eq empty
+ end
+ end
+
+ it 'joins custom_codes file' do
+ custom_file_path = File.join(files_path, 'custom_codes.yml').to_s
+ FoodsoftArticleImport.parse(File.open(File.join(odin_files_path, 'odin_flawless_custom_category.xml')), custom_file_path: custom_file_path, type: 'odin') do |new_attrs, _status, _line|
+ expect(new_attrs[:article_category]).to eq 'Test Indeling - Test Subindeling'
+ end
+ end
+
+ xit 'parses dummy_article with special correctly' do
+ # TODO: find out whether there are special prices for odin files
+ FoodsoftArticleImport.parse(File.open(File.join(odin_files_path, 'bnn_flawless_special.BNN')), type: 'bnn') do |new_attrs, _status, _line|
+ expect(new_attrs.manufacturer).to eq nil
+ expect(new_attrs.unit).to eq '750st'
+ end
+ end
+
+ xit 'parses file with different encoding' do
+ # the bnn file is loaded with encoding ibm850. If file is not ibm850 encoded, some characters might look weird
+ FoodsoftArticleImport.parse(File.open(File.join(odin_files_path, 'bnn_bad_encoding.BNN')), type: 'bnn') do |new_attrs, _status, _line|
+ expect(new_attrs[:order_number]).to eq('64721')
+ expect(new_attrs[:name]).to eq('Greek Dressing - Kräuter Mix')
+ end
+ end
+ end
+end
diff --git a/plugins/article_import/spec/test_helper.rb b/plugins/article_import/spec/test_helper.rb
new file mode 100644
index 000000000..527a16d8f
--- /dev/null
+++ b/plugins/article_import/spec/test_helper.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module TestHelper
+ ENV["FOODSOFT_APP_CONFIG"] = "plugins/article_import/spec/app_config.yml"
+end
+
+RSpec.configure do |config|
+ config.include TestHelper, :type => :feature
+end