Un nouveau bouton AS marque un jour d'astreinte. Ce jour et son lendemain,
une saisie « Fin activité » enregistre l'heure de fin de la dernière
intervention hors horaires (elle peut être le matin si la nuit a dépassé
minuit). L'app en déduit l'heure de retour au bureau (fin + 11h de repos
consécutif) et retire du décompte de la semaine toute la présence avant
cette heure : heures dues réduites, présence pointée avant le retour non
comptée, préconisations Fin cible démarrées à l'heure de retour sans
avertissement de retard, demi-journée entièrement en repos considérée
comme remplie. Les exports .ods gagnent les colonnes Astreinte et Retour,
et les stats n'ont plus les retards liés au repos.
💘 Generated with Crush
Assisted-by: Crush:glm-5.2
407 lines
20 KiB
Python
407 lines
20 KiB
Python
"""Business logic: compute worked hours and expected hours per day/week."""
|
|
from datetime import datetime, timedelta
|
|
|
|
DEFAULT_PLAGES = {
|
|
"matin_debut": "09:00", # présence obligatoire à partir de cette heure le matin
|
|
"matin_fin": "12:00", # présence obligatoire jusqu'à cette heure le matin
|
|
"aprem_debut": "14:00", # présence obligatoire à partir de cette heure l'après-midi
|
|
"aprem_fin": "17:00", # présence obligatoire jusqu'à cette heure l'après-midi (plancher de sortie)
|
|
"pause_dejeuner_fin": "13:30", # reprise par défaut projetée tant que l'entrée aprem n'est pas saisie
|
|
"arrivee_visee": "", # arrivée du matin visée pour les préconisations (vide = matin_debut)
|
|
"depart_vise": "", # départ du soir visé, plafond souple des cibles aprem (vide = aucun)
|
|
}
|
|
|
|
REPOS_QUOTIDIEN_MIN = 11 * 60 # repos consécutif minimum par période de 24h : décale l'heure de retour au bureau
|
|
|
|
|
|
def hhmm_to_minutes(val: str | None) -> int:
|
|
if not val:
|
|
return 0
|
|
h, m = val.split(":")
|
|
return int(h) * 60 + int(m)
|
|
|
|
|
|
def minutes_to_hhmm(minutes: int) -> str:
|
|
sign = "-" if minutes < 0 else ""
|
|
minutes = abs(minutes)
|
|
return f"{sign}{minutes // 60}h{minutes % 60:02d}"
|
|
|
|
|
|
def minutes_to_time(minutes: int) -> str:
|
|
return f"{minutes // 60:02d}:{minutes % 60:02d}"
|
|
|
|
|
|
def _veille(date_str: str) -> str:
|
|
d = datetime.strptime(date_str, "%Y-%m-%d").date()
|
|
return (d - timedelta(days=1)).strftime("%Y-%m-%d")
|
|
|
|
|
|
def jour_astreinte_qualifie(date_str: str, astreintes) -> bool:
|
|
"""True si le jour permet la saisie d'opérations d'astreinte : jour d'astreinte,
|
|
ou lendemain d'un jour d'astreinte (la nuit qui suit l'astreinte)."""
|
|
astreintes = set(astreintes or [])
|
|
return date_str in astreintes or _veille(date_str) in astreintes
|
|
|
|
|
|
def exclusion_retour(conges_date: set, retour_min: int | None, plages: dict) -> tuple[int, int]:
|
|
"""Présence obligatoire (matin, aprem) tombant avant l'heure de retour au bureau,
|
|
en minutes : c'est le temps de présence retiré du décompte de la semaine quand le
|
|
repos de 11h décale l'arrivée. Les demi-journées en congé ne sont pas décomptées
|
|
deux fois."""
|
|
if retour_min is None:
|
|
return 0, 0
|
|
matin_debut = hhmm_to_minutes(plages["matin_debut"])
|
|
matin_fin = hhmm_to_minutes(plages["matin_fin"])
|
|
aprem_debut = hhmm_to_minutes(plages["aprem_debut"])
|
|
aprem_fin = hhmm_to_minutes(plages["aprem_fin"])
|
|
matin_ex = 0 if ("matin" in conges_date or "jour" in conges_date) else max(0, min(matin_fin, retour_min) - matin_debut)
|
|
aprem_ex = 0 if ("aprem" in conges_date or "jour" in conges_date) else max(0, min(aprem_fin, retour_min) - aprem_debut)
|
|
return matin_ex, aprem_ex
|
|
|
|
|
|
def nominaux_du_jour(conges_date: set, du_full_min: int, matin_excl_min: int, aprem_excl_min: int) -> tuple[int, int]:
|
|
"""Nominaux (attendus) matin et aprem en minutes, après congés et exclusion liée
|
|
à l'heure de retour."""
|
|
if "jour" in conges_date:
|
|
m = a = 0
|
|
elif "matin" in conges_date:
|
|
m, a = 0, du_full_min
|
|
elif "aprem" in conges_date:
|
|
m, a = du_full_min, 0
|
|
else:
|
|
m, a = du_full_min // 2, du_full_min - du_full_min // 2
|
|
return max(0, m - matin_excl_min), max(0, a - aprem_excl_min)
|
|
|
|
|
|
def retour_apres_repos(date_str: str, p_by_date: dict, plages: dict, astreintes) -> int | None:
|
|
"""Heure de retour au bureau après les 11h de repos consécutif qui suivent la
|
|
dernière activité d'astreinte.
|
|
|
|
Seule l'heure de FIN de cette activité est saisie, sur le jour d'astreinte ou
|
|
son lendemain. La fin peut tomber le matin (l'intervention a dépassé minuit) :
|
|
saisie sur le jour d'astreinte, une fin matinale désigne la fin de SA nuit ;
|
|
saisie sur le lendemain, une fin matinale désigne la fin de la nuit de la
|
|
veille. Le retour s'applique au jour où se termine le repos : fin 23:30 →
|
|
retour 10:30 le lendemain ; fin 06:00 → retour 17:00. Renvoie des minutes
|
|
depuis minuit, ou None si rien ne contraint le retour au-delà de l'ouverture."""
|
|
astreintes = set(astreintes or [])
|
|
matin_debut = hhmm_to_minutes(plages["matin_debut"])
|
|
veille = _veille(date_str)
|
|
cands = []
|
|
|
|
def _fin_activite(date_op: str) -> int | None:
|
|
row = p_by_date.get(date_op) or {}
|
|
fin = row.get("astreinte_fin")
|
|
return hhmm_to_minutes(fin) if fin else None
|
|
|
|
# Fin saisie sur la veille : fin de soirée → repos terminé aujourd'hui ; fin
|
|
# matinale sur un jour d'astreinte → la nuit a dépassé minuit, le repos se
|
|
# termine aussi aujourd'hui.
|
|
if jour_astreinte_qualifie(veille, astreintes):
|
|
fin = _fin_activite(veille)
|
|
if fin is not None:
|
|
if fin + REPOS_QUOTIDIEN_MIN >= 24 * 60:
|
|
cands.append(fin + REPOS_QUOTIDIEN_MIN - 24 * 60)
|
|
elif veille in astreintes and fin < matin_debut:
|
|
cands.append(fin + REPOS_QUOTIDIEN_MIN)
|
|
# Fin matinale saisie sur le jour même (lendemain d'astreinte) : la nuit de
|
|
# la veille s'est terminée après minuit.
|
|
if veille in astreintes:
|
|
fin = _fin_activite(date_str)
|
|
if fin is not None and fin < matin_debut:
|
|
cands.append(fin + REPOS_QUOTIDIEN_MIN)
|
|
if not cands:
|
|
return None
|
|
best = max(cands)
|
|
# Un repos qui se termine avant l'ouverture ne contraint rien
|
|
return best if best > matin_debut else None
|
|
|
|
|
|
def heures_travaillees(p: dict, retour_min: int | None = None) -> int:
|
|
"""Return worked minutes for a pointage row.
|
|
|
|
If a half-day has started but has no exit time yet, count elapsed time up to now
|
|
(today only — past dates with missing exits are treated as 0). Quand une heure
|
|
de retour est posée (repos de 11h après astreinte), la présence avant cette
|
|
heure ne compte pas dans les heures de la semaine : les entrées sont décalées
|
|
au moins à l'heure de retour.
|
|
"""
|
|
today = datetime.today().strftime("%Y-%m-%d")
|
|
now_min = datetime.today().hour * 60 + datetime.today().minute
|
|
|
|
def entree_min(v):
|
|
m = hhmm_to_minutes(v)
|
|
return max(m, retour_min) if retour_min is not None else m
|
|
|
|
matin = 0
|
|
if p["matin_entree"] and p["matin_sortie"]:
|
|
matin = max(0, hhmm_to_minutes(p["matin_sortie"]) - entree_min(p["matin_entree"]))
|
|
elif p["matin_entree"] and p.get("date") == today:
|
|
matin = max(0, now_min - entree_min(p["matin_entree"]))
|
|
|
|
aprem = 0
|
|
if p["aprem_entree"] and p["aprem_sortie"]:
|
|
aprem = max(0, hhmm_to_minutes(p["aprem_sortie"]) - entree_min(p["aprem_entree"]))
|
|
elif p["aprem_entree"] and p.get("date") == today:
|
|
aprem = max(0, now_min - entree_min(p["aprem_entree"]))
|
|
|
|
return matin + aprem
|
|
|
|
|
|
def heures_dues(date_str: str, conges: list[dict], heures_jour_min: int,
|
|
plages: dict | None = None, retour_min: int | None = None) -> int:
|
|
"""Return expected minutes for a date, accounting for vacations. Si une heure de
|
|
retour est définie (repos de 11h après astreinte), la présence obligatoire avant
|
|
cette heure est retirée du décompte de la semaine."""
|
|
d = datetime.strptime(date_str, "%Y-%m-%d").date()
|
|
# Weekend = no hours due
|
|
if d.weekday() >= 5:
|
|
return 0
|
|
|
|
conges_date = {c["type"] for c in conges if c["date"] == date_str}
|
|
|
|
if "jour" in conges_date:
|
|
return 0
|
|
if "matin" in conges_date and "aprem" in conges_date:
|
|
return 0
|
|
|
|
half = heures_jour_min // 2
|
|
if "matin" in conges_date or "aprem" in conges_date:
|
|
du = half
|
|
else:
|
|
du = heures_jour_min
|
|
|
|
if retour_min is not None and plages:
|
|
matin_ex, aprem_ex = exclusion_retour(conges_date, retour_min, plages)
|
|
du -= matin_ex + aprem_ex
|
|
|
|
return max(0, du)
|
|
|
|
|
|
def compute_week(pointages: list[dict], conges: list[dict], heures_jour_min: int, plages: dict | None = None, astreintes: list[str] | set[str] | None = None) -> dict:
|
|
"""Compute totals for a list of pointages (one week)."""
|
|
plages = {**DEFAULT_PLAGES, **(plages or {})}
|
|
astreintes = set(astreintes or [])
|
|
matin_debut_min = hhmm_to_minutes(plages["matin_debut"])
|
|
matin_fin_min = hhmm_to_minutes(plages["matin_fin"])
|
|
aprem_fin_min = hhmm_to_minutes(plages["aprem_fin"])
|
|
pause_dejeuner_fin_min = hhmm_to_minutes(plages["pause_dejeuner_fin"])
|
|
arrivee_visee_min = hhmm_to_minutes(plages["arrivee_visee"]) if plages.get("arrivee_visee") else matin_debut_min
|
|
depart_vise_min = hhmm_to_minutes(plages["depart_vise"]) if plages.get("depart_vise") else None
|
|
|
|
total_travaille = 0
|
|
total_du = 0
|
|
jours = []
|
|
p_by_date = {p["date"]: p for p in pointages}
|
|
|
|
for p in pointages:
|
|
conges_date = {c["type"] for c in conges if c["date"] == p["date"]}
|
|
# Retour au bureau après 11h de repos, calculé depuis l'heure de fin de la
|
|
# dernière activité d'astreinte (saisie sur le jour ou la veille qualifiés)
|
|
retour_min = retour_apres_repos(p["date"], p_by_date, plages, astreintes)
|
|
matin_ex, aprem_ex = exclusion_retour(conges_date, retour_min, plages)
|
|
travaille = heures_travaillees(p, retour_min)
|
|
du = heures_dues(p["date"], conges, heures_jour_min, plages, retour_min)
|
|
du_full = du + matin_ex + aprem_ex
|
|
matin_nom, aprem_nom = nominaux_du_jour(conges_date, du_full, matin_ex, aprem_ex)
|
|
delta = travaille - du
|
|
total_travaille += travaille
|
|
total_du += du
|
|
jours.append({
|
|
**p,
|
|
"travaille_min": travaille,
|
|
"du_min": du,
|
|
"delta_min": delta,
|
|
"travaille": minutes_to_hhmm(travaille),
|
|
"du": minutes_to_hhmm(du),
|
|
"delta": minutes_to_hhmm(delta),
|
|
"conge": conges_date,
|
|
"du_full_min": du_full,
|
|
"matin_excl_min": matin_ex,
|
|
"aprem_excl_min": aprem_ex,
|
|
"matin_nominal_min": matin_nom,
|
|
"aprem_nominal_min": aprem_nom,
|
|
"astreinte": p["date"] in astreintes,
|
|
"astreinte_qualifiee": jour_astreinte_qualifie(p["date"], astreintes),
|
|
"retour_min": retour_min,
|
|
"retour": minutes_to_time(retour_min) if retour_min is not None else None,
|
|
})
|
|
|
|
# Mark each day as complete (all non-congé slots filled, or day is a congé/weekend).
|
|
# Une demi-journée entièrement couverte par le repos de 11h (retour après sa
|
|
# plage) n'a plus rien à remplir : elle compte comme faite.
|
|
for jour in jours:
|
|
cg = jour["conge"]
|
|
matin_done = bool(jour.get("matin_entree") and jour.get("matin_sortie")) or "matin" in cg or "jour" in cg or jour["matin_nominal_min"] == 0
|
|
aprem_done = bool(jour.get("aprem_entree") and jour.get("aprem_sortie")) or "aprem" in cg or "jour" in cg or jour["aprem_nominal_min"] == 0
|
|
jour["is_complete"] = (jour["du_min"] == 0) or (matin_done and aprem_done)
|
|
|
|
# Cumulative weekly delta — accumulated for all days, but visible only when chain is unbroken
|
|
cumul = 0
|
|
cumul_chain_ok = True
|
|
for jour in jours:
|
|
cumul += jour["delta_min"]
|
|
jour["delta_cumul_min"] = cumul
|
|
jour["delta_cumul"] = minutes_to_hhmm(abs(cumul))
|
|
# Chain breaks on the first incomplete workday — subsequent days don't show cumul
|
|
if not jour["is_complete"] and jour["du_min"] > 0:
|
|
cumul_chain_ok = False
|
|
jour["cumul_visible"] = jour["is_complete"] and cumul_chain_ok
|
|
|
|
# Préconisations : répartit l'écart restant (retard à rattraper, ou avance à
|
|
# écouler) à parts égales sur les heures de départ du soir des demi-journées
|
|
# restantes, pour viser un solde à 0 en fin de semaine. Le matin n'est jamais
|
|
# ajusté : la pause de midi (matin_fin → reprise, 12:00 → 13:30 par défaut) est
|
|
# intouchable, et le nominal matin qui n'y tient pas bascule sur l'après-midi du
|
|
# même jour. L'après-midi vise le départ du soir configuré (plafond souple :
|
|
# dépassé seulement si le rattrapage ne tient nulle part avant), avec minuit
|
|
# comme limite dure en cas de retard, ou aprem_fin comme plancher en cas d'avance.
|
|
for jour in jours:
|
|
jour["cible_matin"] = None
|
|
jour["cible_aprem"] = None
|
|
jour["cible_matin_entree"] = None
|
|
jour["cible_aprem_entree"] = None
|
|
|
|
restant = total_du - total_travaille
|
|
|
|
# Une demi-journée en cours (entrée pointée sans sortie, aujourd'hui) est comptée
|
|
# ci-dessous comme un créneau entier à faire depuis son heure d'entrée ; son temps
|
|
# déjà écoulé, inclus dans total_travaille, doit donc être réintégré au besoin,
|
|
# sinon la même période compte à la fois comme travaillée et comme à venir.
|
|
today = datetime.today().strftime("%Y-%m-%d")
|
|
now_min = datetime.today().hour * 60 + datetime.today().minute
|
|
restant_planif = restant
|
|
for jour in jours:
|
|
if jour["date"] != today:
|
|
continue
|
|
retour = jour.get("retour_min") or 0
|
|
if jour.get("matin_entree") and not jour.get("matin_sortie"):
|
|
restant_planif += max(0, now_min - max(hhmm_to_minutes(jour["matin_entree"]), retour))
|
|
if jour.get("aprem_entree") and not jour.get("aprem_sortie"):
|
|
restant_planif += max(0, now_min - max(hhmm_to_minutes(jour["aprem_entree"]), retour))
|
|
|
|
slots = [] # {jour, half, nominal, debut, ...} — cap*/floor_room bornent le rattrapage
|
|
for jour in jours:
|
|
# Un jour passé non renseigné n'est pas exclu : cette appli permet de saisir
|
|
# ses heures après coup, donc une demi-journée vide reste "à faire" quelle
|
|
# que soit sa date.
|
|
cg = jour["conge"]
|
|
du = jour["du_min"]
|
|
if du == 0:
|
|
continue
|
|
matin_nominal = jour["matin_nominal_min"]
|
|
aprem_nominal = jour["aprem_nominal_min"]
|
|
matin_done = bool(jour.get("matin_entree") and jour.get("matin_sortie")) or "matin" in cg or "jour" in cg
|
|
aprem_done = bool(jour.get("aprem_entree") and jour.get("aprem_sortie")) or "aprem" in cg or "jour" in cg
|
|
if not matin_done:
|
|
# L'arrivée visée ne peut pas précéder l'heure de retour (repos de 11h)
|
|
debut = hhmm_to_minutes(jour["matin_entree"]) if jour.get("matin_entree") else max(arrivee_visee_min, jour["retour_min"] or 0)
|
|
reprise = hhmm_to_minutes(jour["aprem_entree"]) if jour.get("aprem_entree") else pause_dejeuner_fin_min
|
|
cap = max(0, min(matin_fin_min, reprise) - debut)
|
|
# Le nominal matin qui ne tient pas avant la pause bascule sur l'après-midi
|
|
# du même jour (s'il est encore ouvert), sinon dans le rattrapage global.
|
|
surplus = max(0, matin_nominal - cap)
|
|
if surplus and not aprem_done:
|
|
aprem_nominal += surplus
|
|
# Le matin n'est jamais ajusté par le rattrapage hebdomadaire : seule sa
|
|
# capacité avant la pause de midi compte, pour le calcul du surplus ci-dessus.
|
|
if cap > 0 and matin_nominal > 0:
|
|
slots.append({"jour": jour, "half": "matin", "nominal": min(matin_nominal, cap), "debut": debut})
|
|
if not aprem_done:
|
|
debut = hhmm_to_minutes(jour["aprem_entree"]) if jour.get("aprem_entree") else max(pause_dejeuner_fin_min, jour["retour_min"] or 0)
|
|
cap = max(0, 23 * 60 + 59 - debut)
|
|
cap_pref = min(cap, max(0, depart_vise_min - debut)) if depart_vise_min else cap
|
|
# floor_room = marge disponible pour avancer le départ en cas d'avance,
|
|
# sans descendre sous le plancher aprem_fin (présence minimale obligatoire).
|
|
floor_room = max(0, (debut + aprem_nominal) - aprem_fin_min)
|
|
slots.append({"jour": jour, "half": "aprem", "nominal": aprem_nominal, "debut": debut,
|
|
"cap": cap, "cap_pref": cap_pref, "floor_room": floor_room})
|
|
|
|
slots_restants = len(slots)
|
|
ecart_total = (restant_planif - sum(s["nominal"] for s in slots)) if slots else 0
|
|
aprem_slots = [s for s in slots if s["half"] == "aprem"]
|
|
adjs = [0] * len(aprem_slots)
|
|
|
|
# Répartition équitable de l'écart (retard ou avance), plafonnée par la capacité de
|
|
# chaque créneau du soir, en deux passes en cas de retard : d'abord dans le cadre visé
|
|
# (avant le départ du soir), puis au-delà si ça ne tient pas. Le surplus d'un créneau
|
|
# saturé se reporte sur les autres. En cas d'avance, on avance les départs symétriquement,
|
|
# sans jamais descendre sous le plancher aprem_fin (une seule passe, pas de plafond souple).
|
|
if ecart_total > 0:
|
|
left = ecart_total
|
|
for cap_key in ("cap_pref", "cap"):
|
|
while left > 0:
|
|
open_idx = [i for i, s in enumerate(aprem_slots) if adjs[i] < s[cap_key] - s["nominal"]]
|
|
if not open_idx:
|
|
break # tous les créneaux saturés à ce plafond : passe suivante, ou rattrapage impossible
|
|
base, rem = divmod(left, len(open_idx))
|
|
for k, i in enumerate(open_idx):
|
|
give = min(aprem_slots[i][cap_key] - aprem_slots[i]["nominal"] - adjs[i], base + (1 if k < rem else 0))
|
|
adjs[i] += give
|
|
left -= give
|
|
elif ecart_total < 0:
|
|
left = -ecart_total
|
|
while left > 0:
|
|
open_idx = [i for i, s in enumerate(aprem_slots) if -adjs[i] < s["floor_room"]]
|
|
if not open_idx:
|
|
break # tous les créneaux déjà au plancher : l'avance restante n'est pas plaçable
|
|
base, rem = divmod(left, len(open_idx))
|
|
for k, i in enumerate(open_idx):
|
|
take = min(aprem_slots[i]["floor_room"] + adjs[i], base + (1 if k < rem else 0))
|
|
adjs[i] -= take
|
|
left -= take
|
|
|
|
def _en_cours(jour, half: str) -> bool:
|
|
"""True si cette demi-journée est déjà commencée aujourd'hui (entrée pointée,
|
|
pas encore de sortie) : la cible ne peut alors pas retomber avant l'heure
|
|
actuelle, sinon on recommanderait un départ déjà passé."""
|
|
return (
|
|
jour["date"] == today
|
|
and bool(jour.get(f"{half}_entree"))
|
|
and not jour.get(f"{half}_sortie")
|
|
)
|
|
|
|
# Le matin propose toujours son horaire nominal (jamais ajusté par le rattrapage
|
|
# hebdomadaire) ; l'après-midi propose son horaire nominal éventuellement décalé
|
|
# par l'écart (retard ou avance) réparti ci-dessus.
|
|
for s in slots:
|
|
if s["half"] != "matin":
|
|
continue
|
|
# Pas de plancher matin_fin ici : le nominal est déjà borné par cap (≤ matin_fin
|
|
# - debut), donc debut + nominal ne peut pas dépasser matin_fin de lui-même.
|
|
sortie_min = s["debut"] + s["nominal"]
|
|
if _en_cours(s["jour"], "matin"):
|
|
sortie_min = max(sortie_min, now_min)
|
|
entree = f"{s['debut'] // 60:02d}:{s['debut'] % 60:02d}"
|
|
s["jour"]["cible_matin_entree"] = entree
|
|
s["jour"]["cible_matin"] = f"{sortie_min // 60:02d}:{sortie_min % 60:02d}"
|
|
|
|
for i, s in enumerate(aprem_slots):
|
|
sortie_min = max(s["debut"] + s["nominal"] + adjs[i], aprem_fin_min)
|
|
if _en_cours(s["jour"], "aprem"):
|
|
sortie_min = max(sortie_min, now_min)
|
|
entree = f"{s['debut'] // 60:02d}:{s['debut'] % 60:02d}"
|
|
s["jour"]["cible_aprem_entree"] = entree
|
|
s["jour"]["cible_aprem"] = f"{sortie_min // 60:02d}:{sortie_min % 60:02d}"
|
|
|
|
extra_total = max(0, ecart_total)
|
|
avance_total = max(0, -ecart_total)
|
|
reste_min = max(0, restant)
|
|
|
|
return {
|
|
"jours": jours,
|
|
"total_travaille": minutes_to_hhmm(total_travaille),
|
|
"total_du": minutes_to_hhmm(total_du),
|
|
"solde": minutes_to_hhmm(total_travaille - total_du),
|
|
"solde_min": total_travaille - total_du,
|
|
"reste_min": reste_min,
|
|
"reste": minutes_to_hhmm(reste_min),
|
|
"extra_min": extra_total,
|
|
"extra": minutes_to_hhmm(extra_total),
|
|
"avance_min": avance_total,
|
|
"avance": minutes_to_hhmm(avance_total),
|
|
"slots_restants": slots_restants,
|
|
"moyenne_extra_min": (extra_total // slots_restants) if slots_restants else 0,
|
|
"moyenne_extra": minutes_to_hhmm((extra_total // slots_restants) if slots_restants else 0),
|
|
}
|