From fb65e5d38144dc1e7660a42f6f7384b7a154c8a5 Mon Sep 17 00:00:00 2001 From: joe Date: Sat, 10 Jan 2026 18:17:17 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20Odoo=20API=E3=82=92=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=E3=81=97=E3=81=9F=E8=AB=8B=E6=B1=82=E6=9B=B8=E7=99=BA=E8=A1=8C?= =?UTF-8?q?=E3=82=B7=E3=82=B9=E3=83=86=E3=83=A0=E3=82=92=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: aider (ollama/qwen3-coder:8b) --- odoo_invoice_generator.py | 313 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 313 insertions(+) create mode 100644 odoo_invoice_generator.py diff --git a/odoo_invoice_generator.py b/odoo_invoice_generator.py new file mode 100644 index 0000000..63a5223 --- /dev/null +++ b/odoo_invoice_generator.py @@ -0,0 +1,313 @@ +""" +Odoo請求書発行システム(REST APIを使用) +======================================== +""" + +import requests +from datetime import datetime +import json +import logging + + +class OdooAPI: + """ + Odoo APIクライアント + """ + + def __init__(self, base_url: str, db: str, username: str, password: str): + self.base_url = f"{base_url}/api/v13" + self.db = db + self.session = requests.Session() + self.login() + + def login(self) -> bool: + """ + Odoo APIに認証する + + 戻り値: + bool: 認証成功の場合はTrue、失敗の場合はFalse + """ + url = f"{self.base_url}/login/db_{self.db}" + data = { + "jsonrpc": "2.0", + "method": "call", + "params": { + "service": "object", + "method": "service_login", + "args": [self.db, username, password] + } + } + + response = self.session.post(url, json=data) + if response.status_code == 200: + result = response.json() + self.session_id = result["result"]["session_id"] + return True + else: + logging.error(f"ログイン失敗: {response.text}") + return False + + def logout(self) -> bool: + """ + Odoo APIからログアウトする + + 戻り値: + bool: ログアウト成功の場合はTrue、失敗の場合はFalse + """ + url = f"{self.base_url}/login/logout" + data = { + "jsonrpc": "2.0", + "method": "call", + "params": { + "service": "object", + "method": "service_logout", + "args": [self.session_id] + } + } + + response = self.session.post(url, json=data) + if response.status_code == 200: + return True + else: + logging.error(f"ログアウト失敗: {response.text}") + return False + + def get_partner(self, partner_id: int) -> dict | None: + """ + 顧客情報を取得する + + 引数: + partner_id (int): 取得する顧客のID + + 戻り値: + dict | None: 顧客データが見つかった場合、それ以外の場合はNone + """ + url = f"{self.base_url}/res.partner/{partner_id}" + headers = { + "Authorization": f"Session {self.session_id}", + "Content-Type": "application/json" + } + + response = self.session.get(url, headers=headers) + if response.status_code == 200: + return response.json()["result"] + else: + logging.error(f"顧客情報取得失敗: {response.text}") + return None + + def get_product(self, product_id: int) -> dict | None: + """ + 商品情報を取得する + + 引数: + product_id (int): 取得する商品のID + + 戻り値: + dict | None: 商品データが見つかった場合、それ以外の場合はNone + """ + url = f"{self.base_url}/product.product/{product_id}" + headers = { + "Authorization": f"Session {self.session_id}", + "Content-Type": "application/json" + } + + response = self.session.get(url, headers=headers) + if response.status_code == 200: + return response.json()["result"] + else: + logging.error(f"商品情報取得失敗: {response.text}") + return None + + def create_invoice(self, invoice_data: dict) -> dict | None: + """ + 新しい請求書を作成する + + 引数: + invoice_data (dict): 請求書データ + + 戻り値: + dict | None: 作成成功した場合の請求書データ、失敗の場合はNone + """ + url = f"{self.base_url}/account.move" + headers = { + "Authorization": f"Session {self.session_id}", + "Content-Type": "application/json" + } + + response = self.session.post(url, headers=headers, json=invoice_data) + if response.status_code == 200: + return response.json()["result"] + else: + logging.error(f"請求書作成失敗: {response.text}") + return None + + def get_invoices(self) -> list[dict]: + """ + 全ての請求書を取得する + + 戻り値: + list[dict]: 請求書リスト + """ + url = f"{self.base_url}/account.move" + headers = { + "Authorization": f"Session {self.session_id}", + "Content-Type": "application/json" + } + + response = self.session.get(url, headers=headers) + if response.status_code == 200: + return response.json()["result"] + else: + logging.error(f"請求書取得失敗: {response.text}") + return [] + + +class InvoiceGenerator: + """ + Odoo請求書生成器 + """ + + def __init__(self, odoo_api: OdooAPI): + self.odoo_api = odoo_api + + def generate_invoice(self, partner_id: int, product_id: int, quantity: int) -> dict | None: + """ + 指定された顧客と商品に対して新しい請求書を生成する + + 引数: + partner_id (int): 顧客ID + product_id (int): 商品ID + quantity (int): 売上数量 + + 戻り値: + dict | None: 作成成功した場合の請求書データ、失敗の場合はNone + """ + + # 顧客情報を取得 + partner = self.odoo_api.get_partner(partner_id) + if not partner: + logging.error(f"顧客 {partner_id} が見つからない") + return None + + # 商品情報を取得 + product = self.odoo_api.get_product(product_id) + if not product: + logging.error(f"商品 {product_id} が見つからない") + return None + + # 合計金額を計算 + price_unit = product["lst_price"] + subtotal = quantity * price_unit + tax_rate = 0.08 # 8%の消費税(例、必要に応じて変更可能) + tax_amount = subtotal * tax_rate / (1 + tax_rate) + total = subtotal + tax_amount + + # 請求書データを作成 + invoice_data = { + "journal_id": 1, # デフォルトの勘定科目ID(必要に応じて変更可能) + "partner_id": partner["id"], + "date_invoice": datetime.now().strftime("%Y-%m-%d"), + "move_type": "out_invoice", + "state": "draft", # 初期状態 + "name": f"{partner['name']} への請求書", + "reference": f"INV-{datetime.now().strftime('%Y%m%d%H%M%S')}", + "user_id": 1, # デフォルトのユーザーID(必要に応じて変更可能) + "company_id": 1, # デフォルトの会社ID(必要に応じて変更可能) + + "invoice_line_ids": [ + { + "product_id": product["id"], + "name": f"{product['name']} x{quantity}", + "sequence": 10, + "type": "line", + "quantity": quantity, + "price_unit": price_unit, + "account_id": product.get("property_account_exp", {}).get("account_id"), + "analytic_index_ids": [], + "discount": 0.00 + } + ], + + # 追加の行、割引などがあればここに追加可能 + } + + return self.odoo_api.create_invoice(invoice_data) + + def generate_invoices(self, partner_ids: list[int], product_ids: list[int]) -> dict: + """ + 複数の顧客と商品に対して請求書を生成する + + 引数: + partner_ids (list[int]): 顧客IDリスト + product_ids (list[int]): 商品IDリスト + + 戻り値: + dict: 請求書データとエラーメッセージを含む辞書 + """ + + # 結果辞書を初期化 + result = {"invoices": [], "errors": []} + + for partner_id in partner_ids: + for product_id in product_ids: + try: + invoice = self.generate_invoice(partner_id, product_id, 1) + if invoice: + result["invoices"].append(invoice) + except Exception as e: + result["errors"].append(f"請求書生成失敗: {str(e)}") + + return result + + +# 実行例 +if __name__ == "__main__": + # Odoo API接続パラメータを設定 + base_url = "http://localhost:8069" + db_name = "mydatabase" + username = "admin" + password = "password123" + + odoo_api = OdooAPI(base_url, db_name, username, password) + + if not odoo_api.login(): + print("Odoo APIへの認証失敗") + exit(1) + + # インスタンスを作成 + invoice_generator = InvoiceGenerator(odoo_api) + + # 例1: 単一の請求書を生成 + partner_id = 2 + product_id = 3 + quantity = 5 + + invoice = invoice_generator.generate_invoice(partner_id, product_id, quantity) + if invoice: + print("請求書作成成功:") + print(json.dumps(invoice, indent=4)) + else: + print(f"顧客 {partner_id} と商品 {product_id} に対する請求書作成失敗") + + # 例2: 複数の請求書を生成 + partner_ids = [1, 2, 3] + product_ids = [101, 102, 103] + + result = invoice_generator.generate_invoices(partner_ids, product_ids) + print("\n請求書生成結果:") + print(f"作成された請求書: {len(result['invoices'])}") + print(f"エラー: {len(result['errors'])}") + + # 請求書を表示 + for i, invoice in enumerate(result["invoices"]): + print(f"\n請求書 {i+1}:") + print(json.dumps(invoice, indent=4)) + + # エラーを表示 + if result["errors"]: + print("\nエラーログ:") + for error in result["errors"]: + print(error) + + # Odoo APIからログアウト + odoo_api.logout()