From efc7b9ac56545d236d3069a8911acd1216036b96 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Wed, 15 Jan 2025 18:14:19 +0530 Subject: [PATCH 1/9] feat: Add corrective job card operating cost as additional costs in stock entry (cherry picked from commit 2bf10f68a85fd3256fc93815e51eea311aa771c7) # Conflicts: # erpnext/manufacturing/doctype/job_card/job_card.py --- erpnext/manufacturing/doctype/bom/bom.py | 49 +++++++++++++++++++ .../doctype/job_card/job_card.py | 8 +++ 2 files changed, 57 insertions(+) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 6b2dd77c471..f0f9b0c354c 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -1371,6 +1371,55 @@ def add_operations_cost(stock_entry, work_order=None, expense_account=None): }, ) + def get_max_op_qty(): + from frappe.query_builder.functions import Sum + + table = frappe.qb.DocType("Job Card") + query = ( + frappe.qb.from_(table) + .select(Sum(table.total_completed_qty).as_("qty")) + .where( + (table.docstatus == 1) + & (table.work_order == work_order.name) + & (table.is_corrective_job_card == 0) + ) + .groupby(table.operation) + ) + return min([d.qty for d in query.run(as_dict=True)], default=0) + + def get_utilised_cc(): + from frappe.query_builder.functions import Sum + + table = frappe.qb.DocType("Stock Entry") + subquery = ( + frappe.qb.from_(table) + .select(table.name) + .where( + (table.docstatus == 1) + & (table.work_order == work_order.name) + & (table.purpose == "Manufacture") + ) + ) + table = frappe.qb.DocType("Landed Cost Taxes and Charges") + query = ( + frappe.qb.from_(table) + .select(Sum(table.amount).as_("amount")) + .where(table.parent.isin(subquery) & (table.description == "Corrective Operation Cost")) + ) + return query.run(as_dict=True)[0].amount or 0 + + if work_order and work_order.corrective_operation_cost: + max_qty = get_max_op_qty() - work_order.produced_qty + remaining_cc = work_order.corrective_operation_cost - get_utilised_cc() + stock_entry.append( + "additional_costs", + { + "expense_account": expense_account, + "description": "Corrective Operation Cost", + "amount": remaining_cc / max_qty * flt(stock_entry.fg_completed_qty), + }, + ) + @frappe.whitelist() def get_bom_diff(bom1, bom2): diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index a1b53fb7c4a..893c698ff5f 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -650,7 +650,15 @@ class JobCard(Document): ) ) +<<<<<<< HEAD if self.get("operation") == d.operation: +======= + if ( + self.get("operation") == d.operation + or self.operation_row_id == d.operation_row_id + or self.is_corrective_job_card + ): +>>>>>>> 2bf10f68a8 (feat: Add corrective job card operating cost as additional costs in stock entry) self.append( "items", { From 5c9ac274783d4f169897c3838cfad56430ddfbaf Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Thu, 16 Jan 2025 10:08:47 +0530 Subject: [PATCH 2/9] test: Added test for new feature (cherry picked from commit 4fb48b7f226ecd6c5a9a23511584a1953831c1fd) --- .../doctype/job_card/test_job_card.py | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index d6d3775111d..58ed1d5a4bb 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -424,6 +424,46 @@ class TestJobCard(FrappeTestCase): cost_after_cancel = self.work_order.total_operating_cost self.assertEqual(cost_after_cancel, original_cost) + @IntegrationTestCase.change_settings( + "Manufacturing Settings", {"add_corrective_operation_cost_in_finished_good_valuation": 1} + ) + def test_if_corrective_jc_ops_cost_is_added_to_manufacture_stock_entry(self): + job_card = frappe.get_last_doc("Job Card", {"work_order": self.work_order.name}) + job_card.append( + "time_logs", + {"from_time": now(), "to_time": add_to_date(now(), hours=1), "completed_qty": 2}, + ) + job_card.submit() + + corrective_action = frappe.get_doc( + doctype="Operation", is_corrective_operation=1, name=frappe.generate_hash() + ).insert() + + corrective_job_card = make_corrective_job_card( + job_card.name, operation=corrective_action.name, for_operation=job_card.operation + ) + corrective_job_card.hour_rate = 100 + corrective_job_card.insert() + corrective_job_card.append( + "time_logs", + { + "from_time": add_to_date(now(), hours=2), + "to_time": add_to_date(now(), hours=2, minutes=30), + "completed_qty": 2, + }, + ) + corrective_job_card.submit() + self.work_order.reload() + + from erpnext.manufacturing.doctype.work_order.work_order import ( + make_stock_entry as make_stock_entry_for_wo, + ) + + stock_entry = make_stock_entry_for_wo(self.work_order.name, "Manufacture") + self.assertEqual(stock_entry.additional_costs[1].description, "Corrective Operation Cost") + self.assertEqual(stock_entry.additional_costs[1].amount, 50) + self.assertEqual(stock_entry["items"][-1].additional_cost, 6050) + def test_job_card_statuses(self): def assertStatus(status): jc.set_status() From d6e0c6c96954bbb40f413cf75b593c7148a054e2 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Thu, 16 Jan 2025 20:52:44 +0530 Subject: [PATCH 3/9] refactor: added condition which checks for corrective operation setting (cherry picked from commit 063a205e5a29f48457a0a8ad248b0788dbeda5a6) --- erpnext/manufacturing/doctype/bom/bom.py | 10 +++- .../doctype/job_card/test_job_card.py | 60 ++++++++++++++++--- .../stock/doctype/stock_entry/stock_entry.py | 11 ---- 3 files changed, 61 insertions(+), 20 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index f0f9b0c354c..53ec6fc1e8b 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -1408,7 +1408,15 @@ def add_operations_cost(stock_entry, work_order=None, expense_account=None): ) return query.run(as_dict=True)[0].amount or 0 - if work_order and work_order.corrective_operation_cost: + if ( + work_order + and work_order.corrective_operation_cost + and cint( + frappe.db.get_single_value( + "Manufacturing Settings", "add_corrective_operation_cost_in_finished_good_valuation" + ) + ) + ): max_qty = get_max_op_qty() - work_order.produced_qty remaining_cc = work_order.corrective_operation_cost - get_utilised_cc() stock_entry.append( diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index 58ed1d5a4bb..03c605ce156 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -428,10 +428,18 @@ class TestJobCard(FrappeTestCase): "Manufacturing Settings", {"add_corrective_operation_cost_in_finished_good_valuation": 1} ) def test_if_corrective_jc_ops_cost_is_added_to_manufacture_stock_entry(self): - job_card = frappe.get_last_doc("Job Card", {"work_order": self.work_order.name}) + wo = make_wo_order_test_record( + item="_Test FG Item 2", + qty=10, + transfer_material_against=self.transfer_material_against, + source_warehouse=self.source_warehouse, + ) + self.generate_required_stock(wo) + job_card = frappe.get_last_doc("Job Card", {"work_order": wo.name}) + job_card.update({"for_quantity": 4}) job_card.append( "time_logs", - {"from_time": now(), "to_time": add_to_date(now(), hours=1), "completed_qty": 2}, + {"from_time": now(), "to_time": add_to_date(now(), hours=1), "completed_qty": 4}, ) job_card.submit() @@ -449,20 +457,56 @@ class TestJobCard(FrappeTestCase): { "from_time": add_to_date(now(), hours=2), "to_time": add_to_date(now(), hours=2, minutes=30), - "completed_qty": 2, + "completed_qty": 4, }, ) corrective_job_card.submit() - self.work_order.reload() + wo.reload() from erpnext.manufacturing.doctype.work_order.work_order import ( make_stock_entry as make_stock_entry_for_wo, ) - stock_entry = make_stock_entry_for_wo(self.work_order.name, "Manufacture") - self.assertEqual(stock_entry.additional_costs[1].description, "Corrective Operation Cost") - self.assertEqual(stock_entry.additional_costs[1].amount, 50) - self.assertEqual(stock_entry["items"][-1].additional_cost, 6050) + stock_entry = make_stock_entry_for_wo(wo.name, "Manufacture", qty=3) + self.assertEqual(stock_entry.additional_costs[1].amount, 37.5) + frappe.get_doc(stock_entry).submit() + + from erpnext.manufacturing.doctype.work_order.work_order import make_job_card + + make_job_card( + wo.name, + [{"name": wo.operations[0].name, "operation": "_Test Operation 1", "qty": 3, "pending_qty": 3}], + ) + job_card = frappe.get_last_doc("Job Card", {"work_order": wo.name}) + job_card.update({"for_quantity": 3}) + job_card.append( + "time_logs", + { + "from_time": add_to_date(now(), hours=3), + "to_time": add_to_date(now(), hours=4), + "completed_qty": 3, + }, + ) + job_card.submit() + + corrective_job_card = make_corrective_job_card( + job_card.name, operation=corrective_action.name, for_operation=job_card.operation + ) + corrective_job_card.hour_rate = 80 + corrective_job_card.insert() + corrective_job_card.append( + "time_logs", + { + "from_time": add_to_date(now(), hours=4), + "to_time": add_to_date(now(), hours=4, minutes=30), + "completed_qty": 3, + }, + ) + corrective_job_card.submit() + wo.reload() + + stock_entry = make_stock_entry_for_wo(wo.name, "Manufacture", qty=4) + self.assertEqual(stock_entry.additional_costs[1].amount, 52.5) def test_job_card_statuses(self): def assertStatus(status): diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index a2da35c0ef2..eec30f69f36 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -2868,17 +2868,6 @@ def get_operating_cost_per_unit(work_order=None, bom_no=None): if bom.quantity: operating_cost_per_unit = flt(bom.operating_cost) / flt(bom.quantity) - if ( - work_order - and work_order.produced_qty - and cint( - frappe.db.get_single_value( - "Manufacturing Settings", "add_corrective_operation_cost_in_finished_good_valuation" - ) - ) - ): - operating_cost_per_unit += flt(work_order.corrective_operation_cost) / flt(work_order.produced_qty) - return operating_cost_per_unit From c102e51eb18748b60f5793b23bd6dfd35b66835b Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Mon, 20 Jan 2025 12:41:28 +0530 Subject: [PATCH 4/9] fix: logical error in where condition of qb query (cherry picked from commit 47f8a8600374a6a130c45c12e65f1cf11fa450e0) # Conflicts: # erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.json --- erpnext/manufacturing/doctype/bom/bom.py | 3 ++- .../landed_cost_taxes_and_charges.json | 14 +++++++++++++- .../landed_cost_taxes_and_charges.py | 1 + 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 53ec6fc1e8b..5d13471f541 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -1404,7 +1404,7 @@ def add_operations_cost(stock_entry, work_order=None, expense_account=None): query = ( frappe.qb.from_(table) .select(Sum(table.amount).as_("amount")) - .where(table.parent.isin(subquery) & (table.description == "Corrective Operation Cost")) + .where(table.parent.isin(subquery) & (table.has_corrective_cost == 1)) ) return query.run(as_dict=True)[0].amount or 0 @@ -1424,6 +1424,7 @@ def add_operations_cost(stock_entry, work_order=None, expense_account=None): { "expense_account": expense_account, "description": "Corrective Operation Cost", + "has_corrective_cost": 1, "amount": remaining_cc / max_qty * flt(stock_entry.fg_completed_qty), }, ) diff --git a/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.json b/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.json index 9c59c13ac07..2d5823d3d51 100644 --- a/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.json +++ b/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.json @@ -11,7 +11,8 @@ "description", "col_break3", "amount", - "base_amount" + "base_amount", + "has_corrective_cost" ], "fields": [ { @@ -62,12 +63,23 @@ "label": "Amount (Company Currency)", "options": "Company:company:default_currency", "read_only": 1 + }, + { + "default": "0", + "fieldname": "has_corrective_cost", + "fieldtype": "Check", + "label": "Has Corrective Cost", + "read_only": 1 } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], +<<<<<<< HEAD "modified": "2021-05-17 13:57:10.807980", +======= + "modified": "2025-01-20 12:22:03.455762", +>>>>>>> 47f8a86003 (fix: logical error in where condition of qb query) "modified_by": "Administrator", "module": "Stock", "name": "Landed Cost Taxes and Charges", diff --git a/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.py b/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.py index 8509cb71d85..a3f7f037d60 100644 --- a/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.py +++ b/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.py @@ -20,6 +20,7 @@ class LandedCostTaxesandCharges(Document): description: DF.SmallText exchange_rate: DF.Float expense_account: DF.Link | None + has_corrective_cost: DF.Check parent: DF.Data parentfield: DF.Data parenttype: DF.Data From 57f79a22401ec1ffadb8ae4a7dddea5236e213db Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Mon, 27 Jan 2025 22:01:13 +0530 Subject: [PATCH 5/9] fix: merge conflict --- erpnext/manufacturing/doctype/job_card/job_card.py | 4 ---- .../landed_cost_taxes_and_charges.json | 4 ---- 2 files changed, 8 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index 893c698ff5f..50bec328620 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -650,15 +650,11 @@ class JobCard(Document): ) ) -<<<<<<< HEAD - if self.get("operation") == d.operation: -======= if ( self.get("operation") == d.operation or self.operation_row_id == d.operation_row_id or self.is_corrective_job_card ): ->>>>>>> 2bf10f68a8 (feat: Add corrective job card operating cost as additional costs in stock entry) self.append( "items", { diff --git a/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.json b/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.json index 2d5823d3d51..898848ebf42 100644 --- a/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.json +++ b/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.json @@ -75,11 +75,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], -<<<<<<< HEAD - "modified": "2021-05-17 13:57:10.807980", -======= "modified": "2025-01-20 12:22:03.455762", ->>>>>>> 47f8a86003 (fix: logical error in where condition of qb query) "modified_by": "Administrator", "module": "Stock", "name": "Landed Cost Taxes and Charges", From d74c498efecbe3ff80d20782192f046be68c36d5 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Mon, 27 Jan 2025 22:17:13 +0530 Subject: [PATCH 6/9] fix: import --- erpnext/manufacturing/doctype/job_card/test_job_card.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index 03c605ce156..31864c9262c 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -6,6 +6,7 @@ from typing import Literal import frappe from frappe.test_runner import make_test_records +from frappe.tests import IntegrationTestCase from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import random_string from frappe.utils.data import add_to_date, now, today From b59d253d93fa521994c22c81600e7e7e9c8ee6ff Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Mon, 27 Jan 2025 22:55:33 +0530 Subject: [PATCH 7/9] fix: import 2 --- erpnext/manufacturing/doctype/job_card/test_job_card.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index 31864c9262c..5e381bd06ff 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -6,7 +6,6 @@ from typing import Literal import frappe from frappe.test_runner import make_test_records -from frappe.tests import IntegrationTestCase from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import random_string from frappe.utils.data import add_to_date, now, today @@ -425,7 +424,7 @@ class TestJobCard(FrappeTestCase): cost_after_cancel = self.work_order.total_operating_cost self.assertEqual(cost_after_cancel, original_cost) - @IntegrationTestCase.change_settings( + @change_settings( "Manufacturing Settings", {"add_corrective_operation_cost_in_finished_good_valuation": 1} ) def test_if_corrective_jc_ops_cost_is_added_to_manufacture_stock_entry(self): From 1be19819fbc79482004e53c15567ef3ae38b4966 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 28 Jan 2025 09:26:14 +0530 Subject: [PATCH 8/9] fix: removed field not present in v15 --- erpnext/manufacturing/doctype/job_card/job_card.py | 6 +----- erpnext/manufacturing/doctype/job_card/test_job_card.py | 2 ++ 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index 50bec328620..0f0694e33b0 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -650,11 +650,7 @@ class JobCard(Document): ) ) - if ( - self.get("operation") == d.operation - or self.operation_row_id == d.operation_row_id - or self.is_corrective_job_card - ): + if self.get("operation") == d.operation or self.is_corrective_job_card: self.append( "items", { diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index 5e381bd06ff..0119e74cb4c 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -451,6 +451,7 @@ class TestJobCard(FrappeTestCase): job_card.name, operation=corrective_action.name, for_operation=job_card.operation ) corrective_job_card.hour_rate = 100 + corrective_job_card.update({"hour_rate": 100}) corrective_job_card.insert() corrective_job_card.append( "time_logs", @@ -460,6 +461,7 @@ class TestJobCard(FrappeTestCase): "completed_qty": 4, }, ) + print(corrective_job_card.as_dict()) corrective_job_card.submit() wo.reload() From 6c4655dd72dc304d07b2b23a26e7f61eb1ff00b9 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 28 Jan 2025 16:56:05 +0530 Subject: [PATCH 9/9] fix: existing logical error --- erpnext/manufacturing/doctype/job_card/job_card.py | 2 +- erpnext/manufacturing/doctype/job_card/test_job_card.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index 0f0694e33b0..90f915d9c24 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -791,7 +791,7 @@ class JobCard(Document): fields=["total_time_in_mins", "hour_rate"], filters={"is_corrective_job_card": 1, "docstatus": 1, "work_order": self.work_order}, ): - wo.corrective_operation_cost += flt(row.total_time_in_mins) * flt(row.hour_rate) + wo.corrective_operation_cost += (flt(row.total_time_in_mins) / 60) * flt(row.hour_rate) wo.calculate_operating_cost() wo.flags.ignore_validate_update_after_submit = True diff --git a/erpnext/manufacturing/doctype/job_card/test_job_card.py b/erpnext/manufacturing/doctype/job_card/test_job_card.py index 0119e74cb4c..7f456b9881e 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -451,7 +451,6 @@ class TestJobCard(FrappeTestCase): job_card.name, operation=corrective_action.name, for_operation=job_card.operation ) corrective_job_card.hour_rate = 100 - corrective_job_card.update({"hour_rate": 100}) corrective_job_card.insert() corrective_job_card.append( "time_logs", @@ -461,7 +460,6 @@ class TestJobCard(FrappeTestCase): "completed_qty": 4, }, ) - print(corrective_job_card.as_dict()) corrective_job_card.submit() wo.reload() @@ -479,8 +477,10 @@ class TestJobCard(FrappeTestCase): wo.name, [{"name": wo.operations[0].name, "operation": "_Test Operation 1", "qty": 3, "pending_qty": 3}], ) + workstation = job_card.workstation job_card = frappe.get_last_doc("Job Card", {"work_order": wo.name}) job_card.update({"for_quantity": 3}) + job_card.workstation = workstation job_card.append( "time_logs", {