diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index c9d7f1959df..f871907ce6b 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -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) diff --git a/erpnext/selling/doctype/sales_order/services/status.py b/erpnext/selling/doctype/sales_order/services/status.py index 807d1da55aa..c11163088c8 100644 --- a/erpnext/selling/doctype/sales_order/services/status.py +++ b/erpnext/selling/doctype/sales_order/services/status.py @@ -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() diff --git a/erpnext/selling/doctype/sales_order/services/subcontracting.py b/erpnext/selling/doctype/sales_order/services/subcontracting.py new file mode 100644 index 00000000000..8c67f1e0990 --- /dev/null +++ b/erpnext/selling/doctype/sales_order/services/subcontracting.py @@ -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)