This isnt working for me in POS, ERPNext v14.74.3, so I made this script so I made it work with this code because my current version only seems to work for Sales Invoice Document which is not ideal for retail environment:
### multi_uom_pos.py
import frappe
@frappe.whitelist()
def multi_uom_scan(barcode, company=None, price_list=None):
# Find exact barcode row
ib = frappe.db.get_value(
"Item Barcode",
{"barcode": barcode},
["parent as item_code", "uom"],
as_dict=True,
)
if not ib:
return {"found": False, "why": "barcode_not_found"}
item_code = ib.item_code
# Choose UOM: prefer Barcode.UOM -> Sales UOM -> Stock UOM
uom = ib.uom or frappe.db.get_value("Item", item_code, "sales_uom") \
or frappe.db.get_value("Item", item_code, "stock_uom")
cf = frappe.db.get_value(
"UOM Conversion Detail",
{"parent": item_code, "uom": uom},
"conversion_factor",
) or 1
price = None
if price_list:
price = frappe.db.get_value(
"Item Price",
{"item_code": item_code, "price_list": price_list, "selling": 1, "uom": uom},
"price_list_rate",
)
return {
"found": True,
"barcode": barcode,
"item_code": item_code,
"uom": uom,
"conversion_factor": float(cf),
"price_list_rate": float(price) if price is not None else None,
}
js pos_multi_uom_barcode file:
// 🔥 Multi-UOM omni bridge v2 — per-item memory + optional merge
(function () {
const DBG = () => localStorage.multiUomDbg === "1" || window.__multiUomDbg === true;
const log = (...a)=>{ if (DBG()) console.log("%c[Multi-UOM]", "color:#4B8", ...a); };
const warn = (...a)=>console.warn("%c[Multi-UOM]", "color:#C53", ...a);
const sleep = (ms)=>new Promise(r=>setTimeout(r,ms));
const FRESH_MS = 10000;
const MERGE_SAME_ITEM = true; // ← set to false if you prefer separate lines
// Session state
const State = { pending:null, lastCode:null, lastCodeTs:0 };
const Prefs = new Map(); // item_code -> {uom, cf, price}
// Capture any input/scan → set lastCode and resolve server-side
function rememberCode(code){
State.lastCode = (code||"").trim(); State.lastCodeTs = Date.now();
if (!State.lastCode) return;
resolve(State.lastCode).then(snap => { if (snap) State.pending = snap; });
}
document.addEventListener("input", e => { const v=e.target?.value?.trim?.(); if (v && v.length>=3) rememberCode(v); }, true);
document.addEventListener("change", e => { const v=e.target?.value?.trim?.(); if (v && v.length>=3) rememberCode(v); }, true);
let buf="", last=0;
document.addEventListener("keydown", e=>{
const now=performance.now(), dt=now-last; last=now;
if (dt>1000) buf="";
if (e.key.length===1 && !e.ctrlKey && !e.metaKey && !e.altKey) buf+=e.key;
else if (e.key==="Enter"){ const code=buf.trim(); buf=""; if (code) rememberCode(code); }
}, true);
// Server lookup only (no client DB)
async function resolve(code){
try{
const { message:d } = await frappe.call({
method: "application.custom_app.custom.api.multi_uom_pos.multi_uom_scan", <<-- change this to the correct route
args: { barcode: code, company: cur_frm?.doc?.company, price_list: cur_frm?.doc?.selling_price_list },
});
if (d?.found) {
const snap = { item_code:d.item_code, uom:d.uom, cf:d.conversion_factor||1, price:d.price_list_rate, ts:Date.now(), via:code };
log("Pending set:", snap);
return snap;
}
} catch(e){ warn("resolve error:", e); }
return null;
}
async function enforceUntilStable(cdt, cdn, target){
const end=Date.now()+2500; let stableSince=0;
while(Date.now()<end){
const row = locals[cdt]?.[cdn]; if(!row) break;
const wantRate = target.price!=null ? Number(target.price):null;
const okU = row.uom===target.uom;
const okC = Number(row.conversion_factor)===Number(target.cf);
const okR = wantRate==null || Number(row.rate)===wantRate;
if(!okU || !okC || !okR){
await frappe.model.set_value(cdt, cdn, "uom", target.uom);
await frappe.model.set_value(cdt, cdn, "conversion_factor", target.cf);
if (wantRate!=null){
await frappe.model.set_value(cdt, cdn, "price_list_rate", wantRate);
await frappe.model.set_value(cdt, cdn, "rate", wantRate);
}
if (cur_frm){ await cur_frm.refresh_field("items"); cur_frm.trigger("calculate_taxes_and_totals"); }
stableSince=0;
} else {
if(!stableSince) stableSince=Date.now();
if(Date.now()-stableSince>250) break;
}
await sleep(80);
}
log("✅ enforced", {uom:target.uom, cf:target.cf, rate:target.price});
}
async function maybeMergeDuplicate(frm, row, target){
if (!MERGE_SAME_ITEM) return false;
const items = (frm?.doc?.items||[]).filter(r => r.name !== row.name && r.item_code === row.item_code);
if (!items.length) return false;
const prev = items[items.length-1];
// Only merge if UOM matches our target (to avoid mixing cajas with unidades)
if (prev.uom !== target.uom) return false;
prev.qty = Number(prev.qty||0) + Number(row.qty||1);
// remove the new row
frappe.model.clear_doc(row.doctype, row.name);
await frm.refresh_field("items");
frm.trigger("calculate_taxes_and_totals");
log("↪ merged into", prev.name, "qty:", prev.qty);
return true;
}
// Universal row-create hook
if (!frappe?.model?.add_child){ warn("frappe.model.add_child not found"); return; }
if (frappe.model.add_child.__muom_omni_v2){ log("omni v2 already installed"); return; }
const orig = frappe.model.add_child;
frappe.model.add_child = function(){
const row = orig.apply(this, arguments);
setTimeout(async ()=>{
try{
if (!row || row.doctype!=="POS Invoice Item") return;
// Wait until item_code appears
for(let i=0;i<25 && !locals[row.doctype][row.name].item_code;i++) await sleep(25);
const ic = locals[row.doctype][row.name].item_code;
// Choose target: fresh pending → per-item memory → fresh lastCode resolve
const fresh = (snap)=> !!snap && (Date.now()-snap.ts < FRESH_MS);
let snap = fresh(State.pending) && State.pending.item_code===ic ? State.pending : null;
if (!snap && Prefs.has(ic)) snap = { ...Prefs.get(ic), ts: Date.now() };
if (!snap && State.lastCode && (Date.now()-State.lastCodeTs < FRESH_MS)) snap = await resolve(State.lastCode);
if (!snap){ log("bridge: no fresh/cached barcode, keep defaults"); return; }
await enforceUntilStable(row.doctype, row.name, snap);
Prefs.set(ic, { item_code: ic, uom: snap.uom, cf: snap.cf, price: snap.price });
// lock targets so guards can snap back if edited
const r = locals[row.doctype][row.name];
r.__cf_lock_target = snap.cf;
r.__uom_lock_target = snap.uom;
// Optional: merge multiple clicks into one line
await maybeMergeDuplicate(cur_frm, locals[row.doctype][row.name], snap);
// consume this one-shot intent but keep per-item memory
State.pending = null; State.lastCode = null; State.lastCodeTs = 0;
}catch(e){ warn("omni v2 error:", e); }
}, 0);
return row;
};
frappe.model.add_child.__muom_omni_v2 = true;
log("🔥 Multi-UOM omni bridge v2 installed");
})();
// --- Locks for CF (and optional UOM) in POS ---
(function lockCFUOM() {
const LOCK_UOM = true; // ← set to true to keep UOM read-only too
// 1) Model-level guards: snap back to the enforced targets
if (frappe?.model?.on) {
frappe.model.on("POS Invoice Item", "conversion_factor", function (row, cdt, cdn) {
const t = locals[cdt]?.[cdn]?.__cf_lock_target;
if (t != null && Number(row.conversion_factor) !== Number(t)) {
frappe.model.set_value(cdt, cdn, "conversion_factor", Number(t));
}
});
if (LOCK_UOM) {
frappe.model.on("POS Invoice Item", "uom", function (row, cdt, cdn) {
const t = locals[cdt]?.[cdn]?.__uom_lock_target;
if (t && row.uom !== t) {
frappe.model.set_value(cdt, cdn, "uom", t);
}
});
}
}
// 2) Child DocField properties (covers grid & detail pane)
function setDocfieldReadOnly() {
const grid = cur_frm?.get_field("items")?.grid;
if (!grid) return;
grid.update_docfield_property("conversion_factor", "read_only", 1);
if (LOCK_UOM) grid.update_docfield_property("uom", "read_only", 1);
}
setDocfieldReadOnly();
// 3) UI hard-disable (survives re-render)
const style = document.createElement("style");
style.textContent = `
.pos [data-fieldname="conversion_factor"] input{ pointer-events:none!important; opacity:.7; }
${LOCK_UOM ? `.pos [data-fieldname="uom"] input, .pos [data-fieldname="uom"] select{ pointer-events:none!important; opacity:.7; }` : ``}
`;
document.head.appendChild(style);
function disableInputs() {
const cf = document.querySelector('.pos [data-fieldname="conversion_factor"] input');
if (cf) {
cf.readOnly = true; cf.setAttribute('tabindex','-1');
cf.addEventListener('keydown', e=>e.preventDefault(), true);
cf.addEventListener('wheel', e=>e.preventDefault(), {passive:false});
cf.addEventListener('paste', e=>e.preventDefault(), true);
}
if (LOCK_UOM) {
const u = document.querySelector('.pos [data-fieldname="uom"] select, .pos [data-fieldname="uom"] input');
if (u) { u.disabled = true; u.setAttribute('tabindex','-1'); }
}
}
disableInputs();
new MutationObserver(()=>{ setDocfieldReadOnly(); disableInputs(); })
.observe(document.body, {subtree:true, childList:true});
})();