Compare commits

...

6 commits

4 changed files with 212 additions and 192 deletions

View file

@ -1,5 +1,5 @@
import frappe, json import frappe, json
from custom_ui.db_utils import build_error_response, process_query_conditions, build_datatable_dict, get_count_or_filters, build_success_response from custom_ui.db_utils import build_error_response, process_query_conditions, build_datatable_dict, get_count_or_filters, build_success_response, map_lead_client
# =============================================================================== # ===============================================================================
# CLIENT MANAGEMENT API METHODS # CLIENT MANAGEMENT API METHODS
@ -101,49 +101,45 @@ def get_client(client_name):
print("DEBUG: Client not found as Customer. Checking Lead.") print("DEBUG: Client not found as Customer. Checking Lead.")
lead_name = frappe.db.get_all("Lead", pluck="name", filters={"lead_name": client_name})[0] lead_name = frappe.db.get_all("Lead", pluck="name", filters={"lead_name": client_name})[0]
customer = frappe.get_doc("Lead", lead_name) customer = frappe.get_doc("Lead", lead_name)
if not customer:
return build_error_response(f"Client '{client_name}' not found as Customer or Lead.", 404)
print("DEBUG: Retrieved customer/lead document:", customer.as_dict())
clientData = {**clientData, **customer.as_dict()} clientData = {**clientData, **customer.as_dict()}
if customer.doctype == "Lead":
clientData.update(map_lead_client(clientData))
links = []
if customer.doctype == "Customer": if customer.doctype == "Customer":
for contact_link in customer.custom_add_contacts: links = (
contact_doc = frappe.get_doc("Contact", contact_link.contact) [{"link_doctype": "Contact", "link_name": row.contact}
clientData["contacts"].append(contact_doc.as_dict()) for row in customer.get("custom_add_contacts", [])]
else: +
contact_names = frappe.db.get_all("Contact", pluck="name", filters={"links": ["like", f'%{{"link_doctype": "Lead", "link_name": client_name}}%']}) [{"link_doctype": "Address", "link_name": row.address}
for contact_name in contact_names: for row in customer.get("custom_select_address", [])]
contact_doc = frappe.get_doc("Contact", contact_name) )
clientData["contacts"].append(contact_doc.as_dict())
if customer.doctype == "Customer":
for address_link in customer.custom_select_address:
address_doc = frappe.get_doc("Address", address_link.address_name)
# # addressData = {"jobs": [], "contacts": []}
# addressData = {**addressData, **address_doc.as_dict()}
# addressData["estimates"] = frappe.db.get_all("Quotation", fields=["*"], filters={"custom_installation_address": address_doc.address_title})
# addressData["onsite_meetings"] = frappe.db.get_all("On-Site Meeting", fields=["*"], filters={"address": address_doc.address_title})
# jobs = frappe.db.get_all("Project", fields=["*"], or_filters=[
# ["custom_installation_address", "=", address.address_title],
# ["custom_address", "=", address.address_title]
# ])
# for job in jobs if jobs else []:
# jobData = {}
# jobData = {**jobData, **job}
# jobData["sales_invoices"] = frappe.db.get_all("Sales Invoice", fields=["*"], filters={"project": job.name})
# jobData["payment_entries"] = frappe.db.get_all(
# "Payment Entry",
# fields=["*"],
# filters={"party_type": "Customer"},
# or_filters=[
# ["party", "=", client_name],
# ["party_name", "=", client_name]
# ])
# jobData["sales_orders"] = frappe.db.get_all("Sales Order", fields=["*"], filters={"project": job.name})
# jobData["tasks"] = frappe.db.get_all("Task", fields=["*"], filters={"project": job.name})
# addressData["jobs"].append(jobData)
clientData["addresses"].append(address_doc.as_dict())
else: else:
address_names = frappe.db.get_all("Address", pluck="name", filters={"links": ["like", f'%{{"link_doctype": "Lead", "link_name": client_name}}%']}) links = frappe.get_all(
for address_name in address_names: "Dynamic Link",
address_doc = frappe.get_doc("Address", address_name) filters={
clientData["addresses"].append(address_doc.as_dict()) "link_doctype": "Lead",
"link_name": lead_name,
"parenttype": ["in", ["Address", "Contact"]],
},
fields=[
"parenttype as link_doctype",
"parent as link_name",
]
)
print("DEBUG: Retrieved links from lead:", links)
for link in links:
print("DEBUG: Processing link:", link)
linked_doc = frappe.get_doc(link["link_doctype"], link["link_name"])
if link["link_doctype"] == "Contact":
clientData["contacts"].append(linked_doc.as_dict())
elif link["link_doctype"] == "Address":
clientData["addresses"].append(linked_doc.as_dict())
# TODO: Continue getting other linked docs like jobs, invoices, etc.
return build_success_response(clientData) return build_success_response(clientData)
except frappe.ValidationError as ve: except frappe.ValidationError as ve:
return build_error_response(str(ve), 400) return build_error_response(str(ve), 400)
@ -348,6 +344,11 @@ def upsert_client(data):
"phone": contact_doc.phone, "phone": contact_doc.phone,
"role": contact_doc.role "role": contact_doc.role
}) })
new_client_doc.append("links", {
"link_doctype": "Contact",
"link_name": contact_doc.name
}
)
new_client_doc.save(ignore_permissions=True) new_client_doc.save(ignore_permissions=True)
# Address -> Customer/Lead # Address -> Customer/Lead
@ -356,6 +357,8 @@ def upsert_client(data):
"link_doctype": new_client_doc.doctype, "link_doctype": new_client_doc.doctype,
"link_name": new_client_doc.name "link_name": new_client_doc.name
}) })
if new_client_doc.doctype == "Lead":
address_doc.lead_name = new_client_doc.lead_name
# Address -> Contact # Address -> Contact

