taotanapol commited on
Commit
b77bbe1
·
verified ·
1 Parent(s): 6670292

Debugging the customer by shift mismatched

Browse files
Files changed (1) hide show
  1. 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["Group3"].isin(["Adult", "Kid"])]
1591
- if "Group3" in si_all.columns else si_all
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["Group3"].isin(["Adult", "Kid"])]
1972
- if "Group3" in si_all.columns else si_all
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: