Files
soneta-erp-skills/Soneta.Skills.Test/Handel/DokumentyHandlowe/Rozdzial06_MagazynTest.cs
T
2026-06-06 22:33:15 +02:00

257 lines
15 KiB
C#
Raw Blame History

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