Implement MAKAP Modules 1-7 backend foundations

This commit is contained in:
KabundiNjuki
2026-09-01 22:29:31 +03:00
parent 55b9e6523d
commit eafa1242db
27 changed files with 2295 additions and 2 deletions
+23
View File
@@ -5,3 +5,26 @@
tags tags
node_modules node_modules
__pycache__ __pycache__
# Python and local development artifacts
__pycache__/
*.py[cod]
*.pyo
.pytest_cache/
.mypy_cache/
.ruff_cache/
# Local environment and credentials
.env
.env.*
*.pem
*.key
*.secret
# Generated frontend/build artifacts
node_modules/
dist/
# OS/editor files
.DS_Store
.vscode/
.idea/
+25
View File
@@ -247,3 +247,28 @@ app_license = "mit"
# List of apps whose translatable strings should be excluded from this app's translations. # List of apps whose translatable strings should be excluded from this app's translations.
# ignore_translatable_strings_from = [] # ignore_translatable_strings_from = []
# 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 cross-module server-side controls
doc_events = {
"Packaging Specification": {
"validate": "makap_backend.makap_backend.controls.validate_packaging_specification",
},
"Quotation": {
"validate": "makap_backend.makap_backend.controls.validate_quotation",
},
"Sales Order": {
"validate": "makap_backend.makap_backend.controls.validate_sales_order",
},
"Work Order": {
"validate": "makap_backend.makap_backend.controls.validate_work_order",
},
"Quality Inspection": {
"validate": "makap_backend.makap_backend.controls.validate_quality_inspection",
},
"Purchase Order": {
"validate": "makap_backend.makap_backend.controls.validate_purchase_order",
},
}
+232
View File
@@ -0,0 +1,232 @@
import frappe
from frappe import _
PUBLIC_FIELDS = {
"organisation_name",
"trading_name",
"customer_type",
"business_tax_identifier",
"contact_name",
"email_id",
"telephone",
"submitted_address",
"location",
"product_interest",
"initial_enquiry",
"privacy_notice_acknowledged",
"privacy_notice_reference_version",
}
def _clean_public_payload(payload):
if not isinstance(payload, dict):
frappe.throw(_("Registration payload must be an object."))
return {key: payload.get(key) for key in PUBLIC_FIELDS if payload.get(key) not in (None, "")}
@frappe.whitelist(allow_guest=True)
def submit_customer_onboarding(payload=None):
"""Create intake evidence only; never create Customer, Contact, or User."""
if isinstance(payload, str):
payload = frappe.parse_json(payload)
values = _clean_public_payload(payload or {})
values.update(
{
"doctype": "Customer Onboarding Request",
"registration_source": "Website",
"onboarding_status": "Submitted",
"customer_match_result": "Not Checked",
"portal_access_status": "Not Provisioned",
}
)
request = frappe.get_doc(values)
request.insert(ignore_permissions=True)
frappe.db.commit()
return {
"success": True,
"reference": request.name,
"message": "Registration received for review. This is not Customer approval.",
}
def _find_existing_contact(email_id, phone=None):
filters = {"email_id": email_id}
contact_name = frappe.db.get_value("Contact", filters, "name")
if contact_name:
return contact_name
if phone:
return frappe.db.get_value("Contact Phone", {"phone": phone}, "parent")
return None
def approve_customer_onboarding(request_name, create_new_customer=False, customer=None):
"""Reviewer action: resolve identity, approve, and idempotently create/link Customer + Contact."""
request = frappe.get_doc("Customer Onboarding Request", request_name)
if request.onboarding_status not in {"Under Review", "Approved"}:
frappe.throw(_("Only requests Under Review or already Approved can be completed."))
if create_new_customer:
request.customer_match_result = "New Customer Confirmed"
elif customer:
request.customer_match_result = "Existing Customer Confirmed"
request.possible_existing_customer = customer
else:
frappe.throw(_("Provide an existing Customer or explicitly confirm a new Customer."))
if customer:
if not frappe.db.exists("Customer", customer):
frappe.throw(_("The selected Customer does not exist."))
customer_name = customer
else:
customer_name = request.created_linked_customer
if not customer_name:
customer_name = frappe.db.get_value(
"Customer", {"customer_name": request.organisation_name}, "name"
)
if not customer_name:
customer_doc = frappe.get_doc(
{
"doctype": "Customer",
"customer_name": request.organisation_name,
"customer_type": request.customer_type or "Company",
"customer_group": frappe.defaults.get_global_default("customer_group") or "Commercial",
"territory": frappe.defaults.get_global_default("territory") or "All Territories",
}
)
customer_doc.insert(ignore_permissions=True)
customer_name = customer_doc.name
contact_name = request.created_linked_contact or _find_existing_contact(
request.email_id, request.telephone
)
if not contact_name:
contact = frappe.get_doc(
{
"doctype": "Contact",
"first_name": request.contact_name,
"email_ids": [{"email_id": request.email_id, "is_primary": 1}],
"phone_nos": [{"phone": request.telephone, "is_primary_phone": 1}],
"links": [{"link_doctype": "Customer", "link_name": customer_name}],
}
)
contact.insert(ignore_permissions=True)
contact_name = contact.name
else:
contact = frappe.get_doc("Contact", contact_name)
linked = {(row.link_doctype, row.link_name) for row in contact.links}
if ("Customer", customer_name) not in linked:
contact.append("links", {"link_doctype": "Customer", "link_name": customer_name})
contact.save(ignore_permissions=True)
request.created_linked_customer = customer_name
request.created_linked_contact = contact_name
request.onboarding_status = "Completed"
request.save(ignore_permissions=True)
frappe.db.commit()
return {"request": request.name, "customer": customer_name, "contact": contact_name}
def _customer_names_for_logged_in_user():
if frappe.session.user in {"Guest", "Administrator"}:
return []
user_email = frappe.db.get_value("User", frappe.session.user, "email") or frappe.session.user
contact_names = set(
frappe.get_all(
"Contact",
filters={"email_id": user_email},
pluck="name",
)
)
contact_names.update(
frappe.get_all(
"Contact Email",
filters={"email_id": user_email},
pluck="parent",
)
)
if not contact_names:
return []
return frappe.get_all(
"Dynamic Link",
filters={
"parenttype": "Contact",
"parent": ["in", list(contact_names)],
"link_doctype": "Customer",
},
pluck="link_name",
)
@frappe.whitelist()
def get_my_customer_context():
"""Return only Customers linked to the authenticated portal user."""
customer_names = _customer_names_for_logged_in_user()
if not customer_names:
return []
return frappe.get_all(
"Customer",
filters={"name": ["in", customer_names]},
fields=["name", "customer_name", "customer_group", "territory"],
order_by="customer_name asc",
)
@frappe.whitelist()
def get_my_sales_orders(limit=50):
customer_names = _customer_names_for_logged_in_user()
if not customer_names:
return []
return frappe.get_all(
"Sales Order",
filters={"customer": ["in", customer_names], "docstatus": ["<", 2]},
fields=["name", "transaction_date", "delivery_date", "status", "grand_total", "currency"],
order_by="transaction_date desc",
limit_page_length=min(int(limit), 100),
)
@frappe.whitelist()
def get_my_quotations(limit=50):
customer_names = _customer_names_for_logged_in_user()
if not customer_names:
return []
return frappe.get_all(
"Quotation",
filters={"party_name": ["in", customer_names], "docstatus": ["<", 2]},
fields=["name", "transaction_date", "valid_till", "status", "grand_total", "currency"],
order_by="transaction_date desc",
limit_page_length=min(int(limit), 100),
)
@frappe.whitelist()
def create_my_opportunity(subject, opportunity_type="Sales", opportunity_from="Customer", customer=None, message=None):
"""Create a standard Opportunity only for a Customer authorised to the logged-in user."""
customer_names = _customer_names_for_logged_in_user()
if not customer or customer not in customer_names:
frappe.throw("You are not authorised to create an enquiry for this Customer.")
if not subject or not subject.strip():
frappe.throw("Enquiry subject is required.")
opportunity = frappe.get_doc(
{
"doctype": "Opportunity",
"opportunity_from": opportunity_from,
"party_name": customer,
"opportunity_type": opportunity_type,
"title": subject.strip(),
"enquiry_from": "Customer",
"notes": message or "",
}
)
opportunity.insert()
frappe.db.commit()
return {"name": opportunity.name, "customer": customer, "status": opportunity.status}
+135
View File
@@ -0,0 +1,135 @@
import frappe
def _is_manufactured_item(item_code):
if not item_code or not frappe.db.exists("Item", item_code):
return False
item = frappe.db.get_value(
"Item", item_code, ["is_stock_item", "is_sales_item", "item_group"], as_dict=True
)
return bool(item and item.is_stock_item and item.is_sales_item)
def _spec_for_line(line):
return getattr(line, "makap_packaging_specification", None)
def validate_packaging_specification(doc, method=None):
status = doc.specification_status
if status == "Quote Eligible":
if not doc.quote_eligible:
frappe.throw("Quote Eligible status requires Quote Eligible to be checked.")
if not doc.quote_eligible_by or not doc.quote_eligible_on:
frappe.throw("Quote Eligible status requires user and timestamp evidence.")
if status == "Approved for Manufacture":
required = [
(doc.quote_eligible, "Quote Eligible"),
(doc.approved_by, "Approved By"),
(doc.approved_on, "Approved On"),
]
for value, label in required:
if not value:
frappe.throw(f"Approved for Manufacture requires {label}.")
if status == "Withdrawn" and not doc.withdrawal_reason:
frappe.throw("A withdrawn specification must include a Withdrawal Reason.")
if doc.commercially_referenced and doc.is_new() and status != "Draft":
frappe.throw("A new specification cannot be marked as commercially referenced.")
def validate_quotation(doc, method=None):
for line in doc.get("items") or []:
if not _is_manufactured_item(line.item_code):
continue
spec_name = _spec_for_line(line)
requires_spec = frappe.db.get_value(
"Item", line.item_code, "makap_requires_packaging_specification"
)
if requires_spec and not spec_name:
frappe.throw(
f"Quotation item {line.item_code} requires a Packaging Specification."
)
if spec_name:
spec = frappe.get_doc("Packaging Specification", spec_name)
if spec.item != line.item_code:
frappe.throw(
f"Packaging Specification {spec.name} does not belong to Item {line.item_code}."
)
if doc.party_name and spec.customer and spec.customer != doc.party_name:
frappe.throw(
f"Packaging Specification {spec.name} does not belong to Customer {doc.party_name}."
)
if spec.specification_status not in {"Quote Eligible", "Approved for Manufacture"}:
frappe.throw(
f"Packaging Specification {spec.name} is not Quote Eligible."
)
line.makap_specification_revision = spec.revision
line.makap_technical_summary = spec.customer_facing_technical_summary
def validate_sales_order(doc, method=None):
if doc.docstatus == 1 and not doc.makap_customer_acceptance_basis:
frappe.throw("Submitted Sales Order requires Customer Acceptance Basis.")
for line in doc.get("items") or []:
spec_name = _spec_for_line(line)
if not spec_name:
continue
spec = frappe.get_doc("Packaging Specification", spec_name)
if doc.customer and spec.customer and spec.customer != doc.customer:
frappe.throw(
f"Packaging Specification {spec.name} does not belong to Customer {doc.customer}."
)
if spec.item != line.item_code:
frappe.throw(
f"Packaging Specification {spec.name} does not belong to Item {line.item_code}."
)
line.makap_specification_revision = spec.revision
def validate_work_order(doc, method=None):
if doc.docstatus != 1:
return
if doc.makap_manufacturing_readiness_status not in {"Ready", "Released"}:
frappe.throw("Submitted Work Order requires Manufacturing Readiness Status Ready or Released.")
if doc.makap_packaging_specification:
spec_status = frappe.db.get_value(
"Packaging Specification", doc.makap_packaging_specification, "specification_status"
)
if spec_status != "Approved for Manufacture":
frappe.throw("Work Order requires a Packaging Specification approved for manufacture.")
if doc.makap_bom_compatibility_status not in {"Compatible", "Not Applicable"}:
frappe.throw("Work Order requires a compatible BOM decision before submission.")
if doc.makap_process_compatibility_status not in {"Compatible", "Not Applicable"}:
frappe.throw("Work Order requires a compatible process decision before submission.")
def validate_quality_inspection(doc, method=None):
if doc.makap_quality_disposition == "Release":
if doc.makap_quality_eligibility != "Eligible":
frappe.throw("A Release disposition requires Quality Eligibility Eligible.")
if doc.makap_quality_disposition == "Concession":
if not doc.makap_concession_reference or not doc.makap_concession_scope:
frappe.throw("A Concession requires scope and reference evidence.")
if doc.makap_quality_disposition in {"Hold", "Reject", "Rework", "Scrap"}:
if doc.makap_quality_eligibility == "Eligible":
frappe.throw("Held, rejected, rework, or scrap stock cannot be marked Eligible.")
def validate_purchase_order(doc, method=None):
for line in doc.get("items") or []:
if line.makap_supplier_confirmation_status == "Confirmed" and not line.makap_supplier_confirmation_reference:
frappe.throw(
f"Purchase Order item {line.item_code} requires Supplier Confirmation Reference."
)
@@ -0,0 +1,67 @@
{
"actions": [],
"creation": "2026-09-01 00:00:00",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"characteristic",
"requirement_nominal",
"unit",
"minimum_lower_tolerance",
"maximum_upper_tolerance",
"notes"
],
"fields": [
{
"fieldname": "characteristic",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Characteristic",
"reqd": 1
},
{
"fieldname": "requirement_nominal",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Requirement / Nominal",
"reqd": 1
},
{
"fieldname": "unit",
"fieldtype": "Data",
"label": "Unit"
},
{
"fieldname": "minimum_lower_tolerance",
"fieldtype": "Float",
"label": "Minimum / Lower Tolerance"
},
{
"fieldname": "maximum_upper_tolerance",
"fieldtype": "Float",
"label": "Maximum / Upper Tolerance"
},
{
"fieldname": "notes",
"fieldtype": "Small Text",
"label": "Notes"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2026-09-01 21:47:35.807459",
"modified_by": "Administrator",
"module": "makap_backend",
"name": "Critical Acceptance Requirement",
"owner": "Administrator",
"permissions": [],
"row_format": "Dynamic",
"rows_threshold_for_grid_search": 20,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
@@ -0,0 +1,261 @@
{
"actions": [],
"autoname": "naming_series:",
"creation": "2026-09-01 00:00:00",
"doctype": "DocType",
"autoname": "naming_series:",
"engine": "InnoDB",
"field_order": [
"organisation_section",
"naming_series",
"organisation_name",
"trading_name",
"customer_type",
"business_tax_identifier",
"primary_contact_section",
"contact_name",
"email_id",
"telephone",
"submitted_address",
"location",
"requirement_section",
"product_interest",
"initial_enquiry",
"intake_section",
"registration_source",
"assisted_channel",
"review_section",
"onboarding_status",
"customer_match_result",
"possible_existing_customer",
"created_linked_customer",
"created_linked_contact",
"portal_access_status",
"review_notes",
"privacy_section",
"privacy_notice_acknowledged",
"acknowledgement_timestamp",
"privacy_notice_reference_version"
],
"fields": [
{
"fieldname": "organisation_section",
"fieldtype": "Section Break",
"label": "Organisation"
},
{
"default": "COR-.YYYY.-.#####",
"fieldname": "naming_series",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Reference Series",
"options": "COR-.YYYY.-.#####",
"reqd": 1
},
{
"fieldname": "organisation_name",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Organisation Name",
"reqd": 1
},
{
"fieldname": "trading_name",
"fieldtype": "Data",
"label": "Trading Name"
},
{
"fieldname": "customer_type",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Customer Type",
"options": "Company\nIndividual",
"reqd": 1
},
{
"fieldname": "business_tax_identifier",
"fieldtype": "Data",
"label": "Business / Tax Identifier"
},
{
"fieldname": "primary_contact_section",
"fieldtype": "Section Break",
"label": "Primary Contact"
},
{
"fieldname": "contact_name",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Contact Name",
"reqd": 1
},
{
"fieldname": "email_id",
"fieldtype": "Data",
"label": "Email",
"options": "Email",
"reqd": 1
},
{
"fieldname": "telephone",
"fieldtype": "Data",
"label": "Telephone",
"reqd": 1
},
{
"fieldname": "submitted_address",
"fieldtype": "Small Text",
"label": "Submitted Address"
},
{
"fieldname": "location",
"fieldtype": "Data",
"label": "Location"
},
{
"fieldname": "requirement_section",
"fieldtype": "Section Break",
"label": "Requirement"
},
{
"fieldname": "product_interest",
"fieldtype": "Small Text",
"label": "Product Interest"
},
{
"fieldname": "initial_enquiry",
"fieldtype": "Small Text",
"label": "Initial Enquiry"
},
{
"fieldname": "intake_section",
"fieldtype": "Section Break",
"label": "Intake"
},
{
"fieldname": "registration_source",
"fieldtype": "Select",
"label": "Registration Source",
"options": "Website\nAssisted",
"reqd": 1
},
{
"depends_on": "eval:doc.registration_source == 'Assisted'",
"fieldname": "assisted_channel",
"fieldtype": "Data",
"label": "Assisted Channel"
},
{
"fieldname": "review_section",
"fieldtype": "Section Break",
"label": "Review and Resolution"
},
{
"default": "Submitted",
"fieldname": "onboarding_status",
"fieldtype": "Select",
"label": "Onboarding Status",
"options": "Submitted\nUnder Review\nApproved\nRejected\nCompleted",
"reqd": 1
},
{
"default": "Not Checked",
"fieldname": "customer_match_result",
"fieldtype": "Select",
"label": "Customer Match Result",
"options": "Not Checked\nPossible Match\nExisting Customer Confirmed\nNew Customer Confirmed",
"reqd": 1
},
{
"fieldname": "possible_existing_customer",
"fieldtype": "Link",
"label": "Possible Existing Customer",
"options": "Customer"
},
{
"fieldname": "created_linked_customer",
"fieldtype": "Link",
"label": "Created / Linked Customer",
"options": "Customer"
},
{
"fieldname": "created_linked_contact",
"fieldtype": "Link",
"label": "Created / Linked Contact",
"options": "Contact"
},
{
"default": "Not Provisioned",
"fieldname": "portal_access_status",
"fieldtype": "Select",
"label": "Portal Access Status",
"options": "Not Provisioned\nPending\nActive\nRevoked"
},
{
"fieldname": "review_notes",
"fieldtype": "Small Text",
"label": "Review Notes"
},
{
"fieldname": "privacy_section",
"fieldtype": "Section Break",
"label": "Privacy Evidence"
},
{
"default": "0",
"fieldname": "privacy_notice_acknowledged",
"fieldtype": "Check",
"label": "Privacy Notice Acknowledged"
},
{
"depends_on": "eval:doc.privacy_notice_acknowledged == 1",
"fieldname": "acknowledgement_timestamp",
"fieldtype": "Datetime",
"label": "Acknowledgement Timestamp"
},
{
"fieldname": "privacy_notice_reference_version",
"fieldtype": "Data",
"label": "Privacy Notice Reference / Version"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2026-09-01 23:04:15.841393",
"modified_by": "Administrator",
"module": "makap_backend",
"name": "Customer Onboarding Request",
"naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Commercial Reviewer",
"share": 1,
"write": 1
}
],
"row_format": "Dynamic",
"search_fields": "organisation_name,email_id,telephone,business_tax_identifier",
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
@@ -0,0 +1,25 @@
import frappe
from frappe.model.document import Document
class CustomerOnboardingRequest(Document):
def before_insert(self):
self.onboarding_status = self.onboarding_status or "Submitted"
self.customer_match_result = self.customer_match_result or "Not Checked"
self.portal_access_status = self.portal_access_status or "Not Provisioned"
if self.privacy_notice_acknowledged and not self.acknowledgement_timestamp:
self.acknowledgement_timestamp = frappe.utils.now_datetime()
def validate(self):
if self.registration_source not in {"Website", "Assisted"}:
frappe.throw("Registration Source must be Website or Assisted.")
if self.registration_source == "Assisted" and not self.assisted_channel:
frappe.throw("Assisted Channel is required for an Assisted registration.")
if self.registration_source == "Website" and self.assisted_channel:
frappe.throw("Assisted Channel must be empty for a Website registration.")
if self.onboarding_status in {"Approved", "Completed"} and self.customer_match_result not in {"Existing Customer Confirmed", "New Customer Confirmed"}:
frappe.throw("Approval requires confirmed Customer identity resolution.")
if self.onboarding_status == "Completed" and (not self.created_linked_customer or not self.created_linked_contact):
frappe.throw("A Completed request requires linked Customer and Contact.")
if self.onboarding_status == "Rejected" and (self.created_linked_customer or self.created_linked_contact):
frappe.throw("A Rejected request cannot contain Customer or Contact links.")
@@ -0,0 +1,69 @@
{
"actions": [],
"creation": "2026-09-01 00:00:00",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"sequence",
"material_film_description",
"raw_material_item",
"nominal_thickness_requirement",
"uom",
"layer_role_notes"
],
"fields": [
{
"fieldname": "sequence",
"fieldtype": "Int",
"in_list_view": 1,
"label": "Sequence",
"reqd": 1
},
{
"fieldname": "material_film_description",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Material / Film Description",
"reqd": 1
},
{
"fieldname": "raw_material_item",
"fieldtype": "Link",
"label": "Raw Material Item",
"options": "Item"
},
{
"fieldname": "nominal_thickness_requirement",
"fieldtype": "Float",
"label": "Nominal Thickness / Requirement"
},
{
"fieldname": "uom",
"fieldtype": "Link",
"label": "UOM",
"options": "UOM"
},
{
"fieldname": "layer_role_notes",
"fieldtype": "Small Text",
"label": "Layer Role / Notes"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2026-09-01 21:47:35.458192",
"modified_by": "Administrator",
"module": "makap_backend",
"name": "Material Structure",
"owner": "Administrator",
"permissions": [],
"row_format": "Dynamic",
"rows_threshold_for_grid_search": 20,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
@@ -0,0 +1,8 @@
// Copyright (c) 2026, Joseph njuki and contributors
// For license information, please see license.txt
// frappe.ui.form.on("Packaging Specification", {
// refresh(frm) {
// },
// });
@@ -0,0 +1,479 @@
{
"actions": [],
"autoname": "naming_series:",
"creation": "2026-09-01 00:00:00",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"identity_section",
"naming_series",
"specification_series",
"revision",
"customer",
"item",
"product_family",
"specification_status",
"supersedes",
"superseded_by",
"current_approved_revision",
"control_section",
"quote_eligible",
"quote_eligible_by",
"quote_eligible_on",
"commercially_referenced",
"approved_by",
"approved_on",
"change_reason",
"commercial_review_required",
"withdrawal_reason",
"definition_section",
"customer_facing_technical_summary",
"internal_technical_description",
"material_substrate_summary",
"width",
"width_unit",
"length",
"length_unit",
"thickness_gauge",
"thickness_gauge_unit",
"conversion_finishing_notes",
"packing_requirements",
"technical_notes",
"material_structure",
"process_section",
"printing_required",
"print_process",
"lamination_required",
"slitting_required",
"rewinding_required",
"conversion_bag_making_requirement",
"finishing_requirement",
"process_requirement_notes",
"artwork_section",
"artwork_required",
"artwork_status",
"approved_artwork_file_reference",
"artwork_revision",
"colour_count",
"print_colour_notes",
"customer_approval_basis",
"customer_approval_reference_attachment",
"customer_approval_date",
"tooling_section",
"print_tooling_required",
"tooling_type",
"tooling_set_reference",
"tooling_requirement_notes",
"critical_acceptance_requirements"
],
"fields": [
{
"fieldname": "identity_section",
"fieldtype": "Section Break",
"label": "Identity"
},
{
"default": "PS-.YYYY.-.#####",
"fieldname": "naming_series",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Specification Series",
"options": "PS-.YYYY.-.#####",
"reqd": 1
},
{
"fieldname": "specification_series",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Specification Series",
"reqd": 1
},
{
"default": "R01",
"fieldname": "revision",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Revision",
"reqd": 1
},
{
"fieldname": "customer",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Customer",
"options": "Customer",
"reqd": 1
},
{
"fieldname": "item",
"fieldtype": "Link",
"label": "Item",
"options": "Item",
"reqd": 1
},
{
"fieldname": "product_family",
"fieldtype": "Data",
"label": "Product Family",
"reqd": 1
},
{
"default": "Draft",
"fieldname": "specification_status",
"fieldtype": "Select",
"label": "Specification Status",
"options": "Draft\nQuote Eligible\nApproved for Manufacture\nSuperseded\nWithdrawn",
"reqd": 1
},
{
"fieldname": "supersedes",
"fieldtype": "Link",
"label": "Supersedes",
"options": "Packaging Specification"
},
{
"fieldname": "superseded_by",
"fieldtype": "Link",
"label": "Superseded By",
"options": "Packaging Specification"
},
{
"default": "0",
"fieldname": "current_approved_revision",
"fieldtype": "Check",
"label": "Current Approved Revision"
},
{
"fieldname": "control_section",
"fieldtype": "Section Break",
"label": "Control"
},
{
"default": "0",
"fieldname": "quote_eligible",
"fieldtype": "Check",
"label": "Quote Eligible"
},
{
"fieldname": "quote_eligible_by",
"fieldtype": "Link",
"label": "Quote Eligible By",
"options": "User"
},
{
"fieldname": "quote_eligible_on",
"fieldtype": "Datetime",
"label": "Quote Eligible On"
},
{
"default": "0",
"fieldname": "commercially_referenced",
"fieldtype": "Check",
"label": "Commercially Referenced"
},
{
"fieldname": "approved_by",
"fieldtype": "Link",
"label": "Approved By",
"options": "User"
},
{
"fieldname": "approved_on",
"fieldtype": "Datetime",
"label": "Approved On"
},
{
"fieldname": "change_reason",
"fieldtype": "Small Text",
"label": "Change Reason"
},
{
"default": "0",
"fieldname": "commercial_review_required",
"fieldtype": "Check",
"label": "Commercial Review Required"
},
{
"fieldname": "withdrawal_reason",
"fieldtype": "Small Text",
"label": "Withdrawal Reason"
},
{
"fieldname": "definition_section",
"fieldtype": "Section Break",
"label": "Product Definition"
},
{
"fieldname": "customer_facing_technical_summary",
"fieldtype": "Small Text",
"label": "Customer-facing Technical Summary"
},
{
"fieldname": "internal_technical_description",
"fieldtype": "Text Editor",
"label": "Internal Technical Description"
},
{
"fieldname": "material_substrate_summary",
"fieldtype": "Small Text",
"label": "Material / Substrate Summary"
},
{
"fieldname": "width",
"fieldtype": "Float",
"label": "Width"
},
{
"fieldname": "width_unit",
"fieldtype": "Data",
"label": "Width Unit"
},
{
"fieldname": "length",
"fieldtype": "Float",
"label": "Length"
},
{
"fieldname": "length_unit",
"fieldtype": "Data",
"label": "Length Unit"
},
{
"fieldname": "thickness_gauge",
"fieldtype": "Float",
"label": "Thickness / Gauge"
},
{
"fieldname": "thickness_gauge_unit",
"fieldtype": "Data",
"label": "Thickness / Gauge Unit"
},
{
"fieldname": "conversion_finishing_notes",
"fieldtype": "Small Text",
"label": "Conversion / Finishing Notes"
},
{
"fieldname": "packing_requirements",
"fieldtype": "Small Text",
"label": "Packing Requirements"
},
{
"fieldname": "technical_notes",
"fieldtype": "Text Editor",
"label": "Technical Notes"
},
{
"fieldname": "material_structure",
"fieldtype": "Table",
"label": "Material Structure",
"options": "Material Structure"
},
{
"fieldname": "process_section",
"fieldtype": "Section Break",
"label": "Process Requirements"
},
{
"default": "0",
"fieldname": "printing_required",
"fieldtype": "Check",
"label": "Printing Required"
},
{
"fieldname": "print_process",
"fieldtype": "Data",
"label": "Print Process"
},
{
"default": "0",
"fieldname": "lamination_required",
"fieldtype": "Check",
"label": "Lamination Required"
},
{
"default": "0",
"fieldname": "slitting_required",
"fieldtype": "Check",
"label": "Slitting Required"
},
{
"default": "0",
"fieldname": "rewinding_required",
"fieldtype": "Check",
"label": "Rewinding Required"
},
{
"fieldname": "conversion_bag_making_requirement",
"fieldtype": "Data",
"label": "Conversion / Bag-Making Requirement"
},
{
"fieldname": "finishing_requirement",
"fieldtype": "Data",
"label": "Finishing Requirement"
},
{
"fieldname": "process_requirement_notes",
"fieldtype": "Small Text",
"label": "Process Requirement Notes"
},
{
"fieldname": "artwork_section",
"fieldtype": "Section Break",
"label": "Artwork / Prepress"
},
{
"default": "0",
"fieldname": "artwork_required",
"fieldtype": "Check",
"label": "Artwork Required"
},
{
"fieldname": "artwork_status",
"fieldtype": "Select",
"label": "Artwork Status",
"options": "Not Required\nPending\nReceived\nApproved"
},
{
"fieldname": "approved_artwork_file_reference",
"fieldtype": "Data",
"label": "Approved Artwork File / Reference"
},
{
"fieldname": "artwork_revision",
"fieldtype": "Data",
"label": "Artwork Revision"
},
{
"fieldname": "colour_count",
"fieldtype": "Int",
"label": "Colour Count"
},
{
"fieldname": "print_colour_notes",
"fieldtype": "Small Text",
"label": "Print / Colour Notes"
},
{
"fieldname": "customer_approval_basis",
"fieldtype": "Data",
"label": "Customer Approval Basis"
},
{
"fieldname": "customer_approval_reference_attachment",
"fieldtype": "Data",
"label": "Customer Approval Reference / Attachment"
},
{
"fieldname": "customer_approval_date",
"fieldtype": "Date",
"label": "Customer Approval Date"
},
{
"fieldname": "tooling_section",
"fieldtype": "Section Break",
"label": "Tooling"
},
{
"default": "0",
"fieldname": "print_tooling_required",
"fieldtype": "Check",
"label": "Print Tooling Required"
},
{
"fieldname": "tooling_type",
"fieldtype": "Data",
"label": "Tooling Type"
},
{
"fieldname": "tooling_set_reference",
"fieldtype": "Data",
"label": "Tooling Set Reference"
},
{
"fieldname": "tooling_requirement_notes",
"fieldtype": "Small Text",
"label": "Tooling Requirement Notes"
},
{
"fieldname": "critical_acceptance_requirements",
"fieldtype": "Table",
"label": "Critical Acceptance Requirements",
"options": "Critical Acceptance Requirement"
}
],
"links": [],
"modified": "2026-09-02 00:05:48.067955",
"modified_by": "Administrator",
"module": "makap_backend",
"name": "Packaging Specification",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Commercial Reviewer",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Technical Authority",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Production Authority",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Quality Manager",
"share": 1,
"write": 1
}
],
"row_format": "Dynamic",
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"title_field": "specification_series",
"track_changes": 1
}
@@ -0,0 +1,6 @@
import frappe
from frappe.model.document import Document
class PackagingSpecification(Document):
pass
@@ -0,0 +1,9 @@
# Copyright (c) 2026, Joseph njuki and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestPackagingSpecification(FrappeTestCase):
pass
+142
View File
@@ -0,0 +1,142 @@
import frappe
def _require_doc(doctype, name):
if not frappe.db.exists(doctype, name):
frappe.throw(f"{doctype} {name} does not exist.")
return frappe.get_doc(doctype, name)
@frappe.whitelist()
def evaluate_work_order_readiness(work_order_name, save=True):
"""Derive bounded readiness fields from standard Work Order/BOM/specification data."""
work_order = _require_doc("Work Order", work_order_name)
blockers = []
spec_status = None
if work_order.makap_packaging_specification:
spec_status = frappe.db.get_value(
"Packaging Specification", work_order.makap_packaging_specification, "specification_status"
)
if spec_status != "Approved for Manufacture":
blockers.append("Packaging Specification is not Approved for Manufacture")
elif work_order.production_item:
requires_spec = frappe.db.get_value(
"Item", work_order.production_item, "makap_requires_packaging_specification"
)
if requires_spec:
blockers.append("Production Item requires a Packaging Specification")
if not work_order.bom_no:
blockers.append("No BOM is selected")
elif work_order.makap_bom_compatibility_status not in {"Compatible", "Not Applicable"}:
blockers.append("BOM compatibility is not confirmed")
if work_order.makap_process_compatibility_status not in {"Compatible", "Not Applicable"}:
blockers.append("Process compatibility is not confirmed")
work_order.makap_manufacturing_readiness_status = "Blocked" if blockers else "Ready"
work_order.makap_manufacturing_readiness_blockers = "\n".join(blockers)
work_order.makap_readiness_checked_by = frappe.session.user
work_order.makap_readiness_checked_on = frappe.utils.now_datetime()
if save:
work_order.save(ignore_permissions=True)
frappe.db.commit()
return {
"work_order": work_order.name,
"status": work_order.makap_manufacturing_readiness_status,
"blockers": blockers,
"specification_status": spec_status,
}
@frappe.whitelist()
def evaluate_material_availability(work_order_name, save=True):
"""Compare standard Work Order required quantities with standard Bin actual quantity."""
work_order = _require_doc("Work Order", work_order_name)
shortages = []
partially_available = False
for item in work_order.get("required_items") or []:
required = float(item.required_qty or 0)
if not required or not item.item_code:
continue
filters = {"item_code": item.item_code}
if item.source_warehouse:
filters["warehouse"] = item.source_warehouse
available = sum(
float(row.actual_qty or 0)
for row in frappe.get_all("Bin", filters=filters, fields=["actual_qty"])
)
if available <= 0:
shortages.append(f"{item.item_code}: required {required}, available {available}")
elif available < required:
partially_available = True
shortages.append(f"{item.item_code}: required {required}, available {available}")
if shortages and partially_available:
status = "Partially Available"
elif shortages:
status = "Blocked"
else:
status = "Available"
work_order.makap_material_availability_status = status
work_order.makap_material_availability_checked_by = frappe.session.user
work_order.makap_material_availability_checked_on = frappe.utils.now_datetime()
work_order.makap_material_availability_blocker_summary = "\n".join(shortages)
if save:
work_order.save(ignore_permissions=True)
frappe.db.commit()
return {"work_order": work_order.name, "status": status, "shortages": shortages}
@frappe.whitelist()
def apply_quality_disposition(quality_inspection_name, disposition, quantity=None, reason=None):
"""Record bounded quality disposition evidence on a standard Quality Inspection."""
inspection = _require_doc("Quality Inspection", quality_inspection_name)
allowed = {"Pending", "Release", "Hold", "Reject", "Rework", "Scrap", "Concession"}
if disposition not in allowed:
frappe.throw("Invalid quality disposition.")
if disposition == "Release" and inspection.makap_quality_eligibility != "Eligible":
frappe.throw("Release requires Quality Eligibility Eligible.")
if disposition == "Concession" and not inspection.makap_concession_reference:
frappe.throw("Concession requires a Concession Reference.")
inspection.makap_quality_disposition = disposition
inspection.makap_disposition_quantity = quantity
inspection.makap_disposition_reason = reason
inspection.makap_disposition_by = frappe.session.user
inspection.makap_disposition_on = frappe.utils.now_datetime()
if disposition == "Release":
inspection.makap_quality_eligibility = "Eligible"
elif disposition in {"Hold", "Reject", "Rework", "Scrap"}:
inspection.makap_quality_eligibility = "Hold" if disposition == "Hold" else "Ineligible"
inspection.save(ignore_permissions=True)
frappe.db.commit()
return {"quality_inspection": inspection.name, "disposition": disposition}
@frappe.whitelist()
def get_supply_assurance_snapshot(item_code, warehouse=None):
"""Return a derived supply view from standard stock, material requests, and purchase orders."""
bins = frappe.get_all(
"Bin",
filters={"item_code": item_code, **({"warehouse": warehouse} if warehouse else {})},
fields=["warehouse", "actual_qty", "reserved_qty", "ordered_qty", "planned_qty"],
)
mr_items = frappe.get_all(
"Material Request Item",
filters={"item_code": item_code, "docstatus": 1},
fields=["parent", "qty", "ordered_qty", "schedule_date", "warehouse"],
limit_page_length=100,
)
po_items = frappe.get_all(
"Purchase Order Item",
filters={"item_code": item_code, "docstatus": 1},
fields=["parent", "qty", "received_qty", "schedule_date", "warehouse", "makap_expected_eligible_date"],
limit_page_length=100,
)
return {"item_code": item_code, "bins": bins, "material_requests": mr_items, "purchase_orders": po_items}
@@ -0,0 +1,62 @@
{
"actions": [],
"allow_rename": 0,
"creation": "2026-09-01 00:00:00.000000",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"characteristic",
"requirement_nominal",
"unit",
"minimum_lower_tolerance",
"maximum_upper_tolerance",
"notes"
],
"fields": [
{
"fieldname": "characteristic",
"fieldtype": "Data",
"label": "Characteristic",
"reqd": 1
},
{
"fieldname": "requirement_nominal",
"fieldtype": "Data",
"label": "Requirement / Nominal",
"reqd": 1
},
{
"fieldname": "unit",
"fieldtype": "Data",
"label": "Unit"
},
{
"fieldname": "minimum_lower_tolerance",
"fieldtype": "Float",
"label": "Minimum / Lower Tolerance"
},
{
"fieldname": "maximum_upper_tolerance",
"fieldtype": "Float",
"label": "Maximum / Upper Tolerance"
},
{
"fieldname": "notes",
"fieldtype": "Small Text",
"label": "Notes"
}
],
"istable": 1,
"links": [],
"module": "makap_backend",
"name": "Critical Acceptance Requirement",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1,
"editable_grid": 1,
"quick_entry": 0,
"modified_by": "Administrator"
}
@@ -0,0 +1,64 @@
{
"actions": [],
"allow_rename": 0,
"creation": "2026-09-01 00:00:00.000000",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"sequence",
"material_film_description",
"raw_material_item",
"nominal_thickness_requirement",
"uom",
"layer_role_notes"
],
"fields": [
{
"fieldname": "sequence",
"fieldtype": "Int",
"label": "Sequence",
"reqd": 1
},
{
"fieldname": "material_film_description",
"fieldtype": "Data",
"label": "Material / Film Description",
"reqd": 1
},
{
"fieldname": "raw_material_item",
"fieldtype": "Link",
"label": "Raw Material Item",
"options": "Item"
},
{
"fieldname": "nominal_thickness_requirement",
"fieldtype": "Float",
"label": "Nominal Thickness / Requirement"
},
{
"fieldname": "uom",
"fieldtype": "Link",
"label": "UOM",
"options": "UOM"
},
{
"fieldname": "layer_role_notes",
"fieldtype": "Small Text",
"label": "Layer Role / Notes"
}
],
"istable": 1,
"links": [],
"module": "makap_backend",
"name": "Material Structure",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1,
"editable_grid": 1,
"quick_entry": 0,
"modified_by": "Administrator"
}
+178
View File
@@ -0,0 +1,178 @@
import frappe
def ensure_makap_roles():
role_name = "Commercial Reviewer"
if not frappe.db.exists("Role", role_name):
frappe.get_doc(
{
"doctype": "Role",
"role_name": role_name,
"desk_access": 1,
"is_custom": 1,
}
).insert(ignore_permissions=True)
frappe.db.commit()
return role_name
def grant_onboarding_permissions():
doctype = frappe.get_doc("DocType", "Customer Onboarding Request")
role_name = "Commercial Reviewer"
existing_roles = {perm.role for perm in doctype.permissions}
if role_name not in existing_roles:
doctype.append(
"permissions",
{
"role": role_name,
"read": 1,
"write": 1,
"create": 1,
"delete": 0,
"submit": 0,
"cancel": 0,
"amend": 0,
"print": 1,
"email": 1,
"export": 1,
"report": 1,
"share": 1,
},
)
doctype.save(ignore_permissions=True)
frappe.db.commit()
return role_name
def smoke_test_onboarding_validation():
base = {
"doctype": "Customer Onboarding Request",
"organisation_name": "MAKAP Smoke Test Organisation",
"customer_type": "Company",
"contact_name": "Smoke Test Contact",
"email_id": "makap-smoke-test@example.invalid",
"telephone": "+256000000000",
"registration_source": "Website",
"privacy_notice_acknowledged": 1,
"onboarding_status": "Approved",
"customer_match_result": "Not Checked",
}
blocked = frappe.new_doc("Customer Onboarding Request")
blocked.update(base)
try:
blocked.insert(ignore_permissions=True)
except Exception as exc:
blocked_error = str(exc)
else:
frappe.delete_doc("Customer Onboarding Request", blocked.name, ignore_permissions=True)
frappe.db.commit()
frappe.throw("Smoke test failed: unresolved request was Approved.")
approved = frappe.new_doc("Customer Onboarding Request")
approved.update(base)
approved.organisation_name = "MAKAP Smoke Test Approved Organisation"
approved.email_id = "makap-smoke-approved@example.invalid"
approved.customer_match_result = "New Customer Confirmed"
approved.insert(ignore_permissions=True)
approved_name = approved.name
frappe.delete_doc("Customer Onboarding Request", approved_name, ignore_permissions=True)
frappe.db.commit()
return {
"unresolved_request_blocked": "Customer Match Result" in blocked_error,
"approved_confirmed_request_allowed": True,
"temporary_record_deleted": not frappe.db.exists(
"Customer Onboarding Request", approved_name
),
}
def ensure_makap_functional_roles():
role_names = [
"Technical Authority",
"Production Authority",
"Materials Controller",
"Supply Assurance Manager",
"Concession Authority",
]
created = []
for role_name in role_names:
if not frappe.db.exists("Role", role_name):
frappe.get_doc(
{
"doctype": "Role",
"role_name": role_name,
"desk_access": 1,
"is_custom": 1,
}
).insert(ignore_permissions=True)
created.append(role_name)
frappe.db.commit()
return created
def ensure_makap_object_permissions():
permissions = {
"Customer Onboarding Request": {
"Commercial Reviewer": {"read": 1, "write": 1, "create": 1, "print": 1, "report": 1, "export": 1},
},
"Packaging Specification": {
"Commercial Reviewer": {"read": 1, "write": 1, "create": 1, "print": 1, "report": 1, "export": 1},
"Technical Authority": {"read": 1, "write": 1, "create": 1, "print": 1, "report": 1, "export": 1},
"Production Authority": {"read": 1, "print": 1, "report": 1},
"Quality Manager": {"read": 1, "print": 1, "report": 1},
},
}
for doctype_name, role_map in permissions.items():
doc = frappe.get_doc("DocType", doctype_name)
existing = {perm.role for perm in doc.permissions}
changed = False
for role_name, flags in role_map.items():
if role_name in existing:
continue
row = {"role": role_name}
row.update(flags)
doc.append("permissions", row)
changed = True
if changed:
doc.save(ignore_permissions=True)
frappe.db.commit()
return permissions
def ensure_makap_object_permissions():
permissions = {
"Customer Onboarding Request": {
"Commercial Reviewer": {"read": 1, "write": 1, "create": 1, "print": 1, "report": 1, "export": 1},
},
"Packaging Specification": {
"Commercial Reviewer": {"read": 1, "write": 1, "create": 1, "print": 1, "report": 1, "export": 1},
"Technical Authority": {"read": 1, "write": 1, "create": 1, "print": 1, "report": 1, "export": 1},
"Production Authority": {"read": 1, "print": 1, "report": 1},
"Quality Manager": {"read": 1, "print": 1, "report": 1},
},
}
for doctype_name, role_map in permissions.items():
if not frappe.db.exists("DocType", doctype_name):
continue
doc = frappe.get_doc("DocType", doctype_name)
existing = {perm.role for perm in doc.permissions}
changed = False
for role_name, flags in role_map.items():
if role_name in existing:
continue
row = {"role": role_name}
row.update(flags)
doc.append("permissions", row)
changed = True
if changed:
doc.save(ignore_permissions=True)
frappe.db.commit()
return True
@@ -0,0 +1,103 @@
import frappe
FIELDS = [
# Module 2 — commercial/product linkage
("Item", "makap_requires_packaging_specification", "Requires Packaging Specification", "Check", None, 0),
("Quotation Item", "makap_packaging_specification", "Packaging Specification", "Link", "Packaging Specification", 0),
("Quotation Item", "makap_specification_revision", "Packaging Specification Revision", "Data", None, 1),
("Quotation Item", "makap_technical_summary", "Technical Summary", "Small Text", None, 1),
("Sales Order Item", "makap_packaging_specification", "Packaging Specification", "Link", "Packaging Specification", 0),
("Sales Order Item", "makap_specification_revision", "Packaging Specification Revision", "Data", None, 1),
("Sales Order", "makap_customer_acceptance_basis", "Customer Acceptance Basis", "Select", "Customer PO\nSigned Quotation\nEmail/Written Confirmation\nDirect Confirmed Order\nOther", 0),
("Sales Order", "makap_acceptance_reference", "Acceptance Reference", "Data", None, 0),
# Module 4 — manufacturing readiness and process classification
("Operation", "makap_process_type", "Makap Process Type", "Select", "Printing — Gravure\nPrinting — Flexographic\nLamination\nSlitting\nRewinding\nConversion / Bag Making\nFinishing\nOther", 0),
("Work Order", "makap_packaging_specification", "Packaging Specification", "Link", "Packaging Specification", 1),
("Work Order", "makap_specification_revision", "Specification Revision", "Data", None, 1),
("Work Order", "makap_sales_order_item_reference", "Sales Order Item Reference", "Data", None, 0),
("Work Order", "makap_customer", "Customer", "Link", "Customer", 1),
("Work Order", "makap_manufacturing_readiness_status", "Manufacturing Readiness Status", "Select", "Not Checked\nBlocked\nReady\nReleased", 0),
("Work Order", "makap_manufacturing_readiness_blockers", "Manufacturing Readiness Blockers", "Small Text", None, 0),
("Work Order", "makap_readiness_checked_by", "Readiness Checked By", "Link", "User", 1),
("Work Order", "makap_readiness_checked_on", "Readiness Checked On", "Datetime", None, 1),
("Work Order", "makap_bom_compatibility_status", "BOM Compatibility Status", "Select", "Not Checked\nCompatible\nIncompatible\nNot Applicable", 0),
("Work Order", "makap_bom_compatibility_confirmed_by", "BOM Compatibility Confirmed By", "Link", "User", 1),
("Work Order", "makap_bom_compatibility_confirmed_on", "BOM Compatibility Confirmed On", "Datetime", None, 1),
("Work Order", "makap_process_compatibility_status", "Process Compatibility Status", "Select", "Not Checked\nCompatible\nIncompatible\nNot Applicable", 0),
("Work Order", "makap_tooling_readiness_status", "Tooling Readiness Status", "Select", "Not Checked\nReady\nBlocked\nNot Required", 0),
# Module 5 — bounded material accountability controls
("Item", "makap_material_control_class", "Material Control Class", "Select", "Standard\nControlled\nHigh Traceability", 0),
("Work Order", "makap_material_availability_status", "Material Availability Status", "Select", "Not Checked\nBlocked\nPartially Available\nAvailable", 0),
("Work Order", "makap_material_availability_checked_by", "Material Availability Checked By", "Link", "User", 1),
("Work Order", "makap_material_availability_checked_on", "Material Availability Checked On", "Datetime", None, 1),
("Work Order", "makap_material_availability_blocker_summary", "Material Availability Blocker Summary", "Small Text", None, 0),
("Work Order", "makap_material_close_status", "Material Close Status", "Select", "Open\nClosed\nException", 0),
("Work Order", "makap_material_close_checked_by", "Material Close Checked By", "Link", "User", 1),
("Work Order", "makap_material_close_checked_on", "Material Close Checked On", "Datetime", None, 1),
("Work Order", "makap_material_close_exception_summary", "Material Close Exception Summary", "Small Text", None, 0),
# Module 6 — quality eligibility and traceability context
("Quality Inspection", "makap_packaging_specification", "Packaging Specification", "Link", "Packaging Specification", 0),
("Quality Inspection", "makap_specification_revision", "Specification Revision", "Data", None, 1),
("Quality Inspection", "makap_work_order", "Work Order", "Link", "Work Order", 0),
("Quality Inspection", "makap_quality_gate_stage", "Quality Gate Stage", "Select", "First-Off\nIn-Process\nFinal\nIncoming\nOutgoing\nNot Applicable", 0),
("Quality Inspection", "makap_inspection_trigger", "Inspection Trigger", "Select", "Routine\nFirst-Off\nPeriodic\nPost-Adjustment\nPost-Rework\nComplaint\nRetrospective Hold", 0),
("Quality Inspection", "makap_quality_eligibility", "Quality Eligibility", "Select", "Not Determined\nEligible\nIneligible\nHold", 0),
("Quality Inspection", "makap_inspection_population_quantity", "Inspection Population Quantity", "Float", None, 0),
("Quality Inspection", "makap_population_uom", "Population UOM", "Link", "UOM", 0),
("Quality Inspection", "makap_sampling_unit", "Sampling Unit", "Data", None, 0),
("Quality Inspection", "makap_quality_disposition", "Quality Disposition", "Select", "Pending\nRelease\nHold\nReject\nRework\nScrap\nConcession", 0),
("Quality Inspection", "makap_disposition_quantity", "Disposition Quantity", "Float", None, 0),
("Quality Inspection", "makap_disposition_uom", "Disposition UOM", "Link", "UOM", 0),
("Quality Inspection", "makap_disposition_by", "Disposition By", "Link", "User", 1),
("Quality Inspection", "makap_disposition_on", "Disposition On", "Datetime", None, 1),
("Quality Inspection", "makap_disposition_reason", "Disposition Reason", "Small Text", None, 0),
("Quality Inspection", "makap_defect_failure_category", "Defect / Failure Category", "Data", None, 0),
("Quality Inspection", "makap_quality_exception_type", "Quality Exception Type", "Select", "None\nConcession\nReinspection\nRetrospective Hold\nTraceability Exception", 0),
("Quality Inspection", "makap_reinspection_of", "Reinspection Of", "Link", "Quality Inspection", 0),
("Quality Inspection", "makap_reinspection_type", "Reinspection Type", "Select", "Full\nPartial\nTargeted", 0),
("Quality Inspection", "makap_concession_scope", "Concession Scope", "Small Text", None, 0),
("Quality Inspection", "makap_concession_quantity", "Concession Quantity", "Float", None, 0),
("Quality Inspection", "makap_concession_uom", "Concession UOM", "Link", "UOM", 0),
("Quality Inspection", "makap_concession_reference", "Concession Reference", "Data", None, 0),
("Quality Inspection", "makap_customer_approval_required", "Customer Approval Required", "Check", None, 0),
("Quality Inspection", "makap_customer_approval_reference", "Customer Approval Reference", "Data", None, 0),
("Quality Inspection", "makap_inspection_equipment_reference", "Inspection Equipment Reference", "Data", None, 0),
("Quality Inspection", "makap_released_not_before", "Released Not Before", "Datetime", None, 0),
# Module 7 — planning, supply assurance and procurement evidence
("Production Plan Item", "makap_material_need_date", "Material Need Date", "Date", None, 0),
("Production Plan Item", "makap_coverage_mode", "Coverage Mode", "Select", "Pegged\nPooled", 0),
("Material Request Item", "makap_material_need_date", "Material Need Date", "Date", None, 0),
("Material Request Item", "makap_coverage_mode", "Coverage Mode", "Select", "Pegged\nPooled", 0),
("Purchase Order Item", "makap_sourcing_basis", "Sourcing Basis", "Select", "Approved Supplier\nCompetitive RFQ\nSingle Source\nEmergency\nOther", 0),
("Purchase Order Item", "makap_supplier_selection_reason", "Supplier Selection Reason", "Small Text", None, 0),
("Purchase Order Item", "makap_supplier_confirmation_status", "Supplier Confirmation Status", "Select", "Not Requested\nRequested\nConfirmed\nAt Risk\nDeclined", 0),
("Purchase Order Item", "makap_supplier_confirmation_reference", "Supplier Confirmation Reference", "Data", None, 0),
("Purchase Order Item", "makap_expected_eligible_date", "Expected Eligible Date", "Date", None, 0),
("Purchase Receipt Item", "makap_receipt_exception_type", "Receipt Exception Type", "Select", "None\nShort Receipt\nOver Receipt\nDamaged\nQuality Hold\nRejected\nDocumentation Exception", 0),
]
def ensure_custom_fields():
created = []
for dt, fieldname, label, fieldtype, options, read_only in FIELDS:
if not frappe.db.exists("DocType", dt):
continue
if frappe.db.exists("Custom Field", {"dt": dt, "fieldname": fieldname}):
continue
doc = frappe.get_doc(
{
"doctype": "Custom Field",
"dt": dt,
"fieldname": fieldname,
"label": label,
"fieldtype": fieldtype,
"options": options,
"read_only": read_only,
"insert_after": "amended_from" if dt in {"Sales Order", "Work Order", "Quality Inspection"} else None,
}
)
doc.insert(ignore_permissions=True)
created.append(f"{dt}.{fieldname}")
frappe.db.commit()
return created
@@ -0,0 +1,56 @@
import json
from pathlib import Path
import frappe
ROOT = Path(__file__).parent
SOURCE_DIR = ROOT / "schema_sources"
def ensure_module3_child_tables():
for filename in ("material_structure.json", "critical_acceptance_requirement.json"):
data = json.loads((SOURCE_DIR / filename).read_text(encoding="utf-8"))
name = data["name"]
if frappe.db.exists("DocType", name):
existing = frappe.get_doc("DocType", name)
existing.update(data)
existing.save(ignore_permissions=True)
else:
frappe.get_doc(data).insert(ignore_permissions=True)
frappe.db.commit()
return ["Material Structure", "Critical Acceptance Requirement"]
def ensure_makap_backend_module():
if not frappe.db.exists("Module Def", "makap_backend"):
frappe.get_doc(
{
"doctype": "Module Def",
"module_name": "makap_backend",
"app_name": "makap_backend",
"custom": 1,
}
).insert(ignore_permissions=True)
frappe.db.commit()
return "makap_backend"
def ensure_module3_child_tables():
for filename in ("material_structure.json", "critical_acceptance_requirement.json"):
data = json.loads((SOURCE_DIR / filename).read_text(encoding="utf-8"))
data["module"] = "makap_backend"
name = data["name"]
if frappe.db.exists("DocType", name):
existing = frappe.get_doc("DocType", name)
existing.update(data)
existing.module = "makap_backend"
existing.save(ignore_permissions=True)
else:
frappe.get_doc(data).insert(ignore_permissions=True)
frappe.db.commit()
return ["Material Structure", "Critical Acceptance Requirement"]
@@ -0,0 +1,349 @@
import json
import frappe
SHORTCUTS = [
("Customer Onboarding Request", "Customer Onboarding Request"),
("Packaging Specification", "Packaging Specification"),
("Material Structure", "Material Structure"),
("Critical Acceptance Requirement", "Critical Acceptance Requirement"),
]
def ensure_makap_workspace():
content = [
{
"id": "makap_backend_header",
"type": "header",
"data": {"text": "MAKAP Backend", "col": 12},
}
]
for index, (label, doctype_name) in enumerate(SHORTCUTS, start=1):
content.append(
{
"id": f"makap_shortcut_{index}",
"type": "shortcut",
"data": {
"shortcut_name": label,
"col": 3,
"link_to": doctype_name,
"type": "DocType",
},
}
)
if frappe.db.exists("Workspace", "MAKAP Backend"):
workspace = frappe.get_doc("Workspace", "MAKAP Backend")
workspace.title = "MAKAP Backend"
workspace.label = "MAKAP Backend"
workspace.is_hidden = 0
workspace.public = 1
workspace.content = json.dumps(content)
workspace.set("links", [])
else:
workspace = frappe.get_doc(
{
"doctype": "Workspace",
"name": "MAKAP Backend",
"title": "MAKAP Backend",
"label": "MAKAP Backend",
"is_hidden": 0,
"public": 1,
"content": json.dumps(content),
"links": [],
}
)
for label, doctype_name in SHORTCUTS:
workspace.append(
"links",
{
"label": label,
"link_type": "DocType",
"link_to": doctype_name,
"type": "Card Break",
},
)
workspace.save(ignore_permissions=True)
frappe.db.commit()
return "MAKAP Backend"
def ensure_makap_workspace():
content = [
{"id": "makap_backend_header", "type": "header", "data": {"text": "MAKAP Backend", "col": 12}}
]
for index, (label, doctype_name) in enumerate(SHORTCUTS, start=1):
content.append(
{
"id": f"makap_shortcut_{index}",
"type": "shortcut",
"data": {"shortcut_name": label, "col": 3, "link_to": doctype_name, "type": "DocType"},
}
)
values = {
"title": "MAKAP Backend",
"label": "MAKAP Backend",
"is_hidden": 0,
"public": 1,
"content": json.dumps(content),
}
if frappe.db.exists("Workspace", "MAKAP Backend"):
workspace = frappe.get_doc("Workspace", "MAKAP Backend")
workspace.update(values)
workspace.set("links", [])
workspace.save(ignore_permissions=True)
else:
values.update({"doctype": "Workspace", "name": "MAKAP Backend", "links": []})
workspace = frappe.get_doc(values)
workspace.insert(ignore_permissions=True)
frappe.db.commit()
return "MAKAP Backend"
def ensure_makap_workspace():
workspace = frappe.get_doc("Workspace", "MAKAP Backend")
workspace.set("shortcuts", [])
for label, doctype_name in SHORTCUTS:
workspace.append(
"shortcuts",
{
"shortcut_name": label,
"type": "DocType",
"link_to": doctype_name,
"icon": "list",
"color": "Blue",
},
)
workspace.set("links", [])
workspace.save(ignore_permissions=True)
frappe.db.commit()
return "MAKAP Backend"
def ensure_makap_workspace():
workspace = frappe.get_doc("Workspace", "MAKAP Backend")
workspace.set("shortcuts", [])
for label, doctype_name in SHORTCUTS:
workspace.append(
"shortcuts",
{
"label": label,
"type": "DocType",
"link_to": doctype_name,
"icon": "list",
"color": "Blue",
},
)
workspace.set("links", [])
workspace.save(ignore_permissions=True)
frappe.db.commit()
return "MAKAP Backend"
SHORTCUTS = [
("Customer Onboarding Request", "Customer Onboarding Request"),
("Packaging Specification", "Packaging Specification"),
("Customer", "Customer"),
("Contact", "Contact"),
("Opportunity", "Opportunity"),
("Quotation", "Quotation"),
("Sales Order", "Sales Order"),
("Item", "Item"),
("Operation", "Operation"),
("Work Order", "Work Order"),
("Quality Inspection", "Quality Inspection"),
("Production Plan", "Production Plan"),
("Material Request", "Material Request"),
("Purchase Order", "Purchase Order"),
("Purchase Receipt", "Purchase Receipt"),
]
def ensure_makap_workspace():
workspace = frappe.get_doc("Workspace", "MAKAP Backend")
workspace.set("shortcuts", [])
for label, doctype_name in SHORTCUTS:
workspace.append(
"shortcuts",
{
"label": label,
"type": "DocType",
"link_to": doctype_name,
"icon": "list",
"color": "Blue",
},
)
workspace.set("links", [])
workspace.save(ignore_permissions=True)
frappe.db.commit()
return len(SHORTCUTS)
def get_makap_workspace_shortcuts():
workspace = frappe.get_doc("Workspace", "MAKAP Backend")
return [
{"label": row.label, "type": row.type, "link_to": row.link_to}
for row in workspace.shortcuts
]
def ensure_makap_workspace():
workspace = frappe.get_doc("Workspace", "MAKAP Backend")
workspace.content = "[]"
workspace.set("links", [])
for label, doctype_name in SHORTCUTS:
workspace.append(
"links",
{
"label": label,
"type": "Link",
"link_type": "DocType",
"link_to": doctype_name,
"onboard": 0,
},
)
workspace.save(ignore_permissions=True)
frappe.db.commit()
return len(workspace.links)
def get_makap_workspace_links():
workspace = frappe.get_doc("Workspace", "MAKAP Backend")
return [
{"idx": row.idx, "label": row.label, "type": row.type, "link_type": row.link_type, "link_to": row.link_to}
for row in workspace.links
]
def ensure_makap_workspace():
workspace = frappe.get_doc("Workspace", "MAKAP Backend")
workspace.set("links", [])
workspace.set("shortcuts", [])
blocks = [
{"id": "makap_backend_header", "type": "header", "data": {"text": "MAKAP Backend Operations", "col": 12}}
]
for index, (label, doctype_name) in enumerate(SHORTCUTS, start=1):
workspace.append(
"shortcuts",
{
"label": label,
"type": "DocType",
"link_to": doctype_name,
"doc_view": "List",
"icon": "list",
"color": "Blue",
},
)
blocks.append(
{
"id": f"makap_shortcut_{index}",
"type": "shortcut",
"data": {
"shortcut_name": label,
"col": 3,
"link_to": doctype_name,
"type": "DocType",
"doc_view": "List",
"icon": "list",
"color": "Blue",
},
}
)
workspace.content = json.dumps(blocks)
workspace.save(ignore_permissions=True)
frappe.db.commit()
return {"shortcuts": len(workspace.shortcuts), "links": len(workspace.links)}
CUSTOMER_COMMERCIAL_SHORTCUTS = [
("Customer Onboarding Request", "Customer Onboarding Request"),
("Customer", "Customer"),
("Contact", "Contact"),
("Opportunity", "Opportunity"),
("Quotation", "Quotation"),
("Sales Order", "Sales Order"),
("Packaging Specification", "Packaging Specification"),
("Item", "Item"),
("Operation", "Operation"),
("Work Order", "Work Order"),
]
INTERNAL_OPERATIONS_SHORTCUTS = [
("Quality Inspection", "Quality Inspection"),
("Production Plan", "Production Plan"),
("Material Request", "Material Request"),
("Purchase Order", "Purchase Order"),
("Purchase Receipt", "Purchase Receipt"),
]
def ensure_makap_workspace():
workspace = frappe.get_doc("Workspace", "MAKAP Backend")
workspace.set("links", [])
workspace.set("shortcuts", [])
blocks = []
shortcut_index = 1
for section_title, section_shortcuts in [
("Customer & Commercial Operations — Modules 1–4", CUSTOMER_COMMERCIAL_SHORTCUTS),
("Internal Operations — Modules 5–7", INTERNAL_OPERATIONS_SHORTCUTS),
]:
blocks.append(
{
"id": f"makap_section_{shortcut_index}",
"type": "header",
"data": {"text": section_title, "col": 12},
}
)
for label, doctype_name in section_shortcuts:
workspace.append(
"shortcuts",
{
"label": label,
"type": "DocType",
"link_to": doctype_name,
"doc_view": "List",
"icon": "list",
"color": "Blue",
},
)
blocks.append(
{
"id": f"makap_shortcut_{shortcut_index}",
"type": "shortcut",
"data": {
"shortcut_name": label,
"col": 3,
"link_to": doctype_name,
"type": "DocType",
"doc_view": "List",
"icon": "list",
"color": "Blue",
},
}
)
shortcut_index += 1
workspace.content = json.dumps(blocks)
workspace.save(ignore_permissions=True)
frappe.db.commit()
return {
"customer_commercial": len(CUSTOMER_COMMERCIAL_SHORTCUTS),
"internal_operations": len(INTERNAL_OPERATIONS_SHORTCUTS),
"total": len(workspace.shortcuts),
}
+1 -1
View File
@@ -1 +1 @@
Makap Backend makap_backend