Hi all,
I’ve almost published a small module for a case that kits/BOMs don’t cover: products you
sell as one article but assemble per order.
A “server” consists of a monitor, a cpu and a switch — but which monitor depends
on stock and on what the customer wants. A virtual product (PRODUIT_SOUSPRODUITS)
needs a fixed component list, so that doesn’t fit.
What it does
On the composed product you define composition rules per product category
(“1 from Monitors, 1 from Keyboards”) instead of a fixed BOM. The concrete
articles are chosen in the draft order, and are added as real order lines below
the composite line via fk_parent_line. Price stays on the parent, stock runs
through the children.
The composed product is a service, so Dolibarr books no stock on it and it never
reaches the shipment. Only the components do.
Why at order entry, not at shipment
This was the main design decision, and it’s worth flagging for anyone attempting
something similar. Expedition::valid() calls manageStockMvtOnEvt() at line ~856,
before call_trigger(‘SHIPPING_VALIDATE’), and that query reads cd.fk_product from
commandedet — not ed.fk_product from expeditiondet. So you cannot redirect the
stock movement from a trigger there; the best you get is booking wrong and rolling
back. Resolving the composition one step earlier removes the problem instead of
working around it.
The consequence is everything after order validation is untouched core: shipment,
stock movements, batch/serial, delivery note, invoicing, returns. Barcode picking
on a handheld just sees ordinary product lines — no connector changes needed.
Other notes
- ORDER_VALIDATE is blocked while a mandatory rule is unfulfilled. No stock has
been booked at that point, so nothing needs rolling back. - One setting governs component-line visibility across order card, order PDF,
invoice card and invoice PDF at once — deviating per place is deliberately not
possible. Screen goes through printObjectSubLine, PDF through
commonGenerateDocument (pdf_writelinedesc only replaces the description, not the
line). Facture::createFromOrder() copies special_code and fk_parent_line, so the
invoice side needs no extra code. - Components may be chosen a few at a time; completeness is only enforced at
validation.
Dolibarr 20+, PHP 7.4+, GPLv3, EN/NL. Requires STOCK_SUPPORTS_SERVICES and
SHIPMENT_SUPPORTS_SERVICES to be off; the setup page checks both.
Feedback welcome, especially from anyone who has hit the same
sell-abstract/ship-concrete problem and solved it differently.




