feat: auto set batch no on serial no selection (#20757)
* feat: auto set batch no on serial no selection * fix: dialog not shown if set warehouse selected * fix: typo * fix: merge conflict * fix: callback no getting called after serial no selected * fix: available batch qty not fetched without set_warehouse selected * fix: item batch not synced with dialog batch table
This commit is contained in:
parent
1aa8c2ecc4
commit
faea85451f
@ -4,7 +4,7 @@
|
|||||||
erpnext.TransactionController = erpnext.taxes_and_totals.extend({
|
erpnext.TransactionController = erpnext.taxes_and_totals.extend({
|
||||||
setup: function() {
|
setup: function() {
|
||||||
this._super();
|
this._super();
|
||||||
frappe.flags.hide_serial_batch_dialog = false;
|
frappe.flags.hide_serial_batch_dialog = true;
|
||||||
frappe.ui.form.on(this.frm.doctype + " Item", "rate", function(frm, cdt, cdn) {
|
frappe.ui.form.on(this.frm.doctype + " Item", "rate", function(frm, cdt, cdn) {
|
||||||
var item = frappe.get_doc(cdt, cdn);
|
var item = frappe.get_doc(cdt, cdn);
|
||||||
var has_margin_field = frappe.meta.has_field(cdt, 'margin_type');
|
var has_margin_field = frappe.meta.has_field(cdt, 'margin_type');
|
||||||
@ -519,6 +519,15 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
() => me.toggle_conversion_factor(item),
|
() => me.toggle_conversion_factor(item),
|
||||||
|
() => {
|
||||||
|
if (show_batch_dialog)
|
||||||
|
return frappe.db.get_value("Item", item.item_code, ["has_batch_no", "has_serial_no"])
|
||||||
|
.then((r) => {
|
||||||
|
if(r.message.has_batch_no || r.message.has_serial_no) {
|
||||||
|
frappe.flags.hide_serial_batch_dialog = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
() => {
|
() => {
|
||||||
if(show_batch_dialog && !frappe.flags.hide_serial_batch_dialog) {
|
if(show_batch_dialog && !frappe.flags.hide_serial_batch_dialog) {
|
||||||
var d = locals[cdt][cdn];
|
var d = locals[cdt][cdn];
|
||||||
@ -528,7 +537,9 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
|
|||||||
|
|
||||||
erpnext.show_serial_batch_selector(me.frm, d, (item) => {
|
erpnext.show_serial_batch_selector(me.frm, d, (item) => {
|
||||||
me.frm.script_manager.trigger('qty', item.doctype, item.name);
|
me.frm.script_manager.trigger('qty', item.doctype, item.name);
|
||||||
});
|
if (!me.frm.doc.set_warehouse)
|
||||||
|
me.frm.script_manager.trigger('warehouse', item.doctype, item.name);
|
||||||
|
}, undefined, !frappe.flags.hide_serial_batch_dialog);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
() => me.conversion_factor(doc, cdt, cdn, true),
|
() => me.conversion_factor(doc, cdt, cdn, true),
|
||||||
|
@ -5,14 +5,13 @@ erpnext.SerialNoBatchSelector = Class.extend({
|
|||||||
this.show_dialog = show_dialog;
|
this.show_dialog = show_dialog;
|
||||||
// frm, item, warehouse_details, has_batch, oldest
|
// frm, item, warehouse_details, has_batch, oldest
|
||||||
let d = this.item;
|
let d = this.item;
|
||||||
if (d && d.has_batch_no && (!d.batch_no || this.show_dialog)) {
|
this.has_batch = 0; this.has_serial_no = 0;
|
||||||
this.has_batch = 1;
|
|
||||||
this.setup();
|
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
|
// !(this.show_dialog == false) ensures that show_dialog is implictly true, even when undefined
|
||||||
} else if(d && d.has_serial_no && !(this.show_dialog == false)) {
|
if(d && d.has_serial_no && !(this.show_dialog == false)) this.has_serial_no = 1;
|
||||||
this.has_batch = 0;
|
|
||||||
this.setup();
|
this.setup();
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
setup: function() {
|
setup: function() {
|
||||||
@ -36,16 +35,16 @@ erpnext.SerialNoBatchSelector = Class.extend({
|
|||||||
label: __('Item Code'),
|
label: __('Item Code'),
|
||||||
default: me.item_code
|
default: me.item_code
|
||||||
},
|
},
|
||||||
{fieldtype:'Column Break'},
|
|
||||||
{
|
{
|
||||||
fieldname: 'warehouse',
|
fieldname: 'warehouse',
|
||||||
fieldtype:'Link',
|
fieldtype:'Link',
|
||||||
options: 'Warehouse',
|
options: 'Warehouse',
|
||||||
|
reqd: me.has_batch && !me.has_serial_no ? 0 : 1,
|
||||||
label: __(me.warehouse_details.type),
|
label: __(me.warehouse_details.type),
|
||||||
default: me.warehouse_details.name,
|
default: typeof me.warehouse_details.name == "string" ? me.warehouse_details.name : '',
|
||||||
onchange: function(e) {
|
onchange: function(e) {
|
||||||
|
|
||||||
if(me.has_batch) {
|
if(me.has_batch && !me.has_serial_no) {
|
||||||
fields = fields.concat(me.get_batch_fields());
|
fields = fields.concat(me.get_batch_fields());
|
||||||
} else {
|
} else {
|
||||||
fields = fields.concat(me.get_serial_no_fields());
|
fields = fields.concat(me.get_serial_no_fields());
|
||||||
@ -74,15 +73,16 @@ erpnext.SerialNoBatchSelector = Class.extend({
|
|||||||
{
|
{
|
||||||
fieldname: 'qty',
|
fieldname: 'qty',
|
||||||
fieldtype:'Float',
|
fieldtype:'Float',
|
||||||
read_only: me.has_batch,
|
read_only: me.has_batch && !me.has_serial_no,
|
||||||
label: __(me.has_batch ? 'Total Qty' : 'Qty'),
|
label: __(me.has_batch && !me.has_serial_no ? 'Total Qty' : 'Qty'),
|
||||||
default: 0
|
default: 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldname: 'auto_fetch_button',
|
fieldname: 'auto_fetch_button',
|
||||||
fieldtype:'Button',
|
fieldtype:'Button',
|
||||||
hidden: me.has_batch,
|
hidden: me.has_batch && !me.has_serial_no,
|
||||||
label: __('Fetch based on FIFO'),
|
label: __('Auto Fetch'),
|
||||||
|
description: __('Fetch Serial Numbers based on FIFO'),
|
||||||
click: () => {
|
click: () => {
|
||||||
let qty = this.dialog.fields_dict.qty.get_value();
|
let qty = this.dialog.fields_dict.qty.get_value();
|
||||||
let numbers = frappe.call({
|
let numbers = frappe.call({
|
||||||
@ -90,7 +90,7 @@ erpnext.SerialNoBatchSelector = Class.extend({
|
|||||||
args: {
|
args: {
|
||||||
qty: qty,
|
qty: qty,
|
||||||
item_code: me.item_code,
|
item_code: me.item_code,
|
||||||
warehouse: me.warehouse_details.name,
|
warehouse: typeof me.warehouse_details.name == "string" ? me.warehouse_details.name : '',
|
||||||
batch_no: me.item.batch_no || null
|
batch_no: me.item.batch_no || null
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -109,10 +109,12 @@ erpnext.SerialNoBatchSelector = Class.extend({
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
if (this.has_batch) {
|
if (this.has_batch && !this.has_serial_no) {
|
||||||
title = __("Select Batch Numbers");
|
title = __("Select Batch Numbers");
|
||||||
fields = fields.concat(this.get_batch_fields());
|
fields = fields.concat(this.get_batch_fields());
|
||||||
} else {
|
} 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");
|
title = __("Select Serial Numbers");
|
||||||
fields = fields.concat(this.get_serial_no_fields());
|
fields = fields.concat(this.get_serial_no_fields());
|
||||||
}
|
}
|
||||||
@ -122,25 +124,31 @@ erpnext.SerialNoBatchSelector = Class.extend({
|
|||||||
fields: fields
|
fields: fields
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.item.serial_no) {
|
|
||||||
this.dialog.fields_dict.serial_no.set_value(this.item.serial_no);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.dialog.set_primary_action(__('Insert'), function() {
|
this.dialog.set_primary_action(__('Insert'), function() {
|
||||||
me.values = me.dialog.get_values();
|
me.values = me.dialog.get_values();
|
||||||
if(me.validate()) {
|
if(me.validate()) {
|
||||||
me.set_items();
|
frappe.run_serially([
|
||||||
me.dialog.hide();
|
() => me.update_batch_items(),
|
||||||
|
() => me.update_serial_no_item(),
|
||||||
|
() => me.update_batch_serial_no_items(),
|
||||||
|
() => {
|
||||||
|
refresh_field("items");
|
||||||
|
if (me.callback) {
|
||||||
|
return me.callback(me.item);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
() => me.dialog.hide()
|
||||||
|
])
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if(this.show_dialog) {
|
if(this.show_dialog) {
|
||||||
let d = this.item;
|
let d = this.item;
|
||||||
if (d.has_serial_no && d.serial_no) {
|
if (this.item.serial_no) {
|
||||||
this.dialog.set_value('serial_no', d.serial_no);
|
this.dialog.fields_dict.serial_no.set_value(this.item.serial_no);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (d.has_batch_no && d.batch_no) {
|
if (this.has_batch && !this.has_serial_no && d.batch_no) {
|
||||||
this.frm.doc.items.forEach(data => {
|
this.frm.doc.items.forEach(data => {
|
||||||
if(data.item_code == d.item_code) {
|
if(data.item_code == d.item_code) {
|
||||||
this.dialog.fields_dict.batches.df.data.push({
|
this.dialog.fields_dict.batches.df.data.push({
|
||||||
@ -155,7 +163,7 @@ erpnext.SerialNoBatchSelector = Class.extend({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.has_batch) {
|
if (this.has_batch && !this.has_serial_no) {
|
||||||
this.update_total_qty();
|
this.update_total_qty();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -174,7 +182,7 @@ erpnext.SerialNoBatchSelector = Class.extend({
|
|||||||
frappe.throw(__("Please select a warehouse"));
|
frappe.throw(__("Please select a warehouse"));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if(this.has_batch) {
|
if(this.has_batch && !this.has_serial_no) {
|
||||||
if(values.batches.length === 0 || !values.batches) {
|
if(values.batches.length === 0 || !values.batches) {
|
||||||
frappe.throw(__("Please select batches for batched item "
|
frappe.throw(__("Please select batches for batched item "
|
||||||
+ values.item_code));
|
+ values.item_code));
|
||||||
@ -193,34 +201,23 @@ erpnext.SerialNoBatchSelector = Class.extend({
|
|||||||
} else {
|
} else {
|
||||||
let serial_nos = values.serial_no || '';
|
let serial_nos = values.serial_no || '';
|
||||||
if (!serial_nos || !serial_nos.replace(/\s/g, '').length) {
|
if (!serial_nos || !serial_nos.replace(/\s/g, '').length) {
|
||||||
if (!this.show_dialog) {
|
frappe.throw(__("Please enter serial numbers for serialized item "
|
||||||
frappe.throw(__("Please enter serial numbers for serialized item "
|
+ values.item_code));
|
||||||
+ values.item_code));
|
return false;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
set_items: function() {
|
update_batch_items() {
|
||||||
var me = this;
|
// clones an items if muliple batches are selected.
|
||||||
if(this.has_batch) {
|
if(this.has_batch && !this.has_serial_no) {
|
||||||
this.values.batches.map((batch, i) => {
|
this.values.batches.map((batch, i) => {
|
||||||
let batch_no = batch.batch_no;
|
let batch_no = batch.batch_no;
|
||||||
let row = '';
|
let row = '';
|
||||||
|
|
||||||
if (i !== 0 && !this.batch_exists(batch_no)) {
|
if (i !== 0 && !this.batch_exists(batch_no)) {
|
||||||
row = this.frm.add_child("items", {
|
row = this.frm.add_child("items", { ...this.item });
|
||||||
'item_code': this.item.item_code,
|
|
||||||
'item_name': this.item.item_name,
|
|
||||||
'price_list_rate': this.item.price_list_rate,
|
|
||||||
'rate': this.item.rate,
|
|
||||||
'qty': batch.selected_qty,
|
|
||||||
'batch_no': batch_no,
|
|
||||||
'actual_qty': this.item.actual_qty,
|
|
||||||
'discount_percentage': this.item.discount_percentage
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
row = this.frm.doc.items.find(i => i.batch_no === batch_no);
|
row = this.frm.doc.items.find(i => i.batch_no === batch_no);
|
||||||
}
|
}
|
||||||
@ -228,16 +225,59 @@ erpnext.SerialNoBatchSelector = Class.extend({
|
|||||||
if (!row) {
|
if (!row) {
|
||||||
row = this.item;
|
row = this.item;
|
||||||
}
|
}
|
||||||
|
// this ensures that qty & batch no is set
|
||||||
this.map_row_values(row, batch, 'batch_no',
|
this.map_row_values(row, batch, 'batch_no',
|
||||||
'selected_qty', this.values.warehouse);
|
'selected_qty', this.values.warehouse);
|
||||||
});
|
});
|
||||||
} else {
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
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');
|
this.map_row_values(this.item, this.values, 'serial_no', 'qty');
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
refresh_field("items");
|
update_batch_serial_no_items() {
|
||||||
this.callback && this.callback(this.item);
|
// 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: function(batch) {
|
batch_exists: function(batch) {
|
||||||
@ -287,7 +327,7 @@ erpnext.SerialNoBatchSelector = Class.extend({
|
|||||||
return {
|
return {
|
||||||
filters: {
|
filters: {
|
||||||
item_code: me.item_code,
|
item_code: me.item_code,
|
||||||
warehouse: me.warehouse || me.warehouse_details.name
|
warehouse: me.warehouse || typeof me.warehouse_details.name == "string" ? me.warehouse_details.name : ''
|
||||||
},
|
},
|
||||||
query: 'erpnext.controllers.queries.get_batch_no'
|
query: 'erpnext.controllers.queries.get_batch_no'
|
||||||
};
|
};
|
||||||
@ -448,7 +488,7 @@ erpnext.SerialNoBatchSelector = Class.extend({
|
|||||||
{
|
{
|
||||||
fieldname: 'serial_no',
|
fieldname: 'serial_no',
|
||||||
fieldtype: 'Small Text',
|
fieldtype: 'Small Text',
|
||||||
label: __(me.has_batch ? 'Selected Batch Numbers' : 'Selected Serial Numbers'),
|
label: __(me.has_batch && !me.has_serial_no ? 'Selected Batch Numbers' : 'Selected Serial Numbers'),
|
||||||
onchange: function() {
|
onchange: function() {
|
||||||
me.serial_list = this.get_value()
|
me.serial_list = this.get_value()
|
||||||
.replace(/\n/g, ' ').match(/\S+/g) || [];
|
.replace(/\n/g, ' ').match(/\S+/g) || [];
|
||||||
|
@ -413,15 +413,20 @@ erpnext.selling.SellingController = erpnext.TransactionController.extend({
|
|||||||
*/
|
*/
|
||||||
set_batch_number: function(cdt, cdn) {
|
set_batch_number: function(cdt, cdn) {
|
||||||
const doc = frappe.get_doc(cdt, cdn);
|
const doc = frappe.get_doc(cdt, cdn);
|
||||||
if (doc && doc.has_batch_no) {
|
if (doc && doc.has_batch_no && doc.warehouse) {
|
||||||
this._set_batch_number(doc);
|
this._set_batch_number(doc);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_set_batch_number: function(doc) {
|
_set_batch_number: function(doc) {
|
||||||
|
let args = {'item_code': doc.item_code, 'warehouse': doc.warehouse, 'qty': flt(doc.qty) * flt(doc.conversion_factor)};
|
||||||
|
if (doc.has_serial_no && doc.serial_no) {
|
||||||
|
args['serial_no'] = doc.serial_no
|
||||||
|
}
|
||||||
|
|
||||||
return frappe.call({
|
return frappe.call({
|
||||||
method: 'erpnext.stock.doctype.batch.batch.get_batch_no',
|
method: 'erpnext.stock.doctype.batch.batch.get_batch_no',
|
||||||
args: {'item_code': doc.item_code, 'warehouse': doc.warehouse, 'qty': flt(doc.qty) * flt(doc.conversion_factor)},
|
args: args,
|
||||||
callback: function(r) {
|
callback: function(r) {
|
||||||
if(r.message) {
|
if(r.message) {
|
||||||
frappe.model.set_value(doc.doctype, doc.name, 'batch_no', r.message);
|
frappe.model.set_value(doc.doctype, doc.name, 'batch_no', r.message);
|
||||||
|
@ -523,12 +523,15 @@ def get_delivery_note_serial_no(item_code, qty, delivery_note):
|
|||||||
return serial_nos
|
return serial_nos
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def auto_fetch_serial_number(qty, item_code, warehouse, batch_no=None):
|
def auto_fetch_serial_number(qty, item_code, warehouse, batch_nos=None):
|
||||||
serial_numbers = frappe.get_list("Serial No", filters={
|
import json
|
||||||
|
filters = {
|
||||||
"item_code": item_code,
|
"item_code": item_code,
|
||||||
"warehouse": warehouse,
|
"warehouse": warehouse,
|
||||||
"batch_no": batch_no,
|
|
||||||
"delivery_document_no": "",
|
"delivery_document_no": "",
|
||||||
"sales_invoice": ""
|
"sales_invoice": ""
|
||||||
}, limit=qty, order_by="creation")
|
}
|
||||||
|
if batch_nos: filters["batch_no"] = ["in", json.loads(batch_nos)]
|
||||||
|
|
||||||
|
serial_numbers = frappe.get_list("Serial No", filters=filters, limit=qty, order_by="creation")
|
||||||
return [item['name'] for item in serial_numbers]
|
return [item['name'] for item in serial_numbers]
|
||||||
|
Loading…
x
Reference in New Issue
Block a user