From db0ead9db686d705034feed76aa34893c2b43059 Mon Sep 17 00:00:00 2001 From: Preetam Biswal Date: Mon, 29 Dec 2025 18:43:50 +0100 Subject: [PATCH 1/2] docs: Update README and CI workflows for v15/v16 compatibility --- README.md | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index d2d46fa..d5c7fbd 100644 --- a/README.md +++ b/README.md @@ -11,15 +11,30 @@ This app extends the [edocument](https://github.com/prilk-consulting/edocument) This app requires the `edocument` app to be installed first. -You can install this app using the [bench](https://github.com/frappe/bench) CLI: +### Supported Versions + +| Frappe/ERPNext | Branch | Python | Node.js | +|----------------|--------|--------|---------| +| v15 | `version-15` | 3.10 | 18 | +| v16 | `develop` | 3.14 | 24 | + +### Installation Steps ```bash cd $PATH_TO_YOUR_BENCH -bench get-app https://github.com/prilk-consulting/edocument_integration --branch $MAJOR_VERSION -bench install-app edocument_integration -``` -Please use a branch (`MAJOR_VERSION`) that matches the major version of ERPNext you are using. For example, `version-14` or `version-15`. If you are a developer contributing new features, you'll want to use the `develop` branch instead. +# For Frappe/ERPNext v15 +bench get-app https://github.com/prilk-consulting/edocument --branch version-15 +bench get-app https://github.com/prilk-consulting/edocument_integration --branch version-15 + +# For Frappe/ERPNext v16 (develop) +bench get-app https://github.com/prilk-consulting/edocument +bench get-app https://github.com/prilk-consulting/edocument_integration + +# Install on your site +bench --site your-site install-app edocument +bench --site your-site install-app edocument_integration +``` ## Setup From 4ed0106649c63d59cc816cbe579483fb1ed5e76f Mon Sep 17 00:00:00 2001 From: Preetam Biswal Date: Wed, 4 Feb 2026 22:50:55 +0100 Subject: [PATCH 2/2] fix: handle Recommand JSON webhook notifications Webhook now detects Recommand notifications (eventType: document.received) and fetches actual XML from Recommand API using documentId. - Parse incoming data as JSON first - If Recommand notification, fetch XML via get_document_status API - Check for duplicates using EDocument.reference field - Fall back to raw XML for other providers (existing behavior) --- edocument_integration/api.py | 122 ++++++++++++++++++++++++++--------- 1 file changed, 91 insertions(+), 31 deletions(-) diff --git a/edocument_integration/api.py b/edocument_integration/api.py index 181be2b..305a923 100644 --- a/edocument_integration/api.py +++ b/edocument_integration/api.py @@ -122,22 +122,65 @@ def transmit_edocument(edocument_name): frappe.throw(_("Transmission failed: {0}").format(str(e))) +def _handle_recommand_notification(notification: dict) -> dict: + """ + Handle Recommand webhook notification by fetching actual XML from API. + + Args: + notification: Recommand webhook payload with eventType, documentId, teamId + + Returns: + dict with xml_bytes and document_id, or raises exception on error + """ + document_id = notification.get("documentId") + team_id = notification.get("teamId") + + if not document_id or not team_id: + raise ValueError(f"Missing documentId or teamId in notification: {notification}") + + # Find integration settings by team_id (account_id) + settings = frappe.db.get_value( + "EDocument Integration Settings", + {"account_id": team_id, "edocument_integrator": "Recommand"}, + ["name", "edocument_profile", "company"], + as_dict=True, + ) + + if not settings: + raise ValueError(f"No Recommand integration settings found for team_id: {team_id}") + + # Get full integration settings with decrypted credentials + integration_settings = get_edocument_integration_settings(settings.edocument_profile, settings.company) + + # Fetch actual XML from Recommand API + from .recommand_api import get_recommand_client + + client = get_recommand_client(integration_settings) + doc_details = client.get_document_status(team_id, document_id) + + xml_content = doc_details.get("document", {}).get("xml") + if not xml_content: + raise ValueError(f"No XML content in document {document_id}. Response: {doc_details}") + + xml_bytes = xml_content.encode("utf-8") if isinstance(xml_content, str) else xml_content + return {"xml_bytes": xml_bytes, "document_id": document_id} + + +# nosemgrep: frappe-semgrep-rules.rules.security.guest-whitelisted-method @frappe.whitelist(allow_guest=True) def webhook(**kwargs): - # Webhook endpoint to receive incoming PEPPOL documents from providers + """Webhook endpoint to receive incoming PEPPOL documents from providers.""" + import json + request_log = None try: r = frappe.request if not r: - frappe.log_error("No request data received", "E-Document Webhook Error") return {"status": "error", "message": "No request data received"}, 400 - xml_bytes = r.get_data() - if not xml_bytes: - frappe.log_error("No XML content found in request", "E-Document Webhook Error") - return {"status": "error", "message": "No XML content found in request"}, 400 - if isinstance(xml_bytes, str): - xml_bytes = xml_bytes.encode("utf-8") + request_data = r.get_data() + if not request_data: + return {"status": "error", "message": "No content found in request"}, 400 from frappe.integrations.utils import create_request_log @@ -148,22 +191,46 @@ def webhook(**kwargs): request_headers=r.headers, ) - # Create EDocument record and attach XML file (no validation triggered) - edocument = frappe.get_doc( - { - "doctype": "EDocument", - } - ) + # Try to parse as Recommand JSON notification, otherwise treat as raw XML + xml_bytes = None + document_id = None + + try: + data_str = request_data.decode("utf-8") if isinstance(request_data, bytes) else request_data + notification = json.loads(data_str) + + if notification.get("eventType") == "document.received": + result = _handle_recommand_notification(notification) + xml_bytes = result["xml_bytes"] + document_id = result["document_id"] + except (json.JSONDecodeError, UnicodeDecodeError): + pass # Not JSON, treat as raw XML + except ValueError as e: + frappe.log_error(str(e), "E-Document Webhook Error") + return {"status": "error", "message": str(e)}, 400 + + # Fallback: treat as raw XML + if xml_bytes is None: + xml_bytes = request_data.encode("utf-8") if isinstance(request_data, str) else request_data + + # Check for duplicate + if document_id: + existing = frappe.db.exists("EDocument", {"reference": document_id}) + if existing: + result = {"edocument": existing, "skipped": True, "reason": "duplicate"} + request_log.status = "Completed" + request_log.response = frappe.as_json(result) + return {"status": "success", "result": result}, 200 + + # Create EDocument and attach XML + edocument = frappe.get_doc({"doctype": "EDocument", "reference": document_id}) edocument.insert(ignore_permissions=True) - # Manual commit required: Webhook must persist EDocument before returning response to external service - frappe.db.commit() # nosemgrep + frappe.db.commit() # nosemgrep: Webhook must persist before returning - # Attach XML file - filename = f"document_{edocument.name}.xml" file_doc = frappe.get_doc( { "doctype": "File", - "file_name": filename, + "file_name": f"document_{document_id or edocument.name}.xml", "attached_to_doctype": "EDocument", "attached_to_name": edocument.name, "content": xml_bytes, @@ -171,27 +238,20 @@ def webhook(**kwargs): } ) file_doc.save(ignore_permissions=True) - # Manual commit required: Webhook must persist File attachment before returning response to external service - frappe.db.commit() # nosemgrep - - result = { - "edocument": edocument.name, - } + frappe.db.commit() # nosemgrep: Webhook must persist before returning + result = {"edocument": edocument.name, "document_id": document_id} request_log.status = "Completed" request_log.response = frappe.as_json(result) return {"status": "success", "result": result}, 200 + except Exception as e: if request_log: request_log.status = "Failed" request_log.error = frappe.get_traceback() frappe.db.rollback() - frappe.log_error( - f"E-Document webhook processing failed: {e!s}\n{frappe.get_traceback()}", - "E-Document Webhook Error", - ) - # Manual commit required: Webhook must commit error state before returning error response to external service - frappe.db.commit() # nosemgrep + frappe.log_error(f"E-Document webhook failed: {e!s}", "E-Document Webhook Error") + frappe.db.commit() # nosemgrep: Commit error state before returning return {"status": "error", "message": "Internal server error"}, 500 finally: if request_log: