Class: Product

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

Direct Known Subclasses

Matter, ProductGroup, Worker, Zone

Constant Summary

Constants included from Indicateable

Indicateable::DEPRECATED

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!, #mark!, #operate_on_readings, #read!, #read_whole_indicators_from!, #reading, #substract_and_read, #substract_to_readings

Methods inherited from Ekylibre::Record::Base

#already_updated?, #check_if_destroyable?, #check_if_updateable?, columns_definition, #customizable?, customizable?, #customized?, #destroyable?, #editable?, has_picture, #human_attribute_name, nomenclature_reflections, #old_record, #others, refers_to, #unsuppress, #updateable?

Methods included from Userstamp::Stampable

included

Methods included from Userstamp::Stamper

included

Class Method Details

.miscibility_of(products_and_variants) ⇒ Object


437
438
439
# File 'app/models/product.rb', line 437

def miscibility_of(products_and_variants)
  PhytosanitaryMiscibility.new(products_and_variants).legality
end

Instance Method Details

#activityObject


446
447
448
# File 'app/models/product.rb', line 446

def activity
  production ? production.activity : nil
end

#activity_idObject


450
451
452
# File 'app/models/product.rb', line 450

def activity_id
  activity ? activity.id : nil
end

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

add products to current container


681
682
683
684
685
686
687
688
689
690
# File 'app/models/product.rb', line 681

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


623
624
625
626
# File 'app/models/product.rb', line 623

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)

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

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

#best_activity_production(_options = {}) ⇒ Object


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

def best_activity_production(_options = {})
  activity_production
end

#born_at_in_interventionsObject


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

def born_at_in_interventions
  return true unless first_intervention = interventions_used_in.order(started_at: :asc).first
  first_used_at = first_intervention.started_at
  errors.add(:born_at, :on_or_before, restriction: first_used_at.l) if born_at > first_used_at
end

#build_new_phaseObject


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

def build_new_phase
  phases.build(
    product_id: id,
    variant_id: variant.id,
    category_id: variant.category.id,
    nature_id: variant.nature.id
  )

  save
end

#calculate_net_surface_area(options = {}) ⇒ Object

Override net_surface_area indicator to compute it from shape if product has shape indicator unless options :strict is given


840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
# File 'app/models/product.rb', line 840

def calculate_net_surface_area(options = {})
  # TODO: Manage global preferred surface unit or system
  area_unit = options[:unit] || :hectare
  if !options.keys.detect { |k| %i[gathering interpolate cast].include?(k) } &&
     has_indicator?(:shape) && !options[:compute].is_a?(FalseClass)
    unless options[:strict]
      options[:at] = born_at if born_at && born_at > Time.zone.now
    end
    shape = get(:shape, options)
    area = shape.area.in(:square_meter).in(area_unit).round(3) if shape
  else
    area = get(:net_surface_area, options)
  end
  area || 0.in(area_unit)
end

#choose_default_nameObject

Try to find the best name for the new products


560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
# File 'app/models/product.rb', line 560

def choose_default_name
  return if name.present?
  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


725
726
727
728
729
730
731
732
# File 'app/models/product.rb', line 725

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


704
705
706
707
708
709
# File 'app/models/product.rb', line 704

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)


712
713
714
715
716
717
718
719
720
721
722
723
# File 'app/models/product.rb', line 712

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


734
735
736
# File 'app/models/product.rb', line 734

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

#dead?Boolean

Returns:

  • (Boolean)

665
666
667
# File 'app/models/product.rb', line 665

def dead?
  dead_at.present?
end

#dead_at_in_interventionsObject


343
344
345
346
347
348
349
# File 'app/models/product.rb', line 343

def dead_at_in_interventions
  last_used_at = interventions.order(stopped_at: :desc).first.stopped_at
  if dead_at < last_used_at
    # puts ActivityProduction.find_by(support_id: self.id).id.green
    errors.add(:dead_at, :on_or_after, restriction: last_used_at.l)
  end
end

#dead_first_atObject


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

def dead_first_at
  list = issues.where(dead: true).order(:observed_at).limit(1).pluck(:observed_at) +
         intervention_product_parameters.where(dead: true).joins(:intervention).order('interventions.stopped_at').limit(1).pluck('interventions.stopped_at')
  list.any? ? list.min : nil
end

#default_catalog_item(usage) ⇒ Object

Returns item from default catalog for given usage


629
630
631
632
# File 'app/models/product.rb', line 629

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

#deliverable?Boolean

TODO: Removes this ASAP

Returns:

  • (Boolean)

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

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


639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
# File 'app/models/product.rb', line 639

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

#get(indicator, *args) ⇒ Object


860
861
862
863
864
865
866
867
868
869
870
# File 'app/models/product.rb', line 860

def get(indicator, *args)
  return super if args.any?(&:present?)
  in_cache = reading_cache[indicator.to_s]
  return in_cache if in_cache
  indicator_value = super
  reading_cache[indicator.to_s] = indicator_value
  unless new_record?
    update_column(:reading_cache, reading_cache.merge(indicator.to_s => indicator_value))
  end
  indicator_value
end

#groups_at(viewed_at = nil) ⇒ Object

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


676
677
678
# File 'app/models/product.rb', line 676

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

#initial_reading(indicator_name) ⇒ Object


607
608
609
# File 'app/models/product.rb', line 607

def initial_reading(indicator_name)
  first_reading(indicator_name)
end

#initial_shape_areaObject


856
857
858
# File 'app/models/product.rb', line 856

def initial_shape_area
  ::Charta.new_geometry(initial_shape).area.in_square_meter
end

#initializeable?Boolean

Returns:

  • (Boolean)

806
807
808
# File 'app/models/product.rb', line 806

def initializeable?
  new_record? || !(parcel_items.any? || InterventionParameter.of_generic_roles(%i[input output target doer tool]).of_actor(self).any? || fixed_assets.any?)
end

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

Returns all contained products of the given variant


751
752
753
754
# File 'app/models/product.rb', line 751

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


612
613
614
# File 'app/models/product.rb', line 612

def matching_model
  ProductNature.matching_model(self.variety)
end

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

Moves population with given quantity


699
700
701
# File 'app/models/product.rb', line 699

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

#nature_nameObject


467
468
469
# File 'app/models/product.rb', line 467

def nature_name
  nature ? nature.name : nil
end

#net_surface_areaObject


830
831
832
833
834
835
836
# File 'app/models/product.rb', line 830

def net_surface_area
  computed_surface = reading_cache[:net_surface_area] || reading_cache['net_surface_area']
  return computed_surface if computed_surface
  calculated = calculate_net_surface_area
  update(reading_cache: reading_cache.merge(net_surface_area: calculated))
  self.net_surface_area = calculated
end

#ownerObject

Returns the current ownership for the product


739
740
741
742
743
744
# File 'app/models/product.rb', line 739

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


781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
# File 'app/models/product.rb', line 781

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!


772
773
774
775
776
# File 'app/models/product.rb', line 772

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

#picture_path(style = :original) ⇒ Object


746
747
748
# File 'app/models/product.rb', line 746

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

#population(options = {}) ⇒ Object


692
693
694
695
696
# File 'app/models/product.rb', line 692

def population(options = {})
  pops = populations.last_before(options[:at] || Time.zone.now)
  return 0.0 if pops.none?
  pops.first.value
end

#price(options = {}) ⇒ Object

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


618
619
620
# File 'app/models/product.rb', line 618

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

#production(_at = nil) ⇒ Object


442
443
444
# File 'app/models/product.rb', line 442

def production(_at = nil)
  activity_production
end

#read_store_attribute(store_attribute, key) ⇒ Object

[DEPRECATIONS[

- fixed_asset_id

]DEPRECATIONS]


328
329
330
331
332
333
334
335
# File 'app/models/product.rb', line 328

def read_store_attribute(store_attribute, key)
  store = send(store_attribute)
  if store.key?(key)
    super
  else
    get(key)
  end
end

#set_default_valuesObject

Sets nature and variety from variant


582
583
584
585
586
587
588
589
590
591
# File 'app/models/product.rb', line 582

def set_default_values
  if variant
    self.nature_id = variant.nature_id
    self.variety ||= variant.variety
    if derivative_of.blank? && variant.derivative_of.present?
      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 after_save


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
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
# File 'app/models/product.rb', line 489

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!

  # 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
    variant.readings.each do |variant_reading|
      reading = readings.first_of_all(variant_reading.indicator_name) ||
      readings.new(indicator_name: variant_reading.indicator_name)
      reading.value = variant_reading.value
      reading.read_at = born_at
      reading.save!
    end
  end

  if born_at
    # Configure initial_movement
    movement = initial_movement || build_initial_movement
    movement.product = self
    movement.delta = !!initial_population && variant.population_counting_unitary? ? 1 : initial_population
    movement.started_at = born_at
    movement.save!
    update_column(:initial_movement_id, movement.id)

    # 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
end

#shape=(new_shape) ⇒ Object


552
553
554
555
556
557
# File 'app/models/product.rb', line 552

def shape=(new_shape)
  reading_cache[:shape] = new_shape
  reading_cache[:net_surface_area] = calculate_net_surface_area

  shape
end

#stock_infoObject


872
873
874
875
# File 'app/models/product.rb', line 872

def stock_info
  info = "#{self.population.round(2)} #{self.variant.unit_name.downcase}"
  info
end

#unroll_nameObject


483
484
485
# File 'app/models/product.rb', line 483

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


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

def update_default_values
  if current_phase
    phase_variant = current_phase.variant
    return if phase_variant.nil?
    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


811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
# File 'app/models/product.rb', line 811

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


471
472
473
474
475
476
477
478
479
480
481
# File 'app/models/product.rb', line 471

def work_name
  if work_number.present?
    # FIXME: Not I18nized
    name.to_s + ' (' + work_number.to_s + ')'
  elsif identification_number.present?
    # FIXME: Not I18nized
    name.to_s + ' (' + identification_number.to_s + ')'
  else
    name
  end
end