import json from datetime import datetime import dash_bootstrap_components as dbc from dash import Input, Output, callback, dcc, html, register_page from polars import selectors as cs from src.db import query_marches from src.utils.data import DATA_SCHEMA from src.utils.seo import META_CONTENT, make_org_jsonld from src.utils.table import format_values, unformat_montant def get_title(uid: str = None) -> str: return f"Marché {uid} | decp.info" register_page( __name__, path_template="/marches/", title=get_title, name="Marché", description="Consultez les détails de ce marché public : montant, acheteur, titulaires, modifications, etc.", image_url=META_CONTENT["image_url"], order=7, ) layout = [ dcc.Store(id="marche_data"), dcc.Store(id="titulaires_data"), dcc.Location(id="marche_url", refresh="callback-nav"), html.Script(type="application/ld+json", id="marche_jsonld"), dbc.Container( className="marche_infos", children=[ dbc.Row( dbc.Col( [ html.H1(id="marche_objet", style={"fontSize": "1.5em"}), html.P( "Vous consultez un résumé des données de ce marché public" ), html.Ul( [ html.Li( "après son attribution aux titulaires qui l'ont remporté à la suite d'un appel d'offres (ou sans appel d'offres via une attribution directe)" ), html.Li( "après avoir appliqué les éventuelles modifications de montant, durée ou titulaires renseignées par l'acheteur" ), ] ), html.P( "Le montant total payé aux titulaires, la durée du marché et la liste des titulaires peuvent cependant encore évoluer jusqu'à la fin de l'exécution du marché." ), ] ) ), dbc.Row( [ dbc.Col(id="marche_infos_1", width=12, md=4), dbc.Col(id="marche_infos_2", width=12, md=4), dbc.Col( width=12, md=4, children=[ html.H4("Titulaires"), html.Ul(id="marche_infos_titulaires"), ], ), ] ), ], ), ] @callback( Output("marche_data", "data"), Output("titulaires_data", "data"), Input(component_id="marche_url", component_property="pathname"), ) def get_marche_data(url) -> tuple[dict, list]: marche_uid = url.split("/")[-1] # Filtre SQL côté DuckDB, puis Polars pour le post-traitement dff_marche = query_marches("uid = ?", (marche_uid,)) if dff_marche.height == 0: return {}, [] lff = dff_marche.lazy() dff_titulaires = lff.select(cs.starts_with("titulaire")).collect(engine="streaming") dff_marche_unique = lff.unique("uid").collect(engine="streaming") dff_marche_unique = format_values(dff_marche_unique) return dff_marche_unique.to_dicts()[0], dff_titulaires.to_dicts() @callback( Output("marche_objet", "children"), Output("marche_infos_1", "children"), Output("marche_infos_2", "children"), Output("marche_infos_titulaires", "children"), Input("marche_data", "data"), Input("titulaires_data", "data"), ) def update_marche_info(marche, titulaires): def make_parameter(col, bold=True): column_object = DATA_SCHEMA.get(col) column_name = column_object.get("title") if column_object else col if marche and col in marche: if col == "acheteur_nom": value = html.A( href=f"/acheteurs/{marche['acheteur_id']}", children=marche["acheteur_nom"], ) elif col == "sourceDataset": value = html.A( href=marche["sourceFile"], children=marche["sourceDataset"] ) column_name = "Source des données" # Dates elif col in ["dateNotification", "datePublicationDonnees"]: value = datetime.fromisoformat(marche[col]).strftime("%d/%m/%Y") # Listes elif ( col in [ "techniques", "typesPrix", "considerationsSociales", "considerationsEnvironnementales", ] and col in marche and "," in marche[col] ): col_values = marche[col].split(", ") lines = [] for val in col_values: lines.append(html.Li(val)) _content = html.Div( [html.P([column_name, " : "]), html.Ul(children=lines)] ) return _content else: value = marche.get(col) else: value = "" value = html.Strong(value) if bold else value param_content = html.P([column_name, " : ", value]) return param_content marche_objet = make_parameter("objet", bold=False) marche_infos = [ make_parameter("id"), make_parameter("dateNotification"), # date make_parameter("nature"), make_parameter("acheteur_nom"), # lien make_parameter("montant"), make_parameter("codeCPV"), make_parameter("procedure"), make_parameter("techniques"), # list make_parameter("dureeMois"), make_parameter("dureeRestanteMois"), make_parameter("offresRecues"), make_parameter("datePublicationDonnees"), # date make_parameter("formePrix"), make_parameter("typesPrix"), # list make_parameter("attributionAvance"), make_parameter("tauxAvance"), make_parameter("marcheInnovant"), # label make_parameter("modalitesExecution"), make_parameter("considerationsSociales"), # list make_parameter("considerationsEnvironnementales"), # list make_parameter("ccag"), make_parameter("sousTraitanceDeclaree"), make_parameter("typeGroupementOperateurs"), make_parameter("origineFrance"), make_parameter("origineUE"), make_parameter("idAccordCadre"), make_parameter("sourceDataset"), # lien ] half = round(len(marche_infos) / 2) # pas inclus pour l'instant : lieu d'exécution, modifications titulaires_lines = [] for titulaire in titulaires: if titulaire["titulaire_typeIdentifiant"] == "SIRET": categorie = titulaire.get("titulaire_categorie", "") if titulaire.get("titulaire_distance"): distance = str(titulaire.get("titulaire_distance")) + " km" else: distance = "" content = html.Li( [ html.A( href=f"/titulaires/{titulaire['titulaire_id']}", children=titulaire["titulaire_nom"], ), f" ({categorie}, {distance})", ] ) else: content = html.Li(titulaire["titulaire_nom"]) titulaires_lines.append(content) return marche_objet, marche_infos[:half], marche_infos[half:], titulaires_lines @callback( Output(component_id="marche_jsonld", component_property="children"), Input("marche_data", "data"), Input("titulaires_data", "data"), ) def get_marche_jsonld(marche, titulaires) -> str: acheteur_id = marche.get("acheteur_id") type_order = ( "Service" if marche.get("categorie") in ["Services", "Travaux"] else "Product" ) result = [] for titulaire in titulaires: jsonld = { "@context": "https://schema.org", "@type": "Order", "@id": f"https://decp.info/marches/{marche.get('uid')}", "name": f"{marche.get('nature')} conclu par {marche.get('acheteur_nom')} le {marche.get('dateNotification')}", "description": marche.get("objet"), "orderNumber": marche.get("uid"), "orderDate": marche.get("dateNotification"), "price": unformat_montant(marche.get("montant")), "priceCurrency": "EUR", "customer": make_org_jsonld( acheteur_id, org_name=marche.get("acheteur_nom"), org_type="acheteur" ), "seller": make_org_jsonld( titulaire.get("titulaire_id"), org_name=titulaire.get("titulaire_nom"), org_type="titulaire", type_org_id=titulaire.get("titulaire_typeIdentifiant", "SIRET"), ), "orderedItem": { "@type": type_order, "name": marche.get("objet"), "category": { "@type": "CategoryCode", "propertyID": "cpv", "codeValue": marche.get("codeCPV"), # "description": "Description du code CPV" }, # "serviceType": "Description du code CPV" }, } result.append(jsonld) return json.dumps(result, indent=2)