fix: extract address/company from invoice footer dash-format (KONI Accounting style)
This commit is contained in:
parent
a8970701ab
commit
04acdecb91
@ -396,6 +396,35 @@ async def extract_vendor_suggestion(email_id: int):
|
||||
return bare
|
||||
return raw.strip()[:20]
|
||||
|
||||
# Kendte faktureringsplatforme der sender på vegne af leverandører
|
||||
PLATFORM_DOMAINS = {
|
||||
'e-conomic.com', 'e-conomic.dk', 'dinero.dk', 'billy.dk',
|
||||
'uniconta.com', 'visma.com', 'simplybilling.dk', 'fakturasend.dk',
|
||||
'invoicecloud.com', 'invoiced.com', 'stripe.com', 'paypal.com',
|
||||
}
|
||||
|
||||
# Placeholder-adresser (e-conomic og lignende skabeloner)
|
||||
PLACEHOLDER_ADDRESSES = {
|
||||
'vejnavn 1, 1234 by', 'vejnavn 1,1234 by', 'vejnavn 1',
|
||||
'1234 by', 'adresse 1', 'eksempel 1', 'gadenavn 1',
|
||||
}
|
||||
|
||||
def is_placeholder_cvr(cvr: str) -> bool:
|
||||
"""Filtrer åbenlyse placeholder/dummy CVRs"""
|
||||
known_fakes = {
|
||||
'12345678', '87654321', '00000000', '11111111', '22222222',
|
||||
'33333333', '44444444', '55555555', '66666666', '77777777',
|
||||
'88888888', '99999999', '12341234', '11223344', '99887766',
|
||||
}
|
||||
if cvr in known_fakes:
|
||||
return True
|
||||
if len(set(cvr)) == 1:
|
||||
return True
|
||||
digits = [int(c) for c in cvr]
|
||||
if all(digits[i+1] - digits[i] == 1 for i in range(7)):
|
||||
return True
|
||||
return False
|
||||
|
||||
def extract_cvr(text: str, own_cvr: str = '') -> Optional[str]:
|
||||
patterns = [
|
||||
# Med label
|
||||
@ -408,7 +437,7 @@ async def extract_vendor_suggestion(email_id: int):
|
||||
for pat in patterns:
|
||||
for m in re.finditer(pat, text, re.IGNORECASE):
|
||||
val = m.group(1)
|
||||
if val != own_cvr and val.isdigit():
|
||||
if val != own_cvr and val.isdigit() and not is_placeholder_cvr(val):
|
||||
return val
|
||||
return None
|
||||
|
||||
@ -429,47 +458,89 @@ async def extract_vendor_suggestion(email_id: int):
|
||||
return None
|
||||
|
||||
def extract_address(text: str) -> Optional[str]:
|
||||
# Dansk postnummer 4 cifre + by
|
||||
# Format 1: "Vejnavn 12K[, etage/side][ -/,] 4000 By"
|
||||
# Håndterer: "Jernbanegade 12K, st.tv - 4000 Roskilde"
|
||||
# "Nørregade 5, 1. sal, 8000 Aarhus"
|
||||
# "Industrivej 3 - 2200 København N"
|
||||
m = re.search(
|
||||
r'([A-ZÆØÅ][a-zæøåA-ZÆØÅ\-\.]+(?:\s+\d+[A-Za-z]?(?:,?\s*(?:st|tv|th|\d+\.?\s*(?:sal|etage)?))?)?,?\s*\d{4}\s+[A-ZÆØÅ][a-zæøåA-ZÆØÅ\s\-]+)',
|
||||
r'([A-ZÆØÅ][a-zæøåA-ZÆØÅ\-\.]{2,}\s+\d+[A-Za-z]?'
|
||||
r'(?:[,\s]+[a-zæøåA-ZÆØÅ0-9][a-zæøåA-ZÆØÅ0-9\.\s]{0,15}?)?'
|
||||
r'(?:[,\s]+|\s*[-–]\s*)\d{4}\s+[A-ZÆØÅ][a-zæøåA-ZÆØÅ]{2,})',
|
||||
text
|
||||
)
|
||||
if m:
|
||||
return m.group(0).strip()
|
||||
# Fallback: vejnavn + husnummer + postnummer
|
||||
# Format 2: vejnavn-suffix (vej/gade/alle osv.) + husnummer + postnummer
|
||||
m = re.search(
|
||||
r'([A-ZÆØÅ][a-zæøåA-ZÆØÅ]+(?:vej|gade|alle|vænge|torv|plads|stræde|boulevard|have|bakke|skov|park|strand|mark|eng)\s*\d+[A-Za-z]?\s*,?\s*\d{4})',
|
||||
r'([A-ZÆØÅ][a-zæøåA-ZÆØÅ]+(?:vej|gade|alle|vænge|torv|plads|stræde|boulevard|have|bakke|skov|park|strand|mark|eng)'
|
||||
r'\s*\d+[A-Za-z]?(?:[,\s]+|\s*[-–]\s*)\d{4}(?:\s+[A-ZÆØÅ][a-zæøåA-ZÆØÅ]+)?)',
|
||||
text, re.IGNORECASE
|
||||
)
|
||||
if m:
|
||||
return m.group(0).strip()
|
||||
# Format 3: find postnummer + by, tag kontekst foran
|
||||
m = re.search(r'(\d{4}\s+[A-ZÆØÅ][a-zæøåA-ZÆØÅ]{2,})', text)
|
||||
if m:
|
||||
start = max(0, m.start() - 50)
|
||||
snippet = text[start:m.end()].strip().lstrip('-–, ')
|
||||
return snippet
|
||||
return None
|
||||
|
||||
def is_platform_or_spam(domain: str) -> bool:
|
||||
"""Returnerer True hvis domænet tilhører en platform/mailsystem"""
|
||||
spam = {'gmail.com', 'hotmail.com', 'outlook.com', 'yahoo.com', 'live.com', 'icloud.com'}
|
||||
return domain in spam or domain in PLATFORM_DOMAINS or 'bmc' in domain
|
||||
|
||||
def extract_domain(text: str, sender_email: str = '') -> Optional[str]:
|
||||
# Eksplicit www
|
||||
m = re.search(r'(?:www\.|https?://)([\w\-]+\.[\w\-]+(?:\.[\w]{2,6})?)', text, re.IGNORECASE)
|
||||
if m:
|
||||
return m.group(1).lower()
|
||||
# Emailadresser i teksten (ikke @bmcnetworks)
|
||||
dom = m.group(1).lower()
|
||||
if not is_platform_or_spam(dom):
|
||||
return dom
|
||||
# Emailadresser i teksten
|
||||
for em in re.finditer(r'[\w.\-+]+@([\w\-]+\.[\w\-]+(?:\.[\w]{2,6})?)', text):
|
||||
dom = em.group(1).lower()
|
||||
if 'bmc' not in dom and 'gmail' not in dom and 'outlook' not in dom and 'hotmail' not in dom:
|
||||
if not is_platform_or_spam(dom):
|
||||
return dom
|
||||
# Sender email
|
||||
# Sender email (kun hvis ikke platform)
|
||||
if sender_email and '@' in sender_email:
|
||||
dom = sender_email.split('@')[1].lower()
|
||||
if 'gmail' not in dom and 'outlook' not in dom and 'hotmail' not in dom:
|
||||
if not is_platform_or_spam(dom):
|
||||
return dom
|
||||
return None
|
||||
|
||||
def extract_company_name(text: str, sender_name: str = '') -> Optional[str]:
|
||||
"""Prøv at finde firmanavn via CVR-nær tekst eller typiske DK-firmasuffikser"""
|
||||
"""Find firmanavn via DK-suffikser, footer-mønster eller CVR-nær tekst"""
|
||||
# Prioritet 1: navne med DK juridiske suffikser
|
||||
m = re.search(
|
||||
r'\b([\w\s\-&\'\.]+(?:A/S|ApS|IVS|I/S|K/S|P/S|GmbH|Ltd\.?|LLC|AB|AS))\b',
|
||||
text
|
||||
)
|
||||
if m:
|
||||
return m.group(1).strip()
|
||||
|
||||
# Prioritet 2: typisk faktura-footer format:
|
||||
# "FirmaNavn - Adresse - Postnr By - CVR-nr.: XXXXXXXX"
|
||||
m = re.search(
|
||||
r'^([A-ZÆØÅ][A-Za-zæøåÆØÅ\s\-&\'\.]{2,50}?)\s*[-–]\s*[A-ZÆØÅ][a-zæøåA-ZÆØÅ]',
|
||||
text, re.MULTILINE
|
||||
)
|
||||
if m:
|
||||
name = m.group(1).strip()
|
||||
if len(name) > 2 and not any(w in name.lower() for w in ('tlf', 'tel', 'mail', 'bank', 'cvr', 'mobil')):
|
||||
return name
|
||||
|
||||
# Prioritet 3: tekst lige FORAN "CVR" på samme linje
|
||||
m = re.search(
|
||||
r'([A-ZÆØÅ][A-Za-zæøåÆØÅ\s\-&\'\.]{2,50}?)\s*[-–,]?\s*(?:CVR|Cvr)',
|
||||
text
|
||||
)
|
||||
if m:
|
||||
name = m.group(1).strip().rstrip('-–, ')
|
||||
if len(name) > 2:
|
||||
return name
|
||||
|
||||
return sender_name or None
|
||||
|
||||
# ── Hoved-logik ─────────────────────────────────────────────────────────
|
||||
@ -521,14 +592,30 @@ async def extract_vendor_suggestion(email_id: int):
|
||||
sender_name = email.get('sender_name') or ''
|
||||
sender_email = email.get('sender_email') or ''
|
||||
|
||||
# Er afsenderen en faktureringsplatform? (e-conomic etc.)
|
||||
sender_domain = sender_email.split('@')[1].lower() if '@' in sender_email else ''
|
||||
is_platform_sender = sender_domain in PLATFORM_DOMAINS
|
||||
|
||||
# Brug ikke sender_email som leverandør-email når det er en platform
|
||||
# Prøv i stedet at finde en rigtig email i teksten
|
||||
vendor_email = None
|
||||
if not is_platform_sender and sender_email:
|
||||
vendor_email = sender_email
|
||||
else:
|
||||
for em in re.finditer(r'[\w.\-+]+@([\w\-]+\.[\w\-]+(?:\.[\w]{2,6})?)', focused_text):
|
||||
dom = em.group(1).lower()
|
||||
if dom not in PLATFORM_DOMAINS and 'bmc' not in dom:
|
||||
vendor_email = em.group(0)
|
||||
break
|
||||
|
||||
# ── Regex udtræk ────────────────────────────────────────────────────
|
||||
suggestion = {
|
||||
"name": extract_company_name(focused_text, sender_name) or sender_name,
|
||||
"email": sender_email,
|
||||
"email": vendor_email,
|
||||
"cvr_number": extract_cvr(focused_text, own_cvr),
|
||||
"phone": extract_phones(focused_text),
|
||||
"address": extract_address(focused_text),
|
||||
"domain": extract_domain(focused_text, sender_email),
|
||||
"domain": extract_domain(focused_text, sender_email if not is_platform_sender else ''),
|
||||
"source": "regex"
|
||||
}
|
||||
|
||||
@ -540,35 +627,50 @@ async def extract_vendor_suggestion(email_id: int):
|
||||
# Send kun den fokuserede tekst (max 4000 tegn) til AI
|
||||
ai_text = focused_text[:4000]
|
||||
|
||||
# Pre-filtrer hints inden AI ser dem (undgå at sende placeholders som hints)
|
||||
hint_cvr = suggestion.get('cvr_number')
|
||||
if hint_cvr and is_placeholder_cvr(hint_cvr):
|
||||
hint_cvr = None
|
||||
hint_addr = suggestion.get('address')
|
||||
if hint_addr and any(ph in hint_addr.lower() for ph in PLACEHOLDER_ADDRESSES):
|
||||
hint_addr = None
|
||||
hint_domain = suggestion.get('domain')
|
||||
if hint_domain and is_platform_or_spam(hint_domain):
|
||||
hint_domain = None
|
||||
|
||||
prompt = f"""Du er en ekspert i at udtrække firmaoplysninger fra danske fakturaer og e-mails.
|
||||
|
||||
OPGAVE: Find LEVERANDØRENS firmaoplysninger i teksten nedenfor.
|
||||
Leverandøren er AFSENDEREN - IKKE BMC Networks og IKKE køber.
|
||||
Leverandøren er AFSENDEREN (sælger/udsteder) - IKKE BMC Networks og IKKE køber/modtager.
|
||||
|
||||
VIGTIGT: E-mails kan være sendt via faktureringsplatforme som e-conomic, Dinero, Billy osv.
|
||||
I så fald er leverandøren den virksomhed DER EJER fakturaen - IKKE platformen selv.
|
||||
Ignorer alle data tilhørende: e-conomic.com, dinero.dk, billy.dk, uniconta.com
|
||||
|
||||
RETURNER KUN DETTE JSON - ingen forklaring, ingen markdown:
|
||||
{{
|
||||
"name": "Firmanavn ApS",
|
||||
"cvr_number": "12345678",
|
||||
"address": "Vejnavn 1, 2000 By",
|
||||
"cvr_number": "87654321",
|
||||
"address": "Rigtig Vej 5, 2200 København",
|
||||
"phone": "12345678",
|
||||
"email": "kontakt@firma.dk",
|
||||
"domain": "firma.dk"
|
||||
}}
|
||||
|
||||
REGLER:
|
||||
- name: Firmanavn med A/S, ApS, IVS osv. - IKKE BMC Networks
|
||||
- cvr_number: Præcis 8 cifre efter "CVR", "CVR-nr", "Moms" eller "DK" - IGNORER {own_cvr}
|
||||
- address: Fuld adresse med postnummer og by (dansk format: "Vejnavn 1, 1234 By")
|
||||
- name: Firmanavn med A/S, ApS, IVS osv. - IKKE BMC Networks, IKKE e-conomic
|
||||
- cvr_number: Præcis 8 cifre efter "CVR", "CVR-nr", "Moms" eller "DK" - IGNORER {own_cvr}, IGNORER 12345678 (placeholder)
|
||||
- address: Fuld RIGTIG adresse med postnummer og by - IGNORER "Vejnavn 1, 1234 By" (placeholder)
|
||||
- phone: Telefonnummer - foretrukket format: "+45 XXXX XXXX" eller "XXXX XXXX"
|
||||
- email: Kontakt-email til firmaet (IKKE afsender-email hvis den er personlig)
|
||||
- domain: Hjemmeside-domæne f.eks. "firma.dk" eller "www.firma.dk"
|
||||
- email: Kontakt-email til firmaet - IKKE e-conomic.com, IKKE post@e-conomic.com
|
||||
- domain: Hjemmeside-domæne - IKKE e-conomic.com, IKKE faktureringsplatform
|
||||
- Sæt null for felter der IKKE kan findes med sikkerhed
|
||||
|
||||
KENDTE REGEX-RESULTATER (brug som hjælp, ret dem hvis de er forkerte):
|
||||
- cvr: {suggestion.get('cvr_number') or 'ikke fundet'}
|
||||
KENDTE REGEX-RESULTATER (brug som hjælp - disse er allerede filtrerede):
|
||||
- cvr: {hint_cvr or 'ikke fundet'}
|
||||
- phone: {suggestion.get('phone') or 'ikke fundet'}
|
||||
- address: {suggestion.get('address') or 'ikke fundet'}
|
||||
- domain: {suggestion.get('domain') or 'ikke fundet'}
|
||||
- address: {hint_addr or 'ikke fundet'}
|
||||
- domain: {hint_domain or 'ikke fundet'}
|
||||
|
||||
TEKST:
|
||||
{ai_text}
|
||||
@ -591,15 +693,27 @@ JSON:"""
|
||||
except Exception as e:
|
||||
logger.warning(f"⚠️ AI udtræk fejlede, bruger regex-resultat: {e}")
|
||||
|
||||
# Rens: fjern domæner der tilhører kendte mailservere
|
||||
spam_domains = {'gmail.com', 'hotmail.com', 'outlook.com', 'yahoo.com', 'live.com', 'icloud.com'}
|
||||
if suggestion.get('domain') in spam_domains:
|
||||
# Rens: fjern platform/spam domæner
|
||||
if suggestion.get('domain') and is_platform_or_spam(suggestion['domain']):
|
||||
suggestion['domain'] = None
|
||||
|
||||
# Fjern own_cvr hvis den snegte sig ind
|
||||
if suggestion.get('cvr_number') == own_cvr:
|
||||
# Fjern own_cvr og placeholder CVRs
|
||||
if suggestion.get('cvr_number') == own_cvr or (
|
||||
suggestion.get('cvr_number') and is_placeholder_cvr(suggestion['cvr_number'])
|
||||
):
|
||||
suggestion['cvr_number'] = None
|
||||
|
||||
# Fjern placeholder-adresser (e-conomic og lignende skabeloner)
|
||||
addr_lower = (suggestion.get('address') or '').lower().strip()
|
||||
if any(ph in addr_lower for ph in PLACEHOLDER_ADDRESSES):
|
||||
suggestion['address'] = None
|
||||
|
||||
# Fjern platform email hvis AI satte den alligevel
|
||||
if suggestion.get('email') and '@' in suggestion.get('email', ''):
|
||||
em_domain = suggestion['email'].split('@')[-1].lower()
|
||||
if em_domain in PLATFORM_DOMAINS:
|
||||
suggestion['email'] = None
|
||||
|
||||
return suggestion
|
||||
|
||||
except HTTPException:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user