Added Invoice Status Select to Project DocType.

This commit is contained in:
rocketdebris 2026-01-24 16:43:43 -05:00
parent 2fb82917b4
commit 91e4d47d48

View file

@ -31,7 +31,7 @@ def after_migrate():
for doctype in doctypes_to_refresh: for doctype in doctypes_to_refresh:
frappe.clear_cache(doctype=doctype) frappe.clear_cache(doctype=doctype)
frappe.reload_doctype(doctype) frappe.reload_doctype(doctype)
check_and_create_holiday_list() check_and_create_holiday_list()
# update_address_fields() # update_address_fields()
@ -56,7 +56,7 @@ def build_frontend():
frappe.log_error(message="No frontend directory found for custom_ui", title="Frontend Build Skipped") 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}") print(f"⚠️ Frontend directory does not exist. Skipping build. Path was {frontend_path}")
return return
dist_path = os.path.join(app_root, "custom_ui", "public", "dist") dist_path = os.path.join(app_root, "custom_ui", "public", "dist")
should_build = True should_build = True
@ -69,10 +69,10 @@ def build_frontend():
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
frappe.log_error(message=str(e), title="Frontend Build Failed") frappe.log_error(message=str(e), title="Frontend Build Failed")
print(f"\n❌ Frontend build failed: {e}\n") print(f"\n❌ Frontend build failed: {e}\n")
def add_custom_fields(): def add_custom_fields():
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
print("\n🔧 Adding custom fields to doctypes...") print("\n🔧 Adding custom fields to doctypes...")
try: try:
@ -85,7 +85,7 @@ def add_custom_fields():
print(" ✅ Added 'Service' to Address address_type options.") print(" ✅ Added 'Service' to Address address_type options.")
except Exception as e: except Exception as e:
print(f" ⚠️ Failed to update Address address_type: {e}") print(f" ⚠️ Failed to update Address address_type: {e}")
custom_fields = { custom_fields = {
"Customer": [ "Customer": [
dict( dict(
@ -267,7 +267,7 @@ def add_custom_fields():
insert_after="full_address" insert_after="full_address"
), ),
dict( dict(
fieldname="longitude", fieldname="longitude",
label="Longitude", label="Longitude",
fieldtype="Float", fieldtype="Float",
precision=8, precision=8,
@ -277,7 +277,7 @@ def add_custom_fields():
fieldname="onsite_meeting_scheduled", fieldname="onsite_meeting_scheduled",
label="On-Site Meeting Scheduled", label="On-Site Meeting Scheduled",
fieldtype="Select", fieldtype="Select",
options="Not Started\nIn Progress\nCompleted", options="Not Started\nIn Progress\nCompleted",
default="Not Started", default="Not Started",
insert_after="longitude" insert_after="longitude"
), ),
@ -291,7 +291,7 @@ def add_custom_fields():
), ),
dict( dict(
fieldname="job_status", fieldname="job_status",
label="Job Status", label="Job Status",
fieldtype="Select", fieldtype="Select",
options="Not Started\nIn Progress\nCompleted", options="Not Started\nIn Progress\nCompleted",
default="Not Started", default="Not Started",
@ -302,7 +302,7 @@ def add_custom_fields():
label="Payment Received Status", label="Payment Received Status",
fieldtype="Select", fieldtype="Select",
options="Not Started\nIn Progress\nCompleted", options="Not Started\nIn Progress\nCompleted",
default="Not Started", default="Not Started",
insert_after="job_status" insert_after="job_status"
), ),
dict( dict(
@ -588,7 +588,15 @@ def add_custom_fields():
fieldtype="Check", fieldtype="Check",
default=0, default=0,
insert_after="expected_end_time" insert_after="expected_end_time"
) ),
dict(
fieldname="invoice_status",
label="Invoice Status",
fieldtype="Select",
default="Not Ready",
options="Not Ready\nReady to Invoice\nInvoice Created\nInvoice Sent",
insert_after="is_scheduled"
),
], ],
"Project Template": [ "Project Template": [
dict( dict(
@ -616,15 +624,15 @@ def add_custom_fields():
) )
] ]
} }
print("🔧 Custom fields to check per doctype:") print("🔧 Custom fields to check per doctype:")
for key, value in custom_fields.items(): for key, value in custom_fields.items():
print(f"{key}: {len(value)} fields") print(f"{key}: {len(value)} fields")
print(f" Total fields to check: {sum(len(v) for v in custom_fields.values())}\n") print(f" Total fields to check: {sum(len(v) for v in custom_fields.values())}\n")
missing_fields = [] missing_fields = []
fields_to_update = [] fields_to_update = []
for doctype, field_options in custom_fields.items(): for doctype, field_options in custom_fields.items():
meta = frappe.get_meta(doctype) meta = frappe.get_meta(doctype)
for field_spec in field_options: for field_spec in field_options:
@ -637,7 +645,7 @@ def add_custom_fields():
if frappe.db.exists("Custom Field", custom_field_name): if frappe.db.exists("Custom Field", custom_field_name):
custom_field_doc = frappe.get_doc("Custom Field", custom_field_name) custom_field_doc = frappe.get_doc("Custom Field", custom_field_name)
needs_update = False needs_update = False
# Compare important properties # Compare important properties
for key, desired_value in field_spec.items(): for key, desired_value in field_spec.items():
if key == "fieldname": if key == "fieldname":
@ -646,10 +654,10 @@ def add_custom_fields():
if current_value != desired_value: if current_value != desired_value:
needs_update = True needs_update = True
break break
if needs_update: if needs_update:
fields_to_update.append((doctype, fieldname, field_spec)) fields_to_update.append((doctype, fieldname, field_spec))
if missing_fields: if missing_fields:
print("\n❌ Missing custom fields:") print("\n❌ Missing custom fields:")
for entry in missing_fields: for entry in missing_fields:
@ -658,36 +666,36 @@ def add_custom_fields():
missing_field_specs = build_missing_field_specs(custom_fields, missing_fields) missing_field_specs = build_missing_field_specs(custom_fields, missing_fields)
create_custom_fields(missing_field_specs) create_custom_fields(missing_field_specs)
print("✅ Missing custom fields created.") print("✅ Missing custom fields created.")
if fields_to_update: if fields_to_update:
print("\n🔧 Updating custom fields with mismatched specs:") print("\n🔧 Updating custom fields with mismatched specs:")
for doctype, fieldname, field_spec in fields_to_update: for doctype, fieldname, field_spec in fields_to_update:
print(f"{doctype}: {fieldname}") print(f"{doctype}: {fieldname}")
custom_field_name = f"{doctype}-{fieldname}" custom_field_name = f"{doctype}-{fieldname}"
custom_field_doc = frappe.get_doc("Custom Field", custom_field_name) custom_field_doc = frappe.get_doc("Custom Field", custom_field_name)
# Update all properties from field_spec # Update all properties from field_spec
for key, value in field_spec.items(): for key, value in field_spec.items():
if key != "fieldname": if key != "fieldname":
setattr(custom_field_doc, key, value) setattr(custom_field_doc, key, value)
custom_field_doc.save(ignore_permissions=True) custom_field_doc.save(ignore_permissions=True)
frappe.db.commit() frappe.db.commit()
print("✅ Custom fields updated.") print("✅ Custom fields updated.")
if not missing_fields and not fields_to_update: if not missing_fields and not fields_to_update:
print("✅ All custom fields verified.") print("✅ All custom fields verified.")
def update_onsite_meeting_fields(): def update_onsite_meeting_fields():
"""Update On-Site Meeting doctype fields to make start_time and end_time optional.""" """Update On-Site Meeting doctype fields to make start_time and end_time optional."""
print("\n🔧 Updating On-Site Meeting doctype fields...") print("\n🔧 Updating On-Site Meeting doctype fields...")
try: try:
# Get the doctype # Get the doctype
doctype = frappe.get_doc("DocType", "On-Site Meeting") doctype = frappe.get_doc("DocType", "On-Site Meeting")
# Find and update start_time and end_time fields # Find and update start_time and end_time fields
updated_fields = [] updated_fields = []
for field in doctype.fields: for field in doctype.fields:
@ -695,20 +703,20 @@ def update_onsite_meeting_fields():
if field.reqd == 1: if field.reqd == 1:
field.reqd = 0 field.reqd = 0
updated_fields.append(field.fieldname) updated_fields.append(field.fieldname)
if updated_fields: if updated_fields:
# Save the doctype # Save the doctype
doctype.save(ignore_permissions=True) doctype.save(ignore_permissions=True)
print(f"✅ Updated fields: {', '.join(updated_fields)} (set to not required)") print(f"✅ Updated fields: {', '.join(updated_fields)} (set to not required)")
else: else:
print("✅ Fields already configured correctly") print("✅ Fields already configured correctly")
print("🔧 On-Site Meeting field update complete.\n") print("🔧 On-Site Meeting field update complete.\n")
except Exception as e: except Exception as e:
print(f"❌ Error updating On-Site Meeting fields: {str(e)}") print(f"❌ Error updating On-Site Meeting fields: {str(e)}")
frappe.log_error(message=str(e), title="On-Site Meeting Field Update Failed") frappe.log_error(message=str(e), title="On-Site Meeting Field Update Failed")
# Don't raise - this is not critical enough to stop migration # Don't raise - this is not critical enough to stop migration
def update_address_fields(): def update_address_fields():
quotations = frappe.get_all("Quotation", pluck="name") quotations = frappe.get_all("Quotation", pluck="name")
addresses = frappe.get_all("Address", pluck="name") addresses = frappe.get_all("Address", pluck="name")
@ -728,9 +736,9 @@ def update_address_fields():
combined_doctypes.append({"doctype": "Address", "name": address}) combined_doctypes.append({"doctype": "Address", "name": address})
for task in tasks: for task in tasks:
combined_doctypes.append({"doctype": "Task", "name": task}) 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...") 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 update counters
field_counters = { field_counters = {
'quotation_addresses_updated': 0, 'quotation_addresses_updated': 0,
@ -751,7 +759,7 @@ def update_address_fields():
'contacts_updated': 0, 'contacts_updated': 0,
'tasks_updated': 0 'tasks_updated': 0
} }
onsite_meta = frappe.get_meta("On-Site Meeting") onsite_meta = frappe.get_meta("On-Site Meeting")
onsite_status_field = "custom_status" if onsite_meta.has_field("custom_status") else "status" onsite_status_field = "custom_status" if onsite_meta.has_field("custom_status") else "status"
@ -761,7 +769,7 @@ def update_address_fields():
bar_length = 30 bar_length = 30
filled_length = int(bar_length * index // total_doctypes) filled_length = int(bar_length * index // total_doctypes)
bar = '' * filled_length + '' * (bar_length - filled_length) bar = '' * filled_length + '' * (bar_length - filled_length)
# Print a three-line, refreshing progress block without adding new lines each loop # Print a three-line, refreshing progress block without adding new lines each loop
progress_line = f"📊 Progress: [{bar}] {progress_percentage:3d}% ({index}/{total_doctypes})" 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']}" 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']}"
@ -792,7 +800,7 @@ def update_address_fields():
custom_installation_address = getattr(quotation_doc, 'custom_installation_address', None) custom_installation_address = getattr(quotation_doc, 'custom_installation_address', None)
custom_job_address = getattr(quotation_doc, 'custom_job_address', None) custom_job_address = getattr(quotation_doc, 'custom_job_address', None)
custom_project_template = getattr(quotation_doc, 'custom_project_template', None) custom_project_template = getattr(quotation_doc, 'custom_project_template', None)
updates = {} updates = {}
if custom_installation_address and not custom_job_address: if custom_installation_address and not custom_job_address:
updates['custom_job_address'] = custom_installation_address updates['custom_job_address'] = custom_installation_address
@ -802,15 +810,15 @@ def update_address_fields():
updates['custom_project_template'] = "SNW Install" updates['custom_project_template'] = "SNW Install"
field_counters[f"{dict_field}_project_templates_updated"] += 1 field_counters[f"{dict_field}_project_templates_updated"] += 1
field_counters['total_field_updates'] += 1 field_counters['total_field_updates'] += 1
if updates: if updates:
frappe.db.set_value(doc['doctype'], doc['name'], updates) frappe.db.set_value(doc['doctype'], doc['name'], updates)
field_counters[f"{dict_field}s_updated"] += 1 field_counters[f"{dict_field}s_updated"] += 1
if doc['doctype'] == "Address": if doc['doctype'] == "Address":
address_doc = frappe.get_doc("Address", doc['name']) address_doc = frappe.get_doc("Address", doc['name'])
updates = {} updates = {}
# Use getattr with default values instead of direct attribute access # Use getattr with default values instead of direct attribute access
if not getattr(address_doc, 'full_address', None): if not getattr(address_doc, 'full_address', None):
address_parts_1 = [ address_parts_1 = [
@ -822,57 +830,57 @@ def update_address_fields():
address_doc.state or "", address_doc.state or "",
address_doc.pincode or "", address_doc.pincode or "",
] ]
full_address = ", ".join([ full_address = ", ".join([
" ".join(filter(None, address_parts_1)), " ".join(filter(None, address_parts_1)),
" ".join(filter(None, address_parts_2)) " ".join(filter(None, address_parts_2))
]).strip() ]).strip()
updates['full_address'] = full_address updates['full_address'] = full_address
field_counters['full_address'] += 1 field_counters['full_address'] += 1
field_counters['total_field_updates'] += 1 field_counters['total_field_updates'] += 1
onsite_meeting = "Not Started" onsite_meeting = "Not Started"
estimate_sent = "Not Started" estimate_sent = "Not Started"
job_status = "Not Started" job_status = "Not Started"
payment_received = "Not Started" payment_received = "Not Started"
onsite_meetings = frappe.get_all("On-Site Meeting", fields=[onsite_status_field], filters={"address": address_doc.address_title}) 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]: if onsite_meetings and onsite_meetings[0]:
status_value = onsite_meetings[0].get(onsite_status_field) status_value = onsite_meetings[0].get(onsite_status_field)
onsite_meeting = "Completed" if status_value == "Completed" else "In Progress" 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}) 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"]: if estimates and estimates[0] and estimates[0]["custom_sent"] == 1 and estimates[0]["custom_response"]:
estimate_sent = "Completed" estimate_sent = "Completed"
elif estimates and estimates[0] and not (estimates[0]["custom_sent"] == 1 and estimates[0]["custom_response"]): elif estimates and estimates[0] and not (estimates[0]["custom_sent"] == 1 and estimates[0]["custom_response"]):
estimate_sent = "In Progress" estimate_sent = "In Progress"
jobs = frappe.get_all("Project", fields=["status"], filters={"custom_installation_address": address_doc.address_title, "project_template": "SNW Install"}) 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": if jobs and jobs[0] and jobs[0]["status"] == "Completed":
job_status = "Completed" job_status = "Completed"
elif jobs and jobs[0]: elif jobs and jobs[0]:
job_status = "In Progress" job_status = "In Progress"
sales_invoices = frappe.get_all("Sales Invoice", fields=["outstanding_amount"], filters={"custom_installation_address": address_doc.address_title}) 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}) # 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: if sales_invoices and sales_invoices[0] and sales_invoices[0]["outstanding_amount"] == 0:
payment_received = "Completed" payment_received = "Completed"
elif sales_invoices and sales_invoices[0]: elif sales_invoices and sales_invoices[0]:
payment_received = "In Progress" payment_received = "In Progress"
customer_name = getattr(address_doc, 'custom_customer_to_bill', None) customer_name = getattr(address_doc, 'custom_customer_to_bill', None)
links = address_doc.get("links", []) links = address_doc.get("links", [])
customer_links = [link for link in links if link.link_doctype == "Customer"] customer_links = [link for link in links if link.link_doctype == "Customer"]
needs_link_update = False needs_link_update = False
if customer_name and frappe.db.exists("Customer", customer_name): if customer_name and frappe.db.exists("Customer", customer_name):
customer_doc = frappe.get_doc("Customer", customer_name) customer_doc = frappe.get_doc("Customer", customer_name)
# Check if address needs link update # Check if address needs link update
if not customer_links: if not customer_links:
needs_link_update = True needs_link_update = True
if not address_doc.name in [link.address_name for link in customer_doc.get("custom_select_address", [])]: if not address_doc.name in [link.address_name for link in customer_doc.get("custom_select_address", [])]:
customer_doc.append("custom_select_address", { customer_doc.append("custom_select_address", {
"address_name": address_doc.name "address_name": address_doc.name
@ -880,7 +888,7 @@ def update_address_fields():
customer_doc.save(ignore_permissions=True) customer_doc.save(ignore_permissions=True)
field_counters['customers_updated'] += 1 field_counters['customers_updated'] += 1
field_counters['total_field_updates'] += 1 field_counters['total_field_updates'] += 1
if getattr(address_doc, 'custom_onsite_meeting_scheduled', None) != onsite_meeting: if getattr(address_doc, 'custom_onsite_meeting_scheduled', None) != onsite_meeting:
updates['custom_onsite_meeting_scheduled'] = onsite_meeting updates['custom_onsite_meeting_scheduled'] = onsite_meeting
field_counters['custom_onsite_meeting_scheduled'] += 1 field_counters['custom_onsite_meeting_scheduled'] += 1
@ -897,11 +905,11 @@ def update_address_fields():
updates['custom_payment_received_status'] = payment_received updates['custom_payment_received_status'] = payment_received
field_counters['custom_payment_received_status'] += 1 field_counters['custom_payment_received_status'] += 1
field_counters['total_field_updates'] += 1 field_counters['total_field_updates'] += 1
if updates: if updates:
frappe.db.set_value("Address", doc['name'], updates) frappe.db.set_value("Address", doc['name'], updates)
field_counters['addresses_updated'] += 1 field_counters['addresses_updated'] += 1
# Handle address links after db.set_value to avoid timestamp mismatch # Handle address links after db.set_value to avoid timestamp mismatch
if needs_link_update: if needs_link_update:
# Reload the document to get the latest version # Reload the document to get the latest version
@ -922,13 +930,13 @@ def update_address_fields():
frappe.db.set_value("Task", doc["name"], "custom_property", project_address if project_address else alt_project_address) 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['tasks_updated'] += 1
field_counters['total_field_updates'] += 1 field_counters['total_field_updates'] += 1
# Commit every 100 records to avoid long transactions # Commit every 100 records to avoid long transactions
if index % 100 == 0: if index % 100 == 0:
frappe.db.commit() frappe.db.commit()
# Print completion summary # Print completion summary
print(f"\n\n✅ DocType field value update completed!") print(f"\n\n✅ DocType field value update completed!")
print(f"📊 Summary:") print(f"📊 Summary:")
@ -950,7 +958,7 @@ def update_address_fields():
print(f" • Sales Order Addresses Updated: {field_counters['sales_order_addresses_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(f" • Sales Order Project Templates Updated: {field_counters['sales_order_project_templates_updated']:,}")
print("📍 DocType field value updates complete.\n") print("📍 DocType field value updates complete.\n")
def build_missing_field_specs(custom_fields, missing_fields): def build_missing_field_specs(custom_fields, missing_fields):
missing_field_specs = {} missing_field_specs = {}
for entry in missing_fields: for entry in missing_fields:
@ -960,14 +968,14 @@ def build_missing_field_specs(custom_fields, missing_fields):
if field_spec["fieldname"] == fieldname: if field_spec["fieldname"] == fieldname:
missing_field_specs[doctype].append(field_spec) missing_field_specs[doctype].append(field_spec)
break break
return missing_field_specs return missing_field_specs
def check_and_create_holiday_list(year=2026, country="US", weekly_off="Sunday"): def check_and_create_holiday_list(year=2026, country="US", weekly_off="Sunday"):
"""Check if Holiday List for the given year exists, if not create it.""" """Check if Holiday List for the given year exists, if not create it."""
print(f"\n🔧 Checking for Holiday List for {country} in {year}...") print(f"\n🔧 Checking for Holiday List for {country} in {year}...")
holiday_list_name = f"{country} Holidays {year}" holiday_list_name = f"{country} Holidays {year}"
if frappe.db.exists("Holiday List", holiday_list_name): if frappe.db.exists("Holiday List", holiday_list_name):
print(f"✅ Holiday List '{holiday_list_name}' already exists.") print(f"✅ Holiday List '{holiday_list_name}' already exists.")
return return
@ -999,16 +1007,16 @@ def check_and_create_holiday_list(year=2026, country="US", weekly_off="Sunday"):
# hl.make_holiday_entries() # hl.make_holiday_entries()
frappe.db.commit() frappe.db.commit()
print(f"✅ Holiday List '{holiday_list_name}' created successfully.") print(f"✅ Holiday List '{holiday_list_name}' created successfully.")
def get_all_sundays(year): def get_all_sundays(year):
sundays = [] sundays = []
d = date(year, 1, 1) d = date(year, 1, 1)
while d.weekday() != 6: while d.weekday() != 6:
d += timedelta(days=1) d += timedelta(days=1)
while d.year == year: while d.year == year:
sundays.append(d) sundays.append(d)
d += timedelta(days=7) d += timedelta(days=7)
return sundays return sundays