"""HTTP C4 : contrats photocopieurs et consommables partagés.""" from datetime import date from flask import Blueprint, flash, redirect, render_template, request, url_for from flask_login import current_user, login_required from sqlalchemy.orm import joinedload, selectinload from ..core.authorization import permission_required from ..core.models import ( Company, Consumable, ConsumableOrder, ConsumableUsage, ContractEquipmentAssignment, Equipment, EquipmentConsumable, PhotocopierContract, PhotocopierContractPeriod, ) 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, ) from ..extensions import db c4_bp = Blueprint("c4", __name__, template_folder="templates") def _parse_date(value): return date.fromisoformat(value) if value else None @c4_bp.get("/contracts") @login_required @permission_required("contract.view") def contracts(): rows = PhotocopierContract.query.order_by(PhotocopierContract.start_date.desc()).all() return render_template("c4/contracts.html", contracts=rows) @c4_bp.get("/equipment/") @login_required @permission_required("patrimoine.view") def equipment_detail(equipment_id): equipment = Equipment.query.get_or_404(equipment_id) assignments = ContractEquipmentAssignment.query.filter_by(equipment_id=equipment.id).order_by(ContractEquipmentAssignment.entry_date.desc()).all() links = EquipmentConsumable.query.filter_by(equipment_id=equipment.id).all() return render_template("c4/equipment_detail.html", equipment=equipment, assignments=assignments, links=links) @c4_bp.route("/contracts/new", methods=["GET", "POST"]) @login_required @permission_required("contract.manage") def new_contract(): companies = Company.query.order_by(Company.name).all() if request.method == "POST": try: contract = create_contract( supplier_id=request.form.get("supplier_id", type=int), name=request.form.get("name", "").strip(), reference=request.form.get("reference") or None, start_date=_parse_date(request.form.get("start_date")), end_date=_parse_date(request.form.get("end_date")), anniversary_month=request.form.get("anniversary_month", type=int), anniversary_day=request.form.get("anniversary_day", type=int), quota_start_date=_parse_date(request.form.get("quota_start_date")), notes=request.form.get("notes"), ) flash("Contrat photocopieur créé.", "success") return redirect(url_for("c4.contract_detail", contract_id=contract.id)) except (ValueError, TypeError, C4DomainError) as exc: db.session.rollback() flash(str(exc), "danger") return render_template("c4/contract_form.html", companies=companies) @c4_bp.get("/contracts/") @login_required @permission_required("contract.view") def contract_detail(contract_id): contract = (PhotocopierContract.query.options( joinedload(PhotocopierContract.supplier), selectinload(PhotocopierContract.periods), selectinload(PhotocopierContract.assignments).selectinload(ContractEquipmentAssignment.equipment), ).get_or_404(contract_id)) periods = contract.periods projections = {period.id: project_contract_usage(period) for period in periods} data = {period.id: projections[period.id]["usage"] for period in periods} return render_template("c4/contract_detail.html", contract=contract, periods=periods, usage=data, projections=projections) @c4_bp.post("/contracts//equipment") @login_required @permission_required("contract.manage") def contract_equipment(contract_id): contract = PhotocopierContract.query.get_or_404(contract_id) equipment = Equipment.query.get_or_404(request.form.get("equipment_id", type=int)) try: assign_equipment_to_contract(contract=contract, equipment=equipment, entry_date=_parse_date(request.form.get("entry_date")), exit_date=_parse_date(request.form.get("exit_date"))) flash("Machine rattachée au contrat.", "success") except (ValueError, C4DomainError) as exc: db.session.rollback() flash(str(exc), "danger") return redirect(url_for("c4.contract_detail", contract_id=contract_id)) @c4_bp.post("/contracts//close") @login_required @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") return redirect(url_for("c4.contract_detail", contract_id=contract_id)) @c4_bp.get("/consumables") @login_required @permission_required("stock.view") def consumables(): rows = Consumable.query.filter_by(is_active=True).order_by(Consumable.name).all() return render_template("c4/consumables.html", consumables=rows) @c4_bp.get("/consumables/") @login_required @permission_required("stock.view") 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) @c4_bp.post("/consumables//orders") @login_required @permission_required("stock.manage") def order_consumable(consumable_id): consumable = Consumable.query.get_or_404(consumable_id) try: create_consumable_order(consumable=consumable, quantity=request.form.get("quantity", type=int), supplier_id=request.form.get("supplier_id", type=int), user_id=current_user.id) flash("Commande créée.", "success") except (ValueError, C4DomainError) as exc: db.session.rollback() flash(str(exc), "danger") return redirect(url_for("c4.consumable_detail", consumable_id=consumable_id)) @c4_bp.post("/orders//receive") @login_required @permission_required("stock.receive") def receive_order(order_id): order = ConsumableOrder.query.get_or_404(order_id) try: receive_consumable_order(order=order, quantity=request.form.get("quantity", type=int), user_id=current_user.id, comment=request.form.get("comment")) flash("Réception enregistrée ; le stock a été augmenté.", "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") def issue_consumable(consumable_id): consumable = Consumable.query.get_or_404(consumable_id) equipment = Equipment.query.get_or_404(request.form.get("equipment_id", type=int)) try: record_consumable_issue(consumable=consumable, equipment=equipment, quantity=request.form.get("quantity", type=int, default=1), user_id=current_user.id, notes=request.form.get("notes")) flash("Sortie de stock enregistrée.", "success") except (ValueError, C4DomainError) as exc: db.session.rollback() flash(str(exc), "danger") return redirect(url_for("c4.consumable_detail", consumable_id=consumable_id))