第16讲、Odoo 18 序号(Sequence)详解

目录

  1. 序号系统概述
  2. 底层实现原理
  3. 常见序号规则
  4. 使用方法与最佳实践
  5. 案例分析:客户工单管理系统
  6. 常见问题与解决方案
  7. 总结

序号系统概述

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_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

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 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()

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=4number_next=1,则格式化为 0001
    • 如果 padding=6number_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'
多序列组合 在一个编号中组合多个序列的值 在代码中调用多个序列并组合结果
条件序号 根据记录属性选择不同序列 在代码中根据条件选择不同的序列代码

第16讲、Odoo 18 序号(Sequence)详解_第1张图片


使用方法与最佳实践

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 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>
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.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>

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')

案例分析:客户工单管理系统

业务需求

创建一个客户工单管理系统,要求:

  1. 工单编号格式:[部门代码]/[年份]/[流水号]-[优先级]
    • 例如:SUP/2025/0001-H(支持部门2025年第1号高优先级工单)
  2. 每年重置流水号
  3. 不同部门使用不同前缀:
    • 支持部门(Support):SUP
    • 技术部门(Technical):TEC
    • 销售部门(Sales):SAL
  4. 工单优先级标识:
    • 高(High):H
    • 中(Medium):M
    • 低(Low):L

实现方案

1. 序列配置

为三个不同部门创建三个不同的序列:


<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>
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.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)

实现分析

  1. 序列配置特点

    • 使用 use_date_range=True 实现按年度重置
    • 使用 %(range_year)s 在前缀中包含年份
    • 设置 padding=4 确保序号至少有 4 位,不足补零
  2. 序号生成流程

    • 根据工单的 department 字段选择对应的序列代码
    • 使用 next_by_code 获取基本序号
    • 根据 priority 字段添加后缀
    • 组合成最终格式:{序列号}-{优先级}
  3. 底层工作原理

    • 模块安装时创建序列记录
    • 对于 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).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)

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 的序号系统提供了一种灵活、强大且事务安全的方式来生成业务单据的唯一标识符。通过合理配置和使用序号系统,可以满足各种业务场景的编号需求,包括:

  1. 多样化的格式:支持前缀、后缀、填充等多种格式化选项
  2. 日期智能:支持基于日期的动态占位符,实现按日期变化的编号
  3. 周期性重置:支持按年度、月度等周期重置序号
  4. 并发安全:在高并发环境下保证序号不重复
  5. 多公司支持:可以为不同公司配置独立序列

通过本文的详细解读和案例分析,相信您已经对 Odoo 18 序号系统有了全面的了解,能够根据业务需求灵活配置和使用序号功能。

你可能感兴趣的:(odoo18系统开发指南,python)