目录
- 序号系统概述
- 底层实现原理
- 常见序号规则
- 使用方法与最佳实践
- 案例分析:客户工单管理系统
- 常见问题与解决方案
- 总结
序号系统概述
Odoo 中的序号(Sequence)系统是一个用于生成唯一标识符的核心机制,主要用于为业务单据(如销售订单、采购订单、发票等)自动分配编号。序号系统由 ir.sequence
模型实现,它提供了一种事务安全(transaction-safe)的方式来生成这些唯一标识符。
序号系统的主要特点:
- 唯一性:确保生成的编号在特定范围内唯一
- 格式灵活:支持前缀、后缀、填充等多种格式化选项
- 日期智能:支持基于日期的动态占位符
- 并发安全:在高并发环境下保证序号不重复
- 按日期范围重置:支持按年度、月度等周期重置序号
底层实现原理
1. 数据库实现机制
Odoo 18 的序号系统提供了两种实现方式:
1.1 标准实现(Standard)
- 底层机制:利用 PostgreSQL 的序列(Sequence)功能
- 实现方式:
- 创建时,通过
_create_sequence
函数在 PostgreSQL 中创建一个序列 - 序列名称格式为
ir_sequence_XXX
(XXX 为序号记录的 ID,补零至 3 位) - 使用 PostgreSQL 的
nextval()
函数获取下一个值
- 创建时,通过
- 特点:
- 性能高效
- 允许序号间有间隙(如删除记录后)
- 并发安全
源码实现:
def _next_do(self):if self.implementation == 'standard':number_next = _select_nextval(self._cr, 'ir_sequence_%03d' % self.id)else:number_next = _update_nogap(self, self.number_increment)return self.get_next_char(number_next)
1.2 无间隙实现(No Gap)
- 底层机制:通过数据库行锁(row-level lock)确保序号连续
- 实现方式:
- 使用
SELECT ... FOR UPDATE NOWAIT
锁定记录 - 读取当前
number_next
值 - 更新
number_next
为当前值加上增量
- 使用
- 特点:
- 确保序号连续,不会有间隙
- 性能相对较低(需要锁定)
- 适用于财务等要求序号严格连续的场景
源码实现:
def _update_nogap(self, number_increment):self.flush_recordset(['number_next'])number_next = self.number_nextself._cr.execute("SELECT number_next FROM %s WHERE id=%%s FOR UPDATE NOWAIT" % self._table, [self.id])self._cr.execute("UPDATE %s SET number_next=number_next+%%s WHERE id=%%s " % self._table, (number_increment, self.id))self.invalidate_recordset(['number_next'])return number_next
2. 日期范围子序列机制
Odoo 18 支持按日期范围使用不同的子序列,通常用于按年度或月度重置序号:
- 实现模型:
ir.sequence.date_range
- 工作原理:
- 当
use_date_range
设置为True
时启用 - 系统会根据当前日期查找匹配的日期范围记录
- 如果找不到匹配的日期范围,会自动创建一个新的(通常按年度)
- 每个日期范围有自己的序号计数器
- 当
源码实现:
def _next(self, sequence_date=None):""" Returns the next number in the preferred sequence in all the ones given in self."""if not self.use_date_range:return self._next_do()# date modedt = sequence_date or self._context.get('ir_sequence_date', fields.Date.today())seq_date = self.env['ir.sequence.date_range'].search([('sequence_id', '=', self.id), ('date_from', '<=', dt), ('date_to', '>=', dt)], limit=1)if not seq_date:seq_date = self._create_date_range_seq(dt)return seq_date.with_context(ir_sequence_date_range=seq_date.date_from)._next()
3. 前缀和后缀插值机制
Odoo 序号系统支持在前缀和后缀中使用动态占位符,通过字符串插值实现:
- 插值字典:包含日期相关的各种格式化值
- 支持的占位符:
%(year)s
- 年份(4位)%(month)s
- 月份(2位)%(day)s
- 日期(2位)%(y)s
- 年份(2位)%(doy)s
- 一年中的第几天%(woy)s
- 一年中的第几周%(weekday)s
- 星期几%(h24)s
- 小时(24小时制)%(h12)s
- 小时(12小时制)%(min)s
- 分钟%(sec)s
- 秒
源码实现:
def _interpolation_dict(self):now = range_date = effective_date = datetime.now(pytz.timezone(self._context.get('tz') or 'UTC'))if date or self._context.get('ir_sequence_date'):effective_date = fields.Datetime.from_string(date or self._context.get('ir_sequence_date'))if date_range or self._context.get('ir_sequence_date_range'):range_date = fields.Datetime.from_string(date_range or self._context.get('ir_sequence_date_range'))sequences = {'year': '%Y', 'month': '%m', 'day': '%d', 'y': '%y', 'doy': '%j', 'woy': '%W','weekday': '%w', 'h24': '%H', 'h12': '%I', 'min': '%M', 'sec': '%S'}res = {}for key, format in sequences.items():res[key] = effective_date.strftime(format)res['range_' + key] = range_date.strftime(format)res['current_' + key] = now.strftime(format)return res
4. 数字格式化机制
序号的数字部分可以通过 padding
参数控制格式:
- 实现方式:使用 Python 的字符串格式化
- 格式化规则:
'%%0%sd' % self.padding % number_next
- 示例:
- 如果
padding=4
且number_next=1
,则格式化为0001
- 如果
padding=6
且number_next=42
,则格式化为000042
- 如果
源码实现:
def get_next_char(self, number_next):interpolated_prefix, interpolated_suffix = self._get_prefix_suffix()return interpolated_prefix + '%%0%sd' % self.padding % number_next + interpolated_suffix
常见序号规则
1. 基本序号规则
规则类型 | 示例 | 说明 |
---|---|---|
简单数字 | 0001, 0002, ... | 仅使用数字,通过 padding 控制前导零 |
前缀固定 | SO0001, SO0002, ... | 使用固定前缀 + 数字 |
后缀固定 | 0001/A, 0002/A, ... | 使用数字 + 固定后缀 |
前后缀 | SO0001/A, SO0002/A, ... | 使用固定前缀 + 数字 + 固定后缀 |
2. 日期相关序号规则
规则类型 | 示例 | 配置 |
---|---|---|
年份前缀 | 2025/0001, 2025/0002, ... | prefix='%(year)s/' |
年月前缀 | 2025-01/0001, 2025-01/0002, ... | prefix='%(year)s-%(month)s/' |
年份后缀 | 0001/2025, 0002/2025, ... | suffix='/%(year)s' |
年月日完整 | 20250101-0001, 20250101-0002, ... | prefix='%(year)s%(month)s%(day)s-' |
3. 公司或部门相关序号规则
规则类型 | 示例 | 实现方式 |
---|---|---|
公司代码前缀 | COMP1-0001, COMP1-0002, ... | 在前缀中硬编码公司代码或使用上下文变量 |
部门代码前缀 | HR-0001, FIN-0002, ... | 在前缀中硬编码部门代码或使用上下文变量 |
4. 混合序号规则
规则类型 | 示例 | 配置 |
---|---|---|
年份+类型 | SO/2025/0001, PO/2025/0002, ... | 在模型中定义不同序列,前缀包含业务类型和年份 |
公司+年份+类型 | COMP1/SO/2025/0001, ... | 结合公司代码、业务类型和年份的复杂前缀 |
年度重置序号 | 每年从 0001 开始 | 启用 use_date_range=True 并按年创建日期范围 |
5. 特殊序号规则
规则类型 | 说明 | 实现方式 |
---|---|---|
严格连续序号 | 确保序号无间隙,适用于财务凭证等 | 使用 implementation='no_gap' |
多序列组合 | 在一个编号中组合多个序列的值 | 在代码中调用多个序列并组合结果 |
条件序号 | 根据记录属性选择不同序列 | 在代码中根据条件选择不同的序列代码 |
使用方法与最佳实践
1. 通过代码调用序号
1.1 基本调用方式
# 通过代码获取序号
next_number = self.env['ir.sequence'].next_by_code('my.sequence.code')# 带日期参数的调用(用于指定日期的序号)
specific_date = fields.Date.from_string('2025-01-15')
next_number = self.env['ir.sequence'].next_by_code('my.sequence.code', sequence_date=specific_date)
1.2 在模型创建时自动分配序号
@api.model
def create(self, vals):if not vals.get('name') or vals['name'] == '/':vals['name'] = self.env['ir.sequence'].next_by_code('my.model') or '/'return super(MyModel, self).create(vals)
2. 通过XML配置序号
2.1 基本序号配置
<record id="seq_my_model" model="ir.sequence"><field name="name">My Model Sequence</field><field name="code">my.model</field><field name="prefix">MY/%(year)s/</field><field name="padding">4</field><field name="number_next">1</field><field name="number_increment">1</field>
</record>
2.2 带日期范围的序号配置
<record id="seq_my_model_with_range" model="ir.sequence"><field name="name">My Model Sequence (Yearly)</field><field name="code">my.model.yearly</field><field name="prefix">MY/%(range_year)s/</field><field name="padding">4</field><field name="number_next">1</field><field name="use_date_range">True</field>
</record>
3. 序号的高级用法
3.1 公司特定序号
# 获取当前公司的序号
company_id = self.env.company.id
seq_ids = self.env['ir.sequence'].search([('code', '=', 'my.sequence.code'),('company_id', 'in', [company_id, False])
], order='company_id')
if seq_ids:next_number = seq_ids[0].next_by_id()
3.2 序号预测(不消耗)
# 预测下一个序号值但不实际消耗它
seq_id = self.env['ir.sequence'].search([('code', '=', 'my.sequence.code')], limit=1)
if seq_id:predicted_value = seq_id._get_number_next_actual()
3.3 自定义上下文变量
# 使用自定义上下文变量
custom_context = {'ir_sequence_date': '2025-06-01', # 指定日期
}
next_number = self.env['ir.sequence'].with_context(custom_context).next_by_code('my.sequence.code')
案例分析:客户工单管理系统
业务需求
创建一个客户工单管理系统,要求:
- 工单编号格式:
[部门代码]/[年份]/[流水号]-[优先级]
- 例如:
SUP/2025/0001-H
(支持部门2025年第1号高优先级工单)
- 例如:
- 每年重置流水号
- 不同部门使用不同前缀:
- 支持部门(Support):
SUP
- 技术部门(Technical):
TEC
- 销售部门(Sales):
SAL
- 支持部门(Support):
- 工单优先级标识:
- 高(High):
H
- 中(Medium):
M
- 低(Low):
L
- 高(High):
实现方案
1. 序列配置
为三个不同部门创建三个不同的序列:
<!-- 支持部门工单序列 -->
<record id="seq_customer_ticket_support" model="ir.sequence"><field name="name">Customer Ticket (Support)</field><field name="code">customer.ticket.support</field><field name="prefix">SUP/%(range_year)s/</field><field name="padding">4</field><field name="number_next">1</field><field name="number_increment">1</field><field name="use_date_range">True</field><field name="implementation">standard</field><field name="company_id" eval="False"/>
</record><!-- 技术部门工单序列 -->
<record id="seq_customer_ticket_technical" model="ir.sequence"><field name="name">Customer Ticket (Technical)</field><field name="code">customer.ticket.technical</field><field name="prefix">TEC/%(range_year)s/</field><field name="padding">4</field><field name="number_next">1</field><field name="number_increment">1</field><field name="use_date_range">True</field><field name="implementation">standard</field><field name="company_id" eval="False"/>
</record><!-- 销售部门工单序列 -->
<record id="seq_customer_ticket_sales" model="ir.sequence"><field name="name">Customer Ticket (Sales)</field><field name="code">customer.ticket.sales</field><field name="prefix">SAL/%(range_year)s/</field><field name="padding">4</field><field name="number_next">1</field><field name="number_increment">1</field><field name="use_date_range">True</field><field name="implementation">standard</field><field name="company_id" eval="False"/>
</record>
2. 模型实现
class CustomerTicket(models.Model):_name = 'customer.ticket'_description = 'Customer Support Ticket'name = fields.Char(string='Ticket Number', required=True, copy=False, readonly=True, default='/')partner_id = fields.Many2one('res.partner', string='Customer', required=True)department = fields.Selection([('support', 'Support'),('technical', 'Technical'),('sales', 'Sales')], string='Department', required=True)priority = fields.Selection([('low', 'Low'),('medium', 'Medium'),('high', 'High')], string='Priority', default='medium', required=True)description = fields.Text(string='Description')date_created = fields.Date(string='Creation Date', default=fields.Date.today)state = fields.Selection([('draft', 'Draft'),('open', 'Open'),('in_progress', 'In Progress'),('done', 'Done'),('cancelled', 'Cancelled')], string='Status', default='draft')@api.modeldef create(self, vals):"""重写创建方法,自动分配工单编号"""if vals.get('name', '/') == '/':# 根据部门选择不同的序列代码department = vals.get('department')priority = vals.get('priority', 'medium')# 获取部门对应的序列代码seq_code = 'customer.ticket.support' # 默认支持部门if department == 'technical':seq_code = 'customer.ticket.technical'elif department == 'sales':seq_code = 'customer.ticket.sales'# 获取序列号ticket_number = self.env['ir.sequence'].next_by_code(seq_code)# 添加优先级后缀priority_suffix = 'M' # 默认中优先级if priority == 'high':priority_suffix = 'H'elif priority == 'low':priority_suffix = 'L'# 组合完整工单编号vals['name'] = f"{ticket_number}-{priority_suffix}"return super(CustomerTicket, self).create(vals)
实现分析
-
序列配置特点:
- 使用
use_date_range=True
实现按年度重置 - 使用
%(range_year)s
在前缀中包含年份 - 设置
padding=4
确保序号至少有 4 位,不足补零
- 使用
-
序号生成流程:
- 根据工单的
department
字段选择对应的序列代码 - 使用
next_by_code
获取基本序号 - 根据
priority
字段添加后缀 - 组合成最终格式:
{序列号}-{优先级}
- 根据工单的
-
底层工作原理:
- 模块安装时创建序列记录
- 对于
standard
实现,创建 PostgreSQL 序列 - 首次使用时自动创建当年的日期范围记录
- 每年自动创建新的日期范围记录,实现按年重置
常见问题与解决方案
1. 序号重复问题
问题:在高并发环境下可能出现序号重复
解决方案:
- 使用
no_gap
实现,确保序号唯一性 - 添加数据库约束,确保
name
字段唯一
_sql_constraints = [('name_unique', 'UNIQUE(name)', 'Ticket number must be unique!')
]
2. 序号格式变更
问题:业务需求变更,需要修改现有序号格式
解决方案:
- 创建新的序列记录,不要修改现有序列
- 在代码中添加版本判断逻辑,处理新旧格式
# 检查日期判断使用哪种序列格式
today = fields.Date.today()
cutoff_date = fields.Date.from_string('2025-01-01')
if today >= cutoff_date:# 使用新格式序列seq_code = f"customer.ticket.{department}.new"
else:# 使用旧格式序列seq_code = f"customer.ticket.{department}"
3. 序号预览
问题:用户希望在创建记录前预览将要分配的编号
解决方案:
- 添加预览功能,不消耗实际序号
- 使用
_get_number_next_actual
方法获取当前值但不递增
@api.model
def preview_next_number(self, department):seq_code = f"customer.ticket.{department}"seq = self.env['ir.sequence'].search([('code', '=', seq_code)], limit=1)if not seq:return False# 获取当前值但不递增current_date = fields.Date.today()if seq.use_date_range:date_range = seq.date_range_ids.filtered(lambda r: r.date_from <= current_date <= r.date_to)if not date_range:# 模拟创建日期范围的行为year = fields.Date.from_string(current_date).yeardate_from = f'{year}-01-01'date_to = f'{year}-12-31'next_number = 1else:next_number = date_range.number_next_actualelse:next_number = seq.number_next_actual# 格式化并返回预览return seq._get_next_char(next_number)
4. 多公司环境问题
问题:多公司环境下序号混乱或共享
解决方案:
- 为每个公司创建独立序列
- 在代码中根据当前公司选择序列
company_id = self.env.company.id
seq_ids = self.env['ir.sequence'].search([('code', '=', seq_code),('company_id', 'in', [company_id, False])
], order='company_id')
if seq_ids:next_number = seq_ids[0].next_by_id()
5. 性能问题
问题:大量序号生成导致性能下降
解决方案:
- 优先使用
standard
实现而非no_gap
- 避免频繁调用序号生成
- 考虑批量预生成序号
总结
Odoo 18 的序号系统提供了一种灵活、强大且事务安全的方式来生成业务单据的唯一标识符。通过合理配置和使用序号系统,可以满足各种业务场景的编号需求,包括:
- 多样化的格式:支持前缀、后缀、填充等多种格式化选项
- 日期智能:支持基于日期的动态占位符,实现按日期变化的编号
- 周期性重置:支持按年度、月度等周期重置序号
- 并发安全:在高并发环境下保证序号不重复
- 多公司支持:可以为不同公司配置独立序列
通过本文的详细解读和案例分析,相信您已经对 Odoo 18 序号系统有了全面的了解,能够根据业务需求灵活配置和使用序号功能。