From 69c96d189827efdce1995026e40cdabb65e0e89a Mon Sep 17 00:00:00 2001 From: rocketdebris Date: Tue, 2 Dec 2025 13:18:43 -0500 Subject: [PATCH] Added an Invoice List Page. --- custom_ui/api/db/invoices.py | 104 ++++++++++++ frontend/src/api.js | 26 +++ frontend/src/components/pages/Invoices.vue | 179 +++++++++++++++++++++ frontend/src/router.js | 2 + 4 files changed, 311 insertions(+) create mode 100644 custom_ui/api/db/invoices.py create mode 100644 frontend/src/components/pages/Invoices.vue diff --git a/custom_ui/api/db/invoices.py b/custom_ui/api/db/invoices.py new file mode 100644 index 0000000..a4fe47d --- /dev/null +++ b/custom_ui/api/db/invoices.py @@ -0,0 +1,104 @@ +import frappe, json +from custom_ui.db_utils import process_query_conditions, build_datatable_dict, get_count_or_filters, build_success_response, build_error_response + +# =============================================================================== +# ESTIMATES & INVOICES API METHODS +# =============================================================================== + + +@frappe.whitelist() +def get_invoice_table_data(filters={}, sortings=[], page=1, page_size=10): + """Get paginated invoice table data with filtering and sorting support.""" + print("DEBUG: Raw invoice options received:", filters, sortings, page, page_size) + + processed_filters, processed_sortings, is_or, page, page_size = process_query_conditions(filters, sortings, page, page_size) + + if is_or: + count = frappe.db.sql(*get_count_or_filters("Sales Invoice", processed_filters))[0][0] + else: + count = frappe.db.count("Sales Invoice", filters=processed_filters) + + print(f"DEBUG: Number of invoice returned: {count}") + + invoices = frappe.db.get_all( + "Sales Invoice", + fields=["*"], + filters=processed_filters if not is_or else None, + or_filters=processed_filters if is_or else None, + limit=page_size, + start=(page - 1) * page_size, + order_by=processed_sortings + ) + + tableRows = [] + for invoice in invoices: + tableRow = {} + tableRow["id"] = invoice["name"] + tableRow["address"] = invoice.get("custom_installation_address", "") + tableRow["customer"] = invoice.get("customer", "") + tableRow["grand_total"] = invoice.get("grand_total", "") + tableRow["status"] = invoice.get("status", "") + tableRow["items"] = invoice.get("items", "") + tableRows.append(tableRow) + + table_data_dict = build_datatable_dict(data=tableRows, count=count, page=page, page_size=page_size) + return build_success_response(table_data_dict) + + +@frappe.whitelist() +def get_invoice(invoice_name): + """Get detailed information for a specific invoice.""" + try: + invoice = frappe.get_doc("Sales Invoice", invoice_name) + return build_success_response(invoice.as_dict()) + except Exception as e: + return build_error_response(str(e), 500) + + +@frappe.whitelist() +def get_invoice_items(): + items = frappe.db.get_all("Sales Invoice Item", fields=["*"]) + return build_success_response(items) + + +@frappe.whitelist() +def get_invoice_from_address(full_address): + invoice = frappe.db.sql(""" + SELECT i.name, i.custom_installation_address + FROM `tabSalesInvoice` i + JOIN `tabAddress` a + ON i.custom_installation_address = a.name + WHERE a.full_address =%s + """, (full_address,), as_dict=True) + if invoice: + return build_success_response(invoice) + else: + return build_error_response("No invoice found for the given address.", 404) + + +@frappe.whitelist() +def upsert_invoice(data): + """Create or update an invoice.""" + print("DOIFJSEOFJISLFK") + try: + data = json.loads(data) if isinstance(data, str) else data + print("DEBUG: Retrieved address name:", data.get("address_name")) + new_invoice = frappe.get_doc({ + "doctype": "Sales Invoice", + "custom_installation_address": data.get("address_name"), + "contact_email": data.get("contact_email"), + "party_name": data.get("contact_name"), + "customer_name": data.get("customer_name"), + }) + for item in data.get("items", []): + item = json.loads(item) if isinstance(item, str) else item + new_invoice.append("items", { + "item_code": item.get("item_code"), + "qty": item.get("qty"), + }) + new_invoice.insert() + print("DEBUG: New invoice created with name:", new_invoice.name) + return build_success_response(new_invoice.as_dict()) + except Exception as e: + return build_error_response(str(e), 500) + diff --git a/frontend/src/api.js b/frontend/src/api.js index cc25e51..84b138f 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -11,6 +11,7 @@ const FRAPPE_GET_ESTIMATES_METHOD = "custom_ui.api.db.estimates.get_estimate_tab const FRAPPE_GET_JOBS_METHOD = "custom_ui.api.db.get_jobs"; const FRAPPE_UPSERT_JOB_METHOD = "custom_ui.api.db.jobs.upsert_job"; // Invoice methods +const FRAPPE_GET_INVOICES_METHOD = "custom_ui.api.db.invoices.get_invoice_table_data"; const FRAPPE_UPSERT_INVOICE_METHOD = "custom_ui.api.db.invoices.upsert_invoice"; // Warranty methods const FRAPPE_GET_WARRANTY_CLAIMS_METHOD = "custom_ui.api.db.warranties.get_warranty_claims"; @@ -239,6 +240,31 @@ class Api { return result; } + static async getPaginatedInvoiceDetails(paginationParams = {}, filters = {}, sorting = null) { + const { page = 0, pageSize = 10, sortField = null, sortOrder = null } = paginationParams; + + // Use sorting from the dedicated sorting parameter first, then fall back to pagination params + const actualSortField = sorting?.field || sortField; + const actualSortOrder = sorting?.order || sortOrder; + + const options = { + page: page + 1, // Backend expects 1-based pages + page_size: pageSize, + filters, + sorting: + actualSortField && actualSortOrder + ? `${actualSortField} ${actualSortOrder === -1 ? "desc" : "asc"}` + : null, + for_table: true, + }; + + console.log("DEBUG: API - Sending invoice options to backend:", options); + + const result = await this.request(FRAPPE_GET_INVOICES_METHOD, { options }); + return result; + + } + /** * Get paginated job data with filtering and sorting * @param {Object} paginationParams - Pagination parameters from store diff --git a/frontend/src/components/pages/Invoices.vue b/frontend/src/components/pages/Invoices.vue new file mode 100644 index 0000000..ab5a75f --- /dev/null +++ b/frontend/src/components/pages/Invoices.vue @@ -0,0 +1,179 @@ + + + diff --git a/frontend/src/router.js b/frontend/src/router.js index edbdb44..5c31b2f 100644 --- a/frontend/src/router.js +++ b/frontend/src/router.js @@ -3,6 +3,7 @@ import { createRouter, createWebHashHistory } from "vue-router"; import Calendar from "./components/pages/Calendar.vue"; import Clients from "./components/pages/Clients.vue"; import Jobs from "./components/pages/Jobs.vue"; +import Invoices from "./components/pages/Invoices.vue"; import Estimates from "./components/pages/Estimates.vue"; import Create from "./components/pages/Create.vue"; import Routes from "./components/pages/Routes.vue"; @@ -25,6 +26,7 @@ const routes = [ { path: "/client", component: Client }, { path: "/schedule-onsite", component: ScheduleOnSite }, { path: "/jobs", component: Jobs }, + { path: "/invoices", component: Invoices }, { path: "/estimates", component: Estimates }, { path: "/estimate", component: Estimate }, { path: "/routes", component: Routes },