From 2595e47101b259a766517bf8abca2e17dc61398b Mon Sep 17 00:00:00 2001 From: bhavesh95863 Date: Sun, 19 Apr 2026 19:17:39 +0000 Subject: [PATCH] fix: order new flags added, visit with attachment added --- employee_self_service/__init__.py | 2 +- .../employee_self_service_settings.json | 40 +++++- .../employee_self_service_settings.py | 6 +- .../doctype/visit/visit.json | 14 +- employee_self_service/mobile/v1/ess.py | 3 + employee_self_service/mobile/v1/file.py | 129 +++++++++++++++++- employee_self_service/mobile/v1/order.py | 50 +++---- employee_self_service/mobile/v1/visit.py | 1 + employee_self_service/setup/__init__.py | 8 ++ 9 files changed, 223 insertions(+), 30 deletions(-) diff --git a/employee_self_service/__init__.py b/employee_self_service/__init__.py index ba51ced..f394e69 100644 --- a/employee_self_service/__init__.py +++ b/employee_self_service/__init__.py @@ -1 +1 @@ -__version__ = "2.2.2" +__version__ = "2.2.3" diff --git a/employee_self_service/employee_self_service/doctype/employee_self_service_settings/employee_self_service_settings.json b/employee_self_service/employee_self_service/doctype/employee_self_service_settings/employee_self_service_settings.json index 7e15836..5baf580 100755 --- a/employee_self_service/employee_self_service/doctype/employee_self_service_settings/employee_self_service_settings.json +++ b/employee_self_service/employee_self_service/doctype/employee_self_service_settings/employee_self_service_settings.json @@ -15,6 +15,12 @@ "column_break_7qksp", "default_print_format", "geofencing_based_on", + "selling_settings_section", + "allow_user_to_change_rate", + "column_break_goaq", + "allow_user_to_change_uom", + "column_break_kxpj", + "visit_proof_required", "section_break_xjas", "enable_ess_notification", "check_in_with_image", @@ -254,13 +260,45 @@ "fieldname": "show_stock_balance_in_item_list", "fieldtype": "Check", "label": "Show Stock Balance in Item List" + }, + { + "default": "0", + "description": "When enabled, visit proof (image attachment) is mandatory while creating a Visit from the mobile app.", + "fieldname": "visit_proof_required", + "fieldtype": "Check", + "label": "Visit Proof Required" + }, + { + "fieldname": "selling_settings_section", + "fieldtype": "Section Break", + "label": "Selling Settings" + }, + { + "fieldname": "column_break_goaq", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_kxpj", + "fieldtype": "Column Break" + }, + { + "default": "1", + "fieldname": "allow_user_to_change_rate", + "fieldtype": "Check", + "label": "Allow User to Change Rate" + }, + { + "default": "1", + "fieldname": "allow_user_to_change_uom", + "fieldtype": "Check", + "label": "Allow User to Change UOM" } ], "grid_page_length": 50, "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2026-03-13 17:25:02.606955", + "modified": "2026-04-19 18:19:16.219747", "modified_by": "Administrator", "module": "Employee Self Service", "name": "Employee Self Service Settings", diff --git a/employee_self_service/employee_self_service/doctype/employee_self_service_settings/employee_self_service_settings.py b/employee_self_service/employee_self_service/doctype/employee_self_service_settings/employee_self_service_settings.py index 88d74f5..c73d653 100755 --- a/employee_self_service/employee_self_service/doctype/employee_self_service_settings/employee_self_service_settings.py +++ b/employee_self_service/employee_self_service/doctype/employee_self_service_settings/employee_self_service_settings.py @@ -1,9 +1,11 @@ # Copyright (c) 2022, Nesscale Solutions Private Limited and contributors # For license information, please see license.txt -# import frappe from frappe.model.document import Document +from employee_self_service.setup import disable_geolocation_tracking + class EmployeeSelfServiceSettings(Document): - pass + def on_update(self): + disable_geolocation_tracking() diff --git a/employee_self_service/employee_self_service/doctype/visit/visit.json b/employee_self_service/employee_self_service/doctype/visit/visit.json index 08d2ea0..f57efc9 100755 --- a/employee_self_service/employee_self_service/doctype/visit/visit.json +++ b/employee_self_service/employee_self_service/doctype/visit/visit.json @@ -21,7 +21,9 @@ "location", "column_break_ssqu5", "employee", - "user" + "user", + "section_break_visit_proof", + "visit_proof" ], "fields": [ { @@ -100,6 +102,16 @@ "fieldtype": "Link", "label": "User", "options": "User" + }, + { + "fieldname": "section_break_visit_proof", + "fieldtype": "Section Break", + "label": "Visit Proof" + }, + { + "fieldname": "visit_proof", + "fieldtype": "Attach Image", + "label": "Visit Proof" } ], "index_web_pages_for_search": 1, diff --git a/employee_self_service/mobile/v1/ess.py b/employee_self_service/mobile/v1/ess.py index 3149343..c797ac0 100644 --- a/employee_self_service/mobile/v1/ess.py +++ b/employee_self_service/mobile/v1/ess.py @@ -671,6 +671,9 @@ def get_dashboard(): ), "enable_todo": settings.get("enable_todo"), "enable_modular_menu": settings.get("enable_modular_menu"), + "visit_proof_required": settings.get("visit_proof_required"), + "allow_user_to_change_rate": settings.get("allow_user_to_change_rate"), + "allow_user_to_change_uom": settings.get("allow_user_to_change_uom"), } # "approval_requests": get_workflow_documents(internal=True) dashboard_data["employee_image"] = emp_data.get("image") diff --git a/employee_self_service/mobile/v1/file.py b/employee_self_service/mobile/v1/file.py index 702a9b9..6d122ee 100644 --- a/employee_self_service/mobile/v1/file.py +++ b/employee_self_service/mobile/v1/file.py @@ -5,8 +5,18 @@ ess_validate, exception_handler, gen_response, + get_employee_by_user, ) +# Mapping of document type keys to their doctype name and image field +DOCUMENT_IMAGE_MAP = { + "visit": { + "doctype": "Visit", + "field": "visit_proof", + "employee_field": "employee", + }, +} + @frappe.whitelist() @ess_validate(methods=["POST"]) @@ -20,9 +30,15 @@ def upload_documents(): file_doc = upload_file() file_doc.attached_to_doctype = frappe.form_dict.reference_doctype file_doc.attached_to_name = frappe.form_dict.reference_docname + is_private = frappe.form_dict.get("is_private", "1") + file_doc.is_private = int(is_private) file_doc.save(ignore_permissions=True) - frappe.db.commit() - return gen_response(200, "file added successfully.") + return gen_response(200, "File uploaded successfully.", { + "name": file_doc.name, + "file_url": file_doc.file_url, + "file_name": file_doc.file_name, + "is_private": file_doc.is_private, + }) else: return gen_response(500, "Please upload a file for attachment.") except Exception as e: @@ -30,6 +46,45 @@ def upload_documents(): return exception_handler(e) +@frappe.whitelist() +@ess_validate(methods=["POST"]) +def delete_file(file_name): + try: + if not file_name: + return gen_response(500, "Please provide the file name.") + if not frappe.db.exists("File", file_name): + return gen_response(404, "File not found.") + frappe.delete_doc("File", file_name) + return gen_response(200, "File deleted successfully.") + except frappe.PermissionError: + return gen_response(403, "Not permitted to delete this file.") + except Exception as e: + return exception_handler(e) + + +@frappe.whitelist() +@ess_validate(methods=["GET"]) +def get_attachments(reference_doctype, reference_name): + try: + if not reference_doctype: + return gen_response(500, "Please provide a reference document type.") + if not reference_name: + return gen_response(500, "Please provide a reference document name.") + files = frappe.get_all( + "File", + filters={ + "attached_to_doctype": reference_doctype, + "attached_to_name": reference_name, + }, + fields=["name", "file_name", "file_url", "is_private", "file_size"], + ) + return gen_response(200, "Attachments fetched successfully.", files) + except frappe.PermissionError: + return gen_response(403, "Not permitted to view attachments.") + except Exception as e: + return exception_handler(e) + + def get_attchment(reference_doctype, reference_name): return frappe.get_all( "File", @@ -39,3 +94,73 @@ def get_attchment(reference_doctype, reference_name): }, fields=["*"], ) + + +@frappe.whitelist() +@ess_validate(methods=["POST"]) +def attach_document_image(document_type, document_name): + """ + Generic API to attach an image to a supported document. + Args: + document_type: Key from DOCUMENT_IMAGE_MAP (e.g. "checkin", "visit") + document_name: The document ID (e.g. "HR-CHK-00001", "VISIT00001") + """ + try: + # Validate document_type + doc_config = DOCUMENT_IMAGE_MAP.get(document_type) + if not doc_config: + return gen_response( + 400, + f"Unsupported document type '{document_type}'. Supported types: {', '.join(DOCUMENT_IMAGE_MAP.keys())}", + ) + + doctype = doc_config["doctype"] + image_field = doc_config["field"] + employee_field = doc_config["employee_field"] + + # Validate document exists + if not frappe.db.exists(doctype, document_name): + return gen_response(404, f"{doctype} '{document_name}' not found") + + # Get the document + doc = frappe.get_doc(doctype, document_name) + + # Verify the document belongs to the current user's employee + emp_data = get_employee_by_user(frappe.session.user) + if doc.get(employee_field) != emp_data.get("name"): + return gen_response(403, "You don't have permission to attach image to this document") + + # Check if file is in request + if "file" not in frappe.request.files: + return gen_response(400, "No file provided") + + # Delete old image if exists + old_file_url = doc.get(image_field) + if old_file_url: + old_file_doc = frappe.db.get_value("File", {"file_url": old_file_url}, "name") + if old_file_doc: + frappe.delete_doc("File", old_file_doc, ignore_permissions=True) + + # Upload new file + file = upload_file() + file.attached_to_doctype = doctype + file.attached_to_name = doc.name + file.attached_to_field = image_field + file.save(ignore_permissions=True) + + # Update document with image URL + doc.set(image_field, file.get("file_url")) + doc.save(ignore_permissions=True) + + return gen_response( + 200, + "Image attached successfully", + { + "document_type": document_type, + "document_name": doc.name, + "file_url": file.get("file_url"), + "file_name": file.get("file_name"), + }, + ) + except Exception as e: + return exception_handler(e) diff --git a/employee_self_service/mobile/v1/order.py b/employee_self_service/mobile/v1/order.py index 4536285..d8b5df2 100644 --- a/employee_self_service/mobile/v1/order.py +++ b/employee_self_service/mobile/v1/order.py @@ -323,12 +323,14 @@ def get_item_list( filters.append(["Item", "show_in_mobile", "=", 1]) item_list = frappe.get_list( "Item", - fields=["name", "item_name", "item_code", "image", "stock_uom"], + fields=["name", "item_name", "item_code", "image", "sales_uom", "stock_uom"], filters=filters, start=start, or_filters=or_filters, page_length=page_length, ) + for item in item_list: + item["uom"] = item.get("sales_uom") or item.get("stock_uom") if for_filters: items = item_list else: @@ -355,20 +357,11 @@ def get_items_rate(items, customer=None, warehouse=None): show_stock_balance = ess_settings.get("show_stock_balance_in_item_list", 0) for item in items: - item_price = frappe.get_all( - "Item Price", - filters={ - "item_code": item.name, - "price_list": price_list, - "uom": item.stock_uom, - }, - fields=["price_list_rate"], - order_by="valid_from desc", - ) + uom = item.get("uom") item_price = _get_item_price( item_code=item.name, price_list=price_list, - uom=item.stock_uom, + uom=uom, ) item_price_currency = fmt_money( item_price if item_price else 0.0, @@ -412,26 +405,28 @@ def get_uoms(customer, item): global_defaults = get_global_defaults() sales_price_list = get_default_price_list(customer=customer) item_doc = frappe.get_doc("Item", item) + default_uom = item_doc.get("sales_uom") or item_doc.get("stock_uom") + stock_uom = item_doc.get("stock_uom") uoms = [] default_uom_price = _get_item_price( item_code=item_doc.get("name"), price_list=sales_price_list, - uom=item_doc.get("stock_uom"), + uom=default_uom, ) for uom_row in item_doc.get("uoms"): - if uom_row.get("uom") == item_doc.get("stock_uom"): - uom_hint = f"{uom_row.get('uom')} is default uom" - else: - uom_hint = f"1 {uom_row.get('uom')} = {uom_row.get('conversion_factor')} {item_doc.get('stock_uom')}" + uom_hint = f"1 {uom_row.get('uom')} = {uom_row.get('conversion_factor')} {stock_uom}" uom_details = dict( uom=uom_row.get("uom"), conversion_factor=uom_row.get("conversion_factor"), uom_hint=uom_hint, + is_default=uom_row.get("uom") == default_uom, ) get_uom_item_price( sales_price_list, item, uom_details, default_uom_price, global_defaults ) uoms.append(uom_details) + # Put sales_uom first in the list + uoms.sort(key=lambda u: u["uom"] != default_uom) return gen_response(200, "uom details get successfully", uoms) except frappe.PermissionError: return gen_response(500, "Not permitted for item") @@ -492,8 +487,10 @@ def scan_item(barcode): item_list = frappe.get_list( "Item", filters={"name": item_details.get("item_code")}, - fields=["name", "item_name", "item_code", "image", "stock_uom"], + fields=["name", "item_name", "item_code", "image", "sales_uom", "stock_uom"], ) + for item in item_list: + item["uom"] = item.get("sales_uom") or item.get("stock_uom") items = get_items_rate(item_list) if len(items) >= 1: gen_response(200, "Item list get successfully", items[0]) @@ -618,11 +615,6 @@ def _create_update_order(data, sales_order_doc, default_warehouse): enable_target_management = frappe.db.get_single_value( "ESS Target Settings", "enable_target_management" ) - if enable_target_management: - sales_persons = get_sales_person_by_customer(data.get("customer")) - if sales_persons: - sales_order_doc.set("sales_team", sales_persons) - delivery_date = data.get("delivery_date") for item in data.get("items"): item["delivery_date"] = delivery_date @@ -631,6 +623,18 @@ def _create_update_order(data, sales_order_doc, default_warehouse): sales_order_doc.update(data) sales_order_doc.run_method("set_missing_values") sales_order_doc.run_method("calculate_taxes_and_totals") + + # Populate sales team from Customer if not already set + if not sales_order_doc.get("sales_team"): + sales_persons = get_sales_person_by_customer(data.get("customer")) + if sales_persons: + sales_order_doc.set("sales_team", sales_persons) + + if enable_target_management: + sales_persons = get_sales_person_by_customer(data.get("customer")) + if sales_persons: + sales_order_doc.set("sales_team", sales_persons) + sales_order_doc.save() diff --git a/employee_self_service/mobile/v1/visit.py b/employee_self_service/mobile/v1/visit.py index d928317..4a8c28c 100644 --- a/employee_self_service/mobile/v1/visit.py +++ b/employee_self_service/mobile/v1/visit.py @@ -74,6 +74,7 @@ def get_visit_list(): "time_format(time, '%h:%i:%s') as time", "visit_type", "description", + "visit_proof", ], ) return gen_response(200, "Visit list get successfully", visit_list) diff --git a/employee_self_service/setup/__init__.py b/employee_self_service/setup/__init__.py index efd1d8d..644365c 100644 --- a/employee_self_service/setup/__init__.py +++ b/employee_self_service/setup/__init__.py @@ -9,6 +9,7 @@ def after_install(): create_custom_fields() add_default_language_in_ess_settings() + disable_geolocation_tracking() def create_custom_fields(): @@ -29,6 +30,13 @@ def get_all_custom_fields(): return result +def disable_geolocation_tracking(): + if frappe.db.exists("DocType", "HR Settings"): + meta = frappe.get_meta("HR Settings") + if meta.has_field("allow_geolocation_tracking"): + frappe.db.set_single_value("HR Settings", "allow_geolocation_tracking", 0) + + def add_default_language_in_ess_settings(): if frappe.db.exists("DocType", "Employee Self Service Settings"): ess_settings = frappe.get_doc(