257 lines
15 KiB
C#
257 lines
15 KiB
C#
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() { }
|
||
}
|