Compare commits
3 Commits
07854cc0ad
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
67170b8130 | ||
|
|
20cd0b8547 | ||
|
|
3f7e405824 |
Binary file not shown.
3
app/data/datasets.py
Normal file
3
app/data/datasets.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from scriptloader import get_sql
|
||||||
|
from db import get_conn
|
||||||
|
|
||||||
@@ -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:
|
||||||
|
|||||||
0
app/data/parquet_store.py
Normal file
0
app/data/parquet_store.py
Normal 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"))
|
||||||
|
|||||||
92
app/data/sql/ergebnis_kpi.sql
Normal file
92
app/data/sql/ergebnis_kpi.sql
Normal 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
|
||||||
22
app/data/sql/ora_kostenobjekte.sql
Normal file
22
app/data/sql/ora_kostenobjekte.sql
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
/* Orlacle (PENTA) Kostenobjekte */
|
||||||
|
|
||||||
|
select
|
||||||
|
GESCHAEFTSJAHR as jahr,
|
||||||
|
OBJEKTTYP as typ,
|
||||||
|
KOSTENOBJEKT as obj,
|
||||||
|
BEZEICHNUNG,
|
||||||
|
VERANTWORTLICHER,
|
||||||
|
FELD_2_X30 as vorgesetzter,
|
||||||
|
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,
|
||||||
|
sysdate
|
||||||
|
from
|
||||||
|
pkos
|
||||||
|
where
|
||||||
|
objekttyp in ('01', '02')
|
||||||
0
app/kpi/operating_result.py
Normal file
0
app/kpi/operating_result.py
Normal file
@@ -1,34 +1,186 @@
|
|||||||
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
|
||||||
|
import numpy as np
|
||||||
|
from tools.excel_export import df_to_excel_bytes
|
||||||
|
from tools.help_text import get_help
|
||||||
|
|
||||||
# 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.markdown("""
|
||||||
|
<style>
|
||||||
|
.block-container {
|
||||||
|
padding-top: 0.2rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
""", unsafe_allow_html=True)
|
||||||
|
|
||||||
|
description = get_help("costobjects")
|
||||||
|
with st.expander(label="ℹ️ Hilfe / Hinweise", expanded=False): # ❓
|
||||||
|
st.markdown(description)
|
||||||
|
|
||||||
def load_data():
|
@st.cache_data
|
||||||
sql = get_sql("co_kostenobjekte")
|
def cache_data() -> pd.DataFrame:
|
||||||
print(sql)
|
"""
|
||||||
engine = get_conn("co_dw")
|
Load and cache the base dataset for this page.
|
||||||
with engine.connect() as conn:
|
|
||||||
df = pd.read_sql(sql, engine)
|
Streamlit reruns the script on every interaction; caching avoids
|
||||||
print(df)
|
repeated I/O and makes filtering feel instant.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
df = load_data("ora_kostenobjekte", "oracle")
|
||||||
return df
|
return df
|
||||||
|
except:
|
||||||
|
st.warning("Fehler beim Laden der Daten", icon="⚠️")
|
||||||
|
|
||||||
st.dataframe(load_data())
|
|
||||||
|
|
||||||
|
|
||||||
|
def sidebar_filters(df) -> dict:
|
||||||
|
"""
|
||||||
|
Render the sidebar UI and return the current filter selections.
|
||||||
|
|
||||||
|
This function should contain UI concerns only (widgets, layout),
|
||||||
|
and not data filtering logic, to keep the code maintainable.
|
||||||
|
"""
|
||||||
|
|
||||||
|
st.markdown("""
|
||||||
|
<style>
|
||||||
|
section[data-testid="stSidebar"] .block-container {
|
||||||
|
padding-top: 0.5rem;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
section[data-testid="stSidebar"] div[data-testid="stVerticalBlock"] > div {
|
||||||
|
gap: 0.2rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
""", unsafe_allow_html=True)
|
||||||
|
|
||||||
|
st.sidebar.header("Filter")
|
||||||
|
|
||||||
|
if st.sidebar.button("Refresh (Global)"):
|
||||||
|
cache_data.clear()
|
||||||
|
st.rerun()
|
||||||
|
|
||||||
|
filter_text = st.sidebar.text_input(label="Textsuche", placeholder="Suche Objekt, Text, Verantwortlicher")
|
||||||
|
col_s1, col_s2 = st.sidebar.columns(2)
|
||||||
|
with col_s1:
|
||||||
|
year = st.selectbox(label="Jahr", options=(2025, 2026), index=1)
|
||||||
|
with col_s2:
|
||||||
|
typ = st.selectbox(label="Typ", options=sorted(df["typ"].dropna().unique()), index=1)
|
||||||
|
obj = st.sidebar.multiselect("obj", sorted(df["obj"].dropna().unique()))
|
||||||
|
zgrp1 = st.sidebar.multiselect("ZGrp1", sorted(df["zgrp1"].dropna().unique()))
|
||||||
|
zgrp2 = st.sidebar.multiselect("ZGrp2", sorted(df["zgrp2"].dropna().unique()))
|
||||||
|
zgrp3 = st.sidebar.multiselect("ZGrp3", sorted(df["zgrp3"].dropna().unique()))
|
||||||
|
|
||||||
|
return {"year": year, "filter_text": filter_text, "typ": typ, "obj": obj, "zgrp1": zgrp1, "zgrp2": zgrp2, "zgrp3": zgrp3}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def build_mask(df: pd.DataFrame, sidebar_filter: dict) -> np.ndarray:
|
||||||
|
"""
|
||||||
|
Build a boolean mask based on filter selections.
|
||||||
|
|
||||||
|
The mask approach keeps the logic readable and makes it easy
|
||||||
|
to add more conditions later.
|
||||||
|
"""
|
||||||
|
|
||||||
|
mask = np.ones(len(df), dtype=bool)
|
||||||
|
|
||||||
|
filter_text = (sidebar_filter.get("filter_text") or "").strip()
|
||||||
|
if filter_text:
|
||||||
|
m1 = df["bezeichnung"].astype("string").str.contains(filter_text, case=False, na=False, regex=False)
|
||||||
|
m2 = df["verantwortlicher"].astype("string").str.contains(filter_text, case=False, na=False, regex=False)
|
||||||
|
m3 = df["vorgesetzter"].astype("string").str.contains(filter_text, case=False, na=False, regex=False)
|
||||||
|
mask &= (m1 | m2 | m3)
|
||||||
|
|
||||||
|
if sidebar_filter["year"]:
|
||||||
|
mask &= df["jahr"].eq(sidebar_filter["year"])
|
||||||
|
if sidebar_filter["typ"]:
|
||||||
|
mask &= df["typ"].eq(sidebar_filter["typ"])
|
||||||
|
if sidebar_filter["obj"]:
|
||||||
|
mask &= df["obj"].isin(sidebar_filter["obj"])
|
||||||
|
if sidebar_filter["zgrp1"]:
|
||||||
|
mask &= df["zgrp1"].isin(sidebar_filter["zgrp1"])
|
||||||
|
if sidebar_filter["zgrp2"]:
|
||||||
|
mask &= df["zgrp2"].isin(sidebar_filter["zgrp2"])
|
||||||
|
if sidebar_filter["zgrp3"]:
|
||||||
|
mask &= df["zgrp3"].isin(sidebar_filter["zgrp3"])
|
||||||
|
return mask
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def render_table(df):
|
||||||
|
"""
|
||||||
|
Render the result table.
|
||||||
|
|
||||||
|
Keep this function presentation-only: it should not modify data.
|
||||||
|
"""
|
||||||
|
st.markdown("### Übersicht Kostenobjekte")
|
||||||
|
st.dataframe(df, hide_index=True, width="stretch", height="stretch")
|
||||||
|
|
||||||
|
|
||||||
|
def download_data(df):
|
||||||
|
df.to_excel(path)
|
||||||
|
|
||||||
|
|
||||||
|
def page():
|
||||||
|
"""
|
||||||
|
Page entry point: orchestrates data loading, UI, filtering, and rendering.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# -----------------------------
|
||||||
|
# Data loading (cached)
|
||||||
|
# -----------------------------
|
||||||
|
df = cache_data()
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------
|
||||||
|
# UI (Sidebar)
|
||||||
|
# -----------------------------
|
||||||
|
sidebar_filter = sidebar_filters(df)
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------
|
||||||
|
# Business logic (filtering)
|
||||||
|
# -----------------------------
|
||||||
|
mask = build_mask(df, sidebar_filter)
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------
|
||||||
|
# Presentation
|
||||||
|
# -----------------------------
|
||||||
|
df_clean = df[df.columns[:-1]] # letzte Spalten entfernen (sysdate)
|
||||||
|
df_display = df_clean.loc[mask]
|
||||||
|
df_display = df_display.sort_values(by="obj", key=lambda s: pd.to_numeric(s, errors="coerce"))
|
||||||
|
render_table(df_display)
|
||||||
|
|
||||||
|
col1, col2 = st.columns(2)
|
||||||
|
data_as_of = df["sysdate"].max()
|
||||||
|
row_count = len(df_display)
|
||||||
|
|
||||||
|
with col1:
|
||||||
|
st.text(f"Datenstand: {data_as_of}")
|
||||||
|
with col2:
|
||||||
|
st.markdown(f"<div style='text-align: right;'>Anzahl Zeilen: {row_count}</div>", unsafe_allow_html=True)
|
||||||
|
|
||||||
|
st.download_button(
|
||||||
|
"📊 Excel-Datei herunterladen",
|
||||||
|
data=df_to_excel_bytes(df_display),
|
||||||
|
file_name="kostenobjekte",
|
||||||
|
mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
df = load_data()
|
df = page()
|
||||||
print(df)
|
|
||||||
255
app/pages/management_first_level.py
Normal file
255
app/pages/management_first_level.py
Normal 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()
|
||||||
16
app/tools/excel_export.py
Normal file
16
app/tools/excel_export.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import pandas as pd
|
||||||
|
import io
|
||||||
|
|
||||||
|
def df_to_excel_bytes(df):
|
||||||
|
|
||||||
|
"""
|
||||||
|
Convert a DataFrame to an in-memory XLSX file.
|
||||||
|
|
||||||
|
Returns a BytesIO object suitable for st.download_button(data=...).
|
||||||
|
"""
|
||||||
|
|
||||||
|
buffer = io.BytesIO()
|
||||||
|
with pd.ExcelWriter(buffer, engine="openpyxl") as writer:
|
||||||
|
df.to_excel(writer, index=False, sheet_name="Daten")
|
||||||
|
buffer.seek(0)
|
||||||
|
return buffer
|
||||||
5
app/tools/help_text.py
Normal file
5
app/tools/help_text.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
from app_db.app_db import get_value
|
||||||
|
|
||||||
|
def get_help(dashboard :str):
|
||||||
|
helptext = get_value(f"select dash_description from dashboards where dash_name = '{dashboard}.py'")
|
||||||
|
return helptext
|
||||||
79
app/tools/helpers.py
Normal file
79
app/tools/helpers.py
Normal 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}"
|
||||||
11
config/settings.py
Normal file
11
config/settings.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
BASE_DIR = Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
|
STORAGE_DIR = BASE_DIR / "storage"
|
||||||
|
DB_DIR = STORAGE_DIR / "app_db"
|
||||||
|
DB_PATH = DB_DIR / "app.db"
|
||||||
|
DWH_DIR = STORAGE_DIR / "dwh"
|
||||||
|
|
||||||
|
|
||||||
|
print(BASE_DIR, DB_DIR)
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
begin;
|
||||||
|
|
||||||
|
alter table dashboards
|
||||||
|
add column dash_name text not null default "";
|
||||||
|
|
||||||
|
INSERT INTO schema_version (version) VALUES ('20260226_095800_add_col_dashname_dashboards');
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
Reference in New Issue
Block a user