mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-15 07:28:39 +00:00
refactor(selling): extract Sales Order StatusService
Move status computation and progress tracking into selling/doctype/sales_order/services/status.py: - update_status, update_delivery_status, update_picking_status and set_indicator stay as controller delegators (whitelisted wrapper, purchase_order/pick_list/child_item_update callers, portal contract) - check_modified_date moves into the service (internal to update_status) - the billing/delivery/advance status defaults in validate() move to StatusService.set_default_statuses() No behaviour change.
This commit is contained in:
@@ -8,7 +8,6 @@ from typing import Literal
|
||||
import frappe
|
||||
import frappe.utils
|
||||
from frappe import _, qb
|
||||
from frappe.desk.notifications import clear_doctype_notifications
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, parse_json
|
||||
@@ -23,6 +22,7 @@ from erpnext.manufacturing.doctype.blanket_order.blanket_order import (
|
||||
validate_against_blanket_order,
|
||||
)
|
||||
from erpnext.selling.doctype.customer.customer import check_credit_limit
|
||||
from erpnext.selling.doctype.sales_order.services.status import StatusService
|
||||
from erpnext.selling.doctype.sales_order.services.stock_reservation import StockReservationService
|
||||
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
|
||||
@@ -248,12 +248,7 @@ class SalesOrder(SellingController):
|
||||
self.validate_fg_item_for_subcontracting()
|
||||
self.set_status()
|
||||
|
||||
if not self.billing_status:
|
||||
self.billing_status = "Not Billed"
|
||||
if not self.delivery_status:
|
||||
self.delivery_status = "Not Delivered"
|
||||
if not self.advance_payment_status:
|
||||
self.advance_payment_status = "Not Requested"
|
||||
StatusService(self).set_default_statuses()
|
||||
|
||||
self.reset_default_field_value("set_warehouse", "items", "warehouse")
|
||||
if not self.get("is_subcontracted"):
|
||||
@@ -579,23 +574,8 @@ class SalesOrder(SellingController):
|
||||
)
|
||||
)
|
||||
|
||||
def check_modified_date(self):
|
||||
mod_db = frappe.db.get_value("Sales Order", self.name, "modified")
|
||||
if mod_db and cstr(mod_db) != cstr(self.modified):
|
||||
frappe.throw(_("{0} {1} has been modified. Please refresh.").format(self.doctype, self.name))
|
||||
|
||||
def update_status(self, status):
|
||||
self.check_modified_date()
|
||||
self.set_status(update=True, status=status)
|
||||
# Upon Sales Order Re-open, check for credit limit.
|
||||
# Limit should be checked after the 'Hold/Closed' status is reset.
|
||||
if status == "Draft" and self.docstatus == 1:
|
||||
self.check_credit_limit()
|
||||
self.update_reserved_qty()
|
||||
self.update_subcontracting_order_status()
|
||||
self.notify_update()
|
||||
clear_doctype_notifications(self)
|
||||
self.update_blanket_order()
|
||||
StatusService(self).update_status(status)
|
||||
|
||||
def update_subcontracting_order_status(self):
|
||||
from erpnext.subcontracting.doctype.subcontracting_inward_order.subcontracting_inward_order import (
|
||||
@@ -643,65 +623,14 @@ class SalesOrder(SellingController):
|
||||
|
||||
def update_delivery_status(self):
|
||||
"""Update delivery status from Purchase Order for drop shipping"""
|
||||
tot_qty, delivered_qty = 0.0, 0.0
|
||||
|
||||
for item in self.items:
|
||||
if item.delivered_by_supplier:
|
||||
item_delivered_qty = frappe.get_all(
|
||||
"Purchase Order Item",
|
||||
{"sales_order_item": item.name, "docstatus": 1},
|
||||
[{"SUM": "received_qty", "AS": "received_qty"}],
|
||||
pluck="received_qty",
|
||||
)[0]
|
||||
item.db_set("delivered_qty", flt(item_delivered_qty), update_modified=False)
|
||||
|
||||
delivered_qty += min(item.delivered_qty, item.qty)
|
||||
tot_qty += item.qty
|
||||
|
||||
if tot_qty != 0:
|
||||
self.db_set("per_delivered", flt(delivered_qty / tot_qty) * 100, update_modified=False)
|
||||
StatusService(self).update_delivery_status()
|
||||
|
||||
def update_picking_status(self):
|
||||
total_picked_qty = 0.0
|
||||
total_qty = 0.0
|
||||
per_picked = 0.0
|
||||
|
||||
for so_item in self.items:
|
||||
if cint(
|
||||
frappe.get_cached_value("Item", so_item.item_code, "is_stock_item")
|
||||
) or self.has_product_bundle(so_item.item_code):
|
||||
total_picked_qty += flt(so_item.picked_qty)
|
||||
total_qty += flt(so_item.stock_qty)
|
||||
|
||||
if total_picked_qty and total_qty:
|
||||
per_picked = total_picked_qty / total_qty * 100
|
||||
|
||||
pick_percentage = frappe.get_single_value("Stock Settings", "over_picking_allowance")
|
||||
if pick_percentage:
|
||||
total_qty += flt(total_qty) * (pick_percentage / 100)
|
||||
|
||||
if total_picked_qty > total_qty:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Total Picked Quantity {0} is more than ordered qty {1}. You can set the Over Picking Allowance in Stock Settings."
|
||||
).format(total_picked_qty, total_qty)
|
||||
)
|
||||
|
||||
self.db_set("per_picked", flt(per_picked), update_modified=False)
|
||||
StatusService(self).update_picking_status()
|
||||
|
||||
def set_indicator(self):
|
||||
"""Set indicator for portal"""
|
||||
self.indicator_color = {
|
||||
"Draft": "red",
|
||||
"On Hold": "orange",
|
||||
"To Deliver and Bill": "orange",
|
||||
"To Bill": "orange",
|
||||
"To Deliver": "orange",
|
||||
"Completed": "green",
|
||||
"Cancelled": "red",
|
||||
}.get(self.status, "blue")
|
||||
|
||||
self.indicator_title = _(self.status)
|
||||
StatusService(self).set_indicator()
|
||||
|
||||
def on_recurring(self, reference_doc, auto_repeat_doc):
|
||||
def _get_delivery_date(ref_doc_delivery_date, red_doc_transaction_date, transaction_date):
|
||||
|
||||
108
erpnext/selling/doctype/sales_order/services/status.py
Normal file
108
erpnext/selling/doctype/sales_order/services/status.py
Normal file
@@ -0,0 +1,108 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
"""Status computation and progress tracking for Sales Order."""
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.desk.notifications import clear_doctype_notifications
|
||||
from frappe.utils import cint, cstr, flt
|
||||
|
||||
|
||||
class StatusService:
|
||||
def __init__(self, doc):
|
||||
self.doc = doc
|
||||
|
||||
def set_default_statuses(self) -> None:
|
||||
doc = self.doc
|
||||
if not doc.billing_status:
|
||||
doc.billing_status = "Not Billed"
|
||||
if not doc.delivery_status:
|
||||
doc.delivery_status = "Not Delivered"
|
||||
if not doc.advance_payment_status:
|
||||
doc.advance_payment_status = "Not Requested"
|
||||
|
||||
def update_status(self, status: str) -> None:
|
||||
doc = self.doc
|
||||
self.check_modified_date()
|
||||
doc.set_status(update=True, status=status)
|
||||
# Upon Sales Order Re-open, check for credit limit.
|
||||
# Limit should be checked after the 'Hold/Closed' status is reset.
|
||||
if status == "Draft" and doc.docstatus == 1:
|
||||
doc.check_credit_limit()
|
||||
doc.update_reserved_qty()
|
||||
doc.update_subcontracting_order_status()
|
||||
doc.notify_update()
|
||||
clear_doctype_notifications(doc)
|
||||
doc.update_blanket_order()
|
||||
|
||||
def check_modified_date(self) -> None:
|
||||
doc = self.doc
|
||||
mod_db = frappe.db.get_value("Sales Order", doc.name, "modified")
|
||||
if mod_db and cstr(mod_db) != cstr(doc.modified):
|
||||
frappe.throw(_("{0} {1} has been modified. Please refresh.").format(doc.doctype, doc.name))
|
||||
|
||||
def update_delivery_status(self) -> None:
|
||||
"""Update delivery status from Purchase Order for drop shipping"""
|
||||
doc = self.doc
|
||||
tot_qty, delivered_qty = 0.0, 0.0
|
||||
|
||||
for item in doc.items:
|
||||
if item.delivered_by_supplier:
|
||||
item_delivered_qty = frappe.get_all(
|
||||
"Purchase Order Item",
|
||||
{"sales_order_item": item.name, "docstatus": 1},
|
||||
[{"SUM": "received_qty", "AS": "received_qty"}],
|
||||
pluck="received_qty",
|
||||
)[0]
|
||||
item.db_set("delivered_qty", flt(item_delivered_qty), update_modified=False)
|
||||
|
||||
delivered_qty += min(item.delivered_qty, item.qty)
|
||||
tot_qty += item.qty
|
||||
|
||||
if tot_qty != 0:
|
||||
doc.db_set("per_delivered", flt(delivered_qty / tot_qty) * 100, update_modified=False)
|
||||
|
||||
def update_picking_status(self) -> None:
|
||||
doc = self.doc
|
||||
total_picked_qty = 0.0
|
||||
total_qty = 0.0
|
||||
per_picked = 0.0
|
||||
|
||||
for so_item in doc.items:
|
||||
if cint(
|
||||
frappe.get_cached_value("Item", so_item.item_code, "is_stock_item")
|
||||
) or doc.has_product_bundle(so_item.item_code):
|
||||
total_picked_qty += flt(so_item.picked_qty)
|
||||
total_qty += flt(so_item.stock_qty)
|
||||
|
||||
if total_picked_qty and total_qty:
|
||||
per_picked = total_picked_qty / total_qty * 100
|
||||
|
||||
pick_percentage = frappe.get_single_value("Stock Settings", "over_picking_allowance")
|
||||
if pick_percentage:
|
||||
total_qty += flt(total_qty) * (pick_percentage / 100)
|
||||
|
||||
if total_picked_qty > total_qty:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Total Picked Quantity {0} is more than ordered qty {1}. You can set the Over Picking Allowance in Stock Settings."
|
||||
).format(total_picked_qty, total_qty)
|
||||
)
|
||||
|
||||
doc.db_set("per_picked", flt(per_picked), update_modified=False)
|
||||
|
||||
def set_indicator(self) -> None:
|
||||
"""Set indicator for portal"""
|
||||
doc = self.doc
|
||||
doc.indicator_color = {
|
||||
"Draft": "red",
|
||||
"On Hold": "orange",
|
||||
"To Deliver and Bill": "orange",
|
||||
"To Bill": "orange",
|
||||
"To Deliver": "orange",
|
||||
"Completed": "green",
|
||||
"Cancelled": "red",
|
||||
}.get(doc.status, "blue")
|
||||
|
||||
doc.indicator_title = _(doc.status)
|
||||
Reference in New Issue
Block a user