We are implementing a custom export-import feature for Client Scripts, Server Scripts, Notifications, etc., to transfer them from one Frappe instance to another. To maintain consistency, we are using Frappe transactions—either all components (Client Scripts, Server Scripts, Notifications, etc.) should be imported, or none should be (i.e., a rollback should occur on failure).
The exported folder contains JSON files, each listing documents for a specific component. The relevant transaction-handling code is as follows:
def import_module_components(extracted_module_path, message):
“”“Import all module components using a centralized configuration”“”
try:
frappe.db.begin() # Start transaction
for component_key, config in COMPONENT_CONFIG.items():
try:
json_path = os.path.join(extracted_module_path, config['json_file'])
import_from_json(json_path, config['doctype'])
message = f"{message}\n{config['doctype']}: Imported successfully"
except Exception as e:
frappe.db.rollback() # Rollback on failure
print("ERROR OCCURRED WHILE IMPORTING COMPONENT ", component_key, str(e), traceback.format_exc())
return False, f"{message}\nError importing {config['doctype']}: {str(e)}\nAll changes have been rolled back."
frappe.db.commit() # Commit if everything succeeds
return True, message
except Exception as e:
frappe.db.rollback() # Ensure rollback on unexpected errors
print("ERROR OCCURRED WHILE IMPORTING COMPONENT ", component_key, str(e), traceback.format_exc())
return False, f"{message}\nUnexpected error during import: {str(e)}\nAll changes have been rolled back."
def import_from_json(json_path, doctype):
"""Generic import function for Frappe documents from JSON"""
if os.path.exists(json_path):
with open(json_path, 'r') as f:
items = json.load(f)
if not items:
return
for item_data in items:
try:
existing_item = frappe.db.exists(doctype, {'name': item_data.get('name')})
if existing_item:
doc = frappe.get_doc(doctype, existing_item)
for field, value in item_data.items():
if field not in ['modified', 'creation']:
setattr(doc, field, value)
doc.save()
else:
doc = frappe.get_doc({
'doctype': doctype,
**item_data
})
doc.insert()
except Exception as e:
raise Exception(f"Failed to import {doctype} '{item_data.get('name')}': {str(e)}")
The Issue:
When testing with an exported JSON file, Client Scripts and Server Scripts were imported correctly. However, when importing Notifications, an exception occurred:
sender is “XYZ” is not defined.
Surprisingly, the transaction did not roll back properly. The previously imported Client Scripts and Server Scripts remained in the database instead of being rolled back.
However, when I intentionally modified a Server Script JSON file to reference a non-existent parent Doctype, the transaction rolled back correctly—removing all previously imported Client Scripts before attempting to import the Server Script.
Why did the transaction fail to roll back when importing Notifications?
Do insert() and save() in Frappe perform any implicit auto-commit operations?
Could it be related to the way save() is handling updates or an issue with the Notification Doctype specifically?
Any insights would be greatly appreciated!