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; /// /// Rozdział 9 skilla „dokument-handlowy” — Korekty i dokumenty specjalne (W48–W52). /// /// Rozdział obejmuje korekty (przez serwis relacji .NowaKorekta), /// inwentaryzację (INW) oraz przesunięcie międzymagazynowe (MM). Wszystkie testy operują /// wyłącznie na publicznym kontrakcie platformy — jak dodatek programisty zewnętrznego. /// /// /// Reguły wspólne (zob. dokumentacja, rozdz. 9 oraz safe-code.md): /// /// dokument korygowany / nadrzędny musi być zatwierdzony przed wywołaniem relacji, /// relacja to operacja modyfikująca — wykonujemy ją w transakcji edycyjnej /// (Session.Logout(editMode: true)), po niej Session.Save(), /// magazyn księguje obroty/zasoby dopiero po Session.Save(), nie po Commit(), /// Demo blokuje stan ujemny (StanUjemnyVerifier) — rozchód wymaga wcześniejszego, /// zapisanego przyjęcia (PW) tego towaru, /// pola DokumentKorygowany, DokumentyKorygującekalkulowane (read-only) — /// czytamy je, nie ustawiamy; powstają jako efekt utworzenia relacji. /// /// /// Tam, gdzie definicja relacji w Demo wymaga rozstrzygnięcia niedostarczalnego czystym /// publicznym API (np. callback w HandlerSet), test rozpoznaje /// i jest pomijany (Assert.Ignore) z czytelnym powodem — /// to nie błąd testu, lecz ograniczenie kontraktu/konfiguracji. /// [TestFixture] public class Rozdzial09_KorektyTest : DokumentHandlowyTestBase { // === Pomocnicze === /// Serwis relacji bieżącej sesji (rzuca, gdy serwisu brak). private IRelacjeService Relacje => Session.GetRequiredService(); /// Zmienia stan dokumentu na zatwierdzony (w transakcji edycyjnej). private void Zatwierdz(DokumentHandlowy dok) { InTransaction(() => dok.Stan = StanDokumentuHandlowego.Zatwierdzony); } /// /// Wprowadza towar magazynowy na stan: tworzy i ZAPISUJE przyjęcie wewnętrzne (PW). /// Magazyn księguje się dopiero po Session.Save() — warunek konieczny rozchodu (Demo blokuje stan ujemny). /// Save bez Dispose: kontynuujemy pracę na tej samej sesji. /// 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() .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() { } }