feat: validate workflow before so/po update items (#23024)

* feat: validate workflow before so/po update items

* fix: incorrect workflow validation on so/po update items

Co-authored-by: Marica <maricadsouza221197@gmail.com>
This commit is contained in:
Saqib 2020-09-14 19:54:17 +05:30 committed by GitHub
parent 93d7c2f1da
commit 6db92fbb6a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 106 additions and 13 deletions

View File

@ -7,6 +7,7 @@ import json
from frappe import _, throw from frappe import _, throw
from frappe.utils import (today, flt, cint, fmt_money, formatdate, from frappe.utils import (today, flt, cint, fmt_money, formatdate,
getdate, add_days, add_months, get_last_day, nowdate, get_link_to_form) getdate, add_days, add_months, get_last_day, nowdate, get_link_to_form)
from frappe.model.workflow import get_workflow_name, is_transition_condition_satisfied, WorkflowPermissionError
from erpnext.stock.get_item_details import get_conversion_factor, get_item_details from erpnext.stock.get_item_details import get_conversion_factor, get_item_details
from erpnext.setup.utils import get_exchange_rate from erpnext.setup.utils import get_exchange_rate
from erpnext.accounts.utils import get_fiscal_years, validate_fiscal_year, get_account_currency from erpnext.accounts.utils import get_fiscal_years, validate_fiscal_year, get_account_currency
@ -1194,7 +1195,7 @@ def set_purchase_order_defaults(parent_doctype, parent_doctype_name, child_docna
child_item.base_amount = 1 # Initiallize value will update in parent validation child_item.base_amount = 1 # Initiallize value will update in parent validation
return child_item return child_item
def check_and_delete_children(parent, data): def validate_and_delete_children(parent, data):
deleted_children = [] deleted_children = []
updated_item_names = [d.get("docname") for d in data] updated_item_names = [d.get("docname") for d in data]
for item in parent.items: for item in parent.items:
@ -1221,18 +1222,37 @@ def check_and_delete_children(parent, data):
@frappe.whitelist() @frappe.whitelist()
def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, child_docname="items"): def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, child_docname="items"):
def check_permissions(doc, perm_type='create'): def check_doc_permissions(doc, perm_type='create'):
try: try:
doc.check_permission(perm_type) doc.check_permission(perm_type)
except: except frappe.PermissionError:
action = "add" if perm_type == 'create' else "update" actions = { 'create': 'add', 'write': 'update', 'cancel': 'remove' }
frappe.throw(_("You do not have permissions to {} items in a Sales Order.").format(action), title=_("Insufficient Permissions"))
frappe.throw(_("You do not have permissions to {} items in a {}.")
.format(actions[perm_type], parent_doctype), title=_("Insufficient Permissions"))
def validate_workflow_conditions(doc):
workflow = get_workflow_name(doc.doctype)
if not workflow:
return
workflow_doc = frappe.get_doc("Workflow", workflow)
current_state = doc.get(workflow_doc.workflow_state_field)
roles = frappe.get_roles()
transitions = []
for transition in workflow_doc.transitions:
if transition.next_state == current_state and transition.allowed in roles:
if not is_transition_condition_satisfied(transition, doc):
continue
transitions.append(transition.as_dict())
if not transitions:
frappe.throw(_("You do not have workflow access to update this document."), title=_("Insufficient Workflow Permissions"))
def get_new_child_item(item_row): def get_new_child_item(item_row):
if parent_doctype == "Sales Order": new_child_function = set_sales_order_defaults if parent_doctype == "Sales Order" else set_purchase_order_defaults
return set_sales_order_defaults(parent_doctype, parent_doctype_name, child_docname, item_row) return new_child_function(parent_doctype, parent_doctype_name, child_docname, item_row)
if parent_doctype == "Purchase Order":
return set_purchase_order_defaults(parent_doctype, parent_doctype_name, child_docname, item_row)
def validate_quantity(child_item, d): def validate_quantity(child_item, d):
if parent_doctype == "Sales Order" and flt(d.get("qty")) < flt(child_item.delivered_qty): if parent_doctype == "Sales Order" and flt(d.get("qty")) < flt(child_item.delivered_qty):
@ -1246,16 +1266,17 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
sales_doctypes = ['Sales Order', 'Sales Invoice', 'Delivery Note', 'Quotation'] sales_doctypes = ['Sales Order', 'Sales Invoice', 'Delivery Note', 'Quotation']
parent = frappe.get_doc(parent_doctype, parent_doctype_name) parent = frappe.get_doc(parent_doctype, parent_doctype_name)
check_and_delete_children(parent, data) check_doc_permissions(parent, 'cancel')
validate_and_delete_children(parent, data)
for d in data: for d in data:
new_child_flag = False new_child_flag = False
if not d.get("docname"): if not d.get("docname"):
new_child_flag = True new_child_flag = True
check_permissions(parent, 'create') check_doc_permissions(parent, 'create')
child_item = get_new_child_item(d) child_item = get_new_child_item(d)
else: else:
check_permissions(parent, 'write') check_doc_permissions(parent, 'write')
child_item = frappe.get_doc(parent_doctype + ' Item', d.get("docname")) child_item = frappe.get_doc(parent_doctype + ' Item', d.get("docname"))
prev_rate, new_rate = flt(child_item.get("rate")), flt(d.get("rate")) prev_rate, new_rate = flt(child_item.get("rate")), flt(d.get("rate"))
@ -1362,6 +1383,9 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
parent.update_prevdoc_status('submit') parent.update_prevdoc_status('submit')
parent.update_delivery_status() parent.update_delivery_status()
parent.reload()
validate_workflow_conditions(parent)
parent.update_blanket_order() parent.update_blanket_order()
parent.update_billing_percentage() parent.update_billing_percentage()
parent.set_status() parent.set_status()

View File

@ -417,8 +417,43 @@ class TestSalesOrder(unittest.TestCase):
# add new item # add new item
trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 100, 'qty' : 2}]) trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 100, 'qty' : 2}])
self.assertRaises(frappe.ValidationError, update_child_qty_rate,'Sales Order', trans_item, so.name) self.assertRaises(frappe.ValidationError, update_child_qty_rate,'Sales Order', trans_item, so.name)
test_user.remove_roles("Accounts User")
frappe.set_user("Administrator") frappe.set_user("Administrator")
def test_update_child_qty_rate_with_workflow(self):
from frappe.model.workflow import apply_workflow
workflow = make_sales_order_workflow()
so = make_sales_order(item_code= "_Test Item", qty=1, rate=150, do_not_submit=1)
apply_workflow(so, 'Approve')
frappe.set_user("Administrator")
user = 'test@example.com'
test_user = frappe.get_doc('User', user)
test_user.add_roles("Sales User", "Test Junior Approver")
frappe.set_user(user)
# user shouldn't be able to edit since grand_total will become > 200 if qty is doubled
trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 150, 'qty' : 2, 'docname': so.items[0].name}])
self.assertRaises(frappe.ValidationError, update_child_qty_rate, 'Sales Order', trans_item, so.name)
frappe.set_user("Administrator")
user2 = 'test2@example.com'
test_user2 = frappe.get_doc('User', user2)
test_user2.add_roles("Sales User", "Test Approver")
frappe.set_user(user2)
# Test Approver is allowed to edit with grand_total > 200
update_child_qty_rate("Sales Order", trans_item, so.name)
so.reload()
self.assertEqual(so.items[0].qty, 2)
frappe.set_user("Administrator")
test_user.remove_roles("Sales User", "Test Junior Approver", "Test Approver")
test_user2.remove_roles("Sales User", "Test Junior Approver", "Test Approver")
workflow.is_active = 0
workflow.save()
def test_update_child_qty_rate_product_bundle(self): def test_update_child_qty_rate_product_bundle(self):
# test Update Items with product bundle # test Update Items with product bundle
if not frappe.db.exists("Item", "_Product Bundle Item"): if not frappe.db.exists("Item", "_Product Bundle Item"):
@ -973,3 +1008,37 @@ def get_reserved_qty(item_code="_Test Item", warehouse="_Test Warehouse - _TC"):
"reserved_qty")) "reserved_qty"))
test_dependencies = ["Currency Exchange"] test_dependencies = ["Currency Exchange"]
def make_sales_order_workflow():
if frappe.db.exists('Workflow', 'SO Test Workflow'):
doc = frappe.get_doc("Workflow", "SO Test Workflow")
doc.set("is_active", 1)
doc.save()
return doc
frappe.get_doc(dict(doctype='Role', role_name='Test Junior Approver')).insert(ignore_if_duplicate=True)
frappe.get_doc(dict(doctype='Role', role_name='Test Approver')).insert(ignore_if_duplicate=True)
frappe.db.commit()
frappe.cache().hdel('roles', frappe.session.user)
workflow = frappe.get_doc({
"doctype": "Workflow",
"workflow_name": "SO Test Workflow",
"document_type": "Sales Order",
"workflow_state_field": "workflow_state",
"is_active": 1,
"send_email_alert": 0,
})
workflow.append('states', dict( state = 'Pending', allow_edit = 'All' ))
workflow.append('states', dict( state = 'Approved', allow_edit = 'Test Approver', doc_status = 1 ))
workflow.append('transitions', dict(
state = 'Pending', action = 'Approve', next_state = 'Approved', allowed = 'Test Junior Approver', allow_self_approval = 1,
condition = 'doc.grand_total < 200'
))
workflow.append('transitions', dict(
state = 'Pending', action = 'Approve', next_state = 'Approved', allowed = 'Test Approver', allow_self_approval = 1,
condition = 'doc.grand_total > 200'
))
workflow.insert(ignore_permissions=True)
return workflow