Compare commits
No commits in common. "8f9634fdf24618938f37ded00de60af9296a9c91" and "d9aad0d35b5411f8d111e17058b49bcb5b90026b" have entirely different histories.
8f9634fdf2
...
d9aad0d35b
4 changed files with 317 additions and 1197 deletions
|
|
@ -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(
|
||||||
[
|
[
|
||||||
|
|
|
||||||
|
|
@ -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 ---
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue