feat: Dodanie daty przystąpienia do Izby NORDA na profilu firmy
- Nowa kolumna member_since w tabeli companies - Karta "Członek Izby NORDA od" na profilu firmy (niebieski kolor #3b82f6) - Wyświetlanie liczby lat w Izbie - Import 57 dat przystąpienia z pliku Excel od Artura - Skrypt import_member_since.py do importu dat Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
c1e770f806
commit
3221740502
3
app.py
3
app.py
@ -248,7 +248,8 @@ def load_user(user_id):
|
||||
def inject_globals():
|
||||
"""Inject global variables into all templates"""
|
||||
return {
|
||||
'current_year': datetime.now().year
|
||||
'current_year': datetime.now().year,
|
||||
'now': datetime.now
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -308,6 +308,7 @@ class Company(Base):
|
||||
last_verified_at = Column(DateTime)
|
||||
norda_biznes_url = Column(String(500))
|
||||
norda_biznes_member_id = Column(String(50))
|
||||
member_since = Column(Date) # Data przystąpienia do Izby NORDA
|
||||
|
||||
# Metadata
|
||||
last_updated = Column(DateTime, default=datetime.now)
|
||||
|
||||
196
scripts/import_member_since.py
Normal file
196
scripts/import_member_since.py
Normal file
@ -0,0 +1,196 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Import dat przystąpienia firm do Izby NORDA
|
||||
Źródło: .private/inbox-artur/załączniki/Aktualna lista kontaktow wraz z data przystapienia.xlsx
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
# Add parent directory to path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
import pandas as pd
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
# Database connection
|
||||
DATABASE_URL = os.getenv('DATABASE_URL', 'postgresql://nordabiz_app:dev_password@localhost:5433/nordabiz')
|
||||
engine = create_engine(DATABASE_URL)
|
||||
Session = sessionmaker(bind=engine)
|
||||
|
||||
# Path to Excel file
|
||||
EXCEL_PATH = Path(__file__).parent.parent / '.private' / 'inbox-artur' / 'załączniki' / 'Aktualna lista kontaktow wraz z data przystapienia.xlsx'
|
||||
|
||||
|
||||
def normalize_name(name):
|
||||
"""Normalize company name for matching"""
|
||||
if not name:
|
||||
return ''
|
||||
name = str(name).strip().lower()
|
||||
# Remove common suffixes
|
||||
for suffix in [' sp. z o.o.', ' sp.z o.o.', ' spółka z o.o.', ' s.a.', ' sp. j.', ' s.c.']:
|
||||
name = name.replace(suffix, '')
|
||||
# Remove extra spaces
|
||||
name = ' '.join(name.split())
|
||||
return name
|
||||
|
||||
|
||||
# Manual aliases for companies with different names in Excel vs database
|
||||
NAME_ALIASES = {
|
||||
'agat': 'jubiler agat',
|
||||
'eura tech': 'eura-tech',
|
||||
'hotel wieniawa': 'hotel spa wieniawa',
|
||||
'hebel, masiak i wspólnicy': 'hebel masiak i wspólnicy adwokaci i radcowie prawni',
|
||||
'coolair hvac systems': 'coolair',
|
||||
'agis': 'agis nieruchomości',
|
||||
'cris tap': 'cristap',
|
||||
'ekod h.a.p.': 'ekod',
|
||||
'gren house systems': 'green house systems',
|
||||
'hill ob.': 'hill obiekt',
|
||||
'kammet': 'kammet',
|
||||
'kbs': 'kbs instalacje',
|
||||
'kornix phu': 'kornix',
|
||||
'kupsa coathing': 'kupsa coating',
|
||||
'korporacja budowlana kbms': 'kbms',
|
||||
'mkonsult m. matuszak': 'mkonsult',
|
||||
'mesan grupa sbs': 'mesan',
|
||||
'nowak chłodnictwo klimatyzacja': 'nowak chłodnictwo',
|
||||
'porta kmi poland': 'porta kmi',
|
||||
'rubinsolar': 'rubinsolar',
|
||||
'technika budowlana biłas': 'technika budowlana',
|
||||
'kancelaria rachunkowa gawin &wojnowska': 'kancelaria rachunkowa gawin wojnowska',
|
||||
'chopin telewizja kablowa': 'chopin',
|
||||
'pix lab': 'pixlab softwarehouse',
|
||||
'porta kmi sa': 'porta kmi',
|
||||
'rotor': 'rotor',
|
||||
'scrol': 'scrol',
|
||||
'przedsiębiorstwo budowlane sigma': 'sigma budownictwo',
|
||||
'ttm, tk chopin': 'chopin',
|
||||
'ultramare': 'ultramare',
|
||||
'pgk': 'pucka gospodarka komunalna',
|
||||
'perfekta biuro rachunkowe': 'biuro rachunkowości perfekta',
|
||||
'portal usługi ogólnobudowlane': 'portal',
|
||||
'riela polska': 'riela polska',
|
||||
's&k tobaacco sklepy lord': 'phu s&k tobacco',
|
||||
'kancelaria radcy prawnego': 'kancelaria radcy prawnego łukasz gilewicz',
|
||||
}
|
||||
|
||||
|
||||
def parse_date(date_val):
|
||||
"""Parse date from various formats"""
|
||||
if pd.isna(date_val):
|
||||
return None
|
||||
|
||||
if isinstance(date_val, datetime):
|
||||
return date_val.date()
|
||||
|
||||
if isinstance(date_val, str):
|
||||
# Try different formats
|
||||
for fmt in ['%Y-%m-%d', '%d.%m.%Y', '%Y-%m-%d %H:%M:%S']:
|
||||
try:
|
||||
return datetime.strptime(date_val.strip(), fmt).date()
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def main(dry_run=True):
|
||||
print(f"{'[DRY RUN] ' if dry_run else ''}Import dat przystąpienia do Izby NORDA")
|
||||
print(f"Źródło: {EXCEL_PATH}")
|
||||
print("=" * 60)
|
||||
|
||||
# Read Excel
|
||||
df = pd.read_excel(EXCEL_PATH, header=1)
|
||||
print(f"Wczytano {len(df)} wierszy z pliku Excel\n")
|
||||
|
||||
# Get companies from database
|
||||
session = Session()
|
||||
|
||||
from database import Company
|
||||
companies = session.query(Company).all()
|
||||
|
||||
# Create mapping by normalized name
|
||||
company_map = {}
|
||||
for c in companies:
|
||||
norm_name = normalize_name(c.name)
|
||||
company_map[norm_name] = c
|
||||
# Also map by legal_name if different
|
||||
if c.legal_name and normalize_name(c.legal_name) != norm_name:
|
||||
company_map[normalize_name(c.legal_name)] = c
|
||||
|
||||
print(f"Firm w bazie: {len(companies)}")
|
||||
print(f"Unikalnych nazw do matchowania: {len(company_map)}\n")
|
||||
|
||||
# Process Excel data
|
||||
updated = 0
|
||||
not_found = []
|
||||
already_set = 0
|
||||
no_date = 0
|
||||
|
||||
for _, row in df.iterrows():
|
||||
firma = row.get('Firma')
|
||||
date_val = row.get('Data przystąpienia')
|
||||
|
||||
if pd.isna(firma):
|
||||
continue
|
||||
|
||||
norm_firma = normalize_name(firma)
|
||||
member_since = parse_date(date_val)
|
||||
|
||||
if not member_since:
|
||||
no_date += 1
|
||||
continue
|
||||
|
||||
# Check for alias
|
||||
if norm_firma in NAME_ALIASES:
|
||||
norm_firma = NAME_ALIASES[norm_firma]
|
||||
|
||||
if norm_firma in company_map:
|
||||
company = company_map[norm_firma]
|
||||
|
||||
if company.member_since:
|
||||
already_set += 1
|
||||
if company.member_since != member_since:
|
||||
print(f" ⚠️ {company.name}: różne daty ({company.member_since} vs {member_since})")
|
||||
continue
|
||||
|
||||
print(f" ✅ {company.name} → {member_since}")
|
||||
|
||||
if not dry_run:
|
||||
company.member_since = member_since
|
||||
|
||||
updated += 1
|
||||
else:
|
||||
not_found.append((firma, member_since))
|
||||
|
||||
if not dry_run:
|
||||
session.commit()
|
||||
|
||||
session.close()
|
||||
|
||||
# Summary
|
||||
print("\n" + "=" * 60)
|
||||
print(f"PODSUMOWANIE:")
|
||||
print(f" Zaktualizowano: {updated}")
|
||||
print(f" Już ustawione: {already_set}")
|
||||
print(f" Bez daty: {no_date}")
|
||||
print(f" Nie znaleziono: {len(not_found)}")
|
||||
|
||||
if not_found:
|
||||
print(f"\nFirmy nie znalezione w bazie:")
|
||||
for firma, date in not_found[:20]:
|
||||
print(f" - {firma} ({date})")
|
||||
if len(not_found) > 20:
|
||||
print(f" ... i {len(not_found) - 20} więcej")
|
||||
|
||||
if dry_run:
|
||||
print(f"\n[DRY RUN] Aby zapisać zmiany, uruchom z --apply")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
dry_run = '--apply' not in sys.argv
|
||||
main(dry_run=dry_run)
|
||||
@ -1370,6 +1370,27 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Member Since Card (Norda Biznes membership) -->
|
||||
{% if company.member_since %}
|
||||
<div style="background: var(--background); border-radius: var(--radius-lg); padding: var(--spacing-lg); border: 2px solid #3b82f6;">
|
||||
<div style="display: flex; align-items: center; gap: var(--spacing-md);">
|
||||
<div style="width: 48px; height: 48px; border-radius: 12px; display: flex; align-items: center; justify-content: center; background: #3b82f6; color: white;">
|
||||
<svg width="24" height="24" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size: var(--font-size-sm); color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.05em;">Członek Izby NORDA od</div>
|
||||
<div style="font-size: var(--font-size-xl); font-weight: 700; color: #3b82f6;">{{ company.member_since.strftime('%d.%m.%Y') }}</div>
|
||||
{% set years_member = ((now().date() - company.member_since).days / 365.25)|int %}
|
||||
{% if years_member > 0 %}
|
||||
<div style="font-size: var(--font-size-sm); color: var(--text-secondary);">{{ years_member }} lat w Izbie</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- PKD Card (from KRS/CEIDG) -->
|
||||
{% if pkd_codes or company.pkd_code %}
|
||||
<div style="background: var(--background); border-radius: var(--radius-lg); padding: var(--spacing-lg); border: 2px solid #7c3aed;">
|
||||
|
||||
Loading…
Reference in New Issue
Block a user