A local web app for a small room-rental business in Austria: guest register, room ledger, occupancy plan, invoices, the monthly tourist-tax report and the quarterly VAT list. Application code only; it ships no data. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
54 lines
2.8 KiB
Python
54 lines
2.8 KiB
Python
"""Pure normalization rules: company-name spellings and invoice-number formats."""
|
|
import re
|
|
|
|
|
|
def company_key(name):
|
|
"""Grouping key for company-name spellings: case/punctuation/whitespace-insensitive, the German
|
|
legal-form spellings unified (GmbH == Ges.m.b.H. == GesmbH, 'und' == '&'). Different legal forms
|
|
(GmbH vs AG) stay different — near-misses beyond this are only SUGGESTED for manual merge."""
|
|
n = (name or "").strip().lower()
|
|
n = n.replace(" und ", " & ").replace(" u. ", " & ")
|
|
n = re.sub(r"ges\.?\s*m\.?\s*b\.?\s*h\.?", "gmbh", n)
|
|
n = re.sub(r"[^a-z0-9äöüß&]+", "", n)
|
|
return n.replace("gbmh", "gmbh") # 'GbmH' is a transposition typo, not a different legal form
|
|
|
|
|
|
def invoice_seq(number, year):
|
|
"""Canonical integer sequence of an invoice number for the given 4-digit year, whatever the format:
|
|
'202262' -> 62, '2022-62' -> 62, '22-62' -> 62, '62-2015' -> 62 (SEQ-YEAR), '2026-01' -> 1, '62' -> 62.
|
|
Combined / concatenated numbers ('41/46', '5556') return None so they never pass for a single invoice."""
|
|
toks = [t for t in re.split(r"\D+", str(number or "")) if t]
|
|
if not toks:
|
|
return None
|
|
if len(toks) == 1: # bare '25' / glued 'YYYYNN' / 'YYNN'
|
|
s = toks[0]
|
|
if s == year:
|
|
return None # a bare 4-digit year is not a sequence
|
|
if year and s.startswith(year) and len(s) > len(year):
|
|
s = s[len(year):] # 'YYYYNN'
|
|
elif year and s.startswith(year[2:]) and len(s) > 2:
|
|
s = s[2:] # 'YYNN'
|
|
# else keep s verbatim: a bare sequence ('25','62') even when it equals the 2-digit year (invoice #25 in 2025)
|
|
else: # PREFIX-SEQ / SEQ-YEAR: drop ONE year token
|
|
non_year = list(toks)
|
|
for yt in (year, year[2:]):
|
|
if yt in non_year:
|
|
non_year.remove(yt)
|
|
break
|
|
if len(non_year) != 1:
|
|
return None # combined ('41','46') -> no single sequence
|
|
s = non_year[0]
|
|
s = s.lstrip("0") or s
|
|
return int(s) if s.isdigit() and len(s) <= 3 else None
|
|
|
|
|
|
def invoice_year(number):
|
|
"""The 4-digit year an invoice number encodes: '2025-46'->'2025', '25-46'/'26-01'->20YY, else None. A
|
|
New-Year invoice dated in January can still carry the old year's number; this is that year."""
|
|
s = str(number or "")
|
|
m = re.search(r"(?:19|20)\d\d", s)
|
|
if m:
|
|
return m.group(0)
|
|
m = re.match(r"\s*(\d{2})\D", s) # 'YY-NN' 2-digit prefix
|
|
return "20" + m.group(1) if m else None
|