mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-24 20:46:38 +00:00
fix(stock): stop a secondary item type from waiving quality inspection
The inspection skip for secondary rows applied to every purpose, and in
validate_inspection it skipped the row even when the item itself mandated
inspection. Secondary Item Type is only meaningful on the purposes that
produce secondary items, but nothing clears it elsewhere, since
mark_finished_and_secondary_items runs for Manufacture and Repack alone.
A Material Receipt of an item marked Inspection Required Before Purchase
is blocked without an inspection. Setting Secondary Item Type on the row
submitted it clean.
Limit the exemption to the purposes that produce secondary items, and to
other doctypes such as Subcontracting Receipt, which carry the field with
its intended meaning. The client-side mirror is kept in sync.
(cherry picked from commit dfec7bd5c7)
# Conflicts:
# erpnext/public/js/controllers/transaction.js
# erpnext/stock/services/quality_inspection_service.py
This commit is contained in:
@@ -18,10 +18,19 @@ erpnext.stock.qi_outgoing_purposes = [
|
||||
"Subcontracting Delivery",
|
||||
"Disassemble",
|
||||
];
|
||||
erpnext.stock.secondary_item_purposes = ["Manufacture", "Repack", "Disassemble"];
|
||||
erpnext.stock.is_incoming_qi_purpose = (purpose) =>
|
||||
purpose === "Manufacture" || erpnext.stock.qi_incoming_purposes.includes(purpose);
|
||||
erpnext.stock.row_requires_quality_inspection = (purpose, row) => {
|
||||
<<<<<<< HEAD
|
||||
if (row.type || row.is_legacy_scrap_item) return false;
|
||||
=======
|
||||
if (
|
||||
erpnext.stock.secondary_item_purposes.includes(purpose) &&
|
||||
(row.secondary_item_type || row.is_legacy_scrap_item)
|
||||
)
|
||||
return false;
|
||||
>>>>>>> dfec7bd5c7 (fix(stock): stop a secondary item type from waiving quality inspection)
|
||||
if (purpose === "Manufacture") return !!row.is_finished_item;
|
||||
if (erpnext.stock.qi_incoming_purposes.includes(purpose)) return !!row.t_warehouse;
|
||||
if (erpnext.stock.qi_outgoing_purposes.includes(purpose))
|
||||
|
||||
165
erpnext/stock/services/quality_inspection_service.py
Normal file
165
erpnext/stock/services/quality_inspection_service.py
Normal file
@@ -0,0 +1,165 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
"""Quality inspection validation for stock transactions.
|
||||
|
||||
Extracted from ``StockController``. Validates that items requiring quality
|
||||
inspection have a present / submitted / non-rejected Quality Inspection.
|
||||
"""
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
|
||||
from erpnext.exceptions import (
|
||||
QualityInspectionNotSubmittedError,
|
||||
QualityInspectionRejectedError,
|
||||
QualityInspectionRequiredError,
|
||||
)
|
||||
|
||||
# Doctype -> the document-level "inspection required" flag. Shared with
|
||||
# check_item_quality_inspection in stock_controller so the two stay in sync.
|
||||
INSPECTION_FIELDNAME_MAP = {
|
||||
"Purchase Receipt": "inspection_required_before_purchase",
|
||||
"Purchase Invoice": "inspection_required_before_purchase",
|
||||
"Subcontracting Receipt": "inspection_required_before_purchase",
|
||||
"Sales Invoice": "inspection_required_before_delivery",
|
||||
"Delivery Note": "inspection_required_before_delivery",
|
||||
}
|
||||
|
||||
# Purposes whose inward (t_warehouse) row is inspected.
|
||||
QI_INCOMING_PURPOSES = (
|
||||
"Material Receipt",
|
||||
"Repack",
|
||||
"Receive from Customer",
|
||||
"Subcontracting Return",
|
||||
)
|
||||
|
||||
# Purposes whose outgoing (s_warehouse) row is inspected. This is an explicit
|
||||
# allow-list rather than "everything that isn't incoming" so a new purpose can't
|
||||
# silently start requiring a QI. Material Consumption for Manufacture is left out
|
||||
# on purpose: an inspection_required BOM inspects the manufactured output (handled
|
||||
# by the "Manufacture" finished-good rule), not each consumed raw material.
|
||||
# Keep this in sync with erpnext.stock.qi_* helpers in transaction.js.
|
||||
QI_OUTGOING_PURPOSES = (
|
||||
"Material Issue",
|
||||
"Material Transfer",
|
||||
"Material Transfer for Manufacture",
|
||||
"Send to Subcontractor",
|
||||
"Subcontracting Delivery",
|
||||
"Disassemble",
|
||||
)
|
||||
|
||||
|
||||
SECONDARY_ITEM_PURPOSES = ("Manufacture", "Repack", "Disassemble")
|
||||
|
||||
|
||||
def is_inspection_exempt_secondary_row(doc, row) -> bool:
|
||||
"""Whether the row is a secondary item on a document that produces secondary items."""
|
||||
if not (row.get("secondary_item_type") or row.get("is_legacy_scrap_item")):
|
||||
return False
|
||||
|
||||
if doc.doctype == "Stock Entry":
|
||||
return doc.purpose in SECONDARY_ITEM_PURPOSES
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def stock_entry_row_requires_inspection(purpose, row):
|
||||
"""Check if this Stock Entry row need a Quality Inspection."""
|
||||
if purpose in SECONDARY_ITEM_PURPOSES and (
|
||||
row.get("secondary_item_type") or row.get("is_legacy_scrap_item")
|
||||
):
|
||||
return False
|
||||
if purpose == "Manufacture":
|
||||
return bool(row.is_finished_item)
|
||||
if purpose in QI_INCOMING_PURPOSES:
|
||||
return bool(row.t_warehouse)
|
||||
if purpose in QI_OUTGOING_PURPOSES:
|
||||
return bool(row.s_warehouse and row.s_warehouse != row.t_warehouse)
|
||||
return False
|
||||
|
||||
|
||||
class QualityInspectionService:
|
||||
def __init__(self, doc) -> None:
|
||||
self.doc = doc
|
||||
|
||||
def validate_inspection(self):
|
||||
"""Checks if quality inspection is set/ is valid for Items that require inspection."""
|
||||
inspection_required_fieldname = INSPECTION_FIELDNAME_MAP.get(self.doc.doctype)
|
||||
|
||||
# return if inspection is not required on document level
|
||||
if (
|
||||
(not inspection_required_fieldname and self.doc.doctype != "Stock Entry")
|
||||
or (self.doc.doctype == "Stock Entry" and not self.doc.inspection_required)
|
||||
or (self.doc.doctype in ["Sales Invoice", "Purchase Invoice"] and not self.doc.update_stock)
|
||||
):
|
||||
return
|
||||
|
||||
for row in self.doc.get("items"):
|
||||
qi_required = False
|
||||
if inspection_required_fieldname and frappe.get_cached_value(
|
||||
"Item", row.item_code, inspection_required_fieldname
|
||||
):
|
||||
qi_required = True
|
||||
elif self.doc.doctype == "Stock Entry":
|
||||
qi_required = stock_entry_row_requires_inspection(self.doc.purpose, row)
|
||||
|
||||
if is_inspection_exempt_secondary_row(self.doc, row):
|
||||
continue
|
||||
|
||||
if qi_required: # validate row only if inspection is required on item level
|
||||
if self.doc.doctype in [
|
||||
"Purchase Receipt",
|
||||
"Purchase Invoice",
|
||||
"Sales Invoice",
|
||||
"Delivery Note",
|
||||
] and frappe.get_single_value(
|
||||
"Stock Settings", "allow_to_make_quality_inspection_after_purchase_or_delivery"
|
||||
):
|
||||
return
|
||||
|
||||
self.validate_qi_presence(row)
|
||||
if self.doc.docstatus == 1:
|
||||
self.validate_qi_submission(row)
|
||||
self.validate_qi_rejection(row)
|
||||
|
||||
def validate_qi_presence(self, row):
|
||||
"""Check if QI is present on row level. Warn on save and stop on submit if missing."""
|
||||
if not row.quality_inspection:
|
||||
msg = _("Row #{0}: Quality Inspection is required for Item {1}").format(
|
||||
row.idx, frappe.bold(row.item_code)
|
||||
)
|
||||
if self.doc.docstatus == 1:
|
||||
frappe.throw(msg, title=_("Inspection Required"), exc=QualityInspectionRequiredError)
|
||||
else:
|
||||
frappe.msgprint(msg, title=_("Inspection Required"), indicator="blue")
|
||||
|
||||
def validate_qi_submission(self, row):
|
||||
"""Check if QI is submitted on row level, during submission"""
|
||||
action = frappe.get_single_value("Stock Settings", "action_if_quality_inspection_is_not_submitted")
|
||||
qa_docstatus = frappe.db.get_value("Quality Inspection", row.quality_inspection, "docstatus")
|
||||
|
||||
if qa_docstatus != 1:
|
||||
link = frappe.utils.get_link_to_form("Quality Inspection", row.quality_inspection)
|
||||
msg = _("Row #{0}: Quality Inspection {1} is not submitted for the item: {2}").format(
|
||||
row.idx, link, row.item_code
|
||||
)
|
||||
if action == "Stop":
|
||||
frappe.throw(msg, title=_("Inspection Submission"), exc=QualityInspectionNotSubmittedError)
|
||||
else:
|
||||
frappe.msgprint(msg, alert=True, indicator="orange")
|
||||
|
||||
def validate_qi_rejection(self, row):
|
||||
"""Check if QI is rejected on row level, during submission"""
|
||||
action = frappe.get_single_value("Stock Settings", "action_if_quality_inspection_is_rejected")
|
||||
qa_status = frappe.db.get_value("Quality Inspection", row.quality_inspection, "status")
|
||||
|
||||
if qa_status == "Rejected":
|
||||
link = frappe.utils.get_link_to_form("Quality Inspection", row.quality_inspection)
|
||||
msg = _("Row #{0}: Quality Inspection {1} was rejected for item {2}").format(
|
||||
row.idx, link, row.item_code
|
||||
)
|
||||
if action == "Stop":
|
||||
frappe.throw(msg, title=_("Inspection Rejected"), exc=QualityInspectionRejectedError)
|
||||
else:
|
||||
frappe.msgprint(msg, alert=True, indicator="orange")
|
||||
Reference in New Issue
Block a user