View file

@ -50,7 +50,23 @@ def process_filters(filters):
processed_filters = address_filters processed_filters = address_filters
continue # Skip the rest of the loop for address field continue # Skip the rest of the loop for address field
customer_name_fields = ["custom_customer_to_bill", "lead_name"] if field_name == "customer_name" else []
if customer_name_fields:
customer_name_filters = []
for cust_field in customer_name_fields:
if match_mode in ("contains", "contains"):
customer_name_filters.append([cust_field, "like", f"%{filter_obj['value']}%"])
elif match_mode in ("startswith", "starts_with"):
customer_name_filters.append([cust_field, "like", f"{filter_obj['value']}%"])
elif match_mode in ("endswith", "ends_with"):
customer_name_filters.append([cust_field, "like", f"%{filter_obj['value']}"])
elif match_mode in ("equals", "equals"):
customer_name_filters.append([cust_field, "=", filter_obj["value"]])
else:
customer_name_filters.append([cust_field, "like", f"%{filter_obj['value']}%"])
processed_filters = customer_name_filters
continue # Skip the rest of the loop for customer_name field
if match_mode in ("contains", "contains"): if match_mode in ("contains", "contains"):
processed_filters[mapped_field_name] = ["like", f"%{filter_obj['value']}%"] processed_filters[mapped_field_name] = ["like", f"%{filter_obj['value']}%"]
elif match_mode in ("startswith", "starts_with"): elif match_mode in ("startswith", "starts_with"):
@ -143,3 +159,30 @@ def build_full_address(doc):
return f"{first}, {second}" return f"{first}, {second}"
return first or second or "" return first or second or ""
def map_lead_client(client_data):
mappings = {
"lead_name": "customer_name",
"customer_type": "customer_type",
"territory": "territory",
"company_name": "company"
}
for lead_field, client_field in mappings.items():
if lead_field in client_data:
print(f"DEBUG: Mapping field {lead_field} to {client_field} with value {client_data[lead_field]}")
client_data[client_field] = client_data[lead_field]
client_data["customer_group"] = "" # Leads don't have customer groups
return client_data
def map_lead_update(client_data):
mappings = {
"customer_name": "lead_name",
"customer_type": "customer_type",
"territory": "territory",
"company": "company_name"
}
for client_field, lead_field in mappings.items():
if client_field in client_data:
print(f"DEBUG: Mapping field {client_field} to {lead_field} with value {client_data[client_field]}")
client_data[lead_field] = client_data[client_field]
return client_data

View file

@ -132,6 +132,12 @@ def add_custom_fields():
options="Not Started\nIn Progress\nCompleted", options="Not Started\nIn Progress\nCompleted",
default="Not Started", default="Not Started",
insert_after="job_status" insert_after="job_status"
),
dict(
fieldname="lead_name",
label="Lead Name",
fieldtype="Data",
insert_after="custom_customer_to_bill"
) )
], ],
"Contact": [ "Contact": [
@ -268,11 +274,12 @@ def update_address_fields():
["custom_onsite_meeting_scheduled", "onsite_meeting_scheduled"], ["custom_onsite_meeting_scheduled", "onsite_meeting_scheduled"],
["custom_estimate_sent_status", "estimate_sent_status"], ["custom_estimate_sent_status", "estimate_sent_status"],
["custom_job_status", "job_status"], ["custom_job_status", "job_status"],
["custom_payment_received_status", "payment_received_status"] ["custom_payment_received_status", "payment_received_status",],
["custom_lead_name", "lead_name"]
], ],
"Contact": [ "Contact": [
["custom_role", "role"], ["custom_role", "role"],
["custom_email", "email"] ["custom_email", "email"],
], ],
"On-Site Meeting": [ "On-Site Meeting": [
["custom_notes", "notes"], ["custom_notes", "notes"],
@ -285,6 +292,9 @@ def update_address_fields():
], ],
"Sales Order": [ "Sales Order": [
["custom_requires_half_payment", "requires_half_payment"] ["custom_requires_half_payment", "requires_half_payment"]
],
"Lead": [
["custom_customer_type", "customer_type"]
] ]
} }

View file

@ -1,163 +1,127 @@
<template> <template>
<div class="overview-container"> <div class="overview-container">
<!-- Form Mode (new=true or edit mode) --> <template v-if="!editMode">
<template v-if="isNew || editMode"> <Button
<ClientInformationForm @click="toggleEditMode"
ref="clientInfoRef" icon="pi pi-pencil"
v-model:form-data="formData" label="Edit Information"
:is-submitting="isSubmitting" size="small"
:is-edit-mode="editMode" severity="secondary"
@new-client-toggle="handleNewClientToggle"
@customer-selected="handleCustomerSelected"
/>
<ContactInformationForm
ref="contactInfoRef"
v-model:form-data="formData"
:is-submitting="isSubmitting"
:is-edit-mode="editMode"
:is-new-client-locked="isNewClientMode"
:available-contacts="availableContacts"
@new-contact-toggle="handleNewContactToggle"
/>
<AddressInformationForm
v-model:form-data="formData"
:is-submitting="isSubmitting"
:is-edit-mode="editMode"
/> />
</template> </template>
<div class="status-cards">
<template v-if="isNew || editMode">
<template v-if="isNew || editMode">
<ClientInformationForm
ref="clientInfoRef"
:form-data="formData"
@update:form-data="formData = $event"
:is-submitting="isSubmitting"
:is-edit-mode="editMode"
@new-client-toggle="handleNewClientToggle"
@customer-selected="handleCustomerSelected"
/>
<!-- Display Mode (existing client view) --> <ContactInformationForm
<template v-else> ref="contactInfoRef"
<!-- Client Basic Info Card --> :form-data="formData"
<div class="info-card"> @update:form-data="formData = $event"
<div class="card-header"> :is-submitting="isSubmitting"
<h3>Client Information</h3> :is-edit-mode="editMode"
<Button :is-new-client-locked="isNewClientMode"
@click="toggleEditMode" :available-contacts="availableContacts"
icon="pi pi-pencil" @new-contact-toggle="handleNewContactToggle"
label="Edit" />
size="small"
severity="secondary"
/>
</div>
<div class="info-grid">
<div class="info-item">
<label>Customer Name:</label>
<span>{{ clientData?.customerName || "N/A" }}</span>
</div>
<div class="info-item">
<label>Customer Type:</label>
<span>{{ clientData?.customerType || "N/A" }}</span>
</div>
<div class="info-item">
<label>Customer Group:</label>
<span>{{ clientData?.customerGroup || "N/A" }}</span>
</div>
<div class="info-item">
<label>Territory:</label>
<span>{{ clientData?.territory || "N/A" }}</span>
</div>
</div>
</div>
<!-- Address Info Card --> <AddressInformationForm
<div class="info-card" v-if="selectedAddressData"> :form-data="formData"
<h3>Address Information</h3> @update:form-data="formData = $event"
<div class="info-grid"> :is-submitting="isSubmitting"
<div class="info-item full-width"> :is-edit-mode="editMode"
<label>Address Title:</label> />
<span>{{ selectedAddressData.addressTitle || "N/A" }}</span> </template>
</div>
<div class="info-item full-width">
<label>Full Address:</label>
<span>{{ fullAddress }}</span>
</div>
<div class="info-item">
<label>City:</label>
<span>{{ selectedAddressData.city || "N/A" }}</span>
</div>
<div class="info-item">
<label>State:</label>
<span>{{ selectedAddressData.state || "N/A" }}</span>
</div>
<div class="info-item">
<label>Zip Code:</label>
<span>{{ selectedAddressData.pincode || "N/A" }}</span>
</div>
</div>
</div>
<!-- Contact Info Card --> <!-- Display Mode (existing client view) -->
<div class="info-card" v-if="selectedAddressData"> <template v-else>
<h3>Contact Information</h3> <!-- Address Info Card -->
<template v-if="contactsForAddress.length > 0"> <div class="info-card" v-if="selectedAddressData">
<div v-if="contactsForAddress.length > 1" class="contact-selector"> <h3>General Information</h3>
<Dropdown
v-model="selectedContactIndex"
:options="contactOptions"
option-label="label"
option-value="value"
placeholder="Select Contact"
class="w-full"
/>
</div>
<div class="info-grid"> <div class="info-grid">
<div class="info-item"> <div class="info-item full-width">
<label>Contact Name:</label> <label>Address Title:</label>
<span>{{ contactFullName }}</span> <span>{{ selectedAddressData.addressTitle || "N/A" }}</span>
</div> </div>
<div class="info-item"> <div class="info-item full-width">
<label>Phone:</label> <label>Full Address:</label>
<span>{{ primaryContactPhone }}</span> <span>{{ fullAddress }}</span>
</div> </div>
<div class="info-item"> <div class="info-item full-width">
<label>Email:</label> <label>City:</label>
<span>{{ primaryContactEmail }}</span> <span>{{ selectedAddressData.city || "N/A" }}</span>
</div>
<div class="info-item full-width">
<label>State:</label>
<span>{{ selectedAddressData.state || "N/A" }}</span>
</div>
<div class="info-item full-width">
<label>Zip Code:</label>
<span>{{ selectedAddressData.pincode || "N/A" }}</span>
</div> </div>
</div> </div>
</template> </div>
<template v-else>
<p>No contacts available for this address.</p>
</template>
</div>
</template>
<!-- Status Cards (only for existing clients) --> <!-- Client Basic Info Card -->
<div class="status-cards" v-if="!isNew && !editMode && selectedAddressData"> <div class="info-card">
<div class="status-card"> <h3>Contact Information</h3>
<h4>On-Site Meeting</h4> <template v-if="contactsForAddress.length > 0">
<Button <div class="info-grid">
:label="selectedAddressData.customOnsiteMeetingScheduled || 'Not Started'" <div class="info-item">
:severity="getStatusSeverity(selectedAddressData.customOnsiteMeetingScheduled)" <label>Customer Name:</label>
@click="handleStatusClick('onsite')" <span>{{ clientData?.customerName || "N/A" }}</span>
/> </div>
</div> <div class="info-item">
<div class="status-card"> <label>Customer Type:</label>
<h4>Estimate Sent</h4> <span>{{ clientData?.customerType || "N/A" }}</span>
<Button </div>
:label="selectedAddressData.customEstimateSentStatus || 'Not Started'" <div v-if="contactsForAddress.length > 1" class="contact-selector">
:severity="getStatusSeverity(selectedAddressData.customEstimateSentStatus)" <Dropdown
@click="handleStatusClick('estimate')" v-model="selectedContactIndex"
/> :options="contactOptions"
</div> option-label="label"
<div class="status-card"> option-value="value"
<h4>Job Status</h4> placeholder="Select Contact"
<Button class="w-full"
:label="selectedAddressData.customJobStatus || 'Not Started'" />
:severity="getStatusSeverity(selectedAddressData.customJobStatus)" </div>
@click="handleStatusClick('job')" <div class="info-grid">
/> <div class="info-item">
</div> <label>Contact Name:</label>
<div class="status-card"> <span>{{ contactFullName }}</span>
<h4>Payment Received</h4> </div>
<Button <div class="info-item">
:label="selectedAddressData.customPaymentReceivedStatus || 'Not Started'" <label>Phone:</label>
:severity="getStatusSeverity(selectedAddressData.customPaymentReceivedStatus)" <span>{{ primaryContactPhone }}</span>
@click="handleStatusClick('payment')" </div>
/> <div class="info-item">
</div> <label>Email:</label>
<span>{{ primaryContactEmail }}</span>
</div>
</div>
<div class="info-item">
<label>Customer Group:</label>
<span>{{ clientData?.customerGroup || "N/A" }}</span>
</div>
<div class="info-item">
<label>Territory:</label>
<span>{{ clientData?.territory || "N/A" }}</span>
</div>
</div>
</template>
<template v-else>
<p>No contacts available for this address.</p>
</template>
</div>
</template>
</div> </div>
<!-- Form Actions --> <!-- Form Actions -->
@ -684,7 +648,7 @@ const handleCancel = () => {
.info-item { .info-item {
display: flex; display: flex;
flex-direction: column; flex-direction: row;
gap: 0.5rem; gap: 0.5rem;
} }