diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index fea2d5e7009..1747932af65 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -4,7 +4,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ setup: function() { 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) { var item = frappe.get_doc(cdt, cdn); 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), + () => { + 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) { 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) => { 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), diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js index 02bced293f4..d75633e5a94 100644 --- a/erpnext/public/js/utils/serial_no_batch_selector.js +++ b/erpnext/public/js/utils/serial_no_batch_selector.js @@ -5,14 +5,13 @@ erpnext.SerialNoBatchSelector = Class.extend({ this.show_dialog = show_dialog; // frm, item, warehouse_details, has_batch, oldest let d = this.item; - if (d && d.has_batch_no && (!d.batch_no || this.show_dialog)) { - this.has_batch = 1; - this.setup(); + 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 - } else if(d && d.has_serial_no && !(this.show_dialog == false)) { - this.has_batch = 0; - this.setup(); - } + if(d && d.has_serial_no && !(this.show_dialog == false)) this.has_serial_no = 1; + + this.setup(); }, setup: function() { @@ -36,16 +35,16 @@ erpnext.SerialNoBatchSelector = Class.extend({ label: __('Item Code'), default: me.item_code }, - {fieldtype:'Column Break'}, { fieldname: 'warehouse', fieldtype:'Link', options: 'Warehouse', + reqd: me.has_batch && !me.has_serial_no ? 0 : 1, label: __(me.warehouse_details.type), - default: me.warehouse_details.name, + default: typeof me.warehouse_details.name == "string" ? me.warehouse_details.name : '', onchange: function(e) { - if(me.has_batch) { + if(me.has_batch && !me.has_serial_no) { fields = fields.concat(me.get_batch_fields()); } else { fields = fields.concat(me.get_serial_no_fields()); @@ -74,15 +73,16 @@ erpnext.SerialNoBatchSelector = Class.extend({ { fieldname: 'qty', fieldtype:'Float', - read_only: me.has_batch, - label: __(me.has_batch ? 'Total Qty' : 'Qty'), + read_only: me.has_batch && !me.has_serial_no, + label: __(me.has_batch && !me.has_serial_no ? 'Total Qty' : 'Qty'), default: 0 }, { fieldname: 'auto_fetch_button', fieldtype:'Button', - hidden: me.has_batch, - label: __('Fetch based on FIFO'), + 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 numbers = frappe.call({ @@ -90,7 +90,7 @@ erpnext.SerialNoBatchSelector = Class.extend({ args: { qty: qty, 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 } }); @@ -109,10 +109,12 @@ erpnext.SerialNoBatchSelector = Class.extend({ } ]; - if (this.has_batch) { + 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()); } @@ -122,25 +124,31 @@ erpnext.SerialNoBatchSelector = Class.extend({ 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() { me.values = me.dialog.get_values(); if(me.validate()) { - me.set_items(); - me.dialog.hide(); + frappe.run_serially([ + () => 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) { let d = this.item; - if (d.has_serial_no && d.serial_no) { - this.dialog.set_value('serial_no', d.serial_no); + if (this.item.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 => { if(data.item_code == d.item_code) { 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(); } @@ -174,7 +182,7 @@ erpnext.SerialNoBatchSelector = Class.extend({ frappe.throw(__("Please select a warehouse")); return false; } - if(this.has_batch) { + if(this.has_batch && !this.has_serial_no) { if(values.batches.length === 0 || !values.batches) { frappe.throw(__("Please select batches for batched item " + values.item_code)); @@ -193,34 +201,23 @@ erpnext.SerialNoBatchSelector = Class.extend({ } else { let serial_nos = values.serial_no || ''; if (!serial_nos || !serial_nos.replace(/\s/g, '').length) { - if (!this.show_dialog) { - frappe.throw(__("Please enter serial numbers for serialized item " - + values.item_code)); - return false; - } + frappe.throw(__("Please enter serial numbers for serialized item " + + values.item_code)); + return false; } return true; } }, - set_items: function() { - var me = this; - if(this.has_batch) { + 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", { - '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 - }); + row = this.frm.add_child("items", { ...this.item }); } else { row = this.frm.doc.items.find(i => i.batch_no === batch_no); } @@ -228,16 +225,59 @@ erpnext.SerialNoBatchSelector = Class.extend({ 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); }); - } 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'); } + }, - refresh_field("items"); - this.callback && this.callback(this.item); + 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: function(batch) { @@ -287,7 +327,7 @@ erpnext.SerialNoBatchSelector = Class.extend({ return { filters: { 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' }; @@ -448,7 +488,7 @@ erpnext.SerialNoBatchSelector = Class.extend({ { fieldname: 'serial_no', 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() { me.serial_list = this.get_value() .replace(/\n/g, ' ').match(/\S+/g) || []; diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js index 8278745a804..af100692c6f 100644 --- a/erpnext/selling/sales_common.js +++ b/erpnext/selling/sales_common.js @@ -413,15 +413,20 @@ erpnext.selling.SellingController = erpnext.TransactionController.extend({ */ set_batch_number: function(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); } }, _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({ 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) { if(r.message) { frappe.model.set_value(doc.doctype, doc.name, 'batch_no', r.message); diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index 64d4c6c0829..772ac58af6f 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -523,12 +523,15 @@ def get_delivery_note_serial_no(item_code, qty, delivery_note): return serial_nos @frappe.whitelist() -def auto_fetch_serial_number(qty, item_code, warehouse, batch_no=None): - serial_numbers = frappe.get_list("Serial No", filters={ +def auto_fetch_serial_number(qty, item_code, warehouse, batch_nos=None): + import json + filters = { "item_code": item_code, "warehouse": warehouse, - "batch_no": batch_no, "delivery_document_no": "", "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]