下書きが保存出来るようになった

This commit is contained in:
joe 2026-02-24 01:21:25 +09:00
parent d9aad0d35b
commit be14fa7eb2
2 changed files with 154 additions and 71 deletions

102
main.py
View file

@ -222,6 +222,21 @@ class FlutterStyleDashboard:
"amount_color": "#2F3C7E", "amount_color": "#2F3C7E",
"tag_text_color": "#4B4F67", "tag_text_color": "#4B4F67",
"tag_bg": "#E7E9FB", "tag_bg": "#E7E9FB",
"draft_card_bg": "#F5EEE4",
"draft_border": "#D7C4AF",
"draft_badge_bg": "#A7743A",
"draft_shadow_highlight": ft.BoxShadow(
blur_radius=8,
spread_radius=0,
color="#FFFFFF",
offset=ft.Offset(-2, -2),
),
"draft_shadow_depth": ft.BoxShadow(
blur_radius=14,
spread_radius=2,
color="#C3A88C",
offset=ft.Offset(4, 6),
),
"badge_bg": "#35C46B", "badge_bg": "#35C46B",
} }
self.doc_type_palette = { self.doc_type_palette = {
@ -443,24 +458,57 @@ class FlutterStyleDashboard:
is_locked = getattr(self.editing_invoice, 'final_locked', False) is_locked = getattr(self.editing_invoice, 'final_locked', False)
is_view_mode = not getattr(self, 'is_detail_edit_mode', False) is_view_mode = not getattr(self, 'is_detail_edit_mode', False)
is_draft = bool(getattr(self.editing_invoice, 'is_draft', False))
def set_doc_type(dt: DocumentType): def set_doc_type(dt: DocumentType):
self.select_document_type(dt) self.select_document_type(dt)
self.update_main_content() self.update_main_content()
doc_type_items = [ft.PopupMenuItem(content=ft.Text(dt.value), on_click=lambda _, d=dt: set_doc_type(d)) for dt in DocumentType] doc_type_items = [ft.PopupMenuItem(content=ft.Text(dt.value), on_click=lambda _, d=dt: set_doc_type(d)) for dt in DocumentType if dt != DocumentType.DRAFT]
# ドラフト切替
def toggle_draft(e):
self.editing_invoice.is_draft = bool(e.control.value)
self.update_main_content()
draft_toggle = ft.PopupMenuItem(
content=ft.Row([
ft.Text("下書き", size=12),
ft.Checkbox(value=is_draft, on_change=toggle_draft),
], alignment=ft.MainAxisAlignment.SPACE_BETWEEN, vertical_alignment=ft.CrossAxisAlignment.CENTER),
)
doc_type_items.insert(0, draft_toggle)
draft_suffix = " (下書き)" if is_draft else ""
if not getattr(self, 'is_detail_edit_mode', False): if not getattr(self, 'is_detail_edit_mode', False):
# 閲覧時は単なるテキスト表示 title_ctrl = ft.Text(
title_ctrl = ft.Text(self.selected_document_type.value, size=18, weight=ft.FontWeight.BOLD, color=ft.Colors.BLUE_GREY_800) f"{self.selected_document_type.value}{draft_suffix}",
size=18,
weight=ft.FontWeight.BOLD,
color=ft.Colors.BLUE_GREY_800,
)
doc_type_menu = None doc_type_menu = None
else: else:
doc_type_menu = ft.PopupMenuButton( doc_type_menu = ft.PopupMenuButton(
content=ft.Text(self.selected_document_type.value, size=18, weight=ft.FontWeight.BOLD, color=ft.Colors.BLUE_GREY_800), content=ft.Text(
f"{self.selected_document_type.value}{draft_suffix}",
size=18,
weight=ft.FontWeight.BOLD,
color=ft.Colors.BLUE_GREY_800,
),
items=doc_type_items, items=doc_type_items,
tooltip="帳票タイプ変更", tooltip="帳票タイプ変更",
) )
title_ctrl = doc_type_menu title_ctrl = doc_type_menu
draft_badge = ft.Container(
content=ft.Text("下書き", size=11, weight=ft.FontWeight.BOLD, color=ft.Colors.BROWN_800),
padding=ft.Padding.symmetric(horizontal=8, vertical=4),
bgcolor=ft.Colors.BROWN_100,
border_radius=12,
visible=is_draft,
)
app_bar = AppBar( app_bar = AppBar(
title="伝票詳細", title="伝票詳細",
show_back=True, show_back=True,
@ -470,11 +518,15 @@ class FlutterStyleDashboard:
action_icon=ft.Icons.SAVE if getattr(self, 'is_detail_edit_mode', False) else ft.Icons.EDIT, action_icon=ft.Icons.SAVE if getattr(self, 'is_detail_edit_mode', False) else ft.Icons.EDIT,
action_tooltip="保存" if getattr(self, 'is_detail_edit_mode', False) else "編集", action_tooltip="保存" if getattr(self, 'is_detail_edit_mode', False) else "編集",
bottom=None, bottom=None,
trailing_controls=[doc_type_menu] if doc_type_menu else [], trailing_controls=([draft_badge] + ([doc_type_menu] if doc_type_menu else [])) if is_draft else ([doc_type_menu] if doc_type_menu else []),
title_control=title_ctrl, title_control=title_ctrl,
) )
body = self._create_edit_existing_screen() body = self._create_edit_existing_screen()
body = ft.Container(
content=body,
bgcolor=ft.Colors.BROWN_50 if is_draft else None,
)
return ft.Column([ return ft.Column([
app_bar, app_bar,
@ -745,6 +797,7 @@ class FlutterStyleDashboard:
"document_type": invoice.document_type.value if getattr(invoice, "document_type", None) else None, "document_type": invoice.document_type.value if getattr(invoice, "document_type", None) else None,
"date": invoice.date.isoformat() if getattr(invoice, "date", None) else None, "date": invoice.date.isoformat() if getattr(invoice, "date", None) else None,
"notes": getattr(invoice, "notes", ""), "notes": getattr(invoice, "notes", ""),
"is_draft": bool(getattr(invoice, "is_draft", False)),
"customer": { "customer": {
"id": getattr(getattr(invoice, "customer", None), "id", None), "id": getattr(getattr(invoice, "customer", None), "id", None),
"formal_name": getattr(getattr(invoice, "customer", None), "formal_name", None), "formal_name": getattr(getattr(invoice, "customer", None), "formal_name", None),
@ -777,6 +830,8 @@ class FlutterStyleDashboard:
return True return True
if snapshot.get("notes", "") != getattr(invoice, "notes", ""): if snapshot.get("notes", "") != getattr(invoice, "notes", ""):
return True return True
if snapshot.get("is_draft", False) != bool(getattr(invoice, "is_draft", False)):
return True
snap_cust = snapshot.get("customer", {}) or {} snap_cust = snapshot.get("customer", {}) or {}
cust = getattr(invoice, "customer", None) cust = getattr(invoice, "customer", None)
@ -831,6 +886,7 @@ class FlutterStyleDashboard:
first_item = description or "(明細なし)" first_item = description or "(明細なし)"
extra_count = 0 extra_count = 0
is_draft_card = isinstance(slip, Invoice) and bool(getattr(slip, "is_draft", False))
icon_bg = palette.get(slip_type, theme["icon_default_bg"]) icon_bg = palette.get(slip_type, theme["icon_default_bg"])
display_amount = -abs(amount) if isinstance(slip, Invoice) and getattr(slip, "is_offset", False) else amount display_amount = -abs(amount) if isinstance(slip, Invoice) and getattr(slip, "is_offset", False) else amount
@ -938,6 +994,28 @@ class FlutterStyleDashboard:
status_chip = ft.Text("✓ LOCK", size=9, color=theme["tag_text_color"]) if final_locked else ft.Container() status_chip = ft.Text("✓ LOCK", size=9, color=theme["tag_text_color"]) if final_locked else ft.Container()
if is_draft_card:
status_chip = ft.Row(
[
ft.Container(
content=ft.Text("DRAFT", size=9, weight=ft.FontWeight.BOLD, color=ft.Colors.WHITE),
padding=ft.Padding.symmetric(horizontal=6, vertical=2),
bgcolor=theme["draft_badge_bg"],
border_radius=999,
),
status_chip or ft.Container(),
],
spacing=4,
vertical_alignment=ft.CrossAxisAlignment.CENTER,
)
card_bg = theme["draft_card_bg"] if is_draft_card else theme["card_bg"]
card_border = ft.Border.all(1, theme["draft_border"]) if is_draft_card else None
card_shadow = [
theme["draft_shadow_highlight"],
theme["draft_shadow_depth"]
] if is_draft_card else [theme["shadow"]]
card_body = ft.Container( card_body = ft.Container(
content=ft.Column( content=ft.Column(
[left_column, status_chip], [left_column, status_chip],
@ -945,9 +1023,10 @@ class FlutterStyleDashboard:
expand=True, expand=True,
), ),
padding=ft.Padding.symmetric(horizontal=12, vertical=8), padding=ft.Padding.symmetric(horizontal=12, vertical=8),
bgcolor=theme["card_bg"], bgcolor=card_bg,
border_radius=theme["card_radius"], border_radius=theme["card_radius"],
shadow=[theme["shadow"]], border=card_border,
shadow=card_shadow,
) )
return ft.GestureDetector( return ft.GestureDetector(
@ -1129,11 +1208,12 @@ class FlutterStyleDashboard:
customer=default_customer, customer=default_customer,
date=datetime.now(), date=datetime.now(),
items=[], items=[],
document_type=DocumentType.DRAFT, document_type=DocumentType.INVOICE,
invoice_number="NEW-" + str(int(datetime.now().timestamp())) invoice_number="NEW-" + str(int(datetime.now().timestamp()))
) )
self.editing_invoice.is_draft = True
self.selected_customer = default_customer self.selected_customer = default_customer
self.selected_document_type = DocumentType.DRAFT self.selected_document_type = DocumentType.INVOICE
self.is_detail_edit_mode = True self.is_detail_edit_mode = True
self.is_edit_mode = True self.is_edit_mode = True
self.is_customer_picker_open = False self.is_customer_picker_open = False
@ -1161,6 +1241,7 @@ class FlutterStyleDashboard:
"""既存伝票の編集画面(新規・編集共通)""" """既存伝票の編集画面(新規・編集共通)"""
# 編集不可チェック新規作成時はFalse # 編集不可チェック新規作成時はFalse
is_new_invoice = self.editing_invoice.invoice_number.startswith("NEW-") is_new_invoice = self.editing_invoice.invoice_number.startswith("NEW-")
edit_bg = ft.Colors.BROWN_50
# LOCK条件明示的に確定された場合のみLOCK # LOCK条件明示的に確定された場合のみLOCK
# PDF生成だけではLOCKしないお試しPDFを許可 # PDF生成だけではLOCKしないお試しPDFを許可
@ -1199,6 +1280,7 @@ class FlutterStyleDashboard:
value=self.editing_invoice.customer.name if self.editing_invoice.customer.name != "選択してください" else "", value=self.editing_invoice.customer.name if self.editing_invoice.customer.name != "選択してください" else "",
disabled=is_locked, disabled=is_locked,
width=260, width=260,
bgcolor=edit_bg,
) )
def update_customer_name(e): def update_customer_name(e):
@ -1319,6 +1401,7 @@ class FlutterStyleDashboard:
multiline=True, multiline=True,
min_lines=2, min_lines=2,
max_lines=3, max_lines=3,
bgcolor=edit_bg if not is_view_mode and not is_locked else None,
) )
def toggle_edit_mode(_): def toggle_edit_mode(_):
@ -2297,7 +2380,6 @@ class FlutterStyleDashboard:
self.selected_document_type = resolved_type self.selected_document_type = resolved_type
logging.info(f"帳票種類を選択: {resolved_type.value}") logging.info(f"帳票種類を選択: {resolved_type.value}")
self.update_main_content()
def create_slip(self, e=None): def create_slip(self, e=None):
"""伝票作成 - サービス層を使用""" """伝票作成 - サービス層を使用"""

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: