Class: ProductNatureVariant

Inherits:
Ekylibre::Record::Base show all
Includes:
Attachable, Customizable
Defined in:
app/models/product_nature_variant.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-2019 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: product_nature_variants

active                    :boolean          default(FALSE), not null
category_id               :integer          not null
created_at                :datetime         not null
creator_id                :integer
custom_fields             :jsonb
derivative_of             :string
france_maaid              :string
gtin                      :string
id                        :integer          not null, primary key
lock_version              :integer          default(0), not null
name                      :string
nature_id                 :integer          not null
number                    :string           not null
picture_content_type      :string
picture_file_name         :string
picture_file_size         :integer
picture_updated_at        :datetime
providers                 :jsonb
reference_name            :string
stock_account_id          :integer
stock_movement_account_id :integer
unit_name                 :string           not null
updated_at                :datetime         not null
updater_id                :integer
variety                   :string           not null
work_number               :string

Defined Under Namespace

Classes: ItemStruct

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Customizable

#custom_value, #set_custom_value, #validate_custom_fields

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

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(method_name, *args) ⇒ Object

Get indicator value if option :at specify at which moment if option :reading is true, it returns the ProductNatureVariantReading record if option :interpolate is true, it returns the interpolated value :interpolate and :reading options are incompatible


375
376
377
378
# File 'app/models/product_nature_variant.rb', line 375

def method_missing(method_name, *args)
  return super unless Nomen::Indicator.items[method_name]
  get(method_name)
end

Class Method Details

.any_reference_available?Boolean

Returns some nomenclature items are available to be imported, e.g. not already imported

Returns:

  • (Boolean)

498
499
500
# File 'app/models/product_nature_variant.rb', line 498

def any_reference_available?
  Nomen::ProductNatureVariant.without(ProductNatureVariant.pluck(:reference_name).uniq).any?
end

.find_or_import!(variety, options = {}) ⇒ Object

Find or import variant from nomenclature with given attributes variety and derivative_of only are accepted for now


504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
# File 'app/models/product_nature_variant.rb', line 504

def find_or_import!(variety, options = {})
  variants = of_variety(variety)
  if derivative_of = options[:derivative_of]
    variants = variants.derivative_of(derivative_of)
  end
  if variants.empty?
    # Filter and imports
    filtereds = flattened_nomenclature.select do |item|
      item.variety >= variety &&
        ((derivative_of && item.derivative_of && item.derivative_of >= derivative_of) || (derivative_of.blank? && item.derivative_of.blank?))
    end
    filtereds.each do |item|
      import_from_nomenclature(item.name)
    end
  end
  variants.reload
end

.flattened_nomenclatureObject

Returns core attributes of nomenclature merge with nature if necessary name, variety, derivative_od, abilities


526
527
528
529
530
531
532
533
534
535
536
537
538
539
# File 'app/models/product_nature_variant.rb', line 526

def flattened_nomenclature
  @flattened_nomenclature ||= Nomen::ProductNatureVariant.list.collect do |item|
    nature = Nomen::ProductNature[item.nature]
    f = (nature.frozen_indicators || []).map(&:to_sym)
    v = (nature.variable_indicators || []).map(&:to_sym)
    ItemStruct.new(
      item.name,
      Nomen::Variety.find(item.variety || nature.variety),
      Nomen::Variety.find(item.derivative_of || nature.derivative_of),
      WorkingSet::AbilityArray.load(nature.abilities),
      f + v, f, v
    )
  end
end

.import_from_nomenclature(reference_name, force = false) ⇒ Object

Load a product nature variant from product nature variant nomenclature


550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
# File 'app/models/product_nature_variant.rb', line 550

def import_from_nomenclature(reference_name, force = false)
  unless item = Nomen::ProductNatureVariant[reference_name]
    raise ArgumentError, "The product_nature_variant #{reference_name.inspect} is not known"
  end
  unless nature_item = Nomen::ProductNature[item.nature]
    raise ArgumentError, "The nature of the product_nature_variant #{item.nature.inspect} is not known"
  end
  unless !force && (variant = ProductNatureVariant.find_by(reference_name: reference_name.to_s))
    variant = new(
      name: item.human_name,
      active: true,
      nature: ProductNature.import_from_nomenclature(item.nature),
      reference_name: item.name,
      unit_name: I18n.translate("nomenclatures.product_nature_variants.choices.unit_name.#{item.unit_name}"),
      # :frozen_indicators => item.frozen_indicators_values.to_s,
      variety: item.variety || nil,
      derivative_of: item.derivative_of || nil
    )
    unless variant.save
      raise "Cannot import variant #{reference_name.inspect}: #{variant.errors.full_messages.join(', ')}"
    end
  end

  if item.frozen_indicators_values.to_s.present?
    # create frozen indicator for each pair indicator, value ":population => 1unity"
    item.frozen_indicators_values.to_s.strip.split(/[[:space:]]*\,[[:space:]]*/)
        .collect { |i| i.split(/[[:space:]]*\:[[:space:]]*/) }.each do |i|
      indicator_name = i.first.strip.downcase.to_sym
      next unless variant.has_indicator? indicator_name
      variant.read!(indicator_name, i.second)
    end
  end

  variant
