Spaces:
Running
Running
Debugging the customer by shift mismatched
Browse files- streamlit_app.py +123 -4
streamlit_app.py
CHANGED
|
@@ -91,6 +91,15 @@ LANG = {
|
|
| 91 |
"sm_col_delivery": "Delivery",
|
| 92 |
"sm_col_partypack": "Party Pack",
|
| 93 |
"sm_chart_rev_split": "Monthly Revenue by Channel",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
# Forecast tab
|
| 95 |
"fc_month_title": "This Month Forecast",
|
| 96 |
"fc_month_customers": "Forecast Customers (this month)",
|
|
@@ -217,6 +226,15 @@ LANG = {
|
|
| 217 |
"sm_col_delivery": "เดลิเวอรี่",
|
| 218 |
"sm_col_partypack": "พาร์ตี้แพ็ค",
|
| 219 |
"sm_chart_rev_split": "รายได้รายเดือนตามช่องทาง",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 220 |
# Forecast tab
|
| 221 |
"fc_month_title": "พยากรณ์ของเดือนนี้",
|
| 222 |
"fc_month_customers": "พยากรณ์จำนวนลูกค้า (เดือนนี้)",
|
|
@@ -1586,9 +1604,12 @@ with tab_summary:
|
|
| 1586 |
si_all = _summary_filter(fact_shift_items)
|
| 1587 |
if "Restaurant" in si_all.columns:
|
| 1588 |
si_all = si_all[si_all["Restaurant"] == "Copper Buffet"]
|
|
|
|
|
|
|
|
|
|
| 1589 |
si_cust = (
|
| 1590 |
-
si_all[si_all["
|
| 1591 |
-
if "
|
| 1592 |
)
|
| 1593 |
if not si_cust.empty:
|
| 1594 |
si_cust = si_cust.copy()
|
|
@@ -1878,6 +1899,102 @@ with tab_summary:
|
|
| 1878 |
else:
|
| 1879 |
st.caption(t("sm_no_monthly_rows"))
|
| 1880 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1881 |
# ── Daily detail (collapsed by default — can be long) ────────────
|
| 1882 |
# Mirror the Monthly summary column layout, just with Date instead
|
| 1883 |
# of Year / Month. Adds %Cap, %Premium and per-round customer
|
|
@@ -1967,9 +2084,11 @@ with tab_summary:
|
|
| 1967 |
si_all = _summary_filter(fact_shift_items)
|
| 1968 |
if "Restaurant" in si_all.columns:
|
| 1969 |
si_all = si_all[si_all["Restaurant"] == "Copper Buffet"]
|
|
|
|
|
|
|
| 1970 |
si_cust = (
|
| 1971 |
-
si_all[si_all["
|
| 1972 |
-
if "
|
| 1973 |
)
|
| 1974 |
|
| 1975 |
if not si_cust.empty:
|
|
|
|
| 91 |
"sm_col_delivery": "Delivery",
|
| 92 |
"sm_col_partypack": "Party Pack",
|
| 93 |
"sm_chart_rev_split": "Monthly Revenue by Channel",
|
| 94 |
+
"sm_recon_title": "Customer count reconciliation",
|
| 95 |
+
"sm_recon_help": "Rows where Summary Customers ≠ sum of round customers. "
|
| 96 |
+
"Summary numbers come from the daily POS summary report; round "
|
| 97 |
+
"numbers come from shift-level POS line items filtered to "
|
| 98 |
+
"Adult + Kid packages. Gaps usually mean delivery / party-pack / "
|
| 99 |
+
"comp customers counted in Summary but not assigned to a shift.",
|
| 100 |
+
"sm_recon_round_sum": "Sum of Rounds",
|
| 101 |
+
"sm_recon_diff": "Difference",
|
| 102 |
+
"sm_recon_no_gap": "No discrepancy in this filter window — all rows match.",
|
| 103 |
# Forecast tab
|
| 104 |
"fc_month_title": "This Month Forecast",
|
| 105 |
"fc_month_customers": "Forecast Customers (this month)",
|
|
|
|
| 226 |
"sm_col_delivery": "เดลิเวอรี่",
|
| 227 |
"sm_col_partypack": "พาร์ตี้แพ็ค",
|
| 228 |
"sm_chart_rev_split": "รายได้รายเดือนตามช่องทาง",
|
| 229 |
+
"sm_recon_title": "การกระทบยอดจำนวนลูกค้า",
|
| 230 |
+
"sm_recon_help": "แถวที่ค่าลูกค้าจาก Summary ไม่ตรงกับผลรวมของลูกค้าตามรอบ "
|
| 231 |
+
"ตัวเลข Summary มาจากรายงานสรุปประจำวันของ POS ส่วนตัวเลขรอบ "
|
| 232 |
+
"มาจากรายการขายรายไอเทมระดับ Shift กรองเฉพาะแพ็คเกจ Adult + Kid "
|
| 233 |
+
"ความต่างมักหมายถึงลูกค้าเดลิเวอรี่/พาร์ตี้แพ็ค/อภินันทนาการ "
|
| 234 |
+
"ที่ถูกนับใน Summary แต่ไม่ได้ผูกกับรอบใด",
|
| 235 |
+
"sm_recon_round_sum": "ผลรวมรอบ",
|
| 236 |
+
"sm_recon_diff": "ผลต่าง",
|
| 237 |
+
"sm_recon_no_gap": "ไม่พบความต่างในช่วงที่เลือก ทุกแถวตรงกัน",
|
| 238 |
# Forecast tab
|
| 239 |
"fc_month_title": "พยากรณ์ของเดือนนี้",
|
| 240 |
"fc_month_customers": "พยากรณ์จำนวนลูกค้า (เดือนนี้)",
|
|
|
|
| 1604 |
si_all = _summary_filter(fact_shift_items)
|
| 1605 |
if "Restaurant" in si_all.columns:
|
| 1606 |
si_all = si_all[si_all["Restaurant"] == "Copper Buffet"]
|
| 1607 |
+
# Only rows in the customer-paying tiers count toward the
|
| 1608 |
+
# round customer total: Normal + Premium + Party Pack
|
| 1609 |
+
# (Delivery and off-menu rows are excluded).
|
| 1610 |
si_cust = (
|
| 1611 |
+
si_all[si_all["SubType"].isin(["Normal", "Premium", "Party Pack"])]
|
| 1612 |
+
if "SubType" in si_all.columns else si_all
|
| 1613 |
)
|
| 1614 |
if not si_cust.empty:
|
| 1615 |
si_cust = si_cust.copy()
|
|
|
|
| 1899 |
else:
|
| 1900 |
st.caption(t("sm_no_monthly_rows"))
|
| 1901 |
|
| 1902 |
+
# ── Customer count reconciliation (Copper Buffet only) ───────
|
| 1903 |
+
# Surfaces rows where the Summary-based Customers column doesn't
|
| 1904 |
+
# equal the sum of round columns from fact_shift_items, so the
|
| 1905 |
+
# operations team can spot which months / branches drive the gap
|
| 1906 |
+
# and decide whether it's expected (delivery customers etc.) or
|
| 1907 |
+
# actually broken in the source data.
|
| 1908 |
+
if (restaurant_name == "Copper Buffet"
|
| 1909 |
+
and not m.empty
|
| 1910 |
+
and "Customers" in m.columns
|
| 1911 |
+
and round_cols):
|
| 1912 |
+
with st.expander(f"🔍 {t('sm_recon_title')}", expanded=True):
|
| 1913 |
+
st.caption(t("sm_recon_help"))
|
| 1914 |
+
_rec = m.copy()
|
| 1915 |
+
_rec["_RoundSum"] = _rec[round_cols].fillna(0).sum(axis=1)
|
| 1916 |
+
|
| 1917 |
+
# Compute a *raw* Qty total per (Year, Month, Branch)
|
| 1918 |
+
# directly from si_cust before pivoting. If RawQty
|
| 1919 |
+
# matches RoundSum but neither matches Customers, the
|
| 1920 |
+
# gap is between data sources (Summary vs shift POS) or
|
| 1921 |
+
# caused by a Year/Month/Branch merge mismatch. If
|
| 1922 |
+
# RawQty ≠ RoundSum, the pivot itself is dropping data.
|
| 1923 |
+
_raw = pd.DataFrame(columns=["Year", "Month", "Branch", "_RawQty"])
|
| 1924 |
+
try:
|
| 1925 |
+
_si = _summary_filter(fact_shift_items)
|
| 1926 |
+
if "Restaurant" in _si.columns:
|
| 1927 |
+
_si = _si[_si["Restaurant"] == "Copper Buffet"]
|
| 1928 |
+
if "SubType" in _si.columns:
|
| 1929 |
+
_si = _si[_si["SubType"].isin(["Normal", "Premium", "Party Pack"])]
|
| 1930 |
+
if not _si.empty and "Date" in _si.columns:
|
| 1931 |
+
_si = _si.copy()
|
| 1932 |
+
_si["Year"] = pd.to_datetime(_si["Date"], errors="coerce").dt.year
|
| 1933 |
+
_si["Month"] = pd.to_datetime(_si["Date"], errors="coerce").dt.month
|
| 1934 |
+
_raw = (
|
| 1935 |
+
_si.groupby(["Year", "Month", "Branch"], as_index=False)["Qty"]
|
| 1936 |
+
.sum().rename(columns={"Qty": "_RawQty"})
|
| 1937 |
+
)
|
| 1938 |
+
except Exception as e:
|
| 1939 |
+
st.caption(f"(raw-qty debug failed: {e})")
|
| 1940 |
+
|
| 1941 |
+
# Normalize the merge keys to int so a float/int
|
| 1942 |
+
# dtype mismatch can't silently drop rows.
|
| 1943 |
+
if not _raw.empty:
|
| 1944 |
+
_raw["Year"] = pd.to_numeric(_raw["Year"], errors="coerce").astype("Int64")
|
| 1945 |
+
_raw["Month"] = pd.to_numeric(_raw["Month"], errors="coerce").astype("Int64")
|
| 1946 |
+
_rec["Year"] = pd.to_numeric(_rec["Year"], errors="coerce").astype("Int64")
|
| 1947 |
+
_rec["Month"] = pd.to_numeric(_rec["Month"], errors="coerce").astype("Int64")
|
| 1948 |
+
_rec = _rec.merge(_raw, on=["Year", "Month", "Branch"], how="left")
|
| 1949 |
+
_rec["_RawQty"] = _rec["_RawQty"].fillna(0)
|
| 1950 |
+
_rec["_Diff"] = _rec["Customers"].fillna(0) - _rec["_RoundSum"]
|
| 1951 |
+
_rec["_Diff2"] = _rec["_RawQty"] - _rec["_RoundSum"]
|
| 1952 |
+
|
| 1953 |
+
_gap = _rec[
|
| 1954 |
+
(_rec["_Diff"].abs() > 0.5) | (_rec["_Diff2"].abs() > 0.5)
|
| 1955 |
+
]
|
| 1956 |
+
if _gap.empty:
|
| 1957 |
+
st.caption(t("sm_recon_no_gap"))
|
| 1958 |
+
else:
|
| 1959 |
+
_gap = _gap.sort_values(
|
| 1960 |
+
["Year", "Month", "Branch"], ascending=[True, True, True]
|
| 1961 |
+
)
|
| 1962 |
+
_gcols = ["Year", "Month", "Branch",
|
| 1963 |
+
"Customers", "_RoundSum", "_RawQty",
|
| 1964 |
+
"_Diff", "_Diff2"]
|
| 1965 |
+
_gdisp = _gap[_gcols].copy()
|
| 1966 |
+
_gdisp["Customers"] = _gdisp["Customers"].map(fmt_num)
|
| 1967 |
+
_gdisp["_RoundSum"] = _gdisp["_RoundSum"].map(fmt_num)
|
| 1968 |
+
_gdisp["_RawQty"] = _gdisp["_RawQty"].map(fmt_num)
|
| 1969 |
+
_gdisp["_Diff"] = _gdisp["_Diff"].map(
|
| 1970 |
+
lambda v: "—" if pd.isna(v) else f"{v:+,.0f}"
|
| 1971 |
+
)
|
| 1972 |
+
_gdisp["_Diff2"] = _gdisp["_Diff2"].map(
|
| 1973 |
+
lambda v: "—" if pd.isna(v) else f"{v:+,.0f}"
|
| 1974 |
+
)
|
| 1975 |
+
_gdisp = _gdisp.rename(columns={
|
| 1976 |
+
"_RoundSum": t("sm_recon_round_sum"),
|
| 1977 |
+
"_RawQty": "Raw Qty",
|
| 1978 |
+
"_Diff": t("sm_recon_diff"),
|
| 1979 |
+
"_Diff2": "Raw − Rounds",
|
| 1980 |
+
})
|
| 1981 |
+
st.dataframe(
|
| 1982 |
+
_gdisp,
|
| 1983 |
+
use_container_width=True, hide_index=True,
|
| 1984 |
+
column_config={
|
| 1985 |
+
"Year": st.column_config.NumberColumn(format="%d"),
|
| 1986 |
+
"Month": st.column_config.NumberColumn(format="%d"),
|
| 1987 |
+
},
|
| 1988 |
+
)
|
| 1989 |
+
st.caption(
|
| 1990 |
+
"Interpretation: if **Raw − Rounds** is non-zero, "
|
| 1991 |
+
"the pivot is dropping data (likely a Round/Shift "
|
| 1992 |
+
"value falling outside the expected set). If **"
|
| 1993 |
+
"Difference** is non-zero but Raw − Rounds is "
|
| 1994 |
+
"zero, the gap is between data sources or a "
|
| 1995 |
+
"Branch/Year-Month merge mismatch."
|
| 1996 |
+
)
|
| 1997 |
+
|
| 1998 |
# ── Daily detail (collapsed by default — can be long) ────────────
|
| 1999 |
# Mirror the Monthly summary column layout, just with Date instead
|
| 2000 |
# of Year / Month. Adds %Cap, %Premium and per-round customer
|
|
|
|
| 2084 |
si_all = _summary_filter(fact_shift_items)
|
| 2085 |
if "Restaurant" in si_all.columns:
|
| 2086 |
si_all = si_all[si_all["Restaurant"] == "Copper Buffet"]
|
| 2087 |
+
# Only Normal + Premium + Party Pack rows count as
|
| 2088 |
+
# customers (matches the monthly logic above).
|
| 2089 |
si_cust = (
|
| 2090 |
+
si_all[si_all["SubType"].isin(["Normal", "Premium", "Party Pack"])]
|
| 2091 |
+
if "SubType" in si_all.columns else si_all
|
| 2092 |
)
|
| 2093 |
|
| 2094 |
if not si_cust.empty:
|