Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(stock): Pick List Scan #30832

Merged
merged 30 commits into from
May 11, 2022
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
e04fbb6
feat: pick list scan fields
dj12djdjs Apr 28, 2022
159cf28
fix: get correct row to modify with duplicate item_codes and max_qty …
dj12djdjs Apr 28, 2022
ceffbf2
fix: show alert when maximum qty scanned is reached
dj12djdjs Apr 28, 2022
8053f2d
feat: increment picked_qty on scan_barcode
dj12djdjs Apr 28, 2022
24d8f62
revert: barcode scan field from pick list item.
dj12djdjs Apr 29, 2022
2554cdc
revert: scan_mode flag
dj12djdjs Apr 29, 2022
472e634
Merge branch 'develop' into feat-picklist-scan
dj12djdjs Apr 29, 2022
4812891
fix: syntax
dj12djdjs Apr 29, 2022
5f8f83c
fix: warn user pick list is not complete instead of auto fulfilling p…
dj12djdjs Apr 29, 2022
fa1378d
fix: prevent user from proceeding without all qty picked.
dj12djdjs Apr 29, 2022
5ff471e
fix: linter
dj12djdjs Apr 29, 2022
6cc83d6
Merge branch 'feat-picklist-scan' of github.com:dj12djdjs/erpnext int…
dj12djdjs May 2, 2022
afb841c
Merge branch 'develop' into feat-picklist-scan
dj12djdjs May 3, 2022
c2335ec
fix(test): manually select picked_qty since before_submit validates q…
dj12djdjs May 3, 2022
5d8e3f2
Merge branch 'feat-picklist-scan' of /home/devin/repos/erpnext into f…
dj12djdjs May 3, 2022
4a19c1c
Revert "fix(test): manually select picked_qty since before_submit val…
dj12djdjs May 4, 2022
5560cec
fix: max qty message
dj12djdjs May 4, 2022
7ae89de
fix: cleanup dont_allow_new_row logic
dj12djdjs May 4, 2022
d48ab81
fix: auto-fulfill picking list when not in scan mode
ankush Mar 28, 2022
47b4251
feat: prompt qty on scan
dj12djdjs May 5, 2022
0a77c28
fix: cast value to Number type
dj12djdjs May 5, 2022
e9cf5cb
feat: add prompt qty flag to pick list
dj12djdjs May 5, 2022
a4f0be6
Merge branch 'develop' into feat-picklist-scan
ankush May 11, 2022
d3121fd
fix(UX): hide scan section for non-draft docs
ankush May 11, 2022
372d2d8
fix: better message for picked qty shortfall
ankush May 11, 2022
7f14222
docs: document barcode_scanner API
ankush May 11, 2022
d35a13e
refactor: change alert duration to 3 and modern js
ankush May 11, 2022
2f1d118
fix: disable serial no scanning for picklist
ankush May 11, 2022
ab80783
refactor: single function to fetch related row
ankush May 11, 2022
861d4b8
chore: bump ecma version
ankush May 11, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 59 additions & 53 deletions erpnext/public/js/utils/barcode_scanner.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
this.serial_no_field = opts.serial_no_field || "serial_no";
this.batch_no_field = opts.batch_no_field || "batch_no";
this.qty_field = opts.qty_field || "qty";
// field name on row which defines max quantity to be scanned e.g. picklist
this.max_qty_field = opts.max_qty_field;
// scanner won't add a new row if this flag is set.
this.dont_allow_new_row = opts.dont_allow_new_row;
// scanner will ask user to type the quantity instead of incrementing by 1
this.prompt_qty = opts.prompt_qty;

