diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py
index 75cb5516348..0f9431ab440 100644
--- a/erpnext/controllers/subcontracting_controller.py
+++ b/erpnext/controllers/subcontracting_controller.py
@@ -103,6 +103,16 @@ class SubcontractingController(StockController):
_("Row {0}: Item {1} must be a subcontracted item.").format(item.idx, item.item_name)
)
+ if (
+ self.doctype == "Subcontracting Order" and not item.sc_conversion_factor
+ ): # this condition will only be true if user has recently updated from develop branch
+ service_item_qty = frappe.get_value(
+ "Subcontracting Order Service Item",
+ filters={"purchase_order_item": item.purchase_order_item, "parent": self.name},
+ fieldname=["qty"],
+ )
+ item.sc_conversion_factor = service_item_qty / item.qty
+
if (
self.doctype not in "Subcontracting Receipt"
and item.qty
diff --git a/erpnext/manufacturing/doctype/bom/bom_item_preview.html b/erpnext/manufacturing/doctype/bom/bom_item_preview.html
index eb4135e03ac..2c0f091da58 100644
--- a/erpnext/manufacturing/doctype/bom/bom_item_preview.html
+++ b/erpnext/manufacturing/doctype/bom/bom_item_preview.html
@@ -3,7 +3,7 @@
{% if data.image %}
-

+
{% endif %}
diff --git a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py
index 2e8dba1ccfe..983dd2d4cd8 100644
--- a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py
+++ b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py
@@ -9,6 +9,7 @@ if TYPE_CHECKING:
import frappe
from frappe.model.document import Document
+from frappe.utils import date_diff, get_datetime, now
class BOMUpdateTool(Document):
@@ -50,13 +51,21 @@ def auto_update_latest_price_in_all_boms() -> None:
if frappe.db.get_single_value("Manufacturing Settings", "update_bom_costs_automatically"):
wip_log = frappe.get_all(
"BOM Update Log",
- {"update_type": "Update Cost", "status": ["in", ["Queued", "In Progress"]]},
+ fields=["creation", "status"],
+ filters={"update_type": "Update Cost", "status": ["in", ["Queued", "In Progress"]]},
limit_page_length=1,
+ order_by="creation desc",
)
- if not wip_log:
+
+ if not wip_log or is_older_log(wip_log[0]):
create_bom_update_log(update_type="Update Cost")
+def is_older_log(log: dict) -> bool:
+ no_of_days = date_diff(get_datetime(now()), get_datetime(log.creation))
+ return no_of_days > 10
+
+
def create_bom_update_log(
boms: dict[str, str] | None = None,
update_type: Literal["Replace BOM", "Update Cost"] = "Replace BOM",
diff --git a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json
index 26cbc03eeb2..618ccdf8fc8 100644
--- a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json
+++ b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json
@@ -242,14 +242,14 @@
"depends_on": "eval:doc.backflush_raw_materials_based_on == \"BOM\"",
"fieldname": "validate_components_quantities_per_bom",
"fieldtype": "Check",
- "label": "Validate Components Quantities Per BOM"
+ "label": "Validate Components and Quantities Per BOM"
}
],
"icon": "icon-wrench",
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2024-09-02 12:12:03.132567",
+ "modified": "2025-01-02 12:46:33.520853",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Manufacturing Settings",
@@ -267,4 +267,4 @@
"sort_order": "DESC",
"states": [],
"track_changes": 1
-}
+}
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py
index f8ddf007428..86a03e7919c 100644
--- a/erpnext/manufacturing/doctype/work_order/test_work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py
@@ -2401,6 +2401,56 @@ class TestWorkOrder(FrappeTestCase):
frappe.db.set_single_value("Manufacturing Settings", "validate_components_quantities_per_bom", 0)
+ def test_components_as_per_bom_for_manufacture_entry(self):
+ frappe.db.set_single_value("Manufacturing Settings", "backflush_raw_materials_based_on", "BOM")
+ frappe.db.set_single_value("Manufacturing Settings", "validate_components_quantities_per_bom", 1)
+
+ fg_item = "Test FG Item For Component Validation 1"
+ source_warehouse = "Stores - _TC"
+ raw_materials = ["Test Component Validation RM Item 11", "Test Component Validation RM Item 12"]
+
+ make_item(fg_item, {"is_stock_item": 1})
+ for item in raw_materials:
+ make_item(item, {"is_stock_item": 1})
+ test_stock_entry.make_stock_entry(
+ item_code=item,
+ target=source_warehouse,
+ qty=10,
+ basic_rate=100,
+ )
+
+ make_bom(item=fg_item, source_warehouse=source_warehouse, raw_materials=raw_materials)
+
+ wo = make_wo_order_test_record(
+ item=fg_item,
+ qty=10,
+ source_warehouse=source_warehouse,
+ )
+
+ transfer_entry = frappe.get_doc(make_stock_entry(wo.name, "Material Transfer for Manufacture", 10))
+ transfer_entry.save()
+ transfer_entry.remove(transfer_entry.items[0])
+
+ self.assertRaises(frappe.ValidationError, transfer_entry.save)
+
+ transfer_entry = frappe.get_doc(make_stock_entry(wo.name, "Material Transfer for Manufacture", 10))
+ transfer_entry.save()
+ transfer_entry.submit()
+
+ manufacture_entry = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 10))
+ manufacture_entry.save()
+
+ manufacture_entry.remove(manufacture_entry.items[0])
+
+ self.assertRaises(frappe.ValidationError, manufacture_entry.save)
+ manufacture_entry.delete()
+
+ manufacture_entry = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 10))
+ manufacture_entry.save()
+ manufacture_entry.submit()
+
+ frappe.db.set_single_value("Manufacturing Settings", "validate_components_quantities_per_bom", 0)
+
def make_operation(**kwargs):
kwargs = frappe._dict(kwargs)
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index 78f69b1ce62..4b4ee6ad0f1 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -201,7 +201,6 @@ class StockEntry(StockController):
self.validate_purpose()
self.validate_item()
self.validate_customer_provided_item()
- self.validate_qty()
self.set_transfer_qty()
self.validate_uom_is_integer("uom", "qty")
self.validate_uom_is_integer("stock_uom", "transfer_qty")
@@ -232,7 +231,7 @@ class StockEntry(StockController):
self.validate_serialized_batch()
self.calculate_rate_and_amount()
self.validate_putaway_capacity()
- self.validate_component_quantities()
+ self.validate_component_and_quantities()
if not self.get("purpose") == "Manufacture":
# ignore scrap item wh difference and empty source/target wh
@@ -463,40 +462,6 @@ class StockEntry(StockController):
flt(item.qty) * flt(item.conversion_factor), self.precision("transfer_qty", item)
)
- def validate_qty(self):
- manufacture_purpose = ["Manufacture", "Material Consumption for Manufacture"]
-
- if self.purpose in manufacture_purpose and self.work_order:
- if not frappe.get_value("Work Order", self.work_order, "skip_transfer"):
- item_code = []
- for item in self.items:
- if cstr(item.t_warehouse) == "":
- req_items = frappe.get_all(
- "Work Order Item",
- filters={"parent": self.work_order, "item_code": item.item_code},
- fields=["item_code"],
- )
-
- transferred_materials = frappe.db.sql(
- """
- select
- sum(sed.qty) as qty
- from `tabStock Entry` se,`tabStock Entry Detail` sed
- where
- se.name = sed.parent and se.docstatus=1 and
- (se.purpose='Material Transfer for Manufacture' or se.purpose='Manufacture')
- and sed.item_code=%s and se.work_order= %s and ifnull(sed.t_warehouse, '') != ''
- """,
- (item.item_code, self.work_order),
- as_dict=1,
- )
-
- stock_qty = flt(item.qty)
- trans_qty = flt(transferred_materials[0].qty)
- if req_items:
- if stock_qty > trans_qty:
- item_code.append(item.item_code)
-
def validate_fg_completed_qty(self):
item_wise_qty = {}
if self.purpose == "Manufacture" and self.work_order:
@@ -748,7 +713,7 @@ class StockEntry(StockController):
title=_("Insufficient Stock"),
)
- def validate_component_quantities(self):
+ def validate_component_and_quantities(self):
if self.purpose not in ["Manufacture", "Material Transfer for Manufacture"]:
return
@@ -761,20 +726,31 @@ class StockEntry(StockController):
raw_materials = self.get_bom_raw_materials(self.fg_completed_qty)
precision = frappe.get_precision("Stock Entry Detail", "qty")
- for row in self.items:
- if not row.s_warehouse:
- continue
-
- if details := raw_materials.get(row.item_code):
- if flt(details.get("qty"), precision) != flt(row.qty, precision):
+ for item_code, details in raw_materials.items():
+ if matched_item := self.get_matched_items(item_code):
+ if flt(details.get("qty"), precision) != flt(matched_item.qty, precision):
frappe.throw(
_("For the item {0}, the quantity should be {1} according to the BOM {2}.").format(
- frappe.bold(row.item_code),
- flt(details.get("qty"), precision),
+ frappe.bold(item_code),
+ flt(details.get("qty")),
get_link_to_form("BOM", self.bom_no),
),
title=_("Incorrect Component Quantity"),
)
+ else:
+ frappe.throw(
+ _("According to the BOM {0}, the Item '{1}' is missing in the stock entry.").format(
+ get_link_to_form("BOM", self.bom_no), frappe.bold(item_code)
+ ),
+ title=_("Missing Item"),
+ )
+
+ def get_matched_items(self, item_code):
+ for row in self.items:
+ if row.item_code == item_code:
+ return row
+
+ return {}
@frappe.whitelist()
def get_stock_and_rate(self):