nordabiz/scripts/import_board_meeting_2_2026.py
Maciej Pienczyn a8f2178b7e
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
feat: activate board document upload/download with meeting 2/2026 import
Add document management routes (upload, download, soft-delete) to board blueprint,
link BoardDocument to BoardMeeting via meeting_id FK, add documents section to
meeting view template, and include import scripts for meeting 2/2026 data and PDFs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:04:44 +01:00

301 lines
14 KiB
Python

#!/usr/bin/env python3
"""
Import Board Meeting 2/2026 (04.02.2026) data.
One-time script to create BoardMeeting record with full agenda,
attendance, proceedings and decisions from the February 4 session.
Usage:
# Local dev:
python3 scripts/import_board_meeting_2_2026.py
# Production:
DATABASE_URL=$(grep DATABASE_URL .env | cut -d'=' -f2) \
/var/www/nordabiznes/venv/bin/python3 scripts/import_board_meeting_2_2026.py
"""
import os
import sys
from datetime import date, time, datetime
# Add project root to path
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from database import SessionLocal, BoardMeeting, User
def get_user_by_email(db, email):
"""Find user by email, return None if not found."""
return db.query(User).filter(User.email == email).first()
def main():
db = SessionLocal()
try:
# Check if meeting already exists
existing = db.query(BoardMeeting).filter(
BoardMeeting.meeting_number == 2,
BoardMeeting.year == 2026
).first()
if existing:
print(f"Meeting 2/2026 already exists (id={existing.id}). Skipping.")
return existing.id
# Resolve key users
chairperson = get_user_by_email(db, 'leszek@rotor.pl')
secretary = get_user_by_email(db, 'magdalena.kloska@norda-biznes.info')
# Fallback: try partial match for secretary
if not secretary:
secretary = db.query(User).filter(User.email.like('%kloska%')).first()
# Find admin/office_manager as creator
creator = db.query(User).filter(
User.role.in_(['ADMIN', 'OFFICE_MANAGER']),
User.is_active == True
).first()
if not creator:
print("ERROR: No ADMIN or OFFICE_MANAGER user found. Cannot set created_by.")
return None
print(f"Chairperson: {chairperson.name if chairperson else 'NOT FOUND'} (id={chairperson.id if chairperson else '?'})")
print(f"Secretary: {secretary.name if secretary else 'NOT FOUND'} (id={secretary.id if secretary else '?'})")
print(f"Creator: {creator.name} (id={creator.id})")
# Build attendance map: email -> status
PRESENT_EMAILS = [
'leszek@rotor.pl', # Glaza
'andrzej.gorczycki@zukwejherowo.pl', # Gorczycki
'pawel.kwidzinski@norda-biznes.info', # Kwidzinski
'dariusz.schmidtke@tkchopin.pl', # Schmidtke
'artur.wiertel@norda-biznes.info', # Wiertel
'a.jedrzejewski@scrol.pl', # Jedrzejewski
'info@greenhousesystems.pl', # Piechocka
'jm@hebel-masiak.pl', # Masiak
'kuba@bormax.com.pl', # Bornowski
'pawel.piechota@norda-biznes.info', # Piechota
'radoslaw@skwarlo.pl', # Skwarlo
'roman@sigmabudownictwo.pl', # Wiercinski
'mjwesierski@gmail.com', # Wesierski
]
ABSENT_EMAILS = [
'iwonamusial@cristap.pl', # Musial
'krzysztof.kubis@sibuk.pl', # Kubis
'jacek.pomieczynski@eura-tech.eu', # Pomieczynski
]
attendance = {}
for email in PRESENT_EMAILS:
user = get_user_by_email(db, email)
if user:
# Generate initials from name
initials = ''.join(p[0].upper() for p in (user.name or '').split() if p) or ''
attendance[str(user.id)] = {
'status': 'present',
'present': True,
'initials': initials
}
else:
print(f" WARNING: User not found: {email}")
for email in ABSENT_EMAILS:
user = get_user_by_email(db, email)
if user:
initials = ''.join(p[0].upper() for p in (user.name or '').split() if p) or ''
attendance[str(user.id)] = {
'status': 'absent',
'present': False,
'initials': initials
}
else:
print(f" WARNING: User not found: {email}")
present_count = sum(1 for a in attendance.values() if a['present'])
print(f"Attendance: {present_count}/{len(attendance)} present")
# Agenda items (15 points)
agenda_items = [
{"time_start": "16:00", "time_end": "16:05", "title": "Otwarcie posiedzenia i przyjęcie programu"},
{"time_start": "16:05", "time_end": "16:10", "title": "Przyjęcie protokołu z posiedzenia nr 1/2026"},
{"time_start": "16:10", "time_end": "16:30", "title": "Prezentacja i głosowanie nad kandydatami na nowych członków Izby"},
{"time_start": "16:30", "time_end": "16:45", "title": "Informacja o stanie finansów Izby"},
{"time_start": "16:45", "time_end": "17:00", "title": "Omówienie składek członkowskich na 2026 rok"},
{"time_start": "17:00", "time_end": "17:15", "title": "Spotkanie dot. marketingu i komunikacji"},
{"time_start": "17:15", "time_end": "17:30", "title": "Planowanie grilla integracyjnego"},
{"time_start": "17:30", "time_end": "17:45", "title": "Obchody 30-lecia Izby"},
{"time_start": "17:45", "time_end": "18:00", "title": "Konkurs Tytani Przedsiębiorczości"},
{"time_start": "18:00", "time_end": "18:10", "title": "Aplikacja NordaBiznes — aktualizacja"},
{"time_start": "18:10", "time_end": "18:20", "title": "Współpraca z samorządem"},
{"time_start": "18:20", "time_end": "18:30", "title": "Walne Zgromadzenie Członków — termin"},
{"time_start": "18:30", "time_end": "18:40", "title": "Ustalenie terminu kolejnego posiedzenia Rady"},
{"time_start": "18:40", "time_end": "18:55", "title": "Wolne wnioski i dyskusja"},
{"time_start": "18:55", "time_end": "19:00", "title": "Zamknięcie posiedzenia"},
]
# Proceedings (key discussions and decisions)
proceedings = [
{
"agenda_item": 0,
"title": "Otwarcie posiedzenia i przyjęcie programu",
"discussion": "Prezes Leszek Glaza otworzył posiedzenie, powitał obecnych członków Rady. Stwierdzono kworum (13 z 16 członków). Program posiedzenia przyjęto jednogłośnie.",
"decisions": ["Program posiedzenia nr 2/2026 przyjęty jednogłośnie"],
"tasks": []
},
{
"agenda_item": 1,
"title": "Przyjęcie protokołu z posiedzenia nr 1/2026",
"discussion": "Protokół z posiedzenia Rady nr 1/2026 z dnia 07.01.2026 został przedstawiony członkom Rady.",
"decisions": ["Protokół z posiedzenia nr 1/2026 przyjęty jednogłośnie"],
"tasks": []
},
{
"agenda_item": 2,
"title": "Prezentacja i głosowanie nad kandydatami na nowych członków Izby",
"discussion": "Przedstawiono 5 kandydatów na nowych członków Izby Przedsiębiorców NORDA:\n\n1. Konkol Sp. z o.o. — branża budowlana, rekomendacja od członka Rady\n2. Ibet Sp. z o.o. — producent kostki brukowej i elementów betonowych\n3. Audioline — usługi audiologiczne\n4. PC Invest — inwestycje i nieruchomości\n5. Pacific Sun / Fiume — branża turystyczna i gastronomiczna\n\nKażdy kandydat został krótko przedstawiony wraz z uzasadnieniem rekomendacji.",
"decisions": [
"Przyjęto jednogłośnie firmę Konkol Sp. z o.o. jako nowego członka Izby",
"Przyjęto jednogłośnie firmę Ibet Sp. z o.o. jako nowego członka Izby",
"Przyjęto jednogłośnie firmę Audioline jako nowego członka Izby",
"Przyjęto jednogłośnie firmę PC Invest jako nowego członka Izby",
"Przyjęto jednogłośnie firmę Pacific Sun / Fiume jako nowego członka Izby"
],
"tasks": ["Przygotować dokumenty przyjęcia dla 5 nowych członków"]
},
{
"agenda_item": 3,
"title": "Informacja o stanie finansów Izby",
"discussion": "Przedstawiono bieżący stan finansów Izby. Omówiono wpływy ze składek oraz wydatki operacyjne.",
"decisions": [],
"tasks": []
},
{
"agenda_item": 4,
"title": "Omówienie składek członkowskich na 2026 rok",
"discussion": "Dyskutowano nad wysokością składek członkowskich od stycznia 2026. Zaproponowano podział na małe i duże firmy.",
"decisions": [
"Składki od 01.2026: małe firmy 200 zł/miesiąc, duże firmy 300 zł/miesiąc (głosowanie: 12 za, 0 przeciw, 1 wstrzymujący się)"
],
"tasks": ["Przygotować informację o nowych stawkach składek dla członków"]
},
{
"agenda_item": 5,
"title": "Spotkanie dot. marketingu i komunikacji",
"discussion": "Omówiono potrzebę spotkania roboczego dot. strategii social media i komunikacji marketingowej Izby.",
"decisions": [
"Spotkanie ws. social media wyznaczone na 18.02.2026, godz. 09:00, Ekofabryka"
],
"tasks": ["Przygotować spotkanie ws. social media na 18.02.2026"]
},
{
"agenda_item": 6,
"title": "Planowanie grilla integracyjnego",
"discussion": "Omówiono organizację grilla integracyjnego dla członków Izby. Zaproponowano termin i lokalizację.",
"decisions": [
"Grill integracyjny: 16.05.2026, strzelnica / Bractwo Kurkowe Wejherowo"
],
"tasks": ["Zarezerwować lokalizację na grill integracyjny 16.05.2026"]
},
{
"agenda_item": 7,
"title": "Obchody 30-lecia Izby",
"discussion": "Omówiono plany obchodów 30-lecia istnienia Izby Przedsiębiorców NORDA. Powołano komitet organizacyjny.",
"decisions": [
"Komitet 30-lecia Izby: DS, AG, RW + Zarząd"
],
"tasks": ["Komitet 30-lecia — rozpocząć planowanie obchodów"]
},
{
"agenda_item": 8,
"title": "Konkurs Tytani Przedsiębiorczości",
"discussion": "Omówiono stan przygotowań do kolejnej edycji konkursu Tytani Przedsiębiorczości Powiatu Wejherowskiego.",
"decisions": [],
"tasks": []
},
{
"agenda_item": 9,
"title": "Aplikacja NordaBiznes — aktualizacja",
"discussion": "Przedstawiono postępy w rozwoju aplikacji NordaBiznes Partner — katalogu firmowego i platformy networkingowej Izby.",
"decisions": [],
"tasks": []
},
{
"agenda_item": 10,
"title": "Współpraca z samorządem",
"discussion": "Omówiono bieżącą współpracę z samorządem lokalnym.",
"decisions": [],
"tasks": []
},
{
"agenda_item": 11,
"title": "Walne Zgromadzenie Członków — termin",
"discussion": "Ustalono termin i miejsce Walnego Zgromadzenia Członków Izby (zgromadzenie wyborcze).",
"decisions": [
"Walne Zgromadzenie Członków (wyborcze): 08.06.2026, godz. 14:00, Urząd Miasta Wejherowo"
],
"tasks": ["Przygotować zawiadomienia o Walnym Zgromadzeniu"]
},
{
"agenda_item": 12,
"title": "Ustalenie terminu kolejnego posiedzenia Rady",
"discussion": "Zaproponowano termin kolejnego posiedzenia Rady Izby.",
"decisions": [
"Propozycja kolejnego posiedzenia Rady: 04.03.2026, godz. 16:00"
],
"tasks": []
},
]
# Create meeting
meeting = BoardMeeting(
meeting_number=2,
year=2026,
meeting_date=date(2026, 2, 4),
start_time=time(16, 0),
end_time=time(19, 0),
location='Siedziba Izby',
chairperson_id=chairperson.id if chairperson else None,
secretary_id=secretary.id if secretary else None,
guests=None,
agenda_items=agenda_items,
attendance=attendance,
quorum_count=present_count,
quorum_confirmed=present_count >= 9,
proceedings=proceedings,
status=BoardMeeting.STATUS_PROTOCOL_PUBLISHED,
created_by=creator.id,
created_at=datetime(2026, 2, 4, 19, 0),
agenda_published_at=datetime(2026, 2, 4, 16, 0),
protocol_published_at=datetime(2026, 2, 20, 12, 0),
)
db.add(meeting)
db.commit()
print(f"\nMeeting 2/2026 created successfully (id={meeting.id})")
print(f" Date: 04.02.2026, 16:00-19:00")
print(f" Attendance: {present_count}/16 (quorum: {'YES' if meeting.quorum_confirmed else 'NO'})")
print(f" Agenda items: {len(agenda_items)}")
print(f" Proceedings: {len(proceedings)}")
print(f" Status: {meeting.status}")
return meeting.id
except Exception as e:
db.rollback()
print(f"ERROR: {e}")
import traceback
traceback.print_exc()
return None
finally:
db.close()
if __name__ == '__main__':
meeting_id = main()
if meeting_id:
print(f"\nDone. Meeting ID: {meeting_id}")
else:
print("\nFailed to import meeting.")
sys.exit(1)