|
9 | 9 | from frappe import _ |
10 | 10 |
|
11 | 11 |
|
| 12 | +def _validate_edocument_for_transmission(edocument_doc): |
| 13 | + """ |
| 14 | + Validate eDocument before transmission. |
| 15 | +
|
| 16 | + Checks that: |
| 17 | + 1. eDocument profile is configured |
| 18 | + 2. XML is validated successfully |
| 19 | + 3. Source document exists and is submitted (docstatus == 1) for outgoing documents |
| 20 | + 4. Invoice ID in XML matches the source document name for outgoing documents |
| 21 | +
|
| 22 | + Args: |
| 23 | + edocument_doc: EDocument document object |
| 24 | +
|
| 25 | + Raises: |
| 26 | + frappe.ValidationError: If validation fails |
| 27 | + """ |
| 28 | + # Check if profile is configured |
| 29 | + if not edocument_doc.edocument_profile: |
| 30 | + frappe.throw(_("No e-document profile configured for this document.")) |
| 31 | + |
| 32 | + # Check if XML is generated and validated |
| 33 | + if edocument_doc.status != "Validation Successful": |
| 34 | + frappe.throw(_("Document is not validated. Please validate XML first.")) |
| 35 | + |
| 36 | + # Additional validation for outgoing documents with source documents |
| 37 | + if not edocument_doc.edocument_source_document or edocument_doc.direction != "Outgoing": |
| 38 | + return |
| 39 | + |
| 40 | + # Check that source document is submitted |
| 41 | + source_docstatus = frappe.db.get_value( |
| 42 | + edocument_doc.edocument_source_type, edocument_doc.edocument_source_document, "docstatus" |
| 43 | + ) |
| 44 | + if source_docstatus != 1: |
| 45 | + frappe.throw( |
| 46 | + _( |
| 47 | + "Cannot transmit eDocument: The source document '{0}' is not submitted. " |
| 48 | + "Please submit the document first." |
| 49 | + ).format(edocument_doc.edocument_source_document) |
| 50 | + ) |
| 51 | + |
| 52 | + # Get XML content and extract invoice ID |
| 53 | + xml_bytes = edocument_doc._get_xml_from_attached_files() |
| 54 | + xml_content = xml_bytes.decode("utf-8") if isinstance(xml_bytes, bytes) else xml_bytes |
| 55 | + |
| 56 | + try: |
| 57 | + from lxml import etree as ET |
| 58 | + |
| 59 | + root = ET.fromstring(xml_content.encode("utf-8") if isinstance(xml_content, str) else xml_content) |
| 60 | + |
| 61 | + # Extract invoice ID from XML (UBL and CII formats) |
| 62 | + invoice_id = None |
| 63 | + namespaces = root.nsmap |
| 64 | + |
| 65 | + # Try UBL format: cbc:ID |
| 66 | + cbc_ns = namespaces.get("cbc") |
| 67 | + if cbc_ns: |
| 68 | + id_elem = root.find(f".//{{{cbc_ns}}}ID") |
| 69 | + if id_elem is not None and id_elem.text: |
| 70 | + invoice_id = id_elem.text.strip() |
| 71 | + |
| 72 | + # Try CII format: rsm:ExchangedDocument/ram:ID |
| 73 | + if not invoice_id: |
| 74 | + ram_ns = namespaces.get("ram") |
| 75 | + if ram_ns: |
| 76 | + id_elem = root.find(f".//{{{ram_ns}}}ExchangedDocument/{{{ram_ns}}}ID") |
| 77 | + if id_elem is not None and id_elem.text: |
| 78 | + invoice_id = id_elem.text.strip() |
| 79 | + |
| 80 | + # Validate invoice ID matches source document name |
| 81 | + if not invoice_id: |
| 82 | + frappe.throw( |
| 83 | + _( |
| 84 | + "Cannot transmit eDocument: Unable to extract invoice ID from XML. " |
| 85 | + "Please regenerate the eDocument." |
| 86 | + ) |
| 87 | + ) |
| 88 | + |
| 89 | + if invoice_id != edocument_doc.edocument_source_document: |
| 90 | + frappe.throw( |
| 91 | + _( |
| 92 | + "Cannot transmit eDocument: The invoice ID in XML '{0}' does not match " |
| 93 | + "the source document name '{1}'. Please regenerate the eDocument." |
| 94 | + ).format(invoice_id, edocument_doc.edocument_source_document) |
| 95 | + ) |
| 96 | + |
| 97 | + except frappe.ValidationError: |
| 98 | + # Re-raise our own validation errors |
| 99 | + raise |
| 100 | + except Exception as e: |
| 101 | + frappe.log_error( |
| 102 | + f"Error validating invoice ID in XML for eDocument {edocument_doc.name}: {e!s}", |
| 103 | + "EDocument Validation Error", |
| 104 | + ) |
| 105 | + frappe.throw( |
| 106 | + _("Cannot transmit eDocument: Error parsing XML. Please check the eDocument and try again.") |
| 107 | + ) |
| 108 | + |
| 109 | + |
12 | 110 | @frappe.whitelist() |
13 | 111 | def get_edocument_integration_settings(profile, company=None): |
14 | 112 | # Get EDocument Integration Settings for the given profile |
@@ -39,12 +137,8 @@ def transmit_edocument(edocument_name): |
39 | 137 | # Transmit E-document using the configured integrator |
40 | 138 | try: |
41 | 139 | edocument_doc = frappe.get_doc("EDocument", edocument_name) |
42 | | - if not edocument_doc.edocument_profile: |
43 | | - frappe.throw(_("No e-document profile configured for this document.")) |
44 | 140 |
|
45 | | - # Check if XML is generated and validated |
46 | | - if edocument_doc.status != "Validation Successful": |
47 | | - frappe.throw(_("Document is not validated. Please validate XML first.")) |
| 141 | + _validate_edocument_for_transmission(edocument_doc) |
48 | 142 |
|
49 | 143 | # Get XML content from attached file using EDocument's method |
50 | 144 | xml_bytes = edocument_doc._get_xml_from_attached_files() |
@@ -105,6 +199,9 @@ def transmit_edocument(edocument_name): |
105 | 199 | frappe.db.commit() |
106 | 200 |
|
107 | 201 | return transmission_result |
| 202 | + except frappe.ValidationError: |
| 203 | + # Validation errors (e.g., from _validate_source_docstatus) should be re-raised as-is |
| 204 | + raise |
108 | 205 | except Exception as e: |
109 | 206 | # Set status to Transmission Failed |
110 | 207 | frappe.db.set_value( |
|
0 commit comments