143 lines
6.6 KiB
Python
143 lines
6.6 KiB
Python
import os
|
|
import csv
|
|
import pandas as pd
|
|
from datetime import datetime
|
|
from config import EXCEL_EXPORT_DIR
|
|
from utils.momo_url_utils import build_momo_product_url, normalize_momo_product_url
|
|
|
|
class Exporter:
|
|
"""
|
|
報表匯出管理器
|
|
負責將數據轉換為 CSV/Excel 格式並儲存至 exports 目錄
|
|
"""
|
|
def __init__(self):
|
|
self.export_dir = EXCEL_EXPORT_DIR
|
|
if not os.path.exists(self.export_dir):
|
|
try:
|
|
os.makedirs(self.export_dir)
|
|
except OSError:
|
|
pass
|
|
|
|
def _write_csv(self, filename, headers, rows):
|
|
"""內部方法:寫入 CSV 檔案 (含 BOM 以支援 Excel 中文顯示)"""
|
|
filepath = os.path.join(self.export_dir, filename)
|
|
try:
|
|
with open(filepath, 'w', newline='', encoding='utf-8-sig') as f:
|
|
writer = csv.writer(f)
|
|
writer.writerow(headers)
|
|
writer.writerows(rows)
|
|
return filepath
|
|
except Exception as e:
|
|
print(f"❌ Export error: {e}")
|
|
return None
|
|
|
|
def _safe_product_url(self, product):
|
|
sku = str(getattr(product, 'i_code', '') or '')
|
|
return normalize_momo_product_url(getattr(product, 'url', None), sku) or build_momo_product_url(sku)
|
|
|
|
def generate_all_categories_report(self):
|
|
"""匯出所有分類商品快照"""
|
|
# 由於 app.py 呼叫此方法時未傳入數據,需自行查詢資料庫
|
|
from database.manager import DatabaseManager
|
|
from database.models import PriceRecord
|
|
from sqlalchemy import func
|
|
|
|
db = DatabaseManager()
|
|
session = db.get_session()
|
|
try:
|
|
# 查詢每個商品最新價格
|
|
subq = session.query(func.max(PriceRecord.id).label('max_id')).group_by(PriceRecord.product_id).subquery()
|
|
records = session.query(PriceRecord).join(subq, PriceRecord.id == subq.c.max_id).all()
|
|
|
|
rows = []
|
|
for r in records:
|
|
rows.append([r.product.category, r.product.name, r.price, self._safe_product_url(r.product), r.timestamp.strftime('%Y-%m-%d %H:%M')])
|
|
|
|
filename = f"All_Products_{datetime.now().strftime('%Y%m%d_%H%M')}.csv"
|
|
return self._write_csv(filename, ['Category', 'Name', 'Price', 'URL', 'Last Update'], rows)
|
|
finally:
|
|
session.close()
|
|
|
|
def generate_price_change_report(self, items):
|
|
"""匯出價格異動商品"""
|
|
rows = []
|
|
for item in items:
|
|
rec = item['record']
|
|
diff = item['yesterday_diff']
|
|
rows.append([rec.product.category, rec.product.name, rec.price, diff, self._safe_product_url(rec.product)])
|
|
|
|
filename = f"Price_Changes_{datetime.now().strftime('%Y%m%d_%H%M')}.csv"
|
|
return self._write_csv(filename, ['Category', 'Name', 'Price', 'Change', 'URL'], rows)
|
|
|
|
def generate_low_price_report(self):
|
|
return self._write_csv(f"Low_Price_{datetime.now().strftime('%Y%m%d')}.csv", ['Message'], [['Feature not fully implemented']])
|
|
|
|
def generate_custom_report(self, items, title):
|
|
rows = []
|
|
for item in items:
|
|
rec = item['record']
|
|
rows.append([rec.product.category, rec.product.name, rec.price, self._safe_product_url(rec.product)])
|
|
filename = f"{title}_{datetime.now().strftime('%Y%m%d_%H%M')}.csv"
|
|
return self._write_csv(filename, ['Category', 'Name', 'Price', 'URL'], rows)
|
|
|
|
def generate_delisted_report(self, items, title):
|
|
rows = []
|
|
for item in items:
|
|
p = item['product']
|
|
price = item['last_price']
|
|
rows.append([p.category, p.name, price, self._safe_product_url(p), "DELISTED"])
|
|
filename = f"{title}_{datetime.now().strftime('%Y%m%d_%H%M')}.csv"
|
|
return self._write_csv(filename, ['Category', 'Name', 'Last Price', 'URL', 'Status'], rows)
|
|
|
|
def generate_all_products_excel(self, items):
|
|
"""匯出所有商品至 Excel (依分類分頁)"""
|
|
data = []
|
|
for item in items:
|
|
rec = item['record']
|
|
data.append({
|
|
'分類': rec.product.category,
|
|
'商品名稱': rec.product.name,
|
|
'價格': rec.price,
|
|
'連結': self._safe_product_url(rec.product),
|
|
'更新時間': rec.timestamp.strftime('%Y-%m-%d %H:%M')
|
|
})
|
|
|
|
filename = f"MOMO_All_{datetime.now().strftime('%Y%m%d_%H%M')}.xlsx"
|
|
filepath = os.path.join(self.export_dir, filename)
|
|
|
|
df = pd.DataFrame(data)
|
|
with pd.ExcelWriter(filepath, engine='openpyxl') as writer:
|
|
for cat, group in df.groupby('分類'):
|
|
sheet_name = str(cat)[:30].replace('/', '_').replace(':', '') # 處理 Excel 工作表名稱限制
|
|
group.to_excel(writer, sheet_name=sheet_name, index=False)
|
|
|
|
return filepath
|
|
|
|
def generate_changes_excel(self, increase, decrease):
|
|
"""匯出漲跌商品至 Excel (兩個分頁)"""
|
|
filename = f"MOMO_Changes_{datetime.now().strftime('%Y%m%d_%H%M')}.xlsx"
|
|
filepath = os.path.join(self.export_dir, filename)
|
|
|
|
with pd.ExcelWriter(filepath, engine='openpyxl') as writer:
|
|
# 漲價
|
|
data_inc = [{'分類': i['record'].product.category, '商品名稱': i['record'].product.name, '價格': i['record'].price, '漲幅': i['yesterday_diff'], '連結': self._safe_product_url(i['record'].product)} for i in increase]
|
|
pd.DataFrame(data_inc).to_excel(writer, sheet_name='漲價商品', index=False)
|
|
|
|
# 跌價
|
|
data_dec = [{'分類': i['record'].product.category, '商品名稱': i['record'].product.name, '價格': i['record'].price, '跌幅': i['yesterday_diff'], '連結': self._safe_product_url(i['record'].product)} for i in decrease]
|
|
pd.DataFrame(data_dec).to_excel(writer, sheet_name='跌價商品', index=False)
|
|
|
|
return filepath
|
|
|
|
def generate_delisted_excel(self, delisted_items):
|
|
"""匯出下架商品至 Excel"""
|
|
filename = f"MOMO_Delisted_{datetime.now().strftime('%Y%m%d_%H%M')}.xlsx"
|
|
filepath = os.path.join(self.export_dir, filename)
|
|
|
|
data = [{'分類': i['product'].category, '商品名稱': i['product'].name, '最後價格': i['last_price'], '連結': self._safe_product_url(i['product']), '狀態': '下架'} for i in delisted_items]
|
|
|
|
with pd.ExcelWriter(filepath, engine='openpyxl') as writer:
|
|
pd.DataFrame(data).to_excel(writer, sheet_name='下架商品', index=False)
|
|
|
|
return filepath
|