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 .middleware import 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
|
||||
COL_KEYS = [
|
||||
'damage', 'defect', 'demand',
|
||||
'observation', 'prego',
|
||||
'finished', 'other_status',
|
||||
'total',
|
||||
'observation',
|
||||
]
|
||||
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_person = defaultdict(int)
|
||||
|
|
@ -142,9 +145,9 @@ def _collect_report_data(companies, report_date):
|
|||
if sc_name == 'observation':
|
||||
row['observation'] += 1
|
||||
totals['observation'] += 1
|
||||
elif sc_name == 'prego':
|
||||
row['prego'] += 1
|
||||
totals['prego'] += 1
|
||||
elif sc_name in dynamic_cols:
|
||||
row[sc_name] += 1
|
||||
totals[sc_name] += 1
|
||||
|
||||
# --- Statut ---
|
||||
if itv.status_order >= STATUS_ORDERS['finished']:
|
||||
|
|
@ -179,26 +182,37 @@ COLUMN_HEADERS = [
|
|||
('defect', 'Défaut/Panne'),
|
||||
('demand', 'Demande'),
|
||||
('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'),
|
||||
('other_status', 'En cours'),
|
||||
('total', 'Total'),
|
||||
]
|
||||
])
|
||||
|
||||
# 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.
|
||||
COLUMN_SPECIAL_COLORS_RGB = {
|
||||
'prego': (255, 102, 0), # Orange
|
||||
'finished': (112, 173, 71), # Vert
|
||||
'other_status': ( 0, 176, 240), # Bleu ciel
|
||||
}
|
||||
# Version hex pour Excel (AARRGGBB sans alpha, format RRGGBB)
|
||||
COLUMN_SPECIAL_COLORS_HEX = {
|
||||
'prego': 'FF6600',
|
||||
'finished': '70AD47',
|
||||
'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
|
||||
|
|
|
|||
|
|
@ -353,8 +353,6 @@ class Intervention(models.Model):
|
|||
source_category = models.ForeignKey(SourceCategory, on_delete=models.SET_NULL, null=True, blank=True)
|
||||
source_detail = 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"))
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -427,7 +427,9 @@ def get_allowed_update_fields(user, intervention):
|
|||
|
||||
# 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:
|
||||
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)
|
||||
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.title = "Interventions"
|
||||
|
||||
# Définir les en-têtes
|
||||
headers = [
|
||||
'Code',
|
||||
'Titre',
|
||||
'Date de création',
|
||||
'Date de transmission',
|
||||
'Date début prévue',
|
||||
'Date fin prévue',
|
||||
'Date début planifiée',
|
||||
'Date fin planifiée',
|
||||
'Date de début',
|
||||
'Date de fin',
|
||||
'Date appel initial confirmé',
|
||||
'Type d\'origine',
|
||||
'Priorité',
|
||||
'Statut',
|
||||
'Symptôme',
|
||||
'Contrat',
|
||||
'Source catégorie',
|
||||
'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)',
|
||||
# Définir les en-têtes et les largeurs de colonne de manière dynamique
|
||||
column_definitions = [
|
||||
('Code', 15),
|
||||
('Titre', 30),
|
||||
('Date de création', 20),
|
||||
('Date de transmission', 20),
|
||||
('Date début prévue', 20),
|
||||
('Date fin prévue', 20),
|
||||
('Date début planifiée', 20),
|
||||
('Date fin planifiée', 20),
|
||||
('Date de début', 20),
|
||||
('Date de fin', 20),
|
||||
('Date appel initial confirmé', 20),
|
||||
('Type d\'origine', 15),
|
||||
('Priorité', 12),
|
||||
('Statut', 15),
|
||||
('Symptôme', 30),
|
||||
('Contrat', 30),
|
||||
('Source catégorie', 20),
|
||||
]
|
||||
|
||||
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
|
||||
for col_num, header in enumerate(headers, 1):
|
||||
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")
|
||||
|
||||
# Ajuster la largeur des colonnes
|
||||
column_widths = {
|
||||
'A': 15, # Code
|
||||
'B': 40, # Titre
|
||||
'C': 20, # Date création
|
||||
'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
|
||||
from openpyxl.utils import get_column_letter
|
||||
for i, col_def in enumerate(column_definitions, start=1):
|
||||
col_letter = get_column_letter(i)
|
||||
ws.column_dimensions[col_letter].width = col_def[1]
|
||||
|
||||
# Forcer la langue française pour l'export
|
||||
with override('fr'):
|
||||
|
|
@ -348,8 +316,7 @@ def generate_interventions_excel(start_date=None, end_date=None, output_format='
|
|||
symptom_name,
|
||||
contrat_info,
|
||||
source_category_name,
|
||||
intervention.origin_prego_nbr or '',
|
||||
intervention.origin_fms_nbr or '',
|
||||
*hooks.extract_export_row(intervention),
|
||||
documents_str,
|
||||
notes_str,
|
||||
created_by,
|
||||
|
|
|
|||
|
|
@ -1,147 +1,54 @@
|
|||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const sourceCategorySelect = document.getElementById('id_source_category');
|
||||
const sourceDetailContainer = document.getElementById('source-detail-container');
|
||||
const sourceRefContainer = document.getElementById('source-ref-container'); // PREGO
|
||||
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');
|
||||
const sourceRefContainer = document.getElementById('source-ref-container');
|
||||
|
||||
// Fonction pour afficher/masquer le champ détail de la source
|
||||
function toggleSourceDetail() {
|
||||
function handleSourceChange() {
|
||||
if (!sourceCategorySelect) return;
|
||||
const selectedValue = sourceCategorySelect.value;
|
||||
// Rechercher la catégorie sélectionnée dans sourceCategoriesData
|
||||
const selectedCategory = sourceCategoriesData.find(sc => sc.id == selectedValue);
|
||||
const selectedOption = sourceCategorySelect.options[sourceCategorySelect.selectedIndex];
|
||||
const selectedText = selectedOption ? selectedOption.text : '';
|
||||
|
||||
if (selectedCategory && selectedCategory.has_reference && !['PREGO','OBSERVATION','OBSERVATIE'].includes(selectedCategory.name)) {
|
||||
sourceDetailContainer.classList.remove('d-none'); // Afficher
|
||||
} else {
|
||||
sourceDetailContainer.classList.add('d-none'); // Masquer
|
||||
let selectedCategory = null;
|
||||
if (typeof sourceCategoriesData !== 'undefined' && Array.isArray(sourceCategoriesData)) {
|
||||
selectedCategory = sourceCategoriesData.find(sc => sc.id == selectedValue);
|
||||
}
|
||||
|
||||
// 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
|
||||
function syncNoFmsState() {
|
||||
if (!noFmsCheckbox || !fmsInput) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (noFmsCheckbox.checked) {
|
||||
fmsInput.value = '';
|
||||
fmsInput.setAttribute('disabled', 'disabled');
|
||||
} else {
|
||||
fmsInput.removeAttribute('disabled');
|
||||
}
|
||||
if (sourceCategorySelect) {
|
||||
handleSourceChange();
|
||||
sourceCategorySelect.addEventListener('change', handleSourceChange);
|
||||
}
|
||||
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 ---
|
||||
const sourceSel = document.getElementById('id_source_category');
|
||||
const pregoEl = document.getElementById('id_origin_prego_nbr');
|
||||
const fmsEl = document.getElementById('id_origin_fms_nbr');
|
||||
const noFmsEl = document.getElementById('id_no_origin_fms');
|
||||
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.')
|
||||
});
|
||||
}
|
||||
// Dispatch CustomEvent to allow custom validation rules injection (e.g., PREGO/FMS validations in forks)
|
||||
const customRulesEvent = new CustomEvent('streetup:collect-validation-rules', {
|
||||
detail: { rules: rules, isVisible: isVisible, gettext: gettext }
|
||||
});
|
||||
document.dispatchEvent(customRulesEvent);
|
||||
|
||||
|
||||
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)
|
||||
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
|
||||
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) ---
|
||||
// --- Helper: check for inactive assets/locations (corrective maintenance only) ---
|
||||
|
|
@ -604,6 +543,5 @@ function ensureFmsWarningModal() {
|
|||
|
||||
|
||||
window.focusFirstFormError = focusFirstFormError;
|
||||
window.getFmsWarningMessage = getFmsWarningMessage;
|
||||
window.getAssetLocationStatusWarningsAsync = getAssetLocationStatusWarningsAsync;
|
||||
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>
|
||||
{{ form.source_detail }}
|
||||
</div>
|
||||
<!-- Référence générique (utilisée par ex. pour OBSERVATION) -->
|
||||
<div class="mb-3 d-none" id="source-ref-generic-container">
|
||||
<!-- Référence de la source -->
|
||||
<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>
|
||||
{{ form.source_ref }}
|
||||
</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>
|
||||
|
||||
|
|
|
|||
|
|
@ -81,14 +81,12 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{% if intervention.source_ref or intervention.origin_prego_nbr 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 %}">
|
||||
{% 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 %}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>
|
||||
<div class="field-display">
|
||||
<span class="field-value">
|
||||
{% if intervention.source_category.name_fr == 'PREGO' and intervention.origin_prego_nbr %}
|
||||
<a href="https://jira.sprb-gob.brussels/browse/MNT-{{ intervention.origin_prego_nbr }}" target="_blank">MNT-{{ intervention.origin_prego_nbr }}</a>
|
||||
{% elif source_observation %}
|
||||
{% if source_observation %}
|
||||
<a href="{% url 'observations:observation_detail' source_observation.id %}">{{ source_observation.code }}</a>
|
||||
{% elif intervention.source_ref %}
|
||||
{{ intervention.source_ref }}
|
||||
|
|
@ -103,24 +101,6 @@
|
|||
</div>
|
||||
{% 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 %}
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -259,7 +259,7 @@
|
|||
</div>
|
||||
<div class="calendar-toolbar__actions">
|
||||
<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 %}>
|
||||
<i class="bi bi-x-lg"></i>
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -833,7 +833,7 @@
|
|||
<div style="position:relative; display:flex;">
|
||||
<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;"
|
||||
placeholder="{% translate 'Titre, code, localisation, contrat, Prego, FMS…' %}">
|
||||
placeholder="{% translate 'Titre, code, localisation, contrat…' %}">
|
||||
<button type="button" id="interventions-search-clear"
|
||||
style="position:absolute; right:4px; top:50%; transform:translateY(-50%);
|
||||
background:none; border:none; padding:0 2px; line-height:1;
|
||||
|
|
|
|||
|
|
@ -403,19 +403,17 @@ def interventions_events(request):
|
|||
|
||||
if selected_search:
|
||||
words = selected_search.split()
|
||||
interventions = interventions.annotate(
|
||||
_prego_str=Cast('origin_prego_nbr', output_field=CharField()),
|
||||
_fms_str=Cast('origin_fms_nbr', output_field=CharField()),
|
||||
)
|
||||
from common.hooks import hooks
|
||||
interventions = hooks.apply_search_annotations(interventions)
|
||||
def _word_q(word):
|
||||
return (
|
||||
q_base = (
|
||||
Q(title__icontains=word)
|
||||
| Q(code__icontains=word)
|
||||
| Q(location_code__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()
|
||||
for w in words:
|
||||
combined &= _word_q(w)
|
||||
|
|
|
|||
|
|
@ -36,7 +36,6 @@ class InterventionForm(forms.ModelForm):
|
|||
|
||||
documents = MultipleFileField(required=False, label=_("Documents"))
|
||||
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):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
|
@ -87,7 +86,6 @@ class InterventionForm(forms.ModelForm):
|
|||
|
||||
self.fields['documents'].widget.attrs.update({'class': 'form-control'})
|
||||
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)
|
||||
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',
|
||||
'maintain_type', 'type', 'origin_type', 'begin_time', 'end_time',
|
||||
'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',
|
||||
'reason_for_correction', 'correction_details',
|
||||
'documents', 'project'
|
||||
|
|
@ -185,8 +183,6 @@ class InterventionForm(forms.ModelForm):
|
|||
'source_category': forms.Select(attrs={'class': 'form-select'}),
|
||||
'source_detail': 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'}),
|
||||
'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}),
|
||||
}
|
||||
|
||||
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):
|
||||
"""Return True when the selected source category corresponds to OBSERVATION (FR/NL)."""
|
||||
if not source_category:
|
||||
|
|
@ -258,9 +245,6 @@ class InterventionForm(forms.ModelForm):
|
|||
if not cleaned_data.get('origin_type'):
|
||||
cleaned_data['origin_type'] = 'demand'
|
||||
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 []
|
||||
|
||||
# Ensure consistent list of uploaded files
|
||||
|
|
@ -297,27 +281,6 @@ class InterventionForm(forms.ModelForm):
|
|||
_("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)
|
||||
if maintain_type == 'corrective' and self._is_observation_source(source_category):
|
||||
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)."),
|
||||
)
|
||||
|
||||
# Validation spécifique externe via les hooks
|
||||
from common.hooks import hooks
|
||||
cleaned_data = hooks.run_form_cleaners(self, cleaned_data)
|
||||
|
||||
return cleaned_data
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -510,19 +510,17 @@ def interventions_geojson(request):
|
|||
|
||||
if search_q:
|
||||
words = search_q.split()
|
||||
qs = qs.annotate(
|
||||
_prego_str=Cast('origin_prego_nbr', output_field=CharField()),
|
||||
_fms_str=Cast('origin_fms_nbr', output_field=CharField()),
|
||||
)
|
||||
from common.hooks import hooks
|
||||
qs = hooks.apply_search_annotations(qs)
|
||||
def _word_q(word):
|
||||
return (
|
||||
q_base = (
|
||||
Q(title__icontains=word)
|
||||
| Q(code__icontains=word)
|
||||
| Q(location_code__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()
|
||||
for w in words:
|
||||
combined &= _word_q(w)
|
||||
|
|
@ -648,19 +646,17 @@ def interventions_geojson(request):
|
|||
search_mode = 'and'
|
||||
if search_q:
|
||||
words = search_q.split()
|
||||
qs = qs.annotate(
|
||||
_prego_str=Cast('origin_prego_nbr', output_field=CharField()),
|
||||
_fms_str=Cast('origin_fms_nbr', output_field=CharField()),
|
||||
)
|
||||
from common.hooks import hooks
|
||||
qs = hooks.apply_search_annotations(qs)
|
||||
def _word_q(word):
|
||||
return (
|
||||
q_base = (
|
||||
Q(title__icontains=word)
|
||||
| Q(code__icontains=word)
|
||||
| Q(location_code__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()
|
||||
if search_mode == 'and':
|
||||
for _w in words:
|
||||
|
|
@ -779,21 +775,19 @@ def interventions_search(request):
|
|||
base_qs = (
|
||||
filter_viewable_interventions_for_user(request.user)
|
||||
.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):
|
||||
return (
|
||||
q_base = (
|
||||
Q(title__icontains=word)
|
||||
| Q(code__icontains=word)
|
||||
| Q(location_code__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()
|
||||
if mode == 'and':
|
||||
|
|
@ -807,16 +801,17 @@ def interventions_search(request):
|
|||
|
||||
results = []
|
||||
for itv in qs:
|
||||
results.append({
|
||||
item = {
|
||||
'id': itv.id,
|
||||
'code': itv.code or '',
|
||||
'title': itv.title or '',
|
||||
'location_code': itv.location_code or '',
|
||||
'status': itv.status or '',
|
||||
'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})
|
||||
|
||||
|
|
@ -938,8 +933,12 @@ def export_interventions(request):
|
|||
(_("Catégorie source"), lambda i: (i.source_category.getName() if i.source_category else '')),
|
||||
(_("Détail source"), lambda i: i.source_detail 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 '')),
|
||||
(_("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 '')),
|
||||
|
|
@ -957,7 +956,7 @@ def export_interventions(request):
|
|||
(_("Adresse"), lambda i: i.address or ''),
|
||||
(_("Longitude"), lambda i: i.lon if i.lon is not None else ''),
|
||||
(_("Latitude"), lambda i: i.lat if i.lat is not None else ''),
|
||||
]
|
||||
])
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -1775,19 +1775,17 @@ def apply_intervention_filters_from_querydict(qs, params, user=None):
|
|||
if search_mode not in ('and', 'or'):
|
||||
search_mode = 'and'
|
||||
words = search_q.split()
|
||||
qs = qs.annotate(
|
||||
_prego_str=Cast('origin_prego_nbr', output_field=CharField()),
|
||||
_fms_str=Cast('origin_fms_nbr', output_field=CharField()),
|
||||
)
|
||||
from common.hooks import hooks
|
||||
qs = hooks.apply_search_annotations(qs)
|
||||
def _word_q(word):
|
||||
return (
|
||||
q_base = (
|
||||
Q(title__icontains=word)
|
||||
| Q(code__icontains=word)
|
||||
| Q(location_code__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()
|
||||
if search_mode == 'and':
|
||||
for w in words:
|
||||
|
|
@ -2414,6 +2412,11 @@ def _update_intervention_field(intervention, field, new_value, user_config=None)
|
|||
new_value: La nouvelle valeur
|
||||
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 len(new_value) <= 200:
|
||||
intervention.title = new_value
|
||||
|
|
@ -2625,14 +2628,6 @@ def _update_intervention_field(intervention, field, new_value, user_config=None)
|
|||
return True, None
|
||||
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":
|
||||
if len(new_value) <= 1024:
|
||||
|
|
|
|||
|
|
@ -686,10 +686,12 @@ class InterventionPDF(FPDF):
|
|||
if intervention.source_category and intervention.source_category.has_reference:
|
||||
label_fr = "Référence:"
|
||||
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}"
|
||||
elif intervention.source_category.name_fr == "PREGO":
|
||||
text_value = f"MNT-{intervention.origin_prego_nbr}" if intervention.origin_prego_nbr else ""
|
||||
else:
|
||||
text_value = intervention.source_ref or ""
|
||||
else:
|
||||
|
|
@ -1515,11 +1517,9 @@ class InterventionReportPDF(FPDF):
|
|||
if intervention.source_detail:
|
||||
self._add_field(_("Détail source"), intervention.source_detail)
|
||||
|
||||
if intervention.origin_prego_nbr:
|
||||
self._add_field("PREGO", f"MNT-{intervention.origin_prego_nbr}")
|
||||
|
||||
if intervention.origin_fms_nbr:
|
||||
self._add_field("Fix My Street", intervention.origin_fms_nbr)
|
||||
from common.hooks import hooks
|
||||
for label, value in hooks.extract_pdf_fields(intervention):
|
||||
self._add_field(label, value)
|
||||
|
||||
if intervention.init_call_confirmed_time:
|
||||
self._add_field(_("Appel confirmé"),
|
||||
|
|
|
|||
|
|
@ -391,6 +391,14 @@ if 'test' in sys.argv:
|
|||
PASSWORD_HASHERS = [
|
||||
'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:
|
||||
sys.argv.remove('--recreate-db')
|
||||
os.environ['RECREATE_DB'] = '1'
|
||||
|
|
|
|||
Loading…
Reference in a new issue