From 5d94f0bde55329411c419c242749e0260d3bb7c9 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Sun, 21 Jan 2024 20:46:57 +0530 Subject: [PATCH 1/2] fix: UX improvements for Serial and Batch Bundle --- .../js/utils/serial_no_batch_selector.js | 71 ++++++++++++++++++- .../serial_and_batch_bundle.py | 16 +++++ 2 files changed, 84 insertions(+), 3 deletions(-) diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js index bf362e338e..6c775f0db8 100644 --- a/erpnext/public/js/utils/serial_no_batch_selector.js +++ b/erpnext/public/js/utils/serial_no_batch_selector.js @@ -179,11 +179,52 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate { label = __('Serial Nos / Batch Nos'); } - return [ + let fields = [ { fieldtype: 'Section Break', label: __('{0} {1} via CSV File', [primary_label, label]) - }, + } + ] + + if (this.item?.has_serial_no) { + fields = [...fields, + { + fieldtype: 'Check', + label: __('Upload Using CSV file'), + fieldname: 'upload_using_csv', + default: 0, + }, + { + fieldtype: 'Section Break', + depends_on: 'eval:doc.upload_using_csv === 0', + }, + { + fieldtype: 'Small Text', + label: __('Serial Nos'), + fieldname: 'upload_serial_nos', + depends_on: 'eval:doc.upload_using_csv === 0', + }, + { + fieldtype: 'Column Break', + depends_on: 'eval:doc.upload_using_csv === 0', + }, + { + fieldtype: 'Button', + fieldname: 'make_serial_nos', + label: __('Create Serial Nos'), + depends_on: 'eval:doc.upload_using_csv === 0', + click: () => { + this.create_serial_nos(); + } + }, + { + fieldtype: 'Section Break', + depends_on: 'eval:doc.upload_using_csv === 1', + } + ]; + } + + fields = [...fields, { fieldtype: 'Button', fieldname: 'download_csv', @@ -199,7 +240,31 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate { label: __('Attach CSV File'), onchange: () => this.upload_csv_file() } - ] + ]; + + return fields; + } + + create_serial_nos() { + let {upload_serial_nos} = this.dialog.get_values(); + + if (!upload_serial_nos) { + frappe.throw(__('Please enter Serial Nos')); + } + + frappe.call({ + method: 'erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.create_serial_nos', + args: { + item_code: this.item.item_code, + serial_nos: upload_serial_nos + }, + callback: (r) => { + if (r.message) { + this.dialog.fields_dict.entries.df.data = []; + this.set_data(r.message); + } + } + }); } download_csv_file() { diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index 2b87fcd175..856f1811e6 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py @@ -999,9 +999,25 @@ def get_serial_batch_from_data(item_code, kwargs): make_serial_nos(item_code, serial_nos) + if kwargs.get("_has_serial_nos"): + return serial_nos + return serial_nos, batch_nos +@frappe.whitelist() +def create_serial_nos(item_code, serial_nos): + serial_nos = get_serial_batch_from_data( + item_code, + { + "serial_nos": serial_nos, + "_has_serial_nos": True, + }, + ) + + return serial_nos + + def make_serial_nos(item_code, serial_nos): item = frappe.get_cached_value("Item", item_code, ["description", "item_code"], as_dict=1) From fc0d2aeeffed9a2f87be0d87f0a0af0e837c5955 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 22 Jan 2024 12:50:24 +0530 Subject: [PATCH 2/2] fix: auto create serial no on scan --- erpnext/public/js/utils/barcode_scanner.js | 93 ++++++++++++------- .../js/utils/serial_no_batch_selector.js | 43 +++++++-- .../serial_and_batch_bundle.js | 2 +- .../serial_and_batch_bundle.py | 29 ++++++ 4 files changed, 120 insertions(+), 47 deletions(-) diff --git a/erpnext/public/js/utils/barcode_scanner.js b/erpnext/public/js/utils/barcode_scanner.js index cf7fab89ff..aacab0fe6c 100644 --- a/erpnext/public/js/utils/barcode_scanner.js +++ b/erpnext/public/js/utils/barcode_scanner.js @@ -105,32 +105,47 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { this.frm.has_items = false; } - if (serial_no && this.is_duplicate_serial_no(row, item_code, serial_no)) { - this.clean_up(); - reject(); - return; + if (serial_no) { + this.is_duplicate_serial_no(row, item_code, serial_no) + .then((is_duplicate) => { + if (!is_duplicate) { + this.run_serially_tasks(row, data, resolve); + } else { + this.clean_up(); + reject(); + return; + } + }); + } else { + this.run_serially_tasks(row, data, resolve); } - frappe.run_serially([ - () => 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(), - () => resolve(row), - () => { - if (row.serial_and_batch_bundle && !this.frm.is_new()) { - this.frm.save(); - } - frappe.flags.trigger_from_barcode_scanner = false; - } - ]); }); } + run_serially_tasks(row, data, resolve) { + const {item_code, barcode, batch_no, serial_no, uom} = data; + + frappe.run_serially([ + () => 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(), + () => { + if (row.serial_and_batch_bundle && !this.frm.is_new()) { + this.frm.save(); + } + + frappe.flags.trigger_from_barcode_scanner = false; + }, + () => resolve(row), + ]); + } + set_item(row, item_code, barcode, batch_no, serial_no) { return new Promise(resolve => { const increment = async (value = 1) => { @@ -475,26 +490,32 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { } } - 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"); - } - - return is_duplicate; - } else if (row.serial_and_batch_bundle) { - this.check_duplicate_serial_no_in_db(row, serial_no, (r) => { - if (r.message) { + async is_duplicate_serial_no(row, item_code, serial_no) { + let is_duplicate = false; + const promise = new Promise((resolve, reject) => { + if (this.frm.is_new() || !row.serial_and_batch_bundle) { + 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"); } - return r.message; - }) - } + resolve(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"); + } + + is_duplicate = r.message; + resolve(is_duplicate); + }) + } + }); + + return await promise; } - async check_duplicate_serial_no_in_db(row, serial_no, response) { + 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: { @@ -504,7 +525,7 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { callback(r) { response(r); } - }) + }); } check_duplicate_serial_no_in_localstorage(item_code, serial_no) { diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js index 6c775f0db8..44a4957b41 100644 --- a/erpnext/public/js/utils/serial_no_batch_selector.js +++ b/erpnext/public/js/utils/serial_no_batch_selector.js @@ -135,7 +135,7 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate { filters: this.get_serial_no_filters() }; }, - onchange: () => this.update_serial_batch_no() + onchange: () => this.scan_barcode_data() }); } @@ -145,7 +145,7 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate { options: 'Barcode', fieldname: 'scan_batch_no', label: __('Scan Batch No'), - onchange: () => this.update_serial_batch_no() + onchange: () => this.scan_barcode_data() }); } @@ -190,36 +190,38 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate { fields = [...fields, { fieldtype: 'Check', - label: __('Upload Using CSV file'), - fieldname: 'upload_using_csv', + label: __('Import Using CSV file'), + fieldname: 'import_using_csv_file', default: 0, }, { fieldtype: 'Section Break', - depends_on: 'eval:doc.upload_using_csv === 0', + label: __('{0} {1} Manually', [primary_label, label]), + depends_on: 'eval:doc.import_using_csv_file === 0', }, { fieldtype: 'Small Text', - label: __('Serial Nos'), + label: __('Enter Serial Nos'), fieldname: 'upload_serial_nos', - depends_on: 'eval:doc.upload_using_csv === 0', + depends_on: 'eval:doc.import_using_csv_file === 0', + description: __('Enter each serial no in a new line'), }, { fieldtype: 'Column Break', - depends_on: 'eval:doc.upload_using_csv === 0', + depends_on: 'eval:doc.import_using_csv_file === 0', }, { fieldtype: 'Button', fieldname: 'make_serial_nos', label: __('Create Serial Nos'), - depends_on: 'eval:doc.upload_using_csv === 0', + depends_on: 'eval:doc.import_using_csv_file === 0', click: () => { this.create_serial_nos(); } }, { fieldtype: 'Section Break', - depends_on: 'eval:doc.upload_using_csv === 1', + depends_on: 'eval:doc.import_using_csv_file === 1', } ]; } @@ -262,6 +264,7 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate { if (r.message) { this.dialog.fields_dict.entries.df.data = []; this.set_data(r.message); + this.update_bundle_entries(); } } }); @@ -439,6 +442,26 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate { } } + scan_barcode_data() { + const { scan_serial_no, scan_batch_no } = this.dialog.get_values(); + + if (scan_serial_no || scan_batch_no) { + frappe.call({ + method: 'erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.is_serial_batch_no_exists', + args: { + item_code: this.item.item_code, + type_of_transaction: this.item.type_of_transaction, + serial_no: scan_serial_no, + batch_no: scan_batch_no, + }, + callback: (r) => { + this.update_serial_batch_no(); + } + + }) + } + } + update_serial_batch_no() { const { scan_serial_no, scan_batch_no } = this.dialog.get_values(); diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.js b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.js index 9f01ee9ae6..91b743016b 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.js +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.js @@ -74,7 +74,7 @@ frappe.ui.form.on('Serial and Batch Bundle', { let fields = [ { - "label": __("Using CSV File"), + "label": __("Import Using CSV file"), "fieldname": "using_csv_file", "default": 1, "fieldtype": "Check", diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index 856f1811e6..63cc938c09 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py @@ -2095,6 +2095,35 @@ def get_batch_no_from_serial_no(serial_no): return frappe.get_cached_value("Serial No", serial_no, "batch_no") +@frappe.whitelist() +def is_serial_batch_no_exists(item_code, type_of_transaction, serial_no=None, batch_no=None): + if serial_no and not frappe.db.exists("Serial No", serial_no): + if type_of_transaction != "Inward": + frappe.throw(_("Serial No {0} does not exists").format(serial_no)) + + make_serial_no(serial_no, item_code) + + if batch_no and frappe.db.exists("Batch", batch_no): + if type_of_transaction != "Inward": + frappe.throw(_("Batch No {0} does not exists").format(batch_no)) + + make_batch_no(batch_no, item_code) + + +def make_serial_no(serial_no, item_code): + serial_no_doc = frappe.new_doc("Serial No") + serial_no_doc.serial_no = serial_no + serial_no_doc.item_code = item_code + serial_no_doc.save(ignore_permissions=True) + + +def make_batch_no(batch_no, item_code): + batch_doc = frappe.new_doc("Batch") + batch_doc.batch_id = batch_no + batch_doc.item = item_code + batch_doc.save(ignore_permissions=True) + + @frappe.whitelist() def is_duplicate_serial_no(bundle_id, serial_no): return frappe.db.exists("Serial and Batch Entry", {"parent": bundle_id, "serial_no": serial_no})