wizata-dsapi 2.2.0.dev28__tar.gz → 2.2.0.dev30__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.
Files changed (78) hide show
  1. {wizata_dsapi-2.2.0.dev28 → wizata_dsapi-2.2.0.dev30}/PKG-INFO +1 -1
  2. {wizata_dsapi-2.2.0.dev28 → wizata_dsapi-2.2.0.dev30}/wizata_dsapi/__init__.py +4 -0
  3. wizata_dsapi-2.2.0.dev30/wizata_dsapi/inventory_availability.py +138 -0
  4. wizata_dsapi-2.2.0.dev30/wizata_dsapi/planned_movement.py +195 -0
  5. wizata_dsapi-2.2.0.dev30/wizata_dsapi/version.py +1 -0
  6. {wizata_dsapi-2.2.0.dev28 → wizata_dsapi-2.2.0.dev30}/wizata_dsapi/wizata_dsapi_client.py +97 -0
  7. {wizata_dsapi-2.2.0.dev28 → wizata_dsapi-2.2.0.dev30}/wizata_dsapi.egg-info/PKG-INFO +1 -1
  8. {wizata_dsapi-2.2.0.dev28 → wizata_dsapi-2.2.0.dev30}/wizata_dsapi.egg-info/SOURCES.txt +2 -0
  9. wizata_dsapi-2.2.0.dev28/wizata_dsapi/version.py +0 -1
  10. {wizata_dsapi-2.2.0.dev28 → wizata_dsapi-2.2.0.dev30}/LICENSE.txt +0 -0
  11. {wizata_dsapi-2.2.0.dev28 → wizata_dsapi-2.2.0.dev30}/README.rst +0 -0
  12. {wizata_dsapi-2.2.0.dev28 → wizata_dsapi-2.2.0.dev30}/setup.cfg +0 -0
  13. {wizata_dsapi-2.2.0.dev28 → wizata_dsapi-2.2.0.dev30}/setup.py +0 -0
  14. {wizata_dsapi-2.2.0.dev28 → wizata_dsapi-2.2.0.dev30}/wizata_dsapi/api_config.py +0 -0
  15. {wizata_dsapi-2.2.0.dev28 → wizata_dsapi-2.2.0.dev30}/wizata_dsapi/api_dto.py +0 -0
  16. {wizata_dsapi-2.2.0.dev28 → wizata_dsapi-2.2.0.dev30}/wizata_dsapi/api_interface.py +0 -0
  17. {wizata_dsapi-2.2.0.dev28 → wizata_dsapi-2.2.0.dev30}/wizata_dsapi/bucket.py +0 -0
  18. {wizata_dsapi-2.2.0.dev28 → wizata_dsapi-2.2.0.dev30}/wizata_dsapi/business_label.py +0 -0
  19. {wizata_dsapi-2.2.0.dev28 → wizata_dsapi-2.2.0.dev30}/wizata_dsapi/connection.py +0 -0
  20. {wizata_dsapi-2.2.0.dev28 → wizata_dsapi-2.2.0.dev30}/wizata_dsapi/connection_discovery.py +0 -0
  21. {wizata_dsapi-2.2.0.dev28 → wizata_dsapi-2.2.0.dev30}/wizata_dsapi/connection_policy.py +0 -0
  22. {wizata_dsapi-2.2.0.dev28 → wizata_dsapi-2.2.0.dev30}/wizata_dsapi/connection_types.py +0 -0
  23. {wizata_dsapi-2.2.0.dev28 → wizata_dsapi-2.2.0.dev30}/wizata_dsapi/context.py +0 -0
  24. {wizata_dsapi-2.2.0.dev28 → wizata_dsapi-2.2.0.dev30}/wizata_dsapi/dashboard.py +0 -0
  25. {wizata_dsapi-2.2.0.dev28 → wizata_dsapi-2.2.0.dev30}/wizata_dsapi/dataframe_toolkit.py +0 -0
  26. {wizata_dsapi-2.2.0.dev28 → wizata_dsapi-2.2.0.dev30}/wizata_dsapi/datapoint.py +0 -0
  27. {wizata_dsapi-2.2.0.dev28 → wizata_dsapi-2.2.0.dev30}/wizata_dsapi/datastore.py +0 -0
  28. {wizata_dsapi-2.2.0.dev28 → wizata_dsapi-2.2.0.dev30}/wizata_dsapi/deployment.py +0 -0
  29. {wizata_dsapi-2.2.0.dev28 → wizata_dsapi-2.2.0.dev30}/wizata_dsapi/ds_dataframe.py +0 -0
  30. {wizata_dsapi-2.2.0.dev28 → wizata_dsapi-2.2.0.dev30}/wizata_dsapi/dsapi_json_encoder.py +0 -0
  31. {wizata_dsapi-2.2.0.dev28 → wizata_dsapi-2.2.0.dev30}/wizata_dsapi/edge_config.py +0 -0
  32. {wizata_dsapi-2.2.0.dev28 → wizata_dsapi-2.2.0.dev30}/wizata_dsapi/edge_device.py +0 -0
  33. {wizata_dsapi-2.2.0.dev28 → wizata_dsapi-2.2.0.dev30}/wizata_dsapi/edge_module.py +0 -0
  34. {wizata_dsapi-2.2.0.dev28 → wizata_dsapi-2.2.0.dev30}/wizata_dsapi/edge_package.py +0 -0
  35. {wizata_dsapi-2.2.0.dev28 → wizata_dsapi-2.2.0.dev30}/wizata_dsapi/evaluation.py +0 -0
  36. {wizata_dsapi-2.2.0.dev28 → wizata_dsapi-2.2.0.dev30}/wizata_dsapi/execution.py +0 -0
  37. {wizata_dsapi-2.2.0.dev28 → wizata_dsapi-2.2.0.dev30}/wizata_dsapi/execution_log.py +0 -0
  38. {wizata_dsapi-2.2.0.dev28 → wizata_dsapi-2.2.0.dev30}/wizata_dsapi/experiment.py +0 -0
  39. {wizata_dsapi-2.2.0.dev28 → wizata_dsapi-2.2.0.dev30}/wizata_dsapi/formula.py +0 -0
  40. {wizata_dsapi-2.2.0.dev28 → wizata_dsapi-2.2.0.dev30}/wizata_dsapi/graylog_log.py +0 -0
  41. {wizata_dsapi-2.2.0.dev28 → wizata_dsapi-2.2.0.dev30}/wizata_dsapi/group_system.py +0 -0
  42. {wizata_dsapi-2.2.0.dev28 → wizata_dsapi-2.2.0.dev30}/wizata_dsapi/ilogger.py +0 -0
  43. {wizata_dsapi-2.2.0.dev28 → wizata_dsapi-2.2.0.dev30}/wizata_dsapi/insight.py +0 -0
  44. {wizata_dsapi-2.2.0.dev28 → wizata_dsapi-2.2.0.dev30}/wizata_dsapi/inventory.py +0 -0
  45. {wizata_dsapi-2.2.0.dev28 → wizata_dsapi-2.2.0.dev30}/wizata_dsapi/mlmodel.py +0 -0
  46. {wizata_dsapi-2.2.0.dev28 → wizata_dsapi-2.2.0.dev30}/wizata_dsapi/mobile_asset.py +0 -0
  47. {wizata_dsapi-2.2.0.dev28 → wizata_dsapi-2.2.0.dev30}/wizata_dsapi/model_toolkit.py +0 -0
  48. {wizata_dsapi-2.2.0.dev28 → wizata_dsapi-2.2.0.dev30}/wizata_dsapi/models/__init__.py +0 -0
  49. {wizata_dsapi-2.2.0.dev28 → wizata_dsapi-2.2.0.dev30}/wizata_dsapi/models/common.py +0 -0
  50. {wizata_dsapi-2.2.0.dev28 → wizata_dsapi-2.2.0.dev30}/wizata_dsapi/notification.py +0 -0
  51. {wizata_dsapi-2.2.0.dev28 → wizata_dsapi-2.2.0.dev30}/wizata_dsapi/paged_query_result.py +0 -0
  52. {wizata_dsapi-2.2.0.dev28 → wizata_dsapi-2.2.0.dev30}/wizata_dsapi/pipeline.py +0 -0
  53. {wizata_dsapi-2.2.0.dev28 → wizata_dsapi-2.2.0.dev30}/wizata_dsapi/pipeline_image.py +0 -0
  54. {wizata_dsapi-2.2.0.dev28 → wizata_dsapi-2.2.0.dev30}/wizata_dsapi/plot.py +0 -0
  55. {wizata_dsapi-2.2.0.dev28 → wizata_dsapi-2.2.0.dev30}/wizata_dsapi/plots/__init__.py +0 -0
  56. {wizata_dsapi-2.2.0.dev28 → wizata_dsapi-2.2.0.dev30}/wizata_dsapi/plots/common.py +0 -0
  57. {wizata_dsapi-2.2.0.dev28 → wizata_dsapi-2.2.0.dev30}/wizata_dsapi/plots/theme.py +0 -0
  58. {wizata_dsapi-2.2.0.dev28 → wizata_dsapi-2.2.0.dev30}/wizata_dsapi/product.py +0 -0
  59. {wizata_dsapi-2.2.0.dev28 → wizata_dsapi-2.2.0.dev30}/wizata_dsapi/python_package.py +0 -0
  60. {wizata_dsapi-2.2.0.dev28 → wizata_dsapi-2.2.0.dev30}/wizata_dsapi/request.py +0 -0
  61. {wizata_dsapi-2.2.0.dev28 → wizata_dsapi-2.2.0.dev30}/wizata_dsapi/script.py +0 -0
  62. {wizata_dsapi-2.2.0.dev28 → wizata_dsapi-2.2.0.dev30}/wizata_dsapi/scripts/__init__.py +0 -0
  63. {wizata_dsapi-2.2.0.dev28 → wizata_dsapi-2.2.0.dev30}/wizata_dsapi/scripts/common.py +0 -0
  64. {wizata_dsapi-2.2.0.dev28 → wizata_dsapi-2.2.0.dev30}/wizata_dsapi/search.py +0 -0
  65. {wizata_dsapi-2.2.0.dev28 → wizata_dsapi-2.2.0.dev30}/wizata_dsapi/solution_component.py +0 -0
  66. {wizata_dsapi-2.2.0.dev28 → wizata_dsapi-2.2.0.dev30}/wizata_dsapi/streamlit_utils.py +0 -0
  67. {wizata_dsapi-2.2.0.dev28 → wizata_dsapi-2.2.0.dev30}/wizata_dsapi/template.py +0 -0
  68. {wizata_dsapi-2.2.0.dev28 → wizata_dsapi-2.2.0.dev30}/wizata_dsapi/template_config.py +0 -0
  69. {wizata_dsapi-2.2.0.dev28 → wizata_dsapi-2.2.0.dev30}/wizata_dsapi/trigger.py +0 -0
  70. {wizata_dsapi-2.2.0.dev28 → wizata_dsapi-2.2.0.dev30}/wizata_dsapi/twin.py +0 -0
  71. {wizata_dsapi-2.2.0.dev28 → wizata_dsapi-2.2.0.dev30}/wizata_dsapi/twinregistration.py +0 -0
  72. {wizata_dsapi-2.2.0.dev28 → wizata_dsapi-2.2.0.dev30}/wizata_dsapi/user.py +0 -0
  73. {wizata_dsapi-2.2.0.dev28 → wizata_dsapi-2.2.0.dev30}/wizata_dsapi/wizard_function.py +0 -0
  74. {wizata_dsapi-2.2.0.dev28 → wizata_dsapi-2.2.0.dev30}/wizata_dsapi/wizard_request.py +0 -0
  75. {wizata_dsapi-2.2.0.dev28 → wizata_dsapi-2.2.0.dev30}/wizata_dsapi/words.py +0 -0
  76. {wizata_dsapi-2.2.0.dev28 → wizata_dsapi-2.2.0.dev30}/wizata_dsapi.egg-info/dependency_links.txt +0 -0
  77. {wizata_dsapi-2.2.0.dev28 → wizata_dsapi-2.2.0.dev30}/wizata_dsapi.egg-info/requires.txt +0 -0
  78. {wizata_dsapi-2.2.0.dev28 → wizata_dsapi-2.2.0.dev30}/wizata_dsapi.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wizata_dsapi
3
- Version: 2.2.0.dev28
3
+ Version: 2.2.0.dev30
4
4
  Summary: Wizata Data Science Toolkit
5
5
  Author: Wizata S.A.
6
6
  Author-email: info@wizata.com
@@ -81,6 +81,10 @@ from .inventory import (InventoryBalance, InventoryStorage, InventoryStorageData
81
81
  InventoryHistoryEvent, HISTORY_MOVEMENT, HISTORY_COUNT, round_quantity,
82
82
  INVENTORY_MOVEMENTS, INVENTORY_BALANCES, INVENTORY_STOCK,
83
83
  PRODUCT_TAG, PRODUCT_MESSAGE_KEY)
84
+ from .planned_movement import (PlannedMovement, PlannedMovementSet, PLANNED_MOVEMENT_TYPES,
85
+ PLANNED_MOVEMENT_SNAPSHOT_TYPES, PLANNED_MOVEMENT_DATED_TYPES,
86
+ planned_movement_direction, is_dated_planned_movement)
87
+ from .inventory_availability import InventoryAvailability, InventoryAvailabilityProduct, InventoryProjectionDay
84
88
  from .dashboard import Dashboard, DashboardType
85
89
  from .mobile_asset import MobileAssetTile, MobileAssetTileType, MobileAssetConfig
86
90
  from .graylog_log import GraylogLog
@@ -0,0 +1,138 @@
1
+ """
2
+ Production Planner availability (DEV-6137, epic DEV-6044, release 12.2).
3
+
4
+ What a facility can still promise, per product, and how its stock is projected over the
5
+ next days. Computed on read from the stock (balances + movements) and the planned
6
+ movements (DEV-6136); nothing here is stored.
7
+
8
+ - ``total``: stock under the facility, read at the moment of the last availability
9
+ snapshot written (``snapshot_as_of``), so the figures below hold at one moment and ATP
10
+ matches the source's.
11
+ - ``allocated``, ``hold``, ``open_demand``: the snapshot's figures (``allocated``,
12
+ ``hold``, ``openDemand`` planned movements). Without a snapshot they come from the open
13
+ customer order lines (picked not shipped, ordered not picked) and ``from_snapshot`` is
14
+ False.
15
+ - ``available`` = total - allocated - hold; ``atp`` = available - open demand.
16
+ - ``dated``: the unpicked part of the open customer order lines, capped at the open
17
+ demand (lines are read more often than the snapshot); ``undated`` = open demand - dated,
18
+ due at once.
19
+ - ``projection``: one entry per day of the horizon, the level at the end of the day =
20
+ available - undated - dated due by then + supply (open work and purchase orders) due by
21
+ then. ``net_requirement`` is the lowest level when it is below zero.
22
+ """
23
+
24
+ from datetime import date, datetime
25
+ from typing import Optional
26
+
27
+ from .inventory import _ts_in, _ts_out, _uuid_str
28
+
29
+
30
+ def _date_in(value):
31
+ if value is None or isinstance(value, date):
32
+ return value
33
+ return date.fromisoformat(str(value)[:10])
34
+
35
+
36
+ class InventoryProjectionDay:
37
+ """The projected level at the end of ``day``, with what comes in and goes out that day."""
38
+
39
+ def __init__(self, day=None, incoming: float = None, outgoing: float = None, level: float = None):
40
+ self.day: Optional[date] = _date_in(day)
41
+ self.incoming = incoming
42
+ self.outgoing = outgoing
43
+ self.level = level
44
+
45
+ def to_dict(self) -> dict:
46
+ return {"date": self.day.isoformat() if self.day else None, "incoming": self.incoming,
47
+ "outgoing": self.outgoing, "level": self.level}
48
+
49
+ @classmethod
50
+ def from_dict(cls, data: dict) -> "InventoryProjectionDay":
51
+ return cls(day=data.get("date"), incoming=data.get("incoming"), outgoing=data.get("outgoing"),
52
+ level=data.get("level"))
53
+
54
+
55
+ class InventoryAvailabilityProduct:
56
+ """One product's availability at a facility, in ``unit`` (its stock total's unit)."""
57
+
58
+ def __init__(self, key: str = None, name: str = None, unit=None, unit_symbol: str = None):
59
+ self.key = key
60
+ self.name = name
61
+ self.unit: Optional[str] = _uuid_str(unit)
62
+ self.unit_symbol = unit_symbol
63
+ self.total: Optional[float] = None
64
+ self.allocated: Optional[float] = None
65
+ self.hold: Optional[float] = None
66
+ self.open_demand: Optional[float] = None
67
+ self.available: Optional[float] = None
68
+ self.atp: Optional[float] = None
69
+ self.dated: Optional[float] = None
70
+ self.undated: Optional[float] = None
71
+ self.demand_explained: Optional[float] = None
72
+ self.allocation_explained: Optional[float] = None
73
+ self.supply: Optional[float] = None
74
+ self.net_requirement: Optional[float] = None
75
+ self.lowest: Optional[InventoryProjectionDay] = None
76
+ self.projection: list = []
77
+ self.from_snapshot: bool = False
78
+
79
+ _NUMBERS = (("total", "total"), ("allocated", "allocated"), ("hold", "hold"), ("openDemand", "open_demand"),
80
+ ("available", "available"), ("atp", "atp"), ("dated", "dated"), ("undated", "undated"),
81
+ ("demandExplained", "demand_explained"), ("allocationExplained", "allocation_explained"),
82
+ ("supply", "supply"), ("netRequirement", "net_requirement"))
83
+
84
+ def to_dict(self) -> dict:
85
+ obj = {"product": self.key, "name": self.name, "unit": self.unit, "unitSymbol": self.unit_symbol}
86
+ for json_name, attr in self._NUMBERS:
87
+ obj[json_name] = getattr(self, attr)
88
+ obj["fromSnapshot"] = self.from_snapshot
89
+ obj["lowest"] = self.lowest.to_dict() if self.lowest is not None else None
90
+ obj["projection"] = [day.to_dict() for day in self.projection]
91
+ return obj
92
+
93
+ @classmethod
94
+ def from_dict(cls, data: dict) -> "InventoryAvailabilityProduct":
95
+ obj = cls(key=data.get("product"), name=data.get("name"), unit=data.get("unit"),
96
+ unit_symbol=data.get("unitSymbol"))
97
+ for json_name, attr in cls._NUMBERS:
98
+ setattr(obj, attr, data.get(json_name))
99
+ obj.from_snapshot = bool(data.get("fromSnapshot"))
100
+ obj.lowest = InventoryProjectionDay.from_dict(data["lowest"]) if data.get("lowest") else None
101
+ obj.projection = [InventoryProjectionDay.from_dict(d) for d in data.get("projection") or []]
102
+ return obj
103
+
104
+ def __repr__(self) -> str:
105
+ return (f"InventoryAvailabilityProduct({self.key!r}, total={self.total}, atp={self.atp}, "
106
+ f"net_requirement={self.net_requirement})")
107
+
108
+
109
+ class InventoryAvailability:
110
+ """
111
+ Availability of a facility's products at ``as_of`` (DEV-6137). ``snapshot_as_of`` is the
112
+ moment of the last availability snapshot written at or before it (None without one):
113
+ the stock and the snapshot's figures are read there.
114
+ """
115
+
116
+ def __init__(self, facility: str = None, as_of=None, snapshot_as_of=None, horizon_days: int = None):
117
+ self.facility = facility
118
+ self.as_of: Optional[datetime] = _ts_in(as_of)
119
+ self.snapshot_as_of: Optional[datetime] = _ts_in(snapshot_as_of)
120
+ self.horizon_days = horizon_days
121
+ self.products: list = []
122
+
123
+ def product(self, key: str) -> Optional[InventoryAvailabilityProduct]:
124
+ return next((p for p in self.products if p.key == key), None)
125
+
126
+ def to_dict(self) -> dict:
127
+ return {"facility": self.facility,
128
+ "asOf": _ts_out(self.as_of) if isinstance(self.as_of, datetime) else self.as_of,
129
+ "snapshotAsOf": _ts_out(self.snapshot_as_of) if isinstance(self.snapshot_as_of, datetime)
130
+ else self.snapshot_as_of,
131
+ "horizonDays": self.horizon_days, "products": [p.to_dict() for p in self.products]}
132
+
133
+ @classmethod
134
+ def from_dict(cls, data: dict) -> "InventoryAvailability":
135
+ obj = cls(facility=data.get("facility"), as_of=data.get("asOf"), snapshot_as_of=data.get("snapshotAsOf"),
136
+ horizon_days=data.get("horizonDays"))
137
+ obj.products = [InventoryAvailabilityProduct.from_dict(p) for p in data.get("products") or []]
138
+ return obj
@@ -0,0 +1,195 @@
1
+ """
2
+ Production Planner planned movements (epic DEV-6044, DEV-6136, release 12.2).
3
+
4
+ A planned movement is a quantity of a product the planner must not count as free stock:
5
+
6
+ - stock the system of record has reserved or blocked, reported by its availability
7
+ snapshot (``allocated``, ``hold``, ``openDemand``: one row per product, no date);
8
+ - or a movement expected to happen (``customerOrder``, ``workOrder``,
9
+ ``workOrderComponent``, ``purchaseOrder``, ``forecast``: one row per document line,
10
+ due at a date).
11
+
12
+ When an expected movement happens it becomes a real one (a shipped or production
13
+ movement on an inventoryMovement DataPoint) and the planned one closes. Stock itself is
14
+ unchanged: movements and balances stay what they are.
15
+
16
+ A sync writes the **full set** of one source, type and facility at a moment (``asOf``):
17
+ a changed row gets a new version, a row missing from the set is closed. Every version is
18
+ kept (``validFrom`` / ``validTo``), so what was planned at any past moment can be read
19
+ again.
20
+ """
21
+
22
+ from datetime import datetime
23
+ from typing import Optional
24
+
25
+ from .inventory import _ts_in, _ts_out, _uuid_str
26
+
27
+ #: type -> (direction, dated). Snapshot types carry one row per product and no date.
28
+ PLANNED_MOVEMENT_TYPES = {
29
+ "allocated": ("out", False),
30
+ "hold": ("out", False),
31
+ "openDemand": ("out", False),
32
+ "customerOrder": ("out", True),
33
+ "workOrder": ("in", True),
34
+ "workOrderComponent": ("out", True),
35
+ "purchaseOrder": ("in", True),
36
+ "forecast": ("out", True),
37
+ }
38
+ PLANNED_MOVEMENT_SNAPSHOT_TYPES = tuple(t for t, (_, dated) in PLANNED_MOVEMENT_TYPES.items() if not dated)
39
+ PLANNED_MOVEMENT_DATED_TYPES = tuple(t for t, (_, dated) in PLANNED_MOVEMENT_TYPES.items() if dated)
40
+ PLANNED_MOVEMENT_DIRECTIONS = ("in", "out")
41
+
42
+
43
+ def planned_movement_direction(movement_type: str) -> str:
44
+ """``in`` or ``out`` for a type; a ValueError for an unknown one."""
45
+ if movement_type not in PLANNED_MOVEMENT_TYPES:
46
+ raise ValueError(f"type must be one of {', '.join(PLANNED_MOVEMENT_TYPES)}, not {movement_type!r}"[:300])
47
+ return PLANNED_MOVEMENT_TYPES[movement_type][0]
48
+
49
+
50
+ def is_dated_planned_movement(movement_type: str) -> bool:
51
+ """True for a type with one row per document line and a due date."""
52
+ planned_movement_direction(movement_type)
53
+ return PLANNED_MOVEMENT_TYPES[movement_type][1]
54
+
55
+
56
+ class PlannedMovement:
57
+ """
58
+ One version of one planned movement. PostgreSQL ``"PlannedMovement"`` (alembic 0032).
59
+
60
+ - ``source`` names the system it comes from (e.g. ``asc``), ``type`` one of
61
+ :data:`PLANNED_MOVEMENT_TYPES`, ``facility`` the Twin (hardware id) it applies to.
62
+ - A snapshot type is one row per ``product``. A dated type is one row per
63
+ ``document`` (as the source writes it) and ``line`` (its position), due at ``due``.
64
+ - Quantities: ``planned`` in total, ``in_progress`` already started (picked),
65
+ ``done`` already happened (shipped). What remains open is ``planned - done``; it
66
+ is never stored.
67
+ - ``unit``: on write, the backend unit id the quantities are sent in (of the
68
+ product's category); they are converted to the product's default unit. On read,
69
+ the unit they are stored in: the product's default unit when the row was written,
70
+ None when it had none.
71
+ - ``valid_from`` / ``valid_to``: when this version was true. ``valid_to`` is None
72
+ while it still is.
73
+ """
74
+
75
+ @classmethod
76
+ def route(cls):
77
+ return "planner/plannedmovements"
78
+
79
+ def __init__(self, product: str = None, planned: float = None, document: str = None, line: int = None,
80
+ due=None, in_progress: float = None, done: float = None, partner: str = None,
81
+ movement_type: str = None, source: str = None, facility: str = None,
82
+ direction: str = None, unit=None, movement_id=None):
83
+ self.id: Optional[str] = _uuid_str(movement_id)
84
+ self.source: Optional[str] = source
85
+ self.type: Optional[str] = movement_type
86
+ self.facility: Optional[str] = facility
87
+ self.document: Optional[str] = document
88
+ self.line: Optional[int] = line
89
+ self.direction: Optional[str] = direction
90
+ self.product: Optional[str] = product
91
+ self.partner: Optional[str] = partner
92
+ self.due: Optional[datetime] = _ts_in(due)
93
+ self.planned: Optional[float] = planned
94
+ self.in_progress: Optional[float] = in_progress
95
+ self.done: Optional[float] = done
96
+ self.unit: Optional[str] = _uuid_str(unit)
97
+ self.valid_from: Optional[datetime] = None
98
+ self.valid_to: Optional[datetime] = None
99
+ self.created_at: Optional[datetime] = None
100
+ self.created_by: Optional[str] = None
101
+
102
+ @property
103
+ def is_open(self) -> bool:
104
+ """This version is still true."""
105
+ return self.valid_to is None
106
+
107
+ def key(self) -> tuple:
108
+ """What identifies the row within its set: the product for a snapshot type, the
109
+ document and line for a dated one."""
110
+ if self.type in PLANNED_MOVEMENT_TYPES and not PLANNED_MOVEMENT_TYPES[self.type][1]:
111
+ return (self.product,)
112
+ return self.document, self.line
113
+
114
+ def to_dict(self) -> dict:
115
+ obj = {}
116
+ for name, value in (("id", self.id), ("source", self.source), ("type", self.type),
117
+ ("facility", self.facility), ("document", self.document), ("line", self.line),
118
+ ("direction", self.direction), ("product", self.product),
119
+ ("partner", self.partner), ("planned", self.planned),
120
+ ("inProgress", self.in_progress), ("done", self.done), ("unit", self.unit),
121
+ ("createdBy", self.created_by)):
122
+ if value is not None:
123
+ obj[name] = value
124
+ for name, value in (("due", self.due), ("validFrom", self.valid_from), ("validTo", self.valid_to),
125
+ ("createdAt", self.created_at)):
126
+ if value is not None:
127
+ obj[name] = _ts_out(value) if isinstance(value, datetime) else value
128
+ return obj
129
+
130
+ @classmethod
131
+ def from_dict(cls, data: dict) -> "PlannedMovement":
132
+ obj = cls()
133
+ obj.id = _uuid_str(data.get("id"))
134
+ obj.source = data.get("source")
135
+ obj.type = data.get("type")
136
+ obj.facility = data.get("facility")
137
+ obj.document = data.get("document")
138
+ obj.line = data.get("line")
139
+ obj.direction = data.get("direction")
140
+ obj.product = data.get("product")
141
+ obj.partner = data.get("partner")
142
+ obj.due = _ts_in(data.get("due"))
143
+ obj.planned = data.get("planned")
144
+ obj.in_progress = data.get("inProgress", data.get("in_progress"))
145
+ obj.done = data.get("done")
146
+ obj.unit = _uuid_str(data.get("unit"))
147
+ obj.valid_from = _ts_in(data.get("validFrom") or data.get("valid_from"))
148
+ obj.valid_to = _ts_in(data.get("validTo") or data.get("valid_to"))
149
+ obj.created_at = _ts_in(data.get("createdAt") or data.get("created_at"))
150
+ obj.created_by = data.get("createdBy") or data.get("created_by")
151
+ return obj
152
+
153
+ def api_id(self) -> str:
154
+ return self.id
155
+
156
+ def __repr__(self) -> str:
157
+ return (f"PlannedMovement(type={self.type!r}, product={self.product!r}, document={self.document!r}, "
158
+ f"line={self.line!r}, planned={self.planned!r}, done={self.done!r})")
159
+
160
+
161
+ class PlannedMovementSet:
162
+ """
163
+ The outcome of writing one full set (source, type, facility) at ``as_of``: how many
164
+ rows were ``created``, ``changed`` (a new version), ``unchanged`` and ``closed``
165
+ (missing from the set). ``open`` is how many rows the set holds afterwards.
166
+ """
167
+
168
+ def __init__(self, source: str = None, movement_type: str = None, facility: str = None, as_of=None,
169
+ created: int = 0, changed: int = 0, unchanged: int = 0, closed: int = 0, open_rows: int = 0):
170
+ self.source = source
171
+ self.type = movement_type
172
+ self.facility = facility
173
+ self.as_of: Optional[datetime] = _ts_in(as_of)
174
+ self.created = created
175
+ self.changed = changed
176
+ self.unchanged = unchanged
177
+ self.closed = closed
178
+ self.open = open_rows
179
+
180
+ def to_dict(self) -> dict:
181
+ return {"source": self.source, "type": self.type, "facility": self.facility,
182
+ "asOf": _ts_out(self.as_of) if isinstance(self.as_of, datetime) else self.as_of,
183
+ "created": self.created, "changed": self.changed, "unchanged": self.unchanged,
184
+ "closed": self.closed, "open": self.open}
185
+
186
+ @classmethod
187
+ def from_dict(cls, data: dict) -> "PlannedMovementSet":
188
+ return cls(source=data.get("source"), movement_type=data.get("type"), facility=data.get("facility"),
189
+ as_of=data.get("asOf"), created=data.get("created", 0), changed=data.get("changed", 0),
190
+ unchanged=data.get("unchanged", 0), closed=data.get("closed", 0),
191
+ open_rows=data.get("open", 0))
192
+
193
+ def __repr__(self) -> str:
194
+ return (f"PlannedMovementSet(type={self.type!r}, facility={self.facility!r}, created={self.created}, "
195
+ f"changed={self.changed}, unchanged={self.unchanged}, closed={self.closed}, open={self.open})")
@@ -0,0 +1 @@
1
+ __version__ = "2.2.0.dev30"
@@ -58,6 +58,8 @@ import ast
58
58
  from .deployment import Deployment, DeploymentStatus
