334 lines
11 KiB
Python
334 lines
11 KiB
Python
# 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
|
||
)
|