$The MTO inventory paradox
For a standard-inventory e-commerce business, "how much do I have" is a straightforward question. Odoo's product.free_qty (or its older sibling qty_available) gives you the answer, and pushing that number to Amazon is the right thing to do.
For a made-to-order business, the question doesn't make sense. The product doesn't exist until someone orders it. There are no units sitting in a warehouse. The relevant inventory question is "how many can I make in the production lead time," and the answer is usually "as many as anyone orders." Bounded by capacity, not by stock.
Amazon doesn't have a concept of "inventory = my production capacity." Amazon has a concept of "this many units are buyable right now." For an MTO seller, both numbers are technically zero (no stock) and effectively infinite (unbounded production). The convention that works: tell Amazon you have 999 units of every listing. This is large enough that Amazon won't run you out before you can fulfill new orders, and small enough that Amazon doesn't flag the listing as suspicious.
The pattern in Odoo:
product.product.free_qty= 0 (nothing in stock)product.product.qty_available= 0 (nothing in stock)marketplace.listing.qty= 999 (advertise to Amazon as infinite)
The two qty fields disagree on purpose. The product master reflects reality (no inventory). The marketplace listing reflects the message you're sending to the marketplace.
$The bug pattern: connector reads product.free_qty
A naive Odoo + Amazon connector wires inventory sync as:
# Wrong for MTO businesses.
def sync_inventory(env, listing):
product = listing.product_id
amazon_qty = int(product.free_qty)
sp_client.put_listing_attribute(
seller=listing.seller_id,
sku=listing.amazon_sku,
attribute="fulfillment_availability",
value=[{"quantity": amazon_qty, "lead_time_to_ship_max_days": 10}],
)
For a standard-inventory business: correct. free_qty tracks reality, gets pushed to Amazon, Amazon shows the listing as in-stock for that quantity.
For an MTO business: catastrophic. free_qty is 0 by design because production hasn't run. The connector pushes quantity=0 to Amazon. Amazon suppresses the listing with "out of stock." Sales stop. The on-call channel goes quiet because there are no orders to process. The seller looks at the Seller Central dashboard, sees "out of stock," and asks the engineering team why inventory isn't syncing, when in fact it's syncing perfectly to the wrong field.
The pattern is especially bad for MTO businesses with seasonality. The pattern catches you mid-launch: you announce a new product, listings go live, the first sync cycle fires free_qty=0 and Amazon suppresses everything before anyone has ordered.
$The fix: read the listing's own qty
The corrected sync reads from the marketplace listing model, not from the product master:
# Correct for both standard inventory AND MTO.
def sync_inventory(env, listing):
# Read from the listing-specific qty field, not product.free_qty.
amazon_qty = int(listing.qty)
sp_client.put_listing_attribute(
seller=listing.seller_id,
sku=listing.amazon_sku,
attribute="fulfillment_availability",
value=[{
"quantity": amazon_qty,
"lead_time_to_ship_max_days": listing.product_id.sale_delay or 10,
}],
)
Two things change:
- Quantity source:
listing.qtyinstead ofproduct.free_qty. For MTO businesses, this is 999 (or whatever the per-listing override is). For standard businesses, this can be synced fromproduct.free_qtyvia a separate cron or set manually. - Lead time:
product.sale_delayinstead of a hardcoded 1 or 2 days. Critical for MTO. See the lead-time-wipe post for why getting this right matters.
If your Odoo schema doesn't have a marketplace.listing.qty field separately from the product, add it. It's a simple integer with a default of 999. Set the default to 999 for any product flagged as MTO; set it to mirror product.free_qty for non-MTO products.
$Why qty=999 specifically
Three numbers people consider:
- 0: wrong for MTO (suppresses listing).
- 1: wrong for MTO at any scale. Every order needs the listing to be re-incremented before the next order. Not robust to concurrent orders.
- 999: right for most MTO businesses. Customer never sees "only X left in stock!" type messaging (which fires below 20 on Amazon). Large enough that you don't run out between sync cycles.
- 99,999: too large. Amazon's recommendation engine flags listings with absurdly high inventory as potentially scammy, and your account-health metrics show this.
The 999 number is the de facto convention. It's what Amazon's own Vendor Central documentation suggests for "demand-driven" listings, and what most experienced MTO sellers use.
$What about capacity-bounded MTO?
Some MTO businesses have actual production capacity limits. A custom-leather-goods shop might be able to ship 12 wallets per week, so putting qty=999 on each listing means the seller is committing to capacity they don't have. Late shipments cascade.
The fix for capacity-bounded MTO: listing.qty is set to (weekly_capacity × weeks_in_lead_time) - currently_in_production, refreshed daily. So if you can ship 12/week, your lead time is 2 weeks, and you have 8 already in production, your effective Amazon inventory is (12 × 2) - 8 = 16. Customers ordering above that get a longer lead time displayed; the seller doesn't get over-committed.
This is more sophisticated than the 999 default and requires explicit capacity modeling in Odoo. For most MTO businesses, 999 is fine because capacity isn't the real bottleneck; production timing is, and that's handled via lead_time_to_ship_max_days.
$How to audit whether your connector has this bug
Five-minute diagnostic. Open Odoo's developer mode, navigate to any MTO product, check the linked marketplace listing record. Look at the qty field on the listing vs the product:
# In an Odoo shell or via xmlrpc:
listing = env["marketplace.listing"].search([
("product_id.name", "=", "Your MTO Product"),
("channel", "=", "amazon"),
])
print("product.free_qty:", listing.product_id.free_qty)
print("listing.qty:", listing.qty)
If product.free_qty is 0 and listing.qty is also 0 → your connector is reading the wrong field. The MTO listing on Amazon is showing as out-of-stock.
If product.free_qty is 0 and listing.qty is 999 → the schema is right, but check the actual SYNC code path. Maybe the field exists but isn't being read by the publish flow. Trace the connector's inventory-update code path back from the SP-API call to find where the qty value originates.
For a real "is this happening on my catalog" answer: pull every active MTO listing's current Amazon quantity via GET /listings/2021-08-01/items/{seller}/{sku}?includedData=attributes and look at the fulfillment_availability.quantity field. If your MTO listings are showing 0, the bug is operating in production.
$One more wrinkle: per-marketplace qty
If you sell on multiple Amazon marketplaces (US, UK, DE, etc.), fulfillment_availability takes a list of objects, one per marketplace. The connector needs to push the qty per marketplace, not a single qty applied to all marketplaces. For MTO businesses with multi-region distribution, the per-marketplace qty can be different (e.g., 999 in the US for your home market with full production, but 10 in DE for limited customs-cleared inventory in the EU).
The schema:
{
"attributes": {
"fulfillment_availability": [
{"marketplace_id": "ATVPDKIKX0DER", "quantity": 999, "lead_time_to_ship_max_days": 10},
{"marketplace_id": "A1F83G8C2ARO7P", "quantity": 10, "lead_time_to_ship_max_days": 14}
]
}
}
If your connector pushes a single quantity field applied uniformly across all marketplaces, you lose the ability to differentiate. For MTO businesses with limited regional inventory, this matters.