Files
soneta-erp-skills/Soneta.Skills.Test/KadryPlace/Pracownik/RozdzialEF_PlanRcpTest.cs
T
2026-06-06 22:33:15 +02:00

277 lines
13 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
}
}