diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js index 73c3868efaf..c35e4a5967f 100644 --- a/erpnext/public/js/utils/serial_no_batch_selector.js +++ b/erpnext/public/js/utils/serial_no_batch_selector.js @@ -1,623 +1,3 @@ - -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.replace(/\n/g, ' ').match(/\S+/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().replace(/\n/g, ' ').match(/\S+/g) || []; - } else { - this.set_new_description(new_number + ' is already selected.'); - } - - qty_field.set_input(me.serial_list.length); - this.$input.val(""); - this.in_local_change = 0; - } - }, - {fieldtype: 'Column Break'}, - { - fieldname: 'serial_no', - fieldtype: 'Small Text', - 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) || []; - 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 - - erpnext.SerialNoBatchBundleUpdate = class SerialNoBatchBundleUpdate { constructor(frm, item, callback) { this.frm = frm; diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index 382e6a9f3d9..35a3ca8211b 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py @@ -18,7 +18,7 @@ class SerialandBatchBundle(Document): def validate(self): self.validate_serial_and_batch_no() self.validate_duplicate_serial_and_batch_no() - self.validate_voucher_no() + # self.validate_voucher_no() self.validate_serial_nos() def before_save(self): @@ -101,6 +101,9 @@ class SerialandBatchBundle(Document): rate = frappe.db.get_value(self.child_table, self.voucher_detail_no, "valuation_rate") for d in self.ledgers: + if self.voucher_type in ["Stock Reconciliation", "Stock Entry"] and d.incoming_rate: + continue + if not rate or flt(rate, precision) == flt(d.incoming_rate, precision): continue @@ -134,7 +137,7 @@ class SerialandBatchBundle(Document): if values_to_set: self.db_set(values_to_set) - self.validate_voucher_no() + # self.validate_voucher_no() self.validate_quantity(row) self.set_incoming_rate(save=True, row=row) @@ -196,6 +199,9 @@ class SerialandBatchBundle(Document): row.warehouse = self.warehouse def set_total_qty(self, save=False): + if not self.ledgers: + return + self.total_qty = sum([row.qty for row in self.ledgers]) if save: self.db_set("total_qty", self.total_qty) @@ -638,7 +644,7 @@ def get_available_serial_nos(item_code, warehouse): "warehouse": ("is", "set"), } - fields = ["name", "warehouse", "batch_no"] + fields = ["name as serial_no", "warehouse", "batch_no"] if warehouse: filters["warehouse"] = warehouse @@ -654,6 +660,8 @@ def get_available_batch_nos(item_code, warehouse): for entry in sl_entries: batchwise_qty[entry.batch_no] += flt(entry.qty, precision) + return batchwise_qty + def get_stock_ledger_entries(item_code, warehouse): stock_ledger_entry = frappe.qb.DocType("Stock Ledger Entry") diff --git a/erpnext/stock/doctype/serial_and_batch_ledger/serial_and_batch_ledger.json b/erpnext/stock/doctype/serial_and_batch_ledger/serial_and_batch_ledger.json index 7e83c70b5d4..f2d4d55032e 100644 --- a/erpnext/stock/doctype/serial_and_batch_ledger/serial_and_batch_ledger.json +++ b/erpnext/stock/doctype/serial_and_batch_ledger/serial_and_batch_ledger.json @@ -68,7 +68,8 @@ "fieldtype": "Float", "label": "Incoming Rate", "no_copy": 1, - "read_only": 1 + "read_only": 1, + "read_only_depends_on": "eval:parent.type_of_transaction == \"Outward\"" }, { "fieldname": "outgoing_rate", @@ -106,7 +107,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-03-10 12:02:49.560343", + "modified": "2023-03-17 09:11:31.548862", "modified_by": "Administrator", "module": "Stock", "name": "Serial and Batch Ledger", diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index da53644439d..eda4d2d9a71 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -12,6 +12,10 @@ import erpnext from erpnext.accounts.utils import get_company_default from erpnext.controllers.stock_controller import StockController from erpnext.stock.doctype.batch.batch import get_batch_qty +from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import ( + get_available_batch_nos, + get_available_serial_nos, +) from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.stock.utils import get_stock_balance @@ -37,6 +41,8 @@ class StockReconciliation(StockController): if not self.cost_center: self.cost_center = frappe.get_cached_value("Company", self.company, "cost_center") self.validate_posting_time() + self.set_current_serial_and_batch_bundle() + self.set_new_serial_and_batch_bundle() self.remove_items_with_no_change() self.validate_data() self.validate_expense_account() @@ -49,6 +55,9 @@ class StockReconciliation(StockController): if self._action == "submit": self.validate_reserved_stock() + def on_update(self): + self.set_serial_and_batch_bundle() + def on_submit(self): self.update_stock_ledger() self.make_gl_entries() @@ -71,6 +80,87 @@ class StockReconciliation(StockController): self.repost_future_sle_and_gle() self.delete_auto_created_batches() + def set_current_serial_and_batch_bundle(self): + """Set Serial and Batch Bundle for each item""" + for item in self.items: + item_details = frappe.get_cached_value( + "Item", item.item_code, ["has_serial_no", "has_batch_no"], as_dict=1 + ) + + if ( + item_details.has_serial_no or item_details.has_batch_no + ) and not item.current_serial_and_batch_bundle: + serial_and_batch_bundle = frappe.get_doc( + { + "doctype": "Serial and Batch Bundle", + "item_code": item.item_code, + "warehouse": item.warehouse, + "posting_date": self.posting_date, + "posting_time": self.posting_time, + "voucher_type": self.doctype, + "voucher_no": self.name, + "type_of_transaction": "Outward", + } + ) + + if item_details.has_serial_no: + serial_nos_details = get_available_serial_nos(item.item_code, item.warehouse) + + for serial_no_row in serial_nos_details: + serial_and_batch_bundle.append( + "ledgers", + { + "serial_no": serial_no_row.serial_no, + "qty": -1, + "warehouse": serial_no_row.warehouse, + "batch_no": serial_no_row.batch_no, + }, + ) + + if item_details.has_batch_no: + batch_nos_details = get_available_batch_nos(item.item_code, item.warehouse) + + for batch_no, qty in batch_nos_details.items(): + serial_and_batch_bundle.append( + "ledgers", + { + "batch_no": batch_no, + "qty": qty * -1, + "warehouse": item.warehouse, + }, + ) + + item.current_serial_and_batch_bundle = serial_and_batch_bundle.save().name + + def set_new_serial_and_batch_bundle(self): + for item in self.items: + if item.current_serial_and_batch_bundle and not item.serial_and_batch_bundle: + current_doc = frappe.get_doc("Serial and Batch Bundle", item.current_serial_and_batch_bundle) + + item.qty = abs(current_doc.total_qty) + item.valuation_rate = abs(current_doc.avg_rate) + + bundle_doc = frappe.copy_doc(current_doc) + bundle_doc.warehouse = item.warehouse + bundle_doc.type_of_transaction = "Inward" + + for row in bundle_doc.ledgers: + if row.qty < 0: + row.qty = abs(row.qty) + + if row.stock_value_difference < 0: + row.stock_value_difference = abs(row.stock_value_difference) + + row.is_outward = 0 + + bundle_doc.set_total_qty() + bundle_doc.set_avg_rate() + bundle_doc.flags.ignore_permissions = True + bundle_doc.save() + item.serial_and_batch_bundle = bundle_doc.name + elif item.serial_and_batch_bundle: + pass + def remove_items_with_no_change(self): """Remove items if qty or rate is not changed""" self.difference_amount = 0.0 @@ -80,10 +170,11 @@ class StockReconciliation(StockController): item.item_code, item.warehouse, self.posting_date, self.posting_time, batch_no=item.batch_no ) - if ( - (item.qty is None or item.qty == item_dict.get("qty")) - and (item.valuation_rate is None or item.valuation_rate == item_dict.get("rate")) - and (not item.serial_no or (item.serial_no == item_dict.get("serial_nos"))) + if item.current_serial_and_batch_bundle: + return True + + if (item.qty is None or item.qty == item_dict.get("qty")) and ( + item.valuation_rate is None or item.valuation_rate == item_dict.get("rate") ): return False else: @@ -94,11 +185,6 @@ class StockReconciliation(StockController): if item.valuation_rate is None: item.valuation_rate = item_dict.get("rate") - if item_dict.get("serial_nos"): - item.current_serial_no = item_dict.get("serial_nos") - if self.purpose == "Stock Reconciliation" and not item.serial_no and item.qty: - item.serial_no = item.current_serial_no - item.current_qty = item_dict.get("qty") item.current_valuation_rate = item_dict.get("rate") self.difference_amount += flt(item.qty, item.precision("qty")) * flt( @@ -279,15 +365,14 @@ class StockReconciliation(StockController): has_serial_no = False has_batch_no = False for row in self.items: - item = frappe.get_doc("Item", row.item_code) - if item.has_batch_no: - has_batch_no = True + item = frappe.get_cached_value( + "Item", row.item_code, ["has_serial_no", "has_batch_no"], as_dict=1 + ) if item.has_serial_no or item.has_batch_no: - has_serial_no = True - self.get_sle_for_serialized_items(row, sl_entries, item) + self.get_sle_for_serialized_items(row, sl_entries) else: - if row.serial_no or row.batch_no: + if row.serial_and_batch_bundle: frappe.throw( _( "Row #{0}: Item {1} is not a Serialized/Batched Item. It cannot have a Serial No/Batch No against it." @@ -337,89 +422,32 @@ class StockReconciliation(StockController): if has_serial_no and sl_entries: self.update_valuation_rate_for_serial_no() - def get_sle_for_serialized_items(self, row, sl_entries, item): - from erpnext.stock.stock_ledger import get_previous_sle - - serial_nos = get_serial_nos(row.serial_no) - - # To issue existing serial nos - if row.current_qty and (row.current_serial_no or row.batch_no): + def get_sle_for_serialized_items(self, row, sl_entries): + if row.current_serial_and_batch_bundle: args = self.get_sle_for_items(row) args.update( { "actual_qty": -1 * row.current_qty, - "serial_no": row.current_serial_no, - "batch_no": row.batch_no, + "serial_and_batch_bundle": row.current_serial_and_batch_bundle, "valuation_rate": row.current_valuation_rate, } ) - if row.current_serial_no: - args.update( - { - "qty_after_transaction": 0, - } - ) - sl_entries.append(args) - qty_after_transaction = 0 - for serial_no in serial_nos: - args = self.get_sle_for_items(row, [serial_no]) - - previous_sle = get_previous_sle( - { - "item_code": row.item_code, - "posting_date": self.posting_date, - "posting_time": self.posting_time, - "serial_no": serial_no, - } - ) - - if previous_sle and row.warehouse != previous_sle.get("warehouse"): - # If serial no exists in different warehouse - - warehouse = previous_sle.get("warehouse", "") or row.warehouse - - if not qty_after_transaction: - qty_after_transaction = get_stock_balance( - row.item_code, warehouse, self.posting_date, self.posting_time - ) - - qty_after_transaction -= 1 - - new_args = args.copy() - new_args.update( - { - "actual_qty": -1, - "qty_after_transaction": qty_after_transaction, - "warehouse": warehouse, - "valuation_rate": previous_sle.get("valuation_rate"), - } - ) - - sl_entries.append(new_args) - - if row.qty: + if row.current_serial_and_batch_bundle: args = self.get_sle_for_items(row) - - if item.has_serial_no and item.has_batch_no: - args["qty_after_transaction"] = row.qty - args.update( { - "actual_qty": row.qty, - "incoming_rate": row.valuation_rate, - "valuation_rate": row.valuation_rate, + "actual_qty": frappe.get_cached_value( + "Serial and Batch Bundle", row.serial_and_batch_bundle, "total_qty" + ), + "serial_and_batch_bundle": row.current_serial_and_batch_bundle, } ) sl_entries.append(args) - if serial_nos == get_serial_nos(row.current_serial_no): - # update valuation rate - self.update_valuation_rate_for_serial_nos(row, serial_nos) - def update_valuation_rate_for_serial_no(self): for d in self.items: if not d.serial_no: @@ -456,8 +484,6 @@ class StockReconciliation(StockController): "company": self.company, "stock_uom": frappe.db.get_value("Item", row.item_code, "stock_uom"), "is_cancelled": 1 if self.docstatus == 2 else 0, - "serial_no": "\n".join(serial_nos) if serial_nos else "", - "batch_no": row.batch_no, "valuation_rate": flt(row.valuation_rate, row.precision("valuation_rate")), } )