add project template into estimate creation to help project generation flow
This commit is contained in:
parent
8f90ef09fb
commit
54ae6d14f8
8 changed files with 118 additions and 66 deletions
|
|
@ -429,6 +429,7 @@ def upsert_estimate(data):
|
||||||
"customer_address": data.get("address_name"),
|
"customer_address": data.get("address_name"),
|
||||||
"contact_person": data.get("contact_name"),
|
"contact_person": data.get("contact_name"),
|
||||||
"letter_head": data.get("company"),
|
"letter_head": data.get("company"),
|
||||||
|
"custom_project_template": data.get("project_template", None)
|
||||||
})
|
})
|
||||||
for item in data.get("items", []):
|
for item in data.get("items", []):
|
||||||
item = json.loads(item) if isinstance(item, str) else item
|
item = json.loads(item) if isinstance(item, str) else item
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,17 @@ from custom_ui.db_utils import process_query_conditions, build_datatable_dict, g
|
||||||
# JOB MANAGEMENT API METHODS
|
# 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()
|
@frappe.whitelist()
|
||||||
def create_job_from_sales_order(sales_order_name):
|
def create_job_from_sales_order(sales_order_name):
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
@ -498,8 +498,8 @@
|
||||||
"make_attachments_public": 0,
|
"make_attachments_public": 0,
|
||||||
"max_attachments": 0,
|
"max_attachments": 0,
|
||||||
"menu_index": null,
|
"menu_index": null,
|
||||||
"migration_hash": null,
|
"migration_hash": "2521636464aadbebbd70dfbf13252950",
|
||||||
"modified": "2026-01-02 11:26:31.164108",
|
"modified": "2026-01-07 11:10:03.317996",
|
||||||
"module": "Selling",
|
"module": "Selling",
|
||||||
"name": "Quotation Template",
|
"name": "Quotation Template",
|
||||||
"naming_rule": "",
|
"naming_rule": "",
|
||||||
|
|
@ -996,8 +996,8 @@
|
||||||
"make_attachments_public": 0,
|
"make_attachments_public": 0,
|
||||||
"max_attachments": 0,
|
"max_attachments": 0,
|
||||||
"menu_index": null,
|
"menu_index": null,
|
||||||
"migration_hash": null,
|
"migration_hash": "2521636464aadbebbd70dfbf13252950",
|
||||||
"modified": "2025-12-23 02:00:30.908719",
|
"modified": "2026-01-07 11:10:03.406758",
|
||||||
"module": "Selling",
|
"module": "Selling",
|
||||||
"name": "Quotation Template Item",
|
"name": "Quotation Template Item",
|
||||||
"naming_rule": "",
|
"naming_rule": "",
|
||||||
|
|
|
||||||
|
|
@ -191,7 +191,17 @@ fixtures = [
|
||||||
"dt": "Custom Field",
|
"dt": "Custom Field",
|
||||||
"filters": [
|
"filters": [
|
||||||
["dt", "=", "Quotation"],
|
["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"],
|
["dt", "=", "Lead"],
|
||||||
["fieldname", "=", "custom_customer_name"]
|
["fieldname", "=", "custom_customer_name"]
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"dt": "Custom Field",
|
||||||
|
"filters": [
|
||||||
|
["dt", "=", "Project Template"],
|
||||||
|
["fieldname", "=", "company"]
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -193,6 +193,24 @@ def add_custom_fields():
|
||||||
fieldtype="Check",
|
fieldtype="Check",
|
||||||
default=0,
|
default=0,
|
||||||
insert_after="custom_installation_address"
|
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": [
|
"Sales Order": [
|
||||||
|
|
@ -202,6 +220,25 @@ def add_custom_fields():
|
||||||
fieldtype="Check",
|
fieldtype="Check",
|
||||||
default=0,
|
default=0,
|
||||||
insert_after="custom_installation_address"
|
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."
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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_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_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_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
|
// Invoice methods
|
||||||
const FRAPPE_GET_INVOICES_METHOD = "custom_ui.api.db.invoices.get_invoice_table_data";
|
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";
|
const FRAPPE_UPSERT_INVOICE_METHOD = "custom_ui.api.db.invoices.upsert_invoice";
|
||||||
|
|
@ -236,6 +237,10 @@ class Api {
|
||||||
// JOB / PROJECT METHODS
|
// JOB / PROJECT METHODS
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
|
static async getJobTemplates(company) {
|
||||||
|
return await this.request(FRAPPE_GET_JOB_TEMPLATES_METHOD, { company });
|
||||||
|
}
|
||||||
|
|
||||||
static async getJobDetails() {
|
static async getJobDetails() {
|
||||||
const projects = await this.getDocsList("Project");
|
const projects = await this.getDocsList("Project");
|
||||||
const data = [];
|
const data = [];
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,23 @@
|
||||||
</div>
|
</div>
|
||||||
</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 -->
|
<!-- Template Section -->
|
||||||
<div class="template-section">
|
<div class="template-section">
|
||||||
<div v-if="isNew">
|
<div v-if="isNew">
|
||||||
|
|
@ -356,6 +373,7 @@ const formData = reactive({
|
||||||
contact: "",
|
contact: "",
|
||||||
estimateName: null,
|
estimateName: null,
|
||||||
requiresHalfPayment: false,
|
requiresHalfPayment: false,
|
||||||
|
projectTemplate: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const selectedAddress = ref(null);
|
const selectedAddress = ref(null);
|
||||||
|
|
@ -369,6 +387,7 @@ const quotationItems = ref([]);
|
||||||
const selectedItems = ref([]);
|
const selectedItems = ref([]);
|
||||||
const responses = ref(["Accepted", "Rejected"]);
|
const responses = ref(["Accepted", "Rejected"]);
|
||||||
const templates = ref([]);
|
const templates = ref([]);
|
||||||
|
const projectTemplates = ref([]);
|
||||||
const selectedTemplate = ref(null);
|
const selectedTemplate = ref(null);
|
||||||
|
|
||||||
const showAddressModal = ref(false);
|
const showAddressModal = ref(false);
|
||||||
|
|
@ -396,6 +415,16 @@ const itemColumns = [
|
||||||
];
|
];
|
||||||
|
|
||||||
// Methods
|
// 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 () => {
|
const fetchTemplates = async () => {
|
||||||
if (!isNew.value) return;
|
if (!isNew.value) return;
|
||||||
try {
|
try {
|
||||||
|
|
@ -556,6 +585,10 @@ const onQtyChange = (item) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const saveDraft = async () => {
|
const saveDraft = async () => {
|
||||||
|
if (!formData.projectTemplate) {
|
||||||
|
notificationStore.addNotification("Project Template is required.", "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
isSubmitting.value = true;
|
isSubmitting.value = true;
|
||||||
try {
|
try {
|
||||||
const data = {
|
const data = {
|
||||||
|
|
@ -570,6 +603,7 @@ const saveDraft = async () => {
|
||||||
})),
|
})),
|
||||||
estimateName: formData.estimateName,
|
estimateName: formData.estimateName,
|
||||||
requiresHalfPayment: formData.requiresHalfPayment,
|
requiresHalfPayment: formData.requiresHalfPayment,
|
||||||
|
projectTemplate: formData.projectTemplate,
|
||||||
company: company.currentCompany
|
company: company.currentCompany
|
||||||
};
|
};
|
||||||
estimate.value = await Api.createEstimate(data);
|
estimate.value = await Api.createEstimate(data);
|
||||||
|
|
@ -673,7 +707,10 @@ watch(
|
||||||
);
|
);
|
||||||
|
|
||||||
watch(() => company.currentCompany, () => {
|
watch(() => company.currentCompany, () => {
|
||||||
|
if (isNew.value) {
|
||||||
fetchTemplates();
|
fetchTemplates();
|
||||||
|
fetchProjectTemplates();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Watch for query param changes to refresh page behavior
|
// Watch for query param changes to refresh page behavior
|
||||||
|
|
@ -773,6 +810,7 @@ onMounted(async () => {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error loading quotation items:", error);
|
console.error("Error loading quotation items:", error);
|
||||||
}
|
}
|
||||||
|
fetchProjectTemplates();
|
||||||
|
|
||||||
if (isNew.value) {
|
if (isNew.value) {
|
||||||
fetchTemplates();
|
fetchTemplates();
|
||||||
|
|
@ -854,6 +892,7 @@ onMounted(async () => {
|
||||||
|
|
||||||
.address-section,
|
.address-section,
|
||||||
.contact-section,
|
.contact-section,
|
||||||
|
.project-template-section,
|
||||||
.template-section {
|
.template-section {
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue