-
Notifications
You must be signed in to change notification settings - Fork 154
/
product.rb
1032 lines (903 loc) · 40.2 KB
/
product.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
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
241
242
243
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
270
271
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
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
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
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
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
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
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
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
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
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
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# frozen_string_literal: true
# = Informations
#
# == License
#
# Ekylibre - Simple agricultural ERP
# Copyright (C) 2008-2009 Brice Texier, Thibaud Merigon
# Copyright (C) 2010-2012 Brice Texier
# Copyright (C) 2012-2014 Brice Texier, David Joulin
# Copyright (C) 2015-2023 Ekylibre SAS
#
# 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 http://www.gnu.org/licenses.
#
# == Table: products
#
# activity_production_id :integer(4)
# address_id :integer(4)
# birth_date_completeness :string
# birth_farm_number :string
# born_at :datetime
# category_id :integer(4) not null
# codes :jsonb
# conditioning_unit_id :integer(4)
# country :string
# created_at :datetime not null
# creator_id :integer(4)
# custom_fields :jsonb
# dead_at :datetime
# default_storage_id :integer(4)
# derivative_of :string
# description :text
# end_of_life_reason :string
# father_country :string
# father_identification_number :string
# father_variety :string
# filiation_status :string
# first_calving_on :datetime
# fixed_asset_id :integer(4)
# id :integer(4) not null, primary key
# identification_number :string
# initial_born_at :datetime
# initial_container_id :integer(4)
# initial_dead_at :datetime
# initial_enjoyer_id :integer(4)
# initial_father_id :integer(4)
# initial_geolocation :geometry({:srid=>4326, :type=>"st_point"})
# initial_mother_id :integer(4)
# initial_movement_id :integer(4)
# initial_owner_id :integer(4)
# initial_population :decimal(19, 4) default(0.0)
# initial_shape :geometry({:srid=>4326, :type=>"multi_polygon"})
# isacompta_analytic_code :string(2)
# lock_version :integer(4) default(0), not null
# member_variant_id :integer(4)
# mother_country :string
# mother_identification_number :string
# mother_variety :string
# name :string not null
# nature_id :integer(4) not null
# number :string not null
# origin_country :string
# origin_identification_number :string
# originator_id :integer(4)
# parent_id :integer(4)
# person_id :integer(4)
# picture_content_type :string
# picture_file_name :string
# picture_file_size :integer(4)
# picture_updated_at :datetime
# provider :jsonb default("{}")
# reading_cache :jsonb default("{}")
# specie_variety :jsonb default("{}")
# team_id :integer(4)
# tracking_id :integer(4)
# type :string
# type_of_occupancy :string
# updated_at :datetime not null
# updater_id :integer(4)
# uuid :uuid
# variant_id :integer(4) not null
# variety :string not null
# work_number :string
# worker_group_item_id :integer(4)
#
require 'ffaker'
class Product < ApplicationRecord
include Attachable
include Autocastable
include Indicateable
include Versionable
include Customizable
refers_to :variety
refers_to :derivative_of, class_name: 'Variety'
belongs_to :address, class_name: 'EntityAddress'
belongs_to :category, class_name: 'ProductNatureCategory'
belongs_to :default_storage, class_name: 'Product'
belongs_to :initial_container, class_name: 'Product'
belongs_to :initial_enjoyer, class_name: 'Entity'
belongs_to :initial_movement, class_name: 'ProductMovement'
belongs_to :initial_father, class_name: 'Product'
belongs_to :initial_mother, class_name: 'Product'
belongs_to :initial_owner, class_name: 'Entity'
belongs_to :nature, class_name: 'ProductNature'
belongs_to :parent, class_name: 'Product'
belongs_to :person, -> { contacts }, class_name: 'Entity'
belongs_to :tracking
belongs_to :variant, class_name: 'ProductNatureVariant'
belongs_to :activity_production
belongs_to :member_variant, class_name: 'ProductNatureVariant'
belongs_to :conditioning_unit, class_name: 'Unit'
has_many :activity_productions, foreign_key: :support_id
has_many :analyses, class_name: 'Analysis', dependent: :restrict_with_exception
has_many :carrier_linkages, class_name: 'ProductLinkage', foreign_key: :carried_id, dependent: :destroy
has_many :content_localizations, class_name: 'ProductLocalization', foreign_key: :container_id
has_many :contents, class_name: 'Product', through: :content_localizations, source: :product
has_many :enjoyments, class_name: 'ProductEnjoyment', foreign_key: :product_id, dependent: :destroy
has_many :fixed_assets, inverse_of: :product
# has_many :groups, :through => :memberships
has_many :issues, as: :target, dependent: :destroy
has_many :intervention_product_parameters, -> { unscope(where: :type).of_generic_roles(%i[input output target doer tool]) }, foreign_key: :product_id, inverse_of: :product, dependent: :restrict_with_exception
has_many :interventions, -> { distinct }, through: :intervention_product_parameters
has_many :used_intervention_parameters, -> { unscope(where: :type).of_generic_roles(%i[input target doer tool]) }, foreign_key: :product_id, inverse_of: :product, dependent: :restrict_with_exception, class_name: 'InterventionProductParameter'
has_many :interventions_used_in, through: :used_intervention_parameters, source: :intervention
has_many :labellings, class_name: 'ProductLabelling', dependent: :destroy, inverse_of: :product
has_many :labels, through: :labellings
has_many :linkages, class_name: 'ProductLinkage', foreign_key: :carrier_id, dependent: :destroy
has_many :links, class_name: 'ProductLink', foreign_key: :product_id, dependent: :destroy
has_many :localizations, class_name: 'ProductLocalization', foreign_key: :product_id, dependent: :destroy
has_many :memberships, class_name: 'ProductMembership', foreign_key: :member_id, dependent: :destroy
has_many :movements, class_name: 'ProductMovement', foreign_key: :product_id, dependent: :destroy
has_many :populations, class_name: 'ProductPopulation', foreign_key: :product_id, dependent: :destroy
has_many :ownerships, class_name: 'ProductOwnership', foreign_key: :product_id, dependent: :destroy
has_many :inspections, class_name: 'Inspection', foreign_key: :product_id, dependent: :destroy
has_many :inventory_items, dependent: :restrict_with_exception
has_many :parcel_item_storings, foreign_key: :product_id
has_many :parcel_items, through: :parcel_item_storings, dependent: :restrict_with_exception
has_many :phases, class_name: 'ProductPhase', dependent: :destroy
has_many :intervention_participations, class_name: 'InterventionParticipation', dependent: :destroy
has_many :sensors
has_many :supports, class_name: 'ActivityProduction', foreign_key: :support_id, inverse_of: :support
has_many :trackings, class_name: 'Tracking', foreign_key: :product_id, inverse_of: :product
has_many :variants, class_name: 'ProductNatureVariant', through: :phases
has_many :purchase_items, class_name: 'PurchaseItem', inverse_of: :equipment, foreign_key: :equipment_id
has_one :current_phase, -> { current }, class_name: 'ProductPhase', foreign_key: :product_id
has_one :current_localization, -> { current }, class_name: 'ProductLocalization', foreign_key: :product_id
has_one :current_enjoyment, -> { current }, class_name: 'ProductEnjoyment', foreign_key: :product_id
has_one :current_ownership, -> { current }, class_name: 'ProductOwnership', foreign_key: :product_id
has_one :owner, through: :current_ownership
has_many :current_memberships, -> { current }, class_name: 'ProductMembership', foreign_key: :member_id
has_one :container, through: :current_localization
has_many :groups, through: :current_memberships
has_many :crop_group_items, foreign_key: :crop_id
has_many :crop_groups, through: :crop_group_items
has_many :intervention_targets, inverse_of: :product
has_many :crop_group_items, foreign_key: :crop_id
has_many :crop_groups, through: :crop_group_items
has_many :intervention_targets, inverse_of: :product
has_many :crop_group_items, foreign_key: :crop_id
has_many :crop_groups, through: :crop_group_items
has_many :worker_groups, through: :worker_group_items
has_many :worker_group_items, foreign_key: :worker_id
has_many :catalog_items, foreign_key: :product_id, dependent: :destroy
# FIXME: These reflections are meaningless. Will be removed soon or later.
has_one :incoming_parcel_item, -> { with_nature(:incoming) }, class_name: 'ReceptionItem', foreign_key: :product_id, inverse_of: :product
has_one :outgoing_parcel_item, -> { with_nature(:outgoing) }, class_name: 'ShipmentItem', foreign_key: :product_id, inverse_of: :product
has_one :incoming_parcel_item_storing, class_name: 'ParcelItemStoring', foreign_key: :product_id, inverse_of: :product
has_one :last_intervention_target, -> { order(id: :desc).limit(1) }, class_name: 'InterventionTarget'
has_picture
has_geometry :initial_shape, type: :multi_polygon
has_geometry :initial_geolocation, type: :point
enumerize :type_of_occupancy, in: %i[owner rent sharecropper], predicates: true
serialize :specie_variety, HashSerializer
store_accessor :specie_variety, :specie_variety_name
# find Product by work_numbers (work_numbers must be an Array)
scope :of_work_numbers, lambda { |work_numbers|
where(work_number: work_numbers)
}
scope :members_of, lambda { |group, viewed_at|
where(id: ProductMembership.select(:member_id).where(group_id: group.id, nature: 'interior').at(viewed_at))
}
scope :of_variant_active, -> { where(variant: ProductNatureVariant.active) }
scope :members_of_place, ->(place, viewed_at) { contained_by(place, viewed_at) }
scope :contained_by, lambda { |container, viewed_at = Time.zone.now|
where(id: ProductLocalization.select(:product_id).where(container: container).at(viewed_at))
}
scope :derivative_of, ->(*varieties) { of_derivative_of(*varieties) }
scope :can, lambda { |*abilities|
of_expression(abilities.map { |a| "can #{a}" }.join(' or '))
}
scope :can_each, lambda { |*abilities|
of_expression(abilities.map { |a| "can #{a}" }.join(' and '))
}
scope :of_working_set, lambda { |working_set|
item = Onoma::WorkingSet.find(working_set)
raise StandardError.new("#{working_set.inspect} is not in Onoma::WorkingSet nomenclature") unless item
of_expression(item.expression)
}
scope :of_expression, lambda { |expression|
joins(:nature).where(WorkingSet.to_sql(expression, default: :products, abilities: :product_natures, indicators: :product_natures))
}
scope :of_nature, ->(nature) { where(nature_id: nature.try(:id) || nature) }
scope :of_variant, lambda { |variant, _at = Time.zone.now|
where(variant_id: (variant.is_a?(ProductNatureVariant) ? variant.id : variant))
}
scope :at, ->(at) { where(arel_table[:born_at].lteq(at).and(arel_table[:dead_at].eq(nil).or(arel_table[:dead_at].gteq(at)))) }
scope :of_owner, lambda { |owner|
if owner.is_a?(Symbol)
joins(:current_ownership).where(product_ownerships: { nature: owner })
else
joins(:current_ownership).where(product_ownerships: { owner_id: owner.id })
end
}
scope :of_enjoyer, lambda { |enjoyer|
if enjoyer.is_a?(Symbol)
joins(:current_enjoyment).where(product_enjoyments: { nature: enjoyer })
else
joins(:current_enjoyment).where(product_enjoyments: { enjoyer_id: enjoyer.id })
end
}
scope :of_production, lambda { |production|
where(activity_production: production)
}
scope :of_productions, lambda { |*productions|
of_production(productions.flatten)
}
scope :of_crumbs, lambda { |*crumbs|
options = crumbs.extract_options!
crumbs.flatten!
raw_products = Product.distinct.joins(:readings)
.joins("INNER JOIN crumbs ON (product_readings.indicator_datatype = 'shape' AND ST_Contains(ST_CollectionExtract(product_readings.geometry_value, 3), crumbs.geolocation))")
.where(crumbs.any? ? ['crumbs.id IN (?)', crumbs.map(&:id)] : 'crumbs.id IS NOT NULL')
contents = []
contents = raw_products.map(&:contents) unless options[:no_contents]
raw_products.concat(contents).flatten.uniq
}
scope :generic_supports, -> { where(type: %w[Animal AnimalGroup Plant LandParcel Equipment EquipmentFleet]) }
scope :with_campaign, lambda { |campaign|
through_production = joins(activity_production: :campaign).where("campaigns.id = #{campaign.id}").select(:id)
through_productions = joins(activity_productions: :campaigns).where("campaigns.id = #{campaign.id}").select(:id)
where(arel_table[:id].in(through_productions.arel)
.or(arel_table[:id].in(through_production.arel)))
}
scope :supports_of_campaign, lambda { |campaign|
joins(:supports).distinct.merge(ActivityProduction.of_campaign(campaign))
}
scope :shape_intersecting, lambda { |shape|
where(id: ProductReading.multi_polygon_value_surface_intersecting(shape).select(:product_id))
}
scope :shape_within, lambda { |shape|
where(id: ProductReading.multi_polygon_value_within(shape).select(:product_id))
}
scope :shape_covering, lambda { |shape, margin = 0.02|
where(id: ProductReading.multi_polygon_value_covering(shape, margin).select(:product_id))
}
scope :shape_overlapping, lambda { |shape|
where(id: ProductReading.multi_polygon_value_overlapping(shape).select(:product_id))
}
scope :shape_matching, lambda { |shape, margin = 0.05|
where(id: ProductReading.multi_polygon_value_matching(shape, margin).select(:product_id))
}
# scope :saleables, -> { joins(:nature).where(:active => true, :product_natures => {:saleable => true}) }
scope :saleables, -> { joins(:category).merge(ProductNatureCategory.saleables) }
scope :deliverables, -> { joins(:category).merge(ProductNatureCategory.stockables) }
scope :depreciables, -> { joins(:category).merge(ProductNatureCategory.depreciables) }
scope :production_supports, -> { where(variety: ['cultivable_zone']) }
scope :supportables, -> { of_variety(%i[cultivable_zone animal_group equipment]) }
scope :supporters, -> { where(id: ActivityProduction.pluck(:support_id)) }
scope :available, -> {}
scope :availables, ->(**args) {
ActiveSupport::Deprecation.warn("Product#availables and Product#available are deprecated, use Product#at instead")
at = args[:at]
return available if at.blank?
if at.is_a?(String)
if at =~ /\A\d\d\d\d\-\d\d\-\d\d \d\d\:\d\d/
available.at(Time.strptime(at, '%Y-%m-%d %H:%M'))
else
logger.warn('Cannot parse: ' + at)
available
end
else
available.at(at)
end
}
scope :interventionables, ->(at: Time.now) {
at(at).joins(:activity_production).merge(ActivityProduction.at(at))
}
scope :actives, ->(**args) {
date = args[:at]
type = args[:type]
# TODO: Reversion
# product_ids = Larrere::ProductAvailability.at(date).where(available: true).select(:product_id)
# Product.availables(at: date).where(id: product_ids, type: type)
Product.availables(at: date).where(type: type)
}
scope :excluding, ->(*ids) {
where.not(id: ids)
}
scope :alive, ->(at: Time.now) { where('products.dead_at IS NULL OR products.dead_at >= ?', at) }
scope :identifiables, -> { where(nature: ProductNature.identifiables) }
scope :tools, -> { of_variety(:equipment) }
scope :support, -> { joins(:nature).merge(ProductNature.support) }
scope :storage, -> { of_expression('is building_division or can store(product) or can store_liquid or can store_fluid or can store_gaz') }
scope :plants, -> { where(type: 'Plant') }
scope :workers, -> { where(type: 'Worker') }
scope :land_parcels, -> { where(type: 'LandParcel') }
scope :building_divisions, -> { where(type: 'BuildingDivision') }
scope :crops, -> { where(type: %w[LandParcel Plant]) }
scope :animals, -> { where(type: 'Animal') }
scope :of_available_animal_group, -> { where(type: 'AnimalGroup', activity_production_id: nil) }
scope :land_parcel_alive, ->(at: Time.now) { joins(activity_production: :support).where('supports_activity_productions.dead_at IS NULL OR supports_activity_productions.dead_at >= ?', at) }
scope :fathers, -> { animals.indicate(sex: 'male', reproductor: true).order(:name) }
scope :mothers, -> { animals.indicate(sex: 'female', reproductor: true).order(:name) }
scope :mine, -> { of_owner(:own) }
scope :mine_or_undefined, ->(at = nil) {
at ||= Time.zone.now
where.not(id: ProductOwnership.select(:product_id).where(nature: :other).at(at))
}
scope :usable_in_fixed_asset, -> { depreciables.joins('LEFT JOIN fixed_assets ON products.id = fixed_assets.product_id').where('fixed_assets.id IS NULL') }
scope :with_id, lambda { |id|
where(id: id)
}
scope :of_activity_production, lambda { |activity_production|
where(activity_production: activity_production)
}
# [VALIDATORS[ Do not edit these lines directly. Use `rake clean:validations`.
validates :birth_date_completeness, :birth_farm_number, :country, :end_of_life_reason, :father_country, :father_identification_number, :father_variety, :filiation_status, :identification_number, :mother_country, :mother_identification_number, :mother_variety, :origin_country, :origin_identification_number, :picture_content_type, :picture_file_name, :work_number, length: { maximum: 500 }, allow_blank: true
validates :born_at, :dead_at, :first_calving_on, :initial_born_at, :initial_dead_at, :picture_updated_at, timeliness: { on_or_after: -> { Time.new(1, 1, 1).in_time_zone }, on_or_before: -> { Time.zone.now + 100.years } }, allow_blank: true
validates :description, length: { maximum: 500_000 }, allow_blank: true
validates :initial_population, numericality: { greater_than: -1_000_000_000_000_000, less_than: 1_000_000_000_000_000 }, allow_blank: true
validates :isacompta_analytic_code, length: { maximum: 2 }, allow_blank: true
validates :name, presence: true, length: { maximum: 500 }
validates :number, presence: true, uniqueness: true, length: { maximum: 500 }
validates :picture_file_size, numericality: { only_integer: true, greater_than: -2_147_483_649, less_than: 2_147_483_648 }, allow_blank: true
validates :category, :nature, :variant, :variety, presence: true
# ]VALIDATORS]
validates :derivative_of, :variety, length: { allow_nil: true, maximum: 120 }
validates :uuid, :conditioning_unit, presence: true
validates_attachment_content_type :picture, content_type: /image/
validate :born_at_in_interventions, if: ->(product) { product.born_at? && product.interventions_used_in.pluck(:started_at).any? }
validate :dead_at_in_interventions, if: ->(product) { product.dead_at? && product.interventions.pluck(:stopped_at).any? }
validates :conditioning_unit, conditioning: true
delegate :dimension, :of_dimension?, to: :unit
alias_attribute :unit, :conditioning_unit
store :reading_cache, accessors: Onoma::Indicator.all, coder: ReadingsCoder
# [DEPRECATIONS[
# - fixed_asset_id
# ]DEPRECATIONS]
def read_store_attribute(store_attribute, key)
store = send(store_attribute)
if store.key?(key)
super
else
get(key)
end
end
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
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
accepts_nested_attributes_for :readings, allow_destroy: true, reject_if: lambda { |reading|
!(reading['indicator_name'] != 'population') && reading[ProductReading.value_column(reading['indicator_name']).to_s].blank?
}
accepts_nested_attributes_for :memberships, reject_if: :all_blank, allow_destroy: true
accepts_nested_attributes_for :labellings, reject_if: :all_blank, allow_destroy: true
acts_as_numbered force: true
delegate :serial_number, :producer, to: :tracking
delegate :variety, :derivative_of, :name, :nature, :reference_name,
to: :variant, prefix: true
delegate :unit_name, :france_maaid, :phytosanitary_product, :default_unit, to: :variant
delegate :able_to_each?, :able_to?, :of_expression, :subscribing?,
:deliverable?, :asset_account, :product_account, :charge_account,
:stock_account, :population_counting, :population_counting_unitary?,
:identifiable?,
to: :nature
delegate :has_indicator?, :individual_indicators_list, :whole_indicators_list,
:abilities, :abilities_list, :indicators, :indicators_list,
:frozen_indicators, :frozen_indicators_list, :variable_indicators,
:variable_indicators_list, :linkage_points, :linkage_points_list,
to: :nature
after_initialize :choose_default_name
before_validation :set_default_values_on_create, on: :create
before_validation :update_default_values, on: :update
before_validation :set_default_values
validate do
if dead_at && born_at
errors.add(:dead_at, :invalid) if dead_at < born_at
end
if variant
if variety && Onoma::Variety.find(variant_variety)
unless Onoma::Variety.find(variant_variety) >= variety
errors.add(:variety, :invalid)
end
end
if derivative_of && Onoma::Variety.find(variant_derivative_of)
unless Onoma::Variety.find(variant_derivative_of) >= derivative_of
errors.add(:derivative_of, :invalid)
end
end
end
end
after_save do
if initializeable?
set_initial_values
else
product_phases = phases.where(product_id: id,
variant_id: variant.id,
nature_id: variant.nature.id,
category_id: variant.category.id)
build_new_phase unless product_phases.any?
end
if self.persisted? && nature.population_counting_unitary? && population.zero?
m = movements.build(delta: 1, started_at: Time.now)
m.save!
end
end
protect(on: :destroy) do
analyses.exists? || intervention_product_parameters.exists? || issues.exists? || parcel_items.exists?
end
def production(_at = nil)
activity_production
end
def activity
production ? production.activity : nil
end
def activity_id
activity ? activity.id : nil
end
def activity_name
activity ? activity.name : nil
end
def best_activity_production(_options = {})
activity_production
end
# planning
def time_use_in_date(date)
intervention_parameters = InterventionParameter.joins(:intervention).where(product_id: self, interventions: { started_at: date }).includes(:intervention).uniq { |p| p.intervention.number }
if intervention_parameters.any?
intervention_numbers = intervention_parameters.includes(:intervention).map { |p| p.intervention.number }
intervention_proposal_parameters = InterventionProposal::Parameter
.joins(:intervention_proposal)
.where(product_id: self, intervention_proposals: { estimated_date: date })
.where.not(intervention_proposals: { number: intervention_numbers })
duration = intervention_parameters.includes(:intervention).map(&:duration).inject(:+) || 0
duration += (intervention_proposal_parameters.map { |p| p.intervention_proposal.estimated_working_time }.inject(:+) || 0) * 3600
(duration / 3600.0).round(1) if duration.present?
else
0
end
end
def working_duration_info(date)
"#{self.time_use_in_date(date).to_s.gsub!(/\./, ',')} h"
end
# planning
# TODO: Removes this ASAP
def deliverable?
false
end
def available?
dead_at.nil? && !population.zero?
end
def nature_name
nature ? nature.name : nil
end
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
# return the pictogram of variant or category
# old pictogram name are stored with _ instead of -
def pictogram
if variant.pictogram.present?
variant.pictogram.gsub('_', '-')
elsif category.pictogram.present?
category.pictogram.gsub('_', '-')
else
'question'
end
end
def unroll_name
'unrolls.backend/products'.t(attributes.symbolize_keys.merge(population: population, unit_name: unit_name))
end
def unambiguous_name
conditioning_string = if (conditioning_unit.symbol == '.')
if default_unit&.symbol.present?
default_unit.symbol
else
default_unit.name
end
elsif conditioning_unit.symbol.nil?
conditioning_unit.name
else
conditioning_unit.symbol
end
"#{name} - #{conditioning_string}"
end
# set initial owner and localization
# after_save
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!
qty_in_variant_population = UnitComputation.convert_into_variant_population(variant, 1, conditioning_unit)
# set indicators from variant in products readings
variant.readings.each do |variant_reading|
is_stock_related = Unit::STOCK_INDICATOR_PER_DIMENSION.values.push('grains_count').include?(variant_reading.indicator_name)
val = is_stock_related ? variant_reading.value * qty_in_variant_population : variant_reading.value
reading = readings.first_of_all(variant_reading.indicator_name) || readings.new(indicator_name: variant_reading.indicator_name)
reading.value = val
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
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
def shape=(new_shape)
reading_cache[:shape] = new_shape
reading_cache[:net_surface_area] = calculate_net_surface_area
shape
end
# Try to find the best name for the new products
def choose_default_name
return if name.present?
ActiveSupport::Deprecation.warn "Product#choose_default_name is deprecated."
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
def set_default_values
set_default_born_at
self.initial_dead_at = dead_at
self.uuid ||= UUIDTools::UUID.random_create.to_s
self.conditioning_unit ||= variant.guess_conditioning[:unit] if variant
self.dead_at ||= initial_dead_at
self.default_storage ||= initial_container
self.initial_container ||= self.default_storage
end
# Sets nature and variety from variant
def set_default_values_on_create
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
self.category_id = variant.category_id
end
end
# Update nature and variety and variant from phase
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
self.category_id = phase_variant.category_id
end
end
def initial_reading(indicator_name)
first_reading(indicator_name)
end
# Returns the matching model for the record
def matching_model
ProductNature.matching_model(self.variety)
end
# Returns the price for the product.
# It's a shortcut for CatalogItem::give
def price(options = {})
CatalogItem.price(self, options)
end
# Returns age in seconds of the product
def age(at = Time.zone.now)
return 0 if born_at.nil? || born_at >= at
((dead_at || at) - born_at)
end
# Returns item from default catalog for given usage and datetime
def default_catalog_item(usage, at = Time.now, into = default_unit, mode = :base_unit)
return nil unless variant
variant.default_catalog_item(usage, at, into, mode)
end
# 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
# unit price in incoming item
# or unit_price in a sale context
# or unit_price in catalog price
def evaluated_price(_options = {})
filter = {
variant_id: variant_id
}
incoming_item = parcel_item_storings.last.parcel_item if parcel_item_storings.any?
incoming_purchase_item = incoming_item.purchase_invoice_item if incoming_item
outgoing_item = outgoing_parcel_item
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
# search a price in incoming item price
elsif incoming_item && incoming_item.unit_pretax_amount != 0.0
incoming_item.unit_pretax_amount
# search a price in sale item via outgoing item price
elsif outgoing_sale_item
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
def dead?
dead_at.present?
end
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
# Returns groups of the product at a given time (or now by default)
def groups_at(viewed_at = nil)
ProductGroup.groups_of(self, viewed_at || Time.zone.now)
end
# add products to current container
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
def population(at: Time.zone.now, into: unit)
pops = populations.last_before(at)
return 0 if pops.none?
destination_unit = into.is_a?(Unit) ? into : Unit.import_from_lexicon(into)
raise ArgumentError.new("Unknown unit #{into}") unless destination_unit.present?
UnitComputation.convert_stock(pops.first.value, unit, destination_unit)
end
def default_unit_population
unit&.coefficient ? (population * unit.coefficient) : population
end
# Moves population with given quantity
def move!(quantity, options = {})
movements.create!(delta: quantity, started_at: options[:at])
end
# Returns the container for the product at a given time
def container_at(at)
if l = localizations.at(at).first
return l.container
end
nil
end
# Returns the current contents of the product at a given time (or now by default)
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
def containeds(at = Time.zone.now)
list = []
ProductLocalization.where(container_id: id).at(at).each do |localization|
list << localization.product
list += localization.product.containeds(at)
end
list
end
def contents_name(_at = Time.zone.now)
containeds.map(&:name).compact.to_sentence
end
# Returns the current ownership for the product
def owner
if o = current_ownership
return o.owner
end
nil
end
def picture_path(style = :original)
picture.path(style)
end
# Returns all contained products of the given variant
def localized_variants(variant, options = {})
options[:at] ||= Time.zone.now
containeds.select { |p| p.variant == variant }
end
Onoma::Indicator.each do |indicator|
alias_method :"cache_#{indicator}", indicator
define_method indicator.to_sym do |*args|
return get(indicator, *args) if args.present?
send(:"cache_#{indicator}")
end
define_method :"#{indicator}!" do |*args|
return get!(indicator, *args) if args.present?
send(:"cache_#{indicator}")
end
end
# Create a new product parted from self
# See part!
def part_with!(population, options = {})
product = part_with(population, options)
product.save!
product
end
# Build a new product parted from self
# No product_division created.
# Options can be shape, name, born_at
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
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
# TODO: Doc
def variables(_options = {})
list = []
abilities = self.abilities
variety = Onoma::Variety[self.variety]
derivative_of = Onoma::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
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
# Override net_surface_area indicator to compute it from shape if
# product has shape indicator unless options :strict is given
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
def initial_shape_area
::Charta.new_geometry(initial_shape).area.in_square_meter
end
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