h-1.flet.3/components/editor_framework.py
2026-02-24 10:10:16 +09:00

247 lines
8.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""編集系画面で再利用するEditorフレームワーク最小版"""
from dataclasses import dataclass
from typing import Callable, List, Optional
import flet as ft
from models.invoice_models import InvoiceItem, Product
@dataclass
class ValidationResult:
"""編集内容の検証結果。"""
ok: bool
errors: List[str]
def normalize_invoice_items(items: List[InvoiceItem]) -> List[InvoiceItem]:
"""保存前に明細を正規化(空行除去・数値補正)。"""
normalized: List[InvoiceItem] = []
for item in items:
description = (item.description or "").strip()
quantity = int(item.quantity or 0)
unit_price = int(item.unit_price or 0)
if not description and quantity == 0 and unit_price == 0:
continue
if quantity <= 0:
quantity = 1
normalized.append(
InvoiceItem(
description=description,
quantity=quantity,
unit_price=unit_price,
is_discount=bool(getattr(item, "is_discount", False)),
product_id=getattr(item, "product_id", None),
)
)
return normalized
def validate_invoice_items(items: List[InvoiceItem]) -> ValidationResult:
"""明細の最小バリデーション。"""
errors: List[str] = []
if not items:
errors.append("明細を1行以上入力してください")
return ValidationResult(ok=False, errors=errors)
for idx, item in enumerate(items, start=1):
if not (item.description or "").strip():
errors.append(f"{idx}行目: 商品名を入力してください")
if int(item.quantity or 0) <= 0:
errors.append(f"{idx}行目: 数量は1以上で入力してください")
if int(item.unit_price or 0) < 0:
errors.append(f"{idx}行目: 単価は0以上で入力してください")
return ValidationResult(ok=len(errors) == 0, errors=errors)
def build_invoice_items_view_table(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 item in items:
data_rows.append(
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),
]
)
)
list_control = ft.Column(data_rows, spacing=4)
return ft.Column(
[
header_row,
ft.Divider(height=1, color=ft.Colors.GREY_400),
ft.Container(content=list_control, height=250, padding=0),
]
)
def build_invoice_items_edit_table(
items: List[InvoiceItem],
is_locked: bool,
on_update_field: Callable[[int, str, str], None],
on_delete_row: Callable[[int], None],
products: List[Product],
on_product_select: Callable[[int], None] | None = None,
row_refs: Optional[dict] = None,
enable_reorder: bool = False,
on_reorder: Optional[Callable[[int, int], None]] = None,
) -> ft.Column:
"""編集モードの明細テーブル。"""
header_row = ft.Row(
[
ft.Container(width=32),
ft.Text("商品", size=12, weight=ft.FontWeight.BOLD, width=180),
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: List[ft.Control] = []
for i, item in enumerate(items):
product_label = item.description if item.description else "商品選択"
product_field = ft.Button(
content=ft.Text(product_label, no_wrap=True),
width=180,
height=36,
on_click=(lambda _, idx=i: on_product_select(idx)) if on_product_select else None,
disabled=is_locked,
style=ft.ButtonStyle(
padding=ft.Padding.symmetric(horizontal=10, vertical=6),
bgcolor=ft.Colors.BLUE_GREY_100,
shape=ft.RoundedRectangleBorder(radius=4),
),
)
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, idx=i: on_update_field(idx, "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, idx=i: on_update_field(idx, "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: on_delete_row(idx),
)
subtotal_text = ft.Text(
f"¥{item.subtotal:,}",
size=12,
weight=ft.FontWeight.BOLD,
width=70,
text_align=ft.TextAlign.RIGHT,
)
if row_refs is not None:
row_refs[i] = {
"product": product_field,
"quantity": quantity_field,
"unit_price": unit_price_field,
"subtotal": subtotal_text,
}
if enable_reorder and on_reorder and not is_locked:
up_button = ft.IconButton(
ft.Icons.ARROW_UPWARD,
icon_size=16,
tooltip="上へ",
disabled=i == 0,
on_click=(lambda _, idx=i: on_reorder(idx, idx - 1)) if i > 0 else None,
)
down_button = ft.IconButton(
ft.Icons.ARROW_DOWNWARD,
icon_size=16,
tooltip="下へ",
disabled=i == len(items) - 1,
on_click=(lambda _, idx=i: on_reorder(idx, idx + 1)) if i < len(items) - 1 else None,
)
reorder_buttons = ft.Column([up_button, down_button], spacing=0, width=32)
delete_slot = ft.Container(width=0)
else:
reorder_buttons = ft.Container(width=0)
delete_slot = delete_button
row_control = ft.Row(
[
reorder_buttons,
product_field,
quantity_field,
unit_price_field,
subtotal_text,
delete_slot,
],
vertical_alignment=ft.CrossAxisAlignment.CENTER,
key=f"row-{i}-{item.description}",
)
data_rows.append(row_control)
list_control = ft.Column(data_rows, spacing=4)
return ft.Column(
[
header_row,
ft.Divider(height=1, color=ft.Colors.GREY_400),
ft.Container(
content=list_control,
height=250,
padding=0,
bgcolor=None,
border_radius=0,
expand=False,
),
]
)