custom_ui/custom_ui/commands.py
2026-02-19 17:18:39 -06:00

302 lines
12 KiB
Python

import click
import os
import subprocess
import frappe
import json
from custom_ui.utils import create_module
from custom_ui.api.db.general import search_any_field
from custom_ui.install import create_companies, create_project_templates, create_task_types, create_tasks, create_bid_meeting_note_form_templates
@click.command("update-data")
@click.option("--site", default=None, help="Site to update data for")
def update_data(site):
address_names = frappe.get_all("Address", pluck="name")
total_addresses = len(address_names)
updated_addresses = 0
updated_contacts = 0
updated_customers = 0
total_updated_fields = 0
skipped = 0
for address_name in address_names:
should_update = False
address = frappe.get_doc("Address", address_name)
customer_name = address.custom_customer_to_bill
customer_links = [link for link in address.get("links", []) if link.link_doctype == "Customer"]
# lead_links = [link for link in address.get("links", []) if link.link_doctype == "Lead"]
contact_links = [link for link in address.get("links", []) if link.link_doctype == "Contact"] + address.get("custom_linked_contacts", [])
if frappe.db.exists("Customer", customer_name):
customer = frappe.get_doc("Customer", customer_name)
else:
lead_names = frappe.get_all("Lead", filters={"lead_name": customer_name}, pluck="name")
customer_name = lead_names[0] if lead_names else None
customer = frappe.get_doc("Lead", customer_name) if customer_name else None
if not customer_links and customer and customer.doctype == "Customer":
address.append("links", {
"link_doctype": customer.doctype,
"link_name": customer.name
})
updated_addresses += 1
should_update = True
elif not lead_links and customer and customer.doctype == "Lead":
address.append("links", {
"link_doctype": customer.doctype,
"link_name": customer.name
})
updated_addresses += 1
should_update = True
@click.command("build-frontend")
@click.option("--site", default=None, help="Site to build frontend for")
def build_frontend(site):
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 frontend_path:
click.echo("\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)
click.echo("\n✅ Frontend build completed successfully.\n")
except subprocess.CalledProcessError as e:
click.echo(f"\n❌ Frontend build failed: {e}\n")
exit(1)
else:
frappe.log_error(message="No frontend directory found for custom_ui", title="Frontend Build Skipped")
click.echo(f"\n⚠️ Frontend directory does not exist. Skipping build. Path was {frontend_path}\n")
if not site:
return
try:
print(f"\n🧹 Clearing cache for site {site}...\n")
subprocess.check_call(["bench", "--site", site, "clear-cache"])
subprocess.check_call(["bench", "--site", site, "clear-website-cache"])
except subprocess.CalledProcessError as e:
frappe.log_error(message=str(e), title="Clear Cache Failed")
print(f"\n❌ Clearing cache failed: {e}\n")
# Restart bench
try:
print("\n🔄 Restarting bench...\n")
subprocess.check_call(["bench", "restart"])
except subprocess.CalledProcessError as e:
frappe.log_error(message=str(e), title="Bench Restart Failed")
print(f"\n❌ Bench restart failed: {e}\n")
@click.command("create-module")
def create_module_command():
create_module()
click.echo("✅ Custom UI module created or already exists.")
def setup_custom_ui():
pass
@click.command("import-aspire-migration")
@click.option("--site", required=True, help="Site to import data into")
@click.option("--path", required=True, help="Path to the migration output directory containing JSON files")
@click.option("--dry-run", is_flag=True, default=False, help="Print what would be done without inserting")
def import_aspire_migration(site, path, dry_run):
"""Import Aspire migration JSON files into ERPNext in dependency order."""
import time
frappe.init(site=site)
frappe.connect()
# Resolve path relative to the app if not absolute
if not os.path.isabs(path):
path = os.path.join(frappe.get_app_path("custom_ui"), os.path.basename(path))
customers_file = os.path.join(path, "customers.json")
contacts_file = os.path.join(path, "contacts.json")
addresses_file = os.path.join(path, "addresses.json")
updates_file = os.path.join(path, "customer_updates.json")
for f in [customers_file, contacts_file, addresses_file, updates_file]:
if not os.path.exists(f):
click.echo(f"❌ Missing file: {f}")
return
BATCH_SIZE = 1000
# Set flags to skip hooks, validations, and link checks for speed
frappe.flags.in_import = True
frappe.flags.mute_emails = True
frappe.flags.mute_notifications = True
def fast_insert(rec, label="record"):
"""Insert a doc skipping hooks, validations, and permissions."""
doc = frappe.get_doc(rec)
doc.flags.ignore_permissions = True
doc.flags.ignore_links = True
doc.flags.ignore_validate = True
doc.flags.ignore_mandatory = True
doc.db_insert()
return doc
# --- Step 1: Insert Customers ---
click.echo("📦 Step 1: Inserting Customers...")
t0 = time.time()
with open(customers_file) as f:
customers = json.load(f)
# Pre-fetch existing customers in one query for fast duplicate check
existing_customers = set(frappe.get_all("Customer", pluck="name"))
success, skipped, failed = 0, 0, 0
for i, rec in enumerate(customers):
if dry_run:
click.echo(f" [DRY RUN] Would insert Customer: {rec.get('customer_name')}")
continue
try:
if rec.get("customer_name") in existing_customers:
skipped += 1
continue
fast_insert(rec, "Customer")
existing_customers.add(rec.get("customer_name"))
success += 1
except Exception as e:
failed += 1
click.echo(f" ⚠️ Customer '{rec.get('customer_name')}': {e}")
if (i + 1) % BATCH_SIZE == 0:
frappe.db.commit()
click.echo(f" ... committed {i + 1}/{len(customers)}")
frappe.db.commit()
click.echo(f" ✅ Customers — inserted: {success}, skipped: {skipped}, failed: {failed} ({time.time() - t0:.1f}s)")
# --- Step 2: Insert Contacts ---
click.echo("📦 Step 2: Inserting Contacts...")
t0 = time.time()
with open(contacts_file) as f:
contacts = json.load(f)
success, skipped, failed = 0, 0, 0
for i, rec in enumerate(contacts):
if dry_run:
click.echo(f" [DRY RUN] Would insert Contact: {rec.get('first_name')} {rec.get('last_name')}")
continue
try:
fast_insert(rec, "Contact")
success += 1
except Exception as e:
failed += 1
name = f"{rec.get('first_name', '')} {rec.get('last_name', '')}"
click.echo(f" ⚠️ Contact '{name}': {e}")
if (i + 1) % BATCH_SIZE == 0:
frappe.db.commit()
click.echo(f" ... committed {i + 1}/{len(contacts)}")
frappe.db.commit()
click.echo(f" ✅ Contacts — inserted: {success}, skipped: {skipped}, failed: {failed} ({time.time() - t0:.1f}s)")
# --- Step 3: Insert Addresses ---
click.echo("📦 Step 3: Inserting Addresses...")
t0 = time.time()
with open(addresses_file) as f:
addresses = json.load(f)
success, skipped, failed = 0, 0, 0
for i, rec in enumerate(addresses):
if dry_run:
click.echo(f" [DRY RUN] Would insert Address: {rec.get('address_line1')}")
continue
try:
fast_insert(rec, "Address")
success += 1
except Exception as e:
failed += 1
click.echo(f" ⚠️ Address '{rec.get('address_line1', '?')}': {e}")
if (i + 1) % BATCH_SIZE == 0:
frappe.db.commit()
click.echo(f" ... committed {i + 1}/{len(addresses)}")
frappe.db.commit()
click.echo(f" ✅ Addresses — inserted: {success}, skipped: {skipped}, failed: {failed} ({time.time() - t0:.1f}s)")
# --- Step 4: Update Customers with child tables ---
click.echo("📦 Step 4: Updating Customers with contact/property links...")
t0 = time.time()
with open(updates_file) as f:
updates = json.load(f)
# Get child doctype names from Customer meta once
customer_meta = frappe.get_meta("Customer")
contacts_doctype = customer_meta.get_field("contacts").options if customer_meta.has_field("contacts") else None
properties_doctype = customer_meta.get_field("properties").options if customer_meta.has_field("properties") else None
if contacts_doctype:
click.echo(f" → contacts child doctype: {contacts_doctype}")
if properties_doctype:
click.echo(f" → properties child doctype: {properties_doctype}")
success, skipped, failed = 0, 0, 0
for i, rec in enumerate(updates):
customer_name = rec.get("customer_name")
if dry_run:
click.echo(f" [DRY RUN] Would update Customer: {customer_name}")
continue
try:
if customer_name not in existing_customers:
skipped += 1
continue
# Directly insert child rows without loading/saving parent doc
for contact_row in rec.get("contacts", []):
if not contacts_doctype:
break
contact_row.update({
"doctype": contacts_doctype,
"parent": customer_name,
"parenttype": "Customer",
"parentfield": "contacts",
})
fast_insert(contact_row, "contact link")
for property_row in rec.get("properties", []):
if not properties_doctype:
break
property_row.update({
"doctype": properties_doctype,
"parent": customer_name,
"parenttype": "Customer",
"parentfield": "properties",
})
fast_insert(property_row, "property link")
success += 1
except Exception as e:
failed += 1
click.echo(f" ⚠️ Update '{customer_name}': {e}")
if (i + 1) % BATCH_SIZE == 0:
frappe.db.commit()
click.echo(f" ... committed {i + 1}/{len(updates)}")
frappe.db.commit()
click.echo(f" ✅ Updates — applied: {success}, skipped: {skipped}, failed: {failed} ({time.time() - t0:.1f}s)")
click.echo("🎉 Migration complete!")
frappe.flags.in_import = False
frappe.flags.mute_emails = False
frappe.flags.mute_notifications = False
frappe.destroy()
commands = [build_frontend, create_module_command, import_aspire_migration]