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 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.translation import gettext as _
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.dateformat import format as dj_format_date
|
from django.utils.dateformat import format as dj_format_date
|
||||||
|
|
||||||
|
DEFAULT_TOLERANCE_MINUTES = 15
|
||||||
|
|
||||||
|
|
||||||
def get_intervention_milestones(intervention) -> tuple[Optional[datetime], Optional[datetime]]:
|
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
|
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 :
|
Organise les documents et photos de l'intervention en 3 blocs temporels :
|
||||||
1. before_work : Avant réalisation (préparation, constat, signalement)
|
1. before_work : Avant réalisation (préparation, constat, signalement)
|
||||||
2. during_work : Exécution de l'intervention (pendant les travaux)
|
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)
|
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 :
|
Retourne un dictionnaire :
|
||||||
{
|
{
|
||||||
'blocks': [
|
'blocks': [
|
||||||
|
|
@ -99,6 +108,10 @@ def get_sequenced_intervention_documents(intervention) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
start_dt, end_dt = get_intervention_milestones(intervention)
|
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(
|
docs = list(
|
||||||
intervention.documents
|
intervention.documents
|
||||||
.all()
|
.all()
|
||||||
|
|
@ -113,18 +126,18 @@ def get_sequenced_intervention_documents(intervention) -> Dict[str, Any]:
|
||||||
for doc in docs:
|
for doc in docs:
|
||||||
doc_dt = doc.upload_date or getattr(intervention, 'creation_time', None)
|
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
|
# L'intervention n'a pas encore démarré : tout est classé en avant-réalisation
|
||||||
before_docs.append(doc)
|
before_docs.append(doc)
|
||||||
elif not doc_dt:
|
elif not doc_dt:
|
||||||
before_docs.append(doc)
|
before_docs.append(doc)
|
||||||
elif doc_dt < start_dt:
|
elif doc_dt < start_threshold:
|
||||||
before_docs.append(doc)
|
before_docs.append(doc)
|
||||||
else:
|
else:
|
||||||
# doc_dt >= start_dt
|
# doc_dt >= start_threshold
|
||||||
if not end_dt:
|
if not end_threshold:
|
||||||
during_docs.append(doc)
|
during_docs.append(doc)
|
||||||
elif doc_dt <= end_dt:
|
elif doc_dt <= end_threshold:
|
||||||
during_docs.append(doc)
|
during_docs.append(doc)
|
||||||
else:
|
else:
|
||||||
after_docs.append(doc)
|
after_docs.append(doc)
|
||||||
|
|
|
||||||
|
|
@ -574,8 +574,9 @@ $('#statusTimeline').on('click', 'button', function () {
|
||||||
let hasDocuments = false;
|
let hasDocuments = false;
|
||||||
if (docContainer) {
|
if (docContainer) {
|
||||||
const cards = Array.from(docContainer.querySelectorAll('[id^="document-card-"], [id^="doc-thumb-"]'));
|
const cards = Array.from(docContainer.querySelectorAll('[id^="document-card-"], [id^="doc-thumb-"]'));
|
||||||
|
const toleranceMs = 15 * 60 * 1000;
|
||||||
const since = (typeof photosRequiredSince !== 'undefined' && photosRequiredSince)
|
const since = (typeof photosRequiredSince !== 'undefined' && photosRequiredSince)
|
||||||
? new Date(photosRequiredSince).getTime()
|
? (new Date(photosRequiredSince).getTime() - toleranceMs)
|
||||||
: null;
|
: null;
|
||||||
if (since) {
|
if (since) {
|
||||||
hasDocuments = cards.some(card => {
|
hasDocuments = cards.some(card => {
|
||||||
|
|
|
||||||
|
|
@ -4091,7 +4091,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
const photosRequiredSince = "{{ photos_required_since }}";
|
const photosRequiredSince = "{{ photos_required_since }}";
|
||||||
|
|
||||||
function hasPhotos() {
|
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
|
// Documents déjà enregistrés dans la DB
|
||||||
const existingCards = Array.from(document.querySelectorAll('#documentsContainer [id^="document-card-"], #documentsContainer [id^="doc-thumb-"]'));
|
const existingCards = Array.from(document.querySelectorAll('#documentsContainer [id^="document-card-"], #documentsContainer [id^="doc-thumb-"]'));
|
||||||
if (existingCards.length > 0) {
|
if (existingCards.length > 0) {
|
||||||
|
|
|
||||||
|
|
@ -136,6 +136,65 @@ class DocumentSequencingServiceTests(TestCase):
|
||||||
self.assertEqual(blocks_map['during_work']['count'], 0)
|
self.assertEqual(blocks_map['during_work']['count'], 0)
|
||||||
self.assertEqual(blocks_map['after_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):
|
class DocumentSequencingViewsTests(TestCase):
|
||||||
def setUp(self):
|
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):
|
if _is_photo_required_for_user(user, intervention):
|
||||||
photos_since = _get_photos_required_since(intervention)
|
photos_since = _get_photos_required_since(intervention)
|
||||||
if photos_since:
|
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:
|
else:
|
||||||
has_photos = intervention.documents.exists()
|
has_photos = intervention.documents.exists()
|
||||||
if not has_photos:
|
if not has_photos:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue