Soneta.Skills.Test

This commit is contained in:
Marcin Wojas
2026-06-06 22:33:15 +02:00
parent d42ca3e825
commit fb2f2695a3
38 changed files with 10644 additions and 0 deletions
@@ -0,0 +1,415 @@
using System;
using System.Linq;
using AwesomeAssertions;
using NUnit.Framework;
using Soneta.Handel;
using Soneta.Types;
namespace Soneta.Skills.Test.Handel.DokumentyHandlowe;
/// <summary>
/// Rozdział 2 — „Wystawianie dokumentów” (wzorce W4W11).
/// <para>
/// Testy pokazują tworzenie dokumentu handlowego od zera w różnych wariantach: faktura sprzedaży (FV),
/// faktura zakupu (FZ — numer obcy i daty), dokument magazynowy (PW/PZ), zamówienie odbiorcy (ZO),
/// dodawanie pozycji (towar/ilość/cena/rabat), dokument z usługą (MONTAZ — bez magazynu),
/// dokument w walucie obcej (W9) oraz odbiorca inny niż kontrahent (W11).
/// </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/WZ) wymaga wcześniej
/// <b>zapisanego</b> przyjęcia (PW/PZ) tego towaru. Obroty księgują się dopiero po <c>Session.Save()</c>.</item>
/// <item>Po zapisie w środku testu sesja zamyka okno edycji — kolejna edycja rzuca wyjątek.
/// Dlatego wzorzec to: zapis przez <c>SaveDispose()</c> → odczyt na świeżej sesji po <c>Guid</c>.</item>
/// </list>
/// Wszystko operuje wyłącznie na publicznym kontrakcie platformy (jak dodatek programisty zewnętrznego).
/// </para>
/// </summary>
[TestFixture]
public class Rozdzial02_WystawianieTest : DokumentHandlowyTestBase
{
/// <summary>
/// Pomocniczo: przyjmuje BIKINI na magazyn „F” dokumentem PW, <b>zatwierdza</b> je i <b>zapisuje</b>,
/// żeby zbudować stan magazynu pod późniejszy rozchód (FV/WZ). Dopiero ZATWIERDZONE i zapisane
/// przyjęcie księguje zasoby/obroty i odblokowuje rozchód na bazie Demo (kontrola stanu ujemnego).
/// Korzysta z bazowego helpera <see cref="DokumentHandlowyTestBase.PrzyjmijNaStan"/>. Zwraca Guid PW.
/// </summary>
private Guid PrzyjmijBikiniNaStan(double ilosc = 100, double cena = 25)
=> PrzyjmijNaStan(Towar_.Bikini, ilosc, cena);
// ============================== W4 — Faktura sprzedaży (FV) ==============================
[Test]
[Description("W4: FV krajowa od netto z pozycją BIKINI — po zapisie powstaje tabela VAT i wartość dokumentu.")]
public void FakturaSprzedazy_OdNetto_WyliczaSumeIVat()
{
// Najpierw przyjęcie na stan (zapisane) — inaczej rozchód FV zablokuje kontrola stanu ujemnego.
PrzyjmijBikiniNaStan();
Guid guidFv = Guid.Empty;
// Definicja FIRST (helper UtworzDokument), potem magazyn i kontrahent-nabywca.
var fv = UtworzDokument(
Definicje.FakturaSprzedazy,
kontrahent: Kontrahent(Kontrahent_.Abc),
magazyn: Magazyn(Magazyn_.Firma));
// FV NIE zatwierdzamy — zatwierdzenie FV w bazie testowej Demo rzuca NRE w ewidencji VAT.
// SumyVAT/Suma na świeżym dokumencie w pamięci bywają niprzeliczone — przeliczają się
// po zapisie. Dlatego zapisujemy FV w BUFORZE (bez zatwierdzania) i czytamy po Guid.
InTransaction(() =>
{
fv.Data = Date.Today; // data wystawienia
fv.DataOperacji = Date.Today; // faktyczna data sprzedaży
fv.LiczonaOd = SposobLiczeniaVAT.OdNetto; // ustaw przed pozycjami
DodajPozycje(fv, Towar(Towar_.Bikini), 2, 50); // 2 szt po 50
guidFv = fv.Guid;
});
SaveDispose();
var zapis = Get<DokumentHandlowy>(guidFv);
zapis.Should().NotBeNull();
zapis.LiczonaOd.Should().Be(SposobLiczeniaVAT.OdNetto);
// SumyVAT i Suma są wyliczane z pozycji — wyliczone po zapisie (czytamy po Guid).
zapis.SumyVAT.Should().NotBeEmpty();
// Wartość netto jest dodatnia (kontrahent Abc ma rabat, więc netto może być < cena*ilość).
((double)zapis.Suma.Netto).Should().BeGreaterThan(0);
}
[Test]
[Description("W4: FV liczona od brutto — pole LiczonaOd przyjmuje wartość Brutto.")]
public void FakturaSprzedazy_OdBrutto_UstawiaLiczonaOdBrutto()
{
PrzyjmijBikiniNaStan();
var fv = UtworzDokument(
Definicje.FakturaSprzedazy,
kontrahent: Kontrahent(Kontrahent_.Abc),
magazyn: Magazyn(Magazyn_.Firma));
// Asercja na FV w BUFORZE (nie zatwierdzamy FV — zatwierdzenie rzuca NRE w ewidencji VAT).
InTransaction(() =>
{
// LiczonaOd ustawiamy PRZED pozycjami — zmiana po wprowadzeniu pozycji wymusza przeliczenie cen.
fv.LiczonaOd = SposobLiczeniaVAT.OdBrutto;
DodajPozycje(fv, Towar(Towar_.Bikini), 1, 50);
});
fv.LiczonaOd.Should().Be(SposobLiczeniaVAT.OdBrutto);
}
// ============================== W5 — Zakup od dostawcy (PZ) ==============================
[Test]
[Description("W5: zakup od dostawcy (PZ) z datą operacji (zakupu) różną od daty wystawienia — przyjęcie zewnętrzne, przychód.")]
public void FakturaZakupu_UstawiaNumerObcyIDatyZakupu()
{
// W bazie Demo „faktura zakupu" jako dokument handlowy nie istnieje — stronę zakupową
// reprezentuje przyjęcie zewnętrzne „PZ" (przychód, kontrahent-dostawca). PZ NIE wywołuje
// kontroli stanu ujemnego, więc nie potrzebuje wcześniejszego przyjęcia.
Guid guidPz = Guid.Empty;
var dataWystawienia = Date.Today;
var dataZakupu = Date.Today.AddDays(-2);
// PZ to dokument przychodowy — kontrahent jest dostawcą.
var pz = UtworzDokument(
Definicje.FakturaZakupu,
kontrahent: Kontrahent(Kontrahent_.Abc),
magazyn: Magazyn(Magazyn_.Firma));
InTransaction(() =>
{
pz.Data = dataWystawienia; // data wystawienia u nas
pz.DataOperacji = dataZakupu; // faktyczna data zakupu (decyduje o okresie magazynowym)
DodajPozycje(pz, Towar(Towar_.Bikini), 10, 30);
guidPz = pz.Guid;
});
// Bez zatwierdzania — sprawdzamy podstawowe pola dokumentu zakupowego (PZ).
SaveDispose();
var zapis = Get<DokumentHandlowy>(guidPz);
zapis.Should().NotBeNull();
zapis.Definicja.Symbol.Should().Be("PZ");
zapis.Kontrahent.Kod.Should().Be(Kontrahent(Kontrahent_.Abc).Kod);
zapis.DataOperacji.Should().Be(dataZakupu);
zapis.Data.Should().Be(dataWystawienia);
// Data operacji (zakupu) różna od daty wystawienia — to dwa odrębne pola.
zapis.DataOperacji.Should().NotBe(zapis.Data);
}
[Test]
[Description("W5: zakup od dostawcy (PZ) z przyjęciem na magazyn księguje przychód — po zatwierdzeniu i Save powstają zasoby dokumentu.")]
public void FakturaZakupu_KsiegujePrzychod_TworzyZasoby()
{
Guid guidPz = Guid.Empty;
// PZ (przyjęcie zewnętrzne od dostawcy) to dokument przychodowy — kontrahent jest dostawcą.
var pz = UtworzDokument(
Definicje.FakturaZakupu,
kontrahent: Kontrahent(Kontrahent_.Abc),
magazyn: Magazyn(Magazyn_.Firma));
InTransaction(() =>
{
pz.Data = Date.Today;
pz.DataOperacji = Date.Today;
DodajPozycje(pz, Towar(Towar_.Bikini), 5, 30);
guidPz = pz.Guid;
});
// Zasoby dokumentu przychodowego księgują się DOPIERO po zatwierdzeniu + Save.
// Zatwierdzenie PZ (jak PW) jest bezpieczne — nie rzuca NRE (rzuca tylko zatwierdzenie FV).
InTransaction(() => pz.Stan = StanDokumentuHandlowego.Zatwierdzony);
SaveDispose();
var zapis = Get<DokumentHandlowy>(guidPz);
// PZ (przyjęcie od dostawcy) jest dokumentem przychodowym → powstają zasoby magazynowe.
zapis.Zasoby.Cast<object>().Should().NotBeEmpty();
}
// ============================== W6 — Dokument magazynowy (PW/PZ) ==============================
[Test]
[Description("W6: PW (przyjęcie wewnętrzne) buduje stan magazynu — po Save powstają zasoby.")]
public void PrzyjecieWewnetrzne_PW_TworzyZasoby()
{
// PW jest dokumentem wewnętrznym (przychód) — bez kontrahenta, magazyn wymagany.
var guidPw = PrzyjmijBikiniNaStan(50, 25);
var zapis = Get<DokumentHandlowy>(guidPw);
zapis.Should().NotBeNull();
// Kierunek magazynu wynika z definicji (readonly="set"), nie ustawiamy go ręcznie.
zapis.Zasoby.Cast<object>().Should().NotBeEmpty();
}
[Test]
[Description("W6: dokument magazynowy bez magazynu — Save rzuca wyjątek (Magazyn jest wymagany).")]
public void DokumentMagazynowy_BezMagazynu_RzucaPrzyZapisie()
{
// Brak wymaganego magazynu → operacja musi się nie powieść. Wyjątek może paść już
// przy dodaniu pozycji/edycji albo dopiero przy Save — łapiemy całą sekwencję, żeby
// asercja była odporna na moment zgłoszenia (RequiredException / walidacja magazynu).
Action buildIZapisz = () =>
{
var pw = UtworzDokument(Definicje.PrzyjecieWewnetrzne);
InTransaction(() => DodajPozycje(pw, Towar(Towar_.Bikini), 1, 10));
SaveDispose();
};
buildIZapisz.Should().Throw<Exception>();
}
[Test]
[Description("W6: PZ (przyjęcie zewnętrzne od dostawcy) — przychód z kontrahentem-dostawcą.")]
public void PrzyjecieZewnetrzne_PZ_TworzyZasoby()
{
Guid guidPz = Guid.Empty;
// PZ to przyjęcie zewnętrzne — przychód z kontrahentem (dostawcą).
var pz = UtworzDokument(
Definicje.PrzyjecieZewnetrzne,
kontrahent: Kontrahent(Kontrahent_.Zefir),
magazyn: Magazyn(Magazyn_.Firma));
InTransaction(() =>
{
DodajPozycje(pz, Towar(Towar_.Bikini), 20, 25);
guidPz = pz.Guid;
});
// Przychód księguje zasoby/obroty DOPIERO po zatwierdzeniu + Save.
InTransaction(() => pz.Stan = StanDokumentuHandlowego.Zatwierdzony);
SaveDispose();
var zapis = Get<DokumentHandlowy>(guidPz);
zapis.Zasoby.Cast<object>().Should().NotBeEmpty();
}
// ============================== W7 — Zamówienie (ZO) ==============================
[Test]
[Description("W7: ZO (zamówienie odbiorcy) z terminem dostawy — nie buduje stanu magazynu.")]
public void ZamowienieOdbiorcy_ZO_UstawiaTerminDostawy_BezObrotow()
{
Guid guidZo = Guid.Empty;
var termin = Date.Today.AddDays(7);
var zo = UtworzDokument(
Definicje.ZamowienieOdbiorcy,
kontrahent: Kontrahent(Kontrahent_.Abc),
magazyn: Magazyn(Magazyn_.Firma));
InTransaction(() =>
{
zo.Data = Date.Today;
zo.DataOperacji = Date.Today;
// Dostawa to subrow — ustawiamy jego pola, nie przypisujemy całego obiektu.
zo.Dostawa.Termin = termin; // oczekiwany termin dostawy
DodajPozycje(zo, Towar(Towar_.Bikini), 5, 50);
guidZo = zo.Guid;
});
// Zamówienie nie buduje stanu magazynu — nie musimy wcześniej przyjmować towaru.
SaveDispose();
var zapis = Get<DokumentHandlowy>(guidZo);
zapis.Should().NotBeNull();
zapis.Dostawa.Termin.Should().Be(termin);
// Zamówienie to dokument planistyczny — nie tworzy obrotów/zasobów magazynowych.
zapis.Zasoby.Cast<object>().Should().BeEmpty();
}
// ============================== W8 — Dodawanie pozycji ==============================
[Test]
[Description("W8: pozycja z automatyczną ceną (tylko Towar + Ilosc) — cena pobrana z cennika jest dodatnia.")]
public void DodaniePozycji_AutomatycznaCena_PobieraZCennika()
{
PrzyjmijBikiniNaStan();
var fv = UtworzDokument(
Definicje.FakturaSprzedazy,
kontrahent: Kontrahent(Kontrahent_.Abc),
magazyn: Magazyn(Magazyn_.Firma));
PozycjaDokHandlowego poz = null;
InTransaction(() =>
{
// Bez podania ceny (cena = null) — towar inicjuje cenę z cennika/karty.
poz = DodajPozycje(fv, Towar(Towar_.Bikini), 3);
});
// Asercja na FV w BUFORZE (FV nie zatwierdzamy — zatwierdzenie rzuca NRE w ewidencji VAT).
// Cena zaproponowana przez cennik — oczekujemy wartości dodatniej (nie ustawialiśmy jej ręcznie).
((double)poz.Cena.Value).Should().BeGreaterThan(0);
}
[Test]
[Description("W8: ręczne nadpisanie ceny i rabatu — Cena/Rabat przyjmują podane wartości, zapalają korekty.")]
public void DodaniePozycji_RecznaCenaIRabat_NadpisujeWartosci()
{
PrzyjmijBikiniNaStan();
var fv = UtworzDokument(
Definicje.FakturaSprzedazy,
kontrahent: Kontrahent(Kontrahent_.Abc),
magazyn: Magazyn(Magazyn_.Firma));
PozycjaDokHandlowego poz = null;
InTransaction(() =>
{
// Ręczna cena nadpisuje cennik (zapala KorektaCeny); rabat zapala KorektaRabatu.
poz = DodajPozycje(fv, Towar(Towar_.Bikini), 10, 48);
poz.Rabat = new Percent(0.1m); // 10%
});
// Asercja na FV w BUFORZE (FV nie zatwierdzamy — zatwierdzenie rzuca NRE w ewidencji VAT).
((double)poz.Cena.Value).Should().Be(48);
// Rabat 10% został zapamiętany na pozycji.
((double)poz.Rabat).Should().BeApproximately(0.1, 1e-9);
}
// ============================== W10 — Dokument z usługą (MONTAZ) ==============================
[Test]
[Description("W10: FV tylko z usługą (MONTAZ) — liczy VAT/wartość, ale nie tworzy obrotów magazynowych.")]
public void FakturaZUsluga_Montaz_BezObrotowMagazynowych()
{
// Usługa nie pobiera ze stanu — NIE potrzeba wcześniejszego przyjęcia (StanUjemnyVerifier nie blokuje).
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;
// MONTAZ jest towarem typu usługa — bez wpływu na magazyn.
DodajPozycje(fv, Towar(Towar_.Montaz), 1, 200);
guidFv = fv.Guid;
});
SaveDispose();
var zapis = Get<DokumentHandlowy>(guidFv);
zapis.Should().NotBeNull();
// Usługa nie tworzy zasobów magazynowych, ale uczestniczy w tabeli VAT.
zapis.Zasoby.Cast<object>().Should().BeEmpty();
zapis.SumyVAT.Should().NotBeEmpty();
((double)zapis.Suma.Netto).Should().BeGreaterThan(0);
}
// ============================== W11 — Odbiorca inny niż kontrahent ==============================
[Test]
[Description("W11: nabywca (Kontrahent) różny od odbiorcy towaru (Odbiorca) — dwa różne pola typu Kontrahent.")]
public void OdbiorcaInnyNizKontrahent_UstawiaOdbiorce()
{
PrzyjmijBikiniNaStan();
var fv = UtworzDokument(
Definicje.FakturaSprzedazy,
kontrahent: Kontrahent(Kontrahent_.Abc), // nabywca / strona VAT
magazyn: Magazyn(Magazyn_.Firma));
InTransaction(() =>
{
// Odbiorca towaru to inny podmiot niż nabywca — faktura na Kontrahent, dostawa do Odbiorca.
fv.Odbiorca = Kontrahent(Kontrahent_.Zefir);
fv.Osoba = "Jan Kowalski"; // osoba podpisująca po stronie kontrahenta
fv.Dostawa.Termin = Date.Today.AddDays(3);
fv.Dostawa.Sposob = "Kurier";
DodajPozycje(fv, Towar(Towar_.Bikini), 1, 50);
});
// Asercja na FV w BUFORZE (FV nie zatwierdzamy — zatwierdzenie rzuca NRE w ewidencji VAT).
fv.Kontrahent.Kod.Should().Be(Kontrahent(Kontrahent_.Abc).Kod);
fv.Odbiorca.Should().NotBeNull();
fv.Odbiorca.Kod.Should().Be(Kontrahent(Kontrahent_.Zefir).Kod);
// Nabywca i odbiorca to dwa różne podmioty.
fv.Odbiorca.Kod.Should().NotBe(fv.Kontrahent.Kod);
fv.Osoba.Should().Be("Jan Kowalski");
}
// ============================== W9 — Dokument w walucie obcej (bezpiecznie, bez sieci) ==============================
[Test]
[Description("W9: dokument walutowy wymaga kursu — bez kursu EUR na datę operacja zgłasza błąd; test bezpieczny (bez sieci).")]
public void DokumentWalutowy_BezKursuEur_RzucaLubPomijane()
{
// UWAGA: NIE pobieramy kursu z sieci. Baza Demo zwykle nie ma kursu EUR „na dziś”,
// więc próba ustawienia waluty/tabeli kursowej bez dostępnego kursu powinna zgłosić wyjątek
// (np. KursWalutyNotFoundException). Test jedynie potwierdza, że ustawienie dokumentu
// walutowego WYMAGA kursu — nie wymaga połączenia z internetem.
var wm = Soneta.Waluty.WalutyModule.GetInstance(Session); // session.GetWaluty() jest internal
var eur = wm.Waluty.WgSymbolu["EUR"];
if (eur == null)
{
// Demo bez waluty EUR — pomijamy z czytelnym komentarzem (nie wymuszamy sieci/danych).
Assert.Ignore("Baza Demo nie ma waluty EUR — test walutowy pominięty (brak danych, bez sieci).");
return;
}
// Szukamy tabeli kursowej z kursem EUR na dziś — bez sieci.
var tabela = wm.TabeleKursowe.Cast<object>().FirstOrDefault();
if (tabela == null)
{
Assert.Ignore("Baza Demo nie ma tabeli kursowej — test walutowy pominięty (brak danych, bez sieci).");
return;
}
// Próba zbudowania dokumentu walutowego bez gwarancji kursu na datę:
// albo uda się (kurs jest w bazie), albo zgłosi błąd braku kursu — oba przypadki są poprawne.
var fv = UtworzDokument(
Definicje.FakturaSprzedazy,
kontrahent: Kontrahent(Kontrahent_.Abc),
magazyn: Magazyn(Magazyn_.Firma));
Action ustawWalute = () => InTransaction(() =>
{
// TabelaKursowa jest wymagana dla dokumentu walutowego; DataKursu wyznacza, którego kursu szukać.
fv.TabelaKursowa = (Soneta.Waluty.TabelaKursowa)tabela;
fv.DataKursu = Date.Today;
});
// Bezpiecznie: dopuszczamy zarówno sukces (kurs istnieje), jak i wyjątek braku kursu.
// Nie wymuszamy konkretnego typu wyjątku, bo zależy od danych Demo, a sieci nie używamy.
try
{
ustawWalute();
// Jeśli się powiodło, tabela kursowa została przypisana — to też poprawny wynik.
fv.TabelaKursowa.Should().NotBeNull();
}
catch (Exception ex)
{
// Brak kursu na datę → oczekiwany błąd (np. KursWalutyNotFoundException). To poprawny scenariusz.
ex.Should().NotBeNull("brak kursu EUR na datę powinien zgłosić wyjątek, a nie cichą awarię");
}
}
}