fix: use serial/batch fields for subcontracting receipt and in transit stock entry
(cherry picked from commit 3423334e4f)
This commit is contained in:
committed by
Mergify
parent
bfa316ad09
commit
9ec24bcc6c
@@ -380,6 +380,12 @@ class AccountsController(TransactionBase):
|
|||||||
for bundle in bundles:
|
for bundle in bundles:
|
||||||
frappe.delete_doc("Serial and Batch Bundle", bundle.name)
|
frappe.delete_doc("Serial and Batch Bundle", bundle.name)
|
||||||
|
|
||||||
|
batches = frappe.get_all(
|
||||||
|
"Batch", filters={"reference_doctype": self.doctype, "reference_name": self.name}
|
||||||
|
)
|
||||||
|
for row in batches:
|
||||||
|
frappe.delete_doc("Batch", row.name)
|
||||||
|
|
||||||
def validate_return_against_account(self):
|
def validate_return_against_account(self):
|
||||||
if (
|
if (
|
||||||
self.doctype in ["Sales Invoice", "Purchase Invoice"] and self.is_return and self.return_against
|
self.doctype in ["Sales Invoice", "Purchase Invoice"] and self.is_return and self.return_against
|
||||||
|
|||||||
@@ -48,7 +48,9 @@ class StockController(AccountsController):
|
|||||||
super(StockController, self).validate()
|
super(StockController, self).validate()
|
||||||
|
|
||||||
if self.docstatus == 0:
|
if self.docstatus == 0:
|
||||||
self.validate_duplicate_serial_and_batch_bundle()
|
for table_name in ["items", "packed_items", "supplied_items"]:
|
||||||
|
self.validate_duplicate_serial_and_batch_bundle(table_name)
|
||||||
|
|
||||||
if not self.get("is_return"):
|
if not self.get("is_return"):
|
||||||
self.validate_inspection()
|
self.validate_inspection()
|
||||||
self.validate_serialized_batch()
|
self.validate_serialized_batch()
|
||||||
@@ -58,12 +60,19 @@ class StockController(AccountsController):
|
|||||||
self.validate_internal_transfer()
|
self.validate_internal_transfer()
|
||||||
self.validate_putaway_capacity()
|
self.validate_putaway_capacity()
|
||||||
|
|
||||||
def validate_duplicate_serial_and_batch_bundle(self):
|
def validate_duplicate_serial_and_batch_bundle(self, table_name):
|
||||||
if sbb_list := [
|
if not self.get(table_name):
|
||||||
item.get("serial_and_batch_bundle")
|
return
|
||||||
for item in self.items
|
|
||||||
if item.get("serial_and_batch_bundle")
|
sbb_list = []
|
||||||
]:
|
for item in self.get(table_name):
|
||||||
|
if item.get("serial_and_batch_bundle"):
|
||||||
|
sbb_list.append(item.get("serial_and_batch_bundle"))
|
||||||
|
|
||||||
|
if item.get("rejected_serial_and_batch_bundle"):
|
||||||
|
sbb_list.append(item.get("rejected_serial_and_batch_bundle"))
|
||||||
|
|
||||||
|
if sbb_list:
|
||||||
SLE = frappe.qb.DocType("Stock Ledger Entry")
|
SLE = frappe.qb.DocType("Stock Ledger Entry")
|
||||||
data = (
|
data = (
|
||||||
frappe.qb.from_(SLE)
|
frappe.qb.from_(SLE)
|
||||||
@@ -188,7 +197,7 @@ class StockController(AccountsController):
|
|||||||
not row.serial_and_batch_bundle and not row.get("rejected_serial_and_batch_bundle")
|
not row.serial_and_batch_bundle and not row.get("rejected_serial_and_batch_bundle")
|
||||||
):
|
):
|
||||||
bundle_details = {
|
bundle_details = {
|
||||||
"item_code": row.item_code,
|
"item_code": row.get("rm_item_code") or row.item_code,
|
||||||
"posting_date": self.posting_date,
|
"posting_date": self.posting_date,
|
||||||
"posting_time": self.posting_time,
|
"posting_time": self.posting_time,
|
||||||
"voucher_type": self.doctype,
|
"voucher_type": self.doctype,
|
||||||
@@ -200,7 +209,7 @@ class StockController(AccountsController):
|
|||||||
"do_not_submit": True,
|
"do_not_submit": True,
|
||||||
}
|
}
|
||||||
|
|
||||||
if row.qty:
|
if row.get("qty") or row.get("consumed_qty"):
|
||||||
self.update_bundle_details(bundle_details, table_name, row)
|
self.update_bundle_details(bundle_details, table_name, row)
|
||||||
self.create_serial_batch_bundle(bundle_details, row)
|
self.create_serial_batch_bundle(bundle_details, row)
|
||||||
|
|
||||||
@@ -219,6 +228,12 @@ class StockController(AccountsController):
|
|||||||
type_of_transaction = "Inward"
|
type_of_transaction = "Inward"
|
||||||
if not self.is_return:
|
if not self.is_return:
|
||||||
type_of_transaction = "Outward"
|
type_of_transaction = "Outward"
|
||||||
|
elif table_name == "supplied_items":
|
||||||
|
qty = row.consumed_qty
|
||||||
|
warehouse = self.supplier_warehouse
|
||||||
|
type_of_transaction = "Outward"
|
||||||
|
if self.is_return:
|
||||||
|
type_of_transaction = "Inward"
|
||||||
else:
|
else:
|
||||||
type_of_transaction = get_type_of_transaction(self, row)
|
type_of_transaction = get_type_of_transaction(self, row)
|
||||||
|
|
||||||
@@ -550,13 +565,30 @@ class StockController(AccountsController):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def delete_auto_created_batches(self):
|
def delete_auto_created_batches(self):
|
||||||
for row in self.items:
|
for table_name in ["items", "packed_items", "supplied_items"]:
|
||||||
|
if not self.get(table_name):
|
||||||
|
continue
|
||||||
|
|
||||||
|
for row in self.get(table_name):
|
||||||
|
update_values = {}
|
||||||
|
if row.get("batch_no"):
|
||||||
|
update_values["batch_no"] = None
|
||||||
|
|
||||||
if row.serial_and_batch_bundle:
|
if row.serial_and_batch_bundle:
|
||||||
|
update_values["serial_and_batch_bundle"] = None
|
||||||
frappe.db.set_value(
|
frappe.db.set_value(
|
||||||
"Serial and Batch Bundle", row.serial_and_batch_bundle, {"is_cancelled": 1}
|
"Serial and Batch Bundle", row.serial_and_batch_bundle, {"is_cancelled": 1}
|
||||||
)
|
)
|
||||||
|
|
||||||
row.db_set("serial_and_batch_bundle", None)
|
if update_values:
|
||||||
|
row.db_set(update_values)
|
||||||
|
|
||||||
|
if table_name == "items" and row.get("rejected_serial_and_batch_bundle"):
|
||||||
|
frappe.db.set_value(
|
||||||
|
"Serial and Batch Bundle", row.rejected_serial_and_batch_bundle, {"is_cancelled": 1}
|
||||||
|
)
|
||||||
|
|
||||||
|
row.db_set("rejected_serial_and_batch_bundle", None)
|
||||||
|
|
||||||
def set_serial_and_batch_bundle(self, table_name=None, ignore_validate=False):
|
def set_serial_and_batch_bundle(self, table_name=None, ignore_validate=False):
|
||||||
if not table_name:
|
if not table_name:
|
||||||
|
|||||||
@@ -379,10 +379,10 @@ class SubcontractingController(StockController):
|
|||||||
if row.serial_no:
|
if row.serial_no:
|
||||||
details.serial_no.extend(get_serial_nos(row.serial_no))
|
details.serial_no.extend(get_serial_nos(row.serial_no))
|
||||||
|
|
||||||
if row.batch_no:
|
elif row.batch_no:
|
||||||
details.batch_no[row.batch_no] += row.qty
|
details.batch_no[row.batch_no] += row.qty
|
||||||
|
|
||||||
if voucher_bundle_data:
|
elif voucher_bundle_data:
|
||||||
bundle_key = (row.rm_item_code, row.main_item_code, row.t_warehouse, row.voucher_no)
|
bundle_key = (row.rm_item_code, row.main_item_code, row.t_warehouse, row.voucher_no)
|
||||||
|
|
||||||
bundle_data = voucher_bundle_data.get(bundle_key, frappe._dict())
|
bundle_data = voucher_bundle_data.get(bundle_key, frappe._dict())
|
||||||
@@ -392,6 +392,9 @@ class SubcontractingController(StockController):
|
|||||||
|
|
||||||
if bundle_data.batch_nos:
|
if bundle_data.batch_nos:
|
||||||
for batch_no, qty in bundle_data.batch_nos.items():
|
for batch_no, qty in bundle_data.batch_nos.items():
|
||||||
|
if qty < 0:
|
||||||
|
qty = abs(qty)
|
||||||
|
|
||||||
if qty > 0:
|
if qty > 0:
|
||||||
details.batch_no[batch_no] += qty
|
details.batch_no[batch_no] += qty
|
||||||
bundle_data.batch_nos[batch_no] -= qty
|
bundle_data.batch_nos[batch_no] -= qty
|
||||||
@@ -545,17 +548,24 @@ class SubcontractingController(StockController):
|
|||||||
|
|
||||||
rm_obj.reference_name = item_row.name
|
rm_obj.reference_name = item_row.name
|
||||||
|
|
||||||
|
use_serial_batch_fields = frappe.db.get_single_value("Stock Settings", "use_serial_batch_fields")
|
||||||
|
|
||||||
if self.doctype == self.subcontract_data.order_doctype:
|
if self.doctype == self.subcontract_data.order_doctype:
|
||||||
rm_obj.required_qty = qty
|
rm_obj.required_qty = qty
|
||||||
rm_obj.amount = rm_obj.required_qty * rm_obj.rate
|
rm_obj.amount = rm_obj.required_qty * rm_obj.rate
|
||||||
else:
|
else:
|
||||||
rm_obj.consumed_qty = qty
|
rm_obj.consumed_qty = qty
|
||||||
rm_obj.required_qty = bom_item.required_qty or qty
|
rm_obj.required_qty = bom_item.required_qty or qty
|
||||||
|
rm_obj.serial_and_batch_bundle = None
|
||||||
setattr(
|
setattr(
|
||||||
rm_obj, self.subcontract_data.order_field, item_row.get(self.subcontract_data.order_field)
|
rm_obj, self.subcontract_data.order_field, item_row.get(self.subcontract_data.order_field)
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.doctype == "Subcontracting Receipt":
|
if use_serial_batch_fields:
|
||||||
|
rm_obj.use_serial_batch_fields = 1
|
||||||
|
self.__set_batch_nos(bom_item, item_row, rm_obj, qty)
|
||||||
|
|
||||||
|
if self.doctype == "Subcontracting Receipt" and not use_serial_batch_fields:
|
||||||
args = frappe._dict(
|
args = frappe._dict(
|
||||||
{
|
{
|
||||||
"item_code": rm_obj.rm_item_code,
|
"item_code": rm_obj.rm_item_code,
|
||||||
@@ -581,6 +591,68 @@ class SubcontractingController(StockController):
|
|||||||
|
|
||||||
rm_obj.rate = get_incoming_rate(args)
|
rm_obj.rate = get_incoming_rate(args)
|
||||||
|
|
||||||
|
def __set_batch_nos(self, bom_item, item_row, rm_obj, qty):
|
||||||
|
key = (rm_obj.rm_item_code, item_row.item_code, item_row.get(self.subcontract_data.order_field))
|
||||||
|
|
||||||
|
if self.available_materials.get(key) and self.available_materials[key]["batch_no"]:
|
||||||
|
new_rm_obj = None
|
||||||
|
for batch_no, batch_qty in self.available_materials[key]["batch_no"].items():
|
||||||
|
if batch_qty >= qty or (
|
||||||
|
rm_obj.consumed_qty == 0
|
||||||
|
and self.backflush_based_on == "BOM"
|
||||||
|
and len(self.available_materials[key]["batch_no"]) == 1
|
||||||
|
):
|
||||||
|
if rm_obj.consumed_qty == 0:
|
||||||
|
self.__set_consumed_qty(rm_obj, qty)
|
||||||
|
|
||||||
|
self.__set_batch_no_as_per_qty(item_row, rm_obj, batch_no, qty)
|
||||||
|
self.available_materials[key]["batch_no"][batch_no] -= qty
|
||||||
|
return
|
||||||
|
|
||||||
|
elif qty > 0 and batch_qty > 0:
|
||||||
|
qty -= batch_qty
|
||||||
|
new_rm_obj = self.append(self.raw_material_table, bom_item)
|
||||||
|
new_rm_obj.serial_and_batch_bundle = None
|
||||||
|
new_rm_obj.use_serial_batch_fields = 1
|
||||||
|
new_rm_obj.reference_name = item_row.name
|
||||||
|
self.__set_batch_no_as_per_qty(item_row, new_rm_obj, batch_no, batch_qty)
|
||||||
|
self.available_materials[key]["batch_no"][batch_no] = 0
|
||||||
|
|
||||||
|
if new_rm_obj:
|
||||||
|
self.remove(rm_obj)
|
||||||
|
elif abs(qty) > 0:
|
||||||
|
self.__set_consumed_qty(rm_obj, qty)
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.__set_consumed_qty(rm_obj, qty, bom_item.required_qty or qty)
|
||||||
|
self.__set_serial_nos(item_row, rm_obj)
|
||||||
|
|
||||||
|
def __set_consumed_qty(self, rm_obj, consumed_qty, required_qty=0):
|
||||||
|
rm_obj.required_qty = required_qty
|
||||||
|
rm_obj.consumed_qty = consumed_qty
|
||||||
|
|
||||||
|
def __set_serial_nos(self, item_row, rm_obj):
|
||||||
|
key = (rm_obj.rm_item_code, item_row.item_code, item_row.get(self.subcontract_data.order_field))
|
||||||
|
if self.available_materials.get(key) and self.available_materials[key]["serial_no"]:
|
||||||
|
used_serial_nos = self.available_materials[key]["serial_no"][0 : cint(rm_obj.consumed_qty)]
|
||||||
|
rm_obj.serial_no = "\n".join(used_serial_nos)
|
||||||
|
|
||||||
|
# Removed the used serial nos from the list
|
||||||
|
for sn in used_serial_nos:
|
||||||
|
self.available_materials[key]["serial_no"].remove(sn)
|
||||||
|
|
||||||
|
def __set_batch_no_as_per_qty(self, item_row, rm_obj, batch_no, qty):
|
||||||
|
rm_obj.update(
|
||||||
|
{
|
||||||
|
"consumed_qty": qty,
|
||||||
|
"batch_no": batch_no,
|
||||||
|
"required_qty": qty,
|
||||||
|
self.subcontract_data.order_field: item_row.get(self.subcontract_data.order_field),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.__set_serial_nos(item_row, rm_obj)
|
||||||
|
|
||||||
def __get_qty_based_on_material_transfer(self, item_row, transfer_item):
|
def __get_qty_based_on_material_transfer(self, item_row, transfer_item):
|
||||||
key = (item_row.item_code, item_row.get(self.subcontract_data.order_field))
|
key = (item_row.item_code, item_row.get(self.subcontract_data.order_field))
|
||||||
|
|
||||||
@@ -1076,6 +1148,9 @@ def make_rm_stock_entry(
|
|||||||
"serial_and_batch_bundle": rm_item.get("serial_and_batch_bundle"),
|
"serial_and_batch_bundle": rm_item.get("serial_and_batch_bundle"),
|
||||||
"main_item_code": fg_item_code,
|
"main_item_code": fg_item_code,
|
||||||
"allow_alternative_item": item_wh.get(rm_item_code, {}).get("allow_alternative_item"),
|
"allow_alternative_item": item_wh.get(rm_item_code, {}).get("allow_alternative_item"),
|
||||||
|
"use_serial_batch_fields": rm_item.get("use_serial_batch_fields"),
|
||||||
|
"serial_no": rm_item.get("serial_no") if rm_item.get("use_serial_batch_fields") else None,
|
||||||
|
"batch_no": rm_item.get("batch_no") if rm_item.get("use_serial_batch_fields") else None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ class TestSubcontractingController(FrappeTestCase):
|
|||||||
- Create partial SCR against the SCO and check serial nos and batch no.
|
- Create partial SCR against the SCO and check serial nos and batch no.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 0)
|
||||||
set_backflush_based_on("Material Transferred for Subcontract")
|
set_backflush_based_on("Material Transferred for Subcontract")
|
||||||
service_items = [
|
service_items = [
|
||||||
{
|
{
|
||||||
@@ -202,6 +203,8 @@ class TestSubcontractingController(FrappeTestCase):
|
|||||||
if value.get(field):
|
if value.get(field):
|
||||||
self.assertEqual(value.get(field), transferred_detais.get(field))
|
self.assertEqual(value.get(field), transferred_detais.get(field))
|
||||||
|
|
||||||
|
frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 1)
|
||||||
|
|
||||||
def test_subcontracting_with_same_components_different_fg(self):
|
def test_subcontracting_with_same_components_different_fg(self):
|
||||||
"""
|
"""
|
||||||
- Set backflush based on Material Transfer.
|
- Set backflush based on Material Transfer.
|
||||||
@@ -211,6 +214,7 @@ class TestSubcontractingController(FrappeTestCase):
|
|||||||
- Create partial SCR against the SCO and check serial nos.
|
- Create partial SCR against the SCO and check serial nos.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 0)
|
||||||
set_backflush_based_on("Material Transferred for Subcontract")
|
set_backflush_based_on("Material Transferred for Subcontract")
|
||||||
service_items = [
|
service_items = [
|
||||||
{
|
{
|
||||||
@@ -278,6 +282,8 @@ class TestSubcontractingController(FrappeTestCase):
|
|||||||
self.assertEqual(value.qty, 6)
|
self.assertEqual(value.qty, 6)
|
||||||
self.assertEqual(sorted(value.serial_no), sorted(transferred_detais.get("serial_no")[6:12]))
|
self.assertEqual(sorted(value.serial_no), sorted(transferred_detais.get("serial_no")[6:12]))
|
||||||
|
|
||||||
|
frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 1)
|
||||||
|
|
||||||
def test_return_non_consumed_materials(self):
|
def test_return_non_consumed_materials(self):
|
||||||
"""
|
"""
|
||||||
- Set backflush based on Material Transfer.
|
- Set backflush based on Material Transfer.
|
||||||
@@ -288,6 +294,7 @@ class TestSubcontractingController(FrappeTestCase):
|
|||||||
- After that return the non consumed material back to the store from supplier's warehouse.
|
- After that return the non consumed material back to the store from supplier's warehouse.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 0)
|
||||||
set_backflush_based_on("Material Transferred for Subcontract")
|
set_backflush_based_on("Material Transferred for Subcontract")
|
||||||
service_items = [
|
service_items = [
|
||||||
{
|
{
|
||||||
@@ -333,6 +340,7 @@ class TestSubcontractingController(FrappeTestCase):
|
|||||||
get_serial_nos(doc.items[0].serial_no),
|
get_serial_nos(doc.items[0].serial_no),
|
||||||
itemwise_details.get(doc.items[0].item_code)["serial_no"][5:6],
|
itemwise_details.get(doc.items[0].item_code)["serial_no"][5:6],
|
||||||
)
|
)
|
||||||
|
frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 1)
|
||||||
|
|
||||||
def test_item_with_batch_based_on_bom(self):
|
def test_item_with_batch_based_on_bom(self):
|
||||||
"""
|
"""
|
||||||
@@ -578,6 +586,7 @@ class TestSubcontractingController(FrappeTestCase):
|
|||||||
- Create SCR for remaining qty against the SCO and change the qty manually.
|
- Create SCR for remaining qty against the SCO and change the qty manually.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 0)
|
||||||
set_backflush_based_on("Material Transferred for Subcontract")
|
set_backflush_based_on("Material Transferred for Subcontract")
|
||||||
service_items = [
|
service_items = [
|
||||||
{
|
{
|
||||||
@@ -643,6 +652,8 @@ class TestSubcontractingController(FrappeTestCase):
|
|||||||
self.assertEqual(value.qty, details.qty)
|
self.assertEqual(value.qty, details.qty)
|
||||||
self.assertEqual(sorted(value.serial_no), sorted(details.serial_no))
|
self.assertEqual(sorted(value.serial_no), sorted(details.serial_no))
|
||||||
|
|
||||||
|
frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 1)
|
||||||
|
|
||||||
def test_incorrect_serial_no_components_based_on_material_transfer(self):
|
def test_incorrect_serial_no_components_based_on_material_transfer(self):
|
||||||
"""
|
"""
|
||||||
- Set backflush based on Material Transferred for Subcontract.
|
- Set backflush based on Material Transferred for Subcontract.
|
||||||
@@ -652,6 +663,7 @@ class TestSubcontractingController(FrappeTestCase):
|
|||||||
- System should throw the error and not allowed to save the SCR.
|
- System should throw the error and not allowed to save the SCR.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 0)
|
||||||
serial_no = "ABC"
|
serial_no = "ABC"
|
||||||
if not frappe.db.exists("Serial No", serial_no):
|
if not frappe.db.exists("Serial No", serial_no):
|
||||||
frappe.get_doc(
|
frappe.get_doc(
|
||||||
@@ -712,6 +724,7 @@ class TestSubcontractingController(FrappeTestCase):
|
|||||||
scr1.save()
|
scr1.save()
|
||||||
self.delete_bundle_from_scr(scr1)
|
self.delete_bundle_from_scr(scr1)
|
||||||
scr1.delete()
|
scr1.delete()
|
||||||
|
frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 1)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def delete_bundle_from_scr(scr):
|
def delete_bundle_from_scr(scr):
|
||||||
@@ -844,6 +857,223 @@ class TestSubcontractingController(FrappeTestCase):
|
|||||||
for item in sco.get("supplied_items"):
|
for item in sco.get("supplied_items"):
|
||||||
self.assertEqual(item.supplied_qty, 0.0)
|
self.assertEqual(item.supplied_qty, 0.0)
|
||||||
|
|
||||||
|
def test_sco_with_material_transfer_with_use_serial_batch_fields(self):
|
||||||
|
"""
|
||||||
|
- Set backflush based on Material Transfer.
|
||||||
|
- Create SCO for the item Subcontracted Item SA1 and Subcontracted Item SA5.
|
||||||
|
- Transfer the components from Stores to Supplier warehouse with batch no and serial nos.
|
||||||
|
- Transfer extra item Subcontracted SRM Item 4 for the subcontract item Subcontracted Item SA5.
|
||||||
|
- Create partial SCR against the SCO and check serial nos and batch no.
|
||||||
|
"""
|
||||||
|
|
||||||
|
set_backflush_based_on("Material Transferred for Subcontract")
|
||||||
|
service_items = [
|
||||||
|
{
|
||||||
|
"warehouse": "_Test Warehouse - _TC",
|
||||||
|
"item_code": "Subcontracted Service Item 1",
|
||||||
|
"qty": 5,
|
||||||
|
"rate": 100,
|
||||||
|
"fg_item": "Subcontracted Item SA1",
|
||||||
|
"fg_item_qty": 5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"warehouse": "_Test Warehouse - _TC",
|
||||||
|
"item_code": "Subcontracted Service Item 5",
|
||||||
|
"qty": 6,
|
||||||
|
"rate": 100,
|
||||||
|
"fg_item": "Subcontracted Item SA5",
|
||||||
|
"fg_item_qty": 6,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
sco = get_subcontracting_order(service_items=service_items)
|
||||||
|
rm_items = get_rm_items(sco.supplied_items)
|
||||||
|
rm_items.append(
|
||||||
|
{
|
||||||
|
"main_item_code": "Subcontracted Item SA5",
|
||||||
|
"item_code": "Subcontracted SRM Item 4",
|
||||||
|
"qty": 6,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
itemwise_details = make_stock_in_entry(rm_items=rm_items)
|
||||||
|
|
||||||
|
for item in rm_items:
|
||||||
|
item["sco_rm_detail"] = sco.items[0].name if item.get("qty") == 5 else sco.items[1].name
|
||||||
|
|
||||||
|
make_stock_transfer_entry(
|
||||||
|
sco_no=sco.name,
|
||||||
|
rm_items=rm_items,
|
||||||
|
itemwise_details=copy.deepcopy(itemwise_details),
|
||||||
|
)
|
||||||
|
|
||||||
|
scr1 = make_subcontracting_receipt(sco.name)
|
||||||
|
scr1.remove(scr1.items[1])
|
||||||
|
scr1.save()
|
||||||
|
scr1.submit()
|
||||||
|
|
||||||
|
for key, value in get_supplied_items(scr1).items():
|
||||||
|
transferred_detais = itemwise_details.get(key)
|
||||||
|
|
||||||
|
for field in ["qty", "serial_no", "batch_no"]:
|
||||||
|
if value.get(field):
|
||||||
|
data = value.get(field)
|
||||||
|
if field == "serial_no":
|
||||||
|
data = sorted(data)
|
||||||
|
|
||||||
|
self.assertEqual(data, transferred_detais.get(field))
|
||||||
|
|
||||||
|
scr2 = make_subcontracting_receipt(sco.name)
|
||||||
|
scr2.save()
|
||||||
|
scr2.submit()
|
||||||
|
|
||||||
|
for key, value in get_supplied_items(scr2).items():
|
||||||
|
transferred_detais = itemwise_details.get(key)
|
||||||
|
|
||||||
|
for field in ["qty", "serial_no", "batch_no"]:
|
||||||
|
if value.get(field):
|
||||||
|
data = value.get(field)
|
||||||
|
if field == "serial_no":
|
||||||
|
data = sorted(data)
|
||||||
|
|
||||||
|
self.assertEqual(data, transferred_detais.get(field))
|
||||||
|
|
||||||
|
def test_subcontracting_with_same_components_different_fg_with_serial_batch_fields(self):
|
||||||
|
"""
|
||||||
|
- Set backflush based on Material Transfer.
|
||||||
|
- Create SCO for the item Subcontracted Item SA2 and Subcontracted Item SA3.
|
||||||
|
- Transfer the components from Stores to Supplier warehouse with serial nos.
|
||||||
|
- Transfer extra qty of components for the item Subcontracted Item SA2.
|
||||||
|
- Create partial SCR against the SCO and check serial nos.
|
||||||
|
"""
|
||||||
|
|
||||||
|
set_backflush_based_on("Material Transferred for Subcontract")
|
||||||
|
service_items = [
|
||||||
|
{
|
||||||
|
"warehouse": "_Test Warehouse - _TC",
|
||||||
|
"item_code": "Subcontracted Service Item 2",
|
||||||
|
"qty": 5,
|
||||||
|
"rate": 100,
|
||||||
|
"fg_item": "Subcontracted Item SA2",
|
||||||
|
"fg_item_qty": 5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"warehouse": "_Test Warehouse - _TC",
|
||||||
|
"item_code": "Subcontracted Service Item 3",
|
||||||
|
"qty": 6,
|
||||||
|
"rate": 100,
|
||||||
|
"fg_item": "Subcontracted Item SA3",
|
||||||
|
"fg_item_qty": 6,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
sco = get_subcontracting_order(service_items=service_items)
|
||||||
|
rm_items = get_rm_items(sco.supplied_items)
|
||||||
|
rm_items[0]["qty"] += 1
|
||||||
|
itemwise_details = make_stock_in_entry(rm_items=rm_items)
|
||||||
|
|
||||||
|
for item in rm_items:
|
||||||
|
item["sco_rm_detail"] = sco.items[0].name if item.get("qty") == 5 else sco.items[1].name
|
||||||
|
item["use_serial_batch_fields"] = 1
|
||||||
|
|
||||||
|
make_stock_transfer_entry(
|
||||||
|
sco_no=sco.name,
|
||||||
|
rm_items=rm_items,
|
||||||
|
itemwise_details=copy.deepcopy(itemwise_details),
|
||||||
|
)
|
||||||
|
|
||||||
|
scr1 = make_subcontracting_receipt(sco.name)
|
||||||
|
scr1.items[0].qty = 3
|
||||||
|
scr1.remove(scr1.items[1])
|
||||||
|
scr1.save()
|
||||||
|
scr1.submit()
|
||||||
|
|
||||||
|
for key, value in get_supplied_items(scr1).items():
|
||||||
|
transferred_detais = itemwise_details.get(key)
|
||||||
|
|
||||||
|
self.assertEqual(value.qty, 4)
|
||||||
|
self.assertEqual(sorted(value.serial_no), sorted(transferred_detais.get("serial_no")[0:4]))
|
||||||
|
|
||||||
|
scr2 = make_subcontracting_receipt(sco.name)
|
||||||
|
scr2.items[0].qty = 2
|
||||||
|
scr2.remove(scr2.items[1])
|
||||||
|
scr2.save()
|
||||||
|
scr2.submit()
|
||||||
|
|
||||||
|
for key, value in get_supplied_items(scr2).items():
|
||||||
|
transferred_detais = itemwise_details.get(key)
|
||||||
|
|
||||||
|
self.assertEqual(value.qty, 2)
|
||||||
|
self.assertEqual(sorted(value.serial_no), sorted(transferred_detais.get("serial_no")[4:6]))
|
||||||
|
|
||||||
|
scr3 = make_subcontracting_receipt(sco.name)
|
||||||
|
scr3.save()
|
||||||
|
scr3.submit()
|
||||||
|
|
||||||
|
for key, value in get_supplied_items(scr3).items():
|
||||||
|
transferred_detais = itemwise_details.get(key)
|
||||||
|
|
||||||
|
self.assertEqual(value.qty, 6)
|
||||||
|
self.assertEqual(sorted(value.serial_no), sorted(transferred_detais.get("serial_no")[6:12]))
|
||||||
|
|
||||||
|
def test_return_non_consumed_materials_with_serial_batch_fields(self):
|
||||||
|
"""
|
||||||
|
- Set backflush based on Material Transfer.
|
||||||
|
- Create SCO for item Subcontracted Item SA2.
|
||||||
|
- Transfer the components from Stores to Supplier warehouse with serial nos.
|
||||||
|
- Transfer extra qty of component for the subcontracted item Subcontracted Item SA2.
|
||||||
|
- Create SCR for full qty against the SCO and change the qty of raw material.
|
||||||
|
- After that return the non consumed material back to the store from supplier's warehouse.
|
||||||
|
"""
|
||||||
|
|
||||||
|
set_backflush_based_on("Material Transferred for Subcontract")
|
||||||
|
service_items = [
|
||||||
|
{
|
||||||
|
"warehouse": "_Test Warehouse - _TC",
|
||||||
|
"item_code": "Subcontracted Service Item 2",
|
||||||
|
"qty": 5,
|
||||||
|
"rate": 100,
|
||||||
|
"fg_item": "Subcontracted Item SA2",
|
||||||
|
"fg_item_qty": 5,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
sco = get_subcontracting_order(service_items=service_items)
|
||||||
|
rm_items = get_rm_items(sco.supplied_items)
|
||||||
|
rm_items[0]["qty"] += 1
|
||||||
|
itemwise_details = make_stock_in_entry(rm_items=rm_items)
|
||||||
|
|
||||||
|
for item in rm_items:
|
||||||
|
item["use_serial_batch_fields"] = 1
|
||||||
|
item["sco_rm_detail"] = sco.items[0].name
|
||||||
|
|
||||||
|
make_stock_transfer_entry(
|
||||||
|
sco_no=sco.name,
|
||||||
|
rm_items=rm_items,
|
||||||
|
itemwise_details=copy.deepcopy(itemwise_details),
|
||||||
|
)
|
||||||
|
|
||||||
|
scr1 = make_subcontracting_receipt(sco.name)
|
||||||
|
scr1.save()
|
||||||
|
scr1.supplied_items[0].consumed_qty = 5
|
||||||
|
scr1.supplied_items[0].serial_no = "\n".join(
|
||||||
|
sorted(itemwise_details.get("Subcontracted SRM Item 2").get("serial_no")[0:5])
|
||||||
|
)
|
||||||
|
scr1.submit()
|
||||||
|
|
||||||
|
for key, value in get_supplied_items(scr1).items():
|
||||||
|
transferred_detais = itemwise_details.get(key)
|
||||||
|
self.assertTrue(value.use_serial_batch_fields)
|
||||||
|
self.assertEqual(value.qty, 5)
|
||||||
|
self.assertEqual(sorted(value.serial_no), sorted(transferred_detais.get("serial_no")[0:5]))
|
||||||
|
|
||||||
|
sco.load_from_db()
|
||||||
|
self.assertEqual(sco.supplied_items[0].consumed_qty, 5)
|
||||||
|
doc = get_materials_from_supplier(sco.name, [d.name for d in sco.supplied_items])
|
||||||
|
self.assertEqual(doc.items[0].qty, 1)
|
||||||
|
self.assertEqual(doc.items[0].s_warehouse, "_Test Warehouse 1 - _TC")
|
||||||
|
self.assertEqual(doc.items[0].t_warehouse, "_Test Warehouse - _TC")
|
||||||
|
self.assertEqual(
|
||||||
|
get_serial_nos(doc.items[0].serial_no),
|
||||||
|
itemwise_details.get(doc.items[0].item_code)["serial_no"][5:6],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def add_second_row_in_scr(scr):
|
def add_second_row_in_scr(scr):
|
||||||
item_dict = {}
|
item_dict = {}
|
||||||
@@ -914,6 +1144,7 @@ def update_item_details(child_row, details):
|
|||||||
else child_row.get("consumed_qty")
|
else child_row.get("consumed_qty")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
details.use_serial_batch_fields = child_row.get("use_serial_batch_fields")
|
||||||
if child_row.serial_and_batch_bundle:
|
if child_row.serial_and_batch_bundle:
|
||||||
doc = frappe.get_doc("Serial and Batch Bundle", child_row.serial_and_batch_bundle)
|
doc = frappe.get_doc("Serial and Batch Bundle", child_row.serial_and_batch_bundle)
|
||||||
for row in doc.get("entries"):
|
for row in doc.get("entries"):
|
||||||
@@ -945,6 +1176,7 @@ def make_stock_transfer_entry(**args):
|
|||||||
"rate": row.rate or 100,
|
"rate": row.rate or 100,
|
||||||
"stock_uom": row.stock_uom or "Nos",
|
"stock_uom": row.stock_uom or "Nos",
|
||||||
"warehouse": row.warehouse or "_Test Warehouse - _TC",
|
"warehouse": row.warehouse or "_Test Warehouse - _TC",
|
||||||
|
"use_serial_batch_fields": row.get("use_serial_batch_fields"),
|
||||||
}
|
}
|
||||||
|
|
||||||
item_details = args.itemwise_details.get(row.item_code)
|
item_details = args.itemwise_details.get(row.item_code)
|
||||||
@@ -960,9 +1192,12 @@ def make_stock_transfer_entry(**args):
|
|||||||
if batch_qty >= row.qty:
|
if batch_qty >= row.qty:
|
||||||
batches[batch_no] = row.qty
|
batches[batch_no] = row.qty
|
||||||
item_details.batch_no[batch_no] -= row.qty
|
item_details.batch_no[batch_no] -= row.qty
|
||||||
|
if row.get("use_serial_batch_fields"):
|
||||||
|
item["batch_no"] = batch_no
|
||||||
|
|
||||||
break
|
break
|
||||||
|
|
||||||
if serial_nos or batches:
|
if not row.get("use_serial_batch_fields") and (serial_nos or batches):
|
||||||
item["serial_and_batch_bundle"] = make_serial_batch_bundle(
|
item["serial_and_batch_bundle"] = make_serial_batch_bundle(
|
||||||
frappe._dict(
|
frappe._dict(
|
||||||
{
|
{
|
||||||
@@ -978,6 +1213,9 @@ def make_stock_transfer_entry(**args):
|
|||||||
)
|
)
|
||||||
).name
|
).name
|
||||||
|
|
||||||
|
if serial_nos and row.get("use_serial_batch_fields"):
|
||||||
|
item["serial_no"] = "\n".join(serial_nos)
|
||||||
|
|
||||||
items.append(item)
|
items.append(item)
|
||||||
|
|
||||||
ste_dict = make_rm_stock_entry(args.sco_no, items)
|
ste_dict = make_rm_stock_entry(args.sco_no, items)
|
||||||
@@ -1132,6 +1370,7 @@ def get_rm_items(supplied_items):
|
|||||||
"rate": item.rate,
|
"rate": item.rate,
|
||||||
"stock_uom": item.stock_uom,
|
"stock_uom": item.stock_uom,
|
||||||
"warehouse": item.reserve_warehouse,
|
"warehouse": item.reserve_warehouse,
|
||||||
|
"use_serial_batch_fields": 0,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -246,6 +246,8 @@ class DeprecatedBatchNoValuation:
|
|||||||
if self.sle.serial_and_batch_bundle:
|
if self.sle.serial_and_batch_bundle:
|
||||||
query = query.where(bundle.name != self.sle.serial_and_batch_bundle)
|
query = query.where(bundle.name != self.sle.serial_and_batch_bundle)
|
||||||
|
|
||||||
|
query = query.where(bundle.voucher_type != "Pick List")
|
||||||
|
|
||||||
for d in query.run(as_dict=True):
|
for d in query.run(as_dict=True):
|
||||||
self.non_batchwise_balance_value += flt(d.batch_value)
|
self.non_batchwise_balance_value += flt(d.batch_value)
|
||||||
self.non_batchwise_balance_qty += flt(d.batch_qty)
|
self.non_batchwise_balance_qty += flt(d.batch_qty)
|
||||||
|
|||||||
@@ -2635,6 +2635,8 @@ def make_stock_in_entry(source_name, target_doc=None):
|
|||||||
def set_missing_values(source, target):
|
def set_missing_values(source, target):
|
||||||
target.stock_entry_type = "Material Transfer"
|
target.stock_entry_type = "Material Transfer"
|
||||||
target.set_missing_values()
|
target.set_missing_values()
|
||||||
|
|
||||||
|
if not frappe.db.get_single_value("Stock Settings", "use_serial_batch_fields"):
|
||||||
target.make_serial_and_batch_bundle_for_transfer()
|
target.make_serial_and_batch_bundle_for_transfer()
|
||||||
|
|
||||||
def update_item(source_doc, target_doc, source_parent):
|
def update_item(source_doc, target_doc, source_parent):
|
||||||
|
|||||||
@@ -149,7 +149,9 @@ class SubcontractingReceipt(SubcontractingController):
|
|||||||
self.update_prevdoc_status()
|
self.update_prevdoc_status()
|
||||||
self.set_subcontracting_order_status()
|
self.set_subcontracting_order_status()
|
||||||
self.set_consumed_qty_in_subcontract_order()
|
self.set_consumed_qty_in_subcontract_order()
|
||||||
self.make_bundle_using_old_serial_batch_fields()
|
|
||||||
|
for table_name in ["items", "supplied_items"]:
|
||||||
|
self.make_bundle_using_old_serial_batch_fields(table_name)
|
||||||
self.update_stock_ledger()
|
self.update_stock_ledger()
|
||||||
self.make_gl_entries()
|
self.make_gl_entries()
|
||||||
self.repost_future_sle_and_gle()
|
self.repost_future_sle_and_gle()
|
||||||
|
|||||||
@@ -292,6 +292,7 @@ class TestSubcontractingReceipt(FrappeTestCase):
|
|||||||
self.assertRaises(OverAllowanceError, make_return_subcontracting_receipt, **args)
|
self.assertRaises(OverAllowanceError, make_return_subcontracting_receipt, **args)
|
||||||
|
|
||||||
def test_subcontracting_receipt_no_gl_entry(self):
|
def test_subcontracting_receipt_no_gl_entry(self):
|
||||||
|
frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 0)
|
||||||
sco = get_subcontracting_order()
|
sco = get_subcontracting_order()
|
||||||
rm_items = get_rm_items(sco.supplied_items)
|
rm_items = get_rm_items(sco.supplied_items)
|
||||||
itemwise_details = make_stock_in_entry(rm_items=rm_items)
|
itemwise_details = make_stock_in_entry(rm_items=rm_items)
|
||||||
@@ -327,8 +328,10 @@ class TestSubcontractingReceipt(FrappeTestCase):
|
|||||||
# Service Cost(100 * 10) + Raw Materials Cost(100 * 10) + Additional Costs(10 * 10) = 2100
|
# Service Cost(100 * 10) + Raw Materials Cost(100 * 10) + Additional Costs(10 * 10) = 2100
|
||||||
self.assertEqual(stock_value_difference, 2100)
|
self.assertEqual(stock_value_difference, 2100)
|
||||||
self.assertFalse(get_gl_entries("Subcontracting Receipt", scr.name))
|
self.assertFalse(get_gl_entries("Subcontracting Receipt", scr.name))
|
||||||
|
frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 1)
|
||||||
|
|
||||||
def test_subcontracting_receipt_gl_entry(self):
|
def test_subcontracting_receipt_gl_entry(self):
|
||||||
|
frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 0)
|
||||||
sco = get_subcontracting_order(
|
sco = get_subcontracting_order(
|
||||||
company="_Test Company with perpetual inventory",
|
company="_Test Company with perpetual inventory",
|
||||||
warehouse="Stores - TCP1",
|
warehouse="Stores - TCP1",
|
||||||
@@ -387,6 +390,7 @@ class TestSubcontractingReceipt(FrappeTestCase):
|
|||||||
scr.reload()
|
scr.reload()
|
||||||
scr.cancel()
|
scr.cancel()
|
||||||
self.assertTrue(get_gl_entries("Subcontracting Receipt", scr.name))
|
self.assertTrue(get_gl_entries("Subcontracting Receipt", scr.name))
|
||||||
|
frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 1)
|
||||||
|
|
||||||
def test_supplied_items_consumed_qty(self):
|
def test_supplied_items_consumed_qty(self):
|
||||||
# Set Backflush Based On as "Material Transferred for Subcontracting" to transfer RM's more than the required qty
|
# Set Backflush Based On as "Material Transferred for Subcontracting" to transfer RM's more than the required qty
|
||||||
@@ -664,6 +668,7 @@ class TestSubcontractingReceipt(FrappeTestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def test_subcontracting_receipt_valuation_for_fg_with_auto_created_serial_batch_bundle(self):
|
def test_subcontracting_receipt_valuation_for_fg_with_auto_created_serial_batch_bundle(self):
|
||||||
|
frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 0)
|
||||||
set_backflush_based_on("BOM")
|
set_backflush_based_on("BOM")
|
||||||
|
|
||||||
fg_item = make_item(
|
fg_item = make_item(
|
||||||
@@ -760,9 +765,11 @@ class TestSubcontractingReceipt(FrappeTestCase):
|
|||||||
frappe.db.set_single_value(
|
frappe.db.set_single_value(
|
||||||
"Stock Settings", "auto_create_serial_and_batch_bundle_for_outward", 0
|
"Stock Settings", "auto_create_serial_and_batch_bundle_for_outward", 0
|
||||||
)
|
)
|
||||||
|
frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 1)
|
||||||
|
|
||||||
def test_subcontracting_receipt_raw_material_rate(self):
|
def test_subcontracting_receipt_raw_material_rate(self):
|
||||||
# Step - 1: Set Backflush Based On as "BOM"
|
# Step - 1: Set Backflush Based On as "BOM"
|
||||||
|
frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 0)
|
||||||
set_backflush_based_on("BOM")
|
set_backflush_based_on("BOM")
|
||||||
|
|
||||||
# Step - 2: Create FG and RM Items
|
# Step - 2: Create FG and RM Items
|
||||||
@@ -820,6 +827,8 @@ class TestSubcontractingReceipt(FrappeTestCase):
|
|||||||
self.assertEqual(rm_item.rate, 100)
|
self.assertEqual(rm_item.rate, 100)
|
||||||
self.assertEqual(rm_item.amount, rm_item.consumed_qty * rm_item.rate)
|
self.assertEqual(rm_item.amount, rm_item.consumed_qty * rm_item.rate)
|
||||||
|
|
||||||
|
frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 1)
|
||||||
|
|
||||||
def test_quality_inspection_for_subcontracting_receipt(self):
|
def test_quality_inspection_for_subcontracting_receipt(self):
|
||||||
from erpnext.stock.doctype.quality_inspection.test_quality_inspection import (
|
from erpnext.stock.doctype.quality_inspection.test_quality_inspection import (
|
||||||
create_quality_inspection,
|
create_quality_inspection,
|
||||||
|
|||||||
Reference in New Issue
Block a user