diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index d7af491edbe8..fc318f17d3bd 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -483,6 +483,9 @@ def on_submit(self): self.update_blanket_order() + if frappe.db.get_single_value("Buying Settings", "project_update_frequency") == "Each Transaction": + self.update_project() + update_linked_doc(self.doctype, self.name, self.inter_company_order_reference) self.auto_create_subcontracting_order() @@ -506,6 +509,9 @@ def on_cancel(self): self.update_reserved_qty_for_subcontract() self.check_on_hold_or_closed_status() + if frappe.db.get_single_value("Buying Settings", "project_update_frequency") == "Each Transaction": + self.update_project() + self.db_set("status", "Cancelled") self.update_prevdoc_status() @@ -654,6 +660,21 @@ def update_subcontracting_order_status(self): if sco: update_sco_status(sco, "Closed" if self.status == "Closed" else None) + def update_project(self): + projects = frappe._dict() + for d in self.items: + if d.project: + if self.docstatus == 1: + projects[d.project] = projects.get(d.project, 0) + d.base_net_amount + elif self.docstatus == 2: + projects[d.project] = projects.get(d.project, 0) - d.base_net_amount + + pj = frappe.qb.DocType("Project") + for proj, value in projects.items(): + res = frappe.qb.from_(pj).select(pj.total_ordered_cost).where(pj.name == proj).for_update().run() + current_ordered_cost = res and res[0][0] or 0 + frappe.db.set_value("Project", proj, "total_ordered_cost", current_ordered_cost + value) + def set_missing_values(self, for_validate=False): tds_category = frappe.db.get_value("Supplier", self.supplier, "tax_withholding_category") if tds_category and not for_validate: diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index 63d50989111e..a48f316afe49 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -20,6 +20,7 @@ ) from erpnext.controllers.accounts_controller import InvalidQtyError, update_child_qty_rate from erpnext.manufacturing.doctype.blanket_order.test_blanket_order import make_blanket_order +from erpnext.projects.doctype.project.test_project import make_project from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.material_request.material_request import make_purchase_order from erpnext.stock.doctype.material_request.test_material_request import make_material_request @@ -1172,6 +1173,33 @@ def test_po_billed_amount_against_return_entry(self): po.reload() self.assertEqual(po.per_billed, 100) + def test_total_ordered_cost_for_project(self): + if not frappe.db.exists("Project", {"project_name": "_Test Project for Ordered"}): + project = make_project({"project_name": "_Test Project for Ordered"}) + else: + project = frappe.get_doc("Project", "_Test Project for Ordered") + + pi = create_purchase_order(qty=1, rate=500, project=project.name) + self.assertEqual( + frappe.db.get_value("Project", project.name, "total_ordered_cost"), + 500.0, + ) + + pi1 = create_purchase_order(qty=10, rate=500, project=project.name) + self.assertEqual( + frappe.db.get_value("Project", project.name, "total_ordered_cost"), + 5500.0, + ) + + pi1.cancel() + self.assertEqual( + frappe.db.get_value("Project", project.name, "total_ordered_cost"), + 500.0, + ) + + pi.cancel() + self.assertEqual(frappe.db.get_value("Project", project.name, "total_ordered_cost"), 0) + def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier @@ -1275,6 +1303,7 @@ def create_purchase_order(**args): "against_blanket": args.against_blanket, "material_request": args.material_request, "material_request_item": args.material_request_item, + "project": args.project, }, ) diff --git a/erpnext/projects/doctype/project/project.js b/erpnext/projects/doctype/project/project.js index 643e3b21782e..74fd9760aedd 100644 --- a/erpnext/projects/doctype/project/project.js +++ b/erpnext/projects/doctype/project/project.js @@ -87,6 +87,14 @@ frappe.ui.form.on("Project", { __("Actions") ); + frm.add_custom_button( + __("Update Total Ordered Cost"), + () => { + frm.events.update_total_ordered_cost(frm); + }, + __("Actions") + ); + frm.add_custom_button( __("Update Total Purchase Cost"), () => { @@ -144,6 +152,21 @@ frappe.ui.form.on("Project", { }); }, + update_total_ordered_cost: function (frm) { + frappe.call({ + method: "erpnext.projects.doctype.project.project.recalculate_project_total_ordered_cost", + args: { project: frm.doc.name }, + freeze: true, + freeze_message: __("Recalculating Ordered Cost against this Project..."), + callback: function (r) { + if (r && !r.exc) { + frappe.msgprint(__("Total Ordered Cost has been updated")); + frm.refresh(); + } + }, + }); + }, + set_project_status_button: function (frm) { frm.add_custom_button( __("Set Project Status"), diff --git a/erpnext/projects/doctype/project/project.json b/erpnext/projects/doctype/project/project.json index 747d4fe5a83d..1951e9795fa2 100644 --- a/erpnext/projects/doctype/project/project.json +++ b/erpnext/projects/doctype/project/project.json @@ -38,6 +38,7 @@ "project_details", "estimated_costing", "total_costing_amount", + "total_ordered_cost", "total_purchase_cost", "company", "column_break_28", @@ -447,6 +448,12 @@ "print_hide": 1, "reqd": 1, "set_only_once": 1 + }, + { + "fieldname": "total_ordered_cost", + "fieldtype": "Currency", + "label": "Total Ordered Cost (via Purchase Order)", + "read_only": 1 } ], "icon": "fa fa-puzzle-piece", @@ -454,7 +461,7 @@ "index_web_pages_for_search": 1, "links": [], "max_attachments": 4, - "modified": "2024-04-24 10:56:16.001032", + "modified": "2024-05-02 22:34:24.991034", "modified_by": "Administrator", "module": "Projects", "name": "Project", diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py index db4bb2db0c9f..cedbc94bfaeb 100644 --- a/erpnext/projects/doctype/project/project.py +++ b/erpnext/projects/doctype/project/project.py @@ -67,6 +67,7 @@ class Project(Document): total_billed_amount: DF.Currency total_consumed_material_cost: DF.Currency total_costing_amount: DF.Currency + total_ordered_cost: DF.Currency total_purchase_cost: DF.Currency total_sales_amount: DF.Currency users: DF.Table[ProjectUser] @@ -295,6 +296,7 @@ def update_costing(self): self.actual_time = from_time_sheet.time self.update_purchase_costing() + self.update_ordered_costing() self.update_sales_amount() self.update_billed_amount() self.calculate_gross_margin() @@ -311,9 +313,13 @@ def calculate_gross_margin(self): self.per_gross_margin = (self.gross_margin / flt(self.total_billed_amount)) * 100 def update_purchase_costing(self): - total_purchase_cost = calculate_total_purchase_cost(self.name) + total_purchase_cost = get_total_purchase_cost(self.name) self.total_purchase_cost = total_purchase_cost and total_purchase_cost[0][0] or 0 + def update_ordered_costing(self): + total_ordered_cost = get_total_ordered_cost(self.name) + self.total_ordered_cost = total_ordered_cost and total_ordered_cost[0][0] or 0 + def update_sales_amount(self): total_sales_amount = frappe.db.sql( """select sum(base_net_total) @@ -729,7 +735,7 @@ def get_users_email(doc): return [d.email for d in doc.users if frappe.db.get_value("User", d.user, "enabled")] -def calculate_total_purchase_cost(project: str | None = None): +def get_total_purchase_cost(project: str | None = None): if project: pitem = qb.DocType("Purchase Invoice Item") total_purchase_cost = ( @@ -739,16 +745,42 @@ def calculate_total_purchase_cost(project: str | None = None): .run(as_list=True) ) return total_purchase_cost - return None + return 0.0 @frappe.whitelist() def recalculate_project_total_purchase_cost(project: str | None = None): if project: - total_purchase_cost = calculate_total_purchase_cost(project) + total_purchase_cost = get_total_purchase_cost(project) frappe.db.set_value( "Project", project, "total_purchase_cost", (total_purchase_cost and total_purchase_cost[0][0] or 0), ) + + +def get_total_ordered_cost(project: str | None = None): + if project: + pitem = qb.DocType("Purchase Order Item") + frappe.qb.DocType("Purchase Order Item") + total_ordered_cost = ( + qb.from_(pitem) + .select(Sum(pitem.base_net_amount)) + .where((pitem.project == project) & (pitem.docstatus == 1)) + .run(as_list=True) + ) + return total_ordered_cost + return 0.0 + + +@frappe.whitelist() +def recalculate_project_total_ordered_cost(project: str | None = None): + if project: + total_ordered_cost = get_total_ordered_cost(project) + frappe.db.set_value( + "Project", + project, + "total_ordered_cost", + (total_ordered_cost and total_ordered_cost[0][0] or 0), + )