this.items_table_name = opts.items_table_name || "items";
this.items_table = this.frm.doc[this.items_table_name];
Expand Down Expand Up @@ -42,10 +48,7 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
.then((r) => {
const data = r && r.message;
if (!data || Object.keys(data).length === 0) {
frappe.show_alert({
message: __("Cannot find Item with this Barcode"),
indicator: "red",
});
this.show_alert(__("Cannot find Item with this Barcode"), "red");
this.clean_up();
return;
}
Expand All @@ -56,22 +59,18 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {

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);

if (batch_no_scan) {
row = this.get_batch_row_to_modify(batch_no);
} else {
// serial or barcode scan
row = this.get_row_to_modify_on_scan(item_code);
}
let row = this.get_row_to_modify_on_scan(item_code, batch_no);

if (!row) {
if (this.dont_allow_new_row) {
this.show_alert(__("Maximum quantity scanned for item {0}.", [item_code]), "red");
this.clean_up();
return;
}

// add new row if new item/batch is scanned
row = frappe.model.add_child(this.frm.doc, cur_grid.doctype, this.items_table_name);
// trigger any row add triggers defined on child table.
Expand All @@ -83,9 +82,10 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
return;
}

this.show_scan_message(row.idx, row.item_code);
this.set_selector_trigger_flag(row, data);
this.set_item(row, item_code);
this.set_item(row, item_code).then(qty => {
this.show_scan_message(row.idx, row.item_code, qty);
});
this.set_serial_no(row, serial_no);
this.set_batch_no(row, batch_no);
this.set_barcode(row, barcode);
Expand All @@ -106,9 +106,23 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
}

set_item(row, item_code) {
const item_data = { item_code: item_code };
item_data[this.qty_field] = (row[this.qty_field] || 0) + 1;
frappe.model.set_value(row.doctype, row.name, item_data);
return new Promise(resolve => {
const increment = (value = 1) => {
const item_data = {item_code: item_code};
item_data[this.qty_field] = Number((row[this.qty_field] || 0)) + Number(value);
frappe.model.set_value(row.doctype, row.name, item_data);
};

if (this.prompt_qty) {
frappe.prompt(__("Please enter quantity for item {0}", [item_code]), ({value}) => {
increment(value);
resolve(value);
});
} else {
increment();
resolve();
}
});
}

set_serial_no(row, serial_no) {
Expand Down Expand Up @@ -137,53 +151,42 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
}
}

show_scan_message(idx, exist = null) {
show_scan_message(idx, exist = null, qty = 1) {
// show new row or qty increase toast
if (exist) {
frappe.show_alert(
{
message: __("Row #{0}: Qty increased by 1", [idx]),
indicator: "green",
},
5
);
this.show_alert(__("Row #{0}: Qty increased by {1}", [idx, qty]), "green");
} else {
frappe.show_alert(
{
message: __("Row #{0}: Item added", [idx]),
indicator: "green",
},
5
);
this.show_alert(__("Row #{0}: Item added", [idx]), "green")
}
}

is_duplicate_serial_no(row, serial_no) {
const is_duplicate = !!serial_no && !!row[this.serial_no_field]
&& row[this.serial_no_field].includes(serial_no);
const is_duplicate = row[this.serial_no_field]?.includes(serial_no);

if (is_duplicate) {
frappe.show_alert(
{
message: __("Serial No {0} is already added", [serial_no]),
indicator: "orange",
},
5
);
this.show_alert(__("Serial No {0} is already added", [serial_no]), "orange");
}
return is_duplicate;
}

get_batch_row_to_modify(batch_no) {
// get row if batch already exists in table
const existing_batch_row = this.items_table.find((d) => d.batch_no === batch_no);
return existing_batch_row || this.get_existing_blank_row();
}
get_row_to_modify_on_scan(item_code, batch_no) {
let cur_grid = this.frm.fields_dict[this.items_table_name].grid;

// Check if batch is scanned and table has batch no field
let is_batch_no_scan = batch_no && frappe.meta.has_field(cur_grid.doctype, this.batch_no_field);
let check_max_qty = this.max_qty_field && frappe.meta.has_field(cur_grid.doctype, this.max_qty_field);

get_row_to_modify_on_scan(item_code) {
// get an existing item row to increment or blank row to modify
const existing_item_row = this.items_table.find((d) => d.item_code === item_code);
return existing_item_row || this.get_existing_blank_row();
const matching_row = (row) => {
const item_match = row.item_code == item_code;
const batch_match = row.batch_no == batch_no;
const qty_in_limit = flt(row[this.qty_field]) < flt(row[this.max_qty_field]);

return item_match
&& (!is_batch_no_scan || batch_match)
&& (!check_max_qty || qty_in_limit)
}

return this.items_table.find(matching_row) || this.get_existing_blank_row();
}

get_existing_blank_row() {
Expand All @@ -194,4 +197,7 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
this.scan_barcode_field.set_value("");
refresh_field(this.items_table_name);
}
show_alert(msg, indicator, duration=3) {
frappe.show_alert({message: msg, indicator: indicator}, duration);
}
};
13 changes: 13 additions & 0 deletions erpnext/stock/doctype/pick_list/pick_list.js
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,19 @@ frappe.ui.form.on('Pick List', {
get_query_filters: get_query_filters
});
});
},
scan_barcode: (frm) => {
const opts = {
frm,
items_table_name: 'locations',
qty_field: 'picked_qty',
max_qty_field: 'qty',
dont_allow_new_row: true,
prompt_qty: frm.doc.prompt_qty,
serial_no_field: "not_supported", // doesn't make sense for picklist without a separate field.
};
const barcode_scanner = new erpnext.utils.BarcodeScanner(opts);
barcode_scanner.process_scan();
}
});

Expand Down
35 changes: 34 additions & 1 deletion erpnext/stock/doctype/pick_list/pick_list.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@
"parent_warehouse",
"get_item_locations",
"section_break_6",
"scan_barcode",
"column_break_13",
"scan_mode",
"prompt_qty",
"section_break_15",
"locations",
"amended_from",
"print_settings_section",
Expand All @@ -36,6 +41,7 @@
"fieldtype": "Column Break"
},
{
"depends_on": "eval:!doc.docstatus",
"fieldname": "section_break_6",
"fieldtype": "Section Break"
},
Expand Down Expand Up @@ -126,11 +132,38 @@
"fieldtype": "Check",
"label": "Group Same Items",
"print_hide": 1
},
{
"fieldname": "section_break_15",
"fieldtype": "Section Break"
},
{
"fieldname": "scan_barcode",
"fieldtype": "Data",
"label": "Scan Barcode",
"options": "Barcode"
},
{
"fieldname": "column_break_13",
"fieldtype": "Column Break"
},
{
"default": "0",
"description": "If checked, picked qty won't automatically be fulfilled on submit of pick list.",
"fieldname": "scan_mode",
"fieldtype": "Check",
"label": "Scan Mode"
},
{
"default": "0",
"fieldname": "prompt_qty",
"fieldtype": "Check",
"label": "Prompt Qty"
}
],
"is_submittable": 1,
"links": [],
"modified": "2022-04-21 07:56:40.646473",
"modified": "2022-05-11 09:09:53.029312",
"modified_by": "Administrator",
"module": "Stock",
"name": "Pick List",
Expand Down
11 changes: 9 additions & 2 deletions erpnext/stock/doctype/pick_list/pick_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,15 @@ def before_save(self):
def before_submit(self):
update_sales_orders = set()
for item in self.locations:
# if the user has not entered any picked qty, set it to stock_qty, before submit
if item.picked_qty == 0:
if self.scan_mode and item.picked_qty < item.stock_qty:
frappe.throw(
_(
"Row {0} picked quantity is less than the required quantity, additional {1} {2} required."
).format(item.idx, item.stock_qty - item.picked_qty, item.stock_uom),
title=_("Pick List Incomplete"),
)
elif not self.scan_mode and item.picked_qty == 0:
# if the user has not entered any picked qty, set it to stock_qty, before submit
item.picked_qty = item.stock_qty

if item.sales_order_item:
Expand Down
2 changes: 1 addition & 1 deletion erpnext/stock/doctype/pick_list_item/pick_list_item.json
Original file line number Diff line number Diff line change
Expand Up @@ -202,4 +202,4 @@
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@
"options": "Warehouse"
},
{
"depends_on": "eval:!doc.docstatus",
"fieldname": "section_break_22",
"fieldtype": "Section Break"
},
Expand All @@ -182,7 +183,7 @@
"idx": 1,
"is_submittable": 1,
"links": [],
"modified": "2022-03-27 08:57:47.161959",
"modified": "2022-05-11 09:10:26.327652",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Reconciliation",
Expand Down