diff --git a/app/app_db/app.db b/app/app_db/app.db index 1f6c7ef..51b0afe 100644 Binary files a/app/app_db/app.db and b/app/app_db/app.db differ diff --git a/app/data/datasets.py b/app/data/datasets.py new file mode 100644 index 0000000..750aa7c --- /dev/null +++ b/app/data/datasets.py @@ -0,0 +1,3 @@ +from scriptloader import get_sql +from db import get_conn + diff --git a/app/data/db.py b/app/data/db.py index 330dad0..316edd9 100644 --- a/app/data/db.py +++ b/app/data/db.py @@ -1,4 +1,7 @@ +import streamlit as st from sqlalchemy import create_engine, Text +from data.scriptloader import get_sql +import oracledb import pandas as pd from dotenv import load_dotenv from pathlib import Path @@ -26,6 +29,25 @@ def get_conn(db): logging.info(f"Datenbank {db} konnte nicht gefunden werden") return engine +def load_data(sql_file, db): + sql = get_sql(sql_file) + engine = get_conn(db) + with engine.connect(): + df = pd.read_sql(sql, engine) + return df + + + + + + + + + + + + + # def get_data(db): # engine = get_conn(db) # with engine.connect() as conn: diff --git a/app/data/scriptloader.py b/app/data/scriptloader.py index 6ff9b5b..52dccc6 100644 --- a/app/data/scriptloader.py +++ b/app/data/scriptloader.py @@ -7,4 +7,4 @@ def get_sql(filename): if __name__ == "__main__": - print(get_sql("sales_umsatz")) + print(get_sql("ergebnis_kpi")) diff --git a/app/data/sql/ergebnis_kpi.sql b/app/data/sql/ergebnis_kpi.sql new file mode 100644 index 0000000..073511a --- /dev/null +++ b/app/data/sql/ergebnis_kpi.sql @@ -0,0 +1,92 @@ +/******************************************************************************************* + * Basis-Daten aus der Kostenrechnung + *******************************************************************************************/ +with basis as( + Select + p.geschaeftsjahr as jahr, + p.geschaeftsperiode as monat, + p.periode_key, + b.zgrp1, + b.zgrp2, + b.zgrp3, + b.bezeichnung as Bereich, + ko.kostenobjekt, + ko.bezeichnung as obj_bezeichnung, + ko.obj_text, + ko.objektgruppe, + ko.objekttyp, + ko.verantwortlicher, + ks.co_koa_grp, + ks.co_grp_bez, + ks.co_grp_text, + ks.kostenart, + ks.koa_text, + coalesce(ws.uml_kz,'') as uml_kz, + coalesce(ws.zgrp_1_entlastung, '') as zgrp1_entlastung, + coalesce(ws.wert * -1, 0) as wert, + coalesce(ws.wert_plan * -1, 0) as wert_plan, + 0 as wert_forecast + from + co_dw.bi.fact_wertsummen ws + left join co_dw.bi.dim_periode p + on ws.periode_key = p.periode_key + left join co_dw.bi.dim_kostenobjekt ko + on ws.objekt_key = ko.objekt_key + left join co_dw.bi.dim_koastruktur ks + on ws.kostenart = ks.kostenart + left join co_dw.bi.dim_bereich b + on ko.bereich_key = b.bereich_key + where + ks.co_koa_grp_01 = 'GMN001' + and ko.objektgruppe in ('HKST', 'VKST', 'KTRG', 'KtrBereich', 'KtrMNr') + and p.geschaeftsperiode < 14 + and (p.geschaeftsjahr = :jahr or p.geschaeftsjahr = :jahr -1) +), + + + +/******************************************************************************************* + * Gesamtergebnis + *******************************************************************************************/ +ergebnis_gesamt as ( + select + zgrp1, + zgrp2, + zgrp3, + kostenobjekt, + obj_bezeichnung, + obj_text, + co_koa_grp, + co_grp_bez, + co_grp_text, + 'Ergebnis' as main_kpi, + 'Gesamtergebnis' as kpi, + round(sum(case when jahr = :jahr-1 then wert else 0 end),0) as vor_jahr, + round(sum(case when jahr = :jahr-1 and monat <= :monat then wert else 0 end),0) as vor_jahr_bis_monat, + round(sum(case when jahr = :jahr-1 and monat = :monat then wert else 0 end),0) as vor_jahr_monat, + round(sum(case when jahr = :jahr then wert else 0 end),0) as akt_jahr, + round(sum(case when jahr = :jahr and monat <= :monat then wert else 0 end),0) as akt_jahr_bis_monat, + round(sum(case when jahr = :jahr and monat = :monat then wert else 0 end),0) as akt_jahr_monat, + round(sum(case when jahr = :jahr then wert_plan else 0 end),0) as plan_akt_jahr, + round(sum(case when jahr = :jahr and monat <= :monat then wert_plan else 0 end),0) as plan_akt_jahr_bis_monat, + round(sum(case when jahr = :jahr and monat = :monat then wert_plan else 0 end),0) as plan_akt_jahr_monat, + round(sum(case when jahr = :jahr then wert_forecast else 0 end),0) as forecast_akt_jahr, + round(sum(case when jahr = :jahr and monat <= :monat then wert_forecast else 0 end),0) as forecast_akt_jahr_bis_monat, + round(sum(case when jahr = :jahr and monat = :monat then wert_forecast else 0 end),0) as forecast_akt_jahr_monat + from + basis + group by + zgrp1, + zgrp2, + zgrp3, + kostenobjekt, + obj_bezeichnung, + obj_text, + co_koa_grp, + co_grp_bez, + co_grp_text +) + + +select * from ergebnis_gesamt +select * from co_dw.bi.Fact_Wertsummen diff --git a/app/data/sql/ora_kostenobjekte.sql b/app/data/sql/ora_kostenobjekte.sql new file mode 100644 index 0000000..671d81a --- /dev/null +++ b/app/data/sql/ora_kostenobjekte.sql @@ -0,0 +1,19 @@ +/* Orlacle (PENTA) Kostenobjekte */ + +select + GESCHAEFTSJAHR as jahr, + OBJEKTTYP as typ, + KOSTENOBJEKT as obj, + BEZEICHNUNG, + VERANTWORTLICHER, + FELD_2_X30 as VORGESETZER, + ZUORDNUNGSGRUPPE_1 as zgrp1, + ZUORDNUNGSGRUPPE_2 as zgrp2, + ZUORDNUNGSGRUPPE_3 as zgrp3, + ZUORDNUNGSGRUPPE_4 as zgrp4, + ZUORDNUNGSGRUPPE_5 as zgrp5, + ZUORDNUNGSGRUPPE_6 as zgrp6, + FELD_1_X30 as fertigung, + OBJEKTGRUPPE as objgrp +from + pkos diff --git a/app/kpi/operating_result.py b/app/kpi/operating_result.py new file mode 100644 index 0000000..e69de29 diff --git a/app/pages/costobjects.py b/app/pages/costobjects.py index 35c5d77..b239f01 100644 --- a/app/pages/costobjects.py +++ b/app/pages/costobjects.py @@ -1,34 +1,45 @@ import streamlit as st import pandas as pd from data.scriptloader import get_sql -from data.db import get_conn +from data.db import load_data from auth_runtime import require_login from ui.sidebar import build_sidebar, hide_sidebar_if_logged_out from auth import get_fullname_for_user # hide_sidebar_if_logged_out() -st.set_page_config(page_title="Co-App Home", page_icon="🏠") +st.set_page_config(page_title="Co-App Home", page_icon="🏠", layout="wide") authenticator = require_login() st.session_state["authenticator"] = authenticator +@st.cache_data +def cache_data(): + return load_data("ora_kostenobjekte","oracle") +def render_report(): + df = cache_data() + + zgrp1_options = st.multiselect( + "Zuordnungsgruppe1", + ["SPI", "KULA", "AT"] + ) -def load_data(): - sql = get_sql("co_kostenobjekte") - print(sql) - engine = get_conn("co_dw") - with engine.connect() as conn: - df = pd.read_sql(sql, engine) - print(df) - return df + year = 2026 -st.dataframe(load_data()) + df_actual_year = df[df["jahr"] == year] + df_actual_year = df_actual_year.sort_values( + by="obj", + key=lambda s: pd.to_numeric(s, errors="coerce") + ) + if zgrp1_options: + df_view = df_actual_year[df_actual_year["zgrp1"].isin(zgrp1_options)] + else: + df_view = df_actual_year + st.dataframe(df_view, height=600, hide_index=True, width="stretch") if __name__ == "__main__": - df = load_data() - print(df) \ No newline at end of file + df = render_report() \ No newline at end of file diff --git a/app/pages/management_first_level.py b/app/pages/management_first_level.py new file mode 100644 index 0000000..64d5874 --- /dev/null +++ b/app/pages/management_first_level.py @@ -0,0 +1,255 @@ +import streamlit as st +import pandas as pd +from data.scriptloader import get_sql +from data.db import get_conn +from auth_runtime import require_login +from ui.sidebar import build_sidebar, hide_sidebar_if_logged_out +from auth import get_fullname_for_user +from sqlalchemy import text +import duckdb +import altair as alt +from tools.helpers import display_value, calc_variance_pct + +# hide_sidebar_if_logged_out() + +st.set_page_config(page_title="Co-App Home", page_icon="🏠", layout="wide") + +authenticator = require_login() +st.session_state["authenticator"] = authenticator + +DISPLAY_UNIT = "Mio. €" + +def load_data(): + sql = get_sql("ergebnis_kpi") + engine = get_conn("co_dw") + with engine.connect() as conn: + df = pd.read_sql(text(sql), con=conn, params={"jahr": 2025, "monat": 12}) + return df + + +# def calc_variance_pct(actual, plan): +# variance = actual - plan +# if plan == 0: +# return None +# if actual * plan < 0: +# return None +# return variance / abs(plan) + + + + + + + +def build_dashboard(): + + df = load_data() + # st.dataframe(df) + + be_ist_kum = df["akt_jahr"].sum() + + #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + # Kalkulation KPI Betriebsergebnis + #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + operating_result_actual_ytd = df["akt_jahr_bis_monat"].sum() + operating_result_actual_ytd_str = f"{int(operating_result_actual_ytd)/ANZ_EINHEIT:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".") + " Mio. €" + operating_result_plan_ytd = df["plan_akt_jahr_bis_monat"].sum() + operating_result_actual_ytd_py = df["vor_jahr_bis_monat"].sum() + operating_result_variance = operating_result_actual_ytd - operating_result_plan_ytd + operating_result_vaiance_pct = calc_variance_pct(operating_result_actual_ytd, operating_result_plan_ytd) + + + #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + # Dashboard - Ebene 1 Betriebsergebnis + #++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + col_1, col_2 = st.columns(2, border=True,vertical_alignment="center") + + with col_1: + st.metric(label="Betriebsergebnis", value=operating_result_actual_ytd) + + operation_result_monthly = df["akt_jahr_monat"].sum() + + be_ist_vorjahr_kum = df["vor_jahr"].sum() + be_ist_monat = df["akt_jahr_monat"].sum() + be_ist_vorjahr_monat = df["vor_jahr_monat"].sum() + + + be_ist_kum_anz = f"{int(be_ist_kum)/ANZ_EINHEIT:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".") + " Mio. €" + be_ist_monat_anz = f"{int(be_ist_monat)/ANZ_EINHEIT:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".") + " Mio. €" + be_ist_vorjahr_kum_anz = f"{int(be_ist_vorjahr_kum)/ANZ_EINHEIT:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".") + " Mio. €" + be_ist_monat_vorjahr_anz = f"{int(be_ist_vorjahr_monat)/ANZ_EINHEIT:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".") + " Mio. €" + + + #+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + # Ebene 1 - Ergebnis-KPIs + #+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + # Beispiel-Daten (ersetze das durch deine echten Monatswerte) + months = pd.date_range(end=pd.Timestamp.today().normalize(), periods=12, freq="MS") + values = pd.Series([120, 80, -30, 50, 40, 10, -20, 60, 70, 30, 20, -10], index=months) + df = pd.DataFrame({"month": months, "BE": values}).set_index("month") + + current = df["BE"].iloc[-1] + prev = df["BE"].iloc[-2] + delta = current - prev + + col1, col2, col3 = st.columns(3) + + with col1: + st.metric( + "Betriebsergebnis (intern)", + f"{current:,.0f} €", + f"{delta:,.0f} €" + ) + + with st.expander("Verlauf letzte 12 Monate", expanded=False): + # st.bar_chart(df["BE"]) + # fig, ax = plt.subplots() + # ax.plot(df.index, df["BE"], marker="o") + # ax.axhline(0, linewidth=1) # Nulllinie für negative Werte + + # ax.set_xlabel("") + # ax.set_ylabel("€") + # ax.tick_params(axis="x", rotation=45) + + # st.pyplot(fig, clear_figure=True) + df_reset = df.reset_index() + df_reset["Monat"] = df_reset["month"].dt.strftime("%Y-%m") + + chart = ( + alt.Chart(df_reset) + .mark_bar(size=28) # Balkendicke + .encode( + x=alt.X("Monat:N", sort=None, title=""), + y=alt.Y("BE:Q", title="€"), + color=alt.condition( + alt.datum.BE < 0, + alt.value("#d62728"), # rot + alt.value("#2ca02c"), # grün + ) + ) + ) + + labels = ( + alt.Chart(df_reset) + .mark_text(dy=-8) + .encode( + x="Monat:N", + y="BE:Q", + text=alt.Text("BE:Q", format=",.0f") + ) + ) + spark = ( + alt.Chart(df_reset) + .mark_line(point=True) + .encode( + x=alt.X("Monat:N", axis=None), + y=alt.Y("BE:Q", axis=None) + ) + .properties(height=60) + ) + + st.altair_chart(spark, use_container_width=True) + # st.altair_chart(chart + labels, use_container_width=True) + + + + + + + + + + + + + + + + + + + + + + # col_operating_result, col_contribution_margin = st.columns(2,border=True) + + # col_ue_n, col_ae_f, col_ab_f = st.columns(3,border=True,) + + # with col_ue_n: + # with st.expander(label="Umsatz",): + + # st.metric(label="BE-Ist kumuliert (monat)",value=f"{be_ist_kum_anz} ({be_ist_monat_anz})", delta="-5", border=True) + # st.text("Umsatz") + # with col_ae_f: + # st.text("Auftragseingang fest") + # with col_ab_f: + # st.text("Auftragsbestand fest") + + + + + # with col_operating_result: + # internal_operating_result = f"BE = {be_ist_kum_anz} Mio. €" + # st.metric(label="BE-Ist kumuliert (monat)",value=f"{internal_operating_result} ({be_ist_monat_anz})", delta="-5", border=True) + + + # with st.expander(label=st.markdown(f"# {internal_operating_result}")): + # st.text("Verlauf...") + # # st.text("ERGTAB") + + # st.metric(label="BE-Ist kumuliert (monat)",value=f"{be_ist_kum_anz} ({be_ist_monat_anz})", delta="-5", border=True) + + + + + + + + # erg = duckdb.sql(""" + # select + # 'Umsatz' as Kostenart, + # sum(case when co_koa_grp = 'CO1000' then akt_jahr else 0 end) as Aktuell, + # sum(case when co_koa_grp = 'CO1000' then plan_akt_jahr else 0 end) as Plan, + # sum(case when co_koa_grp = 'CO1000' then vor_jahr else 0 end) as Vorjahr + # from + # df + # """).fetchdf() + + # st.metric(label="BE-Ist kumuliert (monat)",value=f"{be_ist_kum_anz} ({be_ist_monat_anz})", delta="-5", border=True) + # st.dataframe(erg, hide_index=True) + + # col_erg_ist = st.columns(1) + + # with col_erg_ist: + + # st.metric(label="BE-Ist (monat)",value=be_ist_monat_anz, delta="-5", border=True) + + # col1, col2 = st.columns(2) + + # with col1: + # # st.metric(label="BE-Ist (kumuliert)",value=be_ist_kum_anz, delta="-5", border=True) + # with st.success("Ergebnis"): + # st.button(f"{be_ist_kum_anz}") + # # st.success("plus 10% zu Vorjahr") + # # with col2: + # # st.error("-10% zu Plan") + + # # with col_erg_plan: + # # st.info("minus 5%") + # # st.warning("minus 10%") + # # st.success("plus 10%") + # # st.error("minus 20%") + + # # with col_erg_vorjahr: + # # st.metric(label="BE-Vorjahr (kumuliert)",value=be_ist_vorjahr_kum_anz, delta="-5", border=True) + # # st.metric(label="BE-Vorjahr (monat)",value=be_ist_monat_anz, delta="-5", border=True) + + # # st.dataframe(load_data()) + + + + +if __name__ == "__main__": + df = build_dashboard() \ No newline at end of file diff --git a/app/tools/helpers.py b/app/tools/helpers.py new file mode 100644 index 0000000..648521a --- /dev/null +++ b/app/tools/helpers.py @@ -0,0 +1,79 @@ + +def calc_variance_pct(actual, plan): + """ + Calculates the percentage variance between actual and plan values + for reporting purposes. + + The percentage variance is only returned if it is economically + meaningful and interpretable: + - The plan value must not be zero + - Actual and plan must have the same sign (no sign change) + + In cases where a percentage variance would be misleading + (e.g. sign change from loss to profit), the function returns None + and absolute variance should be used instead. + + Parameters + ---------- + actual : float | int + Actual (realized) value. + plan : float | int + Planned or budgeted value. + + Returns + ------- + float | None + Percentage variance relative to the absolute plan value, + or None if the percentage variance is not meaningful. + """ + + variance = actual - plan + if plan == 0: + return None + if actual * plan < 0: + return None + return variance / abs(plan) + + + + +def display_value(value, unit): + """ + Formats a numeric KPI value for reporting output based on the configured + display unit (e.g. €, T€, Mio. €). + + - Scales the input value according to DISPLAY_UNIT + - Applies European number formatting (thousands separator, decimal comma) + - Returns 'n/a' if the input value is None + + Parameters + ---------- + value : float | int | None + Raw KPI value in base currency (e.g. EUR). + + Returns + ------- + str + Formatted value including display unit, ready for dashboard display. + """ + + if value is None: + return "n/a" + + unit_factors = { + "Mio. €": 1_000_000, + "T€": 1_000, + "€": 1, + } + + factor = unit_factors.get(unit, 1) + scaled = value / factor + formatted = f"{scaled:,.2f}" + formatted = ( + formatted + .replace(",", "X") + .replace(".", ",") + .replace("X", ".") + ) + + return f"{formatted} {unit}" \ No newline at end of file