Compare commits

...

6 commits

Author SHA1 Message Date
joe
8f9634fdf2 検索バーをトップツールバーに収容 2026-02-24 10:40:00 +09:00
joe
ccd549dc0c 検索バーを検索窓へ途中 2026-02-24 10:29:10 +09:00
joe
0f86ee14ac mini汚染を修復 2026-02-24 10:10:16 +09:00
joe
3e4af336f8 テーマの準備 2026-02-24 02:26:13 +09:00
joe
25032d3b9b 編集画面の充実 2026-02-24 02:04:49 +09:00
joe
be14fa7eb2 下書きが保存出来るようになった 2026-02-24 01:21:25 +09:00
4 changed files with 1198 additions and 318 deletions

View file

@ -110,10 +110,14 @@ def build_invoice_items_edit_table(
on_delete_row: Callable[[int], None], on_delete_row: Callable[[int], None],
products: List[Product], products: List[Product],
on_product_select: Callable[[int], None] | None = None, on_product_select: Callable[[int], None] | None = None,
row_refs: Optional[dict] = None,
enable_reorder: bool = False,
on_reorder: Optional[Callable[[int, int], None]] = None,
) -> ft.Column: ) -> ft.Column:
"""編集モードの明細テーブル。""" """編集モードの明細テーブル。"""
header_row = ft.Row( header_row = ft.Row(
[ [
ft.Container(width=32),
ft.Text("商品", size=12, weight=ft.FontWeight.BOLD, width=180), ft.Text("商品", size=12, weight=ft.FontWeight.BOLD, width=180),
ft.Text("", size=12, weight=ft.FontWeight.BOLD, width=35), 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),
@ -173,26 +177,59 @@ def build_invoice_items_edit_table(
on_click=lambda _, idx=i: on_delete_row(idx), on_click=lambda _, idx=i: on_delete_row(idx),
) )
data_rows.append( subtotal_text = ft.Text(
ft.Row( f"¥{item.subtotal:,}",
[ size=12,
product_field, weight=ft.FontWeight.BOLD,
quantity_field, width=70,
unit_price_field, text_align=ft.TextAlign.RIGHT,
ft.Text(
f"¥{item.subtotal:,}",
size=12,
weight=ft.FontWeight.BOLD,
width=70,
text_align=ft.TextAlign.RIGHT,
),
delete_button,
],
key=f"row-{i}-{item.description}",
)
) )
list_control: ft.Control = ft.Column(data_rows, spacing=4) if row_refs is not None:
row_refs[i] = {
"product": product_field,
"quantity": quantity_field,
"unit_price": unit_price_field,
"subtotal": subtotal_text,
}
if enable_reorder and on_reorder and not is_locked:
up_button = ft.IconButton(
ft.Icons.ARROW_UPWARD,
icon_size=16,
tooltip="上へ",
disabled=i == 0,
on_click=(lambda _, idx=i: on_reorder(idx, idx - 1)) if i > 0 else None,
)
down_button = ft.IconButton(
ft.Icons.ARROW_DOWNWARD,
icon_size=16,
tooltip="下へ",
disabled=i == len(items) - 1,
on_click=(lambda _, idx=i: on_reorder(idx, idx + 1)) if i < len(items) - 1 else None,
)
reorder_buttons = ft.Column([up_button, down_button], spacing=0, width=32)
delete_slot = ft.Container(width=0)
else:
reorder_buttons = ft.Container(width=0)
delete_slot = delete_button
row_control = ft.Row(
[
reorder_buttons,
product_field,
quantity_field,
unit_price_field,
subtotal_text,
delete_slot,
],
vertical_alignment=ft.CrossAxisAlignment.CENTER,
key=f"row-{i}-{item.description}",
)
data_rows.append(row_control)
list_control = ft.Column(data_rows, spacing=4)
return ft.Column( return ft.Column(
[ [

1314
main.py

File diff suppressed because it is too large Load diff

View file

@ -134,7 +134,8 @@ class InvoiceService:
document_type: DocumentType, document_type: DocumentType,
amount: int, amount: int,
notes: str = "", notes: str = "",
items: List[InvoiceItem] = None) -> Optional[Invoice]: items: List[InvoiceItem] = None,
is_draft: bool = False) -> Optional[Invoice]:
"""新規伝票作成 """新規伝票作成
Args: Args:
@ -171,7 +172,8 @@ class InvoiceService:
date=datetime.now(), date=datetime.now(),
items=items, items=items,
document_type=document_type, document_type=document_type,
notes=notes notes=notes,
is_draft=is_draft
) )
# --- 長期保管向け: canonical payload + hash chain --- # --- 長期保管向け: canonical payload + hash chain ---

View file

@ -138,6 +138,7 @@ class InvoiceRepository:
ensure_column("invoices", "pdf_generated_at", "pdf_generated_at TEXT") ensure_column("invoices", "pdf_generated_at", "pdf_generated_at TEXT")
ensure_column("invoices", "pdf_sha256", "pdf_sha256 TEXT") ensure_column("invoices", "pdf_sha256", "pdf_sha256 TEXT")
ensure_column("invoices", "submitted_to_tax_authority", "submitted_to_tax_authority INTEGER DEFAULT 0") ensure_column("invoices", "submitted_to_tax_authority", "submitted_to_tax_authority INTEGER DEFAULT 0")
ensure_column("invoices", "is_draft", "is_draft INTEGER DEFAULT 0")
# Explorer向けインデックス大量データ閲覧時の検索性能を確保 # Explorer向けインデックス大量データ閲覧時の検索性能を確保
cursor.execute("CREATE INDEX IF NOT EXISTS idx_invoices_date ON invoices(date)") cursor.execute("CREATE INDEX IF NOT EXISTS idx_invoices_date ON invoices(date)")
@ -292,8 +293,8 @@ class InvoiceRepository:
payload_json, payload_hash, prev_chain_hash, chain_hash, payload_json, payload_hash, prev_chain_hash, chain_hash,
pdf_template_version, company_info_version, pdf_template_version, company_info_version,
is_offset, offset_target_uuid, is_offset, offset_target_uuid,
pdf_generated_at, pdf_sha256, submitted_to_tax_authority) pdf_generated_at, pdf_sha256, submitted_to_tax_authority, is_draft)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', ( ''', (
invoice.uuid, invoice.uuid,
invoice.document_type.value, invoice.document_type.value,
@ -319,7 +320,8 @@ class InvoiceRepository:
getattr(invoice, "offset_target_uuid", None), getattr(invoice, "offset_target_uuid", None),
getattr(invoice, "pdf_generated_at", None), getattr(invoice, "pdf_generated_at", None),
getattr(invoice, "pdf_sha256", None), getattr(invoice, "pdf_sha256", None),
0 # submitted_to_tax_authority = false by default 0, # submitted_to_tax_authority = false by default
1 if getattr(invoice, "is_draft", False) else 0
)) ))
invoice_id = cursor.lastrowid invoice_id = cursor.lastrowid
@ -355,7 +357,7 @@ class InvoiceRepository:
cursor.execute(''' cursor.execute('''
UPDATE invoices UPDATE invoices
SET document_type = ?, customer_id = ?, customer_name = ?, customer_address = ?, customer_phone = ?, SET document_type = ?, customer_id = ?, customer_name = ?, customer_address = ?, customer_phone = ?,
date = ?, invoice_number = ?, notes = ?, pdf_generated_at = ?, updated_at = CURRENT_TIMESTAMP date = ?, invoice_number = ?, notes = ?, pdf_generated_at = ?, is_draft = ?, updated_at = CURRENT_TIMESTAMP
WHERE uuid = ? WHERE uuid = ?
''', ( ''', (
invoice.document_type.value, invoice.document_type.value,
@ -367,6 +369,7 @@ class InvoiceRepository:
invoice.invoice_number, invoice.invoice_number,
invoice.notes, invoice.notes,
getattr(invoice, 'pdf_generated_at', None), getattr(invoice, 'pdf_generated_at', None),
1 if getattr(invoice, 'is_draft', False) else 0,
invoice.uuid invoice.uuid
)) ))
@ -374,8 +377,9 @@ class InvoiceRepository:
cursor.execute('SELECT id FROM invoices WHERE uuid = ?', (invoice.uuid,)) cursor.execute('SELECT id FROM invoices WHERE uuid = ?', (invoice.uuid,))
result = cursor.fetchone() result = cursor.fetchone()
if not result: if not result:
logging.error(f"伝票ID取得失敗: {invoice.uuid}") logging.warning(f"伝票ID取得失敗: {invoice.uuid} -> 新規保存にフォールバックします")
return False conn.rollback()
return self.save_invoice(invoice)
invoice_id = result[0] invoice_id = result[0]
# 明細を一度削除して再挿入 # 明細を一度削除して再挿入
@ -524,75 +528,72 @@ class InvoiceRepository:
"""DB行をInvoiceオブジェクトに変換""" """DB行をInvoiceオブジェクトに変換"""
try: try:
invoice_id = row[0] invoice_id = row[0]
logging.info(f"変換開始: invoice_id={invoice_id}, row_length={len(row)}") col_map = {desc[0]: idx for idx, desc in enumerate(cursor.description or [])}
# 明細取得 def val(name, default=None):
cursor.execute(''' idx = col_map.get(name)
SELECT description, quantity, unit_price, is_discount if idx is None or idx >= len(row):
FROM invoice_items return default
WHERE invoice_id = ? return row[idx]
''', (invoice_id,))
# 基本フィールド
item_rows = cursor.fetchall() doc_type_val = val("document_type", DocumentType.INVOICE.value)
doc_type = DocumentType(doc_type_val) if doc_type_val in DocumentType._value2member_map_ else DocumentType.INVOICE
date_str = val("date") or datetime.now().isoformat()
date_obj = datetime.fromisoformat(date_str)
customer_name = val("customer_name", "")
customer = Customer(
id=val("customer_id", 0) or 0,
name=customer_name or "",
formal_name=customer_name or "",
address=val("customer_address", "") or "",
phone=val("customer_phone", "") or "",
)
is_draft = bool(val("is_draft", 0))
# 明細取得は別カーソルで行い、元カーソルのdescriptionを汚染しない
item_cursor = cursor.connection.cursor()
item_cursor.execute(
'SELECT description, quantity, unit_price, is_discount, product_id FROM invoice_items WHERE invoice_id = ?',
(invoice_id,),
)
item_rows = item_cursor.fetchall()
items = [ items = [
InvoiceItem( InvoiceItem(
description=ir[0], description=ir[0],
quantity=ir[1], quantity=ir[1],
unit_price=ir[2], unit_price=ir[2],
is_discount=bool(ir[3]) is_discount=bool(ir[3]),
) for ir in item_rows ) for ir in item_rows
] ]
# 顧客情報
customer = Customer(
id=row[3] or 0, # customer_idフィールド
name=row[4], # customer_nameフィールド
formal_name=row[4], # customer_nameフィールド
address=row[5] or "", # customer_addressフィールド
phone=row[6] or "" # customer_phoneフィールド
)
# 伝票タイプ
doc_type = DocumentType.SALES
for dt in DocumentType:
if dt.value == row[2]:
doc_type = dt
break
# 日付変換
date_str = row[10] # 正しいdateフィールドのインデックス
logging.info(f"日付変換: {date_str}")
date_obj = datetime.fromisoformat(date_str)
inv = Invoice( inv = Invoice(
customer=customer, customer=customer,
date=date_obj, date=date_obj,
items=items, items=items,
file_path=row[13], # 正しいfile_pathフィールドのインデックス file_path=val("file_path"),
invoice_number=row[11] or "", # 正しいinvoice_numberフィールドのインデックス invoice_number=val("invoice_number", ""),
notes=row[12], # 正しいnotesフィールドのインデックス notes=val("notes", ""),
document_type=doc_type, document_type=doc_type,
uuid=row[1], uuid=val("uuid"),
is_draft=is_draft,
) )
# 監査用フィールド(存在していれば付与) # 監査・チェーン関連のフィールドは動的属性として保持
try: inv.node_id = val("node_id")
inv.node_id = row[14] inv.payload_json = val("payload_json")
inv.payload_json = row[15] inv.payload_hash = val("payload_hash")
inv.payload_hash = row[16] inv.prev_chain_hash = val("prev_chain_hash")
inv.prev_chain_hash = row[17] inv.chain_hash = val("chain_hash")
inv.chain_hash = row[18] inv.pdf_template_version = val("pdf_template_version")
inv.pdf_template_version = row[19] inv.company_info_version = val("company_info_version")
inv.company_info_version = row[20] inv.is_offset = bool(val("is_offset", 0))
inv.is_offset = bool(row[21]) inv.offset_target_uuid = val("offset_target_uuid")
inv.offset_target_uuid = row[22] inv.pdf_generated_at = val("pdf_generated_at")
inv.pdf_generated_at = row[23] inv.pdf_sha256 = val("pdf_sha256")
inv.pdf_sha256 = row[24] inv.submitted_to_tax_authority = bool(val("submitted_to_tax_authority", 0))
inv.submitted_to_tax_authority = bool(row[27])
except Exception:
pass
logging.info(f"変換成功: {inv.invoice_number}")
return inv return inv
except Exception as e: except Exception as e: