900 lines
36 KiB
Python
900 lines
36 KiB
Python
from curses import meta
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import frappe
|
|
from .utils import create_module
|
|
|
|
def after_install():
|
|
create_module()
|
|
add_custom_fields()
|
|
frappe.db.commit()
|
|
|
|
# Proper way to refresh metadata
|
|
frappe.clear_cache(doctype="Address")
|
|
frappe.reload_doctype("Address")
|
|
frappe.clear_cache(doctype="On-Site Meeting")
|
|
frappe.reload_doctype("On-Site Meeting")
|
|
update_onsite_meeting_fields()
|
|
update_address_fields()
|
|
build_frontend()
|
|
|
|
def after_migrate():
|
|
add_custom_fields()
|
|
update_onsite_meeting_fields()
|
|
frappe.db.commit()
|
|
|
|
# Proper way to refresh metadata for all doctypes with custom fields
|
|
doctypes_to_refresh = ["Lead", "Address", "Contact", "On-Site Meeting", "Quotation", "Sales Order", "Project Template"]
|
|
for doctype in doctypes_to_refresh:
|
|
frappe.clear_cache(doctype=doctype)
|
|
frappe.reload_doctype(doctype)
|
|
|
|
update_address_fields()
|
|
# build_frontend()
|
|
|
|
|
|
def build_frontend():
|
|
app_package_path = frappe.get_app_path("custom_ui")
|
|
app_root = os.path.dirname(app_package_path)
|
|
candidates = [
|
|
os.path.join(app_root, "frontend"),
|
|
os.path.join(app_package_path, "frontend")
|
|
]
|
|
|
|
frontend_path = None
|
|
for p in candidates:
|
|
if os.path.exists(p):
|
|
frontend_path = p
|
|
break
|
|
|
|
if not frontend_path:
|
|
frappe.log_error(message="No frontend directory found for custom_ui", title="Frontend Build Skipped")
|
|
print(f"⚠️ Frontend directory does not exist. Skipping build. Path was {frontend_path}")
|
|
return
|
|
|
|
dist_path = os.path.join(app_root, "custom_ui", "public", "dist")
|
|
should_build = True
|
|
|
|
if should_build:
|
|
print("\n📦 Building frontend for custom_ui...\n")
|
|
try:
|
|
subprocess.check_call(["npm", "install"], cwd=frontend_path)
|
|
subprocess.check_call(["npm", "run", "build"], cwd=frontend_path)
|
|
print("\n✅ Frontend build completed successfully.\n")
|
|
except subprocess.CalledProcessError as e:
|
|
frappe.log_error(message=str(e), title="Frontend Build Failed")
|
|
print(f"\n❌ Frontend build failed: {e}\n")
|
|
|
|
def add_custom_fields():
|
|
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
|
|
|
|
print("\n🔧 Adding custom fields to doctypes...")
|
|
|
|
try:
|
|
address_meta = frappe.get_meta("Address")
|
|
address_type_params = address_meta.get_field("address_type")
|
|
if address_type_params and "Service" not in (address_type_params.options or ""):
|
|
print(" Adding 'Service' to Address type options...")
|
|
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
|
|
make_property_setter("Address", "address_type", "options", (address_type_params.options or "") + "\nService", "DocField")
|
|
print(" ✅ Added 'Service' to Address address_type options.")
|
|
except Exception as e:
|
|
print(f" ⚠️ Failed to update Address address_type: {e}")
|
|
|
|
custom_fields = {
|
|
"Customer": [
|
|
dict(
|
|
fieldname="companies",
|
|
label="Companies",
|
|
fieldtype="Table",
|
|
options="Customer Company Link",
|
|
insert_after="customer_type"
|
|
),
|
|
dict(
|
|
fieldname="quotations",
|
|
label="Quotations",
|
|
fieldtype="Table",
|
|
options="Customer Quotation Link",
|
|
insert_after="companies"
|
|
),
|
|
dict(
|
|
fieldname="onsite_meetings",
|
|
label="On-Site Meetings",
|
|
fieldtype="Table",
|
|
options="Customer On-Site Meeting Link",
|
|
insert_after="quotations"
|
|
),
|
|
dict(
|
|
fieldname="projects",
|
|
label="Projects",
|
|
fieldtype="Table",
|
|
options="Customer Project Link",
|
|
insert_after="onsite_meetings"
|
|
),
|
|
dict(
|
|
fieldname="sales_orders",
|
|
label="Sales Orders",
|
|
fieldtype="Table",
|
|
options="Customer Sales Order Link",
|
|
insert_after="projects"
|
|
),
|
|
dict(
|
|
fieldname="from_lead",
|
|
label="From Lead",
|
|
fieldtype="Link",
|
|
options="Lead",
|
|
insert_after="customer_name"
|
|
),
|
|
dict(
|
|
fieldname="properties",
|
|
label="Properties",
|
|
fieldtype="Table",
|
|
options="Customer Address Link",
|
|
insert_after="customer_name"
|
|
),
|
|
dict(
|
|
fieldname="contacts",
|
|
label="Contacts",
|
|
fieldtype="Table",
|
|
options="Customer Contact Link",
|
|
insert_after="properties"
|
|
),
|
|
dict(
|
|
fieldname="primary_contact",
|
|
label="Primary Contact",
|
|
fieldtype="Link",
|
|
options="Contact",
|
|
insert_after="contacts"
|
|
)
|
|
],
|
|
"Lead": [
|
|
dict(
|
|
fieldname="onsite_meetings",
|
|
label="On-Site Meetings",
|
|
fieldtype="Table",
|
|
options="Lead On-Site Meeting Link",
|
|
insert_after="quotations"
|
|
),
|
|
dict(
|
|
fieldname="custom_billing_address",
|
|
label="Custom Address",
|
|
fieldtype="Link",
|
|
options="Address",
|
|
insert_after="customer_name"
|
|
),
|
|
dict(
|
|
fieldname="quotations",
|
|
label="Quotations",
|
|
fieldtype="Table",
|
|
options="Lead Quotation Link",
|
|
insert_after="companies"
|
|
),
|
|
dict(
|
|
fieldname="companies",
|
|
label="Companies",
|
|
fieldtype="Table",
|
|
options="Lead Company Link",
|
|
insert_after="customer_type"
|
|
),
|
|
dict(
|
|
fieldname="customer_type",
|
|
label="Customer Type",
|
|
fieldtype="Select",
|
|
options="Individual\nCompany\nPartnership",
|
|
insert_after="lead_name"
|
|
),
|
|
dict(
|
|
fieldname="properties",
|
|
label="Properties",
|
|
fieldtype="Table",
|
|
options="Lead Address Link",
|
|
insert_after="customer_name"
|
|
),
|
|
dict(
|
|
fieldname="contacts",
|
|
label="Contacts",
|
|
fieldtype="Table",
|
|
options="Lead Contact Link",
|
|
insert_after="properties"
|
|
),
|
|
dict(
|
|
fieldname="primary_contact",
|
|
label="Primary Contact",
|
|
fieldtype="Link",
|
|
options="Contact",
|
|
insert_after="contacts"
|
|
)
|
|
],
|
|
"Address": [
|
|
dict(
|
|
fieldname="primary_contact",
|
|
label="Primary Contact",
|
|
fieldtype="Link",
|
|
options="Contact",
|
|
insert_after="address_title"
|
|
),
|
|
dict(
|
|
fieldname="projects",
|
|
label="Projects",
|
|
fieldtype="Table",
|
|
options="Address Project Link",
|
|
insert_after="onsite_meetings"
|
|
),
|
|
dict(
|
|
fieldname="sales_orders",
|
|
label="Sales Orders",
|
|
fieldtype="Table",
|
|
options="Address Sales Order Link",
|
|
insert_after="projects"
|
|
),
|
|
dict(
|
|
fieldname="onsite_meetings",
|
|
label="On-Site Meetings",
|
|
fieldtype="Table",
|
|
options="Address On-Site Meeting Link",
|
|
insert_after="quotations"
|
|
),
|
|
dict(
|
|
fieldname="quotations",
|
|
label="Quotations",
|
|
fieldtype="Table",
|
|
options="Address Quotation Link",
|
|
insert_after="companies"
|
|
),
|
|
dict(
|
|
fieldname="full_address",
|
|
label="Full Address",
|
|
fieldtype="Data",
|
|
insert_after="country"
|
|
),
|
|
dict(
|
|
fieldname="latitude",
|
|
label="Latitude",
|
|
fieldtype="Float",
|
|
precision=8,
|
|
insert_after="full_address"
|
|
),
|
|
dict(
|
|
fieldname="longitude",
|
|
label="Longitude",
|
|
fieldtype="Float",
|
|
precision=8,
|
|
insert_after="latitude"
|
|
),
|
|
dict(
|
|
fieldname="onsite_meeting_scheduled",
|
|
label="On-Site Meeting Scheduled",
|
|
fieldtype="Select",
|
|
options="Not Started\nIn Progress\nCompleted",
|
|
default="Not Started",
|
|
insert_after="longitude"
|
|
),
|
|
dict(
|
|
fieldname="estimate_sent_status",
|
|
label="Estimate Sent Status",
|
|
fieldtype="Select",
|
|
options="Not Started\nIn Progress\nCompleted",
|
|
default="Not Started",
|
|
insert_after="onsite_meeting_scheduled"
|
|
),
|
|
dict(
|
|
fieldname="job_status",
|
|
label="Job Status",
|
|
fieldtype="Select",
|
|
options="Not Started\nIn Progress\nCompleted",
|
|
default="Not Started",
|
|
insert_after="estimate_sent_status"
|
|
),
|
|
dict(
|
|
fieldname="payment_received_status",
|
|
label="Payment Received Status",
|
|
fieldtype="Select",
|
|
options="Not Started\nIn Progress\nCompleted",
|
|
default="Not Started",
|
|
insert_after="job_status"
|
|
),
|
|
dict(
|
|
fieldname="lead_name",
|
|
label="Lead Name",
|
|
fieldtype="Data",
|
|
insert_after="custom_customer_to_bill"
|
|
),
|
|
dict(
|
|
fieldname="customer_type",
|
|
label="Customer Type",
|
|
fieldtype="Select",
|
|
options="Customer\nLead",
|
|
insert_after="lead_name"
|
|
),
|
|
dict(
|
|
fieldname="customer_name",
|
|
label="Customer Name",
|
|
fieldtype="Dynamic Link",
|
|
options="customer_type",
|
|
insert_after="customer_type"
|
|
),
|
|
dict(
|
|
fieldname="contacts",
|
|
label="Contacts",
|
|
fieldtype="Table",
|
|
options="Address Contact Link",
|
|
insert_after="customer_name"
|
|
),
|
|
dict(
|
|
fieldname="companies",
|
|
label="Companies",
|
|
fieldtype="Table",
|
|
options="Address Company Link",
|
|
insert_after="contacts"
|
|
)
|
|
],
|
|
"Contact": [
|
|
dict(
|
|
fieldname="role",
|
|
label="Role",
|
|
fieldtype="Select",
|
|
options="Owner\nProperty Manager\nTenant\nBuilder\nNeighbor\nFamily Member\nRealtor\nOther",
|
|
insert_after="designation"
|
|
),
|
|
dict(
|
|
fieldname="email",
|
|
label="Email",
|
|
fieldtype="Data",
|
|
insert_after="last_name",
|
|
options="Email"
|
|
),
|
|
dict(
|
|
fieldname="customer_type",
|
|
label="Customer Type",
|
|
fieldtype="Select",
|
|
options="Customer\nLead",
|
|
insert_after="email"
|
|
),
|
|
dict(
|
|
fieldname="customer_name",
|
|
label="Customer Name",
|
|
fieldtype="Dynamic Link",
|
|
options="customer_type",
|
|
insert_after="customer_type"
|
|
),
|
|
dict(
|
|
fieldname="addresses",
|
|
label="Addresses",
|
|
fieldtype="Table",
|
|
options="Contact Address Link",
|
|
insert_after="customer_name"
|
|
)
|
|
],
|
|
"On-Site Meeting": [
|
|
dict(
|
|
fieldname="notes",
|
|
label="Notes",
|
|
fieldtype="Small Text",
|
|
insert_after="address"
|
|
),
|
|
dict(
|
|
fieldname="assigned_employee",
|
|
label="Assigned Employee",
|
|
fieldtype="Link",
|
|
options="Employee",
|
|
insert_after="notes"
|
|
),
|
|
dict(
|
|
fieldname="status",
|
|
label="Status",
|
|
fieldtype="Select",
|
|
options="Unscheduled\nScheduled\nCompleted\nCancelled",
|
|
default="Unscheduled",
|
|
insert_after="start_time"
|
|
),
|
|
dict(
|
|
fieldname="completed_by",
|
|
label="Completed By",
|
|
fieldtype="Link",
|
|
options="Employee",
|
|
insert_after="status"
|
|
),
|
|
dict(
|
|
fieldname="company",
|
|
label="Company",
|
|
fieldtype="Link",
|
|
options="Company",
|
|
insert_after="assigned_employee"
|
|
),
|
|
dict(
|
|
fieldname="party_type",
|
|
label="Party Type",
|
|
fieldtype="Select",
|
|
options="Customer\nLead",
|
|
insert_after="company"
|
|
),
|
|
dict(
|
|
fieldname="party_name",
|
|
label="Party Name",
|
|
fieldtype="Dynamic Link",
|
|
options="party_type",
|
|
insert_after="party_type"
|
|
),
|
|
dict(
|
|
fieldname="contact",
|
|
label="Contact",
|
|
fieldtype="Link",
|
|
options="Contact",
|
|
insert_after="party_name"
|
|
),
|
|
dict(
|
|
fieldname="project_template",
|
|
label="Project Template",
|
|
fieldtype="Link",
|
|
options="Project Template",
|
|
insert_after="company"
|
|
)
|
|
],
|
|
"Quotation": [
|
|
dict(
|
|
fieldname="requires_half_payment",
|
|
label="Requires Half Payment",
|
|
fieldtype="Check",
|
|
default=0,
|
|
insert_after="custom_installation_address"
|
|
),
|
|
dict(
|
|
fieldname="custom_quotation_template",
|
|
label="Quotation Template",
|
|
fieldtype="Link",
|
|
options="Quotation Template",
|
|
insert_after="company",
|
|
description="The template used for generating this quotation."
|
|
),
|
|
dict(
|
|
fieldname="custom_project_template",
|
|
label="Project Template",
|
|
fieldtype="Link",
|
|
options="Project Template",
|
|
insert_after="custom_quotation_template",
|
|
description="The project template to use when creating a project from this quotation.",
|
|
allow_on_submit=1
|
|
),
|
|
dict(
|
|
fieldname="custom_job_address",
|
|
label="Job Address",
|
|
fieldtype="Link",
|
|
options="Address",
|
|
insert_after="custom_installation_address",
|
|
description="The address where the job will be performed.",
|
|
allow_on_submit=1
|
|
),
|
|
dict(
|
|
fieldname="from_onsite_meeting",
|
|
label="From On-Site Meeting",
|
|
fieldtype="Link",
|
|
options="On-Site Meeting",
|
|
insert_after="custom_job_address"
|
|
),
|
|
dict(
|
|
fieldname="from_onsite_meeting",
|
|
label="From On-Site Meeting",
|
|
fieldtype="Link",
|
|
options="On-Site Meeting",
|
|
insert_after="custom_job_address"
|
|
),
|
|
dict(
|
|
fieldname="actual_customer_name",
|
|
label="Customer",
|
|
fieldtype="Dynamic Link",
|
|
options="customer_type",
|
|
insert_after="from_onsite_meeting",
|
|
allow_on_submit=1
|
|
),
|
|
dict(
|
|
fieldname="customer_type",
|
|
label="Customer Type",
|
|
fieldtype="Select",
|
|
options="Customer\nLead",
|
|
insert_after="customer_name",
|
|
allow_on_submit=1
|
|
)
|
|
],
|
|
"Sales Order": [
|
|
dict(
|
|
fieldname="requires_half_payment",
|
|
label="Requires Half Payment",
|
|
fieldtype="Check",
|
|
default=0,
|
|
insert_after="custom_installation_address"
|
|
),
|
|
dict(
|
|
fieldname="custom_project_template",
|
|
label="Project Template",
|
|
fieldtype="Link",
|
|
options="Project Template",
|
|
description="The project template to use when creating a project from this sales order.",
|
|
insert_after="custom_installation_address",
|
|
allow_on_submit=1
|
|
),
|
|
dict(
|
|
fieldname="custom_job_address",
|
|
label="Job Address",
|
|
fieldtype="Link",
|
|
options="Address",
|
|
insert_after="custom_installation_address",
|
|
description="The address where the job will be performed.",
|
|
allow_on_submit=1
|
|
)
|
|
],
|
|
"Project": [
|
|
dict(
|
|
fieldname="job_address",
|
|
label="Job Address",
|
|
fieldtype="Link",
|
|
options="Address",
|
|
insert_after="project_name",
|
|
description="The address where the job is being performed."
|
|
),
|
|
dict(
|
|
fieldname="customer",
|
|
label="Customer",
|
|
fieldtype="Link",
|
|
options="Customer",
|
|
insert_after="job_address",
|
|
description="The customer for whom the project is being executed."
|
|
)
|
|
],
|
|
"Project Template": [
|
|
dict(
|
|
fieldname="company",
|
|
label="Company",
|
|
fieldtype="Link",
|
|
options="Company",
|
|
insert_after="project_type",
|
|
description="The company associated with this project template."
|
|
)
|
|
]
|
|
}
|
|
|
|
print("🔧 Custom fields to check per doctype:")
|
|
for key, value in custom_fields.items():
|
|
print(f" • {key}: {len(value)} fields")
|
|
print(f" Total fields to check: {sum(len(v) for v in custom_fields.values())}\n")
|
|
|
|
missing_fields = []
|
|
fields_to_update = []
|
|
|
|
for doctype, field_options in custom_fields.items():
|
|
meta = frappe.get_meta(doctype)
|
|
for field_spec in field_options:
|
|
fieldname = field_spec["fieldname"]
|
|
if not meta.has_field(fieldname):
|
|
missing_fields.append(f"{doctype}: {fieldname}")
|
|
else:
|
|
# Field exists, check if specs match
|
|
custom_field_name = f"{doctype}-{fieldname}"
|
|
if frappe.db.exists("Custom Field", custom_field_name):
|
|
custom_field_doc = frappe.get_doc("Custom Field", custom_field_name)
|
|
needs_update = False
|
|
|
|
# Compare important properties
|
|
for key, desired_value in field_spec.items():
|
|
if key == "fieldname":
|
|
continue
|
|
current_value = getattr(custom_field_doc, key, None)
|
|
if current_value != desired_value:
|
|
needs_update = True
|
|
break
|
|
|
|
if needs_update:
|
|
fields_to_update.append((doctype, fieldname, field_spec))
|
|
|
|
if missing_fields:
|
|
print("\n❌ Missing custom fields:")
|
|
for entry in missing_fields:
|
|
print(f" • {entry}")
|
|
print("\n🔧 Creating missing custom fields...")
|
|
missing_field_specs = build_missing_field_specs(custom_fields, missing_fields)
|
|
create_custom_fields(missing_field_specs)
|
|
print("✅ Missing custom fields created.")
|
|
|
|
if fields_to_update:
|
|
print("\n🔧 Updating custom fields with mismatched specs:")
|
|
for doctype, fieldname, field_spec in fields_to_update:
|
|
print(f" • {doctype}: {fieldname}")
|
|
custom_field_name = f"{doctype}-{fieldname}"
|
|
custom_field_doc = frappe.get_doc("Custom Field", custom_field_name)
|
|
|
|
# Update all properties from field_spec
|
|
for key, value in field_spec.items():
|
|
if key != "fieldname":
|
|
setattr(custom_field_doc, key, value)
|
|
|
|
custom_field_doc.save(ignore_permissions=True)
|
|
|
|
frappe.db.commit()
|
|
print("✅ Custom fields updated.")
|
|
|
|
if not missing_fields and not fields_to_update:
|
|
print("✅ All custom fields verified.")
|
|
|
|
|
|
def update_onsite_meeting_fields():
|
|
"""Update On-Site Meeting doctype fields to make start_time and end_time optional."""
|
|
print("\n🔧 Updating On-Site Meeting doctype fields...")
|
|
|
|
try:
|
|
# Get the doctype
|
|
doctype = frappe.get_doc("DocType", "On-Site Meeting")
|
|
|
|
# Find and update start_time and end_time fields
|
|
updated_fields = []
|
|
for field in doctype.fields:
|
|
if field.fieldname in ['start_time', 'end_time']:
|
|
if field.reqd == 1:
|
|
field.reqd = 0
|
|
updated_fields.append(field.fieldname)
|
|
|
|
if updated_fields:
|
|
# Save the doctype
|
|
doctype.save(ignore_permissions=True)
|
|
print(f"✅ Updated fields: {', '.join(updated_fields)} (set to not required)")
|
|
else:
|
|
print("✅ Fields already configured correctly")
|
|
|
|
print("🔧 On-Site Meeting field update complete.\n")
|
|
except Exception as e:
|
|
print(f"❌ Error updating On-Site Meeting fields: {str(e)}")
|
|
frappe.log_error(message=str(e), title="On-Site Meeting Field Update Failed")
|
|
# Don't raise - this is not critical enough to stop migration
|
|
|
|
def update_address_fields():
|
|
quotations = frappe.get_all("Quotation", pluck="name")
|
|
addresses = frappe.get_all("Address", pluck="name")
|
|
sales_orders = frappe.get_all("Sales Order", pluck="name")
|
|
tasks = frappe.get_all("Task", pluck="name")
|
|
total_addresses = len(addresses)
|
|
total_quotations = len(quotations)
|
|
total_sales_orders = len(sales_orders)
|
|
total_tasks = len(tasks)
|
|
total_doctypes = total_addresses + total_quotations + total_sales_orders + total_tasks
|
|
combined_doctypes = []
|
|
for sales_order in sales_orders:
|
|
combined_doctypes.append({"doctype": "Sales Order", "name": sales_order})
|
|
for quotation in quotations:
|
|
combined_doctypes.append({"doctype": "Quotation", "name": quotation})
|
|
for address in addresses:
|
|
combined_doctypes.append({"doctype": "Address", "name": address})
|
|
for task in tasks:
|
|
combined_doctypes.append({"doctype": "Task", "name": task})
|
|
|
|
print(f"\n📍 Updating field values for {total_addresses} addresses, {total_quotations} quotations, {total_sales_orders} sales orders, and {total_tasks} tasks...")
|
|
|
|
# Field update counters
|
|
field_counters = {
|
|
'quotation_addresses_updated': 0,
|
|
'quotation_project_templates_updated': 0,
|
|
'sales_order_addresses_updated': 0,
|
|
'sales_order_project_templates_updated': 0,
|
|
'full_address': 0,
|
|
'custom_onsite_meeting_scheduled': 0,
|
|
'custom_estimate_sent_status': 0,
|
|
'custom_job_status': 0,
|
|
'custom_payment_received_status': 0,
|
|
'address_linked_to_customer': 0,
|
|
'total_field_updates': 0,
|
|
'addresses_updated': 0,
|
|
'quotations_updated': 0,
|
|
'sales_orders_updated': 0,
|
|
'customers_updated': 0,
|
|
'contacts_updated': 0,
|
|
'tasks_updated': 0
|
|
}
|
|
|
|
onsite_meta = frappe.get_meta("On-Site Meeting")
|
|
onsite_status_field = "custom_status" if onsite_meta.has_field("custom_status") else "status"
|
|
|
|
for index, doc in enumerate(combined_doctypes, 1):
|
|
# Calculate progress
|
|
progress_percentage = int((index / total_doctypes) * 100)
|
|
bar_length = 30
|
|
filled_length = int(bar_length * index // total_doctypes)
|
|
bar = '█' * filled_length + '░' * (bar_length - filled_length)
|
|
|
|
# Print a three-line, refreshing progress block without adding new lines each loop
|
|
progress_line = f"📊 Progress: [{bar}] {progress_percentage:3d}% ({index}/{total_doctypes})"
|
|
counters_line = f" Fields updated: {field_counters['total_field_updates']} | DocTypes updated: {field_counters['addresses_updated'] + field_counters['quotations_updated'] + field_counters['sales_orders_updated'] + field_counters['customers_updated']}"
|
|
detail_line = f" Processing: {doc['name'][:40]}..."
|
|
|
|
if index == 1:
|
|
# First render: write the three lines
|
|
sys.stdout.write(
|
|
f"\r\033[K{progress_line}\n"
|
|
f"\033[K{counters_line}\n"
|
|
f"\033[K{detail_line}"
|
|
)
|
|
else:
|
|
# Move cursor up 3 lines, then rewrite each line in place
|
|
sys.stdout.write("\033[2F")
|
|
sys.stdout.write(f"\r\033[K{progress_line}\n")
|
|
sys.stdout.write(f"\033[K{counters_line}\n")
|
|
sys.stdout.write(f"\033[K{detail_line}")
|
|
|
|
if index == total_doctypes:
|
|
sys.stdout.write("\n")
|
|
|
|
sys.stdout.flush()
|
|
|
|
if doc['doctype'] == "Quotation" or doc['doctype'] == "Sales Order":
|
|
dict_field = doc['doctype'].lower().replace(" ", "_")
|
|
quotation_doc = frappe.get_doc(doc['doctype'], doc['name'])
|
|
custom_installation_address = getattr(quotation_doc, 'custom_installation_address', None)
|
|
custom_job_address = getattr(quotation_doc, 'custom_job_address', None)
|
|
custom_project_template = getattr(quotation_doc, 'custom_project_template', None)
|
|
|
|
updates = {}
|
|
if custom_installation_address and not custom_job_address:
|
|
updates['custom_job_address'] = custom_installation_address
|
|
field_counters[f"{dict_field}_addresses_updated"] += 1
|
|
field_counters['total_field_updates'] += 1
|
|
if custom_installation_address and not custom_project_template:
|
|
updates['custom_project_template'] = "SNW Install"
|
|
field_counters[f"{dict_field}_project_templates_updated"] += 1
|
|
field_counters['total_field_updates'] += 1
|
|
|
|
if updates:
|
|
frappe.db.set_value(doc['doctype'], doc['name'], updates)
|
|
field_counters[f"{dict_field}s_updated"] += 1
|
|
|
|
if doc['doctype'] == "Address":
|
|
address_doc = frappe.get_doc("Address", doc['name'])
|
|
updates = {}
|
|
|
|
# Use getattr with default values instead of direct attribute access
|
|
if not getattr(address_doc, 'full_address', None):
|
|
address_parts_1 = [
|
|
address_doc.address_line1 or "",
|
|
address_doc.address_line2 or "",
|
|
address_doc.city or "",
|
|
]
|
|
address_parts_2 = [
|
|
address_doc.state or "",
|
|
address_doc.pincode or "",
|
|
]
|
|
|
|
full_address = ", ".join([
|
|
" ".join(filter(None, address_parts_1)),
|
|
" ".join(filter(None, address_parts_2))
|
|
]).strip()
|
|
updates['full_address'] = full_address
|
|
field_counters['full_address'] += 1
|
|
field_counters['total_field_updates'] += 1
|
|
|
|
onsite_meeting = "Not Started"
|
|
estimate_sent = "Not Started"
|
|
job_status = "Not Started"
|
|
payment_received = "Not Started"
|
|
|
|
|
|
onsite_meetings = frappe.get_all("On-Site Meeting", fields=[onsite_status_field], filters={"address": address_doc.address_title})
|
|
if onsite_meetings and onsite_meetings[0]:
|
|
status_value = onsite_meetings[0].get(onsite_status_field)
|
|
onsite_meeting = "Completed" if status_value == "Completed" else "In Progress"
|
|
|
|
estimates = frappe.get_all("Quotation", fields=["custom_sent", "docstatus", "custom_response"], filters={"custom_job_address": address_doc.address_title})
|
|
if estimates and estimates[0] and estimates[0]["custom_sent"] == 1 and estimates[0]["custom_response"]:
|
|
estimate_sent = "Completed"
|
|
elif estimates and estimates[0] and not (estimates[0]["custom_sent"] == 1 and estimates[0]["custom_response"]):
|
|
estimate_sent = "In Progress"
|
|
|
|
jobs = frappe.get_all("Project", fields=["status"], filters={"custom_installation_address": address_doc.address_title, "project_template": "SNW Install"})
|
|
if jobs and jobs[0] and jobs[0]["status"] == "Completed":
|
|
job_status = "Completed"
|
|
elif jobs and jobs[0]:
|
|
job_status = "In Progress"
|
|
|
|
sales_invoices = frappe.get_all("Sales Invoice", fields=["outstanding_amount"], filters={"custom_installation_address": address_doc.address_title})
|
|
# payments = frappe.get_all("Payment Entry", filters={"custom_installation_address": address_doc.address_title})
|
|
if sales_invoices and sales_invoices[0] and sales_invoices[0]["outstanding_amount"] == 0:
|
|
payment_received = "Completed"
|
|
elif sales_invoices and sales_invoices[0]:
|
|
payment_received = "In Progress"
|
|
|
|
customer_name = getattr(address_doc, 'custom_customer_to_bill', None)
|
|
links = address_doc.get("links", [])
|
|
customer_links = [link for link in links if link.link_doctype == "Customer"]
|
|
needs_link_update = False
|
|
|
|
if customer_name and frappe.db.exists("Customer", customer_name):
|
|
customer_doc = frappe.get_doc("Customer", customer_name)
|
|
|
|
# Check if address needs link update
|
|
if not customer_links:
|
|
needs_link_update = True
|
|
|
|
if not address_doc.name in [link.address_name for link in customer_doc.get("custom_select_address", [])]:
|
|
customer_doc.append("custom_select_address", {
|
|
"address_name": address_doc.name
|
|
})
|
|
customer_doc.save(ignore_permissions=True)
|
|
field_counters['customers_updated'] += 1
|
|
field_counters['total_field_updates'] += 1
|
|
|
|
if getattr(address_doc, 'custom_onsite_meeting_scheduled', None) != onsite_meeting:
|
|
updates['custom_onsite_meeting_scheduled'] = onsite_meeting
|
|
field_counters['custom_onsite_meeting_scheduled'] += 1
|
|
field_counters['total_field_updates'] += 1
|
|
if getattr(address_doc, 'custom_estimate_sent_status', None) != estimate_sent:
|
|
updates['custom_estimate_sent_status'] = estimate_sent
|
|
field_counters['custom_estimate_sent_status'] += 1
|
|
field_counters['total_field_updates'] += 1
|
|
if getattr(address_doc, 'custom_job_status', None) != job_status:
|
|
updates['custom_job_status'] = job_status
|
|
field_counters['custom_job_status'] += 1
|
|
field_counters['total_field_updates'] += 1
|
|
if getattr(address_doc, 'custom_payment_received_status', None) != payment_received:
|
|
updates['custom_payment_received_status'] = payment_received
|
|
field_counters['custom_payment_received_status'] += 1
|
|
field_counters['total_field_updates'] += 1
|
|
|
|
if updates:
|
|
frappe.db.set_value("Address", doc['name'], updates)
|
|
field_counters['addresses_updated'] += 1
|
|
|
|
# Handle address links after db.set_value to avoid timestamp mismatch
|
|
if needs_link_update:
|
|
# Reload the document to get the latest version
|
|
address_doc = frappe.get_doc("Address", doc['name'])
|
|
address_doc.append("links", {
|
|
"link_doctype": "Customer",
|
|
"link_name": customer_name
|
|
})
|
|
address_doc.save(ignore_permissions=True)
|
|
field_counters['address_linked_to_customer'] += 1
|
|
field_counters['total_field_updates'] += 1
|
|
if doc['doctype'] == "Task":
|
|
property = frappe.get_value("Task", doc["name"], "custom_property")
|
|
project = frappe.get_value("Task", doc["name"], "project")
|
|
project_address = frappe.get_value("Project", project, "custom_installation_address")
|
|
alt_project_address = frappe.get_value("Project", project, "custom_address")
|
|
if (project_address or alt_project_address) and not property:
|
|
frappe.db.set_value("Task", doc["name"], "custom_property", project_address if project_address else alt_project_address)
|
|
field_counters['tasks_updated'] += 1
|
|
field_counters['total_field_updates'] += 1
|
|
|
|
|
|
|
|
# Commit every 100 records to avoid long transactions
|
|
if index % 100 == 0:
|
|
frappe.db.commit()
|
|
|
|
# Print completion summary
|
|
print(f"\n\n✅ DocType field value update completed!")
|
|
print(f"📊 Summary:")
|
|
print(f" • Total DocTypes processed: {total_doctypes:,}")
|
|
print(f" • Addresses updated: {field_counters['addresses_updated']:,}")
|
|
print(f" • Quotations updated: {field_counters['quotations_updated']:,}")
|
|
print(f" • Sales Orders updated: {field_counters['sales_orders_updated']:,}")
|
|
print(f" • Customers updated: {field_counters['customers_updated']:,}")
|
|
print(f" • Total field updates: {field_counters['total_field_updates']:,}")
|
|
print(f" • Tasks Updated: {field_counters['tasks_updated']:,}")
|
|
print(f"\n📝 Field-specific updates:")
|
|
print(f" • Full Address: {field_counters['full_address']:,}")
|
|
print(f" • On-Site Meeting Status: {field_counters['custom_onsite_meeting_scheduled']:,}")
|
|
print(f" • Estimate Sent Status: {field_counters['custom_estimate_sent_status']:,}")
|
|
print(f" • Job Status: {field_counters['custom_job_status']:,}")
|
|
print(f" • Payment Received Status: {field_counters['custom_payment_received_status']:,}")
|
|
print(f" • Quotation Addresses Updated: {field_counters['quotation_addresses_updated']:,}")
|
|
print(f" • Quotation Project Templates Updated: {field_counters['quotation_project_templates_updated']:,}")
|
|
print(f" • Sales Order Addresses Updated: {field_counters['sales_order_addresses_updated']:,}")
|
|
print(f" • Sales Order Project Templates Updated: {field_counters['sales_order_project_templates_updated']:,}")
|
|
print("📍 DocType field value updates complete.\n")
|
|
|
|
def build_missing_field_specs(custom_fields, missing_fields):
|
|
missing_field_specs = {}
|
|
for entry in missing_fields:
|
|
doctype, fieldname = entry.split(": ")
|
|
missing_field_specs.setdefault(doctype, [])
|
|
for field_spec in custom_fields.get(doctype, []):
|
|
if field_spec["fieldname"] == fieldname:
|
|
missing_field_specs[doctype].append(field_spec)
|
|
break
|
|
|
|
return missing_field_specs |