diff --git a/src/assets/style.css b/src/assets/style.css index 2058973..c8449d4 100644 --- a/src/assets/style.css +++ b/src/assets/style.css @@ -76,6 +76,7 @@ td[data-dash-column="objet"] { } .marches_table .cell-table tr:nth-child(even) td { background-color: #feeeee; + font-family: sans-serif; } #header > *, @@ -117,14 +118,34 @@ h3 { display: grid; grid-gap: 10px; margin-bottom: 50px; + justify-content: space-between; +} + +.org_title { + grid-column: 1 / 3; + grid-row: 1; +} + +.org_year { + grid-column: 3; + grid-row: 1; } .org_infos { grid-column: 1; - grid-row: 1; + grid-row: 2; +} + +.org_infos > p { + margin: 8px 0; +} + +.org_stats { + grid-column: 2; + grid-row: 2; } .org_map { - grid-column: 2; - grid-row: 1; + grid-column: 3; + grid-row: 2; } diff --git a/src/pages/acheteur.py b/src/pages/acheteur.py index 7db212b..56a50a9 100644 --- a/src/pages/acheteur.py +++ b/src/pages/acheteur.py @@ -1,8 +1,10 @@ +import datetime + import polars as pl from dash import Input, Output, callback, dash_table, dcc, html, register_page from src.figures import point_on_map -from src.utils import get_annuaire_data, lf +from src.utils import format_number, get_annuaire_data, get_departement_region, lf register_page( __name__, @@ -20,24 +22,58 @@ layout = [ html.Div( className="container", children=[ - html.H2( - children=[ - html.Span(id="acheteur_siret"), - " - ", - html.Span(id="acheteur_nom"), - ] - ), html.Div( className="wrapper", children=[ + html.H2( + className="org_title", + children=[ + html.Span(id="acheteur_siret"), + " - ", + html.Span(id="acheteur_nom"), + ], + ), + html.Div( + className="org_year", + children=dcc.Dropdown( + id="acheteur_year", + options=["Toutes"] + + [ + str(year) + for year in range( + 2018, int(datetime.date.today().year) + 1 + ) + ], + placeholder="Année", + ), + ), html.Div( className="org_infos", - children=[html.P(["Commune : ", html.Span(id="commune")])], + children=[ + html.P(["Commune : ", html.Strong(id="acheteur_commune")]), + html.P( + [ + "Département : ", + html.Strong(id="acheteur_departement"), + ] + ), + html.P(["Région : ", html.Strong(id="acheteur_region")]), + html.A( + id="acheteur_lien_annuaire", + children="Plus de détails sur l'Annuaire des entreprises", + target="_blank", + ), + ], + ), + html.Div( + className="org_stats", + children=[ + html.P(id="acheteur_titre_stats"), + html.P(id="acheteur_marches_notifies"), + html.P(id="acheteur_fournisseurs_differents"), + ], ), html.Div(className="org_map", id="acheteur_map"), - # adresse - # code commune - # lat long ], ), # récupérer les données de l'acheteur sur l'api annuaire @@ -51,38 +87,85 @@ layout = [ @callback( Output(component_id="acheteur_siret", component_property="children"), Output(component_id="acheteur_nom", component_property="children"), - Output(component_id="commune", component_property="children"), + Output(component_id="acheteur_commune", component_property="children"), Output(component_id="acheteur_map", component_property="children"), + Output(component_id="acheteur_departement", component_property="children"), + Output(component_id="acheteur_region", component_property="children"), + Output(component_id="acheteur_lien_annuaire", component_property="href"), Input(component_id="url", component_property="pathname"), ) -def update_acheteur(url): +def update_acheteur_infos(url): acheteur_siret = url.split("/")[-1] if len(acheteur_siret) != 14: - return f"Le SIRET renseigné doit faire 14 caractères ({acheteur_siret})" + acheteur_siret = ( + f"Le SIRET renseigné doit faire 14 caractères ({acheteur_siret})" + ) data = get_annuaire_data(acheteur_siret) data_etablissement = data["matching_etablissements"][0] acheteur_map = point_on_map( data_etablissement["latitude"], data_etablissement["longitude"] ) + code_departement, nom_departement, nom_region = get_departement_region( + data_etablissement["code_postal"] + ) + departement = f"{nom_departement} ({code_departement})" + lien_annuaire = ( + f"https://annuaire-entreprises.data.gouv.fr/etablissement/{acheteur_siret}" + ) return ( acheteur_siret, data["nom_raison_sociale"], data_etablissement["libelle_commune"], acheteur_map, + departement, + nom_region, + lien_annuaire, ) +@callback( + Output(component_id="acheteur_marches_notifies", component_property="children"), + Output( + component_id="acheteur_fournisseurs_differents", component_property="children" + ), + Input(component_id="acheteur_data", component_property="data"), +) +def update_acheteur_stats(data): + df = pl.DataFrame(data) + df_marches = df.unique("uid") + nb_marches = format_number(df_marches.height) + # somme_marches = format_number(int(df_marches.select(pl.sum("montant")).item())) + marches_notifies = [html.Strong(nb_marches), " marchés et accord-cadres attribués"] + # + ", pour un total de ", html.Strong(somme_marches + " €")] + del df_marches + + nb_fournisseurs = df.unique("titulaire_id").height + nb_fournisseurs = [ + html.Strong(format_number(nb_fournisseurs)), + " fournisseurs (SIRET) différents", + ] + del df + + return marches_notifies, nb_fournisseurs + + @callback( Output(component_id="acheteur_data", component_property="data"), Input(component_id="url", component_property="pathname"), + Input(component_id="acheteur_year", component_property="value"), ) -def get_acheteur_marches_data(url) -> pl.LazyFrame: +def get_acheteur_marches_data(url, acheteur_year: str) -> pl.LazyFrame: acheteur_siret = url.split("/")[-1] lff = lf.filter(pl.col("acheteur_id") == acheteur_siret) + if acheteur_year and acheteur_year != "Toutes": + lff = lff.filter( + pl.col("dateNotification").cast(pl.String).str.starts_with(acheteur_year) + ) lff = lff.select( "uid", "objet", "dateNotification", + "titulaire_id", "titulaire_nom", "montant", "codeCPV", @@ -98,11 +181,25 @@ def get_acheteur_marches_data(url) -> pl.LazyFrame: Input(component_id="acheteur_data", component_property="data"), ) def get_last_marches_table(data) -> html.Div: + columns = data[0].keys() + table = html.Div( className="marches_table", children=dash_table.DataTable( data=data, page_action="native", + columns=[ + { + "name": i, + "id": i, + "presentation": "markdown", + "type": "text", + "format": {"nully": "N/A"}, + "hideable": False, + } + for i in columns + if i not in ["titulaire_id"] + ], page_size=10, style_cell_conditional=[ { diff --git a/src/utils.py b/src/utils.py index cc49a09..9900ee9 100644 --- a/src/utils.py +++ b/src/utils.py @@ -1,3 +1,4 @@ +import json import logging import os from time import sleep @@ -124,9 +125,6 @@ def get_decp_data() -> pl.LazyFrame: sleep(10) lff: pl.LazyFrame = pl.scan_parquet(os.getenv("DATA_FILE_PARQUET_PATH")) - # Remplacement des valeurs numériques par des chaînes de caractères - # lff = numbers_to_strings(lff) - # Tri des marchés par date de notification lff = lff.sort(by=["dateNotification"], descending=True, nulls_last=True) @@ -136,4 +134,21 @@ def get_decp_data() -> pl.LazyFrame: return lff +def get_departements() -> dict: + with open("data/departements.json", "rb") as f: + data = json.load(f) + return data + + +def get_departement_region(code_postal): + if code_postal > "97000": + code_departement = code_postal[:3] + else: + code_departement = code_postal[:2] + nom_departement = departements[code_departement]["departement"] + nom_region = departements[code_departement]["region"] + return code_departement, nom_departement, nom_region + + lf = get_decp_data() +departements = get_departements()