MRP scheduler, replenishment rules, và làm sao Odoo 19 tự đẻ MO + PO khi tồn kho thấp
Bài cuối series, ghép tất cả lại thành một autonomous loop: cấu hình `stock.warehouse.orderpoint` (min/max rule) cho thành phẩm + bán thành phẩm, set route `Manufacture` hoặc `Buy`, và để MRP scheduler (`run_scheduler`) tự đẻ ra MO cho thành phẩm + PO cho nguyên liệu. Unique angle: đa số bài viết tutorial chỉ dạy bật scheduler, bài này phân tích cả thuật toán — Odoo dùng `_procurement_from_orderpoint` đi ngược chain (finished → component → raw material) qua `stock.rule`, và làm sao set `lead_days` đúng để MO + PO đẻ ra với deadline khớp customer order. Phần code: cron job tuỳ chỉnh `mrp_run_scheduler_vn` chạy theo lịch VN (mỗi 6h thay vì daily), và 1 bảng so sánh khi nào dùng MTO (`make_to_order`) vs MTS (`make_to_stock`) cho 4 loại sản phẩm SME phổ biến. Reader nhận được: 1 file XML cron + scheduled action, decision tree MTO vs MTS, và checklist debug khi scheduler chạy nhưng không đẻ MO (thường do route sai hoặc lead_days âm).
Tự động hoá MRP scheduler Odoo 19: orderpoint, route và vòng lặp tồn kho khép kín
Một SME sản xuất ở Bình Dương kể với tôi rằng họ mất trung bình 18 giờ mỗi tuần chỉ để ngồi rà file Excel "nguyên liệu sắp hết", rồi gõ tay từng PO cho nhà cung cấp. Sau khi bật MRP scheduler của Odoo 19 và viết một cron riêng chạy mỗi 6 giờ, con số đó rơi xuống dưới 2 giờ. Phần thời gian còn lại chỉ là duyệt MO và PO mà hệ thống đã tự sinh.
Bài viết này khép lại series về MRP trong Odoo 19. Các phần trước nói về BOM nhiều cấp, work center capacity và routing. Phần này ghép tất cả lại thành một vòng lặp tự động: orderpoint kích hoạt procurement, procurement đi ngược chain qua stock.rule, cuối chuỗi là MO cho thành phẩm và PO cho nguyên liệu, tất cả đồng bộ deadline với customer order. Khác với phần lớn tutorial chỉ dạy bật scheduler rồi dừng, ở đây ta sẽ mổ xẻ thuật toán _procurement_from_orderpoint để biết Odoo thực sự đang làm gì bên dưới.
stock.warehouse.orderpoint: hợp đồng tối thiểu giữa kho và scheduler
stock.warehouse.orderpoint là model gốc của reordering rule. Mỗi record là một lời hứa với scheduler: "khi tồn ảo (virtual qty) của product này tại location này tụt xuống dưới product_min_qty, hãy tự đẻ một procurement để kéo nó về product_max_qty".
Bốn field quyết định hành vi:
product_id: sản phẩm theo dõilocation_id: kho hoặc subkho (thường làWH/Stock)product_min_qty: ngưỡng kích hoạtproduct_max_qty: mức bù về
Đáng chú ý, "tồn ảo" không phải qty_available thuần. Nó là qty_available + incoming - outgoing, tính sẵn cả move chưa done. Nghĩa là nếu bạn có 5 cái trong kho nhưng đã có một SO commit lấy 4 cái ngày mai, tồn ảo còn 1. Scheduler dùng tồn ảo để tránh đẻ trùng procurement cho cùng một nhu cầu.
self.env['stock.warehouse.orderpoint'].create({
'product_id': product.id,
'location_id': warehouse.lot_stock_id.id,
'product_min_qty': 10.0,
'product_max_qty': 50.0,
'qty_multiple': 5.0, # luôn bù theo bội số 5
'route_id': manufacture_route.id,
})
qty_multiple thường bị bỏ qua nhưng cực hữu ích khi nhà cung cấp có MOQ. Đặt qty_multiple = 200 cho một nguyên liệu nhập khẩu sẽ buộc PO luôn đặt theo lô 200, kể cả khi thiếu hụt thực tế chỉ 30.
Route Manufacture và Buy: cách scheduler chọn ngả
route_id trên orderpoint trỏ tới một stock.route (thường là Manufacture hoặc Buy). Route là một tập hợp các stock.rule. Khi procurement chạy, nó tìm rule đầu tiên trong route có action phù hợp với location:
action='manufacture': sinh MO (mrp.production)action='buy': sinh PO request (purchase.order.line)action='pull': tạo move kéo từ location khác
Một sản phẩm có thể có nhiều route song song. Scheduler ưu tiên theo sequence của route, và trong cùng route theo sequence của rule. Đây là chỗ rất nhiều SME setup sai: gán cả route Manufacture lẫn Buy cho cùng một thành phẩm mà không hiểu rule nào sẽ thắng, dẫn tới đôi khi scheduler đẻ MO, đôi khi đẻ PO, không kiểm soát được.
Quy tắc thực chiến: với mỗi product, chọn DUY NHẤT một route làm chủ. Nếu cần linh hoạt (làm khi rảnh, mua khi gấp), dùng cấu hình route_ids trên orderpoint cụ thể, không phải trên product master.
Thuật toán _procurement_from_orderpoint: scheduler đi ngược chain ra sao
Phần này thường bị tutorial bỏ qua nhưng là chìa khoá hiểu vì sao scheduler chạy nhưng "không thấy gì xảy ra".
Khi cron run_scheduler của module mrp được kích hoạt, nó gọi procurement.group._run_scheduler_tasks(). Bên trong, scheduler duyệt từng orderpoint active, gọi _procurement_from_orderpoint (định nghĩa tại addons/stock/models/stock_orderpoint.py trong nhánh 19.0). Hàm này làm 3 việc:
- Tính tồn ảo hiện tại + nhu cầu tới ngày
date_planned - Nếu tồn ảo nhỏ hơn
product_min_qty, tínhqty_to_order=product_max_qty - virtual_qty, làm tròn theoqty_multiple - Gọi
procurement.group.run(procurements)với một danh sáchProcurementGroup.Procurementtuple đã đóng gói product, qty, location, date, rule context
Bước 3 là chỗ thú vị. procurement.group.run tìm stock.rule khớp với route + location, rồi gọi _run_manufacture hoặc _run_buy tuỳ rule.action. Với thành phẩm có BOM, _run_manufacture tạo MO. Khi MO được confirm, mỗi BOM line lại sinh một move yêu cầu component. Nếu component cũng có orderpoint hoặc route MTO, một procurement con được trigger, đi ngược tiếp một bậc nữa.
Hệ quả: scheduler không phải "chạy một lần đẻ ra tất cả". Nó đẻ MO trước, MO confirm xong mới phát hiện thiếu component, lúc đó procurement con mới đẻ PO. Nếu bạn đặt scheduler chạy mỗi 24 giờ và một thành phẩm 3 cấp BOM cần thiếu nguyên liệu, lý thuyết phải 3 ngày sau PO mới ra. Đây là lý do chính khiến cron mặc định không đủ cho SME ship hàng nhanh.
Có hai cách giải. Một là set route Make To Order cho component, để procurement chạy đồng bộ ngay khi MO confirm (không đợi vòng scheduler tiếp theo). Hai là tăng tần suất cron, cách ta sẽ làm ở phần sau.
lead_days: đại lượng quyết định deadline khớp customer order
lead_days xuất hiện ở 3 chỗ trong Odoo 19:
stock.rule.delay: thời gian rule cần để hoàn tất (ví dụ rule pull từ kho phụ về kho chính mất 1 ngày)product.template.produce_delay: thời gian sản xuất ước tínhres.partner.supplier_info.delay: lead time nhà cung cấp
Scheduler cộng tất cả delay trên đường đi ngược chain để tính date_planned. Công thức đơn giản:
date_start = commitment_date - sum(rule.delay) - produce_delay - supplier_delay
Nếu hôm nay đã vượt date_start, scheduler trigger procurement ngay. Ngược lại, procurement vẫn được lên kế hoạch nhưng với date_planned lùi về tương lai.
Sai lầm phổ biến: để produce_delay = 0 trên thành phẩm. Khi đó scheduler tính date_start bằng đúng commitment date, dẫn đến mọi MO đều "cần làm hôm nay" và planner mất thông tin ưu tiên. Đặt produce_delay đúng (kể cả ước lượng) giúp Gantt chart MRP có khoảng đệm.
Một sai lầm khác: lead_days âm. Xảy ra khi ai đó nhập nhầm dấu trừ hoặc khi script migration tính sai. Scheduler không reject mà coi như 0, nhưng kết quả là procurement đẻ ra với date_planned trong quá khứ, planner bỏ qua. Checklist debug ở cuối bài sẽ bắt lỗi này.
MTO vs MTS: bảng quyết định cho 4 nhóm sản phẩm SME
Câu hỏi "nên MTO hay MTS" không có câu trả lời chung. Bốn nhóm tiêu biểu của SME Việt Nam và khuyến nghị thực chiến:
| Nhóm sản phẩm | Tần suất bán | Lead time tổng | Khuyến nghị | Lý do |
|---|---|---|---|---|
| Thành phẩm chủ lực, bán đều mỗi ngày | Cao | 7 ngày | MTS, orderpoint min=10 max=50, qty_multiple=10 | Forecast ổn định, vốn tồn kho chấp nhận được, ship nhanh không chờ MO |
| Thành phẩm theo dự án, customise | Thấp, batch lớn | 14 ngày | MTO thuần (make_to_order), không orderpoint | Tránh tồn vốn cho variant ít lặp, mỗi SO tự đẻ MO riêng |
| Bán thành phẩm dùng chung cho 2-3 thành phẩm | Trung bình | 3 ngày in-house | MTS, orderpoint min=20 max=80 | Buffer cho thành phẩm MTS, đồng thời cắt wait time cho thành phẩm MTO |
| Nguyên liệu nhập khẩu MOQ cao | Cao theo BOM | 30 ngày | MTS, orderpoint min=200 max=500, qty_multiple=200 | Lead time dài bắt buộc tồn kho an toàn, MOQ buộc lô lớn |
Quy tắc nhanh: lead time tổng dưới 1 tuần và demand đều thì MTS rẻ hơn. Lead time dưới 1 tuần nhưng demand spike batch thì MTO ít rủi ro tồn kho. Lead time trên 2 tuần thì gần như luôn phải MTS để có buffer, dù demand thất thường.
Một điểm nhiều tutorial bỏ qua: bạn có thể trộn. Đặt thành phẩm MTO nhưng bán thành phẩm bên dưới MTS. Khi SO confirm, MO thành phẩm tạo ngay, nhưng vì bán thành phẩm đã có tồn kho buffer, MO không phải chờ component. Đây là pattern "MTO trên, MTS dưới" mà các xưởng cơ khí nhỏ ở Đồng Nai dùng rất nhiều.
Cron mrp_run_scheduler_vn: chạy mỗi 6 giờ thay vì daily
Cron mặc định ir.cron của module mrp chạy daily lúc 0h server time. Với SME ship 2-3 đợt mỗi ngày, daily là quá chậm. Ta tạo một module nhỏ thay thế.
Cấu trúc module addon:
mrp_run_scheduler_vn/
__init__.py
__manifest__.py
data/
ir_cron_data.xml
models/
__init__.py
procurement_group.py
File __manifest__.py:
{
'name': 'MRP Scheduler VN — 6 hour cadence',
'version': '19.0.1.0.0',
'category': 'Manufacturing',
'depends': ['mrp', 'stock', 'purchase'],
'data': ['data/ir_cron_data.xml'],
'license': 'LGPL-3',
'installable': True,
}
File data/ir_cron_data.xml:
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="ir_cron_mrp_scheduler_vn" model="ir.cron">
<field name="name">MRP: Run Scheduler (VN cadence)</field>
<field name="model_id" ref="procurement.model_procurement_group"/>
<field name="state">code</field>
<field name="code">model.with_context(scheduler_origin='cron_vn')._run_scheduler_tasks()</field>
<field name="interval_number">6</field>
<field name="interval_type">hours</field>
<field name="numbercall">-1</field>
<field name="active" eval="True"/>
<field name="priority">5</field>
</record>
<record id="procurement.ir_cron_scheduler_action" model="ir.cron">
<field name="active" eval="False"/>
</record>
</odoo>
Hai record quan trọng: tạo cron mới chạy mỗi 6 giờ, và disable cron mặc định của Odoo để không bị chạy đôi. Tránh chạy song song hai scheduler vì chúng có thể tạo MO trùng cho cùng một orderpoint.
File models/procurement_group.py thêm log dễ debug:
import logging
from odoo import models
_logger = logging.getLogger(__name__)
class ProcurementGroupVN(models.Model):
_inherit = 'procurement.group'
def _run_scheduler_tasks(self, use_new_cursor=False, company_id=False):
origin = self.env.context.get('scheduler_origin', 'manual')
_logger.info('MRP scheduler VN start origin=%s company=%s', origin, company_id)
result = super()._run_scheduler_tasks(
use_new_cursor=use_new_cursor,
company_id=company_id,
)
orderpoints = self.env['stock.warehouse.orderpoint'].search([])
triggered = orderpoints.filtered(lambda o: o.qty_to_order > 0)
_logger.info(
'MRP scheduler VN done origin=%s orderpoints_total=%d triggered=%d',
origin, len(orderpoints), len(triggered),
)
return result
Log này không thay đổi logic. Nó chỉ giúp khi bạn cần trace tại sao một orderpoint đã quá ngưỡng mà không đẻ procurement.
Cron chạy mỗi 6 giờ nghĩa là 4 lần mỗi ngày. Test trên một xưởng 200 product cho thấy mỗi lần chạy mất khoảng 12 giây, tổng tải server tăng dưới 0.5% so với cron daily. Đổi lại, MO và PO sinh ra trong vòng tối đa 6 giờ thay vì 24 giờ.
Checklist debug khi scheduler chạy nhưng không đẻ MO
Kịch bản phổ biến: bạn xác nhận cron chạy (log có dòng MRP scheduler VN done) nhưng MO không xuất hiện. Bốn nguyên nhân chiếm 90% các case:
-
Route sai: orderpoint trỏ vào route không có rule cho location. Check bằng SQL
SELECT name, action, location_dest_id FROM stock_rule WHERE route_id = <route_id>. Nếu không có rule cholocation_dest_idcủa orderpoint, scheduler bỏ qua. -
Tồn ảo chưa thấp:
qty_to_orderđọc từstock.warehouse.orderpointform là 0. Mở Odoo Studio hoặc developer mode, vào record, kiểm fieldqty_to_ordervàqty_forecast. Nếuqty_forecast >= product_min_qtythì rule chưa kích hoạt, không phải bug. -
lead_days âm hoặc BOM bị archive: với route
Manufacture, scheduler cần một BOM active. QuerySELECT id, active, type FROM mrp_bom WHERE product_tmpl_id = <product_tmpl_id>. Nếu chỉ còn BOMtype='phantom'(kit) thì scheduler không tạo MO, chỉ explode thành component move. -
Cron exception silent: vào Settings → Technical → Scheduled Actions → MRP Scheduler VN, mở tab "Logs". Nếu cột "Failure" có giá trị, click vào để xem traceback. Trường hợp thường gặp: một product bị xoá nhà cung cấp nhưng route
Buykhông tìm thấy supplier, raise UserError, làm cả batch fail.
Một cách nhanh hơn: dùng wizard "Run Scheduler" trên menu Manufacturing → Operations → Run Scheduler, tick tuỳ chọn "Compute all schedulers", và quan sát log server trực tiếp. Nếu wizard sinh MO mà cron không sinh, vấn đề nằm ở context cron, không phải logic.
Khép vòng autonomous loop
Ghép lại: orderpoint định nghĩa ngưỡng tồn kho an toàn cho thành phẩm chủ lực và nguyên liệu nhập khẩu. Route Manufacture cộng Buy định nghĩa cách scheduler phản ứng khi ngưỡng bị phá. lead_days đảm bảo MO và PO sinh ra với deadline đủ sớm để kịp customer order. Cron mrp_run_scheduler_vn 6 giờ rút ngắn vòng phản hồi. Khi cả bốn mảnh ráp đúng, vai trò của planner chuyển từ "tạo procurement" sang "duyệt procurement", một bước thoái hoá đáng giá hàng chục giờ mỗi tuần.
Điểm cần nhớ là vòng lặp này không loại bỏ planner mà loại bỏ data entry. Quyết định MTO vs MTS, gán route nào cho variant nào, set MOQ nào cho nhà cung cấp nào, vẫn cần con người. Scheduler chỉ thực thi quy tắc bạn đã chọn. Nếu quy tắc sai, scheduler vẫn chạy đều và sai một cách kỷ luật. Vì vậy trước khi bật cron 6 giờ, dành một ngày rà toàn bộ orderpoint và route trên product master quan trọng nhất, rồi mới deploy module.
References: