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

foodcoops / foodsoft / 19213239496

09 Nov 2025 07:18PM UTC coverage: 50.014% (-17.1%) from 67.092%
19213239496

push

github

web-flow
doc: Fix URL for CI/CD deployment in README

3605 of 7208 relevant lines covered (50.01%)

15.23 hits per line

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

33.62
/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']
1✔
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'
1✔
26
           end
27

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

31
    if request.format.csv?
1✔
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?
1✔
37

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

40
    respond_to do |format|
1✔
41
      format.html
1✔
42
      format.js { render layout: false }
1✔
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_categories
×
77
      load_article_units(@article.current_article_units)
×
78
      render action: 'new', layout: false
×
79
    end
80
  end
81

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

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

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

105
    load_article_units
×
106
  end
107

108
  def prepare_units_migration; end
1✔
109

110
  def migrate_units
1✔
111
    build_article_migration_samples
×
112
  end
113

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

263
    @articles = @updated_article_pairs.pluck(0) + @new_articles
1✔
264
    load_article_units
1✔
265

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

275
  # sync all articles with the external database
276
  # renders a form with articles, which should be updated
277
  def sync
1✔
278
    search_params = {}
×
279

280
    if @supplier.shared_sync_method == 'import'
×
281
      order_numbers = Article.with_latest_versions_and_categories.undeleted.where(supplier_id: @supplier, type: nil).where.not(article_versions: { order_number: nil }).map(&:latest_article_version).map(&:order_number)
×
282
      redirect_to(supplier_articles_path(@supplier), notice: I18n.t('articles.controller.parse_upload.notice', count: 0)) if order_numbers.empty?
×
283
      search_params['order_numbers[]'] = order_numbers
×
284
    elsif !FoodsoftConfig[:shared_supplier_article_sync_limit].nil?
×
285
      search_params[:page] = 1
×
286
      search_params[:per_page] = FoodsoftConfig[:shared_supplier_article_sync_limit]
×
287
    end
288

289
    @updated_article_pairs, @outlisted_articles, @new_articles, import_data = @supplier.sync_from_remote(search_params: search_params)
×
290

291
    if !search_params[:page].nil? && import_data[:pagination][:total_pages] != 1
×
292
      redirect_to supplier_articles_path(@supplier),
×
293
                  alert: I18n.t('articles.controller.sync.exceeds_shared_supplier_article_sync_limit', sync_method: I18n.t("suppliers.shared_supplier_methods.#{@supplier.shared_sync_method}"), import_sync_method: I18n.t('suppliers.shared_supplier_methods.import'))
294
    end
295

296
    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?
×
297
    @ignored_article_count = 0
×
298
    load_article_units((@new_articles + @updated_article_pairs.map(&:first)).map(&:current_article_units).flatten.uniq)
×
299
  rescue StandardError => e
300
    redirect_to supplier_articles_path(@supplier), alert: I18n.t('errors.general_msg', msg: e.message)
×
301
  end
302

303
  # Updates, deletes articles when upload or sync form is submitted
304
  def update_synchronized
1✔
305
    @enable_unit_migration = (params[:enable_unit_migration] == '1')
1✔
306
    @outlisted_articles = Article.includes(:latest_article_version).where(article_versions: { id: params[:outlisted_articles]&.values || [] })
1✔
307
    @updated_articles = Article.includes(:latest_article_version).where(article_versions: { id: params[:articles]&.values&.map do |v|
1✔
308
                                                                                                  v[:id]
×
309
                                                                                                end || [] })
310
    @new_articles = build_articles_from_params_array(params[:new_articles]&.values || [])
1✔
311

312
    # Prepare updated articles with their parameters
313
    updated_article_params = []
1✔
314
    @updated_articles.each do |a|
1✔
315
      current_params = params[:articles].values.detect { |p| p[:id] == a.latest_article_version.id.to_s }
×
316
      current_params.delete(:id)
×
317
      updated_article_params << [a, current_params]
×
318
    end
319

320
    # Use the SupplierSyncService to persist changes
321
    service = SupplierSyncService.new(@supplier)
1✔
322
    success = service.persist(@new_articles, @outlisted_articles, updated_article_params, enable_unit_migration: @enable_unit_migration)
1✔
323

324
    if success
1✔
325
      redirect_to supplier_articles_path(@supplier), notice: I18n.t('articles.controller.update_sync.notice')
1✔
326
    else
327
      load_article_units((@new_articles + @updated_articles).map(&:current_article_units).flatten.uniq)
×
328
      @updated_article_pairs = @updated_articles.map do |article|
×
329
        orig_article = Article.find(article.id)
×
330
        [article, orig_article.unequal_attributes(article)]
×
331
      end
332
      flash.now.alert = I18n.t('articles.controller.error_invalid')
×
333
      render params[:from_action] == 'sync' ? :sync : :parse_upload
×
334
    end
335
  end
336

337
  private
1✔
338

339
  def build_articles_from_params_array(params)
1✔
340
    params.map do |a|
1✔
341
      article = @supplier.articles.build
1✔
342
      article_version = article.article_versions.build(a)
1✔
343
      article.article_versions << article_version
1✔
344
      article.latest_article_version = article_version
1✔
345
      article_version.article = article
1✔
346
      article
1✔
347
    end
348
  end
349

350
  def build_article_migration_samples
1✔
351
    articles = @supplier.articles.with_latest_versions_and_categories.undeleted.includes(latest_article_version: [:article_unit_ratios])
×
352
    samples_hash = {}
×
353
    articles.each do |article|
×
354
      article_version = article.latest_article_version
×
355
      quantity = 1
×
356
      ratios = article_version.article_unit_ratios
×
357

358
      next if ratios.length > 1 ||
×
359
              article_version.billing_unit != article_version.group_order_unit ||
360
              article_version.price_unit != article_version.group_order_unit
361

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

364
      samples_hash[article_version.unit] = {} if samples_hash[article_version.unit].nil?
×
365
      samples_hash[article_version.unit][quantity] = [] if samples_hash[article_version.unit][quantity].nil?
×
366
      samples_hash[article_version.unit][quantity] << article
×
367
    end
368
    @samples = samples_hash.map do |unit, quantities_hash|
×
369
      quantities_hash.map do |quantity, sample_articles|
×
370
        conversion_result = ArticleUnitsLib.convert_old_unit(unit, quantity)
×
371
        { unit: unit, quantity: quantity, articles: sample_articles, conversion_result: conversion_result }
×
372
      end
373
    end
374
    @samples = @samples.flatten
×
375
                       .reject { |sample| sample[:conversion_result].nil? }
×
376

377
    additional_units = @samples.map do |sample|
×
378
      [sample[:conversion_result][:supplier_order_unit], sample[:conversion_result][:group_order_unit],
×
379
       sample[:conversion_result][:first_ratio]&.dig(:unit)]
380
    end.flatten.uniq.compact
381
    load_article_units(additional_units)
×
382
  end
383

384
  def load_article
1✔
385
    @article = Article
×
386
               .with_latest_versions_and_categories
387
               .includes(latest_article_version: [:article_unit_ratios])
388
               .find(params[:id])
389
  end
390

391
  def load_article_units(additional_units = [])
1✔
392
    additional_units = if !@article.nil?
1✔
393
                         @article.current_article_units
×
394
                       elsif !@articles.nil?
1✔
395
                         @articles.map(&:current_article_units)
1✔
396
                                  .flatten
397
                                  .uniq
398
                       else
399
                         additional_units
×
400
                       end
401

402
    @article_units = ArticleUnit.as_options(additional_units: additional_units)
1✔
403
    @all_units = ArticleUnit.as_hash(additional_units: additional_units)
1✔
404
  end
405

406
  def load_article_categories
1✔
407
    @article_categories = ArticleCategory.undeleted.order(:name)
×
408

409
    current_category_id = @article&.latest_article_version&.article_category_id
×
410
    @article_categories = @article_categories.or(ArticleCategory.where(id: current_category_id)) unless current_category_id.nil?
×
411
  end
412

413
  def new_empty_article_ratio
1✔
414
    @empty_article_unit_ratio = ArticleUnitRatio.new
2✔
415
    @empty_article_unit_ratio.article_version = @article.latest_article_version unless @article.nil?
2✔
416
    @empty_article_unit_ratio.sort = -1
2✔
417
  end
418

419
  # @return [Number] Number of articles not taken into account when syncing (having no number)
420
  def ignored_article_count
1✔
421
    if action_name == 'sync' || params[:from_action] == 'sync'
1✔
422
      @ignored_article_count ||= @supplier.articles.includes(:latest_article_version).undeleted.where(article_versions: { order_number: [
×
423
                                                                                                        nil, ''
424
                                                                                                      ] }).count
425
    else
426
      0
1✔
427
    end
428
  end
429
  helper_method :ignored_article_count
1✔
430
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