# Kontrahent — receptury kodu biznesowego (Soneta / enova365) Zbiór gotowych wzorców kodu dla obiektu biznesowego **`Soneta.CRM.Kontrahent`** (tabela `Kontrahenci`). Dokument jest częścią skilla `soneta-programming`. Celem jest, aby agent pisał **bezbłędny kod biznesowy** operujący na kontrahencie — 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 w [`safe-code.md`](../safe-code.md), [`session-login.md`](../session-login.md) oraz > [`worker-extender.md`](../worker-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żenia `switch`, > nazwane parametry `bool`). Snippety operują wyłącznie na **publicznym kontrakcie** platformy — nie > ma odwołań do prywatnych klas ani kodu źródłowego aplikacji. ## Fakty o typie (zweryfikowane skanem DLL — `scan-props.csx`) - **Klasa biznesowa:** `Soneta.CRM.Kontrahent` — `GuidedRow` (root), tabela `Soneta.CRM.Kontrahenci`. - **Implementuje:** `IPodmiot`, `IKontrahent`, `IPodmiotKasowy`, `IElementSlownika`, `IAdresHost`, `IKodowany`, `IAdresyWWWHost`, `IDaneKontaktoweHost`, `IEmailElement`, `IRegonHost`, `IGIODOZgodnyHost`, `IGIODOWymianaDanychHost`, `IGIODOOświadczenieHost`. - **Pola:** 75 bazodanowych + 142 kalkulowane. - **Moduł:** `Soneta.CRM.CRMModule`, dostęp `session.GetCRM()`. Tabela: `crm.Kontrahenci`. - **Kluczowe pola bazodanowe (zapisywalne):** `Kod: string`, `Nazwa: string`, `NIP: string`, `EuVAT: string`, `PESEL: string`, `REGON: string`, `KRS: string`, `StatusPodmiotu: Soneta.Core.StatusPodmiotu`, `RodzajPodmiotu: Soneta.Core.RodzajPodmiotu` (= „Rodzaj VAT dla sprzedaży"), `RodzajPodmiotuZakup: Soneta.Core.RodzajPodmiotu`, `PodatnikVAT: bool`, `VATLiczonyOd: Soneta.CRM.VatKontahentaLiczonyOd`, `FormaPrawna: Soneta.CRM.FormaPrawna`, `Waluta: Soneta.Waluty.Waluta`, `SposobZaplaty: Soneta.Kasa.FormaPlatnosci`, `Termin: int`, `TerminPlanowany: int`, `LimitKredytu: Currency`, `TypLimituKredytowego: Soneta.CRM.TypLimituKredytowego`, `KontrolaKwota: Currency`, `KontrolaDni: int`, `TypPrzeterminowania: Soneta.CRM.TypLimituKredytowego`, `Blokada: bool`, `BlokadaSprzedazy: bool`, `Zamiennik: Kontrahent`, `EFaktura: Soneta.Core.EFaktura`, `EFakturaOkres: FromTo`, `NieWindykowac: bool`, `DefinicjaSprawyWindykacyjnej`, `OddzialFirmy`, `Region`, `Rabat: Percent`, `DomyslnySzablonPolOpcjonalnychKSeF`, `KSeFSposobObslugiWysylkiCeny`. - **Pola złożone:** `Adres: Soneta.Core.Adres`, `AdresDoKorespondencji: Soneta.Core.Adres`, `Kontakt: Soneta.Core.Kontakt` (`Kontakt.EMAIL`, `Kontakt.TelefonKomorkowy`, `Kontakt.WWW`, `Kontakt.SkrytkaPocztowa`, `Kontakt.Skype`), `OdsKarne: Soneta.Kasa.OdsetkiKarne`. - **Pola kalkulowane (tylko do odczytu):** `Nazwa` jest zapisywalna, ale `NazwaFormatowana`, `NazwaPierwszaLinia`, `KodKraju`, `JestIncydentalny`, `IsStandard`, `DomyslnyRachunek`, `Platnik`, `LimitNieograniczony`, `PrzeterminowanieNieograniczone`, `KontrolaAktywna`, `AktualnyStatusVAT`, `AktualnyStatusVATMF`, `AktualnyStatusVATVies` — **nie ustawiaj** ich bezpośrednio. - **Kolekcje (`SubTable`):** `Osoby` (`SubTable`), `Kontakty` (`SubTable`), `AdresyWWW` (`SubTable`), `Kategorie` (`SubTable`), `Branze` (`SubTable`), `Opiekunowie` (`SubTable`), `Rachunki` (`SubTable`), `Rozrachunki` (`SubTable`), `Podrzedni` (`SubTable`), `StatusyVAT` (`SubTable`), `KodyKreskowe` (`SubTable`), `GIODOOświadczenia` (`SubTable`), `GIODOUdostępnienia` (`SubTable`), `PotwierdzeniaGIODO` (`SubTable`). - **Cechy:** `Features: Soneta.Business.FeatureCollection` (indeksator po nazwie definicji cechy). ## 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. - **Pułapki** — typowe błędy i zasady safe-code. --- ## 1. Wyszukiwanie i identyfikacja ### W1 — Wyszukiwanie kontrahenta **Cel:** odnaleźć istniejącego kontrahenta po wybranym kluczu, zanim zaczniemy go modyfikować lub zanim utworzymy nowy rekord. **Warianty:** | Wariant | Klucz | Uwaga | |---|---|---| | Po kodzie | `Kod` | indeks `WgKodu`, klucz unikalny — zwraca pojedynczy rekord | | Po nazwie / fragmencie | `Nazwa` | indeks `WgNazwy` (nieunikalny) lub `SubTable[pattern]` | | Po NIP / EU VAT | `NIP`, `EuVAT` | normalizacja: `Nip.Flat` / `EuVat.Flat` przed porównaniem | | Po adresie | `Adres.*` | miejscowość, kod pocztowy, ulica | | Po PESEL / REGON / KRS | `PESEL`, `REGON`, `KRS` | osoby fizyczne / podmioty | | Dedup przed dodaniem | `NIP` | sprawdzenie, czy podmiot już istnieje | | Kontrahent incydentalny | `JestIncydentalny` | systemowy rekord (`Kontrahent.INCYDENTALNY`) | **Pola i typy:** `Kod: string`, `NIP: string`, `EuVAT: string`, `Nazwa: string`, `Adres: Soneta.Core.Adres`, `PESEL/REGON/KRS: string`, `JestIncydentalny: bool`. **Snippet:** ```csharp var crm = session.GetCRM(); // 1. Po kodzie — klucz unikalny, zwraca pojedynczy rekord lub null Kontrahent poKodzie = crm.Kontrahenci.WgKodu["ABC"]; // 2. Po nazwie — indeks nieunikalny, zwraca zbiór; bierzemy pierwszy Kontrahent poNazwie = crm.Kontrahenci.WgNazwy["Firma XYZ"].FirstOrDefault(); // 3. Po NIP — filtr serwerowy; warunek aplikujemy na indeksie. Porównania tekstowe są case-insensitive var nip = Nip.Flat("123-456-32-18"); // usuwa myślniki Kontrahent poNip = crm.Kontrahenci.WgNIP[(Kontrahent k) => k.NIP == nip].FirstOrDefault(); // 4. Po fragmencie nazwy / mieście — serwerowy LIKE (warunek na indeksie WgNazwy) foreach (Kontrahent k in crm.Kontrahenci.WgNazwy[(Kontrahent k) => k.Nazwa.Contains("bud") && k.Adres.Miejscowosc == "Kraków"]) { // ... } // 5. Dedup przed dodaniem nowego kontrahenta bool juzIstnieje = crm.Kontrahenci.WgNIP[(Kontrahent k) => k.NIP == nip].Any(); ``` **Pułapki:** - `WgKodu[...]` zwraca pojedynczy rekord (klucz unikalny) — może być `null`. `WgNazwy[...]` zwraca **zbiór** (klucz nieunikalny), trzeba `.FirstOrDefault()`/iterację. - **Nie iteruj całej tabeli** `Kontrahenci` z `if` w pamięci — to tabela kartotekowa (rośnie z biznesem). Filtruj przez warunek aplikowany **na indeksie**, np. `crm.Kontrahenci.WgKodu[(Kontrahent k) => …]` (warunek wykonywany przez SQL). Indeksator samej tabeli (`crm.Kontrahenci[…]`) służy do dostępu po `ID`/kluczu, nie przyjmuje wyrażenia LINQ. Patrz [`rowcondition.md`](../rowcondition.md) i [`safe-code.md`](../safe-code.md) §6. - W `RowCondition` (wyrażeniu LINQ) wolno użyć **tylko pól bazodanowych**. `NazwaFormatowana`, `KodKraju`, `Platnik` są kalkulowane → rzucą `LinqConditionException`. - Porównania tekstowe w warunku są **case-insensitive** — nie dubluj `ToLower()`. - Przed porównaniem NIP/EU VAT normalizuj wejście (`Nip.Flat`, `EuVat.Flat`), bo w bazie bywają formaty z myślnikami i bez. ### W2 — Walidacja NIP / REGON / EU VAT **Cel:** sprawdzić poprawność NIP/REGON (suma kontrolna) i EU VAT (format/kraj) przed zapisem, niezależnie od weryfikacji online (W15). **Warianty:** | Wariant | Wejście | Metoda publiczna | |---|---|---| | NIP krajowy | 10 cyfr lub `DDD-DDD-DD-DD` | `Soneta.Core.Nip.Test(string)` | | REGON 9/14 | 9 lub 14 cyfr | `Soneta.Core.Regon.Test(string)` | | EU VAT | prefiks kraju + numer | `Soneta.Core.EuVat.Test(string, ISessionable)` | | Normalizacja | usunięcie myślników/spacji | `Nip.Flat`, `Nip.Format`, `EuVat.Flat` | | Rozbicie EU VAT | kraj + numer | `EuVat.Split(value, out country, out nip)` | **Pola i typy:** `NIP: string`, `REGON: string`, `EuVAT: string`. Walidatory są **statyczne**; `EuVat.Test` wymaga `ISessionable` (sprawdza listę krajów UE w bazie). **Snippet:** ```csharp // Walidatory rzucają NullReferenceException dla null — najpierw odsiej puste wejście. if (!nip.IsNullOrEmpty() && Nip.Test(nip)) { /* NIP poprawny */ } if (!regon.IsNullOrEmpty() && Regon.Test(regon)) { /* REGON poprawny */ } if (!euVat.IsNullOrEmpty() && EuVat.Test(euVat, session)) { /* EU VAT poprawny */ } // Rozbicie EU VAT "PL1234563218" -> kraj "PL", numer "1234563218" EuVat.Split(euVat, out string kodKraju, out string numer); // Walidacja w event-handlerze zapisu (rzut PRZED Commit/Save): if (!kontrahent.NIP.IsNullOrEmpty() && !Nip.Test(kontrahent.NIP)) throw new RowException(kontrahent, "Nieprawidłowy NIP".Translate(), nameof(kontrahent.NIP)); ``` **Pułapki:** - `Nip.Test`, `Regon.Test`, `EuVat.Test` **rzucają `NullReferenceException` dla `null`** (odwołują się do `.Length`). Zawsze najpierw sprawdź `IsNullOrEmpty`. - To walidacja **formatu/sumy kontrolnej**, a nie weryfikacja w MF/VIES — patrz W15. - Komunikaty walidacyjne rzucaj jako `RowException(row, "…".Translate(), nameof(Pole))` **przed** `Commit()` (safe-code §5.1). Wyjątek po `Commit()` nie wycofa zmiany z sesji. - Ustawienie `NIP`/`EuVAT` na samym `Kontrahent` uruchamia wbudowaną synchronizację (NIP↔EuVAT, auto-zmiana `RodzajPodmiotu`) — własna walidacja jest dodatkiem, nie zastępstwem. --- ## 2. Tworzenie, modyfikacja, usuwanie ### W3 — Tworzenie kontrahenta **Cel:** utworzyć nowy rekord kontrahenta z poprawnym minimalnym zestawem pól i wartościami domyślnymi. **Warianty:** | Wariant | Charakterystyka | Pola krytyczne | |---|---|---| | Podmiot gospodarczy krajowy | firma w PL | `StatusPodmiotu=PodmiotGospodarczy`, `RodzajPodmiotu=Krajowy`, `NIP` | | Unijny / zagraniczny | sprzedaż wewn.-unijna / eksport | `EuVAT`, `RodzajPodmiotu=Unijny/Eksportowy` | | Osoba fizyczna / finalny | konsument | `StatusPodmiotu=Finalny`, `PESEL` | **Pola i typy:** `Kod: string`, `Nazwa: string`, `StatusPodmiotu: Soneta.Core.StatusPodmiotu` (`PodmiotGospodarczy=0`, `Finalny=1`), `RodzajPodmiotu: Soneta.Core.RodzajPodmiotu` (`Krajowy=0`, `Eksportowy=1`, `EksportowyPodróżny=2`, `Unijny=3`, `UnijnyTrójstronny=4`, `BezVAT=5`), `PodatnikVAT: bool`, `FormaPrawna: Soneta.CRM.FormaPrawna`. **Nadawanie kodu / numeracji:** `Kod` jest polem tekstowym ustawianym jawnie. Może być wymagana jego unikalność (zależnie od konfiguracji modułu CRM); w razie kolizji `Save()` zgłosi `RowException` z `DuplicateKeyException` w `InnerException`. **Snippet:** ```csharp var crm = session.GetCRM(); using (var t = session.Logout(editMode: true)) { var k = new Kontrahent(); crm.Kontrahenci.AddRow(k); // najpierw dodaj do tabeli, potem ustawiaj pola k.Kod = "FIRMA001"; k.Nazwa = "Firma XYZ Sp. z o.o."; k.StatusPodmiotu = StatusPodmiotu.PodmiotGospodarczy; k.RodzajPodmiotu = RodzajPodmiotu.Krajowy; k.PodatnikVAT = true; k.NIP = "1234563218"; // ustawienie NIP synchronizuje EuVAT t.Commit(); // Commit() w kodzie biznesowym } session.Save(); // zapis do bazy; tu wykryte konflikty/duplikaty ``` **Pułapki:** - Tworzenie **wyłącznie w transakcji** (`session.Logout(editMode: true)`). `AddRow` przed ustawianiem pól. - W workerze/extenderze (uruchamianym z UI) używaj `t.CommitUI()` zamiast `t.Commit()` (safe-code, [`worker-extender.md`](../worker-extender.md)). - `Nazwa` jest zapisywalna; `NazwaFormatowana`/`NazwaPierwszaLinia` są kalkulowane — nie ustawiaj. - Dla podmiotu unijnego ustaw `EuVAT` (z prefiksem kraju) — platforma sama dostosuje `RodzajPodmiotu`. - Brak `Commit()` = automatyczny rollback przy `Dispose()`. ### W4 — Modyfikacja i statusy **Cel:** zmienić dane istniejącego kontrahenta lub jego status dostępności/handlowy. **Warianty:** | Wariant | Pole / operacja | |---|---| | Edycja danych identyfikacyjnych | `Kod`, `Nazwa`, `NIP`, … (blokada optymistyczna) | | Ukrycie na listach | `Blokada: bool` | | Blokada sprzedaży | `BlokadaSprzedazy: bool` | | Zmiana formy prawnej | `FormaPrawna` (poj. lub masowo: worker `ZmienFormePrawnaKontrahentowWorker`) | | Zastąpienie (zamiennik) | `Zamiennik: Kontrahent` (ustawia automatycznie `Blokada=true`) | | Kopiowanie kontrahenta | worker `Soneta.CRM.KopiujKontrahentaWorker` (akcja „Kopiuj kontrahenta...") | **Pola i typy:** `Blokada: bool`, `BlokadaSprzedazy: bool`, `FormaPrawna: Soneta.CRM.FormaPrawna`, `Zamiennik: Soneta.CRM.Kontrahent`. **Snippet:** ```csharp var crm = session.GetCRM(); var k = crm.Kontrahenci.WgKodu["FIRMA001"]; if (k == null) return; using (var t = session.Logout(editMode: true)) { k.Nazwa = "Firma XYZ S.A."; k.BlokadaSprzedazy = true; // zakaz wystawiania dokumentów rozchodu k.Blokada = true; // ukrycie na listach t.Commit(); } session.Save(); // Kopiowanie kontrahenta — programowe użycie workera (bez UI): var kopiarka = new KopiujKontrahentaWorker { Kontrahent = k }; using (var t = session.Logout(editMode: true)) { Kontrahent nowy = kopiarka.Kopiuj(); t.Commit(); } session.Save(); ``` **Pułapki:** - **Blokada optymistyczna**: konflikt edycji (ktoś inny zapisał ten rekord) wybucha w `session.Save()` jako `RowConflictException` — obsłuż go (refresh + retry lub eskalacja), nie połykaj (safe-code §4). - Nie nadpisuj `Kod` rekordów standardowych (`IsStandard == true`) ani incydentalnego (`JestIncydentalny == true`). - `Zamiennik` ma efekt uboczny — ustawienie zamiennika włącza `Blokada=true`. Do rozwiązania „aktualnego" kontrahenta służy `Kontrahent.Coalesce(k)` (zwraca zamiennika albo sam rekord). - Worker `KopiujKontrahentaWorker` ma property `[Context] Kontrahent` — przy ręcznym użyciu ustaw ją przed wywołaniem `Kopiuj()`; operacja musi być w transakcji. ### W5 — Bezpieczne usuwanie **Cel:** usunąć kontrahenta albo świadomie odmówić usunięcia, gdy istnieją powiązania. **Warianty:** | Wariant | Sytuacja | Zalecenie | |---|---|---| | Usunięcie czyste | brak dokumentów/rozrachunków/zadań/zdarzeń | dozwolone (`DeleteRow`) | | Usunięcie zablokowane | są dokumenty/rozrachunki/zapisy | zamiast usuwać → `Blokada=true` | | Kontrahent systemowy | `IsStandard` / `JestIncydentalny` | nie usuwać | **Pola i typy:** `DokumentyHandlowe`, `Rozrachunki`, `Zadania`, `Zdarzenia` (kolekcje `SubTable`), `IsStandard: bool`, `JestIncydentalny: bool`, `Blokada: bool`. **Snippet:** ```csharp var crm = session.GetCRM(); var k = crm.Kontrahenci.WgKodu["FIRMA001"]; if (k == null) return; if (k.IsStandard || k.JestIncydentalny) throw new BusException("Nie można usunąć kontrahenta systemowego.".Translate()); bool maPowiazania = !k.DokumentyHandlowe.IsEmpty || !k.Rozrachunki.IsEmpty || !k.Zadania.IsEmpty || !k.Zdarzenia.IsEmpty; using (var t = session.Logout(editMode: true)) { if (maPowiazania) k.Blokada = true; // miękkie wycofanie zamiast usunięcia else k.Delete(); // twarde usunięcie tylko gdy brak powiązań t.Commit(); } session.Save(); ``` **Pułapki:** - Sprawdź powiązania **przed** `DeleteRow()`. Próba usunięcia powiązanego rekordu i tak zostanie odrzucona przez integralność (wyjątek w `Save()`), ale lepiej zdecydować świadomie. - Preferuj `Blokada=true` (kontrahent znika z list, dane pozostają) zamiast kasowania, gdy są powiązania historyczne. - `IsEmpty`/`Any` na kolekcji `SubTable` to **właściwości** (test serwerowy `exists …`, bez nawiasów) — nie materializuj kolekcji do pamięci (`.ToList().Count`). --- ## 3. Adres, kontakt, osoby ### W6 — Adres **Cel:** wprowadzić lub zaktualizować adres kontrahenta. **Warianty:** | Wariant | Pole | |---|---| | Adres główny | `Adres: Soneta.Core.Adres` | | Adres do korespondencji | `AdresDoKorespondencji: Soneta.Core.Adres` | | Telefon / faks na adresie | `Adres.Telefon`, `Adres.Faks` | | Dane rozszerzone / nietypowa lokalizacja / GLN | `Adres.NietypowaLokalizacja`, `Adres.GLN` | **Pola i typy (`Soneta.Core.Adres`):** `Ulica: string`, `NrDomu: string`, `NrLokalu: string`, `KodPocztowy: int`, `KodPocztowyS: string` (sformatowany, np. `"31-000"`), `Poczta: string`, `Miejscowosc: string`, `Gmina: string`, `Powiat: string`, `Wojewodztwo: Soneta.Core.Wojewodztwa` (enum), `Kraj: string`, `KodKraju: string`, `GLN: string`, `Telefon: string`, `Faks: string`, `ZagranicznyKodPocztowy: string`. **Snippet:** ```csharp var k = session.GetCRM().Kontrahenci.WgKodu["FIRMA001"]; using (var t = session.Logout(editMode: true)) { var a = k.Adres; // property zwraca obiekt adresu — edytujemy jego pola a.Ulica = "Wadowicka"; a.NrDomu = "8A"; a.NrLokalu = "2"; a.KodPocztowyS = "30-415"; // string z myślnikiem; pole int KodPocztowy = 30415 a.Miejscowosc = "Kraków"; a.Poczta = "Kraków"; a.Wojewodztwo = Wojewodztwa.małopolskie; a.Kraj = "Polska"; a.Telefon = "+48 12 345 67 89"; // Adres do korespondencji (gdy różny od głównego) k.AdresDoKorespondencji.Ulica = "Skrytka pocztowa 15"; t.Commit(); } session.Save(); ``` **Pułapki:** - `Adres` to property **kalkulowana zwracająca obiekt złożony** — nie da się przypisać `k.Adres = …`; modyfikuj jego pola. - `KodPocztowy` jest typu **`int`** (np. `30415`). Do wartości z myślnikiem używaj `KodPocztowyS` (string), które samo rozkłada/składa kod. - `Wojewodztwo` to **enum** `Soneta.Core.Wojewodztwa`, nie string. - `KodKraju` adresu bywa kalkulowane z `Kraj` — ustawiaj `Kraj`/`KodKraju` spójnie. ### W7 — Dane kontaktowe i adresy WWW **Cel:** odczytać i zapisać kanały kontaktu (e-mail, telefon, faks, WWW) z oznaczeniem domyślnego. **Warianty:** | Wariant | Kolekcja / pole | |---|---| | Odczyt domyślnego e-maila/telefonu/WWW | `Kontakt.EMAIL`, `Kontakt.TelefonKomorkowy`, `Kontakt.WWW` | | Dodanie kanału kontaktu | `Kontakty: SubTable` (`Rodzaj`, `Kontakt`, `Domyslny`) | | Adresy WWW | `AdresyWWW: SubTable` (`Adres`, `Domyslny`) | | e-faktura | `EFaktura: Soneta.Core.EFaktura`, `EFakturaOkres: FromTo` | **Pola i typy:** `Kontakt: Soneta.Core.Kontakt` (zsumowany „domyślny" kontakt — `EMAIL`, `TelefonKomorkowy`, `WWW`, `SkrytkaPocztowa`, `Skype`). `DaneKontaktowe`: `Host: IDaneKontaktoweHost`, `Rodzaj: RodzajKontaktu`, `Kontakt: string`, `Domyslny: bool`. `AdresWWW`: `Adres: string`, `Domyslny: bool`. **Snippet:** ```csharp var k = session.GetCRM().Kontrahenci.WgKodu["FIRMA001"]; // Odczyt domyślnych kanałów (do automatyzacji wysyłek): string email = k.Kontakt.EMAIL; string tel = k.Kontakt.TelefonKomorkowy; string www = k.Kontakt.WWW; // Dodanie nowego kanału e-mail i oznaczenie go jako domyślny: var rodzajEmail = session.GetCore().RodzajeKontaktow[RodzajeKontaktow.AdresEmail]; using (var t = session.Logout(editMode: true)) { var dk = new DaneKontaktowe { Host = k }; // Host = kontrahent (IDaneKontaktoweHost) session.AddRow(dk); dk.Rodzaj = rodzajEmail; dk.Kontakt = "kontakt@firma-xyz.pl"; dk.Domyslny = true; // Dodanie adresu WWW: var strona = new AdresWWW(k); // ctor przyjmuje IAdresyWWWHost session.AddRow(strona); strona.Adres = "https://www.firma-xyz.pl"; strona.Domyslny = true; t.Commit(); } session.Save(); ``` **Pułapki:** - `DaneKontaktowe.Rodzaj` to rekord słownika `RodzajKontaktu` — pobierz go po stałej Guid przez `session.GetCore().RodzajeKontaktow[RodzajeKontaktow.AdresEmail]` (analogicznie `TelefonKomórkowy`, `TelefonStacjonarny`, `Faks`, `Skype`). - Tylko **jeden** kontakt domyślny w obrębie rodzaju — ustawienie `Domyslny=true` na nowym zwykle zdejmuje flagę z poprzedniego. - `k.Kontakt.*` to **zagregowany** widok domyślnych kontaktów (do odczytu w automatyzacji). Pełna lista kanałów jest w kolekcji `k.Kontakty`. - `AdresWWW` tworzymy konstruktorem z hostem (`new AdresWWW(k)`); pole adresu URL nazywa się `Adres` (nie `Url`). ### W8 — Osoby kontaktowe **Cel:** zarządzać osobami kontaktowymi przypisanymi do kontrahenta. **Warianty:** | Wariant | Operacja | |---|---| | Odczyt listy | `Osoby: SubTable` (`Imie`, `Nazwisko`, `Stanowisko`, `EMAIL`, `Nieaktualny`) | | Dodanie osoby | nowy `KontaktOsoba`, ustaw `Kontrahent` | | Edycja osoby | zmiana pól | | Oznaczenie nieaktualnej | flaga `Nieaktualny` (zamiast usuwania) | | Dołącz / odłącz istniejącą | workery `DolaczOsobeKontrahentaWorker`, `RozlaczKontrahentaWorker` | **Pola i typy (`KontaktOsoba`):** `Imie: string`, `Nazwisko: string`, `Stanowisko: string`, `EMAIL: string`, `Nieaktualny: bool`, `Kontrahent: IKontrahent` (powiązanie). **Snippet:** ```csharp var k = session.GetCRM().Kontrahenci.WgKodu["FIRMA001"]; // Odczyt aktualnych osób: foreach (KontaktOsoba os in k.Osoby[(KontaktOsoba o) => !o.Nieaktualny]) Console.WriteLine($"{os.Imie} {os.Nazwisko} — {os.Stanowisko}"); // Dodanie osoby kontaktowej: using (var t = session.Logout(editMode: true)) { var os = new KontaktOsoba(); session.AddRow(os); os.Kontrahent = k; // powiązanie z kontrahentem os.Imie = "Anna"; os.Nazwisko = "Nowak"; os.Stanowisko = "Kierownik zakupów"; os.EMAIL = "a.nowak@firma-xyz.pl"; t.Commit(); } session.Save(); ``` **Pułapki:** - Powiązanie osoby z kontrahentem ustawiamy przez `os.Kontrahent = k` (pod spodem powstaje rekord relacji w `OsobyKontaktowe`); osoba pojawia się wtedy w `k.Osoby`. - **Nie usuwaj** osób, których dotyczyła historia kontaktu — oznaczaj `Nieaktualny=true`. Uwaga: ustawienie `Nieaktualny` ma efekty uboczne (kaskada na powiązania, integracja z kontem webowym) — rób to tylko w pełnej, zalogowanej sesji aplikacyjnej. - Filtruj aktualne/nieaktualne serwerowo: `k.Osoby[(KontaktOsoba o) => !o.Nieaktualny]`. --- ## 4. Warunki handlowe i finanse ### W9 — Warunki płatności i limity kredytowe **Cel:** ustawić warunki płatności i parametry kontroli kredytu kupieckiego. **Warianty:** | Wariant | Pola | |---|---| | Warunki płatności | `SposobZaplaty: FormaPlatnosci`, `Termin: int`, `TerminPlanowany: int`, `Waluta` | | Limit kredytu | `TypLimituKredytowego`, `LimitKredytu: Currency` | | Kontrola przeterminowania | `TypPrzeterminowania`, `KontrolaKwota: Currency`, `KontrolaDni: int` | | Odczyt stanu (kalkulowane) | `LimitNieograniczony`, `PrzeterminowanieNieograniczone`, `KontrolaAktywna` | | e-faktura | `EFaktura: Soneta.Core.EFaktura`, `EFakturaOkres: FromTo` | | Odsetki / windykacja | `OdsKarne` (złożone), `NieWindykowac`, `DefinicjaSprawyWindykacyjnej` | | Rachunki bankowe | `Rachunki: SubTable`, `DomyslnyRachunek` (kalkulowane) | **Pola i typy:** `SposobZaplaty: Soneta.Kasa.FormaPlatnosci`, `Termin: int`, `LimitKredytu: Soneta.Types.Currency`, `TypLimituKredytowego: Soneta.CRM.TypLimituKredytowego` (`Kwota=0`, `Nieograniczony=1`), `KontrolaKwota: Currency`, `KontrolaDni: int`, `TypPrzeterminowania: Soneta.CRM.TypLimituKredytowego`. **Snippet:** ```csharp var crm = session.GetCRM(); var k = crm.Kontrahenci.WgKodu["FIRMA001"]; using (var t = session.Logout(editMode: true)) { // Warunki płatności: k.SposobZaplaty = session.GetKasa().FormyPlatnosci[FormaPlatnosci.Przelew]; k.Termin = 14; // dni // Limit kredytu kupieckiego: k.TypLimituKredytowego = TypLimituKredytowego.Kwota; k.LimitKredytu = new Currency(50000m, "PLN"); // kwota + symbol waluty // Kontrola przeterminowania: k.TypPrzeterminowania = TypLimituKredytowego.Kwota; k.KontrolaKwota = new Currency(5000m, "PLN"); k.KontrolaDni = 7; t.Commit(); } session.Save(); // Odczyt pól kalkulowanych (tylko do odczytu): bool bezLimitu = k.LimitNieograniczony; RachunekBankowyPodmiotu domyslny = k.DomyslnyRachunek; ``` **Pułapki:** - Kwoty to **`Currency`** (kwota + waluta), nie `decimal`/`double` (safe-code §10). Twórz `new Currency(kwota, waluta)`. - `LimitNieograniczony`, `PrzeterminowanieNieograniczone`, `KontrolaAktywna`, `DomyslnyRachunek` są **kalkulowane** — tylko do odczytu. - `SposobZaplaty` to rekord `FormaPlatnosci` — pobierz go z `session.GetKasa().FormyPlatnosci[…]` (np. stała `FormaPlatnosci.Przelew`), nie ustawiaj „z palca". - Ustawienie `TypLimituKredytowego = Nieograniczony` czyni `LimitKredytu` polem nieaktywnym (w UI read-only) — ustawiaj kwotę tylko dla typu `Kwota`. ### W10 — Konto księgowe / rozrachunkowe **Cel:** odczytać/ustawić powiązanie kontrahenta z rozliczeniami (kontrahent jako `IPodmiotKasowy`). **Warianty:** | Wariant | Mechanizm | |---|---| | Kontrahent jako podmiot kasowy | rzutowanie/użycie przez interfejs `Soneta.Kasa.IPodmiotKasowy` | | Domyślny płatnik | `Platnik: IPodmiotKasowy` (kalkulowane — nadrzędny z relacji lub sam podmiot) | | Rachunki podmiotu | `Rachunki: SubTable` | **Pola i typy:** `Platnik: Soneta.Kasa.IPodmiotKasowy` (kalkulowane), `Rachunki`, `DomyslnyRachunek` (kalkulowane). `Kontrahent` implementuje `IPodmiotKasowy`. **Snippet:** ```csharp var k = session.GetCRM().Kontrahenci.WgKodu["FIRMA001"]; // Kontrahent jest podmiotem kasowym — można go podać tam, gdzie wymagany jest IPodmiotKasowy: IPodmiotKasowy podmiot = k; // Domyślny płatnik (gdy kontrahent jest podrzędny, zwraca nadrzędnego z relacji): IPodmiotKasowy platnik = k.Platnik; ``` **Pułapki:** - `Platnik` jest **kalkulowany** (zależny od relacji podmiotów, W14) — nie zapisuj go bezpośrednio. - Konta księgowe rozrachunkowe należą do modułu księgowego; z poziomu kontrahenta operuj przez interfejs `IPodmiotKasowy` i kolekcje rozrachunków (W11), nie przez prywatne pola księgowe. ### W11 — Rozrachunki i płatności **Cel:** odczytać należności/zobowiązania i ostatnie płatności kontrahenta. **Warianty:** | Wariant | Źródło | |---|---| | Należności i zobowiązania | `Rozrachunki: SubTable` | | Płatności / zapłaty | `Platnosci: SubTable`, `Zaplaty: SubTable` | | Dokumenty rozliczeniowe | `DokumentyRozliczeniowe: SubTable` | | Przelewy | `Przelewy: SubTable` | **Pola i typy:** wszystkie powyższe to kolekcje `SubTable` na `Kontrahent`. `RozrachunekIdx` ma m.in. pola kwotowe i datę rozrachunku. **Snippet:** ```csharp var k = session.GetCRM().Kontrahenci.WgKodu["FIRMA001"]; // Rozrachunki nierozliczone — filtr serwerowy po kolekcji: foreach (RozrachunekIdx r in k.Rozrachunki) { // r.* — kwota, waluta, data, kierunek (należność/zobowiązanie) } // Ostatnie zapłaty (zawężaj zakresem czasu — to dane operacyjne!): var od = Date.Today.AddMonths(-3); foreach (Zaplata z in k.Zaplaty) { // ... } ``` **Pułapki:** - Rozrachunki to dane **wyliczane/operacyjne** — przy szerszych analizach **zawężaj zakres czasowy** i nie ładuj całej historii (safe-code §6.3). - Saldo/przeterminowanie na dany dzień to wynik wyliczeń — czytaj przez dedykowane pola/kolekcje, nie sumuj „ręcznie" całej tabeli. - `RozrachunekIdx` / `Platnosc` / `Zaplata` żyją w module `Soneta.Kasa` — wymagana referencja do `Soneta.Kasa`. --- ## 5. Sprzedaż i dokumenty ### W12 — Dokumenty i dane sprzedażowe **Cel:** odczytać dokumenty handlowe kontrahenta oraz (opcjonalnie) utworzyć dokument. **Warianty:** | Wariant | Źródło / worker | |---|---| | Dokumenty, w których kontrahent jest nabywcą | `DokumentyHandlowe: SubTable` | | Dokumenty, w których jest odbiorcą | `DokumentyHandloweOdbiorcy: SubTable` | | Dokumenty ewidencji | `DokumentyEwidencji: SubTable` | | Utworzenie dokumentu | przez moduł `Handel` (definicja dokumentu + ustawienie `Kontrahent`) | **Pola i typy:** `DokumentyHandlowe`, `DokumentyHandloweOdbiorcy`, `DokumentyEwidencji` — kolekcje `SubTable` na `Kontrahent`. **Snippet:** ```csharp var k = session.GetCRM().Kontrahenci.WgKodu["FIRMA001"]; // Ostatnie dokumenty handlowe kontrahenta jako nabywcy: foreach (var d in k.DokumentyHandlowe) { // d.* — numer, data, wartości } ``` **Pułapki:** - Tworzenie dokumentu handlowego realizuje moduł `Handel` (definicja `DefDokHandlowych`, `new DokumentHandlowy`, ustawienie `Kontrahent`) — to osobny obszar; z poziomu kontrahenta korzystaj z jego kolekcji do odczytu. - `DokHandlowe` to tabela **operacyjna guided** — przy iteracji poprzecznej zawężaj zakres czasowy (safe-code §6.3). Kolekcja `k.DokumentyHandlowe` jest już zawężona do jednego kontrahenta. --- ## 6. Klasyfikacja ### W13 — Cechy, kategorie, branże, GUS **Cel:** uzupełnić cechy definiowalne i klasyfikacje kontrahenta. **Warianty:** | Wariant | Kolekcja / mechanizm | |---|---| | Cecha definiowalna | `Features: FeatureCollection` (odczyt/zapis po nazwie definicji) | | Kategorie | `Kategorie: SubTable` (poj. lub worker `KontrahenciPrzypiszKategorieWorker`) | | Branże | `Branze: SubTable` | | PKD / dane GUS | worker `DaneZGusBirWorker` (online; pobiera też kody PKD) | **Pola i typy:** `Features: Soneta.Business.FeatureCollection` (indeksator `Features["NazwaCechy"]` zwraca/przyjmuje `object`), `Kategorie: SubTable`, `Branze: SubTable`. `KategoriaKth` tworzymy konstruktorem `new KategoriaKth(kontrahent, defKategorii)`. **Snippet:** ```csharp var crm = session.GetCRM(); var k = crm.Kontrahenci.WgKodu["FIRMA001"]; using (var t = session.Logout(editMode: true)) { // Cecha definiowalna — dostęp po nazwie definicji (cecha musi być wcześniej zdefiniowana): k.Features["Segment"] = "Premium"; // Przypisanie do kategorii (defKat: DefKategKth z konfiguracji CRM, indeks WgNazwy): var defKat = crm.DefKategoriiKth.WgNazwy["VIP"]; if (defKat != null && crm.KategorieKth.WgKontrahent[k, defKat] == null) crm.KategorieKth.AddRow(new KategoriaKth(k, defKat)); t.Commit(); } session.Save(); // Odczyt cechy: object segment = k.Features["Segment"]; ``` **Pułapki:** - Cecha jest dostępna **po nazwie definicji**; odwołanie do niezdefiniowanej cechy rzuca wyjątek — upewnij się, że definicja istnieje (cechy vs pola natywne to dwie różne rzeczy). - Przed dodaniem kategorii sprawdź duplikat: `crm.KategorieKth.WgKontrahent[k, defKat]`. - Masowe przypisanie kategorii: worker `KontrahenciPrzypiszKategorieWorker` (`[Context] Kontrahent[]` + `Params.Kategoria`). - Pobranie kodów PKD odbywa się **online** z GUS-BIR (worker `DaneZGusBirWorker`) — patrz W15. --- ## 7. Powiązania ### W14 — Powiązania i opiekunowie **Cel:** zarządzać opiekunami i relacjami między kontrahentami. **Warianty:** | Wariant | Operacja / worker | |---|---| | Opiekun (dodanie / główny) | `Opiekunowie: SubTable`, worker `UstawOpiekunaGlownegoWorker` | | Sprawdzenie opieki na dzień | metody `JestOpiekunemNaDzis(...)`, `OpiekunowieWOkresie(...)` | | Podmiot nadrzędny / podrzędny | workery `NowyPodmiotNadrzednyWorker`, `NowyPodmiotPodrzednyWorker` | | Relacje podmiotów | `Podrzedni: SubTable`, `PodmiotNadrzedny: IPodmiot` | | Połącz / rozłącz | workery `PolaczKontrahentowWorker`, `RozlaczKontrahentaWorker` | **Pola i typy (`Opiekun`):** `Kontrahent: Kontrahent`, `Operator: Operator`, `Typ: TypOpiekuna` (`Glówny=0`, `Zastępca=1`), `Rola: RolaOpiekun`, `OddzialFirmy`, `DataOd: Date`, `DataDo: Date`, `Aktywny: bool`. **Snippet:** ```csharp var crm = session.GetCRM(); var k = crm.Kontrahenci.WgKodu["FIRMA001"]; using (var t = session.Logout(editMode: true)) { var op = new Opiekun(); crm.Opiekunowie.AddRow(op); op.Kontrahent = k; op.Operator = oper; // Operator pobrany z modułu Business op.Typ = TypOpiekuna.Glówny; op.DataOd = Date.Today; op.DataDo = Date.MaxValue; op.Aktywny = true; t.Commit(); } session.Save(); // Odczyt relacji podmiotów: foreach (RelacjaPodmiotu r in k.Podrzedni) { // r.Nadrzedny, r.PowiazaniePodmiotu.Rola, r.PowiazaniePodmiotu.RodzajPowiazania } IPodmiot nadrzedny = k.PodmiotNadrzedny; ``` **Pułapki:** - `Opiekun.Operator` to rekord operatora (dane konfiguracyjne) — w kodzie biznesowym pobieraj go spójnie z bieżącą sesją; nie mieszaj rekordów z różnych sesji (safe-code §2.1, użyj `session.Get(...)`). - Do sprawdzania opieki „na dziś"/„w okresie" używaj metod publicznych `JestOpiekunemNaDzis`, `OpiekunowieWOkresie` zamiast ręcznego filtrowania dat. - Relacje podmiotów (nadrzędny/podrzędny, płatnik/odbiorca) zakładaj workerami `NowyPodmiotNadrzednyWorker`/`NowyPodmiotPodrzednyWorker` — mają walidatory spójności. --- ## 8. Weryfikacja statusu ### W15 — Weryfikacja VAT (GUS / MF / VIES) **Cel:** zweryfikować dane i status podatnika w rejestrach zewnętrznych. **Wszystkie operacje są online** (wymagają połączenia i bywają limitowane). **Warianty:** | Wariant | Worker (jednostkowo / masowo) | Wynik na kontrahencie | |---|---|---| | Dane z GUS-BIR (też PKD) | `DaneZGusBirWorker` / `DaneZGusBirMultipleWorker` | nazwa, adres, REGON, KRS, PKD | | Status MF / biała lista | `DaneZMfWorker`, `KontrahentBialaListaWorker` / `KontrahenciBialaListaWorker` | `AktualnyStatusVATMF` | | Status VIES | `DataFromViesWorker` / `KontrahenciDaneZViesWorker` | `AktualnyStatusVATVies` | | Historia statusu VAT | kolekcja `StatusyVAT: SubTable` | — | **Pola i typy (odczyt wyniku):** `AktualnyStatusVAT`, `AktualnyStatusVATMF`, `AktualnyStatusVATVies` (typ `Soneta.CRM.StatusNumeruVAT`, kalkulowane), `AktStatusVATData/DataMF/DataVIES: DateTime?`, `StatusyVAT: SubTable`. **Snippet:** ```csharp var k = session.GetCRM().Kontrahenci.WgKodu["FIRMA001"]; // Odczyt ostatnio zapisanych statusów (offline — bez sieci): StatusNumeruVAT statusMF = k.AktualnyStatusVATMF; StatusNumeruVAT statusVies = k.AktualnyStatusVATVies; DateTime? dataMF = k.AktStatusVATDataMF; // Historia statusów: foreach (StatusVAT s in k.StatusyVAT) { /* ... */ } // Weryfikacja online — przez worker (przykład: status MF): // var w = new DaneZMfWorker { Kontrahent = k, Context = context }; // w.DaneZMf(); // WYMAGA SIECI — obuduj obsługą braku połączenia/limitów ``` **Pułapki:** - Operacje GUS/MF/VIES **wymagają sieci** — obuduj je obsługą błędów połączenia i limitów; **nie testuj ich w testach jednostkowych** (zależność od usług zewnętrznych). - Status VAT z rejestru to dane „na dzień" — zapisuj datę weryfikacji (`AktStatusVATData*`). - W kodzie offline czytaj wyłącznie pola kalkulowane (`AktualnyStatusVAT*`) i historię `StatusyVAT`. - Nie loguj nadmiarowo numerów NIP/PESEL (safe-code §12). --- ## 9. RODO/GIODO i KSeF ### W16 — RODO / GIODO **Cel:** obsłużyć zgody i wymianę danych osobowych kontrahenta. **Warianty:** | Wariant | Mechanizm / worker | Kolekcja | |---|---|---| | Oświadczenia | `KontrahentDodajOswiadczeniaWorker` | `GIODOOświadczenia: SubTable` | | Pozyskanie danych | `KontrahentDodajPozyskanieDanychWorker` | `GIODOUdostępnienia` | | Udostępnienie danych | `KontrahentDodajUdostepnienieDanychWorker` | `GIODOUdostępnienia` | | Powierzenie danych | `KontrahentDodajPowierzenieDanychWorker` | `GIODOUdostępnienia` | | Potwierdzenia zgodności | — | `PotwierdzeniaGIODO: SubTable` | **Pola i typy:** `GIODOOświadczenia: SubTable`, `GIODOUdostępnienia: SubTable`, `PotwierdzeniaGIODO: SubTable`, `ZgodnoscGIODOPotwierdzona: bool` (kalkulowane). **Snippet:** ```csharp var k = session.GetCRM().Kontrahenci.WgKodu["FIRMA001"]; // Odczyt oświadczeń RODO kontrahenta: foreach (GIODOOświadczenie o in k.GIODOOświadczenia) { // o.* — definicja oświadczenia, okres obowiązywania, status zgody } // Dodawanie oświadczeń realizują workery RODO (dziedziczą po bazowych z Soneta.Core): // new KontrahentDodajOswiadczeniaWorker(...).DodajOświadczenia(); ``` **Pułapki:** - Obowiązywanie zgody jest „na dzień" — czytaj okresy z rekordów `GIODOOświadczenie`, nie zakładaj bezterminowości. - Dane osobowe (PESEL, e-mail osób) są wrażliwe — nie loguj ich (safe-code §12). - Workery RODO mają tryb `ConfirmSave` i wymagają praw do obszaru GIODO. ### W17 — KSeF **Cel:** ustawić parametry KSeF kontrahenta. **Warianty:** | Wariant | Pole | |---|---| | Szablon pól opcjonalnych | `DomyslnySzablonPolOpcjonalnychKSeF: Soneta.Core.KSeFSzablonPolOpcjonalnych` | | Sposób wysyłki ceny | `KSeFSposobObslugiWysylkiCeny: Soneta.Core.SposobObslugiWysylkiCenyDoKSeF` | | Powiązanie z e-fakturą | `EFaktura`, `EFakturaOkres` (patrz W7) | **Pola i typy:** jak w tabeli powyżej (oba pola bazodanowe, zapisywalne). **Snippet:** ```csharp var k = session.GetCRM().Kontrahenci.WgKodu["FIRMA001"]; using (var t = session.Logout(editMode: true)) { k.KSeFSposobObslugiWysylkiCeny = SposobObslugiWysylkiCenyDoKSeF.CenaPoRabacie; // k.DomyslnySzablonPolOpcjonalnychKSeF = ... // rekord szablonu z konfiguracji t.Commit(); } session.Save(); ``` **Pułapki:** - `DomyslnySzablonPolOpcjonalnychKSeF` to referencja do rekordu konfiguracyjnego — pobierz istniejący szablon, nie twórz „w locie". - Konfiguracja KSeF współgra z `EFaktura` (W7) — ustawiaj je spójnie. --- ## 10. Operacje masowe ### W18 — Operacje na zbiorze kontrahentów **Cel:** wykonać operację na wielu kontrahentach efektywnie i bezpiecznie. **Warianty:** | Wariant | Mechanizm | |---|---| | Iteracja z warunkiem | serwerowy LINQ `crm.Kontrahenci[(Kontrahent k) => …]` (patrz [`rowcondition.md`](../rowcondition.md)) | | Masowa aktualizacja | jedna transakcja, paczki (patrz [`safe-code.md`](../safe-code.md)) | | Masowa zmiana formy prawnej | worker `ZmienFormePrawnaKontrahentowWorker` | | Masowe przypisanie kategorii | worker `KontrahenciPrzypiszKategorieWorker` | | Masowa weryfikacja VAT/VIES (online) | `KontrahenciBialaListaWorker`, `KontrahenciDaneZMfWorker`, `KontrahenciDaneZViesWorker` | | Eksport / import | datapack / business.xml (patrz [`datapack-guidedrow.md`](../datapack-guidedrow.md)) | **Snippet:** ```csharp var crm = session.GetCRM(); // Masowa zmiana: ustaw blokadę sprzedaży dla kontrahentów bez NIP — filtr serwerowy + 1 transakcja using (var t = session.Logout(editMode: true)) { foreach (Kontrahent k in crm.Kontrahenci.WgKodu[(Kontrahent k) => k.NIP == null && k.StatusPodmiotu == StatusPodmiotu.PodmiotGospodarczy]) { k.BlokadaSprzedazy = true; } t.Commit(); } session.Save(); ``` **Pułapki:** - **Nie ładuj całej tabeli** do pamięci — filtr serwerowy (`SubTable[condition]`). - Duże operacje dziel na **paczki** (krótkie transakcje), by nie blokować innych użytkowników i nie zwiększać ryzyka konfliktu optymistycznego (safe-code §13.1). - Workery masowe (`*Worker` na typie `Kontrahenci`) mają property `[Context] Kontrahent[]` — przy użyciu programowym ustaw tablicę zaznaczonych rekordów. --- ## Powiązane dokumenty - [`safe-code.md`](../safe-code.md) — sesja, transakcje, blokada optymistyczna, zasady bezpiecznego kodu. - [`session-login.md`](../session-login.md) — `Session`, `Login`, `Database`. - [`worker-extender.md`](../worker-extender.md) — workery, akcje menu Czynności, bindowanie. - [`rowcondition.md`](../rowcondition.md) — serwerowy LINQ, `RowCondition`, `SubTable[condition]`. - [`datapack-guidedrow.md`](../datapack-guidedrow.md) — eksport/import, `GuidedRow`. - [`scan-props.md`](../scan-props.md) / [`scan-workers.md`](../scan-workers.md) — inwentaryzacja pól i workerów.