FU10: localize energy time math to server tz; accrue fixed/credit per elapsed local day
- Store UTC, compute day boundaries in server local timezone (new app/services/timezone.py). - summarize: fixed fee / tax credit accrue only for elapsed whole local days, integrated across contract versions — no future-day inflation, no version-switch collapse. - MQTT import_cost_total / export_revenue_total getters delegate to summarize (no inline apportionment), anchored at the active contract's earliest version effective_from so the total sensors stay monotonic and never jump backward. - effective_from: naive datetimes interpreted as server-local wall-clock, then stored UTC. - frontend ContractForm sends local-midnight naive datetime (was UTC midnight). - get_costs_summary default window uses server-local "today".
This commit is contained in:
+42
-47
@@ -549,43 +549,43 @@ def _energy_cost_provider(session: Session) -> list[ExposableEntity]:
|
||||
def _make_import_cost_getter() -> Callable[["Session"], Any]:
|
||||
"""Return a getter for the cumulative import cost plus prorated standing charges.
|
||||
|
||||
Value = SUM(import_cost, non-degraded) + standing_charges_cumulative
|
||||
Principle B/D: delegates entirely to ``summarize(sess, anchor_utc, now_utc)``,
|
||||
where ``anchor_utc`` is the effective_from of the earliest version of the active
|
||||
contract (i.e. the contract billing start). This makes the cumulative total
|
||||
monotonically increasing and cross-version consistent:
|
||||
|
||||
where standing_charges_cumulative = elapsed_days × (network_fee + management_fee) / 30.
|
||||
Elapsed days are counted from the active contract version's effective_from date
|
||||
(inclusive of today). Returns None when no non-degraded periods exist.
|
||||
value = summarize(anchor → now).metered_import + summarize(anchor → now).fixed_costs
|
||||
|
||||
Returns None when no non-degraded period exists in [anchor, now].
|
||||
No arithmetic is done here — all fixed-cost/credit accounting lives in summarize().
|
||||
"""
|
||||
def _getter(sess: "Session") -> Any:
|
||||
from decimal import Decimal
|
||||
from datetime import UTC, datetime as _dt
|
||||
|
||||
from app.services.contracts import active_contract_versions
|
||||
from app.services.contracts import _as_utc as _cu
|
||||
from app.services.energy_cost import summarize as _summarize
|
||||
from app.models.energy import EnergyCostPeriod as _ECP
|
||||
from app.services.contracts import active_contract_version_at, _as_utc
|
||||
from sqlalchemy import func as _func
|
||||
|
||||
total = sess.query(_func.sum(_ECP.import_cost)).filter(
|
||||
# Quick check: any non-degraded period at all? (avoids full summarize overhead
|
||||
# when there are zero periods, which should return None)
|
||||
has_data = sess.query(_func.sum(_ECP.import_cost)).filter(
|
||||
_ECP.degraded.is_(False)
|
||||
).scalar()
|
||||
if total is None:
|
||||
if has_data is None:
|
||||
return None
|
||||
|
||||
# Add prorated standing charges from the active contract version.
|
||||
standing_cumulative = Decimal("0")
|
||||
versions = active_contract_versions(sess)
|
||||
if not versions:
|
||||
# No active contract — return pure SUM(import_cost) without standing charges.
|
||||
return float(has_data)
|
||||
|
||||
# anchor = earliest version's effective_from (contract billing start).
|
||||
anchor_utc = _cu(versions[0].effective_from)
|
||||
now_utc = _dt.now(UTC)
|
||||
version = active_contract_version_at(sess, now_utc)
|
||||
if version is not None:
|
||||
vals = version.values or {}
|
||||
standing = vals.get("standing", {})
|
||||
network_fee = Decimal(str(standing.get("network_fee") or 0))
|
||||
management_fee = Decimal(str(standing.get("management_fee") or 0))
|
||||
daily_standing = (network_fee + management_fee) / Decimal("30")
|
||||
|
||||
effective_from = _as_utc(version.effective_from)
|
||||
days = (now_utc.date() - effective_from.date()).days + 1
|
||||
days = max(days, 0)
|
||||
standing_cumulative = daily_standing * days
|
||||
|
||||
return float(Decimal(str(total)) + standing_cumulative)
|
||||
result = _summarize(sess, anchor_utc, now_utc)
|
||||
return result["metered_import"] + result["fixed_costs"]
|
||||
|
||||
return _getter
|
||||
|
||||
@@ -594,42 +594,37 @@ def _energy_cost_provider(session: Session) -> list[ExposableEntity]:
|
||||
def _make_export_revenue_getter() -> Callable[["Session"], Any]:
|
||||
"""Return a getter for the cumulative export revenue plus prorated tax credit.
|
||||
|
||||
Value = SUM(export_revenue, non-degraded) + heffingskorting_cumulative
|
||||
Principle B/D: delegates entirely to ``summarize(sess, anchor_utc, now_utc)``.
|
||||
|
||||
where heffingskorting_cumulative = elapsed_days × heffingskorting / 365.
|
||||
Elapsed days are counted from the active contract version's effective_from date
|
||||
(inclusive of today). Returns None when no non-degraded periods exist.
|
||||
value = summarize(anchor → now).metered_export + summarize(anchor → now).credits
|
||||
|
||||
Returns None when no non-degraded period exists in [anchor, now].
|
||||
"""
|
||||
def _getter(sess: "Session") -> Any:
|
||||
from decimal import Decimal
|
||||
from datetime import UTC, datetime as _dt
|
||||
|
||||
from app.services.contracts import active_contract_versions
|
||||
from app.services.contracts import _as_utc as _cu
|
||||
from app.services.energy_cost import summarize as _summarize
|
||||
from app.models.energy import EnergyCostPeriod as _ECP
|
||||
from app.services.contracts import active_contract_version_at, _as_utc
|
||||
from sqlalchemy import func as _func
|
||||
|
||||
total = sess.query(_func.sum(_ECP.export_revenue)).filter(
|
||||
# Quick check: any non-degraded period at all?
|
||||
has_data = sess.query(_func.sum(_ECP.export_revenue)).filter(
|
||||
_ECP.degraded.is_(False)
|
||||
).scalar()
|
||||
if total is None:
|
||||
if has_data is None:
|
||||
return None
|
||||
|
||||
# Add prorated heffingskorting credit from the active contract version.
|
||||
credit_cumulative = Decimal("0")
|
||||
versions = active_contract_versions(sess)
|
||||
if not versions:
|
||||
# No active contract — return pure SUM(export_revenue) without credits.
|
||||
return float(has_data)
|
||||
|
||||
anchor_utc = _cu(versions[0].effective_from)
|
||||
now_utc = _dt.now(UTC)
|
||||
version = active_contract_version_at(sess, now_utc)
|
||||
if version is not None:
|
||||
vals = version.values or {}
|
||||
credits = vals.get("credits", {})
|
||||
heffingskorting = Decimal(str(credits.get("heffingskorting") or 0))
|
||||
daily_credit = heffingskorting / Decimal("365")
|
||||
|
||||
effective_from = _as_utc(version.effective_from)
|
||||
days = (now_utc.date() - effective_from.date()).days + 1
|
||||
days = max(days, 0)
|
||||
credit_cumulative = daily_credit * days
|
||||
|
||||
return float(Decimal(str(total)) + credit_cumulative)
|
||||
result = _summarize(sess, anchor_utc, now_utc)
|
||||
return result["metered_export"] + result["credits"]
|
||||
|
||||
return _getter
|
||||
|
||||
|
||||
Reference in New Issue
Block a user