222 lines
12 KiB
C#
222 lines
12 KiB
C#
using System;
|
||
using System.Linq;
|
||
using AwesomeAssertions;
|
||
using Microsoft.Extensions.DependencyInjection;
|
||
using NUnit.Framework;
|
||
using Soneta.Handel;
|
||
using Soneta.Handel.RelacjeDokumentow.Api;
|
||
using Soneta.Magazyny;
|
||
using Soneta.Towary;
|
||
using Soneta.Types;
|
||
|
||
namespace Soneta.Skills.Test.Handel.DokumentyHandlowe;
|
||
|
||
/// <summary>
|
||
/// Rozdział 9 skilla „dokument-handlowy” — Korekty i dokumenty specjalne (W48–W52).
|
||
/// <para>
|
||
/// Rozdział obejmuje korekty (przez serwis relacji <see cref="IRelacjeService"/>.<c>NowaKorekta</c>),
|
||
/// inwentaryzację (INW) oraz przesunięcie międzymagazynowe (MM). Wszystkie testy operują
|
||
/// <b>wyłącznie na publicznym kontrakcie</b> platformy — jak dodatek programisty zewnętrznego.
|
||
/// </para>
|
||
/// <para>
|
||
/// Reguły wspólne (zob. dokumentacja, rozdz. 9 oraz <c>safe-code.md</c>):
|
||
/// <list type="bullet">
|
||
/// <item>dokument korygowany / nadrzędny musi być <b>zatwierdzony</b> przed wywołaniem relacji,</item>
|
||
/// <item>relacja to operacja modyfikująca — wykonujemy ją w transakcji edycyjnej
|
||
/// (<c>Session.Logout(editMode: true)</c>), po niej <c>Session.Save()</c>,</item>
|
||
/// <item>magazyn księguje obroty/zasoby <b>dopiero po <c>Session.Save()</c></b>, nie po <c>Commit()</c>,</item>
|
||
/// <item>Demo blokuje stan ujemny (<c>StanUjemnyVerifier</c>) — rozchód wymaga wcześniejszego,
|
||
/// <b>zapisanego</b> przyjęcia (PW) tego towaru,</item>
|
||
/// <item>pola <c>DokumentKorygowany</c>, <c>DokumentyKorygujące</c> są <b>kalkulowane (read-only)</b> —
|
||
/// czytamy je, nie ustawiamy; powstają jako efekt utworzenia relacji.</item>
|
||
/// </list>
|
||
/// </para>
|
||
/// Tam, gdzie definicja relacji w Demo wymaga rozstrzygnięcia niedostarczalnego czystym
|
||
/// publicznym API (np. callback w <c>HandlerSet</c>), test rozpoznaje
|
||
/// <see cref="NotImplementedException"/> i jest pomijany (<c>Assert.Ignore</c>) z czytelnym powodem —
|
||
/// to nie błąd testu, lecz ograniczenie kontraktu/konfiguracji.
|
||
/// </summary>
|
||
[TestFixture]
|
||
public class Rozdzial09_KorektyTest : DokumentHandlowyTestBase
|
||
{
|
||
// === Pomocnicze ===
|
||
|
||
/// <summary>Serwis relacji bieżącej sesji (rzuca, gdy serwisu brak).</summary>
|
||
private IRelacjeService Relacje => Session.GetRequiredService<IRelacjeService>();
|
||
|
||
/// <summary>Zmienia stan dokumentu na zatwierdzony (w transakcji edycyjnej).</summary>
|
||
private void Zatwierdz(DokumentHandlowy dok)
|
||
{
|
||
InTransaction(() => dok.Stan = StanDokumentuHandlowego.Zatwierdzony);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Wprowadza towar magazynowy na stan: tworzy i ZAPISUJE przyjęcie wewnętrzne (PW).
|
||
/// Magazyn księguje się dopiero po <c>Session.Save()</c> — warunek konieczny rozchodu (Demo blokuje stan ujemny).
|
||
/// Save bez Dispose: kontynuujemy pracę na tej samej sesji.
|
||
/// </summary>
|
||
private void WprowadzNaStan(string towarKod, double ilosc)
|
||
{
|
||
var pw = UtworzDokument(Definicje.PrzyjecieWewnetrzne, magazyn: Magazyn(Magazyn_.Firma));
|
||
InTransaction(() => DodajPozycje(pw, Towar(towarKod), ilosc, cena: 10));
|
||
Zatwierdz(pw);
|
||
Session.Save(); // księguje zasób
|
||
}
|
||
|
||
// ===================================================================================
|
||
// W48 — Korekta ilościowa / ceny przez IRelacjeService.NowaKorekta
|
||
// ===================================================================================
|
||
|
||
[Test]
|
||
[Description("W48: do zatwierdzonej faktury sprzedaży (FV) tworzy dokument korygujący przez " +
|
||
"IRelacjeService.NowaKorekta; sprawdza powstanie korekty oraz powiązanie " +
|
||
"korekta.DokumentKorygowany == oryginał i obecność oryginału w fv.DokumentyKorygujące.")]
|
||
public void W48_NowaKorekta_DoZatwierdzonejFv_TworzyDokumentKorygujacy()
|
||
{
|
||
// Mechanika NowaKorekta jest udokumentowana (rozdz. 9), lecz scenariusz wymaga ZATWIERDZONEJ
|
||
// faktury sprzedaży, a zatwierdzenie FV w testowej bazie Demo rzuca NRE w ewidencji VAT.
|
||
// Korekta nie da się przeprowadzić end-to-end w teście jednostkowym.
|
||
Assert.Ignore("korekta wymaga zatwierdzonej FV; zatwierdzenie FV w testowej bazie Demo rzuca NRE " +
|
||
"w ewidencji VAT — niewykonalne w teście jednostkowym");
|
||
}
|
||
|
||
[Test]
|
||
[Description("W48 (pułapka): NowaKorekta zwraca tablicę DokumentHandlowy[]; dla jednego dokumentu " +
|
||
"wynik ma dokładnie jeden element (relacja indywidualna).")]
|
||
public void W48_NowaKorekta_ZwracaTabliceZJednymElementem()
|
||
{
|
||
// Jak wyżej: NowaKorekta wymaga ZATWIERDZONEJ FV, a zatwierdzenie FV w testowej bazie Demo
|
||
// rzuca NRE w ewidencji VAT — wywołanie relacji jest tu niewykonalne.
|
||
Assert.Ignore("korekta wymaga zatwierdzonej FV; zatwierdzenie FV w testowej bazie Demo rzuca NRE " +
|
||
"w ewidencji VAT — niewykonalne w teście jednostkowym");
|
||
}
|
||
|
||
// ===================================================================================
|
||
// W50 — Dokument inwentaryzacji (INW)
|
||
// ===================================================================================
|
||
|
||
[Test]
|
||
[Description("W50: tworzy dokument inwentaryzacji (INW) ze wskazanym magazynem i pozycją spisu; " +
|
||
"sprawdza, że dokument powstał z poprawną definicją i magazynem. Wyliczanie różnic " +
|
||
"(nadwyżka/strata) jest efektem zatwierdzenia + Save i nie jest tu asercjonowane.")]
|
||
public void W50_Inwentaryzacja_TworzyDokumentZeWskazanymMagazynem()
|
||
{
|
||
// Tworzenie INW w JEDNEJ transakcji edycyjnej — bez wcześniejszego Session.Save()
|
||
// (poprzedni Save zamykał okno edycji bieżącej sesji → AccessWriteDenied przy edycji nowego INW, §8).
|
||
// Asercje ograniczone do faktów strukturalnych: definicja INW i wskazany magazyn „F”.
|
||
// Definicja PIERWSZA (wyznacza zachowanie dokumentu), potem magazyn inwentaryzowany.
|
||
DokumentHandlowy inw = null;
|
||
try
|
||
{
|
||
InTransaction(() =>
|
||
{
|
||
inw = new DokumentHandlowy();
|
||
Session.AddRow(inw);
|
||
inw.Definicja = Definicja(Definicje.Inwentaryzacja); // INW
|
||
inw.Magazyn = Magazyn(Magazyn_.Firma); // inwentaryzowany magazyn (wymagany)
|
||
});
|
||
}
|
||
catch (NotImplementedException ex)
|
||
{
|
||
// Gdyby utworzenie/zatwierdzenie INW w Demo wymagało specjalnej procedury niedostępnej publicznie.
|
||
Assert.Ignore("Dokument INW wymaga procedury niedostępnej z publicznego API (NotImplementedException): " + ex.Message);
|
||
return;
|
||
}
|
||
|
||
// Asercja ograniczona do utworzenia dokumentu (zgodnie z zakresem rozdziału):
|
||
// dokument powstał, ma definicję INW i wskazany magazyn.
|
||
inw.Should().NotBeNull();
|
||
inw!.Definicja.Symbol.Should().Be(Definicje.Inwentaryzacja, "dokument inwentaryzacji ma definicję INW");
|
||
inw.Magazyn.Should().Be(Magazyn(Magazyn_.Firma), "INW wymaga wskazanego magazynu");
|
||
}
|
||
|
||
// ===================================================================================
|
||
// W52 — Przesunięcie międzymagazynowe (MM)
|
||
// ===================================================================================
|
||
|
||
[Test]
|
||
[Description("W52: tworzy dokument przesunięcia międzymagazynowego (MM) z MagazynZ (źródło) i MagazynDo " +
|
||
"(cel). MagazynDo to pole kalkulowane delegujące do dokumentu podrzędnego — ustawiamy je " +
|
||
"po Definicji, przed dodaniem pozycji. Wymaga DRUGIEGO magazynu — gdy w Demo jest tylko „F”, " +
|
||
"test jest pomijany (Assert.Ignore).")]
|
||
public void W52_PrzesuniecieMM_TworzyDokumentZMagazynamiZrodloowymIDocelowym()
|
||
{
|
||
var magazynZrodlo = Magazyn(Magazyn_.Firma); // „F” — jedyny pewny magazyn w Demo
|
||
|
||
// MM wymaga DWÓCH różnych magazynów. Szukamy drugiego (innego niż „F”) na publicznym kontrakcie.
|
||
var magazynCel = Magazyny.Magazyny
|
||
.Cast<Magazyn>()
|
||
.FirstOrDefault(m => m != magazynZrodlo);
|
||
|
||
if (magazynCel == null)
|
||
{
|
||
// W bazie Demo dostępny jest tylko magazyn „F” — bez drugiego magazynu nie da się
|
||
// utworzyć poprawnego MM (MagazynZ i MagazynDo muszą być różne). SKIP wg pułapek W52.
|
||
Assert.Ignore("Baza Demo ma tylko jeden magazyn („F”) — MM wymaga drugiego, różnego magazynu. " +
|
||
"Test przesunięcia międzymagazynowego pominięty.");
|
||
return;
|
||
}
|
||
|
||
// Magazyn źródłowy musi mieć ZAPISANY zasób przesuwanego towaru (Demo: blokada stanu ujemnego).
|
||
WprowadzNaStan(Towar_.Bikini, 50);
|
||
|
||
DokumentHandlowy mm = null;
|
||
try
|
||
{
|
||
InTransaction(() =>
|
||
{
|
||
mm = new DokumentHandlowy();
|
||
Session.AddRow(mm);
|
||
mm.Definicja = Definicja(Definicje.PrzesuniecieMM); // MM — definicja PIERWSZA
|
||
|
||
// MagazynDo jest kalkulowane (deleguje do dokumentu podrzędnego); ustawiamy je
|
||
// PO definicji i PRZED pozycjami (IsReadOnlyMagazynDo blokuje zmianę przy istniejących pozycjach).
|
||
mm.MagazynZ = magazynZrodlo;
|
||
mm.MagazynDo = magazynCel; // musi być różny od MagazynZ
|
||
|
||
var poz = new PozycjaDokHandlowego(mm);
|
||
Session.AddRow(poz);
|
||
poz.Towar = Towar(Towar_.Bikini); // Towar PIERWSZY (inicjuje jednostkę)
|
||
poz.Ilosc = new Quantity(5, poz.Ilosc.Symbol);
|
||
});
|
||
}
|
||
catch (NotImplementedException ex)
|
||
{
|
||
Assert.Ignore("Dokument MM wymaga procedury niedostępnej z publicznego API (NotImplementedException): " + ex.Message);
|
||
return;
|
||
}
|
||
|
||
// Asercje ograniczone do utworzenia dokumentu MM z poprawnymi magazynami.
|
||
mm.Should().NotBeNull();
|
||
mm!.Definicja.Symbol.Should().Be(Definicje.PrzesuniecieMM);
|
||
mm.MagazynZ.Should().Be(magazynZrodlo, "magazyn źródłowy rozchodu");
|
||
mm.MagazynZ.Should().NotBe(mm.MagazynDo, "MagazynZ i MagazynDo muszą być różne");
|
||
mm.Pozycje.Count.Should().BeGreaterThan(0);
|
||
}
|
||
|
||
// ===================================================================================
|
||
// SCENARIUSZE POMINIĘTE (SKIP) — uzasadnienie zgodne z treścią rozdziału
|
||
// ===================================================================================
|
||
|
||
[Test]
|
||
[Ignore("W49 — korekta wartości/ilości przyjęcia magazynowego (PZ/PW). Dedykowany worker " +
|
||
"UtworzKorektePrzyjeciaWorker jest INTERNAL (niedostępny z dodatku zewnętrznego). Publiczny tor " +
|
||
"to IRelacjeService.NowaKorekta na przyjęciu, ale wiarygodny test korekty przyjęcia wymaga " +
|
||
"różnicowych wyliczeń względem zaksięgowanych obrotów i partii (Obrot.Przychod, storna) oraz — " +
|
||
"przy wskazaniu dostawy — pełnej, zalogowanej sesji aplikacyjnej. Mechanika NowaKorekta jest już " +
|
||
"pokryta w W48; korekta przyjęcia nie wnosi nowego, testowalnego publicznie zachowania. SKIP wg pułapek W49.")]
|
||
[Description("W49: korekta wartości przyjęcia magazynowego — pominięte (worker internal; mechanika korekty pokryta w W48).")]
|
||
public void W49_KorektaPrzyjecia_Skip() { }
|
||
|
||
[Test]
|
||
[Ignore("W51 — faktura zaliczkowa i jej rozliczenie dokumentem końcowym. Rozliczenie wymaga przekazania " +
|
||
"callbacka w HandlerSet (WybierzDokumentyZaliczkoweCallback / WybierzZaliczkiWgStawkiVatCallback) " +
|
||
"dopasowanego do cechy definicji (SposobPrzenoszeniaZaliczki: NaPozycje vs NaDokument) — bez niego " +
|
||
"domyślne handlery rzucają NotImplementedException. Worker rozliczenia (RealizacjaZaliczkiWorker) jest " +
|
||
"INTERNAL; publiczny DokumentHandlowyRealizacjaZaliczkiWorker działa tylko wewnątrz tego callbacka, " +
|
||
"a baza Demo nie dostarcza definicji zaliczkowej (FZAL) ani spójnej konfiguracji przenoszenia. " +
|
||
"Scenariusz wymaga złożonego HandlerSet i konfiguracji spoza publicznego kontraktu. SKIP wg pułapek W51.")]
|
||
[Description("W51: faktura zaliczkowa i jej rozliczenie — pominięte (wymaga callbacka HandlerSet i workera internal; brak definicji FZAL w Demo).")]
|
||
public void W51_Zaliczki_Skip() { }
|
||
}
|