@mcp.tool()
async def amend_document(
doctype: str,
name: str
) -> str:
"""
Amend a document in Frappe (create a new amended version of a cancelled document).
This tool handles document amendment by creating a new document with an amended name
(e.g., DOC-001-1, DOC-001-2) and copying all relevant field values from the original
cancelled document, establishing proper linkage via the 'amended_from' field.
Args:
doctype: DocType name
name: Document name (case-sensitive) - must be a cancelled document
Returns:
Success message with new amended document name if successful, or detailed
error information if amendment fails due to validation errors or constraints.
"""
try:
client = get_client()
# First, get the current document to check its status and get full data
try:
doc_response = await client.get(f"api/resource/{doctype}/{name}")
doc_data = doc_response.get("data", {})
current_docstatus = doc_data.get("docstatus", None)
if current_docstatus is None:
return f"Error: Could not retrieve document {doctype} '{name}'. Document may not exist."
if current_docstatus == 0:
return f"Document {doctype} '{name}' is in Draft status. Only cancelled documents (docstatus=2) can be amended."
if current_docstatus == 1:
return f"Document {doctype} '{name}' is submitted. You must cancel it first before amending."
if current_docstatus != 2:
return f"Document {doctype} '{name}' has unexpected status (docstatus={current_docstatus}). Only cancelled documents (docstatus=2) can be amended."
except Exception as get_error:
return f"Error retrieving document for amendment: {get_error}"
# Generate amended document name
base_name = name
amended_counter = 1
# Check if this document is already an amendment (contains dash and number)
if '-' in name:
parts = name.rsplit('-', 1)
if len(parts) == 2 and parts[1].isdigit():
base_name = parts[0]
amended_counter = int(parts[1]) + 1
# Find the next available amended name
amended_name = f"{base_name}-{amended_counter}"
while True:
try:
# Check if amended name already exists
check_response = await client.get(f"api/resource/{doctype}/{amended_name}")
if "data" in check_response:
# Name exists, try next number
amended_counter += 1
amended_name = f"{base_name}-{amended_counter}"
else:
# Name doesn't exist, we can use it
break
except FrappeApiError as e:
# If we get 404, the name doesn't exist and we can use it
if e.status_code == 404:
break
else:
# Some other error, we should handle it
raise e
# Prepare amended document data
amended_doc = doc_data.copy()
# Clear system fields that should not be copied
system_fields = [
'name', 'creation', 'modified', 'modified_by', 'owner',
'docstatus', 'idx', '_user_tags', '_comments', '_assign', '_liked_by'
]
for field in system_fields:
amended_doc.pop(field, None)
# Set amendment fields
amended_doc['name'] = amended_name
amended_doc['amended_from'] = name
amended_doc['docstatus'] = 0 # New document starts as draft
# Clear any child table names to let Frappe generate new ones
for key, value in amended_doc.items():
if isinstance(value, list):
for item in value:
if isinstance(item, dict):
item.pop('name', None) # Clear child table row names
item['parent'] = amended_name # Update parent reference
# Create the amended document
response = await client.post(
f"api/resource/{doctype}",
json_data=amended_doc
)
if "data" in response:
created_doc = response["data"]
created_name = created_doc.get('name', amended_name)
return f"✅ Document successfully amended: {doctype} '{created_name}' created from cancelled document '{name}'. The amended document is in Draft status and ready for editing."
else:
return f"⚠️ Amendment may have succeeded but response format unexpected: {json.dumps(response, indent=2)}"
except FrappeApiError as api_error:
# Handle specific Frappe API errors with detailed information
if api_error.response_data:
error_data = api_error.response_data
# Check for validation errors in the response
if "exception" in error_data:
exception_msg = error_data["exception"]
# Extract user-friendly error messages
if "ValidationError" in str(exception_msg):
# Common amendment validation errors
if "amended_from" in str(exception_msg).lower():
return (
f"❌ Amendment failed: {doctype} does not have an 'amended_from' field configured. "
f"This DocType may not support amendments. Contact your system administrator to enable amendment functionality."
)
elif "DuplicateEntryError" in str(exception_msg) or "duplicate" in str(exception_msg).lower():
return f"❌ Amendment failed: Document name conflict. The amended name may already exist. Please try again."
else:
# Generic validation error
return f"❌ Validation error: {exception_msg}. Please fix the validation issues before amending."
elif "PermissionError" in str(exception_msg):
return f"❌ Permission denied: You don't have sufficient permissions to amend {doctype} documents."
else:
# Other exceptions
return f"❌ Amendment failed: {exception_msg}"
# Check for server messages with more details
if "_server_messages" in error_data:
try:
messages = json.loads(error_data["_server_messages"])
if messages:
msg_data = json.loads(messages[0])
user_message = msg_data.get("message", "Unknown error")
return f"❌ Amendment failed: {user_message}"
except (json.JSONDecodeError, KeyError, IndexError):
pass
return f"❌ Amendment failed: {api_error}"
except Exception as error:
return _format_error_response(error, "amend_document")