From 2bf10f68a85fd3256fc93815e51eea311aa771c7 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Wed, 15 Jan 2025 18:14:19 +0530 Subject: [PATCH 1/4] feat: Add corrective job card operating cost as additional costs in stock entry --- erpnext/manufacturing/doctype/bom/bom.py | 49 +++++++++++++++++++ .../doctype/job_card/job_card.py | 6 ++- 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 3e4c507e8151..000ceb6a13c0 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -1433,6 +1433,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 b654127f1e20..7ef58e0d456a 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -670,7 +670,11 @@ def get_required_items(self): ) ) - if self.get("operation") == d.operation or self.operation_row_id == d.operation_row_id: + if ( + self.get("operation") == d.operation + or self.operation_row_id == d.operation_row_id + or self.is_corrective_job_card + ): self.append( "items", { From 4fb48b7f226ecd6c5a9a23511584a1953831c1fd Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Thu, 16 Jan 2025 10:08:47 +0530 Subject: [PATCH 2/4] test: Added test for new feature --- .../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 618d7dd3f8f4..34e91bbecd6f 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -442,6 +442,46 @@ def test_corrective_costing(self): 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 063a205e5a29f48457a0a8ad248b0788dbeda5a6 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Thu, 16 Jan 2025 20:52:44 +0530 Subject: [PATCH 3/4] refactor: added condition which checks for corrective operation setting --- 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 000ceb6a13c0..a1194811f0fe 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -1470,7 +1470,15 @@ def get_utilised_cc(): ) 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 34e91bbecd6f..1c85681f5b1b 100644 --- a/erpnext/manufacturing/doctype/job_card/test_job_card.py +++ b/erpnext/manufacturing/doctype/job_card/test_job_card.py @@ -446,10 +446,18 @@ def test_corrective_costing(self): "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() @@ -467,20 +475,56 @@ def test_if_corrective_jc_ops_cost_is_added_to_manufacture_stock_entry(self): { "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 5e4343add100..13777800d858 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -2892,17 +2892,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 47f8a8600374a6a130c45c12e65f1cf11fa450e0 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Mon, 20 Jan 2025 12:41:28 +0530 Subject: [PATCH 4/4] fix: logical error in where condition of qb query --- erpnext/manufacturing/doctype/bom/bom.py | 3 ++- .../landed_cost_taxes_and_charges.json | 12 ++++++++++-- .../landed_cost_taxes_and_charges.py | 1 + 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index a1194811f0fe..79fe1d5b0f06 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -1466,7 +1466,7 @@ def get_utilised_cc(): 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 @@ -1486,6 +1486,7 @@ def get_utilised_cc(): { "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 df5c0f9e91c6..57328772e136 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,19 @@ "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": [], - "modified": "2024-03-27 13:09:59.493991", + "modified": "2025-01-20 12:22:03.455762", "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 8509cb71d853..a3f7f037d607 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