編集画面の充実

This commit is contained in:
joe 2026-02-24 02:04:49 +09:00
parent be14fa7eb2
commit 25032d3b9b
3 changed files with 313 additions and 108 deletions

View file

@ -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
View file

@ -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:

View file

@ -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 ---