diff --git a/loko/assets/management/commands/import_fulcrum_trees.py b/loko/assets/management/commands/import_fulcrum_trees.py index b8bc6f4..e69a3c0 100644 --- a/loko/assets/management/commands/import_fulcrum_trees.py +++ b/loko/assets/management/commands/import_fulcrum_trees.py @@ -83,6 +83,11 @@ class Command(BaseCommand): action='store_true', help="Ignorer l'importation des interventions" ) + parser.add_argument( + '--sync-tree-dates', + action='store_true', + help="Synchroniser uniquement les dates de maintenance sur les arbres (prochaine inspection, diagnostic, périodicité, dernière taille)" + ) parser.add_argument( '--limit', type=int, @@ -115,6 +120,12 @@ class Command(BaseCommand): alt = os.path.expanduser('~/Downloads/fulcrum_interventions.geojson') if os.path.exists(alt): interventions_file = alt + if options.get('sync_tree_dates'): + self.log("=== SYNCHRONISATION DES DATES DE MAINTENANCE SUR LES ARBRES ===") + self.sync_tree_maintenance_dates(gestion_file) + self.log("=== FIN DE LA SYNCHRONISATION DES DATES ===") + return + self.log("=== DÉBUT IMPORTATION DONNÉES FULCRUM (ARBRES & INTERVENTIONS) ===") self.log(f"Fichier arbres : {arbes_file}") self.log(f"Fichier gestion : {gestion_file}") @@ -179,6 +190,11 @@ class Command(BaseCommand): order_map, order_contract_map, tree_uuid_to_id, sit_pos_to_tree_id, limit ) + # ------------------------------------------------------------- + # ÉTAPE 4 : SYNCHRONISATION DES DATES SUR LES ARBRES + # ------------------------------------------------------------- + self.sync_tree_maintenance_dates(gestion_file) + self.log("=== FIN DE L'IMPORTATION AVEC SUCCÈS ===") # ----------------------------------------------------------------- @@ -556,6 +572,15 @@ class Command(BaseCommand): if matched_id: break if matched_id: break + # Date de diagnostic / dernière mise à jour + maj_date = props.get('maj_date') + last_phytosanitary_date = None + if maj_date: + try: + last_phytosanitary_date = datetime.fromisoformat(maj_date.replace('Z', '+00:00')).date() + except Exception: + pass + sit_pos_key = f"{sit_id}.{pos_id}" if sit_id and pos_id else None if matched_id: @@ -587,6 +612,7 @@ class Command(BaseCommand): 'development_stage': dev_stage, 'pruning_type': pruning, 'planting_date': planting_date, + 'last_phytosanitary_date': last_phytosanitary_date, 'model_id': asset_model.id if asset_model else None, 'location_id': location.id if location else None, 'lon': lon, @@ -628,6 +654,7 @@ class Command(BaseCommand): development_stage=dev_stage, pruning_type=pruning, planting_date=planting_date, + last_phytosanitary_date=last_phytosanitary_date, model=asset_model, location=location, lon=lon, @@ -668,7 +695,7 @@ class Command(BaseCommand): 'amenity_value', 'carbon_stock', 'cooling_indicator', 'cooling_energy_indicator', 'biodiversity_index', 'managing_authority', 'management_type', 'status', 'vitality', 'is_dead', 'is_stump', 'is_remarkable', 'is_chronoxyle', 'development_stage', 'pruning_type', - 'planting_date', 'model_id', 'location_id', 'lon', 'lat', 'geom', 'geojson' + 'planting_date', 'last_phytosanitary_date', 'model_id', 'location_id', 'lon', 'lat', 'geom', 'geojson' ], batch_size=BATCH_SIZE ) @@ -796,7 +823,7 @@ class Command(BaseCommand): status = 'to_be_planned' status_order = 5 - # Date limite + # Date limite & Date de diagnostic deadline_str = props.get('in_deadline') expected_end_time = None if deadline_str: @@ -805,6 +832,14 @@ class Command(BaseCommand): except Exception: pass + dia_date_str = props.get('dia_date') + expected_begin_time = None + if dia_date_str: + try: + expected_begin_time = datetime.fromisoformat(dia_date_str.replace('Z', '+00:00')) + except Exception: + pass + # Géométrie MultiPolygon SRID 3812 pt_3812 = Point(lon, lat, srid=4326).transform(3812, clone=True) buffered_geom = MultiPolygon(pt_3812.buffer(1)) @@ -828,6 +863,7 @@ class Command(BaseCommand): existing_itv.type = action_type existing_itv.status = status existing_itv.status_order = status_order + existing_itv.expected_begin_time = expected_begin_time existing_itv.expected_end_time = expected_end_time existing_itv.order_id = order_id existing_itv.contract_id = contract_id @@ -849,6 +885,7 @@ class Command(BaseCommand): type=action_type, status=status, status_order=status_order, + expected_begin_time=expected_begin_time, expected_end_time=expected_end_time, thematic=thematic_nature, asset_category=cat_arbres, @@ -876,7 +913,8 @@ class Command(BaseCommand): chunk, fields=[ 'title', 'description', 'address', 'location_code', 'priority', - 'maintain_type', 'type', 'status', 'status_order', 'expected_end_time', + 'maintain_type', 'type', 'status', 'status_order', + 'expected_begin_time', 'expected_end_time', 'order_id', 'contract_id', 'source_detail', 'lon', 'lat', 'geom' ], batch_size=BATCH_SIZE @@ -935,3 +973,104 @@ class Command(BaseCommand): self.log(f" Insertion liens : {min(i + BATCH_SIZE, len(assets_to_create))} / {len(assets_to_create)}") self.log(f"Importation des interventions terminée. Total en base : {Intervention.objects.count()}") + + def sync_tree_maintenance_dates(self, gestion_file=None): + """ + Synchronise les dates et périodicités de maintenance sur les arbres : + - next_inspection_date : min(expected_end_time) des interventions en cours + - pruning_frequency_years : périodicité extraite du délai/titre (ex: Dans 1 an -> 1) + - last_phytosanitary_date : max(expected_begin_time) des interventions de diagnostic + - last_pruning_date : date d'exécution de la dernière taille (depuis gestion.csv ou interventions passées) + """ + self.log("--- Synchronisation des dates de maintenance sur les arbres ---") + from django.db import connection + + content_type_tree = ContentType.objects.get_for_model(NatureTree) + + with connection.cursor() as cur: + # 1. Prochaine échéance prévisionnelle (next_inspection_date) + cur.execute(""" + UPDATE assets_naturetree t + SET next_inspection_date = sub.min_date + FROM ( + SELECT ia.object_id AS tree_id, MIN(i.expected_end_time)::date AS min_date + FROM interventions_interventionasset ia + JOIN interventions_intervention i ON i.id = ia.intervention_id + WHERE ia.content_type_id = %s + AND i.status != 'finished' + AND i.expected_end_time IS NOT NULL + GROUP BY ia.object_id + ) sub + WHERE t.id = sub.tree_id; + """, [content_type_tree.id]) + self.log(f" Prochaine inspection (next_inspection_date) mise à jour : {cur.rowcount} arbres") + + # 2. Périodicité de taille en années (pruning_frequency_years) + cur.execute(""" + UPDATE assets_naturetree t + SET pruning_frequency_years = sub.freq + FROM ( + SELECT DISTINCT ON (ia.object_id) + ia.object_id AS tree_id, + CAST(SUBSTRING(i.title FROM 'Dans ([0-9]+) an') AS INTEGER) AS freq + FROM interventions_interventionasset ia + JOIN interventions_intervention i ON i.id = ia.intervention_id + WHERE ia.content_type_id = %s + AND i.title ~ 'Dans [0-9]+ an' + ORDER BY ia.object_id, i.id DESC + ) sub + WHERE t.id = sub.tree_id; + """, [content_type_tree.id]) + self.log(f" Périodicité de taille (pruning_frequency_years) mise à jour : {cur.rowcount} arbres") + + # 3. Dernier diagnostic phytosanitaire depuis les interventions + cur.execute(""" + UPDATE assets_naturetree t + SET last_phytosanitary_date = GREATEST(COALESCE(t.last_phytosanitary_date, sub.max_dia), sub.max_dia) + FROM ( + SELECT ia.object_id AS tree_id, MAX(i.expected_begin_time)::date AS max_dia + FROM interventions_interventionasset ia + JOIN interventions_intervention i ON i.id = ia.intervention_id + WHERE ia.content_type_id = %s + AND i.expected_begin_time IS NOT NULL + GROUP BY ia.object_id + ) sub + WHERE t.id = sub.tree_id; + """, [content_type_tree.id]) + self.log(f" Dernier diagnostic (last_phytosanitary_date) consolidé : {cur.rowcount} arbres") + + # 4. Dernière taille (last_pruning_date) depuis fulcrum_gestion.csv si disponible + if gestion_file and os.path.exists(gestion_file): + self.log(" Recherche des tailles exécutées dans gestion.csv...") + try: + pruning_updates = {} + with open(gestion_file, mode='r', encoding='utf-8') as f: + reader = csv.DictReader(f) + for r in reader: + if r.get('_status') in ('Execute', 'Controle', 'Reception provisoire', 'Valide'): + pos_lk = r.get('com_pos_lk') + date_exec = r.get('pla_fin') or r.get('co_dat') or r.get('com_dat') + cat = r.get('com_cat') or '' + if pos_lk and date_exec and ('TRA' in cat or 'ELAG' in cat.upper()): + try: + d = datetime.strptime(date_exec[:10], '%Y-%m-%d').date() + if pos_lk not in pruning_updates or d > pruning_updates[pos_lk]: + pruning_updates[pos_lk] = d + except Exception: + pass + + if pruning_updates: + trees_to_update = [] + for t in NatureTree.objects.filter(external_reference__in=pruning_updates.keys()): + pdate = pruning_updates[t.external_reference] + if not t.last_pruning_date or pdate > t.last_pruning_date: + t.last_pruning_date = pdate + trees_to_update.append(t) + if trees_to_update: + BATCH_SIZE = 2000 + for i in range(0, len(trees_to_update), BATCH_SIZE): + NatureTree.objects.bulk_update(trees_to_update[i:i+BATCH_SIZE], fields=['last_pruning_date']) + self.log(f" Dernière taille (last_pruning_date) mise à jour : {len(trees_to_update)} arbres") + except Exception as e: + self.log(f" Erreur lors de la lecture des tailles dans gestion.csv : {e}") + diff --git a/loko/assets/tests_import_fulcrum.py b/loko/assets/tests_import_fulcrum.py index 8e01f5b..9bb8603 100644 --- a/loko/assets/tests_import_fulcrum.py +++ b/loko/assets/tests_import_fulcrum.py @@ -66,7 +66,8 @@ class ImportFulcrumTreesCommandTest(TestCase): "arb_ont": "Ad", "ges_conduite": "LI", "cf_sit": 4.5, - "arb_anpla": "1995" + "arb_anpla": "1995", + "maj_date": "2024-05-10T00:00:00.000Z" } } ] @@ -88,13 +89,15 @@ class ImportFulcrumTreesCommandTest(TestCase): }, "properties": { "_record_id": mock_itv_uuid, - "_title": "Élagage de formation", + "_title": "099.01.001, Formation, Dans 1 an", "rec_interv_com": "Taille branches basses", "sit_nom": "Boulevard du Test", "comm_commune": "Bruxelles", "sit_id": "099.01", "pos_id": "001", "dia_comma": "BC-TEST-001", + "dia_date": "2024-05-10T00:00:00.000Z", + "in_deadline": "2025-05-10T00:00:00.000Z", "rec_delai": "Pla-1", "rec_interv": "FO", "in_fin_yn": "no", @@ -145,6 +148,15 @@ class ImportFulcrumTreesCommandTest(TestCase): self.assertEqual(itv.order_id, order.id) self.assertEqual(itv.priority, "3") # Pla-1 -> 3 self.assertEqual(itv.status, "to_be_planned") + self.assertIsNotNone(itv.expected_begin_time) + self.assertEqual(itv.expected_begin_time.date(), date(2024, 5, 10)) + self.assertIsNotNone(itv.expected_end_time) + self.assertEqual(itv.expected_end_time.date(), date(2025, 5, 10)) + + # Vérifications dates de maintenance sur l'arbre + self.assertEqual(tree.last_phytosanitary_date, date(2024, 5, 10)) + self.assertEqual(tree.next_inspection_date, date(2025, 5, 10)) + self.assertEqual(tree.pruning_frequency_years, 1) # Vérification liaison InterventionAsset links = InterventionAsset.objects.filter(intervention=itv)