update
This commit is contained in:
parent
84a91359d8
commit
6c703c2c3b
55 changed files with 1130 additions and 358 deletions
|
|
@ -127,83 +127,83 @@ def check_client_exists(client_name):
|
||||||
def get_client_status_counts(weekly=False, week_start_date=None, week_end_date=None):
|
def get_client_status_counts(weekly=False, week_start_date=None, week_end_date=None):
|
||||||
"""Get counts of clients by status categories with optional weekly filtering."""
|
"""Get counts of clients by status categories with optional weekly filtering."""
|
||||||
# Build base filters for date range if weekly filtering is enabled
|
# Build base filters for date range if weekly filtering is enabled
|
||||||
try:
|
# try:
|
||||||
base_filters = {}
|
# base_filters = {}
|
||||||
if weekly and week_start_date and week_end_date:
|
# if weekly and week_start_date and week_end_date:
|
||||||
# Assuming you have a date field to filter by - adjust the field name as needed
|
# # Assuming you have a date field to filter by - adjust the field name as needed
|
||||||
# Common options: creation, modified, custom_date_field, etc.
|
# # Common options: creation, modified, custom_date_field, etc.
|
||||||
base_filters["creation"] = ["between", [week_start_date, week_end_date]]
|
# base_filters["creation"] = ["between", [week_start_date, week_end_date]]
|
||||||
|
|
||||||
# Helper function to merge base filters with status filters
|
# # Helper function to merge base filters with status filters
|
||||||
def get_filters(status_field, status_value):
|
# def get_filters(status_field, status_value):
|
||||||
filters = {status_field: status_value}
|
# filters = {status_field: status_value}
|
||||||
filters.update(base_filters)
|
# filters.update(base_filters)
|
||||||
return filters
|
# return filters
|
||||||
|
|
||||||
onsite_meeting_scheduled_status_counts = {
|
# onsite_meeting_scheduled_status_counts = {
|
||||||
"label": "On-Site Meeting Scheduled",
|
# "label": "On-Site Meeting Scheduled",
|
||||||
"not_started": frappe.db.count("Address", filters=get_filters("custom_onsite_meeting_scheduled", "Not Started")),
|
# "not_started": frappe.db.count("Address", filters=get_filters("custom_onsite_meeting_scheduled", "Not Started")),
|
||||||
"in_progress": frappe.db.count("Address", filters=get_filters("custom_onsite_meeting_scheduled", "In Progress")),
|
# "in_progress": frappe.db.count("Address", filters=get_filters("custom_onsite_meeting_scheduled", "In Progress")),
|
||||||
"completed": frappe.db.count("Address", filters=get_filters("custom_onsite_meeting_scheduled", "Completed"))
|
# "completed": frappe.db.count("Address", filters=get_filters("custom_onsite_meeting_scheduled", "Completed"))
|
||||||
}
|
# }
|
||||||
|
|
||||||
estimate_sent_status_counts = {
|
# estimate_sent_status_counts = {
|
||||||
"label": "Estimate Sent",
|
# "label": "Estimate Sent",
|
||||||
"not_started": frappe.db.count("Address", filters=get_filters("custom_estimate_sent_status", "Not Started")),
|
# "not_started": frappe.db.count("Address", filters=get_filters("custom_estimate_sent_status", "Not Started")),
|
||||||
"in_progress": frappe.db.count("Address", filters=get_filters("custom_estimate_sent_status", "In Progress")),
|
# "in_progress": frappe.db.count("Address", filters=get_filters("custom_estimate_sent_status", "In Progress")),
|
||||||
"completed": frappe.db.count("Address", filters=get_filters("custom_estimate_sent_status", "Completed"))
|
# "completed": frappe.db.count("Address", filters=get_filters("custom_estimate_sent_status", "Completed"))
|
||||||
}
|
# }
|
||||||
|
|
||||||
job_status_counts = {
|
# job_status_counts = {
|
||||||
"label": "Job Status",
|
# "label": "Job Status",
|
||||||
"not_started": frappe.db.count("Address", filters=get_filters("custom_job_status", "Not Started")),
|
# "not_started": frappe.db.count("Address", filters=get_filters("custom_job_status", "Not Started")),
|
||||||
"in_progress": frappe.db.count("Address", filters=get_filters("custom_job_status", "In Progress")),
|
# "in_progress": frappe.db.count("Address", filters=get_filters("custom_job_status", "In Progress")),
|
||||||
"completed": frappe.db.count("Address", filters=get_filters("custom_job_status", "Completed"))
|
# "completed": frappe.db.count("Address", filters=get_filters("custom_job_status", "Completed"))
|
||||||
}
|
# }
|
||||||
|
|
||||||
payment_received_status_counts = {
|
# payment_received_status_counts = {
|
||||||
"label": "Payment Received",
|
# "label": "Payment Received",
|
||||||
"not_started": frappe.db.count("Address", filters=get_filters("custom_payment_received_status", "Not Started")),
|
# "not_started": frappe.db.count("Address", filters=get_filters("custom_payment_received_status", "Not Started")),
|
||||||
"in_progress": frappe.db.count("Address", filters=get_filters("custom_payment_received_status", "In Progress")),
|
# "in_progress": frappe.db.count("Address", filters=get_filters("custom_payment_received_status", "In Progress")),
|
||||||
"completed": frappe.db.count("Address", filters=get_filters("custom_payment_received_status", "Completed"))
|
# "completed": frappe.db.count("Address", filters=get_filters("custom_payment_received_status", "Completed"))
|
||||||
}
|
# }
|
||||||
|
|
||||||
status_dicts = [
|
# status_dicts = [
|
||||||
onsite_meeting_scheduled_status_counts,
|
# onsite_meeting_scheduled_status_counts,
|
||||||
estimate_sent_status_counts,
|
# estimate_sent_status_counts,
|
||||||
job_status_counts,
|
# job_status_counts,
|
||||||
payment_received_status_counts
|
# payment_received_status_counts
|
||||||
]
|
# ]
|
||||||
|
|
||||||
categories = []
|
# categories = []
|
||||||
for status_dict in status_dicts:
|
# for status_dict in status_dicts:
|
||||||
category = {
|
# category = {
|
||||||
"label": status_dict["label"],
|
# "label": status_dict["label"],
|
||||||
"statuses": [
|
# "statuses": [
|
||||||
{
|
# {
|
||||||
"color": "red",
|
# "color": "red",
|
||||||
"label": "Not Started",
|
# "label": "Not Started",
|
||||||
"count": status_dict["not_started"]
|
# "count": status_dict["not_started"]
|
||||||
},
|
# },
|
||||||
{
|
# {
|
||||||
"color": "yellow",
|
# "color": "yellow",
|
||||||
"label": "In Progress",
|
# "label": "In Progress",
|
||||||
"count": status_dict["in_progress"]
|
# "count": status_dict["in_progress"]
|
||||||
},
|
# },
|
||||||
{
|
# {
|
||||||
"color": "green",
|
# "color": "green",
|
||||||
"label": "Completed",
|
# "label": "Completed",
|
||||||
"count": status_dict["completed"]
|
# "count": status_dict["completed"]
|
||||||
}
|
# }
|
||||||
]
|
# ]
|
||||||
}
|
# }
|
||||||
categories.append(category)
|
# categories.append(category)
|
||||||
|
|
||||||
return build_success_response(categories)
|
return build_success_response("success")
|
||||||
except frappe.ValidationError as ve:
|
# except frappe.ValidationError as ve:
|
||||||
return build_error_response(str(ve), 400)
|
# return build_error_response(str(ve), 400)
|
||||||
except Exception as e:
|
# except Exception as e:
|
||||||
return build_error_response(str(e), 500)
|
# return build_error_response(str(e), 500)
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
|
|
@ -683,3 +683,141 @@ def find_primary_contact_or_throw(contacts):
|
||||||
print("#####DEBUG: Primary contact found:", contact)
|
print("#####DEBUG: Primary contact found:", contact)
|
||||||
return contact
|
return contact
|
||||||
raise ValueError("No primary contact found in contacts list.")
|
raise ValueError("No primary contact found in contacts list.")
|
||||||
|
|
||||||
|
|
||||||
|
def find_contact_in_list(contact_docs, contact_ref):
|
||||||
|
"""Find a contact document in a list by matching first_name, last_name, and email."""
|
||||||
|
if not isinstance(contact_ref, dict):
|
||||||
|
return None
|
||||||
|
ref_first = contact_ref.get("first_name", "")
|
||||||
|
ref_last = contact_ref.get("last_name", "")
|
||||||
|
ref_email = contact_ref.get("email", "")
|
||||||
|
for doc in contact_docs:
|
||||||
|
if (doc.first_name == ref_first and
|
||||||
|
doc.last_name == ref_last and
|
||||||
|
(doc.email_id == ref_email or doc.custom_email == ref_email)):
|
||||||
|
return doc
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def create_client_contacts_addresses(client_name, company, contacts=[], addresses=[]):
|
||||||
|
"""Create or link contacts and addresses for an existing client.
|
||||||
|
|
||||||
|
If a contact or address already exists, it will be linked to the client
|
||||||
|
instead of creating a duplicate.
|
||||||
|
"""
|
||||||
|
if isinstance(contacts, str):
|
||||||
|
contacts = json.loads(contacts)
|
||||||
|
if isinstance(addresses, str):
|
||||||
|
addresses = json.loads(addresses)
|
||||||
|
print(f"DEBUG: create_client_contacts_addresses called with client_name: {client_name}, company: {company}")
|
||||||
|
try:
|
||||||
|
client_doc = ClientService.get_client_or_throw(client_name)
|
||||||
|
|
||||||
|
# Build list of existing client contacts (preserves frontend index order)
|
||||||
|
existing_contact_docs = [frappe.get_doc("Contact", link.contact) for link in client_doc.contacts]
|
||||||
|
|
||||||
|
# Process new contacts
|
||||||
|
new_contact_docs = []
|
||||||
|
for contact in contacts:
|
||||||
|
contact_doc = check_and_get_contact(
|
||||||
|
contact.get("first_name"),
|
||||||
|
contact.get("last_name"),
|
||||||
|
contact.get("email"),
|
||||||
|
contact.get("phone_number")
|
||||||
|
)
|
||||||
|
if not contact_doc:
|
||||||
|
contact_doc = ContactService.create({
|
||||||
|
"first_name": contact.get("first_name"),
|
||||||
|
"last_name": contact.get("last_name"),
|
||||||
|
"role": contact.get("contact_role", "Other"),
|
||||||
|
"custom_email": contact.get("email"),
|
||||||
|
"is_primary_contact": 1 if contact.get("is_primary") else 0,
|
||||||
|
"customer_type": client_doc.doctype,
|
||||||
|
"customer_name": client_doc.name,
|
||||||
|
"email_ids": [{
|
||||||
|
"email_id": contact.get("email"),
|
||||||
|
"is_primary": 1
|
||||||
|
}],
|
||||||
|
"phone_nos": [{
|
||||||
|
"phone": contact.get("phone_number"),
|
||||||
|
"is_primary_phone": 1,
|
||||||
|
"is_primary_mobile_no": 1
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
ContactService.link_contact_to_customer(contact_doc, client_doc.doctype, client_doc.name)
|
||||||
|
ClientService.append_link_v2(client_doc.name, "contacts", {"contact": contact_doc.name})
|
||||||
|
new_contact_docs.append(contact_doc)
|
||||||
|
|
||||||
|
# Combined contact list: existing client contacts + newly created/linked contacts
|
||||||
|
# Address contact indices reference this combined list
|
||||||
|
all_contact_docs = existing_contact_docs + new_contact_docs
|
||||||
|
|
||||||
|
# Process addresses
|
||||||
|
address_docs = []
|
||||||
|
for address in addresses:
|
||||||
|
filters = {
|
||||||
|
"address_line1": address.get("address_line1"),
|
||||||
|
"city": address.get("city"),
|
||||||
|
"pincode": address.get("pincode")
|
||||||
|
}
|
||||||
|
if address.get("address_line2"):
|
||||||
|
filters["address_line2"] = address.get("address_line2")
|
||||||
|
|
||||||
|
existing_address = frappe.db.exists("Address", filters)
|
||||||
|
if existing_address:
|
||||||
|
address_doc = frappe.get_doc("Address", existing_address)
|
||||||
|
else:
|
||||||
|
address_doc = AddressService.create({
|
||||||
|
"address_title": AddressService.build_address_title(client_name, address),
|
||||||
|
"address_line1": address.get("address_line1"),
|
||||||
|
"address_line2": address.get("address_line2"),
|
||||||
|
"city": address.get("city"),
|
||||||
|
"state": address.get("state"),
|
||||||
|
"pincode": address.get("pincode"),
|
||||||
|
"country": "United States",
|
||||||
|
"address_type": "Service",
|
||||||
|
"custom_billing_address": 0,
|
||||||
|
"is_primary_address": 0,
|
||||||
|
"is_service_address": 1,
|
||||||
|
"customer_type": client_doc.doctype,
|
||||||
|
"customer_name": client_doc.name
|
||||||
|
})
|
||||||
|
|
||||||
|
# Add company if not already present
|
||||||
|
if company not in [c.company for c in address_doc.companies]:
|
||||||
|
address_doc.append("companies", {"company": company})
|
||||||
|
address_doc.save(ignore_permissions=True)
|
||||||
|
|
||||||
|
# Link address to customer
|
||||||
|
AddressService.link_address_to_customer(address_doc, client_doc.doctype, client_doc.name)
|
||||||
|
|
||||||
|
# Link selected contacts to address
|
||||||
|
for contact_ref in address.get("contacts", []):
|
||||||
|
if not contact_ref:
|
||||||
|
continue
|
||||||
|
# Contact references are dicts with first_name, last_name, email
|
||||||
|
contact_doc = find_contact_in_list(all_contact_docs, contact_ref)
|
||||||
|
if contact_doc:
|
||||||
|
AddressService.link_address_to_contact(address_doc, contact_doc.name)
|
||||||
|
ContactService.link_contact_to_address(contact_doc, address_doc.name)
|
||||||
|
|
||||||
|
# Set primary contact for address
|
||||||
|
primary_ref = address.get("primary_contact")
|
||||||
|
if primary_ref:
|
||||||
|
primary_contact = find_contact_in_list(all_contact_docs, primary_ref)
|
||||||
|
if primary_contact:
|
||||||
|
AddressService.set_primary_contact(address_doc.name, primary_contact.name)
|
||||||
|
|
||||||
|
# Link address to client
|
||||||
|
ClientService.append_link_v2(client_doc.name, "properties", {"address": address_doc.name})
|
||||||
|
address_docs.append(address_doc)
|
||||||
|
|
||||||
|
return build_success_response({
|
||||||
|
"contacts": [c.as_dict() for c in new_contact_docs],
|
||||||
|
"addresses": [a.as_dict() for a in address_docs],
|
||||||
|
"message": "Contacts and addresses created/linked successfully."
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
return build_error_response(str(e), 500)
|
||||||
|
|
@ -107,12 +107,19 @@ def setup_custom_ui():
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@click.command("import-aspire-migration")
|
@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("--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")
|
@click.option("--dry-run", is_flag=True, default=False, help="Print what would be done without inserting")
|
||||||
def import_aspire_migration(path, dry_run):
|
def import_aspire_migration(site, path, dry_run):
|
||||||
"""Import Aspire migration JSON files into ERPNext in dependency order."""
|
"""Import Aspire migration JSON files into ERPNext in dependency order."""
|
||||||
|
import time
|
||||||
|
frappe.init(site=site)
|
||||||
frappe.connect()
|
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")
|
customers_file = os.path.join(path, "customers.json")
|
||||||
contacts_file = os.path.join(path, "contacts.json")
|
contacts_file = os.path.join(path, "contacts.json")
|
||||||
addresses_file = os.path.join(path, "addresses.json")
|
addresses_file = os.path.join(path, "addresses.json")
|
||||||
|
|
@ -123,36 +130,58 @@ def import_aspire_migration(path, dry_run):
|
||||||
click.echo(f"❌ Missing file: {f}")
|
click.echo(f"❌ Missing file: {f}")
|
||||||
return
|
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 ---
|
# --- Step 1: Insert Customers ---
|
||||||
click.echo("📦 Step 1: Inserting Customers...")
|
click.echo("📦 Step 1: Inserting Customers...")
|
||||||
|
t0 = time.time()
|
||||||
with open(customers_file) as f:
|
with open(customers_file) as f:
|
||||||
customers = json.load(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
|
success, skipped, failed = 0, 0, 0
|
||||||
for i, rec in enumerate(customers):
|
for i, rec in enumerate(customers):
|
||||||
if dry_run:
|
if dry_run:
|
||||||
click.echo(f" [DRY RUN] Would insert Customer: {rec.get('customer_name')}")
|
click.echo(f" [DRY RUN] Would insert Customer: {rec.get('customer_name')}")
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
if frappe.db.exists("Customer", rec.get("customer_name")):
|
if rec.get("customer_name") in existing_customers:
|
||||||
skipped += 1
|
skipped += 1
|
||||||
continue
|
continue
|
||||||
doc = frappe.get_doc(rec)
|
fast_insert(rec, "Customer")
|
||||||
doc.insert(ignore_permissions=True)
|
existing_customers.add(rec.get("customer_name"))
|
||||||
success += 1
|
success += 1
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
failed += 1
|
failed += 1
|
||||||
click.echo(f" ⚠️ Customer '{rec.get('customer_name')}': {e}")
|
click.echo(f" ⚠️ Customer '{rec.get('customer_name')}': {e}")
|
||||||
|
|
||||||
if (i + 1) % 500 == 0:
|
if (i + 1) % BATCH_SIZE == 0:
|
||||||
frappe.db.commit()
|
frappe.db.commit()
|
||||||
click.echo(f" ... committed {i + 1}/{len(customers)}")
|
click.echo(f" ... committed {i + 1}/{len(customers)}")
|
||||||
|
|
||||||
frappe.db.commit()
|
frappe.db.commit()
|
||||||
click.echo(f" ✅ Customers — inserted: {success}, skipped: {skipped}, failed: {failed}")
|
click.echo(f" ✅ Customers — inserted: {success}, skipped: {skipped}, failed: {failed} ({time.time() - t0:.1f}s)")
|
||||||
|
|
||||||
# --- Step 2: Insert Contacts ---
|
# --- Step 2: Insert Contacts ---
|
||||||
click.echo("📦 Step 2: Inserting Contacts...")
|
click.echo("📦 Step 2: Inserting Contacts...")
|
||||||
|
t0 = time.time()
|
||||||
with open(contacts_file) as f:
|
with open(contacts_file) as f:
|
||||||
contacts = json.load(f)
|
contacts = json.load(f)
|
||||||
|
|
||||||
|
|
@ -162,23 +191,23 @@ def import_aspire_migration(path, dry_run):
|
||||||
click.echo(f" [DRY RUN] Would insert Contact: {rec.get('first_name')} {rec.get('last_name')}")
|
click.echo(f" [DRY RUN] Would insert Contact: {rec.get('first_name')} {rec.get('last_name')}")
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
doc = frappe.get_doc(rec)
|
fast_insert(rec, "Contact")
|
||||||
doc.insert(ignore_permissions=True)
|
|
||||||
success += 1
|
success += 1
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
failed += 1
|
failed += 1
|
||||||
name = f"{rec.get('first_name', '')} {rec.get('last_name', '')}"
|
name = f"{rec.get('first_name', '')} {rec.get('last_name', '')}"
|
||||||
click.echo(f" ⚠️ Contact '{name}': {e}")
|
click.echo(f" ⚠️ Contact '{name}': {e}")
|
||||||
|
|
||||||
if (i + 1) % 500 == 0:
|
if (i + 1) % BATCH_SIZE == 0:
|
||||||
frappe.db.commit()
|
frappe.db.commit()
|
||||||
click.echo(f" ... committed {i + 1}/{len(contacts)}")
|
click.echo(f" ... committed {i + 1}/{len(contacts)}")
|
||||||
|
|
||||||
frappe.db.commit()
|
frappe.db.commit()
|
||||||
click.echo(f" ✅ Contacts — inserted: {success}, skipped: {skipped}, failed: {failed}")
|
click.echo(f" ✅ Contacts — inserted: {success}, skipped: {skipped}, failed: {failed} ({time.time() - t0:.1f}s)")
|
||||||
|
|
||||||
# --- Step 3: Insert Addresses ---
|
# --- Step 3: Insert Addresses ---
|
||||||
click.echo("📦 Step 3: Inserting Addresses...")
|
click.echo("📦 Step 3: Inserting Addresses...")
|
||||||
|
t0 = time.time()
|
||||||
with open(addresses_file) as f:
|
with open(addresses_file) as f:
|
||||||
addresses = json.load(f)
|
addresses = json.load(f)
|
||||||
|
|
||||||
|
|
@ -188,25 +217,35 @@ def import_aspire_migration(path, dry_run):
|
||||||
click.echo(f" [DRY RUN] Would insert Address: {rec.get('address_line1')}")
|
click.echo(f" [DRY RUN] Would insert Address: {rec.get('address_line1')}")
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
doc = frappe.get_doc(rec)
|
fast_insert(rec, "Address")
|
||||||
doc.insert(ignore_permissions=True)
|
|
||||||
success += 1
|
success += 1
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
failed += 1
|
failed += 1
|
||||||
click.echo(f" ⚠️ Address '{rec.get('address_line1', '?')}': {e}")
|
click.echo(f" ⚠️ Address '{rec.get('address_line1', '?')}': {e}")
|
||||||
|
|
||||||
if (i + 1) % 500 == 0:
|
if (i + 1) % BATCH_SIZE == 0:
|
||||||
frappe.db.commit()
|
frappe.db.commit()
|
||||||
click.echo(f" ... committed {i + 1}/{len(addresses)}")
|
click.echo(f" ... committed {i + 1}/{len(addresses)}")
|
||||||
|
|
||||||
frappe.db.commit()
|
frappe.db.commit()
|
||||||
click.echo(f" ✅ Addresses — inserted: {success}, skipped: {skipped}, failed: {failed}")
|
click.echo(f" ✅ Addresses — inserted: {success}, skipped: {skipped}, failed: {failed} ({time.time() - t0:.1f}s)")
|
||||||
|
|
||||||
# --- Step 4: Update Customers with child tables ---
|
# --- Step 4: Update Customers with child tables ---
|
||||||
click.echo("📦 Step 4: Updating Customers with contact/property links...")
|
click.echo("📦 Step 4: Updating Customers with contact/property links...")
|
||||||
|
t0 = time.time()
|
||||||
with open(updates_file) as f:
|
with open(updates_file) as f:
|
||||||
updates = json.load(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
|
success, skipped, failed = 0, 0, 0
|
||||||
for i, rec in enumerate(updates):
|
for i, rec in enumerate(updates):
|
||||||
customer_name = rec.get("customer_name")
|
customer_name = rec.get("customer_name")
|
||||||
|
|
@ -214,32 +253,49 @@ def import_aspire_migration(path, dry_run):
|
||||||
click.echo(f" [DRY RUN] Would update Customer: {customer_name}")
|
click.echo(f" [DRY RUN] Would update Customer: {customer_name}")
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
if not frappe.db.exists("Customer", customer_name):
|
if customer_name not in existing_customers:
|
||||||
skipped += 1
|
skipped += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
doc = frappe.get_doc("Customer", customer_name)
|
# Directly insert child rows without loading/saving parent doc
|
||||||
|
|
||||||
for contact_row in rec.get("contacts", []):
|
for contact_row in rec.get("contacts", []):
|
||||||
doc.append("contacts", contact_row)
|
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", []):
|
for property_row in rec.get("properties", []):
|
||||||
doc.append("properties", property_row)
|
if not properties_doctype:
|
||||||
|
break
|
||||||
|
property_row.update({
|
||||||
|
"doctype": properties_doctype,
|
||||||
|
"parent": customer_name,
|
||||||
|
"parenttype": "Customer",
|
||||||
|
"parentfield": "properties",
|
||||||
|
})
|
||||||
|
fast_insert(property_row, "property link")
|
||||||
|
|
||||||
doc.save(ignore_permissions=True)
|
|
||||||
success += 1
|
success += 1
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
failed += 1
|
failed += 1
|
||||||
click.echo(f" ⚠️ Update '{customer_name}': {e}")
|
click.echo(f" ⚠️ Update '{customer_name}': {e}")
|
||||||
|
|
||||||
if (i + 1) % 500 == 0:
|
if (i + 1) % BATCH_SIZE == 0:
|
||||||
frappe.db.commit()
|
frappe.db.commit()
|
||||||
click.echo(f" ... committed {i + 1}/{len(updates)}")
|
click.echo(f" ... committed {i + 1}/{len(updates)}")
|
||||||
|
|
||||||
frappe.db.commit()
|
frappe.db.commit()
|
||||||
click.echo(f" ✅ Updates — applied: {success}, skipped: {skipped}, failed: {failed}")
|
click.echo(f" ✅ Updates — applied: {success}, skipped: {skipped}, failed: {failed} ({time.time() - t0:.1f}s)")
|
||||||
click.echo("🎉 Migration complete!")
|
click.echo("🎉 Migration complete!")
|
||||||
|
|
||||||
|
frappe.flags.in_import = False
|
||||||
|
frappe.flags.mute_emails = False
|
||||||
|
frappe.flags.mute_notifications = False
|
||||||
frappe.destroy()
|
frappe.destroy()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"allow_rename": 1,
|
||||||
|
"creation": "2026-02-18 05:53:09.512268",
|
||||||
|
"custom": 1,
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"contact"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "contact",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Contact",
|
||||||
|
"options": "Contact",
|
||||||
|
"reqd": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"grid_page_length": 50,
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"istable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2026-02-18 13:27:02.691142",
|
||||||
|
"modified_by": "casey@shilohcode.com",
|
||||||
|
"module": "Custom UI",
|
||||||
|
"name": "Address Contact Link",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [],
|
||||||
|
"row_format": "Dynamic",
|
||||||
|
"rows_threshold_for_grid_search": 20,
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"states": []
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"allow_rename": 1,
|
||||||
|
"creation": "2026-02-18 05:53:08.988990",
|
||||||
|
"custom": 1,
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"onsite_meeting",
|
||||||
|
"project_template"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "onsite_meeting",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "On-Site Meeting",
|
||||||
|
"options": "On-Site Meeting",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "project_template",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Project Template",
|
||||||
|
"options": "Project Template"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"grid_page_length": 50,
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"istable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2026-02-19 12:15:47.019375",
|
||||||
|
"modified_by": "casey@shilohcode.com",
|
||||||
|
"module": "Custom UI",
|
||||||
|
"name": "Address On-Site Meeting Link",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [],
|
||||||
|
"row_format": "Dynamic",
|
||||||
|
"rows_threshold_for_grid_search": 20,
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"states": []
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"allow_rename": 1,
|
||||||
|
"creation": "2026-02-18 05:53:08.879871",
|
||||||
|
"custom": 1,
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"project",
|
||||||
|
"project_template"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "project",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Project",
|
||||||
|
"options": "Project",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "project_template",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Project Template",
|
||||||
|
"options": "Project Template"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"grid_page_length": 50,
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"istable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2026-02-19 12:15:57.211249",
|
||||||
|
"modified_by": "casey@shilohcode.com",
|
||||||
|
"module": "Custom UI",
|
||||||
|
"name": "Address Project Link",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [],
|
||||||
|
"row_format": "Dynamic",
|
||||||
|
"rows_threshold_for_grid_search": 20,
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"states": []
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"allow_rename": 1,
|
||||||
|
"creation": "2026-02-18 05:53:08.933748",
|
||||||
|
"custom": 1,
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"quotation",
|
||||||
|
"project_template"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "quotation",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Quotation",
|
||||||
|
"options": "Quotation",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "project_template",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Project Template",
|
||||||
|
"options": "Project Template"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"grid_page_length": 50,
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"istable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2026-02-19 12:16:06.875841",
|
||||||
|
"modified_by": "casey@shilohcode.com",
|
||||||
|
"module": "Custom UI",
|
||||||
|
"name": "Address Quotation Link",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [],
|
||||||
|
"row_format": "Dynamic",
|
||||||
|
"rows_threshold_for_grid_search": 20,
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"states": []
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"allow_rename": 1,
|
||||||
|
"creation": "2026-02-18 05:53:09.040022",
|
||||||
|
"custom": 1,
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"sales_order",
|
||||||
|
"project_template"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "sales_order",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Sales Order",
|
||||||
|
"options": "Sales Order",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "project_template",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Project Template",
|
||||||
|
"options": "Project Template"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"grid_page_length": 50,
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"istable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2026-02-19 12:16:15.139526",
|
||||||
|
"modified_by": "casey@shilohcode.com",
|
||||||
|
"module": "Custom UI",
|
||||||
|
"name": "Address Sales Order Link",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [],
|
||||||
|
"row_format": "Dynamic",
|
||||||
|
"rows_threshold_for_grid_search": 20,
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"states": []
|
||||||
|
}
|
||||||
0
custom_ui/custom_ui/doctype/bid_meeting_note/__init__.py
Normal file
0
custom_ui/custom_ui/doctype/bid_meeting_note/__init__.py
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"allow_rename": 1,
|
||||||
|
"creation": "2026-02-18 05:53:10.182623",
|
||||||
|
"custom": 1,
|
||||||
|
"doctype": "DocType",
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"form_template",
|
||||||
|
"bid_meeting",
|
||||||
|
"notes",
|
||||||
|
"fields",
|
||||||
|
"quantities"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "form_template",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Form Template",
|
||||||
|
"options": "Bid Meeting Note Form",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "bid_meeting",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Bid Meeting",
|
||||||
|
"options": "On-Site Meeting",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "notes",
|
||||||
|
"fieldtype": "Small Text",
|
||||||
|
"label": "Notes"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "fields",
|
||||||
|
"fieldtype": "Table",
|
||||||
|
"label": "Fields",
|
||||||
|
"options": "Bid Meeting Note Field"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "quantities",
|
||||||
|
"fieldtype": "Table",
|
||||||
|
"label": "Quantities",
|
||||||
|
"options": "Bid Meeting Note Field Quantity"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"grid_page_length": 50,
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2026-02-18 05:53:10.230323",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Custom UI",
|
||||||
|
"name": "Bid Meeting Note",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "System Manager",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"row_format": "Dynamic",
|
||||||
|
"rows_threshold_for_grid_search": 20,
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"states": []
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
{
|
{
|
||||||
"actions": [],
|
"actions": [],
|
||||||
"allow_rename": 1,
|
"allow_rename": 1,
|
||||||
"creation": "2026-01-30 07:21:50.095868",
|
"creation": "2026-02-18 05:53:10.121229",
|
||||||
|
"custom": 1,
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
|
|
@ -94,7 +95,7 @@
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2026-01-30 07:52:08.063602",
|
"modified": "2026-02-18 05:53:10.166711",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Custom UI",
|
"module": "Custom UI",
|
||||||
"name": "Bid Meeting Note Field",
|
"name": "Bid Meeting Note Field",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
{
|
{
|
||||||
"actions": [],
|
"actions": [],
|
||||||
"allow_rename": 1,
|
"allow_rename": 1,
|
||||||
"creation": "2026-01-30 07:21:50.423957",
|
"creation": "2026-02-18 05:53:10.353383",
|
||||||
|
"custom": 1,
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
|
|
@ -36,7 +37,7 @@
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2026-01-30 07:51:49.006161",
|
"modified": "2026-02-18 05:53:10.394181",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Custom UI",
|
"module": "Custom UI",
|
||||||
"name": "Bid Meeting Note Field Quantity",
|
"name": "Bid Meeting Note Field Quantity",
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
{
|
{
|
||||||
"actions": [],
|
"actions": [],
|
||||||
"allow_rename": 1,
|
"allow_rename": 1,
|
||||||
"creation": "2026-01-30 07:01:57.052796",
|
"autoname": "format:{title}",
|
||||||
|
"creation": "2026-02-18 05:53:10.057094",
|
||||||
|
"custom": 1,
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"title",
|
"title",
|
||||||
"project_template",
|
|
||||||
"notes",
|
"notes",
|
||||||
"fields",
|
"fields",
|
||||||
"company"
|
"company"
|
||||||
|
|
@ -17,15 +18,8 @@
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Title",
|
"label": "Title",
|
||||||
"reqd": 1
|
"reqd": 1,
|
||||||
},
|
"unique": 1
|
||||||
{
|
|
||||||
"fieldname": "project_template",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"in_list_view": 1,
|
|
||||||
"label": "Project Template",
|
|
||||||
"options": "Project Template",
|
|
||||||
"reqd": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "notes",
|
"fieldname": "notes",
|
||||||
|
|
@ -49,10 +43,11 @@
|
||||||
"grid_page_length": 50,
|
"grid_page_length": 50,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2026-01-30 07:17:51.934698",
|
"modified": "2026-02-18 05:53:10.102840",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Custom UI",
|
"module": "Custom UI",
|
||||||
"name": "Bid Meeting Note Form",
|
"name": "Bid Meeting Note Form",
|
||||||
|
"naming_rule": "Expression",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
{
|
{
|
||||||
"actions": [],
|
"actions": [],
|
||||||
"allow_rename": 1,
|
"allow_rename": 1,
|
||||||
"creation": "2026-01-30 07:21:49.918704",
|
"creation": "2026-02-18 05:53:09.994325",
|
||||||
|
"custom": 1,
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
|
|
@ -135,7 +136,7 @@
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2026-01-30 07:52:16.305665",
|
"modified": "2026-02-18 05:53:10.041502",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Custom UI",
|
"module": "Custom UI",
|
||||||
"name": "Bid Meeting Note Form Field",
|
"name": "Bid Meeting Note Form Field",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
{
|
{
|
||||||
"actions": [],
|
"actions": [],
|
||||||
"allow_rename": 1,
|
"allow_rename": 1,
|
||||||
"creation": "2026-01-30 07:01:57.401662",
|
"creation": "2026-02-18 05:53:10.294998",
|
||||||
|
"custom": 1,
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
|
|
@ -16,7 +17,7 @@
|
||||||
"grid_page_length": 50,
|
"grid_page_length": 50,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2026-01-30 07:16:50.657332",
|
"modified": "2026-02-18 05:53:10.338801",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Custom UI",
|
"module": "Custom UI",
|
||||||
"name": "Condition",
|
"name": "Condition",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"allow_rename": 1,
|
||||||
|
"creation": "2026-02-18 05:53:09.096760",
|
||||||
|
"custom": 1,
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"address"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "address",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Address",
|
||||||
|
"options": "Address",
|
||||||
|
"reqd": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"grid_page_length": 50,
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"istable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2026-02-19 12:14:50.291119",
|
||||||
|
"modified_by": "casey@shilohcode.com",
|
||||||
|
"module": "Custom UI",
|
||||||
|
"name": "Contact Address Link",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [],
|
||||||
|
"row_format": "Dynamic",
|
||||||
|
"rows_threshold_for_grid_search": 20,
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"states": []
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
{
|
{
|
||||||
"actions": [],
|
"actions": [],
|
||||||
"allow_rename": 1,
|
"allow_rename": 1,
|
||||||
"creation": "2026-01-30 07:21:48.972109",
|
"creation": "2026-02-18 05:53:09.407100",
|
||||||
|
"custom": 1,
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
|
|
@ -20,7 +21,7 @@
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2026-01-30 07:52:31.110075",
|
"modified": "2026-02-18 05:53:09.446933",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Custom UI",
|
"module": "Custom UI",
|
||||||
"name": "Customer Address Link",
|
"name": "Customer Address Link",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
{
|
{
|
||||||
"actions": [],
|
"actions": [],
|
||||||
"allow_rename": 1,
|
"allow_rename": 1,
|
||||||
"creation": "2026-01-30 07:21:48.896768",
|
"creation": "2026-02-18 05:53:09.354310",
|
||||||
|
"custom": 1,
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
|
|
@ -18,7 +19,7 @@
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2026-01-30 07:52:38.531992",
|
"modified": "2026-02-18 05:53:09.393557",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Custom UI",
|
"module": "Custom UI",
|
||||||
"name": "Customer Company Link",
|
"name": "Customer Company Link",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
{
|
{
|
||||||
"actions": [],
|
"actions": [],
|
||||||
"allow_rename": 1,
|
"allow_rename": 1,
|
||||||
"creation": "2026-01-30 07:21:49.052039",
|
"creation": "2026-02-18 05:53:09.460094",
|
||||||
|
"custom": 1,
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
|
|
@ -20,7 +21,7 @@
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2026-01-30 07:52:24.170798",
|
"modified": "2026-02-18 05:53:09.498323",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Custom UI",
|
"module": "Custom UI",
|
||||||
"name": "Customer Contact Link",
|
"name": "Customer Contact Link",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"allow_rename": 1,
|
||||||
|
"creation": "2026-02-18 05:53:09.563147",
|
||||||
|
"custom": 1,
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"onsite_meeting"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "onsite_meeting",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "On-Site Meeting",
|
||||||
|
"options": "On-Site Meeting",
|
||||||
|
"reqd": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"grid_page_length": 50,
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"istable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2026-02-19 12:28:36.179299",
|
||||||
|
"modified_by": "casey@shilohcode.com",
|
||||||
|
"module": "Custom UI",
|
||||||
|
"name": "Customer On-Site Meeting Link",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [],
|
||||||
|
"row_format": "Dynamic",
|
||||||
|
"rows_threshold_for_grid_search": 20,
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"states": []
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"allow_rename": 1,
|
||||||
|
"creation": "2026-02-18 05:53:09.617767",
|
||||||
|
"custom": 1,
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"project"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "project",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Project",
|
||||||
|
"options": "Project",
|
||||||
|
"reqd": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"grid_page_length": 50,
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"istable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2026-02-19 12:28:47.305053",
|
||||||
|
"modified_by": "casey@shilohcode.com",
|
||||||
|
"module": "Custom UI",
|
||||||
|
"name": "Customer Project Link",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [],
|
||||||
|
"row_format": "Dynamic",
|
||||||
|
"rows_threshold_for_grid_search": 20,
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"states": []
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"allow_rename": 1,
|
||||||
|
"creation": "2026-02-18 05:53:09.670908",
|
||||||
|
"custom": 1,
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"quotation"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "quotation",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Quotation",
|
||||||
|
"options": "Quotation",
|
||||||
|
"reqd": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"grid_page_length": 50,
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"istable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2026-02-19 12:28:57.466997",
|
||||||
|
"modified_by": "casey@shilohcode.com",
|
||||||
|
"module": "Custom UI",
|
||||||
|
"name": "Customer Quotation Link",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [],
|
||||||
|
"row_format": "Dynamic",
|
||||||
|
"rows_threshold_for_grid_search": 20,
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"states": []
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"allow_rename": 1,
|
||||||
|
"creation": "2026-02-18 05:53:09.727425",
|
||||||
|
"custom": 1,
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"sales_order"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "sales_order",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Sales Order",
|
||||||
|
"options": "Sales Order",
|
||||||
|
"reqd": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"grid_page_length": 50,
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"istable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2026-02-19 12:29:06.649786",
|
||||||
|
"modified_by": "casey@shilohcode.com",
|
||||||
|
"module": "Custom UI",
|
||||||
|
"name": "Customer Sales Order Link",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [],
|
||||||
|
"row_format": "Dynamic",
|
||||||
|
"rows_threshold_for_grid_search": 20,
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"states": []
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
{
|
{
|
||||||
"actions": [],
|
"actions": [],
|
||||||
"allow_rename": 1,
|
"allow_rename": 1,
|
||||||
"creation": "2026-01-30 07:21:48.120856",
|
"creation": "2026-02-18 05:53:08.727283",
|
||||||
|
"custom": 1,
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
|
|
@ -29,7 +30,7 @@
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2026-01-30 07:52:52.271939",
|
"modified": "2026-02-18 05:53:08.766401",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Custom UI",
|
"module": "Custom UI",
|
||||||
"name": "Customer Task Link",
|
"name": "Customer Task Link",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"allow_rename": 1,
|
||||||
|
"creation": "2026-02-18 05:53:09.779109",
|
||||||
|
"custom": 1,
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"address"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "address",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Address",
|
||||||
|
"options": "Address",
|
||||||
|
"reqd": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"grid_page_length": 50,
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"istable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2026-02-19 12:16:22.611831",
|
||||||
|
"modified_by": "casey@shilohcode.com",
|
||||||
|
"module": "Custom UI",
|
||||||
|
"name": "Lead Address Link",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [],
|
||||||
|
"row_format": "Dynamic",
|
||||||
|
"rows_threshold_for_grid_search": 20,
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"states": []
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"allow_rename": 1,
|
||||||
|
"creation": "2026-02-18 05:53:08.828617",
|
||||||
|
"custom": 1,
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"company"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "company",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Company",
|
||||||
|
"options": "Company",
|
||||||
|
"reqd": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"grid_page_length": 50,
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"istable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2026-02-19 12:29:19.514404",
|
||||||
|
"modified_by": "casey@shilohcode.com",
|
||||||
|
"module": "Custom UI",
|
||||||
|
"name": "Lead Companies Link",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [],
|
||||||
|
"row_format": "Dynamic",
|
||||||
|
"rows_threshold_for_grid_search": 20,
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"states": []
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"allow_rename": 1,
|
||||||
|
"creation": "2026-02-18 05:53:08.671692",
|
||||||
|
"custom": 1,
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"company"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "company",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Company",
|
||||||
|
"options": "Company",
|
||||||
|
"reqd": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"grid_page_length": 50,
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"istable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2026-02-19 12:29:30.435977",
|
||||||
|
"modified_by": "casey@shilohcode.com",
|
||||||
|
"module": "Custom UI",
|
||||||
|
"name": "Lead Company Link",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [],
|
||||||
|
"row_format": "Dynamic",
|
||||||
|
"rows_threshold_for_grid_search": 20,
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"states": []
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"allow_rename": 1,
|
||||||
|
"creation": "2026-02-18 05:53:09.831349",
|
||||||
|
"custom": 1,
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"contact"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "contact",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Contact",
|
||||||
|
"options": "Contact",
|
||||||
|
"reqd": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"grid_page_length": 50,
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"istable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2026-02-19 12:29:41.971971",
|
||||||
|
"modified_by": "casey@shilohcode.com",
|
||||||
|
"module": "Custom UI",
|
||||||
|
"name": "Lead Contact Link",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [],
|
||||||
|
"row_format": "Dynamic",
|
||||||
|
"rows_threshold_for_grid_search": 20,
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"states": []
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"allow_rename": 1,
|
||||||
|
"creation": "2026-02-18 05:53:09.149765",
|
||||||
|
"custom": 1,
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"onsite_meeting"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "onsite_meeting",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "On-Site Meeting",
|
||||||
|
"options": "On-Site Meeting",
|
||||||
|
"reqd": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"grid_page_length": 50,
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"istable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2026-02-19 12:29:50.898270",
|
||||||
|
"modified_by": "casey@shilohcode.com",
|
||||||
|
"module": "Custom UI",
|
||||||
|
"name": "Lead On-Site Meeting Link",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [],
|
||||||
|
"row_format": "Dynamic",
|
||||||
|
"rows_threshold_for_grid_search": 20,
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"states": []
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"allow_rename": 1,
|
||||||
|
"creation": "2026-02-18 05:53:09.883408",
|
||||||
|
"custom": 1,
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"quotation"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "quotation",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Quotation",
|
||||||
|
"options": "Quotation",
|
||||||
|
"reqd": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"grid_page_length": 50,
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"istable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2026-02-19 12:30:01.642314",
|
||||||
|
"modified_by": "casey@shilohcode.com",
|
||||||
|
"module": "Custom UI",
|
||||||
|
"name": "Lead Quotation Link",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [],
|
||||||
|
"row_format": "Dynamic",
|
||||||
|
"rows_threshold_for_grid_search": 20,
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"states": []
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
{
|
{
|
||||||
"actions": [],
|
"actions": [],
|
||||||
"allow_rename": 1,
|
"allow_rename": 1,
|
||||||
"creation": "2026-01-30 07:21:50.267662",
|
"creation": "2026-02-18 05:53:10.243616",
|
||||||
|
"custom": 1,
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
|
|
@ -22,7 +23,7 @@
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2026-01-30 07:51:59.777431",
|
"modified": "2026-02-18 05:53:10.281244",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Custom UI",
|
"module": "Custom UI",
|
||||||
"name": "Project Task Link",
|
"name": "Project Task Link",
|
||||||
|
|
|
||||||
0
custom_ui/custom_ui/doctype/skip_day/__init__.py
Normal file
0
custom_ui/custom_ui/doctype/skip_day/__init__.py
Normal file
36
custom_ui/custom_ui/doctype/skip_day/skip_day.json
Normal file
36
custom_ui/custom_ui/doctype/skip_day/skip_day.json
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"allow_rename": 1,
|
||||||
|
"creation": "2026-02-18 05:53:10.480897",
|
||||||
|
"custom": 1,
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"date"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "date",
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Date",
|
||||||
|
"reqd": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"grid_page_length": 50,
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"istable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2026-02-18 05:53:10.518277",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Custom UI",
|
||||||
|
"name": "Skip Day",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [],
|
||||||
|
"row_format": "Dynamic",
|
||||||
|
"rows_threshold_for_grid_search": 20,
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"states": []
|
||||||
|
}
|
||||||
|
|
@ -1,153 +0,0 @@
|
||||||
[
|
|
||||||
{
|
|
||||||
"company": "Sprinklers Northwest",
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "Bid Meeting Note Form",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"column": 1,
|
|
||||||
"conditional_on_field": null,
|
|
||||||
"conditional_on_value": null,
|
|
||||||
"default_value": null,
|
|
||||||
"doctype_for_select": null,
|
|
||||||
"doctype_label_field": null,
|
|
||||||
"help_text": "Indicate if a locate is needed for this project.",
|
|
||||||
"include_options": 0,
|
|
||||||
"label": "Locate Needed",
|
|
||||||
"options": null,
|
|
||||||
"order": 0,
|
|
||||||
"parent": "SNW Install Bid Meeting Notes",
|
|
||||||
"parentfield": "fields",
|
|
||||||
"parenttype": "Bid Meeting Note Form",
|
|
||||||
"read_only": 0,
|
|
||||||
"required": 0,
|
|
||||||
"row": 1,
|
|
||||||
"type": "Check"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"column": 2,
|
|
||||||
"conditional_on_field": null,
|
|
||||||
"conditional_on_value": null,
|
|
||||||
"default_value": null,
|
|
||||||
"doctype_for_select": null,
|
|
||||||
"doctype_label_field": null,
|
|
||||||
"help_text": "Indicate if a permit is needed for this project.",
|
|
||||||
"include_options": 0,
|
|
||||||
"label": "Permit Needed",
|
|
||||||
"options": null,
|
|
||||||
"order": 0,
|
|
||||||
"parent": "SNW Install Bid Meeting Notes",
|
|
||||||
"parentfield": "fields",
|
|
||||||
"parenttype": "Bid Meeting Note Form",
|
|
||||||
"read_only": 0,
|
|
||||||
"required": 0,
|
|
||||||
"row": 1,
|
|
||||||
"type": "Check"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"column": 3,
|
|
||||||
"conditional_on_field": null,
|
|
||||||
"conditional_on_value": null,
|
|
||||||
"default_value": null,
|
|
||||||
"doctype_for_select": null,
|
|
||||||
"doctype_label_field": null,
|
|
||||||
"help_text": "Indicate if a backflow test is required after installation.",
|
|
||||||
"include_options": 0,
|
|
||||||
"label": "Back Flow Test Required",
|
|
||||||
"options": null,
|
|
||||||
"order": 0,
|
|
||||||
"parent": "SNW Install Bid Meeting Notes",
|
|
||||||
"parentfield": "fields",
|
|
||||||
"parenttype": "Bid Meeting Note Form",
|
|
||||||
"read_only": 0,
|
|
||||||
"required": 0,
|
|
||||||
"row": 1,
|
|
||||||
"type": "Check"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"column": 1,
|
|
||||||
"conditional_on_field": null,
|
|
||||||
"conditional_on_value": null,
|
|
||||||
"default_value": null,
|
|
||||||
"doctype_for_select": null,
|
|
||||||
"doctype_label_field": null,
|
|
||||||
"help_text": null,
|
|
||||||
"include_options": 0,
|
|
||||||
"label": "Machine Access",
|
|
||||||
"options": null,
|
|
||||||
"order": 0,
|
|
||||||
"parent": "SNW Install Bid Meeting Notes",
|
|
||||||
"parentfield": "fields",
|
|
||||||
"parenttype": "Bid Meeting Note Form",
|
|
||||||
"read_only": 0,
|
|
||||||
"required": 0,
|
|
||||||
"row": 2,
|
|
||||||
"type": "Check"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"column": 2,
|
|
||||||
"conditional_on_field": "Machine Access",
|
|
||||||
"conditional_on_value": null,
|
|
||||||
"default_value": null,
|
|
||||||
"doctype_for_select": null,
|
|
||||||
"doctype_label_field": null,
|
|
||||||
"help_text": null,
|
|
||||||
"include_options": 1,
|
|
||||||
"label": "Machines",
|
|
||||||
"options": "MT, Skip Steer, Excavator-E-50, Link Belt, Tre?, Forks, Auger, Backhoe, Loader, Duzer",
|
|
||||||
"order": 0,
|
|
||||||
"parent": "SNW Install Bid Meeting Notes",
|
|
||||||
"parentfield": "fields",
|
|
||||||
"parenttype": "Bid Meeting Note Form",
|
|
||||||
"read_only": 0,
|
|
||||||
"required": 0,
|
|
||||||
"row": 2,
|
|
||||||
"type": "Multi-Select"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"column": 0,
|
|
||||||
"conditional_on_field": null,
|
|
||||||
"conditional_on_value": null,
|
|
||||||
"default_value": null,
|
|
||||||
"doctype_for_select": null,
|
|
||||||
"doctype_label_field": null,
|
|
||||||
"help_text": null,
|
|
||||||
"include_options": 0,
|
|
||||||
"label": "Materials Required",
|
|
||||||
"options": null,
|
|
||||||
"order": 0,
|
|
||||||
"parent": "SNW Install Bid Meeting Notes",
|
|
||||||
"parentfield": "fields",
|
|
||||||
"parenttype": "Bid Meeting Note Form",
|
|
||||||
"read_only": 0,
|
|
||||||
"required": 0,
|
|
||||||
"row": 3,
|
|
||||||
"type": "Check"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"column": 0,
|
|
||||||
"conditional_on_field": "Materials Required",
|
|
||||||
"conditional_on_value": null,
|
|
||||||
"default_value": null,
|
|
||||||
"doctype_for_select": "Item",
|
|
||||||
"doctype_label_field": "itemName",
|
|
||||||
"help_text": null,
|
|
||||||
"include_options": 0,
|
|
||||||
"label": "Materials",
|
|
||||||
"options": null,
|
|
||||||
"order": 0,
|
|
||||||
"parent": "SNW Install Bid Meeting Notes",
|
|
||||||
"parentfield": "fields",
|
|
||||||
"parenttype": "Bid Meeting Note Form",
|
|
||||||
"read_only": 0,
|
|
||||||
"required": 0,
|
|
||||||
"row": 4,
|
|
||||||
"type": "Multi-Select w/ Quantity"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"modified": "2026-02-18 05:52:37.304228",
|
|
||||||
"name": "SNW Install Bid Meeting Notes",
|
|
||||||
"notes": null,
|
|
||||||
"title": "SNW Install Bid Meeting Notes"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
@ -1,72 +0,0 @@
|
||||||
[
|
|
||||||
{
|
|
||||||
"bid_meeting_note_form": "SNW Install Bid Meeting Notes",
|
|
||||||
"calendar_color": "#c1dec5",
|
|
||||||
"company": "Sprinklers Northwest",
|
|
||||||
"custom__complete_method": "Task Weight",
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "Project Template",
|
|
||||||
"item_groups": "SNW-I, SNW-S, SNW-LS",
|
|
||||||
"modified": "2026-02-16 03:59:53.719382",
|
|
||||||
"name": "SNW Install",
|
|
||||||
"project_type": "External",
|
|
||||||
"tasks": [
|
|
||||||
{
|
|
||||||
"parent": "SNW Install",
|
|
||||||
"parentfield": "tasks",
|
|
||||||
"parenttype": "Project Template",
|
|
||||||
"subject": "Send customer 3-5 day window for start date",
|
|
||||||
"task": "TASK-2025-00001"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"parent": "SNW Install",
|
|
||||||
"parentfield": "tasks",
|
|
||||||
"parenttype": "Project Template",
|
|
||||||
"subject": "811/Locate call in",
|
|
||||||
"task": "TASK-2025-00002"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"parent": "SNW Install",
|
|
||||||
"parentfield": "tasks",
|
|
||||||
"parenttype": "Project Template",
|
|
||||||
"subject": "Permit(s) call in and pay",
|
|
||||||
"task": "TASK-2025-00003"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"parent": "SNW Install",
|
|
||||||
"parentfield": "tasks",
|
|
||||||
"parenttype": "Project Template",
|
|
||||||
"subject": "Primary Job",
|
|
||||||
"task": "TASK-2025-00004"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"parent": "SNW Install",
|
|
||||||
"parentfield": "tasks",
|
|
||||||
"parenttype": "Project Template",
|
|
||||||
"subject": "Hydroseeding",
|
|
||||||
"task": "TASK-2025-00005"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"parent": "SNW Install",
|
|
||||||
"parentfield": "tasks",
|
|
||||||
"parenttype": "Project Template",
|
|
||||||
"subject": "Curbing",
|
|
||||||
"task": "TASK-2025-00006"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"parent": "SNW Install",
|
|
||||||
"parentfield": "tasks",
|
|
||||||
"parenttype": "Project Template",
|
|
||||||
"subject": "15-Day QA",
|
|
||||||
"task": "TASK-2025-00007"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"parent": "SNW Install",
|
|
||||||
"parentfield": "tasks",
|
|
||||||
"parenttype": "Project Template",
|
|
||||||
"subject": "Permit Close-out",
|
|
||||||
"task": "TASK-2025-00008"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
@ -69,6 +69,7 @@ const FRAPPE_GET_CLIENT_METHOD = "custom_ui.api.db.clients.get_client_v2";
|
||||||
const FRAPPE_GET_CLIENT_NAMES_METHOD = "custom_ui.api.db.clients.get_client_names";
|
const FRAPPE_GET_CLIENT_NAMES_METHOD = "custom_ui.api.db.clients.get_client_names";
|
||||||
const FRAPPE_CHECK_CLIENT_EXISTS_METHOD = "custom_ui.api.db.clients.check_client_exists";
|
const FRAPPE_CHECK_CLIENT_EXISTS_METHOD = "custom_ui.api.db.clients.check_client_exists";
|
||||||
const FRAPPE_ADD_ADDRESSES_CONTACTS_METHOD = "custom_ui.api.db.clients.add_addresses_contacts";
|
const FRAPPE_ADD_ADDRESSES_CONTACTS_METHOD = "custom_ui.api.db.clients.add_addresses_contacts";
|
||||||
|
const FRAPPE_CREATE_CLIENT_CONTACTS_ADDRESSES_METHOD = "custom_ui.api.db.clients.create_client_contacts_addresses";
|
||||||
// Employee methods
|
// Employee methods
|
||||||
const FRAPPE_GET_EMPLOYEES_METHOD = "custom_ui.api.db.employees.get_employees";
|
const FRAPPE_GET_EMPLOYEES_METHOD = "custom_ui.api.db.employees.get_employees";
|
||||||
const FRAPPE_GET_EMPLOYEES_ORGANIZED_METHOD = "custom_ui.api.db.employees.get_employees_organized";
|
const FRAPPE_GET_EMPLOYEES_ORGANIZED_METHOD = "custom_ui.api.db.employees.get_employees_organized";
|
||||||
|
|
@ -188,6 +189,10 @@ class Api {
|
||||||
return await this.request(FRAPPE_ADD_ADDRESSES_CONTACTS_METHOD, { clientName, companyName, addresses, contacts });
|
return await this.request(FRAPPE_ADD_ADDRESSES_CONTACTS_METHOD, { clientName, companyName, addresses, contacts });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async createClientContactsAddresses(clientName, company, contacts = [], addresses = []) {
|
||||||
|
return await this.request(FRAPPE_CREATE_CLIENT_CONTACTS_ADDRESSES_METHOD, { clientName, company, contacts, addresses });
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// ON-SITE MEETING METHODS
|
// ON-SITE MEETING METHODS
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
|
||||||
|
|
@ -66,18 +66,23 @@ import Dialog from 'primevue/dialog';
|
||||||
import Button from 'primevue/button';
|
import Button from 'primevue/button';
|
||||||
import ModalContactForm from './ModalContactForm.vue';
|
import ModalContactForm from './ModalContactForm.vue';
|
||||||
import ModalAddressForm from './ModalAddressForm.vue';
|
import ModalAddressForm from './ModalAddressForm.vue';
|
||||||
|
import Api from '../../api';
|
||||||
|
import { useCompanyStore } from '../../stores/company';
|
||||||
|
|
||||||
|
const companyStore = useCompanyStore();
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
visible: Boolean,
|
visible: Boolean,
|
||||||
|
clientName: { type: String, default: '' },
|
||||||
clientContacts: { type: Array, default: () => [] },
|
clientContacts: { type: Array, default: () => [] },
|
||||||
existingContacts: { type: Array, default: () => [] },
|
existingContacts: { type: Array, default: () => [] },
|
||||||
existingAddresses: { type: Array, default: () => [] },
|
existingAddresses: { type: Array, default: () => [] },
|
||||||
isSubmitting: { type: Boolean, default: false },
|
|
||||||
});
|
});
|
||||||
const emit = defineEmits(['update:visible', 'created']);
|
const emit = defineEmits(['update:visible', 'created']);
|
||||||
|
|
||||||
const showContacts = ref(false);
|
const showContacts = ref(false);
|
||||||
const showAddresses = ref(false);
|
const showAddresses = ref(false);
|
||||||
|
const isSubmitting = ref(false);
|
||||||
|
|
||||||
// Direct arrays instead of wrapping in formData objects
|
// Direct arrays instead of wrapping in formData objects
|
||||||
const newContacts = ref([
|
const newContacts = ref([
|
||||||
|
|
@ -136,16 +141,57 @@ function close() {
|
||||||
emit('update:visible', false);
|
emit('update:visible', false);
|
||||||
}
|
}
|
||||||
|
|
||||||
function create() {
|
async function create() {
|
||||||
const payload = {};
|
isSubmitting.value = true;
|
||||||
if (showContacts.value) {
|
try {
|
||||||
payload.contacts = newContacts.value;
|
const contactsToSend = showContacts.value ? newContacts.value : [];
|
||||||
|
const addressesToSend = showAddresses.value ? newAddresses.value : [];
|
||||||
|
|
||||||
|
// Check if any contacts or addresses already exist
|
||||||
|
const existingMessages = [];
|
||||||
|
|
||||||
|
if (contactsToSend.length > 0) {
|
||||||
|
const existingContactsResult = await Api.checkContactsExist(contactsToSend);
|
||||||
|
if (existingContactsResult && existingContactsResult.length > 0) {
|
||||||
|
const names = existingContactsResult.map(c => `${c.firstName || ''} ${c.lastName || ''}`.trim()).join(', ');
|
||||||
|
existingMessages.push(`Contact(s) already exist: ${names}`);
|
||||||
}
|
}
|
||||||
if (showAddresses.value) {
|
|
||||||
payload.addresses = newAddresses.value;
|
|
||||||
}
|
}
|
||||||
emit('created', payload);
|
|
||||||
|
if (addressesToSend.length > 0) {
|
||||||
|
const existingAddressesResult = await Api.checkAddressesExist(addressesToSend);
|
||||||
|
if (existingAddressesResult && existingAddressesResult.length > 0) {
|
||||||
|
const addrs = existingAddressesResult.map(a => `${a.addressLine1 || ''} ${a.city || ''}`).join(', ');
|
||||||
|
existingMessages.push(`Address(es) already exist: ${addrs}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If any exist, prompt the user for confirmation
|
||||||
|
if (existingMessages.length > 0) {
|
||||||
|
const message = existingMessages.join('\n') + '\n\nWould you like to proceed anyway? Existing records will be linked instead of duplicated.';
|
||||||
|
if (!window.confirm(message)) {
|
||||||
|
isSubmitting.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call API to create/link contacts and addresses
|
||||||
|
// Address contacts/primaryContact are dicts with firstName, lastName, email
|
||||||
|
// that the backend matches against created/existing contact docs
|
||||||
|
const result = await Api.createClientContactsAddresses(
|
||||||
|
props.clientName,
|
||||||
|
companyStore.currentCompany,
|
||||||
|
contactsToSend,
|
||||||
|
addressesToSend
|
||||||
|
);
|
||||||
|
|
||||||
|
emit('created', result);
|
||||||
close();
|
close();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating contacts/addresses:', error);
|
||||||
|
} finally {
|
||||||
|
isSubmitting.value = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -98,9 +98,11 @@
|
||||||
<AddContactAddressModal
|
<AddContactAddressModal
|
||||||
:visible="showAddModal"
|
:visible="showAddModal"
|
||||||
@update:visible="showAddModal = $event"
|
@update:visible="showAddModal = $event"
|
||||||
|
:clientName="clientData.customerName"
|
||||||
:clientContacts="clientData.contacts || []"
|
:clientContacts="clientData.contacts || []"
|
||||||
:existingContacts="clientData.contacts?.map(c => c.fullName || c.name) || []"
|
:existingContacts="clientData.contacts?.map(c => c.fullName || c.name) || []"
|
||||||
:existingAddresses="clientData.addresses?.map(a => a.addressLine1) || []"
|
:existingAddresses="clientData.addresses?.map(a => a.addressLine1) || []"
|
||||||
|
@created="onContactsAddressesCreated"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -126,6 +128,8 @@ const props = defineProps({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['refresh']);
|
||||||
|
|
||||||
// Check if client is a Lead
|
// Check if client is a Lead
|
||||||
const isLead = computed(() => props.clientData.doctype === "Lead");
|
const isLead = computed(() => props.clientData.doctype === "Lead");
|
||||||
|
|
||||||
|
|
@ -173,6 +177,11 @@ const formattedCreationDate = computed(() => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Handle successful contact/address creation - emit refresh so parent reloads client data
|
||||||
|
const onContactsAddressesCreated = () => {
|
||||||
|
emit('refresh');
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@
|
||||||
<GeneralClientInfo
|
<GeneralClientInfo
|
||||||
v-if="client.customerName"
|
v-if="client.customerName"
|
||||||
:client-data="client"
|
:client-data="client"
|
||||||
|
@refresh="refreshClient"
|
||||||
/>
|
/>
|
||||||
<AdditionalInfoBar :address="client.addresses[selectedAddressIdx]" v-if="client.customerName" />
|
<AdditionalInfoBar :address="client.addresses[selectedAddressIdx]" v-if="client.customerName" />
|
||||||
|
|
||||||
|
|
@ -461,6 +462,12 @@ const handleCustomerSelected = (clientData) => {
|
||||||
// Handle customer selected from search
|
// Handle customer selected from search
|
||||||
client.value = { ...client.value, ...clientData };
|
client.value = { ...client.value, ...clientData };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const refreshClient = async () => {
|
||||||
|
if (clientName) {
|
||||||
|
await getClient(clientName);
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
<style lang="css">
|
<style lang="css">
|
||||||
.tab-info-alert {
|
.tab-info-alert {
|
||||||
|
|
|
||||||
|
|
@ -390,7 +390,6 @@ const loadChartData = async() => {
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(async() => {
|
onMounted(async() => {
|
||||||
notifications.addWarning("Dashboard metrics are based on dummy data for demonstration purposes. UPDATES COMING SOON!");
|
|
||||||
await loadChartData();
|
await loadChartData();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue