fix Inventory numbers

main
Talha Ahmed 2026-07-31 20:29:41 +05:00
parent 6a0ce38546
commit 84162554d4
3 changed files with 332 additions and 4 deletions

190
app.py
View File

@ -745,6 +745,127 @@ def inventory_chart(d) -> go.Figure:
return fig return fig
def inventory_reprice_table_html(d, rows: list, u_now: float, u_new: float) -> str:
"""The same weekly grid as `inventory_table_html`, re-run at the recommended price.
Deliberately identical markup and identical cover-band shading, so the two tables can be
read against each other cell for cell a different layout for the same quantities would
make the comparison work the reader has to do.
A third row carries the CHANGE, because the interesting number is the difference and
making someone subtract two grids by eye is how a stock risk gets missed.
"""
if not rows:
return ""
cfg = d["cfg"]
cls = cfg["inv_class"]
heads = "".join(f'<th>{pd.Timestamp(r["date"]):%m/%d/%Y}</th>' for r in rows)
at_new, delta = [], []
for r in rows:
cover = float(r["cover_new"] or 0)
at_new.append(
f'<td style="background:{_bucket_bg(cover, cls)}">'
f'<div class="cell-main">{r["units_new"]:,.0f}</div>'
f'<div class="cell-val">&#36;{r["value_new"]:,.0f}</div>'
f'<div class="cell-sub"><span class="cov">{cover:,.0f}</span>'
f'<span class="arr">{r["arrival"]:,.0f}</span></div></td>'
)
# The delta row is intentionally UNSHADED: colour here would compete with the cover
# bands above and imply a second, unrelated scale.
du, dc = r["delta_units"], (r["cover_new"] or 0) - (r["cover_now"] or 0)
colour = (theme.DELTA_GOOD if du > 0 else
("#c9442b" if du < 0 else theme.MUTED))
delta.append(
f'<td><div class="cell-main" style="color:{colour}">{du:+,.0f}</div>'
f'<div class="cell-sub"><span class="cov">{dc:+,.0f} d</span></div></td>'
)
return (
'<div class="invt-wrap"><table class="invt">'
f'<thead><tr><th>At the recommended price</th><th>Daily Sale</th>{heads}</tr></thead>'
"<tbody>"
# --- row 1: the shelf at the new price
"<tr>"
f'<td><div class="p-t">${d["rec_price"]:,.2f}</div>'
f'<div class="p-s">was ${d["current_price"]:,.2f} · {d["delta_pct"]:+.1f}%</div>'
f'<div class="p-s">our projection · class {cls.capitalize()}</div></td>'
f'<td><div class="cell-main">{u_new:,.0f}</div>'
f'<div class="p-s">was {u_now:,.0f}/day</div>'
f'<div class="p-s">{(u_new / u_now - 1) if u_now else 0:+.1%} demand</div></td>'
f'{"".join(at_new)}'
"</tr>"
# --- row 2: the difference against COSMOS's own projection
"<tr>"
'<td><div class="p-t">Change</div>'
'<div class="p-s">vs today\'s price</div>'
'<div class="p-s">units · cover days</div></td>'
'<td><div class="p-s">&nbsp;</div></td>'
f'{"".join(delta)}'
"</tr>"
"</tbody></table></div>"
)
def inventory_reprice_chart(d, rows: list) -> go.Figure:
"""The same shelf under BOTH prices — today's, and the one being recommended.
Sits under the projection above so the consequence of the price move is read in the same
glance as the move itself. COSMOS's own line is the solid one; ours is dashed, because one
is their forecast and the other is our model of it and the chart should not pretend
otherwise. The gap between the two IS the price effect.
"""
fig = go.Figure()
if not rows:
return fig
dates = [pd.Timestamp(r["date"]) for r in rows]
now = [r["units_now"] for r in rows]
new = [r["units_new"] for r in rows]
# Shaded band between the two lines: green when the move leaves more stock on the shelf,
# red when it burns through faster. Colour is never the only cue — the legend names both
# lines and the caption states which is which.
#
# NOT shaded when the two lines coincide. COSMOS returns a flat projection for some very
# low-velocity SKUs (the same units every week), so there is no draw to re-scale and the
# lines sit on top of each other. A band there would colour a difference that does not
# exist — and with `>=` it painted a 5% price CUT green, implying stock gained.
if any(a != b for a, b in zip(now, new)):
gained = new[-1] > now[-1]
fig.add_trace(go.Scatter(
x=dates + dates[::-1], y=new + now[::-1], fill="toself",
fillcolor=("rgba(10,111,101,0.10)" if gained else "rgba(201,68,43,0.10)"),
line=dict(width=0), hoverinfo="skip", showlegend=False))
fig.add_trace(go.Scatter(
x=dates, y=now, name=f"At ${d['current_price']:,.2f} (today)",
mode="lines+markers", line=dict(color=theme.SERIES["us"], width=2),
hovertemplate="%{x|%d %b}<br>%{y:,.0f} units<extra>today's price</extra>"))
fig.add_trace(go.Scatter(
x=dates, y=new, name=f"At ${d['rec_price']:,.2f} (recommended)",
mode="lines+markers", line=dict(color=theme.SERIES["alt"], width=2, dash="dash"),
hovertemplate="%{x|%d %b}<br>%{y:,.0f} units<extra>recommended price</extra>"))
fig.add_hline(y=0, line_color=theme.STATUS["critical"], line_dash="dot", line_width=1)
# Arrivals move the shelf on BOTH lines, so they are annotated once, on the axis.
for r, dt in zip(rows, dates):
if r["arrival"] > 0:
fig.add_annotation(x=dt, y=0, yref="y", text=f"🚚 +{r['arrival']:,.0f}",
showarrow=False, yshift=-14,
font=dict(size=10, color=theme.INK2))
# The first week the shelf empties under the new price — the whole point of the chart.
out = next((r for r in rows if r["stockout"]), None)
if out:
fig.add_vline(x=pd.Timestamp(out["date"]), line_color=theme.STATUS["critical"],
line_dash="dot", line_width=1)
fig.add_annotation(x=pd.Timestamp(out["date"]), y=max(now + new),
text="empty at the new price", showarrow=False,
font=dict(size=10, color=theme.STATUS["critical"]))
fig.update_yaxes(rangemode="tozero", title_text="units")
fig.update_layout(height=300, legend=dict(orientation="h", y=1.12, x=0))
return fig
# Cover-day color bands (Alpha/Beta scheme, matched to the planning sheet) # Cover-day color bands (Alpha/Beta scheme, matched to the planning sheet)
def _bucket_bg(cover: float, inv_class: str) -> str: def _bucket_bg(cover: float, inv_class: str) -> str:
"""Cell shading by projected cover days, per the SKU's Alpha/Beta band scheme. """Cell shading by projected cover days, per the SKU's Alpha/Beta band scheme.
@ -1656,6 +1777,68 @@ for _, r in rows.iterrows():
st.plotly_chart(inventory_chart(d), use_container_width=True, st.plotly_chart(inventory_chart(d), use_container_width=True,
config={"displayModeBar": False}, key=f"inv_{sku}") config={"displayModeBar": False}, key=f"inv_{sku}")
# ---- and the same shelf at the recommended price, directly below ----
# The projection above is COSMOS's, at TODAY's price, so the price move is
# otherwise shown with no stock consequence at all. Raising slows the burn;
# cutting empties the shelf sooner, which on a SKU near a stockout is the
# difference between a sound move and an expensive one.
_ed = d.get("elasticity_detail") or {}
_el = _ed.get("elasticity") or FALLBACK_ELASTICITY
# COSMOS's OWN daily sale, the same figure the grid above prints — not our 30-day
# mean from sales history. They differ (64 vs 63.3 on the audited SKU), and using
# ours put two different "daily sale" numbers in adjacent tables AND shifted every
# cover day: 5,029 / 63.3 = 79 where COSMOS says 78.
#
# Driving both off COSMOS's rate makes the cover column reproduce COSMOS's own
# coverDays exactly, so the two grids reconcile line for line.
_u_now = float((d.get("invp_stats") or {}).get("avg_sale")
or d.get("units_day") or 0.0)
_ratio = ((d["rec_price"] / d["current_price"]) ** _el
if d.get("current_price") else 1.0)
_rows = reproject_inventory(d.get("inv_projection") or [],
_u_now, _u_now * _ratio)
_moved = abs(d["rec_price"] - d["current_price"]) >= 0.005
st.markdown("---")
if not _moved:
st.caption("**No price change is recommended**, so the shelf above is the "
"outlook. Nothing to re-project.")
elif not _rows:
st.caption(esc(
"Cannot re-project the shelf at the new price for this SKU — that needs "
"COSMOS's weekly projection and a usable demand response, and one of "
"them is missing."))
else:
_last = _rows[-1]
st.markdown(esc(
f"**At the recommended ${d['rec_price']:,.2f}** "
f"({d['delta_pct']:+.1f}%), demand moves {_ratio - 1:+.1%}"
f"**{_last['units_new']:,} units** left at the end of the horizon "
f"against {_last['units_now']:,} at today's price "
f"(**{_last['delta_units']:+,}**)."))
if all(r["delta_units"] == 0 for r in _rows):
st.caption(esc(
f"COSMOS projects no depletion for this SKU — the same "
f"{_rows[0]['units_now']:,} units in every snapshot — so there is no "
f"draw to re-scale and the two lines coincide. Only cover days move."))
# The same grid as above, at the new price, so the two read cell for cell.
st.markdown(
inventory_reprice_table_html(d, _rows, _u_now, _u_now * _ratio),
unsafe_allow_html=True)
st.caption(
"Same layout as the COSMOS grid above and the same Alpha/Beta cover "
"shading, so the two compare cell for cell. **Change** is this projection "
"minus COSMOS's — green = more stock left, red = burns through faster.")
st.plotly_chart(inventory_reprice_chart(d, _rows),
use_container_width=True,
config={"displayModeBar": False}, key=f"invrp_{sku}")
st.caption(
"Solid = COSMOS's projection at today's price. Dashed = **our** model of "
"the same shelf at the recommended price, scaling each week's SALES by "
"the elasticity the Scenarios tab uses. Arrivals are unchanged — a PO "
"already placed does not move because we re-priced — and stock is floored "
"at zero. Full week-by-week numbers are on **🔁 Stock at the new price**.")
elif sel == "repricing": elif sel == "repricing":
# What the recommended price does to the SHELF. The Inventory tab above is # What the recommended price does to the SHELF. The Inventory tab above is
# COSMOS's projection at TODAY's price, so a raise or a cut is otherwise shown # COSMOS's projection at TODAY's price, so a raise or a cut is otherwise shown
@ -1677,7 +1860,10 @@ for _, r in rows.iterrows():
# two rows, so this cannot drift from that table. # two rows, so this cannot drift from that table.
_ed = d.get("elasticity_detail") or {} _ed = d.get("elasticity_detail") or {}
el = _ed.get("elasticity") or FALLBACK_ELASTICITY el = _ed.get("elasticity") or FALLBACK_ELASTICITY
u_now = float(d.get("units_day") or 0.0) # COSMOS's own daily sale — the figure its Inventory Planning grid prints — so the
# cover days here reproduce COSMOS's coverDays rather than sitting a day or two off.
u_now = float((d.get("invp_stats") or {}).get("avg_sale")
or d.get("units_day") or 0.0)
ratio = ((d["rec_price"] / d["current_price"]) ** el ratio = ((d["rec_price"] / d["current_price"]) ** el
if d.get("current_price") else 1.0) if d.get("current_price") else 1.0)
u_new = u_now * ratio u_new = u_now * ratio
@ -1698,7 +1884,7 @@ for _, r in rows.iterrows():
r1.metric("Price", f"${d['rec_price']:,.2f}", r1.metric("Price", f"${d['rec_price']:,.2f}",
f"{d['delta_pct']:+.1f}% vs ${d['current_price']:,.2f}", f"{d['delta_pct']:+.1f}% vs ${d['current_price']:,.2f}",
delta_color="off") delta_color="off")
r2.metric("Units/day", f"{u_new:,.1f}", f"{pct:+.1f}% vs {u_now:,.1f} today", r2.metric("Units/day", f"{u_new:,.0f}", f"{pct:+.1f}% vs {u_now:,.0f} today",
delta_color="off") delta_color="off")
last = rows[-1] last = rows[-1]
r3.metric("Stock at horizon", f"{last['units_new']:,}", r3.metric("Stock at horizon", f"{last['units_new']:,}",

View File

@ -1235,6 +1235,10 @@ def reproject_inventory(projections: list, units_day_now: float, units_day_new:
# #
# The consequence is that this column can differ by a day or two from the Inventory # The consequence is that this column can differ by a day or two from the Inventory
# tab, which shows COSMOS's figure verbatim. That is stated in the caption. # tab, which shows COSMOS's figure verbatim. That is stated in the caption.
# FLOORED, not rounded, because that is what COSMOS does: 5,029 units over a 64/day
# rate is 78.58 days and its grid prints 78. Rounding put every week exactly one day
# above COSMOS's own column, which reads as a discrepancy rather than a convention.
# Flooring is also the safer direction for a stockout figure.
cover_now = (units / units_day_now) if units_day_now > 0 else None cover_now = (units / units_day_now) if units_day_now > 0 else None
cover_new = (running / units_day_new) if units_day_new > 0 else None cover_new = (running / units_day_new) if units_day_new > 0 else None
rows.append({ rows.append({
@ -1243,8 +1247,8 @@ def reproject_inventory(projections: list, units_day_now: float, units_day_new:
"units_new": int(round(running)), "units_new": int(round(running)),
"delta_units": int(round(running - units)), "delta_units": int(round(running - units)),
"value_new": round(running * per_unit), "value_new": round(running * per_unit),
"cover_now": int(round(cover_now)) if cover_now is not None else None, "cover_now": int(cover_now) if cover_now is not None else None,
"cover_new": int(round(cover_new)) if cover_new is not None else None, "cover_new": int(cover_new) if cover_new is not None else None,
# COSMOS's own figure, carried alongside rather than mixed into the comparison. # COSMOS's own figure, carried alongside rather than mixed into the comparison.
"cover_cosmos": (int(p["cover_days"]) if p.get("cover_days") is not None else None), "cover_cosmos": (int(p["cover_days"]) if p.get("cover_days") is not None else None),
"arrival": arrival, "arrival": arrival,

View File

@ -108,6 +108,18 @@ def test_both_cover_columns_use_our_velocity_not_cosmos_own():
assert rows[0]["cover_cosmos"] == 78 assert rows[0]["cover_cosmos"] == 78
def test_cover_is_floored_the_way_cosmos_floors_it():
"""5,029 units at 64/day is 78.58 days and COSMOS's grid prints 78, not 79.
Rounding put every week exactly one day above COSMOS's own column — a convention
difference that reads as a discrepancy. Flooring is also the safer direction for a
stockout figure.
"""
rows = reproject_inventory(proj([5029, 4645]), 64.0, 64.0)
assert rows[0]["cover_now"] == 78 # 78.58 floored, not 79
assert rows[1]["cover_now"] == 72 # 72.58 floored, not 73
@pytest.mark.parametrize("projections,now,new", [ @pytest.mark.parametrize("projections,now,new", [
([], 10.0, 8.0), # no projection from COSMOS ([], 10.0, 8.0), # no projection from COSMOS
(STEADY, 0.0, 8.0), # no current velocity -> ratio undefined (STEADY, 0.0, 8.0), # no current velocity -> ratio undefined
@ -124,3 +136,129 @@ def test_value_stays_on_cosmos_cost_basis():
assert rows[0]["value_new"] == pytest.approx(4450, abs=1) assert rows[0]["value_new"] == pytest.approx(4450, abs=1)
# week 1 keeps half the draw, so more units and proportionally more value # week 1 keeps half the draw, so more units and proportionally more value
assert rows[1]["value_new"] > 900 * 4.45 assert rows[1]["value_new"] > 900 * 4.45
# ---------------------------------------------------------------- the chart
# The comparison chart lives in app.py, which cannot be imported without booting Streamlit,
# so it is loaded from the module head the same way the other UI tests do.
import logging
import pathlib
import types
def _app_head():
logging.disable(logging.CRITICAL)
src = (pathlib.Path(__file__).resolve().parents[1] / "app.py").read_text(encoding="utf-8")
head = src.split("# ---------------------------------------------------------------- load data")[0]
mod = types.ModuleType("apphead")
mod.__dict__.update(__name__="apphead",
__file__=str(pathlib.Path("app.py").resolve()))
exec(compile(head, "app.py", "exec"), mod.__dict__)
return mod
def _band(fig):
"""The shaded region between the two lines, or None when the lines coincide."""
for t in fig.data:
if getattr(t, "fill", None) == "toself":
return "green" if "10,111,101" in t.fillcolor else "red"
return None
DEPLETING = proj([1000, 900, 800, 1200, 1100], arrivals=[0, 0, 0, 500, 0])
def test_a_raise_shades_green_a_cut_shades_red():
mod = _app_head()
d_up = {"current_price": 20.0, "rec_price": 21.0}
d_dn = {"current_price": 20.0, "rec_price": 19.0}
assert _band(mod.inventory_reprice_chart(d_up, reproject_inventory(DEPLETING, 10, 8))) == "green"
assert _band(mod.inventory_reprice_chart(d_dn, reproject_inventory(DEPLETING, 10, 13))) == "red"
def test_coincident_lines_are_not_shaded_at_all():
"""A band would colour a difference that does not exist.
With `>=` this painted a 5% price CUT green on a SKU whose COSMOS projection is flat,
implying stock was gained when nothing had changed.
"""
mod = _app_head()
d = {"current_price": 40.99, "rec_price": 38.94}
rows = reproject_inventory(proj([167] * 5), 0.5, 0.6)
assert all(r["delta_units"] == 0 for r in rows)
assert _band(mod.inventory_reprice_chart(d, rows)) is None
def test_both_prices_are_named_in_the_legend():
"""The reader must be able to tell which line is which without the caption."""
mod = _app_head()
fig = mod.inventory_reprice_chart({"current_price": 20.0, "rec_price": 21.0},
reproject_inventory(DEPLETING, 10, 8))
names = [t.name for t in fig.data if t.name]
assert any("$20.00" in n for n in names) and any("$21.00" in n for n in names)
def test_an_empty_reprojection_yields_an_empty_figure_not_an_error():
mod = _app_head()
fig = mod.inventory_reprice_chart({"current_price": 20.0, "rec_price": 21.0}, [])
assert len(fig.data) == 0
# ---------------------------------------------------------------- the grid
import re as _re
def _grid_cells(html):
"""(units, value, cover, arrival) per week, straight out of the rendered markup."""
return _re.findall(
r'<div class="cell-main">([\d,]+)</div><div class="cell-val">&#36;([\d,]+)</div>'
r'<div class="cell-sub"><span class="cov">([\d,]+)</span>'
r'<span class="arr">([\d,]+)</span>', html)
def _d(cur=20.0, rec=21.0):
return {"cfg": {"inv_class": "alpha", "cost": 4.45}, "current_price": cur,
"rec_price": rec, "delta_pct": (rec / cur - 1) * 100}
def test_the_grid_renders_the_model_not_a_recomputation():
"""Every cell must come from the same rows the chart and the table tab use."""
mod = _app_head()
rows = reproject_inventory(DEPLETING, 10, 8)
html = mod.inventory_reprice_table_html(_d(), rows, 10.0, 8.0)
cells = _grid_cells(html)
assert [int(c[0].replace(",", "")) for c in cells] == [r["units_new"] for r in rows]
assert [int(c[3].replace(",", "")) for c in cells] == [int(r["arrival"]) for r in rows]
def test_the_grid_uses_the_same_cover_bands_as_the_cosmos_table_above():
"""Two grids side by side must not shade the same cover two different ways."""
from dashboard import theme
mod = _app_head()
rows = reproject_inventory(DEPLETING, 10, 8)
html = mod.inventory_reprice_table_html(_d(), rows, 10.0, 8.0)
used = set(_re.findall(r"background:(#[0-9a-f]{6})", html))
valid = {c for _, c, _ in theme.COVER_BANDS["alpha"]}
assert used <= valid, f"shading outside the Alpha band palette: {used - valid}"
def test_the_change_row_reports_both_units_and_cover():
mod = _app_head()
rows = reproject_inventory(DEPLETING, 10, 8)
html = mod.inventory_reprice_table_html(_d(), rows, 10.0, 8.0)
assert "Change" in html and "vs today" in html
# a signed unit delta and a signed cover delta for every week
assert len(_re.findall(r'cell-main" style="color:#[0-9a-fA-F]{6}">[+-]', html)) == len(rows)
assert len(_re.findall(r'<span class="cov">[+-][\d,]+ d</span>', html)) == len(rows)
def test_the_grid_has_the_same_column_count_as_the_projection():
mod = _app_head()
rows = reproject_inventory(DEPLETING, 10, 8)
html = mod.inventory_reprice_table_html(_d(), rows, 10.0, 8.0)
assert html.count("<th>") == len(rows) + 2 # weeks + label + Daily Sale
def test_an_empty_reprojection_renders_nothing_rather_than_an_empty_shell():
mod = _app_head()
assert mod.inventory_reprice_table_html(_d(), [], 10.0, 8.0) == ""