refactor: implement extensible architecture for streetup-core with hooks and reporting services
This commit is contained in:
parent
7e74a1f8c0
commit
b871c17fbc
18 changed files with 315 additions and 448 deletions
|
|
@ -10,3 +10,9 @@ class CommonConfig(AppConfig):
|
||||||
from django.contrib.auth.signals import user_logged_in
|
from django.contrib.auth.signals import user_logged_in
|
||||||
from .middleware import set_login_time
|
from .middleware import set_login_time
|
||||||
user_logged_in.connect(set_login_time)
|
user_logged_in.connect(set_login_time)
|
||||||
|
|
||||||
|
# Charger les extensions conditionnelles
|
||||||
|
try:
|
||||||
|
from common import bm_extensions
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
|
||||||
106
streetup/common/hooks.py
Normal file
106
streetup/common/hooks.py
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
from django.db.models import Q
|
||||||
|
|
||||||
|
class ExtensionHooks:
|
||||||
|
def __init__(self):
|
||||||
|
self._form_cleaners = []
|
||||||
|
self._search_annotators = []
|
||||||
|
self._search_filters = []
|
||||||
|
self._pdf_fields_extractors = []
|
||||||
|
self._export_headers = []
|
||||||
|
self._export_row_extractors = []
|
||||||
|
self._custom_allowed_fields = []
|
||||||
|
self._custom_export_fields = []
|
||||||
|
self._inline_validators = []
|
||||||
|
self._custom_source_ref_value_extractor = None
|
||||||
|
|
||||||
|
def register_custom_source_ref_value_extractor(self, func):
|
||||||
|
self._custom_source_ref_value_extractor = func
|
||||||
|
return func
|
||||||
|
|
||||||
|
def get_custom_source_ref_value(self, intervention):
|
||||||
|
if self._custom_source_ref_value_extractor:
|
||||||
|
return self._custom_source_ref_value_extractor(intervention)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def register_custom_export_field(self, label, extractor):
|
||||||
|
self._custom_export_fields.append((label, extractor))
|
||||||
|
return (label, extractor)
|
||||||
|
|
||||||
|
def get_custom_export_fields(self):
|
||||||
|
return self._custom_export_fields
|
||||||
|
|
||||||
|
def register_inline_validator(self, func):
|
||||||
|
self._inline_validators.append(func)
|
||||||
|
return func
|
||||||
|
|
||||||
|
def run_inline_edit_validator(self, field, new_value, intervention):
|
||||||
|
for validator in self._inline_validators:
|
||||||
|
res = validator(field, new_value, intervention)
|
||||||
|
if res[0] is not None:
|
||||||
|
return res
|
||||||
|
return (None, None)
|
||||||
|
|
||||||
|
def register_form_cleaner(self, func):
|
||||||
|
self._form_cleaners.append(func)
|
||||||
|
return func
|
||||||
|
|
||||||
|
def register_search_annotator(self, func):
|
||||||
|
self._search_annotators.append(func)
|
||||||
|
return func
|
||||||
|
|
||||||
|
def register_search_filter(self, func):
|
||||||
|
self._search_filters.append(func)
|
||||||
|
return func
|
||||||
|
|
||||||
|
def register_pdf_fields_extractor(self, func):
|
||||||
|
self._pdf_fields_extractors.append(func)
|
||||||
|
return func
|
||||||
|
|
||||||
|
def register_export_headers(self, headers_list):
|
||||||
|
self._export_headers.extend(headers_list)
|
||||||
|
|
||||||
|
def register_export_row_extractor(self, func):
|
||||||
|
self._export_row_extractors.append(func)
|
||||||
|
return func
|
||||||
|
|
||||||
|
def register_custom_allowed_field(self, field_name_or_list):
|
||||||
|
if isinstance(field_name_or_list, list):
|
||||||
|
self._custom_allowed_fields.extend(field_name_or_list)
|
||||||
|
else:
|
||||||
|
self._custom_allowed_fields.append(field_name_or_list)
|
||||||
|
|
||||||
|
def run_form_cleaners(self, form, cleaned_data):
|
||||||
|
for cleaner in self._form_cleaners:
|
||||||
|
cleaned_data = cleaner(form, cleaned_data)
|
||||||
|
return cleaned_data
|
||||||
|
|
||||||
|
def apply_search_annotations(self, queryset):
|
||||||
|
for annotator in self._search_annotators:
|
||||||
|
queryset = annotator(queryset)
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
def get_search_filters(self, word):
|
||||||
|
q_obj = Q()
|
||||||
|
for filter_func in self._search_filters:
|
||||||
|
q_obj |= filter_func(word)
|
||||||
|
return q_obj
|
||||||
|
|
||||||
|
def extract_pdf_fields(self, intervention):
|
||||||
|
fields = []
|
||||||
|
for extractor in self._pdf_fields_extractors:
|
||||||
|
fields.extend(extractor(intervention))
|
||||||
|
return fields
|
||||||
|
|
||||||
|
def get_export_headers(self):
|
||||||
|
return self._export_headers
|
||||||
|
|
||||||
|
def extract_export_row(self, intervention):
|
||||||
|
row_data = []
|
||||||
|
for extractor in self._export_row_extractors:
|
||||||
|
row_data.extend(extractor(intervention))
|
||||||
|
return row_data
|
||||||
|
|
||||||
|
def get_custom_allowed_fields(self):
|
||||||
|
return self._custom_allowed_fields
|
||||||
|
|
||||||
|
hooks = ExtensionHooks()
|
||||||
|
|
@ -106,10 +106,13 @@ def _collect_report_data(companies, report_date):
|
||||||
# Colonnes
|
# Colonnes
|
||||||
COL_KEYS = [
|
COL_KEYS = [
|
||||||
'damage', 'defect', 'demand',
|
'damage', 'defect', 'demand',
|
||||||
'observation', 'prego',
|
'observation',
|
||||||
'finished', 'other_status',
|
|
||||||
'total',
|
|
||||||
]
|
]
|
||||||
|
dynamic_cols = getattr(settings, 'DAILY_REPORT_SOURCE_COLUMNS', {})
|
||||||
|
for key in dynamic_cols.keys():
|
||||||
|
if key not in COL_KEYS:
|
||||||
|
COL_KEYS.append(key)
|
||||||
|
COL_KEYS.extend(['finished', 'other_status', 'total'])
|
||||||
|
|
||||||
by_thematic = defaultdict(lambda: {k: 0 for k in COL_KEYS})
|
by_thematic = defaultdict(lambda: {k: 0 for k in COL_KEYS})
|
||||||
by_person = defaultdict(int)
|
by_person = defaultdict(int)
|
||||||
|
|
@ -142,9 +145,9 @@ def _collect_report_data(companies, report_date):
|
||||||
if sc_name == 'observation':
|
if sc_name == 'observation':
|
||||||
row['observation'] += 1
|
row['observation'] += 1
|
||||||
totals['observation'] += 1
|
totals['observation'] += 1
|
||||||
elif sc_name == 'prego':
|
elif sc_name in dynamic_cols:
|
||||||
row['prego'] += 1
|
row[sc_name] += 1
|
||||||
totals['prego'] += 1
|
totals[sc_name] += 1
|
||||||
|
|
||||||
# --- Statut ---
|
# --- Statut ---
|
||||||
if itv.status_order >= STATUS_ORDERS['finished']:
|
if itv.status_order >= STATUS_ORDERS['finished']:
|
||||||
|
|
@ -179,26 +182,37 @@ COLUMN_HEADERS = [
|
||||||
('defect', 'Défaut/Panne'),
|
('defect', 'Défaut/Panne'),
|
||||||
('demand', 'Demande'),
|
('demand', 'Demande'),
|
||||||
('observation', 'StreetUp'),
|
('observation', 'StreetUp'),
|
||||||
('prego', 'Issu de Prego'),
|
]
|
||||||
|
|
||||||
|
# Add dynamic source columns from settings
|
||||||
|
dynamic_cols = getattr(settings, 'DAILY_REPORT_SOURCE_COLUMNS', {})
|
||||||
|
for key, cfg in dynamic_cols.items():
|
||||||
|
COLUMN_HEADERS.append((key, cfg['label']))
|
||||||
|
|
||||||
|
COLUMN_HEADERS.extend([
|
||||||
('finished', 'Résolu'),
|
('finished', 'Résolu'),
|
||||||
('other_status', 'En cours'),
|
('other_status', 'En cours'),
|
||||||
('total', 'Total'),
|
('total', 'Total'),
|
||||||
]
|
])
|
||||||
|
|
||||||
# Couleurs spéciales (R, G, B) pour l'en-tête et la ligne totale de certaines colonnes
|
# Couleurs spéciales (R, G, B) pour l'en-tête et la ligne totale de certaines colonnes
|
||||||
# Les colonnes absentes de ce dict héritent de la couleur par défaut.
|
# Les colonnes absentes de ce dict héritent de la couleur par défaut.
|
||||||
COLUMN_SPECIAL_COLORS_RGB = {
|
COLUMN_SPECIAL_COLORS_RGB = {
|
||||||
'prego': (255, 102, 0), # Orange
|
|
||||||
'finished': (112, 173, 71), # Vert
|
'finished': (112, 173, 71), # Vert
|
||||||
'other_status': ( 0, 176, 240), # Bleu ciel
|
'other_status': ( 0, 176, 240), # Bleu ciel
|
||||||
}
|
}
|
||||||
# Version hex pour Excel (AARRGGBB sans alpha, format RRGGBB)
|
# Version hex pour Excel (AARRGGBB sans alpha, format RRGGBB)
|
||||||
COLUMN_SPECIAL_COLORS_HEX = {
|
COLUMN_SPECIAL_COLORS_HEX = {
|
||||||
'prego': 'FF6600',
|
|
||||||
'finished': '70AD47',
|
'finished': '70AD47',
|
||||||
'other_status': '00B0F0',
|
'other_status': '00B0F0',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for key, cfg in dynamic_cols.items():
|
||||||
|
if 'color_rgb' in cfg:
|
||||||
|
COLUMN_SPECIAL_COLORS_RGB[key] = cfg['color_rgb']
|
||||||
|
if 'color_hex' in cfg:
|
||||||
|
COLUMN_SPECIAL_COLORS_HEX[key] = cfg['color_hex']
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# PDF generation
|
# PDF generation
|
||||||
|
|
|
||||||
|
|
@ -353,8 +353,6 @@ class Intervention(models.Model):
|
||||||
source_category = models.ForeignKey(SourceCategory, on_delete=models.SET_NULL, null=True, blank=True)
|
source_category = models.ForeignKey(SourceCategory, on_delete=models.SET_NULL, null=True, blank=True)
|
||||||
source_detail = models.CharField(null=True,blank=True)
|
source_detail = models.CharField(null=True,blank=True)
|
||||||
source_ref = models.CharField(null=True,blank=True)
|
source_ref = models.CharField(null=True,blank=True)
|
||||||
origin_prego_nbr = models.IntegerField(null=True, blank=True)
|
|
||||||
origin_fms_nbr = models.IntegerField(null=True, blank=True)
|
|
||||||
pmo_number = models.IntegerField(null=True, blank=True, verbose_name=_("Numéro PMO"))
|
pmo_number = models.IntegerField(null=True, blank=True, verbose_name=_("Numéro PMO"))
|
||||||
asset_category = models.ForeignKey(AssetCategory, on_delete=models.SET_NULL, null=True, blank=True)
|
asset_category = models.ForeignKey(AssetCategory, on_delete=models.SET_NULL, null=True, blank=True)
|
||||||
symptom = models.ForeignKey(Symptom, on_delete=models.SET_NULL, null=True, blank=True)
|
symptom = models.ForeignKey(Symptom, on_delete=models.SET_NULL, null=True, blank=True)
|
||||||
|
|
|
||||||
|
|
@ -427,7 +427,9 @@ def get_allowed_update_fields(user, intervention):
|
||||||
|
|
||||||
# Source, Référence, Détail, FMS, Type d'origine et Symptôme
|
# Source, Référence, Détail, FMS, Type d'origine et Symptôme
|
||||||
if intervention.status_order < STATUS_ORDERS.get('to_be_processed') and user_config.is_intern:
|
if intervention.status_order < STATUS_ORDERS.get('to_be_processed') and user_config.is_intern:
|
||||||
allowed_fields.update(['source_category', 'source_ref', 'source_detail', 'origin_fms_nbr', 'origin_type', 'symptom'])
|
allowed_fields.update(['source_category', 'source_ref', 'source_detail', 'origin_type', 'symptom'])
|
||||||
|
from common.hooks import hooks
|
||||||
|
allowed_fields.update(hooks.get_custom_allowed_fields())
|
||||||
|
|
||||||
# Priorité : modifiable par les internes tant que le statut n'est pas 'démarré' (in_progress)
|
# Priorité : modifiable par les internes tant que le statut n'est pas 'démarré' (in_progress)
|
||||||
if intervention.status_order < STATUS_ORDERS.get('in_progress') and user_config.is_intern:
|
if intervention.status_order < STATUS_ORDERS.get('in_progress') and user_config.is_intern:
|
||||||
|
|
|
||||||
|
|
@ -108,48 +108,55 @@ def generate_interventions_excel(start_date=None, end_date=None, output_format='
|
||||||
ws = wb.active
|
ws = wb.active
|
||||||
ws.title = "Interventions"
|
ws.title = "Interventions"
|
||||||
|
|
||||||
# Définir les en-têtes
|
# Définir les en-têtes et les largeurs de colonne de manière dynamique
|
||||||
headers = [
|
column_definitions = [
|
||||||
'Code',
|
('Code', 15),
|
||||||
'Titre',
|
('Titre', 30),
|
||||||
'Date de création',
|
('Date de création', 20),
|
||||||
'Date de transmission',
|
('Date de transmission', 20),
|
||||||
'Date début prévue',
|
('Date début prévue', 20),
|
||||||
'Date fin prévue',
|
('Date fin prévue', 20),
|
||||||
'Date début planifiée',
|
('Date début planifiée', 20),
|
||||||
'Date fin planifiée',
|
('Date fin planifiée', 20),
|
||||||
'Date de début',
|
('Date de début', 20),
|
||||||
'Date de fin',
|
('Date de fin', 20),
|
||||||
'Date appel initial confirmé',
|
('Date appel initial confirmé', 20),
|
||||||
'Type d\'origine',
|
('Type d\'origine', 15),
|
||||||
'Priorité',
|
('Priorité', 12),
|
||||||
'Statut',
|
('Statut', 15),
|
||||||
'Symptôme',
|
('Symptôme', 30),
|
||||||
'Contrat',
|
('Contrat', 30),
|
||||||
'Source catégorie',
|
('Source catégorie', 20),
|
||||||
'Numéro PREGO',
|
|
||||||
'Numéro FMS',
|
|
||||||
'Documents joints',
|
|
||||||
'Notes',
|
|
||||||
'Créé par',
|
|
||||||
'Équipe (créateur)',
|
|
||||||
'Thématique (code)',
|
|
||||||
'Thématique (nom)',
|
|
||||||
'Commune',
|
|
||||||
'Localisation code 1',
|
|
||||||
'Localisation label 1',
|
|
||||||
'Localisation code 2',
|
|
||||||
'Localisation label 2',
|
|
||||||
'Localisation code 3',
|
|
||||||
'Localisation label 3',
|
|
||||||
'Localisation code 4',
|
|
||||||
'Localisation label 4',
|
|
||||||
'Catégorie (code)',
|
|
||||||
'Catégorie (nom)',
|
|
||||||
'Modèle (code)',
|
|
||||||
'Modèle (nom)',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
from common.hooks import hooks
|
||||||
|
for header in hooks.get_export_headers():
|
||||||
|
column_definitions.append((header, 15))
|
||||||
|
|
||||||
|
column_definitions.extend([
|
||||||
|
('Documents joints', 50),
|
||||||
|
('Notes', 50),
|
||||||
|
('Créé par', 20),
|
||||||
|
('Équipe (créateur)', 25),
|
||||||
|
('Thématique (code)', 15),
|
||||||
|
('Thématique (nom)', 25),
|
||||||
|
('Commune', 20),
|
||||||
|
('Localisation code 1', 15),
|
||||||
|
('Localisation label 1', 25),
|
||||||
|
('Localisation code 2', 15),
|
||||||
|
('Localisation label 2', 25),
|
||||||
|
('Localisation code 3', 15),
|
||||||
|
('Localisation label 3', 25),
|
||||||
|
('Localisation code 4', 15),
|
||||||
|
('Localisation label 4', 25),
|
||||||
|
('Equipement code', 15),
|
||||||
|
('Equipement nom', 25),
|
||||||
|
('Modèle code', 15),
|
||||||
|
('Modèle nom', 30),
|
||||||
|
])
|
||||||
|
|
||||||
|
headers = [col[0] for col in column_definitions]
|
||||||
|
|
||||||
# Écrire les en-têtes avec style
|
# Écrire les en-têtes avec style
|
||||||
for col_num, header in enumerate(headers, 1):
|
for col_num, header in enumerate(headers, 1):
|
||||||
cell = ws.cell(row=1, column=col_num)
|
cell = ws.cell(row=1, column=col_num)
|
||||||
|
|
@ -159,49 +166,10 @@ def generate_interventions_excel(start_date=None, end_date=None, output_format='
|
||||||
cell.alignment = Alignment(horizontal="center", vertical="center")
|
cell.alignment = Alignment(horizontal="center", vertical="center")
|
||||||
|
|
||||||
# Ajuster la largeur des colonnes
|
# Ajuster la largeur des colonnes
|
||||||
column_widths = {
|
from openpyxl.utils import get_column_letter
|
||||||
'A': 15, # Code
|
for i, col_def in enumerate(column_definitions, start=1):
|
||||||
'B': 40, # Titre
|
col_letter = get_column_letter(i)
|
||||||
'C': 20, # Date création
|
ws.column_dimensions[col_letter].width = col_def[1]
|
||||||
'D': 20, # Date transmission
|
|
||||||
'E': 20, # Date début prévue
|
|
||||||
'F': 20, # Date fin prévue
|
|
||||||
'G': 20, # Date début planifiée
|
|
||||||
'H': 20, # Date fin planifiée
|
|
||||||
'I': 20, # Date début
|
|
||||||
'J': 20, # Date fin
|
|
||||||
'K': 20, # Date appel initial confirmé
|
|
||||||
'L': 15, # Type origine
|
|
||||||
'M': 12, # Priorité
|
|
||||||
'N': 15, # Statut
|
|
||||||
'O': 30, # Symptôme
|
|
||||||
'P': 30, # Contrat
|
|
||||||
'Q': 20, # Source catégorie
|
|
||||||
'R': 15, # PREGO
|
|
||||||
'S': 15, # FMS
|
|
||||||
'T': 50, # Documents joints
|
|
||||||
'U': 50, # Notes
|
|
||||||
'V': 20, # Créé par
|
|
||||||
'W': 25, # Équipe (créateur)
|
|
||||||
'X': 15, # Thématique code
|
|
||||||
'Y': 25, # Thématique nom
|
|
||||||
'Z': 20, # Commune
|
|
||||||
'AA': 15, # Loc code 1
|
|
||||||
'AB': 25, # Loc label 1
|
|
||||||
'AC': 15, # Loc code 2
|
|
||||||
'AD': 25, # Loc label 2
|
|
||||||
'AE': 15, # Loc code 3
|
|
||||||
'AF': 25, # Loc label 3
|
|
||||||
'AG': 15, # Loc code 4
|
|
||||||
'AH': 25, # Loc label 4
|
|
||||||
'AI': 15, # Cat code
|
|
||||||
'AJ': 25, # Cat nom
|
|
||||||
'AK': 15, # Model code
|
|
||||||
'AL': 30, # Model nom
|
|
||||||
}
|
|
||||||
|
|
||||||
for col, width in column_widths.items():
|
|
||||||
ws.column_dimensions[col].width = width
|
|
||||||
|
|
||||||
# Forcer la langue française pour l'export
|
# Forcer la langue française pour l'export
|
||||||
with override('fr'):
|
with override('fr'):
|
||||||
|
|
@ -348,8 +316,7 @@ def generate_interventions_excel(start_date=None, end_date=None, output_format='
|
||||||
symptom_name,
|
symptom_name,
|
||||||
contrat_info,
|
contrat_info,
|
||||||
source_category_name,
|
source_category_name,
|
||||||
intervention.origin_prego_nbr or '',
|
*hooks.extract_export_row(intervention),
|
||||||
intervention.origin_fms_nbr or '',
|
|
||||||
documents_str,
|
documents_str,
|
||||||
notes_str,
|
notes_str,
|
||||||
created_by,
|
created_by,
|
||||||
|
|
|
||||||
|
|
@ -1,147 +1,54 @@
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
const sourceCategorySelect = document.getElementById('id_source_category');
|
const sourceCategorySelect = document.getElementById('id_source_category');
|
||||||
const sourceDetailContainer = document.getElementById('source-detail-container');
|
const sourceDetailContainer = document.getElementById('source-detail-container');
|
||||||
const sourceRefContainer = document.getElementById('source-ref-container'); // PREGO
|
const sourceRefContainer = document.getElementById('source-ref-container');
|
||||||
const sourceRefGenericContainer = document.getElementById('source-ref-generic-container'); // OBSERVATION
|
|
||||||
const sourceRefInput = document.getElementById('id_source_ref');
|
|
||||||
const noFmsCheckbox = document.getElementById('id_no_origin_fms');
|
|
||||||
const fmsInput = document.getElementById('id_origin_fms_nbr');
|
|
||||||
|
|
||||||
// Fonction pour afficher/masquer le champ détail de la source
|
function handleSourceChange() {
|
||||||
function toggleSourceDetail() {
|
if (!sourceCategorySelect) return;
|
||||||
const selectedValue = sourceCategorySelect.value;
|
const selectedValue = sourceCategorySelect.value;
|
||||||
// Rechercher la catégorie sélectionnée dans sourceCategoriesData
|
const selectedOption = sourceCategorySelect.options[sourceCategorySelect.selectedIndex];
|
||||||
const selectedCategory = sourceCategoriesData.find(sc => sc.id == selectedValue);
|
const selectedText = selectedOption ? selectedOption.text : '';
|
||||||
|
|
||||||
if (selectedCategory && selectedCategory.has_reference && !['PREGO','OBSERVATION','OBSERVATIE'].includes(selectedCategory.name)) {
|
let selectedCategory = null;
|
||||||
sourceDetailContainer.classList.remove('d-none'); // Afficher
|
if (typeof sourceCategoriesData !== 'undefined' && Array.isArray(sourceCategoriesData)) {
|
||||||
} else {
|
selectedCategory = sourceCategoriesData.find(sc => sc.id == selectedValue);
|
||||||
sourceDetailContainer.classList.add('d-none'); // Masquer
|
}
|
||||||
|
|
||||||
|
// Dispatch a CustomEvent for forks (like Bruxelles Mobilité) to run custom logic
|
||||||
|
const event = new CustomEvent('streetup:source-changed', {
|
||||||
|
detail: {
|
||||||
|
value: selectedValue,
|
||||||
|
text: selectedText,
|
||||||
|
category: selectedCategory
|
||||||
|
}
|
||||||
|
});
|
||||||
|
document.dispatchEvent(event);
|
||||||
|
|
||||||
|
// Standard core behavior:
|
||||||
|
// 1. Toggle source detail visibility
|
||||||
|
if (sourceDetailContainer) {
|
||||||
|
if (selectedCategory && selectedCategory.has_reference) {
|
||||||
|
// If it is observation, the core template has it as read-only.
|
||||||
|
// We show detail container if category requires detail and has no reference.
|
||||||
|
// Normally, we show detail if has_reference is true.
|
||||||
|
sourceDetailContainer.classList.remove('d-none');
|
||||||
|
} else {
|
||||||
|
sourceDetailContainer.classList.add('d-none');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Toggle source reference container visibility
|
||||||
|
if (sourceRefContainer) {
|
||||||
|
if (selectedCategory && selectedCategory.has_reference) {
|
||||||
|
sourceRefContainer.classList.remove('d-none');
|
||||||
|
} else {
|
||||||
|
sourceRefContainer.classList.add('d-none');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fonction pour afficher/masquer le champ référence de la source
|
if (sourceCategorySelect) {
|
||||||
function syncNoFmsState() {
|
handleSourceChange();
|
||||||
if (!noFmsCheckbox || !fmsInput) {
|
sourceCategorySelect.addEventListener('change', handleSourceChange);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (noFmsCheckbox.checked) {
|
|
||||||
fmsInput.value = '';
|
|
||||||
fmsInput.setAttribute('disabled', 'disabled');
|
|
||||||
} else {
|
|
||||||
fmsInput.removeAttribute('disabled');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
function toggleSourceRef() {
|
|
||||||
const selectedText = sourceCategorySelect.options[sourceCategorySelect.selectedIndex].text || '';
|
|
||||||
const selectedUpper = selectedText.trim().toUpperCase();
|
|
||||||
const mntPrefix = document.getElementById('mnt-prefix');
|
|
||||||
const pregoInput = document.getElementById('id_origin_prego_nbr');
|
|
||||||
|
|
||||||
// Reset visibility by default
|
|
||||||
if (sourceRefContainer) sourceRefContainer.classList.add('d-none');
|
|
||||||
if (sourceRefGenericContainer) sourceRefGenericContainer.classList.add('d-none');
|
|
||||||
if (mntPrefix) mntPrefix.style.display = 'none';
|
|
||||||
if (sourceRefInput) {
|
|
||||||
sourceRefInput.removeAttribute('readonly');
|
|
||||||
sourceRefInput.classList.remove('bg-light');
|
|
||||||
sourceRefInput.setAttribute('title', '');
|
|
||||||
sourceRefInput.removeAttribute('aria-readonly');
|
|
||||||
}
|
|
||||||
|
|
||||||
// PREGO -> show PREGO block with MNT prefix + FMS
|
|
||||||
if (selectedUpper === 'PREGO') {
|
|
||||||
if (sourceRefContainer) sourceRefContainer.classList.remove('d-none');
|
|
||||||
if (mntPrefix) mntPrefix.style.display = 'inline-block';
|
|
||||||
|
|
||||||
if (pregoInput) {
|
|
||||||
pregoInput.setAttribute('pattern', '\\d{1,6}');
|
|
||||||
pregoInput.setAttribute('maxlength', '6');
|
|
||||||
pregoInput.setAttribute('title', gettext('Veuillez entrer un entier de maximum 6 chiffres'));
|
|
||||||
pregoInput.addEventListener('input', function () {
|
|
||||||
this.value = this.value.replace(/\D/g, '').slice(0, 6);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (fmsInput) {
|
|
||||||
fmsInput.removeAttribute('disabled');
|
|
||||||
fmsInput.setAttribute('pattern', '\\d{1,9}');
|
|
||||||
fmsInput.setAttribute('maxlength', '9');
|
|
||||||
fmsInput.addEventListener('input', function () {
|
|
||||||
this.value = this.value.replace(/\D/g, '').slice(0, 9);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// OBSERVATION (FR/NL) -> show generic source_ref only
|
|
||||||
else if (selectedUpper === 'OBSERVATION' || selectedUpper === 'OBSERVATIE') {
|
|
||||||
if (sourceRefGenericContainer) sourceRefGenericContainer.classList.remove('d-none');
|
|
||||||
// Make the reference read-only but still submitted
|
|
||||||
if (sourceRefInput) {
|
|
||||||
sourceRefInput.setAttribute('readonly', 'readonly');
|
|
||||||
sourceRefInput.setAttribute('aria-readonly', 'true');
|
|
||||||
// subtle visual cue it cannot be edited
|
|
||||||
sourceRefInput.classList.add('bg-light');
|
|
||||||
const v = sourceRefInput.value || '';
|
|
||||||
if (v) {
|
|
||||||
sourceRefInput.setAttribute('title', gettext('Référence non modifiable'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clean PREGO/FMS constraints
|
|
||||||
if (pregoInput) {
|
|
||||||
pregoInput.removeAttribute('pattern');
|
|
||||||
pregoInput.removeAttribute('maxlength');
|
|
||||||
pregoInput.removeAttribute('title');
|
|
||||||
}
|
|
||||||
if (fmsInput) {
|
|
||||||
fmsInput.value = '';
|
|
||||||
fmsInput.setAttribute('disabled', 'disabled');
|
|
||||||
fmsInput.removeAttribute('pattern');
|
|
||||||
fmsInput.removeAttribute('maxlength');
|
|
||||||
}
|
|
||||||
if (noFmsCheckbox) {
|
|
||||||
noFmsCheckbox.checked = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Others -> nothing specific
|
|
||||||
else {
|
|
||||||
// ensure PREGO constraints removed
|
|
||||||
if (pregoInput) {
|
|
||||||
pregoInput.removeAttribute('pattern');
|
|
||||||
pregoInput.removeAttribute('maxlength');
|
|
||||||
pregoInput.removeAttribute('title');
|
|
||||||
}
|
|
||||||
if (fmsInput) {
|
|
||||||
fmsInput.removeAttribute('disabled');
|
|
||||||
fmsInput.removeAttribute('pattern');
|
|
||||||
fmsInput.removeAttribute('maxlength');
|
|
||||||
}
|
|
||||||
if (noFmsCheckbox) {
|
|
||||||
noFmsCheckbox.checked = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
syncNoFmsState();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exécuter une première fois si besoin à l'initialisation :
|
|
||||||
if (sourceCategorySelect !== null) {
|
|
||||||
toggleSourceDetail();
|
|
||||||
toggleSourceRef();
|
|
||||||
// Événement pour surveiller les changements sur le champ Catgégorie de la source
|
|
||||||
sourceCategorySelect.addEventListener('change', toggleSourceDetail);
|
|
||||||
sourceCategorySelect.addEventListener('change', toggleSourceRef);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (noFmsCheckbox) {
|
|
||||||
noFmsCheckbox.addEventListener('change', syncNoFmsState);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
window.toggleSourceDetail = toggleSourceDetail;
|
|
||||||
window.syncNoFmsState = syncNoFmsState;
|
|
||||||
window.toggleSourceRef = toggleSourceRef;
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -179,52 +179,11 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Règles spécifiques à la source PREGO ---
|
// Dispatch CustomEvent to allow custom validation rules injection (e.g., PREGO/FMS validations in forks)
|
||||||
const sourceSel = document.getElementById('id_source_category');
|
const customRulesEvent = new CustomEvent('streetup:collect-validation-rules', {
|
||||||
const pregoEl = document.getElementById('id_origin_prego_nbr');
|
detail: { rules: rules, isVisible: isVisible, gettext: gettext }
|
||||||
const fmsEl = document.getElementById('id_origin_fms_nbr');
|
});
|
||||||
const noFmsEl = document.getElementById('id_no_origin_fms');
|
document.dispatchEvent(customRulesEvent);
|
||||||
const docsEl = document.getElementById('id_documents');
|
|
||||||
|
|
||||||
const isPrego = (() => {
|
|
||||||
if (!sourceSel) return false;
|
|
||||||
const opt = sourceSel.options[sourceSel.selectedIndex];
|
|
||||||
const val = (opt?.value || '').trim().toLowerCase();
|
|
||||||
const txt = (opt?.text || '').trim().toLowerCase();
|
|
||||||
return val === 'prego' || txt === 'prego';
|
|
||||||
})();
|
|
||||||
|
|
||||||
if (isPrego) {
|
|
||||||
// 1️⃣ Numéro PREGO obligatoire
|
|
||||||
rules.push({
|
|
||||||
el: pregoEl,
|
|
||||||
test: () => pregoEl && pregoEl.value.trim() !== '',
|
|
||||||
message: gettext('Veuillez indiquer un numéro PREGO.')
|
|
||||||
});
|
|
||||||
|
|
||||||
// 2️⃣ Numéro FMS ou case "Pas de FMS" obligatoire
|
|
||||||
rules.push({
|
|
||||||
el: fmsEl,
|
|
||||||
test: () => {
|
|
||||||
const hasNoFms = !!noFmsEl?.checked;
|
|
||||||
const fmsVal = (fmsEl?.value || '').trim();
|
|
||||||
return hasNoFms || fmsVal !== '';
|
|
||||||
},
|
|
||||||
message: gettext('Veuillez indiquer un numéro FMS ou cocher « Pas de FMS ».')
|
|
||||||
});
|
|
||||||
|
|
||||||
// 3️⃣ Si FMS présent → fichier obligatoire
|
|
||||||
rules.push({
|
|
||||||
el: docsEl,
|
|
||||||
test: () => {
|
|
||||||
const fmsVal = (fmsEl?.value || '').trim();
|
|
||||||
if (!fmsVal) return true; // pas de FMS => pas de contrainte
|
|
||||||
const files = Array.from(docsEl?.files || []);
|
|
||||||
return files.length > 0;
|
|
||||||
},
|
|
||||||
message: gettext('Veuillez joindre au moins un document pour une référence FMS.')
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (!['artworks', 'nature', 'roads', 'its', 'clean'].includes(thematic)) {
|
if (!['artworks', 'nature', 'roads', 'its', 'clean'].includes(thematic)) {
|
||||||
|
|
@ -340,8 +299,12 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
|
||||||
// avertissements via modale (FMS, équipement structures, et statuts des assets/locations)
|
// avertissements via modale (FMS, équipement structures, et statuts des assets/locations)
|
||||||
const submitWarnings = [];
|
const submitWarnings = [];
|
||||||
const fmsWarnMsg = getFmsWarningMessage();
|
|
||||||
if (fmsWarnMsg) submitWarnings.push(fmsWarnMsg);
|
// Dispatch CustomEvent to collect custom warnings (e.g., FMS file validation in forks)
|
||||||
|
const customWarningsEvent = new CustomEvent('streetup:collect-submit-warnings', {
|
||||||
|
detail: { warnings: submitWarnings, gettext: gettext }
|
||||||
|
});
|
||||||
|
document.dispatchEvent(customWarningsEvent);
|
||||||
|
|
||||||
// Vérifier les statuts des assets/locations
|
// Vérifier les statuts des assets/locations
|
||||||
const assetLocationWarnings = await getAssetLocationStatusWarningsAsync();
|
const assetLocationWarnings = await getAssetLocationStatusWarningsAsync();
|
||||||
|
|
@ -460,31 +423,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// --- Helper: faut-il avertir que aucun fichier ne contient le n° FMS ? ---
|
|
||||||
function getFmsWarningMessage() {
|
|
||||||
const sourceSel = document.getElementById('id_source_category');
|
|
||||||
const fmsEl = document.getElementById('id_origin_fms_nbr');
|
|
||||||
const docsEl = document.getElementById('id_documents');
|
|
||||||
|
|
||||||
// seulement si corrective & source PREGO
|
|
||||||
const opt = sourceSel?.options[sourceSel.selectedIndex];
|
|
||||||
const val = (opt?.value || '').trim().toLowerCase();
|
|
||||||
const txt = (opt?.text || '').trim().toLowerCase();
|
|
||||||
const isPrego = (val === 'prego' || txt === 'prego');
|
|
||||||
|
|
||||||
if (getMaintainType() !== 'corrective' || !isPrego) return null;
|
|
||||||
|
|
||||||
const fmsVal = (fmsEl?.value || '').trim();
|
|
||||||
if (!fmsVal) return null; // pas de FMS → pas d’avertissement
|
|
||||||
const files = Array.from(docsEl?.files || []);
|
|
||||||
if (files.length === 0) return null; // règle "fichier obligatoire si FMS" s’en charge
|
|
||||||
|
|
||||||
const hasMatch = files.some(f => String(f.name || '').includes(fmsVal));
|
|
||||||
if (hasMatch) return null;
|
|
||||||
|
|
||||||
// Aucun fichier ne contient le n° FMS → message pour la modale
|
|
||||||
return gettext(`Aucun fichier joint ne contient le n° FMS saisi (${fmsVal}). Voulez-vous envoyer quand même ?`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Modale d’avertissement FMS (création lazy) ---
|
// --- Modale d’avertissement FMS (création lazy) ---
|
||||||
// --- Helper: check for inactive assets/locations (corrective maintenance only) ---
|
// --- Helper: check for inactive assets/locations (corrective maintenance only) ---
|
||||||
|
|
@ -604,6 +543,5 @@ function ensureFmsWarningModal() {
|
||||||
|
|
||||||
|
|
||||||
window.focusFirstFormError = focusFirstFormError;
|
window.focusFirstFormError = focusFirstFormError;
|
||||||
window.getFmsWarningMessage = getFmsWarningMessage;
|
|
||||||
window.getAssetLocationStatusWarningsAsync = getAssetLocationStatusWarningsAsync;
|
window.getAssetLocationStatusWarningsAsync = getAssetLocationStatusWarningsAsync;
|
||||||
window.ensureFmsWarningModal = ensureFmsWarningModal;
|
window.ensureFmsWarningModal = ensureFmsWarningModal;
|
||||||
|
|
|
||||||
|
|
@ -292,29 +292,11 @@
|
||||||
<label for="id_source_detail" class="form-label">{% translate "Détail de la source" %} <span class="text-danger">*</span></label>
|
<label for="id_source_detail" class="form-label">{% translate "Détail de la source" %} <span class="text-danger">*</span></label>
|
||||||
{{ form.source_detail }}
|
{{ form.source_detail }}
|
||||||
</div>
|
</div>
|
||||||
<!-- Référence générique (utilisée par ex. pour OBSERVATION) -->
|
<!-- Référence de la source -->
|
||||||
<div class="mb-3 d-none" id="source-ref-generic-container">
|
<div class="mb-3 d-none" id="source-ref-container">
|
||||||
<label for="id_source_ref" class="form-label">{% translate "Référence de la source" %} <span class="text-danger">*</span></label>
|
<label for="id_source_ref" class="form-label">{% translate "Référence de la source" %} <span class="text-danger">*</span></label>
|
||||||
{{ form.source_ref }}
|
{{ form.source_ref }}
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3 d-none" id="source-ref-container">
|
|
||||||
<label for="id_source_ref" class="form-label">{% translate "Référence de la source" %}<span class="text-danger">*</span></label>
|
|
||||||
<div class="input-group">
|
|
||||||
<span class="input-group-text" id="mnt-prefix" style="display: none;">MNT-</span>
|
|
||||||
{{ form.origin_prego_nbr }}
|
|
||||||
{{ form.origin_prego_nbr.errors }}
|
|
||||||
</div>
|
|
||||||
<div class="form-text mt-3">
|
|
||||||
<label for="id_origin_fms_nbr" class="form-label">{% translate "Numéro FMS" %}</label>
|
|
||||||
{{ form.origin_fms_nbr }}
|
|
||||||
{{ form.origin_fms_nbr.errors }}
|
|
||||||
<div class="form-check mt-2">
|
|
||||||
{{ form.no_origin_fms }}
|
|
||||||
<label class="form-check-label" for="id_no_origin_fms">{% translate "Pas de FMS" %}</label>
|
|
||||||
</div>
|
|
||||||
{{ form.no_origin_fms.errors }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -81,14 +81,12 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if intervention.source_ref or intervention.origin_prego_nbr or 'source_ref' in allowed_update_fields %}
|
{% if intervention.source_ref or 'source_ref' in allowed_update_fields %}
|
||||||
<div class="d-flex align-items-center editable-field cms-conditional-field {% if not intervention.source_ref and not intervention.origin_prego_nbr %}d-none{% endif %}" data-field="source_ref" data-can-edit="{% if 'source_ref' in allowed_update_fields %}true{% else %}false{% endif %}" data-has-value="{% if intervention.source_ref or intervention.origin_prego_nbr %}true{% else %}false{% endif %}">
|
<div class="d-flex align-items-center editable-field cms-conditional-field {% if not intervention.source_ref %}d-none{% endif %}" data-field="source_ref" data-can-edit="{% if 'source_ref' in allowed_update_fields %}true{% else %}false{% endif %}" data-has-value="{% if intervention.source_ref %}true{% else %}false{% endif %}">
|
||||||
<span class="label mx-2">{% translate "Référence" %}:</span>
|
<span class="label mx-2">{% translate "Référence" %}:</span>
|
||||||
<div class="field-display">
|
<div class="field-display">
|
||||||
<span class="field-value">
|
<span class="field-value">
|
||||||
{% if intervention.source_category.name_fr == 'PREGO' and intervention.origin_prego_nbr %}
|
{% if source_observation %}
|
||||||
<a href="https://jira.sprb-gob.brussels/browse/MNT-{{ intervention.origin_prego_nbr }}" target="_blank">MNT-{{ intervention.origin_prego_nbr }}</a>
|
|
||||||
{% elif source_observation %}
|
|
||||||
<a href="{% url 'observations:observation_detail' source_observation.id %}">{{ source_observation.code }}</a>
|
<a href="{% url 'observations:observation_detail' source_observation.id %}">{{ source_observation.code }}</a>
|
||||||
{% elif intervention.source_ref %}
|
{% elif intervention.source_ref %}
|
||||||
{{ intervention.source_ref }}
|
{{ intervention.source_ref }}
|
||||||
|
|
@ -103,24 +101,6 @@
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if intervention.origin_fms_nbr or 'origin_fms_nbr' in allowed_update_fields %}
|
|
||||||
<div class="d-flex align-items-center editable-field cms-conditional-field {% if not intervention.origin_fms_nbr %}d-none{% endif %}" data-field="origin_fms_nbr" data-can-edit="{% if 'origin_fms_nbr' in allowed_update_fields %}true{% else %}false{% endif %}" data-has-value="{% if intervention.origin_fms_nbr %}true{% else %}false{% endif %}">
|
|
||||||
<span class="label mx-2">FMS N°:</span>
|
|
||||||
<div class="field-display">
|
|
||||||
<span class="field-value">
|
|
||||||
{% if intervention.origin_fms_nbr %}
|
|
||||||
<a href="https://fixmystreet.brussels/{{ intervention.origin_fms_nbr }}" target="_blank">{{ intervention.origin_fms_nbr }}</a>
|
|
||||||
{% else %}
|
|
||||||
-
|
|
||||||
{% endif %}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="field-edit d-none" style="flex: 1;">
|
|
||||||
<input type="number" class="form-control form-control-sm" value="{{ intervention.origin_fms_nbr|default:'' }}" placeholder="FMS N°">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if intervention.source_detail or 'source_detail' in allowed_update_fields %}
|
{% if intervention.source_detail or 'source_detail' in allowed_update_fields %}
|
||||||
<div class="d-flex align-items-center editable-field cms-conditional-field {% if not intervention.source_detail %}d-none{% endif %}" data-field="source_detail" data-can-edit="{% if 'source_detail' in allowed_update_fields %}true{% else %}false{% endif %}" data-has-value="{% if intervention.source_detail %}true{% else %}false{% endif %}">
|
<div class="d-flex align-items-center editable-field cms-conditional-field {% if not intervention.source_detail %}d-none{% endif %}" data-field="source_detail" data-can-edit="{% if 'source_detail' in allowed_update_fields %}true{% else %}false{% endif %}" data-has-value="{% if intervention.source_detail %}true{% else %}false{% endif %}">
|
||||||
<span class="label mx-2">{% translate "Détail" %}:</span>
|
<span class="label mx-2">{% translate "Détail" %}:</span>
|
||||||
|
|
|
||||||
|
|
@ -259,7 +259,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="calendar-toolbar__actions">
|
<div class="calendar-toolbar__actions">
|
||||||
<div class="input-group input-group-sm me-2" style="width: 380px;">
|
<div class="input-group input-group-sm me-2" style="width: 380px;">
|
||||||
<input type="text" name="q" id="calendar-search-input" class="form-control" placeholder="{% translate 'Titre, code, localisation, contrat, Prego, FMS…' %}" value="{{ selected_search|default:'' }}">
|
<input type="text" name="q" id="calendar-search-input" class="form-control" placeholder="{% translate 'Titre, code, localisation, contrat…' %}" value="{{ selected_search|default:'' }}">
|
||||||
<button class="btn btn-outline-secondary" type="button" id="calendar-search-clear" title="{% translate 'Effacer la recherche' %}" {% if not selected_search %}style="display:none;"{% endif %}>
|
<button class="btn btn-outline-secondary" type="button" id="calendar-search-clear" title="{% translate 'Effacer la recherche' %}" {% if not selected_search %}style="display:none;"{% endif %}>
|
||||||
<i class="bi bi-x-lg"></i>
|
<i class="bi bi-x-lg"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -833,7 +833,7 @@
|
||||||
<div style="position:relative; display:flex;">
|
<div style="position:relative; display:flex;">
|
||||||
<input type="text" id="interventions-search-input" class="form-control form-control-sm"
|
<input type="text" id="interventions-search-input" class="form-control form-control-sm"
|
||||||
style="width:320px; border-radius:20px 0 0 20px; border-color:#dee2e6; padding-right:1.6rem;"
|
style="width:320px; border-radius:20px 0 0 20px; border-color:#dee2e6; padding-right:1.6rem;"
|
||||||
placeholder="{% translate 'Titre, code, localisation, contrat, Prego, FMS…' %}">
|
placeholder="{% translate 'Titre, code, localisation, contrat…' %}">
|
||||||
<button type="button" id="interventions-search-clear"
|
<button type="button" id="interventions-search-clear"
|
||||||
style="position:absolute; right:4px; top:50%; transform:translateY(-50%);
|
style="position:absolute; right:4px; top:50%; transform:translateY(-50%);
|
||||||
background:none; border:none; padding:0 2px; line-height:1;
|
background:none; border:none; padding:0 2px; line-height:1;
|
||||||
|
|
|
||||||
|
|
@ -403,19 +403,17 @@ def interventions_events(request):
|
||||||
|
|
||||||
if selected_search:
|
if selected_search:
|
||||||
words = selected_search.split()
|
words = selected_search.split()
|
||||||
interventions = interventions.annotate(
|
from common.hooks import hooks
|
||||||
_prego_str=Cast('origin_prego_nbr', output_field=CharField()),
|
interventions = hooks.apply_search_annotations(interventions)
|
||||||
_fms_str=Cast('origin_fms_nbr', output_field=CharField()),
|
|
||||||
)
|
|
||||||
def _word_q(word):
|
def _word_q(word):
|
||||||
return (
|
q_base = (
|
||||||
Q(title__icontains=word)
|
Q(title__icontains=word)
|
||||||
| Q(code__icontains=word)
|
| Q(code__icontains=word)
|
||||||
| Q(location_code__icontains=word)
|
| Q(location_code__icontains=word)
|
||||||
| Q(contract__contract_number__icontains=word)
|
| Q(contract__contract_number__icontains=word)
|
||||||
| Q(_prego_str__icontains=word)
|
|
||||||
| Q(_fms_str__icontains=word)
|
|
||||||
)
|
)
|
||||||
|
q_base |= hooks.get_search_filters(word)
|
||||||
|
return q_base
|
||||||
combined = Q()
|
combined = Q()
|
||||||
for w in words:
|
for w in words:
|
||||||
combined &= _word_q(w)
|
combined &= _word_q(w)
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,6 @@ class InterventionForm(forms.ModelForm):
|
||||||
|
|
||||||
documents = MultipleFileField(required=False, label=_("Documents"))
|
documents = MultipleFileField(required=False, label=_("Documents"))
|
||||||
project = forms.ModelChoiceField(required=False, label=_("Projet"), queryset = Project.get_active_projects())
|
project = forms.ModelChoiceField(required=False, label=_("Projet"), queryset = Project.get_active_projects())
|
||||||
no_origin_fms = forms.BooleanField(required=False, label=_("Pas de FMS"))
|
|
||||||
|
|
||||||
def __init__(self, *args, thematic=None, user=None, **kwargs):
|
def __init__(self, *args, thematic=None, user=None, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
@ -87,7 +86,6 @@ class InterventionForm(forms.ModelForm):
|
||||||
|
|
||||||
self.fields['documents'].widget.attrs.update({'class': 'form-control'})
|
self.fields['documents'].widget.attrs.update({'class': 'form-control'})
|
||||||
self.fields['project'].widget.attrs.update({'class': 'form-select'})
|
self.fields['project'].widget.attrs.update({'class': 'form-select'})
|
||||||
self.fields['no_origin_fms'].widget.attrs.update({'class': 'form-check-input'})
|
|
||||||
|
|
||||||
# Limit status choices to statuses up to 'pris en charge' (assigned)
|
# Limit status choices to statuses up to 'pris en charge' (assigned)
|
||||||
allowed_statuses = ['in_preparation', 'to_be_approved', 'to_be_planned', 'to_be_processed', 'assigned']
|
allowed_statuses = ['in_preparation', 'to_be_approved', 'to_be_planned', 'to_be_processed', 'assigned']
|
||||||
|
|
@ -172,7 +170,7 @@ class InterventionForm(forms.ModelForm):
|
||||||
'code', 'title', 'status', 'priority', 'init_description',
|
'code', 'title', 'status', 'priority', 'init_description',
|
||||||
'maintain_type', 'type', 'origin_type', 'begin_time', 'end_time',
|
'maintain_type', 'type', 'origin_type', 'begin_time', 'end_time',
|
||||||
'asset_category', 'symptom',
|
'asset_category', 'symptom',
|
||||||
'source_category', 'source_detail', 'source_ref', 'origin_prego_nbr', 'origin_fms_nbr',
|
'source_category', 'source_detail', 'source_ref',
|
||||||
'lat', 'lon', 'geom', 'address',
|
'lat', 'lon', 'geom', 'address',
|
||||||
'reason_for_correction', 'correction_details',
|
'reason_for_correction', 'correction_details',
|
||||||
'documents', 'project'
|
'documents', 'project'
|
||||||
|
|
@ -185,8 +183,6 @@ class InterventionForm(forms.ModelForm):
|
||||||
'source_category': forms.Select(attrs={'class': 'form-select'}),
|
'source_category': forms.Select(attrs={'class': 'form-select'}),
|
||||||
'source_detail': forms.TextInput(attrs={'class': 'form-control'}),
|
'source_detail': forms.TextInput(attrs={'class': 'form-control'}),
|
||||||
'source_ref': forms.TextInput(attrs={'class': 'form-control'}),
|
'source_ref': forms.TextInput(attrs={'class': 'form-control'}),
|
||||||
'origin_prego_nbr': forms.NumberInput(attrs={'class': 'form-control', 'placeholder': _('Numéro PREGO')}),
|
|
||||||
'origin_fms_nbr': forms.NumberInput(attrs={'class': 'form-control', 'placeholder': _('Numéro FMS'), 'min': 0, 'max': 2147483647}),
|
|
||||||
|
|
||||||
'asset_category': forms.Select(attrs={'class': 'form-select'}),
|
'asset_category': forms.Select(attrs={'class': 'form-select'}),
|
||||||
'symptom': forms.Select(attrs={'class': 'form-select'}),
|
'symptom': forms.Select(attrs={'class': 'form-select'}),
|
||||||
|
|
@ -210,15 +206,6 @@ class InterventionForm(forms.ModelForm):
|
||||||
'correction_details': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
|
'correction_details': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
|
||||||
}
|
}
|
||||||
|
|
||||||
def _is_prego_source(self, source_category):
|
|
||||||
if not source_category:
|
|
||||||
return False
|
|
||||||
possible_names = [
|
|
||||||
getattr(source_category, attr, "") or ""
|
|
||||||
for attr in ("name_fr", "name_nl")
|
|
||||||
]
|
|
||||||
return any(name.strip().lower() == "prego" for name in possible_names if isinstance(name, str))
|
|
||||||
|
|
||||||
def _is_observation_source(self, source_category):
|
def _is_observation_source(self, source_category):
|
||||||
"""Return True when the selected source category corresponds to OBSERVATION (FR/NL)."""
|
"""Return True when the selected source category corresponds to OBSERVATION (FR/NL)."""
|
||||||
if not source_category:
|
if not source_category:
|
||||||
|
|
@ -258,9 +245,6 @@ class InterventionForm(forms.ModelForm):
|
||||||
if not cleaned_data.get('origin_type'):
|
if not cleaned_data.get('origin_type'):
|
||||||
cleaned_data['origin_type'] = 'demand'
|
cleaned_data['origin_type'] = 'demand'
|
||||||
source_category = cleaned_data.get('source_category')
|
source_category = cleaned_data.get('source_category')
|
||||||
origin_prego_nbr = cleaned_data.get('origin_prego_nbr')
|
|
||||||
origin_fms_nbr = cleaned_data.get('origin_fms_nbr')
|
|
||||||
no_origin_fms = cleaned_data.get('no_origin_fms')
|
|
||||||
documents = cleaned_data.get('documents') or []
|
documents = cleaned_data.get('documents') or []
|
||||||
|
|
||||||
# Ensure consistent list of uploaded files
|
# Ensure consistent list of uploaded files
|
||||||
|
|
@ -297,27 +281,6 @@ class InterventionForm(forms.ModelForm):
|
||||||
_("Un équipement est obligatoire pour ce symptôme."),
|
_("Un équipement est obligatoire pour ce symptôme."),
|
||||||
)
|
)
|
||||||
|
|
||||||
if maintain_type == 'corrective' and self._is_prego_source(source_category):
|
|
||||||
if not origin_prego_nbr:
|
|
||||||
self.add_error(
|
|
||||||
'origin_prego_nbr',
|
|
||||||
_("Veuillez indiquer un numéro PREGO."),
|
|
||||||
)
|
|
||||||
|
|
||||||
if not origin_fms_nbr and not no_origin_fms:
|
|
||||||
self.add_error(
|
|
||||||
'origin_fms_nbr',
|
|
||||||
_("Veuillez indiquer un numéro FMS ou cocher \"Pas de FMS\"."),
|
|
||||||
)
|
|
||||||
if origin_fms_nbr:
|
|
||||||
if not documents:
|
|
||||||
self.add_error(
|
|
||||||
'documents',
|
|
||||||
_("Veuillez joindre au moins un document pour une référence FMS."),
|
|
||||||
)
|
|
||||||
elif no_origin_fms:
|
|
||||||
cleaned_data['origin_fms_nbr'] = None
|
|
||||||
|
|
||||||
# OBSERVATION: require a simple source reference (we use the observation id by convention)
|
# OBSERVATION: require a simple source reference (we use the observation id by convention)
|
||||||
if maintain_type == 'corrective' and self._is_observation_source(source_category):
|
if maintain_type == 'corrective' and self._is_observation_source(source_category):
|
||||||
source_ref = (cleaned_data.get('source_ref') or "").strip()
|
source_ref = (cleaned_data.get('source_ref') or "").strip()
|
||||||
|
|
@ -334,6 +297,10 @@ class InterventionForm(forms.ModelForm):
|
||||||
_("La référence doit être un identifiant numérique (ID d'observation)."),
|
_("La référence doit être un identifiant numérique (ID d'observation)."),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Validation spécifique externe via les hooks
|
||||||
|
from common.hooks import hooks
|
||||||
|
cleaned_data = hooks.run_form_cleaners(self, cleaned_data)
|
||||||
|
|
||||||
return cleaned_data
|
return cleaned_data
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -510,19 +510,17 @@ def interventions_geojson(request):
|
||||||
|
|
||||||
if search_q:
|
if search_q:
|
||||||
words = search_q.split()
|
words = search_q.split()
|
||||||
qs = qs.annotate(
|
from common.hooks import hooks
|
||||||
_prego_str=Cast('origin_prego_nbr', output_field=CharField()),
|
qs = hooks.apply_search_annotations(qs)
|
||||||
_fms_str=Cast('origin_fms_nbr', output_field=CharField()),
|
|
||||||
)
|
|
||||||
def _word_q(word):
|
def _word_q(word):
|
||||||
return (
|
q_base = (
|
||||||
Q(title__icontains=word)
|
Q(title__icontains=word)
|
||||||
| Q(code__icontains=word)
|
| Q(code__icontains=word)
|
||||||
| Q(location_code__icontains=word)
|
| Q(location_code__icontains=word)
|
||||||
| Q(contract__contract_number__icontains=word)
|
| Q(contract__contract_number__icontains=word)
|
||||||
| Q(_prego_str__icontains=word)
|
|
||||||
| Q(_fms_str__icontains=word)
|
|
||||||
)
|
)
|
||||||
|
q_base |= hooks.get_search_filters(word)
|
||||||
|
return q_base
|
||||||
combined = Q()
|
combined = Q()
|
||||||
for w in words:
|
for w in words:
|
||||||
combined &= _word_q(w)
|
combined &= _word_q(w)
|
||||||
|
|
@ -648,19 +646,17 @@ def interventions_geojson(request):
|
||||||
search_mode = 'and'
|
search_mode = 'and'
|
||||||
if search_q:
|
if search_q:
|
||||||
words = search_q.split()
|
words = search_q.split()
|
||||||
qs = qs.annotate(
|
from common.hooks import hooks
|
||||||
_prego_str=Cast('origin_prego_nbr', output_field=CharField()),
|
qs = hooks.apply_search_annotations(qs)
|
||||||
_fms_str=Cast('origin_fms_nbr', output_field=CharField()),
|
|
||||||
)
|
|
||||||
def _word_q(word):
|
def _word_q(word):
|
||||||
return (
|
q_base = (
|
||||||
Q(title__icontains=word)
|
Q(title__icontains=word)
|
||||||
| Q(code__icontains=word)
|
| Q(code__icontains=word)
|
||||||
| Q(location_code__icontains=word)
|
| Q(location_code__icontains=word)
|
||||||
| Q(contract__contract_number__icontains=word)
|
| Q(contract__contract_number__icontains=word)
|
||||||
| Q(_prego_str__icontains=word)
|
|
||||||
| Q(_fms_str__icontains=word)
|
|
||||||
)
|
)
|
||||||
|
q_base |= hooks.get_search_filters(word)
|
||||||
|
return q_base
|
||||||
combined = Q()
|
combined = Q()
|
||||||
if search_mode == 'and':
|
if search_mode == 'and':
|
||||||
for _w in words:
|
for _w in words:
|
||||||
|
|
@ -779,21 +775,19 @@ def interventions_search(request):
|
||||||
base_qs = (
|
base_qs = (
|
||||||
filter_viewable_interventions_for_user(request.user)
|
filter_viewable_interventions_for_user(request.user)
|
||||||
.select_related('contract', 'contract__company')
|
.select_related('contract', 'contract__company')
|
||||||
.annotate(
|
|
||||||
prego_str=Cast('origin_prego_nbr', output_field=CharField()),
|
|
||||||
fms_str=Cast('origin_fms_nbr', output_field=CharField()),
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
from common.hooks import hooks
|
||||||
|
base_qs = hooks.apply_search_annotations(base_qs)
|
||||||
|
|
||||||
def word_filter(word):
|
def word_filter(word):
|
||||||
return (
|
q_base = (
|
||||||
Q(title__icontains=word)
|
Q(title__icontains=word)
|
||||||
| Q(code__icontains=word)
|
| Q(code__icontains=word)
|
||||||
| Q(location_code__icontains=word)
|
| Q(location_code__icontains=word)
|
||||||
| Q(contract__contract_number__icontains=word)
|
| Q(contract__contract_number__icontains=word)
|
||||||
| Q(prego_str__icontains=word)
|
|
||||||
| Q(fms_str__icontains=word)
|
|
||||||
)
|
)
|
||||||
|
q_base |= hooks.get_search_filters(word)
|
||||||
|
return q_base
|
||||||
|
|
||||||
combined = Q()
|
combined = Q()
|
||||||
if mode == 'and':
|
if mode == 'and':
|
||||||
|
|
@ -807,16 +801,17 @@ def interventions_search(request):
|
||||||
|
|
||||||
results = []
|
results = []
|
||||||
for itv in qs:
|
for itv in qs:
|
||||||
results.append({
|
item = {
|
||||||
'id': itv.id,
|
'id': itv.id,
|
||||||
'code': itv.code or '',
|
'code': itv.code or '',
|
||||||
'title': itv.title or '',
|
'title': itv.title or '',
|
||||||
'location_code': itv.location_code or '',
|
'location_code': itv.location_code or '',
|
||||||
'status': itv.status or '',
|
'status': itv.status or '',
|
||||||
'contract_number': itv.contract.contract_number if itv.contract else '',
|
'contract_number': itv.contract.contract_number if itv.contract else '',
|
||||||
'origin_prego_nbr': itv.origin_prego_nbr,
|
}
|
||||||
'origin_fms_nbr': itv.origin_fms_nbr,
|
for key in hooks.get_custom_allowed_fields():
|
||||||
})
|
item[key] = getattr(itv, key, None)
|
||||||
|
results.append(item)
|
||||||
|
|
||||||
return JsonResponse({'results': results})
|
return JsonResponse({'results': results})
|
||||||
|
|
||||||
|
|
@ -938,8 +933,12 @@ def export_interventions(request):
|
||||||
(_("Catégorie source"), lambda i: (i.source_category.getName() if i.source_category else '')),
|
(_("Catégorie source"), lambda i: (i.source_category.getName() if i.source_category else '')),
|
||||||
(_("Détail source"), lambda i: i.source_detail or ''),
|
(_("Détail source"), lambda i: i.source_detail or ''),
|
||||||
(_("Référence source"), lambda i: i.source_ref or ''),
|
(_("Référence source"), lambda i: i.source_ref or ''),
|
||||||
(_("Numéro Prego"), lambda i: i.origin_prego_nbr or ''),
|
]
|
||||||
(_("Numéro FMS"), lambda i: i.origin_fms_nbr or ''),
|
|
||||||
|
from common.hooks import hooks
|
||||||
|
columns.extend(hooks.get_custom_export_fields())
|
||||||
|
|
||||||
|
columns.extend([
|
||||||
(_("Fournisseur assigné"), lambda i: (i.assigned_provider.name if i.assigned_provider else '')),
|
(_("Fournisseur assigné"), lambda i: (i.assigned_provider.name if i.assigned_provider else '')),
|
||||||
(_("Equipe assignée"), lambda i: (i.assigned_team.name if i.assigned_team else '')),
|
(_("Equipe assignée"), lambda i: (i.assigned_team.name if i.assigned_team else '')),
|
||||||
(_("Membre assigné"), lambda i: (i.assigned_member.name if i.assigned_member else '')),
|
(_("Membre assigné"), lambda i: (i.assigned_member.name if i.assigned_member else '')),
|
||||||
|
|
@ -957,7 +956,7 @@ def export_interventions(request):
|
||||||
(_("Adresse"), lambda i: i.address or ''),
|
(_("Adresse"), lambda i: i.address or ''),
|
||||||
(_("Longitude"), lambda i: i.lon if i.lon is not None else ''),
|
(_("Longitude"), lambda i: i.lon if i.lon is not None else ''),
|
||||||
(_("Latitude"), lambda i: i.lat if i.lat is not None else ''),
|
(_("Latitude"), lambda i: i.lat if i.lat is not None else ''),
|
||||||
]
|
])
|
||||||
|
|
||||||
user_config = getattr(request.user, 'config', None)
|
user_config = getattr(request.user, 'config', None)
|
||||||
# If user has role 'operator', include name of files attached to intervention by the creator of the intervention
|
# If user has role 'operator', include name of files attached to intervention by the creator of the intervention
|
||||||
|
|
|
||||||
|
|
@ -1775,19 +1775,17 @@ def apply_intervention_filters_from_querydict(qs, params, user=None):
|
||||||
if search_mode not in ('and', 'or'):
|
if search_mode not in ('and', 'or'):
|
||||||
search_mode = 'and'
|
search_mode = 'and'
|
||||||
words = search_q.split()
|
words = search_q.split()
|
||||||
qs = qs.annotate(
|
from common.hooks import hooks
|
||||||
_prego_str=Cast('origin_prego_nbr', output_field=CharField()),
|
qs = hooks.apply_search_annotations(qs)
|
||||||
_fms_str=Cast('origin_fms_nbr', output_field=CharField()),
|
|
||||||
)
|
|
||||||
def _word_q(word):
|
def _word_q(word):
|
||||||
return (
|
q_base = (
|
||||||
Q(title__icontains=word)
|
Q(title__icontains=word)
|
||||||
| Q(code__icontains=word)
|
| Q(code__icontains=word)
|
||||||
| Q(location_code__icontains=word)
|
| Q(location_code__icontains=word)
|
||||||
| Q(contract__contract_number__icontains=word)
|
| Q(contract__contract_number__icontains=word)
|
||||||
| Q(_prego_str__icontains=word)
|
|
||||||
| Q(_fms_str__icontains=word)
|
|
||||||
)
|
)
|
||||||
|
q_base |= hooks.get_search_filters(word)
|
||||||
|
return q_base
|
||||||
combined = Q()
|
combined = Q()
|
||||||
if search_mode == 'and':
|
if search_mode == 'and':
|
||||||
for w in words:
|
for w in words:
|
||||||
|
|
@ -2414,6 +2412,11 @@ def _update_intervention_field(intervention, field, new_value, user_config=None)
|
||||||
new_value: La nouvelle valeur
|
new_value: La nouvelle valeur
|
||||||
user_config: La configuration utilisateur (optionnel, utilisé pour vérifier les permissions)
|
user_config: La configuration utilisateur (optionnel, utilisé pour vérifier les permissions)
|
||||||
"""
|
"""
|
||||||
|
from common.hooks import hooks
|
||||||
|
success, error = hooks.run_inline_edit_validator(field, new_value, intervention)
|
||||||
|
if success is not None:
|
||||||
|
return success, error
|
||||||
|
|
||||||
if field == "title":
|
if field == "title":
|
||||||
if len(new_value) <= 200:
|
if len(new_value) <= 200:
|
||||||
intervention.title = new_value
|
intervention.title = new_value
|
||||||
|
|
@ -2625,14 +2628,6 @@ def _update_intervention_field(intervention, field, new_value, user_config=None)
|
||||||
return True, None
|
return True, None
|
||||||
return False, _('Value for source_detail must be limited to 255 characters')
|
return False, _('Value for source_detail must be limited to 255 characters')
|
||||||
|
|
||||||
elif field == "origin_fms_nbr":
|
|
||||||
if new_value == "":
|
|
||||||
intervention.origin_fms_nbr = None
|
|
||||||
return True, None
|
|
||||||
if new_value.isdigit():
|
|
||||||
intervention.origin_fms_nbr = int(new_value)
|
|
||||||
return True, None
|
|
||||||
return False, _('Value for FMS number must be a positive integer')
|
|
||||||
|
|
||||||
elif field == "description":
|
elif field == "description":
|
||||||
if len(new_value) <= 1024:
|
if len(new_value) <= 1024:
|
||||||
|
|
|
||||||
|
|
@ -686,10 +686,12 @@ class InterventionPDF(FPDF):
|
||||||
if intervention.source_category and intervention.source_category.has_reference:
|
if intervention.source_category and intervention.source_category.has_reference:
|
||||||
label_fr = "Référence:"
|
label_fr = "Référence:"
|
||||||
label_nl = "Referentie:"
|
label_nl = "Referentie:"
|
||||||
if is_observation_source and intervention.source_ref:
|
from common.hooks import hooks
|
||||||
|
custom_ref = hooks.get_custom_source_ref_value(intervention)
|
||||||
|
if custom_ref is not None:
|
||||||
|
text_value = custom_ref
|
||||||
|
elif is_observation_source and intervention.source_ref:
|
||||||
text_value = f"Observation {intervention.source_ref}"
|
text_value = f"Observation {intervention.source_ref}"
|
||||||
elif intervention.source_category.name_fr == "PREGO":
|
|
||||||
text_value = f"MNT-{intervention.origin_prego_nbr}" if intervention.origin_prego_nbr else ""
|
|
||||||
else:
|
else:
|
||||||
text_value = intervention.source_ref or ""
|
text_value = intervention.source_ref or ""
|
||||||
else:
|
else:
|
||||||
|
|
@ -1515,11 +1517,9 @@ class InterventionReportPDF(FPDF):
|
||||||
if intervention.source_detail:
|
if intervention.source_detail:
|
||||||
self._add_field(_("Détail source"), intervention.source_detail)
|
self._add_field(_("Détail source"), intervention.source_detail)
|
||||||
|
|
||||||
if intervention.origin_prego_nbr:
|
from common.hooks import hooks
|
||||||
self._add_field("PREGO", f"MNT-{intervention.origin_prego_nbr}")
|
for label, value in hooks.extract_pdf_fields(intervention):
|
||||||
|
self._add_field(label, value)
|
||||||
if intervention.origin_fms_nbr:
|
|
||||||
self._add_field("Fix My Street", intervention.origin_fms_nbr)
|
|
||||||
|
|
||||||
if intervention.init_call_confirmed_time:
|
if intervention.init_call_confirmed_time:
|
||||||
self._add_field(_("Appel confirmé"),
|
self._add_field(_("Appel confirmé"),
|
||||||
|
|
|
||||||
|
|
@ -391,6 +391,14 @@ if 'test' in sys.argv:
|
||||||
PASSWORD_HASHERS = [
|
PASSWORD_HASHERS = [
|
||||||
'django.contrib.auth.hashers.MD5PasswordHasher',
|
'django.contrib.auth.hashers.MD5PasswordHasher',
|
||||||
]
|
]
|
||||||
|
STORAGES = {
|
||||||
|
"default": {
|
||||||
|
"BACKEND": "django.core.files.storage.FileSystemStorage",
|
||||||
|
},
|
||||||
|
"staticfiles": {
|
||||||
|
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
|
||||||
|
},
|
||||||
|
}
|
||||||
if '--recreate-db' in sys.argv:
|
if '--recreate-db' in sys.argv:
|
||||||
sys.argv.remove('--recreate-db')
|
sys.argv.remove('--recreate-db')
|
||||||
os.environ['RECREATE_DB'] = '1'
|
os.environ['RECREATE_DB'] = '1'
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue