diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index a0b102dbc54..c953ab48a1c 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -181,45 +181,65 @@ class StatusUpdater(Document): self.status = "Draft" return - if self.doctype in status_map: - _status = self.status - if status and update: - self.db_set("status", status) + if self.doctype not in status_map: + return + _status = self.status + # TODO: revise accidential complexity where update has a double meaning, see below + self.status = status if status else self.status + new_status = self.get_status()["status"] - sl = status_map[self.doctype][:] - sl.reverse() - for s in sl: - if not s[1]: - self.status = s[0] - break - elif s[1].startswith("eval:"): - if frappe.safe_eval( - s[1][5:], - None, - { - "self": self.as_dict(), - "getdate": getdate, - "nowdate": nowdate, - "get_value": frappe.db.get_value, - }, - ): - self.status = s[0] - break - elif getattr(self, s[1])(): - self.status = s[0] - break - - if self.status != _status and self.status not in ( - "Cancelled", - "Partially Ordered", - "Ordered", - "Issued", - "Transferred", - ): - self.add_comment("Label", _(self.status)) + if new_status != _status: + if new_status not in ("Cancelled", "Partially Ordered", "Ordered", "Issued", "Transferred"): + self.add_comment("Label", _(new_status)) + self.status = new_status if update: - self.db_set("status", self.status, update_modified=update_modified) + self.db_set("status", new_status, update_modified=update_modified) + + def get_status(self): + """ + Get the status of the document. + + Returns: + dict: A dictionary containing the status. This allows callers to receive + a dictionary for efficient bulk updates, for example when `per_billed` + and other status fields also need to be updated. + + Note: + Can be overriden on a doctype to implement more localized status updater logic. + + Example: + { + "status": "Draft", + "per_billed": 50, + "billing_status": "Partly Billed" + } + """ + if self.doctype not in status_map: + return {"status": self.status} + + sl = status_map[self.doctype][:] + sl.reverse() + + for s in sl: + if not s[1]: + return {"status": s[0]} + elif s[1].startswith("eval:"): + if frappe.safe_eval( + s[1][5:], + None, + { + "self": self.as_dict(), + "getdate": getdate, + "nowdate": nowdate, + "get_value": frappe.db.get_value, + }, + ): + return {"status": s[0]} + elif getattr(self, s[1])(): + return {"status": s[0]} + + return {"status": self.status} def validate_qty(self): """Validates qty at row level""" @@ -447,6 +467,39 @@ class StatusUpdater(Document): where name='{detail_id}'""".format(**args) ) + @staticmethod + def _calculate_target_parent_percentage( + name, target_parent_dt, target_dt, target_ref_field, target_field + ): + child_records = frappe.get_all( + target_dt, + filters={"parent": name, "parenttype": target_parent_dt}, + fields=[target_ref_field, target_field], + ) + + sum_ref = sum(abs(record[target_ref_field]) for record in child_records) + + if sum_ref > 0: + percentage = round( + sum(min(abs(record[target_field]), abs(record[target_ref_field])) for record in child_records) + / sum_ref + * 100, + 6, + ) + else: + percentage = 0 + + return percentage + + @staticmethod + def _determine_status(percentage, keyword): + if percentage < 0.001: + return f"Not {keyword}" + elif percentage >= 99.999999: + return f"Fully {keyword}" + else: + return f"Partly {keyword}" + def _update_percent_field_in_targets(self, args, update_modified=True): """Update percent field in parent transaction""" if args.get("percent_join_field_parent"): @@ -467,34 +520,28 @@ class StatusUpdater(Document): def _update_percent_field(self, args, update_modified=True): """Update percent field in parent transaction""" - self._update_modified(args, update_modified) + update_data = {} if args.get("target_parent_field"): - frappe.db.sql( - """update `tab{target_parent_dt}` - set {target_parent_field} = round( - ifnull((select - ifnull(sum(case when abs({target_ref_field}) > abs({target_field}) then abs({target_field}) else abs({target_ref_field}) end), 0) - / sum(abs({target_ref_field})) * 100 - from `tab{target_dt}` where parent='{name}' and parenttype='{target_parent_dt}' having sum(abs({target_ref_field})) > 0), 0), 6) - {update_modified} - where name='{name}'""".format(**args) + update_data[args.get("target_parent_field")] = self._calculate_target_parent_percentage( + args["name"], + args["target_parent_dt"], + args["target_dt"], + args["target_ref_field"], + args["target_field"], ) - # update field if args.get("status_field"): - frappe.db.sql( - """update `tab{target_parent_dt}` - set {status_field} = (case when {target_parent_field}<0.001 then 'Not {keyword}' - else case when {target_parent_field}>=99.999999 then 'Fully {keyword}' - else 'Partly {keyword}' end end) - where name='{name}'""".format(**args) + update_data[args.get("status_field")] = self._determine_status( + update_data[args.get("target_parent_field")], args["keyword"] ) - if update_modified: - target = frappe.get_doc(args["target_parent_dt"], args["name"]) - target.set_status(update=True) - target.notify_update() + if update_data: + target = frappe.get_doc(args["target_parent_dt"], args["name"]) + target.update(update_data) # status calculus might depend on it + status = target.get_status() + update_data.update(status) + target.db_set(update_data, update_modified=update_modified, notify=True) def _update_modified(self, args, update_modified): if not update_modified: