""" 伝票入力フレームワーク 表示形式を動的に切り替える伝票入力システム """ 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_select=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)