Files
erpnext/erpnext/public/js/utils/serial_no_batch_selector.js
2025-12-22 16:47:38 +05:30

670 lines
18 KiB
JavaScript

erpnext.SerialNoBatchSelector = class SerialNoBatchSelector {
constructor(opts, show_dialog) {
$.extend(this, opts);
this.show_dialog = show_dialog;
// frm, item, warehouse_details, has_batch, oldest
let d = this.item;
this.has_batch = 0;
this.has_serial_no = 0;
if (d && d.has_batch_no && (!d.batch_no || this.show_dialog)) this.has_batch = 1;
// !(this.show_dialog == false) ensures that show_dialog is implictly true, even when undefined
if (d && d.has_serial_no && !(this.show_dialog == false)) this.has_serial_no = 1;
this.setup();
}
setup() {
this.item_code = this.item.item_code;
this.qty = this.item.qty;
this.make_dialog();
this.on_close_dialog();
}
make_dialog() {
var me = this;
this.data = this.oldest ? this.oldest : [];
let title = "";
let fields = [
{
fieldname: "item_code",
read_only: 1,
fieldtype: "Link",
options: "Item",
label: __("Item Code"),
default: me.item_code,
},
{
fieldname: "warehouse",
fieldtype: "Link",
options: "Warehouse",
reqd: me.has_batch && !me.has_serial_no ? 0 : 1,
label: __(me.warehouse_details.type),
default: typeof me.warehouse_details.name == "string" ? me.warehouse_details.name : "",
onchange: function (e) {
me.warehouse_details.name = this.get_value();
if (me.has_batch && !me.has_serial_no) {
fields = fields.concat(me.get_batch_fields());
} else {
fields = fields.concat(me.get_serial_no_fields());
}
var batches = this.layout.fields_dict.batches;
if (batches) {
batches.grid.df.data = [];
batches.grid.refresh();
batches.grid.add_new_row(null, null, null);
}
},
get_query: function () {
return {
query: "erpnext.controllers.queries.warehouse_query",
filters: [
["Bin", "item_code", "=", me.item_code],
["Warehouse", "is_group", "=", 0],
["Warehouse", "company", "=", me.frm.doc.company],
],
};
},
},
{ fieldtype: "Column Break" },
{
fieldname: "qty",
fieldtype: "Float",
read_only: me.has_batch && !me.has_serial_no,
label: __(me.has_batch && !me.has_serial_no ? "Selected Qty" : "Qty"),
default: flt(me.item.stock_qty) || flt(me.item.transfer_qty),
},
...get_pending_qty_fields(me),
{
fieldname: "uom",
read_only: 1,
fieldtype: "Link",
options: "UOM",
label: __("UOM"),
default: me.item.uom,
},
{
fieldname: "auto_fetch_button",
fieldtype: "Button",
hidden: me.has_batch && !me.has_serial_no,
label: __("Auto Fetch"),
description: __("Fetch Serial Numbers based on FIFO"),
click: () => {
let qty = this.dialog.fields_dict.qty.get_value();
let already_selected_serial_nos = get_selected_serial_nos(me);
let numbers = frappe.call({
method: "erpnext.stock.doctype.serial_no.serial_no.auto_fetch_serial_number",
args: {
qty: qty,
item_code: me.item_code,
warehouse:
typeof me.warehouse_details.name == "string" ? me.warehouse_details.name : "",
batch_nos: me.item.batch_no || null,
posting_date: me.frm.doc.posting_date || me.frm.doc.transaction_date,
exclude_sr_nos: already_selected_serial_nos,
},
});
numbers.then((data) => {
let auto_fetched_serial_numbers = data.message;
let records_length = auto_fetched_serial_numbers.length;
if (!records_length) {
const warehouse = me.dialog.fields_dict.warehouse.get_value().bold();
frappe.msgprint(
__(
"Serial numbers unavailable for Item {0} under warehouse {1}. Please try changing warehouse.",
[me.item.item_code.bold(), warehouse]
)
);
}
if (records_length < qty) {
frappe.msgprint(
__("Fetched only {0} available serial numbers.", [records_length])
);
}
let serial_no_list_field = this.dialog.fields_dict.serial_no;
numbers = auto_fetched_serial_numbers.join("\n");
serial_no_list_field.set_value(numbers);
});
},
},
];
if (this.has_batch && !this.has_serial_no) {
title = __("Select Batch Numbers");
fields = fields.concat(this.get_batch_fields());
} else {
// if only serial no OR
// if both batch_no & serial_no then only select serial_no and auto set batches nos
title = __("Select Serial Numbers");
fields = fields.concat(this.get_serial_no_fields());
}
this.dialog = new frappe.ui.Dialog({
title: title,
fields: fields,
});
this.dialog.set_primary_action(__("Insert"), function () {
me.values = me.dialog.get_values();
if (me.validate()) {
frappe.run_serially([
() => me.update_batch_items(),
() => me.update_serial_no_item(),
() => me.update_batch_serial_no_items(),
() => {
refresh_field("items");
refresh_field("packed_items");
if (me.callback) {
return me.callback(me.item);
}
},
() => me.dialog.hide(),
]);
}
});
if (this.show_dialog) {
let d = this.item;
if (this.item.serial_no) {
this.dialog.fields_dict.serial_no.set_value(this.item.serial_no);
}
if (this.has_batch && !this.has_serial_no && d.batch_no) {
this.frm.doc.items.forEach((data) => {
if (data.item_code == d.item_code) {
this.dialog.fields_dict.batches.df.data.push({
batch_no: data.batch_no,
actual_qty: data.actual_qty,
selected_qty: data.qty,
available_qty: data.actual_batch_qty,
});
}
});
this.dialog.fields_dict.batches.grid.refresh();
}
}
if (this.has_batch && !this.has_serial_no) {
this.update_total_qty();
this.update_pending_qtys();
}
this.dialog.show();
}
on_close_dialog() {
this.dialog.get_close_btn().on("click", () => {
this.on_close && this.on_close(this.item);
});
}
validate() {
let values = this.values;
if (!values.warehouse) {
frappe.throw(__("Please select a warehouse"));
return false;
}
if (this.has_batch && !this.has_serial_no) {
if (values.batches.length === 0 || !values.batches) {
frappe.throw(__("Please select batches for batched item {0}", [values.item_code]));
}
values.batches.map((batch, i) => {
if (!batch.selected_qty || batch.selected_qty === 0) {
if (!this.show_dialog) {
frappe.throw(__("Please select quantity on row {0}", [i + 1]));
}
}
});
return true;
} else {
let serial_nos = values.serial_no || "";
if (!serial_nos || !serial_nos.replace(/\s/g, "").length) {
frappe.throw(__("Please enter serial numbers for serialized item {0}", [values.item_code]));
}
return true;
}
}
update_batch_items() {
// clones an items if muliple batches are selected.
if (this.has_batch && !this.has_serial_no) {
this.values.batches.map((batch, i) => {
let batch_no = batch.batch_no;
let row = "";
if (i !== 0 && !this.batch_exists(batch_no)) {
row = this.frm.add_child("items", { ...this.item });
} else {
row = this.frm.doc.items.find((i) => i.batch_no === batch_no);
}
if (!row) {
row = this.item;
}
// this ensures that qty & batch no is set
this.map_row_values(row, batch, "batch_no", "selected_qty", this.values.warehouse);
});
}
}
update_serial_no_item() {
// just updates serial no for the item
if (this.has_serial_no && !this.has_batch) {
this.map_row_values(this.item, this.values, "serial_no", "qty");
}
}
update_batch_serial_no_items() {
// if serial no selected is from different batches, adds new rows for each batch.
if (this.has_batch && this.has_serial_no) {
const selected_serial_nos = this.values.serial_no.split(/\n/g).filter((s) => s);
return frappe.db
.get_list("Serial No", {
filters: { name: ["in", selected_serial_nos] },
fields: ["batch_no", "name"],
})
.then((data) => {
// data = [{batch_no: 'batch-1', name: "SR-001"},
// {batch_no: 'batch-2', name: "SR-003"}, {batch_no: 'batch-2', name: "SR-004"}]
const batch_serial_map = data.reduce((acc, d) => {
if (!acc[d["batch_no"]]) acc[d["batch_no"]] = [];
acc[d["batch_no"]].push(d["name"]);
return acc;
}, {});
// batch_serial_map = { "batch-1": ['SR-001'], "batch-2": ["SR-003", "SR-004"]}
Object.keys(batch_serial_map).map((batch_no, i) => {
let row = "";
const serial_no = batch_serial_map[batch_no];
if (i == 0) {
row = this.item;
this.map_row_values(
row,
{ qty: serial_no.length, batch_no: batch_no },
"batch_no",
"qty",
this.values.warehouse
);
} else if (!this.batch_exists(batch_no)) {
row = this.frm.add_child("items", { ...this.item });
row.batch_no = batch_no;
} else {
row = this.frm.doc.items.find((i) => i.batch_no === batch_no);
}
const values = {
qty: serial_no.length,
serial_no: serial_no.join("\n"),
};
this.map_row_values(row, values, "serial_no", "qty", this.values.warehouse);
});
});
}
}
batch_exists(batch) {
const batches = this.frm.doc.items.map((data) => data.batch_no);
return batches && in_list(batches, batch) ? true : false;
}
map_row_values(row, values, number, qty_field, warehouse) {
row.qty = values[qty_field];
row.transfer_qty = flt(values[qty_field]) * flt(row.conversion_factor);
row[number] = values[number];
if (this.warehouse_details.type === "Source Warehouse") {
row.s_warehouse = values.warehouse || warehouse;
} else if (this.warehouse_details.type === "Target Warehouse") {
row.t_warehouse = values.warehouse || warehouse;
} else {
row.warehouse = values.warehouse || warehouse;
}
this.frm.dirty();
}
update_total_qty() {
let qty_field = this.dialog.fields_dict.qty;
let total_qty = 0;
this.dialog.fields_dict.batches.df.data.forEach((data) => {
total_qty += flt(data.selected_qty);
});
qty_field.set_input(total_qty);
}
update_pending_qtys() {
const pending_qty_field = this.dialog.fields_dict.pending_qty;
const total_selected_qty_field = this.dialog.fields_dict.total_selected_qty;
if (!pending_qty_field || !total_selected_qty_field) return;
const me = this;
const required_qty = this.dialog.fields_dict.required_qty.value;
const selected_qty = this.dialog.fields_dict.qty.value;
const total_selected_qty = selected_qty + calc_total_selected_qty(me);
const pending_qty = required_qty - total_selected_qty;
pending_qty_field.set_input(pending_qty);
total_selected_qty_field.set_input(total_selected_qty);
}
get_batch_fields() {
var me = this;
return [
{ fieldtype: "Section Break", label: __("Batches") },
{
fieldname: "batches",
fieldtype: "Table",
label: __("Batch Entries"),
fields: [
{
fieldtype: "Link",
read_only: 0,
fieldname: "batch_no",
options: "Batch",
label: __("Select Batch"),
in_list_view: 1,
get_query: function () {
return {
filters: {
item_code: me.item_code,
warehouse:
me.warehouse || typeof me.warehouse_details.name == "string"
? me.warehouse_details.name
: "",
},
query: "erpnext.controllers.queries.get_batch_no",
};
},
change: function () {
const batch_no = this.get_value();
if (!batch_no) {
this.grid_row.on_grid_fields_dict.available_qty.set_value(0);
return;
}
let selected_batches = this.grid.grid_rows.map((row) => {
if (row === this.grid_row) {
return "";
}
if (row.on_grid_fields_dict.batch_no) {
return row.on_grid_fields_dict.batch_no.get_value();
}
});
if (selected_batches.includes(batch_no)) {
this.set_value("");
frappe.throw(__("Batch {0} already selected.", [batch_no]));
}
if (me.warehouse_details.name) {
frappe.call({
method: "erpnext.stock.doctype.batch.batch.get_batch_qty",
args: {
batch_no,
warehouse: me.warehouse_details.name,
item_code: me.item_code,
},
callback: (r) => {
this.grid_row.on_grid_fields_dict.available_qty.set_value(
r.message || 0
);
},
});
} else {
this.set_value("");
frappe.throw(__("Please select a warehouse to get available quantities"));
}
// e.stopImmediatePropagation();
},
},
{
fieldtype: "Float",
read_only: 1,
fieldname: "available_qty",
label: __("Available"),
in_list_view: 1,
default: 0,
change: function () {
this.grid_row.on_grid_fields_dict.selected_qty.set_value("0");
},
},
{
fieldtype: "Float",
read_only: 0,
fieldname: "selected_qty",
label: __("Qty"),
in_list_view: 1,
default: 0,
change: function () {
var batch_no = this.grid_row.on_grid_fields_dict.batch_no.get_value();
var available_qty = this.grid_row.on_grid_fields_dict.available_qty.get_value();
var selected_qty = this.grid_row.on_grid_fields_dict.selected_qty.get_value();
if (batch_no.length === 0 && parseInt(selected_qty) !== 0) {
frappe.throw(__("Please select a batch"));
}
if (
me.warehouse_details.type === "Source Warehouse" &&
parseFloat(available_qty) < parseFloat(selected_qty)
) {
this.set_value("0");
frappe.throw(
__(
"For transfer from source, selected quantity cannot be greater than available quantity"
)
);
} else {
this.grid.refresh();
}
me.update_total_qty();
me.update_pending_qtys();
},
},
],
in_place_edit: true,
data: this.data,
get_data: function () {
return this.data;
},
},
];
}
get_serial_no_fields() {
var me = this;
this.serial_list = [];
let serial_no_filters = {
item_code: me.item_code,
delivery_document_no: "",
};
if (this.item.batch_no) {
serial_no_filters["batch_no"] = this.item.batch_no;
}
if (me.warehouse_details.name) {
serial_no_filters["warehouse"] = me.warehouse_details.name;
}
if (me.frm.doc.doctype === "POS Invoice" && !this.showing_reserved_serial_nos_error) {
frappe
.call({
method: "erpnext.stock.doctype.serial_no.serial_no.get_pos_reserved_serial_nos",
args: {
filters: {
item_code: me.item_code,
warehouse:
typeof me.warehouse_details.name == "string" ? me.warehouse_details.name : "",
},
},
})
.then((data) => {
serial_no_filters["name"] = ["not in", data.message[0]];
});
}
return [
{ fieldtype: "Section Break", label: __("Serial Numbers") },
{
fieldtype: "Link",
fieldname: "serial_no_select",
options: "Serial No",
label: __("Select to add Serial Number."),
get_query: function () {
return {
filters: serial_no_filters,
};
},
onchange: function (e) {
if (this.in_local_change) return;
this.in_local_change = 1;
let serial_no_list_field = this.layout.fields_dict.serial_no;
let qty_field = this.layout.fields_dict.qty;
let new_number = this.get_value();
let list_value = serial_no_list_field.get_value();
let new_line = "\n";
if (!list_value) {
new_line = "";
} else {
me.serial_list = list_value.split(/\n/g) || [];
}
if (!me.serial_list.includes(new_number)) {
this.set_new_description("");
serial_no_list_field.set_value(me.serial_list.join("\n") + new_line + new_number);
me.serial_list = serial_no_list_field.get_value().split(/\n/g) || [];
} else {
this.set_new_description(new_number + " is already selected.");
}
me.serial_list = me.serial_list.filter((serial) => {
if (serial) {
return true;
}
});
qty_field.set_input(me.serial_list.length);
this.$input.val("");
this.in_local_change = 0;
},
},
{ fieldtype: "Section Break" },
{
fieldname: "serial_no",
fieldtype: "Text",
label: __(
me.has_batch && !me.has_serial_no ? "Selected Batch Numbers" : "Selected Serial Numbers"
),
onchange: function () {
me.serial_list = this.get_value().split(/\n/g);
me.serial_list = me.serial_list.filter((serial) => {
if (serial) {
return true;
}
});
this.layout.fields_dict.qty.set_input(me.serial_list.length);
},
},
];
}
};
function get_pending_qty_fields(me) {
if (!check_can_calculate_pending_qty(me)) return [];
const {
frm: {
doc: { fg_completed_qty },
},
item: { item_code, stock_qty },
} = me;
const { qty_consumed_per_unit } = erpnext.stock.bom.items[item_code];
const total_selected_qty = calc_total_selected_qty(me);
const required_qty = flt(fg_completed_qty) * flt(qty_consumed_per_unit);
const pending_qty = required_qty - (flt(stock_qty) + total_selected_qty);
const pending_qty_fields = [
{ fieldtype: "Section Break", label: __("Pending Quantity") },
{
fieldname: "required_qty",
read_only: 1,
fieldtype: "Float",
label: __("Required Qty"),
default: required_qty,
},
{ fieldtype: "Column Break" },
{
fieldname: "total_selected_qty",
read_only: 1,
fieldtype: "Float",
label: __("Total Selected Qty"),
default: total_selected_qty,
},
{ fieldtype: "Column Break" },
{
fieldname: "pending_qty",
read_only: 1,
fieldtype: "Float",
label: __("Pending Qty"),
default: pending_qty,
},
];
return pending_qty_fields;
}
// get all items with same item code except row for which selector is open.
function get_rows_with_same_item_code(me) {
const {
frm: {
doc: { items },
},
item: { name, item_code },
} = me;
return items.filter((item) => item.name !== name && item.item_code === item_code);
}
function calc_total_selected_qty(me) {
const totalSelectedQty = get_rows_with_same_item_code(me)
.map((item) => flt(item.qty))
.reduce((i, j) => i + j, 0);
return totalSelectedQty;
}
function get_selected_serial_nos(me) {
const selected_serial_nos = get_rows_with_same_item_code(me)
.map((item) => item.serial_no)
.filter((serial) => serial)
.map((sr_no_string) => sr_no_string.split("\n"))
.reduce((acc, arr) => acc.concat(arr), [])
.filter((serial) => serial);
return selected_serial_nos;
}
function check_can_calculate_pending_qty(me) {
const {
frm: { doc },
item,
} = me;
const docChecks =
doc.bom_no && doc.fg_completed_qty && erpnext.stock.bom && erpnext.stock.bom.name === doc.bom_no;
const itemChecks =
!!item &&
!item.original_item &&
erpnext.stock.bom &&
erpnext.stock.bom.items &&
item.item_code in erpnext.stock.bom.items;
return docChecks && itemChecks;
}
//# sourceURL=serial_no_batch_selector.js