59
59
  from .product import ProductKind, Product, ProductComponent
60
60
  from .inventory import InventoryBalance, InventoryStorage, InventoryStock, InventoryCount, InventoryHistory
61
+ from .inventory_availability import InventoryAvailability
62
+ from .planned_movement import PlannedMovement, PlannedMovementSet
61
63
  from .connection import EdgeDeviceKey
62
64
  from .edge_config import EdgeConfig
63
65
  from .api_config import _registry
@@ -3169,6 +3171,101 @@ class WizataDSAPIClient(ApiInterface, ApiDtoInterface):
3169
3171
  response = self._planner_request("POST", "planner/inventory/history", body=body)
3170
3172
  return InventoryHistory.from_dict(response.json())
3171
3173
 
3174
+ # Production Planner - planned movements (DEV-6136)
3175
+
3176
+ def set_planned_movements(self, source: str, movement_type: str, facility: str, as_of,
3177
+ movements: list) -> PlannedMovementSet:
3178
+ """
3179
+ Write the full set of planned movements of one source, type and facility as it is
3180
+ at ``as_of``, all or none: afterwards the set holds exactly ``movements``. A row
3181
+ whose values changed gets a new version, a row left out is closed at ``as_of``, an
3182
+ empty list closes them all. Every version is kept.
3183
+
3184
+ Sets are written in time order: an ``as_of`` before the set's last write is a 409.
3185
+ Writing the same ``as_of`` again corrects that write.
3186
+
3187
+ :param source: the system the rows come from, e.g. ``asc``.
3188
+ :param movement_type: ``allocated``, ``hold``, ``openDemand`` (one row per product,
3189
+ from the source's availability snapshot), or ``customerOrder``, ``workOrder``,
3190
+ ``workOrderComponent``, ``purchaseOrder``, ``forecast`` (one row per
3191
+ ``document`` and ``line``, with a ``due`` date).
3192
+ :param facility: hardware id of the Twin the rows apply to.
3193
+ :param as_of: datetime or ISO 8601: when the source held this set.
3194
+ :param movements: PlannedMovement list (or dicts). Quantities: ``planned``, and
3195
+ ``in_progress`` / ``done`` for dated types, in the product's default unit unless
3196
+ ``unit`` names another unit of its category.
3197
+ :return: PlannedMovementSet: how many rows were created, changed, unchanged, closed.
3198
+ """
3199
+ if isinstance(as_of, datetime.datetime):
3200
+ if as_of.tzinfo is None:
3201
+ as_of = as_of.replace(tzinfo=datetime.timezone.utc)
3202
+ as_of = as_of.isoformat()
3203
+ body = {"source": source, "type": movement_type, "facility": facility, "asOf": as_of,
3204
+ "movements": [m.to_dict() if isinstance(m, PlannedMovement) else m for m in movements or []]}
3205
+ response = self._planner_request("POST", "planner/plannedmovements/", body=body)
3206
+ return PlannedMovementSet.from_dict(response.json())
3207
+
3208
+ def search_planned_movements(self, search_query=None) -> PagedQueryResult:
3209
+ """
3210
+ List planned movements: the open rows by default, sorted by due date.
3211
+
3212
+ :param search_query: SearchQuery or dict; filters on ``source``, ``type``,
3213
+ ``facility``, ``product``, ``document``, ``partner`` (eq / in), ``due`` (eq / gte /
3214
+ gt / lt / lte), ``asOf`` (the rows as they were at that moment) or ``history``
3215
+ (true: every version).
3216
+ :return: PagedQueryResult with PlannedMovement results.
3217
+ """
3218
+ if search_query is None:
3219
+ search_query = SearchQuery()
3220
+ elif isinstance(search_query, dict):
3221
+ search_query = SearchQuery.from_dict(search_query)
3222
+ response = self._planner_request("POST", "planner/plannedmovements/search", body=search_query.to_dict())
3223
+ return PagedQueryResult.from_dict(response.json(), dto_class=PlannedMovement)
3224
+
3225
+ def delete_planned_movements(self, source: str, movement_type: str, facility: str) -> int:
3226
+ """
3227
+ Delete every version of every row of one set (e.g. a sync pointed at the wrong
3228
+ facility). To close the rows and keep their history, write an empty set instead.
3229
+
3230
+ :return: how many rows were deleted; 0 when the set held nothing.
3231
+ """
3232
+ response = self._planner_request("DELETE", "planner/plannedmovements/",
3233
+ params={"source": source, "type": movement_type, "facility": facility},
3234
+ expected=(200, 404))
3235
+ return response.json().get("deleted", 0) if response.status_code == 200 else 0
3236
+
3237
+ def get_inventory_availability(self, facility: str, as_of=None, products: list = None,
3238
+ horizon_days: int = None, sources: list = None) -> InventoryAvailability:
3239
+ """
3240
+ What a facility can still promise per product, and its stock projected per day
3241
+ (DEV-6137). The stock under the facility twin and its children and the figures of the
3242
+ last availability snapshot (``allocated``, ``hold``, ``openDemand`` planned movements)
3243
+ are read at that snapshot's moment (``snapshot_as_of``): available = total - allocated
3244
+ - hold, ATP = available - open demand. The open customer order lines date the demand
3245
+ (capped at the open demand; the rest is due at once) and the open work and purchase
3246
+ orders come in when due. Without a snapshot, the lines give allocated and open demand.
3247
+
3248
+ :param facility: hardware id of the facility Twin.
3249
+ :param as_of: datetime, ISO 8601, epoch ms or a relative ``now-1d``; default now.
3250
+ :param products: product keys to keep.
3251
+ :param horizon_days: days projected (UTC days, today first); default 14, at most 120.
3252
+ :param sources: keep the planned movements of those sources.
3253
+ """
3254
+ body = {"facility": facility}
3255
+ if as_of is not None:
3256
+ if isinstance(as_of, datetime.datetime):
3257
+ if as_of.tzinfo is None:
3258
+ as_of = as_of.replace(tzinfo=datetime.timezone.utc)
3259
+ as_of = as_of.isoformat()
3260
+ body["asOf"] = as_of
3261
+ for name, value in (("products", products), ("sources", sources)):
3262
+ if value is not None:
3263
+ body[name] = list(value)
3264
+ if horizon_days is not None:
3265
+ body["horizonDays"] = horizon_days
3266
+ response = self._planner_request("POST", "planner/inventory/availability", body=body)
3267
+ return InventoryAvailability.from_dict(response.json())
3268
+
3172
3269
  # ------------------------------------------------------------ edge (DEV-6013/6014)
