第18讲、Odoo接口开发详解:原理、类型与实践

Python3.8

Python 是一种高级、解释型、通用的编程语言,以其简洁易读的语法而闻名,适用于广泛的应用,包括Web开发、数据分析、人工智能和自动化脚本

1. 引言

Odoo作为一个功能强大的开源ERP和业务应用套件,其开放性和可扩展性是核心优势之一。接口(API)开发在Odoo生态中扮演着至关重要的角色,它使得Odoo能够与外部系统、第三方应用、移动端以及Web前端进行数据交换和功能集成。理解Odoo的接口开发机制,对于实现复杂的业务集成、构建定制化解决方案至关重要。本文档将深入探讨Odoo接口开发的实现原理,区分不同的接口类型,并提供实践指导。

2. Odoo接口开发原理与技术架构

Odoo的接口开发主要基于其稳健的Web框架和模型驱动的架构。其核心原理可以概括为以下几点:

2.1 客户端-服务器架构

Odoo采用标准的客户端-服务器(Client-Server)架构。客户端(通常是Web浏览器,也可以是其他应用程序)通过网络向Odoo服务器发送请求,服务器处理请求后返回响应。这种架构使得Odoo的功能可以通过网络被远程访问和调用。

2.2 RPC(远程过程调用)

Odoo原生支持RPC机制,允许外部应用程序像调用本地函数一样调用Odoo服务器上的方法。这是实现外部系统与Odoo交互的基础。Odoo主要支持两种RPC协议:XML-RPC和JSON-RPC。这两种协议都允许通过HTTP(S)传输结构化的请求和响应数据。

2.3 Web控制器(Controllers)

除了标准的RPC接口,Odoo还提供了一个强大的Web控制器层。开发者可以通过创建自定义控制器来定义HTTP路由(Routes),处理HTTP请求(GET, POST, PUT, DELETE等),并返回各种格式的响应(HTML, JSON, 文件等)。这使得开发者能够构建灵活的、符合RESTful风格的API,或者实现自定义的Web页面和交互逻辑。

2.4 模型驱动

无论是通过RPC还是控制器,对Odoo数据的操作最终都会落实到Odoo的模型(Models)层。Odoo的模型定义了数据结构和业务逻辑。接口调用通常会触发模型方法的执行,如create(), search_read(), write(), unlink()等,从而实现对数据的增删改查。

3. Odoo接口类型详解

Odoo提供了多种接口开发方式,以适应不同的集成需求和技术偏好。主要可以分为以下几类:

3.1 XML-RPC接口

XML-RPC是一种基于XML的RPC协议,允许通过HTTP发送方法调用请求。Odoo通过/xmlrpc/2/common(用于认证和数据库信息)和/xmlrpc/2/object(用于调用模型方法)两个端点提供XML-RPC服务。

原理: 客户端将方法调用信息(方法名、参数)编码为XML格式,通过HTTP POST请求发送到Odoo服务器的指定端点。服务器解析XML请求,执行相应的方法,并将结果编码为XML格式返回给客户端。

适用场景: 主要用于需要与多种异构系统(特别是那些原生支持XML-RPC的系统)进行集成的场景。虽然相对古老,但仍然是一种稳定可靠的集成方式。

实现示例 (Python):

import xmlrpc.client

url = "http://localhost:8069"  # Odoo服务器地址
db = "your_database_name"      # Odoo数据库名
username = "admin"              # Odoo用户名
password = "your_password"      # Odoo密码

# 1. 认证 获取用户ID (uid)
try:
    common = xmlrpc.client.ServerProxy(f'{url}/xmlrpc/2/common')
    uid = common.authenticate(db, username, password, {})
    if not uid:
        print("认证失败!")
        exit()
    print(f"认证成功, UID: {uid}")
except Exception as e:
    print(f"连接或认证错误: {e}")
    exit()

# 2. 调用模型方法 (以查询合作伙伴为例)
models = xmlrpc.client.ServerProxy(f'{url}/xmlrpc/2/object')

try:
    partner_ids = models.execute_kw(db, uid, password,
                                    'res.partner',  # 模型名
                                    'search',       # 方法名
                                    [[['is_company', '=', True]]]) # 参数 (domain)
    print(f"找到的公司数量: {len(partner_ids)}")

    if partner_ids:
        partners_data = models.execute_kw(db, uid, password,
                                          'res.partner',
                                          'read',         # 方法名
                                          [partner_ids],  # 参数 (ids)
                                          {'fields': ['name', 'email', 'phone']}) # 可选参数 (指定字段)
        print("合作伙伴信息:")
        for partner in partners_data:
            print(partner)

except xmlrpc.client.Fault as error:
    print(f"XML-RPC 调用错误: {error}")
except Exception as e:
    print(f"执行模型方法时出错: {e}")

优缺点:

  • 优点:标准化协议,跨语言支持良好,稳定。
  • 缺点:基于XML,数据传输相对冗余;相比JSON-RPC和REST,性能可能稍差。

3.2 JSON-RPC接口

JSON-RPC是一种基于JSON的轻量级RPC协议。Odoo通过/jsonrpc端点提供JSON-RPC服务。

原理: 客户端将方法调用信息编码为JSON格式,通过HTTP POST请求发送到Odoo服务器的/jsonrpc端点。请求体是一个包含jsonrpc, method, params, id等字段的JSON对象。服务器解析JSON请求,执行相应的方法,并将结果编码为JSON格式返回。

适用场景: 广泛用于Web前端(JavaScript可以直接处理JSON)、移动应用以及需要轻量级数据交换的现代系统集成。

实现示例 (请求格式):

POST /jsonrpc HTTP/1.1
Host: localhost:8069
Content-Type: application/json

{
  "jsonrpc": "2.0",
  "method": "call", // 固定为 'call'
  "params": {
    "service": "object", // 'common' 用于认证, 'object' 用于模型方法
    "method": "execute_kw", // 调用模型方法的标准方法
    "args": [
      "your_database_name", // 数据库名
      1, // 用户UID (认证后获得)
      "your_password", // 用户密码
      "res.partner", // 目标模型
      "search_read", // 要调用的模型方法
      [[["is_company", "=", true]]], // 方法的位置参数 (domain)
      {"fields": ["name", "email"], "limit": 5} // 方法的关键字参数
    ]
  },
  "id": 1 // 请求ID,用于匹配响应
}

Python实现示例:

import requests
import json

url = "http://localhost:8069/jsonrpc"
db = "your_database_name"
username = "admin"
password = "your_password"

# 1. 认证获取UID
payload = {
    "jsonrpc": "2.0",
    "method": "call",
    "params": {
        "service": "common",
        "method": "authenticate",
        "args": [db, username, password, {}]
    },
    "id": 1
}

response = requests.post(url, data=json.dumps(payload), headers={"Content-Type": "application/json"})
result = response.json()
if "error" in result:
    print(f"认证错误: {result['error']}")
    exit()

uid = result["result"]
print(f"认证成功, UID: {uid}")

# 2. 调用模型方法
payload = {
    "jsonrpc": "2.0",
    "method": "call",
    "params": {
        "service": "object",
        "method": "execute_kw",
        "args": [
            db, uid, password,
            "res.partner",
            "search_read",
            [[["is_company", "=", True]]],
            {"fields": ["name", "email", "phone"], "limit": 5}
        ]
    },
    "id": 2
}

response = requests.post(url, data=json.dumps(payload), headers={"Content-Type": "application/json"})
result = response.json()
if "error" in result:
    print(f"调用错误: {result['error']}")
else:
    print("合作伙伴信息:")
    for partner in result["result"]:
        print(partner)

优缺点:

  • 优点:基于JSON,轻量、易于解析,性能较好,特别适合Web和移动端开发。
  • 缺点:虽然是标准,但相比RESTful API在语义表达上可能不够直观。

3.3 自定义控制器 (RESTful API)

通过编写自定义控制器,开发者可以创建完全符合REST(Representational State Transfer)架构风格的API。RESTful API使用标准的HTTP方法(GET, POST, PUT, DELETE等)对资源进行操作,并通过URL路径标识资源。

原理: 开发者在Odoo模块的controllers目录下创建Python文件,定义继承自odoo.http.Controller的类。在类中使用@http.route()装饰器来定义路由规则,包括URL路径、允许的HTTP方法、认证方式 (auth)、响应类型 (type)等。控制器方法内部可以使用request对象访问请求信息(如参数、头信息)和Odoo环境 (request.env) 来调用模型方法或执行其他业务逻辑,最后构造并返回HTTP响应。

适用场景: 构建现代Web应用、移动应用的后端API;需要对API结构、请求/响应格式、认证方式有完全控制权的场景;希望提供更直观、语义化的API接口。

实现示例 (controllers/main.py):

from odoo import http
from odoo.http import request, Response
import json

class PartnerApiController(http.Controller):

    # 获取合作伙伴列表 (JSON响应)
    @http.route('/api/v1/partners', type='json', auth='user', methods=['GET'], csrf=False)
    def get_partners_json(self, limit=10, offset=0, domain=None, **kwargs):
        """ 获取合作伙伴列表 (需要用户认证) """
        try:
            # 考虑安全性,对传入的domain进行处理或限制
            search_domain = [('customer_rank', '>', 0)] # 示例:只获取客户
            if domain and isinstance(domain, list):
                # 注意:直接使用外部传入的domain可能存在安全风险,需要校验
                # search_domain.extend(domain)
                pass # 实际应用中应谨慎处理外部domain

            partners = request.env['res.partner'].search_read(
                domain=search_domain,
                fields=['id', 'name', 'email', 'phone'],
                limit=int(limit),
                offset=int(offset)
            )
            total = request.env['res.partner'].search_count(search_domain)
            return {'status': 'success', 'total': total, 'data': partners}
        except Exception as e:
            # 记录错误日志
            request.env.cr.rollback()
            return {'status': 'error', 'message': str(e)}

    # 获取单个合作伙伴详情 (HTTP响应, REST风格)
    @http.route('/api/v1/partners/<int:partner_id>', type='http', auth='user', methods=['GET'], csrf=False)
    def get_partner_http(self, partner_id, **kwargs):
        """ 获取单个合作伙伴详情 (需要用户认证) """
        partner = request.env['res.partner'].search_read(
            domain=[('id', '=', partner_id)],
            fields=['id', 'name', 'email', 'phone', 'street', 'city', 'country_id']
        )
        if not partner:
            return Response(json.dumps({'status': 'error', 'message': 'Partner not found'}), status=404, content_type='application/json')

        # 返回JSON格式的HTTP响应
        return Response(json.dumps({'status': 'success', 'data': partner[0]}), content_type='application/json')

    # 创建合作伙伴 (JSON响应)
    @http.route('/api/v1/partners', type='json', auth='user', methods=['POST'], csrf=False)
    def create_partner(self, **kwargs):
        """ 创建新的合作伙伴 (需要用户认证) """
        required_fields = ['name']
        missing_fields = [f for f in required_fields if f not in kwargs]
        if missing_fields:
            return {'status': 'error', 'message': f'Missing required fields: {missing_fields}'}

        try:
            # 提取请求数据
            partner_data = {
                'name': kwargs.get('name'),
                'email': kwargs.get('email'),
                'phone': kwargs.get('phone'),
                'street': kwargs.get('street'),
                'city': kwargs.get('city'),
                'customer_rank': kwargs.get('is_customer') and 1 or 0,
            }
            
            # 创建记录
            partner = request.env['res.partner'].create(partner_data)
            return {
                'status': 'success',
                'message': 'Partner created successfully',
                'id': partner.id
            }
        except Exception as e:
            request.env.cr.rollback()
            return {'status': 'error', 'message': str(e)}

    # 更新合作伙伴 (HTTP响应, REST风格)
    @http.route('/api/v1/partners/<int:partner_id>', type='json', auth='user', methods=['PUT'], csrf=False)
    def update_partner(self, partner_id, **kwargs):
        """ 更新合作伙伴信息 (需要用户认证) """
        partner = request.env['res.partner'].browse(partner_id)
        if not partner.exists():
            return {'status': 'error', 'message': 'Partner not found'}

        try:
            # 提取要更新的字段
            update_data = {}
            for field in ['name', 'email', 'phone', 'street', 'city']:
                if field in kwargs:
                    update_data[field] = kwargs[field]
            
            if 'is_customer' in kwargs:
                update_data['customer_rank'] = kwargs['is_customer'] and 1 or 0
            
            # 更新记录
            partner.write(update_data)
            return {
                'status': 'success',
                'message': 'Partner updated successfully'
            }
        except Exception as e:
            request.env.cr.rollback()
            return {'status': 'error', 'message': str(e)}

    # 删除合作伙伴 (HTTP响应, REST风格)
    @http.route('/api/v1/partners/<int:partner_id>', type='json', auth='user', methods=['DELETE'], csrf=False)
    def delete_partner(self, partner_id, **kwargs):
        """ 删除合作伙伴 (需要用户认证) """
        partner = request.env['res.partner'].browse(partner_id)
        if not partner.exists():
            return {'status': 'error', 'message': 'Partner not found'}

        try:
            # 删除记录
            partner.unlink()
            return {
                'status': 'success',
                'message': 'Partner deleted successfully'
            }
        except Exception as e:
            request.env.cr.rollback()
            return {'status': 'error', 'message': str(e)}

优缺点:

  • 优点:完全可定制,符合现代API设计标准,语义清晰,可以精确控制请求/响应格式和认证方式。
  • 缺点:需要更多的代码编写,相比直接使用RPC接口,开发工作量较大。

4. 接口认证与安全

无论使用哪种接口类型,安全性都是一个关键考量。Odoo提供了多种认证和安全机制:

4.1 基本认证方式

  1. 用户名/密码认证:最基本的认证方式,适用于XML-RPC和JSON-RPC。
  2. Session认证:基于Cookie的会话认证,主要用于Web界面和控制器。
  3. API密钥认证:通过自定义控制器实现,更适合系统间集成。
  4. OAuth2认证:可以通过第三方模块实现,支持更复杂的授权流程。

4.2 控制器认证参数

在自定义控制器中,@http.route()装饰器的auth参数控制认证方式:

  • auth='none':无需认证,任何人都可以访问(适用于公开API)。
  • auth='public':允许未登录用户访问,但会创建一个公共(匿名)用户会话。
  • auth='user':需要登录用户才能访问(默认值)。
  • auth='admin':仅管理员可以访问。

4.3 安全最佳实践

  1. CSRF保护:对于需要修改数据的请求,Odoo默认启用CSRF保护。如果是外部API调用,可以通过设置csrf=False来禁用。
  2. 数据验证:始终验证和清理输入数据,特别是来自外部的请求参数。
  3. 权限控制:利用Odoo的访问控制列表(ACL)和记录规则来限制数据访问。
  4. 使用sudo()谨慎sudo()方法可以绕过权限检查,应谨慎使用,并确保在必要时恢复正常环境。
  5. API速率限制:考虑实现API调用速率限制,防止滥用。
  6. 日志记录:记录关键API调用,便于审计和故障排查。

4.4 JWT认证实现示例

JWT(JSON Web Token)是一种流行的API认证方式,特别适合无状态的RESTful API。以下是在Odoo中实现JWT认证的简化示例:

# 需要安装PyJWT库: pip install PyJWT
import jwt
import datetime
from odoo import http
from odoo.http import request, Response
import json

SECRET_KEY = "your-secret-key"  # 实际应用中应存储在安全的配置中

class JWTAuthController(http.Controller):
    
    @http.route('/api/auth/token', type='json', auth='none', methods=['POST'], csrf=False)
    def get_token(self, **kwargs):
        """获取JWT令牌的接口"""
        username = kwargs.get('username')
        password = kwargs.get('password')
        
        if not username or not password:
            return {'status': 'error', 'message': 'Missing credentials'}
        
        # 验证用户凭据
        uid = request.env['res.users'].sudo().authenticate(request.env.cr.dbname, username, password, {})
        if not uid:
            return {'status': 'error', 'message': 'Invalid credentials'}
        
        # 生成JWT令牌
        payload = {
            'uid': uid,
            'exp': datetime.datetime.utcnow() + datetime.timedelta(days=1),  # 1天过期
            'iat': datetime.datetime.utcnow(),
        }
        token = jwt.encode(payload, SECRET_KEY, algorithm='HS256')
        
        return {'status': 'success', 'token': token}
    
    @staticmethod
    def validate_token(token):
        """验证JWT令牌"""
        try:
            payload = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
            return payload
        except jwt.ExpiredSignatureError:
            return None
        except jwt.InvalidTokenError:
            return None

# 使用JWT保护的API示例
class ProtectedApiController(http.Controller):
    
    @http.route('/api/protected/data', type='http', auth='none', methods=['GET'], csrf=False)
    def get_protected_data(self, **kwargs):
        """受JWT保护的API端点"""
        # 从请求头获取令牌
        auth_header = request.httprequest.headers.get('Authorization')
        if not auth_header or not auth_header.startswith('Bearer '):
            return Response(json.dumps({'status': 'error', 'message': 'No token provided'}), 
                           status=401, content_type='application/json')
        
        token = auth_header.split(' ')[1]
        payload = JWTAuthController.validate_token(token)
        
        if not payload:
            return Response(json.dumps({'status': 'error', 'message': 'Invalid or expired token'}), 
                           status=401, content_type='application/json')
        
        # 使用令牌中的用户ID设置环境
        uid = payload.get('uid')
        try:
            # 以特定用户身份执行操作
            user = request.env['res.users'].sudo().browse(uid)
            if not user.exists():
                return Response(json.dumps({'status': 'error', 'message': 'User not found'}), 
                               status=401, content_type='application/json')
            
            # 切换到该用户的环境
            env = request.env(user=uid)
            
            # 执行受保护的操作
            data = env['res.partner'].search_read(
                domain=[('customer_rank', '>', 0)],
                fields=['name', 'email'],
                limit=5
            )
            
            return Response(json.dumps({'status': 'success', 'data': data}), 
                           content_type='application/json')
        except Exception as e:
            return Response(json.dumps({'status': 'error', 'message': str(e)}), 
                           status=500, content_type='application/json')

5. 接口的组织与最佳实践

5.1 模块结构

一个标准的Odoo API模块结构如下:

my_api_module/
├── __init__.py              # 导入controllers和models
├── __manifest__.py          # 模块清单
├── controllers/
│   ├── __init__.py          # 导入控制器
│   ├── main.py              # 主控制器
│   └── auth.py              # 认证控制器
├── models/
│   ├── __init__.py          # 导入模型
│   └── api_log.py           # API日志模型
├── security/
│   └── ir.model.access.csv  # 访问权限
└── static/
    └── description/
        └── icon.png         # 模块图标

5.2 API版本控制

在URL路径中包含版本号是一种良好的实践,例如/api/v1/partners。这样在API变更时,可以保持向后兼容性。

# v1版本API
@http.route('/api/v1/partners', ...)
def get_partners_v1(self, **kwargs):
    # v1实现
    pass

# v2版本API(新增功能或改变行为)
@http.route('/api/v2/partners', ...)
def get_partners_v2(self, **kwargs):
    # v2实现
    pass

5.3 响应格式标准化

保持一致的响应格式有助于客户端处理:

def standard_response(success=True, data=None, message=None, status_code=200):
    """生成标准化的API响应"""
    response = {
        'status': 'success' if success else 'error',
    }
    
    if data is not None:
        response['data'] = data
    
    if message:
        response['message'] = message
    
    if not success and status_code == 200:
        status_code = 400  # 默认错误状态码
    
    return Response(json.dumps(response), status=status_code, content_type='application/json')

5.4 API日志记录

记录API调用对于调试和审计非常有用:

def log_api_call(self, endpoint, request_data, response_data, success, user_id=None):
    """记录API调用"""
    try:
        log_data = {
            'name': endpoint,
            'request_data': json.dumps(request_data),
            'response_data': json.dumps(response_data),
            'success': success,
            'user_id': user_id or request.env.uid,
            'ip_address': request.httprequest.remote_addr,
        }
        request.env['api.log'].sudo().create(log_data)
    except Exception as e:
        _logger.error(f"Failed to log API call: {e}")

5.5 分页、过滤和排序

对于返回大量数据的API,应实现分页、过滤和排序功能:

@http.route('/api/v1/partners', type='http', auth='user', methods=['GET'], csrf=False)
def get_partners_paginated(self, **kwargs):
    # 分页参数
    limit = int(kwargs.get('limit', 10))
    offset = int(kwargs.get('offset', 0))
    
    # 过滤参数
    filters = {}
    for key in ['name', 'email', 'country_id']:
        if key in kwargs:
            filters[key] = kwargs[key]
    
    # 构建domain
    domain = []
    for key, value in filters.items():
        if key == 'name' or key == 'email':
            domain.append((key, 'ilike', value))
        else:
            domain.append((key, '=', int(value)))
    
    # 排序参数
    order = kwargs.get('order', 'name asc')
    
    # 查询数据
    partners = request.env['res.partner'].search_read(
        domain=domain,
        fields=['name', 'email', 'phone', 'country_id'],
        limit=limit,
        offset=offset,
        order=order
    )
    
    # 获取总记录数(用于分页)
    total = request.env['res.partner'].search_count(domain)
    
    # 构建响应
    response = {
        'status': 'success',
        'data': partners,
        'pagination': {
            'total': total,
            'limit': limit,
            'offset': offset,
            'pages': (total + limit - 1) // limit
        }
    }
    
    return Response(json.dumps(response), content_type='application/json')

6. Odoo 18的接口增强

Odoo 18版本在接口开发方面引入了一些增强功能:

6.1 HTTP方法支持扩展

@http.route装饰器现在支持更多HTTP方法的限定,包括PUTDELETEPATCH等,使得开发RESTful API更加便捷。

@http.route('/api/resource', methods=['PATCH'], ...)
def update_resource_partial(self, **kwargs):
    # 处理PATCH请求(部分更新)
    pass

6.2 请求头处理增强

可以更方便地访问和处理HTTP请求头:

@http.route('/api/headers', type='http', auth='none', csrf=False)
def handle_headers(self, **kwargs):
    # 获取请求头
    headers = request.httprequest.headers
    user_agent = headers.get('User-Agent')
    content_type = headers.get('Content-Type')
    
    # 设置响应头
    response = Response(json.dumps({'status': 'success'}), content_type='application/json')
    response.headers['X-Custom-Header'] = 'Custom Value'
    
    return response

6.3 JSON请求处理改进

Odoo 18改进了对嵌套JSON数据的处理:

@http.route('/api/complex-data', type='json', auth='none', csrf=False)
def handle_complex_json(self, **kwargs):
    # 直接访问嵌套的JSON数据
    nested_data = kwargs.get('nested', {})
    value = nested_data.get('deep', {}).get('value')
    
    return {'received_value': value}

6.4 Webhook支持

使用auth='none'可以轻松创建外部系统可访问的webhook端点:

@http.route('/webhook/payment-notification', type='json', auth='none', csrf=False)
def payment_webhook(self, **kwargs):
    # 验证webhook签名
    signature = request.httprequest.headers.get('X-Webhook-Signature')
    if not self._validate_webhook_signature(signature, request.httprequest.data):
        return {'status': 'error', 'message': 'Invalid signature'}
    
    # 处理webhook数据
    payment_data = kwargs
    # ... 处理支付通知 ...
    
    return {'status': 'success'}

7. 实际应用场景

7.1 与外部系统集成

7.1.1 ERP系统集成
# 在外部ERP系统中调用Odoo API
def sync_products_from_odoo():
    # 使用XML-RPC连接Odoo
    common = xmlrpc.client.ServerProxy(f'{ODOO_URL}/xmlrpc/2/common')
    uid = common.authenticate(ODOO_DB, ODOO_USER, ODOO_PASSWORD, {})
    models = xmlrpc.client.ServerProxy(f'{ODOO_URL}/xmlrpc/2/object')
    
    # 获取产品数据
    products = models.execute_kw(ODOO_DB, uid, ODOO_PASSWORD,
                                'product.product', 'search_read',
                                [[['sale_ok', '=', True]]],
                                {'fields': ['name', 'list_price', 'default_code', 'barcode']})
    
    # 同步到本地数据库
    for product in products:
        update_local_product(product)
7.1.2 电子商务集成
# Odoo控制器处理来自电商平台的订单
@http.route('/api/v1/orders', type='json', auth='api_key', methods=['POST'], csrf=False)
def receive_ecommerce_order(self, **kwargs):
    # 验证API密钥
    api_key = request.httprequest.headers.get('X-API-Key')
    if not self._validate_api_key(api_key):
        return {'status': 'error', 'message': 'Invalid API key'}
    
    # 创建销售订单
    try:
        order_data = kwargs.get('order', {})
        customer_data = order_data.get('customer', {})
        
        # 查找或创建客户
        customer = self._find_or_create_customer(customer_data)
        
        # 创建订单头
        order_vals = {
            'partner_id': customer.id,
            'date_order': fields.Datetime.now(),
            'external_id': order_data.get('external_id'),
            'note': order_data.get('note'),
        }
        order = request.env['sale.order'].sudo().create(order_vals)
        
        # 创建订单行
        for line in order_data.get('lines', []):
            product = request.env['product.product'].sudo().search([
                ('default_code', '=', line.get('sku'))
            ], limit=1)
            
            if product:
                line_vals = {
                    'order_id': order.id,
                    'product_id': product.id,
                    'product_uom_qty': line.get('quantity', 1.0),
                    'price_unit': line.get('price_unit'),
                }
                request.env['sale.order.line'].sudo().create(line_vals)
        
        # 确认订单
        if order_data.get('confirm', False):
            order.action_confirm()
        
        return {
            'status': 'success',
            'message': 'Order created successfully',
            'order_id': order.id,
            'name': order.name
        }
    except Exception as e:
        request.env.cr.rollback()
        _logger.error(f"Failed to create order: {e}")
        return {'status': 'error', 'message': str(e)}

7.2 移动应用后端

# 为移动应用提供认证API
@http.route('/api/v1/mobile/login', type='json', auth='none', methods=['POST'], csrf=False)
def mobile_login(self, **kwargs):
    username = kwargs.get('username')
    password = kwargs.get('password')
    
    if not username or not password:
        return {'status': 'error', 'message': 'Missing credentials'}
    
    try:
        uid = request.env['res.users'].sudo().authenticate(request.env.cr.dbname, username, password, {})
        if not uid:
            return {'status': 'error', 'message': 'Invalid credentials'}
        
        user = request.env['res.users'].sudo().browse(uid)
        
        # 生成访问令牌
        token_data = {
            'uid': uid,
            'name': user.name,
            'exp': datetime.datetime.utcnow() + datetime.timedelta(days=30),
        }
        token = self._generate_token(token_data)
        
        # 返回用户信息和令牌
        return {
            'status': 'success',
            'token': token,
            'user': {
                'id': user.id,
                'name': user.name,
                'email': user.email,
                'image': user.image_128 and user.image_128.decode('utf-8') or False,
            }
        }
    except Exception as e:
        _logger.error(f"Mobile login error: {e}")
        return {'status': 'error', 'message': 'Login failed'}

7.3 第三方服务集成

# 集成支付网关回调
@http.route('/api/payment/callback', type='http', auth='none', methods=['POST'], csrf=False)
def payment_gateway_callback(self, **kwargs):
    # 验证回调请求
    signature = request.httprequest.headers.get('X-Signature')
    if not self._verify_payment_signature(signature, kwargs):
        return Response("Invalid signature", status=400)
    
    # 处理支付结果
    payment_reference = kwargs.get('reference')
    status = kwargs.get('status')
    amount = float(kwargs.get('amount', '0.0'))
    
    try:
        # 查找相关订单
        order = request.env['sale.order'].sudo().search([
            ('payment_reference', '=', payment_reference)
        ], limit=1)
        
        if not order:
            return Response("Order not found", status=404)
        
        # 更新订单状态
        if status == 'paid':
            # 创建支付记录
            payment_vals = {
                'amount': amount,
                'payment_type': 'inbound',
                'partner_type': 'customer',
                'partner_id': order.partner_id.id,
                'communication': f"Payment for {order.name}",
                # 其他必要字段...
            }
            payment = request.env['account.payment'].sudo().create(payment_vals)
            
            # 确认支付
            payment.action_post()
            
            # 更新订单
            order.write({'payment_status': 'paid'})
            
            # 如果订单未确认,则确认订单
            if order.state == 'draft':
                order.action_confirm()
            
            return Response("Payment processed successfully", status=200)
        else:
            order.write({'payment_status': status})
            return Response("Payment status updated", status=200)
    except Exception as e:
        request.env.cr.rollback()
        _logger.error(f"Payment callback error: {e}")
        return Response(f"Error processing payment: {e}", status=500)

