This is an automated email from the git hooks/post-receive script. New commit to branch develop in repository tutti. See https://gitlab.nuiton.org/codelutin/tutti.git commit a04f0950c4db7eb422367cb3845798678212dadf Author: Benjamin POUSSIN <poussin@codelutin.com> Date: Wed Jan 4 20:48:20 2017 +0100 fixes #8661: [ALGORITHME DE PRELEVEMENTS] mauvais fonctionnement des compteurs quand on décoche l'option "maturité" et/ou "sexe" - modification pour prendre en compte le fait qu'il n'y ait pas de zone - modification pour permettre la somme sur les maturites --- .../service/sampling/CruiseSamplingCache.java | 102 ++++++++++-------- .../IndividualObservationSamplingCacheRequest.java | 18 +++- .../IndividualObservationSamplingContext.java | 3 +- .../IndividualObservationSamplingStatus.java | 1 + .../frequency/IndividualObservationUICache.java | 116 +++++++++++++-------- .../frequency/SamplingNotificationZoneHandler.java | 15 +-- .../resources/i18n/tutti-ui-swing_en_GB.properties | 1 + .../resources/i18n/tutti-ui-swing_fr_FR.properties | 1 + 8 files changed, 162 insertions(+), 95 deletions(-) diff --git a/tutti-service/src/main/java/fr/ifremer/tutti/service/sampling/CruiseSamplingCache.java b/tutti-service/src/main/java/fr/ifremer/tutti/service/sampling/CruiseSamplingCache.java index 3cefb8c..333d73d 100644 --- a/tutti-service/src/main/java/fr/ifremer/tutti/service/sampling/CruiseSamplingCache.java +++ b/tutti-service/src/main/java/fr/ifremer/tutti/service/sampling/CruiseSamplingCache.java @@ -117,6 +117,15 @@ public class CruiseSamplingCache implements CruiseCacheAble { maturityCaracteristicsById.get(speciesProtocol.getMaturityPmfmId())); }); + // poussin 20170104 benthos doit se comporter comme species, il faut sans doute aussi le protocole + // du benthos, pour pouvoir afficher les stats pour le benthos + protocol.getBenthos().forEach(speciesProtocol -> { + cpsDefinitionsBySpecies.putAll(speciesProtocol.getSpeciesReferenceTaxonId(), + speciesProtocol.getCalcifiedPiecesSamplingDefinition()); + maturityCaracteristicBySpecies.put(speciesProtocol.getSpeciesReferenceTaxonId(), + maturityCaracteristicsById.get(speciesProtocol.getMaturityPmfmId())); + }); + protocol.getMaturityCaracteristics().forEach(mc -> matureStatesByMaturityCracteristic.putAll(mc.getId(), mc.getMatureStateIds())); } @@ -142,14 +151,15 @@ public class CruiseSamplingCache implements CruiseCacheAble { Objects.requireNonNull(individualObservations); Optional<Zone> optionalZone = tryFindZone(fishingOperation); - if (!optionalZone.isPresent()) { + // poussin 20170104 #8661: on doit pouvoir faire des stats meme sans zone +// if (!optionalZone.isPresent()) { +// +// // pas de zone définie pour cette opération de pêche, on n'enregistre rien ici. +// return; +// +// } - // pas de zone définie pour cette opération de pêche, on n'enregistre rien ici. - return; - - } - - Zone zone = optionalZone.get(); + Zone zone = optionalZone.orElse(null); setLoading(true); @@ -208,14 +218,15 @@ public class CruiseSamplingCache implements CruiseCacheAble { Objects.requireNonNull(individualObservations); Optional<Zone> optionalZone = tryFindZone(fishingOperation); - if (!optionalZone.isPresent()) { - - // pas de zone définie pour cette opération de pêche, rien à supprimer alors. - return; - - } + // poussin 20170104 #8661: on doit pouvoir faire des stats meme sans zone +// if (!optionalZone.isPresent()) { +// +// // pas de zone définie pour cette opération de pêche, rien à supprimer alors. +// return; +// +// } - Zone zone = optionalZone.get(); + Zone zone = optionalZone.orElse(null); setLoading(true); @@ -275,14 +286,15 @@ public class CruiseSamplingCache implements CruiseCacheAble { Objects.requireNonNull(individualObservations); Optional<Zone> optionalZone = tryFindZone(fishingOperation); - if (!optionalZone.isPresent()) { + // poussin 20170104 #8661: on doit pouvoir faire des stats meme sans zone +// if (!optionalZone.isPresent()) { +// +// // pas de zone définie pour cette opération de pêche, rien à supprimer alors. +// return; +// +// } - // pas de zone définie pour cette opération de pêche, rien à supprimer alors. - return; - - } - - Zone zone = optionalZone.get(); + Zone zone = optionalZone.orElse(null); String fishingOperationId = fishingOperation.getId(); @@ -296,8 +308,6 @@ public class CruiseSamplingCache implements CruiseCacheAble { String keyPrefix = CruiseSamplingInternalCache.addPrefixKey(fishingOperationId, ""); int keyPrefixLength = keyPrefix.length(); - String zoneId = zone.getId(); - fishingOperationCache.getKeys() .stream() .filter(key -> key.startsWith(keyPrefix)) @@ -315,7 +325,7 @@ public class CruiseSamplingCache implements CruiseCacheAble { String cruiseSamplingKey = fishingOperationSamplingKey.substring(keyPrefixLength); cruiseCache.remove(cruiseSamplingKey, individualObservationCount, samplingCount); - String zoneSamplingKey = addPrefixKey(zoneId, cruiseSamplingKey); + String zoneSamplingKey = createZoneSamplingKey(cruiseSamplingKey, zone); zoneCache.remove(zoneSamplingKey, individualObservationCount, samplingCount); fishingOperationCache.remove(fishingOperationSamplingKey, individualObservationCount, samplingCount); @@ -378,10 +388,11 @@ public class CruiseSamplingCache implements CruiseCacheAble { Objects.requireNonNull(fishingOperation); Optional<Zone> optionalZone = tryFindZone(fishingOperation); - if (!optionalZone.isPresent()) { - - throw new ZoneNotDefinedOnFishingOperationException(request); - } + // poussin 20170104 #8661: on doit pouvoir faire des stats meme sans zone +// if (!optionalZone.isPresent()) { +// +// throw new ZoneNotDefinedOnFishingOperationException(request); +// } CaracteristicQualitativeValue gender = request.getGender(); Boolean maturity = getMaturity(request); @@ -396,7 +407,7 @@ public class CruiseSamplingCache implements CruiseCacheAble { IndividualObservationSamplingContext context = createContext(fishingOperation.getIdAsInt(), species, - optionalZone.get(), + optionalZone.orElse(null), optionalCalcifiedPiecesSamplingDefinition.get(), lengthStep, maturity, @@ -517,8 +528,8 @@ public class CruiseSamplingCache implements CruiseCacheAble { } private Boolean getMaturity(IndividualObservationSamplingCacheRequest samplingCacheRequest) { - Boolean maturity = null; - if (samplingCacheRequest.withMaturity()) { + Boolean maturity = samplingCacheRequest.getForcedMaturity(); + if (maturity == null && samplingCacheRequest.withMaturity()) { Caracteristic maturityCaracteristic = maturityCaracteristicBySpecies.get(samplingCacheRequest.getSpecies().getReferenceTaxonId()); if (maturityCaracteristic != null) { CaracteristicQualitativeValue maturityQualitativeValue = samplingCacheRequest.getMaturity(); @@ -557,7 +568,7 @@ public class CruiseSamplingCache implements CruiseCacheAble { } private String createZoneSamplingKey(String cruiseSamplingKey, Zone zone) { - return addPrefixKey(zone.getId(), cruiseSamplingKey); + return addPrefixKey((zone == null ? "" : zone.getId()), cruiseSamplingKey); } private String createFishingOperationSamplingKey(String cruiseSamplingKey, int fishingOperationId) { @@ -684,13 +695,14 @@ public class CruiseSamplingCache implements CruiseCacheAble { Objects.requireNonNull(fishingOperation); Optional<Zone> optionalZone = tryFindZone(fishingOperation); - if (!optionalZone.isPresent()) { - - if (log.isInfoEnabled()) { - log.info("Do not record sampling in cache, fishing operation has no matching zone."); - } - return null; - } + // poussin 20170104 #8661: on doit pouvoir faire des stats meme sans zone +// if (!optionalZone.isPresent()) { +// +// if (log.isInfoEnabled()) { +// log.info("Do not record sampling in cache, fishing operation has no matching zone."); +// } +// return null; +// } Integer lengthStep = request.getLengthClass(); Objects.requireNonNull(lengthStep); @@ -714,7 +726,7 @@ public class CruiseSamplingCache implements CruiseCacheAble { return createContext(fishingOperation.getIdAsInt(), species, - optionalZone.get(), + optionalZone.orElse(null), optionalCalcifiedPiecesSamplingDefinition.get(), lengthStep, maturity, @@ -794,9 +806,17 @@ public class CruiseSamplingCache implements CruiseCacheAble { } else { + // poussin 20171204 si maturity dans la definition est null + // cela veut dire qu'il faut faire les sommes pour les statistiques + // sur toutes les maturites. Elle ne peut etre null que si dans le + // protocole on a pas demander a classifier par maturite, et donc + // dans les definitions toutes les maturites doivent etre null + // il ne peut pas existe des maturite null mixee avec true ou false + // conclusion si dans la definition maturite est null, elle convient + // pour n'importe quelle maturite Optional<CalcifiedPiecesSamplingDefinition> optionalDefinition = cpsDefinitions.stream() - .filter(cpsDef -> Objects.equals(cpsDef.getMaturity(), maturity) + .filter(cpsDef -> (cpsDef.getMaturity() == null || Objects.equals(cpsDef.getMaturity(), maturity)) && lengthStep >= cpsDef.getMinSize() && (cpsDef.getMaxSize() == null || lengthStep <= cpsDef.getMaxSize())) .findFirst(); diff --git a/tutti-service/src/main/java/fr/ifremer/tutti/service/sampling/IndividualObservationSamplingCacheRequest.java b/tutti-service/src/main/java/fr/ifremer/tutti/service/sampling/IndividualObservationSamplingCacheRequest.java index dc83ba5..9b10940 100644 --- a/tutti-service/src/main/java/fr/ifremer/tutti/service/sampling/IndividualObservationSamplingCacheRequest.java +++ b/tutti-service/src/main/java/fr/ifremer/tutti/service/sampling/IndividualObservationSamplingCacheRequest.java @@ -44,7 +44,8 @@ public class IndividualObservationSamplingCacheRequest { private final FishingOperation fishingOperation; private final Species species; private final Integer lengthClass; - private final CaracteristicQualitativeValue maturity; + private Boolean forcedMaturity; + private CaracteristicQualitativeValue maturity; private CaracteristicQualitativeValue gender; private final String samplingCode; @@ -61,6 +62,7 @@ public class IndividualObservationSamplingCacheRequest { this.maturity = maturity; this.gender = gender; this.samplingCode = samplingCode; + this.forcedMaturity = null; } public FishingOperation getFishingOperation() { @@ -80,6 +82,20 @@ public class IndividualObservationSamplingCacheRequest { this.gender = gender; } + public Boolean getForcedMaturity() { + return forcedMaturity; + } + + /** + * Pouvoir modifier la maturite est utile pour pouvoir sommer toutes les observations + * quelque soit la maturite + * @param forcedMaturity true pour les matures, false pour les immature + */ + public void setForcedMaturity(Boolean forcedMaturity) { + this.forcedMaturity = forcedMaturity; + this.maturity = null; + } + public Integer getLengthClass() { return lengthClass; } diff --git a/tutti-service/src/main/java/fr/ifremer/tutti/service/sampling/IndividualObservationSamplingContext.java b/tutti-service/src/main/java/fr/ifremer/tutti/service/sampling/IndividualObservationSamplingContext.java index efa53ce..69316b1 100644 --- a/tutti-service/src/main/java/fr/ifremer/tutti/service/sampling/IndividualObservationSamplingContext.java +++ b/tutti-service/src/main/java/fr/ifremer/tutti/service/sampling/IndividualObservationSamplingContext.java @@ -99,7 +99,8 @@ public class IndividualObservationSamplingContext { String fishingOperationSamplingKey) { Objects.requireNonNull(species); Objects.requireNonNull(calcifiedPiecesSamplingDefinition); - Objects.requireNonNull(zone); + // poussin 20170104 #8661: on doit pouvoir faire des stats meme sans zone +// Objects.requireNonNull(zone); Objects.requireNonNull(cruiseSamplingKey); Objects.requireNonNull(zoneSamplingKey); Objects.requireNonNull(fishingOperationSamplingKey); diff --git a/tutti-service/src/main/java/fr/ifremer/tutti/service/sampling/IndividualObservationSamplingStatus.java b/tutti-service/src/main/java/fr/ifremer/tutti/service/sampling/IndividualObservationSamplingStatus.java index e285b57..6c9f608 100644 --- a/tutti-service/src/main/java/fr/ifremer/tutti/service/sampling/IndividualObservationSamplingStatus.java +++ b/tutti-service/src/main/java/fr/ifremer/tutti/service/sampling/IndividualObservationSamplingStatus.java @@ -117,6 +117,7 @@ public class IndividualObservationSamplingStatus { /** * Ajoute aux donnees de l'objets courante les donnes de l'objet en parametre * cela sert a faire des sommes sur les observations en fusionnant les sexes + * et/ou maturite * @param i */ public void add(IndividualObservationSamplingStatus i) { diff --git a/tutti-ui-swing/src/main/java/fr/ifremer/tutti/ui/swing/content/operation/catches/species/frequency/IndividualObservationUICache.java b/tutti-ui-swing/src/main/java/fr/ifremer/tutti/ui/swing/content/operation/catches/species/frequency/IndividualObservationUICache.java index a6f5de1..f51ec02 100644 --- a/tutti-ui-swing/src/main/java/fr/ifremer/tutti/ui/swing/content/operation/catches/species/frequency/IndividualObservationUICache.java +++ b/tutti-ui-swing/src/main/java/fr/ifremer/tutti/ui/swing/content/operation/catches/species/frequency/IndividualObservationUICache.java @@ -25,8 +25,6 @@ package fr.ifremer.tutti.ui.swing.content.operation.catches.species.frequency; */ import fr.ifremer.tutti.persistence.entities.data.FishingOperation; -import fr.ifremer.tutti.persistence.entities.protocol.CalcifiedPiecesSamplingDefinition; -import fr.ifremer.tutti.persistence.entities.protocol.SpeciesProtocol; import fr.ifremer.tutti.persistence.entities.protocol.Zone; import fr.ifremer.tutti.persistence.entities.referential.CaracteristicQualitativeValue; import fr.ifremer.tutti.persistence.entities.referential.Species; @@ -45,7 +43,6 @@ import java.util.Collection; import java.util.List; import java.util.Objects; import java.util.Optional; -import org.apache.commons.collections4.CollectionUtils; /** * Cache des observations individuelles. @@ -139,54 +136,81 @@ public class IndividualObservationUICache implements Closeable { IndividualObservationSamplingStatus result = cruiseSamplingCache.getIndividualObservationSamplingStatus(samplingCacheRequest); // fixes bug #8661: solution 2) - // il faut bouclé sur tous les sexes possibles pour pouvoir faire - // la somme et retourne un nouvel object status contenant cette - // somme sur les sexes. - SpeciesProtocol speciesProtocol = uiModel.isProtocolFilled() ? uiModel.getSpeciesOrBenthosBatchUISupport().getSpeciesProtocol(row.getSpecies()) : null; - - // s'il y a un protocole, on regarde s'il faut sommer sur le genre ou non - // (la somme sur le genre peut dependre de la maturite - if (speciesProtocol != null) { - // par defaut on merge, sauf si le boolean sex est a vrai - boolean mergeGender = true; - // on recupere la definition du protocole - Collection<CalcifiedPiecesSamplingDefinition> defs = speciesProtocol.getCalcifiedPiecesSamplingDefinition(); - if (CollectionUtils.isNotEmpty(defs)) { - if (defs.size() == 1) { - // un seul element, c'est qu'il n'y a pas de maturity - mergeGender = !speciesProtocol.getCalcifiedPiecesSamplingDefinition(0).isSex(); - } else { - // plusieurs definition, il faut retrouver celui qui correspond a la maturité de la ligne - boolean maturiry = result.getIndividualObservationSamplingContext().getMaturity(); - for (CalcifiedPiecesSamplingDefinition def : defs) { - if (Objects.equals(def.getMaturity(), maturiry)) { - mergeGender = !def.isSex(); - break; // on a trouve l'element souhaite, on sort - } - } - } - } - - // il faut merger, on recalcule status en mergeant les sexes - if (mergeGender) { - // ne pas oublier le null comme valeur possible, qui n'est pas dans les getQualitativeValue - // elle devient la valeur par defaut pour qu'il n'y ait pas de sexe dans le resultat - samplingCacheRequest.setGender(null); - result = cruiseSamplingCache.getIndividualObservationSamplingStatus( - samplingCacheRequest); - - for (CaracteristicQualitativeValue sex : uiModel.getIndividualObservationModel().getSexCaracteristic().getQualitativeValue()) { - samplingCacheRequest.setGender(sex); - result.add( - cruiseSamplingCache.getIndividualObservationSamplingStatus(samplingCacheRequest) - ); - } - } + // on regarde s'il faut sommer sur les maturity et/ou les genres + Boolean protocolMaturity = result.getIndividualObservationSamplingContext().getCalcifiedPiecesSamplingDefinition().getMaturity(); + boolean withSex = result.getIndividualObservationSamplingContext().getCalcifiedPiecesSamplingDefinition().isSex(); + + // on merge si le boolean maturity est a null (pas de maturite) + boolean mergeMaturity = protocolMaturity == null; + // on merge si le boolean sex est a faux (pas de sex) + boolean mergeGender = !withSex; + + if (mergeMaturity && mergeGender) { // il faut sommer sur les deux + samplingCacheRequest.setForcedMaturity(null); // sans maturite + IndividualObservationSamplingStatus iossNullMature = + getIndividualObservationSamplingStatusSumOverGenre(samplingCacheRequest); + + samplingCacheRequest.setForcedMaturity(true); // mature + IndividualObservationSamplingStatus iossMature = + getIndividualObservationSamplingStatusSumOverGenre(samplingCacheRequest); + + samplingCacheRequest.setForcedMaturity(false); // immature + IndividualObservationSamplingStatus iossNoMature = + getIndividualObservationSamplingStatusSumOverGenre(samplingCacheRequest); + + result = iossNullMature; + result.add(iossMature); + result.add(iossNoMature); + } else if (mergeMaturity) { // il faut sommer que sur la maturity + samplingCacheRequest.setForcedMaturity(null); + IndividualObservationSamplingStatus iossNullMature = + cruiseSamplingCache.getIndividualObservationSamplingStatus(samplingCacheRequest); + samplingCacheRequest.setForcedMaturity(true); + IndividualObservationSamplingStatus iossMature = + cruiseSamplingCache.getIndividualObservationSamplingStatus(samplingCacheRequest); + samplingCacheRequest.setForcedMaturity(false); + IndividualObservationSamplingStatus iossNoMature = + cruiseSamplingCache.getIndividualObservationSamplingStatus(samplingCacheRequest); + result = iossNullMature; + result.add(iossMature); + result.add(iossNoMature); + } else if (mergeGender) { // il faut sommer que sur le genre + result = getIndividualObservationSamplingStatusSumOverGenre(samplingCacheRequest); } return result; } + /** + * il faut bouclé sur tous les sexes possibles pour pouvoir faire + * la somme et retourne un nouvel object status contenant cette + * somme sur les sexes. + * + * @param samplingCacheRequest + * @return + * @throws SizeNotDefinedOnIndividualObservationException + * @throws ZoneNotDefinedOnFishingOperationException + * @throws CalcifiedPiecesSamplingAlgorithmEntryNotFoundException + */ + private IndividualObservationSamplingStatus getIndividualObservationSamplingStatusSumOverGenre( + IndividualObservationSamplingCacheRequest samplingCacheRequest) + throws SizeNotDefinedOnIndividualObservationException, ZoneNotDefinedOnFishingOperationException, CalcifiedPiecesSamplingAlgorithmEntryNotFoundException { + // ne pas oublier le null comme valeur possible, qui n'est pas dans les getQualitativeValue + // elle devient la valeur par defaut pour qu'il n'y ait pas de sexe dans le resultat + samplingCacheRequest.setGender(null); + IndividualObservationSamplingStatus result = + cruiseSamplingCache.getIndividualObservationSamplingStatus( + samplingCacheRequest); + + for (CaracteristicQualitativeValue sex : uiModel.getIndividualObservationModel().getSexCaracteristic().getQualitativeValue()) { + samplingCacheRequest.setGender(sex); + result.add( + cruiseSamplingCache.getIndividualObservationSamplingStatus(samplingCacheRequest) + ); + } + return result; + } + public void addIndividualObservations(Collection<IndividualObservationBatchRowModel> individualObservationRows) { Objects.requireNonNull(individualObservationRows); diff --git a/tutti-ui-swing/src/main/java/fr/ifremer/tutti/ui/swing/content/operation/catches/species/frequency/SamplingNotificationZoneHandler.java b/tutti-ui-swing/src/main/java/fr/ifremer/tutti/ui/swing/content/operation/catches/species/frequency/SamplingNotificationZoneHandler.java index 6930c71..a49b220 100644 --- a/tutti-ui-swing/src/main/java/fr/ifremer/tutti/ui/swing/content/operation/catches/species/frequency/SamplingNotificationZoneHandler.java +++ b/tutti-ui-swing/src/main/java/fr/ifremer/tutti/ui/swing/content/operation/catches/species/frequency/SamplingNotificationZoneHandler.java @@ -321,12 +321,12 @@ public class SamplingNotificationZoneHandler implements Closeable { return; } - if (!individualObservationUICache.isFishingOperationWithZone()) { - - // pas de zone définie sur l'opération de pêche - stopUsingStatusNotication(t("tutti.editSpeciesFrequencies.samplingNotification.warning.fishingOperationNotInAZone")); - return; - } +// if (!individualObservationUICache.isFishingOperationWithZone()) { +// +// // pas de zone définie sur l'opération de pêche +// stopUsingStatusNotication(t("tutti.editSpeciesFrequencies.samplingNotification.warning.fishingOperationNotInAZone")); +// return; +// } if (!individualObservationUICache.isSpeciesDefinedInCalcifiedPiecesSampling()) { @@ -407,6 +407,9 @@ public class SamplingNotificationZoneHandler implements Closeable { } String zone = zoneDecorator.toString(individualObservationSamplingContext.getZone()); + if (zone == null) { + zone = t("tutti.editSpeciesFrequencies.samplingNeeded.noZone"); + } return t("tutti.editSpeciesFrequencies.samplingNeeded.summary", key, nbForOperationLabel, zone, nbForZoneLabel, nbForCruiseLabel); diff --git a/tutti-ui-swing/src/main/resources/i18n/tutti-ui-swing_en_GB.properties b/tutti-ui-swing/src/main/resources/i18n/tutti-ui-swing_en_GB.properties index 6b3bded..20eab45 100644 --- a/tutti-ui-swing/src/main/resources/i18n/tutti-ui-swing_en_GB.properties +++ b/tutti-ui-swing/src/main/resources/i18n/tutti-ui-swing_en_GB.properties @@ -1661,6 +1661,7 @@ tutti.editSpeciesFrequencies.logTable.removeRow.confirm.message= tutti.editSpeciesFrequencies.logTable.removeRow.confirm.title= tutti.editSpeciesFrequencies.samplingNeeded.immature= tutti.editSpeciesFrequencies.samplingNeeded.mature= +tutti.editSpeciesFrequencies.samplingNeeded.noZone= tutti.editSpeciesFrequencies.samplingNeeded.summary= tutti.editSpeciesFrequencies.samplingNotification.warning.fishingOperationNotInAZone= tutti.editSpeciesFrequencies.samplingNotification.warning.moreThanOneRowSelected= diff --git a/tutti-ui-swing/src/main/resources/i18n/tutti-ui-swing_fr_FR.properties b/tutti-ui-swing/src/main/resources/i18n/tutti-ui-swing_fr_FR.properties index 78d15de..d6f4274 100644 --- a/tutti-ui-swing/src/main/resources/i18n/tutti-ui-swing_fr_FR.properties +++ b/tutti-ui-swing/src/main/resources/i18n/tutti-ui-swing_fr_FR.properties @@ -1555,6 +1555,7 @@ tutti.editSpeciesFrequencies.logTable.removeRow.confirm.title=Suppression d'une tutti.editSpeciesFrequencies.samplingNeeded.immature=Immature tutti.editSpeciesFrequencies.samplingNeeded.mature=Mature tutti.editSpeciesFrequencies.samplingNeeded.max=max +tutti.editSpeciesFrequencies.samplingNeeded.noZone=toute les traits de la campagne tutti.editSpeciesFrequencies.samplingNeeded.summary=<html><body><strong>%s</strong> \: Trait \: %s | %s \: %s | Campagne \: %s tutti.editSpeciesFrequencies.samplingNotification.warning.fishingOperationNotInAZone=Le trait n'est pas dans une zone. tutti.editSpeciesFrequencies.samplingNotification.warning.moreThanOneRowSelected=Plusieurs observations individuelles sélectionnées. -- To stop receiving notification emails like this one, please contact codelutin.com SCM administrator <admin+scm@codelutin.com>.