編集画面の充実
This commit is contained in:
parent
be14fa7eb2
commit
25032d3b9b
3 changed files with 313 additions and 108 deletions
|
|
@ -110,6 +110,7 @@ def build_invoice_items_edit_table(
|
||||||
on_delete_row: Callable[[int], None],
|
on_delete_row: Callable[[int], None],
|
||||||
products: List[Product],
|
products: List[Product],
|
||||||
on_product_select: Callable[[int], None] | None = None,
|
on_product_select: Callable[[int], None] | None = None,
|
||||||
|
row_refs: Optional[dict] = None,
|
||||||
) -> ft.Column:
|
) -> ft.Column:
|
||||||
"""編集モードの明細テーブル。"""
|
"""編集モードの明細テーブル。"""
|
||||||
header_row = ft.Row(
|
header_row = ft.Row(
|
||||||
|
|
@ -173,19 +174,29 @@ def build_invoice_items_edit_table(
|
||||||
on_click=lambda _, idx=i: on_delete_row(idx),
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
data_rows.append(
|
data_rows.append(
|
||||||
ft.Row(
|
ft.Row(
|
||||||
[
|
[
|
||||||
product_field,
|
product_field,
|
||||||
quantity_field,
|
quantity_field,
|
||||||
unit_price_field,
|
unit_price_field,
|
||||||
ft.Text(
|
subtotal_text,
|
||||||
f"¥{item.subtotal:,}",
|
|
||||||
size=12,
|
|
||||||
weight=ft.FontWeight.BOLD,
|
|
||||||
width=70,
|
|
||||||
text_align=ft.TextAlign.RIGHT,
|
|
||||||
),
|
|
||||||
delete_button,
|
delete_button,
|
||||||
],
|
],
|
||||||
key=f"row-{i}-{item.description}",
|
key=f"row-{i}-{item.description}",
|
||||||
|
|
|
||||||
390
main.py
390
main.py
|
|
@ -247,11 +247,13 @@ class FlutterStyleDashboard:
|
||||||
DocumentType.SALES.value: "#42A5F5",
|
DocumentType.SALES.value: "#42A5F5",
|
||||||
DocumentType.DRAFT.value: "#90A4AE",
|
DocumentType.DRAFT.value: "#90A4AE",
|
||||||
}
|
}
|
||||||
|
|
||||||
# ビジネスロジックサービス
|
# ビジネスロジックサービス
|
||||||
self.app_service = AppService()
|
self.app_service = AppService()
|
||||||
self.invoices = []
|
self.invoices = []
|
||||||
self.customers = []
|
self.customers = []
|
||||||
|
self._item_row_refs: Dict[int, Dict[str, ft.Control]] = {}
|
||||||
|
self._total_amount_text: Optional[ft.Text] = None
|
||||||
|
|
||||||
self.setup_page()
|
self.setup_page()
|
||||||
self.setup_database()
|
self.setup_database()
|
||||||
|
|
@ -313,7 +315,8 @@ class FlutterStyleDashboard:
|
||||||
document_type=invoice.document_type,
|
document_type=invoice.document_type,
|
||||||
amount=invoice.total_amount,
|
amount=invoice.total_amount,
|
||||||
notes=invoice.notes,
|
notes=invoice.notes,
|
||||||
items=invoice.items
|
items=invoice.items,
|
||||||
|
is_draft=invoice.is_draft,
|
||||||
)
|
)
|
||||||
|
|
||||||
logging.info(f"サンプルデータ作成完了: {len(sample_invoices)}件")
|
logging.info(f"サンプルデータ作成完了: {len(sample_invoices)}件")
|
||||||
|
|
@ -509,6 +512,14 @@ class FlutterStyleDashboard:
|
||||||
visible=is_draft,
|
visible=is_draft,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
delete_button = self._build_delete_draft_button(is_view_mode)
|
||||||
|
|
||||||
|
trailing_controls: List[ft.Control] = []
|
||||||
|
if delete_button:
|
||||||
|
trailing_controls.append(delete_button)
|
||||||
|
if is_draft:
|
||||||
|
trailing_controls.append(draft_badge)
|
||||||
|
|
||||||
app_bar = AppBar(
|
app_bar = AppBar(
|
||||||
title="伝票詳細",
|
title="伝票詳細",
|
||||||
show_back=True,
|
show_back=True,
|
||||||
|
|
@ -518,7 +529,7 @@ class FlutterStyleDashboard:
|
||||||
action_icon=ft.Icons.SAVE if getattr(self, 'is_detail_edit_mode', False) else ft.Icons.EDIT,
|
action_icon=ft.Icons.SAVE if getattr(self, 'is_detail_edit_mode', False) else ft.Icons.EDIT,
|
||||||
action_tooltip="保存" if getattr(self, 'is_detail_edit_mode', False) else "編集",
|
action_tooltip="保存" if getattr(self, 'is_detail_edit_mode', False) else "編集",
|
||||||
bottom=None,
|
bottom=None,
|
||||||
trailing_controls=([draft_badge] + ([doc_type_menu] if doc_type_menu else [])) if is_draft else ([doc_type_menu] if doc_type_menu else []),
|
trailing_controls=trailing_controls,
|
||||||
title_control=title_ctrl,
|
title_control=title_ctrl,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -864,7 +875,7 @@ class FlutterStyleDashboard:
|
||||||
logging.warning(f"差分判定失敗: {e}")
|
logging.warning(f"差分判定失敗: {e}")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def create_slip_card(self, slip) -> ft.Container:
|
def create_slip_card(self, slip, interactive: bool = True) -> ft.Control:
|
||||||
"""伝票カード作成(コンパクト表示)"""
|
"""伝票カード作成(コンパクト表示)"""
|
||||||
theme = self.invoice_card_theme
|
theme = self.invoice_card_theme
|
||||||
palette = self.doc_type_palette
|
palette = self.doc_type_palette
|
||||||
|
|
@ -928,7 +939,24 @@ class FlutterStyleDashboard:
|
||||||
bgcolor=theme["tag_bg"],
|
bgcolor=theme["tag_bg"],
|
||||||
border_radius=10,
|
border_radius=10,
|
||||||
),
|
),
|
||||||
ft.Text(f"No: {invoice_number}", size=10, color=theme["subtitle_color"]),
|
ft.Row(
|
||||||
|
[
|
||||||
|
ft.Text(f"No: {invoice_number}", size=10, color=theme["subtitle_color"]),
|
||||||
|
ft.Container(
|
||||||
|
content=ft.Text(
|
||||||
|
"下書き",
|
||||||
|
size=9,
|
||||||
|
weight=ft.FontWeight.BOLD,
|
||||||
|
color=theme["tag_text_color"],
|
||||||
|
),
|
||||||
|
padding=ft.Padding.symmetric(horizontal=8, vertical=2),
|
||||||
|
bgcolor=theme["tag_bg"],
|
||||||
|
border_radius=10,
|
||||||
|
visible=is_draft_card,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
spacing=6,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
spacing=6,
|
spacing=6,
|
||||||
),
|
),
|
||||||
|
|
@ -994,21 +1022,6 @@ class FlutterStyleDashboard:
|
||||||
|
|
||||||
status_chip = ft.Text("✓ LOCK", size=9, color=theme["tag_text_color"]) if final_locked else ft.Container()
|
status_chip = ft.Text("✓ LOCK", size=9, color=theme["tag_text_color"]) if final_locked else ft.Container()
|
||||||
|
|
||||||
if is_draft_card:
|
|
||||||
status_chip = ft.Row(
|
|
||||||
[
|
|
||||||
ft.Container(
|
|
||||||
content=ft.Text("DRAFT", size=9, weight=ft.FontWeight.BOLD, color=ft.Colors.WHITE),
|
|
||||||
padding=ft.Padding.symmetric(horizontal=6, vertical=2),
|
|
||||||
bgcolor=theme["draft_badge_bg"],
|
|
||||||
border_radius=999,
|
|
||||||
),
|
|
||||||
status_chip or ft.Container(),
|
|
||||||
],
|
|
||||||
spacing=4,
|
|
||||||
vertical_alignment=ft.CrossAxisAlignment.CENTER,
|
|
||||||
)
|
|
||||||
|
|
||||||
card_bg = theme["draft_card_bg"] if is_draft_card else theme["card_bg"]
|
card_bg = theme["draft_card_bg"] if is_draft_card else theme["card_bg"]
|
||||||
card_border = ft.Border.all(1, theme["draft_border"]) if is_draft_card else None
|
card_border = ft.Border.all(1, theme["draft_border"]) if is_draft_card else None
|
||||||
card_shadow = [
|
card_shadow = [
|
||||||
|
|
@ -1029,6 +1042,9 @@ class FlutterStyleDashboard:
|
||||||
shadow=card_shadow,
|
shadow=card_shadow,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if not interactive:
|
||||||
|
return ft.Container(content=card_body)
|
||||||
|
|
||||||
return ft.GestureDetector(
|
return ft.GestureDetector(
|
||||||
content=card_body,
|
content=card_body,
|
||||||
on_tap=on_single_tap,
|
on_tap=on_single_tap,
|
||||||
|
|
@ -1242,6 +1258,7 @@ class FlutterStyleDashboard:
|
||||||
# 編集不可チェック(新規作成時はFalse)
|
# 編集不可チェック(新規作成時はFalse)
|
||||||
is_new_invoice = self.editing_invoice.invoice_number.startswith("NEW-")
|
is_new_invoice = self.editing_invoice.invoice_number.startswith("NEW-")
|
||||||
edit_bg = ft.Colors.BROWN_50
|
edit_bg = ft.Colors.BROWN_50
|
||||||
|
is_draft = bool(getattr(self.editing_invoice, "is_draft", False))
|
||||||
|
|
||||||
# LOCK条件:明示的に確定された場合のみLOCK
|
# LOCK条件:明示的に確定された場合のみLOCK
|
||||||
# PDF生成だけではLOCKしない(お試しPDFを許可)
|
# PDF生成だけではLOCKしない(お試しPDFを許可)
|
||||||
|
|
@ -1273,39 +1290,36 @@ class FlutterStyleDashboard:
|
||||||
"""顧客選択画面を開く"""
|
"""顧客選択画面を開く"""
|
||||||
self.open_customer_picker()
|
self.open_customer_picker()
|
||||||
|
|
||||||
customer_field = None
|
def set_doc_type(dt: DocumentType):
|
||||||
if (not is_view_mode and not is_locked) or is_new_invoice:
|
if self.is_detail_edit_mode and not is_locked:
|
||||||
customer_field = ft.TextField(
|
self.select_document_type(dt)
|
||||||
label="顧客名",
|
|
||||||
value=self.editing_invoice.customer.name if self.editing_invoice.customer.name != "選択してください" else "",
|
doc_type_items = [
|
||||||
disabled=is_locked,
|
ft.PopupMenuItem(
|
||||||
width=260,
|
content=ft.Text(dt.value),
|
||||||
bgcolor=edit_bg,
|
on_click=lambda _, d=dt: set_doc_type(d)
|
||||||
)
|
)
|
||||||
|
for dt in DocumentType
|
||||||
|
if dt != DocumentType.DRAFT
|
||||||
|
]
|
||||||
|
|
||||||
def update_customer_name(e):
|
def toggle_draft(e):
|
||||||
"""顧客名を更新"""
|
self.editing_invoice.is_draft = bool(e.control.value)
|
||||||
if self.editing_invoice:
|
self.update_main_content()
|
||||||
customer_name = e.control.value or ""
|
|
||||||
found_customer = None
|
|
||||||
for customer in self.app_service.customer.get_all_customers():
|
|
||||||
if customer.name == customer_name or customer.formal_name == customer_name:
|
|
||||||
found_customer = customer
|
|
||||||
break
|
|
||||||
|
|
||||||
if found_customer:
|
if self.is_detail_edit_mode and not is_locked:
|
||||||
self.editing_invoice.customer = found_customer
|
doc_type_menu = ft.PopupMenuButton(
|
||||||
else:
|
content=ft.Text(
|
||||||
from models.invoice_models import Customer
|
self.selected_document_type.value,
|
||||||
self.editing_invoice.customer = Customer(
|
size=12,
|
||||||
id=0,
|
weight=ft.FontWeight.BOLD,
|
||||||
name=customer_name,
|
color=ft.Colors.WHITE,
|
||||||
formal_name=customer_name,
|
),
|
||||||
address="",
|
items=doc_type_items,
|
||||||
phone=""
|
tooltip="帳票タイプ変更",
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
customer_field.on_change = update_customer_name
|
doc_type_menu = None
|
||||||
|
|
||||||
# 日付・時間ピッカー(テキスト入力NGなのでボタン+ポップアップ)
|
# 日付・時間ピッカー(テキスト入力NGなのでボタン+ポップアップ)
|
||||||
date_button = None
|
date_button = None
|
||||||
|
|
@ -1425,6 +1439,8 @@ class FlutterStyleDashboard:
|
||||||
if not is_new_invoice and self._invoice_snapshot:
|
if not is_new_invoice and self._invoice_snapshot:
|
||||||
if not self._is_invoice_changed(self.editing_invoice, self._invoice_snapshot):
|
if not self._is_invoice_changed(self.editing_invoice, self._invoice_snapshot):
|
||||||
self._show_snack("変更はありませんでした", ft.Colors.BLUE_GREY_600)
|
self._show_snack("変更はありませんでした", ft.Colors.BLUE_GREY_600)
|
||||||
|
self.is_detail_edit_mode = False
|
||||||
|
self.update_main_content()
|
||||||
return
|
return
|
||||||
|
|
||||||
# UIで更新された明細を保存前に正規化して確定
|
# UIで更新された明細を保存前に正規化して確定
|
||||||
|
|
@ -1477,7 +1493,8 @@ class FlutterStyleDashboard:
|
||||||
document_type=self.editing_invoice.document_type,
|
document_type=self.editing_invoice.document_type,
|
||||||
amount=amount,
|
amount=amount,
|
||||||
notes=getattr(self.editing_invoice, 'notes', ''),
|
notes=getattr(self.editing_invoice, 'notes', ''),
|
||||||
items=self.editing_invoice.items # UIの明細を渡す
|
items=self.editing_invoice.items,
|
||||||
|
is_draft=bool(getattr(self.editing_invoice, 'is_draft', False)),
|
||||||
)
|
)
|
||||||
logging.info(f"create_invoice戻り値: {success}")
|
logging.info(f"create_invoice戻り値: {success}")
|
||||||
if success:
|
if success:
|
||||||
|
|
@ -1581,62 +1598,126 @@ class FlutterStyleDashboard:
|
||||||
run_spacing=4,
|
run_spacing=4,
|
||||||
) if summary_tags else ft.Container(height=0)
|
) if summary_tags else ft.Container(height=0)
|
||||||
|
|
||||||
|
customer_label = self.editing_invoice.customer.formal_name
|
||||||
|
if not customer_label or customer_label == "選択してください":
|
||||||
|
customer_label = "顧客を選択"
|
||||||
|
|
||||||
|
if (not is_view_mode and not is_locked) or is_new_invoice:
|
||||||
|
customer_control = ft.Button(
|
||||||
|
content=ft.Text(customer_label, no_wrap=True),
|
||||||
|
width=220,
|
||||||
|
height=36,
|
||||||
|
on_click=lambda _: select_customer(),
|
||||||
|
style=ft.ButtonStyle(
|
||||||
|
padding=ft.Padding.symmetric(horizontal=10, vertical=6),
|
||||||
|
bgcolor=ft.Colors.BLUE_GREY_100,
|
||||||
|
shape=ft.RoundedRectangleBorder(radius=6),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
customer_control = ft.Text(
|
||||||
|
customer_label,
|
||||||
|
size=13,
|
||||||
|
weight=ft.FontWeight.BOLD,
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.is_detail_edit_mode and not is_locked:
|
||||||
|
doc_type_control = ft.PopupMenuButton(
|
||||||
|
content=ft.Text(
|
||||||
|
self.selected_document_type.value,
|
||||||
|
size=12,
|
||||||
|
weight=ft.FontWeight.BOLD,
|
||||||
|
color=ft.Colors.WHITE,
|
||||||
|
),
|
||||||
|
items=doc_type_items,
|
||||||
|
tooltip="帳票タイプ変更",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
doc_type_control = ft.Text(
|
||||||
|
self.selected_document_type.value,
|
||||||
|
size=12,
|
||||||
|
weight=ft.FontWeight.BOLD,
|
||||||
|
color=ft.Colors.WHITE,
|
||||||
|
)
|
||||||
|
|
||||||
|
draft_control: ft.Control
|
||||||
|
if self.is_detail_edit_mode and not is_locked:
|
||||||
|
draft_control = ft.Switch(
|
||||||
|
label="下書き",
|
||||||
|
value=is_draft,
|
||||||
|
on_change=toggle_draft,
|
||||||
|
)
|
||||||
|
elif is_draft:
|
||||||
|
draft_control = ft.Container(
|
||||||
|
content=ft.Text("下書き", size=11, color=ft.Colors.BROWN_800),
|
||||||
|
padding=ft.Padding.symmetric(horizontal=8, vertical=4),
|
||||||
|
bgcolor=ft.Colors.BROWN_100,
|
||||||
|
border_radius=12,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
draft_control = ft.Container()
|
||||||
|
|
||||||
summary_card = ft.Container(
|
summary_card = ft.Container(
|
||||||
content=ft.Column(
|
content=ft.Column(
|
||||||
[
|
[
|
||||||
ft.Row(
|
ft.Row(
|
||||||
[
|
[
|
||||||
customer_field if customer_field else ft.Text(
|
ft.Row(
|
||||||
self.editing_invoice.customer.formal_name,
|
[
|
||||||
size=13,
|
ft.Container(
|
||||||
weight=ft.FontWeight.BOLD,
|
content=doc_type_control if self.is_detail_edit_mode and not is_locked else ft.Text(
|
||||||
|
self.selected_document_type.value,
|
||||||
|
size=9,
|
||||||
|
weight=ft.FontWeight.BOLD,
|
||||||
|
color=self.invoice_card_theme["tag_text_color"],
|
||||||
|
),
|
||||||
|
padding=ft.Padding.symmetric(horizontal=8, vertical=2),
|
||||||
|
bgcolor=self.invoice_card_theme["tag_bg"],
|
||||||
|
border_radius=10,
|
||||||
|
),
|
||||||
|
ft.Text(
|
||||||
|
f"No: {self.editing_invoice.invoice_number}",
|
||||||
|
size=10,
|
||||||
|
color=self.invoice_card_theme["subtitle_color"],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
spacing=6,
|
||||||
),
|
),
|
||||||
ft.Row([
|
draft_control,
|
||||||
ft.Button(
|
],
|
||||||
content=ft.Text("顧客選択", size=12),
|
alignment=ft.MainAxisAlignment.SPACE_BETWEEN,
|
||||||
on_click=lambda _: select_customer(),
|
vertical_alignment=ft.CrossAxisAlignment.CENTER,
|
||||||
disabled=is_locked or is_view_mode,
|
),
|
||||||
) if not is_view_mode and not is_locked else ft.Container(),
|
ft.Row(
|
||||||
ft.Text(
|
[
|
||||||
f"¥{self.editing_invoice.total_amount:,} (税込)",
|
customer_control,
|
||||||
size=15,
|
self._build_total_amount_text(),
|
||||||
weight=ft.FontWeight.BOLD,
|
|
||||||
color=ft.Colors.BLUE_700,
|
|
||||||
),
|
|
||||||
], spacing=8, alignment=ft.MainAxisAlignment.END),
|
|
||||||
],
|
],
|
||||||
alignment=ft.MainAxisAlignment.SPACE_BETWEEN,
|
alignment=ft.MainAxisAlignment.SPACE_BETWEEN,
|
||||||
),
|
),
|
||||||
ft.Row(
|
ft.Row(
|
||||||
[
|
[
|
||||||
ft.Text(
|
date_button if date_button else ft.Text(
|
||||||
self.editing_invoice.invoice_number,
|
self.editing_invoice.date.strftime("%Y/%m/%d"),
|
||||||
size=12,
|
size=12,
|
||||||
color=ft.Colors.BLUE_GREY_500,
|
color=ft.Colors.BLUE_GREY_600,
|
||||||
|
),
|
||||||
|
time_button if time_button else ft.Text(
|
||||||
|
self.editing_invoice.date.strftime("%H:%M"),
|
||||||
|
size=12,
|
||||||
|
color=ft.Colors.BLUE_GREY_600,
|
||||||
),
|
),
|
||||||
ft.Row([
|
|
||||||
date_button if date_button else ft.Text(
|
|
||||||
self.editing_invoice.date.strftime("%Y/%m/%d"),
|
|
||||||
size=12,
|
|
||||||
color=ft.Colors.BLUE_GREY_600,
|
|
||||||
),
|
|
||||||
ft.Text(" "),
|
|
||||||
time_button if time_button else ft.Text(
|
|
||||||
self.editing_invoice.date.strftime("%H:%M"),
|
|
||||||
size=12,
|
|
||||||
color=ft.Colors.BLUE_GREY_600,
|
|
||||||
),
|
|
||||||
], spacing=4, alignment=ft.MainAxisAlignment.END),
|
|
||||||
],
|
],
|
||||||
alignment=ft.MainAxisAlignment.SPACE_BETWEEN,
|
spacing=12,
|
||||||
),
|
),
|
||||||
summary_badges,
|
summary_badges,
|
||||||
],
|
],
|
||||||
spacing=6,
|
spacing=8,
|
||||||
),
|
),
|
||||||
padding=ft.Padding.all(14),
|
padding=ft.Padding.symmetric(horizontal=12, vertical=10),
|
||||||
bgcolor=ft.Colors.BLUE_GREY_50,
|
bgcolor=ft.Colors.BROWN_50 if is_draft else self.invoice_card_theme["card_bg"],
|
||||||
border_radius=10,
|
border_radius=self.invoice_card_theme["card_radius"],
|
||||||
|
shadow=[self.invoice_card_theme["shadow"]],
|
||||||
)
|
)
|
||||||
|
|
||||||
items_section = ft.Container(
|
items_section = ft.Container(
|
||||||
|
|
@ -1666,7 +1747,7 @@ class FlutterStyleDashboard:
|
||||||
spacing=6,
|
spacing=6,
|
||||||
),
|
),
|
||||||
padding=ft.Padding.all(12),
|
padding=ft.Padding.all(12),
|
||||||
bgcolor=ft.Colors.WHITE,
|
bgcolor=ft.Colors.BROWN_50 if is_draft else ft.Colors.WHITE,
|
||||||
border_radius=10,
|
border_radius=10,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -1703,7 +1784,6 @@ class FlutterStyleDashboard:
|
||||||
lower_scroll = ft.Container(
|
lower_scroll = ft.Container(
|
||||||
content=ft.Column(
|
content=ft.Column(
|
||||||
[
|
[
|
||||||
summary_card,
|
|
||||||
notes_section,
|
notes_section,
|
||||||
],
|
],
|
||||||
spacing=12,
|
spacing=12,
|
||||||
|
|
@ -1715,6 +1795,7 @@ class FlutterStyleDashboard:
|
||||||
|
|
||||||
top_stack = ft.Column(
|
top_stack = ft.Column(
|
||||||
[
|
[
|
||||||
|
summary_card,
|
||||||
items_section,
|
items_section,
|
||||||
],
|
],
|
||||||
spacing=12,
|
spacing=12,
|
||||||
|
|
@ -1778,16 +1859,16 @@ class FlutterStyleDashboard:
|
||||||
del self.editing_invoice.items[index]
|
del self.editing_invoice.items[index]
|
||||||
self.update_main_content()
|
self.update_main_content()
|
||||||
|
|
||||||
def _update_item_field(self, item_index: int, field_name: str, value: str):
|
def _update_item_field(self, index: int, field_name: str, value: str):
|
||||||
"""明細フィールドを更新"""
|
"""明細フィールドを更新"""
|
||||||
if not self.editing_invoice or item_index >= len(self.editing_invoice.items):
|
if not self.editing_invoice or index >= len(self.editing_invoice.items):
|
||||||
return
|
return
|
||||||
|
|
||||||
item = self.editing_invoice.items[item_index]
|
item = self.editing_invoice.items[index]
|
||||||
|
|
||||||
# デバッグ用:更新前の値をログ出力
|
# デバッグ用:更新前の値をログ出力
|
||||||
old_value = getattr(item, field_name)
|
old_value = getattr(item, field_name)
|
||||||
logging.debug(f"Updating item {item_index} {field_name}: '{old_value}' -> '{value}'")
|
logging.debug(f"Updating item {index} {field_name}: '{old_value}' -> '{value}'")
|
||||||
|
|
||||||
if field_name == 'description':
|
if field_name == 'description':
|
||||||
item.description = value
|
item.description = value
|
||||||
|
|
@ -1814,8 +1895,9 @@ class FlutterStyleDashboard:
|
||||||
item.unit_price = 0
|
item.unit_price = 0
|
||||||
logging.error(f"Unit price update error: {e}")
|
logging.error(f"Unit price update error: {e}")
|
||||||
|
|
||||||
# 入力途中で画面全体を再描画すると編集値が飛びやすいため、
|
self._refresh_item_row(index)
|
||||||
# ここではモデル更新のみに留める(再描画は保存/行追加/行削除時に実施)。
|
self._refresh_total_amount_display()
|
||||||
|
self.page.update()
|
||||||
|
|
||||||
def _select_product_for_row(self, item_index: int, product_id: Optional[int]):
|
def _select_product_for_row(self, item_index: int, product_id: Optional[int]):
|
||||||
"""商品選択ドロップダウンから呼ばれ、商品情報を行に反映"""
|
"""商品選択ドロップダウンから呼ばれ、商品情報を行に反映"""
|
||||||
|
|
@ -1832,8 +1914,11 @@ class FlutterStyleDashboard:
|
||||||
item.product_id = product.id
|
item.product_id = product.id
|
||||||
item.description = product.name
|
item.description = product.name
|
||||||
item.unit_price = product.unit_price
|
item.unit_price = product.unit_price
|
||||||
|
item.quantity = 1
|
||||||
|
self._refresh_item_row(item_index, full_refresh=True)
|
||||||
|
self._refresh_total_amount_display()
|
||||||
# 行だけ更新し、再描画は即時に行う
|
# 行だけ更新し、再描画は即時に行う
|
||||||
self.update_main_content()
|
self.page.update()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.warning(f"商品選択反映失敗: {e}")
|
logging.warning(f"商品選択反映失敗: {e}")
|
||||||
|
|
||||||
|
|
@ -1882,6 +1967,7 @@ class FlutterStyleDashboard:
|
||||||
on_delete_row=self._delete_item_row,
|
on_delete_row=self._delete_item_row,
|
||||||
products=self.app_service.product.get_all_products(),
|
products=self.app_service.product.get_all_products(),
|
||||||
on_product_select=self._open_product_picker_for_row,
|
on_product_select=self._open_product_picker_for_row,
|
||||||
|
row_refs=self._ensure_item_row_refs(),
|
||||||
)
|
)
|
||||||
|
|
||||||
def _open_product_picker_for_row(self, item_index: int):
|
def _open_product_picker_for_row(self, item_index: int):
|
||||||
|
|
@ -1913,9 +1999,12 @@ class FlutterStyleDashboard:
|
||||||
item.product_id = product.id
|
item.product_id = product.id
|
||||||
item.description = product.name
|
item.description = product.name
|
||||||
item.unit_price = product.unit_price
|
item.unit_price = product.unit_price
|
||||||
|
item.quantity = 1
|
||||||
# 先にフラグを戻してから画面更新(詳細に即戻る)
|
# 先にフラグを戻してから画面更新(詳細に即戻る)
|
||||||
self.is_product_picker_open = False
|
self.is_product_picker_open = False
|
||||||
self.is_new_product_form_open = False
|
self.is_new_product_form_open = False
|
||||||
|
self._refresh_item_row(row, full_refresh=True)
|
||||||
|
self._refresh_total_amount_display()
|
||||||
self.update_main_content()
|
self.update_main_content()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.warning(f"商品選択適用失敗: {e}")
|
logging.warning(f"商品選択適用失敗: {e}")
|
||||||
|
|
@ -1938,6 +2027,108 @@ class FlutterStyleDashboard:
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.warning(f"TimePicker open error: {e}")
|
logging.warning(f"TimePicker open error: {e}")
|
||||||
|
|
||||||
|
def _build_delete_draft_button(self, is_view_mode: bool) -> Optional[ft.Control]:
|
||||||
|
if not is_view_mode:
|
||||||
|
return None
|
||||||
|
invoice = getattr(self, "editing_invoice", None)
|
||||||
|
if not invoice or not getattr(invoice, "is_draft", False):
|
||||||
|
return None
|
||||||
|
return ft.IconButton(
|
||||||
|
icon=ft.Icons.DELETE_FOREVER,
|
||||||
|
icon_color=ft.Colors.RED_400,
|
||||||
|
tooltip="下書きを削除",
|
||||||
|
on_click=self._confirm_delete_current_invoice,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _confirm_delete_current_invoice(self, _=None):
|
||||||
|
invoice = getattr(self, "editing_invoice", None)
|
||||||
|
if not invoice or not getattr(invoice, "is_draft", False):
|
||||||
|
return
|
||||||
|
dialog = ft.AlertDialog(
|
||||||
|
modal=True,
|
||||||
|
title=ft.Text("下書きを削除"),
|
||||||
|
content=ft.Text("この下書きを削除しますか? この操作は元に戻せません。"),
|
||||||
|
actions=[
|
||||||
|
ft.TextButton("キャンセル", on_click=self._close_dialog),
|
||||||
|
ft.TextButton("削除", style=ft.ButtonStyle(color=ft.Colors.RED_600), on_click=lambda _: self._delete_current_draft()),
|
||||||
|
],
|
||||||
|
actions_alignment=ft.MainAxisAlignment.END,
|
||||||
|
)
|
||||||
|
self.page.dialog = dialog
|
||||||
|
dialog.open = True
|
||||||
|
self.page.update()
|
||||||
|
|
||||||
|
def _close_dialog(self, _=None):
|
||||||
|
dialog = getattr(self.page, "dialog", None)
|
||||||
|
if dialog:
|
||||||
|
dialog.open = False
|
||||||
|
self.page.update()
|
||||||
|
|
||||||
|
def _delete_current_draft(self):
|
||||||
|
invoice = getattr(self, "editing_invoice", None)
|
||||||
|
if not invoice or not getattr(invoice, "is_draft", False):
|
||||||
|
self._close_dialog()
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
success = self.app_service.invoice.delete_invoice_by_uuid(invoice.uuid)
|
||||||
|
if success:
|
||||||
|
self._show_snack("下書きを削除しました", ft.Colors.GREEN_600)
|
||||||
|
self.editing_invoice = None
|
||||||
|
self.invoices = self.app_service.invoice.get_recent_invoices(20)
|
||||||
|
self.current_tab = 0
|
||||||
|
else:
|
||||||
|
self._show_snack("削除に失敗しました", ft.Colors.RED_600)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"ドラフト削除エラー: {e}")
|
||||||
|
self._show_snack("削除中にエラーが発生しました", ft.Colors.RED_600)
|
||||||
|
finally:
|
||||||
|
self._close_dialog()
|
||||||
|
self.update_main_content()
|
||||||
|
|
||||||
|
def _build_total_amount_text(self) -> ft.Text:
|
||||||
|
total = self.editing_invoice.total_amount if self.editing_invoice else 0
|
||||||
|
self._total_amount_text = ft.Text(
|
||||||
|
f"¥{total:,} (税込)",
|
||||||
|
size=15,
|
||||||
|
weight=ft.FontWeight.BOLD,
|
||||||
|
color=ft.Colors.BLUE_700,
|
||||||
|
)
|
||||||
|
return self._total_amount_text
|
||||||
|
|
||||||
|
def _refresh_total_amount_display(self):
|
||||||
|
if not self._total_amount_text:
|
||||||
|
return
|
||||||
|
total = self.editing_invoice.total_amount if self.editing_invoice else 0
|
||||||
|
self._total_amount_text.value = f"¥{total:,} (税込)"
|
||||||
|
|
||||||
|
def _ensure_item_row_refs(self) -> Dict[int, Dict[str, ft.Control]]:
|
||||||
|
self._item_row_refs = {}
|
||||||
|
return self._item_row_refs
|
||||||
|
|
||||||
|
def _refresh_item_row(self, index: int, full_refresh: bool = False):
|
||||||
|
if not self.editing_invoice:
|
||||||
|
return
|
||||||
|
row_refs = getattr(self, "_item_row_refs", {})
|
||||||
|
row_controls = row_refs.get(index)
|
||||||
|
if not row_controls or index >= len(self.editing_invoice.items):
|
||||||
|
return
|
||||||
|
item = self.editing_invoice.items[index]
|
||||||
|
|
||||||
|
if full_refresh:
|
||||||
|
product_button = row_controls.get("product")
|
||||||
|
if product_button and isinstance(product_button.content, ft.Text):
|
||||||
|
product_button.content.value = item.description or "商品選択"
|
||||||
|
quantity_field = row_controls.get("quantity")
|
||||||
|
if quantity_field:
|
||||||
|
quantity_field.value = str(item.quantity)
|
||||||
|
unit_price_field = row_controls.get("unit_price")
|
||||||
|
if unit_price_field:
|
||||||
|
unit_price_field.value = f"{item.unit_price:,}"
|
||||||
|
|
||||||
|
subtotal_text = row_controls.get("subtotal")
|
||||||
|
if subtotal_text:
|
||||||
|
subtotal_text.value = f"¥{item.subtotal:,}"
|
||||||
|
|
||||||
def _show_snack(self, message: str, color=ft.Colors.BLUE_GREY_800):
|
def _show_snack(self, message: str, color=ft.Colors.BLUE_GREY_800):
|
||||||
try:
|
try:
|
||||||
logging.info(f"show_snack: {message}")
|
logging.info(f"show_snack: {message}")
|
||||||
|
|
@ -2399,7 +2590,8 @@ class FlutterStyleDashboard:
|
||||||
customer=self.selected_customer,
|
customer=self.selected_customer,
|
||||||
document_type=self.selected_document_type,
|
document_type=self.selected_document_type,
|
||||||
amount=amount,
|
amount=amount,
|
||||||
notes=""
|
notes="",
|
||||||
|
is_draft=bool(getattr(self, "editing_invoice", None) and getattr(self.editing_invoice, "is_draft", False)),
|
||||||
)
|
)
|
||||||
|
|
||||||
if invoice:
|
if invoice:
|
||||||
|
|
|
||||||
|
|
@ -134,7 +134,8 @@ class InvoiceService:
|
||||||
document_type: DocumentType,
|
document_type: DocumentType,
|
||||||
amount: int,
|
amount: int,
|
||||||
notes: str = "",
|
notes: str = "",
|
||||||
items: List[InvoiceItem] = None) -> Optional[Invoice]:
|
items: List[InvoiceItem] = None,
|
||||||
|
is_draft: bool = False) -> Optional[Invoice]:
|
||||||
"""新規伝票作成
|
"""新規伝票作成
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|
@ -171,7 +172,8 @@ class InvoiceService:
|
||||||
date=datetime.now(),
|
date=datetime.now(),
|
||||||
items=items,
|
items=items,
|
||||||
document_type=document_type,
|
document_type=document_type,
|
||||||
notes=notes
|
notes=notes,
|
||||||
|
is_draft=is_draft
|
||||||
)
|
)
|
||||||
|
|
||||||
# --- 長期保管向け: canonical payload + hash chain ---
|
# --- 長期保管向け: canonical payload + hash chain ---
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue