# HANDEL05 — Odczyt i wyszukiwanie > Wspólne fakty o typie, podstawowe typy i szablon wzorca: [../handel.md](../handel.md). 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`](../safe-code.md), > a mechanikę warunków serwerowych [`rowcondition.md`](../rowcondition.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 > rzuci `LinqConditionException`. **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`, oraz `PrimaryKey`. **Nie ma** „gołego" klucza `WgKontrahenta` ani `WgNumeru` — filtruj wyrażeniem na dowolnym z powyższych kluczy (sortowanie bierze się z wybranego klucza). - **Indeksator po Guid:** `hm.DokHandlowe[guid]` (zwraca `DokumentHandlowy`; **rzuca `RowNotFoundException`** dla nieznanego Guid). - **Pozycje dokumentu:** `dok.Pozycje` — `LpSubTable` (sortowane po `Lp`). - **Pozycje danego towaru (historia obrotu):** `towar.Pozycje` — `SubTable` (klucz `WgTowar`). Klucze na `PozycjeDokHan`: `WgDaty` (`Data`), `WgKierunek` (`Towar`, `KierunekMagazynu`, `Data`, `Czas`), `WgTowarDokumentu` (`Towar`, `Dokument`). - **Numer dokumentu:** pole `dok.Numer: NumerDokumentu`. Pełny numer do **odczytu** to `dok.Numer.NumerPelny` (kalkulowane). W warunku serwerowym używaj pola bazodanowego `Numer.Pelny` (np. `dok => dok.Numer.Pelny == "FV 1/2026"`). - **Korekty:** `dok.DokumentKorygowany` (dokument korygowany przez tę korektę), `dok.DokumentyKorygujące` (`IEnumerable` — łańcuch korekt tego dokumentu), `dok.Korekta: bool` (pole bazodanowe — czy dokument jest korektą). Wszystkie powiązania korekt to pola **kalkulowane** (oprócz `Korekta`). - **Kolekcje na `Kontrahent` (z modułu CRM):** `k.DokumentyHandlowe` i `k.DokumentyHandloweOdbiorcy` to **nietypowane** `SubTable` (CRM nie referuje Handlu). Iteracja działa, ale typowane filtrowanie serwerowe rób od strony Handlu: `hm.DokHandlowe.WgKontrahentaObcy[dok => dok.Kontrahent == k]`. --- ### HANDEL-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:** ```csharp var hm = session.GetHandel(); var dok = hm.DokHandlowe[guid]; // dokument wczytany po Guid (HANDEL-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:** - `Ilosc` to `Quantity`, a `Cena`/`WartoscCy` to `DoubleCy`/`Currency` (kwota + waluta), **nie** `decimal`/`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 warunek `dok.Pozycje[p => …]` — wykona się serwerowo. - `p.Suma`/`p.WartoscCy` są przeliczane przez platformę — czytaj je, nie wyliczaj „ręcznie". - `p.Towar` bywa `null` dla pozycji nietowarowych (opis/koszt) — zabezpiecz dostęp (`?.`). --- ### HANDEL-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` | | 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:** ```csharp 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.DokumentyHandlowe` jest **nietypowane** (`SubTable`, nie `SubTable`) — pętla `foreach (DokumentHandlowy …)` działa, ale do filtrowania wyrażeniem LINQ użyj kolekcji od strony Handlu (`hm.DokHandlowe.WgXxx[…]`), gdzie typ wiersza jest znany kompilatorowi. - `Kontrahent` i `Odbiorca` to **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 po `Kod` — referencja generuje szybkie `JOIN` po `ID`. --- ### HANDEL-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`. **Snippet:** ```csharp 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 globalnie `PozycjeDokHan` — to jedna z największych tabel operacyjnych (safe-code §6.3). - Warunek przez referencję (`p.Dokument.Stan == …`) jest dozwolony — `Stan` jest polem bazodanowym i wygeneruje `JOIN`. Nie używaj w warunku pól kalkulowanych dokumentu (np. `p.Dokument.Zatwierdzony` rzuci `LinqConditionException`). - „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 w `Soneta.Magazyny` — wymagana referencja do modułu Magazyny. --- ### HANDEL-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 `||` w jednym wyrażeniu | **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:** ```csharp 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( dok => dok.Definicja == def && dok.Seria == "A"); foreach (DokumentHandlowy dok in hm.DokHandlowe.WgDaty[cond]) { /* ... */ } ``` **Pułapki:** - **Nie używaj `View`** w kodzie biznesowym (to obiekt UI) — filtruj `SubTable[expression]` lub `RowCondition.FromExpression` ([`rowcondition.md`](../rowcondition.md)). - Porównuj definicję/magazyn po **rekordzie** (`dok.Definicja == def`), nie po stringu symbolu — rekord pobierz raz przez `WgSymbolu[...]`/`WgSymbol[...]` poza pętlą. - Stan porównuj enumem (`dok.Stan == StanDokumentuHandlowego.Zatwierdzony`); skróty `dok.Zatwierdzony` są kalkulowane i **nie wolno** ich użyć w warunku LINQ. - Wybór klucza (`WgDaty`, `WgMagazynuNumer`, `WgKontrahentaObcy`) decyduje tylko o **sortowaniu** — warunek i tak trafia do `WHERE`. Dla dużych zbiorów dobierz klucz pasujący do oczekiwanej kolejności. --- ### HANDEL-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:** ```csharp 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 kalkulowane `dok.Numer.NumerPelny` — w wyrażeniu serwerowym rzuciłoby `LinqConditionException`. - 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 po `Guid` (z `GuidedTable`) — dla nieznanego `Guid` **rzuca `Soneta.Business.RowNotFoundException`** (NIE zwraca `null`). Gdy brak pewności istnienia, obuduj go `try/catch`. Nie myl z dostępem po `ID` (klucz wewnętrzny tabeli). - Numer obcy (dostawcy) jest w `dok.Obcy.Numer` — to inne pole niż własny `Numer`. --- ### HANDEL-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` (ł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` | **Pola i typy:** `dok.Korekta: bool` (bazodanowe — czy dokument jest korektą), `dok.DokumentKorygowany: DokumentHandlowy`, `dok.DokumentyKorygujące: IEnumerable`, `dok.DokumentKorygujący`/`DokumentKorygującyOstatni: DokumentHandlowy`, `dok.DokumentyKorygowane: IEnumerable` (cały łańcuch korygowanych) — wszystkie powiązania **kalkulowane** (tylko do odczytu; korekty zakładaj przez `IRelacjeService`). **Snippet:** ```csharp 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ący` są **kalkulowane** (liczone z relacji handlowych) — tylko do odczytu. Tworzenie korekt realizuje `IRelacjeService.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`. - `DokumentKorygowany` zwraca `null`, gdy dokument **nie** jest korektą (`Korekta == false`) — zawsze sprawdź `dok.Korekta` albo `!= null` przed użyciem. - `DokumentyKorygujące` to **łańcuch** (korekta korekty korekty…), a nie pojedynczy element — gdy potrzebujesz tylko najbliższej, użyj `DokumentKorygujący`; gdy ostatniej — `DokumentKorygującyOstatni`. ---