Intégrer le workflow des travaux aux interventions
Some checks are pending
CI - Tests et Syntax / lint-and-test (push) Waiting to run
Some checks are pending
CI - Tests et Syntax / lint-and-test (push) Waiting to run
This commit is contained in:
parent
937883d17d
commit
0ecc833647
5 changed files with 153 additions and 2 deletions
|
|
@ -17,6 +17,17 @@ INTERVENTION_STATUSES = {
|
|||
"annulee": {"label": "Annulée", "color": "danger", "icon": "times-circle"},
|
||||
"reportee": {"label": "Reportée", "color": "info", "icon": "calendar-alt"},
|
||||
"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 = {
|
||||
|
|
@ -31,6 +42,16 @@ INTERVENTION_TRANSITIONS = {
|
|||
"cloturee": set(),
|
||||
"annulee": {"brouillon"},
|
||||
"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
|
||||
|
|
|
|||
|
|
@ -43,6 +43,20 @@ class Intervention(db.Model):
|
|||
# Entreprise extérieure
|
||||
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
|
||||
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])
|
||||
preventive_task = db.relationship("PreventiveTask", foreign_keys=[preventive_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
|
||||
def status_color(self):
|
||||
|
|
@ -144,6 +160,11 @@ class Intervention(db.Model):
|
|||
'cloturee': 'dark',
|
||||
'annulee': 'danger',
|
||||
'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')
|
||||
|
||||
|
|
@ -161,6 +182,11 @@ class Intervention(db.Model):
|
|||
'cloturee': 'Cloturee',
|
||||
'annulee': 'Annulee',
|
||||
'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)
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ from ..core.models.maintenance import Intervention, StatusChange, InterventionCo
|
|||
from ..core.models.planning import ScheduledTask
|
||||
from ..core.models.equipment import Equipment
|
||||
from ..core.models.college import Room
|
||||
from ..core.models.company import Service, Company
|
||||
from app_new.constants import INTERVENTION_STATUSES, INTERVENTION_TRANSITIONS, PRIORITIES
|
||||
|
||||
interventions_bp = Blueprint('interventions', __name__)
|
||||
|
|
@ -386,9 +387,39 @@ def create_for_group():
|
|||
def detail(id):
|
||||
"""Détail d'une intervention."""
|
||||
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',
|
||||
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'])
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@
|
|||
<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>
|
||||
<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 %}
|
||||
{% if step in statuses %}
|
||||
{% set is_current = (step == intervention.status) %}
|
||||
|
|
@ -202,6 +202,51 @@
|
|||
</div>
|
||||
{% 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 -->
|
||||
<div class="card border-0 shadow-sm mb-3">
|
||||
<div class="card-body">
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
Loading…
Reference in a new issue