Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 43 additions & 24 deletions frappe_assistant_core/plugins/core/tools/metadata_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,21 @@ def execute_tool(tool_name: str, arguments: Dict[str, Any]) -> Dict[str, Any]:
else:
raise Exception(f"Unknown metadata tool: {tool_name}")

@staticmethod
def _serialize_field(field) -> Dict[str, Any]:
"""Serialize a DocField row into the shape returned by get_doctype_metadata."""
return {
"fieldname": field.fieldname,
"label": field.label,
"fieldtype": field.fieldtype,
"options": field.options,
"reqd": field.reqd,
"read_only": field.read_only,
"hidden": field.hidden,
"default": field.default,
"description": field.description,
}

@staticmethod
def get_doctype_metadata(doctype: str) -> Dict[str, Any]:
"""Get DocType metadata and field information"""
Expand All @@ -100,40 +115,44 @@ def get_doctype_metadata(doctype: str) -> Dict[str, Any]:

meta = frappe.get_meta(doctype)

# Build field information
fields = []
for field in meta.fields:
field_info = {
"fieldname": field.fieldname,
"label": field.label,
"fieldtype": field.fieldtype,
"options": field.options,
"reqd": field.reqd,
"read_only": field.read_only,
"hidden": field.hidden,
"default": field.default,
"description": field.description,
fields = [MetadataTools._serialize_field(field) for field in meta.fields]

link_fields = [
{"fieldname": field.fieldname, "label": field.label, "options": field.options}
for field in meta.get_link_fields()
]

# Child tables: include the child DocType's own field metadata so the
# caller can build nested row objects without a second tool call (#192).
child_tables = []
for table_field in meta.get_table_fields():
child_doctype = table_field.options
child_entry = {
"fieldname": table_field.fieldname,
"label": table_field.label,
"fieldtype": table_field.fieldtype,
"options": child_doctype,
"reqd": table_field.reqd,
"fields": [],
}
fields.append(field_info)

# Build link fields information
link_fields = []
for field in meta.get_link_fields():
link_fields.append(
{"fieldname": field.fieldname, "label": field.label, "options": field.options}
)
if child_doctype and frappe.db.exists("DocType", child_doctype):
child_meta = frappe.get_meta(child_doctype)
child_entry["fields"] = [MetadataTools._serialize_field(f) for f in child_meta.fields]
child_tables.append(child_entry)

return {
"success": True,
"doctype": doctype,
"module": meta.module,
"is_submittable": meta.is_submittable,
"is_tree": meta.is_tree,
"is_single": meta.istable,
"is_submittable": bool(meta.is_submittable),
"is_tree": bool(meta.is_tree),
"is_single": bool(meta.issingle),
"is_child_table": bool(meta.istable),
"naming_rule": meta.naming_rule,
"title_field": meta.title_field,
"fields": fields,
"link_fields": link_fields,
"child_tables": child_tables,
"permissions": [p.as_dict() for p in meta.permissions],
}

Expand Down
38 changes: 38 additions & 0 deletions frappe_assistant_core/tests/test_metadata_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,44 @@ def test_list_doctypes_custom_only(self):
def test_list_doctypes_with_module_filter(self):
self.skipTest("Module filter test placeholder")

def test_get_doctype_metadata_includes_child_tables(self):
"""Regression guard for #192: child tables must be surfaced with their own field metadata."""
from frappe_assistant_core.plugins.core.tools.metadata_tools import MetadataTools

# User ships with at least one Table field ("roles" -> "Has Role") in every Frappe install.
result = MetadataTools.get_doctype_metadata("User")

self.assertTrue(result.get("success"), result)
self.assertIn("child_tables", result)
child_tables = result["child_tables"]
self.assertIsInstance(child_tables, list)

roles_entry = next((c for c in child_tables if c["fieldname"] == "roles"), None)
self.assertIsNotNone(roles_entry, f"Expected 'roles' in child_tables, got {child_tables}")
self.assertEqual(roles_entry["options"], "Has Role")
self.assertIn(roles_entry["fieldtype"], ("Table", "Table MultiSelect"))

# Recursive child field metadata must be present so create_document has everything it needs.
self.assertTrue(roles_entry["fields"], "Child table 'roles' should expose its own fields")
child_field_names = {f["fieldname"] for f in roles_entry["fields"]}
self.assertIn("role", child_field_names)

def test_get_doctype_metadata_distinguishes_single_from_child_table(self):
"""Regression guard for #192: is_single must use meta.issingle, not meta.istable."""
from frappe_assistant_core.plugins.core.tools.metadata_tools import MetadataTools

# System Settings is a Single doctype (issingle=1, istable=0).
single_result = MetadataTools.get_doctype_metadata("System Settings")
self.assertTrue(single_result.get("success"), single_result)
self.assertTrue(single_result["is_single"])
self.assertFalse(single_result["is_child_table"])

# Has Role is a child table (issingle=0, istable=1).
child_result = MetadataTools.get_doctype_metadata("Has Role")
self.assertTrue(child_result.get("success"), child_result)
self.assertFalse(child_result["is_single"])
self.assertTrue(child_result["is_child_table"])


class TestMetadataToolsIntegration(BaseAssistantTest):
"""Integration tests for metadata tools"""
Expand Down
Loading