From f97d671ec478e3f06d28557f56e1f3f88dbbfc24 Mon Sep 17 00:00:00 2001 From: joe Date: Sun, 22 Feb 2026 11:59:22 +0900 Subject: [PATCH] =?UTF-8?q?=E4=BC=91=E6=86=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app_flutter_style_dashboard.py | 1184 ++++++++++++----- ...260222(請求書)11_請求書分_1円_1864aa2a.PDF | 80 ++ .../20260222(請求書)1_1_1円_8f541afb.PDF | 80 ++ ...60222(請求書)1_新しい商品_1円_d67f16b0.PDF | 80 ++ ...60222(請求書)55_請求書分_25円_f176c22b.PDF | 80 ++ ...60222(請求書)66_請求書分_36円_4e79d0e3.PDF | 80 ++ ...60222(請求書)77_請求書分_49円_64fe98b2.PDF | 80 ++ ...0222(請求書)88_新しい商品_0円_60b38dbf.PDF | 80 ++ ...260222(請求書)88_請求書分_0円_07e20816.PDF | 80 ++ .../20260222(請求書)8_8_64円_bad93f2b.PDF | 80 ++ .../20260222(請求書)9_9_81円_4d208bf7.PDF | 80 ++ models/invoice_models.py | 7 +- services/app_service.py | 122 +- services/repositories.py | 93 +- 14 files changed, 1860 insertions(+), 346 deletions(-) create mode 100644 generated_pdfs/20260222(請求書)11_請求書分_1円_1864aa2a.PDF create mode 100644 generated_pdfs/20260222(請求書)1_1_1円_8f541afb.PDF create mode 100644 generated_pdfs/20260222(請求書)1_新しい商品_1円_d67f16b0.PDF create mode 100644 generated_pdfs/20260222(請求書)55_請求書分_25円_f176c22b.PDF create mode 100644 generated_pdfs/20260222(請求書)66_請求書分_36円_4e79d0e3.PDF create mode 100644 generated_pdfs/20260222(請求書)77_請求書分_49円_64fe98b2.PDF create mode 100644 generated_pdfs/20260222(請求書)88_新しい商品_0円_60b38dbf.PDF create mode 100644 generated_pdfs/20260222(請求書)88_請求書分_0円_07e20816.PDF create mode 100644 generated_pdfs/20260222(請求書)8_8_64円_bad93f2b.PDF create mode 100644 generated_pdfs/20260222(請求書)9_9_81円_4d208bf7.PDF diff --git a/app_flutter_style_dashboard.py b/app_flutter_style_dashboard.py index 74eb2a4..1632034 100644 --- a/app_flutter_style_dashboard.py +++ b/app_flutter_style_dashboard.py @@ -19,6 +19,88 @@ logging.basicConfig( format='%(asctime)s - %(levelname)s - %(message)s' ) +class ZoomableContainer(ft.Container): + """ピンチイン/アウト対応コンテナ""" + + def __init__(self, content=None, min_scale=0.5, max_scale=3.0, **kwargs): + super().__init__(**kwargs) + self.content = content + self.min_scale = min_scale + self.max_scale = max_scale + self.scale = 1.0 + self.initial_distance = 0 + + # ズーム機能を無効化してシンプルなコンテナとして使用 + # 将来的な実装のためにクラスは残す + +class AppBar(ft.Container): + """標準化されたアプリケーションヘッダー""" + + def __init__(self, title: str, show_back: bool = False, show_edit: bool = False, + on_back=None, on_edit=None, page=None): + super().__init__() + self.title = title + self.show_back = show_back + self.show_edit = show_edit + self.on_back = on_back + self.on_edit = on_edit + self.page_ref = page # page_refとして保存 + + self.bgcolor = ft.Colors.BLUE_GREY_50 + self.padding = ft.Padding.symmetric(horizontal=16, vertical=8) + + self.content = self._build_content() + + def _build_content(self) -> ft.Row: + """AppBarのコンテンツを構築""" + controls = [] + + # 左側:戻るボタン + if self.show_back: + controls.append( + ft.IconButton( + icon=ft.Icons.ARROW_BACK, + icon_color=ft.Colors.BLUE_GREY_700, + tooltip="戻る", + on_click=self.on_back if self.on_back else None + ) + ) + else: + controls.append(ft.Container(width=48)) # スペーーサー + + # 中央:タイトル + controls.append( + ft.Container( + content=ft.Text( + self.title, + size=18, + weight=ft.FontWeight.W_500, + color=ft.Colors.BLUE_GREY_800 + ), + expand=True, + alignment=ft.alignment.Alignment(0, 0) # 中央揃え + ) + ) + + # 右側:編集ボタン + if self.show_edit: + controls.append( + ft.IconButton( + icon=ft.Icons.EDIT, + icon_color=ft.Colors.BLUE_GREY_700, + tooltip="編集", + on_click=self.on_edit if self.on_edit else None + ) + ) + else: + controls.append(ft.Container(width=48)) # スペーサー + + return ft.Row( + controls, + alignment=ft.MainAxisAlignment.SPACE_BETWEEN, + vertical_alignment=ft.CrossAxisAlignment.CENTER + ) + class FlutterStyleDashboard: """Flutter風の統合ダッシュボード""" @@ -84,7 +166,7 @@ class FlutterStyleDashboard: invoice.document_type.value, invoice.customer.formal_name, invoice.total_amount, - invoice.date.strftime('%Y-%m-%d'), + invoice.date.strftime('%Y-%m-%d %H:%M'), '完了', invoice.notes )) @@ -95,30 +177,6 @@ class FlutterStyleDashboard: 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) @@ -126,7 +184,6 @@ class FlutterStyleDashboard: self.page.add( ft.Column([ self.main_content, - bottom_nav, ], expand=True) ) @@ -143,109 +200,174 @@ class FlutterStyleDashboard: """メインコンテンツ更新""" 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()) + if self.is_customer_picker_open: + # 顧客選択画面 + self.main_content.controls.append(self.create_customer_picker_screen()) + elif self.current_tab == 0: + # 伝票一覧画面 + self.main_content.controls.append(self._build_invoice_list_screen()) + elif self.current_tab == 1: + # 伝票詳細/編集画面 + self.main_content.controls.append(self._build_invoice_detail_screen()) + elif self.current_tab == 2: + # 新規作成画面 + self.main_content.controls.append(self._build_new_invoice_screen()) self.page.update() - - def create_slip_list_screen(self) -> ft.Container: - """伝票一覧画面""" + + def _build_invoice_list_screen(self) -> ft.Column: + """伝票一覧画面を構築""" + # AppBar(戻るボタンなし、編集ボタンなし) + app_bar = AppBar( + title="伝票一覧", + show_back=False, + show_edit=False + ) + + # 伝票リスト(ズーム対応) + invoice_list = self._build_invoice_list() + zoomable_list = ZoomableContainer( + content=invoice_list, + min_scale=0.8, + max_scale=2.5 + ) + + # 浮動アクションボタン + fab = ft.FloatingActionButton( + icon=ft.Icons.ADD, + on_click=lambda _: self.open_new_invoice(), + tooltip="新規伝票作成" + ) + + return ft.Column([ + app_bar, + ft.Container( + content=zoomable_list, + expand=True, + padding=ft.Padding.all(16) + ), + ], expand=True) + + def _build_invoice_detail_screen(self) -> ft.Column: + """伝票詳細画面を構築""" + if not self.editing_invoice: + return ft.Column([ft.Text("伝票が選択されていません")]) + + # AppBar(戻るボタンあり、編集ボタン条件付き) + is_locked = getattr(self.editing_invoice, 'final_locked', False) + app_bar = AppBar( + title=f"伝票詳細: {self.editing_invoice.invoice_number}", + show_back=True, + show_edit=not is_locked, + on_back=lambda _: self.back_to_list(), + on_edit=lambda _: self.toggle_edit_mode() + ) + + # 伝票詳細コンテンツ(ズーム対応) + detail_content = self._create_edit_existing_screen() + zoomable_content = ZoomableContainer( + content=detail_content, + min_scale=0.8, + max_scale=2.5 + ) + + return ft.Column([ + app_bar, + ft.Container( + content=zoomable_content, + expand=True, + padding=ft.Padding.all(16) + ), + ], expand=True) + + def _build_new_invoice_screen(self) -> ft.Column: + """新規作成画面を構築""" + # AppBar(戻るボタンあり、編集ボタンなし) + app_bar = AppBar( + title="新規伝票作成", + show_back=True, + show_edit=False, + on_back=lambda _: self.back_to_list() + ) + + # 新規作成コンテンツ(ズーム対応) + new_content = self.create_slip_input_screen() + zoomable_content = ZoomableContainer( + content=new_content, + min_scale=0.8, + max_scale=2.5 + ) + + return ft.Column([ + app_bar, + ft.Container( + content=zoomable_content, + expand=True, + padding=ft.Padding.all(16) + ), + ], expand=True) + + def back_to_list(self): + """一覧画面に戻る""" + self.current_tab = 0 + self.editing_invoice = None + self.update_main_content() + + def toggle_edit_mode(self): + """編集モードを切り替え""" + if hasattr(self, 'is_detail_edit_mode'): + self.is_detail_edit_mode = not self.is_detail_edit_mode + else: + self.is_detail_edit_mode = True + self.update_main_content() + + def _build_invoice_list(self) -> ft.Column: + """伝票リストを構築""" # 履歴データ読み込み slips = self.load_slips() + logging.info(f"伝票データ取得: {len(slips)}件") + if not self.show_offsets: slips = [s for s in slips if not (isinstance(s, Invoice) and getattr(s, "is_offset", False))] + logging.info(f"赤伝除外後: {len(slips)}件") 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() + def on_verify_chain(_): + """チェーン検証""" + try: + result = self.app_service.invoice.verify_chain() + self.chain_verify_result = result + self.update_main_content() + except Exception as e: + logging.error(f"チェーン検証エラー: {e}") # 履歴カードリスト slip_cards = [] - for slip in slips: + for i, slip in enumerate(slips): + logging.info(f"伝票{i}: {type(slip)}") 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, - ) + logging.info(f"カード作成数: {len(slip_cards)}") + + return ft.Column([ + # 検証結果表示(あれば) + 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, + ), + ]) def create_customer_picker_screen(self) -> ft.Container: """顧客選択画面(画面内遷移・ダイアログ不使用)""" self.customers = self.app_service.customer.get_all_customers() @@ -254,6 +376,14 @@ class FlutterStyleDashboard: self.is_customer_picker_open = False self.customer_search_query = "" self.update_main_content() + + # AppBar(戻るボタンあり) + app_bar = AppBar( + title="顧客選択", + show_back=True, + show_edit=False, + on_back=back + ) list_container = ft.Column([], spacing=0, scroll=ft.ScrollMode.AUTO, expand=True) @@ -271,7 +401,12 @@ class FlutterStyleDashboard: def select_customer(customer: Customer): self.selected_customer = customer - logging.info(f"顧客を選択: {customer.formal_name}") + # 新規伝票作成中の場合は顧客を設定 + if hasattr(self, 'editing_invoice') and self.editing_invoice: + self.editing_invoice.customer = customer + logging.info(f"伝票に顧客を設定: {customer.formal_name}") + else: + logging.info(f"顧客を選択: {customer.formal_name}") back() def on_search_change(e): @@ -318,7 +453,7 @@ class FlutterStyleDashboard: content = ft.Column( [ - header, + app_bar, # AppBarを追加 ft.Container(content=search_field, padding=ft.padding.all(15)), ft.Container(content=list_container, padding=ft.padding.symmetric(horizontal=15), expand=True), ], @@ -330,6 +465,27 @@ class FlutterStyleDashboard: return ft.Container(content=content, expand=True) + def save_as_draft(self, _): + """下書きとして保存""" + try: + # 下書き伝票を作成 + draft_invoice = self.app_service.invoice.create_draft_invoice( + customer=self.selected_customer, + document_type=DocumentType.DRAFT, + amount=int(self.amount_value or "0"), + notes="下書き" + ) + + if draft_invoice: + logging.info(f"下書き保存成功: {draft_invoice.invoice_number}") + # 一覧を更新 + self.invoices = self.app_service.invoice.get_recent_invoices(20) + self.back_to_list() + else: + logging.error("下書き保存失敗") + except Exception as e: + logging.error(f"下書き保存エラー: {e}") + def create_slip_input_screen(self) -> ft.Container: """伝票入力画面""" # 帳票種類選択(タブ風ボタン) @@ -349,6 +505,22 @@ class FlutterStyleDashboard: ) for i, doc_type in enumerate(document_types) ], wrap=True) + # 下書きボタン(新規作成時のみ表示) + draft_button = ft.Container( + content=ft.ElevatedButton( + content=ft.Row([ + ft.Icon(ft.Icons.DRAFTS, size=16), + ft.Text("下書きとして保存", size=12), + ], spacing=5), + style=ft.ButtonStyle( + bgcolor=ft.Colors.ORANGE_500, + color=ft.Colors.WHITE, + ), + on_click=self.save_as_draft, + ), + margin=ft.margin.only(top=10), + ) + return ft.Container( content=ft.Column([ # ヘッダー @@ -368,6 +540,7 @@ class FlutterStyleDashboard: ft.Text("帳票の種類を選択", size=16, weight=ft.FontWeight.BOLD), ft.Container(height=10), type_buttons, + draft_button, # 下書きボタンを追加 ]), padding=ft.padding.all(20), ), @@ -386,13 +559,14 @@ class FlutterStyleDashboard: border_color=ft.Colors.BLUE_GREY, expand=True, ), + ft.Container(width=8), # スペースを追加 ft.IconButton( ft.Icons.PERSON_SEARCH, tooltip="顧客を選択", on_click=self.open_customer_picker, ), ], - spacing=8, + alignment=ft.MainAxisAlignment.SPACE_BETWEEN, # 検索ボタンを右端に配置 ), ft.Container(height=10), ft.TextField( @@ -453,40 +627,46 @@ class FlutterStyleDashboard: return ft.Container( content=ft.Column([ - # ヘッダー + # ヘッダー(コンパクトに) ft.Container( content=ft.Row([ - ft.Text("📄 発行履歴管理", size=20, weight=ft.FontWeight.BOLD), + ft.Text("📄 履歴", size=16, 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), + ft.Row([ + ft.IconButton( + ft.Icons.VERIFIED, + tooltip="チェーン検証", + icon_color=ft.Colors.BLUE_300, + icon_size=16, # アイコンを小さく + on_click=on_verify_chain, + ), + ft.Row( + [ + ft.Text("赤伝", size=10, color=ft.Colors.WHITE), # 文字を小さく + ft.Switch(value=self.show_offsets, on_change=on_toggle_offsets), + ], + spacing=3, # 間隔を狭める + ), + ft.IconButton( + ft.Icons.CLEAR_ALL, + icon_size=16, # アイコンを小さく + ), + ], spacing=3), # 間隔を狭める ]), - padding=ft.padding.all(15), + padding=ft.padding.all(8), # パディングを狭める bgcolor=ft.Colors.BLUE_GREY, ), # 検証結果表示(あれば) ft.Container( content=self._build_chain_verify_result(), - margin=ft.Margin.only(bottom=10), + margin=ft.Margin.only(bottom=5), # 間隔を狭める ) if self.chain_verify_result else ft.Container(height=0), - # 履歴リスト + # 履歴リスト(極限密度表示) ft.Column( controls=slip_cards, - spacing=10, + spacing=0, # カード間隔を0pxに scroll=ft.ScrollMode.AUTO, expand=True, ), @@ -494,6 +674,27 @@ class FlutterStyleDashboard: expand=True, ) + def can_create_offset_invoice(self, invoice: Invoice) -> bool: + """赤伝発行可能かチェック""" + # LOCK済み伝票であること + if not getattr(invoice, 'final_locked', False): + return False + + # すでに赤伝が存在しないこと + try: + import sqlite3 + with sqlite3.connect('sales.db') as conn: + cursor = conn.cursor() + cursor.execute( + 'SELECT COUNT(*) FROM invoices WHERE offset_target_uuid = ?', + (invoice.uuid,) + ) + offset_count = cursor.fetchone()[0] + return offset_count == 0 + except Exception as e: + logging.error(f"赤伝存在チェックエラー: {e}") + return False + def create_slip_card(self, slip) -> ft.Card: """伝票カード作成""" # サービス層からは Invoice オブジェクトが返る @@ -501,10 +702,19 @@ class FlutterStyleDashboard: slip_type = slip.document_type.value customer_name = slip.customer.formal_name amount = slip.total_amount - date = slip.date.strftime("%Y-%m-%d") + date = slip.date.strftime("%Y-%m-%d %H:%M") status = "赤伝" if getattr(slip, "is_offset", False) else "完了" + # 最初の商品名を取得(複数ある場合は「他」を付与) + if slip.items and len(slip.items) > 0: + first_item_name = slip.items[0].description + if len(slip.items) > 1: + first_item_name += "(他" + str(len(slip.items) - 1) + ")" + else: + first_item_name = "" else: slip_id, slip_type, customer_name, amount, date, status, description, created_at = slip + date = date.strftime("%Y-%m-%d %H:%M") + first_item_name = description or "" # タイプに応じたアイコンと色 type_config = { @@ -517,120 +727,242 @@ class FlutterStyleDashboard: 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 on_single_tap(_): + """シングルタップ:詳細表示""" + if isinstance(slip, Invoice): + self.open_invoice_detail(slip) - def edit_invoice(_=None): - if not isinstance(slip, Invoice): - return - self.open_invoice_edit(slip) - - actions_row = None + def on_double_tap(_): + """ダブルタップ:編集モード切替""" + if isinstance(slip, Invoice): + self.open_invoice_edit(slip) + + def on_long_press(_): + """長押し:コンテキストメニュー""" + self.show_context_menu(slip) + + # 赤伝ボタンの表示条件チェック + show_offset_button = False 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) - + show_offset_button = self.can_create_offset_invoice(slip) + + # 長押しメニューで操作するため、ボタンは不要 + 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), + + return ft.GestureDetector( + content=ft.Card( + content=ft.Container( + content=ft.Column([ + ft.Row([ + ft.Container( + content=ft.Text(config["icon"], size=16), # アイコンを少し大きく + width=28, + height=28, + bgcolor=config["color"], + border_radius=14, + alignment=ft.alignment.Alignment(0, 0), + ), + ft.Container( + content=ft.Column([ + ft.Text(slip_type, size=7, weight=ft.FontWeight.BOLD), # タイプ文字をさらに小さく + ft.Text(customer_name, size=15, weight=ft.FontWeight.W_500), # 顧客名を1.5倍に + ft.Row([ + ft.Text(first_item_name, size=12, color=ft.Colors.GREY_600), # 商品名をさらに大きく + ft.Container(expand=True), # スペースを取る + ft.Text(f"¥{display_amount:,.0f}", size=11, weight=ft.FontWeight.BOLD), # 金額を右寄せ + ]), + ], + spacing=0, # 行間を最小化 + tight=True, # 余白を最小化 + ), + expand=True, + ), + ]), + ft.Container(height=0), # 間隔を完全に削除 + ft.Row([ + ft.Text(f"{date} | {status}", size=9, color=ft.Colors.GREY_600), # 日付を大きく + ft.Container(expand=True), # スペースを取る + # 赤伝ボタン(条件付きで表示) + ft.Container( + content=ft.IconButton( + icon=ft.icons.REMOVE_CIRCLE_OUTLINE, + icon_color=ft.Colors.RED_500, + icon_size=16, + tooltip="赤伝発行", + on_click=lambda _: self.create_offset_invoice_dialog(slip), + disabled=not show_offset_button + ) if show_offset_button else ft.Container(width=20), + ), + ]), + ], + spacing=0, # カラムの行間を最小化 + tight=True, # 余白を最小化 + ), + padding=ft.padding.all(2), # パディングを最小化 + ), + elevation=0, ), - elevation=3, + on_tap=on_single_tap, + on_double_tap=on_double_tap, + on_long_press=on_long_press, ) + def create_offset_invoice_dialog(self, invoice: Invoice): + """赤伝発行確認ダイアログ""" + def close_dialog(_): + self.dialog.open = False + self.update_main_content() + + def confirm_create_offset(_): + # 赤伝を発行 + offset_invoice = self.app_service.invoice.create_offset_invoice( + invoice.uuid, + f"相殺伝票: {invoice.invoice_number}" + ) + if offset_invoice: + logging.info(f"赤伝発行成功: {offset_invoice.invoice_number}") + # 一覧を更新 + self.invoices = self.app_service.invoice.get_recent_invoices(20) + self.update_main_content() + else: + logging.error(f"赤伝発行失敗: {invoice.invoice_number}") + close_dialog(_) + + # 確認ダイアログ + self.dialog = ft.AlertDialog( + modal=True, + title=ft.Text("赤伝発行確認"), + content=ft.Column([ + ft.Text(f"以下の伝票の赤伝を発行します。"), + ft.Container(height=10), + ft.Text(f"伝票番号: {invoice.invoice_number}"), + ft.Text(f"顧客: {invoice.customer.formal_name}"), + ft.Text(f"金額: ¥{invoice.total_amount:,.0f}"), + ft.Container(height=10), + ft.Text("赤伝発行後は取り消せません。よろしいですか?", + color=ft.Colors.RED, weight=ft.FontWeight.BOLD), + ], tight=True), + actions=[ + ft.TextButton("キャンセル", on_click=close_dialog), + ft.ElevatedButton( + "赤伝発行", + on_click=confirm_create_offset, + style=ft.ButtonStyle( + color=ft.Colors.WHITE, + bgcolor=ft.Colors.RED_500 + ) + ), + ], + actions_alignment=ft.MainAxisAlignment.END, + ) + + self.dialog.open = True + self.update_main_content() + + def show_context_menu(self, slip): + """コンテキストメニューを表示""" + if not isinstance(slip, Invoice): + return + + def close_dialog(_): + self.dialog.open = False + self.update_main_content() + + def edit_invoice(_): + self.open_invoice_edit(slip) + close_dialog(_) + + def delete_invoice(_): + self.delete_invoice(slip.uuid) + close_dialog(_) + + def create_offset(_): + self.create_offset_invoice_dialog(slip) + close_dialog(_) + + # メニューアイテムの構築 + menu_items = [] + + # 編集メニュー + if not getattr(slip, 'final_locked', False): + menu_items.append( + ft.PopupMenuItem( + text=ft.Row([ + ft.Icon(ft.Icons.EDIT, size=16), + ft.Text("編集", size=14), + ], spacing=8), + on_click=edit_invoice + ) + ) + + # 赤伝発行メニュー + if self.can_create_offset_invoice(slip): + menu_items.append( + ft.PopupMenuItem( + text=ft.Row([ + ft.Icon(ft.Icons.REMOVE_CIRCLE, size=16), + ft.Text("赤伝発行", size=14), + ], spacing=8), + on_click=create_offset + ) + ) + + # 削除メニュー + menu_items.append( + ft.PopupMenuItem( + text=ft.Row([ + ft.Icon(ft.Icons.DELETE, size=16), + ft.Text("削除", size=14), + ], spacing=8), + on_click=delete_invoice + ) + ) + + # コンテキストメニューダイアログ + self.dialog = ft.AlertDialog( + modal=True, + title=ft.Text(f"操作選択: {slip.invoice_number}"), + content=ft.Column(menu_items, tight=True, spacing=2), + actions=[ + ft.TextButton("キャンセル", on_click=close_dialog), + ], + actions_alignment=ft.MainAxisAlignment.END, + ) + + self.dialog.open = True + self.update_main_content() + + def open_invoice_detail(self, invoice: Invoice): + """伝票詳細を開く""" + self.editing_invoice = invoice + self.current_tab = 1 + self.is_detail_edit_mode = False # 表示モードで開く + self.update_main_content() + + def open_invoice_edit(self, invoice: Invoice): + """伝票編集を開く""" + self.editing_invoice = invoice + self.current_tab = 1 + self.is_detail_edit_mode = True # 編集モードで開く + self.update_main_content() + + def delete_invoice(self, invoice_uuid: str): + """伝票を削除""" + try: + success = self.app_service.invoice.delete_invoice_by_uuid(invoice_uuid) + if success: + logging.info(f"伝票削除成功: {invoice_uuid}") + # リストを更新 + self.invoices = self.app_service.invoice.get_recent_invoices(20) + self.update_main_content() + else: + logging.error(f"伝票削除失敗: {invoice_uuid}") + except Exception as e: + logging.error(f"伝票削除エラー: {e}") + def _build_chain_verify_result(self) -> ft.Control: if not self.chain_verify_result: return ft.Container(height=0) @@ -679,36 +1011,184 @@ class FlutterStyleDashboard: 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() + """伝票編集画面(新規・編集統合)""" + # 常に詳細編集画面を使用 + if not self.editing_invoice: + # 新規伝票の場合は空のInvoiceを作成 + from models.invoice_models import Invoice, Customer, DocumentType + default_customer = Customer( + id=0, + name="選択してください", + formal_name="選択してください", + address="", + phone="" + ) + self.editing_invoice = Invoice( + customer=default_customer, + date=datetime.now(), + items=[], + document_type=DocumentType.SALES, + invoice_number="NEW-" + str(int(datetime.now().timestamp())) # 新規伝票番号 + ) + self.is_detail_edit_mode = True # 新規作成モード + + # 既存・新規共通で詳細編集画面を返す + return self._create_edit_existing_screen() def _create_edit_existing_screen(self) -> ft.Container: - """既存伝票の編集画面""" - # 編集不可チェック - is_locked = getattr(self.editing_invoice, 'submitted_to_tax_authority', False) + """既存伝票の編集画面(新規・編集共通)""" + # 編集不可チェック(新規作成時はFalse) + is_new_invoice = self.editing_invoice.invoice_number.startswith("NEW-") + + # LOCK条件:明示的に確定された場合のみLOCK + # PDF生成だけではLOCKしない(お試しPDFを許可) + pdf_generated = getattr(self.editing_invoice, 'pdf_generated_at', None) is not None + chain_hash = getattr(self.editing_invoice, 'chain_hash', None) is not None + final_locked = getattr(self.editing_invoice, 'final_locked', False) # 明示的確定フラグ + + is_locked = final_locked if not is_new_invoice else False is_view_mode = not getattr(self, 'is_detail_edit_mode', False) + # デバッグ用にLOCK状態をログ出力 + logging.info(f"伝票LOCK状態: {self.editing_invoice.invoice_number}") + logging.info(f" PDF生成: {pdf_generated}") + logging.info(f" チェーン収容: {chain_hash}") + logging.info(f" 明示的確定: {final_locked}") + logging.info(f" LOCK状態: {is_locked}") + logging.info(f" 新規伝票: {is_new_invoice}") + logging.info(f" 編集モード: {getattr(self, 'is_detail_edit_mode', False)}") + logging.info(f" 表示モード: {is_view_mode}") + # 伝票種類選択(ヘッダーに移動) document_types = list(DocumentType) # 明細テーブル - items_table = self._create_items_table(self.editing_invoice.items, is_locked or is_view_mode) + if is_view_mode: + items_table = self._create_view_mode_table(self.editing_invoice.items) + else: + items_table = self._create_edit_mode_table(self.editing_invoice.items, is_locked) - # 顧客表示 - 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, - ) + # 顧客表示・選択 + def select_customer(): + """顧客選択画面を開く""" + self.current_tab = 2 # 顧客選択タブ + self.update_main_content() + + # 編集モード時は顧客名入力フィールドを表示 + if not is_view_mode and not is_locked: + # 編集モード:顧客名入力フィールド + customer_field = ft.TextField( + label="顧客名", + value=self.editing_invoice.customer.name if self.editing_invoice.customer.name != "選択してください" else "", + disabled=is_locked, + width=300, + ) + + def update_customer_name(e): + """顧客名を更新""" + if self.editing_invoice: + # 既存顧客を検索、なければ新規作成 + 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: + self.editing_invoice.customer = found_customer + else: + # 新規顧客を作成 + from models.invoice_models import Customer + self.editing_invoice.customer = Customer( + id=0, + name=customer_name, + formal_name=customer_name, + address="", + phone="" + ) + + customer_field.on_change = update_customer_name + + customer_display = ft.Container( + content=ft.Row([ + customer_field, # 顧客ラベルを削除 + ft.ElevatedButton( + "選択", + icon=ft.Icons.PERSON_SEARCH, + on_click=lambda _: select_customer(), + style=ft.ButtonStyle( + bgcolor=ft.Colors.BLUE_600, + color=ft.Colors.WHITE + ) + ), + ]), + padding=ft.padding.symmetric(horizontal=10, vertical=5), + bgcolor=ft.Colors.BLUE_50, + border_radius=5, + ) + elif is_new_invoice: + # 新規作成時も同じ表示 + customer_field = ft.TextField( + label="顧客名", + value=self.editing_invoice.customer.name if self.editing_invoice.customer.name != "選択してください" else "", + disabled=is_locked, + width=300, + ) + + def update_customer_name(e): + """顧客名を更新""" + if self.editing_invoice: + # 既存顧客を検索、なければ新規作成 + 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: + self.editing_invoice.customer = found_customer + else: + # 新規顧客を作成 + from models.invoice_models import Customer + self.editing_invoice.customer = Customer( + id=0, + name=customer_name, + formal_name=customer_name, + address="", + phone="" + ) + + customer_field.on_change = update_customer_name + + customer_display = ft.Container( + content=ft.Row([ + customer_field, # 顧客ラベルを削除 + ft.ElevatedButton( + "選択", + icon=ft.Icons.PERSON_SEARCH, + on_click=lambda _: select_customer(), + style=ft.ButtonStyle( + bgcolor=ft.Colors.BLUE_600, + color=ft.Colors.WHITE + ) + ), + ]), + padding=ft.padding.symmetric(horizontal=10, vertical=5), + bgcolor=ft.Colors.BLUE_50, + border_radius=5, + ) + else: + # 既存伝票は表示のみ + customer_display = ft.Container( + content=ft.Row([ + 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( @@ -732,13 +1212,83 @@ class FlutterStyleDashboard: 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}") + # TODO: テーブルの明細データを取得して更新 + # 現在は編集された明細データが反映されていない + logging.info(f"更新前明細件数: {len(self.editing_invoice.items)}") + for i, item in enumerate(self.editing_invoice.items): + logging.info(f" 明細{i+1}: {item.description} x{item.quantity} @¥{item.unit_price}") + + # DBに保存(新規・更新共通) + try: + if is_new_invoice: + # 新規作成 + logging.info(f"=== 新規伝票作成開 ===") + logging.info(f"顧客情報: {self.editing_invoice.customer.name} (ID: {self.editing_invoice.customer.id})") + logging.info(f"伝票種類: {self.editing_invoice.document_type.value}") + logging.info(f"明細件数: {len(self.editing_invoice.items)}") + + # 顧客を先にDBに保存(新規顧客の場合) + if self.editing_invoice.customer.id == 0: + logging.info(f"新規顧客をDBに保存します: {self.editing_invoice.customer.name}") + # 新規顧客をDBに保存 + customer_id = self.app_service.customer.create_customer( + name=self.editing_invoice.customer.name, + formal_name=self.editing_invoice.customer.formal_name, + address=self.editing_invoice.customer.address, + phone=self.editing_invoice.customer.phone + ) + logging.info(f"create_customer戻り値: {customer_id}") + if customer_id > 0: # IDが正しく取得できたかチェック + self.editing_invoice.customer.id = customer_id + logging.info(f"新規顧客をDBに保存: {self.editing_invoice.customer.name} (ID: {customer_id})") + else: + logging.error(f"顧客保存失敗: {self.editing_invoice.customer.name}") + return + + # 合計金額は表示時に計算するため、DBには保存しない + amount = 0 # ダミー値(実際は表示時に計算) + + logging.info(f"伝票作成パラメータ: customer.id={self.editing_invoice.customer.id}, document_type={self.editing_invoice.document_type}, amount={amount}") + + success = self.app_service.invoice.create_invoice( + customer=self.editing_invoice.customer, + document_type=self.editing_invoice.document_type, + amount=amount, + notes=getattr(self.editing_invoice, 'notes', ''), + items=self.editing_invoice.items # UIの明細を渡す + ) + logging.info(f"create_invoice戻り値: {success}") + if success: + logging.info(f"伝票作成成功: {self.editing_invoice.invoice_number}") + # 一覧を更新して新規作成画面を閉じる + self.invoices = self.app_service.invoice.get_recent_invoices(20) + logging.info(f"更新後伝票件数: {len(self.invoices)}") + self.editing_invoice = None + self.current_tab = 0 # 一覧タブに戻る + self.update_main_content() + else: + logging.error(f"伝票作成失敗: {self.editing_invoice.invoice_number}") + else: + # 更新 + logging.info(f"=== 伝票更新開 ===") + success = self.app_service.invoice.update_invoice(self.editing_invoice) + if success: + logging.info(f"伝票更新成功: {self.editing_invoice.invoice_number}") + # 一覧を更新して編集画面を閉じる + self.invoices = self.app_service.invoice.get_recent_invoices(20) + self.editing_invoice = None + self.current_tab = 0 # 一覧タブに戻る + self.update_main_content() + else: + logging.error(f"伝票更新失敗: {self.editing_invoice.invoice_number}") + except Exception as e: + logging.error(f"伝票保存エラー: {e}") + import traceback + logging.error(f"詳細エラー: {traceback.format_exc()}") # 編集モード終了(ビューモードに戻る) self.is_detail_edit_mode = False # ビューモードに戻る @@ -801,30 +1351,22 @@ class FlutterStyleDashboard: # 基本情報行(コンパクトに) 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.Text(f"{self.editing_invoice.invoice_number} | {self.editing_invoice.date.strftime('%Y/%m/%d %H:%M')} | {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, ), + # 顧客名入力(編集モードまたは新規作成時) + customer_display if (not is_view_mode and not is_locked) or is_new_invoice else ft.Container(height=0), + # 明細テーブル(フレキシブルに) 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, # 高さを縮小 + height=400, # 高さを拡大して見やすく border=ft.border.all(1, ft.Colors.GREY_300), border_radius=5, padding=ft.padding.all(1), # パディングを最小化 @@ -835,16 +1377,24 @@ class FlutterStyleDashboard: # 合計金額表示 ft.Container( content=ft.Row([ - ft.Container(expand=True), - ft.Text("合計: ", size=14, weight=ft.FontWeight.BOLD), + 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 ), + 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(), ]), - padding=ft.padding.symmetric(horizontal=10, vertical=8), + padding=ft.padding.symmetric(horizontal=5, vertical=8), # 左右のパディングを減らす bgcolor=ft.Colors.GREY_100, border_radius=5, ), @@ -909,14 +1459,17 @@ class FlutterStyleDashboard: if not self.editing_invoice: return - # 新しい明細行を追加 + # 空の明細行を追加(デフォルト値なし) new_item = InvoiceItem( - description="新しい商品", - quantity=1, + description="", + quantity=0, unit_price=0, - is_discount=False ) + + # 元のinvoice.itemsに直接追加 self.editing_invoice.items.append(new_item) + + # UIを更新 self.update_main_content() def _delete_item_row(self, index: int): @@ -966,23 +1519,30 @@ class FlutterStyleDashboard: logging.error(f"Unit price update error: {e}") # 合計金額を更新するために画面を更新 + # 空行削除ロジック:商品名が無く数量も単価も0なら削除 + self._remove_empty_items() 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) + def _remove_empty_items(self): + """商品名が無く数量も単価も0の明細を削除""" + if not self.editing_invoice: + return - # デバッグ用:メソッド開始時の状態をログ出力 - 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)}") + # 空行を特定(ただし最低1行は残す) + non_empty_items = [] + empty_count = 0 - if is_view_mode or is_locked: - # 表示モード:表形式で整然と表示 - return self._create_view_mode_table(items) - else: - # 編集モード:入力フォーム風に表示 - return self._create_edit_mode_table(items, is_locked) + for item in self.editing_invoice.items: + if (not item.description or item.description.strip() == "") and \ + item.quantity == 0 and \ + item.unit_price == 0: + empty_count += 1 + # 最低1行は残すため、空行が複数ある場合のみ削除 + if empty_count > 1: + continue # 削除 + non_empty_items.append(item) + + self.editing_invoice.items = non_empty_items def _create_view_mode_table(self, items: List[InvoiceItem]) -> ft.Column: """表示モード:フレキシブルな表形式で整然と表示""" @@ -1015,9 +1575,11 @@ class FlutterStyleDashboard: 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)) + # 自動空行追加を無効化(ユーザーが明示的に追加する場合のみ) + # TODO: 必要に応じて空行追加ボタンを提供 + + # 空行の自動追加を無効化 + pass # ヘッダー行 header_row = ft.Row([ @@ -1041,7 +1603,7 @@ class FlutterStyleDashboard: 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), + on_change=lambda e, idx=i: self._update_item_field(idx, 'description', e.control.value), ) # 数量フィールド @@ -1054,7 +1616,7 @@ class FlutterStyleDashboard: 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), + on_change=lambda e, idx=i: self._update_item_field(idx, 'quantity', e.control.value), keyboard_type=ft.KeyboardType.NUMBER, ) @@ -1068,7 +1630,7 @@ class FlutterStyleDashboard: 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(',', '')), + on_change=lambda e, idx=i: self._update_item_field(idx, 'unit_price', e.control.value.replace(',', '')), keyboard_type=ft.KeyboardType.NUMBER, ) @@ -1171,7 +1733,15 @@ class FlutterStyleDashboard: """顧客選択時の処理""" self.selected_customer = customer logging.info(f"顧客を選択: {customer.formal_name}") - # UIを更新して選択された顧客を表示 + + # 編集中の伝票があれば顧客を設定 + if self.editing_invoice: + self.editing_invoice.customer = customer + logging.info(f"編集中伝票に顧客を設定: {customer.formal_name}") + + # 顧客選択画面を閉じて元の画面に戻る + self.is_customer_picker_open = False + self.customer_search_query = "" self.update_main_content() def submit_invoice_for_tax(self, invoice_uuid: str) -> bool: diff --git a/generated_pdfs/20260222(請求書)11_請求書分_1円_1864aa2a.PDF b/generated_pdfs/20260222(請求書)11_請求書分_1円_1864aa2a.PDF new file mode 100644 index 0000000..4fb1789 --- /dev/null +++ b/generated_pdfs/20260222(請求書)11_請求書分_1円_1864aa2a.PDF @@ -0,0 +1,80 @@ +%PDF-1.3 +% ReportLab Generated PDF document (opensource) +1 0 obj +<< +/F1 2 0 R /F2 3 0 R /F3 4 0 R +>> +endobj +2 0 obj +<< +/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font +>> +endobj +3 0 obj +<< +/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font +>> +endobj +4 0 obj +<< +/BaseFont /ZapfDingbats /Name /F3 /Subtype /Type1 /Type /Font +>> +endobj +5 0 obj +<< +/Contents 9 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 8 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +6 0 obj +<< +/PageMode /UseNone /Pages 8 0 R /Type /Catalog +>> +endobj +7 0 obj +<< +/Author (SELF001) /CreationDate (D:20260222041212+09'00') /Creator (anonymous) /Keywords (payload_hash=0fdabffbd9c9124a38b9a862328c0845131be5d765c49943861f0d0a96106982,chain_hash=6dcb3e92c0a77e650c042edabebeae8fce1fbd540acd9a7f33b397c5c91f3ce5,node_id=662182dc-8024-43ee-bba5-7072917b95fe) /ModDate (D:20260222041212+09'00') /Producer (ReportLab PDF Library - \(opensource\)) + /Subject (6aedfc62-3182-43a6-a6f7-90fca17ea768) /Title (\376\377\212\313lBf\370\000 \0002\0000\0002\0006\0000\0002\0002\0002\000-\0000\0004\0001\0002) /Trapped /False +>> +endobj +8 0 obj +<< +/Count 1 /Kids [ 5 0 R ] /Type /Pages +>> +endobj +9 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1075 +>> +stream +Gatm9D/Yq6&H;*)JbX>Db]_mrL&7/oM3KEo;tX+hW[kF]92#KT"VrVSjo=1?m``Kf+gl')3gSTiBBFJO(l3t+,5m!pH/>_Dn!Ede,LkK/*oVp%RnqnQbsfBC4j)SENDO`P-O5H(+;*(iGS4%SLr$G"-[+LVWpBMH\+r(;[s:,iJeW0r!aQ-9#r77s;kUD=C5=X!qN!@uLern1Hq*6D`dElug7t$m_qD$<]F1U_18iGR+'>0,UPB$B>?r,r-]TXP!Tmo7@jDq_m>SBg#gNtt>R[?$S5uBh3&t-6"p>j?[pWmK'*Q9F7^B5GMNF75J)3mC_[$@cE];BW>q$LZ8g7UHN=gadnj9=\ps@,9K#T$+/W->'LZSp`p\T2TUkG*XaXDKC1@]@fiN_bt8*BQ-&Tf?8'?&Em_[MEV?lXsleG+dhn-9FCqEAF5ql.L(nDQK!Z.#!nJcl';B>D@M=j2sO-64q':b9SlkF0<.1[eC0aGH:b,%guI&7SPZ;=)5NME,[F1s>N%QBFlnEb7$=9-4IR"X1rPFR7G"cB7DQB^H62p<7+!A"VD_$]ke"k0$TaT$#j`U*sLLQlHmE>s;NlH2Z^2GKK#,IsnnWo/?F>a7\!Z_,!e5;tYQ:e@A0=FOM#&p=K9anLsjJCTXRXP>nQiE%<\n*KRY`>4($J@su-i=k;fF"]oZe9iJ#HWE#d'K/ft&H?X.>pY\8BqO+X,kroUYV?*M4bh%Nb%h(7XG*T4I@deNdlM,37Oe9)4p](k_qT]eS:?o"9lK/Xkp^T<:2DKL6C*8Y>ZIQ0hFTjo,Idf/LVG4>%HV&Z'Hm*g*A4;uVY!0^PlVq^/Hge/[V;KgW9m-tV#`k&HJ4q<=^!=>'PXI?$K*YBsE2Ic^h5W/j=@3&0D&h]ll"[:`W4qGW=OheM>Ltm&\gpDb4NeCG[4=Q0diu9cXf`@#4\<%[pSC_iZ9R`bendstream +endobj +xref +0 10 +0000000000 65535 f +0000000061 00000 n +0000000112 00000 n +0000000219 00000 n +0000000331 00000 n +0000000414 00000 n +0000000617 00000 n +0000000685 00000 n +0000001252 00000 n +0000001311 00000 n +trailer +<< +/ID +[] +% ReportLab generated PDF document -- digest (opensource) + +/Info 7 0 R +/Root 6 0 R +/Size 10 +>> +startxref +2477 +%%EOF diff --git a/generated_pdfs/20260222(請求書)1_1_1円_8f541afb.PDF b/generated_pdfs/20260222(請求書)1_1_1円_8f541afb.PDF new file mode 100644 index 0000000..ddf142f --- /dev/null +++ b/generated_pdfs/20260222(請求書)1_1_1円_8f541afb.PDF @@ -0,0 +1,80 @@ +%PDF-1.3 +% ReportLab Generated PDF document (opensource) +1 0 obj +<< +/F1 2 0 R /F2 3 0 R /F3 4 0 R +>> +endobj +2 0 obj +<< +/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font +>> +endobj +3 0 obj +<< +/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font +>> +endobj +4 0 obj +<< +/BaseFont /ZapfDingbats /Name /F3 /Subtype /Type1 /Type /Font +>> +endobj +5 0 obj +<< +/Contents 9 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 8 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +6 0 obj +<< +/PageMode /UseNone /Pages 8 0 R /Type /Catalog +>> +endobj +7 0 obj +<< +/Author (SELF001) /CreationDate (D:20260222050236+09'00') /Creator (anonymous) /Keywords (payload_hash=50ee99d9d8689c7f763b601167a7df7f30a345eb5fcee45fea98caa972f5b3d1,chain_hash=e7b67e1aacf316efe5515a01de9c1088211f3097074bf31a0df994f5c9c859d9,node_id=662182dc-8024-43ee-bba5-7072917b95fe) /ModDate (D:20260222050236+09'00') /Producer (ReportLab PDF Library - \(opensource\)) + /Subject (77ea7c76-6507-41ac-a67e-ee8204a0e02a) /Title (\376\377\212\313lBf\370\000 \0002\0000\0002\0006\0000\0002\0002\0002\000-\0000\0005\0000\0002) /Trapped /False +>> +endobj +8 0 obj +<< +/Count 1 /Kids [ 5 0 R ] /Type /Pages +>> +endobj +9 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1125 +>> +stream +Gatm9?&tI7'RcT\JTsef_/:.*=XHUhTOR$R6I!o:l&9P,a]eudnUZOO('U4B?-f^$Du.$dq?MoW1V("pBudqEFhro*)fH3*:>*^;FW#qj%3Y^B8?pJqP9*AMa7FbSLfgB65ZLc0JqAfJmouMfi;`P-<-UTVN>5>_8^jYdJ:.I.Zfl_^5Op=#`?\28Rq;)O^H$FO$Hk_8,po(X)He\j4Q(XL^B6'ZDmEgWL"4h*#<1<=Uf&caXI+rAM*@S-CE?m0P!"ets*Sl/UY)t%@g!+BpRDfr?glZCTV0Jr`Y"*)GkP[8$jJ=T2[YD/\d4f4`'a#c,tG5`r1hRMBlr7P-^,]6EKPmm8Bd&r\=>mI50tIl"Zcp3FL:]:&bUf4ak-psFoa%EVuGE%\I2t`!)OB7DUA"`f.QNq5$u))j)+AOc'OLCDuNEor-a"$r8FQNgoBX'afCCmmW@:GVXg)Q*Utf]Zu)lnFe+bsZ1+)mrfW)RIT*4_F).ifL/aJh\nQ[&b`/Sp<@!hZ2/G6VKB;gX<,V(f>l'koDX$;!J8%:5$AV8JQV7B51ML(YasQh2lVNAq5)=XB97mLK*cS!!Y[4@aOB=hNbB4eB[&]LtH=+g#H4m:DYOL/Pc:lb("k59],`o*E!AuuWSL2&#V1Q1s$>e7@@8rQZ:>9oZ>H>*K)R0-&;6@MhFf0^2OP3T\Ctj.A*TNe0WhU[n6UGO;1n2(=8i\9GJ/ZlX$\8`QVo?%8eHjjbCC7k#"hE,BH9so("Sa%s']mHs17\PIKI]&hs^~>endstream +endobj +xref +0 10 +0000000000 65535 f +0000000061 00000 n +0000000112 00000 n +0000000219 00000 n +0000000331 00000 n +0000000414 00000 n +0000000617 00000 n +0000000685 00000 n +0000001252 00000 n +0000001311 00000 n +trailer +<< +/ID +[<25c9ea8180f2dc042a4e92e7a19986c9><25c9ea8180f2dc042a4e92e7a19986c9>] +% ReportLab generated PDF document -- digest (opensource) + +/Info 7 0 R +/Root 6 0 R +/Size 10 +>> +startxref +2527 +%%EOF diff --git a/generated_pdfs/20260222(請求書)1_新しい商品_1円_d67f16b0.PDF b/generated_pdfs/20260222(請求書)1_新しい商品_1円_d67f16b0.PDF new file mode 100644 index 0000000..4ea73f0 --- /dev/null +++ b/generated_pdfs/20260222(請求書)1_新しい商品_1円_d67f16b0.PDF @@ -0,0 +1,80 @@ +%PDF-1.3 +% ReportLab Generated PDF document (opensource) +1 0 obj +<< +/F1 2 0 R /F2 3 0 R /F3 4 0 R +>> +endobj +2 0 obj +<< +/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font +>> +endobj +3 0 obj +<< +/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font +>> +endobj +4 0 obj +<< +/BaseFont /ZapfDingbats /Name /F3 /Subtype /Type1 /Type /Font +>> +endobj +5 0 obj +<< +/Contents 9 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 8 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +6 0 obj +<< +/PageMode /UseNone /Pages 8 0 R /Type /Catalog +>> +endobj +7 0 obj +<< +/Author (SELF001) /CreationDate (D:20260222051105+09'00') /Creator (anonymous) /Keywords (payload_hash=662182dc-8024-43ee-bba5-7072917b95fe,node_id=2026-02-21 20:09:29) /ModDate (D:20260222051105+09'00') /Producer (ReportLab PDF Library - \(opensource\)) + /Subject (3a807978-61f3-43a8-a463-11b792d2ed12) /Title (\376\377\212\313lBf\370\000 \0002\0000\0002\0006\0000\0002\0002\0002\000-\0000\0005\0000\0009) /Trapped /False +>> +endobj +8 0 obj +<< +/Count 1 /Kids [ 5 0 R ] /Type /Pages +>> +endobj +9 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 667 +>> +stream +Gatm:hhr.&&:X(TOiLI@SBU&,hbH>njCg\]"@9O&5s`#`kXUG8Sq?>!pY

0@&33B>@(#P&K2_lXjp'bMY8pcU:gUU.F/Q_p9fQ>]<'Y?KO5?TTXndA0k_V:f;*a>ISe5s*q?>m&a[0m0mJuI1PFijAP@cE*[%cn-O)4AJhir6.tJt8aYX3XhH`8@R__N&aMV>rel+No^f^+uhEXcV5P_BOU@/tCB:cnT+[?E0d&.RSGMBeoYoqn8,VanOsg\`Xr)CW$efcD.`t;RtTM54@sshEHD$q/X6bH2%*mdsedVDA<#d7W&,+k4]X*~>endstream +endobj +xref +0 10 +0000000000 65535 f +0000000061 00000 n +0000000112 00000 n +0000000219 00000 n +0000000331 00000 n +0000000414 00000 n +0000000617 00000 n +0000000685 00000 n +0000001131 00000 n +0000001190 00000 n +trailer +<< +/ID +[<2928a616d9b2f3d911c24a90037fdead><2928a616d9b2f3d911c24a90037fdead>] +% ReportLab generated PDF document -- digest (opensource) + +/Info 7 0 R +/Root 6 0 R +/Size 10 +>> +startxref +1947 +%%EOF diff --git a/generated_pdfs/20260222(請求書)55_請求書分_25円_f176c22b.PDF b/generated_pdfs/20260222(請求書)55_請求書分_25円_f176c22b.PDF new file mode 100644 index 0000000..562e7f8 --- /dev/null +++ b/generated_pdfs/20260222(請求書)55_請求書分_25円_f176c22b.PDF @@ -0,0 +1,80 @@ +%PDF-1.3 +% ReportLab Generated PDF document (opensource) +1 0 obj +<< +/F1 2 0 R /F2 3 0 R /F3 4 0 R +>> +endobj +2 0 obj +<< +/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font +>> +endobj +3 0 obj +<< +/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font +>> +endobj +4 0 obj +<< +/BaseFont /ZapfDingbats /Name /F3 /Subtype /Type1 /Type /Font +>> +endobj +5 0 obj +<< +/Contents 9 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 8 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +6 0 obj +<< +/PageMode /UseNone /Pages 8 0 R /Type /Catalog +>> +endobj +7 0 obj +<< +/Author (SELF001) /CreationDate (D:20260222041448+09'00') /Creator (anonymous) /Keywords (payload_hash=012d6104f2921c04064d3aa43d28fae3b4c1a73002ac30b4fd5ee36f0a50bb73,chain_hash=848546f0726b72be38f5ff2df9b6902260fcbc738699f39e28f3f3a77de294ba,node_id=662182dc-8024-43ee-bba5-7072917b95fe) /ModDate (D:20260222041448+09'00') /Producer (ReportLab PDF Library - \(opensource\)) + /Subject (7f6354c6-826b-4b1c-b83a-ad90c14ead44) /Title (\376\377\212\313lBf\370\000 \0002\0000\0002\0006\0000\0002\0002\0002\000-\0000\0004\0001\0004) /Trapped /False +>> +endobj +8 0 obj +<< +/Count 1 /Kids [ 5 0 R ] /Type /Pages +>> +endobj +9 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1084 +>> +stream +Gatm9>Ap$$'Roe[5Wim-1S$T0DeY\5W?SrS=-]es6F/Aa<5<']!!>OT1rLe"DQbrroUpU20WpY2`7:-HaH3E8V@jQ,Mk80fDu&MJg&&f=Ru0OeZaS%>c[Gh]tLr,(>OTcaM$Ud3N56%Y'Sn2\kFA2?]C]D@RCQrHUa(0qVq(1TAG-MAjbo!im]Ztt?YMq%!LeAnRBl_DK(e@2unBLj+A8d!9D+.Q%JUPm@+BT2!\EQR;Jg<=4d+i)c+Hl`jA+G0kcCJ9g(UJQY3QSq/]V#>-g$&0#r-gPTClaThB;&GJcFhBFqOcaGJ''.RB:Q8=R@1+1;%QiJ$p,ap7@;(eeL[bqQh'-lku(@1"9F#XaGct/.Y.@>A%LkFruRNn_dKtVE+:__%gO1)n#_@[`!3KZsoj=k.U-ktUc;:OY+*-5^Kj3)i5(cK#p;>#5IP%tj;cp:8Z<]W)UfQ4'8[M=/LOM*>NZoB?)q$Jg.Vti/mHDkChKMePt0Ik+3c^55%^YaM8iI1:?]0AL9^ABY\q7#+JpoC%@C9QsDUo8@(4IDutD!f:B"7`SnZtl579TrGeFmph85:1V!Dem%)n#c2tOj:8PZn9p^AT!u0QV";0!s0W21gTS=D;G6e:Y1&m?,,apL]GnSdOjT9iA"E,R+\jI9^NJe\F6h#(Y#8W3O,G#i1[(kClt0^m])glNe,3nW)mXGS*-o),'$tt"shJn:rf28rQMW_;s`<@PCF%U'"q#FU.oK+TA4qaFR9G0#ZtF1?!XsNh`upF9Z>IfJt(dSJFMI6XX<3MaVH/@')4TfFNZ]:,eg'dri?n\gpE=4NeBdCL7.skA>+cHW+YRHcBg(+%rZ@jKRD+Wkpc[--h,*08],P]krK*NYV68<0q=IFCj@+]X5,XgOQ-,:7'gAYLdG1o\CJdT\^)_a/Fc$8CJ)jaTPCNTW$WLHq2UmVjIs9(?(KsUGL3oVo`*?ouaoC#PN1dbQ~>endstream +endobj +xref +0 10 +0000000000 65535 f +0000000061 00000 n +0000000112 00000 n +0000000219 00000 n +0000000331 00000 n +0000000414 00000 n +0000000617 00000 n +0000000685 00000 n +0000001252 00000 n +0000001311 00000 n +trailer +<< +/ID +[<01d09886f5028a2a9435365de8e79ef4><01d09886f5028a2a9435365de8e79ef4>] +% ReportLab generated PDF document -- digest (opensource) + +/Info 7 0 R +/Root 6 0 R +/Size 10 +>> +startxref +2486 +%%EOF diff --git a/generated_pdfs/20260222(請求書)66_請求書分_36円_4e79d0e3.PDF b/generated_pdfs/20260222(請求書)66_請求書分_36円_4e79d0e3.PDF new file mode 100644 index 0000000..a30fba4 --- /dev/null +++ b/generated_pdfs/20260222(請求書)66_請求書分_36円_4e79d0e3.PDF @@ -0,0 +1,80 @@ +%PDF-1.3 +% ReportLab Generated PDF document (opensource) +1 0 obj +<< +/F1 2 0 R /F2 3 0 R /F3 4 0 R +>> +endobj +2 0 obj +<< +/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font +>> +endobj +3 0 obj +<< +/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font +>> +endobj +4 0 obj +<< +/BaseFont /ZapfDingbats /Name /F3 /Subtype /Type1 /Type /Font +>> +endobj +5 0 obj +<< +/Contents 9 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 8 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +6 0 obj +<< +/PageMode /UseNone /Pages 8 0 R /Type /Catalog +>> +endobj +7 0 obj +<< +/Author (SELF001) /CreationDate (D:20260222041658+09'00') /Creator (anonymous) /Keywords (payload_hash=90f63ed3cdbc2690fcbf2e233cef84ef0022e585d30183d5e1e91483329ed536,chain_hash=97c49f63cf0283a76d335c5b5ac3747a1fba1eedc89d88d7e8f074823545247d,node_id=662182dc-8024-43ee-bba5-7072917b95fe) /ModDate (D:20260222041658+09'00') /Producer (ReportLab PDF Library - \(opensource\)) + /Subject (f3e9954a-a680-4f2a-9f78-0b69030f98ba) /Title (\376\377\212\313lBf\370\000 \0002\0000\0002\0006\0000\0002\0002\0002\000-\0000\0004\0001\0006) /Trapped /False +>> +endobj +8 0 obj +<< +/Count 1 /Kids [ 5 0 R ] /Type /Pages +>> +endobj +9 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1082 +>> +stream +Gatm9>Ap;q'RnB35Wm;oK'OAqn@!2CdT3EE+rRUl6-fPa'D$uDD`4bP.n,;rpOAh3\-`7U!PFNUuKE@!t`l*dC,sr*@2&NSSUJ92oDnPJp$i[SQa&%j!*`AYHJKR7uE:b,`C9e_Pb[(1f(edI7@MW;bEu&7GaY6hKa]17:=e)df5[WdTI;#3lZcom+A#lg2&7:%r$pR5K[aN`s.;7%i-:@DeWCJRJg*\;F-)fL4,,-:caGJ''1lRY(,M!j1+1;%=9paD8N^(gUHE'rfn)">9oQ;p>HEl-cJ_/f5aO3W7**63N&[1gX#Y3X=QFj>M;h(=GdIq*(QR-%bDB>9,_mfnU/1pj*5rEfkEn'#).2Mfa0>([MULb.8??)25R/IR=R"=G@*bh'fiI*l6h\B-dGg8?k:==VT(^Xcl(OE>&Oqmn_l+OkN\MVnI_YjN_u'(mGCP"QIJ77Jo5D)sa]C\_oj0M:C"X1IIF)h4R`8/$#NB+f=l3@\lSbkFBAleOIR;+DhDK'*h&G[4,F.GomQ.QUbJLWoWmXAe)?`BWBY*'XS&YF%:Y1&uXBW7t,/?%.FGkE26kW*gbY?[?]!85_gcMXe"Z<9.:9<[aGUT$d;eA(-qKf'oA"`?IEXH)eMPAQr7%pVUK>d$nlM0tOTBe+FbIp&Hnuol2U3CC2@T+!R#8nDb?Me>KG/eR!AH8E&+sFO9VX3OYkAcM(&rpdlQ.K<#A"_!X;-+3%VE=el[?RrD6%"1bAP@H[!D@40Wk>g4dqBosh&HBeh@endstream +endobj +xref +0 10 +0000000000 65535 f +0000000061 00000 n +0000000112 00000 n +0000000219 00000 n +0000000331 00000 n +0000000414 00000 n +0000000617 00000 n +0000000685 00000 n +0000001252 00000 n +0000001311 00000 n +trailer +<< +/ID +[<57920ecfd0977d95960c71d8bad504ba><57920ecfd0977d95960c71d8bad504ba>] +% ReportLab generated PDF document -- digest (opensource) + +/Info 7 0 R +/Root 6 0 R +/Size 10 +>> +startxref +2484 +%%EOF diff --git a/generated_pdfs/20260222(請求書)77_請求書分_49円_64fe98b2.PDF b/generated_pdfs/20260222(請求書)77_請求書分_49円_64fe98b2.PDF new file mode 100644 index 0000000..a6fc92f --- /dev/null +++ b/generated_pdfs/20260222(請求書)77_請求書分_49円_64fe98b2.PDF @@ -0,0 +1,80 @@ +%PDF-1.3 +% ReportLab Generated PDF document (opensource) +1 0 obj +<< +/F1 2 0 R /F2 3 0 R /F3 4 0 R +>> +endobj +2 0 obj +<< +/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font +>> +endobj +3 0 obj +<< +/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font +>> +endobj +4 0 obj +<< +/BaseFont /ZapfDingbats /Name /F3 /Subtype /Type1 /Type /Font +>> +endobj +5 0 obj +<< +/Contents 9 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 8 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +6 0 obj +<< +/PageMode /UseNone /Pages 8 0 R /Type /Catalog +>> +endobj +7 0 obj +<< +/Author (SELF001) /CreationDate (D:20260222042006+09'00') /Creator (anonymous) /Keywords (payload_hash=a53f29c42e97fe09d71917e441a34a049c87341b989851217881e22c3f2a3a36,chain_hash=129dd46213dd83955ed0f17a9ef82c19d985154cd52ff634ed3b1f9f5b3bfcda,node_id=662182dc-8024-43ee-bba5-7072917b95fe) /ModDate (D:20260222042006+09'00') /Producer (ReportLab PDF Library - \(opensource\)) + /Subject (df40aa49-4cb1-43d4-a481-0702c391cfd3) /Title (\376\377\212\313lBf\370\000 \0002\0000\0002\0006\0000\0002\0002\0002\000-\0000\0004\0002\0000) /Trapped /False +>> +endobj +8 0 obj +<< +/Count 1 /Kids [ 5 0 R ] /Type /Pages +>> +endobj +9 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1085 +>> +stream +Gatm9>Ap;q'RnB35Wm;oK'O@6n@!2CdT3EE+rRUl6-fPa'D$uDD`4bP.n,;rpOAh3\-`7Q6:3(As&IKdm)A>o`.LIo/emQGAN&FDZ]^`F[0DO;9*CE+"GY!Cg"bHjp_t#h7e7:@)RCi>Y+bHC^M%L(n@\6M6>l2WBa>oq)[@L+XeP_D'&N`S_CL^B6'ZKE>d)g;]d[%D2@p.%Tf9Iscs2e5[N&[;m\*0YWMHq/J'j(/VDi&!1*^2:,=IQ3)r5lk92#SJ9g?S^VKqQ/i#.:Ua$`Y"*)Gk#GdS1bke@?qS3e4:'J7`[7br&KWTrm*<37Phd&G7J%KG)9\hoO9)s/de8(&7hkaC,3$V<9n?Nn\u`!@BsQd8F5p_?j9F(C$bIgbL-Zk-t:)m*#FVi+)ZPBrU@`%oTscOI(Xn%A,>e8[l;E15;Vt=232J)'IJ7TD-*1G:](IW?i+!`=]=WEX]9#m;s&NqkX=8bC>[hdl[pFIkH4/5YQ`4e['cp^7b&C[;M&SnhVXgBrV01?I]fbV\iV+O1n\rKGsZM&"XrL[lYG>/e3<;kf'.dT<'Mb]F9=Xic%@H$SMko0e*Sm:GSrMq/i?)FBdf]P:%p5MkU/KAq?q6t\K6k+KqK]cGnrf8npiNuKAB:VJ]&0<.!KZ5BEtWDq?ZtlOOU<('7BZ5\S:o?!lX[SKsLCeTRG\Z0"]`CK"ks"p?Jn<.SgSerN7cTeTkm=GlEl+hVO`F.,\3iP@gi;d^H>aQ\rqQp20tuelYptD`t#G6Kl$?r^"4<\rMo*?qAbm$]^M3EPSn$JSK6I.(Bij/2>CO9X5jTrmTr0rd*[3r<$G3IA[~>endstream +endobj +xref +0 10 +0000000000 65535 f +0000000061 00000 n +0000000112 00000 n +0000000219 00000 n +0000000331 00000 n +0000000414 00000 n +0000000617 00000 n +0000000685 00000 n +0000001252 00000 n +0000001311 00000 n +trailer +<< +/ID +[<879018508aff289d4299c03909daffe1><879018508aff289d4299c03909daffe1>] +% ReportLab generated PDF document -- digest (opensource) + +/Info 7 0 R +/Root 6 0 R +/Size 10 +>> +startxref +2487 +%%EOF diff --git a/generated_pdfs/20260222(請求書)88_新しい商品_0円_60b38dbf.PDF b/generated_pdfs/20260222(請求書)88_新しい商品_0円_60b38dbf.PDF new file mode 100644 index 0000000..172fed9 --- /dev/null +++ b/generated_pdfs/20260222(請求書)88_新しい商品_0円_60b38dbf.PDF @@ -0,0 +1,80 @@ +%PDF-1.3 +% ReportLab Generated PDF document (opensource) +1 0 obj +<< +/F1 2 0 R /F2 3 0 R /F3 4 0 R +>> +endobj +2 0 obj +<< +/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font +>> +endobj +3 0 obj +<< +/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font +>> +endobj +4 0 obj +<< +/BaseFont /ZapfDingbats /Name /F3 /Subtype /Type1 /Type /Font +>> +endobj +5 0 obj +<< +/Contents 9 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 8 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +6 0 obj +<< +/PageMode /UseNone /Pages 8 0 R /Type /Catalog +>> +endobj +7 0 obj +<< +/Author (SELF001) /CreationDate (D:20260222043242+09'00') /Creator (anonymous) /Keywords (payload_hash=662182dc-8024-43ee-bba5-7072917b95fe,chain_hash=6308ea7dd46d1e712d09320af0812c99d27cbce5f9b4f40f06796f1a1bdf4fdf,node_id=2026-02-21 19:32:03) /ModDate (D:20260222043242+09'00') /Producer (ReportLab PDF Library - \(opensource\)) + /Subject (604e194d-7513-4723-8614-9505a0325a0e) /Title (\376\377\212\313lBf\370\000 \0002\0000\0002\0006\0000\0002\0002\0002\000-\0000\0004\0003\0002) /Trapped /False +>> +endobj +8 0 obj +<< +/Count 1 /Kids [ 5 0 R ] /Type /Pages +>> +endobj +9 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 612 +>> +stream +Gatm99kt\&&A@sBkdW3d3Zn4j/T"EW$$J\&66M@WTnPg!`^\Y%h;e\>29E.nG^3Ya4bQ)Z1>."nJdZ4.&/niSP5'ScfW=:QSkB"3SjMcd++%k'iR-Mf3'8$ujLQ8oCL0%IcKC`Tqb_1po7g2r0V[,Bh=Dk=n-:ic96jUFP4K!mKG?q;36,J^%,Ga8DmbCMklJMM=O`1'FM?iI3PCnQGD#T!l!`djG??q*f9H:n^pn_V"Xl)>WTju1)DH'8E;kB'gUks=Km.X,]9YWeVauO)`XTm#],sMND0\GK!o6#EOrqc0Vec"n/lGMY[0AFa?A0h.lAQF9]4?qCl~>endstream +endobj +xref +0 10 +0000000000 65535 f +0000000061 00000 n +0000000112 00000 n +0000000219 00000 n +0000000331 00000 n +0000000414 00000 n +0000000617 00000 n +0000000685 00000 n +0000001207 00000 n +0000001266 00000 n +trailer +<< +/ID +[] +% ReportLab generated PDF document -- digest (opensource) + +/Info 7 0 R +/Root 6 0 R +/Size 10 +>> +startxref +1968 +%%EOF diff --git a/generated_pdfs/20260222(請求書)88_請求書分_0円_07e20816.PDF b/generated_pdfs/20260222(請求書)88_請求書分_0円_07e20816.PDF new file mode 100644 index 0000000..7c041e5 --- /dev/null +++ b/generated_pdfs/20260222(請求書)88_請求書分_0円_07e20816.PDF @@ -0,0 +1,80 @@ +%PDF-1.3 +% ReportLab Generated PDF document (opensource) +1 0 obj +<< +/F1 2 0 R /F2 3 0 R /F3 4 0 R +>> +endobj +2 0 obj +<< +/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font +>> +endobj +3 0 obj +<< +/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font +>> +endobj +4 0 obj +<< +/BaseFont /ZapfDingbats /Name /F3 /Subtype /Type1 /Type /Font +>> +endobj +5 0 obj +<< +/Contents 9 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 8 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +6 0 obj +<< +/PageMode /UseNone /Pages 8 0 R /Type /Catalog +>> +endobj +7 0 obj +<< +/Author (SELF001) /CreationDate (D:20260222043203+09'00') /Creator (anonymous) /Keywords (payload_hash=6308ea7dd46d1e712d09320af0812c99d27cbce5f9b4f40f06796f1a1bdf4fdf,chain_hash=d96ac643a3805abb53cb7b774f884b3ac7024e7b665cc913193c9c82226a15ac,node_id=662182dc-8024-43ee-bba5-7072917b95fe) /ModDate (D:20260222043203+09'00') /Producer (ReportLab PDF Library - \(opensource\)) + /Subject (604e194d-7513-4723-8614-9505a0325a0e) /Title (\376\377\212\313lBf\370\000 \0002\0000\0002\0006\0000\0002\0002\0002\000-\0000\0004\0003\0002) /Trapped /False +>> +endobj +8 0 obj +<< +/Count 1 /Kids [ 5 0 R ] /Type /Pages +>> +endobj +9 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1077 +>> +stream +Gatm9D/Z1=&H88.Z/Uu9'XmnYZLI88?DX\t8ZQ$[Whr*"Q4"G35$4OKVu6;7@L%P1EK(l>ZIcE=o?9>g0Mk0q6\k-37d0_$qX,_g-pu_j;1So)IRNY6=+d%e-CgVUXA3qb9G'fH,8O,j_FXn6>\aW)B0i#aC#5NMh[B7gDau3(JIEj/2'=N_8+M8PF0YEOrEMA:Q9dWfk5nuH:7$;+6ZqL@2RJ>9qU<`_mY-k88tcQKP2pDBK45J+Ls:T-OH7*[Q*kZfed@f\PZ5r/G<[E=s)r`&J+59mn'gDR.@\1"&%_.#ds/.l5eUqdlT8lsh'R9u=njFe0;a,+n!\@Uh&GZGU,:2#h/s8:ObIN8Zq?doemM;]9[iV4hDFY]pN5faWHC`*Idd[8);>c9Eu#E!Nk*:.=uS@EO*g&4H9"^2TBg6M"[WT<3\RW'k+rq>\ehu9*7C(+UBdW77!UfYK#iPl\@Y:S$nsIVsdo[VQVHl4>.?2C0q"pk>UfSHX3Q=n!U2(qH0.B:Cal4XtHj,H"%4nr[19aHZ.W3raS=@R\a#M[!h%j<)glEG8l@F`hjJsMk!9Fd!'E@K6oZgSRBK'J*<1(9d>et@6h@V6H4EWO-^pl,Y/5Sn>3graeS^u'tX5X.]dtu~>endstream +endobj +xref +0 10 +0000000000 65535 f +0000000061 00000 n +0000000112 00000 n +0000000219 00000 n +0000000331 00000 n +0000000414 00000 n +0000000617 00000 n +0000000685 00000 n +0000001252 00000 n +0000001311 00000 n +trailer +<< +/ID +[] +% ReportLab generated PDF document -- digest (opensource) + +/Info 7 0 R +/Root 6 0 R +/Size 10 +>> +startxref +2479 +%%EOF diff --git a/generated_pdfs/20260222(請求書)8_8_64円_bad93f2b.PDF b/generated_pdfs/20260222(請求書)8_8_64円_bad93f2b.PDF new file mode 100644 index 0000000..0f56edf --- /dev/null +++ b/generated_pdfs/20260222(請求書)8_8_64円_bad93f2b.PDF @@ -0,0 +1,80 @@ +%PDF-1.3 +% ReportLab Generated PDF document (opensource) +1 0 obj +<< +/F1 2 0 R /F2 3 0 R /F3 4 0 R +>> +endobj +2 0 obj +<< +/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font +>> +endobj +3 0 obj +<< +/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font +>> +endobj +4 0 obj +<< +/BaseFont /ZapfDingbats /Name /F3 /Subtype /Type1 /Type /Font +>> +endobj +5 0 obj +<< +/Contents 9 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 8 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +6 0 obj +<< +/PageMode /UseNone /Pages 8 0 R /Type /Catalog +>> +endobj +7 0 obj +<< +/Author (SELF001) /CreationDate (D:20260222043354+09'00') /Creator (anonymous) /Keywords (payload_hash=6f14e33e1c394977b23bfd2d844a248cab0f0f7e770b252ddf3c5795d22ecf38,chain_hash=b8509d45bac7636bf8fa08647d343fe51eeb65295660499a82a0396bda80fc36,node_id=662182dc-8024-43ee-bba5-7072917b95fe) /ModDate (D:20260222043354+09'00') /Producer (ReportLab PDF Library - \(opensource\)) + /Subject (a77bd46c-d902-49c4-98c2-946d8c22fb75) /Title (\376\377\212\313lBf\370\000 \0002\0000\0002\0006\0000\0002\0002\0002\000-\0000\0004\0003\0003) /Trapped /False +>> +endobj +8 0 obj +<< +/Count 1 /Kids [ 5 0 R ] /Type /Pages +>> +endobj +9 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1140 +>> +stream +Gatm9>Ar7S(k'`6&G5$!&]^A38oNP:!\K;*_i+Z91'k0Qj"kR*8-]3Z5V0p50&YKo.kcS35/Z.r#p3N\MQ,UJR+UW*R*MS,$d/D)oPMl:V!Y,O\b5;c[&T\meJ08besP)Q]Wnd_Xc$@32?-\7Zri4ol#i#Bi;?P#2LCVg?=,mZ]\(nBbE5hPgfALt>SL99q_[jPSL`@hK:/u$[>p12%VCKNF*LS2oI",O$.+%EN.C)jMLW2ObXh.9;bkh6L%45sSX8_4kdAPH.CXb!)gF[O#9qe[?lgKSN"&;nrDak@N?^s$q"`FW#h<M[&q=QTMX-^\k'-*m]'"4ljXE1g]H2#kBWO_#^K[`)=3`lJggWoiMKbI1r\&4188sUZtL-/eHc#]0!7oWmbL]F\m8U/B\S&/q"o#UXDq8B:F5L'8Gik;f4if;cP;$E7FT1RQu\7o/Ftchf_6YWd+*.Mpj$JPV+nBOjXr]YM4:^s`qQrpKrp]1H6S;:Cf=p3EW[F0bqFd3^K:f8`9V@/B#N]7W%j04@6k"211A/EJ5?,4X(]RYqCWpB$UV0An%.?^bo/mll]us'HSf`D;k,i4o~>endstream +endobj +xref +0 10 +0000000000 65535 f +0000000061 00000 n +0000000112 00000 n +0000000219 00000 n +0000000331 00000 n +0000000414 00000 n +0000000617 00000 n +0000000685 00000 n +0000001252 00000 n +0000001311 00000 n +trailer +<< +/ID +[<97d9697378727fc55e319f93ce285125><97d9697378727fc55e319f93ce285125>] +% ReportLab generated PDF document -- digest (opensource) + +/Info 7 0 R +/Root 6 0 R +/Size 10 +>> +startxref +2542 +%%EOF diff --git a/generated_pdfs/20260222(請求書)9_9_81円_4d208bf7.PDF b/generated_pdfs/20260222(請求書)9_9_81円_4d208bf7.PDF new file mode 100644 index 0000000..24ae8b5 --- /dev/null +++ b/generated_pdfs/20260222(請求書)9_9_81円_4d208bf7.PDF @@ -0,0 +1,80 @@ +%PDF-1.3 +% ReportLab Generated PDF document (opensource) +1 0 obj +<< +/F1 2 0 R /F2 3 0 R /F3 4 0 R +>> +endobj +2 0 obj +<< +/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font +>> +endobj +3 0 obj +<< +/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font +>> +endobj +4 0 obj +<< +/BaseFont /ZapfDingbats /Name /F3 /Subtype /Type1 /Type /Font +>> +endobj +5 0 obj +<< +/Contents 9 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 8 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +6 0 obj +<< +/PageMode /UseNone /Pages 8 0 R /Type /Catalog +>> +endobj +7 0 obj +<< +/Author (SELF001) /CreationDate (D:20260222045954+09'00') /Creator (anonymous) /Keywords (payload_hash=662182dc-8024-43ee-bba5-7072917b95fe,chain_hash=056e3b862c025bd6dd40416499f1508e7ab5b79b0e5d367f9dc22e4b5b275051,node_id=2026-02-21 19:59:50) /ModDate (D:20260222045954+09'00') /Producer (ReportLab PDF Library - \(opensource\)) + /Subject (d6c91d92-22e4-47e6-9790-1a4d65886057) /Title (\376\377\212\313lBf\370\000 \0002\0000\0002\0006\0000\0002\0002\0002\000-\0000\0004\0005\0009) /Trapped /False +>> +endobj +8 0 obj +<< +/Count 1 /Kids [ 5 0 R ] /Type /Pages +>> +endobj +9 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 636 +>> +stream +Gatm9gPQ\"&:L1SaC?`&eg8pn>BPXbAO`p:"CWAT"^spnru\>&]pZUQbR=sj9h/7lm\JSU7eYlqP#S>%?NfPMi$IZ2Hos#<^2EZYZ>a@:(MI*s`[3.pC1XJ!``iK0XY388Oh+*b/5EWHLDgA.KKuRN4I#m.,dFtmtuXiqhQTPtoe63NFR,Yc]haB,WYUIpFnrh$UWIT,+DAk-i3&;6m']d<=pC<`G:$S5&hae_#m$a31:`%1[XKOdf5<+cp82C(QNBOEZ-:LP98O?E[V.fM`endstream +endobj +xref +0 10 +0000000000 65535 f +0000000061 00000 n +0000000112 00000 n +0000000219 00000 n +0000000331 00000 n +0000000414 00000 n +0000000617 00000 n +0000000685 00000 n +0000001207 00000 n +0000001266 00000 n +trailer +<< +/ID +[] +% ReportLab generated PDF document -- digest (opensource) + +/Info 7 0 R +/Root 6 0 R +/Size 10 +>> +startxref +1992 +%%EOF diff --git a/models/invoice_models.py b/models/invoice_models.py index 76e0eb1..0949b6a 100644 --- a/models/invoice_models.py +++ b/models/invoice_models.py @@ -15,6 +15,7 @@ class DocumentType(Enum): INVOICE = "請求書" RECEIPT = "領収書" SALES = "売上伝票" + DRAFT = "下書き" # 下書きタイプを追加 class Product: """商品マスタモデル""" @@ -135,7 +136,9 @@ class Invoice: is_shared: bool = False, document_type: DocumentType = DocumentType.INVOICE, file_path: Optional[str] = None, - uuid: Optional[str] = None): + uuid: Optional[str] = None, + is_draft: bool = False, # 下書きフラグ + draft_parent_uuid: Optional[str] = None): # 下書き元のUUID import uuid as uuid_module self.uuid = uuid or str(uuid_module.uuid4()) self.customer = customer @@ -146,6 +149,8 @@ class Invoice: self.is_shared = is_shared self.document_type = document_type self.file_path = file_path + self.is_draft = is_draft + self.draft_parent_uuid = draft_parent_uuid def _generate_invoice_number(self) -> str: """請求書番号を生成""" diff --git a/services/app_service.py b/services/app_service.py index 63ad98f..549527a 100644 --- a/services/app_service.py +++ b/services/app_service.py @@ -87,12 +87,12 @@ class InvoiceService: invoice.company_info_version = "v1" invoice.submitted_to_tax_authority = False - def create_invoice(self, - customer: Customer, - document_type: DocumentType, - amount: int, - notes: str = "") -> Optional[Invoice]: - """新規伝票作成 + def create_draft_invoice(self, + customer: Customer, + document_type: DocumentType, + amount: int, + notes: str = "") -> Optional[Invoice]: + """下書き伝票作成 Args: customer: 顧客情報 @@ -104,12 +104,65 @@ class InvoiceService: 作成されたInvoice、失敗時はNone """ try: - # 初期明細作成 - items = [InvoiceItem( - description=f"{document_type.value}分", - quantity=1, - unit_price=amount - )] + # 下書き伝票を作成 + invoice = Invoice( + customer=customer, + date=datetime.now(), + items=[], # 下書きは空の明細で開始 + document_type=document_type, + notes=notes, + is_draft=True # 下書きフラグを設定 + ) + + # DBに保存 + saved_invoice = self.invoice_repo.save_invoice(invoice) + + if saved_invoice: + logging.info(f"下書き伝票作成成功: {saved_invoice.invoice_number}") + return saved_invoice + else: + logging.error("下書き伝票作成失敗") + return None + + except Exception as e: + logging.error(f"下書き伝票作成エラー: {e}") + return None + + def create_invoice(self, + customer: Customer, + document_type: DocumentType, + amount: int, + notes: str = "", + items: List[InvoiceItem] = None) -> Optional[Invoice]: + """新規伝票作成 + + Args: + customer: 顧客情報 + document_type: 帳票種類 + amount: 金額(税抜) + notes: 備考 + items: 明細リスト(指定しない場合はダミーを作成) + + Returns: + 作成されたInvoice、失敗時はNone + """ + try: + # 明細作成(指定された明細を使用、なければダミーを作成) + if items is None: + items = [InvoiceItem( + description=f"{document_type.value}分", + quantity=1, + unit_price=amount + )] + + # 空の明細リストの場合は保存しない(全て空の場合のみ) + if not items or all(item.description == "" and item.quantity == 0 and item.unit_price == 0 for item in items): + logging.warning(f"明細が空のため伝票作成を中止: {customer.name}") + return None + + # 空行をフィルタリングせず、そのまま保存(ユーザーが意図した空行を保持) + # TODO: 必要に応じて空行を除外するオプションを提供 + filtered_items = items # 伝票作成 invoice = Invoice( @@ -121,7 +174,8 @@ class InvoiceService: ) # --- 長期保管向け: canonical payload + hash chain --- - self._apply_audit_fields(invoice, customer) + # TODO: ユーザーが明示的に要求した場合のみ実行するべき + # self._apply_audit_fields(invoice, customer) # DB保存(PDFは仮生成物なので保存の成否と切り離す) invoice.file_path = None @@ -131,16 +185,17 @@ class InvoiceService: logging.error("伝票DB保存失敗") return None - # PDF生成(任意・仮生成物) - try: - pdf_path = self.pdf_generator.generate_invoice_pdf(invoice, self.company_info) - if pdf_path: - invoice.file_path = pdf_path - invoice.pdf_generated_at = datetime.now().replace(microsecond=0).isoformat() - else: - logging.warning("PDF生成失敗(DB保存は完了)") - except Exception as e: - logging.warning(f"PDF生成例外(DB保存は完了): {e}") + # PDF生成はユーザーが明示的に要求した場合のみ実行 + # TODO: 自動生成は無効化するべき + # try: + # pdf_path = self.pdf_generator.generate_invoice_pdf(invoice, self.company_info) + # if pdf_path: + # invoice.file_path = pdf_path + # invoice.pdf_generated_at = datetime.now().replace(microsecond=0).isoformat() + # else: + # logging.warning("PDF生成失敗(DB保存は完了)") + # except Exception as e: + # logging.warning(f"PDF生成例外(DB保存は完了): {e}") return invoice @@ -254,6 +309,22 @@ class InvoiceService: logging.warning(f"PDF削除失敗: {e}") return False + def update_invoice(self, invoice: Invoice) -> bool: + """伝票を更新""" + try: + return self.invoice_repo.update_invoice(invoice) + except Exception as e: + logging.error(f"伝票更新エラー: {e}") + return False + + def delete_invoice_by_uuid(self, invoice_uuid: str) -> bool: + """UUIDで伝票を削除""" + try: + return self.invoice_repo.delete_invoice_by_uuid(invoice_uuid) + except Exception as e: + logging.error(f"伝票削除エラー: {e}") + return False + def delete_invoice(self, invoice_id: int) -> bool: """伝票を削除""" return self.invoice_repo.delete_invoice(invoice_id) @@ -282,7 +353,7 @@ class CustomerService: self._customer_cache: List[Customer] = [] self._load_customers() - def create_customer(self, name: str, formal_name: str, address: str = "", phone: str = "") -> Customer: + def create_customer(self, name: str, formal_name: str, address: str = "", phone: str = "") -> int: """顧客を新規作成""" customer = Customer( id=0, # 新規はID=0 @@ -295,9 +366,10 @@ class CustomerService: if success: self._customer_cache.append(customer) logging.info(f"新規顧客登録: {formal_name}") + return customer.id # IDを返す else: logging.error(f"新規顧客登録失敗: {formal_name}") - return customer if success else None + return 0 # 失敗時は0を返す def _load_customers(self): """顧客データを読み込み""" diff --git a/services/repositories.py b/services/repositories.py index 062e933..809630c 100644 --- a/services/repositories.py +++ b/services/repositories.py @@ -294,9 +294,9 @@ class InvoiceRepository: invoice.customer.formal_name, invoice.customer.address, invoice.customer.phone, - invoice.subtotal, - invoice.tax, - invoice.total_amount, + 0, # amount - 計算値を保存しない + 0, # tax - 計算値を保存しない + 0, # total_amount - 計算値を保存しない invoice.date.isoformat(), invoice.invoice_number, invoice.notes, @@ -338,6 +338,71 @@ class InvoiceRepository: logging.error(f"伝票保存エラー: {e}") return False + def update_invoice(self, invoice: Invoice) -> bool: + """伝票を更新""" + try: + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + + # 伝票基本情報を更新 + cursor.execute(''' + UPDATE invoices + SET document_type = ?, customer_id = ?, date = ?, + invoice_number = ?, notes = ?, pdf_generated_at = ? + WHERE uuid = ? + ''', ( + invoice.document_type.value, + invoice.customer.id if hasattr(invoice.customer, 'id') else None, + invoice.date.isoformat(), + invoice.invoice_number, + invoice.notes, + getattr(invoice, 'pdf_generated_at', None), + invoice.uuid + )) + + # UUIDからinvoice_idを取得 + cursor.execute('SELECT id FROM invoices WHERE uuid = ?', (invoice.uuid,)) + result = cursor.fetchone() + if not result: + logging.error(f"伝票ID取得失敗: {invoice.uuid}") + return False + invoice_id = result[0] + + # 明細を一度削除して再挿入 + cursor.execute('DELETE FROM invoice_items WHERE invoice_id = ?', (invoice_id,)) + + for item in invoice.items: + cursor.execute(''' + INSERT INTO invoice_items (invoice_id, description, quantity, unit_price, is_discount) + VALUES (?, ?, ?, ?, ?) + ''', ( + invoice_id, # 正しいinvoice_idを使用 + item.description, + item.quantity, + item.unit_price, + item.is_discount + )) + + conn.commit() + return True + + except Exception as e: + logging.error(f"伝票更新エラー: {e}") + return False + + def delete_invoice_by_uuid(self, invoice_uuid: str) -> bool: + """UUIDで伝票を削除""" + try: + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute('DELETE FROM invoice_items WHERE invoice_id = (SELECT id FROM invoices WHERE uuid = ?)', (invoice_uuid,)) + cursor.execute('DELETE FROM invoices WHERE uuid = ?', (invoice_uuid,)) + conn.commit() + return True + except Exception as e: + logging.error(f"伝票削除エラー: {e}") + return False + def update_invoice_file_path(self, invoice_uuid: str, file_path: str, pdf_generated_at: str): """PDFファイルパスを更新""" try: @@ -419,11 +484,11 @@ class InvoiceRepository: # 顧客情報 customer = Customer( - id=row[3] or 0, - name=row[4], - formal_name=row[4], - address=row[5] or "", - phone=row[6] or "" + id=row[15] or 0, # customer_idフィールド + name=row[3], # customer_nameフィールド + formal_name=row[3], # customer_nameフィールド + address=row[4] or "", # customer_addressフィールド + phone=row[5] or "" # customer_phoneフィールド ) # 伝票タイプ @@ -435,11 +500,11 @@ class InvoiceRepository: inv = Invoice( customer=customer, - date=datetime.fromisoformat(row[10]), + date=datetime.fromisoformat(row[9]), # 正しいdateフィールドのインデックス items=items, - file_path=row[13], - invoice_number=row[11] or "", - notes=row[12], + file_path=row[12], + invoice_number=row[10] or "", # 正しいinvoice_numberフィールドのインデックス + notes=row[11], # 正しいnotesフィールドのインデックス document_type=doc_type, uuid=row[1], ) @@ -570,11 +635,13 @@ class CustomerRepository: customer.email )) + # 挿入したIDを取得してcustomerオブジェクトを更新 + customer.id = cursor.lastrowid conn.commit() return True except Exception as e: - logging.error(f"顧客新規登録エラー: {e}") + logging.error(f"顧客登録エラー: {e}") return False def update_customer(self, customer: Customer) -> bool: