Skip to content

Commit ad002cf

Browse files
authored
Merge pull request #6 from maasanto/feat-validate-source
feat: add validation to prevent transmitting edocuments with mismatched invoice IDs
2 parents d3f2dc8 + b121d25 commit ad002cf

File tree

1 file changed

+102
-5
lines changed

1 file changed

+102
-5
lines changed

edocument_integration/api.py

Lines changed: 102 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,104 @@
99
from frappe import _
1010

1111

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+
12110
@frappe.whitelist()
13111
def get_edocument_integration_settings(profile, company=None):
14112
# Get EDocument Integration Settings for the given profile
@@ -39,12 +137,8 @@ def transmit_edocument(edocument_name):
39137
# Transmit E-document using the configured integrator
40138
try:
41139
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."))
44140

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)
48142

49143
# Get XML content from attached file using EDocument's method
50144
xml_bytes = edocument_doc._get_xml_from_attached_files()
@@ -105,6 +199,9 @@ def transmit_edocument(edocument_name):
105199
frappe.db.commit()
106200

107201
return transmission_result
202+
except frappe.ValidationError:
203+
# Validation errors (e.g., from _validate_source_docstatus) should be re-raised as-is
204+
raise
108205
except Exception as e:
109206
# Set status to Transmission Failed
110207
frappe.db.set_value(

0 commit comments

Comments
 (0)