Task: Fixes #7699 (rdkit/rdkit#7904)

org: rdkit
repo: rdkit
pr_number: 7904
issue_title: Fixes #7699
issue_body: Stereo groups in product templates of reactions are now propagated to the product molecules.

If the atoms involved in those stereo groups are present in any stereo groups in the reactant molecules, they will be removed from those stereogroups when they are propagated to the products.
resolved_issues: number: 7669
title: Carry over enhanced stereochemistry groups from reaction product templates to products
body: **Is your feature request related to a problem? Please describe.**
The problem is discussed in https://github.com/rdkit/rdkit/discussions/7660
Basically, for reactions, while stereo groups from reactants are carried over after https://github.com/rdkit/rdkit/issues/2366, this does not happen for stereogroups in product templates, yet.

**Describe the solution you'd like**
I would like to see stereogroups from the product template to be copied over to the actual reaction products in the core RDKit code for reactions.

**Describe alternatives you've considered**
In the discussion linked above, a workaround is shown, which involves copying the stereogroups manually with the help of reaction-realted atom props annotated during reaction application.

**Additional context**
Copying over the example from the discussion.


```python
from rdkit import Chem
from rdkit.Chem import rdChemReactions

input = Chem.MolFromMolBlock(""""
  MJ231601                      

  5  4  0  0  1  0  0  0  0  0999 V2000
   -0.8258    1.4955    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
   -0.8258    0.6705    0.0000 F   0  0  0  0  0  0  0  0  0  0  0  0
   -1.6508    1.4955    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
   -0.8258    2.3205    0.0000 Cl  0  0  0  0  0  0  0  0  0  0  0  0
   -0.0008    1.4955    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
  1  2  1  0  0  0  0
  1  4  1  0  0  0  0
  1  3  1  0  0  0  0
  1  5  1  0  0  0  0
M  END
""")

# rxn molblock with enhanced stereochemistry
rxn = """$RXN V3000

      Mrv2401  072420241107

M  V30 COUNTS 1 1
M  V30 BEGIN REACTANT
M  V30 BEGIN CTAB
M  V30 COUNTS 5 4 0 0 1
M  V30 BEGIN ATOM
M  V30 1 C -1.5417 2.7917 0 1
M  V30 2 F -1.5417 1.2517 0 4
M  V30 3 C -3.0817 2.7917 0 3
M  V30 4 Cl -1.5417 4.3317 0 2
M  V30 5 C -0.0017 2.7917 0 0
M  V30 END ATOM
M  V30 BEGIN BOND
M  V30 1 1 1 2
M  V30 2 1 1 4
M  V30 3 1 1 3
M  V30 4 1 1 5
M  V30 END BOND
M  V30 END CTAB
M  V30 END REACTANT
M  V30 BEGIN PRODUCT
M  V30 BEGIN CTAB
M  V30 COUNTS 5 4 0 0 0
M  V30 BEGIN ATOM
M  V30 1 C 9.2083 2.9167 0 1
M  V30 2 C 7.6683 2.9167 0 3
M  V30 3 F 9.2083 1.3767 0 4
M  V30 4 Cl 9.2083 4.4567 0 2
M  V30 5 I 10.7483 2.9167 0 0
M  V30 END ATOM
M  V30 BEGIN BOND
M  V30 1 1 1 2
M  V30 2 1 1 3
M  V30 3 1 1 4
M  V30 4 1 1 5 CFG=1
M  V30 END BOND
M  V30 BEGIN COLLECTION
M  V30 MDLV30/STERAC1 ATOMS=(1 1)
M  V30 END COLLECTION
M  V30 END CTAB
M  V30 END PRODUCT
M  END
"""

rxn = rdChemReactions.ReactionFromRxnBlock(rxn)
rxn.Initialize()
p = rxn.RunReactants([input])
for prod in p:
    sgs = prod[0].GetStereoGroups()
    if len(sgs) <= 0:
        print("Stereochemistry missing :(")
```

```
Stereochemistry missing :(
Stereochemistry missing :(
```


Reference solution:
fix_patch: diff --git a/Code/GraphMol/ChemReactions/ReactionRunner.cpp b/Code/GraphMol/ChemReactions/ReactionRunner.cpp
index 19fc31fc14f..26639feb67a 100644
--- a/Code/GraphMol/ChemReactions/ReactionRunner.cpp
+++ b/Code/GraphMol/ChemReactions/ReactionRunner.cpp
@@ -1255,15 +1255,14 @@ void copyEnhancedStereoGroups(const ROMol &reactant, RWMOL_SPTR product,
   for (const auto &sg : reactant.getStereoGroups()) {
     std::vector<Atom *> atoms;
     std::vector<Bond *> bonds;
-    for (auto &&reactantAtom : sg.getAtoms()) {
+    for (const auto &reactantAtom : sg.getAtoms()) {
       auto productAtoms = mapping.reactProdAtomMap.find(reactantAtom->getIdx());
       if (productAtoms == mapping.reactProdAtomMap.end()) {
         continue;
       }
 
-      for (auto &&productAtomIdx : productAtoms->second) {
+      for (auto &productAtomIdx : productAtoms->second) {
         auto productAtom = product->getAtomWithIdx(productAtomIdx);
-
         // If chirality destroyed by the reaction, skip the atom
         if (productAtom->getChiralTag() == Atom::CHI_UNSPECIFIED) {
           continue;
@@ -1409,6 +1408,76 @@ void addReactantAtomsAndBonds(const ChemicalReaction &rxn, RWMOL_SPTR product,
   delete (mapping);
 }  // end of addReactantAtomsAndBonds
 
+namespace {
+void copyTemplateStereoGroupsToMol(const ROMol &templateMol,
+                                   RWMOL_SPTR product) {
+  const auto &stereoGroups = templateMol.getStereoGroups();
+  if (stereoGroups.empty()) {
+    return;
+  }
+
+  boost::dynamic_bitset<> atomsInTemplateStereoGroups(product->getNumAtoms());
+  std::vector<StereoGroup> newStereoGroups;
+  for (const auto &sg : stereoGroups) {
+    bool keepIt = true;
+    std::vector<Atom *> atoms;
+    for (const auto &atom : sg.getAtoms()) {
+      if (auto mapNum = atom->getAtomMapNum()) {
+        for (auto productAtom : product->atoms()) {
+          int oldMapNum = 0;
+          if (productAtom->getPropIfPresent(common_properties::reactionMapNum,
+                                            oldMapNum) &&
+              oldMapNum == mapNum) {
+            atoms.push_back(productAtom);
+            atomsInTemplateStereoGroups.set(productAtom->getIdx());
+          }
+        }
+      } else {
+        keepIt = false;
+        break;
+      }
+    }
+    if (keepIt && !atoms.empty()) {
+      std::vector<Bond *> bonds;
+      newStereoGroups.emplace_back(sg.getGroupType(), std::move(atoms),
+                                   std::move(bonds), sg.getReadId());
+    }
+  }
+  if (!newStereoGroups.empty()) {
+    // remove any stereo groups that are already present in the product (these
+    // were copied over from the reactant in copyEnhancedStereoGroups()) and
+    // that overlap with the added ones
+    for (const auto &productSG : product->getStereoGroups()) {
+      unsigned int nOverlappingAtoms = 0;
+      for (const auto atom : productSG.getAtoms()) {
+        if (atomsInTemplateStereoGroups[atom->getIdx()]) {
+          ++nOverlappingAtoms;
+        }
+      }
+      if (!nOverlappingAtoms) {
+        // no overlapping atoms, we can just keep the stereogroup.
+        newStereoGroups.push_back(productSG);
+      } else if (nOverlappingAtoms < productSG.getAtoms().size()) {
+        // some of the atoms in the stereo group are not already there
+        // in the product, we need to split the stereo group
+        std::vector<Atom *> newAtoms;
+        for (const auto atom : productSG.getAtoms()) {
+          if (!atomsInTemplateStereoGroups[atom->getIdx()]) {
+            newAtoms.push_back(atom);
+          }
+        }
+        std::vector<Bond *> newBonds;
+        newStereoGroups.emplace_back(productSG.getGroupType(),
+                                     std::move(newAtoms), std::move(newBonds),
+                                     productSG.getReadId());
+      }
+      // else: all atoms in the stereo group are already there, we can skip it
+    }
+    product->setStereoGroups(std::move(newStereoGroups));
+  }
+}
+}  // namespace
+
 MOL_SPTR_VECT
 generateOneProductSet(const ChemicalReaction &rxn,
                       const MOL_SPTR_VECT &reactants,
@@ -1469,6 +1538,11 @@ generateOneProductSet(const ChemicalReaction &rxn,
       MolOps::setDoubleBondNeighborDirections(*product);
     }
 
+    // if the product template has stereo groups, copy them over now
+    if (!(*pTemplIt)->getStereoGroups().empty()) {
+      copyTemplateStereoGroupsToMol(**pTemplIt, product);
+    }
+
     res[prodId] = product;
     ++prodId;
   }