NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name Lightspeed Serial Scale Bulk Items // @namespace https://github.com/gmkarl/lightspeedbulk/ // @version 0.7.3 // @description Communicates with NCI scales to price bulk items in the Lightspeed Register. // @author Karl Semich // @match https://*.merchantos.com/register.php* // @grant unsafeWindow // @grant GM_getValue // @grant GM_setValue // @grant GM_xmlhttpRequest // ==/UserScript== 'use strict'; //////////////////////// // Lightspeed Backend // //////////////////////// // These define the interface to the register window. function LineItem(id) { this.id = id; } LineItem.fromRegisterReturn = function(result) { if (result.add_line && result.line_type == 'register_transaction') return new LineItem(result.line_id); }; LineItem.prototype = { get element() { return document.getElementById('line_'+this.id+'_3'); }, get description() { return this.element.getElementsByClassName('register-lines-control')[0].children[0].childNodes[0].data; }, editInline: function() { window.eval("merchantos.register.inlineEditLine('transaction_line',"+this.id+","+this.id+");"); }, remove: function() { window.eval("merchantos.register.removeLine("+this.id+");"); } }; function InlineEdit(id) { this.id = id; this.lineItem = new LineItem(id); } InlineEdit.fromRegisterReturn = function(result) { if (result.hasOwnProperty('inline_edit')) return new InlineEdit(result.line_id); }; InlineEdit.prototype = { get description() { return this.lineItem.description; }, remove: function() { this.lineItem.remove(); }, get editElement() { return document.getElementById('transaction_line_'+this.id); }, get priceElement() { return document.getElementById('edit_item_price_' + this.id); }, get quantityElement() { var qtyElems = document.getElementsByName('edit_item_quantity'); for (var i = 0; i < qtyElems.length; ++ i) if (this.editElement.contains(qtyElems[i])) return qtyElems[i]; }, get noteElement() { return document.getElementById('inline_edit_note_' + this.id); }, get saveButton() { var ret = this.editElement.getElementsByClassName('line-buttons')[0].children[0]; if (ret.getAttribute('data-automation') == 'saveButton') return ret; }, save: function() { this.saveButton.click(); }, cancel: function() { window.eval("merchantos.register.cancelInlineEditLine("+this.id+");"); }, set price(p) { return (this.priceElement.value = p); }, get price() { return parseFloat(this.priceElement.value); }, set quantity(q) { return (this.quantityElement.value = q); }, get quantity() { return parseInt(this.quantityElement.value); }, set note(n) { return (this.noteElement.value = n); }, get note() { return this.noteElement.value; } }; // hook into functions to handle behavior var handlers = { onItemSearch : function(text) {}, onDonePay : function() {}, onLineItem : function(item) {}, onInlineEdit : function(edit) {}, }; (function() { try { var original_addItemSearch = unsafeWindow.merchantos.register.addItemSearch; unsafeWindow.merchantos.register.addItemSearch = cloneInto(function(element) { try { eventLog.push("onItemSearch(" + JSON.stringify(element.value) + ")"); handlers.onItemSearch(element.value); } catch(e) { reportExceptionAsIssue(e,"addItemSearch"); } return original_addItemSearch(element); }, unsafeWindow, {cloneFunctions:true}); var original_donePay = unsafeWindow.merchantos.register.donePay; unsafeWindow.merchantos.register.donePay = cloneInto(function() { var callOriginal = true; try { eventLog.push("donePay()"); callOriginal = handlers.onDonePay(); } catch(e) { reportExceptionAsIssue(e,"donePay"); } if (callOriginal) { original_donePay.call(this); } }, unsafeWindow, {cloneFunctions:true}); var original_ajaxRegister_Return = unsafeWindow.merchantos.register.ajaxRegister_Return; unsafeWindow.merchantos.register.ajaxRegister_Return = cloneInto(function(result) { var ret = original_ajaxRegister_Return.call(this, result); var item; try { eventLog.push("ajaxRegister_Return(" + JSON.stringify(result) + ")"); if ((item = LineItem.fromRegisterReturn(result))) { eventLog.push("onLineItem(" + item.id + ")"); if (item.element) handlers.onLineItem(item); else setTimeout(function(){ try { handlers.onLineItem(item); } catch(e) { reportExceptionAsIssue(e,"setTimeout onLineItem"); } },0); } else if ((item = InlineEdit.fromRegisterReturn(result))) { eventLog.push("onInlineEdit(" + item.id + ")"); if (item.editElement) handlers.onInlineEdit(item); else setTimeout(function(){ try { handlers.onInlineEdit(item); } catch(e) { reportExceptionAsIssue(e,"setTimeout onInlineEdit"); } },0); } } catch(e) { reportExceptionAsIssue(e,"ajaxRegister_Return"); } return ret; }, unsafeWindow, {cloneFunctions:true}); } catch(e) { reportExceptionAsIssue(e,"hook insertion"); } })(); // Submit a github issue about a thrown exception var reportExceptionAsIssueRequest; var eventLog = []; function reportExceptionAsIssue(error, label) { try { var issueTitle = label + ": " + error.toString(); var issueStackTrace = error.stack; var elem; var issueState = ""; elem = document.getElementById("session_shop"); issueState += "session_shop: " + session_shop + "\n"; if (elem) { issueState += "session_shop.innerHTML: " + elem.innerHTML + "\n"; } elem = document.getElementById("register"); issueState += "register: " + elem + "\n"; if (elem) { issueState += "register.style.display: " + elem.style.display + "\n"; } var issueEventLog = eventLog.join("\n") .replace(/<select name=\\?"employee_id\\?"[^]*?<\/select>/g, "<!-- censored employee id -->"); console.log(issueTitle); console.log("Stack trace:"); console.log(issueStackTrace); console.log("State:"); console.log(issueState); console.log("Event log:"); console.log(issueEventLog); try { if (document.getElementById("session_shop").innerHTML == "Test Store") return; } catch(e) {} reportExceptionAsIssueRequest = GM_xmlhttpRequest({ url: "https://api.github.com/repos/gmkarl/lightspeedbulk/issues", method: "POST", headers: { "User-Agent": "lightspeedbulk", Accept: "application/vnd.github.v3+json", Authorization: "token be8980229117ea4298" + "497dc0f7f4af73ac24f040", "Content-Type": "application/json" }, data: JSON.stringify({ title: issueTitle, body: "Stack trace:\n```\n" + issueStackTrace + "\n```\nState:\n```\n" + issueState + "\n```\nEvent log:\n```\n" + issueEventLog + "\n```" }), }); } catch(e) { console.log("exception in exception handler"); console.log(e.toString()); console.log(e.stack); } } //////////////////// // Serial Backend // //////////////////// // Defines interfaces to serial scales // requires jUART function SerialScale(dev, jUARTSerial, implementation) { this.status = "Connecting"; this.jUARTSerial = jUARTSerial; this.port = dev; this.implementation = implementation; if (!this.jUARTSerial.open(dev)) { throw new Error("Failed to open serial port: " + dev); } this.currentMessage = ""; var self = this; this.jUARTSerial.recv_callback(cloneInto(function(bytes, size) { if (self.status == "Destroyed") { return; } try { for (var i = 0; i < size; ++ i) if (!self.recvByte(bytes[i])) break; } catch(e) { reportExceptionAsIssue(e,"recv_callback"); } }, unsafeWindow, {cloneFunctions:true})); this.jUARTSerial.set_option(9600,2,7,0,0); } SerialScale.Types = []; SerialScale.jUART = function() { var plugin = unsafeWindow.document.getElementById('jUART'); if (!plugin) { plugin = document.createElement('object'); plugin.type = 'application/x-juart'; plugin.id = 'jUART'; document.body.appendChild(plugin); plugin = unsafeWindow.document.getElementById('jUART'); } return plugin; }; SerialScale.find = function(success, failure) { var serial = null; if (SerialScale.singleton) { serial = SerialScale.singleton.serial.serial; } if (!serial) { serial = SerialScale.jUART().Serial; } if (!serial) throw new Error("jUART unavailable"); var destroyObject = { destroy : function() {} }; function tryScale(scale, next) { destroyObject.destroy = function() { scale.destroy(); } scale.validate(function() { destroyObject.destroy = function(){}; try { success(scale); if (SerialScale.singleton != scale) { console.log("Found " + scale.protocol + " scale at " + scale.serial.port); SerialScale.singleton = scale; GM_setValue('port', scale.serial.port); GM_setValue('protocol', scale.protocol); } } catch(e) { scale.destroy(); console.log(e.toString()); console.log(e.stack); next(); } }, function() { console.log("Device connected to " + scale.serial.port + " not recognized as " + scale.protocol + " scale."); destroyObject.destroy = function(){}; scale.destroy(); next(); }); } function tryPort(scaleOrPort, next) { if (typeof scaleOrPort != "string") try { return tryScale(scaleOrPort, next); } catch(e) { return next(); } var port = scaleOrPort; var typeIndex = 0; console.log("Looking for scale at " + port); function tryNextType() { if (typeIndex >= SerialScale.Types.length) return next(); var Type = SerialScale.Types[typeIndex++]; try { return tryScale(new Type(port, serial), tryNextType); } catch(e) { destroyObject.destroy(); next(); } } tryNextType(); } tryPort(SerialScale.singleton, tryCachedPort); function tryCachedPort() { var port = GM_getValue('port'); var protocol = GM_getValue('protocol'); for (var i in SerialScale.Types) if (SerialScale.Types[i].prototype.protocol == protocol) try { return tryPort(new SerialScale.Types[i](port, serial), tryPortList); } catch(e) { console.log(e.toString()) console.log(e.stack) } return tryPortList(); } function tryPortList() { var ports = [].concat(serial.getports(), "/dev/ttyS0", "/dev/tty.serial0", "/dev/ttyUSB0", "COM0", "/dev/ttyS1", "/dev/tty.serial1", "/dev/ttyUSB1", "COM1", "/dev/ttyS2", "/dev/tty.serial2", "/dev/ttyUSB2", "COM2", "/dev/ttyS3", "/dev/tty.serial3", "/dev/ttyUSB3", "COM3", "/dev/ttyS4", "/dev/tty.serial4", "/dev/ttyUSB4", "COM4"); var portIndex = 0; function tryNextPort() { if (portIndex >= ports.length) failure(); else tryPort(ports[portIndex++], tryNextPort); } tryNextPort(); } return destroyObject; }; SerialScale.bitfieldToString = function(field, names) { var ret = []; for (var i in names) { if (field & (1<<i)) { ret.push(names[i]); } } if (ret.length == 0) return "Weight"; return ret.join(" "); }; SerialScale.prototype = { destroy: function() { this.jUARTSerial.recv_callback(null); this.jUARTSerial.close(); this.status = "Destroyed"; }, recvByte: function(byte) { this.currentMessage += String.fromCharCode(byte); if (byte == this.implementation.endOfMessageByte) { this.implementation.processMessage(this.currentMessage); this.currentMessage = ""; if (this.status == "Destroyed") return false; } return true; }, sendByte: function(byte) { this.jUARTSerial.send(byte); }, }; function Toledo8213(dev, serial) { this.serial = new SerialScale(dev, serial, this); this.weightStatus = 0; this.confidenceStatus = 0; } SerialScale.Types.push(Toledo8213); Toledo8213.weightRE = /^\x02([0-9\.]+)\x0d$/; Toledo8213.statusRE = /^\x02\?(.)\x0d$/; Toledo8213.commandReceivedRE = /^\x02\x0d$/; Toledo8213.confidenceTestStatusRE = /^\x02(.)\x0d$/; Toledo8213.enterEchoModeRE = /^\x02E\x0d$/; Toledo8213.exitEchoModeRE = /^\x02F\x0d$/; Toledo8213.prototype = { protocol: "Toledo 8213", endOfMessageByte: 0x0d, destroy: function() { this.serial.destroy(); this.onStatus = null; clearTimeout(this.timeout); }, validate: function(success, failure) { var onStatusCache = this.onStatus; this.timeout = setTimeout(handleTimeout, 10000); var self = this; function handleTimeout() { self.onStatus = onStatusCache; failure(); } function handleStatus(error, status, weight, units) { clearTimeout(self.timeout); if (self.invalid) return failure(); self.onStatus = onStatusCache; success(); } self.onStatus = handleStatus; self.requestWeight(); }, get status() { if (this.invalid) return "Protocol Error"; return SerialScale.bitfieldToString(this.weightStatus | (this.confidenceStatus << 5), [ "Motion", "Out of Range", "Under Zero", "Outside Zero Capture Range", "Center of Zero", "XICOR RAM Error", "XICOR ROM Error", , "Proc. RAM Error", "ROM Error" ]); }, get error() { return this.invalid || this.weightStatus != 0 || this.confidenceStatus != 0; }, weight: 0, units: 'LB', onStatus: function(error, status, weight, units) {}, validIfEvenParity: function(num) { this.invalid = false; while (num) { if (num & 1) this.invalid = !this.invalid; num >>= 1; } if (this.invalid) console.log("Parity error"); return !this.invalid; }, processMessage: function(msg) { var res; if ((res = Toledo8213.weightRE.exec(msg))) { // weight data message // <STX>XX.XXX<CR> this.weightStatus = 0; var weight = res[1]; if (weight.indexOf('.') == -1) this.weight = parseInt(weight) / 100.0; else this.weight = parseFloat(weight); } else if (Toledo8213.statusRE.test(msg)) { // status message // <STX>?<status byte><CR> var status = msg.charCodeAt(2); if (this.validIfEvenParity(status)) this.weightStatus = status & 0x1f; } else if (Toledo8213.commandReceivedRE.test(msg)) { // command to initiate a confidence test received console.log(this.protocol + " confidence test initiated."); this.requestTestStatus(); return; } else if (Toledo8213.confidenceTestStatusRE.test(msg)) { // confidence test status // <STD><status byte><CR> var status = msg.charCodeAt(1); if (this.validIfEvenParity(status)) { console.log(this.protocol + " confidence test complete? " + (status & (1<<6))); this.confidenceStatus = status & 0x1f; } } else { console.log("Protocol error: " + msg); this.invalid = true; } this.onStatus(this.error, this.status, this.weight, this.units); }, requestWeight: function() { this.serial.sendByte('W'); }, requestHighWeight: function() { this.serial.sendByte('H'); }, zeroScale: function() { this.serial.sendByte('Z'); }, initiateTest: function() { this.serial.sendByte('A'); }, requestTestStatus: function() { this.serial.sendByte('B'); }, /*enterEchoMode: function() { this.serial.sendByte('E'); }, exitEchoMode: function() { this.serial.sendByte('F'); },*/ }; function NCI(dev, serial) { this.serial = new SerialScale(dev, serial, this); } SerialScale.Types.push(NCI); NCI.unrecognizedRE = /^\x0a\?\x0d\x03$/; NCI.statusRE = /^\x0aS(.)(.)(.?)\x0d\x03$/; NCI.lbozWeightRE = /^\x0a(.)LB (..\..)OZ\x0d(\x0aS..\x0d\x03)$/; NCI.decimalWeightRE = /^\x0a(..\....)(..)\x0d(\x0aS..\x0d\x03)$/; NCI.prototype = { protocol: "NCI", endOfMessageByte: 0x03, destroy: function() { this.serial.destroy(); this.onStatus = null; clearTimeout(this.timeout); }, validate: function(success, failure) { var onStatusCache = this.onStatus; this.timeout = setTimeout(handleTimeout, 10000); var self = this; function handleTimeout() { self.onStatus = onStatusCache; failure(); } function handleStatus(error, status, weight, units) { clearTimeout(self.timeout); if (self.invalid) return failure(); self.onStatus = onStatusCache; success(); } self.onStatus = handleStatus; self.requestStatus(); }, onStatus: function(error, status, weight, units) {}, processMessage: function(msg) { var res; if (NCI.unrecognizedRE.test(msg)) { // unrecognized command // <LF>?<CR><ETX> this.status = "Protocol error"; this.error = true; } else if (NCI.statusRE.test(msg)) { // status message // <LF>Shh<CR><ETX> var status1 = msg.charCodeAt(2); var status2 = msg.charCodeAt(3); this.error = true; if (status1 & (1<<2)) this.status = "RAM error"; else if (status1 & (1<<3)) this.status = "EEPROM error"; else if (status2 & (1<<2)) this.status = "ROM error"; else if (status2 & (1<<3)) this.status = "Faulty calibration"; else if (status2 & (1<<1)) this.status = "Over capacity"; else if (status2 & (1<<0)) this.status = "Under capacity"; else if (status1 & (1<<0)) this.status = "Motion"; else if (status1 & (1<<1)) this.status = "At zero"; else if (status2 & (1<<6)) { var status3 = msg.charCodeAt(4); if ((this.error = !!(status3 & (1<<3)))) this.status = "Initial zero error"; else if (status3 & (1<<2)) this.status = "Net weight"; else this.status = "Gross weight"; } else { this.error = false; this.status = "Weight"; } this.onStatus(this.error, this.status, this.weight, this.units); } else if ((res = NCI.lbozWeightRE.exec(msg))) { // lb-oz weight message // <LF>xLB<SP>xx.xOZ<CR><LF>Shh<CR><ETX> var lbs = parseInt(res[1]); var ozs = parseFloat(res[2]); this.weight = lbs + ozs / 16.0; this.units = "LB"; this.processMessage(res[3]); } else if ((res = NCI.decimalWeightRE.exec(msg))) { // decimal lb weight message // <LF>xx.xxxUU<CR><LF>Shh<CR><ETX> this.weight = parseFloat(res[1]); this.units = res[2]; this.processMessage(res[3]); } else if (this.status == "Connecting") { // this invalid message was hopefully the tail end of a partial message. this.status = "Connected"; } else { console.log(msg); this.error = true; this.status = "Protocol Error"; this.invalid = true; this.onStatus(this.error, this.status, this.weight, this.units); } }, sendCommand: function(cmd) { this.serial.sendByte(cmd); this.serial.sendByte(0x0d); }, requestWeight: function() { this.sendCommand('W'); }, requestStatus: function() { this.sendCommand('S'); }, zeroScale: function() { this.sendCommand('Z'); } }; /////////////////////// // Weight GUI Prompt // /////////////////////// var weightPromptElement = document.createElement('table'); weightPromptElement.innerHTML = '<tbody><tr>' + '<td class="line-description"></td>' + '<td><label for="edit_item_weight">Weight</label></td>' + '<td><input name="edit_item_weight" type="number" class="number" tabindex="2000" size="5" maxlength="15" style="margin-right:-18px; padding-right:18px">lb </td>' + '<td class="line-buttons">' + '<button tabindex="2001" class="tare-button">Start Tare</button>' + '<button tabindex="2002" class="save-button">Save</button>' + '<button tabindex="2003" class="cancel-button">Cancel</button>' + '</td>' + '</tr></tbody>'; var incompleteWeightPrompts = {}; function focusIncompleteWeightPrompt() { var elements = document.getElementsByName('edit_item_weight'); if (elements.length == 0) return false; var element = elements[0]; window.eval("merchantos.focus.set('"+element.id+"');"); return true; } function weightPrompt(edit, callback, cancel_callback) { var tare = 0; var save = save_main; var cancel_main = function () { cleanup(); cancel_callback(); }; var cancel = cancel_main; var promptElement = weightPromptElement.cloneNode(true); var editItemWeightElement = promptElement.getElementsByClassName('number')[0]; var startTareElement = promptElement.getElementsByClassName('tare-button')[0]; var saveElement = promptElement.getElementsByClassName('save-button')[0]; var cancelElement = promptElement.getElementsByClassName('cancel-button')[0]; var scaleStatusElement = document.createElement("small"); var scaleStatusText = document.createTextNode(""); var scale = null; scaleStatusElement.appendChild(scaleStatusText); editItemWeightElement.parentElement.appendChild(scaleStatusElement); editItemWeightElement.onkeypress = function(event) { try { if (unsafeWindow.onEnterKey(event, cloneInto( function(){save();}, unsafeWindow, {cloneFunctions:true} ))) return false; } catch(e) { reportExceptionAsIssue(e,"editItemWeightElement.onkeypress"); } }; editItemWeightElement.id = 'edit_item_weight_' + edit.id; startTareElement.onclick = function() { var nextTare = tare; tare = 0; startTareElement.style.visibility = "hidden"; saveElement.innerHTML = "Tare"; cancelElement.innerHTML = "Abort"; focusWeightInput(); save = function() { nextTare = parseFloat(editItemWeightElement.value); cancel(); if (nextTare) { if (scale.requestWeight) { var onStatusCache = scale.onStatus; save = function(){scale.requestWeight();}; scale.onStatus = function(error, status, weight, units) { if (status == "Weight" || status == "Motion") { onStatusCache(error, "Wait for zero (" + status + ")", weight, units); } else { save = save_main; scale.onStatus = onStatusCache; scale.onStatus(error, status, weight, units); } } } startTareElement.innerHTML = "Retare (" + nextTare + " lb)" } else { startTareElement.innerHTML = "Start Tare"; } if (scale.requestWeight) { scale.requestWeight(); } }; cancel = function() { tare = nextTare; startTareElement.style.visibility = "initial"; saveElement.innerHTML = "Save"; cancelElement.innerHTML = "Cancel"; save = save_main; cancel = cancel_main; editItemWeightElement.value = ""; focusWeightInput(); }; }; startTareElement.id = 'start_tare_' + edit.id; saveElement.onclick = function() { try { return save(); } catch(e) { reportExceptionAsIssue(e,"saveElement.onclick"); } }; saveElement.id = 'save_element_weight_' + edit.id; if (document.getElementById(saveElement.id)) document.getElementById(saveElement.id).click(); cancelElement.onclick = function() { try { cancel(); return false; } catch(e) { reportExceptionAsIssue(e,"cancelElement.onclick"); } }; promptElement.getElementsByClassName('line-description')[0].innerHTML = edit.description; function cleanup() { if (scale) scale.onStatus = function(){}; scale = null; editItemWeightElement.onkeypress = null; startTareElement.onclick = null; saveElement.onclick = null; cancelElement.onclick = null; promptElement = null; editItemWeightElement = null; startTareElement = null; scaleStatusElement = null; scaleStatusText = null; saveElement = null; cancelElement = null; } function save_main() { var entry = editItemWeightElement.value; var lbs = parseFloat(entry); if (entry != "" && entry != "0.0" && entry != "0" && (!(lbs - tare > 0.04) || !(lbs < 30))) { lbs = window.prompt("This weight looks unlikely: " + entry + " lbs\nPlease enter or re-enter the proper weight in lbs. Tare of " + tare + " will be subtracted after.", lbs); lbs = parseFloat(lbs); } if (!lbs) { cancel(); } else { cleanup(); callback(lbs - tare); } return false; } edit.editElement.parentElement.appendChild(promptElement); edit.editElement.style.display = 'none'; function focusWeightInput() { window.eval("merchantos.focus.set('"+editItemWeightElement.id+"');"); } focusWeightInput(); function scaleFound(s) { scale = s; scaleStatusText.data = "Scale: " + scale.protocol + " found"; var matchesNeeded = 8; var lastWeight = -1; var numberMatched = 0; scale.onStatus = function(error, status, weight, units) { scaleStatusText.data = "Scale: " + status; if (!error && units && weight) { if (units != 'LB') { scaleStatusText.data = "Scale: " + units + " not LBs"; } else { if (weight == lastWeight) { numberMatched ++; } else { lastWeight = weight; numberMatched = 0; editItemWeightElement.value = weight; } if (numberMatched >= matchesNeeded) { save(); return; } } } else { lastWeight = -1; } scale.requestWeight(); }; scale.requestWeight(); } function noScaleFound() { scaleStatusText.data = "Scale: not found"; } function lookForScale() { scaleStatusText.data = "Scale: searching ..."; try { return SerialScale.find(scaleFound, noScaleFound); } catch(e) { scaleStatusText.data = "Scale: " + e.message; } } scale = lookForScale(); } //////////////////////// // Main Functionality // //////////////////////// // bulk item descriptions end in "$x.xx/lb" // the dollar sign may be missing; the '/' may be a 'per'; the expression may be surrounded in parentheses var bulkRE = /(?:\(\$?|[\$ ])([0-9\.]*) ?(?:\/|per) ?(#|lb|oz)s?\)?$/; var STATE = "user"; handlers.onItemSearch = function(text) { STATE = "itemSearch"; eventLog = [eventLog[eventLog.length-1]]; }; handlers.onDonePay = function() { if (focusIncompleteWeightPrompt()) { window.eval('alertUser("An item needs to be weighed.");'); return false; } else { return true; } }; handlers.onLineItem = function(item) { if (STATE == "itemSearch") { if (item.description.match(bulkRE)) { STATE = "editAuto_" + item.id; item.element.style.display = "none"; setTimeout(function(){item.editInline();},0); } else { STATE = "user"; } } else if (STATE == "editSave") { STATE = "user"; } }; handlers.onInlineEdit = function(edit) { if (STATE == "editAuto_" + edit.id) { edit.quantity = 1; var bulkMatch = edit.description.match(bulkRE); var unitPrice = parseFloat(bulkMatch[1]); var unit = bulkMatch[2]; // popup weight dialog weightPrompt(edit, function(lbs){ var note; if (unit == "oz") { note = Math.round(lbs * 16 * 100)/100 + " oz"; unitPrice *= 16; } else { note = Math.round(lbs*100)/100 + " lb"; } var newPrice = Math.ceil(unitPrice * lbs * 100)/100; if (edit.note != "") { edit.price += newPrice; edit.note += ", " + note; } else { edit.price = newPrice; edit.note = note; } STATE = "editSave"; edit.save(); }, function() { STATE = "user"; if (edit.note == "") { edit.price = 0; edit.remove(); } else { edit.save(); } }); } };