From 7ce9e859c240ec3612d032e37d4131e47dd196e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Sun, 26 Jul 2026 17:14:42 +0200 Subject: [PATCH 01/11] docs(import_pracownikow): spec opcji nadpisywania dat zatrudnienia z pliku [ci skip] Co-Authored-By: Claude Fable 5 --- ...rt-nadpisywanie-dat-zatrudnienia-design.md | 291 ++++++++++++++++++ 1 file changed, 291 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-26-import-nadpisywanie-dat-zatrudnienia-design.md diff --git a/docs/superpowers/specs/2026-07-26-import-nadpisywanie-dat-zatrudnienia-design.md b/docs/superpowers/specs/2026-07-26-import-nadpisywanie-dat-zatrudnienia-design.md new file mode 100644 index 000000000..ffc5d0b8d --- /dev/null +++ b/docs/superpowers/specs/2026-07-26-import-nadpisywanie-dat-zatrudnienia-design.md @@ -0,0 +1,291 @@ +# Import pracowników: opcja „Nadpisuj daty zatrudnienia (od/do) wartościami z pliku" + +Data: 2026-07-26 +Status: projekt (do akceptacji) + +## 1. Problem + +Scenariusz zgłoszony przez użytkownika: tydzień temu wykonano import +pracowników z pliku **bez dat** — wszyscy zaimportowani dostali „datę od" += 19 lipca (patrz §2.4: fallback „data zmian → dziś" dla nowych okresów). +Teraz jest do zaimportowania poprawiony plik, w którym „data od" bywa +np. 5 lat wcześniejsza. + +Pytanie brzmiało: czy ponowny import **zmodyfikuje** istniejącą datę, czy +**doda nowe miejsce pracy** (w tej samej jednostce)? Odpowiedź (zweryfikowana +w kodzie, §2): **ani jedno, ani drugie** — import wyceluje w istniejący +otwarty okres, pokaże różnicę dat w podglądzie, ale przy zapisie jej +**nie zastosuje**. Poprawiony plik niczego więc nie naprawi. + +Pierwotny pomysł („wyczyść daty od/do w całej bazie" + import wypełnia +NULL-e) odrzucono na rzecz bezpieczniejszej opcji **nadpisywania dat +wartościami z pliku** — dotyka wyłącznie osób obecnych w pliku, nikt inny +nie traci dat (decyzja użytkownika w rozmowie projektowej). + +## 2. Aktualne zachowanie (stan na dziś, zweryfikowane w kodzie) + +### 2.1. Skąd biorą się daty w wierszu importu + +Kolumny pliku mapują się (ekran mapowania) na pola wewnętrzne +`data_zatrudnienia` („Data zatrudnienia") i `data_końca_zatrudnienia` +(„Data końca zatrudnienia") — `mapping.py:29-30`; „Data od"/„Data do" to +synonimy nagłówków kolumn pliku (`mapping.py:116-117`). Wiersz odczytuje +je przez `_plik_od()` / `_plik_do()` +(`src/import_pracownikow/models.py:1020-1033`) — kontrakt: zawsze +`date | None`, nigdy string. + +### 2.2. Wybór okresu docelowego (resolver) + +`rozwiaz_okres_zatrudnienia(autor, jednostka, plik_od)` +(`src/import_pracownikow/okresy.py:73-123`) — „data od" jest TOŻSAMOŚCIĄ +okresu (spójne z `unique_together = (autor, jednostka, rozpoczal_prace)`). +Kolejność rozstrzygania przy niepustym `plik_od`: + +1. **Dokładne trafienie** — istnieje AJ z `rozpoczal_prace == plik_od` + → celuj w niego (`okresy.py:98-100`). +2. **Okres niedatowany** — istnieje AJ z `rozpoczal_prace IS NULL` + → celuj w niego („do wypełnienia", `okresy.py:101-106`). +3. **Okres OTWARTY** (`zakonczyl_prace IS NULL`) — celuj w najświeższy + aktywny okres; różnica „daty od" jest POKAZYWANA w podglądzie, ale NIE + powstaje nowy okres i stary NIE jest domykany (świadoma decyzja „nowy + okres tylko gdy stary zamknięty", `okresy.py:107-115`). +4. **Wszystkie okresy zamknięte** (osoba odeszła i wraca) → NOWY okres + z datą z pliku (`okresy.py:116-118`). + +Pusty `plik_od` → celuj w najświeższy aktywny/istniejący okres, a bez +żadnego → nowy okres bez daty (`okresy.py:120-123`). + +### 2.3. Zapis dat na istniejącym okresie: wypełnij-tylko-NULL + +`_integruj_daty_aj` (`src/import_pracownikow/models.py:1333-1357`): + +- „Data od": zapis **wyłącznie gdy** `rozpoczal_prace IS NULL` w bazie + a plik niesie datę. Istniejąca data **nigdy nie jest nadpisywana**. +- „Data do": identycznie — wstaw-tylko-gdy-pusta; różnica wobec + istniejącej daty jest pokazywana w porównywarce, ale nie zapisywana. +- Pusta komórka w pliku → nic nie zmieniaj (nawet nie wypełnia NULL-a + „datą zmian"). + +Po ustawieniu dat, PRZED save, walidowany jest niezmiennik +`rozpoczal_prace < zakonczyl_prace` (`models.py:1370-1388`) — naruszenie +→ `BPPDatabaseError` → wiersz izolowany (reszta importu idzie dalej). +Walidacja celowo w Pythonie przed save, bo DB-owy CHECK +(`poczatek_przed_koncem`, mig. bpp 0469) wewnątrz transakcji dałby +nieizolowany `CheckViolation`. + +### 2.4. Nowe okresy i pochodzenie „19 lipca" + +Nowy AJ tworzy `_materializuj_diff` +(`src/import_pracownikow/pipeline/integrate.py:67-126`). Gdy plik nie +niesie „daty od", nowy okres dostaje fallback **`data_zmian_personalnych` +importu, a w jej braku — dzisiejszą datę** (`integrate.py:98-106`). +Dokładnie stąd wszyscy z zeszłotygodniowego importu (plik bez dat) mają +`rozpoczal_prace` = 19 lipca. Świeży AJ ma od razu konkretną datę, więc +gałąź „wypełnij NULL" z §2.3 już go nie stempluje. + +### 2.5. Podgląd (porównywarka) + +`_porownaj_data` (`models.py:1001-1018`) oznacza pole jako „zmienione" +(`rozne=True`) także wtedy, gdy obie strony są niepuste i różne — z +docstringiem wprost: „różnica pokazana **bez nadpisania**". UWAGA: +„zmienione" obejmuje też wypełnienie NULL-a i nowy okres (`models.py:1017`, +ekstraktory `_stan_data_od/_do` w `roznice.py:65-84`) — to istotne dla +liczenia N w §3.5. Stany pól zasilają siatkę porównań i filtr stanu +(`stany_pol`, `models.py:1204-1219`; liczenie `stany_pol_live` +`models.py:1177-1190`); zamrożenie w `stany_pol_snapshot` następuje NA +POCZĄTKU integracji, przed zmianą bazy (`integrate.py:203`), i stan „po +integracji" jest serwowany z tego snapshotu. + +### 2.5a. Bramka „zmiany potrzebne" (kluczowe dla projektu) + +Integracja wiersza wykonuje się tylko, gdy analiza uznała, że są zmiany +do zapisania: `_check_autor_jednostka_needs_update` (`models.py:1259-1266`) +liczy daty **wyłącznie jako wypełnienie NULL-a** — różnica „baza ma datę, +plik ma inną" NIE ustawia `zmiany_potrzebne` (`analyze.py:766`, +`pewnosc.py:134-137`), wiersz nie wchodzi do `zmiany_potrzebne_set` +(`models.py:399-400`), a nawet włączony do integracji odpadłby na świeżym +re-checku (`integrate.py:227` → `pominiety_bo_nieaktualny`). Wniosek: +samo nadpisywanie w `_integruj_daty_aj` NIE wystarczy — bramka musi być +świadoma flagi (§3.3a). + +### 2.6. Constrainty bazodanowe na `Autor_Jednostka` + +`src/bpp/models/autor.py:769-809`: + +- `unique_together (autor, jednostka, rozpoczal_prace)`; +- częściowy `UniqueConstraint` „jeden niedatowany okres na parę" + (`rozpoczal_prace IS NULL`); +- `ExclusionConstraint` `bpp_autor_jednostka_okresy_bez_nakladan`: + datowane okresy tej samej pary autor+jednostka NIE mogą się nakładać + (GiST, `daterange(rozpoczal_prace, zakonczyl_prace)` z `&&`). + +`daterange(rozpoczal_prace, zakonczyl_prace)` w constraincie ma granice +DOMKNIĘTE `'[]'` (`autor.py:713-732`), a `zakonczyl_prace IS NULL` daje +przedział otwarty w prawo `[od, ∞)`. + +Konsekwencja dla projektu: cofnięcie „daty od" wstecz może najechać na +wcześniejszy zamknięty okres → trzeba to walidować w Pythonie PRZED save +(jak w §2.3 — naruszenie constraintu psuje transakcję i izolację wiersza). + +### 2.6a. Inne miejsca pipeline'u dotykające dat AJ + +- **Odpięcia** (`integrate.py:264-313`, `_wykonaj_odpiecia`) ustawiają + `zakonczyl_prace` (= wczoraj) parom autor+jednostka SPOZA pliku — + rozłączne z projektowaną flagą (guard G1 pomija pary obecne w pliku). +- **Defragmentacja** (`Autor.save()` → `defragmentuj_jednostke`, + `autor.py:431-438`, scalanie `autor.py:620-710`) modyfikuje daty i + KASUJE przyległe AJ; biegnie wewnątrz integracji (`_integrate_autor` → + `a.save()`) tuż PRZED `_integruj_daty_aj`. Interakcja z flagą: gdy + nadpisana „data od" uczyni okres przyległym (dzień-po-dniu) do + zamkniętego okresu, przejdzie constraint i pre-check, a scalenie nastąpi + przy najbliższym `Autor.save()` — akceptowalne (dla świeżych okresów + istnieje już mechanizm `_przepnij_aj_po_defragmentacji`, + `models.py:1451-1484`). +- `pewnosc.odtworz_autor_jednostka` nie pisze dat na istniejących AJ + (odkłada tylko datę nowego okresu w diffie, `pewnosc.py:142`) — bez + konfliktu. + +### 2.7. Wniosek + +W scenariuszu z §1 (otwarty okres z 19 lipca, plik z datą wcześniejszą): +resolver celuje w istniejący okres (§2.2 pkt 3), podgląd pokazuje różnicę +(§2.5), integracja daty nie zmienia (§2.3). **Potrzebna jest opcja +nadpisywania.** + +## 3. Projektowane rozwiązanie + +### 3.1. Model i migracja + +Nowe pole `ImportPracownikow.nadpisuj_daty_zatrudnienia` +(`BooleanField`, `default=False`, verbose_name „Nadpisuj daty zatrudnienia +(od/do) wartościami z pliku") + migracja `import_pracownikow.0028`. +Flaga per-import → zakres z natury ograniczony do osób z pliku (bez ryzyka +multi-hosted). Baseline odświeżamy raz, przy scalaniu (polityka repo). + +### 3.2. Formularz nowego importu: szuflada + popup + +- Pole dochodzi do `NowyImportForm` (`src/import_pracownikow/forms.py`) + i ląduje w istniejącym zwiniętym `
` obok + `przepnij_wszystkie_prace`; nagłówek szuflady zmienia się na ogólne + „Opcje zaawansowane". +- W `src/import_pracownikow/templates/import_pracownikow/` + `importpracownikow_form.html` rozszerzamy istniejący wzorzec JS + (`importpracownikow_form.html:26-45`, confirm na + `id_przepnij_wszystkie_prace`): przy ZAZNACZANIU checkboxa `confirm(...)` + z ostrzeżeniem; anulowanie odznacza pole. Odznaczanie — bez pytania. + +### 3.3. Semantyka integracji (`_integruj_daty_aj`) + +Flaga czytana z `self.parent.nadpisuj_daty_zatrudnienia`: + +- **OFF** → zachowanie dzisiejsze, bit w bit (§2.3). +- **ON** → gdy plik NIESIE datę różną od bazy, data na okresie docelowym + zostaje nadpisana — osobno „od", osobno „do" — z wpisem w `log_zmian` + („data rozpoczęcia pracy: X → Y (nadpisano z pliku)"). +- Pusta komórka w pliku NIGDY nie kasuje daty z bazy (zasada „import + ustawia, nigdy nie kasuje" — spójna z tytułem/stopniem, §11.2 starego + specu). +- Wybór okresu docelowego (resolver, §2.2) — BEZ ZMIAN. W scenariuszu + z §1: data 19 lipca na istniejącym otwartym okresie zostaje zastąpiona + datą z pliku; nie powstaje duplikat miejsca pracy. +- Gałąź „nowy okres" (§2.2 pkt 4 i §2.4) — BEZ ZMIAN (nowy AJ i tak + dostaje daty z pliku). + +### 3.3a. Bramka `zmiany_potrzebne` świadoma flagi (WARUNEK KONIECZNY) + +Bez tej zmiany feature jest martwy dla scenariusza tytułowego (§2.5a): +wiersz, którego JEDYNĄ różnicą jest data, ma dziś `zmiany_potrzebne=False` +i integracja go pomija. Zmiany: + +- `_check_autor_jednostka_needs_update` (`models.py:1259-1266`): przy + fladze ON różnica dat „obie strony niepuste i różne" liczy się jako + zmiana potrzebna (przy OFF — jak dziś: tylko wypełnienie NULL-a); +- przez to samo przechodzi analiza (`analyze.py:766`, `pewnosc.py:134-137`) + i świeży re-check przy integracji (`integrate.py:227`) — wiersz z samą + korektą dat wchodzi do `zmiany_potrzebne_set` (`models.py:399-400`) + i nie kończy jako `pominiety_bo_nieaktualny`. + +### 3.4. Walidacja przed zapisem + +- Istniejący check `od < do` (§2.3) obejmuje wartości po nadpisaniu — + bez zmian, tylko test. +- NOWY pythonowy pre-check nakładania się okresów: po nadpisaniu dat + porównaj przedział z pozostałymi datowanymi okresami tej samej pary + autor+jednostka — lustro `ExclusionConstraint` z §2.6 z IDENTYCZNĄ + semantyką granic: przedziały DOMKNIĘTE `[od, do]`, a `zakonczyl_prace + IS NULL` = otwarty w prawo `[od, ∞)`. Kolizja → `BPPDatabaseError` + z komunikatem wskazującym kolidujący okres → wiersz izolowany, reszta + importu idzie dalej. +- Warunek uruchomienia pre-checku (jednoznacznie): TYLKO gdy flaga ON + faktycznie NADPISAŁA niepustą wartość w bazie inną wartością z pliku. + Wypełnienie NULL-a (także przy fladze ON) idzie dzisiejszą ścieżką BEZ + pre-checku — to zachowanie istnieje dziś i celowo go nie zmieniamy. + +### 3.5. Ostrzeżenie w końcowym formularzu zapisu + +`src/import_pracownikow/templates/import_pracownikow/przeglad.html`, +sekcja zapisu osób (Krok 2): + +- Gdy flaga ON: callout alert (Foundation; ikona `fi-icon`, NIE emoji — + to publiczny frontend): „Włączono nadpisywanie dat zatrudnienia: + **N** wierszy ma daty różne od bazy — zostaną nadpisane wartościami + z pliku." +- Liczenie N — UWAGA na semantykę: stan „zmienione" ze `stany_pol` + obejmuje też wypełnienia NULL-i i nowe okresy (§2.5), które NIE są + nadpisaniami. N liczymy jako „obie strony niepuste i różne" (dla + `data_od` lub `data_do`), w widoku podglądu (`PodgladImportuView`, + `views.py:944-1039`), live z `wstepnie_zaladuj_okresy` (przeciw N+1; + `stany_pol_snapshot` bywa NULL do backfillu — `views.py:826-828` — + więc SQL po snapshotcie by niedoszacował). +- `onsubmit`-owy `confirm` przycisku końcowego zapisu dostaje tę samą + treść, połączoną z istniejącym ostrzeżeniem o pominiętych wierszach + (`przeglad.html:205`). + +### 3.6. Testy + +Pytest (funkcje, `baker`, bez klas): + +1. flaga ON: „data od" i „data do" nadpisane na istniejącym otwartym + okresie (scenariusz §1: 19 lipca → data sprzed lat), wpisy w + `log_zmian`; +2. bramka (§3.3a): wiersz z JEDYNĄ różnicą w datach — przy ON + `zmiany_potrzebne=True`, integracja go wykonuje (nie kończy jako + `pominiety_bo_nieaktualny`); przy OFF `zmiany_potrzebne=False` jak + dziś; +3. flaga OFF: zachowanie dzisiejsze (data zostaje); +4. pusta komórka przy ON: data w bazie NIE jest kasowana; +5. kolizja przedziałów przy ON (wcześniejszy zamknięty okres) → + `BPPDatabaseError`, wiersz izolowany; wypełnienie NULL-a nie odpala + pre-checku; +6. `od >= do` po nadpisaniu → `BPPDatabaseError` (istniejący mechanizm); +7. formularz: pole w szufladzie, default OFF; +8. podgląd: callout z poprawnym N (tylko realne nadpisania: obie strony + niepuste i różne; NIE wypełnienia NULL-i ani nowe okresy) renderuje + się tylko przy fladze ON; +9. confirm końcowego zapisu zawiera ostrzeżenie przy fladze ON. + +### 3.7. Drobiazgi + +- Newsfragment `feature` po polsku w `src/bpp/newsfragments/` + (kanoniczny katalog). +- Dokumentacja użytkownika importu — jeśli istnieje strona o imporcie + pracowników w `docs/`, dopisać akapit o opcji. + +## 4. Poza zakresem (YAGNI) + +- Czyszczenie dat całej bazy/uczelni (pierwotny pomysł — zbędny przy + nadpisywaniu; kasowałby daty osobom spoza pliku). +- Per-wierszowe decyzje „nadpisz/nie nadpisuj". +- Zmiany w resolverze okresów (`okresy.py`) i w fallbacku „data zmian → + dziś" dla nowych okresów. +- Kasowanie dat pustą komórką pliku. + +## 5. Decyzje projektowe (z rozmowy) + +1. Nadpisywanie datami z pliku zamiast czyszczenia całej bazy — + wybór użytkownika po przedstawieniu zweryfikowanego zachowania (§2). +2. Opcja umieszczona w szufladzie formularza NOWEGO importu (nie w + końcowym formularzu) — spójne z `przepnij_wszystkie_prace`; podgląd + od początku wie, że różnice dat będą nadpisane. +3. Potwierdzenie: popup przy zaznaczaniu + callout i confirm w końcowym + formularzu zapisu osób. From 3276c0a4a189b8d5fb7c4fe0d1ec4fff7c84809b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Sun, 26 Jul 2026 17:41:15 +0200 Subject: [PATCH 02/11] docs(import_pracownikow): plan implementacji nadpisywania dat zatrudnienia [ci skip] Co-Authored-By: Claude Fable 5 --- ...26-import-nadpisywanie-dat-zatrudnienia.md | 1086 +++++++++++++++++ 1 file changed, 1086 insertions(+) create mode 100644 docs/superpowers/plans/2026-07-26-import-nadpisywanie-dat-zatrudnienia.md diff --git a/docs/superpowers/plans/2026-07-26-import-nadpisywanie-dat-zatrudnienia.md b/docs/superpowers/plans/2026-07-26-import-nadpisywanie-dat-zatrudnienia.md new file mode 100644 index 000000000..ee70daf0b --- /dev/null +++ b/docs/superpowers/plans/2026-07-26-import-nadpisywanie-dat-zatrudnienia.md @@ -0,0 +1,1086 @@ +# Import: nadpisywanie dat zatrudnienia z pliku — plan implementacji + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Opcja per-import „Nadpisuj daty zatrudnienia (od/do) wartościami +z pliku" — schowana w szufladzie formularza, z confirm-em przy zaznaczaniu +i ostrzeżeniem (callout + confirm) w końcowym formularzu zapisu osób. + +**Architecture:** Flaga `BooleanField` na `ImportPracownikow`; bramka +`zmiany_potrzebne` i `_integruj_daty_aj` stają się flag-aware; pythonowy +pre-check nakładania okresów (lustro `ExclusionConstraint`) przed save; +licznik realnych nadpisań zasila ostrzeżenie finalizacji. + +**Tech Stack:** Django 4.x, pytest + model_bakery, crispy-forms +(Foundation), szablony Django. + +**Spec:** `docs/superpowers/specs/2026-07-26-import-nadpisywanie-dat-zatrudnienia-design.md` + +## Global Constraints + +- ZAWSZE `uv run` przed każdym poleceniem Pythona (`uv run pytest`, `uv run python src/manage.py ...`). +- Testy: pytest, funkcje bez klas, `@pytest.mark.django_db`, `model_bakery.baker.make`. NIGDY unittest.TestCase. +- Pytest z `-n auto` przy pełnych przebiegach; pojedyncze testy mogą iść bez. +- Wyjście testów ZAWSZE do pliku: `... 2>&1 | tee /tmp/wynik.log` — potem grep; nie uruchamiaj tego samego przebiegu dwa razy. +- Max 88 znaków linii (ruff). Po edycjach: `ruff format ` i `ruff check ` (tylko zmienione pliki, nigdy `--all-files`). +- Komentarze szablonów Django `{# ... #}` są JEDNOLINIOWE — każda linia z własnym `{#` i `#}`. +- Publiczny frontend = Foundation Icons (``), NIE emoji. +- NIE modyfikować istniejących (wydanych) migracji; nowa migracja = `0028`. +- Newsfragment: `src/bpp/newsfragments/.feature.rst`, po polsku. +- Baseline bazy NIE odświeżać na gałęzi — raz, przy scalaniu (`make baseline-update`). +- Praca na gałęzi `feat/import-nadpisywanie-dat` od `dev`; commity małe, po każdym tasku. +- Nazwy z specu (spójność międzytaskowa): pole modelu `nadpisuj_daty_zatrudnienia`; metoda wiersza `nadpisze_daty()`; metoda parenta `liczba_nadpisan_dat()`; metoda pre-checku `_sprawdz_nakladanie_okresow(aj)`. + +--- + +### Task 0: Gałąź robocza + +**Files:** brak zmian w plikach. + +- [ ] **Step 1: Utwórz gałąź od dev** + +```bash +cd /Users/mpasternak/Programowanie/bpp +git checkout dev && git pull --ff-only && git checkout -b feat/import-nadpisywanie-dat +``` + +--- + +### Task 1: Pole modelu + migracja 0028 + +**Files:** +- Modify: `src/import_pracownikow/models.py` (klasa `ImportPracownikow`, po polu `przepnij_wszystkie_prace`, ok. linii 171) +- Create: `src/import_pracownikow/migrations/0028_nadpisuj_daty_zatrudnienia.py` (generowana) +- Test: `src/import_pracownikow/tests/test_nadpisywanie_dat.py` (nowy plik) + +**Interfaces:** +- Produces: `ImportPracownikow.nadpisuj_daty_zatrudnienia: bool` (default `False`) — czytane w Taskach 2-6. + +- [ ] **Step 1: Napisz failujący test** + +Utwórz `src/import_pracownikow/tests/test_nadpisywanie_dat.py`: + +```python +"""Opcja „Nadpisuj daty zatrudnienia (od/do) wartościami z pliku" — +flaga modelu, bramka zmian, nadpisywanie w integracji, pre-check +nakładania okresów, licznik ostrzeżenia finalizacji (spec +2026-07-26-import-nadpisywanie-dat-zatrudnienia-design.md).""" + +import pytest +from model_bakery import baker + +from import_pracownikow.models import ImportPracownikow + + +@pytest.mark.django_db +def test_flaga_nadpisywania_domyslnie_wylaczona(): + parent = baker.make(ImportPracownikow) + assert parent.nadpisuj_daty_zatrudnienia is False +``` + +- [ ] **Step 2: Uruchom test — ma paść** + +```bash +uv run pytest src/import_pracownikow/tests/test_nadpisywanie_dat.py -x 2>&1 | tee /tmp/t1.log | tail -5 +``` + +Oczekiwane: FAIL/ERROR (`AttributeError` / pole nie istnieje). + +- [ ] **Step 3: Dodaj pole do modelu** + +W `src/import_pracownikow/models.py`, w klasie `ImportPracownikow`, +bezpośrednio PO polu `przepnij_wszystkie_prace` (przed +`zakres_integracji`): + +```python + nadpisuj_daty_zatrudnienia = models.BooleanField( + "Nadpisuj daty zatrudnienia (od/do) wartościami z pliku", + default=False, + # HTML w help_text (crispy renderuje przez |safe) — wzorzec jak w + # przepnij_wszystkie_prace. Ten sam string dosłownie w migracji 0028. + help_text="Gdy zaznaczone, daty rozpoczęcia i zakończenia pracy " + "ISTNIEJĄCYCH okresów zatrudnienia zostaną " + "NADPISANE wartościami z pliku — tam, gdzie plik " + "niesie datę różną od bazy.
" + "Użyj do KOREKTY dat (np. po wcześniejszym imporcie pliku bez " + "dat, który ostemplował wszystkich datą importu).
" + "Puste komórki pliku niczego nie kasują. Dotyczy wyłącznie osób " + "obecnych w pliku.", + ) +``` + +- [ ] **Step 4: Wygeneruj migrację** + +```bash +DJANGO_BPP_SKIP_DOTENV=1 uv run python src/manage.py makemigrations import_pracownikow -n nadpisuj_daty_zatrudnienia 2>&1 | tail -3 +``` + +Oczekiwane: powstaje `src/import_pracownikow/migrations/0028_nadpisuj_daty_zatrudnienia.py` +(AddField). Obejrzyj plik — ma zawierać wyłącznie to jedno AddField. + +- [ ] **Step 5: Uruchom test — ma przejść** + +```bash +uv run pytest src/import_pracownikow/tests/test_nadpisywanie_dat.py -x 2>&1 | tee /tmp/t1b.log | tail -5 +``` + +Oczekiwane: PASS. + +- [ ] **Step 6: Commit** + +```bash +git add src/import_pracownikow/models.py src/import_pracownikow/migrations/0028_nadpisuj_daty_zatrudnienia.py src/import_pracownikow/tests/test_nadpisywanie_dat.py +git commit -m "feat(import_pracownikow): pole nadpisuj_daty_zatrudnienia (mig 0028)" +``` + +--- + +### Task 2: Formularz (szuflada) + confirm przy zaznaczaniu + +**Files:** +- Modify: `src/import_pracownikow/forms.py` (`NowyImportForm`, linie 16-70) +- Modify: `src/import_pracownikow/templates/import_pracownikow/importpracownikow_form.html` (blok ` {% endblock %} diff --git a/src/import_pracownikow/tests/test_nadpisywanie_dat.py b/src/import_pracownikow/tests/test_nadpisywanie_dat.py index 60d2ef906..e403a1a72 100644 --- a/src/import_pracownikow/tests/test_nadpisywanie_dat.py +++ b/src/import_pracownikow/tests/test_nadpisywanie_dat.py @@ -6,6 +6,7 @@ import pytest from model_bakery import baker +from import_pracownikow.forms import NowyImportForm from import_pracownikow.models import ImportPracownikow @@ -13,3 +14,20 @@ def test_flaga_nadpisywania_domyslnie_wylaczona(): parent = baker.make(ImportPracownikow) assert parent.nadpisuj_daty_zatrudnienia is False + + +def test_formularz_ma_pole_nadpisywania_dat(): + form = NowyImportForm() + assert "nadpisuj_daty_zatrudnienia" in form.fields + assert form.fields["nadpisuj_daty_zatrudnienia"].initial in (None, False) + + +@pytest.mark.django_db +def test_formularz_pole_nadpisywania_w_szufladzie(): + from crispy_forms.utils import render_crispy_form + + html = render_crispy_form(NowyImportForm()) + assert " pozycja_details From 204cfa6b4844ae5cc6742fc0ec642ace4ae441eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Sun, 26 Jul 2026 18:09:56 +0200 Subject: [PATCH 05/11] feat(import_pracownikow): bramka zmiany_potrzebne swiadoma flagi nadpisywania dat Co-Authored-By: Claude Fable 5 --- src/import_pracownikow/models.py | 13 ++++++ .../tests/test_nadpisywanie_dat.py | 45 ++++++++++++++++++- 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/src/import_pracownikow/models.py b/src/import_pracownikow/models.py index 3f8638956..400a17298 100644 --- a/src/import_pracownikow/models.py +++ b/src/import_pracownikow/models.py @@ -1269,6 +1269,11 @@ def _check_autor_jednostka_needs_update(self, dane): # pracy. Guard przed dostępem do atrybutów None (checki niżej łapią # None dopiero przez short-circuit dane.get(...), a #4 primary nie). return False + # §3.3a specu nadpisywania dat: przy fladze „nadpisuj daty" różnica + # wobec ISTNIEJĄCEJ (niepustej) daty też wymaga integracji — bez + # flagi liczy się, jak dotąd, wyłącznie wypełnienie NULL-a. + nadpisywanie = self.parent.nadpisuj_daty_zatrudnienia + plik_od, plik_do = self._plik_od(), self._plik_do() checks = [ # #4: rozpoczęcie stemplujemy TYLKO gdy puste (data z pliku / importu) # — integracja potrzebna, gdy plik niesie datę, a AJ jej nie ma. @@ -1292,6 +1297,14 @@ def _check_autor_jednostka_needs_update(self, dane): # Stanowisko dydaktyczne — overwrite-if-different (mirror funkcja). self.stanowisko_dydaktyczne_id is not None and aj.stanowisko_id != self.stanowisko_dydaktyczne_id, + nadpisywanie + and plik_od is not None + and aj.rozpoczal_prace is not None + and aj.rozpoczal_prace != plik_od, + nadpisywanie + and plik_do is not None + and aj.zakonczyl_prace is not None + and aj.zakonczyl_prace != plik_do, ] return any(checks) diff --git a/src/import_pracownikow/tests/test_nadpisywanie_dat.py b/src/import_pracownikow/tests/test_nadpisywanie_dat.py index e403a1a72..19f77dba0 100644 --- a/src/import_pracownikow/tests/test_nadpisywanie_dat.py +++ b/src/import_pracownikow/tests/test_nadpisywanie_dat.py @@ -3,11 +3,14 @@ nakładania okresów, licznik ostrzeżenia finalizacji (spec 2026-07-26-import-nadpisywanie-dat-zatrudnienia-design.md).""" +from datetime import date + import pytest from model_bakery import baker +from bpp.models import Autor, Autor_Jednostka, Jednostka from import_pracownikow.forms import NowyImportForm -from import_pracownikow.models import ImportPracownikow +from import_pracownikow.models import ImportPracownikow, ImportPracownikowRow @pytest.mark.django_db @@ -31,3 +34,43 @@ def test_formularz_pole_nadpisywania_w_szufladzie(): pozycja_details = html.index(" pozycja_details + + +def _row_z_data(parent, dane, autor, jednostka, aj): + return baker.make( + ImportPracownikowRow, + parent=parent, + autor=autor, + jednostka=jednostka, + autor_jednostka=aj, + dane_znormalizowane=dane, + ) + + +def _scenariusz_tytulowy(nadpisuj): + """Otwarty okres od 2026-07-19 (fallback z poprzedniego importu), + plik niesie 2021-10-01. Jedyna różnica wiersza = data od.""" + parent = baker.make(ImportPracownikow, nadpisuj_daty_zatrudnienia=nadpisuj) + autor, jednostka = baker.make(Autor), baker.make(Jednostka) + aj = baker.make( + Autor_Jednostka, + autor=autor, + jednostka=jednostka, + rozpoczal_prace=date(2026, 7, 19), + zakonczyl_prace=None, + podstawowe_miejsce_pracy=True, + ) + row = _row_z_data(parent, {"data_zatrudnienia": "2021-10-01"}, autor, jednostka, aj) + return row, aj + + +@pytest.mark.django_db +def test_bramka_roznica_dat_flaga_on(): + row, _ = _scenariusz_tytulowy(nadpisuj=True) + assert row.check_if_integration_needed() is True + + +@pytest.mark.django_db +def test_bramka_roznica_dat_flaga_off_jak_dzis(): + row, _ = _scenariusz_tytulowy(nadpisuj=False) + assert row.check_if_integration_needed() is False From 49fd9abcd3c8a53d0a701b6ea738f2529727ee9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Sun, 26 Jul 2026 18:26:12 +0200 Subject: [PATCH 06/11] feat(import_pracownikow): nadpisywanie dat AJ wartosciami z pliku (flaga ON) --- src/import_pracownikow/models.py | 67 +++++++++++++------ .../tests/test_nadpisywanie_dat.py | 64 ++++++++++++++++++ 2 files changed, 110 insertions(+), 21 deletions(-) diff --git a/src/import_pracownikow/models.py b/src/import_pracownikow/models.py index 400a17298..52fcf741b 100644 --- a/src/import_pracownikow/models.py +++ b/src/import_pracownikow/models.py @@ -1360,28 +1360,53 @@ def _ustaw_tytul_i_stopien_autora(self, a): def _integruj_daty_aj(self, aj, dane): """Ustawia daty zatrudnienia na powiązaniu z danych wiersza. - „Data od" (``rozpoczal_prace``) na ISTNIEJĄCYM AJ wypełniamy TYLKO gdy - baza ma ``NULL`` a plik NIESIE datę (§3: „wypełnienie NULL") — nie - nadpisujemy istniejącej daty. Pusty ``plik_od`` na istniejącym AJ → - NIC NIE ZMIENIAJ (§5), nawet gdy ``rozpoczal_prace`` jest ``NULL``. - Fallback ``data zmian → dziś`` dla NOWEGO okresu stemplujemy przy - MATERIALIZACJI (``integrate._materializuj_diff``), nie tutaj — świeży AJ - ma już ``rozpoczal_prace``, więc ta gałąź go nie dotyczy. - - „Data do" (``zakonczyl_prace``) — wstaw-tylko-gdy-pusta (§3): różnicę - wobec istniejącej daty POKAZUJEMY w porównywarce, ale NIE nadpisujemy.""" - if aj.rozpoczal_prace is None and dane.get("data_zatrudnienia"): - aj.rozpoczal_prace = dane["data_zatrudnienia"] - self.log_zmian["autor_jednostka"].append( - f"data rozpoczęcia pracy na {aj.rozpoczal_prace}" - ) + Bez flagi „nadpisuj daty": „data od"/„data do" na ISTNIEJĄCYM AJ + wypełniamy TYLKO gdy baza ma ``NULL`` a plik NIESIE datę (§3: + „wypełnienie NULL") — istniejącej daty nie ruszamy; pusty plik → + nic nie zmieniaj (§5). Fallback ``data zmian → dziś`` dla NOWEGO + okresu stempluje materializacja (``integrate._materializuj_diff``). + + Z flagą ``parent.nadpisuj_daty_zatrudnienia`` (spec §3.3): datę + różną od niepustej wartości w bazie NADPISUJEMY wartością z pliku + (osobno „od" i „do"); puste komórki nadal niczego nie kasują. + + Zwraca ``True`` TYLKO gdy nadpisano niepustą wartość — sygnał dla + pre-checku nakładania okresów; wypełnienia NULL-i zwracają + ``False`` (idą dzisiejszą ścieżką, bez pre-checku).""" + nadpisywanie = self.parent.nadpisuj_daty_zatrudnienia + nadpisano = False + + plik_od = dane.get("data_zatrudnienia") + if plik_od: + if aj.rozpoczal_prace is None: + aj.rozpoczal_prace = plik_od + self.log_zmian["autor_jednostka"].append( + f"data rozpoczęcia pracy na {aj.rozpoczal_prace}" + ) + elif nadpisywanie and aj.rozpoczal_prace != plik_od: + self.log_zmian["autor_jednostka"].append( + f"data rozpoczęcia pracy: {aj.rozpoczal_prace} → " + f"{plik_od} (nadpisano z pliku)" + ) + aj.rozpoczal_prace = plik_od + nadpisano = True data_konca = dane.get("data_końca_zatrudnienia") - if data_konca and aj.zakonczyl_prace is None: - aj.zakonczyl_prace = data_konca - self.log_zmian["autor_jednostka"].append( - f"data końca zatrudnienia na {data_konca}" - ) + if data_konca: + if aj.zakonczyl_prace is None: + aj.zakonczyl_prace = data_konca + self.log_zmian["autor_jednostka"].append( + f"data końca zatrudnienia na {data_konca}" + ) + elif nadpisywanie and aj.zakonczyl_prace != data_konca: + self.log_zmian["autor_jednostka"].append( + f"data końca zatrudnienia: {aj.zakonczyl_prace} → " + f"{data_konca} (nadpisano z pliku)" + ) + aj.zakonczyl_prace = data_konca + nadpisano = True + + return nadpisano def _integrate_autor_jednostka(self): aj = self.autor_jednostka @@ -1392,7 +1417,7 @@ def _integrate_autor_jednostka(self): return dane = self.dane_bardziej_znormalizowane - self._integruj_daty_aj(aj, dane) + nadpisano_daty = self._integruj_daty_aj(aj, dane) # Niezmiennik rozpoczal < zakonczyl walidujemy PRZED jakimkolwiek zapisem. # Model.save() nie woła clean(), a ustaw_podstawowe_miejsce_pracy() niżej diff --git a/src/import_pracownikow/tests/test_nadpisywanie_dat.py b/src/import_pracownikow/tests/test_nadpisywanie_dat.py index 19f77dba0..5e3f7a684 100644 --- a/src/import_pracownikow/tests/test_nadpisywanie_dat.py +++ b/src/import_pracownikow/tests/test_nadpisywanie_dat.py @@ -44,6 +44,10 @@ def _row_z_data(parent, dane, autor, jednostka, aj): jednostka=jednostka, autor_jednostka=aj, dane_znormalizowane=dane, + # jawnie: `zmiany_potrzebne` to zwykły BooleanField (bez default), + # baker.make losowałby True/False — a `integrate()` niżej asertuje + # `self.zmiany_potrzebne` (musi być zdeterminowane, nie flaky). + zmiany_potrzebne=True, ) @@ -74,3 +78,63 @@ def test_bramka_roznica_dat_flaga_on(): def test_bramka_roznica_dat_flaga_off_jak_dzis(): row, _ = _scenariusz_tytulowy(nadpisuj=False) assert row.check_if_integration_needed() is False + + +@pytest.mark.django_db +def test_integracja_nadpisuje_date_od_flaga_on(): + row, aj = _scenariusz_tytulowy(nadpisuj=True) + row.integrate() + aj.refresh_from_db() + assert aj.rozpoczal_prace == date(2021, 10, 1) + assert any("nadpisano z pliku" in wpis for wpis in row.log_zmian["autor_jednostka"]) + + +@pytest.mark.django_db +def test_integracja_nie_nadpisuje_flaga_off(): + row, aj = _scenariusz_tytulowy(nadpisuj=False) + # OFF: bramka nie przepuści wiersza; wołamy integrate() wprost, żeby + # potwierdzić, że nawet wtedy data NIE jest ruszana. + row.integrate() + aj.refresh_from_db() + assert aj.rozpoczal_prace == date(2026, 7, 19) + + +@pytest.mark.django_db +def test_integracja_nadpisuje_date_do(): + parent = baker.make(ImportPracownikow, nadpisuj_daty_zatrudnienia=True) + autor, jednostka = baker.make(Autor), baker.make(Jednostka) + aj = baker.make( + Autor_Jednostka, + autor=autor, + jednostka=jednostka, + rozpoczal_prace=date(2020, 1, 1), + zakonczyl_prace=date(2026, 12, 31), + ) + row = _row_z_data( + parent, + {"data_zatrudnienia": "2020-01-01", "data_końca_zatrudnienia": "2024-06-30"}, + autor, + jednostka, + aj, + ) + row.integrate() + aj.refresh_from_db() + assert aj.zakonczyl_prace == date(2024, 6, 30) + + +@pytest.mark.django_db +def test_pusta_komorka_nie_kasuje_daty_przy_on(): + parent = baker.make(ImportPracownikow, nadpisuj_daty_zatrudnienia=True) + autor, jednostka = baker.make(Autor), baker.make(Jednostka) + aj = baker.make( + Autor_Jednostka, + autor=autor, + jednostka=jednostka, + rozpoczal_prace=date(2020, 1, 1), + zakonczyl_prace=date(2024, 6, 30), + ) + row = _row_z_data(parent, {}, autor, jednostka, aj) + row.integrate() + aj.refresh_from_db() + assert aj.rozpoczal_prace == date(2020, 1, 1) + assert aj.zakonczyl_prace == date(2024, 6, 30) From ba0e2143c4198c47d364084b0819ba7d7cba1cbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Sun, 26 Jul 2026 18:42:56 +0200 Subject: [PATCH 07/11] feat(import_pracownikow): pre-check nakladania okresow przed nadpisaniem dat --- src/import_pracownikow/models.py | 85 +++++++++++++++---- .../tests/test_nadpisywanie_dat.py | 70 +++++++++++++++ 2 files changed, 138 insertions(+), 17 deletions(-) diff --git a/src/import_pracownikow/models.py b/src/import_pracownikow/models.py index 52fcf741b..60c124670 100644 --- a/src/import_pracownikow/models.py +++ b/src/import_pracownikow/models.py @@ -1408,25 +1408,61 @@ def _integruj_daty_aj(self, aj, dane): return nadpisano - def _integrate_autor_jednostka(self): - aj = self.autor_jednostka - if aj is None: - # Ochrona: świeży okres mógł zostać scalony przez defragmentację i - # bez ocalałego AJ (`_przepnij_aj_po_defragmentacji`). Dane zatrudnienia - # niesie już scalony rekord — nie ma czego zapisywać. - return - dane = self.dane_bardziej_znormalizowane + def _sprawdz_nakladanie_okresow(self, aj): + """Pythonowe lustro constraintu + ``bpp_autor_jednostka_okresy_bez_nakladan`` (bpp/models/autor.py): + przedziały DOMKNIĘTE ``[od, do]``, ``zakonczyl_prace IS NULL`` = + otwarty w prawo ``[od, ∞)``. Wołane TYLKO po nadpisaniu niepustej + daty (flaga „nadpisuj daty") — naruszenie constraintu w bazie + zatruwa transakcję i psuje izolację wiersza, więc kolizję łapiemy + PRZED save (jak niezmiennik od Date: Sun, 26 Jul 2026 19:01:21 +0200 Subject: [PATCH 08/11] feat(import_pracownikow): ostrzezenie finalizacji o nadpisaniu dat (callout + confirm) --- src/import_pracownikow/models.py | 43 +++++++++ .../partials/_ostrzezenie_nadpisanie_dat.html | 13 +++ .../import_pracownikow/przeglad.html | 5 +- .../tests/test_nadpisywanie_dat.py | 91 +++++++++++++++++++ src/import_pracownikow/views.py | 7 ++ 5 files changed, 158 insertions(+), 1 deletion(-) create mode 100644 src/import_pracownikow/templates/import_pracownikow/partials/_ostrzezenie_nadpisanie_dat.html diff --git a/src/import_pracownikow/models.py b/src/import_pracownikow/models.py index 60c124670..7c0939ab2 100644 --- a/src/import_pracownikow/models.py +++ b/src/import_pracownikow/models.py @@ -583,6 +583,27 @@ def liczba_wierszy_do_pominiecia(self): autor__isnull=True, utworz_nowego=False ).count() + def liczba_nadpisan_dat(self): + """Ile wierszy przy zapisie osób NADPISZE istniejącą datę + zatrudnienia (flaga ``nadpisuj_daty_zatrudnienia``) — do calloutu + i confirmu finalizacji. Liczone LIVE (``stany_pol_snapshot`` bywa + NULL do backfillu i miesza wypełnienia NULL-i z nadpisaniami); + ``wstepnie_zaladuj_okresy`` + przypięcie ``parent`` chronią przed + N+1.""" + if not self.nadpisuj_daty_zatrudnienia: + return 0 + from import_pracownikow.okresy import wstepnie_zaladuj_okresy + + rows = list( + self.importpracownikowrow_set.filter( + autor__isnull=False, jednostka__isnull=False + ).select_related("autor", "jednostka") + ) + for row in rows: + row.parent = self + wstepnie_zaladuj_okresy(rows) + return sum(1 for row in rows if row.nadpisze_daty()) + @staticmethod def _liczniki_decyzji(queryset, tryb_brak, tryb_zgadywanie): """Rozkład NIEROZSTRZYGNIĘTYCH decyzji (jednostek/tytułów) po ``tryb``: @@ -1046,6 +1067,28 @@ def _plik_do(self): return None return self.dane_bardziej_znormalizowane.get("data_końca_zatrudnienia") or None + def nadpisze_daty(self): + """Czy zapis osób NADPISZE niepustą datę tego wiersza (flaga + ``nadpisuj_daty_zatrudnienia``). Liczy TYLKO realne nadpisania — + obie strony niepuste i różne; wypełnienia NULL-i i nowe okresy to + NIE nadpisania (spec §3.5, stan „zmienione" ze ``stany_pol`` byłby + zawyżony). Zasila licznik ostrzeżenia finalizacji.""" + if not self.parent.nadpisuj_daty_zatrudnienia: + return False + if self.autor_id is None or self.jednostka_id is None: + return False + from import_pracownikow.okresy import rozwiaz_okres_zatrudnienia + + rodzaj, aj = rozwiaz_okres_zatrudnienia( + self.autor, self.jednostka, self._plik_od(), aj_lista=self._aj_lista() + ) + if rodzaj != "istniejacy": + return False + plik_od, plik_do = self._plik_od(), self._plik_do() + return bool( + plik_od and aj.rozpoczal_prace and aj.rozpoczal_prace != plik_od + ) or bool(plik_do and aj.zakonczyl_prace and aj.zakonczyl_prace != plik_do) + def _aj_lista(self): """Lista okresów ``Autor_Jednostka`` dla ``(autor, jednostka)`` wiersza — JEDNO zapytanie (memo na instancji), współdzielone przez resolver diff --git a/src/import_pracownikow/templates/import_pracownikow/partials/_ostrzezenie_nadpisanie_dat.html b/src/import_pracownikow/templates/import_pracownikow/partials/_ostrzezenie_nadpisanie_dat.html new file mode 100644 index 000000000..a03fc9566 --- /dev/null +++ b/src/import_pracownikow/templates/import_pracownikow/partials/_ostrzezenie_nadpisanie_dat.html @@ -0,0 +1,13 @@ +{# Ostrzeżenie finalizacji: flaga „nadpisuj daty zatrudnienia" — zapis #} +{# osób NADPISZE istniejące daty wartościami z pliku (spec nadpisywania #} +{# dat §3.5). Liczba = realne nadpisania (obie strony niepuste i różne). #} +
+

+ + Włączono nadpisywanie dat zatrudnienia. + Wiersze z datami różnymi od bazy: + {{ liczba_nadpisan_dat }} — ich daty rozpoczęcia + / zakończenia pracy zostaną NADPISANE + wartościami z pliku. Ręcznie ustawione daty zostaną utracone. +

+
diff --git a/src/import_pracownikow/templates/import_pracownikow/przeglad.html b/src/import_pracownikow/templates/import_pracownikow/przeglad.html index a476ecdab..760dac6be 100644 --- a/src/import_pracownikow/templates/import_pracownikow/przeglad.html +++ b/src/import_pracownikow/templates/import_pracownikow/przeglad.html @@ -194,6 +194,9 @@

Najpierw słowniki

{# Ostrzeżenie #4 (PR #531): zapis zmienia podstawowe #} {# miejsce pracy — szczegółowy warning tuż przed przyciskiem. #} {% include "import_pracownikow/partials/_ostrzezenie_podstawowe_miejsce.html" %} + {% if nadpisywanie_dat_wlaczone %} + {% include "import_pracownikow/partials/_ostrzezenie_nadpisanie_dat.html" %} + {% endif %} {# Ostrzeżenie finalizacji: wiersze bez decyzji zostaną #} {# pominięte. {% with %} liczy COUNT raz (nie 2× w if+confirm). #} {% with liczba_pominietych=parent_object.liczba_wierszy_do_pominiecia %} @@ -202,7 +205,7 @@

Najpierw słowniki

{% endif %}
+ {% if liczba_pominietych or liczba_nadpisan_dat %}onsubmit="return confirm('{% if liczba_pominietych %}Wiersze bez dopasowania zostaną pominięte: {{ liczba_pominietych }}. {% endif %}{% if liczba_nadpisan_dat %}Daty zatrudnienia zostaną NADPISANE wartościami z pliku dla wierszy: {{ liczba_nadpisan_dat }}. {% endif %}Kontynuować zapis?');"{% endif %}> {% csrf_token %}