taotanapol commited on
Commit
fc15477
Β·
verified Β·
1 Parent(s): 2546499

Update Sales tab to include delivery and party pack

Browse files
Files changed (1) hide show
  1. streamlit_app.py +170 -3
streamlit_app.py CHANGED
@@ -86,6 +86,11 @@ LANG = {
86
  "sm_chart_cap": "%Cap β€” Capacity utilised",
87
  "sm_chart_premium": "%Premium β€” premium share of customers",
88
  "sm_chart_rounds": "Customers by Round (monthly)",
 
 
 
 
 
89
  # Forecast tab
90
  "fc_month_title": "This Month Forecast",
91
  "fc_month_customers": "Forecast Customers (this month)",
@@ -207,6 +212,11 @@ LANG = {
207
  "sm_chart_cap": "%ΰΉƒΰΈŠΰΉ‰ΰΈžΰΈ·ΰΉ‰ΰΈ™ΰΈ—ΰΈ΅ΰΉˆ",
208
  "sm_chart_premium": "%ΰΈ₯ΰΈΉΰΈΰΈ„ΰΉ‰ΰΈ²ΰΈžΰΈ£ΰΈ΅ΰΉ€ΰΈ‘ΰΈ΅ΰΈ’ΰΈ‘",
209
  "sm_chart_rounds": "ΰΈ₯ΰΈΉΰΈΰΈ„ΰΉ‰ΰΈ²ΰΈ•ΰΈ²ΰΈ‘ΰΈ£ΰΈ­ΰΈš (ΰΈ£ΰΈ²ΰΈ’ΰΉ€ΰΈ”ΰΈ·ΰΈ­ΰΈ™)",
 
 
 
 
 
210
  # Forecast tab
211
  "fc_month_title": "ΰΈžΰΈ’ΰΈ²ΰΈΰΈ£ΰΈ“ΰΉŒΰΈ‚ΰΈ­ΰΈ‡ΰΉ€ΰΈ”ΰΈ·ΰΈ­ΰΈ™ΰΈ™ΰΈ΅ΰΉ‰",
212
  "fc_month_customers": "ΰΈžΰΈ’ΰΈ²ΰΈΰΈ£ΰΈ“ΰΉŒΰΈˆΰΈ³ΰΈ™ΰΈ§ΰΈ™ΰΈ₯ูกค้า (ΰΉ€ΰΈ”ΰΈ·ΰΈ­ΰΈ™ΰΈ™ΰΈ΅ΰΉ‰)",
@@ -1479,6 +1489,97 @@ with tab_summary:
1479
  m = monthly.copy().sort_values(
1480
  ["Year", "Month", "Branch"], ascending=[True, True, True]
1481
  ) if not monthly.empty else monthly.copy()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1482
  round_cols: list[str] = []
1483
  if (restaurant_name == "Copper Buffet"
1484
  and not fact_shift_items.empty and not m.empty):
@@ -1676,6 +1777,64 @@ with tab_summary:
1676
  xaxis_title=None, yaxis_title=None)
1677
  st.plotly_chart(style_plotly(fig, height=340), use_container_width=True)
1678
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1679
  # ── Monthly summary table ────────────────────────────────────────
1680
  st.markdown(f"**{t('sm_monthly_summary')}**")
1681
  if not m.empty:
@@ -1683,9 +1842,9 @@ with tab_summary:
1683
  ["Year", "Month", "Branch", "Revenue", "Customers"]
1684
  if c in m.columns]
1685
  tail_cols = [c for c in ["Rev_Per_Head"] if c in m.columns]
1686
- # Column order: base Β· %Cap Β· %Premium Β· Rev/Head Β· rounds.
1687
  metric_cols = [c for c in ["%Cap", "%Premium"] if c in m.columns]
1688
- cols = base_cols + metric_cols + tail_cols + round_cols
1689
 
1690
  # Pre-format money / count / percent columns to strings (printf
1691
  # "," flag is not supported on older Streamlit versions).
@@ -1695,11 +1854,19 @@ with tab_summary:
1695
  disp[c] = disp[c].map(fmt_money)
1696
  if "Customers" in disp.columns:
1697
  disp["Customers"] = disp["Customers"].map(fmt_num)
 
 
1698
  for c in round_cols:
1699
  disp[c] = disp[c].map(fmt_num)
1700
  for c in metric_cols:
1701
  disp[c] = disp[c].map(fmt_pct)
