perfact-api-pd 0.2__tar.gz → 0.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {perfact_api_pd-0.2 → perfact_api_pd-0.3}/PKG-INFO +6 -2
- {perfact_api_pd-0.2 → perfact_api_pd-0.3}/pyproject.toml +8 -1
- {perfact_api_pd-0.2 → perfact_api_pd-0.3}/src/perfact/api/pd/model/pdordlch.py +6 -2
- {perfact_api_pd-0.2 → perfact_api_pd-0.3}/src/perfact/api/pd/model/pdordprd.py +130 -7
- perfact_api_pd-0.3/src/perfact/api/pd/services/__init__.py +13 -0
- perfact_api_pd-0.3/src/perfact/api/pd/services/pdunitperformance_orm.py +425 -0
- perfact_api_pd-0.3/src/perfact/api/pd/services/pdunitperformance_plainsql.py +191 -0
- {perfact_api_pd-0.2 → perfact_api_pd-0.3}/src/perfact_api_pd.egg-info/PKG-INFO +6 -2
- {perfact_api_pd-0.2 → perfact_api_pd-0.3}/src/perfact_api_pd.egg-info/SOURCES.txt +7 -1
- perfact_api_pd-0.3/src/perfact_api_pd.egg-info/requires.txt +10 -0
- perfact_api_pd-0.3/tests/test_models.py +397 -0
- perfact_api_pd-0.3/tests/test_policy.py +34 -0
- perfact_api_pd-0.3/tests/test_s_pdunitperformance.py +484 -0
- {perfact_api_pd-0.2 → perfact_api_pd-0.3}/tox.ini +2 -1
- perfact_api_pd-0.2/src/perfact_api_pd.egg-info/requires.txt +0 -5
- {perfact_api_pd-0.2 → perfact_api_pd-0.3}/README.md +0 -0
- {perfact_api_pd-0.2 → perfact_api_pd-0.3}/setup.cfg +0 -0
- {perfact_api_pd-0.2 → perfact_api_pd-0.3}/src/perfact/api/pd/__init__.py +0 -0
- {perfact_api_pd-0.2 → perfact_api_pd-0.3}/src/perfact/api/pd/model/__init__.py +0 -0
- {perfact_api_pd-0.2 → perfact_api_pd-0.3}/src/perfact/api/pd/model/pdord.py +0 -0
- {perfact_api_pd-0.2 → perfact_api_pd-0.3}/src/perfact/api/pd/model/pdordlc.py +0 -0
- {perfact_api_pd-0.2 → perfact_api_pd-0.3}/src/perfact/api/pd/model/pdordlct.py +0 -0
- {perfact_api_pd-0.2 → perfact_api_pd-0.3}/src/perfact/api/pd/model/pdunit.py +0 -0
- {perfact_api_pd-0.2 → perfact_api_pd-0.3}/src/perfact/api/pd/model/policy.py +0 -0
- {perfact_api_pd-0.2 → perfact_api_pd-0.3}/src/perfact/api/pd/model/py.typed +0 -0
- {perfact_api_pd-0.2 → perfact_api_pd-0.3}/src/perfact/api/pd/py.typed +0 -0
- {perfact_api_pd-0.2 → perfact_api_pd-0.3}/src/perfact_api_pd.egg-info/dependency_links.txt +0 -0
- {perfact_api_pd-0.2 → perfact_api_pd-0.3}/src/perfact_api_pd.egg-info/entry_points.txt +0 -0
- {perfact_api_pd-0.2 → perfact_api_pd-0.3}/src/perfact_api_pd.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: perfact-api-pd
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3
|
|
4
4
|
Summary: PerFact API - pd domain models and services
|
|
5
5
|
Author-email: Viktor Dick <viktor.dick@perfact.de>
|
|
6
6
|
License-Expression: GPL-2.0-or-later
|
|
@@ -11,9 +11,13 @@ Requires-Python: >=3.10
|
|
|
11
11
|
Description-Content-Type: text/markdown
|
|
12
12
|
Requires-Dist: psycopg[binary]
|
|
13
13
|
Requires-Dist: sqlalchemy
|
|
14
|
-
Requires-Dist: perfact-api-base
|
|
14
|
+
Requires-Dist: perfact-api-base
|
|
15
15
|
Requires-Dist: perfact-api-app
|
|
16
16
|
Requires-Dist: perfact-api-pp
|
|
17
|
+
Provides-Extra: test
|
|
18
|
+
Requires-Dist: pytest; extra == "test"
|
|
19
|
+
Requires-Dist: pytest-cov; extra == "test"
|
|
20
|
+
Requires-Dist: pytest-postgresql; extra == "test"
|
|
17
21
|
|
|
18
22
|
# PerFact API - pd - model
|
|
19
23
|
|
|
@@ -18,13 +18,20 @@ classifiers = [
|
|
|
18
18
|
dependencies = [
|
|
19
19
|
"psycopg[binary]",
|
|
20
20
|
"sqlalchemy",
|
|
21
|
-
"perfact-api-base
|
|
21
|
+
"perfact-api-base",
|
|
22
22
|
"perfact-api-app",
|
|
23
23
|
"perfact-api-pp",
|
|
24
24
|
]
|
|
25
25
|
dynamic = ["version"]
|
|
26
26
|
requires-python = ">=3.10"
|
|
27
27
|
|
|
28
|
+
[project.optional-dependencies]
|
|
29
|
+
test = [
|
|
30
|
+
"pytest",
|
|
31
|
+
"pytest-cov",
|
|
32
|
+
"pytest-postgresql",
|
|
33
|
+
]
|
|
34
|
+
|
|
28
35
|
[project.scripts]
|
|
29
36
|
|
|
30
37
|
[tool.distutils.bdist_wheel]
|
|
@@ -31,7 +31,9 @@ class PdOrdLch(Base):
|
|
|
31
31
|
pdord: Mapped["PdOrd"] = relationship("PdOrd")
|
|
32
32
|
|
|
33
33
|
@hybrid_method
|
|
34
|
-
def availability_nominator_eligible_def(
|
|
34
|
+
def availability_nominator_eligible_def(
|
|
35
|
+
self, pdunit_id, /
|
|
36
|
+
) -> bool: # pragma: no cover
|
|
35
37
|
"""Python level - boils down to check if this operation history
|
|
36
38
|
is linked to the workcenter and represents a status change
|
|
37
39
|
from a status with pdordlc_isexecution"""
|
|
@@ -47,7 +49,9 @@ class PdOrdLch(Base):
|
|
|
47
49
|
)
|
|
48
50
|
|
|
49
51
|
@hybrid_method
|
|
50
|
-
def availability_denominator_eligible_def(
|
|
52
|
+
def availability_denominator_eligible_def(
|
|
53
|
+
self, pdunit_id, /
|
|
54
|
+
) -> bool: # pragma: no cover
|
|
51
55
|
"""Python level - boils down to check if this operation history
|
|
52
56
|
is linked to the workcenter and represents a status change
|
|
53
57
|
from a status with pdordlc_isactive(True) and pdordlc_isexecution(False)"""
|
|
@@ -2,16 +2,18 @@ from datetime import datetime
|
|
|
2
2
|
from typing import Any, Literal, cast, overload
|
|
3
3
|
|
|
4
4
|
from perfact.api.base.model import Base, ForeignKey, Mapped, relationship
|
|
5
|
+
from perfact.api.pp.model import PpRsrc, PpShft
|
|
5
6
|
from psycopg.types.range import Range
|
|
6
7
|
from sqlalchemy import and_, case, func, select
|
|
7
8
|
from sqlalchemy.dialects.postgresql import TSTZRANGE
|
|
8
9
|
from sqlalchemy.ext.hybrid import hybrid_method, hybrid_property
|
|
9
|
-
from sqlalchemy.orm import mapped_column
|
|
10
|
+
from sqlalchemy.orm import Session, mapped_column
|
|
10
11
|
from sqlalchemy.sql import ColumnElement
|
|
11
12
|
from sqlalchemy.sql.elements import SQLCoreOperations
|
|
12
13
|
from sqlalchemy.sql.expression import Extract
|
|
13
14
|
|
|
14
15
|
from .pdord import PdOrd
|
|
16
|
+
from .pdordlch import PdOrdLch
|
|
15
17
|
|
|
16
18
|
|
|
17
19
|
class PdOrdPrd(Base):
|
|
@@ -60,7 +62,7 @@ class PdOrdPrd(Base):
|
|
|
60
62
|
search_range = func.tstzrange(starttime, stoptime, "[)")
|
|
61
63
|
condition = and_(
|
|
62
64
|
cast(Any, cls.refrange).is_not(None),
|
|
63
|
-
cls.refrange.op("&&")(search_range),
|
|
65
|
+
cast(Any, cls.refrange).op("&&")(search_range),
|
|
64
66
|
)
|
|
65
67
|
intersection = cls.refrange.op("*")(search_range)
|
|
66
68
|
|
|
@@ -176,31 +178,152 @@ class PdOrdPrd(Base):
|
|
|
176
178
|
return cls.refrange_intersection(starttime, stoptime, True)
|
|
177
179
|
|
|
178
180
|
@hybrid_method
|
|
179
|
-
def performance_eligible_def(
|
|
181
|
+
def performance_eligible_def(
|
|
182
|
+
self, pdunit_id: int, starttime: datetime, stoptime: datetime, /
|
|
183
|
+
) -> bool:
|
|
180
184
|
"""Python-level: check if entry is eligible
|
|
181
185
|
to be taken in account for performance"""
|
|
182
186
|
raise NotImplementedError("Sql expression level only")
|
|
183
187
|
|
|
184
188
|
@performance_eligible_def.expression
|
|
185
|
-
def performance_eligible(
|
|
189
|
+
def performance_eligible(
|
|
190
|
+
cls, pdunit_id: int, starttime: datetime, stoptime: datetime, /
|
|
191
|
+
) -> ColumnElement[bool]:
|
|
186
192
|
"""SQL-level: check if entry is eligible
|
|
187
193
|
to be taken in account for performance"""
|
|
194
|
+
search_range = func.tstzrange(starttime, stoptime, "[)")
|
|
188
195
|
return and_(
|
|
189
196
|
cast(Any, cls.deleted).is_(False),
|
|
190
197
|
cast(Any, cls.pdunit_id) == pdunit_id,
|
|
191
198
|
cast(Any, cls.pdord).has(PdOrd.pdunit_id == pdunit_id),
|
|
199
|
+
cast(Any, cls.refrange).is_not(None),
|
|
200
|
+
cast(Any, cls.refrange).op("&&")(search_range),
|
|
192
201
|
)
|
|
193
202
|
|
|
194
203
|
@hybrid_method
|
|
195
|
-
def quality_eligible_def(
|
|
204
|
+
def quality_eligible_def(
|
|
205
|
+
self, pdunit_id: int, starttime: datetime, stoptime: datetime, /
|
|
206
|
+
) -> bool:
|
|
196
207
|
"""Python-level: check if entry is eligible
|
|
197
208
|
to be taken in account for quality"""
|
|
198
209
|
raise NotImplementedError("Sql expression level only")
|
|
199
210
|
|
|
200
211
|
@quality_eligible_def.expression
|
|
201
|
-
def quality_eligible(
|
|
212
|
+
def quality_eligible(
|
|
213
|
+
cls, pdunit_id: int, starttime: datetime, stoptime: datetime, /
|
|
214
|
+
) -> ColumnElement[bool]:
|
|
202
215
|
"""SQL-level: check if entry is eligible
|
|
203
216
|
to be taken in account for quality"""
|
|
217
|
+
search_range = func.tstzrange(starttime, stoptime, "[)")
|
|
204
218
|
return and_(
|
|
205
|
-
cast(Any, cls.deleted).is_(False),
|
|
219
|
+
cast(Any, cls.deleted).is_(False),
|
|
220
|
+
cast(Any, cls.pdunit_id) == pdunit_id,
|
|
221
|
+
cast(Any, cls.refrange).is_not(None),
|
|
222
|
+
cast(Any, cls.refrange).op("&&")(search_range),
|
|
206
223
|
)
|
|
224
|
+
|
|
225
|
+
@staticmethod
|
|
226
|
+
def oee_performance_computation(
|
|
227
|
+
session: Session, pdunit_id: int, starttime: datetime, stoptime: datetime, /
|
|
228
|
+
) -> float:
|
|
229
|
+
"""OEE function for computation of performance."""
|
|
230
|
+
result = session.execute(
|
|
231
|
+
select(
|
|
232
|
+
func.coalesce(
|
|
233
|
+
func.sum(PdOrdPrd.ideal_time_over_time_range(starttime, stoptime)),
|
|
234
|
+
0.0,
|
|
235
|
+
)
|
|
236
|
+
/ func.nullif(
|
|
237
|
+
func.coalesce(
|
|
238
|
+
func.sum(
|
|
239
|
+
PdOrdPrd.actual_time_over_time_range(starttime, stoptime)
|
|
240
|
+
),
|
|
241
|
+
0.0,
|
|
242
|
+
),
|
|
243
|
+
0.0,
|
|
244
|
+
)
|
|
245
|
+
).where(
|
|
246
|
+
PdOrdPrd.performance_eligible(pdunit_id, starttime, stoptime).is_(True)
|
|
247
|
+
)
|
|
248
|
+
).scalar()
|
|
249
|
+
return float(result or 0.0)
|
|
250
|
+
|
|
251
|
+
@staticmethod
|
|
252
|
+
def oee_quality_computation(
|
|
253
|
+
session: Session, pdunit_id: int, starttime: datetime, stoptime: datetime, /
|
|
254
|
+
) -> float | None:
|
|
255
|
+
"""OEE function computation for quality. None when no production data."""
|
|
256
|
+
return session.execute(
|
|
257
|
+
select(
|
|
258
|
+
func.coalesce(
|
|
259
|
+
func.sum(PdOrdPrd.good_quantity_over_range(starttime, stoptime)),
|
|
260
|
+
0.0,
|
|
261
|
+
)
|
|
262
|
+
/ func.nullif(
|
|
263
|
+
func.coalesce(
|
|
264
|
+
func.sum(
|
|
265
|
+
PdOrdPrd.total_quantity_over_range(starttime, stoptime)
|
|
266
|
+
),
|
|
267
|
+
0.0,
|
|
268
|
+
),
|
|
269
|
+
0.0,
|
|
270
|
+
)
|
|
271
|
+
).where(PdOrdPrd.quality_eligible(pdunit_id, starttime, stoptime).is_(True))
|
|
272
|
+
).scalar()
|
|
273
|
+
|
|
274
|
+
@staticmethod
|
|
275
|
+
def oee_availability_computation(
|
|
276
|
+
session: Session, pdunit_id: int, starttime: datetime, stoptime: datetime, /
|
|
277
|
+
) -> float:
|
|
278
|
+
"""OEE function for computation of availability"""
|
|
279
|
+
search_range = func.tstzrange(starttime, stoptime)
|
|
280
|
+
n_intersection = PdOrdLch.lchtimerange.op("*")(search_range)
|
|
281
|
+
in_seconds = func.extract(
|
|
282
|
+
"epoch", func.upper(n_intersection) - func.lower(n_intersection)
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
current_available_time_nq = session.scalar(
|
|
286
|
+
select(
|
|
287
|
+
PdOrdLch.availability_missing_active_operations_nominator(
|
|
288
|
+
pdunit_id, starttime, stoptime
|
|
289
|
+
)
|
|
290
|
+
)
|
|
291
|
+
)
|
|
292
|
+
current_available_time_n = float(current_available_time_nq or 0.0)
|
|
293
|
+
|
|
294
|
+
current_available_time_dq = session.scalar(
|
|
295
|
+
select(
|
|
296
|
+
PdOrdLch.availability_missing_active_operations_denominator(
|
|
297
|
+
pdunit_id, starttime, stoptime
|
|
298
|
+
)
|
|
299
|
+
)
|
|
300
|
+
)
|
|
301
|
+
current_available_time_d = float(current_available_time_dq or 0.0)
|
|
302
|
+
|
|
303
|
+
# nominator calculation
|
|
304
|
+
nominator_q = session.execute(
|
|
305
|
+
select(func.coalesce(func.sum(in_seconds), 0.0))
|
|
306
|
+
.where(PdOrdLch.availability_nominator_eligible(pdunit_id).is_(True))
|
|
307
|
+
.where(PdOrdLch.lchtimerange.op("&&")(search_range))
|
|
308
|
+
).scalar()
|
|
309
|
+
nominator = float(nominator_q or 0.0) + current_available_time_n
|
|
310
|
+
|
|
311
|
+
# denominator calculation
|
|
312
|
+
d_intersection = n_intersection.op("*")(PpShft.shiftrange)
|
|
313
|
+
in_seconds = func.extract(
|
|
314
|
+
"epoch", func.upper(d_intersection) - func.lower(d_intersection)
|
|
315
|
+
)
|
|
316
|
+
denominator_q = session.execute(
|
|
317
|
+
select(func.coalesce(func.sum(in_seconds), 1.0))
|
|
318
|
+
.select_from(PdOrdLch)
|
|
319
|
+
.join(PpRsrc, PpRsrc.pdunit_id == pdunit_id)
|
|
320
|
+
.join(PpShft, PpShft.pdunit_id == PpRsrc.id)
|
|
321
|
+
.where(PdOrdLch.availability_denominator_eligible(pdunit_id).is_(True))
|
|
322
|
+
.where(PdOrdLch.lchtimerange.op("&&")(search_range))
|
|
323
|
+
.where(n_intersection.op("&&")(PpShft.shiftrange))
|
|
324
|
+
).scalar()
|
|
325
|
+
denominator = nominator + float(denominator_q or 0.0) + current_available_time_d
|
|
326
|
+
|
|
327
|
+
availability = nominator / denominator
|
|
328
|
+
|
|
329
|
+
return float(availability)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from .pdunitperformance_orm import (
|
|
2
|
+
get_time_performance_data_v2,
|
|
3
|
+
get_time_performance_data_v3,
|
|
4
|
+
get_time_performance_data_v4,
|
|
5
|
+
)
|
|
6
|
+
from .pdunitperformance_plainsql import get_time_performance_data
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"get_time_performance_data",
|
|
10
|
+
"get_time_performance_data_v2",
|
|
11
|
+
"get_time_performance_data_v3",
|
|
12
|
+
"get_time_performance_data_v4",
|
|
13
|
+
]
|
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
from datetime import datetime, timedelta, timezone
|
|
2
|
+
from typing import Any, Dict, Mapping, Sequence
|
|
3
|
+
|
|
4
|
+
from perfact.api.pd.model.pdord import PdOrd
|
|
5
|
+
from perfact.api.pd.model.pdord import PdOrd_TimeStat as PdOrdTimeStat
|
|
6
|
+
from perfact.api.pd.model.pdordlc import PdOrdLc
|
|
7
|
+
from perfact.api.pd.model.pdordlch import PdOrdLch
|
|
8
|
+
from perfact.api.pd.model.pdordlct import PdOrdLct
|
|
9
|
+
from perfact.api.pd.model.pdordprd import PdOrdPrd
|
|
10
|
+
from sqlalchemy import bindparam, func, select, text
|
|
11
|
+
from sqlalchemy.engine import RowMapping
|
|
12
|
+
from sqlalchemy.orm import Session, aliased
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _safe_delta(end: Any, start: Any):
|
|
16
|
+
if end and start:
|
|
17
|
+
return (end - start).total_seconds()
|
|
18
|
+
return None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _td_to_sec(td: Any):
|
|
22
|
+
return td.total_seconds() if td is not None else None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _process_performance_in_pure_python(raw_data: Mapping[Any, Any]):
|
|
26
|
+
"""
|
|
27
|
+
Takes a dictionary of raw database columns and calculates
|
|
28
|
+
all time management and performance metrics in pure python
|
|
29
|
+
"""
|
|
30
|
+
now = datetime.now(timezone.utc)
|
|
31
|
+
planned_start = raw_data.get("pdord_planstarttime") or raw_data.get(
|
|
32
|
+
"pdord_starttime"
|
|
33
|
+
)
|
|
34
|
+
planned_end = raw_data.get("pdord_planstoptime") or raw_data.get("pdord_stoptime")
|
|
35
|
+
actual_start_time = raw_data.get("actual_start_time")
|
|
36
|
+
actual_end_time = raw_data.get("actual_end_time")
|
|
37
|
+
|
|
38
|
+
if actual_start_time is not None:
|
|
39
|
+
estimated_start = actual_start_time
|
|
40
|
+
elif planned_start is not None and planned_start < now:
|
|
41
|
+
estimated_start = now
|
|
42
|
+
else:
|
|
43
|
+
estimated_start = planned_start
|
|
44
|
+
|
|
45
|
+
pdord_qty = float(raw_data.get("pdord_qty") or 0)
|
|
46
|
+
produced_qty = float(raw_data.get("produced_quantity") or 0)
|
|
47
|
+
active_time = raw_data.get("pdordtimestat_activetime")
|
|
48
|
+
|
|
49
|
+
if actual_end_time is not None:
|
|
50
|
+
estimated_end = actual_end_time
|
|
51
|
+
elif (
|
|
52
|
+
actual_start_time is not None
|
|
53
|
+
and active_time is not None
|
|
54
|
+
and pdord_qty > 0
|
|
55
|
+
and produced_qty > 0
|
|
56
|
+
):
|
|
57
|
+
extrapolated_seconds = active_time.total_seconds() * (pdord_qty / produced_qty)
|
|
58
|
+
estimated_end = actual_start_time + timedelta(seconds=extrapolated_seconds)
|
|
59
|
+
elif (
|
|
60
|
+
actual_start_time is not None
|
|
61
|
+
and planned_end is not None
|
|
62
|
+
and planned_start is not None
|
|
63
|
+
):
|
|
64
|
+
planned_duration = planned_end - planned_start
|
|
65
|
+
estimated_end = actual_start_time + planned_duration
|
|
66
|
+
elif (
|
|
67
|
+
actual_start_time is None
|
|
68
|
+
and planned_start is not None
|
|
69
|
+
and planned_end is not None
|
|
70
|
+
and planned_start < now
|
|
71
|
+
):
|
|
72
|
+
planned_duration = planned_end - planned_start
|
|
73
|
+
estimated_end = now + planned_duration
|
|
74
|
+
else:
|
|
75
|
+
estimated_end = planned_end
|
|
76
|
+
|
|
77
|
+
remaining_time_seconds = None
|
|
78
|
+
if (
|
|
79
|
+
actual_start_time is not None
|
|
80
|
+
and active_time is not None
|
|
81
|
+
and pdord_qty > 0
|
|
82
|
+
and produced_qty > 0
|
|
83
|
+
):
|
|
84
|
+
calc_remaining = (
|
|
85
|
+
active_time.total_seconds() * (pdord_qty / produced_qty)
|
|
86
|
+
- active_time.total_seconds()
|
|
87
|
+
)
|
|
88
|
+
remaining_time_seconds = max(0.0, calc_remaining)
|
|
89
|
+
|
|
90
|
+
delta_start_seconds = _safe_delta(estimated_start, planned_start)
|
|
91
|
+
delta_end_seconds = _safe_delta(estimated_end, planned_end)
|
|
92
|
+
delta_planned_duration = _safe_delta(planned_end, planned_start)
|
|
93
|
+
|
|
94
|
+
dur_est = _safe_delta(estimated_end, estimated_start)
|
|
95
|
+
dur_plan = _safe_delta(planned_end, planned_start)
|
|
96
|
+
estimated_duration = _safe_delta(estimated_end, estimated_start)
|
|
97
|
+
delta_duration_seconds = (
|
|
98
|
+
(dur_est - dur_plan) if (dur_est is not None and dur_plan is not None) else None
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
planned_cycle = _td_to_sec(raw_data.get("pdord_plancycletime"))
|
|
102
|
+
current_cycle = _td_to_sec(raw_data.get("pdordtimestat_cycle_time"))
|
|
103
|
+
|
|
104
|
+
planned_setup = _td_to_sec(raw_data.get("pdord_plansetuptime"))
|
|
105
|
+
current_setup = _td_to_sec(raw_data.get("pdordtimestat_setuptime"))
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
"time_management": {
|
|
109
|
+
"estimated_start": estimated_start,
|
|
110
|
+
"estimated_end": estimated_end,
|
|
111
|
+
"delta_start_seconds": delta_start_seconds,
|
|
112
|
+
"delta_end_seconds": delta_end_seconds,
|
|
113
|
+
"delta_planned_duration_seconds": delta_planned_duration,
|
|
114
|
+
"estimated_duration_seconds": estimated_duration,
|
|
115
|
+
"delta_duration_seconds": delta_duration_seconds,
|
|
116
|
+
},
|
|
117
|
+
"operation_status": {
|
|
118
|
+
"running_time_seconds": _td_to_sec(active_time),
|
|
119
|
+
"remaining_time_seconds": remaining_time_seconds,
|
|
120
|
+
"downtime_seconds": _td_to_sec(
|
|
121
|
+
raw_data.get("pdordtimestat_spentindowntime")
|
|
122
|
+
),
|
|
123
|
+
},
|
|
124
|
+
"performance": {
|
|
125
|
+
"cycle_time": {
|
|
126
|
+
"planned_seconds": planned_cycle,
|
|
127
|
+
"current_seconds": current_cycle,
|
|
128
|
+
"delta_seconds": (current_cycle - planned_cycle)
|
|
129
|
+
if (current_cycle is not None and planned_cycle is not None)
|
|
130
|
+
else None,
|
|
131
|
+
},
|
|
132
|
+
"setup_time": {
|
|
133
|
+
"current_seconds": current_setup,
|
|
134
|
+
"delta_seconds": (current_setup - planned_setup)
|
|
135
|
+
if (current_setup is not None and planned_setup is not None)
|
|
136
|
+
else None,
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def get_time_performance_data_v3(session: Session, pdord_ids: Sequence[int]):
|
|
143
|
+
raw_fetch_sql = text(
|
|
144
|
+
"""
|
|
145
|
+
select
|
|
146
|
+
pdord_id,
|
|
147
|
+
pdord_planstarttime,
|
|
148
|
+
pdord_starttime,
|
|
149
|
+
pdord_planstoptime,
|
|
150
|
+
pdord_stoptime,
|
|
151
|
+
pdord_qty,
|
|
152
|
+
pdord_plancycletime,
|
|
153
|
+
pdord_plansetuptime,
|
|
154
|
+
|
|
155
|
+
pdordtimestat_activetime,
|
|
156
|
+
pdordtimestat_cycle_time,
|
|
157
|
+
pdordtimestat_setuptime,
|
|
158
|
+
pdordtimestat_spentindowntime,
|
|
159
|
+
|
|
160
|
+
-- Grab current state flags directly from the lifecycle table
|
|
161
|
+
pdordlc_isactive as is_active_state,
|
|
162
|
+
pdordlc_isfinal as is_final_state,
|
|
163
|
+
|
|
164
|
+
(select sum(pdordprd_quantity)
|
|
165
|
+
from pdordprd where pdordprd_pdord_id = pdord_id) as produced_quantity,
|
|
166
|
+
|
|
167
|
+
(select min(pdordlch_createtime)
|
|
168
|
+
from pdordlch
|
|
169
|
+
join pdordlct on pdordlch_pdordlct_id = pdordlct_id
|
|
170
|
+
join pdordlc from_lc on from_lc.pdordlc_id = pdordlct_from_pdordlc_id
|
|
171
|
+
join pdordlc to_lc on to_lc.pdordlc_id = pdordlct_to_pdordlc_id
|
|
172
|
+
where pdordlch_pdord_id = pdord_id
|
|
173
|
+
and from_lc.pdordlc_isactive = false
|
|
174
|
+
and to_lc.pdordlc_isactive = true) as actual_start_time,
|
|
175
|
+
(select max(pdordlch_createtime)
|
|
176
|
+
from pdordlch
|
|
177
|
+
join pdordlct on pdordlch_pdordlct_id = pdordlct_id
|
|
178
|
+
join pdordlc to_lc on to_lc.pdordlc_id = pdordlct_to_pdordlc_id
|
|
179
|
+
where pdordlch_pdord_id = pdord_id
|
|
180
|
+
and to_lc.pdordlc_isfinal = true) as actual_end_time
|
|
181
|
+
from pdord
|
|
182
|
+
join pdordlc on pdordlc_id = pdord_pdordlc_id
|
|
183
|
+
left join pdord_timestat on pdordtimestat_pdord_id = pdord_id
|
|
184
|
+
where pdord_id in :pdord_id_array
|
|
185
|
+
"""
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
params = {"pdord_id_array": pdord_ids}
|
|
189
|
+
stm = raw_fetch_sql.bindparams(bindparam("pdord_id_array", expanding=True))
|
|
190
|
+
resultset = session.execute(stm, params).mappings().fetchall()
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
str(row["pdord_id"]): _process_performance_in_pure_python(row)
|
|
194
|
+
for row in resultset
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def get_time_performance_data_v2(session: Session, pdord_ids: Sequence[int]):
|
|
199
|
+
|
|
200
|
+
from_lc = aliased(PdOrdLc)
|
|
201
|
+
to_lc = aliased(PdOrdLc)
|
|
202
|
+
|
|
203
|
+
prd_sq = (
|
|
204
|
+
select(func.sum(PdOrdPrd.quantity))
|
|
205
|
+
.where(PdOrdPrd.pdord_id == PdOrd.id)
|
|
206
|
+
.scalar_subquery()
|
|
207
|
+
.label("produced_quantity")
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
start_sq = (
|
|
211
|
+
select(func.min(PdOrdLch.createtime))
|
|
212
|
+
.select_from(PdOrdLch)
|
|
213
|
+
.join(PdOrdLct, PdOrdLch.pdordlct_id == PdOrdLct.id)
|
|
214
|
+
.join(from_lc, from_lc.id == PdOrdLct.from_pdordlc_id)
|
|
215
|
+
.join(to_lc, to_lc.id == PdOrdLct.to_pdordlc_id)
|
|
216
|
+
.where(
|
|
217
|
+
PdOrdLch.pdord_id == PdOrd.id,
|
|
218
|
+
from_lc.isactive.is_(False),
|
|
219
|
+
to_lc.isactive,
|
|
220
|
+
)
|
|
221
|
+
.scalar_subquery()
|
|
222
|
+
.label("actual_start_time")
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
end_sq = (
|
|
226
|
+
select(func.max(PdOrdLch.createtime))
|
|
227
|
+
.select_from(PdOrdLch)
|
|
228
|
+
.join(PdOrdLct, PdOrdLch.pdordlct_id == PdOrdLct.id)
|
|
229
|
+
.join(to_lc, to_lc.id == PdOrdLct.to_pdordlc_id)
|
|
230
|
+
.where(PdOrdLch.pdord_id == PdOrd.id, to_lc.isfinal)
|
|
231
|
+
.scalar_subquery()
|
|
232
|
+
.label("actual_end_time")
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
stm = (
|
|
236
|
+
select(
|
|
237
|
+
PdOrd.id.label("pdord_id"),
|
|
238
|
+
PdOrd.planstarttime.label("pdord_planstarttime"),
|
|
239
|
+
PdOrd.starttime.label("pdord_starttime"),
|
|
240
|
+
PdOrd.planstoptime.label("pdord_planstoptime"),
|
|
241
|
+
PdOrd.stoptime.label("pdord_stoptime"),
|
|
242
|
+
PdOrd.qty.label("pdord_qty"),
|
|
243
|
+
PdOrd.plancycletime.label("pdord_plancycletime"),
|
|
244
|
+
PdOrd.plansetuptime.label("pdord_plansetuptime"),
|
|
245
|
+
PdOrdTimeStat.activetime.label("pdordtimestat_activetime"),
|
|
246
|
+
PdOrdTimeStat.cycle_time.label("pdordtimestat_cycle_time"),
|
|
247
|
+
PdOrdTimeStat.setuptime.label("pdordtimestat_setuptime"),
|
|
248
|
+
PdOrdTimeStat.spentindowntime.label("pdordtimestat_spentindowntime"),
|
|
249
|
+
PdOrdLc.isactive.label("is_active_state"),
|
|
250
|
+
PdOrdLc.isfinal.label("is_final_state"),
|
|
251
|
+
prd_sq,
|
|
252
|
+
start_sq,
|
|
253
|
+
end_sq,
|
|
254
|
+
)
|
|
255
|
+
.join(PdOrdLc, PdOrdLc.id == PdOrd.pdordlc_id)
|
|
256
|
+
.outerjoin(PdOrdTimeStat, PdOrdTimeStat.id == PdOrd.id)
|
|
257
|
+
.where(PdOrd.id.in_(pdord_ids))
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
resultset = session.execute(stm).mappings().fetchall()
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
str(row["pdord_id"]): _process_performance_in_pure_python(row)
|
|
264
|
+
for row in resultset
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def get_time_performance_data_v4(session: Session, pdord_ids: Sequence[int]):
|
|
269
|
+
pdords = (
|
|
270
|
+
session.execute(
|
|
271
|
+
select(
|
|
272
|
+
PdOrd.id,
|
|
273
|
+
PdOrd.pdordlc_id,
|
|
274
|
+
PdOrd.planstarttime,
|
|
275
|
+
PdOrd.starttime,
|
|
276
|
+
PdOrd.planstoptime,
|
|
277
|
+
PdOrd.stoptime,
|
|
278
|
+
PdOrd.qty,
|
|
279
|
+
PdOrd.plancycletime,
|
|
280
|
+
PdOrd.plansetuptime,
|
|
281
|
+
).where(PdOrd.id.in_(pdord_ids))
|
|
282
|
+
)
|
|
283
|
+
.mappings()
|
|
284
|
+
.all()
|
|
285
|
+
)
|
|
286
|
+
if not pdords:
|
|
287
|
+
return {}
|
|
288
|
+
|
|
289
|
+
pdord_ids = [pdord["id"] for pdord in pdords]
|
|
290
|
+
pdord_lc_ids = {pdord["pdordlc_id"] for pdord in pdords}
|
|
291
|
+
|
|
292
|
+
timestats = (
|
|
293
|
+
session.execute(
|
|
294
|
+
select(
|
|
295
|
+
PdOrdTimeStat.id,
|
|
296
|
+
PdOrdTimeStat.activetime,
|
|
297
|
+
PdOrdTimeStat.cycle_time,
|
|
298
|
+
PdOrdTimeStat.setuptime,
|
|
299
|
+
PdOrdTimeStat.spentindowntime,
|
|
300
|
+
).where(PdOrdTimeStat.id.in_(pdord_ids))
|
|
301
|
+
)
|
|
302
|
+
.mappings()
|
|
303
|
+
.all()
|
|
304
|
+
)
|
|
305
|
+
timestat_by_pdord_id = {row["id"]: row for row in timestats}
|
|
306
|
+
|
|
307
|
+
prd_rows = session.execute(
|
|
308
|
+
select(PdOrdPrd.pdord_id, PdOrdPrd.quantity).where(
|
|
309
|
+
PdOrdPrd.pdord_id.in_(pdord_ids)
|
|
310
|
+
)
|
|
311
|
+
).all()
|
|
312
|
+
produced_by_pdord_id: dict[int, float] = {}
|
|
313
|
+
for prd_pdord_id, prd_quantity in prd_rows:
|
|
314
|
+
produced_by_pdord_id[prd_pdord_id] = produced_by_pdord_id.get(
|
|
315
|
+
prd_pdord_id, 0.0
|
|
316
|
+
) + float(prd_quantity or 0.0)
|
|
317
|
+
|
|
318
|
+
lch_rows = (
|
|
319
|
+
session.execute(
|
|
320
|
+
select(PdOrdLch.pdord_id, PdOrdLch.pdordlct_id, PdOrdLch.createtime).where(
|
|
321
|
+
PdOrdLch.pdord_id.in_(pdord_ids)
|
|
322
|
+
)
|
|
323
|
+
)
|
|
324
|
+
.mappings()
|
|
325
|
+
.all()
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
lct_ids = {row["pdordlct_id"] for row in lch_rows}
|
|
329
|
+
lct_rows = (
|
|
330
|
+
session.execute(
|
|
331
|
+
select(PdOrdLct.id, PdOrdLct.from_pdordlc_id, PdOrdLct.to_pdordlc_id).where(
|
|
332
|
+
PdOrdLct.id.in_(lct_ids)
|
|
333
|
+
)
|
|
334
|
+
)
|
|
335
|
+
.mappings()
|
|
336
|
+
.all()
|
|
337
|
+
if lct_ids
|
|
338
|
+
else []
|
|
339
|
+
)
|
|
340
|
+
lct_by_id = {row["id"]: row for row in lct_rows}
|
|
341
|
+
|
|
342
|
+
transition_lc_ids = {
|
|
343
|
+
lc_id
|
|
344
|
+
for row in lct_rows
|
|
345
|
+
for lc_id in (row["from_pdordlc_id"], row["to_pdordlc_id"])
|
|
346
|
+
}
|
|
347
|
+
all_lc_ids = pdord_lc_ids | transition_lc_ids
|
|
348
|
+
lc_rows = (
|
|
349
|
+
session.execute(
|
|
350
|
+
select(PdOrdLc.id, PdOrdLc.isactive, PdOrdLc.isfinal).where(
|
|
351
|
+
PdOrdLc.id.in_(all_lc_ids)
|
|
352
|
+
)
|
|
353
|
+
)
|
|
354
|
+
.mappings()
|
|
355
|
+
.all()
|
|
356
|
+
if all_lc_ids
|
|
357
|
+
else []
|
|
358
|
+
)
|
|
359
|
+
lc_by_id = {row["id"]: row for row in lc_rows}
|
|
360
|
+
|
|
361
|
+
history_by_pdord_id: dict[int, list[RowMapping]] = {}
|
|
362
|
+
for row in lch_rows:
|
|
363
|
+
history_by_pdord_id.setdefault(row["pdord_id"], []).append(row)
|
|
364
|
+
|
|
365
|
+
result: Dict[str, Any] = {}
|
|
366
|
+
for pdord in pdords:
|
|
367
|
+
actual_start_time = None
|
|
368
|
+
actual_end_time = None
|
|
369
|
+
|
|
370
|
+
for history in history_by_pdord_id.get(pdord["id"], []):
|
|
371
|
+
transition = lct_by_id.get(history["pdordlct_id"])
|
|
372
|
+
if transition is None:
|
|
373
|
+
continue
|
|
374
|
+
|
|
375
|
+
from_lc = lc_by_id.get(transition["from_pdordlc_id"])
|
|
376
|
+
to_lc = lc_by_id.get(transition["to_pdordlc_id"])
|
|
377
|
+
|
|
378
|
+
if (
|
|
379
|
+
from_lc is not None
|
|
380
|
+
and to_lc is not None
|
|
381
|
+
and (from_lc["isactive"] is False)
|
|
382
|
+
and bool(to_lc["isactive"])
|
|
383
|
+
):
|
|
384
|
+
if (
|
|
385
|
+
actual_start_time is None
|
|
386
|
+
or history["createtime"] < actual_start_time
|
|
387
|
+
):
|
|
388
|
+
actual_start_time = history["createtime"]
|
|
389
|
+
|
|
390
|
+
if to_lc is not None and bool(to_lc["isfinal"]):
|
|
391
|
+
if actual_end_time is None or history["createtime"] > actual_end_time:
|
|
392
|
+
actual_end_time = history["createtime"]
|
|
393
|
+
|
|
394
|
+
timestat = timestat_by_pdord_id.get(pdord["id"])
|
|
395
|
+
order_lc = lc_by_id.get(pdord["pdordlc_id"])
|
|
396
|
+
raw_data = {
|
|
397
|
+
"pdord_id": pdord["id"],
|
|
398
|
+
"pdord_planstarttime": pdord["planstarttime"],
|
|
399
|
+
"pdord_starttime": pdord["starttime"],
|
|
400
|
+
"pdord_planstoptime": pdord["planstoptime"],
|
|
401
|
+
"pdord_stoptime": pdord["stoptime"],
|
|
402
|
+
"pdord_qty": pdord["qty"],
|
|
403
|
+
"pdord_plancycletime": pdord["plancycletime"],
|
|
404
|
+
"pdord_plansetuptime": pdord["plansetuptime"],
|
|
405
|
+
"pdordtimestat_activetime": (
|
|
406
|
+
None if timestat is None else timestat["activetime"]
|
|
407
|
+
),
|
|
408
|
+
"pdordtimestat_cycle_time": (
|
|
409
|
+
None if timestat is None else timestat["cycle_time"]
|
|
410
|
+
),
|
|
411
|
+
"pdordtimestat_setuptime": (
|
|
412
|
+
None if timestat is None else timestat["setuptime"]
|
|
413
|
+
),
|
|
414
|
+
"pdordtimestat_spentindowntime": (
|
|
415
|
+
None if timestat is None else timestat["spentindowntime"]
|
|
416
|
+
),
|
|
417
|
+
"is_active_state": None if order_lc is None else order_lc["isactive"],
|
|
418
|
+
"is_final_state": None if order_lc is None else order_lc["isfinal"],
|
|
419
|
+
"produced_quantity": produced_by_pdord_id.get(pdord["id"], 0.0),
|
|
420
|
+
"actual_start_time": actual_start_time,
|
|
421
|
+
"actual_end_time": actual_end_time,
|
|
422
|
+
}
|
|
423
|
+
result[str(pdord["id"])] = _process_performance_in_pure_python(raw_data)
|
|
424
|
+
|
|
425
|
+
return result
|