using System.Linq; using AwesomeAssertions; using NUnit.Framework; using Soneta.Handel; using Soneta.Kasa; using Soneta.Types; namespace Soneta.Skills.Test.Handel.DokumentyHandlowe; /// /// Rozdział 14 skilla „dokument-handlowy” — Płatności dokumentu handlowego (W75–W82). /// /// Płatności (należności / zobowiązania) powstają automatycznie z dokumentu handlowego /// płatnego (FV, FZ). Dostęp daje kolekcja dok.Platnosci /// (SubTable<Soneta.Kasa.Platnosc>). Testy weryfikują przede wszystkim /// odczyt: istnienie płatności, kwotę, sposób zapłaty, termin, stan rozliczenia /// oraz kalkulowaną flagę dok.InnyPłatnik. /// /// /// Klucz rozdziału: faktura sprzedaży to rozchód magazynowy — w bazie Demo /// StanUjemnyVerifier wymaga wcześniejszego zapisanego przyjęcia (PW) towaru. /// Dlatego najpierw tworzymy i zapisujemy PW na stan, dopiero potem FV z pozycją. Magazyn /// księguje się po Session.Save(); po Save() w środku testu okno edycji się /// zamyka, więc dokument odczytujemy na świeżej sesji przez Get<T>(guid). /// /// Cały kod operuje wyłącznie na publicznym kontrakcie platformy Soneta. /// [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; /// /// Tworzy fakturę sprzedaży (FV) z jedną pozycją BIKINI i zapisuje ją w buforze. /// Wymaga wcześniej ZATWIERDZONEGO i zapisanego przyjęcia (stan towaru) — robi to bazowy /// helper PrzyjmijNaStan (tworzy i zatwierdza PW, dopiero to księguje stan; bez tego /// Demo odrzuca rozchód FV przez kontrolę stanu ujemnego). Zwraca Guid zapisanej FV. /// /// Świadomie NIE zatwierdzamy FV: w testowej bazie Demo zatwierdzenie faktury sprzedaży /// rzuca NullReferenceException w ewidencji VAT. Płatności (Należność), Suma i /// pozostałe pola są już wyliczone na dokumencie w buforze, więc asercje robimy na FV w buforze. /// /// 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(guid); fv.Should().NotBeNull(); // dok.Platnosci to SubTable; iterujemy serwerowo i materializujemy do listy do asercji. var platnosci = fv.Platnosci.Cast().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(guid); // Bierzemy pierwszą (zwykle jedyną) płatność faktury. var p = fv.Platnosci.Cast().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(guid); var p = fv.Platnosci.Cast().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(guid); // Płatność podlegająca rozliczeniu (Rozliczana == true) i bez żadnych zapłat. var p = fv.Platnosci.Cast().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(guid); var p = fv.Platnosci.Cast().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(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() { } }