001/*
002 *  Copyright 2020 Anyware Services
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016
017package org.ametys.plugins.odfpilotage.cost;
018
019import java.util.Collections;
020import java.util.HashMap;
021import java.util.List;
022import java.util.Map;
023import java.util.Objects;
024import java.util.Optional;
025import java.util.Set;
026import java.util.stream.Collectors;
027
028import org.apache.avalon.framework.component.Component;
029import org.apache.avalon.framework.service.ServiceException;
030import org.apache.avalon.framework.service.ServiceManager;
031import org.apache.avalon.framework.service.Serviceable;
032import org.apache.commons.lang3.StringUtils;
033
034import org.ametys.cms.data.ContentValue;
035import org.ametys.cms.repository.Content;
036import org.ametys.odf.ODFHelper;
037import org.ametys.odf.ProgramItem;
038import org.ametys.odf.course.Course;
039import org.ametys.odf.courselist.CourseList;
040import org.ametys.odf.courselist.CourseList.ChoiceType;
041import org.ametys.odf.coursepart.CoursePart;
042import org.ametys.odf.enumeration.OdfReferenceTableHelper;
043import org.ametys.odf.orgunit.OrgUnit;
044import org.ametys.odf.program.Container;
045import org.ametys.odf.program.Program;
046import org.ametys.odf.program.SubProgram;
047import org.ametys.plugins.odfpilotage.cost.entity.CostComputationData;
048import org.ametys.plugins.odfpilotage.cost.entity.CostComputationDataCache;
049import org.ametys.plugins.odfpilotage.cost.entity.CoursePartCostData;
050import org.ametys.plugins.odfpilotage.cost.entity.Effectives;
051import org.ametys.plugins.odfpilotage.cost.entity.EqTD;
052import org.ametys.plugins.odfpilotage.cost.entity.NormDetails;
053import org.ametys.plugins.odfpilotage.cost.entity.OverriddenData;
054import org.ametys.plugins.odfpilotage.cost.entity.VolumesOfHours;
055import org.ametys.plugins.odfpilotage.helper.PilotageHelper;
056import org.ametys.plugins.odfpilotage.helper.ReportHelper;
057import org.ametys.plugins.odfpilotage.helper.ReportUtils;
058import org.ametys.plugins.repository.AmetysObjectResolver;
059import org.ametys.plugins.repository.AmetysRepositoryException;
060import org.ametys.plugins.repository.UnknownAmetysObjectException;
061import org.ametys.runtime.model.ModelItem;
062import org.ametys.runtime.plugin.component.AbstractLogEnabled;
063
064/**
065 * This component computes values used by the cost modeling tool
066 */
067public class CostComputationComponent extends AbstractLogEnabled implements Component, Serviceable
068{
069    /** The Avalon role name */
070    public static final String ROLE = CostComputationComponent.class.getName();
071
072    private static final String __VALUE_YEAR = "annee";
073    private static final String __ETAPE_PORTEUSE = "etapePorteuse";
074    private static final String __EFFECTIF_ESTIMATED = "numberOfStudentsEstimated";
075    
076    /** The ODF enumeration helper */
077    protected OdfReferenceTableHelper _refTableHelper;
078    
079    /** The report helper */
080    protected ReportHelper _reportHelper;
081    
082    /** The ODF helper */
083    protected ODFHelper _odfHelper;
084    
085    /** The ametys object resolver */
086    protected AmetysObjectResolver _resolver;
087    
088    @Override
089    public void service(ServiceManager manager) throws ServiceException
090    {
091        _refTableHelper = (OdfReferenceTableHelper) manager.lookup(OdfReferenceTableHelper.ROLE);
092        _reportHelper = (ReportHelper) manager.lookup(ReportHelper.ROLE);
093        _odfHelper = (ODFHelper) manager.lookup(ODFHelper.ROLE);
094        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
095    }
096    
097    /**
098     * Call methods to initialize data and compute groups distribution from an orgUnit
099     * @param orgUnit the orgUnit
100     * @param catalog the catalog
101     * @param lang the lang
102     * @return CostData object containing informations about the formation 
103     */
104    public CostComputationData computeCostsOnOrgUnits(OrgUnit orgUnit, String catalog, String lang)
105    {
106        OverriddenData overriddenData = new OverriddenData();
107        return computeCostsOnOrgUnits(orgUnit, catalog, lang, overriddenData);
108    }
109    
110    /**
111     * Call methods to initialize data and compute groups distribution from an orgUnit
112     * @param orgUnit the orgUnit
113     * @param catalog the catalog
114     * @param lang the lang
115     * @param overriddenData overridden data by the user
116     * @return CostData object containing informations about the formation 
117     */
118    public CostComputationData computeCostsOnOrgUnits(OrgUnit orgUnit, String catalog, String lang, OverriddenData overriddenData)
119    {
120        CostComputationDataCache cache = new CostComputationDataCache(_refTableHelper.getItems(PilotageHelper.NORME), _refTableHelper.getItems(OdfReferenceTableHelper.ENSEIGNEMENT_NATURE), overriddenData);
121         
122        CostComputationData costData = new CostComputationData();
123        _populateOrgUnits(costData, orgUnit, cache, catalog, lang);
124        
125        return costData;
126    }
127    
128    /**
129     * Call methods to initialize data and compute groups distribution from a catalog
130     * @param programs the catalog
131     * @return CostData object containing informations about the formation
132     */
133    public CostComputationData computeCostsOnPrograms(List<Program> programs)
134    {
135        OverriddenData overriddenData = new OverriddenData();
136        return computeCostsOnPrograms(programs, overriddenData);
137    }
138    
139    /**
140     * Call methods to initialize data and compute groups distribution from a catalog
141     * @param programs the catalog
142     * @param overriddenData overridden data by the user
143     * @return CostData object containing informations about the formation
144     */
145    public CostComputationData computeCostsOnPrograms(List<Program> programs, OverriddenData overriddenData)
146    {
147        CostComputationDataCache cache = new CostComputationDataCache(_refTableHelper.getItems(PilotageHelper.NORME), _refTableHelper.getItems(OdfReferenceTableHelper.ENSEIGNEMENT_NATURE), overriddenData);
148         
149        CostComputationData costData = new CostComputationData();
150        
151        for (Program program : programs)
152        {
153            _setProgramItemStructure(program, costData, cache); 
154            _populateProgram(costData, program, program.getName(), cache);
155        }
156        return costData;
157    }
158    
159    /**
160     * Call methods to initialize data and compute groups distribution from a program
161     * @param program the program
162     * @return CostData object containing informations about the formation
163     */
164    public CostComputationData computeCostsOnProgram(Program program)
165    {
166        List<Program> programs = List.of(program);
167        return computeCostsOnPrograms(programs);
168    }
169    
170    /**
171     * Call methods to initialize data and compute groups distribution from a program
172     * @param program the program
173     * @param overriddenData overridden data by the user
174     * @return CostData object containing informations about the formation
175     */
176    public CostComputationData computeCostsOnProgram(Program program, OverriddenData overriddenData)
177    {
178        List<Program> programs = List.of(program);
179        return computeCostsOnPrograms(programs, overriddenData);
180    }
181
182    /**
183     * Iterate the program item structure and compute the global effective for each course part
184     * @param programItem the program item
185     * @param costData object containing informations about the formation
186     * @param cache the cache values
187     */
188    protected void _setProgramItemStructure(ProgramItem programItem, CostComputationData costData, CostComputationDataCache cache)  
189    {
190        // On ajout le programItem courant à la structure
191        List<ProgramItem> childs = _odfHelper.getChildProgramItems(programItem);
192        for (ProgramItem child : childs)
193        {
194            if (child instanceof Course && !((Course) child).hasCourseLists())
195            {
196                for (CoursePart coursePart : ((Course) child).getCourseParts())
197                {
198                    // On calcule l'effectif global et saisi pour chaque coursePart
199                    _getComputedGlobalAndEnteredEffectives(costData, coursePart, cache);
200                }
201            }
202            // Appel récursif pour continuer de parcourir la structure
203            _setProgramItemStructure(child, costData, cache);
204        }
205    }
206    
207    /** 
208     * Iterate and explore each orgUnit of the structure
209     * @param orgUnit the orgUnit
210     * @param costData object containing informations about the formation
211     * @param cache the cache values
212     * @param catalog the catalog
213     * @param lang the lang
214     */
215    protected void _populateOrgUnits(CostComputationData costData, OrgUnit orgUnit, CostComputationDataCache cache, String catalog, String lang)
216    {
217        List<String> orgUnitsIds = orgUnit.getSubOrgUnits();
218        String orgUnitPath = orgUnit.getName();
219        if (!orgUnitsIds.isEmpty() && orgUnit.getParentOrgUnit() == null)
220        {
221            VolumesOfHours volumeOfHours = new VolumesOfHours(orgUnit.getId());
222            EqTD eqTD = new EqTD(orgUnit.getId());
223            Effectives effectives = new Effectives(orgUnit.getId());
224            
225            for (String orgUnitId : orgUnitsIds)
226            {
227                OrgUnit childOrgUnit = _resolver.resolveById(orgUnitId);
228                String childOrgUnitPath = orgUnitPath + ModelItem.ITEM_PATH_SEPARATOR + childOrgUnit.getName();
229                _populateOrgUnit(costData, childOrgUnit, cache, childOrgUnitPath, catalog, lang);
230                
231                Effectives childEffective = costData.getEffective(childOrgUnit.getId());
232                effectives.sum(childEffective, childOrgUnitPath, orgUnitPath);
233                
234                // Calcule les volumes horaires
235                VolumesOfHours childVolumeOfHour = costData.getVolumesOfHours(childOrgUnit.getId());
236                volumeOfHours.sum(childVolumeOfHour, 1d);
237                
238                // Calcule les heures eqTD
239                EqTD childEqTD = costData.getEqTD(childOrgUnit.getId());
240                eqTD.sum(childEqTD, 1d, childOrgUnitPath, orgUnitPath);
241            }
242            // Stock les informations
243            costData.addEqTD(orgUnit.getId(), eqTD);
244            costData.addVolumeOfHours(orgUnit.getId(), volumeOfHours);
245            costData.setEffective(orgUnit.getId(), effectives);
246            Optional<Double> localEffective = costData.getComputedLocalEffective(orgUnit.getId(), orgUnitPath);
247            Double heReport = localEffective.isPresent() && localEffective.get() != 0 ? eqTD.getProRatedEqTD(orgUnitPath) / localEffective.get() : 0;
248            costData.addHeReport(orgUnitPath, heReport);
249            
250        }
251        else
252        {
253            _populateOrgUnit(costData, orgUnit, cache, orgUnitPath, catalog, lang);
254        }
255    }
256    
257    /**
258     * Iterate and explore each program of an orgUnit
259     * @param orgUnit the orgUnit
260     * @param costData object containing informations about the formation
261     * @param cache the cache values
262     * @param orgUnitPath the current path
263     * @param catalog the catalog 
264     * @param lang the lang
265     */
266    protected void _populateOrgUnit(CostComputationData costData, OrgUnit orgUnit, CostComputationDataCache cache, String orgUnitPath, String catalog, String lang)
267    {
268        List<Program> programs = _odfHelper.getProgramsFromOrgUnit(orgUnit, catalog, lang);
269        
270        VolumesOfHours volumeOfHours = new VolumesOfHours(orgUnit.getId());
271        EqTD eqTD = new EqTD(orgUnit.getId());
272        Effectives effectives = new Effectives(orgUnit.getId());
273        
274        for (Program program : programs)
275        {
276            String programPath = orgUnitPath + ModelItem.ITEM_PATH_SEPARATOR + program.getName();
277            _setProgramItemStructure(program, costData, cache); 
278            _populateProgram(costData, program, programPath, cache);
279            
280            // Somme les effectifs de chaque étape pour calculer les effectifs totaux de la formation
281            Effectives childEffective = costData.getEffective(program.getId());
282            effectives.sum(childEffective, programPath, orgUnitPath);
283            
284            // Calcule les volumes horaires
285            VolumesOfHours childVolumeOfHour = costData.getVolumesOfHours(program.getId());
286            volumeOfHours.sum(childVolumeOfHour, 1d);
287            
288            // Calcule les heures eqTD 
289            EqTD childEqTD = costData.getEqTD(program.getId());
290            eqTD.sum(childEqTD, 1d, programPath, orgUnitPath);
291        }
292        
293        // Stock les informations
294        costData.addEqTD(orgUnit.getId(), eqTD);
295        costData.addVolumeOfHours(orgUnit.getId(), volumeOfHours);
296        costData.setEffective(orgUnit.getId(), effectives);
297        Optional<Double> localEffective = costData.getComputedLocalEffective(orgUnit.getId(), orgUnitPath);
298        Double heReport = localEffective.isPresent() && localEffective.get() != 0 ? eqTD.getProRatedEqTD(orgUnitPath) / localEffective.get() : 0;
299        costData.addHeReport(orgUnitPath, heReport);
300    }
301    
302    /**
303     * Iterate and explore each container of the catalog
304     * @param program the catalog 
305     * @param costData object containing informations about the formation
306     * @param programPath the current path
307     * @param cache the cache values
308     */
309    protected void _populateProgram(CostComputationData costData, Program program, String programPath, CostComputationDataCache cache)
310    {
311        Map<String, Container> containers = _getSteps(costData, program, programPath);
312      
313        costData.addSteps(containers, program);
314        
315        if (containers.isEmpty())
316        {
317            getLogger().warn("[{}] La formation n'a pas de conteneur compatible pour le calcul de pilotage.", programPath);
318        }
319        else
320        {
321            getLogger().info("[{}] Traitement des conteneurs...", programPath);
322            VolumesOfHours volumeOfHours = new VolumesOfHours(program.getId());
323            EqTD eqTD = new EqTD(program.getId());
324            Effectives effectives = new Effectives(program.getId());
325            
326            for (String containerPath : containers.keySet())
327            {
328                Container container = containers.get(containerPath);
329                // Met en mémoire l'étape actuelle
330                cache.setCurrentStep(container);
331                
332                // Parcours les étapes de la formation
333                _populateStep(costData, container, containerPath, cache);
334                
335                // Somme les effectifs de chaque étape pour calculer les effectifs totaux de la formation
336                Effectives childEffective = costData.getEffective(container.getId());
337                effectives.sum(childEffective, containerPath, programPath);
338                
339                // Calcule les volumes horaires
340                VolumesOfHours childVolumeOfHour = costData.getVolumesOfHours(container.getId());
341                volumeOfHours.sum(childVolumeOfHour, 1d);
342                
343                // Calcule les heures eqTD
344                EqTD childEqTD = costData.getEqTD(container.getId());
345                eqTD.sum(childEqTD, 1d, containerPath, programPath);
346            }
347            
348            // Stock les informations
349            costData.addEqTD(program.getId(), eqTD);
350            costData.addVolumeOfHours(program.getId(), volumeOfHours);
351            costData.setEffective(program.getId(), effectives);
352            Optional<Double> localEffective = costData.getComputedLocalEffective(program.getId(), programPath);
353            Double heReport = localEffective.isPresent() && localEffective.get() != 0 ?  eqTD.getProRatedEqTD(programPath) / localEffective.get() : 0;
354            costData.addHeReport(programPath, heReport);
355        }
356        // Lorsque toutes les valeurs des containers sont calculées, on ventile au niveau des subProgram
357        _populateSubProgram(costData, program, programPath, cache);
358    }
359
360    
361    /**
362     * Iterate and explore each sub program of the catalog
363     * @param program the catalog 
364     * @param costData object containing informations about the formation
365     * @param programPath the current path
366     * @param cache the cache values
367     */
368    protected void _populateSubProgram(CostComputationData costData, Program program, String programPath, CostComputationDataCache cache)
369    {
370        Set<SubProgram> subPrograms = _odfHelper.getChildSubPrograms(program);
371        for (SubProgram subProgram : subPrograms)
372        {
373            String programItemPath = programPath + ModelItem.ITEM_PATH_SEPARATOR + subProgram.getName();
374            Map<String, Container> containers = _getSteps(costData, subProgram, programItemPath);
375            
376            VolumesOfHours volumeOfHours = new VolumesOfHours(subProgram.getId());
377            EqTD eqTD = new EqTD(subProgram.getId());
378            Effectives effectives = new Effectives(subProgram.getId());
379            
380            for (String containerPath : containers.keySet())
381            {
382                Container container = containers.get(containerPath);
383                
384                // Somme les effectifs de chaque étape pour calculer les effectifs totaux de la formation
385                Effectives childEffective = costData.getEffective(container.getId());
386                effectives.sum(childEffective, containerPath, programItemPath);
387               
388                // Calcule les heures eqTD
389                EqTD childEqTD = costData.getEqTD(container.getId());
390                eqTD.sum(childEqTD, 1d, containerPath, programItemPath);
391            }
392            
393            // Stock les informations
394            costData.addEqTD(subProgram.getId(), eqTD);
395            costData.addVolumeOfHours(subProgram.getId(), volumeOfHours);
396            costData.setEffective(subProgram.getId(), effectives);
397            Optional<Double> localEffective = costData.getComputedLocalEffective(subProgram.getId(), programItemPath);
398            Double heReport = localEffective.isPresent() && localEffective.get() != 0 ? eqTD.getProRatedEqTD(programItemPath) / localEffective.get() : 0;
399            costData.addHeReport(programItemPath, heReport);
400        }
401    }
402    
403    /**
404     * Iterate recursively each child of the current programItem to find the lowest course level
405     * @param costData object containing informations about the formation
406     * @param objectPath the current path
407     * @param programItem the current node
408     * @param cache the cache values
409     */
410    protected void _populateStep(CostComputationData costData, ProgramItem programItem, String objectPath, CostComputationDataCache cache)
411    {
412        getLogger().info("[{}] Exploration de l'arborescence...", objectPath);
413        
414        // On parcourt récursivement l'arbre de données jusqu'au cours de plus bas niveau
415        VolumesOfHours volumeOfHours = new VolumesOfHours(programItem.getId());
416        EqTD eqTD = costData.getEqTD(programItem.getId()) != null ? costData.getEqTD(programItem.getId()) : new EqTD(programItem.getId());
417        for (ProgramItem child : _odfHelper.getChildProgramItems(programItem))
418        {
419            Double weight = 1D;
420            String childPath = objectPath + ModelItem.ITEM_PATH_SEPARATOR + child.getName();
421            
422            if (child instanceof Course && !((Course) child).hasCourseLists())
423            {
424                // Parcourt les cours de plus bas niveau
425                _populateCourse(costData, (Course) child, childPath, cache);
426            }
427            else
428            {
429                // Si le programItem n'est pas un cours sans enfant, on continue recursivement
430                _populateStep(costData, child, childPath, cache);
431            }
432            
433            EqTD childEqTD = costData.getEqTD(child.getId());
434            // Calcule les heures eqTD
435            if (child instanceof Course)
436            {
437                // Calcul du poids 
438                weight = Optional.of(cache).map(c -> c.getWeight(childPath)).orElse(1d);
439
440                Container currentStepholder = cache.getCurrentStep();
441                Optional<ContentValue> stepHolder = Optional.of(child)          // Pour l'ELP courant
442                    .map(Course.class::cast)                                    // Cast en Course
443                    .map(c -> c.<ContentValue>getValue(__ETAPE_PORTEUSE));      // Récupérer l'étape porteuse
444                
445                boolean isHolded = stepHolder.isEmpty() 
446                        || stepHolder                       
447                            .flatMap(ContentValue::getContentIfExists)          // Récupérer le contenu s'il existe
448                            .map(Container.class::cast)                         // Cast en Container
449                            .map(currentStepholder::equals)                     // Est-ce que l'étape courante = étape porteuse ?
450                            .orElse(false);                                     // Sinon non porté
451                
452                // Si l'étape porteuse du cours n'est pas l'étape courante, la valeur de ses eqTD portés est de 0
453                if (!isHolded)
454                {
455                    childEqTD.addLocalEqTD(childPath, 0d);
456                }
457            }
458            eqTD.sum(childEqTD, weight, childPath, objectPath);
459            
460            // Calcule les volumes horaires
461            VolumesOfHours childVolumeOfHours = costData.getVolumesOfHours(child.getId());
462            volumeOfHours.sum(childVolumeOfHours, weight);
463        }
464        
465        // Stock les informations
466        costData.addEqTD(programItem.getId(), eqTD);
467        Optional<Double> localEffective = costData.getComputedLocalEffective(programItem.getId(), objectPath);
468        Double heReport =  localEffective.isPresent() && localEffective.get() != 0 ? eqTD.getProRatedEqTD(objectPath) / localEffective.get() : 0;
469        costData.addHeReport(objectPath, heReport);
470        costData.addVolumeOfHours(programItem.getId(), volumeOfHours);
471    }
472    
473    /**
474     * Explore the course and iterate its course part to populate them
475     * @param costData object containing informations about the formation
476     * @param coursePath the current path
477     * @param course the course
478     * @param cache the cache values
479     */
480    protected void _populateCourse(CostComputationData costData, Course course, String coursePath, CostComputationDataCache cache)
481    {
482        getLogger().info("[{} - {}] Lecture de l'ELP...", coursePath, course.getCode());
483        
484        VolumesOfHours volumeOfHour = new VolumesOfHours(course.getId());
485        EqTD eqTD = costData.getEqTD(course.getId()) != null ? costData.getEqTD(course.getId()) : new EqTD(course.getId());
486        for (CoursePart coursePart : course.getCourseParts())
487        {
488            String coursePartPath = coursePath + ModelItem.ITEM_PATH_SEPARATOR + coursePart.getName();
489            
490            getLogger().info("[{} - {}] Lecture des heures d'enseignement...", coursePartPath, coursePart.getCode());
491            
492            _populateCoursePart(costData, coursePart, course, coursePartPath, cache);
493            
494            // Calcule les volumes horaires
495            VolumesOfHours childVolumeOfHours = costData.getVolumesOfHours(coursePart.getId());
496            volumeOfHour.sum(childVolumeOfHours, 1d);
497            
498            // Calcule les heures eqTD
499            EqTD childEqTD = costData.getEqTD(coursePart.getId());
500            eqTD.sum(childEqTD, 1d, coursePartPath, coursePath);
501        }
502        // Stock les informations
503        costData.addEqTD(course.getId(), eqTD);
504        Optional<Double> localEffective = costData.getComputedLocalEffective(course.getId(), coursePath);
505        Double heReport = localEffective.isPresent() && localEffective.get() != 0 ? eqTD.getProRatedEqTD(coursePath) / localEffective.get() : 0;
506        costData.addHeReport(coursePath, heReport);
507        costData.addVolumeOfHours(course.getId(), volumeOfHour);
508    }
509    
510    /**
511     * Fill a costData object with informations about the course and the course part
512     * @param costData object containing informations about the formation
513     * @param coursePart the object to explore
514     * @param courseParent The parent of the course part
515     * @param coursePartPath the current path
516     * @param cache the cache values
517     */
518    protected void _populateCoursePart(CostComputationData costData, CoursePart coursePart, Course courseParent, String coursePartPath, CostComputationDataCache cache)
519    {
520        // Récupère les volumes horaires du coursePart et les ajoutes dans costData pour pouvoir les ventiler dans les autres programItem
521        VolumesOfHours volumeOfHours = new VolumesOfHours(_refTableHelper.getItemCode(coursePart.getNature()));
522        volumeOfHours.addVolumes(_refTableHelper.getItemCode(coursePart.getNature()), coursePart.getNumberOfHours());
523        costData.addVolumeOfHours(coursePart.getId(), volumeOfHours);
524        
525        // Récupération de l'ELP porteur
526        Course courseHolder = coursePart.getCourseHolder();
527        if (courseHolder == null)
528        {
529            getLogger().warn("[{}] L'ELP porteur est soit vide, soit invalide.", coursePartPath);
530        }
531        
532        // Récupération des détails de la norme
533        Container stepHolder =  Optional.ofNullable(courseHolder)
534                .map(ch -> _getStepHolder(ch, coursePartPath))
535                .orElse(null);
536        Optional<NormDetails> normDetails =  _getNormDetails(stepHolder, coursePart, coursePartPath, cache);
537        if (normDetails.isEmpty())
538        {
539            getLogger().warn("[{}] Les effectifs maximum et minimum supplémentaires ne peuvent pas être récupérés.", coursePartPath);
540            return;
541        }
542        
543        // Calcule toutes les données nécessaires et les stock dans l'objet CoursePartInformation
544        _computeEffectiveAndVentilation(costData, coursePart, courseParent, normDetails.get(), coursePartPath, cache);
545    }
546  
547    /**
548     * Handle all effectives computations from a course part
549     * @param costData object containing informations about the formation
550     * @param coursePart the object to explore
551     * @param courseParent the parent of the course part
552     * @param normDetails informations about the norm of the course part
553     * @param coursePartPath the course part path
554     * @param cache the cache values
555     */
556    protected void _computeEffectiveAndVentilation(CostComputationData costData,  CoursePart coursePart, Course courseParent, NormDetails normDetails, String coursePartPath, CostComputationDataCache cache)
557
558    {
559        CoursePartCostData coursePartCostData = new CoursePartCostData();
560        
561        // Récupère les volumes horaires du coursePart et les ajoutes dans costData pour pouvoir les ventiler dans les autres programItem
562        String natureId = _refTableHelper.getItemCode(coursePart.getNature());
563        Double numberOfHours = cache.getOverriddenVolumeOfHours(coursePart.getId(), natureId).isPresent() ? cache.getOverriddenVolumeOfHours(coursePart.getId(), natureId).get() : coursePart.getNumberOfHours();
564        VolumesOfHours volumeOfHours = new VolumesOfHours(natureId);
565        volumeOfHours.addVolumes(natureId, numberOfHours);
566        costData.addVolumeOfHours(coursePart.getId(), volumeOfHours);
567        
568        // Prend en compte les effectifs saisis
569        Double enteredEffective = costData.getEnteredEffective(coursePart.getId()).isPresent() ? costData.getEnteredEffective(coursePart.getId()).get() : 0d;
570 
571        // Effectifs provenant uniquement des effectifs saisis au niveau des années auquel un poids est appliqué selon le chemin emprunté
572        Effectives computedEffective = _getComputedLocalEffective(costData, coursePart, courseParent, coursePartPath, cache);
573
574        Long groupsToOpen = cache.getOverriddenGroups(coursePart.getId()).isPresent() ? cache.getOverriddenGroups(coursePart.getId()).get().longValue() : coursePart.getValue("groupsToOpen", false, 0L);
575        Long computedGroups = _computeGroups(enteredEffective, computedEffective.getComputedGlobalEffective(), normDetails);
576        Double eqTDCoef = ReportUtils.transformEqTD2Double(coursePart.getValue("eqTD", false, StringUtils.EMPTY));
577        if (eqTDCoef == null)
578        {
579            eqTDCoef = cache.getEqTDByNature(coursePart.getNature());
580        }
581        double eqTDTotal = _computeEqTDLocal(numberOfHours, groupsToOpen > 0 ? groupsToOpen : computedGroups, eqTDCoef);
582       
583        coursePartCostData.setNormDetails(normDetails);
584        coursePartCostData.setComputedGroups(computedGroups);
585        coursePartCostData.setGroupsToOpen(groupsToOpen);
586        
587        EqTD eqTD = costData.getEqTD(coursePart.getId()) != null ? costData.getEqTD(coursePart.getId()) : new EqTD(coursePart.getId());
588        eqTD.setGlobalEqTD(eqTDTotal);
589        
590        // Calcul de l'EQTD proratisé en calculant un ratio entre l'effectif local et l'effectif local. Si l'effectif est surchargé alors on le prend à la place de l'effectif global
591        Double globalEffective = costData.getEnteredEffective(coursePart.getId()).isPresent() ? costData.getEnteredEffective(coursePart.getId()).get() : computedEffective.getComputedGlobalEffective();
592        double ratio = computedEffective.getComputedGlobalEffective() == 0 ? 0 : computedEffective.getComputedLocalEffective(coursePartPath) / globalEffective;
593        double eqTDTotalProRated = eqTDTotal * ratio; 
594        eqTD.addProRatedEqTD(coursePartPath, eqTDTotalProRated);
595
596        // Heures eqTD portées
597        Course courseHolder = coursePart.getCourseHolder();
598        Double eqTDLocal = eqTDTotal;
599        // Si un cours mutualisé n'est pas porté par l'étape courante, on ne lui
600        // affecte pas les heures eqTD de ce cours
601        if (!Objects.equals(courseHolder, courseParent))
602        {
603            eqTDLocal = 0d;
604        }
605        eqTD.addLocalEqTD(coursePartPath, eqTDLocal);
606        costData.addEqTD(coursePart.getId(), eqTD);
607        Double heReport = costData.getComputedLocalEffective(coursePart.getId(), coursePartPath).get() != 0 ? eqTDTotalProRated / costData.getComputedLocalEffective(coursePart.getId(), coursePartPath).get() : 0;
608        costData.addHeReport(coursePartPath, heReport);
609        costData.addCoursePartCostData(coursePart, coursePartCostData);
610        _computeEqTDVentilation(costData, coursePart, enteredEffective, globalEffective, eqTDTotal);
611    }
612    
613    /**
614     * Compute the effective and the eqTD ventilation by step
615     * @param costData object containing informations about the formation
616     * @param coursePart the object to explore
617     * @param enteredEffective the entered effective of the coursePart
618     * @param globalEffective the global effective of the coursePart
619     * @param eqTDTotal the total eqTD value of the coursePart
620     */
621    protected void _computeEqTDVentilation(CostComputationData costData, CoursePart coursePart, Double enteredEffective, Double globalEffective, Double eqTDTotal)
622    {
623        Map<Container, Double> ventilationEffective = costData.getEffectiveByStep(coursePart.getId());
624        
625        // Ventilation eqTD
626        Map<Container, Double> ventilationEqTD = new HashMap<>();
627        for (Map.Entry<Container, Double> effectifsForStep : ventilationEffective.entrySet())
628        {
629            Double effectif = effectifsForStep.getValue();
630            double effectifTotal = effectif > enteredEffective ? globalEffective : enteredEffective;
631            double eqTDVentile = _calculEqTD(effectif, effectifTotal, eqTDTotal);
632            ventilationEqTD.put(effectifsForStep.getKey(), eqTDVentile);
633        }
634        ventilationEqTD.values().removeIf(Objects::isNull);
635        costData.addEqTDByStep(coursePart.getId(), ventilationEqTD);
636    }
637    
638    /**
639     * Divide the effective of a program item by the total effective of a course list to retrieve the weight 
640     * @param programItem the program item 
641     * @param programItemParent the parent of the program item
642     * @param costData object containing informations about the formation
643     * @return the weight of the program item
644     */
645    protected Double _getCourseWeight(ProgramItem programItem, ProgramItem programItemParent, CostComputationData costData)
646    {
647        Double totalEffective = 0d;
648        Double weight = 1d;
649        if (programItemParent instanceof CourseList && ((CourseList) programItemParent).getType() == ChoiceType.CHOICE)
650        {
651            CourseList courseList = (CourseList) programItemParent;
652            List<Course> children = courseList.getCourses();
653            for (Course course : children)
654            {
655                Optional<Double> enteredEffective = costData.getEnteredEffective(course.getId());
656                Optional<Double> globalComputedEffective = costData.getGlobalComputedEffective(course.getId());
657                totalEffective += enteredEffective.isPresent() ? enteredEffective.get() : globalComputedEffective.isPresent() ? globalComputedEffective.get() : 0d;
658            }
659            // Si un effectif est saisi, on calcule le poids à partir de ce dernier
660            if (costData.getEnteredEffective(programItem.getId()).isPresent() && totalEffective > 0)
661            {
662                weight = costData.getEnteredEffective(programItem.getId()).get() / totalEffective;
663            }
664            // Sinon on utilise l'effectif global
665            else if (costData.getGlobalComputedEffective(programItem.getId()).isPresent() && totalEffective > 0)
666            {
667                weight = costData.getGlobalComputedEffective(programItem.getId()).get() / totalEffective;
668            }
669        }
670        return weight;
671    }
672    
673    /**
674     * Get the step holder associated to the course
675     * @param courseHolder the course holder of a course part
676     * @param coursePartPrefix the current prefix
677     * @return the step holder
678     */
679    protected Container _getStepHolder(Course courseHolder, String coursePartPrefix)
680    {
681        Set<Container> stepsHolder = _getStepsHolder(courseHolder, coursePartPrefix);
682        Container stepHolder = null;
683        if (stepsHolder.size() == 1)
684        {
685            stepHolder = stepsHolder.iterator().next();
686        }
687        else if (stepsHolder.isEmpty())
688        {
689            getLogger().warn("Aucune étape n'est rattachée à l'ELP '{}' directement ou indirectement", courseHolder.getTitle());
690        }
691        else
692        {
693            getLogger().warn("Plusieurs étapes sont rattachées à l'ELP '{}', impossible de déterminer laquelle est porteuse.", courseHolder.getTitle());
694        }
695        return stepHolder;
696        
697    }
698    
699    /**
700     * Retrieve the value of the norm
701     * @param coursePart informations about the teaching hours of a course
702     * @param coursePartPrefix the current prefix
703     * @param stepHolder the step holder of the course
704     * @return the norm value
705     * @param cache the cache values
706     */
707    protected String _getNorm(CoursePart coursePart, String coursePartPrefix, Container stepHolder, CostComputationDataCache cache)
708    {
709        String norm = _getNorm(coursePart, coursePart.getNature(), coursePartPrefix, false, cache);
710        if (norm == null && stepHolder != null)
711        {
712            norm = _getNorm(stepHolder, coursePart.getNature(), coursePartPrefix, true, cache);
713        } 
714        return norm;
715    }
716    
717    /**
718     * Get all informations about the norm
719     * @param stepHolder the step holder
720     * @param coursePart informations about the teaching hours of a course
721     * @param coursePartPrefix the current prefix
722     * @return norm details
723     * @param cache the cache values
724     */
725    protected Optional<NormDetails> _getNormDetails(Container stepHolder, CoursePart coursePart, String coursePartPrefix, CostComputationDataCache cache)
726    {
727        // Récupération de la norme
728        String norm = _getNorm(coursePart, coursePartPrefix, stepHolder, cache);
729        String nature = coursePart.getNature();
730        
731        return Optional.ofNullable(norm)
732                .map(n -> cache.getNorms(n, nature))
733                .or(() -> _getNormFromNature(nature, cache));
734    }
735    
736    private Optional<NormDetails> _getNormFromNature(String nature, CostComputationDataCache cache)
737    {
738        return Optional.of(nature)
739            .map(cache::getEffectiveMinMaxByNature)
740            .map(eff ->
741                new NormDetails(
742                    eff.get("effectifMinSup"),
743                    eff.get("effectifMax"),
744                    StringUtils.EMPTY
745                )
746            );
747    }
748   
749    /**
750     * Get the computed global and entered effectives
751     * @param costData object containing informations about the formation
752     * @param coursePart the course part
753     * @param cache the cached values
754     * @return the computed global and entered effectives
755     */
756    protected Effectives _getComputedGlobalAndEnteredEffectives(CostComputationData costData, CoursePart coursePart, CostComputationDataCache cache)
757    {
758        // Consulte le cache pour vérifier si un effectif a déjà été calculé pour ce coursePart
759        Effectives computedEffective = cache.getComputedEffectiveCache(coursePart.getId());
760        if (computedEffective == null)
761        {
762            computedEffective = new Effectives(coursePart.getId());
763
764            for (Course course : coursePart.getCourses())
765            {
766                computedEffective.sumGlobalEffective(_computeGlobalAndEnteredEffectives(costData, coursePart, course, 1d, cache).getComputedGlobalEffective());
767                costData.add(course.getId(), coursePart.getId());
768            }
769      
770            cache.putComputedEffectiveCache(coursePart.getId(), computedEffective);
771            costData.setEffective(coursePart.getId(), computedEffective);
772
773        }
774        return computedEffective;
775    }
776    
777    /**
778     * Compute the global and entered effectives
779     * @param costData object containing informations about the formation
780     * @param coursePart the course part
781     * @param programItem the item we want to evaluate the capacity
782     * @param initialWeight the weight
783     * @param cache the cached values
784     * @return the computed effective
785     */
786    protected Effectives _computeGlobalAndEnteredEffectives(CostComputationData costData, CoursePart coursePart, ProgramItem programItem, Double initialWeight, CostComputationDataCache cache)
787    {
788        Effectives computedEffective = cache.getComputedEffectiveCache(programItem.getId());
789        if (computedEffective == null)
790        {
791            computedEffective = new Effectives(programItem.getId());
792            Double enteredEffective = programItem instanceof CourseList || ((Content) programItem).getValue(__EFFECTIF_ESTIMATED, false, 0L).doubleValue() == 0d ? null : ((Content) programItem).getValue(__EFFECTIF_ESTIMATED, false, 0L).doubleValue();
793            cache.putEnteredEffectiveCache(programItem.getId(), enteredEffective);
794            computedEffective.setEnteredEffective(Optional.ofNullable(enteredEffective));
795            if (programItem instanceof Container && _refTableHelper.getItemCode(((Container) programItem).getNature()).equals(__VALUE_YEAR))
796            {
797                // Add the etape to course part information
798                Double effectiveEstimated = cache.getOverriddenEffective(programItem.getId()).isPresent() ? cache.getOverriddenEffective(programItem.getId()).get() : ((Content) programItem).getValue(__EFFECTIF_ESTIMATED, false, 0L).doubleValue();
799                computedEffective.sumGlobalEffective(effectiveEstimated);
800                costData.add(effectiveEstimated, (Container) programItem, programItem);
801
802                if (effectiveEstimated == 0)
803                {
804                    getLogger().warn("[{}] Aucun effectif n'a été saisi sur cette étape.", ((Container) programItem).getTitle());
805                }
806            }
807            else
808            {
809                double weight = _getWeight(programItem);
810                for (ProgramItem parent : _odfHelper.getParentProgramItems(programItem))
811                {
812                    computedEffective.sumGlobalEffective(_computeGlobalAndEnteredEffectives(costData, coursePart, parent, weight, cache).getComputedGlobalEffective());
813                    costData.add(parent.getId(), programItem.getId());
814                }
815            }
816            costData.addGlobalComputedEffective(programItem.getId(), computedEffective.getComputedGlobalEffective());
817            costData.addEnteredEffective(programItem.getId(), computedEffective.getEnteredEffective());
818            cache.putComputedEffectiveCache(programItem.getId(), computedEffective);
819        }
820        computedEffective.globalhWeight(initialWeight);
821        costData.cloneWithWeight(initialWeight, programItem);
822        return computedEffective;
823    }
824      
825    /**
826     * Get the computed local effective
827     * @param costData object containing informations about the formation
828     * @param coursePart the course part
829     * @param courseParent the course parent of the course part
830     * @param coursePartPath the course part path
831     * @param cache the cache values
832     * @return the computed effective
833     */
834    protected Effectives _getComputedLocalEffective(CostComputationData costData, CoursePart coursePart, Course courseParent, String coursePartPath, CostComputationDataCache cache)
835    {
836        // Consulte le cache pour vérifier si un effectif a déjà été calculé pour ce coursePart
837        Effectives computedEffective = cache.getComputedEffectiveCache(coursePart.getId()) == null ? new Effectives(coursePart.getId()) : cache.getComputedEffectiveCache(coursePart.getId());
838       
839        String coursePath = StringUtils.substringBeforeLast(coursePartPath, ModelItem.ITEM_PATH_SEPARATOR);
840
841        Effectives currentEffectif = _computeLocalEffective(costData, coursePart, courseParent, coursePath, cache);
842       
843        // Somme la valeur des effectifs du cours parent à son coursePart
844        computedEffective.sumLocalEffective(currentEffectif, coursePath, coursePartPath);
845        cache.putComputedEffectiveCache(coursePart.getId(), computedEffective);
846        costData.addLocalComputedEffective(coursePart.getId(), coursePartPath, computedEffective.getComputedLocalEffective(coursePartPath));
847        return computedEffective;
848    }
849    
850    /**
851     * Compute the local effective
852     * @param costData object containing informations about the formation
853     * @param coursePart the course part
854     * @param programItem the item we want to evaluate the capacity
855     * @param programItemPath the current path
856     * @param cache the cache values
857     * @return the computed effective
858     */
859    protected Effectives _computeLocalEffective(CostComputationData costData, CoursePart coursePart, ProgramItem programItem, String programItemPath, CostComputationDataCache cache)
860    {
861        Effectives computedEffective = cache.getComputedEffectiveCache(programItem.getId()) != null ? cache.getComputedEffectiveCache(programItem.getId()) : new Effectives(programItem.getId());
862        if (!computedEffective.containsComputedLocalEffective(programItemPath))
863        {
864            if (programItem instanceof Container && _refTableHelper.getItemCode(((Container) programItem).getNature()).equals(__VALUE_YEAR))
865            {
866                // numberOfStudentsEstimated est de type Long mais le getDouble sur une propriété Long en JCR fonctionne très bien et fait un #doubleValue() sur le Long
867                Double effectiveEstimated = cache.getOverriddenEffective(programItem.getId()).isPresent() ? cache.getOverriddenEffective(programItem.getId()).get() : ((Content) programItem).getValue(__EFFECTIF_ESTIMATED, false, 0L).doubleValue();
868                computedEffective.addComputedLocalEffective(programItemPath, effectiveEstimated);
869                costData.addLocalComputedEffective(programItem.getId(), programItemPath, computedEffective.getComputedLocalEffective(programItemPath));
870                cache.putComputedEffectiveCache(programItem.getId(), computedEffective);
871                if (effectiveEstimated == 0)
872                {
873                    getLogger().warn("[{}] Aucun effectif n'a été saisi sur cette étape.", ((Container) programItem).getTitle());
874                }
875            }
876            else
877            {
878                // Parcourt l'intégralité des parents du programItem (globaux et locaux) pour gérer la mutualisation
879                for (ProgramItem parent : _odfHelper.getParentProgramItems(programItem))
880                {
881                    // Détermine le path du parent à partir du path enfant
882                    String parentPath = StringUtils.substringBeforeLast(programItemPath, ModelItem.ITEM_PATH_SEPARATOR);
883                    // Détermine le nom du parent à partir du path
884                    String parentName = StringUtils.substringAfterLast(parentPath, ModelItem.ITEM_PATH_SEPARATOR);
885                    // Si le nom du parent contenu dans le path courant est le même que le nom du parent retrouvé par le helper, nous sommes dans la structure locale 
886                    if (parentName.equals(parent.getName()))
887                    {
888                        Effectives effective = _computeLocalEffective(costData, coursePart, parent, parentPath, cache);
889                       
890                        // Ajoute la valeur de l'effectif local du parent à celle du programItem
891                        computedEffective.sumLocalEffective(effective, parentPath, programItemPath);
892                        // Détermine le poids du programItem et l'applique à l'effectif local
893                        _computeLocalEffectiveByWeight(costData, programItem, parent, computedEffective, programItemPath, cache);
894                    }
895                }
896            }
897        }
898        return computedEffective;
899    }
900    
901    /**
902     * Compute the local effective of a programItem 
903     * @param costData object containing informations about the formation
904     * @param programItem the current programItem
905     * @param programItemParent the parent of the programItem
906     * @param computedEffective the computed effective
907     * @param programItemPath the current path
908     * @param cache the values cache
909     */
910    protected void _computeLocalEffectiveByWeight(CostComputationData costData, ProgramItem programItem, ProgramItem programItemParent, Effectives computedEffective, String programItemPath, CostComputationDataCache cache)
911    {
912        Double weight = _getCourseWeight(programItem, programItemParent, costData);
913        cache.putWeight(programItemPath, weight);
914        computedEffective.localWeight(weight, programItemPath);
915        costData.addLocalComputedEffective(programItem.getId(), programItemPath, computedEffective.getComputedLocalEffective(programItemPath));
916        cache.putComputedEffectiveCache(programItem.getId(), computedEffective);
917    }
918  
919    /**
920     * Get all steps of a program item
921     * @param costData object containing informations about the formation
922     * @param programItem the current node
923     * @param programItemPath the current path
924     * @return all steps of a program item
925     */
926    protected Map<String, Container> _getSteps(CostComputationData costData, ProgramItem programItem, String programItemPath)
927    {
928        Map<String, Container> steps = new HashMap<>();
929        for (ProgramItem child : _odfHelper.getChildProgramItems(programItem))
930        {
931            if (child instanceof Container && _refTableHelper.getItemCode(((Container) child).getNature()).equals(__VALUE_YEAR))
932            {
933                Container container = (Container) child;
934                String containerPath =  programItemPath + ModelItem.ITEM_PATH_SEPARATOR + container.getName();
935                steps.put(containerPath, container);
936            }
937            else if (child instanceof Container || child instanceof SubProgram)
938            {
939                steps.putAll(_getSteps(costData, child, programItemPath + ModelItem.ITEM_PATH_SEPARATOR + child.getName()));
940            }
941        }
942        return steps;
943    }
944    
945    /**
946     * Define the weight of an item
947     * @param programItem the item to evaluate
948     * @return the weight
949     */
950    protected double _getWeight(ProgramItem programItem)
951    {
952        double weight = 1.0;
953        if (programItem instanceof CourseList)
954        {
955            CourseList courseList = (CourseList) programItem;
956            ChoiceType type = courseList.getType();
957            
958            switch (type)
959            {
960                case OPTIONAL:
961                    // Si c'est une liste optionnelle, les effectifs et groupes ne sont pas calculés
962                    weight = 0.0;
963                    break;
964                case CHOICE: 
965                    // Si c'est une liste à choix, le poids est calculé
966                    long min = courseList.getMinNumberOfCourses();
967                    long max = courseList.getMaxNumberOfCourses();
968                    long size = courseList.getCourses().size();
969                    
970                    if (min != max)
971                    {
972                        getLogger().warn("[{}] La liste contient min={} et max={}. Retenu {}", courseList.getTitle(), min, max, min);
973                    }
974                    
975                    if (min > size)
976                    {
977                        getLogger().warn("[{}] La liste contient min={} mais n'a que {} éléments.", courseList.getTitle(), min, size);
978                    }
979                    else
980                    {
981                        weight = (double) min / (double) size;
982                    }
983                    break;
984                default:
985                    // Si c'est une liste obligatoire (ou de type null), le poids reste à 1
986                    break;
987            }
988        }
989        return weight;
990    }
991    
992    /**
993     * Compute the TD equivalent
994     * @param effectif the capacity
995     * @param effectifTotal the total capacity
996     * @param eqTDTotal the total TD equivalent
997     * @return the computed TD equivalent
998     */
999    protected double _calculEqTD(double effectif, double effectifTotal, double eqTDTotal)
1000    {
1001        if (effectifTotal == 0.0)
1002        {
1003            return 0.0;
1004        }
1005        return eqTDTotal * effectif / effectifTotal;
1006    }
1007    
1008    /**
1009     * Compute the total TD equivalent
1010     * @param duration the duration of the TD
1011     * @param groupes the group repartition
1012     * @param eqTD the TD equivalent
1013     * @return the computed TD equivalent
1014     */
1015    protected double _computeEqTDLocal(double duration, long groupes, Double eqTD)
1016    {
1017        if (eqTD != null)
1018        {
1019            return duration * groupes * eqTD;
1020        }
1021        return 0;
1022    }
1023    
1024    /**
1025     * compute the number of groups needed 
1026     * @param enteredEffective the forecast capacity
1027     * @param computedEffective the computed capacity
1028     * @param normDetails all informations needed to compute the norm
1029     * @return the number of groups to open
1030     */
1031    protected long _computeGroups(double enteredEffective, double computedEffective, NormDetails normDetails)
1032    {
1033        double effective = enteredEffective > 0 ? enteredEffective : computedEffective;
1034        long effectiveMax = normDetails.getEffectiveMax();
1035        long effectiveMinSup = normDetails.getEffectiveMinSup();
1036        if (effective == 0)
1037        {
1038            return 0;
1039        }
1040        
1041        if (effective < effectiveMax)
1042        {
1043            return 1;
1044        }
1045        
1046        long groups = (long) effective / effectiveMax;
1047        if (effective - groups * effectiveMax >= effectiveMinSup)
1048        {
1049            groups++;
1050        }
1051        
1052        return groups;
1053    }
1054    
1055    /**
1056     * Find steps holders
1057     * @param programItem the item we want to explore
1058     * @param coursePartPrefix the current prefix
1059     * @return a list of steps holders
1060     */
1061    protected Set<Container> _getStepsHolder(ProgramItem programItem, String coursePartPrefix)
1062    {
1063        // Search if the current element is a course and has a step holder
1064        if (programItem instanceof Course)
1065        {
1066            Course course = (Course) programItem;
1067            ContentValue stepHolder = course.getValue(__ETAPE_PORTEUSE);
1068            if (stepHolder != null)
1069            {
1070                getLogger().info("[{}] L'ELP {} ({}) contient une étape porteuse.", coursePartPrefix, course.getTitle(), course.getId());
1071                
1072                try
1073                {
1074                    return Collections.singleton((Container) stepHolder.getContent());
1075                }
1076                catch (AmetysRepositoryException e)
1077                {
1078                    getLogger().warn("[{}] L'étape porteuse {} référencée par l'ELP {} ({}) n'a pas été trouvée. Vérifiez qu'elle n'a pas été supprimée.", coursePartPrefix, stepHolder.getContentId(), course.getTitle(), course.getId());
1079                }
1080            }
1081        }
1082        // Search if the current element is a container and is of type year
1083        else if (programItem instanceof Container)
1084        {
1085            Container container = (Container) programItem;
1086            if (_refTableHelper.getItemCode(container.getNature()).equals(__VALUE_YEAR))
1087            {
1088                return Collections.singleton(container);
1089            }
1090        }
1091        
1092        // In all other cases, search in the parent elements
1093        return _odfHelper.getParentProgramItems(programItem)
1094            .stream()
1095            .map(child -> _getStepsHolder(child, coursePartPrefix))
1096            .flatMap(Set::stream)
1097            .collect(Collectors.toSet());
1098    }
1099    
1100    /**
1101     * Retrieve the norm value of a content
1102     * @param content the content
1103     * @param nature the nature of the content
1104     * @param prefix the current prefix
1105     * @param isStepholder define if the content is a step holder or not
1106     * @param cache the cache values
1107     * @return the norm of the content
1108     */
1109    protected String _getNorm(Content content, String nature, String prefix, boolean isStepholder, CostComputationDataCache cache)
1110    {
1111        String logName = isStepholder ? "étape porteuse" : "heure d'enseignement";
1112        
1113        ContentValue normValue = content.getValue("norme");
1114        String normId = Optional.ofNullable(normValue)
1115                                 .map(ContentValue::getContentId)
1116                                 .orElse(StringUtils.EMPTY);
1117        
1118        if (StringUtils.isNotEmpty(normId))
1119        {
1120            if (!cache.normsContainsNormIdKey(normId))
1121            {
1122                getLogger().warn("[{}] L'{} '{}' possède une norme invalide.", prefix, logName, content.getTitle());
1123            }
1124            else if (!cache.normsContainsNatureIdKey(normId, nature))
1125            {
1126                if (getLogger().isWarnEnabled())
1127                {
1128                    Content norm = normValue.getContent();
1129                    getLogger().warn("[{}] La norme '{}' n'est pas définie pour la nature d'enseignement {}.", prefix, norm.getTitle(), _refTableHelper.getItemCode(nature));
1130                }
1131            }
1132            else
1133            {
1134                return normId;
1135            }
1136        }
1137        else
1138        {
1139            getLogger().info("[{}] L'{} '{}' n'a pas de norme associée.", prefix, logName, content.getTitle());
1140        }
1141        return null;
1142    }
1143    
1144    /**
1145     * Retrieve the orgUnit
1146     * @param courseHolder the course
1147     * @return the orgUnit
1148     */
1149    protected String _getOrgUnits(Course courseHolder)
1150    {
1151        String orgUnits = StringUtils.EMPTY;
1152        for (String orgUnitId : courseHolder.getOrgUnits())
1153        {
1154            if (StringUtils.isNotEmpty(orgUnitId))
1155            {
1156                if (!orgUnits.isEmpty())
1157                {
1158                    orgUnits += ", ";
1159                }
1160                try
1161                {
1162                    OrgUnit orgUnit = _resolver.resolveById(orgUnitId);
1163                    orgUnits += orgUnit.getTitle();
1164                }
1165                catch (UnknownAmetysObjectException e)
1166                {
1167                    // Ignore orgUnit
1168                    getLogger().error("Impossible de retrouver la composante : {}", orgUnitId, e);
1169                }
1170            }
1171        }
1172        
1173        return orgUnits;
1174    }
1175}