From 9285169bb44a014a50c7479979043c833d61485e Mon Sep 17 00:00:00 2001 From: MrWaradana Date: Thu, 20 Nov 2025 17:14:06 +0700 Subject: [PATCH] fix and add plant, formula and master data minor --- .../__pycache__/service.cpython-311.pyc | Bin 12348 -> 12205 bytes src/masterdata/service.py | 2 +- src/modules/equipment/formula.py | 193 ++++++++++++++++++ .../__pycache__/service.cpython-311.pyc | Bin 8709 -> 8934 bytes src/plant_transaction_data/service.py | 14 +- 5 files changed, 201 insertions(+), 8 deletions(-) create mode 100644 src/modules/equipment/formula.py diff --git a/src/masterdata/__pycache__/service.cpython-311.pyc b/src/masterdata/__pycache__/service.cpython-311.pyc index 5860f01443e09d60ef9d7abd53355f54ea0bb4ab..047d6146f3c80ebe7dacb36732feb346790f57f7 100644 GIT binary patch delta 991 zcma)*TSyd97{@*CID2#0i|s0I&g{kA+(j32O+gn)(^Av0(zY9}gEP^_?z){Blgdc( zr3iZY3VMkidXS)?+FpVPdhRKb9wwsKuzK*J3*G2^vk*N9HN*V)zWFYk^Zn0xGIK90 ztO|moiakGHi@9wNgo`{`gPnFCOmmI43*2Hg?ONnu!#4nmHAS3o!z*= z$zU5ZBJ9(a8Q!sXsp^xhN~pWu^P*NCqsk*G| z)Ue3Kv(%5#4u-fu8_-T6k_-l?$hu7X(H}vWEeFvWLhNQRETw{Oa0~M6EFFa}-UvBT z4%A(9k~G{GA8o}M&@6*i%@7P$)+;mnoCF&oAFPWGxFzi&T@aGCl)p>oNn0=0;QmYv zjGBd$Dpjf3NyNVrBFGT8vQgd+4a22K047{^aD|`pW?nD6495h+si@j4&6V`rToHbS z{Y~b0Tv)%!R^YU|_a9^a5Nd>v9y{EPbT{n9HWh);@*wsx{AZg&^Q3PAYyOf5-GlI| zIZ71x(VX;}YSrX)wV=^S3??8Ql~U$_rqMrzm_ne61}~RNYEh}E8K|gv6r;!BYP2Ve znpV_3gcj;*IPoB4Pvq3RqR|mpkG8aJG8&q}g2RX$Vw6D(Fw8)I%nwtsda_(D#CGs* z1;b4UQ>zYm*qR_Ko= delta 1009 zcmaizO-vI(7={^Iwv={Di`jxef7o`V1xmmM2}DXTMKIEW5k&zlOS_P2|1!I1@B#^P zFwuDUG$zKsoAJN_t2Yi_3};9al06WQCSJUl7%ws8!gtOIJ!TuUw%A~YGecXG00n_39N3+1;7noSfI-tt zyO|o8othZ*+B{7uG_kmh2z$WN8Q<+Fp_nbku+2hnR>48QvM_Fo<@E zjKGM)4NLX`G7fL-9&Qp9Q}EfIaQzX}i|nO}|LOIvQvnrdyUmSaISKC_fwl8ka)@cf z6$ZWjuAEFJ^0`ET=5HxUEgr0)VVp3jiY99sWv$7rQYRYv7=k=)!Ww^!_A=_~I2B%MLf%JP0pC(8StkZ5t<1_$4+&zHOH)t0Hh#JXJ$G znFR8pv$@=mdUPG%DZCaV!aq9-u{IG7eP(d^`h_@pD;Z$rKwSE-cUriN!oS;3ufmvL zBFm8VcQ=&VUX#|8tV(B48-gvrCmg|2Mr0A!5EzHf$%TTFOI2cNsl;*vXK%uhe^A75 zDluKh3IozDm}p`ttz=Rvoq#zh5IWuj)XpO25E(>_L2Y4JfNiM}b|oj-C>~0EoP8Om zU5IiPec list[MasterData]: """Returns all documents.""" - query = Select(MasterData).order_by(MasterData.description.asc()) + query = Select(MasterData) if search: query = query.filter(MasterData.name.ilike(f"%{search}%")) diff --git a/src/modules/equipment/formula.py b/src/modules/equipment/formula.py new file mode 100644 index 0000000..849d964 --- /dev/null +++ b/src/modules/equipment/formula.py @@ -0,0 +1,193 @@ +""" +Collected formulas and helpers extracted from module algorithms. + +This file consolidates the core mathematical/financial formulas used across: +- `Eac.py` (NPV, PMT, discounted projections, EAC calculation) +- `insert_actual_data.py` (aggregation formulas, man-hour conversion) +- `Prediksi.py` (future value / fv wrappers) + +Keep these functions pure and well-documented to make debugging and +comparisons easier. +""" +from typing import Iterable, List, Sequence +import math +import numpy as np +try: + import numpy_financial as npf +except Exception: # numpy_financial may not be installed in all envs + npf = None + + +def npv_from_cash_flows(cash_flows: Sequence[float], rate: float, offset: int = 0) -> float: + """Compute discounted sum (NPV) of `cash_flows` at `rate` with optional time offset. + + Formula used in `Eac.py`: + NPV = Σ [Ct / (1 + r)^(offset + t)] + where t is 1-based index of flows in `cash_flows`. + + Args: + cash_flows: sequence of cash flows (Ct) for consecutive periods. + rate: periodic discount/inflation rate (e.g., 0.05 for 5%). + offset: integer number of periods to offset (e.g., last_seq from actuals). + + Returns: + Discounted present value as float. + """ + if not cash_flows: + return 0.0 + total = 0.0 + for i, value in enumerate(cash_flows): + exp = offset + i + 1 + total += float(value) / ((1.0 + rate) ** exp) + return float(total) + + +def pmt_formula(rate: float, nper: int, pv: float) -> float: + """Compute level payment PMT for given rate, periods and present value. + + Uses the standard annuity payment formula (same as numpy_financial.pmt but + returned value uses same sign-convention as mathematical formula): + + PMT = PV * [r (1+r)^n] / [(1+r)^n - 1] + + Special-case when rate == 0 (equal division): PMT = PV / nper + + Note: many code places use sign flips (e.g. `-npf.pmt(...)`) because of + numpy_financial's cashflow sign policy. Use whichever sign convention your + caller expects. + + Args: + rate: periodic rate (decimal) + nper: number of periods (int) + pv: present value to amortize + + Returns: + payment per period (float) + """ + if nper == 0: + raise ValueError("nper must be > 0") + if abs(rate) < 1e-12: + return float(pv) / float(nper) + r = float(rate) + n = int(nper) + numerator = pv * r * ((1 + r) ** n) + denominator = ((1 + r) ** n) - 1 + return float(numerator / denominator) + + +def pmt(rate: float, nper: int, pv: float) -> float: + """Wrapper to compute PMT using `numpy_financial` when available, else fall back to formula. + + Returns the value that `numpy_financial.pmt` would return (i.e. typically + negative for positive PV by that library's sign convention). The caller + can negate the result to get a positive payment if desired (as seen in + `Eac.py` where they do `-npf.pmt(...)`). + """ + if npf is not None: + return float(npf.pmt(rate, nper, pv)) + # numpy_financial returns payments with sign convention; our fallback + # returns payment with the same absolute magnitude but as negative (to + # match npf behavior for positive PV) so callers that negate will get the + # same sign semantics. + res = pmt_formula(rate, nper, pv) + return -res + + +def fv(rate: float, nper: int, pmt: float, pv: float) -> float: + """Future value wrapper to `numpy_financial.fv` when available. + + Used in `Prediksi.py` as `npf.fv(rate, nper + i, pmt, pv)` to compute + forecasting values. + """ + if npf is not None: + return float(npf.fv(rate, nper, pmt, pv)) + # fallback: compute iteratively + r = float(rate) + n = int(nper) + fv_val = -pv + # apply periods + for _ in range(n): + fv_val = fv_val * (1 + r) + (-pmt) + return float(fv_val) + + +def discounted_projection_sum(values: Sequence[float], inflation_rate: float, last_seq: int = 0) -> float: + """Discount a sequence of projected `values` using `inflation_rate` with an initial offset `last_seq`. + + This mirrors the calculation in `Eac.py` where projected flows are discounted + starting at `last_seq + 1`. + """ + return npv_from_cash_flows(values, inflation_rate, offset=last_seq) + + +def rc_labor_cost(interval: float, labor_time: float, labor_human: float, man_hour_rate: float) -> float: + """Compute labor cost for corrective/pm/oh rows. + + Formula from `Prediksi.py` / `insert_actual_data.py`: + rc_xxx_labor_cost = (interval * labor_time * labor_human * man_hour_rate) + """ + return float(interval) * float(labor_time) * float(labor_human) * float(man_hour_rate) + + +def rc_lost_cost(raw_loss_output_mw: float, raw_loss_output_price: float, cm_interval: float) -> float: + """Compute lost production cost as in `Prediksi.py`. + + Formula used: + rc_lost_cost = (raw_loss_output_MW * raw_loss_output_price * raw_cm_interval) * 1000 + The multiplication by 1000 appears in source and is preserved here. + """ + return float(raw_loss_output_mw) * float(raw_loss_output_price) * float(cm_interval) * 1000.0 + + +def rc_total_cost(**components) -> float: + """Sum all provided components into a total RC cost. + + The `Eac.py` / `Prediksi.py` code builds a `rc_total_cost` as the sum of many + parts; this helper simply sums values passed as keyword args (coalescing + None to 0.0) to make testing/comparison easier. + """ + total = 0.0 + for k, v in components.items(): + try: + total += 0.0 if v is None else float(v) + except Exception: + # ignore non-numeric components + continue + return float(total) + + +def seconds_to_hours(seconds: float, ndigits: int = 2) -> float: + """Convert seconds to hours and round to `ndigits` decimals. + + This mirrors usage like: + ROUND(SUM(EXTRACT(EPOCH FROM (actfinish - actstart)) / 3600), 2) + in SQL queries seen in `insert_actual_data.py`. + """ + hours = float(seconds) / 3600.0 + return float(round(hours, ndigits)) + + +def man_count_from_labtrans(count: int) -> int: + """Return man_count as per SQL CASE: if count == 0 -> 3 else count. + + Implemented to match the SQL "CASE WHEN COUNT(b.laborcode) = 0 THEN 3 ELSE COUNT(b.laborcode)". + """ + try: + c = int(count) + except Exception: + c = 0 + return 3 if c == 0 else c + + +__all__ = [ + "npv_from_cash_flows", + "pmt_formula", + "pmt", + "fv", + "discounted_projection_sum", + "rc_labor_cost", + "rc_lost_cost", + "rc_total_cost", + "seconds_to_hours", + "man_count_from_labtrans", +] diff --git a/src/plant_transaction_data/__pycache__/service.cpython-311.pyc b/src/plant_transaction_data/__pycache__/service.cpython-311.pyc index 3c5ab9d4c2d38bfc2a068d2c886765adc2d44f29..d032b246c3fb68a5a4f6f291658dbddf0b32d8c2 100644 GIT binary patch delta 1598 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