
做 Odoo 实施这些年隔三差五就有人来问Odoo 19 里怎么创建序列号说实话这个问题本身就藏着坑——因为序列号在 Odoo 里有两套完全不同的东西。一套是单据自动编号用的序列ir.sequence解决采购单号、销售单号、工单号这种“今天要一个、明天还要一个”的连续编号需求另一套是产品层面的序列号追踪解决“这批货里每一台设备是谁生产的、卖给了谁”这种追溯需求。两套东西都叫序列号配置入口和使用逻辑完全不一样混着看必晕。这篇文章就把两条路线分别讲透从后台界面到底层代码再到库存流转里的实际操作和常见坑一次给全。如果你是第一次碰 Odoo 19 的编号需求建议先花十分钟把下面第一部分看完搞清楚自己到底要的是哪一种再按对应章节操作。如果已经有一定经验可以直接跳到第二或第三部分代码方式和界面方式各取所需。文章最后一部分是高频问题的排查记录都是我实际踩过或者帮人排查过的场景建议收藏备用。1. 先分清你要的是单据编号还是产品追踪1.1 单据编号序列解决什么需求单据编号序列在 Odoo 里的模型叫ir.sequence说白了就是一个“带格式的计数器”。你给它定义一个前缀、一个位数它每被调用一次就吐出一个新的编号比如WO202501-0001、SO-2025-0100这种。Odoo 内置的几乎所有单据都依赖它销售订单号、采购订单号、发票号、付款单号、内部调拨单号甚至自定义的工单、报修单、审批单只要你想让系统自动生成一个不重复的编号用的都是这套机制。每个单据模型在创建记录时会调用next_by_code()方法传入一个“代码”序列表就会返回当前号并往后推进一位。这里有个关键概念要理解序列号一旦被取走就不会再被放回去。哪怕单据创建后被删掉了、事务回滚了、客户取消订单了这个号也已经消耗掉了。所以 Odoo 里的单据编号天然是“可能不连续”的这不是 bug是设计如此。1.2 产品序列号追踪解决什么需求另一套“序列号”指的是产品维度上的唯一标识。你可以给一个产品开启“按序列号追踪”Tracking by Serial Number然后每一件入库的货物都会有一个独一无二的序列号比如一台设备、一台仪器、一辆整车各是各的号。这套机制解决的是追溯问题这批零件是哪一批原料做的这台设备卖给哪个客户了客户报修时输入序列号能不能查到生产记录、发货记录、之前的维修记录序列号追踪就是为了回答这些问题。在 Odoo 里它跟“批次”Lot是同一套功能体系下的两种粒度批次是一批货共用一个号序列号是一台货一个号。需求典型场景是设备制造、电子元器件、医疗器械、售后维修这类行业。如果你的业务只是给单据换个编号规则完全用不到产品序列号反过来如果你想做质量追溯只在后台建一个序列号是没用的必须把产品追踪方式配好。1.3 两张对照表快速定位你的需求对比维度单据编号序列产品序列号核心模型ir.sequencestock.lot序列号是批次的一种配置入口设置 → 技术 → 序列产品档案 → 库存页签 → 追踪方式作用对象单据记录订单、工单、发票库存里的实际货物典型编号SO202501-0001SN-2025-00001生成时机单据创建时自动取号入库、生产完成时生成/录入能否复用不能取走就消耗一物一号唯一对应这两套体系可以同时存在互不干扰。比如你的销售订单号用序列 A 生成订单里卖出去的设备又有自己的产品序列号两者是完全独立的。很多新手把“订单编号前缀”和“产品序列号”混在一个需求里提出来其实要先拆分清楚后面才不会乱。2. 界面操作不写代码也能创建序列2.1 第一步先开开发者模式Odoo 19 的序列配置菜单默认是隐藏的必须先开启开发者模式。操作路径是设置 → 常规设置 → 页面拉到最底部找到“关于”区块点击“激活开发者模式”。还有一种更快的办法直接在浏览器地址栏里给当前页面 URL 加上?debug1回车后顶部会出现一个 bug 图标说明开发者模式已经开启。说实话我平时更常用这个方式因为不用点那么多层菜单。注意 Odoo 19 的开发者模式开启后会存在会话里不需要每次打开都重新切。为什么要开发者模式因为“技术”菜单里全是底层配置序列、邮箱服务器、定时任务、动作、视图都在里面。日常用户界面不需要看到这些但对做配置和实施的人来说这个菜单基本天天都要碰。2.2 找到序列配置页开启开发者模式后依次进入设置 → 技术 → 序列 → 序列。这里就是所有序列记录的列表页。页面上每条记录对应一个计数器字段包括名称、代码、公司、前缀、后缀、填充长度、下一编号等。你可以在这里直接新建也可以找到已有的内置序列进行修改。注意一个细节Odoo 19 里“序列”菜单下面通常还有“序列日期范围”ir.sequence.date_range子菜单那是用来做按年/按月重置计数的后面第三部分会讲到。2.3 核心字段逐个拆解每个都有讲究新建一条序列时要填的字段不多但每个字段背后都有关联逻辑我一个个说清楚。名称就是序列的显示名比如“销售订单编号”“客户工单号”。没有实际逻辑作用但建议起得有意义因为在列表页和日志里都要靠它认人。代码这个最关键。代码是程序调用时的钥匙ir.sequence.next_by_code(这个值)就是靠它找到对应的序列记录。内置单据的代码是写死在模块里的比如销售订单的代码是sale.order采购订单是purchase.order。自定义序列时代码要全局唯一最好用“模块点.用途”的命名风格比如helpdesk.ticket。公司如果不选代表所有公司共用一套号如果选了特定公司就只有这家公司能用。多公司环境下这个字段最容易出事后面排障章节会专门讲。前缀/后缀编号前后的固定文本。前缀里可以插入日期占位符Odoo 19 支持%(year)s四位年份、%(y)s两位年份、%(month)s两位月份、%(day)s两位日期。比如前缀写成WO%(year)s%(month)s-生成的号就是WO202501-0001这种感觉。填充长度流水号部分补零的位数。填 41 就变成 0001填 61 就变成 000001。位数不是越多越好得想想这个系统大概要跑多少年、每年多少单据量。比如一天几十单的量4 位能跑几十年都不重6 位纯粹浪费阅读体验。下一编号下一个会生成的流水号是多少。默认从 1 开始但很多时候客户有历史单据要求接着现有编号继续往下排这时就把这个值改成历史最大号加 1。步进每次递增多少默认 1。正常业务不需要改只有一些特殊需求比如每隔 5 个号取一个才会动它。编号格式这个字段是自动预览的把你填的前缀、填充长度、后缀拼在一起展示比如WO%(year)s%(month)s-####的预览效果。填完前缀后缀瞄一眼这里格式对不对一目了然。2.4 完整走一遍新建一个客户工单序列我来带你把一个真实场景完整走一遍。假设某客户要一个“售后工单编号”规则是前缀HD年月流水 4 位从 1 开始。在序列列表页点击“新建”依次填写名称售后工单编号代码helpdesk.ticket前缀HD%(year)s%(month)s-填充长度4下一编号1步进1保存后列表页的编号格式一列会显示类似HD202501-0001的预览。这时候你在 Python 终端或者代码里调用一次next_by_code(helpdesk.ticket)就能拿到第一个号HD202501-0001。下一次调用就是HD202501-0002一直往后走。这里要特别提醒前缀如果带了年份月份那编号自然会长成HD202501-0001、HD202502-0002这种。但因为序列只有一个全局计数器即使跨了月份流水号依然连续递增不会因为换月就回到 0001。想让流水号在新的一年或新的一个月重新从 1 开始需要用日期区间功能也就是第三部分会说的date_range。2.5 让业务单据用上这个新序列序列建好了还得让某个单据模型真正去调它。分两种情况如果是 Odoo 内置单据比如销售订单、采购订单最直接的做法是去“技术 → 序列”里找到对应的序列记录代码是sale.order、purchase.order这种直接修改它的前缀、填充长度、下一编号。改完保存之后新创建的单据就会按新规则编号。已经存在的单据不会变因为它们创建时已经把号取走了。如果是自定义模型那必须在模型的name字段上设置默认值让它创建时自动取号。这就是下一部分要讲的代码方式。界面上建好的序列只是一个“计数器”不会自己跑到你的模型里这个连接动作需要代码来完成。3. 代码方式把序列写进自定义模块3.1 为什么我更推荐代码方式界面建序列简单直接但对正经项目来说我强烈建议把序列定义放到自定义模块里而不是在界面上手点。原因有三个。第一环境一致性。一个项目通常有开发库、测试库、生产库界面配的序列不会自动跟着代码走每次换环境都要重新点一遍而且很容易漏。定义在模块里装一次模块所有环境的序列就都齐了。第二可版本管理。代码里定义的东西可以走版本控制、代码评审、变更记录项目交接时翻代码就知道全系统有哪些序列规则。第三安装可重复。模块升级时数据文件里的序列会被重新加载保证规则不会被误改成意料之外的样子。3.2 用 XML 数据文件定义序列在自定义模块里新建一个数据文件比如data/sequence_data.xml内容如下odoo data record idseq_helpdesk_ticket modelir.sequence field namename售后工单编号/field field namecodehelpdesk.ticket/field field nameprefixHD%(year)s%(month)s-/field field namepadding4/field field namenumber_next1/field field namenumber_increment1/field /record /data /odoo然后把文件地址写进清单文件__manifest__.py的data列表里{ name: 某某项目扩展模块, version: 19.0.1.0.0, depends: [base], data: [ data/sequence_data.xml, ], }这里有几个 ID 上的讲究。record的id是这条序列在模块内的唯一标识比如seq_helpdesk_ticket之后如果你想在别的地方引用它全靠这个 ID。field namecode里的值才是程序运行时用的调用码两者千万不要搞混。还有一个细节如果这个序列要支持多公司需要在 XML 里加上公司相关处理。最简单的做法是不指定company_id让它对所有公司生效如果要求每家公司独立计数就要在记录上加公司字段或者用“按公司分配”的机制这个根据实际场景来。3.3 在业务模型中调用序列序列定义好了接下来要让模型的编号字段使用它。最常见也最简洁的方式是用 Python 默认值from odoo import fields, models class HelpdeskTicket(models.Model): _name helpdesk.ticket _description 售后工单 name fields.Char( string工单编号, requiredTrue, copyFalse, readonlyTrue, defaultlambda self: self.env[ir.sequence].next_by_code(helpdesk.ticket), )这样每次创建工单记录时系统会自动取一个号填进name字段。有几个点要重点说明。requiredTrue加上default的组合是为了保证记录创建时一定有编号。copyFalse表示复制记录时不会把原编号带过去这是编号字段的标准配置。readonlyTrue防止用户手动修改编号保证号已经生成就不会被乱改。如果你想要比“用默认日期”更灵活的控制比如用订单的某个业务日期作为前缀日期可以写成def _default_ticket_name(self): seq_date self.order_date or fields.Date.today() return self.env[ir.sequence].with_context( ir_sequence_dateseq_date ).next_by_code(helpdesk.ticket)把这方法名填到default里就行。这个ir_sequence_date的上下文参数是控制前缀日期的关键后面排障还会提到。3.4 日期占位符与跨年重置前缀里的日期占位符在 Odoo 19 里支持这几种占位符说明示例%(year)s四位年份2025%(y)s两位年份25%(month)s两位月份01%(day)s两位日期09默认情况下这些占位符取的是服务器当天日期。如果你希望前缀跟着单据的业务日期走就必须显式地传入ir_sequence_date否则每年年初、跨月的时候就会出现“单据日期是上个月编号前缀却是这个月”的尴尬。再来说跨年重置。很多人以为前缀里带上%(year)s就能实现“每年从 0001 重新开始”这是最常见的误解。带年份前缀只是让编号长成2025-0001、2026-0002的样子但底层计数器一直是递增的2026 年的流水号不会回到 1。要实现真正的按年重置需要用日期区间序列ir.sequence.date_range。简单说日期区间就是给同一个序列定义多个时间段每个时间段有自己的独立计数器。2025 年 1 月 1 日到 12 月 31 日一段下一编号是 12026 年 1 月 1 日开始新的一段下一编号也是 1。界面操作可以在序列表单里打开“日期范围”按钮手动添加代码方式则需要在 XML 或 cron 里创建ir.sequence.date_range记录。实际项目里我见过客户的需求大多可以分成两种要么“每年从 1 重新数但客户只关心单号好看”要么“审计上必须每年独立编号”。前者用带年份的前缀就够后者就得老实上日期区间。这两者选型最好在项目初期就定下来因为上线后再改编号规则涉及历史单号的比对和客户习惯的迁移非常痛苦。3.5 修改内置序列记住 noupdate如果你需要在模块里修改 Odoo 内置序列比如改销售订单的前缀数据记录定义时要分清情况。内置序列的数据记录属于各业务模块自带你的模块文件需要引用并更新它们。这里有个关键经验对已经有业务数据的系统更新序列的 XML 记录时最好给外层包上noupdate1odoo data noupdate1 record idsale.seq_sale_order modelir.sequence field nameprefixSO%(year)s%(month)s-/field /record /data /odoonoupdate1的意思是模块整体升级时这条记录只在第一次安装时加载之后升级不会覆盖界面上已经改过的值。否则会出现这种情况实施人员在生产环境里把前缀调好了某次发布模块升级代码里的默认值又把前缀打回原样。这个坑我见过不止一次凡是涉及序列这种“客户很可能手动改过”的数据一律 noupdate 锁住。4. 产品序列号库存追溯的配置与流转4.1 先在库存设置里打开追溯开关如果业务要做产品级追溯先别急着在产品上配得先确认库存模块的追溯功能已经打开。进入库存模块打开 配置 → 设置找到“追溯性”相关的功能开关把“批次号和序列号”勾上。勾选后库存模块会出现“批次/序列号”相关的菜单和功能。这个开关是整体功能的开关不打开它产品档案里根本看不到追踪方式的选择。这里有一个产品和追踪方式的关系要提前说清楚只有库存产品类型是“可存储产品”Storable Product才能启用序列号追踪。服务类产品、消耗品都不能挂序列号逻辑上也好理解——不存进仓库的东西谈不上一物一号的库存流转。4.2 在产品档案上启用序列号追踪打开具体产品切到“库存”页签找到“追踪方式”字段选择“按序列号”或“按批次”。“按批次”是一批货共用一个批次号适合原料、辅料这类不需要单件区分的物料“按序列号”是一台一个号适合整机、设备、单价高需要逐个追溯的成品。如果拿不准就看一个问题售后或质量出问题时你能不能接受只定位到“这批货”而不是“这一台”如果能用批次如果不能用序列号。保存之后这个产品的每次收货、发货、生产领料系统都会要求你录入或生成相应的批次/序列号。有一点要留意产品一旦已经有了库存流转记录再想切换追踪方式往往会被系统拒绝或者必须在没有现存量和历史记录的前提下才能改。所以这个配置要在业务跑起来之前定好不要想着“先跑一段再说”。4.3 入库、出库、生产里序列号怎么流转产品启用序列号追踪后实际业务操作里就要特别注意每一步的序列号处理。入库收货做收货单时在“详细操作”里每一行都能填序列号。如果你有实物上的序列号逐个扫进去如果没有现成的号可以在收货操作的界面里点“生成序列号”之类的按钮弹出一个向导让你填前缀和起始编号系统会批量生成一段连续的序列号。比如填前缀SN2025-和起始编号1一次性生成 50 个序列号就从SN2025-00001配到SN2025-00050。出库发货发货的时候同样要在详细操作里指定发出去的是哪些序列号。这一步非常重要因为它决定了“这个序列号现在在客户手里”这个状态。如果出库时没指定库存里序列号的状态就不会更新后续售后追溯就断了。发错序列号也是个高频事故扫描时务必核对实物。生产过程生产工单里领料时系统会让你写清使用了哪些批次/序列号的原材料产出成品时会给成品生成新的序列号。这样你就能从一台成品的序列号一路追查到它用了哪个批次的零件这在质量召回场景里是救命的功能。4.4 序列号的查看、搜索与条码操作所有生成过的序列号都会集中显示在库存模块的“批次/序列号”菜单里。列表上可以直接按产品、当前位置、状态在库、已发货、已用来过滤也能按序列号本身搜索。实际项目里序列号最大的痛点不是生成而是抄错和扫错。强烈建议给需要序列号管理的产品配上条码直接用扫码枪操作。序列号入库时扫一遍出库时扫一遍单据上显示的就是扫码枪读进来的字符串少一轮人工抄写错误率能降一大截。条码操作还有一个小技巧序列号的格式最好能设计成“一眼能看出区别”的模式比如固定前缀加定长数字SN2025-00001。这样扫描枪扫错位置、扫重了你肉眼扫一眼数字就能发现异常。不要用没有规律的随机字符串做序列号出了问题查起来非常费劲。5. 高频问题与排障实录5.1 单据编号跳号、不连续这是序列问题里被问得最多的一个。明明上一张是0005下一张直接跳到0010中间空了四个号。原因无外乎两类。第一类中间有单据被创建后删除、取消、或者事务回滚了号已经取走但单据没有真正存在。这是正常的Odoo 不会把用完的号放回去重用。第二类系统并发较高时多个用户同时创建单据序列的读取和更新之间存在竞争部分号会被跳过。能不能强制不跳号理论上可以自己重写取号逻辑让它在事务提交时才更新计数器但这么做会影响并发性能而且跟 Odoo 的设计初衷违背。我一般的处理原则是只要编号递增、可审计、不会重复跳几个号不叫故障。如果客户对“连号”有变态级的执念那就得在需求阶段说明代价并约定好可能出现的现实情况。5.2 代码调用时报错“序列不存在”或返回空号最常见的原因是代码里的调用码和序列记录里的code对不上。比如 XML 里定义的 code 是helpdesk.ticketPython 里却写成了helpdesk.ticket_no那系统找不到序列直接报错。排查方法很简单打开 设置 → 技术 → 序列搜索对应的 code确认记录存在、代码拼写一致。也可以直接在开发者模式下的 Python 控制台里跑一行self.env[ir.sequence].next_by_code(helpdesk.ticket)能返回正常编号说明序列本身没问题问题在调用时机或上下文。返回空字符串或者报错说明 code 写错或者序列记录被公司过滤掉了。多公司环境下还有一种情况序列设成了“按公司分配”但当前用户属于的公司没有对应的序列记录。这时要先到序列列表里确认每个公司是否都有记录没有的话补建或者把序列改成全公司共用。5.3 前缀日期显示不对或跨月不归位前面提过序列默认取“今天”作为日期占位符的值。如果你的业务要求单据前缀跟着订单日期走而订单是补录的历史单据、或者是下个月才生效的预订单编号前缀就会和单据日期对不上。解决办法是取号时传入日期上下文name self.env[ir.sequence].with_context( ir_sequence_dateself.date_order or fields.Date.today() ).next_by_code(sale.order)这个ir_sequence_date就是决定前缀日期的关键值。在实现环节就要想好你希望编号按创建时间走还是按单据的业务日期走这两种场景在实际项目中都存在没有绝对的对错但必须在开发时明确写进代码里。5.4 改了编号规则新单据还是老样子界面上把前缀改了、保存了再新建单据却还是旧编号。这个问题九成出在“改错对象”上。第一种情况这个单据模型调用序列时用的是另一个code你改的那条序列记录根本没被调用。排查方式是去对应模块源码里搜next_by_code传入的值再回去核对序列记录。第二种情况多公司或日期区间导致存在多个同 code 的序列你改的是当前公司的这条但单据在另一家公司下创建。要么全公司统一改要么逐条改。还有一个小概率是升级模块时 XML 又把你改过的值覆盖回去了这就是前面强调的noupdate1问题。生产环境里改序列规则前先看一眼这个序列记录来源模块的 XML 有没有可能覆盖提前做好防护。5.5 想按年重置计数为什么没生效如果你用的是带年份前缀的普通序列计数是不会按年重置的这点前面已经强调过。真正生效的按年重置必须用日期区间功能。实现方式有两条路。简单的是在界面上、序列表单的日期范围区域里手工把每一年的起止时间和起始编号录进去。第一条录2025-01-01到2025-12-31下一编号 1下一年再录一条也是从 1 开始。缺点是需要人工维护容易漏。自动化一点的做法是写一个定时任务cron在每年年底自动为启用了日期区间的序列创建下一年的区间记录。这个方案比较适合单据量大的企业我做过一次之后就把方法沉淀了下来。不过说实话对大多数中小业务我还是倾向于“前缀带年份、计数不重置”的方案省心且审计逻辑简单。按年重置看起来漂亮实际上增加了系统复杂度和出错点除非有明确的合规要求不然不推荐为了好看去折腾。最后说点我个人的实际体会。序列号这东西看起来是个小功能但上线后再改牵涉的是已经生成的单据号、对账逻辑、审计记录和客户使用习惯返工成本远比想象中高。所以我在项目里一贯的原则是能用代码装的序列绝不在界面上手点编号规则在开发阶段就跟客户对齐特别是前缀长度、是否含年份、是否按年重置这几个点上线前写个小脚本批量创建几张测试单把序号格式、跳号情况、跨年行为全部验一遍再放量。把这些提前做到位后面省下的都是真金白银。