Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
  • master
1 result

Target

Select target project
  • dnn/ptbcli
1 result
Select Git revision
  • master
1 result
Show changes
Commits on Source (4)
...@@ -1050,24 +1050,33 @@ def commit(conf, message): ...@@ -1050,24 +1050,33 @@ def commit(conf, message):
"""Commit the updated ledger to Git.""" """Commit the updated ledger to Git."""
parent = path.dirname(conf['ledger']) parent = path.dirname(conf['ledger'])
if not path.exists(path.join(parent, '.git')):
return
filename = path.basename(conf['ledger']) filename = path.basename(conf['ledger'])
message = message or '{:%Y-%m-%d}'.format(datetime.date.today()) message = message or '{:%Y-%m-%d}'.format(datetime.date.today())
subprocess.run(['git', 'commit', '-m', message, '--', filename], cwd=parent, check=True)
@main.command()
@click.option('--message', '-m', help="Commit message.")
@click.pass_obj
def push(conf, message):
"""Commit the updated ledger to Git."""
parent = path.dirname(conf['ledger'])
if not path.exists(path.join(parent, '.git')): if not path.exists(path.join(parent, '.git')):
return return
subprocess.run(['git', 'commit', '-m', message, '--', filename], subprocess.run(['git', 'push'], cwd=parent, check=True)
cwd=parent, check=True)
@main.command() @main.command()
@click.argument('account', required=False) @click.argument('account', required=False)
@click.option('--verbose', '-v', is_flag=True) @click.option('--verbose', '-v', is_flag=True)
@click.option('--stat', is_flag=True)
@click.option('--plot', is_flag=True)
@click.option('--save', is_flag=True)
@click.pass_obj @click.pass_obj
def summary(conf, account, verbose, stat, plot, save): def summary(conf, account, verbose):
"""Orders status summary.""" """Orders status summary."""
entries, errors, options = loader.load_file(conf['ledger']) entries, errors, options = loader.load_file(conf['ledger'])
...@@ -1109,94 +1118,6 @@ def summary(conf, account, verbose, stat, plot, save): ...@@ -1109,94 +1118,6 @@ def summary(conf, account, verbose, stat, plot, save):
print(table.addfield('days', daysstr) if verbose else table) print(table.addfield('days', daysstr) if verbose else table)
days = [((r.odate or today) - r.date).days for r in table.namedtuples()]
if stat:
mean = statistics.fmean(days)
median = statistics.median(days)
for label, value in (('mean', mean), ('median', median)):
print(f'{label:>{8}s}: {value:5.1f} days')
if plot:
import numpy as np
import matplotlib.pyplot as plt
days = np.array(days)
def stamp(ax):
ax.text(1, 1, '{:%Y-%m-%d}'.format(datetime.date.today()), fontsize='small',
horizontalalignment='right', verticalalignment='bottom', transform=ax.transAxes)
fig, ax = plt.subplots()
ax.barh(np.arange(len(days)), days, align='center', color='white', edgecolor='black', lw=0.5, hatch='////', )
ax.set_xlim(0, 150)
ax.set_ylim(-1, len(days))
ax.invert_yaxis()
ax.set_xlabel('days')
ax.set_yticks([])
stamp(ax)
if save:
fig.savefig('orders-{:%Y%m%d}.pdf'.format(datetime.date.today()))
days.sort()
n = len(days)
x = np.r_[0, days]
fig, ax = plt.subplots()
ax.step(x, np.arange(0, n + 1) / n, where='post', color='black', lw=0.5)
ax.set_xlim(0, 150)
ax.set_ylim(0.0, 1.0)
ax.set_xlabel('days')
stamp(ax)
plt.show()
@main.command()
@click.option('--collect', is_flag=True)
@click.pass_obj
def acks(conf, collect):
"""Check for order acknowledgements."""
entries, errors, options = loader.load_file(conf['ledger'])
assert not errors
accounts = list_designated_accounts(entries)
expr = reopts(accounts)
types, rows = query.run_query(entries, options, f"""
SELECT
account,
entry_meta('requisition') AS rid,
entry_meta('order') AS oid,
entry_meta('documentation') AS documentation
FROM
flag != '*' AND flag != 'P'
WHERE
account ~ '{expr}'
ORDER BY
account
""")
table = petl.pushheader(rows, [name for name, rtype in types])
def ack(r):
for entry in os.listdir(r.documentation):
if m := re.match(r'OA-(\d+)\.\d+\#\d+\.pdf', entry):
if decimal.Decimal(m.group(1)) != r.oid:
raise ValueError(f'{m.group(1)} != {r.oid}')
return path.join(r.documentation, entry)
return path.join(r.documentation, click.style('?', fg='red'))
table = table.addfield('ack', ack).replace('oid', None, '')
print(table.cutout('documentation'))
if collect:
dest = tempfile.mkdtemp(prefix='order-acks-')
for filepath in table.values('ack'):
if path.isfile(filepath):
shutil.copy(filepath, dest)
subprocess.run(["rundll32", "url.dll,FileProtocolHandler", dest])
@main.command() @main.command()
@click.argument('filename', required=False) @click.argument('filename', required=False)
......
import datetime
import unittest
import timetracking
DATA = '''
Abteilung: 4300/1100 Quantenoptik und Längeneinheit Personal Nr.: 02179417
Name, Vorname: Nicolodi, Daniele Dr. Ausweis Nr.: 7463
September 2022
----------------------------------------------------------------------------------------------------------
Datum TM | TE FZ Kommt Geht FZ TE |Abweichung | Soll NtGes DiffNt Saldo
| | |
| | | Tg Tg Tg
----------------------------------------------------------------------------------------------------------
Do 1 502 | |Periodenbeginn: Übertrag Periode +142.37*
1 502 |164 10:45 - 18:09 164 | | 8.15 6.54 -1.21 +141.16
Fr 2 503 |164 10:28 - 17:46 164 | | 6.15 6.48 +0.33 +141.49
Sa 3 951 | | | 0.00 0.00 +0.00 +141.49
Di 4 502 |164 11:10 - 12:13 DRS 164 | | 8.15 8.15 +0.00 +149.45
----------------------------------------------------------------------------------------------------------
Wochensumme 39.00 39.44 +0.44 +141.49
----------------------------------------------------------------------------------------------------------
Mo 5 501 |164 8:39 - 18:46 164 | | 8.00 9.22 +1.22 +143.11
Di 6 502 |164 9:06 - 18:28 164 | | 8.15 8.52 +0.37 +143.48
Mi 7 502 |164 9:05 - 18:41 164 | | 8.15 9.00 +0.45 +144.33
Do 8 502 | # 9:00 - 18:32 164 | | 8.15 9.00 +0.45 +145.18
Fr 9 503 |164 11:05 - 18:24 164 | | 6.15 6.49 +0.34 +145.52
Sa 10 951 | | | 0.00 0.00 +0.00 +145.52
So 11 952 | | | 0.00 0.00 +0.00 +145.52
----------------------------------------------------------------------------------------------------------
Wochensumme 39.00 43.03 +4.03 +145.52
----------------------------------------------------------------------------------------------------------
Mi 12 502 |164 11:31 - | |
Di 13 502 |164 9:18 - 17:31 164 | | 8.15 7.43 -0.32 +146.04
Mi 14 502 |164 8:55 - 17:36 164 | | 8.15 8.11 -0.04 +146.00
Do 15 502 | *BeA |303:Betriebsausflug | 8.15 8.15 +0.00 +146.00
Fr 16 503 |164 8:43 - 20:22 164 | | 6.15 10.00 +3.45 +149.45
Sa 17 951 | | | 0.00 0.00 +0.00 +149.45
So 18 952 | | | 0.00 0.00 +0.00 +149.45
----------------------------------------------------------------------------------------------------------
Wochensumme 39.00 42.53 +3.53 +149.45
----------------------------------------------------------------------------------------------------------
Mo 19 501 | *U |100:Urlaub | 8.00 8.00 +0.00 +149.45
Di 20 502 | *U |100:Urlaub | 8.15 8.15 +0.00 +149.45
Mi 21 502 | *U |100:Urlaub | 8.15 8.15 +0.00 +149.45
Do 22 502 | *U |100:Urlaub | 8.15 8.15 +0.00 +149.45
Fr 23 503 | *U |100:Urlaub | 6.15 6.15 +0.00 +149.45
Mo 24 501 |164 13:12 - | |
24 501 | |Buchung nicht erfolgt | 8.00 0.00 -8.00 +143.17
Sa 24 951 | | | 0.00 0.00 +0.00 +149.45
So 25 952 | | | 0.00 0.00 +0.00 +149.45
----------------------------------------------------------------------------------------------------------
Wochensumme 39.00 39.00 +0.00 +149.45
----------------------------------------------------------------------------------------------------------
Mo 26 501 | *U |100:Urlaub | 8.00 8.00 +0.00 +149.45
Di 27 502 | *U |100:Urlaub | 8.15 8.15 +0.00 +149.45
Mi 28 502 | *U |100:Urlaub | 8.15 8.15 +0.00 +149.45
Do 29 502 | *U |100:Urlaub | 8.15 8.15 +0.00 +149.45
Fr 30 503 | *U |100:Urlaub | 6.15 6.15 +0.00 +149.45
30 503 | |Periodenabschluss: Saldo Periode +149.45*
----------------------------------------------------------------------------------------------------------
anteilige Wochensumme 39.00 39.00 +0.00 +149.45
----------------------------------------------------------------------------------------------------------
Periodensumme 170.30 177.38 +7.08 +149.45
----------------------------------------------------------------------------------------------------------
Urlaub Urlaub Urlaub Urlaub Urlaub Urlaub Urlaub
AnspAlt AnspAkt Gel.Tag GuthAlt GuthAkt GuthGes GuthRst
Pe Pe Pe Pe Pe Pe Pe
--------------------------------------------------------------
+30,00 +30,00 10,00 +0,00 +30,00 +30,00 +30,00
*** Ende Monatsjournal ***
'''
class TestTimesheetParsing(unittest.TestCase):
def test_table_1(self):
timetracking.Timesheet(datetime.date.today(), DATA)
...@@ -56,6 +56,12 @@ class Client: ...@@ -56,6 +56,12 @@ class Client:
r = self.session.post(r.url, data=data) r = self.session.post(r.url, data=data)
r.raise_for_status() r.raise_for_status()
s = bs4.BeautifulSoup(r.text, features='html.parser')
content = s.find(id='FmeContent')
if not content or content['src'] != 'Today.aspx':
raise ValueError('login failed')
self.url = r.url self.url = r.url
def now(self): def now(self):
...@@ -211,6 +217,7 @@ class Tokenizer: ...@@ -211,6 +217,7 @@ class Tokenizer:
class Code(enum.Enum): class Code(enum.Enum):
NONE = '', ' ' NONE = '', ' '
MISSING = '!', '!'
SICK_W_CERT = 'KrA', 'S' SICK_W_CERT = 'KrA', 'S'
SICK_WO_CERT = 'KrO', 'S' SICK_WO_CERT = 'KrO', 'S'
SICK_HOURS = 'KrU', 'S' SICK_HOURS = 'KrU', 'S'
...@@ -219,10 +226,11 @@ class Code(enum.Enum): ...@@ -219,10 +226,11 @@ class Code(enum.Enum):
MOBILE = 'Mob', 'M' MOBILE = 'Mob', 'M'
BRIDGE = 'BRT', 'B' BRIDGE = 'BRT', 'B'
ADMIN = 'BAw', 'A' ADMIN = 'BAw', 'A'
INTERNAL = 'BeA', 'A'
COVID19_SUSPECT = 'VER', 'Q' COVID19_SUSPECT = 'VER', 'Q'
COVID19_QUARANTINE = 'QUA', 'Q' COVID19_QUARANTINE = 'QUA', 'Q'
BUSYNESS_TRAVEL = 'DR', 'T' BUSYNESS_TRAVEL = 'DR', 'T'
MISSING_BOOKING = '!', '!' BUSYNESS_TRAVEL_NOW = 'DRS', 'T'
def __new__(cls, value, string): def __new__(cls, value, string):
obj = object.__new__(cls) obj = object.__new__(cls)
...@@ -316,18 +324,16 @@ class Timesheet: ...@@ -316,18 +324,16 @@ class Timesheet:
if re.match(r'.*100:Urlaub\s+Absolut\s+\+0,00', line): if re.match(r'.*100:Urlaub\s+Absolut\s+\+0,00', line):
# ignore corrected zero amount # ignore corrected zero amount
continue continue
m = re.match(r'^(?:[A-Za-z]{2})?\s+(?P<day>\d{1,2})\s+\d{3,}\s+\|' if m := re.match(r'^(?:[A-Za-z]{2})?\s+(?P<day>\d{1,2})\s+\d{3}\s+\|'
r'((?P<entry>(\d{3,})?\s+(?P<fin>(#|KoW)\s*)?(?P<tin>\d{1,2}:\d{2})?\s+' r'(\d{3})?\s+((#|KoW)\s+)?(?:(?P<tin>\d{1,2}:\d{2})\s+-)?(?:\s+(?P<tout>\d{1,2}:\d{2}))?\s+((#|GeW)\s+)?(?:\*?(?P<code>\w+))?\s+(\d{3})?\s+\|'
r'-\s+(?P<tout>\d{1,2}:\d{2})?(?P<fout>\s*(#|GeW))?\s+(\d{3,})?\s+)|\s+\*(?P<code>\w+)\s+|\s+)\|' r'(?P<notes>[^|]+)\|'
r'(?P<notes>[^|]+)\|' r'(?P<booking>\s+(?P<expected>\d+\.\d+)\s+(?P<worked>\d+\.\d+)'
r'(?P<booking>\s+(?P<expected>\d+\.\d+)\s+(?P<worked>\d+\.\d+)' r'\s+(?P<difference>[-+]\d+\.\d+)\s+(?P<balance>[-+]\d+\.\d+))?$', line):
r'\s+(?P<difference>[-+]\d+\.\d+)\s+(?P<balance>[-+]\d+\.\d+))?$', line)
if m:
# entry # entry
date = self.month.replace(day=int(m.group('day'))) date = self.month.replace(day=int(m.group('day')))
if m.group('entry'): tin = datetime.strptime(m.group('tin'), '%H:%M') if m.group('tin') else None
tin = datetime.strptime(m.group('tin'), '%H:%M') if m.group('tin') else None tout = datetime.strptime(m.group('tout'), '%H:%M') if m.group('tout') else None
tout = datetime.strptime(m.group('tout'), '%H:%M') if m.group('tout') else None if tin is not None or tout is not None:
self.days[date].entries.append((tin, tout)) self.days[date].entries.append((tin, tout))
if m.group('booking'): if m.group('booking'):
for key in 'expected', 'worked', 'balance': for key in 'expected', 'worked', 'balance':
...@@ -338,10 +344,11 @@ class Timesheet: ...@@ -338,10 +344,11 @@ class Timesheet:
if note == 'Buchung nicht erfolgt': if note == 'Buchung nicht erfolgt':
if self.days[date].code != Code.NONE: if self.days[date].code != Code.NONE:
raise ValueError(line) raise ValueError(line)
if m.group('entry'): if m.group('tin') is not None or m.group('tout') is not None:
raise ValueError(line) raise ValueError(line)
self.days[date].code = Code.MISSING_BOOKING self.days[date].code = Code.MISSING
continue continue
raise ValueError(line) raise ValueError(line)
def parse(self, s): def parse(self, s):
......