電子帳簿保存法
This commit is contained in:
parent
97f2843620
commit
22c4a2e6b7
7 changed files with 487 additions and 20 deletions
|
|
@ -32,6 +32,8 @@ class FlutterStyleDashboard:
|
||||||
self.is_customer_picker_open = False
|
self.is_customer_picker_open = False
|
||||||
self.customer_search_query = ""
|
self.customer_search_query = ""
|
||||||
self.show_offsets = False
|
self.show_offsets = False
|
||||||
|
self.chain_verify_result = None
|
||||||
|
self.is_new_customer_form_open = False
|
||||||
|
|
||||||
# ビジネスロジックサービス
|
# ビジネスロジックサービス
|
||||||
self.app_service = AppService()
|
self.app_service = AppService()
|
||||||
|
|
@ -140,7 +142,9 @@ class FlutterStyleDashboard:
|
||||||
self.main_content.controls.clear()
|
self.main_content.controls.clear()
|
||||||
|
|
||||||
if self.current_tab == 0:
|
if self.current_tab == 0:
|
||||||
if self.is_customer_picker_open:
|
if self.is_new_customer_form_open:
|
||||||
|
self.main_content.controls.append(self.create_new_customer_screen())
|
||||||
|
elif self.is_customer_picker_open:
|
||||||
self.main_content.controls.append(self.create_customer_picker_screen())
|
self.main_content.controls.append(self.create_customer_picker_screen())
|
||||||
else:
|
else:
|
||||||
# 新規作成画面
|
# 新規作成画面
|
||||||
|
|
@ -208,6 +212,13 @@ class FlutterStyleDashboard:
|
||||||
[
|
[
|
||||||
ft.IconButton(ft.Icons.ARROW_BACK, on_click=back),
|
ft.IconButton(ft.Icons.ARROW_BACK, on_click=back),
|
||||||
ft.Text("顧客を選択", size=18, weight=ft.FontWeight.BOLD),
|
ft.Text("顧客を選択", size=18, weight=ft.FontWeight.BOLD),
|
||||||
|
ft.Container(expand=True),
|
||||||
|
ft.IconButton(
|
||||||
|
ft.Icons.PERSON_ADD,
|
||||||
|
tooltip="新規顧客追加",
|
||||||
|
icon_color=ft.Colors.WHITE,
|
||||||
|
on_click=lambda _: self.open_new_customer_form(),
|
||||||
|
),
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
padding=ft.padding.all(15),
|
padding=ft.padding.all(15),
|
||||||
|
|
@ -237,8 +248,8 @@ class FlutterStyleDashboard:
|
||||||
ft.Container(
|
ft.Container(
|
||||||
content=ft.Button(
|
content=ft.Button(
|
||||||
content=ft.Text(doc_type.value, size=12),
|
content=ft.Text(doc_type.value, size=12),
|
||||||
bgcolor=ft.Colors.BLUE_GREY_800 if i == 0 else ft.Colors.GREY_300,
|
bgcolor=ft.Colors.BLUE_GREY_800 if doc_type == self.selected_document_type else ft.Colors.GREY_300,
|
||||||
color=ft.Colors.WHITE if i == 0 else ft.Colors.BLACK,
|
color=ft.Colors.WHITE if doc_type == self.selected_document_type else ft.Colors.BLACK,
|
||||||
on_click=lambda _, idx=i, dt=doc_type: self.select_document_type(dt.value),
|
on_click=lambda _, idx=i, dt=doc_type: self.select_document_type(dt.value),
|
||||||
width=100,
|
width=100,
|
||||||
height=45,
|
height=45,
|
||||||
|
|
@ -337,6 +348,11 @@ class FlutterStyleDashboard:
|
||||||
def on_toggle_offsets(e):
|
def on_toggle_offsets(e):
|
||||||
self.show_offsets = bool(e.control.value)
|
self.show_offsets = bool(e.control.value)
|
||||||
self.update_main_content()
|
self.update_main_content()
|
||||||
|
|
||||||
|
def on_verify_chain(e=None):
|
||||||
|
res = self.app_service.invoice.invoice_repo.verify_chain()
|
||||||
|
self.chain_verify_result = res
|
||||||
|
self.update_main_content()
|
||||||
|
|
||||||
# 履歴カードリスト
|
# 履歴カードリスト
|
||||||
slip_cards = []
|
slip_cards = []
|
||||||
|
|
@ -351,6 +367,12 @@ class FlutterStyleDashboard:
|
||||||
content=ft.Row([
|
content=ft.Row([
|
||||||
ft.Text("📄 発行履歴管理", size=20, weight=ft.FontWeight.BOLD),
|
ft.Text("📄 発行履歴管理", size=20, weight=ft.FontWeight.BOLD),
|
||||||
ft.Container(expand=True),
|
ft.Container(expand=True),
|
||||||
|
ft.IconButton(
|
||||||
|
ft.Icons.VERIFIED,
|
||||||
|
tooltip="チェーン検証",
|
||||||
|
icon_color=ft.Colors.BLUE_300,
|
||||||
|
on_click=on_verify_chain,
|
||||||
|
),
|
||||||
ft.Row(
|
ft.Row(
|
||||||
[
|
[
|
||||||
ft.Text("赤伝を表示", size=12, color=ft.Colors.WHITE),
|
ft.Text("赤伝を表示", size=12, color=ft.Colors.WHITE),
|
||||||
|
|
@ -363,6 +385,12 @@ class FlutterStyleDashboard:
|
||||||
padding=ft.padding.all(15),
|
padding=ft.padding.all(15),
|
||||||
bgcolor=ft.Colors.BLUE_GREY,
|
bgcolor=ft.Colors.BLUE_GREY,
|
||||||
),
|
),
|
||||||
|
|
||||||
|
# 検証結果表示(あれば)
|
||||||
|
ft.Container(
|
||||||
|
content=self._build_chain_verify_result(),
|
||||||
|
margin=ft.Margin.only(bottom=10),
|
||||||
|
) if self.chain_verify_result else ft.Container(height=0),
|
||||||
|
|
||||||
# 履歴リスト
|
# 履歴リスト
|
||||||
ft.Column(
|
ft.Column(
|
||||||
|
|
@ -410,19 +438,49 @@ class FlutterStyleDashboard:
|
||||||
self.invoices = self.app_service.invoice.get_recent_invoices(20)
|
self.invoices = self.app_service.invoice.get_recent_invoices(20)
|
||||||
self.update_main_content()
|
self.update_main_content()
|
||||||
|
|
||||||
|
def regenerate_and_share(_=None):
|
||||||
|
if not isinstance(slip, Invoice):
|
||||||
|
return
|
||||||
|
pdf_path = self.app_service.invoice.regenerate_pdf(slip.uuid)
|
||||||
|
if pdf_path:
|
||||||
|
# TODO: OSの共有ダイアログを呼ぶ(プラットフォーム依存)
|
||||||
|
logging.info(f"PDF再生成: {pdf_path}(共有機能は未実装)")
|
||||||
|
# 共有後に削除(今は即削除)
|
||||||
|
self.app_service.invoice.delete_pdf_file(pdf_path)
|
||||||
|
logging.info(f"PDF削除完了: {pdf_path}")
|
||||||
|
else:
|
||||||
|
logging.error("PDF再生成失敗")
|
||||||
|
|
||||||
actions_row = None
|
actions_row = None
|
||||||
if isinstance(slip, Invoice) and not getattr(slip, "is_offset", False):
|
if isinstance(slip, Invoice):
|
||||||
actions_row = ft.Row(
|
buttons = []
|
||||||
[
|
if not getattr(slip, "submitted_to_tax_authority", False):
|
||||||
|
buttons.append(
|
||||||
ft.IconButton(
|
ft.IconButton(
|
||||||
ft.Icons.REPLAY_CIRCLE_FILLED,
|
ft.Icons.REPLAY_CIRCLE_FILLED,
|
||||||
tooltip="赤伝(相殺)を発行",
|
tooltip="赤伝(相殺)を発行",
|
||||||
icon_color=ft.Colors.RED_400,
|
icon_color=ft.Colors.RED_400,
|
||||||
on_click=issue_offset,
|
on_click=issue_offset,
|
||||||
)
|
)
|
||||||
],
|
)
|
||||||
alignment=ft.MainAxisAlignment.END,
|
if not getattr(slip, "submitted_to_tax_authority", False):
|
||||||
|
buttons.append(
|
||||||
|
ft.IconButton(
|
||||||
|
ft.Icons.CHECK_CIRCLE,
|
||||||
|
tooltip="税務署提出済みに設定",
|
||||||
|
icon_color=ft.Colors.ORANGE_400,
|
||||||
|
on_click=lambda _: self.submit_invoice_for_tax(slip.uuid),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
buttons.append(
|
||||||
|
ft.IconButton(
|
||||||
|
ft.Icons.DOWNLOAD,
|
||||||
|
tooltip="PDF再生成→共有",
|
||||||
|
icon_color=ft.Colors.BLUE_400,
|
||||||
|
on_click=regenerate_and_share,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
actions_row = ft.Row(buttons, alignment=ft.MainAxisAlignment.END)
|
||||||
|
|
||||||
display_amount = amount
|
display_amount = amount
|
||||||
if isinstance(slip, Invoice) and getattr(slip, "is_offset", False):
|
if isinstance(slip, Invoice) and getattr(slip, "is_offset", False):
|
||||||
|
|
@ -458,6 +516,106 @@ class FlutterStyleDashboard:
|
||||||
elevation=3,
|
elevation=3,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _build_chain_verify_result(self) -> ft.Control:
|
||||||
|
if not self.chain_verify_result:
|
||||||
|
return ft.Container(height=0)
|
||||||
|
r = self.chain_verify_result
|
||||||
|
ok = r.get("ok", False)
|
||||||
|
checked = r.get("checked", 0)
|
||||||
|
errors = r.get("errors", [])
|
||||||
|
if ok:
|
||||||
|
return ft.Container(
|
||||||
|
content=ft.Row([
|
||||||
|
ft.Icon(ft.Icons.CHECK_CIRCLE, color=ft.Colors.GREEN, size=20),
|
||||||
|
ft.Text(f"チェーン検証 OK ({checked}件)", size=14, color=ft.Colors.GREEN),
|
||||||
|
]),
|
||||||
|
bgcolor=ft.Colors.GREEN_50,
|
||||||
|
padding=ft.Padding.all(10),
|
||||||
|
border_radius=8,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return ft.Container(
|
||||||
|
content=ft.Column([
|
||||||
|
ft.Row([
|
||||||
|
ft.Icon(ft.Icons.ERROR, color=ft.Colors.RED, size=20),
|
||||||
|
ft.Text(f"チェーン検証 NG (checked={checked})", size=14, color=ft.Colors.RED),
|
||||||
|
]),
|
||||||
|
ft.Text(f"エラー: {errors}", size=12, color=ft.Colors.RED_700),
|
||||||
|
]),
|
||||||
|
bgcolor=ft.Colors.RED_50,
|
||||||
|
padding=ft.Padding.all(10),
|
||||||
|
border_radius=8,
|
||||||
|
)
|
||||||
|
|
||||||
|
def open_new_customer_form(self):
|
||||||
|
"""新規顧客フォームを開く(画面内遷移)"""
|
||||||
|
self.is_new_customer_form_open = True
|
||||||
|
self.update_main_content()
|
||||||
|
|
||||||
|
def create_new_customer_screen(self) -> ft.Container:
|
||||||
|
"""新規顧客登録画面"""
|
||||||
|
name_field = ft.TextField(label="顧客名(略称)")
|
||||||
|
formal_name_field = ft.TextField(label="正式名称")
|
||||||
|
address_field = ft.TextField(label="住所")
|
||||||
|
phone_field = ft.TextField(label="電話番号")
|
||||||
|
|
||||||
|
def save_customer(_):
|
||||||
|
name = (name_field.value or "").strip()
|
||||||
|
formal_name = (formal_name_field.value or "").strip()
|
||||||
|
address = (address_field.value or "").strip()
|
||||||
|
phone = (phone_field.value or "").strip()
|
||||||
|
if not name or not formal_name:
|
||||||
|
# TODO: エラー表示
|
||||||
|
return
|
||||||
|
new_customer = self.app_service.customer.create_customer(name, formal_name, address, phone)
|
||||||
|
if new_customer:
|
||||||
|
self.customers = self.app_service.customer.get_all_customers()
|
||||||
|
self.selected_customer = new_customer
|
||||||
|
logging.info(f"新規顧客登録: {new_customer.formal_name}")
|
||||||
|
self.is_customer_picker_open = False
|
||||||
|
self.is_new_customer_form_open = False
|
||||||
|
self.update_main_content()
|
||||||
|
else:
|
||||||
|
logging.error("新規顧客登録失敗")
|
||||||
|
|
||||||
|
def cancel(_):
|
||||||
|
self.is_new_customer_form_open = False
|
||||||
|
self.update_main_content()
|
||||||
|
|
||||||
|
return ft.Container(
|
||||||
|
content=ft.Column([
|
||||||
|
ft.Container(
|
||||||
|
content=ft.Row([
|
||||||
|
ft.IconButton(ft.Icons.ARROW_BACK, on_click=cancel),
|
||||||
|
ft.Text("新規顧客登録", size=18, weight=ft.FontWeight.BOLD),
|
||||||
|
]),
|
||||||
|
padding=ft.padding.all(15),
|
||||||
|
bgcolor=ft.Colors.BLUE_GREY,
|
||||||
|
),
|
||||||
|
ft.Container(
|
||||||
|
content=ft.Column([
|
||||||
|
ft.Text("顧客情報を入力", size=16, weight=ft.FontWeight.BOLD),
|
||||||
|
ft.Container(height=10),
|
||||||
|
name_field,
|
||||||
|
ft.Container(height=10),
|
||||||
|
formal_name_field,
|
||||||
|
ft.Container(height=10),
|
||||||
|
address_field,
|
||||||
|
ft.Container(height=10),
|
||||||
|
phone_field,
|
||||||
|
ft.Container(height=20),
|
||||||
|
ft.Row([
|
||||||
|
ft.Button("保存", on_click=save_customer, bgcolor=ft.Colors.BLUE_GREY_800, color=ft.Colors.WHITE),
|
||||||
|
ft.Button("キャンセル", on_click=cancel),
|
||||||
|
], spacing=10),
|
||||||
|
]),
|
||||||
|
padding=ft.padding.all(20),
|
||||||
|
expand=True,
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
expand=True,
|
||||||
|
)
|
||||||
|
|
||||||
def open_customer_picker(self, e=None):
|
def open_customer_picker(self, e=None):
|
||||||
"""顧客選択を開く(画面内遷移)"""
|
"""顧客選択を開く(画面内遷移)"""
|
||||||
logging.info("顧客選択画面へ遷移")
|
logging.info("顧客選択画面へ遷移")
|
||||||
|
|
@ -499,6 +657,7 @@ class FlutterStyleDashboard:
|
||||||
if dt.value == doc_type:
|
if dt.value == doc_type:
|
||||||
self.selected_document_type = dt
|
self.selected_document_type = dt
|
||||||
logging.info(f"帳票種類を選択: {doc_type}")
|
logging.info(f"帳票種類を選択: {doc_type}")
|
||||||
|
self.update_main_content()
|
||||||
break
|
break
|
||||||
|
|
||||||
def create_slip(self, e=None):
|
def create_slip(self, e=None):
|
||||||
|
|
|
||||||
182
audit_export.py
Normal file
182
audit_export.py
Normal file
|
|
@ -0,0 +1,182 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
監査・移行用エクスポートツール
|
||||||
|
- SQLite DB から伝票単位で JSON Lines を出力
|
||||||
|
- ハッシュチェーン検証結果と説明文も出力
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sqlite3
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import List, Dict, Any
|
||||||
|
|
||||||
|
def export_invoices_as_jsonl(db_path: str, out_path: str) -> None:
|
||||||
|
"""DBから伝票を1行JSONでエクスポート"""
|
||||||
|
conn = sqlite3.connect(db_path)
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
cur.execute("""
|
||||||
|
SELECT
|
||||||
|
i.uuid,
|
||||||
|
i.document_type,
|
||||||
|
i.customer_name,
|
||||||
|
i.customer_address,
|
||||||
|
i.customer_phone,
|
||||||
|
i.amount,
|
||||||
|
i.tax,
|
||||||
|
i.total_amount,
|
||||||
|
i.date,
|
||||||
|
i.invoice_number,
|
||||||
|
i.notes,
|
||||||
|
i.node_id,
|
||||||
|
i.payload_json,
|
||||||
|
i.payload_hash,
|
||||||
|
i.prev_chain_hash,
|
||||||
|
i.chain_hash,
|
||||||
|
i.pdf_template_version,
|
||||||
|
i.company_info_version,
|
||||||
|
i.is_offset,
|
||||||
|
i.offset_target_uuid,
|
||||||
|
i.pdf_generated_at,
|
||||||
|
i.pdf_sha256,
|
||||||
|
GROUP_CONCAT(
|
||||||
|
json_object(
|
||||||
|
'description', ii.description,
|
||||||
|
'quantity', ii.quantity,
|
||||||
|
'unit_price', ii.unit_price,
|
||||||
|
'is_discount', ii.is_discount
|
||||||
|
), ','
|
||||||
|
) AS items_json
|
||||||
|
FROM invoices i
|
||||||
|
LEFT JOIN invoice_items ii ON i.id = ii.invoice_id
|
||||||
|
GROUP BY i.id
|
||||||
|
ORDER BY i.id ASC
|
||||||
|
""")
|
||||||
|
|
||||||
|
with open(out_path, "w", encoding="utf-8") as f:
|
||||||
|
for row in cur:
|
||||||
|
items = json.loads(row["items_json"] or "[]")
|
||||||
|
record = {
|
||||||
|
"uuid": row["uuid"],
|
||||||
|
"document_type": row["document_type"],
|
||||||
|
"customer": {
|
||||||
|
"name": row["customer_name"],
|
||||||
|
"address": row["customer_address"],
|
||||||
|
"phone": row["customer_phone"],
|
||||||
|
},
|
||||||
|
"amount": row["amount"],
|
||||||
|
"tax": row["tax"],
|
||||||
|
"total_amount": row["total_amount"],
|
||||||
|
"date": row["date"],
|
||||||
|
"invoice_number": row["invoice_number"],
|
||||||
|
"notes": row["notes"],
|
||||||
|
"node_id": row["node_id"],
|
||||||
|
"payload_json": json.loads(row["payload_json"]) if row["payload_json"] else None,
|
||||||
|
"payload_hash": row["payload_hash"],
|
||||||
|
"prev_chain_hash": row["prev_chain_hash"],
|
||||||
|
"chain_hash": row["chain_hash"],
|
||||||
|
"pdf_template_version": row["pdf_template_version"],
|
||||||
|
"company_info_version": row["company_info_version"],
|
||||||
|
"is_offset": bool(row["is_offset"]),
|
||||||
|
"offset_target_uuid": row["offset_target_uuid"],
|
||||||
|
"pdf_generated_at": row["pdf_generated_at"],
|
||||||
|
"pdf_sha256": row["pdf_sha256"],
|
||||||
|
"items": items,
|
||||||
|
}
|
||||||
|
f.write(json.dumps(record, ensure_ascii=False) + "\n")
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
def generate_audit_summary(db_path: str) -> Dict[str, Any]:
|
||||||
|
"""ハッシュチェーン検証とサマリーを返す"""
|
||||||
|
from services.repositories import InvoiceRepository
|
||||||
|
repo = InvoiceRepository(db_path)
|
||||||
|
result = repo.verify_chain()
|
||||||
|
# 件数
|
||||||
|
conn = sqlite3.connect(db_path)
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute("SELECT COUNT(*) FROM invoices")
|
||||||
|
total = cur.fetchone()[0]
|
||||||
|
cur.execute("SELECT COUNT(*) FROM invoices WHERE is_offset=1")
|
||||||
|
offsets = cur.fetchone()[0]
|
||||||
|
conn.close()
|
||||||
|
return {
|
||||||
|
"generated_at": datetime.now().isoformat(),
|
||||||
|
"db_path": db_path,
|
||||||
|
"total_invoices": total,
|
||||||
|
"offset_invoices": offsets,
|
||||||
|
"chain_verification": result,
|
||||||
|
}
|
||||||
|
|
||||||
|
def write_audit_readme(out_dir: Path) -> None:
|
||||||
|
"""監査用説明文(README)を出力"""
|
||||||
|
readme = """# 監査・移出パッケージ
|
||||||
|
|
||||||
|
## 概要
|
||||||
|
本パッケージは「販売アシスト1号」のSQLite DBからエクスポートした伝票データと、ハッシュチェーン検証結果を含みます。
|
||||||
|
|
||||||
|
## ファイル
|
||||||
|
- `invoices.jsonl` : 伝票1件ごとに1行のJSON(payload_json含む)
|
||||||
|
- `audit_summary.json` : 全件数・赤伝件数・チェーン検証結果
|
||||||
|
- `README.md` : 本ファイル
|
||||||
|
|
||||||
|
## ハッシュチェーン方式(改ざん検知)
|
||||||
|
- 各伝票は `payload_json` を正規化JSONとし、その SHA256 を `payload_hash` として保持
|
||||||
|
- `chain_hash = SHA256(prev_chain_hash + ':' + payload_hash)` で直前伝票と連鎖
|
||||||
|
- 先頭伝票の `prev_chain_hash` は 64文字のゼロ(genesis)
|
||||||
|
- `node_id` はDB単位のUUIDで、端末故障時の復旧・識別に使用
|
||||||
|
|
||||||
|
## 検証手順
|
||||||
|
1. `audit_summary.json` の `chain_verification.ok` が `true` であること
|
||||||
|
2. `false` の場合は `chain_verification.errors` を確認
|
||||||
|
3. 任意:スクリプトで `invoices.jsonl` を読み込み、先頭から `chain_hash` を再計算して一致を確認
|
||||||
|
|
||||||
|
## 再現性(PDF生成)
|
||||||
|
- `payload_json` と `pdf_template_version`/`company_info_version` があれば、
|
||||||
|
同一内容のPDFを再生成可能(PDFバイナリの完全一致は保証しない)
|
||||||
|
- PDFは永続保存せず、必要時に再生成して利用・共有・削除する方針
|
||||||
|
|
||||||
|
## 赤伝(相殺)の扱い
|
||||||
|
- `is_offset=true` の伝票は赤伝(相殺伝票)
|
||||||
|
- `offset_target_uuid` で元伝票を指す
|
||||||
|
- 集計・表示ではデフォルト除外(UIの「赤伝を表示」スイッチで切替)
|
||||||
|
|
||||||
|
## 10年保管について
|
||||||
|
- SQLite DB(本エクスポート元)を正とし、10年間保管
|
||||||
|
- バックアップ・復旧はDBファイル単位で行う(Google Drive等)
|
||||||
|
- 必要に応じて本エクスポートパッケージを外部システムへ移行可能
|
||||||
|
|
||||||
|
---
|
||||||
|
生成日時: {generated_at}
|
||||||
|
""".format(generated_at=datetime.now().isoformat())
|
||||||
|
(out_dir / "README.md").write_text(readme, encoding="utf-8")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
db_path = "sales.db"
|
||||||
|
out_dir = Path("audit_export")
|
||||||
|
out_dir.mkdir(exist_ok=True)
|
||||||
|
jsonl_path = out_dir / "invoices.jsonl"
|
||||||
|
summary_path = out_dir / "audit_summary.json"
|
||||||
|
|
||||||
|
print("Exporting invoices as JSONL...")
|
||||||
|
export_invoices_as_jsonl(db_path, str(jsonl_path))
|
||||||
|
print(f" -> {jsonl_path}")
|
||||||
|
|
||||||
|
print("Generating audit summary...")
|
||||||
|
summary = generate_audit_summary(db_path)
|
||||||
|
summary_path.write_text(json.dumps(summary, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||||
|
print(f" -> {summary_path}")
|
||||||
|
|
||||||
|
print("Writing README...")
|
||||||
|
write_audit_readme(out_dir)
|
||||||
|
print(f" -> {out_dir}/README.md")
|
||||||
|
|
||||||
|
print("\nAudit export complete.")
|
||||||
|
print(f"Total invoices: {summary['total_invoices']}")
|
||||||
|
print(f"Offset invoices: {summary['offset_invoices']}")
|
||||||
|
print(f"Chain verification OK: {summary['chain_verification']['ok']}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
38
audit_export/README.md
Normal file
38
audit_export/README.md
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
# 監査・移出パッケージ
|
||||||
|
|
||||||
|
## 概要
|
||||||
|
本パッケージは「販売アシスト1号」のSQLite DBからエクスポートした伝票データと、ハッシュチェーン検証結果を含みます。
|
||||||
|
|
||||||
|
## ファイル
|
||||||
|
- `invoices.jsonl` : 伝票1件ごとに1行のJSON(payload_json含む)
|
||||||
|
- `audit_summary.json` : 全件数・赤伝件数・チェーン検証結果
|
||||||
|
- `README.md` : 本ファイル
|
||||||
|
|
||||||
|
## ハッシュチェーン方式(改ざん検知)
|
||||||
|
- 各伝票は `payload_json` を正規化JSONとし、その SHA256 を `payload_hash` として保持
|
||||||
|
- `chain_hash = SHA256(prev_chain_hash + ':' + payload_hash)` で直前伝票と連鎖
|
||||||
|
- 先頭伝票の `prev_chain_hash` は 64文字のゼロ(genesis)
|
||||||
|
- `node_id` はDB単位のUUIDで、端末故障時の復旧・識別に使用
|
||||||
|
|
||||||
|
## 検証手順
|
||||||
|
1. `audit_summary.json` の `chain_verification.ok` が `true` であること
|
||||||
|
2. `false` の場合は `chain_verification.errors` を確認
|
||||||
|
3. 任意:スクリプトで `invoices.jsonl` を読み込み、先頭から `chain_hash` を再計算して一致を確認
|
||||||
|
|
||||||
|
## 再現性(PDF生成)
|
||||||
|
- `payload_json` と `pdf_template_version`/`company_info_version` があれば、
|
||||||
|
同一内容のPDFを再生成可能(PDFバイナリの完全一致は保証しない)
|
||||||
|
- PDFは永続保存せず、必要時に再生成して利用・共有・削除する方針
|
||||||
|
|
||||||
|
## 赤伝(相殺)の扱い
|
||||||
|
- `is_offset=true` の伝票は赤伝(相殺伝票)
|
||||||
|
- `offset_target_uuid` で元伝票を指す
|
||||||
|
- 集計・表示ではデフォルト除外(UIの「赤伝を表示」スイッチで切替)
|
||||||
|
|
||||||
|
## 10年保管について
|
||||||
|
- SQLite DB(本エクスポート元)を正とし、10年間保管
|
||||||
|
- バックアップ・復旧はDBファイル単位で行う(Google Drive等)
|
||||||
|
- 必要に応じて本エクスポートパッケージを外部システムへ移行可能
|
||||||
|
|
||||||
|
---
|
||||||
|
生成日時: 2026-02-21T07:57:34.454286
|
||||||
12
audit_export/audit_summary.json
Normal file
12
audit_export/audit_summary.json
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"generated_at": "2026-02-21T07:57:34.453267",
|
||||||
|
"db_path": "sales.db",
|
||||||
|
"total_invoices": 8,
|
||||||
|
"offset_invoices": 1,
|
||||||
|
"chain_verification": {
|
||||||
|
"node_id": "662182dc-8024-43ee-bba5-7072917b95fe",
|
||||||
|
"checked": 4,
|
||||||
|
"ok": true,
|
||||||
|
"errors": []
|
||||||
|
}
|
||||||
|
}
|
||||||
8
audit_export/invoices.jsonl
Normal file
8
audit_export/invoices.jsonl
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{"uuid": "113446a2-8ec8-455c-94d9-992d648c55dd", "document_type": "請求書", "customer": {"name": "田中商事株式会社", "address": "東京都千代田区丸の内1-1-1", "phone": "03-1234-5678"}, "amount": 250000.0, "tax": 25000.0, "total_amount": 275000.0, "date": "2026-02-20T21:07:43.446149", "invoice_number": "20260220-2107", "notes": "テスト伝票", "node_id": null, "payload_json": null, "payload_hash": null, "prev_chain_hash": null, "chain_hash": null, "pdf_template_version": null, "company_info_version": null, "is_offset": false, "offset_target_uuid": null, "pdf_generated_at": null, "pdf_sha256": null, "items": {"description": "請求書分", "quantity": 1, "unit_price": 250000, "is_discount": 0}}
|
||||||
|
{"uuid": "eaf13503-667d-4548-b3ff-16d1108b898f", "document_type": "売上伝票", "customer": {"name": "佐藤工業株式会社", "address": "東京都品川区東品川1-1-1", "phone": "03-3456-7890"}, "amount": 250000.0, "tax": 25000.0, "total_amount": 275000.0, "date": "2026-02-20T21:36:46.676752", "invoice_number": "20260220-2136", "notes": "", "node_id": null, "payload_json": null, "payload_hash": null, "prev_chain_hash": null, "chain_hash": null, "pdf_template_version": null, "company_info_version": null, "is_offset": false, "offset_target_uuid": null, "pdf_generated_at": null, "pdf_sha256": null, "items": {"description": "売上伝票分", "quantity": 1, "unit_price": 250000, "is_discount": 0}}
|
||||||
|
{"uuid": "7face002-113c-45ad-a89c-a73082390cf6", "document_type": "請求書", "customer": {"name": "佐藤工業株式会社", "address": "東京都品川区東品川1-1-1", "phone": "03-3456-7890"}, "amount": 250000.0, "tax": 25000.0, "total_amount": 275000.0, "date": "2026-02-20T21:39:44.015079", "invoice_number": "20260220-2139", "notes": "", "node_id": null, "payload_json": null, "payload_hash": null, "prev_chain_hash": null, "chain_hash": null, "pdf_template_version": null, "company_info_version": null, "is_offset": false, "offset_target_uuid": null, "pdf_generated_at": null, "pdf_sha256": null, "items": {"description": "請求書分", "quantity": 1, "unit_price": 250000, "is_discount": 0}}
|
||||||
|
{"uuid": "3772e7ed-19be-4049-ac62-025c725d0912", "document_type": "請求書", "customer": {"name": "佐藤工業株式会社", "address": "東京都品川区東品川1-1-1", "phone": "03-3456-7890"}, "amount": 250000.0, "tax": 25000.0, "total_amount": 275000.0, "date": "2026-02-20T21:41:09.712103", "invoice_number": "20260220-2141", "notes": "", "node_id": null, "payload_json": null, "payload_hash": null, "prev_chain_hash": null, "chain_hash": null, "pdf_template_version": null, "company_info_version": null, "is_offset": false, "offset_target_uuid": null, "pdf_generated_at": null, "pdf_sha256": null, "items": {"description": "請求書分", "quantity": 1, "unit_price": 250000, "is_discount": 0}}
|
||||||
|
{"uuid": "51b9ef4e-377c-4515-a263-1855cb2cb423", "document_type": "請求書", "customer": {"name": "佐藤工業株式会社", "address": "東京都品川区東品川1-1-1", "phone": "03-3456-7890"}, "amount": 12345.0, "tax": 1234.0, "total_amount": 13579.0, "date": "2026-02-20T22:13:38.109736", "invoice_number": "20260220-2213", "notes": "hash test", "node_id": "662182dc-8024-43ee-bba5-7072917b95fe", "payload_json": {"company_info_version": "v1", "customer_master_id": 3, "customer_snapshot": {"address": "東京都品川区東品川1-1-1", "formal_name": "佐藤工業株式会社", "name": "佐藤工業", "phone": "03-3456-7890"}, "date": "2026-02-20T22:13:38", "document_type": "請求書", "invoice_number": "20260220-2213", "items": [{"description": "請求書分", "is_discount": false, "quantity": 1, "unit_price": 12345}], "node_id": "662182dc-8024-43ee-bba5-7072917b95fe", "notes": "hash test", "pdf_template_version": "v1", "schema": "invoice_payload_v1", "tax_calc_rule": "floor(subtotal * tax_rate)", "tax_rate": 0.1, "uuid": "51b9ef4e-377c-4515-a263-1855cb2cb423"}, "payload_hash": "4cedce41aba36c7f3273e39954dbc1d7ce51d9d9ed3a929496933846d897bffc", "prev_chain_hash": "0000000000000000000000000000000000000000000000000000000000000000", "chain_hash": "9262891799ab99d6fedbdd9a4a410176e204d6f42901f4d48263b875e9874a9e", "pdf_template_version": "v1", "company_info_version": "v1", "is_offset": false, "offset_target_uuid": null, "pdf_generated_at": "2026-02-20T22:13:38", "pdf_sha256": null, "items": {"description": "請求書分", "quantity": 1, "unit_price": 12345, "is_discount": 0}}
|
||||||
|
{"uuid": "8f62815c-14bc-4d4b-9cb0-777c0df92870", "document_type": "請求書", "customer": {"name": "佐藤工業株式会社", "address": "東京都品川区東品川1-1-1", "phone": "03-3456-7890"}, "amount": 111.0, "tax": 11.0, "total_amount": 122.0, "date": "2026-02-20T22:26:53.777941", "invoice_number": "20260220-2226", "notes": "ephemeral pdf", "node_id": "662182dc-8024-43ee-bba5-7072917b95fe", "payload_json": {"company_info_version": "v1", "customer_master_id": 3, "customer_snapshot": {"address": "東京都品川区東品川1-1-1", "formal_name": "佐藤工業株式会社", "name": "佐藤工業", "phone": "03-3456-7890"}, "date": "2026-02-20T22:26:53", "document_type": "請求書", "invoice_number": "20260220-2226", "items": [{"description": "請求書分", "is_discount": false, "quantity": 1, "unit_price": 111}], "node_id": "662182dc-8024-43ee-bba5-7072917b95fe", "notes": "ephemeral pdf", "pdf_template_version": "v1", "schema": "invoice_payload_v1", "tax_calc_rule": "floor(subtotal * tax_rate)", "tax_rate": 0.1, "uuid": "8f62815c-14bc-4d4b-9cb0-777c0df92870"}, "payload_hash": "805dbaf93eb5b04db46e3ebead91e79d1bb754272fcbf3fc3ed842260e78e169", "prev_chain_hash": "9262891799ab99d6fedbdd9a4a410176e204d6f42901f4d48263b875e9874a9e", "chain_hash": "7aca5d565b5dff904d8f056f75b82e3b8d9fc1d5dc5f611571d716255eaefad6", "pdf_template_version": "v1", "company_info_version": "v1", "is_offset": false, "offset_target_uuid": null, "pdf_generated_at": null, "pdf_sha256": null, "items": {"description": "請求書分", "quantity": 1, "unit_price": 111, "is_discount": 0}}
|
||||||
|
{"uuid": "61f634cf-cf01-445e-8388-20b5a7fedcd9", "document_type": "請求書", "customer": {"name": "東京都品川区東品川1-1-1", "address": "03-3456-7890", "phone": "250000.0"}, "amount": -250000.0, "tax": -25000.0, "total_amount": -275000.0, "date": "2026-02-20T22:36:01.576234", "invoice_number": "20260220-2236", "notes": "", "node_id": "662182dc-8024-43ee-bba5-7072917b95fe", "payload_json": {"company_info_version": "v1", "customer_master_id": "佐藤工業株式会社", "customer_snapshot": {"address": "03-3456-7890", "formal_name": "東京都品川区東品川1-1-1", "name": "東京都品川区東品川1-1-1", "phone": 250000.0}, "date": "2026-02-20T22:36:01", "document_type": "請求書", "invoice_number": "20260220-2236", "is_offset": true, "items": [{"description": "相殺(赤伝) 対象:20260220-2141", "is_discount": true, "quantity": 1, "unit_price": 250000}], "node_id": "662182dc-8024-43ee-bba5-7072917b95fe", "notes": "", "offset_target_uuid": "3772e7ed-19be-4049-ac62-025c725d0912", "pdf_template_version": "v1", "schema": "invoice_payload_v1", "tax_calc_rule": "floor(subtotal * tax_rate)", "tax_rate": 0.1, "uuid": "61f634cf-cf01-445e-8388-20b5a7fedcd9"}, "payload_hash": "b6c6fa69eca77964dd3957484f88382fc21c57d971a2bf9d7f470493506fb399", "prev_chain_hash": "7aca5d565b5dff904d8f056f75b82e3b8d9fc1d5dc5f611571d716255eaefad6", "chain_hash": "be7d57eeca53eae26e234536678822e1b6e09396aa8b1f41a3aee8d0d1b0b291", "pdf_template_version": "v1", "company_info_version": "v1", "is_offset": true, "offset_target_uuid": "3772e7ed-19be-4049-ac62-025c725d0912", "pdf_generated_at": null, "pdf_sha256": null, "items": {"description": "相殺(赤伝) 対象:20260220-2141", "quantity": 1, "unit_price": 250000, "is_discount": 1}}
|
||||||
|
{"uuid": "7c8ba2a0-f478-4ffc-ac7b-bac1984b8286", "document_type": "見積書", "customer": {"name": "佐藤工業株式会社", "address": "東京都品川区東品川1-1-1", "phone": "03-3456-7890"}, "amount": 1.0, "tax": 0.0, "total_amount": 1.0, "date": "2026-02-20T22:37:37.543579", "invoice_number": "20260220-2237", "notes": "", "node_id": "662182dc-8024-43ee-bba5-7072917b95fe", "payload_json": {"company_info_version": "v1", "customer_master_id": 3, "customer_snapshot": {"address": "東京都品川区東品川1-1-1", "formal_name": "佐藤工業株式会社", "name": "佐藤工業", "phone": "03-3456-7890"}, "date": "2026-02-20T22:37:37", "document_type": "見積書", "invoice_number": "20260220-2237", "is_offset": false, "items": [{"description": "見積書分", "is_discount": false, "quantity": 1, "unit_price": 1}], "node_id": "662182dc-8024-43ee-bba5-7072917b95fe", "notes": "", "offset_target_uuid": null, "pdf_template_version": "v1", "schema": "invoice_payload_v1", "tax_calc_rule": "floor(subtotal * tax_rate)", "tax_rate": 0.1, "uuid": "7c8ba2a0-f478-4ffc-ac7b-bac1984b8286"}, "payload_hash": "658cd041f84a372c8d2f5817f5172b61f8a138655197644ac527a296a43ef567", "prev_chain_hash": "be7d57eeca53eae26e234536678822e1b6e09396aa8b1f41a3aee8d0d1b0b291", "chain_hash": "6963cfb33ea008b1f1f1ee8dc6ce930fa634cf51bb7be94b793e5ef6752cccbd", "pdf_template_version": "v1", "company_info_version": "v1", "is_offset": false, "offset_target_uuid": null, "pdf_generated_at": null, "pdf_sha256": null, "items": {"description": "見積書分", "quantity": 1, "unit_price": 1, "is_discount": 0}}
|
||||||
|
|
@ -71,6 +71,7 @@ class InvoiceService:
|
||||||
"company_info_version": "v1",
|
"company_info_version": "v1",
|
||||||
"is_offset": bool(getattr(invoice, "is_offset", False)),
|
"is_offset": bool(getattr(invoice, "is_offset", False)),
|
||||||
"offset_target_uuid": getattr(invoice, "offset_target_uuid", None),
|
"offset_target_uuid": getattr(invoice, "offset_target_uuid", None),
|
||||||
|
"submitted_to_tax_authority": getattr(invoice, "submitted_to_tax_authority", False),
|
||||||
}
|
}
|
||||||
|
|
||||||
payload_json = json.dumps(payload_obj, ensure_ascii=False, separators=(",", ":"), sort_keys=True)
|
payload_json = json.dumps(payload_obj, ensure_ascii=False, separators=(",", ":"), sort_keys=True)
|
||||||
|
|
@ -84,6 +85,7 @@ class InvoiceService:
|
||||||
invoice.chain_hash = chain_hash
|
invoice.chain_hash = chain_hash
|
||||||
invoice.pdf_template_version = "v1"
|
invoice.pdf_template_version = "v1"
|
||||||
invoice.company_info_version = "v1"
|
invoice.company_info_version = "v1"
|
||||||
|
invoice.submitted_to_tax_authority = False
|
||||||
|
|
||||||
def create_invoice(self,
|
def create_invoice(self,
|
||||||
customer: Customer,
|
customer: Customer,
|
||||||
|
|
@ -198,6 +200,21 @@ class InvoiceService:
|
||||||
logging.error(f"赤伝作成エラー: {e}")
|
logging.error(f"赤伝作成エラー: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def submit_to_tax_authority(self, invoice_uuid: str) -> bool:
|
||||||
|
"""税務署提出済みフラグを設定し、ロックする"""
|
||||||
|
try:
|
||||||
|
with sqlite3.connect(self.invoice_repo.db_path) as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute(
|
||||||
|
"UPDATE invoices SET submitted_to_tax_authority = 1 WHERE uuid = ?",
|
||||||
|
(invoice_uuid,)
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"税務署提出エラー: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
def get_recent_invoices(self, limit: int = 50) -> List[Invoice]:
|
def get_recent_invoices(self, limit: int = 50) -> List[Invoice]:
|
||||||
"""最近の伝票を取得"""
|
"""最近の伝票を取得"""
|
||||||
return self.invoice_repo.get_all_invoices(limit)
|
return self.invoice_repo.get_all_invoices(limit)
|
||||||
|
|
@ -250,11 +267,29 @@ class InvoiceService:
|
||||||
class CustomerService:
|
class CustomerService:
|
||||||
"""顧客ビジネスロジック"""
|
"""顧客ビジネスロジック"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, db_path: str = "sales.db"):
|
||||||
self.customer_repo = CustomerRepository()
|
self.db_path = db_path
|
||||||
|
self.customer_repo = CustomerRepository(db_path)
|
||||||
self._customer_cache: List[Customer] = []
|
self._customer_cache: List[Customer] = []
|
||||||
self._load_customers()
|
self._load_customers()
|
||||||
|
|
||||||
|
def create_customer(self, name: str, formal_name: str, address: str = "", phone: str = "") -> Customer:
|
||||||
|
"""顧客を新規作成"""
|
||||||
|
customer = Customer(
|
||||||
|
id=0, # 新規はID=0
|
||||||
|
name=name,
|
||||||
|
formal_name=formal_name,
|
||||||
|
address=address,
|
||||||
|
phone=phone
|
||||||
|
)
|
||||||
|
success = self.customer_repo.save_customer(customer)
|
||||||
|
if success:
|
||||||
|
self._customer_cache.append(customer)
|
||||||
|
logging.info(f"新規顧客登録: {formal_name}")
|
||||||
|
else:
|
||||||
|
logging.error(f"新規顧客登録失敗: {formal_name}")
|
||||||
|
return customer if success else None
|
||||||
|
|
||||||
def _load_customers(self):
|
def _load_customers(self):
|
||||||
"""顧客データを読み込み"""
|
"""顧客データを読み込み"""
|
||||||
self._customer_cache = self.customer_repo.get_all_customers()
|
self._customer_cache = self.customer_repo.get_all_customers()
|
||||||
|
|
|
||||||
|
|
@ -124,6 +124,7 @@ class InvoiceRepository:
|
||||||
ensure_column("invoices", "offset_target_uuid", "offset_target_uuid TEXT")
|
ensure_column("invoices", "offset_target_uuid", "offset_target_uuid TEXT")
|
||||||
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")
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
|
|
@ -271,8 +272,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)
|
pdf_generated_at, pdf_sha256, submitted_to_tax_authority)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
''', (
|
''', (
|
||||||
invoice.uuid,
|
invoice.uuid,
|
||||||
invoice.document_type.value,
|
invoice.document_type.value,
|
||||||
|
|
@ -298,6 +299,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
|
||||||
))
|
))
|
||||||
|
|
||||||
invoice_id = cursor.lastrowid
|
invoice_id = cursor.lastrowid
|
||||||
|
|
@ -428,6 +430,7 @@ class InvoiceRepository:
|
||||||
inv.offset_target_uuid = row[22]
|
inv.offset_target_uuid = row[22]
|
||||||
inv.pdf_generated_at = row[23]
|
inv.pdf_generated_at = row[23]
|
||||||
inv.pdf_sha256 = row[24]
|
inv.pdf_sha256 = row[24]
|
||||||
|
inv.submitted_to_tax_authority = bool(row[25])
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
@ -515,28 +518,58 @@ class CustomerRepository:
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
def save_customer(self, customer: Customer) -> bool:
|
def save_customer(self, customer: Customer) -> bool:
|
||||||
"""顧客を保存"""
|
"""顧客を保存(新規登録・更新共通)"""
|
||||||
|
# IDが0の場合は新規登録、それ以外は更新
|
||||||
|
if customer.id == 0:
|
||||||
|
return self._insert_customer(customer)
|
||||||
|
else:
|
||||||
|
return self.update_customer(customer)
|
||||||
|
|
||||||
|
def _insert_customer(self, customer: Customer) -> bool:
|
||||||
|
"""顧客を新規登録"""
|
||||||
try:
|
try:
|
||||||
with sqlite3.connect(self.db_path) as conn:
|
with sqlite3.connect(self.db_path) as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
|
||||||
cursor.execute('''
|
cursor.execute("""
|
||||||
INSERT OR REPLACE INTO customers
|
INSERT INTO customers
|
||||||
(id, name, formal_name, address, phone)
|
(name, formal_name, address, phone, email)
|
||||||
VALUES (?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?)
|
||||||
''', (
|
""", (
|
||||||
customer.id,
|
|
||||||
customer.name,
|
customer.name,
|
||||||
customer.formal_name,
|
customer.formal_name,
|
||||||
customer.address,
|
customer.address,
|
||||||
customer.phone
|
customer.phone,
|
||||||
|
customer.email
|
||||||
))
|
))
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"顧客保存エラー: {e}")
|
logging.error(f"顧客新規登録エラー: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def update_customer(self, customer: Customer) -> bool:
|
||||||
|
"""顧客情報を更新"""
|
||||||
|
try:
|
||||||
|
with sqlite3.connect(self.db_path) as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("""
|
||||||
|
UPDATE customers
|
||||||
|
SET name = ?, formal_name = ?, address = ?, phone = ?
|
||||||
|
WHERE id = ?
|
||||||
|
""", (
|
||||||
|
customer.name,
|
||||||
|
customer.formal_name,
|
||||||
|
customer.address,
|
||||||
|
customer.phone,
|
||||||
|
customer.id
|
||||||
|
))
|
||||||
|
conn.commit()
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"顧客更新エラー: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_all_customers(self) -> List[Customer]:
|
def get_all_customers(self) -> List[Customer]:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue