Neues Dashboard: Kostenobjekte

This commit is contained in:
knedlik
2026-02-23 08:11:38 +01:00
parent 07854cc0ad
commit 3f7e405824
10 changed files with 495 additions and 14 deletions

Binary file not shown.

3
app/data/datasets.py Normal file
View File

@@ -0,0 +1,3 @@
from scriptloader import get_sql
from db import get_conn

View File

@@ -1,4 +1,7 @@
import streamlit as st
from sqlalchemy import create_engine, Text from sqlalchemy import create_engine, Text
from data.scriptloader import get_sql
import oracledb
import pandas as pd import pandas as pd
from dotenv import load_dotenv from dotenv import load_dotenv
from pathlib import Path from pathlib import Path
@@ -26,6 +29,25 @@ def get_conn(db):
logging.info(f"Datenbank {db} konnte nicht gefunden werden") logging.info(f"Datenbank {db} konnte nicht gefunden werden")
return engine 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): # def get_data(db):
# engine = get_conn(db) # engine = get_conn(db)
# with engine.connect() as conn: # with engine.connect() as conn:

View File

@@ -7,4 +7,4 @@ def get_sql(filename):
if __name__ == "__main__": if __name__ == "__main__":
print(get_sql("sales_umsatz")) print(get_sql("ergebnis_kpi"))

View File

@@ -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

View File

@@ -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

View File

View File

@@ -1,34 +1,45 @@
import streamlit as st import streamlit as st
import pandas as pd import pandas as pd
from data.scriptloader import get_sql 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 auth_runtime import require_login
from ui.sidebar import build_sidebar, hide_sidebar_if_logged_out from ui.sidebar import build_sidebar, hide_sidebar_if_logged_out
from auth import get_fullname_for_user from auth import get_fullname_for_user
# hide_sidebar_if_logged_out() # 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() authenticator = require_login()
st.session_state["authenticator"] = authenticator 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(): year = 2026
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
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__": if __name__ == "__main__":
df = load_data() df = render_report()
print(df)

View File

@@ -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()

79
app/tools/helpers.py Normal file
View File

@@ -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}"