From 9eae623d3e875e4a68bf55628df126dd80596494 Mon Sep 17 00:00:00 2001 From: Maciej Pienczyn Date: Tue, 13 Jan 2026 10:22:24 +0100 Subject: [PATCH] feat: Add source tracking to events + import scripts - Add source and source_note fields to NordaEvent model - Create import_calendar_2026.py for NORDA calendar events - Create import_excel_members_2026_01_13.py for new members - Add .private/ to .gitignore (confidential materials) Imported 26 events from Kalendarz Izby NORDA 2026 (Artur Wiertel) Imported 31 new member companies from Excel Co-Authored-By: Claude Opus 4.5 --- .gitignore | 3 + database.py | 4 + scripts/import_calendar_2026.py | 255 +++++++++ scripts/import_excel_members_2026_01_13.py | 637 +++++++++++++++++++++ 4 files changed, 899 insertions(+) create mode 100644 scripts/import_calendar_2026.py create mode 100644 scripts/import_excel_members_2026_01_13.py diff --git a/.gitignore b/.gitignore index 38c5c3b..0220d45 100644 --- a/.gitignore +++ b/.gitignore @@ -87,3 +87,6 @@ logs/security/ # Source data files (large, one-time use) data/krs_pdfs/ data/ceidg_json/ + +# Poufne materiały - NIGDY nie commitować +.private/ diff --git a/database.py b/database.py index 42e5d93..59b1e52 100644 --- a/database.py +++ b/database.py @@ -1051,6 +1051,10 @@ class NordaEvent(Base): created_by = Column(Integer, ForeignKey('users.id')) created_at = Column(DateTime, default=datetime.now) + # Źródło danych (tracking) + source = Column(String(255)) # np. 'kalendarz_norda_2026', 'manual', 'api' + source_note = Column(Text) # Pełna informacja o źródle + # Relationships speaker_company = relationship('Company') creator = relationship('User', foreign_keys=[created_by]) diff --git a/scripts/import_calendar_2026.py b/scripts/import_calendar_2026.py new file mode 100644 index 0000000..42b3718 --- /dev/null +++ b/scripts/import_calendar_2026.py @@ -0,0 +1,255 @@ +#!/usr/bin/env python3 +""" +Import Kalendarza Izby NORDA 2026 do NordaBiz +============================================= + +Źródło: Kalendarz Izby NORDA 2026.docx (Artur Wiertel) +Status: Omówiony na Radzie 07.01.2026, brak sprzeciwu + +Importuje: +- 11 Rad Izby (luty-grudzień, 07.01 już istnieje) +- 11 Chwil dla Biznesu (styczeń-listopad) +- 4 wydarzenia specjalne (rajd, festyn, żagle, bal) + +Uruchomienie: + python3 scripts/import_calendar_2026.py + python3 scripts/import_calendar_2026.py --dry-run +""" + +import os +import sys +from datetime import date, time +from sqlalchemy import create_engine, text +from sqlalchemy.orm import sessionmaker + +# Konfiguracja +DATABASE_URL = os.getenv('DATABASE_URL', 'postgresql://nordabiz_app:CHANGE_ME@127.0.0.1:5432/nordabiz') +SOURCE = "kalendarz_norda_2026" +SOURCE_NOTE = "Kalendarz Izby NORDA 2026 - propozycja Artura Wiertela, omówiony na Radzie Izby 07.01.2026 (bez sprzeciwu)" + +# ============================================================ +# DANE DO IMPORTU +# ============================================================ + +# Rady Izby - 1. środa miesiąca, 16:00 (07.01 już istnieje) +RADY_IZBY = [ + # (data, opis) + (date(2026, 2, 4), "Rada Izby NORDA - luty 2026"), + (date(2026, 3, 4), "Rada Izby NORDA - marzec 2026"), + (date(2026, 4, 1), "Rada Izby NORDA - kwiecień 2026"), + (date(2026, 5, 6), "Rada Izby NORDA - maj 2026"), + (date(2026, 6, 3), "Rada Izby NORDA - czerwiec 2026"), + (date(2026, 7, 1), "Rada Izby NORDA - lipiec 2026"), + (date(2026, 8, 5), "Rada Izby NORDA - sierpień 2026"), + (date(2026, 9, 2), "Rada Izby NORDA - wrzesień 2026"), + (date(2026, 10, 7), "Rada Izby NORDA - październik 2026"), + (date(2026, 11, 4), "Rada Izby NORDA - listopad 2026"), + (date(2026, 12, 2), "Rada Izby NORDA - grudzień 2026"), +] + +# Chwila dla Biznesu - ostatni czwartek miesiąca, 18:00, Hotel Olimp +CHWILA_DLA_BIZNESU = [ + (date(2026, 1, 29), "Chwila dla Biznesu - styczeń 2026"), + (date(2026, 2, 26), "Chwila dla Biznesu - luty 2026"), + (date(2026, 3, 26), "Chwila dla Biznesu - marzec 2026"), + (date(2026, 4, 30), "Chwila dla Biznesu - kwiecień 2026"), + (date(2026, 5, 28), "Chwila dla Biznesu - maj 2026"), + (date(2026, 6, 25), "Chwila dla Biznesu - czerwiec 2026"), + (date(2026, 7, 30), "Chwila dla Biznesu - lipiec 2026"), + (date(2026, 8, 27), "Chwila dla Biznesu - sierpień 2026"), + (date(2026, 9, 24), "Chwila dla Biznesu - wrzesień 2026"), + (date(2026, 10, 29), "Chwila dla Biznesu - październik 2026"), + (date(2026, 11, 26), "Chwila dla Biznesu - listopad 2026"), +] + +# Wydarzenia specjalne +WYDARZENIA_SPECJALNE = [ + { + "title": "Rajd Rowerowy NORDA 2026", + "event_date": date(2026, 5, 29), + "event_type": "other", + "time_start": time(10, 0), + "time_end": time(15, 0), + "location": "Start: Wejherowo (do ustalenia)", + "description": "Coroczny rajd rowerowy członków Izby NORDA. Ostatni piątek maja.", + }, + { + "title": "Festyn \"Norda na zielono\" - Park Majkowskiego", + "event_date": date(2026, 6, 13), + "event_type": "other", + "time_start": time(10, 0), + "time_end": time(18, 0), + "location": "Park Majkowskiego, Wejherowo", + "description": "Festyn ekologiczny we współpracy z Ekofabryką. Promocja firm członkowskich.", + }, + { + "title": "Integracyjne Żagle w Chorwacji", + "event_date": date(2026, 9, 12), + "event_type": "other", + "time_start": None, + "time_end": None, + "location": "Chorwacja (szczegóły do ustalenia)", + "description": "Wyjazd integracyjny członków Izby. 12-19 września 2026. Propozycja omówiona na Radzie.", + }, + { + "title": "Bal Integracyjny NORDA 2026", + "event_date": date(2026, 11, 21), + "event_type": "other", + "time_start": time(18, 0), + "time_end": time(23, 59), + "location": "Do ustalenia", + "description": "Coroczny bal integracyjny członków Izby NORDA.", + "is_featured": True, + }, +] + + +def main(): + dry_run = '--dry-run' in sys.argv + + print("=" * 60) + print("IMPORT KALENDARZA NORDA 2026") + print(f"Źródło: {SOURCE}") + if dry_run: + print("TRYB: DRY-RUN (bez zapisu)") + print("=" * 60) + + engine = create_engine(DATABASE_URL) + Session = sessionmaker(bind=engine) + session = Session() + + try: + added = 0 + skipped = 0 + + # 1. Import Rad Izby + print("\n[1/3] Rady Izby (meeting)") + for event_date, title in RADY_IZBY: + # Sprawdź czy już istnieje + existing = session.execute( + text("SELECT id FROM norda_events WHERE event_date = :d AND title LIKE '%Rada%'"), + {"d": event_date} + ).fetchone() + + if existing: + print(f" → Pominięto (istnieje): {event_date} - {title}") + skipped += 1 + continue + + if not dry_run: + session.execute( + text(""" + INSERT INTO norda_events + (title, description, event_type, event_date, time_start, location, source, source_note, created_at) + VALUES (:title, :desc, 'meeting', :event_date, :time_start, :location, :source, :source_note, NOW()) + """), + { + "title": title, + "desc": "Cykliczne spotkanie Rady Izby NORDA. Pierwsza środa miesiąca.", + "event_date": event_date, + "time_start": time(16, 0), + "location": "Biuro Izby NORDA, Wejherowo", + "source": SOURCE, + "source_note": SOURCE_NOTE, + } + ) + print(f" ✓ Dodano: {event_date} - {title}") + added += 1 + + # 2. Import Chwil dla Biznesu + print("\n[2/3] Chwila dla Biznesu (networking)") + for event_date, title in CHWILA_DLA_BIZNESU: + # Sprawdź czy już istnieje + existing = session.execute( + text("SELECT id FROM norda_events WHERE event_date = :d AND title LIKE '%Chwila%'"), + {"d": event_date} + ).fetchone() + + if existing: + print(f" → Pominięto (istnieje): {event_date} - {title}") + skipped += 1 + continue + + if not dry_run: + session.execute( + text(""" + INSERT INTO norda_events + (title, description, event_type, event_date, time_start, location, source, source_note, created_at) + VALUES (:title, :desc, 'networking', :event_date, :time_start, :location, :source, :source_note, NOW()) + """), + { + "title": title, + "desc": "Cykliczne spotkanie networkingowe członków Izby. Ostatni czwartek miesiąca, godz. 18:00.", + "event_date": event_date, + "time_start": time(18, 0), + "location": "Hotel Olimp, Wejherowo", + "source": SOURCE, + "source_note": SOURCE_NOTE, + } + ) + print(f" ✓ Dodano: {event_date} - {title}") + added += 1 + + # 3. Import wydarzeń specjalnych + print("\n[3/3] Wydarzenia specjalne") + for event in WYDARZENIA_SPECJALNE: + # Sprawdź czy już istnieje (po dacie i tytule) + existing = session.execute( + text("SELECT id FROM norda_events WHERE event_date = :d"), + {"d": event["event_date"]} + ).fetchone() + + if existing: + print(f" → Pominięto (istnieje): {event['event_date']} - {event['title']}") + skipped += 1 + continue + + if not dry_run: + session.execute( + text(""" + INSERT INTO norda_events + (title, description, event_type, event_date, time_start, time_end, location, + is_featured, source, source_note, created_at) + VALUES (:title, :desc, :event_type, :event_date, :time_start, :time_end, :location, + :is_featured, :source, :source_note, NOW()) + """), + { + "title": event["title"], + "desc": event["description"], + "event_type": event["event_type"], + "event_date": event["event_date"], + "time_start": event.get("time_start"), + "time_end": event.get("time_end"), + "location": event["location"], + "is_featured": event.get("is_featured", False), + "source": SOURCE, + "source_note": SOURCE_NOTE, + } + ) + print(f" ✓ Dodano: {event['event_date']} - {event['title']}") + added += 1 + + if not dry_run: + session.commit() + + # Podsumowanie + print("\n" + "=" * 60) + print("PODSUMOWANIE") + print("=" * 60) + print(f" Dodano: {added}") + print(f" Pominięto (już istnieją): {skipped}") + if dry_run: + print("\n [DRY-RUN] Żadne dane nie zostały zapisane.") + else: + print("\n✅ Import zakończony pomyślnie!") + + except Exception as e: + session.rollback() + print(f"\n❌ Błąd: {e}") + sys.exit(1) + finally: + session.close() + + +if __name__ == "__main__": + main() diff --git a/scripts/import_excel_members_2026_01_13.py b/scripts/import_excel_members_2026_01_13.py new file mode 100644 index 0000000..0e71c2e --- /dev/null +++ b/scripts/import_excel_members_2026_01_13.py @@ -0,0 +1,637 @@ +#!/usr/bin/env python3 +""" +Import nowych członków NORDA z pliku Excel +Źródło: Aktualna lista kontaktow wraz z data przystapienia.xlsx +Data importu: 2026-01-13 +Przesłane przez: Artur Wiertel + +Ten skrypt: +1. Dodaje nowe źródło danych "Excel - Lista członków NORDA (2026-01-13)" +2. Importuje nowe firmy z oznaczeniem źródła +3. Aktualizuje istniejące firmy (dla starych 80 firm oznacza źródło) +""" + +import os +import sys +import re +from datetime import datetime + +# Dodaj ścieżkę do głównego katalogu aplikacji +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from sqlalchemy import create_engine, text +from sqlalchemy.orm import sessionmaker + +# Konfiguracja bazy danych - używaj zmiennej środowiskowej +DATABASE_URL = os.getenv('DATABASE_URL', 'postgresql://nordabiz_app:CHANGE_ME@127.0.0.1:5432/nordabiz') + +# Źródło danych +DATA_SOURCE_NAME = "Excel - Lista członków NORDA" +DATA_SOURCE_DESC = "Import z pliku Excel od Artura Wiertela (2026-01-13). Plik: Aktualna lista kontaktow wraz z data przystapienia.xlsx" +DATA_SOURCE_TYPE = "excel" +IMPORT_DATE = "2026-01-13" + +# Nowe firmy do dodania (z Excela) +NEW_COMPANIES = [ + { + "name": "Dom Dziecka Pro-Sport", + "legal_name": "Dom Dziecka", + "contact_person": "Marek Mroske", + "phone": "604178533", + "email": "marekm@prosport.wejher.pl", + "address_full": "ul. Sobieskiego 215, 84-200 Wejherowo", + "address_city": "Wejherowo", + "address_postal": "84-200", + "joined": "1997-10-20", + "category": "Other", + }, + { + "name": "Event Investycje", + "legal_name": "Event Investycje Sp. z o.o.", + "contact_person": "Krzysztof Bombera", + "phone": "790272510", + "email": "krzysztof@bombera.pl", + "address_full": "ul. Kręckiego 3/5, 80-318 Gdańsk", + "address_city": "Gdańsk", + "address_postal": "80-318", + "joined": "2016-11-06", + "category": "Services", + }, + { + "name": "Kancelaria Notarialna Henryk Mizak", + "legal_name": "Kancelaria Notarialna", + "contact_person": "Henryk Mizak", + "phone": None, + "email": "notariat306@wp.pl", + "address_full": "ul. Sobieskiego 306, 84-200 Wejherowo", + "address_city": "Wejherowo", + "address_postal": "84-200", + "joined": None, + "category": "Services", + }, + { + "name": "Orlex Design", + "legal_name": "Orlex Design", + "contact_person": "Michał Gębarowski", + "phone": "601318227", + "email": "biuro@orlexbeton.pl", + "address_full": "ul. Mickiewicza 7, 84-241 Gościcino", + "address_city": "Gościcino", + "address_postal": "84-241", + "joined": "2018-06-26", + "category": "Construction", + }, + { + "name": "PTHU Cezary Mazur", + "legal_name": "PTHU Cezary Mazur", + "contact_person": "Cezary Mazur", + "phone": "601652556", + "email": "pthumazur@gmail.com", + "address_full": "ul. Grunwaldzka 12, 84-230 Rumia", + "address_city": "Rumia", + "address_postal": "84-230", + "joined": "2023-04-05", + "category": "Trade", + }, + { + "name": "Podróże i My", + "legal_name": "Podróże i My", + "contact_person": "Witold Gulcz", + "phone": "602697969", + "email": "w.gulcz@itakarumia.pl", + "address_full": "ul. Wierzbięcice 44A/21B, 61-583 Poznań", + "address_city": "Poznań", + "address_postal": "61-583", + "joined": "2018-11-06", + "category": "Services", + }, + { + "name": "Kancelaria Radcy Prawnego Joanna Ostrowska", + "legal_name": "Radca Prawny Joanna Ostrowska", + "contact_person": "Joanna Ostrowska", + "phone": None, + "email": None, + "address_full": None, + "address_city": "Wejherowo", + "address_postal": None, + "joined": None, + "category": "Services", + }, + { + "name": "PZU TFI", + "legal_name": "PZU SA Towarzystwo Funduszy Inwestycyjnych", + "contact_person": "Łukasz Antoniak", + "phone": "887875775", + "email": "lantoniak@pzu.pl", + "address_full": "Rondo I. Daszyński 4, 00-843 Warszawa", + "address_city": "Warszawa", + "address_postal": "00-843", + "joined": "2019-02-05", + "category": "Services", + }, + { + "name": "Rozsądni Bracia", + "legal_name": "Rozsądni Bracia Sp. z o.o.", + "contact_person": "Robert Kiereś, Piotr Kiereś, Michał Kiereś", + "phone": "737856865", + "email": "kontakt@rozsandnibracia.pl", + "address_full": "ul. Wałowa 30/8, 84-200 Wejherowo", + "address_city": "Wejherowo", + "address_postal": "84-200", + "joined": "2024-05-08", + "category": "Services", + }, + { + "name": "TERMO", + "legal_name": "TERMO Sp. z o.o.", + "contact_person": "Bartłomiej Komkowski", + "phone": "503177099", + "email": "biuro@termocenter.pl", + "address_full": "ul. Tulipanowa 14, 84-252 Góra", + "address_city": "Góra", + "address_postal": "84-252", + "joined": "2025-04-01", + "category": "Construction", + }, + { + "name": "Thai Union Poland", + "legal_name": "Thai Union", + "contact_person": "Piotr Sadowski", + "phone": None, + "email": None, + "address_full": None, + "address_city": None, + "address_postal": None, + "joined": None, + "category": "Production", + }, + { + "name": "Usługi Ogólnobudowlane Sławomir Grula", + "legal_name": "Usługi Ogólnobudowlane", + "contact_person": "Sławomir Grula", + "phone": "502919634", + "email": "slawek1055@onet.pl", + "address_full": "ul. Wiejska 6, 84-218 Łęczyce", + "address_city": "Łęczyce", + "address_postal": "84-218", + "joined": None, + "category": "Construction", + }, + { + "name": "Wodmel", + "legal_name": "Wodmel Sp. J.", + "contact_person": "Tomasz Potrykus, Jan Derra", + "phone": None, + "email": "wodmel@op.pl", + "address_full": "ul. Przemysłowa 31A, 84-200 Wejherowo", + "address_city": "Wejherowo", + "address_postal": "84-200", + "joined": "1997-10-14", + "category": "Construction", + }, + { + "name": "IT Space", + "legal_name": "IT Space B & Łaga Bartosz", + "contact_person": "Bartosz Łaga", + "phone": None, + "email": "vwmania6@gmail.com", + "address_full": None, + "address_city": None, + "address_postal": None, + "joined": "2025-08-01", + "category": "IT", + }, + { + "name": "Aertom", + "legal_name": "Aertom", + "contact_person": "Tomasz Dzienisz", + "phone": None, + "email": "dzienisztomasz@gmail.com", + "address_full": None, + "address_city": None, + "address_postal": None, + "joined": "2025-08-01", + "category": "Other", + }, + { + "name": "Wikęd", + "legal_name": "Wikęd", + "contact_person": "Grzegorz Wiśniewski", + "phone": None, + "email": "gwiked@wp.pl", + "address_full": None, + "address_city": None, + "address_postal": None, + "joined": "2025-08-01", + "category": "Production", + }, + { + "name": "Unimot", + "legal_name": "Unimot", + "contact_person": "Łukasz Pawłowski, Tomasz Barejka", + "phone": "509109999", + "email": "Tomasz.barejka@unimot-eig.pl", + "address_full": None, + "address_city": None, + "address_postal": None, + "joined": "2025-11-01", + "category": "Trade", + }, + { + "name": "Omega Energy", + "legal_name": "Omega", + "contact_person": "Piotr Karbowski", + "phone": "601679494", + "email": "p.karbowski@omega-energy.eu", + "address_full": None, + "address_city": None, + "address_postal": None, + "joined": "2025-11-01", + "category": "Production", + }, + { + "name": "Nowatel", + "legal_name": "Nowatel", + "contact_person": "Łukasz Dominik, Stanisław Czech", + "phone": "607623100", + "email": "biuro@nowatel.com", + "address_full": None, + "address_city": None, + "address_postal": None, + "joined": "2025-11-01", + "category": "IT", + }, + { + "name": "Informatyk1", + "legal_name": "Informatyk1", + "contact_person": "Mateusz Kurpet", + "phone": "602354351", + "email": "mateusz@informatyk1.pl", + "address_full": None, + "address_city": None, + "address_postal": None, + "joined": "2025-11-01", + "category": "IT", + }, + { + "name": "Piotrex", + "legal_name": "Piotrex", + "contact_person": "Piotr Wieczorek", + "phone": "518842122", + "email": "biuro@piotrex.info", + "address_full": None, + "address_city": None, + "address_postal": None, + "joined": "2025-12-01", + "category": "Other", + }, + { + "name": "Kancelaria Ostrowski i Wspólnicy", + "legal_name": "Kancelaria Ostrowski i wspólnicy", + "contact_person": "Paweł Cioban", + "phone": "727591189", + "email": "p.cioban@ostrowski-legal.net", + "address_full": None, + "address_city": None, + "address_postal": None, + "joined": "2025-12-01", + "category": "Services", + }, + { + "name": "Family Art", + "legal_name": "Family Art", + "contact_person": "Artur Czaja", + "phone": "531972354", + "email": "artczaj@gmail.com", + "address_full": None, + "address_city": None, + "address_postal": None, + "joined": "2025-12-01", + "category": "Other", + }, + { + "name": "Renk Hurtownie", + "legal_name": "Renk – Pomorskie Hurtownie Centrum Rolno-Spożywcze", + "contact_person": "Marcin Bulczak", + "phone": "504228997", + "email": "sekretariat@renk.pl", + "address_full": None, + "address_city": None, + "address_postal": None, + "joined": "2025-12-01", + "category": "Trade", + }, + { + "name": "Elzit", + "legal_name": "Elzit", + "contact_person": "Bartosz Lang", + "phone": "603089984", + "email": "b.lang@elzit.pl", + "address_full": None, + "address_city": None, + "address_postal": None, + "joined": "2025-11-01", + "category": "IT", + }, + { + "name": "Your Welcome", + "legal_name": "Your Welcome", + "contact_person": "Agnieszka Kulinkowska", + "phone": "519345844", + "email": "agnieszka@yourewelcome.pl", + "address_full": None, + "address_city": None, + "address_postal": None, + "joined": "2025-11-01", + "category": "Services", + }, + { + "name": "3W", + "legal_name": "3W", + "contact_person": "Maciej Kasyna", + "phone": "664019585", + "email": "maciej.kasyna@3wdb.pl", + "address_full": None, + "address_city": None, + "address_postal": None, + "joined": "2025-11-01", + "category": "IT", + }, + { + "name": "Wakat", + "legal_name": "Wakat", + "contact_person": "Dariusz Krasiński", + "phone": "600447635", + "email": "darek.krasinski@wp.pl", + "address_full": None, + "address_city": None, + "address_postal": None, + "joined": "2025-11-01", + "category": "Other", + }, + { + "name": "BIS Maszyny", + "legal_name": "BIS Maszyny", + "contact_person": "Jarosław Rohraff", + "phone": "602228400", + "email": "jarek@bis-bau.pl", + "address_full": None, + "address_city": None, + "address_postal": None, + "joined": "2025-11-01", + "category": "Trade", + }, + { + "name": "Elgreen EM", + "legal_name": "Elgreen EM P.S.A.", + "contact_person": "Krzysztof Zanaboni", + "phone": "514254267", + "email": "kz@elgreen.pl", + "address_full": None, + "address_city": None, + "address_postal": None, + "joined": "2025-10-01", + "category": "Production", + }, + { + "name": "Iwona Spaleniak Coaching", + "legal_name": "Iwona Spaleniak Coach 4 you", + "contact_person": "Iwona Spaleniak", + "phone": "608336529", + "email": "kontakt@iwonaspaleniak.pl", + "address_full": None, + "address_city": None, + "address_postal": None, + "joined": "2026-01-01", + "category": "Services", + }, +] + +# Kategorie (mapowanie nazwa -> id) +CATEGORY_MAP = { + "IT": 1, + "Construction": 2, + "Services": 3, + "Production": 4, + "Trade": 5, + "Other": 6, +} + + +def slugify(text): + """Generuje slug z nazwy firmy""" + text = text.lower() + # Zamień polskie znaki + replacements = { + 'ą': 'a', 'ć': 'c', 'ę': 'e', 'ł': 'l', 'ń': 'n', + 'ó': 'o', 'ś': 's', 'ź': 'z', 'ż': 'z' + } + for pl, en in replacements.items(): + text = text.replace(pl, en) + # Usuń znaki specjalne, zostaw tylko litery, cyfry i spacje + text = re.sub(r'[^a-z0-9\s-]', '', text) + # Zamień spacje na myślniki + text = re.sub(r'\s+', '-', text.strip()) + # Usuń podwójne myślniki + text = re.sub(r'-+', '-', text) + return text + + +def clean_phone(phone): + """Czyści numer telefonu""" + if not phone: + return None + # Usuń wszystko oprócz cyfr i spacji + phone = re.sub(r'[^\d\s]', '', str(phone)) + phone = phone.strip() + return phone if phone else None + + +def clean_email(email): + """Czyści email - bierze pierwszy jeśli jest kilka""" + if not email: + return None + email = str(email).strip() + # Jeśli jest kilka emaili, weź pierwszy + if ';' in email: + email = email.split(';')[0].strip() + if ',' in email: + email = email.split(',')[0].strip() + # Wyciągnij email z formatu "Nazwa " + match = re.search(r'<([^>]+)>', email) + if match: + email = match.group(1) + # Sprawdź czy wygląda jak email + if '@' not in email: + return None + return email.lower().strip() + + +def main(): + print("=" * 60) + print("IMPORT CZŁONKÓW NORDA Z EXCELA") + print(f"Data: {IMPORT_DATE}") + print(f"Źródło: {DATA_SOURCE_NAME}") + print("=" * 60) + + # Połącz z bazą + engine = create_engine(DATABASE_URL) + Session = sessionmaker(bind=engine) + session = Session() + + try: + # 1. Dodaj nowe źródło danych jeśli nie istnieje + print("\n[1/4] Dodawanie źródła danych...") + result = session.execute(text( + "SELECT id FROM data_sources WHERE name = :name" + ), {"name": DATA_SOURCE_NAME}) + existing = result.fetchone() + + if existing: + source_id = existing[0] + print(f" Źródło już istnieje (ID: {source_id})") + else: + session.execute(text(""" + INSERT INTO data_sources (name, type, description, reliability_score, is_active) + VALUES (:name, :type, :desc, 4, true) + """), { + "name": DATA_SOURCE_NAME, + "type": DATA_SOURCE_TYPE, + "desc": DATA_SOURCE_DESC + }) + result = session.execute(text( + "SELECT id FROM data_sources WHERE name = :name" + ), {"name": DATA_SOURCE_NAME}) + source_id = result.fetchone()[0] + print(f" Dodano źródło (ID: {source_id})") + + # 2. Oznacz istniejące firmy źródłem + print("\n[2/4] Oznaczanie istniejących firm źródłem importu...") + # Najpierw sprawdź ile firm nie ma oznaczonego źródła + result = session.execute(text( + "SELECT COUNT(*) FROM companies WHERE data_source IS NULL OR data_source = ''" + )) + count_no_source = result.fetchone()[0] + + if count_no_source > 0: + session.execute(text(""" + UPDATE companies + SET data_source = 'norda-biznes.info (pierwotny import)' + WHERE data_source IS NULL OR data_source = '' + """)) + print(f" Oznaczono {count_no_source} firm jako 'norda-biznes.info (pierwotny import)'") + else: + print(" Wszystkie firmy mają już oznaczone źródło") + + # 3. Importuj nowe firmy + print(f"\n[3/4] Importowanie {len(NEW_COMPANIES)} nowych firm...") + imported = 0 + skipped = 0 + + for company in NEW_COMPANIES: + # Sprawdź czy firma już istnieje + slug = slugify(company["name"]) + result = session.execute(text( + "SELECT id FROM companies WHERE slug = :slug" + ), {"slug": slug}) + existing = result.fetchone() + + if existing: + print(f" ⏭ {company['name']} - już istnieje (slug: {slug})") + skipped += 1 + continue + + # Pobierz ID kategorii + category_id = CATEGORY_MAP.get(company.get("category"), 6) # 6 = Other + + # Przygotuj dane + email = clean_email(company.get("email")) + phone = clean_phone(company.get("phone")) + + # Wstaw firmę + session.execute(text(""" + INSERT INTO companies ( + name, legal_name, slug, category_id, + email, phone, + address_full, address_city, address_postal, + data_source, data_quality, status, + created_at + ) VALUES ( + :name, :legal_name, :slug, :category_id, + :email, :phone, + :address_full, :address_city, :address_postal, + :data_source, 'basic', 'active', + :created_at + ) + """), { + "name": company["name"], + "legal_name": company.get("legal_name"), + "slug": slug, + "category_id": category_id, + "email": email, + "phone": phone, + "address_full": company.get("address_full"), + "address_city": company.get("address_city"), + "address_postal": company.get("address_postal"), + "data_source": f"excel_norda_{IMPORT_DATE}", + "created_at": datetime.now(), + }) + + # Pobierz ID nowej firmy + result = session.execute(text( + "SELECT id FROM companies WHERE slug = :slug" + ), {"slug": slug}) + company_id = result.fetchone()[0] + + # Dodaj wpis do company_data_sources + session.execute(text(""" + INSERT INTO company_data_sources (company_id, source_id, collected_at, fields_collected) + VALUES (:company_id, :source_id, :collected_at, :fields) + """), { + "company_id": company_id, + "source_id": source_id, + "collected_at": datetime.now(), + "fields": ["name", "email", "phone", "address"], + }) + + # Dodaj osobę kontaktową i datę przystąpienia do Nordy + updates = [] + if company.get("contact_person"): + updates.append(f"Osoba kontaktowa: {company['contact_person']}") + if company.get("joined"): + updates.append(f"Członek NORDA od: {company['joined']}") + + if updates: + session.execute(text(""" + UPDATE companies + SET description_short = :desc, + founding_history = :history + WHERE id = :company_id + """), { + "company_id": company_id, + "desc": updates[0] if len(updates) == 1 else None, + "history": f"Członek NORDA od: {company.get('joined', 'brak danych')}. " + (f"Kontakt: {company.get('contact_person')}" if company.get('contact_person') else ""), + }) + + print(f" ✓ {company['name']} (ID: {company_id})") + imported += 1 + + # 4. Podsumowanie + print(f"\n[4/4] Podsumowanie:") + print(f" Zaimportowano: {imported} firm") + print(f" Pominięto (już istnieją): {skipped} firm") + + # Zatwierdź transakcję + session.commit() + print("\n✅ Import zakończony pomyślnie!") + + except Exception as e: + session.rollback() + print(f"\n❌ Błąd: {e}") + raise + finally: + session.close() + + +if __name__ == "__main__": + main()