From 5e2669f4b6e8ed917cc8e882b49a4a617a854464 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Wed, 3 Jan 2024 21:43:38 +0530 Subject: [PATCH 1/9] feat: provision to close SCO --- .../subcontracting_order.js | 23 +++++++++++++++++++ .../subcontracting_order.json | 4 ++-- .../subcontracting_order.py | 5 ++-- 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js index 587a3b4ebf..ae9a3252ef 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js @@ -101,9 +101,32 @@ frappe.ui.form.on('Subcontracting Order', { }, refresh: function (frm) { + if (frm.has_perm("submit")) { + if (frm.doc.status == "Closed") { + frm.add_custom_button(__('Re-open'), () => frm.events.update_subcontracting_order_status(frm), __("Status")); + } else if(flt(frm.doc.per_received, 2) < 100) { + frm.add_custom_button(__('Close'), () => frm.events.update_subcontracting_order_status(frm, "Closed"), __("Status")); + } + } + frm.trigger('get_materials_from_supplier'); }, + update_subcontracting_order_status(frm, status) { + frappe.call({ + method: "erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order.update_subcontracting_order_status", + args: { + sco: frm.doc.name, + status: status, + }, + callback: function (r) { + if (!r.exc) { + frm.reload_doc(); + } + }, + }); + }, + get_materials_from_supplier: function (frm) { let sco_rm_details = []; diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.json b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.json index 28c52c9272..507e23365c 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.json +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.json @@ -370,7 +370,7 @@ "in_standard_filter": 1, "label": "Status", "no_copy": 1, - "options": "Draft\nOpen\nPartially Received\nCompleted\nMaterial Transferred\nPartial Material Transferred\nCancelled", + "options": "Draft\nOpen\nPartially Received\nCompleted\nMaterial Transferred\nPartial Material Transferred\nCancelled\nClosed", "print_hide": 1, "read_only": 1, "reqd": 1, @@ -454,7 +454,7 @@ "icon": "fa fa-file-text", "is_submittable": 1, "links": [], - "modified": "2023-06-03 16:18:17.782538", + "modified": "2024-01-03 20:56:04.670380", "modified_by": "Administrator", "module": "Subcontracting", "name": "Subcontracting Order", diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py index 0fe8c13efb..6690244325 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py @@ -68,6 +68,7 @@ class SubcontractingOrder(SubcontractingController): "Material Transferred", "Partial Material Transferred", "Cancelled", + "Closed", ] supplied_items: DF.Table[SubcontractingOrderSuppliedItem] supplier: DF.Link @@ -357,8 +358,8 @@ def get_mapped_subcontracting_receipt(source_name, target_doc=None): @frappe.whitelist() -def update_subcontracting_order_status(sco): +def update_subcontracting_order_status(sco, status=None): if isinstance(sco, str): sco = frappe.get_doc("Subcontracting Order", sco) - sco.update_status() + sco.update_status(status) From 9e973476b2a925d4557b3a6b17517e46d652ac20 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Wed, 3 Jan 2024 22:13:37 +0530 Subject: [PATCH 2/9] fix: don't allow to submit/cancel SCR against a closed SCO --- .../subcontracting_receipt/subcontracting_receipt.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py index 52bf13c78d..7c2a1f12e2 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -8,6 +8,7 @@ from frappe.utils import cint, flt, get_link_to_form, getdate, nowdate import erpnext from erpnext.accounts.utils import get_account_currency +from erpnext.buying.utils import check_on_hold_or_closed_status from erpnext.controllers.subcontracting_controller import SubcontractingController from erpnext.stock.stock_ledger import get_valuation_rate @@ -142,6 +143,7 @@ class SubcontractingReceipt(SubcontractingController): self.get_current_stock() def on_submit(self): + self.validate_closed_subcontracting_order() self.validate_available_qty_for_consumption() self.update_status_updater_args() self.update_prevdoc_status() @@ -165,6 +167,7 @@ class SubcontractingReceipt(SubcontractingController): "Repost Item Valuation", "Serial and Batch Bundle", ) + self.validate_closed_subcontracting_order() self.update_status_updater_args() self.update_prevdoc_status() self.set_consumed_qty_in_subcontract_order() @@ -175,6 +178,11 @@ class SubcontractingReceipt(SubcontractingController): self.update_status() self.delete_auto_created_batches() + def validate_closed_subcontracting_order(self): + for item in self.items: + if item.subcontracting_order: + check_on_hold_or_closed_status("Subcontracting Order", item.subcontracting_order) + def validate_items_qty(self): for item in self.items: if not (item.qty or item.rejected_qty): From 5bc2035bd082001e2552138a5df55554f2e6f8de Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Wed, 3 Jan 2024 22:20:06 +0530 Subject: [PATCH 3/9] fix: don't allow to submit/cancel SE against a closed SCO --- .../stock/doctype/stock_entry/stock_entry.py | 20 ++++++------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 2ccee94078..bccbc28087 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -24,6 +24,7 @@ from frappe.utils import ( import erpnext from erpnext.accounts.general_ledger import process_gl_map +from erpnext.buying.utils import check_on_hold_or_closed_status from erpnext.controllers.taxes_and_totals import init_landed_taxes_and_totals from erpnext.manufacturing.doctype.bom.bom import ( add_additional_cost, @@ -208,7 +209,6 @@ class StockEntry(StockController): self.validate_bom() self.set_process_loss_qty() self.validate_purchase_order() - self.validate_subcontracting_order() if self.purpose in ("Manufacture", "Repack"): self.mark_finished_and_scrap_items() @@ -274,6 +274,7 @@ class StockEntry(StockController): return False def on_submit(self): + self.validate_closed_subcontracting_order() self.update_stock_ledger() self.update_work_order() self.validate_subcontract_order() @@ -294,6 +295,7 @@ class StockEntry(StockController): self.set_material_request_transfer_status("Completed") def on_cancel(self): + self.validate_closed_subcontracting_order() self.update_subcontract_order_supplied_items() self.update_subcontracting_order_status() @@ -1203,19 +1205,9 @@ class StockEntry(StockController): ) ) - def validate_subcontracting_order(self): - if self.get("subcontracting_order") and self.purpose in [ - "Send to Subcontractor", - "Material Transfer", - ]: - sco_status = frappe.db.get_value("Subcontracting Order", self.subcontracting_order, "status") - - if sco_status == "Closed": - frappe.throw( - _("Cannot create Stock Entry against a closed Subcontracting Order {0}.").format( - self.subcontracting_order - ) - ) + def validate_closed_subcontracting_order(self): + if self.get("subcontracting_order"): + check_on_hold_or_closed_status("Subcontracting Order", self.subcontracting_order) def mark_finished_and_scrap_items(self): if self.purpose != "Repack" and any( From bb839b2924c2dd4c2a06b087d7624ca7aac08f04 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Wed, 3 Jan 2024 22:22:21 +0530 Subject: [PATCH 4/9] fix(ux): filter closed SCO in `Get Items From` dialog --- .../doctype/subcontracting_order/subcontracting_order_list.js | 2 +- .../doctype/subcontracting_receipt/subcontracting_receipt.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order_list.js b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order_list.js index 7ca12642c5..ec54944a84 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order_list.js +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order_list.js @@ -10,7 +10,7 @@ frappe.listview_settings['Subcontracting Order'] = { "Completed": "green", "Partial Material Transferred": "purple", "Material Transferred": "blue", - "Closed": "red", + "Closed": "green", "Cancelled": "red", }; return [__(doc.status), status_colors[doc.status], "status,=," + doc.status]; diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js index 575c4eda73..05357999a1 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js @@ -93,7 +93,8 @@ frappe.ui.form.on('Subcontracting Receipt', { get_query_filters: { docstatus: 1, per_received: ['<', 100], - company: frm.doc.company + company: frm.doc.company, + status: ['!=', 'Closed'], } }); }, __('Get Items From')); From 0d01bd8a5a49f5bba80ef797d5823f762e0fc86a Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Thu, 4 Jan 2024 12:34:25 +0530 Subject: [PATCH 5/9] fix: don't close PO on SCO close --- .../doctype/subcontracting_order/subcontracting_order.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py index 6690244325..bc3061169a 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py @@ -7,7 +7,6 @@ from frappe.model.mapper import get_mapped_doc from frappe.utils import flt from erpnext.buying.doctype.purchase_order.purchase_order import is_subcontracting_order_created -from erpnext.buying.doctype.purchase_order.purchase_order import update_status as update_po_status from erpnext.controllers.subcontracting_controller import SubcontractingController from erpnext.stock.stock_balance import update_bin_qty from erpnext.stock.utils import get_bin @@ -310,9 +309,6 @@ class SubcontractingOrder(SubcontractingController): "Subcontracting Order", self.name, "status", status, update_modified=update_modified ) - if status == "Closed": - update_po_status("Closed", self.purchase_order) - @frappe.whitelist() def make_subcontracting_receipt(source_name, target_doc=None): From 245effcccd3866bac2974023d37cd48b89849aeb Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Thu, 4 Jan 2024 12:48:36 +0530 Subject: [PATCH 6/9] fix: update qty on SCO status change --- .../subcontracting_order/subcontracting_order.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py index bc3061169a..14f9bfd5dc 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py @@ -112,16 +112,10 @@ class SubcontractingOrder(SubcontractingController): def on_submit(self): self.update_prevdoc_status() - self.update_requested_qty() - self.update_ordered_qty_for_subcontracting() - self.update_reserved_qty_for_subcontracting() self.update_status() def on_cancel(self): self.update_prevdoc_status() - self.update_requested_qty() - self.update_ordered_qty_for_subcontracting() - self.update_reserved_qty_for_subcontracting() self.update_status() def validate_purchase_order_for_subcontracting(self): @@ -305,9 +299,11 @@ class SubcontractingOrder(SubcontractingController): status = "Cancelled" if status: - frappe.db.set_value( - "Subcontracting Order", self.name, "status", status, update_modified=update_modified - ) + self.db_set("status", status, update_modified=update_modified) + + self.update_requested_qty() + self.update_ordered_qty_for_subcontracting() + self.update_reserved_qty_for_subcontracting() @frappe.whitelist() From 784b6dcfea2aa9eb3fe557bb4f87cdc1f7341560 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Thu, 4 Jan 2024 13:05:17 +0530 Subject: [PATCH 7/9] fix: don't allow to reopen SCO if PO is closed --- .../doctype/subcontracting_order/subcontracting_order.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py index 14f9bfd5dc..3daa6335f3 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py @@ -7,6 +7,7 @@ from frappe.model.mapper import get_mapped_doc from frappe.utils import flt from erpnext.buying.doctype.purchase_order.purchase_order import is_subcontracting_order_created +from erpnext.buying.utils import check_on_hold_or_closed_status from erpnext.controllers.subcontracting_controller import SubcontractingController from erpnext.stock.stock_balance import update_bin_qty from erpnext.stock.utils import get_bin @@ -271,6 +272,9 @@ class SubcontractingOrder(SubcontractingController): self.set_missing_values() def update_status(self, status=None, update_modified=True): + if self.status == "Closed": + check_on_hold_or_closed_status("Purchase Order", self.purchase_order) + if self.docstatus >= 1 and not status: if self.docstatus == 1: if self.status == "Draft": From 0819675fce1cffe3a930fe2cbd015998e4e7c00d Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Thu, 4 Jan 2024 13:44:53 +0530 Subject: [PATCH 8/9] fix: auto close and reopen SCO based on PO status --- .../buying/doctype/purchase_order/purchase_order.py | 12 ++++++++++++ .../subcontracting_order/subcontracting_order.py | 4 ++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 2efb46e9d3..b830e7d204 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -452,6 +452,7 @@ class PurchaseOrder(BuyingController): self.update_requested_qty() self.update_ordered_qty() self.update_reserved_qty_for_subcontract() + self.update_subcontracting_order_status() self.notify_update() clear_doctype_notifications(self) @@ -627,6 +628,17 @@ class PurchaseOrder(BuyingController): if frappe.db.get_single_value("Buying Settings", "auto_create_subcontracting_order"): make_subcontracting_order(self.name, save=True, notify=True) + def update_subcontracting_order_status(self): + from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import ( + update_subcontracting_order_status as update_sco_status, + ) + + if self.is_subcontracted and not self.is_old_subcontracting_flow: + sco = frappe.db.get_value("Subcontracting Order", {"purchase_order": self.name, "docstatus": 1}) + + if sco: + update_sco_status(sco, "Closed" if self.status == "Closed" else None) + def item_last_purchase_rate(name, conversion_rate, item_code, conversion_factor=1.0): """get last purchase rate for an item""" diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py index 3daa6335f3..309e4fe643 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py @@ -272,7 +272,7 @@ class SubcontractingOrder(SubcontractingController): self.set_missing_values() def update_status(self, status=None, update_modified=True): - if self.status == "Closed": + if self.status == "Closed" and self.status != status: check_on_hold_or_closed_status("Purchase Order", self.purchase_order) if self.docstatus >= 1 and not status: @@ -302,7 +302,7 @@ class SubcontractingOrder(SubcontractingController): elif self.docstatus == 2: status = "Cancelled" - if status: + if status and self.status != status: self.db_set("status", status, update_modified=update_modified) self.update_requested_qty() From cdd5441435ee448d3efb2155a93865ec29bbf8ab Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Thu, 4 Jan 2024 15:15:55 +0530 Subject: [PATCH 9/9] fix(text): test_update_status --- .../subcontracting_order/subcontracting_order.js | 2 +- .../subcontracting_order/subcontracting_order.py | 5 ----- .../test_subcontracting_order.py | 13 ++++++------- 3 files changed, 7 insertions(+), 13 deletions(-) diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js index ae9a3252ef..4c8a0ad60e 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.js @@ -101,7 +101,7 @@ frappe.ui.form.on('Subcontracting Order', { }, refresh: function (frm) { - if (frm.has_perm("submit")) { + if (frm.doc.docstatus == 1 && frm.has_perm("submit")) { if (frm.doc.status == "Closed") { frm.add_custom_button(__('Re-open'), () => frm.events.update_subcontracting_order_status(frm), __("Status")); } else if(flt(frm.doc.per_received, 2) < 100) { diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py index 309e4fe643..daccbbbd0f 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py +++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py @@ -283,11 +283,6 @@ class SubcontractingOrder(SubcontractingController): status = "Completed" elif self.per_received > 0 and self.per_received < 100: status = "Partially Received" - for item in self.supplied_items: - if not item.returned_qty or (item.supplied_qty - item.consumed_qty - item.returned_qty) > 0: - break - else: - status = "Closed" else: total_required_qty = total_supplied_qty = 0 for item in self.supplied_items: diff --git a/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py index 37dabf1bfb..6c0ee45d9c 100644 --- a/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py +++ b/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py @@ -95,14 +95,14 @@ class TestSubcontractingOrder(FrappeTestCase): self.assertEqual(sco.status, "Partially Received") # Closed - ste = get_materials_from_supplier(sco.name, [d.name for d in sco.supplied_items]) - ste.save() - ste.submit() - sco.load_from_db() + sco.update_status("Closed") self.assertEqual(sco.status, "Closed") - ste.cancel() - sco.load_from_db() + scr = make_subcontracting_receipt(sco.name) + scr.save() + self.assertRaises(frappe.exceptions.ValidationError, scr.submit) + sco.update_status() self.assertEqual(sco.status, "Partially Received") + scr.cancel() # Completed scr = make_subcontracting_receipt(sco.name) @@ -564,7 +564,6 @@ class TestSubcontractingOrder(FrappeTestCase): sco.load_from_db() - self.assertEqual(sco.status, "Closed") self.assertEqual(sco.supplied_items[0].returned_qty, 5) def test_ordered_qty_for_subcontracting_order(self):