377 lines
14 KiB
Python
377 lines
14 KiB
Python
import flet as ft
|
|
import sqlite3
|
|
import signal
|
|
import sys
|
|
import logging
|
|
import random
|
|
from datetime import datetime
|
|
from typing import List, Dict, Optional
|
|
|
|
class ErrorHandler:
|
|
"""グローバルエラーハンドラ"""
|
|
|
|
@staticmethod
|
|
def handle_error(error: Exception, context: str = ""):
|
|
"""エラーを一元処理"""
|
|
error_msg = f"{context}: {str(error)}"
|
|
logging.error(error_msg)
|
|
print(f"❌ {error_msg}")
|
|
|
|
try:
|
|
if hasattr(ErrorHandler, 'current_page'):
|
|
ErrorHandler.show_snackbar(ErrorHandler.current_page, error_msg, ft.Colors.RED)
|
|
except:
|
|
pass
|
|
|
|
@staticmethod
|
|
def show_snackbar(page, message: str, color: ft.Colors = ft.Colors.RED):
|
|
"""SnackBarを表示"""
|
|
try:
|
|
page.snack_bar = ft.SnackBar(
|
|
content=ft.Text(message),
|
|
bgcolor=color
|
|
)
|
|
page.snack_bar.open = True
|
|
page.update()
|
|
except:
|
|
pass
|
|
|
|
class DummyDataGenerator:
|
|
"""テスト用ダミーデータ生成"""
|
|
|
|
@staticmethod
|
|
def generate_customers(count: int = 100) -> List[Dict]:
|
|
"""ダミー顧客データ生成"""
|
|
first_names = ["田中", "佐藤", "鈴木", "高橋", "伊藤", "渡辺", "山本", "中村", "小林", "加藤"]
|
|
last_names = ["太郎", "次郎", "三郎", "花子", "美子", "健一", "恵子", "大輔", "由美", "翔太"]
|
|
|
|
customers = []
|
|
for i in range(count):
|
|
name = f"{random.choice(first_names)} {random.choice(last_names)}"
|
|
customers.append({
|
|
'id': i + 1,
|
|
'name': name,
|
|
'phone': f"090-{random.randint(1000, 9999)}-{random.randint(1000, 9999)}",
|
|
'email': f"customer{i+1}@example.com",
|
|
'address': f"東京都{random.choice(['渋谷区', '新宿区', '港区', '千代田区'])}{random.randint(1, 50)}-{random.randint(1, 10)}"
|
|
})
|
|
return customers
|
|
|
|
@staticmethod
|
|
def generate_products(count: int = 50) -> List[Dict]:
|
|
"""ダミー商品データ生成"""
|
|
categories = ["電子機器", "衣料品", "食品", "書籍", "家具"]
|
|
products = []
|
|
|
|
for i in range(count):
|
|
category = random.choice(categories)
|
|
products.append({
|
|
'id': i + 1,
|
|
'name': f"{category}{i+1}",
|
|
'category': category,
|
|
'price': random.randint(100, 50000),
|
|
'stock': random.randint(0, 100)
|
|
})
|
|
return products
|
|
|
|
@staticmethod
|
|
def generate_sales(count: int = 200) -> List[Dict]:
|
|
"""ダミー売上データ生成"""
|
|
customers = DummyDataGenerator.generate_customers(20)
|
|
products = DummyDataGenerator.generate_products(30)
|
|
|
|
sales = []
|
|
for i in range(count):
|
|
customer = random.choice(customers)
|
|
product = random.choice(products)
|
|
quantity = random.randint(1, 10)
|
|
|
|
sales.append({
|
|
'id': i + 1,
|
|
'customer_id': customer['id'],
|
|
'customer_name': customer['name'],
|
|
'product_id': product['id'],
|
|
'product_name': product['name'],
|
|
'quantity': quantity,
|
|
'unit_price': product['price'],
|
|
'total_price': quantity * product['price'],
|
|
'date': datetime.now().strftime("%Y-%m-%d"),
|
|
'created_at': datetime.now().isoformat()
|
|
})
|
|
return sales
|
|
|
|
def main(page: ft.Page):
|
|
"""メイン関数"""
|
|
try:
|
|
# ログ設定
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format='%(asctime)s - %(levelname)s - %(message)s',
|
|
handlers=[
|
|
logging.FileHandler('app.log'),
|
|
logging.StreamHandler()
|
|
]
|
|
)
|
|
|
|
# シグナルハンドラ設定
|
|
def signal_handler(signum, frame):
|
|
print(f"\nシグナル {signum} を受信しました")
|
|
print("✅ 正常終了処理完了")
|
|
logging.info("アプリケーション正常終了")
|
|
sys.exit(0)
|
|
|
|
signal.signal(signal.SIGINT, signal_handler)
|
|
signal.signal(signal.SIGTERM, signal_handler)
|
|
|
|
# データベース初期化
|
|
def init_db():
|
|
try:
|
|
conn = sqlite3.connect('sales.db')
|
|
cursor = conn.cursor()
|
|
|
|
cursor.execute('''
|
|
CREATE TABLE IF NOT EXISTS customers (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL,
|
|
phone TEXT,
|
|
email TEXT,
|
|
address TEXT,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
)
|
|
''')
|
|
|
|
cursor.execute('''
|
|
CREATE TABLE IF NOT EXISTS products (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL,
|
|
category TEXT,
|
|
price REAL NOT NULL,
|
|
stock INTEGER DEFAULT 0,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
)
|
|
''')
|
|
|
|
cursor.execute('''
|
|
CREATE TABLE IF NOT EXISTS sales (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
customer_name TEXT NOT NULL,
|
|
product_name TEXT NOT NULL,
|
|
quantity INTEGER NOT NULL,
|
|
unit_price REAL NOT NULL,
|
|
total_price REAL NOT NULL,
|
|
date TEXT NOT NULL,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
)
|
|
''')
|
|
|
|
conn.commit()
|
|
conn.close()
|
|
logging.info("データベース初期化完了")
|
|
|
|
except Exception as e:
|
|
logging.error(f"データベース初期化エラー: {e}")
|
|
print(f"❌ データベース初期化エラー: {e}")
|
|
|
|
# ダミーデータ生成
|
|
def generate_dummy_data():
|
|
try:
|
|
conn = sqlite3.connect('sales.db')
|
|
cursor = conn.cursor()
|
|
|
|
cursor.execute("SELECT COUNT(*) FROM customers")
|
|
if cursor.fetchone()[0] == 0:
|
|
print("📊 ダミーデータを生成中...")
|
|
|
|
# 顧客データ
|
|
customers = DummyDataGenerator.generate_customers(50)
|
|
for customer in customers:
|
|
cursor.execute('''
|
|
INSERT INTO customers (name, phone, email, address)
|
|
VALUES (?, ?, ?, ?)
|
|
''', (customer['name'], customer['phone'], customer['email'], customer['address']))
|
|
|
|
# 商品データ
|
|
products = DummyDataGenerator.generate_products(30)
|
|
for product in products:
|
|
cursor.execute('''
|
|
INSERT INTO products (name, category, price, stock)
|
|
VALUES (?, ?, ?, ?)
|
|
''', (product['name'], product['category'], product['price'], product['stock']))
|
|
|
|
# 売上データ
|
|
sales = DummyDataGenerator.generate_sales(100)
|
|
for sale in sales:
|
|
cursor.execute('''
|
|
INSERT INTO sales (customer_name, product_name, quantity, unit_price, total_price, date)
|
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
''', (sale['customer_name'], sale['product_name'], sale['quantity'],
|
|
sale['unit_price'], sale['total_price'], sale['date']))
|
|
|
|
conn.commit()
|
|
print("✅ ダミーデータ生成完了")
|
|
|
|
conn.close()
|
|
|
|
except Exception as e:
|
|
logging.error(f"ダミーデータ生成エラー: {e}")
|
|
print(f"❌ ダミーデータ生成エラー: {e}")
|
|
|
|
# ウィンドウ設定
|
|
page.title = "販売アシスト1号 (シンプル版)"
|
|
page.window_width = 800
|
|
page.window_height = 600
|
|
page.theme_mode = ft.ThemeMode.LIGHT
|
|
|
|
# ウィンドウクローズイベント
|
|
page.on_window_close = lambda _: signal_handler(0, None)
|
|
|
|
# データベース初期化とダミーデータ生成
|
|
init_db()
|
|
generate_dummy_data()
|
|
|
|
# 現在のページインデックス
|
|
current_page_index = [0]
|
|
|
|
# UI要素
|
|
title = ft.Text("販売アシスト1号", size=24, weight=ft.FontWeight.BOLD)
|
|
|
|
# フォーム要素
|
|
customer_tf = ft.TextField(label="顧客名", width=200, autofocus=True)
|
|
product_tf = ft.TextField(label="商品名", width=200)
|
|
amount_tf = ft.TextField(label="金額", width=150, keyboard_type=ft.KeyboardType.NUMBER)
|
|
|
|
# ボタン
|
|
add_btn = ft.Button("追加", bgcolor=ft.Colors.BLUE, color=ft.Colors.WHITE)
|
|
|
|
# 売上一覧
|
|
sales_list = ft.Column([], scroll=ft.ScrollMode.AUTO, height=300)
|
|
|
|
# 操作説明
|
|
instructions = ft.Text(
|
|
"操作方法: TABでフォーカス移動、SPACE/ENTERで追加、1-4でページ遷移",
|
|
size=12,
|
|
color=ft.Colors.GREY_600
|
|
)
|
|
|
|
def load_sales():
|
|
"""売上データ読み込み"""
|
|
try:
|
|
conn = sqlite3.connect('sales.db')
|
|
cursor = conn.cursor()
|
|
|
|
cursor.execute('''
|
|
SELECT customer_name, product_name, total_price, date
|
|
FROM sales
|
|
ORDER BY created_at DESC
|
|
LIMIT 20
|
|
''')
|
|
sales = cursor.fetchall()
|
|
conn.close()
|
|
|
|
# リストを更新
|
|
sales_list.controls.clear()
|
|
for sale in sales:
|
|
customer, product, amount, date = sale
|
|
sales_list.controls.append(
|
|
ft.Text(f"{date}: {customer} - {product}: ¥{amount:,.0f}")
|
|
)
|
|
|
|
page.update()
|
|
|
|
except Exception as e:
|
|
logging.error(f"売上データ読み込みエラー: {e}")
|
|
|
|
def add_sale(e):
|
|
"""売上データ追加"""
|
|
if customer_tf.value and product_tf.value and amount_tf.value:
|
|
try:
|
|
# 保存前に値を取得
|
|
customer_val = customer_tf.value
|
|
product_val = product_tf.value
|
|
amount_val = float(amount_tf.value)
|
|
|
|
# データベースに保存
|
|
conn = sqlite3.connect('sales.db')
|
|
cursor = conn.cursor()
|
|
|
|
cursor.execute('''
|
|
INSERT INTO sales (customer_name, product_name, quantity, unit_price, total_price, date)
|
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
''', (customer_val, product_val, 1, amount_val, amount_val, datetime.now().strftime("%Y-%m-%d")))
|
|
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
# フィールドをクリア
|
|
customer_tf.value = ""
|
|
product_tf.value = ""
|
|
amount_tf.value = ""
|
|
|
|
# リスト更新
|
|
load_sales()
|
|
|
|
# 成功メッセージ
|
|
page.snack_bar = ft.SnackBar(
|
|
content=ft.Text("保存しました"),
|
|
bgcolor=ft.Colors.GREEN
|
|
)
|
|
page.snack_bar.open = True
|
|
page.update()
|
|
|
|
logging.info(f"売上データ追加: {customer_val} {product_val} {amount_val}")
|
|
|
|
except Exception as ex:
|
|
logging.error(f"売上データ保存エラー: {ex}")
|
|
page.snack_bar = ft.SnackBar(
|
|
content=ft.Text("エラーが発生しました"),
|
|
bgcolor=ft.Colors.RED
|
|
)
|
|
page.snack_bar.open = True
|
|
page.update()
|
|
|
|
# キーボードイベントハンドラ
|
|
def on_keyboard(e: ft.KeyboardEvent):
|
|
# SPACEまたはENTERで追加
|
|
if e.key == " " or e.key == "Enter":
|
|
add_sale(None)
|
|
# 数字キーでページ遷移(今回はシンプルに)
|
|
elif e.key == "1":
|
|
print("ダッシュボードに遷移します")
|
|
elif e.key == "2":
|
|
print("売上管理に遷移します")
|
|
elif e.key == "3":
|
|
print("顧客管理に遷移します")
|
|
elif e.key == "4":
|
|
print("商品管理に遷移します")
|
|
# ESCで終了
|
|
elif e.key == "Escape":
|
|
signal_handler(0, None)
|
|
|
|
# キーボードイベント設定
|
|
page.on_keyboard_event = on_keyboard
|
|
|
|
# 初期データ読み込み
|
|
load_sales()
|
|
|
|
# ページ構築
|
|
page.add(
|
|
title,
|
|
ft.Divider(),
|
|
ft.Text("売上登録", size=18, weight=ft.FontWeight.BOLD),
|
|
ft.Row([customer_tf, product_tf, amount_tf, add_btn]),
|
|
ft.Divider(),
|
|
ft.Text("売上一覧", size=18),
|
|
instructions,
|
|
ft.Divider(),
|
|
sales_list
|
|
)
|
|
|
|
logging.info("アプリケーション起動完了")
|
|
print("🚀 シンプル版販売アシスト1号起動完了")
|
|
|
|
except Exception as e:
|
|
logging.error(f"アプリケーション起動エラー: {e}")
|
|
print(f"❌ アプリケーション起動エラー: {e}")
|
|
|
|
if __name__ == "__main__":
|
|
ft.run(main)
|