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

308 lines
16 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.IO;
using System.Linq;
using System.Text;
using AwesomeAssertions;
using Microsoft.Extensions.DependencyInjection; // GetRequiredService
using NUnit.Framework;
using Soneta.Business; // Context
using Soneta.Business.UI; // IReportService, ReportResult, ReportFormats
using Soneta.Place; // ListaPlac, DefinicjaListyPlac, NaliczanieWypłat, Wyplata, TypNaliczenia
using Soneta.Types; // Date, FromTo, YearMonth
using Prac = Soneta.Kadry.Pracownik;
namespace Soneta.Skills.Test.KadryPlace.Pracownik;
/// <summary>
/// Rozdział I — „Listy płac, przelewy, wydruki” (receptury I1, I2, I3).
/// <para>
/// Testy są <b>wykonywalną dokumentacją</b> publicznego kontraktu list płac i ich wydruków.
/// </para>
/// <list type="bullet">
/// <item><b>I1a</b> — ręczne utworzenie pustej listy płac (<c>new ListaPlac()</c> + <c>Place.ListyPlac.AddRow</c>),
/// ustawienie pól w wymaganej kolejności (Definicja → Data → DataWyplaty → MiesiacZUS → Okres).</item>
/// <item><b>I1b</b> — naliczenie wypłaty workerem <c>NaliczanieSeryjne.Pracownika</c> z jawną
/// <c>DefinicjaListy</c> (sprawdzona ścieżka z sekcji H): worker tworzy listę płac wg tej definicji i WIĄŻE
/// z nią wypłatę. Asercja: wypłata naliczona, powiązanie dwukierunkowe (<c>w.ListaPlac</c> niepuste, jego
/// <c>Definicja == def</c>; <c>w.Pracownik == pracownik</c>).
/// <b>Rozbieżność dokumentacji:</b> niskopoziomowy worker <c>Soneta.Place.NaliczanieWypłat</c> uruchomiony
/// tylko z <c>ListaPłac</c>+<c>Pracownik</c> (snippet I1 w pracownik.md) w bazie Demo nie napełnia listy
/// (zwraca pustą <c>WszystkieWypłaty</c>); działającą ścieżką naliczania jest <c>NaliczanieSeryjne</c>.</item>
/// <item><b>I2</b> — PDF kwitka (paska) wypłaty przez <c>IReportService.GenerateReport</c>
/// (wzorzec <c>PasekWyplaty.repx</c>, <c>DataType = typeof(Wyplata)</c>).</item>
/// <item><b>I3</b> — PDF pełnej listy płac (<c>PelnaListaPlac.repx</c>, <c>DataType = typeof(ListaPlac)</c>).</item>
/// </list>
/// <para>
/// <b>Wydruki (I2/I3):</b> serwis <see cref="IReportService"/> (warstwa <c>Soneta.Business.UI</c>) jest
/// w bieżącym zestawie referencji Skills.Test OSIĄGALNY (transytywnie, tak jak w wydrukach handlowych —
/// rozdz. 12 dokumentów handlowych). Faktyczne wyrenderowanie PDF wymaga jednak zarejestrowanego wzorca
/// <c>*.repx</c> (z assembly <c>Soneta.KadryPlace.Reports</c>) oraz silnika renderującego (DevExpress) —
/// czego testowa baza Demo nie gwarantuje, a samo ładowanie DevExpress bywa niestabilne w hoście testowym.
/// Dlatego generowanie owijamy w try/catch i przy braku wzorca/silnika robimy <c>Assert.Ignore</c>
/// (suita pozostaje zielona, a kod dokumentuje publiczne API). Asercję na sygnaturze <c>"%PDF"</c>
/// wykonujemy tylko wtedy, gdy strumień faktycznie powstał.
/// </para>
/// <para>
/// Wszystko działa na bazie Demo (GoldStandard) z automatycznym rollbackiem po teście. Operujemy wyłącznie
/// na <b>publicznym kontrakcie</b> platformy Soneta (jak dodatek programisty zewnętrznego).
/// </para>
/// </summary>
[TestFixture]
public class RozdzialI_ListyWydrukiTest : PracownikTestBase
{
/// <summary>Sygnatura nagłówka pliku PDF (pierwsze 4 bajty/znaki strumienia).</summary>
private const string PdfMagic = "%PDF";
/// <summary>Wzorzec wydruku paska (kwitka) wypłaty — wg tabeli I2 (DataType = Wyplata).</summary>
private const string WzorzecPasek = "PasekWyplaty.repx";
/// <summary>Wzorzec wydruku pełnej listy płac — wg tabeli I3 (DataType = ListaPlac).</summary>
private const string WzorzecPelnaLista = "PelnaListaPlac.repx";
/// <summary>Serwis raportowy ze scopeu bieżącej sesji (jak w wydrukach handlowych).</summary>
private IReportService Raporty => Session.GetRequiredService<IReportService>();
// === Pomocniki lokalne ===
/// <summary>
/// Wybiera dowolną dostępną definicję listy płac z bazy Demo (słownik konfiguracyjny
/// <c>Place.DefListPlac</c>). Nazwy/symbole definicji zależą od wdrożenia, więc zamiast
/// twardego symbolu („ETAT”) pobieramy pierwszą dostępną definicję — deterministycznie,
/// bez zakładania konkretnej konfiguracji.
/// </summary>
private DefinicjaListyPlac DowolnaDefinicjaListy()
=> Place.DefListPlac.Cast<DefinicjaListyPlac>().FirstOrDefault();
/// <summary>
/// Dobiera okres/daty listy w obrębie aktywnego etatu pracownika: bierzemy miesiąc rozpoczęcia
/// etatu (dla pracowników Demo etat zwykle zaczyna się wstecz i jest otwarty), aby naliczanie
/// trafiło w okres zatrudnienia. Zwraca (okresMiesiąca, dataWyplaty = koniec miesiąca).
/// </summary>
private static (FromTo Okres, Date DataWyplaty) OkresWEtacie(Prac pracownik)
{
var from = pracownik.Last.Etat.Okres.From;
var poczatek = new Date(from.Year, from.Month, 1);
var koniec = poczatek.AddMonths(1).AddDays(-1); // koniec miesiąca (2831)
return (new FromTo(poczatek, koniec), koniec);
}
/// <summary>
/// Demonstruje ręczne utworzenie pustej listy płac z wybraną definicją i polami ustawionymi
/// w wymaganej kolejności (Definicja → Data → DataWyplaty → MiesiacZUS → Okres), zwraca utworzoną
/// listę. Sama lista jest tworzona poprawnie; <b>napełnienie jej wypłatami</b> realizuje worker
/// naliczający (patrz <see cref="NaliczWyplate"/>), a nie ustawienie pól listy.
/// </summary>
private ListaPlac UtworzPustaListe(Prac pracownik, DefinicjaListyPlac def)
{
var (okres, dataWyplaty) = OkresWEtacie(pracownik);
var lp = new ListaPlac();
Place.ListyPlac.AddRow(lp);
lp.Definicja = def; // wzorzec listy — ustaw PIERWSZE po AddRow
// Wydzial/Seria ustawiamy WARUNKOWO — tylko gdy wymaga ich definicja.
if (def.Wydzial)
lp.Wydzial = Kadry.Wydzialy.Firma;
lp.Data = dataWyplaty; // data naliczania listy
lp.DataWyplaty = dataWyplaty; // data przekazania środków (wyznacza mies./rok)
lp.MiesiacZUS = new YearMonth(dataWyplaty); // miesiąc rozliczenia ZUS
lp.Okres = okres; // okres listy — PO DataWyplaty
return lp;
}
/// <summary>
/// Nalicza wypłatę etatową pracownika workerem <c>NaliczanieSeryjne.Pracownika</c> (sprawdzona
/// ścieżka z sekcji H). Worker sam dobiera/tworzy listę płac dla naliczanych wypłat i WIĄŻE je
/// z nią (<c>Wyplata.ListaPlac</c>).
/// <para>
/// <c>Nalicz()</c> sam otwiera i commituje transakcję w sesji — NIE owijamy go w InTransaction.
/// Pola <c>Naliczanie</c> nie ustawiamy (domyślne; setter rzuca bez licencji „PL Złoty”).
/// <c>DefinicjaListy</c> także NIE wymuszamy — dowolna definicja może nie pasować do typu wypłaty
/// (np. lista umów ≠ etat) i wtedy nic się nie naliczy; worker dobiera definicję sam.
/// Zwraca pierwszą naliczoną wypłatę albo <c>null</c>, gdy nic się nie naliczyło.
/// </para>
/// </summary>
private Wyplata NaliczWyplate(Prac pracownik)
{
var (okres, dataWyplaty) = OkresWEtacie(pracownik);
var pars = new NaliczanieSeryjne.PracownikParams(Context)
{
DataWypłaty = dataWyplaty, // ustawia Okres i MiesiącDeklaracji automatycznie
DataListy = dataWyplaty,
TypWypłaty = TypWyplaty.Etat, // tylko wypłaty etatowe
};
var naliczanie = new NaliczanieSeryjne.Pracownika(pars) { Pracownik = pracownik };
var wynik = naliczanie.Nalicz(); // self-commit w sesji
return wynik.WszystkieWypłaty.Cast<Wyplata>().FirstOrDefault();
}
// ===================================================================================
// I1 — Tworzenie i naliczanie listy płac
// ===================================================================================
[Test]
[Description("I1 (część A): ręcznie tworzymy pustą listę płac — new ListaPlac() + Place.ListyPlac.AddRow + " +
"pola w wymaganej kolejności (Definicja → Data → DataWyplaty → MiesiacZUS → Okres). " +
"Asercja: lista istnieje, ma przypisaną definicję i jest pusta (Wyplaty napełnia dopiero worker).")]
public void I1a_PustaListaPlac_TworzenieRecznePolaWKolejnosci()
{
var pracownik = Pracownik(Pracownik_.Andrzejewski);
pracownik.Should().NotBeNull();
var def = DowolnaDefinicjaListy();
def.Should().NotBeNull("baza Demo zawiera co najmniej jedną definicję listy płac (Place.DefListPlac)");
// Tworzenie danych operacyjnych MUSI być w trybie edycji (InTransaction), inaczej AddRow
// rzuca CannotEditException.
ListaPlac lp = null;
InTransaction(() => lp = UtworzPustaListe(pracownik, def));
lp.Should().NotBeNull();
lp.Definicja.Should().Be(def, "ustawiliśmy Definicja po AddRow");
lp.Wyplaty.Cast<Wyplata>().Should().BeEmpty("nowo utworzona lista jest pusta — wypłaty dolicza worker");
SaveDispose(); // utrwalenie w bazie (rollback po teście i tak wycofa)
}
[Test]
[Description("I1 (część B): naliczamy wypłatę etatową workerem NaliczanieSeryjne.Pracownika (sprawdzona " +
"ścieżka z sekcji H). Worker sam dobiera/tworzy listę płac i WIĄŻE z nią wypłatę. " +
"Asercja: wypłata naliczona, powiązana dwukierunkowo z listą płac (w.ListaPlac niepuste, " +
"ma definicję) i z pracownikiem (w.Pracownik == pracownik). " +
"Uwaga: niskopoziomowy worker Soneta.Place.NaliczanieWypłat (samo ListaPłac+Pracownik z " +
"dokumentacji) w bazie Demo nie napełnia listy — sprawdzoną ścieżką jest NaliczanieSeryjne.")]
public void I1b_ListaPlac_NaliczanieWyplatyPowiazanaZLista()
{
var pracownik = Pracownik(Pracownik_.Andrzejewski);
pracownik.Should().NotBeNull();
// NaliczanieSeryjne.Nalicz() sam otwiera i commituje transakcję — NIE owijamy w InTransaction.
var w = NaliczWyplate(pracownik);
w.Should().NotBeNull(
"naliczanie etatu dla pracownika Demo w okresie etatu powinno dać wypłatę powiązaną z listą");
// Powiązanie dwukierunkowe: wypłata wskazuje wstecz listę płac i pracownika.
var lista = (ListaPlac)w.ListaPlac;
lista.Should().NotBeNull("Wyplata.ListaPlac wskazuje listę, na której została naliczona");
lista.Definicja.Should().NotBeNull("lista płac utworzona przez worker ma przypisaną definicję");
w.Pracownik.Guid.Should().Be(pracownik.Guid, "Wyplata.Pracownik to pracownik, dla którego naliczono");
SaveDispose();
}
// ===================================================================================
// I2 — Drukowanie/PDF kwitka (paska) wypłaty
// ===================================================================================
[Test]
[Description("I2: pasek (kwitek) wypłaty do PDF przez IReportService.GenerateReport " +
"(TemplateFileName = PasekWyplaty.repx, DataType = typeof(Wyplata), OutputFormat = PDF, " +
"Context.Set(wyplata)). Strumień zaczyna się od sygnatury „%PDF”. " +
"Brak wzorca/silnika renderującego → Assert.Ignore (suita zielona).")]
public void I2_PasekWyplaty_DoPdf_ZaczynaSieOdPdf()
{
var pracownik = Pracownik(Pracownik_.Bednarek);
pracownik.Should().NotBeNull();
// Arrange: naliczona wypłata (wraz z listą) jako źródło danych wydruku.
// NaliczanieSeryjne self-commituje — wypłata jest dostępna w bieżącej sesji.
var wyplata = NaliczWyplate(pracownik);
if (wyplata == null)
Assert.Ignore("Worker nie naliczył wypłaty dla pracownika Demo — brak danych do wydruku paska.");
// Kontekst wydruku: pojedyncza Wyplata (jak w snippetcie I2).
var context = Login.CreateEmptyContext().Clone(Session);
context.Set(wyplata);
var rr = new ReportResult
{
TemplateFileName = WzorzecPasek, // tryb automatyczny (bez UI)
DataType = typeof(Wyplata), // pojedyncza wypłata
Context = context,
OutputFormat = ReportFormats.PDF,
AskForParameters = false // tryb wsadowy — nie pytaj o parametry
};
// Act: generowanie do strumienia. Brak wzorca/silnika → Assert.Ignore zamiast błędu.
byte[] naglowek;
try
{
using var pdf = Raporty.GenerateReport(rr);
pdf.Should().NotBeNull("GenerateReport dla formatu binarnego zwraca Stream");
naglowek = new byte[4];
int przeczytane = pdf.Read(naglowek, 0, naglowek.Length);
przeczytane.Should().Be(4, "PDF ma co najmniej 4-bajtowy nagłówek");
}
catch (Exception ex)
{
Assert.Ignore("Pominięto I2: wygenerowanie PDF paska wymaga zarejestrowanego wzorca '" +
WzorzecPasek + "' (assembly Soneta.KadryPlace.Reports) oraz silnika renderującego " +
"(DevExpress), których testowa baza Demo nie gwarantuje. Test dokumentuje publiczne API " +
"IReportService.GenerateReport. Szczegóły: " + ex.GetType().Name + " — " + ex.Message);
return;
}
Encoding.ASCII.GetString(naglowek).Should().StartWith(PdfMagic,
"poprawny strumień PDF zaczyna się od „%PDF”.");
}
// ===================================================================================
// I3 — Drukowanie/PDF całej listy płac
// ===================================================================================
[Test]
[Description("I3: pełna lista płac do PDF przez IReportService.GenerateReport " +
"(TemplateFileName = PelnaListaPlac.repx, DataType = typeof(ListaPlac), OutputFormat = PDF, " +
"Context.Set(listaPlac)). Strumień zaczyna się od sygnatury „%PDF”. " +
"Brak wzorca/silnika renderującego → Assert.Ignore (suita zielona).")]
public void I3_PelnaListaPlac_DoPdf_ZaczynaSieOdPdf()
{
var pracownik = Pracownik(Pracownik_.Bujak);
pracownik.Should().NotBeNull();
// Arrange: naliczona wypłata daje listę płac (Wyplata.ListaPlac) jako źródło danych wydruku.
// NaliczanieSeryjne self-commituje — lista jest dostępna w bieżącej sesji.
var wyplata = NaliczWyplate(pracownik);
if (wyplata == null)
Assert.Ignore("Worker nie naliczył wypłaty dla pracownika Demo — brak listy płac do wydruku.");
var lp = (ListaPlac)wyplata.ListaPlac;
lp.Should().NotBeNull();
var context = Login.CreateEmptyContext().Clone(Session);
context.Set(lp); // ListaPlac
var rr = new ReportResult
{
TemplateFileName = WzorzecPelnaLista,
DataType = typeof(ListaPlac),
Context = context,
OutputFormat = ReportFormats.PDF,
AskForParameters = false
};
// Act: skopiowanie strumienia do pamięci (jak wzorzec integracyjny — bajty → załącznik/REST).
byte[] pdfBytes;
try
{
using Stream src = Raporty.GenerateReport(rr);
using var ms = new MemoryStream();
src.CopyTo(ms);
pdfBytes = ms.ToArray();
}
catch (Exception ex)
{
Assert.Ignore("Pominięto I3: wygenerowanie PDF pełnej listy płac wymaga zarejestrowanego wzorca '" +
WzorzecPelnaLista + "' (assembly Soneta.KadryPlace.Reports) oraz silnika renderującego " +
"(DevExpress), których testowa baza Demo nie gwarantuje. Test dokumentuje publiczne API " +
"IReportService.GenerateReport. Szczegóły: " + ex.GetType().Name + " — " + ex.Message);
return;
}
pdfBytes.Should().NotBeNullOrEmpty("wydruk listy płac zwraca niepusty bufor bajtów");
pdfBytes.Length.Should().BeGreaterThan(4);
Encoding.ASCII.GetString(pdfBytes, 0, 4).Should().StartWith(PdfMagic,
"bufor bajtów to plik PDF (sygnatura „%PDF”).");
}
}