基于Odoo的印刷行业文档管理:版本控制与业务关联实现
2026/8/26 11:50:50 网站建设 项目流程

印刷行业的办公室里,最容易失控的不是生产进度,而是文件。

客户发来的PDF“再改一版”、设计师A和设计师B同时编辑同一个AI源文件、打样稿和签样稿命名成最终版_v3_改、下个月客户投诉印刷色差,却根本找不到当时签样存档的是哪一份文件。这些问题表面上是“文件没管好”,本质上是文档没有和业务单据绑定,也没有版本和权限约束。想靠共享文件夹、网盘、微信文件传输助手解决,只会越堆越乱。

Odoo 作为开源 ERP,很多印刷企业选它做订单、采购、生产、库存管理,却往往忽视文档管理模块。我的判断是:Odoo 做印刷行业文档管理完全可行,但你不能拿原生“附件”功能当文档库直接用,而是要根据印刷行业的业务阶段,梳理需求后在 Odoo 上做一层轻量的文档管理模型。这篇文章会把概念、需求拆解、环境准备、代码实现、验证和排错完整讲一遍。

读完这篇文章,你能搞清楚三件事:印刷企业的文档管理到底要管什么、Odoo 里和文档相关的机制有哪些坑、以及如何用自定义模块实现一套带版本、签入签出和业务关联的文档管理功能。适合 ERP 实施顾问、Odoo 二开工程师、印刷企业信息化负责人阅读。

1. 印刷企业文档管理到底难在哪

很多传统 ERP 实施团队,一听到“印刷行业”就先看财务、采购、库存、生产排程这些模块,文档管理往往放到最后才考虑。但实际上线后,最先暴露问题的恰恰是文档管理。

1.1 印刷文件本身就有特殊性

印刷行业的文档,和普通办公文档完全不是一个量级:

  • 文件体积大。一本画册的印前源文件动辄几百 MB,一个 CTP 输出文件甚至能达到 1GB 以上。
  • 版本多且频繁。客户改稿是常态,从初稿、修改稿、打样稿、签样稿到最终印刷稿,一个产品可能产生十几个版本。
  • 格式专业且封闭。常见的 PDF/X、AI、InDesign、CorelDRAW 源文件、拼版文件、色卡文件,普通网盘根本不能在线预览。
  • 业务链条长。从客户来稿到印前处理、打样确认、拼版输出、印刷机台使用、印后加工,每个环节都可能产生关联文件。

普通办公文档管理办法,在印刷场景里很容易失效。

1.2 共享文件夹和网盘为什么靠不住

很多印刷企业目前用的是共享文件夹或者网盘,甚至还在用“文件服务器+自动同步”的组合。这种方式有三个绕不开的痛点:

第一,版本无法管理。共享文件夹里最终稿.pdf最终稿2.pdf最终稿最终版.pdf并存是常态。谁也不知道哪个才是客户确认过的版本。等到大批量印刷完成后,才发现用了旧版本,损失已经造成。

第二,权限难以控制。共享文件夹通常是一个大目录,所有人都有读写权限。业务员能看印前文件,印前工程师也能看客户报价。离职员工手里可能还保留着大量文件。印刷文件涉及客户商业秘密和工艺参数,权限失控的风险远高于普通办公文件。

第三,文档和业务数据脱节。共享文件夹里只有文件名,没有销售订单号、产品编码、工单号。你想追溯“这个文件对应哪个订单”“哪张工单在生产时使用过这份签样稿”,完全做不到。即使 Odoo 系统里订单、工单、BOM 管得很好,文档一旦脱离业务数据,就等于丢失了追溯能力。

1.3 核心判断:文档必须跟着业务单据走

印刷行业的文档管理,真正要解决的问题不是“把文件找个地方存起来”,而是让每份文件都能和销售订单、产品、工单、工艺路线等业务对象建立关联,并控制版本、权限、状态

打个比方:共享文件夹像一个大仓库,货扔进去就不管了;而 ERP 里的文档管理应该像一个有货架编码、有出入库记录、有领用登记的档案室。你不仅要知道“有什么”,还要知道“它在哪、谁在用、怎么流转、哪个版本生效”。

这也是我建议用 Odoo 而不是单纯上一个 DMS(文档管理系统)的原因:Odoo 里天然有订单、产品、工单、项目等业务对象,只要把文档模块和这些对象绑定,就能让文件融入业务流,而不是孤立存在。

2. 认识 Odoo 的文档相关机制

在动手开发之前,先要把 Odoo 本身和文档相关的几个机制搞清楚,否则很容易做出一个“自己造的第二个网盘”,而不是 ERP 文档管理模块。

2.1 ir.attachment:Odoo 底层的附件表

Odoo 中几乎每个业务对象都可以上传附件,比如订单、产品、工单、项目任务。这些附件统一存放在ir.attachment表中,文件物理上存储在 Odoo 的filestore目录,数据库里保存的是文件路径和元数据。

任何模型上都默认带附件功能。例如在销售订单表单上,系统会自动有一个消息回形针按钮,可以上传客户发来的文件、图纸、报价单等。ir.attachment的设计目的是通用的、轻量的,它适合“业务对象上挂几个参考文件”这种场景,但不适合做正式版的文档管理。

它最明显的缺陷有三个:

  • 没有版本控制。同一份文件上传多次,旧的不会自动归档,也不会自动生成 v1、v2、v3。
  • 没有签入/签出机制。两个人同时下载同一个文件修改,改完再传回去,互相覆盖。
  • 没有业务状态。附件只是“挂在订单下面”,但它处于什么状态、是否已被客户签样确认、是否可以用于生产,附件表里没有任何信息。

因此,直接用ir.attachment做文档管理,本质上还是在用仓库思维,只是把共享文件夹换成了 Odoo 界面。

2.2 Documents 模块与外部存储

Odoo 从 14 版本开始提供了 Documents 模块,它引入了目录、标签、操作规则等概念,界面也贴近网盘,支持拖拽上传、文档操作、工作流审批等。它还可以对接 Nextcloud 等外部存储,实现文件不进入 Odoo 的 filestore,而是放到外部存储服务器上。

从材料看,Documents 模块很适合办公文档、合同、制度文件等场景。它解决的是“文件分类、标签、权限、预览”的问题。

但印刷行业要对接的业务流程更强:文件必须关联到销售订单行、BOM、工单;文件版本必须和打样、签样流程绑定;大文件上传后要控制同名的唯一性;印前工程师可能要在文件上传时自动计算哈希值以校验文件一致性。这些需求用 Documents 模块也能做,但定制成本不低。如果你的项目已经用了 Odoo 的销售、生产、采购模块,我更建议在ir.attachment之上做一套轻量的业务文档模型,这样关联业务对象更方便,二开更直接。

2.3 印刷行业文档管理模块的定位

在 Odoo 里做印刷行业文档管理,合理的定位是:不替换ir.attachment,而是把它作为底层存储,在其上增加一层“文档主记录 + 版本记录 + 业务关联 + 状态控制”的模型。

也就是说,ir.attachment负责“文件存在哪儿”,自定义的print.documentprint.document.version模型负责“这份文件属于哪个业务对象、当前是什么版本、谁在修改、是否允许生产使用”。

这种设计的好处是:

  • 底层的文件存储、权限、物理读写,依然由 Odoo 原生机制保证,稳定可靠。
  • 上层的业务语义由自己控制,想加签样状态、版本约束、大文件校验,都能写进模型逻辑里。
  • 未来如果要把文件迁移到外部存储、NAS、MinIO,只需要调整ir.attachment的存储后端,业务模型不需要大改。

3. 印刷行业 ERP 文档管理需求拆解

直接写代码之前,先把需求拆清楚。这一步不做,后面很容易返工。印刷行业的文档管理需求,可以从三个维度拆:业务阶段、权限角色、文档状态。

3.1 按业务阶段拆:不同阶段产生不同类型的文档

印刷产品的完整业务流程一般分为售前、订单、印前、生产、交付五个阶段,每个阶段产生的文档类型差异很大:

业务阶段典型文档说明
售前/报价客户来稿、需求说明、参考文件客户第一次发来的设计文件或样张
订单合同扫描件、订单确认函、工艺说明与销售订单绑定,具有法务和商务属性
印前/工程打样稿、签样稿、色卡、印前拼版文件决定最终印刷效果的关键文件,签样稿是生产依据
生产CTP 输出文件、印刷机台参数、工艺卡片直接用于机台生产,错误代价高
交付成品照片、发货单、客户签收单用于质量追溯和售后

如果只做一个大而全的“文档列表”,文档类型不区分业务阶段,那么印前工程师上传的签样稿和业务员上传的合同扫描件会混在一起,权限也不好控制。

正确做法是:在文档主记录上设计doc_type字段,用 Selection 类型维护阶段分类,不同角色通过视图和权限只能看到自己关心的类型。

3.2 按权限拆:不同角色对文档的处理能力应该不同

印刷企业内部参与文档流转的角色主要包括业务员、印前/工程人员、生产班组、管理层。他们在文档管理系统中应该有不同的权限边界:

  • 业务员:负责客户来稿上传、查看订单相关文档、反馈客户修改意见,可以上传和更新客户文件,但不能随便改印前生产文件。
  • 印前/工程人员:负责打样稿、签样稿、拼版文件的管理,可以创建新版本、签出签入文件、修改工艺参数,但不能更改商务合同。
  • 生产班组:按工单查看已确认的签样稿、拼版文件,通常是只读权限,避免误操作。
  • 管理层/质量人员:可以全库检索、查看操作日志、审查状态变化,但不应该直接修改生产文件。

在 Odoo 的权限模型里,推荐把base.group_user作为基础权限,再用“技术标签”或“额外用户组”来控制具体操作的写权限。对于大多数企业,做到“业务员和印前分工隔离、生产班组只读”就已经比共享文件夹强很多。

3.3 按流程状态拆:文档不是永久的“可用”状态

印刷文件的流程状态非常重要。很多质量事故,不是因为文件丢了,而是因为用了还没确认的版本去生产

文档状态至少应该包含:

  • 待处理:文件刚刚上传,还没有人处理。
  • 审核中:印前正在检查文件,或已经提交客户确认。
  • 已确认:已满足生产条件,可以作为正式生产依据。
  • 已归档:生产结束,文档冻结,不能随意修改。

状态控制的意义,在于把流程制度固化到系统里。比如“未签样的文档不能关联到生产工单”,这条规则如果只写在制度文件里,执行靠人盯,肯定会漏。但如果在 Odoo 模型里加一个约束,工单关联文档时检查状态,就能从系统层面杜绝误用。

4. 技术方案:在 Odoo 中设计文档管理模型

需求拆完,设计方案就顺理成章了。这套方案的核心思路是:文档主记录 + 版本子表

4.1 为什么使用“主记录 + 版本子表”

假设你是印前工程师,客户发来一个 PDF,你需要修改三次,每次修改都必须保留历史文件,以确定最终客户确认的是哪个版本。如果用单个附件字段保存,上传新版本时旧文件就会丢失,一旦需要回退,只能靠运气。

因此,设计上要分成两层:

  • print.document:文档主记录,一条记录代表一份业务文档。
  • print.document.version:文档版本记录,维护该文档的所有历史版本。

主记录用来存“这份文档的业务属性”:它属于哪个订单、哪个产品、哪个工单;文档类型是什么;当前处于哪个状态;当前生效的是哪个版本。版本子表用来存“文件本身”:上传的附件、版本号、文件哈希、修改说明、签出人。

这样,客户每次修改,不是“替换”文件,而是“追加”一个新版本,旧版本永远可追溯。主记录的current_version_id字段指向当前生效的版本,生产班组看到的始终是明确的一份文件,不会被文件名误导。

4.2 核心模型字段建议

下面这些字段,是一个印刷行业文档管理模块的最小编码:

模型字段说明
print.documentname文档名称
print.documentcode文档编号,可选,便于检索
print.documentdoc_type文档类型,对应业务阶段
print.documentorder_id关联销售订单
print.documentproduct_id关联产品
print.documentworkorder_id关联生产工单
print.documentstate文档状态
print.documentversion_ids版本记录列表
print.documentcurrent_version_id当前生效版本
print.documentchecked_out_by当前签出人
print.document.versionsequence版本序号,如 1、2、3
print.document.versionattachment_id实际文件附件
print.document.versionfile_hash文件 SHA256 哈希值
print.document.versionnote修改说明

4.3 版本自动编号和签入签出设计

版本子表在create时自动计算sequenceversion_name,不需要人工填写。如果有 3 个版本,新增第四个版本时自动命名为 v4,这样可以避免出现最终版_改1_改2这种命名混乱。

签入签出是印刷行业文档管理的关键机制:某个人要修改文件时,先执行“签出”,系统记录checked_out_by为当前用户,其他人如果要签出,会被系统拒绝,防止两个工程师同时修改同一份文件。

签出后,工程师下载文件并修改,改完再上传新版本并“签入”,系统自动释放文件锁。这个思路不复杂,但能直接解决印刷企业多人交叉修改文件的混乱问题。

5. 环境准备:Ubuntu 安装 Odoo 与基础配置

在开发和测试环境里,建议先在 Ubuntu 服务器上把 Odoo 跑起来。搜索热词里“ubuntu安装odoo”“linux odoo安装部署”经常出现,说明很多人卡在环境搭建这一关。下面给出一个常见的 Ubuntu 安装流程,重点是通用步骤和容易踩坑的地方。

5.1 安装系统依赖

Odoo 依赖 Python 3、PostgreSQL、wkhtmltopdf 等组件。下面的命令适用于 Ubuntu 20.04/22.04 等版本,如果你使用新版 Ubuntu 或 Debian,个别包名会有差异。

sudo apt update && sudo apt upgrade -y sudo apt install -y git python3-pip python3-dev python3-venv \ build-essential libxml2-dev libxslt1-dev zlib1g-dev \ libsasl2-dev libldap2-dev libssl-dev libjpeg-dev libpq-dev

这里需要注意,Odoo 不同版本对 Python 版本的依赖不一样。在安装前先用python3 --version确认版本满足你所用 Odoo 版本的要求。如果版本不满足,不要直接硬装,优先考虑使用对应版本的 Ubuntu 或使用 pyenv 管理 Python 版本。

5.2 创建系统用户和数据库用户

生产环境推荐单独创建一个名为odoo的系统用户来运行 Odoo,不要用 root 运行,也不要用个人账号运行。

sudo adduser --system --home=/opt/odoo odoo sudo su - postgres -c "createuser -s odoo" sudo -u postgres psql -c "alter user odoo with password 'odoo';"

这里最容易踩的坑是:PostgreSQL 用户和系统用户名要保持一致,否则 Odoo 连接数据库时会因为对不上用户名报错。在本地调试时,如果 PostgreSQL 使用 peer 认证,db_user配置成与系统用户一致即可免密连接。

5.3 安装 wkhtmltopdf

Odoo 打印 PDF 报表时需要 wkhtmltopdf。如果缺少这个组件,打印报表时会报错或者导出 PDF 空白。

sudo apt install -y wkhtmltopdf

不同 Ubuntu 版本下 wkhtmltopdf 的 apt 源可能缺失或存在兼容性问题。如果apt安装失败,可以到 wkhtmltopdf 官网下载对应系统版本的 deb 包手动安装,安装完成后用wkhtmltopdf --version验证。

5.4 下载 Odoo 源码并创建虚拟环境

Odoo 官方支持 git 克隆源码的方式部署。下面这类方式是目前社区常见做法:

sudo mkdir -p /opt/odoo sudo chown odoo:odoo /opt/odoo sudo -u odoo git clone https://www.github.com/odoo/odoo /opt/odoo/odoo --depth 1 cd /opt/odoo sudo -u odoo python3 -m venv odoo-venv sudo -u odoo ./odoo-venv/bin/pip install -r odoo/requirements.txt

使用--depth 1只克隆最近一次提交,可以节省下载时间和磁盘空间。在正式生产环境,建议锁定具体的 Odoo 版本分支,不要长期跟最新提交,否则后续升级可能带来兼容性问题。

5.5 创建配置文件并启动

/etc/odoo.conf中创建配置文件:

[options] addons_path = /opt/odoo/odoo/addons,/opt/odoo/my-addons db_host = False db_port = False db_user = odoo db_password = False

addons_path里既有 Odoo 内置模块目录,也有自己开发的模块目录。如果插件目录权限不对,启动时会出现模块列表加载不出来的情况。启动命令如下:

sudo -u odoo /opt/odoo/odoo-venv/bin/python /opt/odoo/odoo/odoo-bin -c /etc/odoo.conf

第一次启动时,Odoo 会初始化数据库,并在日志中打印服务地址。看到类似HTTP service (werkzeug) running on 0.0.0.0:8069的日志就表示启动成功。

这里要特别提醒:以上步骤只是最小可运行的部署方式,正式生产环境建议再加 Nginx 反向代理、HTTPS、PostgreSQL 定期备份、Odoo 日志轮转等配置。不要在没有任何备份策略的前提下直接拿生产环境测试模块。

6. 实现印刷行业文档管理模块

下面开始实现一个最小可用的印刷行业文档管理模块。代码按 Odoo 15/16/17 的常见写法演示。如果你的项目还在 Odoo 12 或更早版本,部分字段和装饰器请对照官方 API 调整。

6.1 创建模块目录结构

在自定义 addons 目录下创建模块:

mkdir -p print_document/models mkdir -p print_document/views mkdir -p print_document/security touch print_document/__init__.py touch print_document/__manifest__.py

__init__.py内容:

# 文件路径:print_document/__init__.py from . import models

__manifest__.py内容:

# 文件路径:print_document/__manifest__.py { 'name': '印刷行业文档管理', 'version': '1.0.0', 'category': 'Sales', 'summary': '印刷企业订单、印前、生产环节文档版本管理', 'description': '''印刷行业文档管理模块,支持客户来稿、打样稿、签样稿、印前拼版文件等文档类型的版本管理、签入签出和业务关联。''', 'depends': ['base', 'sale_management', 'mrp', 'mail'], 'data': [ 'security/ir.model.access.csv', 'views/print_document_views.xml', ], 'installable': True, 'application': True, }

depends根据你的业务需要,如果只需要订单关联,去掉mrp也可以。本文为了演示工单关联,保留了对生产模块的依赖。

6.2 模型代码:文档主记录与版本子表

文件路径:print_document/models/print_document.py

# 文件路径:print_document/models/print_document.py import base64 import hashlib from odoo import api, fields, models, _ from odoo.exceptions import ValidationError class PrintDocument(models.Model): _name = 'print.document' _description = '印刷文档主记录' _inherit = ['mail.thread', 'mail.activity.mixin'] _order = 'create_date desc, id desc' name = fields.Char(string='文档名称', required=True) code = fields.Char(string='文档编号', copy=False) doc_type = fields.Selection([ ('customer_file', '客户来稿'), ('proof', '打样稿'), ('signed', '签样稿'), ('prepress', '印前拼版文件'), ('ctp', 'CTP输出文件'), ('process', '工艺参数'), ('other', '其他'), ], string='文档类型', default='customer_file') order_id = fields.Many2one('sale.order', string='销售订单', ondelete='restrict') product_id = fields.Many2one('product.product', string='产品', ondelete='restrict') workorder_id = fields.Many2one('mrp.workorder', string='生产工单', ondelete='restrict') state = fields.Selection([ ('draft', '待处理'), ('review', '审核中'), ('confirmed', '已确认'), ('archive', '已归档'), ], string='状态', default='draft', tracking=True) version_ids = fields.One2many('print.document.version', 'document_id', string='版本记录') current_version_id = fields.Many2one('print.document.version', string='当前生效版本') checked_out_by = fields.Many2one('res.users', string='当前签出人', copy=False, tracking=True) @api.constrains('name', 'order_id') def _check_unique_document(self): for rec in self: if rec.order_id: domain = [ ('name', '=', rec.name), ('order_id', '=', rec.order_id.id), ('id', '!=', rec.id), ] if self.search_count(domain) > 0: raise ValidationError(_('同一销售订单下,文档名称不能重复。')) def action_checkout(self): """签出文档:锁定当前版本,不允许他人继续修改。""" self.ensure_one() if not self.current_version_id: raise ValidationError(_('当前文档还没有版本,无法签出。')) if self.checked_out_by: raise ValidationError(_('当前文档已被 %s 签出。') % self.checked_out_by.name) self.write({'checked_out_by': self.env.user}) self.current_version_id.write({'checked_out_by': self.env.user}) def action_checkin(self): """签入文档:解除锁定。""" self.ensure_one() if self.checked_out_by and self.checked_out_by != self.env.user: raise ValidationError(_('你不是当前签出人,无法签入。')) self.write({'checked_out_by': False}) if self.current_version_id: self.current_version_id.write({'checked_out_by': False}) def action_to_confirmed(self): """手动确认文档状态,表示文档可以用于生产。""" self.ensure_one() if self.state == 'draft': self.write({'state': 'confirmed'}) else: raise ValidationError(_('只有待处理状态的文档可以确认为已确认。')) class PrintDocumentVersion(models.Model): _name = 'print.document.version' _description = '印刷文档版本' _order = 'sequence desc, id desc' document_id = fields.Many2one('print.document', string='所属文档', required=True, ondelete='cascade') sequence = fields.Integer(string='版本序号', readonly=True) version_name = fields.Char(string='版本名称', readonly=True) attachment_id = fields.Many2one('ir.attachment', string='文件', required=True) file_hash = fields.Char(string='文件SHA256') note = fields.Text(string='修改说明') checked_out_by = fields.Many2one('res.users', string='签出人', readonly=True) @api.model_create_multi def create(self, vals_list): for vals in vals_list: document_id = vals.get('document_id') doc = self.env['print.document'].browse(document_id) max_seq = max(doc.version_ids.mapped('sequence')) if doc.version_ids else 0 vals['sequence'] = max_seq + 1 vals['version_name'] = 'v%d' % (max_seq + 1) attachment_id = vals.get('attachment_id') if attachment_id: attachment = self.env['ir.attachment'].browse(attachment_id) vals['file_hash'] = self._compute_file_hash(attachment) return super().create(vals_list) @staticmethod def _compute_file_hash(attachment): """读取附件内容并计算 SHA256,用于文件完整性校验。""" if not attachment.datas: return '' try: raw = base64.b64decode(attachment.datas) return hashlib.sha256(raw).hexdigest() except Exception: return ''

我在代码里做了几件值得注意的事:

  • _inherit = ['mail.thread', 'mail.activity.mixin'],让文档主记录支持消息跟踪,谁在什么时候改了状态、签出、签入,都会留下记录。
  • 版本子表create时自动计算版本序号,不需要人工维护。
  • file_hash在创建版本时自动计算,如果后续生产工单要从系统导出文件,可以先校验哈希值确认文件没有被篡改。
  • action_checkoutaction_checkin实现了最基本的签入签出控制。
  • 增加了“同一订单下文档名称不能重复”的约束,避免业务员重复上传同名文档造成混乱。

需要注意的是,这里用attachment.datas读取的是 base64 编码后的数据。对于超大文件,读取到 Python 内存中会占用较多内存,生产环境建议改用ir.attachment_file_read或者流式读取方式,并评估服务端限流和内存配置。

6.3 视图代码:菜单、列表和表单

文件路径:print_document/views/print_document_views.xml

<!-- 文件路径:print_document/views/print_document_views.xml --> <odoo> <!-- 列表视图 --> <record id="view_print_document_tree" model="ir.ui.view"> <field name="name">print.document.tree</field> <field name="model">print.document</field> <field name="arch" type="xml"> <tree string="印刷文档"> <field name="code"/> <field name="name"/> <field name="doc_type"/> <field name="order_id"/> <field name="product_id"/> <field name="state"/> <field name="current_version_id"/> <field name="checked_out_by"/> </tree> </field> </record> <!-- 表单视图 --> <record id="view_print_document_form" model="ir.ui.view"> <field name="name">print.document.form</field> <field name="model">print.document</field> <field name="arch" type="xml"> <form string="印刷文档"> <header> <button name="action_checkout" string="签出" type="object" attrs="{'invisible': [('checked_out_by', '!=', False)]}"/> <button name="action_checkin" string="签入" type="object" attrs="{'invisible': [('checked_out_by', '=', False)]}"/> <button name="action_to_confirmed" string="确认可用于生产" type="object" attrs="{'invisible': [('state', '!=', 'draft')]}"/> </header> <sheet> <div class="oe_title"> <h1><field name="name"/></h1> <field name="code" placeholder="文档编号,可选"/> </div> <group> <group> <field name="doc_type"/> <field name="state"/> <field name="order_id"/> <field name="product_id"/> <field name="workorder_id"/> </group> <group> <field name="current_version_id"/> <field name="checked_out_by"/> </group> </group> <notebook> <page string="版本记录" name="versions"> <field name="version_ids"> <tree editable="false"> <field name="version_name"/> <field name="attachment_id" widget="many2one_binary"/> <field name="file_hash"/> <field name="checked_out_by"/> <field name="note"/> </tree> </field> </page> </notebook> </sheet> </form> </field> </record> <!-- 窗口动作 --> <record id="action_print_document_list" model="ir.actions.act_window"> <field name="name">印刷文档</field> <field name="res_model">print.document</field> <field name="view_mode">tree,form</field> <field name="context">{'search_default_doc_type': 'customer_file'}</field> </record> <!-- 顶级菜单和子菜单 --> <menuitem id="menu_print_document_root" name="印刷文档" sequence="20" web_icon="print_document,static/description/icon.png"/> <menuitem id="menu_print_document_list" parent="menu_print_document_root" name="文档目录" action="action_print_document_list"/> </odoo>

在版本记录里,我用widget="many2one_binary"显示附件上传控件,这是 Odoo 中比较常用的文件上传方式。点击回形针或选择按钮,就能把文件作为附件上传到版本记录中。

6.4 权限 CSV 文件

文件路径:print_document/security/ir.model.access.csv

id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_print_document_user,print.document.user,model_print_document,base.group_user,1,1,1,0 access_print_document_version_user,print.document.version.user,model_print_document_version,base.group_user,1,1,1,0

这个权限表给了所有内部用户对文档主记录和版本记录的读、写、创建权限,但没有删除权限。删除权限需要单独配置给管理员或者特定安全组。在生产环境中,建议再增加一个“印前工程师”用户组,单独控制签样稿、拼版文件的写权限,让业务员只能创建客户来稿类型。

6.5 安装并升级模块

print_document目录放到配置好的addons_path中后,有两种方式安装:

方式一,命令安装:

sudo -u odoo /opt/odoo/odoo-venv/bin/python /opt/odoo/odoo/odoo-bin \ -c /etc/odoo.conf \ -d demo \ -u print_document \ --stop-after-init

方式二,界面安装:在 Odoo 后台开启开发者模式,进入“应用”菜单,点击“更新应用列表”,搜索print_document,点击安装。

升级模块后,如果看到“模型已安装”且菜单“印刷文档”出现在顶部导航,就说明模块成功加载。

7. 运行结果与效果验证

模块装好后,不要急着录入大量数据,建议先按下面的路径做一轮功能验证,确认每个关键点都是合格的。

7.1 验证路径一:创建文档并上传第一个版本

在“印刷文档 > 文档目录”中新建一条文档记录,填写文档名称、类型、关联的销售订单,然后在“版本记录”页签添加一条记录,上传一个测试 PDF 文件。

预期效果:

  • 版本名称自动生成为v1
  • file_hash字段自动填充为一串 64 位十六进制字符。
  • state默认为“待处理”。

如果版本名称没有自动生成,说明create方法中的版本计算逻辑没有执行。可以先查看版本子表的sequence字段,如果为空,则在表单视图中把该字段设为 readonly 状态,或者检查模块是否成功升级。

7.2 验证路径二:新增第二个版本

再添加一条版本记录,上传修改后的第二次文件。

预期效果:

  • 版本名称自动生成为v2
  • 之前的v1记录仍然存在,文件内容没有被覆盖。
  • 文档主记录的current_version_id暂不会自动变化,需要人工指定当前生效版本。

这里要注意:当前生效版本的自动切换可以写在版本创建逻辑里,也可以由人工控制。在印刷生产场景中,我更推荐人工指定当前生效版本,因为“最新上传的版本”未必是“客户最终确认的版本”,比如客户可能退回使用 v1 版本,这时只靠上传顺序判断是错误的。

7.3 验证路径三:签出与签入

使用用户 A 打开文档,点击“签出”。预期效果是checked_out_by显示为用户 A,其他用户再点击签出会报错“当前文档已被某用户签出”。

用户 A 上传新版本并确认无误后,点击“签入”,预期效果是checked_out_by清空。

这里的重点是异常路径也要测:如果用户 A 签出后没有签入就关闭页面,文档会被一直锁住。生产环境建议增加一个定时任务,自动释放超过一定时间未签入的文档锁,或者增加管理员强制签入按钮。

7.4 验证路径四:唯一性约束

在同一销售订单下创建两个名称完全相同的文档记录,预期效果是第二条保存时报错提示“同一销售订单下,文档名称不能重复。”如果数据成功保存了,要么没有升级最新模块,要么约束写错了字段。

7.5 验证路径五:状态流转

把文档从“待处理”点击“确认可用于生产”,状态变成“已确认”。再次点击,会报错提示只有待处理状态才能确认。这个验证逻辑是为了防止重复确认导致状态不可控。

8. 常见问题与排查方法

在实际开发和上线过程中,下面这些问题是比较常见的,我把排查思路整理成表格,方便你在遇到问题时快速定位。

问题现象可能原因排查方式解决方案
模块安装后菜单不显示没有更新应用列表进入开发者模式后“更新应用列表”刷新应用列表并重新搜索模块
模型报KeyError或字段不存在模块没有升级成功查看 Odoo 日志执行-u print_document强制升级
版本名称没有自动变成 v1/v2create方法没有执行在版本子表点击“创建”后看 sequence 字段检查版本子表视图是否允许创建,检查代码中的api.model_create_multi
file_hash为空attachment.datas读取失败或文件尚未写入先确认附件能正常下载用 try/except 捕获异常,查看日志输出
同一订单下重复文档名仍能保存约束没生效确认模块升级成功,检查模型名重新触发升级
大文件上传超时或失败Nginx 请求体大小限制查看 Nginx 错误日志调整client_max_body_size,并考虑单独配置大文件上传入口
点击打印报表报错wkhtmltopdf 未安装或版本不兼容在服务器执行wkhtmltopdf --version重新安装或安装兼容版本
附件文件无法下载ir.attachment存储路径被改动检查filestore目录是否存在恢复文件存储路径,检查磁盘权限
签出后忘记签入导致文件锁定没有强制释放机制查看checked_out_by字段增加定时任务或管理员强制签入按钮
生产工单引用签样稿时未校验状态业务约束未加在工单模型上检查mrp.workorder是否有相关约束mrp.workorder上增加关联检查和状态校验

