refactor(selling): extract Sales Order SubcontractingService

Move subcontracting (inward) integration into
selling/doctype/sales_order/services/subcontracting.py:

- can_update_items stays as a controller delegator (onload data +
  child_item_update.py caller)
- validate_fg_item_for_subcontracting and
  update_subcontracting_order_status move into the service; validate()
  and StatusService.update_status repointed

No behaviour change.
This commit is contained in:
Nabin Hait
2026-06-06 23:13:52 +05:30
parent 7f3ddfb3a1
commit 0f876c10aa
3 changed files with 75 additions and 53 deletions

View File

@@ -25,6 +25,7 @@ from erpnext.selling.doctype.customer.customer import check_credit_limit
from erpnext.selling.doctype.sales_order.services.delivery_schedule import DeliveryScheduleService
from erpnext.selling.doctype.sales_order.services.status import StatusService
from erpnext.selling.doctype.sales_order.services.stock_reservation import StockReservationService
from erpnext.selling.doctype.sales_order.services.subcontracting import SubcontractingService
from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import has_reserved_stock
from erpnext.stock.get_item_details import get_default_bom
@@ -209,13 +210,7 @@ class SalesOrder(SellingController):
self.set_onload("has_reserved_stock", True)
def can_update_items(self) -> bool:
result = True
if self.is_subcontracted:
if frappe.db.exists("Subcontracting Inward Order", {"sales_order": self.name, "docstatus": 1}):
result = False
return result
return SubcontractingService(self).can_update_items()
def before_validate(self):
self.set_has_unit_price_items()
@@ -246,7 +241,7 @@ class SalesOrder(SellingController):
make_packing_list(self)
self.validate_with_previous_doc()
self.validate_fg_item_for_subcontracting()
SubcontractingService(self).validate_fg_item_for_subcontracting()
self.set_status()
StatusService(self).set_default_statuses()
@@ -255,37 +250,6 @@ class SalesOrder(SellingController):
if not self.get("is_subcontracted"):
StockReservationService(self).enable_auto_reserve_stock()
def validate_fg_item_for_subcontracting(self):
if self.is_subcontracted:
for item in self.items:
if not item.fg_item:
frappe.throw(
_("Row #{0}: Finished Good Item is not specified for service item {1}").format(
item.idx, item.item_code
)
)
else:
if not frappe.get_value("Item", item.fg_item, "is_sub_contracted_item"):
frappe.throw(
_("Row #{0}: Finished Good Item {1} must be a sub-contracted item").format(
item.idx, item.fg_item
)
)
if not frappe.db.get_value(
"Subcontracting BOM",
{"finished_good": item.fg_item, "is_active": 1},
"finished_good_bom",
) and not frappe.get_value("Item", item.fg_item, "default_bom"):
frappe.throw(
_("Row #{0}: BOM not found for FG Item {1}").format(item.idx, item.fg_item)
)
if not item.fg_item_qty:
frappe.throw(_("Row #{0}: Finished Good Item Qty can not be zero").format(item.idx))
else:
for item in self.items:
item.set("fg_item", None)
item.set("fg_item_qty", 0)
def set_has_unit_price_items(self):
"""
If permitted in settings and any item has 0 qty, the SO has unit price items.
@@ -571,19 +535,6 @@ class SalesOrder(SellingController):
def update_status(self, status):
StatusService(self).update_status(status)
def update_subcontracting_order_status(self):
from erpnext.subcontracting.doctype.subcontracting_inward_order.subcontracting_inward_order import (
update_subcontracting_inward_order_status as update_scio_status,
)
if self.is_subcontracted:
scio = frappe.get_cached_value(
"Subcontracting Inward Order", {"sales_order": self.name, "docstatus": 1}, "name"
)
if scio:
update_scio_status(scio, "Closed" if self.status == "Closed" else None)
def update_reserved_qty(self, so_item_rows=None):
StockReservationService(self).update_reserved_qty(so_item_rows)

View File

@@ -8,6 +8,8 @@ from frappe import _
from frappe.desk.notifications import clear_doctype_notifications
from frappe.utils import cint, cstr, flt
from erpnext.selling.doctype.sales_order.services.subcontracting import SubcontractingService
class StatusService:
def __init__(self, doc):
@@ -31,7 +33,7 @@ class StatusService:
if status == "Draft" and doc.docstatus == 1:
doc.check_credit_limit()
doc.update_reserved_qty()
doc.update_subcontracting_order_status()
SubcontractingService(doc).update_subcontracting_order_status()
doc.notify_update()
clear_doctype_notifications(doc)
doc.update_blanket_order()

View File

@@ -0,0 +1,69 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
"""Subcontracting (inward) integration for Sales Order."""
import frappe
from frappe import _
class SubcontractingService:
def __init__(self, doc):
self.doc = doc
def validate_fg_item_for_subcontracting(self) -> None:
doc = self.doc
if doc.is_subcontracted:
for item in doc.items:
if not item.fg_item:
frappe.throw(
_("Row #{0}: Finished Good Item is not specified for service item {1}").format(
item.idx, item.item_code
)
)
else:
if not frappe.get_value("Item", item.fg_item, "is_sub_contracted_item"):
frappe.throw(
_("Row #{0}: Finished Good Item {1} must be a sub-contracted item").format(
item.idx, item.fg_item
)
)
if not frappe.db.get_value(
"Subcontracting BOM",
{"finished_good": item.fg_item, "is_active": 1},
"finished_good_bom",
) and not frappe.get_value("Item", item.fg_item, "default_bom"):
frappe.throw(
_("Row #{0}: BOM not found for FG Item {1}").format(item.idx, item.fg_item)
)
if not item.fg_item_qty:
frappe.throw(_("Row #{0}: Finished Good Item Qty can not be zero").format(item.idx))
else:
for item in doc.items:
item.set("fg_item", None)
item.set("fg_item_qty", 0)
def can_update_items(self) -> bool:
result = True
if self.doc.is_subcontracted:
if frappe.db.exists(
"Subcontracting Inward Order", {"sales_order": self.doc.name, "docstatus": 1}
):
result = False
return result
def update_subcontracting_order_status(self) -> None:
from erpnext.subcontracting.doctype.subcontracting_inward_order.subcontracting_inward_order import (
update_subcontracting_inward_order_status as update_scio_status,
)
doc = self.doc
if doc.is_subcontracted:
scio = frappe.get_cached_value(
"Subcontracting Inward Order", {"sales_order": doc.name, "docstatus": 1}, "name"
)
if scio:
update_scio_status(scio, "Closed" if doc.status == "Closed" else None)