<think>我们正在将之前为Odoo 8/9/10等旧版本编写的代码迁移到Odoo 16。Odoo 16使用了Python 3.8+,并引入了许多API变化。以下是针对之前添加的`multi.level.commission`模型代码的修改点,以使其兼容Odoo 16:
### 1. 模型文件修改 (`models/commission_level.py`)
#### 修改点:
- **移除`# -*- coding: utf-8 -*-`**:Python 3默认使用UTF-8,不需要特别声明。
- **字段字符串参数**:在Odoo 16中,字段的字符串参数(即显示名称)应该放在第一个位置,并明确使用`string`关键字参数(虽然旧版也可以,但新版更规范)。
- **`api.one`和`api.multi`的替代**:使用`api.model`和`api.depends`等替代。
- **`_description`类属性**:在Odoo 16中,每个模型必须定义`_description`。
- **`default`参数**:在`fields.Many2one`中,`default`参数现在更推荐使用可调用对象(如lambda函数),但旧写法仍然兼容。
- **`_constraints`和`_sql_constraints`**:仍然有效,但约束方法可以使用`@api.constrains`装饰器。
- **浮点数比较**:避免直接比较浮点数,使用工具函数(如`float_compare`)来比较。
#### 修改后的代码:
```python
from odoo import models, fields, api, exceptions, tools
class MultiLevelCommission(models.Model):
_name = 'multi.level.commission'
_description = 'Multi-level Sales Commission Rules'
_order = 'sequence, id'
name = fields.Char(string='规则名称', required=True)
sequence = fields.Integer(string='优先级', default=10,
help="数字越小优先级越高")
active = fields.Boolean(string='激活', default=True)
company_id = fields.Many2one(
'res.company', string='公司',
default=lambda self: self.env.company)
# 佣金层级定义
level_lines = fields.One2many(
'commission.level.line',
'commission_id',
string='佣金层级')
# 适用条件
apply_on = fields.Selection([
('global', '全局规则'),
('team', '销售团队'),
('salesman', '销售人员'),
('category', '产品类别')],
string='适用对象', default='global', required=True)
team_id = fields.Many2one('crm.team', string='销售团队')
salesman_id = fields.Many2one('res.users', string='销售人员')
category_id = fields.Many2one('product.category', string='产品类别')
# 生效日期范围
date_from = fields.Date(string='生效日期')
date_to = fields.Date(string='截止日期')
# 约束条件
_sql_constraints = [
('date_range_check',
"CHECK (date_from <= date_to OR date_to IS NULL)",
"截止日期必须晚于生效日期"),
]
@api.constrains('level_lines')
def _check_level_ranges(self):
for record in self:
ranges = []
for line in record.level_lines:
# 检查范围重叠
for r in ranges:
if max(line.min_value, r['min']) <= min(line.max_value, r['max']):
raise exceptions.ValidationError(
"层级范围重叠: %s 与 %s" % (line.name, r['name']))
ranges.append({
'min': line.min_value,
'max': line.max_value,
'name': line.name
})
class CommissionLevelLine(models.Model):
_name = 'commission.level.line'
_description = 'Commission Level Line'
_order = 'min_value asc'
name = fields.Char(string='层级名称', compute='_compute_name', store=True)
commission_id = fields.Many2one(
'multi.level.commission',
string='佣金规则',
required=True,
ondelete='cascade')
# 层级条件
min_value = fields.Float(string='最小值', required=True)
max_value = fields.Float(string='最大值', required=True)
commission_percent = fields.Float(string='佣金比例(%)', required=True)
# 自动计算层级名称
@api.depends('min_value', 'max_value')
def _compute_name(self):
for line in self:
if line.max_value == float('inf'):
line.name = f"{line.min_value}+"
else:
line.name = f"{line.min_value}-{line.max_value}"
```
### 2. 视图文件修改 (`views/commission_level_views.xml`)
#### 修改点:
- **视图结构**:Odoo 16的视图结构基本兼容旧版,但需要注意:
- 按钮的`type`属性:旧版中`type="object"`仍然有效,但推荐使用`type="action"`或`type="method"`(根据具体用途)。
- 归档按钮:Odoo 16中可以使用`<field name="active"/>`字段和内置的归档/取消归档操作,不需要自定义按钮。
- 使用`<field>`的`options`属性来传递一些参数。
#### 修改后的视图(简化并更新):
```xml
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- 佣金规则表单视图 -->
<record id="view_multi_level_commission_form" model="ir.ui.view">
<field name="name">multi.level.commission.form</field>
<field name="model">multi.level.commission</field>
<field name="arch" type="xml">
<form>
<header>
<button name="action_archive" type="object" string="归档" class="oe_stat_button" attrs="{'invisible': [('active','=',False)]}"/>
<button name="action_unarchive" type="object" string="取消归档" class="oe_stat_button" attrs="{'invisible': [('active','=',True)]}"/>
</header>
<sheet>
<group>
<field name="name"/>
<field name="sequence"/>
<field name="apply_on"/>
<field name="team_id" attrs="{'invisible': [('apply_on','!=','team')]}"/>
<field name="salesman_id" attrs="{'invisible': [('apply_on','!=','salesman')]}"/>
<field name="category_id" attrs="{'invisible': [('apply_on','!=','category')]}"/>
<field name="date_from"/>
<field name="date_to"/>
</group>
<field name="level_lines">
<tree editable="bottom">
<field name="name" readonly="1"/>
<field name="min_value"/>
<field name="max_value"/>
<field name="commission_percent"/>
</tree>
</field>
</sheet>
</form>
</field>
</record>
<!-- 佣金规则树形视图 -->
<record id="view_multi_level_commission_tree" model="ir.ui.view">
<field name="name">multi.level.commission.tree</field>
<field name="model">multi.level.commission</field>
<field name="arch" type="xml">
<tree>
<field name="name"/>
<field name="apply_on"/>
<field name="team_id"/>
<field name="salesman_id"/>
<field name="active"/>
</tree>
</field>
</record>
<!-- 佣金规则菜单和动作 -->
<menuitem id="menu_commission_level_root" name="佣金规则"
parent="sale_commission.menu_commission_root"/>
<menuitem id="menu_commission_level" name="多级佣金规则"
parent="menu_commission_level_root"
action="action_multi_level_commission"/>
<record id="action_multi_level_commission" model="ir.actions.act_window">
<field name="name">多级佣金规则</field>
<field name="res_model">multi.level.commission</field>
<field name="view_mode">tree,form</field>
</record>
</odoo>
```
### 3. 修改佣金计算逻辑(`sale_commission_salesman`的`sale_order.py`)
#### 修改点:
- **`@api.depends`**:在Odoo 16中,计算字段的依赖关系仍然使用`@api.depends`。
- **搜索方法**:`search`方法的使用方式相同,但注意Odoo 16中的记录集操作更严格。
- **浮点数比较**:使用`tools.float_compare`来避免浮点数精度问题。
#### 修改后的计算逻辑:
```python
from odoo import models, fields, api, tools
class SaleOrderLine(models.Model):
_inherit = 'sale.order.line'
@api.depends('price_subtotal', 'product_id', 'order_id.user_id')
def _compute_commission(self):
"""重写佣金计算方法以支持多层规则"""
super(SaleOrderLine, self)._compute_commission()
for line in self.filtered(lambda l: l.price_subtotal > 0):
# 获取适用的佣金规则
rules = self.env['multi.level.commission'].search([
'|', ('date_from', '<=', line.order_id.date_order.date()),
('date_from', '=', False),
'|', ('date_to', '>=', line.order_id.date_order.date()),
('date_to', '=', False),
'|', ('apply_on', '=', 'global'),
'&', ('apply_on', '=', 'team'),
('team_id', '=', line.order_id.team_id.id),
'&', ('apply_on', '=', 'salesman'),
('salesman_id', '=', line.order_id.user_id.id),
'&', ('apply_on', '=', 'category'),
('category_id', '=', line.product_id.categ_id.id),
], order='sequence')
# 应用匹配的规则
commission_val = 0.0
for rule in rules:
for level in rule.level_lines:
# 使用浮点数比较工具
if tools.float_compare(line.price_subtotal, level.min_value, precision_digits=2) >= 0 and \
(tools.float_compare(line.price_subtotal, level.max_value, precision_digits=2) <= 0 or level.max_value == float('inf')):
commission_val = line.price_subtotal * level.commission_percent / 100
break
if commission_val:
break
line.commission = commission_val
```
### 4. 访问权限文件 (`security/ir.model.access.csv`)
在Odoo 16中,权限文件的格式没有变化,但注意模型标识(`model_id/id`)的格式应为`model_<model_name>`,其中`<model_name>`是模型名称(点替换为下划线)。例如,`multi.level.commission`模型对应的标识为`model_multi_level_commission`。
### 5. 其他注意事项
- **Python 3语法**:确保使用Python 3的语法(如`print`函数,除法运算符等)。
- **导入路径**:确保在`__init__.py`中正确导入模型文件。
- **菜单结构**:Odoo 16的菜单结构没有重大变化,但注意父菜单的XML ID是否存在。
- **多公司支持**:在计算佣金规则时,考虑当前记录的公司(如果规则中定义了公司)。
### 测试建议
1. 在Odoo 16环境中安装模块,检查是否有错误。
2. 创建佣金规则并测试计算逻辑。
3. 测试不同适用条件(全局、团队、销售人员、产品类别)的规则。
4. 测试日期范围限制和优先级。
通过以上修改,代码应该能够在Odoo 16中正常运行。如果遇到问题,请检查Odoo日志以获取详细错误信息[^1]。
§§相关问题§§
1. 在Odoo 16中如何调试自定义模块的计算字段?
2. 如何为佣金规则添加多公司支持?
3. 如何优化佣金规则搜索以提高性能?
4. 佣金计算如何考虑折扣和税费?</think>### 修改代码以兼容 Odoo 16 的关键调整
以下是针对 Odoo 16 的核心修改点,主要涉及 API 变更、字段定义和计算逻辑优化:
#### 1. 模型文件修改 (`models/commission_level.py`)
```python
from odoo import models, fields, api, exceptions
class MultiLevelCommission(models.Model):
_name = 'multi.level.commission'
_description = '多层级销售佣金规则' # Odoo 16 强制要求_description
_order = 'sequence, id'
# 字段定义使用 string 参数在前
name = fields.Char(string='规则名称', required=True)
sequence = fields.Integer(string='优先级', default=10,
help="数字越小优先级越高")
active = fields.Boolean(string='激活', default=True)
company_id = fields.Many2one(
'res.company', string='公司',
default=lambda self: self.env.company) # 使用 env.company
# 适用条件选择字段
apply_on = fields.Selection(
selection=[ # Odoo 16 要求 selection 参数名
('global', '全局规则'),
('team', '销售团队'),
('salesman', '销售人员'),
('category', '产品类别')
],
string='适用对象', default='global', required=True
)
# 约束条件更新
@api.constrains('level_lines')
def _check_level_ranges(self):
for record in self:
ranges = []
for line in record.level_lines:
for r in ranges:
if max(line.min_value, r['min']) <= min(line.max_value, r['max']):
raise exceptions.ValidationError(
"层级范围重叠: %s 与 %s" % (line.name, r['name'])) # 移除 f-string
ranges.append({
'min': line.min_value,
'max': line.max_value,
'name': line.name
})
class CommissionLevelLine(models.Model):
_name = 'commission.level.line'
_description = '佣金层级明细' # 添加描述
_order = 'min_value asc'
# 计算字段定义
name = fields.Char(string='层级名称', compute='_compute_name', store=True)
@api.depends('min_value', 'max_value')
def _compute_name(self):
for line in self:
if line.max_value == float('inf'):
line.name = "%s+" % line.min_value # 兼容 Python 2/3 的格式化
else:
line.name = "%s-%s" % (line.min_value, line.max_value)
```
#### 2. 佣金计算逻辑修改 (`sale_commission_salesman/models/sale_order.py`)
```python
from odoo import models, api, tools
class SaleOrderLine(models.Model):
_inherit = 'sale.order.line'
@api.depends('price_subtotal', 'product_id', 'order_id.user_id')
def _compute_commission(self):
"""重写佣金计算方法"""
super()._compute_commission() # Odoo 16 推荐 super() 语法
for line in self.filtered(lambda l: l.price_subtotal > 0):
# 使用安全的日期比较
order_date = fields.Date.to_date(line.order_id.date_order)
# 构建域条件
domain = [
'|', ('date_from', '<=', order_date), ('date_from', '=', False),
'|', ('date_to', '>=', order_date), ('date_to', '=', False),
'|', ('apply_on', '=', 'global'),
'|',
'&', ('apply_on', '=', 'team'), ('team_id', '=', line.order_id.team_id.id),
'|',
'&', ('apply_on', '=', 'salesman'), ('salesman_id', '=', line.order_id.user_id.id),
'&', ('apply_on', '=', 'category'), ('category_id', '=', line.product_id.categ_id.id)
]
# 搜索并排序
rules = self.env['multi.level.commission'].search(domain, order='sequence')
# 使用 float_compare 处理浮点数精度
for rule in rules:
for level in rule.level_lines:
subtotal = line.price_subtotal
if (
tools.float_compare(subtotal, level.min_value, precision_digits=2) >= 0 and
(tools.float_compare(subtotal, level.max_value, precision_digits=2) <= 0 or level.max_value == float('inf'))
):
line.commission = subtotal * level.commission_percent / 100
break
if line.commission:
break
```
#### 3. 视图文件修改 (`views/commission_level_views.xml`)
```xml
<!-- 更新按钮定义 -->
<header>
<button name="toggle_active" type="object"
string="归档"
class="oe_stat_button"
icon="fa-archive"
attrs="{'invisible': [('active','=', False)]}"/>
<button name="toggle_active" type="object"
string="取消归档"
class="oe_stat_button"
icon="fa-undo"
attrs="{'invisible': [('active','=', True)]}"/>
</header>
<!-- 添加安全组控制 -->
<record id="group_commission_manager" model="res.groups">
<field name="name">佣金规则管理员</field>
<field name="category_id" ref="base.module_category_sales"/>
</record>
```
#### 4. 权限文件修改 (`security/ir.model.access.csv`)
```csv
id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink
access_multi_level_commission,multi.level.commission,model_multi_level_commission,group_commission_manager,1,1,1,1
access_commission_level_line,commission.level.line,model_commission_level_line,group_commission_manager,1,1,1,1
```
### Odoo 16 兼容性关键修改点
1. **字段定义规范**:
- 所有字段必须明确 `string` 参数
- 选择字段使用 `selection=` 参数
- 强制要求 `_description` 属性
2. **API 变更**:
- 使用 `super().method()` 代替 `super(Class, self).method()`
- 计算字段推荐使用 `@api.depends` 而非 `depends`
- 移除 `api.one` 装饰器,统一使用 `api.model`
3. **视图系统更新**:
- 按钮操作使用 `toggle_active` 代替自定义归档方法
- 图标使用 Font Awesome 5 (例如 `fa-archive`)
- 安全组需明确指定分类
4. **浮点数处理**:
- 使用 `tools.float_compare()` 处理精度问题
- 避免直接比较浮点相等性
5. **日期处理**:
- 使用 `fields.Date.to_date()` 转换日期时间
- 时区敏感操作使用 `fields.Datetime` 上下文
6. **权限系统**:
- 推荐使用安全组而非全局权限
- 权限组需关联应用分类
### 测试建议(Odoo 16 特定)
1. **多公司测试**:
```python
# 确保公司字段正确处理
rule = self.env['multi.level.commission'].create({
'name': '测试规则',
'company_id': self.env.company.id # 显式设置当前公司
})
```
2. **时区测试**:
```python
# 测试带时区的日期比较
with self.env.cr.savepoint():
self.env.user.tz = 'Asia/Shanghai'
# 创建跨时区订单测试
```
3. **权限测试**:
```python
# 验证安全组控制
with self.assertRaises(AccessError):
self.env['multi.level.commission'].with_user(test_user).create({...})
```
这些修改确保您的模块符合 Odoo 16 的 ORM 规范、视图系统和安全模型,同时保持原有业务逻辑不变[^1]。