end

.items_of_expression(expression) ⇒ Object

Lists ProductNatureVariant::Item which match given expression Fully compatible with WSQL


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

def items_of_expression(expression)
  flattened_nomenclature.select do |item|
    WorkingSet.check_record(expression, item)
  end
end

.load_defaults(_options = {}) ⇒ Object


586
587
588
589
590
# File 'app/models/product_nature_variant.rb', line 586

def load_defaults(_options = {})
  Nomen::ProductNatureVariant.all.flatten.collect do |p|
    import_from_nomenclature(p.to_s)
  end
end

Instance Method Details

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

add animals to new variant


239
240
241
242
243
244
245
246
247
248
# File 'app/models/product_nature_variant.rb', line 239

def add_products(products, options = {})
  Intervention.write(:product_evolution, options) do |i|
    i.cast :variant, self, as: 'product_evolution-variant'
    products.each do |p|
      product = (p.is_a?(Product) ? p : Product.find(p))
      member = i.cast :product, product, as: 'product_evolution-target'
      i.variant_cast :variant, member
    end
  end
end

#contractual_pricesObject


362
363
364
365
366
367
368
# File 'app/models/product_nature_variant.rb', line 362

def contractual_prices
  contract_items
    .pluck(:contract_id, :unit_pretax_amount)
    .to_h
    .map { |contract_id, price| [Contract.find(contract_id), price] }
    .to_h
end

#create_product(attributes = {}) ⇒ Object


397
398
399
400
# File 'app/models/product_nature_variant.rb', line 397

def create_product(attributes = {})
  attributes = product_params(attributes)
  matching_model.create(attributes.merge(variant: self))
end

#create_product!(attributes = {}) ⇒ Object

Shortcut for creating a new product of the variant


392
393
394
395
# File 'app/models/product_nature_variant.rb', line 392

def create_product!(attributes = {})
  attributes = product_params(attributes)
  matching_model.create!(attributes.merge(variant: self))
end

#create_unique_account(mode = :stock) ⇒ Object

create unique account for stock management in accountancy


220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
# File 'app/models/product_nature_variant.rb', line 220

def (mode = :stock)
   = mode.to_s + '_account'

   = category.send()
  unless 
    # We want to notice => raise.
    raise "Account '#{}' is not configured on category of #{self.name.inspect} variant. You have to check category first"
  end

  options = {}
  options[:number] = .number + number[-6, 6].rjust(6)
  options[:name] = .name + ' [' + self.name + ']'
  options[:label] = options[:number] + ' - ' + options[:name]
  options[:usages] = .usages

  Account.create!(options)
end

#current_outgoing_stock_ordered_not_deliveredObject

Return current quantity of all products link to the variant currently ordered or invoiced but not delivered


460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
# File 'app/models/product_nature_variant.rb', line 460

def current_outgoing_stock_ordered_not_delivered
  undelivereds = sale_items.includes(:sale).map do |si|
    undelivered = 0
    variants_in_parcel_in_sale = ShipmentItem.where(parcel_id: si.sale.parcels.select(:id), variant: self)
    variants_in_transit_parcel_in_sale = ShipmentItem.where(parcel_id: si.sale.parcels.where.not(state: %i[given draft]).select(:id), variant: self)
    delivered_variants_in_parcel_in_sale = ShipmentItem.where(parcel_id: si.sale.parcels.where(state: :given).select(:id), variant: self)

    undelivered = si.quantity if variants_in_parcel_in_sale.none? && !si.sale.draft? && !si.sale.refused? && !si.sale.aborted?
    undelivered = [undelivered, si.quantity - delivered_variants_in_parcel_in_sale.sum(:population)].max if variants_in_parcel_in_sale.present?
    sale_not_canceled = (si.sale.draft? || si.sale.estimate? || si.sale.order? || si.sale.invoice?)
    undelivered = [undelivered, variants_in_transit_parcel_in_sale.sum(:population)].max if sale_not_canceled && variants_in_transit_parcel_in_sale.present?

    undelivered
  end

  undelivereds += shipment_items.joins(:shipment).where.not(parcels: { state: %i[given draft] }).where(parcels: { sale_id: nil, nature: :outgoing }).pluck(:population)

  undelivereds.compact.sum
