95 lines
5.3 KiB
Python
95 lines
5.3 KiB
Python
|
|
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
|