[Feature] Minimal IUCLID export (#338)

This is an initial implementation that creates a working minimal .i6z document.
It passes schema validation and can be imported into IUCLID.

Caveat:
IUCLID files target individual compounds.
Pathway is not actually covered by the format.

It can be added in either soil or water and soil OECD endpoints.
**I currently only implemented the soil endpoint for all data.**

This sort of works, and I can report all degradation products in a pathway (not a nice view, but we can report many transformation products and add a diagram attachment in the future).

Adding additional information is an absolute pain, as we need to explicitly map each type of information to the relevant OECD field.
I use the XSD scheme for validation, but unfortunately the IUCLID parser is not fully compliant and requires a specific order, etc.

The workflow is: finding the AI structure from the XSD scheme -> make the scheme validation pass -> upload to IUCLID to get obscure error messages -> guess what could be wrong -> repeat 💣

New specifications get released once per year, so we will have to update accordingly.
I believe that this should be a more expensive feature, as it requires significant effort to uphold.

Currently implemented for root compound only in SOIL:

- Soil Texture 2
- Soil Texture 1
- pH value
- Half-life per soil sample / scenario (mapped to disappearance; not sure about that).
- CEC
- Organic Matter (only Carbon)
- Moisture content
- Humidity

<img width="2123" alt="image.png" src="attachments/d29830e1-65ef-4136-8939-1825e0959c62">
<img width="2124" alt="image.png" src="attachments/ac9de2ac-bf68-4ba4-b40b-82f810a9de93">
<img width="2139" alt="image.png" src="attachments/5674c7e6-865e-420e-974a-6b825b331e6c">

Reviewed-on: enviPath/enviPy#338
Co-authored-by: Tobias O <tobias.olenyi@envipath.com>
Co-committed-by: Tobias O <tobias.olenyi@envipath.com>
This commit is contained in:
2026-04-07 19:46:12 +12:00
committed by jebus
parent f7c45b8015
commit d06bd0d4fd
49 changed files with 66402 additions and 1014 deletions

View File

@ -0,0 +1,86 @@
"""
Tests for the IUCLID projection layer.
"""
from django.test import TestCase, tag
from envipy_additional_information.information import Temperature
from epdb.logic import PackageManager, UserManager
from epdb.models import AdditionalInformation, Pathway, Scenario
from epapi.v1.interfaces.iuclid.projections import get_pathway_for_iuclid_export
@tag("api", "iuclid")
class IUCLIDExportScenarioAITests(TestCase):
"""Test that scenario additional information is correctly reflected in the IUCLID export."""
@classmethod
def setUpTestData(cls):
cls.user = UserManager.create_user(
"iuclid-test-user",
"iuclid-test@envipath.com",
"SuperSafe",
set_setting=False,
add_to_group=False,
is_active=True,
)
cls.package = PackageManager.create_package(
cls.user, "IUCLID Test Package", "Package for IUCLID export tests"
)
cls.pathway = Pathway.create(cls.package, "CCO", name="Test Pathway")
cls.root_node = cls.pathway.node_set.get(depth=0)
cls.compound = cls.root_node.default_node_label.compound
cls.scenario = Scenario.objects.create(
package=cls.package,
name="Test Scenario",
scenario_type="biodegradation",
scenario_date="2024-01-01",
)
cls.root_node.scenarios.add(cls.scenario)
def test_scenario_with_no_ai_exports_without_error(self):
"""Scenario attached to a node but with no AI must export cleanly with empty lists.
Regression: previously scenario.parent was accessed here, causing AttributeError.
"""
result = get_pathway_for_iuclid_export(self.user, self.pathway.uuid)
self.assertEqual(len(result.nodes), 1)
node = result.nodes[0]
self.assertEqual(len(node.scenarios), 1)
self.assertEqual(node.scenarios[0].scenario_uuid, self.scenario.uuid)
self.assertEqual(node.scenarios[0].additional_info, [])
self.assertEqual(node.additional_info, [])
def test_direct_scenario_ai_appears_in_export(self):
"""AI added directly to a scenario (no object binding) must appear in the export DTO."""
ai = Temperature(interval={"start": 20, "end": 25})
AdditionalInformation.create(self.package, ai, scenario=self.scenario)
result = get_pathway_for_iuclid_export(self.user, self.pathway.uuid)
node = result.nodes[0]
self.assertEqual(len(node.scenarios), 1)
scenario_dto = node.scenarios[0]
self.assertEqual(len(scenario_dto.additional_info), 1)
# The same AI must roll up to the node level
self.assertEqual(len(node.additional_info), 1)
self.assertEqual(node.additional_info[0], scenario_dto.additional_info[0])
def test_object_bound_ai_excluded_from_scenario_direct_info(self):
"""AI bound to a specific object (object_id set) must not appear in direct scenario info.
get_additional_information(direct_only=True) filters object_id__isnull=False.
"""
ai = Temperature(interval={"start": 10, "end": 15})
AdditionalInformation.create(
self.package, ai, scenario=self.scenario, content_object=self.compound
)
result = get_pathway_for_iuclid_export(self.user, self.pathway.uuid)
node = result.nodes[0]
scenario_dto = node.scenarios[0]
self.assertEqual(scenario_dto.additional_info, [])
self.assertEqual(node.additional_info, [])