end

#current_outgoing_stock_ordered_not_delivered_displayedObject


480
481
482
# File 'app/models/product_nature_variant.rb', line 480

def current_outgoing_stock_ordered_not_delivered_displayed
  variety == 'service' ? '' : current_outgoing_stock_ordered_not_delivered
end

#current_stockObject

Return current stock of all products link to the variant


442
443
444
445
446
447
448
449
450
451
452
453
# File 'app/models/product_nature_variant.rb', line 442

def current_stock
  if variety == 'service'
    quantity_purchased - quantity_received
  else
    products
      .select { |product| product.dead_at.nil? || product.dead_at >= Time.now }
      .map(&:population)
      .compact
      .sum
      .to_f
  end
end

#current_stock_displayedObject


455
456
457
# File 'app/models/product_nature_variant.rb', line 455

def current_stock_displayed
  variety == 'service' ? '' : current_stock
end

#current_stock_per_storage(storage) ⇒ Object


488
489
490
491
492
493
# File 'app/models/product_nature_variant.rb', line 488

def current_stock_per_storage(storage)
  ParcelItemStoring.where(storage: storage)
                   .joins(:parcel_item)
                   .where(parcel_items: { variant_id: self.id })
                   .sum(:quantity)
end

#default_catalog_item(usage) ⇒ Object

Returns item from default catalog for given usage


297
298
299
300
# File 'app/models/product_nature_variant.rb', line 297

def default_catalog_item(usage)
  catalog = Catalog.by_default!(usage)
  catalog.items.find_by(variant: self)
end

#generate(*args) ⇒ Object


380
381
382
383
384
385
386
387
388
389
# File 'app/models/product_nature_variant.rb', line 380

def generate(*args)
  options = args.extract_options!
  product_name = args.shift || options[:name]
  born_at = args.shift || options[:born_at]
  default_storage = args.shift || options[:default_storage]

  product_model = nature.matching_model

  product_model.create!(variant: self, name: product_name + ' ' + born_at.l, initial_owner: Entity.of_company, initial_born_at: born_at, default_storage: default_storage)
end

#get(indicator, _options = {}) ⇒ Object

Returns the direct value of an indicator of variant


273
274
275
276
277
278
279
280
281
282
283
284
285
# File 'app/models/product_nature_variant.rb', line 273

def get(indicator, _options = {})
  unless indicator.is_a?(Nomen::Item) || indicator = Nomen::Indicator[indicator]
    raise ArgumentError, "Unknown indicator #{indicator.inspect}. Expecting one of them: #{Nomen::Indicator.all.sort.to_sentence}."
  end
  if reading = reading(indicator.name)
    return reading.value
  elsif indicator.datatype == :measure
    return 0.0.in(indicator.unit)
  elsif indicator.datatype == :decimal
    return 0.0
  end
  nil
end

#has_frozen_indicator?(indicator) ⇒ Boolean

check if a variant has an indicator which is frozen or not

Returns:

  • (Boolean)

288
289
290
291
292
293
294
# File 'app/models/product_nature_variant.rb', line 288

def has_frozen_indicator?(indicator)
  if indicator.is_a?(Nomen::Item)
    frozen_indicators.include?(indicator)
  else
    frozen_indicators_list.include?(indicator)
  end
end

#last_purchase_item_for(supplier = nil) ⇒ Object

Returns last purchase item for the variant and a given supplier if any, or nil if there's no purchase item matching criterias


425
426
427
428
429
430
431
# File 'app/models/product_nature_variant.rb', line 425

def last_purchase_item_for(supplier = nil)
  return purchase_items.last if supplier.blank?
  purchase_items
    .joins(:purchase)
    .where('purchases.supplier_id = ?', Entity.find(supplier).id)
    .last
end

#picture_path(style = :original) ⇒ Object


484
485
486
# File 'app/models/product_nature_variant.rb', line 484

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

#product_params(attributes = {}) ⇒ Object


402
403
404
405
406
407
408
# File 'app/models/product_nature_variant.rb', line 402

def product_params(attributes = {})
  attributes[:initial_owner] ||= Entity.of_company
  attributes[:initial_born_at] ||= Time.zone.now
  attributes[:born_at] ||= attributes[:initial_born_at]
  attributes[:name] ||= "#{name} (#{attributes[:initial_born_at].to_date.l})"
  attributes
