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],
products: List[Product],
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:
"""編集モードの明細テーブル。"""
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=35),
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),
)
data_rows.append(
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,
],
key=f"row-{i}-{item.description}",
)
subtotal_text = ft.Text(
f"¥{item.subtotal:,}",
size=12,
weight=ft.FontWeight.BOLD,
width=70,
text_align=ft.TextAlign.RIGHT,
)
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(
[

1304
main.py

File diff suppressed because it is too large Load diff

View file

@ -134,7 +134,8 @@ class InvoiceService:
document_type: DocumentType,
amount: int,
notes: str = "",
items: List[InvoiceItem] = None) -> Optional[Invoice]:
items: List[InvoiceItem] = None,
is_draft: bool = False) -> Optional[Invoice]:
"""新規伝票作成
Args:
@ -171,7 +172,8 @@ class InvoiceService:
date=datetime.now(),
items=items,
document_type=document_type,
notes=notes
notes=notes,
is_draft=is_draft
)
# --- 長期保管向け: 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_sha256", "pdf_sha256 TEXT")
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向けインデックス大量データ閲覧時の検索性能を確保
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,
pdf_template_version, company_info_version,
is_offset, offset_target_uuid,
pdf_generated_at, pdf_sha256, submitted_to_tax_authority)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
pdf_generated_at, pdf_sha256, submitted_to_tax_authority, is_draft)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', (
invoice.uuid,
invoice.document_type.value,
@ -319,7 +320,8 @@ class InvoiceRepository:
getattr(invoice, "offset_target_uuid", None),
getattr(invoice, "pdf_generated_at", 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
@ -355,7 +357,7 @@ class InvoiceRepository:
cursor.execute('''
UPDATE invoices
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 = ?
''', (
invoice.document_type.value,
@ -367,6 +369,7 @@ class InvoiceRepository:
invoice.invoice_number,
invoice.notes,
getattr(invoice, 'pdf_generated_at', None),
1 if getattr(invoice, 'is_draft', False) else 0,
invoice.uuid
))
@ -374,8 +377,9 @@ class InvoiceRepository:
cursor.execute('SELECT id FROM invoices WHERE uuid = ?', (invoice.uuid,))
result = cursor.fetchone()
if not result:
logging.error(f"伝票ID取得失敗: {invoice.uuid}")
return False
logging.warning(f"伝票ID取得失敗: {invoice.uuid} -> 新規保存にフォールバックします")
conn.rollback()
return self.save_invoice(invoice)
invoice_id = result[0]
# 明細を一度削除して再挿入
@ -524,75 +528,72 @@ class InvoiceRepository:
"""DB行をInvoiceオブジェクトに変換"""
try:
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 [])}
# 明細取得
cursor.execute('''
SELECT description, quantity, unit_price, is_discount
FROM invoice_items
WHERE invoice_id = ?
''', (invoice_id,))
def val(name, default=None):
idx = col_map.get(name)
if idx is None or idx >= len(row):
return default
return row[idx]
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 = [
InvoiceItem(
description=ir[0],
quantity=ir[1],
unit_price=ir[2],
is_discount=bool(ir[3])
is_discount=bool(ir[3]),
) 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(
customer=customer,
date=date_obj,
items=items,
file_path=row[13], # 正しいfile_pathフィールドのインデックス
invoice_number=row[11] or "", # 正しいinvoice_numberフィールドのインデックス
notes=row[12], # 正しいnotesフィールドのインデックス
file_path=val("file_path"),
invoice_number=val("invoice_number", ""),
notes=val("notes", ""),
document_type=doc_type,
uuid=row[1],
uuid=val("uuid"),
is_draft=is_draft,
)
# 監査用フィールド(存在していれば付与)
try:
inv.node_id = row[14]
inv.payload_json = row[15]
inv.payload_hash = row[16]
inv.prev_chain_hash = row[17]
inv.chain_hash = row[18]
inv.pdf_template_version = row[19]
inv.company_info_version = row[20]
inv.is_offset = bool(row[21])
inv.offset_target_uuid = row[22]
inv.pdf_generated_at = row[23]
inv.pdf_sha256 = row[24]
inv.submitted_to_tax_authority = bool(row[27])
except Exception:
pass
logging.info(f"変換成功: {inv.invoice_number}")
# 監査・チェーン関連のフィールドは動的属性として保持
inv.node_id = val("node_id")
inv.payload_json = val("payload_json")
inv.payload_hash = val("payload_hash")
inv.prev_chain_hash = val("prev_chain_hash")
inv.chain_hash = val("chain_hash")
inv.pdf_template_version = val("pdf_template_version")
inv.company_info_version = val("company_info_version")
inv.is_offset = bool(val("is_offset", 0))
inv.offset_target_uuid = val("offset_target_uuid")
inv.pdf_generated_at = val("pdf_generated_at")
inv.pdf_sha256 = val("pdf_sha256")
inv.submitted_to_tax_authority = bool(val("submitted_to_tax_authority", 0))
return inv
except Exception as e: