Files

396 lines
19 KiB
Markdown

# 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<PozycjaDokHandlowego>` (sortowane po `Lp`).
- **Pozycje danego towaru (historia obrotu):** `towar.Pozycje``SubTable<PozycjaDokHandlowego>`
(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<DokumentHandlowy>` — ł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<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:**
```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<DokumentHandlowy>`) — 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<PozycjaDokHandlowego>`.
**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<DokumentHandlowy>(
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<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:**
```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`**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`.
---