278 KiB
Dokument handlowy — receptury kodu biznesowego (Soneta / enova365)
Zbiór gotowych wzorców kodu dla obiektu biznesowego Soneta.Handel.DokumentHandlowy
(tabela DokHandlowe, moduł HandelModule). Dokument jest częścią skilla soneta-programming.
Celem jest, aby agent pisał bezbłędny kod biznesowy operujący na dokumencie handlowym — fakturach,
dokumentach magazynowych, zamówieniach, ofertach i korektach — trafiający w realne pola, kolekcje i workery
platformy.
Format zwarty: każdy wzorzec opisuje ogólny przypadek + tabelę wariantów, zamiast wielu wąskich pozycji. Fundamenty (sesja, transakcja, blokada optymistyczna, praca z
SubTable, obsługa błędów) są opisane wsafe-code.md,session-login.mdorazworker-extender.md— tutaj się do nich odwołujemy, nie powtarzamy ich.Cały kod w tym dokumencie jest zgodny z C# 10 (target-typed
new,var, wyrażeniaswitch, nazwane parametrybool). Snippety operują wyłącznie na publicznym kontrakcie platformy — nie ma odwołań do prywatnych klas ani kodu źródłowego aplikacji.
Spis treści
- 1. Fundamenty i identyfikacja
- 2. Wystawianie dokumentów
- W4 — Faktura sprzedaży (FV)
- W5 — Faktura zakupu (FZ)
- W6 — Dokument magazynowy (PZ / WZ / RW / PW)
- W7 — Zamówienie (ZO / ZD)
- W8 — Dodawanie pozycji (towar, ilość, cena, rabat, jednostka)
- W9 — Dokument w walucie obcej
- W10 — Dokument z usługą (pozycja usługowa bez wpływu na magazyn)
- W11 — Odbiorca / płatnik inny niż kontrahent + miejsce dostawy
- 3. Stany dokumentu i cykl życia
- 4. Relacje i generowanie dokumentów
- W17 — Generowanie faktury z zamówienia (ZO → FV)
- W18 — Zbiorczy dokument magazynowy z wielu faktur (wiele FA → 1 WZ/PZ)
- W19 — Zbiorcza faktura z wielu dokumentów magazynowych (wiele WZ → 1 FA)
- W20 — Wyszukiwanie dokumentów powiązanych (odczyt pól kalkulowanych)
- W21 — Generowanie dokumentu magazynowego z faktury (FA → WZ pojedynczo)
- W22 — Kopiowanie faktury klientowi (
KopiujKlientowiFaktureWorker) - W23 — Ręczne wiązanie i rozwiązywanie powiązań
- W24 — Odczyt łańcucha powiązań i stan pokrycia zamówienia
- 5. Odczyt i wyszukiwanie
- 6. Magazyn, zasoby, partie, obroty
- W31 — Przeglądanie zasobów utworzonych przez dokument przychodowy (
dok.Zasoby) - W32 — Przetwarzanie obrotów faktury sprzedaży i dokumentu rozchodowego (
dok.Obroty,dok.ObrotyWszystkie) - W33 — Odczyt stanu magazynowego towaru (magazyn / data) —
mag.Zasobyz filtrem - W34 — Wyszukiwanie partii magazynowych (
GrupaDostaw) według cech - W35 — Dokument rozchodowy ze wskazaniem JEDNEJ partii
- W36 — Dokument rozchodowy ze wskazaniem WIELU partii
- W37 — Dokument przyjęcia (PW/PZ) z numerem serii — zapis numeru serii jako cecha
- W38 — Odczyt rozchodu zasobów: powiązanie pozycji rozchodu z partią pierwotną / przyjęciem
- W39 — Odczyt okresów magazynowych i kontekstu wyceny (FIFO/LIFO/wg dostaw)
- W31 — Przeglądanie zasobów utworzonych przez dokument przychodowy (
- 7. Cechy (Features)
- 8. VAT, wartości i waluty
- 9. Korekty i dokumenty specjalne
- 10. Operacje zbiorcze (batch)
- 11. Operacje pomocnicze (przekrojowe)
- W56 — Bezpieczne pobranie / utworzenie kontrahenta i towaru pozycji
- W57 — Przeliczanie jednostek miary towaru przy dodawaniu pozycji
- W58 — Walidacja przed zatwierdzeniem (kompletność, zasób, limit kredytowy)
- W59 — Obsługa błędów i blokada optymistyczna (kolizje
Save, ponowienie) - W60 — Odczyt metadanych dokumentu (
ChangeInfos— kto/kiedy założył i zmienił) - W61 — Praca z definicjami i numeracją (seria, wymuszenie numeru, bufor
Numer)
- 12. Wydruki i raporty
- 13. Tematy specjalistyczne (KSeF, fiskalizacja, kompletacja, Intrastat)
- W67 — Wysłanie faktury do KSeF (pojedynczo i zbiorczo)
- W68 — Sprawdzenie statusu KSeF i odczyt numeru KSeF
- W69 — UPO, numer KSeF z duplikatu, walidacja struktury XML
- W70 — Import faktur z KSeF (dokumenty zakupu)
- W71 — Fiskalizacja dokumentu (paragon fiskalny)
- W72 — E-paragon (e-mail) i ponowny wydruk paragonu
- W73 — Dokument kompletacji (złożenie / rozłożenie kompletu)
- W74 — Intrastat (dane statystyczne i wyszukanie dokumentów do deklaracji)
- 14. Płatności dokumentu handlowego
- W75 — Przeglądanie płatności dokumentu
- W76 — Rozbicie płatności na raty
- W77 — Ręczne dodanie / edycja pojedynczej płatności
- W78 — Warunki płatności z kontrahenta i ich przeliczenie na dokumencie
- W79 — Zmiana płatnika (inny niż kontrahent)
- W80 — Odczyt stanu rozliczenia płatności
- W81 — Płatności w walucie obcej (kwota w walucie vs PLN, kurs)
- W82 — Powiązanie płatności z terminem i rabatem za wcześniejszą zapłatę
Fakty o typie (zweryfikowane skanem DLL — scan-props.csx / scan-workers.csx)
- Klasa biznesowa:
Soneta.Handel.DokumentHandlowy—GuidedRow(root), tabelaSoneta.Handel.DokHandlowe(„Dokumenty handlowe"). - Jeden typ — wiele rodzajów dokumentów. Faktury (FV, FZ, PAR), dokumenty magazynowe (PZ, WZ, PW, RW, MM),
zamówienia (ZO, ZD), oferty (OD, OO), korekty i inne — różni je wyłącznie
Definicja(DefDokHandlowego). To definicja wyznacza kierunek magazynu, numerację, sposób liczenia VAT itd. - Moduł:
Soneta.Handel.HandelModule, dostępsession.GetHandel(). Tabela dokumentów:Handel.DokHandlowe. Definicje:Handel.DefDokHandlowych(kluczWgSymbolu["FV"]). - Implementuje:
IDokumentPlatny,IDokumentKsiegowalny,IDokumentKasowy,IDaneKontrahentaHost,IDokumentCRM,IKodowany,IExportImportXmlHost,IElementSlownika,IKomunikatEDIHost,IEmailElement,IProceduraVATHost,IZrodloOpisuAnalitycznego. - Pola: 128 bazodanowych + 388 kalkulowanych.
Kluczowe pola bazodanowe (zapisywalne)
| Pole | Typ | Znaczenie |
|---|---|---|
Definicja |
Soneta.Handel.DefDokHandlowego |
definicja dokumentu — wyznacza rodzaj/zachowanie (ustaw jako pierwszą) |
Kontrahent |
Soneta.CRM.Kontrahent |
kontrahent (nabywca/dostawca) dokumentu |
Odbiorca |
Soneta.CRM.Kontrahent |
odbiorca towarów (gdy inny niż kontrahent) |
Magazyn |
Soneta.Magazyny.Magazyn |
magazyn, na który wpływa dokument |
Data |
Soneta.Types.Date |
data wystawienia |
DataOperacji |
Soneta.Types.Date |
faktyczna data sprzedaży/zakupu |
Numer |
Soneta.Core.NumerDokumentu |
numeracja dokumentu (zob. wzorzec numeracji) |
Seria |
string |
seria dokumentu |
Stan |
Soneta.Handel.StanDokumentuHandlowego |
Bufor=0, Zatwierdzony=1, Zablokowany=2, Anulowany=3 |
LiczonaOd |
Soneta.Handel.SposobLiczeniaVAT |
liczenie wartości od netto/brutto |
KorektaVAT |
bool |
sumy VAT zmienione ręcznie (niezależne od pozycji) |
Waluta (przez BruttoCy) |
Soneta.Types.Currency |
kwota płatności w walucie |
TabelaKursowa |
Soneta.Waluty.TabelaKursowa |
tabela kursów dla dokumentu walutowego |
RodzajTransakcji |
Soneta.Handel.KodRodzajuTransakcji |
rodzaj transakcji Intrastat |
Opis |
Soneta.Business.MemoText |
opis na wydruku |
Suma |
Soneta.Handel.BruttoNetto |
podsumowana wartość dokumentu |
Kluczowe kolekcje i właściwości kalkulowane (tylko do odczytu, o ile nie zaznaczono)
| Składowa | Typ | Znaczenie |
|---|---|---|
Pozycje |
LpSubTable<PozycjaDokHandlowego> |
pozycje dokumentu |
SumyVAT |
SubTable<SumaVAT> |
tabelka VAT (netto/VAT/brutto wg stawek) |
Platnosci |
SubTable<Soneta.Kasa.Platnosc> |
płatności dokumentu |
Obroty |
SubTable |
obroty magazynowe bezpośrednie dokumentu |
ObrotyWszystkie |
ListWithView |
obroty łącznie z dokumentami zależnymi |
Zasoby |
SubTable |
zasoby magazynowe utworzone przez dokument |
DokumentyMagazynowe |
DokumentHandlowy[] |
dokumenty magazynowe powiązane z fakturą |
DokumentyHandlowe |
DokumentHandlowy[] |
faktury powiązane z dokumentem magazynowym |
DokumentKorygowany |
DokumentHandlowy |
dokument korygowany (kalkulowane — tworzy relacja/UI) |
DokumentyKorygujące |
IEnumerable<DokumentHandlowy> |
korekty tego dokumentu |
DokumentyZaliczkowe |
DokumentHandlowy[] |
nadrzędne dokumenty zaliczkowe |
Rezerwacja |
DokumentHandlowy |
dokument rezerwacji towarów |
SumaPozycji |
BruttoNettoPozycji |
wyliczona suma wartości pozycji |
Bufor / Zatwierdzony / Anulowany |
bool |
skróty stanu (kalkulowane z Stan) |
Features |
Soneta.Business.FeatureCollection |
cechy definiowalne dokumentu |
Pozycja dokumentu — Soneta.Handel.PozycjaDokHandlowego
| Pole | Typ | Znaczenie |
|---|---|---|
Towar |
Soneta.Towary.Towar |
towar pozycji (ustaw pierwszy — inicjuje jednostkę na Ilosc/Cena) |
Ilosc |
Soneta.Towary.Quantity |
ilość; twórz new Quantity(wartość, poz.Ilosc.Symbol) |
Cena |
Soneta.Types.DoubleCy |
cena (netto/brutto wg LiczonaOd); new DoubleCy(wartość, poz.Cena.Symbol) |
Rabat |
Soneta.Types.Percent |
procent rabatu |
Features |
FeatureCollection |
cechy pozycji (m.in. przeniesione z partii/towaru) |
Konstruktor pozycji wymaga dokumentu: new PozycjaDokHandlowego(dokument).
Podstawowe typy i obiekty pomocnicze
| Typ | Rola |
|---|---|
Soneta.Handel.HandelModule |
moduł Handel: DokHandlowe, DefDokHandlowych |
Soneta.Magazyny.MagazynyModule |
magazyny, zasoby, obroty, partie (GrupaDostaw) — session.GetMagazyny() |
Soneta.Towary.TowaryModule |
towary, jednostki, ceny — session.GetTowary() |
Soneta.CRM.CRMModule |
kontrahenci — session.GetCRM() |
Soneta.Handel.DefDokHandlowego |
definicja dokumentu (symbol, kierunek, numeracja, flagi) |
Soneta.Types.Quantity |
ilość z jednostką miary |
Soneta.Types.DoubleCy |
wartość zmiennoprzecinkowa z walutą (cena) |
Soneta.Types.Currency |
kwota z walutą (wartości, płatności) |
Soneta.Types.Percent |
procent (rabat, stawka) |
Soneta.Types.Date |
data biznesowa |
Soneta.Handel.StanDokumentuHandlowego |
stan cyklu życia dokumentu |
Szablon wzorca
Każdy wzorzec (Wn) ma stałą strukturę:
- Cel — co robi i kiedy go użyć.
- Warianty — tabela odmian przypadku.
- Pola i typy — realne właściwości/kolekcje i ich typy.
- Snippet — kod C# 10 na publicznym kontrakcie.
- Pułapki — typowe błędy i zasady safe-code.
1. Fundamenty i identyfikacja
Rozdział opisuje, jak z poziomu sesji dotrzeć do modułów handlowo-magazynowych, jak poprawnie wskazać definicję dokumentu (
DefDokHandlowego) zanim utworzysz dokument, oraz jak na podstawie definicji i flag dokumentu rozpoznać jego rodzaj (faktura / magazynowy / zamówienie / korekta / zaliczka). Cały kod jest zgodny z C# 10 i operuje wyłącznie na publicznym kontrakcie platformy. Fundamenty wspólne (sesja, transakcjasession.Logout(true)+Commit/CommitUI, blokada optymistyczna, praca zSubTable) opisująsafe-code.md,session-login.mdorazworker-extender.md— tutaj się do nich odwołujemy, nie powtarzamy ich.
W1 — Dostęp do modułów handlowo-magazynowych i tabeli DokHandlowe
Cel: z obiektu Session (lub dowolnego ISessionable — Row, Table, Context) dotrzeć do
modułów, na których opiera się logika handlu i magazynu, oraz do tabeli dokumentów DokHandlowe.
To punkt wejścia każdego scenariusza w tym dokumencie.
Warianty:
| Wariant | Wywołanie (extension method na Session) |
Co udostępnia |
|---|---|---|
| Moduł handlowy | session.GetHandel() → HandelModule |
.DokHandlowe (tabela dokumentów), .DefDokHandlowych (definicje) |
| Moduł magazynowy | session.GetMagazyny() → MagazynyModule |
.Magazyny, .Zasoby, .Obroty, .GrupyDostaw (partie), .OkresyMag |
| Moduł towarów | session.GetTowary() → TowaryModule |
.Towary, .Jednostki |
| Moduł CRM | session.GetCRM() → CRMModule |
.Kontrahenci |
| Moduł kasowy | session.GetKasa() → KasaModule |
formy płatności, rozrachunki (dot. płatności dokumentu) |
| Waluty | Soneta.Waluty.WalutyModule.GetInstance(session) |
.Waluty, .TabeleKursowe |
Pola i typy: HandelModule.DokHandlowe: DokHandlowe (tabela DokumentHandlowy),
HandelModule.DefDokHandlowych (tabela DefDokHandlowego),
MagazynyModule.Magazyny, TowaryModule.Towary, CRMModule.Kontrahenci. Wszystkie moduły
implementują ISessionable i mają property .Session.
Snippet:
// Punkt wejścia — z sesji pobieramy moduły handlowo-magazynowe:
var handel = session.GetHandel(); // HandelModule
var magazyny = session.GetMagazyny(); // MagazynyModule
var towary = session.GetTowary(); // TowaryModule
var crm = session.GetCRM(); // CRMModule
// Tabela dokumentów handlowych (operacyjna, guided):
var dokumenty = handel.DokHandlowe;
// Iteracja po dokumentach — ZAWSZE zawężaj zakres (data/definicja/kontrahent),
// to tabela operacyjna rosnąca z biznesem. Filtr aplikujemy na indeksie (warunek serwerowy):
var od = Date.Today.AddMonths(-1);
foreach (DokumentHandlowy d in handel.DokHandlowe.WgDaty[(DokumentHandlowy x) => x.Data >= od])
{
// d.* — Numer, Data, Definicja, Kontrahent, Suma, Stan ...
}
// Z dowolnego ISessionable można zejść do modułu również metodą GetInstance:
var hm = Soneta.Handel.HandelModule.GetInstance(jakisRow); // gdy nie mamy zmiennej Session
Pułapki:
- Moduł i tabela są single-threaded — nie współdziel ich między wątkami; pobieraj je z sesji bieżącego wątku (thread-safety w SKILL.md).
session.GetWaluty()jest internal — z dodatku zewnętrznego użyjSoneta.Waluty.WalutyModule.GetInstance(session).- Nie ładuj całej tabeli
DokHandlowedo pamięci zif-em w pętli. Filtruj serwerowo — warunek aplikuj na indeksie tabeli (np.WgDaty[(DokumentHandlowy x) => …]), żeby wykonał się po stronie SQL (safe-code §6). W warunkuRowConditionużywaj tylko pól bazodanowych — pola kalkulowane rzucąLinqConditionException. - Pobranie modułu nie tworzy ani nie modyfikuje danych — modyfikacje zawsze w transakcji
(
session.Logout(true)+Commit/CommitUI, potemSave).
W2 — Wybór definicji dokumentu (DefDokHandlowego) wg symbolu
Cel: zanim utworzysz dokument, musisz wskazać jego definicję — to ona określa typ dokumentu
(sprzedaż, zakup, magazynowy, zamówienie…), numerację, zachowanie magazynu i płatności. Definicja
jest pierwszym ustawianym polem nowego dokumentu (dok.Definicja = …), zanim ustawisz magazyn,
kontrahenta czy pozycje.
Warianty:
| Wariant | Klucz / mechanizm | Uwaga |
|---|---|---|
| Po symbolu | DefDokHandlowych.WgSymbolu["FV"] |
indeks unikalny — zwraca pojedynczy rekord lub null |
| Filtr po kategorii (typie) | DefDokHandlowych.WgKategorii[KategoriaHandlowa.Sprzedaż] |
zbiór wszystkich definicji danej kategorii |
| Po symbolu w obrębie kategorii | warunek serwerowy na WgSymbolu + sprawdzenie Kategoria |
gdy w bazie istnieje kilka wariantów sprzedaży |
| Walidacja istnienia | WgSymbolu[symbol] != null |
brak definicji = nie da się utworzyć dokumentu |
Typowe symbole w bazie Demo: FV (faktura sprzedaży), FZ (faktura zakupu), PAR (paragon),
PZ/PW (przyjęcia magazynowe), WZ/RW (rozchody magazynowe), ZO (zamówienie
odbiorcy), ZD (zamówienie do dostawcy), MM (przesunięcie międzymagazynowe),
INW (inwentaryzacja), KS (korekta sprzedaży). Symbole zależą od konfiguracji konkretnej bazy —
nie zakładaj ich „na sztywno", weryfikuj != null.
Pola i typy: DefDokHandlowego.Symbol: string (maks. 12 znaków, unikalny),
DefDokHandlowego.Kategoria: Soneta.Handel.KategoriaHandlowa. Indeks WgSymbolu jest unikalny
(zwraca pojedynczy rekord), WgKategorii grupuje definicje po kategorii.
Snippet:
var handel = session.GetHandel();
// 1. Po symbolu — klucz unikalny: pojedynczy rekord albo null
DefDokHandlowego defFV = handel.DefDokHandlowych.WgSymbolu["FV"];
if (defFV == null)
throw new BusException("Brak definicji dokumentu o symbolu FV w tej bazie.".Translate());
// 2. Wszystkie definicje danej kategorii (np. wszystkie definicje sprzedaży):
foreach (DefDokHandlowego d in handel.DefDokHandlowych.WgKategorii[KategoriaHandlowa.Sprzedaż])
{
// d.Symbol, d.Kategoria ...
}
// 3. Użycie definicji przy tworzeniu dokumentu — Definicja USTAWIANA PIERWSZA:
using (var t = session.Logout(editMode: true))
{
var dok = new DokumentHandlowy();
session.AddRow(dok); // AddRow przed ustawianiem pól
dok.Definicja = handel.DefDokHandlowych.WgSymbolu["PW"]; // definicja jako pierwsze pole
// dok.Magazyn / dok.Kontrahent ustawiamy dopiero PO definicji (gdy definicja ich wymaga)
t.Commit(); // CommitUI() w workerze/extenderze
}
session.Save();
Pułapki:
WgSymbolu[...]zwraca pojedynczy rekord (klucz unikalny) i może byćnull— zawsze sprawdź przed użyciem.WgKategorii[...]zwraca zbiór — iteruj lub.FirstOrDefault().- Definicja musi być ustawiona jako pierwsze pole dokumentu — od niej zależy widoczność i wymagalność pozostałych pól (magazyn, kontrahent, numeracja). Ustawienie magazynu/kontrahenta przed definicją jest błędem.
- Symbole nie są gwarantowane — zależą od konfiguracji bazy klienta. Nie polegaj na obecności
„FV"/„WZ"; pobierz definicję i sprawdź
!= null, a w razie potrzeby filtruj poKategoria. DefDokHandlowegoto dane konfiguracyjne (GuidedRow) — odczytuj je, nie twórz „w locie" w kodzie operacyjnym.
W3 — Rozpoznanie rodzaju dokumentu (faktura / magazynowy / zamówienie / korekta / zaliczka)
Cel: ustalić, „czym jest" dany dokument — fakturą, dokumentem magazynowym, zamówieniem, korektą
czy dokumentem zaliczkowym — by rozgałęzić logikę (np. inaczej traktować rozchód magazynowy niż
zamówienie). Rozpoznanie opiera się na kategorii definicji (Definicja.Kategoria) oraz na
gotowych flagach dokumentu (Korekta, JestDokZaliczkowy()).
Warianty:
| Co rozpoznajemy | Mechanizm (publiczny kontrakt) | Wartości / zakres KategoriaHandlowa |
|---|---|---|
| Faktura/handlowy (sprzedaż, zakup, korekty, f. wewnętrzna) | Definicja.Kategoria w zakresie handlowym |
Sprzedaż=2, KorektaSprzedaży=3, Zakup=4, KorektaZakupu=5, FakturaWewnętrzna=6 (zakres HandelPierwszy=1 … HandelOstatni=100) |
| Magazynowy (PW/PZ/WZ/RW/MM/INW…) | Definicja.Kategoria w zakresie magazynowym |
PrzyjęcieMagazynowe=102, WydanieMagazynowe=104, PrzesunięcieMagazynowe=106, Inwentaryzacja=107 … (zakres MagazynPierwszy=101 … MagazynOstatni=200) |
| Zamówienie (ZO/ZD/wewn.) | Definicja.Kategoria |
ZamówienieOdbiorcy=302, ZamówienieDostawcy=303, ZamówienieWewnętrzne=312 |
| Korekta | flaga dok.Korekta lub kategoria typu Korekta* |
dok.Korekta == true; kategorie: KorektaSprzedaży, KorektaZakupu, KorektaPrzyjęciaMagazynowego, KorektaWydaniaMagazynowego … |
| Dokument zaliczkowy | metoda dok.JestDokZaliczkowy() / dok.JestDokZaliczkowy(out bool korekta) |
true = zaliczkowy; out korekta = korekta zaliczki |
Pola i typy:
DokumentHandlowy.Definicja: Soneta.Handel.DefDokHandlowego— definicja dokumentu.DefDokHandlowego.Kategoria: Soneta.Handel.KategoriaHandlowa— kluczowy wyznacznik rodzaju.DokumentHandlowy.Korekta: bool(kalkulowane, read-only) — czy dokument jest korektą.DokumentHandlowy.JestDokZaliczkowy(): boolorazJestDokZaliczkowy(out bool korekta): bool— rozpoznanie zaliczki (drugi przeciążony wariant zwraca też, czy to korekta zaliczki).DefDokHandlowego.Symbol: string— symbol (do logów / komunikatów).
Enum Soneta.Handel.KategoriaHandlowa (wartości publiczne) ma czytelne markery zakresów:
HandelPierwszy=1/HandelOstatni=100, MagazynPierwszy=101/MagazynOstatni=200,
PozostałePierwszy=301/PozostałeOstatni=400. Pozwalają one rozpoznać „grupę" dokumentu zakresem,
bez wyliczania wszystkich symboli.
Snippet:
// Rozpoznanie rodzaju dokumentu na podstawie kategorii jego definicji + flag dokumentu.
// KategoriaHandlowa to enum — markery zakresów (HandelPierwszy/Ostatni, MagazynPierwszy/Ostatni)
// pozwalają klasyfikować grupę dokumentu bez wymieniania wszystkich symboli.
static string RozpoznajRodzaj(DokumentHandlowy dok)
{
KategoriaHandlowa kat = dok.Definicja.Kategoria;
// Zaliczka i korekta mają dedykowane, jednoznaczne testy — sprawdzamy je najpierw:
if (dok.JestDokZaliczkowy(out bool korektaZaliczki))
return korektaZaliczki ? "Korekta zaliczki" : "Dokument zaliczkowy";
if (dok.Korekta)
return "Korekta";
// Klasyfikacja grupy po zakresie wartości enuma (markery są publiczne):
return kat switch
{
>= KategoriaHandlowa.HandelPierwszy and <= KategoriaHandlowa.HandelOstatni => "Faktura / dokument handlowy",
>= KategoriaHandlowa.MagazynPierwszy and <= KategoriaHandlowa.MagazynOstatni => "Dokument magazynowy",
KategoriaHandlowa.ZamówienieOdbiorcy
or KategoriaHandlowa.ZamówienieDostawcy
or KategoriaHandlowa.ZamówienieWewnętrzne => "Zamówienie",
_ => "Inny"
};
}
// Przykład użycia — rozgałęzienie logiki po rodzaju:
DokumentHandlowy dok = session.GetHandel().DokHandlowe.WgDaty[
(DokumentHandlowy d) => d.Data == Date.Today].FirstOrDefault();
if (dok != null && dok.Definicja.Kategoria == KategoriaHandlowa.WydanieMagazynowe)
{
// ... logika dotycząca rozchodu magazynowego
}
Pułapki:
- Rodzaj wynika z definicji, nie z symbolu. Symbol (np. „FV") jest dowolny i zależny od bazy —
rozpoznawaj po
Definicja.Kategoria, a nie po porównaniuSymbol == "FV". - Pomocnicze metody rozszerzające na enumie (
JestHandlowa,JestMagazynowa,JestZamowienie) sąinternal— z dodatku zewnętrznego ich nie wywołasz. Klasyfikuj zakresami markerów (>= HandelPierwszy and <= HandelOstatniitd.) lub porównaniem do konkretnych wartości — tak jak w snippetcie. - Wartości
*Pierwszy/*Ostatnisą oznaczone[Hidden](nie pokazują się w UI), ale to publiczne stałe enuma — wolno ich użyć w kodzie jako granic zakresu. Korektai wynikiJestDokZaliczkowy()są kalkulowane (read-only) — służą tylko do odczytu; nie próbuj ich ustawiać. Korektę tworzy się przez relacje dokumentów (IRelacjeService.NowaKorekta), a nie przez przestawienie flagi.- Sprawdzaj zaliczkę/korektę przed klasyfikacją zakresową: korekta sprzedaży nadal mieści się w
zakresie handlowym, a zaliczka bywa fakturą — dedykowane testy (
JestDokZaliczkowy,Korekta) są bardziej szczegółowe i powinny mieć pierwszeństwo. dok.Definicjamoże w teorii byćnullna świeżo utworzonym, jeszcze nieskonfigurowanym dokumencie — przy klasyfikacji dokumentów „w trakcie tworzenia" zabezpiecz dostęp doKategoria.
2. Wystawianie dokumentów
Rozdział pokazuje, jak utworzyć dokument handlowy od zera w różnych wariantach (faktura
sprzedaży, faktura zakupu, dokument magazynowy, zamówienie, dokument walutowy, dokument z usługą)
oraz jak dodawać i parametryzować pozycje. Wszystkie wzorce operują na publicznym kontrakcie
platformy: tabela DokHandlowe (session.GetHandel().DokHandlowe), definicje
DefDokHandlowych.WgSymbolu[...], pozycje PozycjaDokHandlowego.
Kolejność ustawiania pól jest istotna. Najpierw
AddRow(dok), potemDefinicja(inicjuje kategorię, kierunek magazynu, sposób liczenia VAT, walutę płatności), następnieMagazyn,Kontrahent, daty. Na pozycji najpierwTowar(inicjuje jednostkę, stawkę VAT, cenę i rabat), dopiero potemIlosc,Cena,Rabat. Cała operacja w jednej transakcjisession.Logout(editMode: true)zakończonejCommit()(kod biznesowy) /CommitUI()(worker/extender), a po niejsession.Save()— dopieroSave()księguje obroty magazynowe i wykrywa konflikty.
W4 — Faktura sprzedaży (FV)
Cel: wystawić fakturę sprzedaży: dokument rozchodowy z kontrahentem-nabywcą, pozycjami towarowymi, automatycznie wyliczoną tabelą VAT i płatnością.
Warianty:
| Wariant | Charakterystyka | Pola krytyczne |
|---|---|---|
| FV krajowa od netto | standardowa sprzedaż | Definicja=FV, LiczonaOd=Netto, Kontrahent krajowy |
| FV liczona od brutto | sprzedaż detaliczna / paragonowa | LiczonaOd=Brutto |
| FV z rabatem nagłówkowym | rabat przepisywany na pozycje | Rabat: Percent na dokumencie |
| FV dla odbiorcy unijnego | WDT — stawka 0% | kontrahent RodzajPodmiotu=Unijny, stawka z karty/UE (W11) |
| FV walutowa | sprzedaż w EUR/USD | patrz W9 |
Pola i typy: Definicja: DefDokHandlowego (DefDokHandlowych.WgSymbolu["FV"]),
Magazyn: Magazyn, Kontrahent: Kontrahent, Data: Date (data wystawienia),
DataOperacji: Date (faktyczna data sprzedaży), LiczonaOd: SposobLiczeniaVAT (Netto/Brutto),
Rabat: Percent. Wartości wyliczane: Suma: BruttoNetto, SumyVAT: SubTable<SumaVAT>,
Platnosci: SubTable<Soneta.Kasa.Platnosc> (powstaje automatycznie wg formy/terminu kontrahenta).
Snippet:
var handel = session.GetHandel();
var magazyny = session.GetMagazyny();
var crm = session.GetCRM();
using (var t = session.Logout(editMode: true))
{
var fv = new DokumentHandlowy();
session.AddRow(fv); // AddRow PRZED ustawianiem pól
fv.Definicja = handel.DefDokHandlowych.WgSymbolu["FV"]; // definicja PIERWSZA
fv.Magazyn = magazyny.Magazyny.WgSymbol["F"];
fv.Kontrahent = crm.Kontrahenci.WgKodu["Abc"]; // nabywca
fv.Data = Date.Today; // data wystawienia
fv.DataOperacji = Date.Today; // faktyczna data sprzedaży
fv.LiczonaOd = SposobLiczeniaVAT.Netto; // VAT liczony od netto
// Pozycja towarowa (szczegóły w W8):
var poz = new PozycjaDokHandlowego(fv);
session.AddRow(poz);
poz.Towar = session.GetTowary().Towary.WgKodu["BIKINI"]; // Towar PIERWSZY
poz.Ilosc = new Quantity(2, poz.Ilosc.Symbol);
poz.Cena = new DoubleCy(50m, poz.Cena.Symbol);
t.Commit(); // CommitUI() w workerze/extenderze
}
session.Save(); // tu księgują się obroty i VAT
// Odczyt wyliczonej tabeli VAT i wartości:
foreach (SumaVAT v in fv.SumyVAT) { /* v.Stawka, v.Suma (Netto/VAT/Brutto) */ }
BruttoNetto suma = fv.Suma;
Pułapki:
- Demo blokuje stan ujemny (
StanUjemnyVerifier): FV (rozchód) wymaga wcześniejszego zapisanego przyjęcia (PW/PZ) tego towaru. SamoCommitUInie księguje obrotów — magazyn aktualizuje się dopiero poSession.Save()dokumentu przychodowego. SumyVAT,Suma,Platnoscisą wyliczane z pozycji i parametrów dokumentu — nie ustawiaj ich ręcznie (ręczna korekta tabeli VAT to osobny mechanizm:KorektaVAT=true).LiczonaOdustaw przed pozycjami — zmiana po wprowadzeniu pozycji wymusza przeliczenie cen netto↔brutto.- Stawka VAT pozycji jest inicjowana z karty towaru — nie ustawiaj jej „z palca", jeśli nie musisz jej nadpisać.
W5 — Faktura zakupu (FZ)
Cel: wprowadzić fakturę zakupu otrzymaną od dostawcy: dokument przychodowy z numerem obcym dostawcy oraz datami zakupu i wystawienia dokumentu obcego.
Warianty:
| Wariant | Charakterystyka | Pola krytyczne |
|---|---|---|
| FZ krajowa | zakup od dostawcy PL | Definicja=FZ, Obcy.Numer, DataOperacji (data zakupu) |
| FZ z dostawą magazynową | zakup z przyjęciem na magazyn | Magazyn, kierunek przychodowy z definicji |
| FZ od dostawcy unijnego (WNT) | nabycie wewnątrzwspólnotowe | kontrahent RodzajPodmiotu=Unijny |
| FZ walutowa | zakup w walucie obcej | patrz W9 |
Pola i typy: Definicja=DefDokHandlowych.WgSymbolu["FZ"], Kontrahent = dostawca,
Obcy: DokumentObcy (subrow): Obcy.Numer: string (numer obcy nadany przez dostawcę),
Obcy.DataOtrzymania: Date (data dokumentu obcego). Data: Date (data wystawienia w naszym
systemie), DataOperacji: Date (faktyczna data zakupu).
Snippet:
var handel = session.GetHandel();
using (var t = session.Logout(editMode: true))
{
var fz = new DokumentHandlowy();
session.AddRow(fz);
fz.Definicja = handel.DefDokHandlowych.WgSymbolu["FZ"];
fz.Magazyn = session.GetMagazyny().Magazyny.WgSymbol["F"];
fz.Kontrahent = session.GetCRM().Kontrahenci.WgKodu["ZEFIR"]; // dostawca
fz.Data = Date.Today; // data wystawienia u nas
fz.DataOperacji = Date.Today.AddDays(-2); // faktyczna data zakupu
// Numer i data dokumentu obcego (od dostawcy):
fz.Obcy.Numer = "FV/2026/06/123"; // numer obcy
fz.Obcy.DataOtrzymania = Date.Today.AddDays(-2); // data dokumentu obcego
var poz = new PozycjaDokHandlowego(fz);
session.AddRow(poz);
poz.Towar = session.GetTowary().Towary.WgKodu["BIKINI"];
poz.Ilosc = new Quantity(10, poz.Ilosc.Symbol);
poz.Cena = new DoubleCy(30m, poz.Cena.Symbol);
t.Commit();
}
session.Save();
Pułapki:
Obcyto subrow (pole złożone) — nie da się przypisaćfz.Obcy = …; ustawiaj jego pola (fz.Obcy.Numer,fz.Obcy.DataOtrzymania).- Rozróżniaj trzy daty:
Data(wystawienia u nas),DataOperacji(faktyczna data zakupu/sprzedaży, decyduje o okresie magazynowym),Obcy.DataOtrzymania(data na dokumencie obcym). To trzy różne pola — nie myl ich. - FZ z przyjęciem na magazyn księguje przychód → po
Save()powstają zasoby (dok.Zasoby). - Indeks
WgKontrahentaObcy(Kontrahent + numer obcy) pozwala wykryć duplikat faktury od tego samego dostawcy — sprawdzaj przed dodaniem.
W6 — Dokument magazynowy (PZ / WZ / RW / PW)
Cel: wystawić czysto magazynowy dokument wpływający na stan magazynu, bez części handlowej (VAT/płatności) lub z minimalną.
Warianty:
| Wariant | Symbol | Kierunek | Zastosowanie |
|---|---|---|---|
| Przyjęcie zewnętrzne | PZ |
przychód | przyjęcie od dostawcy |
| Przyjęcie wewnętrzne | PW |
przychód | przyjęcie z produkcji / bilans otwarcia |
| Wydanie zewnętrzne | WZ |
rozchód | wydanie odbiorcy |
| Rozchód wewnętrzny | RW |
rozchód | zużycie wewnętrzne |
Pola i typy: Definicja=DefDokHandlowych.WgSymbolu["PW"] (itd.), Magazyn: Magazyn (wymagany),
Kontrahent (gdy dotyczy — PZ/WZ tak, RW/PW zwykle nie), Data, DataOperacji. Kierunek
magazynu (KierunekMagazynu: KierunekPartii — Przychód=1, Rozchód=-1) jest ustawiany z
definicji (readonly="set"). Wynik: dok.Zasoby (przy przychodzie), dok.Obroty.
Snippet:
var handel = session.GetHandel();
// Przyjęcie wewnętrzne PW (przychód — buduje stan magazynu pod późniejsze rozchody):
using (var t = session.Logout(editMode: true))
{
var pw = new DokumentHandlowy();
session.AddRow(pw);
pw.Definicja = handel.DefDokHandlowych.WgSymbolu["PW"]; // kierunek z definicji
pw.Magazyn = session.GetMagazyny().Magazyny.WgSymbol["F"];
pw.Data = Date.Today;
pw.DataOperacji = Date.Today;
var poz = new PozycjaDokHandlowego(pw);
session.AddRow(poz);
poz.Towar = session.GetTowary().Towary.WgKodu["BIKINI"];
poz.Ilosc = new Quantity(100, poz.Ilosc.Symbol);
poz.Cena = new DoubleCy(25m, poz.Cena.Symbol);
t.Commit();
}
session.Save(); // dopiero teraz powstają zasoby
// Stan magazynowy po przyjęciu:
foreach (var z in pw.Zasoby) { /* z.* — partia, ilość, magazyn */ }
Pułapki:
Magazynjest wymagany (required) dla dokumentów magazynowych — bez niegoSave()rzuciRowException.KierunekMagazynu/TypPartiisąreadonly="set"— wynikają z definicji, nie ustawiaj ich ręcznie.- Rozchód (WZ/RW) na bazie Demo wymaga wcześniejszego zapisanego przychodu (PW/PZ) — inaczej
StanUjemnyVerifierzablokujeSave(). - Obroty/zasoby księgują się po
Session.Save(), nie poCommit()/CommitUI(). Aby je odczytać, zapisz dokument i odśwież.
W7 — Zamówienie (ZO / ZD)
Cel: wystawić zamówienie od odbiorcy (ZO) lub zamówienie do dostawcy (ZD). Zamówienie nie wpływa na stan magazynowy (może tworzyć rezerwacje), jest dokumentem nadrzędnym dla realizacji (FV/WZ — patrz rozdział o relacjach).
Warianty:
| Wariant | Symbol | Strona | Realizacja |
|---|---|---|---|
| Zamówienie odbiorcy | ZO |
klient zamawia u nas | → FV / WZ przez IRelacjeService |
| Zamówienie do dostawcy | ZD |
my zamawiamy u dostawcy | → FZ / PZ przez IRelacjeService |
| ZO z rezerwacją | ZO |
jw. | rezerwacja zasobu (dok.Rezerwacja) |
| ZO z terminem dostawy | ZO |
jw. | Dostawa.Termin |
Pola i typy: Definicja=DefDokHandlowych.WgSymbolu["ZO"] / ["ZD"], Kontrahent,
Magazyn, Data, DataOperacji, Dostawa: DokumentDostawa (subrow): Dostawa.Termin: Date
(termin realizacji), Dostawa.Sposob: string. Powiązanie z realizacją: dok.Rezerwacja,
generowanie dokumentu podrzędnego przez IRelacjeService.NowyPodrzednyIndywidualny(...).
Snippet:
var handel = session.GetHandel();
using (var t = session.Logout(editMode: true))
{
var zo = new DokumentHandlowy();
session.AddRow(zo);
zo.Definicja = handel.DefDokHandlowych.WgSymbolu["ZO"]; // zamówienie odbiorcy
zo.Magazyn = session.GetMagazyny().Magazyny.WgSymbol["F"];
zo.Kontrahent = session.GetCRM().Kontrahenci.WgKodu["Abc"]; // zamawiający odbiorca
zo.Data = Date.Today;
zo.DataOperacji = Date.Today;
zo.Dostawa.Termin = Date.Today.AddDays(7); // oczekiwany termin dostawy
var poz = new PozycjaDokHandlowego(zo);
session.AddRow(poz);
poz.Towar = session.GetTowary().Towary.WgKodu["BIKINI"];
poz.Ilosc = new Quantity(5, poz.Ilosc.Symbol);
poz.Cena = new DoubleCy(50m, poz.Cena.Symbol);
t.Commit();
}
session.Save();
Pułapki:
- Zamówienie nie buduje stanu magazynu — to dokument planistyczny. Realizację (FV/WZ z ZO,
FZ/PZ z ZD) tworzysz przez
IRelacjeService(rozdział o relacjach) — dokument nadrzędny musi być wtedy zatwierdzony. Dostawato subrow — ustawiaj pola (zo.Dostawa.Termin), nie przypisuj całego obiektu.- Rezerwacja ilościowa zamówienia jest zarządzana wewnętrznym workerem
(
ZmienRezerwacjeIlosciowaWorker— internal, niedostępny z dodatku); z poziomu publicznego odczytujzo.Rezerwacja, a rezerwacje steruj przez definicję dokumentu i relacje.
W8 — Dodawanie pozycji (towar, ilość, cena, rabat, jednostka)
Cel: dodać pozycję towarową do dokumentu — z automatycznym pobraniem ceny/rabatu z cennika lub z ręcznym nadpisaniem.
Warianty:
| Wariant | Operacja | Pole |
|---|---|---|
| Pozycja z automatyczną ceną | cena i rabat pobrane z cennika/karty | tylko Towar + Ilosc |
| Ręczna cena | nadpisanie ceny | Cena: DoubleCy (ustawia KorektaCeny=true) |
| Ręczny rabat | nadpisanie rabatu | Rabat: Percent (ustawia KorektaRabatu=true) |
| Inna jednostka | sprzedaż w jednostce zbiorczej | Ilosc z symbolem jednostki towaru |
| Pozycja bez rabatu | wyłączenie rabatu | BezRabatu=true |
| Ręczna wartość | korekta wartości pozycji | WartoscCy: Currency |
Pola i typy (PozycjaDokHandlowego): Towar: Towar (ustaw pierwszy — inicjuje jednostkę,
stawkę VAT, cenę i rabat), Ilosc: Quantity (ilość + symbol jednostki), Cena: DoubleCy (cena
- symbol waluty; netto lub brutto wg
Dokument.LiczonaOd),Rabat: Percent,RabatCeny: DoubleCy(rabat kwotowy),WartoscCy: Currency(wartość pozycji),DefinicjaStawki: DefinicjaStawkiVat(stawka VAT). Flagi nadpisań:KorektaCeny: bool,KorektaRabatu: bool,BezRabatu: bool.
Snippet:
var towary = session.GetTowary();
using (var t = session.Logout(editMode: true))
{
// Wariant A — cena i rabat pobrane automatycznie z cennika/karty towaru:
var poz1 = new PozycjaDokHandlowego(dok);
session.AddRow(poz1);
poz1.Towar = towary.Towary.WgKodu["BIKINI"]; // ustawia jednostkę, cenę, rabat, stawkę VAT
poz1.Ilosc = new Quantity(3, poz1.Ilosc.Symbol); // symbol jednostki z towaru
// Cena i rabat zostają takie, jakie zaproponował cennik.
// Wariant B — ręczne nadpisanie ceny i rabatu:
var poz2 = new PozycjaDokHandlowego(dok);
session.AddRow(poz2);
poz2.Towar = towary.Towary.WgKodu["BIKINI"];
poz2.Ilosc = new Quantity(10, poz2.Ilosc.Symbol);
poz2.Cena = new DoubleCy(48m, poz2.Cena.Symbol); // nadpisanie ceny → KorektaCeny=true
poz2.Rabat = new Percent(0.1); // 10% rabatu → KorektaRabatu=true
t.Commit();
}
session.Save();
Pułapki:
Towarustawiaj jako pierwszy. Dopiero on inicjuje symbol jednostki naIlosc/Cena, stawkę VAT i proponowaną cenę/rabat. UstawienieIlosc/CenaprzedTowaroperowałoby na pustych symbolach.IlosctoQuantity(wartość + symbol jednostki),CenatoDoubleCy(wartość + symbol waluty) — twórz je z symbolem już ustawionym na pozycji:new Quantity(n, poz.Ilosc.Symbol),new DoubleCy(c, poz.Cena.Symbol). Nie wstawiaj „gołego"decimal.- Ręczne ustawienie
Cena/Rabatzapala flagiKorektaCeny/KorektaRabatu— od tej chwili platforma nie przeliczy już automatycznie tej wartości (np. po zmianie kontrahenta/ilości). Cenajest netto albo brutto zależnie odDokument.LiczonaOd— interpretuj ją spójnie z dokumentem.- Konstruktor pozycji wymaga dokumentu:
new PozycjaDokHandlowego(dok), a po nimsession.AddRow(poz).
W9 — Dokument w walucie obcej
Cel: wystawić dokument rozliczany w walucie obcej (EUR/USD): wskazać walutę płatności, tabelę kursową, datę kursu oraz — w razie potrzeby — wpisać kurs ręcznie.
Warianty:
| Wariant | Mechanizm | Pola |
|---|---|---|
| Kurs z tabeli na datę | kurs pobierany z TabelaKursowa |
TabelaKursowa, DataKursu |
| Kurs ręczny | użytkownik podaje kurs | KursWaluty: double |
| Zmiana waluty istniejącego dokumentu | przeliczenie dokumentu i cen | akcja „Zmień walutę dokumentu i cen..." (worker) |
| Waluta na pozycji | cena w walucie | poz.Cena: DoubleCy z symbolem waluty |
Pola i typy: TabelaKursowa: TabelaKursowa (wymagana — WalutyModule.GetInstance(session).TabeleKursowe),
DataKursu: Date, KursWaluty: double, BruttoCy: Currency (kwota płatności w walucie).
Waluta płatności wynika z definicji (DefDokHandlowego.WalutaPlatnosci). Zmianę waluty
istniejącego dokumentu realizuje akcja menu Czynności sterowana klasą parametrów
DokumentHandlowyZmianaWalutyWorkerParams (publiczna): Waluta, WalutaBazowa (read-only),
TabelaKursowa, Data, KursWaluty, ZmienCeny: bool.
Snippet:
using Microsoft.Extensions.DependencyInjection; // dla GetRequiredService, jeśli potrzebne
using Soneta.Waluty;
var wm = WalutyModule.GetInstance(session); // session.GetWaluty() jest internal
var eur = wm.Waluty.WgSymbolu["EUR"];
var tabela = wm.TabeleKursowe.NBP; // np. tabela NBP
// Zmiana waluty istniejącego (buforowego) dokumentu na EUR z ręcznym kursem.
// Worker uruchamiany jest jak akcja menu Czynności — parametry przekazujemy przez Context:
var paramy = new DokumentHandlowyZmianaWalutyWorkerParams(context, dok)
{
Waluta = eur,
TabelaKursowa = tabela,
KursWaluty = 4.3344, // kurs ręczny; przy zmianie tabeli/daty platforma proponuje kurs sama
ZmienCeny = true, // przelicz także ceny pozycji
};
context.Set(paramy);
// akcja „Zmień walutę dokumentu i cen..." (ZmienWalute) wykonuje przeliczenie w transakcji UI
// Dokument walutowy „od zera": ustaw tabelę i datę kursu przed pozycjami:
using (var t = session.Logout(editMode: true))
{
dok.TabelaKursowa = tabela;
dok.DataKursu = Date.Today;
// dok.KursWaluty = 4.3344; // tylko gdy chcesz wymusić kurs ręczny
t.Commit();
}
session.Save();
Pułapki:
- Brak kursu na datę = wyjątek. Jeśli w bazie nie ma kursu danej waluty na
DataKursu, operacja rzucaKursWalutyNotFoundException. Na bazie Demo nie ma kursu EUR „na dziś" — albo dodaj kurs do tabeli kursowej, albo wpisz kurs ręcznie (KursWaluty). TabelaKursowajest wymagana dla dokumentu walutowego.session.GetWaluty()jest internal — używajWalutyModule.GetInstance(session).- Worker
DokumentHandlowyZmianaWalutyWorkerjest klasą internal — z dodatku nie tworzysz jej instancji bezpośrednio; uruchamiasz akcję przez framework Czynności, przekazując publiczną klasęDokumentHandlowyZmianaWalutyWorkerParamsprzezContext. - Zmiana waluty dokumentu jest możliwa tylko w buforze (
dok.Bufor == true).
W10 — Dokument z usługą (pozycja usługowa bez wpływu na magazyn)
Cel: dodać do dokumentu pozycję usługową (np. „MONTAZ", „TRANSPORT") — towar typu usługa nie ma wpływu na stan magazynu, ale uczestniczy w wartości i tabeli VAT.
Warianty:
| Wariant | Charakterystyka |
|---|---|
| FV tylko z usługą | faktura za samą usługę (np. montaż) — brak obrotu magazynowego |
| FV mieszana | towar magazynowy + pozycja usługowa na jednym dokumencie |
| Usługa rozliczana ilościowo | usługa w jednostce (np. „TRANSPORT" w km) |
Pola i typy: identyczne jak w W8 (Towar, Ilosc, Cena, Rabat, DefinicjaStawki).
Różnica jest w karcie towaru: towar usługowy nie generuje obrotu magazynowego —
poz.IloscMagazynu pozostaje zerowa, dok.Zasoby/dok.Obroty nie powstają dla tej pozycji.
Snippet:
var handel = session.GetHandel();
var towary = session.GetTowary();
using (var t = session.Logout(editMode: true))
{
var fv = new DokumentHandlowy();
session.AddRow(fv);
fv.Definicja = handel.DefDokHandlowych.WgSymbolu["FV"];
fv.Magazyn = session.GetMagazyny().Magazyny.WgSymbol["F"];
fv.Kontrahent = session.GetCRM().Kontrahenci.WgKodu["Abc"];
fv.Data = Date.Today;
fv.DataOperacji = Date.Today;
// Pozycja usługowa — towar "MONTAZ" jest usługą (BEZ wpływu na magazyn):
var poz = new PozycjaDokHandlowego(fv);
session.AddRow(poz);
poz.Towar = towary.Towary.WgKodu["MONTAZ"]; // usługa
poz.Ilosc = new Quantity(1, poz.Ilosc.Symbol);
poz.Cena = new DoubleCy(200m, poz.Cena.Symbol);
t.Commit();
}
session.Save();
Pułapki:
- O tym, czy pozycja wpływa na magazyn, decyduje typ towaru (usługa vs towar magazynowy), a nie
pole na pozycji. Dla usługi
StanUjemnyVerifiernie blokuje wystawienia rozchodu — usługa nie pobiera ze stanu. - Faktura zawierająca wyłącznie usługi nie tworzy obrotów magazynowych, ale nadal liczy tabelę VAT i płatność.
- Usługa też ma jednostkę (np. „TRANSPORT" w km) —
Iloscużywa symbolu jednostki z karty towaru.
W11 — Odbiorca / płatnik inny niż kontrahent + miejsce dostawy
Cel: wystawić dokument, na którym nabywca (Kontrahent) różni się od odbiorcy towaru
(Odbiorca), wskazać miejsce dostawy oraz — gdy płatnikiem jest inny podmiot — rozliczyć
płatność na płatnika.
Warianty:
| Wariant | Pole / mechanizm |
|---|---|
| Inny odbiorca towaru | Odbiorca: Kontrahent |
| Miejsce dostawy odbiorcy | OdbiorcaMiejsceDostawy: Lokalizacja |
| Osoba odbierająca | OsobaKontrahenta: KontaktOsoba, Osoba: string (podpisujący) |
| Adres / parametry przesyłki | subrow Dostawa (Dostawa.Termin, Dostawa.Sposob, Dostawa.Odpowiedzialny) |
| Inny płatnik | dok.InnyPłatnik (kalkulowane — wynika z relacji podmiotów / płatności) |
Pola i typy: Kontrahent: Kontrahent (nabywca — strona transakcji/VAT),
Odbiorca: Kontrahent (odbiorca towaru — dane dostawy), OdbiorcaMiejsceDostawy: Lokalizacja
(miejsce docelowe dostawy), OsobaKontrahenta: KontaktOsoba, Osoba: string.
InnyPłatnik jest kalkulowane (read-only) — płatnika ustawia się przez relacje podmiotów
(płatnik podmiotu) lub przez płatność, nie przez bezpośrednie przypisanie na dokumencie.
Snippet:
var crm = session.GetCRM();
using (var t = session.Logout(editMode: true))
{
// dok utworzony jak w W4; Kontrahent = nabywca (np. centrala):
dok.Kontrahent = crm.Kontrahenci.WgKodu["Abc"]; // nabywca / strona VAT
dok.Odbiorca = crm.Kontrahenci.WgKodu["ZEFIR"]; // odbiorca towaru (inny podmiot)
// Miejsce dostawy odbiorcy (lokalizacja zdefiniowana u odbiorcy):
// dok.OdbiorcaMiejsceDostawy = ... // rekord Lokalizacja powiązany z odbiorcą
dok.Osoba = "Jan Kowalski"; // osoba podpisująca po stronie kontrahenta
// Parametry dostawy (subrow):
dok.Dostawa.Termin = Date.Today.AddDays(3);
dok.Dostawa.Sposob = "Kurier";
t.Commit();
}
session.Save();
// Odczyt płatnika (kalkulowane):
bool jestInnyPlatnik = dok.InnyPłatnik;
Pułapki:
Kontrahentto nabywca (strona transakcji i VAT),Odbiorcato fizyczny odbiorca towaru — to dwa różne pola, oba typuKontrahent. Faktura wystawiana jest naKontrahent, dostawa idzie doOdbiorca.InnyPłatnikjest kalkulowane — nie przypisuj go ręcznie. Innego płatnika ustala się przez relacje podmiotów (płatnik nadrzędny) lub przez konfigurację płatności dokumentu.OdbiorcaMiejsceDostawyto referencja do rekorduLokalizacja(zwykle zdefiniowanego u odbiorcy) — pobierz istniejącą lokalizację, nie twórz „w locie".Dostawato subrow — ustawiaj jego pola, nie przypisuj całego obiektu.- Zmiana płatnika rozkłada się na płatności; do podziału płatności na raty/płatników służy publiczny
worker
PodzialPlatnosciWorker.
3. Stany dokumentu i cykl życia
Stan dokumentu handlowego steruje całym jego cyklem życia: od bufora (rekord roboczy, swobodnie
edytowalny i usuwalny), przez zatwierdzenie (księgowanie obrotów magazynowych, generowanie
płatności, blokada większości pól), aż po anulowanie. Stanem steruje jedno zapisywalne pole
dok.Stan, a dodatkowe operacje serwisowe (naprawa, przeliczenie) wykonują publiczne workery.
Fundamenty (sesja, transakcja edycyjna
session.Logout(editMode: true),Commit/CommitUI, blokada optymistyczna wSave()) opisujesafe-code.md— tu się do nich odwołujemy, nie powtarzamy. Cały kod jest zgodny z C# 10 i operuje wyłącznie na publicznym kontrakcie platformy.
Fakty o stanie (zweryfikowane):
- Pole sterujące:
dok.Stan: Soneta.Handel.StanDokumentuHandlowego(zapisywalne w transakcji). - Enum
StanDokumentuHandlowego:Bufor=0,Zatwierdzony=1,Zablokowany=2,Anulowany=3. WartośćZablokowanyustawia platforma (np. po zaksięgowaniu w ewidencji) — nie ustawiaj jej z dodatku „z palca". - Skróty kalkulowane (tylko do odczytu,
bool):dok.Bufor,dok.Zatwierdzony,dok.Anulowany. - Usunięcie z bufora:
dok.Delete()w transakcji (tylko gdy brak zależności). - Workery publiczne (cykl życia / naprawa):
Soneta.Handel.PoprawaStanuDokumentuWorker,Soneta.Magazyny.PrzeliczenieStanuWorker.
W12 — Zatwierdzenie dokumentu (bufor → zatwierdzony)
Cel: przeprowadzić dokument z bufora do stanu zatwierdzonego. Dopiero zatwierdzenie + Save()
księguje obroty magazynowe, tworzy zasoby/partie, generuje płatności i czyni dokument nadrzędnym dla
relacji (np. ZO→FV, FA→WZ — patrz rozdział o relacjach).
Warianty:
| Wariant | Operacja | Uwaga |
|---|---|---|
| Zatwierdzenie pojedyncze | dok.Stan = StanDokumentuHandlowego.Zatwierdzony |
w transakcji + Save() |
| Zatwierdzenie zbiorcze | worker EwidencjonowanieZbiorczeWorker ([Context] DokumentHandlowy[]) |
wiele dokumentów naraz |
| Sprawdzenie stanu | dok.Zatwierdzony / dok.Bufor (kalkulowane bool) |
bez porównywania enuma |
Stan Zablokowany |
ustawiany przez platformę (księgowanie ewidencji) | nie ustawiaj ręcznie |
Pola i typy: dok.Stan: StanDokumentuHandlowego (zapisywalne), dok.Bufor/Zatwierdzony/Anulowany: bool (kalkulowane). Wartości magazynowe widoczne po Save(): dok.Zasoby, dok.Obroty,
dok.SumyVAT.
Snippet:
var hm = session.GetHandel();
var dok = hm.DokHandlowe.WgDaty[/* ... */]; // odczytany dokument w buforze
using (var t = session.Logout(editMode: true))
{
dok.Stan = StanDokumentuHandlowego.Zatwierdzony; // bufor -> zatwierdzony
t.Commit(); // CommitUI() w workerze/extenderze
}
session.Save(); // DOPIERO TERAZ księgowane są obroty/zasoby/płatności
// Sprawdzenie po zapisie — czytaj pola kalkulowane, nie porównuj enuma:
if (dok.Zatwierdzony)
{
foreach (var z in dok.Zasoby) { /* zasoby utworzone przez dokument przychodowy */ }
}
Pułapki:
- Magazyn księguje się dopiero po
Save()— samoCommit()/CommitUI()nie tworzy obrotów ani zasobów. Jeśli baza blokuje stan ujemny (weryfikatorStanUjemnyVerifier, jak w bazie Demo), rozchód (FV/WZ/RW) wymaga wcześniej zapisanego przyjęcia (PW/PZ) tego towaru — inaczejSave()rzuci wyjątek. - Zatwierdzenie uruchamia walidatory dokumentu (kompletność pozycji, magazyn, kontrahent, tabela
VAT). Błędy wychodzą w
Commit()/Save()jakoRowException— nie połykaj ich (safe-code §4). - W workerze/extenderze użyj
t.CommitUI()zamiastt.Commit()(worker-extender.md). - Po
Save()w środku jednej sesji zamyka się okno edycji; kolejna edycja na tym samym obiekcie bez ponownegoLogoutrzuciAccessWriteDenied. Wzorzec: zapis → odczyt na świeżej sesji. - Nie ustawiaj
Stan = Zablokowanyz dodatku — to stan wewnętrzny platformy (np. po zaksięgowaniu w ewidencji).
W13 — Cofnięcie do bufora / odtwierdzenie
Cel: wycofać zatwierdzony dokument z powrotem do bufora, aby go poprawić. Operacja odksięgowuje to, co zatwierdzenie zaksięgowało (obroty, płatności), więc jest dozwolona tylko gdy nie ma zależności blokujących (zamknięty okres magazynowy/VAT, zaksięgowanie w ewidencji, dokumenty podrzędne).
Warianty:
| Wariant | Operacja | Warunek dozwolenia |
|---|---|---|
| Cofnięcie do bufora | dok.Stan = StanDokumentuHandlowego.Bufor |
okres otwarty, brak podrzędnych, nie zaksięgowany |
| Dokument zablokowany | najpierw zdjąć blokadę po stronie ewidencji/księgowości | dok.Stan == Zablokowany blokuje cofnięcie |
| Z dokumentami podrzędnymi | najpierw usuń/rozłącz podrzędne (relacje) | patrz rozdział o relacjach i W16 |
Pola i typy: dok.Stan: StanDokumentuHandlowego, dok.Zatwierdzony/Bufor: bool (kalkulowane),
dok.DokumentyMagazynowe, dok.DokumentyHandlowe, dok.DokumentyKorygujące (kalkulowane — do
sprawdzenia zależności przed cofnięciem).
Snippet:
var dok = session.GetHandel().DokHandlowe.WgDaty[/* ... */];
if (!dok.Zatwierdzony) return; // już w buforze / anulowany — nic do zrobienia
// Cofnięcie jest zablokowane, gdy istnieją dokumenty podrzędne (korekty, magazynowe):
bool maZaleznosci = dok.DokumentyKorygujące.Any() || dok.DokumentyMagazynowe.Length > 0;
if (maZaleznosci)
throw new BusException(
"Nie można cofnąć dokumentu do bufora — istnieją powiązane dokumenty.".Translate());
using (var t = session.Logout(editMode: true))
{
dok.Stan = StanDokumentuHandlowego.Bufor; // odtwierdzenie: zatwierdzony -> bufor
t.Commit(); // CommitUI() w workerze/extenderze
}
session.Save(); // tu odksięgowanie obrotów/płatności i wykrycie konfliktów
Pułapki:
- Cofnięcie dokumentu w zamkniętym okresie magazynowym/VAT albo zaksięgowanego w ewidencji
zakończy się wyjątkiem w
Commit()/Save(). Sprawdź stan otwarcia okresu zanim spróbujesz. - Dokument w stanie
Zablokowanynie cofniesz przezdok.Stan = Bufor— blokada wynika z innego modułu (np. ewidencja zaksięgowana). Do diagnozy/naprawy rozbieżności stanu dokument↔ewidencja służyPoprawaStanuDokumentuWorker(W15). - Jeśli istnieją dokumenty podrzędne (korekty, powiązane magazynowe), cofnięcie się nie powiedzie — najpierw rozwiąż powiązania (rozdział o relacjach), patrz też W16.
- To nie to samo co anulowanie (W14): cofnięcie wraca do edytowalnego bufora, anulowanie zamyka dokument w stanie nieodwracalnym.
W14 — Anulowanie dokumentów
Cel: unieważnić dokument, który nie powinien już brać udziału w obrocie (np. wystawiony omyłkowo),
zachowując go w bazie dla ciągłości numeracji i audytu. Anulowanie odksięgowuje skutki magazynowe i
finansowe, ale rekord pozostaje (w przeciwieństwie do Delete()).
Warianty:
| Wariant | Operacja | Uwaga |
|---|---|---|
| Anulowanie z bufora | dok.Stan = StanDokumentuHandlowego.Anulowany |
bufor → anulowany |
| Anulowanie zatwierdzonego | dok.Stan = StanDokumentuHandlowego.Anulowany |
odksięgowuje obroty/płatności; tylko gdy okres otwarty |
| Sprawdzenie | dok.Anulowany (kalkulowane bool) |
bez porównywania enuma |
Pola i typy: dok.Stan: StanDokumentuHandlowego, dok.Anulowany: bool (kalkulowane).
Snippet:
var dok = session.GetHandel().DokHandlowe.WgDaty[/* ... */];
if (dok.Anulowany) return; // już anulowany
using (var t = session.Logout(editMode: true))
{
dok.Stan = StanDokumentuHandlowego.Anulowany; // bufor lub zatwierdzony -> anulowany
t.Commit(); // CommitUI() w workerze/extenderze
}
session.Save();
// Po anulowaniu dokument pozostaje w bazie (numeracja zachowana), ale nie wpływa na stany:
bool wycofany = dok.Anulowany;
Pułapki:
- Anulowanie zatwierdzonego dokumentu odksięgowuje jego skutki — w zamkniętym okresie albo gdy istnieją dokumenty podrzędne kończy się wyjątkiem. Najpierw rozwiąż zależności (jak w W13).
- Anulowanie jest nieodwracalne — nie ma przejścia
Anulowany → Buforna poziomie polaStan. Gdy chcesz tylko poprawić dokument, użyj cofnięcia do bufora (W13). - Anulowany dokument zwykle nie powinien być źródłem relacji ani korekt — generowanie podrzędnych z anulowanego nadrzędnego zostanie odrzucone.
- Do trwałego usunięcia rekordu (gdy dozwolone) służy
Delete()(W16), a nie anulowanie — anulowanie zachowuje rekord i numer.
W15 — Naprawa i przeliczenie stanu dokumentu
Cel: naprawić rozbieżności między dokumentem a jego skutkami: stan dokumentu vs stan dokumentu
ewidencji (PoprawaStanuDokumentuWorker) oraz zgodność obrotów/zasobów magazynowych z pozycjami
(PrzeliczenieStanuWorker). To operacje serwisowe — uruchamiaj świadomie, nie w pętli zwykłej
logiki.
Warianty:
| Wariant | Worker (publiczny) | Akcja menu / wejście |
|---|---|---|
| Naprawa stanu dokumentu (synchron. z ewidencją) | Soneta.Handel.PoprawaStanuDokumentuWorker |
„Narzędziowe/Naprawa stanu dokumentu"; [Context] Dokument |
| Sprawdzenie poprawności obrotów (bez zapisu) | Soneta.Magazyny.PrzeliczenieStanuWorker, Opcje.SprawdzićPoprawność |
„Narzędziowe/Naliczenie obrotów towaru" |
| Ponowne pełne przeliczenie | PrzeliczenieStanuWorker, Opcje.PonowniePrzeliczyć |
jw. (zapis w transakcji) |
| Poprawa tylko błędnych | PrzeliczenieStanuWorker, Opcje.PoprawićTylkoBłędne |
jw. |
| Poprawa / sprawdzenie samych obrotów | Opcje.PoprawićObroty / Opcje.SprawdzićObroty |
jw. |
Pola i typy (publiczny kontrakt workerów):
PoprawaStanuDokumentuWorker: property[Context] public DokumentHandlowy Dokument; akcjapublic void NaprawStan(); predykat widocznościpublic static bool IsVisibleNaprawStan(DokumentHandlowy dokument). Worker sam zarządza transakcją wewnątrzNaprawStan()(synchronizujedok.Stanz dokumentem ewidencji, w razie potrzeby tworzy/kasuje ewidencję, może przestawićStannaZablokowany/Zatwierdzony).PrzeliczenieStanuWorker: enumpublic enum Opcje { SprawdzićPoprawność, PoprawićTylkoBłędne, PrzeliczyćTylkoNiepoprawione, PonowniePrzeliczyć, PoprawićObroty, SprawdzićObroty }; konstruktor publicznyPrzeliczenieStanuWorker(Opcje wykonaj, bool wszystkieMagazyny, bool rozchód0, bool przywracajWartość); property[Context]Dokument,Towar,Magazyny(Magazyn[]); akcjapublic void PrzeliczStan(). Worker sam otwiera transakcje wewnątrzPrzeliczStan().
Snippet:
var dok = session.GetHandel().DokHandlowe.WgDaty[/* ... */];
// 1. Naprawa rozbieżności stanu dokumentu względem dokumentu ewidencji.
// Worker sam prowadzi transakcje — ustaw tylko kontekst i wywołaj akcję.
var naprawa = new PoprawaStanuDokumentuWorker { Dokument = dok };
naprawa.NaprawStan();
session.Save(); // utrwalenie zmian dokonanych przez workera
// 2. Sprawdzenie poprawności obrotów dokumentu BEZ wprowadzania zmian (tryb diagnostyczny):
var sprawdz = new PrzeliczenieStanuWorker(
PrzeliczenieStanuWorker.Opcje.SprawdzićPoprawność,
wszystkieMagazyny: false, rozchód0: false, przywracajWartość: true) { Dokument = dok };
sprawdz.PrzeliczStan(); // tryb SprawdzićPoprawność nie commituje — tylko raportuje (Trace)
// 3. Pełne ponowne przeliczenie obrotów dokumentu (modyfikuje dane):
var przelicz = new PrzeliczenieStanuWorker(
PrzeliczenieStanuWorker.Opcje.PoprawićTylkoBłędne,
wszystkieMagazyny: false, rozchód0: false, przywracajWartość: true) { Dokument = dok };
przelicz.PrzeliczStan();
session.Save();
Pułapki:
- Oba workery same zarządzają transakcjami wewnątrz swoich akcji (
NaprawStan/PrzeliczStan). Nie owijaj wywołania własnymsession.Logout(true)— wystarczysession.Save()po akcji, by utrwalić zmiany. - W realnej aplikacji akcje są rejestrowane z
Mode = ActionMode.IsolatedSession | Progress, czyli uruchamiają się w izolowanej sesji. Przy programowym wywołaniu działasz na bieżącej sesji — upewnij się, że nie koliduje to z innymi otwartymi transakcjami. Opcje.SprawdzićPoprawnośćto tryb tylko diagnostyczny — nie zmienia danych, raportuje przezTrace. Do faktycznej naprawy użyjPoprawićTylkoBłędne/PonowniePrzeliczyć.PrzeliczenieStanuWorkerrzucaRowException, gdy napotka obrót w zamkniętym okresie magazynowym albo dokument korygowany w buforze („Dokument korygowany … w buforze. Należy go zatwierdzić.") — obsłuż te przypadki, nie wywołuj przeliczenia na ślepo.PoprawaStanuDokumentuWorker.IsVisibleNaprawStanzwracafalsedla dokumentów z obsługą technologii produkcji i magazynu pozabilansowego — to sygnał, że dla takich dokumentów naprawa nie ma zastosowania.- To są narzędzia serwisowe — nie używaj ich jako rutynowego elementu logiki tworzenia dokumentów.
W16 — Bezpieczne usunięcie dokumentu z bufora i obsługa zależności
Cel: trwale usunąć dokument z bazy (Delete()), gdy jest błędny i jeszcze niepowiązany. Usuwanie
jest dozwolone wyłącznie w buforze i tylko gdy nie istnieją zależności (rezerwacje, dokumenty
magazynowe/handlowe powiązane, korekty). W przeciwnym razie świadomie odmów (lub anuluj — W14).
Warianty:
| Wariant | Sytuacja | Zalecenie |
|---|---|---|
| Usunięcie czyste | bufor, brak powiązań i rezerwacji | dozwolone (dok.Delete()) |
| Dokument zatwierdzony | poza buforem | najpierw cofnij do bufora (W13) lub anuluj (W14) |
| Z rezerwacją | dok.Rezerwacja != null |
usuń/zwolnij rezerwację najpierw (relacje) |
| Z dokumentami powiązanymi | DokumentyMagazynowe/DokumentyHandlowe/korekty niepuste |
rozłącz/usuń podrzędne lub anuluj |
Pola i typy (do oceny zależności — kalkulowane, tylko odczyt): dok.Bufor: bool,
dok.Rezerwacja, dok.DokumentyMagazynowe: DokumentHandlowy[], dok.DokumentyHandlowe: DokumentHandlowy[], dok.DokumentyKorygujące: IEnumerable, dok.DokumentKorygowany,
dok.DokumentyZaliczkowe.
Snippet:
var dok = session.GetHandel().DokHandlowe.WgDaty[/* ... */];
// 1. Usuwać można tylko z bufora:
if (!dok.Bufor)
throw new BusException(
"Usunąć można tylko dokument w buforze. Cofnij do bufora lub anuluj.".Translate());
// 2. Zależności blokujące usunięcie (rezerwacja, powiązane, korekty):
bool maZaleznosci =
dok.Rezerwacja != null ||
dok.DokumentyMagazynowe.Length > 0 ||
dok.DokumentyHandlowe.Length > 0 ||
dok.DokumentyKorygujące.Any();
if (maZaleznosci)
throw new BusException(
"Nie można usunąć dokumentu — istnieją powiązania (rezerwacja/dokumenty/korekty).".Translate());
using (var t = session.Logout(editMode: true))
{
dok.Delete(); // twarde usunięcie — tylko gdy bufor i brak zależności
t.Commit(); // CommitUI() w workerze/extenderze
}
session.Save(); // integralność weryfikowana także tutaj
Pułapki:
- Sprawdzaj zależności przed
Delete(). Próba usunięcia powiązanego dokumentu i tak zostanie odrzucona przez integralność (wyjątek wSave()), ale lepiej zdecydować świadomie i zwrócić czytelny komunikat. - Usunięcie usuwa też pozycje dokumentu — wykonuj je jedną transakcją; nie kasuj pozycji „ręcznie"
przed
dok.Delete(), jeśli i tak usuwasz cały dokument. - Gdy dokument jest zatwierdzony, najpierw cofnij go do bufora (W13). Jeśli cofnięcie jest zablokowane (okres zamknięty, podrzędne), rozważ anulowanie (W14) zamiast usuwania — anulowanie zachowuje numer i ścieżkę audytu.
- Rezerwacje rozwiązuje logika relacji/magazynu (workery rezerwacji są internal — z dodatku
operuj przez publiczne API relacji oraz pola
dok.Rezerwacja), nie kasuj rekordów rezerwacji bezpośrednio z dodatku. Delete()na dokumencie poza buforem (zatwierdzony/zablokowany/anulowany) jest zabronione — nie obchodź tego przez bezpośrednie operacje na tabeli.
4. Relacje i generowanie dokumentów
Rozdział opisuje publiczny tor przekształceń dokumentów handlowych: generowanie dokumentów podrzędnych z nadrzędnych (zamówienie → faktura → dokument magazynowy), wiązanie i rozwiązywanie powiązań oraz odczyt łańcucha relacji i stanu pokrycia zamówienia.
Punkt wejścia —
IRelacjeService. Cała logika relacji handlowych jest udostępniona dodatkom zewnętrznym wyłącznie przez serwisSoneta.Handel.RelacjeDokumentow.Api.IRelacjeService(scope:Session). Workery wykonawcze (PowiazDokumentyWorker,UsunPowiazanieDokumentowWorker, akcje menu „Relacje”) są internal — nie instancjonuj ich z dodatku. Pobranie serwisu:using Microsoft.Extensions.DependencyInjection; // GetRequiredService using Soneta.Handel.RelacjeDokumentow.Api; // IRelacjeService, HandlerSet var rel = session.GetRequiredService<IRelacjeService>(); // rzuca, gdy serwisu brak // albo: var rel = session.GetService<IRelacjeService>(); // zwraca null, gdy brakReguły wspólne dla całego rozdziału:
- Dokumenty nadrzędne muszą być zatwierdzone (
dok.Stan = StanDokumentuHandlowego.Zatwierdzony) — z bufora relacja nie powstanie.- Wywołanie metody serwisu (
NowyPodrzedny*,Dolacz*) jest operacją modyfikującą — musi działać w otwartej transakcji edycyjnej (session.Logout(editMode: true)), a po zamknięciu transakcji zatwierdź zmiany przezsession.Save().- Wynik to
DokumentHandlowy[]— tablica utworzonych/dołączonych dokumentów podrzędnych.Context(zaznaczenie / parametry UI) iHandlerSet(callbacki rozstrzygające) są opcjonalne. Jeśli definicja relacji wymaga rozstrzygnięcia (np. wyboru dostaw, magazynu, pozycji) i nie dostarczysz odpowiedniego callbacka, platforma rzuciNotImplementedException.
HandlerSet — callbacki rozstrzygające
HandlerSet to zbiór delegatów wołanych przez silnik relacji, gdy przekształcenie wymaga decyzji,
którą w UI podejmuje użytkownik. W trybie programowym (dodatek, test, worker bez UI) musisz je
dostarczyć sam — inaczej NotImplementedException. Najważniejsze:
| Callback | Typ | Kiedy potrzebny |
|---|---|---|
WybierzMagazynCallback |
Func<Context, Magazyn> |
definicja relacji ma WyborPozycji = WybórMagazynu — wskaż magazyn docelowy |
WybierzMagazynDocelowyCallback |
Func<DokumentDocelowy, Magazyn> |
wybór magazynu dla dokumentu docelowego (domyślnie d.MagazynDo) |
WybierzPozycjeCallback |
Action<DokumentDocelowy> |
definicja ma WyborPozycji = WybórPozycji — zaznacz pozycje (domyślnie PrzeliczPozycje()) |
WybierzDostawyCallback |
Action<DostawaWorker> |
wskazanie partii/dostaw przy rozchodzie (gdy WskazaniePartii wymuszone) |
WybierzDokumentyZaliczkoweCallback |
Action<DokumentDocelowy> |
faktura z zaliczkami |
UstawParametryFakturowania |
Action<DefRelacjiCyklicznaFakturowanieParams> |
fakturowanie cykliczne |
Domyślnie WybierzPozycjeCallback przepisuje wszystkie pozycje (PrzeliczPozycje()). Callbacki bez
sensownej wartości domyślnej (WybierzMagazynCallback, WybierzDostawyCallback,
WybierzDokumentyZaliczkoweCallback) rzucają NotImplementedException, dopóki ich nie nadpiszesz.
W17 — Generowanie faktury z zamówienia (ZO → FV)
Cel: z zatwierdzonego zamówienia (odbiorcy ZO lub do dostawcy ZD) wygenerować pojedynczy
dokument podrzędny o wskazanym symbolu (np. fakturę FV). Relacja jeden nadrzędny → jeden
podrzędny (indywidualna).
Warianty:
| Wariant | Wejście | Symbol podrzędnego | Uwaga |
|---|---|---|---|
| ZO → FV | jedno zamówienie odbiorcy | "FV" |
klasyczna realizacja sprzedaży |
| ZD → ZK (FZ) | zamówienie do dostawcy | "ZK" / "FZ" |
zakup; może wymagać WybierzMagazynCallback |
| FA → WZ pojedynczo | jedna faktura | "WZ" |
wydanie magazynowe do faktury (patrz W21) |
| Wszystkie pozycje | bez HandlerSet lub WybierzPozycjeCallback = przepisz wszystko |
— | gdy definicja relacji ma BrakOkna |
| Wybrane pozycje | WybierzPozycjeCallback zaznacza podzbiór |
— | gdy definicja ma WybórPozycji |
Pola i typy:
IRelacjeService.NowyPodrzednyIndywidualny(DokumentHandlowy[] nadrzedne, string symbolPodrzednego, Context context = null, HandlerSet handlers = null) → DokumentHandlowy[].
Wynik ma Length == nadrzedne.Length (każdy nadrzędny dostaje własny podrzędny).
Pozycja podrzędnego: poz.Dostawa (wskazana partia/dostawa, gdy dotyczy).
Snippet:
using Microsoft.Extensions.DependencyInjection;
using Soneta.Handel;
using Soneta.Handel.RelacjeDokumentow.Api;
var rel = session.GetRequiredService<IRelacjeService>();
// zamowienie jest już zatwierdzone (StanDokumentuHandlowego.Zatwierdzony)
DokumentHandlowy[] faktury;
using (var t = session.Logout(editMode: true))
{
faktury = rel.NowyPodrzednyIndywidualny(
new[] { zamowienie },
"FV"); // bez HandlerSet — gdy relacja nie wymaga rozstrzygnięć
t.Commit(); // CommitUI() w workerze/extenderze
}
session.Save();
DokumentHandlowy faktura = faktury[0]; // jeden nadrzędny → jeden podrzędny
Wariant z wyborem pozycji (przepisz tylko pozycje danego towaru):
using (var t = session.Logout(editMode: true))
{
var wynik = rel.NowyPodrzednyIndywidualny(
new[] { zamowienie }, "FV",
handlers: new HandlerSet
{
WybierzPozycjeCallback = docelowy =>
{
// docelowy: DokumentDocelowy — zaznacz pozycje do przeniesienia
docelowy.PrzeliczPozycje(); // domyślnie: wszystkie
}
});
t.Commit();
}
session.Save();
Pułapki:
- Dokument nadrzędny musi być zatwierdzony — z bufora
NowyPodrzedny*nie zadziała. - Gdy definicja relacji wymaga rozstrzygnięcia (magazyn, dostawy, pozycje), a
HandlerSetgo nie dostarcza →NotImplementedException. Zacznij od wywołania bezHandlerSet; jeśli rzuca, dodaj konkretny callback (patrz tabela powyżej). - Symbol podrzędnego musi odpowiadać istniejącej definicji relacji wychodzącej z definicji
nadrzędnego (konfiguracja
DefRelacjinaDefDokHandlowego). Brak pasującej relacji → pusty wynik lub wyjątek. - Cała operacja w jednej transakcji +
Save(). Mieszane sesje rekordów → użyjsession.Get(...).
W18 — Zbiorczy dokument magazynowy z wielu faktur (wiele FA → 1 WZ/PZ)
Cel: z wielu zatwierdzonych faktur utworzyć jeden zbiorczy dokument podrzędny (np. jeden
dokument magazynowy WZ/PZ zbierający pozycje wszystkich faktur). Relacja wiele nadrzędnych →
jeden podrzędny (zbiorcza).
Warianty:
| Wariant | Wejście | Symbol | Wynik |
|---|---|---|---|
| Wiele FA → 1 WZ | tablica faktur sprzedaży | "WZ" |
1 wydanie zbiorcze |
| Wiele FZ → 1 PZ | tablica faktur zakupu | "PZ" |
1 przyjęcie zbiorcze |
| Wiele ZO → 1 FV | zbiorcza faktura z zamówień | "FV" |
1 faktura zbiorcza |
Pola i typy:
IRelacjeService.NowyPodrzednyZbiorczy(DokumentHandlowy[] nadrzedne, string symbolPodrzednego, Context context = null, HandlerSet handlers = null) → DokumentHandlowy[].
W przeciwieństwie do W17 zwraca zwykle tablicę jednoelementową (jeden dokument zbiorczy).
Snippet:
var rel = session.GetRequiredService<IRelacjeService>();
// faktury: DokumentHandlowy[] — wszystkie zatwierdzone, zgodne (ten sam kontrahent/magazyn wg konfiguracji)
DokumentHandlowy wz;
using (var t = session.Logout(editMode: true))
{
var wynik = rel.NowyPodrzednyZbiorczy(faktury, "WZ");
wz = wynik[0]; // jeden zbiorczy dokument magazynowy
t.Commit();
}
session.Save();
Pułapki:
- Dokumenty zbiorcze powstają tylko z dokumentów zgodnych (wymóg ten sam kontrahent / magazyn / waluta — zależnie od definicji relacji zbiorczej). Niezgodne wejście → wyjątek lub pominięcie.
- Wszystkie nadrzędne muszą być zatwierdzone.
- Tak jak w W17 — brak wymaganego callbacka w
HandlerSet→NotImplementedException. - Nie zakładaj
Length == nadrzedne.Length— tu wynik jest agregatem (zwykle 1 dokument).
W19 — Zbiorcza faktura z wielu dokumentów magazynowych (wiele WZ → 1 FA)
Cel: „odwrotny” kierunek W18 — z wielu zatwierdzonych dokumentów magazynowych (np. WZ)
utworzyć jedną zbiorczą fakturę sprzedaży.
Warianty:
| Wariant | Wejście | Symbol | Uwaga |
|---|---|---|---|
| Wiele WZ → 1 FV | wydania magazynowe | "FV" |
fakturowanie zbiorcze rozchodów |
| Wiele PZ → 1 FZ | przyjęcia magazynowe | "FZ" |
zbiorczy zakup |
Pola i typy: ta sama metoda NowyPodrzednyZbiorczy(...) co w W18 — różni się tylko kierunkiem
(nadrzędne = dokumenty magazynowe, symbol podrzędnego = faktura).
Snippet:
var rel = session.GetRequiredService<IRelacjeService>();
// wydania: DokumentHandlowy[] — zatwierdzone WZ tego samego kontrahenta
DokumentHandlowy fakturaZbiorcza;
using (var t = session.Logout(editMode: true))
{
fakturaZbiorcza = rel.NowyPodrzednyZbiorczy(wydania, "FV")[0];
t.Commit();
}
session.Save();
Pułapki:
- Kierunek relacji (magazynowy → handlowy) musi być skonfigurowany jako
DefRelacjina definicji dokumentu magazynowego. Brak relacji → pusty wynik. - Dokumenty magazynowe muszą być zatwierdzone i zgodne (kontrahent / waluta).
- Walidator stanu ujemnego nie dotyczy tej operacji (rozchód już się dokonał na WZ), ale faktura przejmie wartości z dokumentów źródłowych — nie modyfikuj pozycji ręcznie po przekształceniu, jeśli ma zachować zgodność z magazynem.
W20 — Wyszukiwanie dokumentów powiązanych (odczyt pól kalkulowanych)
Cel: odczytać dokumenty powiązane bez ręcznego przeszukiwania relacji — przez pola kalkulowane na
DokumentHandlowy. Działa w obie strony: dla faktury → jej dokumenty magazynowe, dla magazynowego →
jego faktury.
Warianty:
| Wariant | Pole kalkulowane | Typ | Zwraca |
|---|---|---|---|
| Magazynowe dla faktury | dok.DokumentyMagazynowe |
DokumentHandlowy[] |
WZ/PZ powiązane z fakturą |
| Główny dok. magazynowy | dok.DokumentMagazynowyGłówny |
DokumentHandlowy |
pierwszy/główny magazynowy |
| Faktury dla magazynowego | dok.DokumentyHandlowe |
DokumentHandlowy[] |
faktury powiązane z WZ/PZ/ZO/ofertą |
Pola i typy: wszystkie trzy to właściwości kalkulowane (read-only) na DokumentHandlowy.
DokumentyMagazynowe dla dokumentu, który sam jest magazynowy (TypPartii.Magazynowy itd.),
zwraca { this }. Analogicznie DokumentyHandlowe dla samego dokumentu handlowego zwraca { this }.
Snippet:
// 1. Dla faktury — jej dokumenty magazynowe (wydania/przyjęcia)
foreach (DokumentHandlowy mag in faktura.DokumentyMagazynowe)
{
// mag.Numer, mag.Magazyn, mag.Pozycje ...
}
// główny dokument magazynowy (gdy potrzebny jeden)
DokumentHandlowy glowny = faktura.DokumentMagazynowyGłówny;
// 2. Dla dokumentu magazynowego — faktury, które go „obsługują”
foreach (DokumentHandlowy fa in wz.DokumentyHandlowe)
{
// fa.Numer, fa.Suma ...
}
Pułapki:
- To pola kalkulowane — czytaj, nie ustawiaj. Każde odwołanie uruchamia wyszukiwanie po relacjach, więc nie wołaj ich w pętli dla tysięcy rekordów — buforuj wynik w zmiennej lokalnej.
- Zwracają tablicę (może być pusta), nie
null— bezpiecznie iterować, ale sprawdzaj.Lengthprzed[0]. - Pola respektują prawa dostępu — dokumenty bez prawa odczytu są pomijane (wynik może być węższy niż faktyczny łańcuch relacji).
W21 — Generowanie dokumentu magazynowego z faktury (FA → WZ pojedynczo)
Cel: do pojedynczej zatwierdzonej faktury wygenerować odpowiadający dokument magazynowy
(np. wydanie WZ). To wariant indywidualny (W17), tylko z innym symbolem docelowym.
Warianty:
| Wariant | Wejście | Symbol | Uwaga |
|---|---|---|---|
| FV → WZ | faktura sprzedaży | "WZ" |
wydanie z magazynu |
| FZ → PZ | faktura zakupu | "PZ" |
przyjęcie do magazynu |
| Z wyborem partii | + WybierzDostawyCallback |
— | gdy WskazaniePartii wymuszone na definicji WZ |
Pola i typy: IRelacjeService.NowyPodrzednyIndywidualny(...) — jak W17. Pozycje magazynowe mają
poz.Dostawa (wskazana partia/dostawa).
Snippet (z wyborem partii — wymusza HandlerSet):
using Soneta.Magazyny;
var rel = session.GetRequiredService<IRelacjeService>();
DokumentHandlowy wz;
using (var t = session.Logout(editMode: true))
{
var wynik = rel.NowyPodrzednyIndywidualny(
new[] { faktura }, "WZ",
handlers: new HandlerSet
{
WybierzDostawyCallback = dostawaWorker =>
{
// dla każdej pozycji wskaż pobierane zasoby/partie
foreach (var poz in dostawaWorker.GetListPozycja())
{
dostawaWorker.Pozycja = poz;
foreach (Zasob z in dostawaWorker.Zasoby.Cast<Zasob>())
{
using var tz = z.Session.Logout(editMode: true);
// ... oznacz zasób jako pobrany (Pobrano = true)
tz.Commit();
}
}
}
});
wz = wynik[0];
t.Commit();
}
session.Save();
Pułapki:
- Gdy definicja
WZmaWskazaniePartii = WymuszonyDodawanie, musisz dostarczyćWybierzDostawyCallback— inaczejNotImplementedException. - Rozchód wymaga wcześniejszego zapisanego przyjęcia towaru (
StanUjemnyVerifierw Demo). Magazyn księguje się dopiero poSession.Save()— samoCommit/CommitUInie tworzy obrotów/zasobów. - Po wygenerowaniu WZ odczytaj go zwrotnie przez
faktura.DokumentyMagazynowe(W20).
W22 — Kopiowanie faktury klientowi (KopiujKlientowiFaktureWorker)
Cel: skopiować zatwierdzone faktury sprzedaży klienta jako dokumenty zakupu do bazy klienta (scenariusz biura rachunkowego pracującego na wielu bazach). Worker publiczny.
Dostępność: Soneta.EI.KopiujKlientowiFaktureWorker jest public (rejestracja
[assembly: Worker(typeof(KopiujKlientowiFaktureWorker), typeof(DokHandlowe))]). Akcja menu
„Kopiuj klientowi...”. Widoczna tylko gdy bieżąca baza jest master w konfiguracji „Praca na
wielu bazach” i licencja to Biuro Rachunkowe (IsVisibleKopiuj). Bez tej konfiguracji
nie zadziała (nie znajdzie bazy klienta).
Pola i typy:
[Context] DokumentHandlowy[] Dokumenty— kopiowane faktury (brane są tylkoZatwierdzony).[Context] Params Prms— parametry;Params : ContextBase:DefinicjaDokumentu Definicja— definicja dokumentu zakupu w bazie klienta (lista zDefDokumentow.WgTypu[TypDokumentu.ZakupEwidencja]);bool PrzygotujPrzelewy(domyślnietrue) — czy generować przelewy dla zobowiązań.
object Kopiuj()— akcja[Action("Kopiuj klientowi...", Mode = SingleSession | Progress)]; zwraca komunikat tekstowy, szczegóły pisze do logu.
Snippet (programowe użycie workera z Params):
using Soneta.EI;
// dokumenty: zaznaczone faktury sprzedaży (worker bierze tylko zatwierdzone)
var prms = new KopiujKlientowiFaktureWorker.Params(context)
{
Definicja = /* DefinicjaDokumentu zakupu */,
PrzygotujPrzelewy = true,
};
var worker = new KopiujKlientowiFaktureWorker
{
Dokumenty = dokumenty,
Prms = prms,
};
object komunikat = worker.Kopiuj(); // tworzy dokumenty w bazie klienta; Save robi worker wewnętrznie
Pułapki:
- Worker działa na wielu bazach (
DBItemContext) — sam otwiera/zamyka transakcje iSave()w bazie klienta. Nie opakowuj wywołania w zewnętrzną transakcję na bazie master. - Kopiowane są tylko faktury zatwierdzone; dokumenty z zobowiązaniem (nie należnością) są pomijane (zakup wymaga należności po stronie sprzedaży).
- W bazie klienta tworzony jest automatycznie kontrahent „biuro” (wg NIP z pieczątki firmy), jeśli go brak. Brakujący sposób zapłaty w bazie klienta → dokument pominięty (log).
- Wymaga licencji
Biuro Rachunkowei roli master — w innym układzie akcja jest niewidoczna. - Do zwykłego „kopiuj dokument w tej samej bazie” ten worker nie służy — to specjalizowany scenariusz wielobazowy.
W23 — Ręczne wiązanie i rozwiązywanie powiązań
Cel: dołączyć istniejący dokument do innego jako podrzędny/nadrzędny (bez generowania nowego)
oraz rozwiązać błędnie utworzone powiązanie. Tor publiczny = IRelacjeService.Dolacz*.
Uwaga o dostępności: workery wykonawcze
PowiazDokumentyWorkeriUsunPowiazanieDokumentowWorkersą internal — nie używaj ich z dodatku. Wiązanie realizuj przezIRelacjeService.DolaczPodrzednyIndywidualny/DolaczNadrzedny. Programowego, publicznego API do rozwiązywania powiązań brak — rozwiązywanie powiązań jest dostępne tylko interaktywnie (menu „Relacje” w aplikacji), bo odpowiedni worker jest internal. To ograniczenie publicznego kontraktu.
Warianty:
| Wariant | Metoda | relationName |
|---|---|---|
| Dołącz podrzędny do nadrzędnego | DolaczPodrzednyIndywidualny(documents, relationName) |
nazwa definicji relacji wychodzącej (np. "Faktura") |
| Dołącz dokument do nadrzędnego | DolaczNadrzedny(documents, relationName) |
nazwa relacji od strony nadrzędnego (np. "Zamówienie") |
| Rozwiązanie powiązania | — | tylko interaktywnie (worker internal) |
Pola i typy:
DokumentHandlowy[] DolaczPodrzednyIndywidualny(
DokumentHandlowy[] documents, string relationName,
Context context = null, HandlerSet handlers = null);
DokumentHandlowy[] DolaczNadrzedny(
DokumentHandlowy[] documents, string relationName,
Context context = null, HandlerSet handlers = null);
relationName to nazwa definicji relacji (DefRelacji), nie symbol dokumentu — np. "Zamówienie",
"Faktura", "Korekta wydania magazynowego 2".
Snippet:
var rel = session.GetRequiredService<IRelacjeService>();
// Dołącz fakturę do istniejącego zamówienia jako nadrzędnego (relacja "Zamówienie")
using (var t = session.Logout(editMode: true))
{
var powiazane = rel.DolaczNadrzedny(new[] { faktura }, "Zamówienie");
t.Commit();
}
session.Save();
Pułapki:
relationNamemusi dokładnie pasować do nazwyDefRelacjiskonfigurowanej w bazie (wielkość liter / spacje istotne) — niepasująca nazwa daje pusty/nullwynik w tablicy.Dolacz*przetwarza dokumenty pojedynczo (Array.ConvertAll) — wynik na pozycjiimoże byćnull, jeśli dołączenie konkretnego dokumentu się nie powiodło. Sprawdzaj elementy wyniku.- Dokumenty muszą być zatwierdzone i wzajemnie zgodne (kontrahent / pozycje).
- Rozwiązywanie powiązań programowo z dodatku niedostępne — zaplanuj operację jako działanie użytkownika w aplikacji (menu „Relacje”).
W24 — Odczyt łańcucha powiązań i stan pokrycia zamówienia
Cel: prześledzić łańcuch relacji (oferta → zamówienie → faktura → dokument magazynowy) oraz odczytać stan pokrycia/realizacji zamówienia (czy zamówienie zostało zrealizowane fakturami).
Warianty:
| Wariant | Mechanizm | Typ wyniku |
|---|---|---|
| W górę łańcucha (faktury dla magazynowego/zamówienia) | dok.DokumentyHandlowe (W20) |
DokumentHandlowy[] |
| W dół łańcucha (magazynowe dla faktury) | dok.DokumentyMagazynowe (W20) |
DokumentHandlowy[] |
| Stan pokrycia zamówienia (odczyt) | StanPokryciaZamówieniaWorker.StanPokrycia |
enum StanPokryciaZamówienia |
Pola i typy:
- Odczyt stanu pokrycia: worker public
Soneta.Handel.StanPokryciaZamówieniaWorker([Context] DokumentHandlowy Dokument) → propertyStanPokrycia : StanPokryciaZamówienia. - Enum
Soneta.Handel.StanPokryciaZamówienia:Brak = 0,Częściowe = 1,Pełne = 2,NiePodlega = 3,Niezweryfikowane = 4. - Ważne: worker tylko odczytuje wcześniej wyliczony stan (z cache na
Login). Samo przeliczenie uruchamia akcja menu „Sprawdź pokrycie” (StanPokryciaZamowienWorker,[HandelAction]) — wywołuje ją użytkownik; dopóki nie zostanie odpalona,StanPokryciazwracaNiezweryfikowane.
Snippet:
using Soneta.Handel;
// Odczyt stanu pokrycia pojedynczego zamówienia (po wcześniejszym „Sprawdź pokrycie”):
var w = new StanPokryciaZamówieniaWorker { Dokument = zamowienie };
StanPokryciaZamówienia stan = w.StanPokrycia;
bool zrealizowane = stan == StanPokryciaZamówienia.Pełne;
// Łańcuch relacji w dół: zamówienie -> faktury -> ich dokumenty magazynowe
foreach (DokumentHandlowy fa in zamowienie.DokumentyHandlowe) // faktury zamówienia
foreach (DokumentHandlowy mag in fa.DokumentyMagazynowe) // wydania faktury
{
// mag.Numer, mag.Magazyn ...
}
Pułapki:
StanPokryciaZamówieniaWorker.StanPokryciazwracaNiezweryfikowane, dopóki w sesji/loginie nie wykonano przeliczenia (akcja „Sprawdź pokrycie”). Programowego, publicznego wyzwalacza przeliczenia brak —StanPokryciaZamówień.Przelicz()jest wywoływane przez internal akcję menu. Z dodatku traktujStanPokryciajako odczyt stanu policzonego interaktywnie.- Pola
DokumentyHandlowe/DokumentyMagazynowerespektują prawa dostępu i są kalkulowane — buforuj wynik, nie wołaj w gęstych pętlach (W20). - Stan
NiePodlegaoznacza dokument, którego pokrycie nie dotyczy (np. nie jest zamówieniem) — rozróżniaj go odBrak(zamówienie bez realizacji).
Powiązane sekcje: tworzenie/stan dokumentu (sekcja 1–2), korekty (
IRelacjeService.NowaKorekta,NowaKorektaZbiorcza— analogiczne do W17/W18, symbol korekty opcjonalny), magazyn i partie (dok.Zasoby,dok.Obroty,GrupaDostaw).
5. Odczyt i wyszukiwanie
Odczyt dokumentów handlowych prawie zawsze sprowadza się do filtrowania serwerowego: warunek
budujesz wyrażeniem LINQ i aplikujesz na kluczu tabeli (DokHandlowe.WgXxx[dok => …]) albo na
kolekcji podrzędnej (towar.Pozycje[…], dok.Pozycje[…]). Z bazy do pamięci trafiają wtedy
wyłącznie pasujące wiersze. DokHandlowe to duża tabela operacyjna (guided="Exported") —
nigdy nie iteruj jej w całości z if w pamięci; zawsze zawężaj zakres (okres, kontrahent, definicja)
przez SQL i — przy analizach poprzecznych — ogranicz przedział czasowy.
Fundamenty (sesja, transakcja, blokada optymistyczna) opisuje
safe-code.md, a mechanikę warunków serwerowychrowcondition.md— tu się do nich odwołujemy, nie powtarzamy. Cały kod jest zgodny z C# 10 i operuje wyłącznie na publicznym kontrakcie platformy. W wyrażeniu LINQ wolno użyć tylko pól bazodanowych; pole kalkulowane rzuciLinqConditionException.
Fakty o odczycie (zweryfikowane na tabeli DokHandlowe i PozycjeDokHan):
- Klucze tabeli
DokHandlowe(do filtrowania serwerowego i sortowania):WgDaty(Data,Czas),WgMagazynuNumer(Magazyn,Numer.Pelny),WgMagazynuObcy(Magazyn,Obcy.Numer),WgKontrahentaObcy(Kontrahent,Obcy.Numer,Kategoria),WgOkresIntrastat, orazPrimaryKey. Nie ma „gołego" kluczaWgKontrahentaaniWgNumeru— filtruj wyrażeniem na dowolnym z powyższych kluczy (sortowanie bierze się z wybranego klucza). - Indeksator po Guid:
hm.DokHandlowe[guid](zwracaDokumentHandlowy; rzucaRowNotFoundExceptiondla nieznanego Guid). - Pozycje dokumentu:
dok.Pozycje—LpSubTable<PozycjaDokHandlowego>(sortowane poLp). - Pozycje danego towaru (historia obrotu):
towar.Pozycje—SubTable<PozycjaDokHandlowego>(kluczWgTowar). Klucze naPozycjeDokHan:WgDaty(Data),WgKierunek(Towar,KierunekMagazynu,Data,Czas),WgTowarDokumentu(Towar,Dokument). - Numer dokumentu: pole
dok.Numer: NumerDokumentu. Pełny numer do odczytu todok.Numer.NumerPelny(kalkulowane). W warunku serwerowym używaj pola bazodanowegoNumer.Pelny(np.dok => dok.Numer.Pelny == "FV 1/2026"). - Korekty:
dok.DokumentKorygowany(dokument korygowany przez tę korektę),dok.DokumentyKorygujące(IEnumerable<DokumentHandlowy>— łańcuch korekt tego dokumentu),dok.Korekta: bool(pole bazodanowe — czy dokument jest korektą). Wszystkie powiązania korekt to pola kalkulowane (opróczKorekta). - Kolekcje na
Kontrahent(z modułu CRM):k.DokumentyHandloweik.DokumentyHandloweOdbiorcyto nietypowaneSubTable(CRM nie referuje Handlu). Iteracja działa, ale typowane filtrowanie serwerowe rób od strony Handlu:hm.DokHandlowe.WgKontrahentaObcy[dok => dok.Kontrahent == k].
W25 — Odczytanie pozycji dokumentu
Cel: przejść po pozycjach (towar, ilość, cena, rabat, wartość) wczytanego dokumentu — np. do wydruku, eksportu czy przeliczeń własnych.
Warianty:
| Wariant | Źródło / operacja |
|---|---|
| Wszystkie pozycje wg Lp | dok.Pozycje (LpSubTable, sortowane po Lp) |
| Tylko pozycje danego towaru | dok.Pozycje[(PozycjaDokHandlowego p) => p.Towar == towar] |
| Pozycje o niezerowej ilości | warunek serwerowy na p.Ilosc.Value |
| Wartości pozycji | p.WartoscCy, p.Suma (BruttoNetto: NettoCy/VATCy/BruttoCy) |
Pola i typy (PozycjaDokHandlowego): Towar: Towar, Ilosc: Quantity
(.Value, .Symbol), Cena: DoubleCy, Rabat: Percent, WartoscCy: Currency,
Suma: BruttoNetto (NettoCy, VATCy, BruttoCy — typ Currency; Netto/VAT/Brutto — decimal),
Lp: int, Stawka: StawkaVat, Opis: string.
Snippet:
var hm = session.GetHandel();
var dok = hm.DokHandlowe[guid]; // dokument wczytany po Guid (W29)
if (dok == null) return;
// Iteracja po pozycjach (LpSubTable jest już posortowana po Lp):
foreach (PozycjaDokHandlowego p in dok.Pozycje)
{
string towar = p.Towar?.Kod;
Quantity ilosc = p.Ilosc; // p.Ilosc.Value + p.Ilosc.Symbol (jednostka)
DoubleCy cena = p.Cena;
Percent rabat = p.Rabat;
Currency netto = p.Suma.NettoCy; // wartość netto pozycji w PLN
Currency brutto = p.Suma.BruttoCy;
Currency wartosc = p.WartoscCy; // wartość pozycji w walucie ceny
}
// Tylko pozycje wybranego towaru — filtr serwerowy na kolekcji:
var towar = session.GetTowary().Towary.WgKodu["BIKINI"];
foreach (PozycjaDokHandlowego p in dok.Pozycje[(PozycjaDokHandlowego p) => p.Towar == towar])
{
// ...
}
Pułapki:
IlosctoQuantity, aCena/WartoscCytoDoubleCy/Currency(kwota + waluta), niedecimal/double(safe-code §10). Składowe:p.Ilosc.Value,p.Ilosc.Symbol.- Do filtrowania pozycji na jednym dokumencie możesz iterować
dok.Pozycje(to mała kolekcja), ale i tak preferuj warunekdok.Pozycje[p => …]— wykona się serwerowo. p.Suma/p.WartoscCysą przeliczane przez platformę — czytaj je, nie wyliczaj „ręcznie".p.Towarbywanulldla pozycji nietowarowych (opis/koszt) — zabezpiecz dostęp (?.).
W26 — Odczytanie dokumentów dla kontrahenta
Cel: pobrać dokumenty wystawione na danego kontrahenta — jako nabywcę (Kontrahent) lub jako
odbiorcę (Odbiorca).
Warianty:
| Wariant | Źródło | Typ |
|---|---|---|
| Kontrahent jako nabywca (kolekcja CRM) | k.DokumentyHandlowe |
nietypowany SubTable |
| Odbiorca (kolekcja CRM) | k.DokumentyHandloweOdbiorcy |
nietypowany SubTable |
| Filtr typowany od strony Handlu | hm.DokHandlowe.WgKontrahentaObcy[dok => dok.Kontrahent == k] |
SubTable<DokumentHandlowy> |
| Zawężenie okresem | dołóż && dok.Data >= od w warunku |
— |
Pola i typy: dok.Kontrahent: Kontrahent, dok.Odbiorca: Kontrahent (oba bazodanowe).
Kontrahent.DokumentyHandlowe / DokumentyHandloweOdbiorcy to kolekcje SubTable na kontrahencie
(zawężone już do jednego kontrahenta).
Snippet:
var hm = session.GetHandel();
var k = session.GetCRM().Kontrahenci.WgKodu["Abc"];
if (k == null) return;
// Wariant A — kolekcja na kontrahencie (nietypowana, ale wygodna do prostego przejścia):
foreach (DokumentHandlowy dok in k.DokumentyHandlowe)
{
// dok.Numer.NumerPelny, dok.Data, dok.Suma ...
}
// Wariant B — typowany filtr serwerowy od strony Handlu + zawężenie okresem
// (klucz WgKontrahentaObcy nadaje sortowanie wg kontrahenta):
var od = Date.Today.AddMonths(-3);
foreach (DokumentHandlowy dok in hm.DokHandlowe.WgKontrahentaObcy[
(DokumentHandlowy dok) => dok.Kontrahent == k && dok.Data >= od])
{
// tylko dokumenty kontrahenta z ostatnich 3 miesięcy
}
// Dokumenty, w których kontrahent jest ODBIORCĄ:
foreach (DokumentHandlowy dok in hm.DokHandlowe[
(DokumentHandlowy dok) => dok.Odbiorca == k])
{
// ...
}
Pułapki:
k.DokumentyHandlowejest nietypowane (SubTable, nieSubTable<DokumentHandlowy>) — pętlaforeach (DokumentHandlowy …)działa, ale do filtrowania wyrażeniem LINQ użyj kolekcji od strony Handlu (hm.DokHandlowe.WgXxx[…]), gdzie typ wiersza jest znany kompilatorowi.KontrahentiOdbiorcato dwa różne pola — wybierz świadomie (nabywca ≠ odbiorca towaru).- To dane operacyjne — przy szerokich analizach zawężaj okres (
dok.Data >= od), nie ładuj całej historii (safe-code §6.3). - Porównuj po referencji rekordu (
dok.Kontrahent == k), a nie poKod— referencja generuje szybkieJOINpoID.
W27 — Ostatnie pozycje dokumentów dla wskazanego towaru
Cel: prześledzić historię obrotu danym towarem — pozycje dokumentów, w których towar wystąpił (np. ostatnie zakupy/sprzedaże, kierunek magazynowy, ceny historyczne).
Warianty:
| Wariant | Źródło / warunek |
|---|---|
| Wszystkie pozycje towaru | towar.Pozycje (klucz WgTowar) |
| Tylko rozchody / przychody | filtr na p.KierunekMagazynu (KierunekPartii) |
| Z zakresu dat | towar.Pozycje[p => p.Data >= od] |
| Tylko z dokumentów zatwierdzonych | warunek przez referencję: p.Dokument.Stan == StanDokumentuHandlowego.Zatwierdzony |
| Ostatnie N po dacie | sortuj kluczem WgKierunek/WgDaty i ogranicz w pamięci po zawężeniu |
Pola i typy (PozycjaDokHandlowego): Towar: Towar, Dokument: DokumentHandlowy,
Data: Date, Czas: Time, KierunekMagazynu: Soneta.Magazyny.KierunekPartii
(Rozchód=-1, Brak=0, Przychód=1), Cena: DoubleCy, Ilosc: Quantity. Kolekcja
towar.Pozycje: SubTable<PozycjaDokHandlowego>.
Snippet:
var towar = session.GetTowary().Towary.WgKodu["BIKINI"];
if (towar == null) return;
// Pozycje towaru z ostatnich 6 miesięcy — filtr serwerowy na kolekcji towaru:
var od = Date.Today.AddMonths(-6);
foreach (PozycjaDokHandlowego p in towar.Pozycje[(PozycjaDokHandlowego p) => p.Data >= od])
{
DokumentHandlowy dok = p.Dokument; // dokument macierzysty pozycji
string numer = dok.Numer.NumerPelny;
// p.KierunekMagazynu, p.Ilosc, p.Cena, p.Data ...
}
// Tylko rozchody (sprzedaż/wydania) danego towaru z dokumentów zatwierdzonych:
foreach (PozycjaDokHandlowego p in towar.Pozycje[(PozycjaDokHandlowego p) =>
p.KierunekMagazynu == KierunekPartii.Rozchód
&& p.Dokument.Stan == StanDokumentuHandlowego.Zatwierdzony
&& p.Data >= od])
{
// historia rozchodów towaru
}
Pułapki:
- Filtruj na
towar.Pozycje[…](kolekcja zawężona do jednego towaru), nie iteruj globalniePozycjeDokHan— to jedna z największych tabel operacyjnych (safe-code §6.3). - Warunek przez referencję (
p.Dokument.Stan == …) jest dozwolony —Stanjest polem bazodanowym i wygenerujeJOIN. Nie używaj w warunku pól kalkulowanych dokumentu (np.p.Dokument.ZatwierdzonyrzuciLinqConditionException). - „Ostatnie N" realizuj przez sortowanie kluczem (
WgKierunek/WgDaty) po zawężeniu okresem; nie pobieraj całości po to, by wziąć kilka rekordów. KierunekPartiiżyje wSoneta.Magazyny— wymagana referencja do modułu Magazyny.
W28 — Wyszukiwanie dokumentów wg okresu, definicji, stanu, serii
Cel: odfiltrować dokumenty po kryteriach nagłówkowych (data, definicja, stan, magazyn, seria)
serwerowo, bez obiektów warstwy UI (View).
Warianty:
| Wariant | Warunek (pole bazodanowe) |
|---|---|
| Okres dat | dok.Data >= od && dok.Data <= do |
| Konkretna definicja (symbol) | dok.Definicja == def (rekord z DefDokHandlowych.WgSymbolu[...]) |
| Stan dokumentu | dok.Stan == StanDokumentuHandlowego.Zatwierdzony |
| Magazyn | dok.Magazyn == mag |
| Seria | dok.Seria == "A" |
| Wiele kryteriów | koniunkcja && / alternatywa ` |
Pola i typy: dok.Data: Date, dok.Definicja: DefDokHandlowego,
dok.Stan: StanDokumentuHandlowego, dok.Magazyn: Magazyn, dok.Seria: string,
dok.Kategoria: KategoriaHandlowa. Klucz WgDaty daje sortowanie po dacie.
Snippet:
var hm = session.GetHandel();
var def = hm.DefDokHandlowych.WgSymbolu["FV"]; // definicja faktury sprzedaży
var mag = session.GetMagazyny().Magazyny.WgSymbol["F"];
var od = new Date(2026, 1, 1);
var doDt = new Date(2026, 3, 31);
// Zatwierdzone faktury FV z I kwartału na magazynie F — jeden warunek serwerowy.
// Klucz WgDaty nadaje sortowanie po Data, Czas:
foreach (DokumentHandlowy dok in hm.DokHandlowe.WgDaty[(DokumentHandlowy dok) =>
dok.Definicja == def
&& dok.Magazyn == mag
&& dok.Stan == StanDokumentuHandlowego.Zatwierdzony
&& dok.Data >= od && dok.Data <= doDt])
{
// dok.Numer.NumerPelny, dok.Suma, dok.Kontrahent ...
}
// Wariant: warunek jako wartość przekazywana dalej (np. do metody):
var cond = RowCondition.FromExpression<DokumentHandlowy>(
dok => dok.Definicja == def && dok.Seria == "A");
foreach (DokumentHandlowy dok in hm.DokHandlowe.WgDaty[cond]) { /* ... */ }
Pułapki:
- Nie używaj
Vieww kodzie biznesowym (to obiekt UI) — filtrujSubTable[expression]lubRowCondition.FromExpression(rowcondition.md). - Porównuj definicję/magazyn po rekordzie (
dok.Definicja == def), nie po stringu symbolu — rekord pobierz raz przezWgSymbolu[...]/WgSymbol[...]poza pętlą. - Stan porównuj enumem (
dok.Stan == StanDokumentuHandlowego.Zatwierdzony); skrótydok.Zatwierdzonysą kalkulowane i nie wolno ich użyć w warunku LINQ. - Wybór klucza (
WgDaty,WgMagazynuNumer,WgKontrahentaObcy) decyduje tylko o sortowaniu — warunek i tak trafia doWHERE. Dla dużych zbiorów dobierz klucz pasujący do oczekiwanej kolejności.
W29 — Odczyt dokumentu wg numeru lub Guid
Cel: odnaleźć pojedynczy dokument po jego pełnym numerze (Numer.Pelny) albo po globalnym
identyfikatorze Guid (np. zapisanym wcześniej w innym systemie / w teście).
Warianty:
| Wariant | Mechanizm | Zwraca |
|---|---|---|
| Po Guid | hm.DokHandlowe[guid] (indeksator GuidedTable) |
DokumentHandlowy; rzuca RowNotFoundException, gdy brak |
| Po pełnym numerze | filtr serwerowy dok => dok.Numer.Pelny == numer |
zbiór (bierz .FirstOrDefault()) |
| Po numerze w obrębie magazynu | klucz WgMagazynuNumer (Magazyn + Numer.Pelny) |
precyzyjniej (numer bywa unikalny per magazyn) |
| Po numerze obcym | klucz WgMagazynuObcy / pole dok.Obcy.Numer |
dokument z numerem dostawcy |
Pola i typy: dok.Numer: NumerDokumentu (odczyt pełnego numeru: dok.Numer.NumerPelny;
pole bazodanowe w warunku: Numer.Pelny), dok.Guid: Guid (z GuidedRow),
dok.Obcy.Numer: string (numer dokumentu obcego).
Snippet:
var hm = session.GetHandel();
// 1. Po Guid — najpewniejszy, jednoznaczny dostęp. UWAGA: indeksator GuidedTable RZUCA
// RowNotFoundException dla nieznanego Guid (nie zwraca null) — obuduj try/catch, gdy brak pewności:
DokumentHandlowy poGuid;
try { poGuid = hm.DokHandlowe[guid]; }
catch (Soneta.Business.RowNotFoundException) { poGuid = null; }
// 2. Po pełnym numerze — warunek serwerowy na polu bazodanowym Numer.Pelny.
// Numer może się powtarzać między magazynami, więc bierzemy pierwszy / iterujemy:
DokumentHandlowy poNumerze = hm.DokHandlowe.WgMagazynuNumer[
(DokumentHandlowy dok) => dok.Numer.Pelny == "FV 1/2026"].FirstOrDefault();
// 3. Po numerze w obrębie magazynu (precyzyjniej — numeracja zwykle per magazyn):
var mag = session.GetMagazyny().Magazyny.WgSymbol["F"];
DokumentHandlowy wMagazynie = hm.DokHandlowe.WgMagazynuNumer[(DokumentHandlowy dok) =>
dok.Magazyn == mag && dok.Numer.Pelny == "FV 1/2026"].FirstOrDefault();
if (poGuid != null)
{
string pelny = poGuid.Numer.NumerPelny; // odczyt pełnego numeru (kalkulowane)
}
Pułapki:
- W warunku LINQ używaj pola bazodanowego
Numer.Pelny; do odczytu sformatowanego numeru służy kalkulowanedok.Numer.NumerPelny— w wyrażeniu serwerowym rzuciłobyLinqConditionException. - Pełny numer nie jest globalnie unikalny (numeracja bywa per magazyn/seria/rok) — dlatego filtr
zwraca zbiór; bierz
.FirstOrDefault()albo dołóżdok.Magazyn == mag. - Indeksator
hm.DokHandlowe[guid]to dostęp poGuid(zGuidedTable) — dla nieznanegoGuidrzucaSoneta.Business.RowNotFoundException(NIE zwracanull). Gdy brak pewności istnienia, obuduj gotry/catch. Nie myl z dostępem poID(klucz wewnętrzny tabeli). - Numer obcy (dostawcy) jest w
dok.Obcy.Numer— to inne pole niż własnyNumer.
W30 — Korekty dokumentu i dokument korygowany
Cel: dla danego dokumentu ustalić jego korekty (dokumenty korygujące) oraz — dla korekty — dokument, który koryguje.
Warianty:
| Wariant | Pole / kierunek | Typ |
|---|---|---|
| Dokument korygowany przez tę korektę | korekta.DokumentKorygowany |
DokumentHandlowy (lub null) |
| Wszystkie korekty danego dokumentu | dok.DokumentyKorygujące |
IEnumerable<DokumentHandlowy> (łańcuch) |
| Najbliższa korekta | dok.DokumentKorygujący |
DokumentHandlowy (lub null) |
| Ostatnia korekta w łańcuchu | dok.DokumentKorygującyOstatni |
DokumentHandlowy |
| Czy dokument jest korektą | dok.Korekta |
bool (pole bazodanowe) |
| Serwerowy filtr korekt | hm.DokHandlowe[d => d.Korekta] |
SubTable<DokumentHandlowy> |
Pola i typy: dok.Korekta: bool (bazodanowe — czy dokument jest korektą),
dok.DokumentKorygowany: DokumentHandlowy, dok.DokumentyKorygujące: IEnumerable<DokumentHandlowy>,
dok.DokumentKorygujący/DokumentKorygującyOstatni: DokumentHandlowy,
dok.DokumentyKorygowane: IEnumerable<DokumentHandlowy> (cały łańcuch korygowanych) —
wszystkie powiązania kalkulowane (tylko do odczytu; korekty zakładaj przez IRelacjeService).
Snippet:
var hm = session.GetHandel();
var dok = hm.DokHandlowe[guid];
if (dok == null) return;
// Korekty tego dokumentu (łańcuch korekt — kolejne korekty korekt):
foreach (DokumentHandlowy korekta in dok.DokumentyKorygujące)
{
string nr = korekta.Numer.NumerPelny;
DokumentHandlowy korygowany = korekta.DokumentKorygowany; // wskazuje z powrotem na dok
}
// Gdy mamy w ręku korektę — odczyt dokumentu korygowanego:
if (dok.Korekta)
{
DokumentHandlowy zrodlo = dok.DokumentKorygowany; // dokument pierwotny
}
// Serwerowe wyszukanie samych korekt w okresie (pole Korekta jest bazodanowe):
var od = Date.Today.AddMonths(-1);
foreach (DokumentHandlowy k in hm.DokHandlowe.WgDaty[(DokumentHandlowy d) =>
d.Korekta && d.Data >= od])
{
// d.DokumentKorygowany — dokument, którego dotyczy korekta
}
Pułapki:
DokumentKorygowany/DokumentyKorygujące/DokumentKorygującysą kalkulowane (liczone z relacji handlowych) — tylko do odczytu. Tworzenie korekt realizujeIRelacjeService.NowaKorekta(...)(rozdział o relacjach), nie przypisywanie tych pól.- W warunku serwerowym wolno użyć tylko pola
Korekta(bazodanowe). Pola powiązań korekt są kalkulowane → w LINQ rzucąLinqConditionException. DokumentKorygowanyzwracanull, gdy dokument nie jest korektą (Korekta == false) — zawsze sprawdźdok.Korektaalbo!= nullprzed użyciem.DokumentyKorygująceto łańcuch (korekta korekty korekty…), a nie pojedynczy element — gdy potrzebujesz tylko najbliższej, użyjDokumentKorygujący; gdy ostatniej —DokumentKorygującyOstatni.
6. Magazyn, zasoby, partie, obroty
Sekcja opisuje odczyt efektów magazynowych dokumentu (zasoby, obroty) oraz sterowanie rozchodem przez wskazanie partii (
GrupaDostaw) i kontekst wyceny (FIFO/LIFO/wg dostaw). Cały kod operuje wyłącznie na publicznym kontrakcie platformy i jest zgodny z C# 10.Klucz do zrozumienia całej sekcji: magazyn księguje obroty i zasoby dopiero po
Session.Save()dokumentu. SamoCommit()/CommitUI()w transakcji nie nalicza stanów. W bazie Demo działaStanUjemnyVerifier— rozchód (FV/WZ/RW) wymaga wcześniejszego zapisanego przyjęcia (PW/PZ) tego towaru; w przeciwnym razie zapis rozchodu zostanie odrzucony.Słowniczek typów (moduł
Soneta.Magazyny):
Zasob(tabelaZasoby) — stan towaru: ilość na partii w danym magazynie i okresie.Obrot(tabelaObroty) — pojedynczy ruch (przychód lub rozchód) wiążący partie.GrupaDostaw(tabelaGrupyDostaw, namespaceSoneta.Magazyny.Dostawy) — partia towaru (identyfikowanaNumer+Towar).OkresMagazynowy(tabelaOkresyMag) — przedział czasu, w którym ewidencjonowane są obroty/zasoby; po zamknięciu blokuje modyfikacje.PartiaTowaru— subrow (nie tabela) opisujący stronę partii wObrot/Zasob:Dokument,PozycjaIdent,PartiaTowaru: GrupaDostaw,KontrahentPartii,Data,Czas,Typ,Wartosc.- Enum
KierunekPartii:Rozchód=-1,Brak=0,Przychód=1.- Enum
Magazyn.Algorytm(AlgorytmMagazynowy):FIFO=0,LIFO=1,NieLiczyćStanów=2,WgDostawy=3,WgDostawyPrzyZatwierdzaniu=10,OdNajdroższych=4,OdNajtańszych=5,WgCechyPozycji=6/7,WgCechyDokumentu=8/9.Dostęp do modułu:
var mag = session.GetMagazyny();→mag.Zasoby,mag.Obroty,mag.GrupyDostaw,mag.OkresyMag,mag.Magazyny.
W31 — Przeglądanie zasobów utworzonych przez dokument przychodowy (dok.Zasoby)
Cel: po zapisaniu dokumentu przychodowego (PW/PZ/FZ) odczytać zasoby magazynowe, które ten dokument wprowadził na stan — np. żeby zweryfikować ilości albo powiązać je z partią.
Warianty:
| Wariant | Źródło | Uwaga |
|---|---|---|
| Zasoby utworzone bezpośrednio przez dokument | dok.Zasoby (SubTable<Zasob>) |
filtr po Partia.Dokument == dok |
| Zasoby łącznie z dokumentami zależnymi | dok.ZasobyWszystkie (ListWithView) |
obejmuje powiązane dok. magazynowe |
| Iteracja po module | mag.Zasoby.WgTowar[towar, okres, magazyn] |
gdy nie mamy uchwytu do dokumentu |
Pola i typy: dok.Zasoby: SubTable (elementy Soneta.Magazyny.Zasob). Zasob:
Ilosc: Quantity, IloscRezerwowana: Quantity, Kierunek: KierunekPartii,
Magazyn: Magazyn, Towar: Towar, Okres: OkresMagazynowy, Partia: PartiaTowaru (subrow),
PartiaPierwotna: PartiaTowaru.
Snippet:
// dok — zapisany dokument przychodowy (PW/PZ/FZ), po session.Save()
var mag = session.GetMagazyny();
foreach (Zasob z in dok.Zasoby)
{
// strona partii zasobu: skąd pochodzi (dokument, pozycja, numer partii)
GrupaDostaw partia = z.Partia.PartiaTowaru; // rekord partii (może być null dla prostej ewidencji)
Console.WriteLine(
$"{z.Towar.Kod} mag={z.Magazyn.Symbol} kierunek={z.Kierunek} " +
$"ilość={z.Ilosc} partia={partia?.Numer}");
}
Pułapki:
dok.Zasobyjest puste, dopóki nie wykonaszsession.Save()— przed zapisem magazyn nie zaksięgował zasobów (samCommit/CommitUInie wystarcza).- Wzorzec testowy: zapis dokumentu →
SaveDispose()→ odczyt na świeżej sesji poGuid, bo poSave()w środku testu okno edycji się zamyka. - Zasób przychodowy ma
Kierunek == KierunekPartii.Przychód. Zasób rozchodowy na stanie ujemnym maKierunek == KierunekPartii.Rozchód— nie myl ich przy sumowaniu stanu. - Nie modyfikuj
Zasob/Obrotręcznie — to tabele wyliczane przez moduł magazynowy.
W32 — Przetwarzanie obrotów faktury sprzedaży i dokumentu rozchodowego (dok.Obroty, dok.ObrotyWszystkie)
Cel: odczytać obroty magazynowe (ruchy) wygenerowane przez dokument — rozchód (FV/WZ/RW) lub przychód — w tym obroty z dokumentów zależnych.
Warianty:
| Wariant | Property | Co zwraca |
|---|---|---|
| Obroty związane bezpośrednio z dokumentem | dok.Obroty (SubTable) |
dla przychodu: po stronie przychodowej; dla rozchodu: po stronie rozchodowej |
| Wszystkie obroty (z dok. zależnymi, bez storna zasobu) | dok.ObrotyWszystkie (ListWithView) |
obroty wszystkich powiązanych dok. magazynowych |
| Obroty wszystkich pozycji | dok.ObrotyWszystkiePozycji (ListWithView) |
po pozycjach (z pozycjami zależnymi) |
| Z korektami, wg partii pierwotnej | dok.ObrotyWszystkieWgPartiiPierwotnej (ListWithView) |
uwzględnia dok. korygujące |
Pola i typy: Obrot: Ilosc: Quantity, Towar: Towar, Magazyn: Magazyn,
Okres: OkresMagazynowy, Data: Date, Czas: Time, Korekta: KorektaObrotu,
Stornowany: Obrot, Przychod: PartiaTowaru, Rozchod: PartiaTowaru,
PrzychodPierwotny: PartiaTowaru.
Snippet:
// dok — zapisana faktura sprzedaży / dokument rozchodowy (po session.Save())
// 1) Obroty samego dokumentu (strona dobrana automatycznie wg kierunku magazynu):
foreach (Obrot o in dok.Obroty)
{
// Przychod/Rozchod to subrow PartiaTowaru — wskazuje partię i dokument źródłowy
GrupaDostaw partiaRozchodu = o.Rozchod.PartiaTowaru; // z której partii zszedł towar
GrupaDostaw partiaPrzychodu = o.Przychod.PartiaTowaru; // partia przychodowa (źródło)
Console.WriteLine($"{o.Towar.Kod} ilość={o.Ilosc} z partii={partiaPrzychodu?.Numer}");
}
// 2) Wszystkie obroty łącznie z dokumentami magazynowymi powiązanymi z fakturą:
foreach (Obrot o in dok.ObrotyWszystkie.Cast<Obrot>())
{
if (o.Korekta == KorektaObrotu.StornoZasobu) continue; // ObrotyWszystkie już to pomija
// ... agregacja ilości/wartości
}
Pułapki:
dok.Obrotyautomatycznie dobiera stronę (przychodowa vs rozchodowa) na podstawie kierunku magazynowego dokumentu — nie filtruj jej ręcznie po kierunku.ObrotyWszystkie/ObrotyWszystkiePozycji/ObrotyWszystkieWgPartiiPierwotnejzwracająListWithView— iteruj przez.Cast<Obrot>(). Pomijają już obrotyStornoZasobu.- Obroty pojawiają się po
Session.Save()dokumentu, nie poCommit(). Przychod/Rozchod/PrzychodPierwotnyto subrowPartiaTowaru, nie rekord partii — do rekorduGrupaDostawsięgaj przez.PartiaTowaru, do dokumentu źródłowego przez.Dokument, do pozycji przez.PozycjaIdent.
W33 — Odczyt stanu magazynowego towaru (magazyn / data) — mag.Zasoby z filtrem
Cel: wyliczyć aktualny stan towaru w danym magazynie (i ewentualnie okresie), bez otwierania konkretnego dokumentu — np. do walidacji dostępności przed rozchodem.
Warianty:
| Wariant | Indeks | Sygnatura |
|---|---|---|
| Stan towaru w magazynie | mag.Zasoby.WgTowar[towar, okres, magazyn] |
zawęź serwerowo do magazynu i okresu |
| Stan towaru we wszystkich okresach/magazynach | mag.Zasoby.WgTowar[towar] |
szersze — sumuj ostrożnie |
| Zasoby konkretnej partii | mag.Zasoby.WgPartiaTowaruMagazyn[partia, magazyn, towar] |
gdy znamy GrupaDostaw |
| Zasoby magazynu w okresie | mag.Zasoby.WgMagazyn[magazyn, okres] |
przegląd całego magazynu |
Pola i typy: mag.Zasoby: Zasoby (tabela). Indeksy zwracają SubTable<Zasob>.
OkresMagazynowy z mag.OkresyMag (patrz W39). Ilości to Quantity.
Snippet:
var mag = session.GetMagazyny();
var towar = session.GetTowary().Towary.WgKodu["BIKINI"];
var magazyn = mag.Magazyny.WgSymbol["F"];
var okres = mag.OkresyMag.WgOkres[Date.Today]; // okres obejmujący dzień (patrz W39)
// Stan = suma ilości zasobów przychodowych pomniejszona o rozchodowe (stan ujemny)
Quantity stan = new(0, towar.JednostkaMag.Symbol);
foreach (Zasob z in mag.Zasoby.WgTowar[towar, okres, magazyn])
{
if (z.Kierunek == KierunekPartii.Przychód)
stan += z.Ilosc;
else if (z.Kierunek == KierunekPartii.Rozchód)
stan -= z.Ilosc;
}
Pułapki:
- Nie ładuj całej tabeli
Zasobydo pamięci — zawsze zawężaj indeksem (WgTowar[...],WgMagazyn[...],WgPartiaTowaruMagazyn[...]). Patrzsafe-code.md§6. - Ilości są typu
Quantity(ilość + jednostka), niedouble— operuj naQuantityi pilnuj zgodności jednostek (z.Ilosc.Symbol). - Stan „na dzień" zależy od okresu magazynowego — dla daty historycznej wybierz właściwy
OkresMagazynowy, nie zawsze bieżący. - Towary bez magazynu (np. usługi „MONTAZ", „TRANSPORT" w Demo) nie mają zasobów — zapytanie zwróci pustą kolekcję.
- W bazie Demo stan ujemny jest blokowany przy zapisie rozchodu — odczyt stanu służy do
wcześniejszej walidacji, ale ostateczną kontrolę i tak wykona
Session.Save().
W34 — Wyszukiwanie partii magazynowych (GrupaDostaw) według cech
Cel: odnaleźć partię (GrupaDostaw) po numerze, towarze lub cesze (np. numer serii,
data ważności zapisana jako cecha), zanim wskażemy ją przy rozchodzie.
Warianty:
| Wariant | Klucz / mechanizm | Uwaga |
|---|---|---|
| Po numerze + towarze | mag.GrupyDostaw.WgNumer[numer, towar] |
klucz unikalny — pojedynczy rekord lub null |
| Po numerze (zbiór) | mag.GrupyDostaw.WgNumer[numer] |
zwraca SubTable<GrupaDostaw> |
| Wszystkie partie towaru | mag.GrupyDostaw.WgTowar[towar] |
partie danego towaru |
| Po dacie | mag.GrupyDostaw.WgData[data] |
indeks po Data |
| Po cesze | partie[(GrupaDostaw g) => warunek] na indeksie |
cecha musi być zdefiniowana |
Pola i typy: GrupaDostaw: Numer: string (public virtual, czasem nadawany
automatycznie), Towar: Towar, Data: Date, Blokada: bool,
Features: FeatureCollection, KodKreskowy: string. Klucz WgNumer = (Numer, Towar).
Snippet:
var mag = session.GetMagazyny();
var towar = session.GetTowary().Towary.WgKodu["BIKINI"];
// 1) Partia po numerze i towarze — klucz unikalny:
GrupaDostaw partia = mag.GrupyDostaw.WgNumer["LOT-2026-001", towar];
// 2) Wszystkie niezablokowane partie towaru — filtr serwerowy na indeksie:
foreach (GrupaDostaw g in mag.GrupyDostaw.WgTowar[(GrupaDostaw g) => !g.Blokada])
{
// odczyt cechy zapisanej na partii (np. numer serii / data ważności):
object seria = g.Features["NumerSerii"]; // cecha musi być wcześniej zdefiniowana
}
// 3) Filtr po dacie powstania partii:
foreach (GrupaDostaw g in mag.GrupyDostaw.WgData[Date.Today]) { /* ... */ }
Pułapki:
WgNumer[numer, towar]zwraca pojedynczy rekord (może byćnull);WgNumer[numer]iWgTowar[towar]zwracają zbiór (SubTable).- W
RowConditionużywaj tylko pól bazodanowych (Numer,Towar,Data,Blokada). Pola kalkulowane (np.KodKreskowy) i wartości cech rzucąLinqConditionException— cechę filtruj dopiero po materializacji albo przez dedykowany warunek na cesze. - Cecha (
Features["…"]) wymaga wcześniej zdefiniowanej definicji cechy — odwołanie do niezdefiniowanej cechy rzuca wyjątek (patrzfeatures.md). Numerpartii bywa nadawany automatycznie (autonumerowanie wg karty towaru lub wg cechy) — nie zakładaj, że zawsze ustawisz go ręcznie.
W35 — Dokument rozchodowy ze wskazaniem JEDNEJ partii
Cel: wystawić rozchód (WZ/RW/FV), w którym pozycja schodzi z konkretnej, wskazanej partii — a nie z partii wybranej automatycznie przez algorytm magazynu.
Warianty:
| Wariant | Mechanizm | Uwaga |
|---|---|---|
| Wskazanie partii przez pozycję dostawy | poz.Dostawa = pozycjaPrzyjęcia |
Dostawa: PozycjaDokHandlowego (pozycja PW/PZ) |
| Wskazanie partii pierwotnej | poz.DostawaPierwotna |
dla łańcucha korekt |
| Tryb wskazania na definicji | DefDokHandlowego.WskazaniePartii |
WyborPartiiOpcje (Dozwolony/Wymuszony…) |
| Identyfikacja przez cechę | gdy magazyn WgCechyPozycji |
partia wybierana wg cechy pozycji (W37, W39) |
Pola i typy: poz.Dostawa: PozycjaDokHandlowego (kategoria „Magazyn", opis „Pozycja
dostawy dla danego rozchodu magazynowego"). Tryb sterowany przez
DefDokHandlowego.WskazaniePartii: WyborPartiiOpcje (Zabroniony=0, Dozwolony=1,
Automatyczny=2, Wymuszony=4, WymuszonyDodawanie, WymuszonyZatwierdzanie,
WgTowaru=8).
Snippet:
var mag = session.GetMagazyny();
var towar = session.GetTowary().Towary.WgKodu["BIKINI"];
var magazyn = mag.Magazyny.WgSymbol["F"];
// WARUNEK WSTĘPNY: istnieje ZAPISANE przyjęcie (PW/PZ) tego towaru (Demo blokuje stan ujemny).
// Znajdź pozycję przyjęcia odpowiadającą partii, z której chcemy zejść:
GrupaDostaw partia = mag.GrupyDostaw.WgNumer["LOT-2026-001", towar];
Obrot przychod = mag.Obroty.WgPrzychodPartiaTowaruMagazyn[partia, magazyn, towar]
.Cast<Obrot>().FirstOrDefault();
PozycjaDokHandlowego pozycjaPrzyjecia = przychod?.Przychod.Dokument?
.Pozycje.Cast<PozycjaDokHandlowego>()
.FirstOrDefault(p => p.Towar == towar);
using (var t = session.Logout(editMode: true))
{
var dok = new DokumentHandlowy();
session.AddRow(dok);
dok.Definicja = session.GetHandel().DefDokHandlowych.WgSymbolu["WZ"];
dok.Magazyn = magazyn;
var poz = new PozycjaDokHandlowego(dok);
session.AddRow(poz);
poz.Towar = towar; // USTAW PIERWSZY
poz.Ilosc = new Quantity(2, poz.Ilosc.Symbol);
poz.Dostawa = pozycjaPrzyjecia; // WSKAZANIE JEDNEJ partii (dostawy)
t.Commit(); // CommitUI() w workerze/extenderze
}
session.Save(); // tu nalicza się obrót/zasób rozchodowy
Pułapki:
- Wskazanie partii działa tylko, gdy definicja dokumentu na to pozwala
(
WskazaniePartii != Zabroniony). PrzyZabronionypartia jest dobierana wyłącznie algorytmem magazynu — ustawieniepoz.Dostawazostanie zignorowane lub odrzucone. poz.Dostawato pozycja dokumentu przyjęcia (PozycjaDokHandlowego), a nie rekordGrupaDostaw. PartięGrupaDostawmapujesz na pozycję przyjęcia przez obrót przychodowy (Obrot.Przychod.Dokument+PozycjaIdent) — jak w snippetcie.- Demo blokuje stan ujemny: bez zapisanego przyjęcia tej partii
Session.Save()rozchodu rzuci wyjątek (StanUjemnyVerifier). - Pozycje obu dokumentów muszą być w tej samej sesji — nie mieszaj rekordów z różnych
sesji (
session.Get(...)). - Ustaw
poz.DostawaprzedCommit(); właściwy obrót zostaje naliczony dopiero wSave().
W36 — Dokument rozchodowy ze wskazaniem WIELU partii
Cel: wystawić rozchód, którego ilość pochodzi z kilku różnych partii (np. 10 szt: 6 z LOT-A, 4 z LOT-B) — każda partia jako osobna pozycja rozchodu wskazująca swoją dostawę.
Warianty:
| Wariant | Mechanizm | Uwaga |
|---|---|---|
| Pozycja per partia | po jednej PozycjaDokHandlowego na każdą wskazaną dostawę |
najprostszy, czytelny |
| Wybór przez worker dostaw | IRelacjeService + HandlerSet.WybierzDostawyCallback |
dla relacji nadrzędny→podrzędny |
| Automatyczny rozdział wg algorytmu | WskazaniePartii = Automatyczny |
platforma sama dzieli na partie |
Pola i typy: jak W35 — wiele pozycji, każda z własnym poz.Dostawa i poz.Ilosc.
Przy generowaniu z dokumentu nadrzędnego: IRelacjeService.NowyPodrzednyIndywidualny(...)
z HandlerSet { WybierzDostawyCallback = ... } (namespace
Soneta.Handel.RelacjeDokumentow.Api, wymaga using Microsoft.Extensions.DependencyInjection;).
Snippet:
var mag = session.GetMagazyny();
var towar = session.GetTowary().Towary.WgKodu["BIKINI"];
var magazyn = mag.Magazyny.WgSymbol["F"];
// Mapowanie: numer partii -> ilość do zejścia
var rozdzial = new (string numer, double ilosc)[] { ("LOT-A", 6), ("LOT-B", 4) };
using (var t = session.Logout(editMode: true))
{
var dok = new DokumentHandlowy();
session.AddRow(dok);
dok.Definicja = session.GetHandel().DefDokHandlowych.WgSymbolu["WZ"];
dok.Magazyn = magazyn;
foreach (var (numer, ilosc) in rozdzial)
{
GrupaDostaw partia = mag.GrupyDostaw.WgNumer[numer, towar];
Obrot przychod = mag.Obroty.WgPrzychodPartiaTowaruMagazyn[partia, magazyn, towar]
.Cast<Obrot>().FirstOrDefault();
PozycjaDokHandlowego dostawa = przychod?.Przychod.Dokument?
.Pozycje.Cast<PozycjaDokHandlowego>().FirstOrDefault(p => p.Towar == towar);
var poz = new PozycjaDokHandlowego(dok);
session.AddRow(poz);
poz.Towar = towar;
poz.Ilosc = new Quantity(ilosc, poz.Ilosc.Symbol);
poz.Dostawa = dostawa; // każda pozycja wskazuje INNĄ partię
}
t.Commit();
}
session.Save();
Pułapki:
- Każda wskazana partia = osobna pozycja rozchodu. Nie da się jedną pozycją wskazać
dwóch różnych partii —
poz.Dostawato pojedyncza referencja. - Suma ilości wskazanych partii musi mieścić się w zapisanym stanie każdej partii (Demo blokuje stan ujemny per partia).
- Przy generowaniu z dokumentu nadrzędnego (ZO→FV) wybór wielu dostaw realizuje
HandlerSet.WybierzDostawyCallback— brak implementacji callbacku przyWyborPozycjiDlaRelacji != BrakOknaskutkujeNotImplementedException. - Wszystkie pozycje w jednej transakcji edycyjnej, zapis raz przez
Session.Save().
W37 — Dokument przyjęcia (PW/PZ) z numerem serii — zapis numeru serii jako cecha
Cel: zarejestrować przyjęcie towaru i zapisać numer serii / partii. Jeśli nie ma
dedykowanego pola na serię, numer przenosimy jako cechę (Features) pozycji/dokumentu,
skąd platforma przenosi go na partię (GrupaDostaw) i obrót.
Warianty:
| Wariant | Mechanizm | Uwaga |
|---|---|---|
| Numer partii wprost | GrupaDostaw.Numer |
gdy partia jest tworzona/wskazywana jawnie |
| Numer serii jako cecha pozycji | poz.Features["NumerSerii"] = "..." |
przenoszony na partię/obrót |
| Autonumerowanie wg cechy | WyborPartiiAutonumerowanie.WgCechy |
numer partii brany z cechy |
| Data ważności jako cecha | poz.Features["DataWaznosci"] = date |
analogicznie do serii |
Pola i typy: dok.Features["…"] i poz.Features["…"]
(FeatureCollection, indeksator po nazwie definicji cechy, zwraca/przyjmuje object).
GrupaDostaw.Numer: string. Tryb numeracji partii:
WyborPartiiAutonumerowanie (Brak=0, Standardowe=1, WgCechy=2).
Snippet:
var mag = session.GetMagazyny();
var towar = session.GetTowary().Towary.WgKodu["BIKINI"];
using (var t = session.Logout(editMode: true))
{
var dok = new DokumentHandlowy();
session.AddRow(dok);
dok.Definicja = session.GetHandel().DefDokHandlowych.WgSymbolu["PW"]; // przyjęcie
dok.Magazyn = mag.Magazyny.WgSymbol["F"];
var poz = new PozycjaDokHandlowego(dok);
session.AddRow(poz);
poz.Towar = towar;
poz.Ilosc = new Quantity(10, poz.Ilosc.Symbol);
poz.Cena = new DoubleCy(5m, poz.Cena.Symbol);
// Numer serii jako cecha pozycji — przeniesiony na partię/obrót po Save:
poz.Features["NumerSerii"] = "LOT-2026-001"; // definicja cechy musi istnieć
t.Commit();
}
session.Save();
// Po zapisie partia jest dostępna w GrupyDostaw; numer serii odczytasz z cechy partii:
GrupaDostaw partia = mag.GrupyDostaw.WgTowar[towar].Cast<GrupaDostaw>()
.FirstOrDefault(g => Equals(g.Features["NumerSerii"], "LOT-2026-001"));
Pułapki:
- Cecha musi być wcześniej zdefiniowana (
FeatureSetDefinition) i — by przenosiła się na partię — odpowiednio skonfigurowana w module magazynowym. Odwołanie do niezdefiniowanej cechy rzuca wyjątek. - Partia powstaje dopiero po
Session.Save()przyjęcia — przed zapisemmag.GrupyDostawjej nie zawiera. - Gdy magazyn ma autonumerowanie
WgCechy,GrupaDostaw.Numerjest wyliczany z cechy — nie ustawiaj go ręcznie sprzecznie z cechą. - Filtr partii po wartości cechy rób po materializacji (jak w snippetcie) — wartości
cech nie są polami bazodanowymi, więc nie wejdą do
RowCondition.
W38 — Odczyt rozchodu zasobów: powiązanie pozycji rozchodu z partią pierwotną / przyjęciem
Cel: dla pozycji/obrotu rozchodowego ustalić, z której partii (i którego przyjęcia) zszedł towar — np. do raportu pochodzenia (traceability) lub rozliczenia kosztu.
Warianty:
| Wariant | Źródło | Co zwraca |
|---|---|---|
| Partia rozchodu | obrot.Rozchod.PartiaTowaru |
GrupaDostaw strony rozchodowej |
| Partia przychodowa (źródłowa) | obrot.Przychod.PartiaTowaru |
partia, z której zszedł towar |
| Partia pierwotna | obrot.PrzychodPierwotny.PartiaTowaru |
pierwotne przyjęcie (przed korektami) |
| Dokument/pozycja źródłowa | obrot.Przychod.Dokument, .PozycjaIdent |
przyjęcie i jego pozycja |
| Dostawa na pozycji rozchodu | poz.Dostawa, poz.DostawaPierwotna |
pozycja przyjęcia powiązana z rozchodem |
Pola i typy: subrow PartiaTowaru na Obrot/Zasob:
Dokument: DokumentHandlowy, PozycjaIdent: int, PartiaTowaru: GrupaDostaw,
KontrahentPartii: Kontrahent, Data: Date, Czas: Time, Typ: TypPartii,
Wartosc: decimal. Na pozycji: poz.Dostawa: PozycjaDokHandlowego,
poz.DostawaPierwotna: PozycjaDokHandlowego.
Snippet:
// dok — zapisany dokument rozchodowy (FV/WZ/RW)
foreach (Obrot o in dok.Obroty)
{
// Strona rozchodowa = partia, z której zeszła ilość:
GrupaDostaw partiaRozchodu = o.Rozchod.PartiaTowaru;
// Strona przychodowa = przyjęcie, z którego pochodzi towar (pochodzenie):
DokumentHandlowy przyjecie = o.Przychod.Dokument;
GrupaDostaw partiaZrodlowa = o.Przychod.PartiaTowaru;
// Pierwotne przyjęcie (przed łańcuchem korekt):
GrupaDostaw partiaPierwotna = o.PrzychodPierwotny.PartiaTowaru;
Console.WriteLine(
$"{o.Towar.Kod} ilość={o.Ilosc} z przyjęcia={przyjecie?.Numer} " +
$"partia={partiaZrodlowa?.Numer} kontrahent={o.Przychod.KontrahentPartii?.Kod}");
}
// Powiązanie na poziomie pozycji rozchodu:
foreach (PozycjaDokHandlowego poz in dok.Pozycje)
{
PozycjaDokHandlowego pozycjaPrzyjecia = poz.Dostawa; // pozycja PW/PZ
}
Pułapki:
- Rozróżniaj
Przychod(źródło, czyli przyjęcie),Rozchod(bieżący rozchód) iPrzychodPierwotny(źródło sprzed korekt). Do raportu pochodzenia używajPrzychod/PrzychodPierwotny. obrot.Przychod/Rozchodto subrowPartiaTowaru— nie jestnulljako struktura, ale jego pola (np.PartiaTowaru,Dokument) mogą być puste dla prostej ewidencji bez partii. Zabezpiecz odczyt?..- Jedna pozycja rozchodu może wygenerować wiele obrotów (gdy zeszła z kilku przychodów, np. FIFO) — iteruj po obrotach, nie zakładaj relacji 1:1 pozycja↔partia.
- Odczyt sensowny dopiero po
Session.Save()dokumentu (przed zapisem brak obrotów).
W39 — Odczyt okresów magazynowych i kontekstu wyceny (FIFO/LIFO/wg dostaw)
Cel: ustalić aktywny okres magazynowy dla daty oraz dowiedzieć się, jakim algorytmem magazyn wycenia rozchód (co decyduje o wyborze partii, gdy nie wskazujemy jej ręcznie).
Warianty:
| Wariant | Źródło | Uwaga |
|---|---|---|
| Okres dla daty | mag.OkresyMag.WgOkres[data] |
klucz po Okres.To |
| Czy okres zamknięty | okres.Zamkniety: bool |
zamknięcie blokuje modyfikacje |
| Algorytm rozchodu magazynu | magazyn.Algorytm: AlgorytmMagazynowy |
FIFO/LIFO/wg dostaw/wg cechy |
| Cecha algorytmu (wg cechy) | magazyn.CechaAlgorytmu: string |
nazwa cechy pozycji/dokumentu |
Pola i typy: OkresMagazynowy: Okres: FromTo, Zamkniety: bool. Tabela OkresyMag,
indeks WgOkres (po Okres.To). Magazyn.Algorytm: AlgorytmMagazynowy (FIFO=0,
LIFO=1, NieLiczyćStanów=2, WgDostawy=3, WgDostawyPrzyZatwierdzaniu=10,
OdNajdroższych=4, OdNajtańszych=5, WgCechyPozycji=6/7, WgCechyDokumentu=8/9),
Magazyn.CechaAlgorytmu: string.
Snippet:
var mag = session.GetMagazyny();
var magazyn = mag.Magazyny.WgSymbol["F"];
// Okres magazynowy obejmujący wskazaną datę:
OkresMagazynowy okres = mag.OkresyMag.WgOkres[Date.Today];
bool zamkniety = okres != null && okres.Zamkniety;
// Kontekst wyceny rozchodu (jak magazyn dobiera partie automatycznie):
AlgorytmMagazynowy algorytm = magazyn.Algorytm;
bool rozchodWgCechy =
algorytm is AlgorytmMagazynowy.WgCechyPozycji or AlgorytmMagazynowy.WgCechyPozycjiMalejąco
or AlgorytmMagazynowy.WgCechyDokumentu or AlgorytmMagazynowy.WgCechyDokumentuMalejąco;
string cechaWyceny = rozchodWgCechy ? magazyn.CechaAlgorytmu : null;
string opisWyceny = algorytm switch
{
AlgorytmMagazynowy.FIFO => "rozchód od najstarszych dostaw",
AlgorytmMagazynowy.LIFO => "rozchód od najnowszych dostaw",
AlgorytmMagazynowy.WgDostawy => "rozchód wg wskazanej dostawy (partii)",
_ => algorytm.ToString()
};
Pułapki:
- Gdy magazyn liczy
WgDostawy(wskazanie partii) lubWgCechy*, automatyczny dobór partii zależy odpoz.Dostawa(W35/W36) lub cechy (CechaAlgorytmu) — bez nich rozchód nie zostanie poprawnie rozliczony. NieLiczyćStanówoznacza, że magazyn nie prowadzi zasobów —dok.Zasobypozostanie puste, a kontroli stanu ujemnego nie ma.- Modyfikacja dokumentów w zamkniętym okresie (
okres.Zamkniety == true) zostanie odrzucona — sprawdź to przed edycją wstecz. OkresMagazynowyto dane konfiguracyjne (config="true",guided) — nie twórz okresów „w locie" w kodzie operacyjnym; korzystaj z istniejących.
7. Cechy (Features)
Cechy (Features) to dodatkowe, definiowalne informacje przypisane do Row — tu: do dokumentu
(DokumentHandlowy) i pozycji (PozycjaDokHandlowego). Definicje cech (FeatureDefinition) tworzy
się we wdrożeniu (bez konwersji bazy); cecha jest adresowana po nazwie definicji. Dostęp daje
property Features (Soneta.Business.FeatureCollection) oraz nietypowany indeksator Row["Nazwa"].
Fundamenty cech opisuje references/features.md — tu pokazujemy ich użycie na dokumencie handlowym.
Cechy są częścią publicznego kontraktu. Samo przenoszenie cech (z partii / z dokumentu nadrzędnego) jest sterowane konfiguracją definicji dokumentu/relacji, a nie wywoływane imperatywnie z dodatku — patrz W40.
W40 — Przenoszenie cech z partii (dostawy) / towaru na pozycję dokumentu
Cel: sprawić, by przy rozchodzie magazynowym cechy zapisane na partii (dostawie) trafiły na
pozycję dokumentu rozchodowego, a przy przekształceniach w relacjach — by cechy dokumentu/pozycji
nadrzędnej zostały skopiowane na dokument podrzędny. To mechanizm konfiguracyjny: ustawiasz flagi
na DefDokHandlowego / definicji relacji, platforma kopiuje cechy automatycznie podczas operacji.
Warianty:
| Wariant | Gdzie ustawić | Pole / mechanizm |
|---|---|---|
| Partia (dostawa) → pozycja rozchodu | definicja dokumentu rozchodowego (WZ/RW/FV) | DefDokHandlowego.KopiujCechyDostawy: bool |
| Dokument nadrzędny → podrzędny (cechy nagłówka) | definicja relacji | KopiujCechyDokumentu: bool |
| Dokument nadrzędny → podrzędny (cechy pozycji) | definicja relacji | KopiujCechyPozycji: bool |
| Wybrane cechy + synchronizacja zwrotna | definicja relacji | konfiguracja „kopiuj cechy" z listą definicji + flagą synchronizacji |
| Ręczne dopisanie cechy na pozycji | kod dodatku | poz["Nazwa"] = wartość w transakcji (W41) |
Pola i typy:
DefDokHandlowego.KopiujCechyDostawy: bool— „Kopiuj cechy z dostawy"; włącza przeniesienie cech partii na pozycję dokumentu rozchodowego przy wskazaniu zasobu / księgowaniu rozchodu.- Na definicji relacji:
KopiujCechyDokumentu: bool,KopiujCechyPozycji: bool— wymuszają kopiowanie cech (nagłówka / pozycji) z dokumentu nadrzędnego na podrzędny. poz.Features/poz["Nazwa"]— odczyt/zapis cechy pozycji (typFeatureCollection/object).- Warunkiem działania jest istnienie tej samej definicji cechy zarejestrowanej dla obu tabel
(
PozycjeDokHan, ewentualnie partia/towar) — kopiowane są cechy o zgodnej nazwie.
Snippet:
// Włączenie przenoszenia cech z dostawy na pozycję rozchodu — konfiguracja definicji WZ.
// (jednorazowo, na etapie wdrożenia; wykonywane w sesji KONFIGURACYJNEJ)
var handel = session.GetHandel();
var defWZ = handel.DefDokHandlowych.WgSymbolu["WZ"];
using (var t = session.Logout(editMode: true))
{
defWZ.KopiujCechyDostawy = true; // cechy partii trafią na pozycję dokumentu rozchodowego
t.Commit();
}
session.Save();
// Po włączeniu flagi: tworzysz przyjęcie z cechą partii, a przy rozchodzie (wskazanie zasobu)
// cecha jest kopiowana na pozycję automatycznie — nie kopiujesz jej w kodzie.
// Przyjęcie (PW/PZ) — cecha "NrSerii" zapisana na pozycji = cecha dostawy/partii:
using (var t = session.Logout(editMode: true))
{
var pw = new DokumentHandlowy();
session.AddRow(pw);
pw.Definicja = handel.DefDokHandlowych.WgSymbolu["PW"];
pw.Magazyn = session.GetMagazyny().Magazyny.WgSymbol["F"];
var poz = new PozycjaDokHandlowego(pw);
session.AddRow(poz);
poz.Towar = session.GetTowary().Towary.WgKodu["BIKINI"];
poz.Ilosc = new Quantity(10, poz.Ilosc.Symbol);
poz.Cena = new DoubleCy(5m, poz.Cena.Symbol);
poz["NrSerii"] = "S-2026-001"; // cecha partii (definicja "NrSerii" dla PozycjeDokHan)
pw.Stan = StanDokumentuHandlowego.Zatwierdzony;
t.Commit();
}
session.Save(); // dopiero teraz powstaje zasób/partia z cechą
// Rozchód WZ ze wskazaniem partii — cecha "NrSerii" pojawi się na pozycji WZ
// dzięki KopiujCechyDostawy = true (kopiowane przez platformę przy księgowaniu rozchodu).
Pułapki:
- Przeniesienie cech z dostawy to konfiguracja, nie API: bez
KopiujCechyDostawy = truena definicji dokumentu rozchodowego nic się nie skopiuje — nie próbuj „przepisywać" cech partii imperatywnie z dodatku. - Kopiowane są cechy o tej samej nazwie definicji zarejestrowane dla pozycji; definicja cechy
musi istnieć przed użyciem (inaczej
poz["Nazwa"] = …rzuci wyjątek — patrz W41). - Cecha partii „materializuje się" dopiero po
Session.Save()dokumentu przychodowego (to wtedy powstaje zasób/obrót). Wskazanie partii przy rozchodzie i kopiowanie cechy działa na zapisanych zasobach (Demo blokuje stan ujemny — rozchód wymaga wcześniejszego zapisanego przyjęcia). - Kopiowanie nadrzędny→podrzędny w relacjach (
KopiujCechyDokumentu/KopiujCechyPozycji) ustawia się na definicji relacji, nie na definicji dokumentu; faktyczne tworzenie podrzędnego rób przezIRelacjeService(sekcja relacji), a cechy dojdą same. - Konfigurację definicji rób w sesji konfiguracyjnej (
config: true) — to dane konfiguracyjne, nie operacyjne (safe-code.md).
W41 — Odczyt i zapis cech dokumentu / pozycji (Features)
Cel: odczytać i ustawić wartości cech na dokumencie handlowym i jego pozycjach — zarówno
nietypowano (po nazwie definicji), jak i typowano (gettery FeatureCollection).
Warianty:
| Wariant | Dostęp | Zwraca / przyjmuje |
|---|---|---|
| Odczyt nietypowany | dok["Nazwa"], poz["Nazwa"] |
object (null, gdy brak wartości) |
| Odczyt typowany | dok.Features.GetString/GetInt/GetDecimal/GetDate/GetBool/GetCurrency/GetDoubleCy/GetPercent/GetAmount(...) |
konkretny typ Soneta |
| Zapis (dowolny typ) | dok["Nazwa"] = wartość w transakcji |
— |
| Sprawdzenie istnienia | dok.Features.Exists("Nazwa") |
bool |
| Usunięcie wartości | dok.Features.Remove("Nazwa") w transakcji |
— |
| Kopiowanie całego zestawu | źródło.Features.CopyTo(cel.Features) |
— |
| Lista definicji | dok.Features.Definitions |
FeatureDefinitions |
Pola i typy:
DokumentHandlowy.Features: Soneta.Business.FeatureCollection,PozycjaDokHandlowego.Features: Soneta.Business.FeatureCollection.- Indeksator nietypowany:
object this[string name]naRow(dok["Nazwa"]) — równoważnydok.Features["Nazwa"]. - Gettery typowane (wybór):
GetString,GetInt,GetBool,GetDecimal,GetDouble,GetDate,GetTime,GetFromTo,GetFraction,GetPercent,GetCurrency,GetDoubleCy,GetDictionaryItem,GetRow,GetHistory,GetArray. - Pomocnicze:
Exists(string),Remove(string),IsChanged,Definitions.
Snippet:
var handel = session.GetHandel();
var dok = handel.DokHandlowe.WgDaty[...]; // lub Get<DokumentHandlowy>(guid) w testach
// --- Odczyt nietypowany (object; null gdy brak wartości) ---
object centrum = dok["CentrumKosztow"];
if (centrum == null) { /* cecha bez wartości na tym dokumencie */ }
// --- Odczyt typowany przez Features ---
string opis = dok.Features.GetString("OpisDodatkowy");
Date dostawa = dok.Features.GetDate("DataDostawy");
bool pilne = dok.Features.GetBool("Pilne");
// pozycja:
PozycjaDokHandlowego poz = dok.Pozycje.Cast<PozycjaDokHandlowego>().First();
string nrSerii = poz.Features.GetString("NrSerii");
// --- Zapis cech: wymaga transakcji edycyjnej (jak każda modyfikacja Row) ---
using (var t = session.Logout(editMode: true))
{
dok["OpisDodatkowy"] = "Pilna realizacja"; // String
dok["Pilne"] = true; // Bool
dok["DataDostawy"] = Date.Today.AddDays(3); // Date
poz["NrSerii"] = "S-2026-001"; // String na pozycji
t.Commit(); // CommitUI() w workerze/extenderze
}
session.Save();
// Istnienie / usunięcie wartości:
bool ma = dok.Features.Exists("OpisDodatkowy");
using (var t = session.Logout(editMode: true))
{
dok.Features.Remove("OpisDodatkowy");
t.Commit();
}
session.Save();
Pułapki:
- Cecha musi mieć wcześniej utworzoną definicję (
FeatureDefinition) zarejestrowaną dla właściwej tabeli (DokHandlowedla dokumentu,PozycjeDokHandla pozycji). Odwołanie do niezdefiniowanej cechy rzuca wyjątek — to nie to samo co pole natywne. - Każdy zapis cechy to modyfikacja
Row→ musi być w transakcji (session.Logout(true)+Commit/CommitUI), potemSave. Odczyt transakcji nie wymaga. - Indeksator nietypowany zwraca
object; dla wartości pieniężnych/ilościowych zapisuj właściwy typ Soneta (Currency,DoubleCy,Amount,Percent,Date), nie surowydecimal/double/string. - Cechy algorytmiczne: przypisanie wartości uruchamia algorytm definicji — efekty uboczne; część
cech bywa read-only (
IsReadOnly(fd)/ trybSpecialEdit) i edycja rzuciAccessDeniedException. - W form.xml cechę adresuje się ścieżką
Features.Nazwa(np.{Features.NrSerii}), także przez relację ({Kontrahent.Features.Segment}). dok.Pozycjeto kolekcja pozycji dokumentu — iteruj po niej, nie ładuj całej tabeliPozycjeDokHan.
W42 — Filtrowanie / wyszukiwanie dokumentów i partii po wartości cechy (serwerowo)
Cel: znaleźć dokumenty, pozycje, towary lub partie spełniające warunek na wartości cechy — z filtrowaniem wykonywanym po stronie SQL, bez ładowania całej tabeli do pamięci.
Warianty:
| Wariant | Konstrukcja warunku | Uwaga |
|---|---|---|
| Równość wartości cechy | new FieldCondition.Equal("Features.Nazwa", wartość) |
string-path, bo Features.X nie jest typowaną property |
| Większy / mniejszy | FieldCondition.GreaterEqual / LessEqual("Features.Nazwa", v) |
dla cech liczbowych/dat |
| Łączenie warunków | new RowCondition.And(...) / RowCondition.Or(...) |
składanie warunków serwerowych |
| Na indeksie tabeli | tabela.WgKlucz[condition] |
filtr aplikowany na indeksie (SQL) |
Na kolekcji SubTable |
dok.Pozycje[condition] |
filtr na pozycjach dokumentu |
| W widoku (UI) | view.Condition &= new FieldCondition.Equal("Features.Nazwa", v) |
tylko kod UI / ViewInfo |
Pola i typy:
Soneta.Business.FieldCondition.Equal/GreaterEqual/LessEqual/...(string path, object value)— ścieżka cechy to literał"Features.NazwaDefinicji".Soneta.Business.RowCondition.And/RowCondition.Or— kompozycja warunków.- Indeksy do filtrowania:
handel.DokHandlowe.WgDaty[condition](dokumenty),towary.Towary.WgKodu[condition](towary),magazyny.GrupyDostaw[...](partie).
Snippet:
// 1) Towary po wartości cechy "Dystrybutor" = "Abc" (filtr serwerowy na indeksie)
var towary = session.GetTowary().Towary;
foreach (Towar t in towary.WgKodu[new FieldCondition.Equal("Features.Dystrybutor", "Abc")])
{
// ... tylko towary o tej cesze; SQL filtruje po DataKey cechy
}
// 2) Dokumenty handlowe oznaczone cechą "Pilne" = true
var handel = session.GetHandel();
foreach (DokumentHandlowy d in
handel.DokHandlowe.WgDaty[new FieldCondition.Equal("Features.Pilne", true)])
{
// ...
}
// 3) Złożony warunek: cecha LUB cecha (OR) — wszystkie indeksowane serwerowo
var orWarunek = new RowCondition.Or(
new FieldCondition.Equal("Features.Dystrybutor", "Abc"),
new FieldCondition.Equal("Features.Dystrybutor", "Cba"));
var wybrane = towary.WgKodu[orWarunek].ToArray();
// 4) Filtr po cesze + zakres (np. cecha-data dostawy >= dziś) na dokumentach
var pilneNaDzis = new RowCondition.And(
new FieldCondition.Equal("Features.Pilne", true),
new FieldCondition.GreaterEqual("Features.DataDostawy", Date.Today));
foreach (DokumentHandlowy d in handel.DokHandlowe.WgDaty[pilneNaDzis]) { /* ... */ }
// 5) Pozycje konkretnego dokumentu po cesze (filtr na kolekcji SubTable)
foreach (PozycjaDokHandlowego p in
dok.Pozycje[new FieldCondition.Equal("Features.NrSerii", "S-2026-001")])
{
// ...
}
Pułapki:
- Cechy adresuj string-pathem
"Features.Nazwa"wFieldCondition—Features.Xnie jest typowaną propertyRow, więc nie da się jej użyć w wyrażeniu LINQ ((Row r) => r.Features…). - Warunek aplikuj na indeksie (
WgKodu[...],WgDaty[...]) lub na kolekcjiSubTable(dok.Pozycje[...]) — to wykonuje filtr w SQL. Nie iteruj całej tabeli zifw pamięci (safe-code.md§6). - Wyszukiwanie korzysta z indeksowanego pola
DataKeycechy; wartość w warunku podawaj w typie zgodnym z typem cechy (np.booldla cechy Bool,Datedla cechy Date) — wartości są zapisane w ustalonym formacie tekstowym (patrz tabela typów wreferences/features.md). view.Condition &= …to mechanizm UI (ViewInfo/folder); w kodzie biznesowym używajSubTable[condition], nie obiektuView.DokHandloweto tabela operacyjna guided — przy szerokich przekrojach dodatkowo zawężaj zakres czasowy (data dokumentu), nie tylko warunek na cesze.
8. VAT, wartości i waluty
Rozdział opisuje publiczny kontrakt dokumentu handlowego w zakresie tabeli VAT, podsumowań wartości, ręcznej korekty VAT, sposobu liczenia VAT oraz zmiany waluty dokumentu i cen. Cały kod jest zgodny z C# 10 i operuje wyłącznie na publicznych typach i workerach platformy.
Wartości pieniężne na pozycjach tabeli VAT i podsumowaniach mają dwie reprezentacje:
BruttoNetto— kwoty w walucie systemowej jakodecimal(Netto,VAT,Brutto);BruttoNettoCy— kwoty w walucie dokumentu jakoCurrency(NettoCy,VATCy,BruttoCy). Nie operuj na niezaokrąglonychdecimal— platforma weryfikuje zaokrąglenie (safe-code §10).
W43 — Odczytanie tabeli VAT (SumyVAT)
Cel: odczytać rozbicie wartości dokumentu na stawki VAT (netto / VAT / brutto wg stawki) — np. do wydruku, eksportu lub kontroli sumy podatku.
Warianty:
| Wariant | Źródło | Uwaga |
|---|---|---|
| Tabela VAT dokumentu | dok.SumyVAT (SubTable<SumaVAT>) |
po jednej pozycji na stawkę |
| Kwoty w walucie systemowej | suma.Suma (BruttoNetto) |
Netto/VAT/Brutto jako decimal |
| Kwoty w walucie dokumentu | suma.SumaCy (BruttoNettoCy) |
NettoCy/VATCy/BruttoCy jako Currency |
| Procent / opis stawki | suma.Stawka, suma.DefinicjaStawki |
StawkaVat.Procent: Percent |
| Sumy z dokumentów nadrzędnych | dok.NadrzędneSumyVAT (IList) |
scalone stawki nadrzędnych |
Pola i typy: dok.SumyVAT: SubTable<SumaVAT>. SumaVAT udostępnia: DefinicjaStawki: DefinicjaStawkiVat, Stawka: StawkaVat (Stawka.Procent: Percent), Suma: BruttoNetto
(Netto, VAT, Brutto — decimal), SumaCy: BruttoNettoCy (NettoCy, VATCy, BruttoCy —
Currency), Dokument: DokumentHandlowy.
Snippet:
var dok = session.GetHandel().DokHandlowe.WgDaty[...]; // lub po Guid
// Iteracja po tabeli VAT — jedna pozycja (SumaVAT) na każdą stawkę dokumentu:
foreach (SumaVAT s in dok.SumyVAT)
{
Percent stawka = s.Stawka.Procent; // np. 23%
decimal netto = s.Suma.Netto; // kwota netto w walucie systemowej
decimal vat = s.Suma.VAT; // kwota podatku VAT
decimal brutto = s.Suma.Brutto; // kwota brutto
// Kwoty w walucie dokumentu (Currency = wartość + symbol waluty):
Currency vatCy = s.SumaCy.VATCy;
Console.WriteLine($"{stawka}: netto={netto} VAT={vat} brutto={brutto}");
}
// Łączna kwota VAT dokumentu z tabeli VAT:
decimal vatRazem = dok.SumyVAT.Sum(s => s.Suma.VAT);
Pułapki:
dok.SumyVATtoSubTable<SumaVAT>— kolekcja serwerowa; iteruj po niej, nie materializuj do listy, jeśli wystarczy przebieg jednorazowy. Tabela VAT jest mała (kilka stawek), więc.Sum(...)jest akceptowalne.- Rozróżniaj
Suma(BruttoNetto,decimalw walucie systemowej) odSumaCy(BruttoNettoCy,Currencyw walucie dokumentu). Dla dokumentu walutowego do prezentacji używajSumaCy. StawkatoStawkaVat(typ stawki),ProcentzwracaPercent— nie myl zdecimal.- Tabela VAT jest wyliczana z pozycji dokumentu (chyba że włączono
KorektaVAT— patrz W45). Nie modyfikuj jej, gdy chcesz tylko odczytać wartości.
W44 — Odczyt podsumowań wartości dokumentu
Cel: odczytać zsumowane wartości netto / VAT / brutto całego dokumentu oraz proponowany rabat — bez ręcznego sumowania pozycji.
Warianty:
| Wariant | Pole | Typ | Uwaga |
|---|---|---|---|
| Podsumowanie dokumentu | dok.Suma |
BruttoNetto |
Netto/VAT/Brutto (decimal, waluta systemowa) |
| Wartość brutto w walucie | dok.BruttoCy |
Currency |
brutto w walucie dokumentu |
| Suma wyliczona z pozycji | dok.SumaPozycji |
BruttoNettoPozycji |
Netto/VAT/Brutto (read-only) |
| Suma pozycji tow./prod. | dok.SumaPozycjiTowProd |
BruttoNettoPozycji |
tylko towary i produkty |
| Proponowany rabat | dok.Rabat |
Percent |
przepisywany do pozycji |
Pola i typy: dok.Suma: BruttoNetto (podsumowana wartość dokumentu), dok.BruttoCy: Currency,
dok.SumaPozycji: BruttoNettoPozycji (Netto/VAT/Brutto — decimal, tylko do odczytu,
liczone na bieżąco z pozycji), dok.Rabat: Percent.
Snippet:
var dok = session.GetHandel().DokHandlowe.WgDaty[...];
// Podsumowanie całego dokumentu (waluta systemowa):
decimal netto = dok.Suma.Netto;
decimal vat = dok.Suma.VAT;
decimal brutto = dok.Suma.Brutto;
// Brutto w walucie dokumentu (dla dokumentów walutowych):
Currency bruttoCy = dok.BruttoCy;
// Suma wyliczana z pozycji (przydatne do kontroli spójności z dok.Suma):
var sp = dok.SumaPozycji;
Console.WriteLine($"Pozycje: netto={sp.Netto} VAT={sp.VAT} brutto={sp.Brutto}");
// Proponowany rabat dokumentu (przepisywany do nowych pozycji):
Percent rabat = dok.Rabat;
Pułapki:
dok.Sumato stan zapisany podsumowania, adok.SumaPozycjijest wyliczane na bieżąco z pozycji za każdym odczytem. Dla dokumentu w buforze, przed ponownym przeliczeniem, mogą się chwilowo różnić.SumaPozycji/SumaPozycjiTowProdzwracająBruttoNettoPozycji— typ tylko do odczytu (brak setterów); nie próbuj przez nie modyfikować wartości.dok.RabattoPercent— proponowany rabat dokumentu, przepisywany do nowo dodawanych pozycji; ustawienie nie przelicza wstecznie pozycji już istniejących.- Wartości brutto/netto na poziomie dokumentu zależą od
LiczonaOd(W46) i ewentualnej korekty tabeli VAT (KorektaVAT, W45).
W45 — Ręczna korekta tabeli VAT (KorektaVAT)
Cel: ręcznie skorygować kwoty w tabeli VAT (gdy wyliczenie z pozycji nie odpowiada wartości
docelowej — np. zaokrąglenia faktury źródłowej), włączając flagę KorektaVAT i edytując wiersze
SumyVAT.
Warianty:
| Wariant | Operacja |
|---|---|
| Włączenie trybu korekty | dok.KorektaVAT = true |
| Ręczna zmiana kwoty stawki | edycja suma.Suma.Netto / .VAT / .Brutto na wierszu SumaVAT |
| Dostępność korekty | dok.IsReadOnlyKorektaVAT(), dok.IsReadOnlySumyVAT() (sterowanie UI) |
| Powrót do automatu | dok.KorektaVAT = false (tabela liczona ponownie z pozycji) |
Pola i typy: dok.KorektaVAT: bool (czy sumy VAT zmieniono ręcznie i nie zależą od pozycji),
SumaVAT.Suma: BruttoNetto (Netto/VAT/Brutto — decimal). Wiersze tabeli VAT są edytowalne
tylko gdy KorektaVAT == true (SumaVAT.IsReadOnly() zwraca true przy wyłączonej fladze).
Worker
KorektaTabeliVATWorkerjestinternal— nie da się go zainstancjonować z dodatku zewnętrznego. Publiczny tor korekty prowadzi przez flagędok.KorektaVATi bezpośrednią edycję pól wierszydok.SumyVAT.
Snippet:
var dok = session.GetHandel().DokHandlowe.WgDaty[...];
using (var t = session.Logout(editMode: true)) // CommitUI() w workerze/extenderze
{
// 1. Włącz ręczną korektę — odblokowuje edycję wierszy tabeli VAT:
dok.KorektaVAT = true;
// 2. Skoryguj kwoty na wybranej stawce (np. wyrównanie groszowe na 23%):
foreach (SumaVAT s in dok.SumyVAT)
{
if (s.Stawka.Procent == new Percent(0.23))
{
s.Suma.VAT = 230.01m; // wartości MUSZĄ być zaokrąglone do grosza
s.Suma.Brutto = 1230.01m;
}
}
t.Commit();
}
session.Save();
Pułapki:
- Edycja wierszy
SumyVATbezdok.KorektaVAT = truezostanie zablokowana —SumaVATjest wtedy read-only (sumy zależą od pozycji). - Przypisywane kwoty muszą być zaokrąglone do grosza — w trybie DEBUG ustawienie
niezaokrąglonej wartości
Netto/VAT/BruttorzucaArgumentException. Zaokrąglaj wejście (Soneta.Tools.Math.RoundCy(...)). KorektaVATjest dostępna tylko, gdy definicja dokumentu na to pozwala (Definicja.SumyVATw trybie korekty) — sprawdzajdok.IsReadOnlyKorektaVAT()zanim ustawisz flagę z poziomu UI.- Po włączeniu korekty tabela VAT przestaje śledzić zmiany pozycji. Wyłączenie
(
KorektaVAT = false) przywraca wyliczanie z pozycji i nadpisuje ręczne kwoty. DefinicjaStawkina wierszuSumaVATmożna zmieniać tylko przy włączonej korekcie (IsReadOnlyDefinicjaStawki()zależy odKorektaVAT).
W46 — Sposób liczenia VAT (LiczonaOd) i przeliczenie procedur VAT
Cel: ustawić, czy dokument jest liczony od netto czy od brutto (LiczonaOd), oraz przeliczyć
procedury VAT (JPK) na dokumencie zatwierdzonym/zaksięgowanym przy użyciu publicznego workera.
Warianty:
| Wariant | Mechanizm |
|---|---|
| Liczenie od netto | dok.LiczonaOd = SposobLiczeniaVAT.OdNetto |
| Liczenie od brutto | dok.LiczonaOd = SposobLiczeniaVAT.OdBrutto |
| Od brutto minus netto | dok.LiczonaOd = SposobLiczeniaVAT.OdBruttoMinusNetto |
| Wg ustawień kontrahenta | dok.LiczonaOd = SposobLiczeniaVAT.ZależyOdKontrahenta |
| Przeliczenie procedur VAT | worker PrzeliczProceduryVATWorker (publiczny) |
Pola i typy: dok.LiczonaOd: SposobLiczeniaVAT — enum Soneta.Handel.SposobLiczeniaVAT:
OdNetto=1, OdBrutto=2, OdBruttoMinusNetto=3, ZależyOdKontrahenta=4 (wartość 0 jest
niedozwolona — rzuca RequiredException). Worker PrzeliczProceduryVATWorker ma publiczną klasę
parametrów PrzeliczProceduryVATParams : ContextBase (Zatwierdzone: bool = true,
Zaksiegowane: bool = false) oraz właściwości [Context]: Dokument: DokumentHandlowy,
Params: PrzeliczProceduryVATParams.
Snippet:
var dok = session.GetHandel().DokHandlowe.WgDaty[...];
// 1. Zmiana sposobu liczenia VAT (dokument w buforze):
using (var t = session.Logout(editMode: true))
{
dok.LiczonaOd = SposobLiczeniaVAT.OdBrutto; // 0 jest niedozwolone
t.Commit();
}
session.Save();
// 2. Przeliczenie procedur VAT (JPK) workerem publicznym.
// Worker działa tylko dla dokumentu zatwierdzonego (Params.Zatwierdzone)
// lub zablokowanego/zaksięgowanego (Params.Zaksiegowane):
var p = new PrzeliczProceduryVATWorker.PrzeliczProceduryVATParams(context)
{
Zatwierdzone = true,
Zaksiegowane = false,
};
var worker = new PrzeliczProceduryVATWorker
{
Dokument = dok,
Params = p,
};
worker.PrzeliczProceduryVAT(); // sam otwiera transakcję i Commit
session.Save();
Pułapki:
LiczonaOdnie przyjmuje wartości0(RequiredException). Zawsze ustaw konkretny wariant enuma.- Zmiana
LiczonaOdna dokumencie z pozycjami wpływa na sposób przeliczenia netto↔brutto pozycji i tabeli VAT — rób to przed wprowadzeniem cen lub świadomie po przeliczeniu. PrzeliczProceduryVATWorker.PrzeliczProceduryVAT()nic nie zrobi, jeśli dokument jest w buforze albo stan nie pasuje do flagParams(Zatwierdzone/Zaksiegowane). Worker sam otwiera transakcję (Logout(true)+Commit) — nie owijaj go w dodatkową transakcję edycyjną.- Worker jest widoczny tylko, gdy definicja liczy sumy VAT i ma definicję ewidencji
(
IsVisiblePrzeliczProceduryVAT); z poziomu kodu i tak sprawdź stan dokumentu przed wywołaniem. PrzeliczProceduryVATParamsdziedziczy poContextBase— przy ręcznym tworzeniu przekażContextdo konstruktora.
W47 — Zmiana waluty dokumentu i cen
Cel: zmienić walutę dokumentu handlowego (i opcjonalnie przeliczyć ceny pozycji) — np. wystawić fakturę w EUR zamiast PLN, z kursem z wybranej tabeli kursowej.
Warianty:
| Wariant | Mechanizm |
|---|---|
| Zmiana waluty z przeliczeniem cen | parametry DokumentHandlowyZmianaWalutyWorkerParams + akcja „Zmień walutę dokumentu i cen..." |
| Zmiana waluty bez cen | te same parametry z ZmienCeny = false |
| Ręczne ustawienie waluty/kursu | dok.TabelaKursowa, dok.KursWaluty, dok.DataOgłoszeniaKursu, dok.BruttoCy |
Pola i typy: klasa parametrów (publiczna) DokumentHandlowyZmianaWalutyWorkerParams : PozycjaDokHandlowegoZmianaWalutyCenyWorkerParams (ctor (Context, [Context] DokumentHandlowy))
udostępnia: Waluta: Waluta („na walutę"), WalutaBazowa: Waluta (read-only, „z waluty"),
TabelaKursowa: TabelaKursowa, Data: Date, KursWaluty: double, ZmienCeny: bool. Pola
dokumentu: dok.TabelaKursowa: TabelaKursowa, dok.KursWaluty: double, dok.BruttoCy: Currency.
Moduł walut (jest internal jako extension): Soneta.Waluty.WalutyModule.GetInstance(session) →
.Waluty.WgSymbolu["EUR"], .TabeleKursowe.
Worker
DokumentHandlowyZmianaWalutyWorkerjestinternal— nie da się go zainstancjonować bezpośrednio z dodatku zewnętrznego. Jest jednak zarejestrowany jako akcja menu Czynności („Zmień walutę dokumentu i cen...",Shift+F11) i przyjmuje publiczne parametryDokumentHandlowyZmianaWalutyWorkerParams. Z poziomu kodu dodatku zewnętrznego dostępne tory to: (1) uruchomienie akcji przez mechanizm Czynności z przygotowanymContext, albo (2) bezpośrednie ustawienie pól waluty/kursu na dokumencie i pozycjach.
Snippet:
using Microsoft.Extensions.DependencyInjection; // jeśli korzystasz z serwisów
using Soneta.Waluty;
var dok = session.GetHandel().DokHandlowe.WgDaty[...];
// --- Tor 1: przygotowanie parametrów workera (do uruchomienia przez akcję Czynności) ---
// Worker jest internal — z dodatku przygotowujemy publiczne Params i uruchamiamy akcję
// przez mechanizm menu Czynności (Context z zaznaczonym dokumentem).
var wm = WalutyModule.GetInstance(session);
var p = new DokumentHandlowyZmianaWalutyWorkerParams(context, dok)
{
Waluta = wm.Waluty.WgSymbolu["EUR"], // waluta docelowa
TabelaKursowa = wm.TabeleKursowe.NBP,
Data = Date.Today,
ZmienCeny = true, // przelicz też ceny pozycji
};
// KursWaluty wylicza się automatycznie po ustawieniu Waluta/TabelaKursowa/Data;
// w razie potrzeby można nadpisać: p.KursWaluty = 4.30;
// --- Tor 2: ręczne ustawienie waluty i kursu na dokumencie (bez workera) ---
using (var t = session.Logout(editMode: true))
{
dok.TabelaKursowa = wm.TabeleKursowe.NBP;
dok.KursWaluty = 4.30;
// dok.BruttoCy = new Currency(..., "EUR"); // kwoty w walucie dokumentu
t.Commit();
}
session.Save();
Pułapki:
- Worker
DokumentHandlowyZmianaWalutyWorkerjestinternal— nie wywołasznew ...Worker(...)ani.ZmienWalute()z dodatku zewnętrznego. Używaj publicznychParams+ akcji Czynności lub bezpośredniej edycji pól dokumentu. session.GetWaluty()jest internal — moduł walut pobieraj przezWalutyModule.GetInstance(session)(namespaceSoneta.Waluty).- Jeśli w bazie brak kursu na żądaną datę (np. Demo nie ma kursu EUR „na dziś"), platforma rzuci
KursWalutyNotFoundException.KursWalutyw parametrach wylicza się automatycznie tylko, gdy kurs istnieje; w przeciwnym razie ustawKursWalutyręcznie. - Zmiana waluty ma sens tylko dla dokumentu w buforze (
IsVisibleZmienWalutewymagadok.Bufor); dla dokumentu zatwierdzonego operacja jest niedostępna. WalutaBazowajest read-only — wyznaczana z bieżącej waluty dokumentu (dok.BruttoCy.Symbol). Ustawiasz tylkoWaluta(docelową).- Kwoty pieniężne to
Currency(wartość + symbol), niedecimal/double. SamKursWalutyjestdouble.
9. Korekty i dokumenty specjalne
Rozdział obejmuje korekty (ilościowe, ceny, wartości przyjęcia) oraz dokumenty „specjalne": inwentaryzację (INW), fakturę zaliczkową wraz z jej rozliczeniem oraz przesunięcie międzymagazynowe (MM). Wszystkie wzorce operują wyłącznie na publicznym kontrakcie platformy. Kluczowym narzędziem jest serwis relacji IRelacjeService (namespace Soneta.Handel.RelacjeDokumentow.Api), opisany w rozdziale o relacjach — tutaj koncentrujemy się na metodzie NowaKorekta oraz na specyfice każdego typu dokumentu.
Wspólne reguły (powtórzone z fundamentów,
safe-code.md):
- Dostęp do serwisu:
var rel = session.GetRequiredService<IRelacjeService>();(wymagausing Microsoft.Extensions.DependencyInjection;).- Dokument nadrzędny / korygowany musi być zatwierdzony (
StanDokumentuHandlowego.Zatwierdzony) przed wywołaniem relacji.- Każda modyfikacja w transakcji (
session.Logout(editMode: true)+Commit()/CommitUI()w workerze), potemsession.Save(). Magazyn księguje się dopiero poSave().- Pola
DokumentKorygowany,DokumentyKorygujące,DokumentyZaliczkowesą kalkulowane (read-only) — nie ustawiaj ich ręcznie; powstają jako efekt utworzenia relacji.
W48 — Korekta ilościowa i korekta ceny
Cel: utworzyć dokument korygujący do zatwierdzonej faktury / dokumentu magazynowego (zmiana ilości, ceny, rabatu lub VAT) i zapisać poprawione wartości na pozycjach korekty.
Warianty:
| Wariant | Wywołanie | Uwaga |
|---|---|---|
| Korekta pojedynczego dokumentu | NowaKorekta(new[]{ dok }, symbolKorekty) |
zwraca tablicę korekt (zwykle 1 element) |
| Korekta zbiorcza (wiele dok. → jedna) | NowaKorektaZbiorcza(korygowane, symbolKorekty) |
grupuje korygowane dokumenty |
| Domyślny symbol korekty | NowaKorekta(new[]{ dok }) (bez symbolu) |
platforma dobiera definicję korekty wg definicji korygowanego |
| Korekta ilościowa | po utworzeniu: zmiana poz.Ilosc na pozycji korekty |
różnica ilości |
| Korekta ceny / rabatu | zmiana poz.Cena / poz.Rabat |
różnica wartości |
| Korekta „do zera" (zwrot całości) | ustaw poz.Ilosc = Quantity.Zero (w jednostce pozycji) |
pełny storno |
Pola i typy:
IRelacjeService.NowaKorekta(DokumentHandlowy[] korygowane, string symbolKorekty = null, Context context = null, HandlerSet handlers = null): DokumentHandlowy[].IRelacjeService.NowaKorektaZbiorcza(DokumentHandlowy[] korygowane, string symbolKorekty = null, …): DokumentHandlowy[].- Na pozycji korekty:
PozycjaDokHandlowego.Ilosc: Quantity,Cena: DoubleCy,Rabat: Percent,PozycjaKorygowana(powiązanie z pozycją oryginału, read-only). - Odczyt powiązań:
dok.DokumentyKorygujące(kolekcja korekt),korekta.DokumentKorygowany(oryginał).
Snippet:
using Microsoft.Extensions.DependencyInjection;
using Soneta.Handel;
using Soneta.Handel.RelacjeDokumentow.Api;
using Soneta.Types;
// 1. Oryginał musi być zatwierdzony:
using (var t = session.Logout(editMode: true)) {
faktura.Stan = StanDokumentuHandlowego.Zatwierdzony;
t.Commit();
}
session.Save();
// 2. Utworzenie korekty przez serwis relacji:
var rel = session.GetRequiredService<IRelacjeService>();
DokumentHandlowy korekta;
using (var t = session.Logout(editMode: true)) {
korekta = rel.NowaKorekta(new[] { faktura }, "KWN")[0]; // symbol definicji korekty
// 3. Korekta ilościowa: zmiana ilości na pozycji korekty
// (pozycje korekty są wstępnie zainicjowane wartościami oryginału)
var poz = korekta.Pozycje.First();
poz.Ilosc = new Quantity(8, poz.Ilosc.Symbol); // było 10 -> korygujemy do 8
// 4. Korekta ceny / rabatu — alternatywnie:
// poz.Cena = new DoubleCy(4.5m, poz.Cena.Symbol);
// poz.Rabat = new Percent(0.15);
t.Commit();
}
session.Save();
// Odczyt powiązania:
DokumentHandlowy oryginal = korekta.DokumentKorygowany;
Pułapki:
NowaKorektazwraca tablicęDokumentHandlowy[]— dla jednego dokumentu bierz[0]/.Single().- Korygowany dokument musi być zatwierdzony; korekta do dokumentu w buforze nie powstanie.
- Pozycje korekty są inicjowane wartościami oryginału — modyfikujesz je „do wartości docelowej", a system sam policzy różnicę. Nie wpisuj różnicy „z palca".
symbolKorektyto symbol definicji korekty (np. „KWN", „KS"), a nie symbol korygowanej faktury. Definicja korekty musi istnieć i być odblokowana.- Całą sekwencję (utworzenie + edycja pozycji) wykonuj w jednej transakcji, dopiero potem
Save(). - Symbol jednostki na
Iloscmusi pochodzić z istniejącej pozycji (poz.Ilosc.Symbol) — nie twórzQuantityz gołą liczbą.
W49 — Korekta wartości przyjęcia magazynowego
Cel: skorygować ilość/wartość przyjęcia magazynowego (PZ/PW) tak, aby poprawić zaksięgowane obroty i partie dostaw.
Warianty:
| Wariant | Mechanizm publiczny |
|---|---|
| Korekta przyjęcia ilościowa | IRelacjeService.NowaKorekta(new[]{ przyjecie }, …) + korekta Ilosc na pozycji |
| Korekta wartości (ceny) przyjęcia | jw., zmiana Cena na pozycji korekty |
| Korekta wskazanej dostawy / partii | korekta z odwołaniem do partii — Soneta.Magazyny.GrupaDostaw |
Pola i typy: te same co W48 — IRelacjeService.NowaKorekta(...), PozycjaDokHandlowego.Ilosc/Cena, PozycjaKorygowana.
Snippet:
var rel = session.GetRequiredService<IRelacjeService>();
DokumentHandlowy korektaPrzyjecia;
using (var t = session.Logout(editMode: true)) {
// przyjecie = zatwierdzony dokument PZ/PW
korektaPrzyjecia = rel.NowaKorekta(new[] { przyjecie })[0];
var poz = korektaPrzyjecia.Pozycje.First();
poz.Ilosc = new Quantity(9, poz.Ilosc.Symbol); // przyjęto 10, korygujemy stan do 9
t.Commit();
}
session.Save(); // tu księgują się skorygowane obroty/partie
Pułapki:
- Dedykowany worker
UtworzKorektePrzyjeciaWorkerjestinternal— nie da się go zainstancjonować z dodatku zewnętrznego. Publiczny tor toIRelacjeService.NowaKorekta(wewnętrznie worker robi dokładnie to samo:NowaKorekta+ dostosowaniePozycje[].Iloscz uwzględnieniem obrotów/storn). - Korekta przyjęcia działa na zaksięgowanych obrotach i partiach — różnicowe wyliczenia ilości względem obrotów (
MagazynyModule.Obroty) i storn wykonuje platforma. Z poziomu publicznego kontraktu ustaw docelowąIlosc/Cenana pozycji korekty. - Magazyn (zasoby/obroty) aktualizuje się dopiero po
session.Save(), nie poCommit(). - Jeśli przyjęcie wskazywało partię/dostawę, korekta musi odnosić się do tej samej dostawy — przy złożonych scenariuszach (rozchody z tej partii, przesunięcia) korektę realizuj na pełnej, zalogowanej sesji aplikacyjnej.
W50 — Dokument inwentaryzacji (INW)
Cel: utworzyć dokument spisu z natury (INW), na którym wprowadza się stany rzeczywiste; system wylicza różnice (nadwyżka / strata) względem stanu ewidencyjnego i generuje dokumenty korygujące stan.
Warianty:
| Wariant | Charakterystyka |
|---|---|
| Spis z natury | pozycje = stan rzeczywisty zliczony fizycznie |
| Stan początkowy / bilans otwarcia | INW jako dokument ustalający stany na start |
| Nadwyżka | stan rzeczywisty > ewidencyjny → relacja InwentaryzacjaNadwyżka |
| Strata / niedobór | stan rzeczywisty < ewidencyjny → relacja InwentaryzacjaStrata |
| Inwentaryzacja wg partii / wskazania dostawy | spis z dokładnością do partii (GrupaDostaw) |
Pola i typy:
- Definicja:
session.GetHandel().DefDokHandlowych.WgSymbolu["INW"]. DokumentHandlowy.Magazyn(Soneta.Magazyny.Magazyn) — inwentaryzowany magazyn (wymagany).PozycjaDokHandlowego.Ilosc: Quantity— stan rzeczywisty.- Dokumenty różnic (odczyt):
dok.Podrzędne[...]/ relacje inwentaryzacyjne; różnica wartości dostępna na dokumencie różnicy (np.Ewidencja.Wartosc).
Snippet:
var hm = session.GetHandel();
var magazyny = session.GetMagazyny();
var towary = session.GetTowary();
DokumentHandlowy inw;
using (var t = session.Logout(editMode: true)) {
inw = new DokumentHandlowy();
session.AddRow(inw);
inw.Definicja = hm.DefDokHandlowych.WgSymbolu["INW"]; // definicja PIERWSZA
inw.Magazyn = magazyny.Magazyny.WgSymbol["F"]; // inwentaryzowany magazyn
// Pozycja = stan rzeczywisty zliczony fizycznie:
var poz = new PozycjaDokHandlowego(inw);
session.AddRow(poz);
poz.Towar = towary.Towary.WgKodu["BIKINI"]; // Towar PIERWSZY (inicjuje jednostkę)
poz.Ilosc = new Quantity(9, poz.Ilosc.Symbol); // ewidencyjnie 10 -> spis 9
inw.Stan = StanDokumentuHandlowego.Zatwierdzony; // zatwierdzenie wylicza różnice
t.Commit();
}
session.Save(); // tu powstają dokumenty różnic i korekta stanu
Pułapki:
- INW wymaga wskazanego magazynu; bez niego nie da się policzyć różnic.
- Różnice (nadwyżka/strata) i ich zaksięgowanie powstają przy zatwierdzeniu + Save, nie wcześniej. Dokumenty różnic to obiekty podrzędne — czytaj je przez kolekcje relacji, nie twórz ręcznie.
- Inwentaryzacja wg partii wymaga wskazania dostawy/partii (
Soneta.Magazyny.GrupaDostaw) — bez tego spis odnosi się do stanu zbiorczego. - W bazie Demo obowiązuje blokada stanu ujemnego (
StanUjemnyVerifier) — żeby spis miał sens, towar musi mieć wcześniejsze, zapisane przyjęcie (PW/PZ). - Nie modyfikuj wartości na dokumentach różnic ręcznie — to wynik wyliczeń platformy.
W51 — Faktura zaliczkowa i jej rozliczenie dokumentem końcowym
Cel: wystawić fakturę zaliczkową (FZAL) na poczet przyszłej dostawy, a następnie rozliczyć ją dokumentem końcowym (FV), tak by wartość końcowej została pomniejszona o wpłaconą zaliczkę.
Warianty:
| Wariant | Mechanizm |
|---|---|
| Utworzenie zaliczkowej z zamówienia | NowyPodrzednyIndywidualny(new[]{ zamowienie }, "FZAL") |
| Rozliczenie zaliczki na dokumencie końcowym | NowyPodrzednyIndywidualny(new[]{ zaliczkowa }, "FV", handlers: …) |
| Przenoszenie zaliczki na pozycje | callback WybierzDokumentyZaliczkoweCallback + DokumentHandlowyRealizacjaZaliczkiWorker |
| Przenoszenie zaliczki wg stawki VAT | callback WybierzZaliczkiWgStawkiVatCallback |
| Wiele zaliczek do jednej końcowej | dodaj wszystkie w callbacku (Wybrany = true dla każdej) |
Pola i typy:
IRelacjeService.NowyPodrzednyIndywidualny(DokumentHandlowy[] nadrzedne, string symbolPodrzednego, Context context = null, HandlerSet handlers = null): DokumentHandlowy[].HandlerSet.WybierzDokumentyZaliczkoweCallback: Action<DokumentDocelowy>— wskazanie zaliczek (tor „na pozycje").HandlerSet.WybierzZaliczkiWgStawkiVatCallback: Action<DokumentDocelowy>— tor „wg stawki VAT".- Worker publiczny do wskazania zaliczki:
DokumentHandlowyRealizacjaZaliczkiWorkerz property[Context] Dokument: DokumentHandlowy,[Context] Docelowy: DokumentDocelowy,Wybrany: bool. - Odczyt:
dok.DokumentyZaliczkowe(kalkulowane) — zaliczki powiązane z końcowym;dok.SumyVAT: SubTable<SumaVAT>;dok.BruttoCy.
Snippet:
using Microsoft.Extensions.DependencyInjection;
using Soneta.Handel;
using Soneta.Handel.RelacjeDokumentow.Api;
var rel = session.GetRequiredService<IRelacjeService>();
// zaliczkowa = zatwierdzona faktura zaliczkowa (FZAL).
// Rozliczamy ją dokumentem końcowym FV — callback wskazuje, które zaliczki przenieść:
DokumentHandlowy[] koncowy;
using (var t = session.Logout(editMode: true)) {
koncowy = rel.NowyPodrzednyIndywidualny(
new[] { zaliczkowa },
"FV",
handlers: new HandlerSet {
WybierzDokumentyZaliczkoweCallback = WybierzZaliczki
});
t.Commit();
}
session.Save();
// koncowy[0].BruttoCy == 0, jeśli zaliczka pokryła całość
// Callback: zaznacza wszystkie dokumenty zaliczkowe powiązane z dokumentem docelowym.
static void WybierzZaliczki(DokumentDocelowy target) {
var w = new DokumentHandlowyRealizacjaZaliczkiWorker { Docelowy = target };
foreach (var d in target.DokumentyZaliczkowe.Cast<DokumentHandlowy>()) {
w.Dokument = d;
w.Wybrany = true; // przenosi zaliczkę na dokument końcowy
}
}
Pułapki:
- Bez dostarczenia odpowiedniego callbacka (
WybierzDokumentyZaliczkoweCallback/WybierzZaliczkiWgStawkiVatCallback) domyślne handlery rzucająNotImplementedException— musisz wskazać tryb przenoszenia zaliczki zgodny z konfiguracją definicji końcowej (SposobPrzenoszeniaZaliczki:NaPozycjevsNaDokument). - Tryb przenoszenia (na pozycje / wg stawki VAT) jest cechą definicji dokumentu końcowego — użyj callbacka pasującego do konfiguracji, inaczej rozliczenie nie zadziała.
- Worker rozliczenia (
RealizacjaZaliczkiWorker, edytor kwot wg stawki) jestinternal— z dodatku używaj publicznegoDokumentHandlowyRealizacjaZaliczkiWorker(wskazanie dokumentów) wewnątrz callbacka. - Faktura zaliczkowa musi być zatwierdzona przed rozliczeniem;
DokumentyZaliczkoweto pole kalkulowane — nie ustawiasz go, czytasz. - Tabela VAT dokumentu zaliczkowego jest przeliczana proporcjonalnie do wpłaconej zaliczki (logika
DokumentZaliczkowyWorker) — nie modyfikujSumyVATręcznie.
W52 — Przesunięcie międzymagazynowe (MM)
Cel: przesunąć zasób z jednego magazynu do drugiego dokumentem MM — rozchód z magazynu źródłowego i przychód do magazynu docelowego w jednej operacji.
Warianty:
| Wariant | Mechanizm |
|---|---|
| Przesunięcie w obrębie firmy | MM z MagazynZ (źródło) i MagazynDo (cel) |
| Wskazanie partii / dostawy przy rozchodzie | pozycja z odwołaniem do GrupaDostaw |
| Korekta przesunięcia | IRelacjeService.NowaKorekta(new[]{ mm }, …) |
Pola i typy:
- Definicja:
session.GetHandel().DefDokHandlowych.WgSymbolu["MM"]. DokumentHandlowy.MagazynZ: Soneta.Magazyny.Magazyn— magazyn źródłowy (rozchód).DokumentHandlowy.MagazynDo: Soneta.Magazyny.Magazyn— magazyn docelowy (kalkulowane: ustawia magazyn na podrzędnym dokumencie przesunięciaPodrzędne[TypRelacjiHandlowej.PrzesunięcieDo]; wymaga, by dokument przesunięcia już istniał — ustawiaj poDefinicja).PozycjaDokHandlowego.Towar,Ilosc: Quantity.
Snippet:
var hm = session.GetHandel();
var magazyny = session.GetMagazyny();
var towary = session.GetTowary();
DokumentHandlowy mm;
using (var t = session.Logout(editMode: true)) {
mm = new DokumentHandlowy();
session.AddRow(mm);
mm.Definicja = hm.DefDokHandlowych.WgSymbolu["MM"]; // definicja PIERWSZA
mm.MagazynZ = magazyny.Magazyny.WgSymbol["F"]; // magazyn źródłowy
mm.MagazynDo = magazyny.Magazyny.WgNazwa["Magazyn 2"]; // magazyn docelowy (po ustawieniu definicji)
var poz = new PozycjaDokHandlowego(mm);
session.AddRow(poz);
poz.Towar = towary.Towary.WgKodu["BIKINI"]; // Towar PIERWSZY
poz.Ilosc = new Quantity(5, poz.Ilosc.Symbol);
mm.Stan = StanDokumentuHandlowego.Zatwierdzony;
t.Commit();
}
session.Save(); // tu księguje się rozchód ze źródła i przychód do celu
Pułapki:
MagazynDojest polem kalkulowanym delegującym do podrzędnego dokumentu przesunięcia — ustaw je poDefinicja(a najlepiej przed dodaniem pozycji), boIsReadOnlyMagazynDo()blokuje zmianę magazynu, gdy istnieją już pozycje.MagazynZiMagazynDomuszą być różne i oba dostępne (prawa do magazynów / przypisanie definicji do magazynu wg konfiguracjiOgólne.PrzypisanieDefinicjiDoMagazynu).- Rozchód MM podlega blokadzie stanu ujemnego (Demo:
StanUjemnyVerifier) — magazyn źródłowy musi mieć zapisany zasób przesuwanego towaru. - Obroty (rozchód + przychód) księgują się po
session.Save(), nie poCommit(). - Korektę przesunięcia wykonuj przez
IRelacjeService.NowaKorekta(jak w W48/W49); ręczna korekta partii przy MM jest złożona i wymaga pełnej sesji aplikacyjnej.
10. Operacje zbiorcze (batch)
Operacje na zbiorze dokumentów (ewidencjonowanie do księgowości, hurtowe zatwierdzanie,
generowanie dokumentów podrzędnych) wykonujemy efektywnie i bezpiecznie: filtr serwerowy
zamiast pełnego skanu tabeli, krótkie transakcje (paczki), świadoma obsługa blokady
optymistycznej w Save(). Tabela DokHandlowe jest operacyjna (guided) — pełny skan bez
zakresu czasowego jest zabroniony (safe-code.md §6.3). Duże pętle dziel na paczki, by nie
trzymać długiej transakcji edycyjnej (§13.1).
W53 — Ewidencjonowanie / eksport do księgowości wielu dokumentów
Cel: zbiorczo zaewidencjonować (zaksięgować do ewidencji księgowej) wiele dokumentów
handlowych z danego okresu — np. raport fiskalny zbiorczy z paragonów lub korekt paragonów.
Realizuje to publiczny worker EwidencjonowanieZbiorczeWorker, który sam grupuje dokumenty
(po drukarce / oddziale / rodzaju podmiotu) i tworzy zbiorcze dokumenty ewidencji DokEwidencji.
Warianty:
| Wariant | Ustawienie Params |
|---|---|
| Raport fiskalny z paragonów | RaportDla = RaportDla.Paragonów |
| Raport dla korekt paragonów | RaportDla = RaportDla.KorektParagonów |
| Zawężenie do jednej drukarki | SymbolKasy = "D1" (puste = wszystkie z niepustym symbolem kasy) |
| Wskazanie definicji ewidencji | Definicja (typ SprzedażZbiorczaEwidencja) — gdy chcemy inną niż domyślna |
| Filtr po dacie wystawienia | ZaOkres: FromTo |
| Filtr po dacie dostawy / zaliczki | OkresDostawyZaliczki: FromTo |
| Wielooddziałowość | Oddzial: OddzialFirmy (gdy włączona w konfiguracji) |
Pola i typy:
- Worker:
Soneta.Handel.EwidencjonowanieZbiorczeWorker(public), metoda publicznavoid Ewidencjonuj(), property[Context] Params Param. EwidencjonowanieZbiorczeWorker.Params(Context cx)— konstruktor zContext. Pola:ZaOkres: FromTo,OkresDostawyZaliczki: FromTo,RaportDla: RaportDla,SymbolKasy: string,Definicja: Soneta.Core.DefinicjaDokumentu,Oddzial: OddzialFirmy.EwidencjonowanieZbiorczeWorker.RaportDla(enum):Paragonów,KorektParagonów.- Worker przetwarza tylko dokumenty w stanie
Zatwierdzony/Zablokowany; pomija już zaewidencjonowane (EwidencjaZbiorcza != null).
Snippet:
// Worker SAM otwiera transakcję edycyjną i robi CommitUI() w środku — NIE owijaj go
// w session.Logout(true). Wystarczy go skonfigurować, wywołać i zapisać.
var worker = new EwidencjonowanieZbiorczeWorker
{
Param = new EwidencjonowanieZbiorczeWorker.Params(context)
{
RaportDla = EwidencjonowanieZbiorczeWorker.RaportDla.Paragonów,
ZaOkres = new FromTo(new Date(2026, 6, 1), new Date(2026, 6, 30)), // data wystawienia
OkresDostawyZaliczki = FromTo.All, // bez filtra dostawy
SymbolKasy = "D1", // jedna drukarka
Definicja = CoreModule.GetInstance(session).DefDokumentow.WgSymbolu["SPZE"],
}
};
worker.Ewidencjonuj(); // tworzy zbiorcze DokEwidencji w transakcji wewnętrznej (CommitUI)
session.Save(); // dopiero teraz zapis do bazy — tu wykrywane konflikty optymistyczne
Pułapki:
Ewidencjonuj()samodzielnie otwieraSession.Logout(true)i kończyCommitUI(). Nie wywołuj go we własnej transakcji edycyjnej (zagnieżdżenie/podwójny commit). Po nim wykonajsession.Save()(w testachSaveDispose()).Paramustaw przedEwidencjonuj()— jest to property[Context]; bez niej worker rzuciNullReferenceException.DateiFromToto typy biznesowe — używajDate/Date.Today, nieDateTime(safe-code.md§10).FromTo.All= bez ograniczenia,FromTo.Emptyworker zamienia naAll.Definicjato rekord konfiguracyjny — pobierz istniejący (DefDokumentow.WgTypu[...]/WgSymbolu[...]), nie twórz „w locie". GdyDefinicja == null, worker użyje domyślnej.- Worker działa na danych z
ZaOkres(data wystawienia) — zawsze podaj zakres, nie zostawiaj pełnego skanu całej historii. - Konflikt edycji (ktoś zapisał ten sam dokument) wybuchnie w
session.Save()jakoRowConflictException— obsłuż go (refresh + retry lub eskalacja), nie połykaj (§4).
W54 — Hurtowe zatwierdzanie / generowanie dokumentów dla zaznaczonego zbioru
Cel: wykonać operację cyklu życia (zatwierdzenie, cofnięcie do bufora, anulowanie) na
wielu dokumentach naraz, albo wygenerować dla zaznaczonego zbioru dokumenty podrzędne
(np. wiele zamówień → faktury, wiele faktur → jeden zbiorczy WZ) za pomocą IRelacjeService,
który przyjmuje tablicę dokumentów.
Warianty:
| Wariant | Mechanizm |
|---|---|
| Hurtowe zatwierdzanie | pętla po zbiorze, dok.Stan = StanDokumentuHandlowego.Zatwierdzony, jedna (krótka) transakcja |
| Hurtowe cofnięcie do bufora / anulowanie | dok.Stan = StanDokumentuHandlowego.Bufor / .Anulowany |
| Indywidualne generowanie podrzędnych | IRelacjeService.NowyPodrzednyIndywidualny(DokumentHandlowy[], symbol) — N nadrzędnych → N podrzędnych |
| Zbiorcze generowanie podrzędnego | IRelacjeService.NowyPodrzednyZbiorczy(DokumentHandlowy[], symbol) — wiele FA → 1 WZ |
| Zbiorcza korekta | IRelacjeService.NowaKorektaZbiorcza(DokumentHandlowy[]) |
| Dołączenie nadrzędnego / podrzędnego | DolaczNadrzedny, DolaczPodrzednyIndywidualny |
Pola i typy:
dok.Stan: Soneta.Handel.StanDokumentuHandlowego(Bufor=0,Zatwierdzony=1,Zablokowany=2,Anulowany=3). Skróty read-only:dok.Bufor,dok.Zatwierdzony,dok.Anulowany.IRelacjeService(namespaceSoneta.Handel.RelacjeDokumentow.Api): metody przyjmująDokumentHandlowy[]i zwracająDokumentHandlowy[]. Dokumenty nadrzędne muszą być zatwierdzone. Dostęp:session.GetRequiredService<IRelacjeService>()(using Microsoft.Extensions.DependencyInjection;).
Snippet:
var hm = session.GetHandel();
var fv = hm.DefDokHandlowych.WgSymbolu["FV"];
var od = new Date(2026, 6, 1);
// (1) Hurtowe zatwierdzanie zamówień z czerwca — filtr SERWEROWY + krótka transakcja
using (var t = session.Logout(editMode: true))
{
foreach (DokumentHandlowy d in hm.DokHandlowe[(DokumentHandlowy d) =>
d.Data >= od && d.Definicja == fv && d.Stan == StanDokumentuHandlowego.Bufor])
{
d.Stan = StanDokumentuHandlowego.Zatwierdzony; // pętla po Stan na zaznaczonym zbiorze
}
t.Commit(); // CommitUI() w workerze/extenderze
}
session.Save();
// (2) Wygenerowanie faktur dla zaznaczonych (zatwierdzonych) zamówień — IRelacjeService na tablicy
var rel = session.GetRequiredService<IRelacjeService>();
DokumentHandlowy[] zamowienia = /* zaznaczone, zatwierdzone ZO */;
using (var t = session.Logout(editMode: true))
{
DokumentHandlowy[] faktury = rel.NowyPodrzednyIndywidualny(zamowienia, "FV");
t.Commit();
}
session.Save();
Pułapki:
IRelacjeServicewymaga, by dokumenty nadrzędne były zatwierdzone — najpierw zatwierdź (wariant 1), potem generuj podrzędne.- Operacje masowe wykonuj w jednej transakcji tylko gdy zbiór jest mały; dla dużych dziel na paczki (W55) — długa transakcja blokuje innych i zwiększa ryzyko konfliktu (§13.1).
- Zmiana
Stanmusi być w transakcji (session.Logout(true)); w workerze/extenderzet.CommitUI()zamiastt.Commit(). - Nie iteruj całej tabeli
DokHandlowezifw pamięci — filtr serwerowy z zakresem czasowym (§6.1, §6.3). Zaznaczony w UI zbiór masz wcontextjakoDokumentHandlowy[]. Save()po operacji relacji może rzucićRowConflictException(optimistic lock) — obsłuż (§4).
W55 — Wydajne przetwarzanie wielu dokumentów w jednej sesji (paczki)
Cel: przetworzyć duży zbiór dokumentów (tysiące) w jednej sesji bez blokowania innych
użytkowników i bez ryzyka, że pojedynczy konflikt unieważni całą operację — przez podział na
paczki (krótkie transakcje, okresowy Save()).
Warianty:
| Wariant | Technika |
|---|---|
| Filtr serwerowy z zakresem czasowym | hm.DokHandlowe[(DokumentHandlowy d) => d.Data >= od && d.Data <= doD && …] |
| Paczki o stałym rozmiarze | licznik w pętli + Commit() / Save() co N rekordów |
| Izolacja konfliktu paczki | try/catch (RowConflictException) wokół Save() paczki, retry/log paczki |
| Tylko odczyt (raport) | login.CreateSession(readOnly: true, …) — bez transakcji edycyjnej |
Pola i typy: Soneta.Types.Date (zakres), StanDokumentuHandlowego, RowConflictException
(session.Save()), IDisposable na sesji i transakcji.
Snippet:
const int rozmiarPaczki = 200; // przetwarzaj po 200 dokumentów na transakcję
var hm = session.GetHandel();
var od = new Date(2026, 1, 1);
var doD = Date.Today;
// Materializujemy KLUCZE/ID po stronie serwera (filtr), nie całe rekordy w pamięci wszystkie naraz.
// Iterujemy serwerowy zbiór i commitujemy paczkami — krótka transakcja na każdą paczkę.
int licznik = 0;
ITransaction t = session.Logout(editMode: true);
try
{
foreach (DokumentHandlowy d in hm.DokHandlowe[(DokumentHandlowy d) =>
d.Data >= od && d.Data <= doD && d.Stan == StanDokumentuHandlowego.Bufor])
{
d.Stan = StanDokumentuHandlowego.Zatwierdzony;
if (++licznik % rozmiarPaczki == 0)
{
t.Commit();
t.Dispose();
session.Save(); // zamknięcie paczki — krótka transakcja
t = session.Logout(editMode: true);
}
}
t.Commit();
}
finally
{
t.Dispose();
}
session.Save(); // ostatnia (niepełna) paczka
Pułapki:
- Krótka transakcja to bezpieczeństwo, nie tylko wydajność — operacja > ~30 s powinna iść paczkami (§13.1). Jedna gigantyczna transakcja blokuje innych i zwiększa szansę konfliktu.
- Filtruj serwerowo (
SubTable[condition]), z zakresem czasowym dla tabeli operacyjnej guided (DokHandlowe) — nigdy pełny skan (§6.1, §6.3). Nie używaj.ToList().Where(...)(§13.2). - Po
session.Save()w środku pętli okno edycji jest zamknięte — kolejną edycję otwórz nową transakcją (session.Logout(true)), inaczejAccessWriteDenied. (W testach wzorzec toSave()→SaveDispose()→ odczyt na świeżej sesji poGuid.) - Obsłuż
RowConflictExceptionper paczka (refresh + retry lub log i kontynuacja), nie łapExceptionogólnie (§4, §9.1). Połknięty wyjątek zSave()= utrata danych. - Nie współdziel
Session/Rowmiędzy wątkami — równoległe przetwarzanie wymaga osobnej sesji na wątek (§3.1). - Sesja zawsze w
using/try-finallyzDispose()(§1.1); transakcja bezCommit()= automatyczny rollback.
Powiązane: rozdz. 5 (cykl życia /
Stan), rozdz. 8 (relacje,IRelacjeService),safe-code.md§4 (optimistic lock), §6 (filtr serwerowy), §13 (paczki),rowcondition.md(serwerowy LINQ).
11. Operacje pomocnicze (przekrojowe)
Rozdział zbiera wzorce „okołodokumentowe": bezpieczne pozyskanie kontrahenta i towaru do pozycji,
przeliczanie jednostek, walidację przed zatwierdzeniem, obsługę błędów i blokady optymistycznej,
odczyt metadanych (ChangeInfos) oraz pracę z definicjami i numeracją dokumentu. Fundamenty (sesja,
transakcja, Save, blokada optymistyczna) opisuje safe-code.md i
session-login.md — tutaj się do nich odwołujemy.
Cały kod jest zgodny z C# 10 (target-typed
new,var, file-scoped namespace, wyrażeniaswitch, nazwane parametrybool) i operuje wyłącznie na publicznym kontrakcie platformy.
W56 — Bezpieczne pobranie / utworzenie kontrahenta i towaru pozycji
Cel: przed dodaniem pozycji lub ustawieniem nabywcy bezpiecznie zlokalizować istniejący rekord
(kontrahent, towar), a gdy go brak — świadomie utworzyć nowy albo użyć kontrahenta jednorazowego
(systemowego rekordu „incydentalnego"). Chroni przed NullReferenceException w trakcie transakcji.
Warianty:
| Wariant | Mechanizm | Uwaga |
|---|---|---|
| Kontrahent po kodzie | crm.Kontrahenci.WgKodu["Abc"] |
klucz unikalny, może być null |
| Kontrahent po NIP (dedup) | crm.Kontrahenci.WgNIP[(Kontrahent k)=>k.NIP==nip] |
filtr serwerowy, normalizuj Nip.Flat |
| Kontrahent jednorazowy / incydentalny | Kontrahent.INCYDENTALNY (stała Guid), k.JestIncydentalny |
rekord systemowy — dane nabywcy zapisz na dokumencie |
| Utworzenie nowego kontrahenta | new Kontrahent() + AddRow |
patrz W3 w kontrahent.md |
| Towar po kodzie | tm.Towary.WgKodu["BIKINI"] |
klucz unikalny, może być null |
| Brak towaru | przerwij operację (BusException) |
nie twórz towaru „w locie" w trakcie wystawiania |
Pola i typy: crm.Kontrahenci.WgKodu: GuidedTable (indeks po Kod), Kontrahent.JestIncydentalny: bool (kalkulowane), Kontrahent.INCYDENTALNY: System.Guid (stała), tm.Towary.WgKodu (indeks po
Kod), dok.Kontrahent: Kontrahent. Dostęp do kontrahenta incydentalnego po Guid:
crm.Kontrahenci[Kontrahent.INCYDENTALNY] (indeksator GuidedTable po Guid).
Snippet:
var crm = session.GetCRM();
var tm = session.GetTowary();
// 1. Kontrahent po kodzie — może nie istnieć
Kontrahent kontrahent = crm.Kontrahenci.WgKodu["Abc"];
// 2. Gdy brak po kodzie — dedup po NIP, zanim ewentualnie utworzymy nowego
if (kontrahent == null && !string.IsNullOrEmpty(nip))
{
var flat = Nip.Flat(nip); // normalizacja przed porównaniem
kontrahent = crm.Kontrahenci.WgNIP[(Kontrahent k) => k.NIP == flat].FirstOrDefault();
}
// 3. Sprzedaż jednorazowa (klient detaliczny bez kartoteki) — kontrahent incydentalny
if (kontrahent == null)
kontrahent = crm.Kontrahenci[Kontrahent.INCYDENTALNY]; // systemowy rekord „incydentalny"
// 4. Towar pozycji — gdy brak, przerywamy świadomie (nie wystawiamy „pustej" pozycji)
Towar towar = tm.Towary.WgKodu["BIKINI"];
if (towar == null)
throw new BusException("Brak towaru o kodzie BIKINI.".Translate());
using (var t = session.Logout(editMode: true))
{
dok.Kontrahent = kontrahent; // gdy definicja wymaga nabywcy
t.Commit(); // CommitUI() w workerze/extenderze
}
session.Save();
Pułapki:
WgKodu[...]zwraca jeden rekord lubnull(klucz unikalny).WgNIP[condition]/WgNazwy[...]zwracają zbiór — użyj.FirstOrDefault(). Nie iteruj całej tabeliKontrahenci/Towaryw pamięci — to kartoteki; filtruj serwerowo (SubTable[condition],safe-code.md§6).- Kontrahenta incydentalnego nie wolno ustawić na każdym typie dokumentu — na fakturze sprzedaży
(np.
FV) przypisaniedok.Kontrahent = crm.Kontrahenci[Kontrahent.INCYDENTALNY]rzucaArgumentException(„Nie można ustawiać kontrahenta incydentalnego w dokumentach typu 'FV'"). Rekord incydentalny jest przeznaczony do sprzedaży detalicznej (np. paragon) — na fakturze podaj realnego nabywcę. - Kontrahenta jednorazowego pobieraj jako rekord incydentalny (
Kontrahent.INCYDENTALNY) — nie twórz za każdym razem nowego rekordu w kartotece. Rekordu incydentalnego nie modyfikuj (JestIncydentalny == true); dane konkretnego nabywcy (nazwa, NIP, adres) zapisz na samym dokumencie / w jego polach adresowych, nie na rekordzie kontrahenta. - Nie twórz towaru „w locie" przy wystawianiu dokumentu — brak towaru to błąd danych, nie sytuacja do cichego uzupełnienia. Towar musi mieć ustawioną jednostkę (W57).
- W
RowConditionużywaj tylko pól bazodanowych.JestIncydentalny,NazwaFormatowanaitp. są kalkulowane → w wyrażeniu LINQ rzucąLinqConditionException.
W57 — Przeliczanie jednostek miary towaru przy dodawaniu pozycji
Cel: dodać pozycję w jednostce pomocniczej (np. opakowanie zbiorcze, „km", „kg") i poprawnie przeliczyć ją na jednostkę podstawową towaru, korzystając z przeliczników zdefiniowanych dla towaru.
Warianty:
| Wariant | Mechanizm | Uwaga |
|---|---|---|
| Pozycja w jednostce podstawowej | poz.Ilosc = new Quantity(n, poz.Ilosc.Symbol) |
symbol z pozycji po ustawieniu Towar |
| Pozycja w jednostce pomocniczej | new Quantity(n, "OPAK") |
symbol jednostki pomocniczej |
| Jawne przeliczenie ilości | towar.PrzeliczJednostkę(jednostka, qty, throwError) |
zwraca Quantity w jednostce docelowej |
| Jednostka podstawowa towaru | towar.Jednostka: Jednostka |
jednostka, w której prowadzony jest magazyn |
| Jednostka uzupełniająca (Intrastat/CN) | towar.JednostkaUzupelniajaca: Jednostka |
wymaga zdefiniowanego przelicznika |
| Brak przelicznika | throwError: true → wyjątek |
brak przelicznika = niejednoznaczne przeliczenie |
Pola i typy: Towar.Jednostka: Soneta.Handel.Jednostka, Towar.JednostkaUzupelniajaca: Jednostka, Towar.PrzeliczJednostkę(Jednostka jednostka, Quantity qty, bool throwError): Quantity,
tm.Jednostki (tabela jednostek, indeks WgKodu). Quantity (Soneta.Types) = wartość + symbol
jednostki; poz.Ilosc.Symbol po ustawieniu poz.Towar przyjmuje symbol jednostki podstawowej.
Snippet:
var tm = session.GetTowary();
var towar = tm.Towary.WgKodu["TRANSPORT"]; // towar prowadzony np. w „km"
using (var t = session.Logout(editMode: true))
{
var poz = new PozycjaDokHandlowego(dok); // ctor wymaga dokumentu
session.AddRow(poz);
poz.Towar = towar; // USTAW PIERWSZY — inicjuje jednostkę na Ilosc/Cena
// Wariant A: ilość w jednostce podstawowej towaru (symbol z pozycji)
poz.Ilosc = new Quantity(10, poz.Ilosc.Symbol);
// Wariant B: ilość podana w jednostce pomocniczej i przeliczona na podstawową
var jednPom = tm.Jednostki.WgKodu["OPAK"]; // jednostka pomocnicza
var iloscPom = new Quantity(3, jednPom.Kod);
// throwError: true — brak przelicznika OPAK→podstawowa zgłosi wyjątek zamiast cichego błędu
Quantity iloscPodstawowa = towar.PrzeliczJednostkę(towar.Jednostka, iloscPom, throwError: true);
poz.Ilosc = iloscPodstawowa;
t.Commit();
}
session.Save();
Pułapki:
poz.Towarustaw przedIlosc/Cena— to on inicjuje symbol jednostki na pozycji. Konstrukcjanew Quantity(n, poz.Ilosc.Symbol)gwarantuje zgodny symbol; podanie surowego symbolu spoza jednostek towaru daje przeliczenie tylko przy istniejącym przeliczniku.PrzeliczJednostkę(..., throwError: true)rzuci wyjątek, gdy brak przelicznika między jednostkami — to świadomy wybór: lepszy twardy błąd niż cicha, niepoprawna ilość. Dlafalsezwraca ilość bez przeliczenia (ryzykowne).Quantityto typ wartość+symbol (niedouble). Nie mieszajQuantityo różnych symbolach w arytmetyce — najpierw sprowadź do jednej jednostki przezPrzeliczJednostkę.JednostkaUzupelniajaca(CN/Intrastat) wymaga przelicznika z jednostki podstawowej; jego brak zgłaszany jest przy wyliczeniach Intrastat — zdefiniuj przelicznik na towarze.- Przeliczniki to dane konfiguracyjne towaru — nie twórz ich „w locie" w trakcie wystawiania dokumentu; brak przelicznika to sygnał błędu konfiguracji, nie do obejścia w kodzie pozycji.
W58 — Walidacja przed zatwierdzeniem (kompletność, zasób, limit kredytowy)
Cel: przed zmianą stanu na Zatwierdzony sprawdzić kompletność danych (kontrahent, pozycje),
dostępność zasobu magazynowego oraz przygotować się na automatyczną kontrolę limitu kredytowego
nabywcy. Pozwala zgłosić czytelny błąd zamiast łapać wyjątek głęboko w Save().
Warianty:
| Wariant | Sprawdzenie (publiczny kontrakt) | Egzekwowanie |
|---|---|---|
| Kompletność danych | dok.Kontrahent != null, !dok.Pozycje.IsEmpty |
własna walidacja przed Stan |
| Dostępność zasobu (stan ujemny) | przyjęcie (PW/PZ) zapisane przed rozchodem | weryfikator Demo StanUjemnyVerifier — wyjątek w Save() |
| Limit kredytowy nabywcy | dok.Kontrahent.LimitKredytu, KontrolaAktywna, TypLimituKredytowego |
platforma kontroluje automatycznie przy zatwierdzeniu |
| Termin / forma płatności | dok.Platnosci (W z sekcji N) |
wynika z definicji i kontrahenta |
Pola i typy: dok.Pozycje: SubTable<PozycjaDokHandlowego> (.IsEmpty: bool), dok.Kontrahent: Kontrahent, dok.Stan: StanDokumentuHandlowego. Po stronie kontrahenta (odczyt):
Kontrahent.LimitKredytu: Currency, Kontrahent.TypLimituKredytowego, Kontrahent.KontrolaAktywna: bool (kalkulowane) — patrz W9 w kontrahent.md.
Snippet:
// Walidacja PRZED próbą zmiany stanu — czytelny błąd zamiast wyjątku z głębi Save()
if (dok.Kontrahent == null)
throw new RowException(dok, "Dokument nie ma nabywcy.".Translate());
if (dok.Pozycje.IsEmpty)
throw new RowException(dok, "Dokument nie ma pozycji.".Translate());
// Informacyjnie: czy nabywca ma aktywną kontrolę kredytową (odczyt pól kalkulowanych)
if (dok.Kontrahent.KontrolaAktywna)
{
// limit jest egzekwowany automatycznie przy zatwierdzeniu — patrz pułapki
}
using (var t = session.Logout(editMode: true))
{
dok.Stan = StanDokumentuHandlowego.Zatwierdzony; // tu uruchamia się kontrola limitu/zasobu
t.Commit();
}
session.Save(); // brak zasobu (StanUjemnyVerifier) / przekroczony limit → wyjątek właśnie tutaj
Pułapki:
- Kontrola limitu kredytowego jest wewnętrzna i automatyczna — uruchamia się przy zatwierdzaniu
dokumentu rozchodowego, gdy definicja ma ustawione „zachowanie po przekroczeniu limitu". Z dodatku
zewnętrznego nie wywołujesz jej ręcznie (logika
LimitKredytowyDokumentujestinternal) — czytasz pola kontrahenta (LimitKredytu,KontrolaAktywna) i obsługujeszInvalidOperationExceptionzgłaszany przez platformę przy zatwierdzaniu. - W bazie Demo
StanUjemnyVerifierblokuje rozchód bez wcześniejszego zapisanego przyjęcia. SamoCommitUInie księguje zasobów — magazyn księguje się dopiero poSession.Save(), więc błąd pojawia się wSave(), nie w transakcji. IsEmptyna kolekcjiSubTableto właściwość (serwerowyexists, bez nawiasów) — nie materializujPozycje.ToList().Count.- Walidację własną rzucaj jako
RowException(dok, "…".Translate())przedCommit(). Wyjątek poCommit()nie wycofa zmiany z sesji (safe-code §5.1).
W59 — Obsługa błędów i blokada optymistyczna (kolizje Save, ponowienie)
Cel: poprawnie obsłużyć wyjątki zgłaszane przez Session.Save() — w szczególności konflikt
optymistyczny (ktoś inny zapisał ten sam rekord) — zamiast je „połykać"; w razie konfliktu odświeżyć
dane i ponowić operację.
Warianty:
| Wariant | Wyjątek | Reakcja |
|---|---|---|
| Konflikt optymistyczny | RowConflictException |
świeża sesja → ponów operację (retry) |
| Naruszenie integralności / unikalności | RowException (z InnerException) |
komunikat dla użytkownika, bez retry |
| Walidacja biznesowa | RowException / BusException |
zgłoś użytkownikowi, popraw dane |
| Brak praw / okno edycji zamknięte | AccessWriteDenied |
edytuj na świeżej, zalogowanej sesji |
Pola i typy: Session.Save(), Session.Logout(editMode: true), wyjątki z Soneta.Business
(RowConflictException, RowException, BusException, AccessWriteDenied). Po Save() w środku
operacji okno edycji bywa zamknięte — kolejna edycja na tej samej sesji rzuci AccessWriteDenied.
Snippet:
// Ponowienie przy konflikcie optymistycznym (retry na świeżych danych)
const int maxProb = 3;
for (int proba = 1; ; proba++)
{
var dok = session.GetHandel().DokHandlowe[guidDokumentu]; // świeży odczyt po Guid
try
{
using (var t = session.Logout(editMode: true))
{
dok.Stan = StanDokumentuHandlowego.Zatwierdzony;
t.Commit();
}
session.Save();
break; // sukces
}
catch (RowConflictException) when (proba < maxProb)
{
// ktoś zapisał rekord równolegle — odśwież i spróbuj ponownie
session = session.Login.CreateSession(readOnly: false, config: false, name: "Retry");
}
catch (RowException ex)
{
// naruszenie integralności / unikalności / walidacja — bez retry
throw new BusException($"Nie udało się zapisać dokumentu: {ex.Message}".Translate(), ex);
}
}
Pułapki:
- Konflikt optymistyczny ujawnia się dopiero w
Save()(nie wCommit). Nie połykajRowConflictException— albo ponów na świeżych danych, albo eskaluj (safe-code §4). - Retry rób na świeżym odczycie rekordu (po
Guid) w nowej/odświeżonej sesji — ponowne zapisanie tej samej, „starej" instancji odtworzy konflikt. - Po
Save()wewnątrz dłuższej operacji okno edycji jest zamknięte → następna edycja na tej samej sesji rzuciAccessWriteDenied. Wzorzec: zapis → świeża sesja → odczyt poGuid→ kolejna edycja. - Nie używaj
catch (Exception)bez ponownego rzutu — zgubisz informację o przyczynie. Ogranicz retry liczbą prób, by nie zapętlić przy trwałym konflikcie.
W60 — Odczyt metadanych dokumentu (ChangeInfos — kto/kiedy założył i zmienił)
Cel: odczytać informacje audytowe rekordu dokumentu: kto i kiedy go założył oraz kto ostatnio go
zmodyfikował. Dane pochodzą z tabeli ChangeInfos i są dostępne przez kalkulowane właściwości
GuidedRow (dokument jest GuidedRow).
Warianty:
| Wariant | Właściwość (kalkulowana) | Zawartość |
|---|---|---|
| Kto/kiedy założył | dok.FirstChangeInfo: ChangeInfo |
operator i czas utworzenia |
| Kto/kiedy ostatnio zmienił | dok.LastChangeInfo: ChangeInfo |
operator i czas ostatniej zmiany |
| Pełna historia zmian | session.GetBusiness().ChangeInfos[dok] |
kolekcja wpisów (SubTable) |
| Wyłączenie zapisu historii dla rekordu | dok.SetChangeInfo(false) |
wyłącza rejestrację ChangeInfo dla tego wiersza |
Pola i typy: GuidedRow.FirstChangeInfo: Soneta.Business.ChangeInfo (Caption „Założył"),
GuidedRow.LastChangeInfo: ChangeInfo (Caption „Ostatnia zmiana"). ChangeInfo udostępnia m.in.
Operator (rekord operatora), Time/Godzina (czas) oraz Type: ChangeInfoType. Kolekcja:
session.GetBusiness().ChangeInfos[row].
Snippet:
var dok = session.GetHandel().DokHandlowe[guidDokumentu];
// Kto i kiedy założył dokument (najwcześniejszy wpis ChangeInfos)
ChangeInfo zalozyl = dok.FirstChangeInfo;
if (zalozyl != null)
{
Operator ktoZalozyl = zalozyl.Operator; // rekord operatora
// zalozyl.Time / zalozyl.Godzina — czas utworzenia
}
// Kto ostatnio zmodyfikował
ChangeInfo ostatnia = dok.LastChangeInfo;
if (ostatnia != null)
{
Operator ktoZmienil = ostatnia.Operator;
}
// Pełna historia zmian rekordu
foreach (ChangeInfo ci in session.GetBusiness().ChangeInfos[dok])
{
// ci.Operator, ci.Time, ci.Type (ChangeInfoType: Added / Modified / Deleted ...)
}
Pułapki:
FirstChangeInfo/LastChangeInfosą kalkulowane (zapytaniaselect top 1 ... from ChangeInfos) — tylko do odczytu, nie ustawiaj. Mogą zwrócićnull, gdy historia rekordu jest pusta (np. import bez rejestracjiChangeInfo) — zawsze sprawdź!= null.- Rejestracja
ChangeInfozależy od konfiguracji (ChangeInfoModeper tabela). Jeśli historia jest wyłączona, właściwości mogą być puste — nie zakładaj, że audyt jest zawsze włączony. - Każdy odczyt
FirstChangeInfo/LastChangeInfoto osobne zapytanie SQL — przy przeglądaniu wielu dokumentów nie wywołuj ich w pętli po całej tabeli; ogranicz zakres (safe-code §6). - Nie loguj danych operatora w sposób ujawniający wrażliwe informacje (safe-code §12).
W61 — Praca z definicjami i numeracją (seria, wymuszenie numeru, bufor Numer)
Cel: rozpoznać definicję dokumentu i jej schemat numeracji, ustawić/odczytać serię, w razie potrzeby wymusić konkretny numer, oraz zrozumieć relację między buforem a numerem końcowym (dokument w buforze ma numer „BUFOR", numer właściwy nadawany jest przy zatwierdzeniu).
Warianty:
| Wariant | Mechanizm (publiczny) | Uwaga |
|---|---|---|
| Pobranie definicji | session.GetHandel().DefDokHandlowych.WgSymbolu["FV"] |
symbol z bazy Demo |
| Ustawienie definicji na dokumencie | dok.Definicja = def |
ustaw pierwszą, przed innymi polami |
| Rozpoznanie / ustawienie serii | dok.Seria, dok.GetListSeria() |
seria tylko gdy numeracja ma komponent „Seria" |
| Numer w buforze | dok.BuforNumer → "BUFOR", dok.Numer.NumerPelny |
numer właściwy nadawany przy zatwierdzeniu |
| Wymuszenie numeru | dok.Numer.NumerPelny = "..." |
tylko gdy definicja na to pozwala |
| Pełny numer (do odczytu) | dok.Numer.NumerPelny, dok.NumerPelnyZapisany |
string z serią i numerem |
Pola i typy: dok.Definicja: Soneta.Handel.DefDokHandlowego, dok.Seria: string,
dok.GetListSeria(): string[], dok.Numer: Soneta.Core.NumerDokumentu (bufor numeracji:
NumerPelny: string, PrzeliczSymbol(string component)), dok.NumerPelnyZapisany: string,
dok.BuforNumer: string (kalkulowane → "BUFOR" w buforze), dok.Bufor: bool (kalkulowane).
Snippet:
var hm = session.GetHandel();
using (var t = session.Logout(editMode: true))
{
var dok = new DokumentHandlowy();
session.AddRow(dok);
dok.Definicja = hm.DefDokHandlowych.WgSymbolu["FV"]; // definicja PIERWSZA — niesie schemat numeracji
dok.Kontrahent = session.GetCRM().Kontrahenci.WgKodu["Abc"];
// Seria — tylko gdy schemat numeracji definicji ma komponent „Seria"
string[] dostepneSerie = dok.GetListSeria();
if (dostepneSerie.Length > 0)
dok.Seria = dostepneSerie[0]; // ustawienie serii przelicza numer
t.Commit();
}
session.Save();
// Odczyt numeru: w buforze numer właściwy nie jest jeszcze nadany
bool wBuforze = dok.Bufor; // true → BuforNumer == "BUFOR"
string numer = dok.Numer.NumerPelny; // pełny numer (z serią), nadany przy zatwierdzeniu
// Zatwierdzenie nadaje numer właściwy
using (var t = session.Logout(editMode: true))
{
dok.Stan = StanDokumentuHandlowego.Zatwierdzony;
t.Commit();
}
session.Save();
Pułapki:
Definicjaustaw jako pierwszą — to ona określa wymagane pola (magazyn, kontrahent) oraz schemat numeracji (Numeracja). Zmiana definicji po wypełnieniu dokumentu jest ograniczona (IsReadOnlyDefinicja()).Seriamożna ustawić tylko, gdy numeracja definicji ma komponent „Seria" — w przeciwnym razie setter rzuciRowException(„SeriesDeniedErr"). Sprawdź przezGetListSeria()(zwraca dozwolone wartości; przy słowniku serii — tylko wartości ze słownika).- Numer właściwy nadawany jest przy zatwierdzeniu; dokument w buforze ma
BuforNumer == "BUFOR", aNumer.NumerPelnyzawiera znacznik „/BUFOR". Nie traktuj numeru z bufora jako ostatecznego. - Wymuszenie numeru przez
dok.Numer.NumerPelny = "..."działa tylko w granicach dozwolonych przez definicję (IsReadOnlyNumerPelny()); kolizja z istniejącym numerem ujawni się jakoRowExceptionzDuplicateKeyExceptionwSave(). Numerto obiektNumerDokumentu(bufor numeracji), nie zwykły string — pełny numer czytaj przezNumer.NumerPelnylubNumerPelnyZapisany, nie składaj go ręcznie z serii i liczby.
12. Wydruki i raporty
Wydruk dokumentu handlowego (faktura, dokument magazynowy, paragon) oraz raporty
i zestawienia tworzy się przez serwis IReportService z modułu Soneta.Business.UI.
Serwis bierze wzorzec wydruku (*.repx / *.aspx / *.dotx), kontekst z danymi
(rekord, zaznaczenie, parametry) i zwraca gotowy dokument jako strumień (Stream) —
bez udziału interfejsu użytkownika. To jest jedyny mechanizm, którego dodatek zewnętrzny
powinien używać do programowego generowania wydruków (export do PDF, wysyłka e-mail,
archiwizacja). Klasa ReportResult opisuje co i jak wydrukować.
Dostęp do serwisu (publiczny kontrakt):
using Microsoft.Extensions.DependencyInjection; // GetRequiredService using Soneta.Business.UI; // IReportService, ReportResult, ReportFormats, ReportTargets var raporty = session.GetRequiredService<IReportService>();
Metody IReportService (publiczne):
| Metoda | Zwraca | Zastosowanie |
|---|---|---|
Stream GenerateReport(ReportResult rr) |
strumień (PDF/XLSX/PNG/…) | generowanie wydruku binarnego do strumienia/pliku/e-maila |
string GenerateReportStr(ReportResult rr) |
string | wydruk tekstowy (HTML, TXT) |
void PrintReport(ReportResult rr, bool archive = false, string archivePath = "") |
— | wydruk na drukarkę (sprzęt), opcjonalna archiwizacja na dysk |
Type[] GetParameterTypes(string templateFileName, Context context) |
typy parametrów | sprawdzenie, jakich obiektów parametrów wymaga wzorzec |
Pola ReportResult (publiczne, najważniejsze):
| Pole | Typ | Znaczenie |
|---|---|---|
TemplateFileName |
string |
nazwa wzorca (np. "Sprzedaz.repx", "Zakup.repx"). Ustawienie go włącza tryb automatyczny (bez UI). |
DataType |
Type |
typ danych branych z kontekstu: typeof(DokumentHandlowy) (jeden), typeof(DokumentHandlowy[]) (zaznaczone), typeof(DokHandlowe) (cały widok). |
Context |
Context |
kontekst z rekordem(-ami) i parametrami wydruku (Context.Set(...)). |
OutputFormat |
ReportFormats |
PDF, XLSX, XLS, CSV, DOCX, TXT, HTML, MHT, PNG. Domyślnie HTML. |
Target |
ReportTargets |
cel: File, Printer, PrinterService, Preview, Attachment, Email, ShareDocument, OpenApplication. Domyślnie File. |
AskForParameters |
bool |
false = brak okien z pytaniem o parametry (tryb wsadowy). |
PrinterName |
string |
nazwa drukarki dla Target = Printer. |
Encrypt |
string |
hasło szyfrujące PDF. |
Sign, VisibleSignature |
bool |
podpis certyfikatem (tylko tryb interaktywny okienkowy). |
OutputHandler |
Func<Stream,object> |
własna obsługa gotowego strumienia (tryb wzorca; nieobsługiwane przez IReportService — patrz W66). |
ReportName |
string |
nazwa wydruku z menu (tryb interaktywny; wyklucza się z TemplateFileName/IReportService). |
Reguła spójności (
CheckConsistency):IReportServicewymaga ustawionegoTemplateFileNamei nie akceptujeOutputHandleraniReportName.ReportNameiTemplateFileNamewzajemnie się wykluczają. Naruszenie →ArgumentException.
W62 — Wydruk faktury do PDF / na drukarkę
Cel: wygenerować wydruk pojedynczego dokumentu handlowego (faktura sprzedaży FV, faktura zakupu FZ, paragon) do strumienia PDF albo wysłać go na drukarkę.
Warianty:
| Wariant | Ustawienie | Uwaga |
|---|---|---|
| Faktura sprzedaży → PDF | TemplateFileName = "Sprzedaz.repx", OutputFormat = PDF |
strumień %PDF… |
| Faktura zakupu → PDF | TemplateFileName = "Zakup.repx" |
analogicznie |
| Wydruk HTML / TXT | OutputFormat = HTML / TXT |
użyj GenerateReportStr lub GenerateReport |
| Duplikat / oryginał | parametr ParametryWydrukuDokumentu { Duplikat = … } w kontekście |
parametr wzorca |
| Na drukarkę (sprzęt) | Target = Printer, PrintReport(rr) |
wymaga drukarki — patrz „Pułapki” |
| PDF szyfrowany | Encrypt = "hasło" |
hasło otwarcia pliku |
Pola i typy: IReportService.GenerateReport(ReportResult) : Stream,
ReportResult.TemplateFileName : string, ReportResult.DataType : Type,
ReportResult.OutputFormat : ReportFormats, ReportResult.Context : Context,
ParametryWydrukuDokumentu : ContextBase (parametry wzorca dokumentu, m.in. Duplikat : bool).
Snippet:
using Microsoft.Extensions.DependencyInjection;
using Soneta.Business.UI;
using Soneta.Handel;
// 'dok' to zatwierdzona faktura sprzedaży (FV). 'session' — bieżąca sesja.
var raporty = session.GetRequiredService<IReportService>();
// 1. Kontekst: pojedynczy dokument + jego elementy + parametry wzorca.
var context = new Context(session);
context.Set(dok);
context.Set(dok.Definicja);
context.Set(dok.Kontrahent);
context.Set(new DokumentHandlowy[] { dok }); // wymagane przez niektóre wzorce
context.Set(new ParametryWydrukuDokumentu(context) { Duplikat = false });
// 2. Opis wydruku — tryb automatyczny (TemplateFileName) → bez UI.
var rr = new ReportResult {
TemplateFileName = "Sprzedaz.repx", // "Zakup.repx" dla faktury zakupu
DataType = typeof(DokumentHandlowy), // wydruk dla pojedynczego dokumentu
Context = context,
OutputFormat = ReportFormats.PDF,
AskForParameters = false // tryb wsadowy — nie pytaj o parametry
};
// 3. Generowanie do strumienia i zapis do pliku.
using (Stream pdf = raporty.GenerateReport(rr))
using (var plik = new FileStream(@"C:\Temp\FV.pdf", FileMode.Create, FileAccess.Write))
pdf.CopyTo(plik);
Pułapki:
GenerateReportzwracaStreamdla formatów binarnych (PDF, XLSX, PNG). DlaHTML/TXTużyjGenerateReportStr(zwracastring). Zwrócony strumień opakuj wusing.- Kontekst musi zawierać wszystko, czego wymaga wzorzec: rekord (
Context.Set(dok)), tablicę zaznaczeń i instancję parametrów (ParametryWydrukuDokumentu). Brak parametruAskForParameters = truew trybie wsadowym zawiesi się na oczekiwaniu na UI — w kodzie bez interfejsu zawsze ustawAskForParameters = false.
- Wydruk faktury powinien dotyczyć dokumentu zatwierdzonego (
Stan == Zatwierdzony) — dokument w buforze nie ma jeszcze nadanego numeru pełnego. - Sprawdzenie poprawności PDF w teście: pierwsze 4 znaki strumienia to
"%PDF"; HTML zaczyna się od"<!DOCTYPE html". - Druk na fizyczną drukarkę (
PrintReport,Target = Printer) wymaga sprzętu i sterownika — nie da się tego przetestować jednostkowo. W testach i integracjach używaj ścieżkiGenerateReport→ strumień/PDF.
W63 — Wydruk dokumentu magazynowego (PZ/WZ/MM)
Cel: wydrukować dokument magazynowy (PZ, WZ, MM, RW, PW) — identyczny mechanizm jak dla faktury, różni się tylko wzorcem dobranym do rodzaju dokumentu (wg jego definicji).
Warianty:
| Wariant | Wzorzec / DataType |
|---|---|
| Przyjęcie / wydanie magazynowe | wzorzec magazynowy (*.repx), DataType = typeof(DokumentHandlowy) |
| Przesunięcie MM | wzorzec MM |
| Wydruk wg definicji dokumentu | wzorzec domyślny przypisany do dok.Definicja |
Pola i typy: jak w W62 — IReportService.GenerateReport, ReportResult.TemplateFileName,
DokumentHandlowy.Definicja (decyduje o domyślnym wzorcu).
Snippet:
using Microsoft.Extensions.DependencyInjection;
using Soneta.Business.UI;
using Soneta.Handel;
// 'wz' — zatwierdzony dokument WZ (rozchód magazynowy).
var raporty = session.GetRequiredService<IReportService>();
var context = new Context(session);
context.Set(wz);
context.Set(wz.Definicja);
context.Set(wz.Magazyn);
context.Set(new DokumentHandlowy[] { wz });
context.Set(new ParametryWydrukuDokumentu(context) { Duplikat = false });
var rr = new ReportResult {
TemplateFileName = "WydanieZewnetrzne.repx", // wzorzec właściwy dla danego rodzaju dokumentu
DataType = typeof(DokumentHandlowy),
Context = context,
OutputFormat = ReportFormats.PDF,
AskForParameters = false
};
using (Stream pdf = raporty.GenerateReport(rr)) {
// pdf → plik / e-mail / archiwum
}
Pułapki:
- Dokument magazynowy i faktura to ten sam typ
DokumentHandlowy— różni je definicja (dok.Definicja) i przypisany wzorzec. DobierzTemplateFileNamezgodny z rodzajem dokumentu; nie drukuj WZ wzorcem faktury sprzedaży. - Dla dokumentów magazynowych ustaw w kontekście
dok.Magazyn(część wzorców go wymaga). - Nazwy wzorców są elementem konfiguracji wdrożenia (lista wydruków zarejestrowanych dla typu).
Listę typów parametrów, których wymaga konkretny wzorzec, sprawdzisz przez
GetParameterTypes(templateFileName, context)przed wywołaniemGenerateReport.
W64 — Raport dobowy i okresowy (zestawienie za dzień / okres)
Cel: wygenerować zestawienie/rejestr dokumentów za wskazany dzień (raport dobowy) lub wskazany okres (raport okresowy). Dwie odrębne ścieżki:
- Zestawienie/raport bazodanowy — przez
IReportServicez wzorcem zestawienia i parametrem okresu (analizowalny, zapisywalny do PDF/XLSX) — ścieżka testowalna. - Raport fiskalny drukarki (
RaportDobowy/RaportOkresowy) — wydruk na drukarce fiskalnej przezIFiscalPrinterAPI— wymaga sprzętu, nietestowalny jednostkowo.
Warianty:
| Wariant | Mechanizm | Parametr okresu |
|---|---|---|
| Zestawienie sprzedaży za dzień → PDF | IReportService + wzorzec zestawienia, DataType = typeof(DokHandlowe) |
FromTo(dzień, dzień) w parametrach wzorca |
| Zestawienie za okres → PDF/XLSX | jw. | FromTo(od, do) |
| Fiskalny raport dobowy (sprzęt) | IFiscalPrinterAPI.DrukujRaport(nazwaDrukarki) |
dzień bieżący |
| Fiskalny raport okresowy (sprzęt) | IFiscalPrinterAPI.DrukujRaportOkresowy(nazwaDrukarki, RaportOkresowyParams) |
RaportOkresowyParams.RaportZaOkres : FromTo |
Pola i typy:
Soneta.Fiskal.IFiscalPrinterAPI (publiczny): DrukujRaport(string nazwaDrukarki),
DrukujRaportOkresowy(string nazwaDrukarki, RaportOkresowyParams pars),
Fiskalizuj(DokumentHandlowy dok, string nazwaDrukarki).
Soneta.Fiskal.RaportOkresowyParams : ContextBase — RaportZaOkres : FromTo ([Required]),
inicjalizowany na dzień bieżący; ctor RaportOkresowyParams(Context).
Snippet:
using Microsoft.Extensions.DependencyInjection;
using Soneta.Business.UI;
using Soneta.Types; // FromTo, Date
// --- Ścieżka 1: zestawienie bazodanowe za wskazany dzień → PDF (testowalne) ---
var raporty = session.GetRequiredService<IReportService>();
var dzien = Date.Today;
var context = new Context(session);
context.Set(new FromTo(dzien, dzien)); // parametr okresu wzorca zestawienia
var rr = new ReportResult {
TemplateFileName = "ZestawienieSprzedazy.repx", // wzorzec rejestru/zestawienia
DataType = typeof(Soneta.Handel.DokHandlowe), // wydruk dla zbioru dokumentów z widoku
Context = context,
OutputFormat = ReportFormats.PDF,
AskForParameters = false
};
using (Stream pdf = raporty.GenerateReport(rr)) {
// zapis / wysyłka
}
// --- Ścieżka 2: fiskalny raport okresowy (WYMAGA DRUKARKI FISKALNEJ) ---
// var fiskal = session.GetRequiredService<Soneta.Fiskal.IFiscalPrinterAPI>();
// var pars = new Soneta.Fiskal.RaportOkresowyParams(context) {
// RaportZaOkres = new FromTo(new Date(2026, 6, 1), new Date(2026, 6, 30))
// };
// fiskal.DrukujRaportOkresowy("Posnet Thermal", pars); // druk na sprzęcie
Pułapki:
- Rozróżnij dwie rzeczy o podobnej nazwie: raport dobowy/okresowy drukarki fiskalnej
(
IFiscalPrinterAPI, rozliczenie utargu na sprzęcie) vs. bazodanowe zestawienie/rejestr za dzień/okres (IReportService+ wzorzec). Dodatek raportujący zwykle chce ścieżki 2. RaportOkresowyParams.RaportZaOkresjest[Required]; pustyFromToresetuje się do dnia bieżącego, a otwarty zakres (From == MinValue/To == MaxValue) zwija się do jednego dnia.- Fiskalny raport (
DrukujRaport*) wymaga podłączonej drukarki fiskalnej — operacja sprzętowa, nie do testów jednostkowych. Testuj wyłącznie ustawienieRaportOkresowyParamsi ścieżkę bazodanowąGenerateReport.
W65 — Wydruk zbiorczy dla zaznaczonego zbioru dokumentów
Cel: wygenerować jeden wydruk obejmujący wiele dokumentów naraz (np. seria faktur z zaznaczenia listy) zamiast drukować każdy osobno.
Warianty:
| Wariant | DataType |
Kontekst |
|---|---|---|
| Zaznaczone rekordy | typeof(DokumentHandlowy[]) |
context.Set(tablica) zaznaczonych dokumentów |
| Wszystkie z widoku | typeof(DokHandlowe) |
rekordy dostarcza View/ViewInfo |
| Pojedynczy | typeof(DokumentHandlowy) |
jeden rekord (W62) |
DataTypedecyduje, które rekordy trafiają na wydruk:typeof(T)— jeden obiekt,typeof(T[])— zaznaczone,typeof(Tabela)— wszystkie z widoku.
Pola i typy: ReportResult.DataType : Type, ReportResult.Rows : IEnumerable
(jawne wskazanie rekordów do wydruku), Context.Set(DokumentHandlowy[]).
Snippet:
using Microsoft.Extensions.DependencyInjection;
using Soneta.Business.UI;
using Soneta.Handel;
// 'zaznaczone' — tablica zatwierdzonych dokumentów do wydruku zbiorczego.
DokumentHandlowy[] zaznaczone = /* ... */;
var raporty = session.GetRequiredService<IReportService>();
var context = new Context(session);
context.Set(zaznaczone); // zbiór rekordów do wydruku
var rr = new ReportResult {
TemplateFileName = "Sprzedaz.repx",
DataType = typeof(DokumentHandlowy[]), // wydruk dla ZAZNACZONYCH rekordów
Rows = zaznaczone, // jawne wskazanie zbioru (opcjonalne)
Context = context,
OutputFormat = ReportFormats.PDF,
AskForParameters = false
};
using (Stream pdf = raporty.GenerateReport(rr)) {
// jeden strumień PDF z wieloma dokumentami
}
Pułapki:
- Kluczowa różnica vs W62 to
DataType = typeof(DokumentHandlowy[])— typ tablicowy przełącza wzorzec w tryb wielu rekordów. Ztypeof(DokumentHandlowy)wydrukuje się tylko pierwszy/bieżący dokument. Rows(IEnumerable) pozwala jawnie podać zbiór; pole nie działa dla wydruków z menu (tylko dla automatycznego trybu zTemplateFileName).- Do wydruków masowych ustaw
AskForParameters = false— inaczej każdy dokument mógłby wywołać okno parametrów. - Wszystkie dokumenty w zbiorze powinny pasować do jednego wzorca (ten sam rodzaj/definicja).
W66 — Zapis wydruku do strumienia/pliku (integracja, e-mail)
Cel: uzyskać wydruk jako strumień bajtów, bez drukowania — do zapisania w pliku, dołączenia jako załącznik do e-maila, archiwizacji lub przesłania do zewnętrznego systemu.
Warianty:
| Wariant | Mechanizm |
|---|---|
| Do pliku / strumienia | GenerateReport → Stream → FileStream/MemoryStream |
| Wydruk tekstowy (HTML/TXT) | GenerateReportStr → string |
| Załącznik e-mail | Target = ReportTargets.Email lub strumień z GenerateReport jako załącznik |
| Z archiwizacją na druk | PrintReport(rr, archive: true, archivePath: @"C:\Archiwum") |
Własna obsługa strumienia (tryb wzorca, nie IReportService) |
ReportResult.OutputHandler jako rezultat operacji |
Pola i typy: IReportService.GenerateReport(ReportResult) : Stream,
IReportService.GenerateReportStr(ReportResult) : string,
ReportResult.OutputFormat : ReportFormats, ReportResult.Target : ReportTargets,
ReportResult.Encrypt : string (hasło PDF),
ReportResult.OutputHandler : Func<Stream, object> (tylko rezultat operacji UI).
Snippet:
using Microsoft.Extensions.DependencyInjection;
using Soneta.Business.UI;
using Soneta.Handel;
var raporty = session.GetRequiredService<IReportService>();
var context = new Context(session);
context.Set(dok);
context.Set(new DokumentHandlowy[] { dok });
context.Set(new ParametryWydrukuDokumentu(context) { Duplikat = false });
var rr = new ReportResult {
TemplateFileName = "Sprzedaz.repx",
DataType = typeof(DokumentHandlowy),
Context = context,
OutputFormat = ReportFormats.PDF,
Encrypt = "tajne-haslo", // (opcjonalnie) PDF chroniony hasłem
AskForParameters = false
};
// 1. Do pamięci — np. bajty do wysyłki e-mailem przez własny mechanizm:
byte[] pdfBytes;
using (Stream src = raporty.GenerateReport(rr))
using (var ms = new MemoryStream()) {
src.CopyTo(ms);
pdfBytes = ms.ToArray();
}
// pdfBytes → załącznik wiadomości, REST API, repozytorium dokumentów...
// 2. Wariant: niech mechanizm sam wyśle e-mail (rezultat operacji w workerze UI):
// rr.Target = ReportTargets.Email; // wymaga konfiguracji konta pocztowego i szablonu
Pułapki:
GenerateReportto właściwa droga dla integracji — zwraca strumień, którym dysponujesz dowolnie (plik, e-mail, sieć). Zawszeusingna zwróconym strumieniu (PDF i inne formaty binarne).OutputHandlernie jest obsługiwany przezIReportService(CheckConsistencyrzuciArgumentException). Służy jako rezultat operacji w trybie wzorca (worker/Command z UI), nie do wsadowego generowania w czystym kodzie biznesowym.Target = Email/Attachmentto ścieżki integrujące się z modułem pocztowym (kontoKontoPocztowe, szablonSzablonEmail) — wymagają pełnej, skonfigurowanej sesji aplikacyjnej; w czystym kodzie integracyjnym prościej pobrać strumień zGenerateReporti wysłać go własnym kanałem.- Format dobieraj świadomie:
PDF/XLSX/PNG→GenerateReport(Stream);HTML/TXT→GenerateReportStr(string). - Szyfrowanie (
Encrypt) i podpis (Sign) dotyczą PDF; podpis certyfikatem działa tylko w trybie interaktywnym okienkowym (wymaga okna certyfikatu).
Co jest testowalne, a co nie (sekcja 12):
- Testowalne: generowanie wydruku do strumienia/PDF/HTML/TXT przez
IReportService.GenerateReport/GenerateReportStr(W62, W63, W64-ścieżka bazodanowa, W65, W66). Asercja: PDF zaczyna się od"%PDF", HTML od"<!DOCTYPE html".- Nietestowalne jednostkowo (wymaga sprzętu): druk na fizyczną drukarkę (
PrintReport,Target = Printer) oraz fiskalny raport dobowy/okresowy drukarki (IFiscalPrinterAPI.DrukujRaport/DrukujRaportOkresowy,Fiskalizuj). Dla nich testuj tylko poprawne ustawienieReportResult/RaportOkresowyParams, bez faktycznego druku.
13. Tematy specjalistyczne (KSeF, fiskalizacja, kompletacja, Intrastat)
Rozdział obejmuje obszary, które łączą dokument handlowy z systemami zewnętrznymi (KSeF), urządzeniami (drukarka fiskalna) oraz specjalistyczną logiką magazynową (kompletacja) i sprawozdawczą (Intrastat).
Ważne — co jest, a co nie jest testowalne jednostkowo. Część operacji wymaga sieci (komunikacja z bramką KSeF, wysyłka e-mail e-paragonu) albo sprzętu (drukarka fiskalna). Tych fragmentów nie da się odtworzyć w teście jednostkowym — testuj wyłącznie ustawienie pól i parametrów oraz strukturę (np.
XmlValidated, parametry workera, polaKSeFKomunikat). Każdy wzorzec poniżej oznacza, która część jest „offline" (testowalna), a która „online/sprzętowa" (NIE testuj — patrzdh-facts.md, „Reguły testów").Wszystkie workery wymienione w tym rozdziale są publiczne i mogą być wywołane z dodatku zewnętrznego. Operacje modyfikujące dokument wykonuj w transakcji (
session.Logout(true)+Commit/CommitUI), potemsession.Save(). Kod zgodny z C# 10.
W67 — Wysłanie faktury do KSeF (pojedynczo i zbiorczo)
Cel: wysłać zatwierdzony dokument sprzedaży do Krajowego Systemu e-Faktur — pojedynczo
(KSeFWyslijWorker) albo wsadowo dla wielu dokumentów naraz (KSeFWysylkaWsadowaWorker). Sama wysyłka
to operacja online (NIE testuj); offline (testowalne) jest przygotowanie dokumentu: wygenerowanie XML,
walidacja struktury i ustawienie parametrów autoryzacji.
Warianty:
| Wariant | Worker / akcja | Uwaga |
|---|---|---|
| Wysyłka pojedyncza | KSeFWyslijWorker.Wyslij(dok) (akcja „KSeF/Wyślij") |
dla jednego dokumentu |
| Wysyłka zbiorcza | KSeFWysylkaWsadowaWorker.WyslijZbiorczo() (akcja „KSeF/Wyślij zbiorczo") |
Dokumenty[], generuje XML brakującym, pomija zaimportowane/odrzucone |
| Faktura offline (awaria/tryb 24h) | wysyłka z KSeFKomunikat.Offline == true |
używa tokenu i kontekstu zapisanych na komunikacie |
| Data wystawienia ≠ dziś | weryfikator KSeFWyslijWorker.Weryfikator(dok) |
rzuca wyjątek wg konfiguracji i uprawnień (data przyszła/przeszła) |
Pola i typy:
- Parametry:
KSeFWyslijParams : ContextBase—SystemZewn: SystemZewnPlatformaEDI([Required]),Token: SysZewToken([Required], „Sposób autoryzacji"),KontekstAutentykacjiKSeF(„Kontekst autoryzacji"). Listy:GetListSystemZewn(),GetListToken(),GetListKontekstAutentykacjiKSeF(). KSeFWyslijWorker:[Context] Dokument: DokumentHandlowy,[Context] Parametry: KSeFWyslijParams,[Context] Context: Context. Akcjaobject Wyslij(DokumentHandlowy dok).KSeFWysylkaWsadowaWorker:[Context] Parametry: KsefEksportIWyslijParams,[Context(Required=false)] Dokumenty: DokumentHandlowy[],[Context(Required=false)] Dokument: DokumentHandlowy,[Context] Context: Context.- Warunek wstępny (sprawdzany przez
WeryfikatorPolaXmlValidated): każdy dokument musi miećdok.ImportExportKSeF.XmlValidated == ThreeStateBoolean.True(czyli wcześniej wykonaną walidację struktury — W69).
Snippet:
using Microsoft.Extensions.DependencyInjection;
using Soneta.KSeF.Workers;
var hm = session.GetHandel();
var dok = hm.DokHandlowe.WgNumer[...]; // zatwierdzona faktura sprzedaży
// 1) Walidacja daty wystawienia (offline, testowalne) — rzuca wyjątek dla daty != dziś
// wg konfiguracji KSeF i uprawnień operatora:
KSeFWyslijWorker.Weryfikator(dok);
// 2) Przygotowanie parametrów autoryzacji (offline). System i token wybierane z list:
var ctx = session.GetEmptyContext();
ctx.TryAdd(() => dok);
var parametry = new KSeFWyslijParams(ctx); // konstruktor sam wybiera domyślny system/token
// parametry.SystemZewn / parametry.Token można ustawić jawnie z GetListSystemZewn()/GetListToken()
// 3) Wysyłka pojedyncza — OPERACJA ONLINE (NIE testuj jednostkowo):
var worker = new KSeFWyslijWorker { Dokument = dok, Parametry = parametry, Context = ctx };
object wynik = worker.Wyslij(dok); // wewnątrz: SesjaWysylki + WyslijDokument, zapis KSeFKomunikat
// Wysyłka zbiorcza — ONLINE; Dokument musi być pierwszym elementem tablicy Dokumenty:
DokumentHandlowy[] doks = hm.DokHandlowe.WgNumer[...].ToArray();
var workerZb = new KSeFWysylkaWsadowaWorker { Dokument = doks[0], Dokumenty = doks, Context = ctx, Parametry = paramsZb };
workerZb.WyslijZbiorczo();
Pułapki:
- Tylko dokumenty zatwierdzone podlegają wysyłce (
IsVisible*wymagajądok.Zatwierdzony). Bufor i dokument anulowany nie są wysyłane. - Przed wysyłką dokument musi mieć zwalidowany XML (
XmlValidated == True) — inaczejWeryfikatorPolaXmlValidatedrzuci wyjątek „nie posiada zweryfikowanego pliku XML". Najpierw wykonaj W69 (Sprawdź strukturę pliku) lub wygeneruj XML (wysyłka zbiorcza robi to automatycznie dla statusuBrak). - Wysyłka zbiorcza pomija dokumenty: zaimportowane z KSeF (
ImportExportKSeF.Rodzaj == Import), o nieprawidłowym/niezweryfikowanym XML, wygenerowane inną definicją niż w parametrach, w trybie offline z innym tokenem — wszystkie pominięcia trafiają do logu „KSeF". - Cała komunikacja z bramką (
IKSeFAPIv2Service/IKSeFAPIService) wymaga sieci → NIE testuj jednostkowo. W teście weryfikuj jedynie: utworzenieKSeFWyslijParams, dobór systemu/tokenu z list,Weryfikatororaz że XML jest zwalidowany. - Po wysyłce na dokumencie zatwierdzonym ustawiana jest flaga
Session.SaveImmediatelyIfPossible = true(natychmiastowy zapis komunikatu KSeF).
W68 — Sprawdzenie statusu KSeF i odczyt numeru KSeF
Cel: po wysyłce sprawdzić w bramce, czy dokument został przyjęty, i pobrać nadany numer KSeF
(KSeFSprawdzStatusWorker). Sprawdzenie statusu to operacja online (NIE testuj); odczyt już zapisanego
statusu/numeru jest offline (testowalne — pola kalkulowane na dokumencie).
Warianty:
| Wariant | Mechanizm |
|---|---|
| Sprawdzenie statusu po sesji wysyłki | KSeFSprawdzStatusWorker.SprawdzStatus(dok) (akcja „KSeF/Sprawdź status") — ONLINE |
| Odczyt aktualnego statusu | dok.StatusKSeF: KSeFState — offline, kalkulowane |
| Odczyt numeru KSeF / nr referencyjnych | dok.KSeFKomunikat.NumerDokumentuKSeF itd. — offline |
| Czy dokument w ogóle podlega KSeF | dok.PodlegaKSeF, dok.PosiadaKSeF — offline |
Pola i typy:
dok.StatusKSeF: Soneta.Core.KSeF.KSeFState(kalkulowane). Wartości:NieDotyczy=1, Brak=2, DoWyslania=4, Wyslany=8, Przyjety=16, Odrzucony=32(orazRobocze=14,Razem=31zachowane dla kompatybilności). Status wyliczany z zawartościKSeFKomunikati stanu dokumentu (Bufor/Anulowany ⇒NieDotyczy).dok.KSeFKomunikat(rekordKSeFKomunikat):NumerDokumentuKSeF: string(numer nadany przez KSeF — ustawiony ⇒ statusPrzyjety),NumerReferencyjnyKSeF: string,NumerReferencyjnySesjiKSeF: string,OpisBledu: string(niepusty ⇒ statusOdrzucony),Offline: bool,TokenKSeF: SysZewToken,DataPrzeslaniaKSeF,DataPrzyjeciaKSeF.dok.PosiadaKSeF: bool(ma plikImportExportKSeF),dok.PodlegaKSeF: bool,dok.QRCodeLink: string.
Snippet:
// Sprawdzenie statusu w bramce — OPERACJA ONLINE (NIE testuj jednostkowo):
var worker = new KSeFSprawdzStatusWorker();
MessageBoxInformation wynik = worker.SprawdzStatus(dok); // pobiera status z sesji wysyłki
// Odczyt zapisanego statusu i numeru — OFFLINE, w pełni testowalne:
KSeFState status = dok.StatusKSeF;
if (status == KSeFState.Przyjety)
{
string numerKSeF = dok.KSeFKomunikat.NumerDokumentuKSeF; // numer nadany przez KSeF
string nrSesji = dok.KSeFKomunikat.NumerReferencyjnySesjiKSeF;
}
else if (status == KSeFState.Odrzucony)
{
string blad = dok.KSeFKomunikat.OpisBledu; // przyczyna odrzucenia
}
Pułapki:
StatusKSeFjest kalkulowane — nie da się go ustawić; zmienia się przez samKSeFKomunikat.- Sprawdzenie statusu działa tylko, gdy
dok.StatusKSeF != Przyjetyi istniejeKSeFKomunikatz numerem referencyjnym sesji; dokument w stanieDoWyslanianie ma jeszcze czego sprawdzać. - Worker odczytuje status wszystkich dokumentów z tej samej sesji wysyłki (
NumerReferencyjnySesjiKSeF) i każdemu z nich uzupełnia numer KSeF — to operacja zbiorcza po stronie bramki. - Wywołanie
IKSeFAPIv2Service.SprawdzStatusDokumentowZSesjiwymaga sieci → NIE testuj jednostkowo. W teście weryfikuj jedynie wyliczanieStatusKSeFz różnych ustawieńKSeFKomunikat.
W69 — UPO, numer KSeF z duplikatu, walidacja struktury XML
Cel: trzy operacje pomocnicze KSeF: pobranie UPO (urzędowego poświadczenia odbioru) dla przyjętej
faktury, odzyskanie numeru KSeF z duplikatu (gdy bramka odrzuciła dokument kodem 440 = duplikat) oraz
walidacja struktury XML względem schematu (XSD). Walidacja XML jest offline (testowalna); pobranie
UPO jest online (NIE testuj). Pobranie numeru z duplikatu jest offline (parsuje istniejący OpisBledu).
Warianty:
| Wariant | Worker / akcja | Online? |
|---|---|---|
| Walidacja struktury XML | KSeFSprawdzXMLWorker.Check() (akcja „KSeF/Sprawdź strukturę pliku") |
OFFLINE (lokalny XSD) |
| Pobranie UPO dla dokumentu | KSeFSprawdzUPODokumentuWorker.SprawdzUPO() (akcja „KSeF/Sprawdź UPO...") |
ONLINE |
| Numer KSeF z duplikatu (błąd 440) | PobierzNumerKSeFZDuplikatuWorker.PobierzNumerDokumentuKSeF(dok) |
OFFLINE (parsuje OpisBledu) |
Pola i typy:
KSeFSprawdzXMLWorker:[Context] Dokument: DokumentHandlowy, metodavoid Check(). Ustawiadok.ImportExportKSeF.XmlValidated: ThreeStateBoolean(True/False). Walidator publiczny:KSeFSchemaVerifier.Verify(DokumentHandlowy dok)(rzuca wyjątek przy niezgodności ze schematem).KSeFSprawdzUPODokumentuWorker:[Context] Dokument,void SprawdzUPO(). Wymagadok.StatusKSeF == Przyjetyi tokenu w wersji API v2 (KSeFKomunikat.TokenKSeF.KSeFAPIv2), inaczej rzucaRowException. Zapisuje rekordKSeFUPOi datyDataPrzeslaniaKSeF/DataPrzyjeciaKSeF.PobierzNumerKSeFZDuplikatuWorker: akcjavoid PobierzNumerDokumentuKSeF(DokumentHandlowy dok). Aktywna, gdydok.KSeFKomunikat.OpisBleduzawiera „440"; z opisu wyłuskuje numer dokumentu i sesji, ustawiaNumerDokumentuKSeF/NumerReferencyjnySesjiKSeF(status przechodzi naPrzyjety).
Snippet:
// 1) Walidacja struktury XML — OFFLINE (lokalny XSD), w pełni testowalne:
var xmlWorker = new KSeFSprawdzXMLWorker { Dokument = dok };
xmlWorker.Check();
bool poprawny = dok.ImportExportKSeF.XmlValidated == ThreeStateBoolean.True;
// Alternatywnie sam weryfikator (rzuca wyjątek przy błędzie struktury):
KSeFSchemaVerifier.Verify(dok);
// 2) Numer KSeF z duplikatu — OFFLINE (parsuje OpisBledu z błędu 440):
var dupWorker = new PobierzNumerKSeFZDuplikatuWorker();
dupWorker.PobierzNumerDokumentuKSeF(dok); // ustawia NumerDokumentuKSeF, jeśli OpisBledu zawiera "440"
// 3) Pobranie UPO — OPERACJA ONLINE (NIE testuj jednostkowo):
var upoWorker = new KSeFSprawdzUPODokumentuWorker { Dokument = dok };
upoWorker.SprawdzUPO(); // wymaga StatusKSeF == Przyjety oraz API v2
Pułapki:
Check()opiera się o lokalny XSD (ImportExportKSeF.DefinicjaXmlNag.LocalXSD) — nie potrzebuje sieci, dlatego jest testowalny. Wymaga jednak wcześniej wygenerowanego XML (ImportExportKSeF.Xmlniepusty —IsEnabledCheck).SprawdzUPO()rzucaRowException, gdy dokument nie jestPrzyjetyalbo nie był wysłany w API v2 — obsłuż to przed wywołaniem. Samo pobranie UPO wymaga sieci → NIE testuj.PobierzNumerDokumentuKSeFustawia wOpisBleduznacznikDokumentHandlowy.PobranoNumerKSeFZDuplikatuOpis(link QR z duplikatu może nie działać) — to celowy efekt uboczny, nie błąd.- Walidacja statusu „440 = duplikat" działa wyłącznie na tekście
OpisBledu— jeśli opis nie zawiera „440", worker nic nie robi.
W70 — Import faktur z KSeF (dokumenty zakupu)
Cel: pobrać z KSeF faktury zakupowe (oraz sprzedażowe), zapisać je jako pliki KSeF (KSeFPlik) w bazie,
a następnie utworzyć z nich dokumenty zakupu. Cały proces pobierania jest online (komunikacja z bramką)
i operuje na rekordach konfiguracyjno-systemowych (KSeFZapytanieOFa, KSeFPlik), a tworzenie
dokumentów zakupu z plików KSeF realizowane jest w module księgowym — NIE testuj jednostkowo części
sieciowej.
Warianty:
| Wariant | Mechanizm |
|---|---|
| Zapytanie o faktury za okres | rekord KSeFZapytanieOFa + parametry ParametryPobieraniaFakturKSeF (DataOd, DataDo, PodmiotTworzeniaZapytaniaKSeF) |
| Pobranie paczek wyników | KSeFDownloadPartWorker.Pobierz() (akcja „Pobierz pakiety") — ONLINE; tworzy KSeFPlik |
| Kwalifikacja kierunku (zakup/sprzedaż) | wg porównania NIP z pieczątki firmy z NIP-em Podmiot1 w XML |
| Utworzenie dokumentu zakupu | z KSeFPlik (import XML do dokumentu) — obszar księgowy |
Pola i typy:
KSeFDownloadPartWorker:[Context] KSeFZapytanieOFa: KSeFZapytanieOFa, akcjaobject Pobierz(). Pobiera tylko, gdyKSeFZapytanieOFa.StatusZapytania == StatusZapytania.Przetworzonoi nie pobrano jeszcze wszystkich paczek (PobraneWszystkie).ParametryPobieraniaFakturKSeF:DataOd: DateTimeOffset,DataDo: DateTimeOffset,PodmiotTworzeniaZapytaniaKSeF,PobieranieSamofakturowania.- Wynik: rekordy
KSeFPlik(zRodzajDokumentuKSeFZapytanieOFa:Sprzedaz/Zakup/Razem) tworzone przezKSeFPlik.CreateKSefPlik(...). FormularzFA_RRjest pomijany.
Snippet:
// Pobranie paczek z wynikami zapytania — OPERACJA ONLINE (NIE testuj jednostkowo):
var worker = new KSeFDownloadPartWorker { KSeFZapytanieOFa = zapytanie };
object wynik = worker.Pobierz(); // tworzy rekordy KSeFPlik dla faktur z bramki
// Po pobraniu pliki KSeF są dostępne w module Core (KSeFPliki) i mogą zostać
// zaimportowane jako dokumenty zakupu (obszar księgowy). Kierunek (Zakup/Sprzedaz)
// kwalifikowany jest automatycznie przez porównanie NIP-u z pieczątki firmy z NIP-em
// nadawcy (Podmiot1) w pliku XML.
Pułapki:
- Pobranie paczek wymaga sieci (
IKSeFAPIv2Service.PobierzFakturyZPaczek) → NIE testuj jednostkowo. - Import opiera się o rekordy
KSeFZapytanieOFa/KSeFPlik, a nie bezpośrednio oDokumentHandlowy— dokument zakupu powstaje dopiero w kolejnym kroku (import XML), poza zakresem prostego workera na dokumencie. - Pliki o tym samym numerze KSeF są pomijane (deduplikacja po numerze), tak samo formularz
FA_RR. - Z poziomu dodatku zewnętrznego operuj na publicznych
ParametryPobieraniaFakturKSeFi statusie zapytania; testuj wyłącznie logikę przygotowania parametrów (okres, podmiot), nie samo pobieranie.
W71 — Fiskalizacja dokumentu (paragon fiskalny)
Cel: oznaczyć / wydrukować dokument sprzedaży jako paragon na drukarce fiskalnej. Wydruk na drukarce
to operacja sprzętowa (NIE testuj). Worker FiskalizacjaDokumentuWorker ma jednak rolę „oznacz jako
zafiskalizowane" — ustawienie SymbolKasy na zatwierdzonym dokumencie jest offline (testowalne).
Warianty:
| Wariant | Mechanizm | Sprzęt? |
|---|---|---|
| Oznacz jako zafiskalizowane | FiskalizacjaDokumentuWorker.Execute() (akcja „Narzędziowe/Oznacz jako zafiskalizowane") |
NIE (tylko ustawia SymbolKasy) |
| Symbol drukarki tekstowo | ParametryFiskalizacjiDokumentu.SymbolKasy: string (max 12) |
— |
| Symbol drukarki z listy (z bazy) | ParametryFiskalizacjiDokumentu.SymbolKasyEnum + GetListSymbolKasyEnum() |
— |
| Faktyczny wydruk fiskalny | Fiscalizer (klasa fiskalizatora) |
TAK |
Pola i typy:
FiskalizacjaDokumentuWorker:[Context] Dokument: DokumentHandlowy,[Context] Parametry: ParametryFiskalizacjiDokumentu, metodavoid Execute().ParametryFiskalizacjiDokumentu : ContextBase—SymbolKasy: string([MaxLength(12)], „Symbol drukarki"),SymbolKasyEnum: string(combo, gdy dane drukarki w bazie),GetListSymbolKasyEnum(): List<string>.- Pola dokumentu:
dok.SymbolKasy: string(ustawiane przezUstawSymbolKasy),dok.Kategoria: KategoriaHandlowa. IsVisibleExecute: tylkoSprzedaż/KorektaSprzedaży.IsEnabledExecute: dokument zatwierdzony i z pustymSymbolKasy.
Snippet:
// Oznaczenie dokumentu jako zafiskalizowanego (OFFLINE — ustawia tylko SymbolKasy):
var ctx = session.GetEmptyContext();
ctx.TryAdd(() => dok);
var parametry = new FiskalizacjaDokumentuWorker.ParametryFiskalizacjiDokumentu(ctx)
{
SymbolKasy = "DRUK1" // symbol drukarki, max 12 znaków
};
var worker = new FiskalizacjaDokumentuWorker { Dokument = dok, Parametry = parametry };
worker.Execute(); // wykona się tylko gdy dok zatwierdzony i SymbolKasy pusty
// Po operacji:
string symbol = dok.SymbolKasy; // "DRUK1"
// Faktyczny wydruk na drukarce fiskalnej — OPERACJA SPRZĘTOWA (NIE testuj):
// var fiscalizer = new Fiscalizer(dok);
// fiscalizer.Fiscalize(false);
Pułapki:
Execute()zFiskalizacjaDokumentuWorkernie drukuje — jedynie ustawiaSymbolKasyi dopisuje informację o fiskalizacji do zmian zapisu. Faktyczny wydruk realizuje klasaFiscalizer(sprzęt) → NIE testuj.- Operacja działa wyłącznie dla dokumentów zatwierdzonych o pustym
SymbolKasy(IsEnabledExecute) i kategoriiSprzedaż/KorektaSprzedaży(IsVisibleExecute). SymbolKasyjest przycinany (Trim) i ograniczony do 12 znaków; wybór z listy (SymbolKasyEnum) dostępny tylko, gdy konfiguracja trzyma dane drukarek w bazie (Config.DrukarkaFiskalna.DaneDrukarkiZapisywaneWBazie).- W teście weryfikuj jedynie ustawienie
dok.SymbolKasyi warunkiIsEnabled/IsVisible— nie symuluj wydruku.
W72 — E-paragon (e-mail) i ponowny wydruk paragonu
Cel: obsłużyć e-paragon (paragon w formie elektronicznej wysyłany e-mailem) oraz ponowny wydruk
paragonu na drukarce fiskalnej. Ustawienie pól e-paragonu (EParagon, adres e-mail) jest offline
(testowalne); wysyłka e-mail i wydruk na drukarce są online/sprzętowe (NIE testuj).
Warianty:
| Wariant | Mechanizm | Online/sprzęt? |
|---|---|---|
| Oznaczenie dokumentu jako e-paragon | dok.EParagon: bool, dok.EParagonAdresEmail: string |
NIE (pola) |
| Polityka e-paragonu na definicji | Definicja.OznaczJakoEParagon: OznaczJakoEParagon |
NIE |
| Odczyt danych wysłanego e-paragonu | dok.DaneEParagonu: DaneEParagonu, dok.OtworzUrlEParagonu() |
NIE (odczyt) |
| Ponowny wydruk paragonu | PonownyWydrukParagonuWorker.Drukuj() (akcja „Narzędziowe/Wydrukuj ponownie...") |
TAK (sprzęt) |
Pola i typy:
dok.EParagon: bool— czy dokument jest e-paragonem; ustawienieEParagonAdresEmailautomatycznie ustawiaEParagon(poza politykąOznaczJakoEParagon.Zawsze).dok.EParagonAdresEmail: string— adres e-mail odbiorcy e-paragonu (walidowany; przyEParagon==truenie może być pusty).Definicja.OznaczJakoEParagon: Soneta.Handel.OznaczJakoEParagon—Nigdy=0, Zawsze=1, WgKontrahenta=2.dok.DaneEParagonu: DaneEParagonu,dok.OtworzUrlEParagonu(): HyperlinkResult.PonownyWydrukParagonuWorker:[Context] Paragon: DokumentHandlowy, akcjaobject Drukuj().IsVisibleDrukuj: definicjaFiskalizowany, dokument zatwierdzony, niepustySymbolKasy.
Snippet:
// Oznaczenie dokumentu jako e-paragon i ustawienie adresu e-mail (OFFLINE — testowalne):
using (var t = session.Logout(true))
{
dok.EParagonAdresEmail = "klient@example.com"; // ustawia też EParagon = true
t.Commit();
}
session.Save();
bool jestEParagonem = dok.EParagon; // true
// Odczyt danych wysłanego e-paragonu (offline):
DaneEParagonu dane = dok.DaneEParagonu;
// Ponowny wydruk na drukarce fiskalnej — OPERACJA SPRZĘTOWA (NIE testuj jednostkowo):
var worker = new PonownyWydrukParagonuWorker { Paragon = dok };
object wynik = worker.Drukuj(); // pyta o potwierdzenie, następnie Fiscalizer.Fiscalize(false)
Pułapki:
- Ustawienie
EParagonAdresEmailma efekt uboczny: dla polityki innej niżZawszeautomatycznie ustawiaEParagon = !string.IsNullOrWhiteSpace(value). PrzyEParagon==truepusty adres e-mail nie przejdzie walidacji (EParagonVerifier/EParagonEmailVerifier). - Sama wysyłka e-paragonu e-mailem wymaga sieci, a ponowny wydruk — drukarki fiskalnej → NIE testuj
jednostkowo. Testuj jedynie ustawienie
EParagon/EParagonAdresEmaili wyliczanie politykiOznaczJakoEParagon. PonownyWydrukParagonuWorker.Drukuj()wyświetla pytanie „czy wysłać ponownie" (MessageBoxInformation) — faktyczny wydruk dzieje się w handlerzeYesHandlerprzezFiscalizer.- Ponowny wydruk dostępny tylko dla dokumentu z definicji fiskalizowanej, zatwierdzonego, z niepustym
SymbolKasy(czyli już raz zafiskalizowanego).
W73 — Dokument kompletacji (złożenie / rozłożenie kompletu)
Cel: obsłużyć kompletację „w locie" — rozbicie pozycji-kompletu na składniki (rozchód składników,
przychód wyrobu) wg kartoteki kompletacji. Worker DokumentKompletacjaWorker udostępnia przeliczenie
pozycji wg kartoteki, wycofujące ręczne zmiany użytkownika. To operacja w pełni lokalna (offline) —
testowalna, choć wymaga poprawnie skonfigurowanej definicji kompletacji i magazynu.
Warianty:
| Wariant | Mechanizm |
|---|---|
| Przelicz składniki/produkty wg kartoteki | DokumentKompletacjaWorker.PrzeliczWgKartoteki(dok) (akcja w menu Czynności) |
| Definicja dokumentu kompletacji | Definicja.SposobEdycjiKompletacji: SposobEdycjiKompletacji (≠ None) |
| Powiązanie składniki ↔ wyrób | dokumenty kompletacji rozchodu/przychodu z DefDokHandlowych |
| Powiązanie z obrotami | obroty rozchodowe składników i przychodowy wyrobu po Save |
Pola i typy:
DokumentKompletacjaWorker: akcjavoid PrzeliczWgKartoteki(DokumentHandlowy dokument). Wycofuje relacje podrzędne pozycji (pozycja.PodrzędneRelacje) i przelicza kompletację wg kartoteki.dok.Definicja.SposobEdycjiKompletacji: Soneta.Handel.SposobEdycjiKompletacji— gdyNone, akcja niewidoczna (IsVisiblePrzeliczWgKartoteki).- Definicje kompletacji w module:
hm.DefDokHandlowych.Kompletacja,.KompletacjaRozchód,.KompletacjaPrzychód(typuDefDokHandlowego). - Powiązania składników/wyrobu: pozycje dokumentu (
dok.Pozycje) i ich relacje (PozycjaDokHandlowego.PodrzędneRelacjetypuPozycjaRelacjiHandlowej).
Snippet:
using Soneta.Handel.Kompletacje;
// Dokument kompletacji „w locie" — definicja musi mieć SposobEdycjiKompletacji != None:
var dok = hm.DokHandlowe.WgNumer[...];
// Przeliczenie składników i wyrobu wg kartoteki kompletacji (OFFLINE, w transakcji wew. workera):
var worker = new DokumentKompletacjaWorker();
worker.PrzeliczWgKartoteki(dok); // wycofuje zmiany użytkownika, odtwarza komplet z kartoteki
session.Save(); // obroty składników (rozchód) i wyrobu (przychód) księgowane przy Save
// Sprawdzenie, czy dokument w ogóle obsługuje kompletację:
bool kompletacja = dok.Definicja.SposobEdycjiKompletacji != SposobEdycjiKompletacji.None;
Pułapki:
PrzeliczWgKartotekikasuje ręczne zmiany użytkownika w kompletacji i odtwarza komplet z kartoteki — to operacja jednokierunkowa, nie „aktualizacja przyrostowa".- Worker steruje wewnętrzną flagą
dok.BezKopiowania(włącza/wyłącza wtry/finally) — nie ustawiaj jej samodzielnie obok wywołania workera. - Akcja jest niewidoczna dla
SposobEdycjiKompletacji == Noneoraz dla dokumentu w stanieDetached/Deleted(IsVisiblePrzeliczWgKartoteki). - Obroty magazynowe (rozchód składników, przychód wyrobu) powstają dopiero po
Session.Save()— w teście zastosuj wzorzec „zapis →SaveDispose()→ odczyt na świeżej sesji" i pamiętaj o blokadzie stanu ujemnego w bazie Demo (składniki muszą mieć wcześniejszy zapisany przychód).
W74 — Intrastat (dane statystyczne i wyszukanie dokumentów do deklaracji)
Cel: uzupełnić na pozycjach dokumentu dane potrzebne do deklaracji Intrastat (kod CN, masa, kraj
pochodzenia, ilość w jednostkach uzupełniających) za pomocą DokumentHandlowyZmienIntrastatWorker, oraz
wyszukać dokumenty kwalifikujące się do deklaracji przywozu/wywozu za okres. Operacja jest w pełni lokalna
(offline) — testowalna.
Warianty:
| Wariant | Mechanizm |
|---|---|
| Aktualizacja danych Intrastat na pozycjach | DokumentHandlowyZmienIntrastatWorker.Update() (akcja „Aktualizuj dane dla Intrastatu ...") |
| Wybór aktualizowanych danych | DokumentHandlowyZmienIntrastatParams: KodCN, Masa, Kraj, Przelicznik (bool) |
| Rodzaj Intrastat na definicji | Definicja.Intrastat: RodzajIntrastat (NieUwzględniaj/Przywóz/Wywóz/PrzywózWPodrzędnym) |
| Typ deklaracji (przywóz/wywóz) | TypDeklaracji.IntrastatPrzywóz / IntrastatWywóz |
| Okres dokumentu do deklaracji | dok.OkresIntrastat (miesiąc deklaracji) |
Pola i typy:
DokumentHandlowyZmienIntrastatWorker: konstruktor(DokumentHandlowyZmienIntrastatParams @params)z[Context];[Context] Dokument: DokumentHandlowy,[Context(Required=false)] Dokumenty: DokumentHandlowy[],Params(read-only). Akcjaobject Update().DokumentHandlowyZmienIntrastatParams : ContextBase—KodCN: bool(„Kod CN"),Masa: bool(„Masa"),Kraj: bool(„Kraj pochodzenia"),Przelicznik: bool(„Ilość w jedn. uzupełn.").dok.Definicja.Intrastat: Soneta.Magazyny.RodzajIntrastat.dok.KierunekMagazynu: Soneta.Magazyny.KierunekPartii(kraj pochodzenia aktualizowany tylko, gdyKierunekMagazynu != Brak).- Wykonawcza metoda dokumentu:
dok.UaktualnijIntrastat(bool kodCN, bool masa, bool kraj, bool przelicznik): int(zwraca liczbę zaktualizowanych pozycji).
Snippet:
using Soneta.Deklaracje.UE;
using static Soneta.Deklaracje.UE.DokumentHandlowyZmienIntrastatWorker;
// Aktualizacja danych Intrastat na pozycjach dokumentu (OFFLINE — testowalne):
var ctx = session.GetEmptyContext();
ctx.TryAdd(() => dok);
var parametry = new DokumentHandlowyZmienIntrastatParams(ctx)
{
KodCN = true, // przepisz kod CN z kartoteki towaru
Masa = true, // przelicz masę pozycji
Kraj = true, // ustaw kraj pochodzenia
Przelicznik = true // ilość w jednostce uzupełniającej
};
var worker = new DokumentHandlowyZmienIntrastatWorker(parametry) { Dokument = dok };
worker.Update(); // aktualizuje pozycje; pomija dokumenty z Definicja.Intrastat == NieUwzględniaj
session.Save();
// Wyszukanie dokumentów do deklaracji za okres — filtr serwerowy po rodzaju Intrastatu i okresie:
var hm = session.GetHandel();
var okres = new FromTo(Date.Today.FirstDayMonth(), Date.Today.LastDayMonth());
foreach (DokumentHandlowy d in hm.DokHandlowe.WgNumer[(DokumentHandlowy d) =>
d.OkresIntrastat >= okres.From && d.OkresIntrastat <= okres.To])
{
bool przywoz = d.Definicja.Intrastat == RodzajIntrastat.Przywóz
|| d.Definicja.Intrastat == RodzajIntrastat.PrzywózWPodrzędnym;
// przywoz == true ⇒ TypDeklaracji.IntrastatPrzywóz, w przeciwnym razie IntrastatWywóz
}
Pułapki:
Update()rzucaApplicationException, gdy dokument zawiera koszty dodatkowe z podziałem wg masy (PodzialKosztuDodatkowego == Masa) a zaznaczono aktualizację masy — nie da się wtedy przeliczyć masy.- Dokumenty z
Definicja.Intrastat == RodzajIntrastat.NieUwzględniajsą pomijane (akcja niewidoczna —IsVisibleUpdate). - Kraj pochodzenia aktualizowany jest tylko, gdy
dok.KierunekMagazynu != KierunekPartii.Brak— sam parametrKraj=truenie wystarczy dla dokumentu bez ruchu magazynowego. - Jeśli istnieje już zatwierdzona deklaracja Intrastat za dany okres (
OkresIntrastat.LastDayMonth()), worker dopisze do logu ostrzeżenie, że dane nie zmienią się w zatwierdzonej deklaracji (trzeba wygenerować korektę) — aktualizacja na dokumencie i tak się wykona. - Wyszukiwanie dokumentów do deklaracji filtruj serwerowo po
OkresIntrastati rodzaju Intrastatu z definicji — nie ładuj całej tabeliDokHandlowedo pamięci (safe-code §6).
14. Płatności dokumentu handlowego
Płatności (należności i zobowiązania) powstają automatycznie z dokumentu handlowego płatnego (np. FV, FZ)
i opisują kwoty do uregulowania: termin, sposób zapłaty, ewidencję środków pieniężnych (ŚP) oraz stan
rozliczenia z zapłatami. Z poziomu dokumentu dostęp do nich daje kolekcja dok.Platnosci
(SubTable<Soneta.Kasa.Platnosc>). Pojedyncza płatność to obiekt Soneta.Kasa.Platnosc — w praktyce jedna
z dwóch klas konkretnych: Naleznosc (kierunek Przychod, sprzedaż) lub Zobowiazanie (kierunek
Rozchod, zakup). Wymagana referencja do Soneta.Kasa.
Pojęcia. Kwota płatności (
Kwota: Currency) jest w walucie dokumentu;KwotaKsiegi: Currencyto jej przeliczenie na PLN poKurs. Stan uregulowania toStanRozliczenia(+KwotaRozliczona,DoRozliczenia). Płatności są edytowalne wyłącznie, gdy dokument (i sama płatność) są w buforze — po zatwierdzeniu pola płatności stają się tylko do odczytu.
W75 — Przeglądanie płatności dokumentu
Cel: odczytać płatności wystawione z dokumentu — kwotę, walutę, sposób zapłaty, termin oraz stan rozliczenia — bez modyfikacji.
Warianty:
| Wariant | Źródło / pole |
|---|---|
| Lista płatności dokumentu | dok.Platnosci (SubTable<Platnosc>) |
| Kwota i waluta | p.Kwota: Currency (.Value, .Symbol) |
| Sposób zapłaty | p.SposobZaplaty: Soneta.Kasa.SposobZaplaty (.Nazwa, .Typ, .MPP) |
| Termin płatności | p.Termin: Date, p.TerminDni: int (dni od daty odniesienia) |
| Stan rozliczenia | p.StanRozliczenia, p.Rozliczono: bool, p.KwotaRozliczona, p.DoRozliczenia |
| Kwota nierozliczona po terminie | p.DoRozliczenia + warunek p.Termin < Date.Today |
| Należność / zobowiązanie | p.Kierunek, p.CzyNaleznosc: bool, p.CzyZobowiazanie: bool |
Pola i typy: Platnosc.Kwota: Soneta.Types.Currency, KwotaKsiegi: Currency (PLN),
SposobZaplaty: Soneta.Kasa.SposobZaplaty, Termin: Soneta.Types.Date, TerminDni: int,
StanRozliczenia: Soneta.Kasa.StanRozliczenia (Nierozliczony=0, Czesciowo=1, Calkowicie=2,
NiePodlega=3), Rozliczono: bool, KwotaRozliczona: Currency, DoRozliczenia: Currency,
Kierunek: Soneta.Kasa.KierunekPlatnosci, EwidencjaSP: Soneta.Kasa.EwidencjaSP.
Snippet:
var hm = session.GetHandel();
var dok = hm.DokHandlowe.WgDaty[...]; // lub inny lookup dokumentu
foreach (Platnosc p in dok.Platnosci)
{
Currency kwota = p.Kwota; // w walucie dokumentu
string waluta = p.Kwota.Symbol; // np. "PLN", "EUR"
string sposob = p.SposobZaplaty.Nazwa; // np. "Przelew", "Gotówka"
Date termin = p.Termin;
StanRozliczenia stan = p.StanRozliczenia;
// Kwota pozostała do zapłaty i to, co już przeterminowane:
Currency doZaplaty = p.DoRozliczenia;
bool poTerminie = !p.Rozliczono && p.Termin < Date.Today && p.DoRozliczenia > Currency.Zero;
}
Pułapki:
dok.PlatnoscitoSubTable— iteruj serwerowo, nie materializuj doListtylko po to, by policzyć elementy (IsEmpty/Countsą dostępne na kolekcji). Patrzrowcondition.md.StanRozliczenia.NiePodlegaoznacza płatność nierozliczaną (p.Rozliczana == false) — nie myl jej zNierozliczony(rozliczana, ale jeszcze niezapłacona).Kwotajest w walucie dokumentu; do raportu w PLN użyjKwotaKsiegi(W81), nie mnóż „ręcznie".- „Po terminie" liczysz z
TerminiDoRozliczeniawzględemDate.Today— w samej płatności nie ma gotowego pola „kwota po terminie".
W76 — Rozbicie płatności na raty
Cel: zamienić pojedynczą płatność dokumentu na zestaw rat (cyklicznych miesięcznych) albo na rozbicie
netto + VAT, przy użyciu publicznego workera PodzialPlatnosciWorker.
Warianty:
| Wariant | Ustawienie WParams |
|---|---|
| Raty miesięczne wg liczby rat | Metoda = WOptions.Raty, IlośćRat = n |
| Raty miesięczne wg kwoty raty | Metoda = WOptions.Raty, Kwota = kwotaRaty (worker wyliczy liczbę rat) |
| Rozbicie netto + VAT (MPP) | Metoda = WOptions.NettoPlusVat |
Pola i typy: worker Soneta.Handel.PodzialPlatnosci.PodzialPlatnosciWorker, parametry
Soneta.Handel.PodzialPlatnosci.WParams : ContextBase (inicjowane z Context zawierającego
DokumentHandlowy): Metoda: WOptions (NettoPlusVat=0x1, Raty=0x2), IlośćRat: int,
Kwota: Currency (kwota pojedynczej raty), TerminPierwszejWpłaty: Date (read-only — z warunków
płatności), Cykl: WOptions (Miesięczny). Akcja: PodzielPlatnosci([Context] DokumentHandlowy).
Snippet:
// Worker działa na dokumencie w BUFORZE z kierunkiem płatności (FV/FZ).
// Parametry tworzymy przez Context (wzorzec worker-z-Params), patrz worker-extender.md.
var context = new Context(session);
context.Set(dok); // DokumentHandlowy w kontekście
var wp = new PodzialPlatnosci.WParams(context)
{
Metoda = PodzialPlatnosci.WOptions.Raty,
IlośćRat = 3, // 3 równe raty miesięczne
};
var worker = new PodzialPlatnosci.PodzialPlatnosciWorker(wp);
worker.PodzielPlatnosci(dok); // sam otwiera transakcję i robi CommitUI
session.Save();
Pułapki:
- Akcja jest dostępna tylko gdy
dok.Bufor == trueidok.Definicja.KierunekPlatnosci != Brak(IsVisiblePodzielPlatnosci) — na zatwierdzonym dokumencie się nie wykona. PodzielPlatnoscisam otwiera transakcję (Session.Logout(true)+CommitUI) i usuwa istniejące płatności dokumentu, zastępując je wyliczonymi ratami/podziałem. Nie zawijaj go w drugą transakcję edycyjną; po nim wywołajsession.Save().- W trybie
RatyustawienieKwotaprzeliczaIlośćRat(i odwrotnie) — ustaw jedno z dwóch. - Ostatnia rata przejmuje resztę z zaokrągleń (kwoty rat sumują się do
BruttoCydokumentu) — nie zakładaj równego podziału co do grosza.
W77 — Ręczne dodanie / edycja pojedynczej płatności
Cel: ręcznie ułożyć płatności dokumentu — np. część gotówką, resztę przelewem — ustawiając sposób zapłaty, ewidencję ŚP, termin i kwotę.
Warianty:
| Wariant | Operacja |
|---|---|
| Dodanie należności (sprzedaż) | new Naleznosc(dok) + AddRow |
| Dodanie zobowiązania (zakup) | new Zobowiazanie(dok) + AddRow |
| Edycja istniejącej | zmiana pól na elemencie dok.Platnosci |
| Częściowo gotówka + przelew | dwie płatności o różnym SposobZaplaty, suma Kwota = wartość dokumentu |
Pola i typy: konstruktory Naleznosc(IDokumentPlatny), Zobowiazanie(IDokumentPlatny) (publiczne).
Tabela płatności: KasaModule.GetInstance(session).Platnosci. Pola zapisywalne:
SposobZaplaty: SposobZaplaty, EwidencjaSP: EwidencjaSP, Termin: Date (lub TerminDni: int),
Kwota: Currency, KwotaMPP: Currency, Rachunek: RachunekBankowyPodmiotu, Priorytet: int.
Snippet:
var kasa = KasaModule.GetInstance(session);
var spZaplaty = kasa.SposobyZaplaty;
using (var t = session.Logout(editMode: true)) // dokument MUSI być w buforze
{
// 1) część gotówką
var gotowka = new Naleznosc(dok); // sprzedaż -> Naleznosc; zakup -> Zobowiazanie
kasa.Platnosci.AddRow(gotowka);
gotowka.SposobZaplaty = spZaplaty.Gotówka;
gotowka.Kwota = new Currency(300m, "PLN");
gotowka.Termin = dok.DataDokumentu; // gotówka -> termin = data dokumentu
// 2) reszta przelewem
var przelew = new Naleznosc(dok);
kasa.Platnosci.AddRow(przelew);
przelew.SposobZaplaty = spZaplaty.WgNazwy["Przelew"];
przelew.Kwota = new Currency(dok.BruttoCy.Value - 300m, "PLN");
przelew.TerminDni = 14; // 14 dni od daty odniesienia
// przelew.Rachunek = ... // dla przelewu wskaż rachunek podmiotu
t.Commit(); // CommitUI() w workerze/extenderze
}
session.Save();
Pułapki:
- Płatność można dodać tylko do dokumentu w buforze —
OnAddedrzuca wyjątek („Nie można dodawać płatności do zatwierdzonego dokumentu").Platnosc.Bufor/IsReadOnlychronią edycję po zatwierdzeniu. - Dobierz klasę do kierunku dokumentu: sprzedaż (
KierunekPlatnosci.Przychod) →Naleznosc, zakup (Rozchod) →Zobowiazanie. Zła klasa = niespójny kierunek. KwotatoCurrency— twórznew Currency(wartość, symbolWaluty); symbol musi być zgodny z walutą dokumentu/ewidencji (weryfikator ostrzega o niezgodności).- Dla sposobu zapłaty typu „przelew" wymagany jest
Rachunek(weryfikator-ostrzeżenie). Ustaw rachunek należący do podmiotu płatności (twardy weryfikatorRachunekPodmiotuVerifier). SposobZaplatypobieraj z tabeli (kasa.SposobyZaplaty.Gotówka,...WgNazwy["Przelew"]) — to rekord konfiguracyjny, nie ustawiaj „z palca".
W78 — Warunki płatności z kontrahenta i ich przeliczenie na dokumencie
Cel: odczytać/ustawić warunki płatności dokumentu (sposób, termin w dniach, ewidencja ŚP) spójnie
z domyślnymi warunkami kontrahenta, przez publiczny WarunkiPłatnościWorker.
Warianty:
| Wariant | Mechanizm |
|---|---|
| Domyślne warunki z kontrahenta | Kontrahent.SposobZaplaty, Kontrahent.Termin (W9) — inicjują płatność |
| Odczyt warunków dokumentu | WarunkiPłatnościWorker: Sposób, TerminDni, Termin, EwidencjaSP, Kwota, Raty |
| Zmiana terminu (w dniach) | worker.TerminDni = n lub worker.Termin = data |
| Zmiana sposobu zapłaty | worker.Sposób = ... (przelicza też ewidencję ŚP) |
| Bezpośrednio na płatności | p.TerminDni, p.Termin, p.SposobZaplaty, p.EwidencjaSP |
Pola i typy: worker Soneta.Kasa.WarunkiPłatnościWorker (publiczny, zarejestrowany dla
IDokumentPlatny): [Context] Dokument: IDokumentPlatny, TerminDni: int, Termin: Date,
Sposób: SposobZaplaty, EwidencjaSP: EwidencjaSP, Kwota: Currency (read-only), Raty: int
(liczba płatności). Operuje na pierwszej płatności dokumentu. Na kontrahencie:
Kontrahent.SposobZaplaty: FormaPlatnosci, Kontrahent.Termin: int (patrz kontrahent W9).
Snippet:
// Warunki płatności kontrahenta są przenoszone na płatność przy jej tworzeniu/zmianie podmiotu.
// Do odczytu/zmiany "zbiorczej" warunków dokumentu służy WarunkiPłatnościWorker:
var context = new Context(session);
context.Set(dok); // dok : IDokumentPlatny (DokumentHandlowy)
var warunki = new WarunkiPłatnościWorker { Dokument = dok };
int dni = warunki.TerminDni; // termin liczony w dniach
SposobZaplaty sp = warunki.Sposób;
int liczbaRat = warunki.Raty;
using (var t = session.Logout(editMode: true)) // dokument w buforze
{
if (!warunki.IsReadOnlyTerminDni())
warunki.TerminDni = 21; // przelicza Termin na pierwszej płatności
if (!warunki.IsReadOnlySposób())
warunki.Sposób = session.GetKasa().SposobyZaplaty.WgNazwy["Przelew"];
t.Commit();
}
session.Save();
Pułapki:
WarunkiPłatnościWorkerdziała na pierwszej płatności i tylko gdyRaty <= 1(jedna płatność); przy wielu płatnościach (Raty > 1) pola są read-only (IsReadOnly...zwracajątrue) — wtedy edytuj poszczególne płatności bezpośrednio (W77) albo użyj podziału (W76).TerminDnito dni od daty odniesienia (TerminLiczonyOd/data dokumentu), nie data bezwzględna — ustawienieTerminDniprzeliczaTermin.- Edycja terminu może być zablokowana polityką (
IEdycjaTerminuPlatnosci) — zawsze sprawdzajIsReadOnlyTermin()/IsReadOnlyTerminDni()przed zapisem. - Zmiana
Sposóbprzelicza ewidencję ŚP (subewidencję) — nie ustawiajEwidencjaSP„obok", licz na spójność workera.
W79 — Zmiana płatnika (inny niż kontrahent)
Cel: ustawić na płatności podmiot inny niż kontrahent dokumentu (np. płatnik trzeci) i wykryć tę sytuację z poziomu dokumentu.
Warianty:
| Wariant | Mechanizm |
|---|---|
| Zmiana płatnika płatności | p.Podmiot = innyPodmiot (IPodmiotKasowy) |
| Wykrycie „innego płatnika" | dok.InnyPłatnik: bool (read-only — true, gdy jakaś płatność ma Podmiot != Kontrahent) |
| Płatnik domyślny kontrahenta | Kontrahent.Platnik: IPodmiotKasowy (kalkulowane — nadrzędny z relacji) |
Pola i typy: Platnosc.Podmiot: Soneta.Kasa.IPodmiotKasowy (zapisywalne),
DokumentHandlowy.InnyPłatnik: bool (kalkulowane, read-only),
IsReadOnlyPodmiot(). Kontrahent implementuje IPodmiotKasowy.
Snippet:
// "Inny płatnik" ustawiamy na poziomie POJEDYNCZEJ płatności — pole Podmiot:
IPodmiotKasowy platnik = session.GetCRM().Kontrahenci.WgKodu["PLATNIK"];
using (var t = session.Logout(editMode: true)) // dokument w buforze
{
foreach (Platnosc p in dok.Platnosci)
if (!p.IsReadOnlyPodmiot())
p.Podmiot = platnik; // rozrachunek przejdzie na nowy podmiot
t.Commit();
}
session.Save();
// Odczyt: czy dokument ma płatnika innego niż kontrahent:
bool inny = dok.InnyPłatnik; // kalkulowane, tylko do odczytu
Pułapki:
dok.InnyPłatnikjest wyłącznie do odczytu — to flaga wyliczana z porównaniap.Podmiotzdok.Kontrahent. Aby „zmienić płatnika", ustawPlatnosc.Podmiot, nie próbuj przypisaćInnyPłatnik.Podmiotjest read-only, gdy płatność jest częściowo rozliczona (KwotaRozliczona != 0) — sprawdzajIsReadOnlyPodmiot().- Zmiana podmiotu przenosi rozrachunek na nowy podmiot i może podmienić zablokowany podmiot na jego
zamiennik (wbudowana logika) — odczytaj
p.Podmiotpo zmianie, nie zakładaj wartości wejściowej. Rachunekmusi należeć do nowegoPodmiot(twardy weryfikator) — po zmianie płatnika zweryfikuj/wyczyść rachunek.
W80 — Odczyt stanu rozliczenia płatności
Cel: ustalić, czy płatność jest rozliczona w całości, częściowo czy nierozliczona, oraz dotrzeć do powiązanych rozliczeń (zapłat).
Warianty:
| Wariant | Pole / kolekcja |
|---|---|
| Stan zbiorczy | p.StanRozliczenia (Nierozliczony/Czesciowo/Calkowicie/NiePodlega) |
| Rozliczono całkowicie? | p.Rozliczono: bool, p.Zrealizowane: bool |
| Kwoty | p.KwotaRozliczona, p.DoRozliczenia |
| Data rozliczenia | p.DataRozliczenia: Date (Date.MaxValue = nierozliczona) |
| Rozliczono na dzień | p.RozliczonoDoDnia(Date data) |
| Powiązane rozliczenia/transakcje | p.Dokumenty, p.Zaplaty (kolekcje RozliczenieSP) |
| Czy podlega rozliczeniu | p.Rozliczana: bool |
Pola i typy: StanRozliczenia: Soneta.Kasa.StanRozliczenia, Rozliczono: bool, Zrealizowane: bool,
KwotaRozliczona/DoRozliczenia: Currency, DataRozliczenia: Date, Rozliczana: bool,
Dokumenty/Zaplaty (rozliczenia typu Soneta.Kasa.RozliczenieSP),
metoda RozliczonoDoDnia(Date, bool wgDatyKsi = false): Currency.
Snippet:
foreach (Platnosc p in dok.Platnosci)
{
switch (p.StanRozliczenia)
{
case StanRozliczenia.Calkowicie: /* zapłacona w całości */ break;
case StanRozliczenia.Czesciowo: /* część zapłacona: p.DoRozliczenia > 0 */ break;
case StanRozliczenia.Nierozliczony: /* brak zapłat */ break;
case StanRozliczenia.NiePodlega: /* płatność nierozliczana */ break;
}
Currency zaplaconoDoDzis = p.RozliczonoDoDnia(Date.Today);
// Powiązane rozliczenia (transakcje zapłaty):
foreach (RozliczenieSP r in p.Zaplaty) { /* r.Data, r.KwotaDokumentu, ... */ }
foreach (RozliczenieSP r in p.Dokumenty) { /* r.Data, r.KwotaZaplaty, ... */ }
}
Pułapki:
StanRozliczeniajest kalkulowane zKwotaRozliczona/Kwota— nie ustawiaj go; rozliczenia powstają przez operacje kasowe/rozliczeniowe, nie przez bezpośredni zapis na płatności.DataRozliczenia == Date.MaxValueoznacza „nierozliczona" — nie traktujMaxValuejako realnej daty.- Rozliczenia są rozdzielone na dwie kolekcje (
DokumentyiZaplaty) zależnie od strony powiązania — do pełnego obrazu przejrzyj obie. - Dla płatności
Rozliczana == false(NiePodlega)DoRozliczeniawynosi zero — nie analizuj jej jak zaległości.
W81 — Płatności w walucie obcej (kwota w walucie vs PLN, kurs)
Cel: poprawnie odczytać/ustawić płatność walutową — kwotę w walucie obcej, jej przeliczenie na PLN oraz kurs i tabelę kursową.
Warianty:
| Wariant | Pole |
|---|---|
| Kwota w walucie dokumentu | p.Kwota: Currency (symbol = waluta, np. „EUR") |
| Kwota w PLN (księgowa) | p.KwotaKsiegi: Currency |
| Kurs i tabela | p.Kurs: double, p.TabelaKursowa: TabelaKursowa |
| Interfejs walutowy | IRowWithKurs: KwotaWaluty (= Kwota), KwotaPLN (= KwotaKsiegi) |
| Słownie | p.Słownie: string |
Pola i typy: Kwota: Currency (waluta dokumentu), KwotaKsiegi: Currency (PLN),
Kurs: double, TabelaKursowa: Soneta.Waluty.TabelaKursowa. Platnosc implementuje
Soneta.Waluty.IRowWithKurs (KwotaWaluty, KwotaPLN).
Snippet:
foreach (Platnosc p in dok.Platnosci)
{
if (p.Kwota.Symbol != Currency.SystemSymbol) // płatność walutowa (np. "EUR")
{
Currency wWalucie = p.Kwota; // np. 1000 EUR
Currency wPln = p.KwotaKsiegi; // przeliczenie na PLN
double kurs = p.Kurs; // kurs zastosowany
TabelaKursowa tab = p.TabelaKursowa; // tabela kursów (lub null)
}
}
// Ustawienie kursu ręcznie (gdy dokument/ewidencja walutowa, w buforze):
using (var t = session.Logout(editMode: true))
{
foreach (Platnosc p in dok.Platnosci)
if (p.Kwota.Symbol != Currency.SystemSymbol && !p.IsReadOnlyTabelaKursowa())
p.TabelaKursowa = session.GetKasa().EwidencjeSP /* ... */ ?.TabelaKursowa;
t.Commit();
}
session.Save();
Pułapki:
- Dla płatności w PLN
Kurs == 1.0iTabelaKursowa == null— przeliczeniem zajmuj się tylko, gdyKwota.Symbol != Currency.SystemSymbol. KwotaKsiegiwylicza się zKwota * Kurs; jeśli ustawisz tabelę bez kursu na datę dokumentu, kurs może pozostać0.0(brak kursu) — wtedyKwotaKsiegibędzie zerowa. Upewnij się, że tabela kursowa ma kurs naDataDokumentu(w bazie Demo brak kursów „na dziś" → operacja walutowa rzucaKursWalutyNotFoundException, por. rozdz. o walutach).- Kwota płatności walutowej musi mieć symbol zgodny z walutą dokumentu/ewidencji ŚP — weryfikator ostrzega o niezgodności symboli.
- Sumę płatności w PLN czytaj z
KwotaKsiegi(lubIRowWithKurs.KwotaPLN), nie przeliczajKwotawłasnym kursem.
W82 — Powiązanie płatności z terminem i rabatem za wcześniejszą zapłatę
Cel: obsłużyć rabat za wcześniejszą zapłatę (skonto) — wskazać termin uprawniający do rabatu i odczytać jego wpływ na warunki płatności dokumentu.
Warianty:
| Wariant | Mechanizm |
|---|---|
| Ustawienie terminu rabatu na dokumencie | dok.RabatZaTerminPlatnosci.Termin = data |
| Odczyt naliczonego rabatu | dok.RabatZaTerminPlatnosci.Rabat: Percent |
| Rodzaj rabatu | dok.RabatZaTerminPlatnosci.Rodzaj: RodzajRabatuZaTerminPlatnosci |
| Termin samej płatności | p.Termin, p.TerminDni (W77/W78) |
| Parametry rabatu na kontrahencie | Kontrahent.RodzajRabatuZaTerminPlatnosci, TrybRabatu..., IloscDniDlaRabatu, WartoscRabatuZaKazdyDzien |
Pola i typy: DokumentHandlowy.RabatZaTerminPlatnosci: Soneta.Handel.RabatZaTerminPlatnosci
(subrow) z polami Termin: Date (zapisywalne — termin uprawniający do rabatu), Rabat: Percent
(wyliczane), Rodzaj: RodzajRabatuZaTerminPlatnosci. Na płatności: Termin: Date,
TerminDni: int, TerminLiczonyOd: Date (data odniesienia, read-only).
Snippet:
using (var t = session.Logout(editMode: true)) // dokument w buforze, z kontrahentem
{
// Termin uprawniający do rabatu za wcześniejszą zapłatę (skonto):
if (!dok.RabatZaTerminPlatnosci.IsReadOnlyTermin())
dok.RabatZaTerminPlatnosci.Termin = dok.DataDokumentu.AddDays(7);
t.Commit();
}
session.Save();
// Odczyt naliczonego rabatu (zależny od parametrów rabatu kontrahenta):
Percent rabat = dok.RabatZaTerminPlatnosci.Rabat;
Date terminRabatu = dok.RabatZaTerminPlatnosci.Termin;
Pułapki:
RabatZaTerminPlatnosci.Rabatjest wyliczany z parametrów kontrahenta (tryb: progresywny / podstawowy / progowy) i różnicy dni międzyTerminrabatu a terminem płatności — nie ustawiaj go wprost.- Ustawienie
Termin<Date.Todayzeruje rabat i czyści termin — przekazuj datę przyszłą. - Termin rabatu można ustawić tylko, gdy wszystkie płatności dokumentu mają ten sam termin
(
Dokument.Platnoscizgrupowane poTermin→ jedna grupa); w przeciwnym razie rzucaRowException. - Edycja może być zablokowana polityką
IEdycjaTerminuPlatnosci— sprawdzajIsReadOnlyTermin(). - Naliczenie rabatu wymaga skonfigurowanych parametrów na kontrahencie
(
RodzajRabatuZaTerminPlatnosci,Tryb..., progi/wartości) — bez nichRabatpozostaniePercent.Zero.
Powiązane dokumenty
safe-code.md— sesja, transakcje, blokada optymistyczna, zasady bezpiecznego kodu.session-login.md—Session,Login,Database.worker-extender.md— workery, akcje menu Czynności, bindowanie.rowcondition.md— serwerowy LINQ,RowCondition,SubTable[condition].features.md— cechy (Features), typy, dostęp typowany/nietypowany.datapack-guidedrow.md— eksport/import,GuidedRow.kontrahent.md— receptury dlaKontrahent(nabywca/odbiorca/płatnik dokumentu).scan-props.md/scan-workers.md— inwentaryzacja pól i workerów.