add project template into estimate creation to help project generation flow

This commit is contained in:
Casey 2026-01-07 16:12:31 -06:00
parent 8f90ef09fb
commit 54ae6d14f8
8 changed files with 118 additions and 66 deletions

View File

@ -429,6 +429,7 @@ def upsert_estimate(data):
"customer_address": data.get("address_name"),
"contact_person": data.get("contact_name"),
"letter_head": data.get("company"),
"custom_project_template": data.get("project_template", None)
})
for item in data.get("items", []):
item = json.loads(item) if isinstance(item, str) else item

View File

@ -5,6 +5,17 @@ from custom_ui.db_utils import process_query_conditions, build_datatable_dict, g
# JOB MANAGEMENT API METHODS
# ===============================================================================
@frappe.whitelist()
def get_job_templates(company=None):
"""Get list of job (project) templates."""
filters = {}
if company:
filters["company"] = company
try:
templates = frappe.get_all("Project Template", fields=["*"], filters=filters)
return build_success_response(templates)
except Exception as e:
return build_error_response(str(e), 500)
@frappe.whitelist()
def create_job_from_sales_order(sales_order_name):

View File

@ -1,59 +1 @@
[
{
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"collapsible_depends_on": null,
"columns": 0,
"default": null,
"depends_on": null,
"description": null,
"docstatus": 0,
"doctype": "Custom Field",
"dt": "Lead",
"fetch_from": null,
"fetch_if_empty": 0,
"fieldname": "custom_customer_name",
"fieldtype": "Data",
"hidden": 0,
"hide_border": 0,
"hide_days": 0,
"hide_seconds": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"insert_after": "last_name",
"is_system_generated": 0,
"is_virtual": 0,
"label": "Customer Name",
"length": 0,
"link_filters": null,
"mandatory_depends_on": null,
"modified": "2026-01-07 04:41:50.654606",
"module": null,
"name": "Lead-custom_customer_name",
"no_copy": 0,
"non_negative": 0,
"options": null,
"permlevel": 0,
"placeholder": null,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": null,
"read_only": 0,
"read_only_depends_on": null,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"show_dashboard": 0,
"sort_options": 0,
"translatable": 1,
"unique": 0,
"width": null
}
]
[]

View File

@ -498,8 +498,8 @@
"make_attachments_public": 0,
"max_attachments": 0,
"menu_index": null,
"migration_hash": null,
"modified": "2026-01-02 11:26:31.164108",
"migration_hash": "2521636464aadbebbd70dfbf13252950",
"modified": "2026-01-07 11:10:03.317996",
"module": "Selling",
"name": "Quotation Template",
"naming_rule": "",
@ -996,8 +996,8 @@
"make_attachments_public": 0,
"max_attachments": 0,
"menu_index": null,
"migration_hash": null,
"modified": "2025-12-23 02:00:30.908719",
"migration_hash": "2521636464aadbebbd70dfbf13252950",
"modified": "2026-01-07 11:10:03.406758",
"module": "Selling",
"name": "Quotation Template Item",
"naming_rule": "",

View File

@ -191,7 +191,17 @@ fixtures = [
"dt": "Custom Field",
"filters": [
["dt", "=", "Quotation"],
["fieldname", "=", "custom_quotation_template"]
["fieldname", "in", [
"custom_quotation_template",
"custom_project_template"
]]
]
},
{
"dt": "Custom Field",
"filters": [
["dt", "=", "Sales Order"],
["fieldname", "=", "custom_project_template"]
]
},
{
@ -200,6 +210,13 @@ fixtures = [
["dt", "=", "Lead"],
["fieldname", "=", "custom_customer_name"]
]
},
{
"dt": "Custom Field",
"filters": [
["dt", "=", "Project Template"],
["fieldname", "=", "company"]
]
}
]

View File

@ -193,6 +193,24 @@ def add_custom_fields():
fieldtype="Check",
default=0,
insert_after="custom_installation_address"
),
dict(
fieldname="custom_project_template",
label="Project Template",
fieldtype="Link",
options="Project Template",
insert_after="custom_quotation_template",
module="custom_ui",
description="The project template to use when creating a project from this quotation."
),
dict(
fieldname="custom_quotation_template",
label="Quotation Template",
fieldtype="Link",
options="Quotation Template",
insert_after="terms_and_conditions",
module="custom_ui",
description="The template used for generating this quotation."
)
],
"Sales Order": [
@ -202,6 +220,25 @@ def add_custom_fields():
fieldtype="Check",
default=0,
insert_after="custom_installation_address"
),
dict(
fieldname="custom_project_template",
label="Project Template",
fieldtype="Link",
options="Project Template",
module="custom_ui",
description="The project template to use when creating a project from this sales order."
)
],
"Project Template": [
dict(
fieldname="company",
label="Company",
fieldtype="Link",
options="Company",
insert_after="project_type",
module="custom_ui",
description="The company associated with this project template."
)
]
}

View File

@ -20,6 +20,7 @@ const FRAPPE_GET_JOBS_METHOD = "custom_ui.api.db.jobs.get_jobs_table_data";
const FRAPPE_UPSERT_JOB_METHOD = "custom_ui.api.db.jobs.upsert_job";
const FRAPPE_GET_JOB_TASK_LIST_METHOD = "custom_ui.api.db.jobs.get_job_task_table_data";
const FRAPPE_GET_INSTALL_PROJECTS_METHOD = "custom_ui.api.db.jobs.get_install_projects";
const FRAPPE_GET_JOB_TEMPLATES_METHOD = "custom_ui.api.db.jobs.get_job_templates";
// 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";
@ -236,6 +237,10 @@ class Api {
// JOB / PROJECT METHODS
// ============================================================================
static async getJobTemplates(company) {
return await this.request(FRAPPE_GET_JOB_TEMPLATES_METHOD, { company });
}
static async getJobDetails() {
const projects = await this.getDocsList("Project");
const data = [];

View File

@ -59,6 +59,23 @@
</div>
</div>
<!-- Project Template Section -->
<div class="project-template-section">
<label for="projectTemplate" class="field-label">
Project Template
<span class="required">*</span>
</label>
<Select
v-model="formData.projectTemplate"
:options="projectTemplates"
optionLabel="name"
optionValue="name"
placeholder="Select a project template"
:disabled="!isEditable"
fluid
/>
</div>
<!-- Template Section -->
<div class="template-section">
<div v-if="isNew">
@ -356,6 +373,7 @@ const formData = reactive({
contact: "",
estimateName: null,
requiresHalfPayment: false,
projectTemplate: null,
});
const selectedAddress = ref(null);
@ -369,6 +387,7 @@ const quotationItems = ref([]);
const selectedItems = ref([]);
const responses = ref(["Accepted", "Rejected"]);
const templates = ref([]);
const projectTemplates = ref([]);
const selectedTemplate = ref(null);
const showAddressModal = ref(false);
@ -396,6 +415,16 @@ const itemColumns = [
];
// Methods
const fetchProjectTemplates = async () => {
try {
const result = await Api.getJobTemplates(company.currentCompany);
projectTemplates.value = [...result, { name: "Other" }];
} catch (error) {
console.error("Error fetching project templates:", error);
notificationStore.addNotification("Failed to fetch project templates", "error");
}
};
const fetchTemplates = async () => {
if (!isNew.value) return;
try {
@ -556,6 +585,10 @@ const onQtyChange = (item) => {
};
const saveDraft = async () => {
if (!formData.projectTemplate) {
notificationStore.addNotification("Project Template is required.", "error");
return;
}
isSubmitting.value = true;
try {
const data = {
@ -570,6 +603,7 @@ const saveDraft = async () => {
})),
estimateName: formData.estimateName,
requiresHalfPayment: formData.requiresHalfPayment,
projectTemplate: formData.projectTemplate,
company: company.currentCompany
};
estimate.value = await Api.createEstimate(data);
@ -673,7 +707,10 @@ watch(
);
watch(() => company.currentCompany, () => {
fetchTemplates();
if (isNew.value) {
fetchTemplates();
fetchProjectTemplates();
}
});
// Watch for query param changes to refresh page behavior
@ -773,7 +810,8 @@ onMounted(async () => {
} catch (error) {
console.error("Error loading quotation items:", error);
}
fetchProjectTemplates();
if (isNew.value) {
fetchTemplates();
}
@ -854,6 +892,7 @@ onMounted(async () => {
.address-section,
.contact-section,
.project-template-section,
.template-section {
margin-bottom: 1.5rem;
}