inv/claude/odoo_sync.py
2026-01-31 22:18:30 +09:00

334 lines
11 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.

# api/odoo_sync.py
"""
Odoo連携モジュール
REST API が受け取ったドキュメントを Odoo に同期
"""
import requests
import logging
from typing import Dict, List, Optional
from datetime import datetime
import json
logger = logging.getLogger(__name__)
class OdooClient:
"""Odoo XML-RPC クライアント"""
def __init__(self, odoo_url: str, db: str, username: str, password: str):
self.odoo_url = odoo_url
self.db = db
self.username = username
self.password = password
self.uid = None
self.authenticate()
def authenticate(self):
"""Odoo 認証"""
try:
import xmlrpc.client
common = xmlrpc.client.ServerProxy(f'{self.odoo_url}/xmlrpc/2/common')
self.uid = common.authenticate(self.db, self.username, self.password, {})
logger.info(f"Odoo authenticated: uid={self.uid}")
except Exception as e:
logger.error(f"Odoo authentication failed: {str(e)}")
raise
def create_customer(self, name: str, address: str = "", phone: str = "", email: str = "") -> int:
"""顧客を Odoo に作成"""
try:
import xmlrpc.client
models = xmlrpc.client.ServerProxy(f'{self.odoo_url}/xmlrpc/2/object')
partner_data = {
'name': name,
'street': address,
'phone': phone,
'email': email,
'customer_rank': 1,
}
partner_id = models.execute_kw(
self.db, self.uid, self.password,
'res.partner', 'create', [partner_data]
)
logger.info(f"Created Odoo customer: {partner_id}")
return partner_id
except Exception as e:
logger.error(f"Error creating customer: {str(e)}")
return 0
def create_quotation(self, customer_id: int, items: List[Dict],
payment_due_date: str, notes: str = "") -> int:
"""見積を Odoo に作成"""
try:
import xmlrpc.client
models = xmlrpc.client.ServerProxy(f'{self.odoo_url}/xmlrpc/2/object')
# 見積ラインの準備
order_lines = []
for item in items:
# 商品をOdooから検索簡略版
product_search = models.execute_kw(
self.db, self.uid, self.password,
'product.product', 'search',
[[('name', '=', item['product_name'])]]
)
product_id = product_search[0] if product_search else 1
line_data = (0, 0, {
'product_id': product_id,
'product_qty': item['quantity'],
'price_unit': item['unit_price'],
})
order_lines.append(line_data)
# 見積作成
quotation_data = {
'partner_id': customer_id,
'order_line': order_lines,
'date_order': datetime.now().isoformat(),
'payment_term_id': self._get_payment_term_id(payment_due_date),
'note': notes,
}
quotation_id = models.execute_kw(
self.db, self.uid, self.password,
'sale.order', 'create', [quotation_data]
)
logger.info(f"Created Odoo quotation: {quotation_id}")
return quotation_id
except Exception as e:
logger.error(f"Error creating quotation: {str(e)}")
return 0
def create_invoice(self, customer_id: int, items: List[Dict],
payment_due_date: str, notes: str = "") -> int:
"""請求書を Odoo に作成"""
try:
import xmlrpc.client
models = xmlrpc.client.ServerProxy(f'{self.odoo_url}/xmlrpc/2/object')
invoice_lines = []
for item in items:
product_search = models.execute_kw(
self.db, self.uid, self.password,
'product.product', 'search',
[[('name', '=', item['product_name'])]]
)
product_id = product_search[0] if product_search else 1
line_data = (0, 0, {
'product_id': product_id,
'quantity': item['quantity'],
'price_unit': item['unit_price'],
})
invoice_lines.append(line_data)
invoice_data = {
'partner_id': customer_id,
'invoice_line_ids': invoice_lines,
'invoice_date': datetime.now().date().isoformat(),
'invoice_date_due': payment_due_date,
'note': notes,
}
invoice_id = models.execute_kw(
self.db, self.uid, self.password,
'account.move', 'create', [invoice_data]
)
logger.info(f"Created Odoo invoice: {invoice_id}")
return invoice_id
except Exception as e:
logger.error(f"Error creating invoice: {str(e)}")
return 0
def record_payment(self, invoice_id: int, amount: float, payment_date: str) -> int:
"""支払いを記録"""
try:
import xmlrpc.client
models = xmlrpc.client.ServerProxy(f'{self.odoo_url}/xmlrpc/2/object')
payment_data = {
'move_id': invoice_id,
'amount': amount,
'payment_date': payment_date,
}
payment_id = models.execute_kw(
self.db, self.uid, self.password,
'account.payment', 'create', [payment_data]
)
logger.info(f"Recorded payment: {payment_id}")
return payment_id
except Exception as e:
logger.error(f"Error recording payment: {str(e)}")
return 0
def get_customer_by_email(self, email: str) -> Optional[int]:
"""メールアドレスで顧客を検索"""
try:
import xmlrpc.client
models = xmlrpc.client.ServerProxy(f'{self.odoo_url}/xmlrpc/2/object')
result = models.execute_kw(
self.db, self.uid, self.password,
'res.partner', 'search',
[[('email', '=', email)]]
)
return result[0] if result else None
except Exception as e:
logger.error(f"Error searching customer: {str(e)}")
return None
def _get_payment_term_id(self, due_date: str) -> int:
"""支払い条件を Odoo から取得"""
try:
import xmlrpc.client
models = xmlrpc.client.ServerProxy(f'{self.odoo_url}/xmlrpc/2/object')
# 簡略版「30日」の支払い条件 ID を取得
result = models.execute_kw(
self.db, self.uid, self.password,
'account.payment.term', 'search',
[[('name', 'like', '30')]]
)
return result[0] if result else 1
except Exception as e:
logger.warning(f"Could not get payment term: {str(e)}")
return 1
class SyncService:
"""REST API と Odoo の同期サービス"""
def __init__(self, odoo_client: OdooClient):
self.odoo = odoo_client
def sync_document(self, db_session, document_entity) -> Dict:
"""ドキュメントを Odoo に同期"""
# 顧客情報を取得
customer = db_session.query(Customer).filter(
Customer.id == document_entity.customer_id
).first()
if not customer:
return {"status": "error", "message": "Customer not found"}
# Odoo 顧客 ID を確認・作成
odoo_customer_id = customer.odoo_customer_id
if not odoo_customer_id:
odoo_customer_id = self.odoo.create_customer(
name=customer.name,
address=customer.address or "",
phone=customer.phone or "",
email=customer.email or ""
)
customer.odoo_customer_id = odoo_customer_id
db_session.commit()
if not odoo_customer_id:
return {"status": "error", "message": "Could not create/find Odoo customer"}
# ドキュメントタイプ別処理
items = json.loads(document_entity.items)
try:
if document_entity.doc_type == "quotation":
odoo_id = self.odoo.create_quotation(
customer_id=odoo_customer_id,
items=items,
payment_due_date=self._format_date(document_entity.payment_due_date),
notes=document_entity.notes or ""
)
elif document_entity.doc_type == "invoice":
odoo_id = self.odoo.create_invoice(
customer_id=odoo_customer_id,
items=items,
payment_due_date=self._format_date(document_entity.payment_due_date),
notes=document_entity.notes or ""
)
else:
odoo_id = 0
if odoo_id:
document_entity.odoo_id = odoo_id
return {"status": "success", "odoo_id": odoo_id}
else:
return {"status": "error", "message": "Failed to create Odoo document"}
except Exception as e:
logger.error(f"Sync error: {str(e)}")
return {"status": "error", "message": str(e)}
@staticmethod
def _format_date(timestamp: int) -> str:
"""タイムスタンプを ISO 形式に変換"""
from datetime import datetime
return datetime.fromtimestamp(timestamp / 1000).isoformat()
# FastAPI メインに統合される部分
from fastapi import FastAPI, Depends
from sqlalchemy.orm import Session
app = FastAPI()
# グローバル Odoo クライアント
odoo_client = OdooClient(
odoo_url=os.getenv("ODOO_URL", "http://localhost:8069"),
db="odoo",
username=os.getenv("ODOO_USER", "admin"),
password=os.getenv("ODOO_PASSWORD", "admin")
)
sync_service = SyncService(odoo_client)
@app.post("/api/v1/sync")
async def sync_documents(request: SyncRequest, db: Session = Depends(get_db)):
"""ドキュメント同期エンドポイント"""
synced_count = 0
new_documents = []
for doc in request.documents:
# ドキュメント作成DB
db_doc = Document(...)
db.add(db_doc)
db.commit()
# Odoo に同期
result = sync_service.sync_document(db, db_doc)
if result["status"] == "success":
synced_count += 1
new_documents.append({
"local_id": db_doc.id,
"odoo_id": result.get("odoo_id"),
"doc_type": db_doc.doc_type
})
return SyncResponse(
status="success",
message=f"Synced {synced_count} documents",
synced_documents=synced_count,
new_documents=new_documents
)