diff --git a/custom_ui/api/db/clients.py b/custom_ui/api/db/clients.py index a3145fc..f28bac3 100644 --- a/custom_ui/api/db/clients.py +++ b/custom_ui/api/db/clients.py @@ -130,7 +130,8 @@ def get_client(client_name): 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. + doctypes_to_fetch_history = [] + # TODO: Continue getting other linked docs like jobs, invoices, etc. print("DEBUG: Final client data prepared:", clientData) return build_success_response(clientData) except frappe.ValidationError as ve: diff --git a/custom_ui/api/db/estimates.py b/custom_ui/api/db/estimates.py index 5bdad84..b961ee5 100644 --- a/custom_ui/api/db/estimates.py +++ b/custom_ui/api/db/estimates.py @@ -1,5 +1,6 @@ import frappe, json from frappe.utils.pdf import get_pdf +from custom_ui.api.db.general import get_doc_history from custom_ui.db_utils import process_query_conditions, build_datatable_dict, get_count_or_filters, build_success_response, build_error_response from werkzeug.wrappers import Response from custom_ui.api.db.clients import check_if_customer, convert_lead_to_customer @@ -95,6 +96,8 @@ def get_estimate(estimate_name): address_doc["contacts"] = contacts est_dict["address_details"] = address_doc + + est_dict["history"] = get_doc_history("Quotation", estimate_name) return build_success_response(est_dict) except Exception as e: @@ -293,8 +296,10 @@ def upsert_estimate(data): }) estimate.save() + estimate_dict = estimate.as_dict() + estimate_dict["history"] = get_doc_history("Quotation", estimate_name) print(f"DEBUG: Estimate updated: {estimate.name}") - return build_success_response(estimate.as_dict()) + return build_success_response(estimate_dict) # Otherwise, create new estimate else: @@ -329,6 +334,11 @@ def upsert_estimate(data): print(f"DEBUG: Error in upsert_estimate: {str(e)}") return build_error_response(str(e), 500) +def get_estimate_history(estimate_name): + """Get the history of changes for a specific estimate.""" + + return history + # @frappe.whitelist() # def get_estimate_counts(): # """Get specific counts of estimates based on their status.""" diff --git a/custom_ui/api/db/general.py b/custom_ui/api/db/general.py new file mode 100644 index 0000000..8e2524c --- /dev/null +++ b/custom_ui/api/db/general.py @@ -0,0 +1,59 @@ +import frappe +from custom_ui.db_utils import build_history_entries + +def get_doc_history(doctype, docname): + """Get the history of changes for a specific document.""" + # Fetch comments + comments = frappe.get_all( + "Comment", + filters={ + "reference_doctype": doctype, + "reference_name": docname + }, + fields=["*"], + order_by="creation desc" + ) + versions = frappe.get_all( + "Version", + filters={"docname": docname, "ref_doctype": doctype}, + fields=["*"], + order_by="creation desc" + ) + history_entries = build_history_entries(comments, versions) + print(f"DEBUG: Retrieved history for {doctype} {docname}: {history_entries}") + return history_entries + +def get_docs_history(doctypes_with_names): + """Get history for multiple documents.""" + all_history = {} + for doctype, docname in doctypes_with_names: + history = get_doc_history(doctype, docname) + all_history[f"{doctype}:{docname}"] = history + return all_history + +def search_any_field(doctype, text): + meta = frappe.get_meta(doctype) + like = f"%{text}%" + + conditions = [] + + # 1️⃣ Explicitly include `name` + conditions.append("`name` LIKE %s") + + # 2️⃣ Include searchable DocFields + for field in meta.fields: + if field.fieldtype in ("Data", "Small Text", "Text", "Link"): + conditions.append(f"`{field.fieldname}` LIKE %s") + + query = f""" + SELECT name + FROM `tab{doctype}` + WHERE {" OR ".join(conditions)} + LIMIT 20 + """ + + return frappe.db.sql( + query, + [like] * len(conditions), + as_dict=True + ) \ No newline at end of file diff --git a/custom_ui/commands.py b/custom_ui/commands.py index 8728158..2faee1d 100644 --- a/custom_ui/commands.py +++ b/custom_ui/commands.py @@ -3,7 +3,7 @@ import os import subprocess import frappe from custom_ui.utils import create_module -from custom_ui.db_utils import search_any_field +from custom_ui.api.db.general import search_any_field @click.command("update-data") @click.option("--site", default=None, help="Site to update data for") diff --git a/custom_ui/db_utils.py b/custom_ui/db_utils.py index 3b272cf..8098164 100644 --- a/custom_ui/db_utils.py +++ b/custom_ui/db_utils.py @@ -1,6 +1,5 @@ - -import frappe import json +from frappe.utils import strip_html def map_field_name(frontend_field): field_mapping = { @@ -196,30 +195,37 @@ def map_lead_update(client_data): client_data[lead_field] = client_data[client_field] return client_data - -def search_any_field(doctype, text): - meta = frappe.get_meta(doctype) - like = f"%{text}%" - - conditions = [] - - # 1️⃣ Explicitly include `name` - conditions.append("`name` LIKE %s") - - # 2️⃣ Include searchable DocFields - for field in meta.fields: - if field.fieldtype in ("Data", "Small Text", "Text", "Link"): - conditions.append(f"`{field.fieldname}` LIKE %s") - - query = f""" - SELECT name - FROM `tab{doctype}` - WHERE {" OR ".join(conditions)} - LIMIT 20 - """ - - return frappe.db.sql( - query, - [like] * len(conditions), - as_dict=True - ) \ No newline at end of file +def map_comment_to_history_entry(comment): + return { + "type": comment.get("comment_type", "Comment"), + "user": comment.get("owner"), + "timestamp": comment.get("creation"), + "message": strip_html(comment.get("content", "")) + } + +def map_version_data_to_history_entry(changed_data, creation, owner): + field, old, new = changed_data + return { + "type": "Field Change", + "timestamp": creation, + "user": owner, + "message": f"Changed '{field}' from '{old}' to '{new}'" + } + +def build_history_entries(comments, versions): + history = [] + for comment in comments: + history.append(map_comment_to_history_entry(comment)) + for version in versions: + data = json.loads(version.get("data", "[]")) + for changed_data in data.get("changed", []): + entry = map_version_data_to_history_entry( + changed_data, + version.get("creation"), + version.get("owner") + ) + if entry: + history.append(entry) + # Sort by timestamp descending + history.sort(key=lambda x: x["timestamp"], reverse=True) + return history diff --git a/frontend/src/components/common/DocHistory.vue b/frontend/src/components/common/DocHistory.vue new file mode 100644 index 0000000..2fc7280 --- /dev/null +++ b/frontend/src/components/common/DocHistory.vue @@ -0,0 +1,260 @@ + + + + + diff --git a/frontend/src/components/pages/Estimate.vue b/frontend/src/components/pages/Estimate.vue index b375326..2670fe2 100644 --- a/frontend/src/components/pages/Estimate.vue +++ b/frontend/src/components/pages/Estimate.vue @@ -76,7 +76,7 @@ :disabled="!isEditable" showButtons buttonLayout="horizontal" - @input="updateTotal" + @input="onQtyChange(item)" class="qty-input" /> Price: ${{ (item.standardRate || 0).toFixed(2) }} @@ -90,7 +90,7 @@ locale="en-US" :min="0" :disabled="!isEditable" - @input="updateTotal" + @input="updateDiscountFromAmount(item)" placeholder="$0.00" class="discount-input" /> @@ -101,7 +101,7 @@ :min="0" :max="100" :disabled="!isEditable" - @input="updateTotal" + @input="updateDiscountFromPercentage(item)" placeholder="0%" class="discount-input" /> @@ -123,7 +123,7 @@ /> - Total: ${{ ((item.qty || 0) * (item.standardRate || 0) - (item.discountType === 'percentage' ? ((item.qty || 0) * (item.standardRate || 0) * ((item.discountPercentage || 0) / 100)) : (item.discountAmount || 0))).toFixed(2) }} + Total: ${{ ((item.qty || 0) * (item.standardRate || 0) - (item.discountAmount || 0)).toFixed(2) }}