gmao/tests/integration/test_checkpoint_c4_complete.py
root 254018d132
Some checks are pending
CI - Tests et Syntax / lint-and-test (push) Waiting to run
test(c4): complete contract and consumable coverage
2026-08-24 18:17:27 +00:00

185 lines
9.7 KiB
Python

from datetime import date, datetime, timedelta, timezone
import pytest
from app_new.extensions import db
from app_new.core.models import (
Alert, Company, Consumable, ConsumableOrder, ConsumableUsage, Equipment, EquipmentConsumable,
Meter, MeterReading, PhotocopierContract, PhotocopierContractPeriod, User,
)
from app_new.core.services.c4_service import (
C4DomainError, assign_equipment_to_contract, calculate_contract_usage,
cancel_consumable_order, close_photocopier_contract, create_consumable_order,
create_contract, estimate_consumable_stock_coverage, receive_consumable_order,
visible_open_c4_alerts,
)
def _company(name):
row = Company(name=name)
db.session.add(row)
db.session.flush()
return row
def _equipment(name, device_type="photocopier"):
row = Equipment(name=name, device_type=device_type, serial_number=f"TEST_UI_C4_COMPLETE_{name}")
db.session.add(row)
db.session.flush()
return row
def _meter(equipment, name, role="COPIER_BW"):
row = Meter(equipment=equipment, name=name, meter_type="photocopieur", unit="pages", contract_role=role)
db.session.add(row)
db.session.flush()
return row
def _reading(meter, value, day):
row = MeterReading(meter=meter, value=value,
reading_date=datetime.combine(day, datetime.min.time(), tzinfo=timezone.utc))
db.session.add(row)
db.session.flush()
return row
def test_c4_complete_estimates_missing_contract_start_and_quality(app):
with app.app_context():
supplier = _company("TEST_UI_C4_COMPLETE_ESTIMATE_SUPPLIER")
contract = create_contract(supplier_id=supplier.id, name="TEST_UI_C4_COMPLETE_ESTIMATE",
start_date=date(2026, 10, 1), end_date=date(2027, 9, 30),
anniversary_month=10, anniversary_day=1)
period = contract.periods[0]
machine = _equipment("TEST_UI_C4_COMPLETE_ESTIMATE_MACHINE")
assign_equipment_to_contract(contract=contract, equipment=machine, entry_date=period.start_date)
meter = _meter(machine, "N&B", "COPIER_BW")
_reading(meter, 9000, date(2026, 9, 1))
_reading(meter, 9140, date(2026, 9, 15)) # historique fiable : 10/jour
_reading(meter, 9440, date(2026, 10, 15))
_reading(meter, 9610, date(2026, 11, 1))
usage = calculate_contract_usage(period)
# 10 jours scolaires environ avant le premier relevé + delta mesuré.
assert usage["bw"] > 170
assert usage["quality"] == "ESTIMATED"
def test_c4_complete_missing_contract_start_without_history_is_partial(app):
with app.app_context():
supplier = _company("TEST_UI_C4_COMPLETE_PARTIAL_SUPPLIER")
contract = create_contract(supplier_id=supplier.id, name="TEST_UI_C4_COMPLETE_PARTIAL",
start_date=date(2026, 10, 1), end_date=date(2027, 9, 30),
anniversary_month=10, anniversary_day=1)
period = contract.periods[0]
machine = _equipment("TEST_UI_C4_COMPLETE_PARTIAL_MACHINE")
assign_equipment_to_contract(contract=contract, equipment=machine, entry_date=period.start_date)
meter = _meter(machine, "N&B", "COPIER_BW")
_reading(meter, 100, date(2026, 10, 15))
_reading(meter, 200, date(2026, 11, 1))
usage = calculate_contract_usage(period)
assert usage["bw"] == 100
assert usage["quality"] == "PARTIAL"
def test_c4_complete_unknown_contract_role_is_ignored(app):
with app.app_context():
supplier = _company("TEST_UI_C4_COMPLETE_ROLE_SUPPLIER")
contract = create_contract(supplier_id=supplier.id, name="TEST_UI_C4_COMPLETE_ROLE",
start_date=date(2026, 1, 1), end_date=date(2026, 12, 31),
anniversary_month=1, anniversary_day=1)
period = contract.periods[0]
machine = _equipment("TEST_UI_C4_COMPLETE_ROLE_MACHINE")
assign_equipment_to_contract(contract=contract, equipment=machine, entry_date=period.start_date)
_meter(machine, "Heures", "NONE")
usage = calculate_contract_usage(period)
assert usage["bw"] == 0 and usage["color"] == 0
def test_c4_complete_close_contract_truncates_current_and_future_history(app):
with app.app_context():
supplier = _company("TEST_UI_C4_COMPLETE_CLOSE_SUPPLIER")
contract = create_contract(supplier_id=supplier.id, name="TEST_UI_C4_COMPLETE_CLOSE",
start_date=date(2026, 10, 1), end_date=date(2031, 9, 30),
anniversary_month=10, anniversary_day=1)
machine = _equipment("TEST_UI_C4_COMPLETE_CLOSE_MACHINE")
assignment = assign_equipment_to_contract(contract=contract, equipment=machine, entry_date=date(2026, 10, 1))
close_photocopier_contract(contract=contract, closed_at=date(2028, 3, 15))
assert contract.status == "CLOSED" and contract.end_date == date(2028, 3, 15)
assert assignment.exit_date == date(2028, 3, 15)
assert contract.periods[1].status == "CLOSED"
assert contract.periods[2].status == "CANCELLED"
assert contract.periods[2].start_date == date(2028, 10, 1)
def test_c4_complete_new_contract_after_close_preserves_meter_identity(app):
with app.app_context():
supplier = _company("TEST_UI_C4_COMPLETE_RESET_SUPPLIER")
old = create_contract(supplier_id=supplier.id, name="TEST_UI_C4_COMPLETE_OLD",
start_date=date(2026, 1, 1), end_date=date(2026, 12, 31),
anniversary_month=1, anniversary_day=1)
machine = _equipment("TEST_UI_C4_COMPLETE_RESET_MACHINE")
assignment = assign_equipment_to_contract(contract=old, equipment=machine, entry_date=date(2026, 1, 1))
meter = _meter(machine, "N&B", "COPIER_BW")
_reading(meter, 100, date(2026, 1, 1))
close_photocopier_contract(contract=old, closed_at=date(2026, 9, 30))
new = create_contract(supplier_id=supplier.id, name="TEST_UI_C4_COMPLETE_NEW",
start_date=date(2026, 10, 1), end_date=date(2027, 9, 30),
anniversary_month=10, anniversary_day=1)
new_assignment = assign_equipment_to_contract(contract=new, equipment=machine, entry_date=date(2026, 10, 1))
assert assignment.exit_date == date(2026, 9, 30)
assert new_assignment.entry_date == date(2026, 10, 1)
assert meter.id and meter.current_value == 0
def test_c4_complete_daily_view_alerts_are_open_and_permission_filtered(app, admin_user):
with app.app_context():
quota = Alert(alert_type="QUOTA_OVERAGE_PROJECTED", title="Quota", message="Quota à examiner", is_resolved=False)
stock = Alert(alert_type="CONSUMABLE_STOCK_LOW", title="Stock", message="Stock à examiner", is_resolved=False)
closed = Alert(alert_type="CONSUMABLE_STOCK_LOW", title="Ancienne", message="Ne pas afficher", is_resolved=True)
db.session.add_all([quota, stock, closed])
db.session.commit()
rows = visible_open_c4_alerts(db.session.get(User, admin_user["id"]))
assert {row.alert_type for row in rows} >= {"QUOTA_OVERAGE_PROJECTED", "CONSUMABLE_STOCK_LOW"}
assert all(row.is_resolved is False for row in rows)
def test_c4_complete_cancel_partial_order_keeps_stock_and_aggregates_open_orders(app, admin_user):
with app.app_context():
consumable = Consumable(name="TEST_UI_C4_COMPLETE_ORDER", quantity=1, min_quantity=2, target_quantity=8)
db.session.add(consumable)
db.session.commit()
first = create_consumable_order(consumable=consumable, quantity=5, user_id=admin_user["id"])
second = create_consumable_order(consumable=consumable, quantity=4, user_id=admin_user["id"])
receive_consumable_order(order=first, quantity=3, user_id=admin_user["id"])
assert first.remaining_quantity == 2 and second.remaining_quantity == 4
cancel_consumable_order(order=first, user_id=admin_user["id"], reason="TEST_UI_C4_COMPLETE_CANCEL")
assert first.status == "CANCELLED"
assert consumable.quantity == 4 # stock reçu conservé
assert second.remaining_quantity == 4
with pytest.raises(C4DomainError):
cancel_consumable_order(order=first, user_id=admin_user["id"], reason="second appel")
def test_c4_complete_stock_coverage_uses_active_compatible_machines(app):
with app.app_context():
consumable = Consumable(name="TEST_UI_C4_COMPLETE_COVERAGE", quantity=3)
a, b, retired = (_equipment("TEST_UI_C4_COMPLETE_COVERAGE_A", "printer"),
_equipment("TEST_UI_C4_COMPLETE_COVERAGE_B", "printer"),
_equipment("TEST_UI_C4_COMPLETE_COVERAGE_RETIRED", "printer"))
retired.lifecycle_status = "retired"
db.session.add(consumable)
db.session.flush()
db.session.add_all([EquipmentConsumable(equipment=item, consumable=consumable) for item in (a, b, retired)])
base = datetime(2026, 1, 1, tzinfo=timezone.utc)
for equipment, offsets in ((a, (0, 30, 60)), (b, (0, 60, 120))):
for offset in offsets:
db.session.add(ConsumableUsage(consumable=consumable, equipment=equipment,
quantity_used=1, used_at=base + timedelta(days=offset)))
db.session.commit()
coverage = estimate_consumable_stock_coverage(consumable=consumable)
assert coverage["machines_total"] == 2
assert coverage["machines_with_history"] == 2
assert coverage["quality"] == "ESTIMATED"
assert round(coverage["coverage_days"]) == 60
ConsumableUsage.query.update({ConsumableUsage.used_by_id: None}, synchronize_session=False)
db.session.commit()