Liens entre pages acheteur et titulaire, fournisseur => titulaire #28 #35

This commit is contained in:
Colin Maudry
2025-09-28 21:01:22 +02:00
parent d00efcd500
commit d9db156626
5 changed files with 99 additions and 71 deletions
+1 -1
View File
@@ -38,7 +38,7 @@ python run.py
#### 2.1.0 #### 2.1.0
- Ajout des vues [acheteur](https://decp.info/acheteurs/24350013900189) et [fournisseur](https://decp.info/fournisseurs/51903758414786) ([#28](https://github.com/ColinMaudry/decp.info/issues/28) et [#35](https://github.com/ColinMaudry/decp.info/issues/35)) - Ajout des vues [acheteur](https://decp.info/acheteurs/24350013900189) et [titulaire](https://decp.info/titulaires/51903758414786) ([#28](https://github.com/ColinMaudry/decp.info/issues/28) et [#35](https://github.com/ColinMaudry/decp.info/issues/35))
- Ajout des balises HTML meta Open Graph et Twitter ([#39](https://github.com/ColinMaudry/decp.info/issues/39)) pour de beaux aperçus de liens - Ajout des balises HTML meta Open Graph et Twitter ([#39](https://github.com/ColinMaudry/decp.info/issues/39)) pour de beaux aperçus de liens
- Variables globales uniquement en lecture - Variables globales uniquement en lecture
+1 -1
View File
@@ -24,7 +24,7 @@ j'aimerais beaucoup échanger avec vous pour comprendre vos cas d'usages et vos
En effet, le potentiel des données d'attribution de marchés et des données qui peuvent les enrichir est très loin d'être exploité par En effet, le potentiel des données d'attribution de marchés et des données qui peuvent les enrichir est très loin d'être exploité par
les fonctionnalités actuelles de decp.info. Il est ainsi possible de rajouter les fonctionnalités actuelles de decp.info. Il est ainsi possible de rajouter
- de nombreuses visualisations de données (cartes, graphiques, tableaux) sur des thématiques variées (vivacité de la concurrence, secteurs d'activité, insertion par l'activité économique (IAE), distance acheteur-fournisseur...) - de nombreuses visualisations de données (cartes, graphiques, tableaux) sur des thématiques variées (vivacité de la concurrence, secteurs d'activité, insertion par l'activité économique (IAE), distance acheteur-titulaire...)
- la sauvegarde de filtres pour les retrouver plus tard et les partager - la sauvegarde de filtres pour les retrouver plus tard et les partager
- des alertes par email si des marchés correspondant à certains critères - des alertes par email si des marchés correspondant à certains critères
- le développement d'une API pour alimenter d'autres logiciels - le développement d'une API pour alimenter d'autres logiciels
+18 -9
View File
@@ -4,7 +4,13 @@ import polars as pl
from dash import Input, Output, State, callback, dash_table, dcc, html, register_page from dash import Input, Output, State, callback, dash_table, dcc, html, register_page
from src.figures import point_on_map from src.figures import point_on_map
from src.utils import format_number, get_annuaire_data, get_departement_region, lf from src.utils import (
add_org_links_in_dict,
format_number,
get_annuaire_data,
get_departement_region,
lf,
)
register_page( register_page(
__name__, __name__,
@@ -71,7 +77,7 @@ layout = [
children=[ children=[
html.P(id="acheteur_titre_stats"), html.P(id="acheteur_titre_stats"),
html.P(id="acheteur_marches_attribues"), html.P(id="acheteur_marches_attribues"),
html.P(id="acheteur_fournisseurs_differents"), html.P(id="acheteur_titulaires_differents"),
html.Button( html.Button(
"Téléchargement au format Excel", "Téléchargement au format Excel",
id="btn-download-acheteur-data", id="btn-download-acheteur-data",
@@ -132,7 +138,7 @@ def update_acheteur_infos(url):
@callback( @callback(
Output(component_id="acheteur_marches_attribues", component_property="children"), Output(component_id="acheteur_marches_attribues", component_property="children"),
Output( Output(
component_id="acheteur_fournisseurs_differents", component_property="children" component_id="acheteur_titulaires_differents", component_property="children"
), ),
Input(component_id="acheteur_data", component_property="data"), Input(component_id="acheteur_data", component_property="data"),
) )
@@ -147,14 +153,14 @@ def update_acheteur_stats(data):
# + ", pour un total de ", html.Strong(somme_marches + " €")] # + ", pour un total de ", html.Strong(somme_marches + " €")]
del df_marches del df_marches
nb_fournisseurs = df.unique("titulaire_id").height nb_titulaires = df.unique("titulaire_id").height
nb_fournisseurs = [ nb_titulaires = [
html.Strong(format_number(nb_fournisseurs)), html.Strong(format_number(nb_titulaires)),
" fournisseurs (SIRET) différents", " titulaires (SIRET) différents",
] ]
del df del df
return marches_attribues, nb_fournisseurs return marches_attribues, nb_titulaires
@callback( @callback(
@@ -201,11 +207,14 @@ def get_last_marches_table(data) -> html.Div:
"dureeMois", "dureeMois",
] ]
data = add_org_links_in_dict(data, "titulaire")
table = html.Div( table = html.Div(
className="marches_table", className="marches_table",
id="marches_datatable", id="marches_datatable",
children=dash_table.DataTable( children=dash_table.DataTable(
data=data, data=data,
markdown_options={"html": True},
page_action="native", page_action="native",
columns=[ columns=[
{ {
@@ -234,7 +243,7 @@ def get_last_marches_table(data) -> html.Div:
"minWidth": "200px", "minWidth": "200px",
"textAlign": "left", "textAlign": "left",
"overflow": "hidden", "overflow": "hidden",
"lineHeight": "14px", "lineHeight": "18px",
"whiteSpace": "normal", "whiteSpace": "normal",
}, },
], ],
@@ -4,12 +4,18 @@ import polars as pl
from dash import Input, Output, State, callback, dash_table, dcc, html, register_page from dash import Input, Output, State, callback, dash_table, dcc, html, register_page
from src.figures import point_on_map from src.figures import point_on_map
from src.utils import format_number, get_annuaire_data, get_departement_region, lf from src.utils import (
add_org_links_in_dict,
format_number,
get_annuaire_data,
get_departement_region,
lf,
)
register_page( register_page(
__name__, __name__,
path_template="/fournisseurs/<fournisseur_id>", path_template="/titulaires/<titulaire_id>",
title="decp.info - fournisseur", title="decp.info - titulaire",
name="Fournisseur", name="Fournisseur",
order=5, order=5,
) )
@@ -17,7 +23,7 @@ register_page(
# 21690123100011 # 21690123100011
layout = [ layout = [
dcc.Store(id="fournisseur_data", storage_type="memory"), dcc.Store(id="titulaire_data", storage_type="memory"),
dcc.Location(id="url", refresh="callback-nav"), dcc.Location(id="url", refresh="callback-nav"),
html.Div( html.Div(
className="container", className="container",
@@ -28,15 +34,15 @@ layout = [
html.H2( html.H2(
className="org_title", className="org_title",
children=[ children=[
html.Span(id="fournisseur_siret"), html.Span(id="titulaire_siret"),
" - ", " - ",
html.Span(id="fournisseur_nom"), html.Span(id="titulaire_nom"),
], ],
), ),
html.Div( html.Div(
className="org_year", className="org_year",
children=dcc.Dropdown( children=dcc.Dropdown(
id="fournisseur_year", id="titulaire_year",
options=["Toutes"] options=["Toutes"]
+ [ + [
str(year) str(year)
@@ -51,18 +57,16 @@ layout = [
className="org_infos", className="org_infos",
children=[ children=[
# TODO: ajouter le type d'acheteur : commune, CD, CR, etc. # TODO: ajouter le type d'acheteur : commune, CD, CR, etc.
html.P( html.P(["Commune : ", html.Strong(id="titulaire_commune")]),
["Commune : ", html.Strong(id="fournisseur_commune")]
),
html.P( html.P(
[ [
"Département : ", "Département : ",
html.Strong(id="fournisseur_departement"), html.Strong(id="titulaire_departement"),
] ]
), ),
html.P(["Région : ", html.Strong(id="fournisseur_region")]), html.P(["Région : ", html.Strong(id="titulaire_region")]),
html.A( html.A(
id="fournisseur_lien_annuaire", id="titulaire_lien_annuaire",
children="Plus de détails sur l'Annuaire des entreprises", children="Plus de détails sur l'Annuaire des entreprises",
target="_blank", target="_blank",
), ),
@@ -71,46 +75,46 @@ layout = [
html.Div( html.Div(
className="org_stats", className="org_stats",
children=[ children=[
html.P(id="fournisseur_titre_stats"), html.P(id="titulaire_titre_stats"),
html.P(id="fournisseur_marches_remportes"), html.P(id="titulaire_marches_remportes"),
html.P(id="fournisseur_acheteurs_differents"), html.P(id="titulaire_acheteurs_differents"),
html.Button( html.Button(
"Téléchargement au format Excel", "Téléchargement au format Excel",
id="btn-download-fournisseur-data", id="btn-download-titulaire-data",
), ),
dcc.Download(id="download-fournisseur-data"), dcc.Download(id="download-titulaire-data"),
], ],
), ),
html.Div(className="org_map", id="fournisseur_map"), html.Div(className="org_map", id="titulaire_map"),
], ],
), ),
# récupérer les données de l'acheteur sur l'api annuaire # récupérer les données de l'acheteur sur l'api annuaire
html.H3("Derniers marchés publics remportés"), html.H3("Derniers marchés publics remportés"),
html.Div(id="fournisseur_last_marches", children=""), html.Div(id="titulaire_last_marches", children=""),
], ],
), ),
] ]
@callback( @callback(
Output(component_id="fournisseur_siret", component_property="children"), Output(component_id="titulaire_siret", component_property="children"),
Output(component_id="fournisseur_nom", component_property="children"), Output(component_id="titulaire_nom", component_property="children"),
Output(component_id="fournisseur_commune", component_property="children"), Output(component_id="titulaire_commune", component_property="children"),
Output(component_id="fournisseur_map", component_property="children"), Output(component_id="titulaire_map", component_property="children"),
Output(component_id="fournisseur_departement", component_property="children"), Output(component_id="titulaire_departement", component_property="children"),
Output(component_id="fournisseur_region", component_property="children"), Output(component_id="titulaire_region", component_property="children"),
Output(component_id="fournisseur_lien_annuaire", component_property="href"), Output(component_id="titulaire_lien_annuaire", component_property="href"),
Input(component_id="url", component_property="pathname"), Input(component_id="url", component_property="pathname"),
) )
def update_fournisseur_infos(url): def update_titulaire_infos(url):
fournisseur_siret = url.split("/")[-1] titulaire_siret = url.split("/")[-1]
if len(fournisseur_siret) != 14: if len(titulaire_siret) != 14:
fournisseur_siret = ( titulaire_siret = (
f"Le SIRET renseigné doit faire 14 caractères ({fournisseur_siret})" f"Le SIRET renseigné doit faire 14 caractères ({titulaire_siret})"
) )
data = get_annuaire_data(fournisseur_siret) data = get_annuaire_data(titulaire_siret)
data_etablissement = data["matching_etablissements"][0] data_etablissement = data["matching_etablissements"][0]
fournisseur_map = point_on_map( titulaire_map = point_on_map(
data_etablissement["latitude"], data_etablissement["longitude"] data_etablissement["latitude"], data_etablissement["longitude"]
) )
code_departement, nom_departement, nom_region = get_departement_region( code_departement, nom_departement, nom_region = get_departement_region(
@@ -118,13 +122,13 @@ def update_fournisseur_infos(url):
) )
departement = f"{nom_departement} ({code_departement})" departement = f"{nom_departement} ({code_departement})"
lien_annuaire = ( lien_annuaire = (
f"https://annuaire-entreprises.data.gouv.fr/etablissement/{fournisseur_siret}" f"https://annuaire-entreprises.data.gouv.fr/etablissement/{titulaire_siret}"
) )
return ( return (
fournisseur_siret, titulaire_siret,
data["nom_raison_sociale"], data["nom_raison_sociale"],
data_etablissement["libelle_commune"], data_etablissement["libelle_commune"],
fournisseur_map, titulaire_map,
departement, departement,
nom_region, nom_region,
lien_annuaire, lien_annuaire,
@@ -132,13 +136,13 @@ def update_fournisseur_infos(url):
@callback( @callback(
Output(component_id="fournisseur_marches_remportes", component_property="children"), Output(component_id="titulaire_marches_remportes", component_property="children"),
Output( Output(
component_id="fournisseur_acheteurs_differents", component_property="children" component_id="titulaire_acheteurs_differents", component_property="children"
), ),
Input(component_id="fournisseur_data", component_property="data"), Input(component_id="titulaire_data", component_property="data"),
) )
def update_fournisseur_stats(data): def update_titulaire_stats(data):
df = pl.DataFrame(data) df = pl.DataFrame(data)
if df.height == 0: if df.height == 0:
df = pl.DataFrame(schema=lf.collect_schema()) df = pl.DataFrame(schema=lf.collect_schema())
@@ -152,7 +156,7 @@ def update_fournisseur_stats(data):
nb_acheteurs = df.unique("acheteur_id").height nb_acheteurs = df.unique("acheteur_id").height
nb_acheteurs = [ nb_acheteurs = [
html.Strong(format_number(nb_acheteurs)), html.Strong(format_number(nb_acheteurs)),
" fournisseurs (SIRET) différents", " titulaires (SIRET) différents",
] ]
del df del df
@@ -160,14 +164,14 @@ def update_fournisseur_stats(data):
@callback( @callback(
Output(component_id="fournisseur_data", component_property="data"), Output(component_id="titulaire_data", component_property="data"),
Input(component_id="url", component_property="pathname"), Input(component_id="url", component_property="pathname"),
Input(component_id="fournisseur_year", component_property="value"), Input(component_id="titulaire_year", component_property="value"),
) )
def get_fournisseur_marches_data(url, fournisseur_year: str) -> pl.LazyFrame: def get_titulaire_marches_data(url, titulaire_year: str) -> pl.LazyFrame:
fournisseur_siret = url.split("/")[-1] titulaire_siret = url.split("/")[-1]
lff = lf.filter( lff = lf.filter(
(pl.col("titulaire_id") == fournisseur_siret) (pl.col("titulaire_id") == titulaire_siret)
& (pl.col("titulaire_typeIdentifiant") == "SIRET") & (pl.col("titulaire_typeIdentifiant") == "SIRET")
) )
lff = lff.fill_null("") lff = lff.fill_null("")
@@ -181,9 +185,9 @@ def get_fournisseur_marches_data(url, fournisseur_year: str) -> pl.LazyFrame:
"codeCPV", "codeCPV",
"dureeMois", "dureeMois",
) )
if fournisseur_year and fournisseur_year != "Toutes": if titulaire_year and titulaire_year != "Toutes":
lff = lff.filter( lff = lff.filter(
pl.col("dateNotification").cast(pl.String).str.starts_with(fournisseur_year) pl.col("dateNotification").cast(pl.String).str.starts_with(titulaire_year)
) )
lff = lff.sort(["dateNotification", "uid"], descending=True, nulls_last=True) lff = lff.sort(["dateNotification", "uid"], descending=True, nulls_last=True)
@@ -192,8 +196,8 @@ def get_fournisseur_marches_data(url, fournisseur_year: str) -> pl.LazyFrame:
@callback( @callback(
Output(component_id="fournisseur_last_marches", component_property="children"), Output(component_id="titulaire_last_marches", component_property="children"),
Input(component_id="fournisseur_data", component_property="data"), Input(component_id="titulaire_data", component_property="data"),
) )
def get_last_marches_table(data) -> html.Div: def get_last_marches_table(data) -> html.Div:
columns = [ columns = [
@@ -206,11 +210,15 @@ def get_last_marches_table(data) -> html.Div:
"dureeMois", "dureeMois",
] ]
# Ajout des liens vers les acheteurs
data = add_org_links_in_dict(data, "acheteur")
table = html.Div( table = html.Div(
className="marches_table", className="marches_table",
id="marches_datatable", id="marches_datatable",
children=dash_table.DataTable( children=dash_table.DataTable(
data=data, data=data,
markdown_options={"html": True},
page_action="native", page_action="native",
columns=[ columns=[
{ {
@@ -239,7 +247,7 @@ def get_last_marches_table(data) -> html.Div:
"minWidth": "200px", "minWidth": "200px",
"textAlign": "left", "textAlign": "left",
"overflow": "hidden", "overflow": "hidden",
"lineHeight": "14px", "lineHeight": "18px",
"whiteSpace": "normal", "whiteSpace": "normal",
}, },
], ],
@@ -249,17 +257,17 @@ def get_last_marches_table(data) -> html.Div:
@callback( @callback(
Output("download-fournisseur-data", "data"), Output("download-titulaire-data", "data"),
Input("btn-download-fournisseur-data", "n_clicks"), Input("btn-download-titulaire-data", "n_clicks"),
State(component_id="fournisseur_data", component_property="data"), State(component_id="titulaire_data", component_property="data"),
State(component_id="fournisseur_nom", component_property="children"), State(component_id="titulaire_nom", component_property="children"),
State(component_id="fournisseur_year", component_property="value"), State(component_id="titulaire_year", component_property="value"),
prevent_initial_call=True, prevent_initial_call=True,
) )
def download_fournisseur_data( def download_titulaire_data(
n_clicks, n_clicks,
data: [dict], data: [dict],
fournisseur_nom: str, titulaire_nom: str,
annee: str, annee: str,
): ):
df_to_download = pl.DataFrame(data) df_to_download = pl.DataFrame(data)
@@ -270,4 +278,4 @@ def download_fournisseur_data(
) )
date = datetime.datetime.now().strftime("%Y-%m-%d_%H:%M:%S") date = datetime.datetime.now().strftime("%Y-%m-%d_%H:%M:%S")
return dcc.send_bytes(to_bytes, filename=f"decp_{fournisseur_nom}_{date}.xlsx") return dcc.send_bytes(to_bytes, filename=f"decp_{titulaire_nom}_{date}.xlsx")
+11
View File
@@ -74,6 +74,17 @@ def add_org_links(dff: pl.DataFrame):
return dff return dff
def add_org_links_in_dict(data: list, org_type: str) -> list:
new_data = []
for marche in data:
org_id = marche[org_type + "_id"]
marche[org_type + "_nom"] = (
f'<a href="/{org_type}s/{org_id}">{marche[org_type + "_nom"]}</a>'
)
new_data.append(marche)
return new_data
def booleans_to_strings(lff: pl.LazyFrame) -> pl.LazyFrame: def booleans_to_strings(lff: pl.LazyFrame) -> pl.LazyFrame:
""" """
Convert all boolean columns to string type. Convert all boolean columns to string type.