Compare commits

..

No commits in common. "8f9634fdf24618938f37ded00de60af9296a9c91" and "d9aad0d35b5411f8d111e17058b49bcb5b90026b" have entirely different histories.

4 changed files with 317 additions and 1197 deletions

View file

@ -110,14 +110,10 @@ 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),
@ -177,59 +173,26 @@ def build_invoice_items_edit_table(
on_click=lambda _, idx=i: on_delete_row(idx), on_click=lambda _, idx=i: on_delete_row(idx),
) )
subtotal_text = ft.Text( data_rows.append(
f"¥{item.subtotal:,}", ft.Row(
size=12, [
weight=ft.FontWeight.BOLD, product_field,
width=70, quantity_field,
text_align=ft.TextAlign.RIGHT, unit_price_field,
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}",
)
) )
if row_refs is not None: list_control: ft.Control = ft.Column(data_rows, spacing=4)
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(
[ [

1312
main.py

File diff suppressed because it is too large Load diff

View file

@ -134,8 +134,7 @@ class InvoiceService:
document_type: DocumentType, document_type: DocumentType,
amount: int, amount: int,
notes: str = "", notes: str = "",
items: List[InvoiceItem] = None, items: List[InvoiceItem] = None) -> Optional[Invoice]:
is_draft: bool = False) -> Optional[Invoice]:
"""新規伝票作成 """新規伝票作成
Args: Args:
@ -172,8 +171,7 @@ 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,7 +138,6 @@ 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)")
@ -293,8 +292,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, is_draft) pdf_generated_at, pdf_sha256, submitted_to_tax_authority)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', ( ''', (
invoice.uuid, invoice.uuid,
invoice.document_type.value, invoice.document_type.value,
@ -320,8 +319,7 @@ 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
@ -357,7 +355,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 = ?, is_draft = ?, updated_at = CURRENT_TIMESTAMP date = ?, invoice_number = ?, notes = ?, pdf_generated_at = ?, updated_at = CURRENT_TIMESTAMP
WHERE uuid = ? WHERE uuid = ?
''', ( ''', (
invoice.document_type.value, invoice.document_type.value,
@ -369,7 +367,6 @@ 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
)) ))
@ -377,9 +374,8 @@ 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.warning(f"伝票ID取得失敗: {invoice.uuid} -> 新規保存にフォールバックします") logging.error(f"伝票ID取得失敗: {invoice.uuid}")
conn.rollback() return False
return self.save_invoice(invoice)
invoice_id = result[0] invoice_id = result[0]
# 明細を一度削除して再挿入 # 明細を一度削除して再挿入
@ -528,72 +524,75 @@ class InvoiceRepository:
"""DB行をInvoiceオブジェクトに変換""" """DB行をInvoiceオブジェクトに変換"""
try: try:
invoice_id = row[0] invoice_id = row[0]
col_map = {desc[0]: idx for idx, desc in enumerate(cursor.description or [])} logging.info(f"変換開始: invoice_id={invoice_id}, row_length={len(row)}")
def val(name, default=None): # 明細取得
idx = col_map.get(name) cursor.execute('''
if idx is None or idx >= len(row): SELECT description, quantity, unit_price, is_discount
return default FROM invoice_items
return row[idx] WHERE invoice_id = ?
''', (invoice_id,))
# 基本フィールド
doc_type_val = val("document_type", DocumentType.INVOICE.value) item_rows = cursor.fetchall()
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=val("file_path"), file_path=row[13], # 正しいfile_pathフィールドのインデックス
invoice_number=val("invoice_number", ""), invoice_number=row[11] or "", # 正しいinvoice_numberフィールドのインデックス
notes=val("notes", ""), notes=row[12], # 正しいnotesフィールドのインデックス
document_type=doc_type, document_type=doc_type,
uuid=val("uuid"), uuid=row[1],
is_draft=is_draft,
) )
# 監査・チェーン関連のフィールドは動的属性として保持 # 監査用フィールド(存在していれば付与)
inv.node_id = val("node_id") try:
inv.payload_json = val("payload_json") inv.node_id = row[14]
inv.payload_hash = val("payload_hash") inv.payload_json = row[15]
inv.prev_chain_hash = val("prev_chain_hash") inv.payload_hash = row[16]
inv.chain_hash = val("chain_hash") inv.prev_chain_hash = row[17]
inv.pdf_template_version = val("pdf_template_version") inv.chain_hash = row[18]
inv.company_info_version = val("company_info_version") inv.pdf_template_version = row[19]
inv.is_offset = bool(val("is_offset", 0)) inv.company_info_version = row[20]
inv.offset_target_uuid = val("offset_target_uuid") inv.is_offset = bool(row[21])
inv.pdf_generated_at = val("pdf_generated_at") inv.offset_target_uuid = row[22]
inv.pdf_sha256 = val("pdf_sha256") inv.pdf_generated_at = row[23]
inv.submitted_to_tax_authority = bool(val("submitted_to_tax_authority", 0)) inv.pdf_sha256 = row[24]
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: