From 7cb8415a3f84d77c0ec43f259ed03d392c75a865 Mon Sep 17 00:00:00 2001 From: kunhimohamed Date: Tue, 24 Feb 2026 14:36:09 +0400 Subject: [PATCH] fix(UOM): validate uom convertible at transactions --- erpnext/controllers/buying_controller.py | 5 ++ erpnext/controllers/selling_controller.py | 10 ++++ erpnext/stock/doctype/item/item.py | 53 +++++++++++++++++++ .../doctype/packing_slip/packing_slip.py | 10 ++++ .../stock/doctype/stock_entry/stock_entry.py | 5 ++ erpnext/utilities/transaction_base.py | 14 +++++ 6 files changed, 97 insertions(+) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 9f5e3ec735c8..1f58b90fa437 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -54,6 +54,11 @@ def validate(self): self.validate_stock_or_nonstock_items() self.validate_warehouse() self.validate_asset_return() + self.validate_uom_convertability( + item_table_fieldname="items", + item_code_fieldname="item_code", + uom_fieldname="uom" + ) if self.doctype == "Purchase Invoice": self.validate_purchase_receipt_if_update_stock() diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 2e855b075fd7..ac48ce384bf2 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -58,6 +58,16 @@ def validate(self): self.validate_selling_price() self.set_qty_as_per_stock_uom() self.set_alt_uom_qty() + self.validate_uom_convertability( + item_table_fieldname="items", + item_code_fieldname="item_code", + uom_fieldname="uom" + ) + self.validate_uom_convertability( + item_table_fieldname="packed_items", + item_code_fieldname="item_code", + uom_fieldname="uom" + ) self.set_po_nos() self.set_gross_profit() self.validate_for_duplicate_items() diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 43149b54030b..1df378d3d9df 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -117,6 +117,7 @@ def validate(self): self.validate_fixed_asset() self.validate_retain_sample() self.validate_uom_conversion_factor() + self.validate_uom_convertible() self.validate_weight() self.validate_customer_provided_part() self.validate_auto_reorder_enabled_in_stock_settings() @@ -572,6 +573,58 @@ def validate_uom_conversion_factor(self): frappe.msgprint("Setting conversion factor for UOM {0} from UOM Conversion Factor Master as {1}" .format(d.uom, value), alert=True) d.conversion_factor = value + + def validate_uom_convertible(self): + throw_error = False + error_msg = None + if self.sales_uom: + sales_uom_not_convertible = self.get_conversion_factor(self.sales_uom).get("not_convertible") + if sales_uom_not_convertible: + throw_error = True + error_msg = "the Default Sales Unit of Measure not Convertible" + if self.purchase_uom: + purchase_uom_not_convertible = self.get_conversion_factor(self.purchase_uom).get("not_convertible") + if purchase_uom_not_convertible: + throw_error = True + error_msg = "Default Purchase Unit of Measure not Convertible" + if self.manufacture_uom: + manufacture_uom_not_convertible = self.get_conversion_factor(self.manufacture_uom).get("not_convertible") + if manufacture_uom_not_convertible: + throw_error = True + error_msg = "Default Raw Material UOM not Convertible" + if throw_error: + frappe.throw(error_msg) + + + def get_conversion_factor(self, uom): + # first look for direct conversion factor in item + item_conversion_factors = dict([(c.uom, c.conversion_factor) for c in self.uoms]) + conversion_factor = flt(item_conversion_factors.get(uom)) + + # then look for conversion factor in template item if variant + if not conversion_factor and self.variant_of: + template_item = frappe.get_cached_doc("Item", self.variant_of) + template_item_conversion_factors = dict([(c.uom, c.conversion_factor) for c in template_item.uoms]) + if uom in template_item_conversion_factors: + conversion_factor = flt(item_conversion_factors.get(uom)) + + # then look for global conversion factor for stock uom first then the rest of the item's convertible uoms + if not conversion_factor: + stock_uom = self.stock_uom + item_uoms = [stock_uom] + [cuom for cuom, cf in item_conversion_factors.items() if cuom != stock_uom and flt(cf)] + + for item_uom in item_uoms: + conversion_factor = flt(get_uom_conv_factor(uom, item_uom)) + if conversion_factor: + if item_uom != stock_uom: + # apply item_uom -> stock_uom conversion factor and then exit loop + conversion_factor *= flt(item_conversion_factors.get(item_uom)) + break + + return frappe._dict({ + "conversion_factor": conversion_factor or 1.0, + "not_convertible": 1 if not conversion_factor else 0 + }) def validate_weight(self): weight_fields = ["net_weight_per_unit", "tare_weight_per_unit", "gross_weight_per_unit"] diff --git a/erpnext/stock/doctype/packing_slip/packing_slip.py b/erpnext/stock/doctype/packing_slip/packing_slip.py index 7c9cb3581e09..60cffadf48af 100644 --- a/erpnext/stock/doctype/packing_slip/packing_slip.py +++ b/erpnext/stock/doctype/packing_slip/packing_slip.py @@ -54,6 +54,16 @@ def validate(self): self.validate_warehouse() self.validate_uom_is_integer("stock_uom", "stock_qty") self.validate_uom_is_integer("uom", "qty") + self.validate_uom_convertability( + item_table_fieldname="items", + item_code_fieldname="item_code", + uom_fieldname="uom" + ) + self.validate_uom_convertability( + item_table_fieldname="packaging_items", + item_code_fieldname="item_code", + uom_fieldname="uom" + ) self.calculate_totals() self.validate_qty() self.validate_weights() diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 6ec237a0d0f3..eae15735424f 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -86,6 +86,11 @@ def validate(self): self.set_stock_qty() self.validate_uom_is_integer("uom", "qty") self.validate_uom_is_integer("stock_uom", "stock_qty") + self.validate_uom_convertability( + item_table_fieldname="items", + item_code_fieldname="item_code", + uom_fieldname="uom" + ) self.set_missing_warehouses() self.validate_warehouse() self.set_warehouse_address() diff --git a/erpnext/utilities/transaction_base.py b/erpnext/utilities/transaction_base.py index 386a032a8432..4f6c70a03aad 100644 --- a/erpnext/utilities/transaction_base.py +++ b/erpnext/utilities/transaction_base.py @@ -7,6 +7,8 @@ from frappe.utils import cstr, now_datetime, cint, flt, get_time, get_link_to_form, date_diff, add_days, getdate from erpnext.setup.doctype.terms_and_conditions.terms_and_conditions import get_terms_and_conditions from erpnext.controllers.status_updater import StatusUpdaterERP +from erpnext.stock.get_item_details import get_conversion_factor +from frappe.model.document import Document class UOMMustBeIntegerError(frappe.ValidationError): pass @@ -78,6 +80,10 @@ def _add_calendar_event(self, opts): def validate_uom_is_integer(self, uom_field, qty_fields): validate_uom_is_integer(self, uom_field, qty_fields) + def validate_uom_convertability(self, item_table_fieldname, item_code_fieldname, uom_fieldname): + for each_item in self.get(item_table_fieldname): + validate_uom_convertability(each_item, each_item.get(item_code_fieldname), each_item.get(uom_fieldname)) + def validate_with_previous_doc(self, ref, table_doctype=None): self.exclude_fields = ["conversion_factor", "uom"] if self.get('is_return') else [] @@ -235,3 +241,11 @@ def validate_uom_is_integer(doc, uom_field, qty_fields, child_dt=None): frappe.throw(_("Row {1}: Quantity ({0}) cannot be a fraction. To allow this, disable '{2}' in UOM {3}.") \ .format(qty, d.idx, frappe.bold(_("Must be Whole Number")), frappe.bold(d.get(uom_field))), UOMMustBeIntegerError) + +def validate_uom_convertability(item_row:Document, item_code: str, uom: str) -> None: + do_not_restrict_uom_selection = frappe.db.get_value("Item", item_code, "do_not_restrict_uom_selection") + if do_not_restrict_uom_selection: + return + not_convertible = get_conversion_factor(item_code, uom).get("not_convertible") + if not_convertible: + frappe.throw(f"the uom {uom} Not convertible at row {item_row.idx}")