h-1.flet.3/services/app_service.py
2026-02-20 23:24:01 +09:00

353 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
アプリケーションサービス層
UIとビジネスロジックの橋渡し
"""
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from typing import Optional, List, Dict, Any
from datetime import datetime
from models.invoice_models import Invoice, Customer, InvoiceItem, DocumentType
from services.repositories import InvoiceRepository, CustomerRepository
from services.pdf_generator import PdfGenerator
import logging
import json
import hashlib
import os
class InvoiceService:
"""伝票ビジネスロジック"""
def __init__(self):
self.invoice_repo = InvoiceRepository()
self.customer_repo = CustomerRepository()
self.pdf_generator = PdfGenerator()
self.company_info = self._load_company_info()
def _load_company_info(self) -> Dict[str, str]:
"""自社情報を読み込み"""
# TODO: 設定ファイルから読み込み
return {
'id': 'SELF001',
'name': '自社',
'address': '',
'phone': '',
'registration_number': ''
}
def _apply_audit_fields(self, invoice: Invoice, customer: Customer):
node_id = self.invoice_repo.get_node_id()
prev_chain_hash = self.invoice_repo.get_last_chain_hash(node_id)
payload_obj = {
"schema": "invoice_payload_v1",
"node_id": node_id,
"uuid": invoice.uuid,
"document_type": invoice.document_type.value,
"invoice_number": invoice.invoice_number,
"date": invoice.date.replace(microsecond=0).isoformat(),
"tax_rate": 0.1,
"tax_calc_rule": "floor(subtotal * tax_rate)",
"customer_master_id": getattr(customer, "id", None),
"customer_snapshot": {
"name": customer.name,
"formal_name": customer.formal_name,
"address": customer.address,
"phone": customer.phone,
},
"items": [
{
"description": it.description,
"quantity": int(it.quantity),
"unit_price": int(it.unit_price),
"is_discount": bool(it.is_discount),
}
for it in invoice.items
],
"notes": invoice.notes or "",
"pdf_template_version": "v1",
"company_info_version": "v1",
"is_offset": bool(getattr(invoice, "is_offset", False)),
"offset_target_uuid": getattr(invoice, "offset_target_uuid", None),
}
payload_json = json.dumps(payload_obj, ensure_ascii=False, separators=(",", ":"), sort_keys=True)
payload_hash = hashlib.sha256(payload_json.encode("utf-8")).hexdigest()
chain_hash = hashlib.sha256(f"{prev_chain_hash}:{payload_hash}".encode("utf-8")).hexdigest()
invoice.node_id = node_id
invoice.payload_json = payload_json
invoice.payload_hash = payload_hash
invoice.prev_chain_hash = prev_chain_hash
invoice.chain_hash = chain_hash
invoice.pdf_template_version = "v1"
invoice.company_info_version = "v1"
def create_invoice(self,
customer: Customer,
document_type: DocumentType,
amount: int,
notes: str = "") -> Optional[Invoice]:
"""新規伝票作成
Args:
customer: 顧客情報
document_type: 帳票種類
amount: 金額(税抜)
notes: 備考
Returns:
作成されたInvoice、失敗時はNone
"""
try:
# 初期明細作成
items = [InvoiceItem(
description=f"{document_type.value}",
quantity=1,
unit_price=amount
)]
# 伝票作成
invoice = Invoice(
customer=customer,
date=datetime.now(),
items=items,
document_type=document_type,
notes=notes
)
# --- 長期保管向け: canonical payload + hash chain ---
self._apply_audit_fields(invoice, customer)
# DB保存PDFは仮生成物なので保存の成否と切り離す
invoice.file_path = None
if self.invoice_repo.save_invoice(invoice):
logging.info(f"伝票作成成功: {invoice.invoice_number}")
else:
logging.error("伝票DB保存失敗")
return None
# PDF生成任意・仮生成物
try:
pdf_path = self.pdf_generator.generate_invoice_pdf(invoice, self.company_info)
if pdf_path:
invoice.file_path = pdf_path
invoice.pdf_generated_at = datetime.now().replace(microsecond=0).isoformat()
else:
logging.warning("PDF生成失敗DB保存は完了")
except Exception as e:
logging.warning(f"PDF生成例外DB保存は完了: {e}")
return invoice
except Exception as e:
logging.error(f"伝票作成エラー: {e}")
return None
def create_offset_invoice(self, target_uuid: str, notes: str = "") -> Optional[Invoice]:
"""本伝を相殺する赤伝(マイナス伝票)を作成"""
try:
target = self.invoice_repo.get_invoice_by_uuid(target_uuid)
if not target:
logging.error(f"赤伝作成失敗: 対象uuidが見つかりません: {target_uuid}")
return None
# 1行で相殺値引き行としてマイナス
items = [
InvoiceItem(
description=f"相殺(赤伝) 対象:{target.invoice_number}",
quantity=1,
unit_price=int(target.subtotal),
is_discount=True,
)
]
invoice = Invoice(
customer=target.customer,
date=datetime.now(),
items=items,
document_type=target.document_type,
notes=notes,
)
invoice.is_offset = True
invoice.offset_target_uuid = target.uuid
self._apply_audit_fields(invoice, invoice.customer)
# DB保存PDFは仮生成物
invoice.file_path = None
if not self.invoice_repo.save_invoice(invoice):
logging.error("赤伝DB保存失敗")
return None
# PDF生成任意
try:
pdf_path = self.pdf_generator.generate_invoice_pdf(invoice, self.company_info)
if pdf_path:
invoice.file_path = pdf_path
invoice.pdf_generated_at = datetime.now().replace(microsecond=0).isoformat()
except Exception as e:
logging.warning(f"赤伝PDF生成例外DB保存は完了: {e}")
logging.info(f"赤伝作成成功: {invoice.invoice_number} (target={target.invoice_number})")
return invoice
except Exception as e:
logging.error(f"赤伝作成エラー: {e}")
return None
def get_recent_invoices(self, limit: int = 50) -> List[Invoice]:
"""最近の伝票を取得"""
return self.invoice_repo.get_all_invoices(limit)
def regenerate_pdf(self, invoice_uuid: str) -> Optional[str]:
"""DBを正としてPDFを再生成生成物は仮"""
invoice = self.invoice_repo.get_invoice_by_uuid(invoice_uuid)
if not invoice:
logging.error(f"PDF再生成失敗: uuidが見つかりません: {invoice_uuid}")
return None
# 会社情報は現状v1固定。将来はcompany_info_versionで分岐。
pdf_path = self.pdf_generator.generate_invoice_pdf(invoice, self.company_info)
if not pdf_path:
return None
return pdf_path
def delete_pdf_file(self, pdf_path: str) -> bool:
"""仮生成PDFを削除"""
try:
if not pdf_path:
return True
if os.path.exists(pdf_path):
os.remove(pdf_path)
return True
except Exception as e:
logging.warning(f"PDF削除失敗: {e}")
return False
def delete_invoice(self, invoice_id: int) -> bool:
"""伝票を削除"""
return self.invoice_repo.delete_invoice(invoice_id)
def get_statistics(self) -> Dict[str, Any]:
"""統計情報取得"""
return self.invoice_repo.get_statistics()
def update_company_info(self, info: Dict[str, str]) -> bool:
"""自社情報を更新"""
try:
self.company_info.update(info)
# TODO: 設定ファイルに保存
return True
except Exception as e:
logging.error(f"自社情報更新エラー: {e}")
return False
class CustomerService:
"""顧客ビジネスロジック"""
def __init__(self):
self.customer_repo = CustomerRepository()
self._customer_cache: List[Customer] = []
self._load_customers()
def _load_customers(self):
"""顧客データを読み込み"""
self._customer_cache = self.customer_repo.get_all_customers()
# 初期データがない場合はサンプル作成
if not self._customer_cache:
sample_customers = [
Customer(1, "田中商事", "田中商事株式会社", "東京都千代田区丸の内1-1-1", "03-1234-5678"),
Customer(2, "鈴木商店", "鈴木商店", "東京都港区芝1-1-1", "03-2345-6789"),
Customer(3, "佐藤工業", "佐藤工業株式会社", "東京都品川区東品川1-1-1", "03-3456-7890"),
]
for customer in sample_customers:
self.add_customer(customer)
self._customer_cache = sample_customers
def get_all_customers(self) -> List[Customer]:
"""全顧客を取得"""
return self._customer_cache.copy()
def search_customers(self, query: str) -> List[Customer]:
"""顧客を検索"""
query = query.lower()
return [
c for c in self._customer_cache
if query in c.name.lower() or
query in c.formal_name.lower() or
query in c.address.lower()
]
def add_customer(self, customer: Customer) -> bool:
"""顧客を追加"""
success = self.customer_repo.save_customer(customer)
if success:
self._customer_cache.append(customer)
return success
def delete_customer(self, customer_id: int) -> bool:
"""顧客を削除"""
# TODO: 使用されている顧客は削除不可
self._customer_cache = [c for c in self._customer_cache if c.id != customer_id]
return True
# サービスファクトリ
class AppService:
"""アプリケーションサービス統合"""
def __init__(self):
self.invoice = InvoiceService()
self.customer = CustomerService()
def get_dashboard_data(self) -> Dict[str, Any]:
"""ダッシュボード表示用データ"""
stats = self.invoice.get_statistics()
recent_invoices = self.invoice.get_recent_invoices(5)
return {
'total_invoices': stats['total_count'],
'total_amount': stats['total_amount'],
'monthly_amount': stats['monthly_amount'],
'recent_invoices': recent_invoices,
'customer_count': len(self.customer.get_all_customers())
}
# 使用例
if __name__ == "__main__":
# サービス初期化
app_service = AppService()
# ダッシュボードデータ取得
dashboard = app_service.get_dashboard_data()
print(f"ダッシュボード: {dashboard}")
# 顧客取得
customers = app_service.customer.get_all_customers()
print(f"\n顧客数: {len(customers)}")
if customers:
# 伝票作成テスト
from models.invoice_models import DocumentType
invoice = app_service.invoice.create_invoice(
customer=customers[0],
document_type=DocumentType.INVOICE,
amount=250000,
notes="テスト伝票"
)
if invoice:
print(f"\n伝票作成成功: {invoice.invoice_number}")
print(f"合計金額: ¥{invoice.total_amount:,}")
print(f"PDF: {invoice.file_path}")
else:
print("\n伝票作成失敗")