fix: serial and batch bundle return not working (#38754)

* fix: serial and batch bundle return not working

* test: added test case for delivery note return against denormalized serial no
This commit is contained in:
rohitwaghchaure
2023-12-17 12:42:07 +05:30
committed by GitHub
parent 863116f1cd
commit 0743289925
12 changed files with 460 additions and 133 deletions

View File

@@ -8,6 +8,8 @@ from frappe.model.meta import get_field_precision
from frappe.utils import flt, format_datetime, get_datetime
import erpnext
from erpnext.stock.serial_batch_bundle import get_batches_from_bundle
from erpnext.stock.serial_batch_bundle import get_serial_nos as get_serial_nos_from_bundle
from erpnext.stock.utils import get_incoming_rate
@@ -69,8 +71,6 @@ def validate_return_against(doc):
def validate_returned_items(doc):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
valid_items = frappe._dict()
select_fields = "item_code, qty, stock_qty, rate, parenttype, conversion_factor"
@@ -123,26 +123,6 @@ def validate_returned_items(doc):
)
)
elif ref.batch_no and d.batch_no not in ref.batch_no:
frappe.throw(
_("Row # {0}: Batch No must be same as {1} {2}").format(
d.idx, doc.doctype, doc.return_against
)
)
elif ref.serial_no:
if d.qty and not d.serial_no:
frappe.throw(_("Row # {0}: Serial No is mandatory").format(d.idx))
else:
serial_nos = get_serial_nos(d.serial_no)
for s in serial_nos:
if s not in ref.serial_no:
frappe.throw(
_("Row # {0}: Serial No {1} does not match with {2} {3}").format(
d.idx, s, doc.doctype, doc.return_against
)
)
if (
warehouse_mandatory
and not d.get("warehouse")
@@ -397,71 +377,92 @@ def make_return_doc(
else:
doc.run_method("calculate_taxes_and_totals")
def update_item(source_doc, target_doc, source_parent):
def update_serial_batch_no(source_doc, target_doc, source_parent, item_details, qty_field):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
target_doc.qty = -1 * source_doc.qty
item_details = frappe.get_cached_value(
"Item", source_doc.item_code, ["has_batch_no", "has_serial_no"], as_dict=1
)
returned_serial_nos = []
if source_doc.get("serial_and_batch_bundle"):
if item_details.has_serial_no:
returned_serial_nos = get_returned_serial_nos(source_doc, source_parent)
returned_batches = frappe._dict()
serial_and_batch_field = (
"serial_and_batch_bundle" if qty_field == "stock_qty" else "rejected_serial_and_batch_bundle"
)
old_serial_no_field = "serial_no" if qty_field == "stock_qty" else "rejected_serial_no"
old_batch_no_field = "batch_no"
type_of_transaction = "Inward"
if (
frappe.db.get_value(
"Serial and Batch Bundle", source_doc.serial_and_batch_bundle, "type_of_transaction"
)
== "Inward"
):
type_of_transaction = "Outward"
cls_obj = SerialBatchCreation(
{
"type_of_transaction": type_of_transaction,
"serial_and_batch_bundle": source_doc.serial_and_batch_bundle,
"returned_against": source_doc.name,
"item_code": source_doc.item_code,
"returned_serial_nos": returned_serial_nos,
}
)
cls_obj.duplicate_package()
if cls_obj.serial_and_batch_bundle:
target_doc.serial_and_batch_bundle = cls_obj.serial_and_batch_bundle
if source_doc.get("rejected_serial_and_batch_bundle"):
if (
source_doc.get(serial_and_batch_field)
or source_doc.get(old_serial_no_field)
or source_doc.get(old_batch_no_field)
):
if item_details.has_serial_no:
returned_serial_nos = get_returned_serial_nos(
source_doc, source_parent, serial_no_field="rejected_serial_and_batch_bundle"
source_doc, source_parent, serial_no_field=serial_and_batch_field
)
else:
returned_batches = get_returned_batches(
source_doc, source_parent, batch_no_field=serial_and_batch_field
)
type_of_transaction = "Inward"
if (
if source_doc.get(serial_and_batch_field) and (
frappe.db.get_value(
"Serial and Batch Bundle", source_doc.rejected_serial_and_batch_bundle, "type_of_transaction"
"Serial and Batch Bundle", source_doc.get(serial_and_batch_field), "type_of_transaction"
)
== "Inward"
):
type_of_transaction = "Outward"
elif source_parent.doctype in [
"Purchase Invoice",
"Purchase Receipt",
"Subcontracting Receipt",
]:
type_of_transaction = "Outward"
cls_obj = SerialBatchCreation(
{
"type_of_transaction": type_of_transaction,
"serial_and_batch_bundle": source_doc.rejected_serial_and_batch_bundle,
"serial_and_batch_bundle": source_doc.get(serial_and_batch_field),
"returned_against": source_doc.name,
"item_code": source_doc.item_code,
"returned_serial_nos": returned_serial_nos,
"voucher_type": source_parent.doctype,
"do_not_submit": True,
"warehouse": source_doc.warehouse,
"has_serial_no": item_details.has_serial_no,
"has_batch_no": item_details.has_batch_no,
}
)
cls_obj.duplicate_package()
if cls_obj.serial_and_batch_bundle:
target_doc.serial_and_batch_bundle = cls_obj.serial_and_batch_bundle
serial_nos = []
batches = frappe._dict()
if source_doc.get(old_batch_no_field):
batches = frappe._dict({source_doc.batch_no: source_doc.get(qty_field)})
elif source_doc.get(old_serial_no_field):
serial_nos = get_serial_nos(source_doc.get(old_serial_no_field))
elif source_doc.get(serial_and_batch_field):
if item_details.has_serial_no:
serial_nos = get_serial_nos_from_bundle(source_doc.get(serial_and_batch_field))
else:
batches = get_batches_from_bundle(source_doc.get(serial_and_batch_field))
if serial_nos:
cls_obj.serial_nos = sorted(list(set(serial_nos) - set(returned_serial_nos)))
elif batches:
for batch in batches:
if batch in returned_batches:
batches[batch] -= flt(returned_batches.get(batch))
cls_obj.batches = batches
if source_doc.get(serial_and_batch_field):
cls_obj.duplicate_package()
if cls_obj.serial_and_batch_bundle:
target_doc.set(serial_and_batch_field, cls_obj.serial_and_batch_bundle)
else:
target_doc.set(serial_and_batch_field, cls_obj.make_serial_and_batch_bundle().name)
def update_item(source_doc, target_doc, source_parent):
target_doc.qty = -1 * source_doc.qty
if doctype in ["Purchase Receipt", "Subcontracting Receipt"]:
returned_qty_map = get_returned_qty_map_for_row(
source_parent.name, source_parent.supplier, source_doc.name, doctype
@@ -561,6 +562,17 @@ def make_return_doc(
if default_warehouse_for_sales_return:
target_doc.warehouse = default_warehouse_for_sales_return
item_details = frappe.get_cached_value(
"Item", source_doc.item_code, ["has_batch_no", "has_serial_no"], as_dict=1
)
if not item_details.has_batch_no and not item_details.has_serial_no:
return
for qty_field in ["stock_qty", "rejected_qty"]:
if target_doc.get(qty_field):
update_serial_batch_no(source_doc, target_doc, source_parent, item_details, qty_field)
def update_terms(source_doc, target_doc, source_parent):
target_doc.payment_amount = -source_doc.payment_amount
@@ -716,6 +728,9 @@ def get_returned_serial_nos(
[parent_doc.doctype, "docstatus", "=", 1],
]
if serial_no_field == "rejected_serial_and_batch_bundle":
filters.append([child_doc.doctype, "rejected_qty", ">", 0])
# Required for POS Invoice
if ignore_voucher_detail_no:
filters.append([child_doc.doctype, "name", "!=", ignore_voucher_detail_no])
@@ -723,9 +738,57 @@ def get_returned_serial_nos(
ids = []
for row in frappe.get_all(parent_doc.doctype, fields=fields, filters=filters):
ids.append(row.get("serial_and_batch_bundle"))
if row.get(old_field):
if row.get(old_field) and not row.get(serial_no_field):
serial_nos.extend(get_serial_nos_from_serial_no(row.get(old_field)))
serial_nos.extend(get_serial_nos(ids))
if ids:
serial_nos.extend(get_serial_nos(ids))
return serial_nos
def get_returned_batches(
child_doc, parent_doc, batch_no_field=None, ignore_voucher_detail_no=None
):
from erpnext.stock.serial_batch_bundle import get_batches_from_bundle
batches = frappe._dict()
old_field = "batch_no"
if not batch_no_field:
batch_no_field = "serial_and_batch_bundle"
return_ref_field = frappe.scrub(child_doc.doctype)
if child_doc.doctype == "Delivery Note Item":
return_ref_field = "dn_detail"
fields = [
f"`{'tab' + child_doc.doctype}`.`{batch_no_field}`",
f"`{'tab' + child_doc.doctype}`.`batch_no`",
f"`{'tab' + child_doc.doctype}`.`stock_qty`",
]
filters = [
[parent_doc.doctype, "return_against", "=", parent_doc.name],
[parent_doc.doctype, "is_return", "=", 1],
[child_doc.doctype, return_ref_field, "=", child_doc.name],
[parent_doc.doctype, "docstatus", "=", 1],
]
if batch_no_field == "rejected_serial_and_batch_bundle":
filters.append([child_doc.doctype, "rejected_qty", ">", 0])
# Required for POS Invoice
if ignore_voucher_detail_no:
filters.append([child_doc.doctype, "name", "!=", ignore_voucher_detail_no])
ids = []
for row in frappe.get_all(parent_doc.doctype, fields=fields, filters=filters):
ids.append(row.get("serial_and_batch_bundle"))
if row.get(old_field) and not row.get(batch_no_field):
batches.setdefault(row.get(old_field), row.get("stock_qty"))
if ids:
batches.update(get_batches_from_bundle(ids))
return batches

View File

@@ -308,6 +308,8 @@ class SellingController(StockController):
"warehouse": p.warehouse or d.warehouse,
"item_code": p.item_code,
"qty": flt(p.qty),
"serial_no": p.serial_no if self.docstatus == 2 else None,
"batch_no": p.batch_no if self.docstatus == 2 else None,
"uom": p.uom,
"serial_and_batch_bundle": p.serial_and_batch_bundle
or get_serial_and_batch_bundle(p, self),
@@ -330,6 +332,8 @@ class SellingController(StockController):
"warehouse": d.warehouse,
"item_code": d.item_code,
"qty": d.stock_qty,
"serial_no": d.serial_no if self.docstatus == 2 else None,
"batch_no": d.batch_no if self.docstatus == 2 else None,
"uom": d.uom,
"stock_uom": d.stock_uom,
"conversion_factor": d.conversion_factor,

View File

@@ -463,6 +463,12 @@ class StockController(AccountsController):
sl_dict.update(args)
self.update_inventory_dimensions(d, sl_dict)
if self.docstatus == 2:
# To handle denormalized serial no records, will br deprecated in v16
for field in ["serial_no", "batch_no"]:
if d.get(field):
sl_dict[field] = d.get(field)
return sl_dict
def update_inventory_dimensions(self, row, sl_dict) -> None: