feat: CRM integration - customer intake, service invoice generation, COR print format\n\n- Added customer-intake page (search/create customers, create pallets)\n- Added whitelisted generate_service_invoice() API for native Sales Invoice creation\n- Added COR logo image asset\n- Removed stale fixtures/doctype.json (was causing bench migrate failures)
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
<div class="container" style="padding:20px;">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h3>Customer Intake</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<label>Search Customer</label>
|
||||
<input type="text" id="intake-search" class="form-control" placeholder="Type company name, number, or phone...">
|
||||
<div id="search-results" class="list-group" style="margin-top:8px; max-height:250px; overflow-y:auto;"></div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div id="no-match" style="display:none;">
|
||||
<button id="btn-add-new" class="btn btn-primary">Add New Customer</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<label>Customer Name</label>
|
||||
<input type="text" id="cust-name" class="form-control">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label>Customer Number</label>
|
||||
<input type="text" id="cust-number" class="form-control">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label>Phone</label>
|
||||
<input type="text" id="cust-phone" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" style="margin-top:10px;">
|
||||
<div class="col-md-6">
|
||||
<label>Address Line 1</label>
|
||||
<input type="text" id="cust-address" class="form-control">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label>City</label>
|
||||
<input type="text" id="cust-city" class="form-control">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label>State</label>
|
||||
<input type="text" id="cust-state" class="form-control">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label>Zip</label>
|
||||
<input type="text" id="cust-zip" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" style="margin-top:10px;">
|
||||
<div class="col-md-4">
|
||||
<label>Contact Persons</label>
|
||||
<input type="text" id="cust-contacts" class="form-control">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label>Email</label>
|
||||
<input type="text" id="cust-email" class="form-control">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label>Hours of Operation</label>
|
||||
<input type="text" id="cust-hours" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" style="margin-top:15px;">
|
||||
<div class="col-md-12">
|
||||
<button id="btn-save-cust" class="btn btn-success">Save Customer</button>
|
||||
<button id="btn-create-pallet" class="btn btn-warning" style="margin-left:10px;">Create Pallet</button>
|
||||
<span id="cust-status" style="margin-left:15px;"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,122 @@
|
||||
frappe.pages["customer-intake"].on_page_load = function(wrapper) {
|
||||
var page = frappe.ui.make_app_page({parent: wrapper, title: __("Customer Intake"), single_column: true});
|
||||
var content = frappe.render_template("customer-intake", {});
|
||||
$(page.body).append(content);
|
||||
|
||||
var currentCustomer = null;
|
||||
var searchTimer = null;
|
||||
|
||||
function clearForm() {
|
||||
currentCustomer = null;
|
||||
$("#cust-name, #cust-number, #cust-phone, #cust-address, #cust-city, #cust-state, #cust-zip, #cust-contacts, #cust-email, #cust-hours").val("");
|
||||
$("#search-results").empty();
|
||||
$("#no-match").hide();
|
||||
$("#cust-status").text("");
|
||||
}
|
||||
|
||||
function fillForm(c) {
|
||||
currentCustomer = c;
|
||||
$("#cust-name").val(c.customer_name || "");
|
||||
$("#cust-number").val(c.customer_number || "");
|
||||
$("#cust-phone").val(c.phone || c.address_phone || "");
|
||||
$("#cust-address").val(c.address_line1 || "");
|
||||
$("#cust-city").val(c.city || "");
|
||||
$("#cust-state").val(c.state || "");
|
||||
$("#cust-zip").val(c.pincode || "");
|
||||
$("#cust-contacts").val(c.contact_persons || "");
|
||||
$("#cust-email").val(c.email_id || "");
|
||||
$("#cust-hours").val(c.hours_of_operation || "");
|
||||
$("#cust-status").text("Selected: " + (c.customer_name || c.name));
|
||||
$("#no-match").hide();
|
||||
}
|
||||
|
||||
function doSearch() {
|
||||
var q = $("#intake-search").val().trim();
|
||||
if (q.length < 2) { $("#search-results").empty(); return; }
|
||||
frappe.call({
|
||||
method: "westech_r2.page.customer-intake.customer-intake.search_customers",
|
||||
args: {q: q},
|
||||
callback: function(r) {
|
||||
var list = $("#search-results").empty();
|
||||
if (r.message && r.message.length) {
|
||||
r.message.forEach(function(c) {
|
||||
var item = $('<div class="list-group-item" style="cursor:pointer;">')
|
||||
.html('<b>' + frappe.utils.escape_html(c.customer_name || c.name) + '</b> ' +
|
||||
(c.address_line1 ? '<br>' + frappe.utils.escape_html(c.address_line1) : '') +
|
||||
(c.city ? ', ' + c.city : '') +
|
||||
(c.phone ? ' <br><small>' + c.phone + '</small>' : ''));
|
||||
item.on("click", function() { fillForm(c); });
|
||||
list.append(item);
|
||||
});
|
||||
$("#no-match").hide();
|
||||
} else {
|
||||
$("#no-match").show();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$("#intake-search").on("input", function() {
|
||||
clearTimeout(searchTimer);
|
||||
searchTimer = setTimeout(doSearch, 300);
|
||||
});
|
||||
|
||||
$("#btn-add-new").click(function() {
|
||||
clearForm();
|
||||
$("#cust-name").val($("#intake-search").val());
|
||||
$("#cust-status").text("Adding new customer...");
|
||||
});
|
||||
|
||||
$("#btn-save-cust").click(function() {
|
||||
var data = {
|
||||
customer_name: $("#cust-name").val(),
|
||||
customer_number: $("#cust-number").val(),
|
||||
phone: $("#cust-phone").val(),
|
||||
address_line1: $("#cust-address").val(),
|
||||
city: $("#cust-city").val(),
|
||||
state: $("#cust-state").val(),
|
||||
pincode: $("#cust-zip").val(),
|
||||
contact_persons: $("#cust-contacts").val(),
|
||||
email_id: $("#cust-email").val(),
|
||||
hours_of_operation: $("#cust-hours").val()
|
||||
};
|
||||
if (currentCustomer && currentCustomer.name) {
|
||||
data.name = currentCustomer.name;
|
||||
}
|
||||
frappe.call({
|
||||
method: "westech_r2.page.customer-intake.customer-intake.create_customer_from_intake",
|
||||
args: {data: JSON.stringify(data)},
|
||||
callback: function(r) {
|
||||
if (r.message && r.message.status === "ok") {
|
||||
currentCustomer = {name: r.message.customer, customer_name: data.customer_name};
|
||||
$("#cust-status").text("Saved: " + r.message.customer);
|
||||
frappe.show_alert("Customer saved", 3);
|
||||
} else {
|
||||
frappe.show_alert("Error saving customer", 5);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$("#btn-create-pallet").click(function() {
|
||||
if (!currentCustomer || !currentCustomer.name) {
|
||||
frappe.msgprint("Please select or save a customer first.");
|
||||
return;
|
||||
}
|
||||
frappe.call({
|
||||
method: "westech_r2.page.customer-intake.customer-intake.create_pallet",
|
||||
args: {
|
||||
data: JSON.stringify({
|
||||
customer: currentCustomer.name,
|
||||
customer_number: $("#cust-number").val()
|
||||
})
|
||||
},
|
||||
callback: function(r) {
|
||||
if (r.message && r.message.status === "ok") {
|
||||
frappe.msgprint("Pallet created: " + r.message.pallet);
|
||||
$("#cust-status").text("Pallet: " + r.message.pallet);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
{"creation":"2026-05-20 15:00:00.000000","docstatus":0,"doctype":"Page","idx":0,"module":"westech_r2","name":"customer-intake","page_name":"customer-intake","roles":[],"script":null,"standard":"Yes","style":null,"system_page":0,"title":"Customer Intake"}
|
||||
@@ -0,0 +1,82 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
|
||||
@frappe.whitelist()
|
||||
def search_customers(q=""):
|
||||
if not q or len(q) < 2:
|
||||
return []
|
||||
q = q.strip().lower()
|
||||
customers = frappe.db.sql("""
|
||||
SELECT c.name, c.customer_name, c.customer_number, c.phone, c.email_id,
|
||||
a.address_line1, a.city, a.state, a.pincode
|
||||
FROM tabCustomer c
|
||||
LEFT JOIN tabDynamic Link dl ON dl.link_doctype = 'Customer' AND dl.link_name = c.name AND dl.parenttype = 'Address'
|
||||
LEFT JOIN tabAddress a ON a.name = dl.parent
|
||||
WHERE LOWER(c.customer_name) LIKE %s OR LOWER(c.customer_number) LIKE %s OR LOWER(c.phone) LIKE %s
|
||||
ORDER BY c.customer_name
|
||||
LIMIT 20
|
||||
""", ("%" + q + "%", "%" + q + "%", "%" + q + "%"), as_dict=True)
|
||||
return customers
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_customer(name):
|
||||
if not name:
|
||||
return {}
|
||||
cust = frappe.get_doc("Customer", name)
|
||||
result = cust.as_dict()
|
||||
addr = frappe.db.sql("""
|
||||
SELECT a.address_line1, a.city, a.state, a.pincode, a.phone
|
||||
FROM tabAddress a
|
||||
JOIN tabDynamic Link dl ON dl.parent = a.name AND dl.link_doctype = 'Customer' AND dl.link_name = %s
|
||||
LIMIT 1
|
||||
""", (name,), as_dict=True)
|
||||
if addr:
|
||||
result.update({
|
||||
"address_line1": addr[0].address_line1,
|
||||
"city": addr[0].city,
|
||||
"state": addr[0].state,
|
||||
"pincode": addr[0].pincode,
|
||||
"address_phone": addr[0].phone
|
||||
})
|
||||
return result
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_customer_from_intake(data):
|
||||
data = frappe.parse_json(data)
|
||||
if not data.get("customer_name"):
|
||||
frappe.throw(_("Customer name required"))
|
||||
customer = frappe.new_doc("Customer")
|
||||
customer.customer_name = data.get("customer_name")
|
||||
customer.customer_group = data.get("customer_group", "IT Recycling")
|
||||
customer.customer_type = "Company"
|
||||
customer.customer_number = data.get("customer_number")
|
||||
customer.phone = data.get("phone")
|
||||
customer.email_id = data.get("email_id")
|
||||
customer.legacy_notes = data.get("legacy_notes")
|
||||
customer.hours_of_operation = data.get("hours_of_operation")
|
||||
customer.contact_persons = data.get("contact_persons")
|
||||
customer.save()
|
||||
if data.get("address_line1") or data.get("city"):
|
||||
addr = frappe.new_doc("Address")
|
||||
addr.address_title = customer.customer_name
|
||||
addr.address_type = "Billing"
|
||||
addr.address_line1 = data.get("address_line1", "Unknown")
|
||||
addr.city = data.get("city", "Unknown")
|
||||
addr.state = data.get("state", "")
|
||||
addr.pincode = data.get("pincode", "")
|
||||
addr.country = "United States"
|
||||
addr.append("links", {"link_doctype": "Customer", "link_name": customer.name})
|
||||
addr.save()
|
||||
return {"status": "ok", "customer": customer.name}
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_pallet(data):
|
||||
data = frappe.parse_json(data)
|
||||
if not data.get("customer"):
|
||||
frappe.throw(_("Customer required"))
|
||||
pallet = frappe.new_doc("Pallet")
|
||||
pallet.customer = data.get("customer")
|
||||
pallet.customer_number = data.get("customer_number")
|
||||
pallet.status = "Open"
|
||||
pallet.save()
|
||||
return {"status": "ok", "pallet": pallet.name}
|
||||
Reference in New Issue
Block a user