Class: Product

Inherits:
Ekylibre::Record::Base show all
Includes:
Attachable, Customizable, Indicateable, Versionable
Defined in:
app/models/product.rb

Direct Known Subclasses

Matter, ProductGroup, Worker, Zone

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Customizable

#custom_value, #set_custom_value, #validate_custom_fields

Methods included from Versionable

#add_creation_version, #add_destruction_version, #add_update_version, #last_version, #notably_changed?, #version_object

Methods included from Indicateable

#add_and_read, #add_to_readings, #compute_and_read, #copy_readings_of!, #density, #first_reading, #get, #get!, #mark!, #operate_on_readings, #read!, #read_whole_indicators_from!, #reading, #substract_and_read, #substract_to_readings

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?

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(method_name, *args) ⇒ Object

Returns value of an indicator if its name correspond to


595
596
597
598
599
600
601
602
603
604
# File 'app/models/product.rb', line 595

def method_missing(method_name, *args)
  if Nomen::Indicator.all.include?(method_name.to_s.gsub(/\!\z/, ''))
    if method_name.to_s.end_with?('!')
      return get!(method_name.to_s.gsub(/\!\z/, ''), *args)
    else
      return get(method_name, *args)
    end
  end
  super
end

Class Method Details

.availables(**args) ⇒ Object


316
317
318
319
320
321
322
323
324
325
326
# File 'app/models/product.rb', line 316

def availables(**args)
  if args[:at]
    if args[:at].is_a? String
      available.at(Time.strptime(args[:at], '%Y-%m-%d %H:%M'))
    else
      available.at(args[:at])
    end
  else
    available
  end
end

.new_with_cast(*attributes, &block) ⇒ Object

Auto-cast product to best matching class with type column


307
308
309
310
311
312
313
# File 'app/models/product.rb', line 307

def new_with_cast(*attributes, &block)
  if (h = attributes.first).is_a?(Hash) && !h.nil? && (type = h[:type] || h['type']) && !type.empty? && (klass = type.constantize) != self
    raise "Can not cast #{name} to #{klass.name}" unless klass <= self
    return klass.new(*attributes, &block)
  end
  new_without_cast(*attributes, &block)
end

Instance Method Details

#add_content_products(products, options = {}) ⇒ Object

add products to current container


516
517
518
519
520
521
522
523
524
525
# File 'app/models/product.rb', line 516

def add_content_products(products, options = {})
  Intervention.write(:product_moving, options) do |i|
    i.cast :container, self, as: 'product_moving-container'
    products.each do |p|
      product = (p.is_a?(Product) ? p : Product.find(p))
      member = i.cast :product, product, as: 'product_moving-target'
      i.movement member, :container
    end
  end
end

#age(at = Time.zone.now) ⇒ Object

Returns age in seconds of the product


464
465
466
467
# File 'app/models/product.rb', line 464

def age(at = Time.zone.now)
  return 0 if born_at.nil? || born_at >= at
  ((dead_at || at) - born_at)
end

#available?Boolean

Returns:

  • (Boolean)

334
335
336
# File 'app/models/product.rb', line 334

def available?
  dead_at.nil? && !population.zero?
end

#born_at_in_interventionsObject


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

def born_at_in_interventions
  first_used_at = interventions.order(started_at: :asc).first.started_at
  errors.add(:born_at, :on_or_before, restriction: first_used_at.l) if born_at > first_used_at
end

#choose_default_nameObject

Try to find the best name for the new products


402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
# File 'app/models/product.rb', line 402

def choose_default_name
  return unless name.blank?
  if variant
    if last = variant.products.reorder(id: :desc).first
      self.name = last.name
      array = name.split(/\s+/)
      if array.last =~ /^\(+\d+\)+?$/
        self.name = array[0..-2].join(' ') + ' (' + array.last.gsub(/(^\(+|\)+$)/, '').to_i.succ.to_s + ')'
      else
        name << ' (1)'
      end
    else
      self.name = variant_name
    end
  end
  if name.blank?
    # By default, choose a random name
    self.name = ::FFaker::Name.first_name
  end
end

#containeds(at = Time.zone.now) ⇒ Object


563
564
565
566
567
568
569
570
# File 'app/models/product.rb', line 563

def containeds(at = Time.zone.now)
  list = []
  for localization in ProductLocalization.where(container_id: id).at(at)
    list << localization.product
    list += localization.product.containeds(at)
  end
  list
end

#container_at(at) ⇒ Object

Returns the container for the product at a given time


542
543
544
545
546
547
# File 'app/models/product.rb', line 542

def container_at(at)
  if l = localizations.at(at).first
    return l.container
  end
  nil
end

#contains(varieties = :product, at = Time.zone.now) ⇒ Object

Returns the current contents of the product at a given time (or now by default)


550
551
552
553
554
555
556
557
558
559
560
561
# File 'app/models/product.rb', line 550

def contains(varieties = :product, at = Time.zone.now)
  localizations = content_localizations.at(at).of_product_varieties(varieties)
  if localizations.any?
    # object = []
    # for localization in localizations
    # object << localization.product if localization.product.is_a?(stored_class)
    # end
    return localizations
  else
    return nil
  end
end

#contents_name(_at = Time.zone.now) ⇒ Object


572
573
574
# File 'app/models/product.rb', line 572

def contents_name(_at = Time.zone.now)
  containeds.map(&:name).compact.to_sentence
end

#dead?Boolean

Returns:

  • (Boolean)

506
507
508
# File 'app/models/product.rb', line 506

def dead?
  !finish_way.nil?
end

#dead_at_in_interventionsObject


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

def dead_at_in_interventions
  last_used_at = interventions.order(stopped_at: :desc).first.stopped_at
  errors.add(:dead_at, :on_or_after, restriction: last_used_at.l) if dead_at < last_used_at
end

#default_catalog_item(usage) ⇒ Object

Returns item from default catalog for given usage


470
471
472
473
# File 'app/models/product.rb', line 470

def default_catalog_item(usage)
  return nil unless variant
  variant.default_catalog_item(usage)
end

#deliverable?Boolean

TODO: Removes this ASAP

Returns:

  • (Boolean)

330
331
332
# File 'app/models/product.rb', line 330

def deliverable?
  false
end

#evaluated_price(_options = {}) ⇒ Object

Returns an evaluated price (without taxes) for the product in an intervention context options could contains a parameter :at for the datetime of a catalog price unit_price in a purchase context or unit_price in a sale context or unit_price in catalog price


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
# File 'app/models/product.rb', line 480

def evaluated_price(_options = {})
  filter = {
    variant_id: variant_id
  }
  incoming_item = incoming_parcel_item
  incoming_purchase_item = incoming_item.purchase_item if incoming_item
  outgoing_item = parcel_items.with_nature(:outgoing).first
  outgoing_sale_item = outgoing_item.sale_item if outgoing_item

  price = if incoming_purchase_item
            # search a price in purchase item via incoming item price
            incoming_purchase_item.unit_pretax_amount
          elsif outgoing_sale_item
            # search a price in sale item via outgoing item price
            outgoing_sale_item.unit_pretax_amount
          elsif catalog_item = variant.catalog_items.limit(1).first
            # search a price in catalog price
            if catalog_item.all_taxes_included == true
              catalog_item.reference_tax.pretax_amount_of(catalog_item.amount)
            else
              catalog_item.amount
            end
          end
  price
end

#groups_at(viewed_at = nil) ⇒ Object

Returns groups of the product at a given time (or now by default)


511
512
513
# File 'app/models/product.rb', line 511

def groups_at(viewed_at = nil)
  ProductGroup.groups_of(self, viewed_at || Time.zone.now)
end

#initial_reading(indicator_name) ⇒ Object


448
449
450
# File 'app/models/product.rb', line 448

def initial_reading(indicator_name)
  first_reading(indicator_name)
end

#initializeable?Boolean

Returns:

  • (Boolean)

642
643
644
# File 'app/models/product.rb', line 642

def initializeable?
  new_record? || !(parcel_items.any? || InterventionParameter.of_generic_roles([:input, :output, :target, :doer, :tool]).of_actor(self).any? || fixed_asset.present?)
end

#localized_variants(variant, options = {}) ⇒ Object

Returns all contained products of the given variant


589
590
591
592
# File 'app/models/product.rb', line 589

def localized_variants(variant, options = {})
  options[:at] ||= Time.zone.now
  containeds.select { |p| p.variant == variant }
end

#matching_modelObject

Returns the matching model for the record


453
454
455
# File 'app/models/product.rb', line 453

def matching_model
  ProductNature.matching_model(self.variety)
end

#move!(quantity, options = {}) ⇒ Object

Moves population with given quantity


537
538
539
# File 'app/models/product.rb', line 537

def move!(quantity, options = {})
  movements.create!(delta: quantity, started_at: options[:at])
end

#nature_nameObject


338
339
340
# File 'app/models/product.rb', line 338

def nature_name
  nature ? nature.name : nil
end

#ownerObject

Returns the current ownership for the product


577
578
579
580
581
582
# File 'app/models/product.rb', line 577

def owner
  if o = current_ownership
    return o.owner
  end
  nil
end

#part_with(population, options = {}) ⇒ Object

Build a new product parted from self No product_division created. Options can be shape, name, born_at


617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
# File 'app/models/product.rb', line 617

def part_with(population, options = {})
  attributes = options.slice(:name, :number, :work_number, :identification_number, :tracking, :default_storage, :description, :picture)
  attributes[:name] ||= name
  attributes[:tracking] ||= tracking
  attributes[:variant] = variant
  # Initial values
  attributes[:initial_population] = population
  attributes[:initial_shape] ||= options[:shape] || shape
  attributes[:initial_born_at] = options[:born_at] if options[:born_at]
  attributes[:initial_dead_at] = options[:dead_at] if options[:dead_at]
  ownership = current_ownership
  if ownership && !ownership.unknown?
    attributes[:initial_owner] ||= ownership.owner
  end
  enjoyment = current_enjoyment
  if enjoyment && !enjoyment.unknown?
    attributes[:initial_enjoyer] ||= enjoyment.enjoyer
  end
  localization = current_localization
  if localization && localization.interior?
    attributes[:initial_container] ||= localization.container
  end
  matching_model.new(attributes)
end

#part_with!(population, options = {}) ⇒ Object

Create a new product parted from self See part!


608
609
610
611
612
# File 'app/models/product.rb', line 608

def part_with!(population, options = {})
  product = part_with(population, options)
  product.save!
  product
end

#picture_path(style = :original) ⇒ Object


584
585
586
# File 'app/models/product.rb', line 584

def picture_path(style = :original)
  picture.path(style)
end

#population(options = {}) ⇒ Object


527
528
529
530
531
532
533
534
# File 'app/models/product.rb', line 527

def population(options = {})
  movements = self.movements.at(options[:at] || Time.zone.now)
  if movements.any?
    return movements.last.population
  else
    return 0.0
  end
end

#price(options = {}) ⇒ Object

Returns the price for the product. It's a shortcut for CatalogItem::give


459
460
461
# File 'app/models/product.rb', line 459

def price(options = {})
  CatalogItem.price(self, options)
end

#set_default_valuesObject

Sets nature and variety from variant


424
425
426
427
428
429
430
431
432
433
# File 'app/models/product.rb', line 424

def set_default_values
  if variant
    self.nature_id = variant.nature_id
    self.variety ||= variant.variety
    if derivative_of.blank? && !variant.derivative_of.blank?
      self.derivative_of = variant.derivative_of
    end
  end
  self.category_id = nature.category_id if nature
end

#set_initial_valuesObject

set initial owner and localization


352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
# File 'app/models/product.rb', line 352

def set_initial_values
  # Add first owner on a product
  ownership = ownerships.first_of_all || ownerships.build
  ownership.owner = initial_owner
  ownership.save!

  # Add first enjoyer on a product
  enjoyment = enjoyments.first_of_all || enjoyments.build
  enjoyment.enjoyer = initial_enjoyer || initial_owner
  enjoyment.save!

  # Add first localization on a product
  localization = localizations.first_of_all || localizations.build
  localization.container = self.initial_container
  localization.save!

  if born_at
    # Configure initial_movement
    movement = initial_movement || build_initial_movement
    movement.product = self
    movement.delta = initial_population
    movement.started_at = born_at
    movement.save!

    # Initial shape
    if initial_shape && variable_indicators_list.include?(:shape)
      reading = initial_reading(:shape) || readings.new(indicator_name: :shape)
      reading.value = initial_shape
      reading.read_at = born_at
      reading.save!
      ProductReading.destroy readings.where.not(id: reading.id).where(indicator_name: :shape, read_at: reading.read_at).pluck(:id)
    end
  end

  # Add first frozen indicator on a product from his variant
  if variant
    phase = phases.first_of_all || phases.build
    phase.variant = variant
    phase.save!
    # set indicators from variant in products readings
    for f_v_indicator in variant.readings
      reading = readings.new(indicator_name: f_v_indicator.indicator_name)
      reading.value = f_v_indicator.value
      reading.read_at = born_at
      reading.save!
    end
  end
end

#unroll_nameObject


347
348
349
# File 'app/models/product.rb', line 347

def unroll_name
  'unrolls.backend/products'.t(attributes.symbolize_keys.merge(population: population, unit_name: unit_name))
end

#update_default_valuesObject

Update nature and variety and variant from phase


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

def update_default_values
  if current_phase
    phase_variant = current_phase.variant
    self.nature_id = phase_variant.nature_id
    self.variety ||= phase_variant.variety
    if derivative_of.blank? && !phase_variant.derivative_of.nil?
      self.derivative_of = phase_variant.derivative_of
    end
  end
  self.category_id = nature.category_id if nature
end

#variables(_options = {}) ⇒ Object

TODO: Doc


647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
# File 'app/models/product.rb', line 647

def variables(_options = {})
  list = []
  abilities = self.abilities
  variety       = Nomen::Variety[self.variety]
  derivative_of = Nomen::Variety[self.derivative_of]
  Procedo.each_variable do |variable|
    next if variable.new?
    if v = variable.computed_variety
      next unless variety <= v
    end
    if v = variable.computed_derivative_of
      next unless derivative_of && derivative_of <= v
    end
    next if variable.abilities.detect { |a| !able_to?(a) }
    list << variable
  end
  list
end

#work_nameObject

FIXME: Not I18nized


343
344
345
# File 'app/models/product.rb', line 343

def work_name
  "#{name} (#{work_number})"
end

#working_duration(options = {}) ⇒ Object

Returns working duration of a product


667
668
669
670
671
672
# File 'app/models/product.rb', line 667

def working_duration(options = {})
  role = options[:as] || :tool
  periods = InterventionWorkingPeriod.with_generic_cast(role, self)
  periods = periods.of_campaign(options[:campaign]) if options[:campaign]
  periods.sum(:duration)
end