• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

foodcoops / foodsoft / 16526107939

25 Jul 2025 03:49PM UTC coverage: 42.064% (-24.5%) from 66.58%
16526107939

push

github

robwa
feat(order): add tests for custom remote order method and FTP upload

Introduce tests for registered custom remote order methods.
Verify FTP upload functionality and integration with the new system.

2971 of 7063 relevant lines covered (42.06%)

12.1 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

14.29
/app/controllers/articles_controller.rb
1
class ArticlesController < ApplicationController
1✔
2
  before_action :authenticate_article_meta, :find_supplier
1✔
3

4
  before_action :load_article, only: %i[edit update]
1✔
5
  before_action :load_article_units, only: %i[edit update new create]
1✔
6
  before_action :load_article_categories, only: %i[edit new edit_all copy migrate_units update_all]
1✔
7
  before_action :new_empty_article_ratio,
1✔
8
                only: %i[edit edit_all migrate_units update new create parse_upload sync update_synchronized]
9

10
  def index
1✔
11
    sort = if params['sort']
×
12
             case params['sort']
×
13
             when 'name' then 'article_versions.name'
×
14
             when 'unit' then 'article_versions.unit'
×
15
             when 'article_category' then 'article_categories.name'
×
16
             when 'note' then 'article_versions.note'
×
17
             when 'availability' then 'article_versions.availability'
×
18
             when 'name_reverse' then 'article_versions.name DESC'
×
19
             when 'unit_reverse' then 'article_versions.unit DESC'
×
20
             when 'article_category_reverse' then 'article_categories.name DESC'
×
21
             when 'note_reverse' then 'article_versions.note DESC'
×
22
             when 'availability_reverse' then 'article_versions.availability DESC'
×
23
             end
24
           else
25
             'article_categories.name, article_versions.name'
×
26
           end
27

28
    @articles = Article.with_latest_versions_and_categories.order(sort).undeleted.where(supplier_id: @supplier,
×
29
                                                                                        type: nil)
30

31
    if request.format.csv?
×
32
      send_data ArticlesCsv.new(@articles, encoding: 'utf-8', foodsoft_url: root_url).to_csv, filename: 'articles.csv', type: 'text/csv'
×
33
      return
×
34
    end
35

36
    @articles = @articles.where('article_versions.name LIKE ?', "%#{params[:query]}%") unless params[:query].nil?
×
37

38
    @articles = @articles.page(params[:page]).per(@per_page)
×
39

40
    respond_to do |format|
×
41
      format.html
×
42
      format.js { render layout: false }
×
43
    end
44
  end
45

46
  def new
1✔
47
    @article = @supplier.articles.build
×
48
    @article.latest_article_version = @article.article_versions.build(tax: FoodsoftConfig[:tax_default])
×
49
    render layout: false
×
50
  end
51

52
  def copy
1✔
53
    article = @supplier.articles.find(params[:article_id])
×
54
    @article = article.duplicate_including_latest_version_and_ratios
×
55
    load_article_units(@article.current_article_units)
×
56
    render layout: false
×
57
  end
58

59
  def edit
1✔
60
    render action: 'new', layout: false
×
61
  end
62

63
  def create
1✔
64
    valid = false
×
65
    Article.transaction do
×
66
      @article = Article.create(supplier_id: @supplier.id)
×
67
      @article.attributes = { latest_article_version_attributes: params[:article_version] }
×
68
      raise ActiveRecord::Rollback unless @article.valid?
×
69

70
      valid = @article.save
×
71
    end
72

73
    if valid
×
74
      render layout: false
×
75
    else
76
      load_article_units(@article.current_article_units)
×
77
      render action: 'new', layout: false
×
78
    end
79
  end
80

81
  # Updates one Article and highlights the line if succeded
82
  def update
1✔
83
    Article.transaction do
×
84
      if @article.update(latest_article_version_attributes: params[:article_version])
×
85
        render layout: false
×
86
      else
87
        Rails.logger.info @article.errors.to_yaml.to_s
×
88
        render action: 'new', layout: false
×
89
      end
90
    end
91
  end
92

93
  # Deletes article from database. send error msg, if article is used in a current order
94
  def destroy
1✔
95
    @article = Article.find(params[:id])
×
96
    @article.mark_as_deleted unless @order = @article.in_open_order # If article is in an active Order, the Order will be returned
×
97
    render layout: false
×
98
  end
99

100
  # Renders a form for editing all articles from a supplier
101
  def edit_all
1✔
102
    @articles = @supplier.articles.undeleted
×
103

104
    load_article_units
×
105
  end
106

107
  def prepare_units_migration; end
1✔
108

109
  def migrate_units
1✔
110
    build_article_migration_samples
×
111
  end
112

113
  def complete_units_migration
1✔
114
    @invalid_articles = []
×
115
    @samples = []
×
116

117
    Article.transaction do
×
118
      params[:samples]&.values&.each do |sample|
×
119
        next unless sample[:apply_migration] == '1'
×
120

121
        original_unit = nil
×
122
        articles = Article.with_latest_versions_and_categories
×
123
                          .includes(latest_article_version: [:article_unit_ratios])
124
                          .find(sample[:article_ids])
125
        articles.each do |article|
×
126
          latest_article_version = article.latest_article_version
×
127
          original_unit = latest_article_version.unit
×
128
          next if latest_article_version.article_unit_ratios.length > 1 ||
×
129
                  latest_article_version.billing_unit != latest_article_version.group_order_unit ||
130
                  latest_article_version.price_unit != latest_article_version.group_order_unit
131

132
          article_version_params = sample.slice(:supplier_order_unit, :group_order_granularity, :group_order_unit)
×
133
          article_version_params[:unit] = nil
×
134
          article_version_params[:billing_unit] = article_version_params[:group_order_unit]
×
135
          article_version_params[:price_unit] = article_version_params[:group_order_unit]
×
136
          article_version_params[:article_unit_ratios_attributes] = {}
×
137
          if sample[:first_ratio_unit].present?
×
138
            article_version_params[:article_unit_ratios_attributes]['1'] = {
×
139
              id: latest_article_version.article_unit_ratios.first&.id,
140
              sort: 1,
141
              quantity: sample[:first_ratio_quantity],
142
              unit: sample[:first_ratio_unit]
143
            }
144
          end
145
          article_version_params[:id] = latest_article_version.id
×
146
          @invalid_articles << article unless article.update(latest_article_version_attributes: article_version_params)
×
147
        end
148

149
        errors = articles.find { |a| !a.errors.nil? }&.errors
×
150
        @samples << {
×
151
          unit: original_unit,
152
          conversion_result: sample
153
                    .except(:article_ids, :first_ratio_quantity, :first_ratio_unit)
154
                    .merge(
155
                      first_ratio: {
156
                        quantity: sample[:first_ratio_quantity],
157
                        unit: sample[:first_ratio_unit]
158
                      }
159
                    ),
160
          articles: articles,
161
          errors: errors,
162
          error: errors.present?
163
        }
164
      end
165
      @supplier.update_attribute(:unit_migration_completed, Time.now)
×
166
      raise ActiveRecord::Rollback unless @invalid_articles.empty?
×
167
    end
168

169
    if @invalid_articles.empty?
×
170
      redirect_to supplier_articles_path(@supplier),
×
171
                  notice: I18n.t('articles.controller.complete_units_migration.notice')
172
    else
173
      additional_units = @samples.map do |sample|
×
174
        [sample[:conversion_result][:supplier_order_unit], sample[:conversion_result][:group_order_unit],
×
175
         sample[:conversion_result][:first_ratio]&.dig(:unit)]
176
      end.flatten.uniq.compact
177
      load_article_units(additional_units)
×
178

179
      flash.now.alert = I18n.t('articles.controller.error_invalid')
×
180
      render :migrate_units
×
181
    end
182
  end
183

184
  # Updates all article of specific supplier
185
  def update_all
1✔
186
    invalid_articles = false
×
187

188
    Article.transaction do
×
189
      if params[:articles].present?
×
190
        # Update other article attributes...
191
        @articles = Article.with_latest_versions_and_categories
×
192
                           .includes(latest_article_version: [:article_unit_ratios])
193
                           .find(params[:articles].keys)
194
        @articles.each do |article|
×
195
          article_version_params = params[:articles][article.id.to_s]
×
196
          article_version_params['id'] = article.latest_article_version.id
×
197
          unless article.update(latest_article_version_attributes: article_version_params)
×
198
            invalid_articles ||= true # Remember that there are validation errors
×
199
          end
200
        end
201

202
        @supplier.update_attribute(:unit_migration_completed, Time.now) if params[:complete_migration]
×
203

204
        raise ActiveRecord::Rollback if invalid_articles # Rollback all changes
×
205
      end
206
    end
207

208
    if invalid_articles
×
209
      # An error has occurred, transaction has been rolled back.
210
      flash.now.alert = I18n.t('articles.controller.error_invalid')
×
211
      render :edit_all
×
212
    else
213
      # Successfully done.
214
      redirect_to supplier_articles_path(@supplier), notice: I18n.t('articles.controller.update_all.notice')
×
215
    end
216
  end
217

218
  # makes different actions on selected articles
219
  def update_selected
1✔
220
    raise I18n.t('articles.controller.error_nosel') if params[:selected_articles].nil?
×
221

222
    articles = Article.with_latest_versions_and_categories
×
223
                      .includes(latest_article_version: [:article_unit_ratios])
224
                      .find(params[:selected_articles])
225
    Article.transaction do
×
226
      case params[:selected_action]
×
227
      when 'destroy'
228
        articles.each(&:mark_as_deleted)
×
229
        flash[:notice] = I18n.t('articles.controller.update_sel.notice_destroy')
×
230
      when 'setNotAvailable'
231
        articles.each { |a| a.update_attribute(:availability, false) }
×
232
        flash[:notice] = I18n.t('articles.controller.update_sel.notice_unavail')
×
233
      when 'setAvailable'
234
        articles.each { |a| a.update_attribute(:availability, true) }
×
235
        flash[:notice] = I18n.t('articles.controller.update_sel.notice_avail')
×
236
      else
237
        flash[:alert] = I18n.t('articles.controller.update_sel.notice_noaction')
×
238
      end
239
    end
240
    # action succeded
241
    redirect_to supplier_articles_url(@supplier, per_page: params[:per_page])
×
242
  rescue StandardError => e
243
    redirect_to supplier_articles_url(@supplier, per_page: params[:per_page]),
×
244
                alert: I18n.t('errors.general_msg', msg: e)
245
  end
246

247
  # lets start with parsing articles from uploaded file, yeah
248
  # Renders the upload form
249
  def upload; end
1✔
250

251
  # Update articles from a spreadsheet
252
  def parse_upload
1✔
253
    uploaded_file = params[:articles]['file'] or raise I18n.t('articles.controller.parse_upload.no_file')
×
254
    options = { filename: uploaded_file.original_filename, foodsoft_url: root_url }
×
255
    options[:delete_unavailable] = (params[:articles]['delete_unavailable'] == '1')
×
256
    options[:outlist_absent] = (params[:articles]['outlist_absent'] == '1')
×
257
    options[:convert_units] = (params[:articles]['convert_units'] == '1')
×
258
    @enable_unit_migration = (params[:articles]['activate_unit_migration'] == '1')
×
259
    @updated_article_pairs, @outlisted_articles, @new_articles, import_data = @supplier.sync_from_file(uploaded_file.tempfile,
×
260
                                                                                                       options)
261

262
    @articles = @updated_article_pairs.pluck(0) + @new_articles
×
263
    load_article_units
×
264

265
    if @updated_article_pairs.empty? && @outlisted_articles.empty? && @new_articles.empty?
×
266
      redirect_to supplier_articles_path(@supplier),
×
267
                  notice: I18n.t('articles.controller.parse_upload.notice', count: import_data.length)
268
    end
269
    @ignored_article_count = 0
×
270
  rescue StandardError => e
271
    redirect_to upload_supplier_articles_path(@supplier), alert: I18n.t('errors.general_msg', msg: e.message)
×
272
  end
273

274
  # sync all articles with the external database
275
  # renders a form with articles, which should be updated
276
  def sync
1✔
277
    @updated_article_pairs, @outlisted_articles, @new_articles, import_data = @supplier.sync_from_remote
×
278
    redirect_to(supplier_articles_path(@supplier), notice: I18n.t('articles.controller.parse_upload.notice', count: import_data[:articles].length)) if @updated_article_pairs.empty? && @outlisted_articles.empty? && @new_articles.empty?
×
279
    @ignored_article_count = 0
×
280
    load_article_units((@new_articles + @updated_article_pairs.map(&:first)).map(&:current_article_units).flatten.uniq)
×
281
  rescue StandardError => e
282
    redirect_to upload_supplier_articles_path(@supplier), alert: I18n.t('errors.general_msg', msg: e.message)
×
283
  end
284

285
  # Updates, deletes articles when upload or sync form is submitted
286
  def update_synchronized
1✔
287
    @enable_unit_migration = (params[:enable_unit_migration] == '1')
×
288
    @outlisted_articles = Article.includes(:latest_article_version).where(article_versions: { id: params[:outlisted_articles]&.values || [] })
×
289
    @updated_articles = Article.includes(:latest_article_version).where(article_versions: { id: params[:articles]&.values&.map do |v|
×
290
                                                                                                  v[:id]
×
291
                                                                                                end || [] })
292
    @new_articles = build_articles_from_params_array(params[:new_articles]&.values || [])
×
293

294
    # Prepare updated articles with their parameters
295
    updated_article_params = []
×
296
    @updated_articles.each do |a|
×
297
      current_params = params[:articles].values.detect { |p| p[:id] == a.latest_article_version.id.to_s }
×
298
      current_params.delete(:id)
×
299
      updated_article_params << [a, current_params]
×
300
    end
301

302
    # Use the SupplierSyncService to persist changes
303
    service = SupplierSyncService.new(@supplier)
×
304
    success = service.persist(@new_articles, @outlisted_articles, updated_article_params, enable_unit_migration: @enable_unit_migration)
×
305

306
    if success
×
307
      redirect_to supplier_articles_path(@supplier), notice: I18n.t('articles.controller.update_sync.notice')
×
308
    else
309
      load_article_units((@new_articles + @updated_articles).map(&:current_article_units).flatten.uniq)
×
310
      @updated_article_pairs = @updated_articles.map do |article|
×
311
        orig_article = Article.find(article.id)
×
312
        [article, orig_article.unequal_attributes(article)]
×
313
      end
314
      flash.now.alert = I18n.t('articles.controller.error_invalid')
×
315
      render params[:from_action] == 'sync' ? :sync : :parse_upload
×
316
    end
317
  end
318

319
  private
1✔
320

321
  def build_articles_from_params_array(params)
1✔
322
    params.map do |a|
×
323
      article = @supplier.articles.build
×
324
      article_version = article.article_versions.build(a)
×
325
      article.article_versions << article_version
×
326
      article.latest_article_version = article_version
×
327
      article_version.article = article
×
328
      article
×
329
    end
330
  end
331

332
  def build_article_migration_samples
1✔
333
    articles = @supplier.articles.with_latest_versions_and_categories.undeleted.includes(latest_article_version: [:article_unit_ratios])
×
334
    samples_hash = {}
×
335
    articles.each do |article|
×
336
      article_version = article.latest_article_version
×
337
      quantity = 1
×
338
      ratios = article_version.article_unit_ratios
×
339

340
      next if ratios.length > 1 ||
×
341
              article_version.billing_unit != article_version.group_order_unit ||
342
              article_version.price_unit != article_version.group_order_unit
343

344
      quantity = ratios[0].quantity if ratios.length == 1 && ratios[0].quantity != 1 && ratios[0].unit == 'XPP'
×
345

346
      samples_hash[article_version.unit] = {} if samples_hash[article_version.unit].nil?
×
347
      samples_hash[article_version.unit][quantity] = [] if samples_hash[article_version.unit][quantity].nil?
×
348
      samples_hash[article_version.unit][quantity] << article
×
349
    end
350
    @samples = samples_hash.map do |unit, quantities_hash|
×
351
      quantities_hash.map do |quantity, sample_articles|
×
352
        conversion_result = ArticleUnitsLib.convert_old_unit(unit, quantity)
×
353
        { unit: unit, quantity: quantity, articles: sample_articles, conversion_result: conversion_result }
×
354
      end
355
    end
356
    @samples = @samples.flatten
×
357
                       .reject { |sample| sample[:conversion_result].nil? }
×
358

359
    additional_units = @samples.map do |sample|
×
360
      [sample[:conversion_result][:supplier_order_unit], sample[:conversion_result][:group_order_unit],
×
361
       sample[:conversion_result][:first_ratio]&.dig(:unit)]
362
    end.flatten.uniq.compact
363
    load_article_units(additional_units)
×
364
  end
365

366
  def load_article
1✔
367
    @article = Article
×
368
               .with_latest_versions_and_categories
369
               .includes(latest_article_version: [:article_unit_ratios])
370
               .find(params[:id])
371
  end
372

373
  def load_article_units(additional_units = [])
1✔
374
    additional_units = if !@article.nil?
×
375
                         @article.current_article_units
×
376
                       elsif !@articles.nil?
×
377
                         @articles.map(&:current_article_units)
×
378
                                  .flatten
379
                                  .uniq
380
                       else
381
                         additional_units
×
382
                       end
383

384
    @article_units = ArticleUnit.as_options(additional_units: additional_units)
×
385
    @all_units = ArticleUnit.as_hash(additional_units: additional_units)
×
386
  end
387

388
  def load_article_categories
1✔
389
    @article_categories = ArticleCategory.undeleted.order(:name)
×
390

391
    current_category_id = @article&.latest_article_version&.article_category_id
×
392
    @article_categories = @article_categories.or(ArticleCategory.where(id: current_category_id)) unless current_category_id.nil?
×
393
  end
394

395
  def new_empty_article_ratio
1✔
396
    @empty_article_unit_ratio = ArticleUnitRatio.new
×
397
    @empty_article_unit_ratio.article_version = @article.latest_article_version unless @article.nil?
×
398
    @empty_article_unit_ratio.sort = -1
×
399
  end
400

401
  # @return [Number] Number of articles not taken into account when syncing (having no number)
402
  def ignored_article_count
1✔
403
    if action_name == 'sync' || params[:from_action] == 'sync'
×
404
      @ignored_article_count ||= @supplier.articles.includes(:latest_article_version).undeleted.where(article_versions: { order_number: [
×
405
                                                                                                        nil, ''
406
                                                                                                      ] }).count
407
    else
408
      0
×
409
    end
410
  end
411
  helper_method :ignored_article_count
1✔
412
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc