diff --git a/app_new/c4/routes.py b/app_new/c4/routes.py index b86b87b..5085c73 100644 --- a/app_new/c4/routes.py +++ b/app_new/c4/routes.py @@ -12,8 +12,9 @@ from ..core.models import ( ) from ..core.services.c4_service import ( C4DomainError, assign_equipment_to_contract, calculate_contract_usage, - create_consumable_order, create_contract, evaluate_quota_alert, project_contract_usage, - estimate_consumable_lifetime, receive_consumable_order, record_consumable_issue, + cancel_consumable_order, close_photocopier_contract, create_consumable_order, create_contract, + evaluate_quota_alert, estimate_consumable_lifetime, estimate_consumable_stock_coverage, + project_contract_usage, receive_consumable_order, record_consumable_issue, ) from ..extensions import db @@ -99,11 +100,12 @@ def contract_equipment(contract_id): @permission_required("contract.manage") def close_contract(contract_id): contract = PhotocopierContract.query.get_or_404(contract_id) - contract.status = "CLOSED" - contract.closed_at = _parse_date(request.form.get("closed_at")) or date.today() - contract.end_date = min(contract.end_date, contract.closed_at) - db.session.commit() - flash("Contrat clos ; son historique est conservé.", "success") + try: + close_photocopier_contract(contract=contract, closed_at=_parse_date(request.form.get("closed_at"))) + flash("Contrat clos ; son historique est conservé.", "success") + except C4DomainError as exc: + db.session.rollback() + flash(str(exc), "danger") return redirect(url_for("c4.contract_detail", contract_id=contract_id)) @@ -122,8 +124,12 @@ def consumable_detail(consumable_id): consumable = Consumable.query.get_or_404(consumable_id) usages = consumable.usages.order_by(ConsumableUsage.used_at.desc()).limit(100).all() orders = ConsumableOrder.query.filter_by(consumable_id=consumable.id).order_by(ConsumableOrder.id.desc()).all() - compatible = EquipmentConsumable.query.filter_by(consumable_id=consumable.id).all() - return render_template("c4/consumable_detail.html", consumable=consumable, usages=usages, orders=orders, compatible=compatible) + compatible = (EquipmentConsumable.query.filter_by(consumable_id=consumable.id) + .options(joinedload(EquipmentConsumable.equipment)).all()) + coverage = estimate_consumable_stock_coverage(consumable=consumable) + quantity_on_order = sum(order.remaining_quantity for order in orders if order.status in {"ORDERED", "PARTIALLY_RECEIVED"}) + return render_template("c4/consumable_detail.html", consumable=consumable, usages=usages, orders=orders, + compatible=compatible, coverage=coverage, quantity_on_order=quantity_on_order) @c4_bp.post("/consumables//orders") @@ -154,6 +160,20 @@ def receive_order(order_id): return redirect(url_for("c4.consumable_detail", consumable_id=order.consumable_id)) +@c4_bp.post("/orders//cancel") +@login_required +@permission_required("stock.manage") +def cancel_order(order_id): + order = ConsumableOrder.query.get_or_404(order_id) + try: + cancel_consumable_order(order=order, user_id=current_user.id, reason=request.form.get("reason")) + flash("Commande annulée ; le stock déjà reçu est conservé.", "success") + except (ValueError, C4DomainError) as exc: + db.session.rollback() + flash(str(exc), "danger") + return redirect(url_for("c4.consumable_detail", consumable_id=order.consumable_id)) + + @c4_bp.post("/consumables//issue") @login_required @permission_required("stock.issue") diff --git a/app_new/c4/templates/c4/consumable_detail.html b/app_new/c4/templates/c4/consumable_detail.html index e675dba..3a23220 100644 --- a/app_new/c4/templates/c4/consumable_detail.html +++ b/app_new/c4/templates/c4/consumable_detail.html @@ -1,3 +1,3 @@ {% extends "base.html" %} {% block title %}{{ consumable.name }} — GMAO{% endblock %} -{% block content %}

{{ consumable.name }}

Référence : {{ consumable.reference or '—' }} · Type : {{ consumable.consumable_type }} · Stock : {{ consumable.quantity }} · À commander : {{ consumable.quantity_to_order }}

Compatible avec {{ compatible|length }} équipement(s)

    {% for link in compatible %}
  • {{ link.equipment.name }}
  • {% else %}
  • Aucune machine.
  • {% endfor %}

Commandes

{% for order in orders %}{% else %}{% endfor %}
QuantitéReçueÉtatAction
{{ order.quantity_ordered }}{{ order.quantity_received }}{{ order.status }}{% if order.remaining_quantity %}
{% endif %}
Aucune commande.

Historique des sorties

    {% for usage in usages %}
  • {{ usage.used_at }} — {{ usage.quantity_used }} — {{ usage.equipment.name if usage.equipment else '—' }}
  • {% else %}
  • Aucune sortie.
  • {% endfor %}
{% endblock %} +{% block content %}

{{ consumable.name }}

Référence : {{ consumable.reference or '—' }} · Type : {{ consumable.consumable_type }} · Stock : {{ consumable.quantity }} · Minimum : {{ consumable.min_quantity }} · Cible : {{ consumable.target_quantity or '—' }} · À commander : {{ consumable.quantity_to_order }}

Commandé / à recevoir : {{ quantity_on_order }}

Couverture estimée : {% if coverage.coverage_days is not none %}~{{ '%.0f'|format(coverage.coverage_days) }} jours{% else %}indisponible{% endif %} — {{ coverage.quality }} ({{ coverage.machines_with_history }}/{{ coverage.machines_total }} machines avec historique)

Compatible avec {{ compatible|length }} équipement(s)

    {% for link in compatible %}
  • {{ link.equipment.name }}
  • {% else %}
  • Aucune machine.
  • {% endfor %}

Commandes

{% for order in orders %}{% else %}{% endfor %}
QuantitéReçueÀ recevoirÉtatAction
{{ order.quantity_ordered }}{{ order.quantity_received }}{{ order.remaining_quantity }}{{ order.status }}{% if order.remaining_quantity and order.status in ('ORDERED','PARTIALLY_RECEIVED') %}
{% endif %}
Aucune commande.

Historique des sorties

    {% for usage in usages %}
  • {{ usage.used_at }} — {{ usage.quantity_used }} — {{ usage.equipment.name if usage.equipment else '—' }}
  • {% else %}
  • Aucune sortie.
  • {% endfor %}
{% endblock %} diff --git a/app_new/planning/schedules.py b/app_new/planning/schedules.py index 664c7d8..f9e1c67 100644 --- a/app_new/planning/schedules.py +++ b/app_new/planning/schedules.py @@ -86,7 +86,8 @@ def my_day(): } execution_states = {} from ..core.services.meter_analytics import visible_open_meter_alerts - informational_alerts = visible_open_meter_alerts(current_user) + from ..core.services.c4_service import visible_open_c4_alerts + informational_alerts = visible_open_meter_alerts(current_user) + visible_open_c4_alerts(current_user) for task in ScheduledTask.query.filter(ScheduledTask.scheduled_date == target_date).all(): execution_states[task.id] = { 'status': task.status, diff --git a/app_new/templates/planning/my_day.html b/app_new/templates/planning/my_day.html index e93ac7b..793ba59 100644 --- a/app_new/templates/planning/my_day.html +++ b/app_new/templates/planning/my_day.html @@ -1,7 +1,7 @@ {% extends "base.html" %} {% block title %}Ma journée{% endblock %} {% block content %} -{% if informational_alerts %}
Informations à examiner
    {% for alert in informational_alerts %}
  • {{ alert.meter.name }} — {{ 'Critique' if alert.level == 'CRITICAL' else 'Avertissement' }}{% if alert.intervention_id %} — Intervention créée : #{{ alert.intervention_id }}{% endif %}
  • {% endfor %}
Ces alertes n'occupent aucun créneau ; une intervention liée constitue le travail planifiable.
{% endif %} +{% if informational_alerts %}
Informations à examiner
    {% for alert in informational_alerts %}
  • {% if alert.meter %}{{ alert.meter.name }}{% else %}{{ alert.title }}{% endif %} — {{ alert.message }}{% if alert.intervention_id %} — Intervention créée : #{{ alert.intervention_id }}{% endif %}
  • {% endfor %}
Ces alertes n'occupent aucun créneau ; une intervention liée constitue le travail planifiable.
{% endif %}