from datetime import date, timedelta from decimal import Decimal from uuid import uuid4 import pytest from app_new.extensions import db from app_new.core.models import ( ProductGeneric, CommercialProduct, ProductPackaging, StockLocation, Staff, ReusableContainer, CleaningForecastConfig, CollegeClosure, ClosureWorkDay, ) from app_new.cleaning.services.stock import ( StockError, receive_stock, issue_stock, transfer_stock, dilute, ) from app_new.cleaning.services.forecast import forecast_product def setup_stock(): product = ProductGeneric(name=f"Nettoyant test {uuid4().hex[:8]}", reference_unit="L", product_type="liquide", forecast_daily_quantity=Decimal("1.2"), stock_security=Decimal("2")) ref = CommercialProduct(generic_product=product, commercial_name="Référence test") packaging = ProductPackaging(generic_product=product, commercial_product=ref, name="Bidon 5 L", purchase_unit="bidon", units_per_package=1, reference_quantity_per_package=Decimal("5")) suffix = uuid4().hex[:8] loc_a = StockLocation(name=f"Stock test A {suffix}") loc_b = StockLocation(name=f"Stock test B {suffix}") db.session.add_all([product, ref, packaging, loc_a, loc_b]) db.session.flush() return product, ref, packaging, loc_a, loc_b def test_reception_conversion_and_fefo(app): with app.app_context(): product, ref, packaging, loc_a, loc_b = setup_stock() receive_stock(generic_product_id=product.id, commercial_product_id=ref.id, packaging_id=packaging.id, packages=2, lot_number="ANCIEN", received_at=date.today(), expiry_date=date.today() + timedelta(days=3), location_id=loc_a.id) receive_stock(generic_product_id=product.id, commercial_product_id=ref.id, packaging_id=packaging.id, packages=2, lot_number="RECENT", received_at=date.today(), expiry_date=date.today() + timedelta(days=90), location_id=loc_a.id) db.session.commit() issue_stock(generic_product_id=product.id, quantity=Decimal("6"), source_location_id=loc_a.id) db.session.commit() lots = {lot.lot_number: sum((b.quantity for b in lot.balances), Decimal("0")) for lot in product.lots} assert lots["ANCIEN"] == Decimal("4.000000") assert lots["RECENT"] == Decimal("10.000000") def test_expired_lot_rejected_and_no_negative_stock(app): with app.app_context(): product, ref, packaging, loc_a, _ = setup_stock() receive_stock(generic_product_id=product.id, commercial_product_id=ref.id, packaging_id=packaging.id, packages=1, lot_number="EXPIRE", received_at=date.today(), expiry_date=date.today() - timedelta(days=1), location_id=loc_a.id) db.session.commit() with pytest.raises(StockError): issue_stock(generic_product_id=product.id, quantity=1, source_location_id=loc_a.id) with pytest.raises(StockError): issue_stock(generic_product_id=product.id, quantity=100, source_location_id=loc_a.id) db.session.rollback() def test_transfer_and_dilution(app): with app.app_context(): product, ref, packaging, loc_a, loc_b = setup_stock() receive_stock(generic_product_id=product.id, commercial_product_id=ref.id, packaging_id=packaging.id, packages=1, lot_number="TRANS", received_at=date.today(), location_id=loc_a.id) container = ReusableContainer(identifier=f"FLACON-TEST-{uuid4().hex[:8]}", container_type="750 ml", volume_liters=Decimal("0.750")) db.session.add(container); db.session.flush() transfer_stock(generic_product_id=product.id, quantity=Decimal("1"), source_location_id=loc_a.id, destination_location_id=loc_b.id) db.session.commit() assert sum((b.quantity for b in product.lots[0].balances if b.location_id == loc_b.id), Decimal("0")) == Decimal("1.000000") dilute(generic_product_id=product.id, total_liters=Decimal("0.750"), ratio_percent=Decimal("2"), source_location_id=loc_b.id, container_id=container.id) db.session.commit() assert container.current_product_id == product.id assert sum((b.quantity for b in product.lots[0].balances if b.location_id == loc_b.id), Decimal("0")) == Decimal("0.985000") def test_forecast_distinguishes_school_days_permanence_and_closure(app): with app.app_context(): product, ref, packaging, loc_a, _ = setup_stock() start = date(2026, 9, 7) closure = CollegeClosure(name="Vacances test", start_date=start + timedelta(days=5), end_date=start + timedelta(days=9), closure_type="vacances_scolaires") db.session.add(closure); db.session.flush() db.session.add(ClosureWorkDay(closure_id=closure.id, work_date=start + timedelta(days=6), start_time=__import__("datetime").time(8), end_time=__import__("datetime").time(12))) db.session.commit() result = forecast_product(product, start + timedelta(days=9), start_date=start) assert result["school_days_consumption"] > 0 assert result["permanence_consumption"] == Decimal("0.480000") assert result["closed_consumption"] == 0