2026-08-24 17:56:13 +02:00
|
|
|
"""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 ..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,
|
2026-08-24 18:07:28 +02:00
|
|
|
create_consumable_order, create_contract, evaluate_quota_alert, project_contract_usage,
|
2026-08-24 17:56:13 +02:00
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
2026-08-24 18:07:28 +02:00
|
|
|
@c4_bp.get("/equipment/<int:equipment_id>")
|
|
|
|
|
@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)
|
|
|
|
|
|
|
|
|
|
|
2026-08-24 17:56:13 +02:00
|
|
|
@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/<int:contract_id>")
|
|
|
|
|
@login_required
|
|
|
|
|
@permission_required("contract.view")
|
|
|
|
|
def contract_detail(contract_id):
|
|
|
|
|
contract = PhotocopierContract.query.get_or_404(contract_id)
|
|
|
|
|
periods = contract.periods
|
|
|
|
|
data = {period.id: calculate_contract_usage(period) for period in periods}
|
2026-08-24 18:07:28 +02:00
|
|
|
projections = {period.id: project_contract_usage(period) for period in periods}
|
|
|
|
|
return render_template("c4/contract_detail.html", contract=contract, periods=periods, usage=data, projections=projections)
|
2026-08-24 17:56:13 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@c4_bp.post("/contracts/<int:contract_id>/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/<int:contract_id>/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/<int:consumable_id>")
|
|
|
|
|
@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/<int:consumable_id>/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/<int:order_id>/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/<int:consumable_id>/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))
|