Proper management of the custom fields need to be implemented.
When it is installed on a site the custom fields and adjustments are made. When it is uninstalled it should remove the custom fields and customizations so that the system can function as if it was not installed.
Sample Code from another app, this is a single file managing customizations to fields:
customfields.py
import frappe
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
def setup_custom_fields():
"""Setup custom fields for ERPNext integration with LiftLogic"""
**# Fields to delete if they exist (for clean re-runs)
fields_to_delete_if_exist = [**
# Customer fields
"custom_gym_member",
"custom_gym_trainer",
"custom_primary_branch",
"custom_membership_status",
"custom_last_visit_date",
"custom_total_visits",
# Sales Invoice fields
"custom_gym_membership",
"custom_gym_trainer_subscription",
"custom_gym_branch",
"custom_membership_period",
"custom_trainer_commission_rate",
# Payment Entry fields
"custom_gym_member",
"custom_gym_branch",
"custom_payment_type",
"custom_gym_payment_reference",
]
**# Delete existing custom fields for clean installation**
for fieldname in fields_to_delete_if_exist:
# Try Customer fields
custom_field_name = f"Customer-{fieldname}"
if frappe.db.exists("Custom Field", custom_field_name):
try:
frappe.delete_doc("Custom Field", custom_field_name, ignore_permissions=True, force=True)
frappe.db.commit()
print(f"Deleted existing custom field: {custom_field_name}")
except Exception as e:
print(f"Error deleting custom field {custom_field_name}: {e}")
# Try Sales Invoice fields
custom_field_name = f"Sales Invoice-{fieldname}"
if frappe.db.exists("Custom Field", custom_field_name):
try:
frappe.delete_doc("Custom Field", custom_field_name, ignore_permissions=True, force=True)
frappe.db.commit()
print(f"Deleted existing custom field: {custom_field_name}")
except Exception as e:
print(f"Error deleting custom field {custom_field_name}: {e}")
# Try Payment Entry fields
custom_field_name = f"Payment Entry-{fieldname}"
if frappe.db.exists("Custom Field", custom_field_name):
try:
frappe.delete_doc("Custom Field", custom_field_name, ignore_permissions=True, force=True)
frappe.db.commit()
print(f"Deleted existing custom field: {custom_field_name}")
except Exception as e:
print(f"Error deleting custom field {custom_field_name}: {e}")
**# Define custom fields for each DocType
custom_fields = {**
"Customer": [
{
"fieldname": "liftlogic_section",
"fieldtype": "Section Break",
"label": "Gym Information",
"insert_after": "customer_details",
"collapsible": 1,
},
{
"fieldname": "custom_gym_member",
"fieldtype": "Link",
"label": "Gym Member",
"options": "Gym Member",
"insert_after": "liftlogic_section",
"no_copy": 1,
"read_only": 1,
"in_list_view": 1,
"search_index": 1,
},
{
"fieldname": "custom_primary_branch",
"fieldtype": "Link",
"label": "Primary Branch",
"options": "Gym Branch",
"insert_after": "custom_gym_member",
"fetch_from": "custom_gym_member.primary_branch",
"read_only": 1,
},
{
"fieldname": "liftlogic_column_break",
"fieldtype": "Column Break",
"insert_after": "custom_primary_branch",
},
{
"fieldname": "custom_membership_status",
"fieldtype": "Select",
"label": "Membership Status",
"options": "Lead\nTrial\nActive\nInactive\nGuest",
"insert_after": "liftlogic_column_break",
"fetch_from": "custom_gym_member.status",
"read_only": 1,
"in_list_view": 1,
},
{
"fieldname": "custom_last_visit_date",
"fieldtype": "Date",
"label": "Last Visit Date",
"insert_after": "custom_membership_status",
"read_only": 1,
},
{
"fieldname": "custom_total_visits",
"fieldtype": "Int",
"label": "Total Visits",
"insert_after": "custom_last_visit_date",
"read_only": 1,
"default": 0,
},
{
"fieldname": "custom_gym_trainer",
"fieldtype": "Link",
"label": "Gym Trainer",
"options": "Gym Trainer",
"insert_after": "custom_total_visits",
"read_only": 1,
},
],
"Sales Invoice": [
{
"fieldname": "liftlogic_section",
"fieldtype": "Section Break",
"label": "Gym Information",
"insert_after": "customer",
"collapsible": 1,
},
{
"fieldname": "custom_gym_membership",
"fieldtype": "Link",
"label": "Gym Membership",
"options": "Gym Membership",
"insert_after": "liftlogic_section",
"no_copy": 1,
"read_only": 1,
"in_list_view": 1,
},
{
"fieldname": "custom_gym_trainer_subscription",
"fieldtype": "Link",
"label": "Gym Trainer Subscription",
"options": "Gym Trainer Subscription",
"insert_after": "custom_gym_membership",
"no_copy": 1,
"read_only": 1,
},
{
"fieldname": "liftlogic_column_break",
"fieldtype": "Column Break",
"insert_after": "custom_gym_trainer_subscription",
},
{
"fieldname": "custom_gym_branch",
"fieldtype": "Link",
"label": "Gym Branch",
"options": "Gym Branch",
"insert_after": "liftlogic_column_break",
"read_only": 1,
},
{
"fieldname": "custom_membership_period",
"fieldtype": "Data",
"label": "Membership Period",
"insert_after": "custom_gym_branch",
"read_only": 1,
},
{
"fieldname": "custom_trainer_commission_rate",
"fieldtype": "Float",
"label": "Trainer Commission Rate (%)",
"insert_after": "custom_membership_period",
"precision": 2,
"read_only": 1,
},
],
"Payment Entry": [
{
"fieldname": "liftlogic_section",
"fieldtype": "Section Break",
"label": "Gym Information",
"insert_after": "party",
"collapsible": 1,
},
{
"fieldname": "custom_gym_member",
"fieldtype": "Link",
"label": "Gym Member",
"options": "Gym Member",
"insert_after": "liftlogic_section",
"read_only": 1,
},
{
"fieldname": "custom_gym_branch",
"fieldtype": "Link",
"label": "Gym Branch",
"options": "Gym Branch",
"insert_after": "custom_gym_member",
"read_only": 1,
},
{
"fieldname": "liftlogic_column_break",
"fieldtype": "Column Break",
"insert_after": "custom_gym_branch",
},
{
"fieldname": "custom_payment_type",
"fieldtype": "Select",
"label": "Payment Type",
"options": "Membership\nTrainer Subscription\nAdditional Service\nOther",
"insert_after": "liftlogic_column_break",
"read_only": 1,
},
{
"fieldname": "custom_gym_payment_reference",
"fieldtype": "Link",
"label": "Gym Payment Reference",
"options": "Gym Payment",
"insert_after": "custom_payment_type",
"read_only": 1,
},
],
"Asset": [
{
"fieldname": "liftlogic_facilities_section",
"fieldtype": "Section Break",
"label": "Gym Facilities Information",
"insert_after": "section_break_jtou",
"collapsible": 1,
},
{
"fieldname": "custom_gym_equipment",
"fieldtype": "Link",
"label": "Gym Equipment",
"options": "Gym Equipment",
"insert_after": "liftlogic_facilities_section",
"read_only": 1,
},
{
"fieldname": "custom_gym_room",
"fieldtype": "Link",
"label": "Gym Room",
"options": "Gym Room",
"insert_after": "custom_gym_equipment",
"read_only": 1,
},
{
"fieldname": "custom_gym_branch",
"fieldtype": "Link",
"label": "Gym Branch",
"options": "Gym Branch",
"insert_after": "custom_gym_room",
},
{
"fieldname": "liftlogic_column_break",
"fieldtype": "Column Break",
"insert_after": "custom_gym_branch",
},
{
"fieldname": "custom_equipment_type",
"fieldtype": "Select",
"label": "Equipment Type",
"options": "Cardio\nStrength\nFree Weights\nFunctional\nSpecialty\nFacility\nLocker\nFurniture",
"insert_after": "liftlogic_column_break",
},
{
"fieldname": "custom_room_capacity",
"fieldtype": "Int",
"label": "Room Capacity",
"insert_after": "custom_equipment_type",
},
{
"fieldname": "custom_usage_hours",
"fieldtype": "Float",
"label": "Daily Usage Hours",
"insert_after": "custom_room_capacity",
"precision": 2,
},
{
"fieldname": "custom_maintenance_frequency",
"fieldtype": "Select",
"label": "Maintenance Frequency",
"options": "Daily\nWeekly\nMonthly\nQuarterly\nYearly",
"insert_after": "custom_usage_hours",
"default": "Monthly",
},
{
"fieldname": "custom_last_maintenance_date",
"fieldtype": "Date",
"label": "Last Maintenance Date",
"insert_after": "custom_maintenance_frequency",
},
{
"fieldname": "custom_next_maintenance_date",
"fieldtype": "Date",
"label": "Next Maintenance Date",
"insert_after": "custom_last_maintenance_date",
},
{
"fieldname": "custom_warranty_expiry",
"fieldtype": "Date",
"label": "Warranty Expiry Date",
"insert_after": "custom_next_maintenance_date",
},
],
}
**# Create the custom fields
create_custom_fields(custom_fields)
# Setup property setters (simplified)
setup_property_setters()
# Setup client scripts
setup_client_scripts()**
print("LiftLogic custom fields setup completed successfully!")
def setup_property_setters():
"""Setup property setters for customizing field behavior"""
properties = {
# Customer DocType customizations
"Customer": {
"customer_name": {"label": "Customer/Member Name"},
},
}
for doctype, doctype_properties in properties.items():
for fieldname, props in doctype_properties.items():
for prop, value in props.items():
try:
frappe.make_property_setter({
"doctype": doctype,
"fieldname": fieldname,
"property": prop,
"value": value,
})
except Exception as e:
print(f"Error setting property {prop} for {doctype}.{fieldname}: {e}")
def setup_client_scripts():
"""Setup client scripts for enhanced functionality"""
# Customer client script for gym member integration
customer_script = """
frappe.ui.form.on('Customer', {
refresh: function(frm) {
if(frm.doc.custom_gym_member) {
// Add button to view gym member
frm.add_custom_button(__('View Gym Member'), function() {
frappe.set_route('Form', 'Gym Member', frm.doc.custom_gym_member);
}, __('Actions'));
// Update visit information
update_visit_info(frm);
}
},
custom_gym_member: function(frm) {
if(frm.doc.custom_gym_member) {
// Fetch gym member details
frappe.db.get_doc('Gym Member', frm.doc.custom_gym_member).then(function(gym_member) {
frm.set_value('customer_primary_email', gym_member.email);
frm.set_value('customer_primary_phone', gym_member.phone);
frm.set_value('custom_membership_status', gym_member.status);
});
}
}
});
function update_visit_info(frm) {
if(frm.doc.custom_gym_member) {
// Get visit count from Gym Access Log
frappe.call({
method: 'frappe.client.get_count',
args: {
doctype: 'Gym Access Log',
filters: {
member: frm.doc.custom_gym_member
}
},
callback: function(r) {
if(r.message) {
frm.set_value('custom_total_visits', r.message);
}
}
});
// Get last visit date
frappe.call({
method: 'frappe.client.get_list',
args: {
doctype: 'Gym Access Log',
filters: {
member: frm.doc.custom_gym_member
},
fields: ['access_time'],
order_by: 'access_time desc',
limit: 1
},
callback: function(r) {
if(r.message && r.message.length > 0) {
frm.set_value('custom_last_visit_date', frappe.datetime.get_date_str(r.message[0].access_time));
}
}
});
}
}
"""
# Sales Invoice client script for gym integration
sales_invoice_script = """
frappe.ui.form.on('Sales Invoice', {
refresh: function(frm) {
if(frm.doc.custom_gym_membership) {
// Add button to view gym membership
frm.add_custom_button(__('View Membership'), function() {
frappe.set_route('Form', 'Gym Membership', frm.doc.custom_gym_membership);
}, __('Actions'));
}
if(frm.doc.custom_gym_trainer_subscription) {
// Add button to view trainer subscription
frm.add_custom_button(__('View Trainer Subscription'), function() {
frappe.set_route('Form', 'Gym Trainer Subscription', frm.doc.custom_gym_trainer_subscription);
}, __('Actions'));
}
},
customer: function(frm) {
if(frm.doc.customer) {
// Check if customer is linked to a gym member
frappe.db.get_value('Customer', frm.doc.customer, 'custom_gym_member', function(r) {
if(r && r.custom_gym_member) {
// Get gym member's primary branch
frappe.db.get_value('Gym Member', r.custom_gym_member, 'primary_branch', function(branch_r) {
if(branch_r && branch_r.primary_branch) {
frm.set_value('custom_gym_branch', branch_r.primary_branch);
}
});
}
});
}
}
});
"""
# Payment Entry client script for gym integration
payment_entry_script = """
frappe.ui.form.on('Payment Entry', {
refresh: function(frm) {
if(frm.doc.custom_gym_member) {
// Add button to view gym member
frm.add_custom_button(__('View Gym Member'), function() {
frappe.set_route('Form', 'Gym Member', frm.doc.custom_gym_member);
}, __('Actions'));
}
},
party: function(frm) {
if(frm.doc.party_type === 'Customer' && frm.doc.party) {
// Check if customer is linked to a gym member
frappe.db.get_value('Customer', frm.doc.party, 'custom_gym_member', function(r) {
if(r && r.custom_gym_member) {
frm.set_value('custom_gym_member', r.custom_gym_member);
// Get gym member's primary branch
frappe.db.get_value('Gym Member', r.custom_gym_member, 'primary_branch', function(branch_r) {
if(branch_r && branch_r.primary_branch) {
frm.set_value('custom_gym_branch', branch_r.primary_branch);
}
});
}
});
}
}
});
"""
# Asset client script for gym facilities integration
asset_script = """
frappe.ui.form.on('Asset', {
refresh: function(frm) {
if(frm.doc.custom_gym_equipment) {
// Add button to view gym equipment
frm.add_custom_button(__('View Gym Equipment'), function() {
frappe.set_route('Form', 'Gym Equipment', frm.doc.custom_gym_equipment);
}, __('Gym Actions'));
}
if(frm.doc.custom_gym_room) {
// Add button to view gym room
frm.add_custom_button(__('View Gym Room'), function() {
frappe.set_route('Form', 'Gym Room', frm.doc.custom_gym_room);
}, __('Gym Actions'));
}
if(frm.doc.custom_gym_branch) {
// Add button to view gym branch
frm.add_custom_button(__('View Gym Branch'), function() {
frappe.set_route('Form', 'Gym Branch', frm.doc.custom_gym_branch);
}, __('Gym Actions'));
}
// Add maintenance scheduling button for gym equipment
if(frm.doc.custom_gym_equipment && frm.doc.docstatus === 1) {
frm.add_custom_button(__('Schedule Maintenance'), function() {
frappe.call({
method: 'liftlogic.facilities_integration.create_maintenance_from_asset',
args: {
asset_name: frm.doc.name
},
callback: function(r) {
if(r.message) {
frappe.msgprint(__('Maintenance scheduled successfully'));
frm.reload_doc();
}
}
});
}, __('Gym Actions'));
}
},
custom_gym_equipment: function(frm) {
if(frm.doc.custom_gym_equipment) {
// Fetch equipment details
frappe.db.get_doc('Gym Equipment', frm.doc.custom_gym_equipment).then(function(equipment) {
if(equipment.location && frm.doc.location !== equipment.location) {
frm.set_value('location', equipment.location);
}
// Set equipment type and other details
frappe.call({
method: 'liftlogic.facilities_integration.get_equipment_type',
args: {
item_code: equipment.item_code
},
callback: function(r) {
if(r.message) {
frm.set_value('custom_equipment_type', r.message);
}
}
});
});
}
}
});
"""
**# Create or update client scripts
scripts = [**
{"doctype": "Customer", "script": customer_script, "name": "Customer-LiftLogic"},
{"doctype": "Sales Invoice", "script": sales_invoice_script, "name": "Sales Invoice-LiftLogic"},
{"doctype": "Payment Entry", "script": payment_entry_script, "name": "Payment Entry-LiftLogic"},
{"doctype": "Asset", "script": asset_script, "name": "Asset-LiftLogic"},
]
for script_config in scripts:
if not frappe.db.exists("Client Script", {"dt": script_config["doctype"], "name": script_config["name"]}):
try:
frappe.get_doc({
"doctype": "Client Script",
"dt": script_config["doctype"],
"script": script_config["script"],
"enabled": 1,
"name": script_config["name"]
}).insert()
print(f"Created client script: {script_config['name']}")
except Exception as e:
print(f"Error creating client script {script_config['name']}: {e}")
else:
try:
# Update existing script
client_script = frappe.get_doc("Client Script", script_config["name"])
client_script.script = script_config["script"]
client_script.save()
print(f"Updated client script: {script_config['name']}")
except Exception as e:
print(f"Error updating client script {script_config['name']}: {e}")
def create_gym_customer_group():
"""Create Gym Members customer group if it doesn't exist"""
if not frappe.db.exists("Customer Group", "Gym Members"):
try:
frappe.get_doc({
"doctype": "Customer Group",
"customer_group_name": "Gym Members",
"parent_customer_group": "All Customer Groups",
"is_group": 0
}).insert()
print("Created Customer Group: Gym Members")
except Exception as e:
print(f"Error creating Customer Group 'Gym Members': {e}")
def setup_naming_series():
"""Setup naming series for gym-related documents"""
# Skip naming series setup as it's causing issues
# Users can manually set up naming series if needed
print("Naming series setup skipped - can be configured manually in DocType settings")
Proper management of the custom fields need to be implemented.
When it is installed on a site the custom fields and adjustments are made. When it is uninstalled it should remove the custom fields and customizations so that the system can function as if it was not installed.
Sample Code from another app, this is a single file managing customizations to fields:
customfields.py
import frappe
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
def setup_custom_fields():
"""Setup custom fields for ERPNext integration with LiftLogic"""
def setup_property_setters():
"""Setup property setters for customizing field behavior"""
def setup_client_scripts():
"""Setup client scripts for enhanced functionality"""
frappe.ui.form.on('Customer', {
refresh: function(frm) {
if(frm.doc.custom_gym_member) {
// Add button to view gym member
frm.add_custom_button(__('View Gym Member'), function() {
frappe.set_route('Form', 'Gym Member', frm.doc.custom_gym_member);
}, __('Actions'));
});
function update_visit_info(frm) {
if(frm.doc.custom_gym_member) {
// Get visit count from Gym Access Log
frappe.call({
method: 'frappe.client.get_count',
args: {
doctype: 'Gym Access Log',
filters: {
member: frm.doc.custom_gym_member
}
},
callback: function(r) {
if(r.message) {
frm.set_value('custom_total_visits', r.message);
}
}
});
}
"""
frappe.ui.form.on('Sales Invoice', {
refresh: function(frm) {
if(frm.doc.custom_gym_membership) {
// Add button to view gym membership
frm.add_custom_button(__('View Membership'), function() {
frappe.set_route('Form', 'Gym Membership', frm.doc.custom_gym_membership);
}, __('Actions'));
}
});
"""
frappe.ui.form.on('Payment Entry', {
refresh: function(frm) {
if(frm.doc.custom_gym_member) {
// Add button to view gym member
frm.add_custom_button(__('View Gym Member'), function() {
frappe.set_route('Form', 'Gym Member', frm.doc.custom_gym_member);
}, __('Actions'));
}
},
});
"""
frappe.ui.form.on('Asset', {
refresh: function(frm) {
if(frm.doc.custom_gym_equipment) {
// Add button to view gym equipment
frm.add_custom_button(__('View Gym Equipment'), function() {
frappe.set_route('Form', 'Gym Equipment', frm.doc.custom_gym_equipment);
}, __('Gym Actions'));
}
});
"""
def create_gym_customer_group():
"""Create Gym Members customer group if it doesn't exist"""
if not frappe.db.exists("Customer Group", "Gym Members"):
try:
frappe.get_doc({
"doctype": "Customer Group",
"customer_group_name": "Gym Members",
"parent_customer_group": "All Customer Groups",
"is_group": 0
}).insert()
print("Created Customer Group: Gym Members")
except Exception as e:
print(f"Error creating Customer Group 'Gym Members': {e}")
def setup_naming_series():
"""Setup naming series for gym-related documents"""
# Skip naming series setup as it's causing issues
# Users can manually set up naming series if needed
print("Naming series setup skipped - can be configured manually in DocType settings")