8. 接口测试与调试

8.1 使用Postman测试

Postman是测试API的常用工具。以下是测试Odoo API的基本步骤:

  1. 设置环境变量

    • odoo_url: Odoo服务器URL
    • db_name: 数据库名称
    • username: 用户名
    • password: 密码
  2. 创建认证请求

    • 对于XML-RPC,使用Pre-request Script转换请求
    • 对于JSON-RPC,直接发送JSON请求
    • 对于自定义API,根据认证方式设置
  3. 保存认证响应

    • 使用Tests脚本保存认证令牌或UID
  4. 创建API请求集合

    • 组织不同的API请求
    • 使用保存的认证信息

8.2 日志调试

在Odoo服务器端启用调试日志:

import logging
_logger = logging.getLogger(__name__)

@http.route('/api/debug', ...)
def debug_api(self, **kwargs):
    _logger.info("Request received: %s", kwargs)
    _logger.debug("Request headers: %s", request.httprequest.headers)
    
    # 处理请求...
    
    _logger.info("Response sent: %s", response)
    return response

8.3 使用OpenAPI/Swagger文档

通过第三方模块(如rest_api_documentation),可以为Odoo API生成Swagger文档:

# 在控制器方法中添加OpenAPI文档注释
@http.route('/api/v1/partners', ...)
def get_partners(self, **kwargs):
    """
    @api {get} /api/v1/partners 获取合作伙伴列表
    @apiName GetPartners
    @apiGroup Partner
    @apiVersion 1.0.0
    
    @apiParam {Number} [limit=10] 每页记录数
    @apiParam {Number} [offset=0] 偏移量
    @apiParam {String} [name] 按名称过滤
    
    @apiSuccess {String} status 请求状态
    @apiSuccess {Number} total 总记录数
    @apiSuccess {Object[]} data 合作伙伴列表
    @apiSuccess {Number} data.id 合作伙伴ID
    @apiSuccess {String} data.name 合作伙伴名称
    @apiSuccess {String} data.email 电子邮件
    
    @apiError {String} status 错误状态
    @apiError {String} message 错误消息
    """
    # 实现代码...

9. 总结与最佳实践

9.1 选择合适的接口类型

  • XML-RPC:适用于需要与多种异构系统集成,特别是那些原生支持XML-RPC的系统。
  • JSON-RPC:适用于Web前端、移动应用以及现代系统集成,特别是当性能和数据量是考量因素时。
  • 自定义控制器(RESTful API):适用于需要完全控制API结构、认证方式和响应格式的场景,特别是构建面向外部的API。

9.2 接口设计原则

  1. 一致性:保持URL结构、请求/响应格式的一致性。
  2. 版本控制:在URL中包含版本号,便于API演进。
  3. 资源导向:使用名词(而非动词)来表示资源,如/partners而非/getPartners
  4. HTTP方法语义:正确使用HTTP方法(GET, POST, PUT, DELETE)。
  5. 状态码使用:使用标准HTTP状态码表达请求结果。
  6. 分页与过滤:为返回大量数据的API实现分页和过滤机制。
  7. 错误处理:提供清晰的错误信息和状态码。

9.3 安全最佳实践

  1. 最小权限原则:API只应具有完成其任务所需的最小权限。
  2. 输入验证:始终验证和清理所有输入数据。
  3. HTTPS:始终使用HTTPS加密传输。
  4. 认证与授权:实施强大的认证机制,并确保适当的授权检查。
  5. API密钥轮换:定期轮换API密钥和令牌。
  6. 速率限制:实施API调用速率限制,防止滥用。
  7. 审计日志:记录关键API操作,便于审计和故障排查。

9.4 性能优化

  1. 字段选择:只请求和返回必要的字段。
  2. 批量操作:尽可能使用批量操作而非多次单独调用。
  3. 缓存:适当使用缓存减少数据库查询。
  4. 延迟加载:对于大型数据集,实现分页和延迟加载。
  5. 异步处理:对于耗时操作,考虑异步处理。

通过遵循这些原则和最佳实践,开发者可以创建安全、高效、易于使用的Odoo接口,满足各种集成和扩展需求。无论是构建内部系统集成,还是开发面向外部的API,Odoo的接口开发机制都提供了强大而灵活的支持。

您可能感兴趣的与本文相关的镜像

Python3.8

Python3.8

Conda
Python

Python 是一种高级、解释型、通用的编程语言,以其简洁易读的语法而闻名,适用于广泛的应用,包括Web开发、数据分析、人工智能和自动化脚本

1 ERP 简介 1 1.1 Odoo 历史 1 1.2 ERPⅡ 或商业智能化 2 1.2.1 什么是商业智能 4 2 Odoo 框架简介 7 2.1 python 模块分析 8 2.2 python2 还是 python3 8 3 Odoo 的安装和配置 9 3.1 PostgreSQL 数据库 10 3.2 Ubuntu14.04 下可能缺失的软件包 11 3.3 网页显示 node.js 方面 11 3.4 其他问题 12 3.5 通过命令行运行时的配置 12 3.5.1 –xmlrpc-port=8888 12 3.5.2 –addons-path=addons 12 3.5.3 数据库的一些配置 13 3.5.4 –save 13 3.6 将安装环境封装起来 13 3.7 文档编译 14 4 初入 Odoo 17 4.1 管理数据库 17 4.2 登录界面 18 4.3 Administrator 首选项 19 4.4 导入一个翻译 20 4.5 新的 Demo 用户 20 4.6 模块管理 21 4.7 修改公司信息 21 4.8 打开技术特性支持之后 22 4.9 进销存和财务系统的抽象讨论 22 4.9.1 以采购部门为例 23 4.10 安装和配置模块 24 5 创建自己的模块 27 5.1 快速生成模块骨架 27 5.1.1 python 模块的 init 文件 28 5.1.2 作为 Odoo 模块的说明文件 29 5.2 安装自定义模块 32 5.2.1 模块文件夹管理 32 5.3 一个简单的演示模块 33 5.3.1 controllers 33 5.3.2 views 33 5.3.3 models 35 5.3.4 security 37 5.3.5 美化网页 38 5.4 加分项: 通过 pgadmin3 来查看数据库 39 5.4.1 安装 39 5.4.2 连接服务器 39 5.4.3 图形化查询 40 6 Odoo 开发基础: 请假模块第一谈 43 6.1 纯理论讨论 43 6.2 定义模型 45 6.3 加入菜单 46 6.3.1 act_window 的属性 48 6.3.2 menuitem 的属性 48 6.4 视图优化 48 6.4.1 修改 tree 视图 49 6.4.2 修改 form 视图 49 6.5 完整的 views.xml 51 6.6 给模块加个图标 53 7 Odoo 开发基础: 工作计划模块第一谈 55 7.1 数据访问权限管理 61 7.1.1 access rule 62 7.1.2 record rule 62 8 扩展现有模块-继承机制 65 8.1 给模块增加 field 65 8.2 修改已有的 field 66 8.3 重载原模型的方法 66 8.3.1 什么是 Recordset 67 8.3.2 Odoo 里面的 domain 语法 68 8.3.3 recordset 的 search 方法 69 8.4 视图 xml 文件的继承式修改 70 8.4.1 视图元素添加 71 8.4.2 原视图元素属性修改 71 8.5 多态继承 72 8.6 修改其他数据文件 73 8.6.1 删除记录 73 8.6.2 更新数据 73 8.7 委托继承 74 9 理解模型内的数据文件 75 9.1 理解外部 id 75 9.2 使用外部 id 77 9.3 导出或导入数据文件 77 9.4 快捷输入标签 78 9.5 用 field 标签设置值 78 9.5.1 eval 语法 78 9.5.2 ref 属性 79 9.5.3 One2many 和 Many2many 的 eval 赋值 79 10 Odoo 开发基础: 请假模块第二谈 81 10.1 本例涉及到的数据库表格简介 89 10.2 工作流概念入门 89 10.2.1 定义工作流对象 90 10.2.2 创建节点 91 10.2.3 创建连接 91 11 Odoo 模型层详解 93 11.1 _name 93 11.2 各个表头属性 93 11.3 name 字段 94 11.4 具体模型的数据 94 11.5 模型间的关系 95 11.6 工作流 95 12 Odoo 视图层详解 97 13 附录 99 13.1 Odoo 里老的 API 99 13.2 PostgreSQL 数据库命令行操作 99 13.2.1 命令行数据库备份 99 13.3 反向代理 (reverse proxy) 99 13.3.1 安装 ngnix 软件 100 13.3.2 强制 https 连接 102 13.3.3 nginx 优化 102 13.3.4 轮询机制 102 13.4 跟踪项目源码初始化进程 102 13.4.1 base 模块 104 13.4.2 web 模块 105 13.4.3 web_kanban 模块 105 13.5 配置会计科目 105 13.5.1 配置会计科目类型 105 13.5.2 配置会计科目 106 13.6 分录 106 13.7 新建业务伙伴 106 13.7.1 新建业务伙伴标签 106 13.7.2 新建客户 106 13.8 创建新的产品 107 13.9 设置会计年度 107 13.10向供应商下单 107 13.11会计学入门 107 13.11.1财务报表 108 13.11.2原始凭证 108 13.11.3账户 108 13.11.4分类帐 109 13.11.5会计科目表 109 13.11.6报告期间 110 13.12参考资料 110
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值