Soneta.Skills.Test

This commit is contained in:
Marcin Wojas
2026-06-06 22:33:15 +02:00
parent d42ca3e825
commit fb2f2695a3
38 changed files with 10644 additions and 0 deletions
@@ -0,0 +1,60 @@
using System.Linq;
using Soneta.Business;
using Soneta.Kadry;
using Soneta.Kalend;
using Soneta.Place;
using Soneta.Test;
using Soneta.Types;
using Prac = Soneta.Kadry.Pracownik;
namespace Soneta.Skills.Test.KadryPlace.Pracownik;
/// <summary>
/// Wspólna baza testów domeny Kadry/Płace (pracownik, etat, nieobecności, kalendarz, umowy, wypłaty).
/// Dziedziczy z <see cref="TestBase"/>, dzięki czemu:
/// <list type="bullet">
/// <item>udostępnia gotową sesję operacyjną (<c>Session</c>) powiązaną z testową bazą Demo (GoldStandard),</item>
/// <item>automatycznie wycofuje (rollback) wszystkie zmiany w bazie po zakończeniu testu,</item>
/// <item>daje metody pomocnicze <c>InTransaction</c>/<c>SaveDispose</c> do pracy w transakcjach.</item>
/// </list>
/// Baza dodaje skróty często powtarzane w testach kadrowo-płacowych: dostęp do modułów
/// (Kadry, Płace, Kalendarz) oraz pobieranie pracowników z bazy Demo po kodzie/nazwisku.
/// <para>
/// Cała baza operuje wyłącznie na <b>publicznym kontrakcie</b> platformy Soneta — tak jak dodatek
/// programisty zewnętrznego, który nie ma dostępu do kodu źródłowego aplikacji.
/// </para>
/// </summary>
public abstract class PracownikTestBase : TestBase
{
// === Moduły bieżącej sesji operacyjnej ===
/// <summary>Moduł Kadry — kartoteka pracowników (<c>Pracownicy</c>), historia kadrowa, etaty, umowy.</summary>
protected KadryModule Kadry => Session.GetKadry();
/// <summary>Moduł Płace — wypłaty, listy płac, elementy wynagrodzenia, definicje elementów.</summary>
protected PlaceModule Place => Session.GetPlace();
/// <summary>Moduł Kalendarz — nieobecności, kalendarze, plan pracy, dni pracy, RCP, limity.</summary>
protected KalendModule Kalend => Session.GetKalend();
// === Kody pracowników dostępnych w bazie Demo (GoldStandard) ===
// Baza Demo zawiera ~80 zatrudnionych pracowników etatowych (po jednym zapisie historii każdy).
// Kody są stabilne między uruchomieniami — używamy ich jako punktów wejścia do scenariuszy odczytu.
/// <summary>Kody przykładowych pracowników etatowych z bazy Demo (pole <c>Pracownik.Kod</c>).</summary>
protected static class Pracownik_
{
public const string Andrzejewski = "006";
public const string Bednarek = "007";
public const string Bujak = "008";
public const string Strzelecki = "009";
}
// === Wyszukiwanie pracowników (publiczne API) ===
/// <summary>Pobiera pracownika po kodzie (klucz unikalny <c>WgKodu</c>, case-insensitive) albo <c>null</c>.</summary>
protected Prac Pracownik(string kod) => Kadry.Pracownicy.WgKodu[kod];
/// <summary>Pierwszy pracownik wg kodu — wygodny, deterministyczny punkt startu dla testów odczytu.</summary>
protected Prac PierwszyPracownik() => Kadry.Pracownicy.WgKodu.Cast<Prac>().First();
}
@@ -0,0 +1,446 @@
using System;
using System.Linq;
using AwesomeAssertions;
using NUnit.Framework;
using Soneta.Business;
using Soneta.Kadry;
using Soneta.Types;
using Prac = Soneta.Kadry.Pracownik;
namespace Soneta.Skills.Test.KadryPlace.Pracownik;
/// <summary>
/// Rozdział A — „Pracownik: zatrudnienie i dane kartotekowe" (receptury A1, A2, A7, A9, A10, A14).
/// <para>
/// Testy są <b>wykonywalną dokumentacją</b> publicznego kontraktu platformy Soneta dla domeny
/// Kadry/Płace. Każda metoda mapuje się 1:1 do receptury z dokumentu skilla <c>pracownik.md</c> i
/// pokazuje realny model „root + historia": <c>Pracownik</c> (tabela <c>Pracownicy</c>) trzyma tylko
/// nieliczne pola niezmienne, a praktycznie wszystkie dane kadrowe siedzą w zapisach historycznych
/// <c>PracHistoria</c> (kolekcja <c>Pracownik.Historia</c>), w tym złożone pole <c>Etat</c>.
/// </para>
/// <para>
/// Wszystko działa na bazie Demo (GoldStandard) z automatycznym rollbackiem po teście, więc można
/// swobodnie tworzyć i modyfikować dane. Operujemy wyłącznie na <b>publicznym kontrakcie</b> — tak
/// jak dodatek programisty zewnętrznego bez dostępu do kodu źródłowego aplikacji.
/// </para>
/// </summary>
[TestFixture]
public class RozdzialA_PracownikTest : PracownikTestBase
{
// ============================== A1 — Zatrudnienie nowego pracownika ==============================
[Test]
[Description("A1: dodanie nowego PracownikFirmy (AddRow) automatycznie tworzy pierwszy zapis " +
"historii (Last); dane osobowe ustawiamy na Last; Save() utrwala pracownika.")]
public void A1_ZatrudnienieNowego_TworzyPierwszyZapisHistorii_IZapisuje()
{
Guid guid = Guid.Empty;
var kod = "A1_" + Guid.NewGuid().ToString("N").Substring(0, 6);
InTransaction(() =>
{
// Pracownik jest typem ABSTRAKCYJNYM — tworzymy konkretny PracownikFirmy.
// AddRow zwraca typowany wiersz; w OnAdded powstaje pierwszy PracHistoria + kalendarz,
// dlatego NIE tworzymy zapisu historii ręcznie — od razu mamy pracownik.Last.
var pracownik = Session.AddRow(new PracownikFirmy());
pracownik.Kod = kod; // pole rootu
// Mechanizm „root + historia": dane osobowe idą na ZAPIS historii (pierwszy zapis = Last),
// nie na root. Last istnieje już bezpośrednio po AddRow.
pracownik.Last.Should().NotBeNull("OnAdded tworzy pierwszy zapis historii (Last)");
pracownik.Last.Nazwisko = "Kowalska";
pracownik.Last.Imie = "Gabriela";
pracownik.Last.PESEL = "94010812345";
guid = pracownik.Guid;
});
// Save w osobnej sesji-zamknięciu: wykrywanie konfliktów/duplikatów dzieje się w Save().
SaveDispose();
// Odczyt na świeżej sesji po Guid — potwierdza utrwalenie pracownika i jego pierwszego zapisu.
var zapis = Get<Prac>(guid);
zapis.Should().NotBeNull("pracownik został zapisany do bazy");
zapis.Kod.Should().Be(kod);
zapis.Last.Should().NotBeNull("nadal istnieje pierwszy zapis historii");
zapis.Last.Nazwisko.Should().Be("Kowalska");
zapis.Last.Imie.Should().Be("Gabriela");
// Pierwszy zapis historii ma okres otwarty (do końca czasu).
zapis.Last.Aktualnosc.To.Should().Be(Date.MaxValue, "pierwszy zapis ma okres otwarty");
}
[Test]
[Description("A1: typ Pracownik jest abstrakcyjny — konkretnym typem zatrudnienia jest PracownikFirmy " +
"(dziedziczy po Pracownik); to on jest dodawany do kartoteki.")]
public void A1_PracownikFirmy_JestKonkretnymTypemPracownika()
{
// Dokumentujemy regułę z receptury: new Pracownik() jest niemożliwe (typ abstrakcyjny),
// więc używamy PracownikFirmy. Sprawdzamy relację dziedziczenia bez instancjonowania abstrakta.
typeof(Prac).IsAbstract.Should().BeTrue("Pracownik jest klasą abstrakcyjną");
typeof(Prac).IsAssignableFrom(typeof(PracownikFirmy))
.Should().BeTrue("PracownikFirmy jest konkretnym typem pracownika firmy");
}
// ============================== A2 — Podstawowe dane kadrowe ==============================
[Test]
[Description("A2: dane ewidencyjno-identyfikacyjne (NazwiskoRodowe, ImieOjca, NIP, Urodzony, " +
"Obywatelstwo, Adres) ustawiamy na zapisie historii (Last) oraz jego subrowach.")]
public void A2_DaneKadrowe_UstawianeNaZapisieHistorii_ISubrowach()
{
Guid guid = Guid.Empty;
var kod = "A2_" + Guid.NewGuid().ToString("N").Substring(0, 6);
InTransaction(() =>
{
var pracownik = Session.AddRow(new PracownikFirmy());
pracownik.Kod = kod;
var ph = pracownik.Last; // bieżący (ostatni) zapis kadrowy
ph.Nazwisko = "Nowak";
ph.Imie = "Anna";
ph.NazwiskoRodowe = "Wiśniewska";
ph.ImieOjca = "Jan";
ph.NIP = "1234563218";
// Urodzony, Obywatelstwo, Adres to SUBROWY (pola złożone) — modyfikujemy ich pola,
// nie przypisujemy całego obiektu.
ph.Urodzony.Data = new Date(1994, 1, 8);
ph.Urodzony.Miejsce = "Kraków";
ph.Obywatelstwo.Nazwa = "polskie";
ph.Adres.Ulica = "Wadowicka";
ph.Adres.NrDomu = "8A";
ph.Adres.KodPocztowyS = "30-415"; // wersja stringowa kodu (z myślnikiem)
ph.Adres.Miejscowosc = "Kraków";
guid = pracownik.Guid;
});
SaveDispose();
var ph2 = Get<Prac>(guid).Last;
ph2.NazwiskoRodowe.Should().Be("Wiśniewska");
ph2.ImieOjca.Should().Be("Jan");
ph2.NIP.Should().Be("1234563218");
ph2.Urodzony.Data.Should().Be(new Date(1994, 1, 8));
ph2.Urodzony.Miejsce.Should().Be("Kraków");
ph2.Obywatelstwo.Nazwa.Should().Be("polskie");
ph2.Adres.Ulica.Should().Be("Wadowicka");
ph2.Adres.NrDomu.Should().Be("8A");
ph2.Adres.Miejscowosc.Should().Be("Kraków");
// KodPocztowyS to string z myślnikiem; KodPocztowy to int (bez myślnika).
ph2.Adres.KodPocztowyS.Should().Be("30-415");
ph2.Adres.KodPocztowy.Should().Be(30415);
}
[Test]
[Description("A2: Plec to enum PłećOsoby; przy poprawnym numerze PESEL płeć jest wyliczana " +
"automatycznie przez weryfikator (parzysta cyfra przed kontrolną = kobieta).")]
public void A2_Plec_WyliczanaZPESEL()
{
var p = PierwszyPracownik();
// Pracownik etatowy z Demo ma ustawiony PESEL — płeć powinna być jedną z wartości enuma.
p.Last.Plec.Should().BeOneOf(PłećOsoby.Kobieta, PłećOsoby.Mężczyzna);
}
// ============================== A7 — Ubezpieczenia społeczne i zdrowotne ==============================
[Test]
[Description("A7: tytuł ubezpieczenia (Tyub4) to rekord słownika TytulyUbezpiecz4 pobierany " +
"WgKodu[int]; daty objęcia ubezpieczeniami społecznymi ustawiamy na subrowach Spoleczne.")]
public void A7_Ubezpieczenia_UstawiajaTytulIDatyObjecia()
{
// Tytuł ubezpieczenia jest rekordem słownika KONFIGURACYJNEGO o kluczu int (np. 110 = pracownik).
// Wymaga, by w bazie Demo istniał wpis o tym kodzie — w przeciwnym razie pomijamy część tytułu.
var tyub110 = Kadry.TytulyUbezpiecz4.WgKodu[110] as TytulUbezpieczenia4;
Guid guid = Guid.Empty;
var kod = "A7_" + Guid.NewGuid().ToString("N").Substring(0, 6);
var od = new Date(2026, 1, 1);
InTransaction(() =>
{
var pracownik = Session.AddRow(new PracownikFirmy());
pracownik.Kod = kod;
pracownik.Last.Nazwisko = "Ubezpieczony"; // Nazwisko jest wymagane przy Save
pracownik.Last.Imie = "Tomasz";
// Cała struktura ubezpieczeń jest HISTORYCZNA — siedzi w Etat danego zapisu (Last.Etat).
var ub = pracownik.Last.Etat.Ubezpieczenia;
if (tyub110 != null)
ub.Tyub4 = tyub110; // tytuł ubezpieczenia (słownik), klucz int
// Data objęcia ubezpieczeniami społecznymi (publiczny setter na subrowie Ubezpieczenia).
// UWAGA: na poszczególnych subrowach Spoleczne (Emerytalne/Rentowe) pole `Od` NIE ma
// publicznego settera — jest wyliczane. Publicznie ustawiamy flagi Obowiazkowe/Dobrowolne
// oraz datę ObowiazkoweOd na zbiorczym subrowie Ubezpieczenia.
ub.ObowiazkoweOd = od;
ub.Emerytalne.Obowiazkowe = true;
ub.Rentowe.Obowiazkowe = true;
// Oddział NFZ to subrow — ustawiamy jego pola (np. datę OdDnia), nie cały obiekt.
pracownik.Last.OddzialNFZ.OdDnia = od;
guid = pracownik.Guid;
});
SaveDispose();
var ub2 = Get<Prac>(guid).Last.Etat.Ubezpieczenia;
ub2.ObowiazkoweOd.Should().Be(od);
ub2.Emerytalne.Obowiazkowe.Should().BeTrue();
ub2.Rentowe.Obowiazkowe.Should().BeTrue();
if (tyub110 != null)
ub2.Tyub4.Should().NotBeNull("ustawiliśmy tytuł ubezpieczenia ze słownika");
}
[Test]
[Description("A7 (odczyt): tytuł ubezpieczenia obowiązujący na dzień odczytujemy metodą " +
"Ubezpieczenia.WyliczTyubNaDzień(Date) — bez modyfikacji danych.")]
public void A7_WyliczTyubNaDzien_ZwracaTytulNaDzien()
{
// Odczyt „na dzień" dla istniejącego pracownika z Demo (zatrudniony etatowo).
var p = PierwszyPracownik();
var data = Date.Today;
// pracownik[data] zwraca zapis obowiązujący na datę; z jego Etat.Ubezpieczenia liczymy tytuł.
var zapisNaDzis = p[data];
zapisNaDzis.Should().NotBeNull("pracownik etatowy z Demo ma zapis obowiązujący na dziś");
// Metoda zwraca rekord TytulUbezpieczenia (starszy typ tytułu); może być null, gdy
// pracownik nie ma określonego tytułu na tę datę — istotne, że metoda działa bez wyjątku.
System.Action odczyt = () =>
{
TytulUbezpieczenia _ = zapisNaDzis.Etat.Ubezpieczenia.WyliczTyubNaDzień(data);
};
odczyt.Should().NotThrow("WyliczTyubNaDzień(Date) to publiczny odczyt tytułu na dzień");
}
// ============================== A9 — Rodzina pracownika (ZCNA) ==============================
[Test]
[Description("A9: członka rodziny tworzymy konstruktorem CzlonekRodziny(pracownik); zgłoszenie do " +
"ubezpieczenia zdrowotnego (ZCNA) to Ubezpieczony=true + UbezpieczenieOkres + StPokrewienstwa.")]
public void A9_CzlonekRodziny_ZglaszanyDoUbezpieczeniaZdrowotnego()
{
Guid guidPrac = Guid.Empty;
var kod = "A9_" + Guid.NewGuid().ToString("N").Substring(0, 6);
var od = new Date(2026, 1, 1);
InTransaction(() =>
{
var pracownik = Session.AddRow(new PracownikFirmy());
pracownik.Kod = kod;
pracownik.Last.Nazwisko = "Kowalski";
pracownik.Last.Imie = "Adam";
// Konstruktor CzlonekRodziny(pracownik) wiąże rekord z pracownikiem; AddRow włącza go do sesji.
var dziecko = Session.AddRow(new CzlonekRodziny(pracownik));
dziecko.Nazwisko = "Kowalska";
dziecko.Imie = "Zofia";
dziecko.PESEL = "20290512345";
dziecko.Urodzony.Data = new Date(2020, 9, 5); // Urodzony to subrow
dziecko.StPokrewienstwa = KodStPokrewienstwa.Dziecko; // enum stopnia pokrewieństwa
// Zgłoszenie do ubezpieczenia zdrowotnego (ZCNA):
dziecko.Ubezpieczony = true;
dziecko.UbezpieczenieOkres = new FromTo(od, Date.MaxValue);
dziecko.NaUtrzymaniu = true;
guidPrac = pracownik.Guid;
});
SaveDispose();
var pracownik2 = Get<Prac>(guidPrac);
// CzlonekRodziny pojawia się w kolekcji Rodzina pracownika (płaski child, nie historyczny).
var rodzina = pracownik2.Rodzina.Cast<CzlonekRodziny>().ToList();
rodzina.Should().ContainSingle("dodaliśmy jednego członka rodziny");
var cr = rodzina[0];
cr.Imie.Should().Be("Zofia");
cr.StPokrewienstwa.Should().Be(KodStPokrewienstwa.Dziecko);
cr.Ubezpieczony.Should().BeTrue();
cr.NaUtrzymaniu.Should().BeTrue();
cr.UbezpieczenieOkres.From.Should().Be(od);
// Odczyt aktualnie ubezpieczonych członków rodziny — filtr serwerowy po kolekcji (lambda).
var ubezpieczeni = pracownik2.Rodzina[(CzlonekRodziny c) => c.Ubezpieczony].Cast<CzlonekRodziny>().ToList();
ubezpieczeni.Should().ContainSingle("jedyny członek rodziny jest zgłoszony do ubezpieczenia");
}
// ============================== A10 — Poprzednie miejsca pracy ==============================
[Test]
[Description("A10: poprzedniego pracodawcę dodajemy konkretnym typem HistoriaZatrudnienia(pracownik) " +
"do kolekcji HistoriaZatrudnienia (inna niż Historia bieżącego zatrudnienia).")]
public void A10_PoprzedniPracodawca_DodawanyDoHistoriiZatrudnienia()
{
Guid guidPrac = Guid.Empty;
var kod = "A10_" + Guid.NewGuid().ToString("N").Substring(0, 6);
var okres = new FromTo(new Date(2018, 3, 1), new Date(2025, 12, 31));
InTransaction(() =>
{
var pracownik = Session.AddRow(new PracownikFirmy());
pracownik.Kod = kod;
pracownik.Last.Nazwisko = "Zieliński";
pracownik.Last.Imie = "Piotr";
// HistoriaZatrudnieniaBase ma ctor protected — tworzymy konkretny typ:
// HistoriaZatrudnienia (poprzedni pracodawca; ctor ustawia Typ = Zatrudnienie).
var hz = Session.AddRow(new HistoriaZatrudnienia(pracownik));
hz.Nazwa = "Poprzednia Firma Sp. z o.o.";
hz.Okres = okres;
hz.EfektywnyOkres = okres; // to EfektywnyOkres decyduje o wliczeniu do stażu
hz.Adres1 = "ul. Główna 1, Kraków";
// Drugi typ wpisu: okres nauki (UkonczonaSzkola) — także child pracownika.
var szkola = Session.AddRow(new UkonczonaSzkola(pracownik));
szkola.Nazwa = "Technikum nr 1";
szkola.Okres = new FromTo(new Date(2014, 9, 1), new Date(2018, 6, 30));
guidPrac = pracownik.Guid;
});
SaveDispose();
var pracownik2 = Get<Prac>(guidPrac);
// HistoriaZatrudnienia to kolekcja stażu u POPRZEDNICH pracodawców (typ bazowy w kolekcji).
var wpisy = pracownik2.HistoriaZatrudnienia.Cast<HistoriaZatrudnieniaBase>().ToList();
wpisy.Should().HaveCount(2, "dodaliśmy wpis pracy i wpis nauki");
var praca = wpisy.OfType<HistoriaZatrudnienia>().Single();
praca.Nazwa.Should().Be("Poprzednia Firma Sp. z o.o.");
// FromTo implementuje IEnumerable<Date>, więc porównujemy granice okresu, nie cały obiekt.
praca.Okres.From.Should().Be(okres.From);
praca.Okres.To.Should().Be(okres.To);
praca.EfektywnyOkres.From.Should().Be(okres.From);
praca.EfektywnyOkres.To.Should().Be(okres.To);
// Typ jest ustawiany przez ctor konkretnej klasy (praca vs nauka) — dwa różne wpisy.
wpisy.OfType<UkonczonaSzkola>().Should().ContainSingle("jeden wpis nauki");
}
// ============================== A14 — Aktualizacja historyczna „od daty" vs korekta ==============================
[Test]
[Description("A14: zmiana warunkow 'od daty' - Historia.Update(date) klonuje zapis i skraca stary; " +
"nowy klon dodajemy do tabeli PracHistorie i ustawiamy na nim zmienione warunki.")]
public void A14_AktualizacjaOdDaty_TworzyNowyZapisOdDnia_ISkracaStary()
{
Guid guidPrac = Guid.Empty;
var odDnia = new Date(2026, 7, 1);
InTransaction(() =>
{
var pracownik = Session.AddRow(new PracownikFirmy());
pracownik.Kod = "A14_" + Guid.NewGuid().ToString("N").Substring(0, 6);
pracownik.Last.Nazwisko = "Aktualizowany";
pracownik.Last.Imie = "Marek";
// Stan „przed zmianą" na pierwszym zapisie (pola pewnie zapisywalne na świeżym zapisie).
pracownik.Last.Etat.MiejscePracy = "Kraków";
pracownik.Last.Podatki.UlgaMnoznik = 0m;
// 1) Update klonuje zapis aktualny na odDnia, skraca stary do dnia poprzedniego
// i zwraca NOWY klon z okresem od odDnia.
var nowy = pracownik.Historia.Update(odDnia);
// 2) Update + AddRow to nierozłączna para — bez AddRow klon zostaje „odpięty".
pracownik.Module.PracHistorie.AddRow(nowy);
// 3) Na nowym zapisie wprowadzamy zmienione warunki (obowiązujące od odDnia).
nowy.Etat.MiejscePracy = "Warszawa"; // zmiana miejsca pracy od odDnia
nowy.Podatki.UlgaMnoznik = 1m; // zmiana danych podatkowych od odDnia
guidPrac = pracownik.Guid;
});
SaveDispose();
var pracownik2 = Get<Prac>(guidPrac);
// Mamy teraz dwa zapisy: stary (do odDnia-1) i nowy (od odDnia).
var zapisy = pracownik2.Historia.Cast<PracHistoria>().OrderBy(h => h.Aktualnosc.From).ToList();
zapisy.Should().HaveCount(2, "Update utworzył drugi zapis historii");
var stary = zapisy[0];
var nowy2 = zapisy[1];
// Stary zapis został skrócony do dnia poprzedzającego zmianę.
stary.Aktualnosc.To.Should().Be(odDnia.AddDays(-1));
nowy2.Aktualnosc.From.Should().Be(odDnia, "nowy zapis obowiązuje od wskazanego dnia");
// Warunki różnią się między okresami: inne miejsce pracy i ulga przed/od zmiany.
stary.Etat.MiejscePracy.Should().Be("Kraków");
nowy2.Etat.MiejscePracy.Should().Be("Warszawa");
stary.Podatki.UlgaMnoznik.Should().Be(0m);
nowy2.Podatki.UlgaMnoznik.Should().Be(1m);
}
[Test]
[Description("A14 (odczyt na dzień): indeksator pracownik[date] zwraca zapis obowiązujący na datę " +
"(Aktualnosc zawiera date), a dla daty sprzed zatrudnienia zwraca null.")]
public void A14_OdczytNaDzien_ZwracaWlasciwyZapis_INullPrzedZatrudnieniem()
{
Guid guidPrac = Guid.Empty;
var odDnia = new Date(2026, 7, 1);
InTransaction(() =>
{
var pracownik = Session.AddRow(new PracownikFirmy());
pracownik.Kod = "A14r_" + Guid.NewGuid().ToString("N").Substring(0, 6);
pracownik.Last.Nazwisko = "Czytany";
pracownik.Last.Imie = "Ewa";
var nowy = pracownik.Historia.Update(odDnia);
pracownik.Module.PracHistorie.AddRow(nowy);
nowy.NazwiskoRodowe = "PoZmianie";
guidPrac = pracownik.Guid;
});
SaveDispose();
var pracownik2 = Get<Prac>(guidPrac);
var pierwszy = pracownik2.Historia.GetFirst(); // najstarszy zapis (okres do odDnia-1)
// Odczyt „na dzień": data wewnątrz okresu pierwszego zapisu → zwraca pierwszy zapis.
var dzienWStarymOkresie = pierwszy.Aktualnosc.From;
pracownik2[dzienWStarymOkresie].Should().BeSameAs(pierwszy,
"pracownik[date] zwraca zapis, którego Aktualnosc zawiera date");
// Data w okresie nowego zapisu → zwraca nowy (najświeższy) zapis = Last.
pracownik2[odDnia].Should().BeSameAs(pracownik2.Last,
"od odDnia obowiązuje nowy zapis (Last)");
// Data sprzed zatrudnienia (przed początkiem pierwszego zapisu) → brak zapisu (null).
if (pierwszy.Aktualnosc.From > Date.MinValue)
{
var przedZatrudnieniem = pierwszy.Aktualnosc.From.AddDays(-1);
pracownik2[przedZatrudnieniem].Should().BeNull(
"dla daty sprzed zatrudnienia nie ma zapisu historii");
}
}
[Test]
[Description("A14 (korekta): modyfikacja zapisu pracownik[date] BEZ Update/AddRow zmienia dane " +
"w CAŁYM okresie tego zapisu — nie tworzy nowego okresu.")]
public void A14_Korekta_ZmieniaIstniejacyZapis_BezNowegoOkresu()
{
Guid guidPrac = Guid.Empty;
InTransaction(() =>
{
var pracownik = Session.AddRow(new PracownikFirmy());
pracownik.Kod = "A14k_" + Guid.NewGuid().ToString("N").Substring(0, 6);
pracownik.Last.Nazwisko = "Korygowany";
pracownik.Last.Imie = "Jan";
guidPrac = pracownik.Guid;
});
SaveDispose();
// Korekta: modyfikujemy zapis obowiązujący na wskazaną datę — bez Update, bez AddRow.
InTransaction(() =>
{
var ph = Get<Prac>(guidPrac)[Date.Today];
ph.Should().NotBeNull();
ph.NazwiskoRodowe = "PoprawioneNazwisko"; // korekta w istniejącym okresie
});
SaveDispose();
var pracownik2 = Get<Prac>(guidPrac);
// Liczba zapisów się nie zmieniła — korekta nie tworzy nowego okresu.
pracownik2.Historia.Cast<PracHistoria>().Should().ContainSingle("korekta nie dzieli okresu");
pracownik2.Last.NazwiskoRodowe.Should().Be("PoprawioneNazwisko");
}
}
@@ -0,0 +1,484 @@
using System;
using System.Linq;
using AwesomeAssertions;
using NUnit.Framework;
using Soneta.Business;
using Soneta.Core;
using Soneta.CRM;
using Soneta.Kadry;
using Soneta.Types;
using Prac = Soneta.Kadry.Pracownik;
namespace Soneta.Skills.Test.KadryPlace.Pracownik;
/// <summary>
/// Rozdział A (część kartotekowa) — pozostałe receptury danych osobowych/kadrowych pracownika:
/// A3 (adresy), A4 (kontakt), A5 (rachunki — odczyt), A6 (PIT), A8 (ZUS/NFZ), A11 (wykształcenie/
/// języki/wojsko), A12 (GUS/kod zawodu), A13 (PFRON), A15 (odczyt „na dzień"), A16 (powiązanie
/// z kontrahentem), A17 (archiwum — workery), A18 (zwolnienie), A19 (przerejestrowanie — zmiana Tyub4).
/// <para>
/// Testy są <b>wykonywalną dokumentacją</b> publicznego kontraktu Soneta dla domeny Kadry/Płace.
/// Operujemy wyłącznie na publicznym API — jak dodatek zewnętrzny bez dostępu do kodu źródłowego.
/// Wszystko działa na bazie Demo (GoldStandard) z rollbackiem po teście. Wartości enumów i klucze
/// słowników pobieramy/weryfikujemy dynamicznie, nie zgadujemy.
/// </para>
/// </summary>
[TestFixture]
public class RozdzialArest_KartotekaTest : PracownikTestBase
{
// Helper: świeży pracownik z danymi osobowymi (Last istnieje od razu po AddRow).
private Prac NowyPracownik(string prefix, out Guid guid)
{
var pracownik = Session.AddRow(new PracownikFirmy());
pracownik.Kod = prefix + "_" + Guid.NewGuid().ToString("N").Substring(0, 6);
pracownik.Last.Nazwisko = "Testowy"; // Nazwisko wymagane przy Save
pracownik.Last.Imie = "Jan";
guid = pracownik.Guid;
return pracownik;
}
// Helper: pracownik z USTAWIONYM etatem. Cały subrow Etat jest tylko-do-odczytu, dopóki nie
// ustawi się Etat.Okres (bramka, patrz B1). Po jego ustawieniu pracownik staje się etatowy, więc
// Save wymaga Etat.Wydzial ORAZ Etat.Stanowisko — ustawiamy oba.
private Prac NowyPracownikEtatowy(string prefix, out Guid guid)
{
var pracownik = NowyPracownik(prefix, out guid);
var etat = pracownik.Last.Etat;
etat.Okres = new FromTo(new Date(2026, 1, 1), Date.MaxValue); // PIERWSZE — odblokowuje Etat
etat.Wydzial = Kadry.Wydzialy.Firma; // wymagane dla etatu
etat.Stanowisko = "Specjalista"; // wymagane dla etatu
return pracownik;
}
// ============================== A3 — Adresy ==============================
[Test]
[Description("A3: adresy (zameldowania/zamieszkania/korespondencyjny) to subrowy Soneta.Core.Adres " +
"na zapisie historii (Last) — modyfikujemy ich pola, nie przypisujemy całego obiektu.")]
public void A3_Adresy_SaSubrowamiNaZapisieHistorii()
{
var g = Guid.Empty;
InTransaction(() =>
{
var ph = NowyPracownik("A3", out g).Last;
ph.AdresZamieszkania.Miejscowosc = "Kraków";
ph.AdresZamieszkania.Ulica = "Wadowicka";
ph.AdresZamieszkania.NrDomu = "8A";
ph.AdresZamieszkania.NrLokalu = "12";
ph.AdresZamieszkania.KodPocztowyS = "30-415";
ph.AdresZameldowania.Miejscowosc = "Wieliczka";
ph.AdresDoKorespondencji.Miejscowosc = "Kraków";
});
SaveDispose();
var ph2 = Get<Prac>(g).Last;
ph2.AdresZamieszkania.Ulica.Should().Be("Wadowicka");
ph2.AdresZamieszkania.NrDomu.Should().Be("8A");
ph2.AdresZamieszkania.KodPocztowyS.Should().Be("30-415");
ph2.AdresZamieszkania.KodPocztowy.Should().Be(30415); // int (bez myślnika)
ph2.AdresZameldowania.Miejscowosc.Should().Be("Wieliczka");
ph2.AdresDoKorespondencji.Miejscowosc.Should().Be("Kraków");
// Odczyt adresu na dzień:
Adres adr = Get<Prac>(g)[Date.Today].AdresZamieszkania;
adr.Miejscowosc.Should().Be("Kraków");
}
// ============================== A4 — Dane kontaktowe ==============================
[Test]
[Description("A4: dane kontaktowe (EMAIL/TelefonKomorkowy/WWW) to subrow Soneta.Core.Kontakt " +
"na zapisie historii — pole nazywa się EMAIL (wielkie litery).")]
public void A4_Kontakt_EmailTelefonWWW_NaSubrowieKontakt()
{
var g = Guid.Empty;
InTransaction(() =>
{
var k = NowyPracownik("A4", out g).Last.Kontakt; // subrow Kontakt
k.EMAIL = "g.kowalska@firma.pl";
k.TelefonKomorkowy = "600100200";
k.WWW = "https://firma.pl/g.kowalska";
});
SaveDispose();
var k2 = Get<Prac>(g).Last.Kontakt;
k2.EMAIL.Should().Be("g.kowalska@firma.pl");
k2.TelefonKomorkowy.Should().Be("600100200");
k2.WWW.Should().Be("https://firma.pl/g.kowalska");
}
[Test]
[Description("A4 (dostęp WWW/Pulpity): konto operatora web (IWebOperator) NIE jest zwykłym " +
"zapisywalnym polem PracHistoria — zarządza nim osobny mechanizm operatorów modułu web.")]
[Ignore("Dostęp do Pulpitów (IWebOperator) to osobny mechanizm operatorów/uprawnień web, " +
"nie pole kartoteki kadrowej — poza publicznym kontraktem ustawiania pól na pracowniku.")]
public void A4_DostepWWW_PulpityToOsobnyMechanizm()
{
}
// ============================== A5 — Rachunki bankowe (ODCZYT) ==============================
[Test]
[Description("A5 (odczyt): rachunki pracownika to kolekcja Pracownik.Rachunki " +
"(SubTable<RachunekBankowyPodmiotu>); rachunek główny zwraca Pracownik.DomyslnyRachunek.")]
public void A5_Rachunki_OdczytKolekcjiIRachunkuGlownego()
{
// Czysty odczyt na pracowniku z Demo — bez tworzenia rachunku (ctor numeru rachunku to typ
// biznesowy z walidacją IBAN/NRB, poza prostym kontraktem ustawiania pól).
var p = PierwszyPracownik();
// API odczytu istnieje i nie rzuca — kolekcja i property domyślnego rachunku.
System.Action odczyt = () =>
{
var glowny = p.DomyslnyRachunek; // może być null gdy brak rachunku
if (glowny != null)
{
_ = glowny.Domyslne;
_ = glowny.Rachunek; // subrow rachunku
}
foreach (var r in p.Rachunki)
{
_ = r.Domyslne;
_ = r.Priorytet;
}
};
odczyt.Should().NotThrow("Rachunki/DomyslnyRachunek to publiczny odczyt kontraktu A5");
}
// ============================== A6 — Dane podatkowe (PIT) ==============================
[Test]
[Description("A6: dane PIT to subrow PracHistoria.Podatki — KosztyRodzaj/TypProgow/UlgaCzesc to ENUMY, " +
"UlgaMnoznik to decimal (PIT-2). Wartości enumów pobieramy z realnych nazw składowych.")]
public void A6_DanePodatkowe_NaSubrowiePodatki()
{
var g = Guid.Empty;
InTransaction(() =>
{
var pdt = NowyPracownik("A6", out g).Last.Podatki;
pdt.KosztyRodzaj = RodzajKosztowUzyskania.JedenStosPracy; // enum (jeden stosunek pracy)
pdt.UlgaMnoznik = 1m; // pełna kwota zmniejszająca (PIT-2)
pdt.UlgaCzesc = UlgaPodatkowaCzesc.Ulga112; // podział PIT-2 (1/1)
pdt.TypProgow = TypProgowPodatkowych.Standardowe; // enum
});
SaveDispose();
var pdt2 = Get<Prac>(g).Last.Podatki;
pdt2.KosztyRodzaj.Should().Be(RodzajKosztowUzyskania.JedenStosPracy);
pdt2.UlgaMnoznik.Should().Be(1m);
pdt2.UlgaCzesc.Should().Be(UlgaPodatkowaCzesc.Ulga112);
pdt2.TypProgow.Should().Be(TypProgowPodatkowych.Standardowe);
}
// ============================== A8 — ZUS / NFZ ==============================
[Test]
[Description("A8: oddział NFZ to subrow PracHistoria.OddzialNFZ (OdDnia: Date) — zapisywalny. " +
"DodSwiadczeniaZUS na świeżym zapisie jest tylko-do-odczytu (cały subrow zablokowany).")]
public void A8_DodatkoweSwiadczeniaZUS_IOddzialNFZ()
{
var g = Guid.Empty;
InTransaction(() =>
{
var ph = NowyPracownik("A8", out g).Last;
// ROZBIEŻNOŚĆ z dokumentem: na świeżym zapisie CAŁY subrow DodSwiadczeniaZUS jest
// tylko-do-odczytu (ColReadOnlyException nawet dla Numer) — staje się edytowalny dopiero
// gdy świadczenie zostanie zainicjowane (np. przez UI/kreator). Tu ustawiamy NFZ.
ph.OddzialNFZ.OdDnia = new Date(2026, 1, 1);
});
SaveDispose();
var ph2 = Get<Prac>(g).Last;
ph2.OddzialNFZ.OdDnia.Should().Be(new Date(2026, 1, 1));
// Odczyt dodatkowych świadczeń ZUS — publiczny i nie rzuca (Rodzaj/Okres do odczytu).
System.Action odczyt = () =>
{
_ = ph2.DodSwiadczeniaZUS.Rodzaj;
_ = ph2.DodSwiadczeniaZUS.Okres;
};
odczyt.Should().NotThrow("dane dodatkowych świadczeń ZUS są dostępne do odczytu");
}
// ============================== A11 — Wykształcenie / języki / wojsko ==============================
[Test]
[Description("A11: wykształcenie i wojsko to subrowy PracHistoria (Kod/Stosunek/KategoriaZdrowia = " +
"ENUMY); języki obce to kolekcja na rootcie Pracownik.JęzykiObce.")]
public void A11_WyksztalcenieWojsko_NaHistorii_JezykiNaRootcie()
{
var g = Guid.Empty;
InTransaction(() =>
{
var ph = NowyPracownik("A11", out g).Last;
ph.Wyksztalcenie.Kod = KodWyksztalcenia.Wyzsze; // enum
ph.Wyksztalcenie.TytulNaukowy = "mgr inż.";
ph.Wojsko.Stosunek = KodStosDoSluzbyWojskowej.Rezerwa; // enum (uregulowany = rezerwa)
ph.Wojsko.KategoriaZdrowia = KategoriaZdrowia.A; // enum
ph.Wojsko.NrKsiazeczki = "AB123456";
});
SaveDispose();
var ph2 = Get<Prac>(g).Last;
ph2.Wyksztalcenie.Kod.Should().Be(KodWyksztalcenia.Wyzsze);
ph2.Wyksztalcenie.TytulNaukowy.Should().Be("mgr inż.");
ph2.Wojsko.Stosunek.Should().Be(KodStosDoSluzbyWojskowej.Rezerwa);
ph2.Wojsko.KategoriaZdrowia.Should().Be(KategoriaZdrowia.A);
ph2.Wojsko.NrKsiazeczki.Should().Be("AB123456");
// Odczyt kolekcji języków obcych (na rootcie) — nie rzuca; może być pusta.
System.Action czytajJezyki = () =>
{
foreach (var j in Get<Prac>(g).JęzykiObce) { _ = j.Jezyk; }
};
czytajJezyki.Should().NotThrow("JęzykiObce to publiczna kolekcja na rootcie pracownika");
}
// ============================== A12 — GUS / kod zawodu ==============================
[Test]
[Description("A12: dane statystyczne GUS to subrow PracHistoria.GUS (KodWyksztalcenia = enum " +
"KodWykształceniaGUS, INNE niż A11); kod zawodu to Etat.KodWykonywanegoZawodu (int).")]
public void A12_DaneGUS_IKodZawodu()
{
var g = Guid.Empty;
InTransaction(() =>
{
// Etat.KodWykonywanegoZawodu jest tylko-do-odczytu, dopóki nie ustawi się Etat.Okres
// (bramka subrowa Etat, patrz B1) — używamy więc pracownika z ustawionym etatem.
var ph = NowyPracownikEtatowy("A12", out g).Last;
ph.GUS.KodWyksztalcenia = KodWykształceniaGUS.Wyższe; // enum GUS (z diakrytykiem)
ph.GUS.GlowneMiejscePracy = true;
ph.GUS.PierwszaPraca = false;
ph.Etat.KodWykonywanegoZawodu = 251401; // kod zawodu GUS (int)
});
SaveDispose();
var ph2 = Get<Prac>(g).Last;
ph2.GUS.KodWyksztalcenia.Should().Be(KodWykształceniaGUS.Wyższe);
ph2.GUS.GlowneMiejscePracy.Should().BeTrue();
ph2.Etat.KodWykonywanegoZawodu.Should().Be(251401);
}
// ============================== A13 — PFRON ==============================
[Test]
[Description("A13: dane PFRON/niepełnosprawność to subrow PracHistoria.PFRON — Stopien = enum " +
"StNiepełnosprawności, Okres = FromTo, daty = Soneta.Types.Date.")]
public void A13_PFRON_StopienOkresIDaty()
{
var g = Guid.Empty;
var okres = new FromTo(new Date(2026, 1, 1), new Date(2028, 12, 31));
InTransaction(() =>
{
var pfron = NowyPracownik("A13", out g).Last.PFRON;
pfron.Stopien = StNiepełnosprawności.Umiarkowany; // enum
pfron.Okres = okres;
pfron.DataOrzeczenia = new Date(2025, 12, 1);
pfron.DataDostarczenia = new Date(2025, 12, 15);
});
SaveDispose();
var pfron2 = Get<Prac>(g).Last.PFRON;
pfron2.Stopien.Should().Be(StNiepełnosprawności.Umiarkowany);
pfron2.Okres.From.Should().Be(okres.From);
pfron2.Okres.To.Should().Be(okres.To);
pfron2.DataOrzeczenia.Should().Be(new Date(2025, 12, 1));
pfron2.DataDostarczenia.Should().Be(new Date(2025, 12, 15));
}
// ============================== A15 — Odczyt „na dzień" ==============================
[Test]
[Description("A15 (odczyt): indeksator pracownik[date] zwraca zapis obowiązujący na datę (Aktualnosc " +
"zawiera date), null dla daty sprzed zatrudnienia; GetFirst()/Last to skrajne zapisy.")]
public void A15_OdczytNaDzien_ZwracaZapisLubNull()
{
var p = PierwszyPracownik(); // zatrudniony etatowo pracownik z Demo
// 1) Zapis na dziś — istnieje dla zatrudnionego pracownika.
var phDzis = p[Date.Today];
phDzis.Should().NotBeNull("pracownik etatowy z Demo ma zapis obowiązujący na dziś");
// 2) Indeksator == kolekcja Historia[date].
p[Date.Today].Should().BeSameAs(p.Historia[Date.Today]);
// 3) Skrajne zapisy.
var pierwszy = p.Historia.GetFirst();
var ostatni = p.Last;
pierwszy.Should().NotBeNull();
ostatni.Should().NotBeNull();
p[Date.Today].Should().BeSameAs(p.Historia.GetLast(), "Last == Historia.GetLast()");
// 4) Data sprzed zatrudnienia → brak zapisu (null).
if (pierwszy.Aktualnosc.From > Date.MinValue)
{
var przed = pierwszy.Aktualnosc.From.AddDays(-1);
p[przed].Should().BeNull("dla daty sprzed pierwszego zapisu nie ma zapisu historii");
}
}
// ============================== A16 — Powiązanie z kontrahentem ==============================
[Test]
[Description("A16: powiązanie pracownika z istniejącym kontrahentem to zapisywalne pole rootu " +
"Pracownik.PowiazanyKontrahent (referencja, ta sama sesja); null = brak powiązania.")]
public void A16_PowiazanyKontrahent_UstawianyNaRootcie()
{
// Istniejący kontrahent z Demo (z tej samej sesji co pracownik).
var kontrahent = Session.GetCRM().Kontrahenci.WgKodu.Cast<Soneta.CRM.Kontrahent>().First();
var g = Guid.Empty;
InTransaction(() =>
{
var pracownik = NowyPracownik("A16", out g);
pracownik.PowiazanyKontrahent = kontrahent; // relacja na rootcie
});
SaveDispose();
var p2 = Get<Prac>(g);
p2.PowiazanyKontrahent.Should().NotBeNull("ustawiliśmy powiązanie z istniejącym kontrahentem");
p2.PowiazanyKontrahent.Guid.Should().Be(kontrahent.Guid);
}
// ============================== A17 — Archiwum (workery) ==============================
[Test]
[Description("A17 (odczyt): manager Pracownik.Archiwum udostępnia tylko-do-odczytu status archiwizacji " +
"(Status: enum InformacjeOArchiwum) i flagę Anonimizowany; pracownik aktywny = NieDotyczy.")]
public void A17_Archiwum_ManagerUdostepniaStatusDoOdczytu()
{
// Aktywny pracownik z Demo — nie jest w archiwum. Manager Archiwum to read-only API:
// Przenieś/Przywróć dostępne są WYŁĄCZNIE przez workery (patrz test poniżej).
var p = PierwszyPracownik();
p.Archiwum.Status.Should().Be(InformacjeOArchiwum.NieDotyczy,
"aktywny pracownik nie jest w archiwum (status = NieDotyczy)");
p.Archiwum.Anonimizowany.Should().BeFalse("aktywny pracownik nie jest zanonimizowany");
}
[Test]
[Description("A17 (zmiana stanu): przeniesienie/przywrócenie z archiwum jest dostępne WYŁĄCZNIE przez " +
"workery Pracownik.PrzenieśDoArchiwumWorker / PrzywróćZArchiwumWorker (CommitUI). Kod w ciele.")]
[Ignore("Worker PrzenieśDoArchiwum rzuca NullReferenceException w hoście testowym headless " +
"(Pracownik.ArchiwumManager) — archiwizacja zależy od stanu operatora/kontekstu UI nieobecnego " +
"w bazie Demo. Test dokumentuje jedyną publiczną drogę zmiany stanu archiwum (workery).")]
public void A17_Archiwum_PrzeniesienieIPrzywroceniePrzezWorkery()
{
var g = Guid.Empty;
InTransaction(() => NowyPracownik("A17", out g));
SaveDispose();
// Przeniesienie do archiwum — worker pojedynczego pracownika (CommitUI: worker „jak z UI").
InUITransaction(() =>
{
var worker = new Prac.PrzenieśDoArchiwumWorker { Pracownik = Get<Prac>(g) };
worker.PrzenieśDoArchiwum();
});
SaveDispose();
// Odczyt stanu archiwizacji (read-only API managera).
Get<Prac>(g).Archiwum.Status.Should().Be(InformacjeOArchiwum.WArchiwum);
// Przywrócenie z archiwum — drugi worker.
InUITransaction(() =>
{
var worker = new Prac.PrzywróćZArchiwumWorker { Pracownik = Get<Prac>(g) };
worker.PrzywróćZArchiwum();
});
SaveDispose();
Get<Prac>(g).Archiwum.Status.Should().NotBe(InformacjeOArchiwum.WArchiwum);
}
// ============================== A18 — Zwolnienie / wyrejestrowanie ==============================
[Test]
[Description("A18: zamknięcie zatrudnienia — Etat.Okres.To (ostatni dzień) + subrow Etat.RozwiazanieUmowy " +
"(Inicjatywa/PodstawaPrawna = enumy; wartości pobierane z realnych nazw składowych).")]
public void A18_Zwolnienie_EtatOkresIRozwiazanieUmowy()
{
var g = Guid.Empty;
var dataRozwiazania = new Date(2026, 6, 30);
// Podstawa prawna: enum o stałych „kodowych" (_400.._550, NieDotyczy) — bierzemy pierwszą realną
// wartość różną od NieDotyczy, zamiast zgadywać nazwę.
var podstawa = Enum.GetValues(typeof(KodPodstawyPrawnejZwolnienia))
.Cast<KodPodstawyPrawnejZwolnienia>()
.First(v => v != KodPodstawyPrawnejZwolnienia.NieDotyczy);
InTransaction(() =>
{
// Etat ustawiony (Okres+Wydzial+Stanowisko) — inaczej Save rzuca weryfikatorem wymagań etatu.
var etat = NowyPracownikEtatowy("A18", out g).Last.Etat;
// Zamknięcie okresu zatrudnienia ostatnim dniem pracy:
etat.Okres = new FromTo(etat.Okres.From, dataRozwiazania);
// Tryb rozwiązania (subrow RozwiazanieUmowy):
etat.RozwiazanieUmowy.Inicjatywa = KodInicjatywyZwolnienia.Pracownik; // enum
etat.RozwiazanieUmowy.PodstawaPrawna = podstawa; // enum (dynamicznie)
// Opcjonalnie okres wypowiedzenia:
etat.OkresWypowiedzenia.DataZlozenia = new Date(2026, 5, 31);
etat.OkresWypowiedzenia.Miesiace = 1;
});
SaveDispose();
var etat2 = Get<Prac>(g).Last.Etat;
etat2.Okres.To.Should().Be(dataRozwiazania, "okres zatrudnienia zamknięty ostatnim dniem pracy");
etat2.RozwiazanieUmowy.Inicjatywa.Should().Be(KodInicjatywyZwolnienia.Pracownik);
etat2.RozwiazanieUmowy.PodstawaPrawna.Should().Be(podstawa);
etat2.OkresWypowiedzenia.Miesiace.Should().Be(1);
}
[Test]
[Description("A18 (ZWUA): wyrejestrowanie z ZUS przez WyrejestrujPracownikaWorker wymaga Params(Context) " +
"oraz środowiska deklaracji ZUS — poza prostym kontraktem ustawiania pól etatu.")]
[Ignore("Wyrejestrowanie ZUS (ZWUA) wymaga WyrejestrujPracownikaParams(Context) i kontekstu deklaracji/" +
"KEDU; samo ustawienie Etat.Okres/RozwiazanieUmowy (test A18) nie tworzy dokumentu ZWUA.")]
public void A18_WyrejestrowanieZUS_WymagaContextIKedu()
{
}
// ============================== A19 — Przerejestrowanie (zmiana Tyub4) ==============================
[Test]
[Description("A19: przerejestrowanie = nowy zapis historii od daty (A14: Update + AddRow) ze zmianą " +
"Etat.Ubezpieczenia.Tyub4 (słownik TytulyUbezpiecz4, klucz int); deklaracje ZUS — osobny worker UI.")]
public void A19_Przerejestrowanie_ZmianaTyub4OdDaty()
{
// Tyub4 to słownik o kluczu int — bierzemy dwie różne realne wartości z bazy Demo (nie hardkodujemy).
var tytuly = Kadry.TytulyUbezpiecz4.Cast<TytulUbezpieczenia4>().Take(2).ToList();
if (tytuly.Count < 1)
{
Assert.Ignore("Brak słownika tytułów ubezpieczenia (TytulyUbezpiecz4) w bazie Demo.");
return;
}
var nowyTyub = tytuly.Last();
var g = Guid.Empty;
var odDnia = new Date(2026, 7, 1);
InTransaction(() =>
{
var pracownik = NowyPracownik("A19", out g);
// Nowy zapis historii „od daty" (A14): Update klonuje + skraca poprzedni, AddRow dopina klon.
var nowy = pracownik.Historia.Update(odDnia);
pracownik.Module.PracHistorie.AddRow(nowy);
// Zmiana kodu tytułu ubezpieczenia (przerejestrowanie ubezpieczeniowe) na nowym zapisie:
nowy.Etat.Ubezpieczenia.Tyub4 = nowyTyub;
});
SaveDispose();
var pracownik2 = Get<Prac>(g);
var zapisy = pracownik2.Historia.Cast<PracHistoria>().OrderBy(h => h.Aktualnosc.From).ToList();
zapisy.Should().HaveCount(2, "Update utworzył drugi zapis historii (przerejestrowanie od daty)");
zapisy[0].Aktualnosc.To.Should().Be(odDnia.AddDays(-1), "stary zapis skrócony do dnia poprzedzającego");
zapisy[1].Aktualnosc.From.Should().Be(odDnia, "nowy zapis obowiązuje od dnia przerejestrowania");
zapisy[1].Etat.Ubezpieczenia.Tyub4.Should().NotBeNull("nowy tytuł ubezpieczenia ustawiony od daty");
zapisy[1].Etat.Ubezpieczenia.Tyub4.Guid.Should().Be(nowyTyub.Guid);
}
}
@@ -0,0 +1,214 @@
using System;
using System.Linq;
using AwesomeAssertions;
using NUnit.Framework;
using Soneta.Business;
using Soneta.Kadry;
using Soneta.Place;
using Soneta.Types;
using Prac = Soneta.Kadry.Pracownik;
namespace Soneta.Skills.Test.KadryPlace.Pracownik;
/// <summary>
/// Rozdział B+C — „Etat (umowa o pracę)" i „Dodatki / stałe elementy wynagrodzenia"
/// (receptury B1 i C1 z dokumentu skilla <c>pracownik.md</c>).
/// <para>
/// Testy są <b>wykonywalną dokumentacją</b> publicznego kontraktu platformy Soneta. Pokazują:
/// <list type="bullet">
/// <item><b>B1</b> — warunki etatu siedzą w subrowie <c>PracHistoria.Etat</c>; stawkę ustawiamy na
/// subrowie <c>Etat.Zaszeregowanie</c> w wymaganej KOLEJNOŚCI (najpierw <c>RodzajStawki</c>, potem
/// <c>Wymiar</c>) — odwrócenie kolejności rzuca <see cref="ColReadOnlyException"/>;</item>
/// <item><b>C1</b> — dodatek (stały element wynagrodzenia) jest obiektem historycznym; tworzymy go
/// przez <c>new Dodatek(pracownik)</c> + <c>Kadry.Dodatki.AddRow</c>, a parametry (Element, Okres)
/// ustawiamy na pierwszym zapisie <c>d.Last</c>.</item>
/// </list>
/// </para>
/// <para>
/// Wszystko działa na bazie Demo (GoldStandard) z automatycznym rollbackiem po teście. Operujemy
/// wyłącznie na <b>publicznym kontrakcie</b> — tak jak dodatek programisty zewnętrznego bez dostępu
/// do kodu źródłowego aplikacji.
/// </para>
/// </summary>
[TestFixture]
public class RozdzialBC_EtatDodatkiTest : PracownikTestBase
{
// ============================== B1 — Definiowanie etatu (umowa o pracę) ==============================
[Test]
[Description("B1: warunki etatu ustawiamy na subrowie Etat zapisu historii. KOLEJNOŚĆ: najpierw " +
"Etat.Okres (odblokowuje pozostałe pola etatu), potem TypUmowy/Podstawa/Stanowisko/Wydzial " +
"oraz stawka na subrowie Zaszeregowanie. Wydzial to referencja do korzenia (Wydzialy.Firma).")]
public void B1_DefiniowanieEtatu_NaNowymPracowniku_UstawiaWarunkiIStawke()
{
Guid guid = Guid.Empty;
var kod = "B1_" + Guid.NewGuid().ToString("N").Substring(0, 6);
var okres = new FromTo(new Date(2026, 1, 1), Date.MaxValue);
InTransaction(() =>
{
// A1: AddRow tworzy pierwszy zapis historii (Last) + kalendarz — warunki etatu ustawiamy
// na Etat tego pierwszego zapisu.
var pracownik = Session.AddRow(new PracownikFirmy());
pracownik.Kod = kod;
pracownik.Last.Nazwisko = "Etatowy";
pracownik.Last.Imie = "Robert";
// Etat to SUBROW zapisu PracHistoria — modyfikujemy jego pola, nie przypisujemy obiektu.
var etat = pracownik.Last.Etat;
// KLUCZOWA KOLEJNOŚĆ: na świeżym (auto-utworzonym) zapisie cały Etat jest read-only,
// dopóki nie ustawimy zakresu zatrudnienia Etat.Okres. Okres MUSI być pierwszy —
// dopiero on odblokowuje TypUmowy/Podstawa/Stanowisko/Zaszeregowanie.
etat.Okres = okres; // FromTo, nie DateTime — USTAWIAMY PIERWSZE
etat.TypUmowy = TypUmowyOPrace.NaCzasNieokreślony; // enum, nie string
etat.Podstawa = StosPracyNaPodstawie.UmowyOPrace; // podstawa stosunku pracy (enum)
etat.DataZawarcia = new Date(2025, 12, 20);
etat.DataRozpPracy = new Date(2026, 1, 1);
etat.Stanowisko = "Specjalista";
etat.Wydzial = Kadry.Wydzialy.Firma; // referencja do istniejącego wydziału (korzeń)
// Stawka — subrow Zaszeregowanie. Po ustawieniu Etat.Okres wszystkie pola stawki są
// zapisywalne; ustawiamy je w czytelnej kolejności RodzajStawki -> TypStawki -> Wymiar -> Stawka.
var z = etat.Zaszeregowanie;
z.RodzajStawki = RodzajStawkiZaszeregowania.Miesieczna; // rodzaj stawki
z.TypStawki = TypStawkiZaszeregowania.Dowolna; // typ stawki
z.Wymiar = Fraction.One; // pełny etat
z.Stawka = (Currency)6000m; // kwota brutto miesięcznie
guid = pracownik.Guid;
});
SaveDispose();
// Odczyt na świeżej sesji po Guid — potwierdza utrwalenie warunków etatu i stawki.
var etat2 = Get<Prac>(guid).Last.Etat;
etat2.TypUmowy.Should().Be(TypUmowyOPrace.NaCzasNieokreślony);
etat2.Podstawa.Should().Be(StosPracyNaPodstawie.UmowyOPrace);
etat2.Stanowisko.Should().Be("Specjalista");
etat2.Wydzial.Should().NotBeNull("Wydzial wskazuje na istniejący wydział (korzeń struktury)");
// FromTo implementuje IEnumerable<Date> — porównujemy granice okresu, nie cały obiekt.
etat2.Okres.From.Should().Be(okres.From);
var z2 = etat2.Zaszeregowanie;
z2.RodzajStawki.Should().Be(RodzajStawkiZaszeregowania.Miesieczna);
z2.Wymiar.Should().Be(Fraction.One, "pełny etat");
z2.Stawka.Should().Be((Currency)6000m, "kwota brutto miesięcznie");
}
[Test]
[Description("B1 (pułapka kolejności): na świeżym zapisie historii cały Etat jest tylko-do-odczytu " +
"dopóki nie ustawimy Etat.Okres. Próba ustawienia TypUmowy/RodzajStawki/Wymiar PRZED " +
"Etat.Okres rzuca ColReadOnlyException; po ustawieniu Okres pola stają się zapisywalne.")]
public void B1_Pulapka_PolaEtatuReadOnlyDopokiNieUstawionoOkresu()
{
InTransaction(() =>
{
var pracownik = Session.AddRow(new PracownikFirmy());
pracownik.Kod = "B1x_" + Guid.NewGuid().ToString("N").Substring(0, 6);
pracownik.Last.Nazwisko = "Pulapka";
pracownik.Last.Imie = "Karol";
var etat = pracownik.Last.Etat;
// PRZED ustawieniem Etat.Okres pola etatu są tylko-do-odczytu — przypisanie rzuca wyjątek.
System.Action typUmowyPrzedOkresem = () => etat.TypUmowy = TypUmowyOPrace.NaCzasNieokreślony;
typUmowyPrzedOkresem.Should().Throw<ColReadOnlyException>(
"TypUmowy jest read-only dopóki nie ustawiono Etat.Okres");
System.Action rodzajStawkiPrzedOkresem = () => etat.Zaszeregowanie.RodzajStawki = RodzajStawkiZaszeregowania.Miesieczna;
rodzajStawkiPrzedOkresem.Should().Throw<ColReadOnlyException>(
"Zaszeregowanie.RodzajStawki też jest read-only przed Etat.Okres");
System.Action wymiarPrzedOkresem = () => etat.Zaszeregowanie.Wymiar = new Fraction(1, 2);
wymiarPrzedOkresem.Should().Throw<ColReadOnlyException>(
"Zaszeregowanie.Wymiar też jest read-only przed Etat.Okres");
// Ustawienie Etat.Okres ODBLOKOWUJE pozostałe pola etatu i stawki.
etat.Okres = new FromTo(new Date(2026, 1, 1), Date.MaxValue);
System.Action poOkresie = () =>
{
etat.TypUmowy = TypUmowyOPrace.NaCzasNieokreślony;
etat.Zaszeregowanie.RodzajStawki = RodzajStawkiZaszeregowania.Miesieczna;
etat.Zaszeregowanie.Wymiar = new Fraction(1, 2);
};
poOkresie.Should().NotThrow("po ustawieniu Etat.Okres pola etatu i stawki są zapisywalne");
etat.Zaszeregowanie.Wymiar.Should().Be(new Fraction(1, 2), "½ etatu");
// Nie commitujemy realnych danych — pracownik bez kompletnych warunków;
// mechanizm testów i tak wycofuje transakcję, ale dla jasności nie utrwalamy.
});
}
// ============================== C1 — Dodatki / stałe elementy wynagrodzenia ==============================
[Test]
[Description("C1: dodatek tworzymy przez new Dodatek(pracownik) + Kadry.Dodatki.AddRow (para); " +
"AddRow tworzy pierwszy zapis DodHistoria (d.Last), na którym ustawiamy Element " +
"(z Place.DefElementow.WgNazwy[\"Premia\"]) oraz Okres. Odczyt z pracownik.Dodatki.")]
public void C1_Dodatek_TworzonyZDefinicjaElementu_IOkresem()
{
// Definicja elementu wynagrodzenia ze słownika KONFIGURACYJNEGO (po nazwie).
// W bazie Demo istnieje gotowa definicja "Premia".
var definicjaPremii = Place.DefElementow.WgNazwy["Premia"] as DefinicjaElementu;
definicjaPremii.Should().NotBeNull("baza Demo zawiera definicję elementu \"Premia\"");
Guid guidPrac = Guid.Empty;
var okres = new FromTo(new Date(2026, 1, 1), Date.MaxValue);
InTransaction(() =>
{
// Tworzymy świeżego pracownika z etatem (świeży = nie ma jeszcze żadnych dodatków,
// w odróżnieniu od pracowników z Demo, którym już przypisano premie/składki).
var pracownik = Session.AddRow(new PracownikFirmy());
pracownik.Kod = "C1_" + Guid.NewGuid().ToString("N").Substring(0, 6);
pracownik.Last.Nazwisko = "Premiowany";
pracownik.Last.Imie = "Lucjan";
// Etat.Okres najpierw — odblokowuje warunki etatu (patrz B1). Po ustawieniu Okres
// weryfikator wymaga jednostki organizacyjnej (Wydzial) przy Save.
pracownik.Last.Etat.Okres = okres;
pracownik.Last.Etat.Wydzial = Kadry.Wydzialy.Firma;
pracownik.Last.Etat.Stanowisko = "Specjalista";
// new Dodatek(pracownik) + AddRow — PARA. Sam ctor nie włącza dodatku do sesji ani
// nie tworzy zapisu historii; pierwszy DodHistoria powstaje przy AddRow.
var dodatek = new Dodatek(pracownik);
Kadry.Dodatki.AddRow(dodatek);
// Parametry ustawiamy na pierwszym zapisie historii dodatku (d.Last).
var h = dodatek.Last;
h.Should().NotBeNull("AddRow tworzy pierwszy zapis DodHistoria (Last)");
h.Element = definicjaPremii; // definicja elementu (wymagana)
h.Okres = okres;
guidPrac = pracownik.Guid;
});
SaveDispose();
// Odczyt: dodatek pojawia się w kolekcji childów pracownika (pracownik.Dodatki).
var pracownik2 = Get<Prac>(guidPrac);
var dodatki = pracownik2.Dodatki.Cast<Dodatek>().ToList();
dodatki.Should().ContainSingle("dodaliśmy jeden dodatek do świeżego pracownika");
var d = dodatki[0];
d.Last.Element.Should().NotBeNull("Element jest wymagany");
d.Last.Element.Nazwa.Should().Be("Premia");
d.Last.Okres.From.Should().Be(okres.From, "okres obowiązywania dodatku");
}
[Test]
[Description("C1 (definicja elementu): definicje dodatków pobieramy ze słownika Place.DefElementow; " +
"definicja \"Premia\" istnieje w bazie Demo i jest źródłem typu Dodatek.")]
public void C1_DefinicjaElementu_PobieranaZeSlownika_PoNazwie()
{
// DefElementow to kolekcja konfiguracyjna; indeksowanie WgNazwy zwraca definicję po nazwie.
var premia = Place.DefElementow.WgNazwy["Premia"] as DefinicjaElementu;
premia.Should().NotBeNull("baza Demo zawiera definicję \"Premia\"");
premia.Nazwa.Should().Be("Premia");
// Definicje przeznaczone na dodatki mają RodzajZrodla == RodzajŹródłaWypłaty.Dodatek —
// tym kryterium można filtrować dostępne definicje dodatków.
premia.RodzajZrodla.Should().Be(RodzajŹródłaWypłaty.Dodatek,
"Premia jest definicją źródła typu Dodatek");
}
}
@@ -0,0 +1,365 @@
using System;
using System.Linq;
using AwesomeAssertions;
using NUnit.Framework;
using Soneta.Business;
using Soneta.Core;
using Soneta.HR;
using Soneta.Kadry;
using Soneta.Types;
using Prac = Soneta.Kadry.Pracownik;
namespace Soneta.Skills.Test.KadryPlace.Pracownik;
/// <summary>
/// Rozdział B (pozostałe receptury etatu) — B2..B7 z dokumentu skilla <c>pracownik.md</c>:
/// <list type="bullet">
/// <item><b>B2</b> — aneks (zmiana warunków zatrudnienia „od daty");</item>
/// <item><b>B3</b> — przeszeregowanie (zmiana stawki / grupy zaszeregowania);</item>
/// <item><b>B4</b> — rozwiązanie / wygaśnięcie umowy o pracę;</item>
/// <item><b>B5</b> — obniżenie wymiaru etatu;</item>
/// <item><b>B6</b> — podzielniki kosztów (rozdział kosztów wynagrodzenia);</item>
/// <item><b>B7</b> — aktualizacja danych wg definicji stanowiska (matrycy).</item>
/// </list>
/// <para>
/// Wszystkie zmiany „od daty" realizujemy wzorcem A14: <c>Historia.Update(date)</c> klonuje zapis
/// aktualny na datę, skraca stary do dnia poprzedniego i zwraca nowy klon (okres od daty), który
/// MUSI trafić do tabeli <c>PracHistorie</c> (<c>AddRow</c>). Na świeżym zapisie obowiązuje bramka B1:
/// <c>Etat.Okres</c> ustawiamy jako pierwsze pole etatu (odblokowuje pozostałe), a do <c>Save()</c>
/// wymagane są <c>Etat.Wydzial</c> i <c>Etat.Stanowisko</c>.
/// </para>
/// <para>
/// Kody słowników (przyczyna rozwiązania, definicja stanowiska, grupa zaszeregowania) pobieramy
/// DYNAMICZNIE z bazy Demo (iteracja słownika / pierwszy wpis) — nie zakładamy konkretnych kodów.
/// Wszystko działa na bazie Demo (GoldStandard) z automatycznym rollbackiem; operujemy wyłącznie na
/// publicznym kontrakcie platformy.
/// </para>
/// </summary>
[TestFixture]
public class RozdzialBrest_EtatTest : PracownikTestBase
{
// === Pomocnik: świeży pracownik etatowy z kompletem warunków wymaganych przy Save ===
/// <summary>
/// Tworzy świeżego <see cref="PracownikFirmy"/> z pierwszym zapisem historii i kompletnym etatem
/// (Okres → Wydzial/Stanowisko → stawka). Zwraca pracownika; zakładamy bycie w transakcji.
/// </summary>
private Prac UtworzPracownikaZEtatem(string prefix, FromTo okres,
RodzajStawkiZaszeregowania rodzaj = RodzajStawkiZaszeregowania.Miesieczna,
decimal stawka = 6000m)
{
var pracownik = Session.AddRow(new PracownikFirmy());
pracownik.Kod = prefix + "_" + Guid.NewGuid().ToString("N").Substring(0, 6);
pracownik.Last.Nazwisko = "Testowy";
pracownik.Last.Imie = "Jan";
var etat = pracownik.Last.Etat;
etat.Okres = okres; // BRAMKA B1: Okres najpierw
etat.TypUmowy = TypUmowyOPrace.NaCzasNieokreślony;
etat.Podstawa = StosPracyNaPodstawie.UmowyOPrace;
etat.Stanowisko = "Specjalista"; // wymagane przy Save
etat.Wydzial = Kadry.Wydzialy.Firma; // wymagane przy Save (referencja)
var z = etat.Zaszeregowanie;
z.RodzajStawki = rodzaj;
z.TypStawki = TypStawkiZaszeregowania.Dowolna;
z.Wymiar = Fraction.One;
z.Stawka = (Currency)stawka;
return pracownik;
}
// ============================== B2 — Zmiana warunków zatrudnienia (aneks) ==============================
[Test]
[Description("B2: aneks 'od daty' to nowy zapis historii — Historia.Update(odDnia) + PracHistorie.AddRow; " +
"na sklonowanym Etat (Okres już ustawiony) zmieniamy warunki: Stanowisko, MiejscePracy, " +
"DataZawarcia, Wydzial. Stary okres skraca się do odDnia-1, nowy obowiązuje od odDnia.")]
public void B2_Aneks_ZmianaWarunkow_OdDaty_TworzyNowyZapis()
{
Guid guid = Guid.Empty;
var okres = new FromTo(new Date(2026, 1, 1), Date.MaxValue);
var odDnia = new Date(2026, 7, 1);
InTransaction(() =>
{
var pracownik = UtworzPracownikaZEtatem("B2", okres);
// Aneks od daty — klon zapisu aktualnego na odDnia (skraca stary, zwraca nowy z okresem od odDnia).
var nowy = (PracHistoria)pracownik.Historia.Update(odDnia);
pracownik.Module.PracHistorie.AddRow(nowy); // nierozłączna para z Update
var etat = nowy.Etat; // Okres etatu sklonowany — pola zapisywalne
etat.Stanowisko = "Starszy specjalista";
etat.MiejscePracy = "Oddział Kraków";
etat.DataZawarcia = new Date(2026, 6, 20);
etat.Wydzial = Kadry.Wydzialy.Firma; // referencja (wymagana)
guid = pracownik.Guid;
});
SaveDispose();
var pracownik2 = Get<Prac>(guid);
var zapisy = pracownik2.Historia.Cast<PracHistoria>().OrderBy(h => h.Aktualnosc.From).ToList();
zapisy.Should().HaveCount(2, "aneks utworzył drugi zapis historii");
zapisy[0].Aktualnosc.To.Should().Be(odDnia.AddDays(-1), "stary okres skrócony do dnia poprzedzającego aneks");
zapisy[1].Aktualnosc.From.Should().Be(odDnia, "nowe warunki obowiązują od daty aneksu");
zapisy[1].Etat.Stanowisko.Should().Be("Starszy specjalista");
zapisy[1].Etat.MiejscePracy.Should().Be("Oddział Kraków");
// Stanowisko sprzed aneksu pozostaje na starym zapisie.
zapisy[0].Etat.Stanowisko.Should().Be("Specjalista");
}
// ============================== B3 — Przeszeregowanie (zmiana stawki / grupy) ==============================
[Test]
[Description("B3: przeszeregowanie 'od daty' — nowy zapis historii, a na Etat.Zaszeregowanie podnosimy " +
"Stawka (Currency). Stawka to Currency, Wymiar to Fraction. Grupę pobieramy DYNAMICZNIE " +
"ze słownika GrupyZaszer (pierwszy wpis), nie hardkodujemy kodu.")]
public void B3_Przeszeregowanie_ZmianaStawki_IGrupy_OdDaty()
{
// Grupa zaszeregowania — referencja do słownika; bierzemy pierwszy istniejący wpis (jeśli jest).
var grupa = Kadry.GrupyZaszer.Cast<GrupaZaszeregowania>().FirstOrDefault();
Guid guid = Guid.Empty;
var okres = new FromTo(new Date(2026, 1, 1), Date.MaxValue);
var odDnia = new Date(2026, 7, 1);
InTransaction(() =>
{
var pracownik = UtworzPracownikaZEtatem("B3", okres, stawka: 6000m);
var nowy = (PracHistoria)pracownik.Historia.Update(odDnia);
pracownik.Module.PracHistorie.AddRow(nowy);
var etat = nowy.Etat; // Okres sklonowany — pola zapisywalne
etat.Zaszeregowanie.Stawka = (Currency)7200m; // podwyżka stawki zasadniczej
if (grupa != null)
etat.Grupa = grupa; // grupa zaszeregowania leży na Etat (nie na Zaszeregowanie)
guid = pracownik.Guid;
});
SaveDispose();
var pracownik2 = Get<Prac>(guid);
var nowyZapis = pracownik2.Historia.Cast<PracHistoria>().OrderBy(h => h.Aktualnosc.From).Last();
nowyZapis.Etat.Zaszeregowanie.Stawka.Should().Be((Currency)7200m, "stawka podwyższona od daty przeszeregowania");
if (grupa != null)
nowyZapis.Etat.Grupa.Should().NotBeNull("grupa zaszeregowania powiązana z etatem");
}
// ============================== B4 — Rozwiązanie / wygaśnięcie umowy o pracę ==============================
[Test]
[Description("B4: rozwiązanie umowy — skrócenie Etat.Okres.To do dnia rozwiązania, dane wypowiedzenia " +
"(OkresWypowiedzenia.*), przyczyna ze słownika PrzyczRozwUmow (pobrana DYNAMICZNIE, pierwszy " +
"wpis), tryb (PodstawaPrawna/Inicjatywa enumy) oraz flaga Etat.PracownikZwolniony.")]
public void B4_RozwiazanieUmowy_SkracaOkres_UstawiaPrzyczyneIWypowiedzenie()
{
// Przyczyna rozwiązania to REKORD słownika (referencja), nie enum — bierzemy pierwszy wpis z Demo.
var przyczyna = Kadry.PrzyczRozwUmow.Cast<PrzyczynaRozwUmowy>().FirstOrDefault();
Guid guid = Guid.Empty;
var okres = new FromTo(new Date(2026, 1, 1), Date.MaxValue);
var dataRozwiazania = new Date(2026, 9, 30);
InTransaction(() =>
{
var pracownik = UtworzPracownikaZEtatem("B4", okres);
var etat = pracownik.Last.Etat;
// 1) skrócenie okresu etatu do dnia rozwiązania (zmiana w całym bieżącym okresie zapisu)
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
if (przyczyna != null)
etat.RozwiazanieUmowy.PrzyczynaRozwUmowy = przyczyna; // referencja do słownika
etat.RozwiazanieUmowy.Inicjatywa = KodInicjatywyZwolnienia.Pracownik; // enum
etat.PracownikZwolniony = true; // znacznik zakończenia
guid = pracownik.Guid;
});
SaveDispose();
var etat2 = Get<Prac>(guid).Last.Etat;
etat2.Okres.To.Should().Be(dataRozwiazania, "okres etatu skrócony do dnia rozwiązania");
etat2.OkresWypowiedzenia.DataZlozenia.Should().Be(new Date(2026, 8, 31));
etat2.OkresWypowiedzenia.Miesiace.Should().Be(1);
etat2.RozwiazanieUmowy.Inicjatywa.Should().Be(KodInicjatywyZwolnienia.Pracownik);
etat2.PracownikZwolniony.Should().BeTrue();
if (przyczyna != null)
etat2.RozwiazanieUmowy.PrzyczynaRozwUmowy.Should().NotBeNull("przyczyna rozwiązania ze słownika");
}
[Test]
[Description("B4 (rozróżnienie): PrzyczynaRozwUmowy to rekord słownika (referencja) z polem Typ " +
"(enum TypPrzyczynyRozwUmowy: Rozwiązanie/Wygaśnięcie) — to ono rozróżnia rozwiązanie od " +
"wygaśnięcia. Referencja (rekord) != Typ (enum na rekordzie).")]
public void B4_PrzyczynaRozwUmowy_JestRekordemSlownika_ZTypem()
{
var przyczyny = Kadry.PrzyczRozwUmow.Cast<PrzyczynaRozwUmowy>().ToList();
przyczyny.Should().NotBeEmpty("baza Demo zawiera słownik przyczyn rozwiązania umowy");
// Typ jest enumem na rekordzie słownika — przyjmuje jedną z dwóch wartości domeny.
foreach (var p in przyczyny)
p.Typ.Should().BeOneOf(TypPrzyczynyRozwUmowy.Rozwiązanie, TypPrzyczynyRozwUmowy.Wygaśnięcie);
}
// ============================== B5 — Obniżenie wymiaru etatu ==============================
[Test]
[Description("B5: obniżenie wymiaru 'od daty' przez nowy zapis historii; obniżony wymiar utrwalamy na " +
"Etat.Zaszeregowanie.Wymiar (Fraction). UWAGA: subrow Etat.ObnizenieEtatu jest w PEŁNI " +
"tylko-do-odczytu (brak publicznego settera i metody Save) — pełny zapis stanu obniżenia " +
"realizują workery platformy; w kodzie biznesowym ustawiamy docelowy wymiar na Zaszeregowaniu.")]
public void B5_ObnizenieWymiaru_UstawiaDocelowyWymiar_NaZaszeregowaniu()
{
Guid guid = Guid.Empty;
var okres = new FromTo(new Date(2026, 1, 1), Date.MaxValue);
var odDnia = new Date(2026, 7, 1);
var obnizonyWymiar = new Fraction(4, 5); // np. 4/5 etatu
InTransaction(() =>
{
var pracownik = UtworzPracownikaZEtatem("B5", okres);
var nowy = (PracHistoria)pracownik.Historia.Update(odDnia);
pracownik.Module.PracHistorie.AddRow(nowy);
// Subrow ObnizenieEtatu jest read-only (delegat odczytowy) — nie ustawiamy go bezpośrednio.
// Docelowy wymiar po obniżeniu utrwalamy na Etat.Zaszeregowanie.Wymiar (pole zapisywalne).
nowy.Etat.Zaszeregowanie.Wymiar = obnizonyWymiar;
guid = pracownik.Guid;
});
SaveDispose();
var pracownik2 = Get<Prac>(guid);
var nowyZapis = pracownik2.Historia.Cast<PracHistoria>().OrderBy(h => h.Aktualnosc.From).Last();
nowyZapis.Etat.Zaszeregowanie.Wymiar.Should().Be(obnizonyWymiar, "wymiar obniżony od daty obniżenia");
// Stary okres zachowuje pełny wymiar.
pracownik2.Historia.GetFirst().Etat.Zaszeregowanie.Wymiar.Should().Be(Fraction.One, "przed obniżeniem pełny etat");
}
[Test]
[Description("B5 (kontrakt): wszystkie property subrowa ObniżenieWymiaruEtatu (Wymiar, Stawka, " +
"RodzajStawki, TypStawki, Element, Kalendarz, Info) są tylko-do-odczytu — bezpośrednia " +
"modyfikacja nie jest możliwa przez publiczny kontrakt; stan obniżenia ustawiają workery.")]
public void B5_ObnizenieEtatu_JestTylkoDoOdczytu()
{
var t = typeof(ObniżenieWymiaruEtatu);
foreach (var nazwa in new[] { "Wymiar", "Stawka", "RodzajStawki", "TypStawki", "Element", "Kalendarz", "Info" })
{
var p = t.GetProperty(nazwa);
p.Should().NotBeNull($"subrow ObniżenieWymiaruEtatu ma property {nazwa}");
p!.CanWrite.Should().BeFalse($"property {nazwa} jest tylko-do-odczytu (zapisywana przez worker, nie wprost)");
}
}
// ============================== B6 — Podzielniki kosztów ==============================
[Test]
[Description("B6: trójpoziomowa struktura podzielnika — PodzielnikKosztow(pracownik)+PodzielKosztow.AddRow, " +
"Historia.Update(odDnia)+HistPodzielnikow.AddRow, ElementPodzielnika(historia)+ElemPodzielnikow.AddRow. " +
"ElementPodzialowy to referencja (Wydzial), ustawiamy Wspolczynnik; Procent jest kalkulowany.")]
public void B6_PodzielnikKosztow_TworzyHistorieIElementUdzialu()
{
var core = Session.GetCore();
// Cel rozdziału musi pochodzić z tabeli zgodnej z definicją podzielnika (domyślna definicja
// w Demo opiera się o tabelę CentraKosztow) — bierzemy pierwszy istniejący wpis (DYNAMICZNIE).
var celRozdzialu = core.CentraKosztow.Cast<IElementSlownika>().FirstOrDefault();
if (celRozdzialu == null)
Assert.Ignore("Baza Demo nie zawiera centrów kosztów (CentraKosztow) — brak celu rozdziału zgodnego z domyślną definicją podzielnika.");
Guid guidPrac = Guid.Empty;
var okres = new FromTo(new Date(2026, 1, 1), Date.MaxValue);
var odDnia = new Date(2026, 1, 1);
InTransaction(() =>
{
var pracownik = UtworzPracownikaZEtatem("B6", okres);
// Poziom 1: root podzielnika (źródło = pracownik) + AddRow do tabeli Core.
// Domyślna definicja (TabelaPodzielnika) decyduje, z jakiej tabeli mogą pochodzić elementy udziału.
var podzielnik = new PodzielnikKosztow(pracownik);
core.PodzielKosztow.AddRow(podzielnik);
podzielnik.Nazwa = "Rozdział kosztów";
// Poziom 2: zapis historii „od daty" + AddRow.
var historia = podzielnik.Historia.Update(odDnia);
core.HistPodzielnikow.AddRow(historia);
// Poziom 3: element udziału (cel + współczynnik) + AddRow.
var element = new ElementPodzielnika(historia);
core.ElemPodzielnikow.AddRow(element);
element.ElementPodzialowy = celRozdzialu;
element.Wspolczynnik = 100d; // Procent wyliczany z współczynników
guidPrac = pracownik.Guid;
});
SaveDispose();
var pracownik2 = Get<Prac>(guidPrac);
// Odczyt poprzez strukturę: pracownik (źródło) → podzielnik → historia → elementy udziału.
var podzielnik2 = Session.GetCore().PodzielKosztow.Cast<PodzielnikKosztow>()
.First(p => p.Zrodlo is Prac pr && pr.Guid == guidPrac);
var elementy = podzielnik2.Last.Elementy.Cast<ElementPodzielnika>().ToList();
elementy.Should().ContainSingle("dodaliśmy jeden element udziału");
elementy[0].ElementPodzialowy.Should().NotBeNull("cel rozdziału (centrum kosztów) jest referencją");
elementy[0].Wspolczynnik.Should().Be(100d);
}
// ============================== B7 — Aktualizacja wg definicji stanowiska (matrycy) ==============================
[Test]
[Description("B7: powiązanie etatu z definicją stanowiska (matrycą) 'od daty' — nowy zapis historii, " +
"Etat.Definicja = matryca (referencja z HR.DefStanowisk, pobrana DYNAMICZNIE), a wartości " +
"z matrycy (Stanowisko/wymiar/stawka) przenosimy JAWNIE na etat. Mark [Ignore] gdy brak matryc.")]
public void B7_DefinicjaStanowiska_PowiazanieIPrzeniesienieWartosci()
{
// Definicja stanowiska — matryca konfiguracyjna; pobieramy pierwszą istniejącą (DYNAMICZNIE).
var def = Session.GetHR().DefStanowisk.Cast<DefinicjaStanowiska>().FirstOrDefault();
if (def == null)
Assert.Ignore("Baza Demo nie zawiera definicji stanowisk (DefStanowisk) — nie ma matrycy do powiązania.");
Guid guid = Guid.Empty;
var okres = new FromTo(new Date(2026, 1, 1), Date.MaxValue);
var odDnia = new Date(2026, 7, 1);
InTransaction(() =>
{
var pracownik = UtworzPracownikaZEtatem("B7", okres);
var nowy = (PracHistoria)pracownik.Historia.Update(odDnia);
pracownik.Module.PracHistorie.AddRow(nowy);
var etat = nowy.Etat;
etat.Definicja = def; // powiązanie z definicją stanowiska (referencja)
// Przeniesienie wartości z matrycy zrobiłbyś jawnie (samo wskazanie Definicja nie nadpisuje pól).
if (!string.IsNullOrEmpty(def.Stanowisko))
etat.Stanowisko = def.Stanowisko;
guid = pracownik.Guid;
});
SaveDispose();
var nowyZapis = Get<Prac>(guid).Historia.Cast<PracHistoria>().OrderBy(h => h.Aktualnosc.From).Last();
nowyZapis.Etat.Definicja.Should().NotBeNull("etat powiązany z definicją stanowiska");
}
[Test]
[Description("B7 (kontrakt): definicje stanowisk pobieramy ze słownika konfiguracyjnego HR.DefStanowisk; " +
"klucz po nazwie to WgNazwa (a nie WgNazwy). Iteracja słownika zwraca rekordy DefinicjaStanowiska.")]
public void B7_DefStanowisk_JestSlownikiemKonfiguracyjnym()
{
var defs = Session.GetHR().DefStanowisk.Cast<DefinicjaStanowiska>().ToList();
// Słownik może być pusty w Demo — istotne, że iteracja działa i klucz WgNazwa istnieje.
Session.GetHR().DefStanowisk.WgNazwa.Should().NotBeNull("klucz po nazwie to WgNazwa");
defs.Should().OnlyContain(d => d != null);
}
}
@@ -0,0 +1,519 @@
using System;
using System.Linq;
using AwesomeAssertions;
using NUnit.Framework;
using Soneta.Business;
using Soneta.Kadry;
using Soneta.Place;
using Soneta.Przeszeregowania;
using Soneta.Types;
using Prac = Soneta.Kadry.Pracownik;
namespace Soneta.Skills.Test.KadryPlace.Pracownik;
/// <summary>
/// Rozdział C (część „potrąceniowa") — receptury C2C7 z dokumentu skilla <c>pracownik.md</c>:
/// <list type="bullet">
/// <item><b>C2</b> — potrącenia: w modelu płacowym potrącenie NIE ma osobnej klasy; to
/// <c>Soneta.Kadry.Dodatek</c> z definicją elementu o <c>Algorytm.Potracenie == true</c>;</item>
/// <item><b>C3</b> — akordy: <c>Soneta.Kadry.Akord</c> bez publicznego konstruktora — dodawane przez
/// worker <c>Pracownik.DodajAkordWorker</c>; zakończenie przez <c>ZakończAkordWorker</c>;</item>
/// <item><b>C4</b> — zajęcia komornicze: <c>new ZajęcieKomornicze(pracownik)</c>; anulowanie/przywracanie
/// przez workery <c>AnulujWorker</c>/<c>PrzywrócWorker</c>;</item>
/// <item><b>C5</b> — operacje seryjne na dodatkach (moduł <c>Soneta.Przeszeregowania</c>): worker
/// <c>NowyDodatekWorker</c> oraz dokument <c>Przeszeregowanie</c>;</item>
/// <item><b>C6</b> — świadczenia socjalne (ZFŚS): <c>new SwiadczSocjalne(pracownik)</c> + subrow
/// <c>Rozliczenie</c>;</item>
/// <item><b>C7</b> — pożyczki (KZP/ZFM): trzystopniowo <c>FundPozyczkowy(pracownik, definicja)</c> →
/// <c>Pozyczka(fundusz)</c> → harmonogram rat przez <c>UzgodnijRatyWorker</c>.</item>
/// </list>
/// <para>
/// Faktyczne kwoty/spłaty (<c>Splacono</c>, <c>Pozostało</c>, <c>Rozliczone</c>, stany rat) wyliczają się
/// dopiero przy NALICZENIU WYPŁATY (rozdział H). Te testy weryfikują UTWORZENIE i PARAMETRYZACJĘ obiektów
/// oraz publiczny model — skutki finansowe są poza zakresem (asercje na model albo <c>[Ignore]</c>).
/// </para>
/// <para>
/// Wszystko działa na bazie Demo (GoldStandard) z automatycznym rollbackiem po teście. Operujemy wyłącznie
/// na <b>publicznym kontrakcie</b> — jak dodatek programisty zewnętrznego bez dostępu do kodu źródłowego.
/// Definicje (DefElementow, DefinicjeAkordow, DefSwiadczSocjal, DefFundPozycz) pobieramy DYNAMICZNIE;
/// brak wpisu w Demo kończy test przez <c>Assert.Ignore</c>, nie przez błąd.
/// </para>
/// </summary>
[TestFixture]
public class RozdzialCrest_PotraceniaTest : PracownikTestBase
{
// Helper: świeży pracownik etatowy (Etat.Okres odblokowuje warunki; Wydzial+Stanowisko wymagane przy Save).
private Prac NowyPracownikEtatowy(string prefix, out Guid guid)
{
var pracownik = Session.AddRow(new PracownikFirmy());
pracownik.Kod = prefix + "_" + Guid.NewGuid().ToString("N").Substring(0, 6);
pracownik.Last.Nazwisko = "Testowy";
pracownik.Last.Imie = "Jan";
var etat = pracownik.Last.Etat;
etat.Okres = new FromTo(new Date(2026, 1, 1), Date.MaxValue); // PIERWSZE — odblokowuje Etat
etat.Wydzial = Kadry.Wydzialy.Firma;
etat.Stanowisko = "Specjalista";
guid = pracownik.Guid;
return pracownik;
}
// Helper: pierwsza definicja potrącenia możliwa do podpięcia pod Dodatek.
// WAŻNE: znacznik Algorytm.Potracenie nie wystarcza — element podpinany pod Dodatek MUSI mieć też
// RodzajZrodla == Dodatek (DodHistoria.Element odrzuca definicje o innym rodzaju źródła, np. "Alimenty"
// jako RodzajZrodla == ZajęcieKomornicze).
private DefinicjaElementu PierwszaDefinicjaPotraceniaJakoDodatek() =>
Place.DefElementow.Cast<DefinicjaElementu>()
.FirstOrDefault(d => d.RodzajZrodla == RodzajŹródłaWypłaty.Dodatek
&& d.Algorytm != null && d.Algorytm.Potracenie);
// Helper: pierwsza definicja elementu zajęcia komorniczego (RodzajZrodla == ZajęcieKomornicze).
private DefinicjaElementu PierwszaDefinicjaZajecia() =>
Place.DefElementow.Cast<DefinicjaElementu>()
.FirstOrDefault(d => d.RodzajZrodla == RodzajŹródłaWypłaty.ZajęcieKomornicze);
// ============================== C2 — Potrącenia (stałe / jednorazowe) ==============================
[Test]
[Description("C2: potrącenie NIE jest osobną klasą — to Dodatek z definicją elementu, w której " +
"Algorytm.Potracenie == true. Tworzymy przez new Dodatek(pracownik) + Kadry.Dodatki.AddRow. " +
"UWAGA (zweryfikowane): aby definicję podpiąć pod Dodatek, musi ona mieć RodzajZrodla == Dodatek " +
"ORAZ Algorytm.Potracenie == true — sam znacznik Algorytm.Potracenie nie wystarcza " +
"(DodHistoria.Element odrzuca definicje o innym rodzaju źródła, np. \"Alimenty\").")]
public void C2_Potracenie_ToDodatekZDefinicjaPotracajaca()
{
var defPotracenia = PierwszaDefinicjaPotraceniaJakoDodatek();
if (defPotracenia == null)
Assert.Ignore("Baza Demo nie zawiera definicji Dodatku o Algorytm.Potracenie == true.");
// Potrącenie-Dodatek: charakter minusowy daje algorytm, ale rodzaj źródła musi być Dodatek.
defPotracenia.Algorytm.Potracenie.Should().BeTrue("to definicja o charakterze potrącenia");
defPotracenia.RodzajZrodla.Should().Be(RodzajŹródłaWypłaty.Dodatek,
"potrącenie podpinane pod Dodatek musi mieć RodzajZrodla == Dodatek");
Guid guid = Guid.Empty;
var okres = new FromTo(new Date(2026, 1, 1), Date.MaxValue); // stałe
InTransaction(() =>
{
var pracownik = NowyPracownikEtatowy("C2", out guid);
// Mechanizm identyczny jak C1 (Dodatek + DodHistoria) — różni tylko dobór definicji.
var potracenie = new Dodatek(pracownik);
Kadry.Dodatki.AddRow(potracenie); // tworzy pierwszy zapis DodHistoria (Last)
var h = potracenie.Last;
h.Should().NotBeNull("AddRow tworzy pierwszy zapis DodHistoria");
h.Element = defPotracenia; // definicja o Algorytm.Potracenie == true (wymagana)
h.Okres = okres; // stałe potrącenie — okres otwarty
});
SaveDispose();
var pracownik2 = Get<Prac>(guid);
var dodatki = pracownik2.Dodatki.Cast<Dodatek>().ToList();
dodatki.Should().ContainSingle("dodaliśmy jedno potrącenie (Dodatek) do świeżego pracownika");
dodatki[0].Last.Element.Should().NotBeNull("Element (definicja potrącenia) jest wymagany");
dodatki[0].Last.Element.Algorytm.Potracenie.Should().BeTrue(
"trwale zapisana definicja zachowuje charakter potrącenia");
}
[Test]
[Description("C2 (jednorazowe): potrącenie jednorazowe to Dodatek z OKRESEM zawężonym do jednego " +
"miesiąca rozliczeniowego — naliczy się tylko w wypłatach z tego miesiąca. " +
"Okres ustawiamy przez FromTo.Month(YearMonth).")]
public void C2_PotracenieJednorazowe_OkresZawezonyDoMiesiaca()
{
var defPotracenia = PierwszaDefinicjaPotraceniaJakoDodatek();
if (defPotracenia == null)
Assert.Ignore("Baza Demo nie zawiera definicji Dodatku o Algorytm.Potracenie == true.");
Guid guid = Guid.Empty;
var okresMiesiaca = FromTo.Month(2026, 3); // jeden miesiąc rozliczeniowy (marzec 2026)
InTransaction(() =>
{
var pracownik = NowyPracownikEtatowy("C2j", out guid);
var potracenie = new Dodatek(pracownik);
Kadry.Dodatki.AddRow(potracenie);
potracenie.Last.Element = defPotracenia;
potracenie.Last.Okres = okresMiesiaca; // jednorazowe — tylko marzec 2026
});
SaveDispose();
var h = Get<Prac>(guid).Dodatki.Cast<Dodatek>().Single().Last;
// Okres zawężony do jednego miesiąca — granice pokrywają się z miesiącem rozliczeniowym.
h.Okres.From.Should().Be(okresMiesiaca.From, "potrącenie jednorazowe obejmuje tylko jeden miesiąc");
h.Okres.To.Should().Be(okresMiesiaca.To);
}
// ============================== C3 — Akordy ==============================
[Test]
[Description("C3: Akord NIE ma publicznego konstruktora — kanoniczną ścieżką dodania jest worker " +
"Pracownik.DodajAkordWorker (parametryzowany przez Params(context) + Pracownicy[]). " +
"Definicję akordu pobieramy ze słownika DefinicjeAkordow (klucz WgNazwa). Odczyt z pracownik.Akordy.")]
public void C3_Akord_DodawanyWorkerem_ZDefinicjiSlownika()
{
var defAkordu = Kadry.DefinicjeAkordow.Cast<DefinicjaAkordu>().FirstOrDefault();
if (defAkordu == null)
Assert.Ignore("Baza Demo nie zawiera żadnej definicji akordu (DefinicjeAkordow).");
// Akord NIE ma publicznego ctora — potwierdzenie kanonicznej ścieżki (worker zamiast `new`).
typeof(Akord).GetConstructors()
.Should().NotContain(c => c.GetParameters().Length == 1
&& c.GetParameters()[0].ParameterType == typeof(Prac),
"Akord nie ma publicznego ctora new Akord(pracownik) — dodajemy go workerem");
Guid guid = Guid.Empty;
InTransaction(() => NowyPracownikEtatowy("C3", out guid));
SaveDispose();
// Worker akordu działa „jak z UI" (Params wymaga Context) — używamy InUITransaction + CommitUI.
bool dodano = false;
InUITransaction(() =>
{
var pracownik = Get<Prac>(guid);
var context = Login.CreateEmptyContext().Clone(Session);
var par = new Prac.DodajAkordWorker.Params(context)
{
Definicja = defAkordu,
OdDnia = new Date(2026, 1, 1),
DoDnia = new Date(2026, 12, 31),
};
// Worker akordu ma ctor (Session); parametry przez property Pars/Pracownicy.
var worker = new Prac.DodajAkordWorker(Session) { Pars = par, Pracownicy = new[] { pracownik } };
worker.DodajAkord();
dodano = true;
});
if (!dodano)
Assert.Ignore("DodajAkordWorker nie wykonał się w headless host (zależność od kontekstu UI).");
SaveDispose();
// Odczyt akordów pracownika (child SubTable). Akord jest historyczny — bieżący zapis przez Last.
var akordy = Get<Prac>(guid).Akordy.Cast<Akord>().ToList();
akordy.Should().ContainSingle("worker dodał jeden akord");
akordy[0].Definicja.Should().NotBeNull("akord wiąże definicję ze słownika DefinicjeAkordow");
akordy[0].Last.Should().NotBeNull("akord ma bieżący zapis historii AkordHistoria");
}
// ============================== C4 — Zajęcia wynagrodzenia (komornicze/alimentacyjne) ==============================
[Test]
[Description("C4: zajęcie komornicze to JEDNA klasa ZajęcieKomornicze (alimentacyjne vs niealimentacyjne " +
"rozstrzyga definicja elementu i parametry zapisu historii, nie osobny typ ani pole Priorytet — " +
"którego na ZajęcieKomornicze NIE ma). Ctor publiczny new ZajęcieKomornicze(pracownik) + " +
"Kadry.ZajKomornicze.AddRow. Element (potrącenie zajęcia) jest wymagany. " +
"Rodzaj to enum RodzajeZajęciaWynagrodzenia { Kwota, KwotaMiesięczna }.")]
public void C4_ZajecieKomornicze_TworzoneZParametrami()
{
// Element zajęcia — definicja o RodzajZrodla == ZajęcieKomornicze (dedykowany rodzaj źródła).
var elementZajecia = PierwszaDefinicjaZajecia();
if (elementZajecia == null)
Assert.Ignore("Baza Demo nie zawiera definicji elementu o RodzajZrodla == ZajęcieKomornicze.");
Guid guid = Guid.Empty;
InTransaction(() =>
{
var pracownik = NowyPracownikEtatowy("C4", out guid);
var zajecie = new ZajęcieKomornicze(pracownik); // ctor PUBLICZNY
Kadry.ZajKomornicze.AddRow(zajecie);
zajecie.Rodzaj = RodzajeZajęciaWynagrodzenia.KwotaMiesięczna;
zajecie.Element = elementZajecia; // element płacowy potrącenia (wymagany)
zajecie.NumerSprawy = "KM 123/2026";
zajecie.Data = new Date(2026, 1, 1);
});
SaveDispose();
var zaj = Get<Prac>(guid).ZajęciaKomornicze.Cast<ZajęcieKomornicze>().Single();
zaj.NumerSprawy.Should().Be("KM 123/2026");
zaj.Rodzaj.Should().Be(RodzajeZajęciaWynagrodzenia.KwotaMiesięczna);
zaj.Element.Should().NotBeNull("Element (definicja potrącenia zajęcia) jest wymagany");
// Skutki finansowe (Splacono/Pozostało) wyliczają się przy naliczeniu wypłaty — po samym dodaniu
// pozostają niewyliczone (puste). Nie asercjonujemy na nie tu (zakres: utworzenie/parametryzacja).
zaj.Anulowane.Should().BeFalse("nowo dodane zajęcie nie jest anulowane");
zaj.SplataZakonczona.Should().BeFalse("nowo dodane zajęcie nie jest spłacone");
}
[Test]
[Description("C4 (anulowanie): zajęcie anuluje się WORKEREM ZajęcieKomornicze.AnulujWorker (nie ręcznym " +
"ustawianiem flagi Anulowane) — worker dba o storna i spójność rozliczenia. Tu weryfikujemy " +
"tylko publiczny model anulowania (utworzenie + uruchomienie workera).")]
public void C4_ZajecieKomornicze_AnulujWorker()
{
var elementZajecia = PierwszaDefinicjaZajecia();
if (elementZajecia == null)
Assert.Ignore("Baza Demo nie zawiera definicji elementu o RodzajZrodla == ZajęcieKomornicze.");
Guid guid = Guid.Empty;
InTransaction(() =>
{
var pracownik = NowyPracownikEtatowy("C4a", out guid);
var zajecie = new ZajęcieKomornicze(pracownik);
Kadry.ZajKomornicze.AddRow(zajecie);
zajecie.Element = elementZajecia;
zajecie.NumerSprawy = "KM 999/2026";
zajecie.Data = new Date(2026, 1, 1);
});
SaveDispose();
bool anulowano = false;
InUITransaction(() =>
{
var zaj = Get<Prac>(guid).ZajęciaKomornicze.Cast<ZajęcieKomornicze>().Single();
// Worker przez parameterless ctor + property setter (Zajęcie), nie przez ctor parametryczny.
var worker = new ZajęcieKomornicze.AnulujWorker { Zajęcie = zaj };
worker.Anuluj();
anulowano = true;
});
if (!anulowano)
Assert.Ignore("AnulujWorker nie wykonał się w headless host (zależność od kontekstu UI).");
SaveDispose();
Get<Prac>(guid).ZajęciaKomornicze.Cast<ZajęcieKomornicze>().Single()
.Anulowane.Should().BeTrue("worker AnulujWorker oznacza zajęcie jako anulowane");
}
// ============================== C5 — Operacje seryjne na dodatkach (moduł Przeszeregowania) ==============================
[Test]
[Description("C5: seryjne nadanie dodatku grupie realizuje moduł Soneta.Przeszeregowania — worker " +
"NowyDodatekWorker (Params(context) { Definicja, Podstawa, Procent } + Pracownicy[]). " +
"Worker przyjmuje TABLICĘ pracowników, więc nadaje się do operacji grupowej. " +
"Tu weryfikujemy utworzenie/parametryzację — efekt to nowy Dodatek u pracownika.")]
[Ignore("NowyDodatekWorker (moduł Przeszeregowania) rzuca NullReferenceException w headless host " +
"testowym (Przeszeregowania/NowyDodatek.cs:94) — operacja seryjna zależy od stanu operatora/" +
"kontekstu UI nieobecnego w bazie Demo. Test dokumentuje publiczny model workera seryjnego.")]
public void C5_OperacjaSeryjna_NowyDodatekWorker_GrupaPracownikow()
{
// Definicja dodatku (RodzajZrodla == Dodatek) — np. Premia z Demo.
var def = Place.DefElementow.WgNazwy["Premia"] as DefinicjaElementu;
if (def == null)
Assert.Ignore("Baza Demo nie zawiera definicji dodatku \"Premia\".");
Guid g1 = Guid.Empty, g2 = Guid.Empty;
InTransaction(() =>
{
NowyPracownikEtatowy("C5a", out g1);
NowyPracownikEtatowy("C5b", out g2);
});
SaveDispose();
bool wykonano = false;
InUITransaction(() =>
{
var grupa = new[] { Get<Prac>(g1), Get<Prac>(g2) };
var context = Login.CreateEmptyContext().Clone(Session);
var par = new NowyDodatekWorker.Params(context)
{
Definicja = def,
Podstawa = (Currency)300m,
};
var worker = new NowyDodatekWorker { Pars = par, Pracownicy = grupa };
worker.NowyDodatek();
wykonano = true;
});
if (!wykonano)
Assert.Ignore("NowyDodatekWorker (moduł Przeszeregowania) nie wykonał się w headless host.");
SaveDispose();
// Po wykonaniu operacji seryjnej każdy pracownik z grupy ma nowy dodatek z tej definicji.
// Materializujemy do listy i sprawdzamy LINQ Any (poza drzewem wyrażeń — można użyć ?. i funkcji).
static bool MaPremie(Dodatek d) => d.Last?.Element?.Nazwa == "Premia";
Get<Prac>(g1).Dodatki.Cast<Dodatek>().Any(MaPremie).Should().BeTrue(
"operacja seryjna nadała dodatek pierwszemu pracownikowi");
Get<Prac>(g2).Dodatki.Cast<Dodatek>().Any(MaPremie).Should().BeTrue(
"operacja seryjna nadała dodatek drugiemu pracownikowi");
}
[Test]
[Description("C5 (dokument Przeszeregowanie): dokument zbiorczy Soneta.Przeszeregowania.Przeszeregowanie " +
"ma publiczny ctor + AddRow (kolekcja nie ma AddNew). Jest PLANEM — NIE zmienia danych dopóki " +
"nie zostanie wykonany (WykonajWorker). Tu weryfikujemy utworzenie i parametryzację nagłówka " +
"(Data, Nazwa). Kolekcja Pracownicy jest zarządzana przez przepływ workera, nie prostym Add.")]
public void C5_DokumentPrzeszeregowania_JestPlanemDoWykonania()
{
Guid guid = Guid.Empty;
InTransaction(() =>
{
// Dokument tworzymy przez new + AddRow (kolekcja nie ma AddNew — to standardowy GuidedRow root).
var doc = new Przeszeregowanie();
Session.GetPrzeszeregowania().Przeszeregowania.AddRow(doc);
doc.Data = new Date(2026, 4, 1);
doc.Nazwa = "Przeszeregowanie testowe";
// Dokument to PLAN — pozycje (Elementy) i materializacja danych następują dopiero przy WykonajWorker.
doc.Nazwa.Should().Be("Przeszeregowanie testowe");
doc.Data.Should().Be(new Date(2026, 4, 1));
});
// Bez Save — to wyłącznie weryfikacja utworzenia/parametryzacji planu (rollback po teście).
}
// ============================== C6 — Świadczenia socjalne (ZFŚS) ==============================
[Test]
[Description("C6: świadczenie socjalne to Soneta.Kadry.SwiadczSocjalne (ctor publiczny new SwiadczSocjalne" +
"(pracownik) + Kadry.SwiadczeniaSoc.AddRow). Definicję pobieramy ze słownika DefSwiadczSocjal " +
"(klucz WgNazwy); dane rozliczeniowe (Element, Kwota, Okres) ustawiamy na subrowie Rozliczenie. " +
"Faktyczne rozliczenie (Rozliczone == true) następuje przy naliczeniu wypłaty.")]
public void C6_SwiadczenieSocjalne_TworzoneZRozliczeniem()
{
var defSwiadcz = Kadry.DefSwiadczSocjal.Cast<DefinicjaŚwiadczeniaSocjalnego>().FirstOrDefault();
if (defSwiadcz == null)
Assert.Ignore("Baza Demo nie zawiera definicji świadczenia socjalnego (DefSwiadczSocjal).");
// Element rozliczenia — preferuj domyślny z definicji, w razie braku dowolny element płacowy.
var element = defSwiadcz.Element
?? Place.DefElementow.Cast<DefinicjaElementu>().FirstOrDefault();
if (element == null)
Assert.Ignore("Brak elementu płacowego do rozliczenia świadczenia socjalnego.");
Guid guid = Guid.Empty;
var okres = FromTo.Month(2026, 6);
InTransaction(() =>
{
var pracownik = NowyPracownikEtatowy("C6", out guid);
var sw = new SwiadczSocjalne(pracownik); // ctor PUBLICZNY
Kadry.SwiadczeniaSoc.AddRow(sw);
sw.Definicja = defSwiadcz;
sw.Data = new Date(2026, 6, 1);
// Dane rozliczeniowe — na SUBROWIE Rozliczenie (nadpisują domyślne z definicji).
sw.Rozliczenie.Element = element;
sw.Rozliczenie.Kwota = (Currency)1000m;
sw.Rozliczenie.Okres = okres;
});
SaveDispose();
var s = Get<Prac>(guid).Swiadczenia.Cast<SwiadczSocjalne>().Single();
s.Definicja.Should().NotBeNull("świadczenie wiąże definicję ze słownika DefSwiadczSocjal");
s.Rozliczenie.Kwota.Should().Be((Currency)1000m, "kwota świadczenia z subrowa Rozliczenie");
s.Rozliczenie.Element.Should().NotBeNull("element płacowy rozliczenia");
s.Rozliczenie.Rozliczone.Should().BeFalse("rozliczenie następuje dopiero przy naliczeniu wypłaty");
}
// ============================== C7 — Pożyczki (KZP / ZFM) ==============================
[Test]
[Description("C7: ścieżka trzystopniowa FundPozyczkowy(pracownik, definicja) → Pozyczka(fundusz) → " +
"harmonogram rat. Pożyczki NIE da się utworzyć bez funduszu (ctor wymaga FundPozyczkowy). " +
"Definicję funduszu pobieramy ze słownika DefFundPozycz (WgNazwy). Element (wypłata) i " +
"ElementRaty (potrącenie raty) to RÓŻNE definicje. Harmonogram generuje worker UzgodnijRatyWorker.")]
public void C7_Pozyczka_FunduszPozyczkaHarmonogram()
{
var defFunduszu = Kadry.DefFundPozycz.Cast<DefinicjaFunduszuPozyczkowego>().FirstOrDefault();
if (defFunduszu == null)
Assert.Ignore("Baza Demo nie zawiera definicji funduszu pożyczkowego (DefFundPozycz).");
// Element wypłaty i element raty — dwie różne definicje płacowe (dowolne dostępne).
var elementy = Place.DefElementow.Cast<DefinicjaElementu>().Take(2).ToList();
if (elementy.Count < 2)
Assert.Ignore("Baza Demo nie zawiera co najmniej dwóch definicji elementów (wypłata + rata).");
var elWyplata = elementy[0];
var elRata = elementy[1];
Guid guidPrac = Guid.Empty, guidFundusz = Guid.Empty, guidPozyczka = Guid.Empty;
InTransaction(() =>
{
var pracownik = NowyPracownikEtatowy("C7", out guidPrac);
// 1) Członkostwo w funduszu — ctor wymaga (pracownik, definicja).
var fundusz = new FundPozyczkowy(pracownik, defFunduszu);
Kadry.FundPozyczkowe.AddRow(fundusz);
fundusz.Okres = new FromTo(new Date(2026, 1, 1), Date.MaxValue);
guidFundusz = fundusz.Guid;
// 2) Pożyczka w ramach funduszu — ctor wymaga FundPozyczkowy.
var pozyczka = new Pozyczka(fundusz);
Kadry.Pozyczki.AddRow(pozyczka);
pozyczka.Data = new Date(2026, 1, 10);
pozyczka.Kwota = (Currency)12000m;
pozyczka.Element = elWyplata; // element WYPŁATY pożyczki
pozyczka.ElementRaty = elRata; // element POTRĄCENIA raty (inny niż wypłata)
pozyczka.IloscRat = 12;
pozyczka.SplatyOd = new YearMonth(2026, 2);
guidPozyczka = pozyczka.Guid;
});
SaveDispose();
var pozyczka2 = Get<Pozyczka>(guidPozyczka);
pozyczka2.Should().NotBeNull("pożyczka utrwalona w tabeli Pozyczki");
pozyczka2.Fundusz.Should().NotBeNull("pożyczka należy do funduszu (ctor wymaga FundPozyczkowy)");
pozyczka2.Kwota.Should().Be((Currency)12000m);
pozyczka2.IloscRat.Should().Be(12);
pozyczka2.Element.Should().NotBeNull("element wypłaty pożyczki");
pozyczka2.ElementRaty.Should().NotBeNull("element potrącenia raty");
// Fundusz widoczny przez child pracownika.
Get<Prac>(guidPrac).FunduszePozyczkowe.Cast<FundPozyczkowy>()
.Should().ContainSingle("pracownik jest członkiem jednego funduszu");
}
[Test]
[Description("C7 (harmonogram): harmonogram rat generuje worker Pozyczka.UzgodnijRatyWorker " +
"(Params(context){ UzgodnijRaty, PrzeliczRaty }, property Pożyczka) albo metoda " +
"pozyczka.UpdatePozyczka() — NIE ręczne dodawanie RataPozyczki. Worker rozkłada kapitał/odsetki. " +
"Faktyczne potrącenia rat (Stan/Splacono) aktualizują się dopiero przy naliczeniu wypłaty.")]
public void C7_Pozyczka_HarmonogramRatPrzezWorker()
{
var defFunduszu = Kadry.DefFundPozycz.Cast<DefinicjaFunduszuPozyczkowego>().FirstOrDefault();
if (defFunduszu == null)
Assert.Ignore("Baza Demo nie zawiera definicji funduszu pożyczkowego (DefFundPozycz).");
var elementy = Place.DefElementow.Cast<DefinicjaElementu>().Take(2).ToList();
if (elementy.Count < 2)
Assert.Ignore("Baza Demo nie zawiera co najmniej dwóch definicji elementów.");
Guid guidPozyczka = Guid.Empty;
InTransaction(() =>
{
var pracownik = NowyPracownikEtatowy("C7h", out _);
var fundusz = new FundPozyczkowy(pracownik, defFunduszu);
Kadry.FundPozyczkowe.AddRow(fundusz);
fundusz.Okres = new FromTo(new Date(2026, 1, 1), Date.MaxValue);
var pozyczka = new Pozyczka(fundusz);
Kadry.Pozyczki.AddRow(pozyczka);
pozyczka.Data = new Date(2026, 1, 10);
pozyczka.Kwota = (Currency)12000m;
pozyczka.Element = elementy[0];
pozyczka.ElementRaty = elementy[1];
pozyczka.IloscRat = 12;
pozyczka.SplatyOd = new YearMonth(2026, 2);
guidPozyczka = pozyczka.Guid;
});
SaveDispose();
bool uzgodniono = false;
InUITransaction(() =>
{
var pozyczka = Get<Pozyczka>(guidPozyczka);
var context = Login.CreateEmptyContext().Clone(Session);
// PrzeliczRaty jest tylko-do-odczytu (ustawiane wewnętrznie) — parametryzujemy tylko UzgodnijRaty.
var par = new Pozyczka.UzgodnijRatyWorker.Params(context)
{
UzgodnijRaty = true,
};
var worker = new Pozyczka.UzgodnijRatyWorker { Pars = par, Pożyczka = pozyczka };
worker.UzgodnijRaty();
uzgodniono = true;
});
if (!uzgodniono)
Assert.Ignore("UzgodnijRatyWorker nie wykonał się w headless host (zależność od kontekstu UI).");
SaveDispose();
// Po uzgodnieniu harmonogram rat istnieje (worker rozłożył kapitał/odsetki wg IloscRat/SplatyOd).
var raty = Get<Pozyczka>(guidPozyczka).Raty.Cast<RataPozyczki>().ToList();
raty.Should().NotBeEmpty("UzgodnijRatyWorker buduje harmonogram rat");
raty.Should().OnlyContain(r => r.Stan == StanSpłat.NieSpłacona,
"świeżo wygenerowane raty są niespłacone — spłata nalicza się przy wypłacie");
}
}
@@ -0,0 +1,360 @@
using System.Linq;
using AwesomeAssertions;
using NUnit.Framework;
using Soneta.Business;
using Soneta.Kadry;
using Soneta.Kalend;
using Soneta.Types;
using Prac = Soneta.Kadry.Pracownik;
namespace Soneta.Skills.Test.KadryPlace.Pracownik;
/// <summary>
/// Rozdział D — „Nieobecności i czas pracy" (receptury D1, D2, D7).
/// <para>
/// Testy są <b>wykonywalną dokumentacją</b> publicznego kontraktu platformy Soneta dla obsługi
/// nieobecności pracownika oraz limitów urlopowych. Każda metoda mapuje się 1:1 do receptury
/// z dokumentu skilla <c>pracownik.md</c>:
/// <list type="bullet">
/// <item><b>D1</b> — wprowadzanie nieobecności (<c>NieobecnośćPracownika</c>, kolekcja <c>Nieobecnosci</c>);</item>
/// <item><b>D2</b> — korygowanie nieobecności (zmiana okresu/typu, rekord <c>KorektaNieobecności</c>);</item>
/// <item><b>D7</b> — analiza limitów urlopowych (naliczenie <c>NaliczanieLimitow.DodajLimit()</c> + odczyt z <c>pracownik.Limity</c>).</item>
/// </list>
/// </para>
/// <para>
/// Wszystko działa na bazie Demo (GoldStandard) z automatycznym rollbackiem po teście. Operujemy
/// wyłącznie na <b>publicznym kontrakcie</b> — tak jak dodatek programisty zewnętrznego bez dostępu
/// do kodu źródłowego aplikacji.
/// </para>
/// <para>
/// <b>Uwaga praktyczna (odkryta w trakcie testów):</b> ustawienie <c>Okres</c> na nieobecności typu
/// „urlop wypoczynkowy" wyzwala synchroniczne przeliczenie limitu i — gdy pracownik nie ma jeszcze
/// naliczonego limitu na ten dzień — rzuca <c>LimitNotFoundException</c>. Dlatego dla scenariuszy D1/D2
/// (czysta obsługa rekordu nieobecności) używamy typu nieobecności <b>niewymagającego limitu</b>
/// („Urlop bezpłatny (art 174 kp)"), a urlop wypoczynkowy testujemy dopiero po naliczeniu limitu (D7).
/// </para>
/// </summary>
[TestFixture]
public class RozdzialD_NieobecnosciTest : PracownikTestBase
{
// Typ nieobecności NIEwymagający naliczonego limitu — bezpieczny do scenariuszy obsługi rekordu.
private const string DefBezplatny = "Urlop bezpłatny (art 174 kp)";
private const string DefBezplatny2 = "Urlop bezpłatny (kod 350)";
private const string DefUrlopWyp = "Urlop wypoczynkowy";
// ============================== D1 — Wprowadzanie nieobecności ==============================
[Test]
[Description("D1: Nieobecnosc jest typem ABSTRAKCYJNYM; konkretnym typem nieobecności pracownika " +
"jest NieobecnośćPracownika (dziedziczy po Nieobecnosc) z ctorem (Pracownik).")]
public void D1_NieobecnoscPracownika_JestKonkretnymTypemNieobecnosci()
{
// Dokumentujemy regułę z receptury: new Nieobecnosc() jest niemożliwe (typ abstrakcyjny),
// więc używamy NieobecnośćPracownika. Sprawdzamy relację dziedziczenia bez instancjonowania abstrakta.
typeof(Nieobecnosc).IsAbstract.Should().BeTrue("Nieobecnosc jest klasą abstrakcyjną");
typeof(Nieobecnosc).IsAssignableFrom(typeof(NieobecnośćPracownika))
.Should().BeTrue("NieobecnośćPracownika jest konkretnym typem nieobecności pracownika");
}
[Test]
[Description("D1: nieobecność tworzymy NieobecnośćPracownika(pracownik) (ctor wiąże z pracownikiem) " +
"+ AddRow; ustawiamy Definicja (słownik DefNieobecnosci) i Okres (FromTo); zapis przez Save().")]
public void D1_WprowadzenieNieobecnosci_TworzyRekordWKolekcjiNieobecnosci()
{
var pracownik = Pracownik(Pracownik_.Andrzejewski);
pracownik.Should().NotBeNull("pracownik z Demo istnieje");
var def = Kalend.DefNieobecnosci.WgNazwy[DefBezplatny] as DefinicjaNieobecnosci;
def.Should().NotBeNull($"definicja '{DefBezplatny}' istnieje w bazie Demo");
var okres = new FromTo(new Date(2026, 7, 6), new Date(2026, 7, 10));
InTransaction(() =>
{
// Typ konkretny; ctor NieobecnośćPracownika(pracownik) wiąże nieobecność z pracownikiem.
var nieobecnosc = Session.AddRow(new NieobecnośćPracownika(pracownik));
nieobecnosc.Definicja = def; // rodzaj nieobecności (wymagany)
nieobecnosc.Okres = okres; // zakres dat „oddo"
// Relacja Pracownik jest ustawiana przez ctor i jest tylko do odczytu.
nieobecnosc.Pracownik.Should().BeSameAs(pracownik, "ctor wiąże nieobecność z pracownikiem");
});
SaveDispose();
// Odczyt: nieobecność przecinająca lipiec 2026 została zapisana w kolekcji pracownika.
var lipiec = new FromTo(new Date(2026, 7, 1), new Date(2026, 7, 31));
var pracownik2 = Pracownik(Pracownik_.Andrzejewski);
var nieobecnosci = pracownik2.Nieobecnosci.GetIntersectedRows(lipiec).Cast<Nieobecnosc>().ToList();
nieobecnosci.Should().ContainSingle("dodaliśmy jedną nieobecność w lipcu 2026")
.Which.Definicja.Nazwa.Should().Be(DefBezplatny);
var zapisana = nieobecnosci[0];
zapisana.Okres.From.Should().Be(okres.From);
zapisana.Okres.To.Should().Be(okres.To);
}
[Test]
[Description("D1 (odczyt): pracownik.Nieobecnosci.GetIntersectedRows(FromTo) zwraca nieobecności " +
"przecinające zadany przedział; poza przedziałem nieobecność nie jest zwracana.")]
public void D1_GetIntersectedRows_FiltrujePoPrzecieciuOkresu()
{
var pracownik = Pracownik(Pracownik_.Bednarek);
var def = Kalend.DefNieobecnosci.WgNazwy[DefBezplatny] as DefinicjaNieobecnosci;
InTransaction(() =>
{
var n = Session.AddRow(new NieobecnośćPracownika(pracownik));
n.Definicja = def;
n.Okres = new FromTo(new Date(2026, 8, 3), new Date(2026, 8, 7)); // sierpień
});
SaveDispose();
var pracownik2 = Pracownik(Pracownik_.Bednarek);
// Przedział przecinający się z nieobecnością → znajduje rekord.
var sierpien = new FromTo(new Date(2026, 8, 1), new Date(2026, 8, 31));
pracownik2.Nieobecnosci.GetIntersectedRows(sierpien).Cast<Nieobecnosc>()
.Should().ContainSingle("nieobecność przecina sierpień 2026");
// Przedział rozłączny (wrzesień) → brak rekordu.
var wrzesien = new FromTo(new Date(2026, 9, 1), new Date(2026, 9, 30));
pracownik2.Nieobecnosci.GetIntersectedRows(wrzesien).Cast<Nieobecnosc>()
.Should().BeEmpty("nieobecność nie przecina się z wrześniem 2026");
}
// ============================== D2 — Korygowanie nieobecności ==============================
[Test]
[Description("D2 (wariant A): okres nieobecności jest polem zapisywalnym — na istniejącym rekordzie " +
"można zmienić Okres (np. wydłużyć nieobecność) i utrwalić zmianę przez Save().")]
public void D2_ModyfikacjaOkresu_ZmianaIstniejacegoRekordu()
{
var pracownik = Pracownik(Pracownik_.Bujak);
var def = Kalend.DefNieobecnosci.WgNazwy[DefBezplatny] as DefinicjaNieobecnosci;
// Najpierw wprowadzamy nieobecność (stan „przed korektą").
var okresStary = new FromTo(new Date(2026, 3, 2), new Date(2026, 3, 6));
InTransaction(() =>
{
var n = Session.AddRow(new NieobecnośćPracownika(pracownik));
n.Definicja = def;
n.Okres = okresStary;
});
SaveDispose();
// Korekta wariant A: odszukujemy istniejący rekord i wydłużamy jego okres.
var okresNowy = new FromTo(new Date(2026, 3, 2), new Date(2026, 3, 11));
InTransaction(() =>
{
var pracownikE = Pracownik(Pracownik_.Bujak);
var nieobecnosc = (Nieobecnosc)pracownikE.Nieobecnosci.GetIntersectedRows(okresStary)[0];
nieobecnosc.Okres = okresNowy; // Okres jest polem zapisywalnym
});
SaveDispose();
// Po korekcie istnieje jeden rekord z wydłużonym okresem.
var pracownik2 = Pracownik(Pracownik_.Bujak);
var marzec = new FromTo(new Date(2026, 3, 1), new Date(2026, 3, 31));
var wynik = pracownik2.Nieobecnosci.GetIntersectedRows(marzec).Cast<Nieobecnosc>().ToList();
wynik.Should().ContainSingle("modyfikacja okresu nie tworzy nowego rekordu");
wynik[0].Okres.To.Should().Be(okresNowy.To, "okres został wydłużony do 2026-03-11");
}
[Test]
[Description("D2 (wariant A): zmiana typu nieobecności — pole Definicja jest zapisywalne, " +
"można podmienić rodzaj nieobecności na istniejącym rekordzie.")]
public void D2_ZmianaDefinicji_PodmieniaTypNieobecnosci()
{
var pracownik = Pracownik(Pracownik_.Strzelecki);
var def1 = Kalend.DefNieobecnosci.WgNazwy[DefBezplatny] as DefinicjaNieobecnosci;
var def2 = Kalend.DefNieobecnosci.WgNazwy[DefBezplatny2] as DefinicjaNieobecnosci;
def2.Should().NotBeNull($"definicja '{DefBezplatny2}' istnieje w bazie Demo");
var okres = new FromTo(new Date(2026, 4, 6), new Date(2026, 4, 10));
InTransaction(() =>
{
var n = Session.AddRow(new NieobecnośćPracownika(pracownik));
n.Definicja = def1;
n.Okres = okres;
});
SaveDispose();
// Korekta typu: podmiana definicji na inny rodzaj nieobecności bezpłatnej.
InTransaction(() =>
{
var pracownikE = Pracownik(Pracownik_.Strzelecki);
var def2e = Kalend.DefNieobecnosci.WgNazwy[DefBezplatny2] as DefinicjaNieobecnosci;
var nieobecnosc = (Nieobecnosc)pracownikE.Nieobecnosci.GetIntersectedRows(okres)[0];
nieobecnosc.Definicja = def2e;
});
SaveDispose();
var pracownik2 = Pracownik(Pracownik_.Strzelecki);
var wynik = pracownik2.Nieobecnosci.GetIntersectedRows(okres).Cast<Nieobecnosc>().Single();
wynik.Definicja.Nazwa.Should().Be(DefBezplatny2, "typ nieobecności został zmieniony");
}
[Test]
[Description("D2 (wariant C): korektę dodajemy konstruktorem KorektaNieobecności(nieobecność) — " +
"rekord korygujący o okresie ZAWARTYM w okresie korygowanym; po zapisie nieobecność " +
"pierwotna zostaje oznaczona flagą Korygowana=true.")]
public void D2_KorektaNieobecnosci_OznaczaNieobecnoscJakoKorygowana()
{
var pracownik = Pracownik(Pracownik_.Andrzejewski);
var def = Kalend.DefNieobecnosci.WgNazwy[DefBezplatny] as DefinicjaNieobecnosci;
var okresPierwotny = new FromTo(new Date(2026, 5, 4), new Date(2026, 5, 8));
// Stan „przed korektą": nieobecność nie jest korygowana.
InTransaction(() =>
{
var n = Session.AddRow(new NieobecnośćPracownika(pracownik));
n.Definicja = def;
n.Okres = okresPierwotny;
n.Korygowana.Should().BeFalse("świeża nieobecność nie jest jeszcze korygowana");
});
SaveDispose();
// Wariant C: rekord korekty dotyczy NieobecnośćPracownika (ctor przyjmuje korygowaną nieobecność).
// UWAGA: okres korekty jest OGRANICZONY do okresu nieobecności korygowanej (KorygowanyOkresException
// przy próbie wyjścia poza), dlatego okres korekty musi być PODZBIOREM okresu pierwotnego.
var okresKorekty = new FromTo(new Date(2026, 5, 4), new Date(2026, 5, 6));
InTransaction(() =>
{
var pracownikE = Pracownik(Pracownik_.Andrzejewski);
var nPrac = (NieobecnośćPracownika)pracownikE.Nieobecnosci.GetIntersectedRows(okresPierwotny)[0];
var korekta = Session.AddRow(new KorektaNieobecności(nPrac));
korekta.Definicja = nPrac.Definicja;
korekta.Okres = okresKorekty;
// KorektaNieobecności dziedziczy po Nieobecnosc.
(korekta is Nieobecnosc).Should().BeTrue("KorektaNieobecności jest rodzajem Nieobecnosc");
});
SaveDispose();
// Po korekcie nieobecność pierwotna istnieje i jest oznaczona jako korygowana.
// (Dla nieobecności bez wyliczeń płacowych — jak urlop bezpłatny — sam rekord korekty nie tworzy
// drugiego, samodzielnego wpisu w kolekcji Nieobecnosci; obserwowalnym efektem jest flaga Korygowana.)
var pracownik2 = Pracownik(Pracownik_.Andrzejewski);
var maj = new FromTo(new Date(2026, 5, 1), new Date(2026, 5, 31));
var rekordy = pracownik2.Nieobecnosci.GetIntersectedRows(maj).Cast<Nieobecnosc>().ToList();
rekordy.Should().ContainSingle("nieobecność pierwotna nadal istnieje w kolekcji")
.Which.Korygowana.Should().BeTrue("po dodaniu korekty nieobecność jest oznaczona jako korygowana");
}
[Test]
[Ignore("Worker UstalPonowniePodstawęNaliczaniaWorker (D2 wariant B) jest aktywny tylko dla zwolnień " +
"ZUS / urlopów macierzyńskich (IsEnabledPonownieUstalPodstawę), a FAKTYCZNE przeliczenie kwot " +
"zasiłku następuje dopiero przy ponownym naliczeniu wypłaty (mechanizm PodstawaZasilku). Na bazie " +
"Demo z rollbackiem, bez pełnego scenariusza naliczenia listy płac, nie da się sensownie zweryfikować " +
"efektu workera. LUKA w pracownik.md D2: dokument nie podaje minimalnego, wykonalnego scenariusza " +
"naliczenia wypłaty pozwalającego zweryfikować przeliczenie podstawy.")]
[Description("D2 (wariant B): czynność 'Ustal ponownie podstawę naliczania' przez worker — " +
"niewykonalna na samej korekcie rekordu bez naliczonej wypłaty.")]
public void D2_PonowneUstaleniePodstawy_PrzezWorker_Niewykonalne()
{
// Pozostawione jako [Ignore] — patrz uzasadnienie w atrybucie.
}
// ============================== D7 — Analiza limitów urlopowych ==============================
[Test]
[Description("D7: limit urlopowy NIE jest tworzony ręcznie — najpierw naliczamy go " +
"NaliczanieLimitow.DodajLimit(), potem odczytujemy z pracownik.Limity; arytmetyka " +
"Wykorzystane == Razem - Pozostalo jest spójna.")]
public void D7_NaliczenieLimitu_TworzyLimitDoOdczytu()
{
var pracownik = Pracownik(Pracownik_.Andrzejewski);
var defLimit = Kalend.DefinicjeLimitow.WgNazwy[DefUrlopWyp] as DefinicjaLimitu;
defLimit.Should().NotBeNull($"definicja limitu '{DefUrlopWyp}' istnieje w bazie Demo");
var rok = FromTo.Year(new Date(2026, 1, 1));
InTransaction(() =>
{
// NaliczanieLimitow: publiczny bezparametrowy ctor; Params(Context) z bieżącej sesji testu.
var naliczanie = new NaliczanieLimitow
{
Pars = new NaliczanieLimitow.Params(Context)
{
Definicja = defLimit,
Okres = rok,
KopiujKorekty = true
},
Pracownicy = new[] { pracownik }
};
naliczanie.DodajLimit(); // tworzy/aktualizuje rekordy LimitNieobecnosci
});
SaveDispose();
// Odczyt limitu — filtr serwerowy po kolekcji child pracownika TYLKO po Definicja
// (porównanie FromTo == FromTo nie jest tłumaczone na zapytanie serwerowe — okres filtrujemy w pamięci).
var pracownik2 = Pracownik(Pracownik_.Andrzejewski);
var defLimit2 = Kalend.DefinicjeLimitow.WgNazwy[DefUrlopWyp] as DefinicjaLimitu;
var lim = pracownik2.Limity[(LimitNieobecnosci l) => l.Definicja == defLimit2]
.Cast<LimitNieobecnosci>()
.FirstOrDefault(l => l.Okres.From == rok.From);
lim.Should().NotBeNull("naliczenie utworzyło limit urlopu wypoczynkowego na 2026");
// „Przysługujący" to Razem (limit kodeksowy + przeniesienia + zmiany), wykorzystany = Razem - Pozostalo.
// Uwaga: dla syntetycznych pracowników Demo Razem bywa 0 (brak danych stażu/urodzenia napędzających 20/26 dni),
// dlatego sprawdzamy spójność arytmetyki, a nie konkretną dodatnią wartość.
(lim!.Razem - lim.Pozostalo).Should().Be(lim.Wykorzystane,
"wykorzystany = przysługujący - pozostały (== pole Wykorzystane)");
lim.Razem.Should().BeGreaterThanOrEqualTo(0, "przysługujący limit nie jest ujemny");
}
[Test]
[Description("D7: wprowadzenie urlopu wypoczynkowego wymaga ISTNIEJĄCEGO limitu na ten dzień — ustawienie " +
"Okres na nieobecności urlopowej wyzwala przeliczenie limitu; po wcześniejszym naliczeniu " +
"limitu zapis przechodzi bez LimitNotFoundException, a limit jest odczytywalny.")]
public void D7_UrlopWypoczynkowy_WymagaNaliczonegoLimitu()
{
var defLimit = Kalend.DefinicjeLimitow.WgNazwy[DefUrlopWyp] as DefinicjaLimitu;
var rok = FromTo.Year(new Date(2026, 1, 1));
// 1) Najpierw nalicz limit za rok — to warunek konieczny dla urlopu wypoczynkowego.
InTransaction(() =>
{
var pracownikE = Pracownik(Pracownik_.Bednarek);
var naliczanie = new NaliczanieLimitow
{
Pars = new NaliczanieLimitow.Params(Context)
{
Definicja = defLimit,
Okres = rok,
KopiujKorekty = true
},
Pracownicy = new[] { pracownikE }
};
naliczanie.DodajLimit();
});
SaveDispose();
// 2) Dopiero teraz wprowadzenie urlopu wypoczynkowego nie rzuca LimitNotFoundException
// (definicje pobieramy ponownie w bieżącej sesji — po SaveDispose poprzednie są z innej sesji).
InTransaction(() =>
{
var pracownikE = Pracownik(Pracownik_.Bednarek);
var defUrlop = Kalend.DefNieobecnosci.WgNazwy[DefUrlopWyp] as DefinicjaNieobecnosci;
var n = Session.AddRow(new NieobecnośćPracownika(pracownikE));
n.Definicja = defUrlop;
n.Okres = new FromTo(new Date(2026, 6, 1), new Date(2026, 6, 5));
});
SaveDispose();
// 3) Odczyt: limit istnieje i jest spójny; nieobecność urlopowa została zapisana.
var pracownik2 = Pracownik(Pracownik_.Bednarek);
var defLimit2 = Kalend.DefinicjeLimitow.WgNazwy[DefUrlopWyp] as DefinicjaLimitu;
var lim = pracownik2.Limity[(LimitNieobecnosci l) => l.Definicja == defLimit2]
.Cast<LimitNieobecnosci>()
.FirstOrDefault(l => l.Okres.From == rok.From);
lim.Should().NotBeNull("limit urlopu wypoczynkowego za 2026 został naliczony");
lim!.Wykorzystane.Should().Be(lim.Razem - lim.Pozostalo,
"wykorzystany odczytany z pola jest spójny z Razem - Pozostalo");
var czerwiec = new FromTo(new Date(2026, 6, 1), new Date(2026, 6, 30));
pracownik2.Nieobecnosci.GetIntersectedRows(czerwiec).Cast<Nieobecnosc>()
.Should().ContainSingle("urlop wypoczynkowy został zapisany po naliczeniu limitu");
}
}
@@ -0,0 +1,553 @@
using System.Linq;
using AwesomeAssertions;
using NUnit.Framework;
using Soneta.Business;
using Soneta.Kadry;
using Soneta.Kalend;
using Soneta.Place;
using Soneta.Types;
using Prac = Soneta.Kadry.Pracownik;
namespace Soneta.Skills.Test.KadryPlace.Pracownik;
/// <summary>
/// Rozdział D (część dalsza) — „Nieobecności i czas pracy" (receptury D3D12).
/// <para>
/// Testy są <b>wykonywalną dokumentacją</b> publicznego kontraktu platformy Soneta dla zaawansowanej
/// obsługi nieobecności pracownika: zwolnień ZUS (e-ZLA), deklaracji Z-3/Z-3a, przestoju, parametrów
/// okresu zasiłkowego, naliczania limitów, podstaw nieobecności, bilansu otwarcia, wniosków urlopowych
/// i pracy zdalnej. Każda metoda mapuje się 1:1 do receptury z dokumentu skilla <c>pracownik.md</c>:
/// <list type="bullet">
/// <item><b>D3</b> — model danych e-ZLA (<c>Nieobecnosc.Zwolnienie: ZwolnienieZUS</c>, <c>Nieobecnosc.ZLA: ZLA</c>); sam import sieciowy → [Ignore];</item>
/// <item><b>D4</b> — deklaracje Z-3 / Z-3a (workery <c>Z3Worker</c>/<c>Z3aWorker</c> — wymagają naliczonej podstawy);</item>
/// <item><b>D5</b> — przestój (<c>DodajPrzestojWorker</c>, <c>IndywidualnyProcentWynagrPrzestojowegoWorker</c>);</item>
/// <item><b>D6</b> — parametry okresu zasiłkowego (<c>Zwolnienie.KontynuacjaOkrZas</c>/<c>PrzedluzenieOkrZas</c>);</item>
/// <item><b>D8</b> — naliczanie + przeliczanie limitów (<c>NaliczanieLimitow.DodajLimit()</c>, <c>PrzeliczWykorzystaneWorker</c>);</item>
/// <item><b>D9</b> — podstawy nieobecności (<c>pracownik.PodstawyNieobecności</c> — odczyt; dodawanie → [Ignore]);</item>
/// <item><b>D10</b> — bilans otwarcia (<c>PracHistoria.ChorobowyBO</c>, <c>PracHistoria.DodatkowyBO</c>);</item>
/// <item><b>D11</b> — wnioski urlopowe (<c>WniosekUrlopowy</c>, <c>PlanowanaNieobecność</c>);</item>
/// <item><b>D12</b> — praca zdalna (<c>PracHistoria.PracaZdalna</c>, <c>LokalizacjaPracyZdalnej</c>).</item>
/// </list>
/// </para>
/// <para>
/// Wszystko działa na bazie Demo (GoldStandard) z automatycznym rollbackiem po teście. Operujemy
/// wyłącznie na <b>publicznym kontrakcie</b> — tak jak dodatek programisty zewnętrznego bez dostępu
/// do kodu źródłowego aplikacji. Operacje wymagające sieci (import e-ZLA) lub naliczonej wypłaty
/// (kwoty zasiłku/przestoju, sensowne kwoty deklaracji Z-3, dodawanie podstaw) są oznaczone [Ignore]
/// z asercją na model danych tam, gdzie się da.
/// </para>
/// </summary>
[TestFixture]
public class RozdzialDrest_NieobecnosciTest : PracownikTestBase
{
private const string DefZwolnienieChor = "Zwolnienie chorobowe";
private const string DefUrlopWyp = "Urlop wypoczynkowy";
// Definicja nieobecności NIEwymagająca naliczonego limitu — bezpieczna dla wniosków bez naliczania limitu.
private const string DefBezplatny = "Urlop bezpłatny (art 174 kp)";
// ============================== D3 — Import e-ZLA (model danych) ==============================
[Test]
[Description("D3: dane ZUS zwolnienia leżą w subrowie Nieobecnosc.Zwolnienie typu ZwolnienieZUS, " +
"a dane dokumentu ZLA w subrowie Nieobecnosc.ZLA typu ZLA — odwzorowujemy e-ZLA jako " +
"NieobecnośćPracownika z definicją zasiłkową i ustawiamy pola subrowów (bez sieci).")]
public void D3_ModelDanychEZLA_ZwolnienieIZLAToSubrowyNieobecnosci()
{
var pracownik = Pracownik(Pracownik_.Andrzejewski);
pracownik.Should().NotBeNull("pracownik z Demo istnieje");
var defChor = Kalend.DefNieobecnosci.WgNazwy[DefZwolnienieChor] as DefinicjaNieobecnosci;
defChor.Should().NotBeNull($"definicja zasiłkowa '{DefZwolnienieChor}' istnieje w bazie Demo");
var okres = new FromTo(new Date(2026, 5, 4), new Date(2026, 5, 10));
InTransaction(() =>
{
var nieob = Session.AddRow(new NieobecnośćPracownika(pracownik));
nieob.Definicja = defChor;
nieob.Okres = okres;
// Subrowy Zwolnienie / ZLA są częścią rekordu — nie tworzymy ich osobno, ustawiamy pola.
nieob.Zwolnienie.Numer = "ZLA000001"; // pole Numer ma limit 9 znaków
nieob.Zwolnienie.KodChoroby = "A";
nieob.Zwolnienie.Przyczyna = PrzyczynaZwolnienia.ZwolnienieLekarskie;
nieob.ZLA.Zrodlo = "Import PUE (odwzorowanie testowe)";
});
SaveDispose();
var pracownik2 = Pracownik(Pracownik_.Andrzejewski);
var maj = new FromTo(new Date(2026, 5, 1), new Date(2026, 5, 31));
var zapisana = pracownik2.Nieobecnosci.GetIntersectedRows(maj).Cast<Nieobecnosc>().Single();
zapisana.Definicja.Nazwa.Should().Be(DefZwolnienieChor);
zapisana.Zwolnienie.Numer.Should().Be("ZLA000001", "dane ZUS z subrowa Zwolnienie zostały utrwalone");
zapisana.Zwolnienie.KodChoroby.Should().Be("A");
zapisana.Zwolnienie.Przyczyna.Should().Be(PrzyczynaZwolnienia.ZwolnienieLekarskie);
zapisana.ZLA.Zrodlo.ToString().Should().Contain("Import PUE", "dane dokumentu ZLA z subrowa ZLA zostały utrwalone");
}
[Test]
[Ignore("Sam import e-ZLA z PUE ZUS jest operacją SIECIOWĄ (uwierzytelnienie + bramka ZUS) — nie da się " +
"go odtworzyć w teście jednostkowym na bazie Demo. Model danych (subrowy Zwolnienie/ZLA) jest " +
"pokryty przez D3_ModelDanychEZLA_ZwolnienieIZLAToSubrowyNieobecnosci.")]
[Description("D3: import e-ZLA z PUE — niewykonalny bez sieci.")]
public void D3_ImportEZLA_ZPUE_WymagaSieci_Niewykonalne()
{
}
// ============================== D4 — Deklaracje Z-3 / Z-3a ==============================
[Test]
[Ignore("Sensowny Z-3 wymaga NALICZONEJ wypłaty/podstawy zasiłku — na czystej Demo z rollbackiem, bez " +
"pełnego scenariusza naliczenia listy płac, deklaracja powstałaby z pustymi kwotami, a worker " +
"Z3Worker przyjmuje dane przez Context (KeduContext + Z3ParamContext) i wykonuje logikę KEDU. " +
"Testowalny jest jedynie fakt istnienia workera (sprawdzane przez D4_Z3Worker_TypIstnieje).")]
[Description("D4: generowanie deklaracji Z-3 przez worker — niewykonalne bez naliczonej podstawy zasiłku.")]
public void D4_GenerowanieZ3_PrzezWorker_Niewykonalne()
{
}
[Test]
[Description("D4: workery deklaracji Z-3 / Z-3a istnieją w publicznym kontrakcie (typy " +
"Soneta.Deklaracje.ZUS.ZUSZ3.Z3Worker / Z3aWorker) — dokumentujemy ich dostępność.")]
public void D4_Z3Worker_TypIstnieje()
{
// Workery są w osobnym assembly Soneta.Deklaracje.ZUS — sprawdzamy obecność typu po pełnej nazwie.
var z3 = System.Type.GetType("Soneta.Deklaracje.ZUS.ZUSZ3.Z3Worker, Soneta.Deklaracje.ZUS")
?? FindByFullName("Soneta.Deklaracje.ZUS.ZUSZ3.Z3Worker");
var z3a = System.Type.GetType("Soneta.Deklaracje.ZUS.ZUSZ3.Z3aWorker, Soneta.Deklaracje.ZUS")
?? FindByFullName("Soneta.Deklaracje.ZUS.ZUSZ3.Z3aWorker");
z3.Should().NotBeNull("worker Z-3 (Z3Worker) jest dostępny w publicznym kontrakcie");
z3a.Should().NotBeNull("worker Z-3a (Z3aWorker) jest dostępny w publicznym kontrakcie");
z3!.GetMethod("UtworzDeklaracjeZ3").Should().NotBeNull("Z3Worker eksponuje akcję UtworzDeklaracjeZ3");
}
private static System.Type FindByFullName(string fullName) =>
System.AppDomain.CurrentDomain.GetAssemblies()
.Select(a => { try { return a.GetType(fullName); } catch { return null; } })
.FirstOrDefault(t => t != null);
// ============================== D5 — Przestój ==============================
[Test]
[Description("D5: przestój dodajemy workerem DodajPrzestojWorker — ustawiamy Pracownicy oraz " +
"Pars (DefinicjaStrefy + Okres); worker wykonuje własną transakcję. Strefę przestoju " +
"pobieramy dynamicznie ze słownika DefinicjeStref danej bazy.")]
public void D5_DodajPrzestoj_PrzezWorker()
{
var pracownik = Pracownik(Pracownik_.Bednarek);
// Definicja strefy przestoju — słownik danej bazy; nazwa może się różnić, więc szukamy elastycznie.
var defStrefa = Kalend.DefinicjeStref.Cast<DefinicjaStrefy>()
.FirstOrDefault(d => d.Nazwa != null && d.Nazwa.Contains("rzestój"));
if (defStrefa == null)
{
Assert.Ignore("Brak strefy przestoju w słowniku DefinicjeStref bazy Demo — receptura D5 niewykonalna na tej bazie.");
return;
}
var worker = new DodajPrzestojWorker
{
Pracownicy = new[] { pracownik },
Pars = new DodajPrzestojWorker.Params(Context)
{
DefinicjaStrefy = defStrefa,
Okres = new FromTo(new Date(2026, 6, 1), new Date(2026, 6, 5))
}
};
// Worker wykonuje własną transakcję — wywołujemy poza otwartą transakcją edycyjną.
worker.DodajPrzestoj();
SaveDispose();
// Przestój materializuje się jako strefa w planie pracy — weryfikujemy spójnie, że operacja
// nie rzuciła wyjątku i pracownik jest nadal odczytywalny (skutki płacowe liczą się przy wypłacie).
Pracownik(Pracownik_.Bednarek).Should().NotBeNull("przestój dodany bez błędu");
}
[Test]
[Description("D5: indywidualny procent wynagrodzenia przestojowego (przestój ekonomiczny) ustawiamy " +
"workerem IndywidualnyProcentWynagrPrzestojowegoWorker — Pars.Data + Pars.Procent (ułamek). " +
"Procent jest też trzymany na etacie: PracHistoria.Etat.Postojowe.Procent.")]
public void D5_ProcentWynagrPrzestojowego_PrzezWorker_OdkladaSieNaEtacie()
{
var pracownik = Pracownik(Pracownik_.Bujak);
var worker = new IndywidualnyProcentWynagrPrzestojowegoWorker
{
Pracownicy = new[] { pracownik },
Pars = new IndywidualnyProcentWynagrPrzestojowegoWorker.Params(Context)
{
Data = new Date(2026, 6, 1),
Procent = new Percent(0.5m) // 50% — Percent przyjmujemy jako ułamek, nie liczbę 50
}
};
worker.Aktualizuj();
SaveDispose();
// Procent przestojowego odkłada się na etacie (PracHistoria.Etat.Postojowe).
var pracownik2 = Pracownik(Pracownik_.Bujak);
var historia = pracownik2.Historia[new Date(2026, 6, 1)];
historia.Should().NotBeNull("istnieje zapis historyczny na czerwiec 2026");
historia.Etat.Postojowe.Procent.Should().Be(new Percent(0.5m),
"procent wynagrodzenia przestojowego został zapisany na etacie jako 50%");
}
// ============================== D6 — Parametry okresu zasiłkowego ==============================
[Test]
[Description("D6: parametry okresu zasiłkowego są bazodanowymi polami subrowa Nieobecnosc.Zwolnienie: " +
"KontynuacjaOkrZas (enum), PrzedluzenieOkrZas (bool), PrzedluzeniaData (Date) oraz " +
"flaga PonownieUstalPodstawe ustawiana metodą SetPonownieUstalPodstawe(bool).")]
public void D6_ParametryOkresuZasilkowego_ZapisNaSubrowieZwolnienie()
{
var pracownik = Pracownik(Pracownik_.Strzelecki);
var defChor = Kalend.DefNieobecnosci.WgNazwy[DefZwolnienieChor] as DefinicjaNieobecnosci;
var okres = new FromTo(new Date(2026, 5, 4), new Date(2026, 5, 31));
InTransaction(() =>
{
var nieob = Session.AddRow(new NieobecnośćPracownika(pracownik));
nieob.Definicja = defChor;
nieob.Okres = okres;
});
SaveDispose();
// Zmiana parametrów okresu zasiłkowego wprost na rekordzie.
InTransaction(() =>
{
var pracownikE = Pracownik(Pracownik_.Strzelecki);
var nieob = (Nieobecnosc)pracownikE.Nieobecnosci.GetIntersectedRows(okres)[0];
nieob.Zwolnienie.KontynuacjaOkrZas = KontynuacjaOkrZas.Tak;
nieob.Zwolnienie.PrzedluzenieOkrZas = true;
nieob.Zwolnienie.PrzedluzeniaData = new Date(2026, 5, 31);
nieob.Zwolnienie.SetPonownieUstalPodstawe(true);
});
SaveDispose();
var pracownik2 = Pracownik(Pracownik_.Strzelecki);
var zapisana = pracownik2.Nieobecnosci.GetIntersectedRows(okres).Cast<Nieobecnosc>().Single();
zapisana.Zwolnienie.KontynuacjaOkrZas.Should().Be(KontynuacjaOkrZas.Tak);
zapisana.Zwolnienie.PrzedluzenieOkrZas.Should().BeTrue("okres zasiłkowy oznaczono jako przedłużony");
zapisana.Zwolnienie.PrzedluzeniaData.Should().Be(new Date(2026, 5, 31));
zapisana.Zwolnienie.PonownieUstalPodstawe.Should().BeTrue("flaga ponownego ustalenia podstawy ustawiona");
}
// ============================== D8 — Naliczanie i przeliczanie limitów ==============================
[Test]
[Description("D8: limit naliczamy NaliczanieLimitow.DodajLimit(), a liczbę wykorzystanych dni " +
"przeliczamy workerem LimitNieobecnosci.Pracownicy.PrzeliczWykorzystaneWorker " +
"(Pars.Definicja + Pars.Okres). Po przeliczeniu arytmetyka limitu pozostaje spójna.")]
public void D8_NaliczenieIPrzeliczenieLimitu()
{
var defLimit = Kalend.DefinicjeLimitow.WgNazwy[DefUrlopWyp] as DefinicjaLimitu;
defLimit.Should().NotBeNull($"definicja limitu '{DefUrlopWyp}' istnieje w bazie Demo");
var rok = FromTo.Year(new Date(2026, 1, 1));
// 1) Naliczenie limitu (jak D7).
InTransaction(() =>
{
var pracownikE = Pracownik(Pracownik_.Andrzejewski);
var naliczanie = new NaliczanieLimitow
{
Pars = new NaliczanieLimitow.Params(Context)
{
Definicja = defLimit,
Okres = rok,
KopiujKorekty = true
},
Pracownicy = new[] { pracownikE }
};
naliczanie.DodajLimit();
});
SaveDispose();
// 2) Przeliczenie wykorzystanych — worker wykonuje własną transakcję.
var przelicz = new LimitNieobecnosci.Pracownicy.PrzeliczWykorzystaneWorker
{
Pracownicy = new[] { Pracownik(Pracownik_.Andrzejewski) },
Pars = new LimitNieobecnosci.Pracownicy.PrzeliczWykorzystaneWorker.Params(Context)
{
Definicja = Kalend.DefinicjeLimitow.WgNazwy[DefUrlopWyp] as DefinicjaLimitu,
Okres = rok
}
};
przelicz.PrzeliczWykorzystane();
SaveDispose();
// 3) Odczyt limitu — arytmetyka spójna (Razem bywa 0 dla syntetycznych pracowników Demo).
var pracownik2 = Pracownik(Pracownik_.Andrzejewski);
var defLimit2 = Kalend.DefinicjeLimitow.WgNazwy[DefUrlopWyp] as DefinicjaLimitu;
var lim = pracownik2.Limity[(LimitNieobecnosci l) => l.Definicja == defLimit2]
.Cast<LimitNieobecnosci>()
.FirstOrDefault(l => l.Okres.From == rok.From);
lim.Should().NotBeNull("limit urlopu wypoczynkowego na 2026 został naliczony");
lim!.Wykorzystane.Should().Be(lim.Razem - lim.Pozostalo,
"po przeliczeniu wykorzystany jest spójny z Razem - Pozostalo");
lim.Wykorzystane.Should().BeGreaterThanOrEqualTo(0, "wykorzystane nie jest ujemne");
}
// ============================== D9 — Podstawy nieobecności (odczyt) ==============================
[Test]
[Description("D9 (odczyt): podstawy nieobecności ZUS / urlopu leżą w kolekcji child " +
"pracownik.PodstawyNieobecności (typ Soneta.Place.PodstawaNieobecnosci); filtrujemy " +
"serwerowo po polu Typ (Chorobowa / Wypoczynkowy). Na czystej Demo kolekcja może być pusta.")]
public void D9_OdczytPodstawNieobecnosci_FiltrPoTyp()
{
var pracownik = Pracownik(Pracownik_.Andrzejewski);
// Filtr serwerowy po Typ — nie iterujemy całości z if w pamięci.
var chorobowe = pracownik.PodstawyNieobecności[
(PodstawaNieobecnosci x) => x.Typ == TypyPodstawNieobecnosci.Chorobowa]
.Cast<PodstawaNieobecnosci>().ToList();
// Asercja na MODEL/spójność: każda zwrócona pozycja faktycznie ma Typ == Chorobowa,
// a relacja do pracownika jest spełniona (Pracownik to guided-parent, read-only).
chorobowe.Should().OnlyContain(p => p.Typ == TypyPodstawNieobecnosci.Chorobowa,
"filtr serwerowy zwraca wyłącznie podstawy chorobowe");
chorobowe.Should().OnlyContain(p => p.Pracownik == pracownik,
"podstawa należy do pracownika (relacja child)");
// Na czystej Demo (bez naliczonej wypłaty z zasiłkiem) kolekcja bywa pusta — to dopuszczalne.
}
[Test]
[Ignore("PodstawaNieobecnosci NIE ma publicznego ctora (jedynie (RowCreator) i (Pracownik, " +
"TypyPodstawNieobecnosci) — niepubliczne). Rekordy podstaw powstają z NALICZENIA WYPŁATY, " +
"więc ręczne dodanie podstawy nie jest możliwe przez publiczny kontrakt; testowalny jest " +
"wyłącznie odczyt (D9_OdczytPodstawNieobecnosci_FiltrPoTyp).")]
[Description("D9: ręczne dodanie podstawy nieobecności — niewykonalne (brak publicznego ctora).")]
public void D9_DodanieRecznePodstawy_Niewykonalne()
{
}
// ============================== D10 — Bilans otwarcia ==============================
[Test]
[Description("D10: bilans otwarcia chorobowy leży w subrowie zapisu PracHistoria.ChorobowyBO " +
"(okres zasiłkowy, dni). Edytujemy pola subrowa na właściwym zapisie historycznym " +
"'na dzień' (pracownik.Historia[data]).")]
public void D10_BilansOtwarcia_ChorobowyBO()
{
var data = new Date(2026, 1, 1);
InTransaction(() =>
{
var pracownikE = Pracownik(Pracownik_.Bednarek);
var historia = pracownikE.Historia[data];
historia.Should().NotBeNull("istnieje zapis historyczny obowiązujący na 2026-01-01");
// BO chorobowy / okres zasiłkowy
historia.ChorobowyBO.DniZasilkowe = 33;
historia.ChorobowyBO.ZasilekOdDnia = data;
historia.ChorobowyBO.PrzedluzenieOZ = true;
});
SaveDispose();
var pracownik2 = Pracownik(Pracownik_.Bednarek);
var historia2 = pracownik2.Historia[data];
historia2.ChorobowyBO.DniZasilkowe.Should().Be(33, "BO chorobowy: dni zasiłkowe");
historia2.ChorobowyBO.ZasilekOdDnia.Should().Be(data, "BO chorobowy: zasiłek od dnia");
historia2.ChorobowyBO.PrzedluzenieOZ.Should().BeTrue("BO chorobowy: przedłużenie okresu zasiłkowego");
}
[Test]
[Ignore("DodatkowyBO.UPoprzednich/BezPierwszego/Wykorzystany rzucają ColReadOnlyException na zwykłym " +
"zapisie historycznym z Demo (pole 'tylko do odczytu'). BO urlopowy jest zapisywalny tylko na " +
"zapisie historycznym oznaczonym jako bilans otwarcia / start zatrudnienia — czego nie da się " +
"odtworzyć na gotowych pracownikach Demo bez ingerencji w historię zatrudnienia. Pole ChorobowyBO " +
"jest pokryte przez D10_BilansOtwarcia_ChorobowyBO.")]
[Description("D10: bilans otwarcia urlopowy (DodatkowyBO) — niezapisywalny na zwykłym zapisie historii Demo.")]
public void D10_BilansOtwarcia_DodatkowyBO_ReadOnlyNaHistoriiDemo()
{
}
// ============================== D11 — Wnioski o urlop ==============================
[Test]
[Description("D11: wniosek urlopowy tworzymy ctorem WniosekUrlopowy(pracownik, definicja) + AddRow; " +
"ustawiamy Okres, Data, Stan (StanWnioskuUrlopowego). Wniosek trafia do kolekcji " +
"pracownik.WnioskiUrlopowe; akceptacja to zmiana Stan na Zaakceptowany + DataDecyzji. " +
"Używamy definicji NIEwymagającej limitu — akceptacja wniosku urlopu wypoczynkowego " +
"wyzwoliłaby przeliczenie limitu i LimitNotFoundException bez wcześniejszego naliczenia limitu.")]
public void D11_WniosekUrlopowy_RejestracjaIAkceptacja()
{
var pracownik = Pracownik(Pracownik_.Bujak);
var defUrlop = Kalend.DefNieobecnosci.WgNazwy[DefBezplatny] as DefinicjaNieobecnosci;
defUrlop.Should().NotBeNull($"definicja '{DefBezplatny}' istnieje w bazie Demo");
InTransaction(() =>
{
var wniosek = Session.AddRow(new WniosekUrlopowy(pracownik, defUrlop));
wniosek.Okres = new FromTo(new Date(2026, 8, 3), new Date(2026, 8, 7));
wniosek.Data = new Date(2026, 7, 20);
wniosek.Stan = StanWnioskuUrlopowego.Oczekujący;
wniosek.Pracownik.Should().BeSameAs(pracownik, "ctor wiąże wniosek z pracownikiem");
wniosek.Definicja.Should().BeSameAs(defUrlop, "ctor ustawia definicję nieobecności");
});
SaveDispose();
// Odczyt z kolekcji child + akceptacja (zmiana stanu).
InTransaction(() =>
{
var pracownikE = Pracownik(Pracownik_.Bujak);
var wniosek = pracownikE.WnioskiUrlopowe.Cast<WniosekUrlopowy>()
.First(w => w.Stan == StanWnioskuUrlopowego.Oczekujący);
wniosek.Stan = StanWnioskuUrlopowego.Zaakceptowany;
wniosek.DataDecyzji = new Date(2026, 7, 21);
});
SaveDispose();
var pracownik2 = Pracownik(Pracownik_.Bujak);
var zapisany = pracownik2.WnioskiUrlopowe.Cast<WniosekUrlopowy>().Single();
zapisany.Stan.Should().Be(StanWnioskuUrlopowego.Zaakceptowany, "wniosek został zaakceptowany");
zapisany.DataDecyzji.Should().Be(new Date(2026, 7, 21));
zapisany.Definicja.Nazwa.Should().Be(DefBezplatny);
}
[Test]
[Description("D11: planowana nieobecność (osobny model planu urlopów) — ctor PlanowanaNieobecność(pracownik) " +
"+ AddRow; Definicja, Okres. Pole Stan jest READ-ONLY (StanPlanowanejNieobecności) — zmieniamy " +
"je metodami przejść stanu (StanWprowadzona/StanZatwierdzona/StanAnulowana/StanOczekująca). " +
"Trafia do kolekcji pracownik.PlanowaneNieobecności (FromToSubTable).")]
public void D11_PlanowanaNieobecnosc_Rejestracja()
{
var pracownik = Pracownik(Pracownik_.Strzelecki);
// Definicja planowanej nieobecności MUSI mieć zaznaczone pole 'Planowana' — pobieramy dynamicznie.
var defPlan = Kalend.DefNieobecnosci.Cast<DefinicjaNieobecnosci>().FirstOrDefault(d => d.Planowana);
if (defPlan == null)
{
Assert.Ignore("Brak definicji nieobecności z flagą 'Planowana' w bazie Demo — receptura niewykonalna.");
return;
}
var okres = new FromTo(new Date(2026, 9, 1), new Date(2026, 9, 5));
InTransaction(() =>
{
var plan = Session.AddRow(new PlanowanaNieobecność(pracownik));
plan.Definicja = defPlan;
plan.Okres = okres;
// Stan jest read-only — przejście stanu wykonujemy metodą domenową, nie przypisaniem.
plan.StanWprowadzona();
});
SaveDispose();
var pracownik2 = Pracownik(Pracownik_.Strzelecki);
var wrzesien = new FromTo(new Date(2026, 9, 1), new Date(2026, 9, 30));
var plany = pracownik2.PlanowaneNieobecności.GetIntersectedRows(wrzesien)
.Cast<PlanowanaNieobecność>().ToList();
plany.Should().ContainSingle("dodaliśmy jedną planowaną nieobecność we wrześniu 2026")
.Which.Stan.Should().Be(StanPlanowanejNieobecności.Wprowadzona,
"po StanWprowadzona() plan jest w stanie Wprowadzona");
}
[Test]
[Description("D11: wniosek o delegację jest subrowem wniosku urlopowego (WniosekUrlopowy.Delegacja " +
"typu WniosekODelegację) — ustawiamy pola delegacji na tym subrowie.")]
public void D11_WniosekODelegacje_JestSubrowemWniosku()
{
var pracownik = Pracownik(Pracownik_.Andrzejewski);
var defUrlop = Kalend.DefNieobecnosci.WgNazwy[DefUrlopWyp] as DefinicjaNieobecnosci;
InTransaction(() =>
{
var wniosek = Session.AddRow(new WniosekUrlopowy(pracownik, defUrlop));
wniosek.Okres = new FromTo(new Date(2026, 10, 5), new Date(2026, 10, 9));
wniosek.Data = new Date(2026, 9, 30);
// Delegacja to subrow — ustawiamy jego pola (cel, planowana zaliczka).
wniosek.Delegacja.Cel = "Spotkanie z klientem";
wniosek.Delegacja.WnioskowanaZaliczka = new Currency(500m);
});
SaveDispose();
var pracownik2 = Pracownik(Pracownik_.Andrzejewski);
var zapisany = pracownik2.WnioskiUrlopowe.Cast<WniosekUrlopowy>().Single();
zapisany.Delegacja.Cel.ToString().Should().Contain("klientem", "cel delegacji zapisany na subrowie");
zapisany.Delegacja.WnioskowanaZaliczka.Should().Be(new Currency(500m));
}
// ============================== D12 — Praca zdalna ==============================
[Test]
[Description("D12: parametry pracy zdalnej (model pracy, oświadczenie o warunkach) leżą na " +
"historycznym zapisie etatu: PracHistoria.PracaZdalna (typ PracZdalna). Edytujemy je " +
"na właściwym zapisie 'na dzień' (pracownik.Historia[data]).")]
public void D12_ModelPracyZdalnej_NaHistoriiEtatu()
{
var data = new Date(2026, 6, 1);
InTransaction(() =>
{
var pracownikE = Pracownik(Pracownik_.Bednarek);
var historia = pracownikE.Historia[data];
historia.PracaZdalna.ModelPracy = ModelPracy.PracaHybrydowa;
historia.PracaZdalna.OswiadczenieWarunki = true;
});
SaveDispose();
var pracownik2 = Pracownik(Pracownik_.Bednarek);
var historia2 = pracownik2.Historia[data];
historia2.PracaZdalna.ModelPracy.Should().Be(ModelPracy.PracaHybrydowa, "ustawiono model pracy hybrydowej");
historia2.PracaZdalna.OswiadczenieWarunki.Should().BeTrue("oświadczenie o warunkach lokalowych ustawione");
}
[Test]
[Description("D12: lokalizacja pracy zdalnej ma publiczny ctor LokalizacjaPracyZdalnej(pracownik) — " +
"tworzymy ją + AddRow i ustawiamy adres (subrow Adres). Trafia do kolekcji " +
"pracownik.LokalizacjePracyZdalnej.")]
public void D12_LokalizacjaPracyZdalnej_PublicznyCtor()
{
var pracownik = Pracownik(Pracownik_.Bujak);
InTransaction(() =>
{
var lok = Session.AddRow(new LokalizacjaPracyZdalnej(pracownik));
lok.Adres.Miejscowosc = "Kraków";
lok.Adres.Ulica = "Wadowicka";
});
SaveDispose();
var pracownik2 = Pracownik(Pracownik_.Bujak);
var lokalizacje = pracownik2.LokalizacjePracyZdalnej.Cast<LokalizacjaPracyZdalnej>().ToList();
lokalizacje.Should().ContainSingle("dodaliśmy jedną lokalizację pracy zdalnej")
.Which.Adres.Miejscowosc.Should().Be("Kraków", "adres lokalizacji został zapisany");
}
[Test]
[Description("D12 (odczyt): ewidencję pracy zdalnej okazjonalnej prezentuje worker ODCZYTOWY " +
"Soneta.Kadry.Pracownik.PracaZdalnaWorker (property bez akcji modyfikującej): " +
"DniPracyZdalnejRazem, LimitPracaZdalnaOkazjonalna, PozostaloPracaZdalnaOkazjonalna. " +
"Inicjujemy Pracownik + Okres i odczytujemy spójne, nieujemne wartości.")]
public void D12_PracaZdalnaWorker_OdczytEwidencji()
{
var pracownik = Pracownik(Pracownik_.Andrzejewski);
var worker = new Prac.PracaZdalnaWorker
{
Pracownik = pracownik,
Okres = FromTo.Year(new Date(2026, 1, 1))
};
// Worker odczytowy — property liczone z planu/ewidencji; weryfikujemy spójność wartości.
worker.DniPracyZdalnejRazem.Should().BeGreaterThanOrEqualTo(0, "liczba dni pracy zdalnej nie jest ujemna");
worker.LimitPracaZdalnaOkazjonalna.Should().BeGreaterThanOrEqualTo(0, "limit pracy zdalnej okazjonalnej nie jest ujemny");
worker.PozostaloPracaZdalnaOkazjonalna.Should().BeLessThanOrEqualTo(worker.LimitPracaZdalnaOkazjonalna,
"pozostały limit nie przekracza limitu całkowitego");
}
[Test]
[Ignore("WniosekPracyZdalnej ma NIEPUBLICZNE ctory — w teście jednostkowym nie utworzysz go przez new; " +
"zlecenie pracy zdalnej idzie przez worker GrupoweZleceniePracyZdalnejWorker (czynność Net/UI " +
"wymagająca pełnego Contextu Pulpitu). Testowalne wprost: ModelPracy/OswiadczenieWarunki na " +
"PracHistoria.PracaZdalna (D12_ModelPracyZdalnej_NaHistoriiEtatu) oraz LokalizacjaPracyZdalnej " +
"(D12_LokalizacjaPracyZdalnej_PublicznyCtor).")]
[Description("D12: rejestracja wniosku o pracę zdalną — niewykonalna przez new (ctory niepubliczne).")]
public void D12_WniosekPracyZdalnej_NiepublicznyCtor_Niewykonalne()
{
}
}
@@ -0,0 +1,276 @@
using System;
using System.Linq;
using AwesomeAssertions;
using NUnit.Framework;
using Soneta.Business;
using Soneta.Kalend;
using Soneta.Types;
using Prac = Soneta.Kadry.Pracownik;
namespace Soneta.Skills.Test.KadryPlace.Pracownik;
/// <summary>
/// Rozdział E/F — „Plan pracy i kalendarz" (E1, E2) oraz „RCP — rejestracja czasu pracy" (F1, F2).
/// <para>
/// Testy są <b>wykonywalną dokumentacją</b> publicznego kontraktu platformy Soneta dla planu pracy
/// i rejestracji czasu. Model: pracownik wystawia trzy niezależne kolekcje dni typu
/// <see cref="DateSubTable"/> (indeksator po <see cref="Date"/>, tylko do odczytu — element tworzysz
/// konstruktorem + <c>AddRow</c>):
/// <list type="bullet">
/// <item><c>DniPlanu</c> — plan/harmonogram (dni <see cref="DzienPlanu"/> : <see cref="DzienKalendarzaBase"/>),</item>
/// <item><c>DniPracy</c> — ewidencja czasu pracy (<see cref="DzienPracy"/>),</item>
/// <item><c>DniRCP</c> — zarejestrowany (zweryfikowany) czas pracy (<see cref="DzienRCP"/>) — wynik importu RCP.</item>
/// </list>
/// Wszystkie dni współdzielą subrow <c>Praca : CzasPracy</c> z polami <c>OdGodziny</c>/<c>DoGodziny</c>/<c>Czas</c>.
/// Zdarzenia wejścia/wyjścia (<see cref="WejscieWyjscie"/>) są childem <see cref="DzienPracy"/> (kolekcja <c>WeWy</c>).
/// </para>
/// <para>
/// Operujemy wyłącznie na <b>publicznym kontrakcie</b> (jak dodatek zewnętrzny), na bazie Demo
/// (GoldStandard) z automatycznym rollbackiem. Daty Demo dla planu/pracy są nieznane, więc odczyty
/// istniejących danych traktujemy defensywnie (kolekcja istnieje / indeksator nie rzuca), a scenariusze
/// zapisu budujemy na własnych, jawnych datach dla pracownika "006".
/// </para>
/// </summary>
[TestFixture]
public class RozdzialEF_PlanRcpTest : PracownikTestBase
{
// Data biznesowa do scenariuszy zapisu (jawna, nie Date.Today — data biznesowa Demo bywa inna).
private static readonly Date Dzien = new(2026, 6, 1);
/// <summary>
/// Definicja dnia (typ dnia) ze słownika konfiguracyjnego <c>DefinicjeDni</c>. Demo zawiera kilka
/// definicji; bierzemy pierwszą z brzegu (dowolny istniejący typ dnia), aby świeży dzień planu/pracy
/// miał wymaganą <c>Definicja</c>. Skróty <c>WolnaSobota</c>/<c>Niedziela</c> też są dostępne.
/// </summary>
private DefinicjaDnia DowolnaDefinicjaDnia()
{
return Kalend.DefinicjeDni.Rows.Cast<DefinicjaDnia>().FirstOrDefault();
}
// ============================== E1 — Plan pracy (harmonogram) ==============================
[Test]
[Description("E1 (odczyt): DniPlanu to DateSubTable nietypowany (zwraca Row, rzutujemy na DzienPlanu); " +
"DniPlanu == Etat.Kalendarz.Dni; indeksator [Date] jest tylko do odczytu i zwraca null dla braku dnia.")]
public void E1_DniPlanu_OdczytIndeksatoremPoDacie_ZwracaDzienPlanuLubNull()
{
var p = Pracownik(Pracownik_.Andrzejewski);
p.Should().NotBeNull("pracownik '006' istnieje w bazie Demo");
// DniPlanu jest DateSubTable (nietypowany) — element zwracany jako Row, rzutujemy na DzienPlanu.
p.DniPlanu.Should().NotBeNull("kolekcja planu (harmonogramu) zawsze istnieje");
// Indeksator [Date] to odczyt — nie rzuca; dla daty bez dnia planu zwraca null.
System.Action odczyt = () =>
{
var dp = (DzienPlanu)p.DniPlanu[Dzien];
if (dp is not null)
{
// Godziny pracy leżą na subrowie Praca; Czas/OdGodziny na rootcie dnia są kalkulowane.
Time _ = dp.Praca.OdGodziny;
Time __ = dp.Czas;
DefinicjaDnia ___ = dp.Definicja;
}
};
odczyt.Should().NotThrow("indeksator [Date] na DniPlanu jest bezpiecznym odczytem");
// DzienPlanu dziedziczy z DzienKalendarzaBase (dzień kalendarza pracownika).
typeof(DzienKalendarzaBase).IsAssignableFrom(typeof(DzienPlanu))
.Should().BeTrue("DzienPlanu jest dniem kalendarza (DzienKalendarzaBase)");
}
[Test]
[Description("E1 (zapis): nowy dzień planu tworzymy ctorem DzienPlanu(pracownik, data) + AddRow, " +
"ustawiamy Definicja (ze słownika DefinicjeDni) i godziny na subrowie Praca; po zapisie " +
"indeksator DniPlanu[data] zwraca utworzony dzień.")]
public void E1_UtworzenieDniaPlanu_UstawiaGodzinyNaSubrowiePraca()
{
var def = DowolnaDefinicjaDnia();
def.Should().NotBeNull("Demo zawiera definicje dni (słownik DefinicjeDni)");
Guid guidPrac = Guid.Empty;
InTransaction(() =>
{
var p = Pracownik(Pracownik_.Andrzejewski);
guidPrac = p.Guid;
// Indeksator [Date] jest read-only — nowego dnia nie „przypiszemy", tworzymy ctorem.
var dp = (DzienPlanu)p.DniPlanu[Dzien];
if (dp is null)
{
dp = Session.AddRow(new DzienPlanu(p, Dzien)); // ctor (Pracownik, Date)
dp.Definicja = def; // typ dnia ze słownika (wymagany dla weryfikatorów)
}
// Godziny ustawiamy na subrowie Praca; Czas dnia wylicza się z oddo.
dp.Praca.OdGodziny = new Time(8, 0);
dp.Praca.DoGodziny = new Time(16, 0);
});
SaveDispose();
// Odczyt po zapisie: dzień planu istnieje na wskazanej dacie i ma ustawione godziny.
var p2 = Get<Prac>(guidPrac);
var dp2 = (DzienPlanu)p2.DniPlanu[Dzien];
dp2.Should().NotBeNull("po zapisie dzień planu jest dostępny przez indeksator [Date]");
dp2.Data.Should().Be(Dzien);
dp2.Praca.OdGodziny.Should().Be(new Time(8, 0));
dp2.Praca.DoGodziny.Should().Be(new Time(16, 0));
}
// ============================== E2 — Kopiowanie planu / pracy (publiczne static) ==============================
[Test]
[Description("E2: KalendarzPlanuKopia.Kopiuj(pracownik, okres) to publiczna metoda STATYCZNA " +
"(bez Context) — kopiuje wyliczony plan na okres do bufora DniPlanuKopia. Test wykonuje " +
"wywołanie w transakcji i sprawdza, że nie rzuca oraz że bufor DniPlanuKopia jest dostępny.")]
public void E2_KalendarzPlanuKopia_Kopiuj_StaticNaOkres_NieRzuca()
{
var okres = new FromTo(new Date(2026, 6, 1), new Date(2026, 6, 30));
InTransaction(() =>
{
var p = Pracownik(Pracownik_.Andrzejewski);
// Publiczny static Kopiuj(Pracownik, FromTo) — właściwa droga dla kodu serwerowego/testów
// (worker KopiujWorker wymaga Context/zaznaczenia i jest gardzony licencją BI — patrz E2).
System.Action kopiuj = () => KalendarzPlanuKopia.Kopiuj(p, okres);
kopiuj.Should().NotThrow("Kopiuj(Pracownik, FromTo) to publiczne statyczne API bez Context");
// Kopia trafia do osobnego bufora DniPlanuKopia (DateSubTable), odrębnego od DniPlanu.
p.DniPlanuKopia.Should().NotBeNull("bufor kopii planu (DniPlanuKopia) jest dostępny");
});
SaveDispose();
}
[Test]
[Description("E2: KalendarzPracyKopia.Kopiuj(pracownik, okres) — analogiczny publiczny static dla " +
"kopiowania realizacji (pracy) na okres; kopia trafia do bufora DniPracyKopia.")]
public void E2_KalendarzPracyKopia_Kopiuj_StaticNaOkres_NieRzuca()
{
var okres = new FromTo(new Date(2026, 6, 1), new Date(2026, 6, 30));
InTransaction(() =>
{
var p = Pracownik(Pracownik_.Andrzejewski);
System.Action kopiuj = () => KalendarzPracyKopia.Kopiuj(p, okres);
kopiuj.Should().NotThrow("Kopiuj(Pracownik, FromTo) to publiczne statyczne API bez Context");
p.DniPracyKopia.Should().NotBeNull("bufor kopii pracy (DniPracyKopia) jest dostępny");
});
SaveDispose();
}
// ============================== F1 — Odczyt zarejestrowanego/ewidencjonowanego czasu ==============================
[Test]
[Description("F1 (odczyt): DniPracy i DniRCP to DateSubTable TYPOWANE (DzienPracy / DzienRCP); " +
"indeksator [Date] zwraca właściwy typ lub null i nie rzuca. DzienRCP testujemy tylko " +
"ODCZYTOWO — jest wynikiem importu/weryfikacji RCP, nie tworzymy go ręcznie.")]
public void F1_DniPracyIDniRCP_OdczytIndeksatoremPoDacie_NieRzuca()
{
var p = Pracownik(Pracownik_.Andrzejewski);
p.DniPracy.Should().NotBeNull("kolekcja ewidencji (DniPracy) istnieje");
p.DniRCP.Should().NotBeNull("kolekcja zweryfikowanego RCP (DniRCP) istnieje");
System.Action odczyt = () =>
{
// DniPracy jest typowane — indeksator [Date] zwraca DzienPracy lub null.
DzienPracy dzienPracy = p.DniPracy[Dzien];
if (dzienPracy is not null)
{
Time _ = dzienPracy.Praca.Czas; // przepracowany czas dnia (subrow Praca)
Time __ = dzienPracy.Praca.OdGodziny;
}
// DniRCP jest typowane — DzienRCP lub null; odczyt stanu weryfikacji RCP.
DzienRCP dzienRcp = p.DniRCP[Dzien];
if (dzienRcp is not null)
{
StanWeryfikacjiRCP ___ = dzienRcp.StanRCP;
Time ____ = dzienRcp.Praca.Czas;
}
};
odczyt.Should().NotThrow("indeksatory [Date] na DniPracy/DniRCP to bezpieczny odczyt");
}
[Test]
[Description("F1 (zapis ewidencji): dzień ewidencji tworzymy ctorem DzienPracy(pracownik, data) + AddRow " +
"(sam ctor nie rejestruje wiersza); godziny ustawiamy na subrowie Praca. Po zapisie " +
"DniPracy[data] zwraca utworzony dzień.")]
public void F1_UtworzenieDniaPracy_UstawiaGodzinyNaSubrowiePraca()
{
Guid guidPrac = Guid.Empty;
InTransaction(() =>
{
var p = Pracownik(Pracownik_.Andrzejewski);
guidPrac = p.Guid;
var dp = p.DniPracy[Dzien];
if (dp is null)
{
// ctor (Pracownik, Date) + AddRow — sam ctor nie włącza wiersza do tabeli.
dp = Session.AddRow(new DzienPracy(p, Dzien));
}
dp.Praca.OdGodziny = new Time(8, 0);
dp.Praca.DoGodziny = new Time(16, 0);
});
SaveDispose();
var p2 = Get<Prac>(guidPrac);
var dp2 = p2.DniPracy[Dzien];
dp2.Should().NotBeNull("po zapisie dzień ewidencji jest dostępny przez indeksator [Date]");
dp2.Data.Should().Be(Dzien);
dp2.Praca.OdGodziny.Should().Be(new Time(8, 0));
dp2.Praca.DoGodziny.Should().Be(new Time(16, 0));
}
// ============================== F2 — Wejścia/wyjścia (zdarzenia RCP na dniu pracy) ==============================
[Test]
[Description("F2: zdarzenie WejscieWyjscie jest childem DzienPracy — ctor WejscieWyjscie(dzienPracy) + " +
"AddRow do kalend.WejsciaWyjscia; ustawiamy Godzina i Typ (enum TypWejsciaWyjscia). " +
"Odczyt przez DzienPracy.WeWy (LpSubTable, posortowane po Lp).")]
public void F2_WejscieWyjscie_DodanieWejsciaIWyjscia_DoDniaPracy()
{
Guid guidPrac = Guid.Empty;
InTransaction(() =>
{
var p = Pracownik(Pracownik_.Andrzejewski);
guidPrac = p.Guid;
// Najpierw potrzebny dzień ewidencji (właściciel zdarzeń we/wy).
var dp = p.DniPracy[Dzien];
if (dp is null)
dp = Session.AddRow(new DzienPracy(p, Dzien));
// Wejście 8:00 — ctor wiąże zdarzenie z dniem; AddRow do tabeli WejsciaWyjscia.
var we = new WejscieWyjscie(dp);
Kalend.WejsciaWyjscia.AddRow(we);
we.Godzina = new Time(8, 0);
we.Typ = TypWejsciaWyjscia.Wejscie; // enum, nie string/int
// Wyjście 16:00.
var wy = new WejscieWyjscie(dp);
Kalend.WejsciaWyjscia.AddRow(wy);
wy.Godzina = new Time(16, 0);
wy.Typ = TypWejsciaWyjscia.Wyjscie;
});
SaveDispose();
// Odczyt zdarzeń dnia przez kolekcję WeWy (LpSubTable — kolejność wg Lp).
var p2 = Get<Prac>(guidPrac);
var dzien = p2.DniPracy[Dzien];
dzien.Should().NotBeNull("dzień ewidencji z dodanymi zdarzeniami istnieje");
var zdarzenia = dzien.WeWy.Cast<WejscieWyjscie>().OrderBy(w => w.Lp).ToList();
zdarzenia.Should().HaveCount(2, "dodaliśmy wejście i wyjście");
zdarzenia.Should().Contain(w => w.Typ == TypWejsciaWyjscia.Wejscie && w.Godzina == new Time(8, 0));
zdarzenia.Should().Contain(w => w.Typ == TypWejsciaWyjscia.Wyjscie && w.Godzina == new Time(16, 0));
// Dzien (właściciel) ustawiony przez ctor — wszystkie zdarzenia wskazują nasz dzień pracy.
zdarzenia.Should().OnlyContain(w => w.Dzien.Data == Dzien);
}
}
@@ -0,0 +1,414 @@
using System;
using System.Linq;
using AwesomeAssertions;
using NUnit.Framework;
using Soneta.Business;
using Soneta.Kadry;
using Soneta.Kalend;
using Soneta.Types;
using Prac = Soneta.Kadry.Pracownik;
namespace Soneta.Skills.Test.KadryPlace.Pracownik;
/// <summary>
/// Rozdział E/F (część druga) — operacje na planie pracy i RCP wykraczające poza CRUD dni:
/// <list type="bullet">
/// <item>E3 — aktualizacja kalendarza pracownika (worker seryjny, wymaga Context → <c>[Ignore]</c>),</item>
/// <item>E4 — uzgodnienie doby pracowniczej (worker dnia/grupowy, wymaga Context → <c>[Ignore]</c>),</item>
/// <item>E5 — odczyt normy i czasu przepracowanego przez <c>pracownik.Czasy : KalkulatorPracownika</c> (★ pełny odczyt),</item>
/// <item>F3 — import RCP: sam import plikowy <c>[Ignore]</c>; przeliczenie we/wy przez <c>ImportDniaWorker</c> (★),</item>
/// <item>F4 — weryfikacja/korekta RCP: <c>DzienRCP</c>/<c>StanRCP</c> (★ korekta na świeżym dniu),</item>
/// <item>F5 — praca hybrydowa: strefy dnia i podzielniki (★ odczyt).</item>
/// </list>
/// <para>
/// Operujemy wyłącznie na <b>publicznym kontrakcie</b> platformy Soneta (jak dodatek zewnętrzny),
/// na bazie Demo (GoldStandard) z automatycznym rollbackiem. Daty Demo planu/pracy są nieznane, więc
/// odczyty istniejących danych traktujemy defensywnie (kolekcja istnieje / indeksator nie rzuca),
/// a scenariusze zapisu budujemy na własnych, jawnych datach dla pracownika „006".
/// </para>
/// <para>
/// <b>Granica testowalności.</b> Operacje wymagające <see cref="Context"/> (worker E3/E4 grupowy —
/// <c>Params : ContextBase</c> z ctorem <c>(Context)</c>, karmiony zaznaczeniem listy) lub źródła
/// zewnętrznego (import RCP z pliku/czytnika) są oznaczone <c>[Ignore]</c> z uzasadnieniem — opisują
/// kontrakt, nie wykonują operacji. <c>KalkulatorPracownika</c>/<c>CzasDni</c>/<c>ZestawienieNadgodzin</c>
/// nie są wierszami ORM — to obiekty liczące (czysty odczyt bez transakcji).
/// </para>
/// </summary>
[TestFixture]
public class RozdzialEFrest_PlanRcpTest : PracownikTestBase
{
// Jawne daty/okresy do scenariuszy (nie Date.Today — data biznesowa Demo bywa inna).
private static readonly Date Dzien = new(2026, 6, 1);
private static readonly FromTo Okres = new(new Date(2026, 6, 1), new Date(2026, 6, 30));
private static readonly YearMonth Miesiac = new(2026, 6);
// ============================== E3 — Aktualizacja kalendarza pracownika ==============================
[Test]
[Description("E3 (kontrakt, [Ignore]): AktualizujKalendarzWorker to worker seryjny z menu Czynności. " +
"Pracownicy/Pars są set-only, a Params : ContextBase ma ctor (Context) — bez zaznaczenia " +
"listy (Context) nie da się zbudować parametrów, więc operacji nie wykonujemy w teście.")]
[Ignore("E3: AktualizujKalendarzWorker.Params : ContextBase wymaga Context (zaznaczenie listy pracowników) — brak czystego API bezkontekstowego.")]
public void E3_AktualizujKalendarz_WymagaContext_Ignore()
{
// Świadomie nie wykonujemy — operacja seryjna sterowana zaznaczeniem UI (Context).
// worker.Pracownicy = context.Get<Pracownik[]>();
// worker.Pars = new AktualizujKalendarzWorker.Params(context) { Data = ..., Docelowy = ..., Zmiana = true };
// worker.Aktualizuj(); // Logout + Commit wewnątrz
Assert.Fail("Test oznaczony [Ignore] — nie powinien być uruchamiany.");
}
[Test]
[Description("E3 (odczyt konfiguracji): kalendarz docelowy/źródłowy aktualizacji to konfiguracja " +
"Etat.Kalendarz oraz interpretacja Etat.InterpretacjaKalendarza — odczyt nie wymaga workera " +
"ani Context i nie rzuca; pokazuje skąd worker E3 bierze stan wejściowy.")]
public void E3_KalendarzIInterpretacja_OdczytKonfiguracjiEtatu_NieRzuca()
{
var p = Pracownik(Pracownik_.Andrzejewski);
p.Should().NotBeNull("pracownik '006' istnieje w bazie Demo");
System.Action odczyt = () =>
{
// Etat leży na bieżącym zapisie historycznym (pracownik.Last.Etat); kalendarz i interpretacja
// sterują aktualizacją (E3).
var etat = p.Last?.Etat;
if (etat is not null)
{
Kalendarz kal = etat.Kalendarz; // kalendarz roboczy (źródło/cel zmiany)
InterpretacjaKalendarza interpretacja = etat.InterpretacjaKalendarza;
_ = interpretacja;
if (kal is not null)
{
Time _ = kal.NormaDobowa; // norma dobowa kalendarza
}
}
};
odczyt.Should().NotThrow("odczyt kalendarza/interpretacji z Etatu nie wymaga Context ani transakcji");
}
// ============================== E4 — Uzgodnienie doby pracowniczej ==============================
[Test]
[Description("E4 (kontrakt, odczyt): granica doby to atrybuty KONFIGURACYJNE Etatu " +
"(ConfigPoczątekDobyNiedzieledIŚwięta — read-only, NormaDobowa) — nie ma edytowalnego pola " +
"początku doby na pojedynczym DzienPracy. Odczyt tych pól nie rzuca.")]
public void E4_ModelDoby_OdczytKonfiguracjiEtatu_NieRzuca()
{
var p = Pracownik(Pracownik_.Andrzejewski);
p.Should().NotBeNull("pracownik '006' istnieje w bazie Demo");
System.Action odczyt = () =>
{
var etat = p.Last?.Etat;
if (etat is not null)
{
Time poczatekDobySwieta = etat.ConfigPoczątekDobyNiedzieledIŚwięta; // konfiguracyjne, read-only
Time normaDobowa = etat.NormaDobowa;
_ = poczatekDobySwieta;
_ = normaDobowa;
}
};
odczyt.Should().NotThrow("granica doby/normy to konfiguracja Etatu — czysty odczyt");
}
[Test]
[Description("E4 (kontrakt, [Ignore]): worker pojedynczego dnia DzienPracy.UzgodnijDobePracowniczaWorker " +
"ma Dzień set-only i wymaga istniejącego dnia ewidencji oraz IsEnabled; worker grupowy " +
"(Params : ContextBase) wymaga Context. W Demo brak deterministycznej doby nocnej do uzgodnienia, " +
"więc operacji nie wykonujemy — opisujemy kontrakt (IsEnabled + Uzgodnij/Przenieś).")]
[Ignore("E4: UzgodnijDobePracownicza — worker dnia wymaga deterministycznego dnia nocnego (brak w Demo); worker grupowy wymaga Context.")]
public void E4_UzgodnijDobePracownicza_WymagaContextLubDanych_Ignore()
{
// var dzien = pracownik.DniPracy[data];
// if (DzienPracy.UzgodnijDobePracowniczaWorker.IsEnabledUzgodnijDobePracownicza(dzien)) { ... }
// new DzienPracy.UzgodnijDobePracowniczaWorker { Dzień = dzien }.UzgodnijDobePracownicza();
Assert.Fail("Test oznaczony [Ignore] — nie powinien być uruchamiany.");
}
// ============================== E5 — Odczyt normy / czasu przepracowanego (★ testowalne) ==============================
[Test]
[Description("E5: pracownik.Czasy zwraca KalkulatorPracownika (NIE Row — obiekt liczący, czysty odczyt " +
"bez transakcji). Kalkulator istnieje dla pracownika z bazy Demo.")]
public void E5_Czasy_ZwracaKalkulatorPracownika_NieNull()
{
var p = Pracownik(Pracownik_.Andrzejewski);
p.Should().NotBeNull("pracownik '006' istnieje w bazie Demo");
KalkulatorPracownika kalk = p.Czasy;
kalk.Should().NotBeNull("pracownik.Czasy daje kalkulator czasu pracy (kontekst pracownika)");
}
[Test]
[Description("E5: Norma(okres) (plan) i Praca(okres) (realizacja) zwracają CzasDni (Czas : Time, Dni : int). " +
"Wywołanie to czysty odczyt — nie rzuca i nie wymaga transakcji. Wartości mogą być Empty/Invalid " +
"(brak danych Demo w okresie), więc sprawdzamy tylko sam kontrakt odczytu.")]
public void E5_NormaIPraca_OdczytZaOkres_ZwracaCzasDni_NieRzuca()
{
var p = Pracownik(Pracownik_.Andrzejewski);
var kalk = p.Czasy;
CzasDni norma = CzasDni.Invalid;
CzasDni praca = CzasDni.Invalid;
System.Action odczyt = () =>
{
norma = kalk.Norma(Okres); // params Item[] condition — wywołanie bez filtra
praca = kalk.Praca(Okres); // czas przepracowany (realizacja)
_ = kalk.PracaRozliczana(Okres); // czas rozliczany (do nadgodzin)
};
odczyt.Should().NotThrow("odczyt Norma/Praca przez KalkulatorPracownika jest bezpieczny (bez transakcji)");
// CzasDni to obiekt wynikowy (Time + int) — pola tylko do odczytu; dostęp nie rzuca.
System.Action poleCzasDni = () =>
{
Time _ = norma.Czas; int __ = norma.Dni;
Time ___ = praca.Czas; int ____ = praca.Dni;
};
poleCzasDni.Should().NotThrow("CzasDni wystawia Czas/Dni jako odczyt");
}
[Test]
[Description("E5: NormaKodeksowa(YearMonth) zwraca normę kodeksową miesiąca (pełny etat) jako CzasDni; " +
"dla czerwca 2026 (20 dni roboczych × 8h) norma kodeksowa jest dodatnia — wynik nie jest Invalid " +
"i ma policzalne Dni/Czas.")]
public void E5_NormaKodeksowa_DlaMiesiaca_JestDodatnia()
{
var p = Pracownik(Pracownik_.Andrzejewski);
var kalk = p.Czasy;
CzasDni norma = kalk.NormaKodeksowa(Miesiac);
// Norma kodeksowa miesiąca nie zależy od danych pracownika — to kalendarz kodeksowy.
norma.Should().NotBe(CzasDni.Invalid, "norma kodeksowa istnieje dla każdego pełnego miesiąca");
norma.Dni.Should().BeGreaterThan(0, "czerwiec 2026 ma dni robocze");
norma.Czas.TotalMinutes.Should().BeGreaterThan(0, "pełny etat = dodatnia norma czasu pracy");
}
[Test]
[Description("E5: Nadgodziny(YearMonth) zwraca ZestawienieNadgodzin (struct: N50/N100/NSW/Razem — wszystkie Time, " +
"read-only). Nocne(okres) zwraca Time. Czysty odczyt — nie rzuca; przy braku danych Demo wynik = Zero.")]
public void E5_NadgodzinyINocne_OdczytStatystyk_NieRzuca()
{
var p = Pracownik(Pracownik_.Andrzejewski);
var kalk = p.Czasy;
ZestawienieNadgodzin nadg = ZestawienieNadgodzin.Zero;
Time nocne = new(0);
System.Action odczyt = () =>
{
nadg = kalk.Nadgodziny(Miesiac);
nocne = kalk.Nocne(Okres);
};
odczyt.Should().NotThrow("odczyt nadgodzin/czasu nocnego jest bezpieczny");
// Pola zestawienia to odczyt; Razem agreguje składowe (nie rzuca, może być Zero).
System.Action pola = () => { Time _ = nadg.N50; Time __ = nadg.N100; Time ___ = nadg.Razem; _ = nocne; };
pola.Should().NotThrow("ZestawienieNadgodzin wystawia N50/N100/Razem jako odczyt");
}
[Test]
[Description("E5: DniNie(okres)/NormaNie(okres) odczytują liczbę i normę dni nieobecności za okres. " +
"DniNie zwraca int (>=0), NormaNie zwraca CzasDni. Czysty odczyt — nie rzuca.")]
public void E5_NieobecnosciZaOkres_OdczytLiczbyINormy_NieRzuca()
{
var p = Pracownik(Pracownik_.Andrzejewski);
var kalk = p.Czasy;
int dniNie = -1;
System.Action odczyt = () =>
{
dniNie = kalk.DniNie(Okres); // liczba dni nieobecności
_ = kalk.NormaNie(Okres); // norma nieobecności (CzasDni)
};
odczyt.Should().NotThrow("odczyt nieobecności za okres przez kalkulator jest bezpieczny");
dniNie.Should().BeGreaterThanOrEqualTo(0, "liczba dni nieobecności nie jest ujemna");
}
// ============================== F3 — Import RCP (przeliczenie we/wy, ★) ==============================
[Test]
[Description("F3 ([Ignore]): import surowych odbić z pliku/czytnika RCP wymaga zewnętrznego źródła " +
"(plik/serwis/format) — brak czystego API w publicznym kontrakcie. Testowalny jest jedynie " +
"fragment po wczytaniu: przeliczenie już-wpisanych we/wy przez ImportDniaWorker (osobny test).")]
[Ignore("F3: import z pliku/urządzenia RCP wymaga zewnętrznego źródła (I/O) — poza zakresem testu kontraktu.")]
public void F3_ImportZPliku_WymagaZrodlaZewnetrznego_Ignore()
{
Assert.Fail("Test oznaczony [Ignore] — nie powinien być uruchamiany.");
}
[Test]
[Description("F3 (przeliczenie, ★): po wpisaniu zdarzeń we/wy na dzień ewidencji (jak po imporcie) " +
"ImportDniaWorker { DzienPracy = dzien }.Przelicz() przelicza odbicia na czas pracy — operacja " +
"na obiektach sesji (bez I/O). Worker ma bezparametrowy ctor i property DzienPracy {get;set;}.")]
public void F3_ImportDniaWorker_PrzeliczWeWy_NieRzuca()
{
Guid guidPrac = Guid.Empty;
InTransaction(() =>
{
var p = Pracownik(Pracownik_.Andrzejewski);
guidPrac = p.Guid;
// Dzień ewidencji (właściciel zdarzeń) — tworzymy ctorem + AddRow (sam ctor nie rejestruje).
var dp = p.DniPracy[Dzien] ?? Session.AddRow(new DzienPracy(p, Dzien));
// Surowe odbicia we/wy (tabela pośrednia) — tak wyglądają dane „po imporcie", przed przeliczeniem.
var we = new WejscieWyjscie(dp);
Kalend.WejsciaWyjscia.AddRow(we);
we.Godzina = new Time(8, 0);
we.Typ = TypWejsciaWyjscia.Wejscie;
var wy = new WejscieWyjscie(dp);
Kalend.WejsciaWyjscia.AddRow(wy);
wy.Godzina = new Time(16, 0);
wy.Typ = TypWejsciaWyjscia.Wyjscie;
// Przeliczenie odbić na czas pracy dnia (bez pliku/urządzenia).
System.Action przelicz = () => new ImportDniaWorker { DzienPracy = dp }.Przelicz();
przelicz.Should().NotThrow("ImportDniaWorker.Przelicz() przelicza we/wy na czas pracy bez I/O");
});
SaveDispose();
// Po przeliczeniu dzień ewidencji nadal jest dostępny przez indeksator [Date].
var p2 = Get<Prac>(guidPrac);
var dp2 = p2.DniPracy[Dzien];
dp2.Should().NotBeNull("dzień ewidencji z przeliczonymi odbiciami istnieje po zapisie");
dp2.WeWy.Cast<WejscieWyjscie>().Should().HaveCount(2, "wejście i wyjście zostały zachowane");
}
// ============================== F4 — Weryfikacja / korekta RCP (★ testowalne) ==============================
[Test]
[Description("F4 (odczyt): DniRCP to DateSubTable<DzienRCP> (typowane) — indeksator [Date] zwraca DzienRCP/null " +
"i nie rzuca. DzienRCP to wynik importu/weryfikacji; w Demo zwykle brak (null) dla naszej daty. " +
"Odczytujemy StanRCP (enum StanWeryfikacjiRCP) i Praca.Czas defensywnie.")]
public void F4_DniRCP_OdczytIndeksatoremPoDacie_NieRzuca()
{
var p = Pracownik(Pracownik_.Andrzejewski);
p.DniRCP.Should().NotBeNull("kolekcja zweryfikowanego RCP (DniRCP) istnieje");
System.Action odczyt = () =>
{
DzienRCP dzienRcp = p.DniRCP[Dzien]; // typowane: DzienRCP lub null
if (dzienRcp is not null)
{
StanWeryfikacjiRCP stan = dzienRcp.StanRCP; // enum stanu weryfikacji
Time czas = dzienRcp.Praca.Czas; // czas na subrowie Praca
bool rcpOk = dzienRcp.RcpOK; // flaga stanu po imporcie
_ = stan; _ = czas; _ = rcpOk;
}
};
odczyt.Should().NotThrow("indeksator [Date] na DniRCP to bezpieczny odczyt");
}
[Test]
[Description("F4 (korekta, ★): na świeżo utworzonym DzienRCP korygujemy godziny na subrowie Praca, " +
"ustawiamy StanRCP (enum) na Poprawny i dopisujemy Uwagi (MemoText). Po zapisie DniRCP[data] " +
"zwraca dzień ze zmienionym stanem i godzinami. Czas/OdGodziny na rootcie są kalkulowane (read-only).")]
public void F4_KorektaDzienRCP_ZmianaStanuIGodzin_ZapisOdczyt()
{
Guid guidPrac = Guid.Empty;
InTransaction(() =>
{
var p = Pracownik(Pracownik_.Andrzejewski);
guidPrac = p.Guid;
// W Demo DzienRCP zwykle nie istnieje na naszej dacie — do scenariusza korekty
// tworzymy go ctorem + AddRow (analogicznie do DzienPracy). Korekta dotyczy istniejącego rekordu.
var dzienRcp = p.DniRCP[Dzien] ?? Session.AddRow(new DzienRCP(p, Dzien));
// Korekta godzin na subrowie Praca (root Czas/OdGodziny są kalkulowane).
dzienRcp.Praca.OdGodziny = new Time(8, 0);
dzienRcp.Praca.DoGodziny = new Time(16, 0);
// Zmiana stanu weryfikacji (enum, nie string) + uwagi.
dzienRcp.StanRCP = StanWeryfikacjiRCP.Poprawny;
dzienRcp.Uwagi = (MemoText)"Skorygowano wyjście";
});
SaveDispose();
var p2 = Get<Prac>(guidPrac);
var rcp2 = p2.DniRCP[Dzien];
rcp2.Should().NotBeNull("po zapisie dzień RCP jest dostępny przez indeksator [Date]");
rcp2.StanRCP.Should().Be(StanWeryfikacjiRCP.Poprawny, "stan weryfikacji został ustawiony");
rcp2.Praca.OdGodziny.Should().Be(new Time(8, 0));
rcp2.Praca.DoGodziny.Should().Be(new Time(16, 0));
}
// ============================== F5 — Praca hybrydowa / strefy / podzielniki (odczyt) ==============================
[Test]
[Description("F5 (odczyt): DzienPracy.Strefy to SubTable<StrefaPracy> — podział dnia na strefy " +
"(stacjonarna / zdalna). Każda StrefaPracy ma Definicja : DefinicjaStrefy i CzasRozliczany : Time. " +
"Kolekcja istnieje (może być pusta w Demo); iteracja i odczyt pól nie rzucają.")]
public void F5_StrefyDniaPracy_OdczytPodzialuNaStrefy_NieRzuca()
{
Guid guidPrac = Guid.Empty;
// Świeży dzień pracy daje deterministyczną (pustą) kolekcję Strefy do bezpiecznego odczytu.
InTransaction(() =>
{
var p = Pracownik(Pracownik_.Andrzejewski);
guidPrac = p.Guid;
_ = p.DniPracy[Dzien] ?? Session.AddRow(new DzienPracy(p, Dzien));
});
SaveDispose();
var p2 = Get<Prac>(guidPrac);
var dzien = p2.DniPracy[Dzien];
dzien.Should().NotBeNull("dzień ewidencji istnieje");
dzien.Strefy.Should().NotBeNull("kolekcja stref pracy (Strefy) zawsze istnieje");
System.Action odczyt = () =>
{
foreach (StrefaPracy s in dzien.Strefy.Cast<StrefaPracy>())
{
DefinicjaStrefy def = s.Definicja; // strefa (np. praca zdalna)
Time rozliczany = s.CzasRozliczany; // czas rozliczany w strefie
_ = def; _ = rozliczany;
}
};
odczyt.Should().NotThrow("iteracja po strefach dnia i odczyt pól są bezpieczne");
}
[Test]
[Description("F5 (odczyt podzielników): pracownik.RozliczeniaCzasuPracy (dokumenty) oraz " +
"pracownik.ElementyRozliczeniaCzasuPracy (pozycje) to SubTable — kolekcje istnieją (mogą być puste " +
"w Demo). Element ma Definicja : DefinicjaStrefy i Czas : Time; odczyt nie rzuca. Budowy dokumentu " +
"rozliczenia nie testujemy — wymaga DefinicjaRozliczeniaCzasuPracy i przebiega przez extendery/UI.")]
public void F5_PodzielnikiRozliczeniaCzasuPracy_OdczytKolekcji_NieRzuca()
{
var p = Pracownik(Pracownik_.Andrzejewski);
p.RozliczeniaCzasuPracy.Should().NotBeNull("kolekcja dokumentów rozliczenia czasu pracy istnieje");
p.ElementyRozliczeniaCzasuPracy.Should().NotBeNull("kolekcja pozycji rozliczenia (podzielniki) istnieje");
System.Action odczyt = () =>
{
foreach (ElementRozliczeniaCzasuPracy el in p.ElementyRozliczeniaCzasuPracy.Cast<ElementRozliczeniaCzasuPracy>())
{
DefinicjaStrefy def = el.Definicja;
Time czas = el.Czas;
_ = def; _ = czas;
}
};
odczyt.Should().NotThrow("iteracja po pozycjach podzielnika i odczyt pól są bezpieczne");
}
[Test]
[Description("F5 (kontrakt typów): DefinicjaStrefy wystawia stałe Guid Praca_Zdalna / PracaZdalnaOkazjonalna " +
"(identyfikacja stref pracy zdalnej) oraz enum TypStrefy (NieWplywa/Zwieksza/Zmniejsza). " +
"Stałe są niepuste — to publiczne punkty zaczepienia rozliczenia pracy hybrydowej.")]
public void F5_DefinicjaStrefy_StalePracaZdalnaIEnumTypStrefy_SaDostepne()
{
DefinicjaStrefy.Praca_Zdalna.Should().NotBe(Guid.Empty, "stała identyfikuje strefę pracy zdalnej");
DefinicjaStrefy.PracaZdalnaOkazjonalna.Should().NotBe(Guid.Empty, "stała identyfikuje strefę pracy zdalnej okazjonalnej");
// Enum TypStrefy steruje wpływem strefy na rozliczenie czasu.
System.Enum.IsDefined(typeof(TypStrefy), TypStrefy.NieWplywa).Should().BeTrue();
System.Enum.IsDefined(typeof(TypStrefy), TypStrefy.Zwieksza).Should().BeTrue();
System.Enum.IsDefined(typeof(TypStrefy), TypStrefy.Zmniejsza).Should().BeTrue();
}
}
@@ -0,0 +1,264 @@
using System;
using System.Linq;
using AwesomeAssertions;
using NUnit.Framework;
using Soneta.Kadry;
using Soneta.Place;
using Soneta.Types;
using Prac = Soneta.Kadry.Pracownik;
namespace Soneta.Skills.Test.KadryPlace.Pracownik;
/// <summary>
/// Rozdział G — „Umowy cywilnoprawne" (receptury G1, G2).
/// <para>
/// Testy są <b>wykonywalną dokumentacją</b> publicznego kontraktu platformy Soneta dla umów
/// cywilnoprawnych pracownika. <c>Soneta.Kadry.Umowa</c> to <b>root historyczny</b> (tabela
/// <c>Umowy</c>, child pracownika): dane nagłówkowe (definicja elementu = rodzaj umowy, okres,
/// sposób rozliczenia, typ wartości) siedzą na roocie, a <b>kwota/wartość umowy</b> jest historyczna
/// i siedzi na <c>UmowaHistoria.Wartosc</c> (zapis <c>umowa.Last</c>).
/// </para>
/// <para>
/// Wszystko działa na bazie Demo (GoldStandard) z automatycznym rollbackiem po teście. Pracownicy
/// etatowi z Demo (kody "006".."039") nie mają jeszcze umów cywilnoprawnych — to czysty punkt
/// wejścia dla asercji. Operujemy wyłącznie na <b>publicznym kontrakcie</b> — tak jak dodatek
/// programisty zewnętrznego bez dostępu do kodu źródłowego aplikacji.
/// </para>
/// </summary>
[TestFixture]
public class RozdzialG_UmowyTest : PracownikTestBase
{
// Pobranie definicji elementu = rodzaju umowy ze słownika konfiguracyjnego po stałej Guid.
// Indeksator DefElementow[Guid] zwraca definicję; rzutujemy na DefinicjaElementu.
private DefinicjaElementu DefUmowy(Guid rodzaj) =>
Place.DefElementow[rodzaj] as DefinicjaElementu;
// ============================== G1 — Dodawanie umów cywilnoprawnych ==============================
[Test]
[Description("G1: umowę zlecenie tworzymy przez Session.AddRow(new Umowa(pracownik)); w OnAdded " +
"powstaje pierwszy zapis UmowaHistoria (umowa.Last). Element = rodzaj umowy " +
"(DefElementow[DefinicjaElementu.UmowaZlecenie]); dane nagłówkowe na roocie, " +
"a kwota (Wartosc) na zapisie historycznym Last. Odczyt z pracownik.Umowy.")]
public void G1_UmowaZlecenie_DodawanaZElementemIWartosciaNaLast()
{
// Definicja elementu płacowego = rodzaj umowy (zlecenie) ze słownika konfiguracyjnego.
var defZlecenie = DefUmowy(DefinicjaElementu.UmowaZlecenie);
defZlecenie.Should().NotBeNull("baza Demo zawiera definicję umowy zlecenie (stała Guid)");
// Element przyjmuje tylko definicje o RodzajZrodla == Umowa.
defZlecenie.RodzajZrodla.Should().Be(RodzajŹródłaWypłaty.Umowa,
"definicja umowy zlecenie ma źródło typu Umowa");
Guid guidPrac = Guid.Empty;
var okres = new FromTo(new Date(2026, 1, 1), new Date(2026, 12, 31));
InTransaction(() =>
{
// Pracownik z Demo nie ma umów cywilnoprawnych — czysty punkt wejścia.
var pracownik = Pracownik(Pracownik_.Andrzejewski);
pracownik.Should().NotBeNull();
pracownik.Umowy.Cast<Umowa>().Should().BeEmpty("pracownik Demo nie ma jeszcze umów");
// 1) Utworzenie umowy + dodanie do tabeli; w OnAdded powstaje pierwszy UmowaHistoria.
// NIE tworzymy UmowaHistoria ręcznie — od razu mamy umowa.Last.
var umowa = Session.AddRow(new Umowa(pracownik));
umowa.Last.Should().NotBeNull("OnAdded tworzy pierwszy zapis historii (Last)");
// 2) Definicja elementu = rodzaj umowy (zlecenie).
umowa.Element = defZlecenie;
// 3) Dane nagłówkowe na roocie:
umowa.Data = new Date(2026, 1, 1);
umowa.Okres = okres;
umowa.Tytul = "Umowa zlecenie - obsługa projektu";
umowa.RodzajRozliczenia = RodzajeRozliczeniaUmowy.KwotaDoWypłaty;
umowa.TypWartosci = TypWartosciUmowy.Brutto;
// Jednostka organizacyjna (Wydzial) jest WYMAGANA przez weryfikator przy Save
// (WydzialRequiredVerifier) — wskazujemy korzeń struktury (Wydzialy.Firma).
umowa.Wydzial = Kadry.Wydzialy.Firma;
// 4) KWOTA umowy — na zapisie historycznym Last (UmowaHistoria.Wartosc), nie na roocie.
// umowa.Wartosc/umowa.Brutto na roocie są wyliczane (read-only).
umowa.Last.Wartosc = new Currency(5000m);
guidPrac = pracownik.Guid;
});
SaveDispose();
// Odczyt: umowa pojawia się w kolekcji childów pracownika (pracownik.Umowy).
var pracownik2 = Get<Prac>(guidPrac);
var umowy = pracownik2.Umowy.Cast<Umowa>().ToList();
umowy.Should().ContainSingle("dodaliśmy jedną umowę cywilnoprawną");
var u = umowy[0];
u.Element.Should().NotBeNull("Element (rodzaj umowy) jest wymagany");
u.Element.RodzajZrodla.Should().Be(RodzajŹródłaWypłaty.Umowa);
u.Tytul.Should().Be("Umowa zlecenie - obsługa projektu");
u.RodzajRozliczenia.Should().Be(RodzajeRozliczeniaUmowy.KwotaDoWypłaty);
u.TypWartosci.Should().Be(TypWartosciUmowy.Brutto);
u.Okres.From.Should().Be(okres.From);
u.Okres.To.Should().Be(okres.To);
// Kwota odczytana z zapisu historycznego Last.
u.Last.Wartosc.Should().Be(new Currency(5000m));
}
[Test]
[Description("G1 (o dzieło): wariant rodzaju umowy wskazujemy inną definicją elementu — " +
"DefElementow[DefinicjaElementu.Umowa20] (umowa o dzieło 20% KUP). Mechanizm " +
"tworzenia identyczny jak dla zlecenia (root + zapis historyczny Last).")]
public void G1_UmowaODzielo_WskazywanaInnaDefinicjaElementu()
{
// Wariant „o dzieło" = definicja Umowa20 (20% KUP).
var defDzielo = DefUmowy(DefinicjaElementu.Umowa20);
defDzielo.Should().NotBeNull("baza Demo zawiera definicję umowy o dzieło (Umowa20)");
defDzielo.RodzajZrodla.Should().Be(RodzajŹródłaWypłaty.Umowa);
Guid guidPrac = Guid.Empty;
var okres = new FromTo(new Date(2026, 3, 1), new Date(2026, 5, 31));
InTransaction(() =>
{
var pracownik = Pracownik(Pracownik_.Bednarek);
var umowa = Session.AddRow(new Umowa(pracownik));
umowa.Element = defDzielo;
umowa.Data = new Date(2026, 3, 1);
umowa.Okres = okres;
umowa.Tytul = "Umowa o dzieło - projekt graficzny";
umowa.RodzajRozliczenia = RodzajeRozliczeniaUmowy.KwotaDoWypłaty;
umowa.TypWartosci = TypWartosciUmowy.Brutto;
umowa.Wydzial = Kadry.Wydzialy.Firma; // jednostka organizacyjna wymagana przy Save
umowa.Last.Wartosc = new Currency(3000m);
guidPrac = pracownik.Guid;
});
SaveDispose();
var u = Get<Prac>(guidPrac).Umowy.Cast<Umowa>().Single();
u.Element.RodzajZrodla.Should().Be(RodzajŹródłaWypłaty.Umowa);
u.Tytul.Should().Be("Umowa o dzieło - projekt graficzny");
u.Last.Wartosc.Should().Be(new Currency(3000m));
}
[Test]
[Description("G1 (warianty rodzaju): stałe Guid definicji elementów umów (UmowaZlecenie, Umowa20, " +
"UmowaRyczałtowa) wskazują w słowniku DefElementow definicje o RodzajZrodla == Umowa.")]
public void G1_StaleDefinicjiElementow_WskazujaDefinicjeOZrodleUmowa()
{
// Dokumentujemy warianty rodzaju umowy bez modyfikacji danych — same stałe + słownik.
foreach (var rodzaj in new[]
{
DefinicjaElementu.UmowaZlecenie,
DefinicjaElementu.Umowa20,
DefinicjaElementu.UmowaRyczałtowa,
})
{
var def = DefUmowy(rodzaj);
def.Should().NotBeNull("definicja elementu umowy o danej stałej Guid istnieje w Demo");
def.RodzajZrodla.Should().Be(RodzajŹródłaWypłaty.Umowa,
"tylko definicje o źródle Umowa są akceptowane jako rodzaj umowy");
}
}
// ============================== G2 — Zmiana/aneks umowy ==============================
[Test]
[Description("G2 (korekta): zmiana danych nagłówkowych umowy (Tytul, Okres) w bieżącym okresie — " +
"bez Update/AddRow. Liczba zapisów historii się nie zmienia.")]
public void G2_Korekta_ZmieniaNaglowekBezNowegoOkresu()
{
Guid guidUmowy = Guid.Empty;
var okresPocz = new FromTo(new Date(2026, 1, 1), new Date(2026, 12, 31));
InTransaction(() =>
{
var pracownik = Pracownik(Pracownik_.Bujak);
var umowa = Session.AddRow(new Umowa(pracownik));
umowa.Element = DefUmowy(DefinicjaElementu.UmowaZlecenie);
umowa.Data = new Date(2026, 1, 1);
umowa.Okres = okresPocz;
umowa.Tytul = "Umowa zlecenie";
umowa.RodzajRozliczenia = RodzajeRozliczeniaUmowy.KwotaDoWypłaty;
umowa.TypWartosci = TypWartosciUmowy.Brutto;
umowa.Wydzial = Kadry.Wydzialy.Firma; // jednostka organizacyjna wymagana przy Save
umowa.Last.Wartosc = new Currency(4000m);
guidUmowy = umowa.Guid;
});
SaveDispose();
// Korekta: modyfikujemy dane nagłówkowe — bez Update, bez AddRow.
InTransaction(() =>
{
var umowa = Get<Umowa>(guidUmowy);
umowa.Tytul = "Umowa zlecenie - aneks zakresu prac";
umowa.Okres = new FromTo(umowa.Okres.From, new Date(2027, 6, 30)); // przedłużenie
});
SaveDispose();
var u2 = Get<Umowa>(guidUmowy);
u2.Tytul.Should().Be("Umowa zlecenie - aneks zakresu prac");
u2.Okres.To.Should().Be(new Date(2027, 6, 30), "przedłużono okres umowy");
// Korekta nie dzieli okresu — nadal jeden zapis historii.
u2.Historia.Cast<UmowaHistoria>().Should().ContainSingle("korekta nie tworzy nowego okresu");
}
[Test]
[Description("G2 (aneks 'od daty'): Historia.Update(odDnia) klonuje zapis aktualny na odDnia, " +
"skraca stary do odDnia-1 i zwraca NOWY klon (okres od odDnia); klon dodajemy do " +
"tabeli UmowaHistorie i ustawiamy na nim nową Wartosc.")]
public void G2_AneksOdDaty_TworzyNowyZapisHistoriiOdDnia_ISkracaStary()
{
Guid guidUmowy = Guid.Empty;
var odDnia = new Date(2026, 7, 1);
InTransaction(() =>
{
var pracownik = Pracownik(Pracownik_.Strzelecki);
var umowa = Session.AddRow(new Umowa(pracownik));
umowa.Element = DefUmowy(DefinicjaElementu.UmowaZlecenie);
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";
umowa.RodzajRozliczenia = RodzajeRozliczeniaUmowy.KwotaDoWypłaty;
umowa.TypWartosci = TypWartosciUmowy.Brutto;
umowa.Wydzial = Kadry.Wydzialy.Firma; // jednostka organizacyjna wymagana przy Save
umowa.Last.Wartosc = new Currency(5000m); // wartość początkowa
guidUmowy = umowa.Guid;
});
SaveDispose();
// Aneks „od daty": nowy zapis historyczny obowiązujący od odDnia (analogicznie do PracHistoria/A14).
InTransaction(() =>
{
var umowa = Get<Umowa>(guidUmowy);
// 1) Update klonuje zapis aktualny na odDnia, skraca stary do dnia poprzedniego
// i zwraca NOWY klon z okresem od odDnia.
var nowy = (UmowaHistoria)umowa.Historia.Update(odDnia);
// 2) Update + AddRow to nierozłączna para — bez AddRow klon zostaje „odpięty".
umowa.Module.UmowaHistorie.AddRow(nowy);
// 3) Na nowym zapisie ustawiamy zmienioną wartość (od odDnia).
// UWAGA: UmowaHistoria.PowodAktualizacji jest TYLKO DO ODCZYTU (brak settera),
// mimo że skan oznaczał je jako pole bazodanowe — nie ustawiamy go w kodzie.
nowy.Wartosc = new Currency(6000m);
});
SaveDispose();
var u2 = Get<Umowa>(guidUmowy);
// Mamy teraz dwa zapisy: stary (do odDnia-1) i nowy (od odDnia).
var zapisy = u2.Historia.Cast<UmowaHistoria>().OrderBy(h => h.Aktualnosc.From).ToList();
zapisy.Should().HaveCount(2, "Update utworzył drugi zapis historii umowy");
var stary = zapisy[0];
var nowy2 = zapisy[1];
// Stary zapis został skrócony do dnia poprzedzającego aneks.
stary.Aktualnosc.To.Should().Be(odDnia.AddDays(-1));
nowy2.Aktualnosc.From.Should().Be(odDnia, "nowy zapis obowiązuje od wskazanego dnia");
// Wartość różni się między okresami.
stary.Wartosc.Should().Be(new Currency(5000m));
nowy2.Wartosc.Should().Be(new Currency(6000m));
// Odczyt „na dzień": indeksator umowa[date] zwraca zapis obowiązujący na datę.
u2[odDnia].Wartosc.Should().Be(new Currency(6000m), "od odDnia obowiązuje nowa wartość");
u2[odDnia.AddDays(-1)].Wartosc.Should().Be(new Currency(5000m),
"przed odDnia obowiązuje wartość początkowa");
}
}
@@ -0,0 +1,401 @@
using System;
using System.Linq;
using AwesomeAssertions;
using NUnit.Framework;
using Soneta.Kadry;
using Soneta.Place;
using Soneta.Types;
using Prac = Soneta.Kadry.Pracownik;
namespace Soneta.Skills.Test.KadryPlace.Pracownik;
/// <summary>
/// Rozdział G (reszta) — „Umowy cywilnoprawne" (receptury G3, G4, G5).
/// <para>
/// Testy są <b>wykonywalną dokumentacją</b> publicznego kontraktu platformy Soneta dla operacji na
/// umowach cywilnoprawnych: operacja seryjna „Dodaj umowy" dla grupy osób (G3), rachunek/rozliczenie
/// umowy = wypłata <c>WyplataUmowa</c> naliczana mechanizmem płac (G4), oraz zgłoszenia ZUS
/// zleceniobiorców na podstawie schematu ubezpieczeń umowy (G5).
/// </para>
/// <para>
/// Wszystko działa na bazie Demo (GoldStandard) z automatycznym rollbackiem po teście. Pracownicy
/// etatowi z Demo (kody "006".."039") nie mają jeszcze umów cywilnoprawnych — czysty punkt wejścia.
/// Operujemy wyłącznie na <b>publicznym kontrakcie</b> — tak jak dodatek programisty zewnętrznego bez
/// dostępu do kodu źródłowego aplikacji.
/// </para>
/// </summary>
[TestFixture]
public class RozdzialGrest_UmowyTest : PracownikTestBase
{
// Pobranie definicji elementu = rodzaju umowy ze słownika konfiguracyjnego po stałej Guid.
private DefinicjaElementu DefUmowy(Guid rodzaj) =>
Place.DefElementow[rodzaj] as DefinicjaElementu;
// Dobiera datę mieszczącą się w okresie aktywnego etatu pracownika (jak w H): koniec miesiąca
// rozpoczęcia etatu, ograniczony do [From, To]. Etaty Demo są zwykle otwarte (To = MaxValue).
private static Date DataWEtacie(Prac pracownik)
{
var okres = pracownik.Last.Etat.Okres;
var from = okres.From;
var koniecMiesiaca = new Date(from.Year, from.Month, 1).AddMonths(1).AddDays(-1);
if (koniecMiesiaca < from) koniecMiesiaca = from;
if (okres.To != Date.MaxValue && koniecMiesiaca > okres.To) koniecMiesiaca = okres.To;
return koniecMiesiaca;
}
// ====================== G3 — Operacja seryjna „Dodaj umowy" dla grupy osób ======================
[Test]
[Description("G3 (wariant B - petla, jak G1): operacja seryjna 'Dodaj umowy' = G1 powtorzone dla " +
"każdej osoby z grupy. Dla każdego pracownika tworzymy Session.AddRow(new Umowa(p)) " +
"z tymi samymi danymi nagłówkowymi (Element, Okres, RodzajRozliczenia, TypWartosci, " +
"Wydzial) i kwotą na umowa.Last.Wartosc. Każda osoba dostaje osobny rekord Umowa.")]
public void G3_DodajUmowySeryjnie_PetlaPoGrupie_TworzyUmoweKazdejOsobie()
{
var defZlecenie = DefUmowy(DefinicjaElementu.UmowaZlecenie);
defZlecenie.Should().NotBeNull("baza Demo zawiera definicję umowy zlecenie");
var okres = new FromTo(new Date(2026, 1, 1), new Date(2026, 12, 31));
var kody = new[] { Pracownik_.Andrzejewski, Pracownik_.Bednarek, Pracownik_.Bujak };
var guidy = new Guid[kody.Length];
InTransaction(() =>
{
for (int i = 0; i < kody.Length; i++)
{
var p = Pracownik(kody[i]);
p.Should().NotBeNull();
p.Umowy.Cast<Umowa>().Should().BeEmpty("pracownik Demo nie ma jeszcze umów");
// Jawne tworzenie jak w G1 — operacja seryjna to to samo powtórzone w pętli.
var umowa = Session.AddRow(new Umowa(p));
umowa.Element = defZlecenie;
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; // jednostka organizacyjna wymagana przy Save
umowa.Last.Wartosc = new Currency(4000m); // kwota na zapisie historycznym
guidy[i] = p.Guid;
}
});
SaveDispose();
// Każda osoba z grupy ma teraz jedną umowę o tych samych danych nagłówkowych.
foreach (var g in guidy)
{
var u = Get<Prac>(g).Umowy.Cast<Umowa>().Single();
// Element to definicja konfiguracyjna — po SaveDispose porównujemy po Guid (inna instancja).
u.Element.Guid.Should().Be(defZlecenie.Guid);
u.Element.RodzajZrodla.Should().Be(RodzajŹródłaWypłaty.Umowa);
u.Tytul.Should().Be("Umowa zlecenie - projekt grupowy");
u.RodzajRozliczenia.Should().Be(RodzajeRozliczeniaUmowy.KwotaDoWypłaty);
u.Okres.From.Should().Be(okres.From);
u.Last.Wartosc.Should().Be(new Currency(4000m));
}
}
[Test]
[Description("G3 (wariant A — worker platformy): Pracownik.DodajUmowęWorker (DataType Pracownik, " +
"ctor przyjmuje Session) z ustawionymi Pracownicy (grupa) i Pars " +
"(DodajUmowęWorker.Params(Context): Element, Okres, Data, Tytuł, RodzajRozliczenia, " +
"TypWartości, Wartość, Wydział). Akcja DodajUmowę() (void) tworzy umowę każdej osobie.")]
public void G3_DodajUmowyWorker_TworzyUmoweKazdejZaznaczonejOsobie()
{
var defZlecenie = DefUmowy(DefinicjaElementu.UmowaZlecenie);
var okres = new FromTo(new Date(2026, 1, 1), new Date(2026, 12, 31));
var osoby = new[]
{
Pracownik(Pracownik_.Andrzejewski),
Pracownik(Pracownik_.Bednarek),
Pracownik(Pracownik_.Bujak),
};
var guidy = osoby.Select(p => p.Guid).ToArray();
foreach (var p in osoby)
p.Umowy.Cast<Umowa>().Should().BeEmpty("pracownik Demo nie ma jeszcze umów");
// Parametry operacji seryjnej — Params(Context) (ContextBase), pola z diakrytykami.
var pars = new Prac.DodajUmowęWorker.Params(Context);
pars.Element = defZlecenie;
pars.Okres = okres;
pars.Data = okres.From;
pars.Tytuł = "Umowa zlecenie - operacja seryjna";
pars.RodzajRozliczenia = RodzajeRozliczeniaUmowy.KwotaDoWypłaty;
pars.TypWartości = TypWartosciUmowy.Brutto;
pars.Wartość = new Currency(3500m);
pars.Wydział = Kadry.Wydzialy.Firma; // wymagany
// Worker przyjmuje Session w konstruktorze; Pracownicy = grupa z zaznaczenia.
var worker = new Prac.DodajUmowęWorker(Session) { Pracownicy = osoby, Pars = pars };
worker.DodajUmowę(); // void — tworzy umowy wszystkim Pracownicy
SaveDispose();
// Każda osoba dostała umowę o danych z Pars.
foreach (var g in guidy)
{
var u = Get<Prac>(g).Umowy.Cast<Umowa>().Single();
u.Element.Guid.Should().Be(defZlecenie.Guid); // porównanie po Guid (inna instancja)
u.Tytul.Should().Be("Umowa zlecenie - operacja seryjna");
u.Okres.From.Should().Be(okres.From);
u.Last.Wartosc.Should().Be(new Currency(3500m));
}
}
// ====================== G4 — Rachunek do umowy (rozliczenie = WyplataUmowa) ======================
[Test]
[Description("G4: 'rachunek do umowy zlecenia' = wyplata WyplataUmowa naliczana mechanizmem plac " +
"(jak H2), NIE rekord w pracownik.Rachunki (to rachunki bankowe). Tworzymy umowę " +
"(G1), potem new NaliczanieSeryjne.Umowy(new UmowaParams(Context)) { Umowa = u }." +
"Nalicz(); wynik to WyplataUmowa (Typ == Umowa). Stan rozliczenia: Umowa.Stan, " +
"Umowa.Splacono, Umowa.Pozostało.")]
public void G4_RachunekDoUmowy_NaliczanieTworzyWyplateUmowa_IZmieniaStan()
{
var pracownik = Pracownik(Pracownik_.Strzelecki);
pracownik.Should().NotBeNull();
var data = DataWEtacie(pracownik);
var okresUmowy = new FromTo(new Date(data.Year, data.Month, 1), data);
// 1) Umowa zlecenie (jak G1) — dane operacyjne tworzymy w trybie edycji.
Guid guidUmowy = Guid.Empty;
InTransaction(() =>
{
var u = Session.AddRow(new Umowa(pracownik));
u.Element = DefUmowy(DefinicjaElementu.UmowaZlecenie);
u.Data = okresUmowy.From;
u.Okres = okresUmowy;
u.Tytul = "Umowa zlecenie - rachunek G4";
u.RodzajRozliczenia = RodzajeRozliczeniaUmowy.KwotaDoWypłaty;
u.TypWartosci = TypWartosciUmowy.Brutto;
u.Wydzial = Kadry.Wydzialy.Firma;
u.Last.Wartosc = new Currency(3000m);
guidUmowy = u.Guid;
});
SaveDispose();
var umowa = Get<Umowa>(guidUmowy);
// Przed rozliczeniem umowa jest niewypłacona.
umowa.Stan.Should().Be(StanUmowy.Niewypłacona, "świeżo dodana umowa nie ma rachunku");
// 2) Rachunek = naliczenie wypłaty z umowy (jak H2). UmowaParams NIE ustawia Naliczanie.
var pars = new NaliczanieSeryjne.UmowaParams(Context);
pars.DataWypłaty = data;
pars.DataListy = pars.DataWypłaty;
// Ustawienie Umowa nadpisuje Pracownik właścicielem umowy. Nalicz() commituje sam.
var naliczanie = new NaliczanieSeryjne.Umowy(pars) { Umowa = umowa };
NaliczanieWypłat wynik = naliczanie.Nalicz();
var wyplaty = wynik.WszystkieWypłaty.Cast<Wyplata>().ToList();
wyplaty.Should().NotBeEmpty("naliczenie umowy tworzy rachunek (WyplataUmowa)");
var w = wyplaty[0];
w.Typ.Should().Be(TypWyplaty.Umowa, "rachunek do umowy to wypłata typu Umowa");
w.Should().BeAssignableTo<WyplataUmowa>("rachunek to konkretny typ WyplataUmowa");
((WyplataUmowa)w).Umowa.Guid.Should().Be(umowa.Guid, "WyplataUmowa wskazuje swoją umowę");
SaveDispose();
// 3) Stan rozliczenia umowy po wystawieniu rachunku.
var umowa2 = Get<Umowa>(guidUmowy);
umowa2.Stan.Should().NotBe(StanUmowy.Niewypłacona,
"po naliczeniu rachunku umowa nie jest już całkowicie niewypłacona");
umowa2.Splacono.Value.Should().BeGreaterThan(0m, "część/całość kwoty została rozliczona");
// Splacono + Pozostało odpowiada modelowi rozliczenia (kwoty Currency).
(umowa2.Splacono.Value + umowa2.Pozostało.Value).Should().BeGreaterThanOrEqualTo(0m);
}
[Test]
[Description("G4 (odczyt): rachunki (wypłaty) wystawione do umowy odczytujemy przez " +
"pracownik.Wyplaty.OfType<WyplataUmowa>().Where(x => x.Umowa == umowa); składniki " +
"rachunku to WypElement (Wartosc). pracownik.Rachunki to rachunki BANKOWE — nie umowy.")]
public void G4_OdczytRachunkowUmowy_PrzezWyplatyUmowa()
{
var pracownik = Pracownik(Pracownik_.Andrzejewski);
var data = DataWEtacie(pracownik);
var okresUmowy = new FromTo(new Date(data.Year, data.Month, 1), data);
Guid guidUmowy = Guid.Empty;
InTransaction(() =>
{
var u = Session.AddRow(new Umowa(pracownik));
u.Element = DefUmowy(DefinicjaElementu.UmowaZlecenie);
u.Data = okresUmowy.From;
u.Okres = okresUmowy;
u.Tytul = "Umowa zlecenie - odczyt rachunków";
u.RodzajRozliczenia = RodzajeRozliczeniaUmowy.KwotaDoWypłaty;
u.TypWartosci = TypWartosciUmowy.Brutto;
u.Wydzial = Kadry.Wydzialy.Firma;
u.Last.Wartosc = new Currency(2500m);
guidUmowy = u.Guid;
});
SaveDispose();
var umowa = Get<Umowa>(guidUmowy);
var pars = new NaliczanieSeryjne.UmowaParams(Context);
pars.DataWypłaty = data;
pars.DataListy = pars.DataWypłaty;
new NaliczanieSeryjne.Umowy(pars) { Umowa = umowa }.Nalicz();
SaveDispose();
var pracownik2 = Pracownik(Pracownik_.Andrzejewski);
var umowa2 = Get<Umowa>(guidUmowy);
// Rachunki = wypłaty z umowy filtrowane po umowie (po Guid, bo różne instancje Row).
var rachunki = pracownik2.Wyplaty.OfType<WyplataUmowa>()
.Where(x => x.Umowa != null && x.Umowa.Guid == umowa2.Guid)
.ToList();
rachunki.Should().NotBeEmpty("wystawiliśmy rachunek do umowy");
foreach (var r in rachunki)
foreach (WypElement e in r.Elementy)
e.Definicja.Should().NotBeNull("każdy składnik rachunku ma definicję elementu");
// Składniki naliczone bezpośrednio z umowy (Umowa.Elementy).
umowa2.Elementy.Cast<WypElement>().Should().NotBeEmpty(
"naliczony rachunek wiąże składniki z umową (Umowa.Elementy)");
}
// ====================== G5 — Zgłoszenia ZUS zleceniobiorców (ZUA / ZZA / ZWUA) ======================
[Test]
[Description("G5 (schemat ubezpieczeń): typ zgłoszenia (ZUA vs ZZA) wynika ze schematu " +
"UmowaHistoria.Ubezpieczenia (umowa.Last.Ubezpieczenia), nie z parametru workera. " +
"ZUA = społeczne obowiązkowe (Emerytalne/Rentowe) + zdrowotne; Tyub4 pobierany ze " +
"słownika konfiguracyjnego Kadry.TytulyUbezpiecz4. Spoleczne.Od jest read-only — " +
"datę objęcia ustawiamy zbiorczo przez Ubezpieczenia.ObowiazkoweOd.")]
public void G5_SchematUbezpieczenUmowy_ZUA_SpoleczneObowiazkoweIZdrowotne()
{
var pracownik = Pracownik(Pracownik_.Bednarek);
var okres = new FromTo(new Date(2026, 1, 1), new Date(2026, 12, 31));
// Tytuł ubezpieczenia zleceniobiorcy pobieramy DYNAMICZNIE ze słownika (nie tworzymy w locie).
var tyub4 = Kadry.TytulyUbezpiecz4.Cast<TytulUbezpieczenia4>().FirstOrDefault();
tyub4.Should().NotBeNull("baza Demo zawiera słownik tytułów ubezpieczenia (TytulyUbezpiecz4)");
Guid guidUmowy = Guid.Empty;
InTransaction(() =>
{
var u = Session.AddRow(new Umowa(pracownik));
u.Element = DefUmowy(DefinicjaElementu.UmowaZlecenie);
u.Data = okres.From;
u.Okres = okres;
u.Tytul = "Umowa zlecenie - ZUA";
u.RodzajRozliczenia = RodzajeRozliczeniaUmowy.KwotaDoWypłaty;
u.TypWartosci = TypWartosciUmowy.Brutto;
u.Wydzial = Kadry.Wydzialy.Firma;
u.Last.Wartosc = new Currency(4000m);
// Schemat ubezpieczeń umowy (historyczny) — ZUA: społeczne obowiązkowe + zdrowotne.
var ub = u.Last.Ubezpieczenia;
ub.Tyub4 = tyub4;
ub.ObowiazkoweOd = okres.From; // data objęcia społecznymi obowiązkowymi
ub.Emerytalne.Obowiazkowe = true;
ub.Rentowe.Obowiazkowe = true;
ub.Zdrowotne.ObowiazkoweOd = okres.From; // na Zdrowotne ObowiazkoweOd jest zapisywalne
guidUmowy = u.Guid;
});
SaveDispose();
var umowa = Get<Umowa>(guidUmowy);
var ub2 = umowa.Last.Ubezpieczenia;
ub2.Tyub4.Should().NotBeNull("tytuł ubezpieczenia zapisany na schemacie umowy");
ub2.Emerytalne.Obowiazkowe.Should().BeTrue("ZUA: społeczne obowiązkowe (emerytalne)");
ub2.Rentowe.Obowiazkowe.Should().BeTrue("ZUA: społeczne obowiązkowe (rentowe)");
ub2.Zdrowotne.ObowiazkoweOd.Should().Be(okres.From, "ZUA obejmuje też zdrowotne");
// Schemat ubezpieczeń umowy leży na zapisie historycznym (delegat umowa.Ubezpieczenia).
umowa.Ubezpieczenia.Should().NotBeNull("Umowa.Ubezpieczenia to delegat do Last.Ubezpieczenia");
}
[Test]
[Description("G5 (ZZA): zleceniobiorca podlegający TYLKO zdrowotnemu (np. uczeń/student/zbieg " +
"tytułów) → ZZA. Na schemacie UmowaHistoria.Ubezpieczenia zostawiamy Emerytalne/" +
"Rentowe.Obowiazkowe = false, ustawiamy tylko Zdrowotne.ObowiazkoweOd.")]
public void G5_SchematUbezpieczenUmowy_ZZA_TylkoZdrowotne()
{
var pracownik = Pracownik(Pracownik_.Bujak);
var okres = new FromTo(new Date(2026, 1, 1), new Date(2026, 6, 30));
var tyub4 = Kadry.TytulyUbezpiecz4.Cast<TytulUbezpieczenia4>().FirstOrDefault();
Guid guidUmowy = Guid.Empty;
InTransaction(() =>
{
var u = Session.AddRow(new Umowa(pracownik));
u.Element = DefUmowy(DefinicjaElementu.UmowaZlecenie);
u.Data = okres.From;
u.Okres = okres;
u.Tytul = "Umowa zlecenie - ZZA";
u.RodzajRozliczenia = RodzajeRozliczeniaUmowy.KwotaDoWypłaty;
u.TypWartosci = TypWartosciUmowy.Brutto;
u.Wydzial = Kadry.Wydzialy.Firma;
u.Last.Wartosc = new Currency(2000m);
var ub = u.Last.Ubezpieczenia;
ub.Tyub4 = tyub4;
// ZZA: brak społecznych obowiązkowych, tylko zdrowotne.
// UWAGA: domyślnie umowa zlecenie ma Emerytalne/Rentowe.Obowiazkowe = true (schemat ZUA);
// dla ZZA trzeba je JAWNIE wyłączyć — samo ustawienie zdrowotnego nie wystarcza.
ub.Emerytalne.Obowiazkowe = false;
ub.Rentowe.Obowiazkowe = false;
ub.Zdrowotne.ObowiazkoweOd = okres.From;
guidUmowy = u.Guid;
});
SaveDispose();
var ub2 = Get<Umowa>(guidUmowy).Last.Ubezpieczenia;
ub2.Emerytalne.Obowiazkowe.Should().BeFalse("ZZA: brak społecznych obowiązkowych (emerytalne)");
ub2.Rentowe.Obowiazkowe.Should().BeFalse("ZZA: brak społecznych obowiązkowych (rentowe)");
ub2.Zdrowotne.ObowiazkoweOd.Should().Be(okres.From, "ZZA: tylko zdrowotne");
}
[Test]
[Ignore("Generowanie zgłoszenia ZUA/ZZA workerem ZarejestrujUmowyWorker.Rejestracja wymaga " +
"kompletnej konfiguracji płatnika/KEDU i kontekstu deklaracji ZUS, niedostępnego w " +
"izolowanym środowisku testów Demo (bez sieci). Dokumentujemy kontrakt workera bez " +
"uruchamiania generowania (ZarejestrujUmowy() / WyrejestrujUmowy()).")]
[Description("G5 (worker — kontrakt): Soneta.Deklaracje.ZUS.ZarejestrujUmowyWorker (DataType " +
"Umowa, ctor bezparametrowy, Umowy: Umowa[]). Zgłoszenie: zagnieżdżona Rejestracja " +
"(Pars: ParamsZ — Okres, DataDokumentu, DataWypełnienia, ZarejestrujRodzinę) i akcja " +
"ZarejestrujUmowy(): object generująca ZUA/ZZA wg schematu ubezpieczeń umowy. " +
"Wyrejestrowanie analogicznie WyrejestrujUmowy() → ZWUA. KEDU/wysyłka → sieć.")]
public void G5_ZgloszenieZUS_Worker_KontraktBezGenerowania()
{
var pracownik = Pracownik(Pracownik_.Bednarek);
var okres = new FromTo(new Date(2026, 1, 1), new Date(2026, 12, 31));
Guid guidUmowy = Guid.Empty;
InTransaction(() =>
{
var u = Session.AddRow(new Umowa(pracownik));
u.Element = DefUmowy(DefinicjaElementu.UmowaZlecenie);
u.Data = okres.From;
u.Okres = okres;
u.Tytul = "Umowa zlecenie - zgłoszenie ZUS";
u.RodzajRozliczenia = RodzajeRozliczeniaUmowy.KwotaDoWypłaty;
u.TypWartosci = TypWartosciUmowy.Brutto;
u.Wydzial = Kadry.Wydzialy.Firma;
u.Last.Wartosc = new Currency(4000m);
guidUmowy = u.Guid;
});
SaveDispose();
var umowa = Get<Umowa>(guidUmowy);
// Worker zgłoszeniowy na typie Umowa — operuje na zaznaczonych umowach.
// Uwaga: Umowy oraz Pars są write-only (set-only) — przekazujemy je przez inicjalizator/setter.
var worker = new Soneta.Deklaracje.ZUS.ZarejestrujUmowyWorker { Umowy = new[] { umowa } };
// Parametry zgłoszenia: ParamsZ(Context) — bazowe Okres/DataDokumentu/DataWypełnienia ustawiane
// na wspólnym kontrakcie ZarejestrujBaseWorker (ParamsZ przekazujemy jako Pars do Rejestracji).
var pars = new Soneta.Deklaracje.ZUS.ZarejestrujBaseWorker.ParamsZ(Context);
pars.ZarejestrujRodzinę = false;
var rejestracja = new Soneta.Deklaracje.ZUS.ZarejestrujUmowyWorker.Rejestracja { Pars = pars };
// Generowanie (ZUA/ZZA wg schematu ubezpieczeń) — wymaga kontekstu deklaracji/KEDU:
rejestracja.ZarejestrujUmowy();
SaveDispose();
}
}
@@ -0,0 +1,262 @@
using System;
using System.Linq;
using System.Text;
using AwesomeAssertions;
using NUnit.Framework;
using Soneta.Kadry;
using Soneta.Place;
using Soneta.Types;
using Prac = Soneta.Kadry.Pracownik;
namespace Soneta.Skills.Test.KadryPlace.Pracownik;
/// <summary>
/// Rozdział H — „Płace: naliczanie wypłat" (receptury H1, H2, H3, H4).
/// <para>
/// Testy są <b>wykonywalną dokumentacją</b> publicznego kontraktu naliczania płac w Soneta.
/// Naliczanie realizuje worker <c>Soneta.Place.NaliczanieSeryjne</c> z zagnieżdżonymi klasami
/// parametrów (<c>PracownikParams</c>, <c>UmowaParams</c>) oraz wykonawców
/// (<c>NaliczanieSeryjne.Pracownika</c>, <c>NaliczanieSeryjne.Umowy</c>). Wynikiem jest
/// <c>NaliczanieWypłat</c> z kolekcją <c>WszystkieWypłaty: IList</c> (elementy <c>Wyplata</c>)
/// oraz <c>Nienaliczeni</c> (powody niepowodzenia). <c>Nalicz()</c> sam otwiera i commituje
/// transakcję w sesji — nie owijamy go w dodatkową transakcję.
/// </para>
/// <para>
/// Wszystko działa na bazie Demo (GoldStandard) z automatycznym rollbackiem po teście.
/// Pracownik "006" ma jeden zapis historii — datę wypłaty dobieramy dynamicznie tak, by mieściła
/// się w okresie aktywnego etatu (<c>pracownik.Last.Etat.Okres</c>). Operujemy wyłącznie na
/// <b>publicznym kontrakcie</b> platformy.
/// </para>
/// </summary>
[TestFixture]
public class RozdzialH_WyplatyTest : PracownikTestBase
{
// Dobiera datę wypłaty mieszczącą się w okresie etatu pracownika: bierzemy ostatni dzień
// miesiąca początku etatu, ale nie wcześniej niż From i nie później niż To okresu etatu.
// Dla pracowników Demo etat zwykle zaczyna się wiele lat wstecz i jest otwarty (To = MaxValue),
// więc bezpieczną, deterministyczną datą jest koniec miesiąca rozpoczęcia zatrudnienia.
private static Date DataWyplatyWEtacie(Prac pracownik)
{
var okres = pracownik.Last.Etat.Okres;
var from = okres.From;
// Koniec miesiąca rozpoczęcia etatu (28-31 dzień).
var koniecMiesiaca = new Date(from.Year, from.Month, 1).AddMonths(1).AddDays(-1);
if (koniecMiesiaca < from) koniecMiesiaca = from;
if (okres.To != Date.MaxValue && koniecMiesiaca > okres.To) koniecMiesiaca = okres.To;
return koniecMiesiaca;
}
// Diagnostyka: zbiera powody niepoliczenia (Nienaliczeni) do czytelnego komunikatu asercji.
private static string OpisNienaliczonych(NaliczanieWypłat wynik)
{
if (wynik.Nienaliczeni == null) return "(brak kolekcji Nienaliczeni)";
var sb = new StringBuilder();
foreach (var b in wynik.Nienaliczeni)
sb.Append(b).Append(" | ");
return sb.Length == 0 ? "(brak nienaliczonych)" : sb.ToString();
}
// ============================== H1 — Naliczanie wypłat etatowych ==============================
[Test]
[Description("H1: wypłatę etatową naliczamy workerem NaliczanieSeryjne. Parametry: " +
"new NaliczanieSeryjne.PracownikParams(Context); DataWypłaty (ustawia Okres i " +
"MiesiącDeklaracji automatycznie); DataListy; TypWypłaty = Etat. NIE ustawiamy " +
"Naliczanie (domyślnie PłatnaZDołu). Wykonawca: new NaliczanieSeryjne.Pracownika(pars) " +
"{ Pracownik = p }.Nalicz() — sam commituje w sesji. Wynik: WszystkieWypłaty (IList).")]
public void H1_WyplataEtatowa_NaliczanaWorkeremNaliczanieSeryjne()
{
var pracownik = Pracownik(Pracownik_.Andrzejewski);
pracownik.Should().NotBeNull();
// Datę wypłaty dobieramy w obrębie aktywnego etatu pracownika.
var dataWyplaty = DataWyplatyWEtacie(pracownik);
// Parametry naliczania — Context z tej samej sesji co pracownik (TestBase.Context).
var pars = new NaliczanieSeryjne.PracownikParams(Context);
pars.DataWypłaty = dataWyplaty; // ustawia Okres i MiesiącDeklaracji automatycznie
pars.DataListy = pars.DataWypłaty;
// pars.Naliczanie pozostaje domyślnie PłatnaZDołu (setter rzuca bez licencji PL Złoty).
pars.TypWypłaty = TypWyplaty.Etat; // tylko wypłaty etatowe
// Nalicz() otwiera własną transakcję i commituje — nie owijamy w InTransaction.
var naliczanie = new NaliczanieSeryjne.Pracownika(pars) { Pracownik = pracownik };
NaliczanieWypłat wynik = naliczanie.Nalicz();
// Diagnostyka: jeśli nic nie naliczono, powód jest w Nienaliczeni.
var wyplaty = wynik.WszystkieWypłaty.Cast<Wyplata>().ToList();
wyplaty.Should().NotBeEmpty(
"naliczanie etatu dla pracownika Demo w okresie etatu powinno dać wypłatę; " +
$"data={dataWyplaty}, nienaliczeni: {OpisNienaliczonych(wynik)}");
// Naliczona wypłata jest typu etatowego i wiąże się z pracownikiem.
var w = wyplaty[0];
w.Typ.Should().Be(TypWyplaty.Etat, "filtr TypWypłaty = Etat");
w.Pracownik.Should().Be(pracownik);
w.Data.Should().Be(dataWyplaty, "data wypłaty wg DataWypłaty parametrów");
SaveDispose(); // utrwalenie w bazie (rollback po teście i tak wycofa)
}
// ============================== H2 — Naliczanie wypłat z umów ==============================
[Test]
[Description("H2: wypłatę z umowy cywilnoprawnej naliczamy wykonawcą NaliczanieSeryjne.Umowy. " +
"Najpierw tworzymy umowę zlecenie (jak w G1), potem: " +
"new NaliczanieSeryjne.Umowy(new UmowaParams(Context)) { Umowa = u }.Nalicz(). " +
"Ustawienie Umowa nadpisuje Pracownik. NIE ustawiamy UmowaParams.Naliczanie " +
"(setter rzuca NotSupportedException — umowy zawsze płatne z dołu).")]
public void H2_WyplataZUmowy_NaliczanaWykonawcaUmowy()
{
var pracownik = Pracownik(Pracownik_.Bednarek);
pracownik.Should().NotBeNull();
// Datę wypłaty (i okres umowy) dobieramy w obrębie aktywnego etatu pracownika.
var dataWyplaty = DataWyplatyWEtacie(pracownik);
var okresUmowy = new FromTo(new Date(dataWyplaty.Year, dataWyplaty.Month, 1), dataWyplaty);
// 1) Tworzymy umowę zlecenie (mechanizm jak w sekcji G) — tworzenie danych operacyjnych
// MUSI być w trybie edycji (InTransaction), inaczej AddRow rzuca CannotEditException.
Guid guidUmowy = Guid.Empty;
InTransaction(() =>
{
var u = Session.AddRow(new Umowa(pracownik));
u.Element = Place.DefElementow[DefinicjaElementu.UmowaZlecenie] as DefinicjaElementu;
u.Data = okresUmowy.From;
u.Okres = okresUmowy;
u.Tytul = "Umowa zlecenie - naliczanie H2";
u.RodzajRozliczenia = RodzajeRozliczeniaUmowy.KwotaDoWypłaty;
u.TypWartosci = TypWartosciUmowy.Brutto;
u.Wydzial = Kadry.Wydzialy.Firma; // jednostka organizacyjna wymagana przy zapisie
u.Last.Wartosc = new Currency(3000m); // kwota na zapisie historycznym
guidUmowy = u.Guid;
});
SaveDispose(); // utrwalamy umowę przed naliczaniem
var umowa = Get<Umowa>(guidUmowy);
// 2) Naliczanie wypłaty z umowy.
var pars = new NaliczanieSeryjne.UmowaParams(Context);
pars.DataWypłaty = dataWyplaty;
pars.DataListy = pars.DataWypłaty;
// pars.Naliczanie NIE jest ustawiane (NotSupportedException).
var naliczanie = new NaliczanieSeryjne.Umowy(pars) { Umowa = umowa };
NaliczanieWypłat wynik = naliczanie.Nalicz();
var wyplaty = wynik.WszystkieWypłaty.Cast<Wyplata>().ToList();
wyplaty.Should().NotBeEmpty(
"naliczanie umowy zlecenie powinno dać wypłatę typu Umowa; " +
$"data={dataWyplaty}, nienaliczeni: {OpisNienaliczonych(wynik)}");
var w = wyplaty[0];
w.Typ.Should().Be(TypWyplaty.Umowa, "wypłata z umowy ma typ Umowa");
// Porównujemy po Guid (różne instancje Row po SaveDispose/re-fetch).
w.Pracownik.Guid.Should().Be(pracownik.Guid,
"ustawienie Umowa nadpisuje Pracownik na właściciela umowy");
SaveDispose();
}
// ============================== H3 — Naliczanie pozostałych wypłat ==============================
[Test]
[Description("H3: pozostałe wypłaty naliczamy tym samym wykonawcą co etat " +
"(NaliczanieSeryjne.Pracownika), sterując PracownikParams.TypWypłaty = Inne. " +
"Opcjonalnie PracownikParams.Dodatek = DefinicjaElementu zawęża do jednego składnika. " +
"Wynik czytamy przez Wyplata.Elementy (WypElement: Definicja, Nazwa, Wartosc).")]
public void H3_PozostaleWyplaty_TypWyplatyInne()
{
var pracownik = Pracownik(Pracownik_.Bujak);
pracownik.Should().NotBeNull();
var dataWyplaty = DataWyplatyWEtacie(pracownik);
var pars = new NaliczanieSeryjne.PracownikParams(Context);
pars.DataWypłaty = dataWyplaty;
pars.DataListy = pars.DataWypłaty;
pars.TypWypłaty = TypWyplaty.Inne; // tylko pozostałe składniki (bez etatu)
var naliczanie = new NaliczanieSeryjne.Pracownika(pars) { Pracownik = pracownik };
NaliczanieWypłat wynik = naliczanie.Nalicz();
// Pracownik Demo bez dodatkowych składników "Inne" może nie mieć nic do naliczenia —
// to poprawne zachowanie (puste WszystkieWypłaty, BEZ wyjątku i bez Nienaliczonych-błędów).
// Dokumentujemy więc kontrakt: naliczanie zwraca obiekt wyniku, a wszelkie naliczone
// wypłaty są typu Inne. Asercja nie wymaga niepustego wyniku (zależy od danych pracownika).
wynik.Should().NotBeNull("Nalicz() zawsze zwraca obiekt NaliczanieWypłat");
var wyplaty = wynik.WszystkieWypłaty.Cast<Wyplata>().ToList();
foreach (var w in wyplaty)
{
w.Typ.Should().Be(TypWyplaty.Inne, "filtr TypWypłaty = Inne");
// Składniki wynagrodzenia: WypElement (Definicja, Nazwa, Wartosc).
foreach (WypElement e in w.Elementy)
{
e.Definicja.Should().NotBeNull("każdy składnik ma definicję elementu");
}
}
SaveDispose();
}
// ============================== H4 — Odczyt wypłat za rok ==============================
[Test]
[Description("H4: po naliczeniu wypłaty etatowej (H1) odczytujemy wypłaty pracownika za rok " +
"filtrem serwerowym pracownik.Wyplaty[(Wyplata x) => x.Data >= od && x.Data <= doD]. " +
"Sumujemy Wartosc (Currency, kwota do wypłaty) oraz składniki Elementy " +
"(WypElement.Wartosc/.Netto, decimal). UWAGA: WyplataEtat nie ma CLR-property " +
"Brutto/Netto (wbrew dokumentacji) — agregujemy przez Wartosc i składniki Elementy.")]
public void H4_OdczytWyplatZaRok_FiltrSerwerowyPoDacie()
{
var pracownik = Pracownik(Pracownik_.Strzelecki);
pracownik.Should().NotBeNull();
var dataWyplaty = DataWyplatyWEtacie(pracownik);
// Najpierw nalicz wypłatę etatową, by mieć co odczytywać (H1 jako warunek wstępny H4).
var pars = new NaliczanieSeryjne.PracownikParams(Context);
pars.DataWypłaty = dataWyplaty;
pars.DataListy = pars.DataWypłaty;
pars.TypWypłaty = TypWyplaty.Etat;
var naliczanie = new NaliczanieSeryjne.Pracownika(pars) { Pracownik = pracownik };
var wynikNaliczania = naliczanie.Nalicz();
wynikNaliczania.WszystkieWypłaty.Cast<Wyplata>().Should().NotBeEmpty(
$"warunek wstępny H4: wypłata etatowa musi się naliczyć; data={dataWyplaty}, " +
$"nienaliczeni: {OpisNienaliczonych(wynikNaliczania)}");
SaveDispose();
// Odczyt: filtr serwerowy po dacie wypłaty (cały rok), bez pełnego skanu tabeli operacyjnej.
int rok = dataWyplaty.Year;
var od = new Date(rok, 1, 1);
var doD = new Date(rok, 12, 31);
var pracownik2 = Pracownik(Pracownik_.Strzelecki);
var wyplaty = pracownik2.Wyplaty[(Wyplata x) => x.Data >= od && x.Data <= doD]
.Cast<Wyplata>().ToList();
wyplaty.Should().NotBeEmpty("po naliczeniu wypłata mieści się w roku odczytu");
// Agregacja: suma do wypłaty (Currency.Value -> decimal) i suma składników.
decimal sumaDoWyplaty = 0m;
decimal sumaSkladnikow = 0m;
bool maEtat = false;
foreach (var w in wyplaty)
{
sumaDoWyplaty += w.Wartosc.Value; // kwota do wypłaty; Currency.Value -> decimal
if (w is WyplataEtat) // typ etatowy (agregatów Brutto/Netto brak na CLR)
maEtat = true;
foreach (WypElement e in w.Elementy)
sumaSkladnikow += e.Wartosc; // wartość składnika (decimal)
}
maEtat.Should().BeTrue("naliczyliśmy wypłatę etatową (WyplataEtat)");
sumaSkladnikow.Should().NotBe(0m, "wypłata zawiera składniki (Elementy)");
sumaDoWyplaty.Should().BeGreaterThan(0m, "kwota do wypłaty jest dodatnia");
}
}
@@ -0,0 +1,445 @@
using System;
using System.Linq;
using System.Text;
using AwesomeAssertions;
using NUnit.Framework;
using Soneta.Kadry;
using Soneta.Place;
using Soneta.Types;
using Prac = Soneta.Kadry.Pracownik;
namespace Soneta.Skills.Test.KadryPlace.Pracownik;
/// <summary>
/// Rozdział H (część rozszerzona) — „Płace: odczyt i operacje na naliczonych wypłatach"
/// (receptury H5H11).
/// <para>
/// Każdy test najpierw nalicza wypłatę etatową pracownika Demo workerem
/// <c>Soneta.Place.NaliczanieSeryjne</c> (wzorzec z H1: <c>PracownikParams(Context)</c> +
/// <c>DataWypłaty</c> w okresie etatu + <c>Nalicz()</c>), a następnie odczytuje elementy
/// (<c>Wyplata.Elementy</c> / <c>WypElement.Podatki</c>) albo wykonuje operację publicznym
/// workerem płacowym (zaliczka, przeliczenie podatków, dochód, storno, bufor).
/// </para>
/// <para>
/// Testy operują wyłącznie na <b>publicznym kontrakcie</b> platformy (jak dodatek programisty
/// zewnętrznego) i na bazie Demo (GoldStandard) z automatycznym rollbackiem po teście.
/// Nie ustawiamy <c>PracownikParams.Naliczanie</c> (setter rzuca bez licencji „PL Złoty").
/// </para>
/// </summary>
[TestFixture]
public class RozdzialHrest_WyplatyTest : PracownikTestBase
{
// ====================================================================================
// Helpery wspólne (skopiowane z RozdzialH_WyplatyTest — ten sam, sprawdzony wzorzec H1).
// ====================================================================================
// Dobiera datę wypłaty mieszczącą się w okresie etatu pracownika: koniec miesiąca początku
// etatu, nie wcześniej niż From i nie później niż To okresu etatu.
private static Date DataWyplatyWEtacie(Prac pracownik)
{
var okres = pracownik.Last.Etat.Okres;
var from = okres.From;
var koniecMiesiaca = new Date(from.Year, from.Month, 1).AddMonths(1).AddDays(-1);
if (koniecMiesiaca < from) koniecMiesiaca = from;
if (okres.To != Date.MaxValue && koniecMiesiaca > okres.To) koniecMiesiaca = okres.To;
return koniecMiesiaca;
}
// Diagnostyka: powody niepoliczenia (Nienaliczeni) w czytelnym komunikacie asercji.
private static string OpisNienaliczonych(NaliczanieWypłat wynik)
{
if (wynik.Nienaliczeni == null) return "(brak kolekcji Nienaliczeni)";
var sb = new StringBuilder();
foreach (var b in wynik.Nienaliczeni)
sb.Append(b).Append(" | ");
return sb.Length == 0 ? "(brak nienaliczonych)" : sb.ToString();
}
// Nalicza pojedynczą wypłatę etatową pracownika (wzorzec H1) i zwraca pierwszą wypłatę.
// Nalicz() otwiera i commituje własną transakcję — nie owijamy w InTransaction.
private Wyplata NaliczWyplateEtatowa(Prac pracownik, Date dataWyplaty)
{
var pars = new NaliczanieSeryjne.PracownikParams(Context);
pars.DataWypłaty = dataWyplaty; // ustawia Okres i MiesiącDeklaracji automatycznie
pars.DataListy = pars.DataWypłaty;
pars.TypWypłaty = TypWyplaty.Etat;
var naliczanie = new NaliczanieSeryjne.Pracownika(pars) { Pracownik = pracownik };
NaliczanieWypłat wynik = naliczanie.Nalicz();
var wyplaty = wynik.WszystkieWypłaty.Cast<Wyplata>().ToList();
wyplaty.Should().NotBeEmpty(
"naliczenie etatu pracownika Demo w okresie etatu powinno dać wypłatę; " +
$"data={dataWyplaty}, nienaliczeni: {OpisNienaliczonych(wynik)}");
return wyplaty[0];
}
// ====================================================================================
// H5 — Odczyt elementów wypłaty (brutto/składki/podatek/netto)
// ====================================================================================
[Test]
[Description("H5: składniki naliczonej wypłaty czytamy z Wyplata.Elementy (WypElement). " +
"Pola elementu: Wartosc/Netto/DoWypłaty (decimal), Podatki (subrow Podatki). " +
"Podatki: ZalFIS (zaliczka PIT), Emerytalna/Rentowa/Chorobowa/Zdrowotna (SkladkaZUS " +
"z polami Prac/Firma). Agregaty liczymy ręcznie z elementów; Wyplata.Wartosc to " +
"Currency (kwota do wypłaty) -> .Value na decimal.")]
public void H5_OdczytElementowWyplaty_WartoscNettoPodatki()
{
var pracownik = Pracownik(Pracownik_.Andrzejewski);
pracownik.Should().NotBeNull();
var dataWyplaty = DataWyplatyWEtacie(pracownik);
var wyplata = NaliczWyplateEtatowa(pracownik, dataWyplaty);
// Składniki muszą istnieć (wypłata etatowa zawsze ma elementy wynagrodzenia).
var elementy = wyplata.Elementy.Cast<WypElement>().ToList();
elementy.Should().NotBeEmpty("naliczona wypłata etatowa zawiera składniki Elementy");
// Ręczna agregacja z elementów (wzorzec z dokumentacji H5).
decimal brutto = 0m, netto = 0m, zalPit = 0m, zusPrac = 0m, zusFirma = 0m;
foreach (WypElement e in elementy)
{
e.Definicja.Should().NotBeNull("każdy składnik ma definicję elementu");
brutto += e.Wartosc; // decimal — wartość brutto składnika
netto += e.Netto; // decimal — wartość netto składnika
// Struktura podatkowo-składkowa elementu.
Podatki p = e.Podatki;
p.Should().NotBeNull("WypElement ma subrow Podatki");
zalPit += p.ZalFIS; // zaliczka PIT (fiskus)
// SkladkaZUS: Prac = część pracownika, Firma = część pracodawcy.
zusPrac += p.Emerytalna.Prac + p.Rentowa.Prac + p.Chorobowa.Prac + p.Zdrowotna.Prac;
zusFirma += p.Emerytalna.Firma + p.Rentowa.Firma + p.Wypadkowa.Firma;
}
decimal doWyplaty = wyplata.Wartosc.Value; // Currency -> decimal
brutto.Should().BeGreaterThan(0m, "wypłata etatowa ma dodatni przychód brutto");
netto.Should().BeGreaterThan(0m, "wypłata etatowa ma dodatnie netto");
zusPrac.Should().BeGreaterThan(0m, "od wynagrodzenia etatowego naliczane są składki pracownika");
zusFirma.Should().BeGreaterThan(0m, "pracodawca opłaca część składek (narzuty)");
doWyplaty.Should().BeGreaterThan(0m, "kwota do wypłaty jest dodatnia");
// Zaliczka PIT bywa 0 (np. niska podstawa / ulgi) — sprawdzamy tylko brak ujemności.
zalPit.Should().BeGreaterThanOrEqualTo(0m, "zaliczka PIT nie jest ujemna");
SaveDispose();
}
[Test]
[Description("H5 (worker-agregator): Wyplata.PITInfoWorker (publiczny, [Context] Wypłata) udostępnia " +
"gotowe sumy: DoOpodatkowania/Nieopodatkowane (Currency), Razem/NettoRazem/SkładkiZUS/" +
"SkładkaZdrow/ZalFIS (decimal). Używamy zamiast ręcznej agregacji elementów.")]
public void H5_PITInfoWorker_GotoweAgregaty()
{
var pracownik = Pracownik(Pracownik_.Bednarek);
pracownik.Should().NotBeNull();
var dataWyplaty = DataWyplatyWEtacie(pracownik);
var wyplata = NaliczWyplateEtatowa(pracownik, dataWyplaty);
// Worker-agregator wypłaty — przypinamy wypłatę przez property Wypłata.
var pit = new Wyplata.PITInfoWorker { Wypłata = wyplata };
decimal razem = pit.Razem; // przychód razem (opodatkowane + nieopodatkowane)
decimal nettoRazem = pit.NettoRazem; // wynagrodzenie netto razem
decimal zus = pit.SkładkiZUS; // składki ZUS pracownika
decimal zaliczka = pit.ZalFIS; // zaliczka PIT
razem.Should().BeGreaterThan(0m, "przychód razem wypłaty etatowej jest dodatni");
nettoRazem.Should().BeGreaterThan(0m, "netto razem jest dodatnie");
nettoRazem.Should().BeLessThanOrEqualTo(razem, "netto nie przekracza przychodu brutto");
zus.Should().BeGreaterThan(0m, "od etatu naliczane są składki ZUS pracownika");
zaliczka.Should().BeGreaterThanOrEqualTo(0m, "zaliczka PIT nie jest ujemna");
// DoOpodatkowania to Currency — konwersja przez .Value.
pit.DoOpodatkowania.Value.Should().BeGreaterThan(0m, "podstawa opodatkowania dodatnia");
SaveDispose();
}
// ====================================================================================
// H6 — Wypłata zaliczki (worker WypłaćZaliczkęWorker)
// ====================================================================================
[Test]
[Description("H6: zaliczkę wypłacamy publicznym workerem WypłaćZaliczkęWorker. Parametry: " +
"ZalParams(Context) { Data, Kwota } + ZalParams.Definicja (z WypElement.Params) — " +
"ISTNIEJĄCA definicja elementu z place.DefElementow o RodzajZrodla == RodzajŹródłaWypłaty.Zaliczka; " +
"Pracownicy: Pracownik[]. " +
"Akcja WypłataZaliczki() tworzy rekord Zaliczka i nalicza element realizacji; otwiera " +
"własną transakcję. Brak definicji zaliczki w Demo => Ignore (kontrakt workera udokumentowany).")]
public void H6_WyplataZaliczki_WorkerWyplacZaliczke()
{
var pracownik = Pracownik(Pracownik_.Bujak);
pracownik.Should().NotBeNull();
// Worker wymaga ISTNIEJĄCEJ definicji elementu typu zaliczka — identyfikujemy ją po publicznym
// dyskryminatorze DefinicjaElementu.RodzajZrodla == RodzajŹródłaWypłaty.Zaliczka (brak stałej
// DefinicjaElementu.* dla zaliczki). Sam Kod/Nazwa nie wystarcza (np. „Korekta zaliczki podatku"
// ma RodzajZrodla == Dodatek i worker odrzuca takie podstawienie).
DefinicjaElementu defZaliczki = Place.DefElementow.Cast<DefinicjaElementu>()
.FirstOrDefault(d => d.RodzajZrodla == RodzajŹródłaWypłaty.Zaliczka);
if (defZaliczki == null)
Assert.Ignore("Baza Demo nie zawiera definicji elementu typu zaliczka — " +
"worker WypłaćZaliczkęWorker wymaga istniejącej DefinicjaElementu (ZalParams.Definicja). " +
"Kontrakt workera udokumentowany w H6.");
var dataWyplaty = DataWyplatyWEtacie(pracownik);
var pars = new WypłaćZaliczkęWorker.ZalParams(Context)
{
Data = dataWyplaty,
Kwota = new Currency(1000m),
};
pars.Definicja = defZaliczki; // z bazowej WypElement.Params
var worker = new WypłaćZaliczkęWorker { Params = pars, Pracownicy = new[] { pracownik } };
object wynik = worker.WypłataZaliczki(); // tworzy Zaliczka + nalicza; własna transakcja
wynik.Should().NotBeNull("akcja WypłataZaliczki zwraca obiekt wyniku");
SaveDispose();
// Po wypłaceniu zaliczki pracownik ma rekord Zaliczka z dodatnią wartością.
var zaliczki = Place.Zaliczki.Cast<Zaliczka>()
.Where(z => z.Pracownik != null && z.Pracownik.Guid == pracownik.Guid)
.ToList();
zaliczki.Should().NotBeEmpty("worker utworzył rekord Zaliczka dla pracownika");
zaliczki.Should().Contain(z => z.Wartosc.Value > 0m, "zaliczka ma dodatnią wartość");
}
// ====================================================================================
// H7 — Przelicz składki ZUS i podatki (worker NaliczaniePodatkówMiesięcznie)
// ====================================================================================
[Test]
[Description("H7: ponowne przeliczenie składek ZUS i zaliczek PIT na elementach wypłat z bufora " +
"za dany miesiąc deklaracji realizuje publiczny worker NaliczaniePodatkówMiesięcznie. " +
"ctor przyjmuje YearMonth (miesiąc deklaracji); property Pracownik [Context]; akcja " +
"PrzeliczPodatki() działa we własnej transakcji. Przelicza tylko elementy z bufora " +
"(Wyplata.Bufor) bez ręcznej korekty podatków.")]
public void H7_PrzeliczPodatki_WorkerNaliczaniePodatkowMiesiecznie()
{
var pracownik = Pracownik(Pracownik_.Strzelecki);
pracownik.Should().NotBeNull();
var dataWyplaty = DataWyplatyWEtacie(pracownik);
// Wypłata w buforze (świeżo naliczona, niezatwierdzona) — przeliczalna.
var wyplata = NaliczWyplateEtatowa(pracownik, dataWyplaty);
wyplata.Bufor.Should().BeTrue("świeżo naliczona wypłata jest w buforze");
// Miesiąc deklaracji = miesiąc daty wypłaty.
var miesiac = new YearMonth(dataWyplaty.Year, dataWyplaty.Month);
// Sumy zaliczki PIT przed przeliczeniem (powinny być stabilne — brak zmian danych kadrowych).
decimal zalPrzed = new Wyplata.PITInfoWorker { Wypłata = wyplata }.ZalFIS;
var worker = new NaliczaniePodatkówMiesięcznie(miesiac) { Pracownik = pracownik };
worker.PrzeliczPodatki(); // przelicza składki ZUS i zaliczki PIT; własna transakcja
SaveDispose();
// Po przeliczeniu odczytujemy wypłatę ponownie i sprawdzamy stabilność zaliczki PIT
// (przeliczenie bez zmian danych nie powinno zmienić wyniku).
var prac2 = Pracownik(Pracownik_.Strzelecki);
var od = new Date(dataWyplaty.Year, dataWyplaty.Month, 1);
var doD = new Date(dataWyplaty.Year, dataWyplaty.Month,
DateTime.DaysInMonth(dataWyplaty.Year, dataWyplaty.Month));
var wyplata2 = prac2.Wyplaty[(Wyplata x) => x.Data >= od && x.Data <= doD]
.Cast<Wyplata>().First();
decimal zalPo = new Wyplata.PITInfoWorker { Wypłata = wyplata2 }.ZalFIS;
zalPo.Should().Be(zalPrzed,
"przeliczenie podatków bez zmiany danych kadrowych daje tę samą zaliczkę PIT");
}
// ====================================================================================
// H8 — Dochód z wypłaty (PITInfoWorker.Dochód_*) + dochód roczny
// ====================================================================================
[Test]
[Description("H8: dochód podatkowy wypłaty czytamy z Wyplata.PITInfoWorker: Dochód_Bez26 + Dochód_26 " +
"(decimal), Podstawa (podstawa naliczenia zaliczki), DoOpodatkowania (Currency). " +
"Dochód roczny sumujemy iterując wypłaty roku (filtr serwerowy po dacie) i sumując " +
"Dochód_Bez26+Dochód_26 z PITInfoWorker każdej wypłaty. RozliczanieManager jest internal — " +
"nie wywołujemy go bezpośrednio.")]
public void H8_DochodZWyplaty_IDochodRoczny()
{
var pracownik = Pracownik(Pracownik_.Andrzejewski);
pracownik.Should().NotBeNull();
var dataWyplaty = DataWyplatyWEtacie(pracownik);
var wyplata = NaliczWyplateEtatowa(pracownik, dataWyplaty);
var pit = new Wyplata.PITInfoWorker { Wypłata = wyplata };
decimal dochodWyplaty = pit.Dochód_Bez26 + pit.Dochód_26;
dochodWyplaty.Should().BeGreaterThan(0m, "wypłata etatowa daje dodatni dochód podatkowy");
pit.Podstawa.Should().BeGreaterThanOrEqualTo(0m, "podstawa naliczenia zaliczki nie jest ujemna");
pit.DoOpodatkowania.Value.Should().BeGreaterThan(0m, "podstawa opodatkowania dodatnia");
SaveDispose();
// Dochód roczny: suma dochodów z wypłat roku (filtr serwerowy po dacie — bez skanu tabeli).
int rok = dataWyplaty.Year;
var od = new Date(rok, 1, 1);
var doD = new Date(rok, 12, 31);
var prac2 = Pracownik(Pracownik_.Andrzejewski);
decimal dochodRoczny = 0m;
foreach (Wyplata w in prac2.Wyplaty[(Wyplata x) => x.Data >= od && x.Data <= doD])
{
var p = new Wyplata.PITInfoWorker { Wypłata = w };
dochodRoczny += p.Dochód_Bez26 + p.Dochód_26;
}
dochodRoczny.Should().BeGreaterThanOrEqualTo(dochodWyplaty,
"dochód roczny obejmuje co najmniej naliczoną wypłatę");
}
[Test]
[Ignore("H8.B/C: PobierzDochodRocznyWorker działa tylko dla właściciela (Pracownik is Wlasciciel), " +
"a RozliczaniePracownikowWorker tylko dla folderu pracowników zewnętrznych — pracownik " +
"etatowy Demo \"006\" nie spełnia tych warunków. Wewnętrzny Wyplata.RozliczenieManager jest " +
"niepubliczny. Dochód standardowego pracownika czytamy z PITInfoWorker (test H8 wyżej).")]
public void H8_PobierzDochodRoczny_TylkoWlasciciel()
{
// Udokumentowane jako niewykonalne dla zwykłego pracownika etatowego — patrz powód w [Ignore].
}
// ====================================================================================
// H9 — Kalkulator wynagrodzeń (przez naliczenie próbne + workery agregujące)
// ====================================================================================
[Test]
[Description("H9: brak dedykowanej publicznej klasy kalkulatora — brutto/netto/koszt pracodawcy " +
"liczymy z naliczenia próbnego (H1) i workerów agregujących: Wyplata.PITInfoWorker " +
"(brutto=Razem, netto=NettoRazem, składki pracownika=SkładkiZUS) oraz Wyplata.WyplataSkładkiWorker " +
"(Razem: ZestawienieSkładek z Narzuty = narzuty pracodawcy). " +
"Koszt pracodawcy ≈ brutto + Narzuty. Naliczenie próbne nie wymaga Save().")]
public void H9_KalkulatorWynagrodzen_NaliczenieProbne()
{
var pracownik = Pracownik(Pracownik_.Bednarek);
pracownik.Should().NotBeNull();
var dataWyplaty = DataWyplatyWEtacie(pracownik);
var wyplata = NaliczWyplateEtatowa(pracownik, dataWyplaty);
var pit = new Wyplata.PITInfoWorker { Wypłata = wyplata };
var skl = new WyplataSkładkiWorker { Wypłata = wyplata };
decimal brutto = pit.Razem;
decimal netto = pit.NettoRazem;
decimal narzuty = skl.Razem.Narzuty; // narzuty pracodawcy (ZUS firmy + FP/FGŚP/FEP)
decimal kosztPracodawcy = brutto + narzuty;
brutto.Should().BeGreaterThan(0m, "brutto dodatnie");
netto.Should().BeGreaterThan(0m, "netto dodatnie");
netto.Should().BeLessThanOrEqualTo(brutto, "netto nie przekracza brutto");
narzuty.Should().BeGreaterThan(0m, "pracodawca ponosi narzuty na wynagrodzenie etatowe");
kosztPracodawcy.Should().BeGreaterThan(brutto, "koszt pracodawcy = brutto + narzuty > brutto");
// Składki pracownika i firmy są spójne z ZestawienieSkładek.
skl.Razem.KosztyZUS.Should().BeGreaterThan(0m, "składki ZUS pracownika dodatnie");
skl.Razem.FirmaZUS.Should().BeGreaterThan(0m, "składki ZUS pracodawcy dodatnie");
// To była kalkulacja — nie utrwalamy (Save pominięty świadomie; rollback i tak wycofa).
}
// ====================================================================================
// H10 — Stornowanie elementów wypłaty
// ====================================================================================
[Test]
[Description("H10: oznaczenie elementu do storna realizuje publiczny worker " +
"StornoElementu.ElementDoPrzeliczeniaWorker (na WypElement): ZaznaczElementDoAnulowania()/" +
"ZaznaczElementDoPrzeliczenia()/WycofajZaznaczenie(). Oznaczać można tylko elementy wypłaty " +
"ZATWIERDZONEJ w stanie StanStorna == NieDotyczy. Najpierw zatwierdzamy wypłatę " +
"(Wyplata.ZatwierdźWorker, property Lista), potem oznaczamy i sprawdzamy StanStorna/Storno. " +
"Wytworzenie elementu stornującego (Wystornowany/Stornujący) następuje przy ponownym naliczeniu.")]
public void H10_StornowanieElementu_WorkerElementDoPrzeliczenia()
{
var pracownik = Pracownik(Pracownik_.Bujak);
pracownik.Should().NotBeNull();
var dataWyplaty = DataWyplatyWEtacie(pracownik);
var wyplata = NaliczWyplateEtatowa(pracownik, dataWyplaty);
// Storno dotyczy wypłaty ZATWIERDZONEJ — zatwierdzamy ją workerem (property Lista, nie Wypłata).
new Wyplata.ZatwierdźWorker { Lista = wyplata }.Zatwierdź();
SaveDispose();
var prac2 = Pracownik(Pracownik_.Bujak);
var od = new Date(dataWyplaty.Year, dataWyplaty.Month, 1);
var doD = new Date(dataWyplaty.Year, dataWyplaty.Month,
DateTime.DaysInMonth(dataWyplaty.Year, dataWyplaty.Month));
var wyplata2 = prac2.Wyplaty[(Wyplata x) => x.Data >= od && x.Data <= doD]
.Cast<Wyplata>().First();
wyplata2.Zatwierdzona.Should().BeTrue("po Zatwierdź() wypłata jest zatwierdzona");
// Wybieramy element w stanie NieDotyczy (kandydat do storna).
WypElement element = wyplata2.Elementy.Cast<WypElement>()
.First(e => e.StanStorna == StanStornaElementu.NieDotyczy);
// Oznaczamy element do anulowania — worker otwiera własną transakcję.
var worker = new StornoElementu.ElementDoPrzeliczeniaWorker { Element = element };
worker.ZaznaczElementDoAnulowania();
SaveDispose();
// Po oznaczeniu element jest DoStornowania i ma powiązany rekord Storno.
var prac3 = Pracownik(Pracownik_.Bujak);
var wyplata3 = prac3.Wyplaty[(Wyplata x) => x.Data >= od && x.Data <= doD]
.Cast<Wyplata>().First();
WypElement element3 = wyplata3.Elementy.Cast<WypElement>()
.First(e => e.StanStorna == StanStornaElementu.DoStornowania);
element3.StanStorna.Should().Be(StanStornaElementu.DoStornowania,
"oznaczenie ustawia element na DoStornowania");
element3.Storno.Should().NotBeNull("oznaczenie tworzy powiązany rekord StornoElementu");
}
// ====================================================================================
// H11 — Anulowanie/usunięcie naliczonej wypłaty (bufor)
// ====================================================================================
[Test]
[Description("H11: powrót zatwierdzonej wypłaty do bufora (do ponownego naliczenia) realizuje " +
"publiczny worker Wyplata.OtwórzWorker (property Wypłata, akcja Otwórz() => Zatwierdzona=false), " +
"zatwierdzanie — Wyplata.ZatwierdźWorker (property Lista). CanBufor jest protected (niedostępny " +
"z dodatku). Po Otwórz() wypłata jest znów w buforze i można ją przeliczyć ponownie (H1).")]
public void H11_PowrotDoBufora_WorkerOtworz()
{
var pracownik = Pracownik(Pracownik_.Strzelecki);
pracownik.Should().NotBeNull();
var dataWyplaty = DataWyplatyWEtacie(pracownik);
var wyplata = NaliczWyplateEtatowa(pracownik, dataWyplaty);
wyplata.Bufor.Should().BeTrue("świeżo naliczona wypłata jest w buforze");
// Zatwierdzamy (zejście z bufora).
new Wyplata.ZatwierdźWorker { Lista = wyplata }.Zatwierdź();
SaveDispose();
var od = new Date(dataWyplaty.Year, dataWyplaty.Month, 1);
var doD = new Date(dataWyplaty.Year, dataWyplaty.Month,
DateTime.DaysInMonth(dataWyplaty.Year, dataWyplaty.Month));
var prac2 = Pracownik(Pracownik_.Strzelecki);
var wyplata2 = prac2.Wyplaty[(Wyplata x) => x.Data >= od && x.Data <= doD]
.Cast<Wyplata>().First();
wyplata2.Zatwierdzona.Should().BeTrue("po Zatwierdź() wypłata jest zatwierdzona");
wyplata2.Bufor.Should().BeFalse("zatwierdzona wypłata nie jest w buforze");
// Powrót do bufora workerem OtwórzWorker.
new Wyplata.OtwórzWorker { Wypłata = wyplata2 }.Otwórz();
SaveDispose();
var prac3 = Pracownik(Pracownik_.Strzelecki);
var wyplata3 = prac3.Wyplaty[(Wyplata x) => x.Data >= od && x.Data <= doD]
.Cast<Wyplata>().First();
wyplata3.Bufor.Should().BeTrue("po Otwórz() wypłata wraca do bufora");
wyplata3.Zatwierdzona.Should().BeFalse("po Otwórz() wypłata nie jest zatwierdzona");
}
}
@@ -0,0 +1,307 @@
using System;
using System.IO;
using System.Linq;
using System.Text;
using AwesomeAssertions;
using Microsoft.Extensions.DependencyInjection; // GetRequiredService
using NUnit.Framework;
using Soneta.Business; // Context
using Soneta.Business.UI; // IReportService, ReportResult, ReportFormats
using Soneta.Place; // ListaPlac, DefinicjaListyPlac, NaliczanieWypłat, Wyplata, TypNaliczenia
using Soneta.Types; // Date, FromTo, YearMonth
using Prac = Soneta.Kadry.Pracownik;
namespace Soneta.Skills.Test.KadryPlace.Pracownik;
/// <summary>
/// Rozdział I — „Listy płac, przelewy, wydruki” (receptury I1, I2, I3).
/// <para>
/// Testy są <b>wykonywalną dokumentacją</b> publicznego kontraktu list płac i ich wydruków.
/// </para>
/// <list type="bullet">
/// <item><b>I1a</b> — ręczne utworzenie pustej listy płac (<c>new ListaPlac()</c> + <c>Place.ListyPlac.AddRow</c>),
/// ustawienie pól w wymaganej kolejności (Definicja → Data → DataWyplaty → MiesiacZUS → Okres).</item>
/// <item><b>I1b</b> — naliczenie wypłaty workerem <c>NaliczanieSeryjne.Pracownika</c> z jawną
/// <c>DefinicjaListy</c> (sprawdzona ścieżka z sekcji H): worker tworzy listę płac wg tej definicji i WIĄŻE
/// z nią wypłatę. Asercja: wypłata naliczona, powiązanie dwukierunkowe (<c>w.ListaPlac</c> niepuste, jego
/// <c>Definicja == def</c>; <c>w.Pracownik == pracownik</c>).
/// <b>Rozbieżność dokumentacji:</b> niskopoziomowy worker <c>Soneta.Place.NaliczanieWypłat</c> uruchomiony
/// tylko z <c>ListaPłac</c>+<c>Pracownik</c> (snippet I1 w pracownik.md) w bazie Demo nie napełnia listy
/// (zwraca pustą <c>WszystkieWypłaty</c>); działającą ścieżką naliczania jest <c>NaliczanieSeryjne</c>.</item>
/// <item><b>I2</b> — PDF kwitka (paska) wypłaty przez <c>IReportService.GenerateReport</c>
/// (wzorzec <c>PasekWyplaty.repx</c>, <c>DataType = typeof(Wyplata)</c>).</item>
/// <item><b>I3</b> — PDF pełnej listy płac (<c>PelnaListaPlac.repx</c>, <c>DataType = typeof(ListaPlac)</c>).</item>
/// </list>
/// <para>
/// <b>Wydruki (I2/I3):</b> serwis <see cref="IReportService"/> (warstwa <c>Soneta.Business.UI</c>) jest
/// w bieżącym zestawie referencji Skills.Test OSIĄGALNY (transytywnie, tak jak w wydrukach handlowych —
/// rozdz. 12 dokumentów handlowych). Faktyczne wyrenderowanie PDF wymaga jednak zarejestrowanego wzorca
/// <c>*.repx</c> (z assembly <c>Soneta.KadryPlace.Reports</c>) oraz silnika renderującego (DevExpress) —
/// czego testowa baza Demo nie gwarantuje, a samo ładowanie DevExpress bywa niestabilne w hoście testowym.
/// Dlatego generowanie owijamy w try/catch i przy braku wzorca/silnika robimy <c>Assert.Ignore</c>
/// (suita pozostaje zielona, a kod dokumentuje publiczne API). Asercję na sygnaturze <c>"%PDF"</c>
/// wykonujemy tylko wtedy, gdy strumień faktycznie powstał.
/// </para>
/// <para>
/// Wszystko działa na bazie Demo (GoldStandard) z automatycznym rollbackiem po teście. Operujemy wyłącznie
/// na <b>publicznym kontrakcie</b> platformy Soneta (jak dodatek programisty zewnętrznego).
/// </para>
/// </summary>
[TestFixture]
public class RozdzialI_ListyWydrukiTest : PracownikTestBase
{
/// <summary>Sygnatura nagłówka pliku PDF (pierwsze 4 bajty/znaki strumienia).</summary>
private const string PdfMagic = "%PDF";
/// <summary>Wzorzec wydruku paska (kwitka) wypłaty — wg tabeli I2 (DataType = Wyplata).</summary>
private const string WzorzecPasek = "PasekWyplaty.repx";
/// <summary>Wzorzec wydruku pełnej listy płac — wg tabeli I3 (DataType = ListaPlac).</summary>
private const string WzorzecPelnaLista = "PelnaListaPlac.repx";
/// <summary>Serwis raportowy ze scopeu bieżącej sesji (jak w wydrukach handlowych).</summary>
private IReportService Raporty => Session.GetRequiredService<IReportService>();
// === Pomocniki lokalne ===
/// <summary>
/// Wybiera dowolną dostępną definicję listy płac z bazy Demo (słownik konfiguracyjny
/// <c>Place.DefListPlac</c>). Nazwy/symbole definicji zależą od wdrożenia, więc zamiast
/// twardego symbolu („ETAT”) pobieramy pierwszą dostępną definicję — deterministycznie,
/// bez zakładania konkretnej konfiguracji.
/// </summary>
private DefinicjaListyPlac DowolnaDefinicjaListy()
=> Place.DefListPlac.Cast<DefinicjaListyPlac>().FirstOrDefault();
/// <summary>
/// Dobiera okres/daty listy w obrębie aktywnego etatu pracownika: bierzemy miesiąc rozpoczęcia
/// etatu (dla pracowników Demo etat zwykle zaczyna się wstecz i jest otwarty), aby naliczanie
/// trafiło w okres zatrudnienia. Zwraca (okresMiesiąca, dataWyplaty = koniec miesiąca).
/// </summary>
private static (FromTo Okres, Date DataWyplaty) OkresWEtacie(Prac pracownik)
{
var from = pracownik.Last.Etat.Okres.From;
var poczatek = new Date(from.Year, from.Month, 1);
var koniec = poczatek.AddMonths(1).AddDays(-1); // koniec miesiąca (2831)
return (new FromTo(poczatek, koniec), koniec);
}
/// <summary>
/// Demonstruje ręczne utworzenie pustej listy płac z wybraną definicją i polami ustawionymi
/// w wymaganej kolejności (Definicja → Data → DataWyplaty → MiesiacZUS → Okres), zwraca utworzoną
/// listę. Sama lista jest tworzona poprawnie; <b>napełnienie jej wypłatami</b> realizuje worker
/// naliczający (patrz <see cref="NaliczWyplate"/>), a nie ustawienie pól listy.
/// </summary>
private ListaPlac UtworzPustaListe(Prac pracownik, DefinicjaListyPlac def)
{
var (okres, dataWyplaty) = OkresWEtacie(pracownik);
var lp = new ListaPlac();
Place.ListyPlac.AddRow(lp);
lp.Definicja = def; // wzorzec listy — ustaw PIERWSZE po AddRow
// Wydzial/Seria ustawiamy WARUNKOWO — tylko gdy wymaga ich definicja.
if (def.Wydzial)
lp.Wydzial = Kadry.Wydzialy.Firma;
lp.Data = dataWyplaty; // data naliczania listy
lp.DataWyplaty = dataWyplaty; // data przekazania środków (wyznacza mies./rok)
lp.MiesiacZUS = new YearMonth(dataWyplaty); // miesiąc rozliczenia ZUS
lp.Okres = okres; // okres listy — PO DataWyplaty
return lp;
}
/// <summary>
/// Nalicza wypłatę etatową pracownika workerem <c>NaliczanieSeryjne.Pracownika</c> (sprawdzona
/// ścieżka z sekcji H). Worker sam dobiera/tworzy listę płac dla naliczanych wypłat i WIĄŻE je
/// z nią (<c>Wyplata.ListaPlac</c>).
/// <para>
/// <c>Nalicz()</c> sam otwiera i commituje transakcję w sesji — NIE owijamy go w InTransaction.
/// Pola <c>Naliczanie</c> nie ustawiamy (domyślne; setter rzuca bez licencji „PL Złoty”).
/// <c>DefinicjaListy</c> także NIE wymuszamy — dowolna definicja może nie pasować do typu wypłaty
/// (np. lista umów ≠ etat) i wtedy nic się nie naliczy; worker dobiera definicję sam.
/// Zwraca pierwszą naliczoną wypłatę albo <c>null</c>, gdy nic się nie naliczyło.
/// </para>
/// </summary>
private Wyplata NaliczWyplate(Prac pracownik)
{
var (okres, dataWyplaty) = OkresWEtacie(pracownik);
var pars = new NaliczanieSeryjne.PracownikParams(Context)
{
DataWypłaty = dataWyplaty, // ustawia Okres i MiesiącDeklaracji automatycznie
DataListy = dataWyplaty,
TypWypłaty = TypWyplaty.Etat, // tylko wypłaty etatowe
};
var naliczanie = new NaliczanieSeryjne.Pracownika(pars) { Pracownik = pracownik };
var wynik = naliczanie.Nalicz(); // self-commit w sesji
return wynik.WszystkieWypłaty.Cast<Wyplata>().FirstOrDefault();
}
// ===================================================================================
// I1 — Tworzenie i naliczanie listy płac
// ===================================================================================
[Test]
[Description("I1 (część A): ręcznie tworzymy pustą listę płac — new ListaPlac() + Place.ListyPlac.AddRow + " +
"pola w wymaganej kolejności (Definicja → Data → DataWyplaty → MiesiacZUS → Okres). " +
"Asercja: lista istnieje, ma przypisaną definicję i jest pusta (Wyplaty napełnia dopiero worker).")]
public void I1a_PustaListaPlac_TworzenieRecznePolaWKolejnosci()
{
var pracownik = Pracownik(Pracownik_.Andrzejewski);
pracownik.Should().NotBeNull();
var def = DowolnaDefinicjaListy();
def.Should().NotBeNull("baza Demo zawiera co najmniej jedną definicję listy płac (Place.DefListPlac)");
// Tworzenie danych operacyjnych MUSI być w trybie edycji (InTransaction), inaczej AddRow
// rzuca CannotEditException.
ListaPlac lp = null;
InTransaction(() => lp = UtworzPustaListe(pracownik, def));
lp.Should().NotBeNull();
lp.Definicja.Should().Be(def, "ustawiliśmy Definicja po AddRow");
lp.Wyplaty.Cast<Wyplata>().Should().BeEmpty("nowo utworzona lista jest pusta — wypłaty dolicza worker");
SaveDispose(); // utrwalenie w bazie (rollback po teście i tak wycofa)
}
[Test]
[Description("I1 (część B): naliczamy wypłatę etatową workerem NaliczanieSeryjne.Pracownika (sprawdzona " +
"ścieżka z sekcji H). Worker sam dobiera/tworzy listę płac i WIĄŻE z nią wypłatę. " +
"Asercja: wypłata naliczona, powiązana dwukierunkowo z listą płac (w.ListaPlac niepuste, " +
"ma definicję) i z pracownikiem (w.Pracownik == pracownik). " +
"Uwaga: niskopoziomowy worker Soneta.Place.NaliczanieWypłat (samo ListaPłac+Pracownik z " +
"dokumentacji) w bazie Demo nie napełnia listy — sprawdzoną ścieżką jest NaliczanieSeryjne.")]
public void I1b_ListaPlac_NaliczanieWyplatyPowiazanaZLista()
{
var pracownik = Pracownik(Pracownik_.Andrzejewski);
pracownik.Should().NotBeNull();
// NaliczanieSeryjne.Nalicz() sam otwiera i commituje transakcję — NIE owijamy w InTransaction.
var w = NaliczWyplate(pracownik);
w.Should().NotBeNull(
"naliczanie etatu dla pracownika Demo w okresie etatu powinno dać wypłatę powiązaną z listą");
// Powiązanie dwukierunkowe: wypłata wskazuje wstecz listę płac i pracownika.
var lista = (ListaPlac)w.ListaPlac;
lista.Should().NotBeNull("Wyplata.ListaPlac wskazuje listę, na której została naliczona");
lista.Definicja.Should().NotBeNull("lista płac utworzona przez worker ma przypisaną definicję");
w.Pracownik.Guid.Should().Be(pracownik.Guid, "Wyplata.Pracownik to pracownik, dla którego naliczono");
SaveDispose();
}
// ===================================================================================
// I2 — Drukowanie/PDF kwitka (paska) wypłaty
// ===================================================================================
[Test]
[Description("I2: pasek (kwitek) wypłaty do PDF przez IReportService.GenerateReport " +
"(TemplateFileName = PasekWyplaty.repx, DataType = typeof(Wyplata), OutputFormat = PDF, " +
"Context.Set(wyplata)). Strumień zaczyna się od sygnatury „%PDF”. " +
"Brak wzorca/silnika renderującego → Assert.Ignore (suita zielona).")]
public void I2_PasekWyplaty_DoPdf_ZaczynaSieOdPdf()
{
var pracownik = Pracownik(Pracownik_.Bednarek);
pracownik.Should().NotBeNull();
// Arrange: naliczona wypłata (wraz z listą) jako źródło danych wydruku.
// NaliczanieSeryjne self-commituje — wypłata jest dostępna w bieżącej sesji.
var wyplata = NaliczWyplate(pracownik);
if (wyplata == null)
Assert.Ignore("Worker nie naliczył wypłaty dla pracownika Demo — brak danych do wydruku paska.");
// Kontekst wydruku: pojedyncza Wyplata (jak w snippetcie I2).
var context = Login.CreateEmptyContext().Clone(Session);
context.Set(wyplata);
var rr = new ReportResult
{
TemplateFileName = WzorzecPasek, // tryb automatyczny (bez UI)
DataType = typeof(Wyplata), // pojedyncza wypłata
Context = context,
OutputFormat = ReportFormats.PDF,
AskForParameters = false // tryb wsadowy — nie pytaj o parametry
};
// Act: generowanie do strumienia. Brak wzorca/silnika → Assert.Ignore zamiast błędu.
byte[] naglowek;
try
{
using var pdf = Raporty.GenerateReport(rr);
pdf.Should().NotBeNull("GenerateReport dla formatu binarnego zwraca Stream");
naglowek = new byte[4];
int przeczytane = pdf.Read(naglowek, 0, naglowek.Length);
przeczytane.Should().Be(4, "PDF ma co najmniej 4-bajtowy nagłówek");
}
catch (Exception ex)
{
Assert.Ignore("Pominięto I2: wygenerowanie PDF paska wymaga zarejestrowanego wzorca '" +
WzorzecPasek + "' (assembly Soneta.KadryPlace.Reports) oraz silnika renderującego " +
"(DevExpress), których testowa baza Demo nie gwarantuje. Test dokumentuje publiczne API " +
"IReportService.GenerateReport. Szczegóły: " + ex.GetType().Name + " — " + ex.Message);
return;
}
Encoding.ASCII.GetString(naglowek).Should().StartWith(PdfMagic,
"poprawny strumień PDF zaczyna się od „%PDF”.");
}
// ===================================================================================
// I3 — Drukowanie/PDF całej listy płac
// ===================================================================================
[Test]
[Description("I3: pełna lista płac do PDF przez IReportService.GenerateReport " +
"(TemplateFileName = PelnaListaPlac.repx, DataType = typeof(ListaPlac), OutputFormat = PDF, " +
"Context.Set(listaPlac)). Strumień zaczyna się od sygnatury „%PDF”. " +
"Brak wzorca/silnika renderującego → Assert.Ignore (suita zielona).")]
public void I3_PelnaListaPlac_DoPdf_ZaczynaSieOdPdf()
{
var pracownik = Pracownik(Pracownik_.Bujak);
pracownik.Should().NotBeNull();
// Arrange: naliczona wypłata daje listę płac (Wyplata.ListaPlac) jako źródło danych wydruku.
// NaliczanieSeryjne self-commituje — lista jest dostępna w bieżącej sesji.
var wyplata = NaliczWyplate(pracownik);
if (wyplata == null)
Assert.Ignore("Worker nie naliczył wypłaty dla pracownika Demo — brak listy płac do wydruku.");
var lp = (ListaPlac)wyplata.ListaPlac;
lp.Should().NotBeNull();
var context = Login.CreateEmptyContext().Clone(Session);
context.Set(lp); // ListaPlac
var rr = new ReportResult
{
TemplateFileName = WzorzecPelnaLista,
DataType = typeof(ListaPlac),
Context = context,
OutputFormat = ReportFormats.PDF,
AskForParameters = false
};
// Act: skopiowanie strumienia do pamięci (jak wzorzec integracyjny — bajty → załącznik/REST).
byte[] pdfBytes;
try
{
using Stream src = Raporty.GenerateReport(rr);
using var ms = new MemoryStream();
src.CopyTo(ms);
pdfBytes = ms.ToArray();
}
catch (Exception ex)
{
Assert.Ignore("Pominięto I3: wygenerowanie PDF pełnej listy płac wymaga zarejestrowanego wzorca '" +
WzorzecPelnaLista + "' (assembly Soneta.KadryPlace.Reports) oraz silnika renderującego " +
"(DevExpress), których testowa baza Demo nie gwarantuje. Test dokumentuje publiczne API " +
"IReportService.GenerateReport. Szczegóły: " + ex.GetType().Name + " — " + ex.Message);
return;
}
pdfBytes.Should().NotBeNullOrEmpty("wydruk listy płac zwraca niepusty bufor bajtów");
pdfBytes.Length.Should().BeGreaterThan(4);
Encoding.ASCII.GetString(pdfBytes, 0, 4).Should().StartWith(PdfMagic,
"bufor bajtów to plik PDF (sygnatura „%PDF”).");
}
}
@@ -0,0 +1,224 @@
using System;
using System.Collections;
using System.Linq;
using AwesomeAssertions;
using NUnit.Framework;
using Soneta.Kasa; // EksportPrzelewowWorker, EksportPrzelewowParams, PrzelewBase, PaczkaPrzelewow, RachunekBankowyFirmy, RozrachunekIdx, ...
using Soneta.Place; // ListaPlac (+ ListaPlac.PrzygotujPrzelewyWorker)
using Prac = Soneta.Kadry.Pracownik;
namespace Soneta.Skills.Test.KadryPlace.Pracownik;
/// <summary>
/// Rozdział I (część rozliczeniowa) — „Przelewy wynagrodzeń, eksport do banku, rozliczenia/faktury”
/// (receptury I4, I5, I6).
/// <para>
/// Testy są <b>wykonywalną dokumentacją</b> publicznego kontraktu mechanizmu „z wypłaty do przelewu”
/// i rozliczeń pracownika. Operujemy wyłącznie na <b>publicznym kontrakcie</b> platformy Soneta
/// (jak dodatek programisty zewnętrznego), na bazie Demo (GoldStandard) z automatycznym rollbackiem.
/// </para>
/// <list type="bullet">
/// <item><b>I4</b> — przygotowanie przelewów wynagrodzeń workerem
/// <c>Soneta.Place.ListaPlac.PrzygotujPrzelewyWorker</c> (akcja <c>PrzygotujPrzelewy()</c>).
/// Testowalny jest <b>kontrakt</b> (istnienie workera i jego <c>Params</c> z polami
/// <c>Data</c>/<c>Paczka</c>/<c>ZRachunku</c>) oraz <b>odczyt</b> kolekcji rozliczeniowych pracownika
/// (<c>Przelewy</c>, <c>DokumentyPreliminarza</c>, <c>Rozrachunki</c>). Samo
/// <c>worker.PrzygotujPrzelewy()</c> wymaga skonfigurowanego modułu Kasa (definicja paczki, rachunek
/// firmy, rachunek pracownika), czego baza Demo nie gwarantuje → <c>[Ignore]</c>.</item>
/// <item><b>I5</b> — eksport przelewów do pliku bankowego workerem
/// <c>Soneta.Kasa.EksportPrzelewowWorker</c> (akcja <c>Eksport()</c>) sterowanym
/// <c>Soneta.Kasa.EksportPrzelewowParams</c>. Testowalne jest <b>istnienie publicznego API</b>
/// (konstrukcja workera i parametrów, pole <c>FileName</c>). Wywołanie <c>Eksport()</c> to operacja
/// plikowa/sieciowa → <c>[Ignore]</c>.</item>
/// <item><b>I6</b> — rozliczenia/faktura: odczyt kolekcji rozrachunkowych pracownika
/// (<c>Rozrachunki</c>, <c>DokumentyRozliczeniowe</c>, <c>DokumentyPreliminarza</c>) — asercja, że są
/// dostępne, iterowalne i zwracają typy zgodne z kontraktem. Wystawienie faktury (zbiorczej) z zapłaty
/// to domena handlowa (<c>DokumentHandlowy</c>), poza kontraktem pracownika → <c>[Ignore]</c>.</item>
/// </list>
/// </summary>
[TestFixture]
public class RozdzialIrest_PrzelewyTest : PracownikTestBase
{
// ===================================================================================
// I4 — Przygotowanie przelewów wynagrodzeń (kontrakt workera + odczyt kolekcji)
// ===================================================================================
[Test]
[Description("I4 (kontrakt): worker przygotowania przelewów z listy płac istnieje w publicznym API — " +
"Soneta.Place.ListaPlac.PrzygotujPrzelewyWorker z zagnieżdżonym typem Params. " +
"Asercja przez refleksję publicznego kontraktu: typ workera i Params istnieją, Params ma " +
"pola Data/Paczka/DefinicjaPaczki/ZRachunku, a worker ma metodę PrzygotujPrzelewy(). " +
"Faktyczne wywołanie PrzygotujPrzelewy() jest [Ignore] (osobny test) wymaga konfiguracji Kasa.")]
public void I4_PrzygotujPrzelewy_KontraktWorkera()
{
// Worker płacowy jest typem zagnieżdżonym w ListaPlac (assembly Soneta.KadryPlace, namespace Soneta.Place).
Type workerType = typeof(ListaPlac.PrzygotujPrzelewyWorker);
workerType.Should().NotBeNull("worker przygotowania przelewów istnieje w publicznym kontrakcie");
// Typ parametrów workera (zagnieżdżony Params).
Type paramsType = workerType.GetNestedType("Params");
paramsType.Should().NotBeNull("PrzygotujPrzelewyWorker udostępnia publiczny typ Params");
// Kluczowe pola/właściwości parametrów wg dokumentacji I4.
var skladowe = paramsType.GetMembers()
.Select(m => m.Name)
.ToList();
skladowe.Should().Contain("Data", "Params.Data — data dokumentów przelewu");
skladowe.Should().Contain("Paczka", "Params.Paczka — istniejąca paczka przelewów");
skladowe.Should().Contain("ZRachunku", "Params.ZRachunku — rachunek firmy obciążany przelewami");
// Akcja workera: PrzygotujPrzelewy().
workerType.GetMethod("PrzygotujPrzelewy")
.Should().NotBeNull("worker udostępnia akcję PrzygotujPrzelewy()");
// Dokument przelewu, który powstaje w wyniku akcji, to Soneta.Kasa.PrzelewBase (tabela Przelewy).
typeof(PrzelewBase).Should().NotBeNull("dokument przelewu to Soneta.Kasa.PrzelewBase");
}
[Test]
[Description("I4 (odczyt): kolekcje rozliczeniowe pracownika są dostępne i iterowalne — " +
"Pracownik.Przelewy (PrzelewBase), Pracownik.DokumentyPreliminarza (PreliminarzDokument), " +
"Pracownik.Rozrachunki (RozrachunekIdx). Asercja: iteracja nie rzuca, a elementy (jeśli są) " +
"mają typy zgodne z kontraktem. Bez wywołania PrzygotujPrzelewy — sam odczyt stanu.")]
public void I4_KolekcjeRozliczeniowePracownika_OdczytTypyZgodne()
{
var pracownik = Pracownik(Pracownik_.Andrzejewski);
pracownik.Should().NotBeNull();
// Przelewy — odczyt nie rzuca; elementy (jeśli są) to PrzelewBase.
Action czytajPrzelewy = () => IterujISprawdzTyp<PrzelewBase>(pracownik.Przelewy);
czytajPrzelewy.Should().NotThrow("odczyt kolekcji Pracownik.Przelewy jest bezpieczny");
// Dokumenty preliminarza — elementy to PreliminarzDokument.
Action czytajPreliminarz = () => IterujISprawdzTyp<PreliminarzDokument>(pracownik.DokumentyPreliminarza);
czytajPreliminarz.Should().NotThrow("odczyt kolekcji Pracownik.DokumentyPreliminarza jest bezpieczny");
// Rozrachunki — elementy to RozrachunekIdx.
Action czytajRozrachunki = () => IterujISprawdzTyp<RozrachunekIdx>(pracownik.Rozrachunki);
czytajRozrachunki.Should().NotThrow("odczyt kolekcji Pracownik.Rozrachunki jest bezpieczny");
}
[Test]
[Ignore("I4: faktyczne wywołanie ListaPlac.PrzygotujPrzelewyWorker.PrzygotujPrzelewy() wymaga " +
"skonfigurowanego modułu Kasa (definicja paczki przelewów DefinicjaPaczkiPrzelewu, rachunek firmy " +
"RachunekBankowyFirmy oraz rachunek odbiorcy Pracownik.Rachunki). Baza Demo nie gwarantuje tej " +
"konfiguracji, więc generowanie dokumentów PrzelewBase jest niepewne. Test I4_PrzygotujPrzelewy_KontraktWorkera " +
"pokrywa publiczny kontrakt; samo przygotowanie przelewów dokumentujemy bez uruchamiania.")]
[Description("I4 (wykonanie — pominięte): naliczenie wypłaty etatowej (jak H1/I1b) → ListaPlac z Wyplata.ListaPlac → " +
"new ListaPlac.PrzygotujPrzelewyWorker { Pars = new Params { Data = Date.Today, ... } }.PrzygotujPrzelewy() → " +
"session.Save(). Powstają dokumenty Soneta.Kasa.PrzelewBase w paczce PaczkaPrzelewow.")]
public void I4_PrzygotujPrzelewy_Wykonanie()
{
// Pominięte — patrz powód w [Ignore]. Operacja zapisująca zależna od konfiguracji modułu Kasa.
}
// ===================================================================================
// I5 — Eksport przelewów do pliku bankowego (istnienie API; eksport pliku → Ignore)
// ===================================================================================
[Test]
[Description("I5 (kontrakt API): eksport przelewów to worker Soneta.Kasa.EksportPrzelewowWorker " +
"sterowany Soneta.Kasa.EksportPrzelewowParams. UWAGA: EksportPrzelewowParams NIE ma " +
"konstruktora bezparametrowego — wymaga (Context, RachunekBankowyFirmy, PrzelewBase[]), a sam " +
"konstruktor RZUCA ApplicationException, gdy nie wskazano rachunku firmy (walidacja w ctorze). " +
"Dlatego kontrakt weryfikujemy REFLEKSJĄ (bez instancjonowania): istnienie typów, sygnatura " +
"konstruktora parametrów, publiczne pole FileName, worker + property Params i metoda Eksport().")]
public void I5_EksportPrzelewow_KontraktApi()
{
// Typ parametrów eksportu istnieje w publicznym kontrakcie.
Type paramsType = typeof(EksportPrzelewowParams);
paramsType.Should().NotBeNull("EksportPrzelewowParams istnieje w publicznym kontrakcie");
// Konstruktor parametrów wymaga (Context, RachunekBankowyFirmy, PrzelewBase[]) — sygnatura wg kontraktu.
// (NIE wołamy go: ctor waliduje rachunek i rzuca ApplicationException przy braku konfiguracji.)
var ctor = paramsType.GetConstructor(new[]
{
typeof(Soneta.Business.Context), typeof(RachunekBankowyFirmy), typeof(PrzelewBase[]),
});
ctor.Should().NotBeNull(
"EksportPrzelewowParams wymaga konstruktora (Context, RachunekBankowyFirmy, PrzelewBase[])");
// Publiczne pole ścieżki pliku wyjściowego.
paramsType.GetProperty("FileName")
.Should().NotBeNull("Params.FileName — ścieżka pliku wyjściowego (operacja na dysku)");
// Worker eksportu i jego property Params (sterowanie parametrami).
Type workerType = typeof(EksportPrzelewowWorker);
workerType.Should().NotBeNull("EksportPrzelewowWorker istnieje w publicznym kontrakcie");
workerType.GetProperty("Params")
.Should().NotBeNull("worker przyjmuje parametry przez właściwość Params");
// Akcja eksportu istnieje w kontrakcie (ale jej NIE wołamy — patrz I5_EksportPrzelewow_Eksport).
workerType.GetMethod("Eksport")
.Should().NotBeNull("worker udostępnia akcję Eksport() — w teście jednostkowym nie wywoływaną");
}
[Test]
[Ignore("I5: EksportPrzelewowWorker.Eksport() zapisuje fizyczny plik bankowy na dysk (wg Params.FileName) " +
"i zależy od formatu/sterownika eksportu danego banku; wysyłka online to dodatkowo operacja sieciowa. " +
"To wejście/wyjście do systemu zewnętrznego — poza zakresem testu jednostkowego. Kontrakt API " +
"pokrywa test I5_EksportPrzelewow_KontraktApi (bez wywołania Eksport()).")]
[Description("I5 (wykonanie — pominięte): worker.Eksport() — zapis pliku przelewów wg FileName; po eksporcie " +
"PrzelewBase.Exported = true blokuje dalszą edycję.")]
public void I5_EksportPrzelewow_Eksport()
{
// Pominięte — patrz powód w [Ignore]. Operacja plikowa/sieciowa.
}
// ===================================================================================
// I6 — Rozliczenia / faktura (odczyt rozrachunków; wystawienie faktury → Ignore)
// ===================================================================================
[Test]
[Description("I6 (odczyt): kolekcje rozliczeniowe pracownika są dostępne i iterowalne, a elementy mają " +
"typy zgodne z kontraktem — Pracownik.Rozrachunki (RozrachunekIdx), " +
"Pracownik.DokumentyRozliczeniowe (DokRozliczBase), Pracownik.DokumentyPreliminarza " +
"(PreliminarzDokument). Asercja: iteracja nie rzuca; bez operacji zapisujących.")]
public void I6_Rozliczenia_OdczytStanu()
{
var pracownik = Pracownik(Pracownik_.Bednarek);
pracownik.Should().NotBeNull();
// Rozrachunki — indeksy rozrachunkowe podmiotu (RozrachunekIdx).
Action czytajRozrachunki = () => IterujISprawdzTyp<RozrachunekIdx>(pracownik.Rozrachunki);
czytajRozrachunki.Should().NotThrow("odczyt kolekcji Pracownik.Rozrachunki jest bezpieczny");
// Dokumenty rozliczeniowe — DokRozliczBase.
Action czytajRozliczeniowe = () => IterujISprawdzTyp<DokRozliczBase>(pracownik.DokumentyRozliczeniowe);
czytajRozliczeniowe.Should().NotThrow("odczyt kolekcji Pracownik.DokumentyRozliczeniowe jest bezpieczny");
// Dokumenty preliminarza — PreliminarzDokument.
Action czytajPreliminarz = () => IterujISprawdzTyp<PreliminarzDokument>(pracownik.DokumentyPreliminarza);
czytajPreliminarz.Should().NotThrow("odczyt kolekcji Pracownik.DokumentyPreliminarza jest bezpieczny");
}
[Test]
[Ignore("I6: „Wystaw fakturę (zbiorczą) z zapłaty” NIE istnieje w publicznym kontrakcie pracownika/płac — " +
"faktura to dokument handlowy (Soneta.Handel.DokumentHandlowy). Powiązanie zapłaty z fakturą realizują " +
"rozrachunki/rozliczenia (moduł Kasa), a operacje zapisujące (np. RozliczWgPrzelewowWyplataWorker) wymagają " +
"skonfigurowanego modułu Kasa/Handel, którego baza Demo nie gwarantuje. Wystawianie faktur należy do testów " +
"domeny handlowej (dokument-handlowy.md). Odczyt rozrachunków pokrywa test I6_Rozliczenia_OdczytStanu.")]
[Description("I6 (wykonanie — pominięte): wystawienie faktury zbiorczej z zapłat/rozliczeń (domena handlowa) " +
"oraz rozliczanie zapisujące przez workery rozliczeniowe Kasa.")]
public void I6_WystawienieFaktury_Rozliczenie()
{
// Pominięte — patrz powód w [Ignore]. Domena handlowa + konfiguracja Kasa/Handel.
}
// ===================================================================================
// Pomocniki lokalne
// ===================================================================================
/// <summary>
/// Iteruje kolekcję (np. <c>SubTable&lt;T&gt;</c> z kartoteki pracownika) i sprawdza, że każdy
/// element jest przypisywalny do oczekiwanego typu kontraktu. Sama iteracja po kolekcji
/// rozliczeniowej pracownika jest bezpieczna (zakres = jeden podmiot), więc nie skanujemy całej
/// tabeli operacyjnej (safe-code §6.3). Pusta kolekcja jest poprawna (brak danych w Demo).
/// </summary>
private static void IterujISprawdzTyp<T>(IEnumerable kolekcja)
{
kolekcja.Should().NotBeNull("kolekcja rozliczeniowa pracownika jest dostępna w kontrakcie");
foreach (var element in kolekcja)
element.Should().BeAssignableTo<T>($"elementy kolekcji są typu {typeof(T).Name} (zgodnie z kontraktem)");
}
}
@@ -0,0 +1,283 @@
using System;
using System.Linq;
using AwesomeAssertions;
using NUnit.Framework;
using Soneta.Business;
using Soneta.Deklaracje;
using Soneta.Kadry;
using Soneta.Place;
using Soneta.Types;
using Prac = Soneta.Kadry.Pracownik;
namespace Soneta.Skills.Test.KadryPlace.Pracownik;
/// <summary>
/// Rozdział J — „Deklaracje (ZUS, PIT, PFRON, PPK)" (receptury J1J6).
/// <para>
/// Testy są <b>wykonywalną dokumentacją</b> publicznego kontraktu modułu Deklaracje
/// (<c>Soneta.Deklaracje.DeklaracjeModule</c>, dostęp przez <c>Session.GetDeklaracje()</c>).
/// Wszystkie deklaracje to wiersze tabeli <c>Deklaracje</c>, dziedziczące po abstrakcyjnej
/// <c>Soneta.Deklaracje.Deklaracja</c>; konkretne typy żyją w podprzestrzeniach
/// <c>Soneta.Deklaracje.{ZUS,PIT,PFRON,PPK}.*</c>.
/// </para>
/// <para>
/// <b>Rozróżnienie kluczowe.</b> Naliczenie/utworzenie większości deklaracji (J1J5) to operacja
/// lokalna (zapis wiersza), ale wymaga <c>Context</c> i — dla ZUS — obiektu <c>KEDU</c> (kontener
/// dokumentów ZUS), którego nie da się sensownie zbudować bez środowiska modułu Deklaracje.
/// E-wysyłka (KEDU/PUE/SODiR/MF) jest sieciowa/plikowa. Dlatego testy J1J5 dokumentują
/// <b>KONTRAKT</b> typów/workerów kompilowalnie (przez odwołania do typów <c>typeof(...)</c>,
/// ctory, metody) i są oznaczone <c>[Ignore]</c> z powodem. Realnie wykonujemy J6 (bilanse otwarcia
/// PIT — czyste API biznesowe na pracowniku) oraz próbę naliczenia PIT-11.
/// </para>
/// <para>
/// Operujemy wyłącznie na <b>publicznym kontrakcie</b> platformy, na bazie Demo (GoldStandard),
/// z automatycznym rollbackiem po teście.
/// </para>
/// </summary>
[TestFixture]
public class RozdzialJ_DeklaracjeTest : PracownikTestBase
{
/// <summary>Skrót do modułu Deklaracje bieżącej sesji operacyjnej.</summary>
private DeklaracjeModule Deklaracje => Session.GetDeklaracje();
// ============================== J1 — Zgłoszenia ZUS (ZUA/ZZA, ZCNA, ZWUA) ==============================
[Test]
[Description("J1: zgłoszenia ZUS to wiersze deklaracji w Soneta.Deklaracje.ZUS — ZUA (społeczne+zdrowotne), " +
"ZZA (zdrowotne), ZCNA (rodzina), ZWUA (wyrejestrowanie). Konkretne typy mają ctor " +
"(Pracownik, KEDU): new ZUA(pracownik, kedu). Workerem zbiorczym jest " +
"ZarejestrujPracownikówWorker (zagnieżdżone .Rejestracja/.Rodzina/.Wyrejestrowanie/.ZgloszenieUmow), " +
"Params budowane z Context (ctor (Context)) + pole Kedu. Tu dokumentujemy KONTRAKT typów; " +
"samo utworzenie wymaga Context + KEDU.")]
[Ignore("wymaga Context/KEDU / e-wysyłka sieciowa — dokumentowany kontrakt typów ZUS")]
public void J1_ZgloszeniaZUS_ZUA_ZZA_ZCNA_ZWUA_Kontrakt()
{
// Kontrakt typów zgłoszeniowych ZUS — odwołania kompilowalne (zweryfikowane z DLL).
typeof(Soneta.Deklaracje.ZUS.ZUA).Should().NotBeNull("ZUA — zgłoszenie społeczne+zdrowotne");
typeof(Soneta.Deklaracje.ZUS.ZZA).Should().NotBeNull("ZZA — zgłoszenie tylko zdrowotne");
typeof(Soneta.Deklaracje.ZUS.ZCNA).Should().NotBeNull("ZCNA — zgłoszenie członków rodziny");
typeof(Soneta.Deklaracje.ZUS.ZWUA).Should().NotBeNull("ZWUA — wyrejestrowanie");
// Worker zbiorczy + jego klasy zagnieżdżone (akcje menu „Deklaracje ZUS/Przygotuj …").
typeof(Soneta.Deklaracje.ZUS.ZarejestrujPracownikówWorker.Rejestracja).Should().NotBeNull();
typeof(Soneta.Deklaracje.ZUS.ZarejestrujPracownikówWorker.Rodzina).Should().NotBeNull();
typeof(Soneta.Deklaracje.ZUS.ZarejestrujPracownikówWorker.Wyrejestrowanie).Should().NotBeNull();
// Params zgłoszeniowe mają ctor (Context); KEDU jest wymaganym kontenerem docelowym.
typeof(Soneta.Deklaracje.ZUS.ZarejestrujBaseWorker.ParamsKor)
.GetConstructor(new[] { typeof(Context) })
.Should().NotBeNull("ParamsKor budujemy z Context");
typeof(Soneta.Deklaracje.ZUS.KEDU)
.GetConstructor(new[] { typeof(Session) })
.Should().NotBeNull("KEDU ma ctor (Session), ale realne złożenie wymaga modułu Deklaracje");
}
// ============================== J2 — Deklaracje rozliczeniowe ZUS (DRA, RIA, IMIR/RMUA) ==============================
[Test]
[Description("J2: rozliczeniowe ZUS — DRA (deklaracja rozliczeniowa, ctor (KEDU)), RIA (raport po ustaniu, " +
"ctor (Pracownik, KEDU)), RMUA (informacja miesięczna dla ubezpieczonego = IMIR, ctor " +
"(Pracownik, RMUA.TypOkresuDeklaracji)). Naliczanie seryjne: NaliczanieSeryjneRIAWorker / " +
"NaliczanieSeryjneRMUAWorker (ctor bezparametrowy + Pracownicy/Pars + metoda NaliczRMUA(Context)). " +
"Pojedynczą deklarację przelicza DeklaracjaWorker.Przelicz() (DataType Deklaracja). " +
"KEDU + Context wymagane — dokumentujemy KONTRAKT.")]
[Ignore("wymaga Context/KEDU / e-wysyłka sieciowa — dokumentowany kontrakt rozliczeń ZUS")]
public void J2_RozliczeniaZUS_DRA_RIA_IMIR_Kontrakt()
{
// DRA wiąże się z KEDU (ctor (KEDU)), RIA z pracownikiem i KEDU.
typeof(Soneta.Deklaracje.ZUS.DRA).GetConstructor(new[] { typeof(Soneta.Deklaracje.ZUS.KEDU) })
.Should().NotBeNull("DRA(KEDU)");
typeof(Soneta.Deklaracje.ZUS.RIA)
.GetConstructor(new[] { typeof(Prac), typeof(Soneta.Deklaracje.ZUS.KEDU) })
.Should().NotBeNull("RIA(Pracownik, KEDU)");
// IMIR w CLR nazywa się RMUA (ctor (Pracownik, RMUA.TypOkresuDeklaracji)).
typeof(Soneta.Deklaracje.ZUS.RMUA).Should().NotBeNull("RMUA = informacja miesięczna (IMIR)");
typeof(Soneta.Deklaracje.ZUS.RMUA.TypOkresuDeklaracji).IsEnum
.Should().BeTrue("typ okresu deklaracji RMUA jest enumem");
// Naliczanie seryjne RIA/RMUA — ctor bezparametrowy + Pracownicy/Pars (Context w props).
typeof(Soneta.Deklaracje.ZUS.NaliczanieSeryjneRIAWorker).GetConstructor(Type.EmptyTypes)
.Should().NotBeNull();
typeof(Soneta.Deklaracje.ZUS.NaliczanieSeryjneRMUAWorker).GetMethod("NaliczRMUA")
.Should().NotBeNull("NaliczRMUA(Context) — metoda akcji naliczania IMIR");
// Przeliczenie istniejącego wiersza dowolnej deklaracji.
typeof(DeklaracjaWorker).GetMethod("Przelicz").Should().NotBeNull("DeklaracjaWorker.Przelicz()");
}
// ============================== J3 — Deklaracje PIT (PIT-11, 4R, 8AR, R, IFT) ==============================
[Test]
[Description("J3: imienne PIT (PIT-11, PIT-R, IFT-1/IFT-1R, PIT-8C) nalicza seryjnie zagnieżdżony " +
"Soneta.Deklaracje.PIT.NaliczanieSeryjne.* (PIT_11Worker ma ctor (Session); Params ctor (Context)). " +
"PIT-4R/PIT-8AR (PIT4/PIT8A) są zbiorcze na poziomie podmiotu/US (ctory nonpublic — tworzone " +
"workerami zbiorczymi). Tu dokumentujemy KONTRAKT typów i workerów. Realne naliczenie PIT-11 " +
"próbujemy w J3b.")]
[Ignore("wymaga Context / dane źródłowe (wypłaty + BO PIT) — dokumentowany kontrakt PIT")]
public void J3_DeklaracjePIT_Kontrakt()
{
// Typy deklaracji PIT (wiersze tabeli Deklaracje).
typeof(Soneta.Deklaracje.PIT.PIT11).Should().NotBeNull("PIT-11");
typeof(Soneta.Deklaracje.PIT.PIT4).Should().NotBeNull("PIT-4R (zaliczki)");
typeof(Soneta.Deklaracje.PIT.PIT8A).Should().NotBeNull("PIT-8AR (zryczałtowany)");
typeof(Soneta.Deklaracje.PIT.PITR).Should().NotBeNull("PIT-R");
typeof(Soneta.Deklaracje.PIT.IFT1).Should().NotBeNull("IFT-1/IFT-1R");
// Workery naliczania seryjnego PIT (zagnieżdżone w NaliczanieSeryjne).
typeof(Soneta.Deklaracje.PIT.NaliczanieSeryjne.PIT_11Worker)
.GetConstructor(new[] { typeof(Session) })
.Should().NotBeNull("PIT_11Worker(Session)");
typeof(Soneta.Deklaracje.PIT.NaliczanieSeryjne.PIT_RWorker).Should().NotBeNull();
typeof(Soneta.Deklaracje.PIT.NaliczanieSeryjne.IFT_1Worker).Should().NotBeNull();
typeof(Soneta.Deklaracje.PIT.NaliczanieSeryjne.IFT_1RWorker).Should().NotBeNull();
typeof(Soneta.Deklaracje.PIT.NaliczanieSeryjne.PIT_8CWorker).Should().NotBeNull();
// Params PIT mają ctor (Context).
typeof(Soneta.Deklaracje.PIT.NaliczanieSeryjne.PIT_11Worker.Params)
.GetConstructor(new[] { typeof(Context) })
.Should().NotBeNull("PIT_11Worker.Params(Context)");
}
[Test]
[Description("J3b: próba realnego naliczenia PIT-11 dla pracownika Demo workerem " +
"NaliczanieSeryjne.PIT_11Worker(Session) { Pracownicy = [...] }, ustawiając Pars.Okres (rok) " +
"i Pars.Data, a następnie wywołując Nalicz_PIT_11(). Worker wymaga środowiska Context/danych " +
"źródłowych — w razie wyjątku oznaczamy [Ignore].")]
[Ignore("PIT_11Worker wymaga Context/KEDU oraz danych źródłowych (naliczone wypłaty + BO PIT); " +
"naliczenie w izolacji testu rzuca — dokumentowany kontrakt wywołania")]
public void J3b_NaliczeniePIT11_ProbaRealna()
{
var pracownik = Pracownik(Pracownik_.Andrzejewski);
pracownik.Should().NotBeNull();
var worker = new Soneta.Deklaracje.PIT.NaliczanieSeryjne.PIT_11Worker(Session)
{
Pracownicy = new[] { pracownik },
};
worker.Pars.Okres = FromTo.Year(2025); // rok podatkowy
worker.Pars.Data = Date.Today;
worker.Nalicz_PIT_11(); // tworzy wiersze PIT11 w tabeli Deklaracje
SaveDispose();
}
// ============================== J4 — Deklaracje PFRON (Wn-D, INF-2, DEK-R, INF-D-P) ==============================
[Test]
[Description("J4: PFRON to wiersze deklaracji w Soneta.Deklaracje.PFRON — WN_D (Wn-D), INF_2 (informacja " +
"roczna), DEK_R (deklaracja roczna wpłat), INF_D_P (załącznik o pracowniku niepełnosprawnym). " +
"PFRON nie ma seryjnego naliczania na Pracownicy — deklarację tworzy się w module Deklaracje, " +
"a przelicza DeklaracjaWorker.Przelicz() (DataType Deklaracja). Dane źródłowe pochodzą z " +
"PracHistoria.PFRON (A13). Tworzenie/edycja wymaga Context — dokumentujemy KONTRAKT.")]
[Ignore("wymaga Context / e-wysyłka SODiR — dokumentowany kontrakt typów PFRON")]
public void J4_DeklaracjePFRON_Kontrakt()
{
typeof(Soneta.Deklaracje.PFRON.WN_D).Should().NotBeNull("Wn-D — wniosek o dofinansowanie");
typeof(Soneta.Deklaracje.PFRON.INF_2).Should().NotBeNull("INF-2 — informacja roczna");
typeof(Soneta.Deklaracje.PFRON.DEK_R).Should().NotBeNull("DEK-R — deklaracja roczna wpłat");
typeof(Soneta.Deklaracje.PFRON.INF_D_P).Should().NotBeNull("INF-D-P — załącznik o pracowniku");
// Wszystkie PFRON dziedziczą po Deklaracja, więc przelicza je wspólny DeklaracjaWorker.
typeof(Soneta.Deklaracje.PFRON.WN_D).IsSubclassOf(typeof(Deklaracja))
.Should().BeTrue("PFRON to wiersze tabeli Deklaracje");
typeof(DeklaracjaWorker).GetMethod("Przelicz").Should().NotBeNull();
}
// ============================== J5 — Operacje PPK ==============================
[Test]
[Description("J5: dokumenty PPK to wiersze deklaracji w Soneta.Deklaracje.PPK (RejestracjaUczestnikaPPK, " +
"DeklaracjaUczestnikaPPK, ZakończenieZatrudnieniaUczestnikaPPK, RozliczenieSkładekPPK, …). " +
"Operacje zbiorcze na Pracownicy realizuje DeklaracjePPKPracownikówWorker (zagnieżdżone " +
".Rejestracja/.Rezygnacja/.Wznowienie/.ZakończenieZatrudnienia/.ZmianaDanychIdentyfikacyjnych); " +
"wspólny Params = DeklaracjePPKBaseWorker.Params (ctor (Context), pole DokumentPPK). " +
"Kwalifikacja/auto-zapis to workery na pracowniku (PPKWorker/AutoZapisPPKWorker, ctor (Context)). " +
"Dokumentujemy KONTRAKT — operacje wymagają Context i zwykle DokumentyPracodawcyPPK.")]
[Ignore("wymaga Context / DokumentyPracodawcyPPK — dokumentowany kontrakt operacji PPK")]
public void J5_OperacjePPK_Kontrakt()
{
// Typy dokumentów PPK.
typeof(Soneta.Deklaracje.PPK.RejestracjaUczestnikaPPK).Should().NotBeNull();
// Workery zbiorcze operacji PPK (zagnieżdżone w DeklaracjePPKPracownikówWorker).
typeof(Soneta.Deklaracje.PPK.DeklaracjePPKPracownikówWorker.Rejestracja).Should().NotBeNull();
typeof(Soneta.Deklaracje.PPK.DeklaracjePPKPracownikówWorker.Rezygnacja).Should().NotBeNull();
typeof(Soneta.Deklaracje.PPK.DeklaracjePPKPracownikówWorker.Wznowienie).Should().NotBeNull();
typeof(Soneta.Deklaracje.PPK.DeklaracjePPKPracownikówWorker.ZakończenieZatrudnienia).Should().NotBeNull();
typeof(Soneta.Deklaracje.PPK.DeklaracjePPKPracownikówWorker.ZmianaDanychIdentyfikacyjnych).Should().NotBeNull();
// Wspólny Params ma ctor (Context).
typeof(Soneta.Deklaracje.PPK.DeklaracjePPKBaseWorker.Params)
.GetConstructor(new[] { typeof(Context) })
.Should().NotBeNull("DeklaracjePPKBaseWorker.Params(Context)");
}
// ============================== J6 — Bilanse otwarcia PIT (REALNIE TESTOWALNE) ==============================
[Test]
[Description("J6: bilans otwarcia PIT to kolekcja na pracowniku (pracownik.BilansyOtwarciaPIT, " +
"SubTable<Soneta.Place.BilansOtwarciaPIT>). Tworzymy czystym API biznesowym (BEZ Context/KEDU): " +
"Session.AddRow(new BilansOtwarciaPIT_29(pracownik)) w trybie edycji; ustawiamy Data oraz kwoty " +
"(PrzychodUlgaEtat, Spoleczne). UWAGA: bazowy BilansOtwarciaPIT jest ABSTRAKCYJNY — instancjonujemy " +
"konkretną wersję BilansOtwarciaPIT_29 (Wersja=PIT11_29) lub BilansOtwarciaPIT_11 (PIT11_11), " +
"ctor (Pracownik); brak ctora bezparametrowego, Pracownik read-only. Odczyt przez " +
"pracownik.BilansyOtwarciaPIT.")]
public void J6_BilansOtwarciaPIT_TworzenieIOdczyt()
{
var pracownik = Pracownik(Pracownik_.Andrzejewski);
pracownik.Should().NotBeNull();
// Stan początkowy kolekcji bilansów otwarcia PIT.
int przed = pracownik.BilansyOtwarciaPIT.Cast<BilansOtwarciaPIT>().Count();
var data = new Date(2026, 1, 1);
Guid guidBO = Guid.Empty;
// Tworzenie danych operacyjnych MUSI być w trybie edycji (InTransaction),
// inaczej AddRow rzuca CannotEditException.
InTransaction(() =>
{
// Bazowy BilansOtwarciaPIT jest abstrakcyjny — tworzymy konkretną wersję (_29 => PIT11_29).
BilansOtwarciaPIT bo = Session.AddRow(new BilansOtwarciaPIT_29(pracownik));
bo.Data = data;
bo.PrzychodUlgaEtat = 12000m;
bo.Spoleczne = 1645.20m;
guidBO = bo.Guid;
});
SaveDispose(); // utrwalenie (rollback po teście i tak wycofa)
// Odczyt: bilans jest dopięty do pracownika i ma ustawione wartości.
var boWczytany = Get<BilansOtwarciaPIT>(guidBO);
boWczytany.Should().NotBeNull("bilans otwarcia PIT został zapisany");
boWczytany.Pracownik.Guid.Should().Be(pracownik.Guid, "bilans jest powiązany z pracownikiem");
boWczytany.Data.Should().Be(data);
boWczytany.PrzychodUlgaEtat.Should().Be(12000m);
boWczytany.Spoleczne.Should().Be(1645.20m);
boWczytany.Wersja.Should().Be(WersjaBilansuOtwarciaPIT.PIT11_29, "wersja ustawiana w ctor");
// Odczyt przez kolekcję pracownika — bilans jest widoczny.
var pracownik2 = Pracownik(Pracownik_.Andrzejewski);
var bilanse = pracownik2.BilansyOtwarciaPIT.Cast<BilansOtwarciaPIT>().ToList();
bilanse.Should().HaveCount(przed + 1, "doszedł jeden bilans otwarcia PIT");
bilanse.Should().Contain(b => b.Guid == guidBO);
}
[Test]
[Description("J6b: pozostałe kolekcje wdrożeniowe ERP-7 na pracowniku — pracownik.WynagrodzeniaERP7 " +
"(SubTable<Soneta.Kalend.WynagrodzenieERP7>) i pracownik.NieobecnosciERP7 " +
"(SubTable<Soneta.Kalend.NieobecnoscERP7>). Dokumentujemy KONTRAKT (kolekcje istnieją i są " +
"iterowalne czystym API, bez Context); sam druk Z-3/ERP-7 to generowanie w module Deklaracje.")]
public void J6b_KolekcjeERP7_Odczyt()
{
var pracownik = Pracownik(Pracownik_.Andrzejewski);
pracownik.Should().NotBeNull();
// Kolekcje istnieją i są iterowalne (na Demo zwykle puste — sprawdzamy sam kontrakt).
System.Action odczytWynagrodzen = () => pracownik.WynagrodzeniaERP7.Cast<object>().ToList();
System.Action odczytNieobecnosci = () => pracownik.NieobecnosciERP7.Cast<object>().ToList();
odczytWynagrodzen.Should().NotThrow("kolekcja WynagrodzeniaERP7 jest dostępna czystym API");
odczytNieobecnosci.Should().NotThrow("kolekcja NieobecnosciERP7 jest dostępna czystym API");
}
}
@@ -0,0 +1,330 @@
using System.Linq;
using AwesomeAssertions;
using NUnit.Framework;
using Soneta.Business;
using Soneta.HR;
using Soneta.Kadry;
using Soneta.Types;
using Prac = Soneta.Kadry.Pracownik;
namespace Soneta.Skills.Test.KadryPlace.Pracownik;
/// <summary>
/// Rozdział K (część pierwsza) — „Ewidencje pracownicze" (receptury K1K5).
/// <para>
/// Testy są <b>wykonywalną dokumentacją</b> publicznego kontraktu platformy Soneta dla ewidencji
/// pracowniczych. Wszystkie ewidencje mają wspólny wzorzec: są kolekcjami <c>SubTable</c> na rootcie
/// <c>Pracownik</c> (nie na <c>PracHistoria</c>), a każdy wpis to osobny <c>GuidedRow</c> tworzony
/// konstruktorem <c>new Xxx(pracownik)</c>, który wiąże wpis z pracownikiem. Dodanie realizujemy
/// przez <c>Session.AddRow(new Xxx(pracownik))</c> (równoważne <c>pracownik.Kolekcja.AddRow(...)</c>).
/// Każda metoda mapuje się 1:1 do receptury z dokumentu skilla <c>pracownik.md</c>:
/// <list type="bullet">
/// <item><b>K1</b> — badania lekarskie (<c>new BadanieLekarskie(pracownik)</c>, <c>pracownik.BadaniaLekarskie</c>; pole <c>WazneDo</c> bez „ż");</item>
/// <item><b>K2</b> — szkolenia BHP (<c>new SzkolenieBHP(pracownik)</c>, <c>pracownik.SzkoleniaBHP</c>; pole <c>WażneDo</c> z „ż");</item>
/// <item><b>K3</b> — szkolenia i uprawnienia HR (<c>WniosekOSzkolenie</c>/<c>UkończoneSzkolenie</c>/<c>UprawnieniePracownika</c> — moduł <c>Soneta.HR</c>);</item>
/// <item><b>K4</b> — nagrody/kary (<c>new Nagroda/Kara(pracownik)</c>, abstr. <c>NagrodaKara</c>) i oświadczenia (<c>OświadczeniePracownika(pracownik, def[, data])</c>);</item>
/// <item><b>K5</b> — wypadki przy pracy (<c>new Wypadek(pracownik)</c>, <c>pracownik.Wypadki</c>).</item>
/// </list>
/// </para>
/// <para>
/// Wszystko działa na bazie Demo (GoldStandard) z automatycznym rollbackiem po teście. Operujemy
/// wyłącznie na <b>publicznym kontrakcie</b> — tak jak dodatek programisty zewnętrznego bez dostępu
/// do kodu źródłowego aplikacji. Większość wpisów wymaga <b>definicji</b> (rekord słownikowy z tabeli
/// konfiguracyjnej) — definicję pobieramy dynamicznie (pierwsza z tabeli / po nazwie), a gdy w Demo
/// brak wymaganej definicji, test jest oznaczany <c>Assert.Ignore</c> z powodem.
/// </para>
/// </summary>
[TestFixture]
public class RozdzialK1_EwidencjeTest : PracownikTestBase
{
// Pracownik-host dla wpisów ewidencyjnych — dowolny etatowy z Demo.
private Prac Host() => Pracownik(Pracownik_.Andrzejewski) ?? PierwszyPracownik();
// Pierwsza definicja z tabeli konfiguracyjnej (lub null) — bez twardej zależności od nazwy słownika.
private static T Pierwsza<T>(Table tabela) where T : Row =>
tabela.Cast<T>().FirstOrDefault();
// ============================== K1 — Badania lekarskie ==============================
[Test]
[Description("K1: new BadanieLekarskie(pracownik) wiąże wpis z pracownikiem; Definicja (DefBadanLek) " +
"jest wymagana; Data/Termin/WazneDo to Soneta.Types.Date (WazneDo BEZ z-kreska); wpis trafia " +
"do pracownik.BadaniaLekarskie.")]
public void K1_BadanieLekarskie_DodanieZDefinicja_TrafiaDoKolekcji()
{
var definicja = Pierwsza<DefinicjaBadaniaLekarskiego>(Kadry.DefBadanLek);
if (definicja == null)
Assert.Ignore("Brak definicji badania lekarskiego (DefBadanLek) w bazie Demo — wpisu nie można utworzyć.");
var pracownik = Host();
Soneta.Kadry.BadanieLekarskie badanie = null;
InTransaction(() =>
{
// Konstruktor (Pracownik) wiąże wpis z pracownikiem; AddRow == pracownik.BadaniaLekarskie.AddRow.
badanie = Session.AddRow(new Soneta.Kadry.BadanieLekarskie(pracownik));
badanie.Definicja = definicja; // WYMAGANA — bez niej Save() rzuci RowException
badanie.Data = Date.Today;
// Termin jest WYLICZANY (read-only) z Data + definicji — nie ustawiamy go ręcznie.
// Uwaga na pisownię: w BadanieLekarskie pole nazywa się WazneDo (BEZ „ż").
badanie.WazneDo = new Date(Date.Today.Year + 2, Date.Today.Month, Date.Today.Day);
});
badanie.Pracownik.Should().Be(pracownik, "ctor (Pracownik) ustawia pole Pracownik");
badanie.Definicja.Should().Be(definicja);
pracownik.BadaniaLekarskie.Cast<Soneta.Kadry.BadanieLekarskie>()
.Should().Contain(badanie, "wpis trafia do kolekcji SubTable pracownika");
}
[Test]
[Description("K1: pracownik.Badania to manager (BadaniaLekarskieManager) tylko do odczytu — inny obiekt " +
"niż kolekcja CRUD pracownik.BadaniaLekarskie (SubTable<BadanieLekarskie>).")]
public void K1_Badania_ManagerOdczytu_RozniSieOdKolekcjiCrud()
{
var pracownik = Host();
pracownik.Badania.Should().NotBeNull("manager Badania jest zawsze dostępny (odczyt)");
pracownik.Badania.Should().BeOfType<Prac.BadaniaLekarskieManager>();
// Kolekcja CRUD to osobne API — SubTable.
pracownik.BadaniaLekarskie.Should().NotBeNull();
}
// ============================== K2 — Szkolenia BHP ==============================
[Test]
[Description("K2: new SzkolenieBHP(pracownik) + Definicja (DefSzkolenBHP, wymagana); pole ważności to " +
"WażneDo (Z z-kreska) - w przeciwieństwie do K1; wpis trafia do pracownik.SzkoleniaBHP.")]
public void K2_SzkolenieBHP_DodanieZDefinicja_TrafiaDoKolekcji()
{
var definicja = Pierwsza<DefinicjaSzkoleniaBHP>(Kadry.DefSzkolenBHP);
if (definicja == null)
Assert.Ignore("Brak definicji szkolenia BHP (DefSzkolenBHP) w bazie Demo — wpisu nie można utworzyć.");
var pracownik = Host();
Soneta.Kadry.SzkolenieBHP szkolenie = null;
InTransaction(() =>
{
szkolenie = Session.AddRow(new Soneta.Kadry.SzkolenieBHP(pracownik));
szkolenie.Definicja = definicja;
szkolenie.Data = Date.Today;
// Termin jest WYLICZANY (read-only) z Data + definicji — nie ustawiamy go ręcznie.
szkolenie.Zakres = "Instruktaż ogólny";
szkolenie.Osoba = "Prowadzący BHP";
});
szkolenie.Pracownik.Should().Be(pracownik);
szkolenie.Definicja.Should().Be(definicja);
szkolenie.Zakres.Should().Be("Instruktaż ogólny");
pracownik.SzkoleniaBHP.Cast<Soneta.Kadry.SzkolenieBHP>().Should().Contain(szkolenie);
}
// ============================== K3 — Szkolenia i uprawnienia (HR) ==============================
[Test]
[Description("K3a: WniosekOSzkolenie([Required] Pracownik) z modułu Soneta.HR (session.GetHR()); Definicja " +
"(DefinicjeSzkolen) + Etap (EtapRealizSzkol) to słowniki HR; Koszt to Soneta.Types.Currency.")]
public void K3a_WniosekOSzkolenie_DodanieZBudzetemIKosztem_TrafiaDoKolekcji()
{
var hr = Session.GetHR();
var definicja = Pierwsza<DefinicjaSzkolenia>(hr.DefinicjeSzkolen);
if (definicja == null)
Assert.Ignore("Brak definicji szkolenia HR (DefinicjeSzkolen) w bazie Demo — wniosku nie można utworzyć.");
var pracownik = Host();
WniosekOSzkolenie wniosek = null;
InTransaction(() =>
{
wniosek = Session.AddRow(new WniosekOSzkolenie(pracownik));
wniosek.Definicja = definicja;
// Etap jest opcjonalny do zapisu — ustawiamy gdy słownik niepusty.
var etap = Pierwsza<EtapRealizacjiSzkolenia>(hr.EtapRealizSzkol);
if (etap != null)
wniosek.Etap = etap;
wniosek.DataZgloszenia = Date.Today;
wniosek.Koszt = new Currency(1500m); // Currency, nie decimal
});
wniosek.Pracownik.Should().Be(pracownik);
wniosek.Definicja.Should().Be(definicja);
wniosek.Koszt.Value.Should().Be(1500m);
pracownik.WnioskiOSzkolenia.Cast<WniosekOSzkolenie>().Should().Contain(wniosek);
}
[Test]
[Description("K3b: UkończoneSzkolenie([Required] Pracownik) moduł HR; pola Nazwa/Okres(FromTo)/Ocena; " +
"wpis trafia do pracownik.UkończoneSzkolenia. Drugi ctor (WniosekOSzkolenie) przepina pracownika.")]
public void K3b_UkonczoneSzkolenie_DodanieZPracownika_TrafiaDoKolekcji()
{
var pracownik = Host();
UkończoneSzkolenie ukonczone = null;
InTransaction(() =>
{
ukonczone = Session.AddRow(new UkończoneSzkolenie(pracownik));
ukonczone.Nazwa = "Kurs BHP aktualizacja";
ukonczone.Okres = new FromTo(Date.Today, Date.Today);
ukonczone.Ocena = "bardzo dobry";
});
ukonczone.Pracownik.Should().Be(pracownik);
ukonczone.Nazwa.Should().Be("Kurs BHP aktualizacja");
pracownik.UkończoneSzkolenia.Cast<UkończoneSzkolenie>().Should().Contain(ukonczone);
}
[Test]
[Description("K3c: UprawnieniePracownika([Required] Pracownik) moduł HR; Definicja (DefUprawnien, słownik), " +
"Numer, DataUzyskania/TerminWaznosci (Date); wpis trafia do pracownik.Uprawnienia.")]
public void K3c_UprawnieniePracownika_DodanieZDefinicja_TrafiaDoKolekcji()
{
var hr = Session.GetHR();
var definicja = Pierwsza<DefinicjaUprawnienia>(hr.DefUprawnien);
if (definicja == null)
Assert.Ignore("Brak definicji uprawnienia HR (DefUprawnien) w bazie Demo — uprawnienia nie można utworzyć.");
var pracownik = Host();
UprawnieniePracownika uprawnienie = null;
InTransaction(() =>
{
uprawnienie = Session.AddRow(new UprawnieniePracownika(pracownik));
uprawnienie.Definicja = definicja;
uprawnienie.Numer = "UP/2026/001";
uprawnienie.DataUzyskania = Date.Today;
uprawnienie.TerminWaznosci = new Date(Date.Today.Year + 5, Date.Today.Month, Date.Today.Day);
});
uprawnienie.Pracownik.Should().Be(pracownik);
uprawnienie.Definicja.Should().Be(definicja);
uprawnienie.Numer.Should().Be("UP/2026/001");
pracownik.Uprawnienia.Cast<UprawnieniePracownika>().Should().Contain(uprawnienie);
}
// ============================== K4 — Nagrody/kary; oświadczenia ==============================
[Test]
[Description("K4a: NagrodaKara jest ABSTRAKCYJNA — używamy podtypu new Nagroda(pracownik); ctor ustawia " +
"Typ na Nagroda; Definicja to słownik DefNagrodKar; wpis trafia do pracownik.NagrodyKary.")]
public void K4a_Nagroda_DodaniePodtypuKonkretnego_UstawiaTypNagroda()
{
// Definicja musi zgadzać się typem z wpisem — dla Nagrody bierzemy definicję o Typ == Nagroda
// (przypisanie niezgodnej typem definicji rzuca ArgumentException w set_Definicja).
var definicja = Kadry.DefNagrodKar.Cast<DefinicjaNagrodyKary>()
.FirstOrDefault(d => d.Typ == TypNagrodyKary.Nagroda);
if (definicja == null)
Assert.Ignore("Brak definicji typu Nagroda (DefNagrodKar) w bazie Demo — wpisu nie można utworzyć.");
var pracownik = Host();
Nagroda nagroda = null;
InTransaction(() =>
{
// NIE new NagrodaKara(...) — typ abstrakcyjny. Konkretny podtyp ustawia Typ.
nagroda = Session.AddRow(new Nagroda(pracownik));
nagroda.Definicja = definicja;
nagroda.Data = Date.Today;
});
nagroda.Pracownik.Should().Be(pracownik);
nagroda.Typ.Should().Be(TypNagrodyKary.Nagroda, "ctor podtypu Nagroda ustawia pole Typ");
pracownik.NagrodyKary.Cast<NagrodaKara>().Should().Contain(nagroda);
}
[Test]
[Description("K4a: konkretny podtyp Kara ustawia Typ na Kara; oba podtypy trafiają do tej samej kolekcji " +
"pracownik.NagrodyKary (SubTable<NagrodaKara>).")]
public void K4a_Kara_DodaniePodtypuKonkretnego_UstawiaTypKara()
{
// Dla Kary bierzemy definicję o Typ == Kara (analogicznie do Nagrody).
var definicja = Kadry.DefNagrodKar.Cast<DefinicjaNagrodyKary>()
.FirstOrDefault(d => d.Typ == TypNagrodyKary.Kara);
if (definicja == null)
Assert.Ignore("Brak definicji typu Kara (DefNagrodKar) w bazie Demo — wpisu nie można utworzyć.");
var pracownik = Host();
Kara kara = null;
InTransaction(() =>
{
kara = Session.AddRow(new Kara(pracownik));
kara.Definicja = definicja;
kara.Data = Date.Today;
});
kara.Typ.Should().Be(TypNagrodyKary.Kara, "ctor podtypu Kara ustawia pole Typ");
pracownik.NagrodyKary.Cast<NagrodaKara>().Should().Contain(kara);
}
[Test]
[Description("K4b: OświadczeniePracownika NIE ma ctora samego (Pracownik) — Definicja jest [Required] " +
"w konstruktorze; wariant (pracownik, definicja, Date) ustawia DataZlozenia; słownik DefOswiadczen.")]
public void K4b_Oswiadczenie_DodanieZWymaganaDefinicjaIData_TrafiaDoKolekcji()
{
// Preferuj PIT-2, ale dowolna definicja oświadczenia wystarcza (ctor wymaga definicji).
var definicja = Kadry.DefOswiadczen.Cast<DefinicjaOświadczenia>().FirstOrDefault(d => d.Nazwa == "PIT-2")
?? Pierwsza<DefinicjaOświadczenia>(Kadry.DefOswiadczen);
if (definicja == null)
Assert.Ignore("Brak definicji oświadczenia (DefOswiadczen) w bazie Demo — oświadczenia nie można utworzyć (definicja jest [Required] w ctorze).");
var pracownik = Host();
OświadczeniePracownika oswiadczenie = null;
InTransaction(() =>
{
// Definicja przekazywana w konstruktorze (nie ustawiana po fakcie); wariant z datą złożenia.
oswiadczenie = Session.AddRow(new OświadczeniePracownika(pracownik, definicja, Date.Today));
});
oswiadczenie.Pracownik.Should().Be(pracownik);
oswiadczenie.Definicja.Should().Be(definicja, "definicja jest przekazywana w ctorze");
oswiadczenie.DataZlozenia.Should().Be(Date.Today, "wariant ctora z Date ustawia DataZlozenia");
pracownik.Oświadczenia.Cast<OświadczeniePracownika>().Should().Contain(oswiadczenie);
}
// ============================== K5 — Wypadki przy pracy ==============================
[Test]
[Description("K5: new Wypadek(pracownik); Data to Date, Godzina to Soneta.Types.Time; pola opisowe " +
"(Okolicznosci/Skutki) to MemoText; flagi skutków to bool; wpis trafia do pracownik.Wypadki.")]
public void K5_Wypadek_DodanieZDanymiPodstawowymi_TrafiaDoKolekcji()
{
var pracownik = Host();
Soneta.Kadry.Wypadek wypadek = null;
InTransaction(() =>
{
wypadek = Session.AddRow(new Soneta.Kadry.Wypadek(pracownik));
wypadek.Data = Date.Today;
wypadek.Godzina = new Time(10, 30); // Soneta.Types.Time, nie DateTime
wypadek.DataZgloszenia = Date.Today;
wypadek.Miejsce = "Hala produkcyjna";
wypadek.PrzyPracy = true;
wypadek.Okolicznosci = (MemoText)"Poślizgnięcie na mokrej posadzce."; // MemoText (konwersja ze string), nie string
});
wypadek.Pracownik.Should().Be(pracownik);
wypadek.Miejsce.Should().Be("Hala produkcyjna");
wypadek.PrzyPracy.Should().BeTrue();
wypadek.Godzina.Should().Be(new Time(10, 30));
pracownik.Wypadki.Cast<Soneta.Kadry.Wypadek>().Should().Contain(wypadek);
}
[Test]
[Description("K5: Wypadek wymaga Definicja (Soneta.Core.DefinicjaDokumentu) do numeracji — Numer " +
"(NumerDokumentu) nadaje platforma. Sprawdzamy, że pole Definicja jest częścią kontraktu.")]
public void K5_Wypadek_PoleDefinicjaJestCzesciaKontraktu()
{
var pracownik = Host();
Soneta.Kadry.Wypadek wypadek = null;
InTransaction(() =>
{
wypadek = Session.AddRow(new Soneta.Kadry.Wypadek(pracownik));
wypadek.Data = Date.Today;
});
// Numer jest subrowem nadawanym wg Definicja — nie ustawiamy Numer.Pelny ręcznie.
wypadek.Numer.Should().NotBeNull("Numer to subrow NumerDokumentu zawsze obecny na wpisie");
}
}
@@ -0,0 +1,383 @@
using System.Linq;
using AwesomeAssertions;
using NUnit.Framework;
using Soneta.Business;
using Soneta.Core;
using Soneta.HR;
using Soneta.HR2;
using Soneta.Kadry;
using Soneta.Types;
using Prac = Soneta.Kadry.Pracownik;
namespace Soneta.Skills.Test.KadryPlace.Pracownik;
/// <summary>
/// Rozdział K (część druga) — RODO/GIODO, struktura organizacyjna, oceny, rekrutacja (receptury K6K9).
/// <para>
/// Testy to <b>wykonywalna dokumentacja</b> publicznego kontraktu platformy Soneta dla zaawansowanych
/// obszarów kadrowych. Wszystkie te obszary łączy jedna cecha: rekordy operacyjne wymagają
/// <b>referencji do definicji konfiguracyjnych</b> (słowników GIODO, struktury organizacyjnej, ocen,
/// stanowisk/etapów rekrutacji), które w bazie Demo (GoldStandard) <b>mogą nie istnieć</b>. Strategia
/// jest jednolita: definicję pobieramy dynamicznie (pierwszy rekord z tabeli konfiguracyjnej); gdy
/// jej brak — test jest oznaczany <c>Assert.Ignore</c> z powodem. Tam, gdzie da się przetestować
/// realnie (odczyt kolekcji, dodanie wpisu przy dostępnej definicji), robimy to na żywych danych.
/// </para>
/// <list type="bullet">
/// <item><b>K6</b> — RODO/GIODO: <c>new GIODOOświadczenie(pracownik, def)</c>, <c>new GIODOUprawnienie(pracownik, def)</c>;
/// kolekcje <c>GIODOOświadczenia</c>/<c>GIODOUprawnienia</c>/<c>GIODOUdostępnienia</c>;
/// <c>GIODOWymianaDanych</c> bez publicznego ctora → tylko odczyt + [Ignore]; zapis teczki do pliku → [Ignore].</item>
/// <item><b>K7</b> — struktura organizacyjna: <c>new PowiązanieStrukturyOrganizacyjnej(element, pracownik)</c>,
/// <c>Etat.Wydzial</c> (dane historyczne), manager <c>StrukturaOraganizacyjna</c> (odczyt).</item>
/// <item><b>K8</b> — oceny: <c>new OcenaPracownika(pracownik)</c> + <c>new ElementOcenyPracownika(ocena)</c>,
/// <c>new CelOkresowyPracownika(pracownik)</c>.</item>
/// <item><b>K9</b> — rekrutacja: <c>new RekrutacjaAplikacja(pracownik, wydziałDefStanowiska)</c>,
/// <c>new Rekrutacja(pracownik)</c>, <c>new EtapRekrutacji(rekrutacja)</c>.</item>
/// </list>
/// <para>
/// Operujemy wyłącznie na <b>publicznym kontrakcie</b> — jak dodatek programisty zewnętrznego bez
/// dostępu do źródeł. Baza Demo z rollbackiem po teście (helper <c>InTransaction</c> z <c>TestBase</c>).
/// </para>
/// </summary>
[TestFixture]
public class RozdzialK2_RodoZzlTest : PracownikTestBase
{
// Pracownik-host dla wpisów — dowolny etatowy z Demo (stabilny punkt wejścia).
private Prac Host() => Pracownik(Pracownik_.Andrzejewski) ?? PierwszyPracownik();
// Pierwszy rekord z tabeli konfiguracyjnej (lub null) — bez twardej zależności od nazwy słownika.
private static T Pierwsza<T>(Table tabela) where T : Row =>
tabela.Cast<T>().FirstOrDefault();
// ============================== K6 — RODO/GIODO ==============================
[Test]
[Description("K6: new GIODOOświadczenie(pracownik, definicja) — Host wynika z ctora (Pracownik implementuje " +
"IGIODOOświadczenieHost), Definicja (GIODODefOswiadcz) jest WYMAGANA przez ctor; pole Data to Date; " +
"Rodzaj/Okres są WYLICZANE (read-only) z definicji; wpis trafia do pracownik.GIODOOświadczenia. " +
"Gdy w Demo brak definicji oświadczenia lub brak prawa zapisu do obszaru RODO → Ignore.")]
public void K6_GIODOOswiadczenie_DodanieZDefinicja_TrafiaDoKolekcji()
{
// Tabela konfiguracyjna czytana wprost z sesji operacyjnej (jak słowniki w K1).
var def = Pierwsza<GIODODefinicjaOświadczenia>(Session.GetCore().GIODODefOswiadcz);
if (def == null)
Assert.Ignore("Brak definicji oświadczenia GIODO (CoreModule.GIODODefOswiadcz) w bazie Demo — wpisu nie można utworzyć (Definicja jest wymagana w ctorze).");
var pracownik = Host();
GIODOOświadczenie oswiadczenie = null;
try
{
InTransaction(() =>
{
// Definicja wynika z ctora; Rodzaj/Okres są wyliczane przez platformę — nie ustawiamy ich ręcznie.
oswiadczenie = Session.AddRow(new GIODOOświadczenie(pracownik, def));
oswiadczenie.Data = Date.Today;
oswiadczenie.SposobPozyskania = "Formularz papierowy";
});
}
catch (AccessWriteDeniedException)
{
// Egzekucji praw nie testujemy (safe-code §7.2) — rola Demo blokuje zapis do obszaru RODO/GIODO.
Assert.Ignore("Rola bazy Demo nie ma prawa zapisu do GIODOOświadczenie — egzekucji praw nie testujemy (safe-code §7.2).");
}
oswiadczenie.Host.Should().Be(pracownik, "ctor (host, definicja) ustawia Host na pracownika");
oswiadczenie.Definicja.Should().Be(def, "Definicja przekazywana jest w ctorze");
oswiadczenie.Data.Should().Be(Date.Today);
pracownik.GIODOOświadczenia.Cast<GIODOOświadczenie>()
.Should().Contain(oswiadczenie, "wpis trafia do kolekcji SubTable pracownika");
}
[Test]
[Description("K6: new GIODOUprawnienie(pracownik, definicja) — Uprawniony z ctora (IGIODOUprawnienieHost), " +
"Definicja (GIODODefUprawn) wymagana; pola Data/Przyznane/Odebrane to Date (Okres jest wyliczany, " +
"read-only); wpis trafia do pracownik.GIODOUprawnienia. Gdy brak definicji w Demo → Ignore.")]
public void K6_GIODOUprawnienie_DodanieZDefinicja_TrafiaDoKolekcji()
{
var def = Pierwsza<GIODODefinicjaUprawnienia>(Session.GetCore().GIODODefUprawn);
if (def == null)
Assert.Ignore("Brak definicji uprawnienia GIODO (CoreModule.GIODODefUprawn) w bazie Demo — wpisu nie można utworzyć.");
var pracownik = Host();
GIODOUprawnienie uprawnienie = null;
try
{
InTransaction(() =>
{
uprawnienie = Session.AddRow(new GIODOUprawnienie(pracownik, def));
uprawnienie.Data = Date.Today;
uprawnienie.Przyznane = Date.Today; // Okres jest wyliczany — nie ustawiamy go bezpośrednio.
});
}
catch (AccessWriteDeniedException)
{
Assert.Ignore("Rola bazy Demo nie ma prawa zapisu do GIODOUprawnienie — egzekucji praw nie testujemy (safe-code §7.2).");
}
uprawnienie.Uprawniony.Should().Be(pracownik, "ctor (uprawniony, definicja) ustawia Uprawniony");
uprawnienie.Definicja.Should().Be(def);
uprawnienie.Przyznane.Should().Be(Date.Today);
pracownik.GIODOUprawnienia.Cast<GIODOUprawnienie>().Should().Contain(uprawnienie);
}
[Test]
[Description("K6: GIODOWymianaDanych (pozyskanie/udostępnienie/powierzenie) NIE ma publicznego ctora — " +
"rekordy tworzą wyłącznie workery (DodajPozyskanieDanychWorker itd.). Kolekcja GIODOUdostępnienia " +
"jest jednak dostępna do ODCZYTU jako część publicznego kontraktu.")]
public void K6_GIODOUdostepnienia_KolekcjaDostepnaDoOdczytu()
{
var pracownik = Host();
// GIODOUdostępnienia to SubTable<GIODOWymianaDanych> — odczyt jest częścią kontraktu,
// nawet gdy w Demo nie ma żadnych zapisów wymiany danych.
pracownik.GIODOUdostępnienia.Should().NotBeNull("kolekcja wymiany danych jest zawsze dostępna (odczyt)");
pracownik.GIODOUdostępnienia.Cast<GIODOWymianaDanych>().Should().OnlyContain(w => w != null);
}
[Test]
[Ignore("Dodanie GIODOWymianaDanych wymaga workera (DodajPozyskanieDanychWorker/DodajUdostępnienieDanychWorker/" +
"DodajPowierzenieDanychWorker) oraz podmiotu (IKontrahent) i — w zależności od kierunku — definicji " +
"dokumentu/zbioru danych z konfiguracji modułu RODO, których baza Demo może nie mieć. Brak publicznego " +
"ctora uniemożliwia deterministyczny zapis bez tej konfiguracji.")]
[Description("K6: dodanie zapisu wymiany danych GIODO przez DodajPozyskanieDanychWorker (CommitUI + Save).")]
public void K6_GIODOWymianaDanych_DodaniePrzezWorker()
{
}
[Test]
[Ignore("Zapis teczki personalnej (Pracownik.ZapiszTeczkęDoPlikuWorker.ZapiszTeczkeDoPliku()) to operacja " +
"plikowa — serializuje dokumentację pracownika do plików/katalogu na dysku. Poza zakresem testów " +
"jednostkowych (zależność od systemu plików).")]
[Description("K6: zapis teczki personalnej RODO do pliku (operacja plikowa).")]
public void K6_ZapisTeczkiDoPliku()
{
}
// ============================== K7 — Struktura organizacyjna ==============================
[Test]
[Description("K7: new PowiązanieStrukturyOrganizacyjnej(element, pracownik) — Zrodlo z ctora (Pracownik " +
"implementuje IŹródłoPowiązaniaStrukturyOrganizacyjnej), Element to istniejący element struktury " +
"(CoreModule.ElementyStrOrg — NIE definicja DefElStrukturOrg); Okres to FromTo; wpis trafia do " +
"pracownik.PowiązaniaStrOrg. Gdy brak elementów struktury w Demo lub brak prawa zapisu → Ignore.")]
public void K7_PowiazanieStruktury_DodanieZElementem_TrafiaDoKolekcji()
{
// Elementy struktury (instancje) są w ElementyStrOrg; DefElStrukturOrg trzyma DEFINICJE elementów.
var element = Pierwsza<ElementStrukturyOrganizacyjnej>(Session.GetCore().ElementyStrOrg);
if (element == null)
Assert.Ignore("Brak elementów struktury organizacyjnej (CoreModule.ElementyStrOrg) w bazie Demo — powiązania nie można utworzyć.");
var pracownik = Host();
PowiązanieStrukturyOrganizacyjnej powiazanie = null;
try
{
InTransaction(() =>
{
powiazanie = Session.AddRow(new PowiązanieStrukturyOrganizacyjnej(element, pracownik));
powiazanie.Okres = new FromTo(Date.Today, Date.MaxValue);
});
}
catch (AccessWriteDeniedException)
{
Assert.Ignore("Rola bazy Demo nie ma prawa zapisu do PowiązanieStrukturyOrganizacyjnej — egzekucji praw nie testujemy (safe-code §7.2).");
}
powiazanie.Zrodlo.Should().Be(pracownik, "ctor (element, zrodlo) ustawia Zrodlo na pracownika");
powiazanie.Element.Should().Be(element);
pracownik.PowiązaniaStrOrg.Cast<PowiązanieStrukturyOrganizacyjnej>().Should().Contain(powiazanie);
}
[Test]
[Description("K7: pracownik.StrukturaOraganizacyjna to manager (StrukturaOraganizacyjnaManager) — API tylko " +
"do odczytu nawigacji przełożeni/podwładni. Jest zawsze dostępny, niezależnie od konfiguracji struktury.")]
public void K7_StrukturaOrganizacyjna_ManagerOdczytuJestDostepny()
{
var pracownik = Host();
pracownik.StrukturaOraganizacyjna.Should().NotBeNull("manager struktury jest zawsze dostępny (odczyt)");
pracownik.StrukturaOraganizacyjna.Should().BeOfType<Prac.StrukturaOraganizacyjnaManager>();
// Przełożony „na dzień" może być null (brak skonfigurowanej struktury) — czytamy bez wyjątku.
var _ = pracownik.StrukturaOraganizacyjna.GetDomyślnyPrzełożony(Date.Today);
}
[Test]
[Description("K7: Etat.Wydzial to dane HISTORYCZNE (na PracHistoria.Etat) i jednostka organizacyjna pracownika. " +
"Dla etatowego pracownika z Demo wydział na zapisie obowiązującym dziś jest ustawiony (wymagany dla etatu).")]
public void K7_EtatWydzial_JestUstawionyDlaEtatowca()
{
var pracownik = Host();
var ph = pracownik[Date.Today]; // zapis historii obowiązujący na dzień (A15)
ph.Should().NotBeNull("etatowy pracownik z Demo ma zapis historii obowiązujący dziś");
// Wydzial jest wymagany dla etatu — odczyt jako część kontraktu (referencja do Soneta.Kadry.Wydzial).
ph.Etat.Should().NotBeNull();
ph.Etat.Wydzial.Should().NotBeNull("Etat.Wydzial (jednostka organizacyjna) jest wymagany dla etatu");
}
// ============================== K8 — Oceny okresowe ==============================
[Test]
[Description("K8: new OcenaPracownika(pracownik) (arkusz, root w HR.OcenyPracownikow) + new ElementOcenyPracownika(ocena) " +
"gdzie ocena jest IOcenaPracownika; ElementOcenyPracownika.Wartosc to decimal (Typ/Data są wyliczane, read-only). " +
"Element wymaga Definicja (HR.DefElemOcenPrac) — gdy brak w Demo, sam arkusz i pusta kolekcja elementów wystarczają.")]
public void K8_OcenaPracownika_ArkuszZElementem_TrafiaDoKolekcji()
{
var hr = Session.GetHR();
var pracownik = Host();
var defElementu = Pierwsza<DefElementuOcenyPracownika>(hr.DefElemOcenPrac);
OcenaPracownika ocena = null;
ElementOcenyPracownika element = null;
InTransaction(() =>
{
ocena = Session.AddRow(new OcenaPracownika(pracownik));
ocena.Nazwa = "Ocena roczna 2026";
ocena.Data = Date.Today;
// Element dodajemy tylko gdy istnieje definicja (Definicja jest wymagana do zapisu elementu).
if (defElementu != null)
{
element = Session.AddRow(new ElementOcenyPracownika(ocena)); // ocena jako IOcenaPracownika
element.Definicja = defElementu;
element.Wartosc = 4m; // Wartosc to decimal (Typ/Data ustawia platforma na podstawie definicji)
}
});
ocena.Pracownik.Should().Be(pracownik, "ctor (Pracownik) ustawia ocenianego");
ocena.Nazwa.Should().Be("Ocena roczna 2026");
pracownik.Oceny.Cast<OcenaPracownika>().Should().Contain(ocena, "arkusz trafia do kolekcji pracownika");
if (defElementu != null)
{
element.Ocena.Should().Be(ocena, "ctor (IOcenaPracownika) wiąże element z arkuszem");
element.Wartosc.Should().Be(4m);
ocena.ElementyOceny.Cast<ElementOcenyPracownika>().Should().Contain(element);
}
else
{
Assert.Warn("Brak definicji elementu oceny (HR.DefElemOcenPrac) w Demo — przetestowano sam arkusz oceny bez pozycji.");
}
}
[Test]
[Description("K8: new CelOkresowyPracownika(pracownik) (root w HR2.CeleOkresowePrac); pola Nazwa/Data/Termin/Opis; " +
"Definicja to Soneta.Oceny.DefinicjaElementuOceny (opcjonalna referencja konfiguracyjna); wpis trafia " +
"do pracownik.CeleOkresowe.")]
public void K8_CelOkresowy_Dodanie_TrafiaDoKolekcji()
{
var pracownik = Host();
CelOkresowyPracownika cel = null;
InTransaction(() =>
{
cel = Session.AddRow(new CelOkresowyPracownika(pracownik));
cel.Nazwa = "Wdrożenie nowego modułu";
cel.Data = Date.Today;
cel.Termin = new Date(2026, 12, 31);
cel.Opis = (MemoText)"Cel rozwojowy na bieżący okres oceny.";
});
cel.Pracownik.Should().Be(pracownik, "ctor (Pracownik) ustawia pracownika celu");
cel.Nazwa.Should().Be("Wdrożenie nowego modułu");
cel.Termin.Should().Be(new Date(2026, 12, 31));
pracownik.CeleOkresowe.Cast<CelOkresowyPracownika>().Should().Contain(cel);
}
// ============================== K9 — Rekrutacja ==============================
[Test]
[Description("K9: new RekrutacjaAplikacja(kandydat, wydziałDefStanowiska) — kandydat to Pracownik, ctor przyjmuje " +
"WydziałDefinicjiStanowiska (powstaje z new WydziałDefinicjiStanowiska(DefinicjaStanowiska) — typ z Soneta.HR). " +
"Stan to StanAplikacji; wpis trafia do kandydat.Aplikacje. Gdy brak definicji stanowiska (HR.DefStanowisk) → Ignore.")]
public void K9_RekrutacjaAplikacja_DodanieZeStanowiskiem_TrafiaDoKolekcji()
{
var hr = Session.GetHR();
var defStanowiska = Pierwsza<DefinicjaStanowiska>(hr.DefStanowisk);
if (defStanowiska == null)
Assert.Ignore("Brak definicji stanowiska (HR.DefStanowisk) w bazie Demo — aplikacji rekrutacyjnej nie można utworzyć (ctor wymaga WydziałDefinicjiStanowiska).");
var kandydat = Host();
RekrutacjaAplikacja aplikacja = null;
InTransaction(() =>
{
// WydziałDefinicjiStanowiska powstaje z DefinicjaStanowiska (ctor w Soneta.HR).
var wydzialDef = new WydziałDefinicjiStanowiska(defStanowiska);
aplikacja = Session.AddRow(new RekrutacjaAplikacja(kandydat, wydzialDef));
aplikacja.Data = Date.Today;
aplikacja.Stan = StanAplikacji.Wprowadzona;
});
aplikacja.Pracownik.Should().Be(kandydat, "ctor (Pracownik, …) ustawia kandydata");
aplikacja.Stanowisko.Should().Be(defStanowiska, "WydziałDefinicjiStanowiska niesie referencję do DefinicjaStanowiska");
aplikacja.Stan.Should().Be(StanAplikacji.Wprowadzona);
kandydat.Aplikacje.Cast<RekrutacjaAplikacja>().Should().Contain(aplikacja);
}
[Test]
[Description("K9: new Rekrutacja(kandydat) (root w HR.Rekrutacje; impl. IOcenaPracownika) ustawia pole Pracownik; " +
"+ new EtapRekrutacji(rekrutacja) wiąże etap przez pole Rekrutacja; Etap.Definicja to HR.DefEtaRekrutacji " +
"(wymagana do zapisu etapu), Etap.Lp/Data. Gdy brak definicji etapu w Demo, testujemy samą rekrutację (warn). " +
"Gdy brak prawa zapisu → Ignore.")]
public void K9_RekrutacjaIEtap_Dodanie_TrafiaDoKolekcji()
{
var hr = Session.GetHR();
var kandydat = Host();
var defEtapu = Pierwsza<DefinicjaEtapuRekrutacji>(hr.DefEtaRekrutacji);
Rekrutacja rekrutacja = null;
EtapRekrutacji etap = null;
try
{
InTransaction(() =>
{
rekrutacja = Session.AddRow(new Rekrutacja(kandydat));
if (defEtapu != null)
{
etap = Session.AddRow(new EtapRekrutacji(rekrutacja));
etap.Definicja = defEtapu;
etap.Lp = 1;
etap.Data = Date.Today;
}
});
}
catch (AccessWriteDeniedException)
{
Assert.Ignore("Rola bazy Demo nie ma prawa zapisu do Rekrutacja/EtapRekrutacji — egzekucji praw nie testujemy (safe-code §7.2).");
}
rekrutacja.Should().NotBeNull("ctor (Pracownik) tworzy rekrutację dla kandydata");
rekrutacja.Pracownik.Should().Be(kandydat, "ctor (Pracownik) ustawia kandydata rekrutacji");
// Rekrutacja jest rootem w HR.Rekrutacje (kolekcje na Pracowniku wiążą się przez relacje child).
hr.Rekrutacje.Cast<Rekrutacja>().Should().Contain(rekrutacja, "rekrutacja trafia do tabeli głównej HR.Rekrutacje");
if (defEtapu != null)
{
etap.Rekrutacja.Should().Be(rekrutacja, "ctor (Rekrutacja) wiąże etap z rekrutacją");
etap.Lp.Should().Be(1);
hr.EtapyRekrutacji.Cast<EtapRekrutacji>().Should().Contain(etap, "etap trafia do tabeli głównej HR.EtapyRekrutacji");
}
else
{
Assert.Warn("Brak definicji etapu rekrutacji (HR.DefEtaRekrutacji) w Demo — przetestowano samą rekrutację bez etapów.");
}
}
[Test]
[Description("K9: kandydat.Aplikacje / Rekrutacje / EtapyRekrutacji / Kandydatury to kolekcje SubTable dostępne " +
"do odczytu jako część publicznego kontraktu — niezależnie od stanu konfiguracji rekrutacji.")]
public void K9_KolekcjeRekrutacji_DostepneDoOdczytu()
{
var kandydat = Host();
kandydat.Aplikacje.Should().NotBeNull();
kandydat.Rekrutacje.Should().NotBeNull();
kandydat.EtapyRekrutacji.Should().NotBeNull();
kandydat.Kandydatury.Should().NotBeNull();
}
}
@@ -0,0 +1,59 @@
using System.Linq;
using AwesomeAssertions;
using NUnit.Framework;
using Soneta.Business;
using Soneta.Test;
using Prac = Soneta.Kadry.Pracownik;
namespace Soneta.Skills.Test.KadryPlace.Pracownik;
/// <summary>
/// Test dymny (smoke) potwierdzający, że infrastruktura testowa domeny Kadry/Płace działa:
/// sesja operacyjna jest powiązana z bazą Demo, moduły są dostępne, a kartoteka pracowników
/// jest niepusta. To minimalny punkt wejścia, na którym opierają się pozostałe rozdziały.
/// </summary>
[TestFixture]
public class SmokeTest : PracownikTestBase
{
[Test]
[Description("Moduły Kadry/Płace/Kalendarz są dostępne z sesji i wskazują z powrotem na tę samą sesję.")]
public void Moduly_DostepneIWskazujaNaSesje()
{
// Punkt wejścia każdego scenariusza: z Session pobieramy moduły metodami rozszerzającymi
// (GetKadry/GetPlace/GetKalend). Każdy moduł implementuje ISessionable.
Kadry.Should().NotBeNull("session.GetKadry() musi zwrócić moduł Kadry");
Place.Should().NotBeNull("session.GetPlace() musi zwrócić moduł Płace");
Kalend.Should().NotBeNull("session.GetKalend() musi zwrócić moduł Kalendarz");
Kadry.Session.Should().BeSameAs(Session);
Place.Session.Should().BeSameAs(Session);
Kalend.Session.Should().BeSameAs(Session);
}
[Test]
[Description("Kartoteka pracowników (Pracownicy) z bazy Demo jest niepusta, a lookup po kodzie " +
"(WgKodu) zwraca rekord o zgodnym kodzie — to fundament scenariuszy odczytu.")]
public void Pracownicy_KartotekaNiepusta_LookupPoKodzieDziala()
{
// Iteracja po kluczu WgKodu zwraca wiersze; klucz jest niegeneryczny, więc rzutujemy.
var wszyscy = Kadry.Pracownicy.WgKodu.Cast<Prac>().ToList();
wszyscy.Should().NotBeEmpty("baza Demo zawiera zatrudnionych pracowników");
// Klucz unikalny WgKodu[kod] zwraca pojedynczy rekord lub null.
var pierwszy = wszyscy.First();
var poKodzie = Pracownik(pierwszy.Kod);
poKodzie.Should().BeSameAs(pierwszy, "WgKodu[kod] to klucz unikalny — ten sam rekord co z iteracji");
}
[Test]
[Description("Pracownik etatowy z Demo ma co najmniej jeden zapis historii kadrowej (PracHistoria), " +
"w której przechowywane są dane kadrowe i warunki etatu obowiązujące w danym okresie.")]
public void Pracownik_MaZapisHistoriiKadrowej()
{
var p = PierwszyPracownik();
// Pracownik to obiekt historyczny: dane „na dzień" leżą w kolekcji Historia (HistorySubTable).
p.Historia.Cast<object>().Should().NotBeEmpty(
"zatrudniony pracownik ma przynajmniej jeden zapis historyczny z danymi kadrowymi i etatem");
}
}