这里的两个高频坑需要多说几句。

第一个坑是模块升级不彻底。很多初学者改了 Python 代码后只在界面里刷新页面,没有真正升级模块。Odoo 的模型字段变更,必须执行模块升级才会同步到数据库。所以在改模型、改视图、改权限后,一定要执行-u print_document,或者用开发者模式的“升级”按钮重新升级模块。

第二个坑是大文件处理。印刷文件动辄几百 MB,直接用 Odoo 默认的附件上传方式很可能超时或者内存溢出。更稳妥的做法是配置 Nginx 或反向代理的client_max_body_size,并考虑用 Dropbox、Nextcloud、对象存储等方式对接大文件。从项目规划阶段就要评估文件大小和存储策略,不要把 Odoo 的 filestore 当成无限容量网盘使用。

9. 最佳实践与工程建议

模块跑通只是第一步,把它做成一个印刷企业真正日常使用的功能,还需要在工程层面做大量细化。

9.1 文档命名规范

系统里有了版本号,文件名本身就可以简化。建议规定:文件名只写“客户简称+产品名称+用途”,版本信息交给系统维护。禁止在文件名中出现最终版修改版打死不改版这类字样。

同时在name字段的填写上,也要给出命名模板,比如“客户-产品-文档类型”。如果担心业务员乱填,可以在 Odoo 里配置正则校验,不满足规则就禁止保存。

9.2 权限边界要收敛

默认权限表我给了所有内部用户完整的写权限,这在正式环境是不推荐的。建议最小权限原则:

  • 业务员只能创建和编辑“客户来稿”“其他”类型。
  • 印前/工程人员才能修改“打样稿”“签样稿”“拼版文件”。
  • 生产班组只读或者按工单过滤。
  • 删除权限只给系统管理员。

Odoo 的权限控制可以精细到ir.rule记录规则,建议按部门或用户组增加数据级权限。例如印前工程师只能看到自己负责的文档,管理层可以查看全部。

9.3 备份策略:不仅要备份数据库,还要备份 filestore

很多人以为备份 Odoo 只要备份 PostgreSQL 数据库就够了,这是一个非常大的误区。数据库里存的是元数据和附件路径,真正的文件二进制内容在filestore目录中。如果只备份数据库,一旦服务器磁盘损坏,所有附件都会丢失。

生产环境备份策略至少要包含两部分:

# 备份数据库 pg_dump -U odoo -h localhost -F c odoo_prod > odoo_prod.dump # 备份 filestore tar -czf filestore_$(date +%F).tar.gz /opt/odoo/.local/share/Odoo/filestore

建议把数据库和 filestore 的备份放到不同的存储介质上,并定期做一次“恢复演练”,确认备份确实能用。

9.4 流程约束要落到系统里

不要只在制度上写“签样稿确认后才能生产”。可以在mrp.workorder或其他生产单据模型上增加约束,当用户选择文档时,检查该文档状态是否为“已确认”,如果未确认就弹窗报错。代码很简单,但能防止生产班组在不知情的情况下使用错误版本。

9.5 添加审计日志

Odoo 的mail.thread已经能记录字段变化,可以在此基础上做更完整的审计。例如:谁在什么时间上传了哪个版本、谁签出了文档、谁把文档状态从“待处理”改成“已确认”,这些记录要保留至少一年。印刷行业客户投诉、质量追溯时,这些日志就是证据。

9.6 历史数据迁移

很多印刷企业已经把文件堆在共享文件夹里很多年了,上线新系统时,把这堆文件一次性导入到 Odoo 是很有挑战的事。建议分两步:

  • 先迁移业务价值高的签样稿、拼版文件和生产工单关联文档。
  • 普通历史参考文件可以只做索引,不迁移文件本尊,等哪天真要用的时候再去原位置找。

迁移时不能只搬文件,还要把历史版本、修改时间、操作人尽量导入到系统中,否则只搬一个“最终版”过去,历史追溯链条还是断的。

10. 总结与实际项目落地建议

这篇文章从印刷行业文档混乱的痛点开始,分析了 Odoo 中ir.attachment和 Documents 模块各自的边界,然后给出了一个“文档主记录 + 版本子表”的轻量方案,并完整实现了文档类型、状态、版本、签

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询