From 0eaf6de5dedc5269bfe46a79d313f7825389dcf9 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 23 Mar 2023 15:13:45 +0530 Subject: [PATCH] feat: serial and batch bundle for POS --- .../doctype/pos_invoice/pos_invoice.py | 143 +++--------------- .../pos_invoice_merge_log.py | 2 + .../controllers/sales_and_purchase_return.py | 2 + .../page/point_of_sale/pos_item_details.js | 65 +++----- .../serial_and_batch_bundle.json | 9 +- .../serial_and_batch_bundle.py | 141 +++++++++++++++-- .../batch_wise_balance_history.py | 2 +- erpnext/stock/serial_batch_bundle.py | 2 + 8 files changed, 178 insertions(+), 188 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index dca93e8937..f9265120a0 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -3,7 +3,7 @@ import frappe -from frappe import _ +from frappe import _, bold from frappe.query_builder.functions import IfNull, Sum from frappe.utils import cint, flt, get_link_to_form, getdate, nowdate @@ -16,12 +16,7 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import ( update_multi_mode_option, ) from erpnext.accounts.party import get_due_date, get_party_account -from erpnext.stock.doctype.batch.batch import get_batch_qty, get_pos_reserved_batch_qty -from erpnext.stock.doctype.serial_no.serial_no import ( - get_delivered_serial_nos, - get_pos_reserved_serial_nos, - get_serial_nos, -) +from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos class POSInvoice(SalesInvoice): @@ -71,6 +66,7 @@ class POSInvoice(SalesInvoice): self.apply_loyalty_points() self.check_phone_payments() self.set_status(update=True) + self.submit_serial_batch_bundle() if self.coupon_code: from erpnext.accounts.doctype.pricing_rule.utils import update_coupon_code_count @@ -112,6 +108,14 @@ class POSInvoice(SalesInvoice): update_coupon_code_count(self.coupon_code, "cancelled") + def submit_serial_batch_bundle(self): + for item in self.items: + if item.serial_and_batch_bundle: + doc = frappe.get_doc("Serial and Batch Bundle", item.serial_and_batch_bundle) + + if doc.docstatus == 0: + doc.submit() + def check_phone_payments(self): for pay in self.payments: if pay.type == "Phone" and pay.amount >= 0: @@ -129,88 +133,6 @@ class POSInvoice(SalesInvoice): if paid_amt and pay.amount != paid_amt: return frappe.throw(_("Payment related to {0} is not completed").format(pay.mode_of_payment)) - def validate_pos_reserved_serial_nos(self, item): - serial_nos = get_serial_nos(item.serial_no) - filters = {"item_code": item.item_code, "warehouse": item.warehouse} - if item.batch_no: - filters["batch_no"] = item.batch_no - - reserved_serial_nos = get_pos_reserved_serial_nos(filters) - invalid_serial_nos = [s for s in serial_nos if s in reserved_serial_nos] - - bold_invalid_serial_nos = frappe.bold(", ".join(invalid_serial_nos)) - if len(invalid_serial_nos) == 1: - frappe.throw( - _( - "Row #{}: Serial No. {} has already been transacted into another POS Invoice. Please select valid serial no." - ).format(item.idx, bold_invalid_serial_nos), - title=_("Item Unavailable"), - ) - elif invalid_serial_nos: - frappe.throw( - _( - "Row #{}: Serial Nos. {} have already been transacted into another POS Invoice. Please select valid serial no." - ).format(item.idx, bold_invalid_serial_nos), - title=_("Item Unavailable"), - ) - - def validate_pos_reserved_batch_qty(self, item): - filters = {"item_code": item.item_code, "warehouse": item.warehouse, "batch_no": item.batch_no} - - available_batch_qty = get_batch_qty(item.batch_no, item.warehouse, item.item_code) - reserved_batch_qty = get_pos_reserved_batch_qty(filters) - - bold_item_name = frappe.bold(item.item_name) - bold_extra_batch_qty_needed = frappe.bold( - abs(available_batch_qty - reserved_batch_qty - item.stock_qty) - ) - bold_invalid_batch_no = frappe.bold(item.batch_no) - - if (available_batch_qty - reserved_batch_qty) == 0: - frappe.throw( - _( - "Row #{}: Batch No. {} of item {} has no stock available. Please select valid batch no." - ).format(item.idx, bold_invalid_batch_no, bold_item_name), - title=_("Item Unavailable"), - ) - elif (available_batch_qty - reserved_batch_qty - item.stock_qty) < 0: - frappe.throw( - _( - "Row #{}: Batch No. {} of item {} has less than required stock available, {} more required" - ).format( - item.idx, bold_invalid_batch_no, bold_item_name, bold_extra_batch_qty_needed - ), - title=_("Item Unavailable"), - ) - - def validate_delivered_serial_nos(self, item): - delivered_serial_nos = get_delivered_serial_nos(item.serial_no) - - if delivered_serial_nos: - bold_delivered_serial_nos = frappe.bold(", ".join(delivered_serial_nos)) - frappe.throw( - _( - "Row #{}: Serial No. {} has already been transacted into another Sales Invoice. Please select valid serial no." - ).format(item.idx, bold_delivered_serial_nos), - title=_("Item Unavailable"), - ) - - def validate_invalid_serial_nos(self, item): - serial_nos = get_serial_nos(item.serial_no) - error_msg = [] - invalid_serials, msg = "", "" - for serial_no in serial_nos: - if not frappe.db.exists("Serial No", serial_no): - invalid_serials = invalid_serials + (", " if invalid_serials else "") + serial_no - msg = _("Row #{}: Following Serial numbers for item {} are Invalid: {}").format( - item.idx, frappe.bold(item.get("item_code")), frappe.bold(invalid_serials) - ) - if invalid_serials: - error_msg.append(msg) - - if error_msg: - frappe.throw(error_msg, title=_("Invalid Item"), as_list=True) - def validate_stock_availablility(self): if self.is_return: return @@ -223,13 +145,7 @@ class POSInvoice(SalesInvoice): from erpnext.stock.stock_ledger import is_negative_stock_allowed for d in self.get("items"): - if d.serial_no: - self.validate_pos_reserved_serial_nos(d) - self.validate_delivered_serial_nos(d) - self.validate_invalid_serial_nos(d) - elif d.batch_no: - self.validate_pos_reserved_batch_qty(d) - else: + if not d.serial_and_batch_bundle: if is_negative_stock_allowed(item_code=d.item_code): return @@ -258,36 +174,15 @@ class POSInvoice(SalesInvoice): def validate_serialised_or_batched_item(self): error_msg = [] for d in self.get("items"): - serialized = d.get("has_serial_no") - batched = d.get("has_batch_no") - no_serial_selected = not d.get("serial_no") - no_batch_selected = not d.get("batch_no") + error_msg = "" + if d.get("has_serial_no") and not d.serial_and_batch_bundle: + error_msg = f"Row #{d.idx}: Please select Serial No. for item {bold(d.item_code)}" - msg = "" - item_code = frappe.bold(d.item_code) - serial_nos = get_serial_nos(d.serial_no) - if serialized and batched and (no_batch_selected or no_serial_selected): - msg = _( - "Row #{}: Please select a serial no and batch against item: {} or remove it to complete transaction." - ).format(d.idx, item_code) - elif serialized and no_serial_selected: - msg = _( - "Row #{}: No serial number selected against item: {}. Please select one or remove it to complete transaction." - ).format(d.idx, item_code) - elif batched and no_batch_selected: - msg = _( - "Row #{}: No batch selected against item: {}. Please select a batch or remove it to complete transaction." - ).format(d.idx, item_code) - elif serialized and not no_serial_selected and len(serial_nos) != d.qty: - msg = _("Row #{}: You must select {} serial numbers for item {}.").format( - d.idx, frappe.bold(cint(d.qty)), item_code - ) - - if msg: - error_msg.append(msg) + elif d.get("has_batch_no") and not d.serial_and_batch_bundle: + error_msg = f"Row #{d.idx}: Please select Batch No. for item {bold(d.item_code)}" if error_msg: - frappe.throw(error_msg, title=_("Invalid Item"), as_list=True) + frappe.throw(error_msg, title=_("Serial / Batch Bundle Missing"), as_list=True) def validate_return_items_qty(self): if not self.get("is_return"): @@ -652,7 +547,7 @@ def get_bundle_availability(bundle_item_code, warehouse): item_pos_reserved_qty = get_pos_reserved_qty(item.item_code, warehouse) available_qty = item_bin_qty - item_pos_reserved_qty - max_available_bundles = available_qty / item.stock_qty + max_available_bundles = available_qty / item.qty if bundle_bin_qty > max_available_bundles and frappe.get_value( "Item", item.item_code, "is_stock_item" ): diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py index d8aed219e2..db64d06962 100644 --- a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py +++ b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py @@ -184,6 +184,8 @@ class POSInvoiceMergeLog(Document): item.base_amount = item.base_net_amount item.price_list_rate = 0 si_item = map_child_doc(item, invoice, {"doctype": "Sales Invoice Item"}) + if item.serial_and_batch_bundle: + si_item.serial_and_batch_bundle = item.serial_and_batch_bundle items.append(si_item) for tax in doc.get("taxes"): diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 71fee9f049..86cef3b764 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -408,6 +408,7 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None): { "type_of_transaction": type_of_transaction, "serial_and_batch_bundle": source_doc.serial_and_batch_bundle, + "returned_against": source_doc.name, } ) @@ -429,6 +430,7 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None): { "type_of_transaction": type_of_transaction, "serial_and_batch_bundle": source_doc.rejected_serial_and_batch_bundle, + "returned_against": source_doc.name, } ) diff --git a/erpnext/selling/page/point_of_sale/pos_item_details.js b/erpnext/selling/page/point_of_sale/pos_item_details.js index f9b5bb2e45..1091c46ef3 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_details.js +++ b/erpnext/selling/page/point_of_sale/pos_item_details.js @@ -44,7 +44,8 @@ erpnext.PointOfSale.ItemDetails = class {
-
` +
+
` ) this.$item_name = this.$component.find('.item-name'); @@ -53,6 +54,7 @@ erpnext.PointOfSale.ItemDetails = class { this.$item_image = this.$component.find('.item-image'); this.$form_container = this.$component.find('.form-container'); this.$dicount_section = this.$component.find('.discount-section'); + this.$serial_batch_container = this.$component.find('.serial-batch-container'); } compare_with_current_item(item) { @@ -101,12 +103,9 @@ erpnext.PointOfSale.ItemDetails = class { const serialized = item_row.has_serial_no; const batched = item_row.has_batch_no; - const no_serial_selected = !item_row.serial_no; - const no_batch_selected = !item_row.batch_no; - - if ((serialized && no_serial_selected) || (batched && no_batch_selected) || - (serialized && batched && (no_batch_selected || no_serial_selected))) { + const no_bundle_selected = !item_row.serial_and_batch_bundle; + if ((serialized && no_bundle_selected) || (batched && no_bundle_selected)) { frappe.show_alert({ message: __("Item is removed since no serial / batch no selected."), indicator: 'orange' @@ -200,13 +199,8 @@ erpnext.PointOfSale.ItemDetails = class { } make_auto_serial_selection_btn(item) { - if (item.has_serial_no) { - if (!item.has_batch_no) { - this.$form_container.append( - `
` - ); - } - const label = __('Auto Fetch Serial Numbers'); + if (item.has_serial_no || item.has_batch_no) { + const label = item.has_serial_no ? __('Select Serial No') : __('Select Batch No'); this.$form_container.append( `
${label}
` ); @@ -382,40 +376,19 @@ erpnext.PointOfSale.ItemDetails = class { bind_auto_serial_fetch_event() { this.$form_container.on('click', '.auto-fetch-btn', () => { - this.batch_no_control && this.batch_no_control.set_value(''); - let qty = this.qty_control.get_value(); - let conversion_factor = this.conversion_factor_control.get_value(); - let expiry_date = this.item_row.has_batch_no ? this.events.get_frm().doc.posting_date : ""; + frappe.require("assets/erpnext/js/utils/serial_no_batch_selector.js", () => { + let frm = this.events.get_frm(); + let item_row = this.item_row; + item_row.outward = 1; + item_row.type_of_transaction = "Outward"; - let numbers = frappe.call({ - method: "erpnext.stock.doctype.serial_no.serial_no.auto_fetch_serial_number", - args: { - qty: qty * conversion_factor, - item_code: this.current_item.item_code, - warehouse: this.warehouse_control.get_value() || '', - batch_nos: this.current_item.batch_no || '', - posting_date: expiry_date, - for_doctype: 'POS Invoice' - } - }); - - numbers.then((data) => { - let auto_fetched_serial_numbers = data.message; - let records_length = auto_fetched_serial_numbers.length; - if (!records_length) { - const warehouse = this.warehouse_control.get_value().bold(); - const item_code = this.current_item.item_code.bold(); - frappe.msgprint( - __('Serial numbers unavailable for Item {0} under warehouse {1}. Please try changing warehouse.', [item_code, warehouse]) - ); - } else if (records_length < qty) { - frappe.msgprint( - __('Fetched only {0} available serial numbers.', [records_length]) - ); - this.qty_control.set_value(records_length); - } - numbers = auto_fetched_serial_numbers.join(`\n`); - this.serial_no_control.set_value(numbers); + new erpnext.SerialBatchPackageSelector(frm, item_row, (r) => { + if (r) { + frm.refresh_fields(); + frappe.model.set_value(item_row.doctype, item_row.name, + "serial_and_batch_bundle", r.name); + } + }); }); }) } diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json index 337c6dda2e..788c79dae9 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.json @@ -31,6 +31,7 @@ "column_break_aouy", "posting_date", "posting_time", + "returned_against", "section_break_wzou", "is_cancelled", "is_rejected", @@ -232,12 +233,18 @@ "fieldtype": "Table", "options": "Serial and Batch Entry", "reqd": 1 + }, + { + "fieldname": "returned_against", + "fieldtype": "Data", + "label": "Returned Against", + "read_only": 1 } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-03-22 18:56:37.035516", + "modified": "2023-03-23 13:39:17.843812", "modified_by": "Administrator", "module": "Stock", "name": "Serial and Batch Bundle", 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 311b35fa5c..c4f240ab58 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 @@ -2,6 +2,7 @@ # For license information, please see license.txt import collections +from collections import defaultdict from typing import Dict, List import frappe @@ -31,10 +32,10 @@ class SerialandBatchBundle(Document): self.check_future_entries_exists() self.validate_serial_nos_inventory() self.set_is_outward() + self.validate_qty_and_stock_value_difference() self.calculate_qty_and_amount() self.set_warehouse() self.set_incoming_rate() - self.validate_qty_and_stock_value_difference() def validate_serial_nos_inventory(self): if not (self.has_serial_no and self.type_of_transaction == "Outward"): @@ -100,7 +101,7 @@ class SerialandBatchBundle(Document): d.incoming_rate = abs(sn_obj.serial_no_incoming_rate.get(d.serial_no, 0.0)) else: d.incoming_rate = abs(sn_obj.batch_avg_rate.get(d.batch_no)) - available_qty = flt(sn_obj.batch_available_qty.get(d.batch_no)) + flt(d.qty) + available_qty = flt(sn_obj.available_qty.get(d.batch_no)) + flt(d.qty) self.validate_negative_batch(d.batch_no, available_qty) @@ -417,6 +418,7 @@ class SerialandBatchBundle(Document): frappe.throw(_(msg)) def on_trash(self): + self.validate_voucher_no_docstatus() self.delink_refernce_from_voucher() self.delink_reference_from_batch() self.clear_table() @@ -439,25 +441,48 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals @frappe.whitelist() -def get_serial_batch_ledgers(item_code, voucher_no, name=None): +def get_serial_batch_ledgers(item_code, docstatus=None, voucher_no=None, name=None): + filters = get_filters_for_bundle(item_code, docstatus=docstatus, voucher_no=voucher_no, name=name) + return frappe.get_all( "Serial and Batch Bundle", fields=[ - "`tabSerial and Batch Entry`.`name`", + "`tabSerial and Batch Bundle`.`name`", "`tabSerial and Batch Entry`.`qty`", "`tabSerial and Batch Entry`.`warehouse`", "`tabSerial and Batch Entry`.`batch_no`", "`tabSerial and Batch Entry`.`serial_no`", ], - filters=[ - ["Serial and Batch Bundle", "item_code", "=", item_code], - ["Serial and Batch Entry", "parent", "=", name], - ["Serial and Batch Bundle", "voucher_no", "=", voucher_no], - ["Serial and Batch Bundle", "docstatus", "!=", 2], - ], + filters=filters, ) +def get_filters_for_bundle(item_code, docstatus=None, voucher_no=None, name=None): + filters = [ + ["Serial and Batch Bundle", "item_code", "=", item_code], + ["Serial and Batch Bundle", "is_cancelled", "=", 0], + ] + + if not docstatus: + docstatus = [0, 1] + + if isinstance(docstatus, list): + filters.append(["Serial and Batch Bundle", "docstatus", "in", docstatus]) + else: + filters.append(["Serial and Batch Bundle", "docstatus", "=", docstatus]) + + if voucher_no: + filters.append(["Serial and Batch Bundle", "voucher_no", "=", voucher_no]) + + if name: + if isinstance(name, list): + filters.append(["Serial and Batch Entry", "parent", "in", name]) + else: + filters.append(["Serial and Batch Entry", "parent", "=", name]) + + return filters + + @frappe.whitelist() def add_serial_batch_ledgers(entries, child_row, doc) -> object: if isinstance(child_row, str): @@ -603,15 +628,52 @@ def get_auto_serial_nos(kwargs): elif kwargs.based_on == "Expiry": order_by = "amc_expiry_date asc" + ignore_serial_nos = get_reserved_serial_nos_for_pos(kwargs) + return frappe.get_all( "Serial No", fields=fields, - filters={"item_code": kwargs.item_code, "warehouse": kwargs.warehouse}, + filters={ + "item_code": kwargs.item_code, + "warehouse": kwargs.warehouse, + "name": ("not in", ignore_serial_nos), + }, limit=cint(kwargs.qty), order_by=order_by, ) +def get_reserved_serial_nos_for_pos(kwargs): + from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos + + ignore_serial_nos = [] + pos_invoices = frappe.get_all( + "POS Invoice", + fields=["`tabPOS Invoice Item`.serial_no", "`tabPOS Invoice Item`.serial_and_batch_bundle"], + filters=[ + ["POS Invoice", "consolidated_invoice", "is", "not set"], + ["POS Invoice", "docstatus", "=", 1], + ["POS Invoice Item", "item_code", "=", kwargs.item_code], + ], + ) + + ids = [ + pos_invoice.serial_and_batch_bundle + for pos_invoice in pos_invoices + if pos_invoice.serial_and_batch_bundle + ] + + for d in get_serial_batch_ledgers(ids, docstatus=1, name=ids): + ignore_serial_nos.append(d.serial_no) + + # Will be deprecated in v16 + for pos_invoice in pos_invoices: + if pos_invoice.serial_no: + ignore_serial_nos.extend(get_serial_nos(pos_invoice.serial_no)) + + return ignore_serial_nos + + def get_auto_batch_nos(kwargs): available_batches = get_available_batches(kwargs) @@ -619,6 +681,10 @@ def get_auto_batch_nos(kwargs): batches = [] + reserved_batches = get_reserved_batches_for_pos(kwargs) + if reserved_batches: + remove_batches_reserved_for_pos(available_batches, reserved_batches) + for batch in available_batches: if qty > 0: batch_qty = flt(batch.qty) @@ -642,6 +708,51 @@ def get_auto_batch_nos(kwargs): return batches +def get_reserved_batches_for_pos(kwargs): + reserved_batches = defaultdict(float) + + pos_invoices = frappe.get_all( + "POS Invoice", + fields=[ + "`tabPOS Invoice Item`.batch_no", + "`tabPOS Invoice Item`.qty", + "`tabPOS Invoice Item`.serial_and_batch_bundle", + ], + filters=[ + ["POS Invoice", "consolidated_invoice", "is", "not set"], + ["POS Invoice", "docstatus", "=", 1], + ["POS Invoice Item", "item_code", "=", kwargs.item_code], + ], + ) + + ids = [ + pos_invoice.serial_and_batch_bundle + for pos_invoice in pos_invoices + if pos_invoice.serial_and_batch_bundle + ] + + for d in get_serial_batch_ledgers(ids, docstatus=1, name=ids): + if not d.batch_no: + continue + + reserved_batches[d.batch_no] += flt(d.qty) + + # Will be deprecated in v16 + for pos_invoice in pos_invoices: + if not pos_invoice.batch_no: + continue + + reserved_batches[pos_invoice.batch_no] += flt(pos_invoice.qty) + + return reserved_batches + + +def remove_batches_reserved_for_pos(available_batches, reserved_batches): + for batch in available_batches: + if batch.batch_no in reserved_batches: + available_batches[batch.batch_no] -= reserved_batches[batch.batch_no] + + def get_available_batches(kwargs): stock_ledger_entry = frappe.qb.DocType("Stock Ledger Entry") batch_ledger = frappe.qb.DocType("Serial and Batch Entry") @@ -655,9 +766,7 @@ def get_available_batches(kwargs): .on(batch_ledger.batch_no == batch_table.name) .select( batch_ledger.batch_no, - Sum( - Case().when(stock_ledger_entry.actual_qty > 0, batch_ledger.qty).else_(batch_ledger.qty * -1) - ).as_("qty"), + Sum(batch_ledger.qty).as_("qty"), ) .where( (stock_ledger_entry.item_code == kwargs.item_code) @@ -699,7 +808,7 @@ def get_voucher_wise_serial_batch_from_bundle(**kwargs) -> Dict[str, Dict]: if key not in group_by_voucher: group_by_voucher.setdefault( key, - frappe._dict({"serial_nos": [], "batch_nos": collections.defaultdict(float), "item_row": row}), + frappe._dict({"serial_nos": [], "batch_nos": defaultdict(float), "item_row": row}), ) child_row = group_by_voucher[key] @@ -771,7 +880,7 @@ def get_available_serial_nos(item_code, warehouse): def get_available_batch_nos(item_code, warehouse): sl_entries = get_stock_ledger_entries(item_code, warehouse) - batchwise_qty = collections.defaultdict(float) + batchwise_qty = defaultdict(float) precision = frappe.get_precision("Stock Ledger Entry", "qty") for entry in sl_entries: diff --git a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py index 2c460821d3..483a1f127d 100644 --- a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py +++ b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py @@ -131,7 +131,7 @@ def get_stock_ledger_entries_for_batch_bundle(filters): & (sle.has_batch_no == 1) & (sle.posting_date <= filters["to_date"]) ) - .groupby(sle.voucher_no, sle.batch_no, sle.item_code, sle.warehouse) + .groupby(batch_package.batch_no) .orderby(sle.item_code, sle.warehouse) ) diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index 1266133e68..038cce7ea2 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -642,6 +642,8 @@ class SerialBatchCreation: package = frappe.get_doc("Serial and Batch Bundle", id) new_package = frappe.copy_doc(package) new_package.type_of_transaction = self.type_of_transaction + new_package.returned_against = self.returned_against + print(new_package.voucher_type, new_package.voucher_no) new_package.save() self.serial_and_batch_bundle = new_package.name