From 66bf0374c62303f80b0dec0b471679b14e7f9119 Mon Sep 17 00:00:00 2001 From: Florian HENRY Date: Thu, 2 May 2024 23:05:12 +0200 Subject: [PATCH 1/9] feat: add Total ordered cost on Project --- .../doctype/purchase_order/purchase_order.py | 21 ++++++++++ .../purchase_order/test_purchase_order.py | 38 +++++++++++++++++++ erpnext/projects/doctype/project/project.js | 23 +++++++++++ erpnext/projects/doctype/project/project.json | 9 ++++- erpnext/projects/doctype/project/project.py | 34 ++++++++++++++++- 5 files changed, 123 insertions(+), 2 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 13f1f3b8757a..1e8c40934d94 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() @@ -500,6 +503,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() @@ -646,6 +652,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) + @frappe.request_cache def item_last_purchase_rate(name, conversion_rate, item_code, conversion_factor=1.0): diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index 15b7fa1143a7..f21ef044caae 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 @@ -1180,6 +1181,43 @@ 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", {"project_name": "_Test Project for Ordered"}) + + existing_ordered_cost = frappe.db.sql( + f"""select sum(base_net_amount) + from `tabPurchase Order Item` + where project = '{project.name}' + and docstatus=1""" + ) + existing_ordered_cost = existing_ordered_cost and existing_ordered_cost[0][0] or 0 + + pi = make_purchase_order(currency="USD", conversion_rate=60, project=project.name) + self.assertEqual( + frappe.db.get_value("Project", project.name, "total_ordered_cost"), + existing_ordered_cost + 15000, + ) + + pi1 = make_purchase_order(qty=10, project=project.name) + self.assertEqual( + frappe.db.get_value("Project", project.name, "total_ordered_cost"), + existing_ordered_cost + 15500, + ) + + pi1.cancel() + self.assertEqual( + frappe.db.get_value("Project", project.name, "total_ordered_cost"), + existing_ordered_cost + 15000, + ) + + pi.cancel() + self.assertEqual( + frappe.db.get_value("Project", project.name, "total_ordered_cost"), existing_ordered_cost + ) + def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier diff --git a/erpnext/projects/doctype/project/project.js b/erpnext/projects/doctype/project/project.js index 49e8d8486a5c..1cf351299190 100644 --- a/erpnext/projects/doctype/project/project.js +++ b/erpnext/projects/doctype/project/project.js @@ -86,6 +86,14 @@ frappe.ui.form.on("Project", { __("Actions") ); + frm.add_custom_button( + __("Update Total Ordered Cost"), + () => { + frm.events.update_total_ordered_cost(frm); + }, + __("Actions") + ); + frm.trigger("set_project_status_button"); if (frappe.model.can_read("Task")) { @@ -135,6 +143,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 ff54fd084d0a..5b4e607870f2 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] @@ -289,6 +290,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() @@ -296,7 +298,7 @@ def update_costing(self): def calculate_gross_margin(self): expense_amount = ( flt(self.total_costing_amount) - + flt(self.total_purchase_cost) + - flt(self.total_purchase_cost) + flt(self.get("total_consumed_material_cost", 0)) ) @@ -308,6 +310,10 @@ def update_purchase_costing(self): total_purchase_cost = calculate_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 = calculate_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) @@ -766,3 +772,29 @@ def recalculate_project_total_purchase_cost(project: str | None = None): "total_purchase_cost", (total_purchase_cost and total_purchase_cost[0][0] or 0), ) + + +def calculate_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 None + + +@frappe.whitelist() +def recalculate_project_total_ordered_cost(project: str | None = None): + if project: + total_ordered_cost = calculate_total_ordered_cost(project) + frappe.db.set_value( + "Project", + project, + "total_ordered_cost", + (total_ordered_cost and total_ordered_cost[0][0] or 0), + ) From dd41e17a76266339e8a573de79478ff681a1bc05 Mon Sep 17 00:00:00 2001 From: Florian HENRY Date: Thu, 2 May 2024 23:11:07 +0200 Subject: [PATCH 2/9] feat: add Total ordered cost on Project --- erpnext/projects/doctype/project/project.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/projects/doctype/project/project.js b/erpnext/projects/doctype/project/project.js index 1cf351299190..dd12fcd18db9 100644 --- a/erpnext/projects/doctype/project/project.js +++ b/erpnext/projects/doctype/project/project.js @@ -79,17 +79,17 @@ frappe.ui.form.on("Project", { ); frm.add_custom_button( - __("Update Total Purchase Cost"), + __("Update Total Ordered Cost"), () => { - frm.events.update_total_purchase_cost(frm); + frm.events.update_total_ordered_cost(frm); }, __("Actions") ); frm.add_custom_button( - __("Update Total Ordered Cost"), + __("Update Total Purchase Cost"), () => { - frm.events.update_total_ordered_cost(frm); + frm.events.update_total_purchase_cost(frm); }, __("Actions") ); From aa7e84259da4efe9a6d5c30236249d4bff151e64 Mon Sep 17 00:00:00 2001 From: Florian HENRY Date: Thu, 2 May 2024 23:14:10 +0200 Subject: [PATCH 3/9] feat: add Total ordered cost on Project --- erpnext/projects/doctype/project/project.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py index 5b4e607870f2..50b853dcbd5a 100644 --- a/erpnext/projects/doctype/project/project.py +++ b/erpnext/projects/doctype/project/project.py @@ -298,7 +298,7 @@ def update_costing(self): def calculate_gross_margin(self): expense_amount = ( flt(self.total_costing_amount) - - flt(self.total_purchase_cost) + + flt(self.total_purchase_cost) + flt(self.get("total_consumed_material_cost", 0)) ) From 8efd6da486c58c41c082404dc5a1aafad5856d1c Mon Sep 17 00:00:00 2001 From: Florian HENRY Date: Thu, 2 May 2024 23:44:07 +0200 Subject: [PATCH 4/9] feat: add Total ordered cost on Project --- erpnext/buying/doctype/purchase_order/test_purchase_order.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index f21ef044caae..384284752c3a 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -1195,13 +1195,13 @@ def test_total_ordered_cost_for_project(self): ) existing_ordered_cost = existing_ordered_cost and existing_ordered_cost[0][0] or 0 - pi = make_purchase_order(currency="USD", conversion_rate=60, project=project.name) + pi = create_purchase_order(currency="USD", conversion_rate=60, project=project.name) self.assertEqual( frappe.db.get_value("Project", project.name, "total_ordered_cost"), existing_ordered_cost + 15000, ) - pi1 = make_purchase_order(qty=10, project=project.name) + pi1 = create_purchase_order(qty=10, project=project.name) self.assertEqual( frappe.db.get_value("Project", project.name, "total_ordered_cost"), existing_ordered_cost + 15500, From 43644fd4c752d79a96ccb8055d5ce0262c95f168 Mon Sep 17 00:00:00 2001 From: Florian HENRY Date: Thu, 2 May 2024 23:48:10 +0200 Subject: [PATCH 5/9] feat: add Total ordered cost on Project --- erpnext/buying/doctype/purchase_order/test_purchase_order.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index 384284752c3a..1bee4ff32136 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -1321,6 +1321,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, }, ) From 3dc10ed3503f7dbd588af5c43aa4ef11aa08ef84 Mon Sep 17 00:00:00 2001 From: Florian HENRY Date: Fri, 3 May 2024 00:12:30 +0200 Subject: [PATCH 6/9] feat: add Total ordered cost on Project --- .../purchase_order/test_purchase_order.py | 22 +++++-------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index 1bee4ff32136..724c6fcc9da2 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -1187,36 +1187,26 @@ def test_total_ordered_cost_for_project(self): else: project = frappe.get_doc("Project", {"project_name": "_Test Project for Ordered"}) - existing_ordered_cost = frappe.db.sql( - f"""select sum(base_net_amount) - from `tabPurchase Order Item` - where project = '{project.name}' - and docstatus=1""" - ) - existing_ordered_cost = existing_ordered_cost and existing_ordered_cost[0][0] or 0 - - pi = create_purchase_order(currency="USD", conversion_rate=60, project=project.name) + pi = create_purchase_order(currency="USD", rate=500, project=project.name) self.assertEqual( frappe.db.get_value("Project", project.name, "total_ordered_cost"), - existing_ordered_cost + 15000, + 417300.0, ) - pi1 = create_purchase_order(qty=10, project=project.name) + pi1 = create_purchase_order(qty=10, currency="USD", rate=500, project=project.name) self.assertEqual( frappe.db.get_value("Project", project.name, "total_ordered_cost"), - existing_ordered_cost + 15500, + 834600.0, ) pi1.cancel() self.assertEqual( frappe.db.get_value("Project", project.name, "total_ordered_cost"), - existing_ordered_cost + 15000, + 417300.0, ) pi.cancel() - self.assertEqual( - frappe.db.get_value("Project", project.name, "total_ordered_cost"), existing_ordered_cost - ) + self.assertEqual(frappe.db.get_value("Project", project.name, "total_ordered_cost"), 0) def prepare_data_for_internal_transfer(): From 25bdcd7801331fcbcd9a89734a0f92457d258940 Mon Sep 17 00:00:00 2001 From: Florian HENRY Date: Mon, 6 May 2024 19:43:54 +0200 Subject: [PATCH 7/9] feat: add Total ordered cost on Project --- .../doctype/purchase_order/test_purchase_order.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index 724c6fcc9da2..dc390d340ddc 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -1185,24 +1185,24 @@ 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", {"project_name": "_Test Project for Ordered"}) + project = frappe.get_doc("Project", "_Test Project for Ordered") - pi = create_purchase_order(currency="USD", rate=500, project=project.name) + pi = create_purchase_order(rate=500, project=project.name) self.assertEqual( frappe.db.get_value("Project", project.name, "total_ordered_cost"), - 417300.0, + 5000.0, ) - pi1 = create_purchase_order(qty=10, currency="USD", rate=500, project=project.name) + pi1 = create_purchase_order(qty=10, rate=500, project=project.name) self.assertEqual( frappe.db.get_value("Project", project.name, "total_ordered_cost"), - 834600.0, + 50000.0, ) pi1.cancel() self.assertEqual( frappe.db.get_value("Project", project.name, "total_ordered_cost"), - 417300.0, + 5000.0, ) pi.cancel() From 7aebc775f51673b443a39002b76b70c14b5cf166 Mon Sep 17 00:00:00 2001 From: Florian HENRY Date: Mon, 6 May 2024 19:45:51 +0200 Subject: [PATCH 8/9] feat: add Total ordered cost on Project --- .../buying/doctype/purchase_order/test_purchase_order.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index 2ed6c92aea75..d93e8c3670ea 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -1166,22 +1166,22 @@ def test_total_ordered_cost_for_project(self): else: project = frappe.get_doc("Project", "_Test Project for Ordered") - pi = create_purchase_order(rate=500, project=project.name) + pi = create_purchase_order(qty=1, rate=500, project=project.name) self.assertEqual( frappe.db.get_value("Project", project.name, "total_ordered_cost"), - 5000.0, + 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"), - 50000.0, + 5500.0, ) pi1.cancel() self.assertEqual( frappe.db.get_value("Project", project.name, "total_ordered_cost"), - 5000.0, + 500.0, ) pi.cancel() From 45c288fd5103f1c69c28f8d0b033677bb90fbc03 Mon Sep 17 00:00:00 2001 From: Florian HENRY Date: Sat, 16 Nov 2024 12:38:49 +0100 Subject: [PATCH 9/9] chore: Rename function name and change return type in project.py purchase and cost calculation --- erpnext/projects/doctype/project/project.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py index 7c7519b87754..cedbc94bfaeb 100644 --- a/erpnext/projects/doctype/project/project.py +++ b/erpnext/projects/doctype/project/project.py @@ -313,11 +313,11 @@ 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 = calculate_total_ordered_cost(self.name) + 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): @@ -735,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 = ( @@ -745,13 +745,13 @@ 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, @@ -760,7 +760,7 @@ def recalculate_project_total_purchase_cost(project: str | None = None): ) -def calculate_total_ordered_cost(project: str | None = None): +def get_total_ordered_cost(project: str | None = None): if project: pitem = qb.DocType("Purchase Order Item") frappe.qb.DocType("Purchase Order Item") @@ -771,13 +771,13 @@ def calculate_total_ordered_cost(project: str | None = None): .run(as_list=True) ) return total_ordered_cost - return None + return 0.0 @frappe.whitelist() def recalculate_project_total_ordered_cost(project: str | None = None): if project: - total_ordered_cost = calculate_total_ordered_cost(project) + total_ordered_cost = get_total_ordered_cost(project) frappe.db.set_value( "Project", project,