feat: serial and batch bundle for POS
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
index dca93e8..f926512 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 @@
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 @@
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 @@
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 @@
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 <b>Invalid</b>: {}").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 @@
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 @@
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 @@
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 d8aed21..db64d06 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 @@
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 71fee9f..86cef3b 100644
--- a/erpnext/controllers/sales_and_purchase_return.py
+++ b/erpnext/controllers/sales_and_purchase_return.py
@@ -408,6 +408,7 @@
{
"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 @@
{
"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 f9b5bb2..1091c46 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 @@
<div class="item-image"></div>
</div>
<div class="discount-section"></div>
- <div class="form-container"></div>`
+ <div class="form-container"></div>
+ <div class="serial-batch-container"></div>`
)
this.$item_name = this.$component.find('.item-name');
@@ -53,6 +54,7 @@
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 @@
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;
+ const no_bundle_selected = !item_row.serial_and_batch_bundle;
- if ((serialized && no_serial_selected) || (batched && no_batch_selected) ||
- (serialized && batched && (no_batch_selected || no_serial_selected))) {
-
+ 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 @@
}
make_auto_serial_selection_btn(item) {
- if (item.has_serial_no) {
- if (!item.has_batch_no) {
- this.$form_container.append(
- `<div class="grid-filler no-select"></div>`
- );
- }
- 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(
`<div class="btn btn-sm btn-secondary auto-fetch-btn">${label}</div>`
);
@@ -382,40 +376,19 @@
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 337c6dd..788c79d 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 311b35f..c4f240a 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 @@
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 @@
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 @@
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 @@
@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 @@
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 @@
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 @@
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 @@
.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 @@
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_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 2c46082..483a1f1 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 @@
& (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 1266133..038cce7 100644
--- a/erpnext/stock/serial_batch_bundle.py
+++ b/erpnext/stock/serial_batch_bundle.py
@@ -642,6 +642,8 @@
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