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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user