fix: valuation rate for sales / purchase return for serial / batch nos (#43925)
This commit is contained in:
@@ -600,6 +600,10 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai
|
|||||||
if default_warehouse_for_sales_return:
|
if default_warehouse_for_sales_return:
|
||||||
target_doc.warehouse = default_warehouse_for_sales_return
|
target_doc.warehouse = default_warehouse_for_sales_return
|
||||||
|
|
||||||
|
if not source_doc.use_serial_batch_fields and source_doc.serial_and_batch_bundle:
|
||||||
|
target_doc.serial_no = None
|
||||||
|
target_doc.batch_no = None
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(source_doc.serial_no or source_doc.batch_no)
|
(source_doc.serial_no or source_doc.batch_no)
|
||||||
and not source_doc.serial_and_batch_bundle
|
and not source_doc.serial_and_batch_bundle
|
||||||
@@ -902,6 +906,7 @@ def get_serial_batches_based_on_bundle(field, _bundle_ids):
|
|||||||
"`tabSerial and Batch Entry`.`serial_no`",
|
"`tabSerial and Batch Entry`.`serial_no`",
|
||||||
"`tabSerial and Batch Entry`.`batch_no`",
|
"`tabSerial and Batch Entry`.`batch_no`",
|
||||||
"`tabSerial and Batch Entry`.`qty`",
|
"`tabSerial and Batch Entry`.`qty`",
|
||||||
|
"`tabSerial and Batch Entry`.`incoming_rate`",
|
||||||
"`tabSerial and Batch Bundle`.`voucher_detail_no`",
|
"`tabSerial and Batch Bundle`.`voucher_detail_no`",
|
||||||
"`tabSerial and Batch Bundle`.`voucher_type`",
|
"`tabSerial and Batch Bundle`.`voucher_type`",
|
||||||
"`tabSerial and Batch Bundle`.`voucher_no`",
|
"`tabSerial and Batch Bundle`.`voucher_no`",
|
||||||
@@ -923,15 +928,23 @@ def get_serial_batches_based_on_bundle(field, _bundle_ids):
|
|||||||
|
|
||||||
if key not in available_dict:
|
if key not in available_dict:
|
||||||
available_dict[key] = frappe._dict(
|
available_dict[key] = frappe._dict(
|
||||||
{"qty": 0.0, "serial_nos": defaultdict(float), "batches": defaultdict(float)}
|
{
|
||||||
|
"qty": 0.0,
|
||||||
|
"serial_nos": defaultdict(float),
|
||||||
|
"batches": defaultdict(float),
|
||||||
|
"serial_nos_valuation": defaultdict(float),
|
||||||
|
"batches_valuation": defaultdict(float),
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
available_dict[key]["qty"] += row.qty
|
available_dict[key]["qty"] += row.qty
|
||||||
|
|
||||||
if row.serial_no:
|
if row.serial_no:
|
||||||
available_dict[key]["serial_nos"][row.serial_no] += row.qty
|
available_dict[key]["serial_nos"][row.serial_no] += row.qty
|
||||||
|
available_dict[key]["serial_nos_valuation"][row.serial_no] = row.incoming_rate
|
||||||
elif row.batch_no:
|
elif row.batch_no:
|
||||||
available_dict[key]["batches"][row.batch_no] += row.qty
|
available_dict[key]["batches"][row.batch_no] += row.qty
|
||||||
|
available_dict[key]["batches_valuation"][row.batch_no] = row.incoming_rate
|
||||||
|
|
||||||
return available_dict
|
return available_dict
|
||||||
|
|
||||||
@@ -967,12 +980,13 @@ def get_serial_and_batch_bundle(field, doctype, reference_ids, is_rejected=False
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
fields = [
|
fields = ["serial_and_batch_bundle"]
|
||||||
"serial_and_batch_bundle",
|
|
||||||
]
|
|
||||||
|
|
||||||
if is_rejected:
|
if is_rejected:
|
||||||
fields.extend(["rejected_serial_and_batch_bundle", "return_qty_from_rejected_warehouse"])
|
fields.append("rejected_serial_and_batch_bundle")
|
||||||
|
|
||||||
|
if doctype == "Purchase Receipt Item":
|
||||||
|
fields.append("return_qty_from_rejected_warehouse")
|
||||||
|
|
||||||
del filters["rejected_serial_and_batch_bundle"]
|
del filters["rejected_serial_and_batch_bundle"]
|
||||||
data = frappe.get_all(
|
data = frappe.get_all(
|
||||||
@@ -1006,7 +1020,14 @@ def filter_serial_batches(parent_doc, data, row, warehouse_field=None, qty_field
|
|||||||
warehouse = row.get(warehouse_field)
|
warehouse = row.get(warehouse_field)
|
||||||
qty = abs(row.get(qty_field))
|
qty = abs(row.get(qty_field))
|
||||||
|
|
||||||
filterd_serial_batch = frappe._dict({"serial_nos": [], "batches": defaultdict(float)})
|
filterd_serial_batch = frappe._dict(
|
||||||
|
{
|
||||||
|
"serial_nos": [],
|
||||||
|
"batches": defaultdict(float),
|
||||||
|
"serial_nos_valuation": data.get("serial_nos_valuation"),
|
||||||
|
"batches_valuation": data.get("batches_valuation"),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
if data.serial_nos:
|
if data.serial_nos:
|
||||||
available_serial_nos = []
|
available_serial_nos = []
|
||||||
@@ -1016,7 +1037,7 @@ def filter_serial_batches(parent_doc, data, row, warehouse_field=None, qty_field
|
|||||||
|
|
||||||
if available_serial_nos:
|
if available_serial_nos:
|
||||||
if parent_doc.doctype in ["Purchase Invoice", "Purchase Reecipt"]:
|
if parent_doc.doctype in ["Purchase Invoice", "Purchase Reecipt"]:
|
||||||
available_serial_nos = get_available_serial_nos(available_serial_nos)
|
available_serial_nos = get_available_serial_nos(available_serial_nos, warehouse)
|
||||||
|
|
||||||
if len(available_serial_nos) > qty:
|
if len(available_serial_nos) > qty:
|
||||||
filterd_serial_batch["serial_nos"] = sorted(available_serial_nos[0 : cint(qty)])
|
filterd_serial_batch["serial_nos"] = sorted(available_serial_nos[0 : cint(qty)])
|
||||||
@@ -1101,6 +1122,8 @@ def make_serial_batch_bundle_for_return(data, child_doc, parent_doc, warehouse_f
|
|||||||
"warehouse": warehouse,
|
"warehouse": warehouse,
|
||||||
"serial_nos": data.get("serial_nos"),
|
"serial_nos": data.get("serial_nos"),
|
||||||
"batches": data.get("batches"),
|
"batches": data.get("batches"),
|
||||||
|
"serial_nos_valuation": data.get("serial_nos_valuation"),
|
||||||
|
"batches_valuation": data.get("batches_valuation"),
|
||||||
"posting_date": parent_doc.posting_date,
|
"posting_date": parent_doc.posting_date,
|
||||||
"posting_time": parent_doc.posting_time,
|
"posting_time": parent_doc.posting_time,
|
||||||
"voucher_type": parent_doc.doctype,
|
"voucher_type": parent_doc.doctype,
|
||||||
|
|||||||
@@ -334,6 +334,11 @@ class StockController(AccountsController):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if self.doctype in ["Sales Invoice", "Delivery Note"]:
|
||||||
|
row.db_set(
|
||||||
|
"incoming_rate", frappe.db.get_value("Serial and Batch Bundle", bundle, "avg_rate")
|
||||||
|
)
|
||||||
|
|
||||||
def get_reference_ids(self, table_name, qty_field=None, bundle_field=None) -> tuple[str, list[str]]:
|
def get_reference_ids(self, table_name, qty_field=None, bundle_field=None) -> tuple[str, list[str]]:
|
||||||
field = {
|
field = {
|
||||||
"Sales Invoice": "sales_invoice_item",
|
"Sales Invoice": "sales_invoice_item",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.tests import IntegrationTestCase, UnitTestCase
|
from frappe.tests import IntegrationTestCase, UnitTestCase
|
||||||
@@ -2116,6 +2117,264 @@ class TestDeliveryNote(IntegrationTestCase):
|
|||||||
|
|
||||||
self.assertEqual(stock_value_difference, 100.0 * 5)
|
self.assertEqual(stock_value_difference, 100.0 * 5)
|
||||||
|
|
||||||
|
def test_delivery_note_return_valuation_without_use_serial_batch_field(self):
|
||||||
|
from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return
|
||||||
|
|
||||||
|
batch_item = make_item(
|
||||||
|
"_Test Delivery Note Return Valuation Batch Item",
|
||||||
|
properties={
|
||||||
|
"has_batch_no": 1,
|
||||||
|
"create_new_batch": 1,
|
||||||
|
"is_stock_item": 1,
|
||||||
|
"batch_number_series": "BRTN-DNN-BI-.#####",
|
||||||
|
},
|
||||||
|
).name
|
||||||
|
|
||||||
|
serial_item = make_item(
|
||||||
|
"_Test Delivery Note Return Valuation Serial Item",
|
||||||
|
properties={"has_serial_no": 1, "is_stock_item": 1, "serial_no_series": "SRTN-DNN-TP-.#####"},
|
||||||
|
).name
|
||||||
|
|
||||||
|
batches = {}
|
||||||
|
serial_nos = []
|
||||||
|
for qty, rate in {3: 300, 2: 100}.items():
|
||||||
|
se = make_stock_entry(
|
||||||
|
item_code=batch_item, target="_Test Warehouse - _TC", qty=qty, basic_rate=rate
|
||||||
|
)
|
||||||
|
batches[get_batch_from_bundle(se.items[0].serial_and_batch_bundle)] = qty
|
||||||
|
|
||||||
|
for qty, rate in {2: 100, 1: 50}.items():
|
||||||
|
make_stock_entry(item_code=serial_item, target="_Test Warehouse - _TC", qty=qty, basic_rate=rate)
|
||||||
|
serial_nos.extend(get_serial_nos_from_bundle(se.items[0].serial_and_batch_bundle))
|
||||||
|
|
||||||
|
dn = create_delivery_note(
|
||||||
|
item_code=batch_item,
|
||||||
|
qty=5,
|
||||||
|
rate=1000,
|
||||||
|
use_serial_batch_fields=0,
|
||||||
|
batches=batches,
|
||||||
|
do_not_submit=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
bundle_id = make_serial_batch_bundle(
|
||||||
|
frappe._dict(
|
||||||
|
{
|
||||||
|
"item_code": serial_item,
|
||||||
|
"warehouse": dn.items[0].warehouse,
|
||||||
|
"qty": 3,
|
||||||
|
"voucher_type": "Delivery Note",
|
||||||
|
"serial_nos": serial_nos,
|
||||||
|
"posting_date": dn.posting_date,
|
||||||
|
"posting_time": dn.posting_time,
|
||||||
|
"type_of_transaction": "Outward",
|
||||||
|
"do_not_submit": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
).name
|
||||||
|
|
||||||
|
dn.append(
|
||||||
|
"items",
|
||||||
|
{
|
||||||
|
"item_code": serial_item,
|
||||||
|
"qty": 3,
|
||||||
|
"rate": 700,
|
||||||
|
"base_rate": 700,
|
||||||
|
"item_name": serial_item,
|
||||||
|
"uom": "Nos",
|
||||||
|
"stock_uom": "Nos",
|
||||||
|
"conversion_factor": 1,
|
||||||
|
"warehouse": dn.items[0].warehouse,
|
||||||
|
"use_serial_batch_fields": 0,
|
||||||
|
"serial_and_batch_bundle": bundle_id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
dn.save()
|
||||||
|
dn.submit()
|
||||||
|
dn.reload()
|
||||||
|
|
||||||
|
batch_no_valuation = defaultdict(float)
|
||||||
|
serial_no_valuation = defaultdict(float)
|
||||||
|
|
||||||
|
for row in dn.items:
|
||||||
|
if row.serial_and_batch_bundle:
|
||||||
|
bundle_data = frappe.get_all(
|
||||||
|
"Serial and Batch Entry",
|
||||||
|
filters={"parent": row.serial_and_batch_bundle},
|
||||||
|
fields=["incoming_rate", "serial_no", "batch_no"],
|
||||||
|
)
|
||||||
|
|
||||||
|
for d in bundle_data:
|
||||||
|
if d.batch_no:
|
||||||
|
batch_no_valuation[d.batch_no] = d.incoming_rate
|
||||||
|
elif d.serial_no:
|
||||||
|
serial_no_valuation[d.serial_no] = d.incoming_rate
|
||||||
|
|
||||||
|
return_entry = make_sales_return(dn.name)
|
||||||
|
|
||||||
|
return_entry.save()
|
||||||
|
return_entry.submit()
|
||||||
|
return_entry.reload()
|
||||||
|
|
||||||
|
for row in return_entry.items:
|
||||||
|
if row.item_code == batch_item:
|
||||||
|
bundle_data = frappe.get_all(
|
||||||
|
"Serial and Batch Entry",
|
||||||
|
filters={"parent": row.serial_and_batch_bundle},
|
||||||
|
fields=["incoming_rate", "batch_no"],
|
||||||
|
)
|
||||||
|
|
||||||
|
for d in bundle_data:
|
||||||
|
self.assertEqual(d.incoming_rate, batch_no_valuation[d.batch_no])
|
||||||
|
else:
|
||||||
|
bundle_data = frappe.get_all(
|
||||||
|
"Serial and Batch Entry",
|
||||||
|
filters={"parent": row.serial_and_batch_bundle},
|
||||||
|
fields=["incoming_rate", "serial_no"],
|
||||||
|
)
|
||||||
|
|
||||||
|
for d in bundle_data:
|
||||||
|
self.assertEqual(d.incoming_rate, serial_no_valuation[d.serial_no])
|
||||||
|
|
||||||
|
def test_delivery_note_return_valuation_with_use_serial_batch_field(self):
|
||||||
|
from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return
|
||||||
|
|
||||||
|
batch_item = make_item(
|
||||||
|
"_Test Delivery Note Return Valuation WITH Batch Item",
|
||||||
|
properties={
|
||||||
|
"has_batch_no": 1,
|
||||||
|
"create_new_batch": 1,
|
||||||
|
"is_stock_item": 1,
|
||||||
|
"batch_number_series": "BRTN-DNN-BIW-.#####",
|
||||||
|
},
|
||||||
|
).name
|
||||||
|
|
||||||
|
serial_item = make_item(
|
||||||
|
"_Test Delivery Note Return Valuation WITH Serial Item",
|
||||||
|
properties={"has_serial_no": 1, "is_stock_item": 1, "serial_no_series": "SRTN-DNN-TPW-.#####"},
|
||||||
|
).name
|
||||||
|
|
||||||
|
batches = []
|
||||||
|
serial_nos = []
|
||||||
|
for qty, rate in {3: 300, 2: 100}.items():
|
||||||
|
se = make_stock_entry(
|
||||||
|
item_code=batch_item, target="_Test Warehouse - _TC", qty=qty, basic_rate=rate
|
||||||
|
)
|
||||||
|
batches.append(get_batch_from_bundle(se.items[0].serial_and_batch_bundle))
|
||||||
|
|
||||||
|
for qty, rate in {2: 100, 1: 50}.items():
|
||||||
|
se = make_stock_entry(
|
||||||
|
item_code=serial_item, target="_Test Warehouse - _TC", qty=qty, basic_rate=rate
|
||||||
|
)
|
||||||
|
serial_nos.extend(get_serial_nos_from_bundle(se.items[0].serial_and_batch_bundle))
|
||||||
|
|
||||||
|
dn = create_delivery_note(
|
||||||
|
item_code=batch_item,
|
||||||
|
qty=3,
|
||||||
|
rate=1000,
|
||||||
|
use_serial_batch_fields=1,
|
||||||
|
batch_no=batches[0],
|
||||||
|
do_not_submit=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
dn.append(
|
||||||
|
"items",
|
||||||
|
{
|
||||||
|
"item_code": batch_item,
|
||||||
|
"qty": 2,
|
||||||
|
"rate": 1000,
|
||||||
|
"base_rate": 1000,
|
||||||
|
"item_name": batch_item,
|
||||||
|
"uom": dn.items[0].uom,
|
||||||
|
"stock_uom": dn.items[0].uom,
|
||||||
|
"conversion_factor": 1,
|
||||||
|
"warehouse": dn.items[0].warehouse,
|
||||||
|
"use_serial_batch_fields": 1,
|
||||||
|
"batch_no": batches[1],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
dn.append(
|
||||||
|
"items",
|
||||||
|
{
|
||||||
|
"item_code": serial_item,
|
||||||
|
"qty": 2,
|
||||||
|
"rate": 700,
|
||||||
|
"base_rate": 700,
|
||||||
|
"item_name": serial_item,
|
||||||
|
"uom": "Nos",
|
||||||
|
"stock_uom": "Nos",
|
||||||
|
"conversion_factor": 1,
|
||||||
|
"warehouse": dn.items[0].warehouse,
|
||||||
|
"use_serial_batch_fields": 1,
|
||||||
|
"serial_no": "\n".join(serial_nos[0:2]),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
dn.append(
|
||||||
|
"items",
|
||||||
|
{
|
||||||
|
"item_code": serial_item,
|
||||||
|
"qty": 1,
|
||||||
|
"rate": 700,
|
||||||
|
"base_rate": 700,
|
||||||
|
"item_name": serial_item,
|
||||||
|
"uom": "Nos",
|
||||||
|
"stock_uom": "Nos",
|
||||||
|
"conversion_factor": 1,
|
||||||
|
"warehouse": dn.items[0].warehouse,
|
||||||
|
"use_serial_batch_fields": 1,
|
||||||
|
"serial_no": serial_nos[-1],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
dn.save()
|
||||||
|
dn.submit()
|
||||||
|
dn.reload()
|
||||||
|
|
||||||
|
batch_no_valuation = defaultdict(float)
|
||||||
|
serial_no_valuation = defaultdict(float)
|
||||||
|
|
||||||
|
for row in dn.items:
|
||||||
|
if row.serial_and_batch_bundle:
|
||||||
|
bundle_data = frappe.get_all(
|
||||||
|
"Serial and Batch Entry",
|
||||||
|
filters={"parent": row.serial_and_batch_bundle},
|
||||||
|
fields=["incoming_rate", "serial_no", "batch_no"],
|
||||||
|
)
|
||||||
|
|
||||||
|
for d in bundle_data:
|
||||||
|
if d.batch_no:
|
||||||
|
batch_no_valuation[d.batch_no] = d.incoming_rate
|
||||||
|
elif d.serial_no:
|
||||||
|
serial_no_valuation[d.serial_no] = d.incoming_rate
|
||||||
|
|
||||||
|
return_entry = make_sales_return(dn.name)
|
||||||
|
|
||||||
|
return_entry.save()
|
||||||
|
return_entry.submit()
|
||||||
|
return_entry.reload()
|
||||||
|
|
||||||
|
for row in return_entry.items:
|
||||||
|
if row.item_code == batch_item:
|
||||||
|
bundle_data = frappe.get_all(
|
||||||
|
"Serial and Batch Entry",
|
||||||
|
filters={"parent": row.serial_and_batch_bundle},
|
||||||
|
fields=["incoming_rate", "batch_no"],
|
||||||
|
)
|
||||||
|
|
||||||
|
for d in bundle_data:
|
||||||
|
self.assertEqual(d.incoming_rate, batch_no_valuation[d.batch_no])
|
||||||
|
else:
|
||||||
|
bundle_data = frappe.get_all(
|
||||||
|
"Serial and Batch Entry",
|
||||||
|
filters={"parent": row.serial_and_batch_bundle},
|
||||||
|
fields=["incoming_rate", "serial_no"],
|
||||||
|
)
|
||||||
|
|
||||||
|
for d in bundle_data:
|
||||||
|
self.assertEqual(d.incoming_rate, serial_no_valuation[d.serial_no])
|
||||||
|
|
||||||
|
|
||||||
def create_delivery_note(**args):
|
def create_delivery_note(**args):
|
||||||
dn = frappe.new_doc("Delivery Note")
|
dn = frappe.new_doc("Delivery Note")
|
||||||
@@ -2143,6 +2402,9 @@ def create_delivery_note(**args):
|
|||||||
if args.get("batch_no"):
|
if args.get("batch_no"):
|
||||||
batches = frappe._dict({args.batch_no: qty})
|
batches = frappe._dict({args.batch_no: qty})
|
||||||
|
|
||||||
|
if args.get("batches"):
|
||||||
|
batches = frappe._dict(args.batches)
|
||||||
|
|
||||||
bundle_id = make_serial_batch_bundle(
|
bundle_id = make_serial_batch_bundle(
|
||||||
frappe._dict(
|
frappe._dict(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3721,6 +3721,234 @@ class TestPurchaseReceipt(IntegrationTestCase):
|
|||||||
|
|
||||||
self.assertEqual(pr.items[0].conversion_factor, 1.0)
|
self.assertEqual(pr.items[0].conversion_factor, 1.0)
|
||||||
|
|
||||||
|
def test_purchase_receipt_return_valuation_without_use_serial_batch_field(self):
|
||||||
|
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_return
|
||||||
|
|
||||||
|
batch_item = make_item(
|
||||||
|
"_Test Purchase Receipt Return Valuation Batch Item",
|
||||||
|
properties={
|
||||||
|
"has_batch_no": 1,
|
||||||
|
"create_new_batch": 1,
|
||||||
|
"is_stock_item": 1,
|
||||||
|
"batch_number_series": "BRTN-TPRBI-.#####",
|
||||||
|
},
|
||||||
|
).name
|
||||||
|
|
||||||
|
serial_item = make_item(
|
||||||
|
"_Test Purchase Receipt Return Valuation Serial Item",
|
||||||
|
properties={"has_serial_no": 1, "is_stock_item": 1, "serial_no_series": "SRTN-TPRSI-.#####"},
|
||||||
|
).name
|
||||||
|
|
||||||
|
rej_warehouse = create_warehouse("_Test Purchase Warehouse For Rejected Qty")
|
||||||
|
|
||||||
|
pr = make_purchase_receipt(
|
||||||
|
item_code=batch_item,
|
||||||
|
received_qty=10,
|
||||||
|
qty=8,
|
||||||
|
rejected_qty=2,
|
||||||
|
rejected_warehouse=rej_warehouse,
|
||||||
|
rate=300,
|
||||||
|
do_not_submit=1,
|
||||||
|
use_serial_batch_fields=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
pr.append(
|
||||||
|
"items",
|
||||||
|
{
|
||||||
|
"item_code": serial_item,
|
||||||
|
"qty": 2,
|
||||||
|
"rate": 100,
|
||||||
|
"base_rate": 100,
|
||||||
|
"item_name": serial_item,
|
||||||
|
"uom": "Nos",
|
||||||
|
"stock_uom": "Nos",
|
||||||
|
"conversion_factor": 1,
|
||||||
|
"rejected_qty": 1,
|
||||||
|
"warehouse": pr.items[0].warehouse,
|
||||||
|
"use_serial_batch_fields": 0,
|
||||||
|
"rejected_warehouse": rej_warehouse,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
pr.save()
|
||||||
|
pr.submit()
|
||||||
|
pr.reload()
|
||||||
|
|
||||||
|
batch_no = get_batch_from_bundle(pr.items[0].serial_and_batch_bundle)
|
||||||
|
rejected_batch_no = get_batch_from_bundle(pr.items[0].rejected_serial_and_batch_bundle)
|
||||||
|
|
||||||
|
self.assertEqual(batch_no, rejected_batch_no)
|
||||||
|
|
||||||
|
return_entry = make_purchase_return(pr.name)
|
||||||
|
|
||||||
|
return_entry.save()
|
||||||
|
return_entry.submit()
|
||||||
|
return_entry.reload()
|
||||||
|
|
||||||
|
for row in return_entry.items:
|
||||||
|
if row.item_code == batch_item:
|
||||||
|
bundle_data = frappe.get_all(
|
||||||
|
"Serial and Batch Entry",
|
||||||
|
filters={"parent": row.serial_and_batch_bundle},
|
||||||
|
pluck="incoming_rate",
|
||||||
|
)
|
||||||
|
|
||||||
|
for incoming_rate in bundle_data:
|
||||||
|
self.assertEqual(incoming_rate, 300.00)
|
||||||
|
else:
|
||||||
|
bundle_data = frappe.get_all(
|
||||||
|
"Serial and Batch Entry",
|
||||||
|
filters={"parent": row.serial_and_batch_bundle},
|
||||||
|
pluck="incoming_rate",
|
||||||
|
)
|
||||||
|
|
||||||
|
for incoming_rate in bundle_data:
|
||||||
|
self.assertEqual(incoming_rate, 100.00)
|
||||||
|
|
||||||
|
for row in return_entry.items:
|
||||||
|
if row.item_code == batch_item:
|
||||||
|
bundle_data = frappe.get_all(
|
||||||
|
"Serial and Batch Entry",
|
||||||
|
filters={"parent": row.rejected_serial_and_batch_bundle},
|
||||||
|
pluck="incoming_rate",
|
||||||
|
)
|
||||||
|
|
||||||
|
for incoming_rate in bundle_data:
|
||||||
|
self.assertEqual(incoming_rate, 0)
|
||||||
|
else:
|
||||||
|
bundle_data = frappe.get_all(
|
||||||
|
"Serial and Batch Entry",
|
||||||
|
filters={"parent": row.rejected_serial_and_batch_bundle},
|
||||||
|
pluck="incoming_rate",
|
||||||
|
)
|
||||||
|
|
||||||
|
for incoming_rate in bundle_data:
|
||||||
|
self.assertEqual(incoming_rate, 0)
|
||||||
|
|
||||||
|
def test_purchase_receipt_return_valuation_with_use_serial_batch_field(self):
|
||||||
|
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_return
|
||||||
|
|
||||||
|
batch_item = make_item(
|
||||||
|
"_Test Purchase Receipt Return Valuation With Batch Item",
|
||||||
|
properties={"has_batch_no": 1, "create_new_batch": 1, "is_stock_item": 1},
|
||||||
|
).name
|
||||||
|
|
||||||
|
serial_item = make_item(
|
||||||
|
"_Test Purchase Receipt Return Valuation With Serial Item",
|
||||||
|
properties={"has_serial_no": 1, "is_stock_item": 1},
|
||||||
|
).name
|
||||||
|
|
||||||
|
rej_warehouse = create_warehouse("_Test Purchase Warehouse For Rejected Qty")
|
||||||
|
|
||||||
|
batch_no = "BATCH-RTN-BNU-TPRBI-0001"
|
||||||
|
serial_nos = ["SNU-RTN-TPRSI-0001", "SNU-RTN-TPRSI-0002", "SNU-RTN-TPRSI-0003"]
|
||||||
|
|
||||||
|
if not frappe.db.exists("Batch", batch_no):
|
||||||
|
frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Batch",
|
||||||
|
"batch_id": batch_no,
|
||||||
|
"item": batch_item,
|
||||||
|
}
|
||||||
|
).insert()
|
||||||
|
|
||||||
|
for serial_no in serial_nos:
|
||||||
|
if not frappe.db.exists("Serial No", serial_no):
|
||||||
|
frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Serial No",
|
||||||
|
"item_code": serial_item,
|
||||||
|
"serial_no": serial_no,
|
||||||
|
}
|
||||||
|
).insert()
|
||||||
|
|
||||||
|
pr = make_purchase_receipt(
|
||||||
|
item_code=batch_item,
|
||||||
|
received_qty=10,
|
||||||
|
qty=8,
|
||||||
|
rejected_qty=2,
|
||||||
|
rejected_warehouse=rej_warehouse,
|
||||||
|
batch_no=batch_no,
|
||||||
|
use_serial_batch_fields=1,
|
||||||
|
rate=300,
|
||||||
|
do_not_submit=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
pr.append(
|
||||||
|
"items",
|
||||||
|
{
|
||||||
|
"item_code": serial_item,
|
||||||
|
"qty": 2,
|
||||||
|
"rate": 100,
|
||||||
|
"base_rate": 100,
|
||||||
|
"item_name": serial_item,
|
||||||
|
"uom": "Nos",
|
||||||
|
"stock_uom": "Nos",
|
||||||
|
"conversion_factor": 1,
|
||||||
|
"rejected_qty": 1,
|
||||||
|
"warehouse": pr.items[0].warehouse,
|
||||||
|
"use_serial_batch_fields": 1,
|
||||||
|
"rejected_warehouse": rej_warehouse,
|
||||||
|
"serial_no": "\n".join(serial_nos[:2]),
|
||||||
|
"rejected_serial_no": serial_nos[2],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
pr.save()
|
||||||
|
pr.submit()
|
||||||
|
pr.reload()
|
||||||
|
|
||||||
|
batch_no = get_batch_from_bundle(pr.items[0].serial_and_batch_bundle)
|
||||||
|
rejected_batch_no = get_batch_from_bundle(pr.items[0].rejected_serial_and_batch_bundle)
|
||||||
|
|
||||||
|
self.assertEqual(batch_no, rejected_batch_no)
|
||||||
|
|
||||||
|
return_entry = make_purchase_return(pr.name)
|
||||||
|
|
||||||
|
return_entry.save()
|
||||||
|
return_entry.submit()
|
||||||
|
return_entry.reload()
|
||||||
|
|
||||||
|
for row in return_entry.items:
|
||||||
|
if row.item_code == batch_item:
|
||||||
|
bundle_data = frappe.get_all(
|
||||||
|
"Serial and Batch Entry",
|
||||||
|
filters={"parent": row.serial_and_batch_bundle},
|
||||||
|
pluck="incoming_rate",
|
||||||
|
)
|
||||||
|
|
||||||
|
for incoming_rate in bundle_data:
|
||||||
|
self.assertEqual(incoming_rate, 300.00)
|
||||||
|
else:
|
||||||
|
bundle_data = frappe.get_all(
|
||||||
|
"Serial and Batch Entry",
|
||||||
|
filters={"parent": row.serial_and_batch_bundle},
|
||||||
|
pluck="incoming_rate",
|
||||||
|
)
|
||||||
|
|
||||||
|
for incoming_rate in bundle_data:
|
||||||
|
self.assertEqual(incoming_rate, 100.00)
|
||||||
|
|
||||||
|
for row in return_entry.items:
|
||||||
|
if row.item_code == batch_item:
|
||||||
|
bundle_data = frappe.get_all(
|
||||||
|
"Serial and Batch Entry",
|
||||||
|
filters={"parent": row.rejected_serial_and_batch_bundle},
|
||||||
|
pluck="incoming_rate",
|
||||||
|
)
|
||||||
|
|
||||||
|
for incoming_rate in bundle_data:
|
||||||
|
self.assertEqual(incoming_rate, 0)
|
||||||
|
else:
|
||||||
|
bundle_data = frappe.get_all(
|
||||||
|
"Serial and Batch Entry",
|
||||||
|
filters={"parent": row.rejected_serial_and_batch_bundle},
|
||||||
|
pluck="incoming_rate",
|
||||||
|
)
|
||||||
|
|
||||||
|
for incoming_rate in bundle_data:
|
||||||
|
self.assertEqual(incoming_rate, 0)
|
||||||
|
|
||||||
|
|
||||||
def prepare_data_for_internal_transfer():
|
def prepare_data_for_internal_transfer():
|
||||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
|
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
|
||||||
|
|||||||
@@ -197,7 +197,7 @@ class SerialandBatchBundle(Document):
|
|||||||
def throw_error_message(self, message, exception=frappe.ValidationError):
|
def throw_error_message(self, message, exception=frappe.ValidationError):
|
||||||
frappe.throw(_(message), exception, title=_("Error"))
|
frappe.throw(_(message), exception, title=_("Error"))
|
||||||
|
|
||||||
def set_incoming_rate(self, row=None, save=False, allow_negative_stock=False):
|
def set_incoming_rate(self, parent=None, row=None, save=False, allow_negative_stock=False):
|
||||||
if self.type_of_transaction not in ["Inward", "Outward"] or self.voucher_type in [
|
if self.type_of_transaction not in ["Inward", "Outward"] or self.voucher_type in [
|
||||||
"Installation Note",
|
"Installation Note",
|
||||||
"Job Card",
|
"Job Card",
|
||||||
@@ -206,13 +206,70 @@ class SerialandBatchBundle(Document):
|
|||||||
]:
|
]:
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.type_of_transaction == "Outward":
|
if return_aginst := self.get_return_aginst(parent=parent):
|
||||||
|
self.set_valuation_rate_for_return_entry(return_aginst, save)
|
||||||
|
elif self.type_of_transaction == "Outward":
|
||||||
self.set_incoming_rate_for_outward_transaction(
|
self.set_incoming_rate_for_outward_transaction(
|
||||||
row, save, allow_negative_stock=allow_negative_stock
|
row, save, allow_negative_stock=allow_negative_stock
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.set_incoming_rate_for_inward_transaction(row, save)
|
self.set_incoming_rate_for_inward_transaction(row, save)
|
||||||
|
|
||||||
|
def set_valuation_rate_for_return_entry(self, return_aginst, save=False):
|
||||||
|
if valuation_details := self.get_valuation_rate_for_return_entry(return_aginst):
|
||||||
|
for row in self.entries:
|
||||||
|
if row.serial_no:
|
||||||
|
valuation_rate = valuation_details["serial_nos"].get(row.serial_no)
|
||||||
|
else:
|
||||||
|
valuation_rate = valuation_details["batches"].get(row.batch_no)
|
||||||
|
|
||||||
|
row.incoming_rate = valuation_rate
|
||||||
|
row.stock_value_difference = flt(row.qty) * flt(row.incoming_rate)
|
||||||
|
|
||||||
|
if save:
|
||||||
|
row.db_set(
|
||||||
|
{
|
||||||
|
"incoming_rate": row.incoming_rate,
|
||||||
|
"stock_value_difference": row.stock_value_difference,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_valuation_rate_for_return_entry(self, return_aginst):
|
||||||
|
valuation_details = frappe._dict(
|
||||||
|
{
|
||||||
|
"serial_nos": defaultdict(float),
|
||||||
|
"batches": defaultdict(float),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
bundle_data = frappe.get_all(
|
||||||
|
"Serial and Batch Bundle",
|
||||||
|
fields=[
|
||||||
|
"`tabSerial and Batch Entry`.`serial_no`",
|
||||||
|
"`tabSerial and Batch Entry`.`batch_no`",
|
||||||
|
"`tabSerial and Batch Entry`.`incoming_rate`",
|
||||||
|
],
|
||||||
|
filters=[
|
||||||
|
["Serial and Batch Bundle", "voucher_no", "=", return_aginst],
|
||||||
|
["Serial and Batch Entry", "docstatus", "=", 1],
|
||||||
|
["Serial and Batch Bundle", "is_cancelled", "=", 0],
|
||||||
|
["Serial and Batch Bundle", "item_code", "=", self.item_code],
|
||||||
|
["Serial and Batch Bundle", "warehouse", "=", self.warehouse],
|
||||||
|
],
|
||||||
|
order_by="`tabSerial and Batch Bundle`.`creation`, `tabSerial and Batch Entry`.`idx`",
|
||||||
|
)
|
||||||
|
|
||||||
|
if not bundle_data:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
for row in bundle_data:
|
||||||
|
if row.serial_no:
|
||||||
|
valuation_details["serial_nos"][row.serial_no] = row.incoming_rate
|
||||||
|
else:
|
||||||
|
valuation_details["batches"][row.batch_no] = row.incoming_rate
|
||||||
|
|
||||||
|
return valuation_details
|
||||||
|
|
||||||
def calculate_total_qty(self, save=True):
|
def calculate_total_qty(self, save=True):
|
||||||
self.total_qty = 0.0
|
self.total_qty = 0.0
|
||||||
for d in self.entries:
|
for d in self.entries:
|
||||||
@@ -327,6 +384,33 @@ class SerialandBatchBundle(Document):
|
|||||||
|
|
||||||
return sle
|
return sle
|
||||||
|
|
||||||
|
def get_return_aginst(self, parent=None):
|
||||||
|
return_aginst = None
|
||||||
|
|
||||||
|
if parent and parent.get("is_return") and parent.get("return_against"):
|
||||||
|
return parent.get("return_against")
|
||||||
|
|
||||||
|
if (
|
||||||
|
self.voucher_type
|
||||||
|
in [
|
||||||
|
"Delivery Note",
|
||||||
|
"Sales Invoice",
|
||||||
|
"Purchase Invoice",
|
||||||
|
"Purchase Receipt",
|
||||||
|
"POS Invoice",
|
||||||
|
"Subcontracting Receipt",
|
||||||
|
]
|
||||||
|
and self.voucher_type
|
||||||
|
and self.voucher_no
|
||||||
|
):
|
||||||
|
voucher_details = frappe.db.get_value(
|
||||||
|
self.voucher_type, self.voucher_no, ["is_return", "return_against"], as_dict=True
|
||||||
|
)
|
||||||
|
if voucher_details and voucher_details.get("is_return") and voucher_details.get("return_against"):
|
||||||
|
return voucher_details.get("return_against")
|
||||||
|
|
||||||
|
return return_aginst
|
||||||
|
|
||||||
def set_incoming_rate_for_inward_transaction(self, row=None, save=False):
|
def set_incoming_rate_for_inward_transaction(self, row=None, save=False):
|
||||||
valuation_field = "valuation_rate"
|
valuation_field = "valuation_rate"
|
||||||
if self.voucher_type in ["Sales Invoice", "Delivery Note", "Quotation"]:
|
if self.voucher_type in ["Sales Invoice", "Delivery Note", "Quotation"]:
|
||||||
@@ -354,7 +438,9 @@ class SerialandBatchBundle(Document):
|
|||||||
rate = frappe.db.get_value(child_table, self.voucher_detail_no, valuation_field)
|
rate = frappe.db.get_value(child_table, self.voucher_detail_no, valuation_field)
|
||||||
|
|
||||||
for d in self.entries:
|
for d in self.entries:
|
||||||
if (d.incoming_rate == rate) and d.qty and d.stock_value_difference:
|
if self.is_rejected:
|
||||||
|
rate = 0.0
|
||||||
|
elif (d.incoming_rate == rate) and d.qty and d.stock_value_difference:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
d.incoming_rate = flt(rate, precision)
|
d.incoming_rate = flt(rate, precision)
|
||||||
@@ -403,7 +489,7 @@ class SerialandBatchBundle(Document):
|
|||||||
|
|
||||||
# If user has changed the rate in the child table
|
# If user has changed the rate in the child table
|
||||||
if self.docstatus == 0:
|
if self.docstatus == 0:
|
||||||
self.set_incoming_rate(save=True, row=row)
|
self.set_incoming_rate(parent=parent, row=row, save=True)
|
||||||
|
|
||||||
if self.docstatus == 0 and parent.get("is_return") and parent.is_new():
|
if self.docstatus == 0 and parent.get("is_return") and parent.is_new():
|
||||||
self.reset_qty(row, qty_field=qty_field)
|
self.reset_qty(row, qty_field=qty_field)
|
||||||
|
|||||||
@@ -1088,6 +1088,8 @@ class SerialBatchCreation:
|
|||||||
frappe.db.bulk_insert("Serial No", fields=fields, values=set(serial_nos_details))
|
frappe.db.bulk_insert("Serial No", fields=fields, values=set(serial_nos_details))
|
||||||
|
|
||||||
def set_serial_batch_entries(self, doc):
|
def set_serial_batch_entries(self, doc):
|
||||||
|
incoming_rate = self.get("incoming_rate")
|
||||||
|
|
||||||
if self.get("serial_nos"):
|
if self.get("serial_nos"):
|
||||||
serial_no_wise_batch = frappe._dict({})
|
serial_no_wise_batch = frappe._dict({})
|
||||||
if self.has_batch_no:
|
if self.has_batch_no:
|
||||||
@@ -1095,30 +1097,54 @@ class SerialBatchCreation:
|
|||||||
|
|
||||||
qty = -1 if self.type_of_transaction == "Outward" else 1
|
qty = -1 if self.type_of_transaction == "Outward" else 1
|
||||||
for serial_no in self.serial_nos:
|
for serial_no in self.serial_nos:
|
||||||
|
if self.get("serial_nos_valuation"):
|
||||||
|
incoming_rate = self.get("serial_nos_valuation").get(serial_no)
|
||||||
|
|
||||||
doc.append(
|
doc.append(
|
||||||
"entries",
|
"entries",
|
||||||
{
|
{
|
||||||
"serial_no": serial_no,
|
"serial_no": serial_no,
|
||||||
"qty": qty,
|
"qty": qty,
|
||||||
"batch_no": serial_no_wise_batch.get(serial_no) or self.get("batch_no"),
|
"batch_no": serial_no_wise_batch.get(serial_no) or self.get("batch_no"),
|
||||||
"incoming_rate": self.get("incoming_rate"),
|
"incoming_rate": incoming_rate,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
elif self.get("batches"):
|
elif self.get("batches"):
|
||||||
for batch_no, batch_qty in self.batches.items():
|
for batch_no, batch_qty in self.batches.items():
|
||||||
|
if self.get("batches_valuation"):
|
||||||
|
incoming_rate = self.get("batches_valuation").get(batch_no)
|
||||||
|
|
||||||
doc.append(
|
doc.append(
|
||||||
"entries",
|
"entries",
|
||||||
{
|
{
|
||||||
"batch_no": batch_no,
|
"batch_no": batch_no,
|
||||||
"qty": batch_qty * (-1 if self.type_of_transaction == "Outward" else 1),
|
"qty": batch_qty * (-1 if self.type_of_transaction == "Outward" else 1),
|
||||||
"incoming_rate": self.get("incoming_rate"),
|
"incoming_rate": incoming_rate,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
def create_batch(self):
|
def create_batch(self):
|
||||||
from erpnext.stock.doctype.batch.batch import make_batch
|
from erpnext.stock.doctype.batch.batch import make_batch
|
||||||
|
|
||||||
|
if self.is_rejected:
|
||||||
|
bundle = frappe.db.get_value(
|
||||||
|
"Serial and Batch Bundle",
|
||||||
|
{
|
||||||
|
"voucher_no": self.voucher_no,
|
||||||
|
"voucher_type": self.voucher_type,
|
||||||
|
"voucher_detail_no": self.voucher_detail_no,
|
||||||
|
"is_rejected": 0,
|
||||||
|
"docstatus": 1,
|
||||||
|
"is_cancelled": 0,
|
||||||
|
},
|
||||||
|
"name",
|
||||||
|
)
|
||||||
|
|
||||||
|
if bundle:
|
||||||
|
if batch_no := frappe.db.get_value("Serial and Batch Entry", {"parent": bundle}, "batch_no"):
|
||||||
|
return batch_no
|
||||||
|
|
||||||
return make_batch(
|
return make_batch(
|
||||||
frappe._dict(
|
frappe._dict(
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user