diff --git a/makap_backend/hooks.py b/makap_backend/hooks.py index a32585e..fa787c1 100644 --- a/makap_backend/hooks.py +++ b/makap_backend/hooks.py @@ -6,7 +6,7 @@ app_email = "josephnjuki833@gmail.com" app_license = "mit" # MAKAP Module 3 child-table registration -after_migrate = ["makap_backend.makap_backend.setup.ensure_makap_functional_roles", "makap_backend.makap_backend.setup.ensure_makap_object_permissions", "makap_backend.makap_backend.setup_module3.ensure_module3_child_tables", "makap_backend.makap_backend.setup_extensions.ensure_custom_fields", "makap_backend.makap_backend.setup_module8_dashboard.ensure_module8_number_cards", "makap_backend.makap_backend.setup_workspace_dashboard.ensure_workspace_number_cards", "makap_backend.makap_backend.workspace_setup.ensure_makap_workspace", "makap_backend.makap_backend.setup_module8_permissions.ensure_module8_role_permissions", "makap_backend.makap_backend.portal_shortcut.ensure_portal_shortcut"] +after_migrate = ["makap_backend.makap_backend.setup.ensure_makap_functional_roles", "makap_backend.makap_backend.setup.ensure_makap_object_permissions", "makap_backend.makap_backend.setup_module3.ensure_module3_child_tables", "makap_backend.makap_backend.setup_extensions.ensure_custom_fields", "makap_backend.makap_backend.setup_module8_dashboard.ensure_module8_number_cards", "makap_backend.makap_backend.setup_workspace_dashboard.ensure_workspace_number_cards", "makap_backend.makap_backend.workspace_setup.ensure_makap_workspace", "makap_backend.makap_backend.setup_module8_permissions.ensure_module8_role_permissions", "makap_backend.makap_backend.setup_roles.ensure_shopfloor_permissions", "makap_backend.makap_backend.setup_roles.ensure_makap_role_profiles", "makap_backend.makap_backend.portal_shortcut.ensure_portal_shortcut"] # MAKAP cross-module server-side controls doc_events = { diff --git a/makap_backend/makap_backend/setup.py b/makap_backend/makap_backend/setup.py index ed98283..5fa67d8 100644 --- a/makap_backend/makap_backend/setup.py +++ b/makap_backend/makap_backend/setup.py @@ -103,6 +103,9 @@ def ensure_makap_functional_roles(): "Commercial Authority", # Module 8B "Credit Authority", + # Shop floor and prepress (PRD Section 3) + "Machine Operator", + "Prepress", ] created = [] for role_name in role_names: diff --git a/makap_backend/makap_backend/setup_extensions.py b/makap_backend/makap_backend/setup_extensions.py index 5053459..73c4fdc 100644 --- a/makap_backend/makap_backend/setup_extensions.py +++ b/makap_backend/makap_backend/setup_extensions.py @@ -174,6 +174,15 @@ FIELDS = [ ] +# Quality decisions: only roles granted permlevel 1 on Quality Inspection +# (QA, see setup_roles) can change these. +QUALITY_DECISION_FIELDS = { + "makap_quality_eligibility", "makap_quality_disposition", "makap_disposition_quantity", + "makap_disposition_uom", "makap_disposition_reason", "makap_concession_scope", + "makap_concession_quantity", "makap_concession_uom", "makap_concession_reference", + "makap_released_not_before", +} + # Where each DocType's Makap fields live. Parent forms get one "Makap" tab; # child rows get a section. Operator and roll fields sit in the main view # because they are filled in on the shop floor, not reviewed later. @@ -194,9 +203,10 @@ def _end_anchor(meta): def build_custom_fields(): grouped = {} for dt, fieldname, label, fieldtype, options, read_only in FIELDS: - grouped.setdefault(dt, []).append( - {"fieldname": fieldname, "label": label, "fieldtype": fieldtype, "options": options, "read_only": read_only} - ) + field = {"fieldname": fieldname, "label": label, "fieldtype": fieldtype, "options": options, "read_only": read_only} + if dt == "Quality Inspection" and fieldname in QUALITY_DECISION_FIELDS: + field["permlevel"] = 1 + grouped.setdefault(dt, []).append(field) result = {} for dt, fields in grouped.items(): diff --git a/makap_backend/makap_backend/setup_module8_permissions.py b/makap_backend/makap_backend/setup_module8_permissions.py index 9d45eb0..6700c8f 100644 --- a/makap_backend/makap_backend/setup_module8_permissions.py +++ b/makap_backend/makap_backend/setup_module8_permissions.py @@ -2,15 +2,15 @@ import frappe from frappe.permissions import add_permission, update_permission_property -def _grant(doctype, role, **flags): +def _grant(doctype, role, permlevel=0, **flags): if not frappe.db.exists("DocType", doctype): return if not frappe.db.exists("Role", role): return - if not frappe.db.exists("Custom DocPerm", {"parent": doctype, "role": role, "permlevel": 0}): - add_permission(doctype, role, permlevel=0) + if not frappe.db.exists("Custom DocPerm", {"parent": doctype, "role": role, "permlevel": permlevel}): + add_permission(doctype, role, permlevel=permlevel) for ptype, value in flags.items(): - update_permission_property(doctype, role, 0, ptype, value) + update_permission_property(doctype, role, permlevel, ptype, value) def ensure_module8_role_permissions(): diff --git a/makap_backend/makap_backend/setup_roles.py b/makap_backend/makap_backend/setup_roles.py new file mode 100644 index 0000000..838dee5 --- /dev/null +++ b/makap_backend/makap_backend/setup_roles.py @@ -0,0 +1,70 @@ +"""Role profiles for the user roles in the PRD (Section 3), and the shop-floor +permissions they need. Both are created only when missing, so changes Makap +makes later in the desk are never overwritten on migrate. +""" + +import frappe + +from makap_backend.makap_backend.setup_module8_permissions import _grant + +ROLE_PROFILES = { + "Makap - Sales / Account Manager": ["Sales User", "Commercial Reviewer"], + "Makap - Design / Prepress": ["Prepress", "Technical Authority"], + "Makap - Production Planner": ["Manufacturing Manager", "Production Authority", "Stock User"], + "Makap - Procurement": ["Purchase User", "Purchase Manager", "Supply Assurance Manager", "Stock User"], + "Makap - Warehouse": ["Stock User", "Materials Controller", "Dispatch Preparer"], + "Makap - Machine Operator": ["Machine Operator"], + "Makap - Shift Supervisor": ["Machine Operator", "Manufacturing User"], + "Makap - QC / QA": ["Quality Manager", "Concession Authority"], + "Makap - Finance": ["Accounts User", "Accounts Manager", "Credit Authority", "Commercial Authority"], + "Makap - Management": ["Analytics", "Auditor"], +} + +# Operators record work on Job Cards and label rolls; they don't plan, buy or release. +SHOPFLOOR_GRANTS = [ + ("Job Card", "Machine Operator", dict(read=1, write=1, create=1, submit=1)), + ("Work Order", "Machine Operator", dict(read=1)), + ("Operation", "Machine Operator", dict(read=1)), + ("Workstation", "Machine Operator", dict(read=1)), + ("Item", "Machine Operator", dict(read=1)), + ("Batch", "Machine Operator", dict(read=1, write=1, create=1)), + ("Print Cylinder", "Machine Operator", dict(read=1)), + ("Quality Inspection", "Machine Operator", dict(read=1, write=1, create=1)), + ("Print Cylinder", "Prepress", dict(read=1, write=1, create=1)), + ("Packaging Specification", "Prepress", dict(read=1, write=1)), + ("Item", "Prepress", dict(read=1)), +] + +# Eligibility and disposition decide whether stock can move on or ship, so +# only QA may set them; everyone who can open an inspection may still read them. +QUALITY_DECISION_WRITERS = ["Quality Manager", "Concession Authority", "System Manager"] +QUALITY_DECISION_READERS = [ + "Manufacturing User", "Manufacturing Manager", "Machine Operator", "Stock User", + "Dispatch Preparer", "Dispatch Authority", "Production Authority", +] + + +def ensure_makap_role_profiles(): + created = [] + for profile, roles in ROLE_PROFILES.items(): + if frappe.db.exists("Role Profile", profile): + continue + doc = frappe.new_doc("Role Profile") + doc.role_profile = profile + for role in roles: + if frappe.db.exists("Role", role): + doc.append("roles", {"role": role}) + doc.insert(ignore_permissions=True) + created.append(profile) + return created + + +def ensure_shopfloor_permissions(): + for doctype, role, flags in SHOPFLOOR_GRANTS: + _grant(doctype, role, **flags) + + for role in QUALITY_DECISION_WRITERS: + _grant("Quality Inspection", role, permlevel=1, read=1, write=1) + for role in QUALITY_DECISION_READERS: + _grant("Quality Inspection", role, permlevel=1, read=1) + return True