-- ============================================================= -- R__011_fn_effective_price.sql -- EMS Platform – funkce pro výpočet efektivní ceny per site -- Repeatable migration – nasazuje se při každé změně -- ============================================================= -- Pomocné (audit/ekonomika): sjednocení import/export Wh — musí běžet před R__012/R__056. create or replace function ems.fn_audit_grid_import_wh_for_economics( p_import_wh numeric, p_export_wh numeric, p_grid_power_w int ) returns numeric language sql stable as $$ select case when coalesce(p_import_wh, 0) > 0 and coalesce(p_export_wh, 0) > 0 then coalesce( p_import_wh, greatest(coalesce(p_grid_power_w, 0), 0)::numeric / 4 ) when coalesce(p_export_wh, 0) = 0 then greatest( coalesce( p_import_wh, greatest(coalesce(p_grid_power_w, 0), 0)::numeric / 4 ), greatest(coalesce(p_grid_power_w, 0), 0)::numeric / 4 ) else coalesce( p_import_wh, greatest(coalesce(p_grid_power_w, 0), 0)::numeric / 4 ) end; $$; create or replace function ems.fn_audit_grid_export_wh_for_economics( p_import_wh numeric, p_export_wh numeric, p_grid_power_w int ) returns numeric language sql stable as $$ select case when coalesce(p_import_wh, 0) > 0 and coalesce(p_export_wh, 0) > 0 then coalesce( p_export_wh, abs(least(coalesce(p_grid_power_w, 0), 0))::numeric / 4 ) when coalesce(p_import_wh, 0) = 0 then greatest( coalesce( p_export_wh, abs(least(coalesce(p_grid_power_w, 0), 0))::numeric / 4 ), abs(least(coalesce(p_grid_power_w, 0), 0))::numeric / 4 ) else coalesce( p_export_wh, abs(least(coalesce(p_grid_power_w, 0), 0))::numeric / 4 ) end; $$; comment on function ems.fn_audit_grid_import_wh_for_economics(numeric, numeric, int) is 'Import Wh pro audit/ekonomiku: u čistého importu max(uložený čítač, max(0,P_grid)×¼ h).'; comment on function ems.fn_audit_grid_export_wh_for_economics(numeric, numeric, int) is 'Export Wh pro audit/ekonomiku: u čistého exportu max(uložený čítač, |min(0,P_grid)|×¼ h).'; CREATE OR REPLACE FUNCTION ems.fn_effective_buy_price( p_site_id INT, p_interval_start TIMESTAMPTZ ) RETURNS NUMERIC(10,6) LANGUAGE plpgsql STABLE AS $$ DECLARE v_spot_price NUMERIC; v_energy_czk NUMERIC; v_dist_rate NUMERIC; v_system_services NUMERIC; v_ote_fee NUMERIC; v_vat_rate NUMERIC; v_buy_margin_fixed NUMERIC; v_buy_margin_pct NUMERIC; v_buy_margin NUMERIC; v_is_vt BOOLEAN; v_local_time TIME; v_dow INT; v_hdo_code_id INT; v_tariff_id INT; v_rate_type TEXT; v_purchase_mode TEXT; v_fixed_nt NUMERIC; v_fixed_vt_sur NUMERIC; BEGIN SELECT smc.purchase_pricing_mode, smc.buy_fixed_energy_nt_czk_kwh, smc.buy_fixed_vt_surcharge_czk_kwh, smc.buy_margin_fixed_czk, smc.buy_margin_percent, smc.system_services_czk_kwh, smc.ote_fee_czk_kwh, smc.hdo_code_id, smc.tariff_id, dt.vat_rate INTO v_purchase_mode, v_fixed_nt, v_fixed_vt_sur, v_buy_margin_fixed, v_buy_margin_pct, v_system_services, v_ote_fee, v_hdo_code_id, v_tariff_id, v_vat_rate FROM ems.site_market_config smc LEFT JOIN ems.distribution_tariff dt ON dt.id = smc.tariff_id WHERE smc.site_id = p_site_id AND smc.valid_from <= p_interval_start AND (smc.valid_to IS NULL OR smc.valid_to > p_interval_start) ORDER BY smc.valid_from DESC LIMIT 1; IF NOT FOUND THEN RETURN NULL; END IF; SELECT buy_raw_price_czk_kwh INTO v_spot_price FROM ems.market_interval_price WHERE market_source IN ('OTE_CZ', 'OTE_CZ_DAM') AND interval_start = p_interval_start LIMIT 1; v_local_time := (p_interval_start AT TIME ZONE 'Europe/Prague')::TIME; v_dow := EXTRACT(DOW FROM p_interval_start AT TIME ZONE 'Europe/Prague'); -- 0=neděle, 6=sobota IF v_hdo_code_id IS NOT NULL THEN SELECT EXISTS ( SELECT 1 FROM ems.hdo_code_window w WHERE w.hdo_code_id = v_hdo_code_id AND ( w.day_type = 'all' OR (w.day_type = 'workday' AND v_dow BETWEEN 1 AND 5) OR (w.day_type = 'weekend' AND v_dow IN (0, 6)) ) AND w.rate_type = 'VT' AND v_local_time >= w.window_from AND v_local_time < w.window_to ) INTO v_is_vt; ELSE v_is_vt := false; END IF; v_rate_type := CASE WHEN v_is_vt THEN 'VT' ELSE 'NT' END; IF v_tariff_id IS NOT NULL THEN SELECT price_czk_kwh INTO v_dist_rate FROM ems.distribution_tariff_rate WHERE tariff_id = v_tariff_id AND rate_type = v_rate_type AND valid_from <= p_interval_start::DATE AND (valid_to IS NULL OR valid_to > p_interval_start::DATE) ORDER BY valid_from DESC LIMIT 1; END IF; v_dist_rate := COALESCE(v_dist_rate, 0); v_system_services := COALESCE(v_system_services, 0); v_ote_fee := COALESCE(v_ote_fee, 0); v_buy_margin_fixed := COALESCE(v_buy_margin_fixed, 0); v_buy_margin_pct := COALESCE(v_buy_margin_pct, 0); v_vat_rate := COALESCE(v_vat_rate, 0.21); v_fixed_vt_sur := COALESCE(v_fixed_vt_sur, 0); IF upper(trim(COALESCE(v_purchase_mode, ''))) = 'FIXED' AND v_fixed_nt IS NOT NULL THEN v_energy_czk := v_fixed_nt + CASE WHEN v_is_vt THEN v_fixed_vt_sur ELSE 0 END; v_buy_margin := v_buy_margin_fixed + (v_energy_czk * v_buy_margin_pct / 100.0); ELSIF v_spot_price IS NULL THEN RETURN NULL; ELSE -- Spot: asymetrický faktor na raw OTE (stejné p jako u kladného ×(1+p/100)): -- kladná raw → ×(1+p/100), záporná raw → ×(1−p/100); fixní marže jen přičíst. v_energy_czk := CASE WHEN v_spot_price >= 0 THEN v_spot_price * (1 + v_buy_margin_pct / 100.0) ELSE v_spot_price * (1 - v_buy_margin_pct / 100.0) END; v_buy_margin := v_buy_margin_fixed; END IF; RETURN ROUND( (v_energy_czk + v_dist_rate + v_system_services + v_ote_fee + v_buy_margin) * (1 + v_vat_rate), 6 ); END; $$; COMMENT ON FUNCTION ems.fn_effective_buy_price(INT, TIMESTAMPTZ) IS 'Efektivní nákupní cena elektřiny Kč/kWh včetně DPH. Režim spot: složka OTE buy_raw jako kladná → ×(1+buy_margin_percent/100), záporná → ×(1−buy_margin_percent/100); + buy_margin_fixed_czk + distribuce NT/VT (HDO) + systémové služby + OTE poplatek; pak DPH na celek. Režim fixed: energie = buy_fixed_energy_nt_czk_kwh (+ příplatek VT dle HDO), marže = fix + procento z této uzavřené energické složky (symetricky jako dříve); + příplatky a DPH stejně. DPH aplikováno na celou částku.'; -- ------------------------------------------------------------ CREATE OR REPLACE FUNCTION ems.fn_effective_sell_price( p_site_id INT, p_interval_start TIMESTAMPTZ ) RETURNS NUMERIC(10,6) LANGUAGE plpgsql STABLE AS $$ DECLARE v_spot_price NUMERIC; v_sell_margin_fixed NUMERIC; v_sell_margin_pct NUMERIC; BEGIN SELECT sell_margin_fixed_czk, sell_margin_percent INTO v_sell_margin_fixed, v_sell_margin_pct FROM ems.site_market_config WHERE site_id = p_site_id AND valid_from <= p_interval_start AND (valid_to IS NULL OR valid_to > p_interval_start) ORDER BY valid_from DESC LIMIT 1; IF NOT FOUND THEN RETURN NULL; END IF; SELECT sell_raw_price_czk_kwh INTO v_spot_price FROM ems.market_interval_price WHERE market_source IN ('OTE_CZ', 'OTE_CZ_DAM') AND interval_start = p_interval_start LIMIT 1; IF v_spot_price IS NULL THEN RETURN NULL; END IF; RETURN ROUND( v_spot_price + COALESCE(v_sell_margin_fixed, 0) + (v_spot_price * COALESCE(v_sell_margin_pct, 0) / 100.0), 6 ); END; $$; COMMENT ON FUNCTION ems.fn_effective_sell_price(INT, TIMESTAMPTZ) IS 'Efektivní prodejní cena elektřiny Kč/kWh bez DPH (neplátce DPH). Složky: spot OTE + fixní/procentní prodejní marže (záporná = srážka). Zelený bonus není součástí ceny – počítá se z výroby přes fn_green_bonus_revenue(). Záporná hodnota = platíme za export (záporné spotové ceny).'; -- ------------------------------------------------------------ CREATE OR REPLACE FUNCTION ems.fn_green_bonus_revenue( p_pv_array_id INT, p_interval_start TIMESTAMPTZ, p_production_wh NUMERIC ) RETURNS NUMERIC LANGUAGE plpgsql STABLE AS $$ DECLARE v_bonus_rate NUMERIC; BEGIN SELECT green_bonus_czk_kwh INTO v_bonus_rate FROM ems.asset_pv_array WHERE id = p_pv_array_id AND green_bonus_czk_kwh IS NOT NULL AND green_bonus_valid_from <= p_interval_start::DATE AND (green_bonus_valid_to IS NULL OR green_bonus_valid_to > p_interval_start::DATE); IF v_bonus_rate IS NULL OR p_production_wh IS NULL OR p_production_wh <= 0 THEN RETURN 0; END IF; RETURN ROUND((p_production_wh / 1000.0) * v_bonus_rate, 6); END; $$; COMMENT ON FUNCTION ems.fn_green_bonus_revenue(INT, TIMESTAMPTZ, NUMERIC) IS 'Příjem ze zeleného bonusu za výrobu FVE pole v daném intervalu. Bonus plyne z celkové výroby bez ohledu na to kam energie šla (interní spotřeba, baterie, EV, TČ i export do sítě). Sazba se načítá dle platnosti (valid_from/valid_to) – ročně aktualizovatelné. Vrátí 0 pokud pole nemá zelený bonus nebo výroba je nulová. Použití: SELECT ems.fn_green_bonus_revenue(pv_array_id, interval_start, production_wh);';