Implement MAKAP Modules 1-7 backend foundations
This commit is contained in:
@@ -247,3 +247,28 @@ app_license = "mit"
|
||||
# List of apps whose translatable strings should be excluded from this app's translations.
|
||||
# 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",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
@@ -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."
|
||||
)
|
||||
+67
@@ -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
|
||||
}
|
||||
+261
@@ -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
|
||||
}
|
||||
+25
@@ -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) {
|
||||
|
||||
// },
|
||||
// });
|
||||
+479
@@ -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
|
||||
+9
@@ -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
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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 @@
|
||||
Makap Backend
|
||||
makap_backend
|
||||
|
||||
Reference in New Issue
Block a user