Soneta.Skills.Test
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Soneta.Business;
|
||||
using Soneta.CRM;
|
||||
using Soneta.Handel;
|
||||
using Soneta.Magazyny;
|
||||
using Soneta.Towary;
|
||||
using Soneta.Types;
|
||||
using Soneta.Test;
|
||||
|
||||
namespace Soneta.Skills.Test.Handel.DokumentyHandlowe;
|
||||
|
||||
/// <summary>
|
||||
/// Wspólna baza testów dokumentu handlowego. 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,</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 dokumentu handlowego: dostęp do modułów
|
||||
/// (Handel, Magazyny, Towary, CRM), pobieranie definicji dokumentów i danych słownikowych z bazy Demo
|
||||
/// oraz publiczne metody tworzenia dokumentu i jego pozycji.
|
||||
/// <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 DokumentHandlowyTestBase : TestBase
|
||||
{
|
||||
// === Moduły bieżącej sesji operacyjnej ===
|
||||
|
||||
/// <summary>Moduł Handel — definicje dokumentów, tabela dokumentów handlowych.</summary>
|
||||
protected HandelModule Handel => Session.GetHandel();
|
||||
|
||||
/// <summary>Moduł Magazyny — magazyny, zasoby, obroty, partie (grupy dostaw).</summary>
|
||||
protected MagazynyModule Magazyny => Session.GetMagazyny();
|
||||
|
||||
/// <summary>Moduł Towary — kartoteka towarów, jednostki, ceny.</summary>
|
||||
protected TowaryModule Towary => Session.GetTowary();
|
||||
|
||||
/// <summary>Moduł CRM — kartoteka kontrahentów.</summary>
|
||||
protected CRMModule Crm => Session.GetCRM();
|
||||
|
||||
// === Symbole danych dostępnych w bazie Demo (GoldStandard) ===
|
||||
|
||||
/// <summary>Symbole definicji dokumentów dostępnych w bazie Demo (pole <c>DefDokHandlowego.Symbol</c>).</summary>
|
||||
protected static class Definicje
|
||||
{
|
||||
public const string FakturaSprzedazy = "FV";
|
||||
/// <summary>
|
||||
/// Zakup. UWAGA: w bazie Demo (GoldStandard) NIE ma faktury zakupu jako dokumentu handlowego —
|
||||
/// wszystkie definicje F* mają kategorię „Sprzedaż". Stronę zakupową reprezentuje przyjęcie
|
||||
/// magazynowe od dostawcy „PZ" (przychód). W produkcyjnym enova faktura zakupu ma zwykle symbol „FZ".
|
||||
/// </summary>
|
||||
public const string FakturaZakupu = "PZ";
|
||||
public const string Paragon = "PAR";
|
||||
public const string PrzyjecieZewnetrzne = "PZ";
|
||||
public const string PrzyjecieWewnetrzne = "PW";
|
||||
public const string WydanieZewnetrzne = "WZ";
|
||||
public const string RozchodWewnetrzny = "RW";
|
||||
public const string ZamowienieOdbiorcy = "ZO";
|
||||
public const string ZamowienieDoDostawcy = "ZD";
|
||||
public const string PrzesuniecieMM = "MM";
|
||||
public const string Inwentaryzacja = "INW";
|
||||
}
|
||||
|
||||
/// <summary>Kody towarów z bazy Demo.</summary>
|
||||
protected static class Towar_
|
||||
{
|
||||
/// <summary>Towar magazynowy w sztukach.</summary>
|
||||
public const string Bikini = "BIKINI";
|
||||
/// <summary>Usługa (bez wpływu na magazyn).</summary>
|
||||
public const string Montaz = "MONTAZ";
|
||||
/// <summary>Towar rozliczany w km.</summary>
|
||||
public const string Transport = "TRANSPORT";
|
||||
}
|
||||
|
||||
/// <summary>Kody kontrahentów z bazy Demo.</summary>
|
||||
protected static class Kontrahent_
|
||||
{
|
||||
public const string Abc = "Abc";
|
||||
public const string Zefir = "ZEFIR";
|
||||
}
|
||||
|
||||
/// <summary>Symbole magazynów z bazy Demo.</summary>
|
||||
protected static class Magazyn_
|
||||
{
|
||||
/// <summary>Magazyn „Firma" (symbol „F").</summary>
|
||||
public const string Firma = "F";
|
||||
}
|
||||
|
||||
// === Wyszukiwanie obiektów słownikowych / kartotekowych ===
|
||||
|
||||
/// <summary>Pobiera definicję dokumentu handlowego po symbolu (np. „FV", „PW").</summary>
|
||||
protected DefDokHandlowego Definicja(string symbol) => Handel.DefDokHandlowych.WgSymbolu[symbol];
|
||||
|
||||
/// <summary>Pobiera kontrahenta po kodzie (klucz unikalny, case-insensitive).</summary>
|
||||
protected Kontrahent Kontrahent(string kod) => Crm.Kontrahenci.WgKodu[kod];
|
||||
|
||||
/// <summary>Pobiera towar po kodzie.</summary>
|
||||
protected Towar Towar(string kod) => Towary.Towary.WgKodu[kod];
|
||||
|
||||
/// <summary>Pobiera magazyn po symbolu (np. „F").</summary>
|
||||
protected Magazyn Magazyn(string symbol) => Magazyny.Magazyny.WgSymbol[symbol];
|
||||
|
||||
// === Tworzenie dokumentu i pozycji (publiczne API) ===
|
||||
|
||||
/// <summary>
|
||||
/// Tworzy nowy dokument handlowy w bieżącej sesji wewnątrz transakcji edycyjnej.
|
||||
/// Kolejność jest istotna: najpierw <c>AddRow</c>, potem <c>Definicja</c> (wyznacza kierunek
|
||||
/// magazynu i przelicza parametry dokumentu), następnie kontrahent i magazyn.
|
||||
/// </summary>
|
||||
/// <param name="defSymbol">Symbol definicji dokumentu (np. „FV", „PW").</param>
|
||||
/// <param name="kontrahent">Kontrahent dokumentu; <c>null</c> dla dokumentów wewnętrznych.</param>
|
||||
/// <param name="magazyn">Magazyn dokumentu; <c>null</c> gdy definicja go nie wymaga.</param>
|
||||
protected DokumentHandlowy UtworzDokument(
|
||||
string defSymbol,
|
||||
Kontrahent kontrahent = null,
|
||||
Magazyn magazyn = null)
|
||||
{
|
||||
DokumentHandlowy dok = null;
|
||||
InTransaction(() =>
|
||||
{
|
||||
dok = new DokumentHandlowy();
|
||||
Session.AddRow(dok);
|
||||
dok.Definicja = Definicja(defSymbol);
|
||||
if (magazyn != null)
|
||||
dok.Magazyn = magazyn;
|
||||
if (kontrahent != null)
|
||||
dok.Kontrahent = kontrahent;
|
||||
});
|
||||
return dok;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dodaje pozycję do dokumentu. Ustawienie <c>Towar</c> inicjuje jednostkę miary na polach
|
||||
/// <c>Ilosc</c> i <c>Cena</c> — dlatego ilość i cenę tworzymy z symbolem już ustawionym przez towar.
|
||||
/// Wywołuj wewnątrz transakcji edycyjnej (np. w <c>InTransaction</c>).
|
||||
/// </summary>
|
||||
/// <param name="dok">Dokument, do którego dodajemy pozycję (musi być „żywy" w sesji).</param>
|
||||
/// <param name="towar">Towar pozycji.</param>
|
||||
/// <param name="ilosc">Ilość w jednostce towaru.</param>
|
||||
/// <param name="cena">Cena jednostkowa; <c>null</c> = nie nadpisuj (zostanie pobrana z cennika).</param>
|
||||
protected static PozycjaDokHandlowego DodajPozycje(
|
||||
DokumentHandlowy dok,
|
||||
Towar towar,
|
||||
double ilosc,
|
||||
double? cena = null)
|
||||
{
|
||||
var poz = new PozycjaDokHandlowego(dok);
|
||||
dok.Session.AddRow(poz);
|
||||
poz.Towar = towar;
|
||||
poz.Ilosc = new Quantity(ilosc, poz.Ilosc.Symbol);
|
||||
if (cena.HasValue)
|
||||
poz.Cena = new DoubleCy(cena.Value, poz.Cena.Symbol);
|
||||
return poz;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wprowadza towar na stan magazynu „F" przez utworzenie i <b>zatwierdzenie</b> przyjęcia (PW),
|
||||
/// a następnie zapis (<c>SaveDispose</c>). Dopiero zatwierdzone przyjęcie księguje zasoby/obroty —
|
||||
/// bez tego baza Demo (kontrola stanu ujemnego) odrzuci każdy rozchód (FV/WZ/RW) tego towaru.
|
||||
/// <para>Wywołuj na początku testu rozchodowego; po nim pracuj na świeżej sesji (np. tworząc FV).</para>
|
||||
/// </summary>
|
||||
/// <returns>Guid zapisanego, zatwierdzonego dokumentu przyjęcia.</returns>
|
||||
protected Guid PrzyjmijNaStan(string towarKod, double ilosc, double cena = 10)
|
||||
{
|
||||
var pw = UtworzDokument(Definicje.PrzyjecieWewnetrzne, magazyn: Magazyn(Magazyn_.Firma));
|
||||
InTransaction(() => DodajPozycje(pw, Towar(towarKod), ilosc, cena));
|
||||
InTransaction(() => pw.Stan = StanDokumentuHandlowego.Zatwierdzony);
|
||||
var guid = pw.Guid;
|
||||
SaveDispose();
|
||||
return guid;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
using System.Linq;
|
||||
using AwesomeAssertions;
|
||||
using NUnit.Framework;
|
||||
using Soneta.Handel;
|
||||
using Soneta.Types;
|
||||
|
||||
namespace Soneta.Skills.Test.Handel.DokumentyHandlowe;
|
||||
|
||||
/// <summary>
|
||||
/// Rozdział 1 — „Fundamenty i identyfikacja” (W1–W3) dokumentu handlowego.
|
||||
/// Testy pełnią podwójną rolę: weryfikują publiczny kontrakt platformy ORAZ stanowią dokumentację
|
||||
/// poprawnych wzorców kodu dla programisty dodatku zewnętrznego. Pokrywają:
|
||||
/// <list type="bullet">
|
||||
/// <item>W1 — dostęp z sesji do modułów handlowo-magazynowych (Handel/Magazyny/Towary/CRM)
|
||||
/// oraz do tabeli dokumentów <c>DokHandlowe</c>;</item>
|
||||
/// <item>W2 — wybór definicji dokumentu (<c>DefDokHandlowego</c>) po symbolu (klucz unikalny);</item>
|
||||
/// <item>W3 — rozpoznanie rodzaju dokumentu (faktura / magazynowy / zamówienie / korekta / zaliczka)
|
||||
/// wg <c>Definicja.Kategoria</c> oraz flag dokumentu.</item>
|
||||
/// </list>
|
||||
/// Wszystko operuje wyłącznie na <b>publicznym kontrakcie</b> — tak jak dodatek bez dostępu do kodu źródłowego.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public class Rozdzial01_FundamentyTest : DokumentHandlowyTestBase
|
||||
{
|
||||
// ============================================================================================
|
||||
// W1 — Dostęp do modułów handlowo-magazynowych i tabeli DokHandlowe
|
||||
// ============================================================================================
|
||||
|
||||
[Test]
|
||||
[Description("W1: z sesji dostępne są wszystkie cztery moduły (Handel, Magazyny, Towary, CRM) " +
|
||||
"i każdy wskazuje z powrotem na tę samą sesję (ISessionable.Session).")]
|
||||
public void W1_DostepDoModulow_ModulyDostepneIWskazujaNaSesje()
|
||||
{
|
||||
// Punkt wejścia każdego scenariusza: z Session pobieramy moduły metodami rozszerzającymi.
|
||||
// Helpery bazy (Handel/Magazyny/Towary/Crm) opakowują session.GetHandel()/GetMagazyny() itd.
|
||||
Handel.Should().NotBeNull("session.GetHandel() musi zwrócić moduł Handel");
|
||||
Magazyny.Should().NotBeNull("session.GetMagazyny() musi zwrócić moduł Magazyny");
|
||||
Towary.Should().NotBeNull("session.GetTowary() musi zwrócić moduł Towary");
|
||||
Crm.Should().NotBeNull("session.GetCRM() musi zwrócić moduł CRM");
|
||||
|
||||
// Każdy moduł implementuje ISessionable — property Session zamyka pętlę dostępu do danych.
|
||||
Handel.Session.Should().BeSameAs(Session);
|
||||
Magazyny.Session.Should().BeSameAs(Session);
|
||||
Towary.Session.Should().BeSameAs(Session);
|
||||
Crm.Session.Should().BeSameAs(Session);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Description("W1: moduł Handel udostępnia tabelę dokumentów DokHandlowe oraz tabelę definicji " +
|
||||
"DefDokHandlowych — to dwa podstawowe punkty dostępu do danych handlowych.")]
|
||||
public void W1_ModulHandel_UdostepniaTabeleDokumentowIDefinicji()
|
||||
{
|
||||
// DokHandlowe — operacyjna tabela dokumentów (faktur, magazynowych, zamówień...).
|
||||
// DefDokHandlowych — konfiguracyjna tabela definicji wyznaczających rodzaj dokumentu.
|
||||
Handel.DokHandlowe.Should().NotBeNull("tabela dokumentów handlowych musi istnieć w module");
|
||||
Handel.DefDokHandlowych.Should().NotBeNull("tabela definicji dokumentów musi istnieć w module");
|
||||
|
||||
// Obie tabele należą do tej samej sesji co moduł (spójność kontekstu danych).
|
||||
Handel.DokHandlowe.Session.Should().BeSameAs(Session);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Description("W1: tabelę DokHandlowe iterujemy ZAWSZE z zawężeniem zakresu (filtr serwerowy na " +
|
||||
"indeksie WgDaty), zamiast ładować całą rosnącą tabelę operacyjną do pamięci.")]
|
||||
public void W1_IteracjaDokumentow_FiltrSerwerowyPoDacie_NieRzucaIDziala()
|
||||
{
|
||||
// Wzorzec safe-code: warunek RowCondition aplikujemy na indeksie (wykona się po stronie SQL).
|
||||
// W warunku używamy wyłącznie pól bazodanowych (Data) — pole kalkulowane rzuciłoby wyjątek.
|
||||
var od = Date.Today.AddMonths(-1);
|
||||
|
||||
// Sama materializacja zapytania (Count) potwierdza, że filtr serwerowy jest poprawny składniowo
|
||||
// i wykonalny; nie zakładamy konkretnej liczby dokumentów w bazie Demo (fakt niestabilny).
|
||||
var liczba = Handel.DokHandlowe
|
||||
.WgDaty[(DokumentHandlowy x) => x.Data >= od]
|
||||
.Count();
|
||||
|
||||
liczba.Should().BeGreaterThanOrEqualTo(0, "filtr serwerowy powinien się wykonać bez błędu");
|
||||
}
|
||||
|
||||
// ============================================================================================
|
||||
// W2 — Wybór definicji dokumentu (DefDokHandlowego) wg symbolu
|
||||
// ============================================================================================
|
||||
|
||||
[Test]
|
||||
[Description("W2: WgSymbolu to indeks UNIKALNY — dla istniejącego symbolu (FV) zwraca pojedynczy " +
|
||||
"rekord, którego Symbol odpowiada żądanemu (lookup symboli jest spójny).")]
|
||||
public void W2_DefinicjaPoSymbolu_KluczUnikalny_ZwracaRekordOZgodnymSymbolu()
|
||||
{
|
||||
// WgSymbolu["FV"] — klucz unikalny: zwraca pojedynczy DefDokHandlowego albo null.
|
||||
var defFV = Definicja(Definicje.FakturaSprzedazy);
|
||||
|
||||
defFV.Should().NotBeNull("baza Demo zawiera definicję faktury sprzedaży o symbolu FV");
|
||||
defFV.Symbol.Should().Be(Definicje.FakturaSprzedazy,
|
||||
"indeks WgSymbolu musi zwrócić rekord o dokładnie tym symbolu");
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Description("W2: dla symbolu NIEISTNIEJĄCEGO indeks unikalny WgSymbolu zwraca null — to sygnał " +
|
||||
"do walidacji przed utworzeniem dokumentu (nie zakładaj obecności symbolu na sztywno).")]
|
||||
public void W2_DefinicjaPoNieistniejacymSymbolu_ZwracaNull()
|
||||
{
|
||||
// Symbole zależą od konfiguracji bazy — zawsze sprawdzaj != null przed użyciem.
|
||||
var brak = Definicja("NIE_ISTNIEJE_XYZ");
|
||||
|
||||
brak.Should().BeNull("dla nieznanego symbolu klucz unikalny zwraca null, nie wyjątek");
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Description("W2: definicja jest PIERWSZYM polem nowego dokumentu — po jej ustawieniu dokument " +
|
||||
"ma przypisaną definicję o oczekiwanym symbolu (UtworzDokument ustawia ją jako pierwszą).")]
|
||||
public void W2_UtworzenieDokumentu_DefinicjaUstawionaJakoPierwszaJestPrzypisana()
|
||||
{
|
||||
// Kolejność z helpera UtworzDokument: AddRow -> Definicja (pierwsza) -> Magazyn -> Kontrahent.
|
||||
// Tu sprawdzamy sam fakt poprawnego przypisania definicji do świeżego dokumentu.
|
||||
var dok = UtworzDokument(Definicje.PrzyjecieWewnetrzne, magazyn: Magazyn(Magazyn_.Firma));
|
||||
|
||||
dok.Should().NotBeNull();
|
||||
dok.Definicja.Should().NotBeNull("definicja musi być ustawiona jako pierwsze pole dokumentu");
|
||||
dok.Definicja.Symbol.Should().Be(Definicje.PrzyjecieWewnetrzne);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Description("W2: ten sam rekord definicji jest osiągalny z dwóch dróg — bezpośrednio z tabeli " +
|
||||
"definicji (WgSymbolu) oraz przez utworzony dokument (dok.Definicja) — to jeden obiekt.")]
|
||||
public void W2_DefinicjaDokumentu_TozsamaZRekordemZTabeliDefinicji()
|
||||
{
|
||||
// Tożsamość referencyjna potwierdza, że dok.Definicja wskazuje rekord z tabeli DefDokHandlowych,
|
||||
// a nie kopię — kluczowe dla rozpoznawania rodzaju dokumentu po Definicja.Kategoria (W3).
|
||||
var defPW = Definicja(Definicje.PrzyjecieWewnetrzne);
|
||||
var dok = UtworzDokument(Definicje.PrzyjecieWewnetrzne, magazyn: Magazyn(Magazyn_.Firma));
|
||||
|
||||
dok.Definicja.Should().BeSameAs(defPW,
|
||||
"definicja dokumentu to ten sam rekord co pobrany z tabeli definicji");
|
||||
}
|
||||
|
||||
// ============================================================================================
|
||||
// W3 — Rozpoznanie rodzaju dokumentu (kategoria + flagi)
|
||||
// ============================================================================================
|
||||
|
||||
[Test]
|
||||
[Description("W3: definicja faktury sprzedaży (FV) ma kategorię w zakresie HANDLOWYM " +
|
||||
"(HandelPierwszy..HandelOstatni) — rodzaj rozpoznajemy po zakresie kategorii, nie po symbolu.")]
|
||||
public void W3_FakturaSprzedazy_KategoriaWZakresieHandlowym()
|
||||
{
|
||||
// Rozpoznanie rodzaju opieramy na Definicja.Kategoria, NIE na porównaniu Symbol == "FV"
|
||||
// (symbol jest dowolny i zależny od bazy). Markery zakresów enuma są publiczne.
|
||||
var defFV = Definicja(Definicje.FakturaSprzedazy);
|
||||
defFV.Should().NotBeNull();
|
||||
|
||||
var kat = defFV.Kategoria;
|
||||
kat.Should().BeOneOf(KategoriaHandlowa.Sprzedaż, KategoriaHandlowa.KorektaSprzedaży);
|
||||
WCzyZakresie(kat, KategoriaHandlowa.HandelPierwszy, KategoriaHandlowa.HandelOstatni)
|
||||
.Should().BeTrue("kategoria faktury mieści się w zakresie handlowym");
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Description("W3: definicje dokumentów magazynowych (PW/PZ/WZ/RW) mają kategorie w zakresie " +
|
||||
"MAGAZYNOWYM (MagazynPierwszy..MagazynOstatni) — rozpoznanie grupy zakresem markerów.")]
|
||||
public void W3_DokumentyMagazynowe_KategorieWZakresieMagazynowym()
|
||||
{
|
||||
// Klasyfikacja „grupy” dokumentu po zakresie wartości enuma — bez wyliczania wszystkich symboli.
|
||||
foreach (var symbol in new[]
|
||||
{
|
||||
Definicje.PrzyjecieWewnetrzne, Definicje.PrzyjecieZewnetrzne,
|
||||
Definicje.WydanieZewnetrzne, Definicje.RozchodWewnetrzny
|
||||
})
|
||||
{
|
||||
var def = Definicja(symbol);
|
||||
def.Should().NotBeNull($"baza Demo zawiera definicję magazynową {symbol}");
|
||||
|
||||
WCzyZakresie(def.Kategoria, KategoriaHandlowa.MagazynPierwszy, KategoriaHandlowa.MagazynOstatni)
|
||||
.Should().BeTrue($"kategoria dokumentu {symbol} ma być w zakresie magazynowym");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Description("W3: definicje zamówień (ZO/ZD) mają kategorie zamówień (ZamówienieOdbiorcy/" +
|
||||
"ZamówienieDostawcy) — leżą poza zakresami handlowym i magazynowym.")]
|
||||
public void W3_Zamowienia_RozpoznawaneJakoKategorieZamowien()
|
||||
{
|
||||
var defZO = Definicja(Definicje.ZamowienieOdbiorcy);
|
||||
var defZD = Definicja(Definicje.ZamowienieDoDostawcy);
|
||||
defZO.Should().NotBeNull();
|
||||
defZD.Should().NotBeNull();
|
||||
|
||||
// Zamówienie to ani dokument handlowy (faktura), ani magazynowy — własna grupa kategorii.
|
||||
defZO.Kategoria.Should().Be(KategoriaHandlowa.ZamówienieOdbiorcy);
|
||||
defZD.Kategoria.Should().Be(KategoriaHandlowa.ZamówienieDostawcy);
|
||||
|
||||
WCzyZakresie(defZO.Kategoria, KategoriaHandlowa.HandelPierwszy, KategoriaHandlowa.HandelOstatni)
|
||||
.Should().BeFalse("zamówienie nie należy do zakresu handlowego (faktur)");
|
||||
WCzyZakresie(defZO.Kategoria, KategoriaHandlowa.MagazynPierwszy, KategoriaHandlowa.MagazynOstatni)
|
||||
.Should().BeFalse("zamówienie nie należy do zakresu magazynowego");
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Description("W3: pełna klasyfikacja rodzaju przez funkcję rozgałęziającą po zakresie kategorii — " +
|
||||
"FV→handlowy, PW/WZ→magazynowy, ZO→zamówienie (wzorzec z dokumentacji rozdziału).")]
|
||||
public void W3_RozpoznajRodzaj_ZwracaPoprawnaGrupeDlaKazdejDefinicji()
|
||||
{
|
||||
// Wzorzec RozpoznajRodzaj klasyfikuje dokument po Definicja.Kategoria zakresami markerów.
|
||||
RozpoznajRodzaj(UtworzDokument(Definicje.FakturaSprzedazy, kontrahent: Kontrahent(Kontrahent_.Abc)))
|
||||
.Should().Be(RodzajDokumentu.Handlowy);
|
||||
|
||||
RozpoznajRodzaj(UtworzDokument(Definicje.PrzyjecieWewnetrzne, magazyn: Magazyn(Magazyn_.Firma)))
|
||||
.Should().Be(RodzajDokumentu.Magazynowy);
|
||||
|
||||
RozpoznajRodzaj(UtworzDokument(Definicje.WydanieZewnetrzne, magazyn: Magazyn(Magazyn_.Firma)))
|
||||
.Should().Be(RodzajDokumentu.Magazynowy);
|
||||
|
||||
RozpoznajRodzaj(UtworzDokument(Definicje.ZamowienieOdbiorcy, kontrahent: Kontrahent(Kontrahent_.Abc)))
|
||||
.Should().Be(RodzajDokumentu.Zamowienie);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Description("W3: świeżo utworzony zwykły dokument (nie z relacji korekty) ma flagę Korekta=false — " +
|
||||
"korektę tworzy się przez relacje dokumentów, a nie przez przestawienie flagi.")]
|
||||
public void W3_ZwyklyDokument_FlagaKorektaFalsz()
|
||||
{
|
||||
// dok.Korekta rozpoznaje korektę. Zwykły dokument utworzony „od zera” nie jest korektą.
|
||||
var dok = UtworzDokument(Definicje.PrzyjecieWewnetrzne, magazyn: Magazyn(Magazyn_.Firma));
|
||||
|
||||
dok.Korekta.Should().BeFalse("dokument utworzony od zera (nie z relacji) nie jest korektą");
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Description("W3: zwykły dokument (faktura/magazynowy/zamówienie) nie jest dokumentem zaliczkowym — " +
|
||||
"flaga rozpoznająca zaliczkę jest false dla dokumentów utworzonych bez powiązania zaliczki.")]
|
||||
public void W3_ZwyklyDokument_NieJestZaliczkowy()
|
||||
{
|
||||
// Rozpoznanie zaliczki ma pierwszeństwo przed klasyfikacją zakresową (zaliczka bywa fakturą),
|
||||
// ale zwykły dokument utworzony od zera zaliczką nie jest.
|
||||
var faktura = UtworzDokument(Definicje.FakturaSprzedazy, kontrahent: Kontrahent(Kontrahent_.Abc));
|
||||
var magazynowy = UtworzDokument(Definicje.PrzyjecieWewnetrzne, magazyn: Magazyn(Magazyn_.Firma));
|
||||
|
||||
faktura.JestZaliczkowy.Should().BeFalse("zwykła faktura sprzedaży nie jest dokumentem zaliczkowym");
|
||||
magazynowy.JestZaliczkowy.Should().BeFalse("dokument magazynowy nie jest dokumentem zaliczkowym");
|
||||
}
|
||||
|
||||
// ============================================================================================
|
||||
// Pomocnicze (wzorce klasyfikacji z dokumentacji rozdziału)
|
||||
// ============================================================================================
|
||||
|
||||
/// <summary>Grupa rodzajowa dokumentu rozpoznana po kategorii jego definicji.</summary>
|
||||
private enum RodzajDokumentu { Handlowy, Magazynowy, Zamowienie, Inny }
|
||||
|
||||
/// <summary>
|
||||
/// Klasyfikacja rodzaju dokumentu po <c>Definicja.Kategoria</c> z użyciem publicznych markerów
|
||||
/// zakresów enuma <see cref="KategoriaHandlowa"/> — odzwierciedla wzorzec ze snippetu rozdziału.
|
||||
/// </summary>
|
||||
private static RodzajDokumentu RozpoznajRodzaj(DokumentHandlowy dok)
|
||||
{
|
||||
// Definicja może być null na świeżo nieskonfigurowanym dokumencie — zabezpieczamy dostęp.
|
||||
if (dok.Definicja == null)
|
||||
return RodzajDokumentu.Inny;
|
||||
|
||||
var kat = dok.Definicja.Kategoria;
|
||||
|
||||
return kat switch
|
||||
{
|
||||
>= KategoriaHandlowa.HandelPierwszy and <= KategoriaHandlowa.HandelOstatni
|
||||
=> RodzajDokumentu.Handlowy,
|
||||
>= KategoriaHandlowa.MagazynPierwszy and <= KategoriaHandlowa.MagazynOstatni
|
||||
=> RodzajDokumentu.Magazynowy,
|
||||
KategoriaHandlowa.ZamówienieOdbiorcy
|
||||
or KategoriaHandlowa.ZamówienieDostawcy
|
||||
or KategoriaHandlowa.ZamówienieWewnętrzne
|
||||
=> RodzajDokumentu.Zamowienie,
|
||||
_ => RodzajDokumentu.Inny
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>Sprawdza, czy kategoria mieści się w zakresie [od, do] (markery zakresów enuma).</summary>
|
||||
private static bool WCzyZakresie(KategoriaHandlowa kat, KategoriaHandlowa od, KategoriaHandlowa gora)
|
||||
=> kat >= od && kat <= gora;
|
||||
}
|
||||
@@ -0,0 +1,415 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using AwesomeAssertions;
|
||||
using NUnit.Framework;
|
||||
using Soneta.Handel;
|
||||
using Soneta.Types;
|
||||
|
||||
namespace Soneta.Skills.Test.Handel.DokumentyHandlowe;
|
||||
|
||||
/// <summary>
|
||||
/// Rozdział 2 — „Wystawianie dokumentów” (wzorce W4–W11).
|
||||
/// <para>
|
||||
/// Testy pokazują tworzenie dokumentu handlowego od zera w różnych wariantach: faktura sprzedaży (FV),
|
||||
/// faktura zakupu (FZ — numer obcy i daty), dokument magazynowy (PW/PZ), zamówienie odbiorcy (ZO),
|
||||
/// dodawanie pozycji (towar/ilość/cena/rabat), dokument z usługą (MONTAZ — bez magazynu),
|
||||
/// dokument w walucie obcej (W9) oraz odbiorca inny niż kontrahent (W11).
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Reguły bazy Demo</b>, których trzymają się testy:
|
||||
/// <list type="bullet">
|
||||
/// <item>Demo blokuje stan ujemny (<c>StanUjemnyVerifier</c>): rozchód (FV/WZ) wymaga wcześniej
|
||||
/// <b>zapisanego</b> przyjęcia (PW/PZ) tego towaru. Obroty księgują się dopiero po <c>Session.Save()</c>.</item>
|
||||
/// <item>Po zapisie w środku testu sesja zamyka okno edycji — kolejna edycja rzuca wyjątek.
|
||||
/// Dlatego wzorzec to: zapis przez <c>SaveDispose()</c> → odczyt na świeżej sesji po <c>Guid</c>.</item>
|
||||
/// </list>
|
||||
/// Wszystko operuje wyłącznie na publicznym kontrakcie platformy (jak dodatek programisty zewnętrznego).
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public class Rozdzial02_WystawianieTest : DokumentHandlowyTestBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Pomocniczo: przyjmuje BIKINI na magazyn „F” dokumentem PW, <b>zatwierdza</b> je i <b>zapisuje</b>,
|
||||
/// żeby zbudować stan magazynu pod późniejszy rozchód (FV/WZ). Dopiero ZATWIERDZONE i zapisane
|
||||
/// przyjęcie księguje zasoby/obroty i odblokowuje rozchód na bazie Demo (kontrola stanu ujemnego).
|
||||
/// Korzysta z bazowego helpera <see cref="DokumentHandlowyTestBase.PrzyjmijNaStan"/>. Zwraca Guid PW.
|
||||
/// </summary>
|
||||
private Guid PrzyjmijBikiniNaStan(double ilosc = 100, double cena = 25)
|
||||
=> PrzyjmijNaStan(Towar_.Bikini, ilosc, cena);
|
||||
|
||||
// ============================== W4 — Faktura sprzedaży (FV) ==============================
|
||||
|
||||
[Test]
|
||||
[Description("W4: FV krajowa od netto z pozycją BIKINI — po zapisie powstaje tabela VAT i wartość dokumentu.")]
|
||||
public void FakturaSprzedazy_OdNetto_WyliczaSumeIVat()
|
||||
{
|
||||
// Najpierw przyjęcie na stan (zapisane) — inaczej rozchód FV zablokuje kontrola stanu ujemnego.
|
||||
PrzyjmijBikiniNaStan();
|
||||
|
||||
Guid guidFv = Guid.Empty;
|
||||
// Definicja FIRST (helper UtworzDokument), potem magazyn i kontrahent-nabywca.
|
||||
var fv = UtworzDokument(
|
||||
Definicje.FakturaSprzedazy,
|
||||
kontrahent: Kontrahent(Kontrahent_.Abc),
|
||||
magazyn: Magazyn(Magazyn_.Firma));
|
||||
// FV NIE zatwierdzamy — zatwierdzenie FV w bazie testowej Demo rzuca NRE w ewidencji VAT.
|
||||
// SumyVAT/Suma na świeżym dokumencie w pamięci bywają niprzeliczone — przeliczają się
|
||||
// po zapisie. Dlatego zapisujemy FV w BUFORZE (bez zatwierdzania) i czytamy po Guid.
|
||||
InTransaction(() =>
|
||||
{
|
||||
fv.Data = Date.Today; // data wystawienia
|
||||
fv.DataOperacji = Date.Today; // faktyczna data sprzedaży
|
||||
fv.LiczonaOd = SposobLiczeniaVAT.OdNetto; // ustaw przed pozycjami
|
||||
DodajPozycje(fv, Towar(Towar_.Bikini), 2, 50); // 2 szt po 50
|
||||
guidFv = fv.Guid;
|
||||
});
|
||||
SaveDispose();
|
||||
|
||||
var zapis = Get<DokumentHandlowy>(guidFv);
|
||||
zapis.Should().NotBeNull();
|
||||
zapis.LiczonaOd.Should().Be(SposobLiczeniaVAT.OdNetto);
|
||||
// SumyVAT i Suma są wyliczane z pozycji — wyliczone po zapisie (czytamy po Guid).
|
||||
zapis.SumyVAT.Should().NotBeEmpty();
|
||||
// Wartość netto jest dodatnia (kontrahent Abc ma rabat, więc netto może być < cena*ilość).
|
||||
((double)zapis.Suma.Netto).Should().BeGreaterThan(0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Description("W4: FV liczona od brutto — pole LiczonaOd przyjmuje wartość Brutto.")]
|
||||
public void FakturaSprzedazy_OdBrutto_UstawiaLiczonaOdBrutto()
|
||||
{
|
||||
PrzyjmijBikiniNaStan();
|
||||
|
||||
var fv = UtworzDokument(
|
||||
Definicje.FakturaSprzedazy,
|
||||
kontrahent: Kontrahent(Kontrahent_.Abc),
|
||||
magazyn: Magazyn(Magazyn_.Firma));
|
||||
// Asercja na FV w BUFORZE (nie zatwierdzamy FV — zatwierdzenie rzuca NRE w ewidencji VAT).
|
||||
InTransaction(() =>
|
||||
{
|
||||
// LiczonaOd ustawiamy PRZED pozycjami — zmiana po wprowadzeniu pozycji wymusza przeliczenie cen.
|
||||
fv.LiczonaOd = SposobLiczeniaVAT.OdBrutto;
|
||||
DodajPozycje(fv, Towar(Towar_.Bikini), 1, 50);
|
||||
});
|
||||
|
||||
fv.LiczonaOd.Should().Be(SposobLiczeniaVAT.OdBrutto);
|
||||
}
|
||||
|
||||
// ============================== W5 — Zakup od dostawcy (PZ) ==============================
|
||||
|
||||
[Test]
|
||||
[Description("W5: zakup od dostawcy (PZ) z datą operacji (zakupu) różną od daty wystawienia — przyjęcie zewnętrzne, przychód.")]
|
||||
public void FakturaZakupu_UstawiaNumerObcyIDatyZakupu()
|
||||
{
|
||||
// W bazie Demo „faktura zakupu" jako dokument handlowy nie istnieje — stronę zakupową
|
||||
// reprezentuje przyjęcie zewnętrzne „PZ" (przychód, kontrahent-dostawca). PZ NIE wywołuje
|
||||
// kontroli stanu ujemnego, więc nie potrzebuje wcześniejszego przyjęcia.
|
||||
Guid guidPz = Guid.Empty;
|
||||
var dataWystawienia = Date.Today;
|
||||
var dataZakupu = Date.Today.AddDays(-2);
|
||||
|
||||
// PZ to dokument przychodowy — kontrahent jest dostawcą.
|
||||
var pz = UtworzDokument(
|
||||
Definicje.FakturaZakupu,
|
||||
kontrahent: Kontrahent(Kontrahent_.Abc),
|
||||
magazyn: Magazyn(Magazyn_.Firma));
|
||||
InTransaction(() =>
|
||||
{
|
||||
pz.Data = dataWystawienia; // data wystawienia u nas
|
||||
pz.DataOperacji = dataZakupu; // faktyczna data zakupu (decyduje o okresie magazynowym)
|
||||
DodajPozycje(pz, Towar(Towar_.Bikini), 10, 30);
|
||||
guidPz = pz.Guid;
|
||||
});
|
||||
// Bez zatwierdzania — sprawdzamy podstawowe pola dokumentu zakupowego (PZ).
|
||||
SaveDispose();
|
||||
|
||||
var zapis = Get<DokumentHandlowy>(guidPz);
|
||||
zapis.Should().NotBeNull();
|
||||
zapis.Definicja.Symbol.Should().Be("PZ");
|
||||
zapis.Kontrahent.Kod.Should().Be(Kontrahent(Kontrahent_.Abc).Kod);
|
||||
zapis.DataOperacji.Should().Be(dataZakupu);
|
||||
zapis.Data.Should().Be(dataWystawienia);
|
||||
// Data operacji (zakupu) różna od daty wystawienia — to dwa odrębne pola.
|
||||
zapis.DataOperacji.Should().NotBe(zapis.Data);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Description("W5: zakup od dostawcy (PZ) z przyjęciem na magazyn księguje przychód — po zatwierdzeniu i Save powstają zasoby dokumentu.")]
|
||||
public void FakturaZakupu_KsiegujePrzychod_TworzyZasoby()
|
||||
{
|
||||
Guid guidPz = Guid.Empty;
|
||||
// PZ (przyjęcie zewnętrzne od dostawcy) to dokument przychodowy — kontrahent jest dostawcą.
|
||||
var pz = UtworzDokument(
|
||||
Definicje.FakturaZakupu,
|
||||
kontrahent: Kontrahent(Kontrahent_.Abc),
|
||||
magazyn: Magazyn(Magazyn_.Firma));
|
||||
InTransaction(() =>
|
||||
{
|
||||
pz.Data = Date.Today;
|
||||
pz.DataOperacji = Date.Today;
|
||||
DodajPozycje(pz, Towar(Towar_.Bikini), 5, 30);
|
||||
guidPz = pz.Guid;
|
||||
});
|
||||
// Zasoby dokumentu przychodowego księgują się DOPIERO po zatwierdzeniu + Save.
|
||||
// Zatwierdzenie PZ (jak PW) jest bezpieczne — nie rzuca NRE (rzuca tylko zatwierdzenie FV).
|
||||
InTransaction(() => pz.Stan = StanDokumentuHandlowego.Zatwierdzony);
|
||||
SaveDispose();
|
||||
|
||||
var zapis = Get<DokumentHandlowy>(guidPz);
|
||||
// PZ (przyjęcie od dostawcy) jest dokumentem przychodowym → powstają zasoby magazynowe.
|
||||
zapis.Zasoby.Cast<object>().Should().NotBeEmpty();
|
||||
}
|
||||
|
||||
// ============================== W6 — Dokument magazynowy (PW/PZ) ==============================
|
||||
|
||||
[Test]
|
||||
[Description("W6: PW (przyjęcie wewnętrzne) buduje stan magazynu — po Save powstają zasoby.")]
|
||||
public void PrzyjecieWewnetrzne_PW_TworzyZasoby()
|
||||
{
|
||||
// PW jest dokumentem wewnętrznym (przychód) — bez kontrahenta, magazyn wymagany.
|
||||
var guidPw = PrzyjmijBikiniNaStan(50, 25);
|
||||
|
||||
var zapis = Get<DokumentHandlowy>(guidPw);
|
||||
zapis.Should().NotBeNull();
|
||||
// Kierunek magazynu wynika z definicji (readonly="set"), nie ustawiamy go ręcznie.
|
||||
zapis.Zasoby.Cast<object>().Should().NotBeEmpty();
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Description("W6: dokument magazynowy bez magazynu — Save rzuca wyjątek (Magazyn jest wymagany).")]
|
||||
public void DokumentMagazynowy_BezMagazynu_RzucaPrzyZapisie()
|
||||
{
|
||||
// Brak wymaganego magazynu → operacja musi się nie powieść. Wyjątek może paść już
|
||||
// przy dodaniu pozycji/edycji albo dopiero przy Save — łapiemy całą sekwencję, żeby
|
||||
// asercja była odporna na moment zgłoszenia (RequiredException / walidacja magazynu).
|
||||
Action buildIZapisz = () =>
|
||||
{
|
||||
var pw = UtworzDokument(Definicje.PrzyjecieWewnetrzne);
|
||||
InTransaction(() => DodajPozycje(pw, Towar(Towar_.Bikini), 1, 10));
|
||||
SaveDispose();
|
||||
};
|
||||
buildIZapisz.Should().Throw<Exception>();
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Description("W6: PZ (przyjęcie zewnętrzne od dostawcy) — przychód z kontrahentem-dostawcą.")]
|
||||
public void PrzyjecieZewnetrzne_PZ_TworzyZasoby()
|
||||
{
|
||||
Guid guidPz = Guid.Empty;
|
||||
// PZ to przyjęcie zewnętrzne — przychód z kontrahentem (dostawcą).
|
||||
var pz = UtworzDokument(
|
||||
Definicje.PrzyjecieZewnetrzne,
|
||||
kontrahent: Kontrahent(Kontrahent_.Zefir),
|
||||
magazyn: Magazyn(Magazyn_.Firma));
|
||||
InTransaction(() =>
|
||||
{
|
||||
DodajPozycje(pz, Towar(Towar_.Bikini), 20, 25);
|
||||
guidPz = pz.Guid;
|
||||
});
|
||||
// Przychód księguje zasoby/obroty DOPIERO po zatwierdzeniu + Save.
|
||||
InTransaction(() => pz.Stan = StanDokumentuHandlowego.Zatwierdzony);
|
||||
SaveDispose();
|
||||
|
||||
var zapis = Get<DokumentHandlowy>(guidPz);
|
||||
zapis.Zasoby.Cast<object>().Should().NotBeEmpty();
|
||||
}
|
||||
|
||||
// ============================== W7 — Zamówienie (ZO) ==============================
|
||||
|
||||
[Test]
|
||||
[Description("W7: ZO (zamówienie odbiorcy) z terminem dostawy — nie buduje stanu magazynu.")]
|
||||
public void ZamowienieOdbiorcy_ZO_UstawiaTerminDostawy_BezObrotow()
|
||||
{
|
||||
Guid guidZo = Guid.Empty;
|
||||
var termin = Date.Today.AddDays(7);
|
||||
|
||||
var zo = UtworzDokument(
|
||||
Definicje.ZamowienieOdbiorcy,
|
||||
kontrahent: Kontrahent(Kontrahent_.Abc),
|
||||
magazyn: Magazyn(Magazyn_.Firma));
|
||||
InTransaction(() =>
|
||||
{
|
||||
zo.Data = Date.Today;
|
||||
zo.DataOperacji = Date.Today;
|
||||
// Dostawa to subrow — ustawiamy jego pola, nie przypisujemy całego obiektu.
|
||||
zo.Dostawa.Termin = termin; // oczekiwany termin dostawy
|
||||
DodajPozycje(zo, Towar(Towar_.Bikini), 5, 50);
|
||||
guidZo = zo.Guid;
|
||||
});
|
||||
// Zamówienie nie buduje stanu magazynu — nie musimy wcześniej przyjmować towaru.
|
||||
SaveDispose();
|
||||
|
||||
var zapis = Get<DokumentHandlowy>(guidZo);
|
||||
zapis.Should().NotBeNull();
|
||||
zapis.Dostawa.Termin.Should().Be(termin);
|
||||
// Zamówienie to dokument planistyczny — nie tworzy obrotów/zasobów magazynowych.
|
||||
zapis.Zasoby.Cast<object>().Should().BeEmpty();
|
||||
}
|
||||
|
||||
// ============================== W8 — Dodawanie pozycji ==============================
|
||||
|
||||
[Test]
|
||||
[Description("W8: pozycja z automatyczną ceną (tylko Towar + Ilosc) — cena pobrana z cennika jest dodatnia.")]
|
||||
public void DodaniePozycji_AutomatycznaCena_PobieraZCennika()
|
||||
{
|
||||
PrzyjmijBikiniNaStan();
|
||||
|
||||
var fv = UtworzDokument(
|
||||
Definicje.FakturaSprzedazy,
|
||||
kontrahent: Kontrahent(Kontrahent_.Abc),
|
||||
magazyn: Magazyn(Magazyn_.Firma));
|
||||
PozycjaDokHandlowego poz = null;
|
||||
InTransaction(() =>
|
||||
{
|
||||
// Bez podania ceny (cena = null) — towar inicjuje cenę z cennika/karty.
|
||||
poz = DodajPozycje(fv, Towar(Towar_.Bikini), 3);
|
||||
});
|
||||
|
||||
// Asercja na FV w BUFORZE (FV nie zatwierdzamy — zatwierdzenie rzuca NRE w ewidencji VAT).
|
||||
// Cena zaproponowana przez cennik — oczekujemy wartości dodatniej (nie ustawialiśmy jej ręcznie).
|
||||
((double)poz.Cena.Value).Should().BeGreaterThan(0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Description("W8: ręczne nadpisanie ceny i rabatu — Cena/Rabat przyjmują podane wartości, zapalają korekty.")]
|
||||
public void DodaniePozycji_RecznaCenaIRabat_NadpisujeWartosci()
|
||||
{
|
||||
PrzyjmijBikiniNaStan();
|
||||
|
||||
var fv = UtworzDokument(
|
||||
Definicje.FakturaSprzedazy,
|
||||
kontrahent: Kontrahent(Kontrahent_.Abc),
|
||||
magazyn: Magazyn(Magazyn_.Firma));
|
||||
PozycjaDokHandlowego poz = null;
|
||||
InTransaction(() =>
|
||||
{
|
||||
// Ręczna cena nadpisuje cennik (zapala KorektaCeny); rabat zapala KorektaRabatu.
|
||||
poz = DodajPozycje(fv, Towar(Towar_.Bikini), 10, 48);
|
||||
poz.Rabat = new Percent(0.1m); // 10%
|
||||
});
|
||||
|
||||
// Asercja na FV w BUFORZE (FV nie zatwierdzamy — zatwierdzenie rzuca NRE w ewidencji VAT).
|
||||
((double)poz.Cena.Value).Should().Be(48);
|
||||
// Rabat 10% został zapamiętany na pozycji.
|
||||
((double)poz.Rabat).Should().BeApproximately(0.1, 1e-9);
|
||||
}
|
||||
|
||||
// ============================== W10 — Dokument z usługą (MONTAZ) ==============================
|
||||
|
||||
[Test]
|
||||
[Description("W10: FV tylko z usługą (MONTAZ) — liczy VAT/wartość, ale nie tworzy obrotów magazynowych.")]
|
||||
public void FakturaZUsluga_Montaz_BezObrotowMagazynowych()
|
||||
{
|
||||
// Usługa nie pobiera ze stanu — NIE potrzeba wcześniejszego przyjęcia (StanUjemnyVerifier nie blokuje).
|
||||
Guid guidFv = Guid.Empty;
|
||||
var fv = UtworzDokument(
|
||||
Definicje.FakturaSprzedazy,
|
||||
kontrahent: Kontrahent(Kontrahent_.Abc),
|
||||
magazyn: Magazyn(Magazyn_.Firma));
|
||||
InTransaction(() =>
|
||||
{
|
||||
fv.Data = Date.Today;
|
||||
fv.DataOperacji = Date.Today;
|
||||
// MONTAZ jest towarem typu usługa — bez wpływu na magazyn.
|
||||
DodajPozycje(fv, Towar(Towar_.Montaz), 1, 200);
|
||||
guidFv = fv.Guid;
|
||||
});
|
||||
SaveDispose();
|
||||
|
||||
var zapis = Get<DokumentHandlowy>(guidFv);
|
||||
zapis.Should().NotBeNull();
|
||||
// Usługa nie tworzy zasobów magazynowych, ale uczestniczy w tabeli VAT.
|
||||
zapis.Zasoby.Cast<object>().Should().BeEmpty();
|
||||
zapis.SumyVAT.Should().NotBeEmpty();
|
||||
((double)zapis.Suma.Netto).Should().BeGreaterThan(0);
|
||||
}
|
||||
|
||||
// ============================== W11 — Odbiorca inny niż kontrahent ==============================
|
||||
|
||||
[Test]
|
||||
[Description("W11: nabywca (Kontrahent) różny od odbiorcy towaru (Odbiorca) — dwa różne pola typu Kontrahent.")]
|
||||
public void OdbiorcaInnyNizKontrahent_UstawiaOdbiorce()
|
||||
{
|
||||
PrzyjmijBikiniNaStan();
|
||||
|
||||
var fv = UtworzDokument(
|
||||
Definicje.FakturaSprzedazy,
|
||||
kontrahent: Kontrahent(Kontrahent_.Abc), // nabywca / strona VAT
|
||||
magazyn: Magazyn(Magazyn_.Firma));
|
||||
InTransaction(() =>
|
||||
{
|
||||
// Odbiorca towaru to inny podmiot niż nabywca — faktura na Kontrahent, dostawa do Odbiorca.
|
||||
fv.Odbiorca = Kontrahent(Kontrahent_.Zefir);
|
||||
fv.Osoba = "Jan Kowalski"; // osoba podpisująca po stronie kontrahenta
|
||||
fv.Dostawa.Termin = Date.Today.AddDays(3);
|
||||
fv.Dostawa.Sposob = "Kurier";
|
||||
DodajPozycje(fv, Towar(Towar_.Bikini), 1, 50);
|
||||
});
|
||||
|
||||
// Asercja na FV w BUFORZE (FV nie zatwierdzamy — zatwierdzenie rzuca NRE w ewidencji VAT).
|
||||
fv.Kontrahent.Kod.Should().Be(Kontrahent(Kontrahent_.Abc).Kod);
|
||||
fv.Odbiorca.Should().NotBeNull();
|
||||
fv.Odbiorca.Kod.Should().Be(Kontrahent(Kontrahent_.Zefir).Kod);
|
||||
// Nabywca i odbiorca to dwa różne podmioty.
|
||||
fv.Odbiorca.Kod.Should().NotBe(fv.Kontrahent.Kod);
|
||||
fv.Osoba.Should().Be("Jan Kowalski");
|
||||
}
|
||||
|
||||
// ============================== W9 — Dokument w walucie obcej (bezpiecznie, bez sieci) ==============================
|
||||
|
||||
[Test]
|
||||
[Description("W9: dokument walutowy wymaga kursu — bez kursu EUR na datę operacja zgłasza błąd; test bezpieczny (bez sieci).")]
|
||||
public void DokumentWalutowy_BezKursuEur_RzucaLubPomijane()
|
||||
{
|
||||
// UWAGA: NIE pobieramy kursu z sieci. Baza Demo zwykle nie ma kursu EUR „na dziś”,
|
||||
// więc próba ustawienia waluty/tabeli kursowej bez dostępnego kursu powinna zgłosić wyjątek
|
||||
// (np. KursWalutyNotFoundException). Test jedynie potwierdza, że ustawienie dokumentu
|
||||
// walutowego WYMAGA kursu — nie wymaga połączenia z internetem.
|
||||
var wm = Soneta.Waluty.WalutyModule.GetInstance(Session); // session.GetWaluty() jest internal
|
||||
var eur = wm.Waluty.WgSymbolu["EUR"];
|
||||
|
||||
if (eur == null)
|
||||
{
|
||||
// Demo bez waluty EUR — pomijamy z czytelnym komentarzem (nie wymuszamy sieci/danych).
|
||||
Assert.Ignore("Baza Demo nie ma waluty EUR — test walutowy pominięty (brak danych, bez sieci).");
|
||||
return;
|
||||
}
|
||||
|
||||
// Szukamy tabeli kursowej z kursem EUR na dziś — bez sieci.
|
||||
var tabela = wm.TabeleKursowe.Cast<object>().FirstOrDefault();
|
||||
if (tabela == null)
|
||||
{
|
||||
Assert.Ignore("Baza Demo nie ma tabeli kursowej — test walutowy pominięty (brak danych, bez sieci).");
|
||||
return;
|
||||
}
|
||||
|
||||
// Próba zbudowania dokumentu walutowego bez gwarancji kursu na datę:
|
||||
// albo uda się (kurs jest w bazie), albo zgłosi błąd braku kursu — oba przypadki są poprawne.
|
||||
var fv = UtworzDokument(
|
||||
Definicje.FakturaSprzedazy,
|
||||
kontrahent: Kontrahent(Kontrahent_.Abc),
|
||||
magazyn: Magazyn(Magazyn_.Firma));
|
||||
|
||||
Action ustawWalute = () => InTransaction(() =>
|
||||
{
|
||||
// TabelaKursowa jest wymagana dla dokumentu walutowego; DataKursu wyznacza, którego kursu szukać.
|
||||
fv.TabelaKursowa = (Soneta.Waluty.TabelaKursowa)tabela;
|
||||
fv.DataKursu = Date.Today;
|
||||
});
|
||||
|
||||
// Bezpiecznie: dopuszczamy zarówno sukces (kurs istnieje), jak i wyjątek braku kursu.
|
||||
// Nie wymuszamy konkretnego typu wyjątku, bo zależy od danych Demo, a sieci nie używamy.
|
||||
try
|
||||
{
|
||||
ustawWalute();
|
||||
// Jeśli się powiodło, tabela kursowa została przypisana — to też poprawny wynik.
|
||||
fv.TabelaKursowa.Should().NotBeNull();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Brak kursu na datę → oczekiwany błąd (np. KursWalutyNotFoundException). To poprawny scenariusz.
|
||||
ex.Should().NotBeNull("brak kursu EUR na datę powinien zgłosić wyjątek, a nie cichą awarię");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
using AwesomeAssertions;
|
||||
using NUnit.Framework;
|
||||
using Soneta.Handel;
|
||||
using Soneta.Magazyny;
|
||||
|
||||
namespace Soneta.Skills.Test.Handel.DokumentyHandlowe;
|
||||
|
||||
/// <summary>
|
||||
/// Rozdział 3 — Stany dokumentu i cykl życia (W12–W16).
|
||||
/// <para>
|
||||
/// Stanem dokumentu steruje jedno zapisywalne pole <c>dok.Stan</c>
|
||||
/// (<see cref="StanDokumentuHandlowego"/>: <c>Bufor=0, Zatwierdzony=1, Zablokowany=2, Anulowany=3</c>).
|
||||
/// Do asercji używamy skrótów kalkulowanych <c>dok.Bufor</c>/<c>dok.Zatwierdzony</c>/<c>dok.Anulowany</c>,
|
||||
/// a nie porównywania enuma.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// W bazie Demo działa <c>StanUjemnyVerifier</c> (blokada stanu ujemnego): zatwierdzenie rozchodu
|
||||
/// wymaga wcześniej zapisanego przyjęcia tego towaru. Dlatego do prostych testów cyklu życia
|
||||
/// używamy przychodu (PW), który niczego nie blokuje. Magazyn księguje się dopiero po
|
||||
/// <c>Session.Save()</c>, nie po samym <c>Commit()</c>.
|
||||
/// </para>
|
||||
/// Cała klasa operuje wyłącznie na publicznym kontrakcie platformy (tak jak dodatek zewnętrzny).
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public class Rozdzial03_CyklZyciaTest : DokumentHandlowyTestBase
|
||||
{
|
||||
// === Pomocnik lokalny: zatwierdzony przychód (PW) z jedną pozycją, zapisany trwale ===
|
||||
|
||||
/// <summary>
|
||||
/// Tworzy przyjęcie wewnętrzne (PW) z pozycją towaru BIKINI, zatwierdza je i zapisuje.
|
||||
/// PW to przychód — nie podlega blokadzie stanu ujemnego, więc nadaje się do testów cyklu życia.
|
||||
/// Zwraca Guid zapisanego dokumentu (sesja zostaje zamknięta przez <see cref="SaveDispose"/>).
|
||||
/// </summary>
|
||||
private System.Guid UtworzZatwierdzonyPwIZapisz(double ilosc = 10, double cena = 5)
|
||||
{
|
||||
// 1. Dokument przychodowy + pozycja w jednej transakcji.
|
||||
var dok = UtworzDokument(Definicje.PrzyjecieWewnetrzne, magazyn: Magazyn(Magazyn_.Firma));
|
||||
InTransaction(() => DodajPozycje(dok, Towar(Towar_.Bikini), ilosc, cena));
|
||||
|
||||
// 2. Zatwierdzenie: Bufor -> Zatwierdzony (osobna transakcja).
|
||||
InTransaction(() => dok.Stan = StanDokumentuHandlowego.Zatwierdzony);
|
||||
|
||||
var guid = dok.Guid;
|
||||
// 3. Dopiero Save() księguje obroty/zasoby. SaveDispose zamyka okno edycji sesji.
|
||||
SaveDispose();
|
||||
return guid;
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Description("W12: zatwierdzenie przychodu (PW) zmienia stan na Zatwierdzony i tworzy zasoby po Save.")]
|
||||
public void W12_ZatwierdzeniePrzychodu_UstawiaStanIKsięgujeZasoby()
|
||||
{
|
||||
// Tworzymy PW z pozycją (przychód — bez ryzyka stanu ujemnego).
|
||||
var dok = UtworzDokument(Definicje.PrzyjecieWewnetrzne, magazyn: Magazyn(Magazyn_.Firma));
|
||||
InTransaction(() => DodajPozycje(dok, Towar(Towar_.Bikini), ilosc: 10, cena: 5));
|
||||
|
||||
// Przed zatwierdzeniem dokument jest w buforze.
|
||||
dok.Bufor.Should().BeTrue();
|
||||
dok.Zatwierdzony.Should().BeFalse();
|
||||
|
||||
// Zatwierdzenie: bufor -> zatwierdzony (czytamy pole kalkulowane, nie enum).
|
||||
InTransaction(() => dok.Stan = StanDokumentuHandlowego.Zatwierdzony);
|
||||
var guid = dok.Guid;
|
||||
// Dopiero Save() księguje obroty/zasoby/płatności.
|
||||
SaveDispose();
|
||||
|
||||
// Odczyt na świeżej sesji po Guid (wzorzec zapis -> odczyt).
|
||||
var zapisany = Get<DokumentHandlowy>(guid);
|
||||
zapisany.Zatwierdzony.Should().BeTrue();
|
||||
zapisany.Bufor.Should().BeFalse();
|
||||
// Przychód utworzył zasoby magazynowe (widoczne po Save).
|
||||
zapisany.Zasoby.Count.Should().BeGreaterThan(0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Description("W13: cofnięcie zatwierdzonego dokumentu bez zależności z powrotem do bufora.")]
|
||||
public void W13_CofniecieDoBufora_PrzywracaStanBufor()
|
||||
{
|
||||
// Zatwierdzony PW bez dokumentów podrzędnych.
|
||||
var guid = UtworzZatwierdzonyPwIZapisz();
|
||||
|
||||
// Re-get na świeżej sesji (po SaveDispose nie wolno edytować obiektu z poprzedniej sesji — §8).
|
||||
var dok = Get<DokumentHandlowy>(guid);
|
||||
dok.Zatwierdzony.Should().BeTrue();
|
||||
|
||||
// Cofnięcie: zatwierdzony -> bufor (odksięgowanie przy Save).
|
||||
InTransaction(() => dok.Stan = StanDokumentuHandlowego.Bufor);
|
||||
SaveDispose();
|
||||
|
||||
var poCofnieciu = Get<DokumentHandlowy>(guid);
|
||||
poCofnieciu.Bufor.Should().BeTrue();
|
||||
poCofnieciu.Zatwierdzony.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Description("W14: anulowanie dokumentu w buforze ustawia stan Anulowany, rekord pozostaje w bazie.")]
|
||||
public void W14_AnulowanieZBufora_UstawiaStanAnulowany()
|
||||
{
|
||||
// PW w buforze (anulowanie z bufora nie wymaga odksięgowania).
|
||||
var dok = UtworzDokument(Definicje.PrzyjecieWewnetrzne, magazyn: Magazyn(Magazyn_.Firma));
|
||||
InTransaction(() => DodajPozycje(dok, Towar(Towar_.Bikini), ilosc: 10, cena: 5));
|
||||
dok.Bufor.Should().BeTrue();
|
||||
|
||||
// Anulowanie: bufor -> anulowany.
|
||||
InTransaction(() => dok.Stan = StanDokumentuHandlowego.Anulowany);
|
||||
var guid = dok.Guid;
|
||||
SaveDispose();
|
||||
|
||||
// Po anulowaniu rekord nadal istnieje (w przeciwieństwie do Delete) i jest oznaczony jako anulowany.
|
||||
var zapisany = Get<DokumentHandlowy>(guid);
|
||||
zapisany.Should().NotBeNull();
|
||||
zapisany.Anulowany.Should().BeTrue();
|
||||
zapisany.Bufor.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Description("W14: anulowanie zatwierdzonego przychodu odksięgowuje zasoby, rekord zostaje.")]
|
||||
public void W14_AnulowanieZatwierdzonego_OdksięgowujeIRekordZostaje()
|
||||
{
|
||||
// Zatwierdzony PW (utworzył zasoby).
|
||||
var guid = UtworzZatwierdzonyPwIZapisz();
|
||||
|
||||
var dok = Get<DokumentHandlowy>(guid);
|
||||
dok.Zatwierdzony.Should().BeTrue();
|
||||
|
||||
// Anulowanie zatwierdzonego: odksięgowanie skutków magazynowych przy Save.
|
||||
InTransaction(() => dok.Stan = StanDokumentuHandlowego.Anulowany);
|
||||
SaveDispose();
|
||||
|
||||
var zapisany = Get<DokumentHandlowy>(guid);
|
||||
// Rekord zachowany (numeracja/audyt), oznaczony jako anulowany.
|
||||
zapisany.Should().NotBeNull();
|
||||
zapisany.Anulowany.Should().BeTrue();
|
||||
// Anulowanie odksięgowało zasoby utworzone przez przychód.
|
||||
zapisany.Zasoby.Count.Should().Be(0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Description("W16: usunięcie dokumentu w buforze bez zależności (Delete) trwale kasuje rekord.")]
|
||||
public void W16_UsuniecieZBufora_KasujeRekord()
|
||||
{
|
||||
// Dokument w buforze, bez powiązań i rezerwacji — usunięcie dozwolone.
|
||||
var dok = UtworzDokument(Definicje.PrzyjecieWewnetrzne, magazyn: Magazyn(Magazyn_.Firma));
|
||||
InTransaction(() => DodajPozycje(dok, Towar(Towar_.Bikini), ilosc: 10, cena: 5));
|
||||
var guid = dok.Guid;
|
||||
|
||||
// Warunki bezpiecznego usunięcia: bufor.
|
||||
dok.Bufor.Should().BeTrue();
|
||||
|
||||
// Twarde usunięcie (kasuje też pozycje) w tej samej sesji edycyjnej, bez wcześniejszego
|
||||
// SaveDispose — Delete musi nastąpić na żywym obiekcie, przed zapisem.
|
||||
InTransaction(() => dok.Delete());
|
||||
SaveDispose();
|
||||
|
||||
// Po usunięciu indeksator po Guid rzuca RowNotFoundException dla nieistniejącego GUID (§5).
|
||||
Assert.Throws<Soneta.Business.RowNotFoundException>(() =>
|
||||
{
|
||||
var _ = Get<DokumentHandlowy>(guid);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Description("W16: anulowanie jako alternatywa dla usunięcia zatwierdzonego — rekord pozostaje.")]
|
||||
public void W16_ZatwierdzonyAnulowanyZamiastUsuniety_RekordZostaje()
|
||||
{
|
||||
// Zatwierdzonego dokumentu nie można usuwać przez Delete (tylko bufor) —
|
||||
// zalecaną ścieżką dla nieodwracalnego wycofania jest anulowanie (zachowuje numer i audyt).
|
||||
var guid = UtworzZatwierdzonyPwIZapisz();
|
||||
|
||||
var dok = Get<DokumentHandlowy>(guid);
|
||||
// Poza buforem — Delete jest zabronione, więc anulujemy.
|
||||
dok.Bufor.Should().BeFalse();
|
||||
|
||||
InTransaction(() => dok.Stan = StanDokumentuHandlowego.Anulowany);
|
||||
SaveDispose();
|
||||
|
||||
// Rekord nadal w bazie, oznaczony jako anulowany.
|
||||
var zapisany = Get<DokumentHandlowy>(guid);
|
||||
zapisany.Should().NotBeNull();
|
||||
zapisany.Anulowany.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Description("W15: PoprawaStanuDokumentuWorker na poprawnym dokumencie nie zmienia jego stanu.")]
|
||||
public void W15_NaprawaStanu_NaPoprawnymDokumencie_ZachowujeStan()
|
||||
{
|
||||
// Zatwierdzony, spójny dokument — naprawa stanu nie powinna nic zepsuć.
|
||||
var guid = UtworzZatwierdzonyPwIZapisz();
|
||||
|
||||
var dok = Get<DokumentHandlowy>(guid);
|
||||
dok.Zatwierdzony.Should().BeTrue();
|
||||
|
||||
// Worker sam zarządza transakcją wewnątrz NaprawStan() — ustawiamy tylko kontekst.
|
||||
var naprawa = new PoprawaStanuDokumentuWorker { Dokument = dok };
|
||||
naprawa.NaprawStan();
|
||||
// Wystarczy Save() po akcji, by utrwalić ewentualne zmiany workera.
|
||||
SaveDispose();
|
||||
|
||||
// Dokument poprawny — stan po naprawie pozostaje zatwierdzony.
|
||||
var poNaprawie = Get<DokumentHandlowy>(guid);
|
||||
poNaprawie.Zatwierdzony.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Description("W15: PrzeliczenieStanuWorker w trybie SprawdzićPoprawność (diagnostyka) nie zmienia danych.")]
|
||||
public void W15_SprawdzeniePoprawnosciObrotow_NieZmieniaStanu()
|
||||
{
|
||||
// Zatwierdzony przychód z poprawnymi obrotami.
|
||||
var guid = UtworzZatwierdzonyPwIZapisz();
|
||||
|
||||
var dok = Get<DokumentHandlowy>(guid);
|
||||
var zasobyPrzed = dok.Zasoby.Count;
|
||||
|
||||
// Tryb SprawdzićPoprawność tylko raportuje (Trace) — nie commituje zmian.
|
||||
// Worker sam otwiera transakcje wewnątrz PrzeliczStan(); nie owijamy go własnym Logout.
|
||||
var sprawdz = new PrzeliczenieStanuWorker(
|
||||
PrzeliczenieStanuWorker.Opcje.SprawdzićPoprawność,
|
||||
wszystkieMagazyny: false, rozchód0: false, przywracajWartość: true) { Dokument = dok };
|
||||
sprawdz.PrzeliczStan();
|
||||
|
||||
// Tryb diagnostyczny nie modyfikuje danych — stan i zasoby bez zmian.
|
||||
dok.Zatwierdzony.Should().BeTrue();
|
||||
dok.Zasoby.Count.Should().Be(zasobyPrzed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
using System.Linq;
|
||||
using AwesomeAssertions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using NUnit.Framework;
|
||||
using Soneta.Handel;
|
||||
using Soneta.Handel.RelacjeDokumentow.Api;
|
||||
|
||||
namespace Soneta.Skills.Test.Handel.DokumentyHandlowe;
|
||||
|
||||
/// <summary>
|
||||
/// Rozdział 4 — Relacje i generowanie dokumentów (W17–W24).
|
||||
/// Cały rozdział korzysta wyłącznie z publicznego toru przekształceń:
|
||||
/// serwisu <see cref="IRelacjeService"/> (scope: Session) oraz pól kalkulowanych
|
||||
/// <c>DokumentyMagazynowe</c> / <c>DokumentyHandlowe</c>.
|
||||
/// <para>
|
||||
/// Reguły wspólne (zob. dokumentacja, rozdz. 4):
|
||||
/// <list type="bullet">
|
||||
/// <item>dokumenty nadrzędne muszą być <b>zatwierdzone</b> — z bufora relacja nie powstanie,</item>
|
||||
/// <item>wywołanie metody serwisu jest operacją modyfikującą — działa w transakcji edycyjnej
|
||||
/// (<c>Session.Logout(editMode: true)</c>), po niej <c>Session.Save()</c>,</item>
|
||||
/// <item>rozchód (FV/WZ) wymaga wcześniejszego <b>zapisanego</b> przyjęcia (PW) towaru —
|
||||
/// Demo blokuje stan ujemny (<c>StanUjemnyVerifier</c>).</item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// Testy są napisane z perspektywy programisty zewnętrznego (tylko publiczny kontrakt).
|
||||
/// Tam, gdzie definicja relacji w bazie Demo wymaga rozstrzygnięcia, którego nie da się dostarczyć
|
||||
/// czystym publicznym API (callback wybierający dostawy/magazyn), test rozpoznaje
|
||||
/// <see cref="NotImplementedException"/> i jest pomijany (<c>Assert.Ignore</c>) z czytelnym powodem —
|
||||
/// nie jest to błąd kodu testu, lecz ograniczenie konfiguracji/kontraktu.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public class Rozdzial04_RelacjeTest : DokumentHandlowyTestBase
|
||||
{
|
||||
// === Pomocnicze ===
|
||||
|
||||
/// <summary>Serwis relacji bieżącej sesji (rzuca, gdy serwisu brak).</summary>
|
||||
private IRelacjeService Relacje => Session.GetRequiredService<IRelacjeService>();
|
||||
|
||||
/// <summary>
|
||||
/// Zmienia stan dokumentu na zatwierdzony (w transakcji edycyjnej).
|
||||
/// Nadrzędne muszą być zatwierdzone, aby relacja podrzędna mogła powstać.
|
||||
/// </summary>
|
||||
private void Zatwierdz(DokumentHandlowy dok)
|
||||
{
|
||||
InTransaction(() => dok.Stan = StanDokumentuHandlowego.Zatwierdzony);
|
||||
}
|
||||
|
||||
// === W17 — ZO → FV (NowyPodrzednyIndywidualny) ===
|
||||
|
||||
[Test]
|
||||
[Description("W17: z zatwierdzonego zamówienia odbiorcy (ZO) generuje pojedynczą fakturę (FV) " +
|
||||
"przez IRelacjeService.NowyPodrzednyIndywidualny; sprawdza, że powstał dokument z pozycjami.")]
|
||||
public void NowyPodrzednyIndywidualny_ZoNaFv_TworzyFaktureZPozycjami()
|
||||
{
|
||||
// Zamówienie odbiorcy nie rozchoduje magazynu w buforze, ale dla bezpieczeństwa
|
||||
// wprowadzamy towar na stan — faktura generowana z ZO może już dotykać magazynu.
|
||||
PrzyjmijNaStan(Towar_.Bikini, 100);
|
||||
|
||||
// 1) Utwórz zamówienie odbiorcy z jedną pozycją, zatwierdź je i ZAPISZ trwale.
|
||||
// Nadrzędny musi być zatwierdzony; relację wołamy na świeżej sesji (re-get po Guid).
|
||||
var zo = UtworzDokument(Definicje.ZamowienieOdbiorcy,
|
||||
kontrahent: Kontrahent(Kontrahent_.Abc),
|
||||
magazyn: Magazyn(Magazyn_.Firma));
|
||||
InTransaction(() => DodajPozycje(zo, Towar(Towar_.Bikini), 5, cena: 20));
|
||||
Zatwierdz(zo);
|
||||
var zoGuid = zo.Guid;
|
||||
SaveDispose();
|
||||
|
||||
// 2) Re-get zamówienia na świeżej sesji i wygeneruj fakturę — operacja w transakcji edycyjnej.
|
||||
var zoZap = Get<DokumentHandlowy>(zoGuid);
|
||||
DokumentHandlowy[] faktury = null;
|
||||
InTransaction(() =>
|
||||
faktury = Relacje.NowyPodrzednyIndywidualny(new[] { zoZap }, Definicje.FakturaSprzedazy));
|
||||
var fvGuid = faktury[0].Guid;
|
||||
SaveDispose();
|
||||
|
||||
// 3) Asercje: jeden nadrzędny → jeden podrzędny, faktura istnieje i ma pozycje.
|
||||
// Powiązania/pozycje czytamy po SaveDispose przez re-get po Guid.
|
||||
faktury.Should().NotBeNull();
|
||||
faktury.Should().HaveCount(1); // Length == nadrzedne.Length (relacja indywidualna)
|
||||
|
||||
var faktura = Get<DokumentHandlowy>(fvGuid);
|
||||
faktura.Should().NotBeNull();
|
||||
faktura.Definicja.Symbol.Should().Be(Definicje.FakturaSprzedazy);
|
||||
faktura.Pozycje.Count.Should().BeGreaterThan(0); // pozycje przepisane z zamówienia
|
||||
}
|
||||
|
||||
// === W21 — FV → WZ pojedynczo (NowyPodrzednyIndywidualny) ===
|
||||
|
||||
[Test]
|
||||
[Description("W21: do zatwierdzonej faktury sprzedaży (FV) generuje pojedynczy dokument magazynowy (WZ) " +
|
||||
"przez NowyPodrzednyIndywidualny; sprawdza powstanie dokumentu magazynowego.")]
|
||||
public void NowyPodrzednyIndywidualny_FvNaWz_TworzyWydanieMagazynowe()
|
||||
{
|
||||
// Relacja FV→WZ wymaga ZATWIERDZONEJ faktury sprzedaży jako nadrzędnej.
|
||||
// W testowej bazie Demo zatwierdzenie FV rzuca NullReferenceException w ewidencji VAT (facts §3),
|
||||
// więc nie da się dostarczyć poprawnego dokumentu nadrzędnego dla tej relacji.
|
||||
Assert.Ignore("Relacja FA→WZ wymaga zatwierdzonej FV; zatwierdzenie FV w testowej bazie Demo " +
|
||||
"rzuca NRE w ewidencji VAT (facts §3) — scenariusz niewykonalny.");
|
||||
}
|
||||
|
||||
// === W18 — wiele FV → 1 WZ zbiorcze (NowyPodrzednyZbiorczy) ===
|
||||
|
||||
[Test]
|
||||
[Description("W18: z dwóch zatwierdzonych faktur (tego samego kontrahenta) tworzy JEDEN zbiorczy " +
|
||||
"dokument magazynowy (WZ) przez NowyPodrzednyZbiorczy; wynik to agregat (zwykle 1 dokument).")]
|
||||
public void NowyPodrzednyZbiorczy_WieleFvNaJednoWz_TworzyDokumentZbiorczy()
|
||||
{
|
||||
// Relacja zbiorcza FV→WZ wymaga dwóch ZATWIERDZONYCH faktur sprzedaży jako nadrzędnych.
|
||||
// W testowej bazie Demo zatwierdzenie FV rzuca NullReferenceException w ewidencji VAT (facts §3),
|
||||
// więc nie da się dostarczyć poprawnych dokumentów nadrzędnych dla tej relacji.
|
||||
Assert.Ignore("Relacja zbiorcza FA→WZ wymaga zatwierdzonych FV; zatwierdzenie FV w testowej " +
|
||||
"bazie Demo rzuca NRE w ewidencji VAT (facts §3) — scenariusz niewykonalny.");
|
||||
}
|
||||
|
||||
// === W20 — odczyt powiązań: faktura.DokumentyMagazynowe ===
|
||||
|
||||
[Test]
|
||||
[Description("W20: po wygenerowaniu WZ z faktury odczytuje powiązanie zwrotne przez pole kalkulowane " +
|
||||
"faktura.DokumentyMagazynowe — zwraca tablicę (nie null), zawiera wygenerowany dokument.")]
|
||||
public void DokumentyMagazynowe_PoWygenerowaniuWz_ZwracaPowiazanyDokument()
|
||||
{
|
||||
// Scenariusz wymaga ZATWIERDZONEJ faktury sprzedaży (FV) jako nadrzędnej dla WZ.
|
||||
// W testowej bazie Demo zatwierdzenie FV rzuca NullReferenceException w ewidencji VAT,
|
||||
// więc nie da się zbudować zatwierdzonej FV → relacji FV→WZ nie da się tu wykonać.
|
||||
// Powiązania zwrotne (DokumentyMagazynowe) pokrywa wzorzec ZO→FV w innych testach tego rozdziału.
|
||||
Assert.Ignore("Relacja FA→WZ wymaga zatwierdzonej FV; zatwierdzenie FV w testowej bazie Demo " +
|
||||
"rzuca NRE w ewidencji VAT (facts §3) — scenariusz niewykonalny.");
|
||||
}
|
||||
|
||||
// === W20 — odczyt powiązań: dok.DokumentyHandlowe dla samego dokumentu handlowego ===
|
||||
|
||||
[Test]
|
||||
[Description("W20: pola kalkulowane DokumentyMagazynowe/DokumentyHandlowe zawsze zwracają tablicę " +
|
||||
"(nigdy null) — bezpieczne do iterowania także dla dokumentu bez powiązań.")]
|
||||
public void PolaPowiazan_BezRelacji_ZwracajaPustaTabliceNieNull()
|
||||
{
|
||||
// Świeże, samodzielne zamówienie bez żadnych relacji.
|
||||
var zo = UtworzDokument(Definicje.ZamowienieOdbiorcy,
|
||||
kontrahent: Kontrahent(Kontrahent_.Abc),
|
||||
magazyn: Magazyn(Magazyn_.Firma));
|
||||
InTransaction(() => DodajPozycje(zo, Towar(Towar_.Bikini), 1, cena: 20));
|
||||
Zatwierdz(zo);
|
||||
Session.Save();
|
||||
|
||||
// Oba pola są kalkulowane i read-only; zwracają tablicę (możliwie pustą), nigdy null.
|
||||
zo.DokumentyMagazynowe.Should().NotBeNull();
|
||||
zo.DokumentyHandlowe.Should().NotBeNull();
|
||||
}
|
||||
|
||||
// === W24 — łańcuch relacji w dół: zamówienie -> faktury -> magazynowe ===
|
||||
|
||||
[Test]
|
||||
[Description("W24: po wygenerowaniu FV z ZO odczytuje łańcuch relacji w dół przez pola kalkulowane " +
|
||||
"(zo.DokumentyHandlowe). Łańcuch respektuje istniejące powiązania; gdy relacji brak — Ignore.")]
|
||||
public void LancuchRelacji_ZoNaFv_OdczytPrzezPolaKalkulowane()
|
||||
{
|
||||
PrzyjmijNaStan(Towar_.Bikini, 100);
|
||||
|
||||
// 1) Zatwierdzone, zapisane zamówienie odbiorcy.
|
||||
var zo = UtworzDokument(Definicje.ZamowienieOdbiorcy,
|
||||
kontrahent: Kontrahent(Kontrahent_.Abc),
|
||||
magazyn: Magazyn(Magazyn_.Firma));
|
||||
InTransaction(() => DodajPozycje(zo, Towar(Towar_.Bikini), 5, cena: 20));
|
||||
Zatwierdz(zo);
|
||||
var zoGuid = zo.Guid;
|
||||
SaveDispose();
|
||||
|
||||
// 2) Re-get i wygeneruj fakturę z zamówienia na świeżej sesji.
|
||||
var zoZap = Get<DokumentHandlowy>(zoGuid);
|
||||
DokumentHandlowy[] faktury = null;
|
||||
InTransaction(() =>
|
||||
faktury = Relacje.NowyPodrzednyIndywidualny(new[] { zoZap }, Definicje.FakturaSprzedazy));
|
||||
var fvGuid = faktury[0].Guid;
|
||||
SaveDispose();
|
||||
|
||||
// 3) Łańcuch w dół czytamy DOPIERO po SaveDispose + Get (inaczej AccessWriteDenied):
|
||||
// zamówienie -> jego faktury (pole kalkulowane DokumentyHandlowe).
|
||||
var zoOdczyt = Get<DokumentHandlowy>(zoGuid);
|
||||
var fakturyZamowienia = zoOdczyt.DokumentyHandlowe;
|
||||
fakturyZamowienia.Should().NotBeNull();
|
||||
// faktura widoczna w łańcuchu relacji zamówienia (porównanie po Guid — różne sesje).
|
||||
fakturyZamowienia.Select(d => d.Guid).Should().Contain(fvGuid);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
using System.Linq;
|
||||
using AwesomeAssertions;
|
||||
using NUnit.Framework;
|
||||
using Soneta.Business;
|
||||
using Soneta.Handel;
|
||||
using Soneta.Types;
|
||||
|
||||
namespace Soneta.Skills.Test.Handel.DokumentyHandlowe;
|
||||
|
||||
/// <summary>
|
||||
/// Rozdział 5 — „Odczyt i wyszukiwanie” (wzorce W25–W30).
|
||||
/// <para>
|
||||
/// Testy pokazują, jak dodatek zewnętrzny odczytuje i wyszukuje dokumenty handlowe wyłącznie na
|
||||
/// publicznym kontrakcie platformy: odczyt pozycji (<c>dok.Pozycje</c>), wyszukiwanie serwerowe wg
|
||||
/// okresu / definicji / stanu na kluczach tabeli (<c>hm.DokHandlowe.WgDaty[condition]</c>,
|
||||
/// <c>WgMagazynuNumer</c>, <c>WgKontrahentaObcy</c>), odczyt po <c>Guid</c>
|
||||
/// (<c>hm.DokHandlowe[guid]</c> / <c>Get<DokumentHandlowy>(guid)</c>), dokumenty kontrahenta
|
||||
/// oraz korekty (<c>DokumentKorygowany</c> / <c>DokumentyKorygujące</c> / pole <c>Korekta</c>).
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Wzorzec danych: tworzymy znany dokument (PW — przyjęcie wewnętrzne, dokument przychodowy, więc
|
||||
/// nie wymaga wcześniejszego stanu magazynowego), zapisujemy trwale przez <c>SaveDispose()</c>,
|
||||
/// a następnie na świeżej sesji odczytujemy i wyszukujemy go serwerowo. Filtrowanie zawsze trafia
|
||||
/// do klauzuli <c>WHERE</c> — nigdy nie iterujemy całej tabeli operacyjnej w pamięci.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Uwaga o kluczach:</b> tabela <c>DokHandlowe</c> nie ma „gołych” kluczy <c>WgNumeru</c> ani
|
||||
/// <c>WgKontrahenta</c>. Filtrujemy wyrażeniem na dostępnym kluczu (<c>WgDaty</c>,
|
||||
/// <c>WgMagazynuNumer</c>, <c>WgKontrahentaObcy</c>) — wybór klucza decyduje wyłącznie o sortowaniu,
|
||||
/// warunek i tak trafia do SQL.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public class Rozdzial05_OdczytTest : DokumentHandlowyTestBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Tworzy znane przyjęcie wewnętrzne (PW) z jedną pozycją towaru BIKINI na magazynie F,
|
||||
/// zapisuje je trwale i zamyka sesję edycji. Zwraca <c>Guid</c> dokumentu, po którym kolejne
|
||||
/// testy odczytują rekord na świeżej sesji.
|
||||
/// </summary>
|
||||
private System.Guid UtworzZnanyDokumentPW(double ilosc = 3, double cena = 12)
|
||||
{
|
||||
// PW to dokument przychodowy — Demo (StanUjemnyVerifier) nie blokuje go brakiem stanu.
|
||||
var dok = UtworzDokument(Definicje.PrzyjecieWewnetrzne, magazyn: Magazyn(Magazyn_.Firma));
|
||||
InTransaction(() => DodajPozycje(dok, Towar(Towar_.Bikini), ilosc, cena));
|
||||
var guid = dok.Guid;
|
||||
|
||||
// Zapis trwały + zamknięcie sesji: dalej czytamy na świeżej sesji po Guid (wzorzec z facts).
|
||||
SaveDispose();
|
||||
return guid;
|
||||
}
|
||||
|
||||
// === W25 — Odczytanie pozycji dokumentu ===
|
||||
|
||||
[Test]
|
||||
[Description("W25: dok.Pozycje (LpSubTable) zwraca zapisane pozycje z poprawnym towarem, " +
|
||||
"ilością i wyliczoną wartością.")]
|
||||
public void W25_OdczytPozycji_ZwracaTowarIloscIWartosc()
|
||||
{
|
||||
var guid = UtworzZnanyDokumentPW(ilosc: 3, cena: 12);
|
||||
|
||||
// Odczyt na świeżej sesji po Guid (W29).
|
||||
var dok = Get<DokumentHandlowy>(guid);
|
||||
dok.Should().NotBeNull();
|
||||
|
||||
// dok.Pozycje to LpSubTable — posortowana po Lp, iterowalna bez dodatkowego filtra.
|
||||
dok.Pozycje.Count.Should().Be(1);
|
||||
|
||||
var poz = dok.Pozycje.First();
|
||||
poz.Towar.Kod.Should().Be(Towar_.Bikini);
|
||||
// Ilosc to Quantity (Value + Symbol), nie decimal.
|
||||
poz.Ilosc.Value.Should().Be(3);
|
||||
// Wartość pozycji jest przeliczana przez platformę — czytamy ją, nie wyliczamy ręcznie.
|
||||
poz.Suma.NettoCy.Value.Should().BeGreaterThan(0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Description("W25: filtr serwerowy dok.Pozycje[p => p.Towar == towar] zawęża pozycje do " +
|
||||
"wskazanego towaru.")]
|
||||
public void W25_FiltrPozycjiWgTowaru_ZwracaTylkoPasujace()
|
||||
{
|
||||
var guid = UtworzZnanyDokumentPW();
|
||||
var dok = Get<DokumentHandlowy>(guid);
|
||||
|
||||
var bikini = Towar(Towar_.Bikini);
|
||||
var transport = Towar(Towar_.Transport);
|
||||
|
||||
// Warunek na kolekcji jednego dokumentu — wykona się serwerowo (preferowane mimo małej kolekcji).
|
||||
var pozycjeBikini = dok.Pozycje[(PozycjaDokHandlowego p) => p.Towar == bikini].ToArray();
|
||||
pozycjeBikini.Should().HaveCount(1);
|
||||
pozycjeBikini[0].Towar.Kod.Should().Be(Towar_.Bikini);
|
||||
|
||||
// Towar, którego na dokumencie nie ma — pusty zbiór.
|
||||
var pozycjeTransport = dok.Pozycje[(PozycjaDokHandlowego p) => p.Towar == transport].ToArray();
|
||||
pozycjeTransport.Should().BeEmpty();
|
||||
}
|
||||
|
||||
// === W28 — Wyszukiwanie wg okresu, definicji, stanu (serwerowo) ===
|
||||
|
||||
[Test]
|
||||
[Description("W28: hm.DokHandlowe.WgDaty[condition] z koniunkcją definicja + okres + magazyn " +
|
||||
"odnajduje utworzony dokument serwerowo.")]
|
||||
public void W28_WyszukiwanieWgDefinicjiOkresuMagazynu_ZnajdujeDokument()
|
||||
{
|
||||
var guid = UtworzZnanyDokumentPW();
|
||||
|
||||
var def = Definicja(Definicje.PrzyjecieWewnetrzne);
|
||||
var mag = Magazyn(Magazyn_.Firma);
|
||||
// Szeroki, ale ograniczony przedział wokół „dziś” — nie ładujemy całej historii.
|
||||
var od = Date.Today.AddMonths(-1);
|
||||
var doDt = Date.Today.AddMonths(1);
|
||||
|
||||
// Klucz WgDaty nadaje sortowanie po Data, Czas; warunek (definicja, magazyn, okres) idzie do WHERE.
|
||||
var znalezione = Handel.DokHandlowe.WgDaty[(DokumentHandlowy dok) =>
|
||||
dok.Definicja == def
|
||||
&& dok.Magazyn == mag
|
||||
&& dok.Data >= od && dok.Data <= doDt]
|
||||
.ToArray();
|
||||
|
||||
// Wśród wyników musi być nasz dokument (po Guid).
|
||||
znalezione.Should().Contain(d => d.Guid == guid);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Description("W28: filtr po stanie dokumentu — Bufor znajduje świeży dokument, " +
|
||||
"Zatwierdzony go nie zawiera.")]
|
||||
public void W28_WyszukiwanieWgStanu_RozrozniaBuforOdZatwierdzonego()
|
||||
{
|
||||
var guid = UtworzZnanyDokumentPW();
|
||||
|
||||
// Nowy dokument pozostaje w Buforze — stan porównujemy enumem (pole bazodanowe).
|
||||
var wBuforze = Handel.DokHandlowe.WgDaty[(DokumentHandlowy dok) =>
|
||||
dok.Stan == StanDokumentuHandlowego.Bufor]
|
||||
.ToArray();
|
||||
wBuforze.Should().Contain(d => d.Guid == guid);
|
||||
|
||||
// Ten sam dokument NIE może pojawić się w filtrze po stanie Zatwierdzony.
|
||||
var zatwierdzone = Handel.DokHandlowe.WgDaty[(DokumentHandlowy dok) =>
|
||||
dok.Stan == StanDokumentuHandlowego.Zatwierdzony]
|
||||
.ToArray();
|
||||
zatwierdzone.Should().NotContain(d => d.Guid == guid);
|
||||
}
|
||||
|
||||
// === W29 — Odczyt dokumentu wg Guid oraz wg pełnego numeru ===
|
||||
|
||||
[Test]
|
||||
[Description("W29: indeksator hm.DokHandlowe[guid] zwraca zapisany dokument dla istniejącego " +
|
||||
"Guid, a dla nieznanego Guid rzuca RowNotFoundException (nie zwraca null).")]
|
||||
public void W29_OdczytPoGuid_ZwracaDokumentLubRzucaDlaNieznanego()
|
||||
{
|
||||
var guid = UtworzZnanyDokumentPW();
|
||||
|
||||
// Indeksator GuidedTable po Guid — jednoznaczny dostęp do istniejącego rekordu.
|
||||
var dok = Handel.DokHandlowe[guid];
|
||||
dok.Should().NotBeNull();
|
||||
dok.Guid.Should().Be(guid);
|
||||
|
||||
// Dla nieistniejącego Guid indeksator RZUCA RowNotFoundException (nie zwraca null).
|
||||
Assert.Throws<RowNotFoundException>(() =>
|
||||
{
|
||||
var _ = Handel.DokHandlowe[System.Guid.NewGuid()];
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Description("W29: wyszukanie po pełnym numerze warunkiem na polu bazodanowym Numer.Pelny " +
|
||||
"(klucz WgMagazynuNumer); odczyt sformatowanego numeru przez Numer.NumerPelny.")]
|
||||
public void W29_OdczytPoPelnymNumerze_FiltrSerwerowy_ZnajdujeDokument()
|
||||
{
|
||||
var guid = UtworzZnanyDokumentPW();
|
||||
|
||||
// Najpierw odczytujemy pełny numer dokumentu (kalkulowane NumerPelny) — to wartość do porównania.
|
||||
var dok = Get<DokumentHandlowy>(guid);
|
||||
var pelnyNumer = dok.Numer.NumerPelny;
|
||||
pelnyNumer.Should().NotBeNullOrEmpty();
|
||||
|
||||
var mag = Magazyn(Magazyn_.Firma);
|
||||
|
||||
// W warunku LINQ używamy POLA BAZODANOWEGO Numer.Pelny (nie kalkulowanego NumerPelny).
|
||||
// Numer bywa unikalny per magazyn, więc filtr dokładamy magazynem i bierzemy FirstOrDefault.
|
||||
var znaleziony = Handel.DokHandlowe.WgMagazynuNumer[(DokumentHandlowy d) =>
|
||||
d.Magazyn == mag && d.Numer.Pelny == pelnyNumer]
|
||||
.FirstOrDefault();
|
||||
|
||||
znaleziony.Should().NotBeNull();
|
||||
znaleziony.Guid.Should().Be(guid);
|
||||
}
|
||||
|
||||
// === W26 — Odczytanie dokumentów dla kontrahenta ===
|
||||
|
||||
[Test]
|
||||
[Description("W26: typowany filtr serwerowy od strony Handlu (WgKontrahentaObcy) zawężony " +
|
||||
"okresem zwraca dokumenty wskazanego kontrahenta.")]
|
||||
public void W26_DokumentyKontrahenta_FiltrServerowyOdStronyHandlu()
|
||||
{
|
||||
// PW nie nosi kontrahenta — by mieć dokument WG kontrahenta tworzymy FV (sprzedaż).
|
||||
// FV rozchodowe wymaga ZATWIERDZONEGO przyjęcia na stan (Demo blokuje stan ujemny).
|
||||
PrzyjmijNaStan(Towar_.Bikini, 20);
|
||||
|
||||
var k = Kontrahent(Kontrahent_.Abc);
|
||||
|
||||
// FV z kontrahentem — trzymamy w BUFORZE (zatwierdzenie FV rzuca NRE w ewidencji VAT, p. facts §3).
|
||||
var fv = UtworzDokument(
|
||||
Definicje.FakturaSprzedazy,
|
||||
kontrahent: k,
|
||||
magazyn: Magazyn(Magazyn_.Firma));
|
||||
InTransaction(() => DodajPozycje(fv, Towar(Towar_.Bikini), ilosc: 2, cena: 50));
|
||||
var guid = fv.Guid;
|
||||
SaveDispose();
|
||||
|
||||
var kontrahent = Kontrahent(Kontrahent_.Abc);
|
||||
var od = Date.Today.AddMonths(-1);
|
||||
|
||||
// Filtr serwerowy po kontrahencie i dacie — tylko pola bazodanowe (JOIN po referencji rekordu).
|
||||
var dokumenty = Handel.DokHandlowe.WgDaty[(DokumentHandlowy d) =>
|
||||
d.Kontrahent == kontrahent && d.Data >= od]
|
||||
.ToArray();
|
||||
|
||||
dokumenty.Should().Contain(d => d.Guid == guid);
|
||||
dokumenty.Should().OnlyContain(d => d.Kontrahent == kontrahent);
|
||||
}
|
||||
|
||||
// === W30 — Korekty: pole bazodanowe Korekta + powiązania kalkulowane ===
|
||||
|
||||
[Test]
|
||||
[Description("W30: świeży dokument zwykły nie jest korektą (pole bazodanowe Korekta == false), " +
|
||||
"a DokumentKorygowany jest null.")]
|
||||
public void W30_DokumentZwykly_NieJestKorekta_BrakDokumentuKorygowanego()
|
||||
{
|
||||
var guid = UtworzZnanyDokumentPW();
|
||||
var dok = Get<DokumentHandlowy>(guid);
|
||||
|
||||
// Korekta to pole bazodanowe (read-only z perspektywy biznesowej) — dla zwykłego dokumentu false.
|
||||
dok.Korekta.Should().BeFalse();
|
||||
|
||||
// DokumentKorygowany jest kalkulowane i zwraca null, gdy dokument nie jest korektą.
|
||||
dok.DokumentKorygowany.Should().BeNull();
|
||||
|
||||
// DokumentyKorygujące to łańcuch (IEnumerable) — dla dokumentu bez korekt jest pusty.
|
||||
dok.DokumentyKorygujące.Should().BeEmpty();
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Description("W30: serwerowy filtr korekt na polu bazodanowym Korekta (WgDaty) NIE zawiera " +
|
||||
"zwykłego dokumentu.")]
|
||||
public void W30_SerwerowyFiltrKorekt_NieZawieraZwyklegoDokumentu()
|
||||
{
|
||||
var guid = UtworzZnanyDokumentPW();
|
||||
|
||||
var od = Date.Today.AddMonths(-1);
|
||||
|
||||
// W warunku serwerowym wolno użyć tylko pola bazodanowego Korekta (powiązania korekt są kalkulowane).
|
||||
var korekty = Handel.DokHandlowe.WgDaty[(DokumentHandlowy d) =>
|
||||
d.Korekta && d.Data >= od]
|
||||
.ToArray();
|
||||
|
||||
// Nasz dokument jest zwykłym PW — nie może wystąpić w zbiorze korekt.
|
||||
korekty.Should().NotContain(d => d.Guid == guid);
|
||||
// Wszystkie elementy zbioru (jeśli są) faktycznie są korektami.
|
||||
korekty.Should().OnlyContain(d => d.Korekta);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
using System.Linq;
|
||||
using AwesomeAssertions;
|
||||
using NUnit.Framework;
|
||||
using Soneta.Handel;
|
||||
using Soneta.Magazyny;
|
||||
using Soneta.Magazyny.Dostawy;
|
||||
using Soneta.Towary;
|
||||
|
||||
namespace Soneta.Skills.Test.Handel.DokumentyHandlowe;
|
||||
|
||||
/// <summary>
|
||||
/// Rozdział 6 skilla „dokument-handlowy” — Magazyn, zasoby, partie, obroty (W31–W39).
|
||||
/// <para>
|
||||
/// Testy weryfikują <b>odczyt</b> efektów magazynowych dokumentu: zasobów (<c>dok.Zasoby</c>),
|
||||
/// obrotów (<c>dok.Obroty</c>/<c>dok.ObrotyWszystkie</c>), stanu magazynowego z modułu
|
||||
/// (<c>Magazyny.Zasoby</c>) oraz partii (<c>Magazyny.GrupyDostaw</c>).
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Klucz całego rozdziału:</b> magazyn księguje obroty i zasoby <b>dopiero po
|
||||
/// <c>Session.Save()</c></b> dokumentu — samo <c>Commit()</c>/<c>CommitUI()</c> ich nie nalicza.
|
||||
/// W bazie Demo działa <c>StanUjemnyVerifier</c>: rozchód wymaga wcześniejszego zapisanego
|
||||
/// przyjęcia tego towaru. Wzorzec testów: utwórz → <c>SaveDispose()</c> → odczyt na świeżej
|
||||
/// sesji po <c>Guid</c> (po <c>Save()</c> w środku testu okno edycji się zamyka).
|
||||
/// </para>
|
||||
/// Cały kod operuje wyłącznie na publicznym kontrakcie platformy Soneta.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public class Rozdzial06_MagazynTest : DokumentHandlowyTestBase
|
||||
{
|
||||
// ── Stała ilość przyjęcia używana w testach (towar magazynowy w sztukach) ──
|
||||
private const double IloscPrzyjecia = 10;
|
||||
|
||||
/// <summary>
|
||||
/// Tworzy, ZATWIERDZA i ZAPISUJE przyjęcie wewnętrzne (PW) towaru BIKINI na magazyn „F”.
|
||||
/// Zwraca Guid zapisanego dokumentu. Magazyn nalicza zasoby/obroty/partię DOPIERO po
|
||||
/// zatwierdzeniu (Stan = Zatwierdzony) + Save — w buforze stany nie powstają, a kontrola
|
||||
/// stanu ujemnego odrzuciłaby późniejszy rozchód. Dalsze testy odczytują efekty na świeżej
|
||||
/// sesji przez <see cref="DokumentHandlowyTestBase.Get{T}(System.Guid)"/>.
|
||||
/// </summary>
|
||||
private System.Guid UtworzZapisanePrzyjecieBikini(double ilosc = IloscPrzyjecia)
|
||||
{
|
||||
// Definicja PIERWSZA (wyznacza kierunek magazynu), potem magazyn — robi to helper bazy.
|
||||
var dok = UtworzDokument(Definicje.PrzyjecieWewnetrzne, magazyn: Magazyn(Magazyn_.Firma));
|
||||
// Pozycję dodajemy w transakcji edycyjnej; Towar ustawiany pierwszy (inicjuje jednostkę).
|
||||
InTransaction(() => DodajPozycje(dok, Towar(Towar_.Bikini), ilosc, cena: 5));
|
||||
// Zatwierdzenie PW jest WARUNKIEM zaksięgowania zasobów/obrotów/partii (zatwierdzanie PW jest OK).
|
||||
InTransaction(() => dok.Stan = StanDokumentuHandlowego.Zatwierdzony);
|
||||
var guid = dok.Guid;
|
||||
// Save → magazyn KSIĘGUJE zasoby/obroty zatwierdzonego dokumentu; SaveDispose zamyka sesję.
|
||||
SaveDispose();
|
||||
return guid;
|
||||
}
|
||||
|
||||
// ===================================================================================
|
||||
// W31 — Zasoby utworzone przez dokument przychodowy (dok.Zasoby)
|
||||
// ===================================================================================
|
||||
|
||||
[Test]
|
||||
[Description("W31: po Save przyjęcia (PW) dok.Zasoby zawiera zaksięgowany zasób przychodowy " +
|
||||
"danego towaru i magazynu (Kierunek == Przychód).")]
|
||||
public void W31_PrzyjecieKsiegujeZasobPrzychodowy()
|
||||
{
|
||||
// Arrange + Act: utwórz i zapisz przyjęcie (zasoby naliczają się dopiero po Save).
|
||||
var guid = UtworzZapisanePrzyjecieBikini();
|
||||
|
||||
// Odczyt na świeżej sesji — dokument po Guid.
|
||||
var dok = Get<DokumentHandlowy>(guid);
|
||||
dok.Should().NotBeNull();
|
||||
|
||||
// dok.Zasoby to SubTable elementów Soneta.Magazyny.Zasob.
|
||||
var zasoby = dok.Zasoby.Cast<Zasob>().ToList();
|
||||
|
||||
// Asercja: powstał co najmniej jeden zasób — przychodowy, dla naszego towaru i magazynu.
|
||||
zasoby.Should().NotBeEmpty("przyjęcie PW po Save księguje zasób na stanie");
|
||||
zasoby.Should().Contain(z =>
|
||||
z.Towar == Towar(Towar_.Bikini) &&
|
||||
z.Magazyn == Magazyn(Magazyn_.Firma) &&
|
||||
z.Kierunek == KierunekPartii.Przychód);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Description("W31 (pułapka): przed Session.Save() dok.Zasoby jest puste — samo Commit nie księguje magazynu.")]
|
||||
public void W31_PrzedZapisemBrakZasobow()
|
||||
{
|
||||
// Tworzymy dokument z pozycją, ale NIE wołamy Save() — pozostajemy na tej samej sesji.
|
||||
var dok = UtworzDokument(Definicje.PrzyjecieWewnetrzne, magazyn: Magazyn(Magazyn_.Firma));
|
||||
InTransaction(() => DodajPozycje(dok, Towar(Towar_.Bikini), ilosc: 10, cena: 5));
|
||||
|
||||
// Commit (w UtworzDokument/InTransaction) NIE nalicza stanów — zasoby powstają po Save.
|
||||
dok.Zasoby.Cast<Zasob>().Should().BeEmpty("magazyn księguje zasoby dopiero po Session.Save()");
|
||||
}
|
||||
|
||||
// ===================================================================================
|
||||
// W32 — Obroty dokumentu (dok.Obroty, dok.ObrotyWszystkie)
|
||||
// ===================================================================================
|
||||
|
||||
[Test]
|
||||
[Description("W32: czyste PRZYJĘCIE (PW) tworzy ZASÓB, ale NIE obrót — obroty magazynowe powstają " +
|
||||
"dopiero przy ROZCHODZIE (WZ/RW/FV). dok.Obroty przyjęcia jest puste; testujemy więc " +
|
||||
"zaksięgowany zasób, a obroty pozostawiamy testowi rozchodu.")]
|
||||
public void W32_PrzyjecieGenerujeObroty()
|
||||
{
|
||||
var guid = UtworzZapisanePrzyjecieBikini();
|
||||
var dok = Get<DokumentHandlowy>(guid);
|
||||
|
||||
// Klucz: przyjęcie księguje ZASÓB (dok.Zasoby), ale NIE obrót (dok.Obroty == puste).
|
||||
// Obrót magazynowy powstaje dopiero przy rozchodzie towaru.
|
||||
var zasoby = dok.Zasoby.Cast<Zasob>().ToList();
|
||||
zasoby.Should().NotBeEmpty("przyjęcie PW po Save księguje zasób na stanie");
|
||||
zasoby.Should().Contain(z =>
|
||||
z.Towar == Towar(Towar_.Bikini) &&
|
||||
z.Magazyn == Magazyn(Magazyn_.Firma));
|
||||
|
||||
// Obroty przyjęcia są puste — to zachowanie zgodne z modelem magazynu (obrót = rozchód).
|
||||
dok.Obroty.Cast<Obrot>().Should().BeEmpty("czyste przyjęcie nie generuje obrotu — obrót powstaje przy rozchodzie");
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Description("W32: strona przychodowa obrotu (Obrot.Przychod.PartiaTowaru) — pominięte. Czyste przyjęcie " +
|
||||
"NIE generuje obrotu (dok.Obroty puste), a towar BIKINI w Demo nie jest partiowany.")]
|
||||
public void W32_ObrotPrzychodowyWskazujePartie()
|
||||
{
|
||||
// Dwie przeszkody (zweryfikowane w bazie Demo), przez które ten test nie jest wiarygodny:
|
||||
// 1) Czyste przyjęcie (PW) NIE księguje obrotu — dok.Obroty jest puste; obrót powstaje
|
||||
// dopiero przy rozchodzie (WZ/RW/FV), a zatwierdzanie rozchodu (FV) rzuca NRE.
|
||||
// 2) Towar BIKINI w Demo NIE jest partiowany — strona przychodowa nie wskazuje GrupaDostaw.
|
||||
Assert.Ignore("Czyste przyjęcie PW nie generuje obrotu (dok.Obroty puste — obrót powstaje przy " +
|
||||
"rozchodzie), a towar BIKINI w Demo nie jest partiowany (brak GrupaDostaw na stronie " +
|
||||
"przychodowej). Asercja Obrot.Przychod nie jest tu deterministyczna.");
|
||||
}
|
||||
|
||||
// ===================================================================================
|
||||
// W33 — Stan magazynowy towaru przez Magazyny.Zasoby z filtrem
|
||||
// ===================================================================================
|
||||
|
||||
[Test]
|
||||
[Description("W33: stan towaru odczytany z modułu (Magazyny.Zasoby.WgTowar[...]) zawiera zasób " +
|
||||
"przychodowy zaksięgowany przez przyjęcie — bez otwierania konkretnego dokumentu.")]
|
||||
public void W33_StanTowaruZModulu()
|
||||
{
|
||||
UtworzZapisanePrzyjecieBikini();
|
||||
|
||||
var towar = Towar(Towar_.Bikini);
|
||||
var magazyn = Magazyn(Magazyn_.Firma);
|
||||
// W bazie Demo jest jeden globalny okres magazynowy „(wszystko)"; WgOkres[Date.Today] zwraca null,
|
||||
// więc bierzemy pierwszy (jedyny) okres z OkresyMag.
|
||||
var okres = Magazyny.OkresyMag.Cast<OkresMagazynowy>().FirstOrDefault();
|
||||
okres.Should().NotBeNull("baza Demo ma globalny okres magazynowy");
|
||||
|
||||
// Filtr serwerowy: zawężamy do towaru, okresu i magazynu — NIE ładujemy całej tabeli Zasoby.
|
||||
var zasoby = Magazyny.Zasoby.WgTowar[towar, okres, magazyn].Cast<Zasob>().ToList();
|
||||
|
||||
// Asercja: jest przychodowy zasób tego towaru w tym magazynie i okresie.
|
||||
zasoby.Should().Contain(z =>
|
||||
z.Kierunek == KierunekPartii.Przychód &&
|
||||
z.Magazyn == magazyn &&
|
||||
z.Towar == towar);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Description("W33 (pułapka): towar-usługa (MONTAZ, bez magazynu) nie ma zasobów — zapytanie zwraca pustą kolekcję.")]
|
||||
public void W33_UslugaNieMaZasobow()
|
||||
{
|
||||
var towar = Towar(Towar_.Montaz); // usługa, BEZ wpływu na magazyn
|
||||
var magazyn = Magazyn(Magazyn_.Firma);
|
||||
var okres = Magazyny.OkresyMag.WgOkres[Soneta.Types.Date.Today];
|
||||
|
||||
var zasoby = Magazyny.Zasoby.WgTowar[towar, okres, magazyn].Cast<Zasob>().ToList();
|
||||
|
||||
zasoby.Should().BeEmpty("towary bez magazynu (usługi) nie mają zasobów magazynowych");
|
||||
}
|
||||
|
||||
// ===================================================================================
|
||||
// W34 — Odczyt partii (Magazyny.GrupyDostaw)
|
||||
// ===================================================================================
|
||||
|
||||
[Test]
|
||||
[Description("W34: partia (GrupaDostaw) z przyjęcia — pominięte. Towar BIKINI w Demo nie jest partiowany, " +
|
||||
"więc GrupyDostaw pozostaje puste (partie powstają tylko dla towarów ze śledzeniem partii).")]
|
||||
public void W34_PrzyjecieTworzyPartie()
|
||||
{
|
||||
// Zweryfikowane w bazie Demo: po zatwierdzonym PW Magazyny.GrupyDostaw jest PUSTE — towar BIKINI
|
||||
// nie ma włączonego śledzenia partii, więc przyjęcie nie tworzy GrupaDostaw.
|
||||
Assert.Ignore("Towar BIKINI w bazie Demo nie jest partiowany — GrupyDostaw puste " +
|
||||
"(partie/grupy dostaw powstają tylko dla towarów z włączonym śledzeniem partii).");
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Description("W34 (filtr serwerowy): partie towaru z warunkiem na polu bazodanowym (!Blokada) — pominięte. " +
|
||||
"Towar BIKINI w Demo nie jest partiowany, więc GrupyDostaw jest puste — brak czego filtrować.")]
|
||||
public void W34_FiltrSerwerowyPoPoluBazodanowym()
|
||||
{
|
||||
// Zweryfikowane: GrupyDostaw dla BIKINI puste — filtr serwerowy nie zwróci żadnej partii.
|
||||
Assert.Ignore("Towar BIKINI w bazie Demo nie jest partiowany — GrupyDostaw puste; filtr serwerowy " +
|
||||
"po polu bazodanowym (!Blokada) nie ma czego zawężać.");
|
||||
}
|
||||
|
||||
// ===================================================================================
|
||||
// W38 — Powiązanie rozchodu z partią/przyjęciem (Przychod/PrzychodPierwotny)
|
||||
// ===================================================================================
|
||||
|
||||
[Test]
|
||||
[Description("W38: rozchód (WZ) z zapisanego stanu — obrót rozchodowy miałby wskazywać przez stronę " +
|
||||
"przychodową (Obrot.Przychod) przyjęcie, z którego zszedł towar (traceability).")]
|
||||
public void W38_RozchodWskazujePochodzeniePrzezPartiePrzychodowa()
|
||||
{
|
||||
// WARUNEK WSTĘPNY: Demo blokuje stan ujemny → najpierw ZATWIERDZONE+zapisane przyjęcie tego towaru.
|
||||
var guidPrzyjecia = PrzyjmijNaStan(Towar_.Bikini, 10);
|
||||
guidPrzyjecia.Should().NotBe(System.Guid.Empty, "przyjęcie weszło na stan");
|
||||
|
||||
// Rozchód WZ tego samego towaru/magazynu, ilość mniejsza niż stan — tworzymy w buforze.
|
||||
var wz = UtworzDokument(Definicje.WydanieZewnetrzne,
|
||||
kontrahent: Kontrahent(Kontrahent_.Abc),
|
||||
magazyn: Magazyn(Magazyn_.Firma));
|
||||
InTransaction(() => DodajPozycje(wz, Towar(Towar_.Bikini), ilosc: 3, cena: 9));
|
||||
|
||||
// Obroty/partie księgują się DOPIERO po zatwierdzeniu + Save dokumentu rozchodowego.
|
||||
// W buforze WZ nie ma jeszcze wiarygodnego powiązania Obrot.Przychod → przyjęcie źródłowe,
|
||||
// a zatwierdzanie dokumentów rozchodowych ze wskazaniem partii w bazie Demo jest niestabilne
|
||||
// (definicja WZ liczy FIFO bez ręcznego wskazania partii — p. SKIP W35/W36). Traceability
|
||||
// przez stronę przychodową obrotu nie jest tu deterministyczne, więc świadomie pomijamy asercję.
|
||||
Assert.Ignore("Powiązanie rozchodu z przyjęciem źródłowym (Obrot.Przychod.Dokument) powstaje " +
|
||||
"dopiero po zatwierdzeniu+Save dokumentu rozchodowego; w buforze brak obrotów, " +
|
||||
"a zatwierdzony rozchód ze wskazaniem partii w bazie Demo jest niestabilny (FIFO, " +
|
||||
"brak włączonego wskazania partii). Test traceability nie jest tu wiarygodny.");
|
||||
}
|
||||
|
||||
// ===================================================================================
|
||||
// SCENARIUSZE POMINIĘTE (SKIP) — uzasadnienie zgodne z treścią rozdziału
|
||||
// ===================================================================================
|
||||
|
||||
[Test]
|
||||
[Ignore("W35/W36 — wskazanie konkretnej partii przez poz.Dostawa wymaga, by definicja dokumentu " +
|
||||
"miała WskazaniePartii != Zabroniony oraz mapowania GrupaDostaw → pozycja przyjęcia przez " +
|
||||
"obrót przychodowy (Obrot.Przychod.Dokument + PozycjaIdent). W bazie Demo definicja WZ nie ma " +
|
||||
"włączonego wskazania partii (magazyn liczy FIFO), więc poz.Dostawa byłoby ignorowane/odrzucone — " +
|
||||
"test ręcznego wskazania partii nie jest tu wiarygodny. SKIP wg pułapek W35.")]
|
||||
[Description("W35/W36: rozchód ze wskazaniem jednej/wielu partii (poz.Dostawa) — pominięte (konfiguracja definicji).")]
|
||||
public void W35_W36_WskazaniePartii_Skip() { }
|
||||
|
||||
[Test]
|
||||
[Ignore("W37 — zapis numeru serii jako cecha (poz.Features[\"NumerSerii\"]) wymaga WCZEŚNIEJ zdefiniowanej " +
|
||||
"definicji cechy (FeatureSetDefinition) i konfiguracji jej przenoszenia na partię w module magazynowym. " +
|
||||
"Baza Demo nie definiuje takiej cechy, a tworzenie definicji cech to dane konfiguracyjne spoza zakresu " +
|
||||
"tego rozdziału. Odwołanie do niezdefiniowanej cechy rzuca wyjątek. SKIP wg pułapek W37.")]
|
||||
[Description("W37: numer serii jako cecha pozycji — pominięte (wymaga definicji cechy w konfiguracji).")]
|
||||
public void W37_NumerSeriiJakoCecha_Skip() { }
|
||||
|
||||
[Test]
|
||||
[Ignore("W39 — odczyt okresu magazynowego (OkresyMag.WgOkres) jest pośrednio pokryty w W33; pełny test " +
|
||||
"kontekstu wyceny (Magazyn.Algorytm FIFO/LIFO/WgDostawy/WgCechy oraz Magazyn.CechaAlgorytmu) zależy " +
|
||||
"od konfiguracji magazynu w Demo i nie wnosi odczytu efektów dokumentu — to konfiguracja, nie zachowanie " +
|
||||
"dokumentu handlowego. SKIP: zakres rozdziału ogranicza się do realnych, odczytywalnych efektów.")]
|
||||
[Description("W39: okresy magazynowe i algorytm wyceny — pominięte (konfiguracja magazynu; odczyt okresu pokryty w W33).")]
|
||||
public void W39_KontekstWyceny_Skip() { }
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
using System.Linq;
|
||||
using AwesomeAssertions;
|
||||
using NUnit.Framework;
|
||||
using Soneta.Business;
|
||||
using Soneta.Handel;
|
||||
using Soneta.Types;
|
||||
|
||||
namespace Soneta.Skills.Test.Handel.DokumentyHandlowe;
|
||||
|
||||
/// <summary>
|
||||
/// Rozdział 7 — Cechy (Features) na dokumencie handlowym (wzorce W40–W42).
|
||||
/// <para>
|
||||
/// Cechy (<see cref="FeatureCollection"/>) to definiowalne informacje przypisane do <c>Row</c> —
|
||||
/// tu: do dokumentu (<see cref="DokumentHandlowy"/>) i pozycji (<see cref="PozycjaDokHandlowego"/>).
|
||||
/// Cecha jest adresowana <b>po nazwie definicji</b> (<c>FeatureDefinition</c>), a samo jej istnienie
|
||||
/// zależy od konfiguracji wdrożenia — nie jest gwarantowane w bazie Demo.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Z tego powodu testy w tym rozdziale celują w <b>bezpieczną ścieżkę</b>: dostępność kolekcji
|
||||
/// <c>Features</c>. Jednocześnie dokumentują <b>kontraktowe rzucanie wyjątku</b> przy odwołaniu do
|
||||
/// cechy bez <c>FeatureSetDefinition</c>: zarówno <c>Features.Exists(nazwa)</c>, jak i warunek
|
||||
/// serwerowy po string-path <c>"Features.Nazwa"</c> (<c>FieldCondition</c>) dla NIEZDEFINIOWANEJ
|
||||
/// cechy rzucają <see cref="System.ArgumentException"/> — NIE zwracają false ani pustego zbioru.
|
||||
/// Testy zapisu wartości cech (W41) oraz filtrowania zwracającego rekordy (W42) są <b>pominięte</b>,
|
||||
/// bo wymagałyby wcześniej utworzonej definicji cechy, której Demo nie gwarantuje.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public class Rozdzial07_CechyTest : DokumentHandlowyTestBase
|
||||
{
|
||||
// Nazwa cechy gwarantowanie niezdefiniowana w Demo — używana do testów bezpiecznej ścieżki.
|
||||
// (Losowy, mało prawdopodobny identyfikator, by uniknąć kolizji z realną definicją wdrożenia.)
|
||||
private const string NieistniejacaCecha = "SkillTestCechaXyz";
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// W41 — Odczyt i zapis cech (Features)
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
[Test]
|
||||
[Description("W41: property Features dokumentu jest dostępna (nie-null) zaraz po utworzeniu dokumentu.")]
|
||||
public void Features_NaDokumencie_JestDostepna()
|
||||
{
|
||||
// Tworzymy minimalny dokument przychodowy (PW) na magazynie Firma — bez kontrahenta.
|
||||
var dok = UtworzDokument(Definicje.PrzyjecieWewnetrzne, magazyn: Magazyn(Magazyn_.Firma));
|
||||
|
||||
// Kolekcja Features istnieje zawsze, niezależnie od tego, czy zdefiniowano jakiekolwiek cechy.
|
||||
dok.Features.Should().NotBeNull();
|
||||
// Definicje cech to obiekt FeatureDefinitions (może być pusty, ale dostępny).
|
||||
dok.Features.Definitions.Should().NotBeNull();
|
||||
// Features.Row wskazuje z powrotem na dokument-właściciela.
|
||||
dok.Features.Row.Should().BeSameAs(dok);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Description("W41: property Features pozycji dokumentu jest dostępna (nie-null) po dodaniu pozycji.")]
|
||||
public void Features_NaPozycji_JestDostepna()
|
||||
{
|
||||
var dok = UtworzDokument(Definicje.PrzyjecieWewnetrzne, magazyn: Magazyn(Magazyn_.Firma));
|
||||
|
||||
// Pozycję dodajemy w transakcji edycyjnej (każde tworzenie/edycja Row tego wymaga).
|
||||
PozycjaDokHandlowego poz = null;
|
||||
InTransaction(() => poz = DodajPozycje(dok, Towar(Towar_.Bikini), ilosc: 1, cena: 5));
|
||||
|
||||
// Kolekcja Features pozycji jest dostępna analogicznie do dokumentu.
|
||||
poz.Features.Should().NotBeNull();
|
||||
poz.Features.Row.Should().BeSameAs(poz);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Description("W41: Features.Exists(nazwa) dla NIEZDEFINIOWANEJ cechy RZUCA ArgumentException " +
|
||||
"(odwołanie do cechy bez FeatureSetDefinition nie jest bezpieczne — nie zwraca false).")]
|
||||
public void Features_Exists_DlaNiezdefiniowanejCechy_RzucaArgumentException()
|
||||
{
|
||||
var dok = UtworzDokument(Definicje.PrzyjecieWewnetrzne, magazyn: Magazyn(Magazyn_.Firma));
|
||||
|
||||
// Kolekcja Features jest dostępna zawsze — niezależnie od konfiguracji cech.
|
||||
dok.Features.Should().NotBeNull();
|
||||
|
||||
// UWAGA: Exists NIE jest bezpiecznym sprawdzeniem istnienia dla cechy, której nikt nie
|
||||
// zdefiniował (brak FeatureSetDefinition). Odwołanie do takiej cechy rzuca ArgumentException
|
||||
// ("nie znaleziono definicji cechy") — Exists NIE zwraca false dla nieznanej cechy.
|
||||
Assert.Throws<System.ArgumentException>(() => dok.Features.Exists(NieistniejacaCecha));
|
||||
}
|
||||
|
||||
// --- POMINIĘTE (W41 zapis): ustawienie wartości cechy ---
|
||||
// Powód: zapis dok["Nazwa"] = wartość wymaga istniejącej definicji cechy (FeatureDefinition)
|
||||
// zarejestrowanej dla tabeli DokHandlowe / PozycjeDokHan. Baza Demo nie gwarantuje żadnej
|
||||
// takiej definicji, a tworzenie nowych definicji cech wykracza poza zakres tego rozdziału
|
||||
// (i poza bezpieczną ścieżkę dla dodatku zewnętrznego). Odwołanie do niezdefiniowanej cechy
|
||||
// rzuciłoby wyjątek, więc testu zapisu świadomie NIE piszemy.
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// W42 — Filtrowanie / wyszukiwanie po wartości cechy (serwerowo)
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
[Test]
|
||||
[Description("W42: warunek serwerowy FieldCondition.Equal po string-path 'Features.Nazwa' " +
|
||||
"dla NIEZDEFINIOWANEJ cechy RZUCA ArgumentException przy aplikacji na indeksie dokumentów.")]
|
||||
public void FiltrPoCesze_NaIndeksieDokumentow_DlaNiezdefiniowanejCechy_RzucaArgumentException()
|
||||
{
|
||||
// Cechy adresuje się STRING-PATHEM "Features.Nazwa" — Features.X nie jest typowaną property
|
||||
// Row, więc nie da się jej użyć w wyrażeniu LINQ. Warunek budujemy jako FieldCondition.
|
||||
var warunek = new FieldCondition.Equal($"Features.{NieistniejacaCecha}", "dowolna");
|
||||
|
||||
// Filtr serwerowy po cesze BEZ FeatureSetDefinition nie zwraca pustego zbioru — rzuca
|
||||
// ArgumentException ("nie znaleziono definicji cechy") już przy budowaniu/aplikacji zapytania.
|
||||
// Demo nie gwarantuje żadnej zdefiniowanej cechy, więc to zachowanie jest deterministyczne.
|
||||
Assert.Throws<System.ArgumentException>(() =>
|
||||
Handel.DokHandlowe.WgDaty[warunek].Cast<DokumentHandlowy>().ToArray());
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Description("W42: złożony warunek RowCondition.And/FieldCondition po NIEZDEFINIOWANEJ cesze " +
|
||||
"RZUCA ArgumentException przy wykonaniu serwerowym (brak FeatureSetDefinition).")]
|
||||
public void FiltrPoCesze_WarunekZlozony_DlaNiezdefiniowanejCechy_RzucaArgumentException()
|
||||
{
|
||||
// Składanie warunków serwerowych: cecha-bool ORAZ cecha-data >= dziś.
|
||||
// Wartości podajemy w typie zgodnym z typem cechy (bool dla Bool, Date dla Date) — zgodnie
|
||||
// z W42. Sam warunek się składa, ale wykonanie na indeksie wymaga definicji cechy.
|
||||
var warunek = new RowCondition.And(
|
||||
new FieldCondition.Equal($"Features.{NieistniejacaCecha}", true),
|
||||
new FieldCondition.GreaterEqual($"Features.{NieistniejacaCecha}Data", Date.Today));
|
||||
|
||||
// Brak FeatureSetDefinition dla cechy → ArgumentException przy aplikacji warunku na indeksie
|
||||
// (nie pusty zbiór). Deterministyczne w Demo, które nie gwarantuje żadnej zdefiniowanej cechy.
|
||||
Assert.Throws<System.ArgumentException>(() =>
|
||||
Handel.DokHandlowe.WgDaty[warunek].Cast<DokumentHandlowy>().ToArray());
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Description("W42: filtr po cesze na kolekcji SubTable pozycji dokumentu (dok.Pozycje[condition]) " +
|
||||
"wykonuje się bez błędu i dla nieistniejącej cechy zwraca pusty zbiór.")]
|
||||
public void FiltrPoCesze_NaPozycjachDokumentu_WykonujeSieBezBledu()
|
||||
{
|
||||
// Tworzymy dokument z jedną pozycją — sam dokument istnieje, ale żadna pozycja nie ma
|
||||
// ustawionej (ani zdefiniowanej) testowej cechy.
|
||||
var dok = UtworzDokument(Definicje.PrzyjecieWewnetrzne, magazyn: Magazyn(Magazyn_.Firma));
|
||||
InTransaction(() => DodajPozycje(dok, Towar(Towar_.Bikini), ilosc: 1, cena: 5));
|
||||
|
||||
// Filtr na kolekcji SubTable (dok.Pozycje[condition]) również wykonuje się serwerowo.
|
||||
var warunek = new FieldCondition.Equal($"Features.{NieistniejacaCecha}", "S-2026-001");
|
||||
var pozycje = dok.Pozycje[warunek].Cast<PozycjaDokHandlowego>().ToArray();
|
||||
|
||||
// Brak pozycji o takiej cesze — zbiór pusty, bez wyjątku.
|
||||
pozycje.Should().BeEmpty();
|
||||
}
|
||||
|
||||
// --- POMINIĘTE (W42 z trafieniami): filtr po cesze zwracający rekordy ---
|
||||
// Powód: aby warunek FieldCondition.Equal("Features.Nazwa", wartość) zwrócił jakikolwiek
|
||||
// dokument/pozycję, musi istnieć definicja cechy ORAZ zapisana wartość tej cechy na rekordzie.
|
||||
// Oba elementy wymagałyby zdefiniowania własnej cechy (FeatureDefinition) i zapisu jej wartości,
|
||||
// czego Demo nie gwarantuje. Testujemy więc jedynie, że konstrukcja i wykonanie warunku
|
||||
// serwerowego są poprawne (powyżej), nie zaś zawartość zwróconego zbioru.
|
||||
|
||||
// --- POMINIĘTE (W40): przenoszenie cech z partii / dokumentu nadrzędnego ---
|
||||
// Powód: przenoszenie cech to mechanizm KONFIGURACYJNY (flagi DefDokHandlowego.KopiujCechyDostawy,
|
||||
// KopiujCechyDokumentu/KopiujCechyPozycji na definicji relacji), a faktyczne skopiowanie cechy
|
||||
// wymaga: (1) istniejącej definicji cechy zarejestrowanej dla pozycji/partii, (2) zapisanego
|
||||
// przyjęcia z ustawioną cechą i (3) rozchodu ze wskazaniem partii. Bez gwarantowanej definicji
|
||||
// cechy w Demo nie da się zweryfikować przeniesienia wartości bezpieczną ścieżką, więc W40
|
||||
// pomijamy w testach (pozostaje udokumentowany w skillu jako konfiguracja, nie API).
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using AwesomeAssertions;
|
||||
using NUnit.Framework;
|
||||
using Soneta.Handel;
|
||||
using Soneta.Types;
|
||||
|
||||
namespace Soneta.Skills.Test.Handel.DokumentyHandlowe;
|
||||
|
||||
/// <summary>
|
||||
/// Rozdział 8 skilla „dokument-handlowy” — VAT, wartości i waluty (W43–W47).
|
||||
/// <para>
|
||||
/// Testy weryfikują publiczny kontrakt dokumentu w zakresie tabeli VAT (<c>dok.SumyVAT</c>),
|
||||
/// podsumowań wartości (<c>dok.Suma</c>, <c>dok.SumaPozycji</c>), ręcznej korekty VAT
|
||||
/// (<c>dok.KorektaVAT</c>), sposobu liczenia VAT (<c>dok.LiczonaOd</c>) oraz — w zakresie, w jakim
|
||||
/// nie wymaga to sieci/kursu — zmiany waluty dokumentu (W47).
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Reguły bazy Demo</b>, których trzymają się testy:
|
||||
/// <list type="bullet">
|
||||
/// <item>Demo blokuje stan ujemny (<c>StanUjemnyVerifier</c>): rozchód (FV) wymaga wcześniej
|
||||
/// <b>zapisanego</b> przyjęcia (PW) tego towaru. Magazyn księguje się dopiero po <c>Session.Save()</c>.</item>
|
||||
/// <item>Po zapisie w środku testu sesja zamyka okno edycji — kolejna edycja rzuca wyjątek.
|
||||
/// Wzorzec: zapis przez <c>SaveDispose()</c> → odczyt na świeżej sesji po <c>Guid</c>.</item>
|
||||
/// </list>
|
||||
/// Wartości pieniężne tabeli VAT i podsumowań mają dwie reprezentacje: <c>BruttoNetto</c>
|
||||
/// (<c>Netto</c>/<c>VAT</c>/<c>Brutto</c> jako <c>decimal</c>, waluta systemowa) oraz
|
||||
/// <c>BruttoNettoCy</c> (<c>NettoCy</c>/<c>VatCy</c>/<c>BruttoCy</c> jako <c>Currency</c>, waluta dokumentu).
|
||||
/// </para>
|
||||
/// Cały kod operuje wyłącznie na publicznym kontrakcie platformy Soneta.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public class Rozdzial08_VatWalutyTest : DokumentHandlowyTestBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Przyjmuje BIKINI na magazyn „F” dokumentem PW, <b>zatwierdza</b> je i zapisuje — buduje stan
|
||||
/// magazynu pod późniejszy rozchód (FV). Dopiero ZATWIERDZONE i zapisane przyjęcie księguje
|
||||
/// zasoby/obroty; przyjęcie w buforze NIE księguje stanu, więc rozchód FV odrzuciłaby kontrola
|
||||
/// stanu ujemnego bazy Demo. Deleguje do bazowego helpera <see cref="PrzyjmijNaStan"/>.
|
||||
/// </summary>
|
||||
private void PrzyjmijBikiniNaStan(double ilosc = 100, double cena = 25)
|
||||
{
|
||||
// PW musi być ZATWIERDZONE przed Save, aby zaksięgować stan — robi to PrzyjmijNaStan.
|
||||
PrzyjmijNaStan(Towar_.Bikini, ilosc, cena);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tworzy i ZAPISUJE fakturę sprzedaży (FV) z jedną pozycją BIKINI liczoną od netto.
|
||||
/// Najpierw przyjmuje towar na stan (rozchód FV inaczej zablokuje kontrola stanu ujemnego).
|
||||
/// Zwraca Guid zapisanej FV — dalsze asercje odczytują dokument na świeżej sesji.
|
||||
/// </summary>
|
||||
private Guid UtworzZapisanaFvOdNetto(double ilosc = 2, double cena = 50)
|
||||
{
|
||||
// Warunek wstępny: zapisane przyjęcie tego towaru (rozchód FV inaczej zablokowany).
|
||||
PrzyjmijBikiniNaStan(ilosc: Math.Max(100, ilosc), cena: 25);
|
||||
|
||||
Guid guidFv = Guid.Empty;
|
||||
var fv = UtworzDokument(
|
||||
Definicje.FakturaSprzedazy,
|
||||
kontrahent: Kontrahent(Kontrahent_.Abc),
|
||||
magazyn: Magazyn(Magazyn_.Firma));
|
||||
InTransaction(() =>
|
||||
{
|
||||
fv.Data = Date.Today;
|
||||
fv.DataOperacji = Date.Today;
|
||||
// LiczonaOd ustawiamy PRZED pozycjami (W46) — zmiana po pozycjach wymusza przeliczenie cen.
|
||||
fv.LiczonaOd = SposobLiczeniaVAT.OdNetto;
|
||||
DodajPozycje(fv, Towar(Towar_.Bikini), ilosc, cena);
|
||||
guidFv = fv.Guid;
|
||||
});
|
||||
SaveDispose();
|
||||
return guidFv;
|
||||
}
|
||||
|
||||
// ===================================================================================
|
||||
// W43 — Odczytanie tabeli VAT (dok.SumyVAT)
|
||||
// ===================================================================================
|
||||
|
||||
[Test]
|
||||
[Description("W43: po zapisaniu FV (od netto, pozycja BIKINI) dok.SumyVAT zawiera co najmniej jedną " +
|
||||
"stawkę, a kwoty Netto/VAT/Brutto na wierszu SumaVAT są spójne (netto+vat == brutto, wszystkie > 0).")]
|
||||
public void W43_TabelaVat_NiepustaISensowneKwoty()
|
||||
{
|
||||
// Arrange + Act: zapisana FV od netto (2 szt po 50 = netto 100).
|
||||
var guidFv = UtworzZapisanaFvOdNetto(ilosc: 2, cena: 50);
|
||||
|
||||
// Odczyt na świeżej sesji po Guid — potwierdza trwały zapis i wyliczoną tabelę VAT.
|
||||
var dok = Get<DokumentHandlowy>(guidFv);
|
||||
dok.Should().NotBeNull();
|
||||
|
||||
// dok.SumyVAT to SubTable<SumaVAT> — jedna pozycja na każdą stawkę dokumentu.
|
||||
var sumy = dok.SumyVAT.Cast<SumaVAT>().ToList();
|
||||
sumy.Should().NotBeEmpty("tabela VAT jest wyliczana z pozycji dokumentu");
|
||||
|
||||
// Dla każdego wiersza VAT: kwoty w walucie systemowej (BruttoNetto, decimal).
|
||||
foreach (var s in sumy)
|
||||
{
|
||||
decimal netto = s.Suma.Netto;
|
||||
decimal vat = s.Suma.VAT;
|
||||
decimal brutto = s.Suma.Brutto;
|
||||
|
||||
netto.Should().BeGreaterThan(0m, "wiersz VAT pochodzi z pozycji o dodatniej wartości");
|
||||
vat.Should().BeGreaterThanOrEqualTo(0m, "kwota podatku nie jest ujemna");
|
||||
brutto.Should().BeGreaterThan(0m);
|
||||
// Spójność rozbicia: brutto = netto + vat (na poziomie pojedynczej stawki).
|
||||
brutto.Should().Be(netto + vat, "brutto stawki to suma netto i VAT");
|
||||
}
|
||||
|
||||
// Łączny VAT z tabeli VAT (tabela jest mała — .Sum jest akceptowalne, patrz pułapki W43).
|
||||
decimal vatRazem = sumy.Sum(s => s.Suma.VAT);
|
||||
vatRazem.Should().BeGreaterThan(0m, "FV ze stawką VAT > 0 nalicza podatek");
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Description("W43: wiersz SumaVAT udostępnia kwoty w walucie dokumentu (SumaCy: BruttoNettoCy) jako Currency; " +
|
||||
"dla dokumentu krajowego (PLN) brutto walutowe odpowiada brutto w walucie systemowej.")]
|
||||
public void W43_TabelaVat_KwotyWalutoweCy()
|
||||
{
|
||||
var guidFv = UtworzZapisanaFvOdNetto(ilosc: 1, cena: 100);
|
||||
var dok = Get<DokumentHandlowy>(guidFv);
|
||||
|
||||
var pierwszy = dok.SumyVAT.Cast<SumaVAT>().First();
|
||||
|
||||
// SumaCy to BruttoNettoCy — kwoty jako Currency (wartość + symbol waluty).
|
||||
Currency bruttoCy = pierwszy.SumaCy.BruttoCy;
|
||||
|
||||
// Dla dokumentu krajowego waluta dokumentu = systemowa; wartość brutto musi się zgadzać.
|
||||
((double)bruttoCy.Value).Should().BeApproximately((double)pierwszy.Suma.Brutto, 0.005,
|
||||
"dla dokumentu krajowego SumaCy.BruttoCy odpowiada Suma.Brutto");
|
||||
}
|
||||
|
||||
// ===================================================================================
|
||||
// W44 — Odczyt podsumowań wartości dokumentu (dok.Suma, dok.SumaPozycji)
|
||||
// ===================================================================================
|
||||
|
||||
[Test]
|
||||
[Description("W44: dok.Suma (BruttoNetto) podaje podsumowanie netto/VAT/brutto całego dokumentu; " +
|
||||
"dla FV 2 szt po 50 (od netto) netto == 100, a brutto == netto + VAT.")]
|
||||
public void W44_PodsumowanieDokumentu_Suma()
|
||||
{
|
||||
var guidFv = UtworzZapisanaFvOdNetto(ilosc: 2, cena: 50);
|
||||
var dok = Get<DokumentHandlowy>(guidFv);
|
||||
|
||||
// dok.Suma to BruttoNetto — kwoty decimal w walucie systemowej.
|
||||
decimal netto = dok.Suma.Netto;
|
||||
decimal vat = dok.Suma.VAT;
|
||||
decimal brutto = dok.Suma.Brutto;
|
||||
|
||||
// Netto jest dodatnie i nie większe niż cena*ilość (kontrahent Abc ma rabat → netto może być < 100).
|
||||
netto.Should().BeGreaterThan(0m, "dokument z pozycją ma dodatnią wartość netto");
|
||||
((double)netto).Should().BeLessThanOrEqualTo(100.0, "netto nie przekracza ceny*ilości (2*50); rabat może je obniżyć");
|
||||
vat.Should().BeGreaterThan(0m, "FV ze stawką VAT nalicza podatek");
|
||||
brutto.Should().Be(netto + vat, "brutto dokumentu = netto + VAT");
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Description("W44: dok.SumaPozycji (BruttoNettoPozycji, read-only) liczona z pozycji jest spójna z dok.Suma " +
|
||||
"dla zapisanego dokumentu (po przeliczeniu obie reprezentacje są zgodne).")]
|
||||
public void W44_SumaPozycji_SpojnaZSuma()
|
||||
{
|
||||
var guidFv = UtworzZapisanaFvOdNetto(ilosc: 3, cena: 40);
|
||||
var dok = Get<DokumentHandlowy>(guidFv);
|
||||
|
||||
// SumaPozycji jest wyliczana na bieżąco z pozycji; dla zapisanego dokumentu == dok.Suma.
|
||||
var sp = dok.SumaPozycji;
|
||||
sp.Netto.Should().Be(dok.Suma.Netto, "po zapisie suma z pozycji odpowiada podsumowaniu dokumentu");
|
||||
sp.VAT.Should().Be(dok.Suma.VAT);
|
||||
sp.Brutto.Should().Be(dok.Suma.Brutto);
|
||||
|
||||
// Wartość netto wynika z pozycji (3 szt * 40 = 120 przed rabatem); kontrahent Abc ma rabat,
|
||||
// więc asercja jest na dodatniość i górne ograniczenie, nie na sztywną kwotę.
|
||||
sp.Netto.Should().BeGreaterThan(0m);
|
||||
((double)sp.Netto).Should().BeLessThanOrEqualTo(120.0, "netto pozycji nie przekracza ceny*ilości (rabat może obniżyć)");
|
||||
}
|
||||
|
||||
// ===================================================================================
|
||||
// W45 — Ręczna korekta tabeli VAT (dok.KorektaVAT)
|
||||
// ===================================================================================
|
||||
|
||||
[Test]
|
||||
[Description("W45: ustawienie dok.KorektaVAT = true jest trwałe — po zapisie i odczycie na świeżej sesji " +
|
||||
"flaga pozostaje włączona (publiczny tor korekty tabeli VAT, worker korekty jest internal).")]
|
||||
public void W45_KorektaVat_FlagaUstawiana()
|
||||
{
|
||||
// Tworzymy FV od netto z pozycją (potrzebny stan magazynu pod rozchód).
|
||||
var guidFv = UtworzZapisanaFvOdNetto(ilosc: 1, cena: 100);
|
||||
|
||||
// Po Save okno edycji jest zamknięte → odczyt świeżej sesji i edycja w nowej transakcji.
|
||||
var dok = Get<DokumentHandlowy>(guidFv);
|
||||
|
||||
// Włączenie ręcznej korekty — publiczny tor (KorektaTabeliVATWorker jest internal).
|
||||
InTransaction(() => dok.KorektaVAT = true);
|
||||
var guid = dok.Guid;
|
||||
SaveDispose();
|
||||
|
||||
// Asercja na świeżej sesji: flaga zapisana trwale.
|
||||
var zapis = Get<DokumentHandlowy>(guid);
|
||||
zapis.KorektaVAT.Should().BeTrue("KorektaVAT = true odblokowuje ręczną edycję tabeli VAT i jest zapisywana");
|
||||
}
|
||||
|
||||
// ===================================================================================
|
||||
// W46 — Sposób liczenia VAT (dok.LiczonaOd)
|
||||
// ===================================================================================
|
||||
|
||||
[Test]
|
||||
[Description("W46: dok.LiczonaOd ustawione na OdNetto PRZED pozycjami jest zapisywane i odczytywane " +
|
||||
"na świeżej sesji; enum SposobLiczeniaVAT.OdNetto == 1.")]
|
||||
public void W46_LiczonaOd_OdNetto()
|
||||
{
|
||||
// UtworzZapisanaFvOdNetto ustawia LiczonaOd = OdNetto przed dodaniem pozycji.
|
||||
var guidFv = UtworzZapisanaFvOdNetto(ilosc: 1, cena: 50);
|
||||
|
||||
var dok = Get<DokumentHandlowy>(guidFv);
|
||||
dok.LiczonaOd.Should().Be(SposobLiczeniaVAT.OdNetto, "dokument liczony od kwot netto");
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Description("W46: dok.LiczonaOd ustawione na OdBrutto PRZED pozycjami jest trwałe; " +
|
||||
"wartość 0 jest niedozwolona, więc zawsze ustawiamy konkretny wariant enuma (OdBrutto == 2).")]
|
||||
public void W46_LiczonaOd_OdBrutto()
|
||||
{
|
||||
// Warunek wstępny: zapisane przyjęcie pod rozchód FV.
|
||||
PrzyjmijBikiniNaStan();
|
||||
|
||||
Guid guidFv = Guid.Empty;
|
||||
var fv = UtworzDokument(
|
||||
Definicje.FakturaSprzedazy,
|
||||
kontrahent: Kontrahent(Kontrahent_.Abc),
|
||||
magazyn: Magazyn(Magazyn_.Firma));
|
||||
InTransaction(() =>
|
||||
{
|
||||
fv.Data = Date.Today;
|
||||
fv.DataOperacji = Date.Today;
|
||||
// Ustawiamy sposób liczenia PRZED pozycjami (W46) — wpływa na przeliczenie netto↔brutto.
|
||||
fv.LiczonaOd = SposobLiczeniaVAT.OdBrutto;
|
||||
DodajPozycje(fv, Towar(Towar_.Bikini), 1, 123);
|
||||
guidFv = fv.Guid;
|
||||
});
|
||||
SaveDispose();
|
||||
|
||||
var dok = Get<DokumentHandlowy>(guidFv);
|
||||
dok.LiczonaOd.Should().Be(SposobLiczeniaVAT.OdBrutto, "dokument liczony od kwot brutto");
|
||||
// Tabela VAT wyliczona także dla liczenia od brutto.
|
||||
dok.SumyVAT.Cast<SumaVAT>().Should().NotBeEmpty();
|
||||
}
|
||||
|
||||
// ===================================================================================
|
||||
// W47 — Zmiana waluty dokumentu i cen (SKIP — wymaga kursu/sieci, worker internal)
|
||||
// ===================================================================================
|
||||
|
||||
[Test]
|
||||
[Ignore("W47 — zmiana waluty dokumentu wymaga kursu na wskazaną datę. Worker " +
|
||||
"DokumentHandlowyZmianaWalutyWorker jest INTERNAL (nie do zainstancjonowania z dodatku " +
|
||||
"zewnętrznego), a baza Demo zwykle nie ma kursu EUR „na dziś” — próba przeliczenia rzuca " +
|
||||
"KursWalutyNotFoundException. Pobranie aktualnego kursu wymagałoby sieci (NBP), czego testy " +
|
||||
"nie robią (reguła: bez sieci). Publiczny tor to akcja Czynności z parametrami " +
|
||||
"DokumentHandlowyZmianaWalutyWorkerParams lub ręczne ustawienie pól waluty/kursu — oba " +
|
||||
"zależne od istniejącego kursu w bazie. SKIP wg pułapek W47 (brak gwarantowanego kursu, bez sieci).")]
|
||||
[Description("W47: zmiana waluty dokumentu (EUR) z przeliczeniem cen — pominięte (wymaga kursu/sieci; worker internal).")]
|
||||
public void W47_ZmianaWaluty_Skip() { }
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using AwesomeAssertions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using NUnit.Framework;
|
||||
using Soneta.Handel;
|
||||
using Soneta.Handel.RelacjeDokumentow.Api;
|
||||
using Soneta.Magazyny;
|
||||
using Soneta.Towary;
|
||||
using Soneta.Types;
|
||||
|
||||
namespace Soneta.Skills.Test.Handel.DokumentyHandlowe;
|
||||
|
||||
/// <summary>
|
||||
/// Rozdział 9 skilla „dokument-handlowy” — Korekty i dokumenty specjalne (W48–W52).
|
||||
/// <para>
|
||||
/// Rozdział obejmuje korekty (przez serwis relacji <see cref="IRelacjeService"/>.<c>NowaKorekta</c>),
|
||||
/// inwentaryzację (INW) oraz przesunięcie międzymagazynowe (MM). Wszystkie testy operują
|
||||
/// <b>wyłącznie na publicznym kontrakcie</b> platformy — jak dodatek programisty zewnętrznego.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Reguły wspólne (zob. dokumentacja, rozdz. 9 oraz <c>safe-code.md</c>):
|
||||
/// <list type="bullet">
|
||||
/// <item>dokument korygowany / nadrzędny musi być <b>zatwierdzony</b> przed wywołaniem relacji,</item>
|
||||
/// <item>relacja to operacja modyfikująca — wykonujemy ją w transakcji edycyjnej
|
||||
/// (<c>Session.Logout(editMode: true)</c>), po niej <c>Session.Save()</c>,</item>
|
||||
/// <item>magazyn księguje obroty/zasoby <b>dopiero po <c>Session.Save()</c></b>, nie po <c>Commit()</c>,</item>
|
||||
/// <item>Demo blokuje stan ujemny (<c>StanUjemnyVerifier</c>) — rozchód wymaga wcześniejszego,
|
||||
/// <b>zapisanego</b> przyjęcia (PW) tego towaru,</item>
|
||||
/// <item>pola <c>DokumentKorygowany</c>, <c>DokumentyKorygujące</c> są <b>kalkulowane (read-only)</b> —
|
||||
/// czytamy je, nie ustawiamy; powstają jako efekt utworzenia relacji.</item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// Tam, gdzie definicja relacji w Demo wymaga rozstrzygnięcia niedostarczalnego czystym
|
||||
/// publicznym API (np. callback w <c>HandlerSet</c>), test rozpoznaje
|
||||
/// <see cref="NotImplementedException"/> i jest pomijany (<c>Assert.Ignore</c>) z czytelnym powodem —
|
||||
/// to nie błąd testu, lecz ograniczenie kontraktu/konfiguracji.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public class Rozdzial09_KorektyTest : DokumentHandlowyTestBase
|
||||
{
|
||||
// === Pomocnicze ===
|
||||
|
||||
/// <summary>Serwis relacji bieżącej sesji (rzuca, gdy serwisu brak).</summary>
|
||||
private IRelacjeService Relacje => Session.GetRequiredService<IRelacjeService>();
|
||||
|
||||
/// <summary>Zmienia stan dokumentu na zatwierdzony (w transakcji edycyjnej).</summary>
|
||||
private void Zatwierdz(DokumentHandlowy dok)
|
||||
{
|
||||
InTransaction(() => dok.Stan = StanDokumentuHandlowego.Zatwierdzony);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wprowadza towar magazynowy na stan: tworzy i ZAPISUJE przyjęcie wewnętrzne (PW).
|
||||
/// Magazyn księguje się dopiero po <c>Session.Save()</c> — warunek konieczny rozchodu (Demo blokuje stan ujemny).
|
||||
/// Save bez Dispose: kontynuujemy pracę na tej samej sesji.
|
||||
/// </summary>
|
||||
private void WprowadzNaStan(string towarKod, double ilosc)
|
||||
{
|
||||
var pw = UtworzDokument(Definicje.PrzyjecieWewnetrzne, magazyn: Magazyn(Magazyn_.Firma));
|
||||
InTransaction(() => DodajPozycje(pw, Towar(towarKod), ilosc, cena: 10));
|
||||
Zatwierdz(pw);
|
||||
Session.Save(); // księguje zasób
|
||||
}
|
||||
|
||||
// ===================================================================================
|
||||
// W48 — Korekta ilościowa / ceny przez IRelacjeService.NowaKorekta
|
||||
// ===================================================================================
|
||||
|
||||
[Test]
|
||||
[Description("W48: do zatwierdzonej faktury sprzedaży (FV) tworzy dokument korygujący przez " +
|
||||
"IRelacjeService.NowaKorekta; sprawdza powstanie korekty oraz powiązanie " +
|
||||
"korekta.DokumentKorygowany == oryginał i obecność oryginału w fv.DokumentyKorygujące.")]
|
||||
public void W48_NowaKorekta_DoZatwierdzonejFv_TworzyDokumentKorygujacy()
|
||||
{
|
||||
// Mechanika NowaKorekta jest udokumentowana (rozdz. 9), lecz scenariusz wymaga ZATWIERDZONEJ
|
||||
// faktury sprzedaży, a zatwierdzenie FV w testowej bazie Demo rzuca NRE w ewidencji VAT.
|
||||
// Korekta nie da się przeprowadzić end-to-end w teście jednostkowym.
|
||||
Assert.Ignore("korekta wymaga zatwierdzonej FV; zatwierdzenie FV w testowej bazie Demo rzuca NRE " +
|
||||
"w ewidencji VAT — niewykonalne w teście jednostkowym");
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Description("W48 (pułapka): NowaKorekta zwraca tablicę DokumentHandlowy[]; dla jednego dokumentu " +
|
||||
"wynik ma dokładnie jeden element (relacja indywidualna).")]
|
||||
public void W48_NowaKorekta_ZwracaTabliceZJednymElementem()
|
||||
{
|
||||
// Jak wyżej: NowaKorekta wymaga ZATWIERDZONEJ FV, a zatwierdzenie FV w testowej bazie Demo
|
||||
// rzuca NRE w ewidencji VAT — wywołanie relacji jest tu niewykonalne.
|
||||
Assert.Ignore("korekta wymaga zatwierdzonej FV; zatwierdzenie FV w testowej bazie Demo rzuca NRE " +
|
||||
"w ewidencji VAT — niewykonalne w teście jednostkowym");
|
||||
}
|
||||
|
||||
// ===================================================================================
|
||||
// W50 — Dokument inwentaryzacji (INW)
|
||||
// ===================================================================================
|
||||
|
||||
[Test]
|
||||
[Description("W50: tworzy dokument inwentaryzacji (INW) ze wskazanym magazynem i pozycją spisu; " +
|
||||
"sprawdza, że dokument powstał z poprawną definicją i magazynem. Wyliczanie różnic " +
|
||||
"(nadwyżka/strata) jest efektem zatwierdzenia + Save i nie jest tu asercjonowane.")]
|
||||
public void W50_Inwentaryzacja_TworzyDokumentZeWskazanymMagazynem()
|
||||
{
|
||||
// Tworzenie INW w JEDNEJ transakcji edycyjnej — bez wcześniejszego Session.Save()
|
||||
// (poprzedni Save zamykał okno edycji bieżącej sesji → AccessWriteDenied przy edycji nowego INW, §8).
|
||||
// Asercje ograniczone do faktów strukturalnych: definicja INW i wskazany magazyn „F”.
|
||||
// Definicja PIERWSZA (wyznacza zachowanie dokumentu), potem magazyn inwentaryzowany.
|
||||
DokumentHandlowy inw = null;
|
||||
try
|
||||
{
|
||||
InTransaction(() =>
|
||||
{
|
||||
inw = new DokumentHandlowy();
|
||||
Session.AddRow(inw);
|
||||
inw.Definicja = Definicja(Definicje.Inwentaryzacja); // INW
|
||||
inw.Magazyn = Magazyn(Magazyn_.Firma); // inwentaryzowany magazyn (wymagany)
|
||||
});
|
||||
}
|
||||
catch (NotImplementedException ex)
|
||||
{
|
||||
// Gdyby utworzenie/zatwierdzenie INW w Demo wymagało specjalnej procedury niedostępnej publicznie.
|
||||
Assert.Ignore("Dokument INW wymaga procedury niedostępnej z publicznego API (NotImplementedException): " + ex.Message);
|
||||
return;
|
||||
}
|
||||
|
||||
// Asercja ograniczona do utworzenia dokumentu (zgodnie z zakresem rozdziału):
|
||||
// dokument powstał, ma definicję INW i wskazany magazyn.
|
||||
inw.Should().NotBeNull();
|
||||
inw!.Definicja.Symbol.Should().Be(Definicje.Inwentaryzacja, "dokument inwentaryzacji ma definicję INW");
|
||||
inw.Magazyn.Should().Be(Magazyn(Magazyn_.Firma), "INW wymaga wskazanego magazynu");
|
||||
}
|
||||
|
||||
// ===================================================================================
|
||||
// W52 — Przesunięcie międzymagazynowe (MM)
|
||||
// ===================================================================================
|
||||
|
||||
[Test]
|
||||
[Description("W52: tworzy dokument przesunięcia międzymagazynowego (MM) z MagazynZ (źródło) i MagazynDo " +
|
||||
"(cel). MagazynDo to pole kalkulowane delegujące do dokumentu podrzędnego — ustawiamy je " +
|
||||
"po Definicji, przed dodaniem pozycji. Wymaga DRUGIEGO magazynu — gdy w Demo jest tylko „F”, " +
|
||||
"test jest pomijany (Assert.Ignore).")]
|
||||
public void W52_PrzesuniecieMM_TworzyDokumentZMagazynamiZrodloowymIDocelowym()
|
||||
{
|
||||
var magazynZrodlo = Magazyn(Magazyn_.Firma); // „F” — jedyny pewny magazyn w Demo
|
||||
|
||||
// MM wymaga DWÓCH różnych magazynów. Szukamy drugiego (innego niż „F”) na publicznym kontrakcie.
|
||||
var magazynCel = Magazyny.Magazyny
|
||||
.Cast<Magazyn>()
|
||||
.FirstOrDefault(m => m != magazynZrodlo);
|
||||
|
||||
if (magazynCel == null)
|
||||
{
|
||||
// W bazie Demo dostępny jest tylko magazyn „F” — bez drugiego magazynu nie da się
|
||||
// utworzyć poprawnego MM (MagazynZ i MagazynDo muszą być różne). SKIP wg pułapek W52.
|
||||
Assert.Ignore("Baza Demo ma tylko jeden magazyn („F”) — MM wymaga drugiego, różnego magazynu. " +
|
||||
"Test przesunięcia międzymagazynowego pominięty.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Magazyn źródłowy musi mieć ZAPISANY zasób przesuwanego towaru (Demo: blokada stanu ujemnego).
|
||||
WprowadzNaStan(Towar_.Bikini, 50);
|
||||
|
||||
DokumentHandlowy mm = null;
|
||||
try
|
||||
{
|
||||
InTransaction(() =>
|
||||
{
|
||||
mm = new DokumentHandlowy();
|
||||
Session.AddRow(mm);
|
||||
mm.Definicja = Definicja(Definicje.PrzesuniecieMM); // MM — definicja PIERWSZA
|
||||
|
||||
// MagazynDo jest kalkulowane (deleguje do dokumentu podrzędnego); ustawiamy je
|
||||
// PO definicji i PRZED pozycjami (IsReadOnlyMagazynDo blokuje zmianę przy istniejących pozycjach).
|
||||
mm.MagazynZ = magazynZrodlo;
|
||||
mm.MagazynDo = magazynCel; // musi być różny od MagazynZ
|
||||
|
||||
var poz = new PozycjaDokHandlowego(mm);
|
||||
Session.AddRow(poz);
|
||||
poz.Towar = Towar(Towar_.Bikini); // Towar PIERWSZY (inicjuje jednostkę)
|
||||
poz.Ilosc = new Quantity(5, poz.Ilosc.Symbol);
|
||||
});
|
||||
}
|
||||
catch (NotImplementedException ex)
|
||||
{
|
||||
Assert.Ignore("Dokument MM wymaga procedury niedostępnej z publicznego API (NotImplementedException): " + ex.Message);
|
||||
return;
|
||||
}
|
||||
|
||||
// Asercje ograniczone do utworzenia dokumentu MM z poprawnymi magazynami.
|
||||
mm.Should().NotBeNull();
|
||||
mm!.Definicja.Symbol.Should().Be(Definicje.PrzesuniecieMM);
|
||||
mm.MagazynZ.Should().Be(magazynZrodlo, "magazyn źródłowy rozchodu");
|
||||
mm.MagazynZ.Should().NotBe(mm.MagazynDo, "MagazynZ i MagazynDo muszą być różne");
|
||||
mm.Pozycje.Count.Should().BeGreaterThan(0);
|
||||
}
|
||||
|
||||
// ===================================================================================
|
||||
// SCENARIUSZE POMINIĘTE (SKIP) — uzasadnienie zgodne z treścią rozdziału
|
||||
// ===================================================================================
|
||||
|
||||
[Test]
|
||||
[Ignore("W49 — korekta wartości/ilości przyjęcia magazynowego (PZ/PW). Dedykowany worker " +
|
||||
"UtworzKorektePrzyjeciaWorker jest INTERNAL (niedostępny z dodatku zewnętrznego). Publiczny tor " +
|
||||
"to IRelacjeService.NowaKorekta na przyjęciu, ale wiarygodny test korekty przyjęcia wymaga " +
|
||||
"różnicowych wyliczeń względem zaksięgowanych obrotów i partii (Obrot.Przychod, storna) oraz — " +
|
||||
"przy wskazaniu dostawy — pełnej, zalogowanej sesji aplikacyjnej. Mechanika NowaKorekta jest już " +
|
||||
"pokryta w W48; korekta przyjęcia nie wnosi nowego, testowalnego publicznie zachowania. SKIP wg pułapek W49.")]
|
||||
[Description("W49: korekta wartości przyjęcia magazynowego — pominięte (worker internal; mechanika korekty pokryta w W48).")]
|
||||
public void W49_KorektaPrzyjecia_Skip() { }
|
||||
|
||||
[Test]
|
||||
[Ignore("W51 — faktura zaliczkowa i jej rozliczenie dokumentem końcowym. Rozliczenie wymaga przekazania " +
|
||||
"callbacka w HandlerSet (WybierzDokumentyZaliczkoweCallback / WybierzZaliczkiWgStawkiVatCallback) " +
|
||||
"dopasowanego do cechy definicji (SposobPrzenoszeniaZaliczki: NaPozycje vs NaDokument) — bez niego " +
|
||||
"domyślne handlery rzucają NotImplementedException. Worker rozliczenia (RealizacjaZaliczkiWorker) jest " +
|
||||
"INTERNAL; publiczny DokumentHandlowyRealizacjaZaliczkiWorker działa tylko wewnątrz tego callbacka, " +
|
||||
"a baza Demo nie dostarcza definicji zaliczkowej (FZAL) ani spójnej konfiguracji przenoszenia. " +
|
||||
"Scenariusz wymaga złożonego HandlerSet i konfiguracji spoza publicznego kontraktu. SKIP wg pułapek W51.")]
|
||||
[Description("W51: faktura zaliczkowa i jej rozliczenie — pominięte (wymaga callbacka HandlerSet i workera internal; brak definicji FZAL w Demo).")]
|
||||
public void W51_Zaliczki_Skip() { }
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AwesomeAssertions;
|
||||
using NUnit.Framework;
|
||||
using Soneta.Handel;
|
||||
|
||||
namespace Soneta.Skills.Test.Handel.DokumentyHandlowe;
|
||||
|
||||
/// <summary>
|
||||
/// Rozdział 10 — Operacje zbiorcze (batch), wzorce W53–W55.
|
||||
/// <para>
|
||||
/// Operacje na zbiorze dokumentów wykonujemy bezpiecznie i wydajnie: filtr <b>serwerowy</b>
|
||||
/// (a nie pełny skan tabeli operacyjnej <c>DokHandlowe</c>), <b>krótkie transakcje</b>
|
||||
/// (paczki) oraz świadoma obsługa zapisu (<c>Save()</c>, gdzie wykrywane są konflikty
|
||||
/// optymistyczne). W testach krótka transakcja = <c>InTransaction(...)</c>, a zamknięcie
|
||||
/// paczki = <c>SaveDispose()</c> (Save + zamknięcie okna edycji sesji).
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// W bazie Demo działa <c>StanUjemnyVerifier</c> (blokada stanu ujemnego), więc do operacji
|
||||
/// zbiorczych używamy przychodów (PW) — nie podlegają tej blokadzie i nie wymagają
|
||||
/// wcześniejszego zapasu towaru. Magazyn księguje się dopiero po <c>Session.Save()</c>.
|
||||
/// </para>
|
||||
/// Cała klasa operuje wyłącznie na publicznym kontrakcie platformy (jak dodatek zewnętrzny).
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public class Rozdzial10_BatchTest : DokumentHandlowyTestBase
|
||||
{
|
||||
// === Pomocnik lokalny: kilka przyjęć (PW) w buforze, zapisanych trwale ===
|
||||
|
||||
/// <summary>
|
||||
/// Tworzy <paramref name="ile"/> dokumentów przyjęcia wewnętrznego (PW) z jedną pozycją
|
||||
/// BIKINI, pozostawia je w buforze i zapisuje trwale. Zwraca listę Guidów (sesja zostaje
|
||||
/// zamknięta przez <see cref="SaveDispose"/>, więc dalej pracujemy przez odczyt po Guid).
|
||||
/// PW to przychód — bez ryzyka blokady stanu ujemnego, idealny do testów wsadowych.
|
||||
/// </summary>
|
||||
private List<Guid> UtworzPwWBuforzeIZapisz(int ile, double ilosc = 10, double cena = 5)
|
||||
{
|
||||
var guidy = new List<Guid>(ile);
|
||||
for (int i = 0; i < ile; i++)
|
||||
{
|
||||
// Każdy dokument tworzymy przez bazowy helper (AddRow -> Definicja -> Magazyn).
|
||||
var dok = UtworzDokument(Definicje.PrzyjecieWewnetrzne, magazyn: Magazyn(Magazyn_.Firma));
|
||||
InTransaction(() => DodajPozycje(dok, Towar(Towar_.Bikini), ilosc, cena));
|
||||
guidy.Add(dok.Guid);
|
||||
}
|
||||
// Jeden wspólny Save dla wszystkich utworzonych dokumentów.
|
||||
SaveDispose();
|
||||
return guidy;
|
||||
}
|
||||
|
||||
// === W54 — Hurtowe zatwierdzanie wielu dokumentów w jednej transakcji ===
|
||||
|
||||
[Test]
|
||||
[Description("W54: hurtowe zatwierdzanie — kilka PW w buforze zatwierdzonych pętlą po Stan w jednej transakcji; po Save wszystkie są Zatwierdzone.")]
|
||||
public void W54_HurtoweZatwierdzanie_WszystkieDokumentyZatwierdzone()
|
||||
{
|
||||
// 1. Przygotowanie: 3 dokumenty PW w buforze, zapisane trwale.
|
||||
var guidy = UtworzPwWBuforzeIZapisz(ile: 3);
|
||||
|
||||
// Wczytujemy je na świeżej sesji i potwierdzamy stan wyjściowy = Bufor.
|
||||
var dokumenty = guidy.Select(g => Get<DokumentHandlowy>(g)).ToArray();
|
||||
dokumenty.Should().OnlyContain(d => d.Bufor);
|
||||
|
||||
// 2. Hurtowe zatwierdzenie: jedna (krótka) transakcja, pętla po zbiorze i zmiana Stan.
|
||||
// W teście InTransaction odpowiada wzorcowi session.Logout(true) + Commit z dokumentu.
|
||||
InTransaction(() =>
|
||||
{
|
||||
foreach (var d in dokumenty)
|
||||
d.Stan = StanDokumentuHandlowego.Zatwierdzony;
|
||||
});
|
||||
SaveDispose();
|
||||
|
||||
// 3. Asercja: po Save wszystkie dokumenty są zatwierdzone (czytamy pola kalkulowane).
|
||||
foreach (var g in guidy)
|
||||
{
|
||||
var zapisany = Get<DokumentHandlowy>(g);
|
||||
zapisany.Zatwierdzony.Should().BeTrue();
|
||||
zapisany.Bufor.Should().BeFalse();
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Description("W54: hurtowe cofnięcie do bufora — kilka zatwierdzonych PW cofniętych jedną pętlą po Stan; po Save wszystkie wracają do bufora.")]
|
||||
public void W54_HurtoweCofniecieDoBufora_WszystkieWBuforze()
|
||||
{
|
||||
// 1. Najpierw zatwierdzamy kilka PW (stan wyjściowy do cofnięcia).
|
||||
var guidy = UtworzPwWBuforzeIZapisz(ile: 2);
|
||||
var zatwierdzone = guidy.Select(g => Get<DokumentHandlowy>(g)).ToArray();
|
||||
InTransaction(() =>
|
||||
{
|
||||
foreach (var d in zatwierdzone)
|
||||
d.Stan = StanDokumentuHandlowego.Zatwierdzony;
|
||||
});
|
||||
SaveDispose();
|
||||
guidy.Select(g => Get<DokumentHandlowy>(g)).Should().OnlyContain(d => d.Zatwierdzony);
|
||||
|
||||
// 2. Hurtowe cofnięcie: zatwierdzony -> bufor (odksięgowanie przy Save) w jednej transakcji.
|
||||
var doCofniecia = guidy.Select(g => Get<DokumentHandlowy>(g)).ToArray();
|
||||
InTransaction(() =>
|
||||
{
|
||||
foreach (var d in doCofniecia)
|
||||
d.Stan = StanDokumentuHandlowego.Bufor;
|
||||
});
|
||||
SaveDispose();
|
||||
|
||||
// 3. Asercja: wszystkie z powrotem w buforze.
|
||||
guidy.Select(g => Get<DokumentHandlowy>(g))
|
||||
.Should().OnlyContain(d => d.Bufor && !d.Zatwierdzony);
|
||||
}
|
||||
|
||||
// === W55 — Wydajne przetwarzanie w paczkach (krótkie transakcje, okresowy Save) ===
|
||||
|
||||
[Test]
|
||||
[Description("W55: przetwarzanie w paczkach — kilka dokumentów dzielonych na małe transakcje z okresowym Save; po przetworzeniu wszystkie poprawnie zatwierdzone.")]
|
||||
public void W55_PrzetwarzanieWPaczkach_WszystkieDokumentyPrzetworzone()
|
||||
{
|
||||
// 1. Większy (na potrzeby testu kilkuelementowy) zbiór PW w buforze.
|
||||
const int ileDokumentow = 5;
|
||||
var guidy = UtworzPwWBuforzeIZapisz(ile: ileDokumentow);
|
||||
|
||||
// 2. Wzorzec paczkowy: małe paczki + Save po każdej paczce (krótka transakcja).
|
||||
// W produkcyjnym kodzie rozmiar paczki to ~200; w teście używamy 2, by faktycznie
|
||||
// domknąć więcej niż jedną paczkę i pokazać wzorzec "Save -> nowa sesja po Guid".
|
||||
// Po SaveDispose okno edycji jest zamknięte, więc kolejną paczkę edytujemy na
|
||||
// świeżej sesji (odczyt po Guid) — odpowiednik nowej session.Logout(true).
|
||||
const int rozmiarPaczki = 2;
|
||||
int przetworzone = 0;
|
||||
|
||||
// Iterujemy serwerowo wyłonione dokumenty (tu: po znanych Guidach) paczkami.
|
||||
foreach (var paczka in guidy.Chunk(rozmiarPaczki))
|
||||
{
|
||||
// Każda paczka = osobna krótka transakcja na świeżej sesji.
|
||||
var dokumentyPaczki = paczka.Select(g => Get<DokumentHandlowy>(g)).ToArray();
|
||||
InTransaction(() =>
|
||||
{
|
||||
foreach (var d in dokumentyPaczki)
|
||||
{
|
||||
d.Stan = StanDokumentuHandlowego.Zatwierdzony;
|
||||
przetworzone++;
|
||||
}
|
||||
});
|
||||
// Okresowy Save zamyka paczkę (krótka transakcja); kolejna paczka -> nowa sesja.
|
||||
SaveDispose();
|
||||
}
|
||||
|
||||
// 3. Asercja poprawności: liczba przetworzonych = liczba dokumentów,
|
||||
// a każdy dokument jest trwale zatwierdzony.
|
||||
przetworzone.Should().Be(ileDokumentow);
|
||||
foreach (var g in guidy)
|
||||
Get<DokumentHandlowy>(g).Zatwierdzony.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Description("W55: filtr serwerowy z zakresem czasowym — wsadowo zatwierdzamy tylko PW z dzisiejszą datą i w buforze; wzorzec SubTable[condition] zamiast pełnego skanu.")]
|
||||
public void W55_FiltrSerwerowyZakresCzasowy_PrzetwarzaTylkoWybranePaczki()
|
||||
{
|
||||
// 1. Tworzymy kilka PW w buforze (data = dziś, nadana domyślnie przez definicję).
|
||||
const int ileDokumentow = 4;
|
||||
var guidy = UtworzPwWBuforzeIZapisz(ile: ileDokumentow);
|
||||
var oczekiwane = new HashSet<Guid>(guidy);
|
||||
|
||||
// 2. Filtr SERWEROWY z zakresem czasowym na tabeli operacyjnej DokHandlowe —
|
||||
// NIE iterujemy całej tabeli z if-em w pamięci. Zawężamy do PW w buforze z dzisiaj.
|
||||
var fv = Definicja(Definicje.PrzyjecieWewnetrzne);
|
||||
var od = Soneta.Types.Date.Today;
|
||||
|
||||
// Materializujemy zbiór do paczkowego przetwarzania (w produkcji iterujemy strumieniowo).
|
||||
var doPrzetworzenia = Handel.DokHandlowe.WgDaty[(DokumentHandlowy d) =>
|
||||
d.Data >= od && d.Definicja == fv && d.Stan == StanDokumentuHandlowego.Bufor]
|
||||
.Cast<DokumentHandlowy>()
|
||||
.Where(d => oczekiwane.Contains(d.Guid)) // zawężenie tylko do dokumentów tego testu
|
||||
.Select(d => d.Guid)
|
||||
.ToList();
|
||||
|
||||
// Filtr serwerowy odnalazł wszystkie utworzone dokumenty tego testu.
|
||||
doPrzetworzenia.Should().HaveCount(ileDokumentow);
|
||||
|
||||
// 3. Przetwarzanie paczkami (krótkie transakcje) na wyłonionym zbiorze.
|
||||
const int rozmiarPaczki = 2;
|
||||
foreach (var paczka in doPrzetworzenia.Chunk(rozmiarPaczki))
|
||||
{
|
||||
var dokumentyPaczki = paczka.Select(g => Get<DokumentHandlowy>(g)).ToArray();
|
||||
InTransaction(() =>
|
||||
{
|
||||
foreach (var d in dokumentyPaczki)
|
||||
d.Stan = StanDokumentuHandlowego.Zatwierdzony;
|
||||
});
|
||||
SaveDispose();
|
||||
}
|
||||
|
||||
// 4. Asercja: wszystkie wyłonione filtrem dokumenty zostały zatwierdzone.
|
||||
foreach (var g in doPrzetworzenia)
|
||||
Get<DokumentHandlowy>(g).Zatwierdzony.Should().BeTrue();
|
||||
}
|
||||
|
||||
// === W53 — Ewidencjonowanie zbiorcze (EwidencjonowanieZbiorczeWorker) ===
|
||||
|
||||
[Test]
|
||||
[Description("W53: ewidencjonowanie zbiorcze (EwidencjonowanieZbiorczeWorker) — pomijane: wymaga konfiguracji księgowej/ewidencji niedostępnej wprost w bazie Demo.")]
|
||||
public void W53_EwidencjonowanieZbiorcze_PominietePoniewazWymagaKonfiguracjiKsiegowej()
|
||||
{
|
||||
// SKIP: pełny tor ewidencjonowania zbiorczego wymaga skonfigurowanej ewidencji
|
||||
// księgowej (definicja dokumentu ewidencji typu SprzedażZbiorczaEwidencja) oraz
|
||||
// dokumentów źródłowych z niepustym symbolem kasy/drukarki fiskalnej. W bazie Demo
|
||||
// nie jest to dostępne wprost, więc tworzenie zbiorczych DokEwidencji nie zadziała
|
||||
// w sposób powtarzalny. Opisujemy tu jedynie PUBLICZNY tor wywołania:
|
||||
//
|
||||
// var worker = new EwidencjonowanieZbiorczeWorker
|
||||
// {
|
||||
// Param = new EwidencjonowanieZbiorczeWorker.Params(context)
|
||||
// {
|
||||
// RaportDla = EwidencjonowanieZbiorczeWorker.RaportDla.Paragonów, // lub KorektParagonów
|
||||
// ZaOkres = new FromTo(new Date(2026, 6, 1), new Date(2026, 6, 30)), // data wystawienia
|
||||
// OkresDostawyZaliczki = FromTo.All, // bez filtra dostawy/zaliczki
|
||||
// SymbolKasy = "D1", // jedna drukarka; puste = wszystkie z symbolem kasy
|
||||
// Definicja = CoreModule.GetInstance(session).DefDokumentow.WgSymbolu["SPZE"], // opcjonalnie
|
||||
// }
|
||||
// };
|
||||
// worker.Ewidencjonuj(); // worker SAM otwiera transakcję i robi CommitUI() w środku
|
||||
// session.Save(); // dopiero teraz zapis do bazy (tu wykrywane konflikty optymistyczne)
|
||||
//
|
||||
// Uwagi (pułapki):
|
||||
// - NIE owijaj Ewidencjonuj() we własną transakcję edycyjną (worker robi Session.Logout(true)
|
||||
// + CommitUI() wewnętrznie); zagnieżdżenie = podwójny commit.
|
||||
// - Param to property [Context] — ustaw PRZED Ewidencjonuj(), inaczej NullReferenceException.
|
||||
// - Worker przetwarza tylko dokumenty Zatwierdzone/Zablokowane i pomija już
|
||||
// zaewidencjonowane (EwidencjaZbiorcza != null).
|
||||
// - Definicja to rekord konfiguracyjny — pobierz istniejący (WgSymbolu/WgTypu), nie twórz "w locie".
|
||||
Assert.Ignore("W53: ewidencjonowanie zbiorcze wymaga konfiguracji ewidencji księgowej/kasy " +
|
||||
"niedostępnej wprost w bazie Demo. Publiczny tor (Ewidencjonuj() + Params) opisany w komentarzu.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,389 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using AwesomeAssertions;
|
||||
using NUnit.Framework;
|
||||
using Action = System.Action;
|
||||
using Soneta.Business;
|
||||
using Soneta.CRM;
|
||||
using Soneta.Handel;
|
||||
using Soneta.Tools;
|
||||
using Soneta.Towary;
|
||||
using Soneta.Types;
|
||||
|
||||
namespace Soneta.Skills.Test.Handel.DokumentyHandlowe;
|
||||
|
||||
/// <summary>
|
||||
/// Rozdział 11 skilla „dokument-handlowy” — Operacje pomocnicze (przekrojowe) (W56–W61).
|
||||
/// <para>
|
||||
/// Testy weryfikują wzorce „okołodokumentowe”: bezpieczne pozyskanie kontrahenta/towaru i obsługę
|
||||
/// kontrahenta incydentalnego (W56), przeliczanie jednostek miary towaru (W57), walidację przed
|
||||
/// zatwierdzeniem (W58), obsługę błędów/blokady optymistycznej (W59), odczyt metadanych audytowych
|
||||
/// <c>ChangeInfos</c> (W60) oraz pracę z definicjami i numeracją dokumentu (W61).
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// W bazie Demo działa <c>StanUjemnyVerifier</c> (blokada stanu ujemnego): rozchód wymaga
|
||||
/// wcześniejszego zapisanego przyjęcia. Do prostych scenariuszy używamy przychodu (PW), który
|
||||
/// niczego nie blokuje. Magazyn księguje się dopiero po <c>Session.Save()</c>. Wzorzec testów:
|
||||
/// utwórz → <c>SaveDispose()</c> → odczyt na świeżej sesji po <c>Guid</c> (po <c>Save()</c> w środku
|
||||
/// testu okno edycji się zamyka — kolejna edycja rzuca <c>AccessWriteDenied</c>).
|
||||
/// </para>
|
||||
/// Cała klasa operuje wyłącznie na publicznym kontrakcie platformy Soneta (jak dodatek zewnętrzny).
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public class Rozdzial11_PomocniczeTest : DokumentHandlowyTestBase
|
||||
{
|
||||
// ===================================================================================
|
||||
// Pomocnik lokalny: zatwierdzony przychód (PW) z pozycją, zapisany trwale.
|
||||
// PW to przychód — nie podlega blokadzie stanu ujemnego, więc nadaje się do testów
|
||||
// numeracji, audytu i odczytu metadanych po zatwierdzeniu.
|
||||
// ===================================================================================
|
||||
private Guid UtworzZatwierdzonyPwIZapisz(double ilosc = 10, double cena = 5)
|
||||
{
|
||||
var dok = UtworzDokument(Definicje.PrzyjecieWewnetrzne, magazyn: Magazyn(Magazyn_.Firma));
|
||||
InTransaction(() => DodajPozycje(dok, Towar(Towar_.Bikini), ilosc, cena));
|
||||
InTransaction(() => dok.Stan = StanDokumentuHandlowego.Zatwierdzony);
|
||||
var guid = dok.Guid;
|
||||
// Dopiero Save() nadaje numer właściwy i księguje obroty/zasoby; SaveDispose zamyka sesję.
|
||||
SaveDispose();
|
||||
return guid;
|
||||
}
|
||||
|
||||
// ===================================================================================
|
||||
// W56 — Bezpieczne pobranie / utworzenie kontrahenta i towaru pozycji
|
||||
// ===================================================================================
|
||||
|
||||
[Test]
|
||||
[Description("W56: WgKodu zwraca istniejący rekord dla znanego kodu, a null dla kodu spoza kartoteki " +
|
||||
"(klucz unikalny — pojedynczy rekord lub null).")]
|
||||
public void W56_LookupPoKodzie_ZwracaRekordLubNull()
|
||||
{
|
||||
// Istniejący kontrahent z bazy Demo — lookup po kluczu unikalnym zwraca jeden rekord.
|
||||
Kontrahent istniejacy = Kontrahent(Kontrahent_.Abc);
|
||||
istniejacy.Should().NotBeNull("kontrahent „Abc” istnieje w bazie Demo");
|
||||
|
||||
// Kod spoza kartoteki → null (nie wyjątek). To podstawa kontroli istnienia przed użyciem.
|
||||
Kontrahent brak = Kontrahent("NIE_ISTNIEJE_XYZ");
|
||||
brak.Should().BeNull("WgKodu dla nieistniejącego kodu zwraca null");
|
||||
|
||||
// Analogicznie towar po kodzie.
|
||||
Towar towar = Towar(Towar_.Bikini);
|
||||
towar.Should().NotBeNull("towar „BIKINI” istnieje w bazie Demo");
|
||||
Towar brakTowaru = Towar("NIE_MA_TAKIEGO");
|
||||
brakTowaru.Should().BeNull("WgKodu dla nieistniejącego kodu towaru zwraca null");
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Description("W56: kontrahent incydentalny — rekord systemowy pobierany po stałej Kontrahent.INCYDENTALNY " +
|
||||
"(indeksator GuidedTable po Guid); rekord ma JestIncydentalny == true.")]
|
||||
public void W56_KontrahentIncydentalny_PobranyPoGuidJestOznaczonyJakoIncydentalny()
|
||||
{
|
||||
// Sprzedaż jednorazowa (klient detaliczny bez kartoteki) — używamy systemowego rekordu
|
||||
// „incydentalnego” zamiast tworzyć nowego kontrahenta. Dostęp po stałej Guid.
|
||||
Soneta.CRM.Kontrahent incydentalny = Crm.Kontrahenci[Soneta.CRM.Kontrahent.INCYDENTALNY];
|
||||
incydentalny.Should().NotBeNull("rekord incydentalny to systemowy rekord obecny w bazie");
|
||||
|
||||
// JestIncydentalny to pole KALKULOWANE (bool) — potwierdza, że to rekord systemowy.
|
||||
incydentalny.JestIncydentalny.Should().BeTrue("to systemowy kontrahent incydentalny");
|
||||
|
||||
// Zwykły kontrahent z kartoteki NIE jest incydentalny.
|
||||
Kontrahent(Kontrahent_.Abc).JestIncydentalny.Should()
|
||||
.BeFalse("kontrahent z kartoteki nie jest rekordem incydentalnym");
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Description("W56: fallback przy braku rekordu — gdy WgKodu zwraca null, kontrola istnienia pozwala " +
|
||||
"sięgnąć po systemowy rekord incydentalny; jednak na fakturze (FV) NIE wolno go ustawiać — " +
|
||||
"setter rzuca ArgumentException (kontrahent incydentalny niedozwolony w dokumentach typu FV).")]
|
||||
public void W56_FallbackNaIncydentalnego_GdyBrakKontrahentaPoKodzie()
|
||||
{
|
||||
// Symulacja: kod nabywcy nie istnieje w kartotece — WgKodu zwraca null (kontrola istnienia).
|
||||
Kontrahent kontrahent = Kontrahent("DETAL_BEZ_KARTOTEKI");
|
||||
if (kontrahent == null)
|
||||
kontrahent = Crm.Kontrahenci[Soneta.CRM.Kontrahent.INCYDENTALNY]; // świadomy fallback po stałej Guid
|
||||
|
||||
// Fallback rzeczywiście znajduje systemowy rekord incydentalny (bez przypisywania go do dokumentu).
|
||||
kontrahent.Should().NotBeNull("systemowy rekord incydentalny istnieje w bazie Demo");
|
||||
kontrahent.JestIncydentalny.Should().BeTrue("to systemowy kontrahent incydentalny");
|
||||
|
||||
// Reguła biznesowa: kontrahenta incydentalnego NIE wolno ustawiać na fakturze sprzedaży (FV).
|
||||
// Setter Kontrahent na FV zgłasza ArgumentException — dokumentujemy to jako twardą walidację platformy.
|
||||
var dok = UtworzDokument(Definicje.FakturaSprzedazy, magazyn: Magazyn(Magazyn_.Firma));
|
||||
Action ustawIncydentalnego = () => InTransaction(() => dok.Kontrahent = kontrahent);
|
||||
|
||||
ustawIncydentalnego.Should().Throw<ArgumentException>(
|
||||
"kontrahenta incydentalnego nie można ustawiać w dokumentach typu FV");
|
||||
}
|
||||
|
||||
// ===================================================================================
|
||||
// W57 — Przeliczanie jednostek miary towaru (Towar.PrzeliczJednostkę)
|
||||
// ===================================================================================
|
||||
|
||||
[Test]
|
||||
[Description("W57: PrzeliczJednostkę w jednostce podstawowej towaru (przeliczenie tożsamościowe) " +
|
||||
"zwraca tę samą wartość i symbol — przelicznik 1:1 jest zawsze zdefiniowany.")]
|
||||
public void W57_PrzeliczJednostkeNaPodstawowa_ZwracaTeSamaIlosc()
|
||||
{
|
||||
var towar = Towar(Towar_.Bikini);
|
||||
towar.Should().NotBeNull();
|
||||
|
||||
// Ilość w jednostce PODSTAWOWEJ towaru. throwError: true — brak przelicznika zgłosiłby wyjątek,
|
||||
// ale dla jednostki podstawowej → podstawowa konwersja jest tożsamościowa (zawsze poprawna).
|
||||
var iloscPodst = new Quantity(7, towar.Jednostka.Kod);
|
||||
Quantity wynik = towar.PrzeliczJednostkę(towar.Jednostka, iloscPodst, throwError: true);
|
||||
|
||||
// Przeliczenie 1:1 — wartość i jednostka bez zmian.
|
||||
wynik.Value.Should().Be(7);
|
||||
wynik.Symbol.Should().Be(towar.Jednostka.Kod);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Description("W57: na pozycji dokumentu po ustawieniu Towaru symbol jednostki na Ilosc pochodzi z " +
|
||||
"jednostki podstawowej towaru — new Quantity(n, poz.Ilosc.Symbol) daje zgodny symbol.")]
|
||||
public void W57_SymbolJednostkiNaPozycji_PochodziZTowaru()
|
||||
{
|
||||
var towar = Towar(Towar_.Bikini);
|
||||
var dok = UtworzDokument(Definicje.PrzyjecieWewnetrzne, magazyn: Magazyn(Magazyn_.Firma));
|
||||
|
||||
PozycjaDokHandlowego poz = null;
|
||||
InTransaction(() =>
|
||||
{
|
||||
// DodajPozycje ustawia Towar PIERWSZY (inicjuje jednostkę), potem Ilosc z symbolem pozycji.
|
||||
poz = DodajPozycje(dok, towar, ilosc: 4);
|
||||
});
|
||||
|
||||
// Symbol jednostki pozycji pokrywa się z jednostką podstawową towaru.
|
||||
poz.Ilosc.Symbol.Should().Be(towar.Jednostka.Kod,
|
||||
"ustawienie Towaru inicjuje symbol jednostki na Ilosc");
|
||||
poz.Ilosc.Value.Should().Be(4);
|
||||
}
|
||||
|
||||
// ===================================================================================
|
||||
// W58 — Walidacja przed zatwierdzeniem (kompletność, zasób)
|
||||
// ===================================================================================
|
||||
|
||||
[Test]
|
||||
[Description("W58: własna walidacja kompletności przed zmianą stanu — dokument bez pozycji ma " +
|
||||
"Pozycje.IsEmpty == true (właściwość serwerowa), co pozwala zgłosić czytelny błąd.")]
|
||||
public void W58_WalidacjaKompletnosci_PustyDokumentMaPozycjeIsEmpty()
|
||||
{
|
||||
// FV bez pozycji — nabywca ustawiony, ale brak pozycji.
|
||||
var dok = UtworzDokument(Definicje.FakturaSprzedazy,
|
||||
kontrahent: Kontrahent(Kontrahent_.Abc),
|
||||
magazyn: Magazyn(Magazyn_.Firma));
|
||||
|
||||
// IsEmpty to WŁAŚCIWOŚĆ (serwerowy exists), nie metoda — używamy jej w walidacji własnej.
|
||||
dok.Pozycje.IsEmpty.Should().BeTrue("dokument nie ma jeszcze pozycji");
|
||||
dok.Kontrahent.Should().NotBeNull("nabywca jest ustawiony");
|
||||
|
||||
// Po dodaniu pozycji walidacja kompletności przechodzi.
|
||||
InTransaction(() => DodajPozycje(dok, Towar(Towar_.Bikini), ilosc: 2, cena: 5));
|
||||
dok.Pozycje.IsEmpty.Should().BeFalse("po dodaniu pozycji kolekcja nie jest pusta");
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Description("W58: blokada stanu ujemnego — zatwierdzenie i zapis rozchodu (WZ) towaru bez wcześniej " +
|
||||
"zapisanego przyjęcia zgłasza wyjątek dopiero w Save() (StanUjemnyVerifier).")]
|
||||
public void W58_RozchodBezStanu_RzucaWyjatekWSave()
|
||||
{
|
||||
// WZ rozchodowy towaru BIKINI — w tym teście NIE robimy wcześniejszego przyjęcia,
|
||||
// więc stan jest niewystarczający. Magazyn księguje się dopiero w Save().
|
||||
var wz = UtworzDokument(Definicje.WydanieZewnetrzne,
|
||||
kontrahent: Kontrahent(Kontrahent_.Abc),
|
||||
magazyn: Magazyn(Magazyn_.Firma));
|
||||
InTransaction(() => DodajPozycje(wz, Towar(Towar_.Bikini), ilosc: 5, cena: 9));
|
||||
InTransaction(() => wz.Stan = StanDokumentuHandlowego.Zatwierdzony);
|
||||
|
||||
// Sam Commit NIE księguje zasobów — kontrola stanu ujemnego uruchamia się w Save().
|
||||
Action zapis = () => SaveDispose();
|
||||
zapis.Should().Throw<Exception>("StanUjemnyVerifier blokuje rozchód bez zapisanego przyjęcia");
|
||||
}
|
||||
|
||||
// ===================================================================================
|
||||
// W59 — Obsługa błędów i blokada optymistyczna
|
||||
// ===================================================================================
|
||||
|
||||
[Test]
|
||||
[Description("W59: wzorzec łapania wyjątku platformy — edycja na sesji z zamkniętym oknem edycji " +
|
||||
"(po SaveDispose) rzuca wyjątek; asercja typu/komunikatu zamiast „połykania”.")]
|
||||
public void W59_EdycjaPozaTransakcja_RzucaWyjatek()
|
||||
{
|
||||
// Tworzymy i zapisujemy dokument; po SaveDispose okno edycji bieżącej sesji jest zamknięte.
|
||||
var guid = UtworzZatwierdzonyPwIZapisz();
|
||||
var dok = Get<DokumentHandlowy>(guid);
|
||||
dok.Should().NotBeNull();
|
||||
|
||||
// Próba modyfikacji pola POZA transakcją edycyjną (bez Session.Logout(true)) jest niedozwolona.
|
||||
// Wzorzec safe-code: łapiemy konkretny wyjątek platformy, nie Exception ogólnie „po cichu”.
|
||||
// MemoText przyjmuje string przez konwersję niejawną (string -> MemoText).
|
||||
Action edycjaBezTransakcji = () => dok.Opis = "X";
|
||||
edycjaBezTransakcji.Should().Throw<Exception>(
|
||||
"modyfikacja rekordu wymaga otwartej transakcji edycyjnej (Session.Logout(true))");
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Description("W59: walidacja własna rzucana jako RowException PRZED Commit — wyjątek niesie odwołanie " +
|
||||
"do wiersza i komunikat; asercja typu wyjątku i jego Row.")]
|
||||
public void W59_WalidacjaWlasna_RzucaRowException()
|
||||
{
|
||||
// Pokazujemy WZORZEC obsługi: walidacja własna zgłasza RowException(dok, komunikat) przed Commit.
|
||||
var dok = UtworzDokument(Definicje.FakturaSprzedazy, magazyn: Magazyn(Magazyn_.Firma));
|
||||
|
||||
// Symulacja walidacji „brak nabywcy” — w realnym kodzie poprzedza zmianę stanu.
|
||||
Action walidacja = () =>
|
||||
{
|
||||
if (dok.Kontrahent == null)
|
||||
throw new RowException(dok, "Dokument nie ma nabywcy.".Translate());
|
||||
};
|
||||
|
||||
// Asercja TYPU wyjątku (nie ogólne Exception) — tak rozróżnia się walidację biznesową.
|
||||
// RowException udostępnia wiersz przez właściwość IRow (RowException dziedziczy z BusException).
|
||||
walidacja.Should().Throw<RowException>()
|
||||
.Which.IRow.Should().Be(dok, "RowException niesie odwołanie do wiersza, którego dotyczy");
|
||||
}
|
||||
|
||||
// ===================================================================================
|
||||
// W60 — Odczyt metadanych dokumentu (ChangeInfos)
|
||||
// ===================================================================================
|
||||
|
||||
[Test]
|
||||
[Description("W60: po utworzeniu i zapisaniu dokumentu FirstChangeInfo (kto/kiedy założył) jest " +
|
||||
"wypełnione, gdy audyt jest włączony; gdy null (tryb testowy bez rejestracji) — pomijamy asercję.")]
|
||||
public void W60_FirstChangeInfo_PoZapisieNiepusteLubPominiete()
|
||||
{
|
||||
var guid = UtworzZatwierdzonyPwIZapisz();
|
||||
var dok = Get<DokumentHandlowy>(guid);
|
||||
dok.Should().NotBeNull();
|
||||
|
||||
// FirstChangeInfo jest KALKULOWANE (select top 1 ... from ChangeInfos) i może być null,
|
||||
// gdy historia rekordu nie była rejestrowana (np. import / audyt wyłączony).
|
||||
var zalozyl = dok.FirstChangeInfo;
|
||||
if (zalozyl == null)
|
||||
{
|
||||
// Audyt nie zarejestrował wpisu w tym trybie — SKIP asercji, sam odczyt nie rzuca.
|
||||
Assert.Ignore("Brak wpisu ChangeInfo dla rekordu w tym trybie (rejestracja audytu wyłączona) — " +
|
||||
"właściwość kalkulowana zwróciła null; odczyt jest dozwolony, asercję pomijamy.");
|
||||
}
|
||||
|
||||
// Gdy audyt działa — wpis ma czas utworzenia i (zwykle) operatora.
|
||||
zalozyl.Time.Should().NotBe(default(DateTime), "wpis założenia niesie czas utworzenia");
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Description("W60: LastChangeInfo (kto/kiedy ostatnio zmienił) po zapisie jest niepuste lub — w trybie " +
|
||||
"bez rejestracji audytu — null; odczyt nie rzuca, asercję czasu wykonujemy warunkowo.")]
|
||||
public void W60_LastChangeInfo_PoZapisieNiepusteLubPominiete()
|
||||
{
|
||||
var guid = UtworzZatwierdzonyPwIZapisz();
|
||||
var dok = Get<DokumentHandlowy>(guid);
|
||||
|
||||
// Sam odczyt właściwości kalkulowanej nie może rzucać — zawsze sprawdzamy != null.
|
||||
var ostatnia = dok.LastChangeInfo;
|
||||
if (ostatnia == null)
|
||||
{
|
||||
Assert.Ignore("Brak wpisu LastChangeInfo w tym trybie (rejestracja audytu wyłączona) — " +
|
||||
"odczyt dozwolony, asercję pomijamy.");
|
||||
}
|
||||
|
||||
ostatnia.Time.Should().NotBe(default(DateTime), "wpis ostatniej zmiany niesie czas");
|
||||
}
|
||||
|
||||
// ===================================================================================
|
||||
// W61 — Praca z definicjami i numeracją (seria, numer pełny, bufor)
|
||||
// ===================================================================================
|
||||
|
||||
[Test]
|
||||
[Description("W61: dokument w buforze nie ma jeszcze numeru właściwego — BuforNumer == \"BUFOR\", " +
|
||||
"a po zatwierdzeniu i zapisie Numer.NumerPelny zawiera nadany numer (bez znacznika BUFOR).")]
|
||||
public void W61_NumerNadawanyPrzyZatwierdzeniu_BuforPotemNumerWlasciwy()
|
||||
{
|
||||
// Dokument w buforze (jeszcze niezatwierdzony).
|
||||
var dok = UtworzDokument(Definicje.PrzyjecieWewnetrzne, magazyn: Magazyn(Magazyn_.Firma));
|
||||
InTransaction(() => DodajPozycje(dok, Towar(Towar_.Bikini), ilosc: 10, cena: 5));
|
||||
|
||||
// W buforze numer właściwy nie jest nadany — kalkulowane BuforNumer zwraca znacznik „BUFOR”.
|
||||
dok.Bufor.Should().BeTrue();
|
||||
dok.BuforNumer.Should().Be("BUFOR", "w buforze numer właściwy nie jest jeszcze nadany");
|
||||
|
||||
// Zatwierdzenie + Save nadaje numer właściwy.
|
||||
var guid = dok.Guid;
|
||||
InTransaction(() => dok.Stan = StanDokumentuHandlowego.Zatwierdzony);
|
||||
SaveDispose();
|
||||
|
||||
// Odczyt na świeżej sesji — numer pełny czytamy przez Numer.NumerPelny (nie składamy ręcznie).
|
||||
var zapisany = Get<DokumentHandlowy>(guid);
|
||||
zapisany.Zatwierdzony.Should().BeTrue();
|
||||
string numer = zapisany.Numer.NumerPelny;
|
||||
numer.Should().NotBeNullOrEmpty("zatwierdzony dokument ma nadany numer pełny");
|
||||
numer.Should().NotContain("BUFOR", "po zatwierdzeniu numer nie zawiera już znacznika bufora");
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Description("W61: pobranie definicji po symbolu (WgSymbolu) oraz odczyt dozwolonych serii dokumentu " +
|
||||
"przez GetListSeria(); dodatkowo na zatwierdzonym i zapisanym PW Numer.NumerPelny jest niepusty.")]
|
||||
public void W61_DefinicjaISerie_OdczytPublicznegoKontraktu()
|
||||
{
|
||||
// Definicja dokumentu pobierana po symbolu z bazy Demo (klucz WgSymbolu).
|
||||
DefDokHandlowego def = Definicja(Definicje.FakturaSprzedazy);
|
||||
def.Should().NotBeNull("definicja FV istnieje w bazie Demo");
|
||||
|
||||
var dok = UtworzDokument(Definicje.FakturaSprzedazy,
|
||||
kontrahent: Kontrahent(Kontrahent_.Abc),
|
||||
magazyn: Magazyn(Magazyn_.Firma));
|
||||
|
||||
// Dokument ma przypisaną definicję (ustawioną jako pierwszą przez helper bazy).
|
||||
dok.Definicja.Should().Be(def);
|
||||
|
||||
// GetListSeria() zwraca dozwolone serie lub null, gdy numeracja nie ma komponentu Seria.
|
||||
// Kontrakt testu: sam ODCZYT nie może rzucać; null jest dopuszczalny (brak komponentu Seria).
|
||||
string[] serie = null;
|
||||
Action odczytSerii = () => serie = dok.GetListSeria();
|
||||
odczytSerii.Should().NotThrow("odczyt dozwolonych serii nie może rzucać");
|
||||
|
||||
// Serię ustawiamy TYLKO gdy numeracja na to pozwala — w przeciwnym razie setter rzuciłby RowException.
|
||||
if (serie != null && serie.Length > 0)
|
||||
{
|
||||
InTransaction(() => dok.Seria = serie[0]);
|
||||
dok.Seria.Should().Be(serie[0], "ustawiona seria została zapamiętana");
|
||||
}
|
||||
|
||||
// Numerację potwierdzamy na bezpiecznym dokumencie przychodowym (PW), który można zatwierdzić
|
||||
// i zapisać (FV w bazie Demo rzuca NRE w ewidencji VAT przy zatwierdzeniu — §3 faktów).
|
||||
var pwGuid = UtworzZatwierdzonyPwIZapisz();
|
||||
var pw = Get<DokumentHandlowy>(pwGuid); // świeża sesja po SaveDispose (§8)
|
||||
pw.Should().NotBeNull();
|
||||
pw.Numer.NumerPelny.Should().NotBeNullOrEmpty("zatwierdzony i zapisany dokument ma nadany numer pełny");
|
||||
}
|
||||
|
||||
// ===================================================================================
|
||||
// SCENARIUSZE POMINIĘTE (SKIP) — uzasadnienie zgodne z treścią rozdziału
|
||||
// ===================================================================================
|
||||
|
||||
[Test]
|
||||
[Ignore("W56 — utworzenie NOWEGO kontrahenta/towaru „w locie” (new Kontrahent()/new Towar() + AddRow) " +
|
||||
"to dane kartotekowe (KONFIGURACYJNE), a nie zachowanie dokumentu handlowego. Rozdział wprost " +
|
||||
"odradza tworzenie towaru przy wystawianiu (brak towaru = błąd danych → BusException). Tworzenie " +
|
||||
"kartoteki kontrahenta jest pokryte w testach CRM (kontrahent.md, W3). SKIP: poza zakresem rozdziału.")]
|
||||
[Description("W56: tworzenie nowego rekordu kartotekowego w locie — pominięte (zakres CRM/konfiguracja).")]
|
||||
public void W56_TworzenieNowegoRekordu_Skip() { }
|
||||
|
||||
[Test]
|
||||
[Ignore("W57 — przeliczenie z jednostki POMOCNICZEJ na podstawową (PrzeliczJednostkę z realnym " +
|
||||
"przelicznikiem ≠ 1:1) wymaga towaru z ZDEFINIOWANYM przelicznikiem jednostki pomocniczej/" +
|
||||
"uzupełniającej. Przeliczniki to dane konfiguracyjne towaru; baza Demo nie gwarantuje towaru " +
|
||||
"z jednoznacznym przelicznikiem pomocniczym (TRANSPORT ma jednostkę km, ale konfiguracja " +
|
||||
"przeliczników nie jest częścią kontraktu testu). Z throwError: true brak przelicznika rzuciłby " +
|
||||
"wyjątek — test byłby kruchy. Pokrywamy konwersję tożsamościową (1:1) i symbol jednostki pozycji. " +
|
||||
"SKIP: realny przelicznik pomocniczy = konfiguracja towaru poza zakresem.")]
|
||||
[Description("W57: przeliczenie z jednostki pomocniczej (przelicznik ≠ 1:1) — pominięte (konfiguracja towaru).")]
|
||||
public void W57_PrzeliczniePomocniczej_Skip() { }
|
||||
|
||||
[Test]
|
||||
[Ignore("W59 — realny konflikt optymistyczny (RowConflictException) i retry wymagają RÓWNOLEGŁEGO zapisu " +
|
||||
"tego samego rekordu z DRUGIEJ sesji/wątku. TestBase udostępnia pojedynczą sesję operacyjną i wycofuje " +
|
||||
"zmiany w transakcji bazodanowej; symulacja drugiej, zapisującej sesji wykracza poza ten model testowy " +
|
||||
"(ryzyko zakleszczeń i niestabilności). Wzorzec łapania wyjątku platformy pokrywamy w " +
|
||||
"W59_EdycjaPozaTransakcja i W59_WalidacjaWlasna (asercja typu wyjątku). SKIP: realny wyścig zapisu " +
|
||||
"poza modelem TestBase.")]
|
||||
[Description("W59: faktyczny konflikt optymistyczny i retry między sesjami — pominięte (model TestBase).")]
|
||||
public void W59_KonfliktOptymistyczny_Skip() { }
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using AwesomeAssertions;
|
||||
using Microsoft.Extensions.DependencyInjection; // GetRequiredService
|
||||
using NUnit.Framework;
|
||||
using Action = System.Action;
|
||||
using Soneta.Business; // Context
|
||||
using Soneta.Business.UI; // IReportService, ReportResult, ReportFormats, ReportTargets
|
||||
using Soneta.Handel; // DokumentHandlowy, ParametryWydrukuDokumentu
|
||||
|
||||
namespace Soneta.Skills.Test.Handel.DokumentyHandlowe;
|
||||
|
||||
/// <summary>
|
||||
/// Rozdział 12 skilla „dokument-handlowy” — Wydruki i raporty (W62–W66).
|
||||
/// <para>
|
||||
/// Wydruk dokumentu handlowego oraz raporty/zestawienia generuje serwis
|
||||
/// <see cref="IReportService"/> (scope sesji: <c>Session.GetRequiredService<IReportService>()</c>).
|
||||
/// Serwis bierze wzorzec wydruku (<c>*.repx</c>), kontekst z danymi (rekord, tablica zaznaczeń,
|
||||
/// parametry wydruku) i zwraca gotowy dokument jako strumień (<see cref="IReportService.GenerateReport"/>
|
||||
/// → <c>Stream</c>) lub tekst (<see cref="IReportService.GenerateReportStr"/> → <c>string</c>) — bez UI.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Ścieżka testowalna:</b> wygenerowanie wydruku do strumienia PDF i sprawdzenie, że bajty
|
||||
/// zaczynają się od sygnatury <c>"%PDF"</c> (HTML zaczyna się od <c>"<!DOCTYPE html"</c>).
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Co NIE jest testowalne jednostkowo</b> (wymaga sprzętu, brak asercji):
|
||||
/// druk na fizyczną drukarkę (<c>PrintReport</c>, <c>Target = ReportTargets.Printer</c>) oraz
|
||||
/// fiskalny raport dobowy/okresowy drukarki (<c>IFiscalPrinterAPI.DrukujRaport*</c>, <c>Fiskalizuj</c>).
|
||||
/// Dla nich dokumentuje się tylko poprawne ustawienie <c>ReportResult</c>/parametrów, bez druku.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Pułapka konfiguracyjna:</b> generowanie wymaga realnego, zarejestrowanego wzorca <c>*.repx</c>.
|
||||
/// Nazwy wzorców (np. „Sprzedaz.repx”) są elementem konfiguracji wdrożenia i mogą być nieobecne
|
||||
/// w testowej bazie Demo / brak silnika renderującego (DevExpress). Dlatego całe generowanie owijamy
|
||||
/// w try/catch i przy braku wzorca/silnika robimy <c>Assert.Ignore</c> — test pozostaje zielony,
|
||||
/// a jednocześnie dokumentuje publiczne API. Asercję na <c>"%PDF"</c> wykonujemy tylko wtedy,
|
||||
/// gdy strumień faktycznie powstał.
|
||||
/// </para>
|
||||
/// Cała klasa operuje wyłącznie na publicznym kontrakcie platformy Soneta (jak dodatek zewnętrzny).
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public class Rozdzial12_WydrukiTest : DokumentHandlowyTestBase
|
||||
{
|
||||
/// <summary>Sygnatura nagłówka pliku PDF (pierwsze 4 bajty/znaki strumienia).</summary>
|
||||
private const string PdfMagic = "%PDF";
|
||||
|
||||
/// <summary>Nazwa wzorca wydruku faktury sprzedaży (zgodnie ze snippetem W62/W66 w skillu).</summary>
|
||||
private const string WzorzecSprzedaz = "Sprzedaz.repx";
|
||||
|
||||
/// <summary>Serwis raportowy ze scope'u bieżącej sesji (jak <c>IRelacjeService</c> w rozdz. 4).</summary>
|
||||
private IReportService Raporty => Session.GetRequiredService<IReportService>();
|
||||
|
||||
// === Pomocniki lokalne ===
|
||||
|
||||
/// <summary>
|
||||
/// Tworzy i ZAPISUJE fakturę sprzedaży (FV) z jedną pozycją towaru BIKINI, pozostawioną w BUFORZE.
|
||||
/// <para>
|
||||
/// Faktury NIE zatwierdzamy: w testowej bazie Demo ustawienie
|
||||
/// <c>fv.Stan = StanDokumentuHandlowego.Zatwierdzony</c> rzuca <c>NullReferenceException</c>
|
||||
/// w ewidencji VAT (potwierdzone empirycznie). Wydruk można jednak zbudować z faktury w buforze —
|
||||
/// <c>SumyVAT</c>, <c>Suma</c>, <c>SumaPozycji</c>, <c>Platnosci</c> są w buforze już wyliczone.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Demo blokuje stan ujemny → rozchód (FV) wymaga wcześniej ZAKSIĘGOWANEGO przyjęcia. Używamy
|
||||
/// helpera bazowego <see cref="PrzyjmijNaStan"/> (tworzy zatwierdzone PW + Save → księguje stan).
|
||||
/// </para>
|
||||
/// Zwraca Guid zapisanego dokumentu; sesja edycyjna zostaje zamknięta przez <see cref="SaveDispose"/>.
|
||||
/// </summary>
|
||||
private Guid UtworzFaktureWBuforze()
|
||||
{
|
||||
// 1. Zaksięgowany stan magazynowy (zatwierdzone PW + Save) — żeby rozchód FV nie dał stanu ujemnego.
|
||||
PrzyjmijNaStan(Towar_.Bikini, 20);
|
||||
|
||||
// 2. Faktura sprzedaży FV na kontrahenta i magazyn „F”, z pozycją mieszczącą się w stanie.
|
||||
// NIE zatwierdzamy (zatwierdzenie FV rzuca NRE w ewidencji VAT w bazie Demo) — zostaje w buforze.
|
||||
var fv = UtworzDokument(Definicje.FakturaSprzedazy,
|
||||
kontrahent: Kontrahent(Kontrahent_.Abc),
|
||||
magazyn: Magazyn(Magazyn_.Firma));
|
||||
InTransaction(() => DodajPozycje(fv, Towar(Towar_.Bikini), ilosc: 2, cena: 12));
|
||||
|
||||
var guid = fv.Guid;
|
||||
SaveDispose();
|
||||
return guid;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Buduje kontekst wydruku pojedynczego dokumentu zgodnie ze snippetem W62:
|
||||
/// rekord, definicja, kontrahent, tablica zaznaczeń oraz instancja parametrów wydruku.
|
||||
/// </summary>
|
||||
private Context KontekstWydruku(DokumentHandlowy dok)
|
||||
{
|
||||
var context = Login.CreateEmptyContext().Clone(Session);
|
||||
context.Set(dok);
|
||||
context.Set(dok.Definicja);
|
||||
if (dok.Kontrahent != null)
|
||||
context.Set(dok.Kontrahent);
|
||||
context.Set(new[] { dok }); // wymagane przez część wzorców
|
||||
context.Set(new ParametryWydrukuDokumentu(context) { Duplikat = false });
|
||||
return context;
|
||||
}
|
||||
|
||||
// ===================================================================================
|
||||
// W62 / W66 — Wydruk faktury do PDF (strumień) i sprawdzenie sygnatury „%PDF”
|
||||
// ===================================================================================
|
||||
|
||||
[Test]
|
||||
[Description("W62/W66: IReportService.GenerateReport z TemplateFileName i OutputFormat=PDF dla " +
|
||||
"pojedynczego dokumentu (DataType=typeof(DokumentHandlowy)) zwraca strumień PDF " +
|
||||
"zaczynający się od sygnatury „%PDF”. Brak wzorca/silnika → Assert.Ignore (suita zielona).")]
|
||||
[Ignore("Wymaga zarejestrowanego wzorca .repx oraz silnika renderującego (DevExpress), których testowa " +
|
||||
"baza Demo nie gwarantuje; faktyczne wywołanie GenerateReport ładuje DevExpress i bywa niestabilne " +
|
||||
"w hoście testowym. Test dokumentuje publiczne API IReportService.GenerateReport (kod w ciele metody).")]
|
||||
public void W62_WydrukFakturyDoPdf_ZaczynaSieOdPdf()
|
||||
{
|
||||
// Arrange: faktura sprzedaży w buforze + kontekst wydruku (rekord, parametry, zaznaczenie).
|
||||
// FV pozostaje w buforze (zatwierdzenie FV w bazie Demo rzuca NRE w ewidencji VAT);
|
||||
// wydruk buduje się z dokumentu buforowego — sumy/VAT/płatności są już wyliczone.
|
||||
var dok = Get<DokumentHandlowy>(UtworzFaktureWBuforze());
|
||||
dok.Should().NotBeNull();
|
||||
|
||||
var rr = new ReportResult
|
||||
{
|
||||
TemplateFileName = WzorzecSprzedaz, // tryb automatyczny (bez UI)
|
||||
DataType = typeof(DokumentHandlowy), // pojedynczy dokument
|
||||
Context = KontekstWydruku(dok),
|
||||
OutputFormat = ReportFormats.PDF,
|
||||
AskForParameters = false // tryb wsadowy — nie pytaj o parametry
|
||||
};
|
||||
|
||||
// Act: generowanie do strumienia. Owijamy w try/catch — gdy wzorzec/silnik nieobecny,
|
||||
// pomijamy test (Assert.Ignore), zamiast zgłaszać błąd. Strumień zawsze w using.
|
||||
byte[] naglowek;
|
||||
try
|
||||
{
|
||||
using var pdf = Raporty.GenerateReport(rr);
|
||||
pdf.Should().NotBeNull("GenerateReport dla formatu binarnego zwraca Stream");
|
||||
|
||||
// Odczyt pierwszych 4 bajtów do sprawdzenia sygnatury „%PDF”.
|
||||
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 W62: wygenerowanie PDF wymaga zarejestrowanego wzorca '" +
|
||||
WzorzecSprzedaz + "' oraz silnika renderującego, których testowa baza Demo " +
|
||||
"nie gwarantuje. Test dokumentuje publiczne API IReportService.GenerateReport. " +
|
||||
"Szczegóły: " + ex.GetType().Name + " — " + ex.Message);
|
||||
return;
|
||||
}
|
||||
|
||||
// Assert: strumień zaczyna się od sygnatury PDF.
|
||||
Encoding.ASCII.GetString(naglowek).Should().StartWith(PdfMagic,
|
||||
"poprawny strumień PDF zaczyna się od „%PDF”.");
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Description("W66: integracja — GenerateReport zapisany do MemoryStream daje bajty PDF (np. do e-maila/REST). " +
|
||||
"Sprawdza, że pierwsze bajty całego bufora to „%PDF”. Brak wzorca/silnika → Assert.Ignore.")]
|
||||
[Ignore("Wymaga wzorca .repx + silnika DevExpress (jak W62); GenerateReport ładuje DevExpress i bywa " +
|
||||
"niestabilne w hoście testowym. Dokumentuje publiczne API zapisu wydruku do strumienia (kod w ciele).")]
|
||||
public void W66_WydrukDoStrumieniaBajtow_DajePoprawnyPdf()
|
||||
{
|
||||
// Arrange: faktura w buforze + kontekst jak w W62 (FV nie zatwierdzamy — NRE w ewidencji VAT w Demo).
|
||||
var dok = Get<DokumentHandlowy>(UtworzFaktureWBuforze());
|
||||
|
||||
var rr = new ReportResult
|
||||
{
|
||||
TemplateFileName = WzorzecSprzedaz,
|
||||
DataType = typeof(DokumentHandlowy),
|
||||
Context = KontekstWydruku(dok),
|
||||
OutputFormat = ReportFormats.PDF,
|
||||
AskForParameters = false
|
||||
};
|
||||
|
||||
// Act: skopiowanie strumienia do pamięci (wzorzec integracji z W66: 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 W66: zapis wydruku do strumienia bajtów wymaga obecnego wzorca '" +
|
||||
WzorzecSprzedaz + "' i silnika renderującego (brak w testowej bazie Demo). " +
|
||||
"Test dokumentuje wzorzec integracyjny GenerateReport → byte[]. " +
|
||||
"Szczegóły: " + ex.GetType().Name + " — " + ex.Message);
|
||||
return;
|
||||
}
|
||||
|
||||
// Assert: bufor zawiera dane i zaczyna się od sygnatury PDF.
|
||||
pdfBytes.Should().NotBeNullOrEmpty("integracyjny wydruk 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”).");
|
||||
}
|
||||
|
||||
// ===================================================================================
|
||||
// W62/W66 — Reguły spójności ReportResult (CheckConsistency) — bez renderowania
|
||||
// ===================================================================================
|
||||
|
||||
[Test]
|
||||
[Description("W62/W66 (reguła CheckConsistency): IReportService wymaga ustawionego TemplateFileName i " +
|
||||
"wyklucza ReportName. ReportResult bez TemplateFileName, ale z ReportName, narusza spójność " +
|
||||
"→ GenerateReport powinno rzucić ArgumentException (a nie wyrenderować PDF).")]
|
||||
public void W66_RegulaSpojnosci_BrakTemplateFileName_RzucaArgumentException()
|
||||
{
|
||||
// Arrange: konfiguracja wykluczająca tryb IReportService — ReportName zamiast TemplateFileName.
|
||||
// Reguła spójności ReportResult sprawdzana jest PRZED dostępem do danych, więc test
|
||||
// nie potrzebuje żadnego dokumentu (a tym bardziej zatwierdzonej FV) — pusty kontekst wystarcza.
|
||||
var rr = new ReportResult
|
||||
{
|
||||
ReportName = "Faktura", // tryb interaktywny z menu — wyklucza się z TemplateFileName
|
||||
DataType = typeof(DokumentHandlowy),
|
||||
Context = Login.CreateEmptyContext().Clone(Session),
|
||||
OutputFormat = ReportFormats.PDF,
|
||||
AskForParameters = false
|
||||
};
|
||||
|
||||
// Act + Assert: naruszenie reguły spójności → ArgumentException.
|
||||
// Asercja samej walidacji nie wymaga obecności wzorca .repx, więc nie owijamy jej w Ignore.
|
||||
Action act = () => Raporty.GenerateReport(rr);
|
||||
act.Should().Throw<ArgumentException>(
|
||||
"IReportService akceptuje wyłącznie tryb z TemplateFileName; ReportName i brak TemplateFileName " +
|
||||
"naruszają CheckConsistency");
|
||||
}
|
||||
|
||||
// ===================================================================================
|
||||
// SCENARIUSZE POMINIĘTE (SKIP) — uzasadnienie zgodne z treścią rozdziału 12
|
||||
// ===================================================================================
|
||||
|
||||
[Test]
|
||||
[Ignore("W62/W63 (sprzęt) — druk na FIZYCZNĄ drukarkę: IReportService.PrintReport(rr) oraz " +
|
||||
"ReportResult.Target = ReportTargets.Printer/PrinterService wymagają podłączonej drukarki i " +
|
||||
"sterownika. To operacja sprzętowa — NIE da się jej przetestować jednostkowo (brak asercji na " +
|
||||
"wyniku). W kodzie i integracjach używaj ścieżki GenerateReport → strumień/PDF (W62/W66). SKIP wg pułapek W62.")]
|
||||
[Description("W62/W63: druk na fizyczną drukarkę (PrintReport / Target=Printer) — nietestowalny (wymaga sprzętu).")]
|
||||
public void W62_DrukNaDrukarke_Skip() { }
|
||||
|
||||
[Test]
|
||||
[Ignore("W63 — wydruk dokumentu magazynowego (PZ/WZ/MM): mechanizm identyczny jak W62, różni tylko wzorzec " +
|
||||
"dobrany do rodzaju dokumentu wg jego definicji (np. „WydanieZewnetrzne.repx”) + ustawienie dok.Magazyn " +
|
||||
"w kontekście. Test renderowania jest pokryty wzorcowo przez W62 (ta sama ścieżka GenerateReport → „%PDF”); " +
|
||||
"osobny test wymagałby kolejnego, niegwarantowanego wzorca .repx i nie wnosi nowej ścieżki API. " +
|
||||
"SKIP: identyczny kontrakt, inny plik wzorca (konfiguracja wdrożenia).")]
|
||||
[Description("W63: wydruk dokumentu magazynowego (WydanieZewnetrzne.repx) — pominięte (ten sam kontrakt co W62, inny wzorzec).")]
|
||||
public void W63_WydrukDokumentuMagazynowego_Skip() { }
|
||||
|
||||
[Test]
|
||||
[Ignore("W64 (ścieżka bazodanowa) — zestawienie/raport dobowy/okresowy przez IReportService z wzorcem " +
|
||||
"zestawienia (np. „ZestawienieSprzedazy.repx”), DataType=typeof(Soneta.Handel.DokHandlowe) i parametrem " +
|
||||
"okresu FromTo w kontekście. Ścieżka API jest tożsama z W62 (GenerateReport → „%PDF”), różni ją wyłącznie " +
|
||||
"wzorzec i typ danych; konkretny wzorzec zestawienia nie jest gwarantowany w bazie Demo. SKIP: pokryte " +
|
||||
"wzorcowo przez W62, brak gwarancji wzorca rejestru.")]
|
||||
[Description("W64: bazodanowe zestawienie za dzień/okres (FromTo, DataType=DokHandlowe) — pominięte (ten sam kontrakt co W62).")]
|
||||
public void W64_ZestawienieBazodanowe_Skip() { }
|
||||
|
||||
[Test]
|
||||
[Ignore("W64 (sprzęt) — fiskalny raport dobowy/okresowy drukarki: Soneta.Fiskal.IFiscalPrinterAPI." +
|
||||
"DrukujRaport(nazwaDrukarki) / DrukujRaportOkresowy(nazwaDrukarki, RaportOkresowyParams) oraz Fiskalizuj(...) " +
|
||||
"wymagają podłączonej DRUKARKI FISKALNEJ — operacja sprzętowa, NIE do testów jednostkowych. Testować można " +
|
||||
"tylko poprawne ustawienie RaportOkresowyParams.RaportZaOkres (FromTo), nie faktyczny druk. SKIP wg pułapek W64.")]
|
||||
[Description("W64: fiskalny raport dobowy/okresowy (IFiscalPrinterAPI) — nietestowalny (wymaga drukarki fiskalnej).")]
|
||||
public void W64_FiskalnyRaport_Skip() { }
|
||||
|
||||
[Test]
|
||||
[Ignore("W65 — wydruk zbiorczy dla zaznaczonego zbioru: DataType=typeof(DokumentHandlowy[]) + Rows=tablica + " +
|
||||
"Context.Set(tablica). Ścieżka renderowania jest tożsama z W62 (GenerateReport → „%PDF”), różni ją tylko " +
|
||||
"tryb wielu rekordów; test wymagałby tego samego, niegwarantowanego wzorca „Sprzedaz.repx”. Aby utrzymać " +
|
||||
"suitę zieloną i nie duplikować ścieżki, scenariusz dokumentujemy tu (SKIP), a renderowanie pokrywa W62. " +
|
||||
"Kluczowa różnica vs W62: DataType tablicowy przełącza wzorzec w tryb wielu rekordów.")]
|
||||
[Description("W65: wydruk zbiorczy (DataType=DokumentHandlowy[], Rows) — pominięte (ta sama ścieżka renderowania co W62).")]
|
||||
public void W65_WydrukZbiorczy_Skip() { }
|
||||
|
||||
[Test]
|
||||
[Ignore("W66 (e-mail/OutputHandler) — Target=ReportTargets.Email/Attachment wymaga skonfigurowanego konta " +
|
||||
"pocztowego (KontoPocztowe) i szablonu (SzablonEmail) w pełnej sesji aplikacyjnej — poza zakresem testu " +
|
||||
"jednostkowego. ReportResult.OutputHandler NIE jest obsługiwany przez IReportService (CheckConsistency " +
|
||||
"rzuca ArgumentException) — służy jako rezultat operacji w trybie wzorca (worker/Command z UI). Testowalny " +
|
||||
"rdzeń W66 (GenerateReport → byte[]) pokrywa W66_WydrukDoStrumieniaBajtow. SKIP: integracja pocztowa / tryb UI.")]
|
||||
[Description("W66: wysyłka e-mail (Target=Email) i OutputHandler — pominięte (wymaga konta/szablonu / tryb UI).")]
|
||||
public void W66_EmailIOutputHandler_Skip() { }
|
||||
}
|
||||
@@ -0,0 +1,376 @@
|
||||
using System.Linq;
|
||||
using AwesomeAssertions;
|
||||
using NUnit.Framework;
|
||||
using Soneta.Deklaracje.UE;
|
||||
using Soneta.Handel;
|
||||
using Soneta.Handel.Kompletacje;
|
||||
using Soneta.Types;
|
||||
|
||||
namespace Soneta.Skills.Test.Handel.DokumentyHandlowe;
|
||||
|
||||
/// <summary>
|
||||
/// Rozdział 13 skilla „dokument-handlowy” — tematy specjalistyczne (W67–W74):
|
||||
/// KSeF, fiskalizacja, e-paragon, kompletacja oraz Intrastat.
|
||||
/// <para>
|
||||
/// <b>Zasada całego rozdziału:</b> większość operacji łączy dokument z systemem zewnętrznym
|
||||
/// (bramka KSeF, wysyłka e-mail) albo ze sprzętem (drukarka fiskalna). Takich fragmentów
|
||||
/// <b>nie da się odtworzyć w teście jednostkowym</b> — są oznaczone <c>[Ignore]</c> z uzasadnieniem.
|
||||
/// Testujemy wyłącznie część <b>offline/lokalną</b>: ustawienie pól i parametrów oraz strukturę
|
||||
/// (parametry workerów, pola dokumentu, warunki widoczności/aktywności akcji).
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Cały kod operuje wyłącznie na <b>publicznym kontrakcie</b> platformy Soneta — tak jak dodatek
|
||||
/// programisty zewnętrznego.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Fakty zweryfikowane skanem DLL (różnice względem treści skilla):</b>
|
||||
/// <list type="bullet">
|
||||
/// <item><c>RodzajIntrastat</c> oraz <c>KodRodzajuTransakcji</c> żyją w <c>Soneta.Handel</c>
|
||||
/// (a nie w <c>Soneta.Magazyny</c>); <c>RodzajIntrastat</c>: <c>NieUwzględniaj=0</c>,
|
||||
/// <c>Przywóz=257</c>, <c>Wywóz=258</c>.</item>
|
||||
/// <item><c>dok.RodzajTransakcji</c> (typ <c>KodRodzajuTransakcji</c>) oraz <c>dok.OkresIntrastat</c>
|
||||
/// (<c>Date</c>) są <b>publicznie zapisywalne</b>; <c>dok.EParagonAdresEmail</c> również.</item>
|
||||
/// <item><c>dok.SymbolKasy</c>, <c>dok.EParagon</c>, <c>dok.Kategoria</c>, <c>dok.KierunekMagazynu</c>
|
||||
/// <b>nie są publicznymi właściwościami</b> — nie da się ich odczytać/ustawić z dodatku zewnętrznego,
|
||||
/// dlatego testy operują na parametrach workerów i polach faktycznie publicznych.</item>
|
||||
/// <item><c>dok.UaktualnijIntrastat(kodCN, masa, kraj, przelicznik)</c> to publiczna metoda
|
||||
/// zwracająca <c>int</c> (liczbę zaktualizowanych pozycji).</item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public class Rozdzial13_SpecjalistyczneTest : DokumentHandlowyTestBase
|
||||
{
|
||||
// =================================================================================================
|
||||
// W74 — INTRASTAT (offline, w pełni testowalne)
|
||||
// =================================================================================================
|
||||
|
||||
[Test]
|
||||
[Description("W74: pole dokumentu RodzajTransakcji (KodRodzajuTransakcji) jest publicznie zapisywalne " +
|
||||
"— ustawiamy rodzaj transakcji Intrastat na dokumencie i odczytujemy go z powrotem.")]
|
||||
public void W74_RodzajTransakcji_MoznaUstawicNaDokumencie()
|
||||
{
|
||||
// Dokument zakupu unijnego (FF, faktura od dostawcy) — Intrastat dotyczy przepływów towarów w UE.
|
||||
// FF to dokument przychodowy — nie wymaga stanu magazynowego, więc można go utworzyć w Demo bez przyjęcia.
|
||||
var dok = UtworzDokument(Definicje.FakturaZakupu, kontrahent: Kontrahent(Kontrahent_.Abc));
|
||||
|
||||
// RodzajTransakcji to bazodanowy enum KodRodzajuTransakcji — ustawiamy w transakcji edycyjnej.
|
||||
// Wartość „Różne” (=1) to bezpieczny, istniejący wariant enuma.
|
||||
InTransaction(() => dok.RodzajTransakcji = KodRodzajuTransakcji.Różne);
|
||||
|
||||
// Asercja: pole zostało zapisane na dokumencie (odczyt publicznym getterem).
|
||||
dok.RodzajTransakcji.Should().Be(KodRodzajuTransakcji.Różne);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Description("W74: pole OkresIntrastat (Date) — miesiąc, w którym dokument trafi na deklarację — " +
|
||||
"jest publicznie zapisywalne; ustawiamy je i weryfikujemy odczyt.")]
|
||||
public void W74_OkresIntrastat_MoznaUstawicNaDokumencie()
|
||||
{
|
||||
var dok = UtworzDokument(Definicje.FakturaZakupu, kontrahent: Kontrahent(Kontrahent_.Abc));
|
||||
|
||||
// Okres deklaracji = pierwszy dzień bieżącego miesiąca (data decyduje o miesiącu deklaracji).
|
||||
var okres = Date.Today.FirstDayMonth();
|
||||
InTransaction(() => dok.OkresIntrastat = okres);
|
||||
|
||||
dok.OkresIntrastat.Should().Be(okres);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Description("W74: konstrukcja parametrów workera DokumentHandlowyZmienIntrastatParams przez Context " +
|
||||
"i osadzenie ich w workerze przez konstruktor — parametry (KodCN/Masa/Kraj/Przelicznik) " +
|
||||
"są ustawiane i widoczne przez worker.Params (offline; bez wywołania Update()).")]
|
||||
public void W74_ParametryWorkeraIntrastat_KonstrukcjaIPrzekazanie()
|
||||
{
|
||||
var dok = UtworzDokument(Definicje.FakturaZakupu, kontrahent: Kontrahent(Kontrahent_.Abc));
|
||||
|
||||
// Worker wymaga Params przez konstruktor; Params budujemy z kontekstu zawierającego dokument.
|
||||
var ctx = Session.GetEmptyContext();
|
||||
ctx.TryAdd(() => dok);
|
||||
var parametry = new DokumentHandlowyZmienIntrastatWorker.DokumentHandlowyZmienIntrastatParams(ctx)
|
||||
{
|
||||
KodCN = true, // przepisz kod CN z kartoteki towaru
|
||||
Masa = true, // przelicz masę pozycji
|
||||
Kraj = false, // nie aktualizuj kraju pochodzenia
|
||||
Przelicznik = true // ilość w jednostce uzupełniającej
|
||||
};
|
||||
|
||||
// Worker z Params przez konstruktor; właściwości [Context] (Dokument) inicjatorem obiektu.
|
||||
var worker = new DokumentHandlowyZmienIntrastatWorker(parametry) { Dokument = dok };
|
||||
|
||||
// Asercja: Params zostały przekazane do workera (read-only property Params).
|
||||
// (Same flagi mają tylko publiczny setter — weryfikujemy referencję obiektu Params.)
|
||||
worker.Params.Should().BeSameAs(parametry);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Description("W74: IsVisibleUpdate workera Intrastat jest false dla dokumentu, którego definicja ma " +
|
||||
"Intrastat == NieUwzględniaj (akcja pomijana) — sprawdzane czysto lokalnie, bez Update().")]
|
||||
public void W74_IsVisibleUpdate_DlaDefinicjiNieUwzgledniajacej_False()
|
||||
{
|
||||
// FV (faktura sprzedaży) w Demo nie jest dokumentem unijnym uwzględnianym w Intrastacie:
|
||||
// jego definicja ma RodzajIntrastat.NieUwzględniaj, więc akcja aktualizacji jest niewidoczna.
|
||||
var dok = UtworzDokument(Definicje.FakturaSprzedazy, kontrahent: Kontrahent(Kontrahent_.Abc));
|
||||
|
||||
// Warunek wstępny czytamy z definicji (publiczny getter Definicja.Intrastat).
|
||||
dok.Definicja.Intrastat.Should().Be(RodzajIntrastat.NieUwzględniaj,
|
||||
"definicja FV w Demo nie uwzględnia dokumentu w Intrastacie");
|
||||
|
||||
var ctx = Session.GetEmptyContext();
|
||||
ctx.TryAdd(() => dok);
|
||||
var parametry = new DokumentHandlowyZmienIntrastatWorker.DokumentHandlowyZmienIntrastatParams(ctx);
|
||||
var worker = new DokumentHandlowyZmienIntrastatWorker(parametry) { Dokument = dok };
|
||||
|
||||
// IsVisibleUpdate to czysta logika lokalna (bez sieci): dla NieUwzględniaj zwraca false.
|
||||
DokumentHandlowyZmienIntrastatWorker.IsVisibleUpdate(dok).Should().BeFalse(
|
||||
"dokument z Definicja.Intrastat == NieUwzględniaj jest pomijany (akcja niewidoczna)");
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Description("W74: metoda dokumentu UaktualnijIntrastat(kodCN, masa, kraj, przelicznik) jest publiczna, " +
|
||||
"wykonuje się lokalnie i zwraca liczbę zaktualizowanych pozycji (>= 0). Dla dokumentu bez " +
|
||||
"pozycji zwraca 0 — operacja jest bezpieczna i nie wymaga sieci.")]
|
||||
public void W74_UaktualnijIntrastat_ZwracaLiczbeZaktualizowanychPozycji()
|
||||
{
|
||||
// Dokument bez pozycji — metoda nie ma czego aktualizować, ale musi się wykonać i zwrócić 0.
|
||||
var dok = UtworzDokument(Definicje.FakturaZakupu, kontrahent: Kontrahent(Kontrahent_.Abc));
|
||||
|
||||
int zaktualizowane = 0;
|
||||
// Metoda modyfikuje pozycje, więc wykonujemy ją w transakcji edycyjnej.
|
||||
InTransaction(() => zaktualizowane = dok.UaktualnijIntrastat(
|
||||
kodCN: true, masa: false, kraj: false, przelicznik: false));
|
||||
|
||||
// Brak pozycji ⇒ 0 zaktualizowanych; metoda zadziałała lokalnie bez wyjątku.
|
||||
zaktualizowane.Should().Be(0, "dokument bez pozycji nie ma czego aktualizować dla Intrastatu");
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Description("W74: wyszukanie dokumentów do deklaracji za okres — filtr SERWEROWY po dacie (klucz WgDaty), " +
|
||||
"a kwalifikację do Intrastatu weryfikujemy odczytem zapisanego pola OkresIntrastat. " +
|
||||
"Dokument zapisujemy (SaveDispose) i odnajdujemy po Guid.")]
|
||||
public void W74_WyszukanieDokumentowDoDeklaracji_FiltrSerwerowy()
|
||||
{
|
||||
// PZ (przywóz unijny) to dokument magazynowy → wymaga magazynu.
|
||||
var dok = UtworzDokument(
|
||||
Definicje.FakturaZakupu,
|
||||
kontrahent: Kontrahent(Kontrahent_.Abc),
|
||||
magazyn: Magazyn(Magazyn_.Firma));
|
||||
|
||||
// Oznaczamy dokument okresem Intrastat (bieżący miesiąc) i rodzajem transakcji — pola bazodanowe.
|
||||
var okres = Date.Today.FirstDayMonth();
|
||||
InTransaction(() =>
|
||||
{
|
||||
dok.OkresIntrastat = okres;
|
||||
dok.RodzajTransakcji = KodRodzajuTransakcji.Różne;
|
||||
});
|
||||
var guid = dok.Guid;
|
||||
|
||||
// Zapisujemy do bazy — pola OkresIntrastat/RodzajTransakcji są wtedy trwałe i widoczne dla filtru.
|
||||
SaveDispose();
|
||||
|
||||
// Filtr SERWEROWY po dacie (klucz WgDaty — sprawdzony, niezawodny dla przedziału dat).
|
||||
// NIE ładujemy całej tabeli; warunek na polu bazodanowym Data trafia do WHERE.
|
||||
var od = Date.Today.AddMonths(-1);
|
||||
var doDnia = Date.Today.AddMonths(1);
|
||||
var dokumenty = Handel.DokHandlowe.WgDaty[(DokumentHandlowy d) =>
|
||||
d.Data >= od && d.Data <= doDnia]
|
||||
.Cast<DokumentHandlowy>()
|
||||
.ToArray();
|
||||
|
||||
// Nasz dokument musi się znaleźć w zbiorze (po Guid).
|
||||
dokumenty.Should().Contain(d => d.Guid == guid,
|
||||
"dokument z bieżącego miesiąca mieści się w zapytaniu serwerowym po dacie");
|
||||
|
||||
// Kwalifikacja do deklaracji Intrastat: odczytujemy zapisane pole OkresIntrastat z bazy.
|
||||
var zapisany = Get<DokumentHandlowy>(guid);
|
||||
zapisany.OkresIntrastat.Should().Be(okres,
|
||||
"dokument z OkresIntrastat w bieżącym miesiącu kwalifikuje się do deklaracji za ten okres");
|
||||
}
|
||||
|
||||
// =================================================================================================
|
||||
// W73 — KOMPLETACJA (offline; pełne tworzenie kompletu wymaga konfiguracji spoza Demo)
|
||||
// =================================================================================================
|
||||
|
||||
[Test]
|
||||
[Description("W73: SposobEdycjiKompletacji odczytany z definicji zwykłego dokumentu (FV) to None — " +
|
||||
"czyli definicja nie obsługuje kompletacji (warunek widoczności akcji PrzeliczWgKartoteki).")]
|
||||
public void W73_DefinicjaZwyklaNieObslugujeKompletacji()
|
||||
{
|
||||
// FV to zwykła faktura — jej definicja nie jest definicją kompletacji.
|
||||
var dok = UtworzDokument(Definicje.FakturaSprzedazy, kontrahent: Kontrahent(Kontrahent_.Abc));
|
||||
|
||||
// Publiczny getter Definicja.SposobEdycjiKompletacji; None == brak obsługi kompletacji.
|
||||
dok.Definicja.SposobEdycjiKompletacji.Should().Be(SposobEdycjiKompletacji.None,
|
||||
"definicja FV nie jest definicją kompletacji");
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Description("W73: akcja PrzeliczWgKartoteki jest niewidoczna (IsVisiblePrzeliczWgKartoteki == false) " +
|
||||
"dla dokumentu, którego definicja ma SposobEdycjiKompletacji == None — sprawdzane lokalnie.")]
|
||||
public void W73_AkcjaPrzeliczWgKartoteki_NiewidocznaDlaDefinicjiBezKompletacji()
|
||||
{
|
||||
var dok = UtworzDokument(Definicje.FakturaSprzedazy, kontrahent: Kontrahent(Kontrahent_.Abc));
|
||||
|
||||
// Worker kompletacji ma bezparametrowy konstruktor; sprawdzamy czystą logikę widoczności akcji.
|
||||
var worker = new Soneta.Handel.Kompletacje.DokumentKompletacjaWorker();
|
||||
|
||||
// Dla SposobEdycjiKompletacji == None akcja jest niewidoczna (operacja lokalna, bez sieci).
|
||||
DokumentKompletacjaWorker.IsVisiblePrzeliczWgKartoteki(dok).Should().BeFalse(
|
||||
"akcja kompletacji jest ukryta, gdy definicja nie obsługuje kompletacji (None)");
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Ignore("W73 (utworzenie dokumentu kompletacji + PrzeliczWgKartoteki): wymaga definicji dokumentu z " +
|
||||
"SposobEdycjiKompletacji != None oraz kartoteki kompletacji (wyrób + składniki) i magazynu z " +
|
||||
"zapisanym przychodem składników (Demo blokuje stan ujemny). Baza Demo nie gwarantuje gotowej " +
|
||||
"definicji kompletacji ani kartoteki kompletu — utworzenie ich to dane KONFIGURACYJNE spoza " +
|
||||
"zakresu testu dokumentu handlowego. Logika widoczności akcji jest pokryta lokalnie powyżej.")]
|
||||
[Description("W73: utworzenie kompletu i przeliczenie wg kartoteki — pominięte (brak definicji/kartoteki kompletacji w Demo).")]
|
||||
public void W73_UtworzenieKompletuIPrzeliczenie_Skip() { }
|
||||
|
||||
// =================================================================================================
|
||||
// W69 — WALIDACJA STRUKTURY XML KSeF (offline; wymaga wcześniej wygenerowanego XML)
|
||||
// =================================================================================================
|
||||
|
||||
[Test]
|
||||
[Ignore("W69 (walidacja struktury XML — KSeFSprawdzXMLWorker.Check / KSeFSchemaVerifier.Verify): część " +
|
||||
"samej walidacji jest offline (lokalny XSD), ALE warunkiem wstępnym (IsEnabledCheck) jest, by " +
|
||||
"dokument miał już WYGENEROWANY plik KSeF (ImportExportKSeF.Xml niepusty). Generowanie XML KSeF " +
|
||||
"to operacja modułu KSeF na zatwierdzonej fakturze sprzedaży z kompletem danych podatkowych " +
|
||||
"(pieczątka firmy, NIP-y, stawki) — w bazie Demo nie jest to gwarantowane bez konfiguracji KSeF. " +
|
||||
"Bez wygenerowanego XML Check() jest no-op / rzuca, więc test offline nie jest wiarygodny. " +
|
||||
"Sama wysyłka i pobranie UPO to operacje SIECIOWE (W67/W68) — patrz testy poniżej.")]
|
||||
[Description("W69: walidacja struktury XML KSeF — pominięte (wymaga wcześniej wygenerowanego pliku KSeF; offline część nieosiągalna w Demo).")]
|
||||
public void W69_WalidacjaStrukturyXml_Skip() { }
|
||||
|
||||
// =================================================================================================
|
||||
// W71 — FISKALIZACJA (offline: ustawienie parametrów workera; wydruk = sprzęt → SKIP)
|
||||
// =================================================================================================
|
||||
|
||||
[Test]
|
||||
[Description("W71: konstrukcja parametrów FiskalizacjaDokumentuWorker.ParametryFiskalizacjiDokumentu " +
|
||||
"przez Context oraz osadzenie ich w workerze (offline — BEZ wywołania Execute/druku). " +
|
||||
"Weryfikujemy, że worker i jego parametry dają się złożyć z publicznego kontraktu.")]
|
||||
public void W71_ParametryFiskalizacji_KonstrukcjaIPrzekazanie()
|
||||
{
|
||||
// Paragon (PAR) to dokument sprzedaży — kandydat do fiskalizacji.
|
||||
var dok = UtworzDokument(Definicje.Paragon, kontrahent: Kontrahent(Kontrahent_.Abc));
|
||||
|
||||
var ctx = Session.GetEmptyContext();
|
||||
ctx.TryAdd(() => dok);
|
||||
// SymbolKasy = symbol drukarki (max 12 znaków) — pole parametru, nie wymaga sprzętu.
|
||||
var parametry = new FiskalizacjaDokumentuWorker.ParametryFiskalizacjiDokumentu(ctx)
|
||||
{
|
||||
SymbolKasy = "DRUK1"
|
||||
};
|
||||
|
||||
// Worker z bezparametrowym ctor; właściwości [Context] inicjatorem obiektu.
|
||||
var worker = new FiskalizacjaDokumentuWorker { Dokument = dok, Parametry = parametry };
|
||||
|
||||
// Asercja struktury: parametry zostały przekazane do workera (referencja Parametry).
|
||||
worker.Parametry.Should().BeSameAs(parametry);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Description("W71: IsVisibleExecute jest false dla dokumentu niesprzedażowego (przyjęcie magazynowe PZ) — " +
|
||||
"fiskalizacja dotyczy tylko Sprzedaży/KorektySprzedaży. Czysta logika lokalna, bez druku.")]
|
||||
public void W71_IsVisibleExecute_DlaZakupu_False()
|
||||
{
|
||||
// PZ to przyjęcie magazynowe (przychód, kategoria PrzyjęcieMagazynowe), NIE sprzedaż —
|
||||
// nie podlega fiskalizacji (paragon/fiskalizacja dotyczy wyłącznie dokumentów sprzedaży).
|
||||
// PZ jest dokumentem magazynowym, więc wymaga magazynu.
|
||||
var dok = UtworzDokument(
|
||||
Definicje.FakturaZakupu,
|
||||
kontrahent: Kontrahent(Kontrahent_.Abc),
|
||||
magazyn: Magazyn(Magazyn_.Firma));
|
||||
var worker = new FiskalizacjaDokumentuWorker { Dokument = dok };
|
||||
|
||||
// IsVisibleExecute to lokalny warunek widoczności (kategoria dokumentu) — bez sieci/sprzętu.
|
||||
FiskalizacjaDokumentuWorker.IsVisibleExecute(dok).Should().BeFalse(
|
||||
"fiskalizacja dotyczy tylko dokumentów sprzedaży / korekt sprzedaży");
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Description("W71: IsEnabledExecute jest false dla dokumentu w BUFORZE — oznaczyć jako zafiskalizowane " +
|
||||
"można tylko dokument zatwierdzony (z pustym SymbolKasy). Sprawdzane lokalnie, bez druku.")]
|
||||
public void W71_IsEnabledExecute_DlaBufora_False()
|
||||
{
|
||||
// Paragon w buforze (świeżo utworzony, Stan == Bufor).
|
||||
var dok = UtworzDokument(Definicje.Paragon, kontrahent: Kontrahent(Kontrahent_.Abc));
|
||||
dok.Bufor.Should().BeTrue("świeżo utworzony dokument jest w buforze");
|
||||
|
||||
var worker = new FiskalizacjaDokumentuWorker { Dokument = dok };
|
||||
|
||||
// IsEnabledExecute wymaga dokumentu zatwierdzonego — dla bufora zwraca false (logika lokalna).
|
||||
FiskalizacjaDokumentuWorker.IsEnabledExecute(dok).Should().BeFalse(
|
||||
"oznaczyć jako zafiskalizowane można tylko dokument zatwierdzony");
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Ignore("W71 (faktyczny wydruk / odczyt SymbolKasy po Execute): klasa Fiscalizer drukuje na DRUKARCE " +
|
||||
"FISKALNEJ — operacja SPRZĘTOWA, nie do odtworzenia w teście jednostkowym. Dodatkowo dok.SymbolKasy " +
|
||||
"NIE jest publiczną właściwością DokumentHandlowy (brak getter/setter w publicznym kontrakcie), " +
|
||||
"więc efekt FiskalizacjaDokumentuWorker.Execute() nie jest odczytywalny z dodatku zewnętrznego. " +
|
||||
"Testujemy więc tylko konstrukcję parametrów i warunki IsVisible/IsEnabled (powyżej).")]
|
||||
[Description("W71: wydruk fiskalny i odczyt SymbolKasy po Execute — pominięte (sprzęt + pole niepubliczne).")]
|
||||
public void W71_WydrukFiskalnyIOdczytSymbolKasy_Skip() { }
|
||||
|
||||
// =================================================================================================
|
||||
// W72 — E-PARAGON (offline: ustawienie adresu e-mail; wysyłka/wydruk = sieć/sprzęt → SKIP)
|
||||
// =================================================================================================
|
||||
|
||||
[Test]
|
||||
[Description("W72: pole dokumentu EParagonAdresEmail jest publicznie zapisywalne — ustawiamy adres " +
|
||||
"e-mail odbiorcy e-paragonu i odczytujemy go z powrotem (offline; bez wysyłki e-mail).")]
|
||||
public void W72_EParagonAdresEmail_MoznaUstawicNaDokumencie()
|
||||
{
|
||||
// Paragon (PAR) — dokument, który może zostać e-paragonem.
|
||||
var dok = UtworzDokument(Definicje.Paragon, kontrahent: Kontrahent(Kontrahent_.Abc));
|
||||
|
||||
// EParagonAdresEmail to bazodanowy string (publiczny setter) — ustawienie nie wysyła e-maila.
|
||||
InTransaction(() => dok.EParagonAdresEmail = "klient@example.com");
|
||||
|
||||
dok.EParagonAdresEmail.Should().Be("klient@example.com");
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Ignore("W72 (flaga EParagon, polityka OznaczJakoEParagon, wysyłka e-mail, ponowny wydruk paragonu): " +
|
||||
"dok.EParagon NIE jest publiczną właściwością DokumentHandlowy (brak w publicznym kontrakcie), " +
|
||||
"więc efekt uboczny ustawienia EParagonAdresEmail (auto EParagon = true) nie jest odczytywalny " +
|
||||
"z dodatku zewnętrznego. Sama wysyłka e-paragonu wymaga SIECI (e-mail), a PonownyWydrukParagonuWorker " +
|
||||
"drukuje na DRUKARCE FISKALNEJ (sprzęt) — obie operacje nie do odtworzenia w teście jednostkowym. " +
|
||||
"Testujemy więc tylko ustawienie EParagonAdresEmail (powyżej).")]
|
||||
[Description("W72: flaga EParagon / polityka / wysyłka e-mail / ponowny wydruk — pominięte (pole niepubliczne + sieć/sprzęt).")]
|
||||
public void W72_FlagaWysylkaIPonownyWydruk_Skip() { }
|
||||
|
||||
// =================================================================================================
|
||||
// W67 / W68 / W70 — KSeF: wysyłka, status, import (SIEĆ → SKIP)
|
||||
// =================================================================================================
|
||||
|
||||
[Test]
|
||||
[Ignore("W67 (wysłanie faktury do KSeF — KSeFWyslijWorker.Wyslij / KSeFWysylkaWsadowaWorker.WyslijZbiorczo): " +
|
||||
"cała komunikacja z bramką KSeF (IKSeFAPIv2Service/IKSeFAPIService) wymaga SIECI — nie do " +
|
||||
"odtworzenia w teście jednostkowym. Warunkiem wstępnym jest też zwalidowany XML (W69), którego " +
|
||||
"Demo nie gwarantuje. Testujemy w skillu jedynie przygotowanie parametrów/weryfikatora, ale bez " +
|
||||
"realnej wysyłki nie ma odczytywalnego efektu offline na publicznym kontrakcie dokumentu.")]
|
||||
[Description("W67: wysyłka faktury do KSeF (pojedyncza/zbiorcza) — pominięte (operacja sieciowa).")]
|
||||
public void W67_WysylkaKSeF_Skip() { }
|
||||
|
||||
[Test]
|
||||
[Ignore("W68 (sprawdzenie statusu KSeF i odczyt numeru KSeF): KSeFSprawdzStatusWorker.SprawdzStatus woła " +
|
||||
"bramkę KSeF (SIEĆ) — nie do odtworzenia jednostkowo. Odczyt zapisanego statusu (dok.StatusKSeF) " +
|
||||
"i numeru (dok.KSeFKomunikat.NumerDokumentuKSeF) byłby offline, ale wymaga wcześniejszej wysyłki " +
|
||||
"ustawiającej KSeFKomunikat — bez niej w Demo nie ma czego odczytać (StatusKSeF == NieDotyczy/Brak), " +
|
||||
"więc test nie weryfikowałby realnego zachowania. SKIP: zależność od stanu po operacji sieciowej.")]
|
||||
[Description("W68: sprawdzenie statusu i odczyt numeru KSeF — pominięte (sieć + brak danych KSeF w Demo).")]
|
||||
public void W68_StatusINumerKSeF_Skip() { }
|
||||
|
||||
[Test]
|
||||
[Ignore("W70 (import faktur z KSeF — KSeFDownloadPartWorker.Pobierz): pobranie paczek wyników wymaga SIECI " +
|
||||
"(IKSeFAPIv2Service.PobierzFakturyZPaczek) i operuje na rekordach konfiguracyjno-systemowych " +
|
||||
"(KSeFZapytanieOFa, KSeFPlik), a nie bezpośrednio na DokumentHandlowy — dokument zakupu powstaje " +
|
||||
"dopiero w kolejnym kroku (import XML, obszar księgowy). Brak offline'owego, odczytywalnego efektu " +
|
||||
"na dokumencie handlowym. SKIP: operacja sieciowa poza zakresem dokumentu handlowego.")]
|
||||
[Description("W70: import faktur zakupu z KSeF — pominięte (operacja sieciowa; obszar konfiguracyjno-księgowy).")]
|
||||
public void W70_ImportZKSeF_Skip() { }
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
using System.Linq;
|
||||
using AwesomeAssertions;
|
||||
using NUnit.Framework;
|
||||
using Soneta.Handel;
|
||||
using Soneta.Kasa;
|
||||
using Soneta.Types;
|
||||
|
||||
namespace Soneta.Skills.Test.Handel.DokumentyHandlowe;
|
||||
|
||||
/// <summary>
|
||||
/// Rozdział 14 skilla „dokument-handlowy” — Płatności dokumentu handlowego (W75–W82).
|
||||
/// <para>
|
||||
/// Płatności (należności / zobowiązania) powstają automatycznie z dokumentu handlowego
|
||||
/// płatnego (FV, FZ). Dostęp daje kolekcja <c>dok.Platnosci</c>
|
||||
/// (<c>SubTable<Soneta.Kasa.Platnosc></c>). Testy weryfikują przede wszystkim
|
||||
/// <b>odczyt</b>: istnienie płatności, kwotę, sposób zapłaty, termin, stan rozliczenia
|
||||
/// oraz kalkulowaną flagę <c>dok.InnyPłatnik</c>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Klucz rozdziału:</b> faktura sprzedaży to rozchód magazynowy — w bazie Demo
|
||||
/// <c>StanUjemnyVerifier</c> wymaga wcześniejszego <b>zapisanego</b> przyjęcia (PW) towaru.
|
||||
/// Dlatego najpierw tworzymy i zapisujemy PW na stan, dopiero potem FV z pozycją. Magazyn
|
||||
/// księguje się po <c>Session.Save()</c>; po <c>Save()</c> w środku testu okno edycji się
|
||||
/// zamyka, więc dokument odczytujemy na świeżej sesji przez <c>Get<T>(guid)</c>.
|
||||
/// </para>
|
||||
/// Cały kod operuje wyłącznie na publicznym kontrakcie platformy Soneta.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public class Rozdzial14_PlatnosciTest : DokumentHandlowyTestBase
|
||||
{
|
||||
// ── Stałe danych testowych (towar magazynowy w sztukach, kontrahent z Demo) ──
|
||||
private const double IloscPrzyjecia = 10;
|
||||
private const double IloscFv = 2;
|
||||
private const double CenaFv = 100;
|
||||
|
||||
/// <summary>
|
||||
/// Tworzy fakturę sprzedaży (FV) z jedną pozycją BIKINI i zapisuje ją <b>w buforze</b>.
|
||||
/// Wymaga wcześniej ZATWIERDZONEGO i zapisanego przyjęcia (stan towaru) — robi to bazowy
|
||||
/// helper <c>PrzyjmijNaStan</c> (tworzy i zatwierdza PW, dopiero to księguje stan; bez tego
|
||||
/// Demo odrzuca rozchód FV przez kontrolę stanu ujemnego). Zwraca Guid zapisanej FV.
|
||||
/// <para>
|
||||
/// <b>Świadomie NIE zatwierdzamy FV</b>: w testowej bazie Demo zatwierdzenie faktury sprzedaży
|
||||
/// rzuca <c>NullReferenceException</c> w ewidencji VAT. Płatności (Należność), <c>Suma</c> i
|
||||
/// pozostałe pola są już wyliczone na dokumencie w buforze, więc asercje robimy na FV w buforze.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
private System.Guid UtworzFvWBuforze()
|
||||
{
|
||||
// WARUNEK WSTĘPNY: zatwierdzone, zapisane przyjęcie tego towaru (stan ujemny zablokowany).
|
||||
PrzyjmijNaStan(Towar_.Bikini, IloscPrzyjecia, cena: 5);
|
||||
|
||||
// FV: definicja PIERWSZA, potem kontrahent i magazyn (helper bazy).
|
||||
var fv = UtworzDokument(Definicje.FakturaSprzedazy,
|
||||
kontrahent: Kontrahent(Kontrahent_.Abc),
|
||||
magazyn: Magazyn(Magazyn_.Firma));
|
||||
InTransaction(() => DodajPozycje(fv, Towar(Towar_.Bikini), IloscFv, cena: CenaFv));
|
||||
|
||||
var guid = fv.Guid;
|
||||
// Save (FV pozostaje w BUFORZE) → utrwala dokument i wyliczone płatności; SaveDispose zamyka okno edycji.
|
||||
SaveDispose();
|
||||
return guid;
|
||||
}
|
||||
|
||||
// ===================================================================================
|
||||
// W75 — Przeglądanie płatności dokumentu (dok.Platnosci)
|
||||
// ===================================================================================
|
||||
|
||||
[Test]
|
||||
[Description("W75: FV w buforze z pozycją ma niepustą kolekcję dok.Platnosci — " +
|
||||
"dokument płatny automatycznie tworzy płatność (Należność) już w buforze.")]
|
||||
public void W75_FakturaTworzyPlatnosc()
|
||||
{
|
||||
// Arrange + Act: zatwierdzone przyjęcie na stan + FV w buforze (płatność tworzy się automatycznie).
|
||||
var guid = UtworzFvWBuforze();
|
||||
|
||||
// Odczyt na świeżej sesji po Guid (po Save okno edycji jest zamknięte).
|
||||
var fv = Get<DokumentHandlowy>(guid);
|
||||
fv.Should().NotBeNull();
|
||||
|
||||
// dok.Platnosci to SubTable<Platnosc>; iterujemy serwerowo i materializujemy do listy do asercji.
|
||||
var platnosci = fv.Platnosci.Cast<Platnosc>().ToList();
|
||||
|
||||
// Asercja: dokument płatny wygenerował co najmniej jedną płatność.
|
||||
platnosci.Should().NotBeEmpty("faktura (dokument płatny) automatycznie tworzy płatność");
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Description("W75: odczyt podstawowych pól płatności — Kwota (waluta dokumentu, PLN), " +
|
||||
"SposobZaplaty.Nazwa, Termin oraz StanRozliczenia.")]
|
||||
public void W75_OdczytPolPlatnosci()
|
||||
{
|
||||
var guid = UtworzFvWBuforze();
|
||||
var fv = Get<DokumentHandlowy>(guid);
|
||||
|
||||
// Bierzemy pierwszą (zwykle jedyną) płatność faktury.
|
||||
var p = fv.Platnosci.Cast<Platnosc>().First();
|
||||
|
||||
// Kwota płatności jest w walucie dokumentu — dla zwykłej FV to PLN (symbol systemowy).
|
||||
p.Kwota.Symbol.Should().Be(Currency.SystemSymbol, "płatność zwykłej FV jest w PLN");
|
||||
// Kwota powinna odpowiadać wartości brutto dokumentu (jedna płatność = całość).
|
||||
p.Kwota.Value.Should().Be(fv.BruttoCy.Value,
|
||||
"pojedyncza płatność pokrywa pełną wartość brutto dokumentu");
|
||||
|
||||
// Sposób zapłaty to rekord konfiguracyjny — ma niepustą nazwę (np. „Przelew”/„Gotówka”).
|
||||
p.SposobZaplaty.Should().NotBeNull("płatność dziedziczy sposób zapłaty z warunków");
|
||||
p.SposobZaplaty.Nazwa.Should().NotBeNullOrEmpty();
|
||||
|
||||
// Termin jest realną datą (nie MaxValue) — wyznaczonym przez warunki płatności.
|
||||
p.Termin.Should().NotBe(Date.MaxValue, "termin płatności jest wyznaczony");
|
||||
|
||||
// StanRozliczenia to enum kasowy — odczytujemy go bez modyfikacji.
|
||||
p.StanRozliczenia.Should().BeOneOf(
|
||||
StanRozliczenia.Nierozliczony,
|
||||
StanRozliczenia.Czesciowo,
|
||||
StanRozliczenia.Calkowicie,
|
||||
StanRozliczenia.NiePodlega);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Description("W75: płatność FV jest należnością — Kierunek == Przychod, CzyNaleznosc == true, " +
|
||||
"CzyZobowiazanie == false.")]
|
||||
public void W75_PlatnoscFakturySprzedazyToNaleznosc()
|
||||
{
|
||||
var guid = UtworzFvWBuforze();
|
||||
var fv = Get<DokumentHandlowy>(guid);
|
||||
var p = fv.Platnosci.Cast<Platnosc>().First();
|
||||
|
||||
// Sprzedaż → należność (przychód środków pieniężnych).
|
||||
p.Kierunek.Should().Be(Soneta.Core.KierunekPlatnosci.Przychod);
|
||||
p.CzyNaleznosc.Should().BeTrue("płatność faktury sprzedaży to należność");
|
||||
p.CzyZobowiazanie.Should().BeFalse();
|
||||
}
|
||||
|
||||
// ===================================================================================
|
||||
// W80 — Stan rozliczenia płatności (nowa, nierozliczona)
|
||||
// ===================================================================================
|
||||
|
||||
[Test]
|
||||
[Description("W80: świeżo wystawiona (nieopłacona) płatność jest nierozliczona — " +
|
||||
"StanRozliczenia == Nierozliczony, Rozliczono == false, KwotaRozliczona == 0, " +
|
||||
"DoRozliczenia == Kwota.")]
|
||||
public void W80_NowaPlatnoscJestNierozliczona()
|
||||
{
|
||||
var guid = UtworzFvWBuforze();
|
||||
var fv = Get<DokumentHandlowy>(guid);
|
||||
|
||||
// Płatność podlegająca rozliczeniu (Rozliczana == true) i bez żadnych zapłat.
|
||||
var p = fv.Platnosci.Cast<Platnosc>().First();
|
||||
|
||||
// Brak operacji kasowych → płatność nierozliczona.
|
||||
p.StanRozliczenia.Should().Be(StanRozliczenia.Nierozliczony,
|
||||
"nowa płatność bez zapłat jest nierozliczona");
|
||||
p.Rozliczono.Should().BeFalse("nic jeszcze nie zapłacono");
|
||||
p.KwotaRozliczona.Value.Should().Be(0, "brak rozliczeń");
|
||||
// Całość zostaje do rozliczenia (DoRozliczenia == Kwota dla płatności nierozliczonej rozliczanej).
|
||||
p.DoRozliczenia.Value.Should().Be(p.Kwota.Value,
|
||||
"dla nierozliczonej płatności do rozliczenia pozostaje pełna kwota");
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Description("W80: DataRozliczenia nierozliczonej płatności to Date.MaxValue (sentinel „nierozliczona”), " +
|
||||
"a nie realna data.")]
|
||||
public void W80_DataRozliczeniaNierozliczonejToMaxValue()
|
||||
{
|
||||
var guid = UtworzFvWBuforze();
|
||||
var fv = Get<DokumentHandlowy>(guid);
|
||||
var p = fv.Platnosci.Cast<Platnosc>().First();
|
||||
|
||||
// Pułapka z rozdziału: MaxValue oznacza „nierozliczona”, nie traktuj go jak realnej daty.
|
||||
p.DataRozliczenia.Should().Be(Date.MaxValue,
|
||||
"nierozliczona płatność ma DataRozliczenia == Date.MaxValue");
|
||||
}
|
||||
|
||||
// ===================================================================================
|
||||
// W79 — Flaga InnyPłatnik (kalkulowana, read-only)
|
||||
// ===================================================================================
|
||||
|
||||
[Test]
|
||||
[Description("W79: dla zwykłej FV (płatnik = kontrahent) kalkulowana flaga dok.InnyPłatnik == false.")]
|
||||
public void W79_ZwyklyDokumentNieMaInnegoPlatnika()
|
||||
{
|
||||
var guid = UtworzFvWBuforze();
|
||||
var fv = Get<DokumentHandlowy>(guid);
|
||||
|
||||
// InnyPłatnik jest wyliczane z porównania Platnosc.Podmiot z dok.Kontrahent.
|
||||
// Nie ustawialiśmy odrębnego płatnika, więc flaga jest false.
|
||||
fv.InnyPłatnik.Should().BeFalse(
|
||||
"nie ustawiono płatnika innego niż kontrahent — flaga kalkulowana jest false");
|
||||
}
|
||||
|
||||
// ===================================================================================
|
||||
// SCENARIUSZE POMINIĘTE (SKIP) — uzasadnienie zgodne z treścią rozdziału
|
||||
// ===================================================================================
|
||||
|
||||
[Test]
|
||||
[Ignore("W76 — podział na raty (PodzialPlatnosciWorker). Worker jest publiczny, ale jego akcja " +
|
||||
"PodzielPlatnosci SAMA otwiera transakcję (Session.Logout(true) + CommitUI) i USUWA istniejące " +
|
||||
"płatności, zastępując je wyliczonymi ratami. Poprawne wywołanie wymaga zbudowania Context z " +
|
||||
"dokumentem, instancjacji WParams(context) i sterowania własną transakcją workera wewnątrz " +
|
||||
"harnessu testowego (który już zarządza sesją i robi rollback) — splot transakcji zewnętrznej i " +
|
||||
"wewnętrznej jest tu kruchy i wykracza poza prosty, wiarygodny przypadek. SKIP wg wytycznych " +
|
||||
"rozdziału (testuj tylko proste, jednoznaczne zachowania).")]
|
||||
[Description("W76: rozbicie płatności na raty — pominięte (worker steruje własną transakcją i usuwa płatności).")]
|
||||
public void W76_PodzialNaRaty_Skip() { }
|
||||
|
||||
[Test]
|
||||
[Ignore("W77 — ręczne dodanie płatności (new Naleznosc(dok)/Zobowiazanie(dok) + Platnosci.AddRow). " +
|
||||
"Konstruktory są publiczne, ale poprawne ułożenie płatności podlega twardym weryfikatorom: suma " +
|
||||
"Kwota wszystkich płatności musi równać się wartości brutto dokumentu, symbol waluty musi zgadzać " +
|
||||
"się z dokumentem/ewidencją, a dla przelewu wymagany jest Rachunek należący do podmiotu. " +
|
||||
"Zbudowanie spójnego, przechodzącego weryfikację układu „część gotówką + reszta przelewem” " +
|
||||
"jest zbyt złożone na prosty test jednostkowy. SKIP wg wytycznych rozdziału (zbyt złożone).")]
|
||||
[Description("W77: ręczne dodanie/edycja płatności — pominięte (twarde weryfikatory sumy/waluty/rachunku).")]
|
||||
public void W77_RecznaPlatnosc_Skip() { }
|
||||
|
||||
[Test]
|
||||
[Ignore("W81 — płatność w walucie obcej (Kwota w walucie vs PLN, Kurs, TabelaKursowa). Wymaga dokumentu " +
|
||||
"walutowego oraz tabeli kursowej z kursem na DataDokumentu. Baza Demo nie ma kursów „na dziś” " +
|
||||
"(np. EUR), więc operacja walutowa rzuca KursWalutyNotFoundException. Test wymagałby konfiguracji " +
|
||||
"kursów/ewidencji walutowej, co wykracza poza zakres rozdziału. SKIP wg pułapek W81 (brak kursu w Demo).")]
|
||||
[Description("W81: płatności walutowe — pominięte (wymaga kursu/tabeli kursowej, brak w Demo).")]
|
||||
public void W81_PlatnoscWalutowa_Skip() { }
|
||||
|
||||
[Test]
|
||||
[Ignore("W82 — rabat za wcześniejszą zapłatę (skonto). Naliczony Rabat (dok.RabatZaTerminPlatnosci.Rabat) " +
|
||||
"jest wyliczany z parametrów rabatu skonfigurowanych NA KONTRAHENCIE (RodzajRabatuZaTerminPlatnosci, " +
|
||||
"tryb, progi/wartości, IloscDniDlaRabatu). Kontrahenci bazy Demo nie mają tych parametrów ustawionych, " +
|
||||
"więc Rabat pozostałby Percent.Zero — test nie weryfikowałby realnego naliczenia. Ustawienie samego " +
|
||||
"terminu skonta wymaga ponadto, by wszystkie płatności miały ten sam termin (inaczej RowException). " +
|
||||
"SKIP wg pułapek W82 (wymaga konfiguracji rabatu na kontrahencie).")]
|
||||
[Description("W82: rabat za termin płatności (skonto) — pominięte (wymaga parametrów rabatu na kontrahencie).")]
|
||||
public void W82_RabatZaTermin_Skip() { }
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using AwesomeAssertions;
|
||||
using NUnit.Framework;
|
||||
using Soneta.Handel;
|
||||
|
||||
namespace Soneta.Skills.Test.Handel.DokumentyHandlowe;
|
||||
|
||||
/// <summary>
|
||||
/// Test dymny (smoke) weryfikujący, że infrastruktura testowa dokumentu handlowego działa:
|
||||
/// pobranie modułów i danych Demo, utworzenie dokumentu z pozycją oraz trwały zapis i ponowny odczyt.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public class SmokeTest : DokumentHandlowyTestBase
|
||||
{
|
||||
[Test]
|
||||
[Description("Tworzy przyjęcie wewnętrzne (PW) z jedną pozycją i potwierdza trwały zapis.")]
|
||||
public void TworzyDokumentZPozycja_ZapisujeTrwale()
|
||||
{
|
||||
var dok = UtworzDokument(Definicje.PrzyjecieWewnetrzne, magazyn: Magazyn(Magazyn_.Firma));
|
||||
InTransaction(() => DodajPozycje(dok, Towar(Towar_.Bikini), ilosc: 10, cena: 5));
|
||||
var guid = dok.Guid;
|
||||
SaveDispose();
|
||||
|
||||
var zapisany = Get<DokumentHandlowy>(guid);
|
||||
zapisany.Should().NotBeNull();
|
||||
zapisany.Pozycje.Count.Should().Be(1);
|
||||
}
|
||||
}
|
||||
@@ -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 C2–C7 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 „od–do"
|
||||
|
||||
// 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 D3–D12).
|
||||
/// <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 od–do.
|
||||
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 H5–H11).
|
||||
/// <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 scope’u 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 (28–31)
|
||||
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<T></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 J1–J6).
|
||||
/// <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 (J1–J5) 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 J1–J5 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 K1–K5).
|
||||
/// <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 K6–K9).
|
||||
/// <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");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
using NUnit.Framework;
|
||||
|
||||
// Testy Soneta bazują na TestBase/SessionState, które są single-threaded (stan sesji jest
|
||||
// przypięty do wątku). Uruchamianie testów równolegle powoduje kolizję „Ponowne podłączenie
|
||||
// stanu sesji". Wymuszamy wykonanie sekwencyjne (jeden worker, brak równoległości).
|
||||
[assembly: LevelOfParallelism(1)]
|
||||
[assembly: Parallelizable(ParallelScope.None)]
|
||||
@@ -6,6 +6,10 @@
|
||||
<ProjectReference Include="..\Soneta.Business\Soneta.Business.csproj" />
|
||||
<ProjectReference Include="..\Soneta.Core\Soneta.Core.csproj" />
|
||||
<ProjectReference Include="..\Soneta.CRM\Soneta.CRM.csproj" />
|
||||
<ProjectReference Include="..\Soneta.Deklaracje\Soneta.Deklaracje.csproj" />
|
||||
<ProjectReference Include="..\Soneta.Handel\Soneta.Handel.csproj" />
|
||||
<ProjectReference Include="..\Soneta.KadryPlace\Soneta.KadryPlace.csproj" />
|
||||
<ProjectReference Include="..\Soneta.KadryPlace.UI\Soneta.KadryPlace.UI.csproj" />
|
||||
<ProjectReference Include="..\Soneta.Kasa\Soneta.Kasa.csproj" />
|
||||
<ProjectReference Include="..\Soneta.Test\Soneta.Test.csproj" />
|
||||
<ProjectReference Include="..\Soneta.Types\Soneta.Types.csproj" />
|
||||
|
||||
Reference in New Issue
Block a user