""" Contracts Routes - GMAO College Suivi des contrats d'entreprise. """ from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify from flask_login import login_required, current_user from ..extensions import db from ..contracts.models import Contract, ContractVisit from ..core.models.maintenance import Lot from ..core.models.equipment import Equipment from ..companies.routes import Company from datetime import datetime, date, timedelta, timezone def _optional_date(value): return datetime.strptime(value, '%Y-%m-%d').date() if value else None def _apply_visits(contract): contract.visit_interval_days = request.form.get('visit_interval_days', type=int) contract.last_visit_date = _optional_date(request.form.get('last_visit_date')) contract.next_visit_date = _optional_date(request.form.get('next_visit_date')) def _generate_visits(contract, horizon_days=365): """Matérialise le calendrier contractuel sans créer de doublons.""" if not contract.visit_interval_days or contract.visit_interval_days <= 0: return 0 cursor = contract.next_visit_date or contract.start_date if contract.last_visit_date and cursor <= contract.last_visit_date: cursor = contract.last_visit_date + timedelta(days=contract.visit_interval_days) horizon = min(date.today() + timedelta(days=horizon_days), contract.end_date) if contract.end_date else date.today() + timedelta(days=horizon_days) existing = {visit.scheduled_date for visit in contract.visits} created = 0 while cursor and cursor <= horizon: if cursor >= contract.start_date and cursor not in existing: db.session.add(ContractVisit(contract=contract, scheduled_date=cursor)) created += 1 cursor += timedelta(days=contract.visit_interval_days) return created contracts_bp = Blueprint('contracts', __name__, url_prefix='/contracts', template_folder='templates') @contracts_bp.route('/') @login_required def index(): """Liste de tous les contrats.""" contracts = Contract.query.order_by( db.case((Contract.end_date.is_(None), 1), else_=0), Contract.end_date.asc() ).all() # Stats active = sum(1 for c in contracts if c.is_active) expiring = sum(1 for c in contracts if c.is_expiring_soon) notice = sum(1 for c in contracts if c.must_send_notice_now) expired = sum(1 for c in contracts if c.status == 'expire' or (c.end_date and c.end_date < date.today() and c.status == 'actif')) return render_template('contracts/index.html', contracts=contracts, active_count=active, expiring_count=expiring, notice_count=notice, expired_count=expired) @contracts_bp.route('/new', methods=['GET', 'POST']) @login_required def create(): """Creer un contrat.""" lots = Lot.query.order_by(Lot.name).all() equipments = Equipment.query.order_by(Equipment.name).all() companies = Company.query.order_by(Company.name).all() if request.method == 'POST': contract = Contract( name=request.form.get('name'), contract_number=request.form.get('contract_number'), lot_id=request.form.get('lot_id') or None, equipment_id=request.form.get('equipment_id') or None, company_id=request.form.get('company_id') or None, signed_date=datetime.strptime(request.form.get('signed_date'), '%Y-%m-%d').date() if request.form.get('signed_date') else None, start_date=datetime.strptime(request.form.get('start_date'), '%Y-%m-%d').date() if request.form.get('start_date') else date.today(), end_date=datetime.strptime(request.form.get('end_date'), '%Y-%m-%d').date() if request.form.get('end_date') else None, tacit_renewal=request.form.get('tacit_renewal') == '1', renewal_period_days=int(request.form.get('renewal_period_days', 365)), notice_period_days=int(request.form.get('notice_period_days')) if request.form.get('notice_period_days') else None, notice_deadline=datetime.strptime(request.form.get('notice_deadline'), '%Y-%m-%d').date() if request.form.get('notice_deadline') else None, annual_amount=float(request.form.get('annual_amount')) if request.form.get('annual_amount') else None, status=request.form.get('status', 'actif'), notes=request.form.get('notes', ''), ) db.session.add(contract) _apply_visits(contract) db.session.flush() _generate_visits(contract) db.session.commit() flash('Contrat cree avec succes.', 'success') return redirect(url_for('contracts.detail', id=contract.id)) # Pre-remplir si lot_id ou equipment_id dans l'URL pre_lot = request.args.get('lot_id', type=int) pre_eq = request.args.get('equipment_id', type=int) pre_company = request.args.get('company_id', type=int) return render_template('contracts/form.html', lots=lots, equipments=equipments, companies=companies, pre_lot=pre_lot, pre_eq=pre_eq, pre_company=pre_company, contract=None) @contracts_bp.route('/') @login_required def detail(id): """Detail d'un contrat.""" contract = Contract.query.get_or_404(id) return render_template('contracts/detail.html', contract=contract) @contracts_bp.route('//edit', methods=['GET', 'POST']) @login_required def edit(id): """Modifier un contrat.""" contract = Contract.query.get_or_404(id) lots = Lot.query.order_by(Lot.name).all() equipments = Equipment.query.order_by(Equipment.name).all() companies = Company.query.order_by(Company.name).all() if request.method == 'POST': contract.name = request.form.get('name') contract.contract_number = request.form.get('contract_number') contract.lot_id = request.form.get('lot_id') or None contract.equipment_id = request.form.get('equipment_id') or None contract.company_id = request.form.get('company_id') or None contract.signed_date = datetime.strptime(request.form.get('signed_date'), '%Y-%m-%d').date() if request.form.get('signed_date') else None contract.start_date = datetime.strptime(request.form.get('start_date'), '%Y-%m-%d').date() if request.form.get('start_date') else date.today() contract.end_date = datetime.strptime(request.form.get('end_date'), '%Y-%m-%d').date() if request.form.get('end_date') else None contract.tacit_renewal = request.form.get('tacit_renewal') == '1' contract.renewal_period_days = int(request.form.get('renewal_period_days', 365)) contract.notice_period_days = int(request.form.get('notice_period_days')) if request.form.get('notice_period_days') else None contract.notice_deadline = datetime.strptime(request.form.get('notice_deadline'), '%Y-%m-%d').date() if request.form.get('notice_deadline') else None contract.annual_amount = float(request.form.get('annual_amount')) if request.form.get('annual_amount') else None contract.status = request.form.get('status', 'actif') contract.notes = request.form.get('notes', '') _apply_visits(contract) _generate_visits(contract) db.session.commit() flash('Contrat modifie.', 'success') return redirect(url_for('contracts.detail', id=contract.id)) return render_template('contracts/form.html', contract=contract, lots=lots, equipments=equipments, companies=companies, pre_lot=None, pre_eq=None, pre_company=None) @contracts_bp.route('//delete', methods=['POST']) @login_required def delete(id): """Supprimer un contrat.""" contract = Contract.query.get_or_404(id) db.session.delete(contract) db.session.commit() flash('Contrat supprime.', 'success') return redirect(url_for('contracts.index')) @contracts_bp.route('//visits/generate', methods=['POST']) @login_required def generate_visits(id): contract = Contract.query.get_or_404(id) count = _generate_visits(contract) db.session.commit() flash(f'{count} visite(s) ajoutée(s) au calendrier.', 'success') return redirect(url_for('contracts.detail', id=id)) @contracts_bp.route('//visits//complete', methods=['POST']) @login_required def complete_visit(id, visit_id): contract = Contract.query.get_or_404(id) visit = ContractVisit.query.filter_by(id=visit_id, contract_id=id).first_or_404() visit.status = 'realisee' visit.completed_at = datetime.now(timezone.utc) visit.report = (request.form.get('report') or '').strip() or None contract.last_visit_date = visit.scheduled_date contract.next_visit_date = visit.scheduled_date + timedelta(days=contract.visit_interval_days) if contract.visit_interval_days else None _generate_visits(contract) db.session.commit() flash('Visite extérieure marquée comme réalisée.', 'success') return redirect(url_for('contracts.detail', id=id)) @contracts_bp.route('/api/summary') @login_required def api_summary(): """Resume des contrats pour le dashboard.""" contracts = Contract.query.filter_by(status='actif').all() return jsonify({ 'total': len(contracts), 'active': sum(1 for c in contracts if c.is_active), 'expiring_soon': sum(1 for c in contracts if c.is_expiring_soon), 'must_send_notice': sum(1 for c in contracts if c.must_send_notice_now), 'expired': sum(1 for c in contracts if c.end_date and c.end_date < date.today()), })