diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index 8cffe88f32489..9033a3ac41604 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -745,6 +745,8 @@ def calculate_amounts(against_loan, posting_date, payment_type=""): if payment_type == "Loan Closure": amounts["payable_principal_amount"] = amounts["pending_principal_amount"] amounts["interest_amount"] += amounts["unaccrued_interest"] - amounts["payable_amount"] = amounts["payable_principal_amount"] + amounts["interest_amount"] + amounts["payable_amount"] = ( + amounts["payable_principal_amount"] + amounts["interest_amount"] + amounts["penalty_amount"] + ) return amounts diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 57cbe91fa0916..fa41e1bf0a32d 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -403,17 +403,6 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe var sms_man = new erpnext.SMSManager(this.frm.doc); } - barcode(doc, cdt, cdn) { - const d = locals[cdt][cdn]; - if (!d.barcode) { - // barcode cleared, remove item - d.item_code = ""; - } - // flag required for circular triggers - d._triggerd_from_barcode = true; - this.item_code(doc, cdt, cdn); - } - item_code(doc, cdt, cdn) { var me = this; var item = frappe.get_doc(cdt, cdn); @@ -431,9 +420,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe this.frm.doc.doctype === 'Delivery Note') { show_batch_dialog = 1; } - if (!item._triggerd_from_barcode) { - item.barcode = null; - } + item.barcode = null; if(item.item_code || item.barcode || item.serial_no) { @@ -539,6 +526,12 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe if(!d[k]) d[k] = v; }); + if (d.__disable_batch_serial_selector) { + // reset for future use. + d.__disable_batch_serial_selector = false; + return; + } + if (d.has_batch_no && d.has_serial_no) { d.batch_no = undefined; } diff --git a/erpnext/public/js/utils/barcode_scanner.js b/erpnext/public/js/utils/barcode_scanner.js index abea5fcb20bbb..80a463f85c92b 100644 --- a/erpnext/public/js/utils/barcode_scanner.js +++ b/erpnext/public/js/utils/barcode_scanner.js @@ -21,9 +21,7 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { // batch_no: "LOT12", // present if batch was scanned // serial_no: "987XYZ", // present if serial no was scanned // } - this.scan_api = - opts.scan_api || - "erpnext.selling.page.point_of_sale.point_of_sale.search_for_serial_or_batch_or_barcode_number"; + this.scan_api = opts.scan_api || "erpnext.stock.utils.scan_barcode"; } process_scan() { @@ -52,14 +50,16 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { return; } - me.update_table(data.item_code, data.barcode, data.batch_no, data.serial_no); + me.update_table(data); }); } - update_table(item_code, barcode, batch_no, serial_no) { + update_table(data) { let cur_grid = this.frm.fields_dict[this.items_table_name].grid; let row = null; + const {item_code, barcode, batch_no, serial_no} = data; + // Check if batch is scanned and table has batch no field let batch_no_scan = Boolean(batch_no) && frappe.meta.has_field(cur_grid.doctype, this.batch_no_field); @@ -84,6 +84,7 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { } this.show_scan_message(row.idx, row.item_code); + this.set_selector_trigger_flag(row, data); this.set_item(row, item_code); this.set_serial_no(row, serial_no); this.set_batch_no(row, batch_no); @@ -91,6 +92,19 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner { this.clean_up(); } + // 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(row, data) { + const {batch_no, serial_no, has_batch_no, has_serial_no} = data; + + const require_selecting_batch = has_batch_no && !batch_no; + const require_selecting_serial = has_serial_no && !serial_no; + + if (!(require_selecting_batch || require_selecting_serial)) { + row.__disable_batch_serial_selector = true; + } + } + set_item(row, item_code) { const item_data = { item_code: item_code }; item_data[this.qty_field] = (row[this.qty_field] || 0) + 1; diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.py b/erpnext/selling/page/point_of_sale/point_of_sale.py index bf629824ad990..99afe813cb995 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.py +++ b/erpnext/selling/page/point_of_sale/point_of_sale.py @@ -3,12 +3,14 @@ import json +from typing import Dict, Optional import frappe from frappe.utils.nestedset import get_root_of from erpnext.accounts.doctype.pos_invoice.pos_invoice import get_stock_availability from erpnext.accounts.doctype.pos_profile.pos_profile import get_child_nodes, get_item_groups +from erpnext.stock.utils import scan_barcode def search_by_term(search_term, warehouse, price_list): @@ -150,29 +152,8 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_te @frappe.whitelist() -def search_for_serial_or_batch_or_barcode_number(search_value): - # search barcode no - barcode_data = frappe.db.get_value( - "Item Barcode", {"barcode": search_value}, ["barcode", "parent as item_code"], as_dict=True - ) - if barcode_data: - return barcode_data - - # search serial no - serial_no_data = frappe.db.get_value( - "Serial No", search_value, ["name as serial_no", "item_code"], as_dict=True - ) - if serial_no_data: - return serial_no_data - - # search batch no - batch_no_data = frappe.db.get_value( - "Batch", search_value, ["name as batch_no", "item as item_code"], as_dict=True - ) - if batch_no_data: - return batch_no_data - - return {} +def search_for_serial_or_batch_or_barcode_number(search_value: str) -> Dict[str, Optional[str]]: + return scan_barcode(search_value) def get_conditions(search_term): diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index a94087821a5a9..0669f55d4a68d 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -646,21 +646,6 @@ frappe.ui.form.on('Stock Entry Detail', { frm.events.calculate_basic_amount(frm, item); }, - barcode: function(doc, cdt, cdn) { - var d = locals[cdt][cdn]; - if (d.barcode) { - frappe.call({ - method: "erpnext.stock.get_item_details.get_item_code", - args: {"barcode": d.barcode }, - callback: function(r) { - if (!r.exe){ - frappe.model.set_value(cdt, cdn, "item_code", r.message); - } - } - }); - } - }, - uom: function(doc, cdt, cdn) { var d = locals[cdt][cdn]; if(d.uom && d.item_code){ diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js index 84f65a077e0cf..4438acf8118ec 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js @@ -163,20 +163,7 @@ frappe.ui.form.on("Stock Reconciliation", { }); } }, - set_item_code: function(doc, cdt, cdn) { - var d = frappe.model.get_doc(cdt, cdn); - if (d.barcode) { - frappe.call({ - method: "erpnext.stock.get_item_details.get_item_code", - args: {"barcode": d.barcode }, - callback: function(r) { - if (!r.exe){ - frappe.model.set_value(cdt, cdn, "item_code", r.message); - } - } - }); - } - }, + set_amount_quantity: function(doc, cdt, cdn) { var d = frappe.model.get_doc(cdt, cdn); if (d.qty & d.valuation_rate) { @@ -214,9 +201,6 @@ frappe.ui.form.on("Stock Reconciliation", { }); frappe.ui.form.on("Stock Reconciliation Item", { - barcode: function(frm, cdt, cdn) { - frm.events.set_item_code(frm, cdt, cdn); - }, warehouse: function(frm, cdt, cdn) { var child = locals[cdt][cdn]; diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index b75bc7241d44d..d3a230e3d8949 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -167,6 +167,9 @@ def update_stock(args, out): reserved_so = get_so_reservation_for_item(args) out.serial_no = get_serial_no(out, args.serial_no, sales_order=reserved_so) + if not out.serial_no: + out.pop("serial_no", None) + def set_valuation_rate(out, args): if frappe.db.exists("Product Bundle", args.item_code, cache=True): diff --git a/erpnext/stock/tests/test_utils.py b/erpnext/stock/tests/test_utils.py new file mode 100644 index 0000000000000..9ee0c9f3b5ab7 --- /dev/null +++ b/erpnext/stock/tests/test_utils.py @@ -0,0 +1,31 @@ +import frappe +from frappe.tests.utils import FrappeTestCase + +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.stock.utils import scan_barcode + + +class TestStockUtilities(FrappeTestCase): + def test_barcode_scanning(self): + simple_item = make_item(properties={"barcodes": [{"barcode": "12399"}]}) + self.assertEqual(scan_barcode("12399")["item_code"], simple_item.name) + + batch_item = make_item(properties={"has_batch_no": 1, "create_new_batch": 1}) + batch = frappe.get_doc(doctype="Batch", item=batch_item.name).insert() + + batch_scan = scan_barcode(batch.name) + self.assertEqual(batch_scan["item_code"], batch_item.name) + self.assertEqual(batch_scan["batch_no"], batch.name) + self.assertEqual(batch_scan["has_batch_no"], 1) + self.assertEqual(batch_scan["has_serial_no"], 0) + + serial_item = make_item(properties={"has_serial_no": 1}) + serial = frappe.get_doc( + doctype="Serial No", item_code=serial_item.name, serial_no=frappe.generate_hash() + ).insert() + + serial_scan = scan_barcode(serial.name) + self.assertEqual(serial_scan["item_code"], serial_item.name) + self.assertEqual(serial_scan["serial_no"], serial.name) + self.assertEqual(serial_scan["has_batch_no"], 0) + self.assertEqual(serial_scan["has_serial_no"], 1) diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index 4f1891fd750ed..d40218e143980 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -3,6 +3,7 @@ import json +from typing import Dict, Optional import frappe from frappe import _ @@ -548,3 +549,51 @@ def check_pending_reposting(posting_date: str, throw_error: bool = True) -> bool ) return bool(reposting_pending) + + +@frappe.whitelist() +def scan_barcode(search_value: str) -> Dict[str, Optional[str]]: + + # search barcode no + barcode_data = frappe.db.get_value( + "Item Barcode", + {"barcode": search_value}, + ["barcode", "parent as item_code"], + as_dict=True, + ) + if barcode_data: + return _update_item_info(barcode_data) + + # search serial no + serial_no_data = frappe.db.get_value( + "Serial No", + search_value, + ["name as serial_no", "item_code", "batch_no"], + as_dict=True, + ) + if serial_no_data: + return _update_item_info(serial_no_data) + + # search batch no + batch_no_data = frappe.db.get_value( + "Batch", + search_value, + ["name as batch_no", "item as item_code"], + as_dict=True, + ) + if batch_no_data: + return _update_item_info(batch_no_data) + + return {} + + +def _update_item_info(scan_result: Dict[str, Optional[str]]) -> Dict[str, Optional[str]]: + if item_code := scan_result.get("item_code"): + if item_info := frappe.get_cached_value( + "Item", + item_code, + ["has_batch_no", "has_serial_no"], + as_dict=True, + ): + scan_result.update(item_info) + return scan_result