Skip to content

Commit

Permalink
fix: serial / batch barcode scanner (backport #39114) (#39143)
Browse files Browse the repository at this point in the history
fix: serial / batch barcode scanner (#39114)

(cherry picked from commit f09e213)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
  • Loading branch information
mergify[bot] and rohitwaghchaure authored Jan 4, 2024
1 parent 7a5a4be commit 2db1e1a
Show file tree
Hide file tree
Showing 6 changed files with 284 additions and 65 deletions.
14 changes: 14 additions & 0 deletions erpnext/controllers/accounts_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,17 @@ def onload(self):
if self.doctype in relevant_docs:
self.set_payment_schedule()

def remove_bundle_for_non_stock_invoices(self):
has_sabb = False
if self.doctype in ("Sales Invoice", "Purchase Invoice") and not self.update_stock:
for item in self.get("items"):
if item.serial_and_batch_bundle:
item.serial_and_batch_bundle = None
has_sabb = True

if has_sabb:
self.remove_serial_and_batch_bundle()

def ensure_supplier_is_not_blocked(self):
is_supplier_payment = self.doctype == "Payment Entry" and self.party_type == "Supplier"
is_buying_invoice = self.doctype in ["Purchase Invoice", "Purchase Order"]
Expand Down Expand Up @@ -156,6 +167,9 @@ def validate(self):
if self.get("_action") and self._action != "update_after_submit":
self.set_missing_values(for_validate=True)

if self.get("_action") == "submit":
self.remove_bundle_for_non_stock_invoices()

self.ensure_supplier_is_not_blocked()

self.validate_date_with_fiscal_year()
Expand Down
30 changes: 28 additions & 2 deletions erpnext/public/js/controllers/transaction.js
Original file line number Diff line number Diff line change
Expand Up @@ -454,7 +454,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
item.weight_uom = '';
item.conversion_factor = 0;

if(['Sales Invoice'].includes(this.frm.doc.doctype)) {
if(['Sales Invoice', 'Purchase Invoice'].includes(this.frm.doc.doctype)) {
update_stock = cint(me.frm.doc.update_stock);
show_batch_dialog = update_stock;

Expand Down Expand Up @@ -545,7 +545,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
},
() => me.toggle_conversion_factor(item),
() => {
if (show_batch_dialog)
if (show_batch_dialog && !frappe.flags.trigger_from_barcode_scanner)
return frappe.db.get_value("Item", item.item_code, ["has_batch_no", "has_serial_no"])
.then((r) => {
if (r.message &&
Expand Down Expand Up @@ -1239,6 +1239,20 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
}
}

sync_bundle_data() {
let doctypes = ["Sales Invoice", "Purchase Invoice", "Delivery Note", "Purchase Receipt"];

if (this.frm.is_new() && doctypes.includes(this.frm.doc.doctype)) {
const barcode_scanner = new erpnext.utils.BarcodeScanner({frm:this.frm});
barcode_scanner.sync_bundle_data();
barcode_scanner.remove_item_from_localstorage();
}
}

before_save(doc) {
this.sync_bundle_data();
}

service_start_date(frm, cdt, cdn) {
var child = locals[cdt][cdn];

Expand Down Expand Up @@ -1576,6 +1590,18 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
return item_list;
}

items_delete() {
this.update_localstorage_scanned_data();
}

update_localstorage_scanned_data() {
let doctypes = ["Sales Invoice", "Purchase Invoice", "Delivery Note", "Purchase Receipt"];
if (this.frm.is_new() && doctypes.includes(this.frm.doc.doctype)) {
const barcode_scanner = new erpnext.utils.BarcodeScanner({frm:this.frm});
barcode_scanner.update_localstorage_scanned_data();
}
}

_set_values_for_item_list(children) {
const items_rule_dict = {};

Expand Down
233 changes: 184 additions & 49 deletions erpnext/public/js/utils/barcode_scanner.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
this.scan_barcode_field = this.frm.fields_dict[this.scan_field_name];

this.barcode_field = opts.barcode_field || "barcode";
this.serial_no_field = opts.serial_no_field || "serial_no";
this.batch_no_field = opts.batch_no_field || "batch_no";
this.uom_field = opts.uom_field || "uom";
this.qty_field = opts.qty_field || "qty";
// field name on row which defines max quantity to be scanned e.g. picklist
Expand Down Expand Up @@ -84,6 +82,7 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
update_table(data) {
return new Promise((resolve, reject) => {
let cur_grid = this.frm.fields_dict[this.items_table_name].grid;
frappe.flags.trigger_from_barcode_scanner = true;

const {item_code, barcode, batch_no, serial_no, uom} = data;

Expand All @@ -106,50 +105,38 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
this.frm.has_items = false;
}

if (this.is_duplicate_serial_no(row, serial_no)) {
if (serial_no && this.is_duplicate_serial_no(row, item_code, serial_no)) {
this.clean_up();
reject();
return;
}

frappe.run_serially([
() => this.set_selector_trigger_flag(data),
() => this.set_serial_no(row, serial_no),
() => this.set_batch_no(row, batch_no),
() => this.set_serial_and_batch(row, item_code, serial_no, batch_no),
() => this.set_barcode(row, barcode),
() => this.set_item(row, item_code, barcode, batch_no, serial_no).then(qty => {
this.show_scan_message(row.idx, row.item_code, qty);
}),
() => this.set_barcode_uom(row, uom),
() => this.clean_up(),
() => this.revert_selector_flag(),
() => resolve(row)
() => resolve(row),
() => {
if (row.serial_and_batch_bundle && !this.frm.is_new()) {
this.frm.save();
}

frappe.flags.trigger_from_barcode_scanner = false;
}
]);
});
}

// batch and serial selector is reduandant when all info can be added by scan
// this flag on item row is used by transaction.js to avoid triggering selector
set_selector_trigger_flag(data) {
const {has_batch_no, has_serial_no} = data;

const require_selecting_batch = has_batch_no;
const require_selecting_serial = has_serial_no;

if (!(require_selecting_batch || require_selecting_serial)) {
frappe.flags.hide_serial_batch_dialog = true;
}
}

revert_selector_flag() {
frappe.flags.hide_serial_batch_dialog = false;
}

set_item(row, item_code, barcode, batch_no, serial_no) {
return new Promise(resolve => {
const increment = async (value = 1) => {
const item_data = {item_code: item_code};
item_data[this.qty_field] = Number((row[this.qty_field] || 0)) + Number(value);
frappe.flags.trigger_from_barcode_scanner = true;
await frappe.model.set_value(row.doctype, row.name, item_data);
return value;
};
Expand All @@ -158,8 +145,6 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
frappe.prompt(__("Please enter quantity for item {0}", [item_code]), ({value}) => {
increment(value).then((value) => resolve(value));
});
} else if (this.frm.has_items) {
this.prepare_item_for_scan(row, item_code, barcode, batch_no, serial_no);
} else {
increment().then((value) => resolve(value));
}
Expand All @@ -182,9 +167,8 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
frappe.model.set_value(row.doctype, row.name, item_data);

frappe.run_serially([
() => this.set_batch_no(row, this.dialog.get_value("batch_no")),
() => this.set_barcode(row, this.dialog.get_value("barcode")),
() => this.set_serial_no(row, this.dialog.get_value("serial_no")),
() => this.set_serial_and_batch(row, item_code, this.dialog.get_value("serial_no"), this.dialog.get_value("batch_no")),
() => this.add_child_for_remaining_qty(row),
() => this.clean_up()
]);
Expand Down Expand Up @@ -338,29 +322,141 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
}
}

async set_serial_no(row, serial_no) {
if (serial_no && frappe.meta.has_field(row.doctype, this.serial_no_field)) {
const existing_serial_nos = row[this.serial_no_field];
let new_serial_nos = "";
async set_serial_and_batch(row, item_code, serial_no, batch_no) {
if (this.frm.is_new() || !row.serial_and_batch_bundle) {
this.set_bundle_in_localstorage(row, item_code, serial_no, batch_no);
} else if(row.serial_and_batch_bundle) {
frappe.call({
method: "erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.update_serial_or_batch",
args: {
bundle_id: row.serial_and_batch_bundle,
serial_no: serial_no,
batch_no: batch_no,
},
})
}
}

if (!!existing_serial_nos) {
new_serial_nos = existing_serial_nos + "\n" + serial_no;
} else {
new_serial_nos = serial_no;
get_key_for_localstorage() {
let parts = this.frm.doc.name.split("-");
return parts[parts.length - 1] + this.frm.doc.doctype;
}

update_localstorage_scanned_data() {
let docname = this.frm.doc.name
if (localStorage[docname]) {
let items = JSON.parse(localStorage[docname]);
let existing_items = this.frm.doc.items.map(d => d.item_code);
if (!existing_items.length) {
localStorage.removeItem(docname);
return;
}
await frappe.model.set_value(row.doctype, row.name, this.serial_no_field, new_serial_nos);

for (let item_code in items) {
if (!existing_items.includes(item_code)) {
delete items[item_code];
}
}

localStorage[docname] = JSON.stringify(items);
}
}

async set_barcode_uom(row, uom) {
if (uom && frappe.meta.has_field(row.doctype, this.uom_field)) {
await frappe.model.set_value(row.doctype, row.name, this.uom_field, uom);
async set_bundle_in_localstorage(row, item_code, serial_no, batch_no) {
let docname = this.frm.doc.name

let entries = JSON.parse(localStorage.getItem(docname));
if (!entries) {
entries = {};
}

let key = item_code;
if (!entries[key]) {
entries[key] = [];
}

let existing_row = [];
if (!serial_no && batch_no) {
existing_row = entries[key].filter((e) => e.batch_no === batch_no);
if (existing_row.length) {
existing_row[0].qty += 1;
}
} else if (serial_no) {
existing_row = entries[key].filter((e) => e.serial_no === serial_no);
if (existing_row.length) {
frappe.throw(__("Serial No {0} has already scanned.", [serial_no]));
}
}

if (!existing_row.length) {
entries[key].push({
"serial_no": serial_no,
"batch_no": batch_no,
"qty": 1
});
}

localStorage.setItem(docname, JSON.stringify(entries));

// Auto remove from localstorage after 1 hour
setTimeout(() => {
localStorage.removeItem(docname);
}, 3600000)
}

async set_batch_no(row, batch_no) {
if (batch_no && frappe.meta.has_field(row.doctype, this.batch_no_field)) {
await frappe.model.set_value(row.doctype, row.name, this.batch_no_field, batch_no);
remove_item_from_localstorage() {
let docname = this.frm.doc.name;
if (localStorage[docname]) {
localStorage.removeItem(docname);
}
}

async sync_bundle_data() {
let docname = this.frm.doc.name;

if (localStorage[docname]) {
let entries = JSON.parse(localStorage[docname]);
if (entries) {
for (let entry in entries) {
let row = this.frm.doc.items.filter((item) => {
if (item.item_code === entry) {
return true;
}
})[0];

if (row) {
this.create_serial_and_batch_bundle(row, entries, entry)
.then(() => {
if (!entries) {
localStorage.removeItem(docname);
}
});
}
}
}
}
}

async create_serial_and_batch_bundle(row, entries, key) {
frappe.call({
method: "erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.add_serial_batch_ledgers",
args: {
entries: entries[key],
child_row: row,
doc: this.frm.doc,
warehouse: row.warehouse,
do_not_save: 1
},
callback: function(r) {
row.serial_and_batch_bundle = r.message.name;
delete entries[key];
}
})
}

async set_barcode_uom(row, uom) {
if (uom && frappe.meta.has_field(row.doctype, this.uom_field)) {
await frappe.model.set_value(row.doctype, row.name, this.uom_field, uom);
}
}

Expand All @@ -379,13 +475,52 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
}
}

is_duplicate_serial_no(row, serial_no) {
const is_duplicate = row[this.serial_no_field]?.includes(serial_no);
is_duplicate_serial_no(row, item_code, serial_no) {
if (this.frm.is_new() || !row.serial_and_batch_bundle) {
let is_duplicate = this.check_duplicate_serial_no_in_localstorage(item_code, serial_no);
if (is_duplicate) {
this.show_alert(__("Serial No {0} is already added", [serial_no]), "orange");
}

if (is_duplicate) {
this.show_alert(__("Serial No {0} is already added", [serial_no]), "orange");
return is_duplicate;
} else if (row.serial_and_batch_bundle) {
this.check_duplicate_serial_no_in_db(row, serial_no, (r) => {
if (r.message) {
this.show_alert(__("Serial No {0} is already added", [serial_no]), "orange");
}

return r.message;
})
}
return is_duplicate;
}

async check_duplicate_serial_no_in_db(row, serial_no, response) {
frappe.call({
method: "erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.is_duplicate_serial_no",
args: {
serial_no: serial_no,
bundle_id: row.serial_and_batch_bundle
},
callback(r) {
response(r);
}
})
}

check_duplicate_serial_no_in_localstorage(item_code, serial_no) {
let docname = this.frm.doc.name
let entries = JSON.parse(localStorage.getItem(docname));

if (!entries) {
return false;
}

let existing_row = [];
if (entries[item_code]) {
existing_row = entries[item_code].filter((e) => e.serial_no === serial_no);
}

return existing_row.length;
}

get_row_to_modify_on_scan(item_code, batch_no, uom, barcode) {
Expand Down
Loading

0 comments on commit 2db1e1a

Please sign in to comment.