1261 lines
53 KiB
Python
1261 lines
53 KiB
Python
"""
|
||
Flutter風ダッシュボード
|
||
下部ナビゲーションと洗練されたUIコンポーネントを実装
|
||
"""
|
||
|
||
import flet as ft
|
||
import signal
|
||
import sys
|
||
import logging
|
||
from datetime import datetime
|
||
from typing import List, Dict, Optional
|
||
from models.invoice_models import DocumentType, Invoice, create_sample_invoices, Customer, InvoiceItem
|
||
from components.customer_picker import CustomerPickerModal
|
||
from services.app_service import AppService
|
||
|
||
# ロギング設定
|
||
logging.basicConfig(
|
||
level=logging.INFO,
|
||
format='%(asctime)s - %(levelname)s - %(message)s'
|
||
)
|
||
|
||
class FlutterStyleDashboard:
|
||
"""Flutter風の統合ダッシュボード"""
|
||
|
||
def __init__(self, page: ft.Page):
|
||
self.page = page
|
||
self.current_tab = 0 # 0: 伝票一覧, 1: 詳細編集
|
||
self.selected_customer = None
|
||
self.selected_document_type = DocumentType.INVOICE
|
||
self.amount_value = "250000"
|
||
self.customer_picker = None
|
||
self.editing_invoice = None # 編集中の伝票
|
||
self.is_edit_mode = False # 編集モードフラグ
|
||
self.is_customer_picker_open = False
|
||
self.customer_search_query = ""
|
||
self.show_offsets = False
|
||
self.chain_verify_result = None
|
||
self.is_new_customer_form_open = False
|
||
|
||
# ビジネスロジックサービス
|
||
self.app_service = AppService()
|
||
self.invoices = []
|
||
self.customers = []
|
||
|
||
self.setup_page()
|
||
self.setup_database()
|
||
self.setup_ui()
|
||
|
||
def setup_page(self):
|
||
"""ページ設定"""
|
||
self.page.title = "販売アシスト1号"
|
||
self.page.window.width = 420
|
||
self.page.window.height = 900
|
||
self.page.theme_mode = ft.ThemeMode.LIGHT
|
||
|
||
# Fletのライフサイクルに任せる(SystemExitがasyncioに伝播して警告になりやすい)
|
||
|
||
def setup_database(self):
|
||
"""データ初期化(サービス層経由)"""
|
||
try:
|
||
# 顧客データ読み込み
|
||
self.customers = self.app_service.customer.get_all_customers()
|
||
|
||
# 伝票データ読み込み
|
||
self.invoices = self.app_service.invoice.get_recent_invoices(20)
|
||
|
||
logging.info(f"データ初期化: 顧客{len(self.customers)}件, 伝票{len(self.invoices)}件")
|
||
|
||
except Exception as e:
|
||
logging.error(f"データ初期化エラー: {e}")
|
||
|
||
def create_sample_data(self):
|
||
"""サンプル伝票データ作成"""
|
||
try:
|
||
# サンプルデータ
|
||
sample_invoices = create_sample_invoices()
|
||
|
||
for invoice in sample_invoices:
|
||
self.cursor.execute('''
|
||
INSERT OR REPLACE INTO slips
|
||
(document_type, customer_name, amount, date, status, description)
|
||
VALUES (?, ?, ?, ?, ?, ?)
|
||
''', (
|
||
invoice.document_type.value,
|
||
invoice.customer.formal_name,
|
||
invoice.total_amount,
|
||
invoice.date.strftime('%Y-%m-%d'),
|
||
'完了',
|
||
invoice.notes
|
||
))
|
||
|
||
self.conn.commit()
|
||
except Exception as e:
|
||
logging.error(f"サンプルデータ作成エラー: {e}")
|
||
|
||
def setup_ui(self):
|
||
"""UIセットアップ"""
|
||
# 下部ナビゲーション(NavigationRail風)
|
||
bottom_nav = ft.Container(
|
||
content=ft.Row([
|
||
ft.Container(
|
||
content=ft.IconButton(
|
||
ft.Icons.ADD_BOX,
|
||
selected=self.current_tab == 0,
|
||
on_click=lambda _: self.on_tab_change(0),
|
||
),
|
||
),
|
||
ft.Text("新規作成"),
|
||
ft.Container(
|
||
content=ft.IconButton(
|
||
ft.Icons.HISTORY,
|
||
selected=self.current_tab == 1,
|
||
on_click=lambda _: self.on_tab_change(1),
|
||
),
|
||
),
|
||
ft.Text("発行履歴"),
|
||
], alignment=ft.MainAxisAlignment.CENTER),
|
||
padding=ft.Padding.all(10),
|
||
bgcolor=ft.Colors.BLUE_GREY_50,
|
||
)
|
||
|
||
# メインコンテンツ
|
||
self.main_content = ft.Column([], expand=True)
|
||
|
||
# ページ構成
|
||
self.page.add(
|
||
ft.Column([
|
||
self.main_content,
|
||
bottom_nav,
|
||
], expand=True)
|
||
)
|
||
|
||
# 初期表示
|
||
self.update_main_content()
|
||
|
||
def on_tab_change(self, index):
|
||
"""タブ切り替え"""
|
||
self.current_tab = index
|
||
self.update_main_content()
|
||
self.page.update()
|
||
|
||
def update_main_content(self):
|
||
"""メインコンテンツ更新"""
|
||
self.main_content.controls.clear()
|
||
|
||
if self.current_tab == 0:
|
||
if self.is_new_customer_form_open:
|
||
self.main_content.controls.append(self.create_new_customer_screen())
|
||
elif self.is_customer_picker_open:
|
||
self.main_content.controls.append(self.create_customer_picker_screen())
|
||
else:
|
||
# 伝票一覧画面
|
||
self.main_content.controls.append(self.create_slip_list_screen())
|
||
else:
|
||
# 詳細編集画面
|
||
self.main_content.controls.append(self.create_invoice_edit_screen())
|
||
|
||
self.page.update()
|
||
|
||
def create_slip_list_screen(self) -> ft.Container:
|
||
"""伝票一覧画面"""
|
||
# 履歴データ読み込み
|
||
slips = self.load_slips()
|
||
if not self.show_offsets:
|
||
slips = [s for s in slips if not (isinstance(s, Invoice) and getattr(s, "is_offset", False))]
|
||
|
||
def on_toggle_offsets(e):
|
||
self.show_offsets = bool(e.control.value)
|
||
self.update_main_content()
|
||
|
||
def on_verify_chain(e=None):
|
||
res = self.app_service.invoice.invoice_repo.verify_chain()
|
||
self.chain_verify_result = res
|
||
self.update_main_content()
|
||
|
||
def create_new_slip(_):
|
||
"""新規伝票作成"""
|
||
self.editing_invoice = None
|
||
self.current_tab = 1
|
||
self.update_main_content()
|
||
|
||
# 履歴カードリスト
|
||
slip_cards = []
|
||
for slip in slips:
|
||
card = self.create_slip_card(slip)
|
||
slip_cards.append(card)
|
||
|
||
return ft.Container(
|
||
content=ft.Column([
|
||
# ヘッダー
|
||
ft.Container(
|
||
content=ft.Row([
|
||
ft.Text("📄 伝票一覧", size=20, weight=ft.FontWeight.BOLD),
|
||
ft.Container(expand=True),
|
||
ft.IconButton(
|
||
ft.Icons.VERIFIED,
|
||
tooltip="チェーン検証",
|
||
icon_color=ft.Colors.BLUE_300,
|
||
on_click=on_verify_chain,
|
||
),
|
||
ft.Row(
|
||
[
|
||
ft.Text("赤伝を表示", size=12, color=ft.Colors.WHITE),
|
||
ft.Switch(value=self.show_offsets, on_change=on_toggle_offsets),
|
||
],
|
||
spacing=5,
|
||
),
|
||
ft.IconButton(ft.Icons.CLEAR_ALL, icon_size=20),
|
||
]),
|
||
padding=ft.padding.all(15),
|
||
bgcolor=ft.Colors.BLUE_GREY,
|
||
),
|
||
|
||
# 検証結果表示(あれば)
|
||
ft.Container(
|
||
content=self._build_chain_verify_result(),
|
||
margin=ft.Margin.only(bottom=10),
|
||
) if self.chain_verify_result else ft.Container(height=0),
|
||
|
||
# 伝票リスト
|
||
ft.Column(
|
||
controls=slip_cards,
|
||
spacing=10,
|
||
scroll=ft.ScrollMode.AUTO,
|
||
expand=True,
|
||
),
|
||
|
||
# 新規作成ボタン
|
||
ft.Container(
|
||
content=ft.Button(
|
||
content=ft.Row([
|
||
ft.Icon(ft.Icons.ADD, color=ft.Colors.WHITE),
|
||
ft.Text("新規伝票を作成", color=ft.Colors.WHITE),
|
||
], alignment=ft.MainAxisAlignment.CENTER),
|
||
style=ft.ButtonStyle(
|
||
bgcolor=ft.Colors.BLUE_GREY_800,
|
||
padding=ft.padding.all(20),
|
||
shape=ft.RoundedRectangleBorder(radius=15),
|
||
),
|
||
width=400,
|
||
height=60,
|
||
on_click=create_new_slip,
|
||
),
|
||
padding=ft.padding.all(20),
|
||
),
|
||
]),
|
||
expand=True,
|
||
)
|
||
def create_customer_picker_screen(self) -> ft.Container:
|
||
"""顧客選択画面(画面内遷移・ダイアログ不使用)"""
|
||
self.customers = self.app_service.customer.get_all_customers()
|
||
|
||
def back(_=None):
|
||
self.is_customer_picker_open = False
|
||
self.customer_search_query = ""
|
||
self.update_main_content()
|
||
|
||
list_container = ft.Column([], spacing=0, scroll=ft.ScrollMode.AUTO, expand=True)
|
||
|
||
def render_list(customers: List[Customer]):
|
||
list_container.controls.clear()
|
||
for customer in customers:
|
||
list_container.controls.append(
|
||
ft.ListTile(
|
||
title=ft.Text(customer.formal_name, weight=ft.FontWeight.BOLD),
|
||
subtitle=ft.Text(f"{customer.address}\n{customer.phone}"),
|
||
on_click=lambda _, c=customer: select_customer(c),
|
||
)
|
||
)
|
||
self.page.update()
|
||
|
||
def select_customer(customer: Customer):
|
||
self.selected_customer = customer
|
||
logging.info(f"顧客を選択: {customer.formal_name}")
|
||
back()
|
||
|
||
def on_search_change(e):
|
||
q = (e.control.value or "").strip().lower()
|
||
self.customer_search_query = q
|
||
if not q:
|
||
render_list(self.customers)
|
||
return
|
||
filtered = [
|
||
c
|
||
for c in self.customers
|
||
if q in (c.name or "").lower()
|
||
or q in (c.formal_name or "").lower()
|
||
or q in (c.address or "").lower()
|
||
or q in (c.phone or "").lower()
|
||
]
|
||
render_list(filtered)
|
||
|
||
search_field = ft.TextField(
|
||
label="顧客検索",
|
||
prefix_icon=ft.Icons.SEARCH,
|
||
value=self.customer_search_query,
|
||
on_change=on_search_change,
|
||
autofocus=True,
|
||
)
|
||
|
||
header = ft.Container(
|
||
content=ft.Row(
|
||
[
|
||
ft.IconButton(ft.Icons.ARROW_BACK, on_click=back),
|
||
ft.Text("顧客を選択", size=18, weight=ft.FontWeight.BOLD),
|
||
ft.Container(expand=True),
|
||
ft.IconButton(
|
||
ft.Icons.PERSON_ADD,
|
||
tooltip="新規顧客追加",
|
||
icon_color=ft.Colors.WHITE,
|
||
on_click=lambda _: self.open_new_customer_form(),
|
||
),
|
||
]
|
||
),
|
||
padding=ft.padding.all(15),
|
||
bgcolor=ft.Colors.BLUE_GREY,
|
||
)
|
||
|
||
content = ft.Column(
|
||
[
|
||
header,
|
||
ft.Container(content=search_field, padding=ft.padding.all(15)),
|
||
ft.Container(content=list_container, padding=ft.padding.symmetric(horizontal=15), expand=True),
|
||
],
|
||
expand=True,
|
||
)
|
||
|
||
# 初期表示
|
||
render_list(self.customers)
|
||
|
||
return ft.Container(content=content, expand=True)
|
||
|
||
def create_slip_input_screen(self) -> ft.Container:
|
||
"""伝票入力画面"""
|
||
# 帳票種類選択(タブ風ボタン)
|
||
document_types = list(DocumentType)
|
||
|
||
type_buttons = ft.Row([
|
||
ft.Container(
|
||
content=ft.Button(
|
||
content=ft.Text(doc_type.value, size=12),
|
||
bgcolor=ft.Colors.BLUE_GREY_800 if doc_type == self.selected_document_type else ft.Colors.GREY_300,
|
||
color=ft.Colors.WHITE if doc_type == self.selected_document_type else ft.Colors.BLACK,
|
||
on_click=lambda _, idx=i, dt=doc_type: self.select_document_type(dt.value),
|
||
width=100,
|
||
height=45,
|
||
),
|
||
margin=ft.margin.only(right=5),
|
||
) for i, doc_type in enumerate(document_types)
|
||
], wrap=True)
|
||
|
||
return ft.Container(
|
||
content=ft.Column([
|
||
# ヘッダー
|
||
ft.Container(
|
||
content=ft.Row([
|
||
ft.Text("📋 販売アシスト1号", size=20, weight=ft.FontWeight.BOLD),
|
||
ft.Container(expand=True),
|
||
ft.IconButton(ft.Icons.SETTINGS, icon_size=20),
|
||
]),
|
||
padding=ft.padding.all(15),
|
||
bgcolor=ft.Colors.BLUE_GREY,
|
||
),
|
||
|
||
# 帳票種類選択
|
||
ft.Container(
|
||
content=ft.Column([
|
||
ft.Text("帳票の種類を選択", size=16, weight=ft.FontWeight.BOLD),
|
||
ft.Container(height=10),
|
||
type_buttons,
|
||
]),
|
||
padding=ft.padding.all(20),
|
||
),
|
||
|
||
# 顧客選択
|
||
ft.Container(
|
||
content=ft.Column([
|
||
ft.Text("宛先と基本金額の設定", size=16, weight=ft.FontWeight.BOLD),
|
||
ft.Container(height=10),
|
||
ft.Row(
|
||
[
|
||
ft.TextField(
|
||
label="取引先名",
|
||
value=self.selected_customer.formal_name if self.selected_customer else "未選択",
|
||
read_only=True,
|
||
border_color=ft.Colors.BLUE_GREY,
|
||
expand=True,
|
||
),
|
||
ft.IconButton(
|
||
ft.Icons.PERSON_SEARCH,
|
||
tooltip="顧客を選択",
|
||
on_click=self.open_customer_picker,
|
||
),
|
||
],
|
||
spacing=8,
|
||
),
|
||
ft.Container(height=10),
|
||
ft.TextField(
|
||
label="基本金額 (税抜)",
|
||
value=self.amount_value,
|
||
keyboard_type=ft.KeyboardType.NUMBER,
|
||
on_change=self.on_amount_change,
|
||
border_color=ft.Colors.BLUE_GREY,
|
||
),
|
||
]),
|
||
padding=ft.padding.all(20),
|
||
),
|
||
|
||
# 作成ボタン
|
||
ft.Container(
|
||
content=ft.Button(
|
||
content=ft.Row([
|
||
ft.Icon(ft.Icons.DESCRIPTION, color=ft.Colors.WHITE),
|
||
ft.Text("伝票を作成して詳細編集へ", color=ft.Colors.WHITE),
|
||
], alignment=ft.MainAxisAlignment.CENTER),
|
||
style=ft.ButtonStyle(
|
||
bgcolor=ft.Colors.BLUE_GREY_800,
|
||
padding=ft.padding.all(20),
|
||
shape=ft.RoundedRectangleBorder(radius=15),
|
||
),
|
||
width=400,
|
||
height=60,
|
||
on_click=self.create_slip,
|
||
),
|
||
padding=ft.padding.all(20),
|
||
),
|
||
|
||
]),
|
||
expand=True,
|
||
)
|
||
|
||
def create_slip_history_screen(self) -> ft.Container:
|
||
"""伝票履歴画面"""
|
||
# 履歴データ読み込み
|
||
slips = self.load_slips()
|
||
if not self.show_offsets:
|
||
slips = [s for s in slips if not (isinstance(s, Invoice) and getattr(s, "is_offset", False))]
|
||
|
||
def on_toggle_offsets(e):
|
||
self.show_offsets = bool(e.control.value)
|
||
self.update_main_content()
|
||
|
||
def on_verify_chain(e=None):
|
||
res = self.app_service.invoice.invoice_repo.verify_chain()
|
||
self.chain_verify_result = res
|
||
self.update_main_content()
|
||
|
||
# 履歴カードリスト
|
||
slip_cards = []
|
||
for slip in slips:
|
||
card = self.create_slip_card(slip)
|
||
slip_cards.append(card)
|
||
|
||
return ft.Container(
|
||
content=ft.Column([
|
||
# ヘッダー
|
||
ft.Container(
|
||
content=ft.Row([
|
||
ft.Text("📄 発行履歴管理", size=20, weight=ft.FontWeight.BOLD),
|
||
ft.Container(expand=True),
|
||
ft.IconButton(
|
||
ft.Icons.VERIFIED,
|
||
tooltip="チェーン検証",
|
||
icon_color=ft.Colors.BLUE_300,
|
||
on_click=on_verify_chain,
|
||
),
|
||
ft.Row(
|
||
[
|
||
ft.Text("赤伝を表示", size=12, color=ft.Colors.WHITE),
|
||
ft.Switch(value=self.show_offsets, on_change=on_toggle_offsets),
|
||
],
|
||
spacing=5,
|
||
),
|
||
ft.IconButton(ft.Icons.CLEAR_ALL, icon_size=20),
|
||
]),
|
||
padding=ft.padding.all(15),
|
||
bgcolor=ft.Colors.BLUE_GREY,
|
||
),
|
||
|
||
# 検証結果表示(あれば)
|
||
ft.Container(
|
||
content=self._build_chain_verify_result(),
|
||
margin=ft.Margin.only(bottom=10),
|
||
) if self.chain_verify_result else ft.Container(height=0),
|
||
|
||
# 履歴リスト
|
||
ft.Column(
|
||
controls=slip_cards,
|
||
spacing=10,
|
||
scroll=ft.ScrollMode.AUTO,
|
||
expand=True,
|
||
),
|
||
]),
|
||
expand=True,
|
||
)
|
||
|
||
def create_slip_card(self, slip) -> ft.Card:
|
||
"""伝票カード作成"""
|
||
# サービス層からは Invoice オブジェクトが返る
|
||
if isinstance(slip, Invoice):
|
||
slip_type = slip.document_type.value
|
||
customer_name = slip.customer.formal_name
|
||
amount = slip.total_amount
|
||
date = slip.date.strftime("%Y-%m-%d")
|
||
status = "赤伝" if getattr(slip, "is_offset", False) else "完了"
|
||
else:
|
||
slip_id, slip_type, customer_name, amount, date, status, description, created_at = slip
|
||
|
||
# タイプに応じたアイコンと色
|
||
type_config = {
|
||
"売上伝票": {"icon": "💰", "color": ft.Colors.GREEN},
|
||
"見積書": {"icon": "📄", "color": ft.Colors.BLUE},
|
||
"納品書": {"icon": "📦", "color": ft.Colors.PURPLE},
|
||
"請求書": {"icon": "📋", "color": ft.Colors.ORANGE},
|
||
"領収書": {"icon": "🧾", "color": ft.Colors.RED}
|
||
}
|
||
|
||
config = type_config.get(slip_type, {"icon": "📝", "color": ft.Colors.GREY})
|
||
|
||
def issue_offset(_=None):
|
||
if not isinstance(slip, Invoice):
|
||
return
|
||
if getattr(slip, "is_offset", False):
|
||
return
|
||
offset_invoice = self.app_service.invoice.create_offset_invoice(slip.uuid)
|
||
if offset_invoice and offset_invoice.file_path:
|
||
self.app_service.invoice.delete_pdf_file(offset_invoice.file_path)
|
||
# リストを更新
|
||
self.invoices = self.app_service.invoice.get_recent_invoices(20)
|
||
self.update_main_content()
|
||
|
||
def regenerate_and_share(_=None):
|
||
if not isinstance(slip, Invoice):
|
||
return
|
||
pdf_path = self.app_service.invoice.regenerate_pdf(slip.uuid)
|
||
if pdf_path:
|
||
# TODO: OSの共有ダイアログを呼ぶ(プラットフォーム依存)
|
||
logging.info(f"PDF再生成: {pdf_path}(共有機能は未実装)")
|
||
# 共有後に削除(今は即削除)
|
||
self.app_service.invoice.delete_pdf_file(pdf_path)
|
||
logging.info(f"PDF削除完了: {pdf_path}")
|
||
else:
|
||
logging.error("PDF再生成失敗")
|
||
|
||
def edit_invoice(_=None):
|
||
if not isinstance(slip, Invoice):
|
||
return
|
||
self.open_invoice_edit(slip)
|
||
|
||
actions_row = None
|
||
if isinstance(slip, Invoice):
|
||
buttons = []
|
||
if not getattr(slip, "submitted_to_tax_authority", False):
|
||
buttons.append(
|
||
ft.ElevatedButton(
|
||
content=ft.Text("編集", color=ft.Colors.WHITE),
|
||
style=ft.ButtonStyle(
|
||
bgcolor=ft.Colors.GREEN_600,
|
||
padding=ft.padding.all(10),
|
||
),
|
||
on_click=edit_invoice,
|
||
width=80,
|
||
height=40,
|
||
)
|
||
)
|
||
buttons.append(
|
||
ft.ElevatedButton(
|
||
content=ft.Text("赤伝", color=ft.Colors.WHITE),
|
||
style=ft.ButtonStyle(
|
||
bgcolor=ft.Colors.RED_600,
|
||
padding=ft.padding.all(10),
|
||
),
|
||
on_click=issue_offset,
|
||
width=80,
|
||
height=40,
|
||
)
|
||
)
|
||
buttons.append(
|
||
ft.ElevatedButton(
|
||
content=ft.Text("提出", color=ft.Colors.WHITE),
|
||
style=ft.ButtonStyle(
|
||
bgcolor=ft.Colors.ORANGE_600,
|
||
padding=ft.padding.all(10),
|
||
),
|
||
on_click=lambda _: self.submit_invoice_for_tax(slip.uuid),
|
||
width=80,
|
||
height=40,
|
||
)
|
||
)
|
||
buttons.append(
|
||
ft.IconButton(
|
||
ft.Icons.DOWNLOAD,
|
||
tooltip="PDF再生成→共有",
|
||
icon_color=ft.Colors.BLUE_400,
|
||
on_click=regenerate_and_share,
|
||
)
|
||
)
|
||
actions_row = ft.Row(buttons, alignment=ft.MainAxisAlignment.CENTER, spacing=10)
|
||
|
||
display_amount = amount
|
||
if isinstance(slip, Invoice) and getattr(slip, "is_offset", False):
|
||
display_amount = -abs(amount)
|
||
|
||
return ft.Card(
|
||
content=ft.Container(
|
||
content=ft.Column([
|
||
ft.Row([
|
||
ft.Container(
|
||
content=ft.Text(config["icon"], size=24),
|
||
width=40,
|
||
height=40,
|
||
bgcolor=config["color"],
|
||
border_radius=20,
|
||
alignment=ft.alignment.Alignment(0, 0),
|
||
),
|
||
ft.Container(
|
||
content=ft.Column([
|
||
ft.Text(slip_type, size=12, weight=ft.FontWeight.BOLD),
|
||
ft.Text(f"{customer_name} ¥{display_amount:,.0f}", size=10),
|
||
]),
|
||
expand=True,
|
||
),
|
||
]),
|
||
ft.Container(height=5),
|
||
ft.Text(f"日付: {date}", size=10, color=ft.Colors.GREY_600),
|
||
ft.Text(f"状態: {status}", size=10, color=ft.Colors.GREY_600),
|
||
actions_row if actions_row else ft.Container(height=0),
|
||
]),
|
||
padding=ft.padding.all(15),
|
||
),
|
||
elevation=3,
|
||
)
|
||
|
||
def _build_chain_verify_result(self) -> ft.Control:
|
||
if not self.chain_verify_result:
|
||
return ft.Container(height=0)
|
||
r = self.chain_verify_result
|
||
ok = r.get("ok", False)
|
||
checked = r.get("checked", 0)
|
||
errors = r.get("errors", [])
|
||
if ok:
|
||
return ft.Container(
|
||
content=ft.Row([
|
||
ft.Icon(ft.Icons.CHECK_CIRCLE, color=ft.Colors.GREEN, size=20),
|
||
ft.Text(f"チェーン検証 OK ({checked}件)", size=14, color=ft.Colors.GREEN),
|
||
]),
|
||
bgcolor=ft.Colors.GREEN_50,
|
||
padding=ft.Padding.all(10),
|
||
border_radius=8,
|
||
)
|
||
else:
|
||
return ft.Container(
|
||
content=ft.Column([
|
||
ft.Row([
|
||
ft.Icon(ft.Icons.ERROR, color=ft.Colors.RED, size=20),
|
||
ft.Text(f"チェーン検証 NG (checked={checked})", size=14, color=ft.Colors.RED),
|
||
]),
|
||
ft.Text(f"エラー: {errors}", size=12, color=ft.Colors.RED_700),
|
||
]),
|
||
bgcolor=ft.Colors.RED_50,
|
||
padding=ft.Padding.all(10),
|
||
border_radius=8,
|
||
)
|
||
|
||
def open_invoice_edit(self, invoice: Invoice):
|
||
"""伝票編集画面を開く"""
|
||
self.editing_invoice = invoice
|
||
self.is_edit_mode = True
|
||
self.selected_customer = invoice.customer
|
||
self.selected_document_type = invoice.document_type
|
||
self.amount_value = str(invoice.items[0].unit_price if invoice.items else "0")
|
||
self.is_detail_edit_mode = False # 初期はビューモード
|
||
self.current_tab = 1 # 詳細編集タブに切り替え
|
||
self.update_main_content()
|
||
|
||
def open_new_customer_form(self):
|
||
"""新規顧客フォームを開く(画面内遷移)"""
|
||
self.is_new_customer_form_open = True
|
||
self.update_main_content()
|
||
|
||
def create_invoice_edit_screen(self) -> ft.Container:
|
||
"""伝票編集画面"""
|
||
if self.editing_invoice:
|
||
# 既存伝票の編集
|
||
return self._create_edit_existing_screen()
|
||
else:
|
||
# 新規伝票作成
|
||
return self.create_slip_input_screen()
|
||
|
||
def _create_edit_existing_screen(self) -> ft.Container:
|
||
"""既存伝票の編集画面"""
|
||
# 編集不可チェック
|
||
is_locked = getattr(self.editing_invoice, 'submitted_to_tax_authority', False)
|
||
is_view_mode = not getattr(self, 'is_detail_edit_mode', False)
|
||
|
||
# 伝票種類選択(ヘッダーに移動)
|
||
document_types = list(DocumentType)
|
||
|
||
# 明細テーブル
|
||
items_table = self._create_items_table(self.editing_invoice.items, is_locked or is_view_mode)
|
||
|
||
# 顧客表示
|
||
customer_display = ft.Container(
|
||
content=ft.Row([
|
||
ft.Text("顧客: ", weight=ft.FontWeight.BOLD),
|
||
ft.Text(self.editing_invoice.customer.name), # 顧客名を表示
|
||
]),
|
||
padding=ft.padding.symmetric(horizontal=10, vertical=5),
|
||
bgcolor=ft.Colors.GREY_100,
|
||
border_radius=5,
|
||
)
|
||
|
||
# 備考フィールド
|
||
notes_field = ft.TextField(
|
||
label="備考",
|
||
value=getattr(self.editing_invoice, 'notes', ''),
|
||
disabled=is_locked or is_view_mode,
|
||
multiline=True,
|
||
min_lines=2,
|
||
max_lines=3,
|
||
)
|
||
|
||
def toggle_edit_mode(_):
|
||
"""編集モード切替"""
|
||
old_mode = getattr(self, 'is_detail_edit_mode', False)
|
||
self.is_detail_edit_mode = not old_mode
|
||
logging.debug(f"Toggle edit mode: {old_mode} -> {self.is_detail_edit_mode}")
|
||
self.update_main_content()
|
||
|
||
def save_changes(_):
|
||
if is_locked:
|
||
return
|
||
|
||
# 伝票を更新(現在の明細を保持)
|
||
# TODO: テーブルから実際の値を取得して更新
|
||
# 現在は既存の明細を維持し、備考のみ更新
|
||
self.editing_invoice.notes = notes_field.value
|
||
self.editing_invoice.document_type = self.selected_document_type
|
||
|
||
# TODO: ハッシュ再計算と保存
|
||
logging.info(f"伝票更新: {self.editing_invoice.invoice_number}")
|
||
|
||
# 編集モード終了(ビューモードに戻る)
|
||
self.is_detail_edit_mode = False # ビューモードに戻る
|
||
self.update_main_content()
|
||
|
||
def cancel_edit(_):
|
||
self.is_detail_edit_mode = False
|
||
self.is_edit_mode = False
|
||
self.editing_invoice = None
|
||
self.current_tab = 0 # 一覧タブに戻る
|
||
self.update_main_content()
|
||
|
||
return ft.Container(
|
||
content=ft.Column([
|
||
# ヘッダー
|
||
ft.Container(
|
||
content=ft.Row([
|
||
ft.Container(expand=True),
|
||
# コンパクトな伝票種類選択(セグメント化)
|
||
ft.Container(
|
||
content=ft.Row([
|
||
ft.GestureDetector(
|
||
content=ft.Container(
|
||
content=ft.Text(
|
||
doc_type.value,
|
||
size=10,
|
||
color=ft.Colors.WHITE if doc_type == self.editing_invoice.document_type else ft.Colors.GREY_600,
|
||
weight=ft.FontWeight.BOLD if doc_type == self.editing_invoice.document_type else ft.FontWeight.NORMAL,
|
||
),
|
||
padding=ft.padding.symmetric(horizontal=8, vertical=4),
|
||
bgcolor=ft.Colors.BLUE_600 if doc_type == self.editing_invoice.document_type else ft.Colors.GREY_300,
|
||
border_radius=ft.border_radius.all(4),
|
||
margin=ft.margin.only(right=1),
|
||
),
|
||
on_tap=lambda _, dt=doc_type: self.select_document_type(dt.value) if not is_locked and not is_view_mode else None,
|
||
) for doc_type in document_types
|
||
]),
|
||
padding=ft.padding.all(2),
|
||
bgcolor=ft.Colors.GREY_200,
|
||
border_radius=ft.border_radius.all(6),
|
||
margin=ft.margin.only(right=10),
|
||
),
|
||
ft.ElevatedButton(
|
||
content=ft.Text("編集" if is_view_mode else "保存"),
|
||
style=ft.ButtonStyle(
|
||
bgcolor=ft.Colors.BLUE_600 if is_view_mode else ft.Colors.GREEN_600,
|
||
),
|
||
on_click=toggle_edit_mode if is_view_mode else save_changes,
|
||
disabled=is_locked,
|
||
width=70,
|
||
height=30,
|
||
) if not is_locked else ft.Container(),
|
||
ft.Container(width=5),
|
||
ft.IconButton(ft.Icons.CLOSE, on_click=cancel_edit),
|
||
]),
|
||
padding=ft.padding.symmetric(horizontal=15, vertical=8),
|
||
bgcolor=ft.Colors.BLUE_GREY,
|
||
),
|
||
|
||
# 基本情報行(コンパクトに)
|
||
ft.Container(
|
||
content=ft.Row([
|
||
ft.Text(f"{self.editing_invoice.invoice_number} | {self.editing_invoice.date.strftime('%Y/%m/%d')} | {self.editing_invoice.customer.name}", size=12, weight=ft.FontWeight.BOLD),
|
||
ft.Container(expand=True),
|
||
]),
|
||
padding=ft.padding.symmetric(horizontal=15, vertical=3),
|
||
bgcolor=ft.Colors.GREY_50,
|
||
),
|
||
|
||
# 明細テーブル(フレキシブルに)
|
||
ft.Container(
|
||
content=ft.Column([
|
||
ft.Row([
|
||
ft.Container(expand=True),
|
||
ft.IconButton(
|
||
ft.Icons.ADD_CIRCLE_OUTLINE,
|
||
tooltip="行を追加",
|
||
icon_color=ft.Colors.GREEN_600,
|
||
disabled=is_locked or is_view_mode,
|
||
on_click=lambda _: self._add_item_row(),
|
||
) if not is_locked and not is_view_mode else ft.Container(),
|
||
]),
|
||
ft.Container(height=3),
|
||
ft.Container(
|
||
content=items_table,
|
||
height=300, # 高さを縮小
|
||
border=ft.border.all(1, ft.Colors.GREY_300),
|
||
border_radius=5,
|
||
padding=ft.padding.all(1), # パディングを最小化
|
||
width=None, # 幅を可変に
|
||
expand=True, # 利用可能な幅を全て使用
|
||
),
|
||
ft.Container(height=10),
|
||
# 合計金額表示
|
||
ft.Container(
|
||
content=ft.Row([
|
||
ft.Container(expand=True),
|
||
ft.Text("合計: ", size=14, weight=ft.FontWeight.BOLD),
|
||
ft.Text(
|
||
f"¥{sum(item.subtotal for item in self.editing_invoice.items):,}",
|
||
size=16,
|
||
weight=ft.FontWeight.BOLD,
|
||
color=ft.Colors.BLUE_600
|
||
),
|
||
]),
|
||
padding=ft.padding.symmetric(horizontal=10, vertical=8),
|
||
bgcolor=ft.Colors.GREY_100,
|
||
border_radius=5,
|
||
),
|
||
]),
|
||
padding=ft.padding.all(15),
|
||
expand=True, # 明細部分が最大限のスペースを占有
|
||
),
|
||
|
||
# 備考(コンパクト)
|
||
ft.Container(
|
||
content=ft.Column([
|
||
ft.Text("備考", size=12, weight=ft.FontWeight.BOLD),
|
||
ft.Container(height=3),
|
||
notes_field,
|
||
ft.Container(height=5),
|
||
ft.Text("🔒 税務署提出済みは編集できません" if is_locked else "✅ " + ("編集モード" if not is_view_mode else "ビューモード"),
|
||
size=11, color=ft.Colors.RED_600 if is_locked else (ft.Colors.GREEN_600 if not is_view_mode else ft.Colors.BLUE_600)),
|
||
ft.Container(height=10),
|
||
# PDF生成ボタンを追加
|
||
ft.ElevatedButton(
|
||
content=ft.Row([
|
||
ft.Icon(ft.Icons.DOWNLOAD, size=16),
|
||
ft.Container(width=5),
|
||
ft.Text("PDF生成", size=12, color=ft.Colors.WHITE),
|
||
]),
|
||
style=ft.ButtonStyle(
|
||
bgcolor=ft.Colors.BLUE_600,
|
||
padding=ft.padding.symmetric(horizontal=15, vertical=10),
|
||
),
|
||
on_click=lambda _: self.generate_pdf_from_edit(),
|
||
width=120,
|
||
height=40,
|
||
) if not is_locked else ft.Container(),
|
||
]),
|
||
padding=ft.padding.symmetric(horizontal=15, vertical=10),
|
||
bgcolor=ft.Colors.GREY_50,
|
||
),
|
||
]),
|
||
expand=True,
|
||
)
|
||
def generate_pdf_from_edit(self):
|
||
"""編集画面からPDFを生成"""
|
||
if not self.editing_invoice:
|
||
return
|
||
|
||
try:
|
||
pdf_path = self.app_service.invoice.regenerate_pdf(self.editing_invoice.uuid)
|
||
if pdf_path:
|
||
self.editing_invoice.file_path = pdf_path
|
||
self.editing_invoice.pdf_generated_at = datetime.now().replace(microsecond=0).isoformat()
|
||
logging.info(f"PDF生成完了: {pdf_path}")
|
||
# TODO: 成功メッセージ表示
|
||
else:
|
||
logging.error("PDF生成失敗")
|
||
# TODO: エラーメッセージ表示
|
||
except Exception as e:
|
||
logging.error(f"PDF生成エラー: {e}")
|
||
# TODO: エラーメッセージ表示
|
||
|
||
def _add_item_row(self):
|
||
"""明細行を追加"""
|
||
if not self.editing_invoice:
|
||
return
|
||
|
||
# 新しい明細行を追加
|
||
new_item = InvoiceItem(
|
||
description="新しい商品",
|
||
quantity=1,
|
||
unit_price=0,
|
||
is_discount=False
|
||
)
|
||
self.editing_invoice.items.append(new_item)
|
||
self.update_main_content()
|
||
|
||
def _delete_item_row(self, index: int):
|
||
"""明細行を削除"""
|
||
if not self.editing_invoice or index >= len(self.editing_invoice.items):
|
||
return
|
||
|
||
# 行を削除(最低1行は残す)
|
||
if len(self.editing_invoice.items) > 1:
|
||
del self.editing_invoice.items[index]
|
||
self.update_main_content()
|
||
|
||
def _update_item_field(self, item_index: int, field_name: str, value: str):
|
||
"""明細フィールドを更新"""
|
||
if not self.editing_invoice or item_index >= len(self.editing_invoice.items):
|
||
return
|
||
|
||
item = self.editing_invoice.items[item_index]
|
||
|
||
# デバッグ用:更新前の値をログ出力
|
||
old_value = getattr(item, field_name)
|
||
logging.debug(f"Updating item {item_index} {field_name}: '{old_value}' -> '{value}'")
|
||
|
||
if field_name == 'description':
|
||
item.description = value
|
||
elif field_name == 'quantity':
|
||
try:
|
||
# 空文字の場合は1を設定
|
||
if not value or value.strip() == '':
|
||
item.quantity = 1
|
||
else:
|
||
item.quantity = int(value)
|
||
logging.debug(f"Quantity updated to: {item.quantity}")
|
||
except ValueError as e:
|
||
item.quantity = 1
|
||
logging.error(f"Quantity update error: {e}")
|
||
elif field_name == 'unit_price':
|
||
try:
|
||
# 空文字の場合は0を設定
|
||
if not value or value.strip() == '':
|
||
item.unit_price = 0
|
||
else:
|
||
item.unit_price = int(value)
|
||
logging.debug(f"Unit price updated to: {item.unit_price}")
|
||
except ValueError as e:
|
||
item.unit_price = 0
|
||
logging.error(f"Unit price update error: {e}")
|
||
|
||
# 合計金額を更新するために画面を更新
|
||
self.update_main_content()
|
||
|
||
def _create_items_table(self, items: List[InvoiceItem], is_locked: bool) -> ft.Column:
|
||
"""明細テーブルを作成(編集モードと表示モードで別の見せ方)"""
|
||
# ビューモード状態を取得
|
||
is_view_mode = not getattr(self, 'is_detail_edit_mode', False)
|
||
|
||
# デバッグ用:メソッド開始時の状態をログ出力
|
||
logging.debug(f"Creating items table: locked={is_locked}, view_mode={is_view_mode}, edit_mode={getattr(self, 'is_detail_edit_mode', False)}")
|
||
logging.debug(f"Items count: {len(items)}")
|
||
|
||
if is_view_mode or is_locked:
|
||
# 表示モード:表形式で整然と表示
|
||
return self._create_view_mode_table(items)
|
||
else:
|
||
# 編集モード:入力フォーム風に表示
|
||
return self._create_edit_mode_table(items, is_locked)
|
||
|
||
def _create_view_mode_table(self, items: List[InvoiceItem]) -> ft.Column:
|
||
"""表示モード:フレキシブルな表形式で整然と表示"""
|
||
# ヘッダー行
|
||
header_row = ft.Row([
|
||
ft.Text("商品名", size=12, weight=ft.FontWeight.BOLD, expand=True), # 可変幅
|
||
ft.Text("数", size=12, weight=ft.FontWeight.BOLD, width=35), # 固定幅
|
||
ft.Text("単価", size=12, weight=ft.FontWeight.BOLD, width=70), # 固定幅
|
||
ft.Text("小計", size=12, weight=ft.FontWeight.BOLD, width=70), # 固定幅
|
||
ft.Container(width=35), # 削除ボタン用スペースを確保
|
||
])
|
||
|
||
# データ行
|
||
data_rows = []
|
||
for i, item in enumerate(items):
|
||
row = ft.Row([
|
||
ft.Text(item.description, size=12, expand=True), # 可変幅
|
||
ft.Text(str(item.quantity), size=12, width=35, text_align=ft.TextAlign.RIGHT), # 固定幅
|
||
ft.Text(f"¥{item.unit_price:,}", size=12, width=70, text_align=ft.TextAlign.RIGHT), # 固定幅
|
||
ft.Text(f"¥{item.subtotal:,}", size=12, weight=ft.FontWeight.BOLD, width=70, text_align=ft.TextAlign.RIGHT), # 固定幅
|
||
ft.Container(width=35), # 削除ボタン用スペース
|
||
])
|
||
data_rows.append(row)
|
||
|
||
return ft.Column([
|
||
header_row,
|
||
ft.Divider(height=1, color=ft.Colors.GREY_400),
|
||
ft.Column(data_rows, scroll=ft.ScrollMode.AUTO, height=250), # 高さを制限
|
||
])
|
||
|
||
def _create_edit_mode_table(self, items: List[InvoiceItem], is_locked: bool) -> ft.Column:
|
||
"""編集モード:フレキシブルな表形式"""
|
||
# 編集モードに入ったら自動で空行を追加
|
||
if not any(item.description == "" and item.quantity == 0 and item.unit_price == 0 for item in items):
|
||
items.append(InvoiceItem(description="", quantity=0, unit_price=0))
|
||
|
||
# ヘッダー行
|
||
header_row = ft.Row([
|
||
ft.Text("商品名", size=12, weight=ft.FontWeight.BOLD, expand=True), # 可変幅
|
||
ft.Text("数", size=12, weight=ft.FontWeight.BOLD, width=35), # 固定幅
|
||
ft.Text("単価", size=12, weight=ft.FontWeight.BOLD, width=70), # 固定幅
|
||
ft.Text("小計", size=12, weight=ft.FontWeight.BOLD, width=70), # 固定幅
|
||
ft.Container(width=35), # 削除ボタン用スペースを確保
|
||
])
|
||
|
||
# データ行
|
||
data_rows = []
|
||
for i, item in enumerate(items):
|
||
# 商品名フィールド
|
||
product_field = ft.TextField(
|
||
value=item.description,
|
||
text_size=12,
|
||
height=28,
|
||
width=None, # 幅を可変に
|
||
expand=True, # 可変幅
|
||
border=ft.border.all(1, ft.Colors.BLUE_200),
|
||
bgcolor=ft.Colors.WHITE,
|
||
content_padding=ft.padding.all(5), # 内部余白を最小化
|
||
on_change=lambda e: self._update_item_field(i, 'description', e.control.value),
|
||
)
|
||
|
||
# 数量フィールド
|
||
quantity_field = ft.TextField(
|
||
value=str(item.quantity),
|
||
text_size=12,
|
||
height=28,
|
||
width=35, # 固定幅
|
||
text_align=ft.TextAlign.RIGHT,
|
||
border=ft.border.all(1, ft.Colors.BLUE_200),
|
||
bgcolor=ft.Colors.WHITE,
|
||
content_padding=ft.padding.all(5), # 内部余白を最小化
|
||
on_change=lambda e: self._update_item_field(i, 'quantity', e.control.value),
|
||
keyboard_type=ft.KeyboardType.NUMBER,
|
||
)
|
||
|
||
# 単価フィールド
|
||
unit_price_field = ft.TextField(
|
||
value=f"{item.unit_price:,}",
|
||
text_size=12,
|
||
height=28,
|
||
width=70, # 固定幅
|
||
text_align=ft.TextAlign.RIGHT,
|
||
border=ft.border.all(1, ft.Colors.BLUE_200),
|
||
bgcolor=ft.Colors.WHITE,
|
||
content_padding=ft.padding.all(5), # 内部余白を最小化
|
||
on_change=lambda e: self._update_item_field(i, 'unit_price', e.control.value.replace(',', '')),
|
||
keyboard_type=ft.KeyboardType.NUMBER,
|
||
)
|
||
|
||
# 削除ボタン
|
||
delete_button = ft.IconButton(
|
||
ft.Icons.DELETE_OUTLINE,
|
||
tooltip="行を削除",
|
||
icon_color=ft.Colors.RED_600,
|
||
disabled=is_locked,
|
||
icon_size=16,
|
||
on_click=lambda _, idx=i: self._delete_item_row(idx),
|
||
)
|
||
|
||
# データ行
|
||
row = ft.Row([
|
||
product_field,
|
||
quantity_field,
|
||
unit_price_field,
|
||
ft.Text(f"¥{item.subtotal:,}", size=12, weight=ft.FontWeight.BOLD, width=70, text_align=ft.TextAlign.RIGHT), # 固定幅
|
||
delete_button,
|
||
])
|
||
data_rows.append(row)
|
||
|
||
return ft.Column([
|
||
header_row,
|
||
ft.Divider(height=1, color=ft.Colors.GREY_400),
|
||
ft.Column(data_rows, scroll=ft.ScrollMode.AUTO, height=250), # 高さを制限
|
||
])
|
||
def create_new_customer_screen(self) -> ft.Container:
|
||
"""新規顧客登録画面"""
|
||
name_field = ft.TextField(label="顧客名(略称)")
|
||
formal_name_field = ft.TextField(label="正式名称")
|
||
address_field = ft.TextField(label="住所")
|
||
phone_field = ft.TextField(label="電話番号")
|
||
|
||
def save_customer(_):
|
||
name = (name_field.value or "").strip()
|
||
formal_name = (formal_name_field.value or "").strip()
|
||
address = (address_field.value or "").strip()
|
||
phone = (phone_field.value or "").strip()
|
||
if not name or not formal_name:
|
||
# TODO: エラー表示
|
||
return
|
||
new_customer = self.app_service.customer.create_customer(name, formal_name, address, phone)
|
||
if new_customer:
|
||
self.customers = self.app_service.customer.get_all_customers()
|
||
self.selected_customer = new_customer
|
||
logging.info(f"新規顧客登録: {new_customer.formal_name}")
|
||
self.is_customer_picker_open = False
|
||
self.is_new_customer_form_open = False
|
||
self.update_main_content()
|
||
else:
|
||
logging.error("新規顧客登録失敗")
|
||
|
||
def cancel(_):
|
||
self.is_new_customer_form_open = False
|
||
self.update_main_content()
|
||
|
||
return ft.Container(
|
||
content=ft.Column([
|
||
ft.Container(
|
||
content=ft.Row([
|
||
ft.IconButton(ft.Icons.ARROW_BACK, on_click=cancel),
|
||
ft.Text("新規顧客登録", size=18, weight=ft.FontWeight.BOLD),
|
||
]),
|
||
padding=ft.padding.all(15),
|
||
bgcolor=ft.Colors.BLUE_GREY,
|
||
),
|
||
ft.Container(
|
||
content=ft.Column([
|
||
ft.Text("顧客情報を入力", size=16, weight=ft.FontWeight.BOLD),
|
||
ft.Container(height=10),
|
||
name_field,
|
||
ft.Container(height=10),
|
||
formal_name_field,
|
||
ft.Container(height=10),
|
||
address_field,
|
||
ft.Container(height=10),
|
||
phone_field,
|
||
ft.Container(height=20),
|
||
ft.Row([
|
||
ft.Button("保存", on_click=save_customer, bgcolor=ft.Colors.BLUE_GREY_800, color=ft.Colors.WHITE),
|
||
ft.Button("キャンセル", on_click=cancel),
|
||
], spacing=10),
|
||
]),
|
||
padding=ft.padding.all(20),
|
||
expand=True,
|
||
),
|
||
]),
|
||
expand=True,
|
||
)
|
||
|
||
def open_customer_picker(self, e=None):
|
||
"""顧客選択を開く(画面内遷移)"""
|
||
logging.info("顧客選択画面へ遷移")
|
||
self.is_customer_picker_open = True
|
||
self.update_main_content()
|
||
|
||
def on_customer_selected(self, customer: Customer):
|
||
"""顧客選択時の処理"""
|
||
self.selected_customer = customer
|
||
logging.info(f"顧客を選択: {customer.formal_name}")
|
||
# UIを更新して選択された顧客を表示
|
||
self.update_main_content()
|
||
|
||
def submit_invoice_for_tax(self, invoice_uuid: str) -> bool:
|
||
"""税務署提出済みフラグを設定"""
|
||
success = self.app_service.invoice.submit_to_tax_authority(invoice_uuid)
|
||
if success:
|
||
self.invoices = self.app_service.invoice.get_recent_invoices(20)
|
||
self.update_main_content()
|
||
logging.info(f"税務署提出済み: {invoice_uuid}")
|
||
else:
|
||
logging.error(f"税務署提出失敗: {invoice_uuid}")
|
||
return success
|
||
def on_customer_deleted(self, customer: Customer):
|
||
"""顧客削除時の処理"""
|
||
self.app_service.customer.delete_customer(customer.id)
|
||
self.customers = self.app_service.customer.get_all_customers()
|
||
logging.info(f"顧客を削除: {customer.formal_name}")
|
||
# モーダルを再表示してリストを更新
|
||
if self.customer_picker and self.customer_picker.is_open:
|
||
self.customer_picker.update_customer_list(self.customers)
|
||
|
||
def on_amount_change(self, e):
|
||
"""金額変更時の処理"""
|
||
self.amount_value = e.control.value
|
||
logging.info(f"金額を変更: {self.amount_value}")
|
||
|
||
def on_document_type_change(self, index):
|
||
"""帳票種類変更"""
|
||
document_types = list(DocumentType)
|
||
selected_type = document_types[index]
|
||
logging.info(f"帳票種類を変更: {selected_type.value}")
|
||
# TODO: 選択された種類を保存
|
||
|
||
def select_document_type(self, doc_type: str):
|
||
"""帳票種類選択"""
|
||
# DocumentTypeから対応するenumを見つける
|
||
for dt in DocumentType:
|
||
if dt.value == doc_type:
|
||
self.selected_document_type = dt
|
||
logging.info(f"帳票種類を選択: {doc_type}")
|
||
self.update_main_content()
|
||
break
|
||
|
||
def create_slip(self, e=None):
|
||
"""伝票作成 - サービス層を使用"""
|
||
if not self.selected_customer:
|
||
logging.warning("顧客が選択されていません")
|
||
return
|
||
|
||
try:
|
||
amount = int(self.amount_value) if self.amount_value else 250000
|
||
except ValueError:
|
||
amount = 250000
|
||
|
||
logging.info(f"伝票を作成: {self.selected_document_type.value}, {self.selected_customer.formal_name}, ¥{amount:,}")
|
||
|
||
# サービス層経由で伝票作成
|
||
invoice = self.app_service.invoice.create_invoice(
|
||
customer=self.selected_customer,
|
||
document_type=self.selected_document_type,
|
||
amount=amount,
|
||
notes=""
|
||
)
|
||
|
||
if invoice:
|
||
if invoice.file_path:
|
||
self.app_service.invoice.delete_pdf_file(invoice.file_path)
|
||
invoice.file_path = None
|
||
logging.info(f"伝票作成成功: {invoice.invoice_number}")
|
||
# リストを更新
|
||
self.invoices = self.app_service.invoice.get_recent_invoices(20)
|
||
# 発行履歴タブに切り替え
|
||
self.on_tab_change(1)
|
||
else:
|
||
logging.error("伝票作成失敗")
|
||
|
||
def load_slips(self) -> List[Invoice]:
|
||
"""伝票データ読み込み - サービス層経由"""
|
||
return self.app_service.invoice.get_recent_invoices(20)
|
||
|
||
def main(page: ft.Page):
|
||
"""メイン関数"""
|
||
app = FlutterStyleDashboard(page)
|
||
page.update()
|
||
|
||
if __name__ == "__main__":
|
||
ft.app(target=main)
|