h-1.flet.3/components/slip_entry_framework.py

482 lines
18 KiB
Python

"""
伝票入力フレームワーク
表示形式を動的に切り替える伝票入力システム
"""
import flet as ft
import sqlite3
import logging
import json
from datetime import datetime
from typing import List, Dict, Optional, Any
class DisplayTheme:
"""表示形式テーマ"""
def __init__(self, name: str, title: str, format_type: str, config: Dict):
self.name = name
self.title = title
self.format_type = format_type # 'table', 'card', 'list', 'custom'
self.config = config
class SlipEntryRenderer:
"""伝票入力レンダラ"""
def __init__(self, page: ft.Page):
self.page = page
self.current_theme = None
self.items = []
def set_theme(self, theme: DisplayTheme):
"""テーマを設定"""
self.current_theme = theme
def render_table_format(self, items: List[Dict]) -> ft.Column:
"""表形式でレンダリング"""
if not self.current_theme or self.current_theme.format_type != 'table':
return ft.Column([ft.Text("テーマが設定されていません")])
config = self.current_theme.config
columns = config.get('columns', [])
# ヘッダー行
header_row = ft.Row(
[ft.Text(col.get('label', ''), weight=ft.FontWeight.BOLD, width=col.get('width', 100))
for col in columns],
spacing=5
)
# データ行
data_rows = []
for item in items:
row_controls = []
for col in columns:
field_name = col.get('field', '')
value = item.get(field_name, '')
align = col.get('align', 'left')
text_control = ft.Text(
value=str(value),
width=col.get('width', 100),
text_align=ft.TextAlign.LEFT if align == 'left' else ft.TextAlign.RIGHT
)
row_controls.append(text_control)
data_rows.append(ft.Row(row_controls, spacing=5))
return ft.Column([header_row] + data_rows, spacing=5)
def render_card_format(self, items: List[Dict]) -> ft.Column:
"""カード形式でレンダリング"""
if not self.current_theme or self.current_theme.format_type != 'card':
return ft.Column([ft.Text("テーマが設定されていません")])
config = self.current_theme.config
card_width = config.get('card_width', 300)
card_height = config.get('card_height', 120)
cards = []
for i, item in enumerate(items):
card_content = []
# カード内のフィールド
for field_name, value in item.items():
if field_name != 'id':
card_content.append(ft.Text(f"{field_name}: {value}", size=12))
card = ft.Card(
content=ft.Container(
content=ft.Column(card_content),
padding=10,
width=card_width,
height=card_height
),
margin=ft.margin.only(bottom=5)
)
cards.append(card)
return ft.Column(cards, scroll=ft.ScrollMode.AUTO)
def render_list_format(self, items: List[Dict]) -> ft.Column:
"""リスト形式でレンダリング"""
if not self.current_theme or self.current_theme.format_type != 'list':
return ft.Column([ft.Text("テーマが設定されていません")])
config = self.current_theme.config
item_height = config.get('item_height', 60)
show_dividers = config.get('show_dividers', True)
compact_mode = config.get('compact_mode', False)
list_items = []
for item in items:
item_content = []
for field_name, value in item.items():
if field_name != 'id':
item_content.append(ft.Text(f"{field_name}: {value}", size=12))
list_item = ft.Container(
content=ft.Column(item_content),
padding=10,
height=item_height if not compact_mode else None,
bgcolor=ft.Colors.GREY_50 if not compact_mode else None
)
list_items.append(list_item)
if show_dividers:
list_items.append(ft.Divider(height=1))
return ft.Column(list_items, scroll=ft.ScrollMode.AUTO)
def render_custom_format(self, items: List[Dict]) -> ft.Column:
"""カスタム形式でレンダリング"""
if not self.current_theme or self.current_theme.format_type != 'custom':
return ft.Column([ft.Text("テーマが設定されていません")])
config = self.current_theme.config
# カスタムレンダリングロジック
custom_items = []
for item in items:
custom_content = []
# ユーザー定義のレンダリング
for field_name, value in item.items():
if field_name != 'id':
field_config = config.get('fields', {}).get(field_name, {})
field_type = field_config.get('type', 'text')
if field_type == 'text':
custom_content.append(ft.Text(f"{field_name}: {value}", size=12))
elif field_type == 'badge':
custom_content.append(
ft.Container(
content=ft.Text(value, size=10),
bgcolor=ft.Colors.BLUE,
padding=ft.padding.symmetric(5, 2),
border_radius=5
)
)
elif field_type == 'progress':
custom_content.append(
ft.ProgressBar(
value=float(value) if value else 0,
width=200
)
)
custom_items.append(ft.Container(
content=ft.Column(custom_content),
padding=10,
border=ft.border.all(1, ft.Colors.GREY_300),
border_radius=5,
margin=ft.margin.only(bottom=10)
))
return ft.Column(custom_items, scroll=ft.ScrollMode.AUTO)
class SlipEntryFramework:
"""伝票入力フレームワーク"""
def __init__(self, page: ft.Page):
self.page = page
self.renderer = SlipEntryRenderer(page)
self.themes = {}
self.current_theme_name = None
self.items = []
# UI部品
self.title = ft.Text("伝票入力フレームワーク", size=24, weight=ft.FontWeight.BOLD, color=ft.Colors.BLUE_900)
# テーマ選択
self.theme_dropdown = ft.Dropdown(
label="表示形式",
options=[],
on_change=self.change_theme
)
# 入力フォーム
self.form_fields = ft.Column([], spacing=10)
# 表示エリア
self.display_area = ft.Column([], scroll=ft.ScrollMode.AUTO, height=300)
# ボタン群
self.add_item_btn = ft.Button("明細追加", on_click=self.add_item, bgcolor=ft.Colors.BLUE, color=ft.Colors.WHITE)
self.save_btn = ft.Button("保存", on_click=self.save_slip, bgcolor=ft.Colors.GREEN, color=ft.Colors.WHITE)
self.clear_btn = ft.Button("クリア", on_click=self.clear_form, bgcolor=ft.Colors.ORANGE, color=ft.Colors.WHITE)
# 操作説明
self.instructions = ft.Text(
"操作方法: 表示形式を選択して伝票を入力",
size=12,
color=ft.Colors.GREY_600
)
# 初期化
self._init_themes()
self._build_form()
def _init_themes(self):
"""テーマを初期化"""
try:
conn = sqlite3.connect('sales.db')
cursor = conn.cursor()
# テーマテーブル作成
cursor.execute('''
CREATE TABLE IF NOT EXISTS display_themes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
title TEXT NOT NULL,
format_type TEXT NOT NULL,
layout_config TEXT NOT NULL,
is_active BOOLEAN DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
# デフォルトテーマを挿入
default_themes = [
{
'name': 'table_standard',
'title': '表形式(標準)',
'format_type': 'table',
'layout_config': json.dumps({
'columns': [
{'field': 'item_name', 'label': '商品名', 'width': 200},
{'field': 'quantity', 'label': '数量', 'width': 100, 'align': 'right'},
{'field': 'unit_price', 'label': '単価', 'width': 120, 'align': 'right'},
{'field': 'amount', 'label': '金額', 'width': 120, 'align': 'right'}
]
})
},
{
'name': 'card_standard',
'title': 'カード形式(標準)',
'format_type': 'card',
'layout_config': json.dumps({
'card_width': 350,
'card_height': 100,
'fields_layout': 'vertical'
})
},
{
'name': 'list_standard',
'title': 'リスト形式(標準)',
'format_type': 'list',
'layout_config': json.dumps({
'item_height': 80,
'show_dividers': True,
'compact_mode': False
})
}
]
# デフォルトテーマがなければ挿入
cursor.execute("SELECT COUNT(*) FROM display_themes")
if cursor.fetchone()[0] == 0:
for theme in default_themes:
cursor.execute('''
INSERT INTO display_themes (name, title, format_type, layout_config)
VALUES (?, ?, ?, ?)
''', (theme['name'], theme['title'], theme['format_type'], theme['layout_config']))
conn.commit()
# テーマを読み込み
cursor.execute("SELECT * FROM display_themes WHERE is_active = 1 ORDER BY id")
themes_data = cursor.fetchall()
for theme_data in themes_data:
theme_id, name, title, format_type, layout_config, is_active, created_at = theme_data
self.themes[name] = DisplayTheme(name, title, format_type, json.loads(layout_config))
conn.close()
# ドロップダウンを更新
self.theme_dropdown.options = [
ft.dropdown.Option(theme.title, name)
for name, theme in self.themes.items()
]
if self.themes:
first_theme = list(self.themes.keys())[0]
self.theme_dropdown.value = first_theme
self.current_theme_name = first_theme
self.current_theme = self.themes[first_theme]
self.renderer.set_theme(self.current_theme)
self.page.update()
except Exception as e:
logging.error(f"テーマ初期化エラー: {e}")
def change_theme(self, e):
"""テーマを変更"""
theme_name = e.control.value
if theme_name in self.themes:
self.current_theme_name = theme_name
self.current_theme = self.themes[theme_name]
self.renderer.set_theme(self.current_theme)
self._update_display()
logging.info(f"テーマ変更: {theme_name}")
def _build_form(self):
"""入力フォームを構築"""
self.form_fields.controls.clear()
# 基本フィールド
fields = [
{'name': 'item_name', 'label': '商品名', 'width': 200},
{'name': 'quantity', 'label': '数量', 'width': 100},
{'name': 'unit_price', 'label': '単価', 'width': 120},
{'name': 'amount', 'label': '金額', 'width': 120}
]
for field in fields:
field_control = ft.TextField(
label=field['label'],
value='',
width=field['width']
)
self.form_fields.controls.append(field_control)
self.page.update()
def _update_display(self):
"""表示エリアを更新"""
self.display_area.controls.clear()
if self.items:
if self.current_theme.format_type == 'table':
self.display_area.controls.append(self.renderer.render_table_format(self.items))
elif self.current_theme.format_type == 'card':
self.display_area.controls.append(self.renderer.render_card_format(self.items))
elif self.current_theme.format_type == 'list':
self.display_area.controls.append(self.renderer.render_list_format(self.items))
elif self.current_theme.format_type == 'custom':
self.display_area.controls.append(self.renderer.render_custom_format(self.items))
self.page.update()
def add_item(self, e):
"""明細を追加"""
# フォームデータを収集
form_data = {}
for i, field in enumerate(self.form_fields.controls):
field_name = ['item_name', 'quantity', 'unit_price', 'amount'][i]
form_data[field_name] = field.control.value
# 金額を計算
try:
quantity = float(form_data.get('quantity', 0))
unit_price = float(form_data.get('unit_price', 0))
amount = quantity * unit_price
form_data['amount'] = str(amount)
except:
form_data['amount'] = '0'
# リストに追加
self.items.append(form_data)
# 表示を更新
self._update_display()
# フォームをクリア
self.clear_form(None)
logging.info(f"明細追加: {form_data}")
def clear_form(self, e):
"""フォームをクリア"""
for field_control in self.form_fields.controls:
field_control.value = ''
self.page.update()
logging.info("フォームをクリア")
def save_slip(self, e):
"""伝票を保存"""
if not self.items:
return
try:
# データベースに保存
conn = sqlite3.connect('sales.db')
cursor = conn.cursor()
# 伝票テーブル作成
cursor.execute('''
CREATE TABLE IF NOT EXISTS slips (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
theme_name TEXT NOT NULL,
items_data TEXT NOT NULL,
total_amount REAL NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
# 合計金額を計算
total_amount = sum(float(item.get('amount', 0)) for item in self.items)
# 伝票を保存
cursor.execute('''
INSERT INTO slips (title, theme_name, items_data, total_amount)
VALUES (?, ?, ?, ?)
''', (
f"伝票_{datetime.now().strftime('%Y%m%d_%H%M%S')}",
self.current_theme_name,
json.dumps(self.items),
total_amount
))
conn.commit()
conn.close()
# 成功メッセージ
self._show_snackbar("伝票を保存しました", ft.Colors.GREEN)
logging.info(f"伝票保存: {len(self.items)}件, 合計: {total_amount}")
# クリア
self.items.clear()
self._update_display()
except Exception as ex:
logging.error(f"伝票保存エラー: {ex}")
self._show_snackbar("保存エラー", ft.Colors.RED)
def _show_snackbar(self, message: str, color: ft.Colors):
"""SnackBarを表示"""
try:
self.page.snack_bar = ft.SnackBar(
content=ft.Text(message),
bgcolor=color
)
self.page.snack_bar.open = True
self.page.update()
except:
pass
def build(self):
"""UIを構築して返す"""
return ft.Column([
ft.Row([
self.title,
self.theme_dropdown
], alignment=ft.MainAxisAlignment.SPACE_BETWEEN),
ft.Divider(),
ft.Text("明細入力", size=18, weight=ft.FontWeight.BOLD),
self.form_fields,
ft.Row([self.add_item_btn, self.save_btn, self.clear_btn], spacing=10),
ft.Divider(),
ft.Text("伝票プレビュー", size=18, weight=ft.FontWeight.BOLD),
self.instructions,
self.display_area
], expand=True, spacing=15)
# 使用例
def create_slip_entry_framework(page: ft.Page) -> SlipEntryFramework:
"""伝票入力フレームワークを作成"""
return SlipEntryFramework(page)