zeed-movs-viewer 0.0.0__py3-none-any.whl → 0.0.1__py3-none-any.whl
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.
- movsviewer/automation.py +8 -5
- movsviewer/chartview.py +11 -43
- movsviewer/constants.py +1 -1
- movsviewer/mainui.py +15 -9
- movsviewer/plotutils.py +38 -0
- movsviewer/settings.py +2 -2
- movsviewer/validator.py +1 -51
- movsviewer/viewmodel.py +6 -5
- zeed_movs_viewer-0.0.1.dist-info/METADATA +679 -0
- zeed_movs_viewer-0.0.1.dist-info/RECORD +17 -0
- {zeed_movs_viewer-0.0.0.dist-info → zeed_movs_viewer-0.0.1.dist-info}/WHEEL +1 -1
- zeed_movs_viewer-0.0.1.dist-info/licenses/LICENSE +661 -0
- movsviewer/reader.py +0 -42
- zeed_movs_viewer-0.0.0.dist-info/METADATA +0 -15
- zeed_movs_viewer-0.0.0.dist-info/RECORD +0 -16
- {zeed_movs_viewer-0.0.0.dist-info → zeed_movs_viewer-0.0.1.dist-info}/entry_points.txt +0 -0
movsviewer/automation.py
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
from contextlib import contextmanager
|
2
|
-
from logging import
|
3
|
-
from os import listdir
|
2
|
+
from logging import getLogger
|
4
3
|
from pathlib import Path
|
5
4
|
from tempfile import TemporaryDirectory
|
6
5
|
from typing import TYPE_CHECKING
|
@@ -34,6 +33,8 @@ if TYPE_CHECKING:
|
|
34
33
|
|
35
34
|
from selenium.webdriver.remote.webdriver import WebDriver
|
36
35
|
|
36
|
+
logger = getLogger(__name__)
|
37
|
+
|
37
38
|
|
38
39
|
def get_options(dtemp: str) -> Options:
|
39
40
|
options = Options()
|
@@ -123,9 +124,11 @@ def get_movimenti(
|
|
123
124
|
_c(wait, '#select>option[value=TESTO]')
|
124
125
|
Select(_p(wait, '#select')).select_by_value('TESTO')
|
125
126
|
|
126
|
-
|
127
|
+
pdtemp = Path(dtemp)
|
128
|
+
|
129
|
+
logger.info('prima: %s', list(pdtemp.iterdir()))
|
127
130
|
_c(wait, '#downloadApi').click()
|
128
131
|
_i(wait, '.waiting')
|
129
|
-
info('dopo: %s',
|
132
|
+
logger.info('dopo: %s', list(pdtemp.iterdir()))
|
130
133
|
|
131
|
-
yield
|
134
|
+
yield pdtemp / 'ListaMovimenti.txt'
|
movsviewer/chartview.py
CHANGED
@@ -15,12 +15,17 @@ from typing import cast
|
|
15
15
|
from typing import override
|
16
16
|
|
17
17
|
from guilib.chartwidget.chartwidget import ChartWidget
|
18
|
+
from guilib.chartwidget.model import Column
|
19
|
+
from guilib.chartwidget.model import ColumnHeader
|
20
|
+
from guilib.chartwidget.model import Info
|
21
|
+
from guilib.chartwidget.model import InfoProto
|
18
22
|
from guilib.chartwidget.modelgui import SeriesModel
|
19
23
|
from guilib.chartwidget.modelgui import SeriesModelUnit
|
20
24
|
from guilib.chartwidget.viewmodel import SortFilterViewModel
|
21
25
|
from guilib.dates.converters import date2days
|
22
26
|
from guilib.dates.converters import date2QDateTime
|
23
27
|
from movslib.model import ZERO
|
28
|
+
from movslib.reader import read
|
24
29
|
from PySide6.QtCharts import QBarCategoryAxis
|
25
30
|
from PySide6.QtCharts import QBarSeries
|
26
31
|
from PySide6.QtCharts import QBarSet
|
@@ -30,15 +35,10 @@ from PySide6.QtCharts import QLineSeries
|
|
30
35
|
from PySide6.QtCharts import QValueAxis
|
31
36
|
from PySide6.QtCore import Qt
|
32
37
|
|
33
|
-
from movsviewer.reader import read
|
34
|
-
|
35
38
|
if TYPE_CHECKING:
|
36
39
|
from collections.abc import Iterable
|
37
40
|
from collections.abc import Sequence
|
38
41
|
|
39
|
-
from guilib.chartwidget.model import Column
|
40
|
-
from guilib.chartwidget.model import ColumnHeader
|
41
|
-
from guilib.chartwidget.model import Info
|
42
42
|
from movslib.model import Row
|
43
43
|
from PySide6.QtWidgets import QGraphicsSceneMouseEvent
|
44
44
|
from PySide6.QtWidgets import QGraphicsSceneWheelEvent
|
@@ -226,43 +226,12 @@ class Chart(QChart):
|
|
226
226
|
def mouseMoveEvent(self, event: 'QGraphicsSceneMouseEvent') -> None:
|
227
227
|
super().mouseMoveEvent(event)
|
228
228
|
|
229
|
-
x_curr, y_curr = cast(tuple[float, float], event.pos().toTuple())
|
230
|
-
x_prev, y_prev = cast(tuple[float, float], event.lastPos().toTuple())
|
229
|
+
x_curr, y_curr = cast('tuple[float, float]', event.pos().toTuple())
|
230
|
+
x_prev, y_prev = cast('tuple[float, float]', event.lastPos().toTuple())
|
231
231
|
self.scroll(x_prev - x_curr, y_curr - y_prev)
|
232
232
|
|
233
233
|
|
234
|
-
|
235
|
-
def __init__(self, name: str) -> None:
|
236
|
-
self.name = name
|
237
|
-
|
238
|
-
@override
|
239
|
-
def __eq__(self, other: object) -> bool:
|
240
|
-
if not isinstance(other, CH):
|
241
|
-
return NotImplemented
|
242
|
-
return self.name == other.name
|
243
|
-
|
244
|
-
|
245
|
-
class C:
|
246
|
-
def __init__(
|
247
|
-
self, header: 'ColumnHeader', howmuch: 'Decimal | None'
|
248
|
-
) -> None:
|
249
|
-
self.header = header
|
250
|
-
self.howmuch = howmuch
|
251
|
-
|
252
|
-
|
253
|
-
class I: # noqa: E742
|
254
|
-
def __init__(self, when: 'date', columns: 'Sequence[Column]') -> None:
|
255
|
-
self.when = when
|
256
|
-
self.columns = columns
|
257
|
-
|
258
|
-
def howmuch(self, column_header: 'ColumnHeader') -> 'Decimal | None':
|
259
|
-
for column in self.columns:
|
260
|
-
if column.header == column_header:
|
261
|
-
return column.howmuch
|
262
|
-
return None
|
263
|
-
|
264
|
-
|
265
|
-
MONEY_HEADER = CH('money')
|
234
|
+
MONEY_HEADER = ColumnHeader('money')
|
266
235
|
|
267
236
|
|
268
237
|
@dataclass
|
@@ -301,8 +270,7 @@ class SMFMoney(SMFHelper):
|
|
301
270
|
howmuch_f: float,
|
302
271
|
) -> None:
|
303
272
|
self.line_series.append(when_d, 0)
|
304
|
-
# TODO
|
305
|
-
# TOD000
|
273
|
+
# TODO: fix hover to deal with a variable number of items in series
|
306
274
|
self.line_series.append(when_d, howmuch_f)
|
307
275
|
|
308
276
|
self.shared.y_min = min(howmuch, self.shared.y_min)
|
@@ -391,7 +359,7 @@ class SMFMoneyByYear(SMFHelper):
|
|
391
359
|
self.shared.y_max = Decimal(self.acc)
|
392
360
|
|
393
361
|
|
394
|
-
def series_model_factory(infos: 'Sequence[
|
362
|
+
def series_model_factory(infos: 'Sequence[InfoProto]') -> 'SeriesModel':
|
395
363
|
"""Extract money from info; accumulate and step; group by month / year."""
|
396
364
|
shared = SMFShared(
|
397
365
|
x_min=date.max,
|
@@ -449,7 +417,7 @@ class ChartWidgetWrapper(ChartWidget):
|
|
449
417
|
_, data = read(self.data_path)
|
450
418
|
# convert data to infos
|
451
419
|
infos = [
|
452
|
-
|
420
|
+
Info(row.date, [Column(MONEY_HEADER, row.money)])
|
453
421
|
for row in sorted(data, key=lambda row: row.date)
|
454
422
|
]
|
455
423
|
self.model.update(infos)
|
movsviewer/constants.py
CHANGED
@@ -12,5 +12,5 @@ SETTINGSUI_UI_PATH: Final = _resource('settingsui.ui')
|
|
12
12
|
GECKODRIVER_PATH: Final = _resource('geckodriver.exe')
|
13
13
|
|
14
14
|
SETTINGS_USERNAME: Final = 'username'
|
15
|
-
SETTINGS_PASSWORD: Final = 'password'
|
15
|
+
SETTINGS_PASSWORD: Final = 'password' # noqa:S105
|
16
16
|
SETTINGS_DATA_PATHS: Final = 'dataPaths'
|
movsviewer/mainui.py
CHANGED
@@ -4,7 +4,11 @@ from typing import TYPE_CHECKING
|
|
4
4
|
from typing import Final
|
5
5
|
from typing import cast
|
6
6
|
|
7
|
+
from guilib.chartwidget.viewmodel import (
|
8
|
+
SortFilterViewModel as SortFilterViewModel2,
|
9
|
+
)
|
7
10
|
from guilib.multitabs.widget import MultiTabs
|
11
|
+
from guilib.qwtplot.plot import Plot
|
8
12
|
from guilib.searchsheet.widget import SearchSheet
|
9
13
|
from PySide6.QtCore import QCoreApplication
|
10
14
|
from PySide6.QtCore import QItemSelection
|
@@ -24,9 +28,9 @@ from PySide6.QtWidgets import QPlainTextEdit
|
|
24
28
|
from PySide6.QtWidgets import QToolButton
|
25
29
|
from PySide6.QtWidgets import QWidget
|
26
30
|
|
27
|
-
from movsviewer.chartview import ChartWidgetWrapper
|
28
31
|
from movsviewer.constants import MAINUI_UI_PATH
|
29
32
|
from movsviewer.constants import SETTINGSUI_UI_PATH
|
33
|
+
from movsviewer.plotutils import load_infos
|
30
34
|
from movsviewer.settings import Settings
|
31
35
|
from movsviewer.validator import Validator
|
32
36
|
from movsviewer.viewmodel import SortFilterViewModel
|
@@ -75,7 +79,7 @@ def new_settingsui(settings: Settings) -> Settingsui:
|
|
75
79
|
)
|
76
80
|
_set_data_paths(settingsui.dataPaths, file_names)
|
77
81
|
|
78
|
-
settingsui = cast(Settingsui, QUiLoader().load(SETTINGSUI_UI_PATH))
|
82
|
+
settingsui = cast('Settingsui', QUiLoader().load(SETTINGSUI_UI_PATH))
|
79
83
|
settingsui.usernameLineEdit.setText(settings.username)
|
80
84
|
settingsui.passwordLineEdit.setText(settings.password)
|
81
85
|
_set_data_paths(settingsui.dataPaths, settings.data_paths)
|
@@ -88,7 +92,7 @@ def new_settingsui(settings: Settings) -> Settingsui:
|
|
88
92
|
|
89
93
|
class NewMainui:
|
90
94
|
sheets_charts: Final[
|
91
|
-
dict[str, tuple[SearchSheet,
|
95
|
+
dict[str, tuple[SearchSheet, SortFilterViewModel2, int]]
|
92
96
|
] = {}
|
93
97
|
|
94
98
|
settings: Settings
|
@@ -97,7 +101,7 @@ class NewMainui:
|
|
97
101
|
|
98
102
|
def __call__(self, settings: Settings, settingsui: Settingsui) -> QWidget:
|
99
103
|
self.settings = settings
|
100
|
-
self.mainui = cast(Mainui, QUiLoader().load(MAINUI_UI_PATH))
|
104
|
+
self.mainui = cast('Mainui', QUiLoader().load(MAINUI_UI_PATH))
|
101
105
|
|
102
106
|
self.multi_tabs = MultiTabs(self.mainui.centralwidget)
|
103
107
|
self.mainui.gridLayout.addWidget(self.multi_tabs, 0, 0, 1, 1)
|
@@ -127,19 +131,21 @@ class NewMainui:
|
|
127
131
|
return
|
128
132
|
|
129
133
|
data_paths = self.settings.data_paths[:]
|
130
|
-
for data_path, (sheet,
|
134
|
+
for data_path, (sheet, model2, idx) in self.sheets_charts.items():
|
131
135
|
if data_path in data_paths:
|
132
136
|
data_paths.remove(data_path)
|
133
137
|
sheet.reload()
|
134
|
-
|
138
|
+
model2.update(load_infos((data_path, 'money')))
|
135
139
|
else:
|
136
140
|
self.multi_tabs.remove_double_box(idx)
|
137
141
|
del self.sheets_charts[data_path]
|
138
142
|
for data_path in data_paths:
|
139
143
|
sheet, model = self.new_search_sheet(data_path)
|
140
|
-
|
141
|
-
|
142
|
-
|
144
|
+
model2 = SortFilterViewModel2()
|
145
|
+
plot = Plot(model2, None)
|
146
|
+
model2.update(load_infos((data_path, 'money')))
|
147
|
+
idx = self.multi_tabs.add_double_box(sheet, plot, model.name)
|
148
|
+
self.sheets_charts[data_path] = (sheet, model2, idx)
|
143
149
|
|
144
150
|
def update_status_bar(
|
145
151
|
self, model: SortFilterViewModel, selection_model: QItemSelectionModel
|
movsviewer/plotutils.py
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
from collections import defaultdict
|
2
|
+
from datetime import date
|
3
|
+
from itertools import accumulate
|
4
|
+
from operator import attrgetter
|
5
|
+
from typing import TYPE_CHECKING
|
6
|
+
|
7
|
+
from guilib.chartwidget.model import Column
|
8
|
+
from guilib.chartwidget.model import ColumnHeader
|
9
|
+
from guilib.chartwidget.model import ColumnProto
|
10
|
+
from guilib.chartwidget.model import Info
|
11
|
+
from guilib.chartwidget.model import InfoProto
|
12
|
+
from movslib.movs import read_txt
|
13
|
+
|
14
|
+
if TYPE_CHECKING:
|
15
|
+
from collections.abc import Iterable
|
16
|
+
from decimal import Decimal
|
17
|
+
|
18
|
+
from movslib.model import Row
|
19
|
+
from movslib.model import Rows
|
20
|
+
|
21
|
+
|
22
|
+
def _acc(rows: 'Rows') -> 'Iterable[tuple[date, Decimal]]':
|
23
|
+
def func(a: 'tuple[date, Decimal]', b: 'Row') -> 'tuple[date, Decimal]':
|
24
|
+
return (b.date, a[1] + b.money)
|
25
|
+
|
26
|
+
it = iter(sorted(rows, key=attrgetter('date')))
|
27
|
+
head = next(it)
|
28
|
+
return accumulate(it, func, initial=(head.date, head.money))
|
29
|
+
|
30
|
+
|
31
|
+
def load_infos(*fn_names: tuple[str, str]) -> list[InfoProto]:
|
32
|
+
tmp = defaultdict[date, list[ColumnProto]](list)
|
33
|
+
for fn, name in fn_names:
|
34
|
+
_, rows = read_txt(fn, name)
|
35
|
+
ch = ColumnHeader(rows.name)
|
36
|
+
for d, m in _acc(rows):
|
37
|
+
tmp[d].append(Column(ch, m))
|
38
|
+
return [Info(d, tmp[d]) for d in sorted(tmp)]
|
movsviewer/settings.py
CHANGED
@@ -15,7 +15,7 @@ class Settings:
|
|
15
15
|
@property
|
16
16
|
def username(self) -> str:
|
17
17
|
value = self.settings.value(SETTINGS_USERNAME)
|
18
|
-
return cast(str, value) if value is not None else ''
|
18
|
+
return cast('str', value) if value is not None else ''
|
19
19
|
|
20
20
|
@username.setter
|
21
21
|
def username(self, username: str) -> None:
|
@@ -24,7 +24,7 @@ class Settings:
|
|
24
24
|
@property
|
25
25
|
def password(self) -> str:
|
26
26
|
value = self.settings.value(SETTINGS_PASSWORD)
|
27
|
-
return cast(str, value) if value is not None else ''
|
27
|
+
return cast('str', value) if value is not None else ''
|
28
28
|
|
29
29
|
@password.setter
|
30
30
|
def password(self, password: str) -> None:
|
movsviewer/validator.py
CHANGED
@@ -1,63 +1,13 @@
|
|
1
|
-
from datetime import UTC
|
2
|
-
from datetime import date
|
3
|
-
from datetime import datetime
|
4
|
-
from pathlib import Path
|
5
1
|
from typing import TYPE_CHECKING
|
6
2
|
|
3
|
+
from movsvalidator.movsvalidator import validate
|
7
4
|
from PySide6.QtWidgets import QMessageBox
|
8
5
|
from PySide6.QtWidgets import QWidget
|
9
6
|
|
10
|
-
from movsviewer.reader import read
|
11
|
-
|
12
7
|
if TYPE_CHECKING:
|
13
|
-
from movslib.model import KV
|
14
|
-
from movslib.model import Rows
|
15
|
-
|
16
8
|
from movsviewer.settings import Settings
|
17
9
|
|
18
10
|
|
19
|
-
def validate_saldo(kv: 'KV', csv: 'Rows', messages: list[str]) -> bool:
|
20
|
-
messages.append(f'bpol.saldo_al: {kv.saldo_al}')
|
21
|
-
if kv.saldo_al:
|
22
|
-
ultimo_update = (datetime.now(tz=UTC).date() - kv.saldo_al).days
|
23
|
-
messages.append(
|
24
|
-
f'ultimo update: {ultimo_update} giorni fa'
|
25
|
-
)
|
26
|
-
messages.append(
|
27
|
-
f'bpol.saldo_contabile: {float(kv.saldo_contabile):_}'
|
28
|
-
)
|
29
|
-
messages.append(
|
30
|
-
f'bpol.saldo_disponibile: {float(kv.saldo_disponibile):_}'
|
31
|
-
)
|
32
|
-
|
33
|
-
s = sum(item.money for item in csv)
|
34
|
-
messages.append(f'Σ (item.accredito - item.addebito): {float(s):_}')
|
35
|
-
ret = kv.saldo_contabile == s == kv.saldo_disponibile
|
36
|
-
if not ret:
|
37
|
-
delta = max(
|
38
|
-
[abs(kv.saldo_contabile - s), abs(s - kv.saldo_disponibile)]
|
39
|
-
)
|
40
|
-
messages.append(f'Δ: {float(delta):_}')
|
41
|
-
return ret
|
42
|
-
|
43
|
-
|
44
|
-
def validate_dates(csv: 'Rows', messages: list[str]) -> bool:
|
45
|
-
data_contabile: date | None = None
|
46
|
-
for row in csv:
|
47
|
-
if data_contabile is not None and data_contabile < row.data_contabile:
|
48
|
-
messages.append(f'{data_contabile} < {row.data_contabile}!')
|
49
|
-
return False
|
50
|
-
return True
|
51
|
-
|
52
|
-
|
53
|
-
def validate(fn: str, messages: list[str]) -> bool:
|
54
|
-
messages.append(fn)
|
55
|
-
kv, csv = read(fn, Path(fn).stem)
|
56
|
-
return all(
|
57
|
-
[validate_saldo(kv, csv, messages), validate_dates(csv, messages)]
|
58
|
-
)
|
59
|
-
|
60
|
-
|
61
11
|
class Validator:
|
62
12
|
def __init__(self, parent: QWidget, settings: 'Settings') -> None:
|
63
13
|
self.parent = parent
|
movsviewer/viewmodel.py
CHANGED
@@ -13,6 +13,7 @@ from guilib.searchsheet.model import SearchableModel
|
|
13
13
|
from movslib.model import ZERO
|
14
14
|
from movslib.model import Row
|
15
15
|
from movslib.model import Rows
|
16
|
+
from movslib.reader import read
|
16
17
|
from PySide6.QtCore import QAbstractTableModel
|
17
18
|
from PySide6.QtCore import QItemSelectionModel
|
18
19
|
from PySide6.QtCore import QModelIndex
|
@@ -22,8 +23,6 @@ from PySide6.QtCore import Qt
|
|
22
23
|
from PySide6.QtGui import QBrush
|
23
24
|
from PySide6.QtGui import QColor
|
24
25
|
|
25
|
-
from movsviewer.reader import read
|
26
|
-
|
27
26
|
if TYPE_CHECKING:
|
28
27
|
from PySide6.QtWidgets import QStatusBar
|
29
28
|
|
@@ -96,7 +95,7 @@ class ViewModel(QAbstractTableModel):
|
|
96
95
|
if role == Qt.ItemDataRole.BackgroundRole:
|
97
96
|
max_, min_, val = self._max, self._min, _abs(self._data[row])
|
98
97
|
perc = (
|
99
|
-
(val - min_) / (max_ - min_) if max_ != min_ else Decimal(0.5)
|
98
|
+
(val - min_) / (max_ - min_) if max_ != min_ else Decimal('0.5')
|
100
99
|
)
|
101
100
|
|
102
101
|
hue = int(perc * 120) # 0..359 ; red=0, green=120
|
@@ -106,7 +105,9 @@ class ViewModel(QAbstractTableModel):
|
|
106
105
|
return QBrush(QColor.fromHsl(hue, saturation, lightness))
|
107
106
|
|
108
107
|
if role == Qt.ItemDataRole.UserRole:
|
109
|
-
return cast(
|
108
|
+
return cast(
|
109
|
+
'T_FIELDS', getattr(self._data[row], FIELD_NAMES[column])
|
110
|
+
)
|
110
111
|
|
111
112
|
return None
|
112
113
|
|
@@ -148,7 +149,7 @@ class SortFilterViewModel(SearchableModel):
|
|
148
149
|
|
149
150
|
@override
|
150
151
|
def sourceModel(self) -> ViewModel:
|
151
|
-
return cast(ViewModel, super().sourceModel())
|
152
|
+
return cast('ViewModel', super().sourceModel())
|
152
153
|
|
153
154
|
@override
|
154
155
|
def sort(
|