Files
soneta-erp-skills/Soneta.Skills.Test/Handel/DokumentyHandlowe/Rozdzial02_WystawianieTest.cs
T
2026-06-06 22:33:15 +02:00

416 lines
19 KiB
C#
Raw 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ł 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ę");
}
}
}