diff --git a/posnext/__init__.py b/posnext/__init__.py index 3ced358..b5fdc75 100644 --- a/posnext/__init__.py +++ b/posnext/__init__.py @@ -1 +1 @@ -__version__ = "0.2.1" +__version__ = "0.2.2" diff --git a/posnext/fixtures/custom_field.json b/posnext/fixtures/custom_field.json index e42229e..614f5f4 100644 --- a/posnext/fixtures/custom_field.json +++ b/posnext/fixtures/custom_field.json @@ -2963,6 +2963,63 @@ "unique": 0, "width": null }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": "When enabled, a barcode scan will add the item only if the scanned code exactly matches an item code or registered barcode.", + "docstatus": 0, + "doctype": "Custom Field", + "dt": "POS Profile", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "custom_add_via_barcode_scan", + "fieldtype": "Check", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "custom_auto_search_serial_number", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Add Item via Barcode Scan", + "length": 0, + "link_filters": null, + "mandatory_depends_on": null, + "modified": "2026-06-04 00:00:00.000000", + "module": "Posnext", + "name": "POS Profile-custom_add_via_barcode_scan", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "placeholder": null, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "show_dashboard": 0, + "sort_options": 0, + "translatable": 0, + "unique": 0, + "width": null + }, { "allow_in_quick_entry": 0, "allow_on_submit": 0, @@ -2990,7 +3047,7 @@ "in_list_view": 0, "in_preview": 0, "in_standard_filter": 0, - "insert_after": "custom_auto_search_serial_number", + "insert_after": "custom_add_via_barcode_scan", "is_system_generated": 0, "is_virtual": 0, "label": "Ignore Update Stock ", diff --git a/posnext/public/js/pos_controller.js b/posnext/public/js/pos_controller.js index 3d3d239..f9b4eea 100644 --- a/posnext/public/js/pos_controller.js +++ b/posnext/public/js/pos_controller.js @@ -722,7 +722,11 @@ posnext.PointOfSale.Controller = class { field === "qty" ? value * item_row.conversion_factor : item_row.qty * value; - // await this.check_stock_availability(item_row, qty_needed, this.frm.doc.set_warehouse); + await this.check_stock_availability( + item_row, + qty_needed, + this.frm.doc.set_warehouse, + ); } if (this.is_current_item_being_edited(item_row) || from_selector) { @@ -790,6 +794,29 @@ posnext.PointOfSale.Controller = class { if (field === "serial_no") new_item["qty"] = value.split(`\n`).length || 0; + + if (!this.allow_negative_stock) { + const stock_resp = ( + await this.get_available_stock( + item_code, + this.frm.doc.set_warehouse, + ) + ).message; + const available_qty = stock_resp[0]; + const is_stock_item = stock_resp[1]; + if (is_stock_item && !(available_qty > 0)) { + frappe.show_alert({ + message: __( + "Item Code: {0} is not available under warehouse {1}.", + [item_code.bold(), this.frm.doc.set_warehouse.bold()], + ), + indicator: "red", + }); + frappe.utils.play_sound("error"); + return; + } + } + item_row = this.frm.add_child("items", new_item); await this.trigger_new_item_events(item_row); diff --git a/posnext/public/js/pos_item_selector.js b/posnext/public/js/pos_item_selector.js index 722da02..f2ee927 100644 --- a/posnext/public/js/pos_item_selector.js +++ b/posnext/public/js/pos_item_selector.js @@ -59,6 +59,7 @@ posnext.PointOfSale.ItemSelector = class { this.reload_status = reload_status; this.auto_add_item = settings.auto_add_item_to_cart; this.auto_search_serial = settings.custom_auto_search_serial_number; + this.auto_add_barcode_scan = settings.custom_add_via_barcode_scan; if (settings.custom_default_view) { view = settings.custom_default_view; } @@ -690,6 +691,7 @@ posnext.PointOfSale.ItemSelector = class { this.$component.find(".total-incoming-rate").html(""); this.$component.find(".item-group-field").html(""); this.$component.find(".invoice-posting-date").html(""); + this.$component.find(".barcode-scan-field").remove(); frappe.db .get_single_value("POS Settings", "custom_profile_lock") .then((doc) => { @@ -728,6 +730,23 @@ posnext.PointOfSale.ItemSelector = class { render_input: true, }); + if (this.auto_add_barcode_scan) { + this.$component + .find(".filter-section") + .append( + `
`, + ); + this.barcode_scan_field = frappe.ui.form.make_control({ + df: { + label: __("Barcode"), + fieldtype: "Data", + placeholder: __("Scan barcode to add item"), + }, + parent: this.$component.find(".barcode-scan-field"), + render_input: true, + }); + } + this.item_group_field = frappe.ui.form.make_control({ df: { label: __("Item Group"), @@ -782,6 +801,9 @@ posnext.PointOfSale.ItemSelector = class { this.search_field.toggle_label(false); this.item_group_field.toggle_label(false); + if (this.auto_add_barcode_scan) { + this.barcode_scan_field.toggle_label(false); + } if (this.custom_show_last_incoming_rate) { this.total_incoming_rate.toggle_label(false); } @@ -814,6 +836,10 @@ posnext.PointOfSale.ItemSelector = class { $(this.search_field.$input[0]).val(value).trigger("input"); } + set_barcode_value(value) { + $(this.barcode_scan_field.$input[0]).val(value).trigger("input"); + } + bind_events() { const me = this; if (!window.onScan) { @@ -853,10 +879,14 @@ posnext.PointOfSale.ItemSelector = class { onScan.attachTo(document, { onScan: (sScancode) => { - if (this.search_field && this.$component.is(":visible")) { - this.search_field.set_focus(); - this.set_search_value(sScancode); - this.barcode_scanned = true; + if (this.$component.is(":visible")) { + if (this.auto_add_barcode_scan && this.barcode_scan_field) { + this.set_barcode_value(sScancode); + } else if (this.search_field) { + this.search_field.set_focus(); + this.set_search_value(sScancode); + this.barcode_scanned = true; + } } }, }); @@ -908,6 +938,19 @@ posnext.PointOfSale.ItemSelector = class { // ); }); + if (this.auto_add_barcode_scan && this.barcode_scan_field) { + this.barcode_scan_field.$input.on("input", (e) => { + clearTimeout(this.last_barcode_scan); + this.last_barcode_scan = setTimeout(() => { + const barcode_term = e.target.value; + if (!barcode_term) return; + this.get_items({ search_term: barcode_term }).then(({ message }) => { + this.add_exact_barcode_item(message.items, barcode_term); + }); + }, 100); + }); + } + // this.search_field.$input.on('focus', () => { // this.$clear_search_btn.toggle( // Boolean(this.search_field.$input.val()) @@ -993,6 +1036,33 @@ posnext.PointOfSale.ItemSelector = class { this.set_search_value(""); } + add_exact_barcode_item(items, barcode_term) { + const term = barcode_term.toLowerCase(); + const exact = items.find( + (i) => + i.item_code.toLowerCase() === term || + (i.barcode && i.barcode.toLowerCase() === term), + ); + if (exact) { + // Render matched items so the DOM data-attributes are populated, then + // click the exact wrapper — identical code path to a manual item click. + this.render_item_list(items); + this.$items_container + .find(`.item-wrapper[data-item-code="${escape(exact.item_code)}"]`) + .click(); + frappe.utils.play_sound("submit"); + // Restore the full item list + this.filter_items(); + } else { + frappe.show_alert({ + message: __("No items found. Scan barcode again."), + indicator: "orange", + }); + frappe.utils.play_sound("error"); + } + this.set_barcode_value(""); + } + resize_selector(minimize) { minimize ? this.$component