SKILL: Uporządkowanie skills domenowych - podział na mniejsze pliki i wspólna numeracja

This commit is contained in:
Marcin Wojas
2026-06-07 08:31:37 +02:00
parent fb2f2695a3
commit 5605ad2915
42 changed files with 13500 additions and 13526 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,501 @@
# KADRY02 — Etat — zatrudnienie etatowe
> Wspólne fakty o typie, podstawowe typy i szablon wzorca: [../kadry.md](../kadry.md).
### KADRY-B1 — Definiowanie etatu (umowa o pracę) (★)
**Cel:** ustalić warunki zatrudnienia etatowego pracownika — rodzaj umowy o pracę, okres, daty
zawarcia/rozpoczęcia pracy, stanowisko, jednostkę organizacyjną oraz stawkę zaszeregowania
(wymiar etatu, rodzaj/typ stawki, kwota). Warunki etatu są **historyczne**: siedzą w polu
`Etat` konkretnego zapisu `PracHistoria`. Etat ustawiamy albo na świeżo utworzonym pracowniku
(`pracownik.Last.Etat`, patrz KADRY-A1), albo na nowym zapisie historii „od daty" (patrz KADRY-A14).
**Gdzie leżą pola — `PracHistoria.Etat: Soneta.Kadry.Etat` (subrow zapisu historii):**
| Dana | Pole / typ | Uwaga |
|---|---|---|
| Rodzaj umowy o pracę | `Etat.TypUmowy: Soneta.Kadry.TypUmowyOPrace` | enum: `NaCzasNieokreślony`, `NaOkresPróbny`, `NaCzasOkreślony`, `NaOkresZastępstwa`, `DoDniaPorodu`, `NaCzasWykonywniaPracy`, … (`Brak = 0` = nie dotyczy) |
| Okres etatu (oddo) | `Etat.Okres: Soneta.Types.FromTo` | okres obowiązywania warunków zatrudnienia |
| Data zawarcia umowy | `Etat.DataZawarcia: Soneta.Types.Date` | data podpisania umowy |
| Data rozpoczęcia pracy | `Etat.DataRozpPracy: Soneta.Types.Date` | data faktycznego rozpoczęcia |
| Stanowisko (opis tekstowy) | `Etat.Stanowisko: string` | **wymagane dla etatu** (weryfikator przy `Save()`) |
| Jednostka organizacyjna (wydział) | `Etat.Wydzial: Soneta.Kadry.Wydzial` | **wymagane dla etatu**; pobierz istniejący wydział, korzeń struktury: `session.GetKadry().Wydzialy.Firma` |
| Oddział firmy | `Etat.Oddzial: Soneta.Core.OddzialFirmy` | opcjonalny oddział |
| Miejsce wykonywania pracy | `Etat.MiejscePracy: string` | tekst |
| Podstawa stosunku pracy | `Etat.Podstawa: Soneta.Kadry.StosPracyNaPodstawie` | enum |
**Stawka — subrow `Etat.Zaszeregowanie: Soneta.Kadry.Zaszeregowanie`:**
| Dana | Pole / typ | Uwaga |
|---|---|---|
| Rodzaj stawki | `Zaszeregowanie.RodzajStawki: Soneta.Kadry.RodzajStawkiZaszeregowania` | enum: `Godzinowa = 0`, `Miesieczna = 1`, `DochodDeklarowany = 2` |
| Typ stawki | `Zaszeregowanie.TypStawki: Soneta.Kadry.TypStawkiZaszeregowania` | enum: `Dowolna = 0`, `Minimalna = 1`, `ZZakresu = 2`, `WgWskaźnika = 3`, `Nieokreślona = 10` |
| Wymiar etatu (ułamek) | `Zaszeregowanie.Wymiar: Soneta.Types.Fraction` | `Fraction.One` = pełny etat; `new Fraction(1, 2)` = ½ etatu |
| Kwota stawki | `Zaszeregowanie.Stawka: Soneta.Types.Currency` | kwota brutto (miesięczna lub godzinowa wg `RodzajStawki`) |
| Grupa zaszeregowania | `Zaszeregowanie.Grupa: Soneta.Kadry.GrupaZaszeregowania` | rekord słownika (opcjonalny) |
| Definicja elementu wynagrodzenia | `Zaszeregowanie.Element: Soneta.Place.DefinicjaElementu` | element płacowy wiązany ze stawką (opcjonalny) |
**Pułapki:**
- **Kolejność ma znaczenie — `Etat.Okres` ustaw jako PIERWSZE.** Na świeżo utworzonym
pracowniku (lub świeżym zapisie historii) **cały subrow `Etat` jest w trybie tylko-do-odczytu**,
dopóki nie ustawisz `Etat.Okres` (zakres zatrudnienia). Próba ustawienia `Etat.TypUmowy`,
`Etat.Podstawa` czy `Zaszeregowanie.RodzajStawki`/`Wymiar` przed `Etat.Okres` rzuca
`Soneta.Business.ColReadOnlyException` (np. „'Etat.Typ umowy' — pole w trybie 'tylko do odczytu'").
Po ustawieniu `Etat.Okres` pozostałe pola (w tym `Zaszeregowanie.Wymiar`) są zapisywalne —
kolejność wśród nich nie ma już znaczenia.
- **Pola wymagane dla etatu:** po ustawieniu `Etat.Okres` (pracownik staje się etatowy) `Save()`
wymaga `Etat.Wydzial` **oraz** `Etat.Stanowisko` — bez nich zapis rzuca wyjątek weryfikatora.
- `Etat` to **subrow** zapisu `PracHistoria` — modyfikujesz jego pola (`Last.Etat.Okres = …`), nie
przypisujesz całego obiektu `Etat`.
- `Zaszeregowanie` to z kolei subrow `Etat` — analogicznie modyfikujesz pola
(`Last.Etat.Zaszeregowanie.Stawka = …`).
- `Etat.Wymiar` i `Etat.TypStawki` istnieją także na poziomie `Etat` (delegaty/odczyt) — **kanonicznie
ustawiamy je na `Etat.Zaszeregowanie`** (`Zaszeregowanie.Wymiar`, `Zaszeregowanie.RodzajStawki`,
`Zaszeregowanie.TypStawki`), bo to one są polami bazodanowymi tej struktury.
- `Etat.Wydzial` i `Etat.Oddzial` to **referencje** do istniejących rekordów — nie twórz „w locie";
korzeń struktury organizacyjnej pobierzesz przez `session.GetKadry().Wydzialy.Firma`.
- Zmiana warunków etatu **od konkretnego dnia** to nowy zapis historii (`Historia.Update(date)` +
`PracHistorie.AddRow`, patrz KADRY-A14), a nie nadpisanie bieżącego zapisu (to byłaby korekta całego okresu).
- `TypUmowyOPrace` to enum, nie string; `Okres`/`DataZawarcia`/`DataRozpPracy` to typy biznesowe
`FromTo`/`Date`, nie `DateTime` (safe-code §10.1).
**Snippet:**
```csharp
var kadry = session.GetKadry();
using (var t = session.Logout(editMode: true))
{
// Nowy pracownik (KADRY-A1) — AddRow tworzy pierwszy zapis historii (Last) + kalendarz.
var pracownik = session.AddRow(new PracownikFirmy());
pracownik.Kod = "555";
pracownik.Last.Nazwisko = "Kowalska";
pracownik.Last.Imie = "Gabriela";
// Warunki etatu — na Etat bieżącego (pierwszego) zapisu historii.
// KLUCZOWE: Etat.Okres MUSI być pierwszy — odblokowuje (z trybu read-only) resztę pól Etat.
var etat = pracownik.Last.Etat;
etat.Okres = new FromTo(new Date(2026, 1, 1), Date.MaxValue); // 1) NAJPIERW okres
etat.TypUmowy = TypUmowyOPrace.NaCzasNieokreślony;
etat.DataZawarcia = new Date(2025, 12, 20);
etat.DataRozpPracy = new Date(2026, 1, 1);
etat.Stanowisko = "Specjalista"; // wymagane dla etatu
etat.Wydzial = kadry.Wydzialy.Firma; // wymagane dla etatu (korzeń struktury)
// Stawka zaszeregowania (po ustawieniu Etat.Okres pola są zapisywalne):
var z = etat.Zaszeregowanie;
z.RodzajStawki = RodzajStawkiZaszeregowania.Miesieczna;
z.TypStawki = TypStawkiZaszeregowania.Dowolna;
z.Wymiar = Fraction.One; // pełny etat
z.Stawka = (Currency)6000m; // kwota brutto miesięcznie
t.Commit();
}
session.Save();
```
> **Zmiany warunków zatrudnienia (KADRY-B2KADRY-B7).** Warunki zatrudnienia etatowego siedzą w polu `PracHistoria.Etat: Soneta.Kadry.Etat`
> (subrow zapisu historii). `Etat` jest **historyczny** wraz z całym `PracHistoria` — okres
> obowiązywania warunków trzyma `Etat.Okres: FromTo`, a okres zapisu historii `PracHistoria.Aktualnosc`.
> Zmiana warunków „od dnia" to **nowy zapis historii** (`Historia.Update(date)` + `PracHistorie.AddRow`,
> wzorzec z KADRY-A14) — modyfikacja bieżącego zapisu byłaby korektą całego jego okresu.
>
> **Bramka edycji etatu (KADRY-B1).** Na świeżym zapisie cały subrow `Etat` jest tylko-do-odczytu, dopóki
> nie ustawisz `Etat.Okres` — ustaw go **PIERWSZY**, inaczej dotknięcie `TypUmowy`/`Zaszeregowanie.*`
> rzuca `Soneta.Business.ColReadOnlyException`. Pola wymagane przy zapisie etatu: `Etat.Wydzial`
> **oraz** `Etat.Stanowisko`. Po `Update(date)` klon ma już ustawiony `Etat.Okres` (sklonowany ze
> starego zapisu) — zwykle nie trzeba go ustawiać ponownie, ale jeśli zmieniasz okres etatu, rób to
> jako pierwsze.
---
### KADRY-B2 — Zmiana warunków zatrudnienia (aneks)
**Cel:** zarejestrować aneks do umowy o pracę — zmianę warunków obowiązującą **od wskazanego dnia**
(np. zmiana stanowiska, miejsca pracy, wymiaru, jednostki organizacyjnej). Realizuje się przez
**nowy zapis historyczny** etatu „od daty", nie przez nadpisanie bieżącego.
**Pola `Etat` (subrow `PracHistoria.Etat: Soneta.Kadry.Etat`):**
| Dana | Pole / typ | Uwaga |
|---|---|---|
| Okres etatu (oddo) | `Etat.Okres: Soneta.Types.FromTo` | okres obowiązywania warunków; po `Update` zwykle już ustawiony |
| Rodzaj umowy o pracę | `Etat.TypUmowy: Soneta.Kadry.TypUmowyOPrace` | enum |
| Data zawarcia aneksu | `Etat.DataZawarcia: Soneta.Types.Date` | data podpisania |
| Stanowisko (opis) | `Etat.Stanowisko: string` | wymagane dla etatu |
| Jednostka organizacyjna | `Etat.Wydzial: Soneta.Kadry.Wydzial` | wymagane dla etatu; referencja (`session.GetKadry().Wydzialy.Firma`) |
| Oddział firmy | `Etat.Oddzial: Soneta.Core.OddzialFirmy` | opcjonalny |
| Miejsce wykonywania pracy | `Etat.MiejscePracy: string` | tekst |
| Podstawa stosunku pracy | `Etat.Podstawa: Soneta.Kadry.StosPracyNaPodstawie` | enum |
| Forma organizacji pracy | `Etat.FormaOrganizacjiPracy: Soneta.Kadry.FormaOrganizacjiPracy` | enum |
| Wymiar / stawka (na `Zaszeregowanie`) | `Etat.Zaszeregowanie.Wymiar: Fraction`, `Etat.Zaszeregowanie.Stawka: Currency` | patrz KADRY-B3 |
**Snippet:**
```csharp
var pracownik = session.GetKadry().Pracownicy.WgKodu["006"];
var odDnia = new Date(2026, 7, 1);
using (var t = session.Logout(editMode: true))
{
// Nowy zapis historii „od daty" — klonuje zapis aktualny na `odDnia`, skraca stary do dnia
// poprzedniego i zwraca nowy klon (okres od `odDnia`). Klon MUSI trafić do tabeli zapisów.
var nowy = (PracHistoria)pracownik.Historia.Update(odDnia);
pracownik.Module.PracHistorie.AddRow(nowy);
// Etat na klonie ma już Okres (sklonowany) — pola Etat są zapisywalne. Aneksowane warunki:
var etat = nowy.Etat;
etat.Stanowisko = "Starszy specjalista";
etat.MiejscePracy = "Oddział Kraków";
etat.DataZawarcia = new Date(2026, 6, 20);
etat.Wydzial = session.GetKadry().Wydzialy.Firma; // wymagane (referencja)
t.Commit();
}
session.Save();
```
**Pułapki:**
- `Update(date)` + `PracHistorie.AddRow(nowy)` to **nierozłączna para** — sam `Update` zwraca odpięty
klon; bez `AddRow` zmiana nie zostanie zapisana.
- `Update(date)` rzuca `HistorySubTable.DateDuplicateException`, gdy na `date` już zaczyna się zapis
(`Aktualnosc.From == date`) — wtedy modyfikuj istniejący zapis (`pracownik[date]`).
- Nie ustawiaj `PracHistoria.Aktualnosc` ani (zwykle) `Etat.Okres` ręcznie — zarządza nimi historia.
Jeśli aneks zmienia długość okresu etatu, ustaw `Etat.Okres` **przed** pozostałymi polami (bramka KADRY-B1).
- `Etat` to subrow — modyfikuj jego pola, nie przypisuj całego obiektu.
---
### KADRY-B3 — Przeszeregowanie (zmiana stawki / grupy zaszeregowania)
**Cel:** zmienić wynagrodzenie zasadnicze — stawkę i/lub grupę zaszeregowania, od wskazanego dnia.
**Pola `Etat.Zaszeregowanie: Soneta.Kadry.Zaszeregowanie` (subrow `Etat`):**
| Dana | Pole / typ | Uwaga |
|---|---|---|
| Rodzaj stawki | `Zaszeregowanie.RodzajStawki: Soneta.Kadry.RodzajStawkiZaszeregowania` | enum: `Godzinowa = 0`, `Miesieczna = 1`, `DochodDeklarowany = 2` |
| Typ stawki | `Zaszeregowanie.TypStawki: Soneta.Kadry.TypStawkiZaszeregowania` | enum: `Dowolna`, `Minimalna`, `ZZakresu`, `WgWskaźnika`, `Nieokreślona` |
| Kwota stawki | `Zaszeregowanie.Stawka: Soneta.Types.Currency` | brutto (miesięczna/godzinowa wg `RodzajStawki`) |
| Wymiar etatu | `Zaszeregowanie.Wymiar: Soneta.Types.Fraction` | `Fraction.One` = pełny etat |
| Grupa zaszeregowania | `Etat.Grupa: Soneta.Kadry.GrupaZaszeregowania` | **leży na `Etat`, nie na `Zaszeregowanie`**; referencja do słownika `session.GetKadry().GrupyZaszer` (opcjonalna) |
| Element wynagrodzenia | `Zaszeregowanie.Element: Soneta.Place.DefinicjaElementu` | element płacowy wiązany ze stawką (opcjonalny) |
| Wskaźnik (wg wskaźnika) | `Zaszeregowanie.WskaznikNazwa: string`, `Zaszeregowanie.WskaznikKrotnosc: double` | gdy `TypStawki = WgWskaźnika` |
**Snippet (bezpośrednia zmiana, „od daty"):**
```csharp
var pracownik = session.GetKadry().Pracownicy.WgKodu["006"];
var odDnia = new Date(2026, 7, 1);
using (var t = session.Logout(editMode: true))
{
var nowy = (PracHistoria)pracownik.Historia.Update(odDnia);
pracownik.Module.PracHistorie.AddRow(nowy);
var etat = nowy.Etat; // subrow zapisu; Okres ustawiony przez Update
etat.Zaszeregowanie.Stawka = (Currency)7200m; // podwyżka stawki zasadniczej
// etat.Grupa = session.GetKadry().GrupyZaszer... // ewentualna zmiana grupy (leży na Etat, nie na Zaszeregowanie)
t.Commit();
}
session.Save();
```
**Worker platformy (alternatywa seryjna):** przeszeregowania realizuje moduł
`Soneta.Przeszeregowania` — dokument `Soneta.Kadry.Przeszeregowanie` z elementami
`ElementPrzeszeregowania` (m.in. `Soneta.Kadry.ZmianaStawki`), wykonywany czynnością
`ZmianaStawkiWorker` (zmiana kwoty / procentowa / grupa) dla zaznaczonej grupy pracowników. Element
`ZmianaStawki` ma pola `Grupa: GrupaZaszeregowania`, `Kwota: Currency` i zapisuje wynik do
`Etat.Zaszeregowanie` właściwego zapisu historii.
**Pułapki:**
- `Wymiar`/`Stawka`/`RodzajStawki` na **świeżym** zapisie są zapisywalne dopiero po `Etat.Okres`
(bramka KADRY-B1); po `Update` okres jest już sklonowany, więc pola są zapisywalne.
- `Stawka` to `Currency` (nie `decimal`), `Wymiar` to `Fraction` (nie `double`) — safe-code §10.1.
- `Etat.Grupa`/`Zaszeregowanie.Element` to **referencje** do istniejących rekordów — nie twórz „w locie".
**Uwaga:** `Grupa` jest polem `Etat` (pobierasz ze słownika `session.GetKadry().GrupyZaszer`), a **nie**
polem `Zaszeregowanie``Zaszeregowanie` nie ma property `Grupa`.
- Kanonicznie ustawiasz pola stawki na `Etat.Zaszeregowanie` (pola bazodanowe), nie na delegatach
`Etat.Wymiar`/`Etat.TypStawki`.
---
### KADRY-B4 — Rozwiązanie / wygaśnięcie umowy o pracę
**Cel:** zakończyć stosunek pracy z dniem rozwiązania — ustawić datę końca okresu etatu, dane
wypowiedzenia oraz przyczynę/kod rozwiązania (na potrzeby świadectwa pracy i deklaracji ZUS).
**Wzorzec (zgodny z czynnością „Zwolnij zaznaczonych pracowników"):**
1. skróć `Etat.Okres.To` do dnia rozwiązania (na bieżącym zapisie albo na nowym zapisie „od daty"),
2. ustaw dane wypowiedzenia (`Etat.OkresWypowiedzenia.*`) i przyczynę (`Etat.RozwiazanieUmowy.*`),
3. opcjonalnie oznacz `Etat.PracownikZwolniony = true` i wyrejestruj z ubezpieczeń.
**Pola — `Etat.OkresWypowiedzenia: Soneta.Kadry.OkresWypowiedzenia` (subrow):**
| Dana | Pole / typ | Uwaga |
|---|---|---|
| Data złożenia wypowiedzenia | `OkresWypowiedzenia.DataZlozenia: Soneta.Types.Date` | data wręczenia wypowiedzenia |
| Długość — dni / tygodnie / miesiące | `OkresWypowiedzenia.Dni: int`, `.Tygodnie: int`, `.Miesiace: int` | okres wypowiedzenia |
| Data upływu | `OkresWypowiedzenia.Uplywa: Soneta.Types.Date` | data upływu okresu wypowiedzenia |
| Skrócony | `OkresWypowiedzenia.Skrocony: bool` | skrócony okres wypowiedzenia |
| Zwolnienie z obowiązku pracy od | `OkresWypowiedzenia.ZwolnionyZObowiazkuPracyOd: Date` | |
| Data rozwiązania umowy (odczyt) | `OkresWypowiedzenia.DataRozwiązaniaUmowy: Date` | kalkulowane |
**Pola — `Etat.RozwiazanieUmowy: Soneta.Kadry.RozwiazanieUmowy` (subrow):**
| Dana | Pole / typ | Uwaga |
|---|---|---|
| Przyczyna rozwiązania | `RozwiazanieUmowy.PrzyczynaRozwUmowy: Soneta.Kadry.PrzyczynaRozwUmowy` | **referencja do słownika** `session.GetKadry().PrzyczRozwUmow` (indeks `WgNazwy` lub iteracja; **brak indeksera `WgKodu`**); rekord ma `Typ: TypPrzyczynyRozwUmowy` |
| Opis przyczyny | `RozwiazanieUmowy.PrzyczynaRozwUmowyOpis: string` | tekst |
| Podstawa prawna | `RozwiazanieUmowy.PodstawaPrawna: Soneta.Kadry.KodPodstawyPrawnejZwolnienia` | enum (tryb rozwiązania: za wypowiedzeniem, porozumienie, wygaśnięcie itd.) |
| Kod zwolnienia (ZUS) | `RozwiazanieUmowy.KodZwolnienia: Soneta.Kadry.KodZwolnienia` | enum (kod do ZUS RA/świadectwa) |
| Inicjatywa | `RozwiazanieUmowy.Inicjatywa: Soneta.Kadry.KodInicjatywyZwolnienia` | enum (pracodawca/pracownik) |
| Za odszkodowaniem | `RozwiazanieUmowy.ZaOdszkodowaniem: bool` | |
| Pracownik zwolniony (flaga) | `Etat.PracownikZwolniony: bool` | znacznik zakończenia |
**Snippet:**
```csharp
var pracownik = session.GetKadry().Pracownicy.WgKodu["006"];
var dataRozwiazania = new Date(2026, 9, 30);
using (var t = session.Logout(editMode: true))
{
var ph = pracownik.Last;
var etat = ph.Etat;
// 1) skrócenie okresu etatu do dnia rozwiązania
etat.Okres = new FromTo(etat.Okres.From, dataRozwiazania);
// 2) dane wypowiedzenia
etat.OkresWypowiedzenia.DataZlozenia = new Date(2026, 8, 31);
etat.OkresWypowiedzenia.Miesiace = 1;
// 3) przyczyna / tryb rozwiązania
// PrzyczynaRozwUmowy to rekord słownika — pobierz po nazwie (WgNazwy) albo iteracją (brak WgKodu):
etat.RozwiazanieUmowy.PrzyczynaRozwUmowy = session.GetKadry().PrzyczRozwUmow.WgNazwy["Wypowiedzenie przez pracownika"]; // referencja
// PodstawaPrawna to enum kodów: NieDotyczy, _400.._463, _550 (kody GUS/ZUS) — wybierz właściwy kod:
etat.RozwiazanieUmowy.PodstawaPrawna = KodPodstawyPrawnejZwolnienia._400;
etat.RozwiazanieUmowy.Inicjatywa = KodInicjatywyZwolnienia.Pracownik;
etat.PracownikZwolniony = true; // znacznik zakończenia zatrudnienia
t.Commit();
}
session.Save();
```
**Pułapki:**
- **Wygaśnięcie** vs **rozwiązanie** rozróżnia `PodstawaPrawna` (enum trybu) oraz `KodZwolnienia`
to one trafiają do świadectwa pracy i deklaracji ZUS.
- `PrzyczynaRozwUmowy` to **rekord słownika** (referencja), nie enum — pobierz istniejący wpis z
`session.GetKadry().PrzyczRozwUmow` (indeks `WgNazwy` lub iteracja — **słownik nie ma indeksera
`WgKodu`**). Pomyłka: `RozwiazanieUmowy.PrzyczynaRozwUmowy` (referencja) ≠
`PrzyczynaRozwUmowy.Typ` (enum `TypPrzyczynyRozwUmowy` na rekordzie słownika).
- `KodPodstawyPrawnejZwolnienia` to enum kodów GUS/ZUS o wartościach `NieDotyczy`, `_400`..`_463`,
`_550` (nazwy z prefiksem `_`) — **nie ma** wartości opisowych typu
`RozwiazanieZaWypowiedzeniemPrzezPracownika`. `KodInicjatywyZwolnienia`: `NieDotyczy`, `Pracownik`,
`Pracodawca`.
- Skrócenie `Etat.Okres.To` zmienia warunki w **całym** bieżącym okresie zapisu. Jeśli rozwiązanie
ma obowiązywać od konkretnego dnia z zachowaniem poprzedniego okresu, użyj nowego zapisu
(`Historia.Update(data)` + `PracHistorie.AddRow`), a zmiany rób na klonie.
- Wyrejestrowanie z ubezpieczeń (`IUbezpieczenie.Wyrejestrowany`/daty `Do`) to osobny krok — patrz KADRY-A7.
- `Okres`/`DataZlozenia`/`Uplywa` to `FromTo`/`Date`, nie `DateTime`.
---
### KADRY-B5 — Obniżenie / przywrócenie wymiaru etatu
**Cel:** czasowo obniżyć wymiar etatu i stawkę (operacje typu COVID / seryjne), a następnie
przywrócić warunki. Stan obniżenia jest **odczytowo** widoczny w subrowie
`Etat.ObnizenieEtatu: Soneta.Kadry.ObniżenieWymiaruEtatu` (delegat do zapisu historii etatu).
> **Ważne (zweryfikowane na DLL):** subrow `ObniżenieWymiaruEtatu` jest **w całości tylko-do-odczytu**
> — wszystkie jego property (`Wymiar`, `Stawka`, `RodzajStawki`, `TypStawki`, `Element`, `Kalendarz`,
> `Info`) mają `CanWrite == false`, a klasa **nie udostępnia publicznej metody `Save(...)`**.
> Z poziomu kodu biznesowego **nie da się** ustawić tych pól ani „utrwalić" obniżenia przez ten subrow.
> Pełny zapis stanu obniżenia (z metadanymi `ObniżenieWymiaruEtatuInfo`) realizują **workery platformy**.
> W zwykłym kodzie obniżenie sprowadzasz do ustawienia docelowego `Etat.Zaszeregowanie.Wymiar`
> (i ewentualnie `Stawka`) na nowym zapisie „od daty".
**Pola odczytowe — `Etat.ObnizenieEtatu: Soneta.Kadry.ObniżenieWymiaruEtatu` (subrow, read-only):**
| Dana | Pole / typ | Uwaga |
|---|---|---|
| Obniżony wymiar etatu | `ObnizenieEtatu.Wymiar: Soneta.Types.Fraction` | **read-only** |
| Obniżona stawka | `ObnizenieEtatu.Stawka: Soneta.Types.Currency` | **read-only** |
| Rodzaj / typ stawki | `ObnizenieEtatu.RodzajStawki`, `.TypStawki` | enumy, **read-only** |
| Kalendarz | `ObnizenieEtatu.Kalendarz: Soneta.Kalend.KalendarzBase` | referencja, **read-only** |
| Element wynagrodzenia | `ObnizenieEtatu.Element: Soneta.Place.DefinicjaElementu` | referencja, **read-only** |
| Zakres obniżenia (przełącznik) | `ObnizenieEtatu.Info: Soneta.Kadry.ObniżenieWymiaruEtatuInfo` | enum (`Brak`/`Wymiar`/`Stawka`/`Zaszeregowanie`/`Kalendarz`/…), **read-only** |
**Snippet (obniżenie wymiaru „od daty" w kodzie biznesowym):**
```csharp
var pracownik = session.GetKadry().Pracownicy.WgKodu["006"];
var odDnia = new Date(2026, 7, 1);
using (var t = session.Logout(editMode: true))
{
var ph = (PracHistoria)pracownik.Historia.Update(odDnia);
pracownik.Module.PracHistorie.AddRow(ph);
// Subrow ObnizenieEtatu jest read-only — NIE ustawiamy go bezpośrednio.
// Docelowy wymiar po obniżeniu utrwalamy na Etat.Zaszeregowanie.Wymiar (pole zapisywalne):
ph.Etat.Zaszeregowanie.Wymiar = new Fraction(4, 5); // np. obniżenie do 4/5 etatu
t.Commit();
}
session.Save();
```
**Workery platformy (seryjne, na zaznaczonych pracownikach, tabela `Pracownicy`):**
`ObniżWymiarEtatuWorker` (obniżenie: proporcjonalnie / do / o), `ZmianaStawkiZaszeregowaniaWorker`,
`ZmianaKalendarzaWorker`, `PrzywróćWarunkiZatrudnieniaWorker` (przywrócenie warunków sprzed
obniżenia). To one zakładają nowy zapis historii „od daty" i zapisują pełny stan obniżenia
(`ObniżenieWymiaruEtatuInfo`), którego nie da się ustawić przez publiczny kontrakt subrowa.
**Pułapki:**
- `Etat.ObnizenieEtatu` to **odczytowy delegat** do zapisu historii etatu — wszystkie property są
read-only i klasa nie ma metody `Save(...)`. W kodzie biznesowym obniżenie wymiaru realizujesz
ustawiając `Etat.Zaszeregowanie.Wymiar` (i `Stawka`) na nowym zapisie; pełny zapis stanu obniżenia
z `ObniżenieWymiaruEtatuInfo` zostaw workerom platformy.
- Operacja jest „od daty" — zawsze przez nowy zapis (`Update` + `AddRow`); inaczej zmienisz wymiar
wstecz w całym bieżącym okresie.
- Przywrócenie warunków to osobna operacja (`PrzywróćWarunkiZatrudnieniaWorker`) — nie polega na
usuwaniu obniżenia, lecz na nowym zapisie z przywróconym wymiarem.
---
### KADRY-B6 — Podzielniki kosztów (rozdział kosztów wynagrodzenia)
**Cel:** rozdzielić koszty wynagrodzenia pracownika na wydziały/projekty/centra kosztów wg
współczynników. Struktura: `Pracownik` jako **źródło** podzielnika →
`pracownik.Podzielniki: SubTable<Soneta.Core.PodzielnikKosztow>` → każdy podzielnik ma **historię**
`PodzielnikKosztow.Historia: HistorySubTable<HistoriaPodzielnika>` → a zapis historii ma kolekcję
`HistoriaPodzielnika.Elementy: SubTable<Soneta.Core.ElementPodzielnika>` (poszczególne udziały).
> Uwaga: `Pracownik.ElementyPodzielnika: SubTable<ElementPodzielnika>` to widok zbiorczy elementów
> ze wszystkich podzielników pracownika (do odczytu). **Tworzysz** elementy na konkretnym zapisie
> `HistoriaPodzielnika`, nie przez tę kolekcję.
**Tworzenie obiektów (konstruktory + AddRow):**
| Obiekt | Konstruktor | Tabela / AddRow |
|---|---|---|
| Podzielnik | `new PodzielnikKosztow(pracownik)` (pracownik jako `IZrodloPodzielnikaKosztow`) | `session.GetCore().PodzielKosztow.AddRow(p)` |
| Zapis historii | `podzielnik.Historia.Update(odDnia)` | `session.GetCore().HistPodzielnikow.AddRow(h)` |
| Element udziału | `new ElementPodzielnika(historia)` | `session.GetCore().ElemPodzielnikow.AddRow(e)` |
**Pola — `PodzielnikKosztow`:** `Nazwa: string`, `Definicja: Soneta.Core.DefinicjaPodzielnikaKosztow`,
`Zrodlo: IZrodloPodzielnikaKosztow` (pracownik, ustawiany ctorem), `Last/Historia`.
**Pola — `HistoriaPodzielnika`:** `Aktualnosc: FromTo` (okres zapisu, zarządzany), `Podstawa: decimal`,
`Elementy: SubTable<ElementPodzielnika>`.
**Pola — `ElementPodzielnika`:** `ElementPodzialowy: Soneta.Core.IElementSlownika` (cel rozdziału —
m.in. `Wydzial`, `Projekt`, `CentrumKosztow`, `OddzialFirmy` — iface-ref), `Wspolczynnik: double`,
`Procent: Percent` (kalkulowany z współczynników).
**Snippet:**
```csharp
var core = session.GetCore();
var pracownik = session.GetKadry().Pracownicy.WgKodu["006"];
var odDnia = new Date(2026, 1, 1);
var wydzialA = session.GetKadry().Wydzialy.Firma; // referencja do celu rozdziału (IElementSlownika)
using (var t = session.Logout(editMode: true))
{
var podzielnik = new PodzielnikKosztow(pracownik); // ctor wiąże ze źródłem (pracownik)
core.PodzielKosztow.AddRow(podzielnik);
podzielnik.Nazwa = "Rozdział kosztów";
// podzielnik.Definicja = ... // referencja do definicji (opcjonalnie)
// zapis historii „od daty" (klon + AddRow)
var historia = podzielnik.Historia.Update(odDnia);
core.HistPodzielnikow.AddRow(historia);
// udział: cel rozdziału + współczynnik
var element = new ElementPodzielnika(historia);
core.ElemPodzielnikow.AddRow(element);
element.ElementPodzialowy = wydzialA;
element.Wspolczynnik = 100d; // Procent wyliczany z współczynników
t.Commit();
}
session.Save();
// Odczyt zbiorczy elementów podzielnika pracownika:
foreach (ElementPodzielnika e in pracownik.ElementyPodzielnika)
{
IElementSlownika cel = e.ElementPodzialowy; // np. Wydzial / Projekt
double wsp = e.Wspolczynnik;
}
```
**Pułapki:**
- Trójpoziomowa struktura — `PodzielnikKosztow` (root, źródło = pracownik) → `HistoriaPodzielnika`
(historia „od daty") → `ElementPodzielnika` (udziały). Każdy poziom: konstruktor **+** `AddRow` do
właściwej tabeli `Core`. Sam konstruktor nie wystarczy.
- `Historia.Update(odDnia)` + `HistPodzielnikow.AddRow` — para jak w KADRY-A14; zmiana udziałów „od dnia"
to nowy zapis historii (a wcześniej zwykle usunięcie/`Delete()` elementów starego zapisu przy
aktualizacji tego samego okresu — patrz worker pracy zdalnej).
- `ElementPodzialowy` to **referencja interfejsowa** (`IElementSlownika`) — przypisz istniejący
rekord (`Wydzial`, `Projekt`, `CentrumKosztow`, …), nie twórz „w locie".
- `Procent` jest kalkulowany z `Wspolczynnik` poszczególnych elementów — ustawiasz współczynniki,
nie procenty.
---
### KADRY-B7 — Aktualizacja danych wg definicji stanowiska (matrycy)
**Cel:** powiązać etat z definicją stanowiska i przejąć z niej parametry (stawka/grupa/wymiar,
kalendarz, kod zawodu). Definicja stanowiska to **matryca** — wzorzec wartości dla etatu.
**Pole na etacie:** `Etat.Definicja: Soneta.HR.DefinicjaStanowiska` (referencja do słownika
konfiguracyjnego `session.GetHR().DefStanowisk`). Pokrewne: `Etat.DefinicjaFunkcji: DefinicjaFunkcji`.
**Pola `DefinicjaStanowiska` (matryca, do skopiowania na etat):**
| Dana | Pole / typ |
|---|---|
| Nazwa / stanowisko | `Nazwa: string`, `Stanowisko: string`, `StanowiskoPelne: string` |
| Funkcja | `Funkcja: string`, `DefinicjaFunkcji: Soneta.HR.DefinicjaFunkcji` |
| Zaszeregowanie (wzorzec) | `Zaszeregowanie: Soneta.Kadry.Zaszeregowanie` (`Stawka`, `Wymiar`, `RodzajStawki`, `Element`, `WskaznikNazwa/Krotnosc`) |
| Typ stawki / grupa | `TypStawki: TypStawkiZaszeregowania`, `Grupa: Soneta.Kadry.GrupaZaszeregowania` |
| Kalendarz | `Kalendarz: Soneta.Kalend.Kalendarz`, `NieNadpisujKalendarza: bool` |
| Kod zawodu / praca w szcz. warunkach | `KodWykonywanegoZawodu`, `KodPracyWSzczWarunkach`, `InterpretacjaKalendarza` |
**Snippet:**
```csharp
var pracownik = session.GetKadry().Pracownicy.WgKodu["006"];
var def = session.GetHR().DefStanowisk.WgNazwa["Specjalista ds. kadr"]; // matryca (referencja; klucz WgNazwa)
var odDnia = new Date(2026, 7, 1);
using (var t = session.Logout(editMode: true))
{
var nowy = (PracHistoria)pracownik.Historia.Update(odDnia);
pracownik.Module.PracHistorie.AddRow(nowy);
var etat = nowy.Etat;
etat.Definicja = def; // powiązanie z definicją stanowiska
etat.Stanowisko = def.Stanowisko; // przeniesienie wartości z matrycy
etat.Zaszeregowanie.Wymiar = def.Zaszeregowanie.Wymiar;
etat.Zaszeregowanie.Stawka = def.Zaszeregowanie.Stawka;
t.Commit();
}
session.Save();
```
**Pułapki:**
- `Etat.Definicja` to **referencja** do rekordu konfiguracyjnego `DefStanowisk` — pobierz istniejącą
(`session.GetHR().DefStanowisk`), nie twórz „w locie". Indeks po nazwie to `WgNazwa`
(**nie `WgNazwy`**); w bazie Demo słownik bywa pusty — zabezpiecz się na brak definicji.
- Definicja jest matrycą — przeniesienie wartości (stawka/wymiar/kalendarz) na etat zrób jawnie;
samo wskazanie `Etat.Definicja` nie nadpisuje automatycznie wszystkich pól etatu w kodzie biznesowym.
- Dostępność definicji potrafi zależeć od konfiguracji (`DefinicjeStanowiskDlaWydziałów`) — definicja
może być filtrowana po wydziale.
- Zmiana stanowiska „od dnia" to nowy zapis historii (KADRY-A14), nie nadpisanie bieżącego.
@@ -0,0 +1,609 @@
# KADRY03 — Dodatki, potrącenia, akordy
> Wspólne fakty o typie, podstawowe typy i szablon wzorca: [../kadry.md](../kadry.md).
### KADRY-C1 — Dodatki / stałe elementy wynagrodzenia (★)
**Cel:** przypisać pracownikowi stały element wynagrodzenia (dodatek — np. premia, dodatek
funkcyjny), oparty o definicję elementu płacowego, z okresem obowiązywania i parametrami
(podstawa/procent/czas). W UI: menu czynności *Dodatki i potrącenia → Dodaj nowy*.
**Klasa i model:** `Soneta.Kadry.Dodatek``GuidedRow` root, tabela `Dodatki`, obiekt
**historyczny** (kolekcja `Dodatek.Historia: HistorySubTable<Soneta.Kadry.DodHistoria>`, parametry
„oddo" siedzą w zapisach `DodHistoria`). Dodatek jest childem pracownika i pojawia się w
`pracownik.Dodatki: SubTable<Soneta.Kadry.Dodatek>`.
**Tworzenie:** `new Dodatek(pracownik)` + `session.GetKadry().Dodatki.AddRow(d)`. Dodanie do tabeli
tworzy **pierwszy zapis** `DodHistoria` — dostępny od razu jako `d.Last`. Parametry ustawiamy na
`d.Last`.
**Pola i typy (`DodHistoria` — `d.Last`):**
| Pole | Typ | Uwaga |
|---|---|---|
| `Element` | `Soneta.Place.DefinicjaElementu` | definicja elementu wynagrodzenia (wymagana); pobierz istniejącą z `session.GetPlace().DefElementow.WgNazwy[nazwa]` |
| `Okres` | `Soneta.Types.FromTo` | okres obowiązywania dodatku |
| `Podstawa` | `Soneta.Types.Currency` | kwota podstawy (gdy algorytm definicji jej wymaga) |
| `Procent` | `Soneta.Types.Percent` | procent (gdy algorytm definicji go wymaga) |
| `Czas` | `Soneta.Types.Time` | czas (gdy algorytm definicji go wymaga) |
| `Ulamek` | `Soneta.Types.Fraction` | ułamek (zależnie od definicji) |
| `Dni` | `int` | liczba dni (zależnie od definicji) |
| `Aktualnosc` | `Soneta.Types.FromTo` | okres zapisu historycznego (zarządzany przez historię — nie ustawiaj ręcznie) |
**Pola na rootcie `Dodatek`:** `Nazwa: string`, `Pracownik: Soneta.Kadry.Pracownik` (właściciel,
ustawiany ctorem), `DataZakonczeniaWyplaty: Date`, `Last: DodHistoria`,
`Historia: HistorySubTable<DodHistoria>`, `Dodatki` (tabela: `session.GetKadry().Dodatki`).
**Pobranie definicji elementu:** słownik `session.GetPlace().DefElementow` (kolekcja konfiguracyjna).
Indeksowanie po nazwie: `DefElementow.WgNazwy["Premia"]`. Definicje dodatków mają
`RodzajZrodla == Soneta.Place.RodzajŹródłaWypłaty.Dodatek` — można nimi filtrować dostępne
definicje. W bazie Demo istnieją gotowe definicje, m.in. `"Premia"`, `"Premia procentowa"`.
**Pułapki:**
- **`new Dodatek(pracownik)` + `Dodatki.AddRow(d)` to para** — sam konstruktor nie włącza dodatku do
sesji ani nie tworzy zapisu historii. Pierwszy `DodHistoria` powstaje przy `AddRow`; dopiero potem
istnieje `d.Last`.
- `Podstawa`/`Procent`/`Czas` **mogą być tylko-do-odczytu** w zależności od algorytmu wskazanej
`DefinicjaElementu` — element kwotowy udostępnia `Podstawa`, element procentowy `Procent` itd.
Ustawiaj tylko te pola, których wymaga definicja (próba zapisu pola read-only rzuci wyjątek).
- `Element` jest **wymagany** — bez wskazania definicji elementu dodatek nie ma sensu płacowego.
Definicję pobierasz z istniejącego słownika (`DefElementow`), nie tworzysz „w locie" w tym scenariuszu.
- Zmiana parametrów dodatku **od konkretnego dnia** to nowy zapis historii dodatku
(`d.Historia.Update(date)` + `Dodatki.Module.DodHistorie.AddRow(nowy)`), analogicznie do KADRY-A14 — nie
nadpisuj bieżącego zapisu, jeśli chcesz zachować poprzedni okres.
- `DodHistoria.Aktualnosc` (okres zapisu) zarządza mechanizm historii — sam ustawiasz `Okres`,
`Aktualnosc` zostaw historii.
**Snippet:**
```csharp
var kadry = session.GetKadry();
var pracownik = kadry.Pracownicy.WgKodu["006"];
// Definicja elementu wynagrodzenia ze słownika konfiguracyjnego (po nazwie):
var definicjaPremii = session.GetPlace().DefElementow.WgNazwy["Premia"];
using (var t = session.Logout(editMode: true))
{
// new Dodatek(pracownik) + AddRow — AddRow tworzy pierwszy zapis DodHistoria (d.Last):
var dodatek = new Dodatek(pracownik);
kadry.Dodatki.AddRow(dodatek);
var h = dodatek.Last; // pierwszy zapis historii dodatku
h.Element = definicjaPremii; // definicja elementu (wymagana)
h.Okres = new FromTo(new Date(2026, 1, 1), Date.MaxValue);
h.Podstawa = (Currency)500m; // gdy algorytm definicji wymaga podstawy
t.Commit();
}
session.Save();
// Odczyt dodatków pracownika i ich definicji elementu (kolekcja childów):
foreach (Dodatek d in pracownik.Dodatki)
{
DefinicjaElementu element = d.Last.Element;
FromTo okres = d.Last.Okres;
}
```
### KADRY-C2 — Potrącenia (stałe i jednorazowe) (★)
**Cel:** przypisać pracownikowi potrącenie z wynagrodzenia (np. składka związkowa, spłata
rozliczana ręcznie, potrącenie dobrowolne). W modelu płacowym **potrącenie nie ma osobnej klasy**
to **`Soneta.Kadry.Dodatek`** (jak KADRY-C1), tyle że oparty o **definicję elementu o charakterze
potrącenia**. O „minusowym" charakterze decyduje wyłącznie wskazana `DefinicjaElementu` (jej algorytm),
a nie typ obiektu po stronie pracownika.
**Jak rozpoznać definicję potrącenia (`Soneta.Place.DefinicjaElementu`, słownik `DefElementow`):**
| Pole definicji | Typ | Znaczenie dla potrącenia |
|---|---|---|
| `Algorytm.Potracenie` | `bool` | **kluczowy znacznik**`true` dla elementu potrącającego (element pomniejsza wynagrodzenie) |
| `Algorytm.LimitPotracenia` | `Soneta.Place.TypLimituPotrącenia` | rodzaj limitu (np. do kwoty wolnej) — gdy potrącenie limitowane |
| `Algorytm.TylkoPelnePotracenie` | `bool` | potrącać tylko w pełnej kwocie (bez częściowego) |
| `RodzajZrodla` | `Soneta.Place.RodzajŹródłaWypłaty` | dla potrącenia przez dodatek **musi być** `Dodatek` (= `6`); enum **nie ma** wartości „Potrącenie" (ma natomiast m.in. `ZajęcieKomornicze` = 23, `Świadczenie` = 12, `Pożyczka` = 18, `PożyczkaSpłata` = 19). Minus realizuje algorytm, ale `DodHistoria.Element` **odrzuca** definicje o `RodzajZrodla != Dodatek` (np. „Alimenty" jako `ZajęcieKomornicze`) — patrz pułapki |
**Mechanizm — identyczny jak KADRY-C1 (Dodatek + DodHistoria):**
- `new Dodatek(pracownik)` + `session.GetKadry().Dodatki.AddRow(d)` → powstaje pierwszy `DodHistoria`
(`d.Last`).
- Na `d.Last` ustawiamy `Element` (definicja potrącenia), `Okres` oraz `Podstawa`/`Procent`/`Kwota`
zależnie od algorytmu definicji.
- **Potrącenie stałe**: `Okres` otwarty (do `Date.MaxValue`) lub na czas określony — naliczane w każdej
wypłacie z okresu.
- **Potrącenie jednorazowe**: `Okres` zawężony do jednego miesiąca rozliczeniowego (tylko ten miesiąc
obejmie naliczenie).
- Zakończenie potrącenia: `d.DataZakonczeniaWyplaty` + ewentualnie `d.PrzyczynaZakonczenia`, albo nowy
zapis historii „od daty" (`d.Historia.Update(date)`), analogicznie do KADRY-C1/KADRY-A14.
**Pułapki:**
- Nie szukaj klasy „Potrącenie" — jej **nie ma**. Potrącenie = `Dodatek` z definicją, w której
`Algorytm.Potracenie == true`. Dobór definicji jest jedynym wyróżnikiem.
- **Filtruj po DWÓCH warunkach** (zweryfikowane testem): `d.Algorytm.Potracenie && d.RodzajZrodla ==
RodzajŹródłaWypłaty.Dodatek`. Sam `Algorytm.Potracenie` **nie wystarcza** — przy ustawianiu
`DodHistoria.Element` definicja o innym `RodzajZrodla` (np. „Alimenty" jako `ZajęcieKomornicze`)
rzuca `System.Exception: "Zły rodzaj źródła wypłaty elementu …"`. Element zajęcia komorniczego ma
`RodzajZrodla == ZajęcieKomornicze` i podpinasz go pod `ZajęcieKomornicze`, nie pod `Dodatek` (KADRY-C4).
- `Podstawa`/`Procent`/`Czas` na `DodHistoria` bywają tylko-do-odczytu zależnie od algorytmu definicji
(jak w KADRY-C1) — ustawiaj tylko te, których definicja wymaga.
- `Element` wymagany; pobierany z istniejącego słownika `DefElementow`, nie tworzony „w locie".
**Snippet:**
```csharp
var kadry = session.GetKadry();
var pracownik = kadry.Pracownicy.WgKodu["006"];
// Definicja potrącenia ze słownika — DWA warunki: Potracenie ORAZ RodzajZrodla == Dodatek:
var def = session.GetPlace().DefElementow.Cast<DefinicjaElementu>()
.First(d => d.RodzajZrodla == RodzajŹródłaWypłaty.Dodatek
&& d.Algorytm != null && d.Algorytm.Potracenie);
using (var t = session.Logout(editMode: true))
{
var potracenie = new Dodatek(pracownik);
kadry.Dodatki.AddRow(potracenie); // tworzy pierwszy DodHistoria
var h = potracenie.Last;
h.Element = def; // definicja o Algorytm.Potracenie == true
h.Okres = new FromTo(new Date(2026, 1, 1), Date.MaxValue); // stałe
h.Podstawa = (Currency)50m; // gdy algorytm definicji wymaga kwoty
t.Commit();
}
session.Save();
```
---
### KADRY-C3 — Akordy (★)
**Cel:** przypisać pracownikowi pracę akordową (rozliczaną wg ilości/strefy), z okresem i definicją
akordu; zakończyć akord. W UI: menu czynności *Akordy → Dodaj nowy / Zakończ*.
**Klasa i model:** `Soneta.Kadry.Akord` — `GuidedRow` root, tabela `Akordy`, obiekt **historyczny**
(`Akord.Historia: HistorySubTable<Soneta.Kadry.AkordHistoria>`; parametry „oddo" w zapisach
`AkordHistoria`, dostęp do bieżącego przez `Akord.Last`). Akord jest childem pracownika:
`pracownik.Akordy: SubTable<Soneta.Kadry.Akord>`.
**Pola root `Akord`:**
| Pole | Typ | Uwaga |
|---|---|---|
| `Pracownik` | `Soneta.Kadry.Pracownik` | właściciel (relacja) |
| `Definicja` | `Soneta.Kadry.DefinicjaAkordu` | definicja akordu (słownik `DefinicjeAkordow`) |
| `Okres` | `Soneta.Types.FromTo` | okres obowiązywania akordu |
| `Typ` | `Soneta.Kadry.TypAkordu` | typ akordu |
| `Wydzial` | `Soneta.Kadry.Wydzial` | jednostka organizacyjna realizacji |
| `Last` | `Soneta.Kadry.AkordHistoria` | bieżący zapis historii |
| `Dni` | `DateSubTable<Soneta.Kalend.DzienAkorduBase>` | dzienna realizacja akordu |
**Pola `AkordHistoria` (`Akord.Last`):** `Okres: FromTo`, `Algorytm: Soneta.Kadry.AlgorytmAkordu`
(subrow z `Algorytm.Element: DefinicjaElementu`, `Algorytm.Wspolczynnik`, `Algorytm.Progi`,
`Algorytm.WgCzasu`/`Progresywny` itd.), `Jednostka: string`, `Aktualnosc: FromTo` (zarządzane przez
historię), `Progi: SubTable<Soneta.Kadry.ProgAkordu>`.
**Tworzenie — brak publicznego konstruktora `Akord(pracownik)`.** Akord dodaje się **workerem**
operacyjnym (kanonicznie), nie `new`. Konstruktor `Akord` jest niepubliczny (poza `RowCreator`).
Worker jest „jak z UI" (`Params` dziedziczy z `ContextBase`, ctor wymaga `Context`) — uruchamiaj go w
transakcji `CommitUI`.
**Workery (zagnieżdżone w `Pracownik`):** ctor `(Session)`, parametry przez właściwości `Pars`/`Pracownicy`;
`Params` ma ctor `(Context)`.
| Worker | Metoda | Wzorzec użycia |
|---|---|---|
| `Soneta.Kadry.Pracownik.DodajAkordWorker` | `DodajAkord` | `new Params(ctx) { Definicja, OdDnia, DoDnia, DodajKolejny }`; `new DodajAkordWorker(session) { Pars = par, Pracownicy = tab }` |
| `Soneta.Kadry.Pracownik.ZakończAkordWorker` | `ZakończAkord` | `new Params(ctx) { Definicja, DoDnia, ZakończWszystkie }`; `new ZakończAkordWorker(session) { Pars = par, Pracownicy = tab }` |
**Pułapki:**
- Akordu **nie twórz przez `new Akord(...)`** — kanoniczna ścieżka to `DodajAkordWorker` (analogicznie
`ZakończAkordWorker` do zakończenia). Workery przyjmują **tablicę pracowników**, więc nadają się też do
operacji grupowej.
- `Definicja` (akordu) to rekord słownika `DefinicjeAkordow` — pobierz istniejący, nie twórz „w locie".
Sam akord wiąże dopiero z `DefinicjaElementu` (płacowym) przez `Algorytm.Element` definicji akordu.
- Akord jest historyczny — zmiana parametrów „od daty" to nowy zapis `AkordHistoria`
(`Historia.Update(date)`), analogicznie do KADRY-C1/KADRY-A14.
- Tabela `Akordy` to dane operacyjne — przy przeglądaniu poprzecznym filtruj zakresem (safe-code §6.3);
w zakresie jednego pracownika korzystaj z `pracownik.Akordy`.
**Snippet:**
```csharp
var kadry = session.GetKadry();
var pracownik = kadry.Pracownicy.WgKodu["006"];
var defAkordu = kadry.DefinicjeAkordow.WgNazwa["Akord prosty"]; // klucz WgNazwa (l.poj.)
var context = login.CreateEmptyContext().Clone(session);
using (var t = session.Logout(editMode: true))
{
var par = new Pracownik.DodajAkordWorker.Params(context) // Params: ctor (Context)
{
Definicja = defAkordu,
OdDnia = new Date(2026, 1, 1),
DoDnia = new Date(2026, 12, 31),
};
// ctor (Session); parametry przez właściwości Pars/Pracownicy:
new Pracownik.DodajAkordWorker(session) { Pars = par, Pracownicy = new[] { pracownik } }.DodajAkord();
t.CommitUI();
}
session.Save();
// Odczyt akordów pracownika:
foreach (Akord a in pracownik.Akordy)
{
DefinicjaAkordu def = a.Definicja;
FromTo okres = a.Okres;
DefinicjaElementu element = a.Last.Algorytm.Element;
}
```
---
### KADRY-C4 — Zajęcia wynagrodzenia (komornicze, alimentacyjne) (★)
**Cel:** zarejestrować zajęcie wynagrodzenia (egzekucja komornicza lub alimentacyjna) z numerem sprawy,
kwotą, priorytetem i wierzycielem (komornikiem/rachunkiem odbiorcy); anulować/przywrócić zajęcie.
**Klasa i model:** `Soneta.Kadry.ZajęcieKomornicze` — `GuidedRow` root, tabela `ZajKomornicze`, obiekt
**historyczny** (`Historia: HistorySubTable<Soneta.Kadry.ZajęcieKomorniczeHistoria>`; limity i kwoty
„oddo" w zapisach historii, bieżący przez `Last`). Child pracownika:
`pracownik.ZajęciaKomornicze: SubTable<Soneta.Kadry.ZajęcieKomornicze>`. **Konstruktor publiczny:**
`new ZajęcieKomornicze(pracownik)`.
**Pola root `ZajęcieKomornicze`:**
| Pole | Typ | Uwaga |
|---|---|---|
| `Pracownik` | `Soneta.Kadry.Pracownik` | właściciel |
| `Rodzaj` | `Soneta.Kadry.RodzajeZajęciaWynagrodzenia` | enum: `Kwota = 0`, `KwotaMiesięczna = 1` (jednorazowa kwota vs miesięczna) |
| `Element` | `Soneta.Place.DefinicjaElementu` | element płacowy zajęcia — **wymagany**; musi mieć `RodzajZrodla == ZajęcieKomornicze` (= 23) |
| `NumerSprawy` | `string` | numer sprawy egzekucyjnej |
| `Data` | `Soneta.Types.Date` | data zajęcia |
| `DataSplaty` | `Soneta.Types.Date` | data spłaty/zakończenia |
| `Rozliczenie.Odbiorca` | `Soneta.Kasa.IPodmiotKasowy` | **wierzyciel** — komornik/odbiorca (iface; może być `Kontrahent`, `Bank`, `Pracownik`, `UrzadSkarbowy`…) |
| `Rozliczenie.RachunekOdbiorcy` | `Soneta.Kasa.RachunekBankowyPodmiotu` | rachunek wierzyciela do przelewu |
| `Splacono` | `Soneta.Types.Currency` | kwota spłacona (kalkulowane/narastające) |
| `Pozostało` | `Soneta.Types.Currency` | kwota pozostała (kalkulowane) |
| `SplataZakonczona` | `bool` | spłata zakończona |
| `Anulowane` | `bool` | zajęcie anulowane (patrz workery) |
| `Korekty` | `SubTable<Soneta.Kadry.KorektaZajęciaKomorniczego>` | korekty zajęcia |
| `OpisPrzelewu` | `string` | tytuł przelewu |
**Limity i kwoty — na zapisie `ZajęcieKomorniczeHistoria` (`Last`):** kwota do potrącenia, limity
procentowe i kwotowe, zawieszenie spłaty, priorytet, ustawienia potrąceń z zasiłków/świadczeń (zmiana
„od daty" = nowy zapis historii).
**Workery (zagnieżdżone w `ZajęcieKomornicze`):** ctor **bezparametrowy**, parametr przez właściwość `Zajęcie`.
| Worker | Metoda | Wzorzec użycia |
|---|---|---|
| `Soneta.Kadry.ZajęcieKomornicze.AnulujWorker` | `Anuluj` | `new ZajęcieKomornicze.AnulujWorker { Zajęcie = zaj }.Anuluj()` |
| `Soneta.Kadry.ZajęcieKomornicze.PrzywrócWorker` | `Przywróć` | `new ZajęcieKomornicze.PrzywrócWorker { Zajęcie = zaj }.Przywróć()` |
**Pułapki:**
- **Pole `Priorytet` NIE istnieje** na `ZajęcieKomornicze` (sprostowanie). **Alimentacyjne vs
niealimentacyjne** rozstrzyga konfiguracja: wskazana `DefinicjaElementu` (`RodzajZrodla ==
ZajęcieKomornicze`) i parametry zapisu historii (limity), nie osobny typ klasy — to **jedna klasa**
`ZajęcieKomornicze`.
- `Anulowane` jest **tylko-do-odczytu** (brak publicznego settera) — anuluj **workerem** `AnulujWorker`.
- `Rozliczenie.Odbiorca` jest **interfejsem** `IPodmiotKasowy` — wskaż istniejący podmiot (zwykle
`Kontrahent`-komornik); nie twórz odbiorcy „w locie" w tym scenariuszu.
- Faktyczne **kwoty potrącenia (`Splacono`, `Pozostało`) wyliczają się przy naliczeniu wypłaty** — po
samym dodaniu zajęcia są zerowe/wyjściowe. Pełne rozliczenie wymaga naliczonej wypłaty (patrz sekcja
„niewykonalne publicznym API bez naliczenia").
- Anulowanie/przywracanie realizuj **workerami** (`AnulujWorker`/`PrzywrócWorker`), nie ręcznym
ustawianiem `Anulowane` — workery dbają o storna i spójność rozliczenia.
- Tabela operacyjna — przegląd poprzeczny z filtrem (safe-code §6.3).
**Snippet:**
```csharp
var kadry = session.GetKadry();
var pracownik = kadry.Pracownicy.WgKodu["006"];
// Element zajęcia — definicja o RodzajZrodla == ZajęcieKomornicze (nie zwykłe potrącenie-Dodatek):
var elementZajecia = session.GetPlace().DefElementow.Cast<DefinicjaElementu>()
.First(d => d.RodzajZrodla == RodzajŹródłaWypłaty.ZajęcieKomornicze);
var komornik = session.GetCRM().Kontrahenci.WgKodu["KOMORNIK1"]; // wierzyciel (IPodmiotKasowy)
using (var t = session.Logout(editMode: true))
{
var zajecie = new ZajęcieKomornicze(pracownik); // ctor publiczny
kadry.ZajKomornicze.AddRow(zajecie);
zajecie.Rodzaj = RodzajeZajęciaWynagrodzenia.KwotaMiesięczna;
zajecie.Element = elementZajecia; // wymagany (RodzajZrodla == ZajęcieKomornicze)
zajecie.NumerSprawy = "KM 123/2026";
zajecie.Data = new Date(2026, 1, 1);
zajecie.Rozliczenie.Odbiorca = komornik; // wierzyciel
zajecie.Rozliczenie.RachunekOdbiorcy = komornik.Rachunki.WgKodu["GŁÓWNY"];
t.Commit();
}
session.Save();
// Anulowanie zajęcia (worker bezparametrowy + property Zajęcie, nie ręczna flaga):
using (var t = session.Logout(editMode: true))
{
var zaj = pracownik.ZajęciaKomornicze.First();
new ZajęcieKomornicze.AnulujWorker { Zajęcie = zaj }.Anuluj();
t.CommitUI();
}
session.Save();
```
---
### KADRY-C5 — Operacje seryjne na dodatkach (moduł Przeszeregowania) (★)
**Cel:** dodać / zmienić / zakończyć dodatek (oraz zmienić stawkę) dla **grupy pracowników** jedną
operacją. Realizuje to moduł **`Soneta.Przeszeregowania.PrzeszeregowaniaModule`**. Dokumentem zbiorczym
jest `Przeszeregowanie` (tabela `Przeszeregowania`, root) z pozycjami `ElementPrzeszeregowania`
(tabela `ElementyPrzeszer`, child). Pracownik widzi swoje pozycje przez
`pracownik.ElementyPrzeszeregowania`.
**Workery operacyjne** — ctor **bezparametrowy**, parametry przez właściwości `Pars` (typu `Params`,
ctor `(Context)`) i `Pracownicy: Pracownik[]`. Uruchamiaj w transakcji `CommitUI`. **Uwaga:** workery
te w bezgłowym hoście testowym (bez operatora/kontekstu UI) rzucają `NullReferenceException` — wymagają
realnego środowiska aplikacji.
| Worker | Metoda | Params (publiczne pola) | Działanie |
|---|---|---|---|
| `Soneta.Przeszeregowania.NowyDodatekWorker` | `NowyDodatek` | `Definicja: DefinicjaElementu, Podstawa: Currency, Procent: Percent` | wypłata/nadanie nowego dodatku grupie |
| `Soneta.Przeszeregowania.ZmianaDodatkuWorker` | `ZmianaDodatku` | `Definicja, Podstawa, ZmianaPodstawy: Currency, ProcentowaZmianaPodstawy: Percent, Procent, ZmianaProcentu: Percent, DataStawki: Date, PodstawaPrecyzja, PodstawaSposob` | zmiana parametrów istniejącego dodatku |
| `Soneta.Przeszeregowania.ZakończDodatekWorker` | `ZakończDodatek` | `Definicja: DefinicjaElementu` | zakończenie wypłaty dodatku |
| `Soneta.Przeszeregowania.DodajZmienDodatekWorker` | `DodajZmienDodatek` | `Params` (dodanie lub zmiana łącznie) | dodanie albo zmiana dodatku |
| `Soneta.Przeszeregowania.DodajNagrodęWorker` | (nagroda) | — | seryjne nagrody |
| `Soneta.Przeszeregowania.ZmianaStawkiWorker` | `ZmianaStawki` | — | seryjna zmiana stawki zaszeregowania |
**Dokument `Przeszeregowanie` (alternatywa: zbuduj dokument i wykonaj).** Tworzenie: `new
Przeszeregowanie()` + `session.GetPrzeszeregowania().Przeszeregowania.AddRow(doc)` (kolekcja **nie ma**
`AddNew` — to standardowy `GuidedRow` root z publicznym ctorem bezparametrowym).
| Pole | Typ |
|---|---|
| `Data` | `Soneta.Types.Date` (data przeszeregowania) |
| `DataWykonania` | `Soneta.Types.Date` |
| `Nazwa` | `string` |
| `Realizacja` | `Soneta.Przeszeregowania.RealizacjaPrzeszeregowania` (stan) |
| `Pracownicy` | `ICollection<Soneta.Kadry.Pracownik>` |
| `Elementy` | `SubTable<Soneta.Przeszeregowania.ElementPrzeszeregowania>` |
| `ZarzadzaneWnioskiem` | `bool` |
`ElementPrzeszeregowania` (child) niesie zmianę per pracownik: `Definicja: DefinicjaElementu`,
`Kwota`/`ZmianaKwoty`/`ProcentowaZmianaKwoty`, `Procent`/`ZmianaProcentu`, `Grupa: GrupaZaszeregowania`,
`Krotnosc`/`ZmianaKrotnosci`, `RodzajPrzeszergowania`, `Pracownik`, `PracHistoria`.
**Wykonanie dokumentu:** `Soneta.Przeszeregowania.Przeszeregowanie.WykonajWorker` (metoda `Wykonaj`,
`Params { Wykonaj: bool }`) — materializuje dokument na danych pracowników (tworzy/zmienia dodatki).
`ElementPrzeszeregowania.Wykonaj(Log)` realizuje pojedynczą pozycję.
**Pułapki:**
- To **operacja seryjna na danych operacyjnych** — trzymaj transakcje krótkie, duże grupy dziel na paczki
(safe-code §13.1). Workery przyjmują tablicę pracowników — przekaż przefiltrowaną listę (po stronie
serwera, safe-code §6).
- Workery `NowyDodatek`/`ZmianaDodatku`/`ZakończDodatek` operują na **definicji elementu** (`Definicja`),
więc wybór właściwej `DefinicjaElementu` jest kluczowy (po nazwie / `RodzajZrodla == Dodatek`).
- Sam dokument `Przeszeregowanie` **nie zmienia danych** dopóki nie zostanie wykonany (`WykonajWorker`);
do tego momentu to plan. Po `Wykonaj` zmiany trafiają w dodatki/etat pracowników.
- Indywidualne (jednostkowe) odpowiedniki to workery z KADRY-C2/KADRY-C1 na pojedynczym pracowniku
(`Pracownik.DodajDodatekWorker`/`ZmieńDodatekWorker`/`ZabierzDodatekWorker`); moduł Przeszeregowania
jest dla **grupy**.
**Snippet (operacja seryjna — nowy dodatek dla grupy):**
```csharp
var kadry = session.GetKadry();
var def = session.GetPlace().DefElementow.WgNazwy["Premia"];
// Grupa pracowników — filtr serwerowy (np. po wydziale), nie pełny skan:
Pracownik[] grupa = kadry.Pracownicy[(Pracownik p) => p.Last.Etat.Okres.Contains(Date.Today)]
.Cast<Pracownik>().ToArray();
var context = login.CreateEmptyContext().Clone(session);
using (var t = session.Logout(editMode: true))
{
var par = new NowyDodatekWorker.Params(context) // Params: ctor (Context)
{
Definicja = def,
Podstawa = (Currency)300m,
};
// ctor bezparametrowy; parametry przez właściwości Pars/Pracownicy:
new NowyDodatekWorker { Pars = par, Pracownicy = grupa }.NowyDodatek();
t.CommitUI();
}
session.Save();
```
---
### KADRY-C6 — Świadczenia socjalne (ZFŚS) i ich rozliczenie (★)
**Cel:** przyznać pracownikowi świadczenie socjalne z ZFŚS (zapomoga, dopłata do wypoczynku, paczka)
i ustawić jego rozliczenie płacowe (element, kwota, okres).
**Klasa i model:** `Soneta.Kadry.SwiadczSocjalne` — `GuidedRow` root, tabela `SwiadczeniaSoc`. Child
pracownika: `pracownik.Swiadczenia: SubTable<Soneta.Kadry.SwiadczSocjalne>`. **Konstruktor publiczny:**
`new SwiadczSocjalne(pracownik)`.
**Pola `SwiadczSocjalne`:**
| Pole | Typ | Uwaga |
|---|---|---|
| `Pracownik` | `Soneta.Kadry.Pracownik` | właściciel |
| `Definicja` | `Soneta.Kadry.DefinicjaŚwiadczeniaSocjalnego` | rodzaj świadczenia (słownik `DefSwiadczSocjal`) |
| `Data` | `Soneta.Types.Date` | data przyznania |
| `Nazwa` | `string` | nazwa |
| `Opis` | `Soneta.Business.MemoText` | opis |
| `Rozliczenie` | `Soneta.Kadry.RozliczenieSwiadczenia` (subrow) | dane rozliczeniowe (poniżej) |
**Subrow `Rozliczenie` (`RozliczenieSwiadczenia`):**
| Pole | Typ | Uwaga |
|---|---|---|
| `Rozliczenie.Element` | `Soneta.Place.DefinicjaElementu` | element płacowy do rozliczenia świadczenia |
| `Rozliczenie.Kwota` | `Soneta.Types.Currency` | kwota świadczenia |
| `Rozliczenie.Okres` | `Soneta.Types.FromTo` | okres rozliczenia |
| `Rozliczenie.Data` | `Soneta.Types.Date` | data rozliczenia |
| `Rozliczenie.Rozliczone` | `bool` | czy rozliczone (po naliczeniu wypłaty) |
**Definicja (`DefinicjaŚwiadczeniaSocjalnego`, słownik `DefSwiadczSocjal`):** `Nazwa: string`,
`Element: DefinicjaElementu` (domyślny element rozliczenia), `Kwota: Currency` (domyślna kwota). Z niej
dziedziczy świadczenie domyślny element i kwotę.
**Worker seryjny:** `Soneta.Kadry.SwiadczSocjalne.DodajŚwiadczenieWorker` (metoda `DodajŚwiadczenie`) —
ctor **bezparametrowy**, parametry przez właściwości `Pars` i `Pracownicy: Pracownik[]`; `Params` ma
ctor `(Context)`: `Params { Definicja: DefinicjaŚwiadczeniaSocjalnego, DataPrzyznania: Date, Kwota:
Currency, Element: DefinicjaElementu, DataRozliczenia: Date }` — nadaje świadczenie grupie (menu
*Operacje seryjne / Dodaj świadczenia socjalne*). Wzorzec:
`new DodajŚwiadczenieWorker { Pars = new …Params(ctx){…}, Pracownicy = tab }.DodajŚwiadczenie()`.
**Pułapki:**
- `Definicja` (świadczenia) pobierana ze słownika `DefSwiadczSocjal`; jej `Element`/`Kwota` są domyślne —
na konkretnym świadczeniu nadpisujesz przez `Rozliczenie.Element`/`Rozliczenie.Kwota`.
- **Faktyczne rozliczenie (wypłata świadczenia, `Rozliczenie.Rozliczone == true`) następuje przy
naliczeniu wypłaty** — samo dodanie świadczenia tworzy tylko zlecenie rozliczenia.
- Dla grupy używaj `DodajŚwiadczenieWorker`; pojedynczo — `new SwiadczSocjalne(pracownik)` + `AddRow`.
**Snippet:**
```csharp
var kadry = session.GetKadry();
var pracownik = kadry.Pracownicy.WgKodu["006"];
var defSwiadcz = kadry.DefSwiadczSocjal.WgNazwy["Dopłata do wypoczynku"];
var element = session.GetPlace().DefElementow.WgNazwy["Świadczenie socjalne"];
using (var t = session.Logout(editMode: true))
{
var sw = new SwiadczSocjalne(pracownik);
kadry.SwiadczeniaSoc.AddRow(sw);
sw.Definicja = defSwiadcz;
sw.Data = new Date(2026, 6, 1);
sw.Rozliczenie.Element = element; // element płacowy rozliczenia
sw.Rozliczenie.Kwota = (Currency)1000m;
sw.Rozliczenie.Okres = FromTo.Month(new YearMonth(2026, 6));
t.Commit();
}
session.Save();
// Odczyt świadczeń pracownika:
foreach (SwiadczSocjalne s in pracownik.Swiadczenia)
{
Currency kwota = s.Rozliczenie.Kwota;
bool rozliczone = s.Rozliczenie.Rozliczone;
}
```
---
### KADRY-C7 — Pożyczki (KZP / ZFM) (★)
**Cel:** zarejestrować członkostwo pracownika w funduszu pożyczkowym, udzielić pożyczki, zbudować
harmonogram rat i potrącać raty z wynagrodzenia.
**Hierarchia obiektów (wszystkie `GuidedRow` root, childy pracownika):**
- `Soneta.Kadry.FundPozyczkowy` (tabela `FundPozyczkowe`) — **członkostwo** w funduszu;
`pracownik.FunduszePozyczkowe: SubTable<Soneta.Kadry.FundPozyczkowy>`. Ctor:
`new FundPozyczkowy(pracownik, definicja)`.
- `Soneta.Kadry.Pozyczka` (tabela `Pozyczki`) — **pożyczka** udzielona w ramach funduszu; kolekcja
`fundusz.Pozyczki: SubTable<Soneta.Kadry.Pozyczka>`. Ctor: `new Pozyczka(fundusz)`.
- `Soneta.Kadry.RataPozyczki` (tabela `RatyPozyczek`) — **rata** harmonogramu; `pozyczka.Raty:
SubTable<Soneta.Kadry.RataPozyczki>`. Raty pracownik widzi przez `pracownik.SplacaneRaty`
(oraz `ZyrowaneRaty` jako żyrant). Ctor: `new RataPozyczki(pozyczka)`.
- `Soneta.Kadry.DefinicjaFunduszuPozyczkowego` (słownik `DefFundPozycz`, konfiguracyjny) — zasady
funduszu (oprocentowanie, elementy płacowe wpisowego/składki/wycofania).
**Pola `Pozyczka` (kluczowe):**
| Pole | Typ | Uwaga |
|---|---|---|
| `Fundusz` | `Soneta.Kadry.FundPozyczkowy` | fundusz, w ramach którego udzielono |
| `Data` | `Soneta.Types.Date` | data udzielenia |
| `Kwota` | `Soneta.Types.Currency` | kwota pożyczki |
| `Element` | `Soneta.Place.DefinicjaElementu` | element wypłaty pożyczki |
| `ElementRaty` | `Soneta.Place.DefinicjaElementu` | element potrącenia raty |
| `IloscRat` | `int` | liczba rat |
| `KwotaRaty` | `Soneta.Types.Currency` | kwota raty |
| `SplatyOd` | `Soneta.Types.YearMonth` | miesiąc rozpoczęcia spłat |
| `Procent` | `Soneta.Types.Percent` | oprocentowanie |
| `Sposob` | `Soneta.Kadry.SposóbSpłatyOdsetek` | sposób spłaty odsetek |
| `AlgorytmRaty` | `Soneta.Kadry.AlgorytmRatyPożyczki` | algorytm wyliczania raty |
| `Raty` | `SubTable<Soneta.Kadry.RataPozyczki>` | harmonogram rat |
| `Stan` | `Soneta.Kadry.StanSpłat` | enum: `NieSpłacona = 0`, `Częściowo = 1`, `Całkowicie = 2` |
| `Splacona` | `bool` | spłacona w całości |
**Pola `RataPozyczki`:** `Pozyczka`, `Data: Date`, `Miesiąc: YearMonth`, `Kapital: Currency`,
`Odsetki: Currency`, `Element: DefinicjaElementu` (potrącenie raty), `Stan: StanSpłat`,
`Pozostaje`/`PozostajeKapitał`/`PozostajeOdsetki` (kalkulowane), `Zyrant: Pracownik`,
`Splacajacy: Pracownik`.
**Generowanie harmonogramu (workery):**
| Worker | Metoda | Params / sygnatura |
|---|---|---|
| `Soneta.Kadry.Pozyczka.UzgodnijRatyWorker` | `UzgodnijRaty` | ctor bezparam.; `Pars = new Params(ctx) { UzgodnijRaty = true }` (uwaga: `PrzeliczRaty` jest **tylko-do-odczytu**), `Pożyczka = pozyczka` — **buduje/przelicza harmonogram rat** wg `IloscRat`/`KwotaRaty`/`SplatyOd` |
| `Soneta.Kadry.Pozyczka.PożyczkaWorker` | `Pożyczka` | podsumowanie spłat (props: `Razem`, `Spłaty`, `Pozostaje`, `RazemOdsetki`…) |
| `Soneta.Kadry.Pozyczka.ElementWypłatyWorker` | `Pokaż` | podgląd elementu wypłaty pożyczki |
Metody na samym `Pozyczka`: `pozyczka.UpdatePozyczka()` (przelicz), `pozyczka.Rata(YearMonth,
DefinicjaElementu)`, `pozyczka.RatyZaMiesiąc(YearMonth)`, `pozyczka.SąRaty(YearMonth)`.
**Pułapki:**
- Ścieżka tworzenia jest **trzystopniowa**: `FundPozyczkowy(pracownik, definicja)` → `Pozyczka(fundusz)`
→ harmonogram. Pożyczki **nie da się** utworzyć bez funduszu (ctor wymaga `FundPozyczkowy`).
- Harmonogram rat generuj **workerem** `UzgodnijRatyWorker` (albo `UpdatePozyczka()`), nie ręcznym
dodawaniem `RataPozyczki` — worker rozkłada kapitał/odsetki wg algorytmu.
- `Element` (wypłaty) i `ElementRaty` (potrącenia) to **różne** definicje elementów — `ElementRaty`
realizuje potrącenie raty w wypłacie.
- **Faktyczne potrącenie raty następuje przy naliczeniu wypłaty** — `Stan`/`Splacono`/`Pozostaje`
aktualizują się po naliczeniu. Samo udzielenie pożyczki ich nie zmienia.
- `DefinicjaFunduszuPozyczkowego` to słownik konfiguracyjny — pobierz istniejący wpis, nie twórz „w locie".
**Snippet:**
```csharp
var kadry = session.GetKadry();
var pracownik = kadry.Pracownicy.WgKodu["006"];
var defFunduszu = kadry.DefFundPozycz.WgNazwy["KZP"];
var elWyplata = session.GetPlace().DefElementow.WgNazwy["Pożyczka"];
var elRata = session.GetPlace().DefElementow.WgNazwy["Spłata pożyczki"];
using (var t = session.Logout(editMode: true))
{
// 1) Członkostwo w funduszu
var fundusz = new FundPozyczkowy(pracownik, defFunduszu);
kadry.FundPozyczkowe.AddRow(fundusz);
fundusz.Okres = new FromTo(new Date(2026, 1, 1), Date.MaxValue);
// 2) Pożyczka w ramach funduszu
var pozyczka = new Pozyczka(fundusz);
kadry.Pozyczki.AddRow(pozyczka);
pozyczka.Data = new Date(2026, 1, 10);
pozyczka.Kwota = (Currency)12000m;
pozyczka.Element = elWyplata;
pozyczka.ElementRaty = elRata;
pozyczka.IloscRat = 12;
pozyczka.SplatyOd = new YearMonth(2026, 2);
// 3) Harmonogram rat (worker bezparametrowy; Params: ctor (Context); PrzeliczRaty read-only)
var context = login.CreateEmptyContext().Clone(session);
var par = new Pozyczka.UzgodnijRatyWorker.Params(context) { UzgodnijRaty = true };
new Pozyczka.UzgodnijRatyWorker { Pars = par, Pożyczka = pozyczka }.UzgodnijRaty();
t.CommitUI();
}
session.Save();
// Odczyt harmonogramu:
foreach (FundPozyczkowy f in pracownik.FunduszePozyczkowe)
foreach (Pozyczka p in f.Pozyczki)
foreach (RataPozyczki r in p.Raty)
{
YearMonth m = r.Miesiąc;
Currency kapital = r.Kapital, odsetki = r.Odsetki;
StanSpłat stan = r.Stan;
}
```
@@ -0,0 +1,887 @@
# KADRY04 — Nieobecności i czas pracy
> Wspólne fakty o typie, podstawowe typy i szablon wzorca: [../kadry.md](../kadry.md).
### KADRY-D1 — Wprowadzanie nieobecności (★)
**Cel:** zarejestrować nieobecność pracownika (urlop wypoczynkowy, zwolnienie chorobowe, urlop
bezpłatny, opieka itp.) za wskazany okres oraz odczytać nieobecności obowiązujące w danym przedziale dat.
**Fakty o typie (zweryfikowane skanem DLL):**
- **`Soneta.Kalend.Nieobecnosc` jest klasą abstrakcyjną** — tabela `Nieobecnosci` (`GuidedRow` root,
child `Pracownik`-a). `new Nieobecnosc(...)` się nie skompiluje.
- Konkretny typ do tworzenia: **`Soneta.Kalend.NieobecnośćPracownika`** (dziedziczy z `Nieobecnosc`),
z **publicznym konstruktorem `new NieobecnośćPracownika(Pracownik pracownik)`** — ctor od razu wiąże
nieobecność z pracownikiem. (Drugi konkretny typ to `KorektaNieobecności` — patrz KADRY-D2.)
- Kolekcja na pracowniku: **`pracownik.Nieobecnosci: FromToSubTable<Soneta.Kalend.Nieobecnosc>`**
(uporządkowana po okresie „oddo").
- Tabela z poziomu modułu: `session.GetKalend().Nieobecnosci`.
**Pola i typy (`Nieobecnosc` / `NieobecnośćPracownika`):**
| Pole | Typ | Rodzaj | Opis |
|---|---|---|---|
| `Definicja` | `Soneta.Kalend.DefinicjaNieobecnosci` | bazodanowe, **zapisywalne** | rodzaj nieobecności (słownik konfiguracyjny); decyduje o typie (urlop / zasiłek / bezpłatny) |
| `Okres` | `Soneta.Types.FromTo` | bazodanowe, **zapisywalne** | zakres dat nieobecności „oddo" |
| `OdGodziny`, `DoGodziny` | `Soneta.Types.Time` | — | godziny (nieobecności godzinowe) |
| `Norma`, `NormaNie` | `Soneta.Types.Time` | bazodanowe | normy czasowe |
| `IlośćDni` / `Dni` | `int` | kalkulowane/zapisywalne | liczba dni nieobecności |
| `Pracownik` | `Soneta.Kadry.Pracownik` | **tylko do odczytu** | właściciel (ustawiany ctorem, nie da się zmienić setterem) |
| `Zwolnienie` | `Soneta.Kalend.ZwolnienieZUS` (subrow) | bazodanowe | dane ZUS dla zwolnień chorobowych (`KodChoroby`, `Numer` ZLA, `PonownieUstalPodstawe`…) |
| `Urlop`, `Macierzynski`, `Wychowawczy`, `Okolicznosciowy` | subrowy | bazodanowe | szczegóły poszczególnych typów urlopów |
| `Korygowana` | `bool` | **tylko do odczytu** | czy nieobecność jest korektą (patrz KADRY-D2) |
**Dostęp do definicji nieobecności (`DefNieobecnosci`):**
- `session.GetKalend().DefNieobecnosci.WgNazwy[string]` — pobranie po nazwie, np.
`WgNazwy["Urlop wypoczynkowy"]`, `WgNazwy["Zwolnienie chorobowe"]`,
`WgNazwy["Urlop bezpłatny (art 174 kp)"]`. Nazwy muszą **dokładnie** odpowiadać słownikowi danej bazy
(w Demo nie ma wpisu „Urlop bezpłatny" — jest „Urlop bezpłatny (art 174 kp)"); `WgNazwy[...]` dla
nieistniejącej nazwy zwraca `null`.
- `session.GetKalend().DefNieobecnosci[string]` (indeksator domyślny po nazwie) — równoważne.
- `DefinicjaNieobecnosci` ma pola `Nazwa: string`, `Kod: string`, `Typ: TypNieobecnosci`.
**Wyszukiwanie po dacie/okresie:** `pracownik.Nieobecnosci.GetIntersectedRows(FromTo)` zwraca
`IList` nieobecności przecinających podany przedział.
**Snippet:**
```csharp
var kalend = session.GetKalend();
var pracownik = session.GetKadry().Pracownicy.WgKodu["006"];
// Nieobecność BEZ limitu (np. urlop bezpłatny) można wprowadzić wprost. Dla nieobecności
// LIMITOWANYCH (urlop wypoczynkowy) najpierw musi istnieć naliczony limit — patrz pułapki i KADRY-D7.
var defNieob = kalend.DefNieobecnosci.WgNazwy["Urlop bezpłatny (art 174 kp)"];
using (var t = session.Logout(editMode: true))
{
// typ konkretny; ctor wiąże nieobecność z pracownikiem
var nieobecnosc = session.AddRow(new NieobecnośćPracownika(pracownik));
nieobecnosc.Definicja = defNieob; // rodzaj nieobecności
nieobecnosc.Okres = new FromTo(new Date(2026, 7, 1), new Date(2026, 7, 5));
t.Commit();
}
session.Save();
// Odczyt nieobecności przecinających lipiec 2026:
var lipiec = new FromTo(new Date(2026, 7, 1), new Date(2026, 7, 31));
foreach (Nieobecnosc n in pracownik.Nieobecnosci.GetIntersectedRows(lipiec))
{
// n.Definicja.Nazwa, n.Okres, n.Dni
}
```
**Pułapki:**
- **Nie** rób `new Nieobecnosc(...)` — typ jest abstrakcyjny. Używaj `new NieobecnośćPracownika(pracownik)`.
- **Nieobecności limitowane wymagają istniejącego limitu.** Ustawienie `Okres` dla nieobecności
powiązanej z limitem (np. „Urlop wypoczynkowy") synchronicznie przelicza limit i rzuca
`Soneta.Kalend.DefinicjaLimitu.LimitNotFoundException`, gdy pracownik nie ma naliczonego limitu na
dany rok. Dlatego: albo najpierw nalicz limit (patrz KADRY-D7), albo użyj nieobecności bez limitu
(np. „Urlop bezpłatny (art 174 kp)") — jak w snippetcie powyżej.
- `Definicja` jest **wymagana** — bez niej nieobecność nie zostanie poprawnie naliczona/zapisana.
Pobieraj istniejący wpis słownika przez `DefNieobecnosci.WgNazwy[...]`, nie twórz „w locie".
- `Pracownik` jest **tylko do odczytu** — relację ustawia konstruktor, nie da się jej później zmienić.
- Tabela `Nieobecnosci` jest **operacyjna guided** — przy przeglądaniu poprzecznym (po wszystkich
pracownikach) filtruj zakresem czasowym (safe-code §6.3). W zakresie jednego pracownika korzystaj
z `pracownik.Nieobecnosci` i `GetIntersectedRows`.
- Nakładające się nieobecności i niepoprawne okresy wychwytują weryfikatory przy `Save()`
(`RowException`) — obsłuż wyjątek.
- Pełna transakcja w `session.Logout(editMode: true)`; brak `Commit()` = rollback przy `Dispose()`.
---
### KADRY-D2 — Korygowanie nieobecności już wypłaconych (★)
**Cel:** poprawić nieobecność, która została już rozliczona w wypłacie — zmienić jej okres lub typ
(definicję) i/lub wymusić ponowne ustalenie podstawy naliczania zasiłku. enova rozróżnia dwie ścieżki:
(a) **modyfikacja istniejącej nieobecności** + ponowne ustalenie podstawy, (b) **korekta** jako odrębny
rekord typu `KorektaNieobecności`.
**Fakty o typie (zweryfikowane skanem DLL):**
- Pola `Definicja: DefinicjaNieobecnosci` i `Okres: FromTo` na `Nieobecnosc`**zapisywalne**
publicznie — można je zmienić na istniejącym rekordzie.
- `Nieobecnosc.Korygowana: bool` i `Nieobecnosc.Pracownik`**tylko do odczytu**.
- Subrow `Zwolnienie: Soneta.Kalend.ZwolnienieZUS` posiada flagę `PonownieUstalPodstawe: bool`
oraz **publiczną metodę `SetPonownieUstalPodstawe(bool)`** — to ona steruje przeliczeniem podstawy
zasiłku przy kolejnym naliczeniu wypłaty.
- Worker (czynność menu, `DataType = Nieobecnosc`): klasa
**`Soneta.Kalend.Nieobecnosc.UstalPonowniePodstawęNaliczaniaWorker`** — czynność
„Ustal ponownie podstawę naliczania". Worker:
- ma publiczny bezparametrowy ctor;
- przyjmuje kontekst przez settowalną property `[Context] public Params Nieobecność`;
- klasa `…Worker.Params : ContextBase` ma **publiczny ctor `Params(Context context)`**, który czyta
nieobecność z `context[typeof(Nieobecnosc)]`, oraz settowalną property `UstalPodstawę: bool`;
- metoda `public void PonownieUstalPodstawę()` jest jego akcją;
- `static bool IsEnabledPonownieUstalPodstawę(Nieobecnosc)` mówi, kiedy czynność jest aktywna
(dotyczy zwolnień ZUS i urlopów macierzyńskich: `Zwolnienie.IsZUS || Macierzynski.IsMacierzyński`,
przy braku `BlokadaOkresu`).
- Drugi konkretny typ nieobecności: **`Soneta.Kalend.KorektaNieobecności`** (dziedziczy `Nieobecnosc`),
z **publicznym ctor `new KorektaNieobecności(NieobecnośćPracownika nieobecność)`** — tworzy rekord
korygujący wskazaną nieobecność. Ma zapisywalne `Definicja`, `Okres`, `IlośćDni`,
`RozliczenieWDniu`, `RozliczenieData`, a kolekcje `ElementyKorygowane`/`ElementyKorygowaneStorno`
są tylko do odczytu (wyliczane).
**Wariant A — zmiana okresu/typu + ponowne ustalenie podstawy (modyfikacja istniejącego rekordu):**
```csharp
var pracownik = session.GetKadry().Pracownicy.WgKodu["006"];
var okresStary = new FromTo(new Date(2026, 3, 2), new Date(2026, 3, 10));
// odszukaj istniejącą (już rozliczoną) nieobecność po przecięciu z okresem
var nieobecnosc = (Nieobecnosc)pracownik.Nieobecnosci.GetIntersectedRows(okresStary)[0];
using (var t = session.Logout(editMode: true))
{
nieobecnosc.Okres = new FromTo(new Date(2026, 3, 2), new Date(2026, 3, 12)); // wydłużenie okresu
// dla zwolnień ZUS — wymuś ponowne ustalenie podstawy przy najbliższym naliczeniu wypłaty:
nieobecnosc.Zwolnienie.SetPonownieUstalPodstawe(true);
t.Commit();
}
session.Save();
```
**Wariant B — czynność „Ustal ponownie podstawę naliczania" przez worker (kontekst):**
```csharp
var worker = new Nieobecnosc.UstalPonowniePodstawęNaliczaniaWorker();
var ctx = Context.Empty.Clone(session);
ctx[typeof(Nieobecnosc)] = nieobecnosc; // worker czyta nieobecność z kontekstu
worker.Nieobecność = new Nieobecnosc.UstalPonowniePodstawęNaliczaniaWorker.Params(ctx)
{
UstalPodstawę = true
};
worker.PonownieUstalPodstawę(); // wykonuje własną transakcję + Commit
session.Save();
```
**Wariant C — odrębny rekord korekty (`KorektaNieobecności`):**
```csharp
using (var t = session.Logout(editMode: true))
{
var nPrac = (NieobecnośćPracownika)nieobecnosc; // korekta dotyczy NieobecnośćPracownika
var korekta = session.AddRow(new KorektaNieobecności(nPrac));
korekta.Definicja = nPrac.Definicja;
// Okres korekty MUSI być podzbiorem okresu korygowanej nieobecności (tu: 2..10):
korekta.Okres = new FromTo(new Date(2026, 3, 3), new Date(2026, 3, 8));
t.Commit();
}
session.Save();
// Po zapisie korygowana nieobecność ma flagę Korygowana == true.
```
**Pułapki:**
- **Faktyczne** przeliczenie wartości zasiłku NIE następuje w momencie ustawienia flagi/wywołania
workera — flaga `PonownieUstalPodstawe` jest odczytywana dopiero przy **ponownym naliczeniu wypłaty**
(mechanizm `PodstawaZasilku`). Sam test korekty rekordu nieobecności (Demo, rollback) zweryfikuje
zmianę `Okres`/`Definicja`/flagi, ale **nie zweryfikuje przeliczonych kwot wypłaty** bez pełnego
scenariusza naliczenia listy płac (patrz sekcja „funkcjonalności niewykonalne").
- `IsEnabledPonownieUstalPodstawę` ogranicza czynność do zwolnień ZUS / macierzyńskich — dla zwykłego
urlopu wypoczynkowego worker nie ma zastosowania; tam korektę robisz przez zmianę `Okres`/`Definicja`
albo rekord `KorektaNieobecności`.
- **Okres korekty (`KorektaNieobecności.Okres`) musi być podzbiorem okresu korygowanej nieobecności** —
wyjście poza ten zakres rzuca `Nieobecnosc.KorygowanyOkresException`.
- Dla nieobecności bez skutków płacowych (np. urlop bezpłatny) `KorektaNieobecności` **nie pojawia się
jako osobny wiersz** w `pracownik.Nieobecnosci` — obserwowalnym efektem jest flaga `Korygowana == true`
na nieobecności pierwotnej.
- Korekta zmienia dane operacyjne powiązane z wypłatą — trzymaj transakcję krótką i obsłuż
`RowConflictException` / `RowException` z `Save()` (safe-code §4, §13.1).
- Worker wykonuje własną transakcję (`Session.Logout(true)` + `Commit`) — nie zagnieżdżaj go w innej
otwartej transakcji edycyjnej.
---
### KADRY-D7 — Analiza limitów urlopowych (★)
**Cel:** odczytać limit nieobecności (np. urlop wypoczynkowy) pracownika za dany rok — ile przysługuje,
ile wykorzystano, ile pozostało. Limity **nie są tworzone ręcznie** — powstają przez naliczanie.
**Fakty o typie (zweryfikowane skanem DLL):**
- **`Soneta.Kalend.LimitNieobecnosci`** — tabela `LimNieobecnosci`, `GuidedRow` **child** pracownika
(relacja przez pole `Pracownik`). Instancje powstają wyłącznie przez naliczanie — **nie twórz ich
konstruktorem**.
- Kolekcja na pracowniku: **`pracownik.Limity: SubTable<Soneta.Kalend.LimitNieobecnosci>`**
(nazwa kolekcji to `Limity`, nie „LimityNieobecnosci").
- Tabela z poziomu modułu: `session.GetKalend().LimNieobecnosci`.
**Pola i typy (`LimitNieobecnosci`) — odczyt:**
| Pole | Typ | Rodzaj | Opis |
|---|---|---|---|
| `Definicja` | `Soneta.Kalend.DefinicjaLimitu` | bazodanowe | rodzaj limitu (urlop wypoczynkowy itd.) |
| `Okres` | `Soneta.Types.FromTo` | bazodanowe | okres limitu (zwykle rok) |
| `OkresWażności` | `Soneta.Types.FromTo` | kalkulowane | okres ważności limitu |
| `Limit` | `int` | bazodanowe | limit (dni) wynikający z kodeksu pracy |
| `LimitDni` | `int` | kalkulowane | limit w dniach |
| `LimitGodz` | `Soneta.Types.Time` | bazodanowe | limit w godzinach |
| `Razem` / `RazemGodz` | `int` / `Time` | kalkulowane | łączny przysługujący (limit + przeniesienia + zmiany) |
| `Wykorzystane` / `WykorzystaneGodz` | `int` / `Time` | bazodanowe | wykorzystane dni/godziny |
| `Pozostalo` | `int` | kalkulowane | pozostało (dni, int) |
| `PozostaloDni` | `double` | kalkulowane | pozostało dni (z częścią ułamkową) |
| `PozostaloGodz` | `Soneta.Types.Time` | kalkulowane | pozostało godzin |
| `ZaleglyDni` / `ZaleglyGodz` | `double` / `Time` | kalkulowane | zaległy z poprzednich okresów |
| `Przeniesienie` / `PrzeniesienieDni` | `int` / `double` | kalkulowane | przeniesione z poprzedniego roku |
| `Korekta`, `Zmiana` | `int` | bazodanowe | korekty/zmiany limitu |
| `Pracownik` | `Soneta.Kadry.Pracownik` | bazodanowe (guided-parent), **read-only** | właściciel |
> **Wykorzystany = `Razem - Pozostalo`** (lub bezpośrednio pole `Wykorzystane`). „Przysługujący" to
> `Razem` (limit kodeksowy + przeniesienia + zmiany), a nie samo `Limit`.
**Dostęp do definicji limitów (`DefinicjeLimitow`):**
- `session.GetKalend().DefinicjeLimitow.WgNazwy[string]` — np. `WgNazwy["Urlop wypoczynkowy"]`.
- Skróty typowane (property zwracające `DefinicjaLimitu`): `DefinicjeLimitow.UrlopWypoczynkowy`,
`.UrlopDodatkowy`, `.OpiekaNadZdrowym`, `.UrlopOpiekunczy`, `.ZwolnienieOdPracySilaWyzsza` itd.
- `DefinicjaLimitu` ma pola `Nazwa: string`, `Typ: TypLimitu`.
**Naliczanie limitu (by mógł istnieć do odczytu) — `Soneta.Kalend.NaliczanieLimitow`:**
- Klasa z **publicznym bezparametrowym ctor**; settowalne property:
- `Pars: NaliczanieLimitow.Params` (set),
- `Pracownicy: ICollection<Pracownik>` (set) **albo** `PracownicyIdx: Pracownik[]` (set).
- Klasa `NaliczanieLimitow.Params : ContextBase` ma **publiczny ctor `Params(Context context)`**
oraz settowalne: `Definicja: DefinicjaLimitu`, `Okres: FromTo`, `KopiujKorekty: bool`,
`ZapisPerPracownik: bool`.
- Metoda **`public void DodajLimit()`** — nalicza limit (zapisuje rekordy `LimitNieobecnosci`).
(Jest też `DodajLimitUrlopowy()`.)
**Snippet — naliczenie + odczyt:**
```csharp
var kalend = session.GetKalend();
var pracownik = session.GetKadry().Pracownicy.WgKodu["006"];
var defUrlop = kalend.DefinicjeLimitow.WgNazwy["Urlop wypoczynkowy"]; // lub DefinicjeLimitow.UrlopWypoczynkowy
var rok = FromTo.Year(new Date(2026, 1, 1));
using (var t = session.Logout(editMode: true))
{
var naliczanie = new NaliczanieLimitow
{
Pars = new NaliczanieLimitow.Params(Context.Empty.Clone(session))
{
Definicja = defUrlop,
Okres = rok,
KopiujKorekty = true
},
Pracownicy = new Pracownik[] { pracownik }
};
naliczanie.DodajLimit(); // tworzy/aktualizuje LimitNieobecnosci
t.Commit();
}
session.Save();
// Odczyt limitu urlopu wypoczynkowego za rok 2026.
// UWAGA: filtr serwerowy obejmuje TYLKO pola bazodanowe i prostych porównań — Okres (FromTo)
// NIE da się porównać serwerowo (==), więc filtrujemy serwerowo po Definicja, a rok w pamięci:
var lim = pracownik.Limity[(LimitNieobecnosci l) => l.Definicja == defUrlop]
.Cast<LimitNieobecnosci>()
.FirstOrDefault(l => l.Okres.From == rok.From);
if (lim != null)
{
int przysluguje = lim.Razem; // przysługujący (limit + przeniesienia + zmiany)
int pozostalo = lim.Pozostalo; // pozostało
int wykorzystany = przysluguje - pozostalo; // == lim.Wykorzystane
// lim.PozostaloDni, lim.PozostaloGodz, lim.ZaleglyDni
}
```
**Pułapki:**
- **Nie** twórz `new LimitNieobecnosci(...)` — limit powstaje przez naliczanie (`DodajLimit`). W bazie
Demo limit dla danego roku może jeszcze nie istnieć — w teście trzeba go **najpierw naliczyć**.
- Kolekcja na pracowniku to `pracownik.Limity` (nie `LimityNieobecnosci`).
- **Nie porównuj `Okres` (FromTo) w filtrze serwerowym** — `l.Okres == rok` rzuca `ArgumentException`
(„pole nieznalezione"). Filtruj serwerowo po `Definicja`, a okres/rok porównaj w pamięci
(`.FirstOrDefault(l => l.Okres.From == rok.From)`).
- `Razem` może wynosić `0` dla pracowników bez danych napędzających wymiar urlopu (staż, data
urodzenia) — asercje opieraj na spójności (`Wykorzystane == Razem - Pozostalo`, `Razem >= 0`),
a nie na założeniu `Razem > 0`.
- `Pracownik` na limicie jest read-only (relacja guided) — naliczanie samo wiąże rekord z pracownikiem.
- Filtruj limity serwerowo po `Definicja` i `Okres` (`pracownik.Limity[condition]`), nie iteruj całości
z `if` w pamięci (safe-code §6.1). Tabela `LimNieobecnosci` jest operacyjna guided.
- `Context.Empty.Clone(session)` daje kontekst związany z bieżącą sesją — wymagany przez ctor
`NaliczanieLimitow.Params(Context)`.
- Naliczanie modyfikuje dane operacyjne — w transakcji edycyjnej, krótko, z obsługą wyjątków z `Save()`.
### KADRY-D3 — Import e-ZLA z PUE ZUS (zwolnienia lekarskie)
**Cel:** zaewidencjonować w systemie zwolnienie lekarskie pobrane z PUE ZUS (e-ZLA). Sam **import to
operacja sieciowa** (komunikacja z PUE ZUS) — w kodzie biznesowym/teście dokumentujemy **model danych**
nieobecności chorobowej i jej dane ZUS, a nie samo połączenie z bramką PUE.
**Fakty o typie (zweryfikowane skanem DLL):**
- Zwolnienie chorobowe to `Soneta.Kalend.NieobecnośćPracownika` (typ konkretny z KADRY-D1) z `Definicja`
wskazującą na rodzaj zasiłkowy (np. „Zwolnienie chorobowe").
- Dane ZUS zwolnienia leżą w subrowie **`Nieobecnosc.Zwolnienie: Soneta.Kalend.ZwolnienieZUS`**
(bazodanowy subrow na rekordzie nieobecności).
- Dane samego dokumentu ZLA leżą w subrowie **`Nieobecnosc.ZLA: Soneta.Kalend.ZLA`**
(`ZLA.Data: Date`, `ZLA.Wersja: WersjaZLA`, `ZLA.Zrodlo: MemoText`).
**Pola i typy (`Nieobecnosc.Zwolnienie: ZwolnienieZUS`) — zapisywalne, bazodanowe:**
| Pole | Typ | Opis |
|---|---|---|
| `Numer` | `string` | numer dokumentu ZLA (pole tekstowe — **maks. 9 znaków**) |
| `KodChoroby` | `string` | kod literowy choroby (A, B, C, D, …) |
| `Przyczyna` | `Soneta.Kalend.PrzyczynaZwolnienia` | przyczyna niezdolności do pracy |
| `Kwarantanna` | `Soneta.Kalend.ZwolnienieKwarantanna` | kwarantanna/izolacja |
| `LeczenieSzpitalne` | `bool` | pobyt w szpitalu |
| `ZwolnienieWystawione` | `Soneta.Types.Date` | data wystawienia ZLA |
| `ZwolnienieDostarczone` | `Soneta.Types.Date` | data dostarczenia |
| `PomniejszajZasilek` | `bool` | obniżenie zasiłku |
| `PonownieUstalPodstawe` | `bool` | wymuszenie przeliczenia podstawy (patrz KADRY-D2/KADRY-D6) |
**Pola i typy (`Nieobecnosc.ZLA: ZLA`):** `Data: Date`, `Wersja: WersjaZLA`, `Zrodlo: MemoText`.
**Snippet — ręczne odwzorowanie e-ZLA jako nieobecności chorobowej (bez sieci):**
```csharp
var kalend = session.GetKalend();
var pracownik = session.GetKadry().Pracownicy.WgKodu["006"];
var defChor = kalend.DefNieobecnosci.WgNazwy["Zwolnienie chorobowe"];
using (var t = session.Logout(editMode: true))
{
var nieob = session.AddRow(new NieobecnośćPracownika(pracownik));
nieob.Definicja = defChor;
nieob.Okres = new FromTo(new Date(2026, 5, 4), new Date(2026, 5, 10));
// dane ZUS z e-ZLA (subrow Zwolnienie):
nieob.Zwolnienie.Numer = "ZLA000001"; // pole Numer ma limit 9 znaków
nieob.Zwolnienie.KodChoroby = "A";
t.Commit();
}
session.Save();
```
**Pułapki:**
- **Sam import e-ZLA z PUE wymaga sieci** (uwierzytelnienie + bramka ZUS) — nie da się go odtworzyć
w teście jednostkowym na bazie Demo; testuj wyłącznie **odwzorowanie modelu danych** (subrow `Zwolnienie`).
- `Zwolnienie` i `ZLA` to subrowy — nie tworzysz ich osobno, są częścią rekordu `Nieobecnosc`; ustawiasz
ich pola po utworzeniu nieobecności.
- Definicja zasiłkowa musi istnieć w słowniku bazy (`DefNieobecnosci.WgNazwy[...]``null`).
- **Faktyczne kwoty zasiłku** liczą się dopiero przy naliczeniu wypłaty — patrz uwaga przy KADRY-D2.
---
### KADRY-D4 — Generowanie deklaracji Z-3 / Z-3a dla nieobecności chorobowej
**Cel:** wygenerować zaświadczenie płatnika składek **Z-3** (pracownik etatowy) lub **Z-3a** (umowy/inni
ubezpieczeni) dla konkretnej nieobecności zasiłkowej.
**Fakty o typie (zweryfikowane skanem DLL):**
- Worker (czynność na `Nieobecnosc`): **`Soneta.Deklaracje.ZUS.ZUSZ3.Z3Worker`** — akcja
„Generuj deklarację Z-3", metoda `public object UtworzDeklaracjeZ3()`.
- Analogicznie **`Soneta.Deklaracje.ZUS.ZUSZ3.Z3aWorker`** — akcja „Generuj deklarację Z-3a",
metoda `public object UtworzDeklaracjeZ3a()`.
- Oba workery przyjmują przez `[Context]`:
- `KeduContext: DeklaracjaZUS.PUEContext` (property `Kedu: KEDU`),
- `Z3ParamContext: Z3ParamContext` / `Z3aParamContext` z polami m.in.: `Nieobecnosc: INieobecnoscLubZbieg`,
`NieobecnoscZContextu: bool`, `Pracownik: Pracownik`, `PracownikZContextu: bool`, `Okres: FromTo`,
`OkresZasiłkowy: FromTo`, `OkresZasilkowyOd: Date`, `Współczynnik: Fraction`, `RachBank: string`,
`KontynuacjaŚwiadczenia: bool`.
**Snippet — generowanie Z-3 dla nieobecności (kontekst):**
```csharp
var worker = new Soneta.Deklaracje.ZUS.ZUSZ3.Z3Worker();
var ctx = Context.Empty.Clone(session);
ctx[typeof(Nieobecnosc)] = nieobChorobowa; // worker czyta nieobecność z kontekstu
var deklaracja = worker.UtworzDeklaracjeZ3(); // zwraca obiekt deklaracji Z-3
session.Save();
```
**Pułapki:**
- **Sensowny Z-3 wymaga naliczonej wypłaty/podstawy zasiłku** — bez naliczonej podstawy deklaracja
powstanie z pustymi/zerowymi kwotami. W teście na czystej Demo zweryfikujesz fakt powstania obiektu
i ustawienie pól nagłówkowych (pracownik, okres), ale **nie kwoty zasiłku**.
- Worker przyjmuje dane przez `Context` (`ctx[typeof(Nieobecnosc)]`/`ctx[typeof(Pracownik)]`) — nie ma
prostego ctora parametrowego; zegnij pod swój scenariusz `Z3ParamContext`.
- Z-3 dotyczy etatu, Z-3a umów/innych ubezpieczonych — dobierz worker do tytułu ubezpieczenia.
- Metody zwracają `object` (deklaracja KEDU) — zachowaj/odczytaj wynik, nie zakładaj typu wprost.
---
### KADRY-D5 — Obsługa przestoju (dodanie/usunięcie, przestój ekonomiczny — % wynagrodzenia)
**Cel:** zaewidencjonować przestój pracownika (np. ekonomiczny) za okres oraz wskazać procent
wynagrodzenia przestojowego; usunąć przestój nakładający się na nieobecność ZUS.
**Fakty o typie (zweryfikowane skanem DLL):**
- **Dodanie przestoju:** worker **`Soneta.Kadry.DodajPrzestojWorker`** (czynność „Przestój/Dodaj przestój",
metoda `public void DodajPrzestoj()`):
- settowalne property: `Pracownicy: Pracownik[]`, `Pars: DodajPrzestojWorker.Params`;
- `Params` z polami: `DefinicjaStrefy: Soneta.Kalend.DefinicjaStrefy`, `Okres: FromTo`.
- **Procent wynagrodzenia przestojowego (przestój ekonomiczny):** worker
**`Soneta.Kadry.IndywidualnyProcentWynagrPrzestojowegoWorker`** (czynność
„Przestój/Przestój ekonomiczny - procent wynagr.", metoda `public void Aktualizuj()`):
- `Pracownicy: Pracownik[]`, `Pars.Data: Date`, `Pars.Procent: Soneta.Types.Percent`.
- **Usunięcie przestoju podczas nieobecności ZUS:** worker
**`Soneta.Kadry.UsunPrzestojNieobecnoscWorker`** (czynność „Przestój/Usuń przestój podczas
nieobecności ZUS", metoda `public void UsunPrzestoj()`): `Pracownicy: Pracownik[]`, `Pars.Okres: FromTo`.
- Procent wynagrodzenia przestojowego jest też trzymany na etacie:
`PracHistoria.Etat.Postojowe: Soneta.Kadry.WynagrodzeniePostojowe` (`Procent: Percent`, `Standardowe: bool`).
- `DefinicjaStrefy` (`session.GetKalend().DefinicjeStref`) — słownik konfiguracyjny stref (m.in. przestoju).
**Snippet — dodanie przestoju:**
```csharp
var kalend = session.GetKalend();
var pracownik = session.GetKadry().Pracownicy.WgKodu["006"];
var defStrefa = kalend.DefinicjeStref.WgNazwy["Przestój"]; // nazwa wg słownika danej bazy
var worker = new Soneta.Kadry.DodajPrzestojWorker
{
Pracownicy = new[] { pracownik },
Pars = new Soneta.Kadry.DodajPrzestojWorker.Params(Context.Empty.Clone(session))
{
DefinicjaStrefy = defStrefa,
Okres = new FromTo(new Date(2026, 6, 1), new Date(2026, 6, 5))
}
};
worker.DodajPrzestoj(); // worker wykonuje własną transakcję
session.Save();
```
**Snippet — przestój ekonomiczny (procent):**
```csharp
var worker = new Soneta.Kadry.IndywidualnyProcentWynagrPrzestojowegoWorker
{
Pracownicy = new[] { pracownik },
Pars = new Soneta.Kadry.IndywidualnyProcentWynagrPrzestojowegoWorker.Params(Context.Empty.Clone(session))
{
Data = new Date(2026, 6, 1),
Procent = new Percent(0.5m) // 50% wynagrodzenia
}
};
worker.Aktualizuj();
session.Save();
```
**Pułapki:**
- `DefinicjeStref.WgNazwy[...]` zależy od słownika danej bazy — zweryfikuj nazwę przestoju w Demo
(może być inna niż „Przestój"); dla nieistniejącej nazwy zwraca `null`.
- Worker wykonuje własną transakcję — nie zagnieżdżaj go w otwartej transakcji edycyjnej.
- `Percent` przyjmuj jako ułamek (`0.5m` = 50%), nie liczbę 50.
- `UsunPrzestojNieobecnoscWorker` usuwa przestój **kolidujący z nieobecnością ZUS** — to nie generyczne
„usuń przestój"; zakres działania ogranicza okres + obecność nieobecności ZUS.
- Skutki płacowe (wynagrodzenie przestojowe) liczą się dopiero przy naliczeniu wypłaty.
---
### KADRY-D6 — Ustalanie/zmiana parametrów okresu zasiłkowego
**Cel:** zmienić parametry okresu zasiłkowego nieobecności chorobowej — kontynuację/przedłużenie okresu
zasiłkowego oraz wymusić ponowne ustalenie podstawy naliczania zasiłku.
**Fakty o typie (zweryfikowane skanem DLL):**
- Parametry okresu zasiłkowego są w subrowie **`Nieobecnosc.Zwolnienie: ZwolnienieZUS`** (bazodanowe,
zapisywalne):
- `KontynuacjaOkrZas: Soneta.Kalend.KontynuacjaOkrZas` (enum: `Warunkowo`, `Tak`, `Nie`),
- `PrzedluzenieOkrZas: bool`, `PrzedluzeniaData: Soneta.Types.Date`,
- `PonownieUstalPodstawe: bool` + metoda `SetPonownieUstalPodstawe(bool)` (patrz KADRY-D2).
- Worker korekty okresu zasiłkowego: **`Soneta.Kalend.Nieobecnosc.KorektaOkresuZasiłkowegoWorker`**
(czynność „Zmień pozostałe parametry okresu zasiłkowego", metoda `public void PonownieUstalPodstawę()`):
- settowalne `Pars: KorektaOkresuZasiłkowegoWorker.Params` z polami:
`KontynuacjaOkrZas: KontynuacjaOkrZas`, `PrzedluzenieOkrZas: bool`, `PrzedluzeniaData: Date`.
- BO okresu zasiłkowego (przy wdrożeniu) — patrz KADRY-D10: `PracHistoria.ChorobowyBO`
(`DniZasilkowe`, `ZasilekOdDnia`, `PrzedluzenieOZ`).
**Snippet — zmiana parametrów wprost na rekordzie:**
```csharp
using (var t = session.Logout(editMode: true))
{
nieobChorobowa.Zwolnienie.KontynuacjaOkrZas = KontynuacjaOkrZas.Tak;
nieobChorobowa.Zwolnienie.PrzedluzenieOkrZas = true;
nieobChorobowa.Zwolnienie.PrzedluzeniaData = new Date(2026, 5, 31);
nieobChorobowa.Zwolnienie.SetPonownieUstalPodstawe(true);
t.Commit();
}
session.Save();
```
**Snippet — przez worker korekty okresu zasiłkowego:**
```csharp
var worker = new Nieobecnosc.KorektaOkresuZasiłkowegoWorker();
var ctx = Context.Empty.Clone(session);
ctx[typeof(Nieobecnosc)] = nieobChorobowa;
worker.Pars = new Nieobecnosc.KorektaOkresuZasiłkowegoWorker.Params(ctx)
{
KontynuacjaOkrZas = KontynuacjaOkrZas.Tak,
PrzedluzenieOkrZas = true,
PrzedluzeniaData = new Date(2026, 5, 31)
};
worker.PonownieUstalPodstawę(); // własna transakcja + Commit
session.Save();
```
**Pułapki:**
- **Faktyczne** przeliczenie kwot zasiłku następuje dopiero przy **ponownym naliczeniu wypłaty** — test
na Demo zweryfikuje zmianę pól `KontynuacjaOkrZas`/`PrzedluzenieOkrZas`/`PrzedluzeniaData`/flagi,
ale nie kwoty.
- Parametry okresu zasiłkowego mają sens tylko dla nieobecności **ZUS** (zwolnienia chorobowe/zasiłki) —
dla urlopu wypoczynkowego są bez znaczenia.
- Worker wykonuje własną transakcję — nie zagnieżdżaj go w innej otwartej transakcji.
---
### KADRY-D8 — Naliczanie i przeliczanie limitów nieobecności
**Cel:** naliczyć limit nieobecności (jak KADRY-D7 — `NaliczanieLimitow.DodajLimit()`) oraz przeliczyć liczbę
wykorzystanych dni limitu (czynność „Przelicz wykorzystane").
**Fakty o typie (zweryfikowane skanem DLL):**
- **Naliczenie limitu:** klasa **`Soneta.Kalend.NaliczanieLimitow`** — publiczny bezparametrowy ctor;
settowalne `Pars: NaliczanieLimitow.Params` (`Definicja: DefinicjaLimitu`, `Okres: FromTo`,
`KopiujKorekty: bool`, `ZapisPerPracownik: bool`) oraz `Pracownicy: ICollection<Pracownik>` /
`PracownicyIdx: Pracownik[]`; metoda `public void DodajLimit()` (i `DodajLimitUrlopowy()`).
Wariant UI per-pracownik: worker **`Soneta.Kalend.UI.PracownikLimityNaliczanieWorker`**
(czynność „Nalicz limit nieobecności", metoda `DodajLimit()`) — `Pracownik: Pracownik`,
`Pars` jak wyżej.
- **Przeliczenie wykorzystanych:** worker
**`Soneta.Kalend.LimitNieobecnosci.Pracownicy.PrzeliczWykorzystaneWorker`** (czynność
„Limity nieobecności/Przelicz wykorzystane", metoda `public void PrzeliczWykorzystane()`):
- settowalne `Pracownicy: Pracownik[]`, `Pars.Definicja: DefinicjaLimitu`, `Pars.Okres: FromTo`.
**Snippet — naliczenie + przeliczenie wykorzystanych:**
```csharp
var kalend = session.GetKalend();
var pracownik = session.GetKadry().Pracownicy.WgKodu["006"];
var defUrlop = kalend.DefinicjeLimitow.WgNazwy["Urlop wypoczynkowy"];
var rok = FromTo.Year(new Date(2026, 1, 1));
// 1) naliczenie limitu (jak KADRY-D7)
var naliczanie = new NaliczanieLimitow
{
Pars = new NaliczanieLimitow.Params(Context.Empty.Clone(session))
{
Definicja = defUrlop,
Okres = rok,
KopiujKorekty = true
},
Pracownicy = new[] { pracownik }
};
naliczanie.DodajLimit();
session.Save();
// 2) przeliczenie wykorzystanych
var przelicz = new LimitNieobecnosci.Pracownicy.PrzeliczWykorzystaneWorker
{
Pracownicy = new[] { pracownik },
Pars = new LimitNieobecnosci.Pracownicy.PrzeliczWykorzystaneWorker.Params(Context.Empty.Clone(session))
{
Definicja = defUrlop,
Okres = rok
}
};
przelicz.PrzeliczWykorzystane();
session.Save();
```
**Pułapki:**
- **Nie** twórz `new LimitNieobecnosci(...)` — limit powstaje przez naliczanie (jak w KADRY-D7).
- `PrzeliczWykorzystane` aktualizuje pole `LimitNieobecnosci.Wykorzystane` na podstawie wprowadzonych
nieobecności — ma sens dopiero **po** naliczeniu limitu i wprowadzeniu nieobecności limitowanych.
- `Razem` może wynosić `0` dla pracownika bez danych napędzających wymiar — opieraj asercje na spójności
(`Wykorzystane == Razem - Pozostalo`), nie na `Razem > 0` (patrz KADRY-D7).
- Workery wykonują własne transakcje — wywołuj poza otwartą transakcją edycyjną; obsłuż wyjątki z `Save()`.
---
### KADRY-D9 — Aktualizacja podstaw nieobecności ZUS / podstaw urlopu
**Cel:** odczytać/wprowadzić ręcznie podstawy naliczania zasiłków (chorobowe/macierzyńskie/opiekuńcze/
rehabilitacyjne) używane przy nieobecnościach ZUS — np. przy wdrożeniu lub korekcie podstawy.
**Fakty o typie (zweryfikowane skanem DLL):**
- Kolekcja na pracowniku: **`pracownik.PodstawyNieobecności: SubTable<Soneta.Place.PodstawaNieobecnosci>`**
(jest też `PodstawyNieobecnościOkresowe: SubTable<PodstawaNieobecnosciOkresowa>`).
- **`Soneta.Place.PodstawaNieobecnosci`** — tabela `PodstawyNieobec`, `GuidedRow` **child** pracownika
(relacja przez pole `Pracownik`).
- **Brak publicznego ctora** — `PodstawaNieobecnosci` ma jedynie ctory niepubliczne
(`(RowCreator)`, `(Pracownik, TypyPodstawNieobecnosci)`). Rekordy powstają z **naliczenia wypłaty**;
w kodzie biznesowym/teście realnie testowalny jest **odczyt** (dodawanie ręczne — patrz pułapki/spec).
**Pola i typy (`PodstawaNieobecnosci`) — bazodanowe, zapisywalne:**
| Pole | Typ | Opis |
|---|---|---|
| `Data` | `Soneta.Types.Date` | data podstawy |
| `Miesieczne` | `decimal` | podstawa miesięczna |
| `Kwartalne` / `Roczne` | `decimal` | składowe |
| `Podstawa` | `decimal` | podstawa naliczania chorobowego |
| `PodstawaM` / `PodstawaO` / `PodstawaR` | `decimal` | podstawa macierzyńskiego / opiekuńczego / rehabilitacyjnego |
| `Typ` | `Soneta.Place.TypyPodstawNieobecnosci` | `Chorobowa` / `Wypoczynkowy` |
| `Norma` / `NormaDni` | `Time` / `int` | norma czasu/dni |
| `Praca` / `PracaDni` | `Time` / `int` | przepracowane |
| `ProcentSkladki` | `Soneta.Types.Percent` | procent składki |
> **Podstawy urlopu wypoczynkowego** rozróżnia pole `Typ = TypyPodstawNieobecnosci.Wypoczynkowy`;
> podstawy zasiłków ZUS → `Typ = Chorobowa`.
**Snippet — odczyt podstaw + dodanie podstawy ręcznej:**
```csharp
var pracownik = session.GetKadry().Pracownicy.WgKodu["006"];
// Odczyt podstaw chorobowych (filtr serwerowy po Typ):
foreach (PodstawaNieobecnosci p in
pracownik.PodstawyNieobecności[(PodstawaNieobecnosci x) => x.Typ == TypyPodstawNieobecnosci.Chorobowa])
{
// p.Data, p.Podstawa, p.Miesieczne
}
// UWAGA: PodstawaNieobecnosci NIE ma publicznego ctora — normalnie powstaje z naliczenia wypłaty.
// Ręczne dodanie wymagałoby niepublicznego API → w teście testuj wyłącznie ODCZYT (powyżej).
```
**Pułapki:**
- Kwoty (`Miesieczne`, `Podstawa`, …) są typu `decimal` — to dane operacyjne podstaw; **normalnie
podstawy powstają z naliczenia wypłaty** (brak publicznego ctora — patrz wyżej).
- `Pracownik` na podstawie jest read-only (guided-parent).
- Filtruj serwerowo po `Typ` (`PodstawyNieobecności[condition]`) — nie iteruj całości z `if` w pamięci.
- W teście na czystej Demo kolekcja `PodstawyNieobecności` może być pusta, dopóki nie naliczono wypłaty
z zasiłkiem — testuj odczyt asercją na model/spójność, a scenariusz „dodaj ręcznie" oznacz `[Ignore]`.
---
### KADRY-D10 — Bilans otwarcia nieobecności i urlopów
**Cel:** wprowadzić bilans otwarcia (BO) przy wdrożeniu / starcie roku — historię chorobową (okres
zasiłkowy, dni wykorzystane) oraz urlop wykorzystany u poprzednich pracodawców / w pierwszym miesiącu.
**Fakty o typie (zweryfikowane skanem DLL):**
- BO leży na rekordzie historycznym **`Soneta.Kadry.PracHistoria`** w dwóch subrowach (bazodanowe,
zapisywalne):
- **`PracHistoria.ChorobowyBO: Soneta.Kadry.ChorobowyBO`** (BO chorobowy / okres zasiłkowy),
- **`PracHistoria.DodatkowyBO: Soneta.Kadry.DodatkowyBO`** (BO urlopowy — urlop u poprzednich pracodawców).
- BO nieobecności pojedynczej oznacza też flaga `Nieobecnosc.BilansOtwarcia: bool`
(interfejs `IBilansOtwarcia` na `Nieobecnosc`).
**Pola i typy (`ChorobowyBO`) — bazodanowe:**
| Pole | Typ | Opis |
|---|---|---|
| `Data` | `Soneta.Types.Date` | data BO |
| `MiesiacPodstawy` | `Soneta.Types.YearMonth` | miesiąc podstawy |
| `Podstawa` | `decimal` | podstawa BO |
| `DniWynagrodzenia` | `int` | dni zwolnienia finansowane przez pracodawcę |
| `DniZasilkowe` | `int` | dni wliczane do bieżącego okresu zasiłkowego |
| `DniZwolnienia` | `int` | dni nieprzerwanego zwolnienia dobrowolnego |
| `ZasilekOdDnia` | `Soneta.Types.Date` | zasiłek od dnia |
| `PrzedluzenieOZ` | `bool` | okres zasiłkowy przedłużony o 3 mies. |
**Pola i typy (`DodatkowyBO`) — bazodanowe:**
| Pole | Typ | Opis |
|---|---|---|
| `UPoprzednich` | `decimal` | urlop wykorzystany u poprzednich pracodawców (dni) |
| `Wykorzystany` | `Soneta.Types.Time` | wykorzystany przypadający na bieżące zatrudnienie (godz.) |
| `BezPierwszego` | `bool` | prawo do urlopu w 1. mies. nabyte u poprzedniego pracodawcy |
**Snippet — wprowadzenie BO chorobowego i urlopowego na zapisie historycznym:**
```csharp
var pracownik = session.GetKadry().Pracownicy.WgKodu["006"];
var historia = pracownik.Historia[Date.Today]; // właściwy zapis historyczny „na dzień"
using (var t = session.Logout(editMode: true))
{
// BO chorobowy / okres zasiłkowy
historia.ChorobowyBO.DniZasilkowe = 33;
historia.ChorobowyBO.ZasilekOdDnia = new Date(2026, 1, 1);
// BO urlopowy
historia.DodatkowyBO.UPoprzednich = 10m;
t.Commit();
}
session.Save();
```
**Pułapki:**
- `ChorobowyBO`/`DodatkowyBO` to **subrowy** zapisu `PracHistoria` — nie tworzysz ich osobno, edytujesz
ich pola na istniejącym zapisie historycznym.
- **`ChorobowyBO`** (`DniZasilkowe`, `ZasilekOdDnia`, `PrzedluzenieOZ`, …) jest **zapisywalny** na zwykłym
zapisie historii (zweryfikowane testem KADRY-D10 na Demo).
- **`DodatkowyBO`** (`UPoprzednich`, `BezPierwszego`, `Wykorzystany`) na zwykłym zapisie historii Demo
rzuca **`ColReadOnlyException`** („pole w trybie tylko do odczytu") — BO urlopowy jest zapisywalny tylko
na zapisie historycznym oznaczonym jako **bilans otwarcia / start zatrudnienia**, nie na dowolnym zapisie
„na dzień". W teście na gotowych pracownikach Demo dodawanie `DodatkowyBO` oznacz `[Ignore]`.
- Pobierz właściwy zapis historyczny przez `pracownik.Historia[data]` (patrz KADRY-A14/KADRY-A15) — edycja BO na
niewłaściwym zapisie da błędne dane „na dzień".
- BO ma sens przy wdrożeniu — nie miesza się z normalnym naliczaniem; po wprowadzeniu wpływa na limity
(KADRY-D8) i okres zasiłkowy (KADRY-D6) dopiero przy przeliczeniu/naliczeniu.
---
### KADRY-D11 — Wnioski o urlop / delegację
**Cel:** zarejestrować wniosek urlopowy (lub o delegację), zmienić jego stan (akceptacja/odrzucenie/
przywrócenie) i — docelowo — przekształcić zaakceptowany wniosek w nieobecność.
**Fakty o typie (zweryfikowane skanem DLL):**
- Wniosek urlopowy: **`Soneta.Kadry.WniosekUrlopowy`** — tabela `WnioskiUrlopowe`, `GuidedRow` root.
Konstruktory publiczne: **`new WniosekUrlopowy(Pracownik pracownik)`** oraz
**`new WniosekUrlopowy(Pracownik pracownik, DefinicjaNieobecnosci definicja)`**.
- Kolekcja na pracowniku: **`pracownik.WnioskiUrlopowe: SubTable<Soneta.Kadry.WniosekUrlopowy>`**
(oraz `WnioskiKierownika`, `WnioskiZastępcy` — te same wnioski w roli kierownika/zastępcy).
- Pola `WniosekUrlopowy` (bazodanowe, zapisywalne): `Pracownik: Pracownik`,
`Definicja: DefinicjaNieobecnosci`, `Okres: FromTo`, `Data: Date`, `DataDecyzji: Date`,
`Kierownik: Pracownik`, `Opis: MemoText`, `Stan: Soneta.Kadry.StanWnioskuUrlopowego`.
- `StanWnioskuUrlopowego`: `Oczekujący`, `Anulowany`, `Zaakceptowany`, `Odrzucony`, `Korygowana`.
- Wniosek o delegację jest subrowem wniosku: `WniosekUrlopowy.Delegacja: Soneta.Kadry.WniosekODelegację`
(`DataRozpoczeciaPlanowana`, `DataZakonczeniaPlanowana: DateShortTime`, `KrajDocelowy`, `Cel: MemoText`,
`WnioskowanaZaliczka: Currency`); samodzielny `new WniosekODelegację()` ma publiczny ctor bezparametrowy.
- **Planowane nieobecności** (osobny model, np. plan urlopów): kolekcja
**`pracownik.PlanowaneNieobecności: FromToSubTable<Soneta.Kalend.PlanowanaNieobecność>`**;
typ `PlanowanaNieobecność` (tabela `PlanNieobecnosci`, root) z ctorem
**`new PlanowanaNieobecność(Pracownik pracownik)`**, polami `Definicja`, `Okres: FromTo`.
- **`Definicja` musi mieć zaznaczone pole `Planowana`** (`DefinicjaNieobecnosci.Planowana == true`) —
inaczej setter rzuca `RowException` „Wybrana definicja musi mieć zaznaczone pole 'Planowana'."; dobierz
definicję dynamicznie: `DefNieobecnosci.Cast<DefinicjaNieobecnosci>().First(d => d.Planowana)`.
- **`Stan: StanPlanowanejNieobecności` jest READ-ONLY** (`Oczekująca`, `Wprowadzona`, `Korygowana`,
`Zatwierdzona`, `Anulowana`) — **nie przypisujesz** go wprost (`plan.Stan = …` → błąd kompilacji
„cannot be assigned to"); przejścia stanu wykonujesz metodami domenowymi
**`StanWprowadzona()` / `StanZatwierdzona()` / `StanAnulowana()` / `StanOczekująca()`**.
- Akceptacja/odrzucenie/przywrócenie z poziomu Pulpitu: worker (UI/Net)
**`PracownikNetWnioskiUrlopowe`** z akcjami „Zatwierdź wniosek"/`Zatwierdz`, „Odrzuć wniosek"/`Odrzuc`,
„Przywróć wniosek"/`Przywroc`. W kodzie biznesowym/teście prościej ustawiać `Stan` wprost.
**Snippet — rejestracja wniosku urlopowego + akceptacja:**
```csharp
var kalend = session.GetKalend();
var pracownik = session.GetKadry().Pracownicy.WgKodu["006"];
// UWAGA: dla definicji limitowanej (np. „Urlop wypoczynkowy") akceptacja wniosku (set Stan) wyzwoli
// przeliczenie limitu → LimitNotFoundException, jeśli limit nie został wcześniej naliczony (patrz pułapki).
// Tu używamy definicji bezlimitowej (np. „Urlop bezpłatny (art 174 kp)") albo najpierw naliczamy limit (KADRY-D8).
var defUrlop = kalend.DefNieobecnosci.WgNazwy["Urlop bezpłatny (art 174 kp)"];
using (var t = session.Logout(editMode: true))
{
var wniosek = session.AddRow(new WniosekUrlopowy(pracownik, defUrlop));
wniosek.Okres = new FromTo(new Date(2026, 8, 3), new Date(2026, 8, 7));
wniosek.Data = Date.Today;
wniosek.Stan = StanWnioskuUrlopowego.Oczekujący;
t.Commit();
}
session.Save();
// Akceptacja (zmiana stanu):
using (var t = session.Logout(editMode: true))
{
var wniosek = pracownik.WnioskiUrlopowe
.Cast<WniosekUrlopowy>()
.First(w => w.Stan == StanWnioskuUrlopowego.Oczekujący);
wniosek.Stan = StanWnioskuUrlopowego.Zaakceptowany;
wniosek.DataDecyzji = Date.Today;
t.Commit();
}
session.Save();
```
**Pułapki:**
- **Akceptacja wniosku na definicji limitowanej rzuca `LimitNotFoundException`** bez wcześniej naliczonego
limitu: ustawienie `Stan` (np. `Zaakceptowany`) na wniosku z definicją „Urlop wypoczynkowy" wewnętrznie
ustawia `Okres` nieobecności i wyzwala `DefinicjaLimitu.Przelicz(...)`, który dla pracownika bez limitu
na ten dzień rzuca wyjątek. Rozwiązanie: albo nalicz limit (KADRY-D8) **przed** zmianą stanu, albo do scenariusza
obsługi samego rekordu wniosku użyj definicji **bezlimitowej** (np. „Urlop bezpłatny (art 174 kp)").
- **Przekształcenie wniosku w nieobecność** wymaga, by nieobecność limitowana miała naliczony limit
(jak KADRY-D1) — sama akceptacja wniosku nie tworzy automatycznie rozliczonej nieobecności w teście bez
naliczonego limitu/wypłaty.
- `WniosekODelegację` to subrow wniosku (`WniosekUrlopowy.Delegacja`) — wnioskowanie o delegację
ustawiasz na tym subrowie; pełne rozliczenie delegacji to moduł `Soneta.Delegacje` (osobny dokument
handlowy PWS), poza zakresem wniosku.
- Filtruj kolekcję wniosków przez `WnioskiUrlopowe[condition]` lub iteruj w zakresie jednego pracownika;
nie skanuj globalnej tabeli `WnioskiUrlopowe` bez zakresu (tabela operacyjna guided).
- Stan zmieniaj świadomie wg enuma `StanWnioskuUrlopowego` — workery Net robią to samo z dodatkową
logiką workflow (powiadomienia), której w teście jednostkowym nie odtworzysz.
---
### KADRY-D12 — Praca zdalna (wnioski, lokalizacje, ewidencja)
**Cel:** skonfigurować pracę zdalną pracownika (model pracy, limit pracy zdalnej okazjonalnej),
zarejestrować wniosek o pracę zdalną i lokalizacje jej świadczenia oraz odczytać ewidencję.
**Fakty o typie (zweryfikowane skanem DLL):**
- Parametry pracy zdalnej leżą na etacie/historii: **`PracHistoria.PracaZdalna: Soneta.Kadry.PracZdalna`**
(subrow, bazodanowe, zapisywalne):
- `ModelPracy: Soneta.Kadry.ModelPracy` (`NieDotyczy`, `PracaStacjonarna`, `PracaHybrydowa`, `PracaZdalna`),
- `OswiadczenieWarunki: bool` (warunki lokalowe/techniczne),
- `LimitPZ: int`, `IndywidualnyLimitPZ: bool`, `TypLimituPZ: TypLimituPracyZdalnej`
(`Roczny`, `Miesieczny`, `Tygodniowy`, `Kwartalny`, `Półroczny`).
- Lokalizacje pracy zdalnej: **`pracownik.LokalizacjePracyZdalnej: SubTable<Soneta.Kadry.LokalizacjaPracyZdalnej>`**
(tabela `LokPracZdalnej`).
- Wnioski o pracę zdalną: **`pracownik.WnioskiPracyZdalnej: SubTable<Soneta.Kalend.WniosekPracyZdalnej>`**
(oraz `WnioskiPracyZdalnejKierownika`); typ `WniosekPracyZdalnej` ma ctor
`(Pracownik, DefinicjaRodzajuPracyZdalnej)`**ctory są niepubliczne**, więc tworzenie wniosku idzie
przez worker (`GrupoweZleceniePracyZdalnejWorker`) lub Pulpit, nie wprost `new`.
- Lokalizacja pracy zdalnej: `Soneta.Kadry.LokalizacjaPracyZdalnej` ma **publiczny ctor
`new LokalizacjaPracyZdalnej(Pracownik pracownik)`**.
- Ewidencja/odczyt limitu pracy zdalnej okazjonalnej: worker
**`Soneta.Kadry.Pracownik.PracaZdalnaWorker`** — property odczytowe (bez akcji modyfikującej):
`DniPracyZdalnejRazem: int`, `DniPracyZdalnejOkazjonalnej: int`, `DniPracyZdalnejOkazjonalnejLimit: int`,
`CzasPracyZdalnejRazem: Time`, `LimitPracaZdalnaOkazjonalna: int`, `PozostaloPracaZdalnaOkazjonalna: int`;
kontekst: `Pracownik: Pracownik`, `Okres: FromTo`.
- Grupowe zlecenie pracy zdalnej (Pulpit/seryjne): worker
**`Soneta.Kadry.UI.KadryNet.Workers.GrupoweZleceniePracyZdalnejWorker`** (akcja
„Dodaj wnioski zlecenia pracy zdalnej"/`DodajZleceniaPracyZdalnej`): `Pracownicy: Pracownik[]`,
`Pars.Okres: FromTo`, `Pars.Data: Date`, `Pars.Uwagi: string`.
- Aktualizacja podzielników kosztów na podstawie pracy hybrydowej: worker
**`AktualizujPodzielnikowPracaZdalnaWorker`** (`DefinicjaPodzielnika`, `Okres: YearMonth`, …).
**Snippet — ustawienie modelu pracy zdalnej + lokalizacja + wniosek:**
```csharp
var kadry = session.GetKadry();
var pracownik = kadry.Pracownicy.WgKodu["006"];
using (var t = session.Logout(editMode: true))
{
var historia = pracownik.Historia[Date.Today];
historia.PracaZdalna.ModelPracy = ModelPracy.PracaHybrydowa;
historia.PracaZdalna.OswiadczenieWarunki = true;
// lokalizacja pracy zdalnej (np. adres domowy)
var lok = session.AddRow(new LokalizacjaPracyZdalnej(pracownik));
// … pola adresowe lokalizacji wg LokalizacjaPracyZdalnej
t.Commit();
}
session.Save();
// Odczyt ewidencji pracy zdalnej okazjonalnej (worker odczytowy):
// Pracownik i Okres są zwykłymi, settowalnymi property (nie trzeba przekazywać przez Context):
var pz = new Soneta.Kadry.Pracownik.PracaZdalnaWorker
{
Pracownik = pracownik,
Okres = FromTo.Year(new Date(2026, 1, 1))
};
// odczyt: pz.DniPracyZdalnejRazem, pz.LimitPracaZdalnaOkazjonalna, pz.PozostaloPracaZdalnaOkazjonalna
```
**Pułapki:**
- `PracaZdalnaWorker` to worker **odczytowy** (ma property, brak akcji modyfikującej) — służy do
prezentacji ewidencji/limitu, nie do zapisu.
- `ModelPracy`/`OswiadczenieWarunki` są na **historycznym** zapisie etatu (`PracHistoria.PracaZdalna`) —
edytuj właściwy zapis „na dzień".
- `WniosekPracyZdalnej` ma **niepubliczne ctory** — w teście jednostkowym nie utworzysz go przez `new`;
zlecenie pracy zdalnej idzie przez worker `GrupoweZleceniePracyZdalnejWorker` (czynność Net/UI,
wymaga `Context`). Testuj raczej `ModelPracy`/`OswiadczenieWarunki` na `PracHistoria.PracaZdalna`
i `LokalizacjaPracyZdalnej` (ma publiczny ctor).
- `LokalizacjaPracyZdalnej` ma publiczny ctor `(Pracownik)` — testowalna wprost.
@@ -0,0 +1,348 @@
# KADRY05 — Plan pracy i kalendarz
> Wspólne fakty o typie, podstawowe typy i szablon wzorca: [../kadry.md](../kadry.md).
> **Model kalendarza pracownika.** Każdy `Pracownik` ma kalendarz roboczy
> (`pracownik.Etat.Kalendarz : Soneta.Kalend.Kalendarz`), którego dni leżą w tabeli
> `DniKalendarza` (`DzienKalendarzaBase`, child kalendarza). Pracownik wystawia trzy
> niezależne kolekcje dni typu `DateSubTable` (indeksator po dacie `[Date]`, **tylko do
> odczytu** — element tworzysz konstruktorem + `AddRow`):
> - `pracownik.DniPlanu : DateSubTable` — **plan/harmonogram** (dni `DzienPlanu : DzienKalendarzaBase`); to `pracownik.Etat.Kalendarz.Dni`.
> - `pracownik.DniPracy : DateSubTable<Soneta.Kalend.DzienPracy>` — **ewidencja** (realizacja) czasu pracy.
> - `pracownik.DniRCP : DateSubTable<Soneta.Kalend.DzienRCP>` — **zarejestrowany** czas pracy (RCP) — patrz sekcja F.
>
> Wszystkie dni współdzielą subrow `Praca : Soneta.Kalend.CzasPracy` z polami
> `OdGodziny`/`DoGodziny`/`Czas : Soneta.Types.Time`. Definicja dnia (`Definicja :
> Soneta.Kalend.DefinicjaDnia`) to rekord **konfiguracyjny** (słownik `DefinicjeDni`,
> indeksator `[Kod]`).
>
> **Ograniczenie wykonalności.** Plan i ewidencja są normalnie wyliczane przez kalkulator
> czasu pracy z definicji kalendarza/serii — ręczne tworzenie pojedynczego dnia jest możliwe
> publicznym kontraktem (ctor `(Pracownik, Date)` + `AddRow`), ale **wymaga zdefiniowanego
> `DefinicjaDnia` w konfiguracji**. Operacje masowe (przeliczenie planu na okres) są zaszyte
> w workerach/kalkulatorach UI — patrz KADRY-E2.
### KADRY-E1 — Wprowadzanie planowanego czasu pracy (★)
**Cel:** odczytać lub ustawić plan pracy (harmonogram) pracownika na konkretny dzień —
godziny oddo, normę dobową oraz typ dnia.
**Pola i typy:**
| Element | Lokalizacja | Typ | Uwaga |
|---|---|---|---|
| Plan pracy (cała kolekcja) | `pracownik.DniPlanu` | `Soneta.Business.DateSubTable` | == `pracownik.Etat.Kalendarz.Dni`; indeksator `[Date]` (get) |
| Dzień planu | `pracownik.DniPlanu[data]` | `Soneta.Kalend.DzienPlanu` (`DzienKalendarzaBase`) | `null`, gdy dla daty brak dnia planu |
| Data dnia | `DzienPlanu.Data` | `Soneta.Types.Date` | bazodanowe; ustawiane przez ctor |
| Godziny pracy (subrow) | `DzienPlanu.Praca` | `Soneta.Kalend.CzasPracy` | `Praca.OdGodziny`, `Praca.DoGodziny`, `Praca.Czas : Time` (zapisywalne) |
| Czas (norma dnia, odczyt) | `DzienPlanu.Czas` | `Soneta.Types.Time` | kalkulowane (czas pracy dnia) |
| Od (odczyt) | `DzienPlanu.OdGodziny` | `Soneta.Types.Time` | kalkulowane |
| Definicja dnia | `DzienPlanu.Definicja` | `Soneta.Kalend.DefinicjaDnia` | rekord słownika konfiguracyjnego `DefinicjeDni` |
| Tolerancja wejścia | `DzienPlanu.TolerancjaWe` | `Soneta.Types.Time` | bazodanowe |
| Norma dobowa kalendarza | `pracownik.Etat.Kalendarz.NormaDobowa` | `Soneta.Types.Time` | poziom kalendarza, nie dnia |
| Słownik definicji dni | `session.GetKalend().DefinicjeDni` | `DefinicjeDni` | indeksator `[kod: string]`; skróty `WolnaSobota`, `Niedziela` |
**Snippet:**
```csharp
var kalend = session.GetKalend();
var pracownik = session.GetKadry().Pracownicy.WgKodu["006"];
// --- Odczyt planu na dzień (bezpiecznie: indeksator zwraca null dla braku dnia) ---
var data = new Date(2026, 6, 1);
var dzienPlanu = (DzienPlanu)pracownik.DniPlanu[data];
if (dzienPlanu is not null)
{
Time odGodz = dzienPlanu.Praca.OdGodziny; // np. 8:00
Time doGodz = dzienPlanu.Praca.DoGodziny; // np. 16:00
Time normaDnia = dzienPlanu.Czas; // wyliczona norma dnia (kalkulowane)
DefinicjaDnia typDnia = dzienPlanu.Definicja;
}
// --- Ustawienie/utworzenie dnia planu (wymaga DefinicjaDnia z konfiguracji) ---
using (var t = session.Logout(editMode: true))
{
var dp = (DzienPlanu)pracownik.DniPlanu[data];
if (dp is null)
{
dp = session.AddRow(new DzienPlanu(pracownik, data)); // ctor (Pracownik, Date)
dp.Definicja = kalend.DefinicjeDni["RB"]; // typ dnia ze słownika (np. dzień roboczy)
}
dp.Praca.OdGodziny = new Time(8, 0);
dp.Praca.DoGodziny = new Time(16, 0); // Czas dnia wylicza się z oddo
t.Commit();
}
session.Save();
```
**Pułapki:**
- `DniPlanu` to `DateSubTable` **nietypowany** (zwraca `Row`) — rzutuj na `DzienPlanu`. Indeksator
`[Date]` jest **tylko do odczytu**: nowego dnia nie „przypiszesz", tworzysz go ctorem
`new DzienPlanu(pracownik, data)` + `session.AddRow(...)`.
- Godziny ustawiasz na **subrowie** `Praca` (`dp.Praca.OdGodziny = …`), nie na `dp.OdGodziny`
to ostatnie jest kalkulowane (read-only). Po ustawieniu oddo `Praca.Czas`/`Czas` przeliczają się.
- `Definicja` to rekord **konfiguracyjnego** słownika `DefinicjeDni` — pobierz istniejący wpis
(`kalend.DefinicjeDni[kod]`), nie twórz „w locie". Bez przypisanego `Definicja` świeży dzień planu
może nie przejść weryfikatorów.
- Plan jest zwykle generowany przez kalkulator z definicji kalendarza (serie dni, święta) —
ręczne nadpisywanie pojedynczego dnia to korekta, nie sposób budowy całego harmonogramu (do tego
służy operacja seryjna / kopiowanie planu, KADRY-E2).
- Norma dobowa to atrybut **kalendarza** (`Etat.Kalendarz.NormaDobowa`), nie pojedynczego dnia.
### KADRY-E2 — Planowanie czasu pracy grupy (kopiowanie planu) (★)
**Cel:** skopiować wyliczony plan pracy (harmonogram) na wskazany okres — dla jednego pracownika
albo dla grupy, oraz seryjnie zaktualizować kalendarz pracowników (zmiana kalendarza docelowego).
**Publiczny kontrakt — dwie drogi:**
| Operacja | API | Charakter |
|---|---|---|
| Kopiowanie **planu** pracownika na okres | `Soneta.Kalend.KalendarzPlanuKopia.Kopiuj(Pracownik pracownik, FromTo okres)` (**public static**) | bez UI — proste API |
| Kopiowanie **pracy/realizacji** na okres | `Soneta.Kalend.KalendarzPracyKopia.Kopiuj(Pracownik pracownik, FromTo okres)` (**public static**) | bez UI — proste API |
| Kopiowanie grupy (worker UI) | `KalendarzPlanuKopia.KopiujWorker` / `KalendarzPracyKopia.KopiujWorker` | wymaga `Context` z zaznaczeniem |
| Aktualizacja kalendarza grupy | `Soneta.Kadry.AktualizujKalendarzWorker` | wymaga `Params` z `Context` |
**Worker `KopiujWorker` (BI/„Kopiuj plan…", „Kopiuj pracę…"):** klasa `ContextBase` z ctorem
`(Context context)`; pola `[Context] FromTo Okres`, `[Context] Pracownik[] Pracownicy`; metoda
`void Kopiuj()`. Działa **wyłącznie** z kontekstem UI (zaznaczona lista pracowników) i jest gardzona
licencją BI/BI_PL/PL oraz `IsVisibleKopiuj` (niedostępny na mobile).
**Worker `AktualizujKalendarzWorker`:** pola `[Context] Pracownik[] Pracownicy`,
`Params Pars` (`Pars.Data`, `Pars.TylkoOstatni: bool`, `Pars.PowodAktualizacji: string`,
`Pars.Kalendarze: KalendarzBase[]`, `Pars.Docelowy: Kalendarz`, `Pars.Zmiana: bool`,
`Pars.Interpretacja`), metoda `void Aktualizuj()`. `Params` to `ContextBase` (ctor `(Context)`).
**Snippet (proste API dla jednego pracownika — bez UI):**
```csharp
var pracownik = session.GetKadry().Pracownicy.WgKodu["006"];
var okres = new FromTo(new Date(2026, 6, 1), new Date(2026, 6, 30));
using (var t = session.Logout(editMode: true))
{
// Wylicza plan z kalendarza i zapisuje do kopii planu pracownika za wskazany okres:
KalendarzPlanuKopia.Kopiuj(pracownik, okres); // public static
// analogicznie realizacja: KalendarzPracyKopia.Kopiuj(pracownik, okres);
t.Commit();
}
session.Save();
```
**Snippet (grupa — przez worker; wymaga Context z zaznaczeniem):**
```csharp
// Tylko w warstwie UI/Czynności — Context dostarcza zaznaczonych pracowników.
var worker = new KalendarzPlanuKopia.KopiujWorker(context)
{
Okres = new FromTo(new Date(2026, 6, 1), new Date(2026, 6, 30)),
Pracownicy = context.Get<Pracownik[]>()
};
worker.Kopiuj(); // wewnątrz: Session.Logout + Commit
```
**Pułapki:**
- **Kopiowanie grupy nie ma „czystego" API bezkontekstowego** — `KopiujWorker` i
`AktualizujKalendarzWorker.Params` dziedziczą po `ContextBase` i wymagają `Context` (zaznaczenie z
listy UI). Dla kodu serwerowego/testów używaj **publicznej statycznej** `KalendarzPlanuKopia.Kopiuj(pracownik, okres)`
w pętli po pracownikach — to ona realizuje właściwą logikę (worker w `KopiujInt` woła ją per pracownik).
- `KopiujWorker.Kopiuj()` jest gardzony licencją (BI/BI_PL/PL) i `IsVisibleKopiuj` (m.in. blokada na
mobile) — to logika UI, nie wywołuj jej z kodu biznesowego.
- Kopia planu/pracy trafia do **osobnych** kolekcji `pracownik.DniPlanuKopia`/`pracownik.DniPracyKopia`
(`DateSubTable`), powiązanych z `KalendarzPlanuKopia`/`KalendarzPracyKopia` — to bufor kopii, odrębny
od właściwego `DniPlanu`/`DniPracy`.
- `okres` jest normalizowany przez setter workera do pełnych miesięcy (otwarty `From`/`To`
pierwszy/ostatni dzień miesiąca); przy statycznym `Kopiuj` podawaj zamknięty `FromTo`.
- Operacja seryjna na grupie pracowników = długa transakcja → dziel na paczki, trzymaj transakcje
krótkie (safe-code §13.1).
### KADRY-E3 — Aktualizacja kalendarza pracownika (operacja seryjna „Zaktualizuj kalendarz pracownika")
**Cel:** seryjnie zmienić kalendarz roboczy zaznaczonych pracowników (zmiana kalendarza
docelowego, przeliczenie planu na nowy kalendarz od wskazanej daty) — operacja z menu
„Czynności" na liście pracowników.
**Publiczny kontrakt — worker `Soneta.Kadry.AktualizujKalendarzWorker`:**
| Element | Sygnatura / typ | Uwaga |
|---|---|---|
| Konstruktor | `new AktualizujKalendarzWorker()` | bezparametrowy; worker UI |
| Pracownicy (wejście) | `Pracownicy : Pracownik[]` | **set-only**; karmione z `Context` (zaznaczenie listy) |
| Parametry | `Pars : Params` | **set-only**; `Params` to `ContextBase`, ctor `(Context context)` |
| Wykonanie | `void Aktualizuj()` | właściwa operacja seryjna (Logout + Commit wewnątrz) |
**`Soneta.Kadry.AktualizujKalendarzWorker.Params` (`: ContextBase`, ctor `(Context)`):**
| Pole | Typ | Uwaga |
|---|---|---|
| `Data` | `Soneta.Types.Date` | data, od której obowiązuje nowy kalendarz |
| `TylkoOstatni` | `bool` | aktualizuj tylko ostatni (bieżący) zapis historyczny |
| `PowodAktualizacji` | `string` | opis powodu (do dokumentu aktualizacji) |
| `Kalendarze` | `KalendarzBase[]` | kalendarze źródłowe objęte zmianą; lista przez `GetListKalendarze()` |
| `Docelowy` | `Soneta.Kalend.Kalendarz` | kalendarz docelowy; lista przez `GetListDocelowy()` |
| `Zmiana` | `bool` | flaga: czy zmienić kalendarz (a nie tylko przeliczyć) |
| `Interpretacja` | `Soneta.Kadry.InterpretacjaKalendarza` | `WgPlanu` / `WgObecnosci` / `WgZestawien`; `IsReadOnlyInterpretacja()` |
**Snippet (warstwa UI/Czynności — wymaga `Context` z zaznaczeniem):**
```csharp
// Tylko w warstwie UI: Context dostarcza zaznaczonych pracowników.
var worker = new AktualizujKalendarzWorker
{
Pracownicy = context.Get<Pracownik[]>(),
Pars = new AktualizujKalendarzWorker.Params(context)
{
Data = new Date(2026, 7, 1),
Docelowy = session.GetKalend().Kalendarze.WgKodu["PODSTAWOWY"],
Zmiana = true,
Interpretacja = InterpretacjaKalendarza.WgPlanu,
PowodAktualizacji = "Zmiana systemu czasu pracy"
}
};
worker.Aktualizuj(); // wewnątrz: Session.Logout + Commit
```
**Pułapki:**
- `Params` dziedziczy po `ContextBase` (ctor `(Context)`) — **nie da się go zbudować bez `Context`**.
Dlatego KADRY-E3 nie ma „czystego" API bezkontekstowego; to operacja UI/serwerowa z zaznaczeniem.
- `Pracownicy` i `Pars`**set-only** — nie odczytasz ich z powrotem; ustaw przed `Aktualizuj()`.
- Operacja seryjna = długa transakcja na wielu pracownikach → w realnym użyciu dziel na paczki
(safe-code §13.1). Sam worker zarządza transakcją wewnętrznie.
- Zmiana kalendarza jest **historyczna** (operuje na zapisach `Etat`) — `TylkoOstatni`/`Data`
decydują, których zapisów historycznych dotyczy.
---
### KADRY-E4 — Uzgodnienie doby pracowniczej (model doby; godziny rozpoczęcia doby)
**Cel:** przesunąć granicę doby pracowniczej dla dnia ewidencji — gdy zmiana zaczyna się w jednej
dobie kalendarzowej, a kończy w następnej (nocna), uzgodnienie „przenosi" początek/koniec pracy do
właściwej doby pracowniczej. Operacja na pojedynczym dniu (`DzienPracy`) lub seryjnie na grupie.
**Model doby (publiczny kontrakt):**
| Element | Lokalizacja | Typ | Uwaga |
|---|---|---|---|
| Początek doby w niedziele/święta | `pracownik.Last.Etat.ConfigPoczątekDobyNiedzieledIŚwięta` | `Soneta.Types.Time` | **read-only** (konfiguracyjne); godzina startu doby |
| Norma dobowa | `pracownik.Last.Etat.NormaDobowa` | `Soneta.Types.Time` | bazodanowe; norma czasu doby |
| Norma dobowa kalendarza | `pracownik.Last.Etat.Kalendarz.NormaDobowa` | `Soneta.Types.Time` | poziom kalendarza |
| Interpretacja kalendarza | `pracownik.Last.Etat.InterpretacjaKalendarza` | `Soneta.Kadry.InterpretacjaKalendarza` | `WgPlanu`/`WgObecnosci`/`WgZestawien` — jak interpretować dobę |
> **Uwaga:** `Etat` leży na bieżącym **zapisie historycznym** (`pracownik.Last.Etat : Soneta.Kadry.Etat`,
> gdzie `Last : PracHistoria`) — nie ma property `pracownik.Etat` bezpośrednio na roocie pracownika.
| Godziny pracy dnia | `DzienPracy.Praca` | `Soneta.Kalend.CzasPracy` | `OdGodziny`/`DoGodziny`/`Czas` — granice realizacji w dobie |
**Worker pojedynczego dnia — `Soneta.Kalend.DzienPracy.UzgodnijDobePracowniczaWorker`:**
| Element | Sygnatura | Uwaga |
|---|---|---|
| Konstruktor | `new DzienPracy.UzgodnijDobePracowniczaWorker()` | |
| Dzień (wejście) | `Dzień : DzienPracy` | **set-only** |
| Warunek dostępności | `static bool IsEnabledUzgodnijDobePracownicza(DzienPracy dzień)` | czy operacja ma sens dla dnia |
| Uzgodnienie | `object UzgodnijDobePracownicza()` | przelicza dobę |
| Przeniesienie początku | `DzienPracy PrzenieśPoczątek()` | przenosi początek pracy do poprz. doby |
| Przeniesienie końca | `DzienPracy PrzenieśKoniec()` | przenosi koniec pracy do nast. doby |
| Dokument aktualizacji | `DokumentAktualizacjiKalendarza : IDokumentAktualizacjiKalendarza`, `DataAktualizacji : System.DateTime` | kontekst historii |
**Worker seryjny (grupa) — `Soneta.Kadry.UzgodnijDobePracowniczaPracownikowWorker`:**
| Element | Sygnatura / typ | Uwaga |
|---|---|---|
| Konstruktor | `new UzgodnijDobePracowniczaPracownikowWorker()` | |
| Pracownicy | `Pracownicy : Pracownik[]` | **set-only**; z `Context` |
| Parametry | `Pars : Params` (`ContextBase`, ctor `(Context)`); pole `Okres : FromTo` | **set-only** |
| Wykonanie | `UzgodnijDobePracowniczaResult UzgodnijDobePracownicza()` | zwraca wynik |
**Snippet (pojedynczy dzień):**
```csharp
var pracownik = session.GetKadry().Pracownicy.WgKodu["006"];
var dzien = pracownik.DniPracy[new Date(2026, 6, 1)]; // DzienPracy lub null
if (dzien is not null && DzienPracy.UzgodnijDobePracowniczaWorker.IsEnabledUzgodnijDobePracownicza(dzien))
{
using (var t = session.Logout(editMode: true))
{
var worker = new DzienPracy.UzgodnijDobePracowniczaWorker { Dzień = dzien };
worker.UzgodnijDobePracownicza();
t.Commit();
}
session.Save();
}
```
**Pułapki:**
- Godzina rozpoczęcia doby to atrybut **konfiguracyjny `Etat`** (`ConfigPoczątekDobyNiedzieledIŚwięta`,
read-only) i normy `Etat.NormaDobowa`/`Etat.Kalendarz.NormaDobowa` — nie ma osobnego, edytowalnego
pola „początek doby" na pojedynczym `DzienPracy`.
- `Dzień` workera pojedynczego jest **set-only**; `Pracownicy`/`Pars` workera grupowego również.
- Worker grupowy `Params` to `ContextBase` (ctor `(Context)`) — **wymaga `Context`** (zaznaczenie UI),
brak czystego API bezkontekstowego.
- Uzgodnienie modyfikuje `DzienPracy.Praca` (oddo) i może rozbić pracę na dwie doby — wykonuj w
transakcji (`Logout(editMode:true)` + `Commit`) i zapisz `Save()`.
---
### KADRY-E5 — Odczyt normy czasu pracy i czasu przepracowanego za okres (★ testowalne)
**Cel:** dla pracownika odczytać za zadany okres (`FromTo`/`YearMonth`): normę czasu pracy
(planowaną), czas przepracowany (zrealizowany), nadgodziny, czas nocny, liczbę/normę nieobecności —
bez modyfikacji danych (czysty odczyt statystyk).
**Punkt wejścia — `pracownik.Czasy : Soneta.Kalend.KalkulatorPracownika`:**
| Metoda (publiczna, instancyjna) | Zwraca | Znaczenie |
|---|---|---|
| `Norma(FromTo okres, params Item[] condition)` | `CzasDni` | norma (planowana) czasu pracy za okres |
| `Norma(FromTo okres, DefinicjaStrefy def, params Item[] condition)` | `CzasDni` | norma w obrębie strefy |
| `NormaKodeksowa(YearMonth miesiąc)` | `CzasDni` | norma kodeksowa miesiąca (pełny etat) |
| `NormaKodeksowaWym(Fraction wymiar, Time normaDobowa, YearMonth miesiąc)` | `CzasDni` | norma kodeksowa wg wymiaru etatu |
| `Praca(FromTo okres, params Item[] condition)` | `CzasDni` | czas **przepracowany** (zrealizowany) za okres |
| `Praca(FromTo okres, DefinicjaStrefy def, params Item[] condition)` | `CzasDni` | przepracowany w obrębie strefy |
| `PracaRozliczana(FromTo okres, params Item[] condition)` | `CzasDni` | czas pracy rozliczany (do nadgodzin) |
| `PracaZatr(FromTo okres, bool usprPłatne)` | `CzasDni` | praca w okresie zatrudnienia |
| `Nadgodziny(YearMonth okres)` / `Nadgodziny(FromTo okres)` | `ZestawienieNadgodzin` | nadgodziny |
| `NadgodzinyDobaOkres(FromTo okres)` | `ZestawienieNadgodzin` | nadgodziny dobowe/okresowe |
| `Nocne(YearMonth\|FromTo okres)` | `Time` | czas nocny |
| `NormaNie(YearMonth\|FromTo okres, params Item[] condition)` | `CzasDni` | norma nieobecności |
| `DniNie(YearMonth\|FromTo okres, params Item[] condition)` | `int` | liczba dni nieobecności |
| `Nieobecność(Date data[, bool clip])` | `INieobecnosc` | nieobecność w danym dniu |
**`Soneta.Kalend.CzasDni` (typ wyniku):**
| Pole | Typ | Uwaga |
|---|---|---|
| `Czas` | `Soneta.Types.Time` | sumaryczny czas (read-only) |
| `Dni` | `int` | liczba dni (read-only) |
| `CzasDni.Empty`, `CzasDni.Invalid` | `CzasDni` | wartości specjalne; operatory `+`/`-`/`==` |
**`Soneta.Kalend.ZestawienieNadgodzin` (struct):** `N50`, `N100`, `NSW`, `N100Doba`, `N100Okres`,
`Razem` — wszystkie `Time` (read-only); `ZestawienieNadgodzin.Zero`.
**Snippet (czysty odczyt):**
```csharp
var pracownik = session.GetKadry().Pracownicy.WgKodu["006"];
var kalk = pracownik.Czasy; // KalkulatorPracownika
var okres = new FromTo(new Date(2026, 6, 1), new Date(2026, 6, 30));
CzasDni norma = kalk.Norma(okres); // norma planowana
CzasDni przepracowano = kalk.Praca(okres); // czas zrealizowany
ZestawienieNadgodzin nadg = kalk.Nadgodziny(new YearMonth(2026, 6));
Time nocne = kalk.Nocne(okres);
Time normaCzas = norma.Czas; int normaDni = norma.Dni;
Time pracaCzas = przepracowano.Czas; Time nadgRazem = nadg.Razem;
```
**Pułapki:**
- `KalkulatorPracownika` **nie jest `Row`** — to obiekt liczący (zwykły `object`). Nie zapisuje się,
nie wymaga transakcji; to czysty odczyt. Pobieraj go zawsze przez `pracownik.Czasy` (ma kontekst
pracownika), nie twórz ręcznie ctorem chyba że masz `Pracownik` + ewentualny `Log`.
- Parametr `condition` to **serwerowy filtr** (`Item[]`, RowCondition) — można zawęzić np. do strefy;
zwykle pusty.
- `Norma` = plan, `Praca` = realizacja; nie myl `Praca(okres)` (statystyka) z `DzienPracy` (rekord dnia).
- Wynik `CzasDni.Invalid` sygnalizuje brak danych/błąd okresu — sprawdzaj zanim policzysz różnice.
---
@@ -0,0 +1,321 @@
# KADRY06 — RCP — rejestracja czasu pracy
> Wspólne fakty o typie, podstawowe typy i szablon wzorca: [../kadry.md](../kadry.md).
> **Dwie tabele.** Zarejestrowany (surowy) czas pracy z czytników RCP leży w
> `pracownik.DniRCP : DateSubTable<Soneta.Kalend.DzienRCP>` (tabela `DniRCP`). Pojedyncze zdarzenia
> wejścia/wyjścia (`Soneta.Kalend.WejscieWyjscie`, tabela `WejsciaWyjscia`) są **childem `DzienPracy`**
> (pole `WejscieWyjscie.Dzien : DzienPracy`, kolekcja `dzienPracy.WeWy`), a `DzienPracy` to
> ewidencja w `pracownik.DniPracy`. `DzienRCP` jest stanem zweryfikowanym RCP (z polem
> `StanRCP : StanWeryfikacjiRCP`), powstaje z importu/przeliczenia.
### KADRY-F1 — Rejestracja czasu pracy pracownika (★)
**Cel:** odczytać zarejestrowany/zewidencjonowany czas pracy pracownika za dzień oraz (gdy trzeba)
utworzyć dzień ewidencji.
**Pola i typy:**
| Element | Lokalizacja | Typ | Uwaga |
|---|---|---|---|
| Ewidencja (kolekcja) | `pracownik.DniPracy` | `DateSubTable<Soneta.Kalend.DzienPracy>` | indeksator `[Date]` (get); element ctorem |
| Dzień ewidencji | `pracownik.DniPracy[data]` | `Soneta.Kalend.DzienPracy` | `null` przy braku |
| RCP zweryfikowane (kolekcja) | `pracownik.DniRCP` | `DateSubTable<Soneta.Kalend.DzienRCP>` | analogicznie |
| Dzień RCP | `pracownik.DniRCP[data]` | `Soneta.Kalend.DzienRCP` | `null` przy braku |
| Przepracowany czas (subrow) | `DzienPracy.Praca` / `DzienRCP.Praca` | `Soneta.Kalend.CzasPracy` | `Praca.OdGodziny`, `Praca.DoGodziny`, `Praca.Czas : Time` |
| Czas/Od (odczyt) | `Dzien*.Czas`, `Dzien*.OdGodziny` | `Soneta.Types.Time` | kalkulowane |
| Stan weryfikacji RCP | `DzienRCP.StanRCP` | `Soneta.Kalend.StanWeryfikacjiRCP` | zapisywalne |
| Flaga importu RCP | `Dzien*.RcpOK` | `bool` | zapisywalne; stan rekordu po imporcie |
| Zdarzenia we/wy dnia | `DzienPracy.WeWy` | `LpSubTable<Soneta.Kalend.WejscieWyjscie>` | patrz KADRY-F2 |
| Uwagi / błędy (RCP) | `DzienRCP.Uwagi`, `DzienRCP.Bledy` | `Soneta.Business.MemoText` | zapisywalne |
**Snippet:**
```csharp
var kalend = session.GetKadry().Session.GetKalend();
var pracownik = session.GetKadry().Pracownicy.WgKodu["006"];
var data = new Date(2026, 6, 1);
// --- Odczyt zaewidencjonowanego czasu (ewidencja) ---
var dzienPracy = pracownik.DniPracy[data]; // typowane: DzienPracy lub null
if (dzienPracy is not null)
{
Time przepracowano = dzienPracy.Praca.Czas; // suma czasu pracy dnia
Time od = dzienPracy.Praca.OdGodziny;
Time @do = dzienPracy.Praca.DoGodziny;
}
// --- Odczyt stanu RCP (zweryfikowany rejestr) ---
var dzienRcp = pracownik.DniRCP[data]; // DzienRCP lub null
if (dzienRcp is not null)
{
Time czasRcp = dzienRcp.Praca.Czas;
StanWeryfikacjiRCP stan = dzienRcp.StanRCP;
}
// --- Utworzenie dnia ewidencji (gdy potrzebny ręczny wpis) ---
using (var t = session.Logout(editMode: true))
{
var dp = pracownik.DniPracy[data];
if (dp is null)
{
dp = session.AddRow(new DzienPracy(pracownik, data)); // ctor (Pracownik, Date)
kalend.DniPracy.AddRow(dp); // alternatywnie przez Module.DniPracy
}
dp.Praca.OdGodziny = new Time(8, 0);
dp.Praca.DoGodziny = new Time(16, 0);
t.Commit();
}
session.Save();
```
**Pułapki:**
- `DniPracy`/`DniRCP`**typowane** (`DateSubTable<DzienPracy>` / `<DzienRCP>`) — indeksator
`[Date]` zwraca od razu właściwy typ lub `null`. Nie iteruj całej kolekcji, by „znaleźć" dzień —
użyj indeksatora po dacie albo `[FromTo]` dla zakresu.
- Czas pracy ustawiaj na subrowie `Praca` (oddo); `Dzien*.Czas`/`Dzien*.OdGodziny` na rootcie dnia są
kalkulowane (read-only).
- `DzienRCP` to **wynik weryfikacji** importu RCP (z czytników) — w normalnym przepływie nie tworzysz
go ręcznie, lecz odczytujesz po imporcie/przeliczeniu. `DzienPracy` (ewidencja) to właściwe miejsce
na ręczny wpis.
- Świeży `DzienPracy` z `new DzienPracy(pracownik, data)` trzeba dodać do tabeli
(`Module.DniPracy.AddRow(...)` lub `session.AddRow(...)`) — sam ctor go nie rejestruje.
### KADRY-F2 — Rejestracja wejścia/wyjścia (RCP) (★)
**Cel:** dodać zdarzenie wejścia/wyjścia do dnia oraz odczytać listę zdarzeń RCP danego dnia.
**Pola i typy (`Soneta.Kalend.WejscieWyjscie`, tabela `WejsciaWyjscia`):**
| Pole | Typ | Uwaga |
|---|---|---|
| `Dzien` | `Soneta.Kalend.DzienPracy` | właściciel (guided-parent); ustawiany przez ctor `(DzienPracy)` |
| `Godzina` | `Soneta.Types.Time` | godzina zdarzenia (zapisywalne) |
| `Typ` | `Soneta.Kalend.TypWejsciaWyjscia` | enum: `Niezdefiniowany`, `Wejscie`, `Wyjscie`, `WejscieSluzbowe`, `WyjscieSluzbowe`, `WejsciePrywatne`, `WyjsciePrywatne` |
| `Operacja` | `int` | kod operacji urządzenia (zapisywalne) |
| `Lp` | `int` | liczba porządkowa zdarzeń w dniu (bazodanowe) |
| `DefinicjaZdarzenia` | `Soneta.Kalend.DefinicjaZdarzeniaRCP` | opcjonalna definicja zdarzenia ze słownika `DefZdarzenRCP` |
| Kolekcja zdarzeń dnia | `DzienPracy.WeWy : LpSubTable<WejscieWyjscie>` | uporządkowana po `Lp` |
**Snippet:**
```csharp
var kalend = session.GetKadry().Session.GetKalend();
var pracownik = session.GetKadry().Pracownicy.WgKodu["006"];
var data = new Date(2026, 6, 1);
using (var t = session.Logout(editMode: true))
{
// Upewnij się, że istnieje dzień ewidencji (właściciel zdarzeń):
var dp = pracownik.DniPracy[data];
if (dp is null)
{
dp = session.AddRow(new DzienPracy(pracownik, data));
kalend.DniPracy.AddRow(dp);
}
// Wejście 8:00
var we = new WejscieWyjscie(dp); // ctor wiąże zdarzenie z dniem
kalend.WejsciaWyjscia.AddRow(we);
we.Godzina = new Time(8, 0);
we.Typ = TypWejsciaWyjscia.Wejscie;
// Wyjście 16:00
var wy = new WejscieWyjscie(dp);
kalend.WejsciaWyjscia.AddRow(wy);
wy.Godzina = new Time(16, 0);
wy.Typ = TypWejsciaWyjscia.Wyjscie;
t.Commit();
}
session.Save();
// --- Odczyt zdarzeń dnia ---
var dzien = pracownik.DniPracy[data];
if (dzien is not null)
{
foreach (WejscieWyjscie wewy in dzien.WeWy) // posortowane po Lp
{
// wewy.Godzina, wewy.Typ, wewy.Operacja
}
}
```
**Pułapki:**
- `WejscieWyjscie` jest childem **`DzienPracy`**, nie `DzienRCP` — najpierw potrzebujesz dnia
ewidencji (`pracownik.DniPracy[data]`); zdarzenia wiążesz ctorem `new WejscieWyjscie(dzienPracy)`
i dodajesz do tabeli `kalend.WejsciaWyjscia.AddRow(...)`.
- `Typ` to enum `TypWejsciaWyjscia` (`Wejscie`/`Wyjscie`/…), nie string ani `int`. Para
wejście+wyjście jest podstawą wyliczenia czasu dnia z surowych zdarzeń.
- `DefinicjaZdarzenia` jest **opcjonalna** — przy ręcznym wpisie wystarczą `Godzina` + `Typ`. Jeśli
używasz definicji, pobierz wpis ze słownika konfiguracyjnego `kalend.DefZdarzenRCP` (nie twórz w locie).
- `WeWy` to `LpSubTable` — kolejność zdarzeń wynika z `Lp` (nadawane automatycznie); nie ustawiaj `Lp`
ręcznie. Do usunięcia wszystkich zdarzeń dnia (przy ponownym imporcie) służy kasowanie elementów kolekcji.
- Surowe zdarzenia są przeliczane na czas pracy/RCP przez kalkulator i import — samo dodanie
wejść/wyjść nie aktualizuje automatycznie `DzienRCP` (to robi przeliczenie/import RCP).
### KADRY-F3 — Import danych z RCP (bezpośredni i przez tabelę pośrednią)
**Cel:** wczytać surowe odbicia z czytników RCP i przeliczyć je na ewidencję/zweryfikowany RCP.
**UWAGA: operacja plikowa/sieciowa — opis modelu; samego importu z pliku/urządzenia NIE testujemy.**
**Model danych:**
| Element | Lokalizacja | Typ | Uwaga |
|---|---|---|---|
| Tabela pośrednia (surowe odbicia) | `DzienPracy.WeWy` | `LpSubTable<Soneta.Kalend.WejscieWyjscie>` | zdarzenia we/wy (godzina, typ) — patrz KADRY-F2 |
| Zarejestrowany RCP (zweryfikowany) | `pracownik.DniRCP[data]` | `Soneta.Kalend.DzienRCP` | wynik importu/przeliczenia |
| Ewidencja | `pracownik.DniPracy[data]` | `Soneta.Kalend.DzienPracy` | docelowa realizacja |
| Flaga importu | `DzienPracy.RcpOK` / `DzienRCP.RcpOK` | `bool` | „stan rekordu po imporcie z RCP" |
| Stan weryfikacji | `DzienRCP.StanRCP` | `StanWeryfikacjiRCP` | patrz KADRY-F4 |
**Workery przeliczające (po wczytaniu odbić — operują na obiektach sesji):**
| Worker | Sygnatura | Rola |
|---|---|---|
| `Soneta.Kalend.ImportDniaWorker` | ctor `()`, `DzienPracy DzienPracy {get;set;}`, `void Przelicz()` | przelicza pojedynczy dzień z we/wy na czas pracy |
| `Soneta.Kalend.RCPWeryfikatorWorker` | `Dopasuj()`, `DopasujDlaZaznaczonych()`, `Dodaj()`, `Usun()` (+ `IsVisible*`), props `rw : RCPWeryfikator`, `Strefy`, `Wybrana` | dopasowanie odbić do plan/strefy (UI) |
**Snippet (przeliczenie dnia z już wczytanych we/wy — bez pliku):**
```csharp
var pracownik = session.GetKadry().Pracownicy.WgKodu["006"];
var dzien = pracownik.DniPracy[new Date(2026, 6, 1)]; // dzień z wpisanymi WeWy (KADRY-F2)
if (dzien is not null)
{
using (var t = session.Logout(editMode: true))
{
new ImportDniaWorker { DzienPracy = dzien }.Przelicz(); // we/wy -> czas pracy
t.Commit();
}
session.Save();
}
```
**Pułapki / wykonalność:**
- **Sam import z pliku/urządzenia (czytnik, sieć, format) jest poza zakresem testu** — wymaga
zewnętrznego źródła (plik/serwis), brak czystego API w tym kontrakcie.
- **Testowalny fragment**: przygotowanie tabeli pośredniej `DzienPracy.WeWy` (ctor `WejscieWyjscie(dp)`,
patrz KADRY-F2) + `ImportDniaWorker.Przelicz()` — to przelicza już-wczytane odbicia bez I/O.
- `RCPWeryfikatorWorker` jest mocno UI (metody `IsVisible*`, `Strefy`/`Wybrana`) — to dopasowanie
ręczne; nie wywoływać z kodu biznesowego.
- `DzienRCP` powstaje z importu/przeliczenia — w teście nie twórz go „z palca"; odczytuj po `Przelicz()`.
---
### KADRY-F4 — Weryfikacja i korekta danych RCP (★ testowalne)
**Cel:** odczytać i skorygować zweryfikowany rekord RCP — zmienić stan weryfikacji oraz poprawić
godziny pracy / opisać błędy i uwagi.
**Pola `Soneta.Kalend.DzienRCP` (tabela `DniRCP`, child `Pracownik`):**
| Pole | Typ | Rodzaj | Uwaga |
|---|---|---|---|
| `Data` | `Soneta.Types.Date` | bazodanowe | data dnia (ctor) |
| `Pracownik` | `Soneta.Kadry.Pracownik` | bazodanowe, guided-parent | właściciel |
| `Praca` | `Soneta.Kalend.CzasPracy` | bazodanowe | `Praca.OdGodziny`/`Praca.DoGodziny`/`Praca.Czas : Time` (zapisywalne) |
| `Czas`, `OdGodziny` | `Soneta.Types.Time` | kalkulowane | read-only (z `Praca`) |
| `StanRCP` | `Soneta.Kalend.StanWeryfikacjiRCP` | bazodanowe | stan weryfikacji (zapisywalne) |
| `RcpOK` | `bool` | bazodanowe | stan rekordu po imporcie (zapisywalne) |
| `Uwagi` | `Soneta.Business.MemoText` | bazodanowe | uwagi do weryfikacji |
| `Bledy` | `Soneta.Business.MemoText` | bazodanowe | opis błędów |
| `Strefy` | `SubTable<Soneta.Kalend.StrefaRCP>` | | strefy zarejestrowane |
| `StrefyOrg` | `Soneta.Business.MemoText` | bazodanowe | strefy źródłowe (org.) |
**`Soneta.Kalend.StanWeryfikacjiRCP` (enum):** `DoWeryfikacji`, `WymagaWeryfikacji`,
`PrzekazanyDoWyjaśnienia`, `DoZatwierdzenia`, `Modyfikowany`, `Naniesiony`, `Poprawny`, `Błędny`,
`Wszystkie`.
**Snippet (korekta godzin + zmiana stanu):**
```csharp
var pracownik = session.GetKadry().Pracownicy.WgKodu["006"];
var dzienRcp = pracownik.DniRCP[new Date(2026, 6, 1)]; // DzienRCP lub null
if (dzienRcp is not null)
{
using (var t = session.Logout(editMode: true))
{
dzienRcp.Praca.OdGodziny = new Time(8, 0); // korekta na subrowie Praca
dzienRcp.Praca.DoGodziny = new Time(16, 0);
dzienRcp.StanRCP = StanWeryfikacjiRCP.Poprawny; // zatwierdzenie weryfikacji
dzienRcp.Uwagi = (MemoText)"Skorygowano wyjście";
t.Commit();
}
session.Save();
}
```
**Pułapki:**
- `DniRCP` jest **typowane** (`DateSubTable<DzienRCP>`) — indeksator `[Date]` zwraca `DzienRCP`/`null`;
do zakresu użyj `[FromTo]`. Nie iteruj kolekcji w poszukiwaniu dnia.
- Godziny koryguj na **subrowie `Praca`** (`Praca.OdGodziny`/`DoGodziny`); `DzienRCP.Czas`/`OdGodziny`
na rootcie są kalkulowane (read-only).
- `StanRCP` to enum `StanWeryfikacjiRCP` — nie string. Zmiana stanu może podlegać weryfikatorom.
- W Demo `DzienRCP` istnieje tylko gdy był import/przeliczenie — test korekty zakłada istniejący dzień
(sprawdzaj `is not null`), nie twórz `DzienRCP` ręcznie.
---
### KADRY-F5 — Rozliczenie pracy hybrydowej / aktualizacja podzielników na podstawie pracy hybrydowej
**Cel:** rozliczyć czas pracy hybrydowej (podział na strefy: stacjonarna / praca zdalna / zdalna
okazjonalna) i zaktualizować podzielniki (elementy rozliczenia czasu pracy / strefy dnia), na
podstawie których naliczane są składniki płacowe i koszty.
**Model danych (publiczny kontrakt):**
| Element | Lokalizacja | Typ | Uwaga |
|---|---|---|---|
| Strefy pracy dnia | `DzienPracy.Strefy` | `SubTable<Soneta.Kalend.StrefaPracy>` | podział dnia na strefy |
| Strefa pracy | `Soneta.Kalend.StrefaPracy` (ctor `(DzienPracy dzien)`) | — | `Definicja : DefinicjaStrefy`, `CzasRozliczany : Time`, `OdGodziny`/`Czas` (kalk.) |
| Definicja strefy | `Soneta.Kalend.DefinicjaStrefy` | konfiguracja | `Typ : TypStrefy`, `Wchodzi`, `Rozliczana`; stałe `Praca_Zdalna`, `PracaZdalnaOkazjonalna : Guid` |
| Dokumenty rozliczenia | `pracownik.RozliczeniaCzasuPracy` | `SubTable<Soneta.Kalend.RozliczenieCzasuPracy>` | dokumenty rozliczenia czasu (podzielniki) |
| Elementy rozliczenia | `pracownik.ElementyRozliczeniaCzasuPracy` | `SubTable<Soneta.Kalend.ElementRozliczeniaCzasuPracy>` | pozycje podzielnika |
| Dokument rozliczenia | `Soneta.Kalend.RozliczenieCzasuPracy` (ctor `(Pracownik, DefinicjaRozliczeniaCzasuPracy)`) | root | `Data`, `Seria`, `Stan : StanyRozliczeniaCzasuPracy` |
| Pozycja rozliczenia | `Soneta.Kalend.ElementRozliczeniaCzasuPracy` (ctor `(RozliczenieCzasuPracy dokument)`) | child | `Definicja : DefinicjaStrefy`, `Data`, `OdGodziny`, `Czas`, `CzasPozostały`/`CzasDostępny`/`Zrealizowane` (kalk.) |
**`Soneta.Kalend.TypStrefy` (enum):** `NieWplywa`, `Zwieksza`, `Zmniejsza`.
**Workery (UI/extendery — praca zdalna/hybrydowa):**
- `Soneta.Kalend.StrefaPracy.PracaZdalnaWorker` (`Strefa : StrefaPracy`) — oznaczenie strefy jako
praca zdalna.
- `Soneta.Kadry.PracaZdalna.DzienStrefaExtWorker`, `ElementRozliczeniaCzasuPracyExtWorker`,
`DzienZestawienieExtender` — extendery zestawień/dni dla pracy zdalnej.
**Snippet (odczyt rozkładu na strefy + dokument rozliczenia):**
```csharp
var pracownik = session.GetKadry().Pracownicy.WgKodu["006"];
// Odczyt podziału dnia na strefy (stacjonarna / zdalna):
var dzien = pracownik.DniPracy[new Date(2026, 6, 1)];
if (dzien is not null)
{
foreach (StrefaPracy s in dzien.Strefy)
{
DefinicjaStrefy def = s.Definicja; // strefa (np. praca zdalna)
Time rozliczany = s.CzasRozliczany; // czas rozliczany w strefie
}
}
// Pozycje podzielnika (elementy rozliczenia czasu pracy):
foreach (ElementRozliczeniaCzasuPracy el in pracownik.ElementyRozliczeniaCzasuPracy)
{
DefinicjaStrefy def = el.Definicja;
Time czas = el.Czas;
}
```
**Pułapki / wykonalność:**
- Rozkład pracy hybrydowej to **strefy** (`DzienPracy.Strefy` / `DefinicjaStrefy` z flagą zdalna) +
dokument `RozliczenieCzasuPracy` z pozycjami `ElementRozliczeniaCzasuPracy` (podzielniki).
- `RozliczenieCzasuPracy` to **root** (ctor `(Pracownik, DefinicjaRozliczeniaCzasuPracy)`) — utworzenie
wymaga istniejącej `DefinicjaRozliczeniaCzasuPracy` z konfiguracji; pozycje ctorem
`new ElementRozliczeniaCzasuPracy(dokument)`. Czysty odczyt jest bezpieczny i bez transakcji.
- Aktualizacja podzielników na podstawie pracy hybrydowej przebiega **głównie przez extendery/UI**
(`DzienStrefaExtWorker`, `ElementRozliczeniaCzasuPracyExtWorker`, `PracaZdalnaWorker`) zależne od
`Context`/wniosków e-pracownika — brak prostego, czystego API operacyjnego.
- Wymaga skonfigurowanych `DefinicjaStrefy` (Praca_Zdalna / PracaZdalnaOkazjonalna) — w Demo strefy
mogą nie być włączone, co czyni budowę rozliczenia kruchą do testu (raczej odczyt niż zapis).
@@ -0,0 +1,479 @@
# KADRY07 — Umowy cywilnoprawne
> Wspólne fakty o typie, podstawowe typy i szablon wzorca: [../kadry.md](../kadry.md).
### KADRY-G1 — Dodawanie umów cywilnoprawnych (zlecenie, o dzieło) (★)
**Cel:** utworzyć dla pracownika umowę cywilnoprawną (zlecenie / o dzieło / ryczałtowa) z kompletem
danych pozwalającym na `Session.Save()`: definicja elementu płacowego (rodzaj umowy), okres, wartość,
sposób rozliczenia i typ wartości (brutto/netto).
**Mechanizm (kluczowy):** `Soneta.Kadry.Umowa` to **root historyczny** (tabela `Umowy`), child
pracownika. Dodanie umowy do tabeli (`Module.Umowy.AddRow(umowa)`) w `OnAdded` **automatycznie tworzy
pierwszy zapis** `Soneta.Kadry.UmowaHistoria` (tabela `UmowaHistorie`) oraz domyślną definicję
elementu, okres, datę i numerację. Dlatego **nie tworzymy `UmowaHistoria` ręcznie** — bezpośrednio po
`AddRow` istnieje już `umowa.Last` (pierwszy zapis), na którym ustawiamy **wartość umowy**.
> **Gdzie co siedzi.** Dane „nagłówkowe" umowy (definicja elementu, okres, sposób rozliczenia, typ
> wartości) są na **roocie** `Umowa`. **Kwota/wartość umowy** jest **historyczna** i siedzi na
> `UmowaHistoria.Wartosc` — ustawiasz ją przez `umowa.Last.Wartosc`. Property `umowa.Brutto`/
> `umowa.Wartosc` na roocie oraz `UmowaHistoria.Brutto` są **wyliczane** (read-only).
**Warianty (rodzaj umowy = `DefinicjaElementu`):**
| Rodzaj umowy | Jak wskazać definicję |
|---|---|
| Zlecenie | `DefElementow[DefinicjaElementu.UmowaZlecenie]` (to też wartość domyślna konfiguracji) |
| O dzieło (20% KUP) | `DefElementow[DefinicjaElementu.Umowa20]` |
| Ryczałtowa | `DefElementow[DefinicjaElementu.UmowaRyczałtowa]` |
| Inna (po kodzie/nazwie) | `DefElementow["<kod>"]` (indeksator string = wg kodu) — pod warunkiem że jej `RodzajZrodla == RodzajŹródłaWypłaty.Umowa` |
**Pola i typy:**
| Pole | Gdzie | Typ | Uwaga |
|---|---|---|---|
| `Element` | `Umowa` (root) | `Soneta.Place.DefinicjaElementu` | definicja elementu = rodzaj umowy; akceptowana tylko gdy `RodzajZrodla == RodzajŹródłaWypłaty.Umowa` |
| `Data` | `Umowa` (root) | `Soneta.Types.Date` | data zawarcia/dokumentu |
| `Okres` | `Umowa` (root) | `Soneta.Types.FromTo` | okres obowiązywania umowy |
| `Tytul` | `Umowa` (root) | `string` | tytuł/temat umowy |
| `RodzajRozliczenia` | `Umowa` (root) | `Soneta.Kadry.RodzajeRozliczeniaUmowy` | `KwotaDoWypłaty` / `StawkaZaOkres` / `StawkaZaGodzinę` |
| `TypWartosci` | `Umowa` (root) | `Soneta.Kadry.TypWartosciUmowy` | `Brutto` / `Netto` |
| `Wydzial` | `Umowa` (root) | `Soneta.Kadry.Wydzial` | jednostka organizacyjna — **wymagana** (weryfikator przy `Save()`); ustaw `kadry.Wydzialy.Firma` |
| `Wartosc` | `UmowaHistoria` (`Last`) | `Soneta.Types.Currency` | **kwota/wartość umowy** — ustawiana na zapisie historycznym |
**Snippet:**
```csharp
var kadry = session.GetKadry();
var place = session.GetPlace();
var pracownik = kadry.Pracownicy.WgKodu["006"];
using (var t = session.Logout(editMode: true))
{
// 1) Utworzenie umowy + dodanie do tabeli; w OnAdded powstaje pierwszy UmowaHistoria.
var umowa = session.AddRow(new Umowa(pracownik));
// 2) Definicja elementu = rodzaj umowy (tu: zlecenie). Indeksator Guid + stała publiczna.
umowa.Element = place.DefElementow[DefinicjaElementu.UmowaZlecenie];
// 3) Dane nagłówkowe na roocie:
umowa.Data = new Date(2026, 1, 1);
umowa.Okres = new FromTo(new Date(2026, 1, 1), new Date(2026, 12, 31));
umowa.Tytul = "Umowa zlecenie - obsługa projektu";
umowa.RodzajRozliczenia = RodzajeRozliczeniaUmowy.KwotaDoWypłaty;
umowa.TypWartosci = TypWartosciUmowy.Brutto;
umowa.Wydzial = kadry.Wydzialy.Firma; // wymagane przy Save()
// 4) KWOTA umowy — na zapisie historycznym Last (UmowaHistoria.Wartosc):
umowa.Last.Wartosc = new Currency(5000m);
t.Commit(); // Commit() w kodzie biznesowym; CommitUI() w workerze/UI
}
session.Save(); // tu wykrywane konflikty/duplikaty
```
**Pułapki:**
- **Nie** twórz ręcznie pierwszego `UmowaHistoria` — robi to `OnAdded` przy `AddRow`. Ręczny nowy
zapis dotyczy dopiero *zmiany/aneksu* „od daty" (KADRY-G2).
- **Kwotę ustawiaj na `umowa.Last.Wartosc`**, nie na roocie — `umowa.Brutto`/`umowa.Wartosc` oraz
`Last.Brutto` są wyliczane (read-only). `Wartosc` to `Soneta.Types.Currency`, nie `decimal`
(safe-code §10.1).
- `Element` przyjmie tylko definicję o `RodzajZrodla == RodzajŹródłaWypłaty.Umowa`. Definicje
„etatowe" (np. `EtatMies`) zostaną zignorowane (umowa dostanie domyślną definicję z konfiguracji).
- Jeśli `Element` **nie** zostanie ustawiony, umowa przyjmuje
`Module.Config.Ogólne.DomyślnaDefinicjaUmowy` (domyślnie = `UmowaZlecenie`).
- Dodanie umowy do pracownika **w archiwum** rzuca wyjątek (`WArchiwumException`) z `OnAdded`.
- `RodzajRozliczenia`/`TypWartosci` bywają w UI tylko-do-odczytu zależnie od definicji elementu —
w kodzie biznesowym ustawiasz je wprost (nie używaj `IsReadOnlyXxx`, safe-code §7.1).
- Całość w transakcji (`session.Logout(editMode: true)`); brak `Commit()` = rollback przy `Dispose()`.
### KADRY-G2 — Zmiana/aneks umowy (★)
**Cel:** zmienić warunki istniejącej umowy. Trzy rozłączne przypadki: **(a) korekta** danych
nagłówkowych w bieżącym okresie; **(b) aneks „od daty"** — nowy zapis historyczny `UmowaHistoria`
obowiązujący od wskazanego dnia (analogicznie do `PracHistoria`, sekcja KADRY-A14); **(c) seryjna
aktualizacja stawki** workerem.
**Mechanizm `HistorySubTable<UmowaHistoria>`** (`umowa.Historia`):
| Operacja | API | Efekt |
|---|---|---|
| Odczyt zapisu na dzień | `umowa[date]` (== `(UmowaHistoria)Historia[date]`) | zapis, którego `Aktualnosc` zawiera `date` |
| Ostatni (bieżący) zapis | `umowa.Last` (== `Historia.GetPrev()`) | najświeższy zapis |
| Pierwszy zapis | `umowa.Historia.GetFirst()` | najstarszy zapis |
| **Nowy zapis „od daty"** | `(UmowaHistoria)umowa.Historia.Update(date)` | **klonuje** zapis aktualny na `date`, skraca stary do `date-1`, zwraca **nowy** klon (okres od `date`); klon trzeba dodać do tabeli |
| Okres obowiązywania | `UmowaHistoria.Aktualnosc: FromTo` | „oddo" zapisu (zarządzany przez historię) |
**(a) Korekta danych nagłówkowych umowy (bez nowego okresu):**
```csharp
var umowa = pracownik.Umowy.First(); // lub wyszukanie po polu/numerze
using (var t = session.Logout(editMode: true))
{
umowa.Tytul = "Umowa zlecenie - aneks zakresu prac";
umowa.Okres = new FromTo(umowa.Okres.From, new Date(2027, 6, 30)); // przedłużenie
t.Commit();
}
session.Save();
```
**(b) Aneks „od daty" — zmiana wartości umowy nowym zapisem historycznym:**
```csharp
var odDnia = new Date(2026, 7, 1);
using (var t = session.Logout(editMode: true))
{
// 1) Update klonuje zapis aktualny na `odDnia` i zwraca nowy klon (okres od `odDnia`);
// stary zapis zostaje skrócony do dnia poprzedniego.
var nowy = (UmowaHistoria)umowa.Historia.Update(odDnia);
// 2) Klon trzeba dodać do tabeli zapisów historii umowy.
umowa.Module.UmowaHistorie.AddRow(nowy);
// 3) Na nowym zapisie ustawiamy zmienioną wartość (od `odDnia`):
nowy.Wartosc = new Currency(6000m);
// Uwaga: UmowaHistoria.PowodAktualizacji jest tylko do odczytu (ustawiane wewnętrznie) — nie przypisuj.
t.Commit();
}
session.Save();
```
**(c) Seryjna aktualizacja stawki — worker `Umowa.AktualizacjaStawkiWorker`:**
```csharp
var worker = new Umowa.AktualizacjaStawkiWorker
{
Umowy = new[] { umowa },
Pars = ... // Umowa.AktualizacjaStawkiWorker.Params: Data: Date, Wartosc: Currency
};
// uruchomienie zgodnie z konwencją workerów (patrz worker-extender.md)
```
> Pokrewne workery (wywoływane jak każdy worker enova): `Umowa.KopiujUmowe2Worker`
> (`Umowa Umowa` — kopiuje umowę), `Umowa.WyrejestrujUmoweWorker` (wyrejestrowanie umowy).
**Pułapki:**
- **`Update(date)` + `UmowaHistorie.AddRow(nowy)` to nierozłączna para.** Sam `Update` tworzy
„odpięty" klon — bez `AddRow` zmiana nie zostanie zapisana.
- `Update(date)` rzuca wyjątek duplikatu, gdy na `date` już zaczyna się zapis (`Aktualnosc.From == date`)
— nie da się „aktualizować" dwa razy tego samego dnia; wtedy modyfikuj istniejący zapis (`umowa[date]`).
- **Korekta** (`umowa.Last.Wartosc = …` lub modyfikacja `umowa[date]`) zmienia dane w **całym** okresie
tego zapisu — używaj jej do poprawy błędu, nie do „zmiany od dnia"; do zmiany od dnia → wariant (b).
- `Aktualnosc` (okres zapisu) jest zarządzany przez historię — **nie ustawiaj go ręcznie**; do
skrócenia/wstawienia okresu służy `Update`.
- Wartość zawsze jako `Soneta.Types.Currency`, nie `decimal` (safe-code §10.1); daty jako
`Soneta.Types.Date`/`Date.Today` (§10.2).
- Obsłuż `RowConflictException` z `Save()` (safe-code §4); transakcje trzymaj krótkie (§13.1).
### KADRY-G3 — Operacja seryjna „Dodaj umowy" dla grupy osób (★)
**Cel:** dodać jednakową umowę cywilnoprawną (zlecenie / o dzieło / ryczałtowa) **naraz dla wielu
zaznaczonych pracowników** — operacja seryjna z listy osób. W UI: menu *Operacje seryjne → Dodaj
umowy…*. Każdej osobie z zaznaczenia tworzona jest osobna `Umowa` z tymi samymi danymi nagłówkowymi
(definicja elementu, okres, wartość, sposób rozliczenia), analogicznie do KADRY-G1.
**Worker (publiczny kontrakt):** `Soneta.Kadry.Pracownik.DodajUmowęWorker` — worker **przypisany do
`Pracownik`** (`DataType = Pracownik`). Udostępnia akcję `DodajUmowę` w menu czynności listy
pracowników.
| Składowa | Typ / sygnatura | Uwaga |
|---|---|---|
| Konstruktor | `DodajUmowęWorker(Session session)` | worker ma **ctor z `Session`** (nie bezparametrowy) |
| Zaznaczone osoby | `DodajUmowęWorker.Pracownicy: Pracownik[]` | `[Context]` — tablica pracowników z zaznaczenia listy |
| Parametry | `DodajUmowęWorker.Pars: DodajUmowęWorker.Params` | `[Context]` — okno parametrów operacji; `Params(Context)` |
| Akcja | `void DodajUmowę()` | tworzy umowy dla wszystkich `Pracownicy` (zwraca **`void`**, nie `object`) |
**Parametry operacji (`DodajUmowęWorker.Params`):**
| Pole | Typ | Odpowiednik na `Umowa` (KADRY-G1) |
|---|---|---|
| `Definicja` | `Soneta.Core.DefinicjaDokumentu` | definicja dokumentu umowy (numeracja/seria) |
| `Seria` | `string` | seria numeracji |
| `Wydział` | `Soneta.Kadry.Wydzial` | `Umowa.Wydzial` (wymagany) |
| `Data` | `Soneta.Types.Date` | `Umowa.Data` (data zawarcia) |
| `Okres` | `Soneta.Types.FromTo` | `Umowa.Okres` |
| `Tytuł` | `string` | `Umowa.Tytul` |
| `Element` | `Soneta.Place.DefinicjaElementu` | `Umowa.Element` (rodzaj umowy) |
| `RodzajRozliczenia` | `Soneta.Kadry.RodzajeRozliczeniaUmowy` | `Umowa.RodzajRozliczenia` |
| `Wartość` | `Soneta.Types.Currency` | `umowa.Last.Wartosc` (kwota umowy) |
| `TypWartości` | `Soneta.Kadry.TypWartosciUmowy` | `Umowa.TypWartosci` (`Brutto`/`Netto`) |
**Wariant A — wywołanie workera platformy (zalecane):** zainicjuj `DodajUmowęWorker`, ustaw
`Pracownicy` i `Pars`, wywołaj `DodajUmowę()` (worker uruchamia się jak każdy worker — patrz
[`worker-extender.md`](../worker-extender.md), sekcja *Programowe użycie workera*).
```csharp
var kadry = session.GetKadry();
var place = session.GetPlace();
// Grupa osób (np. z zaznaczenia listy). Tu: kilku pracowników po kodzie:
var osoby = new[]
{
kadry.Pracownicy.WgKodu["006"],
kadry.Pracownicy.WgKodu["007"],
kadry.Pracownicy.WgKodu["008"],
};
var pars = new Pracownik.DodajUmowęWorker.Params(context);
pars.Element = place.DefElementow[DefinicjaElementu.UmowaZlecenie];
pars.Okres = new FromTo(new Date(2026, 1, 1), new Date(2026, 12, 31));
pars.Data = new Date(2026, 1, 1);
pars.Tytuł = "Umowa zlecenie - projekt grupowy";
pars.RodzajRozliczenia = RodzajeRozliczeniaUmowy.KwotaDoWypłaty;
pars.TypWartości = TypWartosciUmowy.Brutto;
pars.Wartość = new Currency(4000m);
pars.Wydział = kadry.Wydzialy.Firma; // wymagany
// Worker ma konstruktor z Session (nie bezparametrowy); Pracownicy/Pars przez inicjalizator:
var worker = new Pracownik.DodajUmowęWorker(session) { Pracownicy = osoby, Pars = pars };
worker.DodajUmowę(); // void
session.Save();
```
**Wariant B — pętla po pracownikach (jawne tworzenie, jak KADRY-G1):** gdy nie chcesz przechodzić przez
worker — dla każdej osoby twórz `new Umowa(p)` i ustaw te same pola co w KADRY-G1. To jawnie pokazuje, że
operacja seryjna = KADRY-G1 powtórzone w pętli.
```csharp
var kadry = session.GetKadry();
var place = session.GetPlace();
var defElementu = place.DefElementow[DefinicjaElementu.UmowaZlecenie];
var okres = new FromTo(new Date(2026, 1, 1), new Date(2026, 12, 31));
using (var t = session.Logout(editMode: true))
{
foreach (var p in osoby)
{
var umowa = session.AddRow(new Umowa(p)); // OnAdded tworzy pierwszy UmowaHistoria
umowa.Element = defElementu;
umowa.Data = okres.From;
umowa.Okres = okres;
umowa.Tytul = "Umowa zlecenie - projekt grupowy";
umowa.RodzajRozliczenia = RodzajeRozliczeniaUmowy.KwotaDoWypłaty;
umowa.TypWartosci = TypWartosciUmowy.Brutto;
umowa.Wydzial = kadry.Wydzialy.Firma; // wymagany przy Save()
umowa.Last.Wartosc = new Currency(4000m); // kwota na zapisie historycznym
}
t.Commit(); // Commit() w kodzie biznesowym; CommitUI() w workerze/UI
}
session.Save();
```
**Pułapki:**
- `Pracownik.DodajUmowęWorker` jest workerem na typie `Pracownik`, a tworzy obiekty `Umowa` — nie myl
go z workerami na `Umowa` (KADRY-G2: `AktualizacjaStawkiWorker`, `KopiujUmowe2Worker`).
- W wariancie B obowiązują wszystkie pułapki KADRY-G1: kwota na `umowa.Last.Wartosc` (root `Brutto`/`Wartosc`
są wyliczane), `Element` tylko o `RodzajZrodla == RodzajŹródłaWypłaty.Umowa`, `Wydzial` wymagany,
dodanie umowy pracownikowi **w archiwum** rzuca `WArchiwumException`.
- Pętlę edycyjną trzymaj krótką (safe-code §13.1); konflikty/duplikaty wykrywane w `Save()` (§4).
- Wartość zawsze jako `Soneta.Types.Currency`, daty jako `Date`/`FromTo`, nie `decimal`/`DateTime`
(safe-code §10).
---
### KADRY-G4 — Rachunek do umowy zlecenia (★)
**Cel:** wystawić **rachunek do umowy zlecenia** — czyli rozliczyć (naliczyć i wypłacić) umowę
cywilnoprawną. W modelu Soneta „rachunek do umowy zlecenia" **nie jest osobnym rekordem** na `Umowa`
ani w `pracownik.Rachunki` — to **wypłata z umowy** typu `Soneta.Place.WyplataUmowa`, naliczana
mechanizmem płac (jak KADRY-H2). `pracownik.Rachunki: SubTable<Soneta.Kasa.RachunekBankowyPodmiotu>` to
**rachunki bankowe** pracownika (numer konta), a nie rachunki do umów — nie myl tych pojęć.
> **Gdzie to siedzi.** Każda umowa ma wstecz powiązane rozliczenia/wypłaty:
> - `Umowa.RozliczeniaWynagrodzenia: LpSubTable<Soneta.Place.RozliczenieWynagrodzenia>` — rozliczenia
> wynagrodzenia z umowy,
> - `Umowa.Elementy: SubTable<Soneta.Place.WypElement>` — naliczone składniki wypłat tej umowy,
> - sama wypłata to `Soneta.Place.WyplataUmowa` (konkretny typ `Wyplata`), z polem zwrotnym
> `WyplataUmowa.Umowa: Soneta.Kadry.Umowa`.
> Stan rozliczenia umowy odczytasz z `Umowa.Stan: Soneta.Kadry.StanUmowy`
> (`Niewypłacona` / `WypłaconaCzęściowo` / `WypłaconaCałkowicie` / `Anulowana`) oraz z
> `Umowa.Splacono`, `Umowa.Pozostało` (`Soneta.Types.Currency`).
**Tworzenie rachunku (wypłaty) do umowy — wykonawca naliczania `NaliczanieSeryjne.Umowy` (jak KADRY-H2):**
| Element | Typ / sygnatura | Uwaga |
|---|---|---|
| Parametry | `new NaliczanieSeryjne.UmowaParams(context)` | `Naliczanie` na sztywno `PłatnaZDołu` (setter rzuca `NotSupportedException`) |
| Data rachunku / listy | `UmowaParams.DataWypłaty`, `.DataListy` | daty rachunku |
| Wykonawca | `new NaliczanieSeryjne.Umowy(UmowaParams) { Umowa = umowa }` | ustawienie `Umowa` ustawia też `Pracownik` z `umowa.Pracownik` |
| Uruchomienie | `Umowy.Nalicz(): NaliczanieWypłat` | tworzy `WyplataUmowa` i liczy składniki |
| Wynik | `NaliczanieWypłat.WszystkieWypłaty: IList` | elementy `Wyplata` (tu `WyplataUmowa`) |
```csharp
var kadry = session.GetKadry();
var pracownik = kadry.Pracownicy.WgKodu["006"];
// Umowa zlecenie pracownika (np. utworzona w KADRY-G1):
Umowa umowa = pracownik.Umowy.Cast<Umowa>()
.First(u => u.Element == session.GetPlace().DefElementow[DefinicjaElementu.UmowaZlecenie]);
var pars = new NaliczanieSeryjne.UmowaParams(context);
pars.DataWypłaty = new Date(2026, 2, 10); // data wystawienia rachunku
pars.DataListy = pars.DataWypłaty;
var naliczanie = new NaliczanieSeryjne.Umowy(pars) { Umowa = umowa };
NaliczanieWypłat wynik = naliczanie.Nalicz(); // tworzy WyplataUmowa (rachunek)
foreach (Wyplata w in wynik.WszystkieWypłaty)
{
// w.Typ == TypWyplaty.Umowa; w to WyplataUmowa; w.Elementy = składniki rachunku
}
session.Save();
// Po naliczeniu — stan rozliczenia umowy:
StanUmowy stan = umowa.Stan; // np. WypłaconaCałkowicie
Currency splacono = umowa.Splacono; // kwota rozliczona
Currency pozostalo = umowa.Pozostało; // pozostała do wypłaty
```
**Odczyt rachunków (wypłat) wystawionych do umowy:**
```csharp
// Wypłaty (rachunki) tej umowy — przez wypłaty pracownika filtrowane po umowie:
foreach (WyplataUmowa w in pracownik.Wyplaty.OfType<WyplataUmowa>().Where(x => x.Umowa == umowa))
{
// w.Data, w.Elementy (WypElement.Wartosc / .Netto / .Podatki.*)
}
// Składniki naliczone bezpośrednio z umowy:
foreach (WypElement e in umowa.Elementy)
{
// e.Wartosc, e.Netto
}
```
**Pułapki:**
- „Rachunek do umowy zlecenia" = `WyplataUmowa`, a nie rekord w `pracownik.Rachunki` (to rachunki
bankowe). Tworzysz go naliczaniem (`NaliczanieSeryjne.Umowy.Nalicz()`), nie `AddRow` po wypłacie.
- **Nie ustawiaj `UmowaParams.Naliczanie`** — umowy są zawsze „płatne z dołu" (setter rzuca
`NotSupportedException`).
- Ustawienie `Umowy.Umowa` nadpisuje `Pracownik` właścicielem umowy — nie ustawiaj `Pracownik` ręcznie.
- `Nalicz()` wewnętrznie otwiera własną transakcję i zatwierdza zmiany w sesji — po nim wołasz tylko
`Session.Save()`; nie owijaj go w dodatkowy `Logout(editMode: true)`.
- `Wyplata` nie ma agregatów `Brutto`/`Netto` — sumuj składniki z `Wyplata.Elementy` (jak w KADRY-H2/KADRY-H4).
- Kwoty jako `Soneta.Types.Currency`, daty jako `Date` (safe-code §10).
---
### KADRY-G5 — Zgłoszenia ZUS zleceniobiorców (ZUA / ZZA na podstawie umowy) (★)
**Cel:** przygotować zgłoszenie zleceniobiorcy do ZUS — **ZUA** (zgłoszenie do ubezpieczeń
społecznych + zdrowotnego) albo **ZZA** (tylko zdrowotne) — **na podstawie schematu ubezpieczeń
umowy**, oraz wyrejestrowanie (**ZWUA**) po jej zakończeniu. O tym, czy powstaje ZUA czy ZZA, decyduje
**schemat ubezpieczeń zapisu umowy**, a nie odrębne pole „rodzaj zgłoszenia".
> **Schemat ubezpieczeń umowy — gdzie siedzi.** Ubezpieczenia umowy są **historyczne** i leżą na
> zapisie `UmowaHistoria.Ubezpieczenia: Soneta.Kadry.Ubezpieczenia` (analogicznie do
> `PracHistoria.Etat.Ubezpieczenia` z KADRY-A7; ta sama struktura `Spoleczne`/`Zdrowotne`). Z roota dostępne
> przez `umowa.Last.Ubezpieczenia` (oraz `umowa.Ubezpieczenia` jako delegat). Kluczowe pola:
> - `Ubezpieczenia.Tyub4: Soneta.Kadry.TytulUbezpieczenia4` — **tytuł ubezpieczenia** (decyduje o ZUA
> vs ZZA); pobierany ze słownika `session.GetKadry().TytulyUbezpiecz4.WgKodu[int]`,
> - `Ubezpieczenia.ObowiazkoweOd: Date` — data objęcia ubezpieczeniami społecznymi obowiązkowymi,
> - `Ubezpieczenia.Emerytalne` / `Rentowe` / `Chorobowe` / `Wypadkowe : Soneta.Kadry.Spoleczne` —
> poszczególne społeczne (`Obowiazkowe`, `Dobrowolne`, `DobrowolneOd`, `Do`; `Od` read-only),
> - `Ubezpieczenia.Zdrowotne: Soneta.Kadry.Zdrowotne` — zdrowotne (`ObowiazkoweOd` zapisywalne).
>
> **Reguła ZUA vs ZZA:** zleceniobiorca podlegający ubezpieczeniom **społecznym** (emerytalne/rentowe
> obowiązkowe) → **ZUA**; podlegający **tylko zdrowotnemu** (np. uczeń/student do 26 r.ż., zbieg
> tytułów) → **ZZA**. Worker rozpoznaje to automatycznie po schemacie `UmowaHistoria.Ubezpieczenia`.
>
> **Uwaga (zweryfikowane testem):** świeży zapis ubezpieczeń umowy zlecenie ma **domyślnie**
> `Emerytalne.Obowiazkowe == true` i `Rentowe.Obowiazkowe == true` (schemat ZUA). Aby uzyskać **ZZA**,
> trzeba je **jawnie wyłączyć** (`ub.Emerytalne.Obowiazkowe = false; ub.Rentowe.Obowiazkowe = false;`)
> — samo ustawienie `Zdrowotne.ObowiazkoweOd` nie wystarcza.
**Worker (publiczny kontrakt):** `Soneta.Deklaracje.ZUS.ZarejestrujUmowyWorker` — worker
**przypisany do `Umowa`** (`DataType = Umowa`). Operuje na zaznaczonych umowach i generuje deklaracje
zgłoszeniowe ZUS. W UI: menu czynności listy umów *Deklaracje ZUS → Przygotuj ZUA i ZZA* (oraz
wyrejestrowanie).
| Składowa | Typ / sygnatura | Uwaga |
|---|---|---|
| Worker | `ZarejestrujUmowyWorker()` | ctor **bezparametrowy**; `Umowy: Umowa[]` jest **set-only** (ustaw przez inicjalizator) |
| Zaznaczone umowy | `ZarejestrujUmowyWorker.Umowy: Umowa[]` | `[Context]` — umowy do zgłoszenia (write-only) |
| Akcja: zgłoszenie | `object ZarejestrujUmowyWorker.Rejestracja.ZarejestrujUmowy()` | tworzy ZUA/ZZA (i ZCNA dla rodziny — `Pars.ZarejestrujRodzinę`); `Rejestracja()` ctor bezparam. |
| Akcja: wyrejestrowanie | `object ZarejestrujUmowyWorker.Wyrejestrowanie.WyrejestrujUmowy()` | tworzy ZWUA |
| Parametry zgłoszenia | `Rejestracja.Pars: ParamsZ` | **set-only**; `ParamsZ(Context)`; pola bazowe `Okres`/`DataDokumentu`/`DataWypełnienia`/`Kedu` (write-only) + własne `ZarejestrujRodzinę: bool` |
| Parametry wyrejestrowania | `Wyrejestrowanie.Pars: ParamsW` | **set-only**; `ParamsW(Context)`; `Okres`/`DataDokumentu`/`DataWypełnienia`/`Kedu` + `WyrejestrujRodzinę: bool` |
**Wspólny kontrakt bazowy `ZarejestrujBaseWorker`** (do odczytu wyniku i sterowania okresem):
| Pole / metoda | Typ / sygnatura | Uwaga |
|---|---|---|
| `Okres` | `Soneta.Types.FromTo` | okres deklaracji |
| `DataDokumentu`, `DataWypełnienia` | `Soneta.Types.Date` | daty na dokumencie |
| `KEDU` | `Soneta.Deklaracje.ZUS.KEDU` | zestaw dokumentów ZUS, do którego trafiają wygenerowane bloki |
| `Deklaracje` | `System.Collections.Generic.IList` | wygenerowane deklaracje (do odczytu po akcji) |
| `CzyJestZUA()`, `CzyJestZZA()` | — | rozpoznanie typu zgłoszenia ze schematu ubezpieczeń |
**Schemat ubezpieczeń umowy + zgłoszenie ZUA/ZZA:**
```csharp
var kadry = session.GetKadry();
var pracownik = kadry.Pracownicy.WgKodu["006"];
Umowa umowa = pracownik.Umowy.Cast<Umowa>().First();
// 1) Schemat ubezpieczeń umowy (historyczny) — ZUA: społeczne obowiązkowe + zdrowotne:
using (var t = session.Logout(editMode: true))
{
var ub = umowa.Last.Ubezpieczenia; // UmowaHistoria.Ubezpieczenia
ub.Tyub4 = kadry.TytulyUbezpiecz4.WgKodu[kodTytulu]; // tytuł zleceniobiorcy (klucz int, ze słownika)
ub.ObowiazkoweOd = umowa.Okres.From; // data objęcia społecznymi
ub.Emerytalne.Obowiazkowe = true;
ub.Rentowe.Obowiazkowe = true;
ub.Zdrowotne.ObowiazkoweOd = umowa.Okres.From;
// (ZZA = tylko zdrowotne: JAWNIE ustaw Emerytalne.Obowiazkowe = false i Rentowe.Obowiazkowe = false
// — domyślnie są true; samo zdrowotne nie wystarcza)
t.Commit();
}
session.Save();
// 2) Zgłoszenie ZUA/ZZA na podstawie umowy — worker (DataType Umowa):
// Uwaga: Umowy oraz Pars są SET-ONLY (brak gettera) — ustawiamy je przez inicjalizator,
// a parametry budujemy jako osobny obiekt ParamsZ(context) i przypisujemy do Pars.
var worker = new Soneta.Deklaracje.ZUS.ZarejestrujUmowyWorker { Umowy = new[] { umowa } };
var pars = new Soneta.Deklaracje.ZUS.ZarejestrujBaseWorker.ParamsZ(context);
pars.Okres = new FromTo(umowa.Okres.From, Date.MaxValue);
pars.DataDokumentu = umowa.Okres.From;
pars.DataWypełnienia = Date.Today;
pars.ZarejestrujRodzinę = false;
var rejestracja = new Soneta.Deklaracje.ZUS.ZarejestrujUmowyWorker.Rejestracja { Pars = pars };
rejestracja.ZarejestrujUmowy(); // generuje ZUA lub ZZA wg schematu ubezpieczeń umowy
session.Save();
```
**Wyrejestrowanie po zakończeniu umowy (ZWUA):**
```csharp
var parsW = new Soneta.Deklaracje.ZUS.ZarejestrujBaseWorker.ParamsW(context);
parsW.Okres = new FromTo(umowa.Okres.To, umowa.Okres.To);
parsW.DataDokumentu = umowa.Okres.To;
parsW.DataWypełnienia = Date.Today;
var wyrej = new Soneta.Deklaracje.ZUS.ZarejestrujUmowyWorker.Wyrejestrowanie { Pars = parsW };
wyrej.WyrejestrujUmowy(); // generuje ZWUA
session.Save();
```
**Pułapki:**
- **Typ zgłoszenia (ZUA vs ZZA) wynika ze schematu `UmowaHistoria.Ubezpieczenia`**, nie z parametru
workera — ustaw poprawnie `Tyub4` + flagi `Spoleczne.Obowiazkowe`/`Zdrowotne` **przed** zgłoszeniem.
- `Ubezpieczenia` jest **historyczne** — zmiana schematu „od daty" to nowy zapis `UmowaHistoria`
(`umowa.Historia.Update(date)` + `UmowaHistorie.AddRow`, jak KADRY-G2/KADRY-A14), nie nadpisywanie bieżącego.
- `Spoleczne.Od` jest **tylko do odczytu** (wyliczane) — datę objęcia społecznymi obowiązkowymi
ustawiasz zbiorczo przez `Ubezpieczenia.ObowiazkoweOd`; na `Zdrowotne` `ObowiazkoweOd` jest
zapisywalne wprost (asymetria — jak w KADRY-A7).
- `Tyub4` to rekord **konfiguracyjnego** słownika `TytulyUbezpiecz4`, klucz `WgKodu[int]` — pobierz
istniejący tytuł zleceniobiorcy, nie twórz „w locie".
- `ZarejestrujUmowyWorker` jest na `Umowa` (umowy), a `ZarejestrujPracownikówWorker` na `Pracownik`
(etatowi) — do zleceniobiorców używaj wersji „Umowy".
- Workery deklaracji uruchamiaj jak każdy worker enova (Context z tej samej sesji); po akcji wołasz
`Session.Save()`. Obsłuż `RowConflictException` z `Save()` (safe-code §4).
- `ZarejestrujRodzinę`/`WyrejestrujRodzinę` sterują dołączeniem ZCNA dla członków rodziny
(`pracownik.Rodzina`, KADRY-A9) — dla zleceniobiorcy zgłoszenie rodziny działa analogicznie do etatu.
@@ -0,0 +1,751 @@
# KADRY08 — Płace — naliczanie wypłat
> Wspólne fakty o typie, podstawowe typy i szablon wzorca: [../kadry.md](../kadry.md).
> **Model danych.** `Wyplata` (`Soneta.Place.Wyplata`) jest klasą **abstrakcyjną**, root `GuidedRow`,
> tabela `Wyplaty`. Konkretne typy: `WyplataEtat` (etat), `WyplataUmowa` (umowy), `WyplataInne`
> (pozostałe). Każda wypłata należy do jednej **listy płac** (`ListaPlac`, tabela `ListyPlac`) i do
> jednego pracownika. Składniki wynagrodzenia to **elementy** (`WypElement`, tabela `WypElementy`,
> root guided) w kolekcji `Wyplata.Elementy: SubTable<WypElement>`.
>
> **Naliczanie** realizuje publiczny worker `Soneta.Place.NaliczanieSeryjne` (klasa abstrakcyjna
> `partial`) z zagnieżdżonymi klasami:
> - parametry: `NaliczanieSeryjne.Params` (bazowa), `NaliczanieSeryjne.PracownikParams : Params`
> (etat + pozostałe), `NaliczanieSeryjne.UmowaParams : Params` (umowy);
> - wykonawcy: `NaliczanieSeryjne.Pracownika : NaliczanieSeryjne` (wypłaty pracownika),
> `NaliczanieSeryjne.Umowy : NaliczanieSeryjne` (wypłaty z umów).
>
> Wynik to obiekt `Soneta.Place.NaliczanieWypłat` z kolekcją `WszystkieWypłaty: IList` (elementy są
> typu `Wyplata`). **Naliczanie samo zatwierdza zmiany w sesji** (`Nalicz()` wewnętrznie otwiera i
> commituje transakcję edycyjną na sesji pracownika) — utrwalenie w bazie wymaga osobnego
> `session.Save()`.
### KADRY-H1 — Naliczanie wypłat etatowych (★)
**Cel:** naliczyć wypłatę etatową (wynagrodzenie zasadnicze etatu + dodatki/potrącenia) dla jednego
pracownika za wskazany okres rozliczeniowy.
**Klasy, pola i typy:**
| Element | Typ / sygnatura | Uwaga |
|---|---|---|
| Parametry | `new NaliczanieSeryjne.PracownikParams(Context)` | ctor przyjmuje `Context` (sesja operacyjna) |
| Data wypłaty | `PracownikParams.DataWypłaty: Date` | ustawienie **automatycznie** wylicza `Okres` (z konfiguracji listy) i `MiesiącDeklaracji` |
| Data listy | `PracownikParams.DataListy: Date` | data dokumentu listy płac |
| Okres naliczania | `PracownikParams.Okres: FromTo` | zwykle wyliczony z `DataWypłaty`; można nadpisać |
| Typ naliczenia | `PracownikParams.Naliczanie: TypNaliczenia` | `PłatnaZGóry`/`PłatnaZDołu`; **domyślnie `PłatnaZDołu`** — patrz Pułapki (licencja) |
| Filtr typu wypłaty | `PracownikParams.TypWypłaty: TypWyplaty` | `Wszystkie`/`Etat`/`Umowa`/`Inne` — dla etatu `Etat` lub `Wszystkie` |
| Wykonawca | `new NaliczanieSeryjne.Pracownika(PracownikParams)` | |
| Pracownik | `NaliczanieSeryjne.Pracownika.Pracownik: Pracownik` | komu naliczamy (z tej samej sesji co `Context`) |
| Uruchomienie | `NaliczanieSeryjne.Pracownika.Nalicz(): NaliczanieWypłat` | nalicza i zatwierdza w sesji |
| Wynik | `NaliczanieWypłat.WszystkieWypłaty: IList` (elementy `Wyplata`) | naliczone wypłaty |
| Błędy naliczania | `NaliczanieWypłat.Nienaliczeni: IEnumerable<BłądNaliczaniaWynagrodzenia>` | pracownicy, dla których się nie udało |
`TypNaliczenia` (`Soneta.Place`): `PłatnaZGóry = 1`, `PłatnaZDołu = 2`.
`TypWyplaty` (`Soneta.Place`): `Wszystkie = 0`, `Etat = 1`, `Umowa = 2`, `Inne = 3`.
**Snippet:**
```csharp
var place = session.GetPlace();
var pracownik = session.GetKadry().Pracownicy.WgKodu["006"];
// Parametry naliczania — Context z tej samej sesji co pracownik:
var pars = new NaliczanieSeryjne.PracownikParams(context);
pars.DataWypłaty = new Date(2024, 6, 28); // ustawia Okres i MiesiącDeklaracji automatycznie
pars.DataListy = pars.DataWypłaty;
// pars.Naliczanie pozostaje domyślnie PłatnaZDołu (nie ustawiamy — patrz Pułapki)
pars.TypWypłaty = TypWyplaty.Etat; // tylko wypłaty etatowe
var naliczanie = new NaliczanieSeryjne.Pracownika(pars) { Pracownik = pracownik };
NaliczanieWypłat wynik = naliczanie.Nalicz(); // nalicza + commit w sesji
foreach (Wyplata w in wynik.WszystkieWypłaty)
{
// w.Pracownik, w.ListaPlac, w.Data, w.MiesiacDeklaracji, w.Wartosc (Currency, do wypłaty)
}
session.Save(); // utrwalenie w bazie (opcjonalne — bez tego zmiany żyją tylko w sesji)
```
**Pułapki:**
- **`Context` musi pochodzić z tej samej sesji co pracownik.** `PracownikParams(Context)` wiąże się z
`Context.Session`; pracownik pobrany z innej sesji spowoduje niespójność.
- **Nie ustawiaj `Naliczanie` jawnie, jeśli nie masz pewności co do licencji.** Setter
`Params.Naliczanie` rzuca wyjątek, gdy licencja nie jest „PL Złoty/Platynowy" — getter wtedy i tak
zwraca `PłatnaZDołu`. Pozostawienie wartości domyślnej (`PłatnaZDołu`) jest bezpieczne.
- `Nalicz()` **otwiera własną transakcję** na sesji pracownika i commituje ją — **nie owijaj** wywołania
w dodatkowy `session.Logout(true)`. Po naliczeniu zmiany są w sesji; do bazy idą dopiero w `Save()`.
- `WszystkieWypłaty` to `IList` nietypowana — iteruj jako `foreach (Wyplata w in ...)`.
- Pracownik w archiwum (`Pracownik.ArchiwumInfo == InformacjeOArchiwum.WArchiwum`) jest pomijany —
`WszystkieWypłaty` będzie puste, bez wyjątku.
- Naliczanie to operacja na danych operacyjnych — sprawdź `wynik.Nienaliczeni` zamiast łapać ogólny
wyjątek; przy `KontynacjaNaliczenia` (tryb seryjny) błędy lądują tam, a nie w `throw`.
### KADRY-H2 — Naliczanie wypłat z umów (★)
**Cel:** naliczyć wypłatę z konkretnej umowy cywilnoprawnej (`Soneta.Kadry.Umowa`).
**Klasy, pola i typy:**
| Element | Typ / sygnatura | Uwaga |
|---|---|---|
| Parametry | `new NaliczanieSeryjne.UmowaParams(Context)` | jak `PracownikParams`, ale `Naliczanie` jest na sztywno `PłatnaZDołu` (setter rzuca `NotSupportedException`) |
| Data wypłaty / listy / okres | `UmowaParams.DataWypłaty`, `.DataListy`, `.Okres` | jak w KADRY-H1 |
| Wykonawca | `new NaliczanieSeryjne.Umowy(UmowaParams)` | w ctorze ustawia `TypWypłaty = Umowa` |
| Umowa | `NaliczanieSeryjne.Umowy.Umowa: Umowa` | ustawienie umowy ustawia też `Pracownik` z `umowa.Pracownik` |
| Uruchomienie | `NaliczanieSeryjne.Umowy.Nalicz(): NaliczanieWypłat` | |
**Snippet:**
```csharp
var pracownik = session.GetKadry().Pracownicy.WgKodu["006"];
Umowa umowa = pracownik.Umowy.Cast<Umowa>().First(); // przykładowa umowa pracownika
var pars = new NaliczanieSeryjne.UmowaParams(context);
pars.DataWypłaty = new Date(2024, 6, 28);
pars.DataListy = pars.DataWypłaty;
var naliczanie = new NaliczanieSeryjne.Umowy(pars) { Umowa = umowa };
NaliczanieWypłat wynik = naliczanie.Nalicz();
foreach (Wyplata w in wynik.WszystkieWypłaty)
{
// w.Typ == TypWyplaty.Umowa; w.Wartosc; w.Elementy
}
session.Save();
```
**Pułapki:**
- **Nie ustawiaj `UmowaParams.Naliczanie`** — setter rzuca `NotSupportedException` (umowy zawsze
„płatne z dołu").
- Ustawienie `Umowy.Umowa` nadpisuje `Pracownik` na właściciela umowy — nie ustawiaj `Pracownik` ręcznie.
- Pozostałe pułapki jak w KADRY-H1 (Context z tej samej sesji, własna transakcja w `Nalicz()`, `Save()`).
### KADRY-H3 — Naliczanie pozostałych wypłat (★)
**Cel:** naliczyć wypłaty „pozostałe" — pojedynczy dodatek/potrącenie (np. premia, zasiłek
jednorazowy) poza zasadniczym wynagrodzeniem etatu, bądź wypłaty typu `Inne`.
**Mechanizm:** używamy tego samego wykonawcy co KADRY-H1 — `NaliczanieSeryjne.Pracownika` — sterując
zakresem przez `PracownikParams`:
- `PracownikParams.TypWypłaty = TypWyplaty.Inne` — naliczanie tylko składników typu „inne",
- `PracownikParams.Dodatek: DefinicjaElementu`**zawężenie do jednej definicji** dodatku/potrącenia
(naliczany jest tylko wskazany składnik).
**Pola i typy:**
| Element | Typ / sygnatura | Uwaga |
|---|---|---|
| Filtr typu | `PracownikParams.TypWypłaty: TypWyplaty` | `Inne` — pozostałe; `Wszystkie` — łącznie z etatem |
| Pojedynczy składnik | `PracownikParams.Dodatek: DefinicjaElementu` | definicja konkretnego dodatku/potrącenia; `null` = bez zawężenia |
**Snippet:**
```csharp
var place = session.GetPlace();
var pracownik = session.GetKadry().Pracownicy.WgKodu["006"];
// Definicja konkretnego dodatku/potrącenia (rekord konfiguracyjny):
DefinicjaElementu defDodatku = place.DefElementow.WgKodu["PREMIA"]; // przykładowy kod
var pars = new NaliczanieSeryjne.PracownikParams(context);
pars.DataWypłaty = new Date(2024, 6, 28);
pars.DataListy = pars.DataWypłaty;
pars.TypWypłaty = TypWyplaty.Inne; // pozostałe wypłaty
pars.Dodatek = defDodatku; // tylko ten składnik
var naliczanie = new NaliczanieSeryjne.Pracownika(pars) { Pracownik = pracownik };
NaliczanieWypłat wynik = naliczanie.Nalicz();
foreach (Wyplata w in wynik.WszystkieWypłaty)
{
foreach (WypElement e in w.Elementy)
{
// e.Definicja, e.Nazwa, e.Wartosc (decimal), e.Okres
}
}
session.Save();
```
**Pułapki:**
- `Dodatek` to rekord **konfiguracyjny** `DefinicjaElementu` — pobierz istniejącą definicję
(np. przez klucz kodu w `place.DefElementow`), nie twórz „w locie".
- `TypWyplaty.Inne` i `TypWyplaty.Etat` są rozłączne — by naliczyć etat + dodatki łącznie użyj
`Wszystkie`.
- Pozostałe pułapki jak w KADRY-H1.
### KADRY-H4 — Przeglądanie/odczyt wypłat za rok (★)
**Cel:** odczytać naliczone wypłaty pracownika za dany rok i zagregować wartości (suma do wypłaty,
brutto/netto/składki/podatek, sumy składników).
**Dostęp do wypłat (publiczny kontrakt):**
| Punkt wejścia | Typ | Uwaga |
|---|---|---|
| `pracownik.Wyplaty` | `SubTable<Wyplata>` | wszystkie wypłaty pracownika (klucz `WgPracownik`) |
| `session.GetPlace().Wyplaty.WgPracownik[pracownik]` | `SubTable<Wyplata>` | równoważnie z modułu |
| `session.GetPlace().Wyplaty.WgData[date]` | `SubTable<Wyplata>` | wypłaty z datą `date` |
| `listaPlac.Wyplaty` | `SubTable<Wyplata>` | wypłaty danej listy płac |
**Pola wypłaty (`Wyplata`) do odczytu:**
| Pole | Typ | Opis |
|---|---|---|
| `Pracownik` | `Pracownik` | właściciel |
| `ListaPlac` | `ListaPlac` | lista płac (`ListaPlac.Okres: FromTo`, `ListaPlac.DataWyplaty: Date`, `ListaPlac.Zatwierdzona: bool`) |
| `Data` | `Date` | data wypłaty (klucz `WgData`) |
| `MiesiacDeklaracji` | `YearMonth` | miesiąc rozliczenia PIT |
| `MiesiacZUS` | `YearMonth` | miesiąc rozliczenia ZUS |
| `Wartosc` | `Currency` | kwota **do wypłaty** (netto) w PLN |
| `Numer` | `NumerDokumentu` | numer dokumentu (`Numer.NumerPelny`) |
| `Typ` | `TypWyplaty` | etat / umowa / inne |
| `Bufor` | `bool` | wypłata w buforze (niezatwierdzona) |
| `Elementy` | `SubTable<WypElement>` | składniki wynagrodzenia |
**Kwoty na poziomie wypłaty (`Soneta.Place.Wyplata`, typ `Soneta.Types.Currency`):** `Wartosc`
(kwota **do wypłaty**, PLN), `WartoscCy` (w walucie listy), `DoWypłaty`, `Gotówka`, `Inne`.
Aby otrzymać `decimal`, użyj **`.Value`** (`w.Wartosc.Value`) — `Currency` nie ma jawnego rzutowania
na `decimal`.
> **Uwaga:** `Wyplata`/`WyplataEtat` **nie udostępnia** publicznych agregatów typu `Brutto`, `Netto`,
> `SkładkiZUS`, `Podatek` jako gotowych właściwości. Brutto/netto/składki/podatek **liczymy sumując
> składniki** z kolekcji `Wyplata.Elementy` (`WypElement.Wartosc`, `WypElement.Netto`, `WypElement.Podatki.*`).
**Składniki (`WypElement`) i ich struktura podatkowo-składkowa:**
| Pole | Typ | Opis |
|---|---|---|
| `Definicja` | `DefinicjaElementu` | definicja składnika |
| `Nazwa` | `string` | nazwa składnika |
| `Wartosc` | `decimal` | wartość składnika |
| `Okres` | `FromTo` | okres, za który naliczono |
| `Podatki` | `Podatki` (subrow) | struktura podatków/składek |
| `Podatki.PodstawaZUS` | `decimal` | podstawa ZUS |
| `Podatki.Emerytalna` / `Rentowa` / `Chorobowa` / `Wypadkowa` / `Zdrowotna` | `SkladkaZUS` (subrow) | każda z polami `Podstawa`, `Prac`, `Firma: decimal` |
| `Podatki.Koszty`, `Podatki.Ulga`, `Podatki.ZalFIS` | `decimal` | koszty, ulga, zaliczka PIT |
**Snippet:**
```csharp
var place = session.GetPlace();
var pracownik = session.GetKadry().Pracownicy.WgKodu["006"];
int rok = 2024;
var od = new Date(rok, 1, 1);
var doD = new Date(rok, 12, 31);
// Filtr serwerowy po dacie wypłaty (zakres roku) — bez pełnego skanu:
decimal sumaDoWypłaty = 0m;
decimal sumaBrutto = 0m;
foreach (Wyplata w in pracownik.Wyplaty[(Wyplata x) => x.Data >= od && x.Data <= doD])
{
sumaDoWypłaty += w.Wartosc.Value; // kwota do wypłaty (Currency -> decimal przez .Value)
// brutto/składki/podatek liczymy z elementów (nie ma gotowych agregatów na wypłacie):
foreach (WypElement e in w.Elementy)
{
sumaBrutto += e.Wartosc; // WypElement.Wartosc to decimal
decimal netto = e.Netto;
decimal podstawaZUS = e.Podatki.PodstawaZUS;
decimal zaliczkaPit = e.Podatki.ZalFIS;
}
}
```
**Pułapki:**
- `Wyplaty` to tabela **operacyjna guided** — zawsze ograniczaj zakresem czasowym (rok), nie iteruj
całości (`safe-code §6.3`). Filtruj serwerowo przez `SubTable[condition]` po `Data`, nie w pamięci.
- `Wartosc` to `Currency` (kwota do wypłaty); konwersja na `decimal` przez `.Value`. Składnik
`WypElement.Wartosc`/`WypElement.Netto` to już `decimal` — nie myl typów ani znaczeń.
- **Nie ma** gotowych właściwości agregujących (`Brutto`/`Netto`/`SkładkiZUS`/`Podatek`) na `Wyplata`
ani `WyplataEtat` — sumuj składniki z `Wyplata.Elementy` (i ich `Podatki.*`).
- `SkladkaZUS` ma pola `Podstawa`, `Prac`, `Firma` (część pracownika i pracodawcy) oraz właściwość
pomocniczą `Składka` (suma) — wybierz właściwą do potrzeb.
- Filtruj po `Data` (data wypłaty) lub `MiesiacDeklaracji`/`MiesiacZUS` zależnie od potrzeby
raportowej — to różne pojęcia roku (rok wypłaty vs rok deklaracji).
### KADRY-H5 — Odczyt elementów wypłaty (brutto/składki/podatek/netto) (★)
**Cel:** odczytać składniki konkretnej **naliczonej** wypłaty (`Soneta.Place.Wyplata`) i wyliczyć
agregaty: brutto, składki ZUS (część pracownika i firmy), zaliczka PIT, netto.
**Model.** Składniki to `Wyplata.Elementy: SubTable<WypElement>` (`Soneta.Place.WypElement`, tabela
operacyjna guided `WypElementy`). `Wyplata` **nie** ma gotowych agregatów `Brutto`/`Netto`/`SkładkiZUS`/
`Podatek` — liczymy je z elementów albo przez worker `Wyplata.PITInfoWorker` (patrz niżej).
**Pola składnika `WypElement` (do odczytu):**
| Pole | Typ | Opis |
|---|---|---|
| `Definicja` | `DefinicjaElementu` | definicja składnika (konfiguracja) |
| `Nazwa` | `string` | nazwa składnika |
| `Wartosc` | `decimal` | wartość brutto składnika (kwota elementu) |
| `Netto` | `decimal` | wartość netto składnika |
| `DoWypłaty` | `decimal` | kwota do wypłaty z tego składnika |
| `Okres` | `FromTo` | okres, za który naliczono |
| `MiesiacDeklaracji` | `YearMonth` | miesiąc rozliczenia PIT |
| `MiesiacZUS` | `YearMonth` | miesiąc rozliczenia ZUS |
| `Podatki` | `Podatki` (subrow) | struktura podatkowo-składkowa |
**Subrow `WypElement.Podatki` (`Soneta.Place.Podatki`) — pola istotne:**
| Pole | Typ | Opis |
|---|---|---|
| `PodstawaZUS` | `decimal` | podstawa wymiaru składek ZUS |
| `Emerytalna` / `Rentowa` / `Chorobowa` / `Wypadkowa` / `Zdrowotna` | `SkladkaZUS` (subrow) | każda z polami `Podstawa`, `Prac`, `Firma: decimal` oraz wyliczanym `Składka` (suma) |
| `Koszty` | `decimal` | koszty uzyskania przychodu |
| `Ulga` | `decimal` | ulga podatkowa (kwota wolna) |
| `ZalFIS` | `decimal` | zaliczka na podatek dochodowy (fiskus) |
| `ZdrowotneDoOdliczenia` | `decimal` | składka zdrowotna do odliczenia |
Subrow `SkladkaZUS` (`Soneta.Place.SkladkaZUS`): `Podstawa` (podstawa), `Prac` (część pracownika,
`decimal`), `Firma` (część pracodawcy, `decimal`), wyliczane `Składka` (suma) i `JestMinus` (`bool`).
**Worker-agregator `Wyplata.PITInfoWorker`** (klasa publiczna, `[Context] Wypłata`) — udostępnia gotowe
sumy podatkowe dla wypłaty:
| Właściwość | Typ | Opis |
|---|---|---|
| `DoOpodatkowania` | `Currency` | suma elementów opodatkowanych (brutto opodatkowane) |
| `Nieopodatkowane` | `Currency` | suma elementów nieopodatkowanych |
| `Razem` | `decimal` | opodatkowane + nieopodatkowane (przychód razem) |
| `NettoRazem` | `decimal` | wynagrodzenie netto razem |
| `NettoOpodat` | `Currency` | netto opodatkowane |
| `SkładkiZUS` | `decimal` | suma składek ZUS pracownika |
| `SkładkaZdrow` | `decimal` | składka zdrowotna |
| `Koszty` | `decimal` | koszty uzyskania razem |
| `Ulga` | `decimal` | ulga podatkowa |
| `ZalFIS` | `decimal` | zaliczka PIT |
| `Dochód_Bez26` / `Dochód_26` | `decimal` | dochód (z podziałem na ulgę „do 26 lat") |
> `PITInfoWorker.Brutto` i `PITInfoWorker.Netto` są oznaczone `[Obsolete]` — używaj `DoOpodatkowania`,
> `Nieopodatkowane`, `Razem`, `NettoRazem`. Worker przyjmuje też kolekcję `Elementy: IEnumerable`
> (zamiast `Wypłata`) i `WykluczoneElementy: DefinicjaElementu[]`.
**Snippet (agregacja ręczna z elementów):**
```csharp
var pracownik = session.GetKadry().Pracownicy.WgKodu["006"];
decimal brutto = 0m, netto = 0m, zusPrac = 0m, zusFirma = 0m, zalPit = 0m;
// jedna konkretna wypłata pracownika (np. ostatnia z czerwca):
var od = new Date(2024, 6, 1);
var doD = new Date(2024, 6, 30);
Wyplata wyplata = pracownik.Wyplaty[(Wyplata x) => x.Data >= od && x.Data <= doD].Cast<Wyplata>().First();
foreach (WypElement e in wyplata.Elementy)
{
brutto += e.Wartosc; // WypElement.Wartosc to decimal
netto += e.Netto;
zalPit += e.Podatki.ZalFIS;
zusPrac += e.Podatki.Emerytalna.Prac + e.Podatki.Rentowa.Prac
+ e.Podatki.Chorobowa.Prac + e.Podatki.Zdrowotna.Prac;
zusFirma += e.Podatki.Emerytalna.Firma + e.Podatki.Rentowa.Firma
+ e.Podatki.Wypadkowa.Firma;
}
decimal doWyplaty = wyplata.Wartosc.Value; // Currency -> decimal przez .Value
```
**Snippet (przez worker — gotowe agregaty):**
```csharp
var pit = new Wyplata.PITInfoWorker { Wypłata = wyplata };
decimal brutto = pit.Razem; // przychód razem
decimal nettoR = pit.NettoRazem;
decimal zus = pit.SkładkiZUS;
decimal zdrow = pit.SkładkaZdrow;
decimal zaliczka = pit.ZalFIS;
```
**Pułapki:**
- `WypElement.Wartosc`/`Netto`/`DoWypłaty` to `decimal`; `Wyplata.Wartosc` (do wypłaty) to `Currency`
konwersja przez `.Value` (§10.1).
- `SkladkaZUS.Prac` to część pracownika, `SkladkaZUS.Firma` to część pracodawcy — wybierz właściwą
zależnie od potrzeby (koszt pracownika vs koszt pracodawcy).
- `Wyplaty`/`WypElementy` to tabele operacyjne guided — pobieraj zakresem czasowym (§6.3), nie iteruj
całości.
- Pomijaj elementy stornowane przy sumowaniu, jeśli liczysz stan bieżący — patrz `WypElement.RozliczenieStorna`
(KADRY-H10); naliczona wypłata po korekcie zawiera zarówno element pierwotny (`Wystornowany`) jak i `Stornujący`.
---
### KADRY-H6 — Wypłata zaliczki / dołączenie zaliczki (★)
**Cel:** naliczyć i wypłacić zaliczkę (wypłata środków „na poczet" przyszłego wynagrodzenia), tworząc
dokument `Soneta.Place.Zaliczka` i element realizacji zaliczki na wypłacie.
**Model.** Zaliczka to rekord operacyjny `Soneta.Place.Zaliczka` (root guided, tabela `Zaliczki`,
`session.GetPlace().Zaliczki`), implementuje `IBazaZrodlaWyplaty` i `IPowiązanieWypłaty`. Element
realizujący zaliczkę to `WypElementZaliczka.Realizacja : WypElementZaliczka : WypElement`, spłata to
`WypElementZaliczka.Spłata`. Powiązanie elementu z zaliczką: `WypElement.BazaZrodla = Zaliczka`,
`RodzajŹródłaWypłaty.Zaliczka`.
**Ścieżka publiczna — worker `Soneta.Place.WypłaćZaliczkęWorker`** (na `Soneta.Kadry.Pracownicy`):
| Element | Typ / sygnatura | Uwaga |
|---|---|---|
| Parametry | `WypłaćZaliczkęWorker.ZalParams : WypElement.Params` | ctor `(Context)`; `Rodzaj == RodzajŹródłaWypłaty.Zaliczka` |
| Definicja | `ZalParams.Definicja: DefinicjaElementu` | definicja elementu zaliczki (z `WypElement.Params`); **musi mieć** `DefinicjaElementu.RodzajZrodla == RodzajŹródłaWypłaty.Zaliczka` — inaczej worker rzuca `WypElement.RodzajDefinicjiException` (np. „Korekta zaliczki podatku" ma `Dodatek`) |
| Data | `ZalParams.Data: Date` | data wypłaty zaliczki (wymagana) |
| Kwota | `ZalParams.Kwota: Currency` | kwota zaliczki (wymagana) |
| Pracownicy | `WypłaćZaliczkęWorker.Pracownicy: Pracownik[]` | dla kogo |
| Akcja | `[Action("Wypłać zaliczkę")] object WypłataZaliczki()` | tworzy `Zaliczka`, nalicza element realizacji |
**Stan zaliczki (`Zaliczka`):** `Wartosc: Currency`, `Splacono: Currency`, `Pozostaje: Currency`
(`= Wartosc - Splacono`), `Stan: StanZaliczki` (`NieSpłacona`/`CzęściowoSpłacona`/`CałkowicieSpłacona`),
`Realizacje: SubTable` (elementy realizacji), `Spłaty: SubTable<WypElement>` (elementy spłaty).
**Mechanizm naliczenia** (realizowany przez worker): dla każdego pracownika tworzony jest
`new Zaliczka(pracownik)`, dodawany przez `Zaliczki.AddRow(zaliczka)`, a następnie niskopoziomowy
obiekt `Soneta.Place.NaliczanieWypłat` z `NaliczŹródłoWypłaty = zaliczka` wykonuje `Nalicz()`.
Dołączenie/spłata zaliczki w kolejnej wypłacie etatowej dzieje się automatycznie podczas zwykłego
naliczania (KADRY-H1) — naliczanie wyszukuje niespłacone zaliczki pracownika i generuje element
`WypElementZaliczka.Spłata`.
**Snippet (uruchomienie workera zaliczki):**
```csharp
var pracownik = session.GetKadry().Pracownicy.WgKodu["006"];
// definicję zaliczki rozpoznajemy po RodzajZrodla (nie po Kodzie/Nazwie — „Korekta zaliczki podatku"
// to RodzajZrodla.Dodatek, którego worker NIE przyjmie):
DefinicjaElementu defZaliczki = session.GetPlace().DefElementow.Cast<DefinicjaElementu>()
.First(d => d.RodzajZrodla == RodzajŹródłaWypłaty.Zaliczka);
var pars = new WypłaćZaliczkęWorker.ZalParams(context) {
Data = new Date(2024, 6, 15),
Kwota = new Currency(1000m, Currency.SystemSymbol),
};
pars.Definicja = defZaliczki;
var worker = new WypłaćZaliczkęWorker { Params = pars, Pracownicy = new[] { pracownik } };
object wynik = worker.WypłataZaliczki(); // tworzy Zaliczka + nalicza; otwiera własną transakcję
session.Save();
```
**Pułapki:**
- `ZalParams.Definicja` to **istniejąca** definicja elementu o `RodzajZrodla == RodzajŹródłaWypłaty.Zaliczka`
pobierz z `place.DefElementow` (filtruj po `RodzajZrodla`, nie po `Kod`/`Nazwa`), nie twórz w locie.
- Baza Demo może nie zawierać definicji o `RodzajZrodla == Zaliczka` — wtedy worker jest niewykonalny
(w teście: `Assert.Ignore`).
- `Zaliczka.SetWartość(...)` jest `internal` — wartości nie ustawiaj ręcznie; przekaż `ZalParams.Kwota`
do workera.
- `Zaliczka` nie kasuje się bezpośrednio, gdy ma realizacje/spłaty (`OnDeleting` rzuca `RowException`).
- Worker otwiera własną transakcję (`Session.Logout(true)` + `CommitUI`) — nie owijaj dodatkowo;
utrwalenie w bazie przez `Save()`.
---
### KADRY-H7 — Korekta podatków i składek; „Przelicz składki ZUS i podatki" (★)
**Cel:** ponownie przeliczyć (skorygować) składki ZUS i zaliczki PIT na już naliczonych elementach
wypłat pracownika za dany miesiąc deklaracji — np. po zmianie progu, tytułu ubezpieczenia, korekcie
danych kadrowych.
**Worker `Soneta.Place.NaliczaniePodatkówMiesięcznie`** (na `Pracownik`/`PracHistoria`):
| Element | Typ / sygnatura | Uwaga |
|---|---|---|
| Miesiąc | publiczny ctor `(YearMonth miesiącDeklaracji)` (atrybut `[Context(typeof(MiesiącDeklaracji),"Miesiąc")]`) | przy ręcznym wywołaniu przekaż `YearMonth` (np. `pars.Miesiąc`); property odczytu `MiesiącDeklaracji: YearMonth` (get) |
| Klasa parametru | `Soneta.Place.MiesiącDeklaracji : ContextBase` | `MiesiącDeklaracji.Miesiąc: YearMonth` (domyślnie `YearMonth.Today`) |
| Pracownik | `NaliczaniePodatkówMiesięcznie.Pracownik: Pracownik` | `[Context]` |
| `NoTrace` | `bool` | wyłączenie śladu (logu) operacji |
| Akcja | `[Action("Przelicz składki ZUS i podatki")] void PrzeliczPodatki()` | przelicza elementy z danego miesiąca |
**Mechanizm:** worker iteruje elementy (`WypElementy.WgDaty`) wszystkich pracowników powiązanych
(`Pracownik.PracownicyPowiązani`) w okresie `MiesiącDeklaracji.ToFromTo()`, dla niezablokowanych
(`!element.Podatki.Korekta && element.Wyplata.Bufor`) wykonuje przeliczenie flag i naliczenie
podatków (`NaliczaniePodatków.NaliczRozrzuć()`). Wszystko w transakcji `Session.Logout(true)` +
`Commit()`.
**Snippet:**
```csharp
var pracownik = session.GetKadry().Pracownicy.WgKodu["006"];
var pars = new MiesiącDeklaracji(context) { Miesiąc = new YearMonth(2024, 6) };
var worker = new NaliczaniePodatkówMiesięcznie(pars.Miesiąc) { Pracownik = pracownik };
worker.PrzeliczPodatki(); // przelicza składki ZUS i zaliczki PIT za czerwiec 2024
session.Save();
```
**Pułapki:**
- Elementy z ręczną korektą podatków (`element.Podatki.Korekta == true`) oraz elementy z wypłat
zatwierdzonych (`!Wyplata.Bufor`) są **pomijane** — przeliczane są tylko elementy z bufora.
- `MiesiącDeklaracji.Miesiąc` to `YearMonth` — to miesiąc deklaracji, nie data wypłaty.
- Worker przelicza także pracowników powiązanych (`PracownicyPowiązani`) — operacja może objąć więcej
niż jedną kartotekę.
---
### KADRY-H8 — Rozliczenie pracownika; dochód / roczny dochód (★)
**Cel:** odczytać dochód z naliczonej wypłaty oraz (dla właścicieli) pobrać roczny dochód do rozliczeń;
opcjonalnie uruchomić rozliczenie pracownika.
**A. Dochód z wypłaty — `Wyplata.PITInfoWorker`** (publiczny, jak w KADRY-H5). Dochód podatkowy:
| Właściwość | Typ | Opis |
|---|---|---|
| `Dochód_Bez26` | `decimal` | dochód poza ulgą „do 26 lat" (`= przychód + przychód50 koszty koszty50`) |
| `Dochód_26` | `decimal` | dochód objęty ulgą „do 26 lat" |
| `DoOpodatkowania` | `Currency` | podstawa opodatkowania (brutto opodatkowane) |
| `Podstawa` | `decimal` | podstawa naliczenia zaliczki |
| `ZalFIS` | `decimal` | zaliczka PIT |
Dochód roczny pracownika sumuje się iterując wypłaty roku (KADRY-H4/KADRY-H5) i sumując `Dochód_Bez26 + Dochód_26`
(lub `DoOpodatkowania`) z `PITInfoWorker` każdej wypłaty.
**B. „Pobierz roczny dochód" — worker `Soneta.Kadry.PobierzDochodRocznyWorker`** (na `Pracownik`/
`PracHistoria`) — **tylko dla właściciela** (`Pracownik is Wlasciciel`):
| Element | Typ / sygnatura | Uwaga |
|---|---|---|
| Parametry | property `PobierzDochodRocznyWorker.Pars : PobierzDochodRocznyWorker.Params` | `Pars.Rok: int` (domyślnie rok ubiegły) |
| Pracownik | `PobierzDochodRocznyWorker.Pracownik: Pracownik` | `[Context]` |
| Akcja | `[Action("Pobierz roczny dochód")] void Pobierz()` | zapisuje `PrzychodRyczalt` (RoczDochSkala/RoczDochLiniowy/RoczDochRyczalt) za rok |
Korzysta z serwisu `IDochódWłaściciela.KwotaDochoduStraty(pracownik, YearMonth, FormaOpodatkowania)`.
**C. „Rozlicz pracownika" — worker `Soneta.Place.RozliczaniePracownikowWorker`** (na `Pracownik`) —
**tylko dla folderu pracowników zewnętrznych** (`KadryIPlace/Kadry/PracownicyZewnetrzni`):
| Element | Typ / sygnatura | Uwaga |
|---|---|---|
| Parametry | `RozliczeniePracownikowParams : RozliczanieUmowZewnetrznychParams` | `Okres: FromTo`, `Data: Date` |
| Akcja | `[Action("Rozlicz pracownika")] RozliczanieUmowZewnetrznych Rozlicz()` | rozlicza umowy zewnętrzne pracownika |
**Snippet (dochód roczny z wypłat):**
```csharp
var pracownik = session.GetKadry().Pracownicy.WgKodu["006"];
var od = new Date(2024, 1, 1); var doD = new Date(2024, 12, 31);
decimal dochodRoczny = 0m;
foreach (Wyplata w in pracownik.Wyplaty[(Wyplata x) => x.Data >= od && x.Data <= doD])
{
var pit = new Wyplata.PITInfoWorker { Wypłata = w };
dochodRoczny += pit.Dochód_Bez26 + pit.Dochód_26;
}
```
**Pułapki:**
- `PobierzDochodRocznyWorker` działa wyłącznie dla `Wlasciciel` i form opodatkowania ogólnych/ryczałtu —
dla zwykłego pracownika nie ma zastosowania (zwraca bez efektu).
- „Rozlicz pracownika" (`RozliczaniePracownikowWorker`) dotyczy **pracowników zewnętrznych** (umowy
zewnętrzne), nie standardowego rozliczenia płacowego.
- Wewnętrzny `Wyplata.RozliczenieManager` (rozliczanie płatności/należności) jest **niepubliczny**
rozliczenie płatności inicjuje setter `Wyplata.Bufor` (zejście z bufora), nie wywołuj go bezpośrednio.
---
### KADRY-H9 — Kalkulator wynagrodzeń (brutto↔netto, koszt pracodawcy) (★)
**Cel:** wyliczyć netto z brutto (lub odwrotnie) oraz całkowity koszt pracodawcy.
**Brak dedykowanej publicznej klasy „kalkulatora wynagrodzeń"** w publicznym kontrakcie (patrz sekcja
„niewykonalne"). Wyliczenie realizujemy przez **naliczenie próbne** (KADRY-H1/KADRY-H3 — `NaliczanieSeryjne`) i
odczyt agregatów workera `Wyplata.PITInfoWorker` oraz `Wyplata.KosztyUzyskaniaPrzychoduWorker`.
**Koszt pracodawcy — `Wyplata.PITInfoWorker` + składki firmy z elementów:**
- brutto: `pit.Razem` / `pit.DoOpodatkowania`,
- netto: `pit.NettoRazem`,
- składki pracownika: `pit.SkładkiZUS`, `pit.SkładkaZdrow`,
- zaliczka PIT: `pit.ZalFIS`,
- składki firmy (narzuty pracodawcy): suma `WypElement.Podatki.{Emerytalna,Rentowa,Wypadkowa}.Firma`
(plus FP/FGŚP/FEP) — patrz `WyplataSkładkiWorker` niżej.
**Agregator składek — `Soneta.Place.WyplataSkładkiWorker`** (publiczny, `[Context] Wypłata` lub
`Elementy: IEnumerable`): udostępnia `Razem: ZestawienieSkładek` z m.in.:
| Właściwość `ZestawienieSkładek` | Typ | Opis |
|---|---|---|
| `KosztyZUS` | `decimal` | składki ZUS pracownika (emer.+rent.+chor.+wyp., część `Prac`) |
| `FirmaZUS` | `decimal` | składki ZUS pracodawcy (część `Firma`) |
| `Narzuty` | `decimal` | narzuty pracodawcy (`FirmaZUS` + FP.Firma + FGSP.Firma + FEP.Firma) |
| `ZUS` | `decimal` | `KosztyZUS + FirmaZUS` |
| `Emerytalna`/`Rentowa`/… | `ISkładka` | pojedyncze składki (`Podstawa`/`Prac`/`Firma`/`Składka`) |
Koszt pracodawcy ≈ brutto (`pit.DoOpodatkowania`/`Razem`) + `skladki.Razem.Narzuty`.
**Snippet (kalkulacja przez naliczenie próbne):**
```csharp
var pracownik = session.GetKadry().Pracownicy.WgKodu["006"];
var pars = new NaliczanieSeryjne.PracownikParams(context);
pars.DataWypłaty = new Date(2024, 6, 28);
pars.DataListy = pars.DataWypłaty;
pars.TypWypłaty = TypWyplaty.Etat;
var nal = new NaliczanieSeryjne.Pracownika(pars) { Pracownik = pracownik };
NaliczanieWypłat wynik = nal.Nalicz();
Wyplata w = (Wyplata)wynik.WszystkieWypłaty[0];
var pit = new Wyplata.PITInfoWorker { Wypłata = w };
var skl = new WyplataSkładkiWorker { Wypłata = w };
decimal brutto = pit.Razem;
decimal netto = pit.NettoRazem;
decimal kosztPracod = brutto + skl.Razem.Narzuty; // brutto + narzuty pracodawcy
// (jeśli to tylko kalkulacja — nie wywołuj Save(), wynik istnieje w sesji)
```
**Pułapki:**
- Brak osobnego „kalkulatora" — wynik zawsze powstaje przez naliczenie i workery agregujące.
- Kalkulacja brutto↔netto zależy od pełnej konfiguracji pracownika (etat, ulgi, koszty, PPK) — nie ma
bezstanowej funkcji „brutto→netto" w publicznym API.
- Jeśli naliczenie ma być tylko próbne, nie wywołuj `Save()` (zmiany zostaną w sesji i znikną z `Dispose`),
albo wykonaj na osobnej sesji „brudnopisowej".
---
### KADRY-H10 — Stornowanie elementów wypłaty; obsługa elementów stornowanych (★)
**Cel:** zastornować (wycofać/skorygować) element już zatwierdzonej wypłaty i poprawnie odczytać stan
storna.
**Model.** Storno opisuje rekord `Soneta.Place.StornoElementu` (tabela `StornaElementow`). Element
ma stan `WypElement.StanStorna: StanStornaElementu` oraz dostęp do storna `WypElement.Storno: StornoElementu`.
Enum `Soneta.Place.StanStornaElementu`: `NieDotyczy=0`, `DoStornowania=1`, `Wystornowany=2`,
`Stornujący=3`, `WycofaneStorno=10` (tylko wyliczany).
Enum `Soneta.Place.RodzajStornaElementu`: `NieDotyczy=0`, `Anulowanie=1`, `Przeliczenie=2`.
**Pola `WypElement` związane ze storno:**
| Pole | Typ | Opis |
|---|---|---|
| `StanStorna` | `StanStornaElementu` | bieżący stan storna elementu |
| `StanStornaEx` | `StanStornaElementu` | jw. + `WycofaneStorno` gdy `Wystornowany` historyczny |
| `Storno` | `StornoElementu` | powiązany rekord storna (lub `null`) |
| `RozliczenieStorna` | `bool` | `true` gdy `Wystornowany` lub `Stornujący` (element nie liczy się do bieżącego stanu) |
| `Wystornowany` | `bool` | do elementu naliczono element stornujący |
| `Stornowane` / `Stornujące` | `SubTable<StornoElementu>` | relacje storn |
| `Korekta` | `bool` | element zmodyfikowany ręcznie przez operatora |
| `UtwórzStorno()` | `WypElement` | (wirtualna) tworzy element stornujący danego typu |
**Workery oznaczania (publiczne, na `WypElement` / `Wyplata`):**
- `StornoElementu.ElementDoPrzeliczeniaWorker` (na `WypElement`):
- `[Action("Oznacz element do przeliczenia")] ZaznaczElementDoPrzeliczenia()``RodzajStornaElementu.Przeliczenie`,
- `[Action("Oznacz element do anulowania")] ZaznaczElementDoAnulowania()``RodzajStornaElementu.Anulowanie`,
- `[Action("Wycofaj oznaczenie anulowania lub przeliczenia")] WycofajZaznaczenie()` — kasuje `Storno`.
- `StornoElementu.WypłataDoPrzeliczeniaWorker` (na `Wyplata`):
- `ZaznaczElementyDoPrzeliczenia()` / `WycofajZaznaczenie()` — dla wszystkich elementów wypłaty.
- `StornoElementu.ListaPłacDoPrzeliczeniaWorker` (na `ListaPlac`, z `Params.Definicja` / `WszystkieElementy`).
**Mechanizm.** Oznaczenie tworzy `StornoElementu` i ustawia element na `DoStornowania`. Właściwe
wytworzenie elementu stornującego (`UtwórzStornujący()`, stan `Wystornowany` na pierwotnym +
`Stornujący` na nowym) następuje przy ponownym naliczeniu wypłaty (KADRY-H1) lub przeliczeniu. Wymagane:
wypłata zatwierdzona (`Wyplata.Zatwierdzona`) i element w stanie `NieDotyczy`.
**Snippet (oznaczenie do anulowania + przeliczenie):**
```csharp
Wyplata w = ...; // zatwierdzona wypłata pracownika 006
WypElement element = w.Elementy.Cast<WypElement>().First(e => e.Definicja.Kod == "PREMIA");
// oznacz element do anulowania:
var worker = new StornoElementu.ElementDoPrzeliczeniaWorker { Element = element };
worker.ZaznaczElementDoAnulowania(); // otwiera własną transakcję
// element.StanStorna == StanStornaElementu.DoStornowania, element.Storno.RodzajStorna == Anulowanie
// ponowne naliczenie wypłaty (KADRY-H1) wygeneruje element stornujący:
var pars = new NaliczanieSeryjne.PracownikParams(context) { DataWypłaty = w.Data, DataListy = w.Data };
new NaliczanieSeryjne.Pracownika(pars) { Pracownik = w.Pracownik }.Nalicz();
session.Save();
```
**Odczyt elementów stornowanych:**
```csharp
foreach (WypElement e in w.Elementy)
{
if (e.RozliczenieStorna) continue; // pomiń wystornowane i stornujące przy sumowaniu stanu bieżącego
// ... e to element „żywy"
}
```
**Pułapki:**
- Oznaczać można tylko elementy wypłaty **zatwierdzonej** i w stanie `NieDotyczy` (`IsEnabled...` to
egzekwuje); na buforze storno nie ma sensu.
- Storno samo w sobie tylko **oznacza** (`DoStornowania`) — wystornowanie (`Wystornowany`/`Stornujący`)
powstaje dopiero przy ponownym naliczeniu/przeliczeniu.
- Przy sumowaniu kwot bieżących pomijaj `RozliczenieStorna == true`, inaczej policzysz element pierwotny
i jego storno podwójnie.
- Nie można przenieść do bufora wypłaty z elementami `DoStornowania`/`Wystornowany` (rzuca `RowException`
— patrz KADRY-H11).
---
### KADRY-H11 — Anulowanie/usunięcie naliczonej wypłaty (bufor, ponowne naliczenie) (★)
**Cel:** „cofnąć" naliczoną i zatwierdzoną wypłatę do edycji (bufor) lub usunąć, by naliczyć ponownie.
**Model.** Wypłata ma flagi `Wyplata.Bufor: bool` (niezatwierdzona/edytowalna) oraz `Wyplata.Zatwierdzona: bool`
(odwrotność `Bufor`). Zejście z bufora = zatwierdzenie; powrót do bufora = otwarcie do edycji.
**Workery (publiczne, na `Wyplata`):**
| Worker / akcja | Sygnatura | Efekt |
|---|---|---|
| `Wyplata.ZatwierdźWorker` | property `Lista: Wyplata`; `[Action("Zatwierdź wypłatę")] void Zatwierdź()` | `Zatwierdzona = true` (zejście z bufora) |
| `Wyplata.OtwórzWorker` | property `Wypłata: Wyplata`; `[Action("Przenieś do bufora")] void Otwórz()` | `Zatwierdzona = false` (powrót do bufora) |
Obie akcje działają w transakcji `Session.Logout(true)` + `Commit()`. **Uwaga na nazwy property:**
worker zatwierdzania przypina wypłatę przez `ZatwierdźWorker.Lista`, a otwierania — przez
`OtwórzWorker.Wypłata`. `IsEnabled...` wymaga `Wyplata.CanBufor` — ale `CanBufor` jest **`protected`**
(niedostępny z dodatku); stan czytaj przez publiczne `Wyplata.Bufor` / `Wyplata.Zatwierdzona`.
**Bezpośrednia flaga `Wyplata.Bufor`:**
- setter `Bufor` rzuca `ColReadOnlyException`, gdy `!CanBufor`;
- zejście z bufora (`Bufor=false`) wyzwala rozliczenie płatności (wewnętrzny `RozliczenieManager`);
- `IsReadOnlyBufor()` true gdy brak praw / `!CanBufor` / wyłączone „ZatwierdzanieFlagą" / lista zatwierdzona.
**Usunięcie / ponowne naliczenie.** Aby przeliczyć od nowa: przenieś wypłatę do bufora
(`OtwórzWorker.Otwórz()`), a następnie wykonaj ponowne naliczenie (KADRY-H1 — `NaliczanieSeryjne`), które
nadpisze elementy. Usunięcie samej wypłaty realizuje standardowe `Row.Delete()` w transakcji (gdy
dozwolone — wypłata w buforze, bez powiązań blokujących).
**Snippet (powrót do bufora + ponowne naliczenie):**
```csharp
Wyplata w = ...; // zatwierdzona wypłata pracownika 006
// 1) przenieś do bufora:
new Wyplata.OtwórzWorker { Wypłata = w }.Otwórz(); // Zatwierdzona = false
// 2) ponowne naliczenie (KADRY-H1):
var pars = new NaliczanieSeryjne.PracownikParams(context) {
DataWypłaty = w.Data, DataListy = w.Data, TypWypłaty = TypWyplaty.Etat,
};
new NaliczanieSeryjne.Pracownika(pars) { Pracownik = w.Pracownik }.Nalicz();
session.Save();
```
**Snippet (usunięcie wypłaty z bufora):**
```csharp
using (ITransaction t = session.Logout(true)) {
w.Bufor = true; // upewnij się, że w buforze (lub OtwórzWorker)
w.Delete();
t.Commit();
}
session.Save();
```
**Pułapki:**
- `Otwórz()` rzuca `RowException`, gdy wypłata nie jest zatwierdzona; `Zatwierdź()` — gdy już
zatwierdzona. Sprawdzaj `IsEnabled...` / stan przed wywołaniem.
- `UpdateBufor()` rzuca `RowException`, gdy na wypłacie są elementy `DoStornowania`/`Wystornowany`
najpierw wycofaj oznaczenia storna (KADRY-H10) lub dokończ przeliczenie.
- Zejście z bufora wykonuje rozliczenie płatności i kopiowanie kursu — nie traktuj go jak zwykłej
zmiany pola.
- Operacje płacowe to dane operacyjne — łap `RowException`/`RowConflictException` z `Save()` (§4, §9),
nie ogólny `Exception`.
@@ -0,0 +1,550 @@
# KADRY09 — Listy płac, przelewy, wydruki
> Wspólne fakty o typie, podstawowe typy i szablon wzorca: [../kadry.md](../kadry.md).
> **Model.** Lista płac to dokument operacyjny `Soneta.Place.ListaPlac` (root `GuidedRow`,
> tabela `ListyPlac`, `session.GetPlace().ListyPlac`). Trzyma kolekcję wypłat
> `ListaPlac.Wyplaty: SubTable<Wyplata>`. Każda `Wyplata` (root `GuidedRow`, tabela `Wyplaty`)
> wskazuje wstecz listę (`Wyplata.ListaPlac: IRow`) i pracownika (`Wyplata.Pracownik: IRow`).
> Wzorzec listy to `DefinicjaListyPlac` (tabela konfiguracyjna `DefListPlac`,
> `session.GetPlace().DefListPlac`, dostęp `WgSymbolu`/`WgNazwy`).
>
> **`Wyplata` jest abstrakcyjna** — konkretne typy: `WyplataEtat`, `WyplataUmowa`, `WyplataInne`
> (ctor `(ListaPlac listaplac, Pracownik pracownik)` oraz wariant z `IPowiązanieWypłaty`).
> W praktyce wypłat **nie tworzy się ręcznie** — robi to worker naliczania.
### KADRY-I1 — Naliczanie/generowanie list płac (★)
**Cel:** utworzyć listę płac dla wybranego okresu i naliczyć na niej wypłaty pracowników
(etat/umowy), tak by `ListaPlac.Wyplaty` zawierała policzone `Wyplata`.
**Warianty:**
| Wariant | Mechanizm | Uwaga |
|---|---|---|
| Ręczne utworzenie pustej listy | `new ListaPlac()` + `ListyPlac.AddRow(lp)` + pola | sterujesz wszystkim sam |
| Naliczanie wypłat na istniejącej liście | worker `Soneta.Place.NaliczanieWypłat` (akcja `Nalicz`) | tworzy `Wyplata*` i liczy elementy |
| Naliczanie planowanych list (zbiorczo) | worker `Soneta.Place.NaliczaniePlanowanychListPłacWorker` (akcja `Nalicz`) | wg `DefinicjaPlanowanejListyPłac` |
**Pola i typy (`ListaPlac`, kolejność ustawiania jest istotna):**
| Pole | Typ | Uwaga |
|---|---|---|
| `Definicja` | `Soneta.Place.DefinicjaListyPlac` | wzorzec listy; ustawić **pierwsze** po `AddRow` |
| `Wydzial` | `Soneta.Kadry.Wydzial` | tylko gdy `Definicja.Wydzial == true` |
| `Seria` | `string` | tylko gdy `Definicja.Seria == true` |
| `Data` | `Soneta.Types.Date` | data naliczania listy |
| `Naliczanie` | `Soneta.Place.TypNaliczenia` | wartości: `PłatnaZGóry`/`PłatnaZDołu`; **nie ustawiaj** — setter rzuca bez licencji „PL Złoty" |
| `DataWyplaty` | `Soneta.Types.Date` | data postawienia środków; wyznacza mies./rok |
| `MiesiacZUS` | `Soneta.Types.YearMonth` | miesiąc rozliczenia ZUS |
| `Okres` | `Soneta.Types.FromTo` | okres listy; **po** `DataWyplaty` i `Naliczanie` |
| `MiesWstecz` | `int` | |
| `Wyplaty` | `SubTable<Wyplata>` | wypełniana przez worker naliczania |
| `Numer` | `Soneta.Core.NumerDokumentu` | nadawany automatycznie |
| `Bufor` / `Zatwierdzona` | `bool` | stan dokumentu |
**Worker `Soneta.Place.NaliczanieWypłat`**`[Context]`: `Context`, `ListaPłac: ListaPlac`,
`Pracownik: Soneta.Kadry.Pracownik`; akcja `NaliczanieWypłat Nalicz()`; właściwości
wynikowe m.in. `Wypłaty: IList`, `Nienaliczeni: IEnumerable<BłądNaliczaniaWynagrodzenia>`,
`DataWypłaty/DataListy/DataZUS: Date`, `Okres: FromTo`, `Naliczanie: TypNaliczenia`.
**Worker `Soneta.Place.NaliczaniePlanowanychListPłacWorker`**`[Context]`:
`Pracownik: Pracownik[]`; `Params Pars` z polami `Definicja: DefinicjaPlanowanejListyPłac`,
`DataWypłaty: Date`, `Okres: FromTo`, `Naliczanie: TypNaliczenia`, `TypWypłaty: TypWyplaty`,
`MiesiącZUS/MiesiącDeklaracji: YearMonth`, `Seria: string`, `MiesWstecz: int`,
`UwzgledniajNieZatwierdzoneListyPlac/EdycjaMiesiącaZUS: bool`;
akcja `NaliczaniePlanowanychListPłac Nalicz()`.
**Snippet (ręczne utworzenie listy + naliczenie wypłaty pracownika):**
```csharp
using Soneta.Business;
using Soneta.Place;
using Soneta.Kadry;
using Soneta.Types;
var place = session.GetPlace();
// 1. Wzorzec listy płac (definicja konfiguracyjna).
var def = place.DefListPlac.WgSymbolu["ETAT"]
?? throw new BusException("Brak definicji listy płac".Translate());
// 2. Pusta lista płac — KOLEJNOŚĆ: AddRow → Definicja → daty/naliczanie → Okres.
var lp = new ListaPlac();
place.ListyPlac.AddRow(lp);
lp.Definicja = def; // pierwsze po AddRow
lp.Data = new Date(2026, 6, 30);
lp.DataWyplaty = new Date(2026, 6, 30); // wyznacza miesiąc/rok
lp.MiesiacZUS = new YearMonth(2026, 6);
lp.Okres = new FromTo(new Date(2026, 6, 1), new Date(2026, 6, 30)); // po DataWyplaty
// Uwaga: NIE ustawiaj lp.Naliczanie — setter rzuca bez licencji „PL Złoty"; getter ma sensowny domyślny.
// 3. Naliczenie wypłaty pracownika — sprawdzona ścieżka to NaliczanieSeryjne (patrz sekcja H);
// naliczona wypłata zostaje automatycznie powiązana z bieżącą listą płac.
var pracownik = session.GetKadry().Pracownicy.WgKodu["006"];
var pars = new NaliczanieSeryjne.PracownikParams(context) // context: w UI z workera, w teście z TestBase
{
DataWypłaty = new Date(2026, 6, 30),
DataListy = new Date(2026, 6, 30),
TypWypłaty = TypWyplaty.Etat,
};
var wynik = new NaliczanieSeryjne.Pracownika(pars) { Pracownik = pracownik }.Nalicz();
// 4. Powiązanie wypłaty z listą jest dwukierunkowe (Wyplata.ListaPlac / Wyplata.Pracownik):
foreach (Wyplata w in wynik.WszystkieWypłaty)
{
// w.ListaPlac, w.Pracownik
}
session.Save();
```
**Pułapki:**
- **Kolejność pól krytyczna:** `Okres` i `MiesWstecz` ustaw **po** `DataWyplaty` i `Naliczanie`
(wzajemne zależności wyliczeń) — patrz wzorzec w kodzie naliczania list.
- `Wydzial`/`Seria` ustawiaj **warunkowo** wg `Definicja.Wydzial`/`Definicja.Seria` — inaczej
ryzyko niespójności klucza `WgDefinicja`.
- Wypłat **nie twórz przez `new WyplataEtat(...)` ręcznie** — naliczaj. Sprawdzoną ścieżką
naliczania jest **`NaliczanieSeryjne.Pracownika(...).Nalicz()`** (sekcja H); sam worker
`NaliczanieWypłat { ListaPłac, Pracownik }.Nalicz()` w bazie Demo potrafi zwrócić pustą listę.
- `Wyplata.ListaPlac`/`Wyplata.Pracownik` to relacje **tylko do odczytu** — powiązania nie ustawisz
setterem; powstają w trakcie naliczania.
- `ListyPlac` to tabela operacyjna guided — przy odczycie filtruj zakresem (`WgDatyWyplaty`,
`WgOkresu`, `WgDefinicja`), nie skanuj całości (safe-code §6.3).
- `Wyplata.ListaPlac`/`Wyplata.Pracownik` to `IRow` (relacje interfejsowe) — porównuj/rzutuj
świadomie.
### KADRY-I2 — Drukowanie/PDF kwitków (pasków) wypłaty (★)
**Cel:** wygenerować pasek (kwitek) wypłaty pracownika do PDF.
**Mechanizm.** Wydruk realizuje serwis **`IReportService`** (namespace `Soneta.Business.UI`,
identycznie jak wydruki handlowe — patrz `handel.md` rozdz. 12). Wzorce pasków to
szablony `*.repx` zarejestrowane atrybutem `[DxReport]` w assembly
**`Soneta.KadryPlace.Reports`** dla `DataType = typeof(Soneta.Place.Wyplata)`:
| Wzorzec (ReportName) | Plik szablonu (`TemplateFileName`) | `DataType` |
|---|---|---|
| „Pasek wypłaty" | `PasekWyplaty.repx` | `Soneta.Place.Wyplata` |
| „Duży pasek wypłaty" | `DuzyPasekWyplaty.repx` | `Soneta.Place.Wyplata` |
| „Paski wypłat" (zbiorczy) | `PaskiWyplaty.repx` | `Soneta.Place.ListaPlac` |
**API (`IReportService` / `ReportResult` — `Soneta.Business.UI`):**
`Stream GenerateReport(ReportResult rr)`,
`ReportResult.TemplateFileName: string`, `.DataType: Type`,
`.OutputFormat: ReportFormats` (`PDF`), `.Context: Context`, `.Target: ReportTargets`.
**Snippet (pasek jednej wypłaty do strumienia PDF):**
```csharp
using Soneta.Business.UI; // IReportService, ReportResult, ReportFormats
using Soneta.Place;
var raporty = session.GetRequiredService<IReportService>();
var context = new Context(session.Context);
context.Set(wyplata); // pojedyncza Wyplata
var rr = new ReportResult {
TemplateFileName = "PasekWyplaty.repx",
DataType = typeof(Wyplata),
OutputFormat = ReportFormats.PDF,
Context = context,
};
using Stream pdf = raporty.GenerateReport(rr); // pierwsze 4 bajty == "%PDF"
```
**Pułapki:**
- `IReportService` pobierasz z kontenera: `session.GetRequiredService<IReportService>()`
(potrzebne `using Microsoft.Extensions.DependencyInjection;`). Serwis i silnik raportów
(DevExpress) oraz szablony pasków z `Soneta.KadryPlace.Reports` są dostępne **transytywnie**
generowanie PDF działa bez dodatkowych referencji (wzorzec jak w `handel.md` rozdz. 12).
- Poprawny PDF zaczyna się od bajtów `"%PDF"` — to wygodna asercja w teście.
- Druk na fizyczną drukarkę (`Target = Printer`, `PrintReport`) wymaga sprzętu — NIE testować.
### KADRY-I3 — Drukowanie/PDF list płac (★)
**Cel:** wygenerować wydruk całej listy płac (pełna lista, zestawienie wypłat) do PDF.
**Mechanizm.** Identyczny jak KADRY-I2 — `IReportService.GenerateReport`, szablony `[DxReport]`
w `Soneta.KadryPlace.Reports`, dla `DataType = typeof(Soneta.Place.ListaPlac)` /
`typeof(Soneta.Place.ListyPlac)`:
| Wzorzec (ReportName) | Plik szablonu | `DataType` |
|---|---|---|
| „Pełna lista płac" | `PelnaListaPlac.repx` | `Soneta.Place.ListaPlac` |
| „Wspólna pełna lista płac" | `Wspolnapelnalistaplac.repx` | `Soneta.Place.ListyPlac` (zbiór) |
| „Paski wypłat" | `PaskiWyplaty.repx` | `Soneta.Place.ListaPlac` |
| Zestawienie wypłat | `ZestawienieWyplat.repx` | `Soneta.Place.ListaPlac` |
**Snippet (pełna lista płac → PDF):**
```csharp
using Soneta.Business.UI;
using Soneta.Place;
var raporty = session.GetRequiredService<IReportService>();
var context = new Context(session.Context);
context.Set(listaPlac); // ListaPlac
var rr = new ReportResult {
TemplateFileName = "PelnaListaPlac.repx",
DataType = typeof(ListaPlac),
OutputFormat = ReportFormats.PDF,
Context = context,
};
using Stream pdf = raporty.GenerateReport(rr);
```
**Pułapki:**
- Mechanizm i dostępność serwisu — jak w KADRY-I2 (działa transytywnie, bez dodatkowych referencji).
- Lista musi być policzona (mieć `Wyplaty`) — inaczej wydruk będzie pusty.
- **Niektóre szablony list wymagają pełnego kontekstu danych.** W bazie Demo wzorzec
`PelnaListaPlac.repx` potrafi rzucić `InvalidOperationException` („Problem z przygotowaniem
raportu") na sztucznie utworzonej liście — to ograniczenie konkretnego szablonu/kontekstu, nie
brak referencji (pasek wypłaty `PasekWyplaty.repx` z KADRY-I2 generuje się poprawnie).
- Do wydruku zbiorczego wielu list ustaw `DataType = typeof(Soneta.Place.ListyPlac)` i przekaż
zbiór przez `Context.Set(...)` / `ReportResult.Rows`.
### KADRY-I4 — Generowanie przelewów wynagrodzeń (przygotowanie przelewów) (★)
**Cel:** z naliczonej, zatwierdzonej listy płac wygenerować dokumenty przelewu wynagrodzeń
(do paczki przelewów), tak by wypłaty pracowników trafiły do zapłaty/preliminarza i mogły zostać
wyeksportowane do banku (KADRY-I5).
> **Dwie różne klasy `Wyplata` — nie myl ich.** W domenie współistnieją:
> - **`Soneta.Place.Wyplata`** (moduł `PlaceModule`, tabela `Wyplaty`) — *naliczona wypłata
> pracownika* (wynik naliczania z sekcji H/KADRY-I1); to dokument **płacowy** ze składnikami
> (`Elementy`), powiązany z listą płac (`Wyplata.ListaPlac`).
> - **`Soneta.Kasa.Wyplata`** (moduł `KasaModule`, tabela `Wyplaty`/`Zaplaty`) — *zapłata kasowa*
> (rozchód środków). To **ona** implementuje `IDokumentPlatny`/`IDokumentKsiegowalny`, ma pola
> rozliczeniowe (`DoRozliczenia`, `Stan`, `StanRozliczenia`, `KwotaRozliczona`, `Rozliczono`,
> `Rozrachunki`, `Zaplaty`, `PreliminarzPoz`, `PozycjePrzelewu`, `BlokadaPrzelewow`).
>
> Mechanizm „z wypłaty do przelewu” łączy oba światy: worker płacowy czyta `Place.Wyplata` z listy
> płac i tworzy dokumenty przelewu w module Kasa (`Soneta.Kasa.PrzelewBase`, w paczce `PaczkaPrzelewow`).
**Mechanizm (publiczny kontrakt — worker płacowy):** sprawdzoną ścieżką tworzenia przelewów z
wynagrodzeń jest worker **`Soneta.Place.ListaPlac.PrzygotujPrzelewyWorker`** (assembly
`Soneta.KadryPlace`, akcja menu *„Przygotuj przelewy”* na liście/listach płac). Kontekstem
działania jest **lista płac** (`Soneta.Place.ListaPlac`) — przygotowuje przelewy dla zatwierdzonych
wypłat tej listy.
**Parametry — `PrzygotujPrzelewyWorker.Params`:**
| Pole | Typ | Uwaga |
|---|---|---|
| `Data` | `Soneta.Types.Date` | data dokumentów przelewu |
| `Paczka` | `Soneta.Kasa.PaczkaPrzelewow` | istniejąca paczka, do której trafią przelewy (opcjonalnie) |
| `DefinicjaPaczki` | `Soneta.Kasa.DefinicjaPaczkiPrzelewu` | definicja, wg której utworzyć nową paczkę (gdy `Paczka == null`) |
| `ZRachunku` | `Soneta.Kasa.RachunekBankowyFirmy` | rachunek firmy obciążany przelewami |
| `Łączone` | `bool` | łączenie przelewów do jednego podmiotu w jeden dokument |
| `ListyPłac` | `string` | opis/oznaczenie list płac (informacyjnie w tytule) |
| `ModyfikacjaTytułów` | `bool` | czy nadpisać tytuły przelewu (`Tytułem1`/`Tytułem2`) |
| `Tytułem1`, `Tytułem2` | `string` | tytuł przelewu (gdy `ModyfikacjaTytułów == true`) |
| `ZEwidencjiZrodlowej` | `bool` | bierz dane rachunku z ewidencji źródłowej |
**Akcja:** `object PrzygotujPrzelewy()` — tworzy w sesji dokumenty `Soneta.Kasa.PrzelewBase`
(tabela `Przelewy`) w paczce `PaczkaPrzelewow`; utrwalenie w bazie wymaga `session.Save()`.
**Model dokumentu przelewu (`Soneta.Kasa.PrzelewBase`, tabela `Przelewy`, root `GuidedRow`):**
| Pole | Typ | Opis |
|---|---|---|
| `Kwota` | `Soneta.Types.Currency` | kwota przelewu |
| `Podmiot` | `Soneta.Kasa.IPodmiotKasowy` | odbiorca (m.in. `Pracownik`, `ZUS`, `UrzadSkarbowy`, `Bank`, `Kontrahent`) |
| `Rachunek` | `Soneta.Kasa.RachunekBankowyPodmiotu` | rachunek odbiorcy |
| `RachunekZleceniodawcy` | `Soneta.Kasa.NumerRachunku` | rachunek firmy (obciążany) |
| `Data` | `Soneta.Types.Date` | data przelewu |
| `Definicja` | `Soneta.Core.DefinicjaDokumentu` | definicja dokumentu |
| `Numer` | `Soneta.Core.NumerDokumentu` | numer (nadawany automatycznie) |
| `Tytulem1`, `Tytulem2` | `string` | tytuł przelewu |
| `Typ2` | `Soneta.Kasa.TypPrzelewu2` | wariant przelewu (zwykły / **MPP** / itp.) |
| `PaczkaPrzelewow` | `Soneta.Kasa.PaczkaPrzelewow` | paczka, do której należy przelew |
| `Bufor` / `Zatwierdzony` | `bool` | stan dokumentu |
| `Exported` | `bool` | czy wyeksportowany (po KADRY-I5 — `true`, blokuje edycję) |
**Przelewy okresowe / MPP:**
- **MPP (mechanizm podzielonej płatności)** to *wariant* przelewu — wyrażany przez
`PrzelewBase.Typ2: Soneta.Kasa.TypPrzelewu2` (oraz na `Kasa.Wyplata` polem `KwotaMPP`,
`MozliweMechanizmyMPP`). Dla wynagrodzeń MPP zwykle nie dotyczy (to mechanizm faktur VAT), ale
kontrakt go przewiduje.
- **Przelewy okresowe** (cykliczne płatności np. składek z list) realizuje osobny worker
księgowy `Soneta.Ksiega.Kasowe.NaliczaniePrzelewowOkresowych` (poza zakresem płac pracownika).
**Powiązanie z wypłatą / preliminarzem (publiczne kolekcje na `Pracownik`):**
| Kolekcja na `Pracownik` | Typ | Zawiera |
|---|---|---|
| `Pracownik.Przelewy` | `SubTable<Soneta.Kasa.PrzelewBase>` | przelewy pracownika |
| `Pracownik.DokumentyPreliminarza` | `SubTable<Soneta.Kasa.PreliminarzDokument>` | dokumenty preliminarza |
| `Pracownik.DokumentyRozliczeniowe` | `SubTable<Soneta.Kasa.DokRozliczBase>` | dokumenty rozliczeniowe |
| `Pracownik.Rozrachunki` | `SubTable<Soneta.Kasa.RozrachunekIdx>` | rozrachunki |
| `Pracownik.Rachunki` | `SubTable<Soneta.Kasa.RachunekBankowyPodmiotu>` | rachunki bankowe pracownika |
> **Korekta (zweryfikowane kompilacją + skanem DLL):** `Pracownik.Platnosci` **nie istnieje** w publicznym
> kontrakcie kartoteki pracownika — kolekcja `Platnosci` występuje tylko na interfejsie
> `Soneta.Kasa.IDokumentPlatny` (np. `Kasa.Wyplata.Platnosci`), nie na `Pracownik`. Płatności podmiotu
> czytaj przez `Pracownik.Rozrachunki` / `Pracownik.DokumentyRozliczeniowe`.
**Snippet (worker — w UI/teście z dostępnym `Context`):**
```csharp
using Soneta.Business;
using Soneta.Place; // ListaPlac, ListaPlac.PrzygotujPrzelewyWorker
using Soneta.Kasa; // PaczkaPrzelewow, PrzelewBase, RachunekBankowyFirmy
using Soneta.Types;
// listaPlac: zatwierdzona lista płac z naliczonymi wypłatami (sekcja KADRY-I1)
var pars = new ListaPlac.PrzygotujPrzelewyWorker.Params
{
Data = Date.Today,
// Paczka = istniejacaPaczka, // albo nowa wg DefinicjaPaczki:
// DefinicjaPaczki = session.GetKasa().DefPaczekPrzelewow.WgSymbolu["..."],
// ZRachunku = rachunekFirmy, // RachunekBankowyFirmy
Łączone = false,
};
var worker = new ListaPlac.PrzygotujPrzelewyWorker { Pars = pars };
// kontekstem workera jest lista płac; uruchomienie akcji:
worker.PrzygotujPrzelewy();
session.Save(); // utrwalenie dokumentów przelewu w bazie
```
**Pułapki / ograniczenia (bądź szczery):**
- **`Place.Wyplata``Kasa.Wyplata`** — pola rozliczeniowe (`DoRozliczenia`, `Stan`,
`StanRozliczenia`, `Rozrachunki`, `BlokadaPrzelewow`) są na **kasowej** `Soneta.Kasa.Wyplata`
(`IDokumentPlatny`), nie na płacowej. Skanując „Wyplata” trafia się na kasową.
- **Lista płac musi być zatwierdzona i naliczona** — `PrzygotujPrzelewy` na pustej/niezatwierdzonej
liście nie ma czego przelać.
- **Wymaga konfiguracji modułu Kasa** — definicji paczki przelewów (`DefinicjaPaczkiPrzelewu`),
rachunku firmy (`RachunekBankowyFirmy`) oraz rachunku pracownika (`Pracownik.Rachunki`). Brak
rachunku odbiorcy → przelew nie powstanie albo będzie niekompletny. **W bazie Demo te elementy
mogą nie być skonfigurowane**, dlatego generowanie przelewów w teście jednostkowym jest
niepewne (patrz spec testowy).
- Worker **sam zatwierdza zmiany w sesji** (otwiera transakcję) — nie owijaj w dodatkowy
`session.Logout(true)`; do bazy idą w `Save()`.
- `PrzelewBase.Podmiot`/`Powiazanie` to relacje **interfejsowe** (`IRow`/`IPodmiotKasowy`) —
rzutuj świadomie.
- `Przelewy` to tabela operacyjna guided — przy odczycie filtruj zakresem (safe-code §6.3).
---
### KADRY-I5 — Eksport wynagrodzeń do banku / pliku przelewów (★)
> **UWAGA — operacja plikowa/integracyjna.** Eksport zapisuje **fizyczny plik** w formacie
> bankowym (Elixir, MT940-pochodne, formaty walutowe). To wejście/wyjście do systemu zewnętrznego —
> **nie jest to przedmiot testu jednostkowego** (zależy od ścieżki na dysku, formatu banku,
> sterownika eksportu i — przy wysyłce online — od sieci). Dokumentujemy **model i publiczny
> kontrakt**, a sam eksport pliku oznaczamy jako nietestowalny jednostkowo.
**Cel:** wyeksportować przygotowane przelewy (KADRY-I4) do pliku przelewów dla systemu bankowości
elektronicznej.
**Mechanizm (publiczny kontrakt — worker Kasa):** worker **`Soneta.Kasa.EksportPrzelewowWorker`**
(akcja menu *„Eksport przelewów”*, metoda `Eksport()`), sterowany przez
`Soneta.Kasa.EksportPrzelewowParams`.
> **Korekta (zweryfikowane kompilacją):** `EksportPrzelewowParams` **nie ma konstruktora
> bezparametrowego** — wymaga `EksportPrzelewowParams(Context ctx, RachunekBankowyFirmy rachunek, PrzelewBase[] przelewy)`.
> Co więcej, **sam konstruktor waliduje rachunek** i rzuca `System.ApplicationException`
> („Eksport niemożliwy. Nie wskazano rachunku w filtrach listy.”), gdy `rachunek == null`. Dlatego nie da się
> utworzyć parametrów samym inicjalizatorem obiektu. W teście jednostkowym kontrakt API weryfikuj **refleksją**
> (istnienie typu, sygnatura konstruktora, property `FileName`/`Params`, metoda `Eksport`), bez instancjonowania.
**Parametry — `Soneta.Kasa.EksportPrzelewowParams`:**
| Pole | Typ | Uwaga |
|---|---|---|
| `FileName` | `string` | **ścieżka pliku wyjściowego** — operacja na dysku |
| `AppendToFile` | `bool` | dopisanie do istniejącego pliku |
| `PrzelewyZgodne` | `IList<Soneta.Kasa.PrzelewBase>` | przelewy do wyeksportowania |
| `Rachunek` | `Soneta.Kasa.RachunekBankowyFirmy` | rachunek firmy (zleceniodawca) |
| `PrmDataPrzelewow` | `Soneta.Types.Date` | data realizacji |
| `PrmNumerPaczki` | `string` | numer paczki |
| `PrmZakres` | `Soneta.Kasa.ZakresEksportuPrzelewow` | zakres (wszystkie / wg paczki / zaznaczone) |
| `EksportujWBuforze` | `bool` | uwzględnij przelewy w buforze |
| `InfoBank`, `InfoFormatKraj`, `InfoFormatWalutowy`, `InfoRachunekBankowy` | `string` | parametry formatu/banku |
| `WithoutHelper` | `bool` | tryb bez kreatora |
**Akcja:** `object Eksport()` — zapisuje plik wg `FileName`. Po eksporcie przelewy są oznaczane
jako wyeksportowane (`PrzelewBase.Exported == true`, blokada dalszej edycji).
**Powiązane (kontekst):**
- Eksport całych **paczek**: worker `Soneta.Kasa.EksportPaczekPrzelewowWorker`.
- Eksport przelewów PPK z pulpitu KBR: `Soneta.EI.UI.PulpitKBR.Workers.PulpitKBEksportPrzelewowWorker`.
**Snippet (kontrakt — w realnej integracji, nie w teście jednostkowym):**
```csharp
using Soneta.Kasa; // EksportPrzelewowWorker, EksportPrzelewowParams, PrzelewBase
using System.Collections.Generic;
PrzelewBase[] przelewy = /* przelewy z KADRY-I4, np. z paczki */;
// Konstruktor jest WYMAGANY (brak ctora bezparametrowego) i waliduje rachunek (rzuca, gdy null):
var par = new EksportPrzelewowParams(context, rachunekFirmy, przelewy) // rachunekFirmy: RachunekBankowyFirmy
{
FileName = @"C:\przelewy\wynagrodzenia.txt", // ŚCIEŻKA PLIKU — operacja I/O
PrmDataPrzelewow = Date.Today,
EksportujWBuforze = false,
};
var worker = new EksportPrzelewowWorker { Params = par };
worker.Eksport(); // zapis pliku na dysk — efekt uboczny poza sesją
```
**Pułapki / ograniczenia (bądź szczery):**
- **Eksport pliku NIE nadaje się do testu jednostkowego** — pisze na dysk, zależy od formatu banku
i sterownika eksportu; w teście co najwyżej dokumentujemy istnienie API
(`EksportPrzelewowWorker`, `EksportPrzelewowParams.FileName`), bez wywołania `Eksport()`.
- Format pliku zależy od **konfiguracji formatu eksportu** danego banku — nie ma jednego
uniwersalnego formatu; `InfoFormat*`/`InfoBank` parametryzują wynik.
- Wysyłka online (bankowość elektroniczna / API banku) to dodatkowo operacja **sieciowa** — poza
zakresem testów jednostkowych.
- Po eksporcie `PrzelewBase.Exported = true` blokuje edycję — ponowny eksport wymaga
`EksportujWBuforze`/zmiany stanu.
---
### KADRY-I6 — Wystawienie faktury / faktury zbiorczej z zapłaty (rozliczenia) (★)
> **Zakres i szczerość.** Faktura jest dokumentem **handlowym** (`Soneta.Handel.DokumentHandlowy`),
> nie płacowym — to nie jest funkcja kartoteki pracownika ani list płac. Powiązanie „z zapłaty”
> dotyczy **rozrachunków/rozliczeń** (moduł Kasa): zapłata (`Soneta.Kasa.Wyplata`/`Wplata` —
> `IDokumentPlatny`) jest **rozliczana** z dokumentem płatnym (np. fakturą) przez rozrachunki.
> Wystawianie faktury z poziomu pracownika/płac w publicznym kontrakcie **nie istnieje**;
> tutaj dokumentujemy model rozliczeń, który łączy zapłatę z fakturą.
**Cel:** powiązać zapłatę z dokumentem płatnym (fakturą) na poziomie rozrachunków/rozliczeń —
oraz wskazać, gdzie w publicznym API leży rozliczanie należności/zobowiązań pracownika.
**Model rozliczeń (publiczny kontrakt, moduł `KasaModule`):**
| Element | Typ / kolekcja | Rola |
|---|---|---|
| Zapłata (rozchód/wpływ) | `Soneta.Kasa.Wyplata` / `Soneta.Kasa.Wplata` | dokument płatny (`IDokumentPlatny`) |
| Płatność (zobowiązanie/należność) | `Soneta.Kasa.Platnosc` (tabela `Platnosci`, `IRozliczalny`) | to z nią rozlicza się zapłatę |
| Rozliczenie (powiązanie SP) | `Soneta.Kasa.RozliczenieSP` (tabela `RozliczeniaSP`, `IRozliczenie`) | wiąże zapłatę z płatnością/dokumentem |
| Rozrachunek | `Soneta.Kasa.RozrachunekIdx` (tabela `RozrachunkiIdx`) | indeks rozrachunkowy podmiotu |
| Stan rozliczenia zapłaty | `Wyplata.StanRozliczenia: Soneta.Kasa.StanRozliczenia`, `Wyplata.DoRozliczenia`, `Wyplata.KwotaRozliczona`, `Wyplata.Rozliczono` | ile pozostało / czy rozliczono |
**Kolekcje na zapłacie (`Soneta.Kasa.Wyplata`):**
- `Wyplata.Zaplaty: SubTable<RozliczenieSP>` oraz `Wyplata.Dokumenty: SubTable<RozliczenieSP>`
rozliczenia,
- `Wyplata.Rozrachunki: SubTable<RozrachunekIdx>` — rozrachunki,
- `Wyplata.PreliminarzPoz: PreliminarzPozycja` — pozycja preliminarza.
**Kolekcje na `Pracownik` (rozrachunki/faktury podmiotu):**
- `Pracownik.Rozrachunki`, `Pracownik.DokumentyRozliczeniowe`,
`Pracownik.DokumentyPreliminarza` (jak w tabeli KADRY-I4). **Uwaga:** `Pracownik.Platnosci` **nie istnieje**
kolekcja `Platnosci` jest tylko na `IDokumentPlatny` (np. `Kasa.Wyplata.Platnosci`).
**Workery rozliczeniowe (publiczny kontrakt, akcje menu):**
| Worker | Rola |
|---|---|
| `Soneta.Kasa.RozliczWgPrzelewowWyplataWorker` | rozliczenie zapłaty wg przelewów |
| `Soneta.Kasa.RozliczPreliminarzIdxWorker` / `...TblWorker` / `...FrmWorker` | rozliczenie z preliminarzem |
| `Soneta.Kasa.PreliminarzPozycja.DodajRozliczenieWorker` | dodanie rozliczenia do pozycji preliminarza |
| `Soneta.Ksiega.UtworzPlatnoscZZapisuWorker` | utworzenie płatności z zapisu (księga) |
**Faktura zbiorcza:** powstaje po stronie **handlowej** — z wielu zapłat/płatności tworzy się jeden
dokument handlowy (faktura) zbiorąc je jako rozliczenia. To domena `handel.md`
(wystawianie i rozliczanie faktur), nie kartoteki pracownika. Z poziomu rozliczeń pracownika
publiczny kontrakt udostępnia **odczyt i rozliczanie** rozrachunków, a nie „wystaw fakturę”.
**Snippet (odczyt stanu rozliczenia zapłat — publiczny kontrakt):**
```csharp
using Soneta.Kasa; // Wyplata, StanRozliczenia
using Soneta.Types;
// Zapłaty pracownika rozliczane z dokumentami (np. fakturami) — odczyt stanu rozliczeń.
// Iteruj zawsze w zakresie/okresie (tabela operacyjna guided — safe-code §6.3).
foreach (RozrachunekIdx r in pracownik.Rozrachunki)
{
// r — pozycja rozrachunkowa pracownika (powiązanie zapłata ↔ dokument)
}
// Stan rozliczenia konkretnej zapłaty kasowej:
// Wyplata zaplata = ...;
// var doRozl = zaplata.DoRozliczenia; // ile pozostało do rozliczenia (Currency)
// var stan = zaplata.StanRozliczenia; // StanRozliczenia (enum)
// var czyRozl = zaplata.Rozliczono; // bool
```
**Pułapki / ograniczenia (bądź szczery):**
- **„Wystaw fakturę z pracownika/płac” nie istnieje w publicznym kontrakcie.** Faktura to dokument
handlowy; powiązanie z zapłatą realizują **rozrachunki/rozliczenia** (moduł Kasa), nie kartoteka
pracownika. To zadanie jest z pogranicza domen — opis kierujemy do `handel.md`.
- Pola rozliczeniowe (`DoRozliczenia`, `Stan`, `StanRozliczenia`, `KwotaRozliczona`, `Rozliczono`,
`Rozrachunki`) są na **`Soneta.Kasa.Wyplata`** (`IDokumentPlatny`), a nie na płacowej
`Soneta.Place.Wyplata`.
- Rozliczanie/tworzenie faktury zbiorczej **wymaga skonfigurowanego modułu Kasa/Handel** (definicje
dokumentów, rachunki, płatności). W bazie Demo część konfiguracji może nie być gotowa — operacje
zapisujące są niepewne w teście (patrz spec testowy).
- `Platnosc`/`RozliczenieSP`/`RozrachunekIdx` to obiekty operacyjne — przy odczycie filtruj zakresem
i nie skanuj całych tabel (safe-code §6.3).
---
#### Spec testowy (zwarty) — KADRY-I4 / KADRY-I5 / KADRY-I6
Konwencja: `Soneta.Skills.Test/KadryPlace/Pracownik/`, klasa `RozdzialI_ListyWydrukiTest`
(lub nowa `RozdzialI_PrzelewyRozliczeniaTest : PracownikTestBase`); baza Demo + rollback;
operujemy wyłącznie na publicznym kontrakcie.
**KADRY-I4 — `I4_PrzygotujPrzelewy_ZListyPlac`**
- *Co testowalne:* naliczenie wypłaty etatowej (`NaliczanieSeryjne.Pracownika`, jak KADRY-I1b) → uzyskanie
`ListaPlac` z `Wyplata.ListaPlac`; **konstrukcja** `ListaPlac.PrzygotujPrzelewyWorker` z `Params`
(asercja, że worker i typ `Params` istnieją w publicznym API; że pola `Data`/`Paczka`/`ZRachunku`
są dostępne). Odczyt kolekcji `Pracownik.Przelewy`, `Pracownik.DokumentyPreliminarza`,
`Pracownik.Rozrachunki` (asercja: kolekcje dostępne, iterowalne).
- *Niepewne / `[Ignore]`/`Assert.Ignore`:* faktyczne **wywołanie** `worker.PrzygotujPrzelewy()` i
powstanie dokumentów `PrzelewBase` — zależy od konfiguracji modułu Kasa (definicja paczki,
`RachunekBankowyFirmy`, rachunek pracownika `Pracownik.Rachunki`), której baza Demo nie gwarantuje.
Owinąć w `try/catch` + `Assert.Ignore` z opisem (wzorzec jak KADRY-I2/KADRY-I3) i asercję na powstaniu
przelewu robić tylko, gdy się udało.
**KADRY-I5 — `I5_EksportPrzelewow_KontraktApi`**
- *Co testowalne:* **istnienie publicznego API** — weryfikacja **refleksją** (NIE instancjonuj!):
typ `EksportPrzelewowParams`, konstruktor `(Context, RachunekBankowyFirmy, PrzelewBase[])`,
property `FileName`; typ `EksportPrzelewowWorker`, property `Params`, metoda `Eksport`.
**Nie używaj inicjalizatora `new EksportPrzelewowParams { ... }`** — nie ma ctora bezparametrowego,
a ctor `(ctx, rachunek, przelewy)` rzuca `ApplicationException`, gdy `rachunek == null` (brak konfiguracji w Demo).
- *Niewykonalne w teście jednostkowym → `[Ignore]`:* wywołanie `worker.Eksport()` — **operacja
plikowa** (zapis na dysk wg `FileName`), zależna od formatu banku/sterownika; wysyłka online =
operacja sieciowa. **Nie wołać `Eksport()`** w teście; udokumentować jako `[Ignore("operacja
plikowa/sieciowa — poza testem jednostkowym")]`.
**KADRY-I6 — `I6_Rozliczenia_OdczytStanu`**
- *Co testowalne:* odczyt kolekcji rozliczeniowych pracownika — `Pracownik.Rozrachunki`,
`Pracownik.DokumentyRozliczeniowe`, `Pracownik.DokumentyPreliminarza`
(asercja: dostępne, iterowalne, typy zgodne — `RozrachunekIdx`, `DokRozliczBase`,
`PreliminarzDokument`). **`Pracownik.Platnosci` NIE istnieje** — pomiń (kolekcja `Platnosci` jest tylko na
`IDokumentPlatny`); odczyt pól rozliczeniowych z `Soneta.Kasa.Wyplata` (`DoRozliczenia`,
`Stan`, `StanRozliczenia`, `Rozliczono`) — gdy istnieje zapłata kasowa w Demo.
- *Niewykonalne / `[Ignore]`:* **wystawienie faktury (zbiorczej) z zapłaty** — funkcja handlowa,
brak w kontrakcie pracownika; rozliczanie zapisujące (`RozliczWgPrzelewowWyplataWorker`,
`RozliczPreliminarz*Worker`) wymaga konfiguracji Kasa/Handel → `Assert.Ignore` przy braku danych.
Dla wystawiania faktur kierować do testów domeny handlowej (`handel.md`).
**Dokładne nazwy (do użycia w testach):**
- Worker płacowy: `Soneta.Place.ListaPlac.PrzygotujPrzelewyWorker` (+ zagn. `.Params`;
akcja `PrzygotujPrzelewy`).
- Worker eksportu: `Soneta.Kasa.EksportPrzelewowWorker` + `Soneta.Kasa.EksportPrzelewowParams`
(akcja `Eksport`); paczki: `Soneta.Kasa.EksportPaczekPrzelewowWorker`.
- Dokumenty: `Soneta.Kasa.PrzelewBase` (tabela `Przelewy`), `Soneta.Kasa.PaczkaPrzelewow`
(tabela `PaczkiPrzelewow`), `Soneta.Kasa.DefinicjaPaczkiPrzelewu`, `Soneta.Kasa.RachunekBankowyFirmy`.
- Rozliczenia: `Soneta.Kasa.Platnosc`, `Soneta.Kasa.RozliczenieSP`, `Soneta.Kasa.RozrachunekIdx`,
`Soneta.Kasa.PreliminarzDokument`, `Soneta.Kasa.PreliminarzPozycja`.
- Zapłata kasowa (`IDokumentPlatny`): `Soneta.Kasa.Wyplata` (NIE `Soneta.Place.Wyplata`).
- Kolekcje na `Pracownik`: `Przelewy`, `Rozrachunki`, `DokumentyPreliminarza`,
`DokumentyRozliczeniowe`, `Rachunki` (**bez `Platnosci`** — ta kolekcja jest tylko na `IDokumentPlatny`).
@@ -0,0 +1,352 @@
# KADRY10 — Deklaracje (ZUS, PIT, PFRON, PPK)
> Wspólne fakty o typie, podstawowe typy i szablon wzorca: [../kadry.md](../kadry.md).
> **Moduł.** `Soneta.Deklaracje.DeklaracjeModule` — dostęp z sesji przez `session.GetDeklaracje()`.
> Wszystkie deklaracje (ZUS, PIT, PFRON, PPK) to wiersze tabeli `Deklaracje`, dziedziczące po
> abstrakcyjnej klasie root `Soneta.Deklaracje.Deklaracja` (`GuidedRow`, implementuje m.in.
> `IDeklaracja`, `IDokumentPlatny`, `IDokumentKsiegowalny`). Konkretne typy żyją w podprzestrzeniach:
> `Soneta.Deklaracje.ZUS.*`, `Soneta.Deklaracje.PIT.*`, `Soneta.Deklaracje.PFRON.*`,
> `Soneta.Deklaracje.PPK.*`.
>
> **Rozróżnienie kluczowe dla testów — NALICZENIE/UTWORZENIE vs E-WYSYŁKA.**
> - **Naliczenie/utworzenie deklaracji** (workery `*Worker` z akcjami „Przygotuj…/Nalicz…/Przelicz”,
> operacje PPK) tworzy **wiersze w bazie** — to operacja lokalna, w zasadzie testowalna na Demo,
> ale **wymaga `Context`** (i dla ZUS zwykle obiektu `KEDU`). Workery nie mają konstruktorów
> bezparametrowych dających pełny kontrakt — `Params` budujemy z `Context`/`Session`.
> - **E-wysyłka** to osobne typy: `EDeklaracja` (tabela `EDeklaracje` — XML, podpis, UPO) oraz
> `ETransmisja` (tabela `ETransmisje` — pojedyncze transmisje do bramki). Eksport KEDU/PUE realizują
> workery `Soneta.Deklaracje.UI.KeduEksportForm.EksportWorker` (akcje „Eksport KEDU”, „Pobierz KEDU”)
> i `Soneta.Deklaracje.UI.PUEEksportForm.EksportWorker` (akcja „Eksport PUE (RUD)”), a uruchomienie
> Programu Płatnika — `Soneta.Deklaracje.ZUS.DeklaracjaZUS.UruchomPPWorker` (akcja
> „Uruchom 'Program Płatnika'”). **To operacje sieciowe/plikowe/zewnętrzne — NIE do testu** (nawet
> utworzenie `EDeklaracja` wymaga podpisu i bramki ZUS/US).
>
> **`KEDU` (`Soneta.Deklaracje.ZUS.KEDU`)** — „zestaw deklaracji”: kontener (komplet dokumentów ZUS),
> do którego workery zgłoszeniowe i rozliczeniowe dopinają wygenerowane bloki. Praktycznie każdy worker
> ZUS przyjmuje `Kedu` w swoich `Params`; bez przekazanego `KEDU` generowanie deklaracji ZUS nie ma
> gdzie zapisać wyniku. KEDU nie jest tworzony „w locie” w sposób trywialny — jest częścią mechanizmu
> deklaracji rozliczeniowych ZUS i jego zbudowanie wymaga środowiska modułu Deklaracje (`Context`).
---
### KADRY-J1 — Zgłoszenia ZUS (ZUA/ZZA, ZCNA, ZWUA)
**Cel:** zgłosić/wyrejestrować pracownika i jego umowy w ZUS oraz zgłosić członków rodziny do
ubezpieczenia zdrowotnego. Typy zgłoszeń to wiersze deklaracji: `ZUA` (społeczne + zdrowotne),
`ZZA` (tylko zdrowotne), `ZCNA` (rodzina), `ZWUA` (wyrejestrowanie), `ZIUA` (zmiana danych
identyfikacyjnych), `ZCZA` (zmiana danych członka rodziny) — wszystkie w `Soneta.Deklaracje.ZUS`.
**Workery — poziom `Pracownicy` (klasy zagnieżdżone `Soneta.Deklaracje.ZUS.ZarejestrujPracownikówWorker`):**
| Worker (akcja) | `Params` (typ) | Pola `Params` | Metoda akcji |
|---|---|---|---|
| `ZarejestrujPracownikówWorker.Rejestracja` — „Deklaracje ZUS/Przygotuj ZUA i ZZA” | `ZarejestrujBaseWorker.ParamsKor` | `Okres: FromTo`, `DataDokumentu`/`DataWypełnienia: Date`, `Kedu: KEDU`, `KorektaZmiana: ZgloszenieZUS.KorektaZmiana`, `ZarejestrujRodzinę: bool` | `object ZarejestrujPracowników()` |
| `ZarejestrujPracownikówWorker.Rodzina` — „Deklaracje ZUS/Przygotuj ZCNA” | `ZarejestrujBaseWorker.Params` | `Okres`, `DataDokumentu`, `DataWypełnienia`, `Kedu` | `object ZarejestrujRodzinę()` |
| `ZarejestrujPracownikówWorker.Wyrejestrowanie` — „Deklaracje ZUS/Przygotuj ZWUA” | `Wyrejestrowanie.ParamsWR` | `Okres`, `DataDokumentu`, `DataWypełnienia`, `Kedu`, `RIA: bool`, `WyrejestrujRodzinę: bool` | `object WyrejestrujPracowników()` |
| `ZarejestrujPracownikówWorker.ZgloszenieUmow` — „Deklaracje ZUS/Przygotuj RUD” | `ZgloszenieUmow.UParams` | `Okres`, `DataWypełnienia`, `Kedu`, `Trwajace: bool` | `object ZgłośUmowy()` |
> Worker przyjmuje zaznaczone osoby przez `Pracownicy: Pracownik[]` (`[Context]`). Wszystkie `Params`
> mają ctor `(Context)`. Po akcji wynik (lista wygenerowanych deklaracji) odczytasz z bazowego
> `Deklaracje: View`, a `Save()` zatwierdza.
**Workery — poziom `Umowy` (zleceniobiorcy), `Soneta.Deklaracje.ZUS.ZarejestrujUmowyWorker`**
opisane w **KADRY-G5** (`Rejestracja.ZarejestrujUmowy()` → ZUA/ZZA wg schematu `UmowaHistoria.Ubezpieczenia`,
`Wyrejestrowanie.WyrejestrujUmowy()` → ZWUA). `ParamsZ`/`ParamsW` mają ctor `(Context)`; pola
bazowe `Okres`/`DataDokumentu`/`DataWypełnienia`/`Kedu` + `ZarejestrujRodzinę`/`WyrejestrujRodzinę`.
**ZCNA na rodzinie (KADRY-A9).** Zgłoszenie członka rodziny do ubezpieczenia zdrowotnego startuje z danych
`CzlonekRodziny` (`Ubezpieczony = true`, `UbezpieczenieOkres`, `StPokrewienstwa` — patrz KADRY-A9), a samą
deklarację ZCNA generuje `ZarejestrujPracownikówWorker.Rodzina` (lub `Rejestracja` z
`Pars.ZarejestrujRodzinę = true`). Dla zleceniobiorcy analogicznie przez `ZarejestrujUmowyWorker`.
**Przerejestrowanie (KADRY-A19).** `Soneta.Deklaracje.UI.PrzerejestrowaniePracownikaWorker` (DataType
`PracHistoria`) oraz `Soneta.Deklaracje.UI.PrzerejestrowanieZleceniobiorcyWorker` (DataType
`UmowaHistoria`) — generują ZWUA+ZUA przy zmianie tytułu/wydziału. `Params` wymaga `KEDU` + `Context`.
**Snippet (przygotowanie ZUA/ZZA dla zaznaczonych pracowników):**
```csharp
var pracownik = session.GetKadry().Pracownicy.WgKodu["006"];
var pars = new Soneta.Deklaracje.ZUS.ZarejestrujBaseWorker.ParamsKor(context)
{
Okres = new FromTo(new Date(2026, 1, 1), Date.MaxValue),
DataDokumentu = new Date(2026, 1, 1),
DataWypełnienia = Date.Today,
Kedu = kedu, // KEDU z modułu Deklaracje (Context)
ZarejestrujRodzinę = false,
};
var rejestracja = new Soneta.Deklaracje.ZUS.ZarejestrujPracownikówWorker.Rejestracja
{
Pracownicy = new[] { pracownik },
Pars = pars,
};
rejestracja.ZarejestrujPracowników(); // tworzy ZUA/ZZA (i ZCNA, gdy ZarejestrujRodzinę)
session.Save();
```
**Pułapki:**
- Typ zgłoszenia (ZUA vs ZZA) wynika ze schematu ubezpieczeń (`Etat.Ubezpieczenia` / `UmowaHistoria.Ubezpieczenia`,
KADRY-A7/KADRY-G5) — nie z parametru workera. Ustaw `Tyub4` i flagi `Spoleczne`/`Zdrowotne` przed zgłoszeniem.
- Każdy `Params` wymaga `Context` (ctor `(Context)`) i pola `Kedu` — bez `KEDU` deklaracja nie ma
kontenera docelowego. Operacja jest **lokalna** (zapis wiersza), ale niewykonalna bez `Context`/`KEDU`.
- `ZWUA` z `RIA = true` powiązany jest z mechanizmem RIA (KADRY-J2).
- Workery zgłoszeniowe na `Pracownicy` dotyczą etatowych; na `Umowy` — zleceniobiorców (KADRY-G5).
---
### KADRY-J2 — Deklaracje rozliczeniowe ZUS (DRA, RIA, IMIR, RUD, IWA; KEDU)
**Cel:** naliczyć/utworzyć deklaracje rozliczeniowe i informacyjne ZUS. Typy (`Soneta.Deklaracje.ZUS`,
wiersze tabeli `Deklaracje`): `DRA` (deklaracja rozliczeniowa z załącznikami RCA/RSA/RZA; ctor `(KEDU)`),
`RIA` (informacja roczna / raport po ustaniu zatrudnienia; ctor `(Pracownik, KEDU)`), `RMUA`
informacja miesięczna dla ubezpieczonego, potocznie **IMIR** (ctor `(Pracownik, RMUA.TypOkresuDeklaracji)`;
**brak osobnego typu `IMIR` w CLR — to `RMUA`**), `RUD` (zgłoszenie umowy o dzieło), `IWA` (informacja o wypadkach/składce wypadkowej),
`OSW` (oświadczenie), `Z3`/`Z3a` (zaświadczenia płatnika ERP-7 — patrz niżej), `KEDU` (zestaw).
**Naliczanie seryjne — poziom `Pracownicy`:**
| Worker (akcja) | `Params` (typ) | Pola `Params` | Metoda |
|---|---|---|---|
| `Soneta.Deklaracje.ZUS.NaliczanieSeryjneRIAWorker` — „Deklaracje ZUS/Przygotuj RIA” | `…RIAWorker.Params` | `DataDokumentu`/`DataWypełnienia: Date`, `Kedu: KEDU`, `Wydział: Wydzial`, `Wszystkie: bool`, `Zerowa: bool` | `object NaliczRMUA(Context)` |
| `Soneta.Deklaracje.ZUS.NaliczanieSeryjneRMUAWorker` — „Deklaracje ZUS/Przygotuj IMIR” | `…RMUAWorker.Params` | `DataDokumentu`/`DataWypełnienia: Date`, `Miesiac: YearMonth`, `Rok: int`, `TypOkresu: RMUA.TypOkresuDeklaracji`, `Oskladkowani: bool`, `Wydział`, `Wszystkie` | `object NaliczRMUA(Context)` |
> Oba workery mają **ctor bezparametrowy**, przyjmują `Pracownicy: Pracownik[]` (`[Context]`) i mają w props
> `Context`, `Kedu`, `Deklaracje: View`. Metoda akcji `NaliczRMUA(Context)` (ta sama nazwa dla RIA i RMUA).
> `Params` są property `Pars` (setter); na workerze `RMUAWorker` pola `Params` są też wystawione bezpośrednio jako property.
**Przeliczenie pojedynczej deklaracji — `Soneta.Deklaracje.DeklaracjaWorker`** (DataType `Deklaracja`,
więc działa dla **dowolnej** deklaracji ZUS/PIT/PFRON): akcja **„Przelicz”** → `void Przelicz()`;
parametr `Deklaracja: Soneta.Deklaracje.Deklaracja` (`[Context]`).
**RUD** generuje `ZarejestrujPracownikówWorker.ZgloszenieUmow` (KADRY-J1) lub jest dostępna na liście umów.
**DRA z załącznikami** to root `DeklaracjaZUS`; nalicza się przez mechanizm KEDU + `Przelicz`.
**E-wysyłka (NIE testować):** eksport KEDU — `KeduEksportForm.EksportWorker` („Eksport KEDU”,
„Pobierz KEDU”); eksport PUE/RUD — `PUEEksportForm.EksportWorker` („Eksport PUE (RUD)”); Program
Płatnika — `DeklaracjaZUS.UruchomPPWorker`.
**Pułapki:**
- `KEDU` jest osią całego rozliczenia ZUS — wszystkie workery rozliczeniowe wpisują wynik do
przekazanego `Kedu`. Bez modułu Deklaracje (`Context`) i `KEDU` operacji nie złożysz.
- `DeklaracjaWorker.Przelicz()` przelicza **istniejący** wiersz deklaracji — najpierw musi powstać
(np. z naliczania seryjnego), więc to nie jest „utworzenie od zera”.
---
### KADRY-J3 — Deklaracje PIT (PIT-11, PIT-4R, PIT-8AR, PIT-R, IFT-1/IFT-1R, PIT-8C)
**Cel:** naliczyć imienne i zbiorcze deklaracje podatkowe. Typy (`Soneta.Deklaracje.PIT`, wiersze
tabeli `Deklaracje`): `PIT11`, `PIT4`/PIT-4R (rozliczeniowa zaliczek), `PIT8A`/PIT-8AR (zryczałtowany),
`PITR` (PIT-R), `IFT1`/`IFT1R`, `PIT8C`, `PIT40`, plus `ZbiorczaPIT`/`IEDeklaracjaZbiorczaItem`
(deklaracje zbiorcze).
**Naliczanie seryjne — poziom `Pracownicy` (klasy zagnieżdżone `Soneta.Deklaracje.PIT.NaliczanieSeryjne`):**
| Worker (akcja) | Ctor | `Params` — pola | Metoda |
|---|---|---|---|
| `NaliczanieSeryjne.PIT_11Worker` — „Deklaracje PIT/Nalicz PIT 11” | `(Session session)` | `Okres: FromTo`, `Data: Date`, `Naliczaj: NaliczanieDeklaracje`, `BezPotwierdzenia: bool`, dane podpisującego (`Imię`/`Nazwisko`/`Stanowisko` + `…Odp`), `TreśćUzasadnienia: string` | `object Nalicz_PIT_11()` |
| `NaliczanieSeryjne.PIT_RWorker` — „Deklaracje PIT/Nalicz PIT R” | `(Session)` | jw. (`Params`) | `Nalicz…()` |
| `NaliczanieSeryjne.PIT_8CWorker` — „Deklaracje PIT/Nalicz PIT 8C” | `(Session)` | jw. | `Nalicz…()` |
| `NaliczanieSeryjne.IFT_1Worker` / `IFT_1RWorker` — „Deklaracje PIT/Nalicz IFT-1 / IFT-1R” | `(Session)` | jw. | `Nalicz…()` |
> `Params` mają ctor `(Context)`; worker `PIT_11Worker` dodatkowo ma ctor `(Session)`. Zaznaczeni
> pracownicy przez `[Context]`.
**Deklaracje płatnika (PIT-4R/PIT-8AR)** są zbiorcze na poziomie podmiotu/oddziału (`PIT4`/`PIT8A`,
`ZbiorczaPIT`) — tworzone/dodawane workerami zbiorczymi (`DodajDoZbiorczejPITWorker`,
`WybierzDeklaracjeDoZbiorczejPITWorker`) i przeliczane `DeklaracjaWorker.Przelicz()` (KADRY-J2) lub
dedykowanymi `…PrzeliczWorker` (np. `PITR.PrzeliczWorker`, `PIT8S.PrzeliczWorker`).
**Snippet (naliczenie PIT-11 dla zaznaczonych pracowników):**
```csharp
var pracownicy = new[] { session.GetKadry().Pracownicy.WgKodu["006"] };
var worker = new Soneta.Deklaracje.PIT.NaliczanieSeryjne.PIT_11Worker(session)
{
Pracownicy = pracownicy,
};
worker.Pars.Okres = FromTo.Year(2025); // rok podatkowy
worker.Pars.Data = Date.Today;
worker.Nalicz_PIT_11(); // tworzy wiersze PIT11 w tabeli Deklaracje
session.Save();
```
**Pułapki:**
- Naliczenie PIT bazuje na naliczonych wypłatach (H) i bilansach otwarcia PIT (KADRY-J6) — bez danych
źródłowych deklaracja będzie zerowa.
- Sygnatury `Params` PIT mają ctor `(Context)`; `PIT_11Worker` ma też ctor `(Session)` — w teście
użyj `(session)` + ustaw `Pracownicy`/`Pars`.
- **E-wysyłka PIT to `EDeklaracja`/`ETransmisja` (bramka MF) — NIE testować.** Samo naliczenie
wiersza PIT jest lokalne (zapis do bazy).
---
### KADRY-J4 — Deklaracje PFRON (Wn-D, INF-2, DEK-R, INF-D-P)
**Cel:** utworzyć/naliczyć deklaracje PFRON. Typy (`Soneta.Deklaracje.PFRON`, wiersze tabeli
`Deklaracje`): `WN_D` (Wn-D — wniosek o dofinansowanie), `WN_U` (Wn-U), `INF_D`/`INF_D_P`
(informacje o pracownikach niepełnosprawnych — załączniki do Wn-D), `INF_2` (informacja roczna),
`DEK_R` (deklaracja roczna wpłat).
**Workery:**
- `Soneta.Deklaracje.DeklaracjaWorker` — akcja **„Przelicz”** (`Przelicz()`) dla każdego z typów PFRON
(są DataType `Deklaracja`).
- `Soneta.Deklaracje.PFRON.INF_D.InfoWorker`, `…INF_D_P.InfoWorker` — properties informacyjne (UI).
- **E-wysyłka SOD (NIE testować):** `Soneta.Deklaracje.UI.SODEksportForm.EksportWorker` (DataType
`WN_D`/`WN_U`/`INF_D`) — eksport do systemu SODiR.
**Dane źródłowe** PFRON pochodzą z `PracHistoria.PFRON` (KADRY-A13: stopień niepełnosprawności, efekt
zachęty, schorzenia SOD) — bez nich deklaracja będzie pusta.
**Pułapki:**
- PFRON nie ma dedykowanego „NaliczanieSeryjne” na `Pracownicy` — deklarację (`WN_D` itd.) tworzy się
w module Deklaracje, a przelicza `DeklaracjaWorker.Przelicz()`. Tworzenie/edycja wymaga `Context`.
- Konfiguracja procentów/odpisu PFRON to workery na `OddzialFirmy`
(`Soneta.Deklaracje.Config.*PFRON*Worker`) — to dane konfiguracyjne, nie deklaracje.
---
### KADRY-J5 — Operacje PPK
**Cel:** obsłużyć cykl życia uczestnictwa w PPK — kwalifikacja/auto-zapis, rejestracja uczestnika,
rezygnacja, wznowienie, zmiana danych, zakończenie zatrudnienia, dokumenty i rozliczenie składek.
Typy dokumentów PPK (`Soneta.Deklaracje.PPK`, wiersze tabeli `Deklaracje`): `RejestracjaUczestnikaPPK`,
`DeklaracjaUczestnikaPPK`, `ZmianaDanychIdentyfikacyjnychUczestnikaPPK`,
`ZmianaDanychKontaktowychUczestnikaPPK`, `ZakończenieZatrudnieniaUczestnikaPPK`, `TransferPPK`,
`WypłataTransferowaPPK`, `WypłataŚrodkówPrzezUczestnikaPPK`, `ZwrotŚrodkówPPK`, `RozliczenieSkładekPPK`,
`RozliczenieNadpłatPPK`, `ZwrotNadpłatyPPK`, `NadanieUczestnikowiNumeruPPK`,
`DokumentyPracodawcyPPK`, `DokumentyInstytucjiFinansowejPPK`.
**Workery operacji PPK — poziom `Pracownicy` (zagnieżdżone `Soneta.Deklaracje.PPK.DeklaracjePPKPracownikówWorker`),
wspólny `Params = DeklaracjePPKBaseWorker.Params` (`Okres: FromTo`, `DokumentPPK: DokumentyPracodawcyPPK`):**
| Worker (akcja) | Metoda |
|---|---|
| `…Worker.Rejestracja` — „Operacje PPK/Rejestracja uczestnika” | `object RejestracjaPracownikow()` |
| `…Worker.Rezygnacja` — „Operacje PPK/Rezygnacja uczestnika” | `object RezygnacjaPracownikow()` |
| `…Worker.Wznowienie` — „Operacje PPK/Automatyczne wznowienie uczestnictwa” | `object WznowieniePracownikow()` |
| `…Worker.ZakończenieZatrudnienia` — „Operacje PPK/Zakończenie zatrudnienia uczestnika” | `object ZakończenieZatrudnieniaPracownikow()` |
| `…Worker.ZmianaDanychIdentyfikacyjnych` — „Operacje PPK/Zmiana danych identyfikacyjnych” | `object ZmianaDanychIdentyfikacyjnychPracownikow()` |
> Przystąpienie/auto-zapis i zmiana procentu składki realizowane są na poziomie **pracownika**
> (dane PPK pracownika), nie tymi workerami zbiorczymi.
**Workery na pracowniku (kwalifikacja PPK) — `Soneta.Kadry.Pracownik`:**
| Worker | Ctor | Wybrane pola/props |
|---|---|---|
| `Pracownik.PPKWorker` (alias `PPK`) | `(Context context)` | `Data: Date`, `Idx: Pracownik`; `Kwalifikacja: PPKWorker.RodzajZgłoszenia`, `DataKwalifikacji[/Min/Max]: Date`, `Kwalifikacja[Min/Max]` |
| `Pracownik.AutoZapisPPKWorker` (alias `AutoZapisPPK`) | `(Context context)` | `Data: Date`, `Pracownik: Pracownik`; `Kwalifikacja: AutoZapisPPKWorker.CzyAutoZapisPPK` |
> Te workery służą do **odczytu kwalifikacji** (czy/kiedy pracownik podlega przystąpieniu lub
> auto-zapisowi do PPK na dany dzień) — mają ctor `(Context)`.
**Przeliczanie/rozliczenie PPK:**
- `Soneta.Deklaracje.PPK.PrzeliczPPKWorker` (DataType m.in. `RozliczenieNadpłatPPK`,
`WypłataTransferowaPPK`, `WypłataŚrodkówPrzezUczestnikaPPK`, `ZwrotŚrodkówPPK`,
`NadanieUczestnikowiNumeruPPK`) — przelicza dokument rozliczeniowy PPK.
- `Soneta.Deklaracje.PPK.NadanieNumeruPPKWorker` (DataType `NadanieUczestnikowiNumeruPPK`).
- `RozliczenieSkładekPPK` / `RejestracjaUczestnikaPPK` / `DeklaracjaUczestnikaPPK` przeliczane przez
`DeklaracjaWorker.Przelicz()` (KADRY-J2, DataType `Deklaracja`).
**E-wysyłka / import-eksport PPK (NIE testować):**
- `Soneta.Deklaracje.PPK.DokumentyPPKEksportWorker` (DataType `DokumentyPracodawcyPPK`,
`DokumentyInstytucjiFinansowejPPK`) — eksport do instytucji finansowej.
- `Soneta.Deklaracje.PPK.DokumentyPPKImportWorker` (DataType `DokumentyInstytucjiFinansowejPPK`) —
import zwrotny.
**Snippet (rejestracja uczestnika PPK dla zaznaczonych):**
```csharp
var pracownicy = new[] { session.GetKadry().Pracownicy.WgKodu["006"] };
var pars = new Soneta.Deklaracje.PPK.DeklaracjePPKBaseWorker.Params(context)
{
Okres = FromTo.Year(2026),
// DokumentPPK = … (DokumentyPracodawcyPPK z modułu Deklaracje)
};
var rej = new Soneta.Deklaracje.PPK.DeklaracjePPKPracownikówWorker.Rejestracja
{
Pracownicy = pracownicy,
Pars = pars,
};
rej.RejestracjaPracownikow(); // tworzy dokumenty rejestracji uczestnika PPK
session.Save();
```
**Pułapki:**
- Zmiana procentu składki PPK / przystąpienie to dane **pracownika** (deklaracja uczestnika PPK,
`DeklaracjaUczestnikaPPK`) — workery zbiorcze obejmują rejestrację, rezygnację, wznowienie, zmianę
danych identyfikacyjnych i zakończenie zatrudnienia.
- `DeklaracjePPKBaseWorker.Params` ma ctor `(Context)`; operacja jest lokalna (tworzy wiersze
dokumentów PPK), ale niewykonalna bez `Context` i zwykle `DokumentPPK`.
- `PPKWorker`/`AutoZapisPPKWorker` na pracowniku są **diagnostyczne** (kwalifikacja na dzień), nie
tworzą dokumentów — i wymagają `Context`.
---
### KADRY-J6 — Bilanse otwarcia deklaracji (PIT, ZUS, ERP-7) przy wdrożeniu
**Cel:** wprowadzić dane historyczne sprzed startu systemu, potrzebne do poprawnego naliczenia
deklaracji w pierwszym okresie. Bilanse są **kolekcjami na pracowniku** (`SubTable`) — tworzy się je
i odczytuje czystym API biznesowym, **bez `Context`/`KEDU`/sieci**.
**Kolekcje na `Soneta.Kadry.Pracownik`:**
| Kolekcja | Typ | Przeznaczenie |
|---|---|---|
| `Pracownik.BilansyOtwarciaPIT` | `SubTable<Soneta.Place.BilansOtwarciaPIT>` | bilans otwarcia PIT (przychody/koszty/składki na start) |
| `Pracownik.WynagrodzeniaERP7` | `SubTable<Soneta.Kalend.WynagrodzenieERP7>` | wynagrodzenia do ERP-7 / Z-3 |
| `Pracownik.NieobecnosciERP7` | `SubTable<Soneta.Kalend.NieobecnoscERP7>` | nieobecności do ERP-7 / Z-3 |
| `Pracownik.DeklaracjePodmiotu` | `SubTable` | deklaracje powiązane z pracownikiem-podmiotem |
**Typ `Soneta.Place.BilansOtwarciaPIT`** (root `GuidedRow`, tabela `BilansyOtwPIT`) jest
**ABSTRAKCYJNY** — instancjonuje się jedną z konkretnych wersji odpowiadających wartościom enuma
`Soneta.Place.WersjaBilansuOtwarciaPIT` (`PIT11_11`, `PIT11_29`):
`Soneta.Place.BilansOtwarciaPIT_11` (Wersja = `PIT11_11`) lub `Soneta.Place.BilansOtwarciaPIT_29`
(Wersja = `PIT11_29`). Konkretne klasy mają publiczny ctor `(Pracownik pracownik)`; bazowy
`BilansOtwarciaPIT` ma ctor `(Pracownik, WersjaBilansuOtwarciaPIT)`, ale jest abstrakcyjny.
Property `Pracownik` i `Wersja`**read-only** (ustawiane przez ctor; brak ctora bezparametrowego).
Pola bazodanowe m.in.: `Data: Date`, kwoty przychodów/kosztów/składek w rozbiciu
etat/umowa/macierzyński (`Przychod26ZwolEtat`, `Przychod26ZwolUmowa`, `PrzychodUlgaEtat`,
`PrzychodUlgaUmowa`, `Spoleczne`, `Spoleczne26`, `Zdrowotne9Procent`, `SkladkiCzlonkowskie` itd.)
oraz kolekcja `Elementy: SubTable<Soneta.Place.ElementBilansuOtwarciaPIT>`.
**ERP-7** (wcześniej druk ZUS Rp-7) opiera się na `WynagrodzeniaERP7`/`NieobecnosciERP7` pracownika
oraz zaświadczeniach `Soneta.Deklaracje.ZUS.Z3`/`Z3a` (workery `ZUSZ3.Z3Worker`/`Z3aWorker` na
`Nieobecnosc`) — sam druk Z-3/ERP-7 to generowanie dokumentu w module Deklaracje.
**Snippet (dodanie bilansu otwarcia PIT i odczyt):**
```csharp
var pracownik = session.GetKadry().Pracownicy.WgKodu["006"];
using (var t = session.Logout(editMode: true))
{
// BilansOtwarciaPIT jest abstrakcyjny — tworzymy konkretną wersję (_29 => PIT11_29, _11 => PIT11_11).
// Pracownik ustawia ctor (property read-only), więc NIE używamy inicjalizatora obiektu na Pracownik.
var bo = session.AddRow(new Soneta.Place.BilansOtwarciaPIT_29(pracownik));
bo.Data = new Date(2026, 1, 1);
bo.PrzychodUlgaEtat = 12000m;
bo.Spoleczne = 1645.20m;
t.Commit();
}
session.Save();
// Odczyt bilansów otwarcia PIT pracownika (typ kolekcji: SubTable<BilansOtwarciaPIT>):
foreach (Soneta.Place.BilansOtwarciaPIT bo in pracownik.BilansyOtwarciaPIT)
{
// bo.Data, bo.PrzychodUlgaEtat, bo.Spoleczne, bo.Wersja
}
```
**Pułapki:**
- `BilansOtwarciaPIT` ma kolekcję `Elementy` — niektóre kwoty są wyliczane z elementów; sprawdź na
Demo, czy ustawiasz pola root, czy elementy.
- Bilanse są **danymi wdrożeniowymi** (jednorazowe na start) — nie myl z naliczonymi deklaracjami.
- ERP-7 (Z-3/Z-3a) wymaga modułu Deklaracje i `KEDU`/PUE do eksportu — samo wprowadzenie
`WynagrodzeniaERP7`/`NieobecnosciERP7` jest lokalne, ale wygenerowanie druku — nie.
@@ -0,0 +1,831 @@
# KADRY11 — Ewidencje pracownicze
> Wspólne fakty o typie, podstawowe typy i szablon wzorca: [../kadry.md](../kadry.md).
> **Wzorzec wspólny.** Wszystkie ewidencje pracownicze to **kolekcje `SubTable` na rootcie
> `Pracownik`** (nie na `PracHistoria`). Każdy element jest osobnym `GuidedRow` (child pracownika)
> z polem `Pracownik: Soneta.Kadry.Pracownik` ustawianym automatycznie przez konstruktor
> `new Xxx(pracownik)`. Schemat dodania jest jednolity:
>
> ```csharp
> using (var t = session.Logout(editMode: true)) {
> var wpis = session.AddRow(new Xxx(pracownik)); // ctor wiąże wpis z pracownikiem
> // ... ustaw pola ...
> t.Commit(); // Commit() w kodzie biznesowym
> }
> session.Save();
> ```
>
> `session.AddRow(new Xxx(pracownik))` i `pracownik.Kolekcja.AddRow(new Xxx(pracownik))` są
> równoważne — wpis trafia do tej samej tabeli i do `SubTable` pracownika. Większość typów wymaga
> wskazania **definicji** (rekord słownikowy, tabela konfiguracyjna) — definicję pobierasz przez
> `WgNazwy[...]` z odpowiedniego modułu, **nie** tworzysz jej w teście operacyjnym.
| Ewidencja | Kolekcja na `Pracownik` | Typ elementu | Tabela |
|---|---|---|---|
| KADRY-K1 Badania lekarskie | `BadaniaLekarskie: SubTable<BadanieLekarskie>` | `Soneta.Kadry.BadanieLekarskie` | `BadaniaLekarskie` |
| KADRY-K2 Szkolenia BHP | `SzkoleniaBHP: SubTable<SzkolenieBHP>` | `Soneta.Kadry.SzkolenieBHP` | `SzkoleniaBHP` |
| KADRY-K3 Wnioski o szkolenia | `WnioskiOSzkolenia: SubTable<WniosekOSzkolenie>` | `Soneta.HR.WniosekOSzkolenie` | `WnioskiOSzkol` |
| KADRY-K3 Ukończone szkolenia | `UkończoneSzkolenia: SubTable<UkończoneSzkolenie>` | `Soneta.HR.UkończoneSzkolenie` | `UkonczSzkolenia` |
| KADRY-K3 Uprawnienia | `Uprawnienia: SubTable<UprawnieniePracownika>` | `Soneta.HR.UprawnieniePracownika` | `UprawnieniaPrac` |
| KADRY-K4 Nagrody i kary | `NagrodyKary: SubTable<NagrodaKara>` | `Nagroda` / `Kara` (`NagrodaKara` abstr.) | `NagrodyKary` |
| KADRY-K4 Oświadczenia | `Oświadczenia: SubTable<OświadczeniePracownika>` | `Soneta.Kadry.OświadczeniePracownika` | `OswiadczeniaPrac` |
| KADRY-K5 Wypadki przy pracy | `Wypadki: SubTable<Wypadek>` | `Soneta.Kadry.Wypadek` | `Wypadki` |
---
### KADRY-K1 — Badania lekarskie
**Cel:** zarejestrować badanie lekarskie pracownika (wstępne/okresowe/kontrolne) wraz z terminami
ważności i datą następnego badania; ewentualnie wykonać operację seryjną dla grupy osób.
**Mechanizm:** `BadanieLekarskie` ma publiczny konstruktor `BadanieLekarskie(Pracownik pracownik)`.
Wpis wymaga `Definicja: DefinicjaBadaniaLekarskiego` (słownik, tabela konfiguracyjna `DefBadanLek`,
pobierana przez `WgNazwy[...]`). Jeśli definicja jest **cykliczna** (`Definicja.Cykliczne == true`,
ma `NastepneDefinicja`/`NastepneTermin`), platforma wylicza termin kolejnego badania —
udostępniony jako wyliczane `NastępneTermin`/`NastępneDefinicja`.
**Pola i typy (rekord `BadanieLekarskie`):**
| Pole | Typ | Uwaga |
|---|---|---|
| `Definicja` | `Soneta.Kadry.DefinicjaBadaniaLekarskiego` | wymagana; słownik `DefBadanLek` |
| `Data` | `Soneta.Types.Date` | data wykonania badania |
| `Termin` | `Soneta.Types.Date` | termin badania — **read-only** (wyliczany z `Data`+definicji); ustawienie rzuca `ColReadOnlyException` |
| `WazneDo` | `Soneta.Types.Date` | „Ważne do" — koniec ważności (ustawialny) |
| `PracaWOkularach` | `bool` | adnotacja medyczna |
| `KwotaDofinansowania` | `decimal`, `DataDofinansowania: Date` | dofinansowanie badania |
| `Opis` | `Soneta.Business.MemoText` | opis/uwagi |
| `Anulowany` | `bool` | flaga anulowania |
| `Pracownik` | `Soneta.Kadry.Pracownik` | ustawiany przez ctor |
| `NastępneTermin`, `NastępneDefinicja`, `Następne` | (wyliczane) | termin/def./wpis następnego badania |
**Manager:** `pracownik.Badania: Pracownik.BadaniaLekarskieManager` — pomocnik tylko do odczytu;
`pracownik.Badania.ZNajkrótszymTerminem(definicja = null): BadanieLekarskie` zwraca badanie z
najbliższym terminem wygaśnięcia (do raportów „badania okresowe do wykonania").
**Snippet:**
```csharp
var kadry = session.GetKadry();
var pracownik = kadry.Pracownicy.WgKodu["006"];
var definicja = kadry.DefBadanLek.WgNazwy["Wstępne"]; // słownik konfiguracyjny
using (var t = session.Logout(editMode: true))
{
var badanie = session.AddRow(new BadanieLekarskie(pracownik)); // ctor wiąże z pracownikiem
badanie.Definicja = definicja;
badanie.Data = Date.Today;
// UWAGA: badanie.Termin jest read-only (wyliczany) — NIE ustawiaj go ręcznie.
badanie.WazneDo = new Date(Date.Today.Year + 2, Date.Today.Month, Date.Today.Day);
t.Commit();
}
session.Save();
```
**Operacja seryjna (grupa pracowników):** w warstwie UI istnieje worker
`DodajBadaniaLekarskieWorker` (warianty `ZListyBadań`, `ZListyPracowników`) z akcją menu
„Operacje seryjne/Dodaj badania lekarskie" — iteruje po wybranych pracownikach i dla każdego robi
`new BadanieLekarskie(pracownik)` + `BadaniaLekarskie.AddRow(...)`. W kodzie biznesowym
seryjność realizujesz tą samą pętlą `foreach (var p in wybrani) { … AddRow … }` w jednej transakcji.
**Pułapki:**
- `Definicja` jest **wymagana** — bez niej `Save()` rzuci `RowException`.
- `Data`/`WazneDo` to `Soneta.Types.Date`, nie `DateTime`. `Termin` jest **read-only** (wyliczany) —
próba ustawienia rzuca `ColReadOnlyException`. Reguła w weryfikatorach: `WazneDo` nie może być
wcześniejsze niż `Termin`; termin następnego badania musi być **późniejszy** niż termin badania
bieżącego — naruszenie wybucha jako `RowException` przy zapisie.
- `pracownik.Badania` to manager (odczyt), a kolekcją CRUD jest `pracownik.BadaniaLekarskie`
(`SubTable<BadanieLekarskie>`). Nie myl tych dwóch.
---
### KADRY-K2 — Szkolenia BHP
**Cel:** zarejestrować odbyte szkolenie BHP (wstępne/okresowe) z terminem ważności i datą szkolenia
następnego (analogicznie do badań lekarskich).
**Mechanizm:** konstruktor `SzkolenieBHP(Pracownik pracownik)`; kolekcja `pracownik.SzkoleniaBHP`.
Wymagana `Definicja: DefinicjaSzkoleniaBHP` (słownik konfiguracyjny `DefSzkolenBHP`, `WgNazwy[...]`).
Cykliczność (`Definicja.Cykliczne`) wylicza `NastępneTermin`/`NastępneDefinicja`.
**Pola i typy (rekord `SzkolenieBHP`):**
| Pole | Typ | Uwaga |
|---|---|---|
| `Definicja` | `Soneta.Kadry.DefinicjaSzkoleniaBHP` | wymagana; słownik `DefSzkolenBHP` |
| `Data` | `Soneta.Types.Date` | data szkolenia |
| `Termin` | `Soneta.Types.Date` | termin — **read-only** (wyliczany); ustawienie rzuca `ColReadOnlyException` |
| `WażneDo` | `Soneta.Types.Date` | koniec ważności (wyliczane) |
| `Zakres` | `string` | zakres szkolenia |
| `Osoba` | `string` | prowadzący / osoba szkoląca |
| `Opis` | `Soneta.Business.MemoText` | uwagi |
| `Anulowany` | `bool` | flaga anulowania |
| `Pracownik` | `Soneta.Kadry.Pracownik` | ustawiany przez ctor |
| `NastępneTermin`, `NastępneDefinicja`, `Następne` | (wyliczane) | następne szkolenie |
**Snippet:**
```csharp
var kadry = session.GetKadry();
var pracownik = kadry.Pracownicy.WgKodu["007"];
var definicja = kadry.DefSzkolenBHP.WgNazwy["Wstępne"];
using (var t = session.Logout(editMode: true))
{
var szkolenie = session.AddRow(new SzkolenieBHP(pracownik));
szkolenie.Definicja = definicja;
szkolenie.Data = Date.Today;
// UWAGA: szkolenie.Termin jest read-only (wyliczany) — NIE ustawiaj go ręcznie.
szkolenie.Zakres = "Instruktaż ogólny";
t.Commit();
}
session.Save();
```
**Operacja seryjna:** UI udostępnia `DodajSzkolenieBHPWorker` (akcja menu, lista pracowników) —
w kodzie biznesowym pętla `foreach` + `new SzkolenieBHP(p)` + `AddRow` w jednej transakcji.
**Pułapki:**
- `Definicja` wymagana (jak w KADRY-K1).
- Uwaga na pisownię: pole nazywa się `WażneDo` (z „ż"), a w `BadanieLekarskie``WazneDo` (bez).
- `Termin` jest **read-only** (wyliczany) — ustawienie rzuca `ColReadOnlyException`.
- `Termin` następnego szkolenia musi być późniejszy niż bieżący — inaczej `RowException`.
---
### KADRY-K3 — Szkolenia i uprawnienia (moduł HR/HR2)
**Cel:** obsłużyć cykl rozwoju kompetencji: **wniosek o szkolenie****ukończone szkolenie**
**uprawnienie/certyfikat**, wraz z kosztem i budżetem szkoleń. Typy leżą w module `Soneta.HR`
(`session.GetHR()`).
**KADRY-K3a — Wniosek o szkolenie**`WniosekOSzkolenie([Required] Pracownik pracownik)`; kolekcja
`pracownik.WnioskiOSzkolenia`. Pola:
| Pole | Typ | Uwaga |
|---|---|---|
| `Definicja` | `Soneta.HR.DefinicjaSzkolenia` | rodzaj szkolenia (słownik HR) |
| `Etap` | `Soneta.HR.EtapRealizacjiSzkolenia` | np. „Wniosek zatwierdzony" (`hr.EtapRealizSzkol.WgNazwy[...]`) |
| `Realizacja` | `Soneta.HR.RealizacjaSzkolenia` | konkretna realizacja |
| `Budzet` | `Soneta.HR.BudżetSzkoleń` | budżet, z którego finansowane |
| `Koszt` | `Soneta.Types.Currency` | koszt szkolenia |
| `DataZgloszenia`, `Termin`, `DataAnulowania` | `Soneta.Types.Date` | daty cyklu wniosku |
| `Kierownik` | `Soneta.Kadry.Pracownik` | akceptujący |
| `SkierowanyPrzezZaklad` | `bool` | skierowanie pracodawcy |
| `Ocena` | `string`, `Opis: MemoText` | ocena/uwagi |
**KADRY-K3b — Ukończone szkolenie** — dwa ctory: `UkończoneSzkolenie([Required] Pracownik pracownik)`
oraz `UkończoneSzkolenie(WniosekOSzkolenie wniosek)` (przepina pracownika z wniosku). Kolekcja
`pracownik.UkończoneSzkolenia`. Pola: `Nazwa: string`, `Okres: FromTo`, `Ocena: string`,
`Opis: MemoText`, `Wniosek: WniosekOSzkolenie` (powiązanie).
**KADRY-K3c — Uprawnienie / certyfikat**`UprawnieniePracownika([Required] Pracownik pracownik)`;
kolekcja `pracownik.Uprawnienia`. Pola:
| Pole | Typ | Uwaga |
|---|---|---|
| `Definicja` | `Soneta.HR.DefinicjaUprawnienia` | rodzaj uprawnienia |
| `Numer` | `string` | numer uprawnienia/certyfikatu |
| `DataUzyskania`, `DataUtraty`, `TerminWaznosci` | `Soneta.Types.Date` | daty ważności |
| `Okres` | `Soneta.Types.FromTo` | okres obowiązywania |
| `WydanePrzez` | `string` | organ wydający |
| `Zrodlo` | `Soneta.HR.IŹródłoUzyskaniaUprawnienia` | źródło (np. ukończone szkolenie) |
**Snippet (wniosek → koszt z budżetu):**
```csharp
var hr = session.GetHR();
var pracownik = session.GetKadry().Pracownicy.WgKodu["008"];
using (var t = session.Logout(editMode: true))
{
var wniosek = session.AddRow(new WniosekOSzkolenie(pracownik));
wniosek.Definicja = hr.DefinicjeSzkolen.WgNazwy["Kurs zawodowy"];
wniosek.Etap = hr.EtapRealizSzkol.WgNazwy["Wniosek zatwierdzony"];
wniosek.DataZgloszenia = Date.Today;
wniosek.Koszt = new Currency(1500m);
t.Commit();
}
session.Save();
```
**Pułapki:**
- Typy KADRY-K3 są w `Soneta.HR` (`session.GetHR()`), nie w `Soneta.Kadry`.
- `Etap`/`Definicja` to wpisy słownikowe HR — pobieraj `WgNazwy[...]`, nie twórz w teście.
- `Koszt`/`Budżet` używają `Soneta.Types.Currency` (waluta), nie `decimal`.
---
### KADRY-K4 — Nagrody i kary; oświadczenia (PIT-2, RODO, zgody)
**KADRY-K4a — Nagrody i kary.** Klasa bazowa `Soneta.Kadry.NagrodaKara` jest **abstrakcyjna** — używaj
konkretnych podtypów: `Soneta.Kadry.Nagroda(Pracownik)` i `Soneta.Kadry.Kara(Pracownik)`. Oba ctory
delegują do `NagrodaKara(pracownik, TypNagrodyKary)` ustawiając `Typ` na `Nagroda`/`Kara`. Kolekcja
`pracownik.NagrodyKary: SubTable<NagrodaKara>`. Pola:
| Pole | Typ | Uwaga |
|---|---|---|
| `Definicja` | `Soneta.Kadry.DefinicjaNagrodyKary` | słownik `DefNagrodKar`; ma własne pole `Typ` (Nagroda/Kara) — musi zgadzać się z podtypem wpisu, inaczej `set_Definicja` rzuca `ArgumentException`; może nieść `Element`/`Kwota` |
| `Typ` | `Soneta.Kadry.TypNagrodyKary` | `Nagroda`/`Kara` (ustawia ctor podtypu) |
| `Data` | `Soneta.Types.Date` | data nadania |
| `DataAnulowania` | `Soneta.Types.Date` | anulowanie |
| `Rozliczenie` | `Soneta.Kadry.RozliczenieSwiadczenia` (subrow) | `Rozliczenie.Kwota: Currency`, `Rozliczenie.Element: DefinicjaElementu`, `Rozliczenie.Okres: FromTo` — powiązanie z wypłatą |
| `Opis` | `Soneta.Business.MemoText` | treść nagrody/kary |
| `Pracownik` | `Soneta.Kadry.Pracownik` | ustawiany przez ctor |
**KADRY-K4b — Oświadczenia (PIT-2, RODO, zgody).** `Soneta.Kadry.OświadczeniePracownika` — trzy ctory:
`OświadczeniePracownika([Required] Pracownik pracownik, [Required] DefinicjaOświadczenia definicja)`,
wariant z `Date dataZłożenia`, oraz `(RowCreator)`. Kolekcja `pracownik.Oświadczenia`. Rodzaj
oświadczenia (PIT-2, zgoda RODO, zgoda na e-doręczenia itp.) określa `Definicja` (słownik
konfiguracyjny `DefOswiadczen`). Pola:
| Pole | Typ | Uwaga |
|---|---|---|
| `Definicja` | `Soneta.Kadry.DefinicjaOświadczenia` | wymagana w ctorze; słownik `DefOswiadczen` |
| `DataZlozenia` | `Soneta.Types.Date` | data złożenia |
| `DataWycofania` | `Soneta.Types.Date` | data wycofania |
| `Okres` | `Soneta.Types.FromTo` | okres obowiązywania (z `Definicja.OkresWaznosci`/`OkresIlosc`) |
| `Tresc` | `Soneta.Business.MemoText` | treść |
| `TrescOswiadczenia` | `Soneta.Kadry.TreśćOświadczenia` | treść strukturalna |
| `Pracownik` | `Soneta.Kadry.Pracownik` | ustawiany przez ctor |
**Snippet (nagroda + oświadczenie PIT-2):**
```csharp
var kadry = session.GetKadry();
var pracownik = kadry.Pracownicy.WgKodu["009"];
using (var t = session.Logout(editMode: true))
{
// nagroda — konkretny podtyp, NIE abstrakcyjna NagrodaKara
var nagroda = session.AddRow(new Nagroda(pracownik));
nagroda.Definicja = kadry.DefNagrodKar.WgNazwy["Nagroda uznaniowa"];
nagroda.Data = Date.Today;
// oświadczenie — definicja jest wymagana w konstruktorze
var defPit2 = kadry.DefOswiadczen.WgNazwy["PIT-2"];
var oswiadczenie = session.AddRow(new OświadczeniePracownika(pracownik, defPit2, Date.Today));
t.Commit();
}
session.Save();
```
**Pułapki:**
- **Nie** rób `new NagrodaKara(...)` — typ abstrakcyjny. Używaj `Nagroda`/`Kara`.
- `Definicja` musi mieć **`Typ` zgodny** z podtypem wpisu (`Nagroda` → def. o `Typ==Nagroda`, `Kara`
def. o `Typ==Kara`); przypisanie niezgodnej typem definicji rzuca `ArgumentException` w `set_Definicja`.
Filtruj słownik: `DefNagrodKar.Cast<DefinicjaNagrodyKary>().FirstOrDefault(d => d.Typ == TypNagrodyKary.Nagroda)`.
- `OświadczeniePracownika` **nie ma** ctora samego `(Pracownik)` — definicja jest `[Required]`
w konstruktorze; bez niej kod się nie skompiluje.
- `Rozliczenie.*` na nagrodzie/karze to subrow powiązania z wypłatą (`Currency`, `DefinicjaElementu`)
— wypełniane przy rozliczaniu w płacach, nie przy samym wpisie.
---
### KADRY-K5 — Wypadki przy pracy
**Cel:** zarejestrować wypadek przy pracy wraz z dokumentacją powypadkową (protokół, decyzja,
okoliczności, skutki) i ewentualnym świadczeniem.
**Mechanizm:** `Soneta.Kadry.Wypadek(Pracownik pracownik)`; kolekcja `pracownik.Wypadki`. Wpis jest
numerowany (`Numer: Soneta.Core.NumerDokumentu`) i wymaga `Definicja: Soneta.Core.DefinicjaDokumentu`
(definicja dokumentu wypadku).
**Pola i typy (rekord `Wypadek`):**
| Pole | Typ | Uwaga |
|---|---|---|
| `Definicja` | `Soneta.Core.DefinicjaDokumentu` | definicja dokumentu (numeracja) |
| `Numer` | `Soneta.Core.NumerDokumentu` (subrow) | `Numer.Pelny`, `Numer.Symbol`, `Numer.Numer` |
| `Data` | `Soneta.Types.Date` | data wypadku |
| `Godzina` | `Soneta.Types.Time` | godzina wypadku |
| `DataZgloszenia` | `Soneta.Types.Date` | data zgłoszenia |
| `Miejsce` | `string` | miejsce wypadku |
| `Rodzaj` | `Soneta.Kadry.RodzajWypadku` | klasyfikacja wypadku |
| `PrzyPracy`, `Ciezki`, `Smiertelny`, `Niezdolnosc` | `bool` | kwalifikacja skutków |
| `Okolicznosci`, `Skutki`, `Odmowa` | `Soneta.Business.MemoText` | dokumentacja opisowa |
| `ProtokolNumer`, `ProtokolData` | `string` / `Date` | protokół powypadkowy |
| `DecyzjaNumer`, `DecyzjaData` | `string` / `Date` | decyzja |
| `PismoNumer`, `PismoData` | `string` / `Date` | pismo |
| `SKW` | `string` | statystyczna karta wypadku |
| `Kwota` | `decimal` | kwota świadczenia |
| `PracHistoria` | `Soneta.Kadry.PracHistoria` | (wyliczane) zapis kadrowy na datę |
| `Pracownik` | `Soneta.Kadry.Pracownik` | ustawiany przez ctor |
**Snippet:**
```csharp
var kadry = session.GetKadry();
var pracownik = kadry.Pracownicy.WgKodu["010"];
using (var t = session.Logout(editMode: true))
{
var wypadek = session.AddRow(new Wypadek(pracownik));
wypadek.Data = Date.Today;
wypadek.Godzina = new Time(10, 30);
wypadek.DataZgloszenia = Date.Today;
wypadek.Miejsce = "Hala produkcyjna";
wypadek.PrzyPracy = true;
wypadek.Okolicznosci = new MemoText("Poślizgnięcie na mokrej posadzce.");
t.Commit();
}
session.Save();
```
**Pułapki:**
- `Numer` jest subrowem nadawanym wg `Definicja` (numeracja) — nie ustawiaj `Numer.Pelny` ręcznie,
numer nadaje platforma; gdy `Definicja` ma własną numerację, podpięcie definicji wystarcza.
- `Godzina` to `Soneta.Types.Time`, `Data` to `Soneta.Types.Date` — nie `DateTime`.
- Pola opisowe (`Okolicznosci`, `Skutki`, `Odmowa`) to `MemoText`, nie `string`.
### KADRY-K6 — RODO/GIODO: oświadczenia, uprawnienia do przetwarzania, wymiana danych
**Cel:** ewidencjonować zgody/oświadczenia RODO pracownika, uprawnienia do przetwarzania danych
osobowych oraz fakty wymiany danych (pozyskanie / udostępnienie / powierzenie). Pracownik jest
hostem GIODO — implementuje `IGIODOOświadczenieHost`, `IGIODOUprawnienieHost`, `IGIODOWymianaDanychHost`,
`IGIODOZgodnyHost`. Zapis „teczki" personalnej do pliku jest operacją plikową (poza zakresem testów).
**Publiczny kontrakt — kolekcje na `Pracownik` (moduł `Soneta.Core`):**
| Kolekcja | Typ elementu | Zawartość |
|---|---|---|
| `GIODOOświadczenia` | `SubTable<Soneta.Core.GIODOOświadczenie>` | oświadczenia / zgody RODO |
| `GIODOUprawnienia` | `SubTable<Soneta.Core.GIODOUprawnienie>` | uprawnienia do przetwarzania danych |
| `GIODOUdostępnienia` | `SubTable<Soneta.Core.GIODOWymianaDanych>` | pozyskanie / udostępnienie / powierzenie danych |
| `PotwierdzeniaGIODO` | `SubTable<Soneta.Core.GIODOZgodny>` | potwierdzenia zgodności; `ZgodnoscGIODOPotwierdzona: bool` (kalkulowane) |
**`GIODOOświadczenie` (tabela `GIODOOswiadcz`, root) — pola bazodanowe:**
| Pole | Typ | Uwaga |
|---|---|---|
| `Host` | `IGIODOOświadczenieHost` | składający oświadczenie (= `Pracownik`) |
| `Definicja` | `Soneta.Core.GIODODefinicjaOświadczenia` | **referencja konfiguracyjna** (wymagana przez ctor) |
| `Data` | `Soneta.Types.Date` | data oświadczenia (zapisywalne) |
| `Okres` | `Soneta.Types.FromTo` | okres obowiązywania zgody — **read-only** (wyliczane z definicji) |
| `Rodzaj` | `Soneta.Core.RodzajeOświadczeńGIODO` | `Oświadczenie`, `UdzielenieZgody`, `WycofanieZgody`**read-only** (wynika z definicji) |
| `Oswiadczenie` | `bool` | flaga oświadczenia |
| `Tresc` | `Soneta.Business.MemoText` | treść |
| `SposobPozyskania` | `string` | — |
| `DataWycofaniaZgody` | `Soneta.Types.Date` | — |
| `WycofanieZgody` | `GIODOOświadczenie` | powiązanie z zapisem wycofania |
| `Bufor` | `bool` | zatwierdzenie |
Ctor: `new GIODOOświadczenie(IGIODOOświadczenieHost host, GIODODefinicjaOświadczenia definicja)`.
**`GIODOUprawnienie` (tabela `GIODOUprawnienia`, root) — pola bazodanowe:**
| Pole | Typ | Uwaga |
|---|---|---|
| `Uprawniony` | `IGIODOUprawnienieHost` | = `Pracownik` |
| `Definicja` | `Soneta.Core.GIODODefinicjaUprawnienia` | **referencja konfiguracyjna** (wymagana przez ctor) |
| `Data`, `Przyznane`, `Odebrane` | `Soneta.Types.Date` | data zapisu / od kiedy przyznane / od kiedy odebrane |
| `Okres` | `Soneta.Types.FromTo` | okres przyznania |
| `Tresc` | `Soneta.Business.MemoText` | — |
| `WycofanieUprawnienia` | `GIODOUprawnienie` | powiązanie z wycofaniem |
Ctor: `new GIODOUprawnienie(IGIODOUprawnienieHost uprawniony, GIODODefinicjaUprawnienia definicja)`.
**`GIODOWymianaDanych` (tabela `GIODOWymDanych`, root) — pola bazodanowe:**
| Pole | Typ | Uwaga |
|---|---|---|
| `Host` | `IGIODOWymianaDanychHost` | = `Pracownik` |
| `Kierunek` | `Soneta.Core.GIODOKierunekWymianyDanych` | `Powierzenie`, `Pozyskanie`, `PowierzenieZbioru`, `PozyskanieZbioru`, `Udostępnienie` |
| `Podmiot` | `Soneta.Core.IKontrahent` | druga strona wymiany |
| `Data` | `Soneta.Types.Date` | data wymiany |
| `Zakres` | `Soneta.Business.MemoText` | zakres danych |
| `SposobPozyskania` | `string` | — |
| `PozyskaneOdOsoby`, `UdostepnioneOsobie`, `NaWniosekOsoby`, `TylkoDostep` | `bool` | flagi |
| `Definicja` | `Soneta.Core.DefinicjaDokumentu` | def. numeracji dokumentu |
| `ZbiorDanych` | `Soneta.Core.GIODO.GIODOZbiorDanych` | zbiór danych |
`GIODOWymianaDanych` **nie ma publicznego konstruktora** — rekordy tworzą wyłącznie workery poniżej
(zwracają konkretne podtypy `GIODOPozyskanieDanych` / `GIODOUdostępnienieDanych` / `GIODOPowierzenieDanych`).
**Workery RODO (jedyna droga dodania przez API; klasy zagnieżdżone w `Soneta.Kadry.Pracownicy`):**
| Worker | Metoda | Zwraca | Parametry (`Pars` / `Params`) |
|---|---|---|---|
| `Pracownicy.DodajOświadczeniaWorker` | `GIODOOświadczenie DodajOświadczenia()` | oświadczenie | `Pars`: `Definicja: GIODODefinicjaOświadczenia`, `Data`, `Oddział`, `SposobPozyskania`, `Zatwierdź: bool` |
| `Pracownicy.DodajUprawnieniaWorker` | `GIODOUprawnienie DodajUprawnienia()` | uprawnienie | `Pars`: `Definicja: GIODODefinicjaUprawnienia`, `Data`, `Przyznane`, `Odebrane`, `Oddział`, `Zatwierdź: bool` |
| `Pracownicy.DodajPozyskanieDanychWorker` | `GIODOPozyskanieDanych DodajPozyskanieDanych()` | wymiana (pozyskanie) | `Pars`: `Podmiot: IKontrahent`, `Data`, `Zakres: string`, `Oddział`, `SposobPozyskania`, `Zatwierdź: bool` |
| `Pracownicy.DodajUdostępnienieDanychWorker` | `GIODOUdostępnienieDanych DodajUdostępnienieDanych()` | wymiana (udostępnienie) | `Pars`: `Podmiot: IKontrahent`, `Data`, `Zakres: string`, `Oddział`, `Zatwierdź: bool` |
| `Pracownicy.DodajPowierzenieDanychWorker` | `GIODOPowierzenieDanych DodajPowierzenieDanych()` | wymiana (powierzenie) | `Pars` (analogicznie) |
Wszystkie workery RODO mają bezparametrowy ctor oraz property `Hosts: Pracownik[]` (`[Context]`, lista
pracowników, których dotyczy operacja) i `Session`.
**Zapis teczki personalnej do pliku — `Pracownik.ZapiszTeczkęDoPlikuWorker`** (akcja
„Teczka.../Zapisz teczkę do pliku", metoda `ZapiszTeczkeDoPliku()`, property `Param`) — to
**operacja plikowa** (serializacja dokumentacji do plików XML/katalogu na dysku). **Poza zakresem
testów jednostkowych → `[Ignore]`** (zależność od systemu plików).
**Snippet (dodanie oświadczenia GIODO workerem):**
```csharp
var kadry = session.GetKadry();
var pracownik = kadry.Pracownicy.WgKodu["006"];
// Definicja oświadczenia z konfiguracji (musi istnieć w bazie):
var defOswiadczenia = session.ExecuteConfig(s =>
s.GetCore().GIODODefinicjeOświadczeń.WgNazwy["Zgoda na przetwarzanie danych"]);
using (var t = session.Logout(editMode: true))
{
var worker = new Pracownik.Pracownicy.DodajOświadczeniaWorker { Hosts = new[] { pracownik } };
worker.Pars.Definicja = session.Get(defOswiadczenia);
worker.Pars.Data = Date.Today;
worker.Pars.Zatwierdź = true;
GIODOOświadczenie oswiadczenie = worker.DodajOświadczenia();
t.CommitUI();
}
session.Save();
// Odczyt oświadczeń pracownika:
foreach (GIODOOświadczenie o in pracownik.GIODOOświadczenia)
{
// o.Definicja, o.Okres, o.Rodzaj, o.Data
}
```
**Snippet (dodanie oświadczenia bez workera — bezpośrednim ctorem):**
```csharp
using (var t = session.Logout(editMode: true))
{
var o = session.AddRow(new GIODOOświadczenie(pracownik, session.Get(defOswiadczenia)));
// host i Definicja wynikają z ctora; Rodzaj/Okres są WYLICZANE (read-only) z definicji — nie ustawiaj ich.
o.Data = Date.Today;
o.SposobPozyskania = "Formularz papierowy";
t.Commit();
}
session.Save();
```
**Pułapki:**
- `GIODOOświadczenie`/`GIODOUprawnienie` wymagają **referencji do definicji konfiguracyjnej**
(`GIODODefinicjaOświadczenia` / `GIODODefinicjaUprawnienia`) — pobierz istniejący rekord
(`ExecuteConfig`), nie twórz „w locie". Bez definicji w bazie scenariusz wymaga uprzedniej
konfiguracji modułu RODO/GIODO.
- `GIODOWymianaDanych` **nie ma publicznego ctora** — dodawaj wyłącznie workerami
`DodajPozyskanieDanychWorker` / `DodajUdostępnienieDanychWorker` / `DodajPowierzenieDanychWorker`.
- Workery RODO modyfikują dane i są uruchamiane „jak z UI" → transakcja edycyjna + `CommitUI()` +
`Save()`. `Hosts`/`Podmiot` muszą pochodzić z bieżącej sesji (safe-code §2.1).
- Obowiązywanie zgody jest „na dzień" — czytaj `Okres`/`Data`, nie zakładaj bezterminowości.
- Dane wrażliwe (treść oświadczeń, podmioty) — nie loguj nadmiarowo (safe-code §12).
- Workery RODO wymagają praw do obszaru GIODO; w teście biznesowym egzekucji praw nie sprawdzamy
(safe-code §7.2).
### KADRY-K7 — Struktura organizacyjna: przypisanie do wydziału/struktury, powiązania
**Cel:** przypisać pracownika do jednostki organizacyjnej (wydziału) oraz do elementów struktury
organizacyjnej (np. stanowiska w strukturze, relacje przełożonypodwładny). Wydział wynika z warunków
etatu (`Etat.Wydzial`, historyczne — patrz sekcja B), a powiązania ze strukturą trzyma osobna kolekcja.
**Publiczny kontrakt:**
| Składnik | Typ | Rodzaj | Uwaga |
|---|---|---|---|
| `Etat.Wydzial` | `Soneta.Kadry.Wydzial` | bazodanowe (na `PracHistoria.Etat`) | jednostka organizacyjna; korzeń: `session.GetKadry().Wydzialy.Firma` (zmiana „od daty" — KADRY-A14) |
| `PowiązaniaStrOrg` | `SubTable<Soneta.Core.PowiązanieStrukturyOrganizacyjnej>` | kolekcja na `Pracownik` | powiązania ze strukturą organizacyjną |
| `StrukturaOraganizacyjna` | `Pracownik.StrukturaOraganizacyjnaManager` | manager (read-only API) | nawigacja przełożeni/podwładni |
| Pracownik implementuje | `IŹródłoPowiązaniaStrukturyOrganizacyjnej` | interfejs | jest źródłem powiązań |
**`PowiązanieStrukturyOrganizacyjnej` (tabela `PowiazaniaStrOrg`, child przez `Zrodlo`) — pola:**
| Pole | Typ | Uwaga |
|---|---|---|
| `Zrodlo` | `IŹródłoPowiązaniaStrukturyOrganizacyjnej` | guided-parent (= `Pracownik`) |
| `Element` | `Soneta.Core.ElementStrukturyOrganizacyjnej` | referencja do **instancji** elementu struktury z tabeli `CoreModule.ElementyStrOrg` (NIE z `DefElStrukturOrg`, która trzyma `DefinicjaElementuStrukturyOrganizacyjnej`); `ElementStrukturyOrganizacyjnej` nie ma publicznego ctora — pobierz istniejący rekord |
| `Okres` | `Soneta.Types.FromTo` | okres obowiązywania powiązania (zapisywalne) |
Ctor: `new PowiązanieStrukturyOrganizacyjnej(ElementStrukturyOrganizacyjnej element, IŹródłoPowiązaniaStrukturyOrganizacyjnej zrodlo)`.
**Manager `StrukturaOraganizacyjnaManager` (tylko odczyt nawigacyjny):**
| Metoda / property | Sygnatura | Zwraca |
|---|---|---|
| `Przełożony(...)` | `Pracownik Przełożony(StrukturaOrganizacyjna, Date, bool, Func<...>)` | bezpośredni przełożony |
| `PrzełożonyWgPodległości(...)` | `Pracownik PrzełożonyWgPodległości(Date, bool, Func<...>)` | przełożony wg podległości |
| `Przełożeni(...)` | `IEnumerable<Pracownik> …` | przełożeni |
| `Podwładni(...)` | `IEnumerable<Pracownik> Podwładni(FromTo, bool, Func<...>)` | podwładni w okresie |
| `GetDomyślnyPrzełożony(naDzień[, bezpośredni, warunek])` | `Pracownik GetDomyślnyPrzełożony(Date, bool=…, Func=…)` | domyślny przełożony na dzień (property `DomyślnyPrzełożony` jest **przestarzała** — używaj metody) |
**Workery zmiany powiązań (klasy zagnieżdżone w `Soneta.Kadry.Pracownik`):**
| Worker | Akcja (menu) | Metoda | Parametry |
|---|---|---|---|
| `Pracownik.DodajPowiązanieStrukturyWorker` | „Struktura organizacyjna/Dodaj lub modyfikuj powiązanie…" | `object DodajPowiązanieStruktury()` | `Params: WybórElementuContext`, `Pracownicy: Pracownik[]` (`[Context]`) |
| `Pracownik.UsuńPowiązanieStrukturyWorker` | „Struktura organizacyjna/Usuń powiązanie…" | `void DodajPowiązanieStruktury()` | `Params: WybórElementuContext`, `Pracownicy: Pracownik[]` |
**Snippet (dodanie powiązania ze strukturą — bezpośrednim ctorem):**
```csharp
var pracownik = session.GetKadry().Pracownicy.WgKodu["006"];
// Instancja elementu struktury (musi istnieć w bazie — tabela ElementyStrOrg, NIE DefElStrukturOrg):
ElementStrukturyOrganizacyjnej element =
session.GetCore().ElementyStrOrg.Cast<ElementStrukturyOrganizacyjnej>().FirstOrDefault();
using (var t = session.Logout(editMode: true))
{
var p = session.AddRow(new PowiązanieStrukturyOrganizacyjnej(element, pracownik));
p.Okres = new FromTo(Date.Today, Date.MaxValue);
t.Commit();
}
session.Save();
// Odczyt nawigacyjny struktury:
Pracownik przelozony = pracownik.StrukturaOraganizacyjna.GetDomyślnyPrzełożony(Date.Today);
```
**Snippet (zmiana wydziału — nowy zapis „od daty", KADRY-A14):**
```csharp
var pracownik = session.GetKadry().Pracownicy.WgKodu["006"];
var kadry = session.GetKadry();
using (var t = session.Logout(editMode: true))
{
var ph = pracownik[Date.Today]; // zapis obowiązujący na dzień (KADRY-A15)
ph.Etat.Wydzial = kadry.Wydzialy.Firma; // referencja do istniejącego wydziału (korzeń struktury)
t.Commit();
}
session.Save();
```
**Pułapki:**
- `Etat.Wydzial` jest **danymi historycznymi** (na `PracHistoria.Etat`) i jest **wymagany dla etatu**
zmieniaj nowym zapisem „od daty" (KADRY-A14), nie nadpisuj bieżącego (zmieniłoby cały okres wstecz).
- `PowiązanieStrukturyOrganizacyjnej.Element` to **referencja konfiguracyjna** — pobierz istniejący
element struktury; bez zdefiniowanej struktury organizacyjnej scenariusz wymaga konfiguracji.
- `StrukturaOraganizacyjnaManager` jest **tylko do odczytu** — zmiany realizują workery
`DodajPowiązanieStrukturyWorker` / `UsuńPowiązanieStrukturyWorker` lub bezpośredni zapis do
`PowiązaniaStrOrg`.
- Workery struktury modyfikują dane „jak z UI" → transakcja + `CommitUI()` + `Save()`; rekordy z
bieżącej sesji (safe-code §2.1).
### KADRY-K8 — Oceny okresowe: arkusze ocen, cele okresowe, karty kompetencji
**Cel:** prowadzić oceny okresowe pracownika (arkusz oceny z elementami), cele okresowe wraz z ich
realizacją, karty kompetencji i karty opisu stanowiska. Funkcjonalność należy do modułów **HR**
(`session.GetHR()`, `OcenyPracownikow`, `EtapyRekrutacji`) i **HR2** (`session.GetHR2()`,
`CeleOkresowePrac`, `KartyKompPrac`, `KartyOpStanowisk`). Pracownik implementuje `IOceniany`,
`IOceniający`, `IOdpowiedzialnyZaOcenę`, `IŹródłoKartyOpisuStanowiska`.
**Publiczny kontrakt — kolekcje na `Pracownik`:**
| Kolekcja | Typ elementu | Zawartość |
|---|---|---|
| `Oceny` | `SubTable<Soneta.HR.OcenaPracownika>` | arkusze ocen okresowych (root) |
| `ElementyOceny` | `SubTable<Soneta.HR.ElementOcenyPracownika>` | pojedyncze elementy/pozycje arkuszy ocen |
| `CeleOkresowe` | `SubTable<Soneta.HR2.CelOkresowyPracownika>` | cele okresowe |
| `KartyKompetencji` | `SubTable<Soneta.HR2.KartaKompetencjiPracownika>` | karty kompetencji |
| `KartyOpisuStanowiska` | `SubTable<Soneta.HR2.KartaOpisuStanowiskaBase>` | karty opisu stanowiska |
| `KartyRealizacjiCelu` | `SubTable<Soneta.HR2.KartaRealizacjiCelu>` | karty realizacji celów |
| `Oceniani` / `Oceniający` | `SubTable<Soneta.Oceny.OcenaOceniany/OcenaOceniający>` | role pracownika w ocenie |
**`OcenaPracownika` (tabela `OcenyPracownikow`, root; impl. `IOcenaPracownika`) — pola:**
| Pole | Typ | Uwaga |
|---|---|---|
| `Pracownik` | `Soneta.Kadry.Pracownik` | oceniany |
| `Nazwa` | `string` | nazwa arkusza |
| `Data`, `Termin` | `Soneta.Types.Date` | data oceny / termin |
| `Opis` | `Soneta.Business.MemoText` | — |
| `Anulowany` | `bool` | — |
| `ElementyOceny` | `SubTable<ElementOcenyPracownika>` | pozycje arkusza |
Ctor: `new OcenaPracownika(Pracownik pracownik)`.
**`ElementOcenyPracownika` (tabela `ElementyOcenPrac`, child przez `Ocena`) — pola:**
| Pole | Typ | Uwaga |
|---|---|---|
| `Ocena` | `IOcenaPracownika` | guided-parent (arkusz oceny lub etap rekrutacji) |
| `Pracownik` | `Soneta.Kadry.Pracownik` | — |
| `Definicja` | `Soneta.HR.DefElementuOcenyPracownika` | **referencja konfiguracyjna** (z tabeli `HRModule.DefElemOcenPrac`); zapisywalna i wymagana do zapisu |
| `Typ` | `Soneta.HR.TypyElementowOceny` | `Historyczny`, `Aktualny`**read-only** (wynika z definicji) |
| `Data` | `Soneta.Types.Date` | **read-only** (wyliczane) |
| `Wartosc` | `decimal` | wartość liczbowa oceny (zapisywalna) |
Ctor: `new ElementOcenyPracownika(IOcenaPracownika ocena)`. Dodawaj przez `session.AddRow(new ElementOcenyPracownika(ocena))` (NIE `ocena.ElementyOceny.AddRow(...)``SubTable` nie udostępnia `AddRow`).
**`CelOkresowyPracownika` (tabela `CeleOkresowePrac`, root) — pola:**
| Pole | Typ | Uwaga |
|---|---|---|
| `Pracownik` | `Soneta.Kadry.Pracownik` | — |
| `Nazwa` | `string` | nazwa celu |
| `Data`, `Termin` | `Soneta.Types.Date` | — |
| `Opis` | `Soneta.Business.MemoText` | — |
| `Definicja` | `Soneta.Oceny.DefinicjaElementuOceny` | **referencja konfiguracyjna** (opcjonalna) |
| `Anulowany` | `bool` | — |
| `Realizacja` | `Soneta.HR2.RealizacjaCelu` | bieżąca realizacja (subrow) |
| `Realizacje` | `SubTable<Soneta.HR2.RealizacjaCelu>` | realizacje celu |
> `CelOkresowyPracownika` **nie ma pola `Wartosc`** — postęp/ocena celu jest reprezentowana przez `Realizacja`/`Realizacje` (`Soneta.HR2.RealizacjaCelu`). Pole `Wartosc` (typu decimal) ma natomiast `ElementOcenyPracownika`.
Ctor: `new CelOkresowyPracownika(Pracownik pracownik)`.
**`KartaOpisuStanowiskaBase` (tabela `KartyOpStanowisk`, root) — pola:**
| Pole | Typ | Uwaga |
|---|---|---|
| `Zrodlo` | `IŹródłoKartyOpisuStanowiska` | = `Pracownik` / `DefinicjaStanowiska` / wakat / oferta |
| `Typ` | `Soneta.HR2.TypyKartOpisuStanowiska` | `KartaOpisuStanowiska`, `OgłoszenieOPracę` |
| `Data` | `Soneta.Types.Date` | — |
| `Elementy` | `SubTable<ElementKartyOpisuStanowiska>` | elementy opisu |
| `Kompetencje` | `SubTable<KompetencjaKartyOpisuStanowiska>` | kompetencje |
`KartaOpisuStanowiskaBase` i `KartaKompetencjiPracownika` **nie mają publicznego ctora bezparametrowego**;
`KartaKompetencjiPracownika` ma ctor `(Pracownik pracownik, IŹródłoKartyCharakterystykiPracownika zrodlo)`.
Karty zwykle tworzone są workerami kopiującymi (`KopiujKartęOpisuStanowiskaWorker.KopiujZDefinicjiStanowiska()`,
`KopiujKartęKompetencjiWorker.KopiujZKOS()`/`KopiujZPoprzedniej()`).
**Workery oceniania (klasy w `Soneta.HR` / `Soneta.HR2`):**
| Worker | Metoda | Parametry |
|---|---|---|
| `Soneta.HR.OcenaPracownikowWorker` | `Oceń()` | `Pars`, `Idxs: Pracownik[]` (`[Context]`); ctor `(Context)` |
| `Soneta.HR.WzorOcenyPracownika.ZainicjujOcenęWorker` | `Zainicjuj()` | `Ocena: IOcenaPracownika`, `Pars`; ctor `(Session)` |
| `Soneta.HR2.KopiujKartęOpisuStanowiskaWorker` | `KopiujZDefinicjiStanowiska()`, `KopiujZPoprzedniej()` | `Karta: KartaOpisuStanowiskaBase` |
| `Soneta.HR2.KopiujKartęKompetencjiWorker` | `KopiujZKOS()`, `KopiujZPoprzedniej()` | `Karta: KartaKompetencjiPracownika` |
**Snippet (dodanie celu okresowego — wymaga definicji elementu oceny w bazie HR2):**
```csharp
var pracownik = session.GetKadry().Pracownicy.WgKodu["006"];
var hr2 = session.GetHR2();
// Definicja elementu oceny z konfiguracji modułu Oceny (musi istnieć):
var defElementu = session.ExecuteConfig(s =>
/* pobranie DefinicjaElementuOceny z modułu Oceny */ default);
using (var t = session.Logout(editMode: true))
{
var cel = new CelOkresowyPracownika(pracownik);
hr2.CeleOkresowePrac.AddRow(cel);
cel.Nazwa = "Wdrożenie nowego modułu";
cel.Data = Date.Today;
cel.Termin = new Date(2026, 12, 31);
cel.Definicja = session.Get(defElementu);
t.Commit();
}
session.Save();
// Odczyt celów okresowych:
foreach (CelOkresowyPracownika c in pracownik.CeleOkresowe)
{
// c.Nazwa, c.Termin, c.Wartosc.Punktacja
}
```
**Snippet (utworzenie arkusza oceny i dodanie elementu):**
```csharp
using (var t = session.Logout(editMode: true))
{
var ocena = new OcenaPracownika(pracownik);
session.GetHR().OcenyPracownikow.AddRow(ocena);
ocena.Nazwa = "Ocena roczna 2026";
ocena.Data = Date.Today;
var el = session.AddRow(new ElementOcenyPracownika(ocena)); // ocena jako IOcenaPracownika
el.Definicja = defElementu; // wymagana (z HRModule.DefElemOcenPrac); Typ/Data są wyliczane (read-only)
el.Wartosc = 4m; // Wartosc to decimal
t.Commit();
}
session.Save();
```
**Pułapki:**
- Cele/elementy ocen wymagają **referencji do definicji konfiguracyjnych** (`DefElementuOcenyPracownika`,
`Soneta.Oceny.DefinicjaElementuOceny`) — bez nich scenariusz wymaga uprzedniej konfiguracji modułu
Oceny/HR/HR2. W bazie Demo te definicje **mogą nie istnieć** — najpierw sprawdź dostępność.
- Karty opisu stanowiska / kompetencji nie mają prostego ctora — twórz je workerami kopiującymi
(`KopiujKartę…Worker`) z definicji stanowiska lub poprzedniej karty.
- `ElementOcenyPracownika.Ocena` to `IOcenaPracownika` — może to być arkusz oceny **lub etap
rekrutacji** (`EtapRekrutacji` także implementuje `IOcenaPracownika`, patrz KADRY-K9).
- `CelOkresowyPracownika` **nie ma pola `Wartosc`** — postęp/wynik celu reprezentują `Realizacja`/`Realizacje`
(`RealizacjaCelu`). Liczbową wartość ma `ElementOcenyPracownika.Wartosc` (`decimal`).
- `ElementOcenyPracownika`: `Typ`/`Data`**read-only** (wyliczane z definicji), a do tabeli dodawaj przez
`session.AddRow(...)``SubTable<ElementOcenyPracownika>` nie ma metody `AddRow`.
- Workery oceniania uruchamiane „jak z UI" → transakcja + `CommitUI()` + `Save()`.
### KADRY-K9 — Rekrutacja: wakaty, ogłoszenia, aplikacje, etapy, stan zatrudnienia
**Cel:** prowadzić proces rekrutacji — wakaty (zapotrzebowanie), oferty/ogłoszenia o pracę, aplikacje
kandydatów oraz etapy rekrutacji z ocenami, aż do zatrudnienia kandydata. Funkcjonalność należy do
modułów **HR2** (`session.GetHR2()`, `RekrutAplikacje`, `RekrutWakaty`) i **HR**
(`session.GetHR()`, `Rekrutacje`, `EtapyRekrutacji`).
**Publiczny kontrakt — kolekcje na `Pracownik` (kandydat jest reprezentowany rekordem `Pracownik`):**
| Kolekcja | Typ elementu | Zawartość |
|---|---|---|
| `Aplikacje` | `SubTable<Soneta.HR2.RekrutacjaAplikacja>` | aplikacje kandydata |
| `Wakaty` | `SubTable<Soneta.HR2.RekrutacjaWakat>` | wakaty |
| `Rekrutacje` / `Kandydatury` | `SubTable<Soneta.HR.Rekrutacja>` | rekrutacje (kandydatury) |
| `EtapyRekrutacji` | `SubTable<Soneta.HR.EtapRekrutacji>` | etapy rekrutacji |
**`RekrutacjaAplikacja` (tabela `RekrutAplikacje`, root; impl. `IŹródłoRekrutacji`) — pola:**
| Pole | Typ | Uwaga |
|---|---|---|
| `Pracownik` | `Soneta.Kadry.Pracownik` | kandydat |
| `Stanowisko` | `Soneta.HR.DefinicjaStanowiska` | **referencja konfiguracyjna** stanowiska |
| `Wydzial` | `Soneta.Kadry.Wydzial` | jednostka organizacyjna |
| `Oferta` | `Soneta.HR2.OfertaPracy` | oferta, na podstawie której wpłynęła aplikacja |
| `Stan` | `Soneta.HR2.StanAplikacji` | `Wprowadzona`, `Zakończona`, `Anulowana` |
| `Data` | `Soneta.Types.Date` | data aplikacji |
| `PlanowanaDataZatrudnienia` | `Soneta.Types.Date` | — |
Ctor: `new RekrutacjaAplikacja(Pracownik pracownik, Soneta.HR.WydziałDefinicjiStanowiska stanowisko)`.
`WydziałDefinicjiStanowiska` jest w module **`Soneta.HR`** (NIE `Soneta.HR2`) i ma ctor
`new WydziałDefinicjiStanowiska(DefinicjaStanowiska definicjaStanowiska)` — definicję pobierz
z `session.GetHR().DefStanowisk`. `RekrutacjaAplikacja.Stanowisko` zwraca tę `DefinicjaStanowiska`.
**`EtapRekrutacji` (tabela `EtapyRekrutacji`, root; impl. `IOcenaPracownika`) — pola:**
| Pole | Typ | Uwaga |
|---|---|---|
| `Rekrutacja` | `Soneta.HR.Rekrutacja` | rekrutacja nadrzędna |
| `Definicja` | `Soneta.HR.DefinicjaEtapuRekrutacji` | **referencja konfiguracyjna** |
| `Lp` | `int` | numer etapu |
| `Data`, `Termin` | `Soneta.Types.Date` | — |
| `Odpowiedzialny` | `Soneta.Oceny.IOceniający` | osoba odpowiedzialna |
| `Opis` | `Soneta.Business.MemoText` | — |
| `ElementyOceny` | `SubTable<ElementOcenyPracownika>` | oceny etapu (etap jest `IOcenaPracownika`) |
Ctor: `new EtapRekrutacji(Rekrutacja rekrutacja)`.
**`Rekrutacja` (tabela; impl. `IOcenaPracownika`) — ctory:**
`new Rekrutacja(Pracownik pracownik)`, `new Rekrutacja(Pracownik pracownik, IŹródłoRekrutacji źródło)`.
**`RekrutacjaWakat` (tabela `RekrutWakaty`, root) — ctory:**
`new RekrutacjaWakat(WydziałDefinicjiStanowiska stanowisko)`,
`new RekrutacjaWakat(DefinicjaStanowiska definicjaStanowiska, Wydzial wydzial)`.
**`OfertaPracy` (tabela; ogłoszenie o pracę) — ctory:**
`new OfertaPracy(WydziałDefinicjiStanowiska stanowisko)`, `new OfertaPracy(RekrutacjaWakat wakat)`.
**Workery rekrutacji (klasy zagnieżdżone):**
| Worker | Metoda | Parametry |
|---|---|---|
| `Soneta.HR2.RekrutacjaAplikacja.NowaRekrutacjaWorker` | rozpoczęcie rekrutacji z aplikacji | `Aplikacje: RekrutacjaAplikacja[]` |
| `Soneta.HR2.RekrutacjaWakat.NowaRekrutacjaWorker` | rozpoczęcie rekrutacji z wakatu | `Wakat: RekrutacjaWakat`, `Pracownicy: Pracownik[]` |
| `Soneta.HR2.OfertaPracy.NowaRekrutacjaWorker` | rozpoczęcie rekrutacji z oferty | `Oferta: OfertaPracy`, `Pracownicy: Pracownik[]` |
| `Soneta.HR.OcenaKandydatowWorker` | `Oceń()` | `Pars`, `Elementy: Rekrutacja[]`; ctor `(Context)` |
| `Soneta.HR.Rekrutacja.ZatrudnijWorker` | `PracHistoria Zatrudnij()` | `Pars`, `Rekrutacja: Rekrutacja` — tworzy zatrudnienie (zapis historii) |
**Snippet (dodanie aplikacji kandydata — wymaga def. stanowiska w bazie):**
```csharp
var hr2 = session.GetHR2();
var kandydat = session.GetKadry().Pracownicy.WgKodu["006"];
// Definicja stanowiska z konfiguracji HR (musi istnieć w session.GetHR().DefStanowisk):
var defStanowiska = session.GetHR().DefStanowisk
.Cast<Soneta.HR.DefinicjaStanowiska>().FirstOrDefault();
var wydzialDefStanowiska = new Soneta.HR.WydziałDefinicjiStanowiska(defStanowiska);
using (var t = session.Logout(editMode: true))
{
var aplikacja = session.AddRow(new RekrutacjaAplikacja(kandydat, wydzialDefStanowiska));
aplikacja.Data = Date.Today;
aplikacja.Stan = StanAplikacji.Wprowadzona;
t.Commit();
}
session.Save();
// Odczyt aplikacji kandydata:
foreach (RekrutacjaAplikacja a in kandydat.Aplikacje)
{
// a.Stanowisko, a.Stan, a.Data, a.Oferta
}
```
**Pułapki:**
- Cały proces rekrutacji wymaga **konfiguracji HR/HR2** (`DefinicjaStanowiska`,
`DefinicjaEtapuRekrutacji`, `WydziałDefinicjiStanowiska`). W bazie Demo te definicje **mogą nie
istnieć** — przed scenariuszem sprawdź dostępność, inaczej `Save()` rzuci wyjątek weryfikatora.
- `RekrutacjaAplikacja` przyjmuje w ctorze `WydziałDefinicjiStanowiska`, nie samą `DefinicjaStanowiska`
(wydział definicji powstaje z `new WydziałDefinicjiStanowiska(definicjaStanowiska)`).
- `EtapRekrutacji` i `Rekrutacja` implementują `IOcenaPracownika` — oceny etapów trzyma
`EtapRekrutacji.ElementyOceny` (te same `ElementOcenyPracownika` co w KADRY-K8).
- `new Rekrutacja(pracownik)` ustawia pole `Pracownik` i dodaje rekord do **roota** `HRModule.Rekrutacje`
(oraz `EtapRekrutacji` do `HRModule.EtapyRekrutacji`). Kolekcje na `Pracownik` (`Rekrutacje`/`Kandydatury`/
`EtapyRekrutacji`) to `ChildTable` wiązane przez relacje — do weryfikacji w teście pewniejszy jest root
`session.GetHR().Rekrutacje` niż `pracownik.Rekrutacje` (zależnie od relacji może być pusta dla samego `Pracownik`).
- Zatrudnienie kandydata realizuje `Rekrutacja.ZatrudnijWorker.Zatrudnij()` (zwraca `PracHistoria`) —
spina rekrutację z zatrudnieniem (sekcja A). Worker modyfikuje dane → transakcja + `CommitUI()` + `Save()`.
- `Stan` aplikacji (`Wprowadzona`/`Zakończona`/`Anulowana`) steruje cyklem życia — nie usuwaj aplikacji
z historią, oznaczaj `Anulowana`.