Calcul de trajet auto après scrape + recalcul local intelligent
- trajet.py : fonction couts() pure (calcul des coûts sans appel réseau) - app.py : mémorisation distance/durée/péage + adresses dans le CSV ; auto-calcul best-effort après ajout (scrape simple et en lot) ; api_distance recalcule en local si adresses inchangées, complet sinon - index.html : affichage auto du trajet en cache à l'ouverture du prospect
This commit is contained in:
93
app.py
93
app.py
@@ -26,6 +26,8 @@ COLONNES = [
|
||||
"Nom du prospect", "Statut", "Département", "Ville", "Code Postal", "Adresse",
|
||||
"Date d'ajout", "Date de contact", "Nom de contact",
|
||||
"Téléphone", "Email", "Infos du lieu", "Type", "Lien Facebook", "Notes",
|
||||
"Trajet distance km", "Trajet durée min", "Trajet km péage",
|
||||
"Trajet adresse départ", "Trajet adresse arrivée",
|
||||
]
|
||||
STATUT_DEFAUT = "À contacter"
|
||||
|
||||
@@ -49,6 +51,8 @@ CONFIG_DEFAUT = {
|
||||
app = Flask(__name__, static_folder="static")
|
||||
verrou_scrape = threading.Lock()
|
||||
verrou_csv = threading.Lock()
|
||||
# Sérialise les calculs de trajet (un appel Nominatim/OSRM à la fois, ~1 req/s).
|
||||
verrou_trajet = threading.Lock()
|
||||
|
||||
|
||||
def lire_prospects():
|
||||
@@ -121,6 +125,56 @@ def ecrire_config(config):
|
||||
os.replace(tmp, CONFIG_PATH)
|
||||
|
||||
|
||||
def _adresse_arrivee(p):
|
||||
"""Reconstruit l'adresse d'arrivée d'un prospect (Adresse, Code Postal, Ville)."""
|
||||
return ", ".join(
|
||||
x.strip() for x in (p.get("Adresse"), p.get("Code Postal"), p.get("Ville")) if x and x.strip())
|
||||
|
||||
|
||||
def _tarifs(config):
|
||||
"""(conso L/100km, prix €/L, péage €/km) depuis la config, avec replis."""
|
||||
return (
|
||||
float(config.get("conso_l_100km") or 0) or CONFIG_DEFAUT["conso_l_100km"],
|
||||
float(config.get("prix_carburant") or 0) or CONFIG_DEFAUT["prix_carburant"],
|
||||
float(config.get("cout_peage_km") or 0), # 0 = pas de péage compté
|
||||
)
|
||||
|
||||
|
||||
def _ecrire_colonnes_trajet(ligne, depart, arrivee, res):
|
||||
"""Mémorise dans une ligne les données géo + adresses utilisées pour le calcul."""
|
||||
ligne["Trajet distance km"] = str(res["distance_km"])
|
||||
ligne["Trajet durée min"] = str(res["duree_min"])
|
||||
ligne["Trajet km péage"] = str(res["km_peage"])
|
||||
ligne["Trajet adresse départ"] = depart
|
||||
ligne["Trajet adresse arrivée"] = arrivee
|
||||
|
||||
|
||||
def _auto_trajet(ligne, config):
|
||||
"""Calcule et mémorise le trajet d'un prospect fraîchement ajouté (best-effort).
|
||||
|
||||
Conçu pour tourner dans un thread daemon : un échec réseau ne doit jamais
|
||||
affecter le scrape. `verrou_trajet` sérialise les appels Nominatim/OSRM.
|
||||
"""
|
||||
try:
|
||||
depart = (config.get("adresse_depart") or "").strip()
|
||||
arrivee = _adresse_arrivee(ligne)
|
||||
lien = (ligne.get("Lien Facebook") or "").strip()
|
||||
if not depart or not arrivee:
|
||||
return
|
||||
with verrou_trajet:
|
||||
res = trajet.calculer(depart, arrivee, *_tarifs(config))
|
||||
with verrou_csv:
|
||||
lignes = lire_prospects()
|
||||
cible = next(
|
||||
(l for l in lignes if (l.get("Lien Facebook") or "").strip() == lien), None
|
||||
) if lien else None
|
||||
if cible is not None:
|
||||
_ecrire_colonnes_trajet(cible, depart, arrivee, res)
|
||||
ecrire_prospects(lignes)
|
||||
except Exception:
|
||||
pass # best-effort : le trajet sera calculé au clic si besoin
|
||||
|
||||
|
||||
@app.get("/")
|
||||
def accueil():
|
||||
return app.send_static_file("index.html")
|
||||
@@ -186,6 +240,7 @@ def api_ajouter():
|
||||
lignes = lire_prospects()
|
||||
lignes.append(ligne)
|
||||
ecrire_prospects(lignes)
|
||||
threading.Thread(target=_auto_trajet, args=(ligne, lire_config()), daemon=True).start()
|
||||
return jsonify({"ok": True, "index": len(lignes) - 1})
|
||||
|
||||
|
||||
@@ -274,9 +329,11 @@ def _ajouter_resultat_lot(resultat):
|
||||
journaliser(url, "doublon", champs["Nom du prospect"])
|
||||
return {"url": url, "statut": "doublon", "nom": champs["Nom du prospect"]}
|
||||
champs["Statut"] = STATUT_DEFAUT
|
||||
lignes.append({col: champs.get(col, "") for col in COLONNES})
|
||||
ligne = {col: champs.get(col, "") for col in COLONNES}
|
||||
lignes.append(ligne)
|
||||
ecrire_prospects(lignes)
|
||||
journaliser(url, "ajoute", champs["Nom du prospect"])
|
||||
threading.Thread(target=_auto_trajet, args=(ligne, lire_config()), daemon=True).start()
|
||||
return {"url": url, "statut": "ajoute", "nom": champs["Nom du prospect"]}
|
||||
|
||||
|
||||
@@ -377,24 +434,42 @@ def api_distance(idx):
|
||||
"error": "pas_de_depart",
|
||||
"message": "Renseignez d'abord votre adresse de départ dans les Paramètres.",
|
||||
}), 400
|
||||
arrivee = ", ".join(x.strip() for x in (p.get("Adresse"), p.get("Code Postal"), p.get("Ville")) if x and x.strip())
|
||||
arrivee = _adresse_arrivee(p)
|
||||
if not arrivee:
|
||||
return jsonify({
|
||||
"error": "pas_d_adresse",
|
||||
"message": "Ce prospect n'a pas d'adresse ou de ville exploitable.",
|
||||
}), 400
|
||||
conso, prix, peage = _tarifs(config)
|
||||
|
||||
# Recalcul local : les données géo en cache restent valides tant que les deux
|
||||
# adresses n'ont pas changé. On réapplique simplement les prix courants.
|
||||
if (p.get("Trajet distance km")
|
||||
and p.get("Trajet adresse départ") == depart
|
||||
and p.get("Trajet adresse arrivée") == arrivee):
|
||||
try:
|
||||
resultat = trajet.calculer(
|
||||
depart, arrivee,
|
||||
float(config.get("conso_l_100km") or 0) or CONFIG_DEFAUT["conso_l_100km"],
|
||||
float(config.get("prix_carburant") or 0) or CONFIG_DEFAUT["prix_carburant"],
|
||||
float(config.get("cout_peage_km") or 0), # 0 = pas de péage compté
|
||||
)
|
||||
return jsonify(resultat)
|
||||
return jsonify({**trajet.couts(
|
||||
float(p["Trajet distance km"]),
|
||||
float(p.get("Trajet durée min") or 0),
|
||||
float(p.get("Trajet km péage") or 0),
|
||||
conso, prix, peage,
|
||||
), "source": "local"})
|
||||
except (TypeError, ValueError):
|
||||
pass # données en cache illisibles : on bascule sur un recalcul complet
|
||||
|
||||
# Recalcul complet : appels Nominatim/OSRM, puis mémorisation des données géo.
|
||||
try:
|
||||
resultat = trajet.calculer(depart, arrivee, conso, prix, peage)
|
||||
except trajet.ErreurTrajet as e:
|
||||
return jsonify({"error": "trajet", "message": str(e)}), 502
|
||||
except Exception as e:
|
||||
return jsonify({"error": "erreur", "message": f"Échec du calcul : {e}"}), 500
|
||||
with verrou_csv:
|
||||
lignes = lire_prospects()
|
||||
if 0 <= idx < len(lignes):
|
||||
_ecrire_colonnes_trajet(lignes[idx], depart, arrivee, resultat)
|
||||
ecrire_prospects(lignes)
|
||||
return jsonify({**resultat, "source": "api"})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -908,13 +908,18 @@ function construireDetail(corps, p) {
|
||||
trajetBtn.textContent = "🚗 Calculer le trajet";
|
||||
const resu = document.createElement("div");
|
||||
resu.className = "trajet-resu";
|
||||
trajetBtn.addEventListener("click", async () => {
|
||||
// `silencieux` : affichage auto à l'ouverture (données déjà en cache) — pas de spinner.
|
||||
async function calculerTrajet(silencieux) {
|
||||
trajetBtn.disabled = true;
|
||||
resu.innerHTML = '<span class="spinner sombre"></span> Calcul en cours…';
|
||||
if (!silencieux) resu.innerHTML = '<span class="spinner sombre"></span> Calcul en cours…';
|
||||
try {
|
||||
const rep = await fetch(`/api/distance/${p.index}`);
|
||||
const d = await rep.json();
|
||||
if (!rep.ok) { resu.innerHTML = `<span style="color:#b03a2e">${echap(d.message || "Échec du calcul.")}</span>`; return; }
|
||||
if (!rep.ok) {
|
||||
// En mode silencieux, on n'affiche pas d'erreur : on laisse simplement le bouton.
|
||||
if (!silencieux) resu.innerHTML = `<span style="color:#b03a2e">${echap(d.message || "Échec du calcul.")}</span>`;
|
||||
return;
|
||||
}
|
||||
const detailAller = d.peage_aller > 0
|
||||
? ` <span class="sous">(carburant ${d.carburant_aller} € + péage ${d.peage_aller} €)</span>` : "";
|
||||
const detailAR = d.peage_aller_retour > 0
|
||||
@@ -925,13 +930,17 @@ function construireDetail(corps, p) {
|
||||
`Aller : <strong>${d.cout_aller} €</strong>${detailAller}<br>` +
|
||||
`Aller-retour : <strong>${d.cout_aller_retour} €</strong>${detailAR} ` +
|
||||
`<span class="sous">(${d.distance_aller_retour_km} km)</span>`;
|
||||
trajetBtn.textContent = "🔄 Recalculer le trajet";
|
||||
} catch {
|
||||
resu.innerHTML = '<span style="color:#b03a2e">Le serveur ne répond pas.</span>';
|
||||
if (!silencieux) resu.innerHTML = '<span style="color:#b03a2e">Le serveur ne répond pas.</span>';
|
||||
} finally {
|
||||
trajetBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
trajetBtn.addEventListener("click", () => calculerTrajet(false));
|
||||
secTrajet.append(trajetBtn, resu);
|
||||
// Trajet déjà calculé (après le scrape) : on l'affiche tout de suite (recalcul local instantané).
|
||||
if ((p["Trajet distance km"] || "").trim()) calculerTrajet(true);
|
||||
|
||||
corps.append(secInfos, secContact, secTrajet);
|
||||
}
|
||||
|
||||
19
trajet.py
19
trajet.py
@@ -75,14 +75,15 @@ def itineraire(depart, arrivee):
|
||||
return route["distance"] / 1000.0, route["duration"] / 60.0, metres_autoroute / 1000.0
|
||||
|
||||
|
||||
def calculer(adresse_depart, adresse_arrivee, conso_l_100km, prix_carburant, cout_peage_km=0.0):
|
||||
"""Estime distance, durée, coût carburant et péage (aller simple et aller-retour).
|
||||
def couts(distance_km, duree_min, km_peage, conso_l_100km, prix_carburant, cout_peage_km=0.0):
|
||||
"""Données géographiques + tarifs -> dict complet (coûts aller simple et aller-retour).
|
||||
|
||||
Calcul purement local (aucun appel réseau) : permet de réappliquer de nouveaux prix
|
||||
sur une distance déjà connue sans re-géocoder ni re-router.
|
||||
|
||||
Le péage est estimé : km d'autoroute du trajet × `cout_peage_km` (tarif moyen
|
||||
paramétrable, en €/km). Mettre 0 pour ne pas compter de péage.
|
||||
"""
|
||||
distance_km, duree_min, km_peage = itineraire(
|
||||
geocoder(adresse_depart), geocoder(adresse_arrivee))
|
||||
carburant = distance_km / 100.0 * conso_l_100km * prix_carburant
|
||||
peage = km_peage * cout_peage_km
|
||||
cout_aller = carburant + peage
|
||||
@@ -100,6 +101,16 @@ def calculer(adresse_depart, adresse_arrivee, conso_l_100km, prix_carburant, cou
|
||||
}
|
||||
|
||||
|
||||
def calculer(adresse_depart, adresse_arrivee, conso_l_100km, prix_carburant, cout_peage_km=0.0):
|
||||
"""Estime distance, durée, coût carburant et péage (aller simple et aller-retour).
|
||||
|
||||
Fait les appels réseau (Nominatim + OSRM) puis délègue le calcul des coûts à `couts`.
|
||||
"""
|
||||
distance_km, duree_min, km_peage = itineraire(
|
||||
geocoder(adresse_depart), geocoder(adresse_arrivee))
|
||||
return couts(distance_km, duree_min, km_peage, conso_l_100km, prix_carburant, cout_peage_km)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
if len(sys.argv) != 3:
|
||||
|
||||
Reference in New Issue
Block a user