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

foodcoops / foodsoft / 14290630730

06 Apr 2025 08:19AM UTC coverage: 44.865% (-23.3%) from 68.13%
14290630730

Pull #1078

github

web-flow
Merge c89532609 into 02853a526
Pull Request #1078: order balancing add group to article: sort ordergroup names alphabetically

3097 of 6903 relevant lines covered (44.86%)

14.96 hits per line

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

39.64
/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_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']
2✔
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'
2✔
26
           end
27

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

31
    if request.format.csv?
2✔
32
      send_data ArticlesCsv.new(@articles, encoding: 'utf-8').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?
2✔
37

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

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

46
  def new
1✔
47
    @article = @supplier.articles.build
1✔
48
    @article.latest_article_version = @article.article_versions.build(tax: FoodsoftConfig[:tax_default])
1✔
49
    render layout: false
1✔
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
1✔
65
    Article.transaction do
1✔
66
      @article = Article.create(supplier_id: @supplier.id)
1✔
67
      @article.attributes = { latest_article_version_attributes: params[:article_version] }
1✔
68
      raise ActiveRecord::Rollback unless @article.valid?
1✔
69

70
      valid = @article.save
1✔
71
    end
72

73
    if valid
1✔
74
      render layout: false
1✔
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')
1✔
254
    options = { filename: uploaded_file.original_filename }
1✔
255
    options[:outlist_absent] = (params[:articles]['outlist_absent'] == '1')
1✔
256
    options[:convert_units] = (params[:articles]['convert_units'] == '1')
1✔
257
    @updated_article_pairs, @outlisted_articles, @new_articles, import_data = @supplier.sync_from_file(uploaded_file.tempfile,
1✔
258
                                                                                                       options)
259

260
    @articles = @updated_article_pairs.pluck(0) + @new_articles
1✔
261
    load_article_units
1✔
262

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

272
  # sync all articles with the external database
273
  # renders a form with articles, which should be updated
274
  def sync
1✔
275
    @updated_article_pairs, @outlisted_articles, @new_articles, import_data = @supplier.sync_from_remote
×
276
    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?
×
277
    @ignored_article_count = 0
×
278
    load_article_units((@new_articles + @updated_article_pairs.map(&:first)).map(&:current_article_units).flatten.uniq)
×
279
  rescue StandardError => e
280
    redirect_to upload_supplier_articles_path(@supplier), alert: I18n.t('errors.general_msg', msg: e.message)
×
281
  end
282

283
  # Updates, deletes articles when upload or sync form is submitted
284
  def update_synchronized
1✔
285
    @outlisted_articles = Article.includes(:latest_article_version).where(article_versions: { id: params[:outlisted_articles]&.values || [] })
1✔
286
    @updated_articles = Article.includes(:latest_article_version).where(article_versions: { id: params[:articles]&.values&.map do |v|
1✔
287
                                                                                                  v[:id]
×
288
                                                                                                end || [] })
289
    @new_articles = (params[:new_articles]&.values || []).map do |a|
1✔
290
      article = @supplier.articles.build
1✔
291
      article_version = article.article_versions.build(a)
1✔
292
      article.article_versions << article_version
1✔
293
      article.latest_article_version = article_version
1✔
294
      article_version.article = article
1✔
295
      article
1✔
296
    end
297

298
    has_error = false
1✔
299
    Article.transaction do
1✔
300
      # delete articles
301
      begin
302
        @outlisted_articles.each(&:mark_as_deleted)
1✔
303
      rescue StandardError
304
        # raises an exception when used in current order
305
        has_error = true
×
306
      end
307
      # Update articles
308
      @updated_articles.each_with_index do |a, index|
1✔
309
        current_params = params[:articles][index.to_s]
×
310
        current_params.delete(:id)
×
311

312
        a.latest_article_version.article_unit_ratios.clear
×
313
        a.latest_article_version.assign_attributes(current_params)
×
314
        a.save
×
315
      end or has_error = true
316
      # Add new articles
317
      @new_articles.each { |a| a.save or has_error = true }
2✔
318

319
      raise ActiveRecord::Rollback if has_error
1✔
320
    end
321

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

335
  private
1✔
336

337
  def build_article_migration_samples
1✔
338
    articles = @supplier.articles.with_latest_versions_and_categories.undeleted.includes(latest_article_version: [:article_unit_ratios])
×
339
    samples_hash = {}
×
340
    articles.each do |article|
×
341
      article_version = article.latest_article_version
×
342
      quantity = 1
×
343
      ratios = article_version.article_unit_ratios
×
344

345
      next if ratios.length > 1 ||
×
346
              article_version.billing_unit != article_version.group_order_unit ||
347
              article_version.price_unit != article_version.group_order_unit
348

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

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

364
    additional_units = @samples.map do |sample|
×
365
      [sample[:conversion_result][:supplier_order_unit], sample[:conversion_result][:group_order_unit],
×
366
       sample[:conversion_result][:first_ratio]&.dig(:unit)]
367
    end.flatten.uniq.compact
368
    load_article_units(additional_units)
×
369
  end
370

371
  def load_article
1✔
372
    @article = Article
×
373
               .with_latest_versions_and_categories
374
               .includes(latest_article_version: [:article_unit_ratios])
375
               .find(params[:id])
376
  end
377

378
  def load_article_units(additional_units = [])
1✔
379
    additional_units = if !@article.nil?
3✔
380
                         @article.current_article_units
×
381
                       elsif !@articles.nil?
3✔
382
                         @articles.map(&:current_article_units)
1✔
383
                                  .flatten
384
                                  .uniq
385
                       else
386
                         additional_units
2✔
387
                       end
388

389
    @article_units = ArticleUnit.as_options(additional_units: additional_units)
3✔
390
    @all_units = ArticleUnit.as_hash(additional_units: additional_units)
3✔
391
  end
392

393
  def load_article_categories
1✔
394
    @article_categories = ArticleCategory.all
×
395
  end
396

397
  def new_empty_article_ratio
1✔
398
    @empty_article_unit_ratio = ArticleUnitRatio.new
4✔
399
    @empty_article_unit_ratio.article_version = @article.latest_article_version unless @article.nil?
4✔
400
    @empty_article_unit_ratio.sort = -1
4✔
401
  end
402

403
  # @return [Number] Number of articles not taken into account when syncing (having no number)
404
  def ignored_article_count
1✔
405
    if action_name == 'sync' || params[:from_action] == 'sync'
1✔
406
      @ignored_article_count ||= @supplier.articles.includes(:latest_article_version).undeleted.where(article_versions: { order_number: [
×
407
                                                                                                        nil, ''
408
                                                                                                      ] }).count
409
    else
410
      0
1✔
411
    end
412
  end
413
  helper_method :ignored_article_count
1✔
414
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