""" アプリケーションサービス層 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伝票作成失敗")