自動で空行追加
This commit is contained in:
parent
22c4a2e6b7
commit
042bbe032d
10 changed files with 1088 additions and 31 deletions
|
|
@ -9,7 +9,7 @@ import sys
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import List, Dict, Optional
|
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 components.customer_picker import CustomerPickerModal
|
||||||
from services.app_service import AppService
|
from services.app_service import AppService
|
||||||
|
|
||||||
|
|
@ -24,11 +24,13 @@ class FlutterStyleDashboard:
|
||||||
|
|
||||||
def __init__(self, page: ft.Page):
|
def __init__(self, page: ft.Page):
|
||||||
self.page = page
|
self.page = page
|
||||||
self.current_tab = 0 # 0: 新規作成, 1: 発行履歴
|
self.current_tab = 0 # 0: 伝票一覧, 1: 詳細編集
|
||||||
self.selected_customer = None
|
self.selected_customer = None
|
||||||
self.selected_document_type = DocumentType.INVOICE
|
self.selected_document_type = DocumentType.INVOICE
|
||||||
self.amount_value = "250000"
|
self.amount_value = "250000"
|
||||||
self.customer_picker = None
|
self.customer_picker = None
|
||||||
|
self.editing_invoice = None # 編集中の伝票
|
||||||
|
self.is_edit_mode = False # 編集モードフラグ
|
||||||
self.is_customer_picker_open = False
|
self.is_customer_picker_open = False
|
||||||
self.customer_search_query = ""
|
self.customer_search_query = ""
|
||||||
self.show_offsets = False
|
self.show_offsets = False
|
||||||
|
|
@ -147,14 +149,103 @@ class FlutterStyleDashboard:
|
||||||
elif self.is_customer_picker_open:
|
elif self.is_customer_picker_open:
|
||||||
self.main_content.controls.append(self.create_customer_picker_screen())
|
self.main_content.controls.append(self.create_customer_picker_screen())
|
||||||
else:
|
else:
|
||||||
# 新規作成画面
|
# 伝票一覧画面
|
||||||
self.main_content.controls.append(self.create_slip_input_screen())
|
self.main_content.controls.append(self.create_slip_list_screen())
|
||||||
else:
|
else:
|
||||||
# 発行履歴画面
|
# 詳細編集画面
|
||||||
self.main_content.controls.append(self.create_slip_history_screen())
|
self.main_content.controls.append(self.create_invoice_edit_screen())
|
||||||
|
|
||||||
self.page.update()
|
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:
|
def create_customer_picker_screen(self) -> ft.Container:
|
||||||
"""顧客選択画面(画面内遷移・ダイアログ不使用)"""
|
"""顧客選択画面(画面内遷移・ダイアログ不使用)"""
|
||||||
self.customers = self.app_service.customer.get_all_customers()
|
self.customers = self.app_service.customer.get_all_customers()
|
||||||
|
|
@ -451,25 +542,49 @@ class FlutterStyleDashboard:
|
||||||
else:
|
else:
|
||||||
logging.error("PDF再生成失敗")
|
logging.error("PDF再生成失敗")
|
||||||
|
|
||||||
|
def edit_invoice(_=None):
|
||||||
|
if not isinstance(slip, Invoice):
|
||||||
|
return
|
||||||
|
self.open_invoice_edit(slip)
|
||||||
|
|
||||||
actions_row = None
|
actions_row = None
|
||||||
if isinstance(slip, Invoice):
|
if isinstance(slip, Invoice):
|
||||||
buttons = []
|
buttons = []
|
||||||
if not getattr(slip, "submitted_to_tax_authority", False):
|
if not getattr(slip, "submitted_to_tax_authority", False):
|
||||||
buttons.append(
|
buttons.append(
|
||||||
ft.IconButton(
|
ft.ElevatedButton(
|
||||||
ft.Icons.REPLAY_CIRCLE_FILLED,
|
content=ft.Text("編集", color=ft.Colors.WHITE),
|
||||||
tooltip="赤伝(相殺)を発行",
|
style=ft.ButtonStyle(
|
||||||
icon_color=ft.Colors.RED_400,
|
bgcolor=ft.Colors.GREEN_600,
|
||||||
on_click=issue_offset,
|
padding=ft.padding.all(10),
|
||||||
|
),
|
||||||
|
on_click=edit_invoice,
|
||||||
|
width=80,
|
||||||
|
height=40,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if not getattr(slip, "submitted_to_tax_authority", False):
|
|
||||||
buttons.append(
|
buttons.append(
|
||||||
ft.IconButton(
|
ft.ElevatedButton(
|
||||||
ft.Icons.CHECK_CIRCLE,
|
content=ft.Text("赤伝", color=ft.Colors.WHITE),
|
||||||
tooltip="税務署提出済みに設定",
|
style=ft.ButtonStyle(
|
||||||
icon_color=ft.Colors.ORANGE_400,
|
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),
|
on_click=lambda _: self.submit_invoice_for_tax(slip.uuid),
|
||||||
|
width=80,
|
||||||
|
height=40,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
buttons.append(
|
buttons.append(
|
||||||
|
|
@ -480,7 +595,7 @@ class FlutterStyleDashboard:
|
||||||
on_click=regenerate_and_share,
|
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
|
display_amount = amount
|
||||||
if isinstance(slip, Invoice) and getattr(slip, "is_offset", False):
|
if isinstance(slip, Invoice) and getattr(slip, "is_offset", False):
|
||||||
|
|
@ -547,11 +662,441 @@ class FlutterStyleDashboard:
|
||||||
border_radius=8,
|
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):
|
def open_new_customer_form(self):
|
||||||
"""新規顧客フォームを開く(画面内遷移)"""
|
"""新規顧客フォームを開く(画面内遷移)"""
|
||||||
self.is_new_customer_form_open = True
|
self.is_new_customer_form_open = True
|
||||||
self.update_main_content()
|
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:
|
def create_new_customer_screen(self) -> ft.Container:
|
||||||
"""新規顧客登録画面"""
|
"""新規顧客登録画面"""
|
||||||
name_field = ft.TextField(label="顧客名(略称)")
|
name_field = ft.TextField(label="顧客名(略称)")
|
||||||
|
|
@ -629,6 +1174,16 @@ class FlutterStyleDashboard:
|
||||||
# UIを更新して選択された顧客を表示
|
# UIを更新して選択された顧客を表示
|
||||||
self.update_main_content()
|
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):
|
def on_customer_deleted(self, customer: Customer):
|
||||||
"""顧客削除時の処理"""
|
"""顧客削除時の処理"""
|
||||||
self.app_service.customer.delete_customer(customer.id)
|
self.app_service.customer.delete_customer(customer.id)
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,10 @@
|
||||||
Flutter風のModalBottomSheetをFletで実装
|
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
|
import flet as ft
|
||||||
from typing import List, Callable, Optional
|
from typing import List, Callable, Optional
|
||||||
from models.invoice_models import Customer
|
from models.invoice_models import Customer
|
||||||
|
|
|
||||||
|
|
@ -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))GE4E<LgC&FaFe^8X*a7nAXsk=@@\(-dlNI@Jq@U[jJ)ULJ7eXp+IRelrI07ZqQGXgrdLTI&?pi'CIAr2#f\+:3M`-HC0)3lLaEBUH'-=0UJ92(*$adZ2[?`6aB+cc?=Q01^!)PX+3Uj7qgfA>1?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<j*RR#DQ8cf(=")=?RSIPq8Rl-L6MD4S7]*>_+@6eN5""Cm'uTK5h*$]>dMlW\igb%dE8`<Gh-YF9O3<9uJ2/\+p4&@'Zj~>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
|
||||||
|
|
@ -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:hOLF<S5M$oA>j*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:@=\lE<UYV.0DiNg"1\b<)(fUY=2AM#hiVbBN<92mYLX3@->s<p\m1EY4mMuZ85G/<Kp7!4!R!0\$ZQ?M'OFujmaNLpaFeP"gUjFj1M/c$Hk#?*h-?@C0T0rRBd2CHK;X2-U$!m~>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
|
||||||
|
[<647dfafbf6f40c211061b89af760d29a><647dfafbf6f40c211061b89af760d29a>]
|
||||||
|
% ReportLab generated PDF document -- digest (opensource)
|
||||||
|
|
||||||
|
/Info 7 0 R
|
||||||
|
/Root 6 0 R
|
||||||
|
/Size 10
|
||||||
|
>>
|
||||||
|
startxref
|
||||||
|
1741
|
||||||
|
%%EOF
|
||||||
|
|
@ -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))GE4E<LgC&FaFe^8X*a7nAXsk=@@\(-dlNI@Jq@U[jJ)ULJ7eXp+IRelrI07ZqQGXgrdLTI&?pi'CIAr2#f\+:3M`-HC0)3lLaEBUH'-=0UJ92(*$adZ2[?`6aB+cc?=Q01^!)PX+3Uj7qgfA>1?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<j*RR#DQ8cf(=")=?RSIPq8Rl-L6MD4S7]*>_+@6eN5""Cm'uTK5h*$]>dMlW\igb%dE8`<Gh-YF9O3<9uJ2/\+p1N:U7;~>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
|
||||||
|
|
@ -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[3<U0OTh[UYpUGW)O_mLl0^kZ%1,13Sg?5T=X9uT+=*Ye9gQr6P,jTN?ZUV9HCgaN$d5`"rB[B+D`,qYql!W&W/Fe_Jl?d-KqV`TW#E'n@bFK_/HK`b.'DeW#[pEsfUi5p.:M#/_Hu`nej?G6p,O9P8P`Dq$9])hZB48"m?NbaXV7]r(3-Is+ou37GT1SDN/;HeRca$=h8GdU(0\lFJ)2fD!&6qXrMp'Up;CKCKgOb&RU!M!@,s%h];ER+GB)J(&5iIu!,'0=<NcIrR?*ajOHj)$(1-rca_0Z(ilK*9Hp^7-Esu-2iKiYKMC+&N(#lIIEqKp;[E2V60K6-!faCWMXO>NE\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
|
||||||
|
|
@ -16,14 +16,43 @@ class DocumentType(Enum):
|
||||||
RECEIPT = "領収書"
|
RECEIPT = "領収書"
|
||||||
SALES = "売上伝票"
|
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:
|
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.description = description
|
||||||
self.quantity = quantity
|
self.quantity = quantity
|
||||||
self.unit_price = unit_price
|
self.unit_price = unit_price
|
||||||
self.is_discount = is_discount # 値引き項目かどうかを示すフラグ
|
self.is_discount = is_discount # 値引き項目かどうかを示すフラグ
|
||||||
|
self.product_id = product_id # 商品マスタID
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def subtotal(self) -> int:
|
def subtotal(self) -> int:
|
||||||
|
|
@ -36,7 +65,8 @@ class InvoiceItem:
|
||||||
description=kwargs.get('description', self.description),
|
description=kwargs.get('description', self.description),
|
||||||
quantity=kwargs.get('quantity', self.quantity),
|
quantity=kwargs.get('quantity', self.quantity),
|
||||||
unit_price=kwargs.get('unit_price', self.unit_price),
|
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]:
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
|
|
@ -45,7 +75,8 @@ class InvoiceItem:
|
||||||
'description': self.description,
|
'description': self.description,
|
||||||
'quantity': self.quantity,
|
'quantity': self.quantity,
|
||||||
'unit_price': self.unit_price,
|
'unit_price': self.unit_price,
|
||||||
'is_discount': self.is_discount
|
'is_discount': self.is_discount,
|
||||||
|
'product_id': self.product_id
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
@ -55,18 +86,20 @@ class InvoiceItem:
|
||||||
description=data['description'],
|
description=data['description'],
|
||||||
quantity=data['quantity'],
|
quantity=data['quantity'],
|
||||||
unit_price=data['unit_price'],
|
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:
|
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.id = id
|
||||||
self.name = name
|
self.name = name
|
||||||
self.formal_name = formal_name
|
self.formal_name = formal_name
|
||||||
self.address = address
|
self.address = address
|
||||||
self.phone = phone
|
self.phone = phone
|
||||||
|
self.email = email
|
||||||
|
|
||||||
def to_dict(self) -> Dict[str, Any]:
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
"""JSON変換"""
|
"""JSON変換"""
|
||||||
|
|
@ -86,7 +119,8 @@ class Customer:
|
||||||
name=data['name'],
|
name=data['name'],
|
||||||
formal_name=data['formal_name'],
|
formal_name=data['formal_name'],
|
||||||
address=data.get('address', ''),
|
address=data.get('address', ''),
|
||||||
phone=data.get('phone', '')
|
phone=data.get('phone', ''),
|
||||||
|
email=data.get('email', '')
|
||||||
)
|
)
|
||||||
|
|
||||||
class Invoice:
|
class Invoice:
|
||||||
|
|
|
||||||
|
|
@ -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 typing import Optional, List, Dict, Any
|
||||||
from datetime import datetime
|
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.repositories import InvoiceRepository, CustomerRepository
|
||||||
from services.pdf_generator import PdfGenerator
|
from services.pdf_generator import PdfGenerator
|
||||||
import logging
|
import logging
|
||||||
|
|
@ -226,11 +226,20 @@ class InvoiceService:
|
||||||
logging.error(f"PDF再生成失敗: uuidが見つかりません: {invoice_uuid}")
|
logging.error(f"PDF再生成失敗: uuidが見つかりません: {invoice_uuid}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
# 既存のPDFパスとnotesをクリアして新規生成
|
||||||
|
invoice.file_path = None # 既存パスをクリア
|
||||||
|
invoice.notes = "" # notesもクリア(古いパスが含まれている可能性)
|
||||||
|
|
||||||
# 会社情報は現状v1固定。将来はcompany_info_versionで分岐。
|
# 会社情報は現状v1固定。将来はcompany_info_versionで分岐。
|
||||||
pdf_path = self.pdf_generator.generate_invoice_pdf(invoice, self.company_info)
|
pdf_path = self.pdf_generator.generate_invoice_pdf(invoice, self.company_info)
|
||||||
if not pdf_path:
|
if not pdf_path:
|
||||||
return None
|
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
|
return pdf_path
|
||||||
|
|
||||||
def delete_pdf_file(self, pdf_path: str) -> bool:
|
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:
|
class AppService:
|
||||||
"""アプリケーションサービス統合"""
|
"""アプリケーションサービス統合"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.invoice = InvoiceService()
|
self.invoice = InvoiceService()
|
||||||
self.customer = CustomerService()
|
self.customer = CustomerService()
|
||||||
|
self.product = ProductService()
|
||||||
|
|
||||||
def get_dashboard_data(self) -> Dict[str, Any]:
|
def get_dashboard_data(self) -> Dict[str, Any]:
|
||||||
"""ダッシュボード表示用データ"""
|
"""ダッシュボード表示用データ"""
|
||||||
|
|
|
||||||
|
|
@ -42,9 +42,41 @@ class PdfGenerator:
|
||||||
生成されたPDFファイルパス、失敗時はNone
|
生成されたPDFファイルパス、失敗時はNone
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# ファイル名生成: {会社ID}_{端末ID}_{連番}.pdf
|
# ファイル名生成ルール: {日付}({タイプ}){株式会社等を除く顧客名}_{件名or商品1行目}_{金額}円_{HASH下8桁}.PDF
|
||||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
date_str = invoice.date.strftime("%Y%m%d")
|
||||||
filename = f"{company_info.get('id', 'COMP')}_{timestamp}.pdf"
|
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)
|
filepath = os.path.join(self.output_dir, filename)
|
||||||
|
|
||||||
# PDF生成
|
# PDF生成
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import sqlite3
|
||||||
import json
|
import json
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import List, Optional, Dict, Any
|
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 logging
|
||||||
import uuid as uuid_module
|
import uuid as uuid_module
|
||||||
import hashlib
|
import hashlib
|
||||||
|
|
@ -82,10 +82,23 @@ class InvoiceRepository:
|
||||||
quantity INTEGER,
|
quantity INTEGER,
|
||||||
unit_price INTEGER,
|
unit_price INTEGER,
|
||||||
is_discount BOOLEAN,
|
is_discount BOOLEAN,
|
||||||
|
product_id INTEGER,
|
||||||
FOREIGN KEY (invoice_id) REFERENCES invoices(id)
|
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()
|
conn.commit()
|
||||||
|
|
||||||
# 既存DB向けの軽量マイグレーション(列追加)
|
# 既存DB向けの軽量マイグレーション(列追加)
|
||||||
|
|
@ -325,6 +338,20 @@ class InvoiceRepository:
|
||||||
logging.error(f"伝票保存エラー: {e}")
|
logging.error(f"伝票保存エラー: {e}")
|
||||||
return False
|
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]:
|
def get_all_invoices(self, limit: int = 100) -> List[Invoice]:
|
||||||
"""全伝票を取得"""
|
"""全伝票を取得"""
|
||||||
invoices = []
|
invoices = []
|
||||||
|
|
@ -572,6 +599,66 @@ class CustomerRepository:
|
||||||
logging.error(f"顧客更新エラー: {e}")
|
logging.error(f"顧客更新エラー: {e}")
|
||||||
return False
|
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]:
|
def get_all_customers(self) -> List[Customer]:
|
||||||
"""全顧客を取得"""
|
"""全顧客を取得"""
|
||||||
customers = []
|
customers = []
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue