From 7e5ca2ec513fca3248189e9082347336b957a6ab Mon Sep 17 00:00:00 2001 From: viehlieb Date: Thu, 30 Mar 2023 14:55:24 +0200 Subject: [PATCH] finish plugin for bnn, foodsoft, odin uploads --- .../articles_controller_override.rb | 32 +++--- .../app/overrides/models/article_override.rb | 70 ++++++------ .../app/overrides/models/supplier_override.rb | 88 +++++++-------- .../foodsoft_article_import.gemspec | 2 +- plugins/article_import/spec/app_config.yml | 34 ++++++ .../spec/integration/articles_spec.rb | 21 +--- .../spec/integration/supplier_spec.rb | 21 ++++ plugins/article_import/spec/spec_helper.rb | 100 ------------------ plugins/article_import/spec/test_helper.rb | 9 ++ 9 files changed, 167 insertions(+), 210 deletions(-) create mode 100644 plugins/article_import/spec/app_config.yml create mode 100644 plugins/article_import/spec/integration/supplier_spec.rb delete mode 100644 plugins/article_import/spec/spec_helper.rb create mode 100644 plugins/article_import/spec/test_helper.rb diff --git a/plugins/article_import/app/overrides/controllers/articles_controller_override.rb b/plugins/article_import/app/overrides/controllers/articles_controller_override.rb index 43e9773b..7777064e 100644 --- a/plugins/article_import/app/overrides/controllers/articles_controller_override.rb +++ b/plugins/article_import/app/overrides/controllers/articles_controller_override.rb @@ -1,18 +1,20 @@ -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') +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') + @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 - @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 index 2018a4e7..5ca177ad 100644 --- a/plugins/article_import/app/overrides/models/article_override.rb +++ b/plugins/article_import/app/overrides/models/article_override.rb @@ -1,37 +1,39 @@ -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 +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 + 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) + Article.compare_attributes(attribute_hash) + end 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 index 504bc26d..62885aac 100644 --- a/plugins/article_import/app/overrides/models/supplier_override.rb +++ b/plugins/article_import/app/overrides/models/supplier_override.rb @@ -1,51 +1,53 @@ -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 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 + 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 - 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 + new_attrs[:article_category] = nil 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) + 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 - - all_order_numbers << article.order_number if article + 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 - 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 diff --git a/plugins/article_import/foodsoft_article_import.gemspec b/plugins/article_import/foodsoft_article_import.gemspec index 0d2d5842..b030005f 100644 --- a/plugins/article_import/foodsoft_article_import.gemspec +++ b/plugins/article_import/foodsoft_article_import.gemspec @@ -11,7 +11,7 @@ Gem::Specification.new do |s| 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,spec}/**/*"] + ["Rakefile", "README.md"] + s.files = Dir["{app,config,db,lib}/**/*"] + ["Rakefile", "README.md"] s.add_dependency "rails" s.add_dependency "deface", "~> 1.0" diff --git a/plugins/article_import/spec/app_config.yml b/plugins/article_import/spec/app_config.yml new file mode 100644 index 00000000..e31af571 --- /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/integration/articles_spec.rb b/plugins/article_import/spec/integration/articles_spec.rb index d70fcae6..0a547515 100644 --- a/plugins/article_import/spec/integration/articles_spec.rb +++ b/plugins/article_import/spec/integration/articles_spec.rb @@ -1,28 +1,15 @@ +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 do - FoodsoftConfig[:use_article_import] = true - login user - end - after do - FoodsoftConfig[:use_article_import] = false - end + + before { login user } describe ':index', js: true do - puts " - " + "______________" + " - " + "______________" + " - " + "______________" + " - " + "Plugin" + " - " + "______________"+ " - " + "______________"+ " - " + "______________" before do login user visit supplier_articles_path(supplier_id: supplier.id) 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 00000000..d80c0d52 --- /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/spec_helper.rb b/plugins/article_import/spec/spec_helper.rb deleted file mode 100644 index d3e43d53..00000000 --- a/plugins/article_import/spec/spec_helper.rb +++ /dev/null @@ -1,100 +0,0 @@ -# frozen_string_literal: true - -require 'simplecov' -SimpleCov.start -# This file was generated by the `rspec --init` command. Conventionally, all -# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. -# The generated `.rspec` file contains `--require spec_helper` which will cause -# this file to always be loaded, without a need to explicitly require it in any -# files. -# -# Given that it is always loaded, you are encouraged to keep this file as -# light-weight as possible. Requiring heavyweight dependencies from this file -# will add to the boot time of your test suite on EVERY test run, even for an -# individual file that may not need all of that loaded. Instead, consider making -# a separate helper file that requires the additional dependencies and performs -# the additional setup, and require it from the spec files that actually need -# it. -# -# See https://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration -RSpec.configure do |config| - # rspec-expectations config goes here. You can use an alternate - # assertion/expectation library such as wrong or the stdlib/minitest - # assertions if you prefer. - config.expect_with :rspec do |expectations| - # This option will default to `true` in RSpec 4. It makes the `description` - # and `failure_message` of custom matchers include text for helper methods - # defined using `chain`, e.g.: - # be_bigger_than(2).and_smaller_than(4).description - # # => "be bigger than 2 and smaller than 4" - # ...rather than: - # # => "be bigger than 2" - expectations.include_chain_clauses_in_custom_matcher_descriptions = true - end - - # rspec-mocks config goes here. You can use an alternate test double - # library (such as bogus or mocha) by changing the `mock_with` option here. - config.mock_with :rspec do |mocks| - # Prevents you from mocking or stubbing a method that does not exist on - # a real object. This is generally recommended, and will default to - # `true` in RSpec 4. - mocks.verify_partial_doubles = true - end - - # This option will default to `:apply_to_host_groups` in RSpec 4 (and will - # have no way to turn it off -- the option exists only for backwards - # compatibility in RSpec 3). It causes shared context metadata to be - # inherited by the metadata hash of host groups and examples, rather than - # triggering implicit auto-inclusion in groups with matching metadata. - config.shared_context_metadata_behavior = :apply_to_host_groups - - # The settings below are suggested to provide a good initial experience - # with RSpec, but feel free to customize to your heart's content. - # # This allows you to limit a spec run to individual examples or groups - # # you care about by tagging them with `:focus` metadata. When nothing - # # is tagged with `:focus`, all examples get run. RSpec also provides - # # aliases for `it`, `describe`, and `context` that include `:focus` - # # metadata: `fit`, `fdescribe` and `fcontext`, respectively. - # config.filter_run_when_matching :focus - # - # # Allows RSpec to persist some state between runs in order to support - # # the `--only-failures` and `--next-failure` CLI options. We recommend - # # you configure your source control system to ignore this file. - # config.example_status_persistence_file_path = "spec/examples.txt" - # - # # Limits the available syntax to the non-monkey patched syntax that is - # # recommended. For more details, see: - # # https://relishapp.com/rspec/rspec-core/docs/configuration/zero-monkey-patching-mode - # config.disable_monkey_patching! - # - # # This setting enables warnings. It's recommended, but in some cases may - # # be too noisy due to issues in dependencies. - # config.warnings = true - # - # # Many RSpec users commonly either run the entire suite or an individual - # # file, and it's useful to allow more verbose output when running an - # # individual spec file. - # if config.files_to_run.one? - # # Use the documentation formatter for detailed output, - # # unless a formatter has already been configured - # # (e.g. via a command-line flag). - # config.default_formatter = "doc" - # end - # - # # Print the 10 slowest examples and example groups at the - # # end of the spec run, to help surface which specs are running - # # particularly slow. - # config.profile_examples = 10 - # - # # Run specs in random order to surface order dependencies. If you find an - # # order dependency and want to debug it, you can fix the order by providing - # # the seed, which is printed after each run. - # # --seed 1234 - # config.order = :random - # - # # Seed global randomization in this process using the `--seed` CLI option. - # # Setting this allows you to use `--seed` to deterministically reproduce - # # test failures related to randomization by passing the same `--seed` value - # # as the one that triggered the failure. - # Kernel.srand config.seed -end diff --git a/plugins/article_import/spec/test_helper.rb b/plugins/article_import/spec/test_helper.rb new file mode 100644 index 00000000..527a16d8 --- /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