mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-20 05:29:18 +00:00
Merge pull request #45063 from frappe/version-15-hotfix
chore: release v15
This commit is contained in:
@@ -103,6 +103,16 @@ class SubcontractingController(StockController):
|
|||||||
_("Row {0}: Item {1} must be a subcontracted item.").format(item.idx, item.item_name)
|
_("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 (
|
if (
|
||||||
self.doctype not in "Subcontracting Receipt"
|
self.doctype not in "Subcontracting Receipt"
|
||||||
and item.qty
|
and item.qty
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<div class="col-md-5" style="max-height: 500px">
|
<div class="col-md-5" style="max-height: 500px">
|
||||||
{% if data.image %}
|
{% if data.image %}
|
||||||
<div class="border image-field " style="overflow: hidden;border-color:#e6e6e6">
|
<div class="border image-field " style="overflow: hidden;border-color:#e6e6e6">
|
||||||
<img class="responsive" src={{ data.image }}>
|
<img class="responsive" style="width: 100%;" src={{ data.image }}>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
|
from frappe.utils import date_diff, get_datetime, now
|
||||||
|
|
||||||
|
|
||||||
class BOMUpdateTool(Document):
|
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"):
|
if frappe.db.get_single_value("Manufacturing Settings", "update_bom_costs_automatically"):
|
||||||
wip_log = frappe.get_all(
|
wip_log = frappe.get_all(
|
||||||
"BOM Update Log",
|
"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,
|
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")
|
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(
|
def create_bom_update_log(
|
||||||
boms: dict[str, str] | None = None,
|
boms: dict[str, str] | None = None,
|
||||||
update_type: Literal["Replace BOM", "Update Cost"] = "Replace BOM",
|
update_type: Literal["Replace BOM", "Update Cost"] = "Replace BOM",
|
||||||
|
|||||||
@@ -242,14 +242,14 @@
|
|||||||
"depends_on": "eval:doc.backflush_raw_materials_based_on == \"BOM\"",
|
"depends_on": "eval:doc.backflush_raw_materials_based_on == \"BOM\"",
|
||||||
"fieldname": "validate_components_quantities_per_bom",
|
"fieldname": "validate_components_quantities_per_bom",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Validate Components Quantities Per BOM"
|
"label": "Validate Components and Quantities Per BOM"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "icon-wrench",
|
"icon": "icon-wrench",
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-09-02 12:12:03.132567",
|
"modified": "2025-01-02 12:46:33.520853",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "Manufacturing Settings",
|
"name": "Manufacturing Settings",
|
||||||
@@ -267,4 +267,4 @@
|
|||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": [],
|
"states": [],
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
@@ -2401,6 +2401,56 @@ class TestWorkOrder(FrappeTestCase):
|
|||||||
|
|
||||||
frappe.db.set_single_value("Manufacturing Settings", "validate_components_quantities_per_bom", 0)
|
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):
|
def make_operation(**kwargs):
|
||||||
kwargs = frappe._dict(kwargs)
|
kwargs = frappe._dict(kwargs)
|
||||||
|
|||||||
@@ -201,7 +201,6 @@ class StockEntry(StockController):
|
|||||||
self.validate_purpose()
|
self.validate_purpose()
|
||||||
self.validate_item()
|
self.validate_item()
|
||||||
self.validate_customer_provided_item()
|
self.validate_customer_provided_item()
|
||||||
self.validate_qty()
|
|
||||||
self.set_transfer_qty()
|
self.set_transfer_qty()
|
||||||
self.validate_uom_is_integer("uom", "qty")
|
self.validate_uom_is_integer("uom", "qty")
|
||||||
self.validate_uom_is_integer("stock_uom", "transfer_qty")
|
self.validate_uom_is_integer("stock_uom", "transfer_qty")
|
||||||
@@ -232,7 +231,7 @@ class StockEntry(StockController):
|
|||||||
self.validate_serialized_batch()
|
self.validate_serialized_batch()
|
||||||
self.calculate_rate_and_amount()
|
self.calculate_rate_and_amount()
|
||||||
self.validate_putaway_capacity()
|
self.validate_putaway_capacity()
|
||||||
self.validate_component_quantities()
|
self.validate_component_and_quantities()
|
||||||
|
|
||||||
if not self.get("purpose") == "Manufacture":
|
if not self.get("purpose") == "Manufacture":
|
||||||
# ignore scrap item wh difference and empty source/target wh
|
# 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)
|
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):
|
def validate_fg_completed_qty(self):
|
||||||
item_wise_qty = {}
|
item_wise_qty = {}
|
||||||
if self.purpose == "Manufacture" and self.work_order:
|
if self.purpose == "Manufacture" and self.work_order:
|
||||||
@@ -748,7 +713,7 @@ class StockEntry(StockController):
|
|||||||
title=_("Insufficient Stock"),
|
title=_("Insufficient Stock"),
|
||||||
)
|
)
|
||||||
|
|
||||||
def validate_component_quantities(self):
|
def validate_component_and_quantities(self):
|
||||||
if self.purpose not in ["Manufacture", "Material Transfer for Manufacture"]:
|
if self.purpose not in ["Manufacture", "Material Transfer for Manufacture"]:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -761,20 +726,31 @@ class StockEntry(StockController):
|
|||||||
raw_materials = self.get_bom_raw_materials(self.fg_completed_qty)
|
raw_materials = self.get_bom_raw_materials(self.fg_completed_qty)
|
||||||
|
|
||||||
precision = frappe.get_precision("Stock Entry Detail", "qty")
|
precision = frappe.get_precision("Stock Entry Detail", "qty")
|
||||||
for row in self.items:
|
for item_code, details in raw_materials.items():
|
||||||
if not row.s_warehouse:
|
if matched_item := self.get_matched_items(item_code):
|
||||||
continue
|
if flt(details.get("qty"), precision) != flt(matched_item.qty, precision):
|
||||||
|
|
||||||
if details := raw_materials.get(row.item_code):
|
|
||||||
if flt(details.get("qty"), precision) != flt(row.qty, precision):
|
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_("For the item {0}, the quantity should be {1} according to the BOM {2}.").format(
|
_("For the item {0}, the quantity should be {1} according to the BOM {2}.").format(
|
||||||
frappe.bold(row.item_code),
|
frappe.bold(item_code),
|
||||||
flt(details.get("qty"), precision),
|
flt(details.get("qty")),
|
||||||
get_link_to_form("BOM", self.bom_no),
|
get_link_to_form("BOM", self.bom_no),
|
||||||
),
|
),
|
||||||
title=_("Incorrect Component Quantity"),
|
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()
|
@frappe.whitelist()
|
||||||
def get_stock_and_rate(self):
|
def get_stock_and_rate(self):
|
||||||
|
|||||||
Reference in New Issue
Block a user