Files
2026-06-06 22:33:15 +02:00

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