Class: Order

Inherits:
ApplicationRecord show all
Includes:
DateTimeAttributeValidate
Defined in:
app/models/order.rb

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#article_idsObject



95
96
97
# File 'app/models/order.rb', line 95

def article_ids
  @article_ids ||= order_articles.map { |oa| oa.article_version.article_id.to_s }
end

#ignore_warningsObject

Returns the value of attribute ignore_warnings.



2
3
4
# File 'app/models/order.rb', line 2

def ignore_warnings
  @ignore_warnings
end

#transport_distributionObject

Returns the value of attribute transport_distribution.



2
3
4
# File 'app/models/order.rb', line 2

def transport_distribution
  @transport_distribution
end

Class Method Details

.finish_ended!Object



357
358
359
360
361
362
363
364
# File 'app/models/order.rb', line 357

def self.finish_ended!
  orders = Order.where.not(end_action: Order.end_actions[:no_end_action]).where(state: 'open').where(ends: ..DateTime.now)
  orders.each do |order|
    order.do_end_action!
  rescue StandardError => e
    ExceptionNotifier.notify_exception(e, data: { foodcoop: FoodsoftConfig.scope, order_id: order.id })
  end
end

.ordergroup_group_orders_map(ordergroup) ⇒ Object

fetch current Order scope’s records and map the current user’s GroupOrders in (if any) (performance enhancement as opposed to fetching each GroupOrder separately from the view)



157
158
159
160
161
162
163
164
165
166
167
# File 'app/models/order.rb', line 157

def self.ordergroup_group_orders_map(ordergroup)
  orders = includes(:supplier)
  group_orders = GroupOrder.where(ordergroup_id: ordergroup.id, order_id: orders.map(&:id))
  group_orders_hash = group_orders.index_by { |go| go.order_id }
  orders.map do |order|
    {
      order: order,
      group_order: group_orders_hash[order.id]
    }
  end
end

.ransackable_associations(_auth_object = nil) ⇒ Object



59
60
61
# File 'app/models/order.rb', line 59

def self.ransackable_associations(_auth_object = nil)
  %w[supplier articles order_articles]
end

.ransackable_attributes(_auth_object = nil) ⇒ Object



55
56
57
# File 'app/models/order.rb', line 55

def self.ransackable_attributes(_auth_object = nil)
  %w[id state supplier_id starts boxfill ends pickup]
end

Instance Method Details

#articles_for_orderingObject



71
72
73
74
75
76
77
78
79
80
81
82
# File 'app/models/order.rb', line 71

def articles_for_ordering
  if stockit?
    # make sure to include those articles which are no longer available
    # but which have already been ordered in this stock order
    StockArticle.available.includes(latest_article_version: :article_category)
                .order('article_categories.name', 'article_versions.name').reject do |a|
      a.quantity_available <= 0 && !a.ordered_in_order?(self)
    end.group_by { |a| a..name }
  else
    supplier.articles.available.group_by { |a| a..name }
  end
end

#articles_grouped_by_categoryObject

Returns OrderArticles in a nested Array, grouped by category and ordered by article name. The array has the following form: e.g: [[“drugs”,[teethpaste, toiletpaper]], [“fruits” => [apple, banana, lemon]]]



181
182
183
184
185
186
187
188
# File 'app/models/order.rb', line 181

def articles_grouped_by_category
  @articles_grouped_by_category ||= order_articles
                                    .includes([:group_order_articles,
                                               { article_version: %i[article_category article_unit_ratios] }])
                                    .order('article_versions.name', 'article_unit_ratios.sort')
                                    .group_by { |oa| oa.article_version..name }
                                    .sort { |a, b| a[0] <=> b[0] }
end

#articles_sort_by_categoryObject



190
191
192
193
194
# File 'app/models/order.rb', line 190

def articles_sort_by_category
  order_articles.includes(:article).order('articles.name').sort do |a, b|
    a.article..name <=> b.article..name
  end
end

#boxfill?Boolean

Returns:

  • (Boolean)


120
121
122
# File 'app/models/order.rb', line 120

def boxfill?
  !!FoodsoftConfig[:use_boxfill] && open? && boxfill.present? && boxfill < Time.now
end

#close!(user, transaction_type = nil, financial_link = nil, create_foodcoop_transaction: false) ⇒ Object

Sets order.status to ‘close’ and updates all Ordergroup.account_balances



272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
# File 'app/models/order.rb', line 272

def close!(user, transaction_type = nil, financial_link = nil, create_foodcoop_transaction: false)
  raise I18n.t('orders.model.error_closed') if closed?

  update_price_of_group_orders!

  transaction do                                        # Start updating account balances
    charge_group_orders!(user, transaction_type, financial_link)

    if stockit?                                         # Decreases the quantity of stock_articles
      for oa in order_articles.includes(article_version: :article)
        oa.update_results!                              # Update units_to_order of order_article
        stock_changes.create! stock_article: oa.article_version.article, quantity: oa.units_to_order * -1
      end
    end

    if create_foodcoop_transaction
      ft = FinancialTransaction.new({ financial_transaction_type: transaction_type,
                                      user: user,
                                      amount: sum(:groups),
                                      note: transaction_note,
                                      financial_link: financial_link })
      ft.save!
    end

    update! state: 'closed', updated_by: user, foodcoop_result: profit
  end
end

#close_direct!(user) ⇒ Object

Close the order directly, without automaticly updating ordergroups account balances



301
302
303
304
305
306
307
308
309
# File 'app/models/order.rb', line 301

def close_direct!(user)
  raise I18n.t('orders.model.error_closed') if closed?

  unless FoodsoftConfig[:charge_members_manually]
    comments.create(user: user,
                    text: I18n.t('orders.model.close_direct_message'))
  end
  update! state: 'closed', updated_by: user
end

#closed?Boolean

Returns:

  • (Boolean)


116
117
118
# File 'app/models/order.rb', line 116

def closed?
  state == 'closed'
end

#do_end_action!Object



345
346
347
348
349
350
351
352
353
354
355
# File 'app/models/order.rb', line 345

def do_end_action!
  if auto_close?
    finish!(created_by)
  elsif auto_close_and_send?
    finish!(created_by)
    send_to_supplier!(created_by)
  elsif auto_close_and_send_min_quantity?
    finish!(created_by)
    send_to_supplier!(created_by) if sum >= supplier.min_order_quantity.to_r
  end
end

#erroneous_article_idsObject

Returns an array of article ids that lead to a validation error.



100
101
102
# File 'app/models/order.rb', line 100

def erroneous_article_ids
  @erroneous_article_ids ||= []
end

#expired?Boolean

Returns:

  • (Boolean)


128
129
130
# File 'app/models/order.rb', line 128

def expired?
  ends.present? && ends < Time.now
end

#finish!(user) ⇒ Object

Finishes this order. This will set the order state to “finish” and the end property to the current time. Ignored if the order is already finished.



244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
# File 'app/models/order.rb', line 244

def finish!(user)
  return if finished?

  Order.transaction do
    # set new order state (needed by notify_order_finished)
    update!(state: 'finished', ends: Time.now, updated_by: user)

    # Update order_articles. Save the current article_version to keep price consistency
    # Also save results for each group_order_result
    # Clean up
    order_articles.each do |oa|
      oa.group_order_articles.each do |goa|
        goa.save_results!
      end
    end

    # Update GroupOrder prices
    group_orders.each(&:update_price!)

    # Stats
    ordergroups.each(&:update_stats!)

    # Notifications
    NotifyFinishedOrderJob.perform_later(self)
  end
end

#finished?Boolean

Returns:

  • (Boolean)


108
109
110
# File 'app/models/order.rb', line 108

def finished?
  %w[finished received].include?(state)
end

#group_order(ordergroup) ⇒ Object

search GroupOrder of given Ordergroup



170
171
172
# File 'app/models/order.rb', line 170

def group_order(ordergroup)
  group_orders.where(ordergroup_id: ordergroup.id).first
end

#include_articlesObject (protected)



378
379
380
# File 'app/models/order.rb', line 378

def include_articles
  errors.add(:articles, I18n.t('orders.model.error_nosel')) if article_ids.empty?
end

#init_datesObject

sets up first guess of dates when initializing a new object I guess ‘def initialize` would work, but it’s tricky stackoverflow.com/questions/1186400



134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
# File 'app/models/order.rb', line 134

def init_dates
  self.starts ||= Time.now
  if FoodsoftConfig[:order_schedule]
    # try to be smart when picking a reference day
    last = begin
      DateTime.parse(FoodsoftConfig[:order_schedule][:initial])
    rescue StandardError
      nil
    end
    last ||= Order.finished.reorder(:starts).first.try(:starts)
    last ||= self.starts
    # adjust boxfill and end date
    if is_boxfill_useful?
      self.boxfill ||= FoodsoftDateUtil.next_occurrence last, self.starts,
                                                        FoodsoftConfig[:order_schedule][:boxfill]
    end
    self.ends ||= FoodsoftDateUtil.next_occurrence last, self.starts, FoodsoftConfig[:order_schedule][:ends]
  end
  self
end

#is_boxfill_useful?Boolean

Returns:

  • (Boolean)


124
125
126
# File 'app/models/order.rb', line 124

def is_boxfill_useful?
  !!FoodsoftConfig[:use_boxfill] && !!supplier.try(:has_tolerance?)
end

#keep_ordered_articlesObject (protected)



382
383
384
385
386
387
388
389
390
# File 'app/models/order.rb', line 382

def keep_ordered_articles
  chosen_order_articles = order_articles.joins(:article_version).where(article_versions: { article_id: article_ids })
  to_be_removed = order_articles - chosen_order_articles
  to_be_removed_but_ordered = to_be_removed.select { |a| a.quantity > 0 || a.tolerance > 0 }
  return if to_be_removed_but_ordered.empty? || ignore_warnings

  errors.add(:articles, I18n.t(stockit? ? 'orders.model.warning_ordered_stock' : 'orders.model.warning_ordered'))
  @erroneous_article_ids = to_be_removed_but_ordered.map { |oa| oa.article_version.article_id }
end

#nameObject



67
68
69
# File 'app/models/order.rb', line 67

def name
  stockit? ? I18n.t('orders.model.stock') : supplier.name
end

#open?Boolean

Returns:

  • (Boolean)


104
105
106
# File 'app/models/order.rb', line 104

def open?
  state == 'open'
end

#profit(options = {}) ⇒ Object

Returns the defecit/benefit for the foodcoop Requires a valid invoice, belonging to this order FIXME: Consider order.foodcoop_result



199
200
201
202
203
204
205
# File 'app/models/order.rb', line 199

def profit(options = {})
  markup = options[:without_markup] || false
  return unless invoice

  groups_sum = markup ? sum(:groups_without_markup) : sum(:groups)
  groups_sum - invoice.net_amount
end

#received?Boolean

Returns:

  • (Boolean)


112
113
114
# File 'app/models/order.rb', line 112

def received?
  state == 'received'
end

#save_order_articlesObject (protected)



392
393
394
395
396
397
398
399
400
401
402
# File 'app/models/order.rb', line 392

def save_order_articles
  # fetch selected articles
  articles_list = Article.find(article_ids)
  # create new order_articles
  articles = article_versions.map(&:article)
  (articles_list - articles).each { |article| order_articles.create(article_version: article.latest_article_version) }
  # delete old order_articles
  articles.reject { |article| articles_list.include?(article) }.each do |article|
    order_articles.detect { |order_article| order_article.article_version.article_id == article.id }.destroy
  end
end

#send_to_supplier!(user) ⇒ Object



311
312
313
314
315
316
317
318
319
320
# File 'app/models/order.rb', line 311

def send_to_supplier!(user)
  if supplier.remote_order_method == :email
    Mailer.deliver_now_with_default_locale do
      Mailer.order_result_supplier(user, self)
    end
  else
    upload_via_ftp
  end
  update_attribute(:remote_ordered_at, Time.now)
end

#starts_before_endsObject (protected)



368
369
370
371
372
373
374
375
376
# File 'app/models/order.rb', line 368

def starts_before_ends
  delta = Rails.env.test? ? 1 : 0 # since Rails 4.2 tests appear to have time differences, with this validation failing
  errors.add(:ends, I18n.t('orders.model.error_starts_before_ends')) if ends && starts && ends <= (starts - delta)
  errors.add(:ends, I18n.t('orders.model.error_boxfill_before_ends')) if ends && boxfill && ends <= (boxfill - delta)
  return unless boxfill && starts && boxfill <= (starts - delta)

  errors.add(:boxfill,
             I18n.t('orders.model.error_starts_before_boxfill'))
end

#stock_group_orderObject



174
175
176
# File 'app/models/order.rb', line 174

def stock_group_order
  group_orders.where(ordergroup_id: nil).first
end

#stockit?Boolean

Returns:

  • (Boolean)


63
64
65
# File 'app/models/order.rb', line 63

def stockit?
  supplier_id.nil?
end

#sum(type = :gross) ⇒ Object

Returns the all round price of a finished order :groups returns the sum of all GroupOrders :clear returns the price without tax, deposit and markup :gross includes tax and deposit. this amount should be equal to suppliers bill :fc, guess what…



212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
# File 'app/models/order.rb', line 212

def sum(type = :gross)
  total = 0
  if %i[net gross fc].include?(type)
    for oa in order_articles.ordered.includes(:article_version)
      quantity = oa.units * oa.article_version.convert_quantity(1, oa.article_version.supplier_order_unit,
                                                                oa.article_version.group_order_unit)
      case type
      when :net
        total += quantity * oa.article_version.group_order_price
      when :gross
        total += quantity * oa.article_version.gross_group_order_price
      when :fc
        total += quantity * oa.article_version.fc_group_order_price
      end
    end
  elsif %i[groups groups_without_markup].include?(type)
    for go in group_orders.includes(group_order_articles: { order_article: :article_version })
      for goa in go.group_order_articles
        case type
        when :groups
          total += goa.result * goa.order_article.article_version.fc_group_order_price
        when :groups_without_markup
          total += goa.result * goa.order_article.article_version.gross_group_order_price
        end
      end
    end
  end
  total
end

#supplier_articlesObject



84
85
86
87
88
89
90
# File 'app/models/order.rb', line 84

def supplier_articles
  if stockit?
    StockArticle.undeleted.with_latest_versions_and_categories.reorder('article_versions.name')
  else
    supplier.articles.undeleted.with_latest_versions_and_categories.reorder('article_versions.name')
  end
end

#upload_via_ftpObject



322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
# File 'app/models/order.rb', line 322

def upload_via_ftp
  raise I18.t('orders.model.error_invalid') unless valid?

  formatter_class = supplier.remote_order_formatter
  raise "No formatter registered for remote order method: #{supplier.read_attribute_before_type_cast(:remote_order_method)}" unless formatter_class

  local_temp_file = Tempfile.new
  begin
    formatter = formatter_class.new(self)
    local_temp_file.write(formatter.to_remote_format)
    local_temp_file.rewind
    remote_filename = formatter.remote_file_name
    uri = URI(supplier.remote_order_url)
    Net::FTP.open(uri.host) do |ftp|
      ftp.(uri.user, uri.password)
      ftp.putbinaryfile(local_temp_file.path, remote_filename)
    end
  ensure
    local_temp_file.close
    local_temp_file.unlink
  end
end