Merge branch 'dev' into feature/73_compte_utilisateur
This commit is contained in:
+16
-8
@@ -49,6 +49,16 @@ Vous pouvez consommer les données qui alimentent decp.info
|
||||
|
||||
- en les téléchargeant [sur data.gouv.fr](https://www.data.gouv.fr/datasets/donnees-essentielles-de-la-commande-publique-consolidees-format-tabulaire) (Parquet, CSV), pensez à lire la description du jeu de données
|
||||
- en interrogeant l'[API REST ouverte](https://www.data.gouv.fr/datasets/donnees-essentielles-de-la-commande-publique-consolidees-format-tabulaire#user-content-api-rest)
|
||||
"""
|
||||
),
|
||||
html.H4("API privée", id="api-privee"),
|
||||
dcc.Markdown(
|
||||
"""
|
||||
Une API HTTP est disponible pour accéder aux mêmes données par programme.
|
||||
Documentation interactive : [Swagger UI](/api/v1/swagger).
|
||||
|
||||
L'accès se fait sur token. Pour en obtenir un, contactez
|
||||
[colin@maudry.com](mailto:colin@maudry.com).
|
||||
"""
|
||||
),
|
||||
html.H4("Contact", id="contact"),
|
||||
@@ -87,14 +97,7 @@ Vous pouvez consommer les données qui alimentent decp.info
|
||||
dcc.Markdown(
|
||||
"""Les données visibles sur ce site proviennent exclusivement de la publication de données ouvertes par les acheteurs publics ou en leur nom, régie par [l'arrêté du 22 décembre 2022](https://www.legifrance.gouv.fr/jorf/id/JORFTEXT000046850496). Leur qualité est donc principalement liée à la qualité de leur saisie par les agents publics, parfois peu aidé·es par la qualité des outils à leur disposition. Je pense que l'analyse de marchés individuels et le comptage de marchés sur des critères autres que financiers sont plutôt fiables. En revanche, certains montants de marché estimés à des valeurs farfelues ([1 euro](https://decp.info/marches/432766947000192025S01301), [1 milliard](https://decp.info/marches/2459004280001320210000000271)) faussent les calculs par aggrégation (sommes, moyennes, médianes) et donc la production de statistiques financières fiables. Acheteurs, acheteuses : s'il vous plaît, essayez d'estimer les montants des marchés publics attribués de manière plus précise.
|
||||
|
||||
Quant à l'exhaustivité, je consolide toutes les sources de données exploitables que j'ai pu identifier (voir [ci-dessous](/a-propos#sources). Certains profils d'acheteurs ne publient pas leurs données malgré l'obligation réglementaire :
|
||||
|
||||
- klekoon.fr (ils y travaillent)
|
||||
- safetender.com (Omnikles)
|
||||
|
||||
**marches-publics.info** (AWS) publie ses données de manière assez sporadique depuis début 2023. Compte tenu de son poids dans le secteur, c'est assez dommageable pour la transparence des marchés publics.
|
||||
|
||||
Au milieu de ces mauvaises nouvelles, je tiens à souligner la belle continuité de la publication par la DGFiP des données des marchés publics remontées via le [protocole PES](https://www.collectivites-locales.gouv.fr/finances-locales/le-protocole-dechange-standard-pes). Merci à leurs équipes."""
|
||||
Quant à l'exhaustivité, je consolide toutes les sources de données exploitables que j'ai pu identifier (voir [ci-dessous](/bin.usr-is-merged/)). Je tiens à souligner la belle continuité de la publication par la DGFiP des données des marchés publics remontées via le [protocole PES](https://www.collectivites-locales.gouv.fr/finances-locales/le-protocole-dechange-standard-pes). Merci à leurs équipes."""
|
||||
),
|
||||
html.H4("Sources de données ", id="sources"),
|
||||
get_sources_tables(os.getenv("SOURCE_STATS_CSV_PATH")),
|
||||
@@ -156,6 +159,11 @@ J'enregistre également les données suivantes, de manière anonyme, afin de mie
|
||||
href="#donnees-brutes",
|
||||
className="toc-link",
|
||||
),
|
||||
html.A(
|
||||
"API privée",
|
||||
href="#api-privee",
|
||||
className="toc-link",
|
||||
),
|
||||
html.A(
|
||||
"Contact", href="#contact", className="toc-link"
|
||||
),
|
||||
|
||||
+14
-4
@@ -34,6 +34,7 @@ from src.utils.table import (
|
||||
get_default_hidden_columns,
|
||||
prepare_table_data,
|
||||
sort_table_data,
|
||||
write_styled_excel,
|
||||
)
|
||||
from src.utils.tracking import track_search
|
||||
|
||||
@@ -257,8 +258,15 @@ def update_acheteur_infos(url):
|
||||
if data_etablissement:
|
||||
data_etablissement = data_etablissement[0]
|
||||
|
||||
# Extraction du code département à partir du code postal
|
||||
code_postal = data_etablissement.get("code_postal", "")
|
||||
departement_code = code_postal[:2] if code_postal else None
|
||||
|
||||
# Création de la carte avec le code département pour un centrage approprié
|
||||
acheteur_map = point_on_map(
|
||||
data_etablissement["latitude"], data_etablissement["longitude"]
|
||||
data_etablissement["latitude"],
|
||||
data_etablissement["longitude"],
|
||||
departement_code,
|
||||
)
|
||||
code_departement, nom_departement, nom_region = get_departement_region(
|
||||
data_etablissement["code_postal"]
|
||||
@@ -390,8 +398,10 @@ def download_acheteur_data(
|
||||
df_to_download = pl.DataFrame(data)
|
||||
|
||||
def to_bytes(buffer):
|
||||
df_to_download.write_excel(
|
||||
buffer, worksheet="DECP" if annee in ["Toutes les années", None] else annee
|
||||
write_styled_excel(
|
||||
df_to_download,
|
||||
buffer,
|
||||
worksheet="DECP" if annee in ["Toutes les années", None] else annee,
|
||||
)
|
||||
|
||||
date = datetime.datetime.now().strftime("%Y-%m-%d_%H:%M:%S")
|
||||
@@ -432,7 +442,7 @@ def download_filtered_acheteur_data(
|
||||
lff = sort_table_data(lff, sort_by)
|
||||
|
||||
def to_bytes(buffer):
|
||||
lff.collect(engine="streaming").write_excel(buffer, worksheet="DECP")
|
||||
write_styled_excel(lff.collect(engine="streaming"), buffer)
|
||||
|
||||
date = datetime.datetime.now().strftime("%Y-%m-%d_%H:%M:%S")
|
||||
return dcc.send_bytes(
|
||||
|
||||
@@ -2,6 +2,7 @@ import polars as pl
|
||||
from dash import Input, Output, callback, dcc, html, register_page
|
||||
|
||||
from src.db import get_cursor
|
||||
from src.utils import logger
|
||||
from src.utils.data import DF_ACHETEURS, DF_TITULAIRES
|
||||
|
||||
NAME = "Liste des marchés publics"
|
||||
@@ -27,9 +28,12 @@ def make_org_nom_verbe(org_type, org_id) -> tuple:
|
||||
|
||||
|
||||
def get_title(code, org_type, org_id):
|
||||
org_nom, verbe = make_org_nom_verbe(org_type, org_id)
|
||||
|
||||
return f"Marchés publics {verbe} par {org_nom} | decp.info"
|
||||
if org_type:
|
||||
org_nom, verbe = make_org_nom_verbe(org_type, org_id)
|
||||
return f"Marchés publics {verbe} par {org_nom} | decp.info"
|
||||
else:
|
||||
logger.warning(f"Pas de org_type pour org_id: {org_id}")
|
||||
return "Marchés publics | decp.info"
|
||||
|
||||
|
||||
def get_description(code, org_type, org_id):
|
||||
|
||||
@@ -0,0 +1,297 @@
|
||||
from dash import Input, Output, State, callback, ctx, dcc, html, register_page
|
||||
|
||||
from src.utils.seo import META_CONTENT
|
||||
|
||||
NAME = "Quelles données pour quelles étapes et quels seuils dans les marchés publics ?"
|
||||
|
||||
register_page(
|
||||
__name__,
|
||||
path="/etapes",
|
||||
title=f"{NAME} | decp.info",
|
||||
name="Étapes et données",
|
||||
description=(
|
||||
"À chaque étape d'un marché public (programmation, publicité, "
|
||||
"attribution), quelles données sont publiées et à partir de quel "
|
||||
"seuil : DECP, BOAMP, JOUE, journaux d'annonces légales, Approch."
|
||||
),
|
||||
image_url=META_CONTENT["image_url"],
|
||||
)
|
||||
|
||||
# Contenu des fiches — à rédiger en Markdown.
|
||||
# Clés barres : "bar-approch", "bar-jal", "bar-boamp", "bar-joue-marche",
|
||||
# "bar-decp", "bar-joue-attribution"
|
||||
# Clés étapes (mobile) : "stage-programmation", "stage-publicite",
|
||||
# "stage-attribution", "stage-contrat", "stage-paiement"
|
||||
ALL_CONTENT: dict[str, str | None] = {
|
||||
"bar-approch": None,
|
||||
"bar-jal": None,
|
||||
"bar-boamp": None,
|
||||
"bar-joue-marche": None,
|
||||
"bar-decp": None,
|
||||
"bar-joue-attribution": None,
|
||||
"stage-programmation": None,
|
||||
"stage-publicite": None,
|
||||
"stage-attribution": None,
|
||||
"stage-contrat": None,
|
||||
"stage-paiement": None,
|
||||
}
|
||||
|
||||
_BAR_IDS = [
|
||||
"bar-approch",
|
||||
"bar-jal",
|
||||
"bar-boamp",
|
||||
"bar-joue-marche",
|
||||
"bar-decp",
|
||||
"bar-joue-attribution",
|
||||
]
|
||||
_STAGE_IDS = [
|
||||
"stage-programmation",
|
||||
"stage-publicite",
|
||||
"stage-attribution",
|
||||
"stage-contrat",
|
||||
"stage-paiement",
|
||||
]
|
||||
|
||||
|
||||
def _lane(*bars):
|
||||
"""Une ligne d'étape : fond segmenté en 5 + barres positionnées."""
|
||||
return html.Div(
|
||||
className="etapes-lane",
|
||||
children=[
|
||||
html.Div(
|
||||
className="etapes-segs",
|
||||
children=[html.Div() for _ in range(5)],
|
||||
),
|
||||
*bars,
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def _bar(label, color, style, bar_id=None):
|
||||
base = {"backgroundColor": color}
|
||||
base.update(style)
|
||||
props = {"className": "etapes-bar", "style": base}
|
||||
if bar_id is not None:
|
||||
props["id"] = bar_id
|
||||
props["n_clicks"] = 0
|
||||
return html.Div(label, **props)
|
||||
|
||||
|
||||
def build_chart():
|
||||
return html.Div(
|
||||
className="etapes-chart-scroll",
|
||||
children=html.Div(
|
||||
className="etapes-chart",
|
||||
children=[
|
||||
# En-tête : coin vide + 5 marqueurs de seuils
|
||||
html.Div(className="etapes-corner"),
|
||||
html.Div(
|
||||
className="etapes-xhead",
|
||||
children=[
|
||||
html.Div("0 €", className="etapes-xcell"),
|
||||
html.Div(
|
||||
[html.Strong("40 000 €"), "seuil DECP"],
|
||||
className="etapes-xcell",
|
||||
),
|
||||
html.Div(
|
||||
[html.Strong("90 000 €"), "publicité"],
|
||||
className="etapes-xcell",
|
||||
),
|
||||
html.Div(
|
||||
[html.Strong("140 k€ / 216 k€"), "seuils formalisés (UE)"],
|
||||
className="etapes-xcell",
|
||||
),
|
||||
html.Div(
|
||||
[html.Strong("5,404 M€"), "travaux (UE)"],
|
||||
className="etapes-xcell",
|
||||
),
|
||||
],
|
||||
),
|
||||
# Programmation
|
||||
html.Div("Programmation", className="etapes-stage"),
|
||||
_lane(
|
||||
_bar(
|
||||
"Approch — sourcing / préinformation (non réglementaire)",
|
||||
"#7c5cff",
|
||||
{"left": "2%", "right": "2%"},
|
||||
bar_id="bar-approch",
|
||||
),
|
||||
),
|
||||
# Publicité (appel d'offres)
|
||||
html.Div(["Publicité"], className="etapes-stage"),
|
||||
_lane(
|
||||
_bar(
|
||||
"JAL",
|
||||
"#f79009",
|
||||
{"left": "40%", "right": "40%", "top": "6px", "height": "20px"},
|
||||
bar_id="bar-jal",
|
||||
),
|
||||
_bar(
|
||||
"BOAMP",
|
||||
"#1570ef",
|
||||
{"left": "40%", "right": "2%", "top": "28px", "height": "20px"},
|
||||
bar_id="bar-boamp",
|
||||
),
|
||||
_bar(
|
||||
"JOUE — avis de marché",
|
||||
"#0e9384",
|
||||
{"left": "60%", "right": "2%", "top": "6px", "height": "20px"},
|
||||
bar_id="bar-joue-marche",
|
||||
),
|
||||
),
|
||||
# Attribution
|
||||
html.Div("Attribution", className="etapes-stage"),
|
||||
_lane(
|
||||
_bar(
|
||||
"DECP — données essentielles",
|
||||
"#12b76a",
|
||||
{"left": "20%", "right": "2%", "top": "6px", "height": "20px"},
|
||||
bar_id="bar-decp",
|
||||
),
|
||||
_bar(
|
||||
"JOUE — avis d'attribution",
|
||||
"#0e9384",
|
||||
{"left": "60%", "right": "2%", "top": "28px", "height": "20px"},
|
||||
bar_id="bar-joue-attribution",
|
||||
),
|
||||
),
|
||||
# Contrat (vide)
|
||||
html.Div("Contrat", className="etapes-stage"),
|
||||
html.Div(
|
||||
"— aucune donnée publiée aujourd'hui —",
|
||||
className="etapes-lane etapes-empty",
|
||||
),
|
||||
# Paiement (vide)
|
||||
html.Div("Paiement", className="etapes-stage"),
|
||||
html.Div(
|
||||
"— aucune donnée publiée aujourd'hui —",
|
||||
className="etapes-lane etapes-empty",
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
# Données par étape, partagées par la vue mobile.
|
||||
# Chaque tuple : (libellé étape, id CSS, [(libellé, couleur, plage seuils)]).
|
||||
STAGES_MOBILE = [
|
||||
(
|
||||
"Programmation",
|
||||
"stage-programmation",
|
||||
[
|
||||
("Approch", "#7c5cff", "tous montants — publication non réglementaire"),
|
||||
],
|
||||
),
|
||||
(
|
||||
"Publicité (appel d'offres)",
|
||||
"stage-publicite",
|
||||
[
|
||||
("JAL", "#f79009", "de 90 000 € au seuil formalisé"),
|
||||
("BOAMP", "#1570ef", "à partir de 90 000 €"),
|
||||
(
|
||||
"JOUE — avis de marché",
|
||||
"#0e9384",
|
||||
"à partir des seuils formalisés (140 k€ / 216 k€)",
|
||||
),
|
||||
],
|
||||
),
|
||||
(
|
||||
"Attribution",
|
||||
"stage-attribution",
|
||||
[
|
||||
("DECP — données essentielles", "#12b76a", "à partir de 40 000 €"),
|
||||
("JOUE — avis d'attribution", "#0e9384", "à partir des seuils formalisés"),
|
||||
],
|
||||
),
|
||||
("Contrat", "stage-contrat", []),
|
||||
("Paiement", "stage-paiement", []),
|
||||
]
|
||||
|
||||
|
||||
def build_mobile():
|
||||
blocks = []
|
||||
for stage, stage_id, items in STAGES_MOBILE:
|
||||
if items:
|
||||
children = [
|
||||
html.Div(
|
||||
[
|
||||
html.I(style={"backgroundColor": color}),
|
||||
html.Span(label, className="etapes-m-label"),
|
||||
html.Span(seuil, className="etapes-m-seuil"),
|
||||
],
|
||||
className="etapes-m-item",
|
||||
)
|
||||
for label, color, seuil in items
|
||||
]
|
||||
else:
|
||||
children = [
|
||||
html.Div(
|
||||
"aucune donnée publiée aujourd'hui",
|
||||
className="etapes-m-item etapes-m-empty",
|
||||
)
|
||||
]
|
||||
blocks.append(
|
||||
html.Div(
|
||||
[
|
||||
html.Div(
|
||||
[
|
||||
html.H4(stage, className="etapes-m-stage"),
|
||||
html.Button(
|
||||
"Voir fiche →",
|
||||
id=stage_id,
|
||||
n_clicks=0,
|
||||
className="etapes-m-link",
|
||||
),
|
||||
],
|
||||
className="etapes-m-header",
|
||||
),
|
||||
*children,
|
||||
],
|
||||
className="etapes-m-block",
|
||||
)
|
||||
)
|
||||
return html.Div(blocks, className="etapes-mobile")
|
||||
|
||||
|
||||
layout = html.Div(
|
||||
className="container",
|
||||
children=[
|
||||
html.H2(NAME),
|
||||
dcc.Markdown(
|
||||
"Un marché public passe par plusieurs étapes. À chacune, des "
|
||||
"données peuvent être publiées — selon le montant du marché et "
|
||||
"des obligations réglementaires. Ce graphique situe les "
|
||||
"principales publications de données par **étape** (de haut en "
|
||||
"bas) et par **seuil** (de gauche à droite, en euros hors taxes)."
|
||||
),
|
||||
build_chart(),
|
||||
build_mobile(),
|
||||
dcc.Store(id="etapes-selected", data=None),
|
||||
html.Div(id="etapes-detail", className="etapes-detail"),
|
||||
dcc.Markdown(
|
||||
"**À noter :** l'axe horizontal n'est pas linéaire — les seuils "
|
||||
"sont espacés régulièrement pour rester lisibles. Les étapes "
|
||||
"*Contrat* et *Paiement* n'ont aujourd'hui aucune donnée publiée "
|
||||
"en open data.",
|
||||
className="etapes-note",
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@callback(
|
||||
Output("etapes-detail", "children"),
|
||||
Output("etapes-selected", "data"),
|
||||
[Input(id_, "n_clicks") for id_ in _BAR_IDS + _STAGE_IDS],
|
||||
State("etapes-selected", "data"),
|
||||
prevent_initial_call=True,
|
||||
)
|
||||
def _show_detail(*args):
|
||||
current = args[-1]
|
||||
triggered = ctx.triggered_id
|
||||
if triggered == current:
|
||||
return None, None
|
||||
content = ALL_CONTENT.get(triggered)
|
||||
if content is None:
|
||||
return dcc.Markdown(f"*Fiche en cours de rédaction.* {triggered}"), triggered
|
||||
return dcc.Markdown(content), triggered
|
||||
+3
-2
@@ -109,7 +109,7 @@ def update_marche_info(marche, titulaires):
|
||||
column_object = DATA_SCHEMA.get(col)
|
||||
column_name = column_object.get("title") if column_object else col
|
||||
|
||||
if marche[col]:
|
||||
if marche and col in marche:
|
||||
if col == "acheteur_nom":
|
||||
value = html.A(
|
||||
href=f"/acheteurs/{marche['acheteur_id']}",
|
||||
@@ -134,6 +134,7 @@ def update_marche_info(marche, titulaires):
|
||||
"considerationsSociales",
|
||||
"considerationsEnvironnementales",
|
||||
]
|
||||
and col in marche
|
||||
and "," in marche[col]
|
||||
):
|
||||
col_values = marche[col].split(", ")
|
||||
@@ -243,7 +244,7 @@ def get_marche_jsonld(marche, titulaires) -> str:
|
||||
titulaire.get("titulaire_id"),
|
||||
org_name=titulaire.get("titulaire_nom"),
|
||||
org_type="titulaire",
|
||||
type_org_id=titulaire.get("titulaire_typeIdentifiant"),
|
||||
type_org_id=titulaire.get("titulaire_typeIdentifiant", "SIRET"),
|
||||
),
|
||||
"orderedItem": {
|
||||
"@type": type_order,
|
||||
|
||||
+47
-22
@@ -16,10 +16,11 @@ from dash import (
|
||||
register_page,
|
||||
)
|
||||
|
||||
from src.db import query_marches, schema
|
||||
from src.db import schema
|
||||
from src.figures import (
|
||||
DataTable,
|
||||
get_barchart_sources,
|
||||
get_considerations_card_content,
|
||||
get_dashboard_summary_table,
|
||||
get_distance_histogram,
|
||||
get_duplicate_matrix,
|
||||
@@ -39,7 +40,12 @@ from src.utils.data import (
|
||||
)
|
||||
from src.utils.frontend import get_enum_values_as_dict
|
||||
from src.utils.seo import META_CONTENT
|
||||
from src.utils.table import COLUMNS, get_default_hidden_columns, prepare_table_data
|
||||
from src.utils.table import (
|
||||
COLUMNS,
|
||||
get_default_hidden_columns,
|
||||
prepare_table_data,
|
||||
write_styled_excel,
|
||||
)
|
||||
|
||||
NAME = "Observatoire"
|
||||
|
||||
@@ -508,17 +514,26 @@ Alors, on fait comment ?
|
||||
size="xl",
|
||||
),
|
||||
# DataTable
|
||||
html.Div(
|
||||
className="marches_table",
|
||||
children=DataTable(
|
||||
dtid="observatoire-preview-table",
|
||||
page_size=5,
|
||||
page_action="custom",
|
||||
sort_action="custom",
|
||||
filter_action="custom",
|
||||
hidden_columns=[],
|
||||
columns=[{"id": col, "name": col} for col in OBSERVATOIRE_COLUMNS],
|
||||
),
|
||||
dcc.Loading(
|
||||
overlay_style={"visibility": "visible", "filter": "blur(2px)"},
|
||||
id="loading-statistques",
|
||||
type="default",
|
||||
children=[
|
||||
html.Div(
|
||||
className="marches_table",
|
||||
children=DataTable(
|
||||
dtid="observatoire-preview-table",
|
||||
page_size=5,
|
||||
page_action="custom",
|
||||
sort_action="custom",
|
||||
filter_action="custom",
|
||||
hidden_columns=[],
|
||||
columns=[
|
||||
{"id": col, "name": col} for col in OBSERVATOIRE_COLUMNS
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -664,10 +679,8 @@ def _compute_dashboard_children(filter_params_normalized: tuple):
|
||||
k: (list(v) if isinstance(v, tuple) else v) for k, v in filter_params_normalized
|
||||
}
|
||||
|
||||
lff: pl.LazyFrame = query_marches().lazy()
|
||||
lff = prepare_dashboard_data(lff=lff, **filter_params)
|
||||
|
||||
dff = lff.collect(engine="streaming")
|
||||
dff = prepare_dashboard_data(**filter_params)
|
||||
lff = dff.lazy()
|
||||
|
||||
df_per_uid = (
|
||||
dff.select("uid", "montant").group_by("uid").agg(pl.col("montant").first())
|
||||
@@ -715,6 +728,15 @@ def _compute_dashboard_children(filter_params_normalized: tuple):
|
||||
)
|
||||
)
|
||||
|
||||
considerations_content = get_considerations_card_content(lff)
|
||||
cards.append(
|
||||
make_card(
|
||||
title="Considérations sociales et environnementales",
|
||||
subtitle="part des marchés concernés",
|
||||
fig=considerations_content,
|
||||
)
|
||||
)
|
||||
|
||||
distance_histogram = get_distance_histogram(lff)
|
||||
cards.append(
|
||||
make_card(
|
||||
@@ -788,13 +810,13 @@ def update_dashboard_cards(*filter_values):
|
||||
prevent_initial_call=True,
|
||||
)
|
||||
def download_observatoire(_n_clicks, filter_params, hidden_columns):
|
||||
lff = prepare_dashboard_data(lff=query_marches().lazy(), **(filter_params or {}))
|
||||
dff = prepare_dashboard_data(**(filter_params or {}))
|
||||
|
||||
if hidden_columns:
|
||||
lff = lff.drop(hidden_columns)
|
||||
dff = dff.drop(hidden_columns)
|
||||
|
||||
def to_bytes(buffer):
|
||||
lff.collect(engine="streaming").write_excel(buffer, worksheet="DECP")
|
||||
write_styled_excel(dff, buffer)
|
||||
|
||||
date = datetime.now().strftime("%Y-%m-%d_%H:%M:%S")
|
||||
return dcc.send_bytes(to_bytes, filename=f"decp_observatoire_{date}.xlsx")
|
||||
@@ -817,6 +839,9 @@ def toggle_montant_modal(n_triggers, _close):
|
||||
prevent_initial_call=False,
|
||||
)
|
||||
def add_organization_name_in_title(acheteur_id, titulaire_id):
|
||||
acheteur_id = acheteur_id.replace(" ", "") if acheteur_id else None
|
||||
titulaire_id = titulaire_id.replace(" ", "") if titulaire_id else None
|
||||
|
||||
def lookup_nom(df_org, id_col, nom_col, org_id):
|
||||
match = df_org.filter(pl.col(id_col) == org_id)
|
||||
return match[nom_col].item(0) if match.height >= 1 else None
|
||||
@@ -879,10 +904,10 @@ def populate_preview_table(
|
||||
if not is_open:
|
||||
return (no_update,) * 9
|
||||
|
||||
lff = prepare_dashboard_data(lff=query_marches().lazy(), **(filter_params or {}))
|
||||
dff = prepare_dashboard_data(**(filter_params or {}))
|
||||
|
||||
return prepare_table_data(
|
||||
lff,
|
||||
dff.lazy(),
|
||||
data_timestamp,
|
||||
filter_query,
|
||||
page_current,
|
||||
|
||||
+26
-13
@@ -21,7 +21,7 @@ from dash import (
|
||||
|
||||
from src.db import query_marches, schema
|
||||
from src.figures import DataTable, make_column_picker
|
||||
from src.utils import logger
|
||||
from src.utils import get_data_update_timestamp, logger
|
||||
from src.utils.seo import META_CONTENT
|
||||
from src.utils.table import (
|
||||
COLUMNS,
|
||||
@@ -30,12 +30,20 @@ from src.utils.table import (
|
||||
invert_columns,
|
||||
prepare_table_data,
|
||||
sort_table_data,
|
||||
write_styled_excel,
|
||||
)
|
||||
from src.utils.tracking import track_search
|
||||
|
||||
update_date_timestamp = os.path.getmtime(os.getenv("DATA_FILE_PARQUET_PATH"))
|
||||
update_date = datetime.fromtimestamp(update_date_timestamp).strftime("%d/%m/%Y")
|
||||
update_date_iso = datetime.fromtimestamp(update_date_timestamp).isoformat()
|
||||
update_date_timestamp = get_data_update_timestamp(
|
||||
os.getenv("DATA_FILE_PARQUET_PATH", ""),
|
||||
os.getenv("DUCKDB_PATH", "./decp.duckdb"),
|
||||
)
|
||||
if update_date_timestamp is not None:
|
||||
update_date = datetime.fromtimestamp(update_date_timestamp).strftime("%d/%m/%Y")
|
||||
update_date_iso = datetime.fromtimestamp(update_date_timestamp).isoformat()
|
||||
else:
|
||||
update_date = "date inconnue"
|
||||
update_date_iso = ""
|
||||
|
||||
|
||||
NAME = "Tableau"
|
||||
@@ -117,7 +125,11 @@ layout = [
|
||||
"contentUrl": "https://www.data.gouv.fr/api/1/datasets/r/11cea8e8-df3e-4ed1-932b-781e2635e432",
|
||||
},
|
||||
],
|
||||
"temporalCoverage": f"2018-01-01/{update_date_iso[:10]}",
|
||||
**(
|
||||
{"temporalCoverage": f"2018-01-01/{update_date_iso[:10]}"}
|
||||
if update_date_iso
|
||||
else {}
|
||||
),
|
||||
"spatialCoverage": {
|
||||
"@type": "Place",
|
||||
"address": {"countryCode": "FR"},
|
||||
@@ -163,18 +175,19 @@ layout = [
|
||||
|
||||
Vous pouvez appliquer un filtre pour chaque colonne en entrant du texte sous le nom de la colonne, puis en tapant sur `Entrée`.
|
||||
|
||||
- Champs textuels : la recherche retourne les valeurs qui contiennent le texte recherché et n'est pas sensible à la casse (majuscules/minuscules).
|
||||
- Exemple : `rennes` retourne "RENNES METROPOLE".
|
||||
- Champs textuels : la recherche retourne les valeurs qui contiennent le texte recherché, n'est sensible ni à la casse (majuscules/minuscules), ni à l'accentuation.
|
||||
- `rennes` => le texte contient "rennes"
|
||||
- `metro* *pole` => le texte contient un mot qui commence par "metro" et un mot qui finit par "pole"
|
||||
- `metropole rennes` => le texte contient les mots "metropole" et "rennes", n'importe où dans le texte
|
||||
- `métropole+rennes` => le texte contient "metropole rennes" ou "métropole rennes", collé et dans cet ordre
|
||||
- `metropole+rennes travaux distri*` => le texte contient "metropole rennes", "travaux" et un mot qui commence par "distri"
|
||||
- Les guillemets simples (apostrophe du 4) doivent être prédédées d'une barre oblique (AltGr + 8). Exemple : `services d\\\'assurances`
|
||||
- Champs numériques (Durée en mois, Montant, ...) : vous pouvez...
|
||||
- soit taper un nombre pour trouver les valeurs strictement égales. Exemple : `12` ne retourne que des 12
|
||||
- soit le précéder de **>** ou **<** pour filtrer les valeurs supérieures ou inférieures. Exemple pour les offres reçues : `> 4` retourne les marchés ayant reçu plus de 4 offres.
|
||||
- Champs date (Date de notification, ...) : vous pouvez également utiliser **>** ou **<**. Exemples :
|
||||
- Champs date (Date de notification, ...) :
|
||||
- `< 2024-01-31` pour "avant le 31 janvier 2024"
|
||||
- `2024` pour "en 2024", `> 2022` pour "à partir de 2022".
|
||||
- Pour les champs textuels et les champs dates :
|
||||
- pour chercher du texte qui **commence par** votre texte, entrez `texte*`. C'est par exemple utile pour filtrer des acheteurs ou titulaires par numéro SIREN (`123456789*`) ou les marchés sur une année en particulier (`2024*`)
|
||||
- pour chercher du texte qui **finit par** votre texte, entrez `*texte`
|
||||
- `2024` pour "en 2024", `> 2022` pour "à partir de 2022"
|
||||
|
||||
Vous pouvez filtrer plusieurs colonnes à la fois.
|
||||
|
||||
@@ -330,7 +343,7 @@ def download_data(n_clicks, filter_query, sort_by, hidden_columns: list | None =
|
||||
lff = sort_table_data(lff, sort_by)
|
||||
|
||||
def to_bytes(buffer):
|
||||
lff.collect(engine="streaming").write_excel(buffer, worksheet="DECP")
|
||||
write_styled_excel(lff.collect(engine="streaming"), buffer)
|
||||
|
||||
date = datetime.now().strftime("%Y-%m-%d_%H:%M:%S")
|
||||
return dcc.send_bytes(to_bytes, filename=f"decp_{date}.xlsx")
|
||||
|
||||
+38
-11
@@ -33,6 +33,7 @@ from src.utils.table import (
|
||||
get_default_hidden_columns,
|
||||
prepare_table_data,
|
||||
sort_table_data,
|
||||
write_styled_excel,
|
||||
)
|
||||
from src.utils.tracking import track_search
|
||||
|
||||
@@ -86,13 +87,19 @@ layout = [
|
||||
className="mb-2",
|
||||
children=[
|
||||
dbc.Col(
|
||||
html.H2(
|
||||
children=[
|
||||
html.Span(id="titulaire_siret"),
|
||||
" - ",
|
||||
html.Span(id="titulaire_nom"),
|
||||
],
|
||||
),
|
||||
[
|
||||
html.H2(
|
||||
children=[
|
||||
html.Span(id="titulaire_siret"),
|
||||
" - ",
|
||||
html.Span(id="titulaire_nom"),
|
||||
],
|
||||
),
|
||||
html.P(
|
||||
id="titulaire_activite_libelle",
|
||||
style={"color": "gray", "marginTop": "-10px"},
|
||||
),
|
||||
],
|
||||
width=8,
|
||||
),
|
||||
dbc.Col(
|
||||
@@ -254,17 +261,34 @@ layout = [
|
||||
Output(component_id="titulaire_departement", component_property="children"),
|
||||
Output(component_id="titulaire_region", component_property="children"),
|
||||
Output(component_id="titulaire_lien_annuaire", component_property="href"),
|
||||
Output(component_id="titulaire_activite_libelle", component_property="children"),
|
||||
Input(component_id="titulaire_url", component_property="pathname"),
|
||||
)
|
||||
def update_titulaire_infos(url):
|
||||
titulaire_siret = url.split("/")[-1]
|
||||
if "titulaire_activite_libelle" in DF_TITULAIRES.columns:
|
||||
activite_libelle_row = DF_TITULAIRES.filter(
|
||||
pl.col("titulaire_id") == titulaire_siret
|
||||
).select("titulaire_activite_libelle")
|
||||
activite_libelle = (
|
||||
activite_libelle_row.item(0, 0) if activite_libelle_row.height > 0 else ""
|
||||
)
|
||||
else:
|
||||
activite_libelle = ""
|
||||
data = get_annuaire_data(titulaire_siret)
|
||||
data_etablissement = data.get("matching_etablissements") if data else None
|
||||
if data_etablissement:
|
||||
data_etablissement = data_etablissement[0]
|
||||
|
||||
# Extraction du code département à partir du code postal
|
||||
code_postal = data_etablissement.get("code_postal", "")
|
||||
departement_code = code_postal[:2] if code_postal else None
|
||||
|
||||
# Création de la carte avec le code département pour un centrage approprié
|
||||
titulaire_map = point_on_map(
|
||||
data_etablissement["latitude"], data_etablissement["longitude"]
|
||||
data_etablissement["latitude"],
|
||||
data_etablissement["longitude"],
|
||||
departement_code,
|
||||
)
|
||||
code_departement, nom_departement, nom_region = get_departement_region(
|
||||
data_etablissement["code_postal"]
|
||||
@@ -294,6 +318,7 @@ def update_titulaire_infos(url):
|
||||
departement,
|
||||
nom_region,
|
||||
lien_annuaire,
|
||||
activite_libelle,
|
||||
)
|
||||
|
||||
|
||||
@@ -411,8 +436,10 @@ def download_titulaire_data(
|
||||
df_to_download = pl.DataFrame(data)
|
||||
|
||||
def to_bytes(buffer):
|
||||
df_to_download.write_excel(
|
||||
buffer, worksheet="DECP" if annee in ["Toutes les années", None] else annee
|
||||
write_styled_excel(
|
||||
df_to_download,
|
||||
buffer,
|
||||
worksheet="DECP" if annee in ["Toutes les années", None] else annee,
|
||||
)
|
||||
|
||||
date = datetime.datetime.now().strftime("%Y-%m-%d_%H:%M:%S")
|
||||
@@ -453,7 +480,7 @@ def download_filtered_titulaire_data(
|
||||
lff = sort_table_data(lff, sort_by)
|
||||
|
||||
def to_bytes(buffer):
|
||||
lff.collect(engine="streaming").write_excel(buffer, worksheet="DECP")
|
||||
write_styled_excel(lff.collect(engine="streaming"), buffer)
|
||||
|
||||
date = datetime.datetime.now().strftime("%Y-%m-%d_%H:%M:%S")
|
||||
return dcc.send_bytes(
|
||||
|
||||
Reference in New Issue
Block a user