using System; using System.Linq; using AwesomeAssertions; using NUnit.Framework; using Soneta.Handel; using Soneta.Types; namespace Soneta.Skills.Test.Handel.DokumentyHandlowe; /// /// Rozdział 8 skilla „dokument-handlowy” — VAT, wartości i waluty (W43–W47). /// /// Testy weryfikują publiczny kontrakt dokumentu w zakresie tabeli VAT (dok.SumyVAT), /// podsumowań wartości (dok.Suma, dok.SumaPozycji), ręcznej korekty VAT /// (dok.KorektaVAT), sposobu liczenia VAT (dok.LiczonaOd) oraz — w zakresie, w jakim /// nie wymaga to sieci/kursu — zmiany waluty dokumentu (W47). /// /// /// Reguły bazy Demo, których trzymają się testy: /// /// Demo blokuje stan ujemny (StanUjemnyVerifier): rozchód (FV) wymaga wcześniej /// zapisanego przyjęcia (PW) tego towaru. Magazyn księguje się dopiero po Session.Save(). /// Po zapisie w środku testu sesja zamyka okno edycji — kolejna edycja rzuca wyjątek. /// Wzorzec: zapis przez SaveDispose() → odczyt na świeżej sesji po Guid. /// /// Wartości pieniężne tabeli VAT i podsumowań mają dwie reprezentacje: BruttoNetto /// (Netto/VAT/Brutto jako decimal, waluta systemowa) oraz /// BruttoNettoCy (NettoCy/VatCy/BruttoCy jako Currency, waluta dokumentu). /// /// Cały kod operuje wyłącznie na publicznym kontrakcie platformy Soneta. /// [TestFixture] public class Rozdzial08_VatWalutyTest : DokumentHandlowyTestBase { /// /// Przyjmuje BIKINI na magazyn „F” dokumentem PW, zatwierdza 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 . /// 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); } /// /// 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. /// 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(guidFv); dok.Should().NotBeNull(); // dok.SumyVAT to SubTable — jedna pozycja na każdą stawkę dokumentu. var sumy = dok.SumyVAT.Cast().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(guidFv); var pierwszy = dok.SumyVAT.Cast().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(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(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(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(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(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(guidFv); dok.LiczonaOd.Should().Be(SposobLiczeniaVAT.OdBrutto, "dokument liczony od kwot brutto"); // Tabela VAT wyliczona także dla liczenia od brutto. dok.SumyVAT.Cast().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() { } }