DUE TO SPAM, SIGN-UP IS DISABLED. Goto Selfserve wiki signup and request an account.
The situation
A 3PL operator runs a warehouse and ships goods on behalf of merchant clients. The 3PL:
- does not sell the goods (the merchant already sold them),
- does not invoice the buyer or take payment,
- only needs to receive, store, pick, pack, ship, and report tracking.
The trap (why the obvious path fails)
OFBiz's out-of-the-box "ship an order" path (quickShipEntireOrder) assumes a sales order. As soon as a SALES_SHIPMENT advances to SHIPMENT_PICKED (and again at SHIPMENT_PACKED, which also marks it ready), OFBiz automatically creates a customer invoice (SALES_INVOICE) — and from there it can auto-create a payment and march the order through to-cash.
That is correct for a store. It is wrong for a 3PL. The invoice trigger is an ECA that fires for any shipment of type SALES_SHIPMENT, and — unlike auto-payment — there is no config property to turn it off.
The rule
Don't use the sales types. Model fulfillment with dedicated non-sales types.
| Use this | Not this |
|---|---|
OrderType = FULFILLMENT_ORDER (standalone) | SALES_ORDER |
ShipmentType = FULFIL_SHIPMENT | SALES_SHIPMENT |
FULFILLMENT_ORDER must be standalone — do not declare it as a child of SALES_ORDER, or sales-typed logic re-attaches through parent-type checks.
Why it works
OFBiz's billing fires from ECAs gated on the sales types. Give it a non-sales type and the triggers simply never match:
| Automation | Fires when… | With FULFILLMENT_ORDER? |
|---|---|---|
| Auto-invoice on ship | shipment is SALES_SHIPMENT | Never fires |
| Auto-payment | order is SALES_ORDER | Never fires |
| GL posting | party is an internal organization | Self-skips |
Result: zero order-to-cash — no invoice, no payment, no accounting postings. By construction, not by patching.
The cost, and how to pay it
The stock warehouse pipeline is sales-coupled: findOrdersToPickMove expects a SALES_ORDER, and quickShipEntireOrder branches its logic on orderTypeId == SALES_ORDER (a FULFILLMENT_ORDER falls into the purchase-order branch and never produces a usable shipment — the shipment is only typed SALES_SHIPMENT when the order is a SALES_ORDER). So you can't reuse the one-call shipping service; you drive the lower-level services yourself, in this order:
- Receive —
receiveInventoryProduct(stocks the shelf; owner = the merchant). - Intake —
storeOrderasFULFILLMENT_ORDER, then approve it. - Reserve —
reserveStoreInventory. - Pick — observe the reservation.
- Track —
createShipmentRouteSegmentbefore shipping (it is rejected once a shipment is shipped). - Ship —
createShipment(FULFIL_SHIPMENT) → issue each item →updateShipmentINPUT → PACKED → SHIPPED(you cannot jump straight toSHIPPED). - Poll — read the order, shipment, and tracking back.
One gap to know: OFBiz has no complete non-sales "issue inventory to shipment" service — the complete one is sales-only. Hand-roll the issuance with createItemIssuance + createInventoryItemDetail, and set availableToPromiseDiff = 0 (the reserve step already lowered ATP — don't subtract it twice, or ATP goes negative).
The tenancy model (many merchants, one warehouse)
All on stock entities — no new tables:
| 3PL concept | OFBiz entity |
|---|---|
| Operator (the 3PL) | PartyGroup |
| Warehouse | one Facility (type WAREHOUSE), owned by the operator |
| Merchant client | PartyGroup |
| Merchant's account at the 3PL | ProductStore, with payToPartyId = the merchant |
| Which warehouse serves that account | ProductStoreFacility |
| API access grant | ProductStoreRole (role FULFILL_API_CLIENT) |
| Order isolation | OrderHeader.productStoreId |
| Commingled stock | InventoryItem.ownerPartyId = the merchant |
So orders stay isolated per store, while every merchant's inventory sits in the same warehouse, told apart only by owner. Onboarding a new client is just repeating this block of data — no code.
Accounting preferences you still need
Even though nothing gets invoiced, two stock behaviors still read accounting prefs, so give each merchant one PartyAcctgPreference:
- COGS at issuance — issuing inventory computes cost using the merchant's
cogsMethodId(e.g.COGS_AVG_COST). - BI facts — the
biplugin loads order facts on approval and needsbaseCurrencyUomId.
Set both on the PartyAcctgPreference, or disable the bi plugin if you don't want fact-loading.
In one line
A 3PL ships; it doesn't sell. Type the order as FULFILLMENT_ORDER and OFBiz stops trying to bill — everything else is just driving the warehouse services in order.