Class: Intervention

Inherits:
Ekylibre::Record::Base show all
Includes:
CastGroupable, Customizable, PeriodicCalculable
Defined in:
app/models/intervention.rb,
app/models/intervention/recorder.rb,
app/models/intervention/recorder/cast.rb

Overview

Informations

License

Ekylibre - Simple agricultural ERP Copyright (C) 2008-2009 Brice Texier, Thibaud Merigon Copyright (C) 2010-2012 Brice Texier Copyright (C) 2012-2016 Brice Texier, David Joulin

This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License along with this program. If not, see www.gnu.org/licenses.

Table: interventions

actions          :string
created_at       :datetime         not null
creator_id       :integer
custom_fields    :jsonb
description      :text
event_id         :integer
id               :integer          not null, primary key
issue_id         :integer
lock_version     :integer          default(0), not null
number           :string
prescription_id  :integer
procedure_name   :string           not null
started_at       :datetime
state            :string           not null
stopped_at       :datetime
updated_at       :datetime         not null
updater_id       :integer
whole_duration   :integer          default(0), not null
working_duration :integer          default(0), not null

Defined Under Namespace

Classes: Recorder

Constant Summary

Constants included from PeriodicCalculable

PeriodicCalculable::PARAMETERS

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Customizable

#custom_value, #set_custom_value, #validate_custom_fields

Methods included from CastGroupable

#add_group_parameter!, #add_parameter!, #add_product_parameter!

Methods inherited from Ekylibre::Record::Base

#already_updated?, attr_readonly_with_conditions, #check_if_destroyable?, #check_if_updateable?, columns_definition, complex_scopes, customizable?, #customizable?, #customized?, #destroyable?, #editable?, has_picture, #human_attribute_name, human_attribute_name_with_id, nomenclature_reflections, #old_record, #others, refers_to, scope_with_registration, simple_scopes, #updateable?

Class Method Details

.find_products(model, options = {}) ⇒ Object

Find a product with given options

- started_at
- work_number
- can
- variety
- derivative_of
- filter: WSQL expression

Options for product creation only:

- default_storage

Special options for worker creation only:

- first_name
- last_name
- born_at
- default_storage

437
438
439
440
441
442
443
444
445
446
# File 'app/models/intervention.rb', line 437

def find_products(model, options = {})
  relation = model
  relation = relation.where('COALESCE(born_at, ?) <= ? ', options[:started_at], options[:started_at]) if options[:started_at]
  relation = relation.of_expression(options[:filter]) if options[:filter]
  relation = relation.of_work_numbers(options[:work_number]) if options[:work_number]
  relation = relation.can(options[:can]) if options[:can]
  relation = relation.of_variety(options[:variety]) if options[:variety]
  relation = relation.derivative_of(options[:derivative_of]) if options[:derivative_of]
  return relation.all if relation.any?
end

.match(actors, options = {}) ⇒ Object

Returns an array of procedures matching the given actors ordered by relevance whose structure is [[procedure, relevance, arity], [procedure, relevance, arity], …] where 'procedure' is a Procedo::Procedure object, 'relevance' is a float, 'arity' is the number of actors matched in the procedure

parameters:

- actors, an array of actors identified for a given procedure

options:

- relevance: sets the relevance threshold above which results are wished.
  A float number between 0 and 1 is expected. Default value: 0.
- limit: sets the number of wanted results. By default all results are returned
- history: sets the use of actors history to calculate relevance.
  A boolean is expected. Default: false,since checking through history is slower
- provisional: sets the use of actors provisional to calculate relevance.
  A boolean is expected. Default: false, since it's slower.
- max_arity: limits results to procedures matching most actors.
  A boolean is expected. Default: false

464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
# File 'app/models/intervention.rb', line 464

def match(actors, options = {})
  actors = [actors].flatten
  limit = options[:limit].to_i - 1
  relevance_threshold = options[:relevance].to_f
  maximum_arity = 0

  # Creating coefficients for relevance calculation for each procedure
  # coefficients depend on provisional, actors history and actors presence in procedures
  history = Hash.new(0)
  provisional = []
  actors_id = []
  actors_id = actors.map(&:id) if options[:history] || options[:provisional]

  # Select interventions from all actors history
  if options[:history]
    # history is considered relevant on 1 year
    history.merge!(Intervention.joins(:product_parameters)
                    .where("intervention_parameters.actor_id IN (#{actors_id.join(', ')})")
                    .where(started_at: (Time.zone.now.midnight - 1.year)..(Time.zone.now))
                    .group('interventions.procedure_name')
                    .count('interventions.procedure_name'))
  end

  if options[:provisional]
    provisional.concat(Intervention.distinct
                        .joins(:product_parameters)
                        .where("intervention_parameters.actor_id IN (#{actors_id.join(', ')})")
                        .where(started_at: (Time.zone.now.midnight - 1.day)..(Time.zone.now + 3.days))
                        .pluck('interventions.procedure_name')).uniq!
  end

  coeff = {}

  history_size = 1.0 # prevents division by zero
  history_size = history.values.reduce(:+).to_f if history.count >= 1

  denominator = 1.0
  denominator += 2.0 if options[:history] && history.present?
  denominator += 3.0 if provisional.present? # if provisional is empty, it's pointless using it for relevance calculation

  result = []
  Procedo.procedures do |procedure_key, procedure|
    coeff[procedure_key] = 1.0 + 2.0 * (history[procedure_key].to_f / history_size) + 3.0 * provisional.count(procedure_key).to_f
    matched_parameters = procedure.matching_parameters_for(actors)
    if matched_parameters.any?
      result << [procedure, (((matched_parameters.values.count.to_f / actors.count) * coeff[procedure_key]) / denominator), matched_parameters.values.count]
      maximum_arity = matched_parameters.values.count if maximum_arity < matched_parameters.values.count
    end
  end
  result.delete_if { |_procedure, relevance, _arity| relevance < relevance_threshold }
  result.delete_if { |_procedure, _relevance, arity| arity < maximum_arity } if options[:max_arity]
  result.sort_by { |_procedure, relevance, _arity| -relevance }[0..limit]
end

.run!(*args) ⇒ Object

Create and run intervention


397
398
399
400
401
402
403
404
405
406
# File 'app/models/intervention.rb', line 397

def run!(*args)
  attributes = args.extract_options!
  attributes[:procedure_name] ||= args.shift
  intervention = transaction do
    intervention = Intervention.create!(attributes)
    yield intervention if block_given?
    intervention.run!
  end
  intervention
end

.used_proceduresObject


390
391
392
393
394
# File 'app/models/intervention.rb', line 390

def used_procedures
  select(:procedure_name).distinct.pluck(:procedure_name).map do |name|
    Procedo.find(name)
  end.compact
end

.write(*args) ⇒ Object

Registers and runs an intervention directly


409
410
411
412
413
414
415
416
417
418
419
420
421
# File 'app/models/intervention.rb', line 409

def write(*args)
  options = args.extract_options!
  procedure_name = args.shift || options[:procedure_name]

  transaction do
    attrs = options.slice(:procedure_name, :description, :issue_id, :prescription_id)
    recorder = Intervention::Recorder.new(attrs)

    yield recorder

    recorder.write!
  end
end

Instance Method Details

#activitiesObject

Returns activities of intervention through TargetDistribution


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

def activities
  # re active when Target Distribution works
  # Activity.of_intervention(self)
  a = []
  targets.each do |target|
    a << target.activity if target.activity
  end
  a.uniq
end

#add_working_period!(started_at, stopped_at) ⇒ Object


385
386
387
# File 'app/models/intervention.rb', line 385

def add_working_period!(started_at, stopped_at)
  working_periods.create!(started_at: started_at, stopped_at: stopped_at)
end

#cost(role = :input) ⇒ Object

Sums all intervention product parameter total_cost of a particular role


292
293
294
295
296
# File 'app/models/intervention.rb', line 292

def cost(role = :input)
  params = product_parameters.of_generic_role(role)
  return params.map(&:cost).compact.sum if params.any?
  nil
end

#cost_per_area(role = :input, area_unit = :hectare) ⇒ Object


298
299
300
301
302
303
304
305
# File 'app/models/intervention.rb', line 298

def cost_per_area(role = :input, area_unit = :hectare)
  if working_zone_area > 0.0.in_square_meter
    params = product_parameters.of_generic_role(role)
    return (params.map(&:cost).compact.sum / working_zone_area(area_unit).to_d) if params.any?
    nil
  end
  nil
end

#currencyObject


321
322
323
# File 'app/models/intervention.rb', line 321

def currency
  Preference[:currency]
end

#earn(role = :output) ⇒ Object


325
326
327
328
329
# File 'app/models/intervention.rb', line 325

def earn(role = :output)
  params = product_parameters.of_generic_role(role)
  return params.map(&:earn).compact.sum if params.any?
  nil
end

#human_actions_namesObject

Returns human actions names


227
228
229
230
# File 'app/models/intervention.rb', line 227

def human_actions_names
  actions.map { |action| Nomen::ProcedureAction.find(action).human_name }
         .to_sentence
end

#human_activities_namesObject

Returns human tool names


191
192
193
# File 'app/models/intervention.rb', line 191

def human_activities_names
  activities.map(&:name).sort.to_sentence
end

#human_doer_namesObject

Returns human doer names


217
218
219
# File 'app/models/intervention.rb', line 217

def human_doer_names
  doers.map(&:product).compact.map(&:work_name).sort.to_sentence
end

#human_target_namesObject

Returns human target names


212
213
214
# File 'app/models/intervention.rb', line 212

def human_target_names
  targets.map(&:product).compact.map(&:work_name).sort.to_sentence
end

#human_tool_namesObject

Returns human tool names


222
223
224
# File 'app/models/intervention.rb', line 222

def human_tool_names
  tools.map(&:product).compact.map(&:work_name).sort.to_sentence
end

#human_working_duration(unit = :hour) ⇒ Object


241
242
243
# File 'app/models/intervention.rb', line 241

def human_working_duration(unit = :hour)
  working_duration.in(:second).convert(unit).round(2).l
end

#human_working_zone_area(*args) ⇒ Object


341
342
343
344
345
346
# File 'app/models/intervention.rb', line 341

def human_working_zone_area(*args)
  options = args.extract_options!
  unit = args.shift || options[:unit] || :hectare
  precision = args.shift || options[:precision] || 2
  working_zone_area(unit: unit).round(precision).l(precision: precision)
end

#nameObject


232
233
234
235
# File 'app/models/intervention.rb', line 232

def name
  # raise self.inspect if self.procedure_name.blank?
  tc(:name, intervention: (procedure ? procedure.human_name : "procedures.#{procedure_name}".t(default: procedure_name.humanize)), number: number)
end

#procedureObject

The Procedo::Procedure behind intervention


200
201
202
# File 'app/models/intervention.rb', line 200

def procedure
  Procedo.find(procedure_name)
end

#product_parametersObject


195
196
197
# File 'app/models/intervention.rb', line 195

def product_parameters
  InterventionProductParameter.where(intervention: self)
end

#referenceObject

Deprecated method to return procedure


205
206
207
208
209
# File 'app/models/intervention.rb', line 205

def reference
  ActiveSupport::Deprecation.warn 'Intervention#reference is deprecated.' \
                                  'Please use Intervention#procedure instead.'
  procedure
end

#run!Object

Run the intervention ie. the state is marked as done Returns intervention


379
380
381
382
383
# File 'app/models/intervention.rb', line 379

def run!
  raise 'Cannot run intervention without procedure' unless runnable?
  update_attributes(state: :done)
  self
end

#runnable?Boolean

Returns:

  • (Boolean)

361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
# File 'app/models/intervention.rb', line 361

def runnable?
  return false unless undone? && procedure
  valid = true
  # Check cardinality and runnability
  procedure.parameters.each do |parameter|
    all_parameters = parameters.where(reference_name: parameter.name)
    # unless parameter.cardinality.include?(parameters.count)
    #   valid = false
    # end
    all_parameters.each do |parameter|
      valid = false unless parameter.runnable?
    end
  end
  valid
end

#start_timeObject


237
238
239
# File 'app/models/intervention.rb', line 237

def start_time
  started_at
end

#statusObject


353
354
355
356
357
358
359
# File 'app/models/intervention.rb', line 353

def status
  if undone?
    return (runnable? ? :caution : :stop)
  elsif done?
    return :go
  end
end

#total_costObject


307
308
309
310
311
# File 'app/models/intervention.rb', line 307

def total_cost
  [:input, :tool, :doer].map do |type|
    (cost(type) || 0.0).to_d.round(2)
  end.sum
end

#total_cost_per_area(area_unit = :hectare) ⇒ Object


313
314
315
316
317
318
319
# File 'app/models/intervention.rb', line 313

def total_cost_per_area(area_unit = :hectare)
  if working_zone_area > 0.0.in_square_meter
    return (total_cost / working_zone_area(area_unit).to_d)
  else
    return nil
  end
end

#update_temporalityObject

Update temporality informations in intervention


246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
# File 'app/models/intervention.rb', line 246

def update_temporality
  reload unless new_record? || destroyed?
  started_at = working_periods.minimum(:started_at)
  stopped_at = working_periods.maximum(:stopped_at)
  update_columns(
    started_at: started_at,
    stopped_at: stopped_at,
    working_duration: working_periods.sum(:duration),
    whole_duration: ((stopped_at && started_at) ? (stopped_at - started_at).to_i : 0)
  )
  event.update_columns(
    started_at: self.started_at,
    stopped_at: self.stopped_at
  ) if event
  outputs.find_each do |output|
    product = output.product
    next unless product
    product.born_at = self.started_at
    product.initial_born_at = product.born_at
    product.save!

    movement = output.product_movement
    next unless movement
    movement.started_at = self.started_at
    movement.stopped_at = self.stopped_at
    movement.save!
  end

  inputs.find_each do |input|
    product = input.product
    next unless product

    movement = input.product_movement
    next unless movement
    movement.started_at = self.started_at
    movement.stopped_at = self.stopped_at
    movement.save!

    # to be sure last
    last_movement = product.movements.last_of_all
    last_movement.stopped_at = nil
    last_movement.save!
  end
end

#with_undestroyable_products?Boolean

Returns:

  • (Boolean)

172
173
174
175
176
177
# File 'app/models/intervention.rb', line 172

def with_undestroyable_products?
  outputs.map(&:product).detect do |product|
    next unless product
    InterventionProductParameter.of_actor(product).where.not(type: 'InterventionOutput').any?
  end
end

#working_area(unit = :hectare) ⇒ Object


348
349
350
351
# File 'app/models/intervention.rb', line 348

def working_area(unit = :hectare)
  ActiveSupport::Deprecation.warn 'Intervention#working_area is deprecated. Please use Intervention#working_zone_area instead.'
  working_zone_area(unit)
end

#working_zone_area(*args) ⇒ Object


331
332
333
334
335
336
337
338
339
# File 'app/models/intervention.rb', line 331

def working_zone_area(*args)
  options = args.extract_options!
  unit = args.shift || options[:unit] || :hectare
  if targets.any?
    area = targets.with_working_zone.map(&:working_zone_area).sum.in(unit)
  end
  area ||= 0.0.in(unit)
  area
end