3173
3270
 
3174
3271
  @staticmethod
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wizata_dsapi
3
- Version: 2.2.0.dev28
3
+ Version: 2.2.0.dev30
4
4
  Summary: Wizata Data Science Toolkit
5
5
  Author: Wizata S.A.
6
6
  Author-email: info@wizata.com
@@ -33,6 +33,7 @@ wizata_dsapi/group_system.py
33
33
  wizata_dsapi/ilogger.py
34
34
  wizata_dsapi/insight.py
35
35
  wizata_dsapi/inventory.py
36
+ wizata_dsapi/inventory_availability.py
36
37
  wizata_dsapi/mlmodel.py
37
38
  wizata_dsapi/mobile_asset.py
38
39
  wizata_dsapi/model_toolkit.py
@@ -40,6 +41,7 @@ wizata_dsapi/notification.py
40
41
  wizata_dsapi/paged_query_result.py
41
42
  wizata_dsapi/pipeline.py
42
43
  wizata_dsapi/pipeline_image.py
44
+ wizata_dsapi/planned_movement.py
43
45
  wizata_dsapi/plot.py
44
46
  wizata_dsapi/product.py
45
47
  wizata_dsapi/python_package.py
@@ -1 +0,0 @@
1
- __version__ = "2.2.0.dev28"