Repository: tefra/xsdata

Issue:
Why is it sometimes choice and sometimes '_or_'
What is the reason that this is the '_or_' variant...
```
    fare_scheduled_stop_point_ref_or_scheduled_stop_point_ref: Optional[object] = field(
        default=None,
        metadata={
            "type": "Elements",
            "choices": (
                {
                    "name": "FareScheduledStopPointRef",
                    "type": FareScheduledStopPointRef,
                    "namespace": "http://www.netex.org.uk/netex",
                },
                {
                    "name": "ScheduledStopPointRef",
                    "type": ScheduledStopPointRef,
                    "namespace": "http://www.netex.org.uk/netex",
                },
            ),
        }
    )
```

And this is the 'choice' variant?

```
    choice: Optional[object] = field(
        default=None,
        metadata={
            "type": "Elements",
            "choices": (
                {
                    "name": "ServiceJourneyPatternRef",
                    "type": ServiceJourneyPatternRef,
                    "namespace": "http://www.netex.org.uk/netex",
                },
                {
                    "name": "ServicePatternRef",
                    "type": ServicePatternRef,
                    "namespace": "http://www.netex.org.uk/netex",
                },
                {
                    "name": "DeadRunJourneyPatternRef",
                    "type": DeadRunJourneyPatternRef,
                    "namespace": "http://www.netex.org.uk/netex",
                },
                {
                    "name": "JourneyPatternRef",
                    "type": JourneyPatternRef,
                    "namespace": "http://www.netex.org.uk/netex",
                },
            ),
        }
    )
```

Does this have to do with the length of the variable?

Discussion:
sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=tefra_xsdata&pullRequest=899) **Quality Gate passed**  
Kudos, no new issues were introduced!

[0 New issues](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=899&resolved=false&inNewCodePeriod=true)  
[0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=tefra_xsdata&pullRequest=899&resolved=false&inNewCodePeriod=true)  
No data about Coverage  
[0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=899&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=tefra_xsdata&pullRequest=899)



Patch:
diff --git a/docs/examples/compound-fields.rst b/docs/examples/compound-fields.rst
index e7384992..3f7e783b 100644
--- a/docs/examples/compound-fields.rst
+++ b/docs/examples/compound-fields.rst
@@ -22,7 +22,9 @@ fields in order to preserve the elements ordering during roundtrip conversions.
         :lines: 33-53
 
 
-All choice elements are grouped into a single list field.
+All choice elements are grouped into a single field. The name of field
+can be manipulated with the :class:`~xsdata.models.config.CompoundFields`
+configuration.
 
 .. testcode::
 
diff --git a/xsdata/codegen/handlers/add_attribute_substitutions.py b/xsdata/codegen/handlers/add_attribute_substitutions.py
index f16aba87..20840b69 100644
--- a/xsdata/codegen/handlers/add_attribute_substitutions.py
+++ b/xsdata/codegen/handlers/add_attribute_substitutions.py
@@ -34,9 +34,9 @@ class AddAttributeSubstitutions(RelativeHandlerInterface):
     def process_attribute(self, target: Class, attr: Attr):
         """
         Check if the given attribute matches any substitution class in order to
-        clone it's attributes to the target class.
+        clone its attributes to the target class.
 
-        The cloned attributes are placed below the attribute the are
+        The cloned attributes are placed below the attribute they are
         supposed to substitute.
 
         Guard against multiple substitutions in case of xs:groups.
@@ -56,6 +56,8 @@ class AddAttributeSubstitutions(RelativeHandlerInterface):
                 clone.restrictions.min_occurs = 0
                 clone.restrictions.max_occurs = attr.restrictions.max_occurs
 
+                attr.substitution = clone.substitution = attr_type.name
+
                 pos = collections.find(target.attrs, clone)
                 index = pos + 1 if pos > -1 else index
                 target.attrs.insert(index, clone)
diff --git a/xsdata/codegen/handlers/create_compound_fields.py b/xsdata/codegen/handlers/create_compound_fields.py
index 0e6d8ae0..0cc943a8 100644
--- a/xsdata/codegen/handlers/create_compound_fields.py
+++ b/xsdata/codegen/handlers/create_compound_fields.py
@@ -71,17 +71,20 @@ class CreateCompoundFields(RelativeHandlerInterface):
         assert choice is not None
 
         names = []
+        substitutions = []
         choices = []
         counters: Dict = {"min": [], "max": []}
 
         for attr in attrs:
             ClassUtils.remove_attribute(target, attr)
             names.append(attr.local_name)
+            substitutions.append(attr.substitution)
+
             choices.append(self.build_attr_choice(attr))
             self.update_counters(attr, counters)
 
         min_occurs, max_occurs = self.sum_counters(counters)
-        name = self.choose_name(target, names)
+        name = self.choose_name(target, names, list(filter(None, substitutions)))
         types = collections.unique_sequence(t for attr in attrs for t in attr.types)
 
         target.attrs.insert(
@@ -115,12 +118,14 @@ class CreateCompoundFields(RelativeHandlerInterface):
 
         return min_occurs, max_occurs
 
-    def choose_name(self, target: Class, names: List[str]) -> str:
-        if (
-            self.config.force_default_name
-            or len(names) > 3
-            or len(names) != len(set(names))
-        ):
+    def choose_name(
+        self, target: Class, names: List[str], substitutions: List[str]
+    ) -> str:
+        if self.config.use_substitution_groups and substitutions:
+            names = substitutions
+
+        names = collections.unique_sequence(names)
+        if self.config.force_default_name or len(names) > self.config.max_name_parts:
             name = self.config.default_name
         else:
             name = "_Or_".join(names)
diff --git a/xsdata/codegen/models.py b/xsdata/codegen/models.py
index 206be926..a9d675ee 100644
--- a/xsdata/codegen/models.py
+++ b/xsdata/codegen/models.py
@@ -210,6 +210,7 @@ class Attr:
     help: Optional[str] = field(default=None, compare=False)
     restrictions: Restrictions = field(default_factory=Restrictions, compare=False)
     parent: Optional["Class"] = field(default=None, compare=False)
+    substitution: Optional[str] = field(default=None, compare=False)
 
     def __post_init__(self):
         self.local_name = self.name
diff --git a/xsdata/models/config.py b/xsdata/models/config.py
index ef1c5e7d..efa89aa8 100644
--- a/xsdata/models/config.py
+++ b/xsdata/models/config.py
@@ -222,15 +222,21 @@ class CompoundFields:
 
     :param enabled: Use compound fields for repeatable elements
     :param default_name: Default compound field name
-    :param force_default_name: Always use the default compound field,
-        otherwise if the number of elements is less than 4 the generator
-        will try to dynamically create the field name e.g.
+    :param use_substitution_groups: Use substitution groups if they
+        exist, instead of element names.
+    :param force_default_name: Always use the default compound field
+        name, or try to generate one by the list of element names if
+        they are no longer than the max name parts. e.g.
         hat_or_dress_or_something.
+    :param max_name_parts: Maximum number of element names before using
+        the default name.
     """
 
     enabled: bool = text_node(default=False, cli="compound-fields")
     default_name: str = attribute(default="choice", cli=False)
+    use_substitution_groups: bool = attribute(default=False, cli=False)
     force_default_name: bool = attribute(default=False, cli=False)
+    max_name_parts: int = attribute(default=3, cli=False)
 
 
 @dataclass


Test patch:
diff --git a/tests/codegen/handlers/test_add_attribute_substitutions.py b/tests/codegen/handlers/test_add_attribute_substitutions.py
index e42dc6c6..43546a60 100644
--- a/tests/codegen/handlers/test_add_attribute_substitutions.py
+++ b/tests/codegen/handlers/test_add_attribute_substitutions.py
@@ -81,6 +81,11 @@ class AddAttributeSubstitutionsTests(FactoryTestCase):
         self.assertEqual(1, target.attrs[2].restrictions.min_occurs)
         self.assertEqual(0, target.attrs[3].restrictions.min_occurs)
 
+        self.assertEqual("foo", target.attrs[0].substitution)
+        self.assertEqual("foo", target.attrs[1].substitution)
+        self.assertEqual(None, target.attrs[2].substitution)
+        self.assertEqual("foo", target.attrs[3].substitution)
+
         self.processor.process_attribute(target, second_attr)
         self.assertEqual(4, len(target.attrs))
 
diff --git a/tests/codegen/handlers/test_create_compound_fields.py b/tests/codegen/handlers/test_create_compound_fields.py
index fd738ff1..b96fe285 100644
--- a/tests/codegen/handlers/test_create_compound_fields.py
+++ b/tests/codegen/handlers/test_create_compound_fields.py
@@ -144,27 +144,32 @@ class CreateCompoundFieldsTests(FactoryTestCase):
     def test_choose_name(self):
         target = ClassFactory.create()
 
-        actual = self.processor.choose_name(target, ["a", "b", "c"])
+        actual = self.processor.choose_name(target, ["a", "b", "c"], [])
         self.assertEqual("a_Or_b_Or_c", actual)
 
-        actual = self.processor.choose_name(target, ["a", "b", "c", "d"])
+        actual = self.processor.choose_name(target, ["a", "b", "c", "d"], [])
         self.assertEqual("choice", actual)
 
         target.attrs.append(AttrFactory.create(name="choice"))
-        actual = self.processor.choose_name(target, ["a", "b", "c", "d"])
+        actual = self.processor.choose_name(target, ["a", "b", "c", "d"], [])
         self.assertEqual("choice_1", actual)
 
-        actual = self.processor.choose_name(target, ["a", "b", "c", "d"])
+        actual = self.processor.choose_name(target, ["a", "b", "c", "d"], [])
         self.assertEqual("choice_1", actual)
 
         self.processor.config.default_name = "ThisOrThat"
-        actual = self.processor.choose_name(target, ["a", "b", "c", "d"])
+        actual = self.processor.choose_name(target, ["a", "b", "c", "d"], [])
         self.assertEqual("ThisOrThat", actual)
 
         self.processor.config.force_default_name = True
-        actual = self.processor.choose_name(target, ["a", "b", "c"])
+        actual = self.processor.choose_name(target, ["a", "b", "c"], [])
         self.assertEqual("ThisOrThat", actual)
 
+        self.processor.config.force_default_name = False
+        self.processor.config.use_substitution_groups = True
+        actual = self.processor.choose_name(target, ["a", "b", "c"], ["d", "e", "f"])
+        self.assertEqual("d_Or_e_Or_f", actual)
+
     def test_build_reserved_names(self):
         base = ClassFactory.create(
             attrs=[
diff --git a/tests/models/test_config.py b/tests/models/test_config.py
index 33cc0ce1..5bde12fe 100644
--- a/tests/models/test_config.py
+++ b/tests/models/test_config.py
@@ -38,7 +38,7 @@ class GeneratorConfigTests(TestCase):
             "    <DocstringStyle>reStructuredText</DocstringStyle>\n"
             "    <FilterStrategy>allGlobals</FilterStrategy>\n"
             "    <RelativeImports>false</RelativeImports>\n"
-            '    <CompoundFields defaultName="choice" forceDefaultName="false">false</CompoundFields>\n'
+            '    <CompoundFields defaultName="choice" useSubstitutionGroups="false" forceDefaultName="false" maxNameParts="3">false</CompoundFields>\n'
             "    <PostponedAnnotations>false</PostponedAnnotations>\n"
             "    <UnnestClasses>false</UnnestClasses>\n"
             "    <IgnorePatterns>false</IgnorePatterns>\n"
@@ -100,7 +100,7 @@ class GeneratorConfigTests(TestCase):
             "    <DocstringStyle>reStructuredText</DocstringStyle>\n"
             "    <FilterStrategy>allGlobals</FilterStrategy>\n"
             "    <RelativeImports>false</RelativeImports>\n"
-            '    <CompoundFields defaultName="choice" forceDefaultName="false">false</CompoundFields>\n'
+            '    <CompoundFields defaultName="choice" useSubstitutionGroups="false" forceDefaultName="false" maxNameParts="3">false</CompoundFields>\n'
             "    <PostponedAnnotations>false</PostponedAnnotations>\n"
             "    <UnnestClasses>false</UnnestClasses>\n"
             "    <IgnorePatterns>false</IgnorePatterns>\n"