From 042bbe032ddfac23cf579f1f20fa7b1d362a4077 Mon Sep 17 00:00:00 2001 From: joe Date: Sat, 21 Feb 2026 23:49:15 +0900 Subject: [PATCH] =?UTF-8?q?=E8=87=AA=E5=8B=95=E3=81=A7=E7=A9=BA=E8=A1=8C?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app_flutter_style_dashboard.py | 593 +++++++++++++++++- components/customer_picker.py | 4 + ...pdfsSELF001_20260220_214109.pdf_250,000円_350cfecc.PDF | 80 +++ ...·1-1-1_æ±äº¬éƒ½å“å·åŒºæ±å“å·1-1-1_250,000円_0260ba6d.PDF | 80 +++ ...“å·åŒºæ±å“å·1-1-1_請求書分_250,000円_1d3b6ecc.PDF | 80 +++ ...)ä½è—¤å•†äº‹_核弾頭_5,000,000,000円_96ab1e47.PDF | 80 +++ models/invoice_models.py | 46 +- services/app_service.py | 29 +- services/pdf_generator.py | 38 +- services/repositories.py | 89 ++- 10 files changed, 1088 insertions(+), 31 deletions(-) create mode 100644 generated_pdfs/20260220(請求書)æ±äº¬éƒ½å“å·åŒºæ±å“å·1-1-1_generated_pdfsSELF001_20260220_214109.pdf_250,000円_350cfecc.PDF create mode 100644 generated_pdfs/20260220(請求書)æ±äº¬éƒ½å“å·åŒºæ±å“å·1-1-1_æ±äº¬éƒ½å“å·åŒºæ±å“å·1-1-1_250,000円_0260ba6d.PDF create mode 100644 generated_pdfs/20260220(請求書)æ±äº¬éƒ½å“å·åŒºæ±å“å·1-1-1_請求書分_250,000円_1d3b6ecc.PDF create mode 100644 generated_pdfs/20260221(請求書)ä½è—¤å•†äº‹_核弾頭_5,000,000,000円_96ab1e47.PDF diff --git a/app_flutter_style_dashboard.py b/app_flutter_style_dashboard.py index 21bf72e..74eb2a4 100644 --- a/app_flutter_style_dashboard.py +++ b/app_flutter_style_dashboard.py @@ -9,7 +9,7 @@ import sys import logging from datetime import datetime from typing import List, Dict, Optional -from models.invoice_models import DocumentType, Invoice, create_sample_invoices, Customer +from models.invoice_models import DocumentType, Invoice, create_sample_invoices, Customer, InvoiceItem from components.customer_picker import CustomerPickerModal from services.app_service import AppService @@ -24,11 +24,13 @@ class FlutterStyleDashboard: def __init__(self, page: ft.Page): self.page = page - self.current_tab = 0 # 0: æ–°è¦ä½œæˆ, 1: 発行履歴 + self.current_tab = 0 # 0: ä¼ç¥¨ä¸€è¦§, 1: 詳細編集 self.selected_customer = None self.selected_document_type = DocumentType.INVOICE self.amount_value = "250000" self.customer_picker = None + self.editing_invoice = None # 編集中ã®ä¼ç¥¨ + self.is_edit_mode = False # 編集モードフラグ self.is_customer_picker_open = False self.customer_search_query = "" self.show_offsets = False @@ -147,14 +149,103 @@ class FlutterStyleDashboard: 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_input_screen()) + # ä¼ç¥¨ä¸€è¦§ç”»é¢ + self.main_content.controls.append(self.create_slip_list_screen()) else: - # ç™ºè¡Œå±¥æ­´ç”»é¢ - self.main_content.controls.append(self.create_slip_history_screen()) + # è©³ç´°ç·¨é›†ç”»é¢ + self.main_content.controls.append(self.create_invoice_edit_screen()) self.page.update() + def create_slip_list_screen(self) -> ft.Container: + """ä¼ç¥¨ä¸€è¦§ç”»é¢""" + # 履歴データ読ã¿è¾¼ã¿ + slips = self.load_slips() + if not self.show_offsets: + slips = [s for s in slips if not (isinstance(s, Invoice) and getattr(s, "is_offset", False))] + + def on_toggle_offsets(e): + self.show_offsets = bool(e.control.value) + self.update_main_content() + + def on_verify_chain(e=None): + res = self.app_service.invoice.invoice_repo.verify_chain() + self.chain_verify_result = res + self.update_main_content() + + def create_new_slip(_): + """æ–°è¦ä¼ç¥¨ä½œæˆ""" + self.editing_invoice = None + self.current_tab = 1 + self.update_main_content() + + # 履歴カードリスト + slip_cards = [] + for slip in slips: + card = self.create_slip_card(slip) + slip_cards.append(card) + + return ft.Container( + content=ft.Column([ + # ヘッダー + ft.Container( + content=ft.Row([ + ft.Text("📄 ä¼ç¥¨ä¸€è¦§", size=20, weight=ft.FontWeight.BOLD), + ft.Container(expand=True), + ft.IconButton( + ft.Icons.VERIFIED, + tooltip="ãƒã‚§ãƒ¼ãƒ³æ¤œè¨¼", + icon_color=ft.Colors.BLUE_300, + on_click=on_verify_chain, + ), + ft.Row( + [ + ft.Text("赤ä¼ã‚’表示", size=12, color=ft.Colors.WHITE), + ft.Switch(value=self.show_offsets, on_change=on_toggle_offsets), + ], + spacing=5, + ), + ft.IconButton(ft.Icons.CLEAR_ALL, icon_size=20), + ]), + padding=ft.padding.all(15), + bgcolor=ft.Colors.BLUE_GREY, + ), + + # æ¤œè¨¼çµæžœè¡¨ç¤ºï¼ˆã‚れã°ï¼‰ + ft.Container( + content=self._build_chain_verify_result(), + margin=ft.Margin.only(bottom=10), + ) if self.chain_verify_result else ft.Container(height=0), + + # ä¼ç¥¨ãƒªã‚¹ãƒˆ + ft.Column( + controls=slip_cards, + spacing=10, + scroll=ft.ScrollMode.AUTO, + expand=True, + ), + + # æ–°è¦ä½œæˆãƒœã‚¿ãƒ³ + ft.Container( + content=ft.Button( + content=ft.Row([ + ft.Icon(ft.Icons.ADD, color=ft.Colors.WHITE), + ft.Text("æ–°è¦ä¼ç¥¨ã‚’作æˆ", color=ft.Colors.WHITE), + ], alignment=ft.MainAxisAlignment.CENTER), + style=ft.ButtonStyle( + bgcolor=ft.Colors.BLUE_GREY_800, + padding=ft.padding.all(20), + shape=ft.RoundedRectangleBorder(radius=15), + ), + width=400, + height=60, + on_click=create_new_slip, + ), + padding=ft.padding.all(20), + ), + ]), + expand=True, + ) def create_customer_picker_screen(self) -> ft.Container: """é¡§å®¢é¸æŠžç”»é¢ï¼ˆç”»é¢å†…é·ç§»ãƒ»ãƒ€ã‚¤ã‚¢ãƒ­ã‚°ä¸ä½¿ç”¨ï¼‰""" self.customers = self.app_service.customer.get_all_customers() @@ -450,28 +541,52 @@ class FlutterStyleDashboard: logging.info(f"PDF削除完了: {pdf_path}") else: logging.error("PDFå†ç”Ÿæˆå¤±æ•—") + + def edit_invoice(_=None): + if not isinstance(slip, Invoice): + return + self.open_invoice_edit(slip) actions_row = None if isinstance(slip, Invoice): buttons = [] if not getattr(slip, "submitted_to_tax_authority", False): buttons.append( - ft.IconButton( - ft.Icons.REPLAY_CIRCLE_FILLED, - tooltip="赤ä¼ï¼ˆç›¸æ®ºï¼‰ã‚’発行", - icon_color=ft.Colors.RED_400, - on_click=issue_offset, + 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, ) ) - if not getattr(slip, "submitted_to_tax_authority", False): buttons.append( - ft.IconButton( - ft.Icons.CHECK_CIRCLE, - tooltip="税務署æå‡ºæ¸ˆã¿ã«è¨­å®š", - icon_color=ft.Colors.ORANGE_400, - on_click=lambda _: self.submit_invoice_for_tax(slip.uuid), + 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, @@ -480,7 +595,7 @@ class FlutterStyleDashboard: on_click=regenerate_and_share, ) ) - actions_row = ft.Row(buttons, alignment=ft.MainAxisAlignment.END) + actions_row = ft.Row(buttons, alignment=ft.MainAxisAlignment.CENTER, spacing=10) display_amount = amount if isinstance(slip, Invoice) and getattr(slip, "is_offset", False): @@ -547,11 +662,441 @@ class FlutterStyleDashboard: border_radius=8, ) + def open_invoice_edit(self, invoice: Invoice): + """ä¼ç¥¨ç·¨é›†ç”»é¢ã‚’é–‹ã""" + self.editing_invoice = invoice + self.is_edit_mode = True + self.selected_customer = invoice.customer + self.selected_document_type = invoice.document_type + self.amount_value = str(invoice.items[0].unit_price if invoice.items else "0") + self.is_detail_edit_mode = False # åˆæœŸã¯ãƒ“ューモード + self.current_tab = 1 # 詳細編集タブã«åˆ‡ã‚Šæ›¿ãˆ + self.update_main_content() + def open_new_customer_form(self): """æ–°è¦é¡§å®¢ãƒ•ォームを開ã(画é¢å†…é·ç§»ï¼‰""" self.is_new_customer_form_open = True self.update_main_content() + def create_invoice_edit_screen(self) -> ft.Container: + """ä¼ç¥¨ç·¨é›†ç”»é¢""" + if self.editing_invoice: + # 既存ä¼ç¥¨ã®ç·¨é›† + return self._create_edit_existing_screen() + else: + # æ–°è¦ä¼ç¥¨ä½œæˆ + return self.create_slip_input_screen() + + def _create_edit_existing_screen(self) -> ft.Container: + """既存ä¼ç¥¨ã®ç·¨é›†ç”»é¢""" + # 編集ä¸å¯ãƒã‚§ãƒƒã‚¯ + is_locked = getattr(self.editing_invoice, 'submitted_to_tax_authority', False) + is_view_mode = not getattr(self, 'is_detail_edit_mode', False) + + # ä¼ç¥¨ç¨®é¡žé¸æŠžï¼ˆãƒ˜ãƒƒãƒ€ãƒ¼ã«ç§»å‹•) + document_types = list(DocumentType) + + # 明細テーブル + items_table = self._create_items_table(self.editing_invoice.items, is_locked or is_view_mode) + + # 顧客表示 + customer_display = ft.Container( + content=ft.Row([ + ft.Text("顧客: ", weight=ft.FontWeight.BOLD), + ft.Text(self.editing_invoice.customer.name), # 顧客åを表示 + ]), + padding=ft.padding.symmetric(horizontal=10, vertical=5), + bgcolor=ft.Colors.GREY_100, + border_radius=5, + ) + + # 備考フィールド + notes_field = ft.TextField( + label="備考", + value=getattr(self.editing_invoice, 'notes', ''), + disabled=is_locked or is_view_mode, + multiline=True, + min_lines=2, + max_lines=3, + ) + + def toggle_edit_mode(_): + """編集モード切替""" + old_mode = getattr(self, 'is_detail_edit_mode', False) + self.is_detail_edit_mode = not old_mode + logging.debug(f"Toggle edit mode: {old_mode} -> {self.is_detail_edit_mode}") + self.update_main_content() + + def save_changes(_): + if is_locked: + return + + # ä¼ç¥¨ã‚’更新(ç¾åœ¨ã®æ˜Žç´°ã‚’ä¿æŒï¼‰ + # TODO: テーブルã‹ã‚‰å®Ÿéš›ã®å€¤ã‚’å–å¾—ã—ã¦æ›´æ–° + # ç¾åœ¨ã¯æ—¢å­˜ã®æ˜Žç´°ã‚’ç¶­æŒã—ã€å‚™è€ƒã®ã¿æ›´æ–° + self.editing_invoice.notes = notes_field.value + self.editing_invoice.document_type = self.selected_document_type + + # TODO: ãƒãƒƒã‚·ãƒ¥å†è¨ˆç®—ã¨ä¿å­˜ + logging.info(f"ä¼ç¥¨æ›´æ–°: {self.editing_invoice.invoice_number}") + + # ç·¨é›†ãƒ¢ãƒ¼ãƒ‰çµ‚äº†ï¼ˆãƒ“ãƒ¥ãƒ¼ãƒ¢ãƒ¼ãƒ‰ã«æˆ»ã‚‹ï¼‰ + self.is_detail_edit_mode = False # ãƒ“ãƒ¥ãƒ¼ãƒ¢ãƒ¼ãƒ‰ã«æˆ»ã‚‹ + self.update_main_content() + + def cancel_edit(_): + self.is_detail_edit_mode = False + self.is_edit_mode = False + self.editing_invoice = None + self.current_tab = 0 # ä¸€è¦§ã‚¿ãƒ–ã«æˆ»ã‚‹ + self.update_main_content() + + return ft.Container( + content=ft.Column([ + # ヘッダー + ft.Container( + content=ft.Row([ + ft.Container(expand=True), + # コンパクトãªä¼ç¥¨ç¨®é¡žé¸æŠžï¼ˆã‚»ã‚°ãƒ¡ãƒ³ãƒˆåŒ–) + ft.Container( + content=ft.Row([ + ft.GestureDetector( + content=ft.Container( + content=ft.Text( + doc_type.value, + size=10, + color=ft.Colors.WHITE if doc_type == self.editing_invoice.document_type else ft.Colors.GREY_600, + weight=ft.FontWeight.BOLD if doc_type == self.editing_invoice.document_type else ft.FontWeight.NORMAL, + ), + padding=ft.padding.symmetric(horizontal=8, vertical=4), + bgcolor=ft.Colors.BLUE_600 if doc_type == self.editing_invoice.document_type else ft.Colors.GREY_300, + border_radius=ft.border_radius.all(4), + margin=ft.margin.only(right=1), + ), + on_tap=lambda _, dt=doc_type: self.select_document_type(dt.value) if not is_locked and not is_view_mode else None, + ) for doc_type in document_types + ]), + padding=ft.padding.all(2), + bgcolor=ft.Colors.GREY_200, + border_radius=ft.border_radius.all(6), + margin=ft.margin.only(right=10), + ), + ft.ElevatedButton( + content=ft.Text("編集" if is_view_mode else "ä¿å­˜"), + style=ft.ButtonStyle( + bgcolor=ft.Colors.BLUE_600 if is_view_mode else ft.Colors.GREEN_600, + ), + on_click=toggle_edit_mode if is_view_mode else save_changes, + disabled=is_locked, + width=70, + height=30, + ) if not is_locked else ft.Container(), + ft.Container(width=5), + ft.IconButton(ft.Icons.CLOSE, on_click=cancel_edit), + ]), + padding=ft.padding.symmetric(horizontal=15, vertical=8), + bgcolor=ft.Colors.BLUE_GREY, + ), + + # 基本情報行(コンパクトã«ï¼‰ + ft.Container( + content=ft.Row([ + ft.Text(f"{self.editing_invoice.invoice_number} | {self.editing_invoice.date.strftime('%Y/%m/%d')} | {self.editing_invoice.customer.name}", size=12, weight=ft.FontWeight.BOLD), + ft.Container(expand=True), + ]), + padding=ft.padding.symmetric(horizontal=15, vertical=3), + bgcolor=ft.Colors.GREY_50, + ), + + # 明細テーブル(フレキシブルã«ï¼‰ + ft.Container( + content=ft.Column([ + ft.Row([ + ft.Container(expand=True), + ft.IconButton( + ft.Icons.ADD_CIRCLE_OUTLINE, + tooltip="行を追加", + icon_color=ft.Colors.GREEN_600, + disabled=is_locked or is_view_mode, + on_click=lambda _: self._add_item_row(), + ) if not is_locked and not is_view_mode else ft.Container(), + ]), + ft.Container(height=3), + ft.Container( + content=items_table, + height=300, # 高ã•ã‚’ç¸®å° + border=ft.border.all(1, ft.Colors.GREY_300), + border_radius=5, + padding=ft.padding.all(1), # パディングを最å°åŒ– + width=None, # å¹…ã‚’å¯å¤‰ã« + expand=True, # 利用å¯èƒ½ãªå¹…ã‚’å…¨ã¦ä½¿ç”¨ + ), + ft.Container(height=10), + # åˆè¨ˆé‡‘é¡è¡¨ç¤º + ft.Container( + content=ft.Row([ + ft.Container(expand=True), + ft.Text("åˆè¨ˆ: ", size=14, weight=ft.FontWeight.BOLD), + ft.Text( + f"Â¥{sum(item.subtotal for item in self.editing_invoice.items):,}", + size=16, + weight=ft.FontWeight.BOLD, + color=ft.Colors.BLUE_600 + ), + ]), + padding=ft.padding.symmetric(horizontal=10, vertical=8), + bgcolor=ft.Colors.GREY_100, + border_radius=5, + ), + ]), + padding=ft.padding.all(15), + expand=True, # æ˜Žç´°éƒ¨åˆ†ãŒæœ€å¤§é™ã®ã‚¹ãƒšãƒ¼ã‚¹ã‚’å æœ‰ + ), + + # 備考(コンパクト) + ft.Container( + content=ft.Column([ + ft.Text("備考", size=12, weight=ft.FontWeight.BOLD), + ft.Container(height=3), + notes_field, + ft.Container(height=5), + ft.Text("🔒 税務署æå‡ºæ¸ˆã¿ã¯ç·¨é›†ã§ãã¾ã›ã‚“" if is_locked else "✅ " + ("編集モード" if not is_view_mode else "ビューモード"), + size=11, color=ft.Colors.RED_600 if is_locked else (ft.Colors.GREEN_600 if not is_view_mode else ft.Colors.BLUE_600)), + ft.Container(height=10), + # PDF生æˆãƒœã‚¿ãƒ³ã‚’追加 + ft.ElevatedButton( + content=ft.Row([ + ft.Icon(ft.Icons.DOWNLOAD, size=16), + ft.Container(width=5), + ft.Text("PDF生æˆ", size=12, color=ft.Colors.WHITE), + ]), + style=ft.ButtonStyle( + bgcolor=ft.Colors.BLUE_600, + padding=ft.padding.symmetric(horizontal=15, vertical=10), + ), + on_click=lambda _: self.generate_pdf_from_edit(), + width=120, + height=40, + ) if not is_locked else ft.Container(), + ]), + padding=ft.padding.symmetric(horizontal=15, vertical=10), + bgcolor=ft.Colors.GREY_50, + ), + ]), + expand=True, + ) + def generate_pdf_from_edit(self): + """編集画é¢ã‹ã‚‰PDFを生æˆ""" + if not self.editing_invoice: + return + + try: + pdf_path = self.app_service.invoice.regenerate_pdf(self.editing_invoice.uuid) + if pdf_path: + self.editing_invoice.file_path = pdf_path + self.editing_invoice.pdf_generated_at = datetime.now().replace(microsecond=0).isoformat() + logging.info(f"PDF生æˆå®Œäº†: {pdf_path}") + # TODO: æˆåŠŸãƒ¡ãƒƒã‚»ãƒ¼ã‚¸è¡¨ç¤º + else: + logging.error("PDF生æˆå¤±æ•—") + # TODO: エラーメッセージ表示 + except Exception as e: + logging.error(f"PDF生æˆã‚¨ãƒ©ãƒ¼: {e}") + # TODO: エラーメッセージ表示 + + def _add_item_row(self): + """明細行を追加""" + if not self.editing_invoice: + return + + # æ–°ã—ã„æ˜Žç´°è¡Œã‚’追加 + new_item = InvoiceItem( + description="æ–°ã—ã„商å“", + quantity=1, + unit_price=0, + is_discount=False + ) + self.editing_invoice.items.append(new_item) + self.update_main_content() + + def _delete_item_row(self, index: int): + """明細行を削除""" + if not self.editing_invoice or index >= len(self.editing_invoice.items): + return + + # 行を削除(最低1è¡Œã¯æ®‹ã™ï¼‰ + if len(self.editing_invoice.items) > 1: + del self.editing_invoice.items[index] + self.update_main_content() + + def _update_item_field(self, item_index: int, field_name: str, value: str): + """明細フィールドを更新""" + if not self.editing_invoice or item_index >= len(self.editing_invoice.items): + return + + item = self.editing_invoice.items[item_index] + + # デãƒãƒƒã‚°ç”¨ï¼šæ›´æ–°å‰ã®å€¤ã‚’ログ出力 + old_value = getattr(item, field_name) + logging.debug(f"Updating item {item_index} {field_name}: '{old_value}' -> '{value}'") + + if field_name == 'description': + item.description = value + elif field_name == 'quantity': + try: + # 空文字ã®å ´åˆã¯1を設定 + if not value or value.strip() == '': + item.quantity = 1 + else: + item.quantity = int(value) + logging.debug(f"Quantity updated to: {item.quantity}") + except ValueError as e: + item.quantity = 1 + logging.error(f"Quantity update error: {e}") + elif field_name == 'unit_price': + try: + # 空文字ã®å ´åˆã¯0を設定 + if not value or value.strip() == '': + item.unit_price = 0 + else: + item.unit_price = int(value) + logging.debug(f"Unit price updated to: {item.unit_price}") + except ValueError as e: + item.unit_price = 0 + logging.error(f"Unit price update error: {e}") + + # åˆè¨ˆé‡‘é¡ã‚’æ›´æ–°ã™ã‚‹ãŸã‚ã«ç”»é¢ã‚’æ›´æ–° + self.update_main_content() + + def _create_items_table(self, items: List[InvoiceItem], is_locked: bool) -> ft.Column: + """明細テーブルを作æˆï¼ˆç·¨é›†ãƒ¢ãƒ¼ãƒ‰ã¨è¡¨ç¤ºãƒ¢ãƒ¼ãƒ‰ã§åˆ¥ã®è¦‹ã›æ–¹ï¼‰""" + # ビューモード状態をå–å¾— + is_view_mode = not getattr(self, 'is_detail_edit_mode', False) + + # デãƒãƒƒã‚°ç”¨ï¼šãƒ¡ã‚½ãƒƒãƒ‰é–‹å§‹æ™‚ã®çŠ¶æ…‹ã‚’ãƒ­ã‚°å‡ºåŠ› + logging.debug(f"Creating items table: locked={is_locked}, view_mode={is_view_mode}, edit_mode={getattr(self, 'is_detail_edit_mode', False)}") + logging.debug(f"Items count: {len(items)}") + + if is_view_mode or is_locked: + # 表示モード:表形å¼ã§æ•´ç„¶ã¨è¡¨ç¤º + return self._create_view_mode_table(items) + else: + # 編集モード:入力フォーム風ã«è¡¨ç¤º + return self._create_edit_mode_table(items, is_locked) + + def _create_view_mode_table(self, items: List[InvoiceItem]) -> ft.Column: + """表示モード:フレキシブルãªè¡¨å½¢å¼ã§æ•´ç„¶ã¨è¡¨ç¤º""" + # ヘッダー行 + header_row = ft.Row([ + ft.Text("商å“å", size=12, weight=ft.FontWeight.BOLD, expand=True), # å¯å¤‰å¹… + ft.Text("æ•°", size=12, weight=ft.FontWeight.BOLD, width=35), # 固定幅 + ft.Text("å˜ä¾¡", size=12, weight=ft.FontWeight.BOLD, width=70), # 固定幅 + ft.Text("å°è¨ˆ", size=12, weight=ft.FontWeight.BOLD, width=70), # 固定幅 + ft.Container(width=35), # å‰Šé™¤ãƒœã‚¿ãƒ³ç”¨ã‚¹ãƒšãƒ¼ã‚¹ã‚’ç¢ºä¿ + ]) + + # データ行 + data_rows = [] + for i, item in enumerate(items): + row = ft.Row([ + ft.Text(item.description, size=12, expand=True), # å¯å¤‰å¹… + ft.Text(str(item.quantity), size=12, width=35, text_align=ft.TextAlign.RIGHT), # 固定幅 + ft.Text(f"Â¥{item.unit_price:,}", size=12, width=70, text_align=ft.TextAlign.RIGHT), # 固定幅 + ft.Text(f"Â¥{item.subtotal:,}", size=12, weight=ft.FontWeight.BOLD, width=70, text_align=ft.TextAlign.RIGHT), # 固定幅 + ft.Container(width=35), # 削除ボタン用スペース + ]) + data_rows.append(row) + + return ft.Column([ + header_row, + ft.Divider(height=1, color=ft.Colors.GREY_400), + ft.Column(data_rows, scroll=ft.ScrollMode.AUTO, height=250), # 高ã•ã‚’åˆ¶é™ + ]) + + def _create_edit_mode_table(self, items: List[InvoiceItem], is_locked: bool) -> ft.Column: + """編集モード:フレキシブルãªè¡¨å½¢å¼""" + # 編集モードã«å…¥ã£ãŸã‚‰è‡ªå‹•ã§ç©ºè¡Œã‚’追加 + if not any(item.description == "" and item.quantity == 0 and item.unit_price == 0 for item in items): + items.append(InvoiceItem(description="", quantity=0, unit_price=0)) + + # ヘッダー行 + header_row = ft.Row([ + ft.Text("商å“å", size=12, weight=ft.FontWeight.BOLD, expand=True), # å¯å¤‰å¹… + ft.Text("æ•°", size=12, weight=ft.FontWeight.BOLD, width=35), # 固定幅 + ft.Text("å˜ä¾¡", size=12, weight=ft.FontWeight.BOLD, width=70), # 固定幅 + ft.Text("å°è¨ˆ", size=12, weight=ft.FontWeight.BOLD, width=70), # 固定幅 + ft.Container(width=35), # å‰Šé™¤ãƒœã‚¿ãƒ³ç”¨ã‚¹ãƒšãƒ¼ã‚¹ã‚’ç¢ºä¿ + ]) + + # データ行 + data_rows = [] + for i, item in enumerate(items): + # 商å“åフィールド + product_field = ft.TextField( + value=item.description, + text_size=12, + height=28, + width=None, # å¹…ã‚’å¯å¤‰ã« + expand=True, # å¯å¤‰å¹… + border=ft.border.all(1, ft.Colors.BLUE_200), + bgcolor=ft.Colors.WHITE, + content_padding=ft.padding.all(5), # 内部余白を最å°åŒ– + on_change=lambda e: self._update_item_field(i, 'description', e.control.value), + ) + + # æ•°é‡ãƒ•ィールド + quantity_field = ft.TextField( + value=str(item.quantity), + text_size=12, + height=28, + width=35, # 固定幅 + text_align=ft.TextAlign.RIGHT, + border=ft.border.all(1, ft.Colors.BLUE_200), + bgcolor=ft.Colors.WHITE, + content_padding=ft.padding.all(5), # 内部余白を最å°åŒ– + on_change=lambda e: self._update_item_field(i, 'quantity', e.control.value), + keyboard_type=ft.KeyboardType.NUMBER, + ) + + # å˜ä¾¡ãƒ•ィールド + unit_price_field = ft.TextField( + value=f"{item.unit_price:,}", + text_size=12, + height=28, + width=70, # 固定幅 + text_align=ft.TextAlign.RIGHT, + border=ft.border.all(1, ft.Colors.BLUE_200), + bgcolor=ft.Colors.WHITE, + content_padding=ft.padding.all(5), # 内部余白を最å°åŒ– + on_change=lambda e: self._update_item_field(i, 'unit_price', e.control.value.replace(',', '')), + keyboard_type=ft.KeyboardType.NUMBER, + ) + + # 削除ボタン + delete_button = ft.IconButton( + ft.Icons.DELETE_OUTLINE, + tooltip="行を削除", + icon_color=ft.Colors.RED_600, + disabled=is_locked, + icon_size=16, + on_click=lambda _, idx=i: self._delete_item_row(idx), + ) + + # データ行 + row = ft.Row([ + product_field, + quantity_field, + unit_price_field, + ft.Text(f"Â¥{item.subtotal:,}", size=12, weight=ft.FontWeight.BOLD, width=70, text_align=ft.TextAlign.RIGHT), # 固定幅 + delete_button, + ]) + data_rows.append(row) + + return ft.Column([ + header_row, + ft.Divider(height=1, color=ft.Colors.GREY_400), + ft.Column(data_rows, scroll=ft.ScrollMode.AUTO, height=250), # 高ã•ã‚’åˆ¶é™ + ]) def create_new_customer_screen(self) -> ft.Container: """æ–°è¦é¡§å®¢ç™»éŒ²ç”»é¢""" name_field = ft.TextField(label="顧客å(略称)") @@ -629,6 +1174,16 @@ class FlutterStyleDashboard: # UIã‚’æ›´æ–°ã—ã¦é¸æŠžã•れãŸé¡§å®¢ã‚’表示 self.update_main_content() + def submit_invoice_for_tax(self, invoice_uuid: str) -> bool: + """税務署æå‡ºæ¸ˆã¿ãƒ•ラグを設定""" + success = self.app_service.invoice.submit_to_tax_authority(invoice_uuid) + if success: + self.invoices = self.app_service.invoice.get_recent_invoices(20) + self.update_main_content() + logging.info(f"税務署æå‡ºæ¸ˆã¿: {invoice_uuid}") + else: + logging.error(f"税務署æå‡ºå¤±æ•—: {invoice_uuid}") + return success def on_customer_deleted(self, customer: Customer): """顧客削除時ã®å‡¦ç†""" self.app_service.customer.delete_customer(customer.id) diff --git a/components/customer_picker.py b/components/customer_picker.py index f40e5ad..aa7f3f3 100644 --- a/components/customer_picker.py +++ b/components/customer_picker.py @@ -3,6 +3,10 @@ Flutter風ã®ModalBottomSheetã‚’Fletã§å®Ÿè£… """ +import sys +import os +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + import flet as ft from typing import List, Callable, Optional from models.invoice_models import Customer diff --git a/generated_pdfs/20260220(請求書)æ±äº¬éƒ½å“å·åŒºæ±å“å·1-1-1_generated_pdfsSELF001_20260220_214109.pdf_250,000円_350cfecc.PDF b/generated_pdfs/20260220(請求書)æ±äº¬éƒ½å“å·åŒºæ±å“å·1-1-1_generated_pdfsSELF001_20260220_214109.pdf_250,000円_350cfecc.PDF new file mode 100644 index 0000000..42c628c --- /dev/null +++ b/generated_pdfs/20260220(請求書)æ±äº¬éƒ½å“å·åŒºæ±å“å·1-1-1_generated_pdfsSELF001_20260220_214109.pdf_250,000円_350cfecc.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:20260221233306+09'00') /Creator (anonymous) /Keywords (node_id=2026-02-20 12:41:09) /ModDate (D:20260221233306+09'00') /Producer (ReportLab PDF Library - \(opensource\)) + /Subject (3772e7ed-19be-4049-ac62-025c725d0912) /Title (\376\377\212\313lBf\370\000 \0002\0000\0002\0006\0000\0002\0002\0000\000-\0002\0001\0004\0001) /Trapped /False +>> +endobj +8 0 obj +<< +/Count 1 /Kids [ 5 0 R ] /Type /Pages +>> +endobj +9 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 556 +>> +stream +Gatm94\rsL&Dd4649e37qlnCi7jOH5;arYj"sh@khmg11=sm?a72%,-m!JKpj7u@YmA9CPpic))GE4E1?4\B;fQt]12\ROd2;NX?G0*&X$GL/!+n=)3ME98`Bms,$-EN.`D`*Qd*S].F"0q%5[/T%gpn9\WnKg85_NALLp@a;F;n/u7d^2@F:\S^9\(N7lcj!ALlp\6]Eo/q\l!.(a5_7TDa%<;.B!2k]=?<\&;B4:;IR./?IdW45;Cj]5cm.l1]:#_Zda$&"'QSWZO\&F!>4'?:C9W:(W+b$Tr/;?Xj/*n)C/QDEGZP!&8_OV!s'al_+@6eN5""Cm'uTK5h*$]>dMlW\igb%dE8`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 +0000001081 00000 n +0000001140 00000 n +trailer +<< +/ID +[<14435c1529362f9feebdcdad3f6da730><14435c1529362f9feebdcdad3f6da730>] +% ReportLab generated PDF document -- digest (opensource) + +/Info 7 0 R +/Root 6 0 R +/Size 10 +>> +startxref +1786 +%%EOF diff --git a/generated_pdfs/20260220(請求書)æ±äº¬éƒ½å“å·åŒºæ±å“å·1-1-1_æ±äº¬éƒ½å“å·åŒºæ±å“å·1-1-1_250,000円_0260ba6d.PDF b/generated_pdfs/20260220(請求書)æ±äº¬éƒ½å“å·åŒºæ±å“å·1-1-1_æ±äº¬éƒ½å“å·åŒºæ±å“å·1-1-1_250,000円_0260ba6d.PDF new file mode 100644 index 0000000..9df9cab --- /dev/null +++ b/generated_pdfs/20260220(請求書)æ±äº¬éƒ½å“å·åŒºæ±å“å·1-1-1_æ±äº¬éƒ½å“å·åŒºæ±å“å·1-1-1_250,000円_0260ba6d.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 (SELF) /CreationDate (D:20260221162914+09'00') /Creator (anonymous) /Keywords () /ModDate (D:20260221162914+09'00') /Producer (ReportLab PDF Library - \(opensource\)) + /Subject (aaa8cbcc-06c2-4aff-9955-0d7eda07d4e7) /Title (\376\377\212\313lBf\370\000 \000T\000E\000S\000T\0000\0000\0001) /Trapped /False +>> +endobj +8 0 obj +<< +/Count 1 /Kids [ 5 0 R ] /Type /Pages +>> +endobj +9 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 571 +>> +stream +Gatm9]8kZ#%.*F5=?8abj$TQ:/H]7X+AH+<$]lRJD>q9XmIT$^>O/A1$Nl,lE]AH*-jK6ta7lc=J1j&JIq<#=r\nf3!<"j$l(>uq!YU7fOteWiGmY,mX[lkkrfn_=l`5Y["mXCro*=LZ1ue0MB.?].`Y;sEa:hOLFj*UZZD[>^6TC^OA;1eG7l#`29a25gs-3TMf7`-ne7>G"S2?=3I*01,^0UF5%UbZ2!FWJf#'/1:]12Rf^gF][U1=\`a!07ZP'`B>6/&>kt`q,F"Y$.YP\#QF)9d`O0<#-c7WN#(,1)6m^_-FkC*@4ER'Q?i]658R0'aW#7fY=ncee0/:g&uWc<;eF8OT[^>BkOB_C6MAoLamY.2[fhLAWTZVSFO.9Whi0V>E!rn?S#q!7p)QD!e_Ao!p)T/>q>R@`InGM0kn=Z:@=\lEsendstream +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 +0000001021 00000 n +0000001080 00000 n +trailer +<< +/ID +[<647dfafbf6f40c211061b89af760d29a><647dfafbf6f40c211061b89af760d29a>] +% ReportLab generated PDF document -- digest (opensource) + +/Info 7 0 R +/Root 6 0 R +/Size 10 +>> +startxref +1741 +%%EOF diff --git a/generated_pdfs/20260220(請求書)æ±äº¬éƒ½å“å·åŒºæ±å“å·1-1-1_請求書分_250,000円_1d3b6ecc.PDF b/generated_pdfs/20260220(請求書)æ±äº¬éƒ½å“å·åŒºæ±å“å·1-1-1_請求書分_250,000円_1d3b6ecc.PDF new file mode 100644 index 0000000..bf670b4 --- /dev/null +++ b/generated_pdfs/20260220(請求書)æ±äº¬éƒ½å“å·åŒºæ±å“å·1-1-1_請求書分_250,000円_1d3b6ecc.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:20260221233933+09'00') /Creator (anonymous) /Keywords (node_id=2026-02-20 12:39:44) /ModDate (D:20260221233933+09'00') /Producer (ReportLab PDF Library - \(opensource\)) + /Subject (7face002-113c-45ad-a89c-a73082390cf6) /Title (\376\377\212\313lBf\370\000 \0002\0000\0002\0006\0000\0002\0002\0000\000-\0002\0001\0003\0009) /Trapped /False +>> +endobj +8 0 obj +<< +/Count 1 /Kids [ 5 0 R ] /Type /Pages +>> +endobj +9 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 556 +>> +stream +Gatm94\rsL&Dd4649e37qlnCi7jOH5;arYj"sh@khmg11=sm?a72%,-m!JKpj7u@YmA9CPpic))GE4E1?4ZlEcH8'12\ROd2;NX?G0*&X$GL/!+n=)3ME98`Bms,$-EN.`D`*Qd*S].F"0q%5[/T%gpn9\WnKg85_NALLp@a;F;n/u7d^2@F:\S^9\(N7lcj!ALlp\6]Eo/q\l!.(a5_7TDa%<;.B!2k]=?<\&;B4:;IR./?IdW45;Cj]5cm.l1]:#_Zda$&"'QSWZO\&F!>4'?:C9W:(W+b$Tr/;?Xj/*n)C/QDEGZP!&8_OV!s'al_+@6eN5""Cm'uTK5h*$]>dMlW\igb%dE8`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 +0000001081 00000 n +0000001140 00000 n +trailer +<< +/ID +[<473678096162e2d017466f3e776ba6ca><473678096162e2d017466f3e776ba6ca>] +% ReportLab generated PDF document -- digest (opensource) + +/Info 7 0 R +/Root 6 0 R +/Size 10 +>> +startxref +1786 +%%EOF diff --git a/generated_pdfs/20260221(請求書)ä½è—¤å•†äº‹_核弾頭_5,000,000,000円_96ab1e47.PDF b/generated_pdfs/20260221(請求書)ä½è—¤å•†äº‹_核弾頭_5,000,000,000円_96ab1e47.PDF new file mode 100644 index 0000000..784cbce --- /dev/null +++ b/generated_pdfs/20260221(請求書)ä½è—¤å•†äº‹_核弾頭_5,000,000,000円_96ab1e47.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 (SELF) /CreationDate (D:20260221162429+09'00') /Creator (anonymous) /Keywords () /ModDate (D:20260221162429+09'00') /Producer (ReportLab PDF Library - \(opensource\)) + /Subject (4458bd92-1629-45aa-b3d9-00376ff9d14f) /Title (\376\377\212\313lBf\370\000 \000T\000E\000S\000T\0000\0000\0001) /Trapped /False +>> +endobj +8 0 obj +<< +/Count 1 /Kids [ 5 0 R ] /Type /Pages +>> +endobj +9 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 541 +>> +stream +Gatm9\PC%-&FK'(;]:=D3VH.''IlM'90+=*,f9S*N#g[Cg5_@^'KK@am$frSgK=`d+1M7lhZ-G04?bR*5lD#Ucp@U3]6_ro$]&Tl-"/k&F6CCF$s>>QZrQ[-8&#M/'2Z*on*cqK>4>VN%oa;G[3NE\e5'3C*4D3g1t=F$A"!)a,m;@E.r!6_^dHo/0YRT_<)R#-G0i~>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 +0000001021 00000 n +0000001080 00000 n +trailer +<< +/ID +[<9f94a7ecbd060db21eed7c31734d1ad5><9f94a7ecbd060db21eed7c31734d1ad5>] +% ReportLab generated PDF document -- digest (opensource) + +/Info 7 0 R +/Root 6 0 R +/Size 10 +>> +startxref +1711 +%%EOF diff --git a/models/invoice_models.py b/models/invoice_models.py index 36f425a..76e0eb1 100644 --- a/models/invoice_models.py +++ b/models/invoice_models.py @@ -16,14 +16,43 @@ class DocumentType(Enum): RECEIPT = "é ˜åŽæ›¸" SALES = "売上ä¼ç¥¨" +class Product: + """商å“マスタモデル""" + + def __init__(self, id: Optional[int] = None, name: str = "", unit_price: int = 0, description: str = ""): + self.id = id + self.name = name + self.unit_price = unit_price + self.description = description + + def to_dict(self) -> Dict[str, Any]: + """JSON変æ›""" + return { + 'id': self.id, + 'name': self.name, + 'unit_price': self.unit_price, + 'description': self.description + } + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> 'Product': + """JSONã‹ã‚‰å¾©å…ƒ""" + return cls( + id=data.get('id'), + name=data.get('name', ''), + unit_price=data.get('unit_price', 0), + description=data.get('description', '') + ) + class InvoiceItem: """ä¼ç¥¨ã®å„明細行を表ã™ãƒ¢ãƒ‡ãƒ«""" - def __init__(self, description: str, quantity: int, unit_price: int, is_discount: bool = False): + def __init__(self, description: str, quantity: int, unit_price: int, is_discount: bool = False, product_id: Optional[int] = None): self.description = description self.quantity = quantity self.unit_price = unit_price self.is_discount = is_discount # 値引ãé …ç›®ã‹ã©ã†ã‹ã‚’示ã™ãƒ•ラグ + self.product_id = product_id # 商å“マスタID @property def subtotal(self) -> int: @@ -36,7 +65,8 @@ class InvoiceItem: description=kwargs.get('description', self.description), quantity=kwargs.get('quantity', self.quantity), unit_price=kwargs.get('unit_price', self.unit_price), - is_discount=kwargs.get('is_discount', self.is_discount) + is_discount=kwargs.get('is_discount', self.is_discount), + product_id=kwargs.get('product_id', self.product_id) ) def to_dict(self) -> Dict[str, Any]: @@ -45,7 +75,8 @@ class InvoiceItem: 'description': self.description, 'quantity': self.quantity, 'unit_price': self.unit_price, - 'is_discount': self.is_discount + 'is_discount': self.is_discount, + 'product_id': self.product_id } @classmethod @@ -55,18 +86,20 @@ class InvoiceItem: description=data['description'], quantity=data['quantity'], unit_price=data['unit_price'], - is_discount=data.get('is_discount', False) + is_discount=data.get('is_discount', False), + product_id=data.get('product_id') ) class Customer: """顧客情報モデル""" - def __init__(self, id: int, name: str, formal_name: str, address: str = "", phone: str = ""): + def __init__(self, id: int, name: str, formal_name: str, address: str = "", phone: str = "", email: str = ""): self.id = id self.name = name self.formal_name = formal_name self.address = address self.phone = phone + self.email = email def to_dict(self) -> Dict[str, Any]: """JSON変æ›""" @@ -86,7 +119,8 @@ class Customer: name=data['name'], formal_name=data['formal_name'], address=data.get('address', ''), - phone=data.get('phone', '') + phone=data.get('phone', ''), + email=data.get('email', '') ) class Invoice: diff --git a/services/app_service.py b/services/app_service.py index b4a1ba9..63ad98f 100644 --- a/services/app_service.py +++ b/services/app_service.py @@ -9,7 +9,7 @@ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from typing import Optional, List, Dict, Any from datetime import datetime -from models.invoice_models import Invoice, Customer, InvoiceItem, DocumentType +from models.invoice_models import Invoice, Customer, InvoiceItem, DocumentType, Product from services.repositories import InvoiceRepository, CustomerRepository from services.pdf_generator import PdfGenerator import logging @@ -226,11 +226,20 @@ class InvoiceService: logging.error(f"PDFå†ç”Ÿæˆå¤±æ•—: uuidãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“: {invoice_uuid}") return None + # 既存ã®PDFパスã¨notesをクリアã—ã¦æ–°è¦ç”Ÿæˆ + invoice.file_path = None # 既存パスをクリア + invoice.notes = "" # notesもクリア(å¤ã„パスãŒå«ã¾ã‚Œã¦ã„ã‚‹å¯èƒ½æ€§ï¼‰ + # 会社情報ã¯ç¾çжv1固定。将æ¥ã¯company_info_versionã§åˆ†å²ã€‚ pdf_path = self.pdf_generator.generate_invoice_pdf(invoice, self.company_info) if not pdf_path: return None - + + # æ–°ã—ã„PDFパスをDBã«ä¿å­˜ + invoice.file_path = pdf_path + invoice.pdf_generated_at = datetime.now().replace(microsecond=0).isoformat() + self.invoice_repo.update_invoice_file_path(invoice.uuid, pdf_path, invoice.pdf_generated_at) + return pdf_path def delete_pdf_file(self, pdf_path: str) -> bool: @@ -335,12 +344,28 @@ class CustomerService: # サービスファクトリ +class ProductService: + """商å“ビジãƒã‚¹ãƒ­ã‚¸ãƒƒã‚¯""" + + def __init__(self): + self.invoice_repo = InvoiceRepository() + + def get_all_products(self) -> List[Product]: + """全商å“ã‚’å–å¾—""" + return self.invoice_repo.get_all_products() + + def save_product(self, product: Product) -> bool: + """商å“ã‚’ä¿å­˜ï¼ˆæ–°è¦ãƒ»æ›´æ–°ï¼‰""" + return self.invoice_repo.save_product(product) + + class AppService: """アプリケーションサービス統åˆ""" def __init__(self): self.invoice = InvoiceService() self.customer = CustomerService() + self.product = ProductService() def get_dashboard_data(self) -> Dict[str, Any]: """ダッシュボード表示用データ""" diff --git a/services/pdf_generator.py b/services/pdf_generator.py index b6f97e3..00f3770 100644 --- a/services/pdf_generator.py +++ b/services/pdf_generator.py @@ -42,9 +42,41 @@ class PdfGenerator: 生æˆã•れãŸPDFファイルパスã€å¤±æ•—時ã¯None """ try: - # ファイルå生æˆ: {会社ID}_{端末ID}_{連番}.pdf - timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") - filename = f"{company_info.get('id', 'COMP')}_{timestamp}.pdf" + # ファイルå生æˆãƒ«ãƒ¼ãƒ«: {日付}({タイプ}){æ ªå¼ä¼šç¤¾ç­‰ã‚’除ã顧客å}_{ä»¶åor商å“1行目}_{金é¡}円_{HASH下8æ¡}.PDF + date_str = invoice.date.strftime("%Y%m%d") + doc_type = invoice.document_type.value if invoice.document_type else "請求" + + # 顧客åã‹ã‚‰æ ªå¼ä¼šç¤¾ç­‰ã‚’除去 + customer_name = invoice.customer.name + for suffix in ["æ ªå¼ä¼šç¤¾", "有é™ä¼šç¤¾", "åˆè³‡ä¼šç¤¾", "åˆåŒä¼šç¤¾"]: + customer_name = customer_name.replace(suffix, "") + + # 顧客åã‹ã‚‰ä¸æ­£ãªæ–‡å­—を除去(ファイルåã«ä½¿ãˆãªã„文字) + import re + customer_name = re.sub(r'[\\/:*?"<>|]', '', customer_name) + + # ä»¶åã¾ãŸã¯å•†å“1行目 + subject_or_product = invoice.notes or "" + if not subject_or_product and invoice.items: + subject_or_product = invoice.items[0].description + + # ä»¶åã‹ã‚‰ä¸æ­£ãªæ–‡å­—を除去 + subject_or_product = re.sub(r'[\\/:*?"<>|]', '', subject_or_product) + + # ä»¶åãŒé•·ã™ãŽã‚‹å ´åˆã¯çŸ­ç¸® + if len(subject_or_product) > 30: + subject_or_product = subject_or_product[:30] + "..." + + # é‡‘é¡ + total_amount = sum(item.subtotal for item in invoice.items) + amount_str = f"{total_amount:,}円" + + # ãƒãƒƒã‚·ãƒ¥ï¼ˆä»®å®Ÿè£…) + import hashlib + hash_input = f"{date_str}{doc_type}{customer_name}{subject_or_product}{total_amount}" + hash_value = hashlib.md5(hash_input.encode()).hexdigest()[:8] + + filename = f"{date_str}({doc_type}){customer_name}_{subject_or_product}_{amount_str}_{hash_value}.PDF" filepath = os.path.join(self.output_dir, filename) # PDFç”Ÿæˆ diff --git a/services/repositories.py b/services/repositories.py index 763abec..062e933 100644 --- a/services/repositories.py +++ b/services/repositories.py @@ -11,7 +11,7 @@ import sqlite3 import json from datetime import datetime from typing import List, Optional, Dict, Any -from models.invoice_models import Invoice, Customer, InvoiceItem, DocumentType +from models.invoice_models import Invoice, Customer, InvoiceItem, DocumentType, Product import logging import uuid as uuid_module import hashlib @@ -82,10 +82,23 @@ class InvoiceRepository: quantity INTEGER, unit_price INTEGER, is_discount BOOLEAN, + product_id INTEGER, FOREIGN KEY (invoice_id) REFERENCES invoices(id) ) ''') + # 商å“マスタテーブル + cursor.execute(''' + CREATE TABLE IF NOT EXISTS products ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + unit_price INTEGER NOT NULL, + description TEXT, + created_at TEXT DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT DEFAULT CURRENT_TIMESTAMP + ) + ''') + conn.commit() # 既存DBå‘ã‘ã®è»½é‡ãƒžã‚¤ã‚°ãƒ¬ãƒ¼ã‚·ãƒ§ãƒ³ï¼ˆåˆ—追加) @@ -325,6 +338,20 @@ class InvoiceRepository: logging.error(f"ä¼ç¥¨ä¿å­˜ã‚¨ãƒ©ãƒ¼: {e}") return False + def update_invoice_file_path(self, invoice_uuid: str, file_path: str, pdf_generated_at: str): + """PDFファイルパスを更新""" + try: + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute(''' + UPDATE invoices + SET file_path = ?, pdf_generated_at = ? + WHERE uuid = ? + ''', (file_path, pdf_generated_at, invoice_uuid)) + conn.commit() + except Exception as e: + logging.error(f"PDFパス更新エラー: {e}") + def get_all_invoices(self, limit: int = 100) -> List[Invoice]: """å…¨ä¼ç¥¨ã‚’å–å¾—""" invoices = [] @@ -572,6 +599,66 @@ class CustomerRepository: logging.error(f"顧客更新エラー: {e}") return False + def get_all_products(self) -> List[Product]: + """全商å“ã‚’å–å¾—""" + products = [] + + try: + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute('SELECT * FROM products ORDER BY name') + + rows = cursor.fetchall() + for row in rows: + products.append(Product( + id=row[0], + name=row[1], + unit_price=row[2], + description=row[3] + )) + + return products + + except Exception as e: + logging.error(f"商å“å–得エラー: {e}") + return [] + + def save_product(self, product: Product) -> bool: + """商å“ã‚’ä¿å­˜ï¼ˆæ–°è¦ãƒ»æ›´æ–°ï¼‰""" + try: + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + + if product.id: + # æ›´æ–° + cursor.execute(""" + UPDATE products + SET name = ?, unit_price = ?, description = ?, updated_at = CURRENT_TIMESTAMP + WHERE id = ? + """, ( + product.name, + product.unit_price, + product.description, + product.id + )) + else: + # æ–°è¦ + cursor.execute(""" + INSERT INTO products (name, unit_price, description) + VALUES (?, ?, ?) + """, ( + product.name, + product.unit_price, + product.description + )) + + conn.commit() + return True + + except Exception as e: + logging.error(f"商å“ä¿å­˜ã‚¨ãƒ©ãƒ¼: {e}") + return False + def get_all_customers(self) -> List[Customer]: """全顧客をå–å¾—""" customers = []