mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-21 07:38:29 +00:00
Co-authored-by: Kavin <78342682+kavin0411@users.noreply.github.com> Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com> Co-authored-by: Kavin <78342682+kavin-114@users.noreply.github.com> fix: add validation for FG Items as per BOM qty (#50579)
This commit is contained in:
@@ -36,6 +36,7 @@
|
|||||||
"backflush_raw_materials_of_subcontract_based_on",
|
"backflush_raw_materials_of_subcontract_based_on",
|
||||||
"column_break_11",
|
"column_break_11",
|
||||||
"over_transfer_allowance",
|
"over_transfer_allowance",
|
||||||
|
"validate_consumed_qty",
|
||||||
"section_break_xcug",
|
"section_break_xcug",
|
||||||
"auto_create_subcontracting_order",
|
"auto_create_subcontracting_order",
|
||||||
"column_break_izrr",
|
"column_break_izrr",
|
||||||
@@ -270,6 +271,14 @@
|
|||||||
"label": "Fixed Outgoing Email Account",
|
"label": "Fixed Outgoing Email Account",
|
||||||
"link_filters": "[[\"Email Account\",\"enable_outgoing\",\"=\",1]]",
|
"link_filters": "[[\"Email Account\",\"enable_outgoing\",\"=\",1]]",
|
||||||
"options": "Email Account"
|
"options": "Email Account"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"depends_on": "eval:doc.backflush_raw_materials_of_subcontract_based_on == \"Material Transferred for Subcontract\"",
|
||||||
|
"description": "Raw materials consumed qty will be validated based on FG BOM required qty",
|
||||||
|
"fieldname": "validate_consumed_qty",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Validate Consumed Qty (as per BOM)"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"grid_page_length": 50,
|
"grid_page_length": 50,
|
||||||
@@ -278,7 +287,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-08-20 22:13:38.506889",
|
"modified": "2025-11-20 12:59:09.925862",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Buying",
|
"module": "Buying",
|
||||||
"name": "Buying Settings",
|
"name": "Buying Settings",
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ class BuyingSettings(Document):
|
|||||||
supp_master_name: DF.Literal["Supplier Name", "Naming Series", "Auto Name"]
|
supp_master_name: DF.Literal["Supplier Name", "Naming Series", "Auto Name"]
|
||||||
supplier_group: DF.Link | None
|
supplier_group: DF.Link | None
|
||||||
use_transaction_date_exchange_rate: DF.Check
|
use_transaction_date_exchange_rate: DF.Check
|
||||||
|
validate_consumed_qty: DF.Check
|
||||||
# end: auto-generated types
|
# end: auto-generated types
|
||||||
|
|
||||||
def validate(self):
|
def validate(self):
|
||||||
|
|||||||
@@ -505,7 +505,7 @@ class SubcontractingController(StockController):
|
|||||||
if item.get("serial_and_batch_bundle"):
|
if item.get("serial_and_batch_bundle"):
|
||||||
frappe.delete_doc("Serial and Batch Bundle", item.serial_and_batch_bundle, force=True)
|
frappe.delete_doc("Serial and Batch Bundle", item.serial_and_batch_bundle, force=True)
|
||||||
|
|
||||||
def __get_materials_from_bom(self, item_code, bom_no, exploded_item=0):
|
def _get_materials_from_bom(self, item_code, bom_no, exploded_item=0):
|
||||||
doctype = "BOM Item" if not exploded_item else "BOM Explosion Item"
|
doctype = "BOM Item" if not exploded_item else "BOM Explosion Item"
|
||||||
fields = [f"`tab{doctype}`.`stock_qty` / `tabBOM`.`quantity` as qty_consumed_per_unit"]
|
fields = [f"`tab{doctype}`.`stock_qty` / `tabBOM`.`quantity` as qty_consumed_per_unit"]
|
||||||
|
|
||||||
@@ -849,7 +849,7 @@ class SubcontractingController(StockController):
|
|||||||
if self.doctype == self.subcontract_data.order_doctype or (
|
if self.doctype == self.subcontract_data.order_doctype or (
|
||||||
self.backflush_based_on == "BOM" or self.is_return
|
self.backflush_based_on == "BOM" or self.is_return
|
||||||
):
|
):
|
||||||
for bom_item in self.__get_materials_from_bom(
|
for bom_item in self._get_materials_from_bom(
|
||||||
row.item_code, row.bom, row.get("include_exploded_items")
|
row.item_code, row.bom, row.get("include_exploded_items")
|
||||||
):
|
):
|
||||||
qty = flt(bom_item.qty_consumed_per_unit) * flt(row.qty) * row.conversion_factor
|
qty = flt(bom_item.qty_consumed_per_unit) * flt(row.qty) * row.conversion_factor
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
|
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
# For license information, please see license.txt
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.mapper import get_mapped_doc
|
from frappe.model.mapper import get_mapped_doc
|
||||||
@@ -17,6 +19,10 @@ from erpnext.stock.get_item_details import get_default_cost_center, get_default_
|
|||||||
from erpnext.stock.stock_ledger import get_valuation_rate
|
from erpnext.stock.stock_ledger import get_valuation_rate
|
||||||
|
|
||||||
|
|
||||||
|
class BOMQuantityError(frappe.ValidationError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class SubcontractingReceipt(SubcontractingController):
|
class SubcontractingReceipt(SubcontractingController):
|
||||||
# begin: auto-generated types
|
# begin: auto-generated types
|
||||||
# This code is auto-generated. Do not modify anything in this block.
|
# This code is auto-generated. Do not modify anything in this block.
|
||||||
@@ -156,6 +162,7 @@ class SubcontractingReceipt(SubcontractingController):
|
|||||||
def on_submit(self):
|
def on_submit(self):
|
||||||
self.validate_closed_subcontracting_order()
|
self.validate_closed_subcontracting_order()
|
||||||
self.validate_available_qty_for_consumption()
|
self.validate_available_qty_for_consumption()
|
||||||
|
self.validate_bom_required_qty()
|
||||||
self.update_status_updater_args()
|
self.update_status_updater_args()
|
||||||
self.update_prevdoc_status()
|
self.update_prevdoc_status()
|
||||||
self.set_subcontracting_order_status(update_bin=False)
|
self.set_subcontracting_order_status(update_bin=False)
|
||||||
@@ -512,12 +519,60 @@ class SubcontractingReceipt(SubcontractingController):
|
|||||||
item.available_qty_for_consumption
|
item.available_qty_for_consumption
|
||||||
and flt(item.available_qty_for_consumption, precision) - flt(item.consumed_qty, precision) < 0
|
and flt(item.available_qty_for_consumption, precision) - flt(item.consumed_qty, precision) < 0
|
||||||
):
|
):
|
||||||
msg = f"""Row {item.idx}: Consumed Qty {flt(item.consumed_qty, precision)}
|
msg = _(
|
||||||
must be less than or equal to Available Qty For Consumption
|
"""Row {0}: Consumed Qty {1} {2} must be less than or equal to Available Qty For Consumption
|
||||||
{flt(item.available_qty_for_consumption, precision)}
|
{3} {4} in Consumed Items Table."""
|
||||||
in Consumed Items Table."""
|
).format(
|
||||||
|
item.idx,
|
||||||
|
flt(item.consumed_qty, precision),
|
||||||
|
item.stock_uom,
|
||||||
|
flt(item.available_qty_for_consumption, precision),
|
||||||
|
item.stock_uom,
|
||||||
|
)
|
||||||
|
|
||||||
frappe.throw(_(msg))
|
frappe.throw(msg)
|
||||||
|
|
||||||
|
def validate_bom_required_qty(self):
|
||||||
|
if (
|
||||||
|
frappe.db.get_single_value("Buying Settings", "backflush_raw_materials_of_subcontract_based_on")
|
||||||
|
== "Material Transferred for Subcontract"
|
||||||
|
) and not (frappe.db.get_single_value("Buying Settings", "validate_consumed_qty")):
|
||||||
|
return
|
||||||
|
|
||||||
|
rm_consumed_dict = self.get_rm_wise_consumed_qty()
|
||||||
|
|
||||||
|
for row in self.items:
|
||||||
|
precision = row.precision("qty")
|
||||||
|
for bom_item in self._get_materials_from_bom(
|
||||||
|
row.item_code, row.bom, row.get("include_exploded_items")
|
||||||
|
):
|
||||||
|
required_qty = flt(
|
||||||
|
bom_item.qty_consumed_per_unit * row.qty * row.conversion_factor, precision
|
||||||
|
)
|
||||||
|
consumed_qty = rm_consumed_dict.get(bom_item.rm_item_code, 0)
|
||||||
|
diff = flt(consumed_qty, precision) - flt(required_qty, precision)
|
||||||
|
|
||||||
|
if diff < 0:
|
||||||
|
msg = _(
|
||||||
|
"""Additional {0} {1} of item {2} required as per BOM to complete this transaction"""
|
||||||
|
).format(
|
||||||
|
frappe.bold(abs(diff)),
|
||||||
|
frappe.bold(bom_item.stock_uom),
|
||||||
|
frappe.bold(bom_item.rm_item_code),
|
||||||
|
)
|
||||||
|
|
||||||
|
frappe.throw(
|
||||||
|
msg,
|
||||||
|
exc=BOMQuantityError,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_rm_wise_consumed_qty(self):
|
||||||
|
rm_dict = defaultdict(float)
|
||||||
|
|
||||||
|
for row in self.supplied_items:
|
||||||
|
rm_dict[row.rm_item_code] += row.consumed_qty
|
||||||
|
|
||||||
|
return rm_dict
|
||||||
|
|
||||||
def update_status_updater_args(self):
|
def update_status_updater_args(self):
|
||||||
if cint(self.is_return):
|
if cint(self.is_return):
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import
|
|||||||
from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import (
|
from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import (
|
||||||
make_subcontracting_receipt,
|
make_subcontracting_receipt,
|
||||||
)
|
)
|
||||||
|
from erpnext.subcontracting.doctype.subcontracting_receipt.subcontracting_receipt import (
|
||||||
|
BOMQuantityError,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestSubcontractingReceipt(FrappeTestCase):
|
class TestSubcontractingReceipt(FrappeTestCase):
|
||||||
@@ -174,7 +177,7 @@ class TestSubcontractingReceipt(FrappeTestCase):
|
|||||||
def test_subcontracting_over_receipt(self):
|
def test_subcontracting_over_receipt(self):
|
||||||
"""
|
"""
|
||||||
Behaviour: Raise multiple SCRs against one SCO that in total
|
Behaviour: Raise multiple SCRs against one SCO that in total
|
||||||
receive more than the required qty in the SCO.
|
receive more than the required qty in the SCO.
|
||||||
Expected Result: Error Raised for Over Receipt against SCO.
|
Expected Result: Error Raised for Over Receipt against SCO.
|
||||||
"""
|
"""
|
||||||
from erpnext.controllers.subcontracting_controller import (
|
from erpnext.controllers.subcontracting_controller import (
|
||||||
@@ -1785,6 +1788,109 @@ class TestSubcontractingReceipt(FrappeTestCase):
|
|||||||
self.assertEqual(scr.items[0].rm_cost_per_qty, 300)
|
self.assertEqual(scr.items[0].rm_cost_per_qty, 300)
|
||||||
self.assertEqual(scr.items[0].service_cost_per_qty, 100)
|
self.assertEqual(scr.items[0].service_cost_per_qty, 100)
|
||||||
|
|
||||||
|
def test_bom_required_qty_validation_based_on_bom(self):
|
||||||
|
set_backflush_based_on("BOM")
|
||||||
|
frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 1)
|
||||||
|
|
||||||
|
fg_item = make_item(properties={"is_stock_item": 1, "is_sub_contracted_item": 1}).name
|
||||||
|
rm_item1 = make_item(
|
||||||
|
properties={
|
||||||
|
"is_stock_item": 1,
|
||||||
|
"has_batch_no": 1,
|
||||||
|
"create_new_batch": 1,
|
||||||
|
"batch_number_series": "BRQV-.####",
|
||||||
|
}
|
||||||
|
).name
|
||||||
|
|
||||||
|
make_bom(item=fg_item, raw_materials=[rm_item1], rm_qty=2)
|
||||||
|
se = make_stock_entry(
|
||||||
|
item_code=rm_item1,
|
||||||
|
qty=1,
|
||||||
|
target="_Test Warehouse 1 - _TC",
|
||||||
|
rate=300,
|
||||||
|
)
|
||||||
|
|
||||||
|
batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle)
|
||||||
|
|
||||||
|
service_items = [
|
||||||
|
{
|
||||||
|
"warehouse": "_Test Warehouse - _TC",
|
||||||
|
"item_code": "Subcontracted Service Item 1",
|
||||||
|
"qty": 1,
|
||||||
|
"rate": 100,
|
||||||
|
"fg_item": fg_item,
|
||||||
|
"fg_item_qty": 1,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
sco = get_subcontracting_order(service_items=service_items)
|
||||||
|
scr = make_subcontracting_receipt(sco.name)
|
||||||
|
scr.save()
|
||||||
|
scr.reload()
|
||||||
|
|
||||||
|
self.assertEqual(scr.supplied_items[0].batch_no, batch_no)
|
||||||
|
self.assertEqual(scr.supplied_items[0].consumed_qty, 1)
|
||||||
|
self.assertEqual(scr.supplied_items[0].required_qty, 2)
|
||||||
|
|
||||||
|
self.assertRaises(BOMQuantityError, scr.submit)
|
||||||
|
|
||||||
|
frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 0)
|
||||||
|
|
||||||
|
def test_bom_required_qty_validation_based_on_transfer(self):
|
||||||
|
from erpnext.controllers.subcontracting_controller import (
|
||||||
|
make_rm_stock_entry as make_subcontract_transfer_entry,
|
||||||
|
)
|
||||||
|
|
||||||
|
set_backflush_based_on("Material Transferred for Subcontract")
|
||||||
|
frappe.db.set_single_value("Buying Settings", "validate_consumed_qty", 1)
|
||||||
|
|
||||||
|
item_code = "_Test Subcontracted Validation FG Item 1"
|
||||||
|
rm_item1 = make_item(
|
||||||
|
properties={
|
||||||
|
"is_stock_item": 1,
|
||||||
|
}
|
||||||
|
).name
|
||||||
|
|
||||||
|
make_subcontracted_item(item_code=item_code, raw_materials=[rm_item1])
|
||||||
|
service_items = [
|
||||||
|
{
|
||||||
|
"warehouse": "_Test Warehouse - _TC",
|
||||||
|
"item_code": "Subcontracted Service Item 1",
|
||||||
|
"qty": 10,
|
||||||
|
"rate": 100,
|
||||||
|
"fg_item": item_code,
|
||||||
|
"fg_item_qty": 10,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
sco = get_subcontracting_order(
|
||||||
|
service_items=service_items,
|
||||||
|
include_exploded_items=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
# inward raw material stock
|
||||||
|
make_stock_entry(target="_Test Warehouse - _TC", item_code=rm_item1, qty=10, basic_rate=100)
|
||||||
|
|
||||||
|
rm_items = [
|
||||||
|
{
|
||||||
|
"item_code": item_code,
|
||||||
|
"rm_item_code": sco.supplied_items[0].rm_item_code,
|
||||||
|
"qty": sco.supplied_items[0].required_qty - 5,
|
||||||
|
"warehouse": "_Test Warehouse - _TC",
|
||||||
|
"stock_uom": "Nos",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
# transfer partial raw materials
|
||||||
|
ste = frappe.get_doc(make_subcontract_transfer_entry(sco.name, rm_items))
|
||||||
|
ste.to_warehouse = "_Test Warehouse 1 - _TC"
|
||||||
|
ste.save()
|
||||||
|
ste.submit()
|
||||||
|
|
||||||
|
scr = make_subcontracting_receipt(sco.name)
|
||||||
|
scr.save()
|
||||||
|
|
||||||
|
self.assertRaises(BOMQuantityError, scr.submit)
|
||||||
|
|
||||||
|
|
||||||
def make_return_subcontracting_receipt(**args):
|
def make_return_subcontracting_receipt(**args):
|
||||||
args = frappe._dict(args)
|
args = frappe._dict(args)
|
||||||
|
|||||||
Reference in New Issue
Block a user