diff --git a/app_new/__init__.py b/app_new/__init__.py index 5899c2b..cf9de43 100644 --- a/app_new/__init__.py +++ b/app_new/__init__.py @@ -138,6 +138,8 @@ def create_app(config_name='default'): app.register_blueprint(housing_bp, url_prefix='/housing') from .prevention import prevention_bp app.register_blueprint(prevention_bp, url_prefix='/prevention') + from .works import works_bp + app.register_blueprint(works_bp, url_prefix='/works') # Blueprints des intégrations from .pronote.routes import pronote_bp diff --git a/app_new/core/models/__init__.py b/app_new/core/models/__init__.py index 7a1e0b1..4fc7185 100644 --- a/app_new/core/models/__init__.py +++ b/app_new/core/models/__init__.py @@ -7,7 +7,7 @@ from .college import College, Site, Building, HousingUnit, HousingOccupancy, Roo from .equipment import EquipmentCategory, Equipment, EquipmentDocument, EquipmentRoomHistory, EquipmentQuantityMovement, EquipmentLifecycleEvent from .maintenance import ( Intervention, StatusChange, InterventionComment, InterventionDocument, - InterventionPart, Lot, LotTask, LotService + InterventionPart, Lot, LotTask, LotService, WorkRequest, WorkRequestQuote, WorkRequestEvent ) from .company import Company, Part, PartStockMovement, Alert, Service from .planning import ( @@ -25,7 +25,7 @@ __all__ = [ 'College', 'Site', 'Building', 'HousingUnit', 'HousingOccupancy', 'Room', 'RoomType', 'RoomSchedule', 'Zone', 'EquipmentCategory', 'Equipment', 'EquipmentDocument', 'EquipmentRoomHistory', 'EquipmentQuantityMovement', 'EquipmentLifecycleEvent', 'Intervention', 'StatusChange', 'InterventionComment', 'InterventionDocument', - 'Lot', 'LotTask', 'LotService', + 'Lot', 'LotTask', 'LotService', 'WorkRequest', 'WorkRequestQuote', 'WorkRequestEvent', 'Company', 'Part', 'PartStockMovement', 'InterventionPart', 'Alert', 'Service', 'WorkSchedule', 'WorkScheduleTemplate', 'AnnualTimeConfig', 'TimeEntry', 'CollegeClosure', 'ClosureSchedule', 'ClosureWorkDay', 'PersonalLeave', 'Training', 'TrainingParticipant', 'Meter', 'MeterReading', 'Consumable', 'ConsumableUsage', 'EquipmentConsumable', diff --git a/app_new/core/models/maintenance.py b/app_new/core/models/maintenance.py index d02bbf1..5cf6564 100644 --- a/app_new/core/models/maintenance.py +++ b/app_new/core/models/maintenance.py @@ -415,6 +415,68 @@ class LotTask(db.Model): return f"" +class WorkRequest(db.Model): + """Demande de travaux suivie en interne par le technicien.""" + __tablename__ = "work_requests" + id = db.Column(db.Integer, primary_key=True) + title = db.Column(db.String(255), nullable=False) + description = db.Column(db.Text, nullable=False) + status = db.Column(db.String(40), nullable=False, default="brouillon", index=True) + execution_mode = db.Column(db.String(30), nullable=False, default="interne") + priority = db.Column(db.String(20), nullable=False, default="normale") + equipment_id = db.Column(db.Integer, db.ForeignKey("equipments.id"), nullable=True) + service_id = db.Column(db.Integer, db.ForeignKey("services.id"), nullable=True) + company_id = db.Column(db.Integer, db.ForeignKey("companies.id"), nullable=True) + created_by_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False) + chief_decision = db.Column(db.String(20), nullable=True) + department_decision = db.Column(db.String(20), nullable=True) + chief_decision_date = db.Column(db.Date, nullable=True) + department_decision_date = db.Column(db.Date, nullable=True) + planned_date = db.Column(db.Date, nullable=True) + completed_date = db.Column(db.Date, nullable=True) + actual_minutes = db.Column(db.Integer, nullable=True) + parts_cost = db.Column(db.Numeric(10, 2), nullable=True) + notes = db.Column(db.Text, nullable=True) + created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc)) + updated_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc)) + equipment = db.relationship("Equipment") + service = db.relationship("Service") + company = db.relationship("Company") + created_by = db.relationship("User") + quotes = db.relationship("WorkRequestQuote", back_populates="request", cascade="all, delete-orphan") + events = db.relationship("WorkRequestEvent", back_populates="request", cascade="all, delete-orphan", order_by="WorkRequestEvent.created_at") + + +class WorkRequestQuote(db.Model): + __tablename__ = "work_request_quotes" + id = db.Column(db.Integer, primary_key=True) + request_id = db.Column(db.Integer, db.ForeignKey("work_requests.id", ondelete="CASCADE"), nullable=False) + company_id = db.Column(db.Integer, db.ForeignKey("companies.id"), nullable=True) + reference = db.Column(db.String(100), nullable=True) + amount_ht = db.Column(db.Numeric(10, 2), nullable=True) + amount_ttc = db.Column(db.Numeric(10, 2), nullable=True) + quote_date = db.Column(db.Date, nullable=True) + valid_until = db.Column(db.Date, nullable=True) + status = db.Column(db.String(20), nullable=False, default="en_attente") + file_path = db.Column(db.String(500), nullable=True) + notes = db.Column(db.Text, nullable=True) + request = db.relationship("WorkRequest", back_populates="quotes") + company = db.relationship("Company") + + +class WorkRequestEvent(db.Model): + __tablename__ = "work_request_events" + id = db.Column(db.Integer, primary_key=True) + request_id = db.Column(db.Integer, db.ForeignKey("work_requests.id", ondelete="CASCADE"), nullable=False) + user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False) + old_status = db.Column(db.String(40), nullable=True) + new_status = db.Column(db.String(40), nullable=False) + comment = db.Column(db.Text, nullable=True) + created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc)) + request = db.relationship("WorkRequest", back_populates="events") + user = db.relationship("User") + + class LotService(db.Model): """Association entre lots et services.""" __tablename__ = "lot_services" diff --git a/app_new/templates/works/detail.html b/app_new/templates/works/detail.html new file mode 100644 index 0000000..537badc --- /dev/null +++ b/app_new/templates/works/detail.html @@ -0,0 +1 @@ +{% extends "base.html" %}{% block title %}Demande de travaux{% endblock %}{% block content %}

{{ item.title }}

{{ item.description }}

Historique

{% endblock %} diff --git a/app_new/templates/works/form.html b/app_new/templates/works/form.html new file mode 100644 index 0000000..fe0b612 --- /dev/null +++ b/app_new/templates/works/form.html @@ -0,0 +1 @@ +{% extends "base.html" %}{% block title %}Nouvelle demande{% endblock %}{% block content %}

Nouvelle demande de travaux

{% endblock %} diff --git a/app_new/templates/works/index.html b/app_new/templates/works/index.html new file mode 100644 index 0000000..cc246ad --- /dev/null +++ b/app_new/templates/works/index.html @@ -0,0 +1 @@ +{% extends "base.html" %}{% block title %}Demandes de travaux{% endblock %}{% block content %}

Demandes de travaux

Nouvelle demande
{% for item in requests %}{% else %}{% endfor %}
DemandeModeStatutDate
{{ item.title }}{{ item.execution_mode }}{{ item.status }}{{ item.created_at|date_fmt }}
Aucune demande.
{% endblock %} diff --git a/app_new/works/__init__.py b/app_new/works/__init__.py new file mode 100644 index 0000000..86ab692 --- /dev/null +++ b/app_new/works/__init__.py @@ -0,0 +1 @@ +from .routes import works_bp diff --git a/app_new/works/routes.py b/app_new/works/routes.py new file mode 100644 index 0000000..a9d2c4c --- /dev/null +++ b/app_new/works/routes.py @@ -0,0 +1,32 @@ +from datetime import date +from flask import Blueprint, flash, redirect, render_template, request, url_for +from flask_login import current_user, login_required +from ..extensions import db +from ..core.models import WorkRequest, WorkRequestEvent, WorkRequestQuote, Equipment, Company, Service + +works_bp = Blueprint("works", __name__, template_folder="templates") + +STATUSES = ["brouillon", "à_valider_chef", "travaux_internes_autorisés", "devis_en_attente", "devis_à_valider_chef", "devis_accepté", "envoyée_département", "entreprise_mandatée", "travaux_planifiés", "travaux_en_cours", "travaux_terminés", "clôturée", "refusée"] + +@works_bp.route("/") +@login_required +def index(): + return render_template("works/index.html", requests=WorkRequest.query.order_by(WorkRequest.created_at.desc()).all()) + +@works_bp.route("/new", methods=["GET", "POST"]) +@login_required +def new(): + if request.method == "POST": + item = WorkRequest(title=request.form["title"].strip(), description=request.form["description"].strip(), execution_mode=request.form.get("execution_mode", "interne"), priority=request.form.get("priority", "normale"), equipment_id=request.form.get("equipment_id", type=int) or None, created_by_id=current_user.id, status="à_valider_chef") + db.session.add(item); db.session.flush(); db.session.add(WorkRequestEvent(request=item, user_id=current_user.id, new_status=item.status, comment="Demande créée")); db.session.commit(); flash("Demande de travaux créée.", "success"); return redirect(url_for("works.detail", id=item.id)) + return render_template("works/form.html", equipments=Equipment.query.filter_by(is_deleted=False).order_by(Equipment.name).all()) + +@works_bp.route("/", methods=["GET", "POST"]) +@login_required +def detail(id): + item = WorkRequest.query.get_or_404(id) + if request.method == "POST": + old = item.status; item.status = request.form.get("status", old); item.notes = request.form.get("notes") or item.notes; item.company_id = request.form.get("company_id", type=int) or item.company_id; item.service_id = request.form.get("service_id", type=int) or item.service_id + if item.status != old: db.session.add(WorkRequestEvent(request=item, user_id=current_user.id, old_status=old, new_status=item.status, comment=request.form.get("comment"))) + db.session.commit(); flash("Demande mise à jour.", "success"); return redirect(url_for("works.detail", id=id)) + return render_template("works/detail.html", item=item, statuses=STATUSES, companies=Company.query.order_by(Company.name).all(), services=Service.query.order_by(Service.name).all())