end

#quantifiersObject

Returns a list of couple indicator/unit usable for the given variant The result is only based on measure indicators


304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
# File 'app/models/product_nature_variant.rb', line 304

def quantifiers
  list = []
  indicators.each do |indicator|
    next unless indicator.gathering == :proportional_to_population
    if indicator.datatype == :measure
      Measure.siblings(indicator.unit).each do |unit_name|
        list << "#{indicator.name}/#{unit_name}"
      end
    elsif indicator.datatype == :integer || indicator.datatype == :decimal
      list << indicator.name.to_s
    end
  end
  variety = Nomen::Variety.find(self.variety)
  # Specials indicators
  if variety <= :product_group
    list << 'members_count' unless list.include?('members_count/unity')
    if variety <= :animal_group
      list << 'members_livestock_unit' unless list.include?('members_livestock_unit/unity')
    end
    list << 'members_population' unless list.include?('members_population/unity')
  end
  list
end

#quantity_purchasedObject


433
434
435
# File 'app/models/product_nature_variant.rb', line 433

def quantity_purchased
  purchase_items.sum(:quantity)
end

#quantity_receivedObject


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

def quantity_received
  reception_items.joins(:reception).where(parcels: { state: :given }).sum(:population)
end

#read!(indicator, value) ⇒ Object

Measure a product for a given indicator


251
252
253
254
255
256
257
258
259
260
261
262
# File 'app/models/product_nature_variant.rb', line 251

def read!(indicator, value)
  unless indicator.is_a?(Nomen::Item)
    indicator = Nomen::Indicator.find(indicator)
    unless indicator
      raise ArgumentError, "Unknown indicator #{indicator.inspect}. Expecting one of them: #{Nomen::Indicator.all.sort.to_sentence}."
    end
  end
  reading = readings.find_or_initialize_by(indicator_name: indicator.name)
  reading.value = value
  reading.save!
  reading
end

#reading(indicator) ⇒ Object

Return the reading


265
266
267
268
269
270
# File 'app/models/product_nature_variant.rb', line 265

def reading(indicator)
  unless indicator.is_a?(Nomen::Item) || indicator = Nomen::Indicator[indicator]
    raise ArgumentError, "Unknown indicator #{indicator.inspect}. Expecting one of them: #{Nomen::Indicator.all.sort.to_sentence}."
  end
  readings.find_by(indicator_name: indicator.name)
end

#take(quantity) ⇒ Object


410
411
412
413
414
415
416
# File 'app/models/product_nature_variant.rb', line 410

def take(quantity)
  products.mine.each_with_object({}) do |product, result|
    reminder = quantity - result.values.sum
    result[product] = [product.population, reminder].min if reminder > 0
    result
  end
end

#take!(quantity) ⇒ Object


418
419
420
# File 'app/models/product_nature_variant.rb', line 418

def take!(quantity)
  raise 'errors.not_enough'.t if take(quantity).values.sum < quantity
end

#unified_quantifiers(options = {}) ⇒ Object

Returns a list of quantifier


329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
# File 'app/models/product_nature_variant.rb', line 329

def unified_quantifiers(options = {})
  list = quantifiers.map do |quantifier|
    pair = quantifier.split('/')
    indicator = Nomen::Indicator.find(pair.first)
    unit = (pair.second.blank? ? nil : Nomen::Unit.find(pair.second))
    hash = { indicator: { name: indicator.name, human_name: indicator.human_name } }
    hash[:unit] = if unit
                    { name: unit.name, symbol: unit.symbol, human_name: unit.human_name }
                  elsif indicator.name =~ /^members\_/
                    unit = Nomen::Unit.find(:unity)
                    { name: '', symbol: unit.symbol, human_name: unit.human_name }
                  else
                    { name: '', symbol: unit_name, human_name: unit_name }
                  end
    hash
  end

  # Add population
  if options[:population]
    # indicator = Nomen::Indicator[:population]
    list << { indicator: { name: :population, human_name: Product.human_attribute_name(:population) }, unit: { name: '', symbol: unit_name, human_name: unit_name } }
  end

  # Add working duration (intervention durations)
  if options[:working_duration]
    Nomen::Unit.where(dimension: :time).find_each do |unit|
      list << { indicator: { name: :working_duration, human_name: :working_duration.tl }, unit: { name: unit.name, symbol: unit.symbol, human_name: unit.human_name } }
    end
  end

  list
end