Hi @magedbjn,
This error is ERPNext’s “stock cycle UOM” guard kicking in. When a Purchase Receipt is created against a Purchase Order, ERPNext enforces that the UOM in the receipt row matches the UOM that was used in the PO row — irrespective of what conversions you’ve set up on the Item master. The UOM Conversion table on the Item only affects the conversion factor; it doesn’t permit cross-UOM receiving by itself. So even though your conversions are correctly defined, the validator at submission time is comparing the row-level UOMs and failing before the conversion factor ever gets applied.
The cleanest way to handle this — without losing the link to the original Purchase Order — is a small customization on the Purchase Receipt Item:
Step 1 — Add a Custom Field on Purchase Receipt Item
Create two custom fields on the Purchase Receipt Item doctype:
The user will enter the actual quantity received in Piece or Carton in these fields. The standard qty / uom columns will continue to hold the Kg figure that matches the PO row.
Step 2 — Client Script on Purchase Receipt
Add a Client Script (DocType: Purchase Receipt, Apply To: Form) that watches custom_received_qty and custom_received_uom, pulls the right conversion factor from the Item’s UOM Conversion table, and writes the converted Kg value into qty — plus recalculates the per-Kg rate from the per-Carton rate:
frappe.ui.form.on('Purchase Receipt Item', {
custom_received_qty: function(frm, cdt, cdn) {
recalc_from_receiving_uom(frm, cdt, cdn);
},
custom_received_uom: function(frm, cdt, cdn) {
recalc_from_receiving_uom(frm, cdt, cdn);
}
});
function recalc_from_receiving_uom(frm, cdt, cdn) {
const row = locals[cdt][cdn];
if (!row.item_code || !row.custom_received_uom || !row.custom_received_qty) return;
frappe.db.get_value(
'UOM Conversion Detail',
{ parent: row.item_code, uom: row.custom_received_uom },
'conversion_factor'
).then(r => {
const cf = r.message && r.message.conversion_factor;
if (!cf) {
frappe.msgprint(__('No UOM conversion found for {0} → stock UOM on item {1}',
[row.custom_received_uom, row.item_code]));
return;
}
// Convert receiving qty (e.g. Cartons) to stock/PO UOM (Kg)
const stock_qty = flt(row.custom_received_qty) * flt(cf);
frappe.model.set_value(cdt, cdn, 'qty', stock_qty);
// Adjust rate: if user typed a per-Carton rate elsewhere, divide it down to per-Kg.
// Here we assume row.rate is currently per-receiving-UOM; convert to per-stock-UOM.
if (row.rate) {
const rate_per_stock_uom = flt(row.rate) / flt(cf);
frappe.model.set_value(cdt, cdn, 'rate', rate_per_stock_uom);
}
});
}
Why this is the right shape:
-
The PO row stays in Kg and the PR row’s uom/qty also stays in Kg, so the UOM equality check passes and the PO → PR linkage (with its over-receipt tolerance, billed-qty tracking, etc.) is preserved.
-
The user enters what they physically count — Cartons or Pieces — into the custom fields, and the script silently converts to Kg in the background using the conversion factor already defined on the Item.
-
The rate also gets adjusted, so if your supplier quotes per-Carton, the GL hits the correct per-Kg rate without manual math.
Two small notes:
-
If rate on your Purchase Receipt is being fetched from the PO (already per-Kg), remove the rate-adjustment block from the script — otherwise it’ll double-divide.
-
For audit clarity, you can show the receiving UOM and qty on the print format so the warehouse team sees what they actually counted, while accounting/stock continues to work in Kg.
Hope this helps! If you’d like, share a screenshot of the Item’s UOM Conversion table and I can tighten the script around your specific UOMs.