diff --git a/custom_ui/api/db/clients.py b/custom_ui/api/db/clients.py index 5c8aae8..dd0839c 100644 --- a/custom_ui/api/db/clients.py +++ b/custom_ui/api/db/clients.py @@ -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): """Get counts of clients by status categories with optional weekly filtering.""" # Build base filters for date range if weekly filtering is enabled - try: - base_filters = {} - 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 - # Common options: creation, modified, custom_date_field, etc. - base_filters["creation"] = ["between", [week_start_date, week_end_date]] + # try: + # base_filters = {} + # 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 + # # Common options: creation, modified, custom_date_field, etc. + # base_filters["creation"] = ["between", [week_start_date, week_end_date]] - # Helper function to merge base filters with status filters - def get_filters(status_field, status_value): - filters = {status_field: status_value} - filters.update(base_filters) - return filters + # # Helper function to merge base filters with status filters + # def get_filters(status_field, status_value): + # filters = {status_field: status_value} + # filters.update(base_filters) + # return filters - onsite_meeting_scheduled_status_counts = { - "label": "On-Site Meeting Scheduled", - "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")), - "completed": frappe.db.count("Address", filters=get_filters("custom_onsite_meeting_scheduled", "Completed")) - } + # onsite_meeting_scheduled_status_counts = { + # "label": "On-Site Meeting Scheduled", + # "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")), + # "completed": frappe.db.count("Address", filters=get_filters("custom_onsite_meeting_scheduled", "Completed")) + # } - estimate_sent_status_counts = { - "label": "Estimate Sent", - "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")), - "completed": frappe.db.count("Address", filters=get_filters("custom_estimate_sent_status", "Completed")) - } + # estimate_sent_status_counts = { + # "label": "Estimate Sent", + # "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")), + # "completed": frappe.db.count("Address", filters=get_filters("custom_estimate_sent_status", "Completed")) + # } - job_status_counts = { - "label": "Job Status", - "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")), - "completed": frappe.db.count("Address", filters=get_filters("custom_job_status", "Completed")) - } + # job_status_counts = { + # "label": "Job Status", + # "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")), + # "completed": frappe.db.count("Address", filters=get_filters("custom_job_status", "Completed")) + # } - payment_received_status_counts = { - "label": "Payment Received", - "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")), - "completed": frappe.db.count("Address", filters=get_filters("custom_payment_received_status", "Completed")) - } + # payment_received_status_counts = { + # "label": "Payment Received", + # "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")), + # "completed": frappe.db.count("Address", filters=get_filters("custom_payment_received_status", "Completed")) + # } - status_dicts = [ - onsite_meeting_scheduled_status_counts, - estimate_sent_status_counts, - job_status_counts, - payment_received_status_counts - ] + # status_dicts = [ + # onsite_meeting_scheduled_status_counts, + # estimate_sent_status_counts, + # job_status_counts, + # payment_received_status_counts + # ] - categories = [] - for status_dict in status_dicts: - category = { - "label": status_dict["label"], - "statuses": [ - { - "color": "red", - "label": "Not Started", - "count": status_dict["not_started"] - }, - { - "color": "yellow", - "label": "In Progress", - "count": status_dict["in_progress"] - }, - { - "color": "green", - "label": "Completed", - "count": status_dict["completed"] - } - ] - } - categories.append(category) + # categories = [] + # for status_dict in status_dicts: + # category = { + # "label": status_dict["label"], + # "statuses": [ + # { + # "color": "red", + # "label": "Not Started", + # "count": status_dict["not_started"] + # }, + # { + # "color": "yellow", + # "label": "In Progress", + # "count": status_dict["in_progress"] + # }, + # { + # "color": "green", + # "label": "Completed", + # "count": status_dict["completed"] + # } + # ] + # } + # categories.append(category) - return build_success_response(categories) - except frappe.ValidationError as ve: - return build_error_response(str(ve), 400) - except Exception as e: - return build_error_response(str(e), 500) + return build_success_response("success") + # except frappe.ValidationError as ve: + # return build_error_response(str(ve), 400) + # except Exception as e: + # return build_error_response(str(e), 500) @frappe.whitelist() @@ -682,4 +682,142 @@ def find_primary_contact_or_throw(contacts): if contact.get("is_primary"): print("#####DEBUG: Primary contact found:", contact) return contact - raise ValueError("No primary contact found in contacts list.") \ No newline at end of file + 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) \ No newline at end of file diff --git a/custom_ui/commands.py b/custom_ui/commands.py index 5910c90..985a503 100644 --- a/custom_ui/commands.py +++ b/custom_ui/commands.py @@ -107,12 +107,19 @@ 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(path, dry_run): +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") @@ -123,36 +130,58 @@ def import_aspire_migration(path, dry_run): 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 frappe.db.exists("Customer", rec.get("customer_name")): + if rec.get("customer_name") in existing_customers: skipped += 1 continue - doc = frappe.get_doc(rec) - doc.insert(ignore_permissions=True) + 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) % 500 == 0: + 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}") + 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) @@ -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')}") continue try: - doc = frappe.get_doc(rec) - doc.insert(ignore_permissions=True) + 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) % 500 == 0: + 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}") + 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) @@ -188,25 +217,35 @@ def import_aspire_migration(path, dry_run): click.echo(f" [DRY RUN] Would insert Address: {rec.get('address_line1')}") continue try: - doc = frappe.get_doc(rec) - doc.insert(ignore_permissions=True) + fast_insert(rec, "Address") success += 1 except Exception as e: failed += 1 click.echo(f" ⚠️ Address '{rec.get('address_line1', '?')}': {e}") - if (i + 1) % 500 == 0: + 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}") + 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") @@ -214,32 +253,49 @@ def import_aspire_migration(path, dry_run): click.echo(f" [DRY RUN] Would update Customer: {customer_name}") continue try: - if not frappe.db.exists("Customer", customer_name): + if customer_name not in existing_customers: skipped += 1 continue - doc = frappe.get_doc("Customer", customer_name) - + # Directly insert child rows without loading/saving parent doc 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", []): - 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 except Exception as e: failed += 1 click.echo(f" ⚠️ Update '{customer_name}': {e}") - if (i + 1) % 500 == 0: + 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}") + 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() diff --git a/custom_ui/custom_ui/doctype/address_contact_link/__init__.py b/custom_ui/custom_ui/doctype/address_contact_link/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/custom_ui/custom_ui/doctype/address_contact_link/address_contact_link.json b/custom_ui/custom_ui/doctype/address_contact_link/address_contact_link.json new file mode 100644 index 0000000..a318e4b --- /dev/null +++ b/custom_ui/custom_ui/doctype/address_contact_link/address_contact_link.json @@ -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": [] +} \ No newline at end of file diff --git a/custom_ui/custom_ui/doctype/address_on_site_meeting_link/__init__.py b/custom_ui/custom_ui/doctype/address_on_site_meeting_link/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/custom_ui/custom_ui/doctype/address_on_site_meeting_link/address_on_site_meeting_link.json b/custom_ui/custom_ui/doctype/address_on_site_meeting_link/address_on_site_meeting_link.json new file mode 100644 index 0000000..4712dc3 --- /dev/null +++ b/custom_ui/custom_ui/doctype/address_on_site_meeting_link/address_on_site_meeting_link.json @@ -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": [] +} \ No newline at end of file diff --git a/custom_ui/custom_ui/doctype/address_project_link/__init__.py b/custom_ui/custom_ui/doctype/address_project_link/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/custom_ui/custom_ui/doctype/address_project_link/address_project_link.json b/custom_ui/custom_ui/doctype/address_project_link/address_project_link.json new file mode 100644 index 0000000..a8ec83c --- /dev/null +++ b/custom_ui/custom_ui/doctype/address_project_link/address_project_link.json @@ -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": [] +} \ No newline at end of file diff --git a/custom_ui/custom_ui/doctype/address_quotation_link/__init__.py b/custom_ui/custom_ui/doctype/address_quotation_link/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/custom_ui/custom_ui/doctype/address_quotation_link/address_quotation_link.json b/custom_ui/custom_ui/doctype/address_quotation_link/address_quotation_link.json new file mode 100644 index 0000000..b9fdf94 --- /dev/null +++ b/custom_ui/custom_ui/doctype/address_quotation_link/address_quotation_link.json @@ -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": [] +} \ No newline at end of file diff --git a/custom_ui/custom_ui/doctype/address_sales_order_link/__init__.py b/custom_ui/custom_ui/doctype/address_sales_order_link/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/custom_ui/custom_ui/doctype/address_sales_order_link/address_sales_order_link.json b/custom_ui/custom_ui/doctype/address_sales_order_link/address_sales_order_link.json new file mode 100644 index 0000000..fec5fd5 --- /dev/null +++ b/custom_ui/custom_ui/doctype/address_sales_order_link/address_sales_order_link.json @@ -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": [] +} \ No newline at end of file diff --git a/custom_ui/custom_ui/doctype/bid_meeting_note/__init__.py b/custom_ui/custom_ui/doctype/bid_meeting_note/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/custom_ui/custom_ui/doctype/bid_meeting_note/bid_meeting_note.json b/custom_ui/custom_ui/doctype/bid_meeting_note/bid_meeting_note.json new file mode 100644 index 0000000..74e9c3d --- /dev/null +++ b/custom_ui/custom_ui/doctype/bid_meeting_note/bid_meeting_note.json @@ -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": [] +} \ No newline at end of file diff --git a/custom_ui/custom_ui/doctype/bid_meeting_note_field/bid_meeting_note_field.json b/custom_ui/custom_ui/doctype/bid_meeting_note_field/bid_meeting_note_field.json index fcf353d..77dd2e7 100644 --- a/custom_ui/custom_ui/doctype/bid_meeting_note_field/bid_meeting_note_field.json +++ b/custom_ui/custom_ui/doctype/bid_meeting_note_field/bid_meeting_note_field.json @@ -1,7 +1,8 @@ { "actions": [], "allow_rename": 1, - "creation": "2026-01-30 07:21:50.095868", + "creation": "2026-02-18 05:53:10.121229", + "custom": 1, "doctype": "DocType", "editable_grid": 1, "engine": "InnoDB", @@ -94,7 +95,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2026-01-30 07:52:08.063602", + "modified": "2026-02-18 05:53:10.166711", "modified_by": "Administrator", "module": "Custom UI", "name": "Bid Meeting Note Field", diff --git a/custom_ui/custom_ui/doctype/bid_meeting_note_field_quantity/bid_meeting_note_field_quantity.json b/custom_ui/custom_ui/doctype/bid_meeting_note_field_quantity/bid_meeting_note_field_quantity.json index 7d35865..2af45fe 100644 --- a/custom_ui/custom_ui/doctype/bid_meeting_note_field_quantity/bid_meeting_note_field_quantity.json +++ b/custom_ui/custom_ui/doctype/bid_meeting_note_field_quantity/bid_meeting_note_field_quantity.json @@ -1,7 +1,8 @@ { "actions": [], "allow_rename": 1, - "creation": "2026-01-30 07:21:50.423957", + "creation": "2026-02-18 05:53:10.353383", + "custom": 1, "doctype": "DocType", "editable_grid": 1, "engine": "InnoDB", @@ -36,7 +37,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2026-01-30 07:51:49.006161", + "modified": "2026-02-18 05:53:10.394181", "modified_by": "Administrator", "module": "Custom UI", "name": "Bid Meeting Note Field Quantity", diff --git a/custom_ui/custom_ui/doctype/bid_meeting_note_form/bid_meeting_note_form.json b/custom_ui/custom_ui/doctype/bid_meeting_note_form/bid_meeting_note_form.json index 55c7294..be1b8d7 100644 --- a/custom_ui/custom_ui/doctype/bid_meeting_note_form/bid_meeting_note_form.json +++ b/custom_ui/custom_ui/doctype/bid_meeting_note_form/bid_meeting_note_form.json @@ -1,12 +1,13 @@ { "actions": [], "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", "engine": "InnoDB", "field_order": [ "title", - "project_template", "notes", "fields", "company" @@ -17,15 +18,8 @@ "fieldtype": "Data", "in_list_view": 1, "label": "Title", - "reqd": 1 - }, - { - "fieldname": "project_template", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Project Template", - "options": "Project Template", - "reqd": 0 + "reqd": 1, + "unique": 1 }, { "fieldname": "notes", @@ -49,10 +43,11 @@ "grid_page_length": 50, "index_web_pages_for_search": 1, "links": [], - "modified": "2026-01-30 07:17:51.934698", + "modified": "2026-02-18 05:53:10.102840", "modified_by": "Administrator", "module": "Custom UI", "name": "Bid Meeting Note Form", + "naming_rule": "Expression", "owner": "Administrator", "permissions": [ { diff --git a/custom_ui/custom_ui/doctype/bid_meeting_note_form_field/bid_meeting_note_form_field.json b/custom_ui/custom_ui/doctype/bid_meeting_note_form_field/bid_meeting_note_form_field.json index ab9a536..8285cba 100644 --- a/custom_ui/custom_ui/doctype/bid_meeting_note_form_field/bid_meeting_note_form_field.json +++ b/custom_ui/custom_ui/doctype/bid_meeting_note_form_field/bid_meeting_note_form_field.json @@ -1,7 +1,8 @@ { "actions": [], "allow_rename": 1, - "creation": "2026-01-30 07:21:49.918704", + "creation": "2026-02-18 05:53:09.994325", + "custom": 1, "doctype": "DocType", "editable_grid": 1, "engine": "InnoDB", @@ -135,7 +136,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2026-01-30 07:52:16.305665", + "modified": "2026-02-18 05:53:10.041502", "modified_by": "Administrator", "module": "Custom UI", "name": "Bid Meeting Note Form Field", diff --git a/custom_ui/custom_ui/doctype/condition/condition.json b/custom_ui/custom_ui/doctype/condition/condition.json index bb10d4f..8cc6266 100644 --- a/custom_ui/custom_ui/doctype/condition/condition.json +++ b/custom_ui/custom_ui/doctype/condition/condition.json @@ -1,7 +1,8 @@ { "actions": [], "allow_rename": 1, - "creation": "2026-01-30 07:01:57.401662", + "creation": "2026-02-18 05:53:10.294998", + "custom": 1, "doctype": "DocType", "engine": "InnoDB", "field_order": [ @@ -16,7 +17,7 @@ "grid_page_length": 50, "index_web_pages_for_search": 1, "links": [], - "modified": "2026-01-30 07:16:50.657332", + "modified": "2026-02-18 05:53:10.338801", "modified_by": "Administrator", "module": "Custom UI", "name": "Condition", diff --git a/custom_ui/custom_ui/doctype/contact_address_link/__init__.py b/custom_ui/custom_ui/doctype/contact_address_link/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/custom_ui/custom_ui/doctype/contact_address_link/contact_address_link.json b/custom_ui/custom_ui/doctype/contact_address_link/contact_address_link.json new file mode 100644 index 0000000..14cde86 --- /dev/null +++ b/custom_ui/custom_ui/doctype/contact_address_link/contact_address_link.json @@ -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": [] +} \ No newline at end of file diff --git a/custom_ui/custom_ui/doctype/customer_address_link/customer_address_link.json b/custom_ui/custom_ui/doctype/customer_address_link/customer_address_link.json index 31ded93..50c2cc0 100644 --- a/custom_ui/custom_ui/doctype/customer_address_link/customer_address_link.json +++ b/custom_ui/custom_ui/doctype/customer_address_link/customer_address_link.json @@ -1,7 +1,8 @@ { "actions": [], "allow_rename": 1, - "creation": "2026-01-30 07:21:48.972109", + "creation": "2026-02-18 05:53:09.407100", + "custom": 1, "doctype": "DocType", "editable_grid": 1, "engine": "InnoDB", @@ -20,7 +21,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2026-01-30 07:52:31.110075", + "modified": "2026-02-18 05:53:09.446933", "modified_by": "Administrator", "module": "Custom UI", "name": "Customer Address Link", diff --git a/custom_ui/custom_ui/doctype/customer_company_link/customer_company_link.json b/custom_ui/custom_ui/doctype/customer_company_link/customer_company_link.json index 7c91601..085db4c 100644 --- a/custom_ui/custom_ui/doctype/customer_company_link/customer_company_link.json +++ b/custom_ui/custom_ui/doctype/customer_company_link/customer_company_link.json @@ -1,7 +1,8 @@ { "actions": [], "allow_rename": 1, - "creation": "2026-01-30 07:21:48.896768", + "creation": "2026-02-18 05:53:09.354310", + "custom": 1, "doctype": "DocType", "editable_grid": 1, "engine": "InnoDB", @@ -18,7 +19,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2026-01-30 07:52:38.531992", + "modified": "2026-02-18 05:53:09.393557", "modified_by": "Administrator", "module": "Custom UI", "name": "Customer Company Link", diff --git a/custom_ui/custom_ui/doctype/customer_contact_link/customer_contact_link.json b/custom_ui/custom_ui/doctype/customer_contact_link/customer_contact_link.json index 4299c50..5eb179d 100644 --- a/custom_ui/custom_ui/doctype/customer_contact_link/customer_contact_link.json +++ b/custom_ui/custom_ui/doctype/customer_contact_link/customer_contact_link.json @@ -1,7 +1,8 @@ { "actions": [], "allow_rename": 1, - "creation": "2026-01-30 07:21:49.052039", + "creation": "2026-02-18 05:53:09.460094", + "custom": 1, "doctype": "DocType", "editable_grid": 1, "engine": "InnoDB", @@ -20,7 +21,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2026-01-30 07:52:24.170798", + "modified": "2026-02-18 05:53:09.498323", "modified_by": "Administrator", "module": "Custom UI", "name": "Customer Contact Link", diff --git a/custom_ui/custom_ui/doctype/customer_on_site_meeting_link/__init__.py b/custom_ui/custom_ui/doctype/customer_on_site_meeting_link/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/custom_ui/custom_ui/doctype/customer_on_site_meeting_link/customer_on_site_meeting_link.json b/custom_ui/custom_ui/doctype/customer_on_site_meeting_link/customer_on_site_meeting_link.json new file mode 100644 index 0000000..179f647 --- /dev/null +++ b/custom_ui/custom_ui/doctype/customer_on_site_meeting_link/customer_on_site_meeting_link.json @@ -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": [] +} \ No newline at end of file diff --git a/custom_ui/custom_ui/doctype/customer_project_link/__init__.py b/custom_ui/custom_ui/doctype/customer_project_link/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/custom_ui/custom_ui/doctype/customer_project_link/customer_project_link.json b/custom_ui/custom_ui/doctype/customer_project_link/customer_project_link.json new file mode 100644 index 0000000..cebc97f --- /dev/null +++ b/custom_ui/custom_ui/doctype/customer_project_link/customer_project_link.json @@ -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": [] +} \ No newline at end of file diff --git a/custom_ui/custom_ui/doctype/customer_quotation_link/__init__.py b/custom_ui/custom_ui/doctype/customer_quotation_link/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/custom_ui/custom_ui/doctype/customer_quotation_link/customer_quotation_link.json b/custom_ui/custom_ui/doctype/customer_quotation_link/customer_quotation_link.json new file mode 100644 index 0000000..add929a --- /dev/null +++ b/custom_ui/custom_ui/doctype/customer_quotation_link/customer_quotation_link.json @@ -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": [] +} \ No newline at end of file diff --git a/custom_ui/custom_ui/doctype/customer_sales_order_link/__init__.py b/custom_ui/custom_ui/doctype/customer_sales_order_link/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/custom_ui/custom_ui/doctype/customer_sales_order_link/customer_sales_order_link.json b/custom_ui/custom_ui/doctype/customer_sales_order_link/customer_sales_order_link.json new file mode 100644 index 0000000..f1628fd --- /dev/null +++ b/custom_ui/custom_ui/doctype/customer_sales_order_link/customer_sales_order_link.json @@ -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": [] +} \ No newline at end of file diff --git a/custom_ui/custom_ui/doctype/customer_task_link/customer_task_link.json b/custom_ui/custom_ui/doctype/customer_task_link/customer_task_link.json index 37263a4..c2d5413 100644 --- a/custom_ui/custom_ui/doctype/customer_task_link/customer_task_link.json +++ b/custom_ui/custom_ui/doctype/customer_task_link/customer_task_link.json @@ -1,7 +1,8 @@ { "actions": [], "allow_rename": 1, - "creation": "2026-01-30 07:21:48.120856", + "creation": "2026-02-18 05:53:08.727283", + "custom": 1, "doctype": "DocType", "editable_grid": 1, "engine": "InnoDB", @@ -29,7 +30,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2026-01-30 07:52:52.271939", + "modified": "2026-02-18 05:53:08.766401", "modified_by": "Administrator", "module": "Custom UI", "name": "Customer Task Link", diff --git a/custom_ui/custom_ui/doctype/lead_address_link/__init__.py b/custom_ui/custom_ui/doctype/lead_address_link/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/custom_ui/custom_ui/doctype/lead_address_link/lead_address_link.json b/custom_ui/custom_ui/doctype/lead_address_link/lead_address_link.json new file mode 100644 index 0000000..a08a35a --- /dev/null +++ b/custom_ui/custom_ui/doctype/lead_address_link/lead_address_link.json @@ -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": [] +} \ No newline at end of file diff --git a/custom_ui/custom_ui/doctype/lead_companies_link/__init__.py b/custom_ui/custom_ui/doctype/lead_companies_link/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/custom_ui/custom_ui/doctype/lead_companies_link/lead_companies_link.json b/custom_ui/custom_ui/doctype/lead_companies_link/lead_companies_link.json new file mode 100644 index 0000000..4858867 --- /dev/null +++ b/custom_ui/custom_ui/doctype/lead_companies_link/lead_companies_link.json @@ -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": [] +} \ No newline at end of file diff --git a/custom_ui/custom_ui/doctype/lead_company_link/__init__.py b/custom_ui/custom_ui/doctype/lead_company_link/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/custom_ui/custom_ui/doctype/lead_company_link/lead_company_link.json b/custom_ui/custom_ui/doctype/lead_company_link/lead_company_link.json new file mode 100644 index 0000000..e8090ec --- /dev/null +++ b/custom_ui/custom_ui/doctype/lead_company_link/lead_company_link.json @@ -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": [] +} \ No newline at end of file diff --git a/custom_ui/custom_ui/doctype/lead_contact_link/__init__.py b/custom_ui/custom_ui/doctype/lead_contact_link/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/custom_ui/custom_ui/doctype/lead_contact_link/lead_contact_link.json b/custom_ui/custom_ui/doctype/lead_contact_link/lead_contact_link.json new file mode 100644 index 0000000..68cf0e8 --- /dev/null +++ b/custom_ui/custom_ui/doctype/lead_contact_link/lead_contact_link.json @@ -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": [] +} \ No newline at end of file diff --git a/custom_ui/custom_ui/doctype/lead_on_site_meeting_link/__init__.py b/custom_ui/custom_ui/doctype/lead_on_site_meeting_link/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/custom_ui/custom_ui/doctype/lead_on_site_meeting_link/lead_on_site_meeting_link.json b/custom_ui/custom_ui/doctype/lead_on_site_meeting_link/lead_on_site_meeting_link.json new file mode 100644 index 0000000..0dd6cbc --- /dev/null +++ b/custom_ui/custom_ui/doctype/lead_on_site_meeting_link/lead_on_site_meeting_link.json @@ -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": [] +} \ No newline at end of file diff --git a/custom_ui/custom_ui/doctype/lead_quotation_link/__init__.py b/custom_ui/custom_ui/doctype/lead_quotation_link/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/custom_ui/custom_ui/doctype/lead_quotation_link/lead_quotation_link.json b/custom_ui/custom_ui/doctype/lead_quotation_link/lead_quotation_link.json new file mode 100644 index 0000000..6a95a3e --- /dev/null +++ b/custom_ui/custom_ui/doctype/lead_quotation_link/lead_quotation_link.json @@ -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": [] +} \ No newline at end of file diff --git a/custom_ui/custom_ui/doctype/project_task_link/project_task_link.json b/custom_ui/custom_ui/doctype/project_task_link/project_task_link.json index 14a618a..5ae6e61 100644 --- a/custom_ui/custom_ui/doctype/project_task_link/project_task_link.json +++ b/custom_ui/custom_ui/doctype/project_task_link/project_task_link.json @@ -1,7 +1,8 @@ { "actions": [], "allow_rename": 1, - "creation": "2026-01-30 07:21:50.267662", + "creation": "2026-02-18 05:53:10.243616", + "custom": 1, "doctype": "DocType", "editable_grid": 1, "engine": "InnoDB", @@ -22,7 +23,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2026-01-30 07:51:59.777431", + "modified": "2026-02-18 05:53:10.281244", "modified_by": "Administrator", "module": "Custom UI", "name": "Project Task Link", diff --git a/custom_ui/custom_ui/doctype/skip_day/__init__.py b/custom_ui/custom_ui/doctype/skip_day/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/custom_ui/custom_ui/doctype/skip_day/skip_day.json b/custom_ui/custom_ui/doctype/skip_day/skip_day.json new file mode 100644 index 0000000..fbdc607 --- /dev/null +++ b/custom_ui/custom_ui/doctype/skip_day/skip_day.json @@ -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": [] +} \ No newline at end of file diff --git a/custom_ui/fixtures/bid_meeting_note_form.json b/custom_ui/fixtures/bid_meeting_note_form.json deleted file mode 100644 index a1cab74..0000000 --- a/custom_ui/fixtures/bid_meeting_note_form.json +++ /dev/null @@ -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" - } -] \ No newline at end of file diff --git a/custom_ui/fixtures/project_template.json b/custom_ui/fixtures/project_template.json deleted file mode 100644 index 5c095ce..0000000 --- a/custom_ui/fixtures/project_template.json +++ /dev/null @@ -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" - } - ] - } -] \ No newline at end of file diff --git a/frontend/src/api.js b/frontend/src/api.js index 82c2d06..6b3671c 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -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_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_CREATE_CLIENT_CONTACTS_ADDRESSES_METHOD = "custom_ui.api.db.clients.create_client_contacts_addresses"; // Employee methods 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"; @@ -188,6 +189,10 @@ class Api { 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 // ============================================================================ diff --git a/frontend/src/components/clientView/AddContactAddressModal.vue b/frontend/src/components/clientView/AddContactAddressModal.vue index 19ccc58..525b100 100644 --- a/frontend/src/components/clientView/AddContactAddressModal.vue +++ b/frontend/src/components/clientView/AddContactAddressModal.vue @@ -66,18 +66,23 @@ import Dialog from 'primevue/dialog'; import Button from 'primevue/button'; import ModalContactForm from './ModalContactForm.vue'; import ModalAddressForm from './ModalAddressForm.vue'; +import Api from '../../api'; +import { useCompanyStore } from '../../stores/company'; + +const companyStore = useCompanyStore(); const props = defineProps({ visible: Boolean, + clientName: { type: String, default: '' }, clientContacts: { type: Array, default: () => [] }, existingContacts: { type: Array, default: () => [] }, existingAddresses: { type: Array, default: () => [] }, - isSubmitting: { type: Boolean, default: false }, }); const emit = defineEmits(['update:visible', 'created']); const showContacts = ref(false); const showAddresses = ref(false); +const isSubmitting = ref(false); // Direct arrays instead of wrapping in formData objects const newContacts = ref([ @@ -136,16 +141,57 @@ function close() { emit('update:visible', false); } -function create() { - const payload = {}; - if (showContacts.value) { - payload.contacts = newContacts.value; +async function create() { + isSubmitting.value = true; + try { + 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 (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(); + } catch (error) { + console.error('Error creating contacts/addresses:', error); + } finally { + isSubmitting.value = false; } - if (showAddresses.value) { - payload.addresses = newAddresses.value; - } - emit('created', payload); - close(); } diff --git a/frontend/src/components/clientView/GeneralClientInfo.vue b/frontend/src/components/clientView/GeneralClientInfo.vue index e5efd04..b5cf1a6 100644 --- a/frontend/src/components/clientView/GeneralClientInfo.vue +++ b/frontend/src/components/clientView/GeneralClientInfo.vue @@ -98,9 +98,11 @@ @@ -126,6 +128,8 @@ const props = defineProps({ }, }); +const emit = defineEmits(['refresh']); + // Check if client is a 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'); +}; + diff --git a/frontend/src/components/pages/Client.vue b/frontend/src/components/pages/Client.vue index 5416acf..6d19837 100644 --- a/frontend/src/components/pages/Client.vue +++ b/frontend/src/components/pages/Client.vue @@ -29,6 +29,7 @@ @@ -461,6 +462,12 @@ const handleCustomerSelected = (clientData) => { // Handle customer selected from search client.value = { ...client.value, ...clientData }; }; + +const refreshClient = async () => { + if (clientName) { + await getClient(clientName); + } +};