"""日報業務邏輯 — 從 routes/daily_sales_routes.py 抽出的純計算函數。 這些函數不依賴 Flask request context,可獨立測試。 """ import calendar from datetime import timedelta import pandas as pd from utils.df_helpers import find_col def get_taiwan_holiday(date): """判斷是否為台灣國定假日""" year = date.year month = date.month day = date.day holidays_2026 = { (1, 1): '元旦', (2, 14): '春節連假', (2, 15): '小年夜', (2, 16): '除夕', (2, 17): '春節 (初一)', (2, 18): '春節 (初二)', (2, 19): '春節 (初三)', (2, 20): '春節連假', (2, 21): '春節連假', (2, 22): '春節連假', (2, 28): '和平紀念日', (3, 2): '和平紀念日補假', (4, 3): '兒童節補假', (4, 4): '清明節', (4, 5): '清明節連假', (4, 6): '清明節補假', (5, 1): '勞動節', (6, 19): '端午節', (9, 25): '中秋節', (9, 28): '教師節', (10, 9): '國慶日補假', (10, 10): '國慶日', (10, 25): '臺灣光復節', (10, 26): '光復節補假', (12, 25): '行憲紀念日', } holidays_2027 = { (1, 1): '元旦', (2, 11): '春節 (除夕)', (2, 12): '春節 (初一)', (2, 13): '春節 (初二)', (2, 14): '春節 (初三)', (2, 15): '春節 (初四)', (2, 16): '春節 (初五)', (2, 17): '春節 (初六)', (2, 28): '和平紀念日', (4, 4): '清明節', (4, 5): '清明節連假', (6, 14): '端午節', (9, 21): '中秋節', (10, 10): '國慶日', (10, 11): '國慶日連假', } holidays = holidays_2026 if year == 2026 else (holidays_2027 if year == 2027 else {}) holiday_name = holidays.get((month, day)) return (True, holiday_name) if holiday_name else (False, None) def prepare_calendar_data(df, selected_month): """準備行事曆數據""" year = selected_month.year month = selected_month.month first_day = pd.Timestamp(year=year, month=month, day=1) last_day = pd.Timestamp(year=year, month=month, day=calendar.monthrange(year, month)[1]) first_weekday = first_day.weekday() calendar_start = first_day - timedelta(days=first_weekday) last_weekday = last_day.weekday() calendar_end = last_day + timedelta(days=(6 - last_weekday)) data_start = first_day - timedelta(days=1) data_end = last_day month_df = df[(df['snapshot_date'] >= data_start) & (df['snapshot_date'] <= data_end)] cols = df.columns.tolist() col_amount = find_col(cols, ['銷售金額', '業績', '金額', '總業績']) col_cost = find_col(cols, ['成本', 'Cost']) col_profit = find_col(cols, ['毛利', 'Profit']) col_qty = find_col(cols, ['銷售數量', '銷量', 'Qty', '數量']) col_name = find_col(cols, ['商品名稱', '品名']) calendar_days = [] current_date = calendar_start while current_date <= calendar_end: weekday = current_date.weekday() weekday_names = ['週一', '週二', '週三', '週四', '週五', '週六', '週日'] is_holiday, holiday_name = get_taiwan_holiday(current_date) day_data = { 'date': current_date.strftime('%Y-%m-%d'), 'day': current_date.day, 'weekday': weekday_names[weekday], 'is_weekend': weekday >= 5, 'is_holiday': is_holiday, 'holiday_name': holiday_name, 'is_current_month': current_date.month == month, 'has_data': False, 'revenue': 0, 'profit': 0, 'margin_rate': 0, 'sku_count': 0, 'qty': 0, 'avg_price': 0, 'dod_percent': 0, 'dod_direction': 'neutral' } if first_day <= current_date <= last_day: day_df = month_df[month_df['snapshot_date'] == current_date] if not day_df.empty: day_data['has_data'] = True if col_amount: day_data['revenue'] = float(day_df[col_amount].sum()) if col_profit: day_data['profit'] = float(day_df[col_profit].sum()) elif col_cost and col_amount: total_cost = float(day_df[col_cost].sum()) day_data['profit'] = day_data['revenue'] - total_cost if day_data['revenue'] > 0: day_data['margin_rate'] = (day_data['profit'] / day_data['revenue']) * 100 if col_qty: day_data['qty'] = float(day_df[col_qty].sum()) if day_data['qty'] > 0: day_data['avg_price'] = day_data['revenue'] / day_data['qty'] if col_name: day_data['sku_count'] = int(day_df[col_name].nunique()) # DoD% prev_date = current_date - timedelta(days=1) prev_df = month_df[month_df['snapshot_date'] == prev_date] if not prev_df.empty and col_amount: prev_revenue = float(prev_df[col_amount].sum()) if prev_revenue > 0: dod = ((day_data['revenue'] - prev_revenue) / prev_revenue) * 100 day_data['dod_percent'] = round(dod, 1) day_data['dod_direction'] = 'up' if dod >= 0 else 'down' calendar_days.append(day_data) current_date += timedelta(days=1) weeks = [] for i in range(0, len(calendar_days), 7): weeks.append(calendar_days[i:i + 7]) prev_month = selected_month - pd.DateOffset(months=1) next_month = selected_month + pd.DateOffset(months=1) return { 'year': year, 'month': month, 'month_name': selected_month.strftime('%Y年%m月'), 'weeks': weeks, 'prev_month': prev_month.strftime('%Y-%m'), 'next_month': next_month.strftime('%Y-%m') } def prepare_marketing_summary(df, selected_date=None, is_month_view=False, month_start=None, month_end=None, sort_by='revenue'): """準備行銷活動業績貢獻數據""" if is_month_view and month_start is not None and month_end is not None: target_df = df[(df['snapshot_date'] >= month_start) & (df['snapshot_date'] <= month_end)] elif selected_date is not None: target_df = df[df['snapshot_date'] == selected_date] else: target_df = df if target_df.empty: return {'coupon': [], 'discount': [], 'bonus': [], 'click': []} cols = target_df.columns.tolist() col_amount = find_col(cols, ['銷售金額', '業績', '金額', '總業績']) col_qty = find_col(cols, ['銷售數量', '銷量', '數量', 'Qty']) col_profit = find_col(cols, ['毛利', 'Profit', '利潤']) col_cost = find_col(cols, ['成本', 'Cost', '總成本']) if not col_amount: return {'coupon': [], 'discount': [], 'bonus': [], 'click': []} marketing_cols = { 'coupon': '折價券活動名稱', 'discount': '折扣活動名稱', 'bonus': '滿額再折扣活動名稱', 'click': '點我再折扣' } result = {} actual_sort_key = sort_by if sort_by in ['revenue', 'qty', 'profit'] else 'revenue' for key, col_name in marketing_cols.items(): if col_name not in cols: result[key] = [] continue activity_df = target_df[ (target_df[col_name].notna()) & (target_df[col_name] != '') & (target_df[col_name] != '0') & (target_df[col_name] != 0) ] if activity_df.empty: result[key] = [] continue agg_args = { 'revenue': (col_amount, 'sum'), 'order_count': (col_amount, 'count') } if col_qty: agg_args['qty'] = (col_qty, 'sum') if col_profit: agg_args['profit'] = (col_profit, 'sum') grouped = activity_df.groupby(col_name).agg(**agg_args).reset_index() if 'profit' not in agg_args and col_cost: cost_agg = activity_df.groupby(col_name)[col_cost].sum().reset_index() grouped = grouped.merge(cost_agg, on=col_name) grouped['profit'] = grouped['revenue'] - grouped[col_cost] grouped = grouped.rename(columns={col_name: 'name'}) sort_col = actual_sort_key if actual_sort_key in grouped.columns else 'revenue' grouped = grouped.sort_values(sort_col, ascending=False).head(15) records = [] for _, row in grouped.iterrows(): record = { 'name': str(row['name'])[:50], 'revenue': float(row['revenue']), 'order_count': int(row['order_count']) } if 'qty' in row: record['qty'] = float(row['qty']) if 'profit' in row: record['profit'] = float(row['profit']) records.append(record) result[key] = records return result