Welcome to libestg3b’s documentation!¶
§ 3b of the German Einkommensteuergesetz (EStG) defines the premiums for work on weekends, holidays and special days, like new year. This library takes a list of work hours / shifts (e.g. on 2018-06-03 from 19:00 until 03:00) and returns the premium factor (e.g. 1.5) as well as the relevant time-spans.
As noted in the license, this software is provided without any warranty or guarantee for correctness.
§ 3b des Deutschen Einkommensteuergesetzes (EStG) definiert die Höhe der steuerfreien Zuschläge für Arbeit in der Nacht, an Sonntagen, Feiertagen sowie besonderen Tagen wie Neujahr. Diese Library errechnet aus einer Liste von Arbeitszeiten die Höhe der maximalen Zuschläge.
Diese Software wird, wie in der Lizenz angegeben, ohne Gewähr und Garantie auf Richtigkeit bereitgestellt.
Usage¶
>>> import datetime as DT
>>> from libestg3b import EStG3b
>>> e = EStG3b('DE')()
>>> e.calculate_shift([datetime(2018, 9, 16, 20), datetime(2018, 9, 17, 2)])
[
Match(
start=datetime.datetime(2018, 9, 16, 19, 0),
end=datetime.datetime(2018, 9, 16, 20, 0),
rules={<Rule: Sonntagsarbeit>}
),
Match(
start=datetime.datetime(2018, 9, 16, 20, 0),
end=datetime.datetime(2018, 9, 17, 0, 0),
rules={<Rule: Sonntagsarbeit>, <Rule: Nachtarbeit 20:00-06:00>}
),
Match(
start=datetime.datetime(2018, 9, 17, 0, 0),
end=datetime.datetime(2018, 9, 17, 2, 0),
rules={<Rule: Sonntagsarbeit (Montag)>, <Rule: Nachtarbeit 00:00-04:00 (Folgetag)>}
),
]
Development¶
Setup¶
Using python 3.6, do the following:
$ virtualenv venv --python=python3.6
$ pip install -e ".[dev]"
Usual Tasks¶
make test
: run tests (usetox
orpy.test
directly to supply flags like-k
)make lint
: run pylava and friendsmake fixlint
: sort imports correctly
Releasing a new version¶
Assuming you have been handed the required credentials, a new version can be released as follows.
- adapt the version in
setup.py
, according to semver. - commit this change as
Version 1.2.3
- tag the resulting commit as
v1.2.3
- push the new tag as well as the
master
branch - update the package on PyPI:
$ rm dist/*
$ python setup.py sdist bdist_wheel
$ twine upload dist/*
Prerequisites¶
This library is currently python 3.6+. If you would like to use this library with a lower python version, please open an issue. We’re happy to change things around.
Versioning¶
New version numbers are assigned following semver. All 0.x.y versions are tested and usable, but do not have a stable public interface.
A version 1.0 will be released, once we deem the library stable.
License¶
All code in this repository is licensed under the MIT license.
using libestg3b¶
- have people work on weekends
- have them write down when exactly
- pipe information into libestg3b
- have it tell you which shifts are relevant for extra pay, and how much
- ???
- PROFIT!
Prerequisites¶
To use this library, you need some kind of a system to track how many hours were spent working. In some cases, this may be a simple CSV file, a web tool to track work hours or an export from your fancy SAP application. In any case, you need a list of hours worked, each with a start and end date and time.
In this example we will use the easiest source: a text file, work.txt
:
2018-09-22T18:00:00 2018-09-22T20:00:00
2018-09-23T02:00:00 2018-09-23T03:00:00
2018-09-24T22:00:00 2018-09-25T01:00:00
So, we’ve got data. Now make it into a list of datetime
objects:
import dateutil.parser
def parse_work():
with open('work.txt') as f:
return (
(dateutil.parser.parse(l[0]), dateutil.parser.parse(l[1]))
for l in map(lambda l: l.split(' '), f.readlines())
)
def main():
shifts = parse_work()
if __name__ == '__main__':
main()
Great! That’s all we need to get libestg3b running.
Turning shifts into bonuses¶
The main functionality of libestg3b is to turn a shift into a list of matching rules defined by law. For example, the German law mandates, that work on sundays can be paid 50% more; work in the night receives a bonus of up to 40%, depending on the exact times.
Let’s use the shift data we’ve got and see what calculate_shifts()
can tell us about it:
...
matches = libestg3b.EStG3b('DE')().calculate_shifts(shifts)
# for each input shift, calculate_shifts returned one object ...
for match in matches:
print(match)
When we run the script with the new code added in the last example, we get:
<Match 2018-09-22T18:00:00~2018-09-22T20:00:00, None, add=0, multiply=0>
<Match 2018-09-23T02:00:00~2018-09-23T03:00:00, DE_NIGHT+DE_SUNDAY, add=0, multiply=0.75>
<Match 2018-09-24T22:00:00~2018-09-25T00:00:00, DE_NIGHT, add=0, multiply=0.25>
<Match 2018-09-25T00:00:00~2018-09-25T01:00:00, DE_NIGHT_START_YESTERDAY, add=0, multiply=0.4>
As you can see, our three shifts turned into four matches. Let’s talk about what happened here:
- 09-22 18:00 to 09-22 20:00Probably the most boring example there is. Not a single rule matched.
- 09-23 02:00 to 09-23 03:00A combination of two rules:
DE_NIGHT
(25%) andDE_SUNDAY
(50%). Since the law allows combination in this case, we end up with a total bonus of 75%. - 09-24 22:00 to 09-25T01:00This one is fun! Again, two matching rules were found, but each of them is not able to cover the whole shift. Therefore we get two
Match
objects: one for the first two hours (DE_NIGHT
, 25%) and one just for the last hour (DE_NIGHT_START_YESTERDAY
, 40%).
Note that multiply
values are given as 0.xx, not 1.xx as is common. This
is to enable simply combination of multiple rules: 1.2 + 1.2
equals 2.4
,
which is too high, but 0.2 + 0.2
equals the correct 0.4
.
Making money out of matches¶
The Match
object only tells us how to modify the base salary
people get. While this is useful information, accounting likes to get absolute
numbers. As you can see in the output above, rules can modify the salary in two
ways: add a fixed amount (5€ more), multiply by some factor (20% more). Since
our example is based on German law, which only works with percentages, we can
simplify our code.
To determine the amount of money to be payed out, have a look at the bonus_*
and minutes
attributes. The first one tells us how much to increase the base
salary, the 2nd one tells us how much time was actually relevant.
import libestg3b
def main():
shifts = parse_work()
matches = libestg3b.EStG3b('DE')().calculate_shifts(shifts)
base_salary = Decimal(25) / 60
total = Decimal(0)
for match in matches:
bonus = match.bonus_multiply + 1
eur = base_salary * match.minutes * bonus
total = total + eur
print(match)
print(f'({base_salary}€ * {bonus:.2f}) * {match.minutes}m = {eur: 2.2f}€')
print(f'\nTotal: {total:.2f}€')
… and when we run it:
<Match 2018-09-22T18:00:00~2018-09-22T20:00:00, None, add=0, multiply=0>
(25€ * 1.00) * 120m = 50.00€
<Match 2018-09-23T02:00:00~2018-09-23T03:00:00, DE_NIGHT+DE_SUNDAY, add=0, multiply=0.75>
(25€ * 1.75) * 60m = 43.75€
<Match 2018-09-24T22:00:00~2018-09-25T00:00:00, DE_NIGHT, add=0, multiply=0.25>
(25€ * 1.25) * 120m = 62.50€
<Match 2018-09-25T00:00:00~2018-09-25T01:00:00, DE_NIGHT_START_YESTERDAY, add=0, multiply=0.4>
(25€ * 1.40) * 60m = 35.00€
Total: 191.25€
That’s it, bascially. Depending on your exact needs, you can now put this code into a CSV, throw it at some API or just print it out.
Have fun!
adding rules¶
Most companies just follow the law when it comes to paying bonuses for work on weekends. Libestg3b comes with a standard set of rules to cover that use case. It is, howerver, also possible to extend the Rules to match your situation. This enables you to define special situations like “12-12 is our annual christmas party, pay 100% extra for any shifts during that”.
Defining it¶
Rules are implemented using Rule
objects. They contain
all neccesary information like the rules name, when to apply it and what do to,
when it does.
from decimal import Decimal
from libestg3b.rule import Rule
r = Rule(
"CHRISTMAS_PARTY",
"Annual company christmas party",
lambda minute, start, holidays: minute.month == 12 and minute.day == 12,
multiply=Decimal(1),
)
We’ve got a rule! Most of this should be pretty easy to understand, but the lambda in there can raise some questions, so let’s go over it: The function you pass into the constructor is the actual implementation of your rule. It defines when to apply it. To make sure it’s able to do its job, a couple of data points are passed into it:
minute
(datetime
)
Also refer to libestg3b.rule.Rule.match()
for further information and
examples.
Boiling it down¶
Writing a lambda function for each rule you want to write is fun, but generates a lot of boilerplate code. Since many rules are quite repetitive (“match on some date”, “match after some time”, …), libestg3b comes with a couple of helpers to save you some time:
DayRule
: match on a given month/day combination (e.g. YYYY-03-28)DayTimeRule
: likeDayRule
, but also require the shift to be after a certain time (e.g. 14:00).
Documentation and examples on these classes can be found in the respective class
docs (follow the link, alice!). To clear things up a bit, have a look at the
following example on how to shorten our CHRISTMAS_PARTY
rule using the
DayRule
class:
from decimal import Decimal
from libestg3b.rule import DayRule
r = DayRule("CHRISTMAS_PARTY", 12, 12, multiply=Decimal(1))
Except for the imports, we can now even fit it into one line without feeling bad.
Plugging it in¶
Feel floating rule objects are great, but they don’t do much. To convince the library to actually use your rules to match shifts, we need to tell it about them:
from decimal import Decimal
from libestg3b import EStG3b
from libestg3b.rule import DayRule, RuleGroup
est = EStG3b("DE")(add_rules=[
RuleGroup(
"GRP_CUSTOM",
"Rules special to our company",
[DayRule("CHRISTMAS_PARTY", 12, 12, multiply=Decimal(1))],
)
])
You’ll quickly notice a new thing here: Groups.
A libestg3b.rule.RuleGroup
is a set of rules of which only one may ever
match. A pratical example of why this might be useful is outlined in German law:
there is night work (+25%), work on sundays (+50%) and work on holidays (+125%).
While work during sunday nights allows combining the rules to yield +175%, work
on sundays, which happen to be a holiday, only allows one of the rules to be
applied, resulting in +125%. In case two or more rules match, group chooses the
one with the highest bonus and discards all other matches.
Since all rules need to be in a group, we just make up a new one (GRP_CUSTOM
)
with nothing in it except for our special rule. This allows it to be matched in
addition to any other rules already predefined by law.
Running it¶
We’ve got a rule, we’ve told the library about it, let’s see, if it actually
works. Make up a shift from 12-12 19:00 until 01:00 the next day, plug it into
calculate_shift
as outlined in the first guide and run
it:
...
import datetime as DT
m = est.calculate_shift([DT.datetime(2018, 12, 12, 19), DT.datetime(2018, 12, 13, 1)])
print(m)
[
<Match 2018-12-12T19:00~2018-12-12T20:00, CHRISTMAS_PARTY, add=0, multiply=1>,
<Match 2018-12-12T20:00~2018-12-13T00:00, CHRISTMAS_PARTY+DE_NIGHT, add=0, multiply=1.25>,
<Match 2018-12-13T00:00~2018-12-13T01:00, DE_NIGHT_START_YESTERDAY, add=0, multiply=0.4>
]
As you can see, our rule worked just as intended. In addition to the predefined
DE_NIGHT
rules, there is now also a match for CHRISTMAS_PARTY
during the
relevant times.
Happy matching!
libestg3b¶
-
libestg3b.
EStG3b
(country)[source]¶ Get the implementation class for the given country.
Parameters: country ( str
) – ISO short code of the desired country, e.g."DE"
Return type: Type
[EStG3bBase
]
-
libestg3b.
EStG3bs
()[source]¶ Get a list containing implementation classes for all implemented countries.
Return type: List
[Type
[EStG3bBase
]]
-
class
libestg3b.
EStG3bBase
(country, groups, add_rules=None, replace_rules=None)[source]¶ Bases:
object
-
calculate_shift
(shift)[source]¶ Turn a shift into a number of matches, containing the relevant rules (if any), which can be used to calculate the appropriate high of bonus payments.
>>> import datetime as DT >>> from libestg3b import EStG3b >>> e = EStG3b("DE") >>> e.calculate_shift([DT.datetime(2018, 12, 24, 13), DT.datetime(2018, 12, 25, 2)]) [ Match(start=datetime.datetime(2018, 12, 24, 13, 0), end=datetime.datetime(2018, 12, 24, 14, 0), rules=set( )), Match(start=datetime.datetime(2018, 12, 24, 14, 0), end=datetime.datetime(2018, 12, 24, 20, 0), rules={ <Rule: DE_HEILIGABEND YYYY-12-24 14:00+> }), Match(start=datetime.datetime(2018, 12, 24, 20, 0), end=datetime.datetime(2018, 12, 25, 0, 0), rules={ <Rule: DE_HEILIGABEND YYYY-12-24 14:00+>, <Rule: DE_NIGHT Nachtarbeit 20:00-06:00> }), Match(start=datetime.datetime(2018, 12, 25, 0, 0), end=datetime.datetime(2018, 12, 25, 2, 0), rules={ <Rule: DE_WEIHNACHTSFEIERTAG_1 YYYY-12-25>, <Rule: DE_NIGHT_START_YESTERDAY Nachtarbeit 00:00-04:00 (Folgetag)> }) ]
Parameters: shift ( Tuple
[datetime
,datetime
]) – a (starttime, endtime) tuple. Describes a shift started and starttime (inclusive) and ending at endtime (exclusive).Return type: List
[Match
]
-
calculate_shifts
(shifts)[source]¶ Behaves similar to
calculate_shift()
, but takes a list of shifts and returns a list of matches. It also merges any shifts that overlap, resulting in a clean list of matches.Parameters: shifts ( List
[Tuple
[datetime
,datetime
]]) –Return type: Iterator
[Match
]
-
-
class
libestg3b.
Match
(start, end, rules)[source]¶ Bases:
object
The final result of the calculation process. It links time worked to additional payments (or the information that none are relevant).
Parameters: - start (
datetime
) – the (inclusive) time this shift part starts at - end (
datetime
) – the (exclusive) time this shift part ends at - rules (
Set
[Rule
]) – all the relevant Rule instances. May be empty to indicate, that no match has been found.
-
rules_str
¶ a human-readable representation of all the rules matched, e.g.
DE_NIGHT+DE_SUNDAY
Return type: str
-
bonus_multiply
¶ the height of the bonus, as a factor to add e.g.
Decimal(0.2)
=> 20%.Return type: Decimal
-
bonus_add
¶ the total amount of monetary units to add as a bonus, e.g.
Decimal(5)
=> 5€.Return type: Decimal
-
minutes
¶ the number of minutes this Match covers, e.g.
Decimal(180)
=> 3h.Return type: Decimal
- start (
libestg3b.rule¶
-
class
libestg3b.rule.
Rule
(slug, description, impl, *, multiply=None, add=None, tests=[])[source]¶ Bases:
object
Defines a situation in which an employee might receive extra pay, e.g. “on 24th of december, pay 50% more”.
Parameters: - slug (
str
) – a machine and human-ish readable name for this rule (see below). - description (
str
) – a human readable short-form description - impl (
Callable
[…,bool
]) – actual matching function, accepting one, two or three parameters. - multiply (
Optional
[Decimal
]) – heigth of the bonus, as a factor. Supplying 0.25 results in a pay increase of 25%. - add (
Optional
[Decimal
]) – heigth of the bonus, as an absolute currency value. Eithermultiply
oradd
must be given, but not both.
The actual logic of a rule is passed in via the
impl
parameter. This function must accept 1-3 arguments:minute
,start
andholidays
. Refer tomatch()
for the meaning of those parameters.-
match
(minute, start, holidays)[source]¶ For matching, a shift must be split into its individual minutes. Each of these minutes is then passed into this method. Additionally the very first minute is provided, to enable rules like (worked after midnight, but started before).
>>> from decimal import Decimal >>> import datetime as DT >>> from libestg3b.rule import Rule >>> m = Rule("NIGHT", "Nachtarbeit", lambda m, f: m.hour >= 20, multiply=Decimal(2)) # Shift started at 2018-02-02 21:00 and this is the first minute: match! >>> m.match(DT.datetime(2018, 2, 2, 21), DT.datetime(2018, 2, 2, 21), None) True # Shift started at 2018-02-02 20:00 and 21:00 is checked: match! >>> m.match(DT.datetime(2018, 2, 2, 21), DT.datetime(2018, 2, 2, 20), None) True # Shift started at 2018-02-02 18:00 and 19:00 is checked: no match >>> m.match(DT.datetime(2018, 2, 2, 19), DT.datetime(2018, 2, 2, 18), None) False # Shift started at 2018-02-02 23:00 and 01:00 on the following day is checked # even though the start of this shift is within the timeframe "after 21:00", # the checked minute is not, so we don't match. >>> m.match(DT.datetime(2018, 2, 3, 1), DT.datetime(2018, 2, 2, 23), None) False
Parameters: - minute (
datetime
) – current minute to be matched - start (
datetime
) – very fist minute in this shift - holidays (
HolidayBase
) – holidays in the currently active country (see python-holidays)
Return type: bool
- minute (
- slug (
-
class
libestg3b.rule.
DayRule
(slug, month, day, **kwargs)[source]¶ Bases:
libestg3b.rule.Rule
Match, if the given minute is within the given day. This can be useful to increase pay on days, which are not official holidays, but still get a special treatment in the law (for example: 31th of December in Germany).
>>> from decimal import Decimal >>> import datetime as DT >>> from libestg3b.rule import DayRule >>> m = DayRule("Helloween", 10, 31, multiply=Decimal("2")) >>> m <Rule: Helloween YYYY-10-31> >>> m.match(DT.datetime(2018, 10, 31, 13), DT.datetime(2018, 10, 31, 12), None) True >>> m.match(DT.datetime(2018, 10, 30, 13), DT.datetime(2018, 10, 30, 12), None) False
Parameters: - slug (
str
) – machine-readable name of this rule, seeRule
- month (
int
) – only match, if shift is within this month, counted from 1 = January - day (
int
) – only match, if shift is on this day, counted from 1
Additionally all keyword arguments defined for
Rule
can be used.- slug (
-
class
libestg3b.rule.
DayTimeRule
(slug, month, day, hour, **kwargs)[source]¶ Bases:
libestg3b.rule.Rule
Like
DayRule
, but additionally require the shift to be after a certain time.>>> from decimal import Decimal >>> import datetime as DT >>> from libestg3b.rule import DayTimeRule >>> m = DayTimeRule("NEWYEARSEVE", 12, 31, 14, multiply=Decimal("1")) >>> m <Rule: NEWYEARSEVE YYYY-12-31 14:00+> >>> m.match(DT.datetime(2018, 12, 31, 13), DT.datetime(2018,12, 31, 13), None) False >>> m.match(DT.datetime(2018, 12, 31, 14), DT.datetime(2018,12, 31, 14), None) True
Parameters: - slug (
str
) – machine-readable name of this rule, seeRule
- month (
int
) – only match, if shift is within this month, counted from 1 = January - day (
int
) – only match, if shift is on this day, counted from 1 - hour (
int
) – only match, if shift is after or in this hour. Supplying14
results in14:00
to24:00
to be matched.
Additionally all keyword arguments defined for
Rule
can be used.- slug (
-
class
libestg3b.rule.
RuleGroup
(slug, description, rules)[source]¶ Bases:
object
A collection of similar
Rule
instances. When the group is evaluated, only the highest matching machter is returned.Parameters: - slug (
str
) – a machine and human-ish readable name for this rule, must not change. - description (
str
) – a short, human-readable text, explaining why the given rules are grouped together. - rules (
Iterable
[Rule
]) – the initial set of rules.
-
append
(rule, replace=False)[source]¶ Parameters: - rule (
Rule
) – rule to add; it must not yet exist in the group. - replace (
bool
) – if rule duplicates an existing one, overwrite it.
Return type: None
- rule (
-
match
(minute, start, holidays)[source]¶ Evaluate this group. The given shift is tested using each of the stored rules. The rule with the highest bonus is the returned. If not a single one matches,
None
is returned.This method is normally used by
libestg3b.EStG3b
, but you can use it to implement more complex scenarios yourself.Parameters: - minute (
datetime
) – minute to evaluate (seelibestgb3.EStG3b
) - start (
datetime
) – the first minute in this shift (seelibestgb3.EStG3b
)
Return type: Optional
[Rule
]- minute (
- slug (