240 lines
7.4 KiB
Python
240 lines
7.4 KiB
Python
from datetime import date
|
|
from io import BytesIO
|
|
|
|
from openpyxl import Workbook
|
|
from sqlalchemy import create_engine, select
|
|
from sqlalchemy.orm import Session, sessionmaker
|
|
|
|
from app.db.session import Base
|
|
from app.models.mix import Mix
|
|
from app.models.product import Product
|
|
from app.models.raw_material import RawMaterial
|
|
from app.models.throughput import ProductionThroughput, ThroughputProduct
|
|
from app.seed import seed_throughput_products_from_costing
|
|
from app.services.throughput_service import (
|
|
calculate_kg,
|
|
import_names_sheet,
|
|
import_production_sheet,
|
|
normalise_staff_name,
|
|
qa_passed,
|
|
)
|
|
|
|
|
|
def _session() -> Session:
|
|
engine = create_engine("sqlite:///:memory:")
|
|
Base.metadata.create_all(bind=engine)
|
|
return sessionmaker(bind=engine, expire_on_commit=False)()
|
|
|
|
|
|
def _costing_mix(db: Session, tenant_id: str = "hunter-premium-produce") -> Mix:
|
|
raw_material = RawMaterial(
|
|
tenant_id=tenant_id,
|
|
name="Maize",
|
|
unit_of_measure="tonne",
|
|
kg_per_unit=1000,
|
|
status="active",
|
|
)
|
|
db.add(raw_material)
|
|
db.flush()
|
|
mix = Mix(tenant_id=tenant_id, client_name="Hunter", name="Maize Mix")
|
|
db.add(mix)
|
|
db.flush()
|
|
return mix
|
|
|
|
|
|
def test_calculate_kg_bags():
|
|
assert calculate_kg(10, "bags", 20) == 200.0
|
|
|
|
|
|
def test_calculate_kg_kg_ignores_bag_size():
|
|
assert calculate_kg(550, "kg", None) == 550.0
|
|
assert calculate_kg(550, "kg", 20) == 550.0
|
|
|
|
|
|
def test_calculate_kg_zero_quantity():
|
|
assert calculate_kg(0, "bags", 20) == 0.0
|
|
assert calculate_kg(None, "bags", 20) == 0.0
|
|
|
|
|
|
def test_staff_name_normalisation():
|
|
assert normalise_staff_name(" Jake ") == "Jake"
|
|
assert normalise_staff_name("jake smith") == "jake smith"
|
|
assert normalise_staff_name("") is None
|
|
assert normalise_staff_name(None) is None
|
|
|
|
|
|
def test_qa_passed_flag():
|
|
entry = ProductionThroughput(
|
|
production_date=date(2026, 1, 1),
|
|
product_name_snapshot="X",
|
|
quantity=1,
|
|
quantity_type="bags",
|
|
scales_checked=True,
|
|
label_correct=True,
|
|
bag_sealed=True,
|
|
pallet_good_condition=True,
|
|
)
|
|
assert qa_passed(entry) is True
|
|
entry.bag_sealed = False
|
|
assert qa_passed(entry) is False
|
|
|
|
|
|
def _make_workbook() -> BytesIO:
|
|
wb = Workbook()
|
|
names = wb.active
|
|
names.title = "Names"
|
|
names.append(["Name", "Item ID"])
|
|
names.append(["Whole Wheat 20kg", 1001])
|
|
names.append(["Bulka Maize", 1002])
|
|
|
|
production = wb.create_sheet("Production")
|
|
production.append(["#VALUE!", "Operations Throughput"])
|
|
production.append([None] * 8 + ["TEST WEIGHT"])
|
|
production.append([
|
|
"DATE", "GRAIN", "BAG SIZE", "SCALES", "LABEL", "SEALED", "PALLET",
|
|
"BOX", 1, 2, 3, 4, 5, "QTY", "STAFF", "NOTES",
|
|
])
|
|
production.append([date(2026, 4, 1), "Whole Wheat 20kg", 20, True, True, True, True, None, None, None, None, None, None, 100, " Jake ", None])
|
|
production.append([date(2026, 4, 1), "Bulka Maize", None, True, True, False, True, "B7", None, None, None, None, None, 1500, "Alex", "ok"])
|
|
production.append([date(2026, 4, 2), "Whole Wheat 20kg", 20, False, True, True, True, None, None, None, None, None, None, 50, "Jake", None])
|
|
|
|
buf = BytesIO()
|
|
wb.save(buf)
|
|
buf.seek(0)
|
|
return buf
|
|
|
|
|
|
def test_import_names_and_production():
|
|
from openpyxl import load_workbook
|
|
|
|
db = _session()
|
|
wb = load_workbook(_make_workbook(), data_only=True)
|
|
created, _ = import_names_sheet(db, wb, "test-tenant")
|
|
assert created == 2
|
|
|
|
imported, skipped = import_production_sheet(db, wb, "test-tenant")
|
|
assert imported == 3
|
|
assert skipped == 0
|
|
|
|
entries = db.scalars(select(ProductionThroughput).order_by(ProductionThroughput.id)).all()
|
|
bags_entry = entries[0]
|
|
assert bags_entry.quantity_type == "bags"
|
|
assert bags_entry.calculated_kg == 2000.0
|
|
assert bags_entry.staff_name == "Jake" # whitespace trimmed
|
|
|
|
bulka_entry = entries[1]
|
|
assert bulka_entry.quantity_type == "kg"
|
|
assert bulka_entry.calculated_kg == 1500.0
|
|
assert qa_passed(bulka_entry) is False # bag_sealed was False
|
|
|
|
# Product master should have absorbed default_bag_size for the wheat product
|
|
wheat = db.scalar(
|
|
select(ThroughputProduct).where(ThroughputProduct.name == "Whole Wheat 20kg")
|
|
)
|
|
assert wheat is not None
|
|
assert wheat.default_bag_size == 20
|
|
|
|
|
|
def test_product_name_snapshot_preserved_when_product_renamed():
|
|
db = _session()
|
|
product = ThroughputProduct(tenant_id="t", name="Original Name", default_bag_size=20)
|
|
db.add(product)
|
|
db.flush()
|
|
entry = ProductionThroughput(
|
|
tenant_id="t",
|
|
production_date=date(2026, 4, 1),
|
|
product_id=product.id,
|
|
product_name_snapshot=product.name,
|
|
bag_size=20,
|
|
quantity=10,
|
|
quantity_type="bags",
|
|
calculated_kg=200,
|
|
)
|
|
db.add(entry)
|
|
db.flush()
|
|
|
|
product.name = "Renamed Product"
|
|
db.flush()
|
|
|
|
reloaded = db.scalar(select(ProductionThroughput).where(ProductionThroughput.id == entry.id))
|
|
assert reloaded.product_name_snapshot == "Original Name"
|
|
|
|
|
|
def test_seed_throughput_products_from_costing_products():
|
|
db = _session()
|
|
mix = _costing_mix(db)
|
|
db.add_all(
|
|
[
|
|
Product(
|
|
tenant_id="hunter-premium-produce",
|
|
client_name="Hunter",
|
|
item_id="1001",
|
|
name="Whole Wheat 20kg",
|
|
mix_id=mix.id,
|
|
sale_type="standard",
|
|
unit_of_measure="20kg bag",
|
|
visible=True,
|
|
),
|
|
Product(
|
|
tenant_id="hunter-premium-produce",
|
|
client_name="Hunter",
|
|
item_id="1002",
|
|
name="Bulka Maize",
|
|
mix_id=mix.id,
|
|
sale_type="bulka",
|
|
unit_of_measure="tonne",
|
|
visible=False,
|
|
),
|
|
]
|
|
)
|
|
db.flush()
|
|
|
|
report = seed_throughput_products_from_costing(db)
|
|
|
|
assert report == {"created": 2, "updated": 0, "skipped": 0}
|
|
products = db.scalars(select(ThroughputProduct).order_by(ThroughputProduct.item_id)).all()
|
|
assert [product.name for product in products] == ["Whole Wheat 20kg", "Bulka Maize"]
|
|
assert products[0].default_bag_size == 20
|
|
assert products[0].is_bulka_default is False
|
|
assert products[0].active is True
|
|
assert products[1].default_bag_size is None
|
|
assert products[1].is_bulka_default is True
|
|
assert products[1].active is False
|
|
|
|
|
|
def test_seed_throughput_products_from_costing_updates_existing_by_item_id():
|
|
db = _session()
|
|
mix = _costing_mix(db)
|
|
db.add(
|
|
Product(
|
|
tenant_id="hunter-premium-produce",
|
|
client_name="Hunter",
|
|
item_id="1001",
|
|
name="Updated Wheat 25kg",
|
|
mix_id=mix.id,
|
|
sale_type="standard",
|
|
unit_of_measure="25kg bag",
|
|
visible=True,
|
|
)
|
|
)
|
|
db.add(
|
|
ThroughputProduct(
|
|
tenant_id="hunter-premium-produce",
|
|
item_id="1001",
|
|
name="Old Wheat",
|
|
default_bag_size=20,
|
|
active=False,
|
|
notes="Seeded from costing products",
|
|
)
|
|
)
|
|
db.flush()
|
|
|
|
report = seed_throughput_products_from_costing(db)
|
|
|
|
assert report == {"created": 0, "updated": 1, "skipped": 0}
|
|
products = db.scalars(select(ThroughputProduct)).all()
|
|
assert len(products) == 1
|
|
assert products[0].name == "Updated Wheat 25kg"
|
|
assert products[0].default_bag_size == 25
|
|
assert products[0].active is True
|