Skills/german-tax-research/scripts/Get-SteuerFrist.ps1
|
#requires -Version 7.0 <# .SYNOPSIS Computes the deemed notification date and the resulting deadline for a German tax notice. .DESCRIPTION Applies the notification fiction of § 122 Abs. 2, Abs. 2a, § 122a Abs. 4 AO to the dispatch date of a Steuerbescheid, then the one-month objection period of § 355 Abs. 1 AO, computed under § 108 AO with §§ 187 Abs. 1, 188 Abs. 2 und 3 BGB. Both the deemed notification day and the end of the period move to the next working day when they fall on a Saturday, Sunday, or a public holiday (§ 108 Abs. 3 AO). Public holidays are state law, so the state of the competent Finanzamt governs. .PARAMETER BescheidDatum Date the notice was posted or dispatched — the date printed on the notice, not the date it arrived. .PARAMETER Bundesland Federal state of the competent Finanzamt. 'Bundesweit' counts only the nationwide holidays and is the conservative default for a first estimate. .PARAMETER Uebermittlung Transmission channel. 'PostAusland' applies the one-month fiction of § 122 Abs. 2 Nr. 2 AO instead of the day-count fiction. .PARAMETER FristMonate Length of the period in months. 1 for the objection period (§ 355 Abs. 1 AO) and the action period (§ 47 FGO), 12 where the Rechtsbehelfsbelehrung is missing or defective (§ 356 Abs. 2 AO). .EXAMPLE ./Get-SteuerFrist.ps1 -BescheidDatum '2026-02-25' -Bundesland Niedersachsen Notice posted 25 February 2026: the fourth day is Sunday 1 March 2026, so notification is deemed on Monday 2 March 2026 and the objection deadline is 2 April 2026. .NOTES The three-day fiction applies to anything dispatched before 1 January 2025; the four-day fiction applies from that date (PostModG, BGBl. 2024 I Nr. 236). The script selects by the dispatch date. 24 and 31 December are not statutory holidays even though tax offices are usually closed, so § 108 Abs. 3 AO does not move a deadline falling on them. The file is UTF-8 without a byte order mark, matching the repository convention, and therefore requires PowerShell 7 — Windows PowerShell 5.1 would decode the § sign and the umlauts with the ANSI code page. #> [CmdletBinding()] [OutputType([PSCustomObject])] param( [Parameter(Mandatory)] [datetime]$BescheidDatum, [Parameter()] [ValidateSet('Bundesweit', 'Baden-Wuerttemberg', 'Bayern', 'Berlin', 'Brandenburg', 'Bremen', 'Hamburg', 'Hessen', 'Mecklenburg-Vorpommern', 'Niedersachsen', 'Nordrhein-Westfalen', 'Rheinland-Pfalz', 'Saarland', 'Sachsen', 'Sachsen-Anhalt', 'Schleswig-Holstein', 'Thueringen')] [string]$Bundesland = 'Bundesweit', [Parameter()] [ValidateSet('Post', 'Elektronisch', 'Datenabruf', 'PostAusland')] [string]$Uebermittlung = 'Post', [Parameter()] [ValidateRange(1, 12)] [int]$FristMonate = 1 ) function Get-EasterSunday { <# .SYNOPSIS Returns Easter Sunday of a Gregorian year (anonymous Gregorian algorithm). #> [CmdletBinding()] [OutputType([datetime])] param( [Parameter(Mandatory)] [int]$Year ) # Anonymous Gregorian algorithm. The literals are the algorithm's own constants # (Metonic cycle, century correction, weekday offset) and carry no local meaning. $a = $Year % 19 $b = [math]::Floor($Year / 100) $c = $Year % 100 $d = [math]::Floor($b / 4) $e = $b % 4 $f = [math]::Floor(($b + 8) / 25) $g = [math]::Floor(($b - $f + 1) / 3) $h = (19 * $a + $b - $d - $g + 15) % 30 $i = [math]::Floor($c / 4) $k = $c % 4 $l = (32 + 2 * $e + 2 * $i - $h - $k) % 7 $m = [math]::Floor(($a + 11 * $h + 22 * $l) / 451) $month = [math]::Floor(($h + $l - 7 * $m + 114) / 31) $day = (($h + $l - 7 * $m + 114) % 31) + 1 return [datetime]::new($Year, $month, $day) } function Get-GesetzlicherFeiertag { <# .SYNOPSIS Returns the statutory holidays of one year for one federal state. #> [CmdletBinding()] [OutputType([datetime[]])] param( [Parameter(Mandatory)] [int]$Year, [Parameter(Mandatory)] [string]$State ) $easter = Get-EasterSunday -Year $Year $holidays = [System.Collections.Generic.List[datetime]]::new() # Nationwide holidays (§ 2 Abs. 2 EinigVtrG and the state acts, identical in all states). $holidays.Add([datetime]::new($Year, 1, 1)) # Neujahr $holidays.Add($easter.AddDays(-2)) # Karfreitag $holidays.Add($easter.AddDays(1)) # Ostermontag $holidays.Add([datetime]::new($Year, 5, 1)) # Tag der Arbeit $holidays.Add($easter.AddDays(39)) # Christi Himmelfahrt $holidays.Add($easter.AddDays(50)) # Pfingstmontag $holidays.Add([datetime]::new($Year, 10, 3)) # Tag der Deutschen Einheit $holidays.Add([datetime]::new($Year, 12, 25)) # 1. Weihnachtstag $holidays.Add([datetime]::new($Year, 12, 26)) # 2. Weihnachtstag $epiphany = @('Baden-Wuerttemberg', 'Bayern', 'Sachsen-Anhalt') $womensDay = @('Berlin', 'Mecklenburg-Vorpommern') $corpusChristi = @('Baden-Wuerttemberg', 'Bayern', 'Hessen', 'Nordrhein-Westfalen', 'Rheinland-Pfalz', 'Saarland') $reformation = @('Brandenburg', 'Bremen', 'Hamburg', 'Mecklenburg-Vorpommern', 'Niedersachsen', 'Sachsen', 'Sachsen-Anhalt', 'Schleswig-Holstein', 'Thueringen') $allSaints = @('Baden-Wuerttemberg', 'Bayern', 'Nordrhein-Westfalen', 'Rheinland-Pfalz', 'Saarland') if ($State -in $epiphany) { $holidays.Add([datetime]::new($Year, 1, 6)) } if ($State -in $womensDay) { $holidays.Add([datetime]::new($Year, 3, 8)) } if ($State -in $corpusChristi) { $holidays.Add($easter.AddDays(60)) } if ($State -eq 'Saarland') { $holidays.Add([datetime]::new($Year, 8, 15)) } if ($State -eq 'Thueringen') { $holidays.Add([datetime]::new($Year, 9, 20)) } if ($State -in $reformation) { $holidays.Add([datetime]::new($Year, 10, 31)) } if ($State -in $allSaints) { $holidays.Add([datetime]::new($Year, 11, 1)) } if ($State -eq 'Sachsen') { # Buß- und Bettag: the Wednesday before 23 November. $reference = [datetime]::new($Year, 11, 23) $offset = (([int]$reference.DayOfWeek - [int][DayOfWeek]::Wednesday) + 7) % 7 if ($offset -eq 0) { $offset = 7 } $holidays.Add($reference.AddDays(-$offset)) } return $holidays.ToArray() } function Get-NaechsterWerktag { <# .SYNOPSIS Moves a date forward to the next working day under § 108 Abs. 3 AO. #> [CmdletBinding()] [OutputType([PSCustomObject])] param( [Parameter(Mandatory)] [datetime]$Date, [Parameter(Mandatory)] [string]$State ) $current = $Date.Date $reasons = [System.Collections.Generic.List[string]]::new() $holidays = Get-GesetzlicherFeiertag -Year $current.Year -State $State while ($true) { if ($current.Year -ne $holidays[0].Year) { $holidays = Get-GesetzlicherFeiertag -Year $current.Year -State $State } if ($current.DayOfWeek -eq [DayOfWeek]::Saturday) { $reasons.Add(('{0:dd.MM.yyyy} ist ein Samstag' -f $current)) } elseif ($current.DayOfWeek -eq [DayOfWeek]::Sunday) { $reasons.Add(('{0:dd.MM.yyyy} ist ein Sonntag' -f $current)) } elseif ($holidays -contains $current) { $reasons.Add(('{0:dd.MM.yyyy} ist ein gesetzlicher Feiertag in {1}' -f $current, $State)) } else { break } $current = $current.AddDays(1) } return [PSCustomObject]@{ Datum = $current Verschoben = $reasons.ToArray() } } $hinweise = [System.Collections.Generic.List[string]]::new() # § 122 Abs. 2 Nr. 1 und Abs. 2a AO: three days until 31.12.2024, four days from # 01.01.2025 (PostModG). The dispatch date decides which version applies. $fiktionsTage = if ($BescheidDatum.Date -ge [datetime]'2025-01-01') { 4 } else { 3 } if ($Uebermittlung -eq 'PostAusland') { $fiktion = $BescheidDatum.Date.AddMonths(1) $fiktionsNorm = '§ 122 Abs. 2 Nr. 2 AO (ein Monat bei Übermittlung ins Ausland)' } else { $fiktion = $BescheidDatum.Date.AddDays($fiktionsTage) $fiktionsNorm = switch ($Uebermittlung) { 'Elektronisch' { "§ 122 Abs. 2a AO ($fiktionsTage Tage nach Absendung)" } 'Datenabruf' { "§ 122a Abs. 4 AO ($fiktionsTage Tage nach der Benachrichtigung)" } default { "§ 122 Abs. 2 Nr. 1 AO ($fiktionsTage Tage nach Aufgabe zur Post)" } } } $bekanntgabe = Get-NaechsterWerktag -Date $fiktion -State $Bundesland foreach ($reason in $bekanntgabe.Verschoben) { $hinweise.Add("Bekanntgabe verschoben: $reason") } # §§ 187 Abs. 1, 188 Abs. 2 BGB: the period ends on the day of the later month bearing the # same number; AddMonths clamps to the last day of a shorter month, which is § 188 Abs. 3 BGB. $fristEndeRoh = $bekanntgabe.Datum.AddMonths($FristMonate) $fristEnde = Get-NaechsterWerktag -Date $fristEndeRoh -State $Bundesland foreach ($reason in $fristEnde.Verschoben) { $hinweise.Add("Fristende verschoben: $reason") } [PSCustomObject]@{ BescheidDatum = $BescheidDatum.Date Uebermittlung = $Uebermittlung Bundesland = $Bundesland Fiktionsnorm = $fiktionsNorm BekanntgabeRoh = $fiktion Bekanntgabe = $bekanntgabe.Datum Fristlaenge = "$FristMonate Monat(e)" FristEndeRoh = $fristEndeRoh FristEnde = $fristEnde.Datum VerbleibendeTage = [int]($fristEnde.Datum - (Get-Date).Date).TotalDays Hinweise = $hinweise.ToArray() } |