big package update
This commit is contained in:
parent
21173e34c6
commit
991038bc47
80
custom_ui/api/db/items.py
Normal file
80
custom_ui/api/db/items.py
Normal file
@ -0,0 +1,80 @@
|
||||
import frappe
|
||||
import json
|
||||
from custom_ui.models import PackageCreationData
|
||||
from custom_ui.services import ProjectService, ItemService
|
||||
from custom_ui.db_utils import build_error_response, build_success_response
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_by_project_template(project_template: str) -> dict:
|
||||
"""Retrieve items associated with a given project template."""
|
||||
print(f"DEBUG: Getting items for Project Template {project_template}")
|
||||
item_groups = ProjectService.get_project_item_groups(project_template)
|
||||
items = ItemService.get_items_by_groups(item_groups)
|
||||
print(f"DEBUG: Retrieved {len(items)} items for Project Template {project_template}")
|
||||
categorized_items = ItemService.build_category_dict(items)
|
||||
return build_success_response(categorized_items)
|
||||
|
||||
@frappe.whitelist()
|
||||
def save_as_package_item(data):
|
||||
"""Save a new Package Item based on the provided data."""
|
||||
from custom_ui.models import BOMItem
|
||||
data = json.loads(data)
|
||||
print(f"DEBUG: Saving Package Item with data: {data}")
|
||||
# Map 'category' to 'item_group' for the model
|
||||
data['item_group'] = data.pop('category')
|
||||
# Convert items dictionaries to BOMItem instances
|
||||
data['items'] = [
|
||||
BOMItem(
|
||||
item_code=item['item_code'],
|
||||
qty=item['qty'],
|
||||
uom=item['uom']
|
||||
) for item in data['items']
|
||||
]
|
||||
data = PackageCreationData(**data)
|
||||
item = frappe.get_doc({
|
||||
"doctype": "Item",
|
||||
"item_code": ItemService.build_item_code(data.code_prefix, data.package_name),
|
||||
"item_name": data.package_name,
|
||||
"is_stock_item": 0,
|
||||
"item_group": data.item_group,
|
||||
"description": data.description,
|
||||
"standard_rate": data.rate or 0.0,
|
||||
"company": data.company,
|
||||
"has_variants": 0,
|
||||
"stock_uom": "Nos",
|
||||
"is_sales_item": 1,
|
||||
"is_purchase_item": 0,
|
||||
"is_pro_applicable": 0,
|
||||
"is_fixed_asset": 0,
|
||||
"is_service_item": 0
|
||||
}).insert()
|
||||
bom = frappe.get_doc({
|
||||
"doctype": "BOM",
|
||||
"item": item.name,
|
||||
"uom": "Nos",
|
||||
"is_active": 1,
|
||||
"is_default": 1,
|
||||
"items": [{
|
||||
"item_code": bom_item.item_code,
|
||||
"qty": bom_item.qty,
|
||||
"uom": bom_item.uom
|
||||
} for bom_item in data.items]
|
||||
}).insert()
|
||||
bom.submit()
|
||||
item.reload() # Refresh to get latest version after BOM submission
|
||||
item.default_bom = bom.name
|
||||
item.save()
|
||||
print(f"DEBUG: Created Package Item with name: {item.name}")
|
||||
item_dict = item.as_dict()
|
||||
item_dict["bom"] = ItemService.get_full_bom_dict(item.item_code) # Attach BOM details to the item dict
|
||||
return build_success_response(item_dict)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_item_categories():
|
||||
"""Retrieve all item groups for categorization."""
|
||||
print("DEBUG: Getting item categories")
|
||||
item_groups = frappe.get_all("Item Group", pluck="name")
|
||||
print(f"DEBUG: Retrieved item categories: {item_groups}")
|
||||
return build_success_response(item_groups)
|
||||
|
||||
16
custom_ui/events/payments.py
Normal file
16
custom_ui/events/payments.py
Normal file
@ -0,0 +1,16 @@
|
||||
import frappe
|
||||
|
||||
def on_submit(doc, method):
|
||||
print("DEBUG: On Submit Triggered for Payment Entry")
|
||||
is_advance_payment = any(ref.reference_doctype == "Sales Order" for ref in doc.references)
|
||||
if is_advance_payment:
|
||||
print("DEBUG: Payment Entry is for an advance payment, checking Sales Order if half down requirement is met.")
|
||||
so_ref = next((ref for ref in doc.references if ref.reference_doctype == "Sales Order"), None)
|
||||
if so_ref:
|
||||
so_doc = frappe.get_doc("Sales Order", so_ref.reference_name)
|
||||
if so_doc.requires_half_payment:
|
||||
is_paid = doc.custom_halfdown_amount <= doc.advance_paid or doc.advance_paid >= so_doc.grand_total / 2
|
||||
if is_paid and not so_doc.custom_halfdown_is_paid:
|
||||
print("DEBUG: Sales Order requires half payment and it has not been marked as paid, marking it as paid now.")
|
||||
so_doc.custom_halfdown_is_paid = 1
|
||||
so_doc.save()
|
||||
File diff suppressed because it is too large
Load Diff
@ -1408,8 +1408,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "c9094ea959c7b6ff11522d064fe04b35",
|
||||
"modified": "2026-01-26 01:52:12.948757",
|
||||
"migration_hash": "330c425fa522cd61f3e1012dfbe56f02",
|
||||
"modified": "2026-02-02 09:31:30.309793",
|
||||
"module": "CRM",
|
||||
"name": "Properties",
|
||||
"naming_rule": "By fieldname",
|
||||
@ -3186,8 +3186,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "c9094ea959c7b6ff11522d064fe04b35",
|
||||
"modified": "2026-01-26 01:52:13.056788",
|
||||
"migration_hash": "330c425fa522cd61f3e1012dfbe56f02",
|
||||
"modified": "2026-02-02 09:31:30.432329",
|
||||
"module": "CRM",
|
||||
"name": "SNW Jobs",
|
||||
"naming_rule": "Autoincrement",
|
||||
@ -4109,8 +4109,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "c9094ea959c7b6ff11522d064fe04b35",
|
||||
"modified": "2026-01-26 01:52:13.154567",
|
||||
"migration_hash": "330c425fa522cd61f3e1012dfbe56f02",
|
||||
"modified": "2026-02-02 09:31:30.520105",
|
||||
"module": "Projects",
|
||||
"name": "Work Schedule",
|
||||
"naming_rule": "",
|
||||
@ -9151,8 +9151,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "c9094ea959c7b6ff11522d064fe04b35",
|
||||
"modified": "2026-01-26 01:52:13.303974",
|
||||
"migration_hash": "330c425fa522cd61f3e1012dfbe56f02",
|
||||
"modified": "2026-02-02 09:31:30.684330",
|
||||
"module": "CRM",
|
||||
"name": "Follow Up Checklist",
|
||||
"naming_rule": "By fieldname",
|
||||
@ -9457,8 +9457,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "c9094ea959c7b6ff11522d064fe04b35",
|
||||
"modified": "2026-01-26 01:52:13.377143",
|
||||
"migration_hash": "330c425fa522cd61f3e1012dfbe56f02",
|
||||
"modified": "2026-02-02 09:31:30.760407",
|
||||
"module": "CRM",
|
||||
"name": "Follow Check List Fields",
|
||||
"naming_rule": "By fieldname",
|
||||
@ -10147,8 +10147,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "c9094ea959c7b6ff11522d064fe04b35",
|
||||
"modified": "2026-01-26 01:52:13.498788",
|
||||
"migration_hash": "330c425fa522cd61f3e1012dfbe56f02",
|
||||
"modified": "2026-02-02 09:31:30.859914",
|
||||
"module": "Brotherton SOP",
|
||||
"name": "SOP-Documentation",
|
||||
"naming_rule": "Set by user",
|
||||
@ -10348,8 +10348,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "c9094ea959c7b6ff11522d064fe04b35",
|
||||
"modified": "2026-01-26 01:52:13.568354",
|
||||
"migration_hash": "330c425fa522cd61f3e1012dfbe56f02",
|
||||
"modified": "2026-02-02 09:31:30.922266",
|
||||
"module": "Desk",
|
||||
"name": "SOP Notes",
|
||||
"naming_rule": "",
|
||||
@ -10694,8 +10694,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "c9094ea959c7b6ff11522d064fe04b35",
|
||||
"modified": "2026-01-26 01:52:13.644007",
|
||||
"migration_hash": "330c425fa522cd61f3e1012dfbe56f02",
|
||||
"modified": "2026-02-02 09:31:30.991664",
|
||||
"module": "Desk",
|
||||
"name": "Tutorials",
|
||||
"naming_rule": "By fieldname",
|
||||
@ -11064,8 +11064,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "c9094ea959c7b6ff11522d064fe04b35",
|
||||
"modified": "2026-01-26 01:52:13.714534",
|
||||
"migration_hash": "330c425fa522cd61f3e1012dfbe56f02",
|
||||
"modified": "2026-02-02 09:31:31.061659",
|
||||
"module": "Desk",
|
||||
"name": "Brotherton Meetings Scheduler",
|
||||
"naming_rule": "",
|
||||
@ -11242,8 +11242,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "c9094ea959c7b6ff11522d064fe04b35",
|
||||
"modified": "2026-01-26 01:52:13.776408",
|
||||
"migration_hash": "330c425fa522cd61f3e1012dfbe56f02",
|
||||
"modified": "2026-02-02 09:31:31.124415",
|
||||
"module": "Desk",
|
||||
"name": "Meeting Participants",
|
||||
"naming_rule": "",
|
||||
@ -11588,8 +11588,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "c9094ea959c7b6ff11522d064fe04b35",
|
||||
"modified": "2026-01-26 01:52:13.863897",
|
||||
"migration_hash": "330c425fa522cd61f3e1012dfbe56f02",
|
||||
"modified": "2026-02-02 09:31:31.204512",
|
||||
"module": "Desk",
|
||||
"name": "Add-On Job Detail",
|
||||
"naming_rule": "By fieldname",
|
||||
@ -11870,8 +11870,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "c9094ea959c7b6ff11522d064fe04b35",
|
||||
"modified": "2026-01-26 01:52:13.938332",
|
||||
"migration_hash": "330c425fa522cd61f3e1012dfbe56f02",
|
||||
"modified": "2026-02-02 09:31:31.273908",
|
||||
"module": "Desk",
|
||||
"name": "Crew Schedule Detail",
|
||||
"naming_rule": "",
|
||||
@ -12152,8 +12152,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "c9094ea959c7b6ff11522d064fe04b35",
|
||||
"modified": "2026-01-26 01:52:14.015669",
|
||||
"migration_hash": "330c425fa522cd61f3e1012dfbe56f02",
|
||||
"modified": "2026-02-02 09:31:31.342599",
|
||||
"module": "Setup",
|
||||
"name": "City",
|
||||
"naming_rule": "By fieldname",
|
||||
@ -14738,8 +14738,8 @@
|
||||
"make_attachments_public": 1,
|
||||
"max_attachments": 5,
|
||||
"menu_index": null,
|
||||
"migration_hash": "c9094ea959c7b6ff11522d064fe04b35",
|
||||
"modified": "2026-01-26 01:52:14.167954",
|
||||
"migration_hash": "330c425fa522cd61f3e1012dfbe56f02",
|
||||
"modified": "2026-02-02 09:31:31.484159",
|
||||
"module": "Projects",
|
||||
"name": "Fencing Job Queue",
|
||||
"naming_rule": "Set by user",
|
||||
@ -15630,8 +15630,8 @@
|
||||
"make_attachments_public": 1,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "c9094ea959c7b6ff11522d064fe04b35",
|
||||
"modified": "2026-01-26 01:52:14.271176",
|
||||
"migration_hash": "330c425fa522cd61f3e1012dfbe56f02",
|
||||
"modified": "2026-02-02 09:31:31.555530",
|
||||
"module": "Setup",
|
||||
"name": "Irrigation District",
|
||||
"naming_rule": "By fieldname",
|
||||
@ -15808,8 +15808,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "c9094ea959c7b6ff11522d064fe04b35",
|
||||
"modified": "2026-01-26 01:52:14.346591",
|
||||
"migration_hash": "330c425fa522cd61f3e1012dfbe56f02",
|
||||
"modified": "2026-02-02 09:31:31.610090",
|
||||
"module": "Setup",
|
||||
"name": "Linked Companies",
|
||||
"naming_rule": "",
|
||||
@ -16154,8 +16154,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "c9094ea959c7b6ff11522d064fe04b35",
|
||||
"modified": "2026-01-26 01:52:14.429267",
|
||||
"migration_hash": "330c425fa522cd61f3e1012dfbe56f02",
|
||||
"modified": "2026-02-02 09:31:31.668108",
|
||||
"module": "Contacts",
|
||||
"name": "Address Contact Role",
|
||||
"naming_rule": "",
|
||||
@ -17056,6 +17056,70 @@
|
||||
"unique": 0,
|
||||
"width": null
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"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,
|
||||
"documentation_url": null,
|
||||
"fetch_from": null,
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "amended_from",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"hide_border": 0,
|
||||
"hide_days": 0,
|
||||
"hide_seconds": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_preview": 0,
|
||||
"in_standard_filter": 0,
|
||||
"is_virtual": 0,
|
||||
"label": "Amended From",
|
||||
"length": 0,
|
||||
"link_filters": null,
|
||||
"make_attachment_public": 0,
|
||||
"mandatory_depends_on": null,
|
||||
"max_height": null,
|
||||
"no_copy": 1,
|
||||
"non_negative": 0,
|
||||
"oldfieldname": null,
|
||||
"oldfieldtype": null,
|
||||
"options": "Backflow Test Form",
|
||||
"parent": "Backflow Test Form",
|
||||
"parentfield": "fields",
|
||||
"parenttype": "DocType",
|
||||
"permlevel": 0,
|
||||
"placeholder": null,
|
||||
"precision": null,
|
||||
"print_hide": 1,
|
||||
"print_hide_if_no_value": 0,
|
||||
"print_width": null,
|
||||
"read_only": 1,
|
||||
"read_only_depends_on": null,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 1,
|
||||
"set_only_once": 0,
|
||||
"show_dashboard": 0,
|
||||
"show_on_timeline": 0,
|
||||
"show_preview_popup": 0,
|
||||
"sort_options": 0,
|
||||
"translatable": 0,
|
||||
"trigger": null,
|
||||
"unique": 0,
|
||||
"width": null
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
@ -17140,8 +17204,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "c9094ea959c7b6ff11522d064fe04b35",
|
||||
"modified": "2026-01-26 01:52:14.536984",
|
||||
"migration_hash": "330c425fa522cd61f3e1012dfbe56f02",
|
||||
"modified": "2026-02-02 09:31:31.743894",
|
||||
"module": "Selling",
|
||||
"name": "Backflow Test Form",
|
||||
"naming_rule": "",
|
||||
@ -17830,8 +17894,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "c9094ea959c7b6ff11522d064fe04b35",
|
||||
"modified": "2026-01-26 01:52:14.668256",
|
||||
"migration_hash": "330c425fa522cd61f3e1012dfbe56f02",
|
||||
"modified": "2026-02-02 09:31:31.827955",
|
||||
"module": "Selling",
|
||||
"name": "Pre-Built Routes",
|
||||
"naming_rule": "By \"Naming Series\" field",
|
||||
@ -18351,8 +18415,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "c9094ea959c7b6ff11522d064fe04b35",
|
||||
"modified": "2026-01-26 01:52:14.752785",
|
||||
"migration_hash": "330c425fa522cd61f3e1012dfbe56f02",
|
||||
"modified": "2026-02-02 09:31:31.885625",
|
||||
"module": "Contacts",
|
||||
"name": "Assigned Address",
|
||||
"naming_rule": "By fieldname",
|
||||
@ -19573,6 +19637,70 @@
|
||||
"unique": 0,
|
||||
"width": null
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"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,
|
||||
"documentation_url": null,
|
||||
"fetch_from": null,
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "amended_from",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"hide_border": 0,
|
||||
"hide_days": 0,
|
||||
"hide_seconds": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_preview": 0,
|
||||
"in_standard_filter": 0,
|
||||
"is_virtual": 0,
|
||||
"label": "Amended From",
|
||||
"length": 0,
|
||||
"link_filters": null,
|
||||
"make_attachment_public": 0,
|
||||
"mandatory_depends_on": null,
|
||||
"max_height": null,
|
||||
"no_copy": 1,
|
||||
"non_negative": 0,
|
||||
"oldfieldname": null,
|
||||
"oldfieldtype": null,
|
||||
"options": "Locate Log",
|
||||
"parent": "Locate Log",
|
||||
"parentfield": "fields",
|
||||
"parenttype": "DocType",
|
||||
"permlevel": 0,
|
||||
"placeholder": null,
|
||||
"precision": null,
|
||||
"print_hide": 1,
|
||||
"print_hide_if_no_value": 0,
|
||||
"print_width": null,
|
||||
"read_only": 1,
|
||||
"read_only_depends_on": null,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 1,
|
||||
"set_only_once": 0,
|
||||
"show_dashboard": 0,
|
||||
"show_on_timeline": 0,
|
||||
"show_preview_popup": 0,
|
||||
"sort_options": 0,
|
||||
"translatable": 0,
|
||||
"trigger": null,
|
||||
"unique": 0,
|
||||
"width": null
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
@ -19671,8 +19799,8 @@
|
||||
"make_attachments_public": 1,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "c9094ea959c7b6ff11522d064fe04b35",
|
||||
"modified": "2026-01-26 01:52:14.894391",
|
||||
"migration_hash": "330c425fa522cd61f3e1012dfbe56f02",
|
||||
"modified": "2026-02-02 09:31:32.021173",
|
||||
"module": "Projects",
|
||||
"name": "Locate Log",
|
||||
"naming_rule": "",
|
||||
@ -20243,6 +20371,70 @@
|
||||
"unique": 0,
|
||||
"width": null
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"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,
|
||||
"documentation_url": null,
|
||||
"fetch_from": null,
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "amended_from",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"hide_border": 0,
|
||||
"hide_days": 0,
|
||||
"hide_seconds": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_preview": 0,
|
||||
"in_standard_filter": 0,
|
||||
"is_virtual": 0,
|
||||
"label": "Amended From",
|
||||
"length": 0,
|
||||
"link_filters": null,
|
||||
"make_attachment_public": 0,
|
||||
"mandatory_depends_on": null,
|
||||
"max_height": null,
|
||||
"no_copy": 1,
|
||||
"non_negative": 0,
|
||||
"oldfieldname": null,
|
||||
"oldfieldtype": null,
|
||||
"options": "Backflow test report form",
|
||||
"parent": "Backflow test report form",
|
||||
"parentfield": "fields",
|
||||
"parenttype": "DocType",
|
||||
"permlevel": 0,
|
||||
"placeholder": null,
|
||||
"precision": null,
|
||||
"print_hide": 1,
|
||||
"print_hide_if_no_value": 0,
|
||||
"print_width": null,
|
||||
"read_only": 1,
|
||||
"read_only_depends_on": null,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 1,
|
||||
"set_only_once": 0,
|
||||
"show_dashboard": 0,
|
||||
"show_on_timeline": 0,
|
||||
"show_preview_popup": 0,
|
||||
"sort_options": 0,
|
||||
"translatable": 0,
|
||||
"trigger": null,
|
||||
"unique": 0,
|
||||
"width": null
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
@ -20327,8 +20519,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "c9094ea959c7b6ff11522d064fe04b35",
|
||||
"modified": "2026-01-26 01:52:14.974731",
|
||||
"migration_hash": "330c425fa522cd61f3e1012dfbe56f02",
|
||||
"modified": "2026-02-02 09:31:32.089145",
|
||||
"module": "Brotherton SOP",
|
||||
"name": "Backflow test report form",
|
||||
"naming_rule": "",
|
||||
@ -20633,8 +20825,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "c9094ea959c7b6ff11522d064fe04b35",
|
||||
"modified": "2026-01-26 01:52:15.060171",
|
||||
"migration_hash": "330c425fa522cd61f3e1012dfbe56f02",
|
||||
"modified": "2026-02-02 09:31:32.161902",
|
||||
"module": "Accounts",
|
||||
"name": "QB Export Entry",
|
||||
"naming_rule": "Autoincrement",
|
||||
@ -21171,8 +21363,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "c9094ea959c7b6ff11522d064fe04b35",
|
||||
"modified": "2026-01-26 01:52:15.161301",
|
||||
"migration_hash": "330c425fa522cd61f3e1012dfbe56f02",
|
||||
"modified": "2026-02-02 09:31:32.228299",
|
||||
"module": "Accounts",
|
||||
"name": "QB Export",
|
||||
"naming_rule": "Expression",
|
||||
@ -21669,8 +21861,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "c9094ea959c7b6ff11522d064fe04b35",
|
||||
"modified": "2026-01-26 01:52:15.226771",
|
||||
"migration_hash": "330c425fa522cd61f3e1012dfbe56f02",
|
||||
"modified": "2026-02-02 09:31:32.288896",
|
||||
"module": "Desk",
|
||||
"name": "Custom Customer Address Link",
|
||||
"naming_rule": "",
|
||||
@ -22015,8 +22207,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "c9094ea959c7b6ff11522d064fe04b35",
|
||||
"modified": "2026-01-26 01:52:15.293419",
|
||||
"migration_hash": "330c425fa522cd61f3e1012dfbe56f02",
|
||||
"modified": "2026-02-02 09:31:32.353749",
|
||||
"module": "Selling",
|
||||
"name": "On-Site Meeting",
|
||||
"naming_rule": "Expression",
|
||||
@ -22193,8 +22385,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "c9094ea959c7b6ff11522d064fe04b35",
|
||||
"modified": "2026-01-26 01:52:15.349611",
|
||||
"migration_hash": "330c425fa522cd61f3e1012dfbe56f02",
|
||||
"modified": "2026-02-02 09:31:32.406321",
|
||||
"module": "Selling",
|
||||
"name": "Route Technician Assignment",
|
||||
"naming_rule": "",
|
||||
@ -22347,8 +22539,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "c9094ea959c7b6ff11522d064fe04b35",
|
||||
"modified": "2026-01-26 01:52:15.412654",
|
||||
"migration_hash": "330c425fa522cd61f3e1012dfbe56f02",
|
||||
"modified": "2026-02-02 09:31:32.462798",
|
||||
"module": "Desk",
|
||||
"name": "Test Doctype",
|
||||
"naming_rule": "",
|
||||
@ -22525,8 +22717,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "c9094ea959c7b6ff11522d064fe04b35",
|
||||
"modified": "2026-01-26 01:52:15.467707",
|
||||
"migration_hash": "330c425fa522cd61f3e1012dfbe56f02",
|
||||
"modified": "2026-02-02 09:31:32.512828",
|
||||
"module": "Custom",
|
||||
"name": "Lead Company Link",
|
||||
"naming_rule": "",
|
||||
@ -22743,8 +22935,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "c9094ea959c7b6ff11522d064fe04b35",
|
||||
"modified": "2026-01-26 01:52:15.520767",
|
||||
"migration_hash": "330c425fa522cd61f3e1012dfbe56f02",
|
||||
"modified": "2026-02-02 09:31:32.565309",
|
||||
"module": "Custom UI",
|
||||
"name": "Customer Task Link",
|
||||
"naming_rule": "",
|
||||
@ -22961,8 +23153,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "c9094ea959c7b6ff11522d064fe04b35",
|
||||
"modified": "2026-01-26 01:52:15.580338",
|
||||
"migration_hash": "330c425fa522cd61f3e1012dfbe56f02",
|
||||
"modified": "2026-02-02 09:31:32.619158",
|
||||
"module": "Custom UI",
|
||||
"name": "Address Task Link",
|
||||
"naming_rule": "",
|
||||
@ -23115,8 +23307,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "c9094ea959c7b6ff11522d064fe04b35",
|
||||
"modified": "2026-01-26 01:52:15.635575",
|
||||
"migration_hash": "330c425fa522cd61f3e1012dfbe56f02",
|
||||
"modified": "2026-02-02 09:31:32.669515",
|
||||
"module": "Custom",
|
||||
"name": "Lead Companies Link",
|
||||
"naming_rule": "",
|
||||
@ -23333,8 +23525,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "c9094ea959c7b6ff11522d064fe04b35",
|
||||
"modified": "2026-01-26 01:52:15.692709",
|
||||
"migration_hash": "330c425fa522cd61f3e1012dfbe56f02",
|
||||
"modified": "2026-02-02 09:31:32.723818",
|
||||
"module": "Custom",
|
||||
"name": "Address Project Link",
|
||||
"naming_rule": "",
|
||||
@ -23551,8 +23743,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "c9094ea959c7b6ff11522d064fe04b35",
|
||||
"modified": "2026-01-26 01:52:15.753245",
|
||||
"migration_hash": "330c425fa522cd61f3e1012dfbe56f02",
|
||||
"modified": "2026-02-02 09:31:32.776210",
|
||||
"module": "Custom",
|
||||
"name": "Address Quotation Link",
|
||||
"naming_rule": "",
|
||||
@ -23769,8 +23961,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "c9094ea959c7b6ff11522d064fe04b35",
|
||||
"modified": "2026-01-26 01:52:15.813569",
|
||||
"migration_hash": "330c425fa522cd61f3e1012dfbe56f02",
|
||||
"modified": "2026-02-02 09:31:32.828052",
|
||||
"module": "Custom",
|
||||
"name": "Address On-Site Meeting Link",
|
||||
"naming_rule": "",
|
||||
@ -23987,8 +24179,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "c9094ea959c7b6ff11522d064fe04b35",
|
||||
"modified": "2026-01-26 01:52:15.869381",
|
||||
"migration_hash": "330c425fa522cd61f3e1012dfbe56f02",
|
||||
"modified": "2026-02-02 09:31:32.883125",
|
||||
"module": "Custom",
|
||||
"name": "Address Sales Order Link",
|
||||
"naming_rule": "",
|
||||
@ -24141,8 +24333,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "c9094ea959c7b6ff11522d064fe04b35",
|
||||
"modified": "2026-01-26 01:52:15.926523",
|
||||
"migration_hash": "330c425fa522cd61f3e1012dfbe56f02",
|
||||
"modified": "2026-02-02 09:31:32.934024",
|
||||
"module": "Custom",
|
||||
"name": "Contact Address Link",
|
||||
"naming_rule": "",
|
||||
@ -24295,8 +24487,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "c9094ea959c7b6ff11522d064fe04b35",
|
||||
"modified": "2026-01-26 01:52:15.982944",
|
||||
"migration_hash": "330c425fa522cd61f3e1012dfbe56f02",
|
||||
"modified": "2026-02-02 09:31:32.986817",
|
||||
"module": "Custom",
|
||||
"name": "Lead On-Site Meeting Link",
|
||||
"naming_rule": "",
|
||||
@ -24897,8 +25089,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "c9094ea959c7b6ff11522d064fe04b35",
|
||||
"modified": "2026-01-26 01:52:16.051786",
|
||||
"migration_hash": "330c425fa522cd61f3e1012dfbe56f02",
|
||||
"modified": "2026-02-02 09:31:33.054497",
|
||||
"module": "Selling",
|
||||
"name": "Quotation Template",
|
||||
"naming_rule": "",
|
||||
@ -25395,8 +25587,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "c9094ea959c7b6ff11522d064fe04b35",
|
||||
"modified": "2026-01-26 01:52:16.131703",
|
||||
"migration_hash": "330c425fa522cd61f3e1012dfbe56f02",
|
||||
"modified": "2026-02-02 09:31:33.128567",
|
||||
"module": "Selling",
|
||||
"name": "Quotation Template Item",
|
||||
"naming_rule": "",
|
||||
@ -25549,8 +25741,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "c9094ea959c7b6ff11522d064fe04b35",
|
||||
"modified": "2026-01-26 01:52:16.186884",
|
||||
"migration_hash": "330c425fa522cd61f3e1012dfbe56f02",
|
||||
"modified": "2026-02-02 09:31:33.180500",
|
||||
"module": "Custom UI",
|
||||
"name": "Customer Company Link",
|
||||
"naming_rule": "",
|
||||
@ -25703,8 +25895,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "c9094ea959c7b6ff11522d064fe04b35",
|
||||
"modified": "2026-01-26 01:52:16.242217",
|
||||
"migration_hash": "330c425fa522cd61f3e1012dfbe56f02",
|
||||
"modified": "2026-02-02 09:31:33.232445",
|
||||
"module": "Custom UI",
|
||||
"name": "Customer Address Link",
|
||||
"naming_rule": "",
|
||||
@ -25857,8 +26049,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "c9094ea959c7b6ff11522d064fe04b35",
|
||||
"modified": "2026-01-26 01:52:16.295479",
|
||||
"migration_hash": "330c425fa522cd61f3e1012dfbe56f02",
|
||||
"modified": "2026-02-02 09:31:33.283358",
|
||||
"module": "Custom UI",
|
||||
"name": "Customer Contact Link",
|
||||
"naming_rule": "",
|
||||
@ -26011,8 +26203,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "c9094ea959c7b6ff11522d064fe04b35",
|
||||
"modified": "2026-01-26 01:52:16.349430",
|
||||
"migration_hash": "330c425fa522cd61f3e1012dfbe56f02",
|
||||
"modified": "2026-02-02 09:31:33.334916",
|
||||
"module": "Custom",
|
||||
"name": "Address Contact Link",
|
||||
"naming_rule": "",
|
||||
@ -26165,8 +26357,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "c9094ea959c7b6ff11522d064fe04b35",
|
||||
"modified": "2026-01-26 01:52:16.402648",
|
||||
"migration_hash": "330c425fa522cd61f3e1012dfbe56f02",
|
||||
"modified": "2026-02-02 09:31:33.388907",
|
||||
"module": "Custom",
|
||||
"name": "Customer On-Site Meeting Link",
|
||||
"naming_rule": "",
|
||||
@ -26319,8 +26511,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "c9094ea959c7b6ff11522d064fe04b35",
|
||||
"modified": "2026-01-26 01:52:16.453671",
|
||||
"migration_hash": "330c425fa522cd61f3e1012dfbe56f02",
|
||||
"modified": "2026-02-02 09:31:33.442065",
|
||||
"module": "Custom",
|
||||
"name": "Customer Project Link",
|
||||
"naming_rule": "",
|
||||
@ -26473,8 +26665,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "c9094ea959c7b6ff11522d064fe04b35",
|
||||
"modified": "2026-01-26 01:52:16.510653",
|
||||
"migration_hash": "330c425fa522cd61f3e1012dfbe56f02",
|
||||
"modified": "2026-02-02 09:31:33.493993",
|
||||
"module": "Custom",
|
||||
"name": "Customer Quotation Link",
|
||||
"naming_rule": "",
|
||||
@ -26627,8 +26819,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "c9094ea959c7b6ff11522d064fe04b35",
|
||||
"modified": "2026-01-26 01:52:16.565855",
|
||||
"migration_hash": "330c425fa522cd61f3e1012dfbe56f02",
|
||||
"modified": "2026-02-02 09:31:33.547121",
|
||||
"module": "Custom",
|
||||
"name": "Customer Sales Order Link",
|
||||
"naming_rule": "",
|
||||
@ -26781,8 +26973,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "c9094ea959c7b6ff11522d064fe04b35",
|
||||
"modified": "2026-01-26 01:52:16.623951",
|
||||
"migration_hash": "330c425fa522cd61f3e1012dfbe56f02",
|
||||
"modified": "2026-02-02 09:31:33.599872",
|
||||
"module": "Custom",
|
||||
"name": "Lead Address Link",
|
||||
"naming_rule": "",
|
||||
@ -26935,8 +27127,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "c9094ea959c7b6ff11522d064fe04b35",
|
||||
"modified": "2026-01-26 01:52:16.678981",
|
||||
"migration_hash": "330c425fa522cd61f3e1012dfbe56f02",
|
||||
"modified": "2026-02-02 09:31:33.670663",
|
||||
"module": "Custom",
|
||||
"name": "Lead Contact Link",
|
||||
"naming_rule": "",
|
||||
@ -27089,8 +27281,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "c9094ea959c7b6ff11522d064fe04b35",
|
||||
"modified": "2026-01-26 01:52:16.735725",
|
||||
"migration_hash": "330c425fa522cd61f3e1012dfbe56f02",
|
||||
"modified": "2026-02-02 09:31:33.723019",
|
||||
"module": "Custom",
|
||||
"name": "Lead Quotation Link",
|
||||
"naming_rule": "",
|
||||
@ -27243,8 +27435,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "c9094ea959c7b6ff11522d064fe04b35",
|
||||
"modified": "2026-01-26 01:52:16.790139",
|
||||
"migration_hash": "330c425fa522cd61f3e1012dfbe56f02",
|
||||
"modified": "2026-02-02 09:31:33.773957",
|
||||
"module": "Custom",
|
||||
"name": "Address Company Link",
|
||||
"naming_rule": "",
|
||||
@ -28229,8 +28421,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "c9094ea959c7b6ff11522d064fe04b35",
|
||||
"modified": "2026-01-26 10:35:03.150818",
|
||||
"migration_hash": "330c425fa522cd61f3e1012dfbe56f02",
|
||||
"modified": "2026-02-02 09:31:33.849415",
|
||||
"module": "Custom UI",
|
||||
"name": "Service Appointment",
|
||||
"naming_rule": "Expression",
|
||||
@ -29303,8 +29495,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "c9094ea959c7b6ff11522d064fe04b35",
|
||||
"modified": "2026-01-26 01:52:16.929388",
|
||||
"migration_hash": "330c425fa522cd61f3e1012dfbe56f02",
|
||||
"modified": "2026-02-02 09:31:33.917771",
|
||||
"module": "Custom UI",
|
||||
"name": "Bid Meeting Note Form Field",
|
||||
"naming_rule": "",
|
||||
@ -29713,8 +29905,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "c9094ea959c7b6ff11522d064fe04b35",
|
||||
"modified": "2026-01-26 01:52:16.999820",
|
||||
"migration_hash": "330c425fa522cd61f3e1012dfbe56f02",
|
||||
"modified": "2026-02-02 09:31:33.980734",
|
||||
"module": "Custom UI",
|
||||
"name": "Bid Meeting Note Form",
|
||||
"naming_rule": "",
|
||||
@ -30595,8 +30787,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "c9094ea959c7b6ff11522d064fe04b35",
|
||||
"modified": "2026-01-26 01:52:17.067503",
|
||||
"migration_hash": "330c425fa522cd61f3e1012dfbe56f02",
|
||||
"modified": "2026-02-02 09:31:34.047833",
|
||||
"module": "Custom UI",
|
||||
"name": "Bid Meeting Note Field",
|
||||
"naming_rule": "",
|
||||
@ -31005,8 +31197,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "c9094ea959c7b6ff11522d064fe04b35",
|
||||
"modified": "2026-01-26 01:52:17.135078",
|
||||
"migration_hash": "330c425fa522cd61f3e1012dfbe56f02",
|
||||
"modified": "2026-02-02 09:31:34.111266",
|
||||
"module": "Custom UI",
|
||||
"name": "Bid Meeting Note",
|
||||
"naming_rule": "",
|
||||
@ -31183,8 +31375,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "c9094ea959c7b6ff11522d064fe04b35",
|
||||
"modified": "2026-01-26 01:52:17.193504",
|
||||
"migration_hash": "330c425fa522cd61f3e1012dfbe56f02",
|
||||
"modified": "2026-02-02 09:31:34.162346",
|
||||
"module": "Custom UI",
|
||||
"name": "Project Task Link",
|
||||
"naming_rule": "",
|
||||
@ -31337,8 +31529,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "c9094ea959c7b6ff11522d064fe04b35",
|
||||
"modified": "2026-01-26 01:52:17.253714",
|
||||
"migration_hash": "330c425fa522cd61f3e1012dfbe56f02",
|
||||
"modified": "2026-02-02 09:31:34.224291",
|
||||
"module": "Custom UI",
|
||||
"name": "Condition",
|
||||
"naming_rule": "",
|
||||
@ -31643,8 +31835,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": "c9094ea959c7b6ff11522d064fe04b35",
|
||||
"modified": "2026-01-26 01:52:17.308226",
|
||||
"migration_hash": "330c425fa522cd61f3e1012dfbe56f02",
|
||||
"modified": "2026-02-02 09:31:34.278957",
|
||||
"module": "Custom UI",
|
||||
"name": "Bid Meeting Note Field Quantity",
|
||||
"naming_rule": "",
|
||||
@ -32693,8 +32885,8 @@
|
||||
"make_attachments_public": 0,
|
||||
"max_attachments": 0,
|
||||
"menu_index": null,
|
||||
"migration_hash": null,
|
||||
"modified": "2026-01-27 04:34:52.205293",
|
||||
"migration_hash": "330c425fa522cd61f3e1012dfbe56f02",
|
||||
"modified": "2026-02-02 09:31:34.354639",
|
||||
"module": "Custom UI",
|
||||
"name": "Service Address 2",
|
||||
"naming_rule": "",
|
||||
|
||||
@ -11583,22 +11583,6 @@
|
||||
"row_name": null,
|
||||
"value": "1"
|
||||
},
|
||||
{
|
||||
"default_value": null,
|
||||
"doc_type": "Sales Order",
|
||||
"docstatus": 0,
|
||||
"doctype": "Property Setter",
|
||||
"doctype_or_field": "DocType",
|
||||
"field_name": null,
|
||||
"is_system_generated": 0,
|
||||
"modified": "2025-04-25 03:31:21.087382",
|
||||
"module": null,
|
||||
"name": "Sales Order-main-field_order",
|
||||
"property": "field_order",
|
||||
"property_type": "Data",
|
||||
"row_name": null,
|
||||
"value": "[\"customer_section\", \"column_break0\", \"custom_installation_address\", \"custom_requires_halfdown\", \"title\", \"naming_series\", \"customer\", \"customer_name\", \"tax_id\", \"column_break_7\", \"transaction_date\", \"order_type\", \"delivery_date\", \"custom_department_type\", \"custom_project_complete\", \"column_break1\", \"po_no\", \"po_date\", \"company\", \"skip_delivery_note\", \"amended_from\", \"custom_section_break_htf05\", \"custom_workflow_related_custom_fields__landry\", \"custom_coordinator_notification\", \"custom_sales_order_addon\", \"accounting_dimensions_section\", \"cost_center\", \"dimension_col_break\", \"project\", \"currency_and_price_list\", \"currency\", \"conversion_rate\", \"column_break2\", \"selling_price_list\", \"price_list_currency\", \"plc_conversion_rate\", \"ignore_pricing_rule\", \"sec_warehouse\", \"scan_barcode\", \"column_break_28\", \"set_warehouse\", \"reserve_stock\", \"items_section\", \"items\", \"section_break_31\", \"total_qty\", \"total_net_weight\", \"column_break_33\", \"base_total\", \"base_net_total\", \"column_break_33a\", \"total\", \"net_total\", \"taxes_section\", \"tax_category\", \"taxes_and_charges\", \"exempt_from_sales_tax\", \"column_break_38\", \"shipping_rule\", \"column_break_49\", \"incoterm\", \"named_place\", \"section_break_40\", \"taxes\", \"section_break_43\", \"base_total_taxes_and_charges\", \"column_break_46\", \"total_taxes_and_charges\", \"totals\", \"base_grand_total\", \"base_rounding_adjustment\", \"base_rounded_total\", \"base_in_words\", \"column_break3\", \"grand_total\", \"rounding_adjustment\", \"rounded_total\", \"in_words\", \"advance_paid\", \"disable_rounded_total\", \"section_break_48\", \"apply_discount_on\", \"base_discount_amount\", \"coupon_code\", \"column_break_50\", \"additional_discount_percentage\", \"discount_amount\", \"sec_tax_breakup\", \"other_charges_calculation\", \"packing_list\", \"packed_items\", \"pricing_rule_details\", \"pricing_rules\", \"contact_info\", \"billing_address_column\", \"customer_address\", \"address_display\", \"customer_group\", \"territory\", \"column_break_84\", \"contact_person\", \"contact_display\", \"contact_phone\", \"contact_mobile\", \"contact_email\", \"shipping_address_column\", \"shipping_address_name\", \"shipping_address\", \"column_break_93\", \"dispatch_address_name\", \"dispatch_address\", \"col_break46\", \"company_address\", \"column_break_92\", \"company_address_display\", \"payment_schedule_section\", \"payment_terms_section\", \"payment_terms_template\", \"payment_schedule\", \"terms_section_break\", \"tc_name\", \"terms\", \"more_info\", \"section_break_78\", \"status\", \"delivery_status\", \"per_delivered\", \"column_break_81\", \"per_billed\", \"per_picked\", \"billing_status\", \"sales_team_section_break\", \"sales_partner\", \"column_break7\", \"amount_eligible_for_commission\", \"commission_rate\", \"total_commission\", \"section_break1\", \"sales_team\", \"loyalty_points_redemption\", \"loyalty_points\", \"column_break_116\", \"loyalty_amount\", \"subscription_section\", \"from_date\", \"to_date\", \"column_break_108\", \"auto_repeat\", \"update_auto_repeat_reference\", \"printing_details\", \"letter_head\", \"group_same_items\", \"column_break4\", \"select_print_heading\", \"language\", \"additional_info_section\", \"is_internal_customer\", \"represents_company\", \"column_break_152\", \"source\", \"inter_company_order_reference\", \"campaign\", \"party_account_currency\", \"connections_tab\"]"
|
||||
},
|
||||
{
|
||||
"default_value": null,
|
||||
"doc_type": "Address",
|
||||
@ -15134,5 +15118,21 @@
|
||||
"property_type": "Data",
|
||||
"row_name": null,
|
||||
"value": "[\"custom_column_break_k7sgq\", \"custom_installation_address\", \"naming_series\", \"project_name\", \"job_address\", \"status\", \"custom_warranty_duration_days\", \"custom_warranty_expiration_date\", \"custom_warranty_information\", \"project_type\", \"percent_complete_method\", \"percent_complete\", \"column_break_5\", \"project_template\", \"expected_start_date\", \"expected_start_time\", \"expected_end_date\", \"expected_end_time\", \"is_scheduled\", \"invoice_status\", \"custom_completion_date\", \"priority\", \"custom_foreman\", \"custom_hidden_fields\", \"department\", \"service_appointment\", \"tasks\", \"is_active\", \"custom_address\", \"custom_section_break_lgkpd\", \"custom_workflow_related_custom_fields__landry\", \"custom_permit_status\", \"custom_utlity_locate_status\", \"custom_crew_scheduling\", \"customer_details\", \"customer\", \"column_break_14\", \"sales_order\", \"users_section\", \"users\", \"copied_from\", \"section_break0\", \"notes\", \"section_break_18\", \"actual_start_date\", \"actual_start_time\", \"actual_time\", \"column_break_20\", \"actual_end_date\", \"actual_end_time\", \"project_details\", \"estimated_costing\", \"total_costing_amount\", \"total_expense_claim\", \"total_purchase_cost\", \"company\", \"column_break_28\", \"total_sales_amount\", \"total_billable_amount\", \"total_billed_amount\", \"total_consumed_material_cost\", \"cost_center\", \"margin\", \"gross_margin\", \"column_break_37\", \"per_gross_margin\", \"monitor_progress\", \"collect_progress\", \"holiday_list\", \"frequency\", \"from_time\", \"to_time\", \"first_email\", \"second_email\", \"daily_time_to_send\", \"day_to_send\", \"weekly_time_to_send\", \"column_break_45\", \"subject\", \"message\"]"
|
||||
},
|
||||
{
|
||||
"default_value": null,
|
||||
"doc_type": "Sales Order",
|
||||
"docstatus": 0,
|
||||
"doctype": "Property Setter",
|
||||
"doctype_or_field": "DocType",
|
||||
"field_name": null,
|
||||
"is_system_generated": 0,
|
||||
"modified": "2026-02-05 12:10:08.553140",
|
||||
"module": null,
|
||||
"name": "Sales Order-main-field_order",
|
||||
"property": "field_order",
|
||||
"property_type": "Data",
|
||||
"row_name": null,
|
||||
"value": "[\"customer_section\", \"column_break0\", \"custom_installation_address\", \"custom_job_address\", \"requires_half_payment\", \"custom_project_template\", \"custom_requires_halfdown\", \"title\", \"naming_series\", \"customer\", \"customer_name\", \"tax_id\", \"custom_halfdown_is_paid\", \"custom_halfdown_amount\", \"column_break_7\", \"transaction_date\", \"order_type\", \"delivery_date\", \"custom_department_type\", \"custom_project_complete\", \"column_break1\", \"po_no\", \"po_date\", \"company\", \"skip_delivery_note\", \"has_unit_price_items\", \"amended_from\", \"custom_section_break_htf05\", \"custom_workflow_related_custom_fields__landry\", \"custom_coordinator_notification\", \"custom_sales_order_addon\", \"accounting_dimensions_section\", \"cost_center\", \"dimension_col_break\", \"project\", \"currency_and_price_list\", \"currency\", \"conversion_rate\", \"column_break2\", \"selling_price_list\", \"price_list_currency\", \"plc_conversion_rate\", \"ignore_pricing_rule\", \"sec_warehouse\", \"scan_barcode\", \"last_scanned_warehouse\", \"column_break_28\", \"set_warehouse\", \"reserve_stock\", \"items_section\", \"items\", \"section_break_31\", \"total_qty\", \"total_net_weight\", \"column_break_33\", \"base_total\", \"base_net_total\", \"column_break_33a\", \"total\", \"net_total\", \"taxes_section\", \"tax_category\", \"taxes_and_charges\", \"exempt_from_sales_tax\", \"column_break_38\", \"shipping_rule\", \"column_break_49\", \"incoterm\", \"named_place\", \"section_break_40\", \"taxes\", \"section_break_43\", \"base_total_taxes_and_charges\", \"column_break_46\", \"total_taxes_and_charges\", \"totals\", \"base_grand_total\", \"base_rounding_adjustment\", \"base_rounded_total\", \"base_in_words\", \"column_break3\", \"grand_total\", \"rounding_adjustment\", \"rounded_total\", \"in_words\", \"advance_paid\", \"disable_rounded_total\", \"section_break_48\", \"apply_discount_on\", \"base_discount_amount\", \"coupon_code\", \"column_break_50\", \"additional_discount_percentage\", \"discount_amount\", \"sec_tax_breakup\", \"other_charges_calculation\", \"packing_list\", \"packed_items\", \"pricing_rule_details\", \"pricing_rules\", \"contact_info\", \"billing_address_column\", \"customer_address\", \"address_display\", \"customer_group\", \"territory\", \"column_break_84\", \"contact_person\", \"contact_display\", \"contact_phone\", \"contact_mobile\", \"contact_email\", \"shipping_address_column\", \"shipping_address_name\", \"shipping_address\", \"column_break_93\", \"dispatch_address_name\", \"dispatch_address\", \"col_break46\", \"company_address\", \"column_break_92\", \"company_contact_person\", \"company_address_display\", \"payment_schedule_section\", \"payment_terms_section\", \"payment_terms_template\", \"payment_schedule\", \"terms_section_break\", \"tc_name\", \"terms\", \"more_info\", \"section_break_78\", \"status\", \"delivery_status\", \"per_delivered\", \"column_break_81\", \"per_billed\", \"per_picked\", \"billing_status\", \"sales_team_section_break\", \"sales_partner\", \"column_break7\", \"amount_eligible_for_commission\", \"commission_rate\", \"total_commission\", \"section_break1\", \"sales_team\", \"loyalty_points_redemption\", \"loyalty_points\", \"column_break_116\", \"loyalty_amount\", \"subscription_section\", \"from_date\", \"to_date\", \"column_break_108\", \"auto_repeat\", \"update_auto_repeat_reference\", \"printing_details\", \"letter_head\", \"group_same_items\", \"column_break4\", \"select_print_heading\", \"language\", \"additional_info_section\", \"is_internal_customer\", \"represents_company\", \"column_break_152\", \"source\", \"inter_company_order_reference\", \"campaign\", \"party_account_currency\", \"connections_tab\"]"
|
||||
}
|
||||
]
|
||||
@ -1 +1,2 @@
|
||||
from .payments import PaymentData
|
||||
from .payments import PaymentData
|
||||
from .item_models import BOMItem, PackageCreationData
|
||||
18
custom_ui/models/item_models.py
Normal file
18
custom_ui/models/item_models.py
Normal file
@ -0,0 +1,18 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass
|
||||
class BOMItem:
|
||||
item_code: str
|
||||
qty: float
|
||||
uom: str
|
||||
item_name: str = None
|
||||
|
||||
@dataclass
|
||||
class PackageCreationData:
|
||||
package_name: str
|
||||
items: list[BOMItem]
|
||||
item_group: str
|
||||
code_prefix: str
|
||||
rate: float = 0.0
|
||||
company: str = None
|
||||
description: str = None
|
||||
@ -8,4 +8,5 @@ from .task_service import TaskService
|
||||
from .service_appointment_service import ServiceAppointmentService
|
||||
from .stripe_service import StripeService
|
||||
from .payment_service import PaymentService
|
||||
from .item_service import ItemService
|
||||
from .item_service import ItemService
|
||||
from .project_service import ProjectService
|
||||
@ -24,9 +24,11 @@ class ItemService:
|
||||
print(f"DEBUG: Getting BOM for Item {item_code}")
|
||||
bom_name = frappe.db.get_value("BOM", {"item": item_code, "is_active": 1}, "name")
|
||||
bom_dict = frappe.get_doc("BOM", bom_name).as_dict()
|
||||
for item in bom_dict.get('exploded_items', []):
|
||||
item_bom_name = frappe.get_value("Item", item["item_name"], "default_bom")
|
||||
item["bom"] = frappe.get_doc("BOM", item_bom_name).as_dict() if item_bom_name else None
|
||||
for item in bom_dict.get('items', []):
|
||||
bom_no = item.get("bom_no")
|
||||
if bom_no:
|
||||
bom_item_code = frappe.db.get_value("BOM", bom_no, "item")
|
||||
item["bom"] = ItemService.get_full_bom_dict(bom_item_code)
|
||||
return bom_dict
|
||||
|
||||
@staticmethod
|
||||
@ -35,4 +37,168 @@ class ItemService:
|
||||
print(f"DEBUG: Checking existence of Item {item_code}")
|
||||
exists = frappe.db.exists("Item", item_code) is not None
|
||||
print(f"DEBUG: Item {item_code} exists: {exists}")
|
||||
return exists
|
||||
return exists
|
||||
|
||||
@staticmethod
|
||||
def get_child_groups(item_group: str) -> list[str]:
|
||||
"""Retrieve all child item groups of a given item group."""
|
||||
print(f"DEBUG: Getting child groups for Item Group {item_group}")
|
||||
children = []
|
||||
child_groups = frappe.get_all("Item Group", filters={"parent_item_group": item_group}, pluck="name")
|
||||
if child_groups:
|
||||
children.extend(child_groups)
|
||||
print(f"DEBUG: Found child groups: {child_groups}. Checking for further children.")
|
||||
for child_group in child_groups:
|
||||
additional_child_groups = ItemService.get_child_groups(child_group)
|
||||
children.extend(additional_child_groups)
|
||||
|
||||
print(f"DEBUG: Retrieved child groups: {child_groups}")
|
||||
return children
|
||||
|
||||
@staticmethod
|
||||
def get_item_names_by_group(item_groups: set[str]) -> list[str]:
|
||||
"""Retrieve item names for items belonging to the specified item groups."""
|
||||
print(f"DEBUG: Getting item names for Item Groups {item_groups}")
|
||||
items = frappe.get_all("Item", filters={"item_group": ["in", list(item_groups)]}, pluck="name")
|
||||
print(f"DEBUG: Retrieved item names: {items}")
|
||||
return items
|
||||
|
||||
@staticmethod
|
||||
def get_items_by_groups(item_groups: list[str]) -> list[dict]:
|
||||
"""Retrieve all items belonging to the specified item groups."""
|
||||
print(f"DEBUG: Getting items for Item Groups {item_groups}")
|
||||
all_groups = set(item_groups)
|
||||
for group in item_groups:
|
||||
all_groups.update(ItemService.get_child_groups(group))
|
||||
|
||||
# Batch fetch all items at once with all needed fields
|
||||
items = frappe.get_all(
|
||||
"Item",
|
||||
filters={"item_group": ["in", list(all_groups)]},
|
||||
fields=[
|
||||
"name", "item_code", "item_name", "item_group", "description",
|
||||
"standard_rate", "stock_uom", "default_bom"
|
||||
]
|
||||
)
|
||||
|
||||
# Get all item codes that have BOMs
|
||||
items_with_boms = [item for item in items if item.get("default_bom")]
|
||||
item_codes_with_boms = [item["item_code"] for item in items_with_boms]
|
||||
|
||||
# Batch fetch all BOMs and their nested structure
|
||||
bom_dict = ItemService.batch_fetch_boms(item_codes_with_boms) if item_codes_with_boms else {}
|
||||
|
||||
# Attach BOMs to items
|
||||
for item in items:
|
||||
if item.get("default_bom"):
|
||||
item["bom"] = bom_dict.get(item["item_code"])
|
||||
else:
|
||||
item["bom"] = None
|
||||
|
||||
print(f"DEBUG: Retrieved {len(items)} items")
|
||||
return items
|
||||
|
||||
@staticmethod
|
||||
def batch_fetch_boms(item_codes: list[str]) -> dict:
|
||||
"""Batch fetch all BOMs and build nested structure efficiently."""
|
||||
if not item_codes:
|
||||
return {}
|
||||
|
||||
print(f"DEBUG: Batch fetching BOMs for {len(item_codes)} items")
|
||||
|
||||
# Fetch all active BOMs for the given items
|
||||
boms = frappe.get_all(
|
||||
"BOM",
|
||||
filters={"item": ["in", item_codes], "is_active": 1},
|
||||
fields=["name", "item"]
|
||||
)
|
||||
|
||||
if not boms:
|
||||
return {}
|
||||
|
||||
bom_names = [bom["name"] for bom in boms]
|
||||
|
||||
# Fetch all BOM items (children) in one query
|
||||
bom_items = frappe.get_all(
|
||||
"BOM Item",
|
||||
filters={"parent": ["in", bom_names]},
|
||||
fields=["parent", "item_code", "item_name", "qty", "uom", "bom_no"],
|
||||
order_by="idx"
|
||||
)
|
||||
|
||||
# Group BOM items by their parent BOM
|
||||
bom_items_map = {}
|
||||
nested_bom_items = set()
|
||||
|
||||
for bom_item in bom_items:
|
||||
parent = bom_item["parent"]
|
||||
if parent not in bom_items_map:
|
||||
bom_items_map[parent] = []
|
||||
bom_items_map[parent].append(bom_item)
|
||||
|
||||
# Track which items have nested BOMs
|
||||
if bom_item.get("bom_no"):
|
||||
nested_bom_items.add(bom_item["item_code"])
|
||||
|
||||
# Recursively fetch nested BOMs if any
|
||||
nested_bom_dict = {}
|
||||
if nested_bom_items:
|
||||
nested_bom_dict = ItemService.batch_fetch_boms(list(nested_bom_items))
|
||||
|
||||
# Build the result dictionary mapping item_code to its BOM structure
|
||||
result = {}
|
||||
for bom in boms:
|
||||
bom_name = bom["name"]
|
||||
item_code = bom["item"]
|
||||
|
||||
items = bom_items_map.get(bom_name, [])
|
||||
# Attach nested BOMs to items
|
||||
for item in items:
|
||||
if item.get("bom_no"):
|
||||
item["bom"] = nested_bom_dict.get(item["item_code"])
|
||||
else:
|
||||
item["bom"] = None
|
||||
|
||||
result[item_code] = {
|
||||
"name": bom_name,
|
||||
"items": items
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def build_category_dict(items: list[dict]) -> dict:
|
||||
"""Build a dictionary categorizing items by their item group."""
|
||||
print(f"DEBUG: Building category dictionary for items")
|
||||
category_dict = {}
|
||||
category_dict["Packages"] = {}
|
||||
for item in items:
|
||||
if item.get("bom"):
|
||||
if item.get("item_group", "Uncategorized") not in category_dict["Packages"]:
|
||||
category_dict["Packages"][item.get("item_group", "Uncategorized")] = []
|
||||
category_dict["Packages"][item.get("item_group", "Uncategorized")].append(item)
|
||||
else:
|
||||
category = item.get("item_group", "Uncategorized")
|
||||
if category not in category_dict:
|
||||
category_dict[category] = []
|
||||
category_dict[category].append(item)
|
||||
print(f"DEBUG: Built category dictionary with categories: {list(category_dict.keys())}")
|
||||
return category_dict
|
||||
|
||||
@staticmethod
|
||||
def build_item_code(prefix: str, item_name: str) -> str:
|
||||
"""Build a unique item code based on the provided prefix and item name."""
|
||||
print(f"DEBUG: Building item code with prefix: {prefix} and item name: {item_name}")
|
||||
# Replace all " " with "-" and convert to uppercase
|
||||
base_code = f"{prefix}-{item_name.replace(' ', '-').upper()}"
|
||||
# Check for existing items with the same base code and append a number if necessary
|
||||
existing_codes = frappe.get_all("Item", filters={"item_code": ["like", f"{base_code}-%"]}, pluck="item_code")
|
||||
if base_code in existing_codes:
|
||||
suffix = 1
|
||||
while f"{base_code}-{suffix}" in existing_codes:
|
||||
suffix += 1
|
||||
final_code = f"{base_code}-{suffix}"
|
||||
else:
|
||||
final_code = base_code
|
||||
print(f"DEBUG: Built item code: {final_code}")
|
||||
return final_code
|
||||
12
custom_ui/services/project_service.py
Normal file
12
custom_ui/services/project_service.py
Normal file
@ -0,0 +1,12 @@
|
||||
import frappe
|
||||
|
||||
class ProjectService:
|
||||
|
||||
@staticmethod
|
||||
def get_project_item_groups(project_template: str) -> list[str]:
|
||||
"""Retrieve item groups associated with a given project template."""
|
||||
print(f"DEBUG: Getting item groups for Project Template {project_template}")
|
||||
item_groups_str = frappe.db.get_value("Project Template", project_template, "item_groups") or ""
|
||||
item_groups = [item_group.strip() for item_group in item_groups_str.split(",") if item_group.strip()]
|
||||
print(f"DEBUG: Retrieved item groups: {item_groups}")
|
||||
return item_groups
|
||||
@ -19,6 +19,9 @@ const FRAPPE_GET_ESTIMATE_TEMPLATES_METHOD = "custom_ui.api.db.estimates.get_est
|
||||
const FRAPPE_CREATE_ESTIMATE_TEMPLATE_METHOD = "custom_ui.api.db.estimates.create_estimate_template";
|
||||
const FRAPPE_GET_UNAPPROVED_ESTIMATES_COUNT_METHOD = "custom_ui.api.db.estimates.get_unapproved_estimates_count";
|
||||
const FRAPPE_GET_ESTIMATES_HALF_DOWN_COUNT_METHOD = "custom_ui.api.db.estimates.get_estimates_half_down_count";
|
||||
// Item methods
|
||||
const FRAPPE_SAVE_AS_PACKAGE_ITEM_METHOD = "custom_ui.api.db.items.save_as_package_item";
|
||||
const FRAPPE_GET_ITEMS_BY_PROJECT_TEMPLATE_METHOD = "custom_ui.api.db.items.get_by_project_template";
|
||||
// Job methods
|
||||
const FRAPPE_GET_JOB_METHOD = "custom_ui.api.db.jobs.get_job";
|
||||
const FRAPPE_GET_JOBS_METHOD = "custom_ui.api.db.jobs.get_jobs_table_data";
|
||||
@ -657,6 +660,22 @@ class Api {
|
||||
return await this.request(FRAPPE_GET_ADDRESSES_METHOD, { fields, filters });
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ITEM/PACKAGE METHODS
|
||||
// ============================================================================
|
||||
|
||||
static async getItemsByProjectTemplate(projectTemplate) {
|
||||
return await this.request(FRAPPE_GET_ITEMS_BY_PROJECT_TEMPLATE_METHOD, { projectTemplate });
|
||||
}
|
||||
|
||||
static async saveAsPackageItem(data) {
|
||||
return await this.request(FRAPPE_SAVE_AS_PACKAGE_ITEM_METHOD, { data });
|
||||
}
|
||||
|
||||
static async getItemCategories() {
|
||||
return await this.request("custom_ui.api.db.items.get_item_categories");
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SERVICE / ROUTE / TIMESHEET METHODS
|
||||
// ============================================================================
|
||||
|
||||
@ -4,17 +4,17 @@
|
||||
<i class="pi pi-inbox"></i>
|
||||
<p>{{ emptyMessage }}</p>
|
||||
</div>
|
||||
<div v-else v-for="item in items" :key="item.itemCode" class="item-card" :class="{ 'item-selected': item._selected }" @click="handleItemClick(item, $event)">
|
||||
<div v-else v-for="item in items" :key="item.itemCode" class="item-card" :class="{ 'item-selected': isItemSelected(item.itemCode) }" @click="handleItemClick(item, $event)">
|
||||
<div class="item-card-header">
|
||||
<span class="item-code">{{ item.itemCode }}</span>
|
||||
<span class="item-name">{{ item.itemName }}</span>
|
||||
<span class="item-price">${{ item.standardRate?.toFixed(2) || '0.00' }}</span>
|
||||
<Button
|
||||
:label="item._selected ? 'Selected' : 'Select'"
|
||||
:icon="item._selected ? 'pi pi-check' : 'pi pi-plus'"
|
||||
:label="isItemSelected(item.itemCode) ? 'Selected' : 'Select'"
|
||||
:icon="isItemSelected(item.itemCode) ? 'pi pi-check' : 'pi pi-plus'"
|
||||
@click.stop="handleItemClick(item, $event)"
|
||||
size="small"
|
||||
:severity="item._selected ? 'success' : 'secondary'"
|
||||
:severity="isItemSelected(item.itemCode) ? 'success' : 'secondary'"
|
||||
class="select-item-button"
|
||||
/>
|
||||
</div>
|
||||
@ -26,7 +26,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
import { ref, watch, computed, shallowRef } from "vue";
|
||||
import Button from "primevue/button";
|
||||
|
||||
const props = defineProps({
|
||||
@ -35,6 +35,10 @@ const props = defineProps({
|
||||
required: true,
|
||||
default: () => []
|
||||
},
|
||||
selectedItems: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
emptyMessage: {
|
||||
type: String,
|
||||
default: "No items found in this category"
|
||||
@ -43,18 +47,36 @@ const props = defineProps({
|
||||
|
||||
const emit = defineEmits(['select']);
|
||||
|
||||
const selectedItems = ref([]);
|
||||
const internalSelection = ref([]);
|
||||
const selectionSet = shallowRef(new Set());
|
||||
|
||||
// Sync internal selection with prop
|
||||
watch(() => props.selectedItems, (newVal) => {
|
||||
internalSelection.value = [...newVal];
|
||||
selectionSet.value = new Set(newVal.map(item => item.itemCode));
|
||||
}, { immediate: true });
|
||||
|
||||
const isItemSelected = (itemCode) => {
|
||||
return selectionSet.value.has(itemCode);
|
||||
};
|
||||
|
||||
const handleItemClick = (item, event) => {
|
||||
// Always multi-select mode - toggle item in selection
|
||||
const index = selectedItems.value.findIndex(i => i.itemCode === item.itemCode);
|
||||
const index = internalSelection.value.findIndex(i => i.itemCode === item.itemCode);
|
||||
const newSet = new Set(selectionSet.value);
|
||||
|
||||
if (index >= 0) {
|
||||
selectedItems.value.splice(index, 1);
|
||||
internalSelection.value.splice(index, 1);
|
||||
newSet.delete(item.itemCode);
|
||||
} else {
|
||||
selectedItems.value.push(item);
|
||||
internalSelection.value.push(item);
|
||||
newSet.add(item.itemCode);
|
||||
}
|
||||
|
||||
// Update Set directly instead of recreating from array
|
||||
selectionSet.value = newSet;
|
||||
// Emit the entire selection array
|
||||
emit('select', [...selectedItems.value]);
|
||||
emit('select', [...internalSelection.value]);
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@ -25,15 +25,13 @@
|
||||
<div class="tabs-container">
|
||||
<Tabs v-model="activeItemTab" v-if="itemGroups.length > 0 || packageGroups.length > 0">
|
||||
<TabList>
|
||||
<Tab v-if="packageGroups.length > 0" value="Packages">
|
||||
<i class="pi pi-box"></i>
|
||||
<span>Packages</span>
|
||||
</Tab>
|
||||
<Tab v-for="group in itemGroups" :key="group" :value="group">{{ group }}</Tab>
|
||||
<Tab v-if="packageGroups.length > 0" value="Packages">Packages</Tab>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<!-- Regular category tabs -->
|
||||
<TabPanel v-for="group in itemGroups" :key="group" :value="group">
|
||||
<ItemSelector :items="getFilteredItemsForGroup(group)" @select="handleItemSelection" />
|
||||
</TabPanel>
|
||||
|
||||
<!-- Packages tab with nested sub-tabs -->
|
||||
<TabPanel v-if="packageGroups.length > 0" value="Packages">
|
||||
<Tabs v-model="activePackageTab" class="nested-tabs">
|
||||
@ -64,12 +62,10 @@
|
||||
class="add-package-button"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="isPackageExpanded(item.itemCode) && item.bom" class="bom-details">
|
||||
<div v-if="isPackageExpanded(item.itemCode) && item.bom && item.bom.items" class="bom-details">
|
||||
<div class="bom-header">Bill of Materials:</div>
|
||||
<div v-for="bomItem in (item.bom.items || [])" :key="bomItem.itemCode" class="bom-item">
|
||||
<span class="bom-item-code">{{ bomItem.itemCode }}</span>
|
||||
<span class="bom-item-name">{{ bomItem.itemName }}</span>
|
||||
<span class="bom-item-qty">Qty: {{ bomItem.qty }}</span>
|
||||
<div v-for="bomItem in item.bom.items" :key="bomItem.itemCode" class="bom-item-wrapper">
|
||||
<BomItem :item="bomItem" :parentPath="item.itemCode" :level="0" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -78,6 +74,14 @@
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</TabPanel>
|
||||
<!-- Regular category tabs -->
|
||||
<TabPanel v-for="group in itemGroups" :key="group" :value="group">
|
||||
<ItemSelector
|
||||
:items="getFilteredItemsForGroup(group)"
|
||||
:selected-items="getSelectedItemsForGroup(group)"
|
||||
@select="handleItemSelection"
|
||||
/>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
<!-- Fallback if no categories -->
|
||||
@ -102,7 +106,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from "vue";
|
||||
import { ref, computed, watch, defineComponent, h, shallowRef } from "vue";
|
||||
import Modal from "../common/Modal.vue";
|
||||
import ItemSelector from "../common/ItemSelector.vue";
|
||||
import InputText from "primevue/inputtext";
|
||||
@ -127,8 +131,73 @@ const props = defineProps({
|
||||
const emit = defineEmits(['update:visible', 'add-items']);
|
||||
|
||||
const searchTerm = ref("");
|
||||
const expandedPackageItems = ref(new Set());
|
||||
const selectedItemsInModal = ref(new Set());
|
||||
const expandedPackageItems = shallowRef(new Set());
|
||||
const selectedItemsInModal = shallowRef(new Set());
|
||||
|
||||
// BomItem component for recursive rendering
|
||||
const BomItem = defineComponent({
|
||||
name: 'BomItem',
|
||||
props: {
|
||||
item: Object,
|
||||
parentPath: String,
|
||||
level: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
const itemPath = computed(() => {
|
||||
return props.parentPath ? `${props.parentPath}.${props.item.itemCode}` : props.item.itemCode;
|
||||
});
|
||||
|
||||
const isPackage = computed(() => {
|
||||
return props.item.bom && props.item.bom.items && props.item.bom.items.length > 0;
|
||||
});
|
||||
|
||||
const isExpanded = computed(() => {
|
||||
return expandedPackageItems.value.has(itemPath.value);
|
||||
});
|
||||
|
||||
const toggleExpansion = () => {
|
||||
if (expandedPackageItems.value.has(itemPath.value)) {
|
||||
expandedPackageItems.value.delete(itemPath.value);
|
||||
} else {
|
||||
expandedPackageItems.value.add(itemPath.value);
|
||||
}
|
||||
expandedPackageItems.value = new Set(expandedPackageItems.value);
|
||||
};
|
||||
|
||||
return () => h('div', {
|
||||
class: 'bom-item',
|
||||
style: { paddingLeft: `${props.level * 1}rem` }
|
||||
}, [
|
||||
h('div', { class: 'bom-item-content' }, [
|
||||
isPackage.value ? h(Button, {
|
||||
icon: isExpanded.value ? 'pi pi-chevron-down' : 'pi pi-chevron-right',
|
||||
onClick: toggleExpansion,
|
||||
text: true,
|
||||
rounded: true,
|
||||
size: 'small',
|
||||
class: 'bom-expand-button'
|
||||
}) : h('i', { class: 'pi pi-circle-fill bom-item-bullet' }),
|
||||
isPackage.value ? h('i', { class: 'pi pi-box package-icon' }) : null,
|
||||
h('span', { class: 'bom-item-code' }, props.item.itemCode),
|
||||
h('span', { class: 'bom-item-name' }, props.item.itemName),
|
||||
h('span', { class: 'bom-item-qty' }, `Qty: ${props.item.qty}`)
|
||||
]),
|
||||
isPackage.value && isExpanded.value && props.item.bom?.items ? h('div', { class: 'nested-bom' },
|
||||
props.item.bom.items.map(nestedItem =>
|
||||
h(BomItem, {
|
||||
key: nestedItem.itemCode,
|
||||
item: nestedItem,
|
||||
parentPath: itemPath.value,
|
||||
level: props.level + 1
|
||||
})
|
||||
)
|
||||
) : null
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
const itemGroups = computed(() => {
|
||||
if (!props.quotationItems || typeof props.quotationItems !== 'object') return [];
|
||||
@ -142,9 +211,9 @@ const packageGroups = computed(() => {
|
||||
return Object.keys(props.quotationItems.Packages).sort();
|
||||
});
|
||||
|
||||
// Active tabs with default to first category
|
||||
// Active tabs with default to Packages
|
||||
const activeItemTab = computed({
|
||||
get: () => _activeItemTab.value || itemGroups.value[0] || "",
|
||||
get: () => _activeItemTab.value || (packageGroups.value.length > 0 ? "Packages" : itemGroups.value[0]) || "",
|
||||
set: (val) => { _activeItemTab.value = val; }
|
||||
});
|
||||
|
||||
@ -176,13 +245,19 @@ const getFilteredItemsForGroup = (group) => {
|
||||
);
|
||||
}
|
||||
|
||||
return items.map((item) => ({
|
||||
...item,
|
||||
id: item.itemCode,
|
||||
_selected: selectedItemsInModal.value.has(item.itemCode)
|
||||
// Map items and mark those that are selected
|
||||
return items.map((item) => ({
|
||||
...item,
|
||||
id: item.itemCode
|
||||
}));
|
||||
};
|
||||
|
||||
const getSelectedItemsForGroup = (group) => {
|
||||
if (selectedItemsInModal.value.size === 0) return [];
|
||||
const allItems = getFilteredItemsForGroup(group);
|
||||
return allItems.filter(item => selectedItemsInModal.value.has(item.itemCode));
|
||||
};
|
||||
|
||||
const getFilteredPackageItemsForGroup = (packageGroup) => {
|
||||
if (!props.quotationItems?.Packages || typeof props.quotationItems.Packages !== 'object') return [];
|
||||
|
||||
@ -213,13 +288,13 @@ const getFilteredPackageItemsForGroup = (packageGroup) => {
|
||||
const selectedItemsCount = computed(() => selectedItemsInModal.value.size);
|
||||
|
||||
const togglePackageExpansion = (itemCode) => {
|
||||
if (expandedPackageItems.value.has(itemCode)) {
|
||||
expandedPackageItems.value.delete(itemCode);
|
||||
const newExpanded = new Set(expandedPackageItems.value);
|
||||
if (newExpanded.has(itemCode)) {
|
||||
newExpanded.delete(itemCode);
|
||||
} else {
|
||||
expandedPackageItems.value.add(itemCode);
|
||||
newExpanded.add(itemCode);
|
||||
}
|
||||
// Force reactivity
|
||||
expandedPackageItems.value = new Set(expandedPackageItems.value);
|
||||
expandedPackageItems.value = newExpanded;
|
||||
};
|
||||
|
||||
const isPackageExpanded = (itemCode) => {
|
||||
@ -229,17 +304,31 @@ const isPackageExpanded = (itemCode) => {
|
||||
const handleItemSelection = (itemOrRows) => {
|
||||
// Handle both single item (from package cards) and array (from DataTable)
|
||||
if (Array.isArray(itemOrRows)) {
|
||||
// From DataTable - replace selection with new array
|
||||
selectedItemsInModal.value = new Set(itemOrRows.map(row => row.itemCode));
|
||||
// From ItemSelector - merge with existing selection
|
||||
const newSelection = new Set(selectedItemsInModal.value);
|
||||
const itemCodes = itemOrRows.map(row => row.itemCode);
|
||||
|
||||
// Check if all items are already selected
|
||||
const allSelected = itemCodes.every(code => newSelection.has(code));
|
||||
|
||||
if (allSelected) {
|
||||
// Deselect all items
|
||||
itemCodes.forEach(code => newSelection.delete(code));
|
||||
} else {
|
||||
// Select all items
|
||||
itemCodes.forEach(code => newSelection.add(code));
|
||||
}
|
||||
|
||||
selectedItemsInModal.value = newSelection;
|
||||
} else {
|
||||
// From package card - toggle single item
|
||||
if (selectedItemsInModal.value.has(itemOrRows.itemCode)) {
|
||||
selectedItemsInModal.value.delete(itemOrRows.itemCode);
|
||||
const newSelection = new Set(selectedItemsInModal.value);
|
||||
if (newSelection.has(itemOrRows.itemCode)) {
|
||||
newSelection.delete(itemOrRows.itemCode);
|
||||
} else {
|
||||
selectedItemsInModal.value.add(itemOrRows.itemCode);
|
||||
newSelection.add(itemOrRows.itemCode);
|
||||
}
|
||||
// Force reactivity
|
||||
selectedItemsInModal.value = new Set(selectedItemsInModal.value);
|
||||
selectedItemsInModal.value = newSelection;
|
||||
}
|
||||
};
|
||||
|
||||
@ -378,6 +467,12 @@ watch(() => props.visible, (newVal) => {
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.modal-title-container :deep(.p-tab) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.selection-badge {
|
||||
background-color: #2196f3;
|
||||
color: white;
|
||||
@ -495,18 +590,40 @@ watch(() => props.visible, (newVal) => {
|
||||
}
|
||||
|
||||
.bom-item {
|
||||
display: grid;
|
||||
grid-template-columns: 120px 1fr 100px;
|
||||
gap: 1rem;
|
||||
padding: 0.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.bom-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.bom-item-content {
|
||||
display: grid;
|
||||
grid-template-columns: 32px 24px 120px 1fr 100px;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.bom-expand-button {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.bom-item-bullet {
|
||||
font-size: 0.4rem;
|
||||
color: #ccc;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.package-icon {
|
||||
color: #2196f3;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.bom-item-code {
|
||||
font-family: monospace;
|
||||
color: #666;
|
||||
@ -523,4 +640,10 @@ watch(() => props.visible, (newVal) => {
|
||||
font-size: 0.85rem;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.nested-bom {
|
||||
background-color: #fafafa;
|
||||
border-left: 2px solid #e0e0e0;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
382
frontend/src/components/modals/SavePackageModal.vue
Normal file
382
frontend/src/components/modals/SavePackageModal.vue
Normal file
@ -0,0 +1,382 @@
|
||||
<template>
|
||||
<Modal
|
||||
:visible="visible"
|
||||
@update:visible="$emit('update:visible', $event)"
|
||||
@close="handleClose"
|
||||
:options="{ showActions: false }"
|
||||
>
|
||||
<template #title>Save as Package</template>
|
||||
<div class="modal-content">
|
||||
<div class="form-section">
|
||||
<label for="packageName" class="field-label">
|
||||
Package Name
|
||||
<span class="required">*</span>
|
||||
</label>
|
||||
<InputText
|
||||
id="packageName"
|
||||
v-model="formData.packageName"
|
||||
placeholder="Enter package name"
|
||||
fluid
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-section">
|
||||
<label for="description" class="field-label">
|
||||
Description
|
||||
</label>
|
||||
<InputText
|
||||
id="description"
|
||||
v-model="formData.description"
|
||||
placeholder="Enter package description (optional)"
|
||||
fluid
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-section">
|
||||
<label for="codePrefix" class="field-label">
|
||||
Code Prefix
|
||||
<span class="required">*</span>
|
||||
</label>
|
||||
<Select
|
||||
id="codePrefix"
|
||||
v-model="formData.codePrefix"
|
||||
:options="codePrefixOptions"
|
||||
placeholder="Select a code prefix"
|
||||
fluid
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-section">
|
||||
<label for="category" class="field-label">
|
||||
Category
|
||||
<span class="required">*</span>
|
||||
</label>
|
||||
<Select
|
||||
id="category"
|
||||
v-model="formData.category"
|
||||
:options="categories"
|
||||
placeholder="Select a category"
|
||||
fluid
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-section">
|
||||
<label for="rate" class="field-label">
|
||||
Rate
|
||||
<span class="required">*</span>
|
||||
</label>
|
||||
<InputNumber
|
||||
id="rate"
|
||||
v-model="formData.rate"
|
||||
mode="currency"
|
||||
currency="USD"
|
||||
locale="en-US"
|
||||
:min="0"
|
||||
placeholder="$0.00"
|
||||
fluid
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-section">
|
||||
<h4>Package Contents</h4>
|
||||
<div v-if="items.length === 0" class="no-items">
|
||||
No items selected
|
||||
</div>
|
||||
<div v-else class="items-list">
|
||||
<div
|
||||
v-for="(item, index) in items"
|
||||
:key="index"
|
||||
class="package-item"
|
||||
>
|
||||
<div class="item-header" @click="toggleItemExpansion(index)">
|
||||
<div class="item-info">
|
||||
<i
|
||||
v-if="isPackage(item)"
|
||||
:class="[
|
||||
'pi',
|
||||
expandedItems.has(index) ? 'pi-chevron-down' : 'pi-chevron-right',
|
||||
'expand-icon'
|
||||
]"
|
||||
></i>
|
||||
<span class="item-name">{{ item.itemName || item.itemCode }}</span>
|
||||
<span v-if="isPackage(item)" class="package-badge">Package</span>
|
||||
</div>
|
||||
<span class="item-qty">Qty: {{ item.qty || 1 }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="isPackage(item) && expandedItems.has(index)"
|
||||
class="package-contents"
|
||||
>
|
||||
<div
|
||||
v-for="(bomItem, bomIndex) in item.bom"
|
||||
:key="bomIndex"
|
||||
class="bom-item"
|
||||
>
|
||||
<span class="bom-item-name">{{ bomItem.itemName || bomItem.itemCode }}</span>
|
||||
<span class="bom-item-qty">Qty: {{ bomItem.qty || 1 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="action-buttons">
|
||||
<Button label="Cancel" @click="handleClose" severity="secondary" />
|
||||
<Button
|
||||
label="Save Package"
|
||||
@click="handleSave"
|
||||
:disabled="!isFormValid"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, watch } from "vue";
|
||||
import Modal from "../common/Modal.vue";
|
||||
import InputText from "primevue/inputtext";
|
||||
import InputNumber from "primevue/inputnumber";
|
||||
import Button from "primevue/button";
|
||||
import Select from "primevue/select";
|
||||
import Api from "../../api";
|
||||
import { useNotificationStore } from "../../stores/notifications-primevue";
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
defaultRate: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:visible", "save"]);
|
||||
|
||||
const notificationStore = useNotificationStore();
|
||||
|
||||
const formData = reactive({
|
||||
packageName: "",
|
||||
description: "",
|
||||
codePrefix: null,
|
||||
category: null,
|
||||
rate: null,
|
||||
});
|
||||
|
||||
const codePrefixOptions = ref(["BLDR", "SNW-I"]);
|
||||
const categories = ref([]);
|
||||
const expandedItems = ref(new Set());
|
||||
const isLoading = ref(false);
|
||||
|
||||
const isFormValid = computed(() => {
|
||||
return formData.packageName.trim() !== "" &&
|
||||
formData.codePrefix !== null &&
|
||||
formData.category !== null &&
|
||||
formData.rate !== null &&
|
||||
formData.rate > 0;
|
||||
});
|
||||
|
||||
const isPackage = (item) => {
|
||||
return item.bom && Array.isArray(item.bom) && item.bom.length > 0;
|
||||
};
|
||||
|
||||
const toggleItemExpansion = (index) => {
|
||||
const item = props.items[index];
|
||||
if (!isPackage(item)) return;
|
||||
|
||||
if (expandedItems.value.has(index)) {
|
||||
expandedItems.value.delete(index);
|
||||
} else {
|
||||
expandedItems.value.add(index);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchCategories = async () => {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
const result = await Api.getItemCategories();
|
||||
categories.value = result || [];
|
||||
} catch (error) {
|
||||
console.error("Error fetching item categories:", error);
|
||||
notificationStore.addNotification("Failed to fetch item categories", "error");
|
||||
categories.value = [];
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
// Reset form
|
||||
formData.packageName = "";
|
||||
formData.description = "";
|
||||
formData.codePrefix = null;
|
||||
formData.category = null;
|
||||
formData.rate = null;
|
||||
expandedItems.value.clear();
|
||||
emit("update:visible", false);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
if (!isFormValid.value) {
|
||||
notificationStore.addNotification("Please fill in all required fields", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
const packageData = {
|
||||
packageName: formData.packageName,
|
||||
description: formData.description,
|
||||
codePrefix: formData.codePrefix,
|
||||
category: formData.category,
|
||||
rate: formData.rate,
|
||||
items: props.items.map(item => ({
|
||||
itemCode: item.itemCode,
|
||||
itemName: item.itemName,
|
||||
qty: item.qty || 1,
|
||||
uom: item.uom || item.stockUom,
|
||||
})),
|
||||
};
|
||||
|
||||
emit("save", packageData);
|
||||
handleClose();
|
||||
};
|
||||
|
||||
// Watch for modal opening to fetch categories
|
||||
watch(
|
||||
() => props.visible,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
fetchCategories();
|
||||
// Set rate to defaultRate when modal opens
|
||||
if (props.defaultRate > 0) {
|
||||
formData.rate = props.defaultRate;
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.modal-content {
|
||||
padding: 1.5rem;
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.field-label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.required {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.no-items {
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.items-list {
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.package-item {
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.package-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.item-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.75rem 1rem;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.item-header:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.item-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.expand-icon {
|
||||
font-size: 0.8rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.item-name {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.package-badge {
|
||||
display: inline-block;
|
||||
padding: 0.2rem 0.5rem;
|
||||
background-color: #e3f2fd;
|
||||
color: #1976d2;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.item-qty {
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.package-contents {
|
||||
background-color: #f8f9fa;
|
||||
padding: 0.5rem 1rem 0.5rem 2.5rem;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.bom-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0.5rem 0;
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.bom-item-name {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.bom-item-qty {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: flex-end;
|
||||
margin-top: 1.5rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
}
|
||||
</style>
|
||||
@ -67,43 +67,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Template Section -->
|
||||
<div class="template-section">
|
||||
<div v-if="isNew">
|
||||
<label for="template" class="field-label">
|
||||
From Template
|
||||
<i class="pi pi-question-circle help-icon" v-tooltip.right="'Pre-fills estimate items and sets default Project Template. Serves as a starting point for this estimate.'"></i>
|
||||
</label>
|
||||
<div class="template-input-group">
|
||||
<Select
|
||||
v-model="selectedTemplate"
|
||||
:options="templateOptions"
|
||||
optionLabel="templateName"
|
||||
optionValue="name"
|
||||
placeholder="Select a template"
|
||||
fluid
|
||||
@change="onTemplateChange"
|
||||
>
|
||||
<template #option="slotProps">
|
||||
<div class="template-option">
|
||||
<div class="template-name">{{ slotProps.option.templateName }}</div>
|
||||
<div class="template-desc">{{ slotProps.option.description }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</Select>
|
||||
<Button
|
||||
v-if="selectedTemplate"
|
||||
icon="pi pi-times"
|
||||
@click="clearTemplate"
|
||||
class="clear-button"
|
||||
severity="secondary"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<Button label="Save As Template" icon="pi pi-save" @click="openSaveTemplateModal" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Project Template Section -->
|
||||
<div class="project-template-section">
|
||||
@ -118,102 +82,137 @@
|
||||
optionLabel="name"
|
||||
optionValue="name"
|
||||
placeholder="Select a project template"
|
||||
:disabled="!isEditable || isProjectTemplateDisabled"
|
||||
:disabled="!isEditable"
|
||||
fluid
|
||||
/>
|
||||
<div v-if="isLoadingQuotationItems" class="loading-message">
|
||||
<i class="pi pi-spin pi-spinner"></i>
|
||||
<span>Loading available items...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Items Section -->
|
||||
<div class="items-section">
|
||||
<h3>Items</h3>
|
||||
<Button
|
||||
v-if="isEditable"
|
||||
label="Add Item"
|
||||
icon="pi pi-plus"
|
||||
@click="showAddItemModal = true"
|
||||
:disabled="!quotationItems || Object.keys(quotationItems).length === 0"
|
||||
/>
|
||||
<div v-for="(item, index) in selectedItems" :key="item.itemCode" class="item-row">
|
||||
<span>{{ item.itemName }}</span>
|
||||
<div class="input-wrapper">
|
||||
<span class="input-label">Quantity</span>
|
||||
<InputNumber
|
||||
v-model="item.qty"
|
||||
:min="1"
|
||||
:disabled="!isEditable"
|
||||
showButtons
|
||||
buttonLayout="horizontal"
|
||||
@input="onQtyChange(item)"
|
||||
class="qty-input"
|
||||
<div class="items-header">
|
||||
<h3>Items</h3>
|
||||
<div v-if="isEditable" class="items-actions">
|
||||
<Button
|
||||
label="Add Item"
|
||||
icon="pi pi-plus"
|
||||
@click="showAddItemModal = true"
|
||||
:disabled="!hasQuotationItems || isLoadingQuotationItems"
|
||||
:loading="isLoadingQuotationItems"
|
||||
/>
|
||||
<Button
|
||||
label="Save as Package"
|
||||
icon="pi pi-save"
|
||||
@click="showSavePackageModal = true"
|
||||
:disabled="selectedItems.length === 0"
|
||||
severity="secondary"
|
||||
/>
|
||||
</div>
|
||||
<span>X</span>
|
||||
<div class="input-wrapper">
|
||||
<span class="input-label">Rate</span>
|
||||
<InputNumber
|
||||
v-model="item.rate"
|
||||
mode="currency"
|
||||
currency="USD"
|
||||
locale="en-US"
|
||||
:min="0"
|
||||
:disabled="!isEditable"
|
||||
@input="onRateChange(item)"
|
||||
class="rate-input"
|
||||
/>
|
||||
</div>
|
||||
<div class="input-wrapper">
|
||||
<span class="input-label">Discount</span>
|
||||
<div class="discount-container">
|
||||
<div class="discount-input-wrapper">
|
||||
<InputNumber
|
||||
v-if="item.discountType === 'currency'"
|
||||
v-model="item.discountAmount"
|
||||
mode="currency"
|
||||
currency="USD"
|
||||
locale="en-US"
|
||||
:min="0"
|
||||
:disabled="!isEditable"
|
||||
@input="updateDiscountFromAmount(item)"
|
||||
placeholder="$0.00"
|
||||
class="discount-input"
|
||||
/>
|
||||
<InputNumber
|
||||
v-else
|
||||
v-model="item.discountPercentage"
|
||||
suffix="%"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:disabled="!isEditable"
|
||||
@input="updateDiscountFromPercentage(item)"
|
||||
placeholder="0%"
|
||||
class="discount-input"
|
||||
/>
|
||||
</div>
|
||||
<div class="discount-toggle">
|
||||
<Button
|
||||
icon="pi pi-dollar"
|
||||
class="p-button-sm p-button-outlined"
|
||||
:class="{ 'p-button-secondary': item.discountType !== 'currency' }"
|
||||
@click="toggleDiscountType(item, 'currency')"
|
||||
:disabled="!isEditable"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-percentage"
|
||||
class="p-button-sm p-button-outlined"
|
||||
:class="{ 'p-button-secondary': item.discountType !== 'percentage' }"
|
||||
@click="toggleDiscountType(item, 'percentage')"
|
||||
:disabled="!isEditable"
|
||||
/>
|
||||
</div>
|
||||
<div v-for="(item, index) in selectedItems" :key="item.itemCode" class="item-container">
|
||||
<div class="item-row">
|
||||
<div class="item-name-wrapper">
|
||||
<Button
|
||||
v-if="isPackageItem(item)"
|
||||
:icon="isItemExpanded(item.itemCode) ? 'pi pi-chevron-down' : 'pi pi-chevron-right'"
|
||||
text
|
||||
rounded
|
||||
size="small"
|
||||
@click="toggleItemExpansion(item.itemCode)"
|
||||
class="expand-button"
|
||||
/>
|
||||
<i v-if="isPackageItem(item)" class="pi pi-box package-icon"></i>
|
||||
<i v-else class="pi pi-circle-fill item-bullet"></i>
|
||||
<span class="item-name">{{ item.itemName }}</span>
|
||||
</div>
|
||||
<div class="input-wrapper">
|
||||
<span class="input-label">Quantity</span>
|
||||
<InputNumber
|
||||
v-model="item.qty"
|
||||
:min="1"
|
||||
:disabled="!isEditable"
|
||||
showButtons
|
||||
buttonLayout="horizontal"
|
||||
@input="onQtyChange(item)"
|
||||
class="qty-input"
|
||||
/>
|
||||
</div>
|
||||
<span>X</span>
|
||||
<div class="input-wrapper">
|
||||
<span class="input-label">Rate</span>
|
||||
<InputNumber
|
||||
v-model="item.rate"
|
||||
mode="currency"
|
||||
currency="USD"
|
||||
locale="en-US"
|
||||
:min="0"
|
||||
:disabled="!isEditable"
|
||||
@input="onRateChange(item)"
|
||||
class="rate-input"
|
||||
/>
|
||||
</div>
|
||||
<div class="input-wrapper">
|
||||
<span class="input-label">Discount</span>
|
||||
<div class="discount-container">
|
||||
<div class="discount-input-wrapper">
|
||||
<InputNumber
|
||||
v-if="item.discountType === 'currency'"
|
||||
v-model="item.discountAmount"
|
||||
mode="currency"
|
||||
currency="USD"
|
||||
locale="en-US"
|
||||
:min="0"
|
||||
:disabled="!isEditable"
|
||||
@input="updateDiscountFromAmount(item)"
|
||||
placeholder="$0.00"
|
||||
class="discount-input"
|
||||
/>
|
||||
<InputNumber
|
||||
v-else
|
||||
v-model="item.discountPercentage"
|
||||
suffix="%"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:disabled="!isEditable"
|
||||
@input="updateDiscountFromPercentage(item)"
|
||||
placeholder="0%"
|
||||
class="discount-input"
|
||||
/>
|
||||
</div>
|
||||
<div class="discount-toggle">
|
||||
<Button
|
||||
icon="pi pi-dollar"
|
||||
class="p-button-sm p-button-outlined"
|
||||
:class="{ 'p-button-secondary': item.discountType !== 'currency' }"
|
||||
@click="toggleDiscountType(item, 'currency')"
|
||||
:disabled="!isEditable"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-percentage"
|
||||
class="p-button-sm p-button-outlined"
|
||||
:class="{ 'p-button-secondary': item.discountType !== 'percentage' }"
|
||||
@click="toggleDiscountType(item, 'percentage')"
|
||||
:disabled="!isEditable"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span>Total: ${{ ((item.qty || 0) * (item.rate || 0) - (item.discountAmount || 0)).toFixed(2) }}</span>
|
||||
<Button
|
||||
v-if="isEditable"
|
||||
icon="pi pi-trash"
|
||||
@click="removeItem(index)"
|
||||
severity="danger"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="isPackageItem(item) && isItemExpanded(item.itemCode)" class="nested-items">
|
||||
<div v-for="bomItem in item.bom.items" :key="bomItem.itemCode">
|
||||
<BomItem :item="bomItem" :parentPath="item.itemCode" :level="0" />
|
||||
</div>
|
||||
</div>
|
||||
<span>Total: ${{ ((item.qty || 0) * (item.rate || 0) - (item.discountAmount || 0)).toFixed(2) }}</span>
|
||||
<Button
|
||||
v-if="isEditable"
|
||||
icon="pi pi-trash"
|
||||
@click="removeItem(index)"
|
||||
severity="danger"
|
||||
/>
|
||||
</div>
|
||||
<div class="total-section">
|
||||
<strong>Total Cost: ${{ totalCost.toFixed(2) }}</strong>
|
||||
@ -301,6 +300,14 @@
|
||||
:quotation-items="quotationItems"
|
||||
@add-items="addSelectedItems"
|
||||
/>
|
||||
<!-- Save Package Modal -->
|
||||
<SavePackageModal
|
||||
:visible="showSavePackageModal"
|
||||
@update:visible="showSavePackageModal = $event"
|
||||
:items="selectedItems"
|
||||
:default-rate="totalCost"
|
||||
@save="handleSavePackage"
|
||||
/>
|
||||
<!-- Down Payment Warning Modal -->
|
||||
<Modal
|
||||
:visible="showDownPaymentWarningModal"
|
||||
@ -400,11 +407,12 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted, watch } from "vue";
|
||||
import { ref, reactive, computed, onMounted, watch, defineComponent, h } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import Modal from "../common/Modal.vue";
|
||||
import SaveTemplateModal from "../modals/SaveTemplateModal.vue";
|
||||
import AddItemModal from "../modals/AddItemModal.vue";
|
||||
import SavePackageModal from "../modals/SavePackageModal.vue";
|
||||
import DataTable from "../common/DataTable.vue";
|
||||
import DocHistory from "../common/DocHistory.vue";
|
||||
import BidMeetingNotes from "../modals/BidMeetingNotes.vue";
|
||||
@ -456,12 +464,10 @@ const estimateResponse = ref(null);
|
||||
const estimateResponseSelection = ref(null);
|
||||
const contacts = ref([]);
|
||||
const contactOptions = ref([]);
|
||||
const quotationItems = ref([]);
|
||||
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);
|
||||
const showAddItemModal = ref(false);
|
||||
@ -469,12 +475,78 @@ const showConfirmationModal = ref(false);
|
||||
const showDownPaymentWarningModal = ref(false);
|
||||
const showResponseModal = ref(false);
|
||||
const showSaveTemplateModal = ref(false);
|
||||
const showSavePackageModal = ref(false);
|
||||
const addressSearchResults = ref([]);
|
||||
const showDrawer = ref(false);
|
||||
const isLoadingQuotationItems = ref(false);
|
||||
const expandedSelectedItems = ref(new Set());
|
||||
|
||||
const estimate = ref(null);
|
||||
const bidMeeting = ref(null);
|
||||
|
||||
// BomItem component for recursive rendering of nested packages
|
||||
const BomItem = defineComponent({
|
||||
name: 'BomItem',
|
||||
props: {
|
||||
item: Object,
|
||||
parentPath: String,
|
||||
level: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
const itemPath = computed(() => {
|
||||
return props.parentPath ? `${props.parentPath}.${props.item.itemCode}` : props.item.itemCode;
|
||||
});
|
||||
|
||||
const isPackage = computed(() => {
|
||||
return props.item.bom && props.item.bom.items && props.item.bom.items.length > 0;
|
||||
});
|
||||
|
||||
const isExpanded = computed(() => {
|
||||
return expandedSelectedItems.value.has(itemPath.value);
|
||||
});
|
||||
|
||||
const toggleExpansion = () => {
|
||||
if (expandedSelectedItems.value.has(itemPath.value)) {
|
||||
expandedSelectedItems.value.delete(itemPath.value);
|
||||
} else {
|
||||
expandedSelectedItems.value.add(itemPath.value);
|
||||
}
|
||||
expandedSelectedItems.value = new Set(expandedSelectedItems.value);
|
||||
};
|
||||
|
||||
return () => h('div', {
|
||||
class: 'nested-item',
|
||||
style: { paddingLeft: `${props.level * 1}rem` }
|
||||
}, [
|
||||
h('div', { class: 'nested-item-content' }, [
|
||||
isPackage.value ? h(Button, {
|
||||
icon: isExpanded.value ? 'pi pi-chevron-down' : 'pi pi-chevron-right',
|
||||
onClick: toggleExpansion,
|
||||
text: true,
|
||||
rounded: true,
|
||||
size: 'small',
|
||||
class: 'nested-expand-button'
|
||||
}) : null,
|
||||
isPackage.value ? h('i', { class: 'pi pi-box package-icon' }) : h('i', { class: 'pi pi-circle-fill item-bullet' }),
|
||||
h('span', { class: 'nested-item-name' }, `${props.item.itemName} (Qty: ${props.item.qty})`)
|
||||
]),
|
||||
isPackage.value && isExpanded.value && props.item.bom?.items ? h('div', { class: 'deeply-nested-items' },
|
||||
props.item.bom.items.map(nestedItem =>
|
||||
h(BomItem, {
|
||||
key: nestedItem.itemCode,
|
||||
item: nestedItem,
|
||||
parentPath: itemPath.value,
|
||||
level: props.level + 1
|
||||
})
|
||||
)
|
||||
) : null
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
// Computed property to determine if fields are editable
|
||||
const isEditable = computed(() => {
|
||||
if (isNew.value) return true;
|
||||
@ -483,15 +555,8 @@ const isEditable = computed(() => {
|
||||
return estimate.value.customSent === 0;
|
||||
});
|
||||
|
||||
const templateOptions = computed(() => {
|
||||
return [
|
||||
{ name: null, templateName: 'None', description: 'Start from scratch' },
|
||||
...templates.value
|
||||
];
|
||||
});
|
||||
|
||||
const isProjectTemplateDisabled = computed(() => {
|
||||
return selectedTemplate.value !== null;
|
||||
const hasQuotationItems = computed(() => {
|
||||
return quotationItems.value && Object.keys(quotationItems.value).length > 0;
|
||||
});
|
||||
|
||||
const itemColumns = [
|
||||
@ -511,87 +576,55 @@ const fetchProjectTemplates = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const fetchTemplates = async () => {
|
||||
if (!isNew.value) return;
|
||||
try {
|
||||
const result = await Api.getEstimateTemplates(company.currentCompany);
|
||||
templates.value = result;
|
||||
|
||||
// Check if template query param exists and set it after templates are loaded
|
||||
const templateParam = route.query.template;
|
||||
if (templateParam) {
|
||||
console.log("DEBUG: Setting template from query param:", templateParam);
|
||||
|
||||
// Find template by name (ID) or templateName (Label)
|
||||
const matchedTemplate = templates.value.find(t =>
|
||||
t.name === templateParam || t.templateName === templateParam
|
||||
);
|
||||
|
||||
if (matchedTemplate) {
|
||||
console.log("DEBUG: Found matched template:", matchedTemplate);
|
||||
selectedTemplate.value = matchedTemplate.name;
|
||||
// Trigger template change to load items and project template
|
||||
onTemplateChange();
|
||||
} else {
|
||||
console.log("DEBUG: No matching template found for param:", templateParam);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching templates:", error);
|
||||
notificationStore.addNotification("Failed to fetch templates", "error");
|
||||
}
|
||||
};
|
||||
|
||||
const onTemplateChange = () => {
|
||||
if (!selectedTemplate.value) {
|
||||
// None selected - clear items and project template
|
||||
selectedItems.value = [];
|
||||
formData.projectTemplate = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const template = templates.value.find(t => t.name === selectedTemplate.value);
|
||||
console.log("DEBUG: Selected template:", template);
|
||||
if (template) {
|
||||
// Auto-select project template if available (check both camelCase and snake_case)
|
||||
const projectTemplateValue = template.projectTemplate || template.project_template;
|
||||
console.log("DEBUG: Project template value from template:", projectTemplateValue);
|
||||
console.log("DEBUG: Available project templates:", projectTemplates.value);
|
||||
if (projectTemplateValue) {
|
||||
formData.projectTemplate = projectTemplateValue;
|
||||
console.log("DEBUG: Set formData.projectTemplate to:", formData.projectTemplate);
|
||||
}
|
||||
|
||||
if (template.items) {
|
||||
selectedItems.value = template.items.map(item => ({
|
||||
itemCode: item.itemCode,
|
||||
itemName: item.itemName,
|
||||
qty: item.quantity,
|
||||
standardRate: item.rate,
|
||||
discountAmount: null,
|
||||
discountPercentage: item.discountPercentage,
|
||||
discountType: item.discountPercentage > 0 ? 'percentage' : 'currency'
|
||||
}));
|
||||
// Calculate discount amounts
|
||||
selectedItems.value.forEach(item => {
|
||||
if (item.discountType === 'percentage') {
|
||||
updateDiscountFromPercentage(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const clearTemplate = () => {
|
||||
selectedTemplate.value = null;
|
||||
selectedItems.value = [];
|
||||
formData.projectTemplate = null;
|
||||
};
|
||||
|
||||
const openSaveTemplateModal = () => {
|
||||
showSaveTemplateModal.value = true;
|
||||
};
|
||||
|
||||
const handleSavePackage = async (packageData) => {
|
||||
try {
|
||||
const newPackageItem = await Api.saveAsPackageItem(packageData);
|
||||
|
||||
// Initialize Packages object if it doesn't exist
|
||||
if (!quotationItems.value.Packages) {
|
||||
quotationItems.value = { ...quotationItems.value, Packages: {} };
|
||||
}
|
||||
|
||||
// Initialize category array if it doesn't exist and add the new package
|
||||
if (!quotationItems.value.Packages[packageData.category]) {
|
||||
quotationItems.value.Packages = {
|
||||
...quotationItems.value.Packages,
|
||||
[packageData.category]: [newPackageItem]
|
||||
};
|
||||
} else {
|
||||
// Add to existing category
|
||||
quotationItems.value.Packages[packageData.category] = [
|
||||
...quotationItems.value.Packages[packageData.category],
|
||||
newPackageItem
|
||||
];
|
||||
}
|
||||
|
||||
// Replace all selected items with just the new package
|
||||
selectedItems.value = [{
|
||||
itemCode: newPackageItem.itemCode || newPackageItem.item_code,
|
||||
itemName: newPackageItem.itemName || newPackageItem.item_name,
|
||||
qty: 1,
|
||||
rate: newPackageItem.standardRate || newPackageItem.standard_rate || packageData.rate,
|
||||
standardRate: newPackageItem.standardRate || newPackageItem.standard_rate || packageData.rate,
|
||||
bom: newPackageItem.bom || null,
|
||||
uom: newPackageItem.uom || newPackageItem.stockUom || newPackageItem.stock_uom || 'Nos',
|
||||
discountAmount: null,
|
||||
discountPercentage: null,
|
||||
discountType: 'currency'
|
||||
}];
|
||||
|
||||
notificationStore.addSuccess("Package saved successfully", "success");
|
||||
showSavePackageModal.value = false;
|
||||
} catch (error) {
|
||||
console.error("Error saving package:", error);
|
||||
notificationStore.addNotification("Failed to save package", "error");
|
||||
}
|
||||
};
|
||||
|
||||
const confirmSaveTemplate = async (templateData) => {
|
||||
try {
|
||||
const data = {
|
||||
@ -827,6 +860,23 @@ const toggleDiscountType = (item, type) => {
|
||||
item.discountType = type;
|
||||
};
|
||||
|
||||
const toggleItemExpansion = (itemCode) => {
|
||||
if (expandedSelectedItems.value.has(itemCode)) {
|
||||
expandedSelectedItems.value.delete(itemCode);
|
||||
} else {
|
||||
expandedSelectedItems.value.add(itemCode);
|
||||
}
|
||||
expandedSelectedItems.value = new Set(expandedSelectedItems.value);
|
||||
};
|
||||
|
||||
const isItemExpanded = (itemCode) => {
|
||||
return expandedSelectedItems.value.has(itemCode);
|
||||
};
|
||||
|
||||
const isPackageItem = (item) => {
|
||||
return item.bom && item.bom.items && item.bom.items.length > 0;
|
||||
};
|
||||
|
||||
const onTabClick = () => {
|
||||
console.log('Bid notes tab clicked');
|
||||
console.log('Current showDrawer value:', showDrawer.value);
|
||||
@ -853,12 +903,29 @@ watch(
|
||||
);
|
||||
|
||||
watch(() => formData.projectTemplate, async (newValue) => {
|
||||
quotationItems.value = await Api.getQuotationItems(newValue);
|
||||
if (!newValue) {
|
||||
quotationItems.value = {};
|
||||
return;
|
||||
}
|
||||
isLoadingQuotationItems.value = true;
|
||||
try {
|
||||
quotationItems.value = await Api.getItemsByProjectTemplate(newValue);
|
||||
console.log("DEBUG: quotationItems after API call:", quotationItems.value);
|
||||
console.log("DEBUG: quotationItems type:", typeof quotationItems.value);
|
||||
console.log("DEBUG: quotationItems keys length:", quotationItems.value ? Object.keys(quotationItems.value).length : 0);
|
||||
console.log("DEBUG: hasQuotationItems computed value:", hasQuotationItems.value);
|
||||
} catch (error) {
|
||||
console.error("Error fetching items by project template:", error);
|
||||
notificationStore.addNotification("Failed to load items for selected project template", "error");
|
||||
quotationItems.value = {};
|
||||
} finally {
|
||||
isLoadingQuotationItems.value = false;
|
||||
console.log("DEBUG: Loading finished, isLoadingQuotationItems:", isLoadingQuotationItems.value);
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => company.currentCompany, () => {
|
||||
if (isNew.value) {
|
||||
fetchTemplates();
|
||||
fetchProjectTemplates();
|
||||
}
|
||||
});
|
||||
@ -928,12 +995,12 @@ watch(
|
||||
|
||||
// Load quotation items if project template is set (needed for item details)
|
||||
if (formData.projectTemplate) {
|
||||
quotationItems.value = await Api.getQuotationItems(formData.projectTemplate);
|
||||
quotationItems.value = await Api.getItemsByProjectTemplate(formData.projectTemplate);
|
||||
}
|
||||
|
||||
if (estimate.value.items && estimate.value.items.length > 0) {
|
||||
selectedItems.value = estimate.value.items.map(item => {
|
||||
const fullItem = quotationItems.value.find(qi => qi.itemCode === item.itemCode);
|
||||
const fullItem = Object.values(quotationItems.value).flat().find(qi => qi.itemCode === item.itemCode);
|
||||
const discountPercentage = item.discountPercentage || item.discount_percentage || 0;
|
||||
const discountAmount = item.discountAmount || item.discount_amount || 0;
|
||||
return {
|
||||
@ -972,12 +1039,12 @@ watch(
|
||||
try {
|
||||
bidMeeting.value = await Api.getBidMeeting(newFromMeetingQuery);
|
||||
if (bidMeeting.value?.bidNotes?.quantities) {
|
||||
// Ensure quotationItems is an array before using find
|
||||
if (!Array.isArray(quotationItems.value)) {
|
||||
quotationItems.value = [];
|
||||
// Ensure quotationItems is an object before using
|
||||
if (typeof quotationItems.value !== 'object' || quotationItems.value === null) {
|
||||
quotationItems.value = {};
|
||||
}
|
||||
selectedItems.value = bidMeeting.value.bidNotes.quantities.map(q => {
|
||||
const item = quotationItems.value.find(i => i.itemCode === q.item);
|
||||
const item = Object.values(quotationItems.value).flat().find(i => i.itemCode === q.item);
|
||||
return {
|
||||
itemCode: q.item,
|
||||
itemName: item?.itemName || q.item,
|
||||
@ -1013,8 +1080,6 @@ onMounted(async () => {
|
||||
await fetchProjectTemplates();
|
||||
|
||||
if (isNew.value) {
|
||||
await fetchTemplates();
|
||||
|
||||
// Handle from-meeting query parameter
|
||||
if (fromMeetingQuery.value) {
|
||||
formData.fromOnsiteMeeting = fromMeetingQuery.value;
|
||||
@ -1023,12 +1088,12 @@ onMounted(async () => {
|
||||
bidMeeting.value = await Api.getBidMeeting(fromMeetingQuery.value);
|
||||
// If new estimate and bid notes have quantities, set default items
|
||||
if (isNew.value && bidMeeting.value?.bidNotes?.quantities) {
|
||||
// Ensure quotationItems is an array before using find
|
||||
if (!Array.isArray(quotationItems.value)) {
|
||||
quotationItems.value = [];
|
||||
// Ensure quotationItems is an object before using
|
||||
if (typeof quotationItems.value !== 'object' || quotationItems.value === null) {
|
||||
quotationItems.value = {};
|
||||
}
|
||||
selectedItems.value = bidMeeting.value.bidNotes.quantities.map(q => {
|
||||
const item = quotationItems.value.find(i => i.itemCode === q.item);
|
||||
const item = Object.values(quotationItems.value).flat().find(i => i.itemCode === q.item);
|
||||
return {
|
||||
itemCode: q.item,
|
||||
itemName: item?.itemName || q.item,
|
||||
@ -1049,7 +1114,7 @@ onMounted(async () => {
|
||||
// Handle project-template query parameter
|
||||
if (projectTemplateQuery.value) {
|
||||
formData.projectTemplate = projectTemplateQuery.value;
|
||||
quotationItems.value = await Api.getQuotationItems(projectTemplateQuery.value);
|
||||
quotationItems.value = await Api.getItemsByProjectTemplate(projectTemplateQuery.value);
|
||||
}
|
||||
|
||||
if (addressQuery.value && isNew.value) {
|
||||
@ -1084,13 +1149,9 @@ onMounted(async () => {
|
||||
|
||||
// Load quotation items if project template is set (needed for item details)
|
||||
if (formData.projectTemplate) {
|
||||
quotationItems.value = await Api.getQuotationItems(formData.projectTemplate);
|
||||
quotationItems.value = await Api.getItemsByProjectTemplate(formData.projectTemplate);
|
||||
}
|
||||
// Ensure quotationItems is an array
|
||||
if (!Array.isArray(quotationItems.value)) {
|
||||
quotationItems.value = [];
|
||||
}
|
||||
|
||||
|
||||
// Populate items from the estimate
|
||||
if (estimate.value.items && estimate.value.items.length > 0) {
|
||||
selectedItems.value = estimate.value.items.map(item => {
|
||||
@ -1186,21 +1247,10 @@ onMounted(async () => {
|
||||
|
||||
.address-section,
|
||||
.contact-section,
|
||||
.project-template-section,
|
||||
.template-section {
|
||||
.project-template-section {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.template-input-group {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.clear-button {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.field-label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
@ -1231,17 +1281,115 @@ onMounted(async () => {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.items-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.items-header h3 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.items-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.item-container {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.item-row {
|
||||
display: grid;
|
||||
grid-template-columns: 3fr 140px 30px 120px 220px 1.5fr auto;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.item-name-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.expand-button {
|
||||
padding: 0.25rem;
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
.package-icon {
|
||||
color: #2196f3;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.item-bullet {
|
||||
color: #666;
|
||||
font-size: 0.5em;
|
||||
}
|
||||
|
||||
.item-name {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.nested-items {
|
||||
margin-left: 2rem;
|
||||
margin-top: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
background-color: #f9f9f9;
|
||||
border-left: 3px solid #2196f3;
|
||||
border-radius: 0 4px 4px 0;
|
||||
}
|
||||
|
||||
.nested-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.nested-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.nested-item-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.25rem 0;
|
||||
color: #666;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.nested-expand-button {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
padding: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.nested-item-name {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.deeply-nested-items {
|
||||
background-color: #fafafa;
|
||||
border-left: 2px solid #e0e0e0;
|
||||
margin-left: 1rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.nested-item .package-icon {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.nested-item .item-bullet {
|
||||
font-size: 0.4em;
|
||||
}
|
||||
|
||||
.qty-input {
|
||||
width: 100%;
|
||||
}
|
||||
@ -1436,20 +1584,6 @@ onMounted(async () => {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.template-option {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.template-name {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.template-desc {
|
||||
font-size: 0.85rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.field-group {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
@ -1487,6 +1621,19 @@ onMounted(async () => {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.loading-message {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.5rem;
|
||||
color: #2196f3;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.loading-message i {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.bid-notes-drawer {
|
||||
box-shadow: -4px 0 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user