From d595ae81171ab0e1d5d0c8f0c5abe3f7634607ea Mon Sep 17 00:00:00 2001 From: JuliusR <> Date: Sat, 18 Dec 2021 11:53:05 +0100 Subject: [PATCH] migrate previous lib/ (except tasks) to app/lib --- app/lib/article_import.rb | 67 ++ app/lib/article_import/bioromeo.rb | 185 ++++ app/lib/article_import/bnn.rb | 90 ++ app/lib/article_import/bnn_codes.yml | 1119 ++++++++++++++++++++++ app/lib/article_import/borkenstein.rb | 94 ++ app/lib/article_import/dnb_codes.yml | 130 +++ app/lib/article_import/dnb_xml.rb | 73 ++ app/lib/article_import/foodsoft.rb | 55 ++ app/lib/article_import/midgard_codes.yml | 294 ++++++ app/lib/ftp_sync.rb | 36 + 10 files changed, 2143 insertions(+) create mode 100644 app/lib/article_import.rb create mode 100644 app/lib/article_import/bioromeo.rb create mode 100644 app/lib/article_import/bnn.rb create mode 100644 app/lib/article_import/bnn_codes.yml create mode 100644 app/lib/article_import/borkenstein.rb create mode 100644 app/lib/article_import/dnb_codes.yml create mode 100644 app/lib/article_import/dnb_xml.rb create mode 100644 app/lib/article_import/foodsoft.rb create mode 100644 app/lib/article_import/midgard_codes.yml create mode 100644 app/lib/ftp_sync.rb diff --git a/app/lib/article_import.rb b/app/lib/article_import.rb new file mode 100644 index 0000000..a88899d --- /dev/null +++ b/app/lib/article_import.rb @@ -0,0 +1,67 @@ +require 'digest/sha1' +require 'tempfile' + +module ArticleImport + + class ConversionFailedException < Exception; end + + # return list of known file formats + # each file_format module has + # #name return a human-readable file format name + # #outlist_unlisted if returns true, unlisted articles are outlisted + # #detect return a likelyhood (0-1) of being able to process + # #parse parse the data + # + def self.file_formats + @@file_formats ||= { + 'bnn' => ArticleImport::Bnn, + 'borkenstein' => ArticleImport::Borkenstein, + 'foodsoft' => ArticleImport::Foodsoft, + 'dnb_xml' => ArticleImport::DnbXml, + 'bioromeo' => ArticleImport::Bioromeo, + }.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, type:, **opts, &blk) + # @todo handle wrong or undetected type + parser = file_formats[type] + if block_given? + parser.parse(file, **opts, &blk) + else + data = [] + parser.parse(file, **opts) { |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[:number] = ':' + Digest::SHA1.hexdigest(s)[-7..-1] + 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, filename: nil, encoding: nil, col_sep: nil) + opts = {csv_options: {}} + opts[:csv_options][:encoding] = encoding if encoding + opts[:csv_options][:col_sep] = col_sep if col_sep + opts[:extension] = File.extname(filename) if filename + Roo::Spreadsheet.open(file, **opts) + end +end diff --git a/app/lib/article_import/bioromeo.rb b/app/lib/article_import/bioromeo.rb new file mode 100644 index 0000000..8a3c1d8 --- /dev/null +++ b/app/lib/article_import/bioromeo.rb @@ -0,0 +1,185 @@ +# Module for import of BioRomeo products from their Excel sheet, from Aug 2014 onwards + +require 'roo' +require 'roo-xls' + +module ArticleImport::Bioromeo + + NAME = "BioRomeo (XLSX, XLS, CSV)" + OUTLIST = true + OPTIONS = { + encoding: "UTF-8", + col_sep: ";" + }.freeze + + RE_UNITS = /(kg|gr|gram|pond|st|stuks?|set|bos|bossen|bosjes?|bak|bakjes?|liter|ltr|[lL]\.|ml|bol|krop)(\s*\.)?/i + RES_PARSE_UNIT_LIST = [ + /\b((per|a)\s*)?([0-9,.]+\s*x\s*[0-9,.]+\s*#{RE_UNITS})/i, # 1x5 kg + /\b((per|a)\s*)?([0-9,.]+\s*#{RE_UNITS}\s+x\s*[0-9,.]+)/i, # 1kg x 5 + /\b((per|a)\s*)?(([0-9,.]+\s*,\s+)*[0-9,.]+\s+of\s+[0-9,.]+\s*#{RE_UNITS})/i, # 1, 2 of 5 kg + /\b((per|a)\s*)?([0-9,.]+\s*#{RE_UNITS})/i, # 1kg + /\b((per|a)\s*)?(#{RE_UNITS})/i # kg + ] + # first parse with dash separator at the end, fallback to less specific + RES_PARSE_UNIT = RES_PARSE_UNIT_LIST.map {|r| /-\s*#{r}\s*$/} + + RES_PARSE_UNIT_LIST.map {|r| /-\s+#{r}/} + + RES_PARSE_UNIT_LIST.map {|r| /#{r}\s*$/} + + RES_PARSE_UNIT_LIST.map {|r| /-#{r}/} + + def self.parse(file, **opts) + opts = OPTIONS.merge(opts) + ss = ArticleImport.open_spreadsheet(file, **opts) + + header_row = true + sheet = ss.sheet(0).parse(clean: true, + number: /^artnr/i, + name: /^product/i, + skal: /^skal$/i, + demeter: /^demeter$/i, + unit_price: /prijs\b.*\beenh/i, + pack_price: /prijs\b.*\bcolli/i, + comment: /^opm(erking)?/i, + ) + + linenum = 0 + category = nil + + sheet.each do |row| + puts("[ROW] #{row.inspect}") + linenum += 1 + row[:name].blank? and next + # (sub)categories are in first two content cells - assume if there's a price it's a product + if row[:number].blank? && row[:unit_price].blank? + category = row[:name] + next + end + # skip products without a number + row[:number].blank? and next + # extract name and unit + errors = [] + notes = [] + unit_price = row[:unit_price] + pack_price = row[:pack_price] + number = row[:number] + name = row[:name] + unit = nil + manufacturer = nil + prod_category = nil + RES_PARSE_UNIT.each do |re| + m=name.match(re) or next + unit = self.normalize_unit(m[3]) + name = name.sub(re, '').sub(/\(\s*\)\s*$/,'').sub(/\s+/, ' ').sub(/\.\s*$/, '').strip + break + end + unit ||= '1 st' if name.match(/\bsla\b/i) + unit ||= '1 bos' if name.match(/\bradijs\b/i) + unit ||= '1 bosje' if category.match(/\bkruid/i) + if unit.nil? + unit = '?' + errors << "Cannot find unit in name '#{name}'" + end + # handle multiple units in one line + if unit.match(/\b(,\s+|of)\b/) + # TODO create multiple articles instead of taking first one + end + # sometimes category is also used to indicate manufacturer + m=category.match(/((eko\s*)?boerderij.*?)\s*$/i) and manufacturer = m[1] + # Ad-hoc fix for package of eggs: always take pack price + if name.match(/^eieren/i) + unit_price = pack_price + prod_category = 'Eieren' + end + prod_category = 'Kaas' if name.match(/^kaas/i) + # figure out unit_quantity + if unit.match(/x/) + unit_quantity, unit = unit.split(/\s*x\s*/i, 2) + unit,unit_quantity = unit_quantity,unit if unit_quantity.match(/[a-z]/i) + elsif (unit_price-pack_price).abs < 1e-3 + unit_quantity = 1 + elsif m=unit.match(/^(.*)\b\s*(st|bos|bossen|bosjes?)\.?\s*$/i) + unit_quantity, unit = m[1..2] + unit_quantity.blank? and unit_quantity = 1 + else + unit_quantity = 1 + end + # there may be a more informative unit in the line + if unit=='st' && !name.match(/kool/i) + RES_PARSE_UNIT.each do |re| + m=name.match(re) or next + unit = self.normalize_unit(m[3]) + name = name.sub(re, '').strip + end + end + # note from various fields + notes.append("Skal #{row[:skal]}") if row[:skal].present? + notes.append(row[:demeter]) if row[:demeter].present? && row[:demeter].is_a?(String) + notes.append("Demeter #{row[:demeter]}") if row[:demeter].present? && row[:demeter].is_a?(Fixnum) + notes.append "(#{row[:comment]})" unless row[:comment].blank? + name.sub!(/(,\.?\s*)?\bDemeter\b/i, '') and notes.prepend("Demeter") + name.sub!(/(,\.?\s*)?\bBIO\b/i, '') and notes.prepend "BIO" + # unit check + errors << check_price(unit, unit_quantity, unit_price, pack_price) + # create new article + name.gsub!(/\s+/, ' ') + article = {:number => number, + :name => name.strip, + :note => notes.count > 0 && notes.map(&:strip).join("; "), + :manufacturer => manufacturer, + :origin => 'Noordoostpolder, NL', + :unit => unit, + :price => pack_price.to_f/unit_quantity.to_f, + :unit_quantity => unit_quantity, + :tax => 6, + :deposit => 0, + :category => prod_category || category + } + errors.compact! + if errors.count > 0 + yield article, errors.join("\n") + else + # outlisting not used by supplier + yield article, nil + end + end + end + + protected + + def self.check_price(unit, unit_quantity, unit_price, pack_price) + if (unit_price-pack_price).abs < 1e-3 + return if unit_quantity == 1 + return "price per unit #{unit_price} is pack price, but unit quantity #{unit_quantity} is not one" + end + + if m = unit.match(/^(.*)(#{RE_UNITS})\s*$/) + amount, what = m[1..2] + else + return "could not parse unit: #{unit}" + end + + # perhaps unit price is kg-price + kgprice = if what =~ /^kg/i + pack_price.to_f / amount.to_f + elsif what =~ /^gr/ + pack_price.to_f / amount.to_f * 1000 + end + if kgprice.present? && (kgprice - unit_price.to_f).abs < 1e-2 + return + end + + unit_price_computed = pack_price.to_f/unit_quantity.to_i + if (unit_price_computed - unit_price.to_f).abs > 1e-2 + "price per unit given #{unit_price.round(3)} does not match computed " + + "#{pack_price.round(3)}/#{unit_quantity}=#{unit_price_computed.round(3)}" + + (kgprice ? " (nor is it a kg-price #{kgprice.round(3)})" : '') + end + end + + def self.normalize_unit(unit) + unit = unit.sub(/1\s*x\s*/, '') + unit = unit.sub(/,([0-9])/, '.\1').gsub(/^per\s*/,'').sub(/^1\s*([^0-9.])/,'\1').sub(/^a\b\s*/,'') + unit = unit.sub(/(bossen|bosjes?)/, 'bos').sub(/(liter|l\.|L\.)/,'ltr').sub(/stuks?/, 'st').sub('gram','gr') + unit = unit.sub(/\s*\.\s*$/,'').sub(/\s+/, ' ').strip + end + +end diff --git a/app/lib/article_import/bnn.rb b/app/lib/article_import/bnn.rb new file mode 100644 index 0000000..89243e8 --- /dev/null +++ b/app/lib/article_import/bnn.rb @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- +require 'csv' +require 'yaml' + +# Module for translation and parsing of BNN-files (www.n-bnn.de) +# +module ArticleImport::Bnn + + private + @@codes = Hash.new + @@midgard = Hash.new + + # Loads the codes_file config/bnn_codes.yml into the class variable @@codes + def self.load_codes + dir = Rails.root.join("app", "lib", "article_import") + begin + @@codes = YAML::load(File.open(dir.join("bnn_codes.yml"))).symbolize_keys + @@midgard = YAML::load(File.open(dir.join("midgard_codes.yml"))).symbolize_keys + rescue => e + raise "Failed to load bnn_codes: #{dir}/{bnn,midgard}_codes.yml: #{e.message}" + end + end + + public + $missing_bnn_codes = Array.new + + # translates codes from BNN to foodsoft-code + def self.translate(key, value) + if @@codes[key][value] + return @@codes[key][value] + elsif @@midgard[key] + return @@midgard[key][value] + elsif value != nil + $missing_bnn_codes << value + return nil + end + end + + NAME = "BNN (CSV)" + OUTLIST = false + OPTIONS = { + encoding: "IBM850", + col_sep: ";" + }.freeze + + # parses a bnn-file + def self.parse(file, **opts) + encoding = opts[:encoding] || OPTIONS[:encoding] + col_sep = opts[:col_sep] || OPTIONS[:col_sep] + CSV.foreach(file, {col_sep: col_sep, encoding: encoding, headers: true}) do |row| + # check if the line is empty + unless row[0] == "" || row[0].nil? + article = { + :name => row[6], + :number => row[0], + :note => row[7], + :manufacturer => self.translate(:manufacturer, row[10]), + :origin => row[12], + :category => self.translate(:category, row[16]), + :unit => row[23], + :price => row[37], + :tax => self.translate(:tax, row[33]), + :unit_quantity => row[22] + } + # TODO: Complete deposit list.... + article.merge!(:deposit => self.translate(:deposit, row[26])) if self.translate(:deposit, row[26]) + + # get scale prices if exists + article.merge!(:scale_quantity => row[40], :scale_price => row[41]) unless row[40].nil? or row[41].nil? + + if row[62] != nil + # consider special prices + article[:note] = "Sonderpreis: #{article[:price]} von #{row[62]} bis #{row[63]}" + yield article, :special + + # 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 + else + yield article, nil + end + end + end + end +end + +# Automatically load codes: +ArticleImport::Bnn.load_codes diff --git a/app/lib/article_import/bnn_codes.yml b/app/lib/article_import/bnn_codes.yml new file mode 100644 index 0000000..600e2d0 --- /dev/null +++ b/app/lib/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 diff --git a/app/lib/article_import/borkenstein.rb b/app/lib/article_import/borkenstein.rb new file mode 100644 index 0000000..da9b6cd --- /dev/null +++ b/app/lib/article_import/borkenstein.rb @@ -0,0 +1,94 @@ +# -*- coding: utf-8 -*- +# Module for Borkenstein csv import + +require 'csv' + +module ArticleImport::Borkenstein + + REGEX = { + :main => /^(.+)\s+\[([^\[\]]+)\]\s+(\d+\.\d+)\((\d+\.\d+)\)$/, + :manufacturer => /^(.+)\s{4}\[\]\s{4}\(\)$/, + :origin => /(.+)\s+(\w+)\/\w+[\/[\w\-]+]?/ + }.freeze + + NAME = "Borkenstein (CSV)" + OUTLIST = false + OPTIONS = { + col_sep: ",", + encoding: "UTF-8" # @todo check this + }.freeze + + def self.parse(file, **opts) + global_manufacturer = nil + + file.set_encoding(opts[:encoding] || OPTIONS[:encoding]) + col_sep = opts[:col_sep] || OPTIONS[:col_sep] + CSV.new(file, {col_sep: col_sep, :headers => false}).each do |row| + + # Set manufacturer + if row[1] == "-" + match = row[2].match(REGEX[:manufacturer]) + global_manufacturer = match.captures.first unless match.nil? + end + + # check if the line is empty + unless row[1].blank? || row[1] == "-" + + # Split string and remove beginning " + matched = row[2].gsub(/^\"/, "").gsub(/\"$/, "").match(REGEX[:main]) + + if matched.nil? + puts "No regular article data for #{row[1]}: #{row[2]}" + + else + + name, units, price_high, price_low = matched.captures + + # Try to get origin + matched_name = name.match(REGEX[:origin]) + if matched_name + name, origin = matched_name.captures + else + name, origin = name.gsub(/\s{2,}/, ""), nil + end + + # Manufacturer + if name.match(/^[A-Za-z]{2,3}\s{1}/) + name.gsub!(/^[A-Za-z]{2,3}\s{1}/, "") + manufacturer = global_manufacturer + end + + + # Get unit quantities + units = units.split("x") + if units.size == 2 + unit_quantity = units.first + unit = units.last + else + unit_quantity = 1 + unit = units.first + end + + article = { + :number => row[1], + :name => name, + :origin => origin, + :manufacturer => manufacturer, + :unit_quantity => unit_quantity, + :unit => unit, + :price => price_low, # Inklusive Rabattstufe von 10% + :tax => 0.0 # Tax is included + } + + # test, if neccecary attributes exists + if article[:unit].nil? || article[:price].nil? || article[:unit_quantity].nil? + raise "Fehler: Einheit, Preis und MwSt. müssen gegeben sein: #{article.inspect}" + end + + yield article, nil + end + end + end + end + +end diff --git a/app/lib/article_import/dnb_codes.yml b/app/lib/article_import/dnb_codes.yml new file mode 100644 index 0000000..f79a6fc --- /dev/null +++ b/app/lib/article_import/dnb_codes.yml @@ -0,0 +1,130 @@ + +# 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/app/lib/article_import/dnb_xml.rb b/app/lib/article_import/dnb_xml.rb new file mode 100644 index 0000000..d264fd0 --- /dev/null +++ b/app/lib/article_import/dnb_xml.rb @@ -0,0 +1,73 @@ +# 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 ArticleImport::DnbXml + + NAME = "De Nieuwe Band (XML)" + OUTLIST = true + OPTIONS = {}.freeze + + # parses a string or file + def self.parse(file, opts={}) + doc = Nokogiri.XML(file, nil, nil, + Nokogiri::XML::ParseOptions::RECOVER + + Nokogiri::XML::ParseOptions::NONET + + Nokogiri::XML::ParseOptions::COMPACT # do not modify doc! + ) + doc.search('product').each do |row| + # create a new article + unit = row.search('eenheid').text + unit = case(unit) + when blank? 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.blank? or (inhoud.to_f-1).abs > 1e-3 and unit = inhoud.gsub(/\.0+\s*$/,'') + unit + deposit = row.search('statiegeld').text + deposit.blank? and deposit = 0 + category = [ + @@codes[:indeling][row.search('indeling').text.to_i], + @@codes[:indeling][row.search('subindeling').text.to_i] + ].compact.join(' - ') + + article = {: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, + :category => category} + + yield article, (row.search('status') == 'Actief' ? :outlisted : nil) + end + end + + private + + @@codes = Hash.new + + def self.load_codes + dir = Rails.root.join("lib", "article_import") + begin + @@codes = YAML::load(File.open(dir.join("dnb_codes.yml"))).symbolize_keys + rescue => e + raise "Failed to load dnb_codes: #{dir}/dnb_codes.yml: #{e.message}" + end + end + +end + +ArticleImport::DnbXml.load_codes diff --git a/app/lib/article_import/foodsoft.rb b/app/lib/article_import/foodsoft.rb new file mode 100644 index 0000000..314ac54 --- /dev/null +++ b/app/lib/article_import/foodsoft.rb @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +# 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 ArticleImport::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, **opts) + opts = OPTIONS.merge(opts) + ss = ArticleImport.open_spreadsheet(file, **opts) + + header_row = true + ss.sheet(0).each do |row| + # skip first header row + if header_row + header_row = false + next + end + # skip empty lines + next if row[2].blank? + + article = {: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], + :scale_quantity => row[11], + :scale_price => row[12], + :category => row[13]} + article.merge!(:deposit => row[9]) unless row[9].nil? + article[:number].blank? and ArticleImport.generate_number(article) + if row[6].nil? || row[7].nil? or row[8].nil? + yield article, "Error: unit, price and tax must be entered" + else + yield article, (row[0]=='x' ? :outlisted : nil) + end + end + end + +end diff --git a/app/lib/article_import/midgard_codes.yml b/app/lib/article_import/midgard_codes.yml new file mode 100644 index 0000000..5b22f0e --- /dev/null +++ b/app/lib/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 diff --git a/app/lib/ftp_sync.rb b/app/lib/ftp_sync.rb new file mode 100644 index 0000000..e47dc63 --- /dev/null +++ b/app/lib/ftp_sync.rb @@ -0,0 +1,36 @@ +require 'net/ftp' +require 'fileutils' + +module FtpSync + + # compares remote with local filelist + # if local file not exists or older than remote file, download remote file + # return array with new files + def self.sync(supplier) + new_files = Array.new + + # change local dir to save files correctly + FileUtils.mkdir_p(supplier.ftp_path) unless File.exists?(supplier.ftp_path) + Dir.chdir(supplier.ftp_path) + + # connect to ftp-server + ftp = Net::FTP.new(supplier.ftp_host, supplier.ftp_user, supplier.ftp_password) + + # loop over the remote filelist + ftp.nlst.each do |filename| + if filename.match(Regexp.new(supplier.ftp_regexp)) + # local file not exist or remote file newer ? + if (File.exist?(filename) and File.new(filename).mtime < ftp.mtime(filename)) or !File.exist?(filename) + # download + ftp.getbinaryfile(filename) + # save filename for return + new_files << filename + end + end + end + # close ftp-session + ftp.close + return new_files + end + +end