Skip to content

Commit

Permalink
fix: serial and batch bundle return not working (backport #38754) (#3…
Browse files Browse the repository at this point in the history
…8806)

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

(cherry picked from commit 0743289)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
  • Loading branch information
mergify[bot] and rohitwaghchaure authored Dec 17, 2023
1 parent 18bd330 commit 8990c48
Show file tree
Hide file tree
Showing 12 changed files with 458 additions and 131 deletions.
191 changes: 127 additions & 64 deletions erpnext/controllers/sales_and_purchase_return.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
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


Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -397,71 +377,92 @@ def set_missing_values(source, target):
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)

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
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"

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
Expand Down Expand Up @@ -561,6 +562,17 @@ def update_item(source_doc, target_doc, source_parent):
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

Expand Down Expand Up @@ -716,16 +728,67 @@ 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])

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
4 changes: 4 additions & 0 deletions erpnext/controllers/selling_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,8 @@ def get_item_list(self):
"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),
Expand All @@ -330,6 +332,8 @@ def get_item_list(self):
"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,
Expand Down
6 changes: 6 additions & 0 deletions erpnext/controllers/stock_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,12 @@ def get_sl_entries(self, d, args):
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:
Expand Down
14 changes: 12 additions & 2 deletions erpnext/public/js/controllers/buying.js
Original file line number Diff line number Diff line change
Expand Up @@ -361,9 +361,14 @@ erpnext.buying = {
new erpnext.SerialBatchPackageSelector(
me.frm, item, (r) => {
if (r) {
let qty = Math.abs(r.total_qty);
if (doc.is_return) {
qty = qty * -1;
}

let update_values = {
"serial_and_batch_bundle": r.name,
"qty": Math.abs(r.total_qty)
"qty": qty
}

if (r.warehouse) {
Expand Down Expand Up @@ -396,9 +401,14 @@ erpnext.buying = {
new erpnext.SerialBatchPackageSelector(
me.frm, item, (r) => {
if (r) {
let qty = Math.abs(r.total_qty);
if (doc.is_return) {
qty = qty * -1;
}

let update_values = {
"serial_and_batch_bundle": r.name,
"rejected_qty": Math.abs(r.total_qty)
"rejected_qty": qty
}

if (r.warehouse) {
Expand Down
7 changes: 6 additions & 1 deletion erpnext/public/js/utils/sales_common.js
Original file line number Diff line number Diff line change
Expand Up @@ -317,9 +317,14 @@ erpnext.sales_common = {
new erpnext.SerialBatchPackageSelector(
me.frm, item, (r) => {
if (r) {
let qty = Math.abs(r.total_qty);
if (doc.is_return) {
qty = qty * -1;
}

frappe.model.set_value(item.doctype, item.name, {
"serial_and_batch_bundle": r.name,
"qty": Math.abs(r.total_qty)
"qty": qty
});
}
}
Expand Down
Loading

0 comments on commit 8990c48

Please sign in to comment.