一个匿名的真实电商案例。一家在 MercadoLibre Full 上运营的汽车配件分销商请求自动化其补货流程。原本作为 Odoo 中一个按钮开始的项目,最终演变为对一种多对多关系的发现,这种关系打破了几乎所有在带有集成物流的市场平台上销售的人的常识。定制模块留在了客户那里;方法论留在了 Transgenia。
在带有集成物流的市场平台上做生意,比如拉美的 MercadoLibre Full,北美的 Amazon FBA,其表面上看起来简单得令人误解。对商务团队来说,问题看起来是操作性的:在平台上手工录入数量既慢又容易出错。对技术团队来说,问题看起来是集成性的:既然有 API,从 Odoo 调用就行了。这两种诊断都是同一个真实瓶颈的结果,但没有一种能真正描述它。
真正的瓶颈存在于一个几乎所有人不加质疑就继承下来的假设中:一个 SKU = 一个商品页面。当这个假设被打破时,而在 MercadoLibre Full 中它总是被打破,补货的设计也随之悄悄地崩溃,表现为间歇性缺货以及那些不知为何总是短缺的采购单。
本文重构了我们如何在一次与不再服务的客户的敏捷精化中得出这个结论,如何设计一个 Odoo 定制模块,用一个确定性引擎取代人工补货而不触及 Odoo 的原生魔法,以及为什么由此产生的模式可以复用到任何带有集成 fulfillment 的市场平台上。
1. 当常识欺骗你:MercadoLibre Full 隐藏的 bug
与客户的第一次会议以一个清晰的操作请求开始:他们想要在 Odoo 内部有一个按钮,把库存推送到 MercadoLibre。分销商的内部团队已经在两个电子表格中构建了补货算法;他们想把它《搬到 Odoo》,让 Odoo 来做这件事。其他一切(数据模型,市场平台上的发布,审批顺序)都被视为不言自明。
事实并非如此。在精化的某一刻,面对一个共享屏幕上显示着客户 Excel 文件的画面,出现了一段重新定义了整个项目的对话:
"我原来以为 MercadoLibre 的 Order ID 就是商品页面的编号,我们可以把所有那些产品嵌套在这个 Order ID 里,这样在生成骨架时,MercadoLibre 就会更新库存。"
"我们得把名字说得非常清楚,不然会混得一塌糊涂。MercadoLibre 里的 Order ID 是一次销售,不是一个商品页面。你想的其实是 Item ID:那才是一个商品页面。而且一个商品页面只有一个产品,但同一个产品可以有一百个商品页面。这是一对多的关系。"
这段对话(匿名化过;会议中有真实姓名)浓缩了这个深层的发现。Order ID 标识一次已完成的销售;Item ID 标识一个已发布的广告。它们是有着不同生命周期的不同实体,MercadoLibre 允许同一个 SKU 出现在不同的商品页面上,每个都有独立的价格,运输方式和库存。在那次对话之前,项目正朝着在商品页面与产品之间建模一个 Many2one 关系并围绕这个假设设计界面的方向前进。它在开发环境中会正常工作;到了生产环境,第一次遇到供应商发货短缺时就会失败。
这里的教训不是《把 MercadoLibre 的文档读得更仔细一点》,那个区分几年前就写在卖家词汇表里了。教训更让人不舒服:信息偏差不是靠文档来纠正的,是靠对话来纠正的。敏捷精化不是给做 Scrum 的人举行的仪式;它是一个 Odoo 项目大部分风险居住的地方。
2. 为什么不是《一个产品 = 一个商品页面》
在 MercadoLibre 中,产品是物理世界中存在的东西:一个零件,有一个 SKU,一个成本,一个存储位置。商品页面是平台上存在的东西:一个广告,带有标题,照片,描述,价格,状态(新品,二手),运输类型(Full,Flex,自提)和分配的库存。在 Odoo 中,前者是 product.product;后者是一个平台用自己的序列号(Item ID)编号的外部实体。
从产品这一侧来看,真实的基数是一对多:同一个 SKU 可以同时存在于多个商品页面。在一个汽车配件生意里,目录里有成千上万个零件,搜索位置的竞争激烈,同一产品有多个商品页面不是意外:这是刻意的商业策略。一个商品页面可能带的是锚定价;另一个是带补贴运费的变体;再一个是与互补产品的捆绑;还有一个是只接受特定参数的 MELI 优选渠道版本。
flowchart LR P((单一 SKU
例如 机油滤清器 XYZ)) L1[商品页面 1
Full 发货
锚定价] L2[商品页面 2
Full 发货
MELI 优选] L3[商品页面 3
Full 发货
与互补品捆绑] L4[商品页面 4
自提
其他渠道] S1[Full 分配库存 1] S2[Full 分配库存 2] S3[Full 分配库存 3] S4[主仓库存] P --> L1 P --> L2 P --> L3 P --> L4 L1 --> S1 L2 --> S2 L3 --> S3 L4 --> S4
这些商品页面中的每一个都像一个有自己需求的虚拟分店:分别累积访问量,咨询,销售和周转速度。MercadoLibre Full 加剧了这种不对称:对每个带 Full 发货的商品页面,平台要求分配到那个特定商品页面的库存,而不是分配到通用 SKU 的库存。实际后果是,到了计算向供应商下多少订单的时候(通常是一家有长交货周期的中国工厂),需要按 SKU 汇总需求;但到了决定向 Full 发多少货的时候,又需要按商品页面拆分。
任何把这两个计算当作同一个问题来处理的补货引擎,都会开始累积误差。一个很小很难追踪的误差,表现为日常运营《解决》的间歇性缺货,就是每隔一段时间就向供应商下一次紧急订单,这是模型假设校准错误的典型症状。
3. 敏捷精化:发现 bug 的那场对话
在软件开发社区,精化是通过一系列短小连续的对话把需求拆解为可执行单元的实践,每场对话都围绕一个双方都能同时看到的具体产物。在 Odoo 项目中很少使用。隐含的假设是:Odoo 作为一个可配置的产品,消除了精化的必要性;只要《接收客户需求》并将其转化为 Studio,server actions 或定制模块就够了。这是个昂贵的假设。
发现 SKU 与商品页面之间存在多对多关系的那次会议,不是一场两小时的 discovery 会议。是一次短冲刺,一个小时,议程极简,一个 Excel 文件投影在屏幕上,我们逐一询问每列,每个公式,每个页签做什么。在其中一个问题上,客户方那位真正了解文件的运营人员非常自然地说:
"能和你或他指定的人做这些对话真的很好,因为这些细节他不会坐下来解释。他只是把东西扔给你,让你去做。"
赞助人把 Excel 交出来的时候,理所当然地认为算法是自包含的,一个开发者可以把它搬走。运营人员知道并非如此,但之前没有人问过她。这里精化的作用不是《收集需求》,而是发现一方的心智模型与另一方的心智模型不一致,而且这两个模型在项目中共存却从未被说清楚过。
多对多的发现出现在那次会议的第二十分钟。它没有出现在任何先前的简报中。在一个线性流程里,它会晚到,也会贵得多,很可能出现在验收测试阶段,那时代码,迁移,培训和期望都已经与错误的模型对齐了。而实际上它是以两个人看着同一张表格进行二十分钟对话的形式来到的。
这个教训对项目其余部分的可迁移性,最好通过一条操作规则来看:在 Odoo 实施中,《跟着 Excel 文件里的算法走》是不够的。需要在把模型翻译成 Python 之前,先翻译心智模型。精化不是方法论的附属品;它是风险居住的地方。
4. 补货的解剖:按累计销售价值的 ABCD 分类
一旦模型问题解决了(SKU 和商品页面是具有一对多基数的不同实体),接下来就是计算问题。目录里有成千上万个零件,并不是所有的都值得同等程度的关注:有些每天都在卖,有些几乎从不卖,还有一些放在目录里但已经几个月没有出货了。把它们一视同仁,正是那种同时产生两种症状的错误:资金被套在死货上,同时活货缺货。
ABC 分类是物流领域的经典工具;它的贡献很简单,但对从未用过的人来说是反直觉的:按累计销售价值分类,而不是按销售数量。一个每月以每件五千比索卖出五件的产品,对生意的贡献比每月以每件五十比索卖出一百件的多。市场平台上认真做补货的第一条规则是按比索衡量,而不是按件数。
在这个项目中我们加了字母 D。A 类集中大约 70% 的累计销售价值,B 类是下一个 20%,C 类是剩下的 10%。D 类是运营例外:在评估期内没有销售的产品。这个区分很重要,因为销售报告可能有噪音,那些没有出货但因录入错误或内部移动而出现的零件,应该被排除在计算之外,而不是最终落到 C 类里带着一个正数最小值强制产生一次不合理的采购。
pie showData title 按类别的销售价值分布 "A 类 (70% 价值)" : 70 "B 类 (20% 价值)" : 20 "C 类 (10% 价值)" : 10
重新计算每月运行一次,有一个专用的 cron。产品会随着时间自然地在类别之间迁移,这种迁移就是业务信号:一个从 B 类升到 A 类的零件值得经理复查,因为它可能在撑起本季度;一个从 A 类降到 D 类的零件是停产的早期警报。技术上,family_id 字段位于 product.template,而不是 product.product,这是一个重要决定,我们在设计错误一节讨论过:销售按主产品汇总,而不是按变体。
每月 cron 遵循一个简化的可审计管道:读取该期间的销售报告,按主产品分组,计算每个 SKU 的总价值,从高到低排序,累计到 70%,90% 和 100% 的切点,分配类别并持久化到 product.template.family_id。出现在目录中但不在销售报告中的产品落入 D 类。这最后一点不是小细节:正是它防止目录中的垃圾消耗采购资金。
flowchart LR A([每月 cron]) --> B[读取 sale.report
过去 N 个月] B --> C[按 product.template
分组] C --> D[累计销售价值
qty x precio_unit] D --> E[从高到低
排序] E --> F{累计
切点} F -->|至 70%| A2[A 类] F -->|71% - 90%| B2[B 类] F -->|91% - 100%| C2[C 类] F -->|无销售| D2[D 类] A2 --> Z[(product.template.family_id)] B2 --> Z C2 --> Z D2 --> Z
分类保持最新,系统就有了它需要的一切来把杠杆放在能赢的地方:收紧 C 类的最小值,为 A 类留出余量,把 D 类标记为排除在补货计算之外,直到它再次开始移动。
5. 加权日需求:为什么 12 月和 1 月权重更大
把产品分类还不够;还需要估算每个产品在下一期间会卖出多少,并且用手头材料能达到的合理精度来做,通常是每个商品页面和每个 SKU 的 12 到 24 个月历史销售。加权日需求(WDD)就是把这份历史转化为一个可运营数字的引擎。想法很简单:取每个历史月份的日均销售,乘以一个可配置的权重,求和并归一化。概念公式如下:
WDD = Σ (月销售 × 月权重) / Σ (月权重 × 月天数)
关键词是可配置的权重。在墨西哥的汽车行业,12 月和 1 月不是任意的月份:12 月集中了年终奖和礼物推动的购买,1 月接受了人们拖延了一整年的《待办维修》的反弹。一个不加权的简单平均值会掩埋这种季节性,产生一个旺季偏低的最小值和一个淡季偏高的最大值。权重是系统的参数,不是硬编码的常数;当业务发生变化时,比如提前的营销活动,海关问题,渠道迁移,经理会调整它们。
WDD 与产品类别的另外两个参数结合起来,生成最小/最大对:最小库存天数和最大库存天数。用 WDD 乘以这些天数就得到以件数表示的数量。A 类产品通常带较大的最小值,接受资金被占用作为不让畅销品缺货的代价;C 类产品带较紧的最小值,以免用周转慢的零件压库存。按类别的天数配置一次性定义,适用于整个目录;对单个产品的额外自定义有意缺席,因为它会开启不一致标准的大门。
flowchart LR H[(按月的
销售历史)] --> V1[1 月销售] H --> V2[2 月销售] H --> V3[...] H --> V12[12 月销售] P[(按月的
可配置权重)] --> M[加权
月销售 x 月权重] V1 --> M V2 --> M V3 --> M V12 --> M M --> S[加权求和] P --> N[归一化
sum 权重 x 天数] S --> DDP[[加权日需求]] N --> DDP F[(ABCD 类别
最小和最大天数)] --> R[Min = WDD x min 天数
Max = WDD x max 天数] DDP --> R R --> O[(stock.warehouse.orderpoint
product_min_qty product_max_qty)]
6. 两个孪生算法以及它们为什么互相关联
从 SKU 与商品页面之间的多对多关系,得出一个不可避免的架构结论:需要两个计算,不是一个。第一个按商品页面运作,用来决定向 MercadoLibre Full 发多少库存。第二个按 SKU 运作,用来决定向供应商下多少订单。诚实的引擎按这个顺序执行它们,而不是相反。
按商品页面的算法取每个活跃商品页面的历史需求,通过 WDD 处理,再乘以该类别的最小和最大库存天数。为每个商品页面返回一对数字。因为一个商品页面不接受小数,不能向 Full 发 3.8 件,而是发 4 件,算法在每一行都向上取整。这个取整在运营上是正确的:宁可每个商品页面多一件,也比因为小数丢一单好。但当向上汇总时会引入一个静默偏差。
按 SKU 的算法取每个商品页面最小值的总和作为需要支撑的可用库存下界,加上非 Full 渠道(自提销售,实体店,其他市场平台)的估计需求,计算向供应商下的订单数量。如果这个计算的第二部分忽略了第一部分向上取整的累积,向供应商的订单就会系统性地少下正好是运营支撑 Full 发货所需的那部分。这个错误在日常运营中表现为没人能解释的季节性缺货,每两个月《解决》一次紧急订单。精化中的引言把它说得很清楚:
"那些因为取整多加出来的,最后你还是得买。它们得从你的第一批补货里出来。从中国。"
架构上的结论是两个算法不是独立的组件:它们是一个管道。第一个的中间状态,累积的取整,喂给第二个。在定制模块中,它们被实现为同一个引擎中两个方法,保证执行顺序;最初把它们分离为独立微服务的尝试在精化阶段被放弃,原因不是基础设施,而是正确性。
flowchart TB
subgraph A1[算法 A - 按 Full 商品页面]
P1[按商品页面的需求] --> D1[商品页面 WDD]
D1 --> F1[类别 最小/最大天数]
F1 --> Q1[按商品页面的数量]
Q1 --> R1[按行向上取整]
R1 --> E1[[按商品页面发货到
MercadoLibre Full]]
end
subgraph A2[算法 B - 按 SKU 向供应商]
S1[每商品页面最小值之和] --> C1[按 SKU 的下界]
N1[非 Full 渠道需求] --> C2[按 SKU 的汇总需求]
C1 --> C2
C2 --> R2[向供应商的订单数量]
end
R1 -. 需要补足的
累积取整 .-> S1
R2 --> PO[[采购订单草稿
通过 Odoo 原生]]
7. 关键架构决策:赋能 Odoo 原生补货计划器
两个算法定义好之后,剩下一个根本问题:系统用它产生的数字做什么?天真的答案,也是几乎任何团队默认会选的,就是在定制模块内部构建一个新的采购引擎:直接创建采购订单,生成内部调拨,管理状态。这个决定复制了 Odoo 已经解决的功能,为库存创造了两个真相源,把未来每次版本迁移变成一次考古问题。
我们做的决定是相反的,也是解决方案的核心:定制模块不采购,不调拨,不下单;它把补货水平写入 Odoo 原生补货点(stock.warehouse.orderpoint),让补货计划器去做它的工作。原生计划器作为独立 cron 运行,读取补货点,检查虚拟库存,即仓库里的加上在途的减去已承诺的,当它检测到虚拟库存降到最小值以下时,就根据为该产品和该仓库配置的路线和补货规则,自动生成采购订单草稿或内部调拨。
这种方法有三个有形的收益。第一是正确性:Odoo 用于采购和调拨的逻辑是社区和 Odoo S.A. 多年打磨的成果;我们不会用几周内写出的新代码超越它。第二是长寿:当 Odoo v20 或 v21 到来时,只要 stock.warehouse.orderpoint 还存在,定制模块就会继续工作,而这个模型是 ERP 的基石,不是边缘产物。第三是与现有运营的整合:补货点出现在 Odoo 标准 UI 中,经理从他已经在用的库存模块中审查它们,生成的采购订单遵循公司已经熟悉的采购审批流程。
值得用否定的方式说清楚:这个定制模块不是并行的 MRP 引擎,不是与 MercadoLibre 的集成,那个阶段,如果业务需要,是可分离的,而且不是《把库存推送到 MercadoLibre》的按钮。它是一个喂入标准接口的水平计算器。其他一切都由 Odoo 完成。正如客户方运营人员在精化会议上说的,在我们先走了长路之后:
"我建议你不要费心去做一个可以推送的东西。我们不需要 MercadoLibre 拥有它。你需要的是给用户展示他要发什么。最重要的是计算,不是连接。"
8. stock.warehouse.orderpoint 作为标准协议
值得停下来看看为什么 Odoo 原生补货点作为一个契约如此稳定。stock.warehouse.orderpoint 模型为每个产品和位置的组合定义了四个运营字段:最小数量,最大数量,采购倍数和交货天数。仅此而已。它不决定何时采购;不决定向哪个供应商;不生成订单。它只声明一个期望状态。所有补货机制,_procurement_cron,路线,采购规则,按订单生产,都围绕这个声明状态运转并相应行动。
实际后果是 stock.warehouse.orderpoint 表现得像一个标准接口:任何来源都可以写入它,计划器都会同等对待。从一个定制模块,从经理的手动 UI,从一个批量导入向导或从一个 OCA 社区补货模块写入补货点,产生的下游行为完全相同。系统不知道,也不在乎,是谁把最小值设成了 50;它只知道现在当虚拟库存降到这个标记以下时需要补货。
flowchart TB A[定制补货模块] B[OCA 补货模块] C[经理手动 UI] D[批量导入向导] E[(stock.warehouse.orderpoint)] F([Odoo 原生补货计划器]) G[采购订单草稿] H[内部调拨草稿] A --> E B --> E C --> E D --> E E --> F F --> G F --> H
针对这个接口设计改变了问题的性质。不再问*《我怎么构建一个尊重财税规则,供应商条款,多仓库路线和审批渠道的采购引擎?》,而是问《我怎么产生正确的数字来写入 product_min_qty 和 product_max_qty?》*。第一个问题打开一个几年的项目;第二个问题在几周内可处理。系统的其余部分,正确的,经审计的,已集成的,已经随 Odoo 一起来了。识别出需要构建的东西和需要复用的东西之间的这种不对称,实际上是一个好的实施团队最经济的能力。
9. 端到端流程
概念部分各就各位后,基数正确,ABCD 分类,加权日需求,两个关联的孪生算法,作为标准接口的补货点,完整系统读起来像一场五幕编舞,其中三幕自动化,两幕需要人工介入。
**第一幕,每月。**一个每月 cron 重新计算目录的类别。读取该期间的销售报告,按主产品分组,计算累计价值,从高到低排序,把字母 A 分配给代表 70% 价值的产品,B 分配给下一个 20%,C 分配给 10%,D 分配给没有销售的产品。product.template 中的 family_id 字段保持更新。
**第二幕,每日。**一个每日 cron 执行两个孪生算法。首先用 WDD 和该类别的库存天数计算每个商品页面的最小和最大值,向上取整。然后按 SKU 汇总这些结果,再加上非 Full 渠道的需求,产生主仓库中每个 SKU 的最小和最大值。每一行计算都物化为 replenishment.suggestion 中一条 draft 状态的记录。
**第三幕,人工。**经理打开建议视图。按类别,仓库或 SKU 过滤。将当前值与建议值并排比较,视图并排显示它们,然后决定:批量批准,选择性批准或丢弃。系统不强制接受全部;经理的判断是显式的可审计的。
**第四幕,自动。**批准时,replenishment.suggestion 记录变为 done,并把值写入对应的 stock.warehouse.orderpoint。如果补货点不存在,就创建它。定制模块的工作到此结束。
**第五幕,原生。**Odoo 的 _procurement_cron 像往常一样运行,找到更新的补货点,计算虚拟库存,并根据为每个产品配置的路线生成采购订单或内部调拨草稿。采购团队从他们已经在用的模块中确认这些 RFQ。仓库团队从库存模块中验证调拨。没有新东西需要学习。
flowchart LR A([每月 cron]) --> B[在 sale.report 上重新计算 ABCD 类别] B --> C[(product.template.family_id)] D([每日 cron]) --> E[按商品页面的算法 用 WDD] E --> F[按 SKU + 非 Full 渠道的算法] F --> G[(replenishment.suggestion state=draft)] G --> H[经理审查并批准] H --> I[(stock.warehouse.orderpoint)] I --> J([_procurement_cron Odoo 原生]) J --> K[采购订单草稿] J --> L[内部调拨草稿]
10. 可审计历史:replenishment.suggestion 与审批流程
让算法的每次执行生成新记录而不是覆盖之前的,这是一个值得明确说明其后果的决策。replenishment.suggestion 模型是事务性的:每一行捕获一个时刻,哪个产品,哪个位置,当前最小和最大值,建议的最小和最大值,计算的需求,哪个类别,哪个用户在什么时候批准。什么都不丢失。下一个季度一个不舒服的变化可以追溯到批准的确切那一天,带着授权它的经理的电子签名。
状态有四个,语义严格。每日 cron 执行时,每条建议诞生为 draft。经理决定应用它时把它移到 approved;系统在写完对应的补货点后自动把它移到 done。或者,经理可以直接把它移到 rejected,可以留一个注释说明为什么。这个被拒绝的记录也保留下来,一条被丢弃的建议往往和被接受的一样有信息价值,因为它指出算法在哪里输给了人工判断以及缺少哪些信号。
stateDiagram-v2 [*] --> draft : 每日 cron 提议 draft --> approved : 经理批准 draft --> rejected : 经理带注释丢弃 approved --> done : 写入 stock.warehouse.orderpoint done --> [*] rejected --> [*]
这份历史履行了两个通常在 ERP 生命周期后期才被提出的功能。第一是合规:在有年度审计的业务中,财税,ISO 质量,苛刻供应商,谁在什么时候决定了什么的记录是要求,不是装饰。第二是持续改进:积累几个月的建议对批准记录,允许根据数据计算,在哪些类别算法命中,在哪些经理系统性地纠正它。这个信号是用证据而不是直觉调整 WDD 权重或按类别库存天数的原材料。
11. 迁移到 Odoo v19 时改变了什么
定制模块最初是在 Odoo v18 上构建的,几个月后,陪同客户迁移到了 Odoo v19。这是一个复查一个设计在 ERP 版本变化下能存活多少的好实验室。简短答案:定制模块中尊重标准接口的部分无痛迁移;耦合到内部模型的部分总是被迫复查。
完好幸存的是全部计算逻辑。两个孪生算法,加权日需求,ABCD 分类,replenishment.suggestion 模型和编排管道的 cron,都无需实质调整就过渡到了 v19。原因很简单:它们读取 sale.report(从 v14 起稳定的模型),写入 stock.warehouse.orderpoint(ERP 的核心契约),不依赖补货计划器的内部细节。与 Odoo 的接触面按设计位于边缘。
需要复查的确实是两个可预见的点。一,向导和建议表单的 XML 视图:v19 演化了视图中的属性,一些 widget 行为不同;需要调整声明,不是重写。二,product.template 扩展中一个调用 Odoo 内部辅助函数的低层方法,其签名变了;替换为等效的公开 API,其余照旧。没有结构性变化。
附带的收益是 Odoo Spreadsheet,它在 v19 中成熟到足以作为 replenishment.suggestion 之上的分析层,无需导出到 Excel。经理可以在 ERP 内部建立针对建议历史的数据透视表,与类别交叉并可视化。
一个背景注记:在本文写作时,与客户的合作已经结束。定制模块留在了他们的实例中,他们获得了代码和运营的所有权;Transgenia 不再维护或支持它。本文记录设计和方法,而不是一个活跃的服务。仍然成立的教训是:一个针对 Odoo 稳定契约而不是内部结构设计的定制模块,即使在没有最初编写者的情况下,也能干净地在版本之间迁移。
12. 这些内容有多少可以复用到 Amazon FBA,Shopify,Prestashop
尽管这个案例发生在 MercadoLibre Full,但设计的本质部分没有一个依赖于 MercadoLibre。SKU 与商品页面之间的一对多关系存在于任何允许同一产品出现在多个广告中的市场平台;集成物流中的向上取整存在于任何 FBA 类型的 fulfillment。因此,架构可以通过较小的调整迁移。
Amazon FBA 是最明显的兄弟案例。等价关系几乎是点对点:ASIN 承担商品页面的功能(有自己需求的销售容器),SKU 仍然是物理产品,分配给 FBA 的库存类似于 Full 库存,ASIN 与 SKU 之间的关系也是一对多。两个孪生算法完全相同;只改变历史需求的来源(Amazon Sales Reports 而不是 MercadoLibre)和词汇。一个基于这个模式设计的 Odoo 定制模块可以通过切换来源连接器服务两个市场平台,而无需触及逻辑。
Shopify 和 Prestashop 是稍微不同的案例,因为它们的基础模型不是集成式的:除非你用 3PL 服务,否则店铺就是你自己的仓库。在那里按商品页面的算法变得简单(一个店铺 = 一个渠道,没有同一 SKU 的多个商品页面),但按 SKU 的部分仍然有效,如果你同时在 Shopify 加上多个市场平台(Amazon,MercadoLibre,eBay)上运营,价值就更高。模式不再是《解决多对多的问题》,而是《针对单一主库存编排多渠道补货》。定制模块的结构不变;变的是商品页面表填得有多满。
不能不经真实工作就迁移的是加权日需求的参数化。月权重是业务的文化产物:年终奖,Buen Fin,汽车维修季节。把同一个权重向量迁移到运动服装电商或 B2B 分销会产生糟糕的估算。运营规则是:公式可以旅行,权重不行。任何复用的第一个交付物应该是在新环境中用至少 12 个月的历史校准权重。
13. 我们一路上纠正的设计错误
定制模块在三轮明确的针对代码的精化之后才发布,不是针对需求的。每一轮都发现了那种不产生可见 bug 但老化很糟的设计错误。把它们记录下来,可以作为面对类似项目的人的工具箱。
**第一个错误:family_id 在 product.product 而不是 product.template。**第一版把 ABCD 类别分配给了变体,而不是主产品。在有按尺寸或颜色变体的目录中,这意味着同一个零件可能红色被分类为 A,蓝色被分类为 C,补货水平不一致。修正是把字段移到 product.template,并在重新计算方法内部,在写入之前从变体升级到父级。可迁移的规则:在与 sale.report(按 product_id 分组)集成时,不要把变体与主产品混淆。
**第二个错误:cron 的语义所有者错了。**每月重新计算类别的 cron 最初挂在 product.family 模型下,因为方法住在那里。它能工作,但令人困惑:cron 修改的是 product.template,不是 product.family。当有新人进入代码时,需要花几分钟理解为什么 cron 在它现在的位置。修正是把 cron 移到 product.template(被修改的模型)之下,让它内部调用 product.family 的方法。功能零变化;对下一个开发者的可读性收益很大。
**第三个错误:order_id 作为 marketplace.publication 的字段名。**在为 MercadoLibre 的商品页面建模时,第一个提案把保存商品页面标识符的字段命名为 order_id。问题在于 order_id 在 Odoo 中在语义上与 sale.order.id 冲突,这会诱发混淆以及在 SQL 查询期间意外的 join。修正是把字段重命名为 publication_key:技术性的,唯一的,不可能冲撞。规则:当你与外部平台集成时,在你的模型中尊重 Odoo 的词汇,用 <channel>_ 前缀标识外部标识符。
**第四个错误:marketplace.publication 中 location_id 可选。**把商品页面链接到库存位置的字段在第一版中被建模为可选。实际上它必须总是被填充,因为按商品页面的算法需要知道在哪里测量可用库存。修正是在模型层面标记它为强制(required=True),并添加一个前置验证,在没有位置的商品页面到达每日 cron 之前拒绝它们。规则:如果一个字段在算法中是必需的,就在模型中把它设为必需;不要把验证留给应用代码去负责。
这四个错误在最初的手工测试中都不产生可见故障。这四个错误都会在六到十二个月后作为昂贵的问题出现,那时目录会已经增长,另一个开发者会进入代码,运营会开始依赖系统做重大决策。早期发现它们,在精化中而不是在生产中,是本文今天描述一个干净设计而不是讲一个强制迁移的编年史的唯一原因。
14. 收尾:敏捷精化作为 Odoo 实施的运营方法
如果这个案例有一个要点值得带走,那就是短小且框架良好的对话产生非对称价值。发现多对多的会议比平均状态会议还短。避免四个结构性错误的精化轮次能装进一个下午。没做它们的假设成本以月和不能收回的预算来支付。在一次 Odoo 实施中,项目最有回报的时刻不是写代码的时刻:而是两个人看着同一张表格并问每一列做什么的时刻。
敏捷精化不是一个方法论标签也不是从其他行业继承的仪式。它是已知最经济的机制,用来在方差在代码中物化之前降低一个软件项目的方差。在 Odoo 项目中很少使用是因为可配置 ERP 的隐含承诺(《只需要捕获需求》)产生了一种错觉,认为繁重的认知工作已经解决了。它没有。本文描述的补货设计以它现在的形式存在,是因为有人在所有其他人把答案视为理所当然时问了《为什么》。
如果你正在为一个在市场平台上有存在的业务设计补货,三个决定能帮你省下大部分痛苦:(1) 从第一天起把 SKU 和商品页面建模为具有一对多基数的不同实体;(2) 把水平写入 stock.warehouse.orderpoint,而不是要采购的数量;(3) 先执行按商品页面的算法,再向 SKU 汇总,尊重累积的取整。有了这三条规则和一个诚实的精化方法,项目的大部分变成了平静的工作。
如果你想看 Transgenia 在实践中如何运营这类项目,进入 gated demo(企业邮箱,15 天访问,零承诺)。如果你运营一家 B2B 分销商并想探索类似方法如何应用到你的库存,行业 › 分销商 页面总结了主张。
我以一个本文有意重复且仍然成立的注记收尾:定制模块留在了客户那里。方法论留在 Transgenia,也留给读到这里的人。
常见问题
什么让同一个 SKU 在 MercadoLibre Full 上有多个商品页面?
MercadoLibre 不对物理产品与已发布广告之间强加一对一关系。同一个 SKU 可以出于商业原因存在于不同的商品页面:一个带锚定价,另一个带补贴运费,另一个在 MELI 优选渠道,另一个作为与互补产品的捆绑。每个商品页面分别累积访问,咨询和销售,在 Full 中要求自己的分配库存。
运营上的后果是补货计算不能把 SKU 当作一个单一整体来处理。有两个不同的计算要做:一个按商品页面(向每个 Full 发多少),一个按 SKU(考虑所有商品页面和非 Full 渠道后向供应商下多少)。
为什么用 ABCD 而不是经典的 ABC?
经典 ABC 按累计销售价值把目录里的活跃产品分为三组(70%,20%,10%)。问题是真实的销售报告总有噪音:因录入错误或不是市场销售的内部移动而出现的出货为零的产品。
加上字母 D 允许明确标记评估期内没有销售的产品,把它们排除在补货计算之外。如果你不这么做,这些产品会落到 C 类里带一个正数最小值,强制产生不合理的采购。D 是一个卫生标签,不是一个管理类别。
加权日需求是一个被证明的统计方法吗?
WDD 并不假装是一种新颖的学术技术。它是一个应用于月度销售历史的加权移动平均,权重可配置。它的价值不在于数学的复杂性,而在于它把权重作为系统的参数而不是隐藏在代码内部的常数暴露出来。
这种开放性把经理变成了模型的操作者:当季节性变化时,当有一个预期的商业事件时,或当检测到海关问题时,调整权重,系统就会响应。更精致的方法(指数平滑,ARIMA,神经网络)是有效的,但把对话从业务转移到工程,通常需要更长时间才能收回实施成本。
为什么用定制模块而不是只配置 Odoo 原生?
Odoo 有 OCA 社区补货模块,带静态 MIN/MAX 规则,还有一些带轻度动态规则。我们在写自己的代码之前审查了它们。有三个原因不合适。第一:没有一个建模了 SKU 与市场平台商品页面之间的多对多关系;它们假设补货点总是按产品和按仓库。第二:经理可配置的月权重是汽车业务的需求,在大多数 OCA 中不存在。第三:以 replenishment.suggestion 作为中间步骤的审批链在审查的模块中不存在。
定制模块不重新发明计划器或采购订单的轮子。它精确地提供了缺失的东西:ABCD 分类,两个关联的孪生算法,以及在写补货点之前的可审计建议。其余一切由 Odoo 标准完成。
同样的方法适用于 Amazon FBA 或 Shopify 吗?
是,需要连接器和词汇的调整,不是架构的。模式(SKU 与销售容器之间的多对多,通过取整关联的两个孪生算法,写入 stock.warehouse.orderpoint)是相同的。Amazon FBA 是最直接的案例,因为它与 MercadoLibre Full 的运营相似:ASIN 承担商品页面的角色,FBA 中的库存类似于 Full 库存。
在 Shopify 或 Prestashop 中,除非你用 3PL,按商品页面的算法会简化,因为店铺就是你自己的仓库。但如果你同时在 Shopify 和一个市场平台上运营,多渠道模式恢复其全部价值。唯一不带工作就迁移不了的是 WDD 的月权重:季节性模式是业务的文化产物,需要在新环境中用至少 12 个月的历史校准。