timeshift.py 61 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100
  1. #!/usr/bin/env python3
  2. """
  3. # timeshift - Simple work time scheduler
  4. # Copyright (c) 2009-2020 Michael Buesch <m@bues.ch>
  5. # Licensed under the GNU/GPL version 2 or later.
  6. """
  7. import sys
  8. import os
  9. import base64
  10. import sqlite3 as sql
  11. import pathlib
  12. try:
  13. raise ImportError #FIXME
  14. from PySide2.QtCore import *
  15. from PySide2.QtGui import *
  16. from PySide2.QtWidgets import *
  17. usingPySide = True
  18. except ImportError as e:
  19. from PyQt5.QtCore import *
  20. from PyQt5.QtGui import *
  21. from PyQt5.QtWidgets import *
  22. usingPySide = False
  23. isAndroid = any("ANDROID" in k.upper() for k in os.environ.keys())
  24. # Shift types
  25. SHIFT_DEFAULT = -1 # (not DB ABI)
  26. SHIFT_EARLY = 0
  27. SHIFT_LATE = 1
  28. SHIFT_NIGHT = 2
  29. SHIFT_DAY = 3
  30. # Day type overrides
  31. DTYPE_DEFAULT = 0 # (not DB ABI)
  32. DTYPE_COMPTIME = 1
  33. DTYPE_HOLIDAY = 2
  34. DTYPE_FEASTDAY = 3
  35. DTYPE_SHORTTIME = 4
  36. # Day flags
  37. DFLAG_UNCERTAIN = (1 << 0)
  38. DFLAG_ATTENDANT = (1 << 1)
  39. def toBase64(string):
  40. return base64.standard_b64encode(
  41. string.encode("UTF-8", "ignore")).decode("UTF-8", "ignore")
  42. def fromBase64(b64str):
  43. return base64.standard_b64decode(
  44. b64str.encode("UTF-8", "ignore")).decode("UTF-8", "ignore")
  45. class Wrapper(object): # Must not be QObject derived.
  46. __slots__ = ( "obj", )
  47. def __init__(self, obj):
  48. self.obj = obj
  49. def floatEqual(f0, f1):
  50. return abs(f0 - f1) < 0.001
  51. def QDateToId(qdate):
  52. """Convert a QDate object to a unique integer ID."""
  53. return QDateTime(qdate).toMSecsSinceEpoch()
  54. def IdToQDate(idNum):
  55. """Convert a unique integer ID to a QDate object."""
  56. return QDateTime.fromMSecsSinceEpoch(int(idNum)).date()
  57. class TsException(Exception): pass
  58. class ICal_Event(QObject):
  59. def __init__(self):
  60. QObject.__init__(self)
  61. self.props = { }
  62. def addProp(self, prop):
  63. self.props[prop.name] = prop
  64. def getProp(self, name):
  65. try:
  66. return self.props[name]
  67. except KeyError as e:
  68. return None
  69. def getDateRange(self):
  70. # Returns a list of QDate objects
  71. start = self.getProp("DTSTART")
  72. if not start:
  73. raise TsException("No DTSTART property")
  74. end = self.getProp("DTEND")
  75. if not end:
  76. dur = self.getProp("DURATION")
  77. if not dur:
  78. return [ start.toQDate() ]
  79. raise TsException("TODO: DURATION property "
  80. "not implemented, yet.")
  81. ret, date = [], start.toQDate()
  82. while date < end.toQDate():
  83. ret.append(date)
  84. date = date.addDays(1)
  85. return ret
  86. class ICal_Prop(QObject):
  87. def __init__(self, name, params, value):
  88. QObject.__init__(self)
  89. self.name = name
  90. self.params = params
  91. self.value = value
  92. def toQDate(self):
  93. date = QDate.fromString(self.value, Qt.ISODate)
  94. if date.isValid():
  95. return date
  96. date = QDate.fromString(self.value, "yyyyMMdd")
  97. if date.isValid():
  98. return date
  99. raise TsException("Date property '%s' "
  100. "format error" % self.name)
  101. class ICal(QObject):
  102. "Simple iCalendar parser"
  103. def __init__(self):
  104. # QObject.__init__(self)
  105. pass
  106. def getEvents(self):
  107. return self.__events
  108. def __parseParams(self, params):
  109. # Returns a list of tuples: (paramName, paramValue)
  110. ret = []
  111. for param in params:
  112. p = param.split('=')
  113. if len(p) != 2:
  114. raise TsException("Invalid parameter '%s'" % param)
  115. p[0] = p[0].upper()
  116. ret.append(tuple(p))
  117. return ret
  118. def __unknown(self, propName, value):
  119. print("ical: Ignoring unexpected '%s:%s'" %\
  120. (propName, value))
  121. def parseICal(self, data):
  122. self.__events = []
  123. inCalendar = False
  124. curEvent = None
  125. for line in data.splitlines():
  126. if not line.strip():
  127. continue
  128. vstart = line.find(':')
  129. value = line[vstart+1:]
  130. prop = line[:vstart].split(';')
  131. propName = prop[0].strip().upper()
  132. try:
  133. propParams = prop[1:]
  134. except IndexError as e:
  135. propParams = []
  136. propParams = self.__parseParams(propParams)
  137. if not inCalendar:
  138. if propName == "BEGIN" and\
  139. value.strip().upper() == "VCALENDAR":
  140. inCalendar = True
  141. continue
  142. self.__unknown(propName, value)
  143. continue
  144. if not curEvent:
  145. if propName in ("METHOD", "PRODID", "VERSION"):
  146. continue
  147. if propName == "BEGIN" and\
  148. value.strip().upper() == "VEVENT":
  149. curEvent = ICal_Event()
  150. continue
  151. if propName == "END" and\
  152. value.strip().upper() == "VCALENDAR":
  153. curEvent = None
  154. inCalendar = False
  155. continue
  156. self.__unknown(propName, value)
  157. continue
  158. if propName == "END" and\
  159. value.strip().upper() == "VEVENT":
  160. self.__events.append(curEvent)
  161. curEvent = None
  162. continue
  163. curEvent.addProp(ICal_Prop(propName, propParams, value))
  164. class ICalImport_Opts(QObject):
  165. def __init__(self, setShift, setDayType):
  166. QObject.__init__(self)
  167. self.setShift = setShift
  168. self.setDayType = setDayType
  169. class ICalImport(ICal):
  170. def __init__(self, widget, db):
  171. ICal.__init__(self)
  172. self.widget = widget
  173. self.db = db
  174. def importICal(self, data, opts):
  175. self.parseICal(data)
  176. for event in self.getEvents():
  177. summary = event.getProp("SUMMARY")
  178. if not summary:
  179. raise TsException(
  180. "Event does not have SUMMARY attribute")
  181. for date in event.getDateRange():
  182. self.__doImport(event, date, opts)
  183. def __doImport(self, event, date, opts):
  184. if opts.setDayType != DTYPE_DEFAULT:
  185. newDType = opts.setDayType
  186. curDType = self.db.getDayTypeOverride(date)
  187. if curDType is not None and\
  188. curDType != newDType:
  189. yes = self.__question(date,
  190. "Has day-type override",
  191. date.toString() + ": "
  192. "Already has day type. Override?")
  193. if not yes:
  194. newDType = curDType
  195. if curDType != newDType:
  196. self.db.setDayTypeOverride(date, newDType)
  197. if opts.setShift != SHIFT_DEFAULT:
  198. newShift = opts.setShift
  199. curShift = self.db.getShiftOverride(date)
  200. if curShift is not None and\
  201. curShift != newShift:
  202. yes = self.__question(date,
  203. "Has shift override",
  204. date.toString() + ": "
  205. "Already has shift. Override?")
  206. if not yes:
  207. newShift = curShift
  208. if curShift != newShift:
  209. self.db.setShiftOverride(date, newShift)
  210. summary = event.getProp("SUMMARY").value
  211. newComment = summary
  212. curComment = self.db.getComment(date)
  213. if curComment and\
  214. curComment != newComment:
  215. yes = self.__question(date, "Comment exists",
  216. "A comment exists:\n'" + curComment +\
  217. "'\n\nAppend '%s'?" % summary)
  218. if yes:
  219. newComment = curComment + '\n' + summary
  220. else:
  221. newComment = curComment
  222. if curComment != newComment:
  223. self.db.setComment(date, newComment)
  224. def __question(self, date, caption, text):
  225. res = QMessageBox.question(self.widget,
  226. date.toString() + ": " + caption,
  227. text,
  228. QMessageBox.Yes | QMessageBox.No |\
  229. QMessageBox.Cancel)
  230. if res & QMessageBox.Cancel:
  231. raise TsException("Cancelled")
  232. return bool(res & QMessageBox.Yes)
  233. class ICalImportDialog(QDialog, ICalImport):
  234. def __init__(self, parent, db):
  235. QDialog.__init__(self, parent)
  236. ICalImport.__init__(self, self, db)
  237. self.setWindowTitle("iCalendar Import")
  238. self.setLayout(QGridLayout())
  239. if isAndroid:
  240. self.layout().setSpacing(50)
  241. self.modGroup = QGroupBox("Tagesoptionen pro ical-event setzen")
  242. self.modGroup.setLayout(QGridLayout())
  243. self.layout().addWidget(self.modGroup, 0, 0)
  244. self.typeCombo = DayTypeComboBox(self)
  245. self.modGroup.layout().addWidget(self.typeCombo, 0, 0)
  246. self.shiftCombo = ShiftComboBox(self, defaultShift=True)
  247. self.modGroup.layout().addWidget(self.shiftCombo, 1, 0)
  248. self.fileButton = QPushButton("iCal Datei Import")
  249. self.layout().addWidget(self.fileButton, 1, 0)
  250. self.fileButton.released.connect(self.fileImport)
  251. def fileImport(self):
  252. fn, fil = QFileDialog.getOpenFileName(self,
  253. "iCalendar Datei",
  254. "",
  255. "iCalendar Dateien (*.ics);;"
  256. "Alle Dateien (*)")
  257. if not fn:
  258. return
  259. self.__fileImport(fn)
  260. self.accept()
  261. def __fileImport(self, filename):
  262. try:
  263. fd = open(filename, "rb")
  264. data = fd.read().decode("UTF-8")
  265. fd.close()
  266. except (IOError, UnicodeError) as e:
  267. QMessageBox.critical(self,
  268. "iCal Laden fehlgeschlagen",
  269. "Laden fehlgeschlagen:\n" + str(e))
  270. return
  271. opts = ICalImport_Opts(
  272. setShift=self.shiftCombo.selectedShift(),
  273. setDayType=self.typeCombo.selectedDayType()
  274. )
  275. try:
  276. self.importICal(data, opts)
  277. except TsException as e:
  278. QMessageBox.critical(self,
  279. "iCal Import fehlgeschlagen",
  280. "Import fehlgeschagen:\n" + str(e))
  281. class DayTypeComboBox(QComboBox):
  282. def __init__(self, parent=None):
  283. QComboBox.__init__(self, parent)
  284. self.addItem("---", DTYPE_DEFAULT)
  285. self.addItem("Zeitausgleich", DTYPE_COMPTIME)
  286. self.addItem("Urlaub", DTYPE_HOLIDAY)
  287. self.addItem("Feiertag", DTYPE_FEASTDAY)
  288. self.addItem("Kurzarbeit", DTYPE_SHORTTIME)
  289. def selectedDayType(self):
  290. return self.itemData(self.currentIndex())
  291. class ShiftComboBox(QComboBox):
  292. def __init__(self, parent=None, shortNames=False, defaultShift=False):
  293. QComboBox.__init__(self, parent)
  294. sfx = "" if shortNames else "schicht"
  295. if defaultShift:
  296. self.addItem("Regulaere Schicht", SHIFT_DEFAULT)
  297. self.addItem("Frueh" + sfx, SHIFT_EARLY)
  298. self.addItem("Nacht" + sfx, SHIFT_NIGHT)
  299. self.addItem("Spaet" + sfx, SHIFT_LATE)
  300. self.addItem("Normal" + sfx, SHIFT_DAY)
  301. def selectedShift(self):
  302. return self.itemData(self.currentIndex())
  303. class ShiftConfigItem(QObject):
  304. def __init__(self, name, shift, workTime, breakTime, attendanceTime):
  305. QObject.__init__(self)
  306. self.name = name
  307. self.shift = shift
  308. self.workTime = workTime
  309. self.breakTime = breakTime
  310. self.attendanceTime = attendanceTime
  311. @staticmethod
  312. def toBytes(item):
  313. return ";".join(
  314. ( toBase64(item.name),
  315. str(item.shift),
  316. str(item.workTime),
  317. str(item.breakTime),
  318. str(item.attendanceTime),
  319. )
  320. ).encode("UTF-8", "ignore")
  321. @staticmethod
  322. def fromBytes(b):
  323. string = b.decode("UTF-8", "ignore")
  324. elems = string.split(";")
  325. try:
  326. return ShiftConfigItem(
  327. fromBase64(elems[0]),
  328. int(elems[1], 10),
  329. float(elems[2]),
  330. float(elems[3]),
  331. float(elems[4])
  332. )
  333. except (IndexError, ValueError) as e:
  334. raise TsException("ShiftConfigItem.fromBytes() "
  335. "invalid string: " + string)
  336. class Preset(QObject):
  337. def __init__(self, name, dayType, shift, workTime, breakTime, attendanceTime):
  338. QObject.__init__(self)
  339. self.name = name
  340. self.dayType = dayType
  341. self.shift = shift
  342. self.workTime = workTime
  343. self.breakTime = breakTime
  344. self.attendanceTime = attendanceTime
  345. @staticmethod
  346. def toBytes(preset):
  347. return ";".join(
  348. ( toBase64(preset.name),
  349. str(preset.dayType),
  350. str(preset.shift),
  351. str(preset.workTime),
  352. str(preset.breakTime),
  353. str(preset.attendanceTime),
  354. )
  355. ).encode("UTF-8", "ignore")
  356. @staticmethod
  357. def fromBytes(b):
  358. string = b.decode("UTF-8", "ignore")
  359. elems = string.split(";")
  360. try:
  361. return Preset(
  362. fromBase64(elems[0]),
  363. int(elems[1], 10),
  364. int(elems[2], 10),
  365. float(elems[3]),
  366. float(elems[4]),
  367. float(elems[5])
  368. )
  369. except (IndexError, ValueError) as e:
  370. raise TsException("Preset.fromBytes() "
  371. "invalid string: " + string)
  372. class Snapshot(QObject):
  373. def __init__(self, date, shiftConfigIndex, accountValue,
  374. holidaysLeft):
  375. QObject.__init__(self)
  376. self.date = date
  377. self.shiftConfigIndex = shiftConfigIndex
  378. self.accountValue = accountValue
  379. self.holidaysLeft = holidaysLeft
  380. @staticmethod
  381. def toBytes(snapshot):
  382. return ";".join(
  383. ( str(QDateToId(snapshot.date)),
  384. str(snapshot.shiftConfigIndex),
  385. str(snapshot.accountValue),
  386. str(snapshot.holidaysLeft),
  387. )
  388. ).encode("UTF-8", "ignore")
  389. @staticmethod
  390. def fromBytes(b):
  391. string = b.decode("UTF-8", "ignore")
  392. elems = string.split(";")
  393. try:
  394. return Snapshot(
  395. IdToQDate(int(elems[0], 10)),
  396. int(elems[1], 10),
  397. float(elems[2]),
  398. int(elems[3], 10)
  399. )
  400. except (IndexError, ValueError) as e:
  401. raise TsException("Snapshot.fromBytes() "
  402. "invalid string: " + string)
  403. class TsDatabase(QObject):
  404. INMEM = ":memory:"
  405. VERSION = 2
  406. COMPAT_VERSIONS = ( 2, )
  407. sql.register_adapter(QDate, QDateToId)
  408. sql.register_converter("QDate", IdToQDate)
  409. sql.register_adapter(ShiftConfigItem, ShiftConfigItem.toBytes)
  410. sql.register_converter("ShiftConfigItem", ShiftConfigItem.fromBytes)
  411. sql.register_adapter(Preset, Preset.toBytes)
  412. sql.register_converter("Preset", Preset.fromBytes)
  413. sql.register_adapter(Snapshot, Snapshot.toBytes)
  414. sql.register_converter("Snapshot", Snapshot.fromBytes)
  415. TAB_params = "params(name TEXT, data TEXT)"
  416. TAB_dayflags = "dayFlags(date QDate, value INTEGER)"
  417. TAB_ovr_daytype = "override_dayType(date QDate, value TEXT)"
  418. TAB_ovr_shift = "override_shift(date QDate, value TEXT)"
  419. TAB_ovr_worktm = "override_workTime(date QDate, value TEXT)"
  420. TAB_ovr_brtm = "override_breakTime(date QDate, value TEXT)"
  421. TAB_ovr_atttm = "override_attendanceTime(date QDate, value TEXT)"
  422. TAB_snaps = "snapshots(date QDate, snapshot Snapshot)"
  423. TAB_comments = "comments(date QDate, comment TEXT)"
  424. TAB_shconf = "shiftConfig(idx INTEGER, item ShiftConfigItem)"
  425. TAB_presets = "presets(idx INTEGER, preset Preset)"
  426. def __init__(self):
  427. QObject.__init__(self)
  428. self.commitTimer = QTimer(self)
  429. self.commitTimer.setSingleShot(True)
  430. self.commitTimer.timeout.connect(self.__commitTimerTimeout)
  431. self.__reset()
  432. self.open(self.INMEM)
  433. def __del__(self):
  434. self.conn.close()
  435. def __sqlError(self, exception):
  436. msg = "SQL error: " + str(exception)
  437. print(msg)
  438. import traceback
  439. traceback.print_stack()
  440. raise TsException(msg)
  441. def __reset(self):
  442. self.conn = None
  443. self.filename = None
  444. self.cachedShiftConfig = None
  445. def __close(self):
  446. if not self.conn:
  447. return
  448. try:
  449. if not self.isInMemory():
  450. print("Closing database...")
  451. self.commit()
  452. self.conn.cursor().execute("VACUUM;")
  453. self.commit()
  454. self.conn.close()
  455. self.__reset()
  456. except sql.Error as e:
  457. self.__sqlError(e)
  458. def close(self):
  459. self.__close()
  460. self.open(self.INMEM)
  461. def open(self, filename):
  462. try:
  463. self.__close()
  464. self.conn = sql.connect(str(filename),
  465. detect_types=sql.PARSE_DECLTYPES)
  466. self.filename = filename
  467. if not self.isInMemory():
  468. self.__checkDatabaseVersion()
  469. self.__initTables(self.conn)
  470. if self.isInMemory():
  471. self.__setDatabaseVersion()
  472. except sql.Error as e:
  473. self.__sqlError(e)
  474. def __setDatabaseVersion(self):
  475. try:
  476. self.__setParameter("dbVersion", self.VERSION)
  477. except sql.Error as e:
  478. self.__sqlError(e)
  479. def __checkDatabaseVersion(self):
  480. try:
  481. dbVer = int(self.__getParameter("dbVersion"), 10)
  482. if dbVer not in self.COMPAT_VERSIONS:
  483. raise TsException("Unsupported database "
  484. "version v%d" % dbVer)
  485. except sql.Error as e:
  486. self.__sqlError(e)
  487. except ValueError as e:
  488. raise TsException("Invalid database version info")
  489. def getFilename(self):
  490. return self.filename
  491. def isInMemory(self):
  492. return self.filename == self.INMEM
  493. def commit(self):
  494. try:
  495. self.conn.commit()
  496. except sql.Error as e:
  497. self.__sqlError(e)
  498. def __commitTimerTimeout(self):
  499. print("Committing database...")
  500. self.commit()
  501. def scheduleCommit(self, msec=5000):
  502. self.commitTimer.start(msec)
  503. def __initTables(self, conn):
  504. script = (
  505. "CREATE TABLE IF NOT EXISTS %s;" % self.TAB_params,
  506. "CREATE TABLE IF NOT EXISTS %s;" % self.TAB_dayflags,
  507. "CREATE TABLE IF NOT EXISTS %s;" % self.TAB_ovr_daytype,
  508. "CREATE TABLE IF NOT EXISTS %s;" % self.TAB_ovr_shift,
  509. "CREATE TABLE IF NOT EXISTS %s;" % self.TAB_ovr_worktm,
  510. "CREATE TABLE IF NOT EXISTS %s;" % self.TAB_ovr_brtm,
  511. "CREATE TABLE IF NOT EXISTS %s;" % self.TAB_ovr_atttm,
  512. "CREATE TABLE IF NOT EXISTS %s;" % self.TAB_snaps,
  513. "CREATE TABLE IF NOT EXISTS %s;" % self.TAB_comments,
  514. "CREATE TABLE IF NOT EXISTS %s;" % self.TAB_shconf,
  515. "CREATE TABLE IF NOT EXISTS %s;" % self.TAB_presets,
  516. )
  517. conn.cursor().executescript("\n".join(script))
  518. conn.commit()
  519. def resetDatabase(self):
  520. self.conn.cursor().executescript("""
  521. DROP TABLE IF EXISTS params;
  522. DROP TABLE IF EXISTS dayFlags;
  523. DROP TABLE IF EXISTS override_dayType;
  524. DROP TABLE IF EXISTS override_shift;
  525. DROP TABLE IF EXISTS override_workTime;
  526. DROP TABLE IF EXISTS override_breakTime;
  527. DROP TABLE IF EXISTS override_attendanceTime;
  528. DROP TABLE IF EXISTS snapshots;
  529. DROP TABLE IF EXISTS comments;
  530. DROP TABLE IF EXISTS shiftConfig;
  531. DROP TABLE IF EXISTS presets;
  532. """)
  533. self.conn.commit()
  534. self.conn.cursor().execute("VACUUM;")
  535. self.conn.commit()
  536. self.__initTables(self.conn)
  537. self.__setDatabaseVersion()
  538. self.conn.commit()
  539. def __cloneTab(self, sourceCursor, targetCursor, tabSignature):
  540. tabName = tabSignature.split("(")[0].strip()
  541. columns = tabSignature.split("(")[1].split(")")[0]
  542. columns = [ c.split()[0] for c in columns.split(",") ]
  543. columns = ", ".join(columns)
  544. targetCursor.execute("DROP TABLE IF EXISTS %s;" % tabName)
  545. targetCursor.execute("CREATE TABLE %s;" % tabSignature)
  546. sourceCursor.execute("SELECT %s FROM %s;" % (columns, tabName))
  547. for rowData in sourceCursor.fetchall():
  548. valTmpl = ", ".join("?" * len(columns.split(",")))
  549. targetCursor.execute("INSERT INTO %s(%s) VALUES(%s);" %\
  550. (tabName, columns, valTmpl),
  551. rowData)
  552. def clone(self, target):
  553. try:
  554. cloneconn = sql.connect(str(target),
  555. detect_types=sql.PARSE_DECLTYPES)
  556. for tabSignature in (self.TAB_params, self.TAB_dayflags,
  557. self.TAB_ovr_daytype, self.TAB_ovr_shift,
  558. self.TAB_ovr_worktm, self.TAB_ovr_brtm,
  559. self.TAB_ovr_atttm, self.TAB_snaps,
  560. self.TAB_comments, self.TAB_shconf,
  561. self.TAB_presets):
  562. self.__cloneTab(sourceCursor=self.conn.cursor(),
  563. targetCursor=cloneconn.cursor(),
  564. tabSignature=tabSignature)
  565. cloneconn.commit()
  566. cloneconn.cursor().execute("VACUUM;")
  567. cloneconn.commit()
  568. cloneconn.close()
  569. except sql.Error as e:
  570. self.__sqlError(e)
  571. def __setParameter(self, param, value):
  572. try:
  573. c = self.conn.cursor()
  574. c.execute("DELETE FROM params WHERE name=?;", (str(param),))
  575. if value is not None:
  576. c.execute("INSERT INTO params(name, data) VALUES(?, ?);",
  577. (str(param), str(value)))
  578. self.scheduleCommit()
  579. except sql.Error as e:
  580. self.__sqlError(e)
  581. def __getParameter(self, param):
  582. try:
  583. c = self.conn.cursor()
  584. c.execute("SELECT data FROM params WHERE name=?;", (param,))
  585. value = c.fetchone()
  586. if value:
  587. return value[0]
  588. return None
  589. except sql.Error as e:
  590. self.__sqlError(e)
  591. def setDayFlags(self, date, value):
  592. try:
  593. c = self.conn.cursor()
  594. c.execute("DELETE FROM dayFlags WHERE date=?;", (date,))
  595. c.execute("INSERT INTO dayFlags(date, value) VALUES(?, ?);",
  596. (date, int(value) & 0xFFFFFFFF))
  597. self.scheduleCommit()
  598. except sql.Error as e:
  599. self.__sqlError(e)
  600. def getDayFlags(self, date):
  601. try:
  602. c = self.conn.cursor()
  603. c.execute("SELECT value FROM dayFlags WHERE date=?;", (date,))
  604. value = c.fetchone()
  605. if not value:
  606. return 0
  607. return int(value[0]) & 0xFFFFFFFF
  608. except sql.Error as e:
  609. self.__sqlError(e)
  610. def __setOverride(self, table, date, value):
  611. try:
  612. c = self.conn.cursor()
  613. c.execute("DELETE FROM %s WHERE date=?;" % table, (date,))
  614. if value is not None:
  615. c.execute("INSERT INTO %s(date, value) VALUES(?, ?);" % table,
  616. (date, str(value)))
  617. self.scheduleCommit()
  618. except sql.Error as e:
  619. self.__sqlError(e)
  620. def __getOverride(self, table, date):
  621. try:
  622. c = self.conn.cursor()
  623. c.execute("SELECT value FROM %s WHERE date=?;" % table, (date,))
  624. value = c.fetchone()
  625. if value:
  626. return value[0]
  627. return None
  628. except sql.Error as e:
  629. self.__sqlError(e)
  630. def __hasOverride(self, table, date):
  631. try:
  632. c = self.conn.cursor()
  633. c.execute("SELECT COUNT(*) FROM %s WHERE date=?;" % table, (date,))
  634. value = c.fetchone()
  635. if value:
  636. return value[0] > 0
  637. return False
  638. except sql.Error as e:
  639. self.__sqlError(e)
  640. def setDayTypeOverride(self, date, daytype):
  641. self.__setOverride("override_dayType", date, daytype)
  642. def hasDayTypeOverride(self, date):
  643. return self.__hasOverride("override_dayType", date)
  644. def getDayTypeOverride(self, date):
  645. try:
  646. return int(self.__getOverride("override_dayType", date), 10)
  647. except (ValueError, TypeError) as e:
  648. return None
  649. def findDayTypeDates(self, daytype, beginDate, endDate):
  650. # Find all dates with the specified "daytype" between
  651. # "beginDate" and "endDate".
  652. # XXX: Currently unused.
  653. try:
  654. c = self.conn.cursor()
  655. c.execute("""
  656. SELECT date FROM override_dayType WHERE
  657. (value=? AND date>=? AND date<=?);
  658. """, (daytype, beginDate, endDate))
  659. dates = c.fetchall()
  660. return [ d[0] for d in dates ]
  661. except (ValueError, TypeError) as e:
  662. return None
  663. def setShiftOverride(self, date, shift):
  664. self.__setOverride("override_shift", date, shift)
  665. def hasShiftOverride(self, date):
  666. return self.__hasOverride("override_shift", date)
  667. def getShiftOverride(self, date):
  668. try:
  669. return int(self.__getOverride("override_shift", date), 10)
  670. except (ValueError, TypeError) as e:
  671. return None
  672. def setWorkTimeOverride(self, date, workTime):
  673. self.__setOverride("override_workTime", date, workTime)
  674. def hasWorkTimeOverride(self, date):
  675. return self.__hasOverride("override_workTime", date)
  676. def getWorkTimeOverride(self, date):
  677. try:
  678. return float(self.__getOverride("override_workTime", date))
  679. except (ValueError, TypeError) as e:
  680. return None
  681. def setBreakTimeOverride(self, date, breakTime):
  682. self.__setOverride("override_breakTime", date, breakTime)
  683. def hasBreakTimeOverride(self, date):
  684. return self.__hasOverride("override_breakTime", date)
  685. def getBreakTimeOverride(self, date):
  686. try:
  687. return float(self.__getOverride("override_breakTime", date))
  688. except (ValueError, TypeError) as e:
  689. return None
  690. def setAttendanceTimeOverride(self, date, attendanceTime):
  691. self.__setOverride("override_attendanceTime", date, attendanceTime)
  692. def hasAttendanceTimeOverride(self, date):
  693. return self.__hasOverride("override_attendanceTime", date)
  694. def getAttendanceTimeOverride(self, date):
  695. try:
  696. return float(self.__getOverride("override_attendanceTime", date))
  697. except (ValueError, TypeError) as e:
  698. return None
  699. def setShiftConfigItems(self, items):
  700. self.cachedShiftConfig = items
  701. try:
  702. c = self.conn.cursor()
  703. c.execute("DROP TABLE IF EXISTS shiftConfig;")
  704. c.execute("CREATE TABLE %s;" % self.TAB_shconf)
  705. for (index, item) in enumerate(items):
  706. c.execute("INSERT INTO shiftConfig(idx, item) VALUES(?, ?);",
  707. (index, item))
  708. self.scheduleCommit()
  709. except sql.Error as e:
  710. self.__sqlError(e)
  711. def getShiftConfigItems(self):
  712. if self.cachedShiftConfig is not None:
  713. return self.cachedShiftConfig
  714. try:
  715. c = self.conn.cursor()
  716. c.execute("CREATE TABLE IF NOT EXISTS %s;" % self.TAB_shconf)
  717. c.execute('SELECT item FROM shiftConfig ORDER BY "idx";')
  718. items = c.fetchall()
  719. items = [ i[0] for i in items ]
  720. self.cachedShiftConfig = items
  721. return items
  722. except sql.Error as e:
  723. self.__sqlError(e)
  724. def setPresets(self, presets):
  725. try:
  726. c = self.conn.cursor()
  727. c.execute("DROP TABLE IF EXISTS presets;")
  728. c.execute("CREATE TABLE %s;" % self.TAB_presets)
  729. for (index, preset) in enumerate(presets):
  730. c.execute("INSERT INTO presets(idx, preset) VALUES(?, ?);",
  731. (index, preset))
  732. self.scheduleCommit()
  733. except sql.Error as e:
  734. self.__sqlError(e)
  735. def getPresets(self):
  736. try:
  737. c = self.conn.cursor()
  738. c.execute("CREATE TABLE IF NOT EXISTS %s;" % self.TAB_presets)
  739. c.execute('SELECT preset FROM presets ORDER BY "idx";')
  740. presets = c.fetchall()
  741. return [ p[0] for p in presets ]
  742. except sql.Error as e:
  743. self.__sqlError(e)
  744. def setSnapshot(self, date, snapshot):
  745. try:
  746. c = self.conn.cursor()
  747. c.execute("DELETE FROM snapshots WHERE date=?;", (date,))
  748. if snapshot is not None:
  749. c.execute("INSERT INTO snapshots(date, snapshot) VALUES(?, ?);",
  750. (date, snapshot))
  751. self.scheduleCommit()
  752. except sql.Error as e:
  753. self.__sqlError(e)
  754. def hasSnapshot(self, date):
  755. try:
  756. c = self.conn.cursor()
  757. c.execute("SELECT COUNT(*) FROM snapshots WHERE date=?;", (date,))
  758. value = c.fetchone()
  759. if value:
  760. return value[0] > 0
  761. return False
  762. except sql.Error as e:
  763. self.__sqlError(e)
  764. def getSnapshot(self, date):
  765. try:
  766. c = self.conn.cursor()
  767. c.execute("SELECT snapshot FROM snapshots WHERE date=?;", (date,))
  768. snapshot = c.fetchone()
  769. if snapshot:
  770. snapshot = snapshot[0]
  771. return snapshot
  772. except sql.Error as e:
  773. self.__sqlError(e)
  774. def getAllSnapshots(self):
  775. try:
  776. c = self.conn.cursor()
  777. c.execute("SELECT snapshot FROM snapshots;")
  778. snapshots = c.fetchall()
  779. return [ s[0] for s in snapshots ]
  780. except sql.Error as e:
  781. self.__sqlError(e)
  782. def findSnapshotForDate(self, date):
  783. # Get the snapshot that is active for a certain date.
  784. try:
  785. c = self.conn.cursor()
  786. c.execute("SELECT snapshot FROM snapshots WHERE date<=? "
  787. "ORDER BY date DESC", (date,))
  788. snapshot = c.fetchone()
  789. if snapshot:
  790. return snapshot[0]
  791. return None
  792. except sql.Error as e:
  793. self.__sqlError(e)
  794. def setComment(self, date, comment):
  795. try:
  796. c = self.conn.cursor()
  797. c.execute("DELETE FROM comments WHERE date=?;", (date,))
  798. if comment:
  799. c.execute("INSERT INTO comments(date, comment) VALUES(?, ?);",
  800. (date, str(comment)))
  801. self.scheduleCommit()
  802. except sql.Error as e:
  803. self.__sqlError(e)
  804. def hasComment(self, date):
  805. try:
  806. c = self.conn.cursor()
  807. c.execute("SELECT COUNT(*) FROM comments WHERE date=?;", (date,))
  808. value = c.fetchone()
  809. if value:
  810. return value[0] > 0
  811. return False
  812. except sql.Error as e:
  813. self.__sqlError(e)
  814. def getComment(self, date):
  815. try:
  816. c = self.conn.cursor()
  817. c.execute("SELECT comment FROM comments WHERE date=?;", (date,))
  818. comment = c.fetchone()
  819. if comment:
  820. comment = comment[0]
  821. return comment
  822. except sql.Error as e:
  823. self.__sqlError(e)
  824. class TimeSpinBox(QWidget):
  825. def __init__(self, parent, val=0.0, minVal=0.0, maxVal=24.0,
  826. step=0.1, decimals=2, prefix=None, suffix="h"):
  827. QWidget.__init__(self, parent)
  828. self.setLayout(QGridLayout())
  829. self.layout().setContentsMargins(QMargins())
  830. self.__spinBox = QDoubleSpinBox(self)
  831. if isAndroid:
  832. self.__spinBox.setButtonSymbols(QDoubleSpinBox.NoButtons)
  833. self.layout().addWidget(self.__spinBox, 0, 0, 2, 1)
  834. if isAndroid:
  835. self.__upButton = QPushButton("+", self)
  836. self.layout().addWidget(self.__upButton, 0, 1)
  837. self.__upButton.released.connect(self.__spinBox.stepUp)
  838. if isAndroid:
  839. self.__downButton = QPushButton("-", self)
  840. self.layout().addWidget(self.__downButton, 1, 1)
  841. self.__downButton.released.connect(self.__spinBox.stepDown)
  842. self.setDecimals(decimals)
  843. self.setMinimum(minVal)
  844. self.setMaximum(maxVal)
  845. self.setValue(val)
  846. self.setSingleStep(step)
  847. self.setAccelerated(True)
  848. self.setKeyboardTracking(False)
  849. if suffix:
  850. self.setSuffix(" " + suffix)
  851. if prefix:
  852. self.setPrefix(prefix + " ")
  853. def __getattr__(self, name):
  854. return getattr(self.__spinBox, name)
  855. class DaySpinBox(QSpinBox):
  856. def __init__(self, parent, val=0, minVal=0, maxVal=365,
  857. step=1, prefix=None, suffix="Tage"):
  858. QSpinBox.__init__(self, parent)
  859. self.setMinimum(minVal)
  860. self.setMaximum(maxVal)
  861. self.setValue(val)
  862. self.setSingleStep(step)
  863. if suffix:
  864. self.setSuffix(" " + suffix)
  865. if prefix:
  866. self.setPrefix(prefix + " ")
  867. class ShiftConfigDialog(QDialog):
  868. def __init__(self, mainWidget):
  869. QDialog.__init__(self, mainWidget)
  870. self.mainWidget = mainWidget
  871. self.setWindowTitle("Schichtkonfiguration")
  872. self.setLayout(QGridLayout())
  873. if isAndroid:
  874. self.layout().setSpacing(50)
  875. self.itemList = QListWidget(self)
  876. self.layout().addWidget(self.itemList, 0, 0, 10, 2)
  877. self.addButton = QPushButton("Neu", self)
  878. self.layout().addWidget(self.addButton, 11, 0)
  879. self.removeButton = QPushButton("Loeschen", self)
  880. self.layout().addWidget(self.removeButton, 11, 1)
  881. label = QLabel("Name", self)
  882. self.layout().addWidget(label, 0, 2)
  883. self.nameEdit = QLineEdit(self)
  884. self.layout().addWidget(self.nameEdit, 0, 3)
  885. label = QLabel("Schicht", self)
  886. self.layout().addWidget(label, 1, 2)
  887. self.shiftCombo = ShiftComboBox(self, shortNames=True)
  888. self.layout().addWidget(self.shiftCombo, 1, 3)
  889. label = QLabel("Arbeitszeit", self)
  890. self.layout().addWidget(label, 2, 2)
  891. self.workTime = TimeSpinBox(self)
  892. self.layout().addWidget(self.workTime, 2, 3)
  893. label = QLabel("Pausenzeit", self)
  894. self.layout().addWidget(label, 3, 2)
  895. self.breakTime = TimeSpinBox(self)
  896. self.layout().addWidget(self.breakTime, 3, 3)
  897. label = QLabel("Anwesenheit", self)
  898. self.layout().addWidget(label, 4, 2)
  899. self.attendanceTime = TimeSpinBox(self)
  900. self.layout().addWidget(self.attendanceTime, 4, 3)
  901. self.updateBlocked = False
  902. self.loadConfig()
  903. self.itemList.currentRowChanged.connect(self.itemChanged)
  904. self.addButton.released.connect(self.addItem)
  905. self.removeButton.released.connect(self.removeItem)
  906. self.nameEdit.textChanged.connect(self.updateCurrentItem)
  907. self.shiftCombo.currentIndexChanged.connect(self.updateCurrentItem)
  908. self.workTime.valueChanged.connect(self.updateCurrentItem)
  909. self.breakTime.valueChanged.connect(self.updateCurrentItem)
  910. self.attendanceTime.valueChanged.connect(self.updateCurrentItem)
  911. def setInputEnabled(self, enable):
  912. self.removeButton.setEnabled(enable)
  913. self.nameEdit.setEnabled(enable)
  914. self.shiftCombo.setEnabled(enable)
  915. self.workTime.setEnabled(enable)
  916. self.breakTime.setEnabled(enable)
  917. self.attendanceTime.setEnabled(enable)
  918. def loadConfig(self):
  919. self.itemList.clear()
  920. shiftConfig = self.mainWidget.db.getShiftConfigItems()
  921. count = 1
  922. for cfg in shiftConfig:
  923. name = "%d \"%s\"" % (count, cfg.name)
  924. count += 1
  925. self.itemList.addItem(name)
  926. if shiftConfig:
  927. self.itemList.setCurrentRow(0)
  928. currentItem = shiftConfig[0]
  929. else:
  930. currentItem = None
  931. self.loadItem(currentItem)
  932. self.setInputEnabled(currentItem is not None)
  933. def loadItem(self, item=None):
  934. self.updateBlocked = True
  935. self.nameEdit.clear()
  936. self.shiftCombo.setCurrentIndex(0)
  937. self.workTime.setValue(0.0)
  938. self.breakTime.setValue(0.0)
  939. self.attendanceTime.setValue(0.0)
  940. if item:
  941. self.nameEdit.setText(item.name)
  942. index = self.shiftCombo.findData(item.shift)
  943. self.shiftCombo.setCurrentIndex(index)
  944. self.workTime.setValue(item.workTime)
  945. self.breakTime.setValue(item.breakTime)
  946. self.attendanceTime.setValue(item.attendanceTime)
  947. self.updateBlocked = False
  948. def updateItem(self, item):
  949. item.name = self.nameEdit.text()
  950. index = self.shiftCombo.currentIndex()
  951. item.shift = self.shiftCombo.itemData(index)
  952. item.workTime = self.workTime.value()
  953. item.breakTime = self.breakTime.value()
  954. item.attendanceTime = self.attendanceTime.value()
  955. def itemChanged(self, row):
  956. if row >= 0:
  957. shiftConfig = self.mainWidget.db.getShiftConfigItems()
  958. self.loadItem(shiftConfig[row])
  959. def updateCurrentItem(self):
  960. if self.updateBlocked:
  961. return
  962. index = self.itemList.currentRow()
  963. if index < 0:
  964. return
  965. shiftConfig = self.mainWidget.db.getShiftConfigItems()
  966. self.updateItem(shiftConfig[index])
  967. self.mainWidget.db.setShiftConfigItems(shiftConfig)
  968. name = "%d \"%s\"" % (index + 1, shiftConfig[index].name)
  969. self.itemList.item(index).setText(name)
  970. def addItem(self):
  971. shiftConfig = self.mainWidget.db.getShiftConfigItems()
  972. index = self.itemList.currentRow()
  973. if index < 0:
  974. index = 0
  975. else:
  976. index += 1
  977. item = ShiftConfigItem("Unbenannt", SHIFT_DAY, 7.0, 0.75, 8.0)
  978. shiftConfig.insert(index, item)
  979. self.mainWidget.db.setShiftConfigItems(shiftConfig)
  980. self.loadConfig()
  981. self.itemList.setCurrentRow(index)
  982. def removeItem(self):
  983. index = self.itemList.currentRow()
  984. if index < 0:
  985. return
  986. for snapshot in self.mainWidget.db.getAllSnapshots():
  987. if snapshot.shiftConfigIndex >= self.itemList.count() - 1:
  988. dateString = snapshot.date.toString("dd.MM.yyyy")
  989. QMessageBox.critical(self, "Eintrag wird verwendet",
  990. "Der Eintrag wird von einem Schnappschuss (%s) verwendet. "
  991. "Loeschen nicht moeglich." % dateString)
  992. return
  993. res = QMessageBox.question(self, "Eintrag loeschen?",
  994. "'%s' loeschen?" % self.itemList.item(index).text(),
  995. QMessageBox.Yes | QMessageBox.No)
  996. if res != QMessageBox.Yes:
  997. return
  998. shiftConfig = self.mainWidget.db.getShiftConfigItems()
  999. shiftConfig.pop(index)
  1000. self.mainWidget.db.setShiftConfigItems(shiftConfig)
  1001. self.loadConfig()
  1002. if index >= self.itemList.count() and index > 0:
  1003. index -= 1
  1004. self.itemList.setCurrentRow(index)
  1005. class EnhancedDialog(QDialog):
  1006. def __init__(self, mainWidget):
  1007. QDialog.__init__(self, mainWidget)
  1008. self.mainWidget = mainWidget
  1009. date = mainWidget.calendar.selectedDate()
  1010. dayFlags = mainWidget.db.getDayFlags(date)
  1011. self.setWindowTitle("Erweitert")
  1012. self.setLayout(QGridLayout())
  1013. if isAndroid:
  1014. self.layout().setSpacing(50)
  1015. self.commentGroup = QGroupBox("Kommentar", self)
  1016. self.commentGroup.setLayout(QGridLayout())
  1017. self.layout().addWidget(self.commentGroup, 0, 0)
  1018. self.comment = QTextEdit(self)
  1019. self.commentGroup.layout().addWidget(self.comment, 0, 0)
  1020. self.comment.document().setPlainText(mainWidget.getCommentFor(date))
  1021. self.flagsGroup = QGroupBox("Tagesoptionen", self)
  1022. self.flagsGroup.setLayout(QGridLayout())
  1023. self.layout().addWidget(self.flagsGroup, 1, 0)
  1024. self.uncertainCheckBox = QCheckBox("Unbestaetigt", self)
  1025. self.flagsGroup.layout().addWidget(self.uncertainCheckBox, 0, 0)
  1026. cs = Qt.Checked if dayFlags & DFLAG_UNCERTAIN else Qt.Unchecked
  1027. self.uncertainCheckBox.setCheckState(cs)
  1028. self.attendantCheckBox = QCheckBox("Anwesenheitsmarker", self)
  1029. self.flagsGroup.layout().addWidget(self.attendantCheckBox, 1, 0)
  1030. cs = Qt.Checked if dayFlags & DFLAG_ATTENDANT else Qt.Unchecked
  1031. self.attendantCheckBox.setCheckState(cs)
  1032. self.uncertainCheckBox.stateChanged.connect(self.__flagCheckBoxChanged)
  1033. self.attendantCheckBox.stateChanged.connect(self.__flagCheckBoxChanged)
  1034. def __flagCheckBoxChanged(self, newState):
  1035. self.commitAndAccept()
  1036. def closeEvent(self, e):
  1037. self.commitAndAccept()
  1038. def commitAndAccept(self):
  1039. self.commit()
  1040. self.accept()
  1041. def commit(self):
  1042. date = self.mainWidget.calendar.selectedDate()
  1043. dayFlags = oldDayFlags = self.mainWidget.getDayFlags(date)
  1044. for checkBox, flag in ((self.uncertainCheckBox, DFLAG_UNCERTAIN),
  1045. (self.attendantCheckBox, DFLAG_ATTENDANT)):
  1046. dayFlags &= ~flag
  1047. dayFlags |= flag if checkBox.checkState() == Qt.Checked else 0
  1048. if dayFlags != oldDayFlags:
  1049. self.mainWidget.setDayFlags(date, dayFlags)
  1050. if dayFlags & DFLAG_ATTENDANT:
  1051. # Automatically reset day type, if attendant flag was set.
  1052. self.mainWidget.setDayType(date, DTYPE_DEFAULT)
  1053. old = self.mainWidget.getCommentFor(date)
  1054. new = self.comment.document().toPlainText()
  1055. if old != new:
  1056. self.mainWidget.setCommentFor(date, new)
  1057. self.mainWidget.recalculate()
  1058. class ManageDialog(QDialog):
  1059. def __init__(self, mainWidget):
  1060. QDialog.__init__(self, mainWidget)
  1061. self.mainWidget = mainWidget
  1062. self.setWindowTitle("Verwalten")
  1063. self.setLayout(QGridLayout())
  1064. if isAndroid:
  1065. self.layout().setSpacing(50)
  1066. self.fileGroup = QGroupBox("Datei", self)
  1067. self.fileGroup.setLayout(QGridLayout())
  1068. self.layout().addWidget(self.fileGroup, 0, 0)
  1069. self.setDbButton = QPushButton("Datenbank waehlen", self)
  1070. self.fileGroup.layout().addWidget(self.setDbButton, 0, 0)
  1071. self.setDbButton.released.connect(self.loadDatabase)
  1072. self.resetCalButton = QPushButton("Kalender loeschen", self)
  1073. self.fileGroup.layout().addWidget(self.resetCalButton, 1, 0)
  1074. self.resetCalButton.released.connect(self.resetCalendar)
  1075. self.schedConfButton = QPushButton("Schichtkonfig", self)
  1076. self.fileGroup.layout().addWidget(self.schedConfButton, 2, 0)
  1077. self.schedConfButton.released.connect(self.doShiftConfig)
  1078. self.icalButton = QPushButton("iCalendar import", self)
  1079. self.fileGroup.layout().addWidget(self.icalButton, 3, 0)
  1080. self.icalButton.released.connect(self.icalImport)
  1081. def loadDatabase(self):
  1082. self.mainWidget.loadDatabase()
  1083. self.accept()
  1084. def resetCalendar(self):
  1085. res = QMessageBox.question(self, "Kalender loeschen?",
  1086. "Wollen Sie wirklich alle Kalendereintraege "
  1087. "und Parameter loeschen?",
  1088. QMessageBox.Yes | QMessageBox.No)
  1089. if res == QMessageBox.Yes:
  1090. self.mainWidget.resetState()
  1091. self.accept()
  1092. def doShiftConfig(self):
  1093. dlg = ShiftConfigDialog(self.mainWidget)
  1094. dlg.exec_()
  1095. self.mainWidget.worldUpdate()
  1096. self.accept()
  1097. def icalImport(self):
  1098. dlg = ICalImportDialog(self.mainWidget, self.mainWidget.db)
  1099. dlg.exec_()
  1100. self.mainWidget.worldUpdate()
  1101. self.accept()
  1102. class PresetDialog(QDialog):
  1103. def __init__(self, mainWidget):
  1104. QDialog.__init__(self, mainWidget)
  1105. self.setWindowTitle("Vorgaben")
  1106. self.setLayout(QGridLayout())
  1107. self.mainWidget = mainWidget
  1108. if isAndroid:
  1109. self.layout().setSpacing(50)
  1110. self.presetList = QListWidget(self)
  1111. self.layout().addWidget(self.presetList, 0, 0, 4, 2)
  1112. self.addButton = QPushButton("Neu", self)
  1113. self.layout().addWidget(self.addButton, 4, 0)
  1114. self.removeButton = QPushButton("Loeschen", self)
  1115. self.layout().addWidget(self.removeButton, 4, 1)
  1116. self.commitButton = QPushButton("Vorgabe uebernehmen", self)
  1117. self.layout().addWidget(self.commitButton, 5, 0, 1, 2)
  1118. self.nameEdit = QLineEdit(self)
  1119. self.layout().addWidget(self.nameEdit, 0, 2)
  1120. self.typeCombo = DayTypeComboBox(self)
  1121. self.layout().addWidget(self.typeCombo, 1, 2)
  1122. self.shiftCombo = ShiftComboBox(self)
  1123. self.layout().addWidget(self.shiftCombo, 2, 2)
  1124. self.workTime = TimeSpinBox(self, prefix="Arb.zeit")
  1125. self.layout().addWidget(self.workTime, 3, 2)
  1126. self.breakTime = TimeSpinBox(self, prefix="Pause")
  1127. self.layout().addWidget(self.breakTime, 4, 2)
  1128. self.attendanceTime = TimeSpinBox(self, prefix="Anwes.")
  1129. self.layout().addWidget(self.attendanceTime, 5, 2)
  1130. self.presetChangeBlocked = False
  1131. self.loadPresets()
  1132. self.presetSelectionChanged()
  1133. self.presetList.currentRowChanged.connect(self.presetSelectionChanged)
  1134. self.presetList.itemDoubleClicked.connect(self.commitPressed)
  1135. self.addButton.released.connect(self.addPreset)
  1136. self.removeButton.released.connect(self.removePreset)
  1137. self.commitButton.released.connect(self.commitPressed)
  1138. self.nameEdit.textChanged.connect(self.presetChanged)
  1139. self.typeCombo.currentIndexChanged.connect(self.presetChanged)
  1140. self.shiftCombo.currentIndexChanged.connect(self.presetChanged)
  1141. self.workTime.valueChanged.connect(self.presetChanged)
  1142. self.breakTime.valueChanged.connect(self.presetChanged)
  1143. self.attendanceTime.valueChanged.connect(self.presetChanged)
  1144. def __addPreset(self, preset):
  1145. item = QListWidgetItem(preset.name)
  1146. item.setData(Qt.UserRole, Wrapper(preset))
  1147. self.presetList.addItem(item)
  1148. def loadPresets(self):
  1149. date = self.mainWidget.calendar.selectedDate()
  1150. shiftConfigItem = self.mainWidget.getShiftConfigItemForDate(date)
  1151. assert(shiftConfigItem)
  1152. self.presetList.clear()
  1153. self.__addPreset( # index0 => special for reset
  1154. Preset(name="--- zuruecksetzen ---",
  1155. dayType=DTYPE_DEFAULT,
  1156. shift=shiftConfigItem.shift,
  1157. workTime=shiftConfigItem.workTime,
  1158. breakTime=shiftConfigItem.breakTime,
  1159. attendanceTime=shiftConfigItem.attendanceTime
  1160. )
  1161. )
  1162. for preset in self.mainWidget.db.getPresets():
  1163. self.__addPreset(preset)
  1164. self.presetList.setCurrentRow(0)
  1165. def applyPreset(self, preset):
  1166. mainWidget = self.mainWidget
  1167. index = mainWidget.typeCombo.findData(preset.dayType)
  1168. mainWidget.typeCombo.setCurrentIndex(index)
  1169. index = mainWidget.shiftCombo.findData(preset.shift)
  1170. mainWidget.shiftCombo.setCurrentIndex(index)
  1171. mainWidget.workTime.setValue(preset.workTime)
  1172. mainWidget.breakTime.setValue(preset.breakTime)
  1173. mainWidget.attendanceTime.setValue(preset.attendanceTime)
  1174. def __enableEdit(self, enable):
  1175. self.nameEdit.setEnabled(enable)
  1176. self.typeCombo.setEnabled(enable)
  1177. self.shiftCombo.setEnabled(enable)
  1178. self.workTime.setEnabled(enable)
  1179. self.breakTime.setEnabled(enable)
  1180. self.attendanceTime.setEnabled(enable)
  1181. def presetSelectionChanged(self):
  1182. index = self.presetList.currentRow()
  1183. self.__enableEdit(index > 0)
  1184. self.removeButton.setEnabled(index > 0)
  1185. self.commitButton.setEnabled(index >= 0)
  1186. item = self.presetList.currentItem()
  1187. if not item:
  1188. return
  1189. preset = item.data(Qt.UserRole).obj
  1190. self.presetChangeBlocked = True
  1191. self.nameEdit.setText(preset.name)
  1192. index = self.typeCombo.findData(preset.dayType)
  1193. self.typeCombo.setCurrentIndex(index)
  1194. index = self.shiftCombo.findData(preset.shift)
  1195. self.shiftCombo.setCurrentIndex(index)
  1196. self.workTime.setValue(preset.workTime)
  1197. self.breakTime.setValue(preset.breakTime)
  1198. self.attendanceTime.setValue(preset.attendanceTime)
  1199. self.presetChangeBlocked = False
  1200. def __updatePresetItem(self, preset):
  1201. preset.name = self.nameEdit.text()
  1202. index = self.typeCombo.currentIndex()
  1203. preset.dayType = self.typeCombo.itemData(index)
  1204. index = self.shiftCombo.currentIndex()
  1205. preset.shift = self.shiftCombo.itemData(index)
  1206. preset.workTime = self.workTime.value()
  1207. preset.breakTime = self.breakTime.value()
  1208. preset.attendanceTime = self.attendanceTime.value()
  1209. def presetChanged(self):
  1210. if self.presetChangeBlocked:
  1211. return
  1212. row = self.presetList.currentRow()
  1213. if row <= 0:
  1214. return
  1215. item = self.presetList.item(row)
  1216. item.setText(self.nameEdit.text())
  1217. self.__updatePresetItem(item.data(Qt.UserRole).obj)
  1218. presets = self.mainWidget.db.getPresets()
  1219. self.__updatePresetItem(presets[row - 1])
  1220. self.mainWidget.db.setPresets(presets)
  1221. def addPreset(self):
  1222. presets = self.mainWidget.db.getPresets()
  1223. index = self.presetList.currentRow()
  1224. if index <= 0:
  1225. index = 1
  1226. else:
  1227. index += 1
  1228. date = self.mainWidget.calendar.selectedDate()
  1229. shiftConfigItem = self.mainWidget.getShiftConfigItemForDate(date)
  1230. preset = Preset(name="Unbenannt",
  1231. dayType=DTYPE_DEFAULT,
  1232. shift=shiftConfigItem.shift,
  1233. workTime=shiftConfigItem.workTime,
  1234. breakTime=shiftConfigItem.breakTime,
  1235. attendanceTime=shiftConfigItem.attendanceTime
  1236. )
  1237. presets.insert(index - 1, preset)
  1238. self.mainWidget.db.setPresets(presets)
  1239. self.loadPresets()
  1240. self.presetList.setCurrentRow(index)
  1241. def removePreset(self):
  1242. index = self.presetList.currentRow()
  1243. if index <= 0:
  1244. return
  1245. res = QMessageBox.question(self, "Vorgabe loeschen?",
  1246. "'%s' loeschen?" % self.presetList.item(index).text(),
  1247. QMessageBox.Yes | QMessageBox.No)
  1248. if res != QMessageBox.Yes:
  1249. return
  1250. presets = self.mainWidget.db.getPresets()
  1251. presets.pop(index - 1)
  1252. self.mainWidget.db.setPresets(presets)
  1253. self.loadPresets()
  1254. if index >= self.presetList.count() and index > 0:
  1255. index -= 1
  1256. self.presetList.setCurrentRow(index)
  1257. def commitPressed(self):
  1258. item = self.presetList.currentItem()
  1259. if item:
  1260. preset = item.data(Qt.UserRole).obj
  1261. self.applyPreset(preset)
  1262. self.accept()
  1263. class SnapshotDialog(QDialog):
  1264. def __init__(self, mainWidget, accountState):
  1265. QDialog.__init__(self, mainWidget)
  1266. self.setWindowTitle("Schnappschuss setzen")
  1267. self.setLayout(QGridLayout())
  1268. self.mainWidget = mainWidget
  1269. self.date = accountState.date
  1270. if isAndroid:
  1271. self.layout().setSpacing(50)
  1272. self.dateLabel = QLabel(self.date.toString("dddd, dd.MM.yyyy"), self)
  1273. self.layout().addWidget(self.dateLabel, 0, 0)
  1274. l = QLabel("Startschicht:", self)
  1275. self.layout().addWidget(l, 1, 0)
  1276. self.shiftConfig = QComboBox(self)
  1277. shiftConfig = mainWidget.db.getShiftConfigItems()
  1278. assert(shiftConfig)
  1279. for index, cfg in enumerate(shiftConfig):
  1280. name = "%d \"%s\"" % (index + 1, cfg.name)
  1281. self.shiftConfig.addItem(name, index)
  1282. self.layout().addWidget(self.shiftConfig, 1, 1)
  1283. index = self.shiftConfig.findData(accountState.shiftConfigIndex)
  1284. if index >= 0:
  1285. self.shiftConfig.setCurrentIndex(index)
  1286. l = QLabel("Zeitkontostand:", self)
  1287. self.layout().addWidget(l, 2, 0)
  1288. self.accountValue = TimeSpinBox(self,
  1289. val=accountState.accountAtStartOfDay,
  1290. minVal=-1000.0, maxVal=1000.0,
  1291. step=0.1, decimals=1)
  1292. self.layout().addWidget(self.accountValue, 2, 1)
  1293. l = QLabel("Urlaubsstand:", self)
  1294. self.layout().addWidget(l, 3, 0)
  1295. self.holidays = DaySpinBox(self,
  1296. val=accountState.holidaysAtStartOfDay)
  1297. self.layout().addWidget(self.holidays, 3, 1)
  1298. self.removeButton = QPushButton("Schnappschuss loeschen", self)
  1299. self.layout().addWidget(self.removeButton, 4, 0, 1, 2)
  1300. self.removeButton.released.connect(self.removeSnapshot)
  1301. if not mainWidget.dateHasSnapshot(self.date):
  1302. self.removeButton.hide()
  1303. self.okButton = QPushButton("Setzen", self)
  1304. self.layout().addWidget(self.okButton, 5, 0)
  1305. self.okButton.released.connect(self.ok)
  1306. self.cancelButton = QPushButton("Abbrechen", self)
  1307. self.layout().addWidget(self.cancelButton, 5, 1)
  1308. self.cancelButton.released.connect(self.cancel)
  1309. def ok(self):
  1310. self.accept()
  1311. def cancel(self):
  1312. self.reject()
  1313. def removeSnapshot(self):
  1314. res = QMessageBox.question(self, "Schnappschuss loeschen?",
  1315. "Wollen Sie den Schnappschuss wirklich loeschen?",
  1316. QMessageBox.Yes | QMessageBox.No)
  1317. if res == QMessageBox.Yes:
  1318. self.mainWidget.removeSnapshot(self.date)
  1319. self.reject()
  1320. def getSnapshot(self):
  1321. index = self.shiftConfig.currentIndex()
  1322. shiftConfigIndex = self.shiftConfig.itemData(index)
  1323. accValue = self.accountValue.value()
  1324. holidaysLeft = self.holidays.value()
  1325. return Snapshot(self.date, shiftConfigIndex,
  1326. accValue, holidaysLeft)
  1327. class Calendar(QCalendarWidget):
  1328. def __init__(self, mainWidget):
  1329. self.__initPens()
  1330. QCalendarWidget.__init__(self, mainWidget)
  1331. self.mainWidget = mainWidget
  1332. self.setFirstDayOfWeek(Qt.Monday)
  1333. self.today = QDate.currentDate()
  1334. self.armTodayTimer()
  1335. def todayTimer(self):
  1336. self.today = self.today.addDays(1)
  1337. self.setSelectedDate(self.today)
  1338. self.armTodayTimer()
  1339. self.redraw()
  1340. def armTodayTimer(self):
  1341. tomorrow = QDateTime(self.today)
  1342. tomorrow = tomorrow.addDays(1)
  1343. secs = QDateTime.currentDateTime().secsTo(tomorrow)
  1344. QTimer.singleShot(secs * 1000, self.todayTimer)
  1345. def __initPens(self):
  1346. self.snapshotPen = QPen(QColor("#007FFF"))
  1347. self.snapshotPen.setWidth(5)
  1348. self.framePen = QPen(QColor("#006400"))
  1349. self.framePen.setWidth(1)
  1350. self.todayPen = QPen(QColor("#006400"))
  1351. self.todayPen.setWidth(6)
  1352. self.commentPen = QPen(QColor("#FF0000"))
  1353. self.commentPen.setWidth(2)
  1354. self.overridesPen = QPen(QColor("#9F9F9F"))
  1355. self.overridesPen.setWidth(20 if isAndroid else 5)
  1356. self.centerPen = QPen(QColor("#007FFF"))
  1357. self.centerPen.setWidth(1)
  1358. self.lowerLeftPen = QPen(QColor("#FF0000"))
  1359. self.lowerLeftPen.setWidth(1)
  1360. self.lowerRightPen = QPen(QColor("#304F7F"))
  1361. self.lowerRightPen.setWidth(1)
  1362. typeLetter = {
  1363. DTYPE_DEFAULT : None,
  1364. DTYPE_COMPTIME : "Z",
  1365. DTYPE_HOLIDAY : "U",
  1366. DTYPE_SHORTTIME : "C",
  1367. DTYPE_FEASTDAY : "F",
  1368. }
  1369. shiftLetter = {
  1370. SHIFT_EARLY : "F",
  1371. SHIFT_LATE : "S",
  1372. SHIFT_NIGHT : "N",
  1373. SHIFT_DAY : "O",
  1374. }
  1375. def paintCell(self, painter, rect, date):
  1376. QCalendarWidget.paintCell(self, painter, rect, date)
  1377. painter.save()
  1378. mainWidget, font = self.mainWidget, painter.font()
  1379. db = mainWidget.db
  1380. rx, ry, rw, rh = rect.x(), rect.y(), rect.width(), rect.height()
  1381. dayFlags = db.getDayFlags(date)
  1382. font.setBold(True)
  1383. painter.setFont(font)
  1384. if mainWidget.dateHasSnapshot(date):
  1385. painter.setPen(self.snapshotPen)
  1386. else:
  1387. painter.setPen(self.framePen)
  1388. painter.drawRect(rx, ry, rw - 1, rh - 1)
  1389. if date == self.today:
  1390. painter.setPen(self.todayPen)
  1391. for (x, y) in ((3, 3), (rw - 3, 3),
  1392. (3, rh - 3),
  1393. (rw - 3, rh - 3)):
  1394. painter.drawPoint(rx + x, ry + y)
  1395. if mainWidget.dateHasComment(date):
  1396. painter.setPen(self.commentPen)
  1397. painter.drawRect(rx + 3, ry + 3, rw - 3 - 3, rh - 3 - 3)
  1398. if mainWidget.dateHasTimeOverrides(date):
  1399. painter.setPen(self.overridesPen)
  1400. painter.drawPoint(rx + rw - 8, ry + 8)
  1401. text = self.typeLetter[mainWidget.getDayType(date)]
  1402. if not text:
  1403. if dayFlags & DFLAG_ATTENDANT:
  1404. text = "A"
  1405. if text:
  1406. painter.setPen(self.lowerLeftPen)
  1407. painter.drawText(rx + 4, ry + rh - 4, text)
  1408. shiftOverride = db.getShiftOverride(date)
  1409. if shiftOverride is not None:
  1410. text = self.shiftLetter[shiftOverride]
  1411. painter.setPen(self.lowerRightPen)
  1412. metrics = QFontMetrics(painter.font())
  1413. painter.drawText(rx + rw - metrics.width(text) - 4,
  1414. ry + rh - 4,
  1415. text)
  1416. if dayFlags & DFLAG_UNCERTAIN:
  1417. text = "???"
  1418. painter.setPen(self.centerPen)
  1419. metrics = QFontMetrics(painter.font())
  1420. painter.drawText(rx + rw // 2 - metrics.width(text) // 2,
  1421. ry + rh // 2 + metrics.height() // 2,
  1422. text)
  1423. painter.restore()
  1424. def redraw(self):
  1425. if self.isVisible():
  1426. self.hide()
  1427. self.show()
  1428. class AccountState(QObject):
  1429. "Calculated account state."
  1430. def __init__(self, date, shiftConfigIndex=0,
  1431. accountAtStartOfDay=0.0, accountAtEndOfDay=0.0,
  1432. holidaysAtStartOfDay=0, holidaysAtEndOfDay=0):
  1433. QObject.__init__(self)
  1434. self.date = date
  1435. self.shiftConfigIndex = shiftConfigIndex
  1436. self.accountAtStartOfDay = accountAtStartOfDay
  1437. self.accountAtEndOfDay = accountAtEndOfDay
  1438. self.holidaysAtStartOfDay = holidaysAtStartOfDay
  1439. self.holidaysAtEndOfDay = holidaysAtEndOfDay
  1440. class MainWidget(QWidget):
  1441. def __init__(self, parent=None):
  1442. QWidget.__init__(self, parent)
  1443. self.setFocusPolicy(Qt.StrongFocus)
  1444. self.setLayout(QGridLayout())
  1445. if isAndroid:
  1446. self.layout().setSpacing(50)
  1447. self.worldUpdateTimer = QTimer(self)
  1448. self.worldUpdateTimer.setSingleShot(True)
  1449. self.worldUpdateTimer.timeout.connect(self.__worldUpdateTimerTimeout)
  1450. vbox = QVBoxLayout()
  1451. self.workTime = TimeSpinBox(self, prefix="Arb.zeit")
  1452. vbox.addWidget(self.workTime)
  1453. self.breakTime = TimeSpinBox(self, prefix="Pause")
  1454. vbox.addWidget(self.breakTime)
  1455. self.attendanceTime = TimeSpinBox(self, prefix="Anwes.")
  1456. vbox.addWidget(self.attendanceTime)
  1457. self.layout().addLayout(vbox, 0, 0)
  1458. vbox = QVBoxLayout()
  1459. self.typeCombo = DayTypeComboBox(self)
  1460. vbox.addWidget(self.typeCombo)
  1461. self.shiftCombo = ShiftComboBox(self)
  1462. vbox.addWidget(self.shiftCombo)
  1463. self.presetButton = QPushButton("Vorgaben", self)
  1464. vbox.addWidget(self.presetButton)
  1465. self.presetButton.released.connect(self.doPresets)
  1466. self.layout().addLayout(vbox, 0, 1)
  1467. self.calendar = Calendar(self)
  1468. self.layout().addWidget(self.calendar, 3, 0, 1, 2)
  1469. self.calendar.selectionChanged.connect(self.recalculate)
  1470. self.typeCombo.currentIndexChanged.connect(self.overrideChanged)
  1471. self.shiftCombo.currentIndexChanged.connect(self.overrideChanged)
  1472. self.workTime.valueChanged.connect(self.overrideChanged)
  1473. self.breakTime.valueChanged.connect(self.overrideChanged)
  1474. self.attendanceTime.valueChanged.connect(self.overrideChanged)
  1475. self.overrideChangeBlocked = False
  1476. hbox = QHBoxLayout()
  1477. self.manageButton = QPushButton("Verwalten", self)
  1478. hbox.addWidget(self.manageButton)
  1479. self.manageButton.released.connect(self.doManage)
  1480. self.snapshotButton = QPushButton("Schnappschuss", self)
  1481. hbox.addWidget(self.snapshotButton)
  1482. self.snapshotButton.released.connect(self.doSnapshot)
  1483. self.enhancedButton = QPushButton("Erweitert", self)
  1484. hbox.addWidget(self.enhancedButton)
  1485. self.enhancedButton.released.connect(self.doEnhanced)
  1486. self.layout().addLayout(hbox, 4, 0, 1, 2)
  1487. self.output = QLabel(self)
  1488. self.output.setAlignment(Qt.AlignHCenter)
  1489. self.output.setFrameShape(QFrame.Panel)
  1490. self.output.setFrameShadow(QFrame.Raised)
  1491. self.layout().addWidget(self.output, 5, 0, 1, 2)
  1492. self.db = TsDatabase()
  1493. self.resetState()
  1494. def shutdown(self):
  1495. self.db.close()
  1496. def resetState(self):
  1497. self.db.resetDatabase()
  1498. self.worldUpdate()
  1499. def worldUpdate(self):
  1500. self.updateTitle()
  1501. self.recalculate()
  1502. self.calendar.redraw()
  1503. def __worldUpdateTimerTimeout(self):
  1504. self.worldUpdate()
  1505. def scheduleWorldUpdate(self, msec=1000):
  1506. self.worldUpdateTimer.start(msec)
  1507. def doLoadDatabase(self, filename, quiet=False):
  1508. try:
  1509. print("Loading database: %s" % filename)
  1510. fi = QFileInfo(filename)
  1511. if not fi.exists() and self.db.isInMemory():
  1512. # Clone the in-memory DB to the new file
  1513. self.db.clone(filename)
  1514. self.db.open(filename)
  1515. self.worldUpdate()
  1516. except Exception as e:
  1517. if not quiet:
  1518. QMessageBox.critical(self, "Laden fehlgeschlagen",
  1519. "Laden fehlgeschlagen:\n" + str(e))
  1520. return False
  1521. return True
  1522. def loadDatabase(self):
  1523. fn, fil = QFileDialog.getSaveFileName(self,
  1524. "Datenbank laden", "",
  1525. "Timeshift Dateien (*.tmd *.tms *.tmz);;"
  1526. "Alle Dateien (*)",
  1527. "",
  1528. QFileDialog.DontConfirmOverwrite)
  1529. if fn:
  1530. self.doLoadDatabase(fn)
  1531. def updateTitle(self):
  1532. if self.db.isInMemory():
  1533. suffix = "<in memory>"
  1534. else:
  1535. fi = QFileInfo(self.db.getFilename())
  1536. suffix = fi.fileName()
  1537. self.parent().setTitleSuffix(suffix)
  1538. def dateHasComment(self, date):
  1539. return self.db.hasComment(date)
  1540. def getCommentFor(self, date):
  1541. comment = self.db.getComment(date)
  1542. return comment if comment else ""
  1543. def setCommentFor(self, date, text):
  1544. self.db.setComment(date, text)
  1545. def doEnhanced(self):
  1546. dlg = EnhancedDialog(self)
  1547. dlg.exec_()
  1548. def doManage(self):
  1549. dlg = ManageDialog(self)
  1550. dlg.exec_()
  1551. def doPresets(self):
  1552. dlg = PresetDialog(self)
  1553. dlg.exec_()
  1554. def __removeSnapshot(self, date):
  1555. self.db.setSnapshot(date, None)
  1556. def removeSnapshot(self, date):
  1557. self.__removeSnapshot(date)
  1558. self.worldUpdate()
  1559. def __setSnapshot(self, snapshot):
  1560. self.db.setSnapshot(snapshot.date, snapshot)
  1561. def doSnapshot(self):
  1562. if not self.db.getShiftConfigItems():
  1563. QMessageBox.critical(self, "Kein Schichtsystem",
  1564. "Kein Schichtsystem konfiguriert")
  1565. return
  1566. date = self.calendar.selectedDate()
  1567. snapshot = self.getSnapshotFor(date)
  1568. accState = AccountState(date)
  1569. if snapshot is None:
  1570. # Calculate the account state w.r.t. the
  1571. # last shapshot.
  1572. snapshot = self.db.findSnapshotForDate(date)
  1573. if snapshot:
  1574. accState = self.__calcAccountState(
  1575. snapshot, date)
  1576. else:
  1577. # We already have a snapshot on that day. Modify it.
  1578. accState.shiftConfigIndex = snapshot.shiftConfigIndex
  1579. accState.accountAtStartOfDay = snapshot.accountValue
  1580. accState.holidaysAtStartOfDay = snapshot.holidaysLeft
  1581. dlg = SnapshotDialog(self, accState)
  1582. if dlg.exec_():
  1583. self.__setSnapshot(dlg.getSnapshot())
  1584. self.worldUpdate()
  1585. def dateHasSnapshot(self, date):
  1586. return self.db.hasSnapshot(date)
  1587. def getSnapshotFor(self, date):
  1588. return self.db.getSnapshot(date)
  1589. def overrideChanged(self):
  1590. if self.overrideChangeBlocked or not self.db.getShiftConfigItems():
  1591. return
  1592. date = self.calendar.selectedDate()
  1593. shiftConfigItem = self.getShiftConfigItemForDate(date)
  1594. assert(shiftConfigItem)
  1595. # Day type
  1596. index = self.typeCombo.currentIndex()
  1597. self.setDayType(date, self.typeCombo.itemData(index))
  1598. # Shift override
  1599. index = self.shiftCombo.currentIndex()
  1600. shift = self.shiftCombo.itemData(index)
  1601. if shift == shiftConfigItem.shift:
  1602. shift = None
  1603. self.setShiftOverride(date, shift)
  1604. # Work time override
  1605. workTime = self.workTime.value()
  1606. if floatEqual(workTime, shiftConfigItem.workTime):
  1607. workTime = None
  1608. self.setWorkTimeOverride(date, workTime)
  1609. # Break time override
  1610. breakTime = self.breakTime.value()
  1611. if floatEqual(breakTime, shiftConfigItem.breakTime):
  1612. breakTime = None
  1613. self.setBreakTimeOverride(date, breakTime)
  1614. # Attendance time override
  1615. attendanceTime = self.attendanceTime.value()
  1616. if floatEqual(attendanceTime, shiftConfigItem.attendanceTime):
  1617. attendanceTime = None
  1618. self.setAttendanceTimeOverride(date, attendanceTime)
  1619. self.scheduleWorldUpdate()
  1620. def getShiftConfigIndexForDate(self, date):
  1621. # Find the shift config index that's valid for the date.
  1622. # May return -1 on error.
  1623. snapshot = self.db.findSnapshotForDate(date)
  1624. if not snapshot:
  1625. return -1
  1626. daysBetween = snapshot.date.daysTo(date)
  1627. assert(daysBetween >= 0)
  1628. index = snapshot.shiftConfigIndex
  1629. index += daysBetween
  1630. index %= len(self.db.getShiftConfigItems())
  1631. return index
  1632. def getShiftConfigItemForDate(self, date):
  1633. index = self.getShiftConfigIndexForDate(date)
  1634. if index >= 0:
  1635. return self.db.getShiftConfigItems()[index]
  1636. return None
  1637. def enableOverrideControls(self, enable):
  1638. self.typeCombo.setEnabled(enable)
  1639. self.shiftCombo.setEnabled(enable)
  1640. self.workTime.setEnabled(enable)
  1641. self.breakTime.setEnabled(enable)
  1642. self.attendanceTime.setEnabled(enable)
  1643. self.presetButton.setEnabled(enable)
  1644. def getDayType(self, date):
  1645. dtype = self.db.getDayTypeOverride(date)
  1646. return dtype if dtype is not None else DTYPE_DEFAULT
  1647. def setDayType(self, date, dtype):
  1648. dtype = dtype if dtype != DTYPE_DEFAULT else None
  1649. self.db.setDayTypeOverride(date, dtype)
  1650. if dtype:
  1651. # Automatically clear attendant flag, if a dtype is set.
  1652. self.setDayFlags(date, self.getDayFlags(date) & ~DFLAG_ATTENDANT)
  1653. def getDayFlags(self, date):
  1654. return self.db.getDayFlags(date)
  1655. def setDayFlags(self, date, newFlags):
  1656. self.db.setDayFlags(date, newFlags)
  1657. def setShiftOverride(self, date, shift):
  1658. self.db.setShiftOverride(date, shift)
  1659. def getRealShift(self, date, shiftConfigItem):
  1660. shift = self.db.getShiftOverride(date)
  1661. return shift if shift is not None else shiftConfigItem.shift
  1662. def setWorkTimeOverride(self, date, workTime):
  1663. self.db.setWorkTimeOverride(date, workTime)
  1664. def getRealWorkTime(self, date, shiftConfigItem):
  1665. time = self.db.getWorkTimeOverride(date)
  1666. return time if time is not None else shiftConfigItem.workTime
  1667. def setBreakTimeOverride(self, date, breakTime):
  1668. self.db.setBreakTimeOverride(date, breakTime)
  1669. def getRealBreakTime(self, date, shiftConfigItem):
  1670. time = self.db.getBreakTimeOverride(date)
  1671. return time if time is not None else shiftConfigItem.breakTime
  1672. def setAttendanceTimeOverride(self, date, attendanceTime):
  1673. self.db.setAttendanceTimeOverride(date, attendanceTime)
  1674. def getRealAttendanceTime(self, date, shiftConfigItem):
  1675. time = self.db.getAttendanceTimeOverride(date)
  1676. return time if time is not None else shiftConfigItem.attendanceTime
  1677. def dateHasTimeOverrides(self, date):
  1678. return self.db.hasAttendanceTimeOverride(date) or\
  1679. self.db.hasWorkTimeOverride(date) or\
  1680. self.db.hasBreakTimeOverride(date)
  1681. def __calcAccountState(self, snapshot, endDate):
  1682. shiftConfig = self.db.getShiftConfigItems()
  1683. nrShiftConfigs = len(shiftConfig)
  1684. state = AccountState(
  1685. date = snapshot.date,
  1686. shiftConfigIndex = snapshot.shiftConfigIndex,
  1687. accountAtStartOfDay = snapshot.accountValue,
  1688. accountAtEndOfDay = snapshot.accountValue,
  1689. holidaysAtStartOfDay = snapshot.holidaysLeft,
  1690. holidaysAtEndOfDay = snapshot.holidaysLeft
  1691. )
  1692. assert(state.date <= endDate)
  1693. while True:
  1694. shiftConfigItem = shiftConfig[state.shiftConfigIndex]
  1695. currentShift = self.getRealShift(state.date, shiftConfigItem)
  1696. workTime = self.getRealWorkTime(state.date, shiftConfigItem)
  1697. breakTime = self.getRealBreakTime(state.date, shiftConfigItem)
  1698. attendanceTime = self.getRealAttendanceTime(state.date, shiftConfigItem)
  1699. dtype = self.getDayType(state.date)
  1700. if dtype == DTYPE_DEFAULT:
  1701. if attendanceTime > 0.001:
  1702. state.accountAtEndOfDay += attendanceTime
  1703. state.accountAtEndOfDay -= workTime
  1704. state.accountAtEndOfDay -= breakTime
  1705. elif dtype == DTYPE_COMPTIME:
  1706. state.accountAtEndOfDay -= workTime
  1707. elif dtype == DTYPE_HOLIDAY:
  1708. state.holidaysAtEndOfDay -= 1
  1709. elif dtype in (DTYPE_FEASTDAY, DTYPE_SHORTTIME):
  1710. pass # no change
  1711. else:
  1712. assert(0)
  1713. if state.date == endDate:
  1714. break
  1715. state.date = state.date.addDays(1)
  1716. state.shiftConfigIndex = (state.shiftConfigIndex + 1) % nrShiftConfigs
  1717. state.accountAtStartOfDay = state.accountAtEndOfDay
  1718. state.holidaysAtStartOfDay = state.holidaysAtEndOfDay
  1719. return state
  1720. def recalculate(self):
  1721. selDate = self.calendar.selectedDate()
  1722. shiftConfig = self.db.getShiftConfigItems()
  1723. if not shiftConfig:
  1724. self.output.setText("Kein Schichtsystem konfiguriert")
  1725. self.enableOverrideControls(False)
  1726. return
  1727. # First find the next snapshot.
  1728. snapshot = self.db.findSnapshotForDate(selDate)
  1729. if not snapshot:
  1730. dateString = selDate.toString("dd.MM.yyyy")
  1731. self.output.setText("Kein Schnappschuss vor dem %s gesetzt" %\
  1732. dateString)
  1733. self.enableOverrideControls(False)
  1734. return
  1735. self.enableOverrideControls(True)
  1736. # Then calculate the account state
  1737. accState = self.__calcAccountState(snapshot, selDate)
  1738. shiftConfigItem = shiftConfig[accState.shiftConfigIndex]
  1739. dtype = self.getDayType(selDate)
  1740. shift = self.getRealShift(selDate, shiftConfigItem)
  1741. workTime = self.getRealWorkTime(selDate, shiftConfigItem)
  1742. breakTime = self.getRealBreakTime(selDate, shiftConfigItem)
  1743. attendanceTime = self.getRealAttendanceTime(selDate, shiftConfigItem)
  1744. self.overrideChangeBlocked = True
  1745. self.typeCombo.setCurrentIndex(self.typeCombo.findData(dtype))
  1746. self.shiftCombo.setCurrentIndex(self.shiftCombo.findData(shift))
  1747. self.workTime.setValue(workTime)
  1748. self.breakTime.setValue(breakTime)
  1749. self.attendanceTime.setValue(attendanceTime)
  1750. self.overrideChangeBlocked = False
  1751. dateString = selDate.toString("dd.MM.yyyy")
  1752. self.output.setText("Stand %s: %.1f h -> %.1f h U: %d d" %\
  1753. (dateString, round(accState.accountAtStartOfDay, 1),
  1754. round(accState.accountAtEndOfDay, 1),
  1755. accState.holidaysAtEndOfDay))
  1756. class MainWindow(QMainWindow):
  1757. def __init__(self, parent=None):
  1758. QMainWindow.__init__(self, parent)
  1759. self.titleSuffix = None
  1760. self.__updateTitle()
  1761. self.setCentralWidget(MainWidget(self))
  1762. def loadDatabase(self, filename, quiet=False):
  1763. return self.centralWidget().doLoadDatabase(filename, quiet)
  1764. def __updateTitle(self):
  1765. title = "Zeitkonto"
  1766. if self.titleSuffix:
  1767. title += " - " + self.titleSuffix
  1768. self.setWindowTitle(title)
  1769. def setTitleSuffix(self, suffix):
  1770. self.titleSuffix = suffix
  1771. self.__updateTitle()
  1772. def closeEvent(self, e):
  1773. self.centralWidget().shutdown()
  1774. def listdir(p):
  1775. try:
  1776. return os.listdir(p)
  1777. except Exception as e:
  1778. return []
  1779. def main(argv):
  1780. print("Using PySide: %s" % usingPySide)
  1781. app = QApplication(argv)
  1782. mainwnd = MainWindow()
  1783. if len(argv) == 2 and argv[1].strip():
  1784. if not mainwnd.loadDatabase(argv[1]):
  1785. return 1
  1786. else:
  1787. if not (listdir("/mnt/sdcard") and
  1788. mainwnd.loadDatabase("/mnt/sdcard/timeshift.tmd",
  1789. quiet=True)):
  1790. dbPath = str(pathlib.Path.home() / ".timeshift.tmd")
  1791. if not mainwnd.loadDatabase(dbPath, quiet=True):
  1792. print("Failed to load default database.", file=sys.stderr)
  1793. mainwnd.show()
  1794. return app.exec_()
  1795. if __name__ == "__main__":
  1796. sys.exit(main(sys.argv))