302 lines
12 KiB
Python
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] |