Odoo 中的序号(Sequence)系统是一个用于生成唯一标识符的核心机制,主要用于为业务单据(如销售订单、采购订单、发票等)自动分配编号。序号系统由 ir.sequence
模型实现,它提供了一种事务安全(transaction-safe)的方式来生成这些唯一标识符。
序号系统的主要特点:
Odoo 18 的序号系统提供了两种实现方式:
_create_sequence
函数在 PostgreSQL 中创建一个序列ir_sequence_XXX
(XXX 为序号记录的 ID,补零至 3 位)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)
SELECT ... FOR UPDATE NOWAIT
锁定记录number_next
值number_next
为当前值加上增量源码实现:
def _update_nogap(self, number_increment):
self.flush_recordset(['number_next'])
number_next = self.number_next
self._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
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 mode
dt = 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()
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
序号的数字部分可以通过 padding
参数控制格式:
'%%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
规则类型 | 示例 | 说明 |
---|---|---|
简单数字 | 0001, 0002, ... |
仅使用数字,通过 padding 控制前导零 |
前缀固定 | SO0001, SO0002, ... |
使用固定前缀 + 数字 |
后缀固定 | 0001/A, 0002/A, ... |
使用数字 + 固定后缀 |
前后缀 | SO0001/A, SO0002/A, ... |
使用固定前缀 + 数字 + 固定后缀 |
规则类型 | 示例 | 配置 |
---|---|---|
年份前缀 | 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-' |
规则类型 | 示例 | 实现方式 |
---|---|---|
公司代码前缀 | COMP1-0001, COMP1-0002, ... |
在前缀中硬编码公司代码或使用上下文变量 |
部门代码前缀 | HR-0001, FIN-0002, ... |
在前缀中硬编码部门代码或使用上下文变量 |
规则类型 | 示例 | 配置 |
---|---|---|
年份+类型 | SO/2025/0001, PO/2025/0002, ... |
在模型中定义不同序列,前缀包含业务类型和年份 |
公司+年份+类型 | COMP1/SO/2025/0001, ... |
结合公司代码、业务类型和年份的复杂前缀 |
年度重置序号 | 每年从 0001 开始 |
启用 use_date_range=True 并按年创建日期范围 |
规则类型 | 说明 | 实现方式 |
---|---|---|
严格连续序号 | 确保序号无间隙,适用于财务凭证等 | 使用 implementation='no_gap' |
多序列组合 | 在一个编号中组合多个序列的值 | 在代码中调用多个序列并组合结果 |
条件序号 | 根据记录属性选择不同序列 | 在代码中根据条件选择不同的序列代码 |
# 通过代码获取序号
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)
@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)
<record id="seq_my_model" model="ir.sequence">
<field name="name">My Model Sequencefield>
<field name="code">my.modelfield>
<field name="prefix">MY/%(year)s/field>
<field name="padding">4field>
<field name="number_next">1field>
<field name="number_increment">1field>
record>
<record id="seq_my_model_with_range" model="ir.sequence">
<field name="name">My Model Sequence (Yearly)field>
<field name="code">my.model.yearlyfield>
<field name="prefix">MY/%(range_year)s/field>
<field name="padding">4field>
<field name="number_next">1field>
<field name="use_date_range">Truefield>
record>
# 获取当前公司的序号
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()
# 预测下一个序号值但不实际消耗它
seq_id = self.env['ir.sequence'].search([('code', '=', 'my.sequence.code')], limit=1)
if seq_id:
predicted_value = seq_id._get_number_next_actual()
# 使用自定义上下文变量
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号高优先级工单)SUP
TEC
SAL
H
M
L
为三个不同部门创建三个不同的序列:
<record id="seq_customer_ticket_support" model="ir.sequence">
<field name="name">Customer Ticket (Support)field>
<field name="code">customer.ticket.supportfield>
<field name="prefix">SUP/%(range_year)s/field>
<field name="padding">4field>
<field name="number_next">1field>
<field name="number_increment">1field>
<field name="use_date_range">Truefield>
<field name="implementation">standardfield>
<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.technicalfield>
<field name="prefix">TEC/%(range_year)s/field>
<field name="padding">4field>
<field name="number_next">1field>
<field name="number_increment">1field>
<field name="use_date_range">Truefield>
<field name="implementation">standardfield>
<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.salesfield>
<field name="prefix">SAL/%(range_year)s/field>
<field name="padding">4field>
<field name="number_next">1field>
<field name="number_increment">1field>
<field name="use_date_range">Truefield>
<field name="implementation">standardfield>
<field name="company_id" eval="False"/>
record>
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.model
def 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 序列问题:在高并发环境下可能出现序号重复
解决方案:
no_gap
实现,确保序号唯一性name
字段唯一_sql_constraints = [
('name_unique', 'UNIQUE(name)', 'Ticket number must be unique!')
]
问题:业务需求变更,需要修改现有序号格式
解决方案:
# 检查日期判断使用哪种序列格式
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}"
问题:用户希望在创建记录前预览将要分配的编号
解决方案:
_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).year
date_from = f'{year}-01-01'
date_to = f'{year}-12-31'
next_number = 1
else:
next_number = date_range.number_next_actual
else:
next_number = seq.number_next_actual
# 格式化并返回预览
return seq._get_next_char(next_number)
问题:多公司环境下序号混乱或共享
解决方案:
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()
问题:大量序号生成导致性能下降
解决方案:
standard
实现而非 no_gap
Odoo 18 的序号系统提供了一种灵活、强大且事务安全的方式来生成业务单据的唯一标识符。通过合理配置和使用序号系统,可以满足各种业务场景的编号需求,包括:
通过本文的详细解读和案例分析,相信您已经对 Odoo 18 序号系统有了全面的了解,能够根据业务需求灵活配置和使用序号功能。