99 lines
3.8 KiB
Python
99 lines
3.8 KiB
Python
"""The Inventory Planning header must quote COSMOS's own figures, not ours re-derived.
|
|
|
|
The weekly cells always matched, because they come straight from the `invp-insight` dateMap.
|
|
The HEADER did not: it was recomputed from our sales-insight history, so one SKU read
|
|
56 / 62 / 65 here against 64 / 130 / 67 in COSMOS Inventory Planning -- same product, same
|
|
screen position, two different numbers.
|
|
|
|
Fixed by carrying COSMOS's fields through verbatim:
|
|
|
|
daily sale ours 7-day mean 56 -> COSMOS `averageSale` 64
|
|
middle cell ours 30-day mean 62 -> COSMOS `potentialDailySale` 130 (a DIFFERENT
|
|
quantity: a demand estimate, not a trailing mean)
|
|
third cell ours 90-day mean 65 -> COSMOS `averageSale6Months` 67
|
|
on hand first projected week 5,029 -> COSMOS `inventory` 3,999
|
|
inbound every arrival 1,770 -> the IMMEDIATE arrival 1,030
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import pathlib
|
|
|
|
SRC = pathlib.Path(__file__).resolve().parents[1] / "app.py"
|
|
TEXT = SRC.read_text(encoding="utf-8")
|
|
|
|
|
|
def _table_body() -> str:
|
|
return TEXT.split("def inventory_table_html")[1].split("\ndef ")[0]
|
|
|
|
|
|
def test_header_reads_cosmos_fields_not_history_means():
|
|
body = _table_body()
|
|
assert "invp_stats" in body, "header no longer reads COSMOS's own figures"
|
|
assert '_st.get("avg_sale")' in body
|
|
assert '_st.get("potential_daily_sale")' in body
|
|
assert '_st.get("avg_6m")' in body
|
|
|
|
|
|
def test_the_old_history_derived_cells_are_gone():
|
|
body = _table_body()
|
|
assert "{d30:,.0f}" not in body, "30-day history mean is back in the header"
|
|
assert "{d90:,.0f}" not in body, "90-day history mean is back in the header"
|
|
|
|
|
|
def test_on_hand_is_cosmos_inventory_not_the_first_projected_week():
|
|
"""5,029 is week one WITH its arrival folded in; 3,999 is the shelf."""
|
|
body = _table_body()
|
|
assert 'get("invp_stats") or {}).get("inventory")' in body
|
|
assert "on hand" in body
|
|
|
|
|
|
def test_inbound_is_the_immediate_arrival_with_later_ones_reported_separately():
|
|
body = _table_body()
|
|
assert 'total_inbound = float(proj[0]["warehouse_arrival"] or 0)' in body
|
|
assert "later_inbound" in body, "later arrivals must still be reported, not dropped"
|
|
|
|
|
|
# ---------------------------------------------------------------- encoding hygiene
|
|
# app.py and dashboard/live_data.py were once written back through a cp1252 round trip, so
|
|
# every em dash, middot and emoji in the UI rendered as garbage. It showed in the BROWSER, not
|
|
# just an editor, which makes it a user-visible defect rather than a tidiness one.
|
|
#
|
|
# The signatures are built with chr() rather than spelled out, so this file cannot match its
|
|
# own check.
|
|
_ROOT = pathlib.Path(__file__).resolve().parents[1]
|
|
_MOJIBAKE = (
|
|
chr(0xE2) + chr(0x20AC), # em/en dash, curly quotes, ellipsis
|
|
chr(0xC2) + chr(0xB7), # middot
|
|
chr(0xF0) + chr(0x178), # any 4-byte emoji
|
|
chr(0xC3) + chr(0xA2), # doubly encoded
|
|
)
|
|
_SKIP = {".venv", "__pycache__", "new_archtetcure_and frontend", ".mojibake_backup"}
|
|
|
|
|
|
def _py_sources():
|
|
for f in _ROOT.rglob("*.py"):
|
|
if not _SKIP.intersection(f.parts):
|
|
yield f
|
|
|
|
|
|
def test_no_source_file_is_mojibake():
|
|
bad = {}
|
|
for f in _py_sources():
|
|
text = f.read_bytes().decode("utf-8-sig")
|
|
hits = sum(text.count(m) for m in _MOJIBAKE)
|
|
if hits:
|
|
bad[str(f.relative_to(_ROOT))] = hits
|
|
assert not bad, f"mojibake in: {bad}"
|
|
|
|
|
|
def test_no_source_file_has_a_bom():
|
|
"""A BOM breaks `ast.parse` on a utf-8 read and shows as a stray char in some editors."""
|
|
bom = bytes([0xEF, 0xBB, 0xBF])
|
|
bad = [str(f.relative_to(_ROOT)) for f in _py_sources() if f.read_bytes()[:3] == bom]
|
|
assert not bad, f"UTF-8 BOM in: {bad}"
|
|
|
|
|
|
def test_every_source_file_is_valid_utf8():
|
|
for f in _py_sources():
|
|
f.read_bytes().decode("utf-8")
|