feat: introduce 15-minute tolerance for intervention document sequencing and photo validation logic
This commit is contained in:
parent
14ab4d15df
commit
022217218f
5 changed files with 85 additions and 10 deletions
|
|
@ -1,9 +1,11 @@
|
|||
from typing import List, Dict, Any, Optional
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
from django.utils.translation import gettext as _
|
||||
from django.utils import timezone
|
||||
from django.utils.dateformat import format as dj_format_date
|
||||
|
||||
DEFAULT_TOLERANCE_MINUTES = 15
|
||||
|
||||
|
||||
def get_intervention_milestones(intervention) -> tuple[Optional[datetime], Optional[datetime]]:
|
||||
"""
|
||||
|
|
@ -68,13 +70,20 @@ def get_intervention_milestones(intervention) -> tuple[Optional[datetime], Optio
|
|||
return start_dt, end_dt
|
||||
|
||||
|
||||
def get_sequenced_intervention_documents(intervention) -> Dict[str, Any]:
|
||||
def get_sequenced_intervention_documents(
|
||||
intervention,
|
||||
tolerance_minutes: int = DEFAULT_TOLERANCE_MINUTES,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Organise les documents et photos de l'intervention en 3 blocs temporels :
|
||||
1. before_work : Avant réalisation (préparation, constat, signalement)
|
||||
2. during_work : Exécution de l'intervention (pendant les travaux)
|
||||
3. after_work : Après réalisation (contrôle, levée de réserves, clôture)
|
||||
|
||||
Une tolérance temporelle (par défaut 15 minutes) est appliquée autour des jalons
|
||||
pour tenir compte des photos ajoutées juste avant de changer le statut ou juste après
|
||||
avoir terminé.
|
||||
|
||||
Retourne un dictionnaire :
|
||||
{
|
||||
'blocks': [
|
||||
|
|
@ -99,6 +108,10 @@ def get_sequenced_intervention_documents(intervention) -> Dict[str, Any]:
|
|||
"""
|
||||
start_dt, end_dt = get_intervention_milestones(intervention)
|
||||
|
||||
# Seuils effectifs avec tolérance temporelle
|
||||
start_threshold = (start_dt - timedelta(minutes=tolerance_minutes)) if start_dt else None
|
||||
end_threshold = (end_dt + timedelta(minutes=tolerance_minutes)) if end_dt else None
|
||||
|
||||
docs = list(
|
||||
intervention.documents
|
||||
.all()
|
||||
|
|
@ -113,18 +126,18 @@ def get_sequenced_intervention_documents(intervention) -> Dict[str, Any]:
|
|||
for doc in docs:
|
||||
doc_dt = doc.upload_date or getattr(intervention, 'creation_time', None)
|
||||
|
||||
if not start_dt:
|
||||
if not start_threshold:
|
||||
# L'intervention n'a pas encore démarré : tout est classé en avant-réalisation
|
||||
before_docs.append(doc)
|
||||
elif not doc_dt:
|
||||
before_docs.append(doc)
|
||||
elif doc_dt < start_dt:
|
||||
elif doc_dt < start_threshold:
|
||||
before_docs.append(doc)
|
||||
else:
|
||||
# doc_dt >= start_dt
|
||||
if not end_dt:
|
||||
# doc_dt >= start_threshold
|
||||
if not end_threshold:
|
||||
during_docs.append(doc)
|
||||
elif doc_dt <= end_dt:
|
||||
elif doc_dt <= end_threshold:
|
||||
during_docs.append(doc)
|
||||
else:
|
||||
after_docs.append(doc)
|
||||
|
|
|
|||
|
|
@ -574,8 +574,9 @@ $('#statusTimeline').on('click', 'button', function () {
|
|||
let hasDocuments = false;
|
||||
if (docContainer) {
|
||||
const cards = Array.from(docContainer.querySelectorAll('[id^="document-card-"], [id^="doc-thumb-"]'));
|
||||
const toleranceMs = 15 * 60 * 1000;
|
||||
const since = (typeof photosRequiredSince !== 'undefined' && photosRequiredSince)
|
||||
? new Date(photosRequiredSince).getTime()
|
||||
? (new Date(photosRequiredSince).getTime() - toleranceMs)
|
||||
: null;
|
||||
if (since) {
|
||||
hasDocuments = cards.some(card => {
|
||||
|
|
|
|||
|
|
@ -4091,7 +4091,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
const photosRequiredSince = "{{ photos_required_since }}";
|
||||
|
||||
function hasPhotos() {
|
||||
const since = photosRequiredSince ? new Date(photosRequiredSince).getTime() : null;
|
||||
const toleranceMs = 15 * 60 * 1000;
|
||||
const since = photosRequiredSince ? (new Date(photosRequiredSince).getTime() - toleranceMs) : null;
|
||||
// Documents déjà enregistrés dans la DB
|
||||
const existingCards = Array.from(document.querySelectorAll('#documentsContainer [id^="document-card-"], #documentsContainer [id^="doc-thumb-"]'));
|
||||
if (existingCards.length > 0) {
|
||||
|
|
|
|||
|
|
@ -136,6 +136,65 @@ class DocumentSequencingServiceTests(TestCase):
|
|||
self.assertEqual(blocks_map['during_work']['count'], 0)
|
||||
self.assertEqual(blocks_map['after_work']['count'], 0)
|
||||
|
||||
def test_sequenced_documents_with_tolerance(self):
|
||||
# Début à T+2h, Fin à T+4h
|
||||
t_begin = self.now + timedelta(hours=2)
|
||||
t_end = self.now + timedelta(hours=4)
|
||||
self.intervention.begin_time = t_begin
|
||||
self.intervention.end_time = t_end
|
||||
self.intervention.save()
|
||||
|
||||
dummy_file = SimpleUploadedFile("photo.jpg", b"fake photo", content_type="image/jpeg")
|
||||
|
||||
# Doc A : 5 min avant le début réel (tolérance 15 min -> classé dans Exécution)
|
||||
doc_just_before = InterventionDocument.objects.create(
|
||||
intervention=self.intervention,
|
||||
file=dummy_file,
|
||||
uploaded_by=self.user,
|
||||
)
|
||||
InterventionDocument.objects.filter(id=doc_just_before.id).update(upload_date=t_begin - timedelta(minutes=5))
|
||||
|
||||
# Doc B : 10 min après la fin réelle (tolérance 15 min -> classé dans Exécution)
|
||||
doc_just_after = InterventionDocument.objects.create(
|
||||
intervention=self.intervention,
|
||||
file=dummy_file,
|
||||
uploaded_by=self.user,
|
||||
)
|
||||
InterventionDocument.objects.filter(id=doc_just_after.id).update(upload_date=t_end + timedelta(minutes=10))
|
||||
|
||||
# Doc C : 1h avant le début réel (hors tolérance -> Avant réalisation)
|
||||
doc_early = InterventionDocument.objects.create(
|
||||
intervention=self.intervention,
|
||||
file=dummy_file,
|
||||
uploaded_by=self.user,
|
||||
)
|
||||
InterventionDocument.objects.filter(id=doc_early.id).update(upload_date=t_begin - timedelta(hours=1))
|
||||
|
||||
# Doc D : 1h après la fin réelle (hors tolérance -> Après réalisation)
|
||||
doc_late = InterventionDocument.objects.create(
|
||||
intervention=self.intervention,
|
||||
file=dummy_file,
|
||||
uploaded_by=self.user,
|
||||
)
|
||||
InterventionDocument.objects.filter(id=doc_late.id).update(upload_date=t_end + timedelta(hours=1))
|
||||
|
||||
result = get_sequenced_intervention_documents(self.intervention)
|
||||
blocks_map = {b['key']: b for b in result['blocks']}
|
||||
|
||||
# Doc C dans Avant réalisation
|
||||
self.assertEqual(blocks_map['before_work']['count'], 1)
|
||||
self.assertEqual(blocks_map['before_work']['documents'][0].id, doc_early.id)
|
||||
|
||||
# Docs A et B dans Exécution de l'intervention (grâce à la tolérance)
|
||||
self.assertEqual(blocks_map['during_work']['count'], 2)
|
||||
during_ids = [d.id for d in blocks_map['during_work']['documents']]
|
||||
self.assertIn(doc_just_before.id, during_ids)
|
||||
self.assertIn(doc_just_after.id, during_ids)
|
||||
|
||||
# Doc D dans Après réalisation
|
||||
self.assertEqual(blocks_map['after_work']['count'], 1)
|
||||
self.assertEqual(blocks_map['after_work']['documents'][0].id, doc_late.id)
|
||||
|
||||
|
||||
class DocumentSequencingViewsTests(TestCase):
|
||||
def setUp(self):
|
||||
|
|
|
|||
|
|
@ -408,7 +408,8 @@ def attempt_status_update(request, intervention, user, new_status, pause_reason=
|
|||
if _is_photo_required_for_user(user, intervention):
|
||||
photos_since = _get_photos_required_since(intervention)
|
||||
if photos_since:
|
||||
has_photos = intervention.documents.filter(upload_date__gte=photos_since).exists()
|
||||
tolerance_since = photos_since - timedelta(minutes=15)
|
||||
has_photos = intervention.documents.filter(upload_date__gte=tolerance_since).exists()
|
||||
else:
|
||||
has_photos = intervention.documents.exists()
|
||||
if not has_photos:
|
||||
|
|
|
|||
Loading…
Reference in a new issue