1702
- disp = disp.rename(columns={"Rev_Per_Head": "Rev / Head"})
 
 
 
 
 
 
1703
  st.dataframe(
1704
  disp,
1705
  use_container_width=True, hide_index=True,
 
86
  "sm_chart_cap": "%Cap β€” Capacity utilised",
87
  "sm_chart_premium": "%Premium β€” premium share of customers",
88
  "sm_chart_rounds": "Customers by Round (monthly)",
89
+ "sm_col_normal": "Normal",
90
+ "sm_col_premium": "Premium",
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)",
 
212
  "sm_chart_cap": "%ΰΉƒΰΈŠΰΉ‰ΰΈžΰΈ·ΰΉ‰ΰΈ™ΰΈ—ΰΈ΅ΰΉˆ",
213
  "sm_chart_premium": "%ΰΈ₯ΰΈΉΰΈΰΈ„ΰΉ‰ΰΈ²ΰΈžΰΈ£ΰΈ΅ΰΉ€ΰΈ‘ΰΈ΅ΰΈ’ΰΈ‘",
214
  "sm_chart_rounds": "ΰΈ₯ΰΈΉΰΈΰΈ„ΰΉ‰ΰΈ²ΰΈ•ΰΈ²ΰΈ‘ΰΈ£ΰΈ­ΰΈš (ΰΈ£ΰΈ²ΰΈ’ΰΉ€ΰΈ”ΰΈ·ΰΈ­ΰΈ™)",
215
+ "sm_col_normal": "ปกติ",
216
+ "sm_col_premium": "ΰΈžΰΈ£ΰΈ΅ΰΉ€ΰΈ‘ΰΈ΅ΰΈ’ΰΈ‘",
217
+ "sm_col_delivery": "ΰΉ€ΰΈ”ΰΈ₯ΰΈ΄ΰΉ€ΰΈ§ΰΈ­ΰΈ£ΰΈ΅ΰΉˆ",
218
+ "sm_col_partypack": "ΰΈžΰΈ²ΰΈ£ΰΉŒΰΈ•ΰΈ΅ΰΉ‰ΰΉΰΈžΰΉ‡ΰΈ„",
219
+ "sm_chart_rev_split": "ΰΈ£ΰΈ²ΰΈ’ΰΉ„ΰΈ”ΰΉ‰ΰΈ£ΰΈ²ΰΈ’ΰΉ€ΰΈ”ΰΈ·ΰΈ­ΰΈ™ΰΈ•ΰΈ²ΰΈ‘ΰΈŠΰΉˆΰΈ­ΰΈ‡ΰΈ—ΰΈ²ΰΈ‡",
220
  # Forecast tab
221
  "fc_month_title": "ΰΈžΰΈ’ΰΈ²ΰΈΰΈ£ΰΈ“ΰΉŒΰΈ‚ΰΈ­ΰΈ‡ΰΉ€ΰΈ”ΰΈ·ΰΈ­ΰΈ™ΰΈ™ΰΈ΅ΰΉ‰",
222
  "fc_month_customers": "ΰΈžΰΈ’ΰΈ²ΰΈΰΈ£ΰΈ“ΰΉŒΰΈˆΰΈ³ΰΈ™ΰΈ§ΰΈ™ΰΈ₯ูกค้า (ΰΉ€ΰΈ”ΰΈ·ΰΈ­ΰΈ™ΰΈ™ΰΈ΅ΰΉ‰)",
 
1489
  m = monthly.copy().sort_values(
1490
  ["Year", "Month", "Branch"], ascending=[True, True, True]
1491
  ) if not monthly.empty else monthly.copy()
1492
+
1493
+ # ── Split Revenue into Normal / Premium / Delivery / Party Pack ──
1494
+ # Per-row revenue = GrossRev + SVC (net of discount, including
1495
+ # service charge but excluding tax β€” matches how the ops team
1496
+ # accounts for revenue).
1497
+ #
1498
+ # Tagging differs between the two restaurants:
1499
+ # β€’ Copper Buffet uses Type='Package' + SubType ∈
1500
+ # ('Normal', 'Premium', 'Delivery', 'Party Pack').
1501
+ # β€’ Tiew Copper is Γ  la carte, so its delivery is tagged with
1502
+ # Type='Delivery' (no SubType split). It has no Premium /
1503
+ # Party Pack channels β€” those columns stay at 0.
1504
+ channel_cols: list[str] = []
1505
+ if not m.empty and not fact_items.empty:
1506
+ fi = fact_items.copy()
1507
+ if "Date" in fi.columns:
1508
+ fi["Date"] = pd.to_datetime(fi["Date"], errors="coerce")
1509
+ fi = fi.dropna(subset=["Date"])
1510
+ if "Restaurant" in fi.columns:
1511
+ fi = fi[fi["Restaurant"] == restaurant_name]
1512
+ if sel_branches and "Branch" in fi.columns:
1513
+ fi = fi[fi["Branch"].isin(sel_branches)]
1514
+ # Apply the same date-range filter the rest of the Summary tab uses.
1515
+ if date_from is not None and "Date" in fi.columns:
1516
+ fi = fi[fi["Date"] >= pd.to_datetime(date_from)]
1517
+ if date_to is not None and "Date" in fi.columns:
1518
+ fi = fi[fi["Date"] <= pd.to_datetime(date_to)]
1519
+ if not fi.empty and "Year" not in fi.columns:
1520
+ fi["Year"] = fi["Date"].dt.year
1521
+ fi["Month"] = fi["Date"].dt.month
1522
+
1523
+ # Revenue per row = GrossRev + SVC. Both coerced to numeric
1524
+ # (NaN→0) so the sum is safe when columns are missing.
1525
+ if not fi.empty:
1526
+ _gross = pd.to_numeric(fi.get("GrossRev", 0), errors="coerce").fillna(0)
1527
+ _svc = pd.to_numeric(fi.get("SVC", 0), errors="coerce").fillna(0)
1528
+ fi["_rev"] = _gross + _svc
1529
+
1530
+ def _bucket_into(source: pd.DataFrame, dest_col: str) -> None:
1531
+ """Sum `source['_rev']` per (Year, Month, Branch) into m[dest_col]."""
1532
+ nonlocal m
1533
+ if source.empty:
1534
+ m[dest_col] = 0.0
1535
+ return
1536
+ agg = (
1537
+ source.groupby(["Year", "Month", "Branch"], as_index=False)["_rev"]
1538
+ .sum()
1539
+ .rename(columns={"_rev": dest_col})
1540
+ )
1541
+ m = m.merge(agg, on=["Year", "Month", "Branch"], how="left")
1542
+ m[dest_col] = m[dest_col].fillna(0.0)
1543
+
1544
+ if restaurant_name == "Copper Buffet":
1545
+ # Filter to Package rows, then bucket by SubType.
1546
+ fi_pkg = (fi[fi["Type"] == "Package"]
1547
+ if "Type" in fi.columns else fi.iloc[0:0])
1548
+ def _by_subtype(sub_value: str) -> pd.DataFrame:
1549
+ if "SubType" not in fi_pkg.columns:
1550
+ return fi_pkg.iloc[0:0]
1551
+ return fi_pkg[fi_pkg["SubType"] == sub_value]
1552
+ _bucket_into(_by_subtype("Normal"), "Normal")
1553
+ _bucket_into(_by_subtype("Premium"), "Premium")
1554
+ _bucket_into(_by_subtype("Delivery"), "Delivery")
1555
+ _bucket_into(_by_subtype("Party Pack"), "PartyPack")
1556
+
1557
+ elif restaurant_name == "Tiew Copper":
1558
+ # Tiew Copper tags delivery via Type='Delivery'; the
1559
+ # rest of the revenue is "Normal" (Γ  la carte food +
1560
+ # beverage). Derive Normal as Revenue βˆ’ Delivery so the
1561
+ # column lines up with the kpi_monthly Revenue total
1562
+ # that drives the other tiles.
1563
+ delivery_rows = (fi[fi["Type"] == "Delivery"]
1564
+ if "Type" in fi.columns else fi.iloc[0:0])
1565
+ _bucket_into(delivery_rows, "Delivery")
1566
+ if "Revenue" in m.columns:
1567
+ m["Normal"] = (m["Revenue"] - m.get("Delivery", 0.0)).clip(lower=0)
1568
+ else:
1569
+ m["Normal"] = 0.0
1570
+ # Tiew Copper has no Premium / Party Pack channels.
1571
+ m["Premium"] = 0.0
1572
+ m["PartyPack"] = 0.0
1573
+
1574
+ else:
1575
+ # Group-level rows (Holding / CK / Conso) β€” no channel
1576
+ # split applies. Leave the columns at 0 for consistency.
1577
+ for c in ("Normal", "Premium", "Delivery", "PartyPack"):
1578
+ m[c] = 0.0
1579
+
1580
+ channel_cols = [c for c in ("Normal", "Premium", "Delivery", "PartyPack")
1581
+ if c in m.columns]
1582
+
1583
  round_cols: list[str] = []
1584
  if (restaurant_name == "Copper Buffet"
1585
  and not fact_shift_items.empty and not m.empty):
 
1777
  xaxis_title=None, yaxis_title=None)
1778
  st.plotly_chart(style_plotly(fig, height=340), use_container_width=True)
1779
 
1780
+ # Stacked-bar revenue split β€” Normal / Premium / Delivery /
1781
+ # Party Pack per month, summed across the branches in scope.
1782
+ # Skipped only when every channel column is flat-zero in the
1783
+ # filter window.
1784
+ if channel_cols and any(
1785
+ (col in m.columns) and m[col].sum() > 0
1786
+ for col in channel_cols
1787
+ ):
1788
+ _RC_COLOR = {
1789
+ t("sm_col_normal"): "#976A4D", # COPPER (primary baseline)
1790
+ t("sm_col_premium"): "#1E2B3A", # NAVY (premium = anchor)
1791
+ t("sm_col_delivery"): "#DC7D3D", # TIEW (delivery accent)
1792
+ t("sm_col_partypack"): "#D4A574", # GOLD (party pack accent)
1793
+ }
1794
+ rev_long = mf[["YearMonth"] + channel_cols].copy()
1795
+ # Rename the internal column names to their localized
1796
+ # labels before melting so the chart legend reads in the
1797
+ # user's language.
1798
+ rev_long = rev_long.rename(columns={
1799
+ "Normal": t("sm_col_normal"),
1800
+ "Premium": t("sm_col_premium"),
1801
+ "Delivery": t("sm_col_delivery"),
1802
+ "PartyPack": t("sm_col_partypack"),
1803
+ })
1804
+ value_vars = [
1805
+ t("sm_col_normal"),
1806
+ t("sm_col_premium"),
1807
+ t("sm_col_delivery"),
1808
+ t("sm_col_partypack"),
1809
+ ]
1810
+ value_vars = [c for c in value_vars if c in rev_long.columns]
1811
+ long_df = rev_long.melt(
1812
+ id_vars=["YearMonth"], value_vars=value_vars,
1813
+ var_name="Channel", value_name="Revenue",
1814
+ )
1815
+ long_df = (
1816
+ long_df.groupby(["YearMonth", "Channel"], as_index=False)["Revenue"]
1817
+ .sum()
1818
+ .sort_values(["YearMonth"])
1819
+ )
1820
+ fig = px.bar(
1821
+ long_df, x="YearMonth", y="Revenue", color="Channel",
1822
+ barmode="stack",
1823
+ color_discrete_map=_RC_COLOR,
1824
+ category_orders={"Channel": value_vars},
1825
+ text="Revenue",
1826
+ )
1827
+ fig.update_traces(
1828
+ texttemplate="ΰΈΏ%{y:,.0f}", textposition="inside",
1829
+ textfont=dict(size=9, color="#FAF7F2"),
1830
+ insidetextanchor="middle",
1831
+ )
1832
+ fig.update_yaxes(tickformat=",.0f")
1833
+ fig.update_layout(title=t("sm_chart_rev_split"),
1834
+ xaxis_title=None, yaxis_title=None,
1835
+ legend_title=None)
1836
+ st.plotly_chart(style_plotly(fig, height=340), use_container_width=True)
1837
+
1838
  # ── Monthly summary table ────────────────────────────────────────
1839
  st.markdown(f"**{t('sm_monthly_summary')}**")
1840
  if not m.empty:
 
1842
  ["Year", "Month", "Branch", "Revenue", "Customers"]
1843
  if c in m.columns]
1844
  tail_cols = [c for c in ["Rev_Per_Head"] if c in m.columns]
1845
+ # Column order: base Β· channels Β· %Cap Β· %Premium Β· Rev/Head Β· rounds.
1846
  metric_cols = [c for c in ["%Cap", "%Premium"] if c in m.columns]
1847
+ cols = base_cols + channel_cols + metric_cols + tail_cols + round_cols
1848
 
1849
  # Pre-format money / count / percent columns to strings (printf
1850
  # "," flag is not supported on older Streamlit versions).
 
1854
  disp[c] = disp[c].map(fmt_money)
1855
  if "Customers" in disp.columns:
1856
  disp["Customers"] = disp["Customers"].map(fmt_num)
1857
+ for c in channel_cols:
1858
+ disp[c] = disp[c].map(fmt_money)
1859
  for c in round_cols:
1860
  disp[c] = disp[c].map(fmt_num)
1861
  for c in metric_cols:
1862
  disp[c] = disp[c].map(fmt_pct)
1863
+ disp = disp.rename(columns={
1864
+ "Rev_Per_Head": "Rev / Head",
1865
+ "Normal": t("sm_col_normal"),
1866
+ "Premium": t("sm_col_premium"),
1867
+ "Delivery": t("sm_col_delivery"),
1868
+ "PartyPack": t("sm_col_partypack"),
1869
+ })
1870
  st.dataframe(
1871
  disp,
1872
  use_container_width=True, hide_index=True,