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():
|
def inject_globals():
|
||||||
"""Inject global variables into all templates"""
|
"""Inject global variables into all templates"""
|
||||||
return {
|
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)
|
last_verified_at = Column(DateTime)
|
||||||
norda_biznes_url = Column(String(500))
|
norda_biznes_url = Column(String(500))
|
||||||
norda_biznes_member_id = Column(String(50))
|
norda_biznes_member_id = Column(String(50))
|
||||||
|
member_since = Column(Date) # Data przystąpienia do Izby NORDA
|
||||||
|
|
||||||
# Metadata
|
# Metadata
|
||||||
last_updated = Column(DateTime, default=datetime.now)
|
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>
|
</div>
|
||||||
{% endif %}
|
{% 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) -->
|
<!-- PKD Card (from KRS/CEIDG) -->
|
||||||
{% if pkd_codes or company.pkd_code %}
|
{% 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;">
|
<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