Intégrer le workflow des travaux aux interventions
Some checks are pending
CI - Tests et Syntax / lint-and-test (push) Waiting to run

This commit is contained in:
root 2026-08-20 22:43:46 +00:00
parent 937883d17d
commit 0ecc833647
5 changed files with 153 additions and 2 deletions

View file

@ -17,6 +17,17 @@ INTERVENTION_STATUSES = {
"annulee": {"label": "Annulée", "color": "danger", "icon": "times-circle"}, "annulee": {"label": "Annulée", "color": "danger", "icon": "times-circle"},
"reportee": {"label": "Reportée", "color": "info", "icon": "calendar-alt"}, "reportee": {"label": "Reportée", "color": "info", "icon": "calendar-alt"},
"ignoree": {"label": "Ignorée", "color": "secondary", "icon": "dash-circle"}, "ignoree": {"label": "Ignorée", "color": "secondary", "icon": "dash-circle"},
# Étapes détaillées d'une demande de travaux (dans la fiche intervention)
"attente_chef": {"label": "Validation du chef", "color": "warning", "icon": "person-check"},
"demande_departement": {"label": "Demande au Département", "color": "info", "icon": "building"},
"attente_devis": {"label": "Devis attendu", "color": "orange", "icon": "file-earmark-text"},
"devis_a_valider": {"label": "Devis à valider", "color": "warning", "icon": "file-check"},
"devis_accepte": {"label": "Devis accepté", "color": "success", "icon": "check2-circle"},
"entreprise_mandatee": {"label": "Entreprise mandatée", "color": "info", "icon": "truck"},
"travaux_planifies": {"label": "Travaux planifiés", "color": "primary", "icon": "calendar-check"},
"travaux_en_cours": {"label": "Travaux en cours", "color": "primary", "icon": "tools"},
"travaux_termines": {"label": "Travaux terminés", "color": "success", "icon": "check-circle"},
"reception": {"label": "Réception", "color": "success", "icon": "clipboard-check"},
} }
INTERVENTION_TRANSITIONS = { INTERVENTION_TRANSITIONS = {
@ -31,6 +42,16 @@ INTERVENTION_TRANSITIONS = {
"cloturee": set(), "cloturee": set(),
"annulee": {"brouillon"}, "annulee": {"brouillon"},
"ignoree": {"planifiee"}, "ignoree": {"planifiee"},
"attente_chef": {"en_cours", "demande_departement", "attente_devis", "annulee"},
"demande_departement": {"attente_devis", "devis_a_valider", "annulee"},
"attente_devis": {"devis_a_valider", "travaux_en_cours", "annulee"},
"devis_a_valider": {"devis_accepte", "attente_devis", "annulee"},
"devis_accepte": {"entreprise_mandatee", "travaux_planifies", "annulee"},
"entreprise_mandatee": {"travaux_planifies", "travaux_en_cours", "annulee"},
"travaux_planifies": {"travaux_en_cours", "reportee", "annulee"},
"travaux_en_cours": {"travaux_termines", "en_attente_pieces", "annulee"},
"travaux_termines": {"reception", "cloturee"},
"reception": {"cloturee", "travaux_en_cours"},
} }
# Statuts équipements # Statuts équipements

View file

@ -43,6 +43,20 @@ class Intervention(db.Model):
# Entreprise extérieure # Entreprise extérieure
company_id = db.Column(db.Integer, db.ForeignKey("companies.id"), nullable=True) company_id = db.Column(db.Integer, db.ForeignKey("companies.id"), nullable=True)
# Workflow travaux intégré à l'intervention
execution_mode = db.Column(db.String(30), nullable=False, default="interne")
department_service_id = db.Column(db.Integer, db.ForeignKey("services.id"), nullable=True)
department_request_date = db.Column(db.Date, nullable=True)
chief_validation_date = db.Column(db.Date, nullable=True)
department_validation_date = db.Column(db.Date, nullable=True)
quote_status = db.Column(db.String(30), nullable=False, default="non_requis")
quote_reference = db.Column(db.String(100), nullable=True)
quote_amount_ht = db.Column(db.Float, nullable=True)
quote_amount_ttc = db.Column(db.Float, nullable=True)
quote_date = db.Column(db.Date, nullable=True)
quote_valid_until = db.Column(db.Date, nullable=True)
work_notes = db.Column(db.Text, nullable=True)
# Équipement # Équipement
equipment_id = db.Column(db.Integer, db.ForeignKey("equipments.id"), nullable=True) equipment_id = db.Column(db.Integer, db.ForeignKey("equipments.id"), nullable=True)
@ -129,6 +143,8 @@ class Intervention(db.Model):
room = db.relationship("Room", foreign_keys=[room_id]) room = db.relationship("Room", foreign_keys=[room_id])
preventive_task = db.relationship("PreventiveTask", foreign_keys=[preventive_task_id]) preventive_task = db.relationship("PreventiveTask", foreign_keys=[preventive_task_id])
lot_task = db.relationship("LotTask", foreign_keys=[lot_task_id]) lot_task = db.relationship("LotTask", foreign_keys=[lot_task_id])
company = db.relationship("Company", foreign_keys=[company_id])
department_service = db.relationship("Service", foreign_keys=[department_service_id])
@property @property
def status_color(self): def status_color(self):
@ -144,6 +160,11 @@ class Intervention(db.Model):
'cloturee': 'dark', 'cloturee': 'dark',
'annulee': 'danger', 'annulee': 'danger',
'ignoree': 'secondary', 'ignoree': 'secondary',
'attente_chef': 'warning', 'demande_departement': 'info',
'attente_devis': 'orange', 'devis_a_valider': 'warning',
'devis_accepte': 'success', 'entreprise_mandatee': 'info',
'travaux_planifies': 'primary', 'travaux_en_cours': 'primary',
'travaux_termines': 'success', 'reception': 'success',
} }
return colors.get(self.status, 'secondary') return colors.get(self.status, 'secondary')
@ -161,6 +182,11 @@ class Intervention(db.Model):
'cloturee': 'Cloturee', 'cloturee': 'Cloturee',
'annulee': 'Annulee', 'annulee': 'Annulee',
'ignoree': 'Ignoree', 'ignoree': 'Ignoree',
'attente_chef': 'Validation du chef', 'demande_departement': 'Demande au Département',
'attente_devis': 'Devis attendu', 'devis_a_valider': 'Devis à valider',
'devis_accepte': 'Devis accepté', 'entreprise_mandatee': 'Entreprise mandatée',
'travaux_planifies': 'Travaux planifiés', 'travaux_en_cours': 'Travaux en cours',
'travaux_termines': 'Travaux terminés', 'reception': 'Réception',
} }
return labels.get(self.status, self.status) return labels.get(self.status, self.status)

View file

@ -21,6 +21,7 @@ from ..core.models.maintenance import Intervention, StatusChange, InterventionCo
from ..core.models.planning import ScheduledTask from ..core.models.planning import ScheduledTask
from ..core.models.equipment import Equipment from ..core.models.equipment import Equipment
from ..core.models.college import Room from ..core.models.college import Room
from ..core.models.company import Service, Company
from app_new.constants import INTERVENTION_STATUSES, INTERVENTION_TRANSITIONS, PRIORITIES from app_new.constants import INTERVENTION_STATUSES, INTERVENTION_TRANSITIONS, PRIORITIES
interventions_bp = Blueprint('interventions', __name__) interventions_bp = Blueprint('interventions', __name__)
@ -386,9 +387,39 @@ def create_for_group():
def detail(id): def detail(id):
"""Détail d'une intervention.""" """Détail d'une intervention."""
intervention = Intervention.query.get_or_404(id) intervention = Intervention.query.get_or_404(id)
transitions = [(key, value['label']) for key, value in INTERVENTION_STATUSES.items()
if key in INTERVENTION_TRANSITIONS.get(intervention.status, set())]
history = intervention.status_history.order_by(StatusChange.created_at.desc()).all()
return render_template('interventions/detail.html', return render_template('interventions/detail.html',
intervention=intervention, intervention=intervention,
statuses=INTERVENTION_STATUSES) statuses=INTERVENTION_STATUSES,
transitions=transitions,
history=history,
services=Service.query.filter_by(is_active=True).order_by(Service.name).all(),
companies=Company.query.filter_by(is_active=True).order_by(Company.name).all())
@interventions_bp.route('/<int:id>/work-details', methods=['POST'])
@login_required
def save_work_details(id):
"""Enregistre les informations de travaux depuis la fiche intervention."""
intervention = Intervention.query.get_or_404(id)
intervention.execution_mode = request.form.get('execution_mode', 'interne')
service_id = request.form.get('department_service_id')
intervention.department_service_id = int(service_id) if service_id and service_id.isdigit() else None
company_id = request.form.get('company_id')
intervention.company_id = int(company_id) if company_id and company_id.isdigit() else None
intervention.quote_status = request.form.get('quote_status', 'non_requis')
intervention.quote_reference = request.form.get('quote_reference') or None
intervention.quote_amount_ht = float(request.form['quote_amount_ht']) if request.form.get('quote_amount_ht') else None
intervention.quote_amount_ttc = float(request.form['quote_amount_ttc']) if request.form.get('quote_amount_ttc') else None
for field in ('department_request_date', 'chief_validation_date', 'department_validation_date', 'quote_date', 'quote_valid_until'):
value = request.form.get(field)
setattr(intervention, field, datetime.strptime(value, '%Y-%m-%d').date() if value else None)
intervention.work_notes = request.form.get('work_notes') or None
db.session.commit()
flash('Workflow travaux enregistré.', 'success')
return redirect(url_for('interventions.detail', id=id))
@interventions_bp.route('/<int:id>/edit', methods=['GET', 'POST']) @interventions_bp.route('/<int:id>/edit', methods=['GET', 'POST'])

View file

@ -52,7 +52,7 @@
<div class="card-body py-2 py-md-3"> <div class="card-body py-2 py-md-3">
<h6 class="text-muted mb-2" style="font-size:0.85rem;"><i class="bi bi-diagram-3"></i> Workflow</h6> <h6 class="text-muted mb-2" style="font-size:0.85rem;"><i class="bi bi-diagram-3"></i> Workflow</h6>
<div class="d-flex flex-wrap align-items-center gap-1" style="font-size:0.75rem;"> <div class="d-flex flex-wrap align-items-center gap-1" style="font-size:0.75rem;">
{% set workflow_order = ['brouillon', 'attente_chef', 'valide_chef', 'demande_gima', 'gima_accepte', 'budget_previsionnel', 'en_cours', 'en_attente_pieces', 'en_attente_entreprise', 'terminee', 'cloturee'] %} {% set workflow_order = ['brouillon', 'attente_chef', 'demande_departement', 'attente_devis', 'devis_a_valider', 'devis_accepte', 'entreprise_mandatee', 'travaux_planifies', 'travaux_en_cours', 'travaux_termines', 'reception', 'cloturee'] if intervention.execution_mode != 'interne' else ['brouillon', 'attente_chef', 'en_cours', 'en_attente_pieces', 'en_attente_entreprise', 'terminee', 'cloturee'] %}
{% for step in workflow_order %} {% for step in workflow_order %}
{% if step in statuses %} {% if step in statuses %}
{% set is_current = (step == intervention.status) %} {% set is_current = (step == intervention.status) %}
@ -202,6 +202,51 @@
</div> </div>
{% endif %} {% endif %}
<!-- Workflow travaux intégré -->
<div class="card border-0 shadow-sm mb-3 border-start border-warning border-4">
<div class="card-header bg-light d-flex justify-content-between align-items-center">
<h6 class="mb-0"><i class="bi bi-hammer text-warning"></i> Suivi des travaux</h6>
<span class="badge bg-secondary">Dans cette intervention</span>
</div>
<div class="card-body">
<form method="POST" action="{{ url_for('interventions.save_work_details', id=intervention.id) }}">
<div class="row g-2">
<div class="col-md-4">
<label class="form-label small">Mode de réalisation</label>
<select name="execution_mode" class="form-select form-select-sm">
{% for value, label in [('interne', 'Travaux réalisés par moi-même'), ('chef', 'Validation de mon chef'), ('departement', 'Demande au Département'), ('entreprise', 'Entreprise extérieure')] %}
<option value="{{ value }}" {% if intervention.execution_mode == value %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-4">
<label class="form-label small">Service du Département</label>
<select name="department_service_id" class="form-select form-select-sm">
<option value="">— Non concerné —</option>
{% for service in services %}<option value="{{ service.id }}" {% if intervention.department_service_id == service.id %}selected{% endif %}>{{ service.name }}</option>{% endfor %}
</select>
</div>
<div class="col-md-4">
<label class="form-label small">Entreprise mandatée</label>
<select name="company_id" class="form-select form-select-sm">
<option value="">— Aucune —</option>
{% for company in companies %}<option value="{{ company.id }}" {% if intervention.company_id == company.id %}selected{% endif %}>{{ company.name }}</option>{% endfor %}
</select>
</div>
<div class="col-md-4"><label class="form-label small">État du devis</label><select name="quote_status" class="form-select form-select-sm">{% for value,label in [('non_requis','Non requis'),('attendu','Attendu'),('recu','Reçu'),('valide_chef','Validé par le chef'),('valide_departement','Validé par le Département'),('refuse','Refusé')] %}<option value="{{ value }}" {% if intervention.quote_status == value %}selected{% endif %}>{{ label }}</option>{% endfor %}</select></div>
<div class="col-md-4"><label class="form-label small">Référence devis</label><input name="quote_reference" class="form-control form-control-sm" value="{{ intervention.quote_reference or '' }}"></div>
<div class="col-md-2"><label class="form-label small">Montant HT</label><input type="number" step="0.01" name="quote_amount_ht" class="form-control form-control-sm" value="{{ intervention.quote_amount_ht or '' }}"></div>
<div class="col-md-2"><label class="form-label small">Montant TTC</label><input type="number" step="0.01" name="quote_amount_ttc" class="form-control form-control-sm" value="{{ intervention.quote_amount_ttc or '' }}"></div>
<div class="col-md-4"><label class="form-label small">Date demande Département</label><input type="date" name="department_request_date" class="form-control form-control-sm" value="{{ intervention.department_request_date or '' }}"></div>
<div class="col-md-4"><label class="form-label small">Date validation chef</label><input type="date" name="chief_validation_date" class="form-control form-control-sm" value="{{ intervention.chief_validation_date or '' }}"></div>
<div class="col-md-4"><label class="form-label small">Date validation Département</label><input type="date" name="department_validation_date" class="form-control form-control-sm" value="{{ intervention.department_validation_date or '' }}"></div>
<div class="col-12"><label class="form-label small">Notes de suivi</label><textarea name="work_notes" class="form-control form-control-sm" rows="2">{{ intervention.work_notes or '' }}</textarea></div>
</div>
<button class="btn btn-warning btn-sm mt-3"><i class="bi bi-save"></i> Enregistrer le suivi</button>
</form>
</div>
</div>
<!-- Équipement --> <!-- Équipement -->
<div class="card border-0 shadow-sm mb-3"> <div class="card border-0 shadow-sm mb-3">
<div class="card-body"> <div class="card-body">

View file

@ -0,0 +1,28 @@
"""Intègre le workflow des travaux aux interventions."""
from alembic import op
import sqlalchemy as sa
revision = "fd4e5f6a7b8c"
down_revision = "fc3d4e5f6a7b"
branch_labels = None
depends_on = None
def upgrade():
op.add_column("interventions", sa.Column("execution_mode", sa.String(30), nullable=False, server_default="interne"))
op.add_column("interventions", sa.Column("department_service_id", sa.Integer(), sa.ForeignKey("services.id"), nullable=True))
op.add_column("interventions", sa.Column("department_request_date", sa.Date(), nullable=True))
op.add_column("interventions", sa.Column("chief_validation_date", sa.Date(), nullable=True))
op.add_column("interventions", sa.Column("department_validation_date", sa.Date(), nullable=True))
op.add_column("interventions", sa.Column("quote_status", sa.String(30), nullable=False, server_default="non_requis"))
op.add_column("interventions", sa.Column("quote_reference", sa.String(100), nullable=True))
op.add_column("interventions", sa.Column("quote_amount_ht", sa.Float(), nullable=True))
op.add_column("interventions", sa.Column("quote_amount_ttc", sa.Float(), nullable=True))
op.add_column("interventions", sa.Column("quote_date", sa.Date(), nullable=True))
op.add_column("interventions", sa.Column("quote_valid_until", sa.Date(), nullable=True))
op.add_column("interventions", sa.Column("work_notes", sa.Text(), nullable=True))
def downgrade():
for name in ("work_notes", "quote_valid_until", "quote_date", "quote_amount_ttc", "quote_amount_ht", "quote_reference", "quote_status", "department_validation_date", "chief_validation_date", "department_request_date", "department_service_id", "execution_mode"):
op.drop_column("interventions", name)