From 5605ad2915eb78cd5851635539c069c085f21c3a Mon Sep 17 00:00:00 2001 From: Marcin Wojas Date: Sun, 7 Jun 2026 08:31:37 +0200 Subject: [PATCH] =?UTF-8?q?SKILL:=20Uporz=C4=85dkowanie=20skills=20domenow?= =?UTF-8?q?ych=20-=20podzia=C5=82=20=20na=20mniejsze=20pliki=20i=20wsp?= =?UTF-8?q?=C3=B3lna=20numeracja?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- soneta-programming/SKILL.md | 6 +- soneta-programming/references/domeny/crm.md | 90 + .../domeny/crm/CRM01-wyszukiwanie.md | 109 + .../references/domeny/crm/CRM02-tworzenie.md | 159 + .../domeny/crm/CRM03-adres-kontakt.md | 166 + .../domeny/crm/CRM04-warunki-finanse.md | 141 + .../references/domeny/crm/CRM05-sprzedaz.md | 41 + .../domeny/crm/CRM06-klasyfikacja.md | 54 + .../references/domeny/crm/CRM07-powiazania.md | 60 + .../domeny/crm/CRM08-weryfikacja-statusu.md | 49 + .../references/domeny/crm/CRM09-rodo-ksef.md | 78 + .../domeny/crm/CRM10-operacje-masowe.md | 46 + .../references/domeny/dokument-handlowy.md | 5581 ------------- .../references/domeny/handel.md | 143 + .../domeny/handel/HANDEL01-fundamenty.md | 225 + .../domeny/handel/HANDEL02-wystawianie.md | 520 ++ .../domeny/handel/HANDEL03-cykl-zycia.md | 326 + .../domeny/handel/HANDEL04-relacje.md | 490 ++ .../domeny/handel/HANDEL05-odczyt.md | 395 + .../domeny/handel/HANDEL06-magazyn.md | 554 ++ .../domeny/handel/HANDEL07-cechy.md | 261 + .../domeny/handel/HANDEL08-vat-waluty.md | 333 + .../domeny/handel/HANDEL09-korekty.md | 298 + .../domeny/handel/HANDEL10-batch.md | 222 + .../domeny/handel/HANDEL11-pomocnicze.md | 408 + .../domeny/handel/HANDEL12-wydruki.md | 397 + .../domeny/handel/HANDEL13-specjalistyczne.md | 505 ++ .../domeny/handel/HANDEL14-platnosci.md | 453 ++ soneta-programming/references/domeny/kadry.md | 98 + .../domeny/kadry/KADRY01-pracownik.md | 1247 +++ .../references/domeny/kadry/KADRY02-etat.md | 501 ++ .../kadry/KADRY03-dodatki-potracenia.md | 609 ++ .../domeny/kadry/KADRY04-nieobecnosci.md | 887 +++ .../domeny/kadry/KADRY05-plan-pracy.md | 348 + .../references/domeny/kadry/KADRY06-rcp.md | 321 + .../references/domeny/kadry/KADRY07-umowy.md | 479 ++ .../references/domeny/kadry/KADRY08-place.md | 751 ++ .../domeny/kadry/KADRY09-listy-place.md | 550 ++ .../domeny/kadry/KADRY10-deklaracje.md | 352 + .../domeny/kadry/KADRY11-ewidencje.md | 831 ++ .../references/domeny/kontrahent.md | 957 --- .../references/domeny/pracownik.md | 6985 ----------------- 42 files changed, 13500 insertions(+), 13526 deletions(-) create mode 100644 soneta-programming/references/domeny/crm.md create mode 100644 soneta-programming/references/domeny/crm/CRM01-wyszukiwanie.md create mode 100644 soneta-programming/references/domeny/crm/CRM02-tworzenie.md create mode 100644 soneta-programming/references/domeny/crm/CRM03-adres-kontakt.md create mode 100644 soneta-programming/references/domeny/crm/CRM04-warunki-finanse.md create mode 100644 soneta-programming/references/domeny/crm/CRM05-sprzedaz.md create mode 100644 soneta-programming/references/domeny/crm/CRM06-klasyfikacja.md create mode 100644 soneta-programming/references/domeny/crm/CRM07-powiazania.md create mode 100644 soneta-programming/references/domeny/crm/CRM08-weryfikacja-statusu.md create mode 100644 soneta-programming/references/domeny/crm/CRM09-rodo-ksef.md create mode 100644 soneta-programming/references/domeny/crm/CRM10-operacje-masowe.md delete mode 100644 soneta-programming/references/domeny/dokument-handlowy.md create mode 100644 soneta-programming/references/domeny/handel.md create mode 100644 soneta-programming/references/domeny/handel/HANDEL01-fundamenty.md create mode 100644 soneta-programming/references/domeny/handel/HANDEL02-wystawianie.md create mode 100644 soneta-programming/references/domeny/handel/HANDEL03-cykl-zycia.md create mode 100644 soneta-programming/references/domeny/handel/HANDEL04-relacje.md create mode 100644 soneta-programming/references/domeny/handel/HANDEL05-odczyt.md create mode 100644 soneta-programming/references/domeny/handel/HANDEL06-magazyn.md create mode 100644 soneta-programming/references/domeny/handel/HANDEL07-cechy.md create mode 100644 soneta-programming/references/domeny/handel/HANDEL08-vat-waluty.md create mode 100644 soneta-programming/references/domeny/handel/HANDEL09-korekty.md create mode 100644 soneta-programming/references/domeny/handel/HANDEL10-batch.md create mode 100644 soneta-programming/references/domeny/handel/HANDEL11-pomocnicze.md create mode 100644 soneta-programming/references/domeny/handel/HANDEL12-wydruki.md create mode 100644 soneta-programming/references/domeny/handel/HANDEL13-specjalistyczne.md create mode 100644 soneta-programming/references/domeny/handel/HANDEL14-platnosci.md create mode 100644 soneta-programming/references/domeny/kadry.md create mode 100644 soneta-programming/references/domeny/kadry/KADRY01-pracownik.md create mode 100644 soneta-programming/references/domeny/kadry/KADRY02-etat.md create mode 100644 soneta-programming/references/domeny/kadry/KADRY03-dodatki-potracenia.md create mode 100644 soneta-programming/references/domeny/kadry/KADRY04-nieobecnosci.md create mode 100644 soneta-programming/references/domeny/kadry/KADRY05-plan-pracy.md create mode 100644 soneta-programming/references/domeny/kadry/KADRY06-rcp.md create mode 100644 soneta-programming/references/domeny/kadry/KADRY07-umowy.md create mode 100644 soneta-programming/references/domeny/kadry/KADRY08-place.md create mode 100644 soneta-programming/references/domeny/kadry/KADRY09-listy-place.md create mode 100644 soneta-programming/references/domeny/kadry/KADRY10-deklaracje.md create mode 100644 soneta-programming/references/domeny/kadry/KADRY11-ewidencje.md delete mode 100644 soneta-programming/references/domeny/kontrahent.md delete mode 100644 soneta-programming/references/domeny/pracownik.md diff --git a/soneta-programming/SKILL.md b/soneta-programming/SKILL.md index 47be731..9161576 100644 --- a/soneta-programming/SKILL.md +++ b/soneta-programming/SKILL.md @@ -40,9 +40,9 @@ SKILL.md zawiera "duży obraz" - hierarchię klas, thread-safety, kanoniczne wzo | ViewInfo - definicja widoków list (folderów), CreateView, klasa Params, powiązanie z viewform.xml | [references/viewinfo.md](references/viewinfo.md) | | Cechy (Features) - tabela Features, typy cech, dostęp typowany/nietypowany, bindowanie w form.xml | [references/features.md](references/features.md) | | Gotowe wzorce kodu end-to-end (import, CRUD, obsługa błędów) | [references/examples.md](references/examples.md) | -| Receptury kodu per obiekt biznesowy (domeny) — `Kontrahent` (pola, kolekcje, workery, finanse, RODO, KSeF) | [references/domeny/kontrahent.md](references/domeny/kontrahent.md) | -| Receptury kodu per obiekt biznesowy (domeny) — `DokumentHandlowy` (faktury/magazynowe/zamówienia/korekty, relacje `IRelacjeService`, cykl życia, magazyn/partie/obroty, VAT/waluty, płatności, KSeF/fiskal/Intrastat, wydruki) | [references/domeny/dokument-handlowy.md](references/domeny/dokument-handlowy.md) | -| Receptury kodu per obiekt biznesowy (domeny) — `Pracownik`/Kadry-Płace (zatrudnienie i dane kadrowe, historia `PracHistoria`+`Etat`, dodatki, nieobecności/limity, plan pracy/RCP, umowy cywilnoprawne, naliczanie wypłat, listy płac, wydruki PDF) | [references/domeny/pracownik.md](references/domeny/pracownik.md) | +| Receptury kodu per obiekt biznesowy (domena CRM) — `Kontrahent` (pola, kolekcje, workery, finanse, RODO, KSeF). Indeks + mapa receptur (CRM-W1–W18); rozdziały `references/domeny/crm/CRM01..CRM10` | [references/domeny/crm.md](references/domeny/crm.md) | +| Receptury kodu per obiekt biznesowy (domena Handel) — `DokumentHandlowy` (faktury/magazynowe/zamówienia/korekty, relacje `IRelacjeService`, cykl życia, magazyn/partie/obroty, VAT/waluty, płatności, KSeF/fiskal/Intrastat, wydruki). Indeks + mapa receptur (HANDEL-W1–W82); rozdziały `references/domeny/handel/HANDEL01..HANDEL14` | [references/domeny/handel.md](references/domeny/handel.md) | +| Receptury kodu per obiekt biznesowy (domena Kadry-Płace) — `Pracownik` (zatrudnienie i dane kadrowe, historia `PracHistoria`+`Etat`, dodatki, nieobecności/limity, plan pracy/RCP, umowy cywilnoprawne, naliczanie wypłat, listy płac, wydruki PDF). Indeks + mapa receptur (KADRY-A*…K*); rozdziały `references/domeny/kadry/KADRY01..KADRY11` | [references/domeny/kadry.md](references/domeny/kadry.md) | | **Zasady bezpiecznego kodu biznesowego — checklist do review i refaktoringu** | [references/safe-code.md](references/safe-code.md) | | Skanowanie pól obiektu biznesowego z DLL (Roslyn MetadataReference) | [references/scan-props.md](references/scan-props.md) | | Inwentaryzacja modułów i tabel (`*Module` / `*Row` / `*Table`) z DLL | [references/scan-modules.md](references/scan-modules.md) | diff --git a/soneta-programming/references/domeny/crm.md b/soneta-programming/references/domeny/crm.md new file mode 100644 index 0000000..b8a8ee5 --- /dev/null +++ b/soneta-programming/references/domeny/crm.md @@ -0,0 +1,90 @@ +# 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 (`CRM-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. + +--- + + +## Mapa receptur + +| Rozdział | Plik | Receptury | +|---|---|---| +| CRM01 — Wyszukiwanie i identyfikacja | [crm/CRM01-wyszukiwanie.md](crm/CRM01-wyszukiwanie.md) | CRM-W1–W2 | +| CRM02 — Tworzenie, modyfikacja, usuwanie | [crm/CRM02-tworzenie.md](crm/CRM02-tworzenie.md) | CRM-W3–W5 | +| CRM03 — Adres, kontakt, osoby | [crm/CRM03-adres-kontakt.md](crm/CRM03-adres-kontakt.md) | CRM-W6–W8 | +| CRM04 — Warunki handlowe i finanse | [crm/CRM04-warunki-finanse.md](crm/CRM04-warunki-finanse.md) | CRM-W9–W11 | +| CRM05 — Sprzedaż i dokumenty | [crm/CRM05-sprzedaz.md](crm/CRM05-sprzedaz.md) | CRM-W12 | +| CRM06 — Klasyfikacja | [crm/CRM06-klasyfikacja.md](crm/CRM06-klasyfikacja.md) | CRM-W13 | +| CRM07 — Powiązania | [crm/CRM07-powiazania.md](crm/CRM07-powiazania.md) | CRM-W14 | +| CRM08 — Weryfikacja statusu | [crm/CRM08-weryfikacja-statusu.md](crm/CRM08-weryfikacja-statusu.md) | CRM-W15 | +| CRM09 — RODO/GIODO i KSeF | [crm/CRM09-rodo-ksef.md](crm/CRM09-rodo-ksef.md) | CRM-W16–W17 | +| CRM10 — Operacje masowe | [crm/CRM10-operacje-masowe.md](crm/CRM10-operacje-masowe.md) | CRM-W18 | + +## 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. diff --git a/soneta-programming/references/domeny/crm/CRM01-wyszukiwanie.md b/soneta-programming/references/domeny/crm/CRM01-wyszukiwanie.md new file mode 100644 index 0000000..df35b28 --- /dev/null +++ b/soneta-programming/references/domeny/crm/CRM01-wyszukiwanie.md @@ -0,0 +1,109 @@ +# CRM01 — Wyszukiwanie i identyfikacja + +> Wspólne fakty o typie, podstawowe typy i szablon wzorca: [../crm.md](../crm.md). + +### CRM-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. + +### CRM-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 (CRM-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 CRM-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. + +--- + diff --git a/soneta-programming/references/domeny/crm/CRM02-tworzenie.md b/soneta-programming/references/domeny/crm/CRM02-tworzenie.md new file mode 100644 index 0000000..4fa8dd6 --- /dev/null +++ b/soneta-programming/references/domeny/crm/CRM02-tworzenie.md @@ -0,0 +1,159 @@ +# CRM02 — Tworzenie, modyfikacja, usuwanie + +> Wspólne fakty o typie, podstawowe typy i szablon wzorca: [../crm.md](../crm.md). + +### CRM-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()`. + +### CRM-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. + +### CRM-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`). + +--- + diff --git a/soneta-programming/references/domeny/crm/CRM03-adres-kontakt.md b/soneta-programming/references/domeny/crm/CRM03-adres-kontakt.md new file mode 100644 index 0000000..a73c07e --- /dev/null +++ b/soneta-programming/references/domeny/crm/CRM03-adres-kontakt.md @@ -0,0 +1,166 @@ +# CRM03 — Adres, kontakt, osoby + +> Wspólne fakty o typie, podstawowe typy i szablon wzorca: [../crm.md](../crm.md). + +### CRM-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. + +### CRM-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`). + +### CRM-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]`. + +--- + diff --git a/soneta-programming/references/domeny/crm/CRM04-warunki-finanse.md b/soneta-programming/references/domeny/crm/CRM04-warunki-finanse.md new file mode 100644 index 0000000..3bba59c --- /dev/null +++ b/soneta-programming/references/domeny/crm/CRM04-warunki-finanse.md @@ -0,0 +1,141 @@ +# CRM04 — Warunki handlowe i finanse + +> Wspólne fakty o typie, podstawowe typy i szablon wzorca: [../crm.md](../crm.md). + +### CRM-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`. + +### CRM-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, CRM-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 (CRM-W11), nie przez prywatne pola księgowe. + +### CRM-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`. + +--- + diff --git a/soneta-programming/references/domeny/crm/CRM05-sprzedaz.md b/soneta-programming/references/domeny/crm/CRM05-sprzedaz.md new file mode 100644 index 0000000..924090c --- /dev/null +++ b/soneta-programming/references/domeny/crm/CRM05-sprzedaz.md @@ -0,0 +1,41 @@ +# CRM05 — Sprzedaż i dokumenty + +> Wspólne fakty o typie, podstawowe typy i szablon wzorca: [../crm.md](../crm.md). + +### CRM-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. + +--- + diff --git a/soneta-programming/references/domeny/crm/CRM06-klasyfikacja.md b/soneta-programming/references/domeny/crm/CRM06-klasyfikacja.md new file mode 100644 index 0000000..9383d16 --- /dev/null +++ b/soneta-programming/references/domeny/crm/CRM06-klasyfikacja.md @@ -0,0 +1,54 @@ +# CRM06 — Klasyfikacja + +> Wspólne fakty o typie, podstawowe typy i szablon wzorca: [../crm.md](../crm.md). + +### CRM-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 CRM-W15. + +--- + diff --git a/soneta-programming/references/domeny/crm/CRM07-powiazania.md b/soneta-programming/references/domeny/crm/CRM07-powiazania.md new file mode 100644 index 0000000..ffdfd3f --- /dev/null +++ b/soneta-programming/references/domeny/crm/CRM07-powiazania.md @@ -0,0 +1,60 @@ +# CRM07 — Powiązania + +> Wspólne fakty o typie, podstawowe typy i szablon wzorca: [../crm.md](../crm.md). + +### CRM-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. + +--- + diff --git a/soneta-programming/references/domeny/crm/CRM08-weryfikacja-statusu.md b/soneta-programming/references/domeny/crm/CRM08-weryfikacja-statusu.md new file mode 100644 index 0000000..c5cb378 --- /dev/null +++ b/soneta-programming/references/domeny/crm/CRM08-weryfikacja-statusu.md @@ -0,0 +1,49 @@ +# CRM08 — Weryfikacja statusu + +> Wspólne fakty o typie, podstawowe typy i szablon wzorca: [../crm.md](../crm.md). + +### CRM-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). + +--- + diff --git a/soneta-programming/references/domeny/crm/CRM09-rodo-ksef.md b/soneta-programming/references/domeny/crm/CRM09-rodo-ksef.md new file mode 100644 index 0000000..4fcd415 --- /dev/null +++ b/soneta-programming/references/domeny/crm/CRM09-rodo-ksef.md @@ -0,0 +1,78 @@ +# CRM09 — RODO/GIODO i KSeF + +> Wspólne fakty o typie, podstawowe typy i szablon wzorca: [../crm.md](../crm.md). + +### CRM-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. + +### CRM-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 CRM-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` (CRM-W7) — ustawiaj je spójnie. + +--- + diff --git a/soneta-programming/references/domeny/crm/CRM10-operacje-masowe.md b/soneta-programming/references/domeny/crm/CRM10-operacje-masowe.md new file mode 100644 index 0000000..d04880d --- /dev/null +++ b/soneta-programming/references/domeny/crm/CRM10-operacje-masowe.md @@ -0,0 +1,46 @@ +# CRM10 — Operacje masowe + +> Wspólne fakty o typie, podstawowe typy i szablon wzorca: [../crm.md](../crm.md). + +### CRM-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. + +--- + diff --git a/soneta-programming/references/domeny/dokument-handlowy.md b/soneta-programming/references/domeny/dokument-handlowy.md deleted file mode 100644 index 10b81a3..0000000 --- a/soneta-programming/references/domeny/dokument-handlowy.md +++ /dev/null @@ -1,5581 +0,0 @@ -# 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 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. - -## Spis treści - -- [1. Fundamenty i identyfikacja](#1-fundamenty-i-identyfikacja) - - [W1 — Dostęp do modułów handlowo-magazynowych i tabeli `DokHandlowe`](#w1--dostęp-do-modułów-handlowo-magazynowych-i-tabeli-dokhandlowe) - - [W2 — Wybór definicji dokumentu (`DefDokHandlowego`) wg symbolu](#w2--wybór-definicji-dokumentu-defdokhandlowego-wg-symbolu) - - [W3 — Rozpoznanie rodzaju dokumentu (faktura / magazynowy / zamówienie / korekta / zaliczka)](#w3--rozpoznanie-rodzaju-dokumentu-faktura--magazynowy--zamówienie--korekta--zaliczka) -- [2. Wystawianie dokumentów](#2-wystawianie-dokumentów) - - [W4 — Faktura sprzedaży (FV)](#w4--faktura-sprzedaży-fv) - - [W5 — Faktura zakupu (FZ)](#w5--faktura-zakupu-fz) - - [W6 — Dokument magazynowy (PZ / WZ / RW / PW)](#w6--dokument-magazynowy-pz--wz--rw--pw) - - [W7 — Zamówienie (ZO / ZD)](#w7--zamówienie-zo--zd) - - [W8 — Dodawanie pozycji (towar, ilość, cena, rabat, jednostka)](#w8--dodawanie-pozycji-towar-ilość-cena-rabat-jednostka) - - [W9 — Dokument w walucie obcej](#w9--dokument-w-walucie-obcej) - - [W10 — Dokument z usługą (pozycja usługowa bez wpływu na magazyn)](#w10--dokument-z-usługą-pozycja-usługowa-bez-wpływu-na-magazyn) - - [W11 — Odbiorca / płatnik inny niż kontrahent + miejsce dostawy](#w11--odbiorca--płatnik-inny-niż-kontrahent--miejsce-dostawy) -- [3. Stany dokumentu i cykl życia](#3-stany-dokumentu-i-cykl-życia) - - [W12 — Zatwierdzenie dokumentu (bufor → zatwierdzony)](#w12--zatwierdzenie-dokumentu-bufor--zatwierdzony) - - [W13 — Cofnięcie do bufora / odtwierdzenie](#w13--cofnięcie-do-bufora--odtwierdzenie) - - [W14 — Anulowanie dokumentów](#w14--anulowanie-dokumentów) - - [W15 — Naprawa i przeliczenie stanu dokumentu](#w15--naprawa-i-przeliczenie-stanu-dokumentu) - - [W16 — Bezpieczne usunięcie dokumentu z bufora i obsługa zależności](#w16--bezpieczne-usunięcie-dokumentu-z-bufora-i-obsługa-zależności) -- [4. Relacje i generowanie dokumentów](#4-relacje-i-generowanie-dokumentów) - - [W17 — Generowanie faktury z zamówienia (ZO → FV)](#w17--generowanie-faktury-z-zamówienia-zo--fv) - - [W18 — Zbiorczy dokument magazynowy z wielu faktur (wiele FA → 1 WZ/PZ)](#w18--zbiorczy-dokument-magazynowy-z-wielu-faktur-wiele-fa--1-wzpz) - - [W19 — Zbiorcza faktura z wielu dokumentów magazynowych (wiele WZ → 1 FA)](#w19--zbiorcza-faktura-z-wielu-dokumentów-magazynowych-wiele-wz--1-fa) - - [W20 — Wyszukiwanie dokumentów powiązanych (odczyt pól kalkulowanych)](#w20--wyszukiwanie-dokumentów-powiązanych-odczyt-pól-kalkulowanych) - - [W21 — Generowanie dokumentu magazynowego z faktury (FA → WZ pojedynczo)](#w21--generowanie-dokumentu-magazynowego-z-faktury-fa--wz-pojedynczo) - - [W22 — Kopiowanie faktury klientowi (`KopiujKlientowiFaktureWorker`)](#w22--kopiowanie-faktury-klientowi-kopiujklientowifaktureworker) - - [W23 — Ręczne wiązanie i rozwiązywanie powiązań](#w23--ręczne-wiązanie-i-rozwiązywanie-powiązań) - - [W24 — Odczyt łańcucha powiązań i stan pokrycia zamówienia](#w24--odczyt-łańcucha-powiązań-i-stan-pokrycia-zamówienia) -- [5. Odczyt i wyszukiwanie](#5-odczyt-i-wyszukiwanie) - - [W25 — Odczytanie pozycji dokumentu](#w25--odczytanie-pozycji-dokumentu) - - [W26 — Odczytanie dokumentów dla kontrahenta](#w26--odczytanie-dokumentów-dla-kontrahenta) - - [W27 — Ostatnie pozycje dokumentów dla wskazanego towaru](#w27--ostatnie-pozycje-dokumentów-dla-wskazanego-towaru) - - [W28 — Wyszukiwanie dokumentów wg okresu, definicji, stanu, serii](#w28--wyszukiwanie-dokumentów-wg-okresu-definicji-stanu-serii) - - [W29 — Odczyt dokumentu wg numeru lub Guid](#w29--odczyt-dokumentu-wg-numeru-lub-guid) - - [W30 — Korekty dokumentu i dokument korygowany](#w30--korekty-dokumentu-i-dokument-korygowany) -- [6. Magazyn, zasoby, partie, obroty](#6-magazyn-zasoby-partie-obroty) - - [W31 — Przeglądanie zasobów utworzonych przez dokument przychodowy (`dok.Zasoby`)](#w31--przeglądanie-zasobów-utworzonych-przez-dokument-przychodowy-dokzasoby) - - [W32 — Przetwarzanie obrotów faktury sprzedaży i dokumentu rozchodowego (`dok.Obroty`, `dok.ObrotyWszystkie`)](#w32--przetwarzanie-obrotów-faktury-sprzedaży-i-dokumentu-rozchodowego-dokobroty-dokobrotywszystkie) - - [W33 — Odczyt stanu magazynowego towaru (magazyn / data) — `mag.Zasoby` z filtrem](#w33--odczyt-stanu-magazynowego-towaru-magazyn--data--magzasoby-z-filtrem) - - [W34 — Wyszukiwanie partii magazynowych (`GrupaDostaw`) według cech](#w34--wyszukiwanie-partii-magazynowych-grupadostaw-według-cech) - - [W35 — Dokument rozchodowy ze wskazaniem JEDNEJ partii](#w35--dokument-rozchodowy-ze-wskazaniem-jednej-partii) - - [W36 — Dokument rozchodowy ze wskazaniem WIELU partii](#w36--dokument-rozchodowy-ze-wskazaniem-wielu-partii) - - [W37 — Dokument przyjęcia (PW/PZ) z numerem serii — zapis numeru serii jako cecha](#w37--dokument-przyjęcia-pwpz-z-numerem-serii--zapis-numeru-serii-jako-cecha) - - [W38 — Odczyt rozchodu zasobów: powiązanie pozycji rozchodu z partią pierwotną / przyjęciem](#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)](#w39--odczyt-okresów-magazynowych-i-kontekstu-wyceny-fifolifowg-dostaw) -- [7. Cechy (Features)](#7-cechy-features) - - [W40 — Przenoszenie cech z partii (dostawy) / towaru na pozycję dokumentu](#w40--przenoszenie-cech-z-partii-dostawy--towaru-na-pozycję-dokumentu) - - [W41 — Odczyt i zapis cech dokumentu / pozycji (`Features`)](#w41--odczyt-i-zapis-cech-dokumentu--pozycji-features) - - [W42 — Filtrowanie / wyszukiwanie dokumentów i partii po wartości cechy (serwerowo)](#w42--filtrowanie--wyszukiwanie-dokumentów-i-partii-po-wartości-cechy-serwerowo) -- [8. VAT, wartości i waluty](#8-vat-wartości-i-waluty) - - [W43 — Odczytanie tabeli VAT (`SumyVAT`)](#w43--odczytanie-tabeli-vat-sumyvat) - - [W44 — Odczyt podsumowań wartości dokumentu](#w44--odczyt-podsumowań-wartości-dokumentu) - - [W45 — Ręczna korekta tabeli VAT (`KorektaVAT`)](#w45--ręczna-korekta-tabeli-vat-korektavat) - - [W46 — Sposób liczenia VAT (`LiczonaOd`) i przeliczenie procedur VAT](#w46--sposób-liczenia-vat-liczonaod-i-przeliczenie-procedur-vat) - - [W47 — Zmiana waluty dokumentu i cen](#w47--zmiana-waluty-dokumentu-i-cen) -- [9. Korekty i dokumenty specjalne](#9-korekty-i-dokumenty-specjalne) - - [W48 — Korekta ilościowa i korekta ceny](#w48--korekta-ilościowa-i-korekta-ceny) - - [W49 — Korekta wartości przyjęcia magazynowego](#w49--korekta-wartości-przyjęcia-magazynowego) - - [W50 — Dokument inwentaryzacji (INW)](#w50--dokument-inwentaryzacji-inw) - - [W51 — Faktura zaliczkowa i jej rozliczenie dokumentem końcowym](#w51--faktura-zaliczkowa-i-jej-rozliczenie-dokumentem-końcowym) - - [W52 — Przesunięcie międzymagazynowe (MM)](#w52--przesunięcie-międzymagazynowe-mm) -- [10. Operacje zbiorcze (batch)](#10-operacje-zbiorcze-batch) - - [W53 — Ewidencjonowanie / eksport do księgowości wielu dokumentów](#w53--ewidencjonowanie--eksport-do-księgowości-wielu-dokumentów) - - [W54 — Hurtowe zatwierdzanie / generowanie dokumentów dla zaznaczonego zbioru](#w54--hurtowe-zatwierdzanie--generowanie-dokumentów-dla-zaznaczonego-zbioru) - - [W55 — Wydajne przetwarzanie wielu dokumentów w jednej sesji (paczki)](#w55--wydajne-przetwarzanie-wielu-dokumentów-w-jednej-sesji-paczki) -- [11. Operacje pomocnicze (przekrojowe)](#11-operacje-pomocnicze-przekrojowe) - - [W56 — Bezpieczne pobranie / utworzenie kontrahenta i towaru pozycji](#w56--bezpieczne-pobranie--utworzenie-kontrahenta-i-towaru-pozycji) - - [W57 — Przeliczanie jednostek miary towaru przy dodawaniu pozycji](#w57--przeliczanie-jednostek-miary-towaru-przy-dodawaniu-pozycji) - - [W58 — Walidacja przed zatwierdzeniem (kompletność, zasób, limit kredytowy)](#w58--walidacja-przed-zatwierdzeniem-kompletność-zasób-limit-kredytowy) - - [W59 — Obsługa błędów i blokada optymistyczna (kolizje `Save`, ponowienie)](#w59--obsługa-błędów-i-blokada-optymistyczna-kolizje-save-ponowienie) - - [W60 — Odczyt metadanych dokumentu (`ChangeInfos` — kto/kiedy założył i zmienił)](#w60--odczyt-metadanych-dokumentu-changeinfos--ktokiedy-założył-i-zmienił) - - [W61 — Praca z definicjami i numeracją (seria, wymuszenie numeru, bufor `Numer`)](#w61--praca-z-definicjami-i-numeracją-seria-wymuszenie-numeru-bufor-numer) -- [12. Wydruki i raporty](#12-wydruki-i-raporty) - - [W62 — Wydruk faktury do PDF / na drukarkę](#w62--wydruk-faktury-do-pdf--na-drukarkę) - - [W63 — Wydruk dokumentu magazynowego (PZ/WZ/MM)](#w63--wydruk-dokumentu-magazynowego-pzwzmm) - - [W64 — Raport dobowy i okresowy (zestawienie za dzień / okres)](#w64--raport-dobowy-i-okresowy-zestawienie-za-dzień--okres) - - [W65 — Wydruk zbiorczy dla zaznaczonego zbioru dokumentów](#w65--wydruk-zbiorczy-dla-zaznaczonego-zbioru-dokumentów) - - [W66 — Zapis wydruku do strumienia/pliku (integracja, e-mail)](#w66--zapis-wydruku-do-strumieniapliku-integracja-e-mail) -- [13. Tematy specjalistyczne (KSeF, fiskalizacja, kompletacja, Intrastat)](#13-tematy-specjalistyczne-ksef-fiskalizacja-kompletacja-intrastat) - - [W67 — Wysłanie faktury do KSeF (pojedynczo i zbiorczo)](#w67--wysłanie-faktury-do-ksef-pojedynczo-i-zbiorczo) - - [W68 — Sprawdzenie statusu KSeF i odczyt numeru KSeF](#w68--sprawdzenie-statusu-ksef-i-odczyt-numeru-ksef) - - [W69 — UPO, numer KSeF z duplikatu, walidacja struktury XML](#w69--upo-numer-ksef-z-duplikatu-walidacja-struktury-xml) - - [W70 — Import faktur z KSeF (dokumenty zakupu)](#w70--import-faktur-z-ksef-dokumenty-zakupu) - - [W71 — Fiskalizacja dokumentu (paragon fiskalny)](#w71--fiskalizacja-dokumentu-paragon-fiskalny) - - [W72 — E-paragon (e-mail) i ponowny wydruk paragonu](#w72--e-paragon-e-mail-i-ponowny-wydruk-paragonu) - - [W73 — Dokument kompletacji (złożenie / rozłożenie kompletu)](#w73--dokument-kompletacji-złożenie--rozłożenie-kompletu) - - [W74 — Intrastat (dane statystyczne i wyszukanie dokumentów do deklaracji)](#w74--intrastat-dane-statystyczne-i-wyszukanie-dokumentów-do-deklaracji) -- [14. Płatności dokumentu handlowego](#14-płatności-dokumentu-handlowego) - - [W75 — Przeglądanie płatności dokumentu](#w75--przeglądanie-płatności-dokumentu) - - [W76 — Rozbicie płatności na raty](#w76--rozbicie-płatności-na-raty) - - [W77 — Ręczne dodanie / edycja pojedynczej płatności](#w77--ręczne-dodanie--edycja-pojedynczej-płatności) - - [W78 — Warunki płatności z kontrahenta i ich przeliczenie na dokumencie](#w78--warunki-płatności-z-kontrahenta-i-ich-przeliczenie-na-dokumencie) - - [W79 — Zmiana płatnika (inny niż kontrahent)](#w79--zmiana-płatnika-inny-niż-kontrahent) - - [W80 — Odczyt stanu rozliczenia płatności](#w80--odczyt-stanu-rozliczenia-płatności) - - [W81 — Płatności w walucie obcej (kwota w walucie vs PLN, kurs)](#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ę](#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), tabela `Soneta.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ęp `session.GetHandel()`. - Tabela dokumentów: `Handel.DokHandlowe`. Definicje: `Handel.DefDokHandlowych` (klucz `WgSymbolu["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` | pozycje dokumentu | -| `SumyVAT` | `SubTable` | tabelka VAT (netto/VAT/brutto wg stawek) | -| `Platnosci` | `SubTable` | 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` | 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, transakcja `session.Logout(true)` + `Commit`/`CommitUI`, -> blokada optymistyczna, praca z `SubTable`) opisują [`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. - -### 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:** - -```csharp -// 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żyj - `Soneta.Waluty.WalutyModule.GetInstance(session)`. -- **Nie ładuj całej tabeli `DokHandlowe`** do pamięci z `if`-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 warunku `RowCondition` uż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`, potem `Save`). - -### 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:** - -```csharp -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 po `Kategoria`. -- `DefDokHandlowego` to 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(): bool` oraz `JestDokZaliczkowy(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:** - -```csharp -// 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ównaniu `Symbol == "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 <= HandelOstatni` itd.) lub porównaniem do konkretnych wartości — tak jak - w snippetcie. -- Wartości `*Pierwszy`/`*Ostatni` są oznaczone `[Hidden]` (nie pokazują się w UI), ale to **publiczne** - stałe enuma — wolno ich użyć w kodzie jako granic zakresu. -- `Korekta` i wyniki `JestDokZaliczkowy()` 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.Definicja` może w teorii być `null` na świeżo utworzonym, jeszcze nieskonfigurowanym - dokumencie — przy klasyfikacji dokumentów „w trakcie tworzenia" zabezpiecz dostęp do `Kategoria`. - ---- - -## 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)`, potem `Definicja` (inicjuje -> kategorię, kierunek magazynu, sposób liczenia VAT, walutę płatności), następnie `Magazyn`, -> `Kontrahent`, daty. Na pozycji najpierw `Towar` (inicjuje jednostkę, stawkę VAT, cenę i rabat), -> dopiero potem `Ilosc`, `Cena`, `Rabat`. Cała operacja w jednej transakcji -> `session.Logout(editMode: true)` zakończonej `Commit()` (kod biznesowy) / `CommitUI()` -> (worker/extender), a po niej `session.Save()` — dopiero `Save()` 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`, -`Platnosci: SubTable` (powstaje automatycznie wg formy/terminu kontrahenta). - -**Snippet:** - -```csharp -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. Samo `CommitUI` nie księguje obrotów — magazyn - aktualizuje się dopiero po `Session.Save()` dokumentu przychodowego. -- `SumyVAT`, `Suma`, `Platnosci` są **wyliczane** z pozycji i parametrów dokumentu — nie ustawiaj - ich ręcznie (ręczna korekta tabeli VAT to osobny mechanizm: `KorektaVAT=true`). -- `LiczonaOd` ustaw 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:** - -```csharp -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:** -- `Obcy` to 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:** - -```csharp -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:** -- `Magazyn` jest **wymagany** (`required`) dla dokumentów magazynowych — bez niego `Save()` rzuci - `RowException`. -- `KierunekMagazynu`/`TypPartii` są `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 - `StanUjemnyVerifier` zablokuje `Save()`. -- Obroty/zasoby księgują się **po `Session.Save()`**, nie po `Commit()`/`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:** - -```csharp -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**. -- `Dostawa` to 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 - odczytuj `zo.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:** - -```csharp -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:** -- **`Towar` ustawiaj jako pierwszy.** Dopiero on inicjuje symbol jednostki na `Ilosc`/`Cena`, stawkę - VAT i proponowaną cenę/rabat. Ustawienie `Ilosc`/`Cena` przed `Towar` operowałoby na pustych - symbolach. -- `Ilosc` to `Quantity` (wartość + symbol jednostki), `Cena` to `DoubleCy` (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`/`Rabat` zapala flagi `KorektaCeny`/`KorektaRabatu` — od tej chwili - platforma **nie przeliczy** już automatycznie tej wartości (np. po zmianie kontrahenta/ilości). -- `Cena` jest netto albo brutto zależnie od `Dokument.LiczonaOd` — interpretuj ją spójnie z - dokumentem. -- Konstruktor pozycji wymaga dokumentu: `new PozycjaDokHandlowego(dok)`, a po nim `session.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:** - -```csharp -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 - rzuca `KursWalutyNotFoundException`. Na bazie Demo nie ma kursu EUR „na dziś" — albo dodaj kurs do - tabeli kursowej, albo wpisz kurs ręcznie (`KursWaluty`). -- `TabelaKursowa` jest **wymagana** dla dokumentu walutowego. -- `session.GetWaluty()` jest **internal** — używaj `WalutyModule.GetInstance(session)`. -- Worker `DokumentHandlowyZmianaWalutyWorker` jest klasą **internal** — z dodatku nie tworzysz jej - instancji bezpośrednio; uruchamiasz akcję przez framework Czynności, przekazując publiczną klasę - `DokumentHandlowyZmianaWalutyWorkerParams` przez `Context`. -- 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:** - -```csharp -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 `StanUjemnyVerifier` nie 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) — `Ilosc` uż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:** - -```csharp -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:** -- `Kontrahent` to **nabywca** (strona transakcji i VAT), `Odbiorca` to fizyczny odbiorca towaru — - to dwa różne pola, oba typu `Kontrahent`. Faktura wystawiana jest na `Kontrahent`, dostawa idzie - do `Odbiorca`. -- `InnyPłatnik` jest **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. -- `OdbiorcaMiejsceDostawy` to referencja do rekordu `Lokalizacja` (zwykle zdefiniowanego u - odbiorcy) — pobierz istniejącą lokalizację, nie twórz „w locie". -- `Dostawa` to 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 w `Save()`) opisuje [`safe-code.md`](../safe-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ść `Zablokowany` ustawia **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:** - -```csharp -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()`** — samo `Commit()`/`CommitUI()` nie tworzy obrotów ani - zasobów. Jeśli baza blokuje stan ujemny (weryfikator `StanUjemnyVerifier`, jak w bazie Demo), - rozchód (FV/WZ/RW) wymaga **wcześniej zapisanego** przyjęcia (PW/PZ) tego towaru — inaczej `Save()` - rzuci wyjątek. -- Zatwierdzenie uruchamia walidatory dokumentu (kompletność pozycji, magazyn, kontrahent, tabela - VAT). Błędy wychodzą w `Commit()`/`Save()` jako `RowException` — nie połykaj ich (safe-code §4). -- W workerze/extenderze użyj `t.CommitUI()` zamiast `t.Commit()` - ([`worker-extender.md`](../worker-extender.md)). -- Po `Save()` w środku jednej sesji zamyka się okno edycji; kolejna edycja na **tym samym** obiekcie - bez ponownego `Logout` rzuci `AccessWriteDenied`. Wzorzec: zapis → odczyt na świeżej sesji. -- Nie ustawiaj `Stan = Zablokowany` z 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:** - -```csharp -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 `Zablokowany` nie cofniesz przez `dok.Stan = Bufor` — blokada wynika z innego - modułu (np. ewidencja zaksięgowana). Do diagnozy/naprawy rozbieżności stanu dokument↔ewidencja służy - `PoprawaStanuDokumentuWorker` (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:** - -```csharp -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 → Bufor` na poziomie pola `Stan`. - 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`; akcja - `public void NaprawStan()`; predykat widoczności - `public static bool IsVisibleNaprawStan(DokumentHandlowy dokument)`. Worker sam zarządza - transakcją wewnątrz `NaprawStan()` (synchronizuje `dok.Stan` z dokumentem ewidencji, w razie potrzeby - tworzy/kasuje ewidencję, może przestawić `Stan` na `Zablokowany`/`Zatwierdzony`). -- `PrzeliczenieStanuWorker`: enum `public enum Opcje { SprawdzićPoprawność, PoprawićTylkoBłędne, - PrzeliczyćTylkoNiepoprawione, PonowniePrzeliczyć, PoprawićObroty, SprawdzićObroty }`; konstruktor - publiczny `PrzeliczenieStanuWorker(Opcje wykonaj, bool wszystkieMagazyny, bool rozchód0, bool - przywracajWartość)`; property `[Context]` `Dokument`, `Towar`, `Magazyny` (`Magazyn[]`); akcja - `public void PrzeliczStan()`. Worker sam otwiera transakcje wewnątrz `PrzeliczStan()`. - -**Snippet:** - -```csharp -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łasnym `session.Logout(true)` — wystarczy `session.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 przez - `Trace`. Do faktycznej naprawy użyj `PoprawićTylkoBłędne`/`PonowniePrzeliczyć`. -- `PrzeliczenieStanuWorker` rzuca `RowException`, 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.IsVisibleNaprawStan` zwraca `false` dla 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:** - -```csharp -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 w `Save()`), 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 serwis `Soneta.Handel.RelacjeDokumentow.Api.IRelacjeService` -> (scope: `Session`). Workery wykonawcze (`PowiazDokumentyWorker`, `UsunPowiazanieDokumentowWorker`, -> akcje menu „Relacje”) są **internal** — nie instancjonuj ich z dodatku. Pobranie serwisu: -> -> ```csharp -> using Microsoft.Extensions.DependencyInjection; // GetRequiredService -> using Soneta.Handel.RelacjeDokumentow.Api; // IRelacjeService, HandlerSet -> -> var rel = session.GetRequiredService(); // rzuca, gdy serwisu brak -> // albo: var rel = session.GetService(); // zwraca null, gdy brak -> ``` -> -> **Reguł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 przez `session.Save()`. -> - Wynik to `DokumentHandlowy[]` — tablica utworzonych/dołączonych dokumentów podrzędnych. -> - `Context` (zaznaczenie / parametry UI) i `HandlerSet` (callbacki rozstrzygające) są **opcjonalne**. -> Jeśli definicja relacji wymaga rozstrzygnięcia (np. wyboru dostaw, magazynu, pozycji) i **nie -> dostarczysz odpowiedniego callbacka**, platforma rzuci `NotImplementedException`. - -### 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` | definicja relacji ma `WyborPozycji = WybórMagazynu` — wskaż magazyn docelowy | -| `WybierzMagazynDocelowyCallback` | `Func` | wybór magazynu dla dokumentu docelowego (domyślnie `d.MagazynDo`) | -| `WybierzPozycjeCallback` | `Action` | definicja ma `WyborPozycji = WybórPozycji` — zaznacz pozycje (domyślnie `PrzeliczPozycje()`) | -| `WybierzDostawyCallback` | `Action` | wskazanie partii/dostaw przy rozchodzie (gdy `WskazaniePartii` wymuszone) | -| `WybierzDokumentyZaliczkoweCallback` | `Action` | faktura z zaliczkami | -| `UstawParametryFakturowania` | `Action` | 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:** - -```csharp -using Microsoft.Extensions.DependencyInjection; -using Soneta.Handel; -using Soneta.Handel.RelacjeDokumentow.Api; - -var rel = session.GetRequiredService(); - -// 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): - -```csharp -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 `HandlerSet` go nie - dostarcza → `NotImplementedException`. Zacznij od wywołania bez `HandlerSet`; 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 `DefRelacji` na `DefDokHandlowego`). Brak pasującej relacji → pusty wynik - lub wyjątek. -- Cała operacja w **jednej** transakcji + `Save()`. Mieszane sesje rekordów → użyj `session.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:** - -```csharp -var rel = session.GetRequiredService(); - -// 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:** - -```csharp -var rel = session.GetRequiredService(); - -// 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 `DefRelacji` na 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:** - -```csharp -// 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 `.Length` - przed `[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`):** - -```csharp -using Soneta.Magazyny; - -var rel = session.GetRequiredService(); - -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()) - { - 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 `WZ` ma `WskazaniePartii = WymuszonyDodawanie`, **musisz** dostarczyć - `WybierzDostawyCallback` — inaczej `NotImplementedException`. -- Rozchód wymaga wcześniejszego **zapisanego** przyjęcia towaru (`StanUjemnyVerifier` w Demo). Magazyn - księguje się dopiero po `Session.Save()` — samo `Commit`/`CommitUI` nie 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ą tylko `Zatwierdzony`). -- `[Context] Params Prms` — parametry; `Params : ContextBase`: - - `DefinicjaDokumentu Definicja` — definicja dokumentu zakupu w bazie klienta (lista z - `DefDokumentow.WgTypu[TypDokumentu.ZakupEwidencja]`); - - `bool PrzygotujPrzelewy` (domyślnie `true`) — 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`):** - -```csharp -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 i `Save()` - 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 Rachunkowe` i 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 `PowiazDokumentyWorker` i -> `UsunPowiazanieDokumentowWorker` są **internal** — nie używaj ich z dodatku. Wiązanie realizuj przez -> `IRelacjeService.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:** -```csharp -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:** - -```csharp -var rel = session.GetRequiredService(); - -// 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:** -- `relationName` musi dokładnie pasować do **nazwy `DefRelacji`** skonfigurowanej w bazie (wielkość - liter / spacje istotne) — niepasująca nazwa daje pusty/`null` wynik w tablicy. -- `Dolacz*` przetwarza dokumenty **pojedynczo** (`Array.ConvertAll`) — wynik na pozycji `i` moż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`) → property `StanPokrycia : 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, `StanPokrycia` zwraca `Niezweryfikowane`. - -**Snippet:** - -```csharp -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.StanPokrycia` zwraca `Niezweryfikowane`, 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 traktuj `StanPokrycia` jako **odczyt** stanu policzonego interaktywnie. -- Pola `DokumentyHandlowe`/`DokumentyMagazynowe` respektują prawa dostępu i są kalkulowane — buforuj - wynik, nie wołaj w gęstych pętlach (W20). -- Stan `NiePodlega` oznacza dokument, którego pokrycie nie dotyczy (np. nie jest zamówieniem) — - rozróżniaj go od `Brak` (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`](../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]`. - ---- - -### 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 (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 (`?.`). - ---- - -### 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`. - ---- - -### 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. - ---- - -### 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. - ---- - -### 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`. - ---- - -### 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`. - ---- - -## 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. Samo `Commit()`/`CommitUI()` w transakcji nie nalicza -> stanów. W bazie Demo działa `StanUjemnyVerifier` — **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` (tabela `Zasoby`) — stan towaru: ilość na partii w danym magazynie i okresie. -> - `Obrot` (tabela `Obroty`) — pojedynczy ruch (przychód lub rozchód) wiążący partie. -> - `GrupaDostaw` (tabela `GrupyDostaw`, namespace `Soneta.Magazyny.Dostawy`) — **partia** -> towaru (identyfikowana `Numer` + `Towar`). -> - `OkresMagazynowy` (tabela `OkresyMag`) — przedział czasu, w którym ewidencjonowane są -> obroty/zasoby; po zamknięciu blokuje modyfikacje. -> - `PartiaTowaru` — **subrow** (nie tabela) opisujący stronę partii w `Obrot`/`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`) | 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:** - -```csharp -// 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.Zasoby` jest **puste, dopóki nie wykonasz `session.Save()`** — przed zapisem magazyn - nie zaksięgował zasobów (sam `Commit`/`CommitUI` nie wystarcza). -- Wzorzec testowy: zapis dokumentu → `SaveDispose()` → odczyt na świeżej sesji po `Guid`, - bo po `Save()` w środku testu okno edycji się zamyka. -- Zasób przychodowy ma `Kierunek == KierunekPartii.Przychód`. Zasób rozchodowy na stanie - ujemnym ma `Kierunek == KierunekPartii.Rozchód` — nie myl ich przy sumowaniu stanu. -- Nie modyfikuj `Zasob`/`Obrot` rę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:** - -```csharp -// 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()) -{ - if (o.Korekta == KorektaObrotu.StornoZasobu) continue; // ObrotyWszystkie już to pomija - // ... agregacja ilości/wartości -} -``` - -**Pułapki:** -- `dok.Obroty` automatycznie dobiera stronę (przychodowa vs rozchodowa) na podstawie - kierunku magazynowego dokumentu — nie filtruj jej ręcznie po kierunku. -- `ObrotyWszystkie`/`ObrotyWszystkiePozycji`/`ObrotyWszystkieWgPartiiPierwotnej` zwracają - `ListWithView` — iteruj przez `.Cast()`. Pomijają już obroty `StornoZasobu`. -- Obroty pojawiają się **po `Session.Save()`** dokumentu, nie po `Commit()`. -- `Przychod`/`Rozchod`/`PrzychodPierwotny` to **subrow `PartiaTowaru`**, nie rekord partii — - do rekordu `GrupaDostaw` się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`. -`OkresMagazynowy` z `mag.OkresyMag` (patrz W39). Ilości to `Quantity`. - -**Snippet:** - -```csharp -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 `Zasoby` do pamięci** — zawsze zawężaj indeksem - (`WgTowar[...]`, `WgMagazyn[...]`, `WgPartiaTowaruMagazyn[...]`). Patrz `safe-code.md` §6. -- Ilości są typu `Quantity` (ilość + jednostka), nie `double` — operuj na `Quantity` i - 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` | -| 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:** - -```csharp -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]` - i `WgTowar[towar]` zwracają **zbiór** (`SubTable`). -- W `RowCondition` uż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 (patrz `features.md`). -- `Numer` partii 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:** - -```csharp -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().FirstOrDefault(); -PozycjaDokHandlowego pozycjaPrzyjecia = przychod?.Przychod.Dokument? - .Pozycje.Cast() - .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`). Przy `Zabroniony` partia jest dobierana wyłącznie - algorytmem magazynu — ustawienie `poz.Dostawa` zostanie zignorowane lub odrzucone. -- `poz.Dostawa` to **pozycja dokumentu przyjęcia** (`PozycjaDokHandlowego`), a nie rekord - `GrupaDostaw`. Partię `GrupaDostaw` mapujesz 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.Dostawa` **przed** `Commit()`; właściwy obrót zostaje naliczony dopiero w - `Save()`. - ---- - -### 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:** - -```csharp -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().FirstOrDefault(); - PozycjaDokHandlowego dostawa = przychod?.Przychod.Dokument? - .Pozycje.Cast().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.Dostawa` to 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 przy - `WyborPozycjiDlaRelacji != BrakOkna` skutkuje `NotImplementedException`. -- 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:** - -```csharp -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() - .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 zapisem - `mag.GrupyDostaw` jej nie zawiera. -- Gdy magazyn ma autonumerowanie `WgCechy`, `GrupaDostaw.Numer` jest **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:** - -```csharp -// 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) i - `PrzychodPierwotny` (źródło sprzed korekt). Do raportu pochodzenia używaj `Przychod`/ - `PrzychodPierwotny`. -- `obrot.Przychod`/`Rozchod` to **subrow `PartiaTowaru`** — nie jest `null` jako 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:** - -```csharp -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) lub `WgCechy*`, automatyczny dobór partii - zależy od `poz.Dostawa` (W35/W36) lub cechy (`CechaAlgorytmu`) — bez nich rozchód nie - zostanie poprawnie rozliczony. -- `NieLiczyćStanów` oznacza, że magazyn **nie prowadzi zasobów** — `dok.Zasoby` pozostanie - puste, a kontroli stanu ujemnego nie ma. -- Modyfikacja dokumentów w **zamkniętym** okresie (`okres.Zamkniety == true`) zostanie - odrzucona — sprawdź to przed edycją wstecz. -- `OkresMagazynowy` to 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 (typ `FeatureCollection` / `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:** - -```csharp -// 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 = true` na - 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 przez - `IRelacjeService` (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]` na `Row` (`dok["Nazwa"]`) — równoważny - `dok.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:** - -```csharp -var handel = session.GetHandel(); -var dok = handel.DokHandlowe.WgDaty[...]; // lub Get(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().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 (`DokHandlowe` dla dokumentu, `PozycjeDokHan` dla 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`), potem `Save`. 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 surowy `decimal`/`double`/`string`. -- Cechy **algorytmiczne**: przypisanie wartości uruchamia algorytm definicji — efekty uboczne; część - cech bywa read-only (`IsReadOnly(fd)` / tryb `SpecialEdit`) i edycja rzuci `AccessDeniedException`. -- W form.xml cechę adresuje się ścieżką `Features.Nazwa` (np. `{Features.NrSerii}`), także przez - relację (`{Kontrahent.Features.Segment}`). -- `dok.Pozycje` to kolekcja pozycji dokumentu — iteruj po niej, nie ładuj całej tabeli - `PozycjeDokHan`. - ---- - -### 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:** - -```csharp -// 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"` w `FieldCondition` — `Features.X` nie jest - typowaną property `Row`, więc nie da się jej użyć w wyrażeniu LINQ (`(Row r) => r.Features…`). -- Warunek aplikuj **na indeksie** (`WgKodu[...]`, `WgDaty[...]`) lub na kolekcji `SubTable` - (`dok.Pozycje[...]`) — to wykonuje filtr w SQL. Nie iteruj całej tabeli z `if` w pamięci - (`safe-code.md` §6). -- Wyszukiwanie korzysta z indeksowanego pola `DataKey` cechy; wartość w warunku podawaj w typie - zgodnym z typem cechy (np. `bool` dla cechy Bool, `Date` dla cechy Date) — wartości są zapisane w - ustalonym formacie tekstowym (patrz tabela typów w `references/features.md`). -- `view.Condition &= …` to mechanizm **UI** (ViewInfo/folder); w kodzie biznesowym używaj - `SubTable[condition]`, nie obiektu `View`. -- `DokHandlowe` to 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 jako `decimal` (`Netto`, `VAT`, `Brutto`); `BruttoNettoCy` -> — kwoty w walucie dokumentu jako `Currency` (`NettoCy`, `VATCy`, `BruttoCy`). Nie operuj na -> niezaokrąglonych `decimal` — 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`) | 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` udostępnia: `DefinicjaStawki: -DefinicjaStawkiVat`, `Stawka: StawkaVat` (`Stawka.Procent: Percent`), `Suma: BruttoNetto` -(`Netto`, `VAT`, `Brutto` — `decimal`), `SumaCy: BruttoNettoCy` (`NettoCy`, `VATCy`, `BruttoCy` — -`Currency`), `Dokument: DokumentHandlowy`. - -**Snippet:** - -```csharp -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.SumyVAT` to `SubTable` — 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`, `decimal` w walucie systemowej) od `SumaCy` (`BruttoNettoCy`, - `Currency` w walucie dokumentu). Dla dokumentu walutowego do prezentacji używaj `SumaCy`. -- `Stawka` to `StawkaVat` (typ stawki), `Procent` zwraca `Percent` — nie myl z `decimal`. -- 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:** - -```csharp -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.Suma` to **stan zapisany** podsumowania, a `dok.SumaPozycji` jest **wyliczane na bieżąco** - z pozycji za każdym odczytem. Dla dokumentu w buforze, przed ponownym przeliczeniem, mogą się - chwilowo różnić. -- `SumaPozycji`/`SumaPozycjiTowProd` zwracają `BruttoNettoPozycji` — typ **tylko do odczytu** (brak - setterów); nie próbuj przez nie modyfikować wartości. -- `dok.Rabat` to `Percent` — 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 `KorektaTabeliVATWorker` jest `internal`** — nie da się go zainstancjonować z dodatku -> zewnętrznego. Publiczny tor korekty prowadzi przez flagę `dok.KorektaVAT` i bezpośrednią edycję -> pól wierszy `dok.SumyVAT`. - -**Snippet:** - -```csharp -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 `SumyVAT` bez `dok.KorektaVAT = true` zostanie zablokowana — `SumaVAT` jest 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`/`Brutto` rzuca `ArgumentException`. Zaokrąglaj wejście - (`Soneta.Tools.Math.RoundCy(...)`). -- `KorektaVAT` jest dostępna tylko, gdy definicja dokumentu na to pozwala - (`Definicja.SumyVAT` w trybie korekty) — sprawdzaj `dok.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. -- `DefinicjaStawki` na wierszu `SumaVAT` można zmieniać tylko przy włączonej korekcie - (`IsReadOnlyDefinicjaStawki()` zależy od `KorektaVAT`). - ---- - -### 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:** - -```csharp -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:** -- `LiczonaOd` nie przyjmuje wartości `0` (`RequiredException`). Zawsze ustaw konkretny wariant enuma. -- Zmiana `LiczonaOd` na 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 flag `Params` (`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. -- `PrzeliczProceduryVATParams` dziedziczy po `ContextBase` — przy ręcznym tworzeniu przekaż `Context` - do 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 `DokumentHandlowyZmianaWalutyWorker` jest `internal`** — 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 parametry -> `DokumentHandlowyZmianaWalutyWorkerParams`. Z poziomu kodu dodatku zewnętrznego dostępne tory to: -> (1) uruchomienie akcji przez mechanizm Czynności z przygotowanym `Context`, albo (2) bezpośrednie -> ustawienie pól waluty/kursu na dokumencie i pozycjach. - -**Snippet:** - -```csharp -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 `DokumentHandlowyZmianaWalutyWorker` jest `internal` — **nie** wywołasz `new ...Worker(...)` - ani `.ZmienWalute()` z dodatku zewnętrznego. Używaj publicznych `Params` + akcji Czynności lub - bezpośredniej edycji pól dokumentu. -- `session.GetWaluty()` jest **internal** — moduł walut pobieraj przez - `WalutyModule.GetInstance(session)` (namespace `Soneta.Waluty`). -- Jeśli w bazie **brak kursu** na żądaną datę (np. Demo nie ma kursu EUR „na dziś"), platforma rzuci - `KursWalutyNotFoundException`. `KursWaluty` w parametrach wylicza się automatycznie tylko, gdy kurs - istnieje; w przeciwnym razie ustaw `KursWaluty` ręcznie. -- Zmiana waluty ma sens tylko dla dokumentu w **buforze** (`IsVisibleZmienWalute` wymaga - `dok.Bufor`); dla dokumentu zatwierdzonego operacja jest niedostępna. -- `WalutaBazowa` jest read-only — wyznaczana z bieżącej waluty dokumentu (`dok.BruttoCy.Symbol`). - Ustawiasz tylko `Waluta` (docelową). -- Kwoty pieniężne to `Currency` (wartość + symbol), nie `decimal`/`double`. Sam `KursWaluty` jest - `double`. - ---- - ---- - -## 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`](../safe-code.md)): -> - Dostęp do serwisu: `var rel = session.GetRequiredService();` (wymaga `using 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), potem `session.Save()`. Magazyn księguje się dopiero po `Save()`. -> - Pola `DokumentKorygowany`, `DokumentyKorygujące`, `DokumentyZaliczkowe` są **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:** - -```csharp -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(); - -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:** -- `NowaKorekta` zwraca **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". -- `symbolKorekty` to 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 `Ilosc` musi pochodzić z istniejącej pozycji (`poz.Ilosc.Symbol`) — nie twórz `Quantity` z 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:** - -```csharp -var rel = session.GetRequiredService(); - -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 `UtworzKorektePrzyjeciaWorker` jest `internal`** — nie da się go zainstancjonować z dodatku zewnętrznego. Publiczny tor to **`IRelacjeService.NowaKorekta`** (wewnętrznie worker robi dokładnie to samo: `NowaKorekta` + dostosowanie `Pozycje[].Ilosc` z 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`/`Cena` na pozycji korekty. -- Magazyn (zasoby/obroty) aktualizuje się dopiero po `session.Save()`, nie po `Commit()`. -- 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:** - -```csharp -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` — wskazanie zaliczek (tor „na pozycje"). -- `HandlerSet.WybierzZaliczkiWgStawkiVatCallback: Action` — tor „wg stawki VAT". -- Worker publiczny do wskazania zaliczki: `DokumentHandlowyRealizacjaZaliczkiWorker` z property `[Context] Dokument: DokumentHandlowy`, `[Context] Docelowy: DokumentDocelowy`, `Wybrany: bool`. -- Odczyt: `dok.DokumentyZaliczkowe` (kalkulowane) — zaliczki powiązane z końcowym; `dok.SumyVAT: SubTable`; `dok.BruttoCy`. - -**Snippet:** - -```csharp -using Microsoft.Extensions.DependencyInjection; -using Soneta.Handel; -using Soneta.Handel.RelacjeDokumentow.Api; - -var rel = session.GetRequiredService(); - -// 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()) { - 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`: `NaPozycje` vs `NaDokument`). -- 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) jest `internal` — z dodatku używaj publicznego `DokumentHandlowyRealizacjaZaliczkiWorker` (wskazanie dokumentów) wewnątrz callbacka. -- Faktura zaliczkowa musi być **zatwierdzona** przed rozliczeniem; `DokumentyZaliczkowe` to pole **kalkulowane** — nie ustawiasz go, czytasz. -- Tabela VAT dokumentu zaliczkowego jest przeliczana proporcjonalnie do wpłaconej zaliczki (logika `DokumentZaliczkowyWorker`) — nie modyfikuj `SumyVAT` rę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ęcia `Podrzędne[TypRelacjiHandlowej.PrzesunięcieDo]`; wymaga, by dokument przesunięcia już istniał — ustawiaj po `Definicja`). -- `PozycjaDokHandlowego.Towar`, `Ilosc: Quantity`. - -**Snippet:** - -```csharp -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:** -- `MagazynDo` jest **polem kalkulowanym** delegującym do podrzędnego dokumentu przesunięcia — ustaw je **po** `Definicja` (a najlepiej przed dodaniem pozycji), bo `IsReadOnlyMagazynDo()` blokuje zmianę magazynu, gdy istnieją już pozycje. -- `MagazynZ` i `MagazynDo` **muszą być różne** i oba dostępne (prawa do magazynów / przypisanie definicji do magazynu wg konfiguracji `Ogó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 po `Commit()`. -- 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 publiczna - `void Ewidencjonuj()`, property `[Context] Params Param`. -- `EwidencjonowanieZbiorczeWorker.Params(Context cx)` — konstruktor z `Context`. 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:** - -```csharp -// 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** otwiera `Session.Logout(true)` i kończy `CommitUI()`. Nie - wywołuj go we własnej transakcji edycyjnej (zagnieżdżenie/podwójny commit). Po nim wykonaj - `session.Save()` (w testach `SaveDispose()`). -- `Param` ustaw **przed** `Ewidencjonuj()` — jest to property `[Context]`; bez niej worker - rzuci `NullReferenceException`. -- `Date` i `FromTo` to typy biznesowe — używaj `Date`/`Date.Today`, nie `DateTime` - (`safe-code.md` §10). `FromTo.All` = bez ograniczenia, `FromTo.Empty` worker zamienia na `All`. -- `Definicja` to rekord konfiguracyjny — pobierz istniejący (`DefDokumentow.WgTypu[...]` / - `WgSymbolu[...]`), nie twórz „w locie". Gdy `Definicja == 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()` jako - `RowConflictException` — 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` (namespace `Soneta.Handel.RelacjeDokumentow.Api`): metody przyjmują - `DokumentHandlowy[]` i zwracają `DokumentHandlowy[]`. Dokumenty nadrzędne muszą być - **zatwierdzone**. Dostęp: `session.GetRequiredService()` - (`using Microsoft.Extensions.DependencyInjection;`). - -**Snippet:** - -```csharp -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(); -DokumentHandlowy[] zamowienia = /* zaznaczone, zatwierdzone ZO */; -using (var t = session.Logout(editMode: true)) -{ - DokumentHandlowy[] faktury = rel.NowyPodrzednyIndywidualny(zamowienia, "FV"); - t.Commit(); -} -session.Save(); -``` - -**Pułapki:** -- `IRelacjeService` wymaga, 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 `Stan` musi być w transakcji (`session.Logout(true)`); w workerze/extenderze - `t.CommitUI()` zamiast `t.Commit()`. -- Nie iteruj całej tabeli `DokHandlowe` z `if` w pamięci — filtr serwerowy z zakresem czasowym - (§6.1, §6.3). Zaznaczony w UI zbiór masz w `context` jako `DokumentHandlowy[]`. -- `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:** - -```csharp -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)`), inaczej `AccessWriteDenied`. (W testach wzorzec to - `Save()` → `SaveDispose()` → odczyt na świeżej sesji po `Guid`.) -- Obsłuż `RowConflictException` per paczka (refresh + retry lub log i kontynuacja), nie łap - `Exception` ogólnie (§4, §9.1). Połknięty wyjątek z `Save()` = utrata danych. -- Nie współdziel `Session`/`Row` między wątkami — równoległe przetwarzanie wymaga osobnej sesji - na wątek (§3.1). -- Sesja zawsze w `using`/`try-finally` z `Dispose()` (§1.1); transakcja bez `Commit()` = - 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`](../safe-code.md) i -[`session-login.md`](../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żenia `switch`, -> nazwane parametry `bool`) 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:** - -```csharp -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 lub `null` (klucz unikalny). `WgNIP[condition]` / - `WgNazwy[...]` zwracają **zbiór** — użyj `.FirstOrDefault()`. Nie iteruj całej tabeli `Kontrahenci` - / `Towary` w 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`) przypisanie `dok.Kontrahent = crm.Kontrahenci[Kontrahent.INCYDENTALNY]` rzuca - `ArgumentException` („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 `RowCondition` używaj tylko pól bazodanowych. `JestIncydentalny`, `NazwaFormatowana` itp. 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:** - -```csharp -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.Towar` ustaw **przed** `Ilosc`/`Cena` — to on inicjuje symbol jednostki na pozycji. Konstrukcja - `new 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ść. Dla `false` - zwraca ilość bez przeliczenia (ryzykowne). -- `Quantity` to typ wartość+symbol (nie `double`). Nie mieszaj `Quantity` o różnych symbolach w - arytmetyce — najpierw sprowadź do jednej jednostki przez `PrzeliczJednostkę`. -- `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` (`.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:** - -```csharp -// 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 `LimitKredytowyDokumentu` jest `internal`) — - czytasz pola kontrahenta (`LimitKredytu`, `KontrolaAktywna`) i obsługujesz `InvalidOperationException` - zgłaszany przez platformę przy zatwierdzaniu. -- W bazie Demo `StanUjemnyVerifier` blokuje rozchód bez wcześniejszego **zapisanego** przyjęcia. - Samo `CommitUI` nie księguje zasobów — magazyn księguje się dopiero po `Session.Save()`, więc błąd - pojawia się w `Save()`, nie w transakcji. -- `IsEmpty` na kolekcji `SubTable` to **właściwość** (serwerowy `exists`, bez nawiasów) — nie - materializuj `Pozycje.ToList().Count`. -- Walidację własną rzucaj jako `RowException(dok, "…".Translate())` **przed** `Commit()`. Wyjątek po - `Commit()` 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:** - -```csharp -// 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 w `Commit`). Nie połykaj - `RowConflictException` — 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 rzuci `AccessWriteDenied`. Wzorzec: zapis → świeża sesja → odczyt po `Guid` → 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:** - -```csharp -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` / `LastChangeInfo` są **kalkulowane** (zapytania `select top 1 ... from - ChangeInfos`) — tylko do odczytu, nie ustawiaj. Mogą zwrócić `null`, gdy historia rekordu jest - pusta (np. import bez rejestracji `ChangeInfo`) — zawsze sprawdź `!= null`. -- Rejestracja `ChangeInfo` zależy od konfiguracji (`ChangeInfoMode` per 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`/`LastChangeInfo` to 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:** - -```csharp -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:** -- `Definicja` ustaw **jako pierwszą** — to ona określa wymagane pola (magazyn, kontrahent) oraz - schemat numeracji (`Numeracja`). Zmiana definicji po wypełnieniu dokumentu jest ograniczona - (`IsReadOnlyDefinicja()`). -- `Seria` można ustawić **tylko**, gdy numeracja definicji ma komponent „Seria" — w przeciwnym razie - setter rzuci `RowException` („SeriesDeniedErr"). Sprawdź przez `GetListSeria()` (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"`, - a `Numer.NumerPelny` zawiera 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ę jako `RowException` - z `DuplicateKeyException` w `Save()`. -- `Numer` to obiekt `NumerDokumentu` (bufor numeracji), nie zwykły string — pełny numer czytaj przez - `Numer.NumerPelny` lub `NumerPelnyZapisany`, 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):** -> ```csharp -> using Microsoft.Extensions.DependencyInjection; // GetRequiredService -> using Soneta.Business.UI; // IReportService, ReportResult, ReportFormats, ReportTargets -> -> var raporty = session.GetRequiredService(); -> ``` - -**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` | 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`):** `IReportService` wymaga ustawionego -> `TemplateFileName` i **nie** akceptuje `OutputHandler` ani `ReportName`. `ReportName` -> i `TemplateFileName` wzajemnie 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:** - -```csharp -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(); - -// 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:** -- `GenerateReport` zwraca **`Stream`** dla formatów binarnych (PDF, XLSX, PNG). Dla - `HTML`/`TXT` użyj `GenerateReportStr` (zwraca `string`). Zwrócony strumień **opakuj w `using`**. -- Kontekst musi zawierać wszystko, czego wymaga wzorzec: rekord (`Context.Set(dok)`), - tablicę zaznaczeń **i** instancję parametrów (`ParametryWydrukuDokumentu`). Brak parametru - + `AskForParameters = true` w trybie wsadowym zawiesi się na oczekiwaniu na UI — w kodzie - bez interfejsu zawsze ustaw `AskForParameters = 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 `"(); - -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. Dobierz `TemplateFileName` zgodny 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łaniem `GenerateReport`. - ---- - -### 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: -1. **Zestawienie/raport bazodanowy** — przez `IReportService` z wzorcem zestawienia i - parametrem okresu (analizowalny, zapisywalny do PDF/XLSX) — **ścieżka testowalna**. -2. **Raport fiskalny drukarki** (`RaportDobowy`/`RaportOkresowy`) — wydruk na **drukarce - fiskalnej** przez `IFiscalPrinterAPI` — 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:** - -```csharp -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(); - -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(); -// 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.RaportZaOkres` jest `[Required]`; pusty `FromTo` resetuje 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 ustawienie `RaportOkresowyParams` - i ś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) | - -> `DataType` decyduje, 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:** - -```csharp -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(); - -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. Z `typeof(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 z `TemplateFileName`). -- 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` (tylko rezultat operacji UI). - -**Snippet:** - -```csharp -using Microsoft.Extensions.DependencyInjection; -using Soneta.Business.UI; -using Soneta.Handel; - -var raporty = session.GetRequiredService(); - -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:** -- `GenerateReport` to właściwa droga dla integracji — zwraca strumień, którym dysponujesz - dowolnie (plik, e-mail, sieć). **Zawsze `using`** na zwróconym strumieniu (PDF i inne - formaty binarne). -- `OutputHandler` **nie jest obsługiwany przez `IReportService`** (`CheckConsistency` rzuci - `ArgumentException`). Służy jako rezultat operacji w trybie wzorca (worker/Command z UI), - nie do wsadowego generowania w czystym kodzie biznesowym. -- `Target = Email`/`Attachment` to ścieżki integrujące się z modułem pocztowym (konto - `KontoPocztowe`, szablon `SzablonEmail`) — wymagają pełnej, skonfigurowanej sesji - aplikacyjnej; w czystym kodzie integracyjnym prościej pobrać strumień z `GenerateReport` - i 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 `" - **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 ustawienie `ReportResult`/`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, pola `KSeFKomunikat`). Każdy wzorzec poniżej oznacza, która część -> jest „offline" (testowalna), a która „online/sprzętowa" (NIE testuj — patrz `dh-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`), potem -> `session.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`. Akcja `object 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:** - -```csharp -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`) — inaczej `WeryfikatorPolaXmlValidated` - rzuci wyjątek „nie posiada zweryfikowanego pliku XML". Najpierw wykonaj W69 (Sprawdź strukturę pliku) lub - wygeneruj XML (wysyłka zbiorcza robi to automatycznie dla statusu `Brak`). -- 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: utworzenie `KSeFWyslijParams`, dobór systemu/tokenu z list, - `Weryfikator` oraz ż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` (oraz `Robocze=14`, `Razem=31` - zachowane dla kompatybilności). Status wyliczany z zawartości `KSeFKomunikat` i stanu dokumentu (Bufor/Anulowany ⇒ `NieDotyczy`). -- `dok.KSeFKomunikat` (rekord `KSeFKomunikat`): `NumerDokumentuKSeF: string` (numer nadany przez KSeF — - ustawiony ⇒ status `Przyjety`), `NumerReferencyjnyKSeF: string`, `NumerReferencyjnySesjiKSeF: string`, - `OpisBledu: string` (niepusty ⇒ status `Odrzucony`), `Offline: bool`, `TokenKSeF: SysZewToken`, - `DataPrzeslaniaKSeF`, `DataPrzyjeciaKSeF`. -- `dok.PosiadaKSeF: bool` (ma plik `ImportExportKSeF`), `dok.PodlegaKSeF: bool`, `dok.QRCodeLink: string`. - -**Snippet:** - -```csharp -// 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:** -- `StatusKSeF` jest **kalkulowane** — nie da się go ustawić; zmienia się przez sam `KSeFKomunikat`. -- Sprawdzenie statusu działa tylko, gdy `dok.StatusKSeF != Przyjety` i istnieje `KSeFKomunikat` z numerem - referencyjnym sesji; dokument w stanie `DoWyslania` nie 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.SprawdzStatusDokumentowZSesji` **wymaga sieci** → **NIE testuj jednostkowo**. - W teście weryfikuj jedynie wyliczanie `StatusKSeF` z 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`, metoda `void Check()`. Ustawia - `dok.ImportExportKSeF.XmlValidated: ThreeStateBoolean` (`True`/`False`). Walidator publiczny: - `KSeFSchemaVerifier.Verify(DokumentHandlowy dok)` (rzuca wyjątek przy niezgodności ze schematem). -- `KSeFSprawdzUPODokumentuWorker`: `[Context] Dokument`, `void SprawdzUPO()`. Wymaga - `dok.StatusKSeF == Przyjety` i tokenu w wersji API v2 (`KSeFKomunikat.TokenKSeF.KSeFAPIv2`), inaczej rzuca - `RowException`. Zapisuje rekord `KSeFUPO` i daty `DataPrzeslaniaKSeF`/`DataPrzyjeciaKSeF`. -- `PobierzNumerKSeFZDuplikatuWorker`: akcja `void PobierzNumerDokumentuKSeF(DokumentHandlowy dok)`. - Aktywna, gdy `dok.KSeFKomunikat.OpisBledu` zawiera „440"; z opisu wyłuskuje numer dokumentu i sesji, - ustawia `NumerDokumentuKSeF` / `NumerReferencyjnySesjiKSeF` (status przechodzi na `Przyjety`). - -**Snippet:** - -```csharp -// 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.Xml` - niepusty — `IsEnabledCheck`). -- `SprawdzUPO()` rzuca `RowException`, gdy dokument nie jest `Przyjety` albo nie był wysłany w API v2 — - obsłuż to przed wywołaniem. Samo pobranie UPO **wymaga sieci** → NIE testuj. -- `PobierzNumerDokumentuKSeF` ustawia w `OpisBledu` znacznik `DokumentHandlowy.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`, akcja `object Pobierz()`. - Pobiera tylko, gdy `KSeFZapytanieOFa.StatusZapytania == StatusZapytania.Przetworzono` i nie pobrano - jeszcze wszystkich paczek (`PobraneWszystkie`). -- `ParametryPobieraniaFakturKSeF`: `DataOd: DateTimeOffset`, `DataDo: DateTimeOffset`, - `PodmiotTworzeniaZapytaniaKSeF`, `PobieranieSamofakturowania`. -- Wynik: rekordy `KSeFPlik` (z `RodzajDokumentuKSeFZapytanieOFa`: `Sprzedaz`/`Zakup`/`Razem`) tworzone przez - `KSeFPlik.CreateKSefPlik(...)`. Formularz `FA_RR` jest pomijany. - -**Snippet:** - -```csharp -// 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 o `DokumentHandlowy` — - 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 `ParametryPobieraniaFakturKSeF` i 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`, metoda `void Execute()`. -- `ParametryFiskalizacjiDokumentu : ContextBase` — `SymbolKasy: string` (`[MaxLength(12)]`, „Symbol drukarki"), - `SymbolKasyEnum: string` (combo, gdy dane drukarki w bazie), `GetListSymbolKasyEnum(): List`. -- Pola dokumentu: `dok.SymbolKasy: string` (ustawiane przez `UstawSymbolKasy`), `dok.Kategoria: KategoriaHandlowa`. -- `IsVisibleExecute`: tylko `Sprzedaż`/`KorektaSprzedaży`. `IsEnabledExecute`: dokument **zatwierdzony** - i z **pustym** `SymbolKasy`. - -**Snippet:** - -```csharp -// 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()` z `FiskalizacjaDokumentuWorker` **nie drukuje** — jedynie ustawia `SymbolKasy` i dopisuje - informację o fiskalizacji do zmian zapisu. Faktyczny wydruk realizuje klasa `Fiscalizer` (sprzęt) → NIE testuj. -- Operacja działa wyłącznie dla dokumentów **zatwierdzonych** o pustym `SymbolKasy` (`IsEnabledExecute`) i - kategorii `Sprzedaż`/`KorektaSprzedaży` (`IsVisibleExecute`). -- `SymbolKasy` jest 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.SymbolKasy` i warunki `IsEnabled/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; ustawienie `EParagonAdresEmail` automatycznie ustawia - `EParagon` (poza polityką `OznaczJakoEParagon.Zawsze`). -- `dok.EParagonAdresEmail: string` — adres e-mail odbiorcy e-paragonu (walidowany; przy `EParagon==true` - nie 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`, akcja `object Drukuj()`. - `IsVisibleDrukuj`: definicja `Fiskalizowany`, dokument zatwierdzony, niepusty `SymbolKasy`. - -**Snippet:** - -```csharp -// 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 `EParagonAdresEmail` ma efekt uboczny: dla polityki innej niż `Zawsze` automatycznie ustawia - `EParagon = !string.IsNullOrWhiteSpace(value)`. Przy `EParagon==true` pusty 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`/`EParagonAdresEmail` i wyliczanie polityki `OznaczJakoEParagon`. -- `PonownyWydrukParagonuWorker.Drukuj()` wyświetla pytanie „czy wysłać ponownie" (`MessageBoxInformation`) — - faktyczny wydruk dzieje się w handlerze `YesHandler` przez `Fiscalizer`. -- 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`: akcja `void PrzeliczWgKartoteki(DokumentHandlowy dokument)`. Wycofuje - relacje podrzędne pozycji (`pozycja.PodrzędneRelacje`) i przelicza kompletację wg kartoteki. -- `dok.Definicja.SposobEdycjiKompletacji: Soneta.Handel.SposobEdycjiKompletacji` — gdy `None`, akcja - niewidoczna (`IsVisiblePrzeliczWgKartoteki`). -- Definicje kompletacji w module: `hm.DefDokHandlowych.Kompletacja`, `.KompletacjaRozchód`, - `.KompletacjaPrzychód` (typu `DefDokHandlowego`). -- Powiązania składników/wyrobu: pozycje dokumentu (`dok.Pozycje`) i ich relacje - (`PozycjaDokHandlowego.PodrzędneRelacje` typu `PozycjaRelacjiHandlowej`). - -**Snippet:** - -```csharp -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:** -- `PrzeliczWgKartoteki` **kasuje 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 w `try/finally`) — nie ustawiaj jej - samodzielnie obok wywołania workera. -- Akcja jest niewidoczna dla `SposobEdycjiKompletacji == None` oraz dla dokumentu w stanie `Detached`/`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). Akcja `object 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, gdy `KierunekMagazynu != Brak`). -- Wykonawcza metoda dokumentu: `dok.UaktualnijIntrastat(bool kodCN, bool masa, bool kraj, bool przelicznik): int` - (zwraca liczbę zaktualizowanych pozycji). - -**Snippet:** - -```csharp -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()` rzuca `ApplicationException`, 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ędniaj` są **pomijane** (akcja niewidoczna — - `IsVisibleUpdate`). -- Kraj pochodzenia aktualizowany jest tylko, gdy `dok.KierunekMagazynu != KierunekPartii.Brak` — sam parametr - `Kraj=true` nie 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 `OkresIntrastat` i rodzaju Intrastatu z - definicji — nie ładuj całej tabeli `DokHandlowe` do 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`). 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: Currency` to jej -> przeliczenie na PLN po `Kurs`. Stan uregulowania to `StanRozliczenia` (+ `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`) | -| 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:** - -```csharp -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.Platnosci` to `SubTable` — iteruj serwerowo, nie materializuj do `List` tylko po to, by policzyć - elementy (`IsEmpty`/`Count` są dostępne na kolekcji). Patrz [`rowcondition.md`](references/rowcondition.md). -- `StanRozliczenia.NiePodlega` oznacza płatność **nierozliczaną** (`p.Rozliczana == false`) — nie myl jej - z `Nierozliczony` (rozliczana, ale jeszcze niezapłacona). -- `Kwota` jest w walucie dokumentu; do raportu w PLN użyj `KwotaKsiegi` (W81), nie mnóż „ręcznie". -- „Po terminie" liczysz z `Termin` i `DoRozliczenia` względem `Date.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:** - -```csharp -// 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 == true` i `dok.Definicja.KierunekPlatnosci != Brak` - (`IsVisiblePodzielPlatnosci`) — na zatwierdzonym dokumencie się nie wykona. -- `PodzielPlatnosci` **sam 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łaj `session.Save()`. -- W trybie `Raty` ustawienie `Kwota` przelicza `IlośćRat` (i odwrotnie) — ustaw jedno z dwóch. -- Ostatnia rata przejmuje resztę z zaokrągleń (kwoty rat sumują się do `BruttoCy` dokumentu) — 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:** - -```csharp -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** — `OnAdded` rzuca wyjątek - („Nie można dodawać płatności do zatwierdzonego dokumentu"). `Platnosc.Bufor`/`IsReadOnly` chronią - edycję po zatwierdzeniu. -- Dobierz klasę do kierunku dokumentu: sprzedaż (`KierunekPlatnosci.Przychod`) → `Naleznosc`, zakup - (`Rozchod`) → `Zobowiazanie`. Zła klasa = niespójny kierunek. -- `Kwota` to `Currency` — twórz `new 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 weryfikator `RachunekPodmiotuVerifier`). -- `SposobZaplaty` pobieraj 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:** - -```csharp -// 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ściWorker` działa na **pierwszej** płatności i tylko gdy `Raty <= 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). -- `TerminDni` to dni od **daty odniesienia** (`TerminLiczonyOd`/data dokumentu), nie data bezwzględna — - ustawienie `TerminDni` przelicza `Termin`. -- Edycja terminu może być zablokowana polityką (`IEdycjaTerminuPlatnosci`) — zawsze sprawdzaj - `IsReadOnlyTermin()`/`IsReadOnlyTerminDni()` przed zapisem. -- Zmiana `Sposób` przelicza ewidencję ŚP (subewidencję) — nie ustawiaj `EwidencjaSP` „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:** - -```csharp -// "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łatnik` jest **wyłącznie do odczytu** — to flaga wyliczana z porównania `p.Podmiot` z - `dok.Kontrahent`. Aby „zmienić płatnika", ustaw `Platnosc.Podmiot`, nie próbuj przypisać `InnyPłatnik`. -- `Podmiot` jest read-only, gdy płatność jest częściowo rozliczona (`KwotaRozliczona != 0`) — sprawdzaj - `IsReadOnlyPodmiot()`. -- Zmiana podmiotu przenosi rozrachunek na nowy podmiot i może podmienić zablokowany podmiot na jego - zamiennik (wbudowana logika) — odczytaj `p.Podmiot` po zmianie, nie zakładaj wartości wejściowej. -- `Rachunek` musi należeć do nowego `Podmiot` (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:** - -```csharp -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:** -- `StanRozliczenia` jest kalkulowane z `KwotaRozliczona`/`Kwota` — nie ustawiaj go; rozliczenia powstają - przez operacje kasowe/rozliczeniowe, nie przez bezpośredni zapis na płatności. -- `DataRozliczenia == Date.MaxValue` oznacza „nierozliczona" — nie traktuj `MaxValue` jako realnej daty. -- Rozliczenia są rozdzielone na dwie kolekcje (`Dokumenty` i `Zaplaty`) zależnie od strony powiązania — - do pełnego obrazu przejrzyj obie. -- Dla płatności `Rozliczana == false` (`NiePodlega`) `DoRozliczenia` wynosi 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:** - -```csharp -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.0` i `TabelaKursowa == null` — przeliczeniem zajmuj się tylko, gdy - `Kwota.Symbol != Currency.SystemSymbol`. -- `KwotaKsiegi` wylicza się z `Kwota * Kurs`; jeśli ustawisz tabelę bez kursu na datę dokumentu, kurs może - pozostać `0.0` (brak kursu) — wtedy `KwotaKsiegi` będzie zerowa. Upewnij się, że tabela kursowa ma kurs - na `DataDokumentu` (w bazie Demo brak kursów „na dziś" → operacja walutowa rzuca - `KursWalutyNotFoundException`, 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` (lub `IRowWithKurs.KwotaPLN`), nie przeliczaj `Kwota` wł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:** - -```csharp -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.Rabat` jest **wyliczany** z parametrów kontrahenta (tryb: progresywny / - podstawowy / progowy) i różnicy dni między `Termin` rabatu a terminem płatności — nie ustawiaj go wprost. -- Ustawienie `Termin` < `Date.Today` zeruje 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.Platnosci` zgrupowane po `Termin` → jedna grupa); w przeciwnym razie rzuca `RowException`. -- Edycja może być zablokowana polityką `IEdycjaTerminuPlatnosci` — sprawdzaj `IsReadOnlyTermin()`. -- Naliczenie rabatu wymaga skonfigurowanych parametrów na kontrahencie - (`RodzajRabatuZaTerminPlatnosci`, `Tryb...`, progi/wartości) — bez nich `Rabat` pozostanie `Percent.Zero`. - ---- - -## 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]`. -- [`features.md`](../features.md) — cechy (`Features`), typy, dostęp typowany/nietypowany. -- [`datapack-guidedrow.md`](../datapack-guidedrow.md) — eksport/import, `GuidedRow`. -- [`kontrahent.md`](kontrahent.md) — receptury dla `Kontrahent` (nabywca/odbiorca/płatnik dokumentu). -- [`scan-props.md`](../scan-props.md) / [`scan-workers.md`](../scan-workers.md) — inwentaryzacja pól i workerów. diff --git a/soneta-programming/references/domeny/handel.md b/soneta-programming/references/domeny/handel.md new file mode 100644 index 0000000..2ee12b3 --- /dev/null +++ b/soneta-programming/references/domeny/handel.md @@ -0,0 +1,143 @@ +# 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 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` / `scan-workers.csx`) + +- **Klasa biznesowa:** `Soneta.Handel.DokumentHandlowy` — `GuidedRow` (root), tabela `Soneta.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ęp `session.GetHandel()`. + Tabela dokumentów: `Handel.DokHandlowe`. Definicje: `Handel.DefDokHandlowych` (klucz `WgSymbolu["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` | pozycje dokumentu | +| `SumyVAT` | `SubTable` | tabelka VAT (netto/VAT/brutto wg stawek) | +| `Platnosci` | `SubTable` | 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` | 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 (`HANDEL-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. + +--- + + + +## Mapa receptur + +| Rozdział | Plik | Receptury | +|---|---|---| +| HANDEL01 — Fundamenty i identyfikacja | [handel/HANDEL01-fundamenty.md](handel/HANDEL01-fundamenty.md) | HANDEL-W1–W3 | +| HANDEL02 — Wystawianie dokumentów | [handel/HANDEL02-wystawianie.md](handel/HANDEL02-wystawianie.md) | HANDEL-W4–W11 | +| HANDEL03 — Stany dokumentu i cykl życia | [handel/HANDEL03-cykl-zycia.md](handel/HANDEL03-cykl-zycia.md) | HANDEL-W12–W16 | +| HANDEL04 — Relacje i generowanie dokumentów | [handel/HANDEL04-relacje.md](handel/HANDEL04-relacje.md) | HANDEL-W17–W24 | +| HANDEL05 — Odczyt i wyszukiwanie | [handel/HANDEL05-odczyt.md](handel/HANDEL05-odczyt.md) | HANDEL-W25–W30 | +| HANDEL06 — Magazyn, zasoby, partie, obroty | [handel/HANDEL06-magazyn.md](handel/HANDEL06-magazyn.md) | HANDEL-W31–W39 | +| HANDEL07 — Cechy (Features) | [handel/HANDEL07-cechy.md](handel/HANDEL07-cechy.md) | HANDEL-W40–W42 | +| HANDEL08 — VAT, wartości i waluty | [handel/HANDEL08-vat-waluty.md](handel/HANDEL08-vat-waluty.md) | HANDEL-W43–W47 | +| HANDEL09 — Korekty i dokumenty specjalne | [handel/HANDEL09-korekty.md](handel/HANDEL09-korekty.md) | HANDEL-W48–W52 | +| HANDEL10 — Operacje zbiorcze (batch) | [handel/HANDEL10-batch.md](handel/HANDEL10-batch.md) | HANDEL-W53–W55 | +| HANDEL11 — Operacje pomocnicze (przekrojowe) | [handel/HANDEL11-pomocnicze.md](handel/HANDEL11-pomocnicze.md) | HANDEL-W56–W61 | +| HANDEL12 — Wydruki i raporty | [handel/HANDEL12-wydruki.md](handel/HANDEL12-wydruki.md) | HANDEL-W62–W66 | +| HANDEL13 — Tematy specjalistyczne (KSeF, fiskalizacja, kompletacja, Intrastat) | [handel/HANDEL13-specjalistyczne.md](handel/HANDEL13-specjalistyczne.md) | HANDEL-W67–W74 | +| HANDEL14 — Płatności dokumentu handlowego | [handel/HANDEL14-platnosci.md](handel/HANDEL14-platnosci.md) | HANDEL-W75–W82 | + +## 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]`. +- [`features.md`](../features.md) — cechy (`Features`), typy, dostęp typowany/nietypowany. +- [`datapack-guidedrow.md`](../datapack-guidedrow.md) — eksport/import, `GuidedRow`. +- [`crm.md`](crm.md) — receptury dla `Kontrahent` (nabywca/odbiorca/płatnik dokumentu). +- [`scan-props.md`](../scan-props.md) / [`scan-workers.md`](../scan-workers.md) — inwentaryzacja pól i workerów. diff --git a/soneta-programming/references/domeny/handel/HANDEL01-fundamenty.md b/soneta-programming/references/domeny/handel/HANDEL01-fundamenty.md new file mode 100644 index 0000000..813c802 --- /dev/null +++ b/soneta-programming/references/domeny/handel/HANDEL01-fundamenty.md @@ -0,0 +1,225 @@ +# HANDEL01 — Fundamenty i identyfikacja + +> Wspólne fakty o typie, podstawowe typy i szablon wzorca: [../handel.md](../handel.md). + +> 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, transakcja `session.Logout(true)` + `Commit`/`CommitUI`, +> blokada optymistyczna, praca z `SubTable`) opisują [`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. + +### HANDEL-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:** + +```csharp +// 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żyj + `Soneta.Waluty.WalutyModule.GetInstance(session)`. +- **Nie ładuj całej tabeli `DokHandlowe`** do pamięci z `if`-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 warunku `RowCondition` uż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`, potem `Save`). + +### HANDEL-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:** + +```csharp +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 po `Kategoria`. +- `DefDokHandlowego` to dane **konfiguracyjne** (`GuidedRow`) — odczytuj je, nie twórz „w locie" w + kodzie operacyjnym. + +### HANDEL-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(): bool` oraz `JestDokZaliczkowy(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:** + +```csharp +// 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ównaniu `Symbol == "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 <= HandelOstatni` itd.) lub porównaniem do konkretnych wartości — tak jak + w snippetcie. +- Wartości `*Pierwszy`/`*Ostatni` są oznaczone `[Hidden]` (nie pokazują się w UI), ale to **publiczne** + stałe enuma — wolno ich użyć w kodzie jako granic zakresu. +- `Korekta` i wyniki `JestDokZaliczkowy()` 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.Definicja` może w teorii być `null` na świeżo utworzonym, jeszcze nieskonfigurowanym + dokumencie — przy klasyfikacji dokumentów „w trakcie tworzenia" zabezpiecz dostęp do `Kategoria`. + +--- + diff --git a/soneta-programming/references/domeny/handel/HANDEL02-wystawianie.md b/soneta-programming/references/domeny/handel/HANDEL02-wystawianie.md new file mode 100644 index 0000000..c6f2a39 --- /dev/null +++ b/soneta-programming/references/domeny/handel/HANDEL02-wystawianie.md @@ -0,0 +1,520 @@ +# HANDEL02 — Wystawianie dokumentów + +> Wspólne fakty o typie, podstawowe typy i szablon wzorca: [../handel.md](../handel.md). + +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)`, potem `Definicja` (inicjuje +> kategorię, kierunek magazynu, sposób liczenia VAT, walutę płatności), następnie `Magazyn`, +> `Kontrahent`, daty. Na pozycji najpierw `Towar` (inicjuje jednostkę, stawkę VAT, cenę i rabat), +> dopiero potem `Ilosc`, `Cena`, `Rabat`. Cała operacja w jednej transakcji +> `session.Logout(editMode: true)` zakończonej `Commit()` (kod biznesowy) / `CommitUI()` +> (worker/extender), a po niej `session.Save()` — dopiero `Save()` księguje obroty magazynowe i +> wykrywa konflikty. + +--- + +### HANDEL-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 (HANDEL-W11) | +| FV walutowa | sprzedaż w EUR/USD | patrz **HANDEL-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`, +`Platnosci: SubTable` (powstaje automatycznie wg formy/terminu kontrahenta). + +**Snippet:** + +```csharp +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 HANDEL-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. Samo `CommitUI` nie księguje obrotów — magazyn + aktualizuje się dopiero po `Session.Save()` dokumentu przychodowego. +- `SumyVAT`, `Suma`, `Platnosci` są **wyliczane** z pozycji i parametrów dokumentu — nie ustawiaj + ich ręcznie (ręczna korekta tabeli VAT to osobny mechanizm: `KorektaVAT=true`). +- `LiczonaOd` ustaw 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ć. + +--- + +### HANDEL-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 **HANDEL-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:** + +```csharp +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:** +- `Obcy` to 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. + +--- + +### HANDEL-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:** + +```csharp +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:** +- `Magazyn` jest **wymagany** (`required`) dla dokumentów magazynowych — bez niego `Save()` rzuci + `RowException`. +- `KierunekMagazynu`/`TypPartii` są `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 + `StanUjemnyVerifier` zablokuje `Save()`. +- Obroty/zasoby księgują się **po `Session.Save()`**, nie po `Commit()`/`CommitUI()`. Aby je + odczytać, zapisz dokument i odśwież. + +--- + +### HANDEL-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:** + +```csharp +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**. +- `Dostawa` to 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 + odczytuj `zo.Rezerwacja`, a rezerwacje steruj przez definicję dokumentu i relacje. + +--- + +### HANDEL-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:** + +```csharp +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:** +- **`Towar` ustawiaj jako pierwszy.** Dopiero on inicjuje symbol jednostki na `Ilosc`/`Cena`, stawkę + VAT i proponowaną cenę/rabat. Ustawienie `Ilosc`/`Cena` przed `Towar` operowałoby na pustych + symbolach. +- `Ilosc` to `Quantity` (wartość + symbol jednostki), `Cena` to `DoubleCy` (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`/`Rabat` zapala flagi `KorektaCeny`/`KorektaRabatu` — od tej chwili + platforma **nie przeliczy** już automatycznie tej wartości (np. po zmianie kontrahenta/ilości). +- `Cena` jest netto albo brutto zależnie od `Dokument.LiczonaOd` — interpretuj ją spójnie z + dokumentem. +- Konstruktor pozycji wymaga dokumentu: `new PozycjaDokHandlowego(dok)`, a po nim `session.AddRow(poz)`. + +--- + +### HANDEL-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:** + +```csharp +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 + rzuca `KursWalutyNotFoundException`. Na bazie Demo nie ma kursu EUR „na dziś" — albo dodaj kurs do + tabeli kursowej, albo wpisz kurs ręcznie (`KursWaluty`). +- `TabelaKursowa` jest **wymagana** dla dokumentu walutowego. +- `session.GetWaluty()` jest **internal** — używaj `WalutyModule.GetInstance(session)`. +- Worker `DokumentHandlowyZmianaWalutyWorker` jest klasą **internal** — z dodatku nie tworzysz jej + instancji bezpośrednio; uruchamiasz akcję przez framework Czynności, przekazując publiczną klasę + `DokumentHandlowyZmianaWalutyWorkerParams` przez `Context`. +- Zmiana waluty dokumentu jest możliwa tylko w **buforze** (`dok.Bufor == true`). + +--- + +### HANDEL-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 HANDEL-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:** + +```csharp +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 `StanUjemnyVerifier` nie 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) — `Ilosc` używa symbolu jednostki z karty towaru. + +--- + +### HANDEL-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:** + +```csharp +var crm = session.GetCRM(); + +using (var t = session.Logout(editMode: true)) +{ + // dok utworzony jak w HANDEL-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:** +- `Kontrahent` to **nabywca** (strona transakcji i VAT), `Odbiorca` to fizyczny odbiorca towaru — + to dwa różne pola, oba typu `Kontrahent`. Faktura wystawiana jest na `Kontrahent`, dostawa idzie + do `Odbiorca`. +- `InnyPłatnik` jest **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. +- `OdbiorcaMiejsceDostawy` to referencja do rekordu `Lokalizacja` (zwykle zdefiniowanego u + odbiorcy) — pobierz istniejącą lokalizację, nie twórz „w locie". +- `Dostawa` to 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`. + +--- + diff --git a/soneta-programming/references/domeny/handel/HANDEL03-cykl-zycia.md b/soneta-programming/references/domeny/handel/HANDEL03-cykl-zycia.md new file mode 100644 index 0000000..50e44be --- /dev/null +++ b/soneta-programming/references/domeny/handel/HANDEL03-cykl-zycia.md @@ -0,0 +1,326 @@ +# HANDEL03 — Stany dokumentu i cykl życia + +> Wspólne fakty o typie, podstawowe typy i szablon wzorca: [../handel.md](../handel.md). + +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 w `Save()`) opisuje [`safe-code.md`](../safe-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ść `Zablokowany` ustawia **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`. + +--- + +### HANDEL-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:** + +```csharp +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()`** — samo `Commit()`/`CommitUI()` nie tworzy obrotów ani + zasobów. Jeśli baza blokuje stan ujemny (weryfikator `StanUjemnyVerifier`, jak w bazie Demo), + rozchód (FV/WZ/RW) wymaga **wcześniej zapisanego** przyjęcia (PW/PZ) tego towaru — inaczej `Save()` + rzuci wyjątek. +- Zatwierdzenie uruchamia walidatory dokumentu (kompletność pozycji, magazyn, kontrahent, tabela + VAT). Błędy wychodzą w `Commit()`/`Save()` jako `RowException` — nie połykaj ich (safe-code §4). +- W workerze/extenderze użyj `t.CommitUI()` zamiast `t.Commit()` + ([`worker-extender.md`](../worker-extender.md)). +- Po `Save()` w środku jednej sesji zamyka się okno edycji; kolejna edycja na **tym samym** obiekcie + bez ponownego `Logout` rzuci `AccessWriteDenied`. Wzorzec: zapis → odczyt na świeżej sesji. +- Nie ustawiaj `Stan = Zablokowany` z dodatku — to stan wewnętrzny platformy (np. po zaksięgowaniu w + ewidencji). + +--- + +### HANDEL-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 HANDEL-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:** + +```csharp +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 `Zablokowany` nie cofniesz przez `dok.Stan = Bufor` — blokada wynika z innego + modułu (np. ewidencja zaksięgowana). Do diagnozy/naprawy rozbieżności stanu dokument↔ewidencja służy + `PoprawaStanuDokumentuWorker` (HANDEL-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ż HANDEL-W16. +- To **nie** to samo co anulowanie (HANDEL-W14): cofnięcie wraca do edytowalnego bufora, anulowanie zamyka + dokument w stanie nieodwracalnym. + +--- + +### HANDEL-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:** + +```csharp +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 HANDEL-W13). +- Anulowanie jest **nieodwracalne** — nie ma przejścia `Anulowany → Bufor` na poziomie pola `Stan`. + Gdy chcesz tylko poprawić dokument, użyj cofnięcia do bufora (HANDEL-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()` (HANDEL-W16), a nie anulowanie — + anulowanie zachowuje rekord i numer. + +--- + +### HANDEL-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`; akcja + `public void NaprawStan()`; predykat widoczności + `public static bool IsVisibleNaprawStan(DokumentHandlowy dokument)`. Worker sam zarządza + transakcją wewnątrz `NaprawStan()` (synchronizuje `dok.Stan` z dokumentem ewidencji, w razie potrzeby + tworzy/kasuje ewidencję, może przestawić `Stan` na `Zablokowany`/`Zatwierdzony`). +- `PrzeliczenieStanuWorker`: enum `public enum Opcje { SprawdzićPoprawność, PoprawićTylkoBłędne, + PrzeliczyćTylkoNiepoprawione, PonowniePrzeliczyć, PoprawićObroty, SprawdzićObroty }`; konstruktor + publiczny `PrzeliczenieStanuWorker(Opcje wykonaj, bool wszystkieMagazyny, bool rozchód0, bool + przywracajWartość)`; property `[Context]` `Dokument`, `Towar`, `Magazyny` (`Magazyn[]`); akcja + `public void PrzeliczStan()`. Worker sam otwiera transakcje wewnątrz `PrzeliczStan()`. + +**Snippet:** + +```csharp +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łasnym `session.Logout(true)` — wystarczy `session.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 przez + `Trace`. Do faktycznej naprawy użyj `PoprawićTylkoBłędne`/`PonowniePrzeliczyć`. +- `PrzeliczenieStanuWorker` rzuca `RowException`, 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.IsVisibleNaprawStan` zwraca `false` dla 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. + +--- + +### HANDEL-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 — HANDEL-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 (HANDEL-W13) lub anuluj (HANDEL-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:** + +```csharp +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 w `Save()`), 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 (HANDEL-W13). Jeśli cofnięcie jest + zablokowane (okres zamknięty, podrzędne), rozważ **anulowanie** (HANDEL-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. + +--- + diff --git a/soneta-programming/references/domeny/handel/HANDEL04-relacje.md b/soneta-programming/references/domeny/handel/HANDEL04-relacje.md new file mode 100644 index 0000000..8ef4c43 --- /dev/null +++ b/soneta-programming/references/domeny/handel/HANDEL04-relacje.md @@ -0,0 +1,490 @@ +# HANDEL04 — Relacje i generowanie dokumentów + +> Wspólne fakty o typie, podstawowe typy i szablon wzorca: [../handel.md](../handel.md). + +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 serwis `Soneta.Handel.RelacjeDokumentow.Api.IRelacjeService` +> (scope: `Session`). Workery wykonawcze (`PowiazDokumentyWorker`, `UsunPowiazanieDokumentowWorker`, +> akcje menu „Relacje”) są **internal** — nie instancjonuj ich z dodatku. Pobranie serwisu: +> +> ```csharp +> using Microsoft.Extensions.DependencyInjection; // GetRequiredService +> using Soneta.Handel.RelacjeDokumentow.Api; // IRelacjeService, HandlerSet +> +> var rel = session.GetRequiredService(); // rzuca, gdy serwisu brak +> // albo: var rel = session.GetService(); // zwraca null, gdy brak +> ``` +> +> **Reguł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 przez `session.Save()`. +> - Wynik to `DokumentHandlowy[]` — tablica utworzonych/dołączonych dokumentów podrzędnych. +> - `Context` (zaznaczenie / parametry UI) i `HandlerSet` (callbacki rozstrzygające) są **opcjonalne**. +> Jeśli definicja relacji wymaga rozstrzygnięcia (np. wyboru dostaw, magazynu, pozycji) i **nie +> dostarczysz odpowiedniego callbacka**, platforma rzuci `NotImplementedException`. + +### 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` | definicja relacji ma `WyborPozycji = WybórMagazynu` — wskaż magazyn docelowy | +| `WybierzMagazynDocelowyCallback` | `Func` | wybór magazynu dla dokumentu docelowego (domyślnie `d.MagazynDo`) | +| `WybierzPozycjeCallback` | `Action` | definicja ma `WyborPozycji = WybórPozycji` — zaznacz pozycje (domyślnie `PrzeliczPozycje()`) | +| `WybierzDostawyCallback` | `Action` | wskazanie partii/dostaw przy rozchodzie (gdy `WskazaniePartii` wymuszone) | +| `WybierzDokumentyZaliczkoweCallback` | `Action` | faktura z zaliczkami | +| `UstawParametryFakturowania` | `Action` | 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. + +--- + +### HANDEL-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 HANDEL-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:** + +```csharp +using Microsoft.Extensions.DependencyInjection; +using Soneta.Handel; +using Soneta.Handel.RelacjeDokumentow.Api; + +var rel = session.GetRequiredService(); + +// 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): + +```csharp +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 `HandlerSet` go nie + dostarcza → `NotImplementedException`. Zacznij od wywołania bez `HandlerSet`; 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 `DefRelacji` na `DefDokHandlowego`). Brak pasującej relacji → pusty wynik + lub wyjątek. +- Cała operacja w **jednej** transakcji + `Save()`. Mieszane sesje rekordów → użyj `session.Get(...)`. + +--- + +### HANDEL-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 HANDEL-W17 zwraca zwykle tablicę **jednoelementową** (jeden dokument zbiorczy). + +**Snippet:** + +```csharp +var rel = session.GetRequiredService(); + +// 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 HANDEL-W17 — brak wymaganego callbacka w `HandlerSet` → `NotImplementedException`. +- Nie zakładaj `Length == nadrzedne.Length` — tu wynik jest **agregatem** (zwykle 1 dokument). + +--- + +### HANDEL-W19 — Zbiorcza faktura z wielu dokumentów magazynowych (wiele WZ → 1 FA) + +**Cel:** „odwrotny” kierunek HANDEL-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 HANDEL-W18 — różni się tylko kierunkiem +(nadrzędne = dokumenty magazynowe, symbol podrzędnego = faktura). + +**Snippet:** + +```csharp +var rel = session.GetRequiredService(); + +// 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 `DefRelacji` na 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. + +--- + +### HANDEL-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:** + +```csharp +// 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 `.Length` + przed `[0]`. +- Pola respektują **prawa dostępu** — dokumenty bez prawa odczytu są pomijane (wynik może być węższy + niż faktyczny łańcuch relacji). + +--- + +### HANDEL-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 (HANDEL-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 HANDEL-W17. Pozycje magazynowe mają +`poz.Dostawa` (wskazana partia/dostawa). + +**Snippet (z wyborem partii — wymusza `HandlerSet`):** + +```csharp +using Soneta.Magazyny; + +var rel = session.GetRequiredService(); + +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()) + { + 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 `WZ` ma `WskazaniePartii = WymuszonyDodawanie`, **musisz** dostarczyć + `WybierzDostawyCallback` — inaczej `NotImplementedException`. +- Rozchód wymaga wcześniejszego **zapisanego** przyjęcia towaru (`StanUjemnyVerifier` w Demo). Magazyn + księguje się dopiero po `Session.Save()` — samo `Commit`/`CommitUI` nie tworzy obrotów/zasobów. +- Po wygenerowaniu WZ odczytaj go zwrotnie przez `faktura.DokumentyMagazynowe` (HANDEL-W20). + +--- + +### HANDEL-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ą tylko `Zatwierdzony`). +- `[Context] Params Prms` — parametry; `Params : ContextBase`: + - `DefinicjaDokumentu Definicja` — definicja dokumentu zakupu w bazie klienta (lista z + `DefDokumentow.WgTypu[TypDokumentu.ZakupEwidencja]`); + - `bool PrzygotujPrzelewy` (domyślnie `true`) — 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`):** + +```csharp +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 i `Save()` + 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 Rachunkowe` i 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. + +--- + +### HANDEL-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 `PowiazDokumentyWorker` i +> `UsunPowiazanieDokumentowWorker` są **internal** — nie używaj ich z dodatku. Wiązanie realizuj przez +> `IRelacjeService.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:** +```csharp +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:** + +```csharp +var rel = session.GetRequiredService(); + +// 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:** +- `relationName` musi dokładnie pasować do **nazwy `DefRelacji`** skonfigurowanej w bazie (wielkość + liter / spacje istotne) — niepasująca nazwa daje pusty/`null` wynik w tablicy. +- `Dolacz*` przetwarza dokumenty **pojedynczo** (`Array.ConvertAll`) — wynik na pozycji `i` moż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”). + +--- + +### HANDEL-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` (HANDEL-W20) | `DokumentHandlowy[]` | +| W dół łańcucha (magazynowe dla faktury) | `dok.DokumentyMagazynowe` (HANDEL-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`) → property `StanPokrycia : 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, `StanPokrycia` zwraca `Niezweryfikowane`. + +**Snippet:** + +```csharp +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.StanPokrycia` zwraca `Niezweryfikowane`, 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 traktuj `StanPokrycia` jako **odczyt** stanu policzonego interaktywnie. +- Pola `DokumentyHandlowe`/`DokumentyMagazynowe` respektują prawa dostępu i są kalkulowane — buforuj + wynik, nie wołaj w gęstych pętlach (HANDEL-W20). +- Stan `NiePodlega` oznacza dokument, którego pokrycie nie dotyczy (np. nie jest zamówieniem) — + rozróżniaj go od `Brak` (zamówienie bez realizacji). + +--- + +> **Powiązane sekcje:** tworzenie/stan dokumentu (sekcja 1–2), korekty (`IRelacjeService.NowaKorekta`, +> `NowaKorektaZbiorcza` — analogiczne do HANDEL-W17/HANDEL-W18, symbol korekty opcjonalny), magazyn i partie +> (`dok.Zasoby`, `dok.Obroty`, `GrupaDostaw`). + +--- + diff --git a/soneta-programming/references/domeny/handel/HANDEL05-odczyt.md b/soneta-programming/references/domeny/handel/HANDEL05-odczyt.md new file mode 100644 index 0000000..7dcef27 --- /dev/null +++ b/soneta-programming/references/domeny/handel/HANDEL05-odczyt.md @@ -0,0 +1,395 @@ +# 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`. + +--- + diff --git a/soneta-programming/references/domeny/handel/HANDEL06-magazyn.md b/soneta-programming/references/domeny/handel/HANDEL06-magazyn.md new file mode 100644 index 0000000..8a54e24 --- /dev/null +++ b/soneta-programming/references/domeny/handel/HANDEL06-magazyn.md @@ -0,0 +1,554 @@ +# HANDEL06 — Magazyn, zasoby, partie, obroty + +> Wspólne fakty o typie, podstawowe typy i szablon wzorca: [../handel.md](../handel.md). + +> 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. Samo `Commit()`/`CommitUI()` w transakcji nie nalicza +> stanów. W bazie Demo działa `StanUjemnyVerifier` — **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` (tabela `Zasoby`) — stan towaru: ilość na partii w danym magazynie i okresie. +> - `Obrot` (tabela `Obroty`) — pojedynczy ruch (przychód lub rozchód) wiążący partie. +> - `GrupaDostaw` (tabela `GrupyDostaw`, namespace `Soneta.Magazyny.Dostawy`) — **partia** +> towaru (identyfikowana `Numer` + `Towar`). +> - `OkresMagazynowy` (tabela `OkresyMag`) — przedział czasu, w którym ewidencjonowane są +> obroty/zasoby; po zamknięciu blokuje modyfikacje. +> - `PartiaTowaru` — **subrow** (nie tabela) opisujący stronę partii w `Obrot`/`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`. + +--- + +### HANDEL-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`) | 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:** + +```csharp +// 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.Zasoby` jest **puste, dopóki nie wykonasz `session.Save()`** — przed zapisem magazyn + nie zaksięgował zasobów (sam `Commit`/`CommitUI` nie wystarcza). +- Wzorzec testowy: zapis dokumentu → `SaveDispose()` → odczyt na świeżej sesji po `Guid`, + bo po `Save()` w środku testu okno edycji się zamyka. +- Zasób przychodowy ma `Kierunek == KierunekPartii.Przychód`. Zasób rozchodowy na stanie + ujemnym ma `Kierunek == KierunekPartii.Rozchód` — nie myl ich przy sumowaniu stanu. +- Nie modyfikuj `Zasob`/`Obrot` ręcznie — to tabele wyliczane przez moduł magazynowy. + +--- + +### HANDEL-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:** + +```csharp +// 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()) +{ + if (o.Korekta == KorektaObrotu.StornoZasobu) continue; // ObrotyWszystkie już to pomija + // ... agregacja ilości/wartości +} +``` + +**Pułapki:** +- `dok.Obroty` automatycznie dobiera stronę (przychodowa vs rozchodowa) na podstawie + kierunku magazynowego dokumentu — nie filtruj jej ręcznie po kierunku. +- `ObrotyWszystkie`/`ObrotyWszystkiePozycji`/`ObrotyWszystkieWgPartiiPierwotnej` zwracają + `ListWithView` — iteruj przez `.Cast()`. Pomijają już obroty `StornoZasobu`. +- Obroty pojawiają się **po `Session.Save()`** dokumentu, nie po `Commit()`. +- `Przychod`/`Rozchod`/`PrzychodPierwotny` to **subrow `PartiaTowaru`**, nie rekord partii — + do rekordu `GrupaDostaw` sięgaj przez `.PartiaTowaru`, do dokumentu źródłowego przez + `.Dokument`, do pozycji przez `.PozycjaIdent`. + +--- + +### HANDEL-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`. +`OkresMagazynowy` z `mag.OkresyMag` (patrz HANDEL-W39). Ilości to `Quantity`. + +**Snippet:** + +```csharp +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 HANDEL-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 `Zasoby` do pamięci** — zawsze zawężaj indeksem + (`WgTowar[...]`, `WgMagazyn[...]`, `WgPartiaTowaruMagazyn[...]`). Patrz `safe-code.md` §6. +- Ilości są typu `Quantity` (ilość + jednostka), nie `double` — operuj na `Quantity` i + 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()`. + +--- + +### HANDEL-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` | +| 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:** + +```csharp +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]` + i `WgTowar[towar]` zwracają **zbiór** (`SubTable`). +- W `RowCondition` uż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 (patrz `features.md`). +- `Numer` partii bywa **nadawany automatycznie** (autonumerowanie wg karty towaru lub wg + cechy) — nie zakładaj, że zawsze ustawisz go ręcznie. + +--- + +### HANDEL-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 (HANDEL-W37, HANDEL-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:** + +```csharp +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().FirstOrDefault(); +PozycjaDokHandlowego pozycjaPrzyjecia = przychod?.Przychod.Dokument? + .Pozycje.Cast() + .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`). Przy `Zabroniony` partia jest dobierana wyłącznie + algorytmem magazynu — ustawienie `poz.Dostawa` zostanie zignorowane lub odrzucone. +- `poz.Dostawa` to **pozycja dokumentu przyjęcia** (`PozycjaDokHandlowego`), a nie rekord + `GrupaDostaw`. Partię `GrupaDostaw` mapujesz 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.Dostawa` **przed** `Commit()`; właściwy obrót zostaje naliczony dopiero w + `Save()`. + +--- + +### HANDEL-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 HANDEL-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:** + +```csharp +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().FirstOrDefault(); + PozycjaDokHandlowego dostawa = przychod?.Przychod.Dokument? + .Pozycje.Cast().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.Dostawa` to 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 przy + `WyborPozycjiDlaRelacji != BrakOkna` skutkuje `NotImplementedException`. +- Wszystkie pozycje w jednej transakcji edycyjnej, zapis raz przez `Session.Save()`. + +--- + +### HANDEL-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:** + +```csharp +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() + .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 zapisem + `mag.GrupyDostaw` jej nie zawiera. +- Gdy magazyn ma autonumerowanie `WgCechy`, `GrupaDostaw.Numer` jest **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`. + +--- + +### HANDEL-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:** + +```csharp +// 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) i + `PrzychodPierwotny` (źródło sprzed korekt). Do raportu pochodzenia używaj `Przychod`/ + `PrzychodPierwotny`. +- `obrot.Przychod`/`Rozchod` to **subrow `PartiaTowaru`** — nie jest `null` jako 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). + +--- + +### HANDEL-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:** + +```csharp +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) lub `WgCechy*`, automatyczny dobór partii + zależy od `poz.Dostawa` (HANDEL-W35/HANDEL-W36) lub cechy (`CechaAlgorytmu`) — bez nich rozchód nie + zostanie poprawnie rozliczony. +- `NieLiczyćStanów` oznacza, że magazyn **nie prowadzi zasobów** — `dok.Zasoby` pozostanie + puste, a kontroli stanu ujemnego nie ma. +- Modyfikacja dokumentów w **zamkniętym** okresie (`okres.Zamkniety == true`) zostanie + odrzucona — sprawdź to przed edycją wstecz. +- `OkresMagazynowy` to dane konfiguracyjne (`config="true"`, `guided`) — nie twórz okresów + „w locie" w kodzie operacyjnym; korzystaj z istniejących. + +--- + diff --git a/soneta-programming/references/domeny/handel/HANDEL07-cechy.md b/soneta-programming/references/domeny/handel/HANDEL07-cechy.md new file mode 100644 index 0000000..3dc1330 --- /dev/null +++ b/soneta-programming/references/domeny/handel/HANDEL07-cechy.md @@ -0,0 +1,261 @@ +# HANDEL07 — Cechy (Features) + +> Wspólne fakty o typie, podstawowe typy i szablon wzorca: [../handel.md](../handel.md). + +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 HANDEL-W40. + +--- + +### HANDEL-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 (HANDEL-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 (typ `FeatureCollection` / `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:** + +```csharp +// 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 = true` na + 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 HANDEL-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 przez + `IRelacjeService` (sekcja relacji), a cechy dojdą same. +- Konfigurację definicji rób w sesji **konfiguracyjnej** (`config: true`) — to dane konfiguracyjne, + nie operacyjne (`safe-code.md`). + +--- + +### HANDEL-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]` na `Row` (`dok["Nazwa"]`) — równoważny + `dok.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:** + +```csharp +var handel = session.GetHandel(); +var dok = handel.DokHandlowe.WgDaty[...]; // lub Get(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().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 (`DokHandlowe` dla dokumentu, `PozycjeDokHan` dla 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`), potem `Save`. 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 surowy `decimal`/`double`/`string`. +- Cechy **algorytmiczne**: przypisanie wartości uruchamia algorytm definicji — efekty uboczne; część + cech bywa read-only (`IsReadOnly(fd)` / tryb `SpecialEdit`) i edycja rzuci `AccessDeniedException`. +- W form.xml cechę adresuje się ścieżką `Features.Nazwa` (np. `{Features.NrSerii}`), także przez + relację (`{Kontrahent.Features.Segment}`). +- `dok.Pozycje` to kolekcja pozycji dokumentu — iteruj po niej, nie ładuj całej tabeli + `PozycjeDokHan`. + +--- + +### HANDEL-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:** + +```csharp +// 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"` w `FieldCondition` — `Features.X` nie jest + typowaną property `Row`, więc nie da się jej użyć w wyrażeniu LINQ (`(Row r) => r.Features…`). +- Warunek aplikuj **na indeksie** (`WgKodu[...]`, `WgDaty[...]`) lub na kolekcji `SubTable` + (`dok.Pozycje[...]`) — to wykonuje filtr w SQL. Nie iteruj całej tabeli z `if` w pamięci + (`safe-code.md` §6). +- Wyszukiwanie korzysta z indeksowanego pola `DataKey` cechy; wartość w warunku podawaj w typie + zgodnym z typem cechy (np. `bool` dla cechy Bool, `Date` dla cechy Date) — wartości są zapisane w + ustalonym formacie tekstowym (patrz tabela typów w `references/features.md`). +- `view.Condition &= …` to mechanizm **UI** (ViewInfo/folder); w kodzie biznesowym używaj + `SubTable[condition]`, nie obiektu `View`. +- `DokHandlowe` to tabela operacyjna guided — przy szerokich przekrojach dodatkowo zawężaj zakres + czasowy (data dokumentu), nie tylko warunek na cesze. + +--- + diff --git a/soneta-programming/references/domeny/handel/HANDEL08-vat-waluty.md b/soneta-programming/references/domeny/handel/HANDEL08-vat-waluty.md new file mode 100644 index 0000000..c878f45 --- /dev/null +++ b/soneta-programming/references/domeny/handel/HANDEL08-vat-waluty.md @@ -0,0 +1,333 @@ +# HANDEL08 — VAT, wartości i waluty + +> Wspólne fakty o typie, podstawowe typy i szablon wzorca: [../handel.md](../handel.md). + +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 jako `decimal` (`Netto`, `VAT`, `Brutto`); `BruttoNettoCy` +> — kwoty w walucie dokumentu jako `Currency` (`NettoCy`, `VATCy`, `BruttoCy`). Nie operuj na +> niezaokrąglonych `decimal` — platforma weryfikuje zaokrąglenie (safe-code §10). + +--- + +### HANDEL-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`) | 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` udostępnia: `DefinicjaStawki: +DefinicjaStawkiVat`, `Stawka: StawkaVat` (`Stawka.Procent: Percent`), `Suma: BruttoNetto` +(`Netto`, `VAT`, `Brutto` — `decimal`), `SumaCy: BruttoNettoCy` (`NettoCy`, `VATCy`, `BruttoCy` — +`Currency`), `Dokument: DokumentHandlowy`. + +**Snippet:** + +```csharp +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.SumyVAT` to `SubTable` — 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`, `decimal` w walucie systemowej) od `SumaCy` (`BruttoNettoCy`, + `Currency` w walucie dokumentu). Dla dokumentu walutowego do prezentacji używaj `SumaCy`. +- `Stawka` to `StawkaVat` (typ stawki), `Procent` zwraca `Percent` — nie myl z `decimal`. +- Tabela VAT jest **wyliczana z pozycji** dokumentu (chyba że włączono `KorektaVAT` — patrz HANDEL-W45). Nie + modyfikuj jej, gdy chcesz tylko odczytać wartości. + +--- + +### HANDEL-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:** + +```csharp +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.Suma` to **stan zapisany** podsumowania, a `dok.SumaPozycji` jest **wyliczane na bieżąco** + z pozycji za każdym odczytem. Dla dokumentu w buforze, przed ponownym przeliczeniem, mogą się + chwilowo różnić. +- `SumaPozycji`/`SumaPozycjiTowProd` zwracają `BruttoNettoPozycji` — typ **tylko do odczytu** (brak + setterów); nie próbuj przez nie modyfikować wartości. +- `dok.Rabat` to `Percent` — 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` (HANDEL-W46) i ewentualnej korekty + tabeli VAT (`KorektaVAT`, HANDEL-W45). + +--- + +### HANDEL-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 `KorektaTabeliVATWorker` jest `internal`** — nie da się go zainstancjonować z dodatku +> zewnętrznego. Publiczny tor korekty prowadzi przez flagę `dok.KorektaVAT` i bezpośrednią edycję +> pól wierszy `dok.SumyVAT`. + +**Snippet:** + +```csharp +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 `SumyVAT` bez `dok.KorektaVAT = true` zostanie zablokowana — `SumaVAT` jest 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`/`Brutto` rzuca `ArgumentException`. Zaokrąglaj wejście + (`Soneta.Tools.Math.RoundCy(...)`). +- `KorektaVAT` jest dostępna tylko, gdy definicja dokumentu na to pozwala + (`Definicja.SumyVAT` w trybie korekty) — sprawdzaj `dok.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. +- `DefinicjaStawki` na wierszu `SumaVAT` można zmieniać tylko przy włączonej korekcie + (`IsReadOnlyDefinicjaStawki()` zależy od `KorektaVAT`). + +--- + +### HANDEL-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:** + +```csharp +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:** +- `LiczonaOd` nie przyjmuje wartości `0` (`RequiredException`). Zawsze ustaw konkretny wariant enuma. +- Zmiana `LiczonaOd` na 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 flag `Params` (`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. +- `PrzeliczProceduryVATParams` dziedziczy po `ContextBase` — przy ręcznym tworzeniu przekaż `Context` + do konstruktora. + +--- + +### HANDEL-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 `DokumentHandlowyZmianaWalutyWorker` jest `internal`** — 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 parametry +> `DokumentHandlowyZmianaWalutyWorkerParams`. Z poziomu kodu dodatku zewnętrznego dostępne tory to: +> (1) uruchomienie akcji przez mechanizm Czynności z przygotowanym `Context`, albo (2) bezpośrednie +> ustawienie pól waluty/kursu na dokumencie i pozycjach. + +**Snippet:** + +```csharp +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 `DokumentHandlowyZmianaWalutyWorker` jest `internal` — **nie** wywołasz `new ...Worker(...)` + ani `.ZmienWalute()` z dodatku zewnętrznego. Używaj publicznych `Params` + akcji Czynności lub + bezpośredniej edycji pól dokumentu. +- `session.GetWaluty()` jest **internal** — moduł walut pobieraj przez + `WalutyModule.GetInstance(session)` (namespace `Soneta.Waluty`). +- Jeśli w bazie **brak kursu** na żądaną datę (np. Demo nie ma kursu EUR „na dziś"), platforma rzuci + `KursWalutyNotFoundException`. `KursWaluty` w parametrach wylicza się automatycznie tylko, gdy kurs + istnieje; w przeciwnym razie ustaw `KursWaluty` ręcznie. +- Zmiana waluty ma sens tylko dla dokumentu w **buforze** (`IsVisibleZmienWalute` wymaga + `dok.Bufor`); dla dokumentu zatwierdzonego operacja jest niedostępna. +- `WalutaBazowa` jest read-only — wyznaczana z bieżącej waluty dokumentu (`dok.BruttoCy.Symbol`). + Ustawiasz tylko `Waluta` (docelową). +- Kwoty pieniężne to `Currency` (wartość + symbol), nie `decimal`/`double`. Sam `KursWaluty` jest + `double`. + +--- + +--- + diff --git a/soneta-programming/references/domeny/handel/HANDEL09-korekty.md b/soneta-programming/references/domeny/handel/HANDEL09-korekty.md new file mode 100644 index 0000000..d756b83 --- /dev/null +++ b/soneta-programming/references/domeny/handel/HANDEL09-korekty.md @@ -0,0 +1,298 @@ +# HANDEL09 — Korekty i dokumenty specjalne + +> Wspólne fakty o typie, podstawowe typy i szablon wzorca: [../handel.md](../handel.md). + +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`](../safe-code.md)): +> - Dostęp do serwisu: `var rel = session.GetRequiredService();` (wymaga `using 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), potem `session.Save()`. Magazyn księguje się dopiero po `Save()`. +> - Pola `DokumentKorygowany`, `DokumentyKorygujące`, `DokumentyZaliczkowe` są **kalkulowane (read-only)** — nie ustawiaj ich ręcznie; powstają jako efekt utworzenia relacji. + +--- + +### HANDEL-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:** + +```csharp +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(); + +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:** +- `NowaKorekta` zwraca **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". +- `symbolKorekty` to 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 `Ilosc` musi pochodzić z istniejącej pozycji (`poz.Ilosc.Symbol`) — nie twórz `Quantity` z gołą liczbą. + +--- + +### HANDEL-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 HANDEL-W48 — `IRelacjeService.NowaKorekta(...)`, `PozycjaDokHandlowego.Ilosc/Cena`, `PozycjaKorygowana`. + +**Snippet:** + +```csharp +var rel = session.GetRequiredService(); + +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 `UtworzKorektePrzyjeciaWorker` jest `internal`** — nie da się go zainstancjonować z dodatku zewnętrznego. Publiczny tor to **`IRelacjeService.NowaKorekta`** (wewnętrznie worker robi dokładnie to samo: `NowaKorekta` + dostosowanie `Pozycje[].Ilosc` z 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`/`Cena` na pozycji korekty. +- Magazyn (zasoby/obroty) aktualizuje się dopiero po `session.Save()`, nie po `Commit()`. +- 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. + +--- + +### HANDEL-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:** + +```csharp +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. + +--- + +### HANDEL-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` — wskazanie zaliczek (tor „na pozycje"). +- `HandlerSet.WybierzZaliczkiWgStawkiVatCallback: Action` — tor „wg stawki VAT". +- Worker publiczny do wskazania zaliczki: `DokumentHandlowyRealizacjaZaliczkiWorker` z property `[Context] Dokument: DokumentHandlowy`, `[Context] Docelowy: DokumentDocelowy`, `Wybrany: bool`. +- Odczyt: `dok.DokumentyZaliczkowe` (kalkulowane) — zaliczki powiązane z końcowym; `dok.SumyVAT: SubTable`; `dok.BruttoCy`. + +**Snippet:** + +```csharp +using Microsoft.Extensions.DependencyInjection; +using Soneta.Handel; +using Soneta.Handel.RelacjeDokumentow.Api; + +var rel = session.GetRequiredService(); + +// 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()) { + 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`: `NaPozycje` vs `NaDokument`). +- 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) jest `internal` — z dodatku używaj publicznego `DokumentHandlowyRealizacjaZaliczkiWorker` (wskazanie dokumentów) wewnątrz callbacka. +- Faktura zaliczkowa musi być **zatwierdzona** przed rozliczeniem; `DokumentyZaliczkowe` to pole **kalkulowane** — nie ustawiasz go, czytasz. +- Tabela VAT dokumentu zaliczkowego jest przeliczana proporcjonalnie do wpłaconej zaliczki (logika `DokumentZaliczkowyWorker`) — nie modyfikuj `SumyVAT` ręcznie. + +--- + +### HANDEL-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ęcia `Podrzędne[TypRelacjiHandlowej.PrzesunięcieDo]`; wymaga, by dokument przesunięcia już istniał — ustawiaj po `Definicja`). +- `PozycjaDokHandlowego.Towar`, `Ilosc: Quantity`. + +**Snippet:** + +```csharp +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:** +- `MagazynDo` jest **polem kalkulowanym** delegującym do podrzędnego dokumentu przesunięcia — ustaw je **po** `Definicja` (a najlepiej przed dodaniem pozycji), bo `IsReadOnlyMagazynDo()` blokuje zmianę magazynu, gdy istnieją już pozycje. +- `MagazynZ` i `MagazynDo` **muszą być różne** i oba dostępne (prawa do magazynów / przypisanie definicji do magazynu wg konfiguracji `Ogó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 po `Commit()`. +- Korektę przesunięcia wykonuj przez `IRelacjeService.NowaKorekta` (jak w HANDEL-W48/HANDEL-W49); ręczna korekta partii przy MM jest złożona i wymaga pełnej sesji aplikacyjnej. + +--- + diff --git a/soneta-programming/references/domeny/handel/HANDEL10-batch.md b/soneta-programming/references/domeny/handel/HANDEL10-batch.md new file mode 100644 index 0000000..0d8dfa0 --- /dev/null +++ b/soneta-programming/references/domeny/handel/HANDEL10-batch.md @@ -0,0 +1,222 @@ +# HANDEL10 — Operacje zbiorcze (batch) + +> Wspólne fakty o typie, podstawowe typy i szablon wzorca: [../handel.md](../handel.md). + +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). + +### HANDEL-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 publiczna + `void Ewidencjonuj()`, property `[Context] Params Param`. +- `EwidencjonowanieZbiorczeWorker.Params(Context cx)` — konstruktor z `Context`. 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:** + +```csharp +// 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** otwiera `Session.Logout(true)` i kończy `CommitUI()`. Nie + wywołuj go we własnej transakcji edycyjnej (zagnieżdżenie/podwójny commit). Po nim wykonaj + `session.Save()` (w testach `SaveDispose()`). +- `Param` ustaw **przed** `Ewidencjonuj()` — jest to property `[Context]`; bez niej worker + rzuci `NullReferenceException`. +- `Date` i `FromTo` to typy biznesowe — używaj `Date`/`Date.Today`, nie `DateTime` + (`safe-code.md` §10). `FromTo.All` = bez ograniczenia, `FromTo.Empty` worker zamienia na `All`. +- `Definicja` to rekord konfiguracyjny — pobierz istniejący (`DefDokumentow.WgTypu[...]` / + `WgSymbolu[...]`), nie twórz „w locie". Gdy `Definicja == 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()` jako + `RowConflictException` — obsłuż go (refresh + retry lub eskalacja), nie połykaj (§4). + +### HANDEL-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` (namespace `Soneta.Handel.RelacjeDokumentow.Api`): metody przyjmują + `DokumentHandlowy[]` i zwracają `DokumentHandlowy[]`. Dokumenty nadrzędne muszą być + **zatwierdzone**. Dostęp: `session.GetRequiredService()` + (`using Microsoft.Extensions.DependencyInjection;`). + +**Snippet:** + +```csharp +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(); +DokumentHandlowy[] zamowienia = /* zaznaczone, zatwierdzone ZO */; +using (var t = session.Logout(editMode: true)) +{ + DokumentHandlowy[] faktury = rel.NowyPodrzednyIndywidualny(zamowienia, "FV"); + t.Commit(); +} +session.Save(); +``` + +**Pułapki:** +- `IRelacjeService` wymaga, 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 (HANDEL-W55) — długa transakcja blokuje innych i zwiększa ryzyko konfliktu (§13.1). +- Zmiana `Stan` musi być w transakcji (`session.Logout(true)`); w workerze/extenderze + `t.CommitUI()` zamiast `t.Commit()`. +- Nie iteruj całej tabeli `DokHandlowe` z `if` w pamięci — filtr serwerowy z zakresem czasowym + (§6.1, §6.3). Zaznaczony w UI zbiór masz w `context` jako `DokumentHandlowy[]`. +- `Save()` po operacji relacji może rzucić `RowConflictException` (optimistic lock) — obsłuż (§4). + +### HANDEL-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:** + +```csharp +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)`), inaczej `AccessWriteDenied`. (W testach wzorzec to + `Save()` → `SaveDispose()` → odczyt na świeżej sesji po `Guid`.) +- Obsłuż `RowConflictException` per paczka (refresh + retry lub log i kontynuacja), nie łap + `Exception` ogólnie (§4, §9.1). Połknięty wyjątek z `Save()` = utrata danych. +- Nie współdziel `Session`/`Row` między wątkami — równoległe przetwarzanie wymaga osobnej sesji + na wątek (§3.1). +- Sesja zawsze w `using`/`try-finally` z `Dispose()` (§1.1); transakcja bez `Commit()` = + 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). + +--- + diff --git a/soneta-programming/references/domeny/handel/HANDEL11-pomocnicze.md b/soneta-programming/references/domeny/handel/HANDEL11-pomocnicze.md new file mode 100644 index 0000000..0465635 --- /dev/null +++ b/soneta-programming/references/domeny/handel/HANDEL11-pomocnicze.md @@ -0,0 +1,408 @@ +# HANDEL11 — Operacje pomocnicze (przekrojowe) + +> Wspólne fakty o typie, podstawowe typy i szablon wzorca: [../handel.md](../handel.md). + +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`](../safe-code.md) i +[`session-login.md`](../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żenia `switch`, +> nazwane parametry `bool`) i operuje **wyłącznie na publicznym kontrakcie** platformy. + +--- + +### HANDEL-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 CRM-W3 w `crm.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:** + +```csharp +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 lub `null` (klucz unikalny). `WgNIP[condition]` / + `WgNazwy[...]` zwracają **zbiór** — użyj `.FirstOrDefault()`. Nie iteruj całej tabeli `Kontrahenci` + / `Towary` w 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`) przypisanie `dok.Kontrahent = crm.Kontrahenci[Kontrahent.INCYDENTALNY]` rzuca + `ArgumentException` („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ę (HANDEL-W57). +- W `RowCondition` używaj tylko pól bazodanowych. `JestIncydentalny`, `NazwaFormatowana` itp. są + kalkulowane → w wyrażeniu LINQ rzucą `LinqConditionException`. + +--- + +### HANDEL-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:** + +```csharp +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.Towar` ustaw **przed** `Ilosc`/`Cena` — to on inicjuje symbol jednostki na pozycji. Konstrukcja + `new 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ść. Dla `false` + zwraca ilość bez przeliczenia (ryzykowne). +- `Quantity` to typ wartość+symbol (nie `double`). Nie mieszaj `Quantity` o różnych symbolach w + arytmetyce — najpierw sprowadź do jednej jednostki przez `PrzeliczJednostkę`. +- `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. + +--- + +### HANDEL-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` (`.IsEmpty: bool`), `dok.Kontrahent: +Kontrahent`, `dok.Stan: StanDokumentuHandlowego`. Po stronie kontrahenta (odczyt): +`Kontrahent.LimitKredytu: Currency`, `Kontrahent.TypLimituKredytowego`, `Kontrahent.KontrolaAktywna: +bool` (kalkulowane) — patrz CRM-W9 w `crm.md`. + +**Snippet:** + +```csharp +// 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 `LimitKredytowyDokumentu` jest `internal`) — + czytasz pola kontrahenta (`LimitKredytu`, `KontrolaAktywna`) i obsługujesz `InvalidOperationException` + zgłaszany przez platformę przy zatwierdzaniu. +- W bazie Demo `StanUjemnyVerifier` blokuje rozchód bez wcześniejszego **zapisanego** przyjęcia. + Samo `CommitUI` nie księguje zasobów — magazyn księguje się dopiero po `Session.Save()`, więc błąd + pojawia się w `Save()`, nie w transakcji. +- `IsEmpty` na kolekcji `SubTable` to **właściwość** (serwerowy `exists`, bez nawiasów) — nie + materializuj `Pozycje.ToList().Count`. +- Walidację własną rzucaj jako `RowException(dok, "…".Translate())` **przed** `Commit()`. Wyjątek po + `Commit()` nie wycofa zmiany z sesji (safe-code §5.1). + +--- + +### HANDEL-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:** + +```csharp +// 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 w `Commit`). Nie połykaj + `RowConflictException` — 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 rzuci `AccessWriteDenied`. Wzorzec: zapis → świeża sesja → odczyt po `Guid` → 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. + +--- + +### HANDEL-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:** + +```csharp +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` / `LastChangeInfo` są **kalkulowane** (zapytania `select top 1 ... from + ChangeInfos`) — tylko do odczytu, nie ustawiaj. Mogą zwrócić `null`, gdy historia rekordu jest + pusta (np. import bez rejestracji `ChangeInfo`) — zawsze sprawdź `!= null`. +- Rejestracja `ChangeInfo` zależy od konfiguracji (`ChangeInfoMode` per 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`/`LastChangeInfo` to 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). + +--- + +### HANDEL-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:** + +```csharp +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:** +- `Definicja` ustaw **jako pierwszą** — to ona określa wymagane pola (magazyn, kontrahent) oraz + schemat numeracji (`Numeracja`). Zmiana definicji po wypełnieniu dokumentu jest ograniczona + (`IsReadOnlyDefinicja()`). +- `Seria` można ustawić **tylko**, gdy numeracja definicji ma komponent „Seria" — w przeciwnym razie + setter rzuci `RowException` („SeriesDeniedErr"). Sprawdź przez `GetListSeria()` (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"`, + a `Numer.NumerPelny` zawiera 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ę jako `RowException` + z `DuplicateKeyException` w `Save()`. +- `Numer` to obiekt `NumerDokumentu` (bufor numeracji), nie zwykły string — pełny numer czytaj przez + `Numer.NumerPelny` lub `NumerPelnyZapisany`, nie składaj go ręcznie z serii i liczby. + +--- + +--- + diff --git a/soneta-programming/references/domeny/handel/HANDEL12-wydruki.md b/soneta-programming/references/domeny/handel/HANDEL12-wydruki.md new file mode 100644 index 0000000..4eda2a8 --- /dev/null +++ b/soneta-programming/references/domeny/handel/HANDEL12-wydruki.md @@ -0,0 +1,397 @@ +# HANDEL12 — Wydruki i raporty + +> Wspólne fakty o typie, podstawowe typy i szablon wzorca: [../handel.md](../handel.md). + +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):** +> ```csharp +> using Microsoft.Extensions.DependencyInjection; // GetRequiredService +> using Soneta.Business.UI; // IReportService, ReportResult, ReportFormats, ReportTargets +> +> var raporty = session.GetRequiredService(); +> ``` + +**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` | własna obsługa gotowego strumienia (tryb wzorca; **nieobsługiwane przez `IReportService`** — patrz HANDEL-W66). | +| `ReportName` | `string` | nazwa wydruku z menu (tryb interaktywny; **wyklucza się** z `TemplateFileName`/`IReportService`). | + +> **Reguła spójności (`CheckConsistency`):** `IReportService` wymaga ustawionego +> `TemplateFileName` i **nie** akceptuje `OutputHandler` ani `ReportName`. `ReportName` +> i `TemplateFileName` wzajemnie się wykluczają. Naruszenie → `ArgumentException`. + +--- + +### HANDEL-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:** + +```csharp +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(); + +// 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:** +- `GenerateReport` zwraca **`Stream`** dla formatów binarnych (PDF, XLSX, PNG). Dla + `HTML`/`TXT` użyj `GenerateReportStr` (zwraca `string`). Zwrócony strumień **opakuj w `using`**. +- Kontekst musi zawierać wszystko, czego wymaga wzorzec: rekord (`Context.Set(dok)`), + tablicę zaznaczeń **i** instancję parametrów (`ParametryWydrukuDokumentu`). Brak parametru + + `AskForParameters = true` w trybie wsadowym zawiesi się na oczekiwaniu na UI — w kodzie + bez interfejsu zawsze ustaw `AskForParameters = 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 `"(); + +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. Dobierz `TemplateFileName` zgodny 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łaniem `GenerateReport`. + +--- + +### HANDEL-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: +1. **Zestawienie/raport bazodanowy** — przez `IReportService` z wzorcem zestawienia i + parametrem okresu (analizowalny, zapisywalny do PDF/XLSX) — **ścieżka testowalna**. +2. **Raport fiskalny drukarki** (`RaportDobowy`/`RaportOkresowy`) — wydruk na **drukarce + fiskalnej** przez `IFiscalPrinterAPI` — 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:** + +```csharp +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(); + +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(); +// 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.RaportZaOkres` jest `[Required]`; pusty `FromTo` resetuje 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 ustawienie `RaportOkresowyParams` + i ścieżkę bazodanową `GenerateReport`. + +--- + +### HANDEL-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 (HANDEL-W62) | + +> `DataType` decyduje, 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:** + +```csharp +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(); + +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 HANDEL-W62 to **`DataType = typeof(DokumentHandlowy[])`** — typ tablicowy + przełącza wzorzec w tryb wielu rekordów. Z `typeof(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 z `TemplateFileName`). +- 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). + +--- + +### HANDEL-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` (tylko rezultat operacji UI). + +**Snippet:** + +```csharp +using Microsoft.Extensions.DependencyInjection; +using Soneta.Business.UI; +using Soneta.Handel; + +var raporty = session.GetRequiredService(); + +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:** +- `GenerateReport` to właściwa droga dla integracji — zwraca strumień, którym dysponujesz + dowolnie (plik, e-mail, sieć). **Zawsze `using`** na zwróconym strumieniu (PDF i inne + formaty binarne). +- `OutputHandler` **nie jest obsługiwany przez `IReportService`** (`CheckConsistency` rzuci + `ArgumentException`). Służy jako rezultat operacji w trybie wzorca (worker/Command z UI), + nie do wsadowego generowania w czystym kodzie biznesowym. +- `Target = Email`/`Attachment` to ścieżki integrujące się z modułem pocztowym (konto + `KontoPocztowe`, szablon `SzablonEmail`) — wymagają pełnej, skonfigurowanej sesji + aplikacyjnej; w czystym kodzie integracyjnym prościej pobrać strumień z `GenerateReport` + i 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` (HANDEL-W62, HANDEL-W63, HANDEL-W64-ścieżka bazodanowa, +> HANDEL-W65, HANDEL-W66). Asercja: PDF zaczyna się od `"%PDF"`, HTML od `" - **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 ustawienie `ReportResult`/`RaportOkresowyParams`, bez faktycznego +> druku. + +--- + diff --git a/soneta-programming/references/domeny/handel/HANDEL13-specjalistyczne.md b/soneta-programming/references/domeny/handel/HANDEL13-specjalistyczne.md new file mode 100644 index 0000000..b6e7f42 --- /dev/null +++ b/soneta-programming/references/domeny/handel/HANDEL13-specjalistyczne.md @@ -0,0 +1,505 @@ +# HANDEL13 — Tematy specjalistyczne (KSeF, fiskalizacja, kompletacja, Intrastat) + +> Wspólne fakty o typie, podstawowe typy i szablon wzorca: [../handel.md](../handel.md). + +> 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, pola `KSeFKomunikat`). Każdy wzorzec poniżej oznacza, która część +> jest „offline" (testowalna), a która „online/sprzętowa" (NIE testuj — patrz `dh-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`), potem +> `session.Save()`. Kod zgodny z C# 10. + +--- + +### HANDEL-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`. Akcja `object 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 — HANDEL-W69). + +**Snippet:** + +```csharp +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`) — inaczej `WeryfikatorPolaXmlValidated` + rzuci wyjątek „nie posiada zweryfikowanego pliku XML". Najpierw wykonaj HANDEL-W69 (Sprawdź strukturę pliku) lub + wygeneruj XML (wysyłka zbiorcza robi to automatycznie dla statusu `Brak`). +- 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: utworzenie `KSeFWyslijParams`, dobór systemu/tokenu z list, + `Weryfikator` oraz że XML jest zwalidowany. +- Po wysyłce na dokumencie zatwierdzonym ustawiana jest flaga `Session.SaveImmediatelyIfPossible = true` + (natychmiastowy zapis komunikatu KSeF). + +--- + +### HANDEL-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` (oraz `Robocze=14`, `Razem=31` + zachowane dla kompatybilności). Status wyliczany z zawartości `KSeFKomunikat` i stanu dokumentu (Bufor/Anulowany ⇒ `NieDotyczy`). +- `dok.KSeFKomunikat` (rekord `KSeFKomunikat`): `NumerDokumentuKSeF: string` (numer nadany przez KSeF — + ustawiony ⇒ status `Przyjety`), `NumerReferencyjnyKSeF: string`, `NumerReferencyjnySesjiKSeF: string`, + `OpisBledu: string` (niepusty ⇒ status `Odrzucony`), `Offline: bool`, `TokenKSeF: SysZewToken`, + `DataPrzeslaniaKSeF`, `DataPrzyjeciaKSeF`. +- `dok.PosiadaKSeF: bool` (ma plik `ImportExportKSeF`), `dok.PodlegaKSeF: bool`, `dok.QRCodeLink: string`. + +**Snippet:** + +```csharp +// 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:** +- `StatusKSeF` jest **kalkulowane** — nie da się go ustawić; zmienia się przez sam `KSeFKomunikat`. +- Sprawdzenie statusu działa tylko, gdy `dok.StatusKSeF != Przyjety` i istnieje `KSeFKomunikat` z numerem + referencyjnym sesji; dokument w stanie `DoWyslania` nie 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.SprawdzStatusDokumentowZSesji` **wymaga sieci** → **NIE testuj jednostkowo**. + W teście weryfikuj jedynie wyliczanie `StatusKSeF` z różnych ustawień `KSeFKomunikat`. + +--- + +### HANDEL-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`, metoda `void Check()`. Ustawia + `dok.ImportExportKSeF.XmlValidated: ThreeStateBoolean` (`True`/`False`). Walidator publiczny: + `KSeFSchemaVerifier.Verify(DokumentHandlowy dok)` (rzuca wyjątek przy niezgodności ze schematem). +- `KSeFSprawdzUPODokumentuWorker`: `[Context] Dokument`, `void SprawdzUPO()`. Wymaga + `dok.StatusKSeF == Przyjety` i tokenu w wersji API v2 (`KSeFKomunikat.TokenKSeF.KSeFAPIv2`), inaczej rzuca + `RowException`. Zapisuje rekord `KSeFUPO` i daty `DataPrzeslaniaKSeF`/`DataPrzyjeciaKSeF`. +- `PobierzNumerKSeFZDuplikatuWorker`: akcja `void PobierzNumerDokumentuKSeF(DokumentHandlowy dok)`. + Aktywna, gdy `dok.KSeFKomunikat.OpisBledu` zawiera „440"; z opisu wyłuskuje numer dokumentu i sesji, + ustawia `NumerDokumentuKSeF` / `NumerReferencyjnySesjiKSeF` (status przechodzi na `Przyjety`). + +**Snippet:** + +```csharp +// 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.Xml` + niepusty — `IsEnabledCheck`). +- `SprawdzUPO()` rzuca `RowException`, gdy dokument nie jest `Przyjety` albo nie był wysłany w API v2 — + obsłuż to przed wywołaniem. Samo pobranie UPO **wymaga sieci** → NIE testuj. +- `PobierzNumerDokumentuKSeF` ustawia w `OpisBledu` znacznik `DokumentHandlowy.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. + +--- + +### HANDEL-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`, akcja `object Pobierz()`. + Pobiera tylko, gdy `KSeFZapytanieOFa.StatusZapytania == StatusZapytania.Przetworzono` i nie pobrano + jeszcze wszystkich paczek (`PobraneWszystkie`). +- `ParametryPobieraniaFakturKSeF`: `DataOd: DateTimeOffset`, `DataDo: DateTimeOffset`, + `PodmiotTworzeniaZapytaniaKSeF`, `PobieranieSamofakturowania`. +- Wynik: rekordy `KSeFPlik` (z `RodzajDokumentuKSeFZapytanieOFa`: `Sprzedaz`/`Zakup`/`Razem`) tworzone przez + `KSeFPlik.CreateKSefPlik(...)`. Formularz `FA_RR` jest pomijany. + +**Snippet:** + +```csharp +// 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 o `DokumentHandlowy` — + 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 `ParametryPobieraniaFakturKSeF` i statusie zapytania; + testuj wyłącznie logikę przygotowania parametrów (okres, podmiot), nie samo pobieranie. + +--- + +### HANDEL-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`, metoda `void Execute()`. +- `ParametryFiskalizacjiDokumentu : ContextBase` — `SymbolKasy: string` (`[MaxLength(12)]`, „Symbol drukarki"), + `SymbolKasyEnum: string` (combo, gdy dane drukarki w bazie), `GetListSymbolKasyEnum(): List`. +- Pola dokumentu: `dok.SymbolKasy: string` (ustawiane przez `UstawSymbolKasy`), `dok.Kategoria: KategoriaHandlowa`. +- `IsVisibleExecute`: tylko `Sprzedaż`/`KorektaSprzedaży`. `IsEnabledExecute`: dokument **zatwierdzony** + i z **pustym** `SymbolKasy`. + +**Snippet:** + +```csharp +// 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()` z `FiskalizacjaDokumentuWorker` **nie drukuje** — jedynie ustawia `SymbolKasy` i dopisuje + informację o fiskalizacji do zmian zapisu. Faktyczny wydruk realizuje klasa `Fiscalizer` (sprzęt) → NIE testuj. +- Operacja działa wyłącznie dla dokumentów **zatwierdzonych** o pustym `SymbolKasy` (`IsEnabledExecute`) i + kategorii `Sprzedaż`/`KorektaSprzedaży` (`IsVisibleExecute`). +- `SymbolKasy` jest 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.SymbolKasy` i warunki `IsEnabled/IsVisible` — nie symuluj wydruku. + +--- + +### HANDEL-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; ustawienie `EParagonAdresEmail` automatycznie ustawia + `EParagon` (poza polityką `OznaczJakoEParagon.Zawsze`). +- `dok.EParagonAdresEmail: string` — adres e-mail odbiorcy e-paragonu (walidowany; przy `EParagon==true` + nie 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`, akcja `object Drukuj()`. + `IsVisibleDrukuj`: definicja `Fiskalizowany`, dokument zatwierdzony, niepusty `SymbolKasy`. + +**Snippet:** + +```csharp +// 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 `EParagonAdresEmail` ma efekt uboczny: dla polityki innej niż `Zawsze` automatycznie ustawia + `EParagon = !string.IsNullOrWhiteSpace(value)`. Przy `EParagon==true` pusty 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`/`EParagonAdresEmail` i wyliczanie polityki `OznaczJakoEParagon`. +- `PonownyWydrukParagonuWorker.Drukuj()` wyświetla pytanie „czy wysłać ponownie" (`MessageBoxInformation`) — + faktyczny wydruk dzieje się w handlerze `YesHandler` przez `Fiscalizer`. +- Ponowny wydruk dostępny tylko dla dokumentu z definicji **fiskalizowanej**, zatwierdzonego, z niepustym + `SymbolKasy` (czyli już raz zafiskalizowanego). + +--- + +### HANDEL-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`: akcja `void PrzeliczWgKartoteki(DokumentHandlowy dokument)`. Wycofuje + relacje podrzędne pozycji (`pozycja.PodrzędneRelacje`) i przelicza kompletację wg kartoteki. +- `dok.Definicja.SposobEdycjiKompletacji: Soneta.Handel.SposobEdycjiKompletacji` — gdy `None`, akcja + niewidoczna (`IsVisiblePrzeliczWgKartoteki`). +- Definicje kompletacji w module: `hm.DefDokHandlowych.Kompletacja`, `.KompletacjaRozchód`, + `.KompletacjaPrzychód` (typu `DefDokHandlowego`). +- Powiązania składników/wyrobu: pozycje dokumentu (`dok.Pozycje`) i ich relacje + (`PozycjaDokHandlowego.PodrzędneRelacje` typu `PozycjaRelacjiHandlowej`). + +**Snippet:** + +```csharp +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:** +- `PrzeliczWgKartoteki` **kasuje 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 w `try/finally`) — nie ustawiaj jej + samodzielnie obok wywołania workera. +- Akcja jest niewidoczna dla `SposobEdycjiKompletacji == None` oraz dla dokumentu w stanie `Detached`/`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). + +--- + +### HANDEL-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). Akcja `object 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, gdy `KierunekMagazynu != Brak`). +- Wykonawcza metoda dokumentu: `dok.UaktualnijIntrastat(bool kodCN, bool masa, bool kraj, bool przelicznik): int` + (zwraca liczbę zaktualizowanych pozycji). + +**Snippet:** + +```csharp +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()` rzuca `ApplicationException`, 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ędniaj` są **pomijane** (akcja niewidoczna — + `IsVisibleUpdate`). +- Kraj pochodzenia aktualizowany jest tylko, gdy `dok.KierunekMagazynu != KierunekPartii.Brak` — sam parametr + `Kraj=true` nie 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 `OkresIntrastat` i rodzaju Intrastatu z + definicji — nie ładuj całej tabeli `DokHandlowe` do pamięci (safe-code §6). + +--- + diff --git a/soneta-programming/references/domeny/handel/HANDEL14-platnosci.md b/soneta-programming/references/domeny/handel/HANDEL14-platnosci.md new file mode 100644 index 0000000..e72cfe2 --- /dev/null +++ b/soneta-programming/references/domeny/handel/HANDEL14-platnosci.md @@ -0,0 +1,453 @@ +# HANDEL14 — Płatności dokumentu handlowego + +> Wspólne fakty o typie, podstawowe typy i szablon wzorca: [../handel.md](../handel.md). + +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`). 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: Currency` to jej +> przeliczenie na PLN po `Kurs`. Stan uregulowania to `StanRozliczenia` (+ `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. + +--- + +### HANDEL-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`) | +| 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:** + +```csharp +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.Platnosci` to `SubTable` — iteruj serwerowo, nie materializuj do `List` tylko po to, by policzyć + elementy (`IsEmpty`/`Count` są dostępne na kolekcji). Patrz [`rowcondition.md`](references/rowcondition.md). +- `StanRozliczenia.NiePodlega` oznacza płatność **nierozliczaną** (`p.Rozliczana == false`) — nie myl jej + z `Nierozliczony` (rozliczana, ale jeszcze niezapłacona). +- `Kwota` jest w walucie dokumentu; do raportu w PLN użyj `KwotaKsiegi` (HANDEL-W81), nie mnóż „ręcznie". +- „Po terminie" liczysz z `Termin` i `DoRozliczenia` względem `Date.Today` — w samej płatności nie ma + gotowego pola „kwota po terminie". + +--- + +### HANDEL-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:** + +```csharp +// 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 == true` i `dok.Definicja.KierunekPlatnosci != Brak` + (`IsVisiblePodzielPlatnosci`) — na zatwierdzonym dokumencie się nie wykona. +- `PodzielPlatnosci` **sam 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łaj `session.Save()`. +- W trybie `Raty` ustawienie `Kwota` przelicza `IlośćRat` (i odwrotnie) — ustaw jedno z dwóch. +- Ostatnia rata przejmuje resztę z zaokrągleń (kwoty rat sumują się do `BruttoCy` dokumentu) — nie zakładaj + równego podziału co do grosza. + +--- + +### HANDEL-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:** + +```csharp +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** — `OnAdded` rzuca wyjątek + („Nie można dodawać płatności do zatwierdzonego dokumentu"). `Platnosc.Bufor`/`IsReadOnly` chronią + edycję po zatwierdzeniu. +- Dobierz klasę do kierunku dokumentu: sprzedaż (`KierunekPlatnosci.Przychod`) → `Naleznosc`, zakup + (`Rozchod`) → `Zobowiazanie`. Zła klasa = niespójny kierunek. +- `Kwota` to `Currency` — twórz `new 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 weryfikator `RachunekPodmiotuVerifier`). +- `SposobZaplaty` pobieraj z tabeli (`kasa.SposobyZaplaty.Gotówka`, `...WgNazwy["Przelew"]`) — to rekord + konfiguracyjny, nie ustawiaj „z palca". + +--- + +### HANDEL-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` (HANDEL-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 HANDEL-W9). + +**Snippet:** + +```csharp +// 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ściWorker` działa na **pierwszej** płatności i tylko gdy `Raty <= 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 (HANDEL-W77) albo użyj podziału (HANDEL-W76). +- `TerminDni` to dni od **daty odniesienia** (`TerminLiczonyOd`/data dokumentu), nie data bezwzględna — + ustawienie `TerminDni` przelicza `Termin`. +- Edycja terminu może być zablokowana polityką (`IEdycjaTerminuPlatnosci`) — zawsze sprawdzaj + `IsReadOnlyTermin()`/`IsReadOnlyTerminDni()` przed zapisem. +- Zmiana `Sposób` przelicza ewidencję ŚP (subewidencję) — nie ustawiaj `EwidencjaSP` „obok", licz na + spójność workera. + +--- + +### HANDEL-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:** + +```csharp +// "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łatnik` jest **wyłącznie do odczytu** — to flaga wyliczana z porównania `p.Podmiot` z + `dok.Kontrahent`. Aby „zmienić płatnika", ustaw `Platnosc.Podmiot`, nie próbuj przypisać `InnyPłatnik`. +- `Podmiot` jest read-only, gdy płatność jest częściowo rozliczona (`KwotaRozliczona != 0`) — sprawdzaj + `IsReadOnlyPodmiot()`. +- Zmiana podmiotu przenosi rozrachunek na nowy podmiot i może podmienić zablokowany podmiot na jego + zamiennik (wbudowana logika) — odczytaj `p.Podmiot` po zmianie, nie zakładaj wartości wejściowej. +- `Rachunek` musi należeć do nowego `Podmiot` (twardy weryfikator) — po zmianie płatnika zweryfikuj/wyczyść + rachunek. + +--- + +### HANDEL-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:** + +```csharp +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:** +- `StanRozliczenia` jest kalkulowane z `KwotaRozliczona`/`Kwota` — nie ustawiaj go; rozliczenia powstają + przez operacje kasowe/rozliczeniowe, nie przez bezpośredni zapis na płatności. +- `DataRozliczenia == Date.MaxValue` oznacza „nierozliczona" — nie traktuj `MaxValue` jako realnej daty. +- Rozliczenia są rozdzielone na dwie kolekcje (`Dokumenty` i `Zaplaty`) zależnie od strony powiązania — + do pełnego obrazu przejrzyj obie. +- Dla płatności `Rozliczana == false` (`NiePodlega`) `DoRozliczenia` wynosi zero — nie analizuj jej jak + zaległości. + +--- + +### HANDEL-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:** + +```csharp +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.0` i `TabelaKursowa == null` — przeliczeniem zajmuj się tylko, gdy + `Kwota.Symbol != Currency.SystemSymbol`. +- `KwotaKsiegi` wylicza się z `Kwota * Kurs`; jeśli ustawisz tabelę bez kursu na datę dokumentu, kurs może + pozostać `0.0` (brak kursu) — wtedy `KwotaKsiegi` będzie zerowa. Upewnij się, że tabela kursowa ma kurs + na `DataDokumentu` (w bazie Demo brak kursów „na dziś" → operacja walutowa rzuca + `KursWalutyNotFoundException`, 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` (lub `IRowWithKurs.KwotaPLN`), nie przeliczaj `Kwota` własnym + kursem. + +--- + +### HANDEL-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` (HANDEL-W77/HANDEL-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:** + +```csharp +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.Rabat` jest **wyliczany** z parametrów kontrahenta (tryb: progresywny / + podstawowy / progowy) i różnicy dni między `Termin` rabatu a terminem płatności — nie ustawiaj go wprost. +- Ustawienie `Termin` < `Date.Today` zeruje 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.Platnosci` zgrupowane po `Termin` → jedna grupa); w przeciwnym razie rzuca `RowException`. +- Edycja może być zablokowana polityką `IEdycjaTerminuPlatnosci` — sprawdzaj `IsReadOnlyTermin()`. +- Naliczenie rabatu wymaga skonfigurowanych parametrów na kontrahencie + (`RodzajRabatuZaTerminPlatnosci`, `Tryb...`, progi/wartości) — bez nich `Rabat` pozostanie `Percent.Zero`. + +--- + diff --git a/soneta-programming/references/domeny/kadry.md b/soneta-programming/references/domeny/kadry.md new file mode 100644 index 0000000..470da8a --- /dev/null +++ b/soneta-programming/references/domeny/kadry.md @@ -0,0 +1,98 @@ +# Pracownik / Kadry-Płace — receptury kodu biznesowego (Soneta / enova365) + +Zbiór gotowych wzorców kodu dla domeny **Kadry i Płace**: obiekt biznesowy +**`Soneta.Kadry.Pracownik`** (tabela `Pracownicy`) wraz z jego historią kadrową, etatem, +nieobecnościami, planem pracy, umowami cywilnoprawnymi i wypłatami. Dokument jest częścią skilla +`soneta-programming`. Celem jest, aby agent pisał **bezbłędny kod biznesowy** operujący na +pracowniku — trafiający w realne pola, kolekcje i workery platformy. + +> Format **zwarty**: każdy wzorzec opisuje ogólny przypadek + tabelę wariantów. Fundamenty (sesja, +> transakcja, blokada optymistyczna, praca z `SubTable`, obsługa błędów, wywoływanie workeró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.Kadry.Pracownik` — `GuidedRow` (root), tabela `Soneta.Kadry.Pracownicy`. +- **Moduły i dostęp z sesji:** + - `Soneta.Kadry.KadryModule` — `session.GetKadry()`; tabela `kadry.Pracownicy`. + - `Soneta.Place.PlaceModule` — `session.GetPlace()`; wypłaty, listy płac, definicje elementów. + - `Soneta.Kalend.KalendModule` — `session.GetKalend()`; nieobecności, kalendarze, plan pracy, RCP, limity. + - `Soneta.HR.HRModule` (`session.GetHR()`), `Soneta.HR2.HR2Module` (`session.GetHR2()`) — definicje + stanowisk, struktura, ZZL/oceny/rekrutacja. +- **Obiekt historyczny:** dane kadrowe i warunki etatu obowiązują „od–do" i są przechowywane w + zapisach historycznych. Kolekcja `Pracownik.Historia: HistorySubTable`. + Rekord `PracHistoria` (tabela `PracHistorie`, child pracownika) zawiera m.in. złożone pole + `Etat: Soneta.Kadry.Etat` (warunki zatrudnienia), adresy, dane podatkowe/ubezpieczeniowe. +- **Najważniejsze pola bazodanowe `Pracownik` (poziom root):** `Kod: string`, `Nazwisko: string`, + `Imie: string`, `PESEL: string`, `ArchiwumInfo`, `NumerRachunkuUS`, `NumerRachunkuZUS`. + (Większość danych kadrowych jest w `PracHistoria`, nie na root.) +- **Kluczowe kolekcje (`SubTable`) na `Pracownik`:** + - `Historia: HistorySubTable` — zapisy historyczne (dane kadrowe + `Etat`). + - `Nieobecnosci: FromToSubTable` — nieobecności. + - `Limity: SubTable` — limity nieobecności (np. urlop). + - `Dodatki: SubTable` — stałe elementy wynagrodzenia (dodatki). + - `Akordy: SubTable` — akordy. + - `Umowy: SubTable` — umowy cywilnoprawne; `UmowyZewnetrzne: SubTable`. + - `Rachunki: SubTable` — rachunki bankowe pracownika. + - `DniPracy: DateSubTable` — plan/realizacja czasu pracy (dzień po dniu). + - `DniRCP: DateSubTable` — zarejestrowany czas pracy (RCP). + - `DniPlanu: DateSubTable` — plan pracy (harmonogram). + - `Kalendarze: SubTable` — kalendarze pracownika. + - `PlanowaneWypłaty`, `PlanowaneElementy`, `PlanowaneNieobecności` — dane planistyczne. +- **Cechy:** `Features: Soneta.Business.FeatureCollection` (indeksator po nazwie definicji cechy). +- **Dane w bazie Demo (GoldStandard):** ~80 zatrudnionych pracowników etatowych, kody `"006"`, `"007"`, + `"008"`, … (po jednym zapisie historii każdy). To stabilne punkty wejścia do scenariuszy odczytu. + +## Podstawowe typy domenowe + +| Typ | Namespace | Zastosowanie | +|---|---|---| +| `Date` | `Soneta.Types` | data bez czasu (daty zatrudnienia, obowiązywania) | +| `FromTo` | `Soneta.Types` | zakres dat „od–do" (okres etatu, nieobecności); `FromTo.Parse`, `FromTo.Year` | +| `Time` | `Soneta.Types` | czas/norma (np. norma dobowa `8:00`) | +| `Fraction` | `Soneta.Types` | wymiar etatu jako ułamek (np. `Fraction.One` = pełny etat, `1/2`) | +| `Currency` / `decimal` | `Soneta.Types` / — | kwoty (stawka, wartość wypłaty) | +| `YearMonth` | `Soneta.Types` | miesiąc rozliczeniowy (okres wypłaty) | + +## Szablon wzorca + +Każdy wzorzec (`KADRY-Xn`, gdzie `X` = litera sekcji z listy zadań) ma stałą strukturę: + +- **Cel** — co robi i kiedy go użyć. +- **Warianty** — tabela odmian przypadku (gdy dotyczy). +- **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. + +> **Konwencja testów:** każdy wzorzec ma odpowiadający test w +> `Soneta.Skills.Test/KadryPlace/Pracownik/` (klasa dziedzicząca z `PracownikTestBase : TestBase`). +> Testy są wykonywane na bazie Demo z automatycznym rollbackiem — można w nich tworzyć i modyfikować +> dowolne dane. Stanowią wykonywalną dokumentację publicznego API. + +--- + + + + +## Mapa receptur + +| Rozdział | Plik | Receptury | +|---|---|---| +| KADRY01 — Pracownik — zatrudnienie i dane kartotekowe | [kadry/KADRY01-pracownik.md](kadry/KADRY01-pracownik.md) | KADRY-A* | +| KADRY02 — Etat — zatrudnienie etatowe | [kadry/KADRY02-etat.md](kadry/KADRY02-etat.md) | KADRY-B* | +| KADRY03 — Dodatki, potrącenia, akordy | [kadry/KADRY03-dodatki-potracenia.md](kadry/KADRY03-dodatki-potracenia.md) | KADRY-C* | +| KADRY04 — Nieobecności i czas pracy | [kadry/KADRY04-nieobecnosci.md](kadry/KADRY04-nieobecnosci.md) | KADRY-D* | +| KADRY05 — Plan pracy i kalendarz | [kadry/KADRY05-plan-pracy.md](kadry/KADRY05-plan-pracy.md) | KADRY-E* | +| KADRY06 — RCP — rejestracja czasu pracy | [kadry/KADRY06-rcp.md](kadry/KADRY06-rcp.md) | KADRY-F* | +| KADRY07 — Umowy cywilnoprawne | [kadry/KADRY07-umowy.md](kadry/KADRY07-umowy.md) | KADRY-G* | +| KADRY08 — Płace — naliczanie wypłat | [kadry/KADRY08-place.md](kadry/KADRY08-place.md) | KADRY-H* | +| KADRY09 — Listy płac, przelewy, wydruki | [kadry/KADRY09-listy-place.md](kadry/KADRY09-listy-place.md) | KADRY-I* | +| KADRY10 — Deklaracje (ZUS, PIT, PFRON, PPK) | [kadry/KADRY10-deklaracje.md](kadry/KADRY10-deklaracje.md) | KADRY-J* | +| KADRY11 — Ewidencje pracownicze | [kadry/KADRY11-ewidencje.md](kadry/KADRY11-ewidencje.md) | KADRY-K* | + diff --git a/soneta-programming/references/domeny/kadry/KADRY01-pracownik.md b/soneta-programming/references/domeny/kadry/KADRY01-pracownik.md new file mode 100644 index 0000000..7abbc66 --- /dev/null +++ b/soneta-programming/references/domeny/kadry/KADRY01-pracownik.md @@ -0,0 +1,1247 @@ +# KADRY01 — Pracownik — zatrudnienie i dane kartotekowe + +> Wspólne fakty o typie, podstawowe typy i szablon wzorca: [../kadry.md](../kadry.md). + +> **Model „root + historia".** `Pracownik` (root, tabela `Pracownicy`) trzyma tylko nieliczne pola +> niezmienne w czasie (`Kod`, `Net`, `NumerRachunkuUS`, `NumerRachunkuZUS`, `Rodzaj`, `Typ`, +> `Wieloetatowosc`). **Praktycznie wszystkie dane kadrowe są historyczne** i leżą w zapisach +> `PracHistoria` (tabela `PracHistorie`, child `Pracownik`-a, też `GuidedRow` root z własnym Guid). +> Kolekcją zapisów jest `Pracownik.Historia: HistorySubTable`. Warunki +> zatrudnienia (umowa, wymiar, ubezpieczenia, stanowisko) siedzą w złożonym polu +> `PracHistoria.Etat: Soneta.Kadry.Etat`. +> +> **Skróty na rootcie** (delegują do zapisu „na dziś"/ostatniego): +> - `pracownik.Last : PracHistoria` — **ostatni** (najświehigy) zapis historii; do edycji świeżo +> utworzonego pracownika i odczytu „bieżących" danych kartotekowych. +> - `pracownik[date] : PracHistoria` — indeksator zwracający zapis **obowiązujący na zadaną datę**. +> - Wiele pól osobowych jest też dostępnych z poziomu rootu jako property delegujące (np. `Imie`, +> `Nazwisko`, `PESEL`) — ale **kanonicznie ustawiamy je na zapisie** (`Last.Imie`, `pracownik[d].PESEL`). +> +> **`Pracownik` jest klasą abstrakcyjną** — nie da się zrobić `new Pracownik()`. Konkretny typ +> pracownika firmy to **`Soneta.Kadry.PracownikFirmy`**. + +### KADRY-A1 — Zatrudnienie nowego pracownika (★) + +**Cel:** utworzyć nowego pracownika z minimalnym kompletem danych pozwalającym na `Session.Save()`. + +**Mechanizm (kluczowy):** dodanie nowego `PracownikFirmy` do tabeli (`AddRow`) automatycznie tworzy +**pierwszy zapis** `PracHistoria` oraz kalendarz pracownika (dzieje się w `OnAdded`). Dlatego **nie +tworzymy** `PracHistoria` ręcznie przy zatrudnieniu — bezpośrednio po `AddRow` istnieje już +`pracownik.Last` (pierwszy zapis), na którym ustawiamy dane osobowe. + +**Pola minimalne do zapisu:** + +| Pole | Gdzie | Typ | Uwaga | +|---|---|---|---| +| `Kod` | `Pracownik` (root) | `string` | identyfikator; przy pustym platforma podstawia prefiks + `?` | +| `Imie` | `PracHistoria` (`Last.Imie`) | `string` | wymagane (domyślnie `"?"` z `OnAdded`) | +| `Nazwisko` | `PracHistoria` (`Last.Nazwisko`) | `string` | wymagane (domyślnie `"?"`) | + +Pierwszy zapis historii ma okres otwarty (do `Date.MaxValue`); warunki etatu (KADRY-A1 → B) ustawia się +na `Last.Etat` (np. `Etat.Okres`, `Etat.TypUmowy`, `Etat.Wymiar`) — szczegóły w sekcji B. + +**Snippet:** + +```csharp +var kadry = session.GetKadry(); + +using (var t = session.Logout(editMode: true)) +{ + // AddRow zwraca dodany, typowany wiersz; w OnAdded powstaje pierwszy PracHistoria + kalendarz + var pracownik = session.AddRow(new PracownikFirmy()); + + pracownik.Kod = "555"; // pole rootu + + // dane osobowe — na ZAPISIE historii (pierwszy zapis = Last) + pracownik.Last.Nazwisko = "Kowalska"; + pracownik.Last.Imie = "Gabriela"; + pracownik.Last.PESEL = "94010812345"; + + t.Commit(); // Commit() w kodzie biznesowym; CommitUI() w workerze/UI +} +session.Save(); // zapis do bazy; tu wykrywane konflikty/duplikaty +``` + +**Pułapki:** +- **Nie** rób `new Pracownik()` — typ jest abstrakcyjny. Używaj `new PracownikFirmy()`. +- **Nie** dodawaj ręcznie pierwszego `PracHistoria` — robi to `OnAdded`. Ręczne dodanie zapisu + dotyczy dopiero *aktualizacji* danych „od daty" (KADRY-A14). +- Dane osobowe ustawiaj na `Last`/`pracownik[date]`, nie próbuj ich „obejść" przez root — root + deleguje do zapisu, ale zapis jest właściwym miejscem. +- `Kod` bywa polem wymagającym unikalności (zależnie od konfiguracji) — kolizja wybuchnie w + `Save()` jako `RowException` (z `DuplicateKeyException` w `InnerException`). +- Całość w transakcji (`session.Logout(editMode: true)`); brak `Commit()` = rollback przy `Dispose()`. + +### KADRY-A2 — Podstawowe dane kadrowe (★) + +**Cel:** uzupełnić dane ewidencyjno-identyfikacyjne pracownika (PESEL, NIP, urodzenie, płeć, +obywatelstwo, rodzice, dokument tożsamości, adresy). + +**Gdzie leżą pola — root vs PracHistoria:** + +| Dana | Lokalizacja | Pole / typ | +|---|---|---| +| Imię, drugie imię, nazwisko | `PracHistoria` | `Imie`, `ImieDrugie`, `Nazwisko: string` | +| Nazwisko rodowe, imię ojca/matki, nazwisko rodowe matki | `PracHistoria` | `NazwiskoRodowe`, `ImieOjca`, `ImieMatki`, `NazwiskoRodoweMatki: string` | +| PESEL | `PracHistoria` (oraz delegat na root) | `PESEL: string` | +| NIP | `PracHistoria` | `NIP: string` | +| Płeć | `PracHistoria` | `Plec: Soneta.Kadry.PłećOsoby` (`Kobieta`/`Mężczyzna`) | +| Data i miejsce urodzenia | `PracHistoria` (subrow `Urodzony`) | `Urodzony.Data: Date`, `Urodzony.Miejsce: string` | +| Obywatelstwo | `PracHistoria` (subrow `Obywatelstwo`) | `Obywatelstwo.Nazwa: string` (słownik), `Obywatelstwo.KodKraju: string` | +| Dokument tożsamości | `PracHistoria` (subrow `Dokument`) | `Dokument.Rodzaj: KodRodzajuDokumentu`, `Dokument.SeriaNumer: string`, `Dokument.WydanyPrzez`, `Dokument.DataWydania/DataWaznosci: Date` | +| Adres zamieszkania / zameldowania / korespondencji | `PracHistoria` | `Adres`, `AdresZameldowania`, `AdresZamieszkania`, `AdresDoKorespondencji: Soneta.Core.Adres` | +| Urząd skarbowy, koszty/ulga | `PracHistoria` (subrow `Podatki`) | `Podatki.UrzadSkarbowy`, `Podatki.KosztyRodzaj`, `Podatki.UlgaMnoznik`, … | +| Kod, numery rachunków US/ZUS | `Pracownik` (root) | `Kod: string`, `NumerRachunkuUS`, `NumerRachunkuZUS` | + +**Pułapki:** +- `Plec` jest **wyliczana z PESEL** przez weryfikator — przy poprawnym PESEL nie musisz jej ustawiać; + ustawienie ręczne ma sens dla osób bez PESEL. Typ to enum `PłećOsoby`, nie string. +- `Urodzony`, `Obywatelstwo`, `Dokument`, `Podatki` to **subrowy** (pola złożone) — edytujesz ich + pola (`Last.Urodzony.Data = …`), nie przypisujesz całego obiektu. +- `Adres` (i pozostałe) to property zwracające `Soneta.Core.Adres` — modyfikuj ich pola + (`Last.Adres.Miejscowosc = …`), nie przypisuj `Last.Adres = …`. `KodPocztowy` jest `int`; do + wartości z myślnikiem używaj `Adres.KodPocztowyS` (string). +- PESEL/NIP są danymi wrażliwymi — nie loguj ich (safe-code §12). + +**Snippet:** + +```csharp +var pracownik = session.GetKadry().Pracownicy.WgKodu["555"]; + +using (var t = session.Logout(editMode: true)) +{ + var ph = pracownik.Last; // bieżący (ostatni) zapis kadrowy + + ph.NazwiskoRodowe = "Nowak"; + ph.ImieOjca = "Jan"; + ph.NIP = "1234563218"; + + ph.Urodzony.Data = new Date(1994, 1, 8); // subrow Urodzony + ph.Urodzony.Miejsce = "Kraków"; + ph.Obywatelstwo.Nazwa = "polskie"; // subrow Obywatelstwo (słownik) + + ph.Adres.Ulica = "Wadowicka"; // subrow Adres + ph.Adres.NrDomu = "8A"; + ph.Adres.KodPocztowyS = "30-415"; + ph.Adres.Miejscowosc = "Kraków"; + + t.Commit(); +} +session.Save(); +``` + +### KADRY-A7 — Dane ubezpieczenia społecznego i zdrowotnego (★) + +**Cel:** ustawić/odczytać tytuł ubezpieczenia, oddział NFZ oraz parametry ubezpieczeń społecznych +(emerytalne, rentowe, chorobowe, wypadkowe) i zdrowotnego — daty zgłoszeń i wyrejestrowań. + +**Gdzie leżą pola:** + +| Dana | Lokalizacja | Pole / typ | +|---|---|---| +| Tytuł ubezpieczenia (kod) | `PracHistoria.Etat.Ubezpieczenia` | `Tyub4: Soneta.Kadry.TytulUbezpieczenia4` (rekord słownika, tytuł 6-znakowy) | +| Zbiorczy stan ubezpieczeń | `PracHistoria.Etat` | `Etat.Ubezpieczenia: Soneta.Kadry.Ubezpieczenia` (subrow) | +| Społeczne (poszczególne) | `…Etat.Ubezpieczenia.*` | `Emerytalne`, `Rentowe`, `Chorobowe`, `Wypadkowe : Soneta.Kadry.Spoleczne` | +| Zdrowotne | `…Etat.Ubezpieczenia.Zdrowotne` | `Soneta.Kadry.Zdrowotne` (subrow) | +| Data objęcia ubezpieczeniami społ. (od) | `…Etat.Ubezpieczenia` (zbiorczo) | `ObowiazkoweOd: Date` — **zapisywalne** na zbiorczym subrowie `Ubezpieczenia` | +| Objęcie poszczególnym społ. | `…Ubezpieczenia.Emerytalne` itd. | `Obowiazkowe: bool`, `Dobrowolne: bool`, `DobrowolneOd: Date`, `Do: Date` (wyrej.) — **zapisywalne**; `Od: Date` jest tylko do odczytu (wyliczane) | +| Data objęcia zdrowotnym | `…Ubezpieczenia.Zdrowotne` | `ObowiazkoweOd: Date` — **zapisywalne** (asymetria względem `Spoleczne`) | +| Przyczyna wyrejestrowania | `…Ubezpieczenia.Emerytalne.Przyczyna` | `Przyczyna: Soneta.Kadry.Wyrejestrowanie` (subrow z kodem) | +| Oddział NFZ | `PracHistoria.OddzialNFZ` | `OddzialNFZ: Soneta.Kadry.OddzialNFZ` (subrow; `Oddział`, `KodGminy`, `OdDnia`) | +| Tytuł na dzień (odczyt) | `PracHistoria.Etat.Ubezpieczenia` | `WyliczTyubNaDzień(Date)` | + +**Pułapki:** +- Cała struktura ubezpieczeń jest **historyczna** (siedzi w `Etat` danego `PracHistoria`) — zmiana + „od daty" wymaga nowego zapisu historii (KADRY-A14), nie nadpisywania bieżącego. +- `Tyub4` to rekord **konfiguracyjnego** słownika `TytulUbezpieczenia4` — pobierz istniejący wpis + przez `session.GetKadry().TytulyUbezpiecz4.WgKodu[kod]`, gdzie **`kod` jest typu `int`** (np. `110`, + `2241`), nie twórz „w locie". (Pole `Tyub`/`TypUbezpieczenia` to starsze typy — używaj `Tyub4`.) +- `OddzialNFZ` to subrow z polem `Oddział` (enum oddziałów) — ustawiasz `OddzialNFZ.Oddział`, nie + całą strukturę. +- `Emerytalne`/`Rentowe`/`Chorobowe`/`Wypadkowe` to subrowy `Spoleczne`. Ustawiasz na nich flagi + `Obowiazkowe`/`Dobrowolne` oraz `DobrowolneOd`/`Do`. **`Od` jest tylko do odczytu** (wyliczane) — + datę objęcia ubezpieczeniami obowiązkowymi ustawiasz **zbiorczo** przez `Ubezpieczenia.ObowiazkoweOd`. + Na subrowie `Zdrowotne` z kolei `ObowiazkoweOd` jest zapisywalne bezpośrednio. + +**Snippet:** + +```csharp +var pracownik = session.GetKadry().Pracownicy.WgKodu["555"]; + +// Odczyt tytułu ubezpieczenia obowiązującego na dziś: +var data = Date.Today; +TytulUbezpieczenia tyubNaDzis = pracownik[data].Etat.Ubezpieczenia.WyliczTyubNaDzień(data); + +using (var t = session.Logout(editMode: true)) +{ + var ub = pracownik.Last.Etat.Ubezpieczenia; // subrow ubezpieczeń bieżącego zapisu + + // Tytuł ubezpieczenia (rekord słownika konfiguracyjnego); klucz WgKodu jest po Kod: int: + ub.Tyub4 = session.GetKadry().TytulyUbezpiecz4.WgKodu[110]; // np. 0110 = pracownik + + // Data objęcia ubezpieczeniami społecznymi obowiązkowymi — ZBIORCZO (Od na Spoleczne jest read-only): + ub.ObowiazkoweOd = new Date(2026, 1, 1); + ub.Emerytalne.Obowiazkowe = true; + ub.Rentowe.Obowiazkowe = true; + ub.Chorobowe.Obowiazkowe = true; + + // Ubezpieczenie zdrowotne — datę objęcia ustawiasz wprost na subrowie Zdrowotne: + ub.Zdrowotne.ObowiazkoweOd = new Date(2026, 1, 1); + + // Oddział NFZ (subrow): + pracownik.Last.OddzialNFZ.OdDnia = new Date(2026, 1, 1); + + t.Commit(); +} +session.Save(); +``` + +### KADRY-A9 — Dane o rodzinie pracownika (★) + +**Cel:** ewidencjonować członków rodziny i zgłaszać ich do ubezpieczenia zdrowotnego (ZCNA). + +**Kolekcja i typ:** `Pracownik.Rodzina: SubTable` (tabela `Rodzina`, +`GuidedRow` root, child `Pracownik`-a). Nowy członek rodziny tworzony jest konstruktorem +`new CzlonekRodziny(pracownik)`. + +**Pola i typy (`CzlonekRodziny`):** + +| Pole | Typ | Opis | +|---|---|---| +| `Nazwisko`, `Imie`, `ImieDrugie` | `string` | dane osobowe (wymagane: `Nazwisko`, `Imie`) | +| `PESEL`, `NIP` | `string` | identyfikatory | +| `Urodzony` | `Soneta.Kadry.Urodzony` (subrow) | `Urodzony.Data: Date`, `Urodzony.Miejsce: string` | +| `Dokument` | `Soneta.Kadry.DokumentOsoby` (subrow) | dokument tożsamości | +| `StPokrewienstwa` | `Soneta.Kadry.KodStPokrewienstwa` (enum) | stopień pokrewieństwa | +| `Ubezpieczony` | `bool` | zgłoszony do ubezpieczenia zdrowotnego (ZCNA) | +| `UbezpieczenieOkres` | `Soneta.Types.FromTo` | okres zgłoszenia do ubezpieczenia | +| `StNiepelnosprawnosci` | `Soneta.Kadry.KodStNiepelnosprawnosci` (enum) | stopień niepełnosprawności | +| `WspolneGospDomowe`, `NaUtrzymaniu`, `OdbKsztalcenie` | `bool` | przesłanki zgłoszenia | +| `Adres` | `Soneta.Core.Adres` | adres (gdy inny niż pracownika) | +| `Pracownik` | `Soneta.Kadry.Pracownik` | właściciel (ustawiany ctorem) | + +**Pułapki:** +- Zgłoszenie do ubezpieczenia zdrowotnego (ZCNA) realizuje się przez `Ubezpieczony = true` + + `UbezpieczenieOkres` + `StPokrewienstwa` — to z tych pól generowana jest deklaracja ZCNA. Brak + dedykowanego „pola daty wysyłki ZCNA" na członku rodziny. +- `CzlonekRodziny` nie jest historyczny — to płaski child pracownika; okres ubezpieczenia trzyma + pole `UbezpieczenieOkres: FromTo`. +- Konstruktor `new CzlonekRodziny(pracownik)` od razu wiąże rekord z pracownikiem; pojawia się on + w `pracownik.Rodzina`. + +**Snippet:** + +```csharp +var pracownik = session.GetKadry().Pracownicy.WgKodu["555"]; + +using (var t = session.Logout(editMode: true)) +{ + var dziecko = new CzlonekRodziny(pracownik); // ctor wiąże z pracownikiem + session.AddRow(dziecko); + dziecko.Nazwisko = "Kowalska"; + dziecko.Imie = "Zofia"; + dziecko.PESEL = "20290512345"; + dziecko.Urodzony.Data = new Date(2020, 9, 5); + dziecko.StPokrewienstwa = KodStPokrewienstwa.Dziecko; // wartość enum wg słownika + + // Zgłoszenie do ubezpieczenia zdrowotnego (ZCNA): + dziecko.Ubezpieczony = true; + dziecko.UbezpieczenieOkres = new FromTo(new Date(2026, 1, 1), Date.MaxValue); + dziecko.NaUtrzymaniu = true; + + t.Commit(); +} +session.Save(); + +// Odczyt aktualnie ubezpieczonych członków rodziny — filtr serwerowy po kolekcji: +foreach (CzlonekRodziny cr in pracownik.Rodzina[(CzlonekRodziny c) => c.Ubezpieczony]) +{ + // cr.Nazwisko, cr.Imie, cr.StPokrewienstwa +} +``` + +### KADRY-A10 — Poprzednie miejsca pracy (★) + +**Cel:** rejestrować historię zatrudnienia u poprzednich pracodawców i okresy nauki (do wyliczenia +stażu pracy i uprawnień urlopowych). + +**Kolekcja i typ:** `Pracownik.HistoriaZatrudnienia: SubTable` +(tabela `HistZatrudnien`, `GuidedRow` root, child `Pracownik`-a). To **inna kolekcja niż +`Pracownik.Historia`** — `Historia` to historia *bieżącego* zatrudnienia (zapisy `PracHistoria`), +a `HistoriaZatrudnienia` to staż u *poprzednich* pracodawców. + +`HistoriaZatrudnieniaBase` jest typem bazowym z **konstruktorem `protected`** — nie da się go +utworzyć bezpośrednio. Konkretne typy do tworzenia wpisów to: +- `Soneta.Kadry.HistoriaZatrudnienia` — poprzedni pracodawca (ustawia `Typ = Zatrudnienie`), +- `Soneta.Kadry.UkonczonaSzkola` — okres nauki. +Oba mają publiczny ctor `(Pracownik)`. + +**Pola i typy (`HistoriaZatrudnieniaBase`):** + +| Pole | Typ | Opis | +|---|---|---| +| `Typ` | `Soneta.Kadry.TypHistoriiZatrudnienia` | rodzaj wpisu (praca / nauka) — ustawiany przez ctor konkretnej klasy, readonly | +| `Nazwa` | `string` | nazwa zakładu pracy / szkoły (wymagane) | +| `Okres` | `Soneta.Types.FromTo` | okres zatrudnienia/nauki | +| `EfektywnyOkres` | `Soneta.Types.FromTo` | okres efektywnie wliczany do stażu | +| `Adres1`, `Adres2` | `string` | adres pracodawcy | +| `Korekta` | `Soneta.Kadry.StazPracy` | ręczna korekta naliczonego stażu | +| `Staz` | `Soneta.Kadry.StazPracyPracownika` | wyliczony staż (kalkulowane) | +| `RodzajDokumentu` | `Soneta.Kadry.RodzajDokumentu` | dokument potwierdzający | +| `Pracownik` | `Soneta.Kadry.Pracownik` | właściciel (relacja, readonly) | + +**Pułapki:** +- **Nie** rób `new HistoriaZatrudnieniaBase(...)` — ctor jest `protected`. Twórz konkretny typ: + `new HistoriaZatrudnienia(pracownik)` (praca u poprzedniego pracodawcy, `Typ = Zatrudnienie`) albo + `new UkonczonaSzkola(pracownik)` (nauka). +- `EfektywnyOkres` ⊆ `Okres` — to on (a nie sam `Okres`) decyduje o wliczeniu do stażu; jeśli go nie + ustawisz, obowiązują weryfikatory ciągłości. +- Wpisy są niezależne od `PracHistoria` — nie myl `HistoriaZatrudnienia` (poprzedni pracodawcy) + z `Historia` (zapisy bieżącego zatrudnienia). + +**Snippet:** + +```csharp +var pracownik = session.GetKadry().Pracownicy.WgKodu["555"]; + +using (var t = session.Logout(editMode: true)) +{ + // konkretny typ: poprzedni pracodawca (Typ = Zatrudnienie ustawia ctor); AddRow zwraca typowany wiersz + var hz = session.AddRow(new HistoriaZatrudnienia(pracownik)); + hz.Nazwa = "Poprzednia Firma Sp. z o.o."; + hz.Okres = new FromTo(new Date(2018, 3, 1), new Date(2025, 12, 31)); + hz.EfektywnyOkres = hz.Okres; + hz.Adres1 = "ul. Główna 1, Kraków"; + t.Commit(); +} +session.Save(); + +// Odczyt historii zatrudnienia (wszystkie typy wpisów, bazowy typ kolekcji): +foreach (HistoriaZatrudnieniaBase hz in pracownik.HistoriaZatrudnienia) +{ + // hz.Nazwa, hz.Okres, hz.EfektywnyOkres, hz.Typ +} +``` + +### KADRY-A14 — Aktualizacja danych historycznych: zmiana „od daty" vs korekta (★) + +**Cel:** poprawnie zmienić dane kadrowe — **nowy zapis obowiązujący od wskazanego dnia** (zmiana +warunków: podwyżka, zmiana wymiaru etatu, zmiana danych podatkowych) **kontra korekta istniejącego +zapisu** (poprawa błędu w obecnym okresie). Plus: odczyt zapisu obowiązującego „na dzień". + +**Mechanizm `HistorySubTable`:** + +| Operacja | API | Efekt | +|---|---|---| +| Odczyt zapisu na dzień | `pracownik[date]` (== `(PracHistoria)Historia[date]`) | zwraca zapis, którego `Aktualnosc` zawiera `date` | +| Pierwszy zapis | `pracownik.Historia.GetFirst()` | najstarszy zapis | +| Ostatni zapis | `pracownik.Last` (== `Historia.GetPrev()`) | najświeższy zapis | +| **Nowy zapis „od daty"** | `(PracHistoria)pracownik.Historia.Update(date)` | **klonuje** zapis aktualny na `date`, skraca jego okres do `date-1`, zwraca **nowy** klon z okresem od `date`; nowy klon trzeba dodać do tabeli | +| Okres obowiązywania | `PracHistoria.Aktualnosc: Soneta.Types.FromTo` | „od–do" zapisu (zarządzane przez historię) | + +**Wzorzec aktualizacji „od daty" (zmiana warunków od dnia):** + +```csharp +var pracownik = session.GetKadry().Pracownicy.WgKodu["555"]; +var odDnia = new Date(2026, 7, 1); + +using (var t = session.Logout(editMode: true)) +{ + // 1) Update klonuje zapis aktualny na `odDnia` i zwraca nowy klon (okres od `odDnia`). + // Stary zapis zostaje skrócony do dnia poprzedniego. + var nowy = (PracHistoria)pracownik.Historia.Update(odDnia); + + // 2) Klon trzeba dodać do tabeli zapisów historii. + pracownik.Module.PracHistorie.AddRow(nowy); + + // 3) Na nowym zapisie wprowadzamy zmienione warunki (od `odDnia`): + nowy.Etat.MiejscePracy = "Oddział Kraków"; // np. zmiana miejsca pracy + nowy.Podatki.UlgaMnoznik = 1m; // np. zmiana danych podatkowych + + // Uwaga: część pól Etat (np. Etat.Zaszeregowanie.Wymiar) na świeżym klonie potrafi być + // w trybie tylko-do-odczytu (ColReadOnlyException) — odblokowanie zależy od konfiguracji etatu + // (patrz pułapki w sekcji KADRY-B1: bramką jest Etat.Okres). Dla pewności w przykładzie zmieniamy + // pola bezpiecznie zapisywalne (MiejscePracy, dane podatkowe). + + t.Commit(); +} +session.Save(); +``` + +**Wzorzec korekty istniejącego zapisu (bez nowego okresu):** + +```csharp +using (var t = session.Logout(editMode: true)) +{ + // Modyfikujemy zapis obowiązujący na zadaną datę — bez Update, bez AddRow. + var ph = pracownik[new Date(2026, 3, 15)]; + ph.NazwiskoRodowe = "PoprawioneNazwisko"; // korekta w istniejącym okresie + t.Commit(); +} +session.Save(); +``` + +**Pułapki:** +- **`Update(date)` + `AddRow(nowy)` to nierozłączna para.** Sam `Update` tworzy „odpięty" klon — + bez `PracHistorie.AddRow(nowy)` zmiana nie zostanie zapisana. +- `Update(date)` rzuca `HistorySubTable.DateDuplicateException`, gdy na `date` już zaczyna się zapis + (`Aktualnosc.From == date`) — nie da się „aktualizować" dwa razy tego samego dnia; wtedy + modyfikuj istniejący zapis (`pracownik[date]`). +- **Korekta** (modyfikacja `pracownik[date]`) zmienia dane w **całym** okresie tego zapisu — używaj + jej tylko do poprawy błędu, nie do „zmiany od dnia". +- `Aktualnosc` (okres zapisu) jest zarządzany przez mechanizm historii — **nie ustawiaj go ręcznie**; + do skrócenia/wstawienia okresu służy `Update`. +- Odczyt „na dzień": `pracownik[date]` zwraca `null`, jeśli dla daty brak zapisu — dla daty sprzed + zatrudnienia. `pracownik.Last` zawsze zwraca najświeższy zapis. +- Aktualizacja danych to operacja na danych operacyjnych pracownika — trzymaj transakcje krótkie + (safe-code §13.1) i obsłuż `RowConflictException` z `Save()` (safe-code §4). + +### KADRY-A3 — Adresy (zameldowania / zamieszkania / korespondencyjny) + +**Cel:** uzupełnić/odczytać adresy pracownika. Adresy są **historyczne** — leżą na zapisie +`PracHistoria`, dostęp przez `pracownik.Last` (bieżący) lub `pracownik[date]` (na dzień). Każdy adres +to subrow typu `Soneta.Core.Adres` — modyfikujesz jego pola, nie przypisujesz całego obiektu. + +**Gdzie leżą pola — `PracHistoria` (subrowy `Soneta.Core.Adres`):** + +| Dana | Pole / typ | Uwaga | +|---|---|---| +| Adres podstawowy (kanoniczny) | `Adres: Soneta.Core.Adres` | adres główny pracownika | +| Adres zameldowania | `AdresZameldowania: Soneta.Core.Adres` | | +| Adres zamieszkania | `AdresZamieszkania: Soneta.Core.Adres` | | +| Adres do korespondencji | `AdresDoKorespondencji: Soneta.Core.Adres` | | +| Adres na przelewach | `AdresNaPrzelewach: Soneta.Kadry.AdresPracownikaNaPrzelewach` | osobny typ — adres umieszczany na przelewach | + +**Pola subrowa `Soneta.Core.Adres` (zapisywalne, `bazodanowe`):** + +| Pole | Typ | Opis | +|---|---|---| +| `Miejscowosc` | `string` | miejscowość | +| `Ulica` | `string` | nazwa ulicy/alei/osiedla | +| `NrDomu` | `string` | numer domu/bloku | +| `NrLokalu` | `string` | numer lokalu | +| `KodPocztowy` | `int` | kod pocztowy (liczbowo) | +| `KodPocztowyS` | `string` | kod pocztowy z myślnikiem (np. `"30-415"`) | +| `Poczta` | `string` | poczta | +| `Gmina`, `Powiat` | `string` | gmina, powiat | +| `Wojewodztwo` | `Soneta.Core.Wojewodztwa` (enum) | województwo | +| `Kraj`, `KodKraju` | `string` | kraj / kod kraju | +| `ZagranicznyKodPocztowy` | `string` | zagraniczny kod pocztowy | +| `Telefon`, `Faks` | `string` | telefon/faks związany z adresem | +| `Pełny` | `string` | sformatowany adres (tylko odczyt) | + +**Pułapki:** +- `Adres`, `AdresZameldowania`, … to **subrowy** — modyfikuj pola (`Last.AdresZamieszkania.Ulica = …`), + **nie** przypisuj całego obiektu (`Last.AdresZamieszkania = …` — błąd). +- `KodPocztowy` to `int`; do wartości z myślnikiem używaj `KodPocztowyS` (string). +- Cała struktura jest historyczna — zmiana adresu „od daty" to nowy zapis historii (KADRY-A14), korekta + bieżącego okresu to modyfikacja `pracownik[date]`. +- `Wojewodztwo` to enum `Soneta.Core.Wojewodztwa`, nie string. + +**Snippet:** + +```csharp +var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; + +using (var t = session.Logout(editMode: true)) +{ + var ph = pracownik.Last; + + ph.AdresZamieszkania.Miejscowosc = "Kraków"; + ph.AdresZamieszkania.Ulica = "Wadowicka"; + ph.AdresZamieszkania.NrDomu = "8A"; + ph.AdresZamieszkania.NrLokalu = "12"; + ph.AdresZamieszkania.KodPocztowyS = "30-415"; + + ph.AdresZameldowania.Miejscowosc = "Wieliczka"; + ph.AdresDoKorespondencji.Miejscowosc = "Kraków"; + + t.Commit(); +} +session.Save(); + +// Odczyt adresu na dzień: +Adres adr = pracownik[Date.Today].AdresZamieszkania; +string opis = $"{adr.Ulica} {adr.NrDomu}, {adr.KodPocztowyS} {adr.Miejscowosc}"; +``` + +--- + +### KADRY-A4 — Dane kontaktowe (e-mail, telefon) i dostęp WWW/Pulpity + +**Cel:** ustawić/odczytać dane kontaktowe pracownika (e-mail, telefon komórkowy, WWW). Dane kontaktowe +leżą w subrowie `Kontakt: Soneta.Core.Kontakt` — dostępnym zarówno na rootcie `Pracownik`, jak i na +zapisie historii `PracHistoria` (historyczne). Pracownik dodatkowo udostępnia `EMAIL: string` na rootcie. + +**Gdzie leżą pola — subrow `Soneta.Core.Kontakt` (`PracHistoria.Kontakt` / `Pracownik.Kontakt`):** + +| Pole | Typ | Opis | +|---|---|---| +| `EMAIL` | `string` | adres poczty elektronicznej | +| `TelefonKomorkowy` | `string` | telefon komórkowy | +| `WWW` | `string` | adres strony internetowej | +| `Skype` | `string` | identyfikator Skype | +| `SkrytkaPocztowa` | `string` | skrytka pocztowa | + +**Telefon stacjonarny/faks** — w kontekście adresu: `PracHistoria.Adres.Telefon`, `…Adres.Faks: string` +(patrz KADRY-A3). **Rozbudowane kanały kontaktu** (wiele kontaktów z rodzajem/celem): kolekcja +`PracHistoria.Kontakty: SubTable` (pola `Kontakt: string`, +`Rodzaj: Soneta.Core.RodzajKontaktu`, `Domyslny: bool`, `Opis: string`). + +**Dostęp WWW / Pulpity (IWebOperator):** `Pracownik` implementuje interfejs +`Soneta.…IWebOperator`. Konto dostępu do Pulpitów (operator web, login, uprawnienia) **nie jest +zwykłym zapisywalnym polem** pracownika — jest zarządzane osobnym mechanizmem operatorów/uprawnień +modułu web (poza prostym kontraktem ustawiania pól na pracowniku). W publicznym kontrakcie danych +kartotekowych operujesz danymi kontaktowymi (e-mail/telefon/WWW), a powiązanie operatora web jest +realizowane przez konfigurację operatorów, nie przez `pracownik.Last`. + +**Pułapki:** +- `Kontakt` to **subrow** — modyfikuj pola (`Last.Kontakt.EMAIL = …`), nie przypisuj całego obiektu. +- Pole nazywa się `EMAIL` (wielkimi literami) — uwaga na wielkość liter. +- E-mail/telefon w kontekście „na przelewach"/PPK to inne pola (`OdpisPPK.Email`, + `OdpisPPK.TelefonKomorkowy`) — nie myl z kontaktem osobowym. +- Dostęp do Pulpitu (IWebOperator) nie jest częścią `PracHistoria` — nie szukaj „pola WWW dostępu" + na zapisie kadrowym. + +**Snippet:** + +```csharp +var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; + +using (var t = session.Logout(editMode: true)) +{ + var k = pracownik.Last.Kontakt; // subrow Kontakt bieżącego zapisu + k.EMAIL = "g.kowalska@firma.pl"; + k.TelefonKomorkowy = "600100200"; + k.WWW = "https://firma.pl/g.kowalska"; + t.Commit(); +} +session.Save(); + +// Odczyt: +string mail = pracownik.Last.Kontakt.EMAIL; +``` + +--- + +### KADRY-A5 — Rachunki bankowe (rachunek do przelewu wynagrodzenia) + +**Cel:** zarejestrować/odczytać rachunki bankowe pracownika oraz wskazać rachunek główny do przelewu +wynagrodzenia. + +**Kolekcja i typ:** `Pracownik.Rachunki: SubTable` (rachunki są +na rootcie pracownika, nie w historii). Rachunek główny (domyślny) zwraca +`Pracownik.DomyslnyRachunek: Soneta.Kasa.RachunekBankowyPodmiotu`. Numery rachunków US/ZUS pracownika +to osobne pola rootu: `Pracownik.NumerRachunkuUS: Soneta.Core.NumerRachunkuUS`, +`Pracownik.NumerRachunkuZUS: Soneta.Core.NumerRachunkuZUS`. + +**Pola i typy (`Soneta.Kasa.RachunekBankowyPodmiotu`):** + +| Pole | Typ | Opis | +|---|---|---| +| `Rachunek` | `Soneta.Kasa.RachunekBankowy` (subrow) | właściwy rachunek; `Rachunek.Numer: Soneta.Kasa.NumerRachunku`, `Rachunek.Bank: Soneta.Kasa.IBank` | +| `Rachunek.Numer.Pełny` / `.PełnyNRB` | `string` | pełny numer rachunku (do odczytu/ustawienia) | +| `Domyslne` | `bool` | rachunek domyślny (do odczytu — odpowiada `DomyslnyRachunek`) | +| `Priorytet` | `int` | priorytet rachunku | +| `Procent` | `Soneta.Types.Percent` | udział % (przy podziale wynagrodzenia na rachunki) | +| `Kwota` | `Soneta.Types.Currency` | kwota stała (przy podziale wynagrodzenia) | +| `Nazwa1`, `Nazwa2` | `string` | linie informacji na przelewie | +| `Oddzial` | `Soneta.Core.OddzialFirmy` | oddział | +| `Blokada` | `bool` | blokada rachunku | +| `Podmiot` | `Soneta.Kasa.IPodmiotKasowy` | właściciel (pracownik) | + +**Pułapki:** +- `RachunekBankowyPodmiotu` to typ z modułu `Soneta.Kasa` — element kolekcji `pracownik.Rachunki`. +- Rachunek główny do wynagrodzenia odczytujesz przez `pracownik.DomyslnyRachunek` (nie iteruj + kolekcji szukając `Domyslne == true`, gdy wystarczy property). +- `Rachunek` to **subrow** — numer ustawiasz na `r.Rachunek.Numer` (typ biznesowy `NumerRachunku`), + nie jako prosty string na poziomie `RachunekBankowyPodmiotu`. +- Numer rachunku to typ biznesowy (`NumerRachunku`/`NumerRachunkuPodmiotu`), z walidacją IBAN/NRB — + nie traktuj go jak zwykły `string`. + +**Snippet (odczyt — bezpieczny, bez zależności od konstruktora rachunku):** + +```csharp +var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; + +// Rachunek główny do przelewu wynagrodzenia: +RachunekBankowyPodmiotu glowny = pracownik.DomyslnyRachunek; +if (glowny != null) +{ + string numer = glowny.Rachunek.Numer.Pełny; +} + +// Wszystkie rachunki pracownika: +foreach (RachunekBankowyPodmiotu r in pracownik.Rachunki) +{ + bool czyDomyslny = r.Domyslne; + int priorytet = r.Priorytet; +} +``` + +--- + +### KADRY-A6 — Dane podatkowe (PIT) + +**Cel:** ustawić/odczytać dane podatkowe pracownika: koszty uzyskania przychodu, ulgę podatkową +(PIT-2), próg/typ progów podatkowych, urząd skarbowy oraz numer rachunku US. Dane są **historyczne** — +subrow `PracHistoria.Podatki`; numer rachunku US to pole rootu pracownika. + +**Gdzie leżą pola — subrow `PracHistoria.Podatki` (`pracownik.Last.Podatki`):** + +| Dana | Pole / typ | Uwaga | +|---|---|---| +| Rodzaj kosztów uzyskania | `Podatki.KosztyRodzaj: Soneta.Kadry.RodzajKosztowUzyskania` (enum) | podstawowe / podwyższone / brak | +| Mnożnik kosztów | `Podatki.KosztyMnoznik: decimal` | np. 1 / 0 | +| Koszty autorskie 50% | `Podatki.Koszty50Procent: Percent`, `Koszty50Limit: decimal`, `Koszty50NieNaliczajOd: YearMonth`, `Koszty50NieNaliczajOdDnia: Date` | koszty autorskie | +| Ulga podatkowa (mnożnik) | `Podatki.UlgaMnoznik: decimal` | PIT-2: pełna kwota zmniejszająca = mnożnik (np. `1m`), `0` = brak | +| Część ulgi (podział PIT-2) | `Podatki.UlgaCzesc: Soneta.Kadry.UlgaPodatkowaCzesc` (enum) | 1/1, 1/2, 1/3 (podział między płatników) | +| Limit ulgi | `Podatki.UlgaLimit: bool` | | +| Ulga „klasa średnia" / emeryt / duża rodzina / zagranica | `Podatki.UlgaKlasaSrednia`, `UlgaEmeryt`, `UlgaDuzaRodzina`, `UlgaZagranica: bool`; `UlgaZagranicaOd/Do: int` | dodatkowe ulgi | +| Typ progów podatkowych | `Podatki.TypProgow: Soneta.Kadry.TypProgowPodatkowych` (enum) | standardowe / indywidualne | +| Progi podatkowe (indywidualne) | `Podatki.ProgiPodatkowe: SubTable` | gdy `TypProgow` = indywidualne | +| Podwyższona zaliczka (próg) | `Podatki.PodwProg2019: bool` | | +| Naliczanie PIT po 26 r.ż. (ulga dla młodych) | `Podatki.Pit26: Soneta.Kadry.NaliczajPit26` (enum) | „zerowy PIT" dla młodych | +| Rezygnacja z rozp. 07.01.22 | `Podatki.RezygnacjaRozp070122`, `…Umowa: bool` | | +| Kwota wolna przy umowie | `Podatki.UmowaKwotaWolna: bool` | | +| Adres na PIT = zameldowania | `Podatki.NaPITAdresZameldowania: bool` | | +| Urząd skarbowy | `Podatki.UrzadSkarbowy: Soneta.Core.IPodmiotUI` (ref); `UrzadSkarbowyEx: Soneta.CRM.UrzadSkarbowy` | pobierz istniejący US | +| Numer rachunku US (pracownika) | `Pracownik.NumerRachunkuUS: Soneta.Core.NumerRachunkuUS` (root) | `NumerRachunkuUS.Numer/.Pełny` | + +**Pułapki:** +- `Podatki` to **subrow** zapisu historii — modyfikuj pola (`Last.Podatki.UlgaMnoznik = …`), nie + przypisuj całego obiektu; zmiana „od daty" to nowy zapis historii (KADRY-A14). +- PIT-2 (ulga) reprezentowana jest przez `UlgaMnoznik` (pełna/część kwoty zmniejszającej) oraz + `UlgaCzesc` (podział między płatników) — nie ma jednego pola „PIT2 bool". +- `KosztyRodzaj`, `TypProgow`, `UlgaCzesc`, `Pit26` to **enumy**, nie string. +- `UrzadSkarbowy` to **referencja** do istniejącego podmiotu — nie twórz „w locie". +- `NumerRachunkuUS` jest na **rootcie** pracownika, nie w `Podatki`. + +**Snippet:** + +```csharp +var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; + +using (var t = session.Logout(editMode: true)) +{ + var p = pracownik.Last.Podatki; + p.KosztyRodzaj = RodzajKosztowUzyskania.JedenStosPracy; // JedenStosPracy/JedenStos25/WiecejStosPracy/WiecejStos25 + p.UlgaMnoznik = 1m; // pełna kwota zmniejszająca (PIT-2) + p.UlgaCzesc = UlgaPodatkowaCzesc.Ulga112; // podział PIT-2: Ulga112/Ulga124/Ulga136 + p.TypProgow = TypProgowPodatkowych.Standardowe; // enum + t.Commit(); +} +session.Save(); + +// Odczyt: +decimal mnoznikUlgi = pracownik.Last.Podatki.UlgaMnoznik; +RodzajKosztowUzyskania koszty = pracownik.Last.Podatki.KosztyRodzaj; +``` + +--- + +### KADRY-A8 — Pozostałe dane ubezpieczeniowe / informacje ZUS (oddział, kod) + +**Cel:** ustawić/odczytać dodatkowe dane ZUS pracownika — oddział ZUS oraz dodatkowe świadczenia ZUS +(emerytury/renty z dodatkowymi świadczeniami). Dane są **historyczne** — subrow +`PracHistoria.DodSwiadczeniaZUS`. (Tytuł ubezpieczenia i parametry ubezpieczeń społ./zdrow. opisuje KADRY-A7.) + +**Gdzie leżą pola — subrow `PracHistoria.DodSwiadczeniaZUS: Soneta.Kadry.DodatkoweŚwiadczeniaZUS`:** + +| Pole | Typ | Opis | +|---|---|---| +| `OddzialZUS` | `Soneta.CRM.OddziałZUS` (ref) | oddział ZUS (referencja do podmiotu/słownika ZUS) | +| `Rodzaj` | `Soneta.Kadry.RodzajeDodatkowychŚwiadczeńZUS` (enum) | rodzaj dodatkowego świadczenia ZUS | +| `Okres` | `Soneta.Types.FromTo` | okres obowiązywania świadczenia | +| `Numer` | `string` | numer (decyzji/świadczenia) | + +**Oddział NFZ (komplementarny do ZUS):** `PracHistoria.OddzialNFZ: Soneta.Kadry.OddzialNFZ` — +pola `OddzialNFZ.Oddział: OddziałNFZ` (enum oddziałów), `OddzialNFZ.KodGminy: string`, +`OddzialNFZ.OdDnia: Date` (patrz też KADRY-A7). + +**Pułapki:** +- `DodSwiadczeniaZUS` to **subrow** — modyfikuj pola (`Last.DodSwiadczeniaZUS.OddzialZUS = …`). +- `OddzialZUS` to **referencja** (`Soneta.CRM.OddziałZUS`) do istniejącego rekordu — pobierz + istniejący, nie twórz „w locie". +- `Rodzaj` to **enum** `RodzajeDodatkowychŚwiadczeńZUS`, nie string. +- **Cały subrow `DodSwiadczeniaZUS` bywa tylko-do-odczytu** na świeżym zapisie (rzuca + `ColReadOnlyException` nawet dla `Numer`) — pola te aktywuje dopiero zainicjowanie świadczenia + w kreatorze/UI. Zapisywalne wprost jest `OddzialNFZ.OdDnia`. +- Zmiana „od daty" to nowy zapis historii (KADRY-A14). + +**Snippet:** + +```csharp +var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; + +// Odczyt dodatkowych świadczeń ZUS i oddziału NFZ na dziś: +var ph = pracownik.Last; +RodzajeDodatkowychŚwiadczeńZUS rodzaj = ph.DodSwiadczeniaZUS.Rodzaj; +FromTo okres = ph.DodSwiadczeniaZUS.Okres; +var oddzialNfz = ph.OddzialNFZ.Oddział; // enum oddziałów NFZ + +using (var t = session.Logout(editMode: true)) +{ + // Zapisywalne wprost na świeżym zapisie: data objęcia oddziałem NFZ. + ph.OddzialNFZ.OdDnia = new Date(2026, 1, 1); + t.Commit(); +} +session.Save(); +``` + +--- + +### KADRY-A11 — Wykształcenie, znajomość języków obcych, służba wojskowa + +**Cel:** ewidencjonować wykształcenie, znane języki obce oraz dane służby wojskowej pracownika. +Wykształcenie i wojsko to subrowy zapisu historii (`PracHistoria`); języki obce to kolekcja na rootcie +pracownika. + +**Wykształcenie — subrow `PracHistoria.Wyksztalcenie: Soneta.Kadry.Wyksztalcenie`:** + +| Pole | Typ | Opis | +|---|---|---| +| `Kod` | `Soneta.Kadry.KodWyksztalcenia` (enum) | poziom/rodzaj wykształcenia | +| `StopienNaukowy` | `string` | stopień naukowy | +| `TytulNaukowy` | `string` | tytuł naukowy | + +(Kod wykształcenia GUS jest osobno — patrz KADRY-A12: `PracHistoria.GUS.KodWyksztalcenia`.) + +**Języki obce — kolekcja `Pracownik.JęzykiObce: SubTable`:** + +| Pole | Typ | Opis | +|---|---|---| +| `Jezyk` | `Soneta.Kadry.DefinicjaJęzykaObcego` (ref słownik) | język | +| `Mowa` | `Soneta.Kadry.DefinicjaStopiaZnajomościJęzykaObcego` (ref) | stopień znajomości w mowie | +| `Pismo` | `Soneta.Kadry.DefinicjaStopiaZnajomościJęzykaObcego` (ref) | stopień znajomości w piśmie | +| `Zaswiadczenie` | `string` | nr/opis zaświadczenia | +| `DataWydaniaZaswiadczenia` | `Soneta.Types.Date` | data wydania zaświadczenia | +| `Uwagi` | `Soneta.Business.MemoText` | uwagi | + +**Służba wojskowa — subrow `PracHistoria.Wojsko: Soneta.Kadry.Wojsko`:** + +| Pole | Typ | Opis | +|---|---|---| +| `Stosunek` | `Soneta.Kadry.KodStosDoSluzbyWojskowej` (enum) | stosunek do służby wojskowej | +| `KategoriaZdrowia` | `Soneta.Kadry.KategoriaZdrowia` (enum) | kategoria zdrowia (A, B, …) | +| `Stopien` | `string` | stopień wojskowy | +| `NrKsiazeczki` | `string` | nr książeczki wojskowej | +| `NrSpecjalnosci` | `string` | nr specjalności wojskowej | +| `WKU` | `string` | właściwa WKU | +| `PrzydzialMobilizacyjny` | `string` | przydział mobilizacyjny | +| `Podlega` | `bool` | czy podlega obowiązkowi (odczyt) | + +**Pułapki:** +- `Wyksztalcenie` i `Wojsko` to **subrowy** `PracHistoria` (historyczne) — modyfikuj pola, zmiana + „od daty" przez KADRY-A14. `JęzykiObce` to **kolekcja na rootcie** pracownika (nie historyczna). +- `Jezyk`, `Mowa`, `Pismo` to **referencje** do rekordów słownika (`DefinicjaJęzykaObcego`, + `DefinicjaStopiaZnajomościJęzykaObcego`) — pobierz istniejące, nie twórz „w locie". +- `Kod` (wykształcenie), `Stosunek`, `KategoriaZdrowia` to **enumy**, nie string. + +**Snippet:** + +```csharp +var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; + +using (var t = session.Logout(editMode: true)) +{ + // Wykształcenie (subrow historii): + pracownik.Last.Wyksztalcenie.Kod = KodWyksztalcenia.Wyzsze; // enum + pracownik.Last.Wyksztalcenie.TytulNaukowy = "mgr inż."; + + // Służba wojskowa (subrow historii): + pracownik.Last.Wojsko.Stosunek = KodStosDoSluzbyWojskowej.Rezerwa; // NieDotyczy/NiePodlega/Przedpoborowy/Poborowy/Rezerwa/Inne + pracownik.Last.Wojsko.KategoriaZdrowia = KategoriaZdrowia.A; + pracownik.Last.Wojsko.NrKsiazeczki = "AB123456"; + + t.Commit(); +} +session.Save(); + +// Odczyt znajomości języków obcych (kolekcja na rootcie): +foreach (ZnajomośćJęzykaObcego j in pracownik.JęzykiObce) +{ + var jezyk = j.Jezyk; // DefinicjaJęzykaObcego + var mowa = j.Mowa; // DefinicjaStopiaZnajomościJęzykaObcego +} +``` + +--- + +### KADRY-A12 — Dane statystyczne GUS, kod zawodu GUS + +**Cel:** ustawić/odczytać dane statystyczne GUS pracownika (kod wykształcenia GUS, rodzaj zatrudnienia +GUS, przesłanki statystyczne) oraz kod wykonywanego zawodu. Dane są **historyczne** — subrow +`PracHistoria.GUS`; kod zawodu siedzi w `Etat`. + +**Dane statystyczne — subrow `PracHistoria.GUS: Soneta.Kadry.StatystykaGUS`:** + +| Pole | Typ | Opis | +|---|---|---| +| `KodWyksztalcenia` | `Soneta.Kadry.KodWykształceniaGUS` (enum) | kod wykształcenia wg GUS | +| `RodzajZatrudnienia` | `Soneta.Kadry.RodzajZatrudnieniaGUS` (enum) | rodzaj zatrudnienia wg GUS | +| `PopMiejsceZatrudnienia` | `Soneta.Kadry.PopMiejsceZatrudnienia` (enum) | poprzednie miejsce zatrudnienia | +| `GlowneMiejscePracy` | `bool` | główne miejsce pracy | +| `PierwszaPraca` | `bool` | pierwsza praca | +| `PracaWNocy` | `bool` | praca w nocy | +| `StRobotnicze` | `bool` | stanowisko robotnicze | +| `SezonowyDorywczy` | `bool` | zatrudnienie sezonowe/dorywcze | +| `PraceInterwencyjne` | `bool` | prace interwencyjne | + +**Kod wykonywanego zawodu — `PracHistoria.Etat`:** + +| Pole | Typ | Opis | +|---|---|---| +| `Etat.KodWykonywanegoZawodu` | `int` | kod zawodu GUS (liczbowo) | +| `Etat.KodWykonywanegoZawoduLnk` | `Soneta.Kadry.KodWykonywanegoZawodu` (ref/odczyt) | dowiązany rekord słownika kodu zawodu | + +**Pułapki:** +- `GUS` to **subrow** `PracHistoria` (historyczny) — modyfikuj pola; zmiana „od daty" przez KADRY-A14. +- `KodWyksztalcenia` (GUS, enum `KodWykształceniaGUS`) to **inne pole** niż KADRY-A11 + `Wyksztalcenie.Kod` (enum `KodWyksztalcenia`) — nie myl ich. +- `Etat.KodWykonywanegoZawodu` to `int`; `…Lnk` to dowiązanie do słownika (kanonicznie ustawiasz + kod liczbowo, dowiązanie jest pochodne). +- Pola GUS to enumy/bool, nie string. + +**Snippet:** + +```csharp +var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; + +using (var t = session.Logout(editMode: true)) +{ + var ph = pracownik.Last; + ph.GUS.KodWyksztalcenia = KodWykształceniaGUS.Wyższe; // enum GUS (uwaga na diakrytyk) + ph.GUS.GlowneMiejscePracy = true; + ph.GUS.PierwszaPraca = false; + + ph.Etat.KodWykonywanegoZawodu = 251401; // kod zawodu GUS (int) + t.Commit(); +} +session.Save(); + +// Odczyt: +KodWykształceniaGUS kw = pracownik.Last.GUS.KodWyksztalcenia; +int kodZawodu = pracownik.Last.Etat.KodWykonywanegoZawodu; +``` + +--- + +### KADRY-A13 — PFRON i niepełnosprawność / schorzenia + +**Cel:** ewidencjonować dane o niepełnosprawności (stopień, orzeczenie, okresy) oraz dane PFRON +(dofinansowania, schorzenia szczególne). Dane są **historyczne** — subrow `PracHistoria.PFRON`. + +**Gdzie leżą pola — subrow `PracHistoria.PFRON: Soneta.Kadry.DanePFRON`:** + +| Dana | Pole / typ | Uwaga | +|---|---|---| +| Stopień niepełnosprawności | `PFRON.Stopien: Soneta.Kadry.StNiepełnosprawności` (enum) | stopień orzeczony | +| Stopień wg PFRON | `PFRON.StopienPFRON: Soneta.Kadry.KodStNiepelnosprawnosciPFRON` (enum) | klasyfikacja PFRON | +| Okres orzeczenia/uprawnień | `PFRON.Okres: Soneta.Types.FromTo` | okres niepełnosprawności | +| Data orzeczenia | `PFRON.DataOrzeczenia: Soneta.Types.Date` | | +| Data dostarczenia orzeczenia | `PFRON.DataDostarczenia: Soneta.Types.Date` | | +| Data wniosku / zaświadczenia | `PFRON.DataWniosku`, `PFRON.DataZaswiadczenia: Date` | | +| Data zgłoszenia do ewidencji PFRON | `PFRON.DataZgloszeniaDoEwidencji: Date` | | +| Organ wydający orzeczenie | `PFRON.OrganWydajacyOrzeczenie: Soneta.Kadry.OrganWydajacyOrzeczenie` (enum) | | +| Schorzenie szczególne (flaga) | `PFRON.SzczegolneSchorzenie: bool`, `PFRON.SzczegolneSchorzeniePFRON: bool` | | +| Typ schorzenia | `PFRON.TypSchorzenia: Soneta.Kadry.SzczegolneSchorzenia` (enum) | rodzaj schorzenia | +| Schorzenia SOD (1–4) | `PFRON.TypSchorzeniaSOD`, `…2SOD`, `…3SOD`, `…4SOD: Soneta.Kadry.SzczególneSchorzeniaSOD` (enum) | schorzenia dla dofinansowania SOD | +| Lista schorzeń SOD (odczyt) | `PFRON.SchorzeniaSOD: IEnumerable` | wyliczane | +| Efekt zachęty | `PFRON.EfektZachety: bool` | warunek dofinansowania | +| Pomoc publiczna | `PFRON.PomocPubliczna: Soneta.Kadry.StanowiPomocPubliczną` (enum) | | +| Dofinansowanie dodatkowe SOD | `PFRON.DodatkoweDofinansowanieSOD: bool` | | +| Urlop dodatkowy (niepełnospr.) | `PFRON.NaliczajUrlopDodatkowy: bool`, `…Od: Date` | | +| Wymiar urlopu podstawowego | `PFRON.WymiarUPodstawowego: Soneta.Types.Fraction` | | +| Wiek emerytalny od | `PFRON.WiekEmerytalnyOd: Date` | | +| Zgoda na przekazanie danych | `PFRON.ZgodaNaPrzekazanieDanych: bool` | | + +(Zgody na pracę powyżej norm dla osób niepełnosprawnych są na `Etat`: +`Etat.PracownikNiepelnosprawnyZgodaNaPrace8h`, `…ZgodaNaPraceNadgodziny`, +`…ZgodaNaPraceWPorzeNocnej: bool`.) + +**Pułapki:** +- `PFRON` to **subrow** `PracHistoria` (historyczny) — modyfikuj pola; zmiana „od daty" przez KADRY-A14. +- `Stopien`, `StopienPFRON`, `TypSchorzenia`, `…SOD`, `OrganWydajacyOrzeczenie`, `PomocPubliczna` + to **enumy**, nie string. +- `SchorzeniaSOD` (lista) jest **wyliczana** — schorzenia ustawiasz przez pola `TypSchorzeniaSOD…4SOD`. +- Daty to `Soneta.Types.Date`, okres to `FromTo` — nie `DateTime`. + +**Snippet:** + +```csharp +var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; + +using (var t = session.Logout(editMode: true)) +{ + var pfron = pracownik.Last.PFRON; + pfron.Stopien = StNiepełnosprawności.Umiarkowany; // enum + pfron.Okres = new FromTo(new Date(2026, 1, 1), new Date(2028, 12, 31)); + pfron.DataOrzeczenia = new Date(2025, 12, 1); + pfron.DataDostarczenia = new Date(2025, 12, 15); + pfron.SzczegolneSchorzenie = true; + pfron.TypSchorzeniaSOD = SzczególneSchorzeniaSOD.ChoróbPsychiczna; // wg słownika SOD + t.Commit(); +} +session.Save(); + +// Odczyt stopnia i okresu niepełnosprawności na dzień: +var ph = pracownik[Date.Today]; +StNiepełnosprawności stopien = ph.PFRON.Stopien; +FromTo okresNiepeln = ph.PFRON.Okres; +``` + +### KADRY-A15 — Odczyt danych pracownika „na dzień" (★) + +**Cel:** pobrać właściwy rekord historyczny `PracHistoria` obowiązujący dla zadanej daty — i odróżnić +**odczyt** (nie zmienia historii) od **zmiany „od daty"** z KADRY-A14 (`Update` + `AddRow`). Receptura +czysto odczytowa: idealna do uruchomienia na bazie Demo (kody `"006"`–`"009"`). + +**API odczytu — `Pracownik` + `HistorySubTable`:** + +| Operacja | API | Zwraca | +|---|---|---| +| Zapis obowiązujący na dzień | `pracownik[date]` (indeksator, `Item[Date]`) | `PracHistoria` którego `Aktualnosc` zawiera `date`, albo `null` dla daty sprzed zatrudnienia | +| Równoważnie przez kolekcję | `pracownik.Historia[date]` | jw. (indeksator `HistorySubTable.Item[Date]`) | +| Najstarszy (pierwszy) zapis | `pracownik.Historia.GetFirst()` | `PracHistoria` (pierwszy okres zatrudnienia) | +| Najświeższy (ostatni) zapis | `pracownik.Last` (== `Historia.GetLast()`) | `PracHistoria` (zawsze niepusty dla istniejącego pracownika) | +| Sąsiedni zapis | `Historia.GetPrev(ph)` / `Historia.GetNext(ph)` | poprzedni / następny zapis względem podanego | +| Okres obowiązywania zapisu | `PracHistoria.Aktualnosc: FromTo` | „od–do" zapisu (read-only z punktu widzenia kodu — zarządza historia) | + +**Różnica odczyt (KADRY-A15) vs zmiana (KADRY-A14):** + +| Aspekt | KADRY-A15 — odczyt | KADRY-A14 — zmiana „od daty" | +|---|---|---| +| Wywołanie | `pracownik[date]` | `pracownik.Historia.Update(date)` + `PracHistorie.AddRow(nowy)` | +| Efekt na historii | **żaden** (nie tworzy/skraca zapisów) | klonuje zapis aktualny na `date`, skraca poprzedni do `date-1`, dodaje nowy | +| Transakcja | niepotrzebna (sam odczyt) | wymagana (`session.Logout(editMode: true)` + `Save()`) | +| Zwraca | istniejący zapis (lub `null`) | **nowy** klon do uzupełnienia | + +**Snippet (odczyt — bez transakcji):** + +```csharp +var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; + +// 1) Zapis obowiązujący na konkretny dzień: +var data = new Date(2026, 3, 15); +PracHistoria phNaDzien = pracownik[data]; // == pracownik.Historia[data] +if (phNaDzien != null) +{ + string nazwisko = phNaDzien.Nazwisko; + FromTo okresZapisu = phNaDzien.Aktualnosc; // okres obowiązywania tego zapisu + Fraction wymiar = phNaDzien.Etat.Wymiar; // warunki etatu „na dzień" +} + +// 2) Pierwszy i ostatni zapis historii: +PracHistoria pierwszy = pracownik.Historia.GetFirst(); // najstarszy +PracHistoria ostatni = pracownik.Last; // najświeższy (== GetLast()) + +// 3) Odczyt aktualny „na dziś" (uwzględnia datę biznesową aplikacji): +PracHistoria phDzis = pracownik[Date.Today]; +``` + +**Pułapki:** +- `pracownik[date]` zwraca **`null`** dla daty sprzed pierwszego zapisu (przed zatrudnieniem) — zawsze + sprawdzaj `null` przy datach historycznych. `pracownik.Last` jest niepusty dla istniejącego pracownika. +- Indeksator to **tylko odczyt** — nie próbuj „ustawiać" `pracownik[date] = …`. Zmiana danych w okresie + to korekta (`pracownik[date].Pole = …` w transakcji, KADRY-A14) lub nowy zapis (`Update`, KADRY-A14). +- `Aktualnosc` jest zarządzana przez mechanizm historii — odczytujesz, nie ustawiasz. +- `data` to `Soneta.Types.Date`, nie `DateTime`; do „dziś" używaj `Date.Today` (safe-code §10.2). +- Czysty odczyt nie wymaga transakcji edycyjnej — nie otwieraj `Logout(editMode: true)` bez potrzeby. + +### KADRY-A16 — Powiązanie pracownika z kontrahentem (★) + +**Cel:** powiązać pracownika z istniejącym kontrahentem (np. gdy pracownik jest jednocześnie +kontrahentem firmy). Dwie drogi: bezpośrednie ustawienie relacji na rootcie **albo** worker +„Powiąż z kontrahentem". + +**Publiczny kontrakt — pole relacji na `Pracownik` (root):** + +| Pole | Typ | Rodzaj | Uwaga | +|---|---|---|---| +| `PowiazanyKontrahent` | `Soneta.CRM.Kontrahent` | bazodanowe, **zapisywalne** | referencja do istniejącego kontrahenta; `null` = brak powiązania | + +**Worker (alternatywa, ta sama operacja):** `Soneta.Kadry.PowiazZKontrahentemWorker` +(`[Action("Powiąż z kontrahentem")]`, metoda `Powiaz()`): + +| Składnik | Sygnatura | Uwaga | +|---|---|---| +| Pracownik | `Pracownik { get; set; }` | `[Context]` — pracownik do powiązania | +| Parametry | `Prms: Soneta.Kadry.MyParams` | `MyParams.Kontrahent: Soneta.CRM.Kontrahent` — kontrahent docelowy | +| Akcja | `void Powiaz()` | ustawia powiązanie (działa na danych sesji workera) | + +**Snippet (bezpośrednio — zalecane w kodzie biznesowym):** + +```csharp +var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; +var kontrahent = session.GetCRM().Kontrahenci.WgKodu["KLIENT01"]; // istniejący kontrahent + +using (var t = session.Logout(editMode: true)) +{ + pracownik.PowiazanyKontrahent = kontrahent; // relacja na rootcie pracownika + t.Commit(); +} +session.Save(); + +// Odczyt powiązania: +Kontrahent powiazany = pracownik.PowiazanyKontrahent; // null gdy brak +``` + +**Snippet (przez worker — gdy chcesz przejść tą samą ścieżką co UI):** + +```csharp +using (var t = session.Logout(editMode: true)) +{ + var worker = new PowiazZKontrahentemWorker + { + Pracownik = pracownik, + Prms = new MyParams(context) { Kontrahent = kontrahent }, + }; + worker.Powiaz(); + t.CommitUI(); // worker uruchamiany „jak z UI" +} +session.Save(); +``` + +**Pułapki:** +- `Kontrahent` i `Pracownik` muszą pochodzić z **tej samej sesji** (safe-code §2.1) — kontrahenta z innej + sesji przepuść przez `session.Get(...)`. +- Relacja wskazuje **istniejący** rekord kontrahenta — nie twórz kontrahenta „w locie" w tym scenariuszu. +- `MyParams` ma konstruktor `MyParams(Context context)` — wymaga `Context` (dlatego ścieżka workera ma sens + głównie z UI). W czystym kodzie biznesowym prościej ustawić `PowiazanyKontrahent` wprost. +- W teście jednostkowym preferuj bezpośrednie pole `PowiazanyKontrahent` — nie wymaga `Context`. + +### KADRY-A17 — Przeniesienie do archiwum i przywrócenie (★) + +**Cel:** przenieść pracownika do archiwum (po zakończeniu zatrudnienia) oraz przywrócić go z archiwum. +**Operacja przenoszenia/przywracania jest dostępna wyłącznie przez workery** — manager `Archiwum` +udostępnia tylko odczyt statusu. + +**Publiczny kontrakt odczytu — `Pracownik`:** + +| Składnik | Typ | Rodzaj | Uwaga | +|---|---|---|---| +| `Archiwum` | `Pracownik.ArchiwumManager` | manager (read-only API) | `Archiwum.Status: InformacjeOArchiwum`, `Archiwum.Anonimizowany: bool`, `Archiwum.Okresy: Periods` | +| `ArchiwumInfo` | `Soneta.Kadry.InformacjeOArchiwum` | bazodanowe | bieżąca informacja o archiwizacji | +| `InformacjeOArchiwum` | `FromToSubTable` | kolekcja | historia okresów w archiwum (`PracownikWArchiwum.Okres: FromTo`) | + +> **`ArchiwumManager` nie ma publicznej metody Przenieś/Przywróć** — wystawia jedynie właściwości +> tylko-do-odczytu (`Status`, `Anonimizowany`, `Okresy`). Zmiana stanu archiwum następuje wyłącznie +> przez workery poniżej. + +**Workery (jedyna droga zmiany stanu):** + +| Worker | Akcja (menu) | Metoda | Parametry | +|---|---|---|---| +| `Pracownik.PrzenieśDoArchiwumWorker` | „Archiwum/Przenieś do archiwum" | `void PrzenieśDoArchiwum()` | `Pracownik { get; set; }` (`[Context]`, pojedynczy) | +| `Pracownik.PrzywróćZArchiwumWorker` | „Archiwum/Przywróć z archiwum" | `void PrzywróćZArchiwum()` | `Pracownik { get; set; }` (`[Context]`, pojedynczy) | +| `Pracownik.PrzenieśDoArchiwumLstWorker` | „Operacje seryjne/Archiwum/Przenieś do archiwum…" | `void PrzenieśDoArchiwum()` | `Pracownicy: Pracownik[]` (grupowo) | +| `Pracownik.PrzywróćZArchiwumLstWorker` | „Operacje seryjne/Archiwum/Przywróć z archiwum…" | `void PrzywróćZArchiwum()` | `Pracownicy: Pracownik[]` (grupowo) | + +**Snippet (programowe wywołanie workera — pojedynczy pracownik):** + +```csharp +var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; + +// Przeniesienie do archiwum: +using (var t = session.Logout(editMode: true)) +{ + var worker = new Pracownik.PrzenieśDoArchiwumWorker { Pracownik = pracownik }; + worker.PrzenieśDoArchiwum(); + t.CommitUI(); +} +session.Save(); + +// Odczyt stanu archiwizacji: +InformacjeOArchiwum status = pracownik.Archiwum.Status; +bool zanonimizowany = pracownik.Archiwum.Anonimizowany; + +// Przywrócenie z archiwum: +using (var t = session.Logout(editMode: true)) +{ + var worker = new Pracownik.PrzywróćZArchiwumWorker { Pracownik = pracownik }; + worker.PrzywróćZArchiwum(); + t.CommitUI(); +} +session.Save(); +``` + +**Snippet (operacja seryjna — wielu pracowników):** + +```csharp +var lista = session.GetKadry().Pracownicy + .Cast() + .Where(p => /* kryterium */ true) + .ToArray(); + +using (var t = session.Logout(editMode: true)) +{ + var worker = new Pracownik.PrzenieśDoArchiwumLstWorker { Pracownicy = lista }; + worker.PrzenieśDoArchiwum(); + t.CommitUI(); +} +session.Save(); +``` + +**Pułapki:** +- **Brak publicznej metody na managerze** — nie szukaj `pracownik.Archiwum.Przenieś(...)`; jedyne + publiczne API zmiany to workery `PrzenieśDoArchiwumWorker`/`PrzywróćZArchiwumWorker`. +- Workery archiwizacji modyfikują dane → wywołuj w transakcji edycyjnej i `Save()`. Worker uruchamiany + „jak z UI" → `CommitUI()` (worker-extender §3, pkt 4). +- `Archiwum`, `ArchiwumInfo`, `InformacjeOArchiwum` służą **tylko do odczytu** stanu/historii archiwum. +- Pracownik z workera i `Pracownicy[]` muszą być z bieżącej sesji (safe-code §2.1). +- Archiwizacja bywa powiązana z anonimizacją (`Archiwum.Anonimizowany`) — to oddzielny stan; przeniesienie + do archiwum nie musi oznaczać anonimizacji. + +### KADRY-A18 — Wyrejestrowanie / zwolnienie pracownika (★) + +**Cel:** zakończyć zatrudnienie — ustawić rozwiązanie umowy (data, tryb, inicjatywa, podstawa prawna), +ewentualnie okres wypowiedzenia, oraz wygenerować wyrejestrowanie z ZUS (ZWUA) workerem. + +**Publiczny kontrakt — `PracHistoria.Etat` (dane historyczne zatrudnienia):** + +| Dana | Pole / typ | Uwaga | +|---|---|---| +| Koniec okresu zatrudnienia | `Etat.Okres: Soneta.Types.FromTo` | `Okres.To` = ostatni dzień zatrudnienia (zmiana „od daty" → KADRY-A14) | +| Rozwiązanie umowy (subrow) | `Etat.RozwiazanieUmowy: Soneta.Kadry.RozwiazanieUmowy` | zbiorczy subrow trybu zwolnienia | +| Inicjatywa zwolnienia | `Etat.RozwiazanieUmowy.Inicjatywa: KodInicjatywyZwolnienia` | enum | +| Kod zwolnienia (ZUS) | `Etat.RozwiazanieUmowy.KodZwolnienia: KodZwolnienia` | kod trybu rozwiązania | +| Podstawa prawna | `Etat.RozwiazanieUmowy.PodstawaPrawna: KodPodstawyPrawnejZwolnienia` | enum | +| Przyczyna rozwiązania | `Etat.RozwiazanieUmowy.PrzyczynaRozwUmowy: PrzyczynaRozwUmowy` | rekord słownika; opis: `PrzyczynaRozwUmowyOpis: string` | +| Za odszkodowaniem | `Etat.RozwiazanieUmowy.ZaOdszkodowaniem: bool` | — | +| Okres wypowiedzenia (subrow) | `Etat.OkresWypowiedzenia: Soneta.Kadry.OkresWypowiedzenia` | parametry wypowiedzenia | +| Długość wypowiedzenia | `Etat.OkresWypowiedzenia.Dni / .Tygodnie / .Miesiace: int` | składowe okresu | +| Data złożenia wypowiedzenia | `Etat.OkresWypowiedzenia.DataZlozenia: Date` | — | +| Skrócony okres | `Etat.OkresWypowiedzenia.Skrocony: bool` | — | +| Zwolnienie z obowiązku pracy od | `Etat.OkresWypowiedzenia.ZwolnionyZObowiazkuPracyOd: Date` | — | +| Data upływu wypowiedzenia | `Etat.OkresWypowiedzenia.Uplywa: Date` | wyliczana data rozwiązania (`DataRozwiązaniaUmowy` read-only) | + +**Worker ZUS (wyrejestrowanie ZWUA):** `Soneta.Kadry.Pracownik.WyrejestrujPracownikaWorker` +(`[Action("Operacje seryjne/Wyrejestruj pracowników...")]`, metoda `Wyrejestruj()`): + +| Składnik | Sygnatura | Uwaga | +|---|---|---| +| Ctor (parametry z `Context`) | `WyrejestrujPracownikaWorker(WyrejestrujPracownikaParams pars)` | `pars` inicjowany z `Context` | +| Data wyrejestrowania | `WyrejestrujPracownikaParams.Data: Date` | data zdarzenia ZWUA | +| Pracownicy | `Pracownicy: Pracownik[]` (`[Context]`) | lista do wyrejestrowania | +| Bieżąca data | `Current: Date` | data robocza | +| Akcja | `void Wyrejestruj()` | tworzy wyrejestrowania ZUS (ZWUA) | + +**Snippet (ustawienie rozwiązania umowy — nowy zapis „od daty", KADRY-A14):** + +```csharp +var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; +var dataRozwiazania = new Date(2026, 6, 30); + +using (var t = session.Logout(editMode: true)) +{ + var ph = pracownik[dataRozwiazania]; // zapis obowiązujący na dzień rozwiązania (KADRY-A15) + var etat = ph.Etat; + + // Zamknięcie okresu zatrudnienia ostatnim dniem pracy: + etat.Okres = new FromTo(etat.Okres.From, dataRozwiazania); + + // Tryb rozwiązania (subrow RozwiazanieUmowy): + etat.RozwiazanieUmowy.Inicjatywa = KodInicjatywyZwolnienia.Pracownik; + etat.RozwiazanieUmowy.PodstawaPrawna = KodPodstawyPrawnejZwolnienia._550; // kody numeryczne wg słownika (NieDotyczy, _400.._463, _550) + + // Opcjonalnie okres wypowiedzenia: + etat.OkresWypowiedzenia.DataZlozenia = new Date(2026, 5, 31); + etat.OkresWypowiedzenia.Miesiace = 1; + + t.Commit(); +} +session.Save(); +``` + +**Snippet (wyrejestrowanie z ZUS — worker):** + +```csharp +using (var t = session.Logout(editMode: true)) +{ + var pars = new Pracownik.WyrejestrujPracownikaWorker.WyrejestrujPracownikaParams(context) + { + Data = new Date(2026, 7, 1), + }; + var worker = new Pracownik.WyrejestrujPracownikaWorker(pars) + { + Pracownicy = new[] { pracownik }, + Current = Date.Today, + }; + worker.Wyrejestruj(); + t.CommitUI(); +} +session.Save(); +``` + +**Pułapki:** +- `RozwiazanieUmowy` i `OkresWypowiedzenia` to **subrowy** `Etat` — modyfikuj ich pola, nie przypisuj całych + obiektów. +- Konkretne wartości enumów (`KodInicjatywyZwolnienia`, `KodPodstawyPrawnejZwolnienia`, `PrzyczynaRozwUmowy`) + zależą od słownika danej bazy — w teście pobierz/odczytaj realne wartości z Demo zamiast zgadywać. +- `WyrejestrujPracownikaWorker` ma **konstruktor** przyjmujący `WyrejestrujPracownikaParams`, który z kolei + wymaga `Context` (`WyrejestrujPracownikaParams(Context cx)`) — worker jest praktycznie wywoływalny tylko + z dostępnym `Context`. Bez `Context` operację wyrejestrowania ZUS zrealizujesz tylko częściowo (samo + ustawienie `Etat.Okres`/`RozwiazanieUmowy` nie tworzy dokumentu ZWUA). +- `Uplywa`/`DataRozwiązaniaUmowy` bywają wyliczane — nie nadpisuj pól read-only. +- Zmiana warunków „od dnia" to nowy zapis (KADRY-A14); samo zamknięcie `Etat.Okres.To` na bieżącym zapisie jest + korektą całego okresu — używaj świadomie. + +### KADRY-A19 — Przerejestrowanie pracownika (★) + +**Cel:** zmienić kod tytułu ubezpieczenia (`Tyub4`) lub jednostkę (`Wydzial`) od konkretnego dnia — +co skutkuje **wyrejestrowaniem ze starym kodem i ponownym zgłoszeniem z nowym** (ZUS ZWUA + ZUA). +Realizacja: nowy zapis historii „od daty" (KADRY-A14) z innym `Etat.Ubezpieczenia.Tyub4`/`Etat.Wydzial`, +a generowanie deklaracji ZUS — workerem przerejestrowania. + +**Publiczny kontrakt — pola do zmiany (na nowym zapisie `PracHistoria`):** + +| Dana | Pole / typ | Uwaga | +|---|---|---| +| Tytuł ubezpieczenia | `Etat.Ubezpieczenia.Tyub4: Soneta.Kadry.TytulUbezpieczenia4` | rekord słownika; `session.GetKadry().TytulyUbezpiecz4.WgKodu[int]` (klucz `int`, np. `110`) | +| Jednostka organizacyjna | `Etat.Wydzial: Soneta.Kadry.Wydzial` | referencja do istniejącego wydziału | + +**Worker ZUS (przerejestrowanie):** `Soneta.Deklaracje.UI.PrzerejestrowaniePracownikaWorker` +(`[Action("Przerejestrowanie pracownika …")]`, metoda `PrzerejestrowaniePracownika()`): + +| Składnik (`PrzerejestrowaniePracownikaWorker.Params`) | Typ | Uwaga | +|---|---|---| +| `DataRejestracji` | `Soneta.Types.Date` | data ponownego zgłoszenia | +| `DataWypełnienia` | `Soneta.Types.Date` | data wypełnienia deklaracji | +| `Kedu` | `Soneta.Deklaracje.ZUS.KEDU` | zbiór deklaracji ZUS (KEDU) | +| `Przyczyna` | `Soneta.Kadry.Wyrejestrowanie` | przyczyna wyrejestrowania (do ZWUA) | + +**Snippet (zmiana kodu tytułu ubezpieczenia / wydziału „od daty"):** + +```csharp +var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; +var odDnia = new Date(2026, 7, 1); + +using (var t = session.Logout(editMode: true)) +{ + // Nowy zapis historii „od daty" (KADRY-A14): Update klonuje + skraca poprzedni, AddRow dopina klon. + var nowy = (PracHistoria)pracownik.Historia.Update(odDnia); + pracownik.Module.PracHistorie.AddRow(nowy); + + // Zmiana kodu tytułu ubezpieczenia (przerejestrowanie ubezpieczeniowe): + nowy.Etat.Ubezpieczenia.Tyub4 = session.GetKadry().TytulyUbezpiecz4.WgKodu[110]; + + // Lub/oraz zmiana jednostki organizacyjnej: + nowy.Etat.Wydzial = session.GetKadry().Wydzialy.Firma; + + t.Commit(); +} +session.Save(); +``` + +**Pułapki:** +- Przerejestrowanie to **nowy zapis historii** (KADRY-A14: `Update(odDnia)` + `PracHistorie.AddRow(nowy)`) — nie + nadpisuj `Tyub4`/`Wydzial` na bieżącym zapisie (to zmieniłoby cały okres wstecz). +- `Tyub4` pobierasz ze słownika `TytulyUbezpiecz4` po **`int`** (`WgKodu[110]`), nie po stringu i nie „w locie". +- `Wydzial` to referencja do istniejącego wydziału (korzeń: `session.GetKadry().Wydzialy.Firma`). +- `PrzerejestrowaniePracownikaWorker` żyje w `Soneta.Deklaracje.UI` i jego `Params` wymaga m.in. `KEDU` + (zbiór deklaracji) oraz `Context` — generowanie ZWUA+ZUA jest realnie wykonalne tylko w środowisku z + `Context`/`KEDU`. Sama zmiana danych kadrowych (`Tyub4`/`Wydzial`) jest w pełni wykonalna publicznym API + bez workera; deklaracje ZUS — tylko przez worker UI. +- `Update(odDnia)` rzuca `DateDuplicateException`, jeśli na `odDnia` już zaczyna się zapis (KADRY-A14). + diff --git a/soneta-programming/references/domeny/kadry/KADRY02-etat.md b/soneta-programming/references/domeny/kadry/KADRY02-etat.md new file mode 100644 index 0000000..3746c8a --- /dev/null +++ b/soneta-programming/references/domeny/kadry/KADRY02-etat.md @@ -0,0 +1,501 @@ +# KADRY02 — Etat — zatrudnienie etatowe + +> Wspólne fakty o typie, podstawowe typy i szablon wzorca: [../kadry.md](../kadry.md). + +### KADRY-B1 — Definiowanie etatu (umowa o pracę) (★) + +**Cel:** ustalić warunki zatrudnienia etatowego pracownika — rodzaj umowy o pracę, okres, daty +zawarcia/rozpoczęcia pracy, stanowisko, jednostkę organizacyjną oraz stawkę zaszeregowania +(wymiar etatu, rodzaj/typ stawki, kwota). Warunki etatu są **historyczne**: siedzą w polu +`Etat` konkretnego zapisu `PracHistoria`. Etat ustawiamy albo na świeżo utworzonym pracowniku +(`pracownik.Last.Etat`, patrz KADRY-A1), albo na nowym zapisie historii „od daty" (patrz KADRY-A14). + +**Gdzie leżą pola — `PracHistoria.Etat: Soneta.Kadry.Etat` (subrow zapisu historii):** + +| Dana | Pole / typ | Uwaga | +|---|---|---| +| Rodzaj umowy o pracę | `Etat.TypUmowy: Soneta.Kadry.TypUmowyOPrace` | enum: `NaCzasNieokreślony`, `NaOkresPróbny`, `NaCzasOkreślony`, `NaOkresZastępstwa`, `DoDniaPorodu`, `NaCzasWykonywniaPracy`, … (`Brak = 0` = nie dotyczy) | +| Okres etatu (od–do) | `Etat.Okres: Soneta.Types.FromTo` | okres obowiązywania warunków zatrudnienia | +| Data zawarcia umowy | `Etat.DataZawarcia: Soneta.Types.Date` | data podpisania umowy | +| Data rozpoczęcia pracy | `Etat.DataRozpPracy: Soneta.Types.Date` | data faktycznego rozpoczęcia | +| Stanowisko (opis tekstowy) | `Etat.Stanowisko: string` | **wymagane dla etatu** (weryfikator przy `Save()`) | +| Jednostka organizacyjna (wydział) | `Etat.Wydzial: Soneta.Kadry.Wydzial` | **wymagane dla etatu**; pobierz istniejący wydział, korzeń struktury: `session.GetKadry().Wydzialy.Firma` | +| Oddział firmy | `Etat.Oddzial: Soneta.Core.OddzialFirmy` | opcjonalny oddział | +| Miejsce wykonywania pracy | `Etat.MiejscePracy: string` | tekst | +| Podstawa stosunku pracy | `Etat.Podstawa: Soneta.Kadry.StosPracyNaPodstawie` | enum | + +**Stawka — subrow `Etat.Zaszeregowanie: Soneta.Kadry.Zaszeregowanie`:** + +| Dana | Pole / typ | Uwaga | +|---|---|---| +| Rodzaj stawki | `Zaszeregowanie.RodzajStawki: Soneta.Kadry.RodzajStawkiZaszeregowania` | enum: `Godzinowa = 0`, `Miesieczna = 1`, `DochodDeklarowany = 2` | +| Typ stawki | `Zaszeregowanie.TypStawki: Soneta.Kadry.TypStawkiZaszeregowania` | enum: `Dowolna = 0`, `Minimalna = 1`, `ZZakresu = 2`, `WgWskaźnika = 3`, `Nieokreślona = 10` | +| Wymiar etatu (ułamek) | `Zaszeregowanie.Wymiar: Soneta.Types.Fraction` | `Fraction.One` = pełny etat; `new Fraction(1, 2)` = ½ etatu | +| Kwota stawki | `Zaszeregowanie.Stawka: Soneta.Types.Currency` | kwota brutto (miesięczna lub godzinowa wg `RodzajStawki`) | +| Grupa zaszeregowania | `Zaszeregowanie.Grupa: Soneta.Kadry.GrupaZaszeregowania` | rekord słownika (opcjonalny) | +| Definicja elementu wynagrodzenia | `Zaszeregowanie.Element: Soneta.Place.DefinicjaElementu` | element płacowy wiązany ze stawką (opcjonalny) | + +**Pułapki:** +- **Kolejność ma znaczenie — `Etat.Okres` ustaw jako PIERWSZE.** Na świeżo utworzonym + pracowniku (lub świeżym zapisie historii) **cały subrow `Etat` jest w trybie tylko-do-odczytu**, + dopóki nie ustawisz `Etat.Okres` (zakres zatrudnienia). Próba ustawienia `Etat.TypUmowy`, + `Etat.Podstawa` czy `Zaszeregowanie.RodzajStawki`/`Wymiar` przed `Etat.Okres` rzuca + `Soneta.Business.ColReadOnlyException` (np. „'Etat.Typ umowy' — pole w trybie 'tylko do odczytu'"). + Po ustawieniu `Etat.Okres` pozostałe pola (w tym `Zaszeregowanie.Wymiar`) są zapisywalne — + kolejność wśród nich nie ma już znaczenia. +- **Pola wymagane dla etatu:** po ustawieniu `Etat.Okres` (pracownik staje się etatowy) `Save()` + wymaga `Etat.Wydzial` **oraz** `Etat.Stanowisko` — bez nich zapis rzuca wyjątek weryfikatora. +- `Etat` to **subrow** zapisu `PracHistoria` — modyfikujesz jego pola (`Last.Etat.Okres = …`), nie + przypisujesz całego obiektu `Etat`. +- `Zaszeregowanie` to z kolei subrow `Etat` — analogicznie modyfikujesz pola + (`Last.Etat.Zaszeregowanie.Stawka = …`). +- `Etat.Wymiar` i `Etat.TypStawki` istnieją także na poziomie `Etat` (delegaty/odczyt) — **kanonicznie + ustawiamy je na `Etat.Zaszeregowanie`** (`Zaszeregowanie.Wymiar`, `Zaszeregowanie.RodzajStawki`, + `Zaszeregowanie.TypStawki`), bo to one są polami bazodanowymi tej struktury. +- `Etat.Wydzial` i `Etat.Oddzial` to **referencje** do istniejących rekordów — nie twórz „w locie"; + korzeń struktury organizacyjnej pobierzesz przez `session.GetKadry().Wydzialy.Firma`. +- Zmiana warunków etatu **od konkretnego dnia** to nowy zapis historii (`Historia.Update(date)` + + `PracHistorie.AddRow`, patrz KADRY-A14), a nie nadpisanie bieżącego zapisu (to byłaby korekta całego okresu). +- `TypUmowyOPrace` to enum, nie string; `Okres`/`DataZawarcia`/`DataRozpPracy` to typy biznesowe + `FromTo`/`Date`, nie `DateTime` (safe-code §10.1). + +**Snippet:** + +```csharp +var kadry = session.GetKadry(); + +using (var t = session.Logout(editMode: true)) +{ + // Nowy pracownik (KADRY-A1) — AddRow tworzy pierwszy zapis historii (Last) + kalendarz. + var pracownik = session.AddRow(new PracownikFirmy()); + pracownik.Kod = "555"; + pracownik.Last.Nazwisko = "Kowalska"; + pracownik.Last.Imie = "Gabriela"; + + // Warunki etatu — na Etat bieżącego (pierwszego) zapisu historii. + // KLUCZOWE: Etat.Okres MUSI być pierwszy — odblokowuje (z trybu read-only) resztę pól Etat. + var etat = pracownik.Last.Etat; + etat.Okres = new FromTo(new Date(2026, 1, 1), Date.MaxValue); // 1) NAJPIERW okres + etat.TypUmowy = TypUmowyOPrace.NaCzasNieokreślony; + etat.DataZawarcia = new Date(2025, 12, 20); + etat.DataRozpPracy = new Date(2026, 1, 1); + etat.Stanowisko = "Specjalista"; // wymagane dla etatu + etat.Wydzial = kadry.Wydzialy.Firma; // wymagane dla etatu (korzeń struktury) + + // Stawka zaszeregowania (po ustawieniu Etat.Okres pola są zapisywalne): + var z = etat.Zaszeregowanie; + z.RodzajStawki = RodzajStawkiZaszeregowania.Miesieczna; + z.TypStawki = TypStawkiZaszeregowania.Dowolna; + z.Wymiar = Fraction.One; // pełny etat + z.Stawka = (Currency)6000m; // kwota brutto miesięcznie + + t.Commit(); +} +session.Save(); +``` + +> **Zmiany warunków zatrudnienia (KADRY-B2–KADRY-B7).** Warunki zatrudnienia etatowego siedzą w polu `PracHistoria.Etat: Soneta.Kadry.Etat` +> (subrow zapisu historii). `Etat` jest **historyczny** wraz z całym `PracHistoria` — okres +> obowiązywania warunków trzyma `Etat.Okres: FromTo`, a okres zapisu historii `PracHistoria.Aktualnosc`. +> Zmiana warunków „od dnia" to **nowy zapis historii** (`Historia.Update(date)` + `PracHistorie.AddRow`, +> wzorzec z KADRY-A14) — modyfikacja bieżącego zapisu byłaby korektą całego jego okresu. +> +> **Bramka edycji etatu (KADRY-B1).** Na świeżym zapisie cały subrow `Etat` jest tylko-do-odczytu, dopóki +> nie ustawisz `Etat.Okres` — ustaw go **PIERWSZY**, inaczej dotknięcie `TypUmowy`/`Zaszeregowanie.*` +> rzuca `Soneta.Business.ColReadOnlyException`. Pola wymagane przy zapisie etatu: `Etat.Wydzial` +> **oraz** `Etat.Stanowisko`. Po `Update(date)` klon ma już ustawiony `Etat.Okres` (sklonowany ze +> starego zapisu) — zwykle nie trzeba go ustawiać ponownie, ale jeśli zmieniasz okres etatu, rób to +> jako pierwsze. + +--- + +### KADRY-B2 — Zmiana warunków zatrudnienia (aneks) + +**Cel:** zarejestrować aneks do umowy o pracę — zmianę warunków obowiązującą **od wskazanego dnia** +(np. zmiana stanowiska, miejsca pracy, wymiaru, jednostki organizacyjnej). Realizuje się przez +**nowy zapis historyczny** etatu „od daty", nie przez nadpisanie bieżącego. + +**Pola `Etat` (subrow `PracHistoria.Etat: Soneta.Kadry.Etat`):** + +| Dana | Pole / typ | Uwaga | +|---|---|---| +| Okres etatu (od–do) | `Etat.Okres: Soneta.Types.FromTo` | okres obowiązywania warunków; po `Update` zwykle już ustawiony | +| Rodzaj umowy o pracę | `Etat.TypUmowy: Soneta.Kadry.TypUmowyOPrace` | enum | +| Data zawarcia aneksu | `Etat.DataZawarcia: Soneta.Types.Date` | data podpisania | +| Stanowisko (opis) | `Etat.Stanowisko: string` | wymagane dla etatu | +| Jednostka organizacyjna | `Etat.Wydzial: Soneta.Kadry.Wydzial` | wymagane dla etatu; referencja (`session.GetKadry().Wydzialy.Firma`) | +| Oddział firmy | `Etat.Oddzial: Soneta.Core.OddzialFirmy` | opcjonalny | +| Miejsce wykonywania pracy | `Etat.MiejscePracy: string` | tekst | +| Podstawa stosunku pracy | `Etat.Podstawa: Soneta.Kadry.StosPracyNaPodstawie` | enum | +| Forma organizacji pracy | `Etat.FormaOrganizacjiPracy: Soneta.Kadry.FormaOrganizacjiPracy` | enum | +| Wymiar / stawka (na `Zaszeregowanie`) | `Etat.Zaszeregowanie.Wymiar: Fraction`, `Etat.Zaszeregowanie.Stawka: Currency` | patrz KADRY-B3 | + +**Snippet:** + +```csharp +var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; +var odDnia = new Date(2026, 7, 1); + +using (var t = session.Logout(editMode: true)) +{ + // Nowy zapis historii „od daty" — klonuje zapis aktualny na `odDnia`, skraca stary do dnia + // poprzedniego i zwraca nowy klon (okres od `odDnia`). Klon MUSI trafić do tabeli zapisów. + var nowy = (PracHistoria)pracownik.Historia.Update(odDnia); + pracownik.Module.PracHistorie.AddRow(nowy); + + // Etat na klonie ma już Okres (sklonowany) — pola Etat są zapisywalne. Aneksowane warunki: + var etat = nowy.Etat; + etat.Stanowisko = "Starszy specjalista"; + etat.MiejscePracy = "Oddział Kraków"; + etat.DataZawarcia = new Date(2026, 6, 20); + etat.Wydzial = session.GetKadry().Wydzialy.Firma; // wymagane (referencja) + + t.Commit(); +} +session.Save(); +``` + +**Pułapki:** +- `Update(date)` + `PracHistorie.AddRow(nowy)` to **nierozłączna para** — sam `Update` zwraca odpięty + klon; bez `AddRow` zmiana nie zostanie zapisana. +- `Update(date)` rzuca `HistorySubTable.DateDuplicateException`, gdy na `date` już zaczyna się zapis + (`Aktualnosc.From == date`) — wtedy modyfikuj istniejący zapis (`pracownik[date]`). +- Nie ustawiaj `PracHistoria.Aktualnosc` ani (zwykle) `Etat.Okres` ręcznie — zarządza nimi historia. + Jeśli aneks zmienia długość okresu etatu, ustaw `Etat.Okres` **przed** pozostałymi polami (bramka KADRY-B1). +- `Etat` to subrow — modyfikuj jego pola, nie przypisuj całego obiektu. + +--- + +### KADRY-B3 — Przeszeregowanie (zmiana stawki / grupy zaszeregowania) + +**Cel:** zmienić wynagrodzenie zasadnicze — stawkę i/lub grupę zaszeregowania, od wskazanego dnia. + +**Pola `Etat.Zaszeregowanie: Soneta.Kadry.Zaszeregowanie` (subrow `Etat`):** + +| Dana | Pole / typ | Uwaga | +|---|---|---| +| Rodzaj stawki | `Zaszeregowanie.RodzajStawki: Soneta.Kadry.RodzajStawkiZaszeregowania` | enum: `Godzinowa = 0`, `Miesieczna = 1`, `DochodDeklarowany = 2` | +| Typ stawki | `Zaszeregowanie.TypStawki: Soneta.Kadry.TypStawkiZaszeregowania` | enum: `Dowolna`, `Minimalna`, `ZZakresu`, `WgWskaźnika`, `Nieokreślona` | +| Kwota stawki | `Zaszeregowanie.Stawka: Soneta.Types.Currency` | brutto (miesięczna/godzinowa wg `RodzajStawki`) | +| Wymiar etatu | `Zaszeregowanie.Wymiar: Soneta.Types.Fraction` | `Fraction.One` = pełny etat | +| Grupa zaszeregowania | `Etat.Grupa: Soneta.Kadry.GrupaZaszeregowania` | **leży na `Etat`, nie na `Zaszeregowanie`**; referencja do słownika `session.GetKadry().GrupyZaszer` (opcjonalna) | +| Element wynagrodzenia | `Zaszeregowanie.Element: Soneta.Place.DefinicjaElementu` | element płacowy wiązany ze stawką (opcjonalny) | +| Wskaźnik (wg wskaźnika) | `Zaszeregowanie.WskaznikNazwa: string`, `Zaszeregowanie.WskaznikKrotnosc: double` | gdy `TypStawki = WgWskaźnika` | + +**Snippet (bezpośrednia zmiana, „od daty"):** + +```csharp +var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; +var odDnia = new Date(2026, 7, 1); + +using (var t = session.Logout(editMode: true)) +{ + var nowy = (PracHistoria)pracownik.Historia.Update(odDnia); + pracownik.Module.PracHistorie.AddRow(nowy); + + var etat = nowy.Etat; // subrow zapisu; Okres ustawiony przez Update + etat.Zaszeregowanie.Stawka = (Currency)7200m; // podwyżka stawki zasadniczej + // etat.Grupa = session.GetKadry().GrupyZaszer... // ewentualna zmiana grupy (leży na Etat, nie na Zaszeregowanie) + + t.Commit(); +} +session.Save(); +``` + +**Worker platformy (alternatywa seryjna):** przeszeregowania realizuje moduł +`Soneta.Przeszeregowania` — dokument `Soneta.Kadry.Przeszeregowanie` z elementami +`ElementPrzeszeregowania` (m.in. `Soneta.Kadry.ZmianaStawki`), wykonywany czynnością +`ZmianaStawkiWorker` (zmiana kwoty / procentowa / grupa) dla zaznaczonej grupy pracowników. Element +`ZmianaStawki` ma pola `Grupa: GrupaZaszeregowania`, `Kwota: Currency` i zapisuje wynik do +`Etat.Zaszeregowanie` właściwego zapisu historii. + +**Pułapki:** +- `Wymiar`/`Stawka`/`RodzajStawki` na **świeżym** zapisie są zapisywalne dopiero po `Etat.Okres` + (bramka KADRY-B1); po `Update` okres jest już sklonowany, więc pola są zapisywalne. +- `Stawka` to `Currency` (nie `decimal`), `Wymiar` to `Fraction` (nie `double`) — safe-code §10.1. +- `Etat.Grupa`/`Zaszeregowanie.Element` to **referencje** do istniejących rekordów — nie twórz „w locie". + **Uwaga:** `Grupa` jest polem `Etat` (pobierasz ze słownika `session.GetKadry().GrupyZaszer`), a **nie** + polem `Zaszeregowanie` — `Zaszeregowanie` nie ma property `Grupa`. +- Kanonicznie ustawiasz pola stawki na `Etat.Zaszeregowanie` (pola bazodanowe), nie na delegatach + `Etat.Wymiar`/`Etat.TypStawki`. + +--- + +### KADRY-B4 — Rozwiązanie / wygaśnięcie umowy o pracę + +**Cel:** zakończyć stosunek pracy z dniem rozwiązania — ustawić datę końca okresu etatu, dane +wypowiedzenia oraz przyczynę/kod rozwiązania (na potrzeby świadectwa pracy i deklaracji ZUS). + +**Wzorzec (zgodny z czynnością „Zwolnij zaznaczonych pracowników"):** +1. skróć `Etat.Okres.To` do dnia rozwiązania (na bieżącym zapisie albo na nowym zapisie „od daty"), +2. ustaw dane wypowiedzenia (`Etat.OkresWypowiedzenia.*`) i przyczynę (`Etat.RozwiazanieUmowy.*`), +3. opcjonalnie oznacz `Etat.PracownikZwolniony = true` i wyrejestruj z ubezpieczeń. + +**Pola — `Etat.OkresWypowiedzenia: Soneta.Kadry.OkresWypowiedzenia` (subrow):** + +| Dana | Pole / typ | Uwaga | +|---|---|---| +| Data złożenia wypowiedzenia | `OkresWypowiedzenia.DataZlozenia: Soneta.Types.Date` | data wręczenia wypowiedzenia | +| Długość — dni / tygodnie / miesiące | `OkresWypowiedzenia.Dni: int`, `.Tygodnie: int`, `.Miesiace: int` | okres wypowiedzenia | +| Data upływu | `OkresWypowiedzenia.Uplywa: Soneta.Types.Date` | data upływu okresu wypowiedzenia | +| Skrócony | `OkresWypowiedzenia.Skrocony: bool` | skrócony okres wypowiedzenia | +| Zwolnienie z obowiązku pracy od | `OkresWypowiedzenia.ZwolnionyZObowiazkuPracyOd: Date` | | +| Data rozwiązania umowy (odczyt) | `OkresWypowiedzenia.DataRozwiązaniaUmowy: Date` | kalkulowane | + +**Pola — `Etat.RozwiazanieUmowy: Soneta.Kadry.RozwiazanieUmowy` (subrow):** + +| Dana | Pole / typ | Uwaga | +|---|---|---| +| Przyczyna rozwiązania | `RozwiazanieUmowy.PrzyczynaRozwUmowy: Soneta.Kadry.PrzyczynaRozwUmowy` | **referencja do słownika** `session.GetKadry().PrzyczRozwUmow` (indeks `WgNazwy` lub iteracja; **brak indeksera `WgKodu`**); rekord ma `Typ: TypPrzyczynyRozwUmowy` | +| Opis przyczyny | `RozwiazanieUmowy.PrzyczynaRozwUmowyOpis: string` | tekst | +| Podstawa prawna | `RozwiazanieUmowy.PodstawaPrawna: Soneta.Kadry.KodPodstawyPrawnejZwolnienia` | enum (tryb rozwiązania: za wypowiedzeniem, porozumienie, wygaśnięcie itd.) | +| Kod zwolnienia (ZUS) | `RozwiazanieUmowy.KodZwolnienia: Soneta.Kadry.KodZwolnienia` | enum (kod do ZUS RA/świadectwa) | +| Inicjatywa | `RozwiazanieUmowy.Inicjatywa: Soneta.Kadry.KodInicjatywyZwolnienia` | enum (pracodawca/pracownik) | +| Za odszkodowaniem | `RozwiazanieUmowy.ZaOdszkodowaniem: bool` | | +| Pracownik zwolniony (flaga) | `Etat.PracownikZwolniony: bool` | znacznik zakończenia | + +**Snippet:** + +```csharp +var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; +var dataRozwiazania = new Date(2026, 9, 30); + +using (var t = session.Logout(editMode: true)) +{ + var ph = pracownik.Last; + var etat = ph.Etat; + + // 1) skrócenie okresu etatu do dnia rozwiązania + etat.Okres = new FromTo(etat.Okres.From, dataRozwiazania); + + // 2) dane wypowiedzenia + etat.OkresWypowiedzenia.DataZlozenia = new Date(2026, 8, 31); + etat.OkresWypowiedzenia.Miesiace = 1; + + // 3) przyczyna / tryb rozwiązania + // PrzyczynaRozwUmowy to rekord słownika — pobierz po nazwie (WgNazwy) albo iteracją (brak WgKodu): + etat.RozwiazanieUmowy.PrzyczynaRozwUmowy = session.GetKadry().PrzyczRozwUmow.WgNazwy["Wypowiedzenie przez pracownika"]; // referencja + // PodstawaPrawna to enum kodów: NieDotyczy, _400.._463, _550 (kody GUS/ZUS) — wybierz właściwy kod: + etat.RozwiazanieUmowy.PodstawaPrawna = KodPodstawyPrawnejZwolnienia._400; + etat.RozwiazanieUmowy.Inicjatywa = KodInicjatywyZwolnienia.Pracownik; + + etat.PracownikZwolniony = true; // znacznik zakończenia zatrudnienia + + t.Commit(); +} +session.Save(); +``` + +**Pułapki:** +- **Wygaśnięcie** vs **rozwiązanie** rozróżnia `PodstawaPrawna` (enum trybu) oraz `KodZwolnienia` — + to one trafiają do świadectwa pracy i deklaracji ZUS. +- `PrzyczynaRozwUmowy` to **rekord słownika** (referencja), nie enum — pobierz istniejący wpis z + `session.GetKadry().PrzyczRozwUmow` (indeks `WgNazwy` lub iteracja — **słownik nie ma indeksera + `WgKodu`**). Pomyłka: `RozwiazanieUmowy.PrzyczynaRozwUmowy` (referencja) ≠ + `PrzyczynaRozwUmowy.Typ` (enum `TypPrzyczynyRozwUmowy` na rekordzie słownika). +- `KodPodstawyPrawnejZwolnienia` to enum kodów GUS/ZUS o wartościach `NieDotyczy`, `_400`..`_463`, + `_550` (nazwy z prefiksem `_`) — **nie ma** wartości opisowych typu + `RozwiazanieZaWypowiedzeniemPrzezPracownika`. `KodInicjatywyZwolnienia`: `NieDotyczy`, `Pracownik`, + `Pracodawca`. +- Skrócenie `Etat.Okres.To` zmienia warunki w **całym** bieżącym okresie zapisu. Jeśli rozwiązanie + ma obowiązywać od konkretnego dnia z zachowaniem poprzedniego okresu, użyj nowego zapisu + (`Historia.Update(data)` + `PracHistorie.AddRow`), a zmiany rób na klonie. +- Wyrejestrowanie z ubezpieczeń (`IUbezpieczenie.Wyrejestrowany`/daty `Do`) to osobny krok — patrz KADRY-A7. +- `Okres`/`DataZlozenia`/`Uplywa` to `FromTo`/`Date`, nie `DateTime`. + +--- + +### KADRY-B5 — Obniżenie / przywrócenie wymiaru etatu + +**Cel:** czasowo obniżyć wymiar etatu i stawkę (operacje typu COVID / seryjne), a następnie +przywrócić warunki. Stan obniżenia jest **odczytowo** widoczny w subrowie +`Etat.ObnizenieEtatu: Soneta.Kadry.ObniżenieWymiaruEtatu` (delegat do zapisu historii etatu). + +> **Ważne (zweryfikowane na DLL):** subrow `ObniżenieWymiaruEtatu` jest **w całości tylko-do-odczytu** +> — wszystkie jego property (`Wymiar`, `Stawka`, `RodzajStawki`, `TypStawki`, `Element`, `Kalendarz`, +> `Info`) mają `CanWrite == false`, a klasa **nie udostępnia publicznej metody `Save(...)`**. +> Z poziomu kodu biznesowego **nie da się** ustawić tych pól ani „utrwalić" obniżenia przez ten subrow. +> Pełny zapis stanu obniżenia (z metadanymi `ObniżenieWymiaruEtatuInfo`) realizują **workery platformy**. +> W zwykłym kodzie obniżenie sprowadzasz do ustawienia docelowego `Etat.Zaszeregowanie.Wymiar` +> (i ewentualnie `Stawka`) na nowym zapisie „od daty". + +**Pola odczytowe — `Etat.ObnizenieEtatu: Soneta.Kadry.ObniżenieWymiaruEtatu` (subrow, read-only):** + +| Dana | Pole / typ | Uwaga | +|---|---|---| +| Obniżony wymiar etatu | `ObnizenieEtatu.Wymiar: Soneta.Types.Fraction` | **read-only** | +| Obniżona stawka | `ObnizenieEtatu.Stawka: Soneta.Types.Currency` | **read-only** | +| Rodzaj / typ stawki | `ObnizenieEtatu.RodzajStawki`, `.TypStawki` | enumy, **read-only** | +| Kalendarz | `ObnizenieEtatu.Kalendarz: Soneta.Kalend.KalendarzBase` | referencja, **read-only** | +| Element wynagrodzenia | `ObnizenieEtatu.Element: Soneta.Place.DefinicjaElementu` | referencja, **read-only** | +| Zakres obniżenia (przełącznik) | `ObnizenieEtatu.Info: Soneta.Kadry.ObniżenieWymiaruEtatuInfo` | enum (`Brak`/`Wymiar`/`Stawka`/`Zaszeregowanie`/`Kalendarz`/…), **read-only** | + +**Snippet (obniżenie wymiaru „od daty" w kodzie biznesowym):** + +```csharp +var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; +var odDnia = new Date(2026, 7, 1); + +using (var t = session.Logout(editMode: true)) +{ + var ph = (PracHistoria)pracownik.Historia.Update(odDnia); + pracownik.Module.PracHistorie.AddRow(ph); + + // Subrow ObnizenieEtatu jest read-only — NIE ustawiamy go bezpośrednio. + // Docelowy wymiar po obniżeniu utrwalamy na Etat.Zaszeregowanie.Wymiar (pole zapisywalne): + ph.Etat.Zaszeregowanie.Wymiar = new Fraction(4, 5); // np. obniżenie do 4/5 etatu + + t.Commit(); +} +session.Save(); +``` + +**Workery platformy (seryjne, na zaznaczonych pracownikach, tabela `Pracownicy`):** +`ObniżWymiarEtatuWorker` (obniżenie: proporcjonalnie / do / o), `ZmianaStawkiZaszeregowaniaWorker`, +`ZmianaKalendarzaWorker`, `PrzywróćWarunkiZatrudnieniaWorker` (przywrócenie warunków sprzed +obniżenia). To one zakładają nowy zapis historii „od daty" i zapisują pełny stan obniżenia +(`ObniżenieWymiaruEtatuInfo`), którego nie da się ustawić przez publiczny kontrakt subrowa. + +**Pułapki:** +- `Etat.ObnizenieEtatu` to **odczytowy delegat** do zapisu historii etatu — wszystkie property są + read-only i klasa nie ma metody `Save(...)`. W kodzie biznesowym obniżenie wymiaru realizujesz + ustawiając `Etat.Zaszeregowanie.Wymiar` (i `Stawka`) na nowym zapisie; pełny zapis stanu obniżenia + z `ObniżenieWymiaruEtatuInfo` zostaw workerom platformy. +- Operacja jest „od daty" — zawsze przez nowy zapis (`Update` + `AddRow`); inaczej zmienisz wymiar + wstecz w całym bieżącym okresie. +- Przywrócenie warunków to osobna operacja (`PrzywróćWarunkiZatrudnieniaWorker`) — nie polega na + usuwaniu obniżenia, lecz na nowym zapisie z przywróconym wymiarem. + +--- + +### KADRY-B6 — Podzielniki kosztów (rozdział kosztów wynagrodzenia) + +**Cel:** rozdzielić koszty wynagrodzenia pracownika na wydziały/projekty/centra kosztów wg +współczynników. Struktura: `Pracownik` jako **źródło** podzielnika → +`pracownik.Podzielniki: SubTable` → każdy podzielnik ma **historię** +`PodzielnikKosztow.Historia: HistorySubTable` → a zapis historii ma kolekcję +`HistoriaPodzielnika.Elementy: SubTable` (poszczególne udziały). + +> Uwaga: `Pracownik.ElementyPodzielnika: SubTable` to widok zbiorczy elementów +> ze wszystkich podzielników pracownika (do odczytu). **Tworzysz** elementy na konkretnym zapisie +> `HistoriaPodzielnika`, nie przez tę kolekcję. + +**Tworzenie obiektów (konstruktory + AddRow):** + +| Obiekt | Konstruktor | Tabela / AddRow | +|---|---|---| +| Podzielnik | `new PodzielnikKosztow(pracownik)` (pracownik jako `IZrodloPodzielnikaKosztow`) | `session.GetCore().PodzielKosztow.AddRow(p)` | +| Zapis historii | `podzielnik.Historia.Update(odDnia)` | `session.GetCore().HistPodzielnikow.AddRow(h)` | +| Element udziału | `new ElementPodzielnika(historia)` | `session.GetCore().ElemPodzielnikow.AddRow(e)` | + +**Pola — `PodzielnikKosztow`:** `Nazwa: string`, `Definicja: Soneta.Core.DefinicjaPodzielnikaKosztow`, +`Zrodlo: IZrodloPodzielnikaKosztow` (pracownik, ustawiany ctorem), `Last/Historia`. +**Pola — `HistoriaPodzielnika`:** `Aktualnosc: FromTo` (okres zapisu, zarządzany), `Podstawa: decimal`, +`Elementy: SubTable`. +**Pola — `ElementPodzielnika`:** `ElementPodzialowy: Soneta.Core.IElementSlownika` (cel rozdziału — +m.in. `Wydzial`, `Projekt`, `CentrumKosztow`, `OddzialFirmy` — iface-ref), `Wspolczynnik: double`, +`Procent: Percent` (kalkulowany z współczynników). + +**Snippet:** + +```csharp +var core = session.GetCore(); +var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; +var odDnia = new Date(2026, 1, 1); +var wydzialA = session.GetKadry().Wydzialy.Firma; // referencja do celu rozdziału (IElementSlownika) + +using (var t = session.Logout(editMode: true)) +{ + var podzielnik = new PodzielnikKosztow(pracownik); // ctor wiąże ze źródłem (pracownik) + core.PodzielKosztow.AddRow(podzielnik); + podzielnik.Nazwa = "Rozdział kosztów"; + // podzielnik.Definicja = ... // referencja do definicji (opcjonalnie) + + // zapis historii „od daty" (klon + AddRow) + var historia = podzielnik.Historia.Update(odDnia); + core.HistPodzielnikow.AddRow(historia); + + // udział: cel rozdziału + współczynnik + var element = new ElementPodzielnika(historia); + core.ElemPodzielnikow.AddRow(element); + element.ElementPodzialowy = wydzialA; + element.Wspolczynnik = 100d; // Procent wyliczany z współczynników + + t.Commit(); +} +session.Save(); + +// Odczyt zbiorczy elementów podzielnika pracownika: +foreach (ElementPodzielnika e in pracownik.ElementyPodzielnika) +{ + IElementSlownika cel = e.ElementPodzialowy; // np. Wydzial / Projekt + double wsp = e.Wspolczynnik; +} +``` + +**Pułapki:** +- Trójpoziomowa struktura — `PodzielnikKosztow` (root, źródło = pracownik) → `HistoriaPodzielnika` + (historia „od daty") → `ElementPodzielnika` (udziały). Każdy poziom: konstruktor **+** `AddRow` do + właściwej tabeli `Core`. Sam konstruktor nie wystarczy. +- `Historia.Update(odDnia)` + `HistPodzielnikow.AddRow` — para jak w KADRY-A14; zmiana udziałów „od dnia" + to nowy zapis historii (a wcześniej zwykle usunięcie/`Delete()` elementów starego zapisu przy + aktualizacji tego samego okresu — patrz worker pracy zdalnej). +- `ElementPodzialowy` to **referencja interfejsowa** (`IElementSlownika`) — przypisz istniejący + rekord (`Wydzial`, `Projekt`, `CentrumKosztow`, …), nie twórz „w locie". +- `Procent` jest kalkulowany z `Wspolczynnik` poszczególnych elementów — ustawiasz współczynniki, + nie procenty. + +--- + +### KADRY-B7 — Aktualizacja danych wg definicji stanowiska (matrycy) + +**Cel:** powiązać etat z definicją stanowiska i przejąć z niej parametry (stawka/grupa/wymiar, +kalendarz, kod zawodu). Definicja stanowiska to **matryca** — wzorzec wartości dla etatu. + +**Pole na etacie:** `Etat.Definicja: Soneta.HR.DefinicjaStanowiska` (referencja do słownika +konfiguracyjnego `session.GetHR().DefStanowisk`). Pokrewne: `Etat.DefinicjaFunkcji: DefinicjaFunkcji`. + +**Pola `DefinicjaStanowiska` (matryca, do skopiowania na etat):** + +| Dana | Pole / typ | +|---|---| +| Nazwa / stanowisko | `Nazwa: string`, `Stanowisko: string`, `StanowiskoPelne: string` | +| Funkcja | `Funkcja: string`, `DefinicjaFunkcji: Soneta.HR.DefinicjaFunkcji` | +| Zaszeregowanie (wzorzec) | `Zaszeregowanie: Soneta.Kadry.Zaszeregowanie` (`Stawka`, `Wymiar`, `RodzajStawki`, `Element`, `WskaznikNazwa/Krotnosc`) | +| Typ stawki / grupa | `TypStawki: TypStawkiZaszeregowania`, `Grupa: Soneta.Kadry.GrupaZaszeregowania` | +| Kalendarz | `Kalendarz: Soneta.Kalend.Kalendarz`, `NieNadpisujKalendarza: bool` | +| Kod zawodu / praca w szcz. warunkach | `KodWykonywanegoZawodu`, `KodPracyWSzczWarunkach`, `InterpretacjaKalendarza` | + +**Snippet:** + +```csharp +var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; +var def = session.GetHR().DefStanowisk.WgNazwa["Specjalista ds. kadr"]; // matryca (referencja; klucz WgNazwa) +var odDnia = new Date(2026, 7, 1); + +using (var t = session.Logout(editMode: true)) +{ + var nowy = (PracHistoria)pracownik.Historia.Update(odDnia); + pracownik.Module.PracHistorie.AddRow(nowy); + + var etat = nowy.Etat; + etat.Definicja = def; // powiązanie z definicją stanowiska + etat.Stanowisko = def.Stanowisko; // przeniesienie wartości z matrycy + etat.Zaszeregowanie.Wymiar = def.Zaszeregowanie.Wymiar; + etat.Zaszeregowanie.Stawka = def.Zaszeregowanie.Stawka; + + t.Commit(); +} +session.Save(); +``` + +**Pułapki:** +- `Etat.Definicja` to **referencja** do rekordu konfiguracyjnego `DefStanowisk` — pobierz istniejącą + (`session.GetHR().DefStanowisk`), nie twórz „w locie". Indeks po nazwie to `WgNazwa` + (**nie `WgNazwy`**); w bazie Demo słownik bywa pusty — zabezpiecz się na brak definicji. +- Definicja jest matrycą — przeniesienie wartości (stawka/wymiar/kalendarz) na etat zrób jawnie; + samo wskazanie `Etat.Definicja` nie nadpisuje automatycznie wszystkich pól etatu w kodzie biznesowym. +- Dostępność definicji potrafi zależeć od konfiguracji (`DefinicjeStanowiskDlaWydziałów`) — definicja + może być filtrowana po wydziale. +- Zmiana stanowiska „od dnia" to nowy zapis historii (KADRY-A14), nie nadpisanie bieżącego. + diff --git a/soneta-programming/references/domeny/kadry/KADRY03-dodatki-potracenia.md b/soneta-programming/references/domeny/kadry/KADRY03-dodatki-potracenia.md new file mode 100644 index 0000000..1acd322 --- /dev/null +++ b/soneta-programming/references/domeny/kadry/KADRY03-dodatki-potracenia.md @@ -0,0 +1,609 @@ +# KADRY03 — Dodatki, potrącenia, akordy + +> Wspólne fakty o typie, podstawowe typy i szablon wzorca: [../kadry.md](../kadry.md). + +### KADRY-C1 — Dodatki / stałe elementy wynagrodzenia (★) + +**Cel:** przypisać pracownikowi stały element wynagrodzenia (dodatek — np. premia, dodatek +funkcyjny), oparty o definicję elementu płacowego, z okresem obowiązywania i parametrami +(podstawa/procent/czas). W UI: menu czynności *Dodatki i potrącenia → Dodaj nowy*. + +**Klasa i model:** `Soneta.Kadry.Dodatek` — `GuidedRow` root, tabela `Dodatki`, obiekt +**historyczny** (kolekcja `Dodatek.Historia: HistorySubTable`, parametry +„od–do" siedzą w zapisach `DodHistoria`). Dodatek jest childem pracownika i pojawia się w +`pracownik.Dodatki: SubTable`. + +**Tworzenie:** `new Dodatek(pracownik)` + `session.GetKadry().Dodatki.AddRow(d)`. Dodanie do tabeli +tworzy **pierwszy zapis** `DodHistoria` — dostępny od razu jako `d.Last`. Parametry ustawiamy na +`d.Last`. + +**Pola i typy (`DodHistoria` — `d.Last`):** + +| Pole | Typ | Uwaga | +|---|---|---| +| `Element` | `Soneta.Place.DefinicjaElementu` | definicja elementu wynagrodzenia (wymagana); pobierz istniejącą z `session.GetPlace().DefElementow.WgNazwy[nazwa]` | +| `Okres` | `Soneta.Types.FromTo` | okres obowiązywania dodatku | +| `Podstawa` | `Soneta.Types.Currency` | kwota podstawy (gdy algorytm definicji jej wymaga) | +| `Procent` | `Soneta.Types.Percent` | procent (gdy algorytm definicji go wymaga) | +| `Czas` | `Soneta.Types.Time` | czas (gdy algorytm definicji go wymaga) | +| `Ulamek` | `Soneta.Types.Fraction` | ułamek (zależnie od definicji) | +| `Dni` | `int` | liczba dni (zależnie od definicji) | +| `Aktualnosc` | `Soneta.Types.FromTo` | okres zapisu historycznego (zarządzany przez historię — nie ustawiaj ręcznie) | + +**Pola na rootcie `Dodatek`:** `Nazwa: string`, `Pracownik: Soneta.Kadry.Pracownik` (właściciel, +ustawiany ctorem), `DataZakonczeniaWyplaty: Date`, `Last: DodHistoria`, +`Historia: HistorySubTable`, `Dodatki` (tabela: `session.GetKadry().Dodatki`). + +**Pobranie definicji elementu:** słownik `session.GetPlace().DefElementow` (kolekcja konfiguracyjna). +Indeksowanie po nazwie: `DefElementow.WgNazwy["Premia"]`. Definicje dodatków mają +`RodzajZrodla == Soneta.Place.RodzajŹródłaWypłaty.Dodatek` — można nimi filtrować dostępne +definicje. W bazie Demo istnieją gotowe definicje, m.in. `"Premia"`, `"Premia procentowa"`. + +**Pułapki:** +- **`new Dodatek(pracownik)` + `Dodatki.AddRow(d)` to para** — sam konstruktor nie włącza dodatku do + sesji ani nie tworzy zapisu historii. Pierwszy `DodHistoria` powstaje przy `AddRow`; dopiero potem + istnieje `d.Last`. +- `Podstawa`/`Procent`/`Czas` **mogą być tylko-do-odczytu** w zależności od algorytmu wskazanej + `DefinicjaElementu` — element kwotowy udostępnia `Podstawa`, element procentowy `Procent` itd. + Ustawiaj tylko te pola, których wymaga definicja (próba zapisu pola read-only rzuci wyjątek). +- `Element` jest **wymagany** — bez wskazania definicji elementu dodatek nie ma sensu płacowego. + Definicję pobierasz z istniejącego słownika (`DefElementow`), nie tworzysz „w locie" w tym scenariuszu. +- Zmiana parametrów dodatku **od konkretnego dnia** to nowy zapis historii dodatku + (`d.Historia.Update(date)` + `Dodatki.Module.DodHistorie.AddRow(nowy)`), analogicznie do KADRY-A14 — nie + nadpisuj bieżącego zapisu, jeśli chcesz zachować poprzedni okres. +- `DodHistoria.Aktualnosc` (okres zapisu) zarządza mechanizm historii — sam ustawiasz `Okres`, + `Aktualnosc` zostaw historii. + +**Snippet:** + +```csharp +var kadry = session.GetKadry(); +var pracownik = kadry.Pracownicy.WgKodu["006"]; + +// Definicja elementu wynagrodzenia ze słownika konfiguracyjnego (po nazwie): +var definicjaPremii = session.GetPlace().DefElementow.WgNazwy["Premia"]; + +using (var t = session.Logout(editMode: true)) +{ + // new Dodatek(pracownik) + AddRow — AddRow tworzy pierwszy zapis DodHistoria (d.Last): + var dodatek = new Dodatek(pracownik); + kadry.Dodatki.AddRow(dodatek); + + var h = dodatek.Last; // pierwszy zapis historii dodatku + h.Element = definicjaPremii; // definicja elementu (wymagana) + h.Okres = new FromTo(new Date(2026, 1, 1), Date.MaxValue); + h.Podstawa = (Currency)500m; // gdy algorytm definicji wymaga podstawy + + t.Commit(); +} +session.Save(); + +// Odczyt dodatków pracownika i ich definicji elementu (kolekcja childów): +foreach (Dodatek d in pracownik.Dodatki) +{ + DefinicjaElementu element = d.Last.Element; + FromTo okres = d.Last.Okres; +} +``` + +### KADRY-C2 — Potrącenia (stałe i jednorazowe) (★) + +**Cel:** przypisać pracownikowi potrącenie z wynagrodzenia (np. składka związkowa, spłata +rozliczana ręcznie, potrącenie dobrowolne). W modelu płacowym **potrącenie nie ma osobnej klasy** — +to **`Soneta.Kadry.Dodatek`** (jak KADRY-C1), tyle że oparty o **definicję elementu o charakterze +potrącenia**. O „minusowym" charakterze decyduje wyłącznie wskazana `DefinicjaElementu` (jej algorytm), +a nie typ obiektu po stronie pracownika. + +**Jak rozpoznać definicję potrącenia (`Soneta.Place.DefinicjaElementu`, słownik `DefElementow`):** + +| Pole definicji | Typ | Znaczenie dla potrącenia | +|---|---|---| +| `Algorytm.Potracenie` | `bool` | **kluczowy znacznik** — `true` dla elementu potrącającego (element pomniejsza wynagrodzenie) | +| `Algorytm.LimitPotracenia` | `Soneta.Place.TypLimituPotrącenia` | rodzaj limitu (np. do kwoty wolnej) — gdy potrącenie limitowane | +| `Algorytm.TylkoPelnePotracenie` | `bool` | potrącać tylko w pełnej kwocie (bez częściowego) | +| `RodzajZrodla` | `Soneta.Place.RodzajŹródłaWypłaty` | dla potrącenia przez dodatek **musi być** `Dodatek` (= `6`); enum **nie ma** wartości „Potrącenie" (ma natomiast m.in. `ZajęcieKomornicze` = 23, `Świadczenie` = 12, `Pożyczka` = 18, `PożyczkaSpłata` = 19). Minus realizuje algorytm, ale `DodHistoria.Element` **odrzuca** definicje o `RodzajZrodla != Dodatek` (np. „Alimenty" jako `ZajęcieKomornicze`) — patrz pułapki | + +**Mechanizm — identyczny jak KADRY-C1 (Dodatek + DodHistoria):** +- `new Dodatek(pracownik)` + `session.GetKadry().Dodatki.AddRow(d)` → powstaje pierwszy `DodHistoria` + (`d.Last`). +- Na `d.Last` ustawiamy `Element` (definicja potrącenia), `Okres` oraz `Podstawa`/`Procent`/`Kwota` + zależnie od algorytmu definicji. +- **Potrącenie stałe**: `Okres` otwarty (do `Date.MaxValue`) lub na czas określony — naliczane w każdej + wypłacie z okresu. +- **Potrącenie jednorazowe**: `Okres` zawężony do jednego miesiąca rozliczeniowego (tylko ten miesiąc + obejmie naliczenie). +- Zakończenie potrącenia: `d.DataZakonczeniaWyplaty` + ewentualnie `d.PrzyczynaZakonczenia`, albo nowy + zapis historii „od daty" (`d.Historia.Update(date)`), analogicznie do KADRY-C1/KADRY-A14. + +**Pułapki:** +- Nie szukaj klasy „Potrącenie" — jej **nie ma**. Potrącenie = `Dodatek` z definicją, w której + `Algorytm.Potracenie == true`. Dobór definicji jest jedynym wyróżnikiem. +- **Filtruj po DWÓCH warunkach** (zweryfikowane testem): `d.Algorytm.Potracenie && d.RodzajZrodla == + RodzajŹródłaWypłaty.Dodatek`. Sam `Algorytm.Potracenie` **nie wystarcza** — przy ustawianiu + `DodHistoria.Element` definicja o innym `RodzajZrodla` (np. „Alimenty" jako `ZajęcieKomornicze`) + rzuca `System.Exception: "Zły rodzaj źródła wypłaty elementu …"`. Element zajęcia komorniczego ma + `RodzajZrodla == ZajęcieKomornicze` i podpinasz go pod `ZajęcieKomornicze`, nie pod `Dodatek` (KADRY-C4). +- `Podstawa`/`Procent`/`Czas` na `DodHistoria` bywają tylko-do-odczytu zależnie od algorytmu definicji + (jak w KADRY-C1) — ustawiaj tylko te, których definicja wymaga. +- `Element` wymagany; pobierany z istniejącego słownika `DefElementow`, nie tworzony „w locie". + +**Snippet:** + +```csharp +var kadry = session.GetKadry(); +var pracownik = kadry.Pracownicy.WgKodu["006"]; + +// Definicja potrącenia ze słownika — DWA warunki: Potracenie ORAZ RodzajZrodla == Dodatek: +var def = session.GetPlace().DefElementow.Cast() + .First(d => d.RodzajZrodla == RodzajŹródłaWypłaty.Dodatek + && d.Algorytm != null && d.Algorytm.Potracenie); + +using (var t = session.Logout(editMode: true)) +{ + var potracenie = new Dodatek(pracownik); + kadry.Dodatki.AddRow(potracenie); // tworzy pierwszy DodHistoria + + var h = potracenie.Last; + h.Element = def; // definicja o Algorytm.Potracenie == true + h.Okres = new FromTo(new Date(2026, 1, 1), Date.MaxValue); // stałe + h.Podstawa = (Currency)50m; // gdy algorytm definicji wymaga kwoty + + t.Commit(); +} +session.Save(); +``` + +--- + +### KADRY-C3 — Akordy (★) + +**Cel:** przypisać pracownikowi pracę akordową (rozliczaną wg ilości/strefy), z okresem i definicją +akordu; zakończyć akord. W UI: menu czynności *Akordy → Dodaj nowy / Zakończ*. + +**Klasa i model:** `Soneta.Kadry.Akord` — `GuidedRow` root, tabela `Akordy`, obiekt **historyczny** +(`Akord.Historia: HistorySubTable`; parametry „od–do" w zapisach +`AkordHistoria`, dostęp do bieżącego przez `Akord.Last`). Akord jest childem pracownika: +`pracownik.Akordy: SubTable`. + +**Pola root `Akord`:** + +| Pole | Typ | Uwaga | +|---|---|---| +| `Pracownik` | `Soneta.Kadry.Pracownik` | właściciel (relacja) | +| `Definicja` | `Soneta.Kadry.DefinicjaAkordu` | definicja akordu (słownik `DefinicjeAkordow`) | +| `Okres` | `Soneta.Types.FromTo` | okres obowiązywania akordu | +| `Typ` | `Soneta.Kadry.TypAkordu` | typ akordu | +| `Wydzial` | `Soneta.Kadry.Wydzial` | jednostka organizacyjna realizacji | +| `Last` | `Soneta.Kadry.AkordHistoria` | bieżący zapis historii | +| `Dni` | `DateSubTable` | dzienna realizacja akordu | + +**Pola `AkordHistoria` (`Akord.Last`):** `Okres: FromTo`, `Algorytm: Soneta.Kadry.AlgorytmAkordu` +(subrow z `Algorytm.Element: DefinicjaElementu`, `Algorytm.Wspolczynnik`, `Algorytm.Progi`, +`Algorytm.WgCzasu`/`Progresywny` itd.), `Jednostka: string`, `Aktualnosc: FromTo` (zarządzane przez +historię), `Progi: SubTable`. + +**Tworzenie — brak publicznego konstruktora `Akord(pracownik)`.** Akord dodaje się **workerem** +operacyjnym (kanonicznie), nie `new`. Konstruktor `Akord` jest niepubliczny (poza `RowCreator`). +Worker jest „jak z UI" (`Params` dziedziczy z `ContextBase`, ctor wymaga `Context`) — uruchamiaj go w +transakcji `CommitUI`. + +**Workery (zagnieżdżone w `Pracownik`):** ctor `(Session)`, parametry przez właściwości `Pars`/`Pracownicy`; +`Params` ma ctor `(Context)`. + +| Worker | Metoda | Wzorzec użycia | +|---|---|---| +| `Soneta.Kadry.Pracownik.DodajAkordWorker` | `DodajAkord` | `new Params(ctx) { Definicja, OdDnia, DoDnia, DodajKolejny }`; `new DodajAkordWorker(session) { Pars = par, Pracownicy = tab }` | +| `Soneta.Kadry.Pracownik.ZakończAkordWorker` | `ZakończAkord` | `new Params(ctx) { Definicja, DoDnia, ZakończWszystkie }`; `new ZakończAkordWorker(session) { Pars = par, Pracownicy = tab }` | + +**Pułapki:** +- Akordu **nie twórz przez `new Akord(...)`** — kanoniczna ścieżka to `DodajAkordWorker` (analogicznie + `ZakończAkordWorker` do zakończenia). Workery przyjmują **tablicę pracowników**, więc nadają się też do + operacji grupowej. +- `Definicja` (akordu) to rekord słownika `DefinicjeAkordow` — pobierz istniejący, nie twórz „w locie". + Sam akord wiąże dopiero z `DefinicjaElementu` (płacowym) przez `Algorytm.Element` definicji akordu. +- Akord jest historyczny — zmiana parametrów „od daty" to nowy zapis `AkordHistoria` + (`Historia.Update(date)`), analogicznie do KADRY-C1/KADRY-A14. +- Tabela `Akordy` to dane operacyjne — przy przeglądaniu poprzecznym filtruj zakresem (safe-code §6.3); + w zakresie jednego pracownika korzystaj z `pracownik.Akordy`. + +**Snippet:** + +```csharp +var kadry = session.GetKadry(); +var pracownik = kadry.Pracownicy.WgKodu["006"]; +var defAkordu = kadry.DefinicjeAkordow.WgNazwa["Akord prosty"]; // klucz WgNazwa (l.poj.) +var context = login.CreateEmptyContext().Clone(session); + +using (var t = session.Logout(editMode: true)) +{ + var par = new Pracownik.DodajAkordWorker.Params(context) // Params: ctor (Context) + { + Definicja = defAkordu, + OdDnia = new Date(2026, 1, 1), + DoDnia = new Date(2026, 12, 31), + }; + // ctor (Session); parametry przez właściwości Pars/Pracownicy: + new Pracownik.DodajAkordWorker(session) { Pars = par, Pracownicy = new[] { pracownik } }.DodajAkord(); + t.CommitUI(); +} +session.Save(); + +// Odczyt akordów pracownika: +foreach (Akord a in pracownik.Akordy) +{ + DefinicjaAkordu def = a.Definicja; + FromTo okres = a.Okres; + DefinicjaElementu element = a.Last.Algorytm.Element; +} +``` + +--- + +### KADRY-C4 — Zajęcia wynagrodzenia (komornicze, alimentacyjne) (★) + +**Cel:** zarejestrować zajęcie wynagrodzenia (egzekucja komornicza lub alimentacyjna) z numerem sprawy, +kwotą, priorytetem i wierzycielem (komornikiem/rachunkiem odbiorcy); anulować/przywrócić zajęcie. + +**Klasa i model:** `Soneta.Kadry.ZajęcieKomornicze` — `GuidedRow` root, tabela `ZajKomornicze`, obiekt +**historyczny** (`Historia: HistorySubTable`; limity i kwoty +„od–do" w zapisach historii, bieżący przez `Last`). Child pracownika: +`pracownik.ZajęciaKomornicze: SubTable`. **Konstruktor publiczny:** +`new ZajęcieKomornicze(pracownik)`. + +**Pola root `ZajęcieKomornicze`:** + +| Pole | Typ | Uwaga | +|---|---|---| +| `Pracownik` | `Soneta.Kadry.Pracownik` | właściciel | +| `Rodzaj` | `Soneta.Kadry.RodzajeZajęciaWynagrodzenia` | enum: `Kwota = 0`, `KwotaMiesięczna = 1` (jednorazowa kwota vs miesięczna) | +| `Element` | `Soneta.Place.DefinicjaElementu` | element płacowy zajęcia — **wymagany**; musi mieć `RodzajZrodla == ZajęcieKomornicze` (= 23) | +| `NumerSprawy` | `string` | numer sprawy egzekucyjnej | +| `Data` | `Soneta.Types.Date` | data zajęcia | +| `DataSplaty` | `Soneta.Types.Date` | data spłaty/zakończenia | +| `Rozliczenie.Odbiorca` | `Soneta.Kasa.IPodmiotKasowy` | **wierzyciel** — komornik/odbiorca (iface; może być `Kontrahent`, `Bank`, `Pracownik`, `UrzadSkarbowy`…) | +| `Rozliczenie.RachunekOdbiorcy` | `Soneta.Kasa.RachunekBankowyPodmiotu` | rachunek wierzyciela do przelewu | +| `Splacono` | `Soneta.Types.Currency` | kwota spłacona (kalkulowane/narastające) | +| `Pozostało` | `Soneta.Types.Currency` | kwota pozostała (kalkulowane) | +| `SplataZakonczona` | `bool` | spłata zakończona | +| `Anulowane` | `bool` | zajęcie anulowane (patrz workery) | +| `Korekty` | `SubTable` | korekty zajęcia | +| `OpisPrzelewu` | `string` | tytuł przelewu | + +**Limity i kwoty — na zapisie `ZajęcieKomorniczeHistoria` (`Last`):** kwota do potrącenia, limity +procentowe i kwotowe, zawieszenie spłaty, priorytet, ustawienia potrąceń z zasiłków/świadczeń (zmiana +„od daty" = nowy zapis historii). + +**Workery (zagnieżdżone w `ZajęcieKomornicze`):** ctor **bezparametrowy**, parametr przez właściwość `Zajęcie`. + +| Worker | Metoda | Wzorzec użycia | +|---|---|---| +| `Soneta.Kadry.ZajęcieKomornicze.AnulujWorker` | `Anuluj` | `new ZajęcieKomornicze.AnulujWorker { Zajęcie = zaj }.Anuluj()` | +| `Soneta.Kadry.ZajęcieKomornicze.PrzywrócWorker` | `Przywróć` | `new ZajęcieKomornicze.PrzywrócWorker { Zajęcie = zaj }.Przywróć()` | + +**Pułapki:** +- **Pole `Priorytet` NIE istnieje** na `ZajęcieKomornicze` (sprostowanie). **Alimentacyjne vs + niealimentacyjne** rozstrzyga konfiguracja: wskazana `DefinicjaElementu` (`RodzajZrodla == + ZajęcieKomornicze`) i parametry zapisu historii (limity), nie osobny typ klasy — to **jedna klasa** + `ZajęcieKomornicze`. +- `Anulowane` jest **tylko-do-odczytu** (brak publicznego settera) — anuluj **workerem** `AnulujWorker`. +- `Rozliczenie.Odbiorca` jest **interfejsem** `IPodmiotKasowy` — wskaż istniejący podmiot (zwykle + `Kontrahent`-komornik); nie twórz odbiorcy „w locie" w tym scenariuszu. +- Faktyczne **kwoty potrącenia (`Splacono`, `Pozostało`) wyliczają się przy naliczeniu wypłaty** — po + samym dodaniu zajęcia są zerowe/wyjściowe. Pełne rozliczenie wymaga naliczonej wypłaty (patrz sekcja + „niewykonalne publicznym API bez naliczenia"). +- Anulowanie/przywracanie realizuj **workerami** (`AnulujWorker`/`PrzywrócWorker`), nie ręcznym + ustawianiem `Anulowane` — workery dbają o storna i spójność rozliczenia. +- Tabela operacyjna — przegląd poprzeczny z filtrem (safe-code §6.3). + +**Snippet:** + +```csharp +var kadry = session.GetKadry(); +var pracownik = kadry.Pracownicy.WgKodu["006"]; +// Element zajęcia — definicja o RodzajZrodla == ZajęcieKomornicze (nie zwykłe potrącenie-Dodatek): +var elementZajecia = session.GetPlace().DefElementow.Cast() + .First(d => d.RodzajZrodla == RodzajŹródłaWypłaty.ZajęcieKomornicze); +var komornik = session.GetCRM().Kontrahenci.WgKodu["KOMORNIK1"]; // wierzyciel (IPodmiotKasowy) + +using (var t = session.Logout(editMode: true)) +{ + var zajecie = new ZajęcieKomornicze(pracownik); // ctor publiczny + kadry.ZajKomornicze.AddRow(zajecie); + + zajecie.Rodzaj = RodzajeZajęciaWynagrodzenia.KwotaMiesięczna; + zajecie.Element = elementZajecia; // wymagany (RodzajZrodla == ZajęcieKomornicze) + zajecie.NumerSprawy = "KM 123/2026"; + zajecie.Data = new Date(2026, 1, 1); + zajecie.Rozliczenie.Odbiorca = komornik; // wierzyciel + zajecie.Rozliczenie.RachunekOdbiorcy = komornik.Rachunki.WgKodu["GŁÓWNY"]; + + t.Commit(); +} +session.Save(); + +// Anulowanie zajęcia (worker bezparametrowy + property Zajęcie, nie ręczna flaga): +using (var t = session.Logout(editMode: true)) +{ + var zaj = pracownik.ZajęciaKomornicze.First(); + new ZajęcieKomornicze.AnulujWorker { Zajęcie = zaj }.Anuluj(); + t.CommitUI(); +} +session.Save(); +``` + +--- + +### KADRY-C5 — Operacje seryjne na dodatkach (moduł Przeszeregowania) (★) + +**Cel:** dodać / zmienić / zakończyć dodatek (oraz zmienić stawkę) dla **grupy pracowników** jedną +operacją. Realizuje to moduł **`Soneta.Przeszeregowania.PrzeszeregowaniaModule`**. Dokumentem zbiorczym +jest `Przeszeregowanie` (tabela `Przeszeregowania`, root) z pozycjami `ElementPrzeszeregowania` +(tabela `ElementyPrzeszer`, child). Pracownik widzi swoje pozycje przez +`pracownik.ElementyPrzeszeregowania`. + +**Workery operacyjne** — ctor **bezparametrowy**, parametry przez właściwości `Pars` (typu `Params`, +ctor `(Context)`) i `Pracownicy: Pracownik[]`. Uruchamiaj w transakcji `CommitUI`. **Uwaga:** workery +te w bezgłowym hoście testowym (bez operatora/kontekstu UI) rzucają `NullReferenceException` — wymagają +realnego środowiska aplikacji. + +| Worker | Metoda | Params (publiczne pola) | Działanie | +|---|---|---|---| +| `Soneta.Przeszeregowania.NowyDodatekWorker` | `NowyDodatek` | `Definicja: DefinicjaElementu, Podstawa: Currency, Procent: Percent` | wypłata/nadanie nowego dodatku grupie | +| `Soneta.Przeszeregowania.ZmianaDodatkuWorker` | `ZmianaDodatku` | `Definicja, Podstawa, ZmianaPodstawy: Currency, ProcentowaZmianaPodstawy: Percent, Procent, ZmianaProcentu: Percent, DataStawki: Date, PodstawaPrecyzja, PodstawaSposob` | zmiana parametrów istniejącego dodatku | +| `Soneta.Przeszeregowania.ZakończDodatekWorker` | `ZakończDodatek` | `Definicja: DefinicjaElementu` | zakończenie wypłaty dodatku | +| `Soneta.Przeszeregowania.DodajZmienDodatekWorker` | `DodajZmienDodatek` | `Params` (dodanie lub zmiana łącznie) | dodanie albo zmiana dodatku | +| `Soneta.Przeszeregowania.DodajNagrodęWorker` | (nagroda) | — | seryjne nagrody | +| `Soneta.Przeszeregowania.ZmianaStawkiWorker` | `ZmianaStawki` | — | seryjna zmiana stawki zaszeregowania | + +**Dokument `Przeszeregowanie` (alternatywa: zbuduj dokument i wykonaj).** Tworzenie: `new +Przeszeregowanie()` + `session.GetPrzeszeregowania().Przeszeregowania.AddRow(doc)` (kolekcja **nie ma** +`AddNew` — to standardowy `GuidedRow` root z publicznym ctorem bezparametrowym). + +| Pole | Typ | +|---|---| +| `Data` | `Soneta.Types.Date` (data przeszeregowania) | +| `DataWykonania` | `Soneta.Types.Date` | +| `Nazwa` | `string` | +| `Realizacja` | `Soneta.Przeszeregowania.RealizacjaPrzeszeregowania` (stan) | +| `Pracownicy` | `ICollection` | +| `Elementy` | `SubTable` | +| `ZarzadzaneWnioskiem` | `bool` | + +`ElementPrzeszeregowania` (child) niesie zmianę per pracownik: `Definicja: DefinicjaElementu`, +`Kwota`/`ZmianaKwoty`/`ProcentowaZmianaKwoty`, `Procent`/`ZmianaProcentu`, `Grupa: GrupaZaszeregowania`, +`Krotnosc`/`ZmianaKrotnosci`, `RodzajPrzeszergowania`, `Pracownik`, `PracHistoria`. + +**Wykonanie dokumentu:** `Soneta.Przeszeregowania.Przeszeregowanie.WykonajWorker` (metoda `Wykonaj`, +`Params { Wykonaj: bool }`) — materializuje dokument na danych pracowników (tworzy/zmienia dodatki). +`ElementPrzeszeregowania.Wykonaj(Log)` realizuje pojedynczą pozycję. + +**Pułapki:** +- To **operacja seryjna na danych operacyjnych** — trzymaj transakcje krótkie, duże grupy dziel na paczki + (safe-code §13.1). Workery przyjmują tablicę pracowników — przekaż przefiltrowaną listę (po stronie + serwera, safe-code §6). +- Workery `NowyDodatek`/`ZmianaDodatku`/`ZakończDodatek` operują na **definicji elementu** (`Definicja`), + więc wybór właściwej `DefinicjaElementu` jest kluczowy (po nazwie / `RodzajZrodla == Dodatek`). +- Sam dokument `Przeszeregowanie` **nie zmienia danych** dopóki nie zostanie wykonany (`WykonajWorker`); + do tego momentu to plan. Po `Wykonaj` zmiany trafiają w dodatki/etat pracowników. +- Indywidualne (jednostkowe) odpowiedniki to workery z KADRY-C2/KADRY-C1 na pojedynczym pracowniku + (`Pracownik.DodajDodatekWorker`/`ZmieńDodatekWorker`/`ZabierzDodatekWorker`); moduł Przeszeregowania + jest dla **grupy**. + +**Snippet (operacja seryjna — nowy dodatek dla grupy):** + +```csharp +var kadry = session.GetKadry(); +var def = session.GetPlace().DefElementow.WgNazwy["Premia"]; + +// Grupa pracowników — filtr serwerowy (np. po wydziale), nie pełny skan: +Pracownik[] grupa = kadry.Pracownicy[(Pracownik p) => p.Last.Etat.Okres.Contains(Date.Today)] + .Cast().ToArray(); + +var context = login.CreateEmptyContext().Clone(session); + +using (var t = session.Logout(editMode: true)) +{ + var par = new NowyDodatekWorker.Params(context) // Params: ctor (Context) + { + Definicja = def, + Podstawa = (Currency)300m, + }; + // ctor bezparametrowy; parametry przez właściwości Pars/Pracownicy: + new NowyDodatekWorker { Pars = par, Pracownicy = grupa }.NowyDodatek(); + t.CommitUI(); +} +session.Save(); +``` + +--- + +### KADRY-C6 — Świadczenia socjalne (ZFŚS) i ich rozliczenie (★) + +**Cel:** przyznać pracownikowi świadczenie socjalne z ZFŚS (zapomoga, dopłata do wypoczynku, paczka) +i ustawić jego rozliczenie płacowe (element, kwota, okres). + +**Klasa i model:** `Soneta.Kadry.SwiadczSocjalne` — `GuidedRow` root, tabela `SwiadczeniaSoc`. Child +pracownika: `pracownik.Swiadczenia: SubTable`. **Konstruktor publiczny:** +`new SwiadczSocjalne(pracownik)`. + +**Pola `SwiadczSocjalne`:** + +| Pole | Typ | Uwaga | +|---|---|---| +| `Pracownik` | `Soneta.Kadry.Pracownik` | właściciel | +| `Definicja` | `Soneta.Kadry.DefinicjaŚwiadczeniaSocjalnego` | rodzaj świadczenia (słownik `DefSwiadczSocjal`) | +| `Data` | `Soneta.Types.Date` | data przyznania | +| `Nazwa` | `string` | nazwa | +| `Opis` | `Soneta.Business.MemoText` | opis | +| `Rozliczenie` | `Soneta.Kadry.RozliczenieSwiadczenia` (subrow) | dane rozliczeniowe (poniżej) | + +**Subrow `Rozliczenie` (`RozliczenieSwiadczenia`):** + +| Pole | Typ | Uwaga | +|---|---|---| +| `Rozliczenie.Element` | `Soneta.Place.DefinicjaElementu` | element płacowy do rozliczenia świadczenia | +| `Rozliczenie.Kwota` | `Soneta.Types.Currency` | kwota świadczenia | +| `Rozliczenie.Okres` | `Soneta.Types.FromTo` | okres rozliczenia | +| `Rozliczenie.Data` | `Soneta.Types.Date` | data rozliczenia | +| `Rozliczenie.Rozliczone` | `bool` | czy rozliczone (po naliczeniu wypłaty) | + +**Definicja (`DefinicjaŚwiadczeniaSocjalnego`, słownik `DefSwiadczSocjal`):** `Nazwa: string`, +`Element: DefinicjaElementu` (domyślny element rozliczenia), `Kwota: Currency` (domyślna kwota). Z niej +dziedziczy świadczenie domyślny element i kwotę. + +**Worker seryjny:** `Soneta.Kadry.SwiadczSocjalne.DodajŚwiadczenieWorker` (metoda `DodajŚwiadczenie`) — +ctor **bezparametrowy**, parametry przez właściwości `Pars` i `Pracownicy: Pracownik[]`; `Params` ma +ctor `(Context)`: `Params { Definicja: DefinicjaŚwiadczeniaSocjalnego, DataPrzyznania: Date, Kwota: +Currency, Element: DefinicjaElementu, DataRozliczenia: Date }` — nadaje świadczenie grupie (menu +*Operacje seryjne / Dodaj świadczenia socjalne*). Wzorzec: +`new DodajŚwiadczenieWorker { Pars = new …Params(ctx){…}, Pracownicy = tab }.DodajŚwiadczenie()`. + +**Pułapki:** +- `Definicja` (świadczenia) pobierana ze słownika `DefSwiadczSocjal`; jej `Element`/`Kwota` są domyślne — + na konkretnym świadczeniu nadpisujesz przez `Rozliczenie.Element`/`Rozliczenie.Kwota`. +- **Faktyczne rozliczenie (wypłata świadczenia, `Rozliczenie.Rozliczone == true`) następuje przy + naliczeniu wypłaty** — samo dodanie świadczenia tworzy tylko zlecenie rozliczenia. +- Dla grupy używaj `DodajŚwiadczenieWorker`; pojedynczo — `new SwiadczSocjalne(pracownik)` + `AddRow`. + +**Snippet:** + +```csharp +var kadry = session.GetKadry(); +var pracownik = kadry.Pracownicy.WgKodu["006"]; +var defSwiadcz = kadry.DefSwiadczSocjal.WgNazwy["Dopłata do wypoczynku"]; +var element = session.GetPlace().DefElementow.WgNazwy["Świadczenie socjalne"]; + +using (var t = session.Logout(editMode: true)) +{ + var sw = new SwiadczSocjalne(pracownik); + kadry.SwiadczeniaSoc.AddRow(sw); + + sw.Definicja = defSwiadcz; + sw.Data = new Date(2026, 6, 1); + sw.Rozliczenie.Element = element; // element płacowy rozliczenia + sw.Rozliczenie.Kwota = (Currency)1000m; + sw.Rozliczenie.Okres = FromTo.Month(new YearMonth(2026, 6)); + + t.Commit(); +} +session.Save(); + +// Odczyt świadczeń pracownika: +foreach (SwiadczSocjalne s in pracownik.Swiadczenia) +{ + Currency kwota = s.Rozliczenie.Kwota; + bool rozliczone = s.Rozliczenie.Rozliczone; +} +``` + +--- + +### KADRY-C7 — Pożyczki (KZP / ZFM) (★) + +**Cel:** zarejestrować członkostwo pracownika w funduszu pożyczkowym, udzielić pożyczki, zbudować +harmonogram rat i potrącać raty z wynagrodzenia. + +**Hierarchia obiektów (wszystkie `GuidedRow` root, childy pracownika):** +- `Soneta.Kadry.FundPozyczkowy` (tabela `FundPozyczkowe`) — **członkostwo** w funduszu; + `pracownik.FunduszePozyczkowe: SubTable`. Ctor: + `new FundPozyczkowy(pracownik, definicja)`. +- `Soneta.Kadry.Pozyczka` (tabela `Pozyczki`) — **pożyczka** udzielona w ramach funduszu; kolekcja + `fundusz.Pozyczki: SubTable`. Ctor: `new Pozyczka(fundusz)`. +- `Soneta.Kadry.RataPozyczki` (tabela `RatyPozyczek`) — **rata** harmonogramu; `pozyczka.Raty: + SubTable`. Raty pracownik widzi przez `pracownik.SplacaneRaty` + (oraz `ZyrowaneRaty` jako żyrant). Ctor: `new RataPozyczki(pozyczka)`. +- `Soneta.Kadry.DefinicjaFunduszuPozyczkowego` (słownik `DefFundPozycz`, konfiguracyjny) — zasady + funduszu (oprocentowanie, elementy płacowe wpisowego/składki/wycofania). + +**Pola `Pozyczka` (kluczowe):** + +| Pole | Typ | Uwaga | +|---|---|---| +| `Fundusz` | `Soneta.Kadry.FundPozyczkowy` | fundusz, w ramach którego udzielono | +| `Data` | `Soneta.Types.Date` | data udzielenia | +| `Kwota` | `Soneta.Types.Currency` | kwota pożyczki | +| `Element` | `Soneta.Place.DefinicjaElementu` | element wypłaty pożyczki | +| `ElementRaty` | `Soneta.Place.DefinicjaElementu` | element potrącenia raty | +| `IloscRat` | `int` | liczba rat | +| `KwotaRaty` | `Soneta.Types.Currency` | kwota raty | +| `SplatyOd` | `Soneta.Types.YearMonth` | miesiąc rozpoczęcia spłat | +| `Procent` | `Soneta.Types.Percent` | oprocentowanie | +| `Sposob` | `Soneta.Kadry.SposóbSpłatyOdsetek` | sposób spłaty odsetek | +| `AlgorytmRaty` | `Soneta.Kadry.AlgorytmRatyPożyczki` | algorytm wyliczania raty | +| `Raty` | `SubTable` | harmonogram rat | +| `Stan` | `Soneta.Kadry.StanSpłat` | enum: `NieSpłacona = 0`, `Częściowo = 1`, `Całkowicie = 2` | +| `Splacona` | `bool` | spłacona w całości | + +**Pola `RataPozyczki`:** `Pozyczka`, `Data: Date`, `Miesiąc: YearMonth`, `Kapital: Currency`, +`Odsetki: Currency`, `Element: DefinicjaElementu` (potrącenie raty), `Stan: StanSpłat`, +`Pozostaje`/`PozostajeKapitał`/`PozostajeOdsetki` (kalkulowane), `Zyrant: Pracownik`, +`Splacajacy: Pracownik`. + +**Generowanie harmonogramu (workery):** + +| Worker | Metoda | Params / sygnatura | +|---|---|---| +| `Soneta.Kadry.Pozyczka.UzgodnijRatyWorker` | `UzgodnijRaty` | ctor bezparam.; `Pars = new Params(ctx) { UzgodnijRaty = true }` (uwaga: `PrzeliczRaty` jest **tylko-do-odczytu**), `Pożyczka = pozyczka` — **buduje/przelicza harmonogram rat** wg `IloscRat`/`KwotaRaty`/`SplatyOd` | +| `Soneta.Kadry.Pozyczka.PożyczkaWorker` | `Pożyczka` | podsumowanie spłat (props: `Razem`, `Spłaty`, `Pozostaje`, `RazemOdsetki`…) | +| `Soneta.Kadry.Pozyczka.ElementWypłatyWorker` | `Pokaż` | podgląd elementu wypłaty pożyczki | + +Metody na samym `Pozyczka`: `pozyczka.UpdatePozyczka()` (przelicz), `pozyczka.Rata(YearMonth, +DefinicjaElementu)`, `pozyczka.RatyZaMiesiąc(YearMonth)`, `pozyczka.SąRaty(YearMonth)`. + +**Pułapki:** +- Ścieżka tworzenia jest **trzystopniowa**: `FundPozyczkowy(pracownik, definicja)` → `Pozyczka(fundusz)` + → harmonogram. Pożyczki **nie da się** utworzyć bez funduszu (ctor wymaga `FundPozyczkowy`). +- Harmonogram rat generuj **workerem** `UzgodnijRatyWorker` (albo `UpdatePozyczka()`), nie ręcznym + dodawaniem `RataPozyczki` — worker rozkłada kapitał/odsetki wg algorytmu. +- `Element` (wypłaty) i `ElementRaty` (potrącenia) to **różne** definicje elementów — `ElementRaty` + realizuje potrącenie raty w wypłacie. +- **Faktyczne potrącenie raty następuje przy naliczeniu wypłaty** — `Stan`/`Splacono`/`Pozostaje` + aktualizują się po naliczeniu. Samo udzielenie pożyczki ich nie zmienia. +- `DefinicjaFunduszuPozyczkowego` to słownik konfiguracyjny — pobierz istniejący wpis, nie twórz „w locie". + +**Snippet:** + +```csharp +var kadry = session.GetKadry(); +var pracownik = kadry.Pracownicy.WgKodu["006"]; +var defFunduszu = kadry.DefFundPozycz.WgNazwy["KZP"]; +var elWyplata = session.GetPlace().DefElementow.WgNazwy["Pożyczka"]; +var elRata = session.GetPlace().DefElementow.WgNazwy["Spłata pożyczki"]; + +using (var t = session.Logout(editMode: true)) +{ + // 1) Członkostwo w funduszu + var fundusz = new FundPozyczkowy(pracownik, defFunduszu); + kadry.FundPozyczkowe.AddRow(fundusz); + fundusz.Okres = new FromTo(new Date(2026, 1, 1), Date.MaxValue); + + // 2) Pożyczka w ramach funduszu + var pozyczka = new Pozyczka(fundusz); + kadry.Pozyczki.AddRow(pozyczka); + pozyczka.Data = new Date(2026, 1, 10); + pozyczka.Kwota = (Currency)12000m; + pozyczka.Element = elWyplata; + pozyczka.ElementRaty = elRata; + pozyczka.IloscRat = 12; + pozyczka.SplatyOd = new YearMonth(2026, 2); + + // 3) Harmonogram rat (worker bezparametrowy; Params: ctor (Context); PrzeliczRaty read-only) + var context = login.CreateEmptyContext().Clone(session); + var par = new Pozyczka.UzgodnijRatyWorker.Params(context) { UzgodnijRaty = true }; + new Pozyczka.UzgodnijRatyWorker { Pars = par, Pożyczka = pozyczka }.UzgodnijRaty(); + + t.CommitUI(); +} +session.Save(); + +// Odczyt harmonogramu: +foreach (FundPozyczkowy f in pracownik.FunduszePozyczkowe) + foreach (Pozyczka p in f.Pozyczki) + foreach (RataPozyczki r in p.Raty) + { + YearMonth m = r.Miesiąc; + Currency kapital = r.Kapital, odsetki = r.Odsetki; + StanSpłat stan = r.Stan; + } +``` + diff --git a/soneta-programming/references/domeny/kadry/KADRY04-nieobecnosci.md b/soneta-programming/references/domeny/kadry/KADRY04-nieobecnosci.md new file mode 100644 index 0000000..58e5bc7 --- /dev/null +++ b/soneta-programming/references/domeny/kadry/KADRY04-nieobecnosci.md @@ -0,0 +1,887 @@ +# KADRY04 — Nieobecności i czas pracy + +> Wspólne fakty o typie, podstawowe typy i szablon wzorca: [../kadry.md](../kadry.md). + +### KADRY-D1 — Wprowadzanie nieobecności (★) + +**Cel:** zarejestrować nieobecność pracownika (urlop wypoczynkowy, zwolnienie chorobowe, urlop +bezpłatny, opieka itp.) za wskazany okres oraz odczytać nieobecności obowiązujące w danym przedziale dat. + +**Fakty o typie (zweryfikowane skanem DLL):** + +- **`Soneta.Kalend.Nieobecnosc` jest klasą abstrakcyjną** — tabela `Nieobecnosci` (`GuidedRow` root, + child `Pracownik`-a). `new Nieobecnosc(...)` się nie skompiluje. +- Konkretny typ do tworzenia: **`Soneta.Kalend.NieobecnośćPracownika`** (dziedziczy z `Nieobecnosc`), + z **publicznym konstruktorem `new NieobecnośćPracownika(Pracownik pracownik)`** — ctor od razu wiąże + nieobecność z pracownikiem. (Drugi konkretny typ to `KorektaNieobecności` — patrz KADRY-D2.) +- Kolekcja na pracowniku: **`pracownik.Nieobecnosci: FromToSubTable`** + (uporządkowana po okresie „od–do"). +- Tabela z poziomu modułu: `session.GetKalend().Nieobecnosci`. + +**Pola i typy (`Nieobecnosc` / `NieobecnośćPracownika`):** + +| Pole | Typ | Rodzaj | Opis | +|---|---|---|---| +| `Definicja` | `Soneta.Kalend.DefinicjaNieobecnosci` | bazodanowe, **zapisywalne** | rodzaj nieobecności (słownik konfiguracyjny); decyduje o typie (urlop / zasiłek / bezpłatny) | +| `Okres` | `Soneta.Types.FromTo` | bazodanowe, **zapisywalne** | zakres dat nieobecności „od–do" | +| `OdGodziny`, `DoGodziny` | `Soneta.Types.Time` | — | godziny (nieobecności godzinowe) | +| `Norma`, `NormaNie` | `Soneta.Types.Time` | bazodanowe | normy czasowe | +| `IlośćDni` / `Dni` | `int` | kalkulowane/zapisywalne | liczba dni nieobecności | +| `Pracownik` | `Soneta.Kadry.Pracownik` | **tylko do odczytu** | właściciel (ustawiany ctorem, nie da się zmienić setterem) | +| `Zwolnienie` | `Soneta.Kalend.ZwolnienieZUS` (subrow) | bazodanowe | dane ZUS dla zwolnień chorobowych (`KodChoroby`, `Numer` ZLA, `PonownieUstalPodstawe`…) | +| `Urlop`, `Macierzynski`, `Wychowawczy`, `Okolicznosciowy` | subrowy | bazodanowe | szczegóły poszczególnych typów urlopów | +| `Korygowana` | `bool` | **tylko do odczytu** | czy nieobecność jest korektą (patrz KADRY-D2) | + +**Dostęp do definicji nieobecności (`DefNieobecnosci`):** + +- `session.GetKalend().DefNieobecnosci.WgNazwy[string]` — pobranie po nazwie, np. + `WgNazwy["Urlop wypoczynkowy"]`, `WgNazwy["Zwolnienie chorobowe"]`, + `WgNazwy["Urlop bezpłatny (art 174 kp)"]`. Nazwy muszą **dokładnie** odpowiadać słownikowi danej bazy + (w Demo nie ma wpisu „Urlop bezpłatny" — jest „Urlop bezpłatny (art 174 kp)"); `WgNazwy[...]` dla + nieistniejącej nazwy zwraca `null`. +- `session.GetKalend().DefNieobecnosci[string]` (indeksator domyślny po nazwie) — równoważne. +- `DefinicjaNieobecnosci` ma pola `Nazwa: string`, `Kod: string`, `Typ: TypNieobecnosci`. + +**Wyszukiwanie po dacie/okresie:** `pracownik.Nieobecnosci.GetIntersectedRows(FromTo)` zwraca +`IList` nieobecności przecinających podany przedział. + +**Snippet:** + +```csharp +var kalend = session.GetKalend(); +var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; +// Nieobecność BEZ limitu (np. urlop bezpłatny) można wprowadzić wprost. Dla nieobecności +// LIMITOWANYCH (urlop wypoczynkowy) najpierw musi istnieć naliczony limit — patrz pułapki i KADRY-D7. +var defNieob = kalend.DefNieobecnosci.WgNazwy["Urlop bezpłatny (art 174 kp)"]; + +using (var t = session.Logout(editMode: true)) +{ + // typ konkretny; ctor wiąże nieobecność z pracownikiem + var nieobecnosc = session.AddRow(new NieobecnośćPracownika(pracownik)); + nieobecnosc.Definicja = defNieob; // rodzaj nieobecności + nieobecnosc.Okres = new FromTo(new Date(2026, 7, 1), new Date(2026, 7, 5)); + t.Commit(); +} +session.Save(); + +// Odczyt nieobecności przecinających lipiec 2026: +var lipiec = new FromTo(new Date(2026, 7, 1), new Date(2026, 7, 31)); +foreach (Nieobecnosc n in pracownik.Nieobecnosci.GetIntersectedRows(lipiec)) +{ + // n.Definicja.Nazwa, n.Okres, n.Dni +} +``` + +**Pułapki:** +- **Nie** rób `new Nieobecnosc(...)` — typ jest abstrakcyjny. Używaj `new NieobecnośćPracownika(pracownik)`. +- **Nieobecności limitowane wymagają istniejącego limitu.** Ustawienie `Okres` dla nieobecności + powiązanej z limitem (np. „Urlop wypoczynkowy") synchronicznie przelicza limit i rzuca + `Soneta.Kalend.DefinicjaLimitu.LimitNotFoundException`, gdy pracownik nie ma naliczonego limitu na + dany rok. Dlatego: albo najpierw nalicz limit (patrz KADRY-D7), albo użyj nieobecności bez limitu + (np. „Urlop bezpłatny (art 174 kp)") — jak w snippetcie powyżej. +- `Definicja` jest **wymagana** — bez niej nieobecność nie zostanie poprawnie naliczona/zapisana. + Pobieraj istniejący wpis słownika przez `DefNieobecnosci.WgNazwy[...]`, nie twórz „w locie". +- `Pracownik` jest **tylko do odczytu** — relację ustawia konstruktor, nie da się jej później zmienić. +- Tabela `Nieobecnosci` jest **operacyjna guided** — przy przeglądaniu poprzecznym (po wszystkich + pracownikach) filtruj zakresem czasowym (safe-code §6.3). W zakresie jednego pracownika korzystaj + z `pracownik.Nieobecnosci` i `GetIntersectedRows`. +- Nakładające się nieobecności i niepoprawne okresy wychwytują weryfikatory przy `Save()` + (`RowException`) — obsłuż wyjątek. +- Pełna transakcja w `session.Logout(editMode: true)`; brak `Commit()` = rollback przy `Dispose()`. + +--- + +### KADRY-D2 — Korygowanie nieobecności już wypłaconych (★) + +**Cel:** poprawić nieobecność, która została już rozliczona w wypłacie — zmienić jej okres lub typ +(definicję) i/lub wymusić ponowne ustalenie podstawy naliczania zasiłku. enova rozróżnia dwie ścieżki: +(a) **modyfikacja istniejącej nieobecności** + ponowne ustalenie podstawy, (b) **korekta** jako odrębny +rekord typu `KorektaNieobecności`. + +**Fakty o typie (zweryfikowane skanem DLL):** + +- Pola `Definicja: DefinicjaNieobecnosci` i `Okres: FromTo` na `Nieobecnosc` są **zapisywalne** + publicznie — można je zmienić na istniejącym rekordzie. +- `Nieobecnosc.Korygowana: bool` i `Nieobecnosc.Pracownik` są **tylko do odczytu**. +- Subrow `Zwolnienie: Soneta.Kalend.ZwolnienieZUS` posiada flagę `PonownieUstalPodstawe: bool` + oraz **publiczną metodę `SetPonownieUstalPodstawe(bool)`** — to ona steruje przeliczeniem podstawy + zasiłku przy kolejnym naliczeniu wypłaty. +- Worker (czynność menu, `DataType = Nieobecnosc`): klasa + **`Soneta.Kalend.Nieobecnosc.UstalPonowniePodstawęNaliczaniaWorker`** — czynność + „Ustal ponownie podstawę naliczania". Worker: + - ma publiczny bezparametrowy ctor; + - przyjmuje kontekst przez settowalną property `[Context] public Params Nieobecność`; + - klasa `…Worker.Params : ContextBase` ma **publiczny ctor `Params(Context context)`**, który czyta + nieobecność z `context[typeof(Nieobecnosc)]`, oraz settowalną property `UstalPodstawę: bool`; + - metoda `public void PonownieUstalPodstawę()` jest jego akcją; + - `static bool IsEnabledPonownieUstalPodstawę(Nieobecnosc)` mówi, kiedy czynność jest aktywna + (dotyczy zwolnień ZUS i urlopów macierzyńskich: `Zwolnienie.IsZUS || Macierzynski.IsMacierzyński`, + przy braku `BlokadaOkresu`). +- Drugi konkretny typ nieobecności: **`Soneta.Kalend.KorektaNieobecności`** (dziedziczy `Nieobecnosc`), + z **publicznym ctor `new KorektaNieobecności(NieobecnośćPracownika nieobecność)`** — tworzy rekord + korygujący wskazaną nieobecność. Ma zapisywalne `Definicja`, `Okres`, `IlośćDni`, + `RozliczenieWDniu`, `RozliczenieData`, a kolekcje `ElementyKorygowane`/`ElementyKorygowaneStorno` + są tylko do odczytu (wyliczane). + +**Wariant A — zmiana okresu/typu + ponowne ustalenie podstawy (modyfikacja istniejącego rekordu):** + +```csharp +var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; +var okresStary = new FromTo(new Date(2026, 3, 2), new Date(2026, 3, 10)); + +// odszukaj istniejącą (już rozliczoną) nieobecność po przecięciu z okresem +var nieobecnosc = (Nieobecnosc)pracownik.Nieobecnosci.GetIntersectedRows(okresStary)[0]; + +using (var t = session.Logout(editMode: true)) +{ + nieobecnosc.Okres = new FromTo(new Date(2026, 3, 2), new Date(2026, 3, 12)); // wydłużenie okresu + // dla zwolnień ZUS — wymuś ponowne ustalenie podstawy przy najbliższym naliczeniu wypłaty: + nieobecnosc.Zwolnienie.SetPonownieUstalPodstawe(true); + t.Commit(); +} +session.Save(); +``` + +**Wariant B — czynność „Ustal ponownie podstawę naliczania" przez worker (kontekst):** + +```csharp +var worker = new Nieobecnosc.UstalPonowniePodstawęNaliczaniaWorker(); +var ctx = Context.Empty.Clone(session); +ctx[typeof(Nieobecnosc)] = nieobecnosc; // worker czyta nieobecność z kontekstu +worker.Nieobecność = new Nieobecnosc.UstalPonowniePodstawęNaliczaniaWorker.Params(ctx) +{ + UstalPodstawę = true +}; +worker.PonownieUstalPodstawę(); // wykonuje własną transakcję + Commit +session.Save(); +``` + +**Wariant C — odrębny rekord korekty (`KorektaNieobecności`):** + +```csharp +using (var t = session.Logout(editMode: true)) +{ + var nPrac = (NieobecnośćPracownika)nieobecnosc; // korekta dotyczy NieobecnośćPracownika + var korekta = session.AddRow(new KorektaNieobecności(nPrac)); + korekta.Definicja = nPrac.Definicja; + // Okres korekty MUSI być podzbiorem okresu korygowanej nieobecności (tu: 2..10): + korekta.Okres = new FromTo(new Date(2026, 3, 3), new Date(2026, 3, 8)); + t.Commit(); +} +session.Save(); +// Po zapisie korygowana nieobecność ma flagę Korygowana == true. +``` + +**Pułapki:** +- **Faktyczne** przeliczenie wartości zasiłku NIE następuje w momencie ustawienia flagi/wywołania + workera — flaga `PonownieUstalPodstawe` jest odczytywana dopiero przy **ponownym naliczeniu wypłaty** + (mechanizm `PodstawaZasilku`). Sam test korekty rekordu nieobecności (Demo, rollback) zweryfikuje + zmianę `Okres`/`Definicja`/flagi, ale **nie zweryfikuje przeliczonych kwot wypłaty** bez pełnego + scenariusza naliczenia listy płac (patrz sekcja „funkcjonalności niewykonalne"). +- `IsEnabledPonownieUstalPodstawę` ogranicza czynność do zwolnień ZUS / macierzyńskich — dla zwykłego + urlopu wypoczynkowego worker nie ma zastosowania; tam korektę robisz przez zmianę `Okres`/`Definicja` + albo rekord `KorektaNieobecności`. +- **Okres korekty (`KorektaNieobecności.Okres`) musi być podzbiorem okresu korygowanej nieobecności** — + wyjście poza ten zakres rzuca `Nieobecnosc.KorygowanyOkresException`. +- Dla nieobecności bez skutków płacowych (np. urlop bezpłatny) `KorektaNieobecności` **nie pojawia się + jako osobny wiersz** w `pracownik.Nieobecnosci` — obserwowalnym efektem jest flaga `Korygowana == true` + na nieobecności pierwotnej. +- Korekta zmienia dane operacyjne powiązane z wypłatą — trzymaj transakcję krótką i obsłuż + `RowConflictException` / `RowException` z `Save()` (safe-code §4, §13.1). +- Worker wykonuje własną transakcję (`Session.Logout(true)` + `Commit`) — nie zagnieżdżaj go w innej + otwartej transakcji edycyjnej. + +--- + +### KADRY-D7 — Analiza limitów urlopowych (★) + +**Cel:** odczytać limit nieobecności (np. urlop wypoczynkowy) pracownika za dany rok — ile przysługuje, +ile wykorzystano, ile pozostało. Limity **nie są tworzone ręcznie** — powstają przez naliczanie. + +**Fakty o typie (zweryfikowane skanem DLL):** + +- **`Soneta.Kalend.LimitNieobecnosci`** — tabela `LimNieobecnosci`, `GuidedRow` **child** pracownika + (relacja przez pole `Pracownik`). Instancje powstają wyłącznie przez naliczanie — **nie twórz ich + konstruktorem**. +- Kolekcja na pracowniku: **`pracownik.Limity: SubTable`** + (nazwa kolekcji to `Limity`, nie „LimityNieobecnosci"). +- Tabela z poziomu modułu: `session.GetKalend().LimNieobecnosci`. + +**Pola i typy (`LimitNieobecnosci`) — odczyt:** + +| Pole | Typ | Rodzaj | Opis | +|---|---|---|---| +| `Definicja` | `Soneta.Kalend.DefinicjaLimitu` | bazodanowe | rodzaj limitu (urlop wypoczynkowy itd.) | +| `Okres` | `Soneta.Types.FromTo` | bazodanowe | okres limitu (zwykle rok) | +| `OkresWażności` | `Soneta.Types.FromTo` | kalkulowane | okres ważności limitu | +| `Limit` | `int` | bazodanowe | limit (dni) wynikający z kodeksu pracy | +| `LimitDni` | `int` | kalkulowane | limit w dniach | +| `LimitGodz` | `Soneta.Types.Time` | bazodanowe | limit w godzinach | +| `Razem` / `RazemGodz` | `int` / `Time` | kalkulowane | łączny przysługujący (limit + przeniesienia + zmiany) | +| `Wykorzystane` / `WykorzystaneGodz` | `int` / `Time` | bazodanowe | wykorzystane dni/godziny | +| `Pozostalo` | `int` | kalkulowane | pozostało (dni, int) | +| `PozostaloDni` | `double` | kalkulowane | pozostało dni (z częścią ułamkową) | +| `PozostaloGodz` | `Soneta.Types.Time` | kalkulowane | pozostało godzin | +| `ZaleglyDni` / `ZaleglyGodz` | `double` / `Time` | kalkulowane | zaległy z poprzednich okresów | +| `Przeniesienie` / `PrzeniesienieDni` | `int` / `double` | kalkulowane | przeniesione z poprzedniego roku | +| `Korekta`, `Zmiana` | `int` | bazodanowe | korekty/zmiany limitu | +| `Pracownik` | `Soneta.Kadry.Pracownik` | bazodanowe (guided-parent), **read-only** | właściciel | + +> **Wykorzystany = `Razem - Pozostalo`** (lub bezpośrednio pole `Wykorzystane`). „Przysługujący" to +> `Razem` (limit kodeksowy + przeniesienia + zmiany), a nie samo `Limit`. + +**Dostęp do definicji limitów (`DefinicjeLimitow`):** + +- `session.GetKalend().DefinicjeLimitow.WgNazwy[string]` — np. `WgNazwy["Urlop wypoczynkowy"]`. +- Skróty typowane (property zwracające `DefinicjaLimitu`): `DefinicjeLimitow.UrlopWypoczynkowy`, + `.UrlopDodatkowy`, `.OpiekaNadZdrowym`, `.UrlopOpiekunczy`, `.ZwolnienieOdPracySilaWyzsza` itd. +- `DefinicjaLimitu` ma pola `Nazwa: string`, `Typ: TypLimitu`. + +**Naliczanie limitu (by mógł istnieć do odczytu) — `Soneta.Kalend.NaliczanieLimitow`:** + +- Klasa z **publicznym bezparametrowym ctor**; settowalne property: + - `Pars: NaliczanieLimitow.Params` (set), + - `Pracownicy: ICollection` (set) **albo** `PracownicyIdx: Pracownik[]` (set). +- Klasa `NaliczanieLimitow.Params : ContextBase` ma **publiczny ctor `Params(Context context)`** + oraz settowalne: `Definicja: DefinicjaLimitu`, `Okres: FromTo`, `KopiujKorekty: bool`, + `ZapisPerPracownik: bool`. +- Metoda **`public void DodajLimit()`** — nalicza limit (zapisuje rekordy `LimitNieobecnosci`). + (Jest też `DodajLimitUrlopowy()`.) + +**Snippet — naliczenie + odczyt:** + +```csharp +var kalend = session.GetKalend(); +var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; +var defUrlop = kalend.DefinicjeLimitow.WgNazwy["Urlop wypoczynkowy"]; // lub DefinicjeLimitow.UrlopWypoczynkowy +var rok = FromTo.Year(new Date(2026, 1, 1)); + +using (var t = session.Logout(editMode: true)) +{ + var naliczanie = new NaliczanieLimitow + { + Pars = new NaliczanieLimitow.Params(Context.Empty.Clone(session)) + { + Definicja = defUrlop, + Okres = rok, + KopiujKorekty = true + }, + Pracownicy = new Pracownik[] { pracownik } + }; + naliczanie.DodajLimit(); // tworzy/aktualizuje LimitNieobecnosci + t.Commit(); +} +session.Save(); + +// Odczyt limitu urlopu wypoczynkowego za rok 2026. +// UWAGA: filtr serwerowy obejmuje TYLKO pola bazodanowe i prostych porównań — Okres (FromTo) +// NIE da się porównać serwerowo (==), więc filtrujemy serwerowo po Definicja, a rok w pamięci: +var lim = pracownik.Limity[(LimitNieobecnosci l) => l.Definicja == defUrlop] + .Cast() + .FirstOrDefault(l => l.Okres.From == rok.From); +if (lim != null) +{ + int przysluguje = lim.Razem; // przysługujący (limit + przeniesienia + zmiany) + int pozostalo = lim.Pozostalo; // pozostało + int wykorzystany = przysluguje - pozostalo; // == lim.Wykorzystane + // lim.PozostaloDni, lim.PozostaloGodz, lim.ZaleglyDni +} +``` + +**Pułapki:** +- **Nie** twórz `new LimitNieobecnosci(...)` — limit powstaje przez naliczanie (`DodajLimit`). W bazie + Demo limit dla danego roku może jeszcze nie istnieć — w teście trzeba go **najpierw naliczyć**. +- Kolekcja na pracowniku to `pracownik.Limity` (nie `LimityNieobecnosci`). +- **Nie porównuj `Okres` (FromTo) w filtrze serwerowym** — `l.Okres == rok` rzuca `ArgumentException` + („pole nieznalezione"). Filtruj serwerowo po `Definicja`, a okres/rok porównaj w pamięci + (`.FirstOrDefault(l => l.Okres.From == rok.From)`). +- `Razem` może wynosić `0` dla pracowników bez danych napędzających wymiar urlopu (staż, data + urodzenia) — asercje opieraj na spójności (`Wykorzystane == Razem - Pozostalo`, `Razem >= 0`), + a nie na założeniu `Razem > 0`. +- `Pracownik` na limicie jest read-only (relacja guided) — naliczanie samo wiąże rekord z pracownikiem. +- Filtruj limity serwerowo po `Definicja` i `Okres` (`pracownik.Limity[condition]`), nie iteruj całości + z `if` w pamięci (safe-code §6.1). Tabela `LimNieobecnosci` jest operacyjna guided. +- `Context.Empty.Clone(session)` daje kontekst związany z bieżącą sesją — wymagany przez ctor + `NaliczanieLimitow.Params(Context)`. +- Naliczanie modyfikuje dane operacyjne — w transakcji edycyjnej, krótko, z obsługą wyjątków z `Save()`. + +### KADRY-D3 — Import e-ZLA z PUE ZUS (zwolnienia lekarskie) + +**Cel:** zaewidencjonować w systemie zwolnienie lekarskie pobrane z PUE ZUS (e-ZLA). Sam **import to +operacja sieciowa** (komunikacja z PUE ZUS) — w kodzie biznesowym/teście dokumentujemy **model danych** +nieobecności chorobowej i jej dane ZUS, a nie samo połączenie z bramką PUE. + +**Fakty o typie (zweryfikowane skanem DLL):** + +- Zwolnienie chorobowe to `Soneta.Kalend.NieobecnośćPracownika` (typ konkretny z KADRY-D1) z `Definicja` + wskazującą na rodzaj zasiłkowy (np. „Zwolnienie chorobowe"). +- Dane ZUS zwolnienia leżą w subrowie **`Nieobecnosc.Zwolnienie: Soneta.Kalend.ZwolnienieZUS`** + (bazodanowy subrow na rekordzie nieobecności). +- Dane samego dokumentu ZLA leżą w subrowie **`Nieobecnosc.ZLA: Soneta.Kalend.ZLA`** + (`ZLA.Data: Date`, `ZLA.Wersja: WersjaZLA`, `ZLA.Zrodlo: MemoText`). + +**Pola i typy (`Nieobecnosc.Zwolnienie: ZwolnienieZUS`) — zapisywalne, bazodanowe:** + +| Pole | Typ | Opis | +|---|---|---| +| `Numer` | `string` | numer dokumentu ZLA (pole tekstowe — **maks. 9 znaków**) | +| `KodChoroby` | `string` | kod literowy choroby (A, B, C, D, …) | +| `Przyczyna` | `Soneta.Kalend.PrzyczynaZwolnienia` | przyczyna niezdolności do pracy | +| `Kwarantanna` | `Soneta.Kalend.ZwolnienieKwarantanna` | kwarantanna/izolacja | +| `LeczenieSzpitalne` | `bool` | pobyt w szpitalu | +| `ZwolnienieWystawione` | `Soneta.Types.Date` | data wystawienia ZLA | +| `ZwolnienieDostarczone` | `Soneta.Types.Date` | data dostarczenia | +| `PomniejszajZasilek` | `bool` | obniżenie zasiłku | +| `PonownieUstalPodstawe` | `bool` | wymuszenie przeliczenia podstawy (patrz KADRY-D2/KADRY-D6) | + +**Pola i typy (`Nieobecnosc.ZLA: ZLA`):** `Data: Date`, `Wersja: WersjaZLA`, `Zrodlo: MemoText`. + +**Snippet — ręczne odwzorowanie e-ZLA jako nieobecności chorobowej (bez sieci):** + +```csharp +var kalend = session.GetKalend(); +var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; +var defChor = kalend.DefNieobecnosci.WgNazwy["Zwolnienie chorobowe"]; + +using (var t = session.Logout(editMode: true)) +{ + var nieob = session.AddRow(new NieobecnośćPracownika(pracownik)); + nieob.Definicja = defChor; + nieob.Okres = new FromTo(new Date(2026, 5, 4), new Date(2026, 5, 10)); + // dane ZUS z e-ZLA (subrow Zwolnienie): + nieob.Zwolnienie.Numer = "ZLA000001"; // pole Numer ma limit 9 znaków + nieob.Zwolnienie.KodChoroby = "A"; + t.Commit(); +} +session.Save(); +``` + +**Pułapki:** +- **Sam import e-ZLA z PUE wymaga sieci** (uwierzytelnienie + bramka ZUS) — nie da się go odtworzyć + w teście jednostkowym na bazie Demo; testuj wyłącznie **odwzorowanie modelu danych** (subrow `Zwolnienie`). +- `Zwolnienie` i `ZLA` to subrowy — nie tworzysz ich osobno, są częścią rekordu `Nieobecnosc`; ustawiasz + ich pola po utworzeniu nieobecności. +- Definicja zasiłkowa musi istnieć w słowniku bazy (`DefNieobecnosci.WgNazwy[...]` ≠ `null`). +- **Faktyczne kwoty zasiłku** liczą się dopiero przy naliczeniu wypłaty — patrz uwaga przy KADRY-D2. + +--- + +### KADRY-D4 — Generowanie deklaracji Z-3 / Z-3a dla nieobecności chorobowej + +**Cel:** wygenerować zaświadczenie płatnika składek **Z-3** (pracownik etatowy) lub **Z-3a** (umowy/inni +ubezpieczeni) dla konkretnej nieobecności zasiłkowej. + +**Fakty o typie (zweryfikowane skanem DLL):** + +- Worker (czynność na `Nieobecnosc`): **`Soneta.Deklaracje.ZUS.ZUSZ3.Z3Worker`** — akcja + „Generuj deklarację Z-3", metoda `public object UtworzDeklaracjeZ3()`. +- Analogicznie **`Soneta.Deklaracje.ZUS.ZUSZ3.Z3aWorker`** — akcja „Generuj deklarację Z-3a", + metoda `public object UtworzDeklaracjeZ3a()`. +- Oba workery przyjmują przez `[Context]`: + - `KeduContext: DeklaracjaZUS.PUEContext` (property `Kedu: KEDU`), + - `Z3ParamContext: Z3ParamContext` / `Z3aParamContext` z polami m.in.: `Nieobecnosc: INieobecnoscLubZbieg`, + `NieobecnoscZContextu: bool`, `Pracownik: Pracownik`, `PracownikZContextu: bool`, `Okres: FromTo`, + `OkresZasiłkowy: FromTo`, `OkresZasilkowyOd: Date`, `Współczynnik: Fraction`, `RachBank: string`, + `KontynuacjaŚwiadczenia: bool`. + +**Snippet — generowanie Z-3 dla nieobecności (kontekst):** + +```csharp +var worker = new Soneta.Deklaracje.ZUS.ZUSZ3.Z3Worker(); +var ctx = Context.Empty.Clone(session); +ctx[typeof(Nieobecnosc)] = nieobChorobowa; // worker czyta nieobecność z kontekstu + +var deklaracja = worker.UtworzDeklaracjeZ3(); // zwraca obiekt deklaracji Z-3 +session.Save(); +``` + +**Pułapki:** +- **Sensowny Z-3 wymaga naliczonej wypłaty/podstawy zasiłku** — bez naliczonej podstawy deklaracja + powstanie z pustymi/zerowymi kwotami. W teście na czystej Demo zweryfikujesz fakt powstania obiektu + i ustawienie pól nagłówkowych (pracownik, okres), ale **nie kwoty zasiłku**. +- Worker przyjmuje dane przez `Context` (`ctx[typeof(Nieobecnosc)]`/`ctx[typeof(Pracownik)]`) — nie ma + prostego ctora parametrowego; zegnij pod swój scenariusz `Z3ParamContext`. +- Z-3 dotyczy etatu, Z-3a umów/innych ubezpieczonych — dobierz worker do tytułu ubezpieczenia. +- Metody zwracają `object` (deklaracja KEDU) — zachowaj/odczytaj wynik, nie zakładaj typu wprost. + +--- + +### KADRY-D5 — Obsługa przestoju (dodanie/usunięcie, przestój ekonomiczny — % wynagrodzenia) + +**Cel:** zaewidencjonować przestój pracownika (np. ekonomiczny) za okres oraz wskazać procent +wynagrodzenia przestojowego; usunąć przestój nakładający się na nieobecność ZUS. + +**Fakty o typie (zweryfikowane skanem DLL):** + +- **Dodanie przestoju:** worker **`Soneta.Kadry.DodajPrzestojWorker`** (czynność „Przestój/Dodaj przestój", + metoda `public void DodajPrzestoj()`): + - settowalne property: `Pracownicy: Pracownik[]`, `Pars: DodajPrzestojWorker.Params`; + - `Params` z polami: `DefinicjaStrefy: Soneta.Kalend.DefinicjaStrefy`, `Okres: FromTo`. +- **Procent wynagrodzenia przestojowego (przestój ekonomiczny):** worker + **`Soneta.Kadry.IndywidualnyProcentWynagrPrzestojowegoWorker`** (czynność + „Przestój/Przestój ekonomiczny - procent wynagr.", metoda `public void Aktualizuj()`): + - `Pracownicy: Pracownik[]`, `Pars.Data: Date`, `Pars.Procent: Soneta.Types.Percent`. +- **Usunięcie przestoju podczas nieobecności ZUS:** worker + **`Soneta.Kadry.UsunPrzestojNieobecnoscWorker`** (czynność „Przestój/Usuń przestój podczas + nieobecności ZUS", metoda `public void UsunPrzestoj()`): `Pracownicy: Pracownik[]`, `Pars.Okres: FromTo`. +- Procent wynagrodzenia przestojowego jest też trzymany na etacie: + `PracHistoria.Etat.Postojowe: Soneta.Kadry.WynagrodzeniePostojowe` (`Procent: Percent`, `Standardowe: bool`). +- `DefinicjaStrefy` (`session.GetKalend().DefinicjeStref`) — słownik konfiguracyjny stref (m.in. przestoju). + +**Snippet — dodanie przestoju:** + +```csharp +var kalend = session.GetKalend(); +var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; +var defStrefa = kalend.DefinicjeStref.WgNazwy["Przestój"]; // nazwa wg słownika danej bazy + +var worker = new Soneta.Kadry.DodajPrzestojWorker +{ + Pracownicy = new[] { pracownik }, + Pars = new Soneta.Kadry.DodajPrzestojWorker.Params(Context.Empty.Clone(session)) + { + DefinicjaStrefy = defStrefa, + Okres = new FromTo(new Date(2026, 6, 1), new Date(2026, 6, 5)) + } +}; +worker.DodajPrzestoj(); // worker wykonuje własną transakcję +session.Save(); +``` + +**Snippet — przestój ekonomiczny (procent):** + +```csharp +var worker = new Soneta.Kadry.IndywidualnyProcentWynagrPrzestojowegoWorker +{ + Pracownicy = new[] { pracownik }, + Pars = new Soneta.Kadry.IndywidualnyProcentWynagrPrzestojowegoWorker.Params(Context.Empty.Clone(session)) + { + Data = new Date(2026, 6, 1), + Procent = new Percent(0.5m) // 50% wynagrodzenia + } +}; +worker.Aktualizuj(); +session.Save(); +``` + +**Pułapki:** +- `DefinicjeStref.WgNazwy[...]` zależy od słownika danej bazy — zweryfikuj nazwę przestoju w Demo + (może być inna niż „Przestój"); dla nieistniejącej nazwy zwraca `null`. +- Worker wykonuje własną transakcję — nie zagnieżdżaj go w otwartej transakcji edycyjnej. +- `Percent` przyjmuj jako ułamek (`0.5m` = 50%), nie liczbę 50. +- `UsunPrzestojNieobecnoscWorker` usuwa przestój **kolidujący z nieobecnością ZUS** — to nie generyczne + „usuń przestój"; zakres działania ogranicza okres + obecność nieobecności ZUS. +- Skutki płacowe (wynagrodzenie przestojowe) liczą się dopiero przy naliczeniu wypłaty. + +--- + +### KADRY-D6 — Ustalanie/zmiana parametrów okresu zasiłkowego + +**Cel:** zmienić parametry okresu zasiłkowego nieobecności chorobowej — kontynuację/przedłużenie okresu +zasiłkowego oraz wymusić ponowne ustalenie podstawy naliczania zasiłku. + +**Fakty o typie (zweryfikowane skanem DLL):** + +- Parametry okresu zasiłkowego są w subrowie **`Nieobecnosc.Zwolnienie: ZwolnienieZUS`** (bazodanowe, + zapisywalne): + - `KontynuacjaOkrZas: Soneta.Kalend.KontynuacjaOkrZas` (enum: `Warunkowo`, `Tak`, `Nie`), + - `PrzedluzenieOkrZas: bool`, `PrzedluzeniaData: Soneta.Types.Date`, + - `PonownieUstalPodstawe: bool` + metoda `SetPonownieUstalPodstawe(bool)` (patrz KADRY-D2). +- Worker korekty okresu zasiłkowego: **`Soneta.Kalend.Nieobecnosc.KorektaOkresuZasiłkowegoWorker`** + (czynność „Zmień pozostałe parametry okresu zasiłkowego", metoda `public void PonownieUstalPodstawę()`): + - settowalne `Pars: KorektaOkresuZasiłkowegoWorker.Params` z polami: + `KontynuacjaOkrZas: KontynuacjaOkrZas`, `PrzedluzenieOkrZas: bool`, `PrzedluzeniaData: Date`. +- BO okresu zasiłkowego (przy wdrożeniu) — patrz KADRY-D10: `PracHistoria.ChorobowyBO` + (`DniZasilkowe`, `ZasilekOdDnia`, `PrzedluzenieOZ`). + +**Snippet — zmiana parametrów wprost na rekordzie:** + +```csharp +using (var t = session.Logout(editMode: true)) +{ + nieobChorobowa.Zwolnienie.KontynuacjaOkrZas = KontynuacjaOkrZas.Tak; + nieobChorobowa.Zwolnienie.PrzedluzenieOkrZas = true; + nieobChorobowa.Zwolnienie.PrzedluzeniaData = new Date(2026, 5, 31); + nieobChorobowa.Zwolnienie.SetPonownieUstalPodstawe(true); + t.Commit(); +} +session.Save(); +``` + +**Snippet — przez worker korekty okresu zasiłkowego:** + +```csharp +var worker = new Nieobecnosc.KorektaOkresuZasiłkowegoWorker(); +var ctx = Context.Empty.Clone(session); +ctx[typeof(Nieobecnosc)] = nieobChorobowa; +worker.Pars = new Nieobecnosc.KorektaOkresuZasiłkowegoWorker.Params(ctx) +{ + KontynuacjaOkrZas = KontynuacjaOkrZas.Tak, + PrzedluzenieOkrZas = true, + PrzedluzeniaData = new Date(2026, 5, 31) +}; +worker.PonownieUstalPodstawę(); // własna transakcja + Commit +session.Save(); +``` + +**Pułapki:** +- **Faktyczne** przeliczenie kwot zasiłku następuje dopiero przy **ponownym naliczeniu wypłaty** — test + na Demo zweryfikuje zmianę pól `KontynuacjaOkrZas`/`PrzedluzenieOkrZas`/`PrzedluzeniaData`/flagi, + ale nie kwoty. +- Parametry okresu zasiłkowego mają sens tylko dla nieobecności **ZUS** (zwolnienia chorobowe/zasiłki) — + dla urlopu wypoczynkowego są bez znaczenia. +- Worker wykonuje własną transakcję — nie zagnieżdżaj go w innej otwartej transakcji. + +--- + +### KADRY-D8 — Naliczanie i przeliczanie limitów nieobecności + +**Cel:** naliczyć limit nieobecności (jak KADRY-D7 — `NaliczanieLimitow.DodajLimit()`) oraz przeliczyć liczbę +wykorzystanych dni limitu (czynność „Przelicz wykorzystane"). + +**Fakty o typie (zweryfikowane skanem DLL):** + +- **Naliczenie limitu:** klasa **`Soneta.Kalend.NaliczanieLimitow`** — publiczny bezparametrowy ctor; + settowalne `Pars: NaliczanieLimitow.Params` (`Definicja: DefinicjaLimitu`, `Okres: FromTo`, + `KopiujKorekty: bool`, `ZapisPerPracownik: bool`) oraz `Pracownicy: ICollection` / + `PracownicyIdx: Pracownik[]`; metoda `public void DodajLimit()` (i `DodajLimitUrlopowy()`). + Wariant UI per-pracownik: worker **`Soneta.Kalend.UI.PracownikLimityNaliczanieWorker`** + (czynność „Nalicz limit nieobecności", metoda `DodajLimit()`) — `Pracownik: Pracownik`, + `Pars` jak wyżej. +- **Przeliczenie wykorzystanych:** worker + **`Soneta.Kalend.LimitNieobecnosci.Pracownicy.PrzeliczWykorzystaneWorker`** (czynność + „Limity nieobecności/Przelicz wykorzystane", metoda `public void PrzeliczWykorzystane()`): + - settowalne `Pracownicy: Pracownik[]`, `Pars.Definicja: DefinicjaLimitu`, `Pars.Okres: FromTo`. + +**Snippet — naliczenie + przeliczenie wykorzystanych:** + +```csharp +var kalend = session.GetKalend(); +var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; +var defUrlop = kalend.DefinicjeLimitow.WgNazwy["Urlop wypoczynkowy"]; +var rok = FromTo.Year(new Date(2026, 1, 1)); + +// 1) naliczenie limitu (jak KADRY-D7) +var naliczanie = new NaliczanieLimitow +{ + Pars = new NaliczanieLimitow.Params(Context.Empty.Clone(session)) + { + Definicja = defUrlop, + Okres = rok, + KopiujKorekty = true + }, + Pracownicy = new[] { pracownik } +}; +naliczanie.DodajLimit(); +session.Save(); + +// 2) przeliczenie wykorzystanych +var przelicz = new LimitNieobecnosci.Pracownicy.PrzeliczWykorzystaneWorker +{ + Pracownicy = new[] { pracownik }, + Pars = new LimitNieobecnosci.Pracownicy.PrzeliczWykorzystaneWorker.Params(Context.Empty.Clone(session)) + { + Definicja = defUrlop, + Okres = rok + } +}; +przelicz.PrzeliczWykorzystane(); +session.Save(); +``` + +**Pułapki:** +- **Nie** twórz `new LimitNieobecnosci(...)` — limit powstaje przez naliczanie (jak w KADRY-D7). +- `PrzeliczWykorzystane` aktualizuje pole `LimitNieobecnosci.Wykorzystane` na podstawie wprowadzonych + nieobecności — ma sens dopiero **po** naliczeniu limitu i wprowadzeniu nieobecności limitowanych. +- `Razem` może wynosić `0` dla pracownika bez danych napędzających wymiar — opieraj asercje na spójności + (`Wykorzystane == Razem - Pozostalo`), nie na `Razem > 0` (patrz KADRY-D7). +- Workery wykonują własne transakcje — wywołuj poza otwartą transakcją edycyjną; obsłuż wyjątki z `Save()`. + +--- + +### KADRY-D9 — Aktualizacja podstaw nieobecności ZUS / podstaw urlopu + +**Cel:** odczytać/wprowadzić ręcznie podstawy naliczania zasiłków (chorobowe/macierzyńskie/opiekuńcze/ +rehabilitacyjne) używane przy nieobecnościach ZUS — np. przy wdrożeniu lub korekcie podstawy. + +**Fakty o typie (zweryfikowane skanem DLL):** + +- Kolekcja na pracowniku: **`pracownik.PodstawyNieobecności: SubTable`** + (jest też `PodstawyNieobecnościOkresowe: SubTable`). +- **`Soneta.Place.PodstawaNieobecnosci`** — tabela `PodstawyNieobec`, `GuidedRow` **child** pracownika + (relacja przez pole `Pracownik`). +- **Brak publicznego ctora** — `PodstawaNieobecnosci` ma jedynie ctory niepubliczne + (`(RowCreator)`, `(Pracownik, TypyPodstawNieobecnosci)`). Rekordy powstają z **naliczenia wypłaty**; + w kodzie biznesowym/teście realnie testowalny jest **odczyt** (dodawanie ręczne — patrz pułapki/spec). + +**Pola i typy (`PodstawaNieobecnosci`) — bazodanowe, zapisywalne:** + +| Pole | Typ | Opis | +|---|---|---| +| `Data` | `Soneta.Types.Date` | data podstawy | +| `Miesieczne` | `decimal` | podstawa miesięczna | +| `Kwartalne` / `Roczne` | `decimal` | składowe | +| `Podstawa` | `decimal` | podstawa naliczania chorobowego | +| `PodstawaM` / `PodstawaO` / `PodstawaR` | `decimal` | podstawa macierzyńskiego / opiekuńczego / rehabilitacyjnego | +| `Typ` | `Soneta.Place.TypyPodstawNieobecnosci` | `Chorobowa` / `Wypoczynkowy` | +| `Norma` / `NormaDni` | `Time` / `int` | norma czasu/dni | +| `Praca` / `PracaDni` | `Time` / `int` | przepracowane | +| `ProcentSkladki` | `Soneta.Types.Percent` | procent składki | + +> **Podstawy urlopu wypoczynkowego** rozróżnia pole `Typ = TypyPodstawNieobecnosci.Wypoczynkowy`; +> podstawy zasiłków ZUS → `Typ = Chorobowa`. + +**Snippet — odczyt podstaw + dodanie podstawy ręcznej:** + +```csharp +var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; + +// Odczyt podstaw chorobowych (filtr serwerowy po Typ): +foreach (PodstawaNieobecnosci p in + pracownik.PodstawyNieobecności[(PodstawaNieobecnosci x) => x.Typ == TypyPodstawNieobecnosci.Chorobowa]) +{ + // p.Data, p.Podstawa, p.Miesieczne +} + +// UWAGA: PodstawaNieobecnosci NIE ma publicznego ctora — normalnie powstaje z naliczenia wypłaty. +// Ręczne dodanie wymagałoby niepublicznego API → w teście testuj wyłącznie ODCZYT (powyżej). +``` + +**Pułapki:** +- Kwoty (`Miesieczne`, `Podstawa`, …) są typu `decimal` — to dane operacyjne podstaw; **normalnie + podstawy powstają z naliczenia wypłaty** (brak publicznego ctora — patrz wyżej). +- `Pracownik` na podstawie jest read-only (guided-parent). +- Filtruj serwerowo po `Typ` (`PodstawyNieobecności[condition]`) — nie iteruj całości z `if` w pamięci. +- W teście na czystej Demo kolekcja `PodstawyNieobecności` może być pusta, dopóki nie naliczono wypłaty + z zasiłkiem — testuj odczyt asercją na model/spójność, a scenariusz „dodaj ręcznie" oznacz `[Ignore]`. + +--- + +### KADRY-D10 — Bilans otwarcia nieobecności i urlopów + +**Cel:** wprowadzić bilans otwarcia (BO) przy wdrożeniu / starcie roku — historię chorobową (okres +zasiłkowy, dni wykorzystane) oraz urlop wykorzystany u poprzednich pracodawców / w pierwszym miesiącu. + +**Fakty o typie (zweryfikowane skanem DLL):** + +- BO leży na rekordzie historycznym **`Soneta.Kadry.PracHistoria`** w dwóch subrowach (bazodanowe, + zapisywalne): + - **`PracHistoria.ChorobowyBO: Soneta.Kadry.ChorobowyBO`** (BO chorobowy / okres zasiłkowy), + - **`PracHistoria.DodatkowyBO: Soneta.Kadry.DodatkowyBO`** (BO urlopowy — urlop u poprzednich pracodawców). +- BO nieobecności pojedynczej oznacza też flaga `Nieobecnosc.BilansOtwarcia: bool` + (interfejs `IBilansOtwarcia` na `Nieobecnosc`). + +**Pola i typy (`ChorobowyBO`) — bazodanowe:** + +| Pole | Typ | Opis | +|---|---|---| +| `Data` | `Soneta.Types.Date` | data BO | +| `MiesiacPodstawy` | `Soneta.Types.YearMonth` | miesiąc podstawy | +| `Podstawa` | `decimal` | podstawa BO | +| `DniWynagrodzenia` | `int` | dni zwolnienia finansowane przez pracodawcę | +| `DniZasilkowe` | `int` | dni wliczane do bieżącego okresu zasiłkowego | +| `DniZwolnienia` | `int` | dni nieprzerwanego zwolnienia dobrowolnego | +| `ZasilekOdDnia` | `Soneta.Types.Date` | zasiłek od dnia | +| `PrzedluzenieOZ` | `bool` | okres zasiłkowy przedłużony o 3 mies. | + +**Pola i typy (`DodatkowyBO`) — bazodanowe:** + +| Pole | Typ | Opis | +|---|---|---| +| `UPoprzednich` | `decimal` | urlop wykorzystany u poprzednich pracodawców (dni) | +| `Wykorzystany` | `Soneta.Types.Time` | wykorzystany przypadający na bieżące zatrudnienie (godz.) | +| `BezPierwszego` | `bool` | prawo do urlopu w 1. mies. nabyte u poprzedniego pracodawcy | + +**Snippet — wprowadzenie BO chorobowego i urlopowego na zapisie historycznym:** + +```csharp +var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; +var historia = pracownik.Historia[Date.Today]; // właściwy zapis historyczny „na dzień" + +using (var t = session.Logout(editMode: true)) +{ + // BO chorobowy / okres zasiłkowy + historia.ChorobowyBO.DniZasilkowe = 33; + historia.ChorobowyBO.ZasilekOdDnia = new Date(2026, 1, 1); + // BO urlopowy + historia.DodatkowyBO.UPoprzednich = 10m; + t.Commit(); +} +session.Save(); +``` + +**Pułapki:** +- `ChorobowyBO`/`DodatkowyBO` to **subrowy** zapisu `PracHistoria` — nie tworzysz ich osobno, edytujesz + ich pola na istniejącym zapisie historycznym. +- **`ChorobowyBO`** (`DniZasilkowe`, `ZasilekOdDnia`, `PrzedluzenieOZ`, …) jest **zapisywalny** na zwykłym + zapisie historii (zweryfikowane testem KADRY-D10 na Demo). +- **`DodatkowyBO`** (`UPoprzednich`, `BezPierwszego`, `Wykorzystany`) na zwykłym zapisie historii Demo + rzuca **`ColReadOnlyException`** („pole w trybie tylko do odczytu") — BO urlopowy jest zapisywalny tylko + na zapisie historycznym oznaczonym jako **bilans otwarcia / start zatrudnienia**, nie na dowolnym zapisie + „na dzień". W teście na gotowych pracownikach Demo dodawanie `DodatkowyBO` oznacz `[Ignore]`. +- Pobierz właściwy zapis historyczny przez `pracownik.Historia[data]` (patrz KADRY-A14/KADRY-A15) — edycja BO na + niewłaściwym zapisie da błędne dane „na dzień". +- BO ma sens przy wdrożeniu — nie miesza się z normalnym naliczaniem; po wprowadzeniu wpływa na limity + (KADRY-D8) i okres zasiłkowy (KADRY-D6) dopiero przy przeliczeniu/naliczeniu. + +--- + +### KADRY-D11 — Wnioski o urlop / delegację + +**Cel:** zarejestrować wniosek urlopowy (lub o delegację), zmienić jego stan (akceptacja/odrzucenie/ +przywrócenie) i — docelowo — przekształcić zaakceptowany wniosek w nieobecność. + +**Fakty o typie (zweryfikowane skanem DLL):** + +- Wniosek urlopowy: **`Soneta.Kadry.WniosekUrlopowy`** — tabela `WnioskiUrlopowe`, `GuidedRow` root. + Konstruktory publiczne: **`new WniosekUrlopowy(Pracownik pracownik)`** oraz + **`new WniosekUrlopowy(Pracownik pracownik, DefinicjaNieobecnosci definicja)`**. +- Kolekcja na pracowniku: **`pracownik.WnioskiUrlopowe: SubTable`** + (oraz `WnioskiKierownika`, `WnioskiZastępcy` — te same wnioski w roli kierownika/zastępcy). +- Pola `WniosekUrlopowy` (bazodanowe, zapisywalne): `Pracownik: Pracownik`, + `Definicja: DefinicjaNieobecnosci`, `Okres: FromTo`, `Data: Date`, `DataDecyzji: Date`, + `Kierownik: Pracownik`, `Opis: MemoText`, `Stan: Soneta.Kadry.StanWnioskuUrlopowego`. + - `StanWnioskuUrlopowego`: `Oczekujący`, `Anulowany`, `Zaakceptowany`, `Odrzucony`, `Korygowana`. +- Wniosek o delegację jest subrowem wniosku: `WniosekUrlopowy.Delegacja: Soneta.Kadry.WniosekODelegację` + (`DataRozpoczeciaPlanowana`, `DataZakonczeniaPlanowana: DateShortTime`, `KrajDocelowy`, `Cel: MemoText`, + `WnioskowanaZaliczka: Currency`); samodzielny `new WniosekODelegację()` ma publiczny ctor bezparametrowy. +- **Planowane nieobecności** (osobny model, np. plan urlopów): kolekcja + **`pracownik.PlanowaneNieobecności: FromToSubTable`**; + typ `PlanowanaNieobecność` (tabela `PlanNieobecnosci`, root) z ctorem + **`new PlanowanaNieobecność(Pracownik pracownik)`**, polami `Definicja`, `Okres: FromTo`. + - **`Definicja` musi mieć zaznaczone pole `Planowana`** (`DefinicjaNieobecnosci.Planowana == true`) — + inaczej setter rzuca `RowException` „Wybrana definicja musi mieć zaznaczone pole 'Planowana'."; dobierz + definicję dynamicznie: `DefNieobecnosci.Cast().First(d => d.Planowana)`. + - **`Stan: StanPlanowanejNieobecności` jest READ-ONLY** (`Oczekująca`, `Wprowadzona`, `Korygowana`, + `Zatwierdzona`, `Anulowana`) — **nie przypisujesz** go wprost (`plan.Stan = …` → błąd kompilacji + „cannot be assigned to"); przejścia stanu wykonujesz metodami domenowymi + **`StanWprowadzona()` / `StanZatwierdzona()` / `StanAnulowana()` / `StanOczekująca()`**. +- Akceptacja/odrzucenie/przywrócenie z poziomu Pulpitu: worker (UI/Net) + **`PracownikNetWnioskiUrlopowe`** z akcjami „Zatwierdź wniosek"/`Zatwierdz`, „Odrzuć wniosek"/`Odrzuc`, + „Przywróć wniosek"/`Przywroc`. W kodzie biznesowym/teście prościej ustawiać `Stan` wprost. + +**Snippet — rejestracja wniosku urlopowego + akceptacja:** + +```csharp +var kalend = session.GetKalend(); +var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; +// UWAGA: dla definicji limitowanej (np. „Urlop wypoczynkowy") akceptacja wniosku (set Stan) wyzwoli +// przeliczenie limitu → LimitNotFoundException, jeśli limit nie został wcześniej naliczony (patrz pułapki). +// Tu używamy definicji bezlimitowej (np. „Urlop bezpłatny (art 174 kp)") albo najpierw naliczamy limit (KADRY-D8). +var defUrlop = kalend.DefNieobecnosci.WgNazwy["Urlop bezpłatny (art 174 kp)"]; + +using (var t = session.Logout(editMode: true)) +{ + var wniosek = session.AddRow(new WniosekUrlopowy(pracownik, defUrlop)); + wniosek.Okres = new FromTo(new Date(2026, 8, 3), new Date(2026, 8, 7)); + wniosek.Data = Date.Today; + wniosek.Stan = StanWnioskuUrlopowego.Oczekujący; + t.Commit(); +} +session.Save(); + +// Akceptacja (zmiana stanu): +using (var t = session.Logout(editMode: true)) +{ + var wniosek = pracownik.WnioskiUrlopowe + .Cast() + .First(w => w.Stan == StanWnioskuUrlopowego.Oczekujący); + wniosek.Stan = StanWnioskuUrlopowego.Zaakceptowany; + wniosek.DataDecyzji = Date.Today; + t.Commit(); +} +session.Save(); +``` + +**Pułapki:** +- **Akceptacja wniosku na definicji limitowanej rzuca `LimitNotFoundException`** bez wcześniej naliczonego + limitu: ustawienie `Stan` (np. `Zaakceptowany`) na wniosku z definicją „Urlop wypoczynkowy" wewnętrznie + ustawia `Okres` nieobecności i wyzwala `DefinicjaLimitu.Przelicz(...)`, który dla pracownika bez limitu + na ten dzień rzuca wyjątek. Rozwiązanie: albo nalicz limit (KADRY-D8) **przed** zmianą stanu, albo do scenariusza + obsługi samego rekordu wniosku użyj definicji **bezlimitowej** (np. „Urlop bezpłatny (art 174 kp)"). +- **Przekształcenie wniosku w nieobecność** wymaga, by nieobecność limitowana miała naliczony limit + (jak KADRY-D1) — sama akceptacja wniosku nie tworzy automatycznie rozliczonej nieobecności w teście bez + naliczonego limitu/wypłaty. +- `WniosekODelegację` to subrow wniosku (`WniosekUrlopowy.Delegacja`) — wnioskowanie o delegację + ustawiasz na tym subrowie; pełne rozliczenie delegacji to moduł `Soneta.Delegacje` (osobny dokument + handlowy PWS), poza zakresem wniosku. +- Filtruj kolekcję wniosków przez `WnioskiUrlopowe[condition]` lub iteruj w zakresie jednego pracownika; + nie skanuj globalnej tabeli `WnioskiUrlopowe` bez zakresu (tabela operacyjna guided). +- Stan zmieniaj świadomie wg enuma `StanWnioskuUrlopowego` — workery Net robią to samo z dodatkową + logiką workflow (powiadomienia), której w teście jednostkowym nie odtworzysz. + +--- + +### KADRY-D12 — Praca zdalna (wnioski, lokalizacje, ewidencja) + +**Cel:** skonfigurować pracę zdalną pracownika (model pracy, limit pracy zdalnej okazjonalnej), +zarejestrować wniosek o pracę zdalną i lokalizacje jej świadczenia oraz odczytać ewidencję. + +**Fakty o typie (zweryfikowane skanem DLL):** + +- Parametry pracy zdalnej leżą na etacie/historii: **`PracHistoria.PracaZdalna: Soneta.Kadry.PracZdalna`** + (subrow, bazodanowe, zapisywalne): + - `ModelPracy: Soneta.Kadry.ModelPracy` (`NieDotyczy`, `PracaStacjonarna`, `PracaHybrydowa`, `PracaZdalna`), + - `OswiadczenieWarunki: bool` (warunki lokalowe/techniczne), + - `LimitPZ: int`, `IndywidualnyLimitPZ: bool`, `TypLimituPZ: TypLimituPracyZdalnej` + (`Roczny`, `Miesieczny`, `Tygodniowy`, `Kwartalny`, `Półroczny`). +- Lokalizacje pracy zdalnej: **`pracownik.LokalizacjePracyZdalnej: SubTable`** + (tabela `LokPracZdalnej`). +- Wnioski o pracę zdalną: **`pracownik.WnioskiPracyZdalnej: SubTable`** + (oraz `WnioskiPracyZdalnejKierownika`); typ `WniosekPracyZdalnej` ma ctor + `(Pracownik, DefinicjaRodzajuPracyZdalnej)` — **ctory są niepubliczne**, więc tworzenie wniosku idzie + przez worker (`GrupoweZleceniePracyZdalnejWorker`) lub Pulpit, nie wprost `new`. +- Lokalizacja pracy zdalnej: `Soneta.Kadry.LokalizacjaPracyZdalnej` ma **publiczny ctor + `new LokalizacjaPracyZdalnej(Pracownik pracownik)`**. +- Ewidencja/odczyt limitu pracy zdalnej okazjonalnej: worker + **`Soneta.Kadry.Pracownik.PracaZdalnaWorker`** — property odczytowe (bez akcji modyfikującej): + `DniPracyZdalnejRazem: int`, `DniPracyZdalnejOkazjonalnej: int`, `DniPracyZdalnejOkazjonalnejLimit: int`, + `CzasPracyZdalnejRazem: Time`, `LimitPracaZdalnaOkazjonalna: int`, `PozostaloPracaZdalnaOkazjonalna: int`; + kontekst: `Pracownik: Pracownik`, `Okres: FromTo`. +- Grupowe zlecenie pracy zdalnej (Pulpit/seryjne): worker + **`Soneta.Kadry.UI.KadryNet.Workers.GrupoweZleceniePracyZdalnejWorker`** (akcja + „Dodaj wnioski zlecenia pracy zdalnej"/`DodajZleceniaPracyZdalnej`): `Pracownicy: Pracownik[]`, + `Pars.Okres: FromTo`, `Pars.Data: Date`, `Pars.Uwagi: string`. +- Aktualizacja podzielników kosztów na podstawie pracy hybrydowej: worker + **`AktualizujPodzielnikowPracaZdalnaWorker`** (`DefinicjaPodzielnika`, `Okres: YearMonth`, …). + +**Snippet — ustawienie modelu pracy zdalnej + lokalizacja + wniosek:** + +```csharp +var kadry = session.GetKadry(); +var pracownik = kadry.Pracownicy.WgKodu["006"]; + +using (var t = session.Logout(editMode: true)) +{ + var historia = pracownik.Historia[Date.Today]; + historia.PracaZdalna.ModelPracy = ModelPracy.PracaHybrydowa; + historia.PracaZdalna.OswiadczenieWarunki = true; + + // lokalizacja pracy zdalnej (np. adres domowy) + var lok = session.AddRow(new LokalizacjaPracyZdalnej(pracownik)); + // … pola adresowe lokalizacji wg LokalizacjaPracyZdalnej + t.Commit(); +} +session.Save(); + +// Odczyt ewidencji pracy zdalnej okazjonalnej (worker odczytowy): +// Pracownik i Okres są zwykłymi, settowalnymi property (nie trzeba przekazywać przez Context): +var pz = new Soneta.Kadry.Pracownik.PracaZdalnaWorker +{ + Pracownik = pracownik, + Okres = FromTo.Year(new Date(2026, 1, 1)) +}; +// odczyt: pz.DniPracyZdalnejRazem, pz.LimitPracaZdalnaOkazjonalna, pz.PozostaloPracaZdalnaOkazjonalna +``` + +**Pułapki:** +- `PracaZdalnaWorker` to worker **odczytowy** (ma property, brak akcji modyfikującej) — służy do + prezentacji ewidencji/limitu, nie do zapisu. +- `ModelPracy`/`OswiadczenieWarunki` są na **historycznym** zapisie etatu (`PracHistoria.PracaZdalna`) — + edytuj właściwy zapis „na dzień". +- `WniosekPracyZdalnej` ma **niepubliczne ctory** — w teście jednostkowym nie utworzysz go przez `new`; + zlecenie pracy zdalnej idzie przez worker `GrupoweZleceniePracyZdalnejWorker` (czynność Net/UI, + wymaga `Context`). Testuj raczej `ModelPracy`/`OswiadczenieWarunki` na `PracHistoria.PracaZdalna` + i `LokalizacjaPracyZdalnej` (ma publiczny ctor). +- `LokalizacjaPracyZdalnej` ma publiczny ctor `(Pracownik)` — testowalna wprost. + diff --git a/soneta-programming/references/domeny/kadry/KADRY05-plan-pracy.md b/soneta-programming/references/domeny/kadry/KADRY05-plan-pracy.md new file mode 100644 index 0000000..9ed5cb2 --- /dev/null +++ b/soneta-programming/references/domeny/kadry/KADRY05-plan-pracy.md @@ -0,0 +1,348 @@ +# KADRY05 — Plan pracy i kalendarz + +> Wspólne fakty o typie, podstawowe typy i szablon wzorca: [../kadry.md](../kadry.md). + +> **Model kalendarza pracownika.** Każdy `Pracownik` ma kalendarz roboczy +> (`pracownik.Etat.Kalendarz : Soneta.Kalend.Kalendarz`), którego dni leżą w tabeli +> `DniKalendarza` (`DzienKalendarzaBase`, child kalendarza). Pracownik wystawia trzy +> niezależne kolekcje dni typu `DateSubTable` (indeksator po dacie `[Date]`, **tylko do +> odczytu** — element tworzysz konstruktorem + `AddRow`): +> - `pracownik.DniPlanu : DateSubTable` — **plan/harmonogram** (dni `DzienPlanu : DzienKalendarzaBase`); to `pracownik.Etat.Kalendarz.Dni`. +> - `pracownik.DniPracy : DateSubTable` — **ewidencja** (realizacja) czasu pracy. +> - `pracownik.DniRCP : DateSubTable` — **zarejestrowany** czas pracy (RCP) — patrz sekcja F. +> +> Wszystkie dni współdzielą subrow `Praca : Soneta.Kalend.CzasPracy` z polami +> `OdGodziny`/`DoGodziny`/`Czas : Soneta.Types.Time`. Definicja dnia (`Definicja : +> Soneta.Kalend.DefinicjaDnia`) to rekord **konfiguracyjny** (słownik `DefinicjeDni`, +> indeksator `[Kod]`). +> +> **Ograniczenie wykonalności.** Plan i ewidencja są normalnie wyliczane przez kalkulator +> czasu pracy z definicji kalendarza/serii — ręczne tworzenie pojedynczego dnia jest możliwe +> publicznym kontraktem (ctor `(Pracownik, Date)` + `AddRow`), ale **wymaga zdefiniowanego +> `DefinicjaDnia` w konfiguracji**. Operacje masowe (przeliczenie planu na okres) są zaszyte +> w workerach/kalkulatorach UI — patrz KADRY-E2. + +### KADRY-E1 — Wprowadzanie planowanego czasu pracy (★) + +**Cel:** odczytać lub ustawić plan pracy (harmonogram) pracownika na konkretny dzień — +godziny od–do, normę dobową oraz typ dnia. + +**Pola i typy:** + +| Element | Lokalizacja | Typ | Uwaga | +|---|---|---|---| +| Plan pracy (cała kolekcja) | `pracownik.DniPlanu` | `Soneta.Business.DateSubTable` | == `pracownik.Etat.Kalendarz.Dni`; indeksator `[Date]` (get) | +| Dzień planu | `pracownik.DniPlanu[data]` | `Soneta.Kalend.DzienPlanu` (`DzienKalendarzaBase`) | `null`, gdy dla daty brak dnia planu | +| Data dnia | `DzienPlanu.Data` | `Soneta.Types.Date` | bazodanowe; ustawiane przez ctor | +| Godziny pracy (subrow) | `DzienPlanu.Praca` | `Soneta.Kalend.CzasPracy` | `Praca.OdGodziny`, `Praca.DoGodziny`, `Praca.Czas : Time` (zapisywalne) | +| Czas (norma dnia, odczyt) | `DzienPlanu.Czas` | `Soneta.Types.Time` | kalkulowane (czas pracy dnia) | +| Od (odczyt) | `DzienPlanu.OdGodziny` | `Soneta.Types.Time` | kalkulowane | +| Definicja dnia | `DzienPlanu.Definicja` | `Soneta.Kalend.DefinicjaDnia` | rekord słownika konfiguracyjnego `DefinicjeDni` | +| Tolerancja wejścia | `DzienPlanu.TolerancjaWe` | `Soneta.Types.Time` | bazodanowe | +| Norma dobowa kalendarza | `pracownik.Etat.Kalendarz.NormaDobowa` | `Soneta.Types.Time` | poziom kalendarza, nie dnia | +| Słownik definicji dni | `session.GetKalend().DefinicjeDni` | `DefinicjeDni` | indeksator `[kod: string]`; skróty `WolnaSobota`, `Niedziela` | + +**Snippet:** + +```csharp +var kalend = session.GetKalend(); +var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; + +// --- Odczyt planu na dzień (bezpiecznie: indeksator zwraca null dla braku dnia) --- +var data = new Date(2026, 6, 1); +var dzienPlanu = (DzienPlanu)pracownik.DniPlanu[data]; +if (dzienPlanu is not null) +{ + Time odGodz = dzienPlanu.Praca.OdGodziny; // np. 8:00 + Time doGodz = dzienPlanu.Praca.DoGodziny; // np. 16:00 + Time normaDnia = dzienPlanu.Czas; // wyliczona norma dnia (kalkulowane) + DefinicjaDnia typDnia = dzienPlanu.Definicja; +} + +// --- Ustawienie/utworzenie dnia planu (wymaga DefinicjaDnia z konfiguracji) --- +using (var t = session.Logout(editMode: true)) +{ + var dp = (DzienPlanu)pracownik.DniPlanu[data]; + if (dp is null) + { + dp = session.AddRow(new DzienPlanu(pracownik, data)); // ctor (Pracownik, Date) + dp.Definicja = kalend.DefinicjeDni["RB"]; // typ dnia ze słownika (np. dzień roboczy) + } + dp.Praca.OdGodziny = new Time(8, 0); + dp.Praca.DoGodziny = new Time(16, 0); // Czas dnia wylicza się z od–do + + t.Commit(); +} +session.Save(); +``` + +**Pułapki:** +- `DniPlanu` to `DateSubTable` **nietypowany** (zwraca `Row`) — rzutuj na `DzienPlanu`. Indeksator + `[Date]` jest **tylko do odczytu**: nowego dnia nie „przypiszesz", tworzysz go ctorem + `new DzienPlanu(pracownik, data)` + `session.AddRow(...)`. +- Godziny ustawiasz na **subrowie** `Praca` (`dp.Praca.OdGodziny = …`), nie na `dp.OdGodziny` — + to ostatnie jest kalkulowane (read-only). Po ustawieniu od–do `Praca.Czas`/`Czas` przeliczają się. +- `Definicja` to rekord **konfiguracyjnego** słownika `DefinicjeDni` — pobierz istniejący wpis + (`kalend.DefinicjeDni[kod]`), nie twórz „w locie". Bez przypisanego `Definicja` świeży dzień planu + może nie przejść weryfikatorów. +- Plan jest zwykle generowany przez kalkulator z definicji kalendarza (serie dni, święta) — + ręczne nadpisywanie pojedynczego dnia to korekta, nie sposób budowy całego harmonogramu (do tego + służy operacja seryjna / kopiowanie planu, KADRY-E2). +- Norma dobowa to atrybut **kalendarza** (`Etat.Kalendarz.NormaDobowa`), nie pojedynczego dnia. + +### KADRY-E2 — Planowanie czasu pracy grupy (kopiowanie planu) (★) + +**Cel:** skopiować wyliczony plan pracy (harmonogram) na wskazany okres — dla jednego pracownika +albo dla grupy, oraz seryjnie zaktualizować kalendarz pracowników (zmiana kalendarza docelowego). + +**Publiczny kontrakt — dwie drogi:** + +| Operacja | API | Charakter | +|---|---|---| +| Kopiowanie **planu** pracownika na okres | `Soneta.Kalend.KalendarzPlanuKopia.Kopiuj(Pracownik pracownik, FromTo okres)` (**public static**) | bez UI — proste API | +| Kopiowanie **pracy/realizacji** na okres | `Soneta.Kalend.KalendarzPracyKopia.Kopiuj(Pracownik pracownik, FromTo okres)` (**public static**) | bez UI — proste API | +| Kopiowanie grupy (worker UI) | `KalendarzPlanuKopia.KopiujWorker` / `KalendarzPracyKopia.KopiujWorker` | wymaga `Context` z zaznaczeniem | +| Aktualizacja kalendarza grupy | `Soneta.Kadry.AktualizujKalendarzWorker` | wymaga `Params` z `Context` | + +**Worker `KopiujWorker` (BI/„Kopiuj plan…", „Kopiuj pracę…"):** klasa `ContextBase` z ctorem +`(Context context)`; pola `[Context] FromTo Okres`, `[Context] Pracownik[] Pracownicy`; metoda +`void Kopiuj()`. Działa **wyłącznie** z kontekstem UI (zaznaczona lista pracowników) i jest gardzona +licencją BI/BI_PL/PL oraz `IsVisibleKopiuj` (niedostępny na mobile). + +**Worker `AktualizujKalendarzWorker`:** pola `[Context] Pracownik[] Pracownicy`, +`Params Pars` (`Pars.Data`, `Pars.TylkoOstatni: bool`, `Pars.PowodAktualizacji: string`, +`Pars.Kalendarze: KalendarzBase[]`, `Pars.Docelowy: Kalendarz`, `Pars.Zmiana: bool`, +`Pars.Interpretacja`), metoda `void Aktualizuj()`. `Params` to `ContextBase` (ctor `(Context)`). + +**Snippet (proste API dla jednego pracownika — bez UI):** + +```csharp +var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; +var okres = new FromTo(new Date(2026, 6, 1), new Date(2026, 6, 30)); + +using (var t = session.Logout(editMode: true)) +{ + // Wylicza plan z kalendarza i zapisuje do kopii planu pracownika za wskazany okres: + KalendarzPlanuKopia.Kopiuj(pracownik, okres); // public static + // analogicznie realizacja: KalendarzPracyKopia.Kopiuj(pracownik, okres); + t.Commit(); +} +session.Save(); +``` + +**Snippet (grupa — przez worker; wymaga Context z zaznaczeniem):** + +```csharp +// Tylko w warstwie UI/Czynności — Context dostarcza zaznaczonych pracowników. +var worker = new KalendarzPlanuKopia.KopiujWorker(context) +{ + Okres = new FromTo(new Date(2026, 6, 1), new Date(2026, 6, 30)), + Pracownicy = context.Get() +}; +worker.Kopiuj(); // wewnątrz: Session.Logout + Commit +``` + +**Pułapki:** +- **Kopiowanie grupy nie ma „czystego" API bezkontekstowego** — `KopiujWorker` i + `AktualizujKalendarzWorker.Params` dziedziczą po `ContextBase` i wymagają `Context` (zaznaczenie z + listy UI). Dla kodu serwerowego/testów używaj **publicznej statycznej** `KalendarzPlanuKopia.Kopiuj(pracownik, okres)` + w pętli po pracownikach — to ona realizuje właściwą logikę (worker w `KopiujInt` woła ją per pracownik). +- `KopiujWorker.Kopiuj()` jest gardzony licencją (BI/BI_PL/PL) i `IsVisibleKopiuj` (m.in. blokada na + mobile) — to logika UI, nie wywołuj jej z kodu biznesowego. +- Kopia planu/pracy trafia do **osobnych** kolekcji `pracownik.DniPlanuKopia`/`pracownik.DniPracyKopia` + (`DateSubTable`), powiązanych z `KalendarzPlanuKopia`/`KalendarzPracyKopia` — to bufor kopii, odrębny + od właściwego `DniPlanu`/`DniPracy`. +- `okres` jest normalizowany przez setter workera do pełnych miesięcy (otwarty `From`/`To` → + pierwszy/ostatni dzień miesiąca); przy statycznym `Kopiuj` podawaj zamknięty `FromTo`. +- Operacja seryjna na grupie pracowników = długa transakcja → dziel na paczki, trzymaj transakcje + krótkie (safe-code §13.1). + +### KADRY-E3 — Aktualizacja kalendarza pracownika (operacja seryjna „Zaktualizuj kalendarz pracownika") + +**Cel:** seryjnie zmienić kalendarz roboczy zaznaczonych pracowników (zmiana kalendarza +docelowego, przeliczenie planu na nowy kalendarz od wskazanej daty) — operacja z menu +„Czynności" na liście pracowników. + +**Publiczny kontrakt — worker `Soneta.Kadry.AktualizujKalendarzWorker`:** + +| Element | Sygnatura / typ | Uwaga | +|---|---|---| +| Konstruktor | `new AktualizujKalendarzWorker()` | bezparametrowy; worker UI | +| Pracownicy (wejście) | `Pracownicy : Pracownik[]` | **set-only**; karmione z `Context` (zaznaczenie listy) | +| Parametry | `Pars : Params` | **set-only**; `Params` to `ContextBase`, ctor `(Context context)` | +| Wykonanie | `void Aktualizuj()` | właściwa operacja seryjna (Logout + Commit wewnątrz) | + +**`Soneta.Kadry.AktualizujKalendarzWorker.Params` (`: ContextBase`, ctor `(Context)`):** + +| Pole | Typ | Uwaga | +|---|---|---| +| `Data` | `Soneta.Types.Date` | data, od której obowiązuje nowy kalendarz | +| `TylkoOstatni` | `bool` | aktualizuj tylko ostatni (bieżący) zapis historyczny | +| `PowodAktualizacji` | `string` | opis powodu (do dokumentu aktualizacji) | +| `Kalendarze` | `KalendarzBase[]` | kalendarze źródłowe objęte zmianą; lista przez `GetListKalendarze()` | +| `Docelowy` | `Soneta.Kalend.Kalendarz` | kalendarz docelowy; lista przez `GetListDocelowy()` | +| `Zmiana` | `bool` | flaga: czy zmienić kalendarz (a nie tylko przeliczyć) | +| `Interpretacja` | `Soneta.Kadry.InterpretacjaKalendarza` | `WgPlanu` / `WgObecnosci` / `WgZestawien`; `IsReadOnlyInterpretacja()` | + +**Snippet (warstwa UI/Czynności — wymaga `Context` z zaznaczeniem):** + +```csharp +// Tylko w warstwie UI: Context dostarcza zaznaczonych pracowników. +var worker = new AktualizujKalendarzWorker +{ + Pracownicy = context.Get(), + Pars = new AktualizujKalendarzWorker.Params(context) + { + Data = new Date(2026, 7, 1), + Docelowy = session.GetKalend().Kalendarze.WgKodu["PODSTAWOWY"], + Zmiana = true, + Interpretacja = InterpretacjaKalendarza.WgPlanu, + PowodAktualizacji = "Zmiana systemu czasu pracy" + } +}; +worker.Aktualizuj(); // wewnątrz: Session.Logout + Commit +``` + +**Pułapki:** +- `Params` dziedziczy po `ContextBase` (ctor `(Context)`) — **nie da się go zbudować bez `Context`**. + Dlatego KADRY-E3 nie ma „czystego" API bezkontekstowego; to operacja UI/serwerowa z zaznaczeniem. +- `Pracownicy` i `Pars` są **set-only** — nie odczytasz ich z powrotem; ustaw przed `Aktualizuj()`. +- Operacja seryjna = długa transakcja na wielu pracownikach → w realnym użyciu dziel na paczki + (safe-code §13.1). Sam worker zarządza transakcją wewnętrznie. +- Zmiana kalendarza jest **historyczna** (operuje na zapisach `Etat`) — `TylkoOstatni`/`Data` + decydują, których zapisów historycznych dotyczy. + +--- + +### KADRY-E4 — Uzgodnienie doby pracowniczej (model doby; godziny rozpoczęcia doby) + +**Cel:** przesunąć granicę doby pracowniczej dla dnia ewidencji — gdy zmiana zaczyna się w jednej +dobie kalendarzowej, a kończy w następnej (nocna), uzgodnienie „przenosi" początek/koniec pracy do +właściwej doby pracowniczej. Operacja na pojedynczym dniu (`DzienPracy`) lub seryjnie na grupie. + +**Model doby (publiczny kontrakt):** + +| Element | Lokalizacja | Typ | Uwaga | +|---|---|---|---| +| Początek doby w niedziele/święta | `pracownik.Last.Etat.ConfigPoczątekDobyNiedzieledIŚwięta` | `Soneta.Types.Time` | **read-only** (konfiguracyjne); godzina startu doby | +| Norma dobowa | `pracownik.Last.Etat.NormaDobowa` | `Soneta.Types.Time` | bazodanowe; norma czasu doby | +| Norma dobowa kalendarza | `pracownik.Last.Etat.Kalendarz.NormaDobowa` | `Soneta.Types.Time` | poziom kalendarza | +| Interpretacja kalendarza | `pracownik.Last.Etat.InterpretacjaKalendarza` | `Soneta.Kadry.InterpretacjaKalendarza` | `WgPlanu`/`WgObecnosci`/`WgZestawien` — jak interpretować dobę | + +> **Uwaga:** `Etat` leży na bieżącym **zapisie historycznym** (`pracownik.Last.Etat : Soneta.Kadry.Etat`, +> gdzie `Last : PracHistoria`) — nie ma property `pracownik.Etat` bezpośrednio na roocie pracownika. +| Godziny pracy dnia | `DzienPracy.Praca` | `Soneta.Kalend.CzasPracy` | `OdGodziny`/`DoGodziny`/`Czas` — granice realizacji w dobie | + +**Worker pojedynczego dnia — `Soneta.Kalend.DzienPracy.UzgodnijDobePracowniczaWorker`:** + +| Element | Sygnatura | Uwaga | +|---|---|---| +| Konstruktor | `new DzienPracy.UzgodnijDobePracowniczaWorker()` | | +| Dzień (wejście) | `Dzień : DzienPracy` | **set-only** | +| Warunek dostępności | `static bool IsEnabledUzgodnijDobePracownicza(DzienPracy dzień)` | czy operacja ma sens dla dnia | +| Uzgodnienie | `object UzgodnijDobePracownicza()` | przelicza dobę | +| Przeniesienie początku | `DzienPracy PrzenieśPoczątek()` | przenosi początek pracy do poprz. doby | +| Przeniesienie końca | `DzienPracy PrzenieśKoniec()` | przenosi koniec pracy do nast. doby | +| Dokument aktualizacji | `DokumentAktualizacjiKalendarza : IDokumentAktualizacjiKalendarza`, `DataAktualizacji : System.DateTime` | kontekst historii | + +**Worker seryjny (grupa) — `Soneta.Kadry.UzgodnijDobePracowniczaPracownikowWorker`:** + +| Element | Sygnatura / typ | Uwaga | +|---|---|---| +| Konstruktor | `new UzgodnijDobePracowniczaPracownikowWorker()` | | +| Pracownicy | `Pracownicy : Pracownik[]` | **set-only**; z `Context` | +| Parametry | `Pars : Params` (`ContextBase`, ctor `(Context)`); pole `Okres : FromTo` | **set-only** | +| Wykonanie | `UzgodnijDobePracowniczaResult UzgodnijDobePracownicza()` | zwraca wynik | + +**Snippet (pojedynczy dzień):** + +```csharp +var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; +var dzien = pracownik.DniPracy[new Date(2026, 6, 1)]; // DzienPracy lub null +if (dzien is not null && DzienPracy.UzgodnijDobePracowniczaWorker.IsEnabledUzgodnijDobePracownicza(dzien)) +{ + using (var t = session.Logout(editMode: true)) + { + var worker = new DzienPracy.UzgodnijDobePracowniczaWorker { Dzień = dzien }; + worker.UzgodnijDobePracownicza(); + t.Commit(); + } + session.Save(); +} +``` + +**Pułapki:** +- Godzina rozpoczęcia doby to atrybut **konfiguracyjny `Etat`** (`ConfigPoczątekDobyNiedzieledIŚwięta`, + read-only) i normy `Etat.NormaDobowa`/`Etat.Kalendarz.NormaDobowa` — nie ma osobnego, edytowalnego + pola „początek doby" na pojedynczym `DzienPracy`. +- `Dzień` workera pojedynczego jest **set-only**; `Pracownicy`/`Pars` workera grupowego również. +- Worker grupowy `Params` to `ContextBase` (ctor `(Context)`) — **wymaga `Context`** (zaznaczenie UI), + brak czystego API bezkontekstowego. +- Uzgodnienie modyfikuje `DzienPracy.Praca` (od–do) i może rozbić pracę na dwie doby — wykonuj w + transakcji (`Logout(editMode:true)` + `Commit`) i zapisz `Save()`. + +--- + +### KADRY-E5 — Odczyt normy czasu pracy i czasu przepracowanego za okres (★ testowalne) + +**Cel:** dla pracownika odczytać za zadany okres (`FromTo`/`YearMonth`): normę czasu pracy +(planowaną), czas przepracowany (zrealizowany), nadgodziny, czas nocny, liczbę/normę nieobecności — +bez modyfikacji danych (czysty odczyt statystyk). + +**Punkt wejścia — `pracownik.Czasy : Soneta.Kalend.KalkulatorPracownika`:** + +| Metoda (publiczna, instancyjna) | Zwraca | Znaczenie | +|---|---|---| +| `Norma(FromTo okres, params Item[] condition)` | `CzasDni` | norma (planowana) czasu pracy za okres | +| `Norma(FromTo okres, DefinicjaStrefy def, params Item[] condition)` | `CzasDni` | norma w obrębie strefy | +| `NormaKodeksowa(YearMonth miesiąc)` | `CzasDni` | norma kodeksowa miesiąca (pełny etat) | +| `NormaKodeksowaWym(Fraction wymiar, Time normaDobowa, YearMonth miesiąc)` | `CzasDni` | norma kodeksowa wg wymiaru etatu | +| `Praca(FromTo okres, params Item[] condition)` | `CzasDni` | czas **przepracowany** (zrealizowany) za okres | +| `Praca(FromTo okres, DefinicjaStrefy def, params Item[] condition)` | `CzasDni` | przepracowany w obrębie strefy | +| `PracaRozliczana(FromTo okres, params Item[] condition)` | `CzasDni` | czas pracy rozliczany (do nadgodzin) | +| `PracaZatr(FromTo okres, bool usprPłatne)` | `CzasDni` | praca w okresie zatrudnienia | +| `Nadgodziny(YearMonth okres)` / `Nadgodziny(FromTo okres)` | `ZestawienieNadgodzin` | nadgodziny | +| `NadgodzinyDobaOkres(FromTo okres)` | `ZestawienieNadgodzin` | nadgodziny dobowe/okresowe | +| `Nocne(YearMonth\|FromTo okres)` | `Time` | czas nocny | +| `NormaNie(YearMonth\|FromTo okres, params Item[] condition)` | `CzasDni` | norma nieobecności | +| `DniNie(YearMonth\|FromTo okres, params Item[] condition)` | `int` | liczba dni nieobecności | +| `Nieobecność(Date data[, bool clip])` | `INieobecnosc` | nieobecność w danym dniu | + +**`Soneta.Kalend.CzasDni` (typ wyniku):** + +| Pole | Typ | Uwaga | +|---|---|---| +| `Czas` | `Soneta.Types.Time` | sumaryczny czas (read-only) | +| `Dni` | `int` | liczba dni (read-only) | +| `CzasDni.Empty`, `CzasDni.Invalid` | `CzasDni` | wartości specjalne; operatory `+`/`-`/`==` | + +**`Soneta.Kalend.ZestawienieNadgodzin` (struct):** `N50`, `N100`, `NSW`, `N100Doba`, `N100Okres`, +`Razem` — wszystkie `Time` (read-only); `ZestawienieNadgodzin.Zero`. + +**Snippet (czysty odczyt):** + +```csharp +var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; +var kalk = pracownik.Czasy; // KalkulatorPracownika +var okres = new FromTo(new Date(2026, 6, 1), new Date(2026, 6, 30)); + +CzasDni norma = kalk.Norma(okres); // norma planowana +CzasDni przepracowano = kalk.Praca(okres); // czas zrealizowany +ZestawienieNadgodzin nadg = kalk.Nadgodziny(new YearMonth(2026, 6)); +Time nocne = kalk.Nocne(okres); + +Time normaCzas = norma.Czas; int normaDni = norma.Dni; +Time pracaCzas = przepracowano.Czas; Time nadgRazem = nadg.Razem; +``` + +**Pułapki:** +- `KalkulatorPracownika` **nie jest `Row`** — to obiekt liczący (zwykły `object`). Nie zapisuje się, + nie wymaga transakcji; to czysty odczyt. Pobieraj go zawsze przez `pracownik.Czasy` (ma kontekst + pracownika), nie twórz ręcznie ctorem chyba że masz `Pracownik` + ewentualny `Log`. +- Parametr `condition` to **serwerowy filtr** (`Item[]`, RowCondition) — można zawęzić np. do strefy; + zwykle pusty. +- `Norma` = plan, `Praca` = realizacja; nie myl `Praca(okres)` (statystyka) z `DzienPracy` (rekord dnia). +- Wynik `CzasDni.Invalid` sygnalizuje brak danych/błąd okresu — sprawdzaj zanim policzysz różnice. + +--- + diff --git a/soneta-programming/references/domeny/kadry/KADRY06-rcp.md b/soneta-programming/references/domeny/kadry/KADRY06-rcp.md new file mode 100644 index 0000000..03449b9 --- /dev/null +++ b/soneta-programming/references/domeny/kadry/KADRY06-rcp.md @@ -0,0 +1,321 @@ +# KADRY06 — RCP — rejestracja czasu pracy + +> Wspólne fakty o typie, podstawowe typy i szablon wzorca: [../kadry.md](../kadry.md). + +> **Dwie tabele.** Zarejestrowany (surowy) czas pracy z czytników RCP leży w +> `pracownik.DniRCP : DateSubTable` (tabela `DniRCP`). Pojedyncze zdarzenia +> wejścia/wyjścia (`Soneta.Kalend.WejscieWyjscie`, tabela `WejsciaWyjscia`) są **childem `DzienPracy`** +> (pole `WejscieWyjscie.Dzien : DzienPracy`, kolekcja `dzienPracy.WeWy`), a `DzienPracy` to +> ewidencja w `pracownik.DniPracy`. `DzienRCP` jest stanem zweryfikowanym RCP (z polem +> `StanRCP : StanWeryfikacjiRCP`), powstaje z importu/przeliczenia. + +### KADRY-F1 — Rejestracja czasu pracy pracownika (★) + +**Cel:** odczytać zarejestrowany/zewidencjonowany czas pracy pracownika za dzień oraz (gdy trzeba) +utworzyć dzień ewidencji. + +**Pola i typy:** + +| Element | Lokalizacja | Typ | Uwaga | +|---|---|---|---| +| Ewidencja (kolekcja) | `pracownik.DniPracy` | `DateSubTable` | indeksator `[Date]` (get); element ctorem | +| Dzień ewidencji | `pracownik.DniPracy[data]` | `Soneta.Kalend.DzienPracy` | `null` przy braku | +| RCP zweryfikowane (kolekcja) | `pracownik.DniRCP` | `DateSubTable` | analogicznie | +| Dzień RCP | `pracownik.DniRCP[data]` | `Soneta.Kalend.DzienRCP` | `null` przy braku | +| Przepracowany czas (subrow) | `DzienPracy.Praca` / `DzienRCP.Praca` | `Soneta.Kalend.CzasPracy` | `Praca.OdGodziny`, `Praca.DoGodziny`, `Praca.Czas : Time` | +| Czas/Od (odczyt) | `Dzien*.Czas`, `Dzien*.OdGodziny` | `Soneta.Types.Time` | kalkulowane | +| Stan weryfikacji RCP | `DzienRCP.StanRCP` | `Soneta.Kalend.StanWeryfikacjiRCP` | zapisywalne | +| Flaga importu RCP | `Dzien*.RcpOK` | `bool` | zapisywalne; stan rekordu po imporcie | +| Zdarzenia we/wy dnia | `DzienPracy.WeWy` | `LpSubTable` | patrz KADRY-F2 | +| Uwagi / błędy (RCP) | `DzienRCP.Uwagi`, `DzienRCP.Bledy` | `Soneta.Business.MemoText` | zapisywalne | + +**Snippet:** + +```csharp +var kalend = session.GetKadry().Session.GetKalend(); +var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; +var data = new Date(2026, 6, 1); + +// --- Odczyt zaewidencjonowanego czasu (ewidencja) --- +var dzienPracy = pracownik.DniPracy[data]; // typowane: DzienPracy lub null +if (dzienPracy is not null) +{ + Time przepracowano = dzienPracy.Praca.Czas; // suma czasu pracy dnia + Time od = dzienPracy.Praca.OdGodziny; + Time @do = dzienPracy.Praca.DoGodziny; +} + +// --- Odczyt stanu RCP (zweryfikowany rejestr) --- +var dzienRcp = pracownik.DniRCP[data]; // DzienRCP lub null +if (dzienRcp is not null) +{ + Time czasRcp = dzienRcp.Praca.Czas; + StanWeryfikacjiRCP stan = dzienRcp.StanRCP; +} + +// --- Utworzenie dnia ewidencji (gdy potrzebny ręczny wpis) --- +using (var t = session.Logout(editMode: true)) +{ + var dp = pracownik.DniPracy[data]; + if (dp is null) + { + dp = session.AddRow(new DzienPracy(pracownik, data)); // ctor (Pracownik, Date) + kalend.DniPracy.AddRow(dp); // alternatywnie przez Module.DniPracy + } + dp.Praca.OdGodziny = new Time(8, 0); + dp.Praca.DoGodziny = new Time(16, 0); + t.Commit(); +} +session.Save(); +``` + +**Pułapki:** +- `DniPracy`/`DniRCP` są **typowane** (`DateSubTable` / ``) — indeksator + `[Date]` zwraca od razu właściwy typ lub `null`. Nie iteruj całej kolekcji, by „znaleźć" dzień — + użyj indeksatora po dacie albo `[FromTo]` dla zakresu. +- Czas pracy ustawiaj na subrowie `Praca` (od–do); `Dzien*.Czas`/`Dzien*.OdGodziny` na rootcie dnia są + kalkulowane (read-only). +- `DzienRCP` to **wynik weryfikacji** importu RCP (z czytników) — w normalnym przepływie nie tworzysz + go ręcznie, lecz odczytujesz po imporcie/przeliczeniu. `DzienPracy` (ewidencja) to właściwe miejsce + na ręczny wpis. +- Świeży `DzienPracy` z `new DzienPracy(pracownik, data)` trzeba dodać do tabeli + (`Module.DniPracy.AddRow(...)` lub `session.AddRow(...)`) — sam ctor go nie rejestruje. + +### KADRY-F2 — Rejestracja wejścia/wyjścia (RCP) (★) + +**Cel:** dodać zdarzenie wejścia/wyjścia do dnia oraz odczytać listę zdarzeń RCP danego dnia. + +**Pola i typy (`Soneta.Kalend.WejscieWyjscie`, tabela `WejsciaWyjscia`):** + +| Pole | Typ | Uwaga | +|---|---|---| +| `Dzien` | `Soneta.Kalend.DzienPracy` | właściciel (guided-parent); ustawiany przez ctor `(DzienPracy)` | +| `Godzina` | `Soneta.Types.Time` | godzina zdarzenia (zapisywalne) | +| `Typ` | `Soneta.Kalend.TypWejsciaWyjscia` | enum: `Niezdefiniowany`, `Wejscie`, `Wyjscie`, `WejscieSluzbowe`, `WyjscieSluzbowe`, `WejsciePrywatne`, `WyjsciePrywatne` | +| `Operacja` | `int` | kod operacji urządzenia (zapisywalne) | +| `Lp` | `int` | liczba porządkowa zdarzeń w dniu (bazodanowe) | +| `DefinicjaZdarzenia` | `Soneta.Kalend.DefinicjaZdarzeniaRCP` | opcjonalna definicja zdarzenia ze słownika `DefZdarzenRCP` | +| Kolekcja zdarzeń dnia | `DzienPracy.WeWy : LpSubTable` | uporządkowana po `Lp` | + +**Snippet:** + +```csharp +var kalend = session.GetKadry().Session.GetKalend(); +var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; +var data = new Date(2026, 6, 1); + +using (var t = session.Logout(editMode: true)) +{ + // Upewnij się, że istnieje dzień ewidencji (właściciel zdarzeń): + var dp = pracownik.DniPracy[data]; + if (dp is null) + { + dp = session.AddRow(new DzienPracy(pracownik, data)); + kalend.DniPracy.AddRow(dp); + } + + // Wejście 8:00 + var we = new WejscieWyjscie(dp); // ctor wiąże zdarzenie z dniem + kalend.WejsciaWyjscia.AddRow(we); + we.Godzina = new Time(8, 0); + we.Typ = TypWejsciaWyjscia.Wejscie; + + // Wyjście 16:00 + var wy = new WejscieWyjscie(dp); + kalend.WejsciaWyjscia.AddRow(wy); + wy.Godzina = new Time(16, 0); + wy.Typ = TypWejsciaWyjscia.Wyjscie; + + t.Commit(); +} +session.Save(); + +// --- Odczyt zdarzeń dnia --- +var dzien = pracownik.DniPracy[data]; +if (dzien is not null) +{ + foreach (WejscieWyjscie wewy in dzien.WeWy) // posortowane po Lp + { + // wewy.Godzina, wewy.Typ, wewy.Operacja + } +} +``` + +**Pułapki:** +- `WejscieWyjscie` jest childem **`DzienPracy`**, nie `DzienRCP` — najpierw potrzebujesz dnia + ewidencji (`pracownik.DniPracy[data]`); zdarzenia wiążesz ctorem `new WejscieWyjscie(dzienPracy)` + i dodajesz do tabeli `kalend.WejsciaWyjscia.AddRow(...)`. +- `Typ` to enum `TypWejsciaWyjscia` (`Wejscie`/`Wyjscie`/…), nie string ani `int`. Para + wejście+wyjście jest podstawą wyliczenia czasu dnia z surowych zdarzeń. +- `DefinicjaZdarzenia` jest **opcjonalna** — przy ręcznym wpisie wystarczą `Godzina` + `Typ`. Jeśli + używasz definicji, pobierz wpis ze słownika konfiguracyjnego `kalend.DefZdarzenRCP` (nie twórz w locie). +- `WeWy` to `LpSubTable` — kolejność zdarzeń wynika z `Lp` (nadawane automatycznie); nie ustawiaj `Lp` + ręcznie. Do usunięcia wszystkich zdarzeń dnia (przy ponownym imporcie) służy kasowanie elementów kolekcji. +- Surowe zdarzenia są przeliczane na czas pracy/RCP przez kalkulator i import — samo dodanie + wejść/wyjść nie aktualizuje automatycznie `DzienRCP` (to robi przeliczenie/import RCP). + +### KADRY-F3 — Import danych z RCP (bezpośredni i przez tabelę pośrednią) + +**Cel:** wczytać surowe odbicia z czytników RCP i przeliczyć je na ewidencję/zweryfikowany RCP. +**UWAGA: operacja plikowa/sieciowa — opis modelu; samego importu z pliku/urządzenia NIE testujemy.** + +**Model danych:** + +| Element | Lokalizacja | Typ | Uwaga | +|---|---|---|---| +| Tabela pośrednia (surowe odbicia) | `DzienPracy.WeWy` | `LpSubTable` | zdarzenia we/wy (godzina, typ) — patrz KADRY-F2 | +| Zarejestrowany RCP (zweryfikowany) | `pracownik.DniRCP[data]` | `Soneta.Kalend.DzienRCP` | wynik importu/przeliczenia | +| Ewidencja | `pracownik.DniPracy[data]` | `Soneta.Kalend.DzienPracy` | docelowa realizacja | +| Flaga importu | `DzienPracy.RcpOK` / `DzienRCP.RcpOK` | `bool` | „stan rekordu po imporcie z RCP" | +| Stan weryfikacji | `DzienRCP.StanRCP` | `StanWeryfikacjiRCP` | patrz KADRY-F4 | + +**Workery przeliczające (po wczytaniu odbić — operują na obiektach sesji):** + +| Worker | Sygnatura | Rola | +|---|---|---| +| `Soneta.Kalend.ImportDniaWorker` | ctor `()`, `DzienPracy DzienPracy {get;set;}`, `void Przelicz()` | przelicza pojedynczy dzień z we/wy na czas pracy | +| `Soneta.Kalend.RCPWeryfikatorWorker` | `Dopasuj()`, `DopasujDlaZaznaczonych()`, `Dodaj()`, `Usun()` (+ `IsVisible*`), props `rw : RCPWeryfikator`, `Strefy`, `Wybrana` | dopasowanie odbić do plan/strefy (UI) | + +**Snippet (przeliczenie dnia z już wczytanych we/wy — bez pliku):** + +```csharp +var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; +var dzien = pracownik.DniPracy[new Date(2026, 6, 1)]; // dzień z wpisanymi WeWy (KADRY-F2) +if (dzien is not null) +{ + using (var t = session.Logout(editMode: true)) + { + new ImportDniaWorker { DzienPracy = dzien }.Przelicz(); // we/wy -> czas pracy + t.Commit(); + } + session.Save(); +} +``` + +**Pułapki / wykonalność:** +- **Sam import z pliku/urządzenia (czytnik, sieć, format) jest poza zakresem testu** — wymaga + zewnętrznego źródła (plik/serwis), brak czystego API w tym kontrakcie. +- **Testowalny fragment**: przygotowanie tabeli pośredniej `DzienPracy.WeWy` (ctor `WejscieWyjscie(dp)`, + patrz KADRY-F2) + `ImportDniaWorker.Przelicz()` — to przelicza już-wczytane odbicia bez I/O. +- `RCPWeryfikatorWorker` jest mocno UI (metody `IsVisible*`, `Strefy`/`Wybrana`) — to dopasowanie + ręczne; nie wywoływać z kodu biznesowego. +- `DzienRCP` powstaje z importu/przeliczenia — w teście nie twórz go „z palca"; odczytuj po `Przelicz()`. + +--- + +### KADRY-F4 — Weryfikacja i korekta danych RCP (★ testowalne) + +**Cel:** odczytać i skorygować zweryfikowany rekord RCP — zmienić stan weryfikacji oraz poprawić +godziny pracy / opisać błędy i uwagi. + +**Pola `Soneta.Kalend.DzienRCP` (tabela `DniRCP`, child `Pracownik`):** + +| Pole | Typ | Rodzaj | Uwaga | +|---|---|---|---| +| `Data` | `Soneta.Types.Date` | bazodanowe | data dnia (ctor) | +| `Pracownik` | `Soneta.Kadry.Pracownik` | bazodanowe, guided-parent | właściciel | +| `Praca` | `Soneta.Kalend.CzasPracy` | bazodanowe | `Praca.OdGodziny`/`Praca.DoGodziny`/`Praca.Czas : Time` (zapisywalne) | +| `Czas`, `OdGodziny` | `Soneta.Types.Time` | kalkulowane | read-only (z `Praca`) | +| `StanRCP` | `Soneta.Kalend.StanWeryfikacjiRCP` | bazodanowe | stan weryfikacji (zapisywalne) | +| `RcpOK` | `bool` | bazodanowe | stan rekordu po imporcie (zapisywalne) | +| `Uwagi` | `Soneta.Business.MemoText` | bazodanowe | uwagi do weryfikacji | +| `Bledy` | `Soneta.Business.MemoText` | bazodanowe | opis błędów | +| `Strefy` | `SubTable` | | strefy zarejestrowane | +| `StrefyOrg` | `Soneta.Business.MemoText` | bazodanowe | strefy źródłowe (org.) | + +**`Soneta.Kalend.StanWeryfikacjiRCP` (enum):** `DoWeryfikacji`, `WymagaWeryfikacji`, +`PrzekazanyDoWyjaśnienia`, `DoZatwierdzenia`, `Modyfikowany`, `Naniesiony`, `Poprawny`, `Błędny`, +`Wszystkie`. + +**Snippet (korekta godzin + zmiana stanu):** + +```csharp +var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; +var dzienRcp = pracownik.DniRCP[new Date(2026, 6, 1)]; // DzienRCP lub null +if (dzienRcp is not null) +{ + using (var t = session.Logout(editMode: true)) + { + dzienRcp.Praca.OdGodziny = new Time(8, 0); // korekta na subrowie Praca + dzienRcp.Praca.DoGodziny = new Time(16, 0); + dzienRcp.StanRCP = StanWeryfikacjiRCP.Poprawny; // zatwierdzenie weryfikacji + dzienRcp.Uwagi = (MemoText)"Skorygowano wyjście"; + t.Commit(); + } + session.Save(); +} +``` + +**Pułapki:** +- `DniRCP` jest **typowane** (`DateSubTable`) — indeksator `[Date]` zwraca `DzienRCP`/`null`; + do zakresu użyj `[FromTo]`. Nie iteruj kolekcji w poszukiwaniu dnia. +- Godziny koryguj na **subrowie `Praca`** (`Praca.OdGodziny`/`DoGodziny`); `DzienRCP.Czas`/`OdGodziny` + na rootcie są kalkulowane (read-only). +- `StanRCP` to enum `StanWeryfikacjiRCP` — nie string. Zmiana stanu może podlegać weryfikatorom. +- W Demo `DzienRCP` istnieje tylko gdy był import/przeliczenie — test korekty zakłada istniejący dzień + (sprawdzaj `is not null`), nie twórz `DzienRCP` ręcznie. + +--- + +### KADRY-F5 — Rozliczenie pracy hybrydowej / aktualizacja podzielników na podstawie pracy hybrydowej + +**Cel:** rozliczyć czas pracy hybrydowej (podział na strefy: stacjonarna / praca zdalna / zdalna +okazjonalna) i zaktualizować podzielniki (elementy rozliczenia czasu pracy / strefy dnia), na +podstawie których naliczane są składniki płacowe i koszty. + +**Model danych (publiczny kontrakt):** + +| Element | Lokalizacja | Typ | Uwaga | +|---|---|---|---| +| Strefy pracy dnia | `DzienPracy.Strefy` | `SubTable` | podział dnia na strefy | +| Strefa pracy | `Soneta.Kalend.StrefaPracy` (ctor `(DzienPracy dzien)`) | — | `Definicja : DefinicjaStrefy`, `CzasRozliczany : Time`, `OdGodziny`/`Czas` (kalk.) | +| Definicja strefy | `Soneta.Kalend.DefinicjaStrefy` | konfiguracja | `Typ : TypStrefy`, `Wchodzi`, `Rozliczana`; stałe `Praca_Zdalna`, `PracaZdalnaOkazjonalna : Guid` | +| Dokumenty rozliczenia | `pracownik.RozliczeniaCzasuPracy` | `SubTable` | dokumenty rozliczenia czasu (podzielniki) | +| Elementy rozliczenia | `pracownik.ElementyRozliczeniaCzasuPracy` | `SubTable` | pozycje podzielnika | +| Dokument rozliczenia | `Soneta.Kalend.RozliczenieCzasuPracy` (ctor `(Pracownik, DefinicjaRozliczeniaCzasuPracy)`) | root | `Data`, `Seria`, `Stan : StanyRozliczeniaCzasuPracy` | +| Pozycja rozliczenia | `Soneta.Kalend.ElementRozliczeniaCzasuPracy` (ctor `(RozliczenieCzasuPracy dokument)`) | child | `Definicja : DefinicjaStrefy`, `Data`, `OdGodziny`, `Czas`, `CzasPozostały`/`CzasDostępny`/`Zrealizowane` (kalk.) | + +**`Soneta.Kalend.TypStrefy` (enum):** `NieWplywa`, `Zwieksza`, `Zmniejsza`. + +**Workery (UI/extendery — praca zdalna/hybrydowa):** +- `Soneta.Kalend.StrefaPracy.PracaZdalnaWorker` (`Strefa : StrefaPracy`) — oznaczenie strefy jako + praca zdalna. +- `Soneta.Kadry.PracaZdalna.DzienStrefaExtWorker`, `ElementRozliczeniaCzasuPracyExtWorker`, + `DzienZestawienieExtender` — extendery zestawień/dni dla pracy zdalnej. + +**Snippet (odczyt rozkładu na strefy + dokument rozliczenia):** + +```csharp +var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; + +// Odczyt podziału dnia na strefy (stacjonarna / zdalna): +var dzien = pracownik.DniPracy[new Date(2026, 6, 1)]; +if (dzien is not null) +{ + foreach (StrefaPracy s in dzien.Strefy) + { + DefinicjaStrefy def = s.Definicja; // strefa (np. praca zdalna) + Time rozliczany = s.CzasRozliczany; // czas rozliczany w strefie + } +} + +// Pozycje podzielnika (elementy rozliczenia czasu pracy): +foreach (ElementRozliczeniaCzasuPracy el in pracownik.ElementyRozliczeniaCzasuPracy) +{ + DefinicjaStrefy def = el.Definicja; + Time czas = el.Czas; +} +``` + +**Pułapki / wykonalność:** +- Rozkład pracy hybrydowej to **strefy** (`DzienPracy.Strefy` / `DefinicjaStrefy` z flagą zdalna) + + dokument `RozliczenieCzasuPracy` z pozycjami `ElementRozliczeniaCzasuPracy` (podzielniki). +- `RozliczenieCzasuPracy` to **root** (ctor `(Pracownik, DefinicjaRozliczeniaCzasuPracy)`) — utworzenie + wymaga istniejącej `DefinicjaRozliczeniaCzasuPracy` z konfiguracji; pozycje ctorem + `new ElementRozliczeniaCzasuPracy(dokument)`. Czysty odczyt jest bezpieczny i bez transakcji. +- Aktualizacja podzielników na podstawie pracy hybrydowej przebiega **głównie przez extendery/UI** + (`DzienStrefaExtWorker`, `ElementRozliczeniaCzasuPracyExtWorker`, `PracaZdalnaWorker`) zależne od + `Context`/wniosków e-pracownika — brak prostego, czystego API operacyjnego. +- Wymaga skonfigurowanych `DefinicjaStrefy` (Praca_Zdalna / PracaZdalnaOkazjonalna) — w Demo strefy + mogą nie być włączone, co czyni budowę rozliczenia kruchą do testu (raczej odczyt niż zapis). + diff --git a/soneta-programming/references/domeny/kadry/KADRY07-umowy.md b/soneta-programming/references/domeny/kadry/KADRY07-umowy.md new file mode 100644 index 0000000..fd8ef26 --- /dev/null +++ b/soneta-programming/references/domeny/kadry/KADRY07-umowy.md @@ -0,0 +1,479 @@ +# KADRY07 — Umowy cywilnoprawne + +> Wspólne fakty o typie, podstawowe typy i szablon wzorca: [../kadry.md](../kadry.md). + +### KADRY-G1 — Dodawanie umów cywilnoprawnych (zlecenie, o dzieło) (★) + +**Cel:** utworzyć dla pracownika umowę cywilnoprawną (zlecenie / o dzieło / ryczałtowa) z kompletem +danych pozwalającym na `Session.Save()`: definicja elementu płacowego (rodzaj umowy), okres, wartość, +sposób rozliczenia i typ wartości (brutto/netto). + +**Mechanizm (kluczowy):** `Soneta.Kadry.Umowa` to **root historyczny** (tabela `Umowy`), child +pracownika. Dodanie umowy do tabeli (`Module.Umowy.AddRow(umowa)`) w `OnAdded` **automatycznie tworzy +pierwszy zapis** `Soneta.Kadry.UmowaHistoria` (tabela `UmowaHistorie`) oraz domyślną definicję +elementu, okres, datę i numerację. Dlatego **nie tworzymy `UmowaHistoria` ręcznie** — bezpośrednio po +`AddRow` istnieje już `umowa.Last` (pierwszy zapis), na którym ustawiamy **wartość umowy**. + +> **Gdzie co siedzi.** Dane „nagłówkowe" umowy (definicja elementu, okres, sposób rozliczenia, typ +> wartości) są na **roocie** `Umowa`. **Kwota/wartość umowy** jest **historyczna** i siedzi na +> `UmowaHistoria.Wartosc` — ustawiasz ją przez `umowa.Last.Wartosc`. Property `umowa.Brutto`/ +> `umowa.Wartosc` na roocie oraz `UmowaHistoria.Brutto` są **wyliczane** (read-only). + +**Warianty (rodzaj umowy = `DefinicjaElementu`):** + +| Rodzaj umowy | Jak wskazać definicję | +|---|---| +| Zlecenie | `DefElementow[DefinicjaElementu.UmowaZlecenie]` (to też wartość domyślna konfiguracji) | +| O dzieło (20% KUP) | `DefElementow[DefinicjaElementu.Umowa20]` | +| Ryczałtowa | `DefElementow[DefinicjaElementu.UmowaRyczałtowa]` | +| Inna (po kodzie/nazwie) | `DefElementow[""]` (indeksator string = wg kodu) — pod warunkiem że jej `RodzajZrodla == RodzajŹródłaWypłaty.Umowa` | + +**Pola i typy:** + +| Pole | Gdzie | Typ | Uwaga | +|---|---|---|---| +| `Element` | `Umowa` (root) | `Soneta.Place.DefinicjaElementu` | definicja elementu = rodzaj umowy; akceptowana tylko gdy `RodzajZrodla == RodzajŹródłaWypłaty.Umowa` | +| `Data` | `Umowa` (root) | `Soneta.Types.Date` | data zawarcia/dokumentu | +| `Okres` | `Umowa` (root) | `Soneta.Types.FromTo` | okres obowiązywania umowy | +| `Tytul` | `Umowa` (root) | `string` | tytuł/temat umowy | +| `RodzajRozliczenia` | `Umowa` (root) | `Soneta.Kadry.RodzajeRozliczeniaUmowy` | `KwotaDoWypłaty` / `StawkaZaOkres` / `StawkaZaGodzinę` | +| `TypWartosci` | `Umowa` (root) | `Soneta.Kadry.TypWartosciUmowy` | `Brutto` / `Netto` | +| `Wydzial` | `Umowa` (root) | `Soneta.Kadry.Wydzial` | jednostka organizacyjna — **wymagana** (weryfikator przy `Save()`); ustaw `kadry.Wydzialy.Firma` | +| `Wartosc` | `UmowaHistoria` (`Last`) | `Soneta.Types.Currency` | **kwota/wartość umowy** — ustawiana na zapisie historycznym | + +**Snippet:** + +```csharp +var kadry = session.GetKadry(); +var place = session.GetPlace(); +var pracownik = kadry.Pracownicy.WgKodu["006"]; + +using (var t = session.Logout(editMode: true)) +{ + // 1) Utworzenie umowy + dodanie do tabeli; w OnAdded powstaje pierwszy UmowaHistoria. + var umowa = session.AddRow(new Umowa(pracownik)); + + // 2) Definicja elementu = rodzaj umowy (tu: zlecenie). Indeksator Guid + stała publiczna. + umowa.Element = place.DefElementow[DefinicjaElementu.UmowaZlecenie]; + + // 3) Dane nagłówkowe na roocie: + umowa.Data = new Date(2026, 1, 1); + umowa.Okres = new FromTo(new Date(2026, 1, 1), new Date(2026, 12, 31)); + umowa.Tytul = "Umowa zlecenie - obsługa projektu"; + umowa.RodzajRozliczenia = RodzajeRozliczeniaUmowy.KwotaDoWypłaty; + umowa.TypWartosci = TypWartosciUmowy.Brutto; + umowa.Wydzial = kadry.Wydzialy.Firma; // wymagane przy Save() + + // 4) KWOTA umowy — na zapisie historycznym Last (UmowaHistoria.Wartosc): + umowa.Last.Wartosc = new Currency(5000m); + + t.Commit(); // Commit() w kodzie biznesowym; CommitUI() w workerze/UI +} +session.Save(); // tu wykrywane konflikty/duplikaty +``` + +**Pułapki:** +- **Nie** twórz ręcznie pierwszego `UmowaHistoria` — robi to `OnAdded` przy `AddRow`. Ręczny nowy + zapis dotyczy dopiero *zmiany/aneksu* „od daty" (KADRY-G2). +- **Kwotę ustawiaj na `umowa.Last.Wartosc`**, nie na roocie — `umowa.Brutto`/`umowa.Wartosc` oraz + `Last.Brutto` są wyliczane (read-only). `Wartosc` to `Soneta.Types.Currency`, nie `decimal` + (safe-code §10.1). +- `Element` przyjmie tylko definicję o `RodzajZrodla == RodzajŹródłaWypłaty.Umowa`. Definicje + „etatowe" (np. `EtatMies`) zostaną zignorowane (umowa dostanie domyślną definicję z konfiguracji). +- Jeśli `Element` **nie** zostanie ustawiony, umowa przyjmuje + `Module.Config.Ogólne.DomyślnaDefinicjaUmowy` (domyślnie = `UmowaZlecenie`). +- Dodanie umowy do pracownika **w archiwum** rzuca wyjątek (`WArchiwumException`) z `OnAdded`. +- `RodzajRozliczenia`/`TypWartosci` bywają w UI tylko-do-odczytu zależnie od definicji elementu — + w kodzie biznesowym ustawiasz je wprost (nie używaj `IsReadOnlyXxx`, safe-code §7.1). +- Całość w transakcji (`session.Logout(editMode: true)`); brak `Commit()` = rollback przy `Dispose()`. + +### KADRY-G2 — Zmiana/aneks umowy (★) + +**Cel:** zmienić warunki istniejącej umowy. Trzy rozłączne przypadki: **(a) korekta** danych +nagłówkowych w bieżącym okresie; **(b) aneks „od daty"** — nowy zapis historyczny `UmowaHistoria` +obowiązujący od wskazanego dnia (analogicznie do `PracHistoria`, sekcja KADRY-A14); **(c) seryjna +aktualizacja stawki** workerem. + +**Mechanizm `HistorySubTable`** (`umowa.Historia`): + +| Operacja | API | Efekt | +|---|---|---| +| Odczyt zapisu na dzień | `umowa[date]` (== `(UmowaHistoria)Historia[date]`) | zapis, którego `Aktualnosc` zawiera `date` | +| Ostatni (bieżący) zapis | `umowa.Last` (== `Historia.GetPrev()`) | najświeższy zapis | +| Pierwszy zapis | `umowa.Historia.GetFirst()` | najstarszy zapis | +| **Nowy zapis „od daty"** | `(UmowaHistoria)umowa.Historia.Update(date)` | **klonuje** zapis aktualny na `date`, skraca stary do `date-1`, zwraca **nowy** klon (okres od `date`); klon trzeba dodać do tabeli | +| Okres obowiązywania | `UmowaHistoria.Aktualnosc: FromTo` | „od–do" zapisu (zarządzany przez historię) | + +**(a) Korekta danych nagłówkowych umowy (bez nowego okresu):** + +```csharp +var umowa = pracownik.Umowy.First(); // lub wyszukanie po polu/numerze + +using (var t = session.Logout(editMode: true)) +{ + umowa.Tytul = "Umowa zlecenie - aneks zakresu prac"; + umowa.Okres = new FromTo(umowa.Okres.From, new Date(2027, 6, 30)); // przedłużenie + t.Commit(); +} +session.Save(); +``` + +**(b) Aneks „od daty" — zmiana wartości umowy nowym zapisem historycznym:** + +```csharp +var odDnia = new Date(2026, 7, 1); + +using (var t = session.Logout(editMode: true)) +{ + // 1) Update klonuje zapis aktualny na `odDnia` i zwraca nowy klon (okres od `odDnia`); + // stary zapis zostaje skrócony do dnia poprzedniego. + var nowy = (UmowaHistoria)umowa.Historia.Update(odDnia); + + // 2) Klon trzeba dodać do tabeli zapisów historii umowy. + umowa.Module.UmowaHistorie.AddRow(nowy); + + // 3) Na nowym zapisie ustawiamy zmienioną wartość (od `odDnia`): + nowy.Wartosc = new Currency(6000m); + // Uwaga: UmowaHistoria.PowodAktualizacji jest tylko do odczytu (ustawiane wewnętrznie) — nie przypisuj. + + t.Commit(); +} +session.Save(); +``` + +**(c) Seryjna aktualizacja stawki — worker `Umowa.AktualizacjaStawkiWorker`:** + +```csharp +var worker = new Umowa.AktualizacjaStawkiWorker +{ + Umowy = new[] { umowa }, + Pars = ... // Umowa.AktualizacjaStawkiWorker.Params: Data: Date, Wartosc: Currency +}; +// uruchomienie zgodnie z konwencją workerów (patrz worker-extender.md) +``` + +> Pokrewne workery (wywoływane jak każdy worker enova): `Umowa.KopiujUmowe2Worker` +> (`Umowa Umowa` — kopiuje umowę), `Umowa.WyrejestrujUmoweWorker` (wyrejestrowanie umowy). + +**Pułapki:** +- **`Update(date)` + `UmowaHistorie.AddRow(nowy)` to nierozłączna para.** Sam `Update` tworzy + „odpięty" klon — bez `AddRow` zmiana nie zostanie zapisana. +- `Update(date)` rzuca wyjątek duplikatu, gdy na `date` już zaczyna się zapis (`Aktualnosc.From == date`) + — nie da się „aktualizować" dwa razy tego samego dnia; wtedy modyfikuj istniejący zapis (`umowa[date]`). +- **Korekta** (`umowa.Last.Wartosc = …` lub modyfikacja `umowa[date]`) zmienia dane w **całym** okresie + tego zapisu — używaj jej do poprawy błędu, nie do „zmiany od dnia"; do zmiany od dnia → wariant (b). +- `Aktualnosc` (okres zapisu) jest zarządzany przez historię — **nie ustawiaj go ręcznie**; do + skrócenia/wstawienia okresu służy `Update`. +- Wartość zawsze jako `Soneta.Types.Currency`, nie `decimal` (safe-code §10.1); daty jako + `Soneta.Types.Date`/`Date.Today` (§10.2). +- Obsłuż `RowConflictException` z `Save()` (safe-code §4); transakcje trzymaj krótkie (§13.1). + +### KADRY-G3 — Operacja seryjna „Dodaj umowy" dla grupy osób (★) + +**Cel:** dodać jednakową umowę cywilnoprawną (zlecenie / o dzieło / ryczałtowa) **naraz dla wielu +zaznaczonych pracowników** — operacja seryjna z listy osób. W UI: menu *Operacje seryjne → Dodaj +umowy…*. Każdej osobie z zaznaczenia tworzona jest osobna `Umowa` z tymi samymi danymi nagłówkowymi +(definicja elementu, okres, wartość, sposób rozliczenia), analogicznie do KADRY-G1. + +**Worker (publiczny kontrakt):** `Soneta.Kadry.Pracownik.DodajUmowęWorker` — worker **przypisany do +`Pracownik`** (`DataType = Pracownik`). Udostępnia akcję `DodajUmowę` w menu czynności listy +pracowników. + +| Składowa | Typ / sygnatura | Uwaga | +|---|---|---| +| Konstruktor | `DodajUmowęWorker(Session session)` | worker ma **ctor z `Session`** (nie bezparametrowy) | +| Zaznaczone osoby | `DodajUmowęWorker.Pracownicy: Pracownik[]` | `[Context]` — tablica pracowników z zaznaczenia listy | +| Parametry | `DodajUmowęWorker.Pars: DodajUmowęWorker.Params` | `[Context]` — okno parametrów operacji; `Params(Context)` | +| Akcja | `void DodajUmowę()` | tworzy umowy dla wszystkich `Pracownicy` (zwraca **`void`**, nie `object`) | + +**Parametry operacji (`DodajUmowęWorker.Params`):** + +| Pole | Typ | Odpowiednik na `Umowa` (KADRY-G1) | +|---|---|---| +| `Definicja` | `Soneta.Core.DefinicjaDokumentu` | definicja dokumentu umowy (numeracja/seria) | +| `Seria` | `string` | seria numeracji | +| `Wydział` | `Soneta.Kadry.Wydzial` | `Umowa.Wydzial` (wymagany) | +| `Data` | `Soneta.Types.Date` | `Umowa.Data` (data zawarcia) | +| `Okres` | `Soneta.Types.FromTo` | `Umowa.Okres` | +| `Tytuł` | `string` | `Umowa.Tytul` | +| `Element` | `Soneta.Place.DefinicjaElementu` | `Umowa.Element` (rodzaj umowy) | +| `RodzajRozliczenia` | `Soneta.Kadry.RodzajeRozliczeniaUmowy` | `Umowa.RodzajRozliczenia` | +| `Wartość` | `Soneta.Types.Currency` | `umowa.Last.Wartosc` (kwota umowy) | +| `TypWartości` | `Soneta.Kadry.TypWartosciUmowy` | `Umowa.TypWartosci` (`Brutto`/`Netto`) | + +**Wariant A — wywołanie workera platformy (zalecane):** zainicjuj `DodajUmowęWorker`, ustaw +`Pracownicy` i `Pars`, wywołaj `DodajUmowę()` (worker uruchamia się jak każdy worker — patrz +[`worker-extender.md`](../worker-extender.md), sekcja *Programowe użycie workera*). + +```csharp +var kadry = session.GetKadry(); +var place = session.GetPlace(); + +// Grupa osób (np. z zaznaczenia listy). Tu: kilku pracowników po kodzie: +var osoby = new[] +{ + kadry.Pracownicy.WgKodu["006"], + kadry.Pracownicy.WgKodu["007"], + kadry.Pracownicy.WgKodu["008"], +}; + +var pars = new Pracownik.DodajUmowęWorker.Params(context); +pars.Element = place.DefElementow[DefinicjaElementu.UmowaZlecenie]; +pars.Okres = new FromTo(new Date(2026, 1, 1), new Date(2026, 12, 31)); +pars.Data = new Date(2026, 1, 1); +pars.Tytuł = "Umowa zlecenie - projekt grupowy"; +pars.RodzajRozliczenia = RodzajeRozliczeniaUmowy.KwotaDoWypłaty; +pars.TypWartości = TypWartosciUmowy.Brutto; +pars.Wartość = new Currency(4000m); +pars.Wydział = kadry.Wydzialy.Firma; // wymagany + +// Worker ma konstruktor z Session (nie bezparametrowy); Pracownicy/Pars przez inicjalizator: +var worker = new Pracownik.DodajUmowęWorker(session) { Pracownicy = osoby, Pars = pars }; +worker.DodajUmowę(); // void +session.Save(); +``` + +**Wariant B — pętla po pracownikach (jawne tworzenie, jak KADRY-G1):** gdy nie chcesz przechodzić przez +worker — dla każdej osoby twórz `new Umowa(p)` i ustaw te same pola co w KADRY-G1. To jawnie pokazuje, że +operacja seryjna = KADRY-G1 powtórzone w pętli. + +```csharp +var kadry = session.GetKadry(); +var place = session.GetPlace(); +var defElementu = place.DefElementow[DefinicjaElementu.UmowaZlecenie]; +var okres = new FromTo(new Date(2026, 1, 1), new Date(2026, 12, 31)); + +using (var t = session.Logout(editMode: true)) +{ + foreach (var p in osoby) + { + var umowa = session.AddRow(new Umowa(p)); // OnAdded tworzy pierwszy UmowaHistoria + umowa.Element = defElementu; + umowa.Data = okres.From; + umowa.Okres = okres; + umowa.Tytul = "Umowa zlecenie - projekt grupowy"; + umowa.RodzajRozliczenia = RodzajeRozliczeniaUmowy.KwotaDoWypłaty; + umowa.TypWartosci = TypWartosciUmowy.Brutto; + umowa.Wydzial = kadry.Wydzialy.Firma; // wymagany przy Save() + umowa.Last.Wartosc = new Currency(4000m); // kwota na zapisie historycznym + } + t.Commit(); // Commit() w kodzie biznesowym; CommitUI() w workerze/UI +} +session.Save(); +``` + +**Pułapki:** +- `Pracownik.DodajUmowęWorker` jest workerem na typie `Pracownik`, a tworzy obiekty `Umowa` — nie myl + go z workerami na `Umowa` (KADRY-G2: `AktualizacjaStawkiWorker`, `KopiujUmowe2Worker`). +- W wariancie B obowiązują wszystkie pułapki KADRY-G1: kwota na `umowa.Last.Wartosc` (root `Brutto`/`Wartosc` + są wyliczane), `Element` tylko o `RodzajZrodla == RodzajŹródłaWypłaty.Umowa`, `Wydzial` wymagany, + dodanie umowy pracownikowi **w archiwum** rzuca `WArchiwumException`. +- Pętlę edycyjną trzymaj krótką (safe-code §13.1); konflikty/duplikaty wykrywane w `Save()` (§4). +- Wartość zawsze jako `Soneta.Types.Currency`, daty jako `Date`/`FromTo`, nie `decimal`/`DateTime` + (safe-code §10). + +--- + +### KADRY-G4 — Rachunek do umowy zlecenia (★) + +**Cel:** wystawić **rachunek do umowy zlecenia** — czyli rozliczyć (naliczyć i wypłacić) umowę +cywilnoprawną. W modelu Soneta „rachunek do umowy zlecenia" **nie jest osobnym rekordem** na `Umowa` +ani w `pracownik.Rachunki` — to **wypłata z umowy** typu `Soneta.Place.WyplataUmowa`, naliczana +mechanizmem płac (jak KADRY-H2). `pracownik.Rachunki: SubTable` to +**rachunki bankowe** pracownika (numer konta), a nie rachunki do umów — nie myl tych pojęć. + +> **Gdzie to siedzi.** Każda umowa ma wstecz powiązane rozliczenia/wypłaty: +> - `Umowa.RozliczeniaWynagrodzenia: LpSubTable` — rozliczenia +> wynagrodzenia z umowy, +> - `Umowa.Elementy: SubTable` — naliczone składniki wypłat tej umowy, +> - sama wypłata to `Soneta.Place.WyplataUmowa` (konkretny typ `Wyplata`), z polem zwrotnym +> `WyplataUmowa.Umowa: Soneta.Kadry.Umowa`. +> Stan rozliczenia umowy odczytasz z `Umowa.Stan: Soneta.Kadry.StanUmowy` +> (`Niewypłacona` / `WypłaconaCzęściowo` / `WypłaconaCałkowicie` / `Anulowana`) oraz z +> `Umowa.Splacono`, `Umowa.Pozostało` (`Soneta.Types.Currency`). + +**Tworzenie rachunku (wypłaty) do umowy — wykonawca naliczania `NaliczanieSeryjne.Umowy` (jak KADRY-H2):** + +| Element | Typ / sygnatura | Uwaga | +|---|---|---| +| Parametry | `new NaliczanieSeryjne.UmowaParams(context)` | `Naliczanie` na sztywno `PłatnaZDołu` (setter rzuca `NotSupportedException`) | +| Data rachunku / listy | `UmowaParams.DataWypłaty`, `.DataListy` | daty rachunku | +| Wykonawca | `new NaliczanieSeryjne.Umowy(UmowaParams) { Umowa = umowa }` | ustawienie `Umowa` ustawia też `Pracownik` z `umowa.Pracownik` | +| Uruchomienie | `Umowy.Nalicz(): NaliczanieWypłat` | tworzy `WyplataUmowa` i liczy składniki | +| Wynik | `NaliczanieWypłat.WszystkieWypłaty: IList` | elementy `Wyplata` (tu `WyplataUmowa`) | + +```csharp +var kadry = session.GetKadry(); +var pracownik = kadry.Pracownicy.WgKodu["006"]; + +// Umowa zlecenie pracownika (np. utworzona w KADRY-G1): +Umowa umowa = pracownik.Umowy.Cast() + .First(u => u.Element == session.GetPlace().DefElementow[DefinicjaElementu.UmowaZlecenie]); + +var pars = new NaliczanieSeryjne.UmowaParams(context); +pars.DataWypłaty = new Date(2026, 2, 10); // data wystawienia rachunku +pars.DataListy = pars.DataWypłaty; + +var naliczanie = new NaliczanieSeryjne.Umowy(pars) { Umowa = umowa }; +NaliczanieWypłat wynik = naliczanie.Nalicz(); // tworzy WyplataUmowa (rachunek) + +foreach (Wyplata w in wynik.WszystkieWypłaty) +{ + // w.Typ == TypWyplaty.Umowa; w to WyplataUmowa; w.Elementy = składniki rachunku +} +session.Save(); + +// Po naliczeniu — stan rozliczenia umowy: +StanUmowy stan = umowa.Stan; // np. WypłaconaCałkowicie +Currency splacono = umowa.Splacono; // kwota rozliczona +Currency pozostalo = umowa.Pozostało; // pozostała do wypłaty +``` + +**Odczyt rachunków (wypłat) wystawionych do umowy:** + +```csharp +// Wypłaty (rachunki) tej umowy — przez wypłaty pracownika filtrowane po umowie: +foreach (WyplataUmowa w in pracownik.Wyplaty.OfType().Where(x => x.Umowa == umowa)) +{ + // w.Data, w.Elementy (WypElement.Wartosc / .Netto / .Podatki.*) +} + +// Składniki naliczone bezpośrednio z umowy: +foreach (WypElement e in umowa.Elementy) +{ + // e.Wartosc, e.Netto +} +``` + +**Pułapki:** +- „Rachunek do umowy zlecenia" = `WyplataUmowa`, a nie rekord w `pracownik.Rachunki` (to rachunki + bankowe). Tworzysz go naliczaniem (`NaliczanieSeryjne.Umowy.Nalicz()`), nie `AddRow` po wypłacie. +- **Nie ustawiaj `UmowaParams.Naliczanie`** — umowy są zawsze „płatne z dołu" (setter rzuca + `NotSupportedException`). +- Ustawienie `Umowy.Umowa` nadpisuje `Pracownik` właścicielem umowy — nie ustawiaj `Pracownik` ręcznie. +- `Nalicz()` wewnętrznie otwiera własną transakcję i zatwierdza zmiany w sesji — po nim wołasz tylko + `Session.Save()`; nie owijaj go w dodatkowy `Logout(editMode: true)`. +- `Wyplata` nie ma agregatów `Brutto`/`Netto` — sumuj składniki z `Wyplata.Elementy` (jak w KADRY-H2/KADRY-H4). +- Kwoty jako `Soneta.Types.Currency`, daty jako `Date` (safe-code §10). + +--- + +### KADRY-G5 — Zgłoszenia ZUS zleceniobiorców (ZUA / ZZA na podstawie umowy) (★) + +**Cel:** przygotować zgłoszenie zleceniobiorcy do ZUS — **ZUA** (zgłoszenie do ubezpieczeń +społecznych + zdrowotnego) albo **ZZA** (tylko zdrowotne) — **na podstawie schematu ubezpieczeń +umowy**, oraz wyrejestrowanie (**ZWUA**) po jej zakończeniu. O tym, czy powstaje ZUA czy ZZA, decyduje +**schemat ubezpieczeń zapisu umowy**, a nie odrębne pole „rodzaj zgłoszenia". + +> **Schemat ubezpieczeń umowy — gdzie siedzi.** Ubezpieczenia umowy są **historyczne** i leżą na +> zapisie `UmowaHistoria.Ubezpieczenia: Soneta.Kadry.Ubezpieczenia` (analogicznie do +> `PracHistoria.Etat.Ubezpieczenia` z KADRY-A7; ta sama struktura `Spoleczne`/`Zdrowotne`). Z roota dostępne +> przez `umowa.Last.Ubezpieczenia` (oraz `umowa.Ubezpieczenia` jako delegat). Kluczowe pola: +> - `Ubezpieczenia.Tyub4: Soneta.Kadry.TytulUbezpieczenia4` — **tytuł ubezpieczenia** (decyduje o ZUA +> vs ZZA); pobierany ze słownika `session.GetKadry().TytulyUbezpiecz4.WgKodu[int]`, +> - `Ubezpieczenia.ObowiazkoweOd: Date` — data objęcia ubezpieczeniami społecznymi obowiązkowymi, +> - `Ubezpieczenia.Emerytalne` / `Rentowe` / `Chorobowe` / `Wypadkowe : Soneta.Kadry.Spoleczne` — +> poszczególne społeczne (`Obowiazkowe`, `Dobrowolne`, `DobrowolneOd`, `Do`; `Od` read-only), +> - `Ubezpieczenia.Zdrowotne: Soneta.Kadry.Zdrowotne` — zdrowotne (`ObowiazkoweOd` zapisywalne). +> +> **Reguła ZUA vs ZZA:** zleceniobiorca podlegający ubezpieczeniom **społecznym** (emerytalne/rentowe +> obowiązkowe) → **ZUA**; podlegający **tylko zdrowotnemu** (np. uczeń/student do 26 r.ż., zbieg +> tytułów) → **ZZA**. Worker rozpoznaje to automatycznie po schemacie `UmowaHistoria.Ubezpieczenia`. +> +> **Uwaga (zweryfikowane testem):** świeży zapis ubezpieczeń umowy zlecenie ma **domyślnie** +> `Emerytalne.Obowiazkowe == true` i `Rentowe.Obowiazkowe == true` (schemat ZUA). Aby uzyskać **ZZA**, +> trzeba je **jawnie wyłączyć** (`ub.Emerytalne.Obowiazkowe = false; ub.Rentowe.Obowiazkowe = false;`) +> — samo ustawienie `Zdrowotne.ObowiazkoweOd` nie wystarcza. + +**Worker (publiczny kontrakt):** `Soneta.Deklaracje.ZUS.ZarejestrujUmowyWorker` — worker +**przypisany do `Umowa`** (`DataType = Umowa`). Operuje na zaznaczonych umowach i generuje deklaracje +zgłoszeniowe ZUS. W UI: menu czynności listy umów *Deklaracje ZUS → Przygotuj ZUA i ZZA* (oraz +wyrejestrowanie). + +| Składowa | Typ / sygnatura | Uwaga | +|---|---|---| +| Worker | `ZarejestrujUmowyWorker()` | ctor **bezparametrowy**; `Umowy: Umowa[]` jest **set-only** (ustaw przez inicjalizator) | +| Zaznaczone umowy | `ZarejestrujUmowyWorker.Umowy: Umowa[]` | `[Context]` — umowy do zgłoszenia (write-only) | +| Akcja: zgłoszenie | `object ZarejestrujUmowyWorker.Rejestracja.ZarejestrujUmowy()` | tworzy ZUA/ZZA (i ZCNA dla rodziny — `Pars.ZarejestrujRodzinę`); `Rejestracja()` ctor bezparam. | +| Akcja: wyrejestrowanie | `object ZarejestrujUmowyWorker.Wyrejestrowanie.WyrejestrujUmowy()` | tworzy ZWUA | +| Parametry zgłoszenia | `Rejestracja.Pars: ParamsZ` | **set-only**; `ParamsZ(Context)`; pola bazowe `Okres`/`DataDokumentu`/`DataWypełnienia`/`Kedu` (write-only) + własne `ZarejestrujRodzinę: bool` | +| Parametry wyrejestrowania | `Wyrejestrowanie.Pars: ParamsW` | **set-only**; `ParamsW(Context)`; `Okres`/`DataDokumentu`/`DataWypełnienia`/`Kedu` + `WyrejestrujRodzinę: bool` | + +**Wspólny kontrakt bazowy `ZarejestrujBaseWorker`** (do odczytu wyniku i sterowania okresem): + +| Pole / metoda | Typ / sygnatura | Uwaga | +|---|---|---| +| `Okres` | `Soneta.Types.FromTo` | okres deklaracji | +| `DataDokumentu`, `DataWypełnienia` | `Soneta.Types.Date` | daty na dokumencie | +| `KEDU` | `Soneta.Deklaracje.ZUS.KEDU` | zestaw dokumentów ZUS, do którego trafiają wygenerowane bloki | +| `Deklaracje` | `System.Collections.Generic.IList` | wygenerowane deklaracje (do odczytu po akcji) | +| `CzyJestZUA()`, `CzyJestZZA()` | — | rozpoznanie typu zgłoszenia ze schematu ubezpieczeń | + +**Schemat ubezpieczeń umowy + zgłoszenie ZUA/ZZA:** + +```csharp +var kadry = session.GetKadry(); +var pracownik = kadry.Pracownicy.WgKodu["006"]; +Umowa umowa = pracownik.Umowy.Cast().First(); + +// 1) Schemat ubezpieczeń umowy (historyczny) — ZUA: społeczne obowiązkowe + zdrowotne: +using (var t = session.Logout(editMode: true)) +{ + var ub = umowa.Last.Ubezpieczenia; // UmowaHistoria.Ubezpieczenia + ub.Tyub4 = kadry.TytulyUbezpiecz4.WgKodu[kodTytulu]; // tytuł zleceniobiorcy (klucz int, ze słownika) + ub.ObowiazkoweOd = umowa.Okres.From; // data objęcia społecznymi + ub.Emerytalne.Obowiazkowe = true; + ub.Rentowe.Obowiazkowe = true; + ub.Zdrowotne.ObowiazkoweOd = umowa.Okres.From; + // (ZZA = tylko zdrowotne: JAWNIE ustaw Emerytalne.Obowiazkowe = false i Rentowe.Obowiazkowe = false + // — domyślnie są true; samo zdrowotne nie wystarcza) + t.Commit(); +} +session.Save(); + +// 2) Zgłoszenie ZUA/ZZA na podstawie umowy — worker (DataType Umowa): +// Uwaga: Umowy oraz Pars są SET-ONLY (brak gettera) — ustawiamy je przez inicjalizator, +// a parametry budujemy jako osobny obiekt ParamsZ(context) i przypisujemy do Pars. +var worker = new Soneta.Deklaracje.ZUS.ZarejestrujUmowyWorker { Umowy = new[] { umowa } }; + +var pars = new Soneta.Deklaracje.ZUS.ZarejestrujBaseWorker.ParamsZ(context); +pars.Okres = new FromTo(umowa.Okres.From, Date.MaxValue); +pars.DataDokumentu = umowa.Okres.From; +pars.DataWypełnienia = Date.Today; +pars.ZarejestrujRodzinę = false; + +var rejestracja = new Soneta.Deklaracje.ZUS.ZarejestrujUmowyWorker.Rejestracja { Pars = pars }; +rejestracja.ZarejestrujUmowy(); // generuje ZUA lub ZZA wg schematu ubezpieczeń umowy +session.Save(); +``` + +**Wyrejestrowanie po zakończeniu umowy (ZWUA):** + +```csharp +var parsW = new Soneta.Deklaracje.ZUS.ZarejestrujBaseWorker.ParamsW(context); +parsW.Okres = new FromTo(umowa.Okres.To, umowa.Okres.To); +parsW.DataDokumentu = umowa.Okres.To; +parsW.DataWypełnienia = Date.Today; + +var wyrej = new Soneta.Deklaracje.ZUS.ZarejestrujUmowyWorker.Wyrejestrowanie { Pars = parsW }; +wyrej.WyrejestrujUmowy(); // generuje ZWUA +session.Save(); +``` + +**Pułapki:** +- **Typ zgłoszenia (ZUA vs ZZA) wynika ze schematu `UmowaHistoria.Ubezpieczenia`**, nie z parametru + workera — ustaw poprawnie `Tyub4` + flagi `Spoleczne.Obowiazkowe`/`Zdrowotne` **przed** zgłoszeniem. +- `Ubezpieczenia` jest **historyczne** — zmiana schematu „od daty" to nowy zapis `UmowaHistoria` + (`umowa.Historia.Update(date)` + `UmowaHistorie.AddRow`, jak KADRY-G2/KADRY-A14), nie nadpisywanie bieżącego. +- `Spoleczne.Od` jest **tylko do odczytu** (wyliczane) — datę objęcia społecznymi obowiązkowymi + ustawiasz zbiorczo przez `Ubezpieczenia.ObowiazkoweOd`; na `Zdrowotne` `ObowiazkoweOd` jest + zapisywalne wprost (asymetria — jak w KADRY-A7). +- `Tyub4` to rekord **konfiguracyjnego** słownika `TytulyUbezpiecz4`, klucz `WgKodu[int]` — pobierz + istniejący tytuł zleceniobiorcy, nie twórz „w locie". +- `ZarejestrujUmowyWorker` jest na `Umowa` (umowy), a `ZarejestrujPracownikówWorker` na `Pracownik` + (etatowi) — do zleceniobiorców używaj wersji „Umowy". +- Workery deklaracji uruchamiaj jak każdy worker enova (Context z tej samej sesji); po akcji wołasz + `Session.Save()`. Obsłuż `RowConflictException` z `Save()` (safe-code §4). +- `ZarejestrujRodzinę`/`WyrejestrujRodzinę` sterują dołączeniem ZCNA dla członków rodziny + (`pracownik.Rodzina`, KADRY-A9) — dla zleceniobiorcy zgłoszenie rodziny działa analogicznie do etatu. + diff --git a/soneta-programming/references/domeny/kadry/KADRY08-place.md b/soneta-programming/references/domeny/kadry/KADRY08-place.md new file mode 100644 index 0000000..080a8c9 --- /dev/null +++ b/soneta-programming/references/domeny/kadry/KADRY08-place.md @@ -0,0 +1,751 @@ +# KADRY08 — Płace — naliczanie wypłat + +> Wspólne fakty o typie, podstawowe typy i szablon wzorca: [../kadry.md](../kadry.md). + +> **Model danych.** `Wyplata` (`Soneta.Place.Wyplata`) jest klasą **abstrakcyjną**, root `GuidedRow`, +> tabela `Wyplaty`. Konkretne typy: `WyplataEtat` (etat), `WyplataUmowa` (umowy), `WyplataInne` +> (pozostałe). Każda wypłata należy do jednej **listy płac** (`ListaPlac`, tabela `ListyPlac`) i do +> jednego pracownika. Składniki wynagrodzenia to **elementy** (`WypElement`, tabela `WypElementy`, +> root guided) w kolekcji `Wyplata.Elementy: SubTable`. +> +> **Naliczanie** realizuje publiczny worker `Soneta.Place.NaliczanieSeryjne` (klasa abstrakcyjna +> `partial`) z zagnieżdżonymi klasami: +> - parametry: `NaliczanieSeryjne.Params` (bazowa), `NaliczanieSeryjne.PracownikParams : Params` +> (etat + pozostałe), `NaliczanieSeryjne.UmowaParams : Params` (umowy); +> - wykonawcy: `NaliczanieSeryjne.Pracownika : NaliczanieSeryjne` (wypłaty pracownika), +> `NaliczanieSeryjne.Umowy : NaliczanieSeryjne` (wypłaty z umów). +> +> Wynik to obiekt `Soneta.Place.NaliczanieWypłat` z kolekcją `WszystkieWypłaty: IList` (elementy są +> typu `Wyplata`). **Naliczanie samo zatwierdza zmiany w sesji** (`Nalicz()` wewnętrznie otwiera i +> commituje transakcję edycyjną na sesji pracownika) — utrwalenie w bazie wymaga osobnego +> `session.Save()`. + +### KADRY-H1 — Naliczanie wypłat etatowych (★) + +**Cel:** naliczyć wypłatę etatową (wynagrodzenie zasadnicze etatu + dodatki/potrącenia) dla jednego +pracownika za wskazany okres rozliczeniowy. + +**Klasy, pola i typy:** + +| Element | Typ / sygnatura | Uwaga | +|---|---|---| +| Parametry | `new NaliczanieSeryjne.PracownikParams(Context)` | ctor przyjmuje `Context` (sesja operacyjna) | +| Data wypłaty | `PracownikParams.DataWypłaty: Date` | ustawienie **automatycznie** wylicza `Okres` (z konfiguracji listy) i `MiesiącDeklaracji` | +| Data listy | `PracownikParams.DataListy: Date` | data dokumentu listy płac | +| Okres naliczania | `PracownikParams.Okres: FromTo` | zwykle wyliczony z `DataWypłaty`; można nadpisać | +| Typ naliczenia | `PracownikParams.Naliczanie: TypNaliczenia` | `PłatnaZGóry`/`PłatnaZDołu`; **domyślnie `PłatnaZDołu`** — patrz Pułapki (licencja) | +| Filtr typu wypłaty | `PracownikParams.TypWypłaty: TypWyplaty` | `Wszystkie`/`Etat`/`Umowa`/`Inne` — dla etatu `Etat` lub `Wszystkie` | +| Wykonawca | `new NaliczanieSeryjne.Pracownika(PracownikParams)` | | +| Pracownik | `NaliczanieSeryjne.Pracownika.Pracownik: Pracownik` | komu naliczamy (z tej samej sesji co `Context`) | +| Uruchomienie | `NaliczanieSeryjne.Pracownika.Nalicz(): NaliczanieWypłat` | nalicza i zatwierdza w sesji | +| Wynik | `NaliczanieWypłat.WszystkieWypłaty: IList` (elementy `Wyplata`) | naliczone wypłaty | +| Błędy naliczania | `NaliczanieWypłat.Nienaliczeni: IEnumerable` | pracownicy, dla których się nie udało | + +`TypNaliczenia` (`Soneta.Place`): `PłatnaZGóry = 1`, `PłatnaZDołu = 2`. +`TypWyplaty` (`Soneta.Place`): `Wszystkie = 0`, `Etat = 1`, `Umowa = 2`, `Inne = 3`. + +**Snippet:** + +```csharp +var place = session.GetPlace(); +var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; + +// Parametry naliczania — Context z tej samej sesji co pracownik: +var pars = new NaliczanieSeryjne.PracownikParams(context); +pars.DataWypłaty = new Date(2024, 6, 28); // ustawia Okres i MiesiącDeklaracji automatycznie +pars.DataListy = pars.DataWypłaty; +// pars.Naliczanie pozostaje domyślnie PłatnaZDołu (nie ustawiamy — patrz Pułapki) +pars.TypWypłaty = TypWyplaty.Etat; // tylko wypłaty etatowe + +var naliczanie = new NaliczanieSeryjne.Pracownika(pars) { Pracownik = pracownik }; +NaliczanieWypłat wynik = naliczanie.Nalicz(); // nalicza + commit w sesji + +foreach (Wyplata w in wynik.WszystkieWypłaty) +{ + // w.Pracownik, w.ListaPlac, w.Data, w.MiesiacDeklaracji, w.Wartosc (Currency, do wypłaty) +} + +session.Save(); // utrwalenie w bazie (opcjonalne — bez tego zmiany żyją tylko w sesji) +``` + +**Pułapki:** +- **`Context` musi pochodzić z tej samej sesji co pracownik.** `PracownikParams(Context)` wiąże się z + `Context.Session`; pracownik pobrany z innej sesji spowoduje niespójność. +- **Nie ustawiaj `Naliczanie` jawnie, jeśli nie masz pewności co do licencji.** Setter + `Params.Naliczanie` rzuca wyjątek, gdy licencja nie jest „PL Złoty/Platynowy" — getter wtedy i tak + zwraca `PłatnaZDołu`. Pozostawienie wartości domyślnej (`PłatnaZDołu`) jest bezpieczne. +- `Nalicz()` **otwiera własną transakcję** na sesji pracownika i commituje ją — **nie owijaj** wywołania + w dodatkowy `session.Logout(true)`. Po naliczeniu zmiany są w sesji; do bazy idą dopiero w `Save()`. +- `WszystkieWypłaty` to `IList` nietypowana — iteruj jako `foreach (Wyplata w in ...)`. +- Pracownik w archiwum (`Pracownik.ArchiwumInfo == InformacjeOArchiwum.WArchiwum`) jest pomijany — + `WszystkieWypłaty` będzie puste, bez wyjątku. +- Naliczanie to operacja na danych operacyjnych — sprawdź `wynik.Nienaliczeni` zamiast łapać ogólny + wyjątek; przy `KontynacjaNaliczenia` (tryb seryjny) błędy lądują tam, a nie w `throw`. + +### KADRY-H2 — Naliczanie wypłat z umów (★) + +**Cel:** naliczyć wypłatę z konkretnej umowy cywilnoprawnej (`Soneta.Kadry.Umowa`). + +**Klasy, pola i typy:** + +| Element | Typ / sygnatura | Uwaga | +|---|---|---| +| Parametry | `new NaliczanieSeryjne.UmowaParams(Context)` | jak `PracownikParams`, ale `Naliczanie` jest na sztywno `PłatnaZDołu` (setter rzuca `NotSupportedException`) | +| Data wypłaty / listy / okres | `UmowaParams.DataWypłaty`, `.DataListy`, `.Okres` | jak w KADRY-H1 | +| Wykonawca | `new NaliczanieSeryjne.Umowy(UmowaParams)` | w ctorze ustawia `TypWypłaty = Umowa` | +| Umowa | `NaliczanieSeryjne.Umowy.Umowa: Umowa` | ustawienie umowy ustawia też `Pracownik` z `umowa.Pracownik` | +| Uruchomienie | `NaliczanieSeryjne.Umowy.Nalicz(): NaliczanieWypłat` | | + +**Snippet:** + +```csharp +var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; +Umowa umowa = pracownik.Umowy.Cast().First(); // przykładowa umowa pracownika + +var pars = new NaliczanieSeryjne.UmowaParams(context); +pars.DataWypłaty = new Date(2024, 6, 28); +pars.DataListy = pars.DataWypłaty; + +var naliczanie = new NaliczanieSeryjne.Umowy(pars) { Umowa = umowa }; +NaliczanieWypłat wynik = naliczanie.Nalicz(); + +foreach (Wyplata w in wynik.WszystkieWypłaty) +{ + // w.Typ == TypWyplaty.Umowa; w.Wartosc; w.Elementy +} +session.Save(); +``` + +**Pułapki:** +- **Nie ustawiaj `UmowaParams.Naliczanie`** — setter rzuca `NotSupportedException` (umowy zawsze + „płatne z dołu"). +- Ustawienie `Umowy.Umowa` nadpisuje `Pracownik` na właściciela umowy — nie ustawiaj `Pracownik` ręcznie. +- Pozostałe pułapki jak w KADRY-H1 (Context z tej samej sesji, własna transakcja w `Nalicz()`, `Save()`). + +### KADRY-H3 — Naliczanie pozostałych wypłat (★) + +**Cel:** naliczyć wypłaty „pozostałe" — pojedynczy dodatek/potrącenie (np. premia, zasiłek +jednorazowy) poza zasadniczym wynagrodzeniem etatu, bądź wypłaty typu `Inne`. + +**Mechanizm:** używamy tego samego wykonawcy co KADRY-H1 — `NaliczanieSeryjne.Pracownika` — sterując +zakresem przez `PracownikParams`: +- `PracownikParams.TypWypłaty = TypWyplaty.Inne` — naliczanie tylko składników typu „inne", +- `PracownikParams.Dodatek: DefinicjaElementu` — **zawężenie do jednej definicji** dodatku/potrącenia + (naliczany jest tylko wskazany składnik). + +**Pola i typy:** + +| Element | Typ / sygnatura | Uwaga | +|---|---|---| +| Filtr typu | `PracownikParams.TypWypłaty: TypWyplaty` | `Inne` — pozostałe; `Wszystkie` — łącznie z etatem | +| Pojedynczy składnik | `PracownikParams.Dodatek: DefinicjaElementu` | definicja konkretnego dodatku/potrącenia; `null` = bez zawężenia | + +**Snippet:** + +```csharp +var place = session.GetPlace(); +var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; + +// Definicja konkretnego dodatku/potrącenia (rekord konfiguracyjny): +DefinicjaElementu defDodatku = place.DefElementow.WgKodu["PREMIA"]; // przykładowy kod + +var pars = new NaliczanieSeryjne.PracownikParams(context); +pars.DataWypłaty = new Date(2024, 6, 28); +pars.DataListy = pars.DataWypłaty; +pars.TypWypłaty = TypWyplaty.Inne; // pozostałe wypłaty +pars.Dodatek = defDodatku; // tylko ten składnik + +var naliczanie = new NaliczanieSeryjne.Pracownika(pars) { Pracownik = pracownik }; +NaliczanieWypłat wynik = naliczanie.Nalicz(); + +foreach (Wyplata w in wynik.WszystkieWypłaty) +{ + foreach (WypElement e in w.Elementy) + { + // e.Definicja, e.Nazwa, e.Wartosc (decimal), e.Okres + } +} +session.Save(); +``` + +**Pułapki:** +- `Dodatek` to rekord **konfiguracyjny** `DefinicjaElementu` — pobierz istniejącą definicję + (np. przez klucz kodu w `place.DefElementow`), nie twórz „w locie". +- `TypWyplaty.Inne` i `TypWyplaty.Etat` są rozłączne — by naliczyć etat + dodatki łącznie użyj + `Wszystkie`. +- Pozostałe pułapki jak w KADRY-H1. + +### KADRY-H4 — Przeglądanie/odczyt wypłat za rok (★) + +**Cel:** odczytać naliczone wypłaty pracownika za dany rok i zagregować wartości (suma do wypłaty, +brutto/netto/składki/podatek, sumy składników). + +**Dostęp do wypłat (publiczny kontrakt):** + +| Punkt wejścia | Typ | Uwaga | +|---|---|---| +| `pracownik.Wyplaty` | `SubTable` | wszystkie wypłaty pracownika (klucz `WgPracownik`) | +| `session.GetPlace().Wyplaty.WgPracownik[pracownik]` | `SubTable` | równoważnie z modułu | +| `session.GetPlace().Wyplaty.WgData[date]` | `SubTable` | wypłaty z datą `date` | +| `listaPlac.Wyplaty` | `SubTable` | wypłaty danej listy płac | + +**Pola wypłaty (`Wyplata`) do odczytu:** + +| Pole | Typ | Opis | +|---|---|---| +| `Pracownik` | `Pracownik` | właściciel | +| `ListaPlac` | `ListaPlac` | lista płac (`ListaPlac.Okres: FromTo`, `ListaPlac.DataWyplaty: Date`, `ListaPlac.Zatwierdzona: bool`) | +| `Data` | `Date` | data wypłaty (klucz `WgData`) | +| `MiesiacDeklaracji` | `YearMonth` | miesiąc rozliczenia PIT | +| `MiesiacZUS` | `YearMonth` | miesiąc rozliczenia ZUS | +| `Wartosc` | `Currency` | kwota **do wypłaty** (netto) w PLN | +| `Numer` | `NumerDokumentu` | numer dokumentu (`Numer.NumerPelny`) | +| `Typ` | `TypWyplaty` | etat / umowa / inne | +| `Bufor` | `bool` | wypłata w buforze (niezatwierdzona) | +| `Elementy` | `SubTable` | składniki wynagrodzenia | + +**Kwoty na poziomie wypłaty (`Soneta.Place.Wyplata`, typ `Soneta.Types.Currency`):** `Wartosc` +(kwota **do wypłaty**, PLN), `WartoscCy` (w walucie listy), `DoWypłaty`, `Gotówka`, `Inne`. +Aby otrzymać `decimal`, użyj **`.Value`** (`w.Wartosc.Value`) — `Currency` nie ma jawnego rzutowania +na `decimal`. + +> **Uwaga:** `Wyplata`/`WyplataEtat` **nie udostępnia** publicznych agregatów typu `Brutto`, `Netto`, +> `SkładkiZUS`, `Podatek` jako gotowych właściwości. Brutto/netto/składki/podatek **liczymy sumując +> składniki** z kolekcji `Wyplata.Elementy` (`WypElement.Wartosc`, `WypElement.Netto`, `WypElement.Podatki.*`). + +**Składniki (`WypElement`) i ich struktura podatkowo-składkowa:** + +| Pole | Typ | Opis | +|---|---|---| +| `Definicja` | `DefinicjaElementu` | definicja składnika | +| `Nazwa` | `string` | nazwa składnika | +| `Wartosc` | `decimal` | wartość składnika | +| `Okres` | `FromTo` | okres, za który naliczono | +| `Podatki` | `Podatki` (subrow) | struktura podatków/składek | +| `Podatki.PodstawaZUS` | `decimal` | podstawa ZUS | +| `Podatki.Emerytalna` / `Rentowa` / `Chorobowa` / `Wypadkowa` / `Zdrowotna` | `SkladkaZUS` (subrow) | każda z polami `Podstawa`, `Prac`, `Firma: decimal` | +| `Podatki.Koszty`, `Podatki.Ulga`, `Podatki.ZalFIS` | `decimal` | koszty, ulga, zaliczka PIT | + +**Snippet:** + +```csharp +var place = session.GetPlace(); +var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; + +int rok = 2024; +var od = new Date(rok, 1, 1); +var doD = new Date(rok, 12, 31); + +// Filtr serwerowy po dacie wypłaty (zakres roku) — bez pełnego skanu: +decimal sumaDoWypłaty = 0m; +decimal sumaBrutto = 0m; + +foreach (Wyplata w in pracownik.Wyplaty[(Wyplata x) => x.Data >= od && x.Data <= doD]) +{ + sumaDoWypłaty += w.Wartosc.Value; // kwota do wypłaty (Currency -> decimal przez .Value) + + // brutto/składki/podatek liczymy z elementów (nie ma gotowych agregatów na wypłacie): + foreach (WypElement e in w.Elementy) + { + sumaBrutto += e.Wartosc; // WypElement.Wartosc to decimal + decimal netto = e.Netto; + decimal podstawaZUS = e.Podatki.PodstawaZUS; + decimal zaliczkaPit = e.Podatki.ZalFIS; + } +} +``` + +**Pułapki:** +- `Wyplaty` to tabela **operacyjna guided** — zawsze ograniczaj zakresem czasowym (rok), nie iteruj + całości (`safe-code §6.3`). Filtruj serwerowo przez `SubTable[condition]` po `Data`, nie w pamięci. +- `Wartosc` to `Currency` (kwota do wypłaty); konwersja na `decimal` przez `.Value`. Składnik + `WypElement.Wartosc`/`WypElement.Netto` to już `decimal` — nie myl typów ani znaczeń. +- **Nie ma** gotowych właściwości agregujących (`Brutto`/`Netto`/`SkładkiZUS`/`Podatek`) na `Wyplata` + ani `WyplataEtat` — sumuj składniki z `Wyplata.Elementy` (i ich `Podatki.*`). +- `SkladkaZUS` ma pola `Podstawa`, `Prac`, `Firma` (część pracownika i pracodawcy) oraz właściwość + pomocniczą `Składka` (suma) — wybierz właściwą do potrzeb. +- Filtruj po `Data` (data wypłaty) lub `MiesiacDeklaracji`/`MiesiacZUS` zależnie od potrzeby + raportowej — to różne pojęcia roku (rok wypłaty vs rok deklaracji). + +### KADRY-H5 — Odczyt elementów wypłaty (brutto/składki/podatek/netto) (★) + +**Cel:** odczytać składniki konkretnej **naliczonej** wypłaty (`Soneta.Place.Wyplata`) i wyliczyć +agregaty: brutto, składki ZUS (część pracownika i firmy), zaliczka PIT, netto. + +**Model.** Składniki to `Wyplata.Elementy: SubTable` (`Soneta.Place.WypElement`, tabela +operacyjna guided `WypElementy`). `Wyplata` **nie** ma gotowych agregatów `Brutto`/`Netto`/`SkładkiZUS`/ +`Podatek` — liczymy je z elementów albo przez worker `Wyplata.PITInfoWorker` (patrz niżej). + +**Pola składnika `WypElement` (do odczytu):** + +| Pole | Typ | Opis | +|---|---|---| +| `Definicja` | `DefinicjaElementu` | definicja składnika (konfiguracja) | +| `Nazwa` | `string` | nazwa składnika | +| `Wartosc` | `decimal` | wartość brutto składnika (kwota elementu) | +| `Netto` | `decimal` | wartość netto składnika | +| `DoWypłaty` | `decimal` | kwota do wypłaty z tego składnika | +| `Okres` | `FromTo` | okres, za który naliczono | +| `MiesiacDeklaracji` | `YearMonth` | miesiąc rozliczenia PIT | +| `MiesiacZUS` | `YearMonth` | miesiąc rozliczenia ZUS | +| `Podatki` | `Podatki` (subrow) | struktura podatkowo-składkowa | + +**Subrow `WypElement.Podatki` (`Soneta.Place.Podatki`) — pola istotne:** + +| Pole | Typ | Opis | +|---|---|---| +| `PodstawaZUS` | `decimal` | podstawa wymiaru składek ZUS | +| `Emerytalna` / `Rentowa` / `Chorobowa` / `Wypadkowa` / `Zdrowotna` | `SkladkaZUS` (subrow) | każda z polami `Podstawa`, `Prac`, `Firma: decimal` oraz wyliczanym `Składka` (suma) | +| `Koszty` | `decimal` | koszty uzyskania przychodu | +| `Ulga` | `decimal` | ulga podatkowa (kwota wolna) | +| `ZalFIS` | `decimal` | zaliczka na podatek dochodowy (fiskus) | +| `ZdrowotneDoOdliczenia` | `decimal` | składka zdrowotna do odliczenia | + +Subrow `SkladkaZUS` (`Soneta.Place.SkladkaZUS`): `Podstawa` (podstawa), `Prac` (część pracownika, +`decimal`), `Firma` (część pracodawcy, `decimal`), wyliczane `Składka` (suma) i `JestMinus` (`bool`). + +**Worker-agregator `Wyplata.PITInfoWorker`** (klasa publiczna, `[Context] Wypłata`) — udostępnia gotowe +sumy podatkowe dla wypłaty: + +| Właściwość | Typ | Opis | +|---|---|---| +| `DoOpodatkowania` | `Currency` | suma elementów opodatkowanych (brutto opodatkowane) | +| `Nieopodatkowane` | `Currency` | suma elementów nieopodatkowanych | +| `Razem` | `decimal` | opodatkowane + nieopodatkowane (przychód razem) | +| `NettoRazem` | `decimal` | wynagrodzenie netto razem | +| `NettoOpodat` | `Currency` | netto opodatkowane | +| `SkładkiZUS` | `decimal` | suma składek ZUS pracownika | +| `SkładkaZdrow` | `decimal` | składka zdrowotna | +| `Koszty` | `decimal` | koszty uzyskania razem | +| `Ulga` | `decimal` | ulga podatkowa | +| `ZalFIS` | `decimal` | zaliczka PIT | +| `Dochód_Bez26` / `Dochód_26` | `decimal` | dochód (z podziałem na ulgę „do 26 lat") | + +> `PITInfoWorker.Brutto` i `PITInfoWorker.Netto` są oznaczone `[Obsolete]` — używaj `DoOpodatkowania`, +> `Nieopodatkowane`, `Razem`, `NettoRazem`. Worker przyjmuje też kolekcję `Elementy: IEnumerable` +> (zamiast `Wypłata`) i `WykluczoneElementy: DefinicjaElementu[]`. + +**Snippet (agregacja ręczna z elementów):** + +```csharp +var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; + +decimal brutto = 0m, netto = 0m, zusPrac = 0m, zusFirma = 0m, zalPit = 0m; + +// jedna konkretna wypłata pracownika (np. ostatnia z czerwca): +var od = new Date(2024, 6, 1); +var doD = new Date(2024, 6, 30); +Wyplata wyplata = pracownik.Wyplaty[(Wyplata x) => x.Data >= od && x.Data <= doD].Cast().First(); + +foreach (WypElement e in wyplata.Elementy) +{ + brutto += e.Wartosc; // WypElement.Wartosc to decimal + netto += e.Netto; + zalPit += e.Podatki.ZalFIS; + + zusPrac += e.Podatki.Emerytalna.Prac + e.Podatki.Rentowa.Prac + + e.Podatki.Chorobowa.Prac + e.Podatki.Zdrowotna.Prac; + zusFirma += e.Podatki.Emerytalna.Firma + e.Podatki.Rentowa.Firma + + e.Podatki.Wypadkowa.Firma; +} + +decimal doWyplaty = wyplata.Wartosc.Value; // Currency -> decimal przez .Value +``` + +**Snippet (przez worker — gotowe agregaty):** + +```csharp +var pit = new Wyplata.PITInfoWorker { Wypłata = wyplata }; +decimal brutto = pit.Razem; // przychód razem +decimal nettoR = pit.NettoRazem; +decimal zus = pit.SkładkiZUS; +decimal zdrow = pit.SkładkaZdrow; +decimal zaliczka = pit.ZalFIS; +``` + +**Pułapki:** +- `WypElement.Wartosc`/`Netto`/`DoWypłaty` to `decimal`; `Wyplata.Wartosc` (do wypłaty) to `Currency` — + konwersja przez `.Value` (§10.1). +- `SkladkaZUS.Prac` to część pracownika, `SkladkaZUS.Firma` to część pracodawcy — wybierz właściwą + zależnie od potrzeby (koszt pracownika vs koszt pracodawcy). +- `Wyplaty`/`WypElementy` to tabele operacyjne guided — pobieraj zakresem czasowym (§6.3), nie iteruj + całości. +- Pomijaj elementy stornowane przy sumowaniu, jeśli liczysz stan bieżący — patrz `WypElement.RozliczenieStorna` + (KADRY-H10); naliczona wypłata po korekcie zawiera zarówno element pierwotny (`Wystornowany`) jak i `Stornujący`. + +--- + +### KADRY-H6 — Wypłata zaliczki / dołączenie zaliczki (★) + +**Cel:** naliczyć i wypłacić zaliczkę (wypłata środków „na poczet" przyszłego wynagrodzenia), tworząc +dokument `Soneta.Place.Zaliczka` i element realizacji zaliczki na wypłacie. + +**Model.** Zaliczka to rekord operacyjny `Soneta.Place.Zaliczka` (root guided, tabela `Zaliczki`, +`session.GetPlace().Zaliczki`), implementuje `IBazaZrodlaWyplaty` i `IPowiązanieWypłaty`. Element +realizujący zaliczkę to `WypElementZaliczka.Realizacja : WypElementZaliczka : WypElement`, spłata to +`WypElementZaliczka.Spłata`. Powiązanie elementu z zaliczką: `WypElement.BazaZrodla = Zaliczka`, +`RodzajŹródłaWypłaty.Zaliczka`. + +**Ścieżka publiczna — worker `Soneta.Place.WypłaćZaliczkęWorker`** (na `Soneta.Kadry.Pracownicy`): + +| Element | Typ / sygnatura | Uwaga | +|---|---|---| +| Parametry | `WypłaćZaliczkęWorker.ZalParams : WypElement.Params` | ctor `(Context)`; `Rodzaj == RodzajŹródłaWypłaty.Zaliczka` | +| Definicja | `ZalParams.Definicja: DefinicjaElementu` | definicja elementu zaliczki (z `WypElement.Params`); **musi mieć** `DefinicjaElementu.RodzajZrodla == RodzajŹródłaWypłaty.Zaliczka` — inaczej worker rzuca `WypElement.RodzajDefinicjiException` (np. „Korekta zaliczki podatku" ma `Dodatek`) | +| Data | `ZalParams.Data: Date` | data wypłaty zaliczki (wymagana) | +| Kwota | `ZalParams.Kwota: Currency` | kwota zaliczki (wymagana) | +| Pracownicy | `WypłaćZaliczkęWorker.Pracownicy: Pracownik[]` | dla kogo | +| Akcja | `[Action("Wypłać zaliczkę")] object WypłataZaliczki()` | tworzy `Zaliczka`, nalicza element realizacji | + +**Stan zaliczki (`Zaliczka`):** `Wartosc: Currency`, `Splacono: Currency`, `Pozostaje: Currency` +(`= Wartosc - Splacono`), `Stan: StanZaliczki` (`NieSpłacona`/`CzęściowoSpłacona`/`CałkowicieSpłacona`), +`Realizacje: SubTable` (elementy realizacji), `Spłaty: SubTable` (elementy spłaty). + +**Mechanizm naliczenia** (realizowany przez worker): dla każdego pracownika tworzony jest +`new Zaliczka(pracownik)`, dodawany przez `Zaliczki.AddRow(zaliczka)`, a następnie niskopoziomowy +obiekt `Soneta.Place.NaliczanieWypłat` z `NaliczŹródłoWypłaty = zaliczka` wykonuje `Nalicz()`. +Dołączenie/spłata zaliczki w kolejnej wypłacie etatowej dzieje się automatycznie podczas zwykłego +naliczania (KADRY-H1) — naliczanie wyszukuje niespłacone zaliczki pracownika i generuje element +`WypElementZaliczka.Spłata`. + +**Snippet (uruchomienie workera zaliczki):** + +```csharp +var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; +// definicję zaliczki rozpoznajemy po RodzajZrodla (nie po Kodzie/Nazwie — „Korekta zaliczki podatku" +// to RodzajZrodla.Dodatek, którego worker NIE przyjmie): +DefinicjaElementu defZaliczki = session.GetPlace().DefElementow.Cast() + .First(d => d.RodzajZrodla == RodzajŹródłaWypłaty.Zaliczka); + +var pars = new WypłaćZaliczkęWorker.ZalParams(context) { + Data = new Date(2024, 6, 15), + Kwota = new Currency(1000m, Currency.SystemSymbol), +}; +pars.Definicja = defZaliczki; + +var worker = new WypłaćZaliczkęWorker { Params = pars, Pracownicy = new[] { pracownik } }; +object wynik = worker.WypłataZaliczki(); // tworzy Zaliczka + nalicza; otwiera własną transakcję +session.Save(); +``` + +**Pułapki:** +- `ZalParams.Definicja` to **istniejąca** definicja elementu o `RodzajZrodla == RodzajŹródłaWypłaty.Zaliczka` — + pobierz z `place.DefElementow` (filtruj po `RodzajZrodla`, nie po `Kod`/`Nazwa`), nie twórz w locie. +- Baza Demo może nie zawierać definicji o `RodzajZrodla == Zaliczka` — wtedy worker jest niewykonalny + (w teście: `Assert.Ignore`). +- `Zaliczka.SetWartość(...)` jest `internal` — wartości nie ustawiaj ręcznie; przekaż `ZalParams.Kwota` + do workera. +- `Zaliczka` nie kasuje się bezpośrednio, gdy ma realizacje/spłaty (`OnDeleting` rzuca `RowException`). +- Worker otwiera własną transakcję (`Session.Logout(true)` + `CommitUI`) — nie owijaj dodatkowo; + utrwalenie w bazie przez `Save()`. + +--- + +### KADRY-H7 — Korekta podatków i składek; „Przelicz składki ZUS i podatki" (★) + +**Cel:** ponownie przeliczyć (skorygować) składki ZUS i zaliczki PIT na już naliczonych elementach +wypłat pracownika za dany miesiąc deklaracji — np. po zmianie progu, tytułu ubezpieczenia, korekcie +danych kadrowych. + +**Worker `Soneta.Place.NaliczaniePodatkówMiesięcznie`** (na `Pracownik`/`PracHistoria`): + +| Element | Typ / sygnatura | Uwaga | +|---|---|---| +| Miesiąc | publiczny ctor `(YearMonth miesiącDeklaracji)` (atrybut `[Context(typeof(MiesiącDeklaracji),"Miesiąc")]`) | przy ręcznym wywołaniu przekaż `YearMonth` (np. `pars.Miesiąc`); property odczytu `MiesiącDeklaracji: YearMonth` (get) | +| Klasa parametru | `Soneta.Place.MiesiącDeklaracji : ContextBase` | `MiesiącDeklaracji.Miesiąc: YearMonth` (domyślnie `YearMonth.Today`) | +| Pracownik | `NaliczaniePodatkówMiesięcznie.Pracownik: Pracownik` | `[Context]` | +| `NoTrace` | `bool` | wyłączenie śladu (logu) operacji | +| Akcja | `[Action("Przelicz składki ZUS i podatki")] void PrzeliczPodatki()` | przelicza elementy z danego miesiąca | + +**Mechanizm:** worker iteruje elementy (`WypElementy.WgDaty`) wszystkich pracowników powiązanych +(`Pracownik.PracownicyPowiązani`) w okresie `MiesiącDeklaracji.ToFromTo()`, dla niezablokowanych +(`!element.Podatki.Korekta && element.Wyplata.Bufor`) wykonuje przeliczenie flag i naliczenie +podatków (`NaliczaniePodatków.NaliczRozrzuć()`). Wszystko w transakcji `Session.Logout(true)` + +`Commit()`. + +**Snippet:** + +```csharp +var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; + +var pars = new MiesiącDeklaracji(context) { Miesiąc = new YearMonth(2024, 6) }; +var worker = new NaliczaniePodatkówMiesięcznie(pars.Miesiąc) { Pracownik = pracownik }; +worker.PrzeliczPodatki(); // przelicza składki ZUS i zaliczki PIT za czerwiec 2024 +session.Save(); +``` + +**Pułapki:** +- Elementy z ręczną korektą podatków (`element.Podatki.Korekta == true`) oraz elementy z wypłat + zatwierdzonych (`!Wyplata.Bufor`) są **pomijane** — przeliczane są tylko elementy z bufora. +- `MiesiącDeklaracji.Miesiąc` to `YearMonth` — to miesiąc deklaracji, nie data wypłaty. +- Worker przelicza także pracowników powiązanych (`PracownicyPowiązani`) — operacja może objąć więcej + niż jedną kartotekę. + +--- + +### KADRY-H8 — Rozliczenie pracownika; dochód / roczny dochód (★) + +**Cel:** odczytać dochód z naliczonej wypłaty oraz (dla właścicieli) pobrać roczny dochód do rozliczeń; +opcjonalnie uruchomić rozliczenie pracownika. + +**A. Dochód z wypłaty — `Wyplata.PITInfoWorker`** (publiczny, jak w KADRY-H5). Dochód podatkowy: + +| Właściwość | Typ | Opis | +|---|---|---| +| `Dochód_Bez26` | `decimal` | dochód poza ulgą „do 26 lat" (`= przychód + przychód50 − koszty − koszty50`) | +| `Dochód_26` | `decimal` | dochód objęty ulgą „do 26 lat" | +| `DoOpodatkowania` | `Currency` | podstawa opodatkowania (brutto opodatkowane) | +| `Podstawa` | `decimal` | podstawa naliczenia zaliczki | +| `ZalFIS` | `decimal` | zaliczka PIT | + +Dochód roczny pracownika sumuje się iterując wypłaty roku (KADRY-H4/KADRY-H5) i sumując `Dochód_Bez26 + Dochód_26` +(lub `DoOpodatkowania`) z `PITInfoWorker` każdej wypłaty. + +**B. „Pobierz roczny dochód" — worker `Soneta.Kadry.PobierzDochodRocznyWorker`** (na `Pracownik`/ +`PracHistoria`) — **tylko dla właściciela** (`Pracownik is Wlasciciel`): + +| Element | Typ / sygnatura | Uwaga | +|---|---|---| +| Parametry | property `PobierzDochodRocznyWorker.Pars : PobierzDochodRocznyWorker.Params` | `Pars.Rok: int` (domyślnie rok ubiegły) | +| Pracownik | `PobierzDochodRocznyWorker.Pracownik: Pracownik` | `[Context]` | +| Akcja | `[Action("Pobierz roczny dochód")] void Pobierz()` | zapisuje `PrzychodRyczalt` (RoczDochSkala/RoczDochLiniowy/RoczDochRyczalt) za rok | + +Korzysta z serwisu `IDochódWłaściciela.KwotaDochoduStraty(pracownik, YearMonth, FormaOpodatkowania)`. + +**C. „Rozlicz pracownika" — worker `Soneta.Place.RozliczaniePracownikowWorker`** (na `Pracownik`) — +**tylko dla folderu pracowników zewnętrznych** (`KadryIPlace/Kadry/PracownicyZewnetrzni`): + +| Element | Typ / sygnatura | Uwaga | +|---|---|---| +| Parametry | `RozliczeniePracownikowParams : RozliczanieUmowZewnetrznychParams` | `Okres: FromTo`, `Data: Date` | +| Akcja | `[Action("Rozlicz pracownika")] RozliczanieUmowZewnetrznych Rozlicz()` | rozlicza umowy zewnętrzne pracownika | + +**Snippet (dochód roczny z wypłat):** + +```csharp +var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; +var od = new Date(2024, 1, 1); var doD = new Date(2024, 12, 31); + +decimal dochodRoczny = 0m; +foreach (Wyplata w in pracownik.Wyplaty[(Wyplata x) => x.Data >= od && x.Data <= doD]) +{ + var pit = new Wyplata.PITInfoWorker { Wypłata = w }; + dochodRoczny += pit.Dochód_Bez26 + pit.Dochód_26; +} +``` + +**Pułapki:** +- `PobierzDochodRocznyWorker` działa wyłącznie dla `Wlasciciel` i form opodatkowania ogólnych/ryczałtu — + dla zwykłego pracownika nie ma zastosowania (zwraca bez efektu). +- „Rozlicz pracownika" (`RozliczaniePracownikowWorker`) dotyczy **pracowników zewnętrznych** (umowy + zewnętrzne), nie standardowego rozliczenia płacowego. +- Wewnętrzny `Wyplata.RozliczenieManager` (rozliczanie płatności/należności) jest **niepubliczny** — + rozliczenie płatności inicjuje setter `Wyplata.Bufor` (zejście z bufora), nie wywołuj go bezpośrednio. + +--- + +### KADRY-H9 — Kalkulator wynagrodzeń (brutto↔netto, koszt pracodawcy) (★) + +**Cel:** wyliczyć netto z brutto (lub odwrotnie) oraz całkowity koszt pracodawcy. + +**Brak dedykowanej publicznej klasy „kalkulatora wynagrodzeń"** w publicznym kontrakcie (patrz sekcja +„niewykonalne"). Wyliczenie realizujemy przez **naliczenie próbne** (KADRY-H1/KADRY-H3 — `NaliczanieSeryjne`) i +odczyt agregatów workera `Wyplata.PITInfoWorker` oraz `Wyplata.KosztyUzyskaniaPrzychoduWorker`. + +**Koszt pracodawcy — `Wyplata.PITInfoWorker` + składki firmy z elementów:** +- brutto: `pit.Razem` / `pit.DoOpodatkowania`, +- netto: `pit.NettoRazem`, +- składki pracownika: `pit.SkładkiZUS`, `pit.SkładkaZdrow`, +- zaliczka PIT: `pit.ZalFIS`, +- składki firmy (narzuty pracodawcy): suma `WypElement.Podatki.{Emerytalna,Rentowa,Wypadkowa}.Firma` + (plus FP/FGŚP/FEP) — patrz `WyplataSkładkiWorker` niżej. + +**Agregator składek — `Soneta.Place.WyplataSkładkiWorker`** (publiczny, `[Context] Wypłata` lub +`Elementy: IEnumerable`): udostępnia `Razem: ZestawienieSkładek` z m.in.: + +| Właściwość `ZestawienieSkładek` | Typ | Opis | +|---|---|---| +| `KosztyZUS` | `decimal` | składki ZUS pracownika (emer.+rent.+chor.+wyp., część `Prac`) | +| `FirmaZUS` | `decimal` | składki ZUS pracodawcy (część `Firma`) | +| `Narzuty` | `decimal` | narzuty pracodawcy (`FirmaZUS` + FP.Firma + FGSP.Firma + FEP.Firma) | +| `ZUS` | `decimal` | `KosztyZUS + FirmaZUS` | +| `Emerytalna`/`Rentowa`/… | `ISkładka` | pojedyncze składki (`Podstawa`/`Prac`/`Firma`/`Składka`) | + +Koszt pracodawcy ≈ brutto (`pit.DoOpodatkowania`/`Razem`) + `skladki.Razem.Narzuty`. + +**Snippet (kalkulacja przez naliczenie próbne):** + +```csharp +var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; + +var pars = new NaliczanieSeryjne.PracownikParams(context); +pars.DataWypłaty = new Date(2024, 6, 28); +pars.DataListy = pars.DataWypłaty; +pars.TypWypłaty = TypWyplaty.Etat; + +var nal = new NaliczanieSeryjne.Pracownika(pars) { Pracownik = pracownik }; +NaliczanieWypłat wynik = nal.Nalicz(); + +Wyplata w = (Wyplata)wynik.WszystkieWypłaty[0]; +var pit = new Wyplata.PITInfoWorker { Wypłata = w }; +var skl = new WyplataSkładkiWorker { Wypłata = w }; + +decimal brutto = pit.Razem; +decimal netto = pit.NettoRazem; +decimal kosztPracod = brutto + skl.Razem.Narzuty; // brutto + narzuty pracodawcy +// (jeśli to tylko kalkulacja — nie wywołuj Save(), wynik istnieje w sesji) +``` + +**Pułapki:** +- Brak osobnego „kalkulatora" — wynik zawsze powstaje przez naliczenie i workery agregujące. +- Kalkulacja brutto↔netto zależy od pełnej konfiguracji pracownika (etat, ulgi, koszty, PPK) — nie ma + bezstanowej funkcji „brutto→netto" w publicznym API. +- Jeśli naliczenie ma być tylko próbne, nie wywołuj `Save()` (zmiany zostaną w sesji i znikną z `Dispose`), + albo wykonaj na osobnej sesji „brudnopisowej". + +--- + +### KADRY-H10 — Stornowanie elementów wypłaty; obsługa elementów stornowanych (★) + +**Cel:** zastornować (wycofać/skorygować) element już zatwierdzonej wypłaty i poprawnie odczytać stan +storna. + +**Model.** Storno opisuje rekord `Soneta.Place.StornoElementu` (tabela `StornaElementow`). Element +ma stan `WypElement.StanStorna: StanStornaElementu` oraz dostęp do storna `WypElement.Storno: StornoElementu`. + +Enum `Soneta.Place.StanStornaElementu`: `NieDotyczy=0`, `DoStornowania=1`, `Wystornowany=2`, +`Stornujący=3`, `WycofaneStorno=10` (tylko wyliczany). +Enum `Soneta.Place.RodzajStornaElementu`: `NieDotyczy=0`, `Anulowanie=1`, `Przeliczenie=2`. + +**Pola `WypElement` związane ze storno:** + +| Pole | Typ | Opis | +|---|---|---| +| `StanStorna` | `StanStornaElementu` | bieżący stan storna elementu | +| `StanStornaEx` | `StanStornaElementu` | jw. + `WycofaneStorno` gdy `Wystornowany` historyczny | +| `Storno` | `StornoElementu` | powiązany rekord storna (lub `null`) | +| `RozliczenieStorna` | `bool` | `true` gdy `Wystornowany` lub `Stornujący` (element nie liczy się do bieżącego stanu) | +| `Wystornowany` | `bool` | do elementu naliczono element stornujący | +| `Stornowane` / `Stornujące` | `SubTable` | relacje storn | +| `Korekta` | `bool` | element zmodyfikowany ręcznie przez operatora | +| `UtwórzStorno()` | `WypElement` | (wirtualna) tworzy element stornujący danego typu | + +**Workery oznaczania (publiczne, na `WypElement` / `Wyplata`):** +- `StornoElementu.ElementDoPrzeliczeniaWorker` (na `WypElement`): + - `[Action("Oznacz element do przeliczenia")] ZaznaczElementDoPrzeliczenia()` — `RodzajStornaElementu.Przeliczenie`, + - `[Action("Oznacz element do anulowania")] ZaznaczElementDoAnulowania()` — `RodzajStornaElementu.Anulowanie`, + - `[Action("Wycofaj oznaczenie anulowania lub przeliczenia")] WycofajZaznaczenie()` — kasuje `Storno`. +- `StornoElementu.WypłataDoPrzeliczeniaWorker` (na `Wyplata`): + - `ZaznaczElementyDoPrzeliczenia()` / `WycofajZaznaczenie()` — dla wszystkich elementów wypłaty. +- `StornoElementu.ListaPłacDoPrzeliczeniaWorker` (na `ListaPlac`, z `Params.Definicja` / `WszystkieElementy`). + +**Mechanizm.** Oznaczenie tworzy `StornoElementu` i ustawia element na `DoStornowania`. Właściwe +wytworzenie elementu stornującego (`UtwórzStornujący()`, stan `Wystornowany` na pierwotnym + +`Stornujący` na nowym) następuje przy ponownym naliczeniu wypłaty (KADRY-H1) lub przeliczeniu. Wymagane: +wypłata zatwierdzona (`Wyplata.Zatwierdzona`) i element w stanie `NieDotyczy`. + +**Snippet (oznaczenie do anulowania + przeliczenie):** + +```csharp +Wyplata w = ...; // zatwierdzona wypłata pracownika 006 +WypElement element = w.Elementy.Cast().First(e => e.Definicja.Kod == "PREMIA"); + +// oznacz element do anulowania: +var worker = new StornoElementu.ElementDoPrzeliczeniaWorker { Element = element }; +worker.ZaznaczElementDoAnulowania(); // otwiera własną transakcję +// element.StanStorna == StanStornaElementu.DoStornowania, element.Storno.RodzajStorna == Anulowanie + +// ponowne naliczenie wypłaty (KADRY-H1) wygeneruje element stornujący: +var pars = new NaliczanieSeryjne.PracownikParams(context) { DataWypłaty = w.Data, DataListy = w.Data }; +new NaliczanieSeryjne.Pracownika(pars) { Pracownik = w.Pracownik }.Nalicz(); +session.Save(); +``` + +**Odczyt elementów stornowanych:** + +```csharp +foreach (WypElement e in w.Elementy) +{ + if (e.RozliczenieStorna) continue; // pomiń wystornowane i stornujące przy sumowaniu stanu bieżącego + // ... e to element „żywy" +} +``` + +**Pułapki:** +- Oznaczać można tylko elementy wypłaty **zatwierdzonej** i w stanie `NieDotyczy` (`IsEnabled...` to + egzekwuje); na buforze storno nie ma sensu. +- Storno samo w sobie tylko **oznacza** (`DoStornowania`) — wystornowanie (`Wystornowany`/`Stornujący`) + powstaje dopiero przy ponownym naliczeniu/przeliczeniu. +- Przy sumowaniu kwot bieżących pomijaj `RozliczenieStorna == true`, inaczej policzysz element pierwotny + i jego storno podwójnie. +- Nie można przenieść do bufora wypłaty z elementami `DoStornowania`/`Wystornowany` (rzuca `RowException` + — patrz KADRY-H11). + +--- + +### KADRY-H11 — Anulowanie/usunięcie naliczonej wypłaty (bufor, ponowne naliczenie) (★) + +**Cel:** „cofnąć" naliczoną i zatwierdzoną wypłatę do edycji (bufor) lub usunąć, by naliczyć ponownie. + +**Model.** Wypłata ma flagi `Wyplata.Bufor: bool` (niezatwierdzona/edytowalna) oraz `Wyplata.Zatwierdzona: bool` +(odwrotność `Bufor`). Zejście z bufora = zatwierdzenie; powrót do bufora = otwarcie do edycji. + +**Workery (publiczne, na `Wyplata`):** + +| Worker / akcja | Sygnatura | Efekt | +|---|---|---| +| `Wyplata.ZatwierdźWorker` | property `Lista: Wyplata`; `[Action("Zatwierdź wypłatę")] void Zatwierdź()` | `Zatwierdzona = true` (zejście z bufora) | +| `Wyplata.OtwórzWorker` | property `Wypłata: Wyplata`; `[Action("Przenieś do bufora")] void Otwórz()` | `Zatwierdzona = false` (powrót do bufora) | + +Obie akcje działają w transakcji `Session.Logout(true)` + `Commit()`. **Uwaga na nazwy property:** +worker zatwierdzania przypina wypłatę przez `ZatwierdźWorker.Lista`, a otwierania — przez +`OtwórzWorker.Wypłata`. `IsEnabled...` wymaga `Wyplata.CanBufor` — ale `CanBufor` jest **`protected`** +(niedostępny z dodatku); stan czytaj przez publiczne `Wyplata.Bufor` / `Wyplata.Zatwierdzona`. + +**Bezpośrednia flaga `Wyplata.Bufor`:** +- setter `Bufor` rzuca `ColReadOnlyException`, gdy `!CanBufor`; +- zejście z bufora (`Bufor=false`) wyzwala rozliczenie płatności (wewnętrzny `RozliczenieManager`); +- `IsReadOnlyBufor()` true gdy brak praw / `!CanBufor` / wyłączone „ZatwierdzanieFlagą" / lista zatwierdzona. + +**Usunięcie / ponowne naliczenie.** Aby przeliczyć od nowa: przenieś wypłatę do bufora +(`OtwórzWorker.Otwórz()`), a następnie wykonaj ponowne naliczenie (KADRY-H1 — `NaliczanieSeryjne`), które +nadpisze elementy. Usunięcie samej wypłaty realizuje standardowe `Row.Delete()` w transakcji (gdy +dozwolone — wypłata w buforze, bez powiązań blokujących). + +**Snippet (powrót do bufora + ponowne naliczenie):** + +```csharp +Wyplata w = ...; // zatwierdzona wypłata pracownika 006 + +// 1) przenieś do bufora: +new Wyplata.OtwórzWorker { Wypłata = w }.Otwórz(); // Zatwierdzona = false + +// 2) ponowne naliczenie (KADRY-H1): +var pars = new NaliczanieSeryjne.PracownikParams(context) { + DataWypłaty = w.Data, DataListy = w.Data, TypWypłaty = TypWyplaty.Etat, +}; +new NaliczanieSeryjne.Pracownika(pars) { Pracownik = w.Pracownik }.Nalicz(); +session.Save(); +``` + +**Snippet (usunięcie wypłaty z bufora):** + +```csharp +using (ITransaction t = session.Logout(true)) { + w.Bufor = true; // upewnij się, że w buforze (lub OtwórzWorker) + w.Delete(); + t.Commit(); +} +session.Save(); +``` + +**Pułapki:** +- `Otwórz()` rzuca `RowException`, gdy wypłata nie jest zatwierdzona; `Zatwierdź()` — gdy już + zatwierdzona. Sprawdzaj `IsEnabled...` / stan przed wywołaniem. +- `UpdateBufor()` rzuca `RowException`, gdy na wypłacie są elementy `DoStornowania`/`Wystornowany` — + najpierw wycofaj oznaczenia storna (KADRY-H10) lub dokończ przeliczenie. +- Zejście z bufora wykonuje rozliczenie płatności i kopiowanie kursu — nie traktuj go jak zwykłej + zmiany pola. +- Operacje płacowe to dane operacyjne — łap `RowException`/`RowConflictException` z `Save()` (§4, §9), + nie ogólny `Exception`. + diff --git a/soneta-programming/references/domeny/kadry/KADRY09-listy-place.md b/soneta-programming/references/domeny/kadry/KADRY09-listy-place.md new file mode 100644 index 0000000..1f5a285 --- /dev/null +++ b/soneta-programming/references/domeny/kadry/KADRY09-listy-place.md @@ -0,0 +1,550 @@ +# KADRY09 — Listy płac, przelewy, wydruki + +> Wspólne fakty o typie, podstawowe typy i szablon wzorca: [../kadry.md](../kadry.md). + +> **Model.** Lista płac to dokument operacyjny `Soneta.Place.ListaPlac` (root `GuidedRow`, +> tabela `ListyPlac`, `session.GetPlace().ListyPlac`). Trzyma kolekcję wypłat +> `ListaPlac.Wyplaty: SubTable`. Każda `Wyplata` (root `GuidedRow`, tabela `Wyplaty`) +> wskazuje wstecz listę (`Wyplata.ListaPlac: IRow`) i pracownika (`Wyplata.Pracownik: IRow`). +> Wzorzec listy to `DefinicjaListyPlac` (tabela konfiguracyjna `DefListPlac`, +> `session.GetPlace().DefListPlac`, dostęp `WgSymbolu`/`WgNazwy`). +> +> **`Wyplata` jest abstrakcyjna** — konkretne typy: `WyplataEtat`, `WyplataUmowa`, `WyplataInne` +> (ctor `(ListaPlac listaplac, Pracownik pracownik)` oraz wariant z `IPowiązanieWypłaty`). +> W praktyce wypłat **nie tworzy się ręcznie** — robi to worker naliczania. + +### KADRY-I1 — Naliczanie/generowanie list płac (★) + +**Cel:** utworzyć listę płac dla wybranego okresu i naliczyć na niej wypłaty pracowników +(etat/umowy), tak by `ListaPlac.Wyplaty` zawierała policzone `Wyplata`. + +**Warianty:** + +| Wariant | Mechanizm | Uwaga | +|---|---|---| +| Ręczne utworzenie pustej listy | `new ListaPlac()` + `ListyPlac.AddRow(lp)` + pola | sterujesz wszystkim sam | +| Naliczanie wypłat na istniejącej liście | worker `Soneta.Place.NaliczanieWypłat` (akcja `Nalicz`) | tworzy `Wyplata*` i liczy elementy | +| Naliczanie planowanych list (zbiorczo) | worker `Soneta.Place.NaliczaniePlanowanychListPłacWorker` (akcja `Nalicz`) | wg `DefinicjaPlanowanejListyPłac` | + +**Pola i typy (`ListaPlac`, kolejność ustawiania jest istotna):** + +| Pole | Typ | Uwaga | +|---|---|---| +| `Definicja` | `Soneta.Place.DefinicjaListyPlac` | wzorzec listy; ustawić **pierwsze** po `AddRow` | +| `Wydzial` | `Soneta.Kadry.Wydzial` | tylko gdy `Definicja.Wydzial == true` | +| `Seria` | `string` | tylko gdy `Definicja.Seria == true` | +| `Data` | `Soneta.Types.Date` | data naliczania listy | +| `Naliczanie` | `Soneta.Place.TypNaliczenia` | wartości: `PłatnaZGóry`/`PłatnaZDołu`; **nie ustawiaj** — setter rzuca bez licencji „PL Złoty" | +| `DataWyplaty` | `Soneta.Types.Date` | data postawienia środków; wyznacza mies./rok | +| `MiesiacZUS` | `Soneta.Types.YearMonth` | miesiąc rozliczenia ZUS | +| `Okres` | `Soneta.Types.FromTo` | okres listy; **po** `DataWyplaty` i `Naliczanie` | +| `MiesWstecz` | `int` | | +| `Wyplaty` | `SubTable` | wypełniana przez worker naliczania | +| `Numer` | `Soneta.Core.NumerDokumentu` | nadawany automatycznie | +| `Bufor` / `Zatwierdzona` | `bool` | stan dokumentu | + +**Worker `Soneta.Place.NaliczanieWypłat`** — `[Context]`: `Context`, `ListaPłac: ListaPlac`, +`Pracownik: Soneta.Kadry.Pracownik`; akcja `NaliczanieWypłat Nalicz()`; właściwości +wynikowe m.in. `Wypłaty: IList`, `Nienaliczeni: IEnumerable`, +`DataWypłaty/DataListy/DataZUS: Date`, `Okres: FromTo`, `Naliczanie: TypNaliczenia`. + +**Worker `Soneta.Place.NaliczaniePlanowanychListPłacWorker`** — `[Context]`: +`Pracownik: Pracownik[]`; `Params Pars` z polami `Definicja: DefinicjaPlanowanejListyPłac`, +`DataWypłaty: Date`, `Okres: FromTo`, `Naliczanie: TypNaliczenia`, `TypWypłaty: TypWyplaty`, +`MiesiącZUS/MiesiącDeklaracji: YearMonth`, `Seria: string`, `MiesWstecz: int`, +`UwzgledniajNieZatwierdzoneListyPlac/EdycjaMiesiącaZUS: bool`; +akcja `NaliczaniePlanowanychListPłac Nalicz()`. + +**Snippet (ręczne utworzenie listy + naliczenie wypłaty pracownika):** + +```csharp +using Soneta.Business; +using Soneta.Place; +using Soneta.Kadry; +using Soneta.Types; + +var place = session.GetPlace(); + +// 1. Wzorzec listy płac (definicja konfiguracyjna). +var def = place.DefListPlac.WgSymbolu["ETAT"] + ?? throw new BusException("Brak definicji listy płac".Translate()); + +// 2. Pusta lista płac — KOLEJNOŚĆ: AddRow → Definicja → daty/naliczanie → Okres. +var lp = new ListaPlac(); +place.ListyPlac.AddRow(lp); +lp.Definicja = def; // pierwsze po AddRow +lp.Data = new Date(2026, 6, 30); +lp.DataWyplaty = new Date(2026, 6, 30); // wyznacza miesiąc/rok +lp.MiesiacZUS = new YearMonth(2026, 6); +lp.Okres = new FromTo(new Date(2026, 6, 1), new Date(2026, 6, 30)); // po DataWyplaty +// Uwaga: NIE ustawiaj lp.Naliczanie — setter rzuca bez licencji „PL Złoty"; getter ma sensowny domyślny. + +// 3. Naliczenie wypłaty pracownika — sprawdzona ścieżka to NaliczanieSeryjne (patrz sekcja H); +// naliczona wypłata zostaje automatycznie powiązana z bieżącą listą płac. +var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; +var pars = new NaliczanieSeryjne.PracownikParams(context) // context: w UI z workera, w teście z TestBase +{ + DataWypłaty = new Date(2026, 6, 30), + DataListy = new Date(2026, 6, 30), + TypWypłaty = TypWyplaty.Etat, +}; +var wynik = new NaliczanieSeryjne.Pracownika(pars) { Pracownik = pracownik }.Nalicz(); + +// 4. Powiązanie wypłaty z listą jest dwukierunkowe (Wyplata.ListaPlac / Wyplata.Pracownik): +foreach (Wyplata w in wynik.WszystkieWypłaty) +{ + // w.ListaPlac, w.Pracownik +} + +session.Save(); +``` + +**Pułapki:** +- **Kolejność pól krytyczna:** `Okres` i `MiesWstecz` ustaw **po** `DataWyplaty` i `Naliczanie` + (wzajemne zależności wyliczeń) — patrz wzorzec w kodzie naliczania list. +- `Wydzial`/`Seria` ustawiaj **warunkowo** wg `Definicja.Wydzial`/`Definicja.Seria` — inaczej + ryzyko niespójności klucza `WgDefinicja`. +- Wypłat **nie twórz przez `new WyplataEtat(...)` ręcznie** — naliczaj. Sprawdzoną ścieżką + naliczania jest **`NaliczanieSeryjne.Pracownika(...).Nalicz()`** (sekcja H); sam worker + `NaliczanieWypłat { ListaPłac, Pracownik }.Nalicz()` w bazie Demo potrafi zwrócić pustą listę. +- `Wyplata.ListaPlac`/`Wyplata.Pracownik` to relacje **tylko do odczytu** — powiązania nie ustawisz + setterem; powstają w trakcie naliczania. +- `ListyPlac` to tabela operacyjna guided — przy odczycie filtruj zakresem (`WgDatyWyplaty`, + `WgOkresu`, `WgDefinicja`), nie skanuj całości (safe-code §6.3). +- `Wyplata.ListaPlac`/`Wyplata.Pracownik` to `IRow` (relacje interfejsowe) — porównuj/rzutuj + świadomie. + +### KADRY-I2 — Drukowanie/PDF kwitków (pasków) wypłaty (★) + +**Cel:** wygenerować pasek (kwitek) wypłaty pracownika do PDF. + +**Mechanizm.** Wydruk realizuje serwis **`IReportService`** (namespace `Soneta.Business.UI`, +identycznie jak wydruki handlowe — patrz `handel.md` rozdz. 12). Wzorce pasków to +szablony `*.repx` zarejestrowane atrybutem `[DxReport]` w assembly +**`Soneta.KadryPlace.Reports`** dla `DataType = typeof(Soneta.Place.Wyplata)`: + +| Wzorzec (ReportName) | Plik szablonu (`TemplateFileName`) | `DataType` | +|---|---|---| +| „Pasek wypłaty" | `PasekWyplaty.repx` | `Soneta.Place.Wyplata` | +| „Duży pasek wypłaty" | `DuzyPasekWyplaty.repx` | `Soneta.Place.Wyplata` | +| „Paski wypłat" (zbiorczy) | `PaskiWyplaty.repx` | `Soneta.Place.ListaPlac` | + +**API (`IReportService` / `ReportResult` — `Soneta.Business.UI`):** +`Stream GenerateReport(ReportResult rr)`, +`ReportResult.TemplateFileName: string`, `.DataType: Type`, +`.OutputFormat: ReportFormats` (`PDF`), `.Context: Context`, `.Target: ReportTargets`. + +**Snippet (pasek jednej wypłaty do strumienia PDF):** + +```csharp +using Soneta.Business.UI; // IReportService, ReportResult, ReportFormats +using Soneta.Place; + +var raporty = session.GetRequiredService(); + +var context = new Context(session.Context); +context.Set(wyplata); // pojedyncza Wyplata + +var rr = new ReportResult { + TemplateFileName = "PasekWyplaty.repx", + DataType = typeof(Wyplata), + OutputFormat = ReportFormats.PDF, + Context = context, +}; + +using Stream pdf = raporty.GenerateReport(rr); // pierwsze 4 bajty == "%PDF" +``` + +**Pułapki:** +- `IReportService` pobierasz z kontenera: `session.GetRequiredService()` + (potrzebne `using Microsoft.Extensions.DependencyInjection;`). Serwis i silnik raportów + (DevExpress) oraz szablony pasków z `Soneta.KadryPlace.Reports` są dostępne **transytywnie** — + generowanie PDF działa bez dodatkowych referencji (wzorzec jak w `handel.md` rozdz. 12). +- Poprawny PDF zaczyna się od bajtów `"%PDF"` — to wygodna asercja w teście. +- Druk na fizyczną drukarkę (`Target = Printer`, `PrintReport`) wymaga sprzętu — NIE testować. + +### KADRY-I3 — Drukowanie/PDF list płac (★) + +**Cel:** wygenerować wydruk całej listy płac (pełna lista, zestawienie wypłat) do PDF. + +**Mechanizm.** Identyczny jak KADRY-I2 — `IReportService.GenerateReport`, szablony `[DxReport]` +w `Soneta.KadryPlace.Reports`, dla `DataType = typeof(Soneta.Place.ListaPlac)` / +`typeof(Soneta.Place.ListyPlac)`: + +| Wzorzec (ReportName) | Plik szablonu | `DataType` | +|---|---|---| +| „Pełna lista płac" | `PelnaListaPlac.repx` | `Soneta.Place.ListaPlac` | +| „Wspólna pełna lista płac" | `Wspolnapelnalistaplac.repx` | `Soneta.Place.ListyPlac` (zbiór) | +| „Paski wypłat" | `PaskiWyplaty.repx` | `Soneta.Place.ListaPlac` | +| Zestawienie wypłat | `ZestawienieWyplat.repx` | `Soneta.Place.ListaPlac` | + +**Snippet (pełna lista płac → PDF):** + +```csharp +using Soneta.Business.UI; +using Soneta.Place; + +var raporty = session.GetRequiredService(); + +var context = new Context(session.Context); +context.Set(listaPlac); // ListaPlac + +var rr = new ReportResult { + TemplateFileName = "PelnaListaPlac.repx", + DataType = typeof(ListaPlac), + OutputFormat = ReportFormats.PDF, + Context = context, +}; + +using Stream pdf = raporty.GenerateReport(rr); +``` + +**Pułapki:** +- Mechanizm i dostępność serwisu — jak w KADRY-I2 (działa transytywnie, bez dodatkowych referencji). +- Lista musi być policzona (mieć `Wyplaty`) — inaczej wydruk będzie pusty. +- **Niektóre szablony list wymagają pełnego kontekstu danych.** W bazie Demo wzorzec + `PelnaListaPlac.repx` potrafi rzucić `InvalidOperationException` („Problem z przygotowaniem + raportu") na sztucznie utworzonej liście — to ograniczenie konkretnego szablonu/kontekstu, nie + brak referencji (pasek wypłaty `PasekWyplaty.repx` z KADRY-I2 generuje się poprawnie). +- Do wydruku zbiorczego wielu list ustaw `DataType = typeof(Soneta.Place.ListyPlac)` i przekaż + zbiór przez `Context.Set(...)` / `ReportResult.Rows`. + + +### KADRY-I4 — Generowanie przelewów wynagrodzeń (przygotowanie przelewów) (★) + +**Cel:** z naliczonej, zatwierdzonej listy płac wygenerować dokumenty przelewu wynagrodzeń +(do paczki przelewów), tak by wypłaty pracowników trafiły do zapłaty/preliminarza i mogły zostać +wyeksportowane do banku (KADRY-I5). + +> **Dwie różne klasy `Wyplata` — nie myl ich.** W domenie współistnieją: +> - **`Soneta.Place.Wyplata`** (moduł `PlaceModule`, tabela `Wyplaty`) — *naliczona wypłata +> pracownika* (wynik naliczania z sekcji H/KADRY-I1); to dokument **płacowy** ze składnikami +> (`Elementy`), powiązany z listą płac (`Wyplata.ListaPlac`). +> - **`Soneta.Kasa.Wyplata`** (moduł `KasaModule`, tabela `Wyplaty`/`Zaplaty`) — *zapłata kasowa* +> (rozchód środków). To **ona** implementuje `IDokumentPlatny`/`IDokumentKsiegowalny`, ma pola +> rozliczeniowe (`DoRozliczenia`, `Stan`, `StanRozliczenia`, `KwotaRozliczona`, `Rozliczono`, +> `Rozrachunki`, `Zaplaty`, `PreliminarzPoz`, `PozycjePrzelewu`, `BlokadaPrzelewow`). +> +> Mechanizm „z wypłaty do przelewu” łączy oba światy: worker płacowy czyta `Place.Wyplata` z listy +> płac i tworzy dokumenty przelewu w module Kasa (`Soneta.Kasa.PrzelewBase`, w paczce `PaczkaPrzelewow`). + +**Mechanizm (publiczny kontrakt — worker płacowy):** sprawdzoną ścieżką tworzenia przelewów z +wynagrodzeń jest worker **`Soneta.Place.ListaPlac.PrzygotujPrzelewyWorker`** (assembly +`Soneta.KadryPlace`, akcja menu *„Przygotuj przelewy”* na liście/listach płac). Kontekstem +działania jest **lista płac** (`Soneta.Place.ListaPlac`) — przygotowuje przelewy dla zatwierdzonych +wypłat tej listy. + +**Parametry — `PrzygotujPrzelewyWorker.Params`:** + +| Pole | Typ | Uwaga | +|---|---|---| +| `Data` | `Soneta.Types.Date` | data dokumentów przelewu | +| `Paczka` | `Soneta.Kasa.PaczkaPrzelewow` | istniejąca paczka, do której trafią przelewy (opcjonalnie) | +| `DefinicjaPaczki` | `Soneta.Kasa.DefinicjaPaczkiPrzelewu` | definicja, wg której utworzyć nową paczkę (gdy `Paczka == null`) | +| `ZRachunku` | `Soneta.Kasa.RachunekBankowyFirmy` | rachunek firmy obciążany przelewami | +| `Łączone` | `bool` | łączenie przelewów do jednego podmiotu w jeden dokument | +| `ListyPłac` | `string` | opis/oznaczenie list płac (informacyjnie w tytule) | +| `ModyfikacjaTytułów` | `bool` | czy nadpisać tytuły przelewu (`Tytułem1`/`Tytułem2`) | +| `Tytułem1`, `Tytułem2` | `string` | tytuł przelewu (gdy `ModyfikacjaTytułów == true`) | +| `ZEwidencjiZrodlowej` | `bool` | bierz dane rachunku z ewidencji źródłowej | + +**Akcja:** `object PrzygotujPrzelewy()` — tworzy w sesji dokumenty `Soneta.Kasa.PrzelewBase` +(tabela `Przelewy`) w paczce `PaczkaPrzelewow`; utrwalenie w bazie wymaga `session.Save()`. + +**Model dokumentu przelewu (`Soneta.Kasa.PrzelewBase`, tabela `Przelewy`, root `GuidedRow`):** + +| Pole | Typ | Opis | +|---|---|---| +| `Kwota` | `Soneta.Types.Currency` | kwota przelewu | +| `Podmiot` | `Soneta.Kasa.IPodmiotKasowy` | odbiorca (m.in. `Pracownik`, `ZUS`, `UrzadSkarbowy`, `Bank`, `Kontrahent`) | +| `Rachunek` | `Soneta.Kasa.RachunekBankowyPodmiotu` | rachunek odbiorcy | +| `RachunekZleceniodawcy` | `Soneta.Kasa.NumerRachunku` | rachunek firmy (obciążany) | +| `Data` | `Soneta.Types.Date` | data przelewu | +| `Definicja` | `Soneta.Core.DefinicjaDokumentu` | definicja dokumentu | +| `Numer` | `Soneta.Core.NumerDokumentu` | numer (nadawany automatycznie) | +| `Tytulem1`, `Tytulem2` | `string` | tytuł przelewu | +| `Typ2` | `Soneta.Kasa.TypPrzelewu2` | wariant przelewu (zwykły / **MPP** / itp.) | +| `PaczkaPrzelewow` | `Soneta.Kasa.PaczkaPrzelewow` | paczka, do której należy przelew | +| `Bufor` / `Zatwierdzony` | `bool` | stan dokumentu | +| `Exported` | `bool` | czy wyeksportowany (po KADRY-I5 — `true`, blokuje edycję) | + +**Przelewy okresowe / MPP:** +- **MPP (mechanizm podzielonej płatności)** to *wariant* przelewu — wyrażany przez + `PrzelewBase.Typ2: Soneta.Kasa.TypPrzelewu2` (oraz na `Kasa.Wyplata` polem `KwotaMPP`, + `MozliweMechanizmyMPP`). Dla wynagrodzeń MPP zwykle nie dotyczy (to mechanizm faktur VAT), ale + kontrakt go przewiduje. +- **Przelewy okresowe** (cykliczne płatności np. składek z list) realizuje osobny worker + księgowy `Soneta.Ksiega.Kasowe.NaliczaniePrzelewowOkresowych` (poza zakresem płac pracownika). + +**Powiązanie z wypłatą / preliminarzem (publiczne kolekcje na `Pracownik`):** + +| Kolekcja na `Pracownik` | Typ | Zawiera | +|---|---|---| +| `Pracownik.Przelewy` | `SubTable` | przelewy pracownika | +| `Pracownik.DokumentyPreliminarza` | `SubTable` | dokumenty preliminarza | +| `Pracownik.DokumentyRozliczeniowe` | `SubTable` | dokumenty rozliczeniowe | +| `Pracownik.Rozrachunki` | `SubTable` | rozrachunki | +| `Pracownik.Rachunki` | `SubTable` | rachunki bankowe pracownika | + +> **Korekta (zweryfikowane kompilacją + skanem DLL):** `Pracownik.Platnosci` **nie istnieje** w publicznym +> kontrakcie kartoteki pracownika — kolekcja `Platnosci` występuje tylko na interfejsie +> `Soneta.Kasa.IDokumentPlatny` (np. `Kasa.Wyplata.Platnosci`), nie na `Pracownik`. Płatności podmiotu +> czytaj przez `Pracownik.Rozrachunki` / `Pracownik.DokumentyRozliczeniowe`. + +**Snippet (worker — w UI/teście z dostępnym `Context`):** + +```csharp +using Soneta.Business; +using Soneta.Place; // ListaPlac, ListaPlac.PrzygotujPrzelewyWorker +using Soneta.Kasa; // PaczkaPrzelewow, PrzelewBase, RachunekBankowyFirmy +using Soneta.Types; + +// listaPlac: zatwierdzona lista płac z naliczonymi wypłatami (sekcja KADRY-I1) +var pars = new ListaPlac.PrzygotujPrzelewyWorker.Params +{ + Data = Date.Today, + // Paczka = istniejacaPaczka, // albo nowa wg DefinicjaPaczki: + // DefinicjaPaczki = session.GetKasa().DefPaczekPrzelewow.WgSymbolu["..."], + // ZRachunku = rachunekFirmy, // RachunekBankowyFirmy + Łączone = false, +}; + +var worker = new ListaPlac.PrzygotujPrzelewyWorker { Pars = pars }; +// kontekstem workera jest lista płac; uruchomienie akcji: +worker.PrzygotujPrzelewy(); + +session.Save(); // utrwalenie dokumentów przelewu w bazie +``` + +**Pułapki / ograniczenia (bądź szczery):** +- **`Place.Wyplata` ≠ `Kasa.Wyplata`** — pola rozliczeniowe (`DoRozliczenia`, `Stan`, + `StanRozliczenia`, `Rozrachunki`, `BlokadaPrzelewow`) są na **kasowej** `Soneta.Kasa.Wyplata` + (`IDokumentPlatny`), nie na płacowej. Skanując „Wyplata” trafia się na kasową. +- **Lista płac musi być zatwierdzona i naliczona** — `PrzygotujPrzelewy` na pustej/niezatwierdzonej + liście nie ma czego przelać. +- **Wymaga konfiguracji modułu Kasa** — definicji paczki przelewów (`DefinicjaPaczkiPrzelewu`), + rachunku firmy (`RachunekBankowyFirmy`) oraz rachunku pracownika (`Pracownik.Rachunki`). Brak + rachunku odbiorcy → przelew nie powstanie albo będzie niekompletny. **W bazie Demo te elementy + mogą nie być skonfigurowane**, dlatego generowanie przelewów w teście jednostkowym jest + niepewne (patrz spec testowy). +- Worker **sam zatwierdza zmiany w sesji** (otwiera transakcję) — nie owijaj w dodatkowy + `session.Logout(true)`; do bazy idą w `Save()`. +- `PrzelewBase.Podmiot`/`Powiazanie` to relacje **interfejsowe** (`IRow`/`IPodmiotKasowy`) — + rzutuj świadomie. +- `Przelewy` to tabela operacyjna guided — przy odczycie filtruj zakresem (safe-code §6.3). + +--- + +### KADRY-I5 — Eksport wynagrodzeń do banku / pliku przelewów (★) + +> **UWAGA — operacja plikowa/integracyjna.** Eksport zapisuje **fizyczny plik** w formacie +> bankowym (Elixir, MT940-pochodne, formaty walutowe). To wejście/wyjście do systemu zewnętrznego — +> **nie jest to przedmiot testu jednostkowego** (zależy od ścieżki na dysku, formatu banku, +> sterownika eksportu i — przy wysyłce online — od sieci). Dokumentujemy **model i publiczny +> kontrakt**, a sam eksport pliku oznaczamy jako nietestowalny jednostkowo. + +**Cel:** wyeksportować przygotowane przelewy (KADRY-I4) do pliku przelewów dla systemu bankowości +elektronicznej. + +**Mechanizm (publiczny kontrakt — worker Kasa):** worker **`Soneta.Kasa.EksportPrzelewowWorker`** +(akcja menu *„Eksport przelewów”*, metoda `Eksport()`), sterowany przez +`Soneta.Kasa.EksportPrzelewowParams`. + +> **Korekta (zweryfikowane kompilacją):** `EksportPrzelewowParams` **nie ma konstruktora +> bezparametrowego** — wymaga `EksportPrzelewowParams(Context ctx, RachunekBankowyFirmy rachunek, PrzelewBase[] przelewy)`. +> Co więcej, **sam konstruktor waliduje rachunek** i rzuca `System.ApplicationException` +> („Eksport niemożliwy. Nie wskazano rachunku w filtrach listy.”), gdy `rachunek == null`. Dlatego nie da się +> utworzyć parametrów samym inicjalizatorem obiektu. W teście jednostkowym kontrakt API weryfikuj **refleksją** +> (istnienie typu, sygnatura konstruktora, property `FileName`/`Params`, metoda `Eksport`), bez instancjonowania. + +**Parametry — `Soneta.Kasa.EksportPrzelewowParams`:** + +| Pole | Typ | Uwaga | +|---|---|---| +| `FileName` | `string` | **ścieżka pliku wyjściowego** — operacja na dysku | +| `AppendToFile` | `bool` | dopisanie do istniejącego pliku | +| `PrzelewyZgodne` | `IList` | przelewy do wyeksportowania | +| `Rachunek` | `Soneta.Kasa.RachunekBankowyFirmy` | rachunek firmy (zleceniodawca) | +| `PrmDataPrzelewow` | `Soneta.Types.Date` | data realizacji | +| `PrmNumerPaczki` | `string` | numer paczki | +| `PrmZakres` | `Soneta.Kasa.ZakresEksportuPrzelewow` | zakres (wszystkie / wg paczki / zaznaczone) | +| `EksportujWBuforze` | `bool` | uwzględnij przelewy w buforze | +| `InfoBank`, `InfoFormatKraj`, `InfoFormatWalutowy`, `InfoRachunekBankowy` | `string` | parametry formatu/banku | +| `WithoutHelper` | `bool` | tryb bez kreatora | + +**Akcja:** `object Eksport()` — zapisuje plik wg `FileName`. Po eksporcie przelewy są oznaczane +jako wyeksportowane (`PrzelewBase.Exported == true`, blokada dalszej edycji). + +**Powiązane (kontekst):** +- Eksport całych **paczek**: worker `Soneta.Kasa.EksportPaczekPrzelewowWorker`. +- Eksport przelewów PPK z pulpitu KBR: `Soneta.EI.UI.PulpitKBR.Workers.PulpitKBEksportPrzelewowWorker`. + +**Snippet (kontrakt — w realnej integracji, nie w teście jednostkowym):** + +```csharp +using Soneta.Kasa; // EksportPrzelewowWorker, EksportPrzelewowParams, PrzelewBase +using System.Collections.Generic; + +PrzelewBase[] przelewy = /* przelewy z KADRY-I4, np. z paczki */; + +// Konstruktor jest WYMAGANY (brak ctora bezparametrowego) i waliduje rachunek (rzuca, gdy null): +var par = new EksportPrzelewowParams(context, rachunekFirmy, przelewy) // rachunekFirmy: RachunekBankowyFirmy +{ + FileName = @"C:\przelewy\wynagrodzenia.txt", // ŚCIEŻKA PLIKU — operacja I/O + PrmDataPrzelewow = Date.Today, + EksportujWBuforze = false, +}; + +var worker = new EksportPrzelewowWorker { Params = par }; +worker.Eksport(); // zapis pliku na dysk — efekt uboczny poza sesją +``` + +**Pułapki / ograniczenia (bądź szczery):** +- **Eksport pliku NIE nadaje się do testu jednostkowego** — pisze na dysk, zależy od formatu banku + i sterownika eksportu; w teście co najwyżej dokumentujemy istnienie API + (`EksportPrzelewowWorker`, `EksportPrzelewowParams.FileName`), bez wywołania `Eksport()`. +- Format pliku zależy od **konfiguracji formatu eksportu** danego banku — nie ma jednego + uniwersalnego formatu; `InfoFormat*`/`InfoBank` parametryzują wynik. +- Wysyłka online (bankowość elektroniczna / API banku) to dodatkowo operacja **sieciowa** — poza + zakresem testów jednostkowych. +- Po eksporcie `PrzelewBase.Exported = true` blokuje edycję — ponowny eksport wymaga + `EksportujWBuforze`/zmiany stanu. + +--- + +### KADRY-I6 — Wystawienie faktury / faktury zbiorczej z zapłaty (rozliczenia) (★) + +> **Zakres i szczerość.** Faktura jest dokumentem **handlowym** (`Soneta.Handel.DokumentHandlowy`), +> nie płacowym — to nie jest funkcja kartoteki pracownika ani list płac. Powiązanie „z zapłaty” +> dotyczy **rozrachunków/rozliczeń** (moduł Kasa): zapłata (`Soneta.Kasa.Wyplata`/`Wplata` — +> `IDokumentPlatny`) jest **rozliczana** z dokumentem płatnym (np. fakturą) przez rozrachunki. +> Wystawianie faktury z poziomu pracownika/płac w publicznym kontrakcie **nie istnieje**; +> tutaj dokumentujemy model rozliczeń, który łączy zapłatę z fakturą. + +**Cel:** powiązać zapłatę z dokumentem płatnym (fakturą) na poziomie rozrachunków/rozliczeń — +oraz wskazać, gdzie w publicznym API leży rozliczanie należności/zobowiązań pracownika. + +**Model rozliczeń (publiczny kontrakt, moduł `KasaModule`):** + +| Element | Typ / kolekcja | Rola | +|---|---|---| +| Zapłata (rozchód/wpływ) | `Soneta.Kasa.Wyplata` / `Soneta.Kasa.Wplata` | dokument płatny (`IDokumentPlatny`) | +| Płatność (zobowiązanie/należność) | `Soneta.Kasa.Platnosc` (tabela `Platnosci`, `IRozliczalny`) | to z nią rozlicza się zapłatę | +| Rozliczenie (powiązanie SP) | `Soneta.Kasa.RozliczenieSP` (tabela `RozliczeniaSP`, `IRozliczenie`) | wiąże zapłatę z płatnością/dokumentem | +| Rozrachunek | `Soneta.Kasa.RozrachunekIdx` (tabela `RozrachunkiIdx`) | indeks rozrachunkowy podmiotu | +| Stan rozliczenia zapłaty | `Wyplata.StanRozliczenia: Soneta.Kasa.StanRozliczenia`, `Wyplata.DoRozliczenia`, `Wyplata.KwotaRozliczona`, `Wyplata.Rozliczono` | ile pozostało / czy rozliczono | + +**Kolekcje na zapłacie (`Soneta.Kasa.Wyplata`):** +- `Wyplata.Zaplaty: SubTable` oraz `Wyplata.Dokumenty: SubTable` — + rozliczenia, +- `Wyplata.Rozrachunki: SubTable` — rozrachunki, +- `Wyplata.PreliminarzPoz: PreliminarzPozycja` — pozycja preliminarza. + +**Kolekcje na `Pracownik` (rozrachunki/faktury podmiotu):** +- `Pracownik.Rozrachunki`, `Pracownik.DokumentyRozliczeniowe`, + `Pracownik.DokumentyPreliminarza` (jak w tabeli KADRY-I4). **Uwaga:** `Pracownik.Platnosci` **nie istnieje** — + kolekcja `Platnosci` jest tylko na `IDokumentPlatny` (np. `Kasa.Wyplata.Platnosci`). + +**Workery rozliczeniowe (publiczny kontrakt, akcje menu):** + +| Worker | Rola | +|---|---| +| `Soneta.Kasa.RozliczWgPrzelewowWyplataWorker` | rozliczenie zapłaty wg przelewów | +| `Soneta.Kasa.RozliczPreliminarzIdxWorker` / `...TblWorker` / `...FrmWorker` | rozliczenie z preliminarzem | +| `Soneta.Kasa.PreliminarzPozycja.DodajRozliczenieWorker` | dodanie rozliczenia do pozycji preliminarza | +| `Soneta.Ksiega.UtworzPlatnoscZZapisuWorker` | utworzenie płatności z zapisu (księga) | + +**Faktura zbiorcza:** powstaje po stronie **handlowej** — z wielu zapłat/płatności tworzy się jeden +dokument handlowy (faktura) zbiorąc je jako rozliczenia. To domena `handel.md` +(wystawianie i rozliczanie faktur), nie kartoteki pracownika. Z poziomu rozliczeń pracownika +publiczny kontrakt udostępnia **odczyt i rozliczanie** rozrachunków, a nie „wystaw fakturę”. + +**Snippet (odczyt stanu rozliczenia zapłat — publiczny kontrakt):** + +```csharp +using Soneta.Kasa; // Wyplata, StanRozliczenia +using Soneta.Types; + +// Zapłaty pracownika rozliczane z dokumentami (np. fakturami) — odczyt stanu rozliczeń. +// Iteruj zawsze w zakresie/okresie (tabela operacyjna guided — safe-code §6.3). +foreach (RozrachunekIdx r in pracownik.Rozrachunki) +{ + // r — pozycja rozrachunkowa pracownika (powiązanie zapłata ↔ dokument) +} + +// Stan rozliczenia konkretnej zapłaty kasowej: +// Wyplata zaplata = ...; +// var doRozl = zaplata.DoRozliczenia; // ile pozostało do rozliczenia (Currency) +// var stan = zaplata.StanRozliczenia; // StanRozliczenia (enum) +// var czyRozl = zaplata.Rozliczono; // bool +``` + +**Pułapki / ograniczenia (bądź szczery):** +- **„Wystaw fakturę z pracownika/płac” nie istnieje w publicznym kontrakcie.** Faktura to dokument + handlowy; powiązanie z zapłatą realizują **rozrachunki/rozliczenia** (moduł Kasa), nie kartoteka + pracownika. To zadanie jest z pogranicza domen — opis kierujemy do `handel.md`. +- Pola rozliczeniowe (`DoRozliczenia`, `Stan`, `StanRozliczenia`, `KwotaRozliczona`, `Rozliczono`, + `Rozrachunki`) są na **`Soneta.Kasa.Wyplata`** (`IDokumentPlatny`), a nie na płacowej + `Soneta.Place.Wyplata`. +- Rozliczanie/tworzenie faktury zbiorczej **wymaga skonfigurowanego modułu Kasa/Handel** (definicje + dokumentów, rachunki, płatności). W bazie Demo część konfiguracji może nie być gotowa — operacje + zapisujące są niepewne w teście (patrz spec testowy). +- `Platnosc`/`RozliczenieSP`/`RozrachunekIdx` to obiekty operacyjne — przy odczycie filtruj zakresem + i nie skanuj całych tabel (safe-code §6.3). + +--- + +#### Spec testowy (zwarty) — KADRY-I4 / KADRY-I5 / KADRY-I6 + +Konwencja: `Soneta.Skills.Test/KadryPlace/Pracownik/`, klasa `RozdzialI_ListyWydrukiTest` +(lub nowa `RozdzialI_PrzelewyRozliczeniaTest : PracownikTestBase`); baza Demo + rollback; +operujemy wyłącznie na publicznym kontrakcie. + +**KADRY-I4 — `I4_PrzygotujPrzelewy_ZListyPlac`** +- *Co testowalne:* naliczenie wypłaty etatowej (`NaliczanieSeryjne.Pracownika`, jak KADRY-I1b) → uzyskanie + `ListaPlac` z `Wyplata.ListaPlac`; **konstrukcja** `ListaPlac.PrzygotujPrzelewyWorker` z `Params` + (asercja, że worker i typ `Params` istnieją w publicznym API; że pola `Data`/`Paczka`/`ZRachunku` + są dostępne). Odczyt kolekcji `Pracownik.Przelewy`, `Pracownik.DokumentyPreliminarza`, + `Pracownik.Rozrachunki` (asercja: kolekcje dostępne, iterowalne). +- *Niepewne / `[Ignore]`/`Assert.Ignore`:* faktyczne **wywołanie** `worker.PrzygotujPrzelewy()` i + powstanie dokumentów `PrzelewBase` — zależy od konfiguracji modułu Kasa (definicja paczki, + `RachunekBankowyFirmy`, rachunek pracownika `Pracownik.Rachunki`), której baza Demo nie gwarantuje. + Owinąć w `try/catch` + `Assert.Ignore` z opisem (wzorzec jak KADRY-I2/KADRY-I3) i asercję na powstaniu + przelewu robić tylko, gdy się udało. + +**KADRY-I5 — `I5_EksportPrzelewow_KontraktApi`** +- *Co testowalne:* **istnienie publicznego API** — weryfikacja **refleksją** (NIE instancjonuj!): + typ `EksportPrzelewowParams`, konstruktor `(Context, RachunekBankowyFirmy, PrzelewBase[])`, + property `FileName`; typ `EksportPrzelewowWorker`, property `Params`, metoda `Eksport`. + **Nie używaj inicjalizatora `new EksportPrzelewowParams { ... }`** — nie ma ctora bezparametrowego, + a ctor `(ctx, rachunek, przelewy)` rzuca `ApplicationException`, gdy `rachunek == null` (brak konfiguracji w Demo). +- *Niewykonalne w teście jednostkowym → `[Ignore]`:* wywołanie `worker.Eksport()` — **operacja + plikowa** (zapis na dysk wg `FileName`), zależna od formatu banku/sterownika; wysyłka online = + operacja sieciowa. **Nie wołać `Eksport()`** w teście; udokumentować jako `[Ignore("operacja + plikowa/sieciowa — poza testem jednostkowym")]`. + +**KADRY-I6 — `I6_Rozliczenia_OdczytStanu`** +- *Co testowalne:* odczyt kolekcji rozliczeniowych pracownika — `Pracownik.Rozrachunki`, + `Pracownik.DokumentyRozliczeniowe`, `Pracownik.DokumentyPreliminarza` + (asercja: dostępne, iterowalne, typy zgodne — `RozrachunekIdx`, `DokRozliczBase`, + `PreliminarzDokument`). **`Pracownik.Platnosci` NIE istnieje** — pomiń (kolekcja `Platnosci` jest tylko na + `IDokumentPlatny`); odczyt pól rozliczeniowych z `Soneta.Kasa.Wyplata` (`DoRozliczenia`, + `Stan`, `StanRozliczenia`, `Rozliczono`) — gdy istnieje zapłata kasowa w Demo. +- *Niewykonalne / `[Ignore]`:* **wystawienie faktury (zbiorczej) z zapłaty** — funkcja handlowa, + brak w kontrakcie pracownika; rozliczanie zapisujące (`RozliczWgPrzelewowWyplataWorker`, + `RozliczPreliminarz*Worker`) wymaga konfiguracji Kasa/Handel → `Assert.Ignore` przy braku danych. + Dla wystawiania faktur kierować do testów domeny handlowej (`handel.md`). + +**Dokładne nazwy (do użycia w testach):** +- Worker płacowy: `Soneta.Place.ListaPlac.PrzygotujPrzelewyWorker` (+ zagn. `.Params`; + akcja `PrzygotujPrzelewy`). +- Worker eksportu: `Soneta.Kasa.EksportPrzelewowWorker` + `Soneta.Kasa.EksportPrzelewowParams` + (akcja `Eksport`); paczki: `Soneta.Kasa.EksportPaczekPrzelewowWorker`. +- Dokumenty: `Soneta.Kasa.PrzelewBase` (tabela `Przelewy`), `Soneta.Kasa.PaczkaPrzelewow` + (tabela `PaczkiPrzelewow`), `Soneta.Kasa.DefinicjaPaczkiPrzelewu`, `Soneta.Kasa.RachunekBankowyFirmy`. +- Rozliczenia: `Soneta.Kasa.Platnosc`, `Soneta.Kasa.RozliczenieSP`, `Soneta.Kasa.RozrachunekIdx`, + `Soneta.Kasa.PreliminarzDokument`, `Soneta.Kasa.PreliminarzPozycja`. +- Zapłata kasowa (`IDokumentPlatny`): `Soneta.Kasa.Wyplata` (NIE `Soneta.Place.Wyplata`). +- Kolekcje na `Pracownik`: `Przelewy`, `Rozrachunki`, `DokumentyPreliminarza`, + `DokumentyRozliczeniowe`, `Rachunki` (**bez `Platnosci`** — ta kolekcja jest tylko na `IDokumentPlatny`). + diff --git a/soneta-programming/references/domeny/kadry/KADRY10-deklaracje.md b/soneta-programming/references/domeny/kadry/KADRY10-deklaracje.md new file mode 100644 index 0000000..ea03dd0 --- /dev/null +++ b/soneta-programming/references/domeny/kadry/KADRY10-deklaracje.md @@ -0,0 +1,352 @@ +# KADRY10 — Deklaracje (ZUS, PIT, PFRON, PPK) + +> Wspólne fakty o typie, podstawowe typy i szablon wzorca: [../kadry.md](../kadry.md). + +> **Moduł.** `Soneta.Deklaracje.DeklaracjeModule` — dostęp z sesji przez `session.GetDeklaracje()`. +> Wszystkie deklaracje (ZUS, PIT, PFRON, PPK) to wiersze tabeli `Deklaracje`, dziedziczące po +> abstrakcyjnej klasie root `Soneta.Deklaracje.Deklaracja` (`GuidedRow`, implementuje m.in. +> `IDeklaracja`, `IDokumentPlatny`, `IDokumentKsiegowalny`). Konkretne typy żyją w podprzestrzeniach: +> `Soneta.Deklaracje.ZUS.*`, `Soneta.Deklaracje.PIT.*`, `Soneta.Deklaracje.PFRON.*`, +> `Soneta.Deklaracje.PPK.*`. +> +> **Rozróżnienie kluczowe dla testów — NALICZENIE/UTWORZENIE vs E-WYSYŁKA.** +> - **Naliczenie/utworzenie deklaracji** (workery `*Worker` z akcjami „Przygotuj…/Nalicz…/Przelicz”, +> operacje PPK) tworzy **wiersze w bazie** — to operacja lokalna, w zasadzie testowalna na Demo, +> ale **wymaga `Context`** (i dla ZUS zwykle obiektu `KEDU`). Workery nie mają konstruktorów +> bezparametrowych dających pełny kontrakt — `Params` budujemy z `Context`/`Session`. +> - **E-wysyłka** to osobne typy: `EDeklaracja` (tabela `EDeklaracje` — XML, podpis, UPO) oraz +> `ETransmisja` (tabela `ETransmisje` — pojedyncze transmisje do bramki). Eksport KEDU/PUE realizują +> workery `Soneta.Deklaracje.UI.KeduEksportForm.EksportWorker` (akcje „Eksport KEDU”, „Pobierz KEDU”) +> i `Soneta.Deklaracje.UI.PUEEksportForm.EksportWorker` (akcja „Eksport PUE (RUD)”), a uruchomienie +> Programu Płatnika — `Soneta.Deklaracje.ZUS.DeklaracjaZUS.UruchomPPWorker` (akcja +> „Uruchom 'Program Płatnika'”). **To operacje sieciowe/plikowe/zewnętrzne — NIE do testu** (nawet +> utworzenie `EDeklaracja` wymaga podpisu i bramki ZUS/US). +> +> **`KEDU` (`Soneta.Deklaracje.ZUS.KEDU`)** — „zestaw deklaracji”: kontener (komplet dokumentów ZUS), +> do którego workery zgłoszeniowe i rozliczeniowe dopinają wygenerowane bloki. Praktycznie każdy worker +> ZUS przyjmuje `Kedu` w swoich `Params`; bez przekazanego `KEDU` generowanie deklaracji ZUS nie ma +> gdzie zapisać wyniku. KEDU nie jest tworzony „w locie” w sposób trywialny — jest częścią mechanizmu +> deklaracji rozliczeniowych ZUS i jego zbudowanie wymaga środowiska modułu Deklaracje (`Context`). + +--- + +### KADRY-J1 — Zgłoszenia ZUS (ZUA/ZZA, ZCNA, ZWUA) + +**Cel:** zgłosić/wyrejestrować pracownika i jego umowy w ZUS oraz zgłosić członków rodziny do +ubezpieczenia zdrowotnego. Typy zgłoszeń to wiersze deklaracji: `ZUA` (społeczne + zdrowotne), +`ZZA` (tylko zdrowotne), `ZCNA` (rodzina), `ZWUA` (wyrejestrowanie), `ZIUA` (zmiana danych +identyfikacyjnych), `ZCZA` (zmiana danych członka rodziny) — wszystkie w `Soneta.Deklaracje.ZUS`. + +**Workery — poziom `Pracownicy` (klasy zagnieżdżone `Soneta.Deklaracje.ZUS.ZarejestrujPracownikówWorker`):** + +| Worker (akcja) | `Params` (typ) | Pola `Params` | Metoda akcji | +|---|---|---|---| +| `ZarejestrujPracownikówWorker.Rejestracja` — „Deklaracje ZUS/Przygotuj ZUA i ZZA” | `ZarejestrujBaseWorker.ParamsKor` | `Okres: FromTo`, `DataDokumentu`/`DataWypełnienia: Date`, `Kedu: KEDU`, `KorektaZmiana: ZgloszenieZUS.KorektaZmiana`, `ZarejestrujRodzinę: bool` | `object ZarejestrujPracowników()` | +| `ZarejestrujPracownikówWorker.Rodzina` — „Deklaracje ZUS/Przygotuj ZCNA” | `ZarejestrujBaseWorker.Params` | `Okres`, `DataDokumentu`, `DataWypełnienia`, `Kedu` | `object ZarejestrujRodzinę()` | +| `ZarejestrujPracownikówWorker.Wyrejestrowanie` — „Deklaracje ZUS/Przygotuj ZWUA” | `Wyrejestrowanie.ParamsWR` | `Okres`, `DataDokumentu`, `DataWypełnienia`, `Kedu`, `RIA: bool`, `WyrejestrujRodzinę: bool` | `object WyrejestrujPracowników()` | +| `ZarejestrujPracownikówWorker.ZgloszenieUmow` — „Deklaracje ZUS/Przygotuj RUD” | `ZgloszenieUmow.UParams` | `Okres`, `DataWypełnienia`, `Kedu`, `Trwajace: bool` | `object ZgłośUmowy()` | + +> Worker przyjmuje zaznaczone osoby przez `Pracownicy: Pracownik[]` (`[Context]`). Wszystkie `Params` +> mają ctor `(Context)`. Po akcji wynik (lista wygenerowanych deklaracji) odczytasz z bazowego +> `Deklaracje: View`, a `Save()` zatwierdza. + +**Workery — poziom `Umowy` (zleceniobiorcy), `Soneta.Deklaracje.ZUS.ZarejestrujUmowyWorker`** — +opisane w **KADRY-G5** (`Rejestracja.ZarejestrujUmowy()` → ZUA/ZZA wg schematu `UmowaHistoria.Ubezpieczenia`, +`Wyrejestrowanie.WyrejestrujUmowy()` → ZWUA). `ParamsZ`/`ParamsW` mają ctor `(Context)`; pola +bazowe `Okres`/`DataDokumentu`/`DataWypełnienia`/`Kedu` + `ZarejestrujRodzinę`/`WyrejestrujRodzinę`. + +**ZCNA na rodzinie (KADRY-A9).** Zgłoszenie członka rodziny do ubezpieczenia zdrowotnego startuje z danych +`CzlonekRodziny` (`Ubezpieczony = true`, `UbezpieczenieOkres`, `StPokrewienstwa` — patrz KADRY-A9), a samą +deklarację ZCNA generuje `ZarejestrujPracownikówWorker.Rodzina` (lub `Rejestracja` z +`Pars.ZarejestrujRodzinę = true`). Dla zleceniobiorcy analogicznie przez `ZarejestrujUmowyWorker`. + +**Przerejestrowanie (KADRY-A19).** `Soneta.Deklaracje.UI.PrzerejestrowaniePracownikaWorker` (DataType +`PracHistoria`) oraz `Soneta.Deklaracje.UI.PrzerejestrowanieZleceniobiorcyWorker` (DataType +`UmowaHistoria`) — generują ZWUA+ZUA przy zmianie tytułu/wydziału. `Params` wymaga `KEDU` + `Context`. + +**Snippet (przygotowanie ZUA/ZZA dla zaznaczonych pracowników):** + +```csharp +var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; + +var pars = new Soneta.Deklaracje.ZUS.ZarejestrujBaseWorker.ParamsKor(context) +{ + Okres = new FromTo(new Date(2026, 1, 1), Date.MaxValue), + DataDokumentu = new Date(2026, 1, 1), + DataWypełnienia = Date.Today, + Kedu = kedu, // KEDU z modułu Deklaracje (Context) + ZarejestrujRodzinę = false, +}; +var rejestracja = new Soneta.Deklaracje.ZUS.ZarejestrujPracownikówWorker.Rejestracja +{ + Pracownicy = new[] { pracownik }, + Pars = pars, +}; +rejestracja.ZarejestrujPracowników(); // tworzy ZUA/ZZA (i ZCNA, gdy ZarejestrujRodzinę) +session.Save(); +``` + +**Pułapki:** +- Typ zgłoszenia (ZUA vs ZZA) wynika ze schematu ubezpieczeń (`Etat.Ubezpieczenia` / `UmowaHistoria.Ubezpieczenia`, + KADRY-A7/KADRY-G5) — nie z parametru workera. Ustaw `Tyub4` i flagi `Spoleczne`/`Zdrowotne` przed zgłoszeniem. +- Każdy `Params` wymaga `Context` (ctor `(Context)`) i pola `Kedu` — bez `KEDU` deklaracja nie ma + kontenera docelowego. Operacja jest **lokalna** (zapis wiersza), ale niewykonalna bez `Context`/`KEDU`. +- `ZWUA` z `RIA = true` powiązany jest z mechanizmem RIA (KADRY-J2). +- Workery zgłoszeniowe na `Pracownicy` dotyczą etatowych; na `Umowy` — zleceniobiorców (KADRY-G5). + +--- + +### KADRY-J2 — Deklaracje rozliczeniowe ZUS (DRA, RIA, IMIR, RUD, IWA; KEDU) + +**Cel:** naliczyć/utworzyć deklaracje rozliczeniowe i informacyjne ZUS. Typy (`Soneta.Deklaracje.ZUS`, +wiersze tabeli `Deklaracje`): `DRA` (deklaracja rozliczeniowa z załącznikami RCA/RSA/RZA; ctor `(KEDU)`), +`RIA` (informacja roczna / raport po ustaniu zatrudnienia; ctor `(Pracownik, KEDU)`), `RMUA` — +informacja miesięczna dla ubezpieczonego, potocznie **IMIR** (ctor `(Pracownik, RMUA.TypOkresuDeklaracji)`; +**brak osobnego typu `IMIR` w CLR — to `RMUA`**), `RUD` (zgłoszenie umowy o dzieło), `IWA` (informacja o wypadkach/składce wypadkowej), +`OSW` (oświadczenie), `Z3`/`Z3a` (zaświadczenia płatnika ERP-7 — patrz niżej), `KEDU` (zestaw). + +**Naliczanie seryjne — poziom `Pracownicy`:** + +| Worker (akcja) | `Params` (typ) | Pola `Params` | Metoda | +|---|---|---|---| +| `Soneta.Deklaracje.ZUS.NaliczanieSeryjneRIAWorker` — „Deklaracje ZUS/Przygotuj RIA” | `…RIAWorker.Params` | `DataDokumentu`/`DataWypełnienia: Date`, `Kedu: KEDU`, `Wydział: Wydzial`, `Wszystkie: bool`, `Zerowa: bool` | `object NaliczRMUA(Context)` | +| `Soneta.Deklaracje.ZUS.NaliczanieSeryjneRMUAWorker` — „Deklaracje ZUS/Przygotuj IMIR” | `…RMUAWorker.Params` | `DataDokumentu`/`DataWypełnienia: Date`, `Miesiac: YearMonth`, `Rok: int`, `TypOkresu: RMUA.TypOkresuDeklaracji`, `Oskladkowani: bool`, `Wydział`, `Wszystkie` | `object NaliczRMUA(Context)` | + +> Oba workery mają **ctor bezparametrowy**, przyjmują `Pracownicy: Pracownik[]` (`[Context]`) i mają w props +> `Context`, `Kedu`, `Deklaracje: View`. Metoda akcji `NaliczRMUA(Context)` (ta sama nazwa dla RIA i RMUA). +> `Params` są property `Pars` (setter); na workerze `RMUAWorker` pola `Params` są też wystawione bezpośrednio jako property. + +**Przeliczenie pojedynczej deklaracji — `Soneta.Deklaracje.DeklaracjaWorker`** (DataType `Deklaracja`, +więc działa dla **dowolnej** deklaracji ZUS/PIT/PFRON): akcja **„Przelicz”** → `void Przelicz()`; +parametr `Deklaracja: Soneta.Deklaracje.Deklaracja` (`[Context]`). + +**RUD** generuje `ZarejestrujPracownikówWorker.ZgloszenieUmow` (KADRY-J1) lub jest dostępna na liście umów. +**DRA z załącznikami** to root `DeklaracjaZUS`; nalicza się przez mechanizm KEDU + `Przelicz`. + +**E-wysyłka (NIE testować):** eksport KEDU — `KeduEksportForm.EksportWorker` („Eksport KEDU”, +„Pobierz KEDU”); eksport PUE/RUD — `PUEEksportForm.EksportWorker` („Eksport PUE (RUD)”); Program +Płatnika — `DeklaracjaZUS.UruchomPPWorker`. + +**Pułapki:** +- `KEDU` jest osią całego rozliczenia ZUS — wszystkie workery rozliczeniowe wpisują wynik do + przekazanego `Kedu`. Bez modułu Deklaracje (`Context`) i `KEDU` operacji nie złożysz. +- `DeklaracjaWorker.Przelicz()` przelicza **istniejący** wiersz deklaracji — najpierw musi powstać + (np. z naliczania seryjnego), więc to nie jest „utworzenie od zera”. + +--- + +### KADRY-J3 — Deklaracje PIT (PIT-11, PIT-4R, PIT-8AR, PIT-R, IFT-1/IFT-1R, PIT-8C) + +**Cel:** naliczyć imienne i zbiorcze deklaracje podatkowe. Typy (`Soneta.Deklaracje.PIT`, wiersze +tabeli `Deklaracje`): `PIT11`, `PIT4`/PIT-4R (rozliczeniowa zaliczek), `PIT8A`/PIT-8AR (zryczałtowany), +`PITR` (PIT-R), `IFT1`/`IFT1R`, `PIT8C`, `PIT40`, plus `ZbiorczaPIT`/`IEDeklaracjaZbiorczaItem` +(deklaracje zbiorcze). + +**Naliczanie seryjne — poziom `Pracownicy` (klasy zagnieżdżone `Soneta.Deklaracje.PIT.NaliczanieSeryjne`):** + +| Worker (akcja) | Ctor | `Params` — pola | Metoda | +|---|---|---|---| +| `NaliczanieSeryjne.PIT_11Worker` — „Deklaracje PIT/Nalicz PIT 11” | `(Session session)` | `Okres: FromTo`, `Data: Date`, `Naliczaj: NaliczanieDeklaracje`, `BezPotwierdzenia: bool`, dane podpisującego (`Imię`/`Nazwisko`/`Stanowisko` + `…Odp`), `TreśćUzasadnienia: string` | `object Nalicz_PIT_11()` | +| `NaliczanieSeryjne.PIT_RWorker` — „Deklaracje PIT/Nalicz PIT R” | `(Session)` | jw. (`Params`) | `Nalicz…()` | +| `NaliczanieSeryjne.PIT_8CWorker` — „Deklaracje PIT/Nalicz PIT 8C” | `(Session)` | jw. | `Nalicz…()` | +| `NaliczanieSeryjne.IFT_1Worker` / `IFT_1RWorker` — „Deklaracje PIT/Nalicz IFT-1 / IFT-1R” | `(Session)` | jw. | `Nalicz…()` | + +> `Params` mają ctor `(Context)`; worker `PIT_11Worker` dodatkowo ma ctor `(Session)`. Zaznaczeni +> pracownicy przez `[Context]`. + +**Deklaracje płatnika (PIT-4R/PIT-8AR)** są zbiorcze na poziomie podmiotu/oddziału (`PIT4`/`PIT8A`, +`ZbiorczaPIT`) — tworzone/dodawane workerami zbiorczymi (`DodajDoZbiorczejPITWorker`, +`WybierzDeklaracjeDoZbiorczejPITWorker`) i przeliczane `DeklaracjaWorker.Przelicz()` (KADRY-J2) lub +dedykowanymi `…PrzeliczWorker` (np. `PITR.PrzeliczWorker`, `PIT8S.PrzeliczWorker`). + +**Snippet (naliczenie PIT-11 dla zaznaczonych pracowników):** + +```csharp +var pracownicy = new[] { session.GetKadry().Pracownicy.WgKodu["006"] }; + +var worker = new Soneta.Deklaracje.PIT.NaliczanieSeryjne.PIT_11Worker(session) +{ + Pracownicy = pracownicy, +}; +worker.Pars.Okres = FromTo.Year(2025); // rok podatkowy +worker.Pars.Data = Date.Today; +worker.Nalicz_PIT_11(); // tworzy wiersze PIT11 w tabeli Deklaracje +session.Save(); +``` + +**Pułapki:** +- Naliczenie PIT bazuje na naliczonych wypłatach (H) i bilansach otwarcia PIT (KADRY-J6) — bez danych + źródłowych deklaracja będzie zerowa. +- Sygnatury `Params` PIT mają ctor `(Context)`; `PIT_11Worker` ma też ctor `(Session)` — w teście + użyj `(session)` + ustaw `Pracownicy`/`Pars`. +- **E-wysyłka PIT to `EDeklaracja`/`ETransmisja` (bramka MF) — NIE testować.** Samo naliczenie + wiersza PIT jest lokalne (zapis do bazy). + +--- + +### KADRY-J4 — Deklaracje PFRON (Wn-D, INF-2, DEK-R, INF-D-P) + +**Cel:** utworzyć/naliczyć deklaracje PFRON. Typy (`Soneta.Deklaracje.PFRON`, wiersze tabeli +`Deklaracje`): `WN_D` (Wn-D — wniosek o dofinansowanie), `WN_U` (Wn-U), `INF_D`/`INF_D_P` +(informacje o pracownikach niepełnosprawnych — załączniki do Wn-D), `INF_2` (informacja roczna), +`DEK_R` (deklaracja roczna wpłat). + +**Workery:** +- `Soneta.Deklaracje.DeklaracjaWorker` — akcja **„Przelicz”** (`Przelicz()`) dla każdego z typów PFRON + (są DataType `Deklaracja`). +- `Soneta.Deklaracje.PFRON.INF_D.InfoWorker`, `…INF_D_P.InfoWorker` — properties informacyjne (UI). +- **E-wysyłka SOD (NIE testować):** `Soneta.Deklaracje.UI.SODEksportForm.EksportWorker` (DataType + `WN_D`/`WN_U`/`INF_D`) — eksport do systemu SODiR. + +**Dane źródłowe** PFRON pochodzą z `PracHistoria.PFRON` (KADRY-A13: stopień niepełnosprawności, efekt +zachęty, schorzenia SOD) — bez nich deklaracja będzie pusta. + +**Pułapki:** +- PFRON nie ma dedykowanego „NaliczanieSeryjne” na `Pracownicy` — deklarację (`WN_D` itd.) tworzy się + w module Deklaracje, a przelicza `DeklaracjaWorker.Przelicz()`. Tworzenie/edycja wymaga `Context`. +- Konfiguracja procentów/odpisu PFRON to workery na `OddzialFirmy` + (`Soneta.Deklaracje.Config.*PFRON*Worker`) — to dane konfiguracyjne, nie deklaracje. + +--- + +### KADRY-J5 — Operacje PPK + +**Cel:** obsłużyć cykl życia uczestnictwa w PPK — kwalifikacja/auto-zapis, rejestracja uczestnika, +rezygnacja, wznowienie, zmiana danych, zakończenie zatrudnienia, dokumenty i rozliczenie składek. +Typy dokumentów PPK (`Soneta.Deklaracje.PPK`, wiersze tabeli `Deklaracje`): `RejestracjaUczestnikaPPK`, +`DeklaracjaUczestnikaPPK`, `ZmianaDanychIdentyfikacyjnychUczestnikaPPK`, +`ZmianaDanychKontaktowychUczestnikaPPK`, `ZakończenieZatrudnieniaUczestnikaPPK`, `TransferPPK`, +`WypłataTransferowaPPK`, `WypłataŚrodkówPrzezUczestnikaPPK`, `ZwrotŚrodkówPPK`, `RozliczenieSkładekPPK`, +`RozliczenieNadpłatPPK`, `ZwrotNadpłatyPPK`, `NadanieUczestnikowiNumeruPPK`, +`DokumentyPracodawcyPPK`, `DokumentyInstytucjiFinansowejPPK`. + +**Workery operacji PPK — poziom `Pracownicy` (zagnieżdżone `Soneta.Deklaracje.PPK.DeklaracjePPKPracownikówWorker`), +wspólny `Params = DeklaracjePPKBaseWorker.Params` (`Okres: FromTo`, `DokumentPPK: DokumentyPracodawcyPPK`):** + +| Worker (akcja) | Metoda | +|---|---| +| `…Worker.Rejestracja` — „Operacje PPK/Rejestracja uczestnika” | `object RejestracjaPracownikow()` | +| `…Worker.Rezygnacja` — „Operacje PPK/Rezygnacja uczestnika” | `object RezygnacjaPracownikow()` | +| `…Worker.Wznowienie` — „Operacje PPK/Automatyczne wznowienie uczestnictwa” | `object WznowieniePracownikow()` | +| `…Worker.ZakończenieZatrudnienia` — „Operacje PPK/Zakończenie zatrudnienia uczestnika” | `object ZakończenieZatrudnieniaPracownikow()` | +| `…Worker.ZmianaDanychIdentyfikacyjnych` — „Operacje PPK/Zmiana danych identyfikacyjnych” | `object ZmianaDanychIdentyfikacyjnychPracownikow()` | + +> Przystąpienie/auto-zapis i zmiana procentu składki realizowane są na poziomie **pracownika** +> (dane PPK pracownika), nie tymi workerami zbiorczymi. + +**Workery na pracowniku (kwalifikacja PPK) — `Soneta.Kadry.Pracownik`:** + +| Worker | Ctor | Wybrane pola/props | +|---|---|---| +| `Pracownik.PPKWorker` (alias `PPK`) | `(Context context)` | `Data: Date`, `Idx: Pracownik`; `Kwalifikacja: PPKWorker.RodzajZgłoszenia`, `DataKwalifikacji[/Min/Max]: Date`, `Kwalifikacja[Min/Max]` | +| `Pracownik.AutoZapisPPKWorker` (alias `AutoZapisPPK`) | `(Context context)` | `Data: Date`, `Pracownik: Pracownik`; `Kwalifikacja: AutoZapisPPKWorker.CzyAutoZapisPPK` | + +> Te workery służą do **odczytu kwalifikacji** (czy/kiedy pracownik podlega przystąpieniu lub +> auto-zapisowi do PPK na dany dzień) — mają ctor `(Context)`. + +**Przeliczanie/rozliczenie PPK:** +- `Soneta.Deklaracje.PPK.PrzeliczPPKWorker` (DataType m.in. `RozliczenieNadpłatPPK`, + `WypłataTransferowaPPK`, `WypłataŚrodkówPrzezUczestnikaPPK`, `ZwrotŚrodkówPPK`, + `NadanieUczestnikowiNumeruPPK`) — przelicza dokument rozliczeniowy PPK. +- `Soneta.Deklaracje.PPK.NadanieNumeruPPKWorker` (DataType `NadanieUczestnikowiNumeruPPK`). +- `RozliczenieSkładekPPK` / `RejestracjaUczestnikaPPK` / `DeklaracjaUczestnikaPPK` przeliczane przez + `DeklaracjaWorker.Przelicz()` (KADRY-J2, DataType `Deklaracja`). + +**E-wysyłka / import-eksport PPK (NIE testować):** +- `Soneta.Deklaracje.PPK.DokumentyPPKEksportWorker` (DataType `DokumentyPracodawcyPPK`, + `DokumentyInstytucjiFinansowejPPK`) — eksport do instytucji finansowej. +- `Soneta.Deklaracje.PPK.DokumentyPPKImportWorker` (DataType `DokumentyInstytucjiFinansowejPPK`) — + import zwrotny. + +**Snippet (rejestracja uczestnika PPK dla zaznaczonych):** + +```csharp +var pracownicy = new[] { session.GetKadry().Pracownicy.WgKodu["006"] }; + +var pars = new Soneta.Deklaracje.PPK.DeklaracjePPKBaseWorker.Params(context) +{ + Okres = FromTo.Year(2026), + // DokumentPPK = … (DokumentyPracodawcyPPK z modułu Deklaracje) +}; +var rej = new Soneta.Deklaracje.PPK.DeklaracjePPKPracownikówWorker.Rejestracja +{ + Pracownicy = pracownicy, + Pars = pars, +}; +rej.RejestracjaPracownikow(); // tworzy dokumenty rejestracji uczestnika PPK +session.Save(); +``` + +**Pułapki:** +- Zmiana procentu składki PPK / przystąpienie to dane **pracownika** (deklaracja uczestnika PPK, + `DeklaracjaUczestnikaPPK`) — workery zbiorcze obejmują rejestrację, rezygnację, wznowienie, zmianę + danych identyfikacyjnych i zakończenie zatrudnienia. +- `DeklaracjePPKBaseWorker.Params` ma ctor `(Context)`; operacja jest lokalna (tworzy wiersze + dokumentów PPK), ale niewykonalna bez `Context` i zwykle `DokumentPPK`. +- `PPKWorker`/`AutoZapisPPKWorker` na pracowniku są **diagnostyczne** (kwalifikacja na dzień), nie + tworzą dokumentów — i wymagają `Context`. + +--- + +### KADRY-J6 — Bilanse otwarcia deklaracji (PIT, ZUS, ERP-7) przy wdrożeniu + +**Cel:** wprowadzić dane historyczne sprzed startu systemu, potrzebne do poprawnego naliczenia +deklaracji w pierwszym okresie. Bilanse są **kolekcjami na pracowniku** (`SubTable`) — tworzy się je +i odczytuje czystym API biznesowym, **bez `Context`/`KEDU`/sieci**. + +**Kolekcje na `Soneta.Kadry.Pracownik`:** + +| Kolekcja | Typ | Przeznaczenie | +|---|---|---| +| `Pracownik.BilansyOtwarciaPIT` | `SubTable` | bilans otwarcia PIT (przychody/koszty/składki na start) | +| `Pracownik.WynagrodzeniaERP7` | `SubTable` | wynagrodzenia do ERP-7 / Z-3 | +| `Pracownik.NieobecnosciERP7` | `SubTable` | nieobecności do ERP-7 / Z-3 | +| `Pracownik.DeklaracjePodmiotu` | `SubTable` | deklaracje powiązane z pracownikiem-podmiotem | + +**Typ `Soneta.Place.BilansOtwarciaPIT`** (root `GuidedRow`, tabela `BilansyOtwPIT`) jest +**ABSTRAKCYJNY** — instancjonuje się jedną z konkretnych wersji odpowiadających wartościom enuma +`Soneta.Place.WersjaBilansuOtwarciaPIT` (`PIT11_11`, `PIT11_29`): +`Soneta.Place.BilansOtwarciaPIT_11` (Wersja = `PIT11_11`) lub `Soneta.Place.BilansOtwarciaPIT_29` +(Wersja = `PIT11_29`). Konkretne klasy mają publiczny ctor `(Pracownik pracownik)`; bazowy +`BilansOtwarciaPIT` ma ctor `(Pracownik, WersjaBilansuOtwarciaPIT)`, ale jest abstrakcyjny. +Property `Pracownik` i `Wersja` są **read-only** (ustawiane przez ctor; brak ctora bezparametrowego). +Pola bazodanowe m.in.: `Data: Date`, kwoty przychodów/kosztów/składek w rozbiciu +etat/umowa/macierzyński (`Przychod26ZwolEtat`, `Przychod26ZwolUmowa`, `PrzychodUlgaEtat`, +`PrzychodUlgaUmowa`, `Spoleczne`, `Spoleczne26`, `Zdrowotne9Procent`, `SkladkiCzlonkowskie` itd.) +oraz kolekcja `Elementy: SubTable`. + +**ERP-7** (wcześniej druk ZUS Rp-7) opiera się na `WynagrodzeniaERP7`/`NieobecnosciERP7` pracownika +oraz zaświadczeniach `Soneta.Deklaracje.ZUS.Z3`/`Z3a` (workery `ZUSZ3.Z3Worker`/`Z3aWorker` na +`Nieobecnosc`) — sam druk Z-3/ERP-7 to generowanie dokumentu w module Deklaracje. + +**Snippet (dodanie bilansu otwarcia PIT i odczyt):** + +```csharp +var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; + +using (var t = session.Logout(editMode: true)) +{ + // BilansOtwarciaPIT jest abstrakcyjny — tworzymy konkretną wersję (_29 => PIT11_29, _11 => PIT11_11). + // Pracownik ustawia ctor (property read-only), więc NIE używamy inicjalizatora obiektu na Pracownik. + var bo = session.AddRow(new Soneta.Place.BilansOtwarciaPIT_29(pracownik)); + bo.Data = new Date(2026, 1, 1); + bo.PrzychodUlgaEtat = 12000m; + bo.Spoleczne = 1645.20m; + t.Commit(); +} +session.Save(); + +// Odczyt bilansów otwarcia PIT pracownika (typ kolekcji: SubTable): +foreach (Soneta.Place.BilansOtwarciaPIT bo in pracownik.BilansyOtwarciaPIT) +{ + // bo.Data, bo.PrzychodUlgaEtat, bo.Spoleczne, bo.Wersja +} +``` + +**Pułapki:** +- `BilansOtwarciaPIT` ma kolekcję `Elementy` — niektóre kwoty są wyliczane z elementów; sprawdź na + Demo, czy ustawiasz pola root, czy elementy. +- Bilanse są **danymi wdrożeniowymi** (jednorazowe na start) — nie myl z naliczonymi deklaracjami. +- ERP-7 (Z-3/Z-3a) wymaga modułu Deklaracje i `KEDU`/PUE do eksportu — samo wprowadzenie + `WynagrodzeniaERP7`/`NieobecnosciERP7` jest lokalne, ale wygenerowanie druku — nie. + diff --git a/soneta-programming/references/domeny/kadry/KADRY11-ewidencje.md b/soneta-programming/references/domeny/kadry/KADRY11-ewidencje.md new file mode 100644 index 0000000..ffdb6ec --- /dev/null +++ b/soneta-programming/references/domeny/kadry/KADRY11-ewidencje.md @@ -0,0 +1,831 @@ +# KADRY11 — Ewidencje pracownicze + +> Wspólne fakty o typie, podstawowe typy i szablon wzorca: [../kadry.md](../kadry.md). + +> **Wzorzec wspólny.** Wszystkie ewidencje pracownicze to **kolekcje `SubTable` na rootcie +> `Pracownik`** (nie na `PracHistoria`). Każdy element jest osobnym `GuidedRow` (child pracownika) +> z polem `Pracownik: Soneta.Kadry.Pracownik` ustawianym automatycznie przez konstruktor +> `new Xxx(pracownik)`. Schemat dodania jest jednolity: +> +> ```csharp +> using (var t = session.Logout(editMode: true)) { +> var wpis = session.AddRow(new Xxx(pracownik)); // ctor wiąże wpis z pracownikiem +> // ... ustaw pola ... +> t.Commit(); // Commit() w kodzie biznesowym +> } +> session.Save(); +> ``` +> +> `session.AddRow(new Xxx(pracownik))` i `pracownik.Kolekcja.AddRow(new Xxx(pracownik))` są +> równoważne — wpis trafia do tej samej tabeli i do `SubTable` pracownika. Większość typów wymaga +> wskazania **definicji** (rekord słownikowy, tabela konfiguracyjna) — definicję pobierasz przez +> `WgNazwy[...]` z odpowiedniego modułu, **nie** tworzysz jej w teście operacyjnym. + +| Ewidencja | Kolekcja na `Pracownik` | Typ elementu | Tabela | +|---|---|---|---| +| KADRY-K1 Badania lekarskie | `BadaniaLekarskie: SubTable` | `Soneta.Kadry.BadanieLekarskie` | `BadaniaLekarskie` | +| KADRY-K2 Szkolenia BHP | `SzkoleniaBHP: SubTable` | `Soneta.Kadry.SzkolenieBHP` | `SzkoleniaBHP` | +| KADRY-K3 Wnioski o szkolenia | `WnioskiOSzkolenia: SubTable` | `Soneta.HR.WniosekOSzkolenie` | `WnioskiOSzkol` | +| KADRY-K3 Ukończone szkolenia | `UkończoneSzkolenia: SubTable` | `Soneta.HR.UkończoneSzkolenie` | `UkonczSzkolenia` | +| KADRY-K3 Uprawnienia | `Uprawnienia: SubTable` | `Soneta.HR.UprawnieniePracownika` | `UprawnieniaPrac` | +| KADRY-K4 Nagrody i kary | `NagrodyKary: SubTable` | `Nagroda` / `Kara` (`NagrodaKara` abstr.) | `NagrodyKary` | +| KADRY-K4 Oświadczenia | `Oświadczenia: SubTable` | `Soneta.Kadry.OświadczeniePracownika` | `OswiadczeniaPrac` | +| KADRY-K5 Wypadki przy pracy | `Wypadki: SubTable` | `Soneta.Kadry.Wypadek` | `Wypadki` | + +--- + +### KADRY-K1 — Badania lekarskie + +**Cel:** zarejestrować badanie lekarskie pracownika (wstępne/okresowe/kontrolne) wraz z terminami +ważności i datą następnego badania; ewentualnie wykonać operację seryjną dla grupy osób. + +**Mechanizm:** `BadanieLekarskie` ma publiczny konstruktor `BadanieLekarskie(Pracownik pracownik)`. +Wpis wymaga `Definicja: DefinicjaBadaniaLekarskiego` (słownik, tabela konfiguracyjna `DefBadanLek`, +pobierana przez `WgNazwy[...]`). Jeśli definicja jest **cykliczna** (`Definicja.Cykliczne == true`, +ma `NastepneDefinicja`/`NastepneTermin`), platforma wylicza termin kolejnego badania — +udostępniony jako wyliczane `NastępneTermin`/`NastępneDefinicja`. + +**Pola i typy (rekord `BadanieLekarskie`):** + +| Pole | Typ | Uwaga | +|---|---|---| +| `Definicja` | `Soneta.Kadry.DefinicjaBadaniaLekarskiego` | wymagana; słownik `DefBadanLek` | +| `Data` | `Soneta.Types.Date` | data wykonania badania | +| `Termin` | `Soneta.Types.Date` | termin badania — **read-only** (wyliczany z `Data`+definicji); ustawienie rzuca `ColReadOnlyException` | +| `WazneDo` | `Soneta.Types.Date` | „Ważne do" — koniec ważności (ustawialny) | +| `PracaWOkularach` | `bool` | adnotacja medyczna | +| `KwotaDofinansowania` | `decimal`, `DataDofinansowania: Date` | dofinansowanie badania | +| `Opis` | `Soneta.Business.MemoText` | opis/uwagi | +| `Anulowany` | `bool` | flaga anulowania | +| `Pracownik` | `Soneta.Kadry.Pracownik` | ustawiany przez ctor | +| `NastępneTermin`, `NastępneDefinicja`, `Następne` | (wyliczane) | termin/def./wpis następnego badania | + +**Manager:** `pracownik.Badania: Pracownik.BadaniaLekarskieManager` — pomocnik tylko do odczytu; +`pracownik.Badania.ZNajkrótszymTerminem(definicja = null): BadanieLekarskie` zwraca badanie z +najbliższym terminem wygaśnięcia (do raportów „badania okresowe do wykonania"). + +**Snippet:** + +```csharp +var kadry = session.GetKadry(); +var pracownik = kadry.Pracownicy.WgKodu["006"]; +var definicja = kadry.DefBadanLek.WgNazwy["Wstępne"]; // słownik konfiguracyjny + +using (var t = session.Logout(editMode: true)) +{ + var badanie = session.AddRow(new BadanieLekarskie(pracownik)); // ctor wiąże z pracownikiem + badanie.Definicja = definicja; + badanie.Data = Date.Today; + // UWAGA: badanie.Termin jest read-only (wyliczany) — NIE ustawiaj go ręcznie. + badanie.WazneDo = new Date(Date.Today.Year + 2, Date.Today.Month, Date.Today.Day); + + t.Commit(); +} +session.Save(); +``` + +**Operacja seryjna (grupa pracowników):** w warstwie UI istnieje worker +`DodajBadaniaLekarskieWorker` (warianty `ZListyBadań`, `ZListyPracowników`) z akcją menu +„Operacje seryjne/Dodaj badania lekarskie" — iteruje po wybranych pracownikach i dla każdego robi +`new BadanieLekarskie(pracownik)` + `BadaniaLekarskie.AddRow(...)`. W kodzie biznesowym +seryjność realizujesz tą samą pętlą `foreach (var p in wybrani) { … AddRow … }` w jednej transakcji. + +**Pułapki:** +- `Definicja` jest **wymagana** — bez niej `Save()` rzuci `RowException`. +- `Data`/`WazneDo` to `Soneta.Types.Date`, nie `DateTime`. `Termin` jest **read-only** (wyliczany) — + próba ustawienia rzuca `ColReadOnlyException`. Reguła w weryfikatorach: `WazneDo` nie może być + wcześniejsze niż `Termin`; termin następnego badania musi być **późniejszy** niż termin badania + bieżącego — naruszenie wybucha jako `RowException` przy zapisie. +- `pracownik.Badania` to manager (odczyt), a kolekcją CRUD jest `pracownik.BadaniaLekarskie` + (`SubTable`). Nie myl tych dwóch. + +--- + +### KADRY-K2 — Szkolenia BHP + +**Cel:** zarejestrować odbyte szkolenie BHP (wstępne/okresowe) z terminem ważności i datą szkolenia +następnego (analogicznie do badań lekarskich). + +**Mechanizm:** konstruktor `SzkolenieBHP(Pracownik pracownik)`; kolekcja `pracownik.SzkoleniaBHP`. +Wymagana `Definicja: DefinicjaSzkoleniaBHP` (słownik konfiguracyjny `DefSzkolenBHP`, `WgNazwy[...]`). +Cykliczność (`Definicja.Cykliczne`) wylicza `NastępneTermin`/`NastępneDefinicja`. + +**Pola i typy (rekord `SzkolenieBHP`):** + +| Pole | Typ | Uwaga | +|---|---|---| +| `Definicja` | `Soneta.Kadry.DefinicjaSzkoleniaBHP` | wymagana; słownik `DefSzkolenBHP` | +| `Data` | `Soneta.Types.Date` | data szkolenia | +| `Termin` | `Soneta.Types.Date` | termin — **read-only** (wyliczany); ustawienie rzuca `ColReadOnlyException` | +| `WażneDo` | `Soneta.Types.Date` | koniec ważności (wyliczane) | +| `Zakres` | `string` | zakres szkolenia | +| `Osoba` | `string` | prowadzący / osoba szkoląca | +| `Opis` | `Soneta.Business.MemoText` | uwagi | +| `Anulowany` | `bool` | flaga anulowania | +| `Pracownik` | `Soneta.Kadry.Pracownik` | ustawiany przez ctor | +| `NastępneTermin`, `NastępneDefinicja`, `Następne` | (wyliczane) | następne szkolenie | + +**Snippet:** + +```csharp +var kadry = session.GetKadry(); +var pracownik = kadry.Pracownicy.WgKodu["007"]; +var definicja = kadry.DefSzkolenBHP.WgNazwy["Wstępne"]; + +using (var t = session.Logout(editMode: true)) +{ + var szkolenie = session.AddRow(new SzkolenieBHP(pracownik)); + szkolenie.Definicja = definicja; + szkolenie.Data = Date.Today; + // UWAGA: szkolenie.Termin jest read-only (wyliczany) — NIE ustawiaj go ręcznie. + szkolenie.Zakres = "Instruktaż ogólny"; + + t.Commit(); +} +session.Save(); +``` + +**Operacja seryjna:** UI udostępnia `DodajSzkolenieBHPWorker` (akcja menu, lista pracowników) — +w kodzie biznesowym pętla `foreach` + `new SzkolenieBHP(p)` + `AddRow` w jednej transakcji. + +**Pułapki:** +- `Definicja` wymagana (jak w KADRY-K1). +- Uwaga na pisownię: pole nazywa się `WażneDo` (z „ż"), a w `BadanieLekarskie` — `WazneDo` (bez). +- `Termin` jest **read-only** (wyliczany) — ustawienie rzuca `ColReadOnlyException`. +- `Termin` następnego szkolenia musi być późniejszy niż bieżący — inaczej `RowException`. + +--- + +### KADRY-K3 — Szkolenia i uprawnienia (moduł HR/HR2) + +**Cel:** obsłużyć cykl rozwoju kompetencji: **wniosek o szkolenie** → **ukończone szkolenie** → +**uprawnienie/certyfikat**, wraz z kosztem i budżetem szkoleń. Typy leżą w module `Soneta.HR` +(`session.GetHR()`). + +**KADRY-K3a — Wniosek o szkolenie** — `WniosekOSzkolenie([Required] Pracownik pracownik)`; kolekcja +`pracownik.WnioskiOSzkolenia`. Pola: + +| Pole | Typ | Uwaga | +|---|---|---| +| `Definicja` | `Soneta.HR.DefinicjaSzkolenia` | rodzaj szkolenia (słownik HR) | +| `Etap` | `Soneta.HR.EtapRealizacjiSzkolenia` | np. „Wniosek zatwierdzony" (`hr.EtapRealizSzkol.WgNazwy[...]`) | +| `Realizacja` | `Soneta.HR.RealizacjaSzkolenia` | konkretna realizacja | +| `Budzet` | `Soneta.HR.BudżetSzkoleń` | budżet, z którego finansowane | +| `Koszt` | `Soneta.Types.Currency` | koszt szkolenia | +| `DataZgloszenia`, `Termin`, `DataAnulowania` | `Soneta.Types.Date` | daty cyklu wniosku | +| `Kierownik` | `Soneta.Kadry.Pracownik` | akceptujący | +| `SkierowanyPrzezZaklad` | `bool` | skierowanie pracodawcy | +| `Ocena` | `string`, `Opis: MemoText` | ocena/uwagi | + +**KADRY-K3b — Ukończone szkolenie** — dwa ctory: `UkończoneSzkolenie([Required] Pracownik pracownik)` +oraz `UkończoneSzkolenie(WniosekOSzkolenie wniosek)` (przepina pracownika z wniosku). Kolekcja +`pracownik.UkończoneSzkolenia`. Pola: `Nazwa: string`, `Okres: FromTo`, `Ocena: string`, +`Opis: MemoText`, `Wniosek: WniosekOSzkolenie` (powiązanie). + +**KADRY-K3c — Uprawnienie / certyfikat** — `UprawnieniePracownika([Required] Pracownik pracownik)`; +kolekcja `pracownik.Uprawnienia`. Pola: + +| Pole | Typ | Uwaga | +|---|---|---| +| `Definicja` | `Soneta.HR.DefinicjaUprawnienia` | rodzaj uprawnienia | +| `Numer` | `string` | numer uprawnienia/certyfikatu | +| `DataUzyskania`, `DataUtraty`, `TerminWaznosci` | `Soneta.Types.Date` | daty ważności | +| `Okres` | `Soneta.Types.FromTo` | okres obowiązywania | +| `WydanePrzez` | `string` | organ wydający | +| `Zrodlo` | `Soneta.HR.IŹródłoUzyskaniaUprawnienia` | źródło (np. ukończone szkolenie) | + +**Snippet (wniosek → koszt z budżetu):** + +```csharp +var hr = session.GetHR(); +var pracownik = session.GetKadry().Pracownicy.WgKodu["008"]; + +using (var t = session.Logout(editMode: true)) +{ + var wniosek = session.AddRow(new WniosekOSzkolenie(pracownik)); + wniosek.Definicja = hr.DefinicjeSzkolen.WgNazwy["Kurs zawodowy"]; + wniosek.Etap = hr.EtapRealizSzkol.WgNazwy["Wniosek zatwierdzony"]; + wniosek.DataZgloszenia = Date.Today; + wniosek.Koszt = new Currency(1500m); + + t.Commit(); +} +session.Save(); +``` + +**Pułapki:** +- Typy KADRY-K3 są w `Soneta.HR` (`session.GetHR()`), nie w `Soneta.Kadry`. +- `Etap`/`Definicja` to wpisy słownikowe HR — pobieraj `WgNazwy[...]`, nie twórz w teście. +- `Koszt`/`Budżet` używają `Soneta.Types.Currency` (waluta), nie `decimal`. + +--- + +### KADRY-K4 — Nagrody i kary; oświadczenia (PIT-2, RODO, zgody) + +**KADRY-K4a — Nagrody i kary.** Klasa bazowa `Soneta.Kadry.NagrodaKara` jest **abstrakcyjna** — używaj +konkretnych podtypów: `Soneta.Kadry.Nagroda(Pracownik)` i `Soneta.Kadry.Kara(Pracownik)`. Oba ctory +delegują do `NagrodaKara(pracownik, TypNagrodyKary)` ustawiając `Typ` na `Nagroda`/`Kara`. Kolekcja +`pracownik.NagrodyKary: SubTable`. Pola: + +| Pole | Typ | Uwaga | +|---|---|---| +| `Definicja` | `Soneta.Kadry.DefinicjaNagrodyKary` | słownik `DefNagrodKar`; ma własne pole `Typ` (Nagroda/Kara) — musi zgadzać się z podtypem wpisu, inaczej `set_Definicja` rzuca `ArgumentException`; może nieść `Element`/`Kwota` | +| `Typ` | `Soneta.Kadry.TypNagrodyKary` | `Nagroda`/`Kara` (ustawia ctor podtypu) | +| `Data` | `Soneta.Types.Date` | data nadania | +| `DataAnulowania` | `Soneta.Types.Date` | anulowanie | +| `Rozliczenie` | `Soneta.Kadry.RozliczenieSwiadczenia` (subrow) | `Rozliczenie.Kwota: Currency`, `Rozliczenie.Element: DefinicjaElementu`, `Rozliczenie.Okres: FromTo` — powiązanie z wypłatą | +| `Opis` | `Soneta.Business.MemoText` | treść nagrody/kary | +| `Pracownik` | `Soneta.Kadry.Pracownik` | ustawiany przez ctor | + +**KADRY-K4b — Oświadczenia (PIT-2, RODO, zgody).** `Soneta.Kadry.OświadczeniePracownika` — trzy ctory: +`OświadczeniePracownika([Required] Pracownik pracownik, [Required] DefinicjaOświadczenia definicja)`, +wariant z `Date dataZłożenia`, oraz `(RowCreator)`. Kolekcja `pracownik.Oświadczenia`. Rodzaj +oświadczenia (PIT-2, zgoda RODO, zgoda na e-doręczenia itp.) określa `Definicja` (słownik +konfiguracyjny `DefOswiadczen`). Pola: + +| Pole | Typ | Uwaga | +|---|---|---| +| `Definicja` | `Soneta.Kadry.DefinicjaOświadczenia` | wymagana w ctorze; słownik `DefOswiadczen` | +| `DataZlozenia` | `Soneta.Types.Date` | data złożenia | +| `DataWycofania` | `Soneta.Types.Date` | data wycofania | +| `Okres` | `Soneta.Types.FromTo` | okres obowiązywania (z `Definicja.OkresWaznosci`/`OkresIlosc`) | +| `Tresc` | `Soneta.Business.MemoText` | treść | +| `TrescOswiadczenia` | `Soneta.Kadry.TreśćOświadczenia` | treść strukturalna | +| `Pracownik` | `Soneta.Kadry.Pracownik` | ustawiany przez ctor | + +**Snippet (nagroda + oświadczenie PIT-2):** + +```csharp +var kadry = session.GetKadry(); +var pracownik = kadry.Pracownicy.WgKodu["009"]; + +using (var t = session.Logout(editMode: true)) +{ + // nagroda — konkretny podtyp, NIE abstrakcyjna NagrodaKara + var nagroda = session.AddRow(new Nagroda(pracownik)); + nagroda.Definicja = kadry.DefNagrodKar.WgNazwy["Nagroda uznaniowa"]; + nagroda.Data = Date.Today; + + // oświadczenie — definicja jest wymagana w konstruktorze + var defPit2 = kadry.DefOswiadczen.WgNazwy["PIT-2"]; + var oswiadczenie = session.AddRow(new OświadczeniePracownika(pracownik, defPit2, Date.Today)); + + t.Commit(); +} +session.Save(); +``` + +**Pułapki:** +- **Nie** rób `new NagrodaKara(...)` — typ abstrakcyjny. Używaj `Nagroda`/`Kara`. +- `Definicja` musi mieć **`Typ` zgodny** z podtypem wpisu (`Nagroda` → def. o `Typ==Nagroda`, `Kara` → + def. o `Typ==Kara`); przypisanie niezgodnej typem definicji rzuca `ArgumentException` w `set_Definicja`. + Filtruj słownik: `DefNagrodKar.Cast().FirstOrDefault(d => d.Typ == TypNagrodyKary.Nagroda)`. +- `OświadczeniePracownika` **nie ma** ctora samego `(Pracownik)` — definicja jest `[Required]` + w konstruktorze; bez niej kod się nie skompiluje. +- `Rozliczenie.*` na nagrodzie/karze to subrow powiązania z wypłatą (`Currency`, `DefinicjaElementu`) + — wypełniane przy rozliczaniu w płacach, nie przy samym wpisie. + +--- + +### KADRY-K5 — Wypadki przy pracy + +**Cel:** zarejestrować wypadek przy pracy wraz z dokumentacją powypadkową (protokół, decyzja, +okoliczności, skutki) i ewentualnym świadczeniem. + +**Mechanizm:** `Soneta.Kadry.Wypadek(Pracownik pracownik)`; kolekcja `pracownik.Wypadki`. Wpis jest +numerowany (`Numer: Soneta.Core.NumerDokumentu`) i wymaga `Definicja: Soneta.Core.DefinicjaDokumentu` +(definicja dokumentu wypadku). + +**Pola i typy (rekord `Wypadek`):** + +| Pole | Typ | Uwaga | +|---|---|---| +| `Definicja` | `Soneta.Core.DefinicjaDokumentu` | definicja dokumentu (numeracja) | +| `Numer` | `Soneta.Core.NumerDokumentu` (subrow) | `Numer.Pelny`, `Numer.Symbol`, `Numer.Numer` | +| `Data` | `Soneta.Types.Date` | data wypadku | +| `Godzina` | `Soneta.Types.Time` | godzina wypadku | +| `DataZgloszenia` | `Soneta.Types.Date` | data zgłoszenia | +| `Miejsce` | `string` | miejsce wypadku | +| `Rodzaj` | `Soneta.Kadry.RodzajWypadku` | klasyfikacja wypadku | +| `PrzyPracy`, `Ciezki`, `Smiertelny`, `Niezdolnosc` | `bool` | kwalifikacja skutków | +| `Okolicznosci`, `Skutki`, `Odmowa` | `Soneta.Business.MemoText` | dokumentacja opisowa | +| `ProtokolNumer`, `ProtokolData` | `string` / `Date` | protokół powypadkowy | +| `DecyzjaNumer`, `DecyzjaData` | `string` / `Date` | decyzja | +| `PismoNumer`, `PismoData` | `string` / `Date` | pismo | +| `SKW` | `string` | statystyczna karta wypadku | +| `Kwota` | `decimal` | kwota świadczenia | +| `PracHistoria` | `Soneta.Kadry.PracHistoria` | (wyliczane) zapis kadrowy na datę | +| `Pracownik` | `Soneta.Kadry.Pracownik` | ustawiany przez ctor | + +**Snippet:** + +```csharp +var kadry = session.GetKadry(); +var pracownik = kadry.Pracownicy.WgKodu["010"]; + +using (var t = session.Logout(editMode: true)) +{ + var wypadek = session.AddRow(new Wypadek(pracownik)); + wypadek.Data = Date.Today; + wypadek.Godzina = new Time(10, 30); + wypadek.DataZgloszenia = Date.Today; + wypadek.Miejsce = "Hala produkcyjna"; + wypadek.PrzyPracy = true; + wypadek.Okolicznosci = new MemoText("Poślizgnięcie na mokrej posadzce."); + + t.Commit(); +} +session.Save(); +``` + +**Pułapki:** +- `Numer` jest subrowem nadawanym wg `Definicja` (numeracja) — nie ustawiaj `Numer.Pelny` ręcznie, + numer nadaje platforma; gdy `Definicja` ma własną numerację, podpięcie definicji wystarcza. +- `Godzina` to `Soneta.Types.Time`, `Data` to `Soneta.Types.Date` — nie `DateTime`. +- Pola opisowe (`Okolicznosci`, `Skutki`, `Odmowa`) to `MemoText`, nie `string`. + +### KADRY-K6 — RODO/GIODO: oświadczenia, uprawnienia do przetwarzania, wymiana danych + +**Cel:** ewidencjonować zgody/oświadczenia RODO pracownika, uprawnienia do przetwarzania danych +osobowych oraz fakty wymiany danych (pozyskanie / udostępnienie / powierzenie). Pracownik jest +hostem GIODO — implementuje `IGIODOOświadczenieHost`, `IGIODOUprawnienieHost`, `IGIODOWymianaDanychHost`, +`IGIODOZgodnyHost`. Zapis „teczki" personalnej do pliku jest operacją plikową (poza zakresem testów). + +**Publiczny kontrakt — kolekcje na `Pracownik` (moduł `Soneta.Core`):** + +| Kolekcja | Typ elementu | Zawartość | +|---|---|---| +| `GIODOOświadczenia` | `SubTable` | oświadczenia / zgody RODO | +| `GIODOUprawnienia` | `SubTable` | uprawnienia do przetwarzania danych | +| `GIODOUdostępnienia` | `SubTable` | pozyskanie / udostępnienie / powierzenie danych | +| `PotwierdzeniaGIODO` | `SubTable` | potwierdzenia zgodności; `ZgodnoscGIODOPotwierdzona: bool` (kalkulowane) | + +**`GIODOOświadczenie` (tabela `GIODOOswiadcz`, root) — pola bazodanowe:** + +| Pole | Typ | Uwaga | +|---|---|---| +| `Host` | `IGIODOOświadczenieHost` | składający oświadczenie (= `Pracownik`) | +| `Definicja` | `Soneta.Core.GIODODefinicjaOświadczenia` | **referencja konfiguracyjna** (wymagana przez ctor) | +| `Data` | `Soneta.Types.Date` | data oświadczenia (zapisywalne) | +| `Okres` | `Soneta.Types.FromTo` | okres obowiązywania zgody — **read-only** (wyliczane z definicji) | +| `Rodzaj` | `Soneta.Core.RodzajeOświadczeńGIODO` | `Oświadczenie`, `UdzielenieZgody`, `WycofanieZgody` — **read-only** (wynika z definicji) | +| `Oswiadczenie` | `bool` | flaga oświadczenia | +| `Tresc` | `Soneta.Business.MemoText` | treść | +| `SposobPozyskania` | `string` | — | +| `DataWycofaniaZgody` | `Soneta.Types.Date` | — | +| `WycofanieZgody` | `GIODOOświadczenie` | powiązanie z zapisem wycofania | +| `Bufor` | `bool` | zatwierdzenie | + +Ctor: `new GIODOOświadczenie(IGIODOOświadczenieHost host, GIODODefinicjaOświadczenia definicja)`. + +**`GIODOUprawnienie` (tabela `GIODOUprawnienia`, root) — pola bazodanowe:** + +| Pole | Typ | Uwaga | +|---|---|---| +| `Uprawniony` | `IGIODOUprawnienieHost` | = `Pracownik` | +| `Definicja` | `Soneta.Core.GIODODefinicjaUprawnienia` | **referencja konfiguracyjna** (wymagana przez ctor) | +| `Data`, `Przyznane`, `Odebrane` | `Soneta.Types.Date` | data zapisu / od kiedy przyznane / od kiedy odebrane | +| `Okres` | `Soneta.Types.FromTo` | okres przyznania | +| `Tresc` | `Soneta.Business.MemoText` | — | +| `WycofanieUprawnienia` | `GIODOUprawnienie` | powiązanie z wycofaniem | + +Ctor: `new GIODOUprawnienie(IGIODOUprawnienieHost uprawniony, GIODODefinicjaUprawnienia definicja)`. + +**`GIODOWymianaDanych` (tabela `GIODOWymDanych`, root) — pola bazodanowe:** + +| Pole | Typ | Uwaga | +|---|---|---| +| `Host` | `IGIODOWymianaDanychHost` | = `Pracownik` | +| `Kierunek` | `Soneta.Core.GIODOKierunekWymianyDanych` | `Powierzenie`, `Pozyskanie`, `PowierzenieZbioru`, `PozyskanieZbioru`, `Udostępnienie` | +| `Podmiot` | `Soneta.Core.IKontrahent` | druga strona wymiany | +| `Data` | `Soneta.Types.Date` | data wymiany | +| `Zakres` | `Soneta.Business.MemoText` | zakres danych | +| `SposobPozyskania` | `string` | — | +| `PozyskaneOdOsoby`, `UdostepnioneOsobie`, `NaWniosekOsoby`, `TylkoDostep` | `bool` | flagi | +| `Definicja` | `Soneta.Core.DefinicjaDokumentu` | def. numeracji dokumentu | +| `ZbiorDanych` | `Soneta.Core.GIODO.GIODOZbiorDanych` | zbiór danych | + +`GIODOWymianaDanych` **nie ma publicznego konstruktora** — rekordy tworzą wyłącznie workery poniżej +(zwracają konkretne podtypy `GIODOPozyskanieDanych` / `GIODOUdostępnienieDanych` / `GIODOPowierzenieDanych`). + +**Workery RODO (jedyna droga dodania przez API; klasy zagnieżdżone w `Soneta.Kadry.Pracownicy`):** + +| Worker | Metoda | Zwraca | Parametry (`Pars` / `Params`) | +|---|---|---|---| +| `Pracownicy.DodajOświadczeniaWorker` | `GIODOOświadczenie DodajOświadczenia()` | oświadczenie | `Pars`: `Definicja: GIODODefinicjaOświadczenia`, `Data`, `Oddział`, `SposobPozyskania`, `Zatwierdź: bool` | +| `Pracownicy.DodajUprawnieniaWorker` | `GIODOUprawnienie DodajUprawnienia()` | uprawnienie | `Pars`: `Definicja: GIODODefinicjaUprawnienia`, `Data`, `Przyznane`, `Odebrane`, `Oddział`, `Zatwierdź: bool` | +| `Pracownicy.DodajPozyskanieDanychWorker` | `GIODOPozyskanieDanych DodajPozyskanieDanych()` | wymiana (pozyskanie) | `Pars`: `Podmiot: IKontrahent`, `Data`, `Zakres: string`, `Oddział`, `SposobPozyskania`, `Zatwierdź: bool` | +| `Pracownicy.DodajUdostępnienieDanychWorker` | `GIODOUdostępnienieDanych DodajUdostępnienieDanych()` | wymiana (udostępnienie) | `Pars`: `Podmiot: IKontrahent`, `Data`, `Zakres: string`, `Oddział`, `Zatwierdź: bool` | +| `Pracownicy.DodajPowierzenieDanychWorker` | `GIODOPowierzenieDanych DodajPowierzenieDanych()` | wymiana (powierzenie) | `Pars` (analogicznie) | + +Wszystkie workery RODO mają bezparametrowy ctor oraz property `Hosts: Pracownik[]` (`[Context]`, lista +pracowników, których dotyczy operacja) i `Session`. + +**Zapis teczki personalnej do pliku — `Pracownik.ZapiszTeczkęDoPlikuWorker`** (akcja +„Teczka.../Zapisz teczkę do pliku", metoda `ZapiszTeczkeDoPliku()`, property `Param`) — to +**operacja plikowa** (serializacja dokumentacji do plików XML/katalogu na dysku). **Poza zakresem +testów jednostkowych → `[Ignore]`** (zależność od systemu plików). + +**Snippet (dodanie oświadczenia GIODO workerem):** + +```csharp +var kadry = session.GetKadry(); +var pracownik = kadry.Pracownicy.WgKodu["006"]; + +// Definicja oświadczenia z konfiguracji (musi istnieć w bazie): +var defOswiadczenia = session.ExecuteConfig(s => + s.GetCore().GIODODefinicjeOświadczeń.WgNazwy["Zgoda na przetwarzanie danych"]); + +using (var t = session.Logout(editMode: true)) +{ + var worker = new Pracownik.Pracownicy.DodajOświadczeniaWorker { Hosts = new[] { pracownik } }; + worker.Pars.Definicja = session.Get(defOswiadczenia); + worker.Pars.Data = Date.Today; + worker.Pars.Zatwierdź = true; + GIODOOświadczenie oswiadczenie = worker.DodajOświadczenia(); + t.CommitUI(); +} +session.Save(); + +// Odczyt oświadczeń pracownika: +foreach (GIODOOświadczenie o in pracownik.GIODOOświadczenia) +{ + // o.Definicja, o.Okres, o.Rodzaj, o.Data +} +``` + +**Snippet (dodanie oświadczenia bez workera — bezpośrednim ctorem):** + +```csharp +using (var t = session.Logout(editMode: true)) +{ + var o = session.AddRow(new GIODOOświadczenie(pracownik, session.Get(defOswiadczenia))); + // host i Definicja wynikają z ctora; Rodzaj/Okres są WYLICZANE (read-only) z definicji — nie ustawiaj ich. + o.Data = Date.Today; + o.SposobPozyskania = "Formularz papierowy"; + t.Commit(); +} +session.Save(); +``` + +**Pułapki:** +- `GIODOOświadczenie`/`GIODOUprawnienie` wymagają **referencji do definicji konfiguracyjnej** + (`GIODODefinicjaOświadczenia` / `GIODODefinicjaUprawnienia`) — pobierz istniejący rekord + (`ExecuteConfig`), nie twórz „w locie". Bez definicji w bazie scenariusz wymaga uprzedniej + konfiguracji modułu RODO/GIODO. +- `GIODOWymianaDanych` **nie ma publicznego ctora** — dodawaj wyłącznie workerami + `DodajPozyskanieDanychWorker` / `DodajUdostępnienieDanychWorker` / `DodajPowierzenieDanychWorker`. +- Workery RODO modyfikują dane i są uruchamiane „jak z UI" → transakcja edycyjna + `CommitUI()` + + `Save()`. `Hosts`/`Podmiot` muszą pochodzić z bieżącej sesji (safe-code §2.1). +- Obowiązywanie zgody jest „na dzień" — czytaj `Okres`/`Data`, nie zakładaj bezterminowości. +- Dane wrażliwe (treść oświadczeń, podmioty) — nie loguj nadmiarowo (safe-code §12). +- Workery RODO wymagają praw do obszaru GIODO; w teście biznesowym egzekucji praw nie sprawdzamy + (safe-code §7.2). + +### KADRY-K7 — Struktura organizacyjna: przypisanie do wydziału/struktury, powiązania + +**Cel:** przypisać pracownika do jednostki organizacyjnej (wydziału) oraz do elementów struktury +organizacyjnej (np. stanowiska w strukturze, relacje przełożony–podwładny). Wydział wynika z warunków +etatu (`Etat.Wydzial`, historyczne — patrz sekcja B), a powiązania ze strukturą trzyma osobna kolekcja. + +**Publiczny kontrakt:** + +| Składnik | Typ | Rodzaj | Uwaga | +|---|---|---|---| +| `Etat.Wydzial` | `Soneta.Kadry.Wydzial` | bazodanowe (na `PracHistoria.Etat`) | jednostka organizacyjna; korzeń: `session.GetKadry().Wydzialy.Firma` (zmiana „od daty" — KADRY-A14) | +| `PowiązaniaStrOrg` | `SubTable` | kolekcja na `Pracownik` | powiązania ze strukturą organizacyjną | +| `StrukturaOraganizacyjna` | `Pracownik.StrukturaOraganizacyjnaManager` | manager (read-only API) | nawigacja przełożeni/podwładni | +| Pracownik implementuje | `IŹródłoPowiązaniaStrukturyOrganizacyjnej` | interfejs | jest źródłem powiązań | + +**`PowiązanieStrukturyOrganizacyjnej` (tabela `PowiazaniaStrOrg`, child przez `Zrodlo`) — pola:** + +| Pole | Typ | Uwaga | +|---|---|---| +| `Zrodlo` | `IŹródłoPowiązaniaStrukturyOrganizacyjnej` | guided-parent (= `Pracownik`) | +| `Element` | `Soneta.Core.ElementStrukturyOrganizacyjnej` | referencja do **instancji** elementu struktury z tabeli `CoreModule.ElementyStrOrg` (NIE z `DefElStrukturOrg`, która trzyma `DefinicjaElementuStrukturyOrganizacyjnej`); `ElementStrukturyOrganizacyjnej` nie ma publicznego ctora — pobierz istniejący rekord | +| `Okres` | `Soneta.Types.FromTo` | okres obowiązywania powiązania (zapisywalne) | + +Ctor: `new PowiązanieStrukturyOrganizacyjnej(ElementStrukturyOrganizacyjnej element, IŹródłoPowiązaniaStrukturyOrganizacyjnej zrodlo)`. + +**Manager `StrukturaOraganizacyjnaManager` (tylko odczyt nawigacyjny):** + +| Metoda / property | Sygnatura | Zwraca | +|---|---|---| +| `Przełożony(...)` | `Pracownik Przełożony(StrukturaOrganizacyjna, Date, bool, Func<...>)` | bezpośredni przełożony | +| `PrzełożonyWgPodległości(...)` | `Pracownik PrzełożonyWgPodległości(Date, bool, Func<...>)` | przełożony wg podległości | +| `Przełożeni(...)` | `IEnumerable …` | przełożeni | +| `Podwładni(...)` | `IEnumerable Podwładni(FromTo, bool, Func<...>)` | podwładni w okresie | +| `GetDomyślnyPrzełożony(naDzień[, bezpośredni, warunek])` | `Pracownik GetDomyślnyPrzełożony(Date, bool=…, Func=…)` | domyślny przełożony na dzień (property `DomyślnyPrzełożony` jest **przestarzała** — używaj metody) | + +**Workery zmiany powiązań (klasy zagnieżdżone w `Soneta.Kadry.Pracownik`):** + +| Worker | Akcja (menu) | Metoda | Parametry | +|---|---|---|---| +| `Pracownik.DodajPowiązanieStrukturyWorker` | „Struktura organizacyjna/Dodaj lub modyfikuj powiązanie…" | `object DodajPowiązanieStruktury()` | `Params: WybórElementuContext`, `Pracownicy: Pracownik[]` (`[Context]`) | +| `Pracownik.UsuńPowiązanieStrukturyWorker` | „Struktura organizacyjna/Usuń powiązanie…" | `void DodajPowiązanieStruktury()` | `Params: WybórElementuContext`, `Pracownicy: Pracownik[]` | + +**Snippet (dodanie powiązania ze strukturą — bezpośrednim ctorem):** + +```csharp +var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; + +// Instancja elementu struktury (musi istnieć w bazie — tabela ElementyStrOrg, NIE DefElStrukturOrg): +ElementStrukturyOrganizacyjnej element = + session.GetCore().ElementyStrOrg.Cast().FirstOrDefault(); + +using (var t = session.Logout(editMode: true)) +{ + var p = session.AddRow(new PowiązanieStrukturyOrganizacyjnej(element, pracownik)); + p.Okres = new FromTo(Date.Today, Date.MaxValue); + t.Commit(); +} +session.Save(); + +// Odczyt nawigacyjny struktury: +Pracownik przelozony = pracownik.StrukturaOraganizacyjna.GetDomyślnyPrzełożony(Date.Today); +``` + +**Snippet (zmiana wydziału — nowy zapis „od daty", KADRY-A14):** + +```csharp +var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; +var kadry = session.GetKadry(); + +using (var t = session.Logout(editMode: true)) +{ + var ph = pracownik[Date.Today]; // zapis obowiązujący na dzień (KADRY-A15) + ph.Etat.Wydzial = kadry.Wydzialy.Firma; // referencja do istniejącego wydziału (korzeń struktury) + t.Commit(); +} +session.Save(); +``` + +**Pułapki:** +- `Etat.Wydzial` jest **danymi historycznymi** (na `PracHistoria.Etat`) i jest **wymagany dla etatu** — + zmieniaj nowym zapisem „od daty" (KADRY-A14), nie nadpisuj bieżącego (zmieniłoby cały okres wstecz). +- `PowiązanieStrukturyOrganizacyjnej.Element` to **referencja konfiguracyjna** — pobierz istniejący + element struktury; bez zdefiniowanej struktury organizacyjnej scenariusz wymaga konfiguracji. +- `StrukturaOraganizacyjnaManager` jest **tylko do odczytu** — zmiany realizują workery + `DodajPowiązanieStrukturyWorker` / `UsuńPowiązanieStrukturyWorker` lub bezpośredni zapis do + `PowiązaniaStrOrg`. +- Workery struktury modyfikują dane „jak z UI" → transakcja + `CommitUI()` + `Save()`; rekordy z + bieżącej sesji (safe-code §2.1). + +### KADRY-K8 — Oceny okresowe: arkusze ocen, cele okresowe, karty kompetencji + +**Cel:** prowadzić oceny okresowe pracownika (arkusz oceny z elementami), cele okresowe wraz z ich +realizacją, karty kompetencji i karty opisu stanowiska. Funkcjonalność należy do modułów **HR** +(`session.GetHR()`, `OcenyPracownikow`, `EtapyRekrutacji`) i **HR2** (`session.GetHR2()`, +`CeleOkresowePrac`, `KartyKompPrac`, `KartyOpStanowisk`). Pracownik implementuje `IOceniany`, +`IOceniający`, `IOdpowiedzialnyZaOcenę`, `IŹródłoKartyOpisuStanowiska`. + +**Publiczny kontrakt — kolekcje na `Pracownik`:** + +| Kolekcja | Typ elementu | Zawartość | +|---|---|---| +| `Oceny` | `SubTable` | arkusze ocen okresowych (root) | +| `ElementyOceny` | `SubTable` | pojedyncze elementy/pozycje arkuszy ocen | +| `CeleOkresowe` | `SubTable` | cele okresowe | +| `KartyKompetencji` | `SubTable` | karty kompetencji | +| `KartyOpisuStanowiska` | `SubTable` | karty opisu stanowiska | +| `KartyRealizacjiCelu` | `SubTable` | karty realizacji celów | +| `Oceniani` / `Oceniający` | `SubTable` | role pracownika w ocenie | + +**`OcenaPracownika` (tabela `OcenyPracownikow`, root; impl. `IOcenaPracownika`) — pola:** + +| Pole | Typ | Uwaga | +|---|---|---| +| `Pracownik` | `Soneta.Kadry.Pracownik` | oceniany | +| `Nazwa` | `string` | nazwa arkusza | +| `Data`, `Termin` | `Soneta.Types.Date` | data oceny / termin | +| `Opis` | `Soneta.Business.MemoText` | — | +| `Anulowany` | `bool` | — | +| `ElementyOceny` | `SubTable` | pozycje arkusza | + +Ctor: `new OcenaPracownika(Pracownik pracownik)`. + +**`ElementOcenyPracownika` (tabela `ElementyOcenPrac`, child przez `Ocena`) — pola:** + +| Pole | Typ | Uwaga | +|---|---|---| +| `Ocena` | `IOcenaPracownika` | guided-parent (arkusz oceny lub etap rekrutacji) | +| `Pracownik` | `Soneta.Kadry.Pracownik` | — | +| `Definicja` | `Soneta.HR.DefElementuOcenyPracownika` | **referencja konfiguracyjna** (z tabeli `HRModule.DefElemOcenPrac`); zapisywalna i wymagana do zapisu | +| `Typ` | `Soneta.HR.TypyElementowOceny` | `Historyczny`, `Aktualny` — **read-only** (wynika z definicji) | +| `Data` | `Soneta.Types.Date` | **read-only** (wyliczane) | +| `Wartosc` | `decimal` | wartość liczbowa oceny (zapisywalna) | + +Ctor: `new ElementOcenyPracownika(IOcenaPracownika ocena)`. Dodawaj przez `session.AddRow(new ElementOcenyPracownika(ocena))` (NIE `ocena.ElementyOceny.AddRow(...)` — `SubTable` nie udostępnia `AddRow`). + +**`CelOkresowyPracownika` (tabela `CeleOkresowePrac`, root) — pola:** + +| Pole | Typ | Uwaga | +|---|---|---| +| `Pracownik` | `Soneta.Kadry.Pracownik` | — | +| `Nazwa` | `string` | nazwa celu | +| `Data`, `Termin` | `Soneta.Types.Date` | — | +| `Opis` | `Soneta.Business.MemoText` | — | +| `Definicja` | `Soneta.Oceny.DefinicjaElementuOceny` | **referencja konfiguracyjna** (opcjonalna) | +| `Anulowany` | `bool` | — | +| `Realizacja` | `Soneta.HR2.RealizacjaCelu` | bieżąca realizacja (subrow) | +| `Realizacje` | `SubTable` | realizacje celu | + +> `CelOkresowyPracownika` **nie ma pola `Wartosc`** — postęp/ocena celu jest reprezentowana przez `Realizacja`/`Realizacje` (`Soneta.HR2.RealizacjaCelu`). Pole `Wartosc` (typu decimal) ma natomiast `ElementOcenyPracownika`. + +Ctor: `new CelOkresowyPracownika(Pracownik pracownik)`. + +**`KartaOpisuStanowiskaBase` (tabela `KartyOpStanowisk`, root) — pola:** + +| Pole | Typ | Uwaga | +|---|---|---| +| `Zrodlo` | `IŹródłoKartyOpisuStanowiska` | = `Pracownik` / `DefinicjaStanowiska` / wakat / oferta | +| `Typ` | `Soneta.HR2.TypyKartOpisuStanowiska` | `KartaOpisuStanowiska`, `OgłoszenieOPracę` | +| `Data` | `Soneta.Types.Date` | — | +| `Elementy` | `SubTable` | elementy opisu | +| `Kompetencje` | `SubTable` | kompetencje | + +`KartaOpisuStanowiskaBase` i `KartaKompetencjiPracownika` **nie mają publicznego ctora bezparametrowego**; +`KartaKompetencjiPracownika` ma ctor `(Pracownik pracownik, IŹródłoKartyCharakterystykiPracownika zrodlo)`. +Karty zwykle tworzone są workerami kopiującymi (`KopiujKartęOpisuStanowiskaWorker.KopiujZDefinicjiStanowiska()`, +`KopiujKartęKompetencjiWorker.KopiujZKOS()`/`KopiujZPoprzedniej()`). + +**Workery oceniania (klasy w `Soneta.HR` / `Soneta.HR2`):** + +| Worker | Metoda | Parametry | +|---|---|---| +| `Soneta.HR.OcenaPracownikowWorker` | `Oceń()` | `Pars`, `Idxs: Pracownik[]` (`[Context]`); ctor `(Context)` | +| `Soneta.HR.WzorOcenyPracownika.ZainicjujOcenęWorker` | `Zainicjuj()` | `Ocena: IOcenaPracownika`, `Pars`; ctor `(Session)` | +| `Soneta.HR2.KopiujKartęOpisuStanowiskaWorker` | `KopiujZDefinicjiStanowiska()`, `KopiujZPoprzedniej()` | `Karta: KartaOpisuStanowiskaBase` | +| `Soneta.HR2.KopiujKartęKompetencjiWorker` | `KopiujZKOS()`, `KopiujZPoprzedniej()` | `Karta: KartaKompetencjiPracownika` | + +**Snippet (dodanie celu okresowego — wymaga definicji elementu oceny w bazie HR2):** + +```csharp +var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; +var hr2 = session.GetHR2(); + +// Definicja elementu oceny z konfiguracji modułu Oceny (musi istnieć): +var defElementu = session.ExecuteConfig(s => + /* pobranie DefinicjaElementuOceny z modułu Oceny */ default); + +using (var t = session.Logout(editMode: true)) +{ + var cel = new CelOkresowyPracownika(pracownik); + hr2.CeleOkresowePrac.AddRow(cel); + cel.Nazwa = "Wdrożenie nowego modułu"; + cel.Data = Date.Today; + cel.Termin = new Date(2026, 12, 31); + cel.Definicja = session.Get(defElementu); + t.Commit(); +} +session.Save(); + +// Odczyt celów okresowych: +foreach (CelOkresowyPracownika c in pracownik.CeleOkresowe) +{ + // c.Nazwa, c.Termin, c.Wartosc.Punktacja +} +``` + +**Snippet (utworzenie arkusza oceny i dodanie elementu):** + +```csharp +using (var t = session.Logout(editMode: true)) +{ + var ocena = new OcenaPracownika(pracownik); + session.GetHR().OcenyPracownikow.AddRow(ocena); + ocena.Nazwa = "Ocena roczna 2026"; + ocena.Data = Date.Today; + + var el = session.AddRow(new ElementOcenyPracownika(ocena)); // ocena jako IOcenaPracownika + el.Definicja = defElementu; // wymagana (z HRModule.DefElemOcenPrac); Typ/Data są wyliczane (read-only) + el.Wartosc = 4m; // Wartosc to decimal + t.Commit(); +} +session.Save(); +``` + +**Pułapki:** +- Cele/elementy ocen wymagają **referencji do definicji konfiguracyjnych** (`DefElementuOcenyPracownika`, + `Soneta.Oceny.DefinicjaElementuOceny`) — bez nich scenariusz wymaga uprzedniej konfiguracji modułu + Oceny/HR/HR2. W bazie Demo te definicje **mogą nie istnieć** — najpierw sprawdź dostępność. +- Karty opisu stanowiska / kompetencji nie mają prostego ctora — twórz je workerami kopiującymi + (`KopiujKartę…Worker`) z definicji stanowiska lub poprzedniej karty. +- `ElementOcenyPracownika.Ocena` to `IOcenaPracownika` — może to być arkusz oceny **lub etap + rekrutacji** (`EtapRekrutacji` także implementuje `IOcenaPracownika`, patrz KADRY-K9). +- `CelOkresowyPracownika` **nie ma pola `Wartosc`** — postęp/wynik celu reprezentują `Realizacja`/`Realizacje` + (`RealizacjaCelu`). Liczbową wartość ma `ElementOcenyPracownika.Wartosc` (`decimal`). +- `ElementOcenyPracownika`: `Typ`/`Data` są **read-only** (wyliczane z definicji), a do tabeli dodawaj przez + `session.AddRow(...)` — `SubTable` nie ma metody `AddRow`. +- Workery oceniania uruchamiane „jak z UI" → transakcja + `CommitUI()` + `Save()`. + +### KADRY-K9 — Rekrutacja: wakaty, ogłoszenia, aplikacje, etapy, stan zatrudnienia + +**Cel:** prowadzić proces rekrutacji — wakaty (zapotrzebowanie), oferty/ogłoszenia o pracę, aplikacje +kandydatów oraz etapy rekrutacji z ocenami, aż do zatrudnienia kandydata. Funkcjonalność należy do +modułów **HR2** (`session.GetHR2()`, `RekrutAplikacje`, `RekrutWakaty`) i **HR** +(`session.GetHR()`, `Rekrutacje`, `EtapyRekrutacji`). + +**Publiczny kontrakt — kolekcje na `Pracownik` (kandydat jest reprezentowany rekordem `Pracownik`):** + +| Kolekcja | Typ elementu | Zawartość | +|---|---|---| +| `Aplikacje` | `SubTable` | aplikacje kandydata | +| `Wakaty` | `SubTable` | wakaty | +| `Rekrutacje` / `Kandydatury` | `SubTable` | rekrutacje (kandydatury) | +| `EtapyRekrutacji` | `SubTable` | etapy rekrutacji | + +**`RekrutacjaAplikacja` (tabela `RekrutAplikacje`, root; impl. `IŹródłoRekrutacji`) — pola:** + +| Pole | Typ | Uwaga | +|---|---|---| +| `Pracownik` | `Soneta.Kadry.Pracownik` | kandydat | +| `Stanowisko` | `Soneta.HR.DefinicjaStanowiska` | **referencja konfiguracyjna** stanowiska | +| `Wydzial` | `Soneta.Kadry.Wydzial` | jednostka organizacyjna | +| `Oferta` | `Soneta.HR2.OfertaPracy` | oferta, na podstawie której wpłynęła aplikacja | +| `Stan` | `Soneta.HR2.StanAplikacji` | `Wprowadzona`, `Zakończona`, `Anulowana` | +| `Data` | `Soneta.Types.Date` | data aplikacji | +| `PlanowanaDataZatrudnienia` | `Soneta.Types.Date` | — | + +Ctor: `new RekrutacjaAplikacja(Pracownik pracownik, Soneta.HR.WydziałDefinicjiStanowiska stanowisko)`. +`WydziałDefinicjiStanowiska` jest w module **`Soneta.HR`** (NIE `Soneta.HR2`) i ma ctor +`new WydziałDefinicjiStanowiska(DefinicjaStanowiska definicjaStanowiska)` — definicję pobierz +z `session.GetHR().DefStanowisk`. `RekrutacjaAplikacja.Stanowisko` zwraca tę `DefinicjaStanowiska`. + +**`EtapRekrutacji` (tabela `EtapyRekrutacji`, root; impl. `IOcenaPracownika`) — pola:** + +| Pole | Typ | Uwaga | +|---|---|---| +| `Rekrutacja` | `Soneta.HR.Rekrutacja` | rekrutacja nadrzędna | +| `Definicja` | `Soneta.HR.DefinicjaEtapuRekrutacji` | **referencja konfiguracyjna** | +| `Lp` | `int` | numer etapu | +| `Data`, `Termin` | `Soneta.Types.Date` | — | +| `Odpowiedzialny` | `Soneta.Oceny.IOceniający` | osoba odpowiedzialna | +| `Opis` | `Soneta.Business.MemoText` | — | +| `ElementyOceny` | `SubTable` | oceny etapu (etap jest `IOcenaPracownika`) | + +Ctor: `new EtapRekrutacji(Rekrutacja rekrutacja)`. + +**`Rekrutacja` (tabela; impl. `IOcenaPracownika`) — ctory:** +`new Rekrutacja(Pracownik pracownik)`, `new Rekrutacja(Pracownik pracownik, IŹródłoRekrutacji źródło)`. + +**`RekrutacjaWakat` (tabela `RekrutWakaty`, root) — ctory:** +`new RekrutacjaWakat(WydziałDefinicjiStanowiska stanowisko)`, +`new RekrutacjaWakat(DefinicjaStanowiska definicjaStanowiska, Wydzial wydzial)`. + +**`OfertaPracy` (tabela; ogłoszenie o pracę) — ctory:** +`new OfertaPracy(WydziałDefinicjiStanowiska stanowisko)`, `new OfertaPracy(RekrutacjaWakat wakat)`. + +**Workery rekrutacji (klasy zagnieżdżone):** + +| Worker | Metoda | Parametry | +|---|---|---| +| `Soneta.HR2.RekrutacjaAplikacja.NowaRekrutacjaWorker` | rozpoczęcie rekrutacji z aplikacji | `Aplikacje: RekrutacjaAplikacja[]` | +| `Soneta.HR2.RekrutacjaWakat.NowaRekrutacjaWorker` | rozpoczęcie rekrutacji z wakatu | `Wakat: RekrutacjaWakat`, `Pracownicy: Pracownik[]` | +| `Soneta.HR2.OfertaPracy.NowaRekrutacjaWorker` | rozpoczęcie rekrutacji z oferty | `Oferta: OfertaPracy`, `Pracownicy: Pracownik[]` | +| `Soneta.HR.OcenaKandydatowWorker` | `Oceń()` | `Pars`, `Elementy: Rekrutacja[]`; ctor `(Context)` | +| `Soneta.HR.Rekrutacja.ZatrudnijWorker` | `PracHistoria Zatrudnij()` | `Pars`, `Rekrutacja: Rekrutacja` — tworzy zatrudnienie (zapis historii) | + +**Snippet (dodanie aplikacji kandydata — wymaga def. stanowiska w bazie):** + +```csharp +var hr2 = session.GetHR2(); +var kandydat = session.GetKadry().Pracownicy.WgKodu["006"]; + +// Definicja stanowiska z konfiguracji HR (musi istnieć w session.GetHR().DefStanowisk): +var defStanowiska = session.GetHR().DefStanowisk + .Cast().FirstOrDefault(); +var wydzialDefStanowiska = new Soneta.HR.WydziałDefinicjiStanowiska(defStanowiska); + +using (var t = session.Logout(editMode: true)) +{ + var aplikacja = session.AddRow(new RekrutacjaAplikacja(kandydat, wydzialDefStanowiska)); + aplikacja.Data = Date.Today; + aplikacja.Stan = StanAplikacji.Wprowadzona; + t.Commit(); +} +session.Save(); + +// Odczyt aplikacji kandydata: +foreach (RekrutacjaAplikacja a in kandydat.Aplikacje) +{ + // a.Stanowisko, a.Stan, a.Data, a.Oferta +} +``` + +**Pułapki:** +- Cały proces rekrutacji wymaga **konfiguracji HR/HR2** (`DefinicjaStanowiska`, + `DefinicjaEtapuRekrutacji`, `WydziałDefinicjiStanowiska`). W bazie Demo te definicje **mogą nie + istnieć** — przed scenariuszem sprawdź dostępność, inaczej `Save()` rzuci wyjątek weryfikatora. +- `RekrutacjaAplikacja` przyjmuje w ctorze `WydziałDefinicjiStanowiska`, nie samą `DefinicjaStanowiska` + (wydział definicji powstaje z `new WydziałDefinicjiStanowiska(definicjaStanowiska)`). +- `EtapRekrutacji` i `Rekrutacja` implementują `IOcenaPracownika` — oceny etapów trzyma + `EtapRekrutacji.ElementyOceny` (te same `ElementOcenyPracownika` co w KADRY-K8). +- `new Rekrutacja(pracownik)` ustawia pole `Pracownik` i dodaje rekord do **roota** `HRModule.Rekrutacje` + (oraz `EtapRekrutacji` do `HRModule.EtapyRekrutacji`). Kolekcje na `Pracownik` (`Rekrutacje`/`Kandydatury`/ + `EtapyRekrutacji`) to `ChildTable` wiązane przez relacje — do weryfikacji w teście pewniejszy jest root + `session.GetHR().Rekrutacje` niż `pracownik.Rekrutacje` (zależnie od relacji może być pusta dla samego `Pracownik`). +- Zatrudnienie kandydata realizuje `Rekrutacja.ZatrudnijWorker.Zatrudnij()` (zwraca `PracHistoria`) — + spina rekrutację z zatrudnieniem (sekcja A). Worker modyfikuje dane → transakcja + `CommitUI()` + `Save()`. +- `Stan` aplikacji (`Wprowadzona`/`Zakończona`/`Anulowana`) steruje cyklem życia — nie usuwaj aplikacji + z historią, oznaczaj `Anulowana`. diff --git a/soneta-programming/references/domeny/kontrahent.md b/soneta-programming/references/domeny/kontrahent.md deleted file mode 100644 index b7550e1..0000000 --- a/soneta-programming/references/domeny/kontrahent.md +++ /dev/null @@ -1,957 +0,0 @@ -# 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. diff --git a/soneta-programming/references/domeny/pracownik.md b/soneta-programming/references/domeny/pracownik.md deleted file mode 100644 index f8406e6..0000000 --- a/soneta-programming/references/domeny/pracownik.md +++ /dev/null @@ -1,6985 +0,0 @@ -# Pracownik / Kadry-Płace — receptury kodu biznesowego (Soneta / enova365) - -Zbiór gotowych wzorców kodu dla domeny **Kadry i Płace**: obiekt biznesowy -**`Soneta.Kadry.Pracownik`** (tabela `Pracownicy`) wraz z jego historią kadrową, etatem, -nieobecnościami, planem pracy, umowami cywilnoprawnymi i wypłatami. Dokument jest częścią skilla -`soneta-programming`. Celem jest, aby agent pisał **bezbłędny kod biznesowy** operujący na -pracowniku — trafiający w realne pola, kolekcje i workery platformy. - -> Format **zwarty**: każdy wzorzec opisuje ogólny przypadek + tabelę wariantów. Fundamenty (sesja, -> transakcja, blokada optymistyczna, praca z `SubTable`, obsługa błędów, wywoływanie workeró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.Kadry.Pracownik` — `GuidedRow` (root), tabela `Soneta.Kadry.Pracownicy`. -- **Moduły i dostęp z sesji:** - - `Soneta.Kadry.KadryModule` — `session.GetKadry()`; tabela `kadry.Pracownicy`. - - `Soneta.Place.PlaceModule` — `session.GetPlace()`; wypłaty, listy płac, definicje elementów. - - `Soneta.Kalend.KalendModule` — `session.GetKalend()`; nieobecności, kalendarze, plan pracy, RCP, limity. - - `Soneta.HR.HRModule` (`session.GetHR()`), `Soneta.HR2.HR2Module` (`session.GetHR2()`) — definicje - stanowisk, struktura, ZZL/oceny/rekrutacja. -- **Obiekt historyczny:** dane kadrowe i warunki etatu obowiązują „od–do" i są przechowywane w - zapisach historycznych. Kolekcja `Pracownik.Historia: HistorySubTable`. - Rekord `PracHistoria` (tabela `PracHistorie`, child pracownika) zawiera m.in. złożone pole - `Etat: Soneta.Kadry.Etat` (warunki zatrudnienia), adresy, dane podatkowe/ubezpieczeniowe. -- **Najważniejsze pola bazodanowe `Pracownik` (poziom root):** `Kod: string`, `Nazwisko: string`, - `Imie: string`, `PESEL: string`, `ArchiwumInfo`, `NumerRachunkuUS`, `NumerRachunkuZUS`. - (Większość danych kadrowych jest w `PracHistoria`, nie na root.) -- **Kluczowe kolekcje (`SubTable`) na `Pracownik`:** - - `Historia: HistorySubTable` — zapisy historyczne (dane kadrowe + `Etat`). - - `Nieobecnosci: FromToSubTable` — nieobecności. - - `Limity: SubTable` — limity nieobecności (np. urlop). - - `Dodatki: SubTable` — stałe elementy wynagrodzenia (dodatki). - - `Akordy: SubTable` — akordy. - - `Umowy: SubTable` — umowy cywilnoprawne; `UmowyZewnetrzne: SubTable`. - - `Rachunki: SubTable` — rachunki bankowe pracownika. - - `DniPracy: DateSubTable` — plan/realizacja czasu pracy (dzień po dniu). - - `DniRCP: DateSubTable` — zarejestrowany czas pracy (RCP). - - `DniPlanu: DateSubTable` — plan pracy (harmonogram). - - `Kalendarze: SubTable` — kalendarze pracownika. - - `PlanowaneWypłaty`, `PlanowaneElementy`, `PlanowaneNieobecności` — dane planistyczne. -- **Cechy:** `Features: Soneta.Business.FeatureCollection` (indeksator po nazwie definicji cechy). -- **Dane w bazie Demo (GoldStandard):** ~80 zatrudnionych pracowników etatowych, kody `"006"`, `"007"`, - `"008"`, … (po jednym zapisie historii każdy). To stabilne punkty wejścia do scenariuszy odczytu. - -## Podstawowe typy domenowe - -| Typ | Namespace | Zastosowanie | -|---|---|---| -| `Date` | `Soneta.Types` | data bez czasu (daty zatrudnienia, obowiązywania) | -| `FromTo` | `Soneta.Types` | zakres dat „od–do" (okres etatu, nieobecności); `FromTo.Parse`, `FromTo.Year` | -| `Time` | `Soneta.Types` | czas/norma (np. norma dobowa `8:00`) | -| `Fraction` | `Soneta.Types` | wymiar etatu jako ułamek (np. `Fraction.One` = pełny etat, `1/2`) | -| `Currency` / `decimal` | `Soneta.Types` / — | kwoty (stawka, wartość wypłaty) | -| `YearMonth` | `Soneta.Types` | miesiąc rozliczeniowy (okres wypłaty) | - -## Szablon wzorca - -Każdy wzorzec (`Xn`, gdzie `X` = litera sekcji z listy zadań) ma stałą strukturę: - -- **Cel** — co robi i kiedy go użyć. -- **Warianty** — tabela odmian przypadku (gdy dotyczy). -- **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. - -> **Konwencja testów:** każdy wzorzec ma odpowiadający test w -> `Soneta.Skills.Test/KadryPlace/Pracownik/` (klasa dziedzicząca z `PracownikTestBase : TestBase`). -> Testy są wykonywane na bazie Demo z automatycznym rollbackiem — można w nich tworzyć i modyfikować -> dowolne dane. Stanowią wykonywalną dokumentację publicznego API. - ---- - - - -## Spis treści - -- [Fakty o typie](#fakty-o-typie-zweryfikowane-skanem-dll--scan-propscsx) -- [Podstawowe typy domenowe](#podstawowe-typy-domenowe) -- [Szablon wzorca](#szablon-wzorca) -Pozycje oznaczone (★) to funkcjonalności priorytetowe (wprost wskazane). Każda receptura ma własny -podrozdział `### Xn` oraz odpowiadający test. - -- **A. Pracownik — zatrudnienie i dane kartotekowe:** A1 (★) zatrudnienie · A2 (★) dane kadrowe · - A3 adresy · A4 kontakt/WWW · A5 rachunki · A6 PIT · A7 (★) ubezpieczenia · A8 ZUS/NFZ · - A9 (★) rodzina · A10 (★) poprzednie miejsca pracy · A11 wykształcenie/języki/wojsko · A12 GUS · - A13 PFRON · A14 (★) aktualizacja historyczna · A15 odczyt na dzień · A16 powiązanie z kontrahentem · - A17 archiwum · A18 zwolnienie/ZWUA · A19 przerejestrowanie -- **B. Etat:** B1 (★) definiowanie etatu · B2 aneks · B3 przeszeregowanie · B4 rozwiązanie umowy · - B5 obniżenie wymiaru · B6 podzielniki kosztów · B7 definicja stanowiska -- **C. Dodatki, potrącenia, akordy:** C1 (★) dodatki · C2 potrącenia · C3 akordy · C4 zajęcia - komornicze · C5 operacje seryjne · C6 ZFŚS · C7 pożyczki -- **D. Nieobecności i czas pracy:** D1 (★) wprowadzanie · D2 (★) korygowanie · D3 e-ZLA · D4 Z-3/Z-3a · - D5 przestój · D6 okres zasiłkowy · D7 (★) limity (odczyt) · D8 naliczanie limitów · D9 podstawy · - D10 bilans otwarcia · D11 wnioski urlopowe · D12 praca zdalna -- **E. Plan pracy i kalendarz:** E1 (★) plan czasu pracy · E2 (★) kopiowanie planu · - E3 aktualizacja kalendarza · E4 doba pracownicza · E5 odczyt normy/czasu przepracowanego -- **F. RCP:** F1 (★) rejestracja czasu · F2 (★) wejścia/wyjścia · F3 import RCP · F4 weryfikacja/korekta · - F5 praca hybrydowa/podzielniki -- **G. Umowy cywilnoprawne:** G1 (★) dodawanie · G2 (★) zmiana/aneks · G3 operacja seryjna · - G4 rozliczenie/rachunek umowy · G5 zgłoszenia ZUS zleceniobiorców -- **H. Płace — naliczanie wypłat:** H1 (★) etatowe · H2 (★) z umów · H3 (★) pozostałe · H4 (★) odczyt - za rok · H5 elementy wypłaty · H6 zaliczka · H7 przelicz podatki · H8 dochód · H9 kalkulator · - H10 stornowanie · H11 anulowanie/bufor -- **I. Listy płac, przelewy, wydruki:** I1 (★) listy płac · I2 (★) PDF paska · I3 (★) PDF listy · - I4 przelewy · I5 eksport do banku · I6 rozliczenia/faktura -- **J. Deklaracje (ZUS, PIT, PFRON, PPK):** J1 zgłoszenia ZUS · J2 rozliczeniowe ZUS · J3 PIT · - J4 PFRON · J5 PPK · J6 bilanse otwarcia -- **K. Ewidencje pracownicze:** K1 badania lekarskie · K2 BHP · K3 szkolenia/uprawnienia · - K4 nagrody/kary/oświadczenia · K5 wypadki · K6 RODO · K7 struktura organizacyjna · K8 oceny · - K9 rekrutacja - -> **Testy weryfikujące** (wykonywalna dokumentacja, baza Demo z rollbackiem) w -> `Soneta.Skills.Test/KadryPlace/Pracownik/`: `SmokeTest`, `RozdzialA_PracownikTest`, -> `RozdzialArest_KartotekaTest`, `RozdzialBC_EtatDodatkiTest`, `RozdzialBrest_EtatTest`, -> `RozdzialCrest_PotraceniaTest`, `RozdzialD_NieobecnosciTest`, `RozdzialDrest_NieobecnosciTest`, -> `RozdzialEF_PlanRcpTest`, `RozdzialEFrest_PlanRcpTest`, `RozdzialG_UmowyTest`, `RozdzialGrest_UmowyTest`, -> `RozdzialH_WyplatyTest`, `RozdzialHrest_WyplatyTest`, `RozdzialI_ListyWydrukiTest`, -> `RozdzialIrest_PrzelewyTest`, `RozdzialJ_DeklaracjeTest`, `RozdzialK1_EwidencjeTest`, -> `RozdzialK2_RodoZzlTest`. Łącznie **172 testy** (134 zielone, 38 świadomie pominiętych `[Ignore]` — -> operacje wymagające sieci, `Context`/`KEDU`, konfiguracji niedostępnej w Demo, praw dostępu lub -> składowych `internal`; powód podany przy każdym pominiętym teście i w pułapkach receptury). - ---- - -## A. Pracownik — zatrudnienie i dane kartotekowe - -> **Model „root + historia".** `Pracownik` (root, tabela `Pracownicy`) trzyma tylko nieliczne pola -> niezmienne w czasie (`Kod`, `Net`, `NumerRachunkuUS`, `NumerRachunkuZUS`, `Rodzaj`, `Typ`, -> `Wieloetatowosc`). **Praktycznie wszystkie dane kadrowe są historyczne** i leżą w zapisach -> `PracHistoria` (tabela `PracHistorie`, child `Pracownik`-a, też `GuidedRow` root z własnym Guid). -> Kolekcją zapisów jest `Pracownik.Historia: HistorySubTable`. Warunki -> zatrudnienia (umowa, wymiar, ubezpieczenia, stanowisko) siedzą w złożonym polu -> `PracHistoria.Etat: Soneta.Kadry.Etat`. -> -> **Skróty na rootcie** (delegują do zapisu „na dziś"/ostatniego): -> - `pracownik.Last : PracHistoria` — **ostatni** (najświehigy) zapis historii; do edycji świeżo -> utworzonego pracownika i odczytu „bieżących" danych kartotekowych. -> - `pracownik[date] : PracHistoria` — indeksator zwracający zapis **obowiązujący na zadaną datę**. -> - Wiele pól osobowych jest też dostępnych z poziomu rootu jako property delegujące (np. `Imie`, -> `Nazwisko`, `PESEL`) — ale **kanonicznie ustawiamy je na zapisie** (`Last.Imie`, `pracownik[d].PESEL`). -> -> **`Pracownik` jest klasą abstrakcyjną** — nie da się zrobić `new Pracownik()`. Konkretny typ -> pracownika firmy to **`Soneta.Kadry.PracownikFirmy`**. - -### A1 — Zatrudnienie nowego pracownika (★) - -**Cel:** utworzyć nowego pracownika z minimalnym kompletem danych pozwalającym na `Session.Save()`. - -**Mechanizm (kluczowy):** dodanie nowego `PracownikFirmy` do tabeli (`AddRow`) automatycznie tworzy -**pierwszy zapis** `PracHistoria` oraz kalendarz pracownika (dzieje się w `OnAdded`). Dlatego **nie -tworzymy** `PracHistoria` ręcznie przy zatrudnieniu — bezpośrednio po `AddRow` istnieje już -`pracownik.Last` (pierwszy zapis), na którym ustawiamy dane osobowe. - -**Pola minimalne do zapisu:** - -| Pole | Gdzie | Typ | Uwaga | -|---|---|---|---| -| `Kod` | `Pracownik` (root) | `string` | identyfikator; przy pustym platforma podstawia prefiks + `?` | -| `Imie` | `PracHistoria` (`Last.Imie`) | `string` | wymagane (domyślnie `"?"` z `OnAdded`) | -| `Nazwisko` | `PracHistoria` (`Last.Nazwisko`) | `string` | wymagane (domyślnie `"?"`) | - -Pierwszy zapis historii ma okres otwarty (do `Date.MaxValue`); warunki etatu (A1 → B) ustawia się -na `Last.Etat` (np. `Etat.Okres`, `Etat.TypUmowy`, `Etat.Wymiar`) — szczegóły w sekcji B. - -**Snippet:** - -```csharp -var kadry = session.GetKadry(); - -using (var t = session.Logout(editMode: true)) -{ - // AddRow zwraca dodany, typowany wiersz; w OnAdded powstaje pierwszy PracHistoria + kalendarz - var pracownik = session.AddRow(new PracownikFirmy()); - - pracownik.Kod = "555"; // pole rootu - - // dane osobowe — na ZAPISIE historii (pierwszy zapis = Last) - pracownik.Last.Nazwisko = "Kowalska"; - pracownik.Last.Imie = "Gabriela"; - pracownik.Last.PESEL = "94010812345"; - - t.Commit(); // Commit() w kodzie biznesowym; CommitUI() w workerze/UI -} -session.Save(); // zapis do bazy; tu wykrywane konflikty/duplikaty -``` - -**Pułapki:** -- **Nie** rób `new Pracownik()` — typ jest abstrakcyjny. Używaj `new PracownikFirmy()`. -- **Nie** dodawaj ręcznie pierwszego `PracHistoria` — robi to `OnAdded`. Ręczne dodanie zapisu - dotyczy dopiero *aktualizacji* danych „od daty" (A14). -- Dane osobowe ustawiaj na `Last`/`pracownik[date]`, nie próbuj ich „obejść" przez root — root - deleguje do zapisu, ale zapis jest właściwym miejscem. -- `Kod` bywa polem wymagającym unikalności (zależnie od konfiguracji) — kolizja wybuchnie w - `Save()` jako `RowException` (z `DuplicateKeyException` w `InnerException`). -- Całość w transakcji (`session.Logout(editMode: true)`); brak `Commit()` = rollback przy `Dispose()`. - -### A2 — Podstawowe dane kadrowe (★) - -**Cel:** uzupełnić dane ewidencyjno-identyfikacyjne pracownika (PESEL, NIP, urodzenie, płeć, -obywatelstwo, rodzice, dokument tożsamości, adresy). - -**Gdzie leżą pola — root vs PracHistoria:** - -| Dana | Lokalizacja | Pole / typ | -|---|---|---| -| Imię, drugie imię, nazwisko | `PracHistoria` | `Imie`, `ImieDrugie`, `Nazwisko: string` | -| Nazwisko rodowe, imię ojca/matki, nazwisko rodowe matki | `PracHistoria` | `NazwiskoRodowe`, `ImieOjca`, `ImieMatki`, `NazwiskoRodoweMatki: string` | -| PESEL | `PracHistoria` (oraz delegat na root) | `PESEL: string` | -| NIP | `PracHistoria` | `NIP: string` | -| Płeć | `PracHistoria` | `Plec: Soneta.Kadry.PłećOsoby` (`Kobieta`/`Mężczyzna`) | -| Data i miejsce urodzenia | `PracHistoria` (subrow `Urodzony`) | `Urodzony.Data: Date`, `Urodzony.Miejsce: string` | -| Obywatelstwo | `PracHistoria` (subrow `Obywatelstwo`) | `Obywatelstwo.Nazwa: string` (słownik), `Obywatelstwo.KodKraju: string` | -| Dokument tożsamości | `PracHistoria` (subrow `Dokument`) | `Dokument.Rodzaj: KodRodzajuDokumentu`, `Dokument.SeriaNumer: string`, `Dokument.WydanyPrzez`, `Dokument.DataWydania/DataWaznosci: Date` | -| Adres zamieszkania / zameldowania / korespondencji | `PracHistoria` | `Adres`, `AdresZameldowania`, `AdresZamieszkania`, `AdresDoKorespondencji: Soneta.Core.Adres` | -| Urząd skarbowy, koszty/ulga | `PracHistoria` (subrow `Podatki`) | `Podatki.UrzadSkarbowy`, `Podatki.KosztyRodzaj`, `Podatki.UlgaMnoznik`, … | -| Kod, numery rachunków US/ZUS | `Pracownik` (root) | `Kod: string`, `NumerRachunkuUS`, `NumerRachunkuZUS` | - -**Pułapki:** -- `Plec` jest **wyliczana z PESEL** przez weryfikator — przy poprawnym PESEL nie musisz jej ustawiać; - ustawienie ręczne ma sens dla osób bez PESEL. Typ to enum `PłećOsoby`, nie string. -- `Urodzony`, `Obywatelstwo`, `Dokument`, `Podatki` to **subrowy** (pola złożone) — edytujesz ich - pola (`Last.Urodzony.Data = …`), nie przypisujesz całego obiektu. -- `Adres` (i pozostałe) to property zwracające `Soneta.Core.Adres` — modyfikuj ich pola - (`Last.Adres.Miejscowosc = …`), nie przypisuj `Last.Adres = …`. `KodPocztowy` jest `int`; do - wartości z myślnikiem używaj `Adres.KodPocztowyS` (string). -- PESEL/NIP są danymi wrażliwymi — nie loguj ich (safe-code §12). - -**Snippet:** - -```csharp -var pracownik = session.GetKadry().Pracownicy.WgKodu["555"]; - -using (var t = session.Logout(editMode: true)) -{ - var ph = pracownik.Last; // bieżący (ostatni) zapis kadrowy - - ph.NazwiskoRodowe = "Nowak"; - ph.ImieOjca = "Jan"; - ph.NIP = "1234563218"; - - ph.Urodzony.Data = new Date(1994, 1, 8); // subrow Urodzony - ph.Urodzony.Miejsce = "Kraków"; - ph.Obywatelstwo.Nazwa = "polskie"; // subrow Obywatelstwo (słownik) - - ph.Adres.Ulica = "Wadowicka"; // subrow Adres - ph.Adres.NrDomu = "8A"; - ph.Adres.KodPocztowyS = "30-415"; - ph.Adres.Miejscowosc = "Kraków"; - - t.Commit(); -} -session.Save(); -``` - -### A7 — Dane ubezpieczenia społecznego i zdrowotnego (★) - -**Cel:** ustawić/odczytać tytuł ubezpieczenia, oddział NFZ oraz parametry ubezpieczeń społecznych -(emerytalne, rentowe, chorobowe, wypadkowe) i zdrowotnego — daty zgłoszeń i wyrejestrowań. - -**Gdzie leżą pola:** - -| Dana | Lokalizacja | Pole / typ | -|---|---|---| -| Tytuł ubezpieczenia (kod) | `PracHistoria.Etat.Ubezpieczenia` | `Tyub4: Soneta.Kadry.TytulUbezpieczenia4` (rekord słownika, tytuł 6-znakowy) | -| Zbiorczy stan ubezpieczeń | `PracHistoria.Etat` | `Etat.Ubezpieczenia: Soneta.Kadry.Ubezpieczenia` (subrow) | -| Społeczne (poszczególne) | `…Etat.Ubezpieczenia.*` | `Emerytalne`, `Rentowe`, `Chorobowe`, `Wypadkowe : Soneta.Kadry.Spoleczne` | -| Zdrowotne | `…Etat.Ubezpieczenia.Zdrowotne` | `Soneta.Kadry.Zdrowotne` (subrow) | -| Data objęcia ubezpieczeniami społ. (od) | `…Etat.Ubezpieczenia` (zbiorczo) | `ObowiazkoweOd: Date` — **zapisywalne** na zbiorczym subrowie `Ubezpieczenia` | -| Objęcie poszczególnym społ. | `…Ubezpieczenia.Emerytalne` itd. | `Obowiazkowe: bool`, `Dobrowolne: bool`, `DobrowolneOd: Date`, `Do: Date` (wyrej.) — **zapisywalne**; `Od: Date` jest tylko do odczytu (wyliczane) | -| Data objęcia zdrowotnym | `…Ubezpieczenia.Zdrowotne` | `ObowiazkoweOd: Date` — **zapisywalne** (asymetria względem `Spoleczne`) | -| Przyczyna wyrejestrowania | `…Ubezpieczenia.Emerytalne.Przyczyna` | `Przyczyna: Soneta.Kadry.Wyrejestrowanie` (subrow z kodem) | -| Oddział NFZ | `PracHistoria.OddzialNFZ` | `OddzialNFZ: Soneta.Kadry.OddzialNFZ` (subrow; `Oddział`, `KodGminy`, `OdDnia`) | -| Tytuł na dzień (odczyt) | `PracHistoria.Etat.Ubezpieczenia` | `WyliczTyubNaDzień(Date)` | - -**Pułapki:** -- Cała struktura ubezpieczeń jest **historyczna** (siedzi w `Etat` danego `PracHistoria`) — zmiana - „od daty" wymaga nowego zapisu historii (A14), nie nadpisywania bieżącego. -- `Tyub4` to rekord **konfiguracyjnego** słownika `TytulUbezpieczenia4` — pobierz istniejący wpis - przez `session.GetKadry().TytulyUbezpiecz4.WgKodu[kod]`, gdzie **`kod` jest typu `int`** (np. `110`, - `2241`), nie twórz „w locie". (Pole `Tyub`/`TypUbezpieczenia` to starsze typy — używaj `Tyub4`.) -- `OddzialNFZ` to subrow z polem `Oddział` (enum oddziałów) — ustawiasz `OddzialNFZ.Oddział`, nie - całą strukturę. -- `Emerytalne`/`Rentowe`/`Chorobowe`/`Wypadkowe` to subrowy `Spoleczne`. Ustawiasz na nich flagi - `Obowiazkowe`/`Dobrowolne` oraz `DobrowolneOd`/`Do`. **`Od` jest tylko do odczytu** (wyliczane) — - datę objęcia ubezpieczeniami obowiązkowymi ustawiasz **zbiorczo** przez `Ubezpieczenia.ObowiazkoweOd`. - Na subrowie `Zdrowotne` z kolei `ObowiazkoweOd` jest zapisywalne bezpośrednio. - -**Snippet:** - -```csharp -var pracownik = session.GetKadry().Pracownicy.WgKodu["555"]; - -// Odczyt tytułu ubezpieczenia obowiązującego na dziś: -var data = Date.Today; -TytulUbezpieczenia tyubNaDzis = pracownik[data].Etat.Ubezpieczenia.WyliczTyubNaDzień(data); - -using (var t = session.Logout(editMode: true)) -{ - var ub = pracownik.Last.Etat.Ubezpieczenia; // subrow ubezpieczeń bieżącego zapisu - - // Tytuł ubezpieczenia (rekord słownika konfiguracyjnego); klucz WgKodu jest po Kod: int: - ub.Tyub4 = session.GetKadry().TytulyUbezpiecz4.WgKodu[110]; // np. 0110 = pracownik - - // Data objęcia ubezpieczeniami społecznymi obowiązkowymi — ZBIORCZO (Od na Spoleczne jest read-only): - ub.ObowiazkoweOd = new Date(2026, 1, 1); - ub.Emerytalne.Obowiazkowe = true; - ub.Rentowe.Obowiazkowe = true; - ub.Chorobowe.Obowiazkowe = true; - - // Ubezpieczenie zdrowotne — datę objęcia ustawiasz wprost na subrowie Zdrowotne: - ub.Zdrowotne.ObowiazkoweOd = new Date(2026, 1, 1); - - // Oddział NFZ (subrow): - pracownik.Last.OddzialNFZ.OdDnia = new Date(2026, 1, 1); - - t.Commit(); -} -session.Save(); -``` - -### A9 — Dane o rodzinie pracownika (★) - -**Cel:** ewidencjonować członków rodziny i zgłaszać ich do ubezpieczenia zdrowotnego (ZCNA). - -**Kolekcja i typ:** `Pracownik.Rodzina: SubTable` (tabela `Rodzina`, -`GuidedRow` root, child `Pracownik`-a). Nowy członek rodziny tworzony jest konstruktorem -`new CzlonekRodziny(pracownik)`. - -**Pola i typy (`CzlonekRodziny`):** - -| Pole | Typ | Opis | -|---|---|---| -| `Nazwisko`, `Imie`, `ImieDrugie` | `string` | dane osobowe (wymagane: `Nazwisko`, `Imie`) | -| `PESEL`, `NIP` | `string` | identyfikatory | -| `Urodzony` | `Soneta.Kadry.Urodzony` (subrow) | `Urodzony.Data: Date`, `Urodzony.Miejsce: string` | -| `Dokument` | `Soneta.Kadry.DokumentOsoby` (subrow) | dokument tożsamości | -| `StPokrewienstwa` | `Soneta.Kadry.KodStPokrewienstwa` (enum) | stopień pokrewieństwa | -| `Ubezpieczony` | `bool` | zgłoszony do ubezpieczenia zdrowotnego (ZCNA) | -| `UbezpieczenieOkres` | `Soneta.Types.FromTo` | okres zgłoszenia do ubezpieczenia | -| `StNiepelnosprawnosci` | `Soneta.Kadry.KodStNiepelnosprawnosci` (enum) | stopień niepełnosprawności | -| `WspolneGospDomowe`, `NaUtrzymaniu`, `OdbKsztalcenie` | `bool` | przesłanki zgłoszenia | -| `Adres` | `Soneta.Core.Adres` | adres (gdy inny niż pracownika) | -| `Pracownik` | `Soneta.Kadry.Pracownik` | właściciel (ustawiany ctorem) | - -**Pułapki:** -- Zgłoszenie do ubezpieczenia zdrowotnego (ZCNA) realizuje się przez `Ubezpieczony = true` + - `UbezpieczenieOkres` + `StPokrewienstwa` — to z tych pól generowana jest deklaracja ZCNA. Brak - dedykowanego „pola daty wysyłki ZCNA" na członku rodziny. -- `CzlonekRodziny` nie jest historyczny — to płaski child pracownika; okres ubezpieczenia trzyma - pole `UbezpieczenieOkres: FromTo`. -- Konstruktor `new CzlonekRodziny(pracownik)` od razu wiąże rekord z pracownikiem; pojawia się on - w `pracownik.Rodzina`. - -**Snippet:** - -```csharp -var pracownik = session.GetKadry().Pracownicy.WgKodu["555"]; - -using (var t = session.Logout(editMode: true)) -{ - var dziecko = new CzlonekRodziny(pracownik); // ctor wiąże z pracownikiem - session.AddRow(dziecko); - dziecko.Nazwisko = "Kowalska"; - dziecko.Imie = "Zofia"; - dziecko.PESEL = "20290512345"; - dziecko.Urodzony.Data = new Date(2020, 9, 5); - dziecko.StPokrewienstwa = KodStPokrewienstwa.Dziecko; // wartość enum wg słownika - - // Zgłoszenie do ubezpieczenia zdrowotnego (ZCNA): - dziecko.Ubezpieczony = true; - dziecko.UbezpieczenieOkres = new FromTo(new Date(2026, 1, 1), Date.MaxValue); - dziecko.NaUtrzymaniu = true; - - t.Commit(); -} -session.Save(); - -// Odczyt aktualnie ubezpieczonych członków rodziny — filtr serwerowy po kolekcji: -foreach (CzlonekRodziny cr in pracownik.Rodzina[(CzlonekRodziny c) => c.Ubezpieczony]) -{ - // cr.Nazwisko, cr.Imie, cr.StPokrewienstwa -} -``` - -### A10 — Poprzednie miejsca pracy (★) - -**Cel:** rejestrować historię zatrudnienia u poprzednich pracodawców i okresy nauki (do wyliczenia -stażu pracy i uprawnień urlopowych). - -**Kolekcja i typ:** `Pracownik.HistoriaZatrudnienia: SubTable` -(tabela `HistZatrudnien`, `GuidedRow` root, child `Pracownik`-a). To **inna kolekcja niż -`Pracownik.Historia`** — `Historia` to historia *bieżącego* zatrudnienia (zapisy `PracHistoria`), -a `HistoriaZatrudnienia` to staż u *poprzednich* pracodawców. - -`HistoriaZatrudnieniaBase` jest typem bazowym z **konstruktorem `protected`** — nie da się go -utworzyć bezpośrednio. Konkretne typy do tworzenia wpisów to: -- `Soneta.Kadry.HistoriaZatrudnienia` — poprzedni pracodawca (ustawia `Typ = Zatrudnienie`), -- `Soneta.Kadry.UkonczonaSzkola` — okres nauki. -Oba mają publiczny ctor `(Pracownik)`. - -**Pola i typy (`HistoriaZatrudnieniaBase`):** - -| Pole | Typ | Opis | -|---|---|---| -| `Typ` | `Soneta.Kadry.TypHistoriiZatrudnienia` | rodzaj wpisu (praca / nauka) — ustawiany przez ctor konkretnej klasy, readonly | -| `Nazwa` | `string` | nazwa zakładu pracy / szkoły (wymagane) | -| `Okres` | `Soneta.Types.FromTo` | okres zatrudnienia/nauki | -| `EfektywnyOkres` | `Soneta.Types.FromTo` | okres efektywnie wliczany do stażu | -| `Adres1`, `Adres2` | `string` | adres pracodawcy | -| `Korekta` | `Soneta.Kadry.StazPracy` | ręczna korekta naliczonego stażu | -| `Staz` | `Soneta.Kadry.StazPracyPracownika` | wyliczony staż (kalkulowane) | -| `RodzajDokumentu` | `Soneta.Kadry.RodzajDokumentu` | dokument potwierdzający | -| `Pracownik` | `Soneta.Kadry.Pracownik` | właściciel (relacja, readonly) | - -**Pułapki:** -- **Nie** rób `new HistoriaZatrudnieniaBase(...)` — ctor jest `protected`. Twórz konkretny typ: - `new HistoriaZatrudnienia(pracownik)` (praca u poprzedniego pracodawcy, `Typ = Zatrudnienie`) albo - `new UkonczonaSzkola(pracownik)` (nauka). -- `EfektywnyOkres` ⊆ `Okres` — to on (a nie sam `Okres`) decyduje o wliczeniu do stażu; jeśli go nie - ustawisz, obowiązują weryfikatory ciągłości. -- Wpisy są niezależne od `PracHistoria` — nie myl `HistoriaZatrudnienia` (poprzedni pracodawcy) - z `Historia` (zapisy bieżącego zatrudnienia). - -**Snippet:** - -```csharp -var pracownik = session.GetKadry().Pracownicy.WgKodu["555"]; - -using (var t = session.Logout(editMode: true)) -{ - // konkretny typ: poprzedni pracodawca (Typ = Zatrudnienie ustawia ctor); AddRow zwraca typowany wiersz - var hz = session.AddRow(new HistoriaZatrudnienia(pracownik)); - hz.Nazwa = "Poprzednia Firma Sp. z o.o."; - hz.Okres = new FromTo(new Date(2018, 3, 1), new Date(2025, 12, 31)); - hz.EfektywnyOkres = hz.Okres; - hz.Adres1 = "ul. Główna 1, Kraków"; - t.Commit(); -} -session.Save(); - -// Odczyt historii zatrudnienia (wszystkie typy wpisów, bazowy typ kolekcji): -foreach (HistoriaZatrudnieniaBase hz in pracownik.HistoriaZatrudnienia) -{ - // hz.Nazwa, hz.Okres, hz.EfektywnyOkres, hz.Typ -} -``` - -### A14 — Aktualizacja danych historycznych: zmiana „od daty" vs korekta (★) - -**Cel:** poprawnie zmienić dane kadrowe — **nowy zapis obowiązujący od wskazanego dnia** (zmiana -warunków: podwyżka, zmiana wymiaru etatu, zmiana danych podatkowych) **kontra korekta istniejącego -zapisu** (poprawa błędu w obecnym okresie). Plus: odczyt zapisu obowiązującego „na dzień". - -**Mechanizm `HistorySubTable`:** - -| Operacja | API | Efekt | -|---|---|---| -| Odczyt zapisu na dzień | `pracownik[date]` (== `(PracHistoria)Historia[date]`) | zwraca zapis, którego `Aktualnosc` zawiera `date` | -| Pierwszy zapis | `pracownik.Historia.GetFirst()` | najstarszy zapis | -| Ostatni zapis | `pracownik.Last` (== `Historia.GetPrev()`) | najświeższy zapis | -| **Nowy zapis „od daty"** | `(PracHistoria)pracownik.Historia.Update(date)` | **klonuje** zapis aktualny na `date`, skraca jego okres do `date-1`, zwraca **nowy** klon z okresem od `date`; nowy klon trzeba dodać do tabeli | -| Okres obowiązywania | `PracHistoria.Aktualnosc: Soneta.Types.FromTo` | „od–do" zapisu (zarządzane przez historię) | - -**Wzorzec aktualizacji „od daty" (zmiana warunków od dnia):** - -```csharp -var pracownik = session.GetKadry().Pracownicy.WgKodu["555"]; -var odDnia = new Date(2026, 7, 1); - -using (var t = session.Logout(editMode: true)) -{ - // 1) Update klonuje zapis aktualny na `odDnia` i zwraca nowy klon (okres od `odDnia`). - // Stary zapis zostaje skrócony do dnia poprzedniego. - var nowy = (PracHistoria)pracownik.Historia.Update(odDnia); - - // 2) Klon trzeba dodać do tabeli zapisów historii. - pracownik.Module.PracHistorie.AddRow(nowy); - - // 3) Na nowym zapisie wprowadzamy zmienione warunki (od `odDnia`): - nowy.Etat.MiejscePracy = "Oddział Kraków"; // np. zmiana miejsca pracy - nowy.Podatki.UlgaMnoznik = 1m; // np. zmiana danych podatkowych - - // Uwaga: część pól Etat (np. Etat.Zaszeregowanie.Wymiar) na świeżym klonie potrafi być - // w trybie tylko-do-odczytu (ColReadOnlyException) — odblokowanie zależy od konfiguracji etatu - // (patrz pułapki w sekcji B1: bramką jest Etat.Okres). Dla pewności w przykładzie zmieniamy - // pola bezpiecznie zapisywalne (MiejscePracy, dane podatkowe). - - t.Commit(); -} -session.Save(); -``` - -**Wzorzec korekty istniejącego zapisu (bez nowego okresu):** - -```csharp -using (var t = session.Logout(editMode: true)) -{ - // Modyfikujemy zapis obowiązujący na zadaną datę — bez Update, bez AddRow. - var ph = pracownik[new Date(2026, 3, 15)]; - ph.NazwiskoRodowe = "PoprawioneNazwisko"; // korekta w istniejącym okresie - t.Commit(); -} -session.Save(); -``` - -**Pułapki:** -- **`Update(date)` + `AddRow(nowy)` to nierozłączna para.** Sam `Update` tworzy „odpięty" klon — - bez `PracHistorie.AddRow(nowy)` zmiana nie zostanie zapisana. -- `Update(date)` rzuca `HistorySubTable.DateDuplicateException`, gdy na `date` już zaczyna się zapis - (`Aktualnosc.From == date`) — nie da się „aktualizować" dwa razy tego samego dnia; wtedy - modyfikuj istniejący zapis (`pracownik[date]`). -- **Korekta** (modyfikacja `pracownik[date]`) zmienia dane w **całym** okresie tego zapisu — używaj - jej tylko do poprawy błędu, nie do „zmiany od dnia". -- `Aktualnosc` (okres zapisu) jest zarządzany przez mechanizm historii — **nie ustawiaj go ręcznie**; - do skrócenia/wstawienia okresu służy `Update`. -- Odczyt „na dzień": `pracownik[date]` zwraca `null`, jeśli dla daty brak zapisu — dla daty sprzed - zatrudnienia. `pracownik.Last` zawsze zwraca najświeższy zapis. -- Aktualizacja danych to operacja na danych operacyjnych pracownika — trzymaj transakcje krótkie - (safe-code §13.1) i obsłuż `RowConflictException` z `Save()` (safe-code §4). - -### A3 — Adresy (zameldowania / zamieszkania / korespondencyjny) - -**Cel:** uzupełnić/odczytać adresy pracownika. Adresy są **historyczne** — leżą na zapisie -`PracHistoria`, dostęp przez `pracownik.Last` (bieżący) lub `pracownik[date]` (na dzień). Każdy adres -to subrow typu `Soneta.Core.Adres` — modyfikujesz jego pola, nie przypisujesz całego obiektu. - -**Gdzie leżą pola — `PracHistoria` (subrowy `Soneta.Core.Adres`):** - -| Dana | Pole / typ | Uwaga | -|---|---|---| -| Adres podstawowy (kanoniczny) | `Adres: Soneta.Core.Adres` | adres główny pracownika | -| Adres zameldowania | `AdresZameldowania: Soneta.Core.Adres` | | -| Adres zamieszkania | `AdresZamieszkania: Soneta.Core.Adres` | | -| Adres do korespondencji | `AdresDoKorespondencji: Soneta.Core.Adres` | | -| Adres na przelewach | `AdresNaPrzelewach: Soneta.Kadry.AdresPracownikaNaPrzelewach` | osobny typ — adres umieszczany na przelewach | - -**Pola subrowa `Soneta.Core.Adres` (zapisywalne, `bazodanowe`):** - -| Pole | Typ | Opis | -|---|---|---| -| `Miejscowosc` | `string` | miejscowość | -| `Ulica` | `string` | nazwa ulicy/alei/osiedla | -| `NrDomu` | `string` | numer domu/bloku | -| `NrLokalu` | `string` | numer lokalu | -| `KodPocztowy` | `int` | kod pocztowy (liczbowo) | -| `KodPocztowyS` | `string` | kod pocztowy z myślnikiem (np. `"30-415"`) | -| `Poczta` | `string` | poczta | -| `Gmina`, `Powiat` | `string` | gmina, powiat | -| `Wojewodztwo` | `Soneta.Core.Wojewodztwa` (enum) | województwo | -| `Kraj`, `KodKraju` | `string` | kraj / kod kraju | -| `ZagranicznyKodPocztowy` | `string` | zagraniczny kod pocztowy | -| `Telefon`, `Faks` | `string` | telefon/faks związany z adresem | -| `Pełny` | `string` | sformatowany adres (tylko odczyt) | - -**Pułapki:** -- `Adres`, `AdresZameldowania`, … to **subrowy** — modyfikuj pola (`Last.AdresZamieszkania.Ulica = …`), - **nie** przypisuj całego obiektu (`Last.AdresZamieszkania = …` — błąd). -- `KodPocztowy` to `int`; do wartości z myślnikiem używaj `KodPocztowyS` (string). -- Cała struktura jest historyczna — zmiana adresu „od daty" to nowy zapis historii (A14), korekta - bieżącego okresu to modyfikacja `pracownik[date]`. -- `Wojewodztwo` to enum `Soneta.Core.Wojewodztwa`, nie string. - -**Snippet:** - -```csharp -var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; - -using (var t = session.Logout(editMode: true)) -{ - var ph = pracownik.Last; - - ph.AdresZamieszkania.Miejscowosc = "Kraków"; - ph.AdresZamieszkania.Ulica = "Wadowicka"; - ph.AdresZamieszkania.NrDomu = "8A"; - ph.AdresZamieszkania.NrLokalu = "12"; - ph.AdresZamieszkania.KodPocztowyS = "30-415"; - - ph.AdresZameldowania.Miejscowosc = "Wieliczka"; - ph.AdresDoKorespondencji.Miejscowosc = "Kraków"; - - t.Commit(); -} -session.Save(); - -// Odczyt adresu na dzień: -Adres adr = pracownik[Date.Today].AdresZamieszkania; -string opis = $"{adr.Ulica} {adr.NrDomu}, {adr.KodPocztowyS} {adr.Miejscowosc}"; -``` - ---- - -### A4 — Dane kontaktowe (e-mail, telefon) i dostęp WWW/Pulpity - -**Cel:** ustawić/odczytać dane kontaktowe pracownika (e-mail, telefon komórkowy, WWW). Dane kontaktowe -leżą w subrowie `Kontakt: Soneta.Core.Kontakt` — dostępnym zarówno na rootcie `Pracownik`, jak i na -zapisie historii `PracHistoria` (historyczne). Pracownik dodatkowo udostępnia `EMAIL: string` na rootcie. - -**Gdzie leżą pola — subrow `Soneta.Core.Kontakt` (`PracHistoria.Kontakt` / `Pracownik.Kontakt`):** - -| Pole | Typ | Opis | -|---|---|---| -| `EMAIL` | `string` | adres poczty elektronicznej | -| `TelefonKomorkowy` | `string` | telefon komórkowy | -| `WWW` | `string` | adres strony internetowej | -| `Skype` | `string` | identyfikator Skype | -| `SkrytkaPocztowa` | `string` | skrytka pocztowa | - -**Telefon stacjonarny/faks** — w kontekście adresu: `PracHistoria.Adres.Telefon`, `…Adres.Faks: string` -(patrz A3). **Rozbudowane kanały kontaktu** (wiele kontaktów z rodzajem/celem): kolekcja -`PracHistoria.Kontakty: SubTable` (pola `Kontakt: string`, -`Rodzaj: Soneta.Core.RodzajKontaktu`, `Domyslny: bool`, `Opis: string`). - -**Dostęp WWW / Pulpity (IWebOperator):** `Pracownik` implementuje interfejs -`Soneta.…IWebOperator`. Konto dostępu do Pulpitów (operator web, login, uprawnienia) **nie jest -zwykłym zapisywalnym polem** pracownika — jest zarządzane osobnym mechanizmem operatorów/uprawnień -modułu web (poza prostym kontraktem ustawiania pól na pracowniku). W publicznym kontrakcie danych -kartotekowych operujesz danymi kontaktowymi (e-mail/telefon/WWW), a powiązanie operatora web jest -realizowane przez konfigurację operatorów, nie przez `pracownik.Last`. - -**Pułapki:** -- `Kontakt` to **subrow** — modyfikuj pola (`Last.Kontakt.EMAIL = …`), nie przypisuj całego obiektu. -- Pole nazywa się `EMAIL` (wielkimi literami) — uwaga na wielkość liter. -- E-mail/telefon w kontekście „na przelewach"/PPK to inne pola (`OdpisPPK.Email`, - `OdpisPPK.TelefonKomorkowy`) — nie myl z kontaktem osobowym. -- Dostęp do Pulpitu (IWebOperator) nie jest częścią `PracHistoria` — nie szukaj „pola WWW dostępu" - na zapisie kadrowym. - -**Snippet:** - -```csharp -var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; - -using (var t = session.Logout(editMode: true)) -{ - var k = pracownik.Last.Kontakt; // subrow Kontakt bieżącego zapisu - k.EMAIL = "g.kowalska@firma.pl"; - k.TelefonKomorkowy = "600100200"; - k.WWW = "https://firma.pl/g.kowalska"; - t.Commit(); -} -session.Save(); - -// Odczyt: -string mail = pracownik.Last.Kontakt.EMAIL; -``` - ---- - -### A5 — Rachunki bankowe (rachunek do przelewu wynagrodzenia) - -**Cel:** zarejestrować/odczytać rachunki bankowe pracownika oraz wskazać rachunek główny do przelewu -wynagrodzenia. - -**Kolekcja i typ:** `Pracownik.Rachunki: SubTable` (rachunki są -na rootcie pracownika, nie w historii). Rachunek główny (domyślny) zwraca -`Pracownik.DomyslnyRachunek: Soneta.Kasa.RachunekBankowyPodmiotu`. Numery rachunków US/ZUS pracownika -to osobne pola rootu: `Pracownik.NumerRachunkuUS: Soneta.Core.NumerRachunkuUS`, -`Pracownik.NumerRachunkuZUS: Soneta.Core.NumerRachunkuZUS`. - -**Pola i typy (`Soneta.Kasa.RachunekBankowyPodmiotu`):** - -| Pole | Typ | Opis | -|---|---|---| -| `Rachunek` | `Soneta.Kasa.RachunekBankowy` (subrow) | właściwy rachunek; `Rachunek.Numer: Soneta.Kasa.NumerRachunku`, `Rachunek.Bank: Soneta.Kasa.IBank` | -| `Rachunek.Numer.Pełny` / `.PełnyNRB` | `string` | pełny numer rachunku (do odczytu/ustawienia) | -| `Domyslne` | `bool` | rachunek domyślny (do odczytu — odpowiada `DomyslnyRachunek`) | -| `Priorytet` | `int` | priorytet rachunku | -| `Procent` | `Soneta.Types.Percent` | udział % (przy podziale wynagrodzenia na rachunki) | -| `Kwota` | `Soneta.Types.Currency` | kwota stała (przy podziale wynagrodzenia) | -| `Nazwa1`, `Nazwa2` | `string` | linie informacji na przelewie | -| `Oddzial` | `Soneta.Core.OddzialFirmy` | oddział | -| `Blokada` | `bool` | blokada rachunku | -| `Podmiot` | `Soneta.Kasa.IPodmiotKasowy` | właściciel (pracownik) | - -**Pułapki:** -- `RachunekBankowyPodmiotu` to typ z modułu `Soneta.Kasa` — element kolekcji `pracownik.Rachunki`. -- Rachunek główny do wynagrodzenia odczytujesz przez `pracownik.DomyslnyRachunek` (nie iteruj - kolekcji szukając `Domyslne == true`, gdy wystarczy property). -- `Rachunek` to **subrow** — numer ustawiasz na `r.Rachunek.Numer` (typ biznesowy `NumerRachunku`), - nie jako prosty string na poziomie `RachunekBankowyPodmiotu`. -- Numer rachunku to typ biznesowy (`NumerRachunku`/`NumerRachunkuPodmiotu`), z walidacją IBAN/NRB — - nie traktuj go jak zwykły `string`. - -**Snippet (odczyt — bezpieczny, bez zależności od konstruktora rachunku):** - -```csharp -var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; - -// Rachunek główny do przelewu wynagrodzenia: -RachunekBankowyPodmiotu glowny = pracownik.DomyslnyRachunek; -if (glowny != null) -{ - string numer = glowny.Rachunek.Numer.Pełny; -} - -// Wszystkie rachunki pracownika: -foreach (RachunekBankowyPodmiotu r in pracownik.Rachunki) -{ - bool czyDomyslny = r.Domyslne; - int priorytet = r.Priorytet; -} -``` - ---- - -### A6 — Dane podatkowe (PIT) - -**Cel:** ustawić/odczytać dane podatkowe pracownika: koszty uzyskania przychodu, ulgę podatkową -(PIT-2), próg/typ progów podatkowych, urząd skarbowy oraz numer rachunku US. Dane są **historyczne** — -subrow `PracHistoria.Podatki`; numer rachunku US to pole rootu pracownika. - -**Gdzie leżą pola — subrow `PracHistoria.Podatki` (`pracownik.Last.Podatki`):** - -| Dana | Pole / typ | Uwaga | -|---|---|---| -| Rodzaj kosztów uzyskania | `Podatki.KosztyRodzaj: Soneta.Kadry.RodzajKosztowUzyskania` (enum) | podstawowe / podwyższone / brak | -| Mnożnik kosztów | `Podatki.KosztyMnoznik: decimal` | np. 1 / 0 | -| Koszty autorskie 50% | `Podatki.Koszty50Procent: Percent`, `Koszty50Limit: decimal`, `Koszty50NieNaliczajOd: YearMonth`, `Koszty50NieNaliczajOdDnia: Date` | koszty autorskie | -| Ulga podatkowa (mnożnik) | `Podatki.UlgaMnoznik: decimal` | PIT-2: pełna kwota zmniejszająca = mnożnik (np. `1m`), `0` = brak | -| Część ulgi (podział PIT-2) | `Podatki.UlgaCzesc: Soneta.Kadry.UlgaPodatkowaCzesc` (enum) | 1/1, 1/2, 1/3 (podział między płatników) | -| Limit ulgi | `Podatki.UlgaLimit: bool` | | -| Ulga „klasa średnia" / emeryt / duża rodzina / zagranica | `Podatki.UlgaKlasaSrednia`, `UlgaEmeryt`, `UlgaDuzaRodzina`, `UlgaZagranica: bool`; `UlgaZagranicaOd/Do: int` | dodatkowe ulgi | -| Typ progów podatkowych | `Podatki.TypProgow: Soneta.Kadry.TypProgowPodatkowych` (enum) | standardowe / indywidualne | -| Progi podatkowe (indywidualne) | `Podatki.ProgiPodatkowe: SubTable` | gdy `TypProgow` = indywidualne | -| Podwyższona zaliczka (próg) | `Podatki.PodwProg2019: bool` | | -| Naliczanie PIT po 26 r.ż. (ulga dla młodych) | `Podatki.Pit26: Soneta.Kadry.NaliczajPit26` (enum) | „zerowy PIT" dla młodych | -| Rezygnacja z rozp. 07.01.22 | `Podatki.RezygnacjaRozp070122`, `…Umowa: bool` | | -| Kwota wolna przy umowie | `Podatki.UmowaKwotaWolna: bool` | | -| Adres na PIT = zameldowania | `Podatki.NaPITAdresZameldowania: bool` | | -| Urząd skarbowy | `Podatki.UrzadSkarbowy: Soneta.Core.IPodmiotUI` (ref); `UrzadSkarbowyEx: Soneta.CRM.UrzadSkarbowy` | pobierz istniejący US | -| Numer rachunku US (pracownika) | `Pracownik.NumerRachunkuUS: Soneta.Core.NumerRachunkuUS` (root) | `NumerRachunkuUS.Numer/.Pełny` | - -**Pułapki:** -- `Podatki` to **subrow** zapisu historii — modyfikuj pola (`Last.Podatki.UlgaMnoznik = …`), nie - przypisuj całego obiektu; zmiana „od daty" to nowy zapis historii (A14). -- PIT-2 (ulga) reprezentowana jest przez `UlgaMnoznik` (pełna/część kwoty zmniejszającej) oraz - `UlgaCzesc` (podział między płatników) — nie ma jednego pola „PIT2 bool". -- `KosztyRodzaj`, `TypProgow`, `UlgaCzesc`, `Pit26` to **enumy**, nie string. -- `UrzadSkarbowy` to **referencja** do istniejącego podmiotu — nie twórz „w locie". -- `NumerRachunkuUS` jest na **rootcie** pracownika, nie w `Podatki`. - -**Snippet:** - -```csharp -var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; - -using (var t = session.Logout(editMode: true)) -{ - var p = pracownik.Last.Podatki; - p.KosztyRodzaj = RodzajKosztowUzyskania.JedenStosPracy; // JedenStosPracy/JedenStos25/WiecejStosPracy/WiecejStos25 - p.UlgaMnoznik = 1m; // pełna kwota zmniejszająca (PIT-2) - p.UlgaCzesc = UlgaPodatkowaCzesc.Ulga112; // podział PIT-2: Ulga112/Ulga124/Ulga136 - p.TypProgow = TypProgowPodatkowych.Standardowe; // enum - t.Commit(); -} -session.Save(); - -// Odczyt: -decimal mnoznikUlgi = pracownik.Last.Podatki.UlgaMnoznik; -RodzajKosztowUzyskania koszty = pracownik.Last.Podatki.KosztyRodzaj; -``` - ---- - -### A8 — Pozostałe dane ubezpieczeniowe / informacje ZUS (oddział, kod) - -**Cel:** ustawić/odczytać dodatkowe dane ZUS pracownika — oddział ZUS oraz dodatkowe świadczenia ZUS -(emerytury/renty z dodatkowymi świadczeniami). Dane są **historyczne** — subrow -`PracHistoria.DodSwiadczeniaZUS`. (Tytuł ubezpieczenia i parametry ubezpieczeń społ./zdrow. opisuje A7.) - -**Gdzie leżą pola — subrow `PracHistoria.DodSwiadczeniaZUS: Soneta.Kadry.DodatkoweŚwiadczeniaZUS`:** - -| Pole | Typ | Opis | -|---|---|---| -| `OddzialZUS` | `Soneta.CRM.OddziałZUS` (ref) | oddział ZUS (referencja do podmiotu/słownika ZUS) | -| `Rodzaj` | `Soneta.Kadry.RodzajeDodatkowychŚwiadczeńZUS` (enum) | rodzaj dodatkowego świadczenia ZUS | -| `Okres` | `Soneta.Types.FromTo` | okres obowiązywania świadczenia | -| `Numer` | `string` | numer (decyzji/świadczenia) | - -**Oddział NFZ (komplementarny do ZUS):** `PracHistoria.OddzialNFZ: Soneta.Kadry.OddzialNFZ` — -pola `OddzialNFZ.Oddział: OddziałNFZ` (enum oddziałów), `OddzialNFZ.KodGminy: string`, -`OddzialNFZ.OdDnia: Date` (patrz też A7). - -**Pułapki:** -- `DodSwiadczeniaZUS` to **subrow** — modyfikuj pola (`Last.DodSwiadczeniaZUS.OddzialZUS = …`). -- `OddzialZUS` to **referencja** (`Soneta.CRM.OddziałZUS`) do istniejącego rekordu — pobierz - istniejący, nie twórz „w locie". -- `Rodzaj` to **enum** `RodzajeDodatkowychŚwiadczeńZUS`, nie string. -- **Cały subrow `DodSwiadczeniaZUS` bywa tylko-do-odczytu** na świeżym zapisie (rzuca - `ColReadOnlyException` nawet dla `Numer`) — pola te aktywuje dopiero zainicjowanie świadczenia - w kreatorze/UI. Zapisywalne wprost jest `OddzialNFZ.OdDnia`. -- Zmiana „od daty" to nowy zapis historii (A14). - -**Snippet:** - -```csharp -var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; - -// Odczyt dodatkowych świadczeń ZUS i oddziału NFZ na dziś: -var ph = pracownik.Last; -RodzajeDodatkowychŚwiadczeńZUS rodzaj = ph.DodSwiadczeniaZUS.Rodzaj; -FromTo okres = ph.DodSwiadczeniaZUS.Okres; -var oddzialNfz = ph.OddzialNFZ.Oddział; // enum oddziałów NFZ - -using (var t = session.Logout(editMode: true)) -{ - // Zapisywalne wprost na świeżym zapisie: data objęcia oddziałem NFZ. - ph.OddzialNFZ.OdDnia = new Date(2026, 1, 1); - t.Commit(); -} -session.Save(); -``` - ---- - -### A11 — Wykształcenie, znajomość języków obcych, służba wojskowa - -**Cel:** ewidencjonować wykształcenie, znane języki obce oraz dane służby wojskowej pracownika. -Wykształcenie i wojsko to subrowy zapisu historii (`PracHistoria`); języki obce to kolekcja na rootcie -pracownika. - -**Wykształcenie — subrow `PracHistoria.Wyksztalcenie: Soneta.Kadry.Wyksztalcenie`:** - -| Pole | Typ | Opis | -|---|---|---| -| `Kod` | `Soneta.Kadry.KodWyksztalcenia` (enum) | poziom/rodzaj wykształcenia | -| `StopienNaukowy` | `string` | stopień naukowy | -| `TytulNaukowy` | `string` | tytuł naukowy | - -(Kod wykształcenia GUS jest osobno — patrz A12: `PracHistoria.GUS.KodWyksztalcenia`.) - -**Języki obce — kolekcja `Pracownik.JęzykiObce: SubTable`:** - -| Pole | Typ | Opis | -|---|---|---| -| `Jezyk` | `Soneta.Kadry.DefinicjaJęzykaObcego` (ref słownik) | język | -| `Mowa` | `Soneta.Kadry.DefinicjaStopiaZnajomościJęzykaObcego` (ref) | stopień znajomości w mowie | -| `Pismo` | `Soneta.Kadry.DefinicjaStopiaZnajomościJęzykaObcego` (ref) | stopień znajomości w piśmie | -| `Zaswiadczenie` | `string` | nr/opis zaświadczenia | -| `DataWydaniaZaswiadczenia` | `Soneta.Types.Date` | data wydania zaświadczenia | -| `Uwagi` | `Soneta.Business.MemoText` | uwagi | - -**Służba wojskowa — subrow `PracHistoria.Wojsko: Soneta.Kadry.Wojsko`:** - -| Pole | Typ | Opis | -|---|---|---| -| `Stosunek` | `Soneta.Kadry.KodStosDoSluzbyWojskowej` (enum) | stosunek do służby wojskowej | -| `KategoriaZdrowia` | `Soneta.Kadry.KategoriaZdrowia` (enum) | kategoria zdrowia (A, B, …) | -| `Stopien` | `string` | stopień wojskowy | -| `NrKsiazeczki` | `string` | nr książeczki wojskowej | -| `NrSpecjalnosci` | `string` | nr specjalności wojskowej | -| `WKU` | `string` | właściwa WKU | -| `PrzydzialMobilizacyjny` | `string` | przydział mobilizacyjny | -| `Podlega` | `bool` | czy podlega obowiązkowi (odczyt) | - -**Pułapki:** -- `Wyksztalcenie` i `Wojsko` to **subrowy** `PracHistoria` (historyczne) — modyfikuj pola, zmiana - „od daty" przez A14. `JęzykiObce` to **kolekcja na rootcie** pracownika (nie historyczna). -- `Jezyk`, `Mowa`, `Pismo` to **referencje** do rekordów słownika (`DefinicjaJęzykaObcego`, - `DefinicjaStopiaZnajomościJęzykaObcego`) — pobierz istniejące, nie twórz „w locie". -- `Kod` (wykształcenie), `Stosunek`, `KategoriaZdrowia` to **enumy**, nie string. - -**Snippet:** - -```csharp -var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; - -using (var t = session.Logout(editMode: true)) -{ - // Wykształcenie (subrow historii): - pracownik.Last.Wyksztalcenie.Kod = KodWyksztalcenia.Wyzsze; // enum - pracownik.Last.Wyksztalcenie.TytulNaukowy = "mgr inż."; - - // Służba wojskowa (subrow historii): - pracownik.Last.Wojsko.Stosunek = KodStosDoSluzbyWojskowej.Rezerwa; // NieDotyczy/NiePodlega/Przedpoborowy/Poborowy/Rezerwa/Inne - pracownik.Last.Wojsko.KategoriaZdrowia = KategoriaZdrowia.A; - pracownik.Last.Wojsko.NrKsiazeczki = "AB123456"; - - t.Commit(); -} -session.Save(); - -// Odczyt znajomości języków obcych (kolekcja na rootcie): -foreach (ZnajomośćJęzykaObcego j in pracownik.JęzykiObce) -{ - var jezyk = j.Jezyk; // DefinicjaJęzykaObcego - var mowa = j.Mowa; // DefinicjaStopiaZnajomościJęzykaObcego -} -``` - ---- - -### A12 — Dane statystyczne GUS, kod zawodu GUS - -**Cel:** ustawić/odczytać dane statystyczne GUS pracownika (kod wykształcenia GUS, rodzaj zatrudnienia -GUS, przesłanki statystyczne) oraz kod wykonywanego zawodu. Dane są **historyczne** — subrow -`PracHistoria.GUS`; kod zawodu siedzi w `Etat`. - -**Dane statystyczne — subrow `PracHistoria.GUS: Soneta.Kadry.StatystykaGUS`:** - -| Pole | Typ | Opis | -|---|---|---| -| `KodWyksztalcenia` | `Soneta.Kadry.KodWykształceniaGUS` (enum) | kod wykształcenia wg GUS | -| `RodzajZatrudnienia` | `Soneta.Kadry.RodzajZatrudnieniaGUS` (enum) | rodzaj zatrudnienia wg GUS | -| `PopMiejsceZatrudnienia` | `Soneta.Kadry.PopMiejsceZatrudnienia` (enum) | poprzednie miejsce zatrudnienia | -| `GlowneMiejscePracy` | `bool` | główne miejsce pracy | -| `PierwszaPraca` | `bool` | pierwsza praca | -| `PracaWNocy` | `bool` | praca w nocy | -| `StRobotnicze` | `bool` | stanowisko robotnicze | -| `SezonowyDorywczy` | `bool` | zatrudnienie sezonowe/dorywcze | -| `PraceInterwencyjne` | `bool` | prace interwencyjne | - -**Kod wykonywanego zawodu — `PracHistoria.Etat`:** - -| Pole | Typ | Opis | -|---|---|---| -| `Etat.KodWykonywanegoZawodu` | `int` | kod zawodu GUS (liczbowo) | -| `Etat.KodWykonywanegoZawoduLnk` | `Soneta.Kadry.KodWykonywanegoZawodu` (ref/odczyt) | dowiązany rekord słownika kodu zawodu | - -**Pułapki:** -- `GUS` to **subrow** `PracHistoria` (historyczny) — modyfikuj pola; zmiana „od daty" przez A14. -- `KodWyksztalcenia` (GUS, enum `KodWykształceniaGUS`) to **inne pole** niż A11 - `Wyksztalcenie.Kod` (enum `KodWyksztalcenia`) — nie myl ich. -- `Etat.KodWykonywanegoZawodu` to `int`; `…Lnk` to dowiązanie do słownika (kanonicznie ustawiasz - kod liczbowo, dowiązanie jest pochodne). -- Pola GUS to enumy/bool, nie string. - -**Snippet:** - -```csharp -var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; - -using (var t = session.Logout(editMode: true)) -{ - var ph = pracownik.Last; - ph.GUS.KodWyksztalcenia = KodWykształceniaGUS.Wyższe; // enum GUS (uwaga na diakrytyk) - ph.GUS.GlowneMiejscePracy = true; - ph.GUS.PierwszaPraca = false; - - ph.Etat.KodWykonywanegoZawodu = 251401; // kod zawodu GUS (int) - t.Commit(); -} -session.Save(); - -// Odczyt: -KodWykształceniaGUS kw = pracownik.Last.GUS.KodWyksztalcenia; -int kodZawodu = pracownik.Last.Etat.KodWykonywanegoZawodu; -``` - ---- - -### A13 — PFRON i niepełnosprawność / schorzenia - -**Cel:** ewidencjonować dane o niepełnosprawności (stopień, orzeczenie, okresy) oraz dane PFRON -(dofinansowania, schorzenia szczególne). Dane są **historyczne** — subrow `PracHistoria.PFRON`. - -**Gdzie leżą pola — subrow `PracHistoria.PFRON: Soneta.Kadry.DanePFRON`:** - -| Dana | Pole / typ | Uwaga | -|---|---|---| -| Stopień niepełnosprawności | `PFRON.Stopien: Soneta.Kadry.StNiepełnosprawności` (enum) | stopień orzeczony | -| Stopień wg PFRON | `PFRON.StopienPFRON: Soneta.Kadry.KodStNiepelnosprawnosciPFRON` (enum) | klasyfikacja PFRON | -| Okres orzeczenia/uprawnień | `PFRON.Okres: Soneta.Types.FromTo` | okres niepełnosprawności | -| Data orzeczenia | `PFRON.DataOrzeczenia: Soneta.Types.Date` | | -| Data dostarczenia orzeczenia | `PFRON.DataDostarczenia: Soneta.Types.Date` | | -| Data wniosku / zaświadczenia | `PFRON.DataWniosku`, `PFRON.DataZaswiadczenia: Date` | | -| Data zgłoszenia do ewidencji PFRON | `PFRON.DataZgloszeniaDoEwidencji: Date` | | -| Organ wydający orzeczenie | `PFRON.OrganWydajacyOrzeczenie: Soneta.Kadry.OrganWydajacyOrzeczenie` (enum) | | -| Schorzenie szczególne (flaga) | `PFRON.SzczegolneSchorzenie: bool`, `PFRON.SzczegolneSchorzeniePFRON: bool` | | -| Typ schorzenia | `PFRON.TypSchorzenia: Soneta.Kadry.SzczegolneSchorzenia` (enum) | rodzaj schorzenia | -| Schorzenia SOD (1–4) | `PFRON.TypSchorzeniaSOD`, `…2SOD`, `…3SOD`, `…4SOD: Soneta.Kadry.SzczególneSchorzeniaSOD` (enum) | schorzenia dla dofinansowania SOD | -| Lista schorzeń SOD (odczyt) | `PFRON.SchorzeniaSOD: IEnumerable` | wyliczane | -| Efekt zachęty | `PFRON.EfektZachety: bool` | warunek dofinansowania | -| Pomoc publiczna | `PFRON.PomocPubliczna: Soneta.Kadry.StanowiPomocPubliczną` (enum) | | -| Dofinansowanie dodatkowe SOD | `PFRON.DodatkoweDofinansowanieSOD: bool` | | -| Urlop dodatkowy (niepełnospr.) | `PFRON.NaliczajUrlopDodatkowy: bool`, `…Od: Date` | | -| Wymiar urlopu podstawowego | `PFRON.WymiarUPodstawowego: Soneta.Types.Fraction` | | -| Wiek emerytalny od | `PFRON.WiekEmerytalnyOd: Date` | | -| Zgoda na przekazanie danych | `PFRON.ZgodaNaPrzekazanieDanych: bool` | | - -(Zgody na pracę powyżej norm dla osób niepełnosprawnych są na `Etat`: -`Etat.PracownikNiepelnosprawnyZgodaNaPrace8h`, `…ZgodaNaPraceNadgodziny`, -`…ZgodaNaPraceWPorzeNocnej: bool`.) - -**Pułapki:** -- `PFRON` to **subrow** `PracHistoria` (historyczny) — modyfikuj pola; zmiana „od daty" przez A14. -- `Stopien`, `StopienPFRON`, `TypSchorzenia`, `…SOD`, `OrganWydajacyOrzeczenie`, `PomocPubliczna` - to **enumy**, nie string. -- `SchorzeniaSOD` (lista) jest **wyliczana** — schorzenia ustawiasz przez pola `TypSchorzeniaSOD…4SOD`. -- Daty to `Soneta.Types.Date`, okres to `FromTo` — nie `DateTime`. - -**Snippet:** - -```csharp -var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; - -using (var t = session.Logout(editMode: true)) -{ - var pfron = pracownik.Last.PFRON; - pfron.Stopien = StNiepełnosprawności.Umiarkowany; // enum - pfron.Okres = new FromTo(new Date(2026, 1, 1), new Date(2028, 12, 31)); - pfron.DataOrzeczenia = new Date(2025, 12, 1); - pfron.DataDostarczenia = new Date(2025, 12, 15); - pfron.SzczegolneSchorzenie = true; - pfron.TypSchorzeniaSOD = SzczególneSchorzeniaSOD.ChoróbPsychiczna; // wg słownika SOD - t.Commit(); -} -session.Save(); - -// Odczyt stopnia i okresu niepełnosprawności na dzień: -var ph = pracownik[Date.Today]; -StNiepełnosprawności stopien = ph.PFRON.Stopien; -FromTo okresNiepeln = ph.PFRON.Okres; -``` - -### A15 — Odczyt danych pracownika „na dzień" (★) - -**Cel:** pobrać właściwy rekord historyczny `PracHistoria` obowiązujący dla zadanej daty — i odróżnić -**odczyt** (nie zmienia historii) od **zmiany „od daty"** z A14 (`Update` + `AddRow`). Receptura -czysto odczytowa: idealna do uruchomienia na bazie Demo (kody `"006"`–`"009"`). - -**API odczytu — `Pracownik` + `HistorySubTable`:** - -| Operacja | API | Zwraca | -|---|---|---| -| Zapis obowiązujący na dzień | `pracownik[date]` (indeksator, `Item[Date]`) | `PracHistoria` którego `Aktualnosc` zawiera `date`, albo `null` dla daty sprzed zatrudnienia | -| Równoważnie przez kolekcję | `pracownik.Historia[date]` | jw. (indeksator `HistorySubTable.Item[Date]`) | -| Najstarszy (pierwszy) zapis | `pracownik.Historia.GetFirst()` | `PracHistoria` (pierwszy okres zatrudnienia) | -| Najświeższy (ostatni) zapis | `pracownik.Last` (== `Historia.GetLast()`) | `PracHistoria` (zawsze niepusty dla istniejącego pracownika) | -| Sąsiedni zapis | `Historia.GetPrev(ph)` / `Historia.GetNext(ph)` | poprzedni / następny zapis względem podanego | -| Okres obowiązywania zapisu | `PracHistoria.Aktualnosc: FromTo` | „od–do" zapisu (read-only z punktu widzenia kodu — zarządza historia) | - -**Różnica odczyt (A15) vs zmiana (A14):** - -| Aspekt | A15 — odczyt | A14 — zmiana „od daty" | -|---|---|---| -| Wywołanie | `pracownik[date]` | `pracownik.Historia.Update(date)` + `PracHistorie.AddRow(nowy)` | -| Efekt na historii | **żaden** (nie tworzy/skraca zapisów) | klonuje zapis aktualny na `date`, skraca poprzedni do `date-1`, dodaje nowy | -| Transakcja | niepotrzebna (sam odczyt) | wymagana (`session.Logout(editMode: true)` + `Save()`) | -| Zwraca | istniejący zapis (lub `null`) | **nowy** klon do uzupełnienia | - -**Snippet (odczyt — bez transakcji):** - -```csharp -var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; - -// 1) Zapis obowiązujący na konkretny dzień: -var data = new Date(2026, 3, 15); -PracHistoria phNaDzien = pracownik[data]; // == pracownik.Historia[data] -if (phNaDzien != null) -{ - string nazwisko = phNaDzien.Nazwisko; - FromTo okresZapisu = phNaDzien.Aktualnosc; // okres obowiązywania tego zapisu - Fraction wymiar = phNaDzien.Etat.Wymiar; // warunki etatu „na dzień" -} - -// 2) Pierwszy i ostatni zapis historii: -PracHistoria pierwszy = pracownik.Historia.GetFirst(); // najstarszy -PracHistoria ostatni = pracownik.Last; // najświeższy (== GetLast()) - -// 3) Odczyt aktualny „na dziś" (uwzględnia datę biznesową aplikacji): -PracHistoria phDzis = pracownik[Date.Today]; -``` - -**Pułapki:** -- `pracownik[date]` zwraca **`null`** dla daty sprzed pierwszego zapisu (przed zatrudnieniem) — zawsze - sprawdzaj `null` przy datach historycznych. `pracownik.Last` jest niepusty dla istniejącego pracownika. -- Indeksator to **tylko odczyt** — nie próbuj „ustawiać" `pracownik[date] = …`. Zmiana danych w okresie - to korekta (`pracownik[date].Pole = …` w transakcji, A14) lub nowy zapis (`Update`, A14). -- `Aktualnosc` jest zarządzana przez mechanizm historii — odczytujesz, nie ustawiasz. -- `data` to `Soneta.Types.Date`, nie `DateTime`; do „dziś" używaj `Date.Today` (safe-code §10.2). -- Czysty odczyt nie wymaga transakcji edycyjnej — nie otwieraj `Logout(editMode: true)` bez potrzeby. - -### A16 — Powiązanie pracownika z kontrahentem (★) - -**Cel:** powiązać pracownika z istniejącym kontrahentem (np. gdy pracownik jest jednocześnie -kontrahentem firmy). Dwie drogi: bezpośrednie ustawienie relacji na rootcie **albo** worker -„Powiąż z kontrahentem". - -**Publiczny kontrakt — pole relacji na `Pracownik` (root):** - -| Pole | Typ | Rodzaj | Uwaga | -|---|---|---|---| -| `PowiazanyKontrahent` | `Soneta.CRM.Kontrahent` | bazodanowe, **zapisywalne** | referencja do istniejącego kontrahenta; `null` = brak powiązania | - -**Worker (alternatywa, ta sama operacja):** `Soneta.Kadry.PowiazZKontrahentemWorker` -(`[Action("Powiąż z kontrahentem")]`, metoda `Powiaz()`): - -| Składnik | Sygnatura | Uwaga | -|---|---|---| -| Pracownik | `Pracownik { get; set; }` | `[Context]` — pracownik do powiązania | -| Parametry | `Prms: Soneta.Kadry.MyParams` | `MyParams.Kontrahent: Soneta.CRM.Kontrahent` — kontrahent docelowy | -| Akcja | `void Powiaz()` | ustawia powiązanie (działa na danych sesji workera) | - -**Snippet (bezpośrednio — zalecane w kodzie biznesowym):** - -```csharp -var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; -var kontrahent = session.GetCRM().Kontrahenci.WgKodu["KLIENT01"]; // istniejący kontrahent - -using (var t = session.Logout(editMode: true)) -{ - pracownik.PowiazanyKontrahent = kontrahent; // relacja na rootcie pracownika - t.Commit(); -} -session.Save(); - -// Odczyt powiązania: -Kontrahent powiazany = pracownik.PowiazanyKontrahent; // null gdy brak -``` - -**Snippet (przez worker — gdy chcesz przejść tą samą ścieżką co UI):** - -```csharp -using (var t = session.Logout(editMode: true)) -{ - var worker = new PowiazZKontrahentemWorker - { - Pracownik = pracownik, - Prms = new MyParams(context) { Kontrahent = kontrahent }, - }; - worker.Powiaz(); - t.CommitUI(); // worker uruchamiany „jak z UI" -} -session.Save(); -``` - -**Pułapki:** -- `Kontrahent` i `Pracownik` muszą pochodzić z **tej samej sesji** (safe-code §2.1) — kontrahenta z innej - sesji przepuść przez `session.Get(...)`. -- Relacja wskazuje **istniejący** rekord kontrahenta — nie twórz kontrahenta „w locie" w tym scenariuszu. -- `MyParams` ma konstruktor `MyParams(Context context)` — wymaga `Context` (dlatego ścieżka workera ma sens - głównie z UI). W czystym kodzie biznesowym prościej ustawić `PowiazanyKontrahent` wprost. -- W teście jednostkowym preferuj bezpośrednie pole `PowiazanyKontrahent` — nie wymaga `Context`. - -### A17 — Przeniesienie do archiwum i przywrócenie (★) - -**Cel:** przenieść pracownika do archiwum (po zakończeniu zatrudnienia) oraz przywrócić go z archiwum. -**Operacja przenoszenia/przywracania jest dostępna wyłącznie przez workery** — manager `Archiwum` -udostępnia tylko odczyt statusu. - -**Publiczny kontrakt odczytu — `Pracownik`:** - -| Składnik | Typ | Rodzaj | Uwaga | -|---|---|---|---| -| `Archiwum` | `Pracownik.ArchiwumManager` | manager (read-only API) | `Archiwum.Status: InformacjeOArchiwum`, `Archiwum.Anonimizowany: bool`, `Archiwum.Okresy: Periods` | -| `ArchiwumInfo` | `Soneta.Kadry.InformacjeOArchiwum` | bazodanowe | bieżąca informacja o archiwizacji | -| `InformacjeOArchiwum` | `FromToSubTable` | kolekcja | historia okresów w archiwum (`PracownikWArchiwum.Okres: FromTo`) | - -> **`ArchiwumManager` nie ma publicznej metody Przenieś/Przywróć** — wystawia jedynie właściwości -> tylko-do-odczytu (`Status`, `Anonimizowany`, `Okresy`). Zmiana stanu archiwum następuje wyłącznie -> przez workery poniżej. - -**Workery (jedyna droga zmiany stanu):** - -| Worker | Akcja (menu) | Metoda | Parametry | -|---|---|---|---| -| `Pracownik.PrzenieśDoArchiwumWorker` | „Archiwum/Przenieś do archiwum" | `void PrzenieśDoArchiwum()` | `Pracownik { get; set; }` (`[Context]`, pojedynczy) | -| `Pracownik.PrzywróćZArchiwumWorker` | „Archiwum/Przywróć z archiwum" | `void PrzywróćZArchiwum()` | `Pracownik { get; set; }` (`[Context]`, pojedynczy) | -| `Pracownik.PrzenieśDoArchiwumLstWorker` | „Operacje seryjne/Archiwum/Przenieś do archiwum…" | `void PrzenieśDoArchiwum()` | `Pracownicy: Pracownik[]` (grupowo) | -| `Pracownik.PrzywróćZArchiwumLstWorker` | „Operacje seryjne/Archiwum/Przywróć z archiwum…" | `void PrzywróćZArchiwum()` | `Pracownicy: Pracownik[]` (grupowo) | - -**Snippet (programowe wywołanie workera — pojedynczy pracownik):** - -```csharp -var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; - -// Przeniesienie do archiwum: -using (var t = session.Logout(editMode: true)) -{ - var worker = new Pracownik.PrzenieśDoArchiwumWorker { Pracownik = pracownik }; - worker.PrzenieśDoArchiwum(); - t.CommitUI(); -} -session.Save(); - -// Odczyt stanu archiwizacji: -InformacjeOArchiwum status = pracownik.Archiwum.Status; -bool zanonimizowany = pracownik.Archiwum.Anonimizowany; - -// Przywrócenie z archiwum: -using (var t = session.Logout(editMode: true)) -{ - var worker = new Pracownik.PrzywróćZArchiwumWorker { Pracownik = pracownik }; - worker.PrzywróćZArchiwum(); - t.CommitUI(); -} -session.Save(); -``` - -**Snippet (operacja seryjna — wielu pracowników):** - -```csharp -var lista = session.GetKadry().Pracownicy - .Cast() - .Where(p => /* kryterium */ true) - .ToArray(); - -using (var t = session.Logout(editMode: true)) -{ - var worker = new Pracownik.PrzenieśDoArchiwumLstWorker { Pracownicy = lista }; - worker.PrzenieśDoArchiwum(); - t.CommitUI(); -} -session.Save(); -``` - -**Pułapki:** -- **Brak publicznej metody na managerze** — nie szukaj `pracownik.Archiwum.Przenieś(...)`; jedyne - publiczne API zmiany to workery `PrzenieśDoArchiwumWorker`/`PrzywróćZArchiwumWorker`. -- Workery archiwizacji modyfikują dane → wywołuj w transakcji edycyjnej i `Save()`. Worker uruchamiany - „jak z UI" → `CommitUI()` (worker-extender §3, pkt 4). -- `Archiwum`, `ArchiwumInfo`, `InformacjeOArchiwum` służą **tylko do odczytu** stanu/historii archiwum. -- Pracownik z workera i `Pracownicy[]` muszą być z bieżącej sesji (safe-code §2.1). -- Archiwizacja bywa powiązana z anonimizacją (`Archiwum.Anonimizowany`) — to oddzielny stan; przeniesienie - do archiwum nie musi oznaczać anonimizacji. - -### A18 — Wyrejestrowanie / zwolnienie pracownika (★) - -**Cel:** zakończyć zatrudnienie — ustawić rozwiązanie umowy (data, tryb, inicjatywa, podstawa prawna), -ewentualnie okres wypowiedzenia, oraz wygenerować wyrejestrowanie z ZUS (ZWUA) workerem. - -**Publiczny kontrakt — `PracHistoria.Etat` (dane historyczne zatrudnienia):** - -| Dana | Pole / typ | Uwaga | -|---|---|---| -| Koniec okresu zatrudnienia | `Etat.Okres: Soneta.Types.FromTo` | `Okres.To` = ostatni dzień zatrudnienia (zmiana „od daty" → A14) | -| Rozwiązanie umowy (subrow) | `Etat.RozwiazanieUmowy: Soneta.Kadry.RozwiazanieUmowy` | zbiorczy subrow trybu zwolnienia | -| Inicjatywa zwolnienia | `Etat.RozwiazanieUmowy.Inicjatywa: KodInicjatywyZwolnienia` | enum | -| Kod zwolnienia (ZUS) | `Etat.RozwiazanieUmowy.KodZwolnienia: KodZwolnienia` | kod trybu rozwiązania | -| Podstawa prawna | `Etat.RozwiazanieUmowy.PodstawaPrawna: KodPodstawyPrawnejZwolnienia` | enum | -| Przyczyna rozwiązania | `Etat.RozwiazanieUmowy.PrzyczynaRozwUmowy: PrzyczynaRozwUmowy` | rekord słownika; opis: `PrzyczynaRozwUmowyOpis: string` | -| Za odszkodowaniem | `Etat.RozwiazanieUmowy.ZaOdszkodowaniem: bool` | — | -| Okres wypowiedzenia (subrow) | `Etat.OkresWypowiedzenia: Soneta.Kadry.OkresWypowiedzenia` | parametry wypowiedzenia | -| Długość wypowiedzenia | `Etat.OkresWypowiedzenia.Dni / .Tygodnie / .Miesiace: int` | składowe okresu | -| Data złożenia wypowiedzenia | `Etat.OkresWypowiedzenia.DataZlozenia: Date` | — | -| Skrócony okres | `Etat.OkresWypowiedzenia.Skrocony: bool` | — | -| Zwolnienie z obowiązku pracy od | `Etat.OkresWypowiedzenia.ZwolnionyZObowiazkuPracyOd: Date` | — | -| Data upływu wypowiedzenia | `Etat.OkresWypowiedzenia.Uplywa: Date` | wyliczana data rozwiązania (`DataRozwiązaniaUmowy` read-only) | - -**Worker ZUS (wyrejestrowanie ZWUA):** `Soneta.Kadry.Pracownik.WyrejestrujPracownikaWorker` -(`[Action("Operacje seryjne/Wyrejestruj pracowników...")]`, metoda `Wyrejestruj()`): - -| Składnik | Sygnatura | Uwaga | -|---|---|---| -| Ctor (parametry z `Context`) | `WyrejestrujPracownikaWorker(WyrejestrujPracownikaParams pars)` | `pars` inicjowany z `Context` | -| Data wyrejestrowania | `WyrejestrujPracownikaParams.Data: Date` | data zdarzenia ZWUA | -| Pracownicy | `Pracownicy: Pracownik[]` (`[Context]`) | lista do wyrejestrowania | -| Bieżąca data | `Current: Date` | data robocza | -| Akcja | `void Wyrejestruj()` | tworzy wyrejestrowania ZUS (ZWUA) | - -**Snippet (ustawienie rozwiązania umowy — nowy zapis „od daty", A14):** - -```csharp -var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; -var dataRozwiazania = new Date(2026, 6, 30); - -using (var t = session.Logout(editMode: true)) -{ - var ph = pracownik[dataRozwiazania]; // zapis obowiązujący na dzień rozwiązania (A15) - var etat = ph.Etat; - - // Zamknięcie okresu zatrudnienia ostatnim dniem pracy: - etat.Okres = new FromTo(etat.Okres.From, dataRozwiazania); - - // Tryb rozwiązania (subrow RozwiazanieUmowy): - etat.RozwiazanieUmowy.Inicjatywa = KodInicjatywyZwolnienia.Pracownik; - etat.RozwiazanieUmowy.PodstawaPrawna = KodPodstawyPrawnejZwolnienia._550; // kody numeryczne wg słownika (NieDotyczy, _400.._463, _550) - - // Opcjonalnie okres wypowiedzenia: - etat.OkresWypowiedzenia.DataZlozenia = new Date(2026, 5, 31); - etat.OkresWypowiedzenia.Miesiace = 1; - - t.Commit(); -} -session.Save(); -``` - -**Snippet (wyrejestrowanie z ZUS — worker):** - -```csharp -using (var t = session.Logout(editMode: true)) -{ - var pars = new Pracownik.WyrejestrujPracownikaWorker.WyrejestrujPracownikaParams(context) - { - Data = new Date(2026, 7, 1), - }; - var worker = new Pracownik.WyrejestrujPracownikaWorker(pars) - { - Pracownicy = new[] { pracownik }, - Current = Date.Today, - }; - worker.Wyrejestruj(); - t.CommitUI(); -} -session.Save(); -``` - -**Pułapki:** -- `RozwiazanieUmowy` i `OkresWypowiedzenia` to **subrowy** `Etat` — modyfikuj ich pola, nie przypisuj całych - obiektów. -- Konkretne wartości enumów (`KodInicjatywyZwolnienia`, `KodPodstawyPrawnejZwolnienia`, `PrzyczynaRozwUmowy`) - zależą od słownika danej bazy — w teście pobierz/odczytaj realne wartości z Demo zamiast zgadywać. -- `WyrejestrujPracownikaWorker` ma **konstruktor** przyjmujący `WyrejestrujPracownikaParams`, który z kolei - wymaga `Context` (`WyrejestrujPracownikaParams(Context cx)`) — worker jest praktycznie wywoływalny tylko - z dostępnym `Context`. Bez `Context` operację wyrejestrowania ZUS zrealizujesz tylko częściowo (samo - ustawienie `Etat.Okres`/`RozwiazanieUmowy` nie tworzy dokumentu ZWUA). -- `Uplywa`/`DataRozwiązaniaUmowy` bywają wyliczane — nie nadpisuj pól read-only. -- Zmiana warunków „od dnia" to nowy zapis (A14); samo zamknięcie `Etat.Okres.To` na bieżącym zapisie jest - korektą całego okresu — używaj świadomie. - -### A19 — Przerejestrowanie pracownika (★) - -**Cel:** zmienić kod tytułu ubezpieczenia (`Tyub4`) lub jednostkę (`Wydzial`) od konkretnego dnia — -co skutkuje **wyrejestrowaniem ze starym kodem i ponownym zgłoszeniem z nowym** (ZUS ZWUA + ZUA). -Realizacja: nowy zapis historii „od daty" (A14) z innym `Etat.Ubezpieczenia.Tyub4`/`Etat.Wydzial`, -a generowanie deklaracji ZUS — workerem przerejestrowania. - -**Publiczny kontrakt — pola do zmiany (na nowym zapisie `PracHistoria`):** - -| Dana | Pole / typ | Uwaga | -|---|---|---| -| Tytuł ubezpieczenia | `Etat.Ubezpieczenia.Tyub4: Soneta.Kadry.TytulUbezpieczenia4` | rekord słownika; `session.GetKadry().TytulyUbezpiecz4.WgKodu[int]` (klucz `int`, np. `110`) | -| Jednostka organizacyjna | `Etat.Wydzial: Soneta.Kadry.Wydzial` | referencja do istniejącego wydziału | - -**Worker ZUS (przerejestrowanie):** `Soneta.Deklaracje.UI.PrzerejestrowaniePracownikaWorker` -(`[Action("Przerejestrowanie pracownika …")]`, metoda `PrzerejestrowaniePracownika()`): - -| Składnik (`PrzerejestrowaniePracownikaWorker.Params`) | Typ | Uwaga | -|---|---|---| -| `DataRejestracji` | `Soneta.Types.Date` | data ponownego zgłoszenia | -| `DataWypełnienia` | `Soneta.Types.Date` | data wypełnienia deklaracji | -| `Kedu` | `Soneta.Deklaracje.ZUS.KEDU` | zbiór deklaracji ZUS (KEDU) | -| `Przyczyna` | `Soneta.Kadry.Wyrejestrowanie` | przyczyna wyrejestrowania (do ZWUA) | - -**Snippet (zmiana kodu tytułu ubezpieczenia / wydziału „od daty"):** - -```csharp -var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; -var odDnia = new Date(2026, 7, 1); - -using (var t = session.Logout(editMode: true)) -{ - // Nowy zapis historii „od daty" (A14): Update klonuje + skraca poprzedni, AddRow dopina klon. - var nowy = (PracHistoria)pracownik.Historia.Update(odDnia); - pracownik.Module.PracHistorie.AddRow(nowy); - - // Zmiana kodu tytułu ubezpieczenia (przerejestrowanie ubezpieczeniowe): - nowy.Etat.Ubezpieczenia.Tyub4 = session.GetKadry().TytulyUbezpiecz4.WgKodu[110]; - - // Lub/oraz zmiana jednostki organizacyjnej: - nowy.Etat.Wydzial = session.GetKadry().Wydzialy.Firma; - - t.Commit(); -} -session.Save(); -``` - -**Pułapki:** -- Przerejestrowanie to **nowy zapis historii** (A14: `Update(odDnia)` + `PracHistorie.AddRow(nowy)`) — nie - nadpisuj `Tyub4`/`Wydzial` na bieżącym zapisie (to zmieniłoby cały okres wstecz). -- `Tyub4` pobierasz ze słownika `TytulyUbezpiecz4` po **`int`** (`WgKodu[110]`), nie po stringu i nie „w locie". -- `Wydzial` to referencja do istniejącego wydziału (korzeń: `session.GetKadry().Wydzialy.Firma`). -- `PrzerejestrowaniePracownikaWorker` żyje w `Soneta.Deklaracje.UI` i jego `Params` wymaga m.in. `KEDU` - (zbiór deklaracji) oraz `Context` — generowanie ZWUA+ZUA jest realnie wykonalne tylko w środowisku z - `Context`/`KEDU`. Sama zmiana danych kadrowych (`Tyub4`/`Wydzial`) jest w pełni wykonalna publicznym API - bez workera; deklaracje ZUS — tylko przez worker UI. -- `Update(odDnia)` rzuca `DateDuplicateException`, jeśli na `odDnia` już zaczyna się zapis (A14). - -## B. Etat — zatrudnienie etatowe - -### B1 — Definiowanie etatu (umowa o pracę) (★) - -**Cel:** ustalić warunki zatrudnienia etatowego pracownika — rodzaj umowy o pracę, okres, daty -zawarcia/rozpoczęcia pracy, stanowisko, jednostkę organizacyjną oraz stawkę zaszeregowania -(wymiar etatu, rodzaj/typ stawki, kwota). Warunki etatu są **historyczne**: siedzą w polu -`Etat` konkretnego zapisu `PracHistoria`. Etat ustawiamy albo na świeżo utworzonym pracowniku -(`pracownik.Last.Etat`, patrz A1), albo na nowym zapisie historii „od daty" (patrz A14). - -**Gdzie leżą pola — `PracHistoria.Etat: Soneta.Kadry.Etat` (subrow zapisu historii):** - -| Dana | Pole / typ | Uwaga | -|---|---|---| -| Rodzaj umowy o pracę | `Etat.TypUmowy: Soneta.Kadry.TypUmowyOPrace` | enum: `NaCzasNieokreślony`, `NaOkresPróbny`, `NaCzasOkreślony`, `NaOkresZastępstwa`, `DoDniaPorodu`, `NaCzasWykonywniaPracy`, … (`Brak = 0` = nie dotyczy) | -| Okres etatu (od–do) | `Etat.Okres: Soneta.Types.FromTo` | okres obowiązywania warunków zatrudnienia | -| Data zawarcia umowy | `Etat.DataZawarcia: Soneta.Types.Date` | data podpisania umowy | -| Data rozpoczęcia pracy | `Etat.DataRozpPracy: Soneta.Types.Date` | data faktycznego rozpoczęcia | -| Stanowisko (opis tekstowy) | `Etat.Stanowisko: string` | **wymagane dla etatu** (weryfikator przy `Save()`) | -| Jednostka organizacyjna (wydział) | `Etat.Wydzial: Soneta.Kadry.Wydzial` | **wymagane dla etatu**; pobierz istniejący wydział, korzeń struktury: `session.GetKadry().Wydzialy.Firma` | -| Oddział firmy | `Etat.Oddzial: Soneta.Core.OddzialFirmy` | opcjonalny oddział | -| Miejsce wykonywania pracy | `Etat.MiejscePracy: string` | tekst | -| Podstawa stosunku pracy | `Etat.Podstawa: Soneta.Kadry.StosPracyNaPodstawie` | enum | - -**Stawka — subrow `Etat.Zaszeregowanie: Soneta.Kadry.Zaszeregowanie`:** - -| Dana | Pole / typ | Uwaga | -|---|---|---| -| Rodzaj stawki | `Zaszeregowanie.RodzajStawki: Soneta.Kadry.RodzajStawkiZaszeregowania` | enum: `Godzinowa = 0`, `Miesieczna = 1`, `DochodDeklarowany = 2` | -| Typ stawki | `Zaszeregowanie.TypStawki: Soneta.Kadry.TypStawkiZaszeregowania` | enum: `Dowolna = 0`, `Minimalna = 1`, `ZZakresu = 2`, `WgWskaźnika = 3`, `Nieokreślona = 10` | -| Wymiar etatu (ułamek) | `Zaszeregowanie.Wymiar: Soneta.Types.Fraction` | `Fraction.One` = pełny etat; `new Fraction(1, 2)` = ½ etatu | -| Kwota stawki | `Zaszeregowanie.Stawka: Soneta.Types.Currency` | kwota brutto (miesięczna lub godzinowa wg `RodzajStawki`) | -| Grupa zaszeregowania | `Zaszeregowanie.Grupa: Soneta.Kadry.GrupaZaszeregowania` | rekord słownika (opcjonalny) | -| Definicja elementu wynagrodzenia | `Zaszeregowanie.Element: Soneta.Place.DefinicjaElementu` | element płacowy wiązany ze stawką (opcjonalny) | - -**Pułapki:** -- **Kolejność ma znaczenie — `Etat.Okres` ustaw jako PIERWSZE.** Na świeżo utworzonym - pracowniku (lub świeżym zapisie historii) **cały subrow `Etat` jest w trybie tylko-do-odczytu**, - dopóki nie ustawisz `Etat.Okres` (zakres zatrudnienia). Próba ustawienia `Etat.TypUmowy`, - `Etat.Podstawa` czy `Zaszeregowanie.RodzajStawki`/`Wymiar` przed `Etat.Okres` rzuca - `Soneta.Business.ColReadOnlyException` (np. „'Etat.Typ umowy' — pole w trybie 'tylko do odczytu'"). - Po ustawieniu `Etat.Okres` pozostałe pola (w tym `Zaszeregowanie.Wymiar`) są zapisywalne — - kolejność wśród nich nie ma już znaczenia. -- **Pola wymagane dla etatu:** po ustawieniu `Etat.Okres` (pracownik staje się etatowy) `Save()` - wymaga `Etat.Wydzial` **oraz** `Etat.Stanowisko` — bez nich zapis rzuca wyjątek weryfikatora. -- `Etat` to **subrow** zapisu `PracHistoria` — modyfikujesz jego pola (`Last.Etat.Okres = …`), nie - przypisujesz całego obiektu `Etat`. -- `Zaszeregowanie` to z kolei subrow `Etat` — analogicznie modyfikujesz pola - (`Last.Etat.Zaszeregowanie.Stawka = …`). -- `Etat.Wymiar` i `Etat.TypStawki` istnieją także na poziomie `Etat` (delegaty/odczyt) — **kanonicznie - ustawiamy je na `Etat.Zaszeregowanie`** (`Zaszeregowanie.Wymiar`, `Zaszeregowanie.RodzajStawki`, - `Zaszeregowanie.TypStawki`), bo to one są polami bazodanowymi tej struktury. -- `Etat.Wydzial` i `Etat.Oddzial` to **referencje** do istniejących rekordów — nie twórz „w locie"; - korzeń struktury organizacyjnej pobierzesz przez `session.GetKadry().Wydzialy.Firma`. -- Zmiana warunków etatu **od konkretnego dnia** to nowy zapis historii (`Historia.Update(date)` + - `PracHistorie.AddRow`, patrz A14), a nie nadpisanie bieżącego zapisu (to byłaby korekta całego okresu). -- `TypUmowyOPrace` to enum, nie string; `Okres`/`DataZawarcia`/`DataRozpPracy` to typy biznesowe - `FromTo`/`Date`, nie `DateTime` (safe-code §10.1). - -**Snippet:** - -```csharp -var kadry = session.GetKadry(); - -using (var t = session.Logout(editMode: true)) -{ - // Nowy pracownik (A1) — AddRow tworzy pierwszy zapis historii (Last) + kalendarz. - var pracownik = session.AddRow(new PracownikFirmy()); - pracownik.Kod = "555"; - pracownik.Last.Nazwisko = "Kowalska"; - pracownik.Last.Imie = "Gabriela"; - - // Warunki etatu — na Etat bieżącego (pierwszego) zapisu historii. - // KLUCZOWE: Etat.Okres MUSI być pierwszy — odblokowuje (z trybu read-only) resztę pól Etat. - var etat = pracownik.Last.Etat; - etat.Okres = new FromTo(new Date(2026, 1, 1), Date.MaxValue); // 1) NAJPIERW okres - etat.TypUmowy = TypUmowyOPrace.NaCzasNieokreślony; - etat.DataZawarcia = new Date(2025, 12, 20); - etat.DataRozpPracy = new Date(2026, 1, 1); - etat.Stanowisko = "Specjalista"; // wymagane dla etatu - etat.Wydzial = kadry.Wydzialy.Firma; // wymagane dla etatu (korzeń struktury) - - // Stawka zaszeregowania (po ustawieniu Etat.Okres pola są zapisywalne): - var z = etat.Zaszeregowanie; - z.RodzajStawki = RodzajStawkiZaszeregowania.Miesieczna; - z.TypStawki = TypStawkiZaszeregowania.Dowolna; - z.Wymiar = Fraction.One; // pełny etat - z.Stawka = (Currency)6000m; // kwota brutto miesięcznie - - t.Commit(); -} -session.Save(); -``` - -> **Zmiany warunków zatrudnienia (B2–B7).** Warunki zatrudnienia etatowego siedzą w polu `PracHistoria.Etat: Soneta.Kadry.Etat` -> (subrow zapisu historii). `Etat` jest **historyczny** wraz z całym `PracHistoria` — okres -> obowiązywania warunków trzyma `Etat.Okres: FromTo`, a okres zapisu historii `PracHistoria.Aktualnosc`. -> Zmiana warunków „od dnia" to **nowy zapis historii** (`Historia.Update(date)` + `PracHistorie.AddRow`, -> wzorzec z A14) — modyfikacja bieżącego zapisu byłaby korektą całego jego okresu. -> -> **Bramka edycji etatu (B1).** Na świeżym zapisie cały subrow `Etat` jest tylko-do-odczytu, dopóki -> nie ustawisz `Etat.Okres` — ustaw go **PIERWSZY**, inaczej dotknięcie `TypUmowy`/`Zaszeregowanie.*` -> rzuca `Soneta.Business.ColReadOnlyException`. Pola wymagane przy zapisie etatu: `Etat.Wydzial` -> **oraz** `Etat.Stanowisko`. Po `Update(date)` klon ma już ustawiony `Etat.Okres` (sklonowany ze -> starego zapisu) — zwykle nie trzeba go ustawiać ponownie, ale jeśli zmieniasz okres etatu, rób to -> jako pierwsze. - ---- - -### B2 — Zmiana warunków zatrudnienia (aneks) - -**Cel:** zarejestrować aneks do umowy o pracę — zmianę warunków obowiązującą **od wskazanego dnia** -(np. zmiana stanowiska, miejsca pracy, wymiaru, jednostki organizacyjnej). Realizuje się przez -**nowy zapis historyczny** etatu „od daty", nie przez nadpisanie bieżącego. - -**Pola `Etat` (subrow `PracHistoria.Etat: Soneta.Kadry.Etat`):** - -| Dana | Pole / typ | Uwaga | -|---|---|---| -| Okres etatu (od–do) | `Etat.Okres: Soneta.Types.FromTo` | okres obowiązywania warunków; po `Update` zwykle już ustawiony | -| Rodzaj umowy o pracę | `Etat.TypUmowy: Soneta.Kadry.TypUmowyOPrace` | enum | -| Data zawarcia aneksu | `Etat.DataZawarcia: Soneta.Types.Date` | data podpisania | -| Stanowisko (opis) | `Etat.Stanowisko: string` | wymagane dla etatu | -| Jednostka organizacyjna | `Etat.Wydzial: Soneta.Kadry.Wydzial` | wymagane dla etatu; referencja (`session.GetKadry().Wydzialy.Firma`) | -| Oddział firmy | `Etat.Oddzial: Soneta.Core.OddzialFirmy` | opcjonalny | -| Miejsce wykonywania pracy | `Etat.MiejscePracy: string` | tekst | -| Podstawa stosunku pracy | `Etat.Podstawa: Soneta.Kadry.StosPracyNaPodstawie` | enum | -| Forma organizacji pracy | `Etat.FormaOrganizacjiPracy: Soneta.Kadry.FormaOrganizacjiPracy` | enum | -| Wymiar / stawka (na `Zaszeregowanie`) | `Etat.Zaszeregowanie.Wymiar: Fraction`, `Etat.Zaszeregowanie.Stawka: Currency` | patrz B3 | - -**Snippet:** - -```csharp -var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; -var odDnia = new Date(2026, 7, 1); - -using (var t = session.Logout(editMode: true)) -{ - // Nowy zapis historii „od daty" — klonuje zapis aktualny na `odDnia`, skraca stary do dnia - // poprzedniego i zwraca nowy klon (okres od `odDnia`). Klon MUSI trafić do tabeli zapisów. - var nowy = (PracHistoria)pracownik.Historia.Update(odDnia); - pracownik.Module.PracHistorie.AddRow(nowy); - - // Etat na klonie ma już Okres (sklonowany) — pola Etat są zapisywalne. Aneksowane warunki: - var etat = nowy.Etat; - etat.Stanowisko = "Starszy specjalista"; - etat.MiejscePracy = "Oddział Kraków"; - etat.DataZawarcia = new Date(2026, 6, 20); - etat.Wydzial = session.GetKadry().Wydzialy.Firma; // wymagane (referencja) - - t.Commit(); -} -session.Save(); -``` - -**Pułapki:** -- `Update(date)` + `PracHistorie.AddRow(nowy)` to **nierozłączna para** — sam `Update` zwraca odpięty - klon; bez `AddRow` zmiana nie zostanie zapisana. -- `Update(date)` rzuca `HistorySubTable.DateDuplicateException`, gdy na `date` już zaczyna się zapis - (`Aktualnosc.From == date`) — wtedy modyfikuj istniejący zapis (`pracownik[date]`). -- Nie ustawiaj `PracHistoria.Aktualnosc` ani (zwykle) `Etat.Okres` ręcznie — zarządza nimi historia. - Jeśli aneks zmienia długość okresu etatu, ustaw `Etat.Okres` **przed** pozostałymi polami (bramka B1). -- `Etat` to subrow — modyfikuj jego pola, nie przypisuj całego obiektu. - ---- - -### B3 — Przeszeregowanie (zmiana stawki / grupy zaszeregowania) - -**Cel:** zmienić wynagrodzenie zasadnicze — stawkę i/lub grupę zaszeregowania, od wskazanego dnia. - -**Pola `Etat.Zaszeregowanie: Soneta.Kadry.Zaszeregowanie` (subrow `Etat`):** - -| Dana | Pole / typ | Uwaga | -|---|---|---| -| Rodzaj stawki | `Zaszeregowanie.RodzajStawki: Soneta.Kadry.RodzajStawkiZaszeregowania` | enum: `Godzinowa = 0`, `Miesieczna = 1`, `DochodDeklarowany = 2` | -| Typ stawki | `Zaszeregowanie.TypStawki: Soneta.Kadry.TypStawkiZaszeregowania` | enum: `Dowolna`, `Minimalna`, `ZZakresu`, `WgWskaźnika`, `Nieokreślona` | -| Kwota stawki | `Zaszeregowanie.Stawka: Soneta.Types.Currency` | brutto (miesięczna/godzinowa wg `RodzajStawki`) | -| Wymiar etatu | `Zaszeregowanie.Wymiar: Soneta.Types.Fraction` | `Fraction.One` = pełny etat | -| Grupa zaszeregowania | `Etat.Grupa: Soneta.Kadry.GrupaZaszeregowania` | **leży na `Etat`, nie na `Zaszeregowanie`**; referencja do słownika `session.GetKadry().GrupyZaszer` (opcjonalna) | -| Element wynagrodzenia | `Zaszeregowanie.Element: Soneta.Place.DefinicjaElementu` | element płacowy wiązany ze stawką (opcjonalny) | -| Wskaźnik (wg wskaźnika) | `Zaszeregowanie.WskaznikNazwa: string`, `Zaszeregowanie.WskaznikKrotnosc: double` | gdy `TypStawki = WgWskaźnika` | - -**Snippet (bezpośrednia zmiana, „od daty"):** - -```csharp -var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; -var odDnia = new Date(2026, 7, 1); - -using (var t = session.Logout(editMode: true)) -{ - var nowy = (PracHistoria)pracownik.Historia.Update(odDnia); - pracownik.Module.PracHistorie.AddRow(nowy); - - var etat = nowy.Etat; // subrow zapisu; Okres ustawiony przez Update - etat.Zaszeregowanie.Stawka = (Currency)7200m; // podwyżka stawki zasadniczej - // etat.Grupa = session.GetKadry().GrupyZaszer... // ewentualna zmiana grupy (leży na Etat, nie na Zaszeregowanie) - - t.Commit(); -} -session.Save(); -``` - -**Worker platformy (alternatywa seryjna):** przeszeregowania realizuje moduł -`Soneta.Przeszeregowania` — dokument `Soneta.Kadry.Przeszeregowanie` z elementami -`ElementPrzeszeregowania` (m.in. `Soneta.Kadry.ZmianaStawki`), wykonywany czynnością -`ZmianaStawkiWorker` (zmiana kwoty / procentowa / grupa) dla zaznaczonej grupy pracowników. Element -`ZmianaStawki` ma pola `Grupa: GrupaZaszeregowania`, `Kwota: Currency` i zapisuje wynik do -`Etat.Zaszeregowanie` właściwego zapisu historii. - -**Pułapki:** -- `Wymiar`/`Stawka`/`RodzajStawki` na **świeżym** zapisie są zapisywalne dopiero po `Etat.Okres` - (bramka B1); po `Update` okres jest już sklonowany, więc pola są zapisywalne. -- `Stawka` to `Currency` (nie `decimal`), `Wymiar` to `Fraction` (nie `double`) — safe-code §10.1. -- `Etat.Grupa`/`Zaszeregowanie.Element` to **referencje** do istniejących rekordów — nie twórz „w locie". - **Uwaga:** `Grupa` jest polem `Etat` (pobierasz ze słownika `session.GetKadry().GrupyZaszer`), a **nie** - polem `Zaszeregowanie` — `Zaszeregowanie` nie ma property `Grupa`. -- Kanonicznie ustawiasz pola stawki na `Etat.Zaszeregowanie` (pola bazodanowe), nie na delegatach - `Etat.Wymiar`/`Etat.TypStawki`. - ---- - -### B4 — Rozwiązanie / wygaśnięcie umowy o pracę - -**Cel:** zakończyć stosunek pracy z dniem rozwiązania — ustawić datę końca okresu etatu, dane -wypowiedzenia oraz przyczynę/kod rozwiązania (na potrzeby świadectwa pracy i deklaracji ZUS). - -**Wzorzec (zgodny z czynnością „Zwolnij zaznaczonych pracowników"):** -1. skróć `Etat.Okres.To` do dnia rozwiązania (na bieżącym zapisie albo na nowym zapisie „od daty"), -2. ustaw dane wypowiedzenia (`Etat.OkresWypowiedzenia.*`) i przyczynę (`Etat.RozwiazanieUmowy.*`), -3. opcjonalnie oznacz `Etat.PracownikZwolniony = true` i wyrejestruj z ubezpieczeń. - -**Pola — `Etat.OkresWypowiedzenia: Soneta.Kadry.OkresWypowiedzenia` (subrow):** - -| Dana | Pole / typ | Uwaga | -|---|---|---| -| Data złożenia wypowiedzenia | `OkresWypowiedzenia.DataZlozenia: Soneta.Types.Date` | data wręczenia wypowiedzenia | -| Długość — dni / tygodnie / miesiące | `OkresWypowiedzenia.Dni: int`, `.Tygodnie: int`, `.Miesiace: int` | okres wypowiedzenia | -| Data upływu | `OkresWypowiedzenia.Uplywa: Soneta.Types.Date` | data upływu okresu wypowiedzenia | -| Skrócony | `OkresWypowiedzenia.Skrocony: bool` | skrócony okres wypowiedzenia | -| Zwolnienie z obowiązku pracy od | `OkresWypowiedzenia.ZwolnionyZObowiazkuPracyOd: Date` | | -| Data rozwiązania umowy (odczyt) | `OkresWypowiedzenia.DataRozwiązaniaUmowy: Date` | kalkulowane | - -**Pola — `Etat.RozwiazanieUmowy: Soneta.Kadry.RozwiazanieUmowy` (subrow):** - -| Dana | Pole / typ | Uwaga | -|---|---|---| -| Przyczyna rozwiązania | `RozwiazanieUmowy.PrzyczynaRozwUmowy: Soneta.Kadry.PrzyczynaRozwUmowy` | **referencja do słownika** `session.GetKadry().PrzyczRozwUmow` (indeks `WgNazwy` lub iteracja; **brak indeksera `WgKodu`**); rekord ma `Typ: TypPrzyczynyRozwUmowy` | -| Opis przyczyny | `RozwiazanieUmowy.PrzyczynaRozwUmowyOpis: string` | tekst | -| Podstawa prawna | `RozwiazanieUmowy.PodstawaPrawna: Soneta.Kadry.KodPodstawyPrawnejZwolnienia` | enum (tryb rozwiązania: za wypowiedzeniem, porozumienie, wygaśnięcie itd.) | -| Kod zwolnienia (ZUS) | `RozwiazanieUmowy.KodZwolnienia: Soneta.Kadry.KodZwolnienia` | enum (kod do ZUS RA/świadectwa) | -| Inicjatywa | `RozwiazanieUmowy.Inicjatywa: Soneta.Kadry.KodInicjatywyZwolnienia` | enum (pracodawca/pracownik) | -| Za odszkodowaniem | `RozwiazanieUmowy.ZaOdszkodowaniem: bool` | | -| Pracownik zwolniony (flaga) | `Etat.PracownikZwolniony: bool` | znacznik zakończenia | - -**Snippet:** - -```csharp -var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; -var dataRozwiazania = new Date(2026, 9, 30); - -using (var t = session.Logout(editMode: true)) -{ - var ph = pracownik.Last; - var etat = ph.Etat; - - // 1) skrócenie okresu etatu do dnia rozwiązania - etat.Okres = new FromTo(etat.Okres.From, dataRozwiazania); - - // 2) dane wypowiedzenia - etat.OkresWypowiedzenia.DataZlozenia = new Date(2026, 8, 31); - etat.OkresWypowiedzenia.Miesiace = 1; - - // 3) przyczyna / tryb rozwiązania - // PrzyczynaRozwUmowy to rekord słownika — pobierz po nazwie (WgNazwy) albo iteracją (brak WgKodu): - etat.RozwiazanieUmowy.PrzyczynaRozwUmowy = session.GetKadry().PrzyczRozwUmow.WgNazwy["Wypowiedzenie przez pracownika"]; // referencja - // PodstawaPrawna to enum kodów: NieDotyczy, _400.._463, _550 (kody GUS/ZUS) — wybierz właściwy kod: - etat.RozwiazanieUmowy.PodstawaPrawna = KodPodstawyPrawnejZwolnienia._400; - etat.RozwiazanieUmowy.Inicjatywa = KodInicjatywyZwolnienia.Pracownik; - - etat.PracownikZwolniony = true; // znacznik zakończenia zatrudnienia - - t.Commit(); -} -session.Save(); -``` - -**Pułapki:** -- **Wygaśnięcie** vs **rozwiązanie** rozróżnia `PodstawaPrawna` (enum trybu) oraz `KodZwolnienia` — - to one trafiają do świadectwa pracy i deklaracji ZUS. -- `PrzyczynaRozwUmowy` to **rekord słownika** (referencja), nie enum — pobierz istniejący wpis z - `session.GetKadry().PrzyczRozwUmow` (indeks `WgNazwy` lub iteracja — **słownik nie ma indeksera - `WgKodu`**). Pomyłka: `RozwiazanieUmowy.PrzyczynaRozwUmowy` (referencja) ≠ - `PrzyczynaRozwUmowy.Typ` (enum `TypPrzyczynyRozwUmowy` na rekordzie słownika). -- `KodPodstawyPrawnejZwolnienia` to enum kodów GUS/ZUS o wartościach `NieDotyczy`, `_400`..`_463`, - `_550` (nazwy z prefiksem `_`) — **nie ma** wartości opisowych typu - `RozwiazanieZaWypowiedzeniemPrzezPracownika`. `KodInicjatywyZwolnienia`: `NieDotyczy`, `Pracownik`, - `Pracodawca`. -- Skrócenie `Etat.Okres.To` zmienia warunki w **całym** bieżącym okresie zapisu. Jeśli rozwiązanie - ma obowiązywać od konkretnego dnia z zachowaniem poprzedniego okresu, użyj nowego zapisu - (`Historia.Update(data)` + `PracHistorie.AddRow`), a zmiany rób na klonie. -- Wyrejestrowanie z ubezpieczeń (`IUbezpieczenie.Wyrejestrowany`/daty `Do`) to osobny krok — patrz A7. -- `Okres`/`DataZlozenia`/`Uplywa` to `FromTo`/`Date`, nie `DateTime`. - ---- - -### B5 — Obniżenie / przywrócenie wymiaru etatu - -**Cel:** czasowo obniżyć wymiar etatu i stawkę (operacje typu COVID / seryjne), a następnie -przywrócić warunki. Stan obniżenia jest **odczytowo** widoczny w subrowie -`Etat.ObnizenieEtatu: Soneta.Kadry.ObniżenieWymiaruEtatu` (delegat do zapisu historii etatu). - -> **Ważne (zweryfikowane na DLL):** subrow `ObniżenieWymiaruEtatu` jest **w całości tylko-do-odczytu** -> — wszystkie jego property (`Wymiar`, `Stawka`, `RodzajStawki`, `TypStawki`, `Element`, `Kalendarz`, -> `Info`) mają `CanWrite == false`, a klasa **nie udostępnia publicznej metody `Save(...)`**. -> Z poziomu kodu biznesowego **nie da się** ustawić tych pól ani „utrwalić" obniżenia przez ten subrow. -> Pełny zapis stanu obniżenia (z metadanymi `ObniżenieWymiaruEtatuInfo`) realizują **workery platformy**. -> W zwykłym kodzie obniżenie sprowadzasz do ustawienia docelowego `Etat.Zaszeregowanie.Wymiar` -> (i ewentualnie `Stawka`) na nowym zapisie „od daty". - -**Pola odczytowe — `Etat.ObnizenieEtatu: Soneta.Kadry.ObniżenieWymiaruEtatu` (subrow, read-only):** - -| Dana | Pole / typ | Uwaga | -|---|---|---| -| Obniżony wymiar etatu | `ObnizenieEtatu.Wymiar: Soneta.Types.Fraction` | **read-only** | -| Obniżona stawka | `ObnizenieEtatu.Stawka: Soneta.Types.Currency` | **read-only** | -| Rodzaj / typ stawki | `ObnizenieEtatu.RodzajStawki`, `.TypStawki` | enumy, **read-only** | -| Kalendarz | `ObnizenieEtatu.Kalendarz: Soneta.Kalend.KalendarzBase` | referencja, **read-only** | -| Element wynagrodzenia | `ObnizenieEtatu.Element: Soneta.Place.DefinicjaElementu` | referencja, **read-only** | -| Zakres obniżenia (przełącznik) | `ObnizenieEtatu.Info: Soneta.Kadry.ObniżenieWymiaruEtatuInfo` | enum (`Brak`/`Wymiar`/`Stawka`/`Zaszeregowanie`/`Kalendarz`/…), **read-only** | - -**Snippet (obniżenie wymiaru „od daty" w kodzie biznesowym):** - -```csharp -var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; -var odDnia = new Date(2026, 7, 1); - -using (var t = session.Logout(editMode: true)) -{ - var ph = (PracHistoria)pracownik.Historia.Update(odDnia); - pracownik.Module.PracHistorie.AddRow(ph); - - // Subrow ObnizenieEtatu jest read-only — NIE ustawiamy go bezpośrednio. - // Docelowy wymiar po obniżeniu utrwalamy na Etat.Zaszeregowanie.Wymiar (pole zapisywalne): - ph.Etat.Zaszeregowanie.Wymiar = new Fraction(4, 5); // np. obniżenie do 4/5 etatu - - t.Commit(); -} -session.Save(); -``` - -**Workery platformy (seryjne, na zaznaczonych pracownikach, tabela `Pracownicy`):** -`ObniżWymiarEtatuWorker` (obniżenie: proporcjonalnie / do / o), `ZmianaStawkiZaszeregowaniaWorker`, -`ZmianaKalendarzaWorker`, `PrzywróćWarunkiZatrudnieniaWorker` (przywrócenie warunków sprzed -obniżenia). To one zakładają nowy zapis historii „od daty" i zapisują pełny stan obniżenia -(`ObniżenieWymiaruEtatuInfo`), którego nie da się ustawić przez publiczny kontrakt subrowa. - -**Pułapki:** -- `Etat.ObnizenieEtatu` to **odczytowy delegat** do zapisu historii etatu — wszystkie property są - read-only i klasa nie ma metody `Save(...)`. W kodzie biznesowym obniżenie wymiaru realizujesz - ustawiając `Etat.Zaszeregowanie.Wymiar` (i `Stawka`) na nowym zapisie; pełny zapis stanu obniżenia - z `ObniżenieWymiaruEtatuInfo` zostaw workerom platformy. -- Operacja jest „od daty" — zawsze przez nowy zapis (`Update` + `AddRow`); inaczej zmienisz wymiar - wstecz w całym bieżącym okresie. -- Przywrócenie warunków to osobna operacja (`PrzywróćWarunkiZatrudnieniaWorker`) — nie polega na - usuwaniu obniżenia, lecz na nowym zapisie z przywróconym wymiarem. - ---- - -### B6 — Podzielniki kosztów (rozdział kosztów wynagrodzenia) - -**Cel:** rozdzielić koszty wynagrodzenia pracownika na wydziały/projekty/centra kosztów wg -współczynników. Struktura: `Pracownik` jako **źródło** podzielnika → -`pracownik.Podzielniki: SubTable` → każdy podzielnik ma **historię** -`PodzielnikKosztow.Historia: HistorySubTable` → a zapis historii ma kolekcję -`HistoriaPodzielnika.Elementy: SubTable` (poszczególne udziały). - -> Uwaga: `Pracownik.ElementyPodzielnika: SubTable` to widok zbiorczy elementów -> ze wszystkich podzielników pracownika (do odczytu). **Tworzysz** elementy na konkretnym zapisie -> `HistoriaPodzielnika`, nie przez tę kolekcję. - -**Tworzenie obiektów (konstruktory + AddRow):** - -| Obiekt | Konstruktor | Tabela / AddRow | -|---|---|---| -| Podzielnik | `new PodzielnikKosztow(pracownik)` (pracownik jako `IZrodloPodzielnikaKosztow`) | `session.GetCore().PodzielKosztow.AddRow(p)` | -| Zapis historii | `podzielnik.Historia.Update(odDnia)` | `session.GetCore().HistPodzielnikow.AddRow(h)` | -| Element udziału | `new ElementPodzielnika(historia)` | `session.GetCore().ElemPodzielnikow.AddRow(e)` | - -**Pola — `PodzielnikKosztow`:** `Nazwa: string`, `Definicja: Soneta.Core.DefinicjaPodzielnikaKosztow`, -`Zrodlo: IZrodloPodzielnikaKosztow` (pracownik, ustawiany ctorem), `Last/Historia`. -**Pola — `HistoriaPodzielnika`:** `Aktualnosc: FromTo` (okres zapisu, zarządzany), `Podstawa: decimal`, -`Elementy: SubTable`. -**Pola — `ElementPodzielnika`:** `ElementPodzialowy: Soneta.Core.IElementSlownika` (cel rozdziału — -m.in. `Wydzial`, `Projekt`, `CentrumKosztow`, `OddzialFirmy` — iface-ref), `Wspolczynnik: double`, -`Procent: Percent` (kalkulowany z współczynników). - -**Snippet:** - -```csharp -var core = session.GetCore(); -var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; -var odDnia = new Date(2026, 1, 1); -var wydzialA = session.GetKadry().Wydzialy.Firma; // referencja do celu rozdziału (IElementSlownika) - -using (var t = session.Logout(editMode: true)) -{ - var podzielnik = new PodzielnikKosztow(pracownik); // ctor wiąże ze źródłem (pracownik) - core.PodzielKosztow.AddRow(podzielnik); - podzielnik.Nazwa = "Rozdział kosztów"; - // podzielnik.Definicja = ... // referencja do definicji (opcjonalnie) - - // zapis historii „od daty" (klon + AddRow) - var historia = podzielnik.Historia.Update(odDnia); - core.HistPodzielnikow.AddRow(historia); - - // udział: cel rozdziału + współczynnik - var element = new ElementPodzielnika(historia); - core.ElemPodzielnikow.AddRow(element); - element.ElementPodzialowy = wydzialA; - element.Wspolczynnik = 100d; // Procent wyliczany z współczynników - - t.Commit(); -} -session.Save(); - -// Odczyt zbiorczy elementów podzielnika pracownika: -foreach (ElementPodzielnika e in pracownik.ElementyPodzielnika) -{ - IElementSlownika cel = e.ElementPodzialowy; // np. Wydzial / Projekt - double wsp = e.Wspolczynnik; -} -``` - -**Pułapki:** -- Trójpoziomowa struktura — `PodzielnikKosztow` (root, źródło = pracownik) → `HistoriaPodzielnika` - (historia „od daty") → `ElementPodzielnika` (udziały). Każdy poziom: konstruktor **+** `AddRow` do - właściwej tabeli `Core`. Sam konstruktor nie wystarczy. -- `Historia.Update(odDnia)` + `HistPodzielnikow.AddRow` — para jak w A14; zmiana udziałów „od dnia" - to nowy zapis historii (a wcześniej zwykle usunięcie/`Delete()` elementów starego zapisu przy - aktualizacji tego samego okresu — patrz worker pracy zdalnej). -- `ElementPodzialowy` to **referencja interfejsowa** (`IElementSlownika`) — przypisz istniejący - rekord (`Wydzial`, `Projekt`, `CentrumKosztow`, …), nie twórz „w locie". -- `Procent` jest kalkulowany z `Wspolczynnik` poszczególnych elementów — ustawiasz współczynniki, - nie procenty. - ---- - -### B7 — Aktualizacja danych wg definicji stanowiska (matrycy) - -**Cel:** powiązać etat z definicją stanowiska i przejąć z niej parametry (stawka/grupa/wymiar, -kalendarz, kod zawodu). Definicja stanowiska to **matryca** — wzorzec wartości dla etatu. - -**Pole na etacie:** `Etat.Definicja: Soneta.HR.DefinicjaStanowiska` (referencja do słownika -konfiguracyjnego `session.GetHR().DefStanowisk`). Pokrewne: `Etat.DefinicjaFunkcji: DefinicjaFunkcji`. - -**Pola `DefinicjaStanowiska` (matryca, do skopiowania na etat):** - -| Dana | Pole / typ | -|---|---| -| Nazwa / stanowisko | `Nazwa: string`, `Stanowisko: string`, `StanowiskoPelne: string` | -| Funkcja | `Funkcja: string`, `DefinicjaFunkcji: Soneta.HR.DefinicjaFunkcji` | -| Zaszeregowanie (wzorzec) | `Zaszeregowanie: Soneta.Kadry.Zaszeregowanie` (`Stawka`, `Wymiar`, `RodzajStawki`, `Element`, `WskaznikNazwa/Krotnosc`) | -| Typ stawki / grupa | `TypStawki: TypStawkiZaszeregowania`, `Grupa: Soneta.Kadry.GrupaZaszeregowania` | -| Kalendarz | `Kalendarz: Soneta.Kalend.Kalendarz`, `NieNadpisujKalendarza: bool` | -| Kod zawodu / praca w szcz. warunkach | `KodWykonywanegoZawodu`, `KodPracyWSzczWarunkach`, `InterpretacjaKalendarza` | - -**Snippet:** - -```csharp -var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; -var def = session.GetHR().DefStanowisk.WgNazwa["Specjalista ds. kadr"]; // matryca (referencja; klucz WgNazwa) -var odDnia = new Date(2026, 7, 1); - -using (var t = session.Logout(editMode: true)) -{ - var nowy = (PracHistoria)pracownik.Historia.Update(odDnia); - pracownik.Module.PracHistorie.AddRow(nowy); - - var etat = nowy.Etat; - etat.Definicja = def; // powiązanie z definicją stanowiska - etat.Stanowisko = def.Stanowisko; // przeniesienie wartości z matrycy - etat.Zaszeregowanie.Wymiar = def.Zaszeregowanie.Wymiar; - etat.Zaszeregowanie.Stawka = def.Zaszeregowanie.Stawka; - - t.Commit(); -} -session.Save(); -``` - -**Pułapki:** -- `Etat.Definicja` to **referencja** do rekordu konfiguracyjnego `DefStanowisk` — pobierz istniejącą - (`session.GetHR().DefStanowisk`), nie twórz „w locie". Indeks po nazwie to `WgNazwa` - (**nie `WgNazwy`**); w bazie Demo słownik bywa pusty — zabezpiecz się na brak definicji. -- Definicja jest matrycą — przeniesienie wartości (stawka/wymiar/kalendarz) na etat zrób jawnie; - samo wskazanie `Etat.Definicja` nie nadpisuje automatycznie wszystkich pól etatu w kodzie biznesowym. -- Dostępność definicji potrafi zależeć od konfiguracji (`DefinicjeStanowiskDlaWydziałów`) — definicja - może być filtrowana po wydziale. -- Zmiana stanowiska „od dnia" to nowy zapis historii (A14), nie nadpisanie bieżącego. - -## C. Dodatki, potrącenia, akordy - -### C1 — Dodatki / stałe elementy wynagrodzenia (★) - -**Cel:** przypisać pracownikowi stały element wynagrodzenia (dodatek — np. premia, dodatek -funkcyjny), oparty o definicję elementu płacowego, z okresem obowiązywania i parametrami -(podstawa/procent/czas). W UI: menu czynności *Dodatki i potrącenia → Dodaj nowy*. - -**Klasa i model:** `Soneta.Kadry.Dodatek` — `GuidedRow` root, tabela `Dodatki`, obiekt -**historyczny** (kolekcja `Dodatek.Historia: HistorySubTable`, parametry -„od–do" siedzą w zapisach `DodHistoria`). Dodatek jest childem pracownika i pojawia się w -`pracownik.Dodatki: SubTable`. - -**Tworzenie:** `new Dodatek(pracownik)` + `session.GetKadry().Dodatki.AddRow(d)`. Dodanie do tabeli -tworzy **pierwszy zapis** `DodHistoria` — dostępny od razu jako `d.Last`. Parametry ustawiamy na -`d.Last`. - -**Pola i typy (`DodHistoria` — `d.Last`):** - -| Pole | Typ | Uwaga | -|---|---|---| -| `Element` | `Soneta.Place.DefinicjaElementu` | definicja elementu wynagrodzenia (wymagana); pobierz istniejącą z `session.GetPlace().DefElementow.WgNazwy[nazwa]` | -| `Okres` | `Soneta.Types.FromTo` | okres obowiązywania dodatku | -| `Podstawa` | `Soneta.Types.Currency` | kwota podstawy (gdy algorytm definicji jej wymaga) | -| `Procent` | `Soneta.Types.Percent` | procent (gdy algorytm definicji go wymaga) | -| `Czas` | `Soneta.Types.Time` | czas (gdy algorytm definicji go wymaga) | -| `Ulamek` | `Soneta.Types.Fraction` | ułamek (zależnie od definicji) | -| `Dni` | `int` | liczba dni (zależnie od definicji) | -| `Aktualnosc` | `Soneta.Types.FromTo` | okres zapisu historycznego (zarządzany przez historię — nie ustawiaj ręcznie) | - -**Pola na rootcie `Dodatek`:** `Nazwa: string`, `Pracownik: Soneta.Kadry.Pracownik` (właściciel, -ustawiany ctorem), `DataZakonczeniaWyplaty: Date`, `Last: DodHistoria`, -`Historia: HistorySubTable`, `Dodatki` (tabela: `session.GetKadry().Dodatki`). - -**Pobranie definicji elementu:** słownik `session.GetPlace().DefElementow` (kolekcja konfiguracyjna). -Indeksowanie po nazwie: `DefElementow.WgNazwy["Premia"]`. Definicje dodatków mają -`RodzajZrodla == Soneta.Place.RodzajŹródłaWypłaty.Dodatek` — można nimi filtrować dostępne -definicje. W bazie Demo istnieją gotowe definicje, m.in. `"Premia"`, `"Premia procentowa"`. - -**Pułapki:** -- **`new Dodatek(pracownik)` + `Dodatki.AddRow(d)` to para** — sam konstruktor nie włącza dodatku do - sesji ani nie tworzy zapisu historii. Pierwszy `DodHistoria` powstaje przy `AddRow`; dopiero potem - istnieje `d.Last`. -- `Podstawa`/`Procent`/`Czas` **mogą być tylko-do-odczytu** w zależności od algorytmu wskazanej - `DefinicjaElementu` — element kwotowy udostępnia `Podstawa`, element procentowy `Procent` itd. - Ustawiaj tylko te pola, których wymaga definicja (próba zapisu pola read-only rzuci wyjątek). -- `Element` jest **wymagany** — bez wskazania definicji elementu dodatek nie ma sensu płacowego. - Definicję pobierasz z istniejącego słownika (`DefElementow`), nie tworzysz „w locie" w tym scenariuszu. -- Zmiana parametrów dodatku **od konkretnego dnia** to nowy zapis historii dodatku - (`d.Historia.Update(date)` + `Dodatki.Module.DodHistorie.AddRow(nowy)`), analogicznie do A14 — nie - nadpisuj bieżącego zapisu, jeśli chcesz zachować poprzedni okres. -- `DodHistoria.Aktualnosc` (okres zapisu) zarządza mechanizm historii — sam ustawiasz `Okres`, - `Aktualnosc` zostaw historii. - -**Snippet:** - -```csharp -var kadry = session.GetKadry(); -var pracownik = kadry.Pracownicy.WgKodu["006"]; - -// Definicja elementu wynagrodzenia ze słownika konfiguracyjnego (po nazwie): -var definicjaPremii = session.GetPlace().DefElementow.WgNazwy["Premia"]; - -using (var t = session.Logout(editMode: true)) -{ - // new Dodatek(pracownik) + AddRow — AddRow tworzy pierwszy zapis DodHistoria (d.Last): - var dodatek = new Dodatek(pracownik); - kadry.Dodatki.AddRow(dodatek); - - var h = dodatek.Last; // pierwszy zapis historii dodatku - h.Element = definicjaPremii; // definicja elementu (wymagana) - h.Okres = new FromTo(new Date(2026, 1, 1), Date.MaxValue); - h.Podstawa = (Currency)500m; // gdy algorytm definicji wymaga podstawy - - t.Commit(); -} -session.Save(); - -// Odczyt dodatków pracownika i ich definicji elementu (kolekcja childów): -foreach (Dodatek d in pracownik.Dodatki) -{ - DefinicjaElementu element = d.Last.Element; - FromTo okres = d.Last.Okres; -} -``` - -### C2 — Potrącenia (stałe i jednorazowe) (★) - -**Cel:** przypisać pracownikowi potrącenie z wynagrodzenia (np. składka związkowa, spłata -rozliczana ręcznie, potrącenie dobrowolne). W modelu płacowym **potrącenie nie ma osobnej klasy** — -to **`Soneta.Kadry.Dodatek`** (jak C1), tyle że oparty o **definicję elementu o charakterze -potrącenia**. O „minusowym" charakterze decyduje wyłącznie wskazana `DefinicjaElementu` (jej algorytm), -a nie typ obiektu po stronie pracownika. - -**Jak rozpoznać definicję potrącenia (`Soneta.Place.DefinicjaElementu`, słownik `DefElementow`):** - -| Pole definicji | Typ | Znaczenie dla potrącenia | -|---|---|---| -| `Algorytm.Potracenie` | `bool` | **kluczowy znacznik** — `true` dla elementu potrącającego (element pomniejsza wynagrodzenie) | -| `Algorytm.LimitPotracenia` | `Soneta.Place.TypLimituPotrącenia` | rodzaj limitu (np. do kwoty wolnej) — gdy potrącenie limitowane | -| `Algorytm.TylkoPelnePotracenie` | `bool` | potrącać tylko w pełnej kwocie (bez częściowego) | -| `RodzajZrodla` | `Soneta.Place.RodzajŹródłaWypłaty` | dla potrącenia przez dodatek **musi być** `Dodatek` (= `6`); enum **nie ma** wartości „Potrącenie" (ma natomiast m.in. `ZajęcieKomornicze` = 23, `Świadczenie` = 12, `Pożyczka` = 18, `PożyczkaSpłata` = 19). Minus realizuje algorytm, ale `DodHistoria.Element` **odrzuca** definicje o `RodzajZrodla != Dodatek` (np. „Alimenty" jako `ZajęcieKomornicze`) — patrz pułapki | - -**Mechanizm — identyczny jak C1 (Dodatek + DodHistoria):** -- `new Dodatek(pracownik)` + `session.GetKadry().Dodatki.AddRow(d)` → powstaje pierwszy `DodHistoria` - (`d.Last`). -- Na `d.Last` ustawiamy `Element` (definicja potrącenia), `Okres` oraz `Podstawa`/`Procent`/`Kwota` - zależnie od algorytmu definicji. -- **Potrącenie stałe**: `Okres` otwarty (do `Date.MaxValue`) lub na czas określony — naliczane w każdej - wypłacie z okresu. -- **Potrącenie jednorazowe**: `Okres` zawężony do jednego miesiąca rozliczeniowego (tylko ten miesiąc - obejmie naliczenie). -- Zakończenie potrącenia: `d.DataZakonczeniaWyplaty` + ewentualnie `d.PrzyczynaZakonczenia`, albo nowy - zapis historii „od daty" (`d.Historia.Update(date)`), analogicznie do C1/A14. - -**Pułapki:** -- Nie szukaj klasy „Potrącenie" — jej **nie ma**. Potrącenie = `Dodatek` z definicją, w której - `Algorytm.Potracenie == true`. Dobór definicji jest jedynym wyróżnikiem. -- **Filtruj po DWÓCH warunkach** (zweryfikowane testem): `d.Algorytm.Potracenie && d.RodzajZrodla == - RodzajŹródłaWypłaty.Dodatek`. Sam `Algorytm.Potracenie` **nie wystarcza** — przy ustawianiu - `DodHistoria.Element` definicja o innym `RodzajZrodla` (np. „Alimenty" jako `ZajęcieKomornicze`) - rzuca `System.Exception: "Zły rodzaj źródła wypłaty elementu …"`. Element zajęcia komorniczego ma - `RodzajZrodla == ZajęcieKomornicze` i podpinasz go pod `ZajęcieKomornicze`, nie pod `Dodatek` (C4). -- `Podstawa`/`Procent`/`Czas` na `DodHistoria` bywają tylko-do-odczytu zależnie od algorytmu definicji - (jak w C1) — ustawiaj tylko te, których definicja wymaga. -- `Element` wymagany; pobierany z istniejącego słownika `DefElementow`, nie tworzony „w locie". - -**Snippet:** - -```csharp -var kadry = session.GetKadry(); -var pracownik = kadry.Pracownicy.WgKodu["006"]; - -// Definicja potrącenia ze słownika — DWA warunki: Potracenie ORAZ RodzajZrodla == Dodatek: -var def = session.GetPlace().DefElementow.Cast() - .First(d => d.RodzajZrodla == RodzajŹródłaWypłaty.Dodatek - && d.Algorytm != null && d.Algorytm.Potracenie); - -using (var t = session.Logout(editMode: true)) -{ - var potracenie = new Dodatek(pracownik); - kadry.Dodatki.AddRow(potracenie); // tworzy pierwszy DodHistoria - - var h = potracenie.Last; - h.Element = def; // definicja o Algorytm.Potracenie == true - h.Okres = new FromTo(new Date(2026, 1, 1), Date.MaxValue); // stałe - h.Podstawa = (Currency)50m; // gdy algorytm definicji wymaga kwoty - - t.Commit(); -} -session.Save(); -``` - ---- - -### C3 — Akordy (★) - -**Cel:** przypisać pracownikowi pracę akordową (rozliczaną wg ilości/strefy), z okresem i definicją -akordu; zakończyć akord. W UI: menu czynności *Akordy → Dodaj nowy / Zakończ*. - -**Klasa i model:** `Soneta.Kadry.Akord` — `GuidedRow` root, tabela `Akordy`, obiekt **historyczny** -(`Akord.Historia: HistorySubTable`; parametry „od–do" w zapisach -`AkordHistoria`, dostęp do bieżącego przez `Akord.Last`). Akord jest childem pracownika: -`pracownik.Akordy: SubTable`. - -**Pola root `Akord`:** - -| Pole | Typ | Uwaga | -|---|---|---| -| `Pracownik` | `Soneta.Kadry.Pracownik` | właściciel (relacja) | -| `Definicja` | `Soneta.Kadry.DefinicjaAkordu` | definicja akordu (słownik `DefinicjeAkordow`) | -| `Okres` | `Soneta.Types.FromTo` | okres obowiązywania akordu | -| `Typ` | `Soneta.Kadry.TypAkordu` | typ akordu | -| `Wydzial` | `Soneta.Kadry.Wydzial` | jednostka organizacyjna realizacji | -| `Last` | `Soneta.Kadry.AkordHistoria` | bieżący zapis historii | -| `Dni` | `DateSubTable` | dzienna realizacja akordu | - -**Pola `AkordHistoria` (`Akord.Last`):** `Okres: FromTo`, `Algorytm: Soneta.Kadry.AlgorytmAkordu` -(subrow z `Algorytm.Element: DefinicjaElementu`, `Algorytm.Wspolczynnik`, `Algorytm.Progi`, -`Algorytm.WgCzasu`/`Progresywny` itd.), `Jednostka: string`, `Aktualnosc: FromTo` (zarządzane przez -historię), `Progi: SubTable`. - -**Tworzenie — brak publicznego konstruktora `Akord(pracownik)`.** Akord dodaje się **workerem** -operacyjnym (kanonicznie), nie `new`. Konstruktor `Akord` jest niepubliczny (poza `RowCreator`). -Worker jest „jak z UI" (`Params` dziedziczy z `ContextBase`, ctor wymaga `Context`) — uruchamiaj go w -transakcji `CommitUI`. - -**Workery (zagnieżdżone w `Pracownik`):** ctor `(Session)`, parametry przez właściwości `Pars`/`Pracownicy`; -`Params` ma ctor `(Context)`. - -| Worker | Metoda | Wzorzec użycia | -|---|---|---| -| `Soneta.Kadry.Pracownik.DodajAkordWorker` | `DodajAkord` | `new Params(ctx) { Definicja, OdDnia, DoDnia, DodajKolejny }`; `new DodajAkordWorker(session) { Pars = par, Pracownicy = tab }` | -| `Soneta.Kadry.Pracownik.ZakończAkordWorker` | `ZakończAkord` | `new Params(ctx) { Definicja, DoDnia, ZakończWszystkie }`; `new ZakończAkordWorker(session) { Pars = par, Pracownicy = tab }` | - -**Pułapki:** -- Akordu **nie twórz przez `new Akord(...)`** — kanoniczna ścieżka to `DodajAkordWorker` (analogicznie - `ZakończAkordWorker` do zakończenia). Workery przyjmują **tablicę pracowników**, więc nadają się też do - operacji grupowej. -- `Definicja` (akordu) to rekord słownika `DefinicjeAkordow` — pobierz istniejący, nie twórz „w locie". - Sam akord wiąże dopiero z `DefinicjaElementu` (płacowym) przez `Algorytm.Element` definicji akordu. -- Akord jest historyczny — zmiana parametrów „od daty" to nowy zapis `AkordHistoria` - (`Historia.Update(date)`), analogicznie do C1/A14. -- Tabela `Akordy` to dane operacyjne — przy przeglądaniu poprzecznym filtruj zakresem (safe-code §6.3); - w zakresie jednego pracownika korzystaj z `pracownik.Akordy`. - -**Snippet:** - -```csharp -var kadry = session.GetKadry(); -var pracownik = kadry.Pracownicy.WgKodu["006"]; -var defAkordu = kadry.DefinicjeAkordow.WgNazwa["Akord prosty"]; // klucz WgNazwa (l.poj.) -var context = login.CreateEmptyContext().Clone(session); - -using (var t = session.Logout(editMode: true)) -{ - var par = new Pracownik.DodajAkordWorker.Params(context) // Params: ctor (Context) - { - Definicja = defAkordu, - OdDnia = new Date(2026, 1, 1), - DoDnia = new Date(2026, 12, 31), - }; - // ctor (Session); parametry przez właściwości Pars/Pracownicy: - new Pracownik.DodajAkordWorker(session) { Pars = par, Pracownicy = new[] { pracownik } }.DodajAkord(); - t.CommitUI(); -} -session.Save(); - -// Odczyt akordów pracownika: -foreach (Akord a in pracownik.Akordy) -{ - DefinicjaAkordu def = a.Definicja; - FromTo okres = a.Okres; - DefinicjaElementu element = a.Last.Algorytm.Element; -} -``` - ---- - -### C4 — Zajęcia wynagrodzenia (komornicze, alimentacyjne) (★) - -**Cel:** zarejestrować zajęcie wynagrodzenia (egzekucja komornicza lub alimentacyjna) z numerem sprawy, -kwotą, priorytetem i wierzycielem (komornikiem/rachunkiem odbiorcy); anulować/przywrócić zajęcie. - -**Klasa i model:** `Soneta.Kadry.ZajęcieKomornicze` — `GuidedRow` root, tabela `ZajKomornicze`, obiekt -**historyczny** (`Historia: HistorySubTable`; limity i kwoty -„od–do" w zapisach historii, bieżący przez `Last`). Child pracownika: -`pracownik.ZajęciaKomornicze: SubTable`. **Konstruktor publiczny:** -`new ZajęcieKomornicze(pracownik)`. - -**Pola root `ZajęcieKomornicze`:** - -| Pole | Typ | Uwaga | -|---|---|---| -| `Pracownik` | `Soneta.Kadry.Pracownik` | właściciel | -| `Rodzaj` | `Soneta.Kadry.RodzajeZajęciaWynagrodzenia` | enum: `Kwota = 0`, `KwotaMiesięczna = 1` (jednorazowa kwota vs miesięczna) | -| `Element` | `Soneta.Place.DefinicjaElementu` | element płacowy zajęcia — **wymagany**; musi mieć `RodzajZrodla == ZajęcieKomornicze` (= 23) | -| `NumerSprawy` | `string` | numer sprawy egzekucyjnej | -| `Data` | `Soneta.Types.Date` | data zajęcia | -| `DataSplaty` | `Soneta.Types.Date` | data spłaty/zakończenia | -| `Rozliczenie.Odbiorca` | `Soneta.Kasa.IPodmiotKasowy` | **wierzyciel** — komornik/odbiorca (iface; może być `Kontrahent`, `Bank`, `Pracownik`, `UrzadSkarbowy`…) | -| `Rozliczenie.RachunekOdbiorcy` | `Soneta.Kasa.RachunekBankowyPodmiotu` | rachunek wierzyciela do przelewu | -| `Splacono` | `Soneta.Types.Currency` | kwota spłacona (kalkulowane/narastające) | -| `Pozostało` | `Soneta.Types.Currency` | kwota pozostała (kalkulowane) | -| `SplataZakonczona` | `bool` | spłata zakończona | -| `Anulowane` | `bool` | zajęcie anulowane (patrz workery) | -| `Korekty` | `SubTable` | korekty zajęcia | -| `OpisPrzelewu` | `string` | tytuł przelewu | - -**Limity i kwoty — na zapisie `ZajęcieKomorniczeHistoria` (`Last`):** kwota do potrącenia, limity -procentowe i kwotowe, zawieszenie spłaty, priorytet, ustawienia potrąceń z zasiłków/świadczeń (zmiana -„od daty" = nowy zapis historii). - -**Workery (zagnieżdżone w `ZajęcieKomornicze`):** ctor **bezparametrowy**, parametr przez właściwość `Zajęcie`. - -| Worker | Metoda | Wzorzec użycia | -|---|---|---| -| `Soneta.Kadry.ZajęcieKomornicze.AnulujWorker` | `Anuluj` | `new ZajęcieKomornicze.AnulujWorker { Zajęcie = zaj }.Anuluj()` | -| `Soneta.Kadry.ZajęcieKomornicze.PrzywrócWorker` | `Przywróć` | `new ZajęcieKomornicze.PrzywrócWorker { Zajęcie = zaj }.Przywróć()` | - -**Pułapki:** -- **Pole `Priorytet` NIE istnieje** na `ZajęcieKomornicze` (sprostowanie). **Alimentacyjne vs - niealimentacyjne** rozstrzyga konfiguracja: wskazana `DefinicjaElementu` (`RodzajZrodla == - ZajęcieKomornicze`) i parametry zapisu historii (limity), nie osobny typ klasy — to **jedna klasa** - `ZajęcieKomornicze`. -- `Anulowane` jest **tylko-do-odczytu** (brak publicznego settera) — anuluj **workerem** `AnulujWorker`. -- `Rozliczenie.Odbiorca` jest **interfejsem** `IPodmiotKasowy` — wskaż istniejący podmiot (zwykle - `Kontrahent`-komornik); nie twórz odbiorcy „w locie" w tym scenariuszu. -- Faktyczne **kwoty potrącenia (`Splacono`, `Pozostało`) wyliczają się przy naliczeniu wypłaty** — po - samym dodaniu zajęcia są zerowe/wyjściowe. Pełne rozliczenie wymaga naliczonej wypłaty (patrz sekcja - „niewykonalne publicznym API bez naliczenia"). -- Anulowanie/przywracanie realizuj **workerami** (`AnulujWorker`/`PrzywrócWorker`), nie ręcznym - ustawianiem `Anulowane` — workery dbają o storna i spójność rozliczenia. -- Tabela operacyjna — przegląd poprzeczny z filtrem (safe-code §6.3). - -**Snippet:** - -```csharp -var kadry = session.GetKadry(); -var pracownik = kadry.Pracownicy.WgKodu["006"]; -// Element zajęcia — definicja o RodzajZrodla == ZajęcieKomornicze (nie zwykłe potrącenie-Dodatek): -var elementZajecia = session.GetPlace().DefElementow.Cast() - .First(d => d.RodzajZrodla == RodzajŹródłaWypłaty.ZajęcieKomornicze); -var komornik = session.GetCRM().Kontrahenci.WgKodu["KOMORNIK1"]; // wierzyciel (IPodmiotKasowy) - -using (var t = session.Logout(editMode: true)) -{ - var zajecie = new ZajęcieKomornicze(pracownik); // ctor publiczny - kadry.ZajKomornicze.AddRow(zajecie); - - zajecie.Rodzaj = RodzajeZajęciaWynagrodzenia.KwotaMiesięczna; - zajecie.Element = elementZajecia; // wymagany (RodzajZrodla == ZajęcieKomornicze) - zajecie.NumerSprawy = "KM 123/2026"; - zajecie.Data = new Date(2026, 1, 1); - zajecie.Rozliczenie.Odbiorca = komornik; // wierzyciel - zajecie.Rozliczenie.RachunekOdbiorcy = komornik.Rachunki.WgKodu["GŁÓWNY"]; - - t.Commit(); -} -session.Save(); - -// Anulowanie zajęcia (worker bezparametrowy + property Zajęcie, nie ręczna flaga): -using (var t = session.Logout(editMode: true)) -{ - var zaj = pracownik.ZajęciaKomornicze.First(); - new ZajęcieKomornicze.AnulujWorker { Zajęcie = zaj }.Anuluj(); - t.CommitUI(); -} -session.Save(); -``` - ---- - -### C5 — Operacje seryjne na dodatkach (moduł Przeszeregowania) (★) - -**Cel:** dodać / zmienić / zakończyć dodatek (oraz zmienić stawkę) dla **grupy pracowników** jedną -operacją. Realizuje to moduł **`Soneta.Przeszeregowania.PrzeszeregowaniaModule`**. Dokumentem zbiorczym -jest `Przeszeregowanie` (tabela `Przeszeregowania`, root) z pozycjami `ElementPrzeszeregowania` -(tabela `ElementyPrzeszer`, child). Pracownik widzi swoje pozycje przez -`pracownik.ElementyPrzeszeregowania`. - -**Workery operacyjne** — ctor **bezparametrowy**, parametry przez właściwości `Pars` (typu `Params`, -ctor `(Context)`) i `Pracownicy: Pracownik[]`. Uruchamiaj w transakcji `CommitUI`. **Uwaga:** workery -te w bezgłowym hoście testowym (bez operatora/kontekstu UI) rzucają `NullReferenceException` — wymagają -realnego środowiska aplikacji. - -| Worker | Metoda | Params (publiczne pola) | Działanie | -|---|---|---|---| -| `Soneta.Przeszeregowania.NowyDodatekWorker` | `NowyDodatek` | `Definicja: DefinicjaElementu, Podstawa: Currency, Procent: Percent` | wypłata/nadanie nowego dodatku grupie | -| `Soneta.Przeszeregowania.ZmianaDodatkuWorker` | `ZmianaDodatku` | `Definicja, Podstawa, ZmianaPodstawy: Currency, ProcentowaZmianaPodstawy: Percent, Procent, ZmianaProcentu: Percent, DataStawki: Date, PodstawaPrecyzja, PodstawaSposob` | zmiana parametrów istniejącego dodatku | -| `Soneta.Przeszeregowania.ZakończDodatekWorker` | `ZakończDodatek` | `Definicja: DefinicjaElementu` | zakończenie wypłaty dodatku | -| `Soneta.Przeszeregowania.DodajZmienDodatekWorker` | `DodajZmienDodatek` | `Params` (dodanie lub zmiana łącznie) | dodanie albo zmiana dodatku | -| `Soneta.Przeszeregowania.DodajNagrodęWorker` | (nagroda) | — | seryjne nagrody | -| `Soneta.Przeszeregowania.ZmianaStawkiWorker` | `ZmianaStawki` | — | seryjna zmiana stawki zaszeregowania | - -**Dokument `Przeszeregowanie` (alternatywa: zbuduj dokument i wykonaj).** Tworzenie: `new -Przeszeregowanie()` + `session.GetPrzeszeregowania().Przeszeregowania.AddRow(doc)` (kolekcja **nie ma** -`AddNew` — to standardowy `GuidedRow` root z publicznym ctorem bezparametrowym). - -| Pole | Typ | -|---|---| -| `Data` | `Soneta.Types.Date` (data przeszeregowania) | -| `DataWykonania` | `Soneta.Types.Date` | -| `Nazwa` | `string` | -| `Realizacja` | `Soneta.Przeszeregowania.RealizacjaPrzeszeregowania` (stan) | -| `Pracownicy` | `ICollection` | -| `Elementy` | `SubTable` | -| `ZarzadzaneWnioskiem` | `bool` | - -`ElementPrzeszeregowania` (child) niesie zmianę per pracownik: `Definicja: DefinicjaElementu`, -`Kwota`/`ZmianaKwoty`/`ProcentowaZmianaKwoty`, `Procent`/`ZmianaProcentu`, `Grupa: GrupaZaszeregowania`, -`Krotnosc`/`ZmianaKrotnosci`, `RodzajPrzeszergowania`, `Pracownik`, `PracHistoria`. - -**Wykonanie dokumentu:** `Soneta.Przeszeregowania.Przeszeregowanie.WykonajWorker` (metoda `Wykonaj`, -`Params { Wykonaj: bool }`) — materializuje dokument na danych pracowników (tworzy/zmienia dodatki). -`ElementPrzeszeregowania.Wykonaj(Log)` realizuje pojedynczą pozycję. - -**Pułapki:** -- To **operacja seryjna na danych operacyjnych** — trzymaj transakcje krótkie, duże grupy dziel na paczki - (safe-code §13.1). Workery przyjmują tablicę pracowników — przekaż przefiltrowaną listę (po stronie - serwera, safe-code §6). -- Workery `NowyDodatek`/`ZmianaDodatku`/`ZakończDodatek` operują na **definicji elementu** (`Definicja`), - więc wybór właściwej `DefinicjaElementu` jest kluczowy (po nazwie / `RodzajZrodla == Dodatek`). -- Sam dokument `Przeszeregowanie` **nie zmienia danych** dopóki nie zostanie wykonany (`WykonajWorker`); - do tego momentu to plan. Po `Wykonaj` zmiany trafiają w dodatki/etat pracowników. -- Indywidualne (jednostkowe) odpowiedniki to workery z C2/C1 na pojedynczym pracowniku - (`Pracownik.DodajDodatekWorker`/`ZmieńDodatekWorker`/`ZabierzDodatekWorker`); moduł Przeszeregowania - jest dla **grupy**. - -**Snippet (operacja seryjna — nowy dodatek dla grupy):** - -```csharp -var kadry = session.GetKadry(); -var def = session.GetPlace().DefElementow.WgNazwy["Premia"]; - -// Grupa pracowników — filtr serwerowy (np. po wydziale), nie pełny skan: -Pracownik[] grupa = kadry.Pracownicy[(Pracownik p) => p.Last.Etat.Okres.Contains(Date.Today)] - .Cast().ToArray(); - -var context = login.CreateEmptyContext().Clone(session); - -using (var t = session.Logout(editMode: true)) -{ - var par = new NowyDodatekWorker.Params(context) // Params: ctor (Context) - { - Definicja = def, - Podstawa = (Currency)300m, - }; - // ctor bezparametrowy; parametry przez właściwości Pars/Pracownicy: - new NowyDodatekWorker { Pars = par, Pracownicy = grupa }.NowyDodatek(); - t.CommitUI(); -} -session.Save(); -``` - ---- - -### C6 — Świadczenia socjalne (ZFŚS) i ich rozliczenie (★) - -**Cel:** przyznać pracownikowi świadczenie socjalne z ZFŚS (zapomoga, dopłata do wypoczynku, paczka) -i ustawić jego rozliczenie płacowe (element, kwota, okres). - -**Klasa i model:** `Soneta.Kadry.SwiadczSocjalne` — `GuidedRow` root, tabela `SwiadczeniaSoc`. Child -pracownika: `pracownik.Swiadczenia: SubTable`. **Konstruktor publiczny:** -`new SwiadczSocjalne(pracownik)`. - -**Pola `SwiadczSocjalne`:** - -| Pole | Typ | Uwaga | -|---|---|---| -| `Pracownik` | `Soneta.Kadry.Pracownik` | właściciel | -| `Definicja` | `Soneta.Kadry.DefinicjaŚwiadczeniaSocjalnego` | rodzaj świadczenia (słownik `DefSwiadczSocjal`) | -| `Data` | `Soneta.Types.Date` | data przyznania | -| `Nazwa` | `string` | nazwa | -| `Opis` | `Soneta.Business.MemoText` | opis | -| `Rozliczenie` | `Soneta.Kadry.RozliczenieSwiadczenia` (subrow) | dane rozliczeniowe (poniżej) | - -**Subrow `Rozliczenie` (`RozliczenieSwiadczenia`):** - -| Pole | Typ | Uwaga | -|---|---|---| -| `Rozliczenie.Element` | `Soneta.Place.DefinicjaElementu` | element płacowy do rozliczenia świadczenia | -| `Rozliczenie.Kwota` | `Soneta.Types.Currency` | kwota świadczenia | -| `Rozliczenie.Okres` | `Soneta.Types.FromTo` | okres rozliczenia | -| `Rozliczenie.Data` | `Soneta.Types.Date` | data rozliczenia | -| `Rozliczenie.Rozliczone` | `bool` | czy rozliczone (po naliczeniu wypłaty) | - -**Definicja (`DefinicjaŚwiadczeniaSocjalnego`, słownik `DefSwiadczSocjal`):** `Nazwa: string`, -`Element: DefinicjaElementu` (domyślny element rozliczenia), `Kwota: Currency` (domyślna kwota). Z niej -dziedziczy świadczenie domyślny element i kwotę. - -**Worker seryjny:** `Soneta.Kadry.SwiadczSocjalne.DodajŚwiadczenieWorker` (metoda `DodajŚwiadczenie`) — -ctor **bezparametrowy**, parametry przez właściwości `Pars` i `Pracownicy: Pracownik[]`; `Params` ma -ctor `(Context)`: `Params { Definicja: DefinicjaŚwiadczeniaSocjalnego, DataPrzyznania: Date, Kwota: -Currency, Element: DefinicjaElementu, DataRozliczenia: Date }` — nadaje świadczenie grupie (menu -*Operacje seryjne / Dodaj świadczenia socjalne*). Wzorzec: -`new DodajŚwiadczenieWorker { Pars = new …Params(ctx){…}, Pracownicy = tab }.DodajŚwiadczenie()`. - -**Pułapki:** -- `Definicja` (świadczenia) pobierana ze słownika `DefSwiadczSocjal`; jej `Element`/`Kwota` są domyślne — - na konkretnym świadczeniu nadpisujesz przez `Rozliczenie.Element`/`Rozliczenie.Kwota`. -- **Faktyczne rozliczenie (wypłata świadczenia, `Rozliczenie.Rozliczone == true`) następuje przy - naliczeniu wypłaty** — samo dodanie świadczenia tworzy tylko zlecenie rozliczenia. -- Dla grupy używaj `DodajŚwiadczenieWorker`; pojedynczo — `new SwiadczSocjalne(pracownik)` + `AddRow`. - -**Snippet:** - -```csharp -var kadry = session.GetKadry(); -var pracownik = kadry.Pracownicy.WgKodu["006"]; -var defSwiadcz = kadry.DefSwiadczSocjal.WgNazwy["Dopłata do wypoczynku"]; -var element = session.GetPlace().DefElementow.WgNazwy["Świadczenie socjalne"]; - -using (var t = session.Logout(editMode: true)) -{ - var sw = new SwiadczSocjalne(pracownik); - kadry.SwiadczeniaSoc.AddRow(sw); - - sw.Definicja = defSwiadcz; - sw.Data = new Date(2026, 6, 1); - sw.Rozliczenie.Element = element; // element płacowy rozliczenia - sw.Rozliczenie.Kwota = (Currency)1000m; - sw.Rozliczenie.Okres = FromTo.Month(new YearMonth(2026, 6)); - - t.Commit(); -} -session.Save(); - -// Odczyt świadczeń pracownika: -foreach (SwiadczSocjalne s in pracownik.Swiadczenia) -{ - Currency kwota = s.Rozliczenie.Kwota; - bool rozliczone = s.Rozliczenie.Rozliczone; -} -``` - ---- - -### C7 — Pożyczki (KZP / ZFM) (★) - -**Cel:** zarejestrować członkostwo pracownika w funduszu pożyczkowym, udzielić pożyczki, zbudować -harmonogram rat i potrącać raty z wynagrodzenia. - -**Hierarchia obiektów (wszystkie `GuidedRow` root, childy pracownika):** -- `Soneta.Kadry.FundPozyczkowy` (tabela `FundPozyczkowe`) — **członkostwo** w funduszu; - `pracownik.FunduszePozyczkowe: SubTable`. Ctor: - `new FundPozyczkowy(pracownik, definicja)`. -- `Soneta.Kadry.Pozyczka` (tabela `Pozyczki`) — **pożyczka** udzielona w ramach funduszu; kolekcja - `fundusz.Pozyczki: SubTable`. Ctor: `new Pozyczka(fundusz)`. -- `Soneta.Kadry.RataPozyczki` (tabela `RatyPozyczek`) — **rata** harmonogramu; `pozyczka.Raty: - SubTable`. Raty pracownik widzi przez `pracownik.SplacaneRaty` - (oraz `ZyrowaneRaty` jako żyrant). Ctor: `new RataPozyczki(pozyczka)`. -- `Soneta.Kadry.DefinicjaFunduszuPozyczkowego` (słownik `DefFundPozycz`, konfiguracyjny) — zasady - funduszu (oprocentowanie, elementy płacowe wpisowego/składki/wycofania). - -**Pola `Pozyczka` (kluczowe):** - -| Pole | Typ | Uwaga | -|---|---|---| -| `Fundusz` | `Soneta.Kadry.FundPozyczkowy` | fundusz, w ramach którego udzielono | -| `Data` | `Soneta.Types.Date` | data udzielenia | -| `Kwota` | `Soneta.Types.Currency` | kwota pożyczki | -| `Element` | `Soneta.Place.DefinicjaElementu` | element wypłaty pożyczki | -| `ElementRaty` | `Soneta.Place.DefinicjaElementu` | element potrącenia raty | -| `IloscRat` | `int` | liczba rat | -| `KwotaRaty` | `Soneta.Types.Currency` | kwota raty | -| `SplatyOd` | `Soneta.Types.YearMonth` | miesiąc rozpoczęcia spłat | -| `Procent` | `Soneta.Types.Percent` | oprocentowanie | -| `Sposob` | `Soneta.Kadry.SposóbSpłatyOdsetek` | sposób spłaty odsetek | -| `AlgorytmRaty` | `Soneta.Kadry.AlgorytmRatyPożyczki` | algorytm wyliczania raty | -| `Raty` | `SubTable` | harmonogram rat | -| `Stan` | `Soneta.Kadry.StanSpłat` | enum: `NieSpłacona = 0`, `Częściowo = 1`, `Całkowicie = 2` | -| `Splacona` | `bool` | spłacona w całości | - -**Pola `RataPozyczki`:** `Pozyczka`, `Data: Date`, `Miesiąc: YearMonth`, `Kapital: Currency`, -`Odsetki: Currency`, `Element: DefinicjaElementu` (potrącenie raty), `Stan: StanSpłat`, -`Pozostaje`/`PozostajeKapitał`/`PozostajeOdsetki` (kalkulowane), `Zyrant: Pracownik`, -`Splacajacy: Pracownik`. - -**Generowanie harmonogramu (workery):** - -| Worker | Metoda | Params / sygnatura | -|---|---|---| -| `Soneta.Kadry.Pozyczka.UzgodnijRatyWorker` | `UzgodnijRaty` | ctor bezparam.; `Pars = new Params(ctx) { UzgodnijRaty = true }` (uwaga: `PrzeliczRaty` jest **tylko-do-odczytu**), `Pożyczka = pozyczka` — **buduje/przelicza harmonogram rat** wg `IloscRat`/`KwotaRaty`/`SplatyOd` | -| `Soneta.Kadry.Pozyczka.PożyczkaWorker` | `Pożyczka` | podsumowanie spłat (props: `Razem`, `Spłaty`, `Pozostaje`, `RazemOdsetki`…) | -| `Soneta.Kadry.Pozyczka.ElementWypłatyWorker` | `Pokaż` | podgląd elementu wypłaty pożyczki | - -Metody na samym `Pozyczka`: `pozyczka.UpdatePozyczka()` (przelicz), `pozyczka.Rata(YearMonth, -DefinicjaElementu)`, `pozyczka.RatyZaMiesiąc(YearMonth)`, `pozyczka.SąRaty(YearMonth)`. - -**Pułapki:** -- Ścieżka tworzenia jest **trzystopniowa**: `FundPozyczkowy(pracownik, definicja)` → `Pozyczka(fundusz)` - → harmonogram. Pożyczki **nie da się** utworzyć bez funduszu (ctor wymaga `FundPozyczkowy`). -- Harmonogram rat generuj **workerem** `UzgodnijRatyWorker` (albo `UpdatePozyczka()`), nie ręcznym - dodawaniem `RataPozyczki` — worker rozkłada kapitał/odsetki wg algorytmu. -- `Element` (wypłaty) i `ElementRaty` (potrącenia) to **różne** definicje elementów — `ElementRaty` - realizuje potrącenie raty w wypłacie. -- **Faktyczne potrącenie raty następuje przy naliczeniu wypłaty** — `Stan`/`Splacono`/`Pozostaje` - aktualizują się po naliczeniu. Samo udzielenie pożyczki ich nie zmienia. -- `DefinicjaFunduszuPozyczkowego` to słownik konfiguracyjny — pobierz istniejący wpis, nie twórz „w locie". - -**Snippet:** - -```csharp -var kadry = session.GetKadry(); -var pracownik = kadry.Pracownicy.WgKodu["006"]; -var defFunduszu = kadry.DefFundPozycz.WgNazwy["KZP"]; -var elWyplata = session.GetPlace().DefElementow.WgNazwy["Pożyczka"]; -var elRata = session.GetPlace().DefElementow.WgNazwy["Spłata pożyczki"]; - -using (var t = session.Logout(editMode: true)) -{ - // 1) Członkostwo w funduszu - var fundusz = new FundPozyczkowy(pracownik, defFunduszu); - kadry.FundPozyczkowe.AddRow(fundusz); - fundusz.Okres = new FromTo(new Date(2026, 1, 1), Date.MaxValue); - - // 2) Pożyczka w ramach funduszu - var pozyczka = new Pozyczka(fundusz); - kadry.Pozyczki.AddRow(pozyczka); - pozyczka.Data = new Date(2026, 1, 10); - pozyczka.Kwota = (Currency)12000m; - pozyczka.Element = elWyplata; - pozyczka.ElementRaty = elRata; - pozyczka.IloscRat = 12; - pozyczka.SplatyOd = new YearMonth(2026, 2); - - // 3) Harmonogram rat (worker bezparametrowy; Params: ctor (Context); PrzeliczRaty read-only) - var context = login.CreateEmptyContext().Clone(session); - var par = new Pozyczka.UzgodnijRatyWorker.Params(context) { UzgodnijRaty = true }; - new Pozyczka.UzgodnijRatyWorker { Pars = par, Pożyczka = pozyczka }.UzgodnijRaty(); - - t.CommitUI(); -} -session.Save(); - -// Odczyt harmonogramu: -foreach (FundPozyczkowy f in pracownik.FunduszePozyczkowe) - foreach (Pozyczka p in f.Pozyczki) - foreach (RataPozyczki r in p.Raty) - { - YearMonth m = r.Miesiąc; - Currency kapital = r.Kapital, odsetki = r.Odsetki; - StanSpłat stan = r.Stan; - } -``` - -## D. Nieobecności i czas pracy - -### D1 — Wprowadzanie nieobecności (★) - -**Cel:** zarejestrować nieobecność pracownika (urlop wypoczynkowy, zwolnienie chorobowe, urlop -bezpłatny, opieka itp.) za wskazany okres oraz odczytać nieobecności obowiązujące w danym przedziale dat. - -**Fakty o typie (zweryfikowane skanem DLL):** - -- **`Soneta.Kalend.Nieobecnosc` jest klasą abstrakcyjną** — tabela `Nieobecnosci` (`GuidedRow` root, - child `Pracownik`-a). `new Nieobecnosc(...)` się nie skompiluje. -- Konkretny typ do tworzenia: **`Soneta.Kalend.NieobecnośćPracownika`** (dziedziczy z `Nieobecnosc`), - z **publicznym konstruktorem `new NieobecnośćPracownika(Pracownik pracownik)`** — ctor od razu wiąże - nieobecność z pracownikiem. (Drugi konkretny typ to `KorektaNieobecności` — patrz D2.) -- Kolekcja na pracowniku: **`pracownik.Nieobecnosci: FromToSubTable`** - (uporządkowana po okresie „od–do"). -- Tabela z poziomu modułu: `session.GetKalend().Nieobecnosci`. - -**Pola i typy (`Nieobecnosc` / `NieobecnośćPracownika`):** - -| Pole | Typ | Rodzaj | Opis | -|---|---|---|---| -| `Definicja` | `Soneta.Kalend.DefinicjaNieobecnosci` | bazodanowe, **zapisywalne** | rodzaj nieobecności (słownik konfiguracyjny); decyduje o typie (urlop / zasiłek / bezpłatny) | -| `Okres` | `Soneta.Types.FromTo` | bazodanowe, **zapisywalne** | zakres dat nieobecności „od–do" | -| `OdGodziny`, `DoGodziny` | `Soneta.Types.Time` | — | godziny (nieobecności godzinowe) | -| `Norma`, `NormaNie` | `Soneta.Types.Time` | bazodanowe | normy czasowe | -| `IlośćDni` / `Dni` | `int` | kalkulowane/zapisywalne | liczba dni nieobecności | -| `Pracownik` | `Soneta.Kadry.Pracownik` | **tylko do odczytu** | właściciel (ustawiany ctorem, nie da się zmienić setterem) | -| `Zwolnienie` | `Soneta.Kalend.ZwolnienieZUS` (subrow) | bazodanowe | dane ZUS dla zwolnień chorobowych (`KodChoroby`, `Numer` ZLA, `PonownieUstalPodstawe`…) | -| `Urlop`, `Macierzynski`, `Wychowawczy`, `Okolicznosciowy` | subrowy | bazodanowe | szczegóły poszczególnych typów urlopów | -| `Korygowana` | `bool` | **tylko do odczytu** | czy nieobecność jest korektą (patrz D2) | - -**Dostęp do definicji nieobecności (`DefNieobecnosci`):** - -- `session.GetKalend().DefNieobecnosci.WgNazwy[string]` — pobranie po nazwie, np. - `WgNazwy["Urlop wypoczynkowy"]`, `WgNazwy["Zwolnienie chorobowe"]`, - `WgNazwy["Urlop bezpłatny (art 174 kp)"]`. Nazwy muszą **dokładnie** odpowiadać słownikowi danej bazy - (w Demo nie ma wpisu „Urlop bezpłatny" — jest „Urlop bezpłatny (art 174 kp)"); `WgNazwy[...]` dla - nieistniejącej nazwy zwraca `null`. -- `session.GetKalend().DefNieobecnosci[string]` (indeksator domyślny po nazwie) — równoważne. -- `DefinicjaNieobecnosci` ma pola `Nazwa: string`, `Kod: string`, `Typ: TypNieobecnosci`. - -**Wyszukiwanie po dacie/okresie:** `pracownik.Nieobecnosci.GetIntersectedRows(FromTo)` zwraca -`IList` nieobecności przecinających podany przedział. - -**Snippet:** - -```csharp -var kalend = session.GetKalend(); -var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; -// Nieobecność BEZ limitu (np. urlop bezpłatny) można wprowadzić wprost. Dla nieobecności -// LIMITOWANYCH (urlop wypoczynkowy) najpierw musi istnieć naliczony limit — patrz pułapki i D7. -var defNieob = kalend.DefNieobecnosci.WgNazwy["Urlop bezpłatny (art 174 kp)"]; - -using (var t = session.Logout(editMode: true)) -{ - // typ konkretny; ctor wiąże nieobecność z pracownikiem - var nieobecnosc = session.AddRow(new NieobecnośćPracownika(pracownik)); - nieobecnosc.Definicja = defNieob; // rodzaj nieobecności - nieobecnosc.Okres = new FromTo(new Date(2026, 7, 1), new Date(2026, 7, 5)); - t.Commit(); -} -session.Save(); - -// Odczyt nieobecności przecinających lipiec 2026: -var lipiec = new FromTo(new Date(2026, 7, 1), new Date(2026, 7, 31)); -foreach (Nieobecnosc n in pracownik.Nieobecnosci.GetIntersectedRows(lipiec)) -{ - // n.Definicja.Nazwa, n.Okres, n.Dni -} -``` - -**Pułapki:** -- **Nie** rób `new Nieobecnosc(...)` — typ jest abstrakcyjny. Używaj `new NieobecnośćPracownika(pracownik)`. -- **Nieobecności limitowane wymagają istniejącego limitu.** Ustawienie `Okres` dla nieobecności - powiązanej z limitem (np. „Urlop wypoczynkowy") synchronicznie przelicza limit i rzuca - `Soneta.Kalend.DefinicjaLimitu.LimitNotFoundException`, gdy pracownik nie ma naliczonego limitu na - dany rok. Dlatego: albo najpierw nalicz limit (patrz D7), albo użyj nieobecności bez limitu - (np. „Urlop bezpłatny (art 174 kp)") — jak w snippetcie powyżej. -- `Definicja` jest **wymagana** — bez niej nieobecność nie zostanie poprawnie naliczona/zapisana. - Pobieraj istniejący wpis słownika przez `DefNieobecnosci.WgNazwy[...]`, nie twórz „w locie". -- `Pracownik` jest **tylko do odczytu** — relację ustawia konstruktor, nie da się jej później zmienić. -- Tabela `Nieobecnosci` jest **operacyjna guided** — przy przeglądaniu poprzecznym (po wszystkich - pracownikach) filtruj zakresem czasowym (safe-code §6.3). W zakresie jednego pracownika korzystaj - z `pracownik.Nieobecnosci` i `GetIntersectedRows`. -- Nakładające się nieobecności i niepoprawne okresy wychwytują weryfikatory przy `Save()` - (`RowException`) — obsłuż wyjątek. -- Pełna transakcja w `session.Logout(editMode: true)`; brak `Commit()` = rollback przy `Dispose()`. - ---- - -### D2 — Korygowanie nieobecności już wypłaconych (★) - -**Cel:** poprawić nieobecność, która została już rozliczona w wypłacie — zmienić jej okres lub typ -(definicję) i/lub wymusić ponowne ustalenie podstawy naliczania zasiłku. enova rozróżnia dwie ścieżki: -(a) **modyfikacja istniejącej nieobecności** + ponowne ustalenie podstawy, (b) **korekta** jako odrębny -rekord typu `KorektaNieobecności`. - -**Fakty o typie (zweryfikowane skanem DLL):** - -- Pola `Definicja: DefinicjaNieobecnosci` i `Okres: FromTo` na `Nieobecnosc` są **zapisywalne** - publicznie — można je zmienić na istniejącym rekordzie. -- `Nieobecnosc.Korygowana: bool` i `Nieobecnosc.Pracownik` są **tylko do odczytu**. -- Subrow `Zwolnienie: Soneta.Kalend.ZwolnienieZUS` posiada flagę `PonownieUstalPodstawe: bool` - oraz **publiczną metodę `SetPonownieUstalPodstawe(bool)`** — to ona steruje przeliczeniem podstawy - zasiłku przy kolejnym naliczeniu wypłaty. -- Worker (czynność menu, `DataType = Nieobecnosc`): klasa - **`Soneta.Kalend.Nieobecnosc.UstalPonowniePodstawęNaliczaniaWorker`** — czynność - „Ustal ponownie podstawę naliczania". Worker: - - ma publiczny bezparametrowy ctor; - - przyjmuje kontekst przez settowalną property `[Context] public Params Nieobecność`; - - klasa `…Worker.Params : ContextBase` ma **publiczny ctor `Params(Context context)`**, który czyta - nieobecność z `context[typeof(Nieobecnosc)]`, oraz settowalną property `UstalPodstawę: bool`; - - metoda `public void PonownieUstalPodstawę()` jest jego akcją; - - `static bool IsEnabledPonownieUstalPodstawę(Nieobecnosc)` mówi, kiedy czynność jest aktywna - (dotyczy zwolnień ZUS i urlopów macierzyńskich: `Zwolnienie.IsZUS || Macierzynski.IsMacierzyński`, - przy braku `BlokadaOkresu`). -- Drugi konkretny typ nieobecności: **`Soneta.Kalend.KorektaNieobecności`** (dziedziczy `Nieobecnosc`), - z **publicznym ctor `new KorektaNieobecności(NieobecnośćPracownika nieobecność)`** — tworzy rekord - korygujący wskazaną nieobecność. Ma zapisywalne `Definicja`, `Okres`, `IlośćDni`, - `RozliczenieWDniu`, `RozliczenieData`, a kolekcje `ElementyKorygowane`/`ElementyKorygowaneStorno` - są tylko do odczytu (wyliczane). - -**Wariant A — zmiana okresu/typu + ponowne ustalenie podstawy (modyfikacja istniejącego rekordu):** - -```csharp -var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; -var okresStary = new FromTo(new Date(2026, 3, 2), new Date(2026, 3, 10)); - -// odszukaj istniejącą (już rozliczoną) nieobecność po przecięciu z okresem -var nieobecnosc = (Nieobecnosc)pracownik.Nieobecnosci.GetIntersectedRows(okresStary)[0]; - -using (var t = session.Logout(editMode: true)) -{ - nieobecnosc.Okres = new FromTo(new Date(2026, 3, 2), new Date(2026, 3, 12)); // wydłużenie okresu - // dla zwolnień ZUS — wymuś ponowne ustalenie podstawy przy najbliższym naliczeniu wypłaty: - nieobecnosc.Zwolnienie.SetPonownieUstalPodstawe(true); - t.Commit(); -} -session.Save(); -``` - -**Wariant B — czynność „Ustal ponownie podstawę naliczania" przez worker (kontekst):** - -```csharp -var worker = new Nieobecnosc.UstalPonowniePodstawęNaliczaniaWorker(); -var ctx = Context.Empty.Clone(session); -ctx[typeof(Nieobecnosc)] = nieobecnosc; // worker czyta nieobecność z kontekstu -worker.Nieobecność = new Nieobecnosc.UstalPonowniePodstawęNaliczaniaWorker.Params(ctx) -{ - UstalPodstawę = true -}; -worker.PonownieUstalPodstawę(); // wykonuje własną transakcję + Commit -session.Save(); -``` - -**Wariant C — odrębny rekord korekty (`KorektaNieobecności`):** - -```csharp -using (var t = session.Logout(editMode: true)) -{ - var nPrac = (NieobecnośćPracownika)nieobecnosc; // korekta dotyczy NieobecnośćPracownika - var korekta = session.AddRow(new KorektaNieobecności(nPrac)); - korekta.Definicja = nPrac.Definicja; - // Okres korekty MUSI być podzbiorem okresu korygowanej nieobecności (tu: 2..10): - korekta.Okres = new FromTo(new Date(2026, 3, 3), new Date(2026, 3, 8)); - t.Commit(); -} -session.Save(); -// Po zapisie korygowana nieobecność ma flagę Korygowana == true. -``` - -**Pułapki:** -- **Faktyczne** przeliczenie wartości zasiłku NIE następuje w momencie ustawienia flagi/wywołania - workera — flaga `PonownieUstalPodstawe` jest odczytywana dopiero przy **ponownym naliczeniu wypłaty** - (mechanizm `PodstawaZasilku`). Sam test korekty rekordu nieobecności (Demo, rollback) zweryfikuje - zmianę `Okres`/`Definicja`/flagi, ale **nie zweryfikuje przeliczonych kwot wypłaty** bez pełnego - scenariusza naliczenia listy płac (patrz sekcja „funkcjonalności niewykonalne"). -- `IsEnabledPonownieUstalPodstawę` ogranicza czynność do zwolnień ZUS / macierzyńskich — dla zwykłego - urlopu wypoczynkowego worker nie ma zastosowania; tam korektę robisz przez zmianę `Okres`/`Definicja` - albo rekord `KorektaNieobecności`. -- **Okres korekty (`KorektaNieobecności.Okres`) musi być podzbiorem okresu korygowanej nieobecności** — - wyjście poza ten zakres rzuca `Nieobecnosc.KorygowanyOkresException`. -- Dla nieobecności bez skutków płacowych (np. urlop bezpłatny) `KorektaNieobecności` **nie pojawia się - jako osobny wiersz** w `pracownik.Nieobecnosci` — obserwowalnym efektem jest flaga `Korygowana == true` - na nieobecności pierwotnej. -- Korekta zmienia dane operacyjne powiązane z wypłatą — trzymaj transakcję krótką i obsłuż - `RowConflictException` / `RowException` z `Save()` (safe-code §4, §13.1). -- Worker wykonuje własną transakcję (`Session.Logout(true)` + `Commit`) — nie zagnieżdżaj go w innej - otwartej transakcji edycyjnej. - ---- - -### D7 — Analiza limitów urlopowych (★) - -**Cel:** odczytać limit nieobecności (np. urlop wypoczynkowy) pracownika za dany rok — ile przysługuje, -ile wykorzystano, ile pozostało. Limity **nie są tworzone ręcznie** — powstają przez naliczanie. - -**Fakty o typie (zweryfikowane skanem DLL):** - -- **`Soneta.Kalend.LimitNieobecnosci`** — tabela `LimNieobecnosci`, `GuidedRow` **child** pracownika - (relacja przez pole `Pracownik`). Instancje powstają wyłącznie przez naliczanie — **nie twórz ich - konstruktorem**. -- Kolekcja na pracowniku: **`pracownik.Limity: SubTable`** - (nazwa kolekcji to `Limity`, nie „LimityNieobecnosci"). -- Tabela z poziomu modułu: `session.GetKalend().LimNieobecnosci`. - -**Pola i typy (`LimitNieobecnosci`) — odczyt:** - -| Pole | Typ | Rodzaj | Opis | -|---|---|---|---| -| `Definicja` | `Soneta.Kalend.DefinicjaLimitu` | bazodanowe | rodzaj limitu (urlop wypoczynkowy itd.) | -| `Okres` | `Soneta.Types.FromTo` | bazodanowe | okres limitu (zwykle rok) | -| `OkresWażności` | `Soneta.Types.FromTo` | kalkulowane | okres ważności limitu | -| `Limit` | `int` | bazodanowe | limit (dni) wynikający z kodeksu pracy | -| `LimitDni` | `int` | kalkulowane | limit w dniach | -| `LimitGodz` | `Soneta.Types.Time` | bazodanowe | limit w godzinach | -| `Razem` / `RazemGodz` | `int` / `Time` | kalkulowane | łączny przysługujący (limit + przeniesienia + zmiany) | -| `Wykorzystane` / `WykorzystaneGodz` | `int` / `Time` | bazodanowe | wykorzystane dni/godziny | -| `Pozostalo` | `int` | kalkulowane | pozostało (dni, int) | -| `PozostaloDni` | `double` | kalkulowane | pozostało dni (z częścią ułamkową) | -| `PozostaloGodz` | `Soneta.Types.Time` | kalkulowane | pozostało godzin | -| `ZaleglyDni` / `ZaleglyGodz` | `double` / `Time` | kalkulowane | zaległy z poprzednich okresów | -| `Przeniesienie` / `PrzeniesienieDni` | `int` / `double` | kalkulowane | przeniesione z poprzedniego roku | -| `Korekta`, `Zmiana` | `int` | bazodanowe | korekty/zmiany limitu | -| `Pracownik` | `Soneta.Kadry.Pracownik` | bazodanowe (guided-parent), **read-only** | właściciel | - -> **Wykorzystany = `Razem - Pozostalo`** (lub bezpośrednio pole `Wykorzystane`). „Przysługujący" to -> `Razem` (limit kodeksowy + przeniesienia + zmiany), a nie samo `Limit`. - -**Dostęp do definicji limitów (`DefinicjeLimitow`):** - -- `session.GetKalend().DefinicjeLimitow.WgNazwy[string]` — np. `WgNazwy["Urlop wypoczynkowy"]`. -- Skróty typowane (property zwracające `DefinicjaLimitu`): `DefinicjeLimitow.UrlopWypoczynkowy`, - `.UrlopDodatkowy`, `.OpiekaNadZdrowym`, `.UrlopOpiekunczy`, `.ZwolnienieOdPracySilaWyzsza` itd. -- `DefinicjaLimitu` ma pola `Nazwa: string`, `Typ: TypLimitu`. - -**Naliczanie limitu (by mógł istnieć do odczytu) — `Soneta.Kalend.NaliczanieLimitow`:** - -- Klasa z **publicznym bezparametrowym ctor**; settowalne property: - - `Pars: NaliczanieLimitow.Params` (set), - - `Pracownicy: ICollection` (set) **albo** `PracownicyIdx: Pracownik[]` (set). -- Klasa `NaliczanieLimitow.Params : ContextBase` ma **publiczny ctor `Params(Context context)`** - oraz settowalne: `Definicja: DefinicjaLimitu`, `Okres: FromTo`, `KopiujKorekty: bool`, - `ZapisPerPracownik: bool`. -- Metoda **`public void DodajLimit()`** — nalicza limit (zapisuje rekordy `LimitNieobecnosci`). - (Jest też `DodajLimitUrlopowy()`.) - -**Snippet — naliczenie + odczyt:** - -```csharp -var kalend = session.GetKalend(); -var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; -var defUrlop = kalend.DefinicjeLimitow.WgNazwy["Urlop wypoczynkowy"]; // lub DefinicjeLimitow.UrlopWypoczynkowy -var rok = FromTo.Year(new Date(2026, 1, 1)); - -using (var t = session.Logout(editMode: true)) -{ - var naliczanie = new NaliczanieLimitow - { - Pars = new NaliczanieLimitow.Params(Context.Empty.Clone(session)) - { - Definicja = defUrlop, - Okres = rok, - KopiujKorekty = true - }, - Pracownicy = new Pracownik[] { pracownik } - }; - naliczanie.DodajLimit(); // tworzy/aktualizuje LimitNieobecnosci - t.Commit(); -} -session.Save(); - -// Odczyt limitu urlopu wypoczynkowego za rok 2026. -// UWAGA: filtr serwerowy obejmuje TYLKO pola bazodanowe i prostych porównań — Okres (FromTo) -// NIE da się porównać serwerowo (==), więc filtrujemy serwerowo po Definicja, a rok w pamięci: -var lim = pracownik.Limity[(LimitNieobecnosci l) => l.Definicja == defUrlop] - .Cast() - .FirstOrDefault(l => l.Okres.From == rok.From); -if (lim != null) -{ - int przysluguje = lim.Razem; // przysługujący (limit + przeniesienia + zmiany) - int pozostalo = lim.Pozostalo; // pozostało - int wykorzystany = przysluguje - pozostalo; // == lim.Wykorzystane - // lim.PozostaloDni, lim.PozostaloGodz, lim.ZaleglyDni -} -``` - -**Pułapki:** -- **Nie** twórz `new LimitNieobecnosci(...)` — limit powstaje przez naliczanie (`DodajLimit`). W bazie - Demo limit dla danego roku może jeszcze nie istnieć — w teście trzeba go **najpierw naliczyć**. -- Kolekcja na pracowniku to `pracownik.Limity` (nie `LimityNieobecnosci`). -- **Nie porównuj `Okres` (FromTo) w filtrze serwerowym** — `l.Okres == rok` rzuca `ArgumentException` - („pole nieznalezione"). Filtruj serwerowo po `Definicja`, a okres/rok porównaj w pamięci - (`.FirstOrDefault(l => l.Okres.From == rok.From)`). -- `Razem` może wynosić `0` dla pracowników bez danych napędzających wymiar urlopu (staż, data - urodzenia) — asercje opieraj na spójności (`Wykorzystane == Razem - Pozostalo`, `Razem >= 0`), - a nie na założeniu `Razem > 0`. -- `Pracownik` na limicie jest read-only (relacja guided) — naliczanie samo wiąże rekord z pracownikiem. -- Filtruj limity serwerowo po `Definicja` i `Okres` (`pracownik.Limity[condition]`), nie iteruj całości - z `if` w pamięci (safe-code §6.1). Tabela `LimNieobecnosci` jest operacyjna guided. -- `Context.Empty.Clone(session)` daje kontekst związany z bieżącą sesją — wymagany przez ctor - `NaliczanieLimitow.Params(Context)`. -- Naliczanie modyfikuje dane operacyjne — w transakcji edycyjnej, krótko, z obsługą wyjątków z `Save()`. - -### D3 — Import e-ZLA z PUE ZUS (zwolnienia lekarskie) - -**Cel:** zaewidencjonować w systemie zwolnienie lekarskie pobrane z PUE ZUS (e-ZLA). Sam **import to -operacja sieciowa** (komunikacja z PUE ZUS) — w kodzie biznesowym/teście dokumentujemy **model danych** -nieobecności chorobowej i jej dane ZUS, a nie samo połączenie z bramką PUE. - -**Fakty o typie (zweryfikowane skanem DLL):** - -- Zwolnienie chorobowe to `Soneta.Kalend.NieobecnośćPracownika` (typ konkretny z D1) z `Definicja` - wskazującą na rodzaj zasiłkowy (np. „Zwolnienie chorobowe"). -- Dane ZUS zwolnienia leżą w subrowie **`Nieobecnosc.Zwolnienie: Soneta.Kalend.ZwolnienieZUS`** - (bazodanowy subrow na rekordzie nieobecności). -- Dane samego dokumentu ZLA leżą w subrowie **`Nieobecnosc.ZLA: Soneta.Kalend.ZLA`** - (`ZLA.Data: Date`, `ZLA.Wersja: WersjaZLA`, `ZLA.Zrodlo: MemoText`). - -**Pola i typy (`Nieobecnosc.Zwolnienie: ZwolnienieZUS`) — zapisywalne, bazodanowe:** - -| Pole | Typ | Opis | -|---|---|---| -| `Numer` | `string` | numer dokumentu ZLA (pole tekstowe — **maks. 9 znaków**) | -| `KodChoroby` | `string` | kod literowy choroby (A, B, C, D, …) | -| `Przyczyna` | `Soneta.Kalend.PrzyczynaZwolnienia` | przyczyna niezdolności do pracy | -| `Kwarantanna` | `Soneta.Kalend.ZwolnienieKwarantanna` | kwarantanna/izolacja | -| `LeczenieSzpitalne` | `bool` | pobyt w szpitalu | -| `ZwolnienieWystawione` | `Soneta.Types.Date` | data wystawienia ZLA | -| `ZwolnienieDostarczone` | `Soneta.Types.Date` | data dostarczenia | -| `PomniejszajZasilek` | `bool` | obniżenie zasiłku | -| `PonownieUstalPodstawe` | `bool` | wymuszenie przeliczenia podstawy (patrz D2/D6) | - -**Pola i typy (`Nieobecnosc.ZLA: ZLA`):** `Data: Date`, `Wersja: WersjaZLA`, `Zrodlo: MemoText`. - -**Snippet — ręczne odwzorowanie e-ZLA jako nieobecności chorobowej (bez sieci):** - -```csharp -var kalend = session.GetKalend(); -var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; -var defChor = kalend.DefNieobecnosci.WgNazwy["Zwolnienie chorobowe"]; - -using (var t = session.Logout(editMode: true)) -{ - var nieob = session.AddRow(new NieobecnośćPracownika(pracownik)); - nieob.Definicja = defChor; - nieob.Okres = new FromTo(new Date(2026, 5, 4), new Date(2026, 5, 10)); - // dane ZUS z e-ZLA (subrow Zwolnienie): - nieob.Zwolnienie.Numer = "ZLA000001"; // pole Numer ma limit 9 znaków - nieob.Zwolnienie.KodChoroby = "A"; - t.Commit(); -} -session.Save(); -``` - -**Pułapki:** -- **Sam import e-ZLA z PUE wymaga sieci** (uwierzytelnienie + bramka ZUS) — nie da się go odtworzyć - w teście jednostkowym na bazie Demo; testuj wyłącznie **odwzorowanie modelu danych** (subrow `Zwolnienie`). -- `Zwolnienie` i `ZLA` to subrowy — nie tworzysz ich osobno, są częścią rekordu `Nieobecnosc`; ustawiasz - ich pola po utworzeniu nieobecności. -- Definicja zasiłkowa musi istnieć w słowniku bazy (`DefNieobecnosci.WgNazwy[...]` ≠ `null`). -- **Faktyczne kwoty zasiłku** liczą się dopiero przy naliczeniu wypłaty — patrz uwaga przy D2. - ---- - -### D4 — Generowanie deklaracji Z-3 / Z-3a dla nieobecności chorobowej - -**Cel:** wygenerować zaświadczenie płatnika składek **Z-3** (pracownik etatowy) lub **Z-3a** (umowy/inni -ubezpieczeni) dla konkretnej nieobecności zasiłkowej. - -**Fakty o typie (zweryfikowane skanem DLL):** - -- Worker (czynność na `Nieobecnosc`): **`Soneta.Deklaracje.ZUS.ZUSZ3.Z3Worker`** — akcja - „Generuj deklarację Z-3", metoda `public object UtworzDeklaracjeZ3()`. -- Analogicznie **`Soneta.Deklaracje.ZUS.ZUSZ3.Z3aWorker`** — akcja „Generuj deklarację Z-3a", - metoda `public object UtworzDeklaracjeZ3a()`. -- Oba workery przyjmują przez `[Context]`: - - `KeduContext: DeklaracjaZUS.PUEContext` (property `Kedu: KEDU`), - - `Z3ParamContext: Z3ParamContext` / `Z3aParamContext` z polami m.in.: `Nieobecnosc: INieobecnoscLubZbieg`, - `NieobecnoscZContextu: bool`, `Pracownik: Pracownik`, `PracownikZContextu: bool`, `Okres: FromTo`, - `OkresZasiłkowy: FromTo`, `OkresZasilkowyOd: Date`, `Współczynnik: Fraction`, `RachBank: string`, - `KontynuacjaŚwiadczenia: bool`. - -**Snippet — generowanie Z-3 dla nieobecności (kontekst):** - -```csharp -var worker = new Soneta.Deklaracje.ZUS.ZUSZ3.Z3Worker(); -var ctx = Context.Empty.Clone(session); -ctx[typeof(Nieobecnosc)] = nieobChorobowa; // worker czyta nieobecność z kontekstu - -var deklaracja = worker.UtworzDeklaracjeZ3(); // zwraca obiekt deklaracji Z-3 -session.Save(); -``` - -**Pułapki:** -- **Sensowny Z-3 wymaga naliczonej wypłaty/podstawy zasiłku** — bez naliczonej podstawy deklaracja - powstanie z pustymi/zerowymi kwotami. W teście na czystej Demo zweryfikujesz fakt powstania obiektu - i ustawienie pól nagłówkowych (pracownik, okres), ale **nie kwoty zasiłku**. -- Worker przyjmuje dane przez `Context` (`ctx[typeof(Nieobecnosc)]`/`ctx[typeof(Pracownik)]`) — nie ma - prostego ctora parametrowego; zegnij pod swój scenariusz `Z3ParamContext`. -- Z-3 dotyczy etatu, Z-3a umów/innych ubezpieczonych — dobierz worker do tytułu ubezpieczenia. -- Metody zwracają `object` (deklaracja KEDU) — zachowaj/odczytaj wynik, nie zakładaj typu wprost. - ---- - -### D5 — Obsługa przestoju (dodanie/usunięcie, przestój ekonomiczny — % wynagrodzenia) - -**Cel:** zaewidencjonować przestój pracownika (np. ekonomiczny) za okres oraz wskazać procent -wynagrodzenia przestojowego; usunąć przestój nakładający się na nieobecność ZUS. - -**Fakty o typie (zweryfikowane skanem DLL):** - -- **Dodanie przestoju:** worker **`Soneta.Kadry.DodajPrzestojWorker`** (czynność „Przestój/Dodaj przestój", - metoda `public void DodajPrzestoj()`): - - settowalne property: `Pracownicy: Pracownik[]`, `Pars: DodajPrzestojWorker.Params`; - - `Params` z polami: `DefinicjaStrefy: Soneta.Kalend.DefinicjaStrefy`, `Okres: FromTo`. -- **Procent wynagrodzenia przestojowego (przestój ekonomiczny):** worker - **`Soneta.Kadry.IndywidualnyProcentWynagrPrzestojowegoWorker`** (czynność - „Przestój/Przestój ekonomiczny - procent wynagr.", metoda `public void Aktualizuj()`): - - `Pracownicy: Pracownik[]`, `Pars.Data: Date`, `Pars.Procent: Soneta.Types.Percent`. -- **Usunięcie przestoju podczas nieobecności ZUS:** worker - **`Soneta.Kadry.UsunPrzestojNieobecnoscWorker`** (czynność „Przestój/Usuń przestój podczas - nieobecności ZUS", metoda `public void UsunPrzestoj()`): `Pracownicy: Pracownik[]`, `Pars.Okres: FromTo`. -- Procent wynagrodzenia przestojowego jest też trzymany na etacie: - `PracHistoria.Etat.Postojowe: Soneta.Kadry.WynagrodzeniePostojowe` (`Procent: Percent`, `Standardowe: bool`). -- `DefinicjaStrefy` (`session.GetKalend().DefinicjeStref`) — słownik konfiguracyjny stref (m.in. przestoju). - -**Snippet — dodanie przestoju:** - -```csharp -var kalend = session.GetKalend(); -var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; -var defStrefa = kalend.DefinicjeStref.WgNazwy["Przestój"]; // nazwa wg słownika danej bazy - -var worker = new Soneta.Kadry.DodajPrzestojWorker -{ - Pracownicy = new[] { pracownik }, - Pars = new Soneta.Kadry.DodajPrzestojWorker.Params(Context.Empty.Clone(session)) - { - DefinicjaStrefy = defStrefa, - Okres = new FromTo(new Date(2026, 6, 1), new Date(2026, 6, 5)) - } -}; -worker.DodajPrzestoj(); // worker wykonuje własną transakcję -session.Save(); -``` - -**Snippet — przestój ekonomiczny (procent):** - -```csharp -var worker = new Soneta.Kadry.IndywidualnyProcentWynagrPrzestojowegoWorker -{ - Pracownicy = new[] { pracownik }, - Pars = new Soneta.Kadry.IndywidualnyProcentWynagrPrzestojowegoWorker.Params(Context.Empty.Clone(session)) - { - Data = new Date(2026, 6, 1), - Procent = new Percent(0.5m) // 50% wynagrodzenia - } -}; -worker.Aktualizuj(); -session.Save(); -``` - -**Pułapki:** -- `DefinicjeStref.WgNazwy[...]` zależy od słownika danej bazy — zweryfikuj nazwę przestoju w Demo - (może być inna niż „Przestój"); dla nieistniejącej nazwy zwraca `null`. -- Worker wykonuje własną transakcję — nie zagnieżdżaj go w otwartej transakcji edycyjnej. -- `Percent` przyjmuj jako ułamek (`0.5m` = 50%), nie liczbę 50. -- `UsunPrzestojNieobecnoscWorker` usuwa przestój **kolidujący z nieobecnością ZUS** — to nie generyczne - „usuń przestój"; zakres działania ogranicza okres + obecność nieobecności ZUS. -- Skutki płacowe (wynagrodzenie przestojowe) liczą się dopiero przy naliczeniu wypłaty. - ---- - -### D6 — Ustalanie/zmiana parametrów okresu zasiłkowego - -**Cel:** zmienić parametry okresu zasiłkowego nieobecności chorobowej — kontynuację/przedłużenie okresu -zasiłkowego oraz wymusić ponowne ustalenie podstawy naliczania zasiłku. - -**Fakty o typie (zweryfikowane skanem DLL):** - -- Parametry okresu zasiłkowego są w subrowie **`Nieobecnosc.Zwolnienie: ZwolnienieZUS`** (bazodanowe, - zapisywalne): - - `KontynuacjaOkrZas: Soneta.Kalend.KontynuacjaOkrZas` (enum: `Warunkowo`, `Tak`, `Nie`), - - `PrzedluzenieOkrZas: bool`, `PrzedluzeniaData: Soneta.Types.Date`, - - `PonownieUstalPodstawe: bool` + metoda `SetPonownieUstalPodstawe(bool)` (patrz D2). -- Worker korekty okresu zasiłkowego: **`Soneta.Kalend.Nieobecnosc.KorektaOkresuZasiłkowegoWorker`** - (czynność „Zmień pozostałe parametry okresu zasiłkowego", metoda `public void PonownieUstalPodstawę()`): - - settowalne `Pars: KorektaOkresuZasiłkowegoWorker.Params` z polami: - `KontynuacjaOkrZas: KontynuacjaOkrZas`, `PrzedluzenieOkrZas: bool`, `PrzedluzeniaData: Date`. -- BO okresu zasiłkowego (przy wdrożeniu) — patrz D10: `PracHistoria.ChorobowyBO` - (`DniZasilkowe`, `ZasilekOdDnia`, `PrzedluzenieOZ`). - -**Snippet — zmiana parametrów wprost na rekordzie:** - -```csharp -using (var t = session.Logout(editMode: true)) -{ - nieobChorobowa.Zwolnienie.KontynuacjaOkrZas = KontynuacjaOkrZas.Tak; - nieobChorobowa.Zwolnienie.PrzedluzenieOkrZas = true; - nieobChorobowa.Zwolnienie.PrzedluzeniaData = new Date(2026, 5, 31); - nieobChorobowa.Zwolnienie.SetPonownieUstalPodstawe(true); - t.Commit(); -} -session.Save(); -``` - -**Snippet — przez worker korekty okresu zasiłkowego:** - -```csharp -var worker = new Nieobecnosc.KorektaOkresuZasiłkowegoWorker(); -var ctx = Context.Empty.Clone(session); -ctx[typeof(Nieobecnosc)] = nieobChorobowa; -worker.Pars = new Nieobecnosc.KorektaOkresuZasiłkowegoWorker.Params(ctx) -{ - KontynuacjaOkrZas = KontynuacjaOkrZas.Tak, - PrzedluzenieOkrZas = true, - PrzedluzeniaData = new Date(2026, 5, 31) -}; -worker.PonownieUstalPodstawę(); // własna transakcja + Commit -session.Save(); -``` - -**Pułapki:** -- **Faktyczne** przeliczenie kwot zasiłku następuje dopiero przy **ponownym naliczeniu wypłaty** — test - na Demo zweryfikuje zmianę pól `KontynuacjaOkrZas`/`PrzedluzenieOkrZas`/`PrzedluzeniaData`/flagi, - ale nie kwoty. -- Parametry okresu zasiłkowego mają sens tylko dla nieobecności **ZUS** (zwolnienia chorobowe/zasiłki) — - dla urlopu wypoczynkowego są bez znaczenia. -- Worker wykonuje własną transakcję — nie zagnieżdżaj go w innej otwartej transakcji. - ---- - -### D8 — Naliczanie i przeliczanie limitów nieobecności - -**Cel:** naliczyć limit nieobecności (jak D7 — `NaliczanieLimitow.DodajLimit()`) oraz przeliczyć liczbę -wykorzystanych dni limitu (czynność „Przelicz wykorzystane"). - -**Fakty o typie (zweryfikowane skanem DLL):** - -- **Naliczenie limitu:** klasa **`Soneta.Kalend.NaliczanieLimitow`** — publiczny bezparametrowy ctor; - settowalne `Pars: NaliczanieLimitow.Params` (`Definicja: DefinicjaLimitu`, `Okres: FromTo`, - `KopiujKorekty: bool`, `ZapisPerPracownik: bool`) oraz `Pracownicy: ICollection` / - `PracownicyIdx: Pracownik[]`; metoda `public void DodajLimit()` (i `DodajLimitUrlopowy()`). - Wariant UI per-pracownik: worker **`Soneta.Kalend.UI.PracownikLimityNaliczanieWorker`** - (czynność „Nalicz limit nieobecności", metoda `DodajLimit()`) — `Pracownik: Pracownik`, - `Pars` jak wyżej. -- **Przeliczenie wykorzystanych:** worker - **`Soneta.Kalend.LimitNieobecnosci.Pracownicy.PrzeliczWykorzystaneWorker`** (czynność - „Limity nieobecności/Przelicz wykorzystane", metoda `public void PrzeliczWykorzystane()`): - - settowalne `Pracownicy: Pracownik[]`, `Pars.Definicja: DefinicjaLimitu`, `Pars.Okres: FromTo`. - -**Snippet — naliczenie + przeliczenie wykorzystanych:** - -```csharp -var kalend = session.GetKalend(); -var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; -var defUrlop = kalend.DefinicjeLimitow.WgNazwy["Urlop wypoczynkowy"]; -var rok = FromTo.Year(new Date(2026, 1, 1)); - -// 1) naliczenie limitu (jak D7) -var naliczanie = new NaliczanieLimitow -{ - Pars = new NaliczanieLimitow.Params(Context.Empty.Clone(session)) - { - Definicja = defUrlop, - Okres = rok, - KopiujKorekty = true - }, - Pracownicy = new[] { pracownik } -}; -naliczanie.DodajLimit(); -session.Save(); - -// 2) przeliczenie wykorzystanych -var przelicz = new LimitNieobecnosci.Pracownicy.PrzeliczWykorzystaneWorker -{ - Pracownicy = new[] { pracownik }, - Pars = new LimitNieobecnosci.Pracownicy.PrzeliczWykorzystaneWorker.Params(Context.Empty.Clone(session)) - { - Definicja = defUrlop, - Okres = rok - } -}; -przelicz.PrzeliczWykorzystane(); -session.Save(); -``` - -**Pułapki:** -- **Nie** twórz `new LimitNieobecnosci(...)` — limit powstaje przez naliczanie (jak w D7). -- `PrzeliczWykorzystane` aktualizuje pole `LimitNieobecnosci.Wykorzystane` na podstawie wprowadzonych - nieobecności — ma sens dopiero **po** naliczeniu limitu i wprowadzeniu nieobecności limitowanych. -- `Razem` może wynosić `0` dla pracownika bez danych napędzających wymiar — opieraj asercje na spójności - (`Wykorzystane == Razem - Pozostalo`), nie na `Razem > 0` (patrz D7). -- Workery wykonują własne transakcje — wywołuj poza otwartą transakcją edycyjną; obsłuż wyjątki z `Save()`. - ---- - -### D9 — Aktualizacja podstaw nieobecności ZUS / podstaw urlopu - -**Cel:** odczytać/wprowadzić ręcznie podstawy naliczania zasiłków (chorobowe/macierzyńskie/opiekuńcze/ -rehabilitacyjne) używane przy nieobecnościach ZUS — np. przy wdrożeniu lub korekcie podstawy. - -**Fakty o typie (zweryfikowane skanem DLL):** - -- Kolekcja na pracowniku: **`pracownik.PodstawyNieobecności: SubTable`** - (jest też `PodstawyNieobecnościOkresowe: SubTable`). -- **`Soneta.Place.PodstawaNieobecnosci`** — tabela `PodstawyNieobec`, `GuidedRow` **child** pracownika - (relacja przez pole `Pracownik`). -- **Brak publicznego ctora** — `PodstawaNieobecnosci` ma jedynie ctory niepubliczne - (`(RowCreator)`, `(Pracownik, TypyPodstawNieobecnosci)`). Rekordy powstają z **naliczenia wypłaty**; - w kodzie biznesowym/teście realnie testowalny jest **odczyt** (dodawanie ręczne — patrz pułapki/spec). - -**Pola i typy (`PodstawaNieobecnosci`) — bazodanowe, zapisywalne:** - -| Pole | Typ | Opis | -|---|---|---| -| `Data` | `Soneta.Types.Date` | data podstawy | -| `Miesieczne` | `decimal` | podstawa miesięczna | -| `Kwartalne` / `Roczne` | `decimal` | składowe | -| `Podstawa` | `decimal` | podstawa naliczania chorobowego | -| `PodstawaM` / `PodstawaO` / `PodstawaR` | `decimal` | podstawa macierzyńskiego / opiekuńczego / rehabilitacyjnego | -| `Typ` | `Soneta.Place.TypyPodstawNieobecnosci` | `Chorobowa` / `Wypoczynkowy` | -| `Norma` / `NormaDni` | `Time` / `int` | norma czasu/dni | -| `Praca` / `PracaDni` | `Time` / `int` | przepracowane | -| `ProcentSkladki` | `Soneta.Types.Percent` | procent składki | - -> **Podstawy urlopu wypoczynkowego** rozróżnia pole `Typ = TypyPodstawNieobecnosci.Wypoczynkowy`; -> podstawy zasiłków ZUS → `Typ = Chorobowa`. - -**Snippet — odczyt podstaw + dodanie podstawy ręcznej:** - -```csharp -var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; - -// Odczyt podstaw chorobowych (filtr serwerowy po Typ): -foreach (PodstawaNieobecnosci p in - pracownik.PodstawyNieobecności[(PodstawaNieobecnosci x) => x.Typ == TypyPodstawNieobecnosci.Chorobowa]) -{ - // p.Data, p.Podstawa, p.Miesieczne -} - -// UWAGA: PodstawaNieobecnosci NIE ma publicznego ctora — normalnie powstaje z naliczenia wypłaty. -// Ręczne dodanie wymagałoby niepublicznego API → w teście testuj wyłącznie ODCZYT (powyżej). -``` - -**Pułapki:** -- Kwoty (`Miesieczne`, `Podstawa`, …) są typu `decimal` — to dane operacyjne podstaw; **normalnie - podstawy powstają z naliczenia wypłaty** (brak publicznego ctora — patrz wyżej). -- `Pracownik` na podstawie jest read-only (guided-parent). -- Filtruj serwerowo po `Typ` (`PodstawyNieobecności[condition]`) — nie iteruj całości z `if` w pamięci. -- W teście na czystej Demo kolekcja `PodstawyNieobecności` może być pusta, dopóki nie naliczono wypłaty - z zasiłkiem — testuj odczyt asercją na model/spójność, a scenariusz „dodaj ręcznie" oznacz `[Ignore]`. - ---- - -### D10 — Bilans otwarcia nieobecności i urlopów - -**Cel:** wprowadzić bilans otwarcia (BO) przy wdrożeniu / starcie roku — historię chorobową (okres -zasiłkowy, dni wykorzystane) oraz urlop wykorzystany u poprzednich pracodawców / w pierwszym miesiącu. - -**Fakty o typie (zweryfikowane skanem DLL):** - -- BO leży na rekordzie historycznym **`Soneta.Kadry.PracHistoria`** w dwóch subrowach (bazodanowe, - zapisywalne): - - **`PracHistoria.ChorobowyBO: Soneta.Kadry.ChorobowyBO`** (BO chorobowy / okres zasiłkowy), - - **`PracHistoria.DodatkowyBO: Soneta.Kadry.DodatkowyBO`** (BO urlopowy — urlop u poprzednich pracodawców). -- BO nieobecności pojedynczej oznacza też flaga `Nieobecnosc.BilansOtwarcia: bool` - (interfejs `IBilansOtwarcia` na `Nieobecnosc`). - -**Pola i typy (`ChorobowyBO`) — bazodanowe:** - -| Pole | Typ | Opis | -|---|---|---| -| `Data` | `Soneta.Types.Date` | data BO | -| `MiesiacPodstawy` | `Soneta.Types.YearMonth` | miesiąc podstawy | -| `Podstawa` | `decimal` | podstawa BO | -| `DniWynagrodzenia` | `int` | dni zwolnienia finansowane przez pracodawcę | -| `DniZasilkowe` | `int` | dni wliczane do bieżącego okresu zasiłkowego | -| `DniZwolnienia` | `int` | dni nieprzerwanego zwolnienia dobrowolnego | -| `ZasilekOdDnia` | `Soneta.Types.Date` | zasiłek od dnia | -| `PrzedluzenieOZ` | `bool` | okres zasiłkowy przedłużony o 3 mies. | - -**Pola i typy (`DodatkowyBO`) — bazodanowe:** - -| Pole | Typ | Opis | -|---|---|---| -| `UPoprzednich` | `decimal` | urlop wykorzystany u poprzednich pracodawców (dni) | -| `Wykorzystany` | `Soneta.Types.Time` | wykorzystany przypadający na bieżące zatrudnienie (godz.) | -| `BezPierwszego` | `bool` | prawo do urlopu w 1. mies. nabyte u poprzedniego pracodawcy | - -**Snippet — wprowadzenie BO chorobowego i urlopowego na zapisie historycznym:** - -```csharp -var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; -var historia = pracownik.Historia[Date.Today]; // właściwy zapis historyczny „na dzień" - -using (var t = session.Logout(editMode: true)) -{ - // BO chorobowy / okres zasiłkowy - historia.ChorobowyBO.DniZasilkowe = 33; - historia.ChorobowyBO.ZasilekOdDnia = new Date(2026, 1, 1); - // BO urlopowy - historia.DodatkowyBO.UPoprzednich = 10m; - t.Commit(); -} -session.Save(); -``` - -**Pułapki:** -- `ChorobowyBO`/`DodatkowyBO` to **subrowy** zapisu `PracHistoria` — nie tworzysz ich osobno, edytujesz - ich pola na istniejącym zapisie historycznym. -- **`ChorobowyBO`** (`DniZasilkowe`, `ZasilekOdDnia`, `PrzedluzenieOZ`, …) jest **zapisywalny** na zwykłym - zapisie historii (zweryfikowane testem D10 na Demo). -- **`DodatkowyBO`** (`UPoprzednich`, `BezPierwszego`, `Wykorzystany`) na zwykłym zapisie historii Demo - rzuca **`ColReadOnlyException`** („pole w trybie tylko do odczytu") — BO urlopowy jest zapisywalny tylko - na zapisie historycznym oznaczonym jako **bilans otwarcia / start zatrudnienia**, nie na dowolnym zapisie - „na dzień". W teście na gotowych pracownikach Demo dodawanie `DodatkowyBO` oznacz `[Ignore]`. -- Pobierz właściwy zapis historyczny przez `pracownik.Historia[data]` (patrz A14/A15) — edycja BO na - niewłaściwym zapisie da błędne dane „na dzień". -- BO ma sens przy wdrożeniu — nie miesza się z normalnym naliczaniem; po wprowadzeniu wpływa na limity - (D8) i okres zasiłkowy (D6) dopiero przy przeliczeniu/naliczeniu. - ---- - -### D11 — Wnioski o urlop / delegację - -**Cel:** zarejestrować wniosek urlopowy (lub o delegację), zmienić jego stan (akceptacja/odrzucenie/ -przywrócenie) i — docelowo — przekształcić zaakceptowany wniosek w nieobecność. - -**Fakty o typie (zweryfikowane skanem DLL):** - -- Wniosek urlopowy: **`Soneta.Kadry.WniosekUrlopowy`** — tabela `WnioskiUrlopowe`, `GuidedRow` root. - Konstruktory publiczne: **`new WniosekUrlopowy(Pracownik pracownik)`** oraz - **`new WniosekUrlopowy(Pracownik pracownik, DefinicjaNieobecnosci definicja)`**. -- Kolekcja na pracowniku: **`pracownik.WnioskiUrlopowe: SubTable`** - (oraz `WnioskiKierownika`, `WnioskiZastępcy` — te same wnioski w roli kierownika/zastępcy). -- Pola `WniosekUrlopowy` (bazodanowe, zapisywalne): `Pracownik: Pracownik`, - `Definicja: DefinicjaNieobecnosci`, `Okres: FromTo`, `Data: Date`, `DataDecyzji: Date`, - `Kierownik: Pracownik`, `Opis: MemoText`, `Stan: Soneta.Kadry.StanWnioskuUrlopowego`. - - `StanWnioskuUrlopowego`: `Oczekujący`, `Anulowany`, `Zaakceptowany`, `Odrzucony`, `Korygowana`. -- Wniosek o delegację jest subrowem wniosku: `WniosekUrlopowy.Delegacja: Soneta.Kadry.WniosekODelegację` - (`DataRozpoczeciaPlanowana`, `DataZakonczeniaPlanowana: DateShortTime`, `KrajDocelowy`, `Cel: MemoText`, - `WnioskowanaZaliczka: Currency`); samodzielny `new WniosekODelegację()` ma publiczny ctor bezparametrowy. -- **Planowane nieobecności** (osobny model, np. plan urlopów): kolekcja - **`pracownik.PlanowaneNieobecności: FromToSubTable`**; - typ `PlanowanaNieobecność` (tabela `PlanNieobecnosci`, root) z ctorem - **`new PlanowanaNieobecność(Pracownik pracownik)`**, polami `Definicja`, `Okres: FromTo`. - - **`Definicja` musi mieć zaznaczone pole `Planowana`** (`DefinicjaNieobecnosci.Planowana == true`) — - inaczej setter rzuca `RowException` „Wybrana definicja musi mieć zaznaczone pole 'Planowana'."; dobierz - definicję dynamicznie: `DefNieobecnosci.Cast().First(d => d.Planowana)`. - - **`Stan: StanPlanowanejNieobecności` jest READ-ONLY** (`Oczekująca`, `Wprowadzona`, `Korygowana`, - `Zatwierdzona`, `Anulowana`) — **nie przypisujesz** go wprost (`plan.Stan = …` → błąd kompilacji - „cannot be assigned to"); przejścia stanu wykonujesz metodami domenowymi - **`StanWprowadzona()` / `StanZatwierdzona()` / `StanAnulowana()` / `StanOczekująca()`**. -- Akceptacja/odrzucenie/przywrócenie z poziomu Pulpitu: worker (UI/Net) - **`PracownikNetWnioskiUrlopowe`** z akcjami „Zatwierdź wniosek"/`Zatwierdz`, „Odrzuć wniosek"/`Odrzuc`, - „Przywróć wniosek"/`Przywroc`. W kodzie biznesowym/teście prościej ustawiać `Stan` wprost. - -**Snippet — rejestracja wniosku urlopowego + akceptacja:** - -```csharp -var kalend = session.GetKalend(); -var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; -// UWAGA: dla definicji limitowanej (np. „Urlop wypoczynkowy") akceptacja wniosku (set Stan) wyzwoli -// przeliczenie limitu → LimitNotFoundException, jeśli limit nie został wcześniej naliczony (patrz pułapki). -// Tu używamy definicji bezlimitowej (np. „Urlop bezpłatny (art 174 kp)") albo najpierw naliczamy limit (D8). -var defUrlop = kalend.DefNieobecnosci.WgNazwy["Urlop bezpłatny (art 174 kp)"]; - -using (var t = session.Logout(editMode: true)) -{ - var wniosek = session.AddRow(new WniosekUrlopowy(pracownik, defUrlop)); - wniosek.Okres = new FromTo(new Date(2026, 8, 3), new Date(2026, 8, 7)); - wniosek.Data = Date.Today; - wniosek.Stan = StanWnioskuUrlopowego.Oczekujący; - t.Commit(); -} -session.Save(); - -// Akceptacja (zmiana stanu): -using (var t = session.Logout(editMode: true)) -{ - var wniosek = pracownik.WnioskiUrlopowe - .Cast() - .First(w => w.Stan == StanWnioskuUrlopowego.Oczekujący); - wniosek.Stan = StanWnioskuUrlopowego.Zaakceptowany; - wniosek.DataDecyzji = Date.Today; - t.Commit(); -} -session.Save(); -``` - -**Pułapki:** -- **Akceptacja wniosku na definicji limitowanej rzuca `LimitNotFoundException`** bez wcześniej naliczonego - limitu: ustawienie `Stan` (np. `Zaakceptowany`) na wniosku z definicją „Urlop wypoczynkowy" wewnętrznie - ustawia `Okres` nieobecności i wyzwala `DefinicjaLimitu.Przelicz(...)`, który dla pracownika bez limitu - na ten dzień rzuca wyjątek. Rozwiązanie: albo nalicz limit (D8) **przed** zmianą stanu, albo do scenariusza - obsługi samego rekordu wniosku użyj definicji **bezlimitowej** (np. „Urlop bezpłatny (art 174 kp)"). -- **Przekształcenie wniosku w nieobecność** wymaga, by nieobecność limitowana miała naliczony limit - (jak D1) — sama akceptacja wniosku nie tworzy automatycznie rozliczonej nieobecności w teście bez - naliczonego limitu/wypłaty. -- `WniosekODelegację` to subrow wniosku (`WniosekUrlopowy.Delegacja`) — wnioskowanie o delegację - ustawiasz na tym subrowie; pełne rozliczenie delegacji to moduł `Soneta.Delegacje` (osobny dokument - handlowy PWS), poza zakresem wniosku. -- Filtruj kolekcję wniosków przez `WnioskiUrlopowe[condition]` lub iteruj w zakresie jednego pracownika; - nie skanuj globalnej tabeli `WnioskiUrlopowe` bez zakresu (tabela operacyjna guided). -- Stan zmieniaj świadomie wg enuma `StanWnioskuUrlopowego` — workery Net robią to samo z dodatkową - logiką workflow (powiadomienia), której w teście jednostkowym nie odtworzysz. - ---- - -### D12 — Praca zdalna (wnioski, lokalizacje, ewidencja) - -**Cel:** skonfigurować pracę zdalną pracownika (model pracy, limit pracy zdalnej okazjonalnej), -zarejestrować wniosek o pracę zdalną i lokalizacje jej świadczenia oraz odczytać ewidencję. - -**Fakty o typie (zweryfikowane skanem DLL):** - -- Parametry pracy zdalnej leżą na etacie/historii: **`PracHistoria.PracaZdalna: Soneta.Kadry.PracZdalna`** - (subrow, bazodanowe, zapisywalne): - - `ModelPracy: Soneta.Kadry.ModelPracy` (`NieDotyczy`, `PracaStacjonarna`, `PracaHybrydowa`, `PracaZdalna`), - - `OswiadczenieWarunki: bool` (warunki lokalowe/techniczne), - - `LimitPZ: int`, `IndywidualnyLimitPZ: bool`, `TypLimituPZ: TypLimituPracyZdalnej` - (`Roczny`, `Miesieczny`, `Tygodniowy`, `Kwartalny`, `Półroczny`). -- Lokalizacje pracy zdalnej: **`pracownik.LokalizacjePracyZdalnej: SubTable`** - (tabela `LokPracZdalnej`). -- Wnioski o pracę zdalną: **`pracownik.WnioskiPracyZdalnej: SubTable`** - (oraz `WnioskiPracyZdalnejKierownika`); typ `WniosekPracyZdalnej` ma ctor - `(Pracownik, DefinicjaRodzajuPracyZdalnej)` — **ctory są niepubliczne**, więc tworzenie wniosku idzie - przez worker (`GrupoweZleceniePracyZdalnejWorker`) lub Pulpit, nie wprost `new`. -- Lokalizacja pracy zdalnej: `Soneta.Kadry.LokalizacjaPracyZdalnej` ma **publiczny ctor - `new LokalizacjaPracyZdalnej(Pracownik pracownik)`**. -- Ewidencja/odczyt limitu pracy zdalnej okazjonalnej: worker - **`Soneta.Kadry.Pracownik.PracaZdalnaWorker`** — property odczytowe (bez akcji modyfikującej): - `DniPracyZdalnejRazem: int`, `DniPracyZdalnejOkazjonalnej: int`, `DniPracyZdalnejOkazjonalnejLimit: int`, - `CzasPracyZdalnejRazem: Time`, `LimitPracaZdalnaOkazjonalna: int`, `PozostaloPracaZdalnaOkazjonalna: int`; - kontekst: `Pracownik: Pracownik`, `Okres: FromTo`. -- Grupowe zlecenie pracy zdalnej (Pulpit/seryjne): worker - **`Soneta.Kadry.UI.KadryNet.Workers.GrupoweZleceniePracyZdalnejWorker`** (akcja - „Dodaj wnioski zlecenia pracy zdalnej"/`DodajZleceniaPracyZdalnej`): `Pracownicy: Pracownik[]`, - `Pars.Okres: FromTo`, `Pars.Data: Date`, `Pars.Uwagi: string`. -- Aktualizacja podzielników kosztów na podstawie pracy hybrydowej: worker - **`AktualizujPodzielnikowPracaZdalnaWorker`** (`DefinicjaPodzielnika`, `Okres: YearMonth`, …). - -**Snippet — ustawienie modelu pracy zdalnej + lokalizacja + wniosek:** - -```csharp -var kadry = session.GetKadry(); -var pracownik = kadry.Pracownicy.WgKodu["006"]; - -using (var t = session.Logout(editMode: true)) -{ - var historia = pracownik.Historia[Date.Today]; - historia.PracaZdalna.ModelPracy = ModelPracy.PracaHybrydowa; - historia.PracaZdalna.OswiadczenieWarunki = true; - - // lokalizacja pracy zdalnej (np. adres domowy) - var lok = session.AddRow(new LokalizacjaPracyZdalnej(pracownik)); - // … pola adresowe lokalizacji wg LokalizacjaPracyZdalnej - t.Commit(); -} -session.Save(); - -// Odczyt ewidencji pracy zdalnej okazjonalnej (worker odczytowy): -// Pracownik i Okres są zwykłymi, settowalnymi property (nie trzeba przekazywać przez Context): -var pz = new Soneta.Kadry.Pracownik.PracaZdalnaWorker -{ - Pracownik = pracownik, - Okres = FromTo.Year(new Date(2026, 1, 1)) -}; -// odczyt: pz.DniPracyZdalnejRazem, pz.LimitPracaZdalnaOkazjonalna, pz.PozostaloPracaZdalnaOkazjonalna -``` - -**Pułapki:** -- `PracaZdalnaWorker` to worker **odczytowy** (ma property, brak akcji modyfikującej) — służy do - prezentacji ewidencji/limitu, nie do zapisu. -- `ModelPracy`/`OswiadczenieWarunki` są na **historycznym** zapisie etatu (`PracHistoria.PracaZdalna`) — - edytuj właściwy zapis „na dzień". -- `WniosekPracyZdalnej` ma **niepubliczne ctory** — w teście jednostkowym nie utworzysz go przez `new`; - zlecenie pracy zdalnej idzie przez worker `GrupoweZleceniePracyZdalnejWorker` (czynność Net/UI, - wymaga `Context`). Testuj raczej `ModelPracy`/`OswiadczenieWarunki` na `PracHistoria.PracaZdalna` - i `LokalizacjaPracyZdalnej` (ma publiczny ctor). -- `LokalizacjaPracyZdalnej` ma publiczny ctor `(Pracownik)` — testowalna wprost. - -## E. Plan pracy i kalendarz - -> **Model kalendarza pracownika.** Każdy `Pracownik` ma kalendarz roboczy -> (`pracownik.Etat.Kalendarz : Soneta.Kalend.Kalendarz`), którego dni leżą w tabeli -> `DniKalendarza` (`DzienKalendarzaBase`, child kalendarza). Pracownik wystawia trzy -> niezależne kolekcje dni typu `DateSubTable` (indeksator po dacie `[Date]`, **tylko do -> odczytu** — element tworzysz konstruktorem + `AddRow`): -> - `pracownik.DniPlanu : DateSubTable` — **plan/harmonogram** (dni `DzienPlanu : DzienKalendarzaBase`); to `pracownik.Etat.Kalendarz.Dni`. -> - `pracownik.DniPracy : DateSubTable` — **ewidencja** (realizacja) czasu pracy. -> - `pracownik.DniRCP : DateSubTable` — **zarejestrowany** czas pracy (RCP) — patrz sekcja F. -> -> Wszystkie dni współdzielą subrow `Praca : Soneta.Kalend.CzasPracy` z polami -> `OdGodziny`/`DoGodziny`/`Czas : Soneta.Types.Time`. Definicja dnia (`Definicja : -> Soneta.Kalend.DefinicjaDnia`) to rekord **konfiguracyjny** (słownik `DefinicjeDni`, -> indeksator `[Kod]`). -> -> **Ograniczenie wykonalności.** Plan i ewidencja są normalnie wyliczane przez kalkulator -> czasu pracy z definicji kalendarza/serii — ręczne tworzenie pojedynczego dnia jest możliwe -> publicznym kontraktem (ctor `(Pracownik, Date)` + `AddRow`), ale **wymaga zdefiniowanego -> `DefinicjaDnia` w konfiguracji**. Operacje masowe (przeliczenie planu na okres) są zaszyte -> w workerach/kalkulatorach UI — patrz E2. - -### E1 — Wprowadzanie planowanego czasu pracy (★) - -**Cel:** odczytać lub ustawić plan pracy (harmonogram) pracownika na konkretny dzień — -godziny od–do, normę dobową oraz typ dnia. - -**Pola i typy:** - -| Element | Lokalizacja | Typ | Uwaga | -|---|---|---|---| -| Plan pracy (cała kolekcja) | `pracownik.DniPlanu` | `Soneta.Business.DateSubTable` | == `pracownik.Etat.Kalendarz.Dni`; indeksator `[Date]` (get) | -| Dzień planu | `pracownik.DniPlanu[data]` | `Soneta.Kalend.DzienPlanu` (`DzienKalendarzaBase`) | `null`, gdy dla daty brak dnia planu | -| Data dnia | `DzienPlanu.Data` | `Soneta.Types.Date` | bazodanowe; ustawiane przez ctor | -| Godziny pracy (subrow) | `DzienPlanu.Praca` | `Soneta.Kalend.CzasPracy` | `Praca.OdGodziny`, `Praca.DoGodziny`, `Praca.Czas : Time` (zapisywalne) | -| Czas (norma dnia, odczyt) | `DzienPlanu.Czas` | `Soneta.Types.Time` | kalkulowane (czas pracy dnia) | -| Od (odczyt) | `DzienPlanu.OdGodziny` | `Soneta.Types.Time` | kalkulowane | -| Definicja dnia | `DzienPlanu.Definicja` | `Soneta.Kalend.DefinicjaDnia` | rekord słownika konfiguracyjnego `DefinicjeDni` | -| Tolerancja wejścia | `DzienPlanu.TolerancjaWe` | `Soneta.Types.Time` | bazodanowe | -| Norma dobowa kalendarza | `pracownik.Etat.Kalendarz.NormaDobowa` | `Soneta.Types.Time` | poziom kalendarza, nie dnia | -| Słownik definicji dni | `session.GetKalend().DefinicjeDni` | `DefinicjeDni` | indeksator `[kod: string]`; skróty `WolnaSobota`, `Niedziela` | - -**Snippet:** - -```csharp -var kalend = session.GetKalend(); -var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; - -// --- Odczyt planu na dzień (bezpiecznie: indeksator zwraca null dla braku dnia) --- -var data = new Date(2026, 6, 1); -var dzienPlanu = (DzienPlanu)pracownik.DniPlanu[data]; -if (dzienPlanu is not null) -{ - Time odGodz = dzienPlanu.Praca.OdGodziny; // np. 8:00 - Time doGodz = dzienPlanu.Praca.DoGodziny; // np. 16:00 - Time normaDnia = dzienPlanu.Czas; // wyliczona norma dnia (kalkulowane) - DefinicjaDnia typDnia = dzienPlanu.Definicja; -} - -// --- Ustawienie/utworzenie dnia planu (wymaga DefinicjaDnia z konfiguracji) --- -using (var t = session.Logout(editMode: true)) -{ - var dp = (DzienPlanu)pracownik.DniPlanu[data]; - if (dp is null) - { - dp = session.AddRow(new DzienPlanu(pracownik, data)); // ctor (Pracownik, Date) - dp.Definicja = kalend.DefinicjeDni["RB"]; // typ dnia ze słownika (np. dzień roboczy) - } - dp.Praca.OdGodziny = new Time(8, 0); - dp.Praca.DoGodziny = new Time(16, 0); // Czas dnia wylicza się z od–do - - t.Commit(); -} -session.Save(); -``` - -**Pułapki:** -- `DniPlanu` to `DateSubTable` **nietypowany** (zwraca `Row`) — rzutuj na `DzienPlanu`. Indeksator - `[Date]` jest **tylko do odczytu**: nowego dnia nie „przypiszesz", tworzysz go ctorem - `new DzienPlanu(pracownik, data)` + `session.AddRow(...)`. -- Godziny ustawiasz na **subrowie** `Praca` (`dp.Praca.OdGodziny = …`), nie na `dp.OdGodziny` — - to ostatnie jest kalkulowane (read-only). Po ustawieniu od–do `Praca.Czas`/`Czas` przeliczają się. -- `Definicja` to rekord **konfiguracyjnego** słownika `DefinicjeDni` — pobierz istniejący wpis - (`kalend.DefinicjeDni[kod]`), nie twórz „w locie". Bez przypisanego `Definicja` świeży dzień planu - może nie przejść weryfikatorów. -- Plan jest zwykle generowany przez kalkulator z definicji kalendarza (serie dni, święta) — - ręczne nadpisywanie pojedynczego dnia to korekta, nie sposób budowy całego harmonogramu (do tego - służy operacja seryjna / kopiowanie planu, E2). -- Norma dobowa to atrybut **kalendarza** (`Etat.Kalendarz.NormaDobowa`), nie pojedynczego dnia. - -### E2 — Planowanie czasu pracy grupy (kopiowanie planu) (★) - -**Cel:** skopiować wyliczony plan pracy (harmonogram) na wskazany okres — dla jednego pracownika -albo dla grupy, oraz seryjnie zaktualizować kalendarz pracowników (zmiana kalendarza docelowego). - -**Publiczny kontrakt — dwie drogi:** - -| Operacja | API | Charakter | -|---|---|---| -| Kopiowanie **planu** pracownika na okres | `Soneta.Kalend.KalendarzPlanuKopia.Kopiuj(Pracownik pracownik, FromTo okres)` (**public static**) | bez UI — proste API | -| Kopiowanie **pracy/realizacji** na okres | `Soneta.Kalend.KalendarzPracyKopia.Kopiuj(Pracownik pracownik, FromTo okres)` (**public static**) | bez UI — proste API | -| Kopiowanie grupy (worker UI) | `KalendarzPlanuKopia.KopiujWorker` / `KalendarzPracyKopia.KopiujWorker` | wymaga `Context` z zaznaczeniem | -| Aktualizacja kalendarza grupy | `Soneta.Kadry.AktualizujKalendarzWorker` | wymaga `Params` z `Context` | - -**Worker `KopiujWorker` (BI/„Kopiuj plan…", „Kopiuj pracę…"):** klasa `ContextBase` z ctorem -`(Context context)`; pola `[Context] FromTo Okres`, `[Context] Pracownik[] Pracownicy`; metoda -`void Kopiuj()`. Działa **wyłącznie** z kontekstem UI (zaznaczona lista pracowników) i jest gardzona -licencją BI/BI_PL/PL oraz `IsVisibleKopiuj` (niedostępny na mobile). - -**Worker `AktualizujKalendarzWorker`:** pola `[Context] Pracownik[] Pracownicy`, -`Params Pars` (`Pars.Data`, `Pars.TylkoOstatni: bool`, `Pars.PowodAktualizacji: string`, -`Pars.Kalendarze: KalendarzBase[]`, `Pars.Docelowy: Kalendarz`, `Pars.Zmiana: bool`, -`Pars.Interpretacja`), metoda `void Aktualizuj()`. `Params` to `ContextBase` (ctor `(Context)`). - -**Snippet (proste API dla jednego pracownika — bez UI):** - -```csharp -var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; -var okres = new FromTo(new Date(2026, 6, 1), new Date(2026, 6, 30)); - -using (var t = session.Logout(editMode: true)) -{ - // Wylicza plan z kalendarza i zapisuje do kopii planu pracownika za wskazany okres: - KalendarzPlanuKopia.Kopiuj(pracownik, okres); // public static - // analogicznie realizacja: KalendarzPracyKopia.Kopiuj(pracownik, okres); - t.Commit(); -} -session.Save(); -``` - -**Snippet (grupa — przez worker; wymaga Context z zaznaczeniem):** - -```csharp -// Tylko w warstwie UI/Czynności — Context dostarcza zaznaczonych pracowników. -var worker = new KalendarzPlanuKopia.KopiujWorker(context) -{ - Okres = new FromTo(new Date(2026, 6, 1), new Date(2026, 6, 30)), - Pracownicy = context.Get() -}; -worker.Kopiuj(); // wewnątrz: Session.Logout + Commit -``` - -**Pułapki:** -- **Kopiowanie grupy nie ma „czystego" API bezkontekstowego** — `KopiujWorker` i - `AktualizujKalendarzWorker.Params` dziedziczą po `ContextBase` i wymagają `Context` (zaznaczenie z - listy UI). Dla kodu serwerowego/testów używaj **publicznej statycznej** `KalendarzPlanuKopia.Kopiuj(pracownik, okres)` - w pętli po pracownikach — to ona realizuje właściwą logikę (worker w `KopiujInt` woła ją per pracownik). -- `KopiujWorker.Kopiuj()` jest gardzony licencją (BI/BI_PL/PL) i `IsVisibleKopiuj` (m.in. blokada na - mobile) — to logika UI, nie wywołuj jej z kodu biznesowego. -- Kopia planu/pracy trafia do **osobnych** kolekcji `pracownik.DniPlanuKopia`/`pracownik.DniPracyKopia` - (`DateSubTable`), powiązanych z `KalendarzPlanuKopia`/`KalendarzPracyKopia` — to bufor kopii, odrębny - od właściwego `DniPlanu`/`DniPracy`. -- `okres` jest normalizowany przez setter workera do pełnych miesięcy (otwarty `From`/`To` → - pierwszy/ostatni dzień miesiąca); przy statycznym `Kopiuj` podawaj zamknięty `FromTo`. -- Operacja seryjna na grupie pracowników = długa transakcja → dziel na paczki, trzymaj transakcje - krótkie (safe-code §13.1). - -### E3 — Aktualizacja kalendarza pracownika (operacja seryjna „Zaktualizuj kalendarz pracownika") - -**Cel:** seryjnie zmienić kalendarz roboczy zaznaczonych pracowników (zmiana kalendarza -docelowego, przeliczenie planu na nowy kalendarz od wskazanej daty) — operacja z menu -„Czynności" na liście pracowników. - -**Publiczny kontrakt — worker `Soneta.Kadry.AktualizujKalendarzWorker`:** - -| Element | Sygnatura / typ | Uwaga | -|---|---|---| -| Konstruktor | `new AktualizujKalendarzWorker()` | bezparametrowy; worker UI | -| Pracownicy (wejście) | `Pracownicy : Pracownik[]` | **set-only**; karmione z `Context` (zaznaczenie listy) | -| Parametry | `Pars : Params` | **set-only**; `Params` to `ContextBase`, ctor `(Context context)` | -| Wykonanie | `void Aktualizuj()` | właściwa operacja seryjna (Logout + Commit wewnątrz) | - -**`Soneta.Kadry.AktualizujKalendarzWorker.Params` (`: ContextBase`, ctor `(Context)`):** - -| Pole | Typ | Uwaga | -|---|---|---| -| `Data` | `Soneta.Types.Date` | data, od której obowiązuje nowy kalendarz | -| `TylkoOstatni` | `bool` | aktualizuj tylko ostatni (bieżący) zapis historyczny | -| `PowodAktualizacji` | `string` | opis powodu (do dokumentu aktualizacji) | -| `Kalendarze` | `KalendarzBase[]` | kalendarze źródłowe objęte zmianą; lista przez `GetListKalendarze()` | -| `Docelowy` | `Soneta.Kalend.Kalendarz` | kalendarz docelowy; lista przez `GetListDocelowy()` | -| `Zmiana` | `bool` | flaga: czy zmienić kalendarz (a nie tylko przeliczyć) | -| `Interpretacja` | `Soneta.Kadry.InterpretacjaKalendarza` | `WgPlanu` / `WgObecnosci` / `WgZestawien`; `IsReadOnlyInterpretacja()` | - -**Snippet (warstwa UI/Czynności — wymaga `Context` z zaznaczeniem):** - -```csharp -// Tylko w warstwie UI: Context dostarcza zaznaczonych pracowników. -var worker = new AktualizujKalendarzWorker -{ - Pracownicy = context.Get(), - Pars = new AktualizujKalendarzWorker.Params(context) - { - Data = new Date(2026, 7, 1), - Docelowy = session.GetKalend().Kalendarze.WgKodu["PODSTAWOWY"], - Zmiana = true, - Interpretacja = InterpretacjaKalendarza.WgPlanu, - PowodAktualizacji = "Zmiana systemu czasu pracy" - } -}; -worker.Aktualizuj(); // wewnątrz: Session.Logout + Commit -``` - -**Pułapki:** -- `Params` dziedziczy po `ContextBase` (ctor `(Context)`) — **nie da się go zbudować bez `Context`**. - Dlatego E3 nie ma „czystego" API bezkontekstowego; to operacja UI/serwerowa z zaznaczeniem. -- `Pracownicy` i `Pars` są **set-only** — nie odczytasz ich z powrotem; ustaw przed `Aktualizuj()`. -- Operacja seryjna = długa transakcja na wielu pracownikach → w realnym użyciu dziel na paczki - (safe-code §13.1). Sam worker zarządza transakcją wewnętrznie. -- Zmiana kalendarza jest **historyczna** (operuje na zapisach `Etat`) — `TylkoOstatni`/`Data` - decydują, których zapisów historycznych dotyczy. - ---- - -### E4 — Uzgodnienie doby pracowniczej (model doby; godziny rozpoczęcia doby) - -**Cel:** przesunąć granicę doby pracowniczej dla dnia ewidencji — gdy zmiana zaczyna się w jednej -dobie kalendarzowej, a kończy w następnej (nocna), uzgodnienie „przenosi" początek/koniec pracy do -właściwej doby pracowniczej. Operacja na pojedynczym dniu (`DzienPracy`) lub seryjnie na grupie. - -**Model doby (publiczny kontrakt):** - -| Element | Lokalizacja | Typ | Uwaga | -|---|---|---|---| -| Początek doby w niedziele/święta | `pracownik.Last.Etat.ConfigPoczątekDobyNiedzieledIŚwięta` | `Soneta.Types.Time` | **read-only** (konfiguracyjne); godzina startu doby | -| Norma dobowa | `pracownik.Last.Etat.NormaDobowa` | `Soneta.Types.Time` | bazodanowe; norma czasu doby | -| Norma dobowa kalendarza | `pracownik.Last.Etat.Kalendarz.NormaDobowa` | `Soneta.Types.Time` | poziom kalendarza | -| Interpretacja kalendarza | `pracownik.Last.Etat.InterpretacjaKalendarza` | `Soneta.Kadry.InterpretacjaKalendarza` | `WgPlanu`/`WgObecnosci`/`WgZestawien` — jak interpretować dobę | - -> **Uwaga:** `Etat` leży na bieżącym **zapisie historycznym** (`pracownik.Last.Etat : Soneta.Kadry.Etat`, -> gdzie `Last : PracHistoria`) — nie ma property `pracownik.Etat` bezpośrednio na roocie pracownika. -| Godziny pracy dnia | `DzienPracy.Praca` | `Soneta.Kalend.CzasPracy` | `OdGodziny`/`DoGodziny`/`Czas` — granice realizacji w dobie | - -**Worker pojedynczego dnia — `Soneta.Kalend.DzienPracy.UzgodnijDobePracowniczaWorker`:** - -| Element | Sygnatura | Uwaga | -|---|---|---| -| Konstruktor | `new DzienPracy.UzgodnijDobePracowniczaWorker()` | | -| Dzień (wejście) | `Dzień : DzienPracy` | **set-only** | -| Warunek dostępności | `static bool IsEnabledUzgodnijDobePracownicza(DzienPracy dzień)` | czy operacja ma sens dla dnia | -| Uzgodnienie | `object UzgodnijDobePracownicza()` | przelicza dobę | -| Przeniesienie początku | `DzienPracy PrzenieśPoczątek()` | przenosi początek pracy do poprz. doby | -| Przeniesienie końca | `DzienPracy PrzenieśKoniec()` | przenosi koniec pracy do nast. doby | -| Dokument aktualizacji | `DokumentAktualizacjiKalendarza : IDokumentAktualizacjiKalendarza`, `DataAktualizacji : System.DateTime` | kontekst historii | - -**Worker seryjny (grupa) — `Soneta.Kadry.UzgodnijDobePracowniczaPracownikowWorker`:** - -| Element | Sygnatura / typ | Uwaga | -|---|---|---| -| Konstruktor | `new UzgodnijDobePracowniczaPracownikowWorker()` | | -| Pracownicy | `Pracownicy : Pracownik[]` | **set-only**; z `Context` | -| Parametry | `Pars : Params` (`ContextBase`, ctor `(Context)`); pole `Okres : FromTo` | **set-only** | -| Wykonanie | `UzgodnijDobePracowniczaResult UzgodnijDobePracownicza()` | zwraca wynik | - -**Snippet (pojedynczy dzień):** - -```csharp -var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; -var dzien = pracownik.DniPracy[new Date(2026, 6, 1)]; // DzienPracy lub null -if (dzien is not null && DzienPracy.UzgodnijDobePracowniczaWorker.IsEnabledUzgodnijDobePracownicza(dzien)) -{ - using (var t = session.Logout(editMode: true)) - { - var worker = new DzienPracy.UzgodnijDobePracowniczaWorker { Dzień = dzien }; - worker.UzgodnijDobePracownicza(); - t.Commit(); - } - session.Save(); -} -``` - -**Pułapki:** -- Godzina rozpoczęcia doby to atrybut **konfiguracyjny `Etat`** (`ConfigPoczątekDobyNiedzieledIŚwięta`, - read-only) i normy `Etat.NormaDobowa`/`Etat.Kalendarz.NormaDobowa` — nie ma osobnego, edytowalnego - pola „początek doby" na pojedynczym `DzienPracy`. -- `Dzień` workera pojedynczego jest **set-only**; `Pracownicy`/`Pars` workera grupowego również. -- Worker grupowy `Params` to `ContextBase` (ctor `(Context)`) — **wymaga `Context`** (zaznaczenie UI), - brak czystego API bezkontekstowego. -- Uzgodnienie modyfikuje `DzienPracy.Praca` (od–do) i może rozbić pracę na dwie doby — wykonuj w - transakcji (`Logout(editMode:true)` + `Commit`) i zapisz `Save()`. - ---- - -### E5 — Odczyt normy czasu pracy i czasu przepracowanego za okres (★ testowalne) - -**Cel:** dla pracownika odczytać za zadany okres (`FromTo`/`YearMonth`): normę czasu pracy -(planowaną), czas przepracowany (zrealizowany), nadgodziny, czas nocny, liczbę/normę nieobecności — -bez modyfikacji danych (czysty odczyt statystyk). - -**Punkt wejścia — `pracownik.Czasy : Soneta.Kalend.KalkulatorPracownika`:** - -| Metoda (publiczna, instancyjna) | Zwraca | Znaczenie | -|---|---|---| -| `Norma(FromTo okres, params Item[] condition)` | `CzasDni` | norma (planowana) czasu pracy za okres | -| `Norma(FromTo okres, DefinicjaStrefy def, params Item[] condition)` | `CzasDni` | norma w obrębie strefy | -| `NormaKodeksowa(YearMonth miesiąc)` | `CzasDni` | norma kodeksowa miesiąca (pełny etat) | -| `NormaKodeksowaWym(Fraction wymiar, Time normaDobowa, YearMonth miesiąc)` | `CzasDni` | norma kodeksowa wg wymiaru etatu | -| `Praca(FromTo okres, params Item[] condition)` | `CzasDni` | czas **przepracowany** (zrealizowany) za okres | -| `Praca(FromTo okres, DefinicjaStrefy def, params Item[] condition)` | `CzasDni` | przepracowany w obrębie strefy | -| `PracaRozliczana(FromTo okres, params Item[] condition)` | `CzasDni` | czas pracy rozliczany (do nadgodzin) | -| `PracaZatr(FromTo okres, bool usprPłatne)` | `CzasDni` | praca w okresie zatrudnienia | -| `Nadgodziny(YearMonth okres)` / `Nadgodziny(FromTo okres)` | `ZestawienieNadgodzin` | nadgodziny | -| `NadgodzinyDobaOkres(FromTo okres)` | `ZestawienieNadgodzin` | nadgodziny dobowe/okresowe | -| `Nocne(YearMonth\|FromTo okres)` | `Time` | czas nocny | -| `NormaNie(YearMonth\|FromTo okres, params Item[] condition)` | `CzasDni` | norma nieobecności | -| `DniNie(YearMonth\|FromTo okres, params Item[] condition)` | `int` | liczba dni nieobecności | -| `Nieobecność(Date data[, bool clip])` | `INieobecnosc` | nieobecność w danym dniu | - -**`Soneta.Kalend.CzasDni` (typ wyniku):** - -| Pole | Typ | Uwaga | -|---|---|---| -| `Czas` | `Soneta.Types.Time` | sumaryczny czas (read-only) | -| `Dni` | `int` | liczba dni (read-only) | -| `CzasDni.Empty`, `CzasDni.Invalid` | `CzasDni` | wartości specjalne; operatory `+`/`-`/`==` | - -**`Soneta.Kalend.ZestawienieNadgodzin` (struct):** `N50`, `N100`, `NSW`, `N100Doba`, `N100Okres`, -`Razem` — wszystkie `Time` (read-only); `ZestawienieNadgodzin.Zero`. - -**Snippet (czysty odczyt):** - -```csharp -var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; -var kalk = pracownik.Czasy; // KalkulatorPracownika -var okres = new FromTo(new Date(2026, 6, 1), new Date(2026, 6, 30)); - -CzasDni norma = kalk.Norma(okres); // norma planowana -CzasDni przepracowano = kalk.Praca(okres); // czas zrealizowany -ZestawienieNadgodzin nadg = kalk.Nadgodziny(new YearMonth(2026, 6)); -Time nocne = kalk.Nocne(okres); - -Time normaCzas = norma.Czas; int normaDni = norma.Dni; -Time pracaCzas = przepracowano.Czas; Time nadgRazem = nadg.Razem; -``` - -**Pułapki:** -- `KalkulatorPracownika` **nie jest `Row`** — to obiekt liczący (zwykły `object`). Nie zapisuje się, - nie wymaga transakcji; to czysty odczyt. Pobieraj go zawsze przez `pracownik.Czasy` (ma kontekst - pracownika), nie twórz ręcznie ctorem chyba że masz `Pracownik` + ewentualny `Log`. -- Parametr `condition` to **serwerowy filtr** (`Item[]`, RowCondition) — można zawęzić np. do strefy; - zwykle pusty. -- `Norma` = plan, `Praca` = realizacja; nie myl `Praca(okres)` (statystyka) z `DzienPracy` (rekord dnia). -- Wynik `CzasDni.Invalid` sygnalizuje brak danych/błąd okresu — sprawdzaj zanim policzysz różnice. - ---- - -## F. RCP — rejestracja czasu pracy - -> **Dwie tabele.** Zarejestrowany (surowy) czas pracy z czytników RCP leży w -> `pracownik.DniRCP : DateSubTable` (tabela `DniRCP`). Pojedyncze zdarzenia -> wejścia/wyjścia (`Soneta.Kalend.WejscieWyjscie`, tabela `WejsciaWyjscia`) są **childem `DzienPracy`** -> (pole `WejscieWyjscie.Dzien : DzienPracy`, kolekcja `dzienPracy.WeWy`), a `DzienPracy` to -> ewidencja w `pracownik.DniPracy`. `DzienRCP` jest stanem zweryfikowanym RCP (z polem -> `StanRCP : StanWeryfikacjiRCP`), powstaje z importu/przeliczenia. - -### F1 — Rejestracja czasu pracy pracownika (★) - -**Cel:** odczytać zarejestrowany/zewidencjonowany czas pracy pracownika za dzień oraz (gdy trzeba) -utworzyć dzień ewidencji. - -**Pola i typy:** - -| Element | Lokalizacja | Typ | Uwaga | -|---|---|---|---| -| Ewidencja (kolekcja) | `pracownik.DniPracy` | `DateSubTable` | indeksator `[Date]` (get); element ctorem | -| Dzień ewidencji | `pracownik.DniPracy[data]` | `Soneta.Kalend.DzienPracy` | `null` przy braku | -| RCP zweryfikowane (kolekcja) | `pracownik.DniRCP` | `DateSubTable` | analogicznie | -| Dzień RCP | `pracownik.DniRCP[data]` | `Soneta.Kalend.DzienRCP` | `null` przy braku | -| Przepracowany czas (subrow) | `DzienPracy.Praca` / `DzienRCP.Praca` | `Soneta.Kalend.CzasPracy` | `Praca.OdGodziny`, `Praca.DoGodziny`, `Praca.Czas : Time` | -| Czas/Od (odczyt) | `Dzien*.Czas`, `Dzien*.OdGodziny` | `Soneta.Types.Time` | kalkulowane | -| Stan weryfikacji RCP | `DzienRCP.StanRCP` | `Soneta.Kalend.StanWeryfikacjiRCP` | zapisywalne | -| Flaga importu RCP | `Dzien*.RcpOK` | `bool` | zapisywalne; stan rekordu po imporcie | -| Zdarzenia we/wy dnia | `DzienPracy.WeWy` | `LpSubTable` | patrz F2 | -| Uwagi / błędy (RCP) | `DzienRCP.Uwagi`, `DzienRCP.Bledy` | `Soneta.Business.MemoText` | zapisywalne | - -**Snippet:** - -```csharp -var kalend = session.GetKadry().Session.GetKalend(); -var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; -var data = new Date(2026, 6, 1); - -// --- Odczyt zaewidencjonowanego czasu (ewidencja) --- -var dzienPracy = pracownik.DniPracy[data]; // typowane: DzienPracy lub null -if (dzienPracy is not null) -{ - Time przepracowano = dzienPracy.Praca.Czas; // suma czasu pracy dnia - Time od = dzienPracy.Praca.OdGodziny; - Time @do = dzienPracy.Praca.DoGodziny; -} - -// --- Odczyt stanu RCP (zweryfikowany rejestr) --- -var dzienRcp = pracownik.DniRCP[data]; // DzienRCP lub null -if (dzienRcp is not null) -{ - Time czasRcp = dzienRcp.Praca.Czas; - StanWeryfikacjiRCP stan = dzienRcp.StanRCP; -} - -// --- Utworzenie dnia ewidencji (gdy potrzebny ręczny wpis) --- -using (var t = session.Logout(editMode: true)) -{ - var dp = pracownik.DniPracy[data]; - if (dp is null) - { - dp = session.AddRow(new DzienPracy(pracownik, data)); // ctor (Pracownik, Date) - kalend.DniPracy.AddRow(dp); // alternatywnie przez Module.DniPracy - } - dp.Praca.OdGodziny = new Time(8, 0); - dp.Praca.DoGodziny = new Time(16, 0); - t.Commit(); -} -session.Save(); -``` - -**Pułapki:** -- `DniPracy`/`DniRCP` są **typowane** (`DateSubTable` / ``) — indeksator - `[Date]` zwraca od razu właściwy typ lub `null`. Nie iteruj całej kolekcji, by „znaleźć" dzień — - użyj indeksatora po dacie albo `[FromTo]` dla zakresu. -- Czas pracy ustawiaj na subrowie `Praca` (od–do); `Dzien*.Czas`/`Dzien*.OdGodziny` na rootcie dnia są - kalkulowane (read-only). -- `DzienRCP` to **wynik weryfikacji** importu RCP (z czytników) — w normalnym przepływie nie tworzysz - go ręcznie, lecz odczytujesz po imporcie/przeliczeniu. `DzienPracy` (ewidencja) to właściwe miejsce - na ręczny wpis. -- Świeży `DzienPracy` z `new DzienPracy(pracownik, data)` trzeba dodać do tabeli - (`Module.DniPracy.AddRow(...)` lub `session.AddRow(...)`) — sam ctor go nie rejestruje. - -### F2 — Rejestracja wejścia/wyjścia (RCP) (★) - -**Cel:** dodać zdarzenie wejścia/wyjścia do dnia oraz odczytać listę zdarzeń RCP danego dnia. - -**Pola i typy (`Soneta.Kalend.WejscieWyjscie`, tabela `WejsciaWyjscia`):** - -| Pole | Typ | Uwaga | -|---|---|---| -| `Dzien` | `Soneta.Kalend.DzienPracy` | właściciel (guided-parent); ustawiany przez ctor `(DzienPracy)` | -| `Godzina` | `Soneta.Types.Time` | godzina zdarzenia (zapisywalne) | -| `Typ` | `Soneta.Kalend.TypWejsciaWyjscia` | enum: `Niezdefiniowany`, `Wejscie`, `Wyjscie`, `WejscieSluzbowe`, `WyjscieSluzbowe`, `WejsciePrywatne`, `WyjsciePrywatne` | -| `Operacja` | `int` | kod operacji urządzenia (zapisywalne) | -| `Lp` | `int` | liczba porządkowa zdarzeń w dniu (bazodanowe) | -| `DefinicjaZdarzenia` | `Soneta.Kalend.DefinicjaZdarzeniaRCP` | opcjonalna definicja zdarzenia ze słownika `DefZdarzenRCP` | -| Kolekcja zdarzeń dnia | `DzienPracy.WeWy : LpSubTable` | uporządkowana po `Lp` | - -**Snippet:** - -```csharp -var kalend = session.GetKadry().Session.GetKalend(); -var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; -var data = new Date(2026, 6, 1); - -using (var t = session.Logout(editMode: true)) -{ - // Upewnij się, że istnieje dzień ewidencji (właściciel zdarzeń): - var dp = pracownik.DniPracy[data]; - if (dp is null) - { - dp = session.AddRow(new DzienPracy(pracownik, data)); - kalend.DniPracy.AddRow(dp); - } - - // Wejście 8:00 - var we = new WejscieWyjscie(dp); // ctor wiąże zdarzenie z dniem - kalend.WejsciaWyjscia.AddRow(we); - we.Godzina = new Time(8, 0); - we.Typ = TypWejsciaWyjscia.Wejscie; - - // Wyjście 16:00 - var wy = new WejscieWyjscie(dp); - kalend.WejsciaWyjscia.AddRow(wy); - wy.Godzina = new Time(16, 0); - wy.Typ = TypWejsciaWyjscia.Wyjscie; - - t.Commit(); -} -session.Save(); - -// --- Odczyt zdarzeń dnia --- -var dzien = pracownik.DniPracy[data]; -if (dzien is not null) -{ - foreach (WejscieWyjscie wewy in dzien.WeWy) // posortowane po Lp - { - // wewy.Godzina, wewy.Typ, wewy.Operacja - } -} -``` - -**Pułapki:** -- `WejscieWyjscie` jest childem **`DzienPracy`**, nie `DzienRCP` — najpierw potrzebujesz dnia - ewidencji (`pracownik.DniPracy[data]`); zdarzenia wiążesz ctorem `new WejscieWyjscie(dzienPracy)` - i dodajesz do tabeli `kalend.WejsciaWyjscia.AddRow(...)`. -- `Typ` to enum `TypWejsciaWyjscia` (`Wejscie`/`Wyjscie`/…), nie string ani `int`. Para - wejście+wyjście jest podstawą wyliczenia czasu dnia z surowych zdarzeń. -- `DefinicjaZdarzenia` jest **opcjonalna** — przy ręcznym wpisie wystarczą `Godzina` + `Typ`. Jeśli - używasz definicji, pobierz wpis ze słownika konfiguracyjnego `kalend.DefZdarzenRCP` (nie twórz w locie). -- `WeWy` to `LpSubTable` — kolejność zdarzeń wynika z `Lp` (nadawane automatycznie); nie ustawiaj `Lp` - ręcznie. Do usunięcia wszystkich zdarzeń dnia (przy ponownym imporcie) służy kasowanie elementów kolekcji. -- Surowe zdarzenia są przeliczane na czas pracy/RCP przez kalkulator i import — samo dodanie - wejść/wyjść nie aktualizuje automatycznie `DzienRCP` (to robi przeliczenie/import RCP). - -### F3 — Import danych z RCP (bezpośredni i przez tabelę pośrednią) - -**Cel:** wczytać surowe odbicia z czytników RCP i przeliczyć je na ewidencję/zweryfikowany RCP. -**UWAGA: operacja plikowa/sieciowa — opis modelu; samego importu z pliku/urządzenia NIE testujemy.** - -**Model danych:** - -| Element | Lokalizacja | Typ | Uwaga | -|---|---|---|---| -| Tabela pośrednia (surowe odbicia) | `DzienPracy.WeWy` | `LpSubTable` | zdarzenia we/wy (godzina, typ) — patrz F2 | -| Zarejestrowany RCP (zweryfikowany) | `pracownik.DniRCP[data]` | `Soneta.Kalend.DzienRCP` | wynik importu/przeliczenia | -| Ewidencja | `pracownik.DniPracy[data]` | `Soneta.Kalend.DzienPracy` | docelowa realizacja | -| Flaga importu | `DzienPracy.RcpOK` / `DzienRCP.RcpOK` | `bool` | „stan rekordu po imporcie z RCP" | -| Stan weryfikacji | `DzienRCP.StanRCP` | `StanWeryfikacjiRCP` | patrz F4 | - -**Workery przeliczające (po wczytaniu odbić — operują na obiektach sesji):** - -| Worker | Sygnatura | Rola | -|---|---|---| -| `Soneta.Kalend.ImportDniaWorker` | ctor `()`, `DzienPracy DzienPracy {get;set;}`, `void Przelicz()` | przelicza pojedynczy dzień z we/wy na czas pracy | -| `Soneta.Kalend.RCPWeryfikatorWorker` | `Dopasuj()`, `DopasujDlaZaznaczonych()`, `Dodaj()`, `Usun()` (+ `IsVisible*`), props `rw : RCPWeryfikator`, `Strefy`, `Wybrana` | dopasowanie odbić do plan/strefy (UI) | - -**Snippet (przeliczenie dnia z już wczytanych we/wy — bez pliku):** - -```csharp -var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; -var dzien = pracownik.DniPracy[new Date(2026, 6, 1)]; // dzień z wpisanymi WeWy (F2) -if (dzien is not null) -{ - using (var t = session.Logout(editMode: true)) - { - new ImportDniaWorker { DzienPracy = dzien }.Przelicz(); // we/wy -> czas pracy - t.Commit(); - } - session.Save(); -} -``` - -**Pułapki / wykonalność:** -- **Sam import z pliku/urządzenia (czytnik, sieć, format) jest poza zakresem testu** — wymaga - zewnętrznego źródła (plik/serwis), brak czystego API w tym kontrakcie. -- **Testowalny fragment**: przygotowanie tabeli pośredniej `DzienPracy.WeWy` (ctor `WejscieWyjscie(dp)`, - patrz F2) + `ImportDniaWorker.Przelicz()` — to przelicza już-wczytane odbicia bez I/O. -- `RCPWeryfikatorWorker` jest mocno UI (metody `IsVisible*`, `Strefy`/`Wybrana`) — to dopasowanie - ręczne; nie wywoływać z kodu biznesowego. -- `DzienRCP` powstaje z importu/przeliczenia — w teście nie twórz go „z palca"; odczytuj po `Przelicz()`. - ---- - -### F4 — Weryfikacja i korekta danych RCP (★ testowalne) - -**Cel:** odczytać i skorygować zweryfikowany rekord RCP — zmienić stan weryfikacji oraz poprawić -godziny pracy / opisać błędy i uwagi. - -**Pola `Soneta.Kalend.DzienRCP` (tabela `DniRCP`, child `Pracownik`):** - -| Pole | Typ | Rodzaj | Uwaga | -|---|---|---|---| -| `Data` | `Soneta.Types.Date` | bazodanowe | data dnia (ctor) | -| `Pracownik` | `Soneta.Kadry.Pracownik` | bazodanowe, guided-parent | właściciel | -| `Praca` | `Soneta.Kalend.CzasPracy` | bazodanowe | `Praca.OdGodziny`/`Praca.DoGodziny`/`Praca.Czas : Time` (zapisywalne) | -| `Czas`, `OdGodziny` | `Soneta.Types.Time` | kalkulowane | read-only (z `Praca`) | -| `StanRCP` | `Soneta.Kalend.StanWeryfikacjiRCP` | bazodanowe | stan weryfikacji (zapisywalne) | -| `RcpOK` | `bool` | bazodanowe | stan rekordu po imporcie (zapisywalne) | -| `Uwagi` | `Soneta.Business.MemoText` | bazodanowe | uwagi do weryfikacji | -| `Bledy` | `Soneta.Business.MemoText` | bazodanowe | opis błędów | -| `Strefy` | `SubTable` | | strefy zarejestrowane | -| `StrefyOrg` | `Soneta.Business.MemoText` | bazodanowe | strefy źródłowe (org.) | - -**`Soneta.Kalend.StanWeryfikacjiRCP` (enum):** `DoWeryfikacji`, `WymagaWeryfikacji`, -`PrzekazanyDoWyjaśnienia`, `DoZatwierdzenia`, `Modyfikowany`, `Naniesiony`, `Poprawny`, `Błędny`, -`Wszystkie`. - -**Snippet (korekta godzin + zmiana stanu):** - -```csharp -var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; -var dzienRcp = pracownik.DniRCP[new Date(2026, 6, 1)]; // DzienRCP lub null -if (dzienRcp is not null) -{ - using (var t = session.Logout(editMode: true)) - { - dzienRcp.Praca.OdGodziny = new Time(8, 0); // korekta na subrowie Praca - dzienRcp.Praca.DoGodziny = new Time(16, 0); - dzienRcp.StanRCP = StanWeryfikacjiRCP.Poprawny; // zatwierdzenie weryfikacji - dzienRcp.Uwagi = (MemoText)"Skorygowano wyjście"; - t.Commit(); - } - session.Save(); -} -``` - -**Pułapki:** -- `DniRCP` jest **typowane** (`DateSubTable`) — indeksator `[Date]` zwraca `DzienRCP`/`null`; - do zakresu użyj `[FromTo]`. Nie iteruj kolekcji w poszukiwaniu dnia. -- Godziny koryguj na **subrowie `Praca`** (`Praca.OdGodziny`/`DoGodziny`); `DzienRCP.Czas`/`OdGodziny` - na rootcie są kalkulowane (read-only). -- `StanRCP` to enum `StanWeryfikacjiRCP` — nie string. Zmiana stanu może podlegać weryfikatorom. -- W Demo `DzienRCP` istnieje tylko gdy był import/przeliczenie — test korekty zakłada istniejący dzień - (sprawdzaj `is not null`), nie twórz `DzienRCP` ręcznie. - ---- - -### F5 — Rozliczenie pracy hybrydowej / aktualizacja podzielników na podstawie pracy hybrydowej - -**Cel:** rozliczyć czas pracy hybrydowej (podział na strefy: stacjonarna / praca zdalna / zdalna -okazjonalna) i zaktualizować podzielniki (elementy rozliczenia czasu pracy / strefy dnia), na -podstawie których naliczane są składniki płacowe i koszty. - -**Model danych (publiczny kontrakt):** - -| Element | Lokalizacja | Typ | Uwaga | -|---|---|---|---| -| Strefy pracy dnia | `DzienPracy.Strefy` | `SubTable` | podział dnia na strefy | -| Strefa pracy | `Soneta.Kalend.StrefaPracy` (ctor `(DzienPracy dzien)`) | — | `Definicja : DefinicjaStrefy`, `CzasRozliczany : Time`, `OdGodziny`/`Czas` (kalk.) | -| Definicja strefy | `Soneta.Kalend.DefinicjaStrefy` | konfiguracja | `Typ : TypStrefy`, `Wchodzi`, `Rozliczana`; stałe `Praca_Zdalna`, `PracaZdalnaOkazjonalna : Guid` | -| Dokumenty rozliczenia | `pracownik.RozliczeniaCzasuPracy` | `SubTable` | dokumenty rozliczenia czasu (podzielniki) | -| Elementy rozliczenia | `pracownik.ElementyRozliczeniaCzasuPracy` | `SubTable` | pozycje podzielnika | -| Dokument rozliczenia | `Soneta.Kalend.RozliczenieCzasuPracy` (ctor `(Pracownik, DefinicjaRozliczeniaCzasuPracy)`) | root | `Data`, `Seria`, `Stan : StanyRozliczeniaCzasuPracy` | -| Pozycja rozliczenia | `Soneta.Kalend.ElementRozliczeniaCzasuPracy` (ctor `(RozliczenieCzasuPracy dokument)`) | child | `Definicja : DefinicjaStrefy`, `Data`, `OdGodziny`, `Czas`, `CzasPozostały`/`CzasDostępny`/`Zrealizowane` (kalk.) | - -**`Soneta.Kalend.TypStrefy` (enum):** `NieWplywa`, `Zwieksza`, `Zmniejsza`. - -**Workery (UI/extendery — praca zdalna/hybrydowa):** -- `Soneta.Kalend.StrefaPracy.PracaZdalnaWorker` (`Strefa : StrefaPracy`) — oznaczenie strefy jako - praca zdalna. -- `Soneta.Kadry.PracaZdalna.DzienStrefaExtWorker`, `ElementRozliczeniaCzasuPracyExtWorker`, - `DzienZestawienieExtender` — extendery zestawień/dni dla pracy zdalnej. - -**Snippet (odczyt rozkładu na strefy + dokument rozliczenia):** - -```csharp -var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; - -// Odczyt podziału dnia na strefy (stacjonarna / zdalna): -var dzien = pracownik.DniPracy[new Date(2026, 6, 1)]; -if (dzien is not null) -{ - foreach (StrefaPracy s in dzien.Strefy) - { - DefinicjaStrefy def = s.Definicja; // strefa (np. praca zdalna) - Time rozliczany = s.CzasRozliczany; // czas rozliczany w strefie - } -} - -// Pozycje podzielnika (elementy rozliczenia czasu pracy): -foreach (ElementRozliczeniaCzasuPracy el in pracownik.ElementyRozliczeniaCzasuPracy) -{ - DefinicjaStrefy def = el.Definicja; - Time czas = el.Czas; -} -``` - -**Pułapki / wykonalność:** -- Rozkład pracy hybrydowej to **strefy** (`DzienPracy.Strefy` / `DefinicjaStrefy` z flagą zdalna) + - dokument `RozliczenieCzasuPracy` z pozycjami `ElementRozliczeniaCzasuPracy` (podzielniki). -- `RozliczenieCzasuPracy` to **root** (ctor `(Pracownik, DefinicjaRozliczeniaCzasuPracy)`) — utworzenie - wymaga istniejącej `DefinicjaRozliczeniaCzasuPracy` z konfiguracji; pozycje ctorem - `new ElementRozliczeniaCzasuPracy(dokument)`. Czysty odczyt jest bezpieczny i bez transakcji. -- Aktualizacja podzielników na podstawie pracy hybrydowej przebiega **głównie przez extendery/UI** - (`DzienStrefaExtWorker`, `ElementRozliczeniaCzasuPracyExtWorker`, `PracaZdalnaWorker`) zależne od - `Context`/wniosków e-pracownika — brak prostego, czystego API operacyjnego. -- Wymaga skonfigurowanych `DefinicjaStrefy` (Praca_Zdalna / PracaZdalnaOkazjonalna) — w Demo strefy - mogą nie być włączone, co czyni budowę rozliczenia kruchą do testu (raczej odczyt niż zapis). - -## G. Umowy cywilnoprawne - -### G1 — Dodawanie umów cywilnoprawnych (zlecenie, o dzieło) (★) - -**Cel:** utworzyć dla pracownika umowę cywilnoprawną (zlecenie / o dzieło / ryczałtowa) z kompletem -danych pozwalającym na `Session.Save()`: definicja elementu płacowego (rodzaj umowy), okres, wartość, -sposób rozliczenia i typ wartości (brutto/netto). - -**Mechanizm (kluczowy):** `Soneta.Kadry.Umowa` to **root historyczny** (tabela `Umowy`), child -pracownika. Dodanie umowy do tabeli (`Module.Umowy.AddRow(umowa)`) w `OnAdded` **automatycznie tworzy -pierwszy zapis** `Soneta.Kadry.UmowaHistoria` (tabela `UmowaHistorie`) oraz domyślną definicję -elementu, okres, datę i numerację. Dlatego **nie tworzymy `UmowaHistoria` ręcznie** — bezpośrednio po -`AddRow` istnieje już `umowa.Last` (pierwszy zapis), na którym ustawiamy **wartość umowy**. - -> **Gdzie co siedzi.** Dane „nagłówkowe" umowy (definicja elementu, okres, sposób rozliczenia, typ -> wartości) są na **roocie** `Umowa`. **Kwota/wartość umowy** jest **historyczna** i siedzi na -> `UmowaHistoria.Wartosc` — ustawiasz ją przez `umowa.Last.Wartosc`. Property `umowa.Brutto`/ -> `umowa.Wartosc` na roocie oraz `UmowaHistoria.Brutto` są **wyliczane** (read-only). - -**Warianty (rodzaj umowy = `DefinicjaElementu`):** - -| Rodzaj umowy | Jak wskazać definicję | -|---|---| -| Zlecenie | `DefElementow[DefinicjaElementu.UmowaZlecenie]` (to też wartość domyślna konfiguracji) | -| O dzieło (20% KUP) | `DefElementow[DefinicjaElementu.Umowa20]` | -| Ryczałtowa | `DefElementow[DefinicjaElementu.UmowaRyczałtowa]` | -| Inna (po kodzie/nazwie) | `DefElementow[""]` (indeksator string = wg kodu) — pod warunkiem że jej `RodzajZrodla == RodzajŹródłaWypłaty.Umowa` | - -**Pola i typy:** - -| Pole | Gdzie | Typ | Uwaga | -|---|---|---|---| -| `Element` | `Umowa` (root) | `Soneta.Place.DefinicjaElementu` | definicja elementu = rodzaj umowy; akceptowana tylko gdy `RodzajZrodla == RodzajŹródłaWypłaty.Umowa` | -| `Data` | `Umowa` (root) | `Soneta.Types.Date` | data zawarcia/dokumentu | -| `Okres` | `Umowa` (root) | `Soneta.Types.FromTo` | okres obowiązywania umowy | -| `Tytul` | `Umowa` (root) | `string` | tytuł/temat umowy | -| `RodzajRozliczenia` | `Umowa` (root) | `Soneta.Kadry.RodzajeRozliczeniaUmowy` | `KwotaDoWypłaty` / `StawkaZaOkres` / `StawkaZaGodzinę` | -| `TypWartosci` | `Umowa` (root) | `Soneta.Kadry.TypWartosciUmowy` | `Brutto` / `Netto` | -| `Wydzial` | `Umowa` (root) | `Soneta.Kadry.Wydzial` | jednostka organizacyjna — **wymagana** (weryfikator przy `Save()`); ustaw `kadry.Wydzialy.Firma` | -| `Wartosc` | `UmowaHistoria` (`Last`) | `Soneta.Types.Currency` | **kwota/wartość umowy** — ustawiana na zapisie historycznym | - -**Snippet:** - -```csharp -var kadry = session.GetKadry(); -var place = session.GetPlace(); -var pracownik = kadry.Pracownicy.WgKodu["006"]; - -using (var t = session.Logout(editMode: true)) -{ - // 1) Utworzenie umowy + dodanie do tabeli; w OnAdded powstaje pierwszy UmowaHistoria. - var umowa = session.AddRow(new Umowa(pracownik)); - - // 2) Definicja elementu = rodzaj umowy (tu: zlecenie). Indeksator Guid + stała publiczna. - umowa.Element = place.DefElementow[DefinicjaElementu.UmowaZlecenie]; - - // 3) Dane nagłówkowe na roocie: - umowa.Data = new Date(2026, 1, 1); - umowa.Okres = new FromTo(new Date(2026, 1, 1), new Date(2026, 12, 31)); - umowa.Tytul = "Umowa zlecenie - obsługa projektu"; - umowa.RodzajRozliczenia = RodzajeRozliczeniaUmowy.KwotaDoWypłaty; - umowa.TypWartosci = TypWartosciUmowy.Brutto; - umowa.Wydzial = kadry.Wydzialy.Firma; // wymagane przy Save() - - // 4) KWOTA umowy — na zapisie historycznym Last (UmowaHistoria.Wartosc): - umowa.Last.Wartosc = new Currency(5000m); - - t.Commit(); // Commit() w kodzie biznesowym; CommitUI() w workerze/UI -} -session.Save(); // tu wykrywane konflikty/duplikaty -``` - -**Pułapki:** -- **Nie** twórz ręcznie pierwszego `UmowaHistoria` — robi to `OnAdded` przy `AddRow`. Ręczny nowy - zapis dotyczy dopiero *zmiany/aneksu* „od daty" (G2). -- **Kwotę ustawiaj na `umowa.Last.Wartosc`**, nie na roocie — `umowa.Brutto`/`umowa.Wartosc` oraz - `Last.Brutto` są wyliczane (read-only). `Wartosc` to `Soneta.Types.Currency`, nie `decimal` - (safe-code §10.1). -- `Element` przyjmie tylko definicję o `RodzajZrodla == RodzajŹródłaWypłaty.Umowa`. Definicje - „etatowe" (np. `EtatMies`) zostaną zignorowane (umowa dostanie domyślną definicję z konfiguracji). -- Jeśli `Element` **nie** zostanie ustawiony, umowa przyjmuje - `Module.Config.Ogólne.DomyślnaDefinicjaUmowy` (domyślnie = `UmowaZlecenie`). -- Dodanie umowy do pracownika **w archiwum** rzuca wyjątek (`WArchiwumException`) z `OnAdded`. -- `RodzajRozliczenia`/`TypWartosci` bywają w UI tylko-do-odczytu zależnie od definicji elementu — - w kodzie biznesowym ustawiasz je wprost (nie używaj `IsReadOnlyXxx`, safe-code §7.1). -- Całość w transakcji (`session.Logout(editMode: true)`); brak `Commit()` = rollback przy `Dispose()`. - -### G2 — Zmiana/aneks umowy (★) - -**Cel:** zmienić warunki istniejącej umowy. Trzy rozłączne przypadki: **(a) korekta** danych -nagłówkowych w bieżącym okresie; **(b) aneks „od daty"** — nowy zapis historyczny `UmowaHistoria` -obowiązujący od wskazanego dnia (analogicznie do `PracHistoria`, sekcja A14); **(c) seryjna -aktualizacja stawki** workerem. - -**Mechanizm `HistorySubTable`** (`umowa.Historia`): - -| Operacja | API | Efekt | -|---|---|---| -| Odczyt zapisu na dzień | `umowa[date]` (== `(UmowaHistoria)Historia[date]`) | zapis, którego `Aktualnosc` zawiera `date` | -| Ostatni (bieżący) zapis | `umowa.Last` (== `Historia.GetPrev()`) | najświeższy zapis | -| Pierwszy zapis | `umowa.Historia.GetFirst()` | najstarszy zapis | -| **Nowy zapis „od daty"** | `(UmowaHistoria)umowa.Historia.Update(date)` | **klonuje** zapis aktualny na `date`, skraca stary do `date-1`, zwraca **nowy** klon (okres od `date`); klon trzeba dodać do tabeli | -| Okres obowiązywania | `UmowaHistoria.Aktualnosc: FromTo` | „od–do" zapisu (zarządzany przez historię) | - -**(a) Korekta danych nagłówkowych umowy (bez nowego okresu):** - -```csharp -var umowa = pracownik.Umowy.First(); // lub wyszukanie po polu/numerze - -using (var t = session.Logout(editMode: true)) -{ - umowa.Tytul = "Umowa zlecenie - aneks zakresu prac"; - umowa.Okres = new FromTo(umowa.Okres.From, new Date(2027, 6, 30)); // przedłużenie - t.Commit(); -} -session.Save(); -``` - -**(b) Aneks „od daty" — zmiana wartości umowy nowym zapisem historycznym:** - -```csharp -var odDnia = new Date(2026, 7, 1); - -using (var t = session.Logout(editMode: true)) -{ - // 1) Update klonuje zapis aktualny na `odDnia` i zwraca nowy klon (okres od `odDnia`); - // stary zapis zostaje skrócony do dnia poprzedniego. - var nowy = (UmowaHistoria)umowa.Historia.Update(odDnia); - - // 2) Klon trzeba dodać do tabeli zapisów historii umowy. - umowa.Module.UmowaHistorie.AddRow(nowy); - - // 3) Na nowym zapisie ustawiamy zmienioną wartość (od `odDnia`): - nowy.Wartosc = new Currency(6000m); - // Uwaga: UmowaHistoria.PowodAktualizacji jest tylko do odczytu (ustawiane wewnętrznie) — nie przypisuj. - - t.Commit(); -} -session.Save(); -``` - -**(c) Seryjna aktualizacja stawki — worker `Umowa.AktualizacjaStawkiWorker`:** - -```csharp -var worker = new Umowa.AktualizacjaStawkiWorker -{ - Umowy = new[] { umowa }, - Pars = ... // Umowa.AktualizacjaStawkiWorker.Params: Data: Date, Wartosc: Currency -}; -// uruchomienie zgodnie z konwencją workerów (patrz worker-extender.md) -``` - -> Pokrewne workery (wywoływane jak każdy worker enova): `Umowa.KopiujUmowe2Worker` -> (`Umowa Umowa` — kopiuje umowę), `Umowa.WyrejestrujUmoweWorker` (wyrejestrowanie umowy). - -**Pułapki:** -- **`Update(date)` + `UmowaHistorie.AddRow(nowy)` to nierozłączna para.** Sam `Update` tworzy - „odpięty" klon — bez `AddRow` zmiana nie zostanie zapisana. -- `Update(date)` rzuca wyjątek duplikatu, gdy na `date` już zaczyna się zapis (`Aktualnosc.From == date`) - — nie da się „aktualizować" dwa razy tego samego dnia; wtedy modyfikuj istniejący zapis (`umowa[date]`). -- **Korekta** (`umowa.Last.Wartosc = …` lub modyfikacja `umowa[date]`) zmienia dane w **całym** okresie - tego zapisu — używaj jej do poprawy błędu, nie do „zmiany od dnia"; do zmiany od dnia → wariant (b). -- `Aktualnosc` (okres zapisu) jest zarządzany przez historię — **nie ustawiaj go ręcznie**; do - skrócenia/wstawienia okresu służy `Update`. -- Wartość zawsze jako `Soneta.Types.Currency`, nie `decimal` (safe-code §10.1); daty jako - `Soneta.Types.Date`/`Date.Today` (§10.2). -- Obsłuż `RowConflictException` z `Save()` (safe-code §4); transakcje trzymaj krótkie (§13.1). - -### G3 — Operacja seryjna „Dodaj umowy" dla grupy osób (★) - -**Cel:** dodać jednakową umowę cywilnoprawną (zlecenie / o dzieło / ryczałtowa) **naraz dla wielu -zaznaczonych pracowników** — operacja seryjna z listy osób. W UI: menu *Operacje seryjne → Dodaj -umowy…*. Każdej osobie z zaznaczenia tworzona jest osobna `Umowa` z tymi samymi danymi nagłówkowymi -(definicja elementu, okres, wartość, sposób rozliczenia), analogicznie do G1. - -**Worker (publiczny kontrakt):** `Soneta.Kadry.Pracownik.DodajUmowęWorker` — worker **przypisany do -`Pracownik`** (`DataType = Pracownik`). Udostępnia akcję `DodajUmowę` w menu czynności listy -pracowników. - -| Składowa | Typ / sygnatura | Uwaga | -|---|---|---| -| Konstruktor | `DodajUmowęWorker(Session session)` | worker ma **ctor z `Session`** (nie bezparametrowy) | -| Zaznaczone osoby | `DodajUmowęWorker.Pracownicy: Pracownik[]` | `[Context]` — tablica pracowników z zaznaczenia listy | -| Parametry | `DodajUmowęWorker.Pars: DodajUmowęWorker.Params` | `[Context]` — okno parametrów operacji; `Params(Context)` | -| Akcja | `void DodajUmowę()` | tworzy umowy dla wszystkich `Pracownicy` (zwraca **`void`**, nie `object`) | - -**Parametry operacji (`DodajUmowęWorker.Params`):** - -| Pole | Typ | Odpowiednik na `Umowa` (G1) | -|---|---|---| -| `Definicja` | `Soneta.Core.DefinicjaDokumentu` | definicja dokumentu umowy (numeracja/seria) | -| `Seria` | `string` | seria numeracji | -| `Wydział` | `Soneta.Kadry.Wydzial` | `Umowa.Wydzial` (wymagany) | -| `Data` | `Soneta.Types.Date` | `Umowa.Data` (data zawarcia) | -| `Okres` | `Soneta.Types.FromTo` | `Umowa.Okres` | -| `Tytuł` | `string` | `Umowa.Tytul` | -| `Element` | `Soneta.Place.DefinicjaElementu` | `Umowa.Element` (rodzaj umowy) | -| `RodzajRozliczenia` | `Soneta.Kadry.RodzajeRozliczeniaUmowy` | `Umowa.RodzajRozliczenia` | -| `Wartość` | `Soneta.Types.Currency` | `umowa.Last.Wartosc` (kwota umowy) | -| `TypWartości` | `Soneta.Kadry.TypWartosciUmowy` | `Umowa.TypWartosci` (`Brutto`/`Netto`) | - -**Wariant A — wywołanie workera platformy (zalecane):** zainicjuj `DodajUmowęWorker`, ustaw -`Pracownicy` i `Pars`, wywołaj `DodajUmowę()` (worker uruchamia się jak każdy worker — patrz -[`worker-extender.md`](../worker-extender.md), sekcja *Programowe użycie workera*). - -```csharp -var kadry = session.GetKadry(); -var place = session.GetPlace(); - -// Grupa osób (np. z zaznaczenia listy). Tu: kilku pracowników po kodzie: -var osoby = new[] -{ - kadry.Pracownicy.WgKodu["006"], - kadry.Pracownicy.WgKodu["007"], - kadry.Pracownicy.WgKodu["008"], -}; - -var pars = new Pracownik.DodajUmowęWorker.Params(context); -pars.Element = place.DefElementow[DefinicjaElementu.UmowaZlecenie]; -pars.Okres = new FromTo(new Date(2026, 1, 1), new Date(2026, 12, 31)); -pars.Data = new Date(2026, 1, 1); -pars.Tytuł = "Umowa zlecenie - projekt grupowy"; -pars.RodzajRozliczenia = RodzajeRozliczeniaUmowy.KwotaDoWypłaty; -pars.TypWartości = TypWartosciUmowy.Brutto; -pars.Wartość = new Currency(4000m); -pars.Wydział = kadry.Wydzialy.Firma; // wymagany - -// Worker ma konstruktor z Session (nie bezparametrowy); Pracownicy/Pars przez inicjalizator: -var worker = new Pracownik.DodajUmowęWorker(session) { Pracownicy = osoby, Pars = pars }; -worker.DodajUmowę(); // void -session.Save(); -``` - -**Wariant B — pętla po pracownikach (jawne tworzenie, jak G1):** gdy nie chcesz przechodzić przez -worker — dla każdej osoby twórz `new Umowa(p)` i ustaw te same pola co w G1. To jawnie pokazuje, że -operacja seryjna = G1 powtórzone w pętli. - -```csharp -var kadry = session.GetKadry(); -var place = session.GetPlace(); -var defElementu = place.DefElementow[DefinicjaElementu.UmowaZlecenie]; -var okres = new FromTo(new Date(2026, 1, 1), new Date(2026, 12, 31)); - -using (var t = session.Logout(editMode: true)) -{ - foreach (var p in osoby) - { - var umowa = session.AddRow(new Umowa(p)); // OnAdded tworzy pierwszy UmowaHistoria - umowa.Element = defElementu; - umowa.Data = okres.From; - umowa.Okres = okres; - umowa.Tytul = "Umowa zlecenie - projekt grupowy"; - umowa.RodzajRozliczenia = RodzajeRozliczeniaUmowy.KwotaDoWypłaty; - umowa.TypWartosci = TypWartosciUmowy.Brutto; - umowa.Wydzial = kadry.Wydzialy.Firma; // wymagany przy Save() - umowa.Last.Wartosc = new Currency(4000m); // kwota na zapisie historycznym - } - t.Commit(); // Commit() w kodzie biznesowym; CommitUI() w workerze/UI -} -session.Save(); -``` - -**Pułapki:** -- `Pracownik.DodajUmowęWorker` jest workerem na typie `Pracownik`, a tworzy obiekty `Umowa` — nie myl - go z workerami na `Umowa` (G2: `AktualizacjaStawkiWorker`, `KopiujUmowe2Worker`). -- W wariancie B obowiązują wszystkie pułapki G1: kwota na `umowa.Last.Wartosc` (root `Brutto`/`Wartosc` - są wyliczane), `Element` tylko o `RodzajZrodla == RodzajŹródłaWypłaty.Umowa`, `Wydzial` wymagany, - dodanie umowy pracownikowi **w archiwum** rzuca `WArchiwumException`. -- Pętlę edycyjną trzymaj krótką (safe-code §13.1); konflikty/duplikaty wykrywane w `Save()` (§4). -- Wartość zawsze jako `Soneta.Types.Currency`, daty jako `Date`/`FromTo`, nie `decimal`/`DateTime` - (safe-code §10). - ---- - -### G4 — Rachunek do umowy zlecenia (★) - -**Cel:** wystawić **rachunek do umowy zlecenia** — czyli rozliczyć (naliczyć i wypłacić) umowę -cywilnoprawną. W modelu Soneta „rachunek do umowy zlecenia" **nie jest osobnym rekordem** na `Umowa` -ani w `pracownik.Rachunki` — to **wypłata z umowy** typu `Soneta.Place.WyplataUmowa`, naliczana -mechanizmem płac (jak H2). `pracownik.Rachunki: SubTable` to -**rachunki bankowe** pracownika (numer konta), a nie rachunki do umów — nie myl tych pojęć. - -> **Gdzie to siedzi.** Każda umowa ma wstecz powiązane rozliczenia/wypłaty: -> - `Umowa.RozliczeniaWynagrodzenia: LpSubTable` — rozliczenia -> wynagrodzenia z umowy, -> - `Umowa.Elementy: SubTable` — naliczone składniki wypłat tej umowy, -> - sama wypłata to `Soneta.Place.WyplataUmowa` (konkretny typ `Wyplata`), z polem zwrotnym -> `WyplataUmowa.Umowa: Soneta.Kadry.Umowa`. -> Stan rozliczenia umowy odczytasz z `Umowa.Stan: Soneta.Kadry.StanUmowy` -> (`Niewypłacona` / `WypłaconaCzęściowo` / `WypłaconaCałkowicie` / `Anulowana`) oraz z -> `Umowa.Splacono`, `Umowa.Pozostało` (`Soneta.Types.Currency`). - -**Tworzenie rachunku (wypłaty) do umowy — wykonawca naliczania `NaliczanieSeryjne.Umowy` (jak H2):** - -| Element | Typ / sygnatura | Uwaga | -|---|---|---| -| Parametry | `new NaliczanieSeryjne.UmowaParams(context)` | `Naliczanie` na sztywno `PłatnaZDołu` (setter rzuca `NotSupportedException`) | -| Data rachunku / listy | `UmowaParams.DataWypłaty`, `.DataListy` | daty rachunku | -| Wykonawca | `new NaliczanieSeryjne.Umowy(UmowaParams) { Umowa = umowa }` | ustawienie `Umowa` ustawia też `Pracownik` z `umowa.Pracownik` | -| Uruchomienie | `Umowy.Nalicz(): NaliczanieWypłat` | tworzy `WyplataUmowa` i liczy składniki | -| Wynik | `NaliczanieWypłat.WszystkieWypłaty: IList` | elementy `Wyplata` (tu `WyplataUmowa`) | - -```csharp -var kadry = session.GetKadry(); -var pracownik = kadry.Pracownicy.WgKodu["006"]; - -// Umowa zlecenie pracownika (np. utworzona w G1): -Umowa umowa = pracownik.Umowy.Cast() - .First(u => u.Element == session.GetPlace().DefElementow[DefinicjaElementu.UmowaZlecenie]); - -var pars = new NaliczanieSeryjne.UmowaParams(context); -pars.DataWypłaty = new Date(2026, 2, 10); // data wystawienia rachunku -pars.DataListy = pars.DataWypłaty; - -var naliczanie = new NaliczanieSeryjne.Umowy(pars) { Umowa = umowa }; -NaliczanieWypłat wynik = naliczanie.Nalicz(); // tworzy WyplataUmowa (rachunek) - -foreach (Wyplata w in wynik.WszystkieWypłaty) -{ - // w.Typ == TypWyplaty.Umowa; w to WyplataUmowa; w.Elementy = składniki rachunku -} -session.Save(); - -// Po naliczeniu — stan rozliczenia umowy: -StanUmowy stan = umowa.Stan; // np. WypłaconaCałkowicie -Currency splacono = umowa.Splacono; // kwota rozliczona -Currency pozostalo = umowa.Pozostało; // pozostała do wypłaty -``` - -**Odczyt rachunków (wypłat) wystawionych do umowy:** - -```csharp -// Wypłaty (rachunki) tej umowy — przez wypłaty pracownika filtrowane po umowie: -foreach (WyplataUmowa w in pracownik.Wyplaty.OfType().Where(x => x.Umowa == umowa)) -{ - // w.Data, w.Elementy (WypElement.Wartosc / .Netto / .Podatki.*) -} - -// Składniki naliczone bezpośrednio z umowy: -foreach (WypElement e in umowa.Elementy) -{ - // e.Wartosc, e.Netto -} -``` - -**Pułapki:** -- „Rachunek do umowy zlecenia" = `WyplataUmowa`, a nie rekord w `pracownik.Rachunki` (to rachunki - bankowe). Tworzysz go naliczaniem (`NaliczanieSeryjne.Umowy.Nalicz()`), nie `AddRow` po wypłacie. -- **Nie ustawiaj `UmowaParams.Naliczanie`** — umowy są zawsze „płatne z dołu" (setter rzuca - `NotSupportedException`). -- Ustawienie `Umowy.Umowa` nadpisuje `Pracownik` właścicielem umowy — nie ustawiaj `Pracownik` ręcznie. -- `Nalicz()` wewnętrznie otwiera własną transakcję i zatwierdza zmiany w sesji — po nim wołasz tylko - `Session.Save()`; nie owijaj go w dodatkowy `Logout(editMode: true)`. -- `Wyplata` nie ma agregatów `Brutto`/`Netto` — sumuj składniki z `Wyplata.Elementy` (jak w H2/H4). -- Kwoty jako `Soneta.Types.Currency`, daty jako `Date` (safe-code §10). - ---- - -### G5 — Zgłoszenia ZUS zleceniobiorców (ZUA / ZZA na podstawie umowy) (★) - -**Cel:** przygotować zgłoszenie zleceniobiorcy do ZUS — **ZUA** (zgłoszenie do ubezpieczeń -społecznych + zdrowotnego) albo **ZZA** (tylko zdrowotne) — **na podstawie schematu ubezpieczeń -umowy**, oraz wyrejestrowanie (**ZWUA**) po jej zakończeniu. O tym, czy powstaje ZUA czy ZZA, decyduje -**schemat ubezpieczeń zapisu umowy**, a nie odrębne pole „rodzaj zgłoszenia". - -> **Schemat ubezpieczeń umowy — gdzie siedzi.** Ubezpieczenia umowy są **historyczne** i leżą na -> zapisie `UmowaHistoria.Ubezpieczenia: Soneta.Kadry.Ubezpieczenia` (analogicznie do -> `PracHistoria.Etat.Ubezpieczenia` z A7; ta sama struktura `Spoleczne`/`Zdrowotne`). Z roota dostępne -> przez `umowa.Last.Ubezpieczenia` (oraz `umowa.Ubezpieczenia` jako delegat). Kluczowe pola: -> - `Ubezpieczenia.Tyub4: Soneta.Kadry.TytulUbezpieczenia4` — **tytuł ubezpieczenia** (decyduje o ZUA -> vs ZZA); pobierany ze słownika `session.GetKadry().TytulyUbezpiecz4.WgKodu[int]`, -> - `Ubezpieczenia.ObowiazkoweOd: Date` — data objęcia ubezpieczeniami społecznymi obowiązkowymi, -> - `Ubezpieczenia.Emerytalne` / `Rentowe` / `Chorobowe` / `Wypadkowe : Soneta.Kadry.Spoleczne` — -> poszczególne społeczne (`Obowiazkowe`, `Dobrowolne`, `DobrowolneOd`, `Do`; `Od` read-only), -> - `Ubezpieczenia.Zdrowotne: Soneta.Kadry.Zdrowotne` — zdrowotne (`ObowiazkoweOd` zapisywalne). -> -> **Reguła ZUA vs ZZA:** zleceniobiorca podlegający ubezpieczeniom **społecznym** (emerytalne/rentowe -> obowiązkowe) → **ZUA**; podlegający **tylko zdrowotnemu** (np. uczeń/student do 26 r.ż., zbieg -> tytułów) → **ZZA**. Worker rozpoznaje to automatycznie po schemacie `UmowaHistoria.Ubezpieczenia`. -> -> **Uwaga (zweryfikowane testem):** świeży zapis ubezpieczeń umowy zlecenie ma **domyślnie** -> `Emerytalne.Obowiazkowe == true` i `Rentowe.Obowiazkowe == true` (schemat ZUA). Aby uzyskać **ZZA**, -> trzeba je **jawnie wyłączyć** (`ub.Emerytalne.Obowiazkowe = false; ub.Rentowe.Obowiazkowe = false;`) -> — samo ustawienie `Zdrowotne.ObowiazkoweOd` nie wystarcza. - -**Worker (publiczny kontrakt):** `Soneta.Deklaracje.ZUS.ZarejestrujUmowyWorker` — worker -**przypisany do `Umowa`** (`DataType = Umowa`). Operuje na zaznaczonych umowach i generuje deklaracje -zgłoszeniowe ZUS. W UI: menu czynności listy umów *Deklaracje ZUS → Przygotuj ZUA i ZZA* (oraz -wyrejestrowanie). - -| Składowa | Typ / sygnatura | Uwaga | -|---|---|---| -| Worker | `ZarejestrujUmowyWorker()` | ctor **bezparametrowy**; `Umowy: Umowa[]` jest **set-only** (ustaw przez inicjalizator) | -| Zaznaczone umowy | `ZarejestrujUmowyWorker.Umowy: Umowa[]` | `[Context]` — umowy do zgłoszenia (write-only) | -| Akcja: zgłoszenie | `object ZarejestrujUmowyWorker.Rejestracja.ZarejestrujUmowy()` | tworzy ZUA/ZZA (i ZCNA dla rodziny — `Pars.ZarejestrujRodzinę`); `Rejestracja()` ctor bezparam. | -| Akcja: wyrejestrowanie | `object ZarejestrujUmowyWorker.Wyrejestrowanie.WyrejestrujUmowy()` | tworzy ZWUA | -| Parametry zgłoszenia | `Rejestracja.Pars: ParamsZ` | **set-only**; `ParamsZ(Context)`; pola bazowe `Okres`/`DataDokumentu`/`DataWypełnienia`/`Kedu` (write-only) + własne `ZarejestrujRodzinę: bool` | -| Parametry wyrejestrowania | `Wyrejestrowanie.Pars: ParamsW` | **set-only**; `ParamsW(Context)`; `Okres`/`DataDokumentu`/`DataWypełnienia`/`Kedu` + `WyrejestrujRodzinę: bool` | - -**Wspólny kontrakt bazowy `ZarejestrujBaseWorker`** (do odczytu wyniku i sterowania okresem): - -| Pole / metoda | Typ / sygnatura | Uwaga | -|---|---|---| -| `Okres` | `Soneta.Types.FromTo` | okres deklaracji | -| `DataDokumentu`, `DataWypełnienia` | `Soneta.Types.Date` | daty na dokumencie | -| `KEDU` | `Soneta.Deklaracje.ZUS.KEDU` | zestaw dokumentów ZUS, do którego trafiają wygenerowane bloki | -| `Deklaracje` | `System.Collections.Generic.IList` | wygenerowane deklaracje (do odczytu po akcji) | -| `CzyJestZUA()`, `CzyJestZZA()` | — | rozpoznanie typu zgłoszenia ze schematu ubezpieczeń | - -**Schemat ubezpieczeń umowy + zgłoszenie ZUA/ZZA:** - -```csharp -var kadry = session.GetKadry(); -var pracownik = kadry.Pracownicy.WgKodu["006"]; -Umowa umowa = pracownik.Umowy.Cast().First(); - -// 1) Schemat ubezpieczeń umowy (historyczny) — ZUA: społeczne obowiązkowe + zdrowotne: -using (var t = session.Logout(editMode: true)) -{ - var ub = umowa.Last.Ubezpieczenia; // UmowaHistoria.Ubezpieczenia - ub.Tyub4 = kadry.TytulyUbezpiecz4.WgKodu[kodTytulu]; // tytuł zleceniobiorcy (klucz int, ze słownika) - ub.ObowiazkoweOd = umowa.Okres.From; // data objęcia społecznymi - ub.Emerytalne.Obowiazkowe = true; - ub.Rentowe.Obowiazkowe = true; - ub.Zdrowotne.ObowiazkoweOd = umowa.Okres.From; - // (ZZA = tylko zdrowotne: JAWNIE ustaw Emerytalne.Obowiazkowe = false i Rentowe.Obowiazkowe = false - // — domyślnie są true; samo zdrowotne nie wystarcza) - t.Commit(); -} -session.Save(); - -// 2) Zgłoszenie ZUA/ZZA na podstawie umowy — worker (DataType Umowa): -// Uwaga: Umowy oraz Pars są SET-ONLY (brak gettera) — ustawiamy je przez inicjalizator, -// a parametry budujemy jako osobny obiekt ParamsZ(context) i przypisujemy do Pars. -var worker = new Soneta.Deklaracje.ZUS.ZarejestrujUmowyWorker { Umowy = new[] { umowa } }; - -var pars = new Soneta.Deklaracje.ZUS.ZarejestrujBaseWorker.ParamsZ(context); -pars.Okres = new FromTo(umowa.Okres.From, Date.MaxValue); -pars.DataDokumentu = umowa.Okres.From; -pars.DataWypełnienia = Date.Today; -pars.ZarejestrujRodzinę = false; - -var rejestracja = new Soneta.Deklaracje.ZUS.ZarejestrujUmowyWorker.Rejestracja { Pars = pars }; -rejestracja.ZarejestrujUmowy(); // generuje ZUA lub ZZA wg schematu ubezpieczeń umowy -session.Save(); -``` - -**Wyrejestrowanie po zakończeniu umowy (ZWUA):** - -```csharp -var parsW = new Soneta.Deklaracje.ZUS.ZarejestrujBaseWorker.ParamsW(context); -parsW.Okres = new FromTo(umowa.Okres.To, umowa.Okres.To); -parsW.DataDokumentu = umowa.Okres.To; -parsW.DataWypełnienia = Date.Today; - -var wyrej = new Soneta.Deklaracje.ZUS.ZarejestrujUmowyWorker.Wyrejestrowanie { Pars = parsW }; -wyrej.WyrejestrujUmowy(); // generuje ZWUA -session.Save(); -``` - -**Pułapki:** -- **Typ zgłoszenia (ZUA vs ZZA) wynika ze schematu `UmowaHistoria.Ubezpieczenia`**, nie z parametru - workera — ustaw poprawnie `Tyub4` + flagi `Spoleczne.Obowiazkowe`/`Zdrowotne` **przed** zgłoszeniem. -- `Ubezpieczenia` jest **historyczne** — zmiana schematu „od daty" to nowy zapis `UmowaHistoria` - (`umowa.Historia.Update(date)` + `UmowaHistorie.AddRow`, jak G2/A14), nie nadpisywanie bieżącego. -- `Spoleczne.Od` jest **tylko do odczytu** (wyliczane) — datę objęcia społecznymi obowiązkowymi - ustawiasz zbiorczo przez `Ubezpieczenia.ObowiazkoweOd`; na `Zdrowotne` `ObowiazkoweOd` jest - zapisywalne wprost (asymetria — jak w A7). -- `Tyub4` to rekord **konfiguracyjnego** słownika `TytulyUbezpiecz4`, klucz `WgKodu[int]` — pobierz - istniejący tytuł zleceniobiorcy, nie twórz „w locie". -- `ZarejestrujUmowyWorker` jest na `Umowa` (umowy), a `ZarejestrujPracownikówWorker` na `Pracownik` - (etatowi) — do zleceniobiorców używaj wersji „Umowy". -- Workery deklaracji uruchamiaj jak każdy worker enova (Context z tej samej sesji); po akcji wołasz - `Session.Save()`. Obsłuż `RowConflictException` z `Save()` (safe-code §4). -- `ZarejestrujRodzinę`/`WyrejestrujRodzinę` sterują dołączeniem ZCNA dla członków rodziny - (`pracownik.Rodzina`, A9) — dla zleceniobiorcy zgłoszenie rodziny działa analogicznie do etatu. - -## H. Płace — naliczanie wypłat - -> **Model danych.** `Wyplata` (`Soneta.Place.Wyplata`) jest klasą **abstrakcyjną**, root `GuidedRow`, -> tabela `Wyplaty`. Konkretne typy: `WyplataEtat` (etat), `WyplataUmowa` (umowy), `WyplataInne` -> (pozostałe). Każda wypłata należy do jednej **listy płac** (`ListaPlac`, tabela `ListyPlac`) i do -> jednego pracownika. Składniki wynagrodzenia to **elementy** (`WypElement`, tabela `WypElementy`, -> root guided) w kolekcji `Wyplata.Elementy: SubTable`. -> -> **Naliczanie** realizuje publiczny worker `Soneta.Place.NaliczanieSeryjne` (klasa abstrakcyjna -> `partial`) z zagnieżdżonymi klasami: -> - parametry: `NaliczanieSeryjne.Params` (bazowa), `NaliczanieSeryjne.PracownikParams : Params` -> (etat + pozostałe), `NaliczanieSeryjne.UmowaParams : Params` (umowy); -> - wykonawcy: `NaliczanieSeryjne.Pracownika : NaliczanieSeryjne` (wypłaty pracownika), -> `NaliczanieSeryjne.Umowy : NaliczanieSeryjne` (wypłaty z umów). -> -> Wynik to obiekt `Soneta.Place.NaliczanieWypłat` z kolekcją `WszystkieWypłaty: IList` (elementy są -> typu `Wyplata`). **Naliczanie samo zatwierdza zmiany w sesji** (`Nalicz()` wewnętrznie otwiera i -> commituje transakcję edycyjną na sesji pracownika) — utrwalenie w bazie wymaga osobnego -> `session.Save()`. - -### H1 — Naliczanie wypłat etatowych (★) - -**Cel:** naliczyć wypłatę etatową (wynagrodzenie zasadnicze etatu + dodatki/potrącenia) dla jednego -pracownika za wskazany okres rozliczeniowy. - -**Klasy, pola i typy:** - -| Element | Typ / sygnatura | Uwaga | -|---|---|---| -| Parametry | `new NaliczanieSeryjne.PracownikParams(Context)` | ctor przyjmuje `Context` (sesja operacyjna) | -| Data wypłaty | `PracownikParams.DataWypłaty: Date` | ustawienie **automatycznie** wylicza `Okres` (z konfiguracji listy) i `MiesiącDeklaracji` | -| Data listy | `PracownikParams.DataListy: Date` | data dokumentu listy płac | -| Okres naliczania | `PracownikParams.Okres: FromTo` | zwykle wyliczony z `DataWypłaty`; można nadpisać | -| Typ naliczenia | `PracownikParams.Naliczanie: TypNaliczenia` | `PłatnaZGóry`/`PłatnaZDołu`; **domyślnie `PłatnaZDołu`** — patrz Pułapki (licencja) | -| Filtr typu wypłaty | `PracownikParams.TypWypłaty: TypWyplaty` | `Wszystkie`/`Etat`/`Umowa`/`Inne` — dla etatu `Etat` lub `Wszystkie` | -| Wykonawca | `new NaliczanieSeryjne.Pracownika(PracownikParams)` | | -| Pracownik | `NaliczanieSeryjne.Pracownika.Pracownik: Pracownik` | komu naliczamy (z tej samej sesji co `Context`) | -| Uruchomienie | `NaliczanieSeryjne.Pracownika.Nalicz(): NaliczanieWypłat` | nalicza i zatwierdza w sesji | -| Wynik | `NaliczanieWypłat.WszystkieWypłaty: IList` (elementy `Wyplata`) | naliczone wypłaty | -| Błędy naliczania | `NaliczanieWypłat.Nienaliczeni: IEnumerable` | pracownicy, dla których się nie udało | - -`TypNaliczenia` (`Soneta.Place`): `PłatnaZGóry = 1`, `PłatnaZDołu = 2`. -`TypWyplaty` (`Soneta.Place`): `Wszystkie = 0`, `Etat = 1`, `Umowa = 2`, `Inne = 3`. - -**Snippet:** - -```csharp -var place = session.GetPlace(); -var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; - -// Parametry naliczania — Context z tej samej sesji co pracownik: -var pars = new NaliczanieSeryjne.PracownikParams(context); -pars.DataWypłaty = new Date(2024, 6, 28); // ustawia Okres i MiesiącDeklaracji automatycznie -pars.DataListy = pars.DataWypłaty; -// pars.Naliczanie pozostaje domyślnie PłatnaZDołu (nie ustawiamy — patrz Pułapki) -pars.TypWypłaty = TypWyplaty.Etat; // tylko wypłaty etatowe - -var naliczanie = new NaliczanieSeryjne.Pracownika(pars) { Pracownik = pracownik }; -NaliczanieWypłat wynik = naliczanie.Nalicz(); // nalicza + commit w sesji - -foreach (Wyplata w in wynik.WszystkieWypłaty) -{ - // w.Pracownik, w.ListaPlac, w.Data, w.MiesiacDeklaracji, w.Wartosc (Currency, do wypłaty) -} - -session.Save(); // utrwalenie w bazie (opcjonalne — bez tego zmiany żyją tylko w sesji) -``` - -**Pułapki:** -- **`Context` musi pochodzić z tej samej sesji co pracownik.** `PracownikParams(Context)` wiąże się z - `Context.Session`; pracownik pobrany z innej sesji spowoduje niespójność. -- **Nie ustawiaj `Naliczanie` jawnie, jeśli nie masz pewności co do licencji.** Setter - `Params.Naliczanie` rzuca wyjątek, gdy licencja nie jest „PL Złoty/Platynowy" — getter wtedy i tak - zwraca `PłatnaZDołu`. Pozostawienie wartości domyślnej (`PłatnaZDołu`) jest bezpieczne. -- `Nalicz()` **otwiera własną transakcję** na sesji pracownika i commituje ją — **nie owijaj** wywołania - w dodatkowy `session.Logout(true)`. Po naliczeniu zmiany są w sesji; do bazy idą dopiero w `Save()`. -- `WszystkieWypłaty` to `IList` nietypowana — iteruj jako `foreach (Wyplata w in ...)`. -- Pracownik w archiwum (`Pracownik.ArchiwumInfo == InformacjeOArchiwum.WArchiwum`) jest pomijany — - `WszystkieWypłaty` będzie puste, bez wyjątku. -- Naliczanie to operacja na danych operacyjnych — sprawdź `wynik.Nienaliczeni` zamiast łapać ogólny - wyjątek; przy `KontynacjaNaliczenia` (tryb seryjny) błędy lądują tam, a nie w `throw`. - -### H2 — Naliczanie wypłat z umów (★) - -**Cel:** naliczyć wypłatę z konkretnej umowy cywilnoprawnej (`Soneta.Kadry.Umowa`). - -**Klasy, pola i typy:** - -| Element | Typ / sygnatura | Uwaga | -|---|---|---| -| Parametry | `new NaliczanieSeryjne.UmowaParams(Context)` | jak `PracownikParams`, ale `Naliczanie` jest na sztywno `PłatnaZDołu` (setter rzuca `NotSupportedException`) | -| Data wypłaty / listy / okres | `UmowaParams.DataWypłaty`, `.DataListy`, `.Okres` | jak w H1 | -| Wykonawca | `new NaliczanieSeryjne.Umowy(UmowaParams)` | w ctorze ustawia `TypWypłaty = Umowa` | -| Umowa | `NaliczanieSeryjne.Umowy.Umowa: Umowa` | ustawienie umowy ustawia też `Pracownik` z `umowa.Pracownik` | -| Uruchomienie | `NaliczanieSeryjne.Umowy.Nalicz(): NaliczanieWypłat` | | - -**Snippet:** - -```csharp -var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; -Umowa umowa = pracownik.Umowy.Cast().First(); // przykładowa umowa pracownika - -var pars = new NaliczanieSeryjne.UmowaParams(context); -pars.DataWypłaty = new Date(2024, 6, 28); -pars.DataListy = pars.DataWypłaty; - -var naliczanie = new NaliczanieSeryjne.Umowy(pars) { Umowa = umowa }; -NaliczanieWypłat wynik = naliczanie.Nalicz(); - -foreach (Wyplata w in wynik.WszystkieWypłaty) -{ - // w.Typ == TypWyplaty.Umowa; w.Wartosc; w.Elementy -} -session.Save(); -``` - -**Pułapki:** -- **Nie ustawiaj `UmowaParams.Naliczanie`** — setter rzuca `NotSupportedException` (umowy zawsze - „płatne z dołu"). -- Ustawienie `Umowy.Umowa` nadpisuje `Pracownik` na właściciela umowy — nie ustawiaj `Pracownik` ręcznie. -- Pozostałe pułapki jak w H1 (Context z tej samej sesji, własna transakcja w `Nalicz()`, `Save()`). - -### H3 — Naliczanie pozostałych wypłat (★) - -**Cel:** naliczyć wypłaty „pozostałe" — pojedynczy dodatek/potrącenie (np. premia, zasiłek -jednorazowy) poza zasadniczym wynagrodzeniem etatu, bądź wypłaty typu `Inne`. - -**Mechanizm:** używamy tego samego wykonawcy co H1 — `NaliczanieSeryjne.Pracownika` — sterując -zakresem przez `PracownikParams`: -- `PracownikParams.TypWypłaty = TypWyplaty.Inne` — naliczanie tylko składników typu „inne", -- `PracownikParams.Dodatek: DefinicjaElementu` — **zawężenie do jednej definicji** dodatku/potrącenia - (naliczany jest tylko wskazany składnik). - -**Pola i typy:** - -| Element | Typ / sygnatura | Uwaga | -|---|---|---| -| Filtr typu | `PracownikParams.TypWypłaty: TypWyplaty` | `Inne` — pozostałe; `Wszystkie` — łącznie z etatem | -| Pojedynczy składnik | `PracownikParams.Dodatek: DefinicjaElementu` | definicja konkretnego dodatku/potrącenia; `null` = bez zawężenia | - -**Snippet:** - -```csharp -var place = session.GetPlace(); -var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; - -// Definicja konkretnego dodatku/potrącenia (rekord konfiguracyjny): -DefinicjaElementu defDodatku = place.DefElementow.WgKodu["PREMIA"]; // przykładowy kod - -var pars = new NaliczanieSeryjne.PracownikParams(context); -pars.DataWypłaty = new Date(2024, 6, 28); -pars.DataListy = pars.DataWypłaty; -pars.TypWypłaty = TypWyplaty.Inne; // pozostałe wypłaty -pars.Dodatek = defDodatku; // tylko ten składnik - -var naliczanie = new NaliczanieSeryjne.Pracownika(pars) { Pracownik = pracownik }; -NaliczanieWypłat wynik = naliczanie.Nalicz(); - -foreach (Wyplata w in wynik.WszystkieWypłaty) -{ - foreach (WypElement e in w.Elementy) - { - // e.Definicja, e.Nazwa, e.Wartosc (decimal), e.Okres - } -} -session.Save(); -``` - -**Pułapki:** -- `Dodatek` to rekord **konfiguracyjny** `DefinicjaElementu` — pobierz istniejącą definicję - (np. przez klucz kodu w `place.DefElementow`), nie twórz „w locie". -- `TypWyplaty.Inne` i `TypWyplaty.Etat` są rozłączne — by naliczyć etat + dodatki łącznie użyj - `Wszystkie`. -- Pozostałe pułapki jak w H1. - -### H4 — Przeglądanie/odczyt wypłat za rok (★) - -**Cel:** odczytać naliczone wypłaty pracownika za dany rok i zagregować wartości (suma do wypłaty, -brutto/netto/składki/podatek, sumy składników). - -**Dostęp do wypłat (publiczny kontrakt):** - -| Punkt wejścia | Typ | Uwaga | -|---|---|---| -| `pracownik.Wyplaty` | `SubTable` | wszystkie wypłaty pracownika (klucz `WgPracownik`) | -| `session.GetPlace().Wyplaty.WgPracownik[pracownik]` | `SubTable` | równoważnie z modułu | -| `session.GetPlace().Wyplaty.WgData[date]` | `SubTable` | wypłaty z datą `date` | -| `listaPlac.Wyplaty` | `SubTable` | wypłaty danej listy płac | - -**Pola wypłaty (`Wyplata`) do odczytu:** - -| Pole | Typ | Opis | -|---|---|---| -| `Pracownik` | `Pracownik` | właściciel | -| `ListaPlac` | `ListaPlac` | lista płac (`ListaPlac.Okres: FromTo`, `ListaPlac.DataWyplaty: Date`, `ListaPlac.Zatwierdzona: bool`) | -| `Data` | `Date` | data wypłaty (klucz `WgData`) | -| `MiesiacDeklaracji` | `YearMonth` | miesiąc rozliczenia PIT | -| `MiesiacZUS` | `YearMonth` | miesiąc rozliczenia ZUS | -| `Wartosc` | `Currency` | kwota **do wypłaty** (netto) w PLN | -| `Numer` | `NumerDokumentu` | numer dokumentu (`Numer.NumerPelny`) | -| `Typ` | `TypWyplaty` | etat / umowa / inne | -| `Bufor` | `bool` | wypłata w buforze (niezatwierdzona) | -| `Elementy` | `SubTable` | składniki wynagrodzenia | - -**Kwoty na poziomie wypłaty (`Soneta.Place.Wyplata`, typ `Soneta.Types.Currency`):** `Wartosc` -(kwota **do wypłaty**, PLN), `WartoscCy` (w walucie listy), `DoWypłaty`, `Gotówka`, `Inne`. -Aby otrzymać `decimal`, użyj **`.Value`** (`w.Wartosc.Value`) — `Currency` nie ma jawnego rzutowania -na `decimal`. - -> **Uwaga:** `Wyplata`/`WyplataEtat` **nie udostępnia** publicznych agregatów typu `Brutto`, `Netto`, -> `SkładkiZUS`, `Podatek` jako gotowych właściwości. Brutto/netto/składki/podatek **liczymy sumując -> składniki** z kolekcji `Wyplata.Elementy` (`WypElement.Wartosc`, `WypElement.Netto`, `WypElement.Podatki.*`). - -**Składniki (`WypElement`) i ich struktura podatkowo-składkowa:** - -| Pole | Typ | Opis | -|---|---|---| -| `Definicja` | `DefinicjaElementu` | definicja składnika | -| `Nazwa` | `string` | nazwa składnika | -| `Wartosc` | `decimal` | wartość składnika | -| `Okres` | `FromTo` | okres, za który naliczono | -| `Podatki` | `Podatki` (subrow) | struktura podatków/składek | -| `Podatki.PodstawaZUS` | `decimal` | podstawa ZUS | -| `Podatki.Emerytalna` / `Rentowa` / `Chorobowa` / `Wypadkowa` / `Zdrowotna` | `SkladkaZUS` (subrow) | każda z polami `Podstawa`, `Prac`, `Firma: decimal` | -| `Podatki.Koszty`, `Podatki.Ulga`, `Podatki.ZalFIS` | `decimal` | koszty, ulga, zaliczka PIT | - -**Snippet:** - -```csharp -var place = session.GetPlace(); -var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; - -int rok = 2024; -var od = new Date(rok, 1, 1); -var doD = new Date(rok, 12, 31); - -// Filtr serwerowy po dacie wypłaty (zakres roku) — bez pełnego skanu: -decimal sumaDoWypłaty = 0m; -decimal sumaBrutto = 0m; - -foreach (Wyplata w in pracownik.Wyplaty[(Wyplata x) => x.Data >= od && x.Data <= doD]) -{ - sumaDoWypłaty += w.Wartosc.Value; // kwota do wypłaty (Currency -> decimal przez .Value) - - // brutto/składki/podatek liczymy z elementów (nie ma gotowych agregatów na wypłacie): - foreach (WypElement e in w.Elementy) - { - sumaBrutto += e.Wartosc; // WypElement.Wartosc to decimal - decimal netto = e.Netto; - decimal podstawaZUS = e.Podatki.PodstawaZUS; - decimal zaliczkaPit = e.Podatki.ZalFIS; - } -} -``` - -**Pułapki:** -- `Wyplaty` to tabela **operacyjna guided** — zawsze ograniczaj zakresem czasowym (rok), nie iteruj - całości (`safe-code §6.3`). Filtruj serwerowo przez `SubTable[condition]` po `Data`, nie w pamięci. -- `Wartosc` to `Currency` (kwota do wypłaty); konwersja na `decimal` przez `.Value`. Składnik - `WypElement.Wartosc`/`WypElement.Netto` to już `decimal` — nie myl typów ani znaczeń. -- **Nie ma** gotowych właściwości agregujących (`Brutto`/`Netto`/`SkładkiZUS`/`Podatek`) na `Wyplata` - ani `WyplataEtat` — sumuj składniki z `Wyplata.Elementy` (i ich `Podatki.*`). -- `SkladkaZUS` ma pola `Podstawa`, `Prac`, `Firma` (część pracownika i pracodawcy) oraz właściwość - pomocniczą `Składka` (suma) — wybierz właściwą do potrzeb. -- Filtruj po `Data` (data wypłaty) lub `MiesiacDeklaracji`/`MiesiacZUS` zależnie od potrzeby - raportowej — to różne pojęcia roku (rok wypłaty vs rok deklaracji). - -### H5 — Odczyt elementów wypłaty (brutto/składki/podatek/netto) (★) - -**Cel:** odczytać składniki konkretnej **naliczonej** wypłaty (`Soneta.Place.Wyplata`) i wyliczyć -agregaty: brutto, składki ZUS (część pracownika i firmy), zaliczka PIT, netto. - -**Model.** Składniki to `Wyplata.Elementy: SubTable` (`Soneta.Place.WypElement`, tabela -operacyjna guided `WypElementy`). `Wyplata` **nie** ma gotowych agregatów `Brutto`/`Netto`/`SkładkiZUS`/ -`Podatek` — liczymy je z elementów albo przez worker `Wyplata.PITInfoWorker` (patrz niżej). - -**Pola składnika `WypElement` (do odczytu):** - -| Pole | Typ | Opis | -|---|---|---| -| `Definicja` | `DefinicjaElementu` | definicja składnika (konfiguracja) | -| `Nazwa` | `string` | nazwa składnika | -| `Wartosc` | `decimal` | wartość brutto składnika (kwota elementu) | -| `Netto` | `decimal` | wartość netto składnika | -| `DoWypłaty` | `decimal` | kwota do wypłaty z tego składnika | -| `Okres` | `FromTo` | okres, za który naliczono | -| `MiesiacDeklaracji` | `YearMonth` | miesiąc rozliczenia PIT | -| `MiesiacZUS` | `YearMonth` | miesiąc rozliczenia ZUS | -| `Podatki` | `Podatki` (subrow) | struktura podatkowo-składkowa | - -**Subrow `WypElement.Podatki` (`Soneta.Place.Podatki`) — pola istotne:** - -| Pole | Typ | Opis | -|---|---|---| -| `PodstawaZUS` | `decimal` | podstawa wymiaru składek ZUS | -| `Emerytalna` / `Rentowa` / `Chorobowa` / `Wypadkowa` / `Zdrowotna` | `SkladkaZUS` (subrow) | każda z polami `Podstawa`, `Prac`, `Firma: decimal` oraz wyliczanym `Składka` (suma) | -| `Koszty` | `decimal` | koszty uzyskania przychodu | -| `Ulga` | `decimal` | ulga podatkowa (kwota wolna) | -| `ZalFIS` | `decimal` | zaliczka na podatek dochodowy (fiskus) | -| `ZdrowotneDoOdliczenia` | `decimal` | składka zdrowotna do odliczenia | - -Subrow `SkladkaZUS` (`Soneta.Place.SkladkaZUS`): `Podstawa` (podstawa), `Prac` (część pracownika, -`decimal`), `Firma` (część pracodawcy, `decimal`), wyliczane `Składka` (suma) i `JestMinus` (`bool`). - -**Worker-agregator `Wyplata.PITInfoWorker`** (klasa publiczna, `[Context] Wypłata`) — udostępnia gotowe -sumy podatkowe dla wypłaty: - -| Właściwość | Typ | Opis | -|---|---|---| -| `DoOpodatkowania` | `Currency` | suma elementów opodatkowanych (brutto opodatkowane) | -| `Nieopodatkowane` | `Currency` | suma elementów nieopodatkowanych | -| `Razem` | `decimal` | opodatkowane + nieopodatkowane (przychód razem) | -| `NettoRazem` | `decimal` | wynagrodzenie netto razem | -| `NettoOpodat` | `Currency` | netto opodatkowane | -| `SkładkiZUS` | `decimal` | suma składek ZUS pracownika | -| `SkładkaZdrow` | `decimal` | składka zdrowotna | -| `Koszty` | `decimal` | koszty uzyskania razem | -| `Ulga` | `decimal` | ulga podatkowa | -| `ZalFIS` | `decimal` | zaliczka PIT | -| `Dochód_Bez26` / `Dochód_26` | `decimal` | dochód (z podziałem na ulgę „do 26 lat") | - -> `PITInfoWorker.Brutto` i `PITInfoWorker.Netto` są oznaczone `[Obsolete]` — używaj `DoOpodatkowania`, -> `Nieopodatkowane`, `Razem`, `NettoRazem`. Worker przyjmuje też kolekcję `Elementy: IEnumerable` -> (zamiast `Wypłata`) i `WykluczoneElementy: DefinicjaElementu[]`. - -**Snippet (agregacja ręczna z elementów):** - -```csharp -var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; - -decimal brutto = 0m, netto = 0m, zusPrac = 0m, zusFirma = 0m, zalPit = 0m; - -// jedna konkretna wypłata pracownika (np. ostatnia z czerwca): -var od = new Date(2024, 6, 1); -var doD = new Date(2024, 6, 30); -Wyplata wyplata = pracownik.Wyplaty[(Wyplata x) => x.Data >= od && x.Data <= doD].Cast().First(); - -foreach (WypElement e in wyplata.Elementy) -{ - brutto += e.Wartosc; // WypElement.Wartosc to decimal - netto += e.Netto; - zalPit += e.Podatki.ZalFIS; - - zusPrac += e.Podatki.Emerytalna.Prac + e.Podatki.Rentowa.Prac - + e.Podatki.Chorobowa.Prac + e.Podatki.Zdrowotna.Prac; - zusFirma += e.Podatki.Emerytalna.Firma + e.Podatki.Rentowa.Firma - + e.Podatki.Wypadkowa.Firma; -} - -decimal doWyplaty = wyplata.Wartosc.Value; // Currency -> decimal przez .Value -``` - -**Snippet (przez worker — gotowe agregaty):** - -```csharp -var pit = new Wyplata.PITInfoWorker { Wypłata = wyplata }; -decimal brutto = pit.Razem; // przychód razem -decimal nettoR = pit.NettoRazem; -decimal zus = pit.SkładkiZUS; -decimal zdrow = pit.SkładkaZdrow; -decimal zaliczka = pit.ZalFIS; -``` - -**Pułapki:** -- `WypElement.Wartosc`/`Netto`/`DoWypłaty` to `decimal`; `Wyplata.Wartosc` (do wypłaty) to `Currency` — - konwersja przez `.Value` (§10.1). -- `SkladkaZUS.Prac` to część pracownika, `SkladkaZUS.Firma` to część pracodawcy — wybierz właściwą - zależnie od potrzeby (koszt pracownika vs koszt pracodawcy). -- `Wyplaty`/`WypElementy` to tabele operacyjne guided — pobieraj zakresem czasowym (§6.3), nie iteruj - całości. -- Pomijaj elementy stornowane przy sumowaniu, jeśli liczysz stan bieżący — patrz `WypElement.RozliczenieStorna` - (H10); naliczona wypłata po korekcie zawiera zarówno element pierwotny (`Wystornowany`) jak i `Stornujący`. - ---- - -### H6 — Wypłata zaliczki / dołączenie zaliczki (★) - -**Cel:** naliczyć i wypłacić zaliczkę (wypłata środków „na poczet" przyszłego wynagrodzenia), tworząc -dokument `Soneta.Place.Zaliczka` i element realizacji zaliczki na wypłacie. - -**Model.** Zaliczka to rekord operacyjny `Soneta.Place.Zaliczka` (root guided, tabela `Zaliczki`, -`session.GetPlace().Zaliczki`), implementuje `IBazaZrodlaWyplaty` i `IPowiązanieWypłaty`. Element -realizujący zaliczkę to `WypElementZaliczka.Realizacja : WypElementZaliczka : WypElement`, spłata to -`WypElementZaliczka.Spłata`. Powiązanie elementu z zaliczką: `WypElement.BazaZrodla = Zaliczka`, -`RodzajŹródłaWypłaty.Zaliczka`. - -**Ścieżka publiczna — worker `Soneta.Place.WypłaćZaliczkęWorker`** (na `Soneta.Kadry.Pracownicy`): - -| Element | Typ / sygnatura | Uwaga | -|---|---|---| -| Parametry | `WypłaćZaliczkęWorker.ZalParams : WypElement.Params` | ctor `(Context)`; `Rodzaj == RodzajŹródłaWypłaty.Zaliczka` | -| Definicja | `ZalParams.Definicja: DefinicjaElementu` | definicja elementu zaliczki (z `WypElement.Params`); **musi mieć** `DefinicjaElementu.RodzajZrodla == RodzajŹródłaWypłaty.Zaliczka` — inaczej worker rzuca `WypElement.RodzajDefinicjiException` (np. „Korekta zaliczki podatku" ma `Dodatek`) | -| Data | `ZalParams.Data: Date` | data wypłaty zaliczki (wymagana) | -| Kwota | `ZalParams.Kwota: Currency` | kwota zaliczki (wymagana) | -| Pracownicy | `WypłaćZaliczkęWorker.Pracownicy: Pracownik[]` | dla kogo | -| Akcja | `[Action("Wypłać zaliczkę")] object WypłataZaliczki()` | tworzy `Zaliczka`, nalicza element realizacji | - -**Stan zaliczki (`Zaliczka`):** `Wartosc: Currency`, `Splacono: Currency`, `Pozostaje: Currency` -(`= Wartosc - Splacono`), `Stan: StanZaliczki` (`NieSpłacona`/`CzęściowoSpłacona`/`CałkowicieSpłacona`), -`Realizacje: SubTable` (elementy realizacji), `Spłaty: SubTable` (elementy spłaty). - -**Mechanizm naliczenia** (realizowany przez worker): dla każdego pracownika tworzony jest -`new Zaliczka(pracownik)`, dodawany przez `Zaliczki.AddRow(zaliczka)`, a następnie niskopoziomowy -obiekt `Soneta.Place.NaliczanieWypłat` z `NaliczŹródłoWypłaty = zaliczka` wykonuje `Nalicz()`. -Dołączenie/spłata zaliczki w kolejnej wypłacie etatowej dzieje się automatycznie podczas zwykłego -naliczania (H1) — naliczanie wyszukuje niespłacone zaliczki pracownika i generuje element -`WypElementZaliczka.Spłata`. - -**Snippet (uruchomienie workera zaliczki):** - -```csharp -var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; -// definicję zaliczki rozpoznajemy po RodzajZrodla (nie po Kodzie/Nazwie — „Korekta zaliczki podatku" -// to RodzajZrodla.Dodatek, którego worker NIE przyjmie): -DefinicjaElementu defZaliczki = session.GetPlace().DefElementow.Cast() - .First(d => d.RodzajZrodla == RodzajŹródłaWypłaty.Zaliczka); - -var pars = new WypłaćZaliczkęWorker.ZalParams(context) { - Data = new Date(2024, 6, 15), - Kwota = new Currency(1000m, Currency.SystemSymbol), -}; -pars.Definicja = defZaliczki; - -var worker = new WypłaćZaliczkęWorker { Params = pars, Pracownicy = new[] { pracownik } }; -object wynik = worker.WypłataZaliczki(); // tworzy Zaliczka + nalicza; otwiera własną transakcję -session.Save(); -``` - -**Pułapki:** -- `ZalParams.Definicja` to **istniejąca** definicja elementu o `RodzajZrodla == RodzajŹródłaWypłaty.Zaliczka` — - pobierz z `place.DefElementow` (filtruj po `RodzajZrodla`, nie po `Kod`/`Nazwa`), nie twórz w locie. -- Baza Demo może nie zawierać definicji o `RodzajZrodla == Zaliczka` — wtedy worker jest niewykonalny - (w teście: `Assert.Ignore`). -- `Zaliczka.SetWartość(...)` jest `internal` — wartości nie ustawiaj ręcznie; przekaż `ZalParams.Kwota` - do workera. -- `Zaliczka` nie kasuje się bezpośrednio, gdy ma realizacje/spłaty (`OnDeleting` rzuca `RowException`). -- Worker otwiera własną transakcję (`Session.Logout(true)` + `CommitUI`) — nie owijaj dodatkowo; - utrwalenie w bazie przez `Save()`. - ---- - -### H7 — Korekta podatków i składek; „Przelicz składki ZUS i podatki" (★) - -**Cel:** ponownie przeliczyć (skorygować) składki ZUS i zaliczki PIT na już naliczonych elementach -wypłat pracownika za dany miesiąc deklaracji — np. po zmianie progu, tytułu ubezpieczenia, korekcie -danych kadrowych. - -**Worker `Soneta.Place.NaliczaniePodatkówMiesięcznie`** (na `Pracownik`/`PracHistoria`): - -| Element | Typ / sygnatura | Uwaga | -|---|---|---| -| Miesiąc | publiczny ctor `(YearMonth miesiącDeklaracji)` (atrybut `[Context(typeof(MiesiącDeklaracji),"Miesiąc")]`) | przy ręcznym wywołaniu przekaż `YearMonth` (np. `pars.Miesiąc`); property odczytu `MiesiącDeklaracji: YearMonth` (get) | -| Klasa parametru | `Soneta.Place.MiesiącDeklaracji : ContextBase` | `MiesiącDeklaracji.Miesiąc: YearMonth` (domyślnie `YearMonth.Today`) | -| Pracownik | `NaliczaniePodatkówMiesięcznie.Pracownik: Pracownik` | `[Context]` | -| `NoTrace` | `bool` | wyłączenie śladu (logu) operacji | -| Akcja | `[Action("Przelicz składki ZUS i podatki")] void PrzeliczPodatki()` | przelicza elementy z danego miesiąca | - -**Mechanizm:** worker iteruje elementy (`WypElementy.WgDaty`) wszystkich pracowników powiązanych -(`Pracownik.PracownicyPowiązani`) w okresie `MiesiącDeklaracji.ToFromTo()`, dla niezablokowanych -(`!element.Podatki.Korekta && element.Wyplata.Bufor`) wykonuje przeliczenie flag i naliczenie -podatków (`NaliczaniePodatków.NaliczRozrzuć()`). Wszystko w transakcji `Session.Logout(true)` + -`Commit()`. - -**Snippet:** - -```csharp -var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; - -var pars = new MiesiącDeklaracji(context) { Miesiąc = new YearMonth(2024, 6) }; -var worker = new NaliczaniePodatkówMiesięcznie(pars.Miesiąc) { Pracownik = pracownik }; -worker.PrzeliczPodatki(); // przelicza składki ZUS i zaliczki PIT za czerwiec 2024 -session.Save(); -``` - -**Pułapki:** -- Elementy z ręczną korektą podatków (`element.Podatki.Korekta == true`) oraz elementy z wypłat - zatwierdzonych (`!Wyplata.Bufor`) są **pomijane** — przeliczane są tylko elementy z bufora. -- `MiesiącDeklaracji.Miesiąc` to `YearMonth` — to miesiąc deklaracji, nie data wypłaty. -- Worker przelicza także pracowników powiązanych (`PracownicyPowiązani`) — operacja może objąć więcej - niż jedną kartotekę. - ---- - -### H8 — Rozliczenie pracownika; dochód / roczny dochód (★) - -**Cel:** odczytać dochód z naliczonej wypłaty oraz (dla właścicieli) pobrać roczny dochód do rozliczeń; -opcjonalnie uruchomić rozliczenie pracownika. - -**A. Dochód z wypłaty — `Wyplata.PITInfoWorker`** (publiczny, jak w H5). Dochód podatkowy: - -| Właściwość | Typ | Opis | -|---|---|---| -| `Dochód_Bez26` | `decimal` | dochód poza ulgą „do 26 lat" (`= przychód + przychód50 − koszty − koszty50`) | -| `Dochód_26` | `decimal` | dochód objęty ulgą „do 26 lat" | -| `DoOpodatkowania` | `Currency` | podstawa opodatkowania (brutto opodatkowane) | -| `Podstawa` | `decimal` | podstawa naliczenia zaliczki | -| `ZalFIS` | `decimal` | zaliczka PIT | - -Dochód roczny pracownika sumuje się iterując wypłaty roku (H4/H5) i sumując `Dochód_Bez26 + Dochód_26` -(lub `DoOpodatkowania`) z `PITInfoWorker` każdej wypłaty. - -**B. „Pobierz roczny dochód" — worker `Soneta.Kadry.PobierzDochodRocznyWorker`** (na `Pracownik`/ -`PracHistoria`) — **tylko dla właściciela** (`Pracownik is Wlasciciel`): - -| Element | Typ / sygnatura | Uwaga | -|---|---|---| -| Parametry | property `PobierzDochodRocznyWorker.Pars : PobierzDochodRocznyWorker.Params` | `Pars.Rok: int` (domyślnie rok ubiegły) | -| Pracownik | `PobierzDochodRocznyWorker.Pracownik: Pracownik` | `[Context]` | -| Akcja | `[Action("Pobierz roczny dochód")] void Pobierz()` | zapisuje `PrzychodRyczalt` (RoczDochSkala/RoczDochLiniowy/RoczDochRyczalt) za rok | - -Korzysta z serwisu `IDochódWłaściciela.KwotaDochoduStraty(pracownik, YearMonth, FormaOpodatkowania)`. - -**C. „Rozlicz pracownika" — worker `Soneta.Place.RozliczaniePracownikowWorker`** (na `Pracownik`) — -**tylko dla folderu pracowników zewnętrznych** (`KadryIPlace/Kadry/PracownicyZewnetrzni`): - -| Element | Typ / sygnatura | Uwaga | -|---|---|---| -| Parametry | `RozliczeniePracownikowParams : RozliczanieUmowZewnetrznychParams` | `Okres: FromTo`, `Data: Date` | -| Akcja | `[Action("Rozlicz pracownika")] RozliczanieUmowZewnetrznych Rozlicz()` | rozlicza umowy zewnętrzne pracownika | - -**Snippet (dochód roczny z wypłat):** - -```csharp -var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; -var od = new Date(2024, 1, 1); var doD = new Date(2024, 12, 31); - -decimal dochodRoczny = 0m; -foreach (Wyplata w in pracownik.Wyplaty[(Wyplata x) => x.Data >= od && x.Data <= doD]) -{ - var pit = new Wyplata.PITInfoWorker { Wypłata = w }; - dochodRoczny += pit.Dochód_Bez26 + pit.Dochód_26; -} -``` - -**Pułapki:** -- `PobierzDochodRocznyWorker` działa wyłącznie dla `Wlasciciel` i form opodatkowania ogólnych/ryczałtu — - dla zwykłego pracownika nie ma zastosowania (zwraca bez efektu). -- „Rozlicz pracownika" (`RozliczaniePracownikowWorker`) dotyczy **pracowników zewnętrznych** (umowy - zewnętrzne), nie standardowego rozliczenia płacowego. -- Wewnętrzny `Wyplata.RozliczenieManager` (rozliczanie płatności/należności) jest **niepubliczny** — - rozliczenie płatności inicjuje setter `Wyplata.Bufor` (zejście z bufora), nie wywołuj go bezpośrednio. - ---- - -### H9 — Kalkulator wynagrodzeń (brutto↔netto, koszt pracodawcy) (★) - -**Cel:** wyliczyć netto z brutto (lub odwrotnie) oraz całkowity koszt pracodawcy. - -**Brak dedykowanej publicznej klasy „kalkulatora wynagrodzeń"** w publicznym kontrakcie (patrz sekcja -„niewykonalne"). Wyliczenie realizujemy przez **naliczenie próbne** (H1/H3 — `NaliczanieSeryjne`) i -odczyt agregatów workera `Wyplata.PITInfoWorker` oraz `Wyplata.KosztyUzyskaniaPrzychoduWorker`. - -**Koszt pracodawcy — `Wyplata.PITInfoWorker` + składki firmy z elementów:** -- brutto: `pit.Razem` / `pit.DoOpodatkowania`, -- netto: `pit.NettoRazem`, -- składki pracownika: `pit.SkładkiZUS`, `pit.SkładkaZdrow`, -- zaliczka PIT: `pit.ZalFIS`, -- składki firmy (narzuty pracodawcy): suma `WypElement.Podatki.{Emerytalna,Rentowa,Wypadkowa}.Firma` - (plus FP/FGŚP/FEP) — patrz `WyplataSkładkiWorker` niżej. - -**Agregator składek — `Soneta.Place.WyplataSkładkiWorker`** (publiczny, `[Context] Wypłata` lub -`Elementy: IEnumerable`): udostępnia `Razem: ZestawienieSkładek` z m.in.: - -| Właściwość `ZestawienieSkładek` | Typ | Opis | -|---|---|---| -| `KosztyZUS` | `decimal` | składki ZUS pracownika (emer.+rent.+chor.+wyp., część `Prac`) | -| `FirmaZUS` | `decimal` | składki ZUS pracodawcy (część `Firma`) | -| `Narzuty` | `decimal` | narzuty pracodawcy (`FirmaZUS` + FP.Firma + FGSP.Firma + FEP.Firma) | -| `ZUS` | `decimal` | `KosztyZUS + FirmaZUS` | -| `Emerytalna`/`Rentowa`/… | `ISkładka` | pojedyncze składki (`Podstawa`/`Prac`/`Firma`/`Składka`) | - -Koszt pracodawcy ≈ brutto (`pit.DoOpodatkowania`/`Razem`) + `skladki.Razem.Narzuty`. - -**Snippet (kalkulacja przez naliczenie próbne):** - -```csharp -var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; - -var pars = new NaliczanieSeryjne.PracownikParams(context); -pars.DataWypłaty = new Date(2024, 6, 28); -pars.DataListy = pars.DataWypłaty; -pars.TypWypłaty = TypWyplaty.Etat; - -var nal = new NaliczanieSeryjne.Pracownika(pars) { Pracownik = pracownik }; -NaliczanieWypłat wynik = nal.Nalicz(); - -Wyplata w = (Wyplata)wynik.WszystkieWypłaty[0]; -var pit = new Wyplata.PITInfoWorker { Wypłata = w }; -var skl = new WyplataSkładkiWorker { Wypłata = w }; - -decimal brutto = pit.Razem; -decimal netto = pit.NettoRazem; -decimal kosztPracod = brutto + skl.Razem.Narzuty; // brutto + narzuty pracodawcy -// (jeśli to tylko kalkulacja — nie wywołuj Save(), wynik istnieje w sesji) -``` - -**Pułapki:** -- Brak osobnego „kalkulatora" — wynik zawsze powstaje przez naliczenie i workery agregujące. -- Kalkulacja brutto↔netto zależy od pełnej konfiguracji pracownika (etat, ulgi, koszty, PPK) — nie ma - bezstanowej funkcji „brutto→netto" w publicznym API. -- Jeśli naliczenie ma być tylko próbne, nie wywołuj `Save()` (zmiany zostaną w sesji i znikną z `Dispose`), - albo wykonaj na osobnej sesji „brudnopisowej". - ---- - -### H10 — Stornowanie elementów wypłaty; obsługa elementów stornowanych (★) - -**Cel:** zastornować (wycofać/skorygować) element już zatwierdzonej wypłaty i poprawnie odczytać stan -storna. - -**Model.** Storno opisuje rekord `Soneta.Place.StornoElementu` (tabela `StornaElementow`). Element -ma stan `WypElement.StanStorna: StanStornaElementu` oraz dostęp do storna `WypElement.Storno: StornoElementu`. - -Enum `Soneta.Place.StanStornaElementu`: `NieDotyczy=0`, `DoStornowania=1`, `Wystornowany=2`, -`Stornujący=3`, `WycofaneStorno=10` (tylko wyliczany). -Enum `Soneta.Place.RodzajStornaElementu`: `NieDotyczy=0`, `Anulowanie=1`, `Przeliczenie=2`. - -**Pola `WypElement` związane ze storno:** - -| Pole | Typ | Opis | -|---|---|---| -| `StanStorna` | `StanStornaElementu` | bieżący stan storna elementu | -| `StanStornaEx` | `StanStornaElementu` | jw. + `WycofaneStorno` gdy `Wystornowany` historyczny | -| `Storno` | `StornoElementu` | powiązany rekord storna (lub `null`) | -| `RozliczenieStorna` | `bool` | `true` gdy `Wystornowany` lub `Stornujący` (element nie liczy się do bieżącego stanu) | -| `Wystornowany` | `bool` | do elementu naliczono element stornujący | -| `Stornowane` / `Stornujące` | `SubTable` | relacje storn | -| `Korekta` | `bool` | element zmodyfikowany ręcznie przez operatora | -| `UtwórzStorno()` | `WypElement` | (wirtualna) tworzy element stornujący danego typu | - -**Workery oznaczania (publiczne, na `WypElement` / `Wyplata`):** -- `StornoElementu.ElementDoPrzeliczeniaWorker` (na `WypElement`): - - `[Action("Oznacz element do przeliczenia")] ZaznaczElementDoPrzeliczenia()` — `RodzajStornaElementu.Przeliczenie`, - - `[Action("Oznacz element do anulowania")] ZaznaczElementDoAnulowania()` — `RodzajStornaElementu.Anulowanie`, - - `[Action("Wycofaj oznaczenie anulowania lub przeliczenia")] WycofajZaznaczenie()` — kasuje `Storno`. -- `StornoElementu.WypłataDoPrzeliczeniaWorker` (na `Wyplata`): - - `ZaznaczElementyDoPrzeliczenia()` / `WycofajZaznaczenie()` — dla wszystkich elementów wypłaty. -- `StornoElementu.ListaPłacDoPrzeliczeniaWorker` (na `ListaPlac`, z `Params.Definicja` / `WszystkieElementy`). - -**Mechanizm.** Oznaczenie tworzy `StornoElementu` i ustawia element na `DoStornowania`. Właściwe -wytworzenie elementu stornującego (`UtwórzStornujący()`, stan `Wystornowany` na pierwotnym + -`Stornujący` na nowym) następuje przy ponownym naliczeniu wypłaty (H1) lub przeliczeniu. Wymagane: -wypłata zatwierdzona (`Wyplata.Zatwierdzona`) i element w stanie `NieDotyczy`. - -**Snippet (oznaczenie do anulowania + przeliczenie):** - -```csharp -Wyplata w = ...; // zatwierdzona wypłata pracownika 006 -WypElement element = w.Elementy.Cast().First(e => e.Definicja.Kod == "PREMIA"); - -// oznacz element do anulowania: -var worker = new StornoElementu.ElementDoPrzeliczeniaWorker { Element = element }; -worker.ZaznaczElementDoAnulowania(); // otwiera własną transakcję -// element.StanStorna == StanStornaElementu.DoStornowania, element.Storno.RodzajStorna == Anulowanie - -// ponowne naliczenie wypłaty (H1) wygeneruje element stornujący: -var pars = new NaliczanieSeryjne.PracownikParams(context) { DataWypłaty = w.Data, DataListy = w.Data }; -new NaliczanieSeryjne.Pracownika(pars) { Pracownik = w.Pracownik }.Nalicz(); -session.Save(); -``` - -**Odczyt elementów stornowanych:** - -```csharp -foreach (WypElement e in w.Elementy) -{ - if (e.RozliczenieStorna) continue; // pomiń wystornowane i stornujące przy sumowaniu stanu bieżącego - // ... e to element „żywy" -} -``` - -**Pułapki:** -- Oznaczać można tylko elementy wypłaty **zatwierdzonej** i w stanie `NieDotyczy` (`IsEnabled...` to - egzekwuje); na buforze storno nie ma sensu. -- Storno samo w sobie tylko **oznacza** (`DoStornowania`) — wystornowanie (`Wystornowany`/`Stornujący`) - powstaje dopiero przy ponownym naliczeniu/przeliczeniu. -- Przy sumowaniu kwot bieżących pomijaj `RozliczenieStorna == true`, inaczej policzysz element pierwotny - i jego storno podwójnie. -- Nie można przenieść do bufora wypłaty z elementami `DoStornowania`/`Wystornowany` (rzuca `RowException` - — patrz H11). - ---- - -### H11 — Anulowanie/usunięcie naliczonej wypłaty (bufor, ponowne naliczenie) (★) - -**Cel:** „cofnąć" naliczoną i zatwierdzoną wypłatę do edycji (bufor) lub usunąć, by naliczyć ponownie. - -**Model.** Wypłata ma flagi `Wyplata.Bufor: bool` (niezatwierdzona/edytowalna) oraz `Wyplata.Zatwierdzona: bool` -(odwrotność `Bufor`). Zejście z bufora = zatwierdzenie; powrót do bufora = otwarcie do edycji. - -**Workery (publiczne, na `Wyplata`):** - -| Worker / akcja | Sygnatura | Efekt | -|---|---|---| -| `Wyplata.ZatwierdźWorker` | property `Lista: Wyplata`; `[Action("Zatwierdź wypłatę")] void Zatwierdź()` | `Zatwierdzona = true` (zejście z bufora) | -| `Wyplata.OtwórzWorker` | property `Wypłata: Wyplata`; `[Action("Przenieś do bufora")] void Otwórz()` | `Zatwierdzona = false` (powrót do bufora) | - -Obie akcje działają w transakcji `Session.Logout(true)` + `Commit()`. **Uwaga na nazwy property:** -worker zatwierdzania przypina wypłatę przez `ZatwierdźWorker.Lista`, a otwierania — przez -`OtwórzWorker.Wypłata`. `IsEnabled...` wymaga `Wyplata.CanBufor` — ale `CanBufor` jest **`protected`** -(niedostępny z dodatku); stan czytaj przez publiczne `Wyplata.Bufor` / `Wyplata.Zatwierdzona`. - -**Bezpośrednia flaga `Wyplata.Bufor`:** -- setter `Bufor` rzuca `ColReadOnlyException`, gdy `!CanBufor`; -- zejście z bufora (`Bufor=false`) wyzwala rozliczenie płatności (wewnętrzny `RozliczenieManager`); -- `IsReadOnlyBufor()` true gdy brak praw / `!CanBufor` / wyłączone „ZatwierdzanieFlagą" / lista zatwierdzona. - -**Usunięcie / ponowne naliczenie.** Aby przeliczyć od nowa: przenieś wypłatę do bufora -(`OtwórzWorker.Otwórz()`), a następnie wykonaj ponowne naliczenie (H1 — `NaliczanieSeryjne`), które -nadpisze elementy. Usunięcie samej wypłaty realizuje standardowe `Row.Delete()` w transakcji (gdy -dozwolone — wypłata w buforze, bez powiązań blokujących). - -**Snippet (powrót do bufora + ponowne naliczenie):** - -```csharp -Wyplata w = ...; // zatwierdzona wypłata pracownika 006 - -// 1) przenieś do bufora: -new Wyplata.OtwórzWorker { Wypłata = w }.Otwórz(); // Zatwierdzona = false - -// 2) ponowne naliczenie (H1): -var pars = new NaliczanieSeryjne.PracownikParams(context) { - DataWypłaty = w.Data, DataListy = w.Data, TypWypłaty = TypWyplaty.Etat, -}; -new NaliczanieSeryjne.Pracownika(pars) { Pracownik = w.Pracownik }.Nalicz(); -session.Save(); -``` - -**Snippet (usunięcie wypłaty z bufora):** - -```csharp -using (ITransaction t = session.Logout(true)) { - w.Bufor = true; // upewnij się, że w buforze (lub OtwórzWorker) - w.Delete(); - t.Commit(); -} -session.Save(); -``` - -**Pułapki:** -- `Otwórz()` rzuca `RowException`, gdy wypłata nie jest zatwierdzona; `Zatwierdź()` — gdy już - zatwierdzona. Sprawdzaj `IsEnabled...` / stan przed wywołaniem. -- `UpdateBufor()` rzuca `RowException`, gdy na wypłacie są elementy `DoStornowania`/`Wystornowany` — - najpierw wycofaj oznaczenia storna (H10) lub dokończ przeliczenie. -- Zejście z bufora wykonuje rozliczenie płatności i kopiowanie kursu — nie traktuj go jak zwykłej - zmiany pola. -- Operacje płacowe to dane operacyjne — łap `RowException`/`RowConflictException` z `Save()` (§4, §9), - nie ogólny `Exception`. - -## I. Listy płac, przelewy, wydruki - -> **Model.** Lista płac to dokument operacyjny `Soneta.Place.ListaPlac` (root `GuidedRow`, -> tabela `ListyPlac`, `session.GetPlace().ListyPlac`). Trzyma kolekcję wypłat -> `ListaPlac.Wyplaty: SubTable`. Każda `Wyplata` (root `GuidedRow`, tabela `Wyplaty`) -> wskazuje wstecz listę (`Wyplata.ListaPlac: IRow`) i pracownika (`Wyplata.Pracownik: IRow`). -> Wzorzec listy to `DefinicjaListyPlac` (tabela konfiguracyjna `DefListPlac`, -> `session.GetPlace().DefListPlac`, dostęp `WgSymbolu`/`WgNazwy`). -> -> **`Wyplata` jest abstrakcyjna** — konkretne typy: `WyplataEtat`, `WyplataUmowa`, `WyplataInne` -> (ctor `(ListaPlac listaplac, Pracownik pracownik)` oraz wariant z `IPowiązanieWypłaty`). -> W praktyce wypłat **nie tworzy się ręcznie** — robi to worker naliczania. - -### I1 — Naliczanie/generowanie list płac (★) - -**Cel:** utworzyć listę płac dla wybranego okresu i naliczyć na niej wypłaty pracowników -(etat/umowy), tak by `ListaPlac.Wyplaty` zawierała policzone `Wyplata`. - -**Warianty:** - -| Wariant | Mechanizm | Uwaga | -|---|---|---| -| Ręczne utworzenie pustej listy | `new ListaPlac()` + `ListyPlac.AddRow(lp)` + pola | sterujesz wszystkim sam | -| Naliczanie wypłat na istniejącej liście | worker `Soneta.Place.NaliczanieWypłat` (akcja `Nalicz`) | tworzy `Wyplata*` i liczy elementy | -| Naliczanie planowanych list (zbiorczo) | worker `Soneta.Place.NaliczaniePlanowanychListPłacWorker` (akcja `Nalicz`) | wg `DefinicjaPlanowanejListyPłac` | - -**Pola i typy (`ListaPlac`, kolejność ustawiania jest istotna):** - -| Pole | Typ | Uwaga | -|---|---|---| -| `Definicja` | `Soneta.Place.DefinicjaListyPlac` | wzorzec listy; ustawić **pierwsze** po `AddRow` | -| `Wydzial` | `Soneta.Kadry.Wydzial` | tylko gdy `Definicja.Wydzial == true` | -| `Seria` | `string` | tylko gdy `Definicja.Seria == true` | -| `Data` | `Soneta.Types.Date` | data naliczania listy | -| `Naliczanie` | `Soneta.Place.TypNaliczenia` | wartości: `PłatnaZGóry`/`PłatnaZDołu`; **nie ustawiaj** — setter rzuca bez licencji „PL Złoty" | -| `DataWyplaty` | `Soneta.Types.Date` | data postawienia środków; wyznacza mies./rok | -| `MiesiacZUS` | `Soneta.Types.YearMonth` | miesiąc rozliczenia ZUS | -| `Okres` | `Soneta.Types.FromTo` | okres listy; **po** `DataWyplaty` i `Naliczanie` | -| `MiesWstecz` | `int` | | -| `Wyplaty` | `SubTable` | wypełniana przez worker naliczania | -| `Numer` | `Soneta.Core.NumerDokumentu` | nadawany automatycznie | -| `Bufor` / `Zatwierdzona` | `bool` | stan dokumentu | - -**Worker `Soneta.Place.NaliczanieWypłat`** — `[Context]`: `Context`, `ListaPłac: ListaPlac`, -`Pracownik: Soneta.Kadry.Pracownik`; akcja `NaliczanieWypłat Nalicz()`; właściwości -wynikowe m.in. `Wypłaty: IList`, `Nienaliczeni: IEnumerable`, -`DataWypłaty/DataListy/DataZUS: Date`, `Okres: FromTo`, `Naliczanie: TypNaliczenia`. - -**Worker `Soneta.Place.NaliczaniePlanowanychListPłacWorker`** — `[Context]`: -`Pracownik: Pracownik[]`; `Params Pars` z polami `Definicja: DefinicjaPlanowanejListyPłac`, -`DataWypłaty: Date`, `Okres: FromTo`, `Naliczanie: TypNaliczenia`, `TypWypłaty: TypWyplaty`, -`MiesiącZUS/MiesiącDeklaracji: YearMonth`, `Seria: string`, `MiesWstecz: int`, -`UwzgledniajNieZatwierdzoneListyPlac/EdycjaMiesiącaZUS: bool`; -akcja `NaliczaniePlanowanychListPłac Nalicz()`. - -**Snippet (ręczne utworzenie listy + naliczenie wypłaty pracownika):** - -```csharp -using Soneta.Business; -using Soneta.Place; -using Soneta.Kadry; -using Soneta.Types; - -var place = session.GetPlace(); - -// 1. Wzorzec listy płac (definicja konfiguracyjna). -var def = place.DefListPlac.WgSymbolu["ETAT"] - ?? throw new BusException("Brak definicji listy płac".Translate()); - -// 2. Pusta lista płac — KOLEJNOŚĆ: AddRow → Definicja → daty/naliczanie → Okres. -var lp = new ListaPlac(); -place.ListyPlac.AddRow(lp); -lp.Definicja = def; // pierwsze po AddRow -lp.Data = new Date(2026, 6, 30); -lp.DataWyplaty = new Date(2026, 6, 30); // wyznacza miesiąc/rok -lp.MiesiacZUS = new YearMonth(2026, 6); -lp.Okres = new FromTo(new Date(2026, 6, 1), new Date(2026, 6, 30)); // po DataWyplaty -// Uwaga: NIE ustawiaj lp.Naliczanie — setter rzuca bez licencji „PL Złoty"; getter ma sensowny domyślny. - -// 3. Naliczenie wypłaty pracownika — sprawdzona ścieżka to NaliczanieSeryjne (patrz sekcja H); -// naliczona wypłata zostaje automatycznie powiązana z bieżącą listą płac. -var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; -var pars = new NaliczanieSeryjne.PracownikParams(context) // context: w UI z workera, w teście z TestBase -{ - DataWypłaty = new Date(2026, 6, 30), - DataListy = new Date(2026, 6, 30), - TypWypłaty = TypWyplaty.Etat, -}; -var wynik = new NaliczanieSeryjne.Pracownika(pars) { Pracownik = pracownik }.Nalicz(); - -// 4. Powiązanie wypłaty z listą jest dwukierunkowe (Wyplata.ListaPlac / Wyplata.Pracownik): -foreach (Wyplata w in wynik.WszystkieWypłaty) -{ - // w.ListaPlac, w.Pracownik -} - -session.Save(); -``` - -**Pułapki:** -- **Kolejność pól krytyczna:** `Okres` i `MiesWstecz` ustaw **po** `DataWyplaty` i `Naliczanie` - (wzajemne zależności wyliczeń) — patrz wzorzec w kodzie naliczania list. -- `Wydzial`/`Seria` ustawiaj **warunkowo** wg `Definicja.Wydzial`/`Definicja.Seria` — inaczej - ryzyko niespójności klucza `WgDefinicja`. -- Wypłat **nie twórz przez `new WyplataEtat(...)` ręcznie** — naliczaj. Sprawdzoną ścieżką - naliczania jest **`NaliczanieSeryjne.Pracownika(...).Nalicz()`** (sekcja H); sam worker - `NaliczanieWypłat { ListaPłac, Pracownik }.Nalicz()` w bazie Demo potrafi zwrócić pustą listę. -- `Wyplata.ListaPlac`/`Wyplata.Pracownik` to relacje **tylko do odczytu** — powiązania nie ustawisz - setterem; powstają w trakcie naliczania. -- `ListyPlac` to tabela operacyjna guided — przy odczycie filtruj zakresem (`WgDatyWyplaty`, - `WgOkresu`, `WgDefinicja`), nie skanuj całości (safe-code §6.3). -- `Wyplata.ListaPlac`/`Wyplata.Pracownik` to `IRow` (relacje interfejsowe) — porównuj/rzutuj - świadomie. - -### I2 — Drukowanie/PDF kwitków (pasków) wypłaty (★) - -**Cel:** wygenerować pasek (kwitek) wypłaty pracownika do PDF. - -**Mechanizm.** Wydruk realizuje serwis **`IReportService`** (namespace `Soneta.Business.UI`, -identycznie jak wydruki handlowe — patrz `dokument-handlowy.md` rozdz. 12). Wzorce pasków to -szablony `*.repx` zarejestrowane atrybutem `[DxReport]` w assembly -**`Soneta.KadryPlace.Reports`** dla `DataType = typeof(Soneta.Place.Wyplata)`: - -| Wzorzec (ReportName) | Plik szablonu (`TemplateFileName`) | `DataType` | -|---|---|---| -| „Pasek wypłaty" | `PasekWyplaty.repx` | `Soneta.Place.Wyplata` | -| „Duży pasek wypłaty" | `DuzyPasekWyplaty.repx` | `Soneta.Place.Wyplata` | -| „Paski wypłat" (zbiorczy) | `PaskiWyplaty.repx` | `Soneta.Place.ListaPlac` | - -**API (`IReportService` / `ReportResult` — `Soneta.Business.UI`):** -`Stream GenerateReport(ReportResult rr)`, -`ReportResult.TemplateFileName: string`, `.DataType: Type`, -`.OutputFormat: ReportFormats` (`PDF`), `.Context: Context`, `.Target: ReportTargets`. - -**Snippet (pasek jednej wypłaty do strumienia PDF):** - -```csharp -using Soneta.Business.UI; // IReportService, ReportResult, ReportFormats -using Soneta.Place; - -var raporty = session.GetRequiredService(); - -var context = new Context(session.Context); -context.Set(wyplata); // pojedyncza Wyplata - -var rr = new ReportResult { - TemplateFileName = "PasekWyplaty.repx", - DataType = typeof(Wyplata), - OutputFormat = ReportFormats.PDF, - Context = context, -}; - -using Stream pdf = raporty.GenerateReport(rr); // pierwsze 4 bajty == "%PDF" -``` - -**Pułapki:** -- `IReportService` pobierasz z kontenera: `session.GetRequiredService()` - (potrzebne `using Microsoft.Extensions.DependencyInjection;`). Serwis i silnik raportów - (DevExpress) oraz szablony pasków z `Soneta.KadryPlace.Reports` są dostępne **transytywnie** — - generowanie PDF działa bez dodatkowych referencji (wzorzec jak w `dokument-handlowy.md` rozdz. 12). -- Poprawny PDF zaczyna się od bajtów `"%PDF"` — to wygodna asercja w teście. -- Druk na fizyczną drukarkę (`Target = Printer`, `PrintReport`) wymaga sprzętu — NIE testować. - -### I3 — Drukowanie/PDF list płac (★) - -**Cel:** wygenerować wydruk całej listy płac (pełna lista, zestawienie wypłat) do PDF. - -**Mechanizm.** Identyczny jak I2 — `IReportService.GenerateReport`, szablony `[DxReport]` -w `Soneta.KadryPlace.Reports`, dla `DataType = typeof(Soneta.Place.ListaPlac)` / -`typeof(Soneta.Place.ListyPlac)`: - -| Wzorzec (ReportName) | Plik szablonu | `DataType` | -|---|---|---| -| „Pełna lista płac" | `PelnaListaPlac.repx` | `Soneta.Place.ListaPlac` | -| „Wspólna pełna lista płac" | `Wspolnapelnalistaplac.repx` | `Soneta.Place.ListyPlac` (zbiór) | -| „Paski wypłat" | `PaskiWyplaty.repx` | `Soneta.Place.ListaPlac` | -| Zestawienie wypłat | `ZestawienieWyplat.repx` | `Soneta.Place.ListaPlac` | - -**Snippet (pełna lista płac → PDF):** - -```csharp -using Soneta.Business.UI; -using Soneta.Place; - -var raporty = session.GetRequiredService(); - -var context = new Context(session.Context); -context.Set(listaPlac); // ListaPlac - -var rr = new ReportResult { - TemplateFileName = "PelnaListaPlac.repx", - DataType = typeof(ListaPlac), - OutputFormat = ReportFormats.PDF, - Context = context, -}; - -using Stream pdf = raporty.GenerateReport(rr); -``` - -**Pułapki:** -- Mechanizm i dostępność serwisu — jak w I2 (działa transytywnie, bez dodatkowych referencji). -- Lista musi być policzona (mieć `Wyplaty`) — inaczej wydruk będzie pusty. -- **Niektóre szablony list wymagają pełnego kontekstu danych.** W bazie Demo wzorzec - `PelnaListaPlac.repx` potrafi rzucić `InvalidOperationException` („Problem z przygotowaniem - raportu") na sztucznie utworzonej liście — to ograniczenie konkretnego szablonu/kontekstu, nie - brak referencji (pasek wypłaty `PasekWyplaty.repx` z I2 generuje się poprawnie). -- Do wydruku zbiorczego wielu list ustaw `DataType = typeof(Soneta.Place.ListyPlac)` i przekaż - zbiór przez `Context.Set(...)` / `ReportResult.Rows`. - - -### I4 — Generowanie przelewów wynagrodzeń (przygotowanie przelewów) (★) - -**Cel:** z naliczonej, zatwierdzonej listy płac wygenerować dokumenty przelewu wynagrodzeń -(do paczki przelewów), tak by wypłaty pracowników trafiły do zapłaty/preliminarza i mogły zostać -wyeksportowane do banku (I5). - -> **Dwie różne klasy `Wyplata` — nie myl ich.** W domenie współistnieją: -> - **`Soneta.Place.Wyplata`** (moduł `PlaceModule`, tabela `Wyplaty`) — *naliczona wypłata -> pracownika* (wynik naliczania z sekcji H/I1); to dokument **płacowy** ze składnikami -> (`Elementy`), powiązany z listą płac (`Wyplata.ListaPlac`). -> - **`Soneta.Kasa.Wyplata`** (moduł `KasaModule`, tabela `Wyplaty`/`Zaplaty`) — *zapłata kasowa* -> (rozchód środków). To **ona** implementuje `IDokumentPlatny`/`IDokumentKsiegowalny`, ma pola -> rozliczeniowe (`DoRozliczenia`, `Stan`, `StanRozliczenia`, `KwotaRozliczona`, `Rozliczono`, -> `Rozrachunki`, `Zaplaty`, `PreliminarzPoz`, `PozycjePrzelewu`, `BlokadaPrzelewow`). -> -> Mechanizm „z wypłaty do przelewu” łączy oba światy: worker płacowy czyta `Place.Wyplata` z listy -> płac i tworzy dokumenty przelewu w module Kasa (`Soneta.Kasa.PrzelewBase`, w paczce `PaczkaPrzelewow`). - -**Mechanizm (publiczny kontrakt — worker płacowy):** sprawdzoną ścieżką tworzenia przelewów z -wynagrodzeń jest worker **`Soneta.Place.ListaPlac.PrzygotujPrzelewyWorker`** (assembly -`Soneta.KadryPlace`, akcja menu *„Przygotuj przelewy”* na liście/listach płac). Kontekstem -działania jest **lista płac** (`Soneta.Place.ListaPlac`) — przygotowuje przelewy dla zatwierdzonych -wypłat tej listy. - -**Parametry — `PrzygotujPrzelewyWorker.Params`:** - -| Pole | Typ | Uwaga | -|---|---|---| -| `Data` | `Soneta.Types.Date` | data dokumentów przelewu | -| `Paczka` | `Soneta.Kasa.PaczkaPrzelewow` | istniejąca paczka, do której trafią przelewy (opcjonalnie) | -| `DefinicjaPaczki` | `Soneta.Kasa.DefinicjaPaczkiPrzelewu` | definicja, wg której utworzyć nową paczkę (gdy `Paczka == null`) | -| `ZRachunku` | `Soneta.Kasa.RachunekBankowyFirmy` | rachunek firmy obciążany przelewami | -| `Łączone` | `bool` | łączenie przelewów do jednego podmiotu w jeden dokument | -| `ListyPłac` | `string` | opis/oznaczenie list płac (informacyjnie w tytule) | -| `ModyfikacjaTytułów` | `bool` | czy nadpisać tytuły przelewu (`Tytułem1`/`Tytułem2`) | -| `Tytułem1`, `Tytułem2` | `string` | tytuł przelewu (gdy `ModyfikacjaTytułów == true`) | -| `ZEwidencjiZrodlowej` | `bool` | bierz dane rachunku z ewidencji źródłowej | - -**Akcja:** `object PrzygotujPrzelewy()` — tworzy w sesji dokumenty `Soneta.Kasa.PrzelewBase` -(tabela `Przelewy`) w paczce `PaczkaPrzelewow`; utrwalenie w bazie wymaga `session.Save()`. - -**Model dokumentu przelewu (`Soneta.Kasa.PrzelewBase`, tabela `Przelewy`, root `GuidedRow`):** - -| Pole | Typ | Opis | -|---|---|---| -| `Kwota` | `Soneta.Types.Currency` | kwota przelewu | -| `Podmiot` | `Soneta.Kasa.IPodmiotKasowy` | odbiorca (m.in. `Pracownik`, `ZUS`, `UrzadSkarbowy`, `Bank`, `Kontrahent`) | -| `Rachunek` | `Soneta.Kasa.RachunekBankowyPodmiotu` | rachunek odbiorcy | -| `RachunekZleceniodawcy` | `Soneta.Kasa.NumerRachunku` | rachunek firmy (obciążany) | -| `Data` | `Soneta.Types.Date` | data przelewu | -| `Definicja` | `Soneta.Core.DefinicjaDokumentu` | definicja dokumentu | -| `Numer` | `Soneta.Core.NumerDokumentu` | numer (nadawany automatycznie) | -| `Tytulem1`, `Tytulem2` | `string` | tytuł przelewu | -| `Typ2` | `Soneta.Kasa.TypPrzelewu2` | wariant przelewu (zwykły / **MPP** / itp.) | -| `PaczkaPrzelewow` | `Soneta.Kasa.PaczkaPrzelewow` | paczka, do której należy przelew | -| `Bufor` / `Zatwierdzony` | `bool` | stan dokumentu | -| `Exported` | `bool` | czy wyeksportowany (po I5 — `true`, blokuje edycję) | - -**Przelewy okresowe / MPP:** -- **MPP (mechanizm podzielonej płatności)** to *wariant* przelewu — wyrażany przez - `PrzelewBase.Typ2: Soneta.Kasa.TypPrzelewu2` (oraz na `Kasa.Wyplata` polem `KwotaMPP`, - `MozliweMechanizmyMPP`). Dla wynagrodzeń MPP zwykle nie dotyczy (to mechanizm faktur VAT), ale - kontrakt go przewiduje. -- **Przelewy okresowe** (cykliczne płatności np. składek z list) realizuje osobny worker - księgowy `Soneta.Ksiega.Kasowe.NaliczaniePrzelewowOkresowych` (poza zakresem płac pracownika). - -**Powiązanie z wypłatą / preliminarzem (publiczne kolekcje na `Pracownik`):** - -| Kolekcja na `Pracownik` | Typ | Zawiera | -|---|---|---| -| `Pracownik.Przelewy` | `SubTable` | przelewy pracownika | -| `Pracownik.DokumentyPreliminarza` | `SubTable` | dokumenty preliminarza | -| `Pracownik.DokumentyRozliczeniowe` | `SubTable` | dokumenty rozliczeniowe | -| `Pracownik.Rozrachunki` | `SubTable` | rozrachunki | -| `Pracownik.Rachunki` | `SubTable` | rachunki bankowe pracownika | - -> **Korekta (zweryfikowane kompilacją + skanem DLL):** `Pracownik.Platnosci` **nie istnieje** w publicznym -> kontrakcie kartoteki pracownika — kolekcja `Platnosci` występuje tylko na interfejsie -> `Soneta.Kasa.IDokumentPlatny` (np. `Kasa.Wyplata.Platnosci`), nie na `Pracownik`. Płatności podmiotu -> czytaj przez `Pracownik.Rozrachunki` / `Pracownik.DokumentyRozliczeniowe`. - -**Snippet (worker — w UI/teście z dostępnym `Context`):** - -```csharp -using Soneta.Business; -using Soneta.Place; // ListaPlac, ListaPlac.PrzygotujPrzelewyWorker -using Soneta.Kasa; // PaczkaPrzelewow, PrzelewBase, RachunekBankowyFirmy -using Soneta.Types; - -// listaPlac: zatwierdzona lista płac z naliczonymi wypłatami (sekcja I1) -var pars = new ListaPlac.PrzygotujPrzelewyWorker.Params -{ - Data = Date.Today, - // Paczka = istniejacaPaczka, // albo nowa wg DefinicjaPaczki: - // DefinicjaPaczki = session.GetKasa().DefPaczekPrzelewow.WgSymbolu["..."], - // ZRachunku = rachunekFirmy, // RachunekBankowyFirmy - Łączone = false, -}; - -var worker = new ListaPlac.PrzygotujPrzelewyWorker { Pars = pars }; -// kontekstem workera jest lista płac; uruchomienie akcji: -worker.PrzygotujPrzelewy(); - -session.Save(); // utrwalenie dokumentów przelewu w bazie -``` - -**Pułapki / ograniczenia (bądź szczery):** -- **`Place.Wyplata` ≠ `Kasa.Wyplata`** — pola rozliczeniowe (`DoRozliczenia`, `Stan`, - `StanRozliczenia`, `Rozrachunki`, `BlokadaPrzelewow`) są na **kasowej** `Soneta.Kasa.Wyplata` - (`IDokumentPlatny`), nie na płacowej. Skanując „Wyplata” trafia się na kasową. -- **Lista płac musi być zatwierdzona i naliczona** — `PrzygotujPrzelewy` na pustej/niezatwierdzonej - liście nie ma czego przelać. -- **Wymaga konfiguracji modułu Kasa** — definicji paczki przelewów (`DefinicjaPaczkiPrzelewu`), - rachunku firmy (`RachunekBankowyFirmy`) oraz rachunku pracownika (`Pracownik.Rachunki`). Brak - rachunku odbiorcy → przelew nie powstanie albo będzie niekompletny. **W bazie Demo te elementy - mogą nie być skonfigurowane**, dlatego generowanie przelewów w teście jednostkowym jest - niepewne (patrz spec testowy). -- Worker **sam zatwierdza zmiany w sesji** (otwiera transakcję) — nie owijaj w dodatkowy - `session.Logout(true)`; do bazy idą w `Save()`. -- `PrzelewBase.Podmiot`/`Powiazanie` to relacje **interfejsowe** (`IRow`/`IPodmiotKasowy`) — - rzutuj świadomie. -- `Przelewy` to tabela operacyjna guided — przy odczycie filtruj zakresem (safe-code §6.3). - ---- - -### I5 — Eksport wynagrodzeń do banku / pliku przelewów (★) - -> **UWAGA — operacja plikowa/integracyjna.** Eksport zapisuje **fizyczny plik** w formacie -> bankowym (Elixir, MT940-pochodne, formaty walutowe). To wejście/wyjście do systemu zewnętrznego — -> **nie jest to przedmiot testu jednostkowego** (zależy od ścieżki na dysku, formatu banku, -> sterownika eksportu i — przy wysyłce online — od sieci). Dokumentujemy **model i publiczny -> kontrakt**, a sam eksport pliku oznaczamy jako nietestowalny jednostkowo. - -**Cel:** wyeksportować przygotowane przelewy (I4) do pliku przelewów dla systemu bankowości -elektronicznej. - -**Mechanizm (publiczny kontrakt — worker Kasa):** worker **`Soneta.Kasa.EksportPrzelewowWorker`** -(akcja menu *„Eksport przelewów”*, metoda `Eksport()`), sterowany przez -`Soneta.Kasa.EksportPrzelewowParams`. - -> **Korekta (zweryfikowane kompilacją):** `EksportPrzelewowParams` **nie ma konstruktora -> bezparametrowego** — wymaga `EksportPrzelewowParams(Context ctx, RachunekBankowyFirmy rachunek, PrzelewBase[] przelewy)`. -> Co więcej, **sam konstruktor waliduje rachunek** i rzuca `System.ApplicationException` -> („Eksport niemożliwy. Nie wskazano rachunku w filtrach listy.”), gdy `rachunek == null`. Dlatego nie da się -> utworzyć parametrów samym inicjalizatorem obiektu. W teście jednostkowym kontrakt API weryfikuj **refleksją** -> (istnienie typu, sygnatura konstruktora, property `FileName`/`Params`, metoda `Eksport`), bez instancjonowania. - -**Parametry — `Soneta.Kasa.EksportPrzelewowParams`:** - -| Pole | Typ | Uwaga | -|---|---|---| -| `FileName` | `string` | **ścieżka pliku wyjściowego** — operacja na dysku | -| `AppendToFile` | `bool` | dopisanie do istniejącego pliku | -| `PrzelewyZgodne` | `IList` | przelewy do wyeksportowania | -| `Rachunek` | `Soneta.Kasa.RachunekBankowyFirmy` | rachunek firmy (zleceniodawca) | -| `PrmDataPrzelewow` | `Soneta.Types.Date` | data realizacji | -| `PrmNumerPaczki` | `string` | numer paczki | -| `PrmZakres` | `Soneta.Kasa.ZakresEksportuPrzelewow` | zakres (wszystkie / wg paczki / zaznaczone) | -| `EksportujWBuforze` | `bool` | uwzględnij przelewy w buforze | -| `InfoBank`, `InfoFormatKraj`, `InfoFormatWalutowy`, `InfoRachunekBankowy` | `string` | parametry formatu/banku | -| `WithoutHelper` | `bool` | tryb bez kreatora | - -**Akcja:** `object Eksport()` — zapisuje plik wg `FileName`. Po eksporcie przelewy są oznaczane -jako wyeksportowane (`PrzelewBase.Exported == true`, blokada dalszej edycji). - -**Powiązane (kontekst):** -- Eksport całych **paczek**: worker `Soneta.Kasa.EksportPaczekPrzelewowWorker`. -- Eksport przelewów PPK z pulpitu KBR: `Soneta.EI.UI.PulpitKBR.Workers.PulpitKBEksportPrzelewowWorker`. - -**Snippet (kontrakt — w realnej integracji, nie w teście jednostkowym):** - -```csharp -using Soneta.Kasa; // EksportPrzelewowWorker, EksportPrzelewowParams, PrzelewBase -using System.Collections.Generic; - -PrzelewBase[] przelewy = /* przelewy z I4, np. z paczki */; - -// Konstruktor jest WYMAGANY (brak ctora bezparametrowego) i waliduje rachunek (rzuca, gdy null): -var par = new EksportPrzelewowParams(context, rachunekFirmy, przelewy) // rachunekFirmy: RachunekBankowyFirmy -{ - FileName = @"C:\przelewy\wynagrodzenia.txt", // ŚCIEŻKA PLIKU — operacja I/O - PrmDataPrzelewow = Date.Today, - EksportujWBuforze = false, -}; - -var worker = new EksportPrzelewowWorker { Params = par }; -worker.Eksport(); // zapis pliku na dysk — efekt uboczny poza sesją -``` - -**Pułapki / ograniczenia (bądź szczery):** -- **Eksport pliku NIE nadaje się do testu jednostkowego** — pisze na dysk, zależy od formatu banku - i sterownika eksportu; w teście co najwyżej dokumentujemy istnienie API - (`EksportPrzelewowWorker`, `EksportPrzelewowParams.FileName`), bez wywołania `Eksport()`. -- Format pliku zależy od **konfiguracji formatu eksportu** danego banku — nie ma jednego - uniwersalnego formatu; `InfoFormat*`/`InfoBank` parametryzują wynik. -- Wysyłka online (bankowość elektroniczna / API banku) to dodatkowo operacja **sieciowa** — poza - zakresem testów jednostkowych. -- Po eksporcie `PrzelewBase.Exported = true` blokuje edycję — ponowny eksport wymaga - `EksportujWBuforze`/zmiany stanu. - ---- - -### I6 — Wystawienie faktury / faktury zbiorczej z zapłaty (rozliczenia) (★) - -> **Zakres i szczerość.** Faktura jest dokumentem **handlowym** (`Soneta.Handel.DokumentHandlowy`), -> nie płacowym — to nie jest funkcja kartoteki pracownika ani list płac. Powiązanie „z zapłaty” -> dotyczy **rozrachunków/rozliczeń** (moduł Kasa): zapłata (`Soneta.Kasa.Wyplata`/`Wplata` — -> `IDokumentPlatny`) jest **rozliczana** z dokumentem płatnym (np. fakturą) przez rozrachunki. -> Wystawianie faktury z poziomu pracownika/płac w publicznym kontrakcie **nie istnieje**; -> tutaj dokumentujemy model rozliczeń, który łączy zapłatę z fakturą. - -**Cel:** powiązać zapłatę z dokumentem płatnym (fakturą) na poziomie rozrachunków/rozliczeń — -oraz wskazać, gdzie w publicznym API leży rozliczanie należności/zobowiązań pracownika. - -**Model rozliczeń (publiczny kontrakt, moduł `KasaModule`):** - -| Element | Typ / kolekcja | Rola | -|---|---|---| -| Zapłata (rozchód/wpływ) | `Soneta.Kasa.Wyplata` / `Soneta.Kasa.Wplata` | dokument płatny (`IDokumentPlatny`) | -| Płatność (zobowiązanie/należność) | `Soneta.Kasa.Platnosc` (tabela `Platnosci`, `IRozliczalny`) | to z nią rozlicza się zapłatę | -| Rozliczenie (powiązanie SP) | `Soneta.Kasa.RozliczenieSP` (tabela `RozliczeniaSP`, `IRozliczenie`) | wiąże zapłatę z płatnością/dokumentem | -| Rozrachunek | `Soneta.Kasa.RozrachunekIdx` (tabela `RozrachunkiIdx`) | indeks rozrachunkowy podmiotu | -| Stan rozliczenia zapłaty | `Wyplata.StanRozliczenia: Soneta.Kasa.StanRozliczenia`, `Wyplata.DoRozliczenia`, `Wyplata.KwotaRozliczona`, `Wyplata.Rozliczono` | ile pozostało / czy rozliczono | - -**Kolekcje na zapłacie (`Soneta.Kasa.Wyplata`):** -- `Wyplata.Zaplaty: SubTable` oraz `Wyplata.Dokumenty: SubTable` — - rozliczenia, -- `Wyplata.Rozrachunki: SubTable` — rozrachunki, -- `Wyplata.PreliminarzPoz: PreliminarzPozycja` — pozycja preliminarza. - -**Kolekcje na `Pracownik` (rozrachunki/faktury podmiotu):** -- `Pracownik.Rozrachunki`, `Pracownik.DokumentyRozliczeniowe`, - `Pracownik.DokumentyPreliminarza` (jak w tabeli I4). **Uwaga:** `Pracownik.Platnosci` **nie istnieje** — - kolekcja `Platnosci` jest tylko na `IDokumentPlatny` (np. `Kasa.Wyplata.Platnosci`). - -**Workery rozliczeniowe (publiczny kontrakt, akcje menu):** - -| Worker | Rola | -|---|---| -| `Soneta.Kasa.RozliczWgPrzelewowWyplataWorker` | rozliczenie zapłaty wg przelewów | -| `Soneta.Kasa.RozliczPreliminarzIdxWorker` / `...TblWorker` / `...FrmWorker` | rozliczenie z preliminarzem | -| `Soneta.Kasa.PreliminarzPozycja.DodajRozliczenieWorker` | dodanie rozliczenia do pozycji preliminarza | -| `Soneta.Ksiega.UtworzPlatnoscZZapisuWorker` | utworzenie płatności z zapisu (księga) | - -**Faktura zbiorcza:** powstaje po stronie **handlowej** — z wielu zapłat/płatności tworzy się jeden -dokument handlowy (faktura) zbiorąc je jako rozliczenia. To domena `dokument-handlowy.md` -(wystawianie i rozliczanie faktur), nie kartoteki pracownika. Z poziomu rozliczeń pracownika -publiczny kontrakt udostępnia **odczyt i rozliczanie** rozrachunków, a nie „wystaw fakturę”. - -**Snippet (odczyt stanu rozliczenia zapłat — publiczny kontrakt):** - -```csharp -using Soneta.Kasa; // Wyplata, StanRozliczenia -using Soneta.Types; - -// Zapłaty pracownika rozliczane z dokumentami (np. fakturami) — odczyt stanu rozliczeń. -// Iteruj zawsze w zakresie/okresie (tabela operacyjna guided — safe-code §6.3). -foreach (RozrachunekIdx r in pracownik.Rozrachunki) -{ - // r — pozycja rozrachunkowa pracownika (powiązanie zapłata ↔ dokument) -} - -// Stan rozliczenia konkretnej zapłaty kasowej: -// Wyplata zaplata = ...; -// var doRozl = zaplata.DoRozliczenia; // ile pozostało do rozliczenia (Currency) -// var stan = zaplata.StanRozliczenia; // StanRozliczenia (enum) -// var czyRozl = zaplata.Rozliczono; // bool -``` - -**Pułapki / ograniczenia (bądź szczery):** -- **„Wystaw fakturę z pracownika/płac” nie istnieje w publicznym kontrakcie.** Faktura to dokument - handlowy; powiązanie z zapłatą realizują **rozrachunki/rozliczenia** (moduł Kasa), nie kartoteka - pracownika. To zadanie jest z pogranicza domen — opis kierujemy do `dokument-handlowy.md`. -- Pola rozliczeniowe (`DoRozliczenia`, `Stan`, `StanRozliczenia`, `KwotaRozliczona`, `Rozliczono`, - `Rozrachunki`) są na **`Soneta.Kasa.Wyplata`** (`IDokumentPlatny`), a nie na płacowej - `Soneta.Place.Wyplata`. -- Rozliczanie/tworzenie faktury zbiorczej **wymaga skonfigurowanego modułu Kasa/Handel** (definicje - dokumentów, rachunki, płatności). W bazie Demo część konfiguracji może nie być gotowa — operacje - zapisujące są niepewne w teście (patrz spec testowy). -- `Platnosc`/`RozliczenieSP`/`RozrachunekIdx` to obiekty operacyjne — przy odczycie filtruj zakresem - i nie skanuj całych tabel (safe-code §6.3). - ---- - -#### Spec testowy (zwarty) — I4 / I5 / I6 - -Konwencja: `Soneta.Skills.Test/KadryPlace/Pracownik/`, klasa `RozdzialI_ListyWydrukiTest` -(lub nowa `RozdzialI_PrzelewyRozliczeniaTest : PracownikTestBase`); baza Demo + rollback; -operujemy wyłącznie na publicznym kontrakcie. - -**I4 — `I4_PrzygotujPrzelewy_ZListyPlac`** -- *Co testowalne:* naliczenie wypłaty etatowej (`NaliczanieSeryjne.Pracownika`, jak I1b) → uzyskanie - `ListaPlac` z `Wyplata.ListaPlac`; **konstrukcja** `ListaPlac.PrzygotujPrzelewyWorker` z `Params` - (asercja, że worker i typ `Params` istnieją w publicznym API; że pola `Data`/`Paczka`/`ZRachunku` - są dostępne). Odczyt kolekcji `Pracownik.Przelewy`, `Pracownik.DokumentyPreliminarza`, - `Pracownik.Rozrachunki` (asercja: kolekcje dostępne, iterowalne). -- *Niepewne / `[Ignore]`/`Assert.Ignore`:* faktyczne **wywołanie** `worker.PrzygotujPrzelewy()` i - powstanie dokumentów `PrzelewBase` — zależy od konfiguracji modułu Kasa (definicja paczki, - `RachunekBankowyFirmy`, rachunek pracownika `Pracownik.Rachunki`), której baza Demo nie gwarantuje. - Owinąć w `try/catch` + `Assert.Ignore` z opisem (wzorzec jak I2/I3) i asercję na powstaniu - przelewu robić tylko, gdy się udało. - -**I5 — `I5_EksportPrzelewow_KontraktApi`** -- *Co testowalne:* **istnienie publicznego API** — weryfikacja **refleksją** (NIE instancjonuj!): - typ `EksportPrzelewowParams`, konstruktor `(Context, RachunekBankowyFirmy, PrzelewBase[])`, - property `FileName`; typ `EksportPrzelewowWorker`, property `Params`, metoda `Eksport`. - **Nie używaj inicjalizatora `new EksportPrzelewowParams { ... }`** — nie ma ctora bezparametrowego, - a ctor `(ctx, rachunek, przelewy)` rzuca `ApplicationException`, gdy `rachunek == null` (brak konfiguracji w Demo). -- *Niewykonalne w teście jednostkowym → `[Ignore]`:* wywołanie `worker.Eksport()` — **operacja - plikowa** (zapis na dysk wg `FileName`), zależna od formatu banku/sterownika; wysyłka online = - operacja sieciowa. **Nie wołać `Eksport()`** w teście; udokumentować jako `[Ignore("operacja - plikowa/sieciowa — poza testem jednostkowym")]`. - -**I6 — `I6_Rozliczenia_OdczytStanu`** -- *Co testowalne:* odczyt kolekcji rozliczeniowych pracownika — `Pracownik.Rozrachunki`, - `Pracownik.DokumentyRozliczeniowe`, `Pracownik.DokumentyPreliminarza` - (asercja: dostępne, iterowalne, typy zgodne — `RozrachunekIdx`, `DokRozliczBase`, - `PreliminarzDokument`). **`Pracownik.Platnosci` NIE istnieje** — pomiń (kolekcja `Platnosci` jest tylko na - `IDokumentPlatny`); odczyt pól rozliczeniowych z `Soneta.Kasa.Wyplata` (`DoRozliczenia`, - `Stan`, `StanRozliczenia`, `Rozliczono`) — gdy istnieje zapłata kasowa w Demo. -- *Niewykonalne / `[Ignore]`:* **wystawienie faktury (zbiorczej) z zapłaty** — funkcja handlowa, - brak w kontrakcie pracownika; rozliczanie zapisujące (`RozliczWgPrzelewowWyplataWorker`, - `RozliczPreliminarz*Worker`) wymaga konfiguracji Kasa/Handel → `Assert.Ignore` przy braku danych. - Dla wystawiania faktur kierować do testów domeny handlowej (`dokument-handlowy.md`). - -**Dokładne nazwy (do użycia w testach):** -- Worker płacowy: `Soneta.Place.ListaPlac.PrzygotujPrzelewyWorker` (+ zagn. `.Params`; - akcja `PrzygotujPrzelewy`). -- Worker eksportu: `Soneta.Kasa.EksportPrzelewowWorker` + `Soneta.Kasa.EksportPrzelewowParams` - (akcja `Eksport`); paczki: `Soneta.Kasa.EksportPaczekPrzelewowWorker`. -- Dokumenty: `Soneta.Kasa.PrzelewBase` (tabela `Przelewy`), `Soneta.Kasa.PaczkaPrzelewow` - (tabela `PaczkiPrzelewow`), `Soneta.Kasa.DefinicjaPaczkiPrzelewu`, `Soneta.Kasa.RachunekBankowyFirmy`. -- Rozliczenia: `Soneta.Kasa.Platnosc`, `Soneta.Kasa.RozliczenieSP`, `Soneta.Kasa.RozrachunekIdx`, - `Soneta.Kasa.PreliminarzDokument`, `Soneta.Kasa.PreliminarzPozycja`. -- Zapłata kasowa (`IDokumentPlatny`): `Soneta.Kasa.Wyplata` (NIE `Soneta.Place.Wyplata`). -- Kolekcje na `Pracownik`: `Przelewy`, `Rozrachunki`, `DokumentyPreliminarza`, - `DokumentyRozliczeniowe`, `Rachunki` (**bez `Platnosci`** — ta kolekcja jest tylko na `IDokumentPlatny`). - -## J. Deklaracje (ZUS, PIT, PFRON, PPK) - -> **Moduł.** `Soneta.Deklaracje.DeklaracjeModule` — dostęp z sesji przez `session.GetDeklaracje()`. -> Wszystkie deklaracje (ZUS, PIT, PFRON, PPK) to wiersze tabeli `Deklaracje`, dziedziczące po -> abstrakcyjnej klasie root `Soneta.Deklaracje.Deklaracja` (`GuidedRow`, implementuje m.in. -> `IDeklaracja`, `IDokumentPlatny`, `IDokumentKsiegowalny`). Konkretne typy żyją w podprzestrzeniach: -> `Soneta.Deklaracje.ZUS.*`, `Soneta.Deklaracje.PIT.*`, `Soneta.Deklaracje.PFRON.*`, -> `Soneta.Deklaracje.PPK.*`. -> -> **Rozróżnienie kluczowe dla testów — NALICZENIE/UTWORZENIE vs E-WYSYŁKA.** -> - **Naliczenie/utworzenie deklaracji** (workery `*Worker` z akcjami „Przygotuj…/Nalicz…/Przelicz”, -> operacje PPK) tworzy **wiersze w bazie** — to operacja lokalna, w zasadzie testowalna na Demo, -> ale **wymaga `Context`** (i dla ZUS zwykle obiektu `KEDU`). Workery nie mają konstruktorów -> bezparametrowych dających pełny kontrakt — `Params` budujemy z `Context`/`Session`. -> - **E-wysyłka** to osobne typy: `EDeklaracja` (tabela `EDeklaracje` — XML, podpis, UPO) oraz -> `ETransmisja` (tabela `ETransmisje` — pojedyncze transmisje do bramki). Eksport KEDU/PUE realizują -> workery `Soneta.Deklaracje.UI.KeduEksportForm.EksportWorker` (akcje „Eksport KEDU”, „Pobierz KEDU”) -> i `Soneta.Deklaracje.UI.PUEEksportForm.EksportWorker` (akcja „Eksport PUE (RUD)”), a uruchomienie -> Programu Płatnika — `Soneta.Deklaracje.ZUS.DeklaracjaZUS.UruchomPPWorker` (akcja -> „Uruchom 'Program Płatnika'”). **To operacje sieciowe/plikowe/zewnętrzne — NIE do testu** (nawet -> utworzenie `EDeklaracja` wymaga podpisu i bramki ZUS/US). -> -> **`KEDU` (`Soneta.Deklaracje.ZUS.KEDU`)** — „zestaw deklaracji”: kontener (komplet dokumentów ZUS), -> do którego workery zgłoszeniowe i rozliczeniowe dopinają wygenerowane bloki. Praktycznie każdy worker -> ZUS przyjmuje `Kedu` w swoich `Params`; bez przekazanego `KEDU` generowanie deklaracji ZUS nie ma -> gdzie zapisać wyniku. KEDU nie jest tworzony „w locie” w sposób trywialny — jest częścią mechanizmu -> deklaracji rozliczeniowych ZUS i jego zbudowanie wymaga środowiska modułu Deklaracje (`Context`). - ---- - -### J1 — Zgłoszenia ZUS (ZUA/ZZA, ZCNA, ZWUA) - -**Cel:** zgłosić/wyrejestrować pracownika i jego umowy w ZUS oraz zgłosić członków rodziny do -ubezpieczenia zdrowotnego. Typy zgłoszeń to wiersze deklaracji: `ZUA` (społeczne + zdrowotne), -`ZZA` (tylko zdrowotne), `ZCNA` (rodzina), `ZWUA` (wyrejestrowanie), `ZIUA` (zmiana danych -identyfikacyjnych), `ZCZA` (zmiana danych członka rodziny) — wszystkie w `Soneta.Deklaracje.ZUS`. - -**Workery — poziom `Pracownicy` (klasy zagnieżdżone `Soneta.Deklaracje.ZUS.ZarejestrujPracownikówWorker`):** - -| Worker (akcja) | `Params` (typ) | Pola `Params` | Metoda akcji | -|---|---|---|---| -| `ZarejestrujPracownikówWorker.Rejestracja` — „Deklaracje ZUS/Przygotuj ZUA i ZZA” | `ZarejestrujBaseWorker.ParamsKor` | `Okres: FromTo`, `DataDokumentu`/`DataWypełnienia: Date`, `Kedu: KEDU`, `KorektaZmiana: ZgloszenieZUS.KorektaZmiana`, `ZarejestrujRodzinę: bool` | `object ZarejestrujPracowników()` | -| `ZarejestrujPracownikówWorker.Rodzina` — „Deklaracje ZUS/Przygotuj ZCNA” | `ZarejestrujBaseWorker.Params` | `Okres`, `DataDokumentu`, `DataWypełnienia`, `Kedu` | `object ZarejestrujRodzinę()` | -| `ZarejestrujPracownikówWorker.Wyrejestrowanie` — „Deklaracje ZUS/Przygotuj ZWUA” | `Wyrejestrowanie.ParamsWR` | `Okres`, `DataDokumentu`, `DataWypełnienia`, `Kedu`, `RIA: bool`, `WyrejestrujRodzinę: bool` | `object WyrejestrujPracowników()` | -| `ZarejestrujPracownikówWorker.ZgloszenieUmow` — „Deklaracje ZUS/Przygotuj RUD” | `ZgloszenieUmow.UParams` | `Okres`, `DataWypełnienia`, `Kedu`, `Trwajace: bool` | `object ZgłośUmowy()` | - -> Worker przyjmuje zaznaczone osoby przez `Pracownicy: Pracownik[]` (`[Context]`). Wszystkie `Params` -> mają ctor `(Context)`. Po akcji wynik (lista wygenerowanych deklaracji) odczytasz z bazowego -> `Deklaracje: View`, a `Save()` zatwierdza. - -**Workery — poziom `Umowy` (zleceniobiorcy), `Soneta.Deklaracje.ZUS.ZarejestrujUmowyWorker`** — -opisane w **G5** (`Rejestracja.ZarejestrujUmowy()` → ZUA/ZZA wg schematu `UmowaHistoria.Ubezpieczenia`, -`Wyrejestrowanie.WyrejestrujUmowy()` → ZWUA). `ParamsZ`/`ParamsW` mają ctor `(Context)`; pola -bazowe `Okres`/`DataDokumentu`/`DataWypełnienia`/`Kedu` + `ZarejestrujRodzinę`/`WyrejestrujRodzinę`. - -**ZCNA na rodzinie (A9).** Zgłoszenie członka rodziny do ubezpieczenia zdrowotnego startuje z danych -`CzlonekRodziny` (`Ubezpieczony = true`, `UbezpieczenieOkres`, `StPokrewienstwa` — patrz A9), a samą -deklarację ZCNA generuje `ZarejestrujPracownikówWorker.Rodzina` (lub `Rejestracja` z -`Pars.ZarejestrujRodzinę = true`). Dla zleceniobiorcy analogicznie przez `ZarejestrujUmowyWorker`. - -**Przerejestrowanie (A19).** `Soneta.Deklaracje.UI.PrzerejestrowaniePracownikaWorker` (DataType -`PracHistoria`) oraz `Soneta.Deklaracje.UI.PrzerejestrowanieZleceniobiorcyWorker` (DataType -`UmowaHistoria`) — generują ZWUA+ZUA przy zmianie tytułu/wydziału. `Params` wymaga `KEDU` + `Context`. - -**Snippet (przygotowanie ZUA/ZZA dla zaznaczonych pracowników):** - -```csharp -var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; - -var pars = new Soneta.Deklaracje.ZUS.ZarejestrujBaseWorker.ParamsKor(context) -{ - Okres = new FromTo(new Date(2026, 1, 1), Date.MaxValue), - DataDokumentu = new Date(2026, 1, 1), - DataWypełnienia = Date.Today, - Kedu = kedu, // KEDU z modułu Deklaracje (Context) - ZarejestrujRodzinę = false, -}; -var rejestracja = new Soneta.Deklaracje.ZUS.ZarejestrujPracownikówWorker.Rejestracja -{ - Pracownicy = new[] { pracownik }, - Pars = pars, -}; -rejestracja.ZarejestrujPracowników(); // tworzy ZUA/ZZA (i ZCNA, gdy ZarejestrujRodzinę) -session.Save(); -``` - -**Pułapki:** -- Typ zgłoszenia (ZUA vs ZZA) wynika ze schematu ubezpieczeń (`Etat.Ubezpieczenia` / `UmowaHistoria.Ubezpieczenia`, - A7/G5) — nie z parametru workera. Ustaw `Tyub4` i flagi `Spoleczne`/`Zdrowotne` przed zgłoszeniem. -- Każdy `Params` wymaga `Context` (ctor `(Context)`) i pola `Kedu` — bez `KEDU` deklaracja nie ma - kontenera docelowego. Operacja jest **lokalna** (zapis wiersza), ale niewykonalna bez `Context`/`KEDU`. -- `ZWUA` z `RIA = true` powiązany jest z mechanizmem RIA (J2). -- Workery zgłoszeniowe na `Pracownicy` dotyczą etatowych; na `Umowy` — zleceniobiorców (G5). - ---- - -### J2 — Deklaracje rozliczeniowe ZUS (DRA, RIA, IMIR, RUD, IWA; KEDU) - -**Cel:** naliczyć/utworzyć deklaracje rozliczeniowe i informacyjne ZUS. Typy (`Soneta.Deklaracje.ZUS`, -wiersze tabeli `Deklaracje`): `DRA` (deklaracja rozliczeniowa z załącznikami RCA/RSA/RZA; ctor `(KEDU)`), -`RIA` (informacja roczna / raport po ustaniu zatrudnienia; ctor `(Pracownik, KEDU)`), `RMUA` — -informacja miesięczna dla ubezpieczonego, potocznie **IMIR** (ctor `(Pracownik, RMUA.TypOkresuDeklaracji)`; -**brak osobnego typu `IMIR` w CLR — to `RMUA`**), `RUD` (zgłoszenie umowy o dzieło), `IWA` (informacja o wypadkach/składce wypadkowej), -`OSW` (oświadczenie), `Z3`/`Z3a` (zaświadczenia płatnika ERP-7 — patrz niżej), `KEDU` (zestaw). - -**Naliczanie seryjne — poziom `Pracownicy`:** - -| Worker (akcja) | `Params` (typ) | Pola `Params` | Metoda | -|---|---|---|---| -| `Soneta.Deklaracje.ZUS.NaliczanieSeryjneRIAWorker` — „Deklaracje ZUS/Przygotuj RIA” | `…RIAWorker.Params` | `DataDokumentu`/`DataWypełnienia: Date`, `Kedu: KEDU`, `Wydział: Wydzial`, `Wszystkie: bool`, `Zerowa: bool` | `object NaliczRMUA(Context)` | -| `Soneta.Deklaracje.ZUS.NaliczanieSeryjneRMUAWorker` — „Deklaracje ZUS/Przygotuj IMIR” | `…RMUAWorker.Params` | `DataDokumentu`/`DataWypełnienia: Date`, `Miesiac: YearMonth`, `Rok: int`, `TypOkresu: RMUA.TypOkresuDeklaracji`, `Oskladkowani: bool`, `Wydział`, `Wszystkie` | `object NaliczRMUA(Context)` | - -> Oba workery mają **ctor bezparametrowy**, przyjmują `Pracownicy: Pracownik[]` (`[Context]`) i mają w props -> `Context`, `Kedu`, `Deklaracje: View`. Metoda akcji `NaliczRMUA(Context)` (ta sama nazwa dla RIA i RMUA). -> `Params` są property `Pars` (setter); na workerze `RMUAWorker` pola `Params` są też wystawione bezpośrednio jako property. - -**Przeliczenie pojedynczej deklaracji — `Soneta.Deklaracje.DeklaracjaWorker`** (DataType `Deklaracja`, -więc działa dla **dowolnej** deklaracji ZUS/PIT/PFRON): akcja **„Przelicz”** → `void Przelicz()`; -parametr `Deklaracja: Soneta.Deklaracje.Deklaracja` (`[Context]`). - -**RUD** generuje `ZarejestrujPracownikówWorker.ZgloszenieUmow` (J1) lub jest dostępna na liście umów. -**DRA z załącznikami** to root `DeklaracjaZUS`; nalicza się przez mechanizm KEDU + `Przelicz`. - -**E-wysyłka (NIE testować):** eksport KEDU — `KeduEksportForm.EksportWorker` („Eksport KEDU”, -„Pobierz KEDU”); eksport PUE/RUD — `PUEEksportForm.EksportWorker` („Eksport PUE (RUD)”); Program -Płatnika — `DeklaracjaZUS.UruchomPPWorker`. - -**Pułapki:** -- `KEDU` jest osią całego rozliczenia ZUS — wszystkie workery rozliczeniowe wpisują wynik do - przekazanego `Kedu`. Bez modułu Deklaracje (`Context`) i `KEDU` operacji nie złożysz. -- `DeklaracjaWorker.Przelicz()` przelicza **istniejący** wiersz deklaracji — najpierw musi powstać - (np. z naliczania seryjnego), więc to nie jest „utworzenie od zera”. - ---- - -### J3 — Deklaracje PIT (PIT-11, PIT-4R, PIT-8AR, PIT-R, IFT-1/IFT-1R, PIT-8C) - -**Cel:** naliczyć imienne i zbiorcze deklaracje podatkowe. Typy (`Soneta.Deklaracje.PIT`, wiersze -tabeli `Deklaracje`): `PIT11`, `PIT4`/PIT-4R (rozliczeniowa zaliczek), `PIT8A`/PIT-8AR (zryczałtowany), -`PITR` (PIT-R), `IFT1`/`IFT1R`, `PIT8C`, `PIT40`, plus `ZbiorczaPIT`/`IEDeklaracjaZbiorczaItem` -(deklaracje zbiorcze). - -**Naliczanie seryjne — poziom `Pracownicy` (klasy zagnieżdżone `Soneta.Deklaracje.PIT.NaliczanieSeryjne`):** - -| Worker (akcja) | Ctor | `Params` — pola | Metoda | -|---|---|---|---| -| `NaliczanieSeryjne.PIT_11Worker` — „Deklaracje PIT/Nalicz PIT 11” | `(Session session)` | `Okres: FromTo`, `Data: Date`, `Naliczaj: NaliczanieDeklaracje`, `BezPotwierdzenia: bool`, dane podpisującego (`Imię`/`Nazwisko`/`Stanowisko` + `…Odp`), `TreśćUzasadnienia: string` | `object Nalicz_PIT_11()` | -| `NaliczanieSeryjne.PIT_RWorker` — „Deklaracje PIT/Nalicz PIT R” | `(Session)` | jw. (`Params`) | `Nalicz…()` | -| `NaliczanieSeryjne.PIT_8CWorker` — „Deklaracje PIT/Nalicz PIT 8C” | `(Session)` | jw. | `Nalicz…()` | -| `NaliczanieSeryjne.IFT_1Worker` / `IFT_1RWorker` — „Deklaracje PIT/Nalicz IFT-1 / IFT-1R” | `(Session)` | jw. | `Nalicz…()` | - -> `Params` mają ctor `(Context)`; worker `PIT_11Worker` dodatkowo ma ctor `(Session)`. Zaznaczeni -> pracownicy przez `[Context]`. - -**Deklaracje płatnika (PIT-4R/PIT-8AR)** są zbiorcze na poziomie podmiotu/oddziału (`PIT4`/`PIT8A`, -`ZbiorczaPIT`) — tworzone/dodawane workerami zbiorczymi (`DodajDoZbiorczejPITWorker`, -`WybierzDeklaracjeDoZbiorczejPITWorker`) i przeliczane `DeklaracjaWorker.Przelicz()` (J2) lub -dedykowanymi `…PrzeliczWorker` (np. `PITR.PrzeliczWorker`, `PIT8S.PrzeliczWorker`). - -**Snippet (naliczenie PIT-11 dla zaznaczonych pracowników):** - -```csharp -var pracownicy = new[] { session.GetKadry().Pracownicy.WgKodu["006"] }; - -var worker = new Soneta.Deklaracje.PIT.NaliczanieSeryjne.PIT_11Worker(session) -{ - Pracownicy = pracownicy, -}; -worker.Pars.Okres = FromTo.Year(2025); // rok podatkowy -worker.Pars.Data = Date.Today; -worker.Nalicz_PIT_11(); // tworzy wiersze PIT11 w tabeli Deklaracje -session.Save(); -``` - -**Pułapki:** -- Naliczenie PIT bazuje na naliczonych wypłatach (H) i bilansach otwarcia PIT (J6) — bez danych - źródłowych deklaracja będzie zerowa. -- Sygnatury `Params` PIT mają ctor `(Context)`; `PIT_11Worker` ma też ctor `(Session)` — w teście - użyj `(session)` + ustaw `Pracownicy`/`Pars`. -- **E-wysyłka PIT to `EDeklaracja`/`ETransmisja` (bramka MF) — NIE testować.** Samo naliczenie - wiersza PIT jest lokalne (zapis do bazy). - ---- - -### J4 — Deklaracje PFRON (Wn-D, INF-2, DEK-R, INF-D-P) - -**Cel:** utworzyć/naliczyć deklaracje PFRON. Typy (`Soneta.Deklaracje.PFRON`, wiersze tabeli -`Deklaracje`): `WN_D` (Wn-D — wniosek o dofinansowanie), `WN_U` (Wn-U), `INF_D`/`INF_D_P` -(informacje o pracownikach niepełnosprawnych — załączniki do Wn-D), `INF_2` (informacja roczna), -`DEK_R` (deklaracja roczna wpłat). - -**Workery:** -- `Soneta.Deklaracje.DeklaracjaWorker` — akcja **„Przelicz”** (`Przelicz()`) dla każdego z typów PFRON - (są DataType `Deklaracja`). -- `Soneta.Deklaracje.PFRON.INF_D.InfoWorker`, `…INF_D_P.InfoWorker` — properties informacyjne (UI). -- **E-wysyłka SOD (NIE testować):** `Soneta.Deklaracje.UI.SODEksportForm.EksportWorker` (DataType - `WN_D`/`WN_U`/`INF_D`) — eksport do systemu SODiR. - -**Dane źródłowe** PFRON pochodzą z `PracHistoria.PFRON` (A13: stopień niepełnosprawności, efekt -zachęty, schorzenia SOD) — bez nich deklaracja będzie pusta. - -**Pułapki:** -- PFRON nie ma dedykowanego „NaliczanieSeryjne” na `Pracownicy` — deklarację (`WN_D` itd.) tworzy się - w module Deklaracje, a przelicza `DeklaracjaWorker.Przelicz()`. Tworzenie/edycja wymaga `Context`. -- Konfiguracja procentów/odpisu PFRON to workery na `OddzialFirmy` - (`Soneta.Deklaracje.Config.*PFRON*Worker`) — to dane konfiguracyjne, nie deklaracje. - ---- - -### J5 — Operacje PPK - -**Cel:** obsłużyć cykl życia uczestnictwa w PPK — kwalifikacja/auto-zapis, rejestracja uczestnika, -rezygnacja, wznowienie, zmiana danych, zakończenie zatrudnienia, dokumenty i rozliczenie składek. -Typy dokumentów PPK (`Soneta.Deklaracje.PPK`, wiersze tabeli `Deklaracje`): `RejestracjaUczestnikaPPK`, -`DeklaracjaUczestnikaPPK`, `ZmianaDanychIdentyfikacyjnychUczestnikaPPK`, -`ZmianaDanychKontaktowychUczestnikaPPK`, `ZakończenieZatrudnieniaUczestnikaPPK`, `TransferPPK`, -`WypłataTransferowaPPK`, `WypłataŚrodkówPrzezUczestnikaPPK`, `ZwrotŚrodkówPPK`, `RozliczenieSkładekPPK`, -`RozliczenieNadpłatPPK`, `ZwrotNadpłatyPPK`, `NadanieUczestnikowiNumeruPPK`, -`DokumentyPracodawcyPPK`, `DokumentyInstytucjiFinansowejPPK`. - -**Workery operacji PPK — poziom `Pracownicy` (zagnieżdżone `Soneta.Deklaracje.PPK.DeklaracjePPKPracownikówWorker`), -wspólny `Params = DeklaracjePPKBaseWorker.Params` (`Okres: FromTo`, `DokumentPPK: DokumentyPracodawcyPPK`):** - -| Worker (akcja) | Metoda | -|---|---| -| `…Worker.Rejestracja` — „Operacje PPK/Rejestracja uczestnika” | `object RejestracjaPracownikow()` | -| `…Worker.Rezygnacja` — „Operacje PPK/Rezygnacja uczestnika” | `object RezygnacjaPracownikow()` | -| `…Worker.Wznowienie` — „Operacje PPK/Automatyczne wznowienie uczestnictwa” | `object WznowieniePracownikow()` | -| `…Worker.ZakończenieZatrudnienia` — „Operacje PPK/Zakończenie zatrudnienia uczestnika” | `object ZakończenieZatrudnieniaPracownikow()` | -| `…Worker.ZmianaDanychIdentyfikacyjnych` — „Operacje PPK/Zmiana danych identyfikacyjnych” | `object ZmianaDanychIdentyfikacyjnychPracownikow()` | - -> Przystąpienie/auto-zapis i zmiana procentu składki realizowane są na poziomie **pracownika** -> (dane PPK pracownika), nie tymi workerami zbiorczymi. - -**Workery na pracowniku (kwalifikacja PPK) — `Soneta.Kadry.Pracownik`:** - -| Worker | Ctor | Wybrane pola/props | -|---|---|---| -| `Pracownik.PPKWorker` (alias `PPK`) | `(Context context)` | `Data: Date`, `Idx: Pracownik`; `Kwalifikacja: PPKWorker.RodzajZgłoszenia`, `DataKwalifikacji[/Min/Max]: Date`, `Kwalifikacja[Min/Max]` | -| `Pracownik.AutoZapisPPKWorker` (alias `AutoZapisPPK`) | `(Context context)` | `Data: Date`, `Pracownik: Pracownik`; `Kwalifikacja: AutoZapisPPKWorker.CzyAutoZapisPPK` | - -> Te workery służą do **odczytu kwalifikacji** (czy/kiedy pracownik podlega przystąpieniu lub -> auto-zapisowi do PPK na dany dzień) — mają ctor `(Context)`. - -**Przeliczanie/rozliczenie PPK:** -- `Soneta.Deklaracje.PPK.PrzeliczPPKWorker` (DataType m.in. `RozliczenieNadpłatPPK`, - `WypłataTransferowaPPK`, `WypłataŚrodkówPrzezUczestnikaPPK`, `ZwrotŚrodkówPPK`, - `NadanieUczestnikowiNumeruPPK`) — przelicza dokument rozliczeniowy PPK. -- `Soneta.Deklaracje.PPK.NadanieNumeruPPKWorker` (DataType `NadanieUczestnikowiNumeruPPK`). -- `RozliczenieSkładekPPK` / `RejestracjaUczestnikaPPK` / `DeklaracjaUczestnikaPPK` przeliczane przez - `DeklaracjaWorker.Przelicz()` (J2, DataType `Deklaracja`). - -**E-wysyłka / import-eksport PPK (NIE testować):** -- `Soneta.Deklaracje.PPK.DokumentyPPKEksportWorker` (DataType `DokumentyPracodawcyPPK`, - `DokumentyInstytucjiFinansowejPPK`) — eksport do instytucji finansowej. -- `Soneta.Deklaracje.PPK.DokumentyPPKImportWorker` (DataType `DokumentyInstytucjiFinansowejPPK`) — - import zwrotny. - -**Snippet (rejestracja uczestnika PPK dla zaznaczonych):** - -```csharp -var pracownicy = new[] { session.GetKadry().Pracownicy.WgKodu["006"] }; - -var pars = new Soneta.Deklaracje.PPK.DeklaracjePPKBaseWorker.Params(context) -{ - Okres = FromTo.Year(2026), - // DokumentPPK = … (DokumentyPracodawcyPPK z modułu Deklaracje) -}; -var rej = new Soneta.Deklaracje.PPK.DeklaracjePPKPracownikówWorker.Rejestracja -{ - Pracownicy = pracownicy, - Pars = pars, -}; -rej.RejestracjaPracownikow(); // tworzy dokumenty rejestracji uczestnika PPK -session.Save(); -``` - -**Pułapki:** -- Zmiana procentu składki PPK / przystąpienie to dane **pracownika** (deklaracja uczestnika PPK, - `DeklaracjaUczestnikaPPK`) — workery zbiorcze obejmują rejestrację, rezygnację, wznowienie, zmianę - danych identyfikacyjnych i zakończenie zatrudnienia. -- `DeklaracjePPKBaseWorker.Params` ma ctor `(Context)`; operacja jest lokalna (tworzy wiersze - dokumentów PPK), ale niewykonalna bez `Context` i zwykle `DokumentPPK`. -- `PPKWorker`/`AutoZapisPPKWorker` na pracowniku są **diagnostyczne** (kwalifikacja na dzień), nie - tworzą dokumentów — i wymagają `Context`. - ---- - -### J6 — Bilanse otwarcia deklaracji (PIT, ZUS, ERP-7) przy wdrożeniu - -**Cel:** wprowadzić dane historyczne sprzed startu systemu, potrzebne do poprawnego naliczenia -deklaracji w pierwszym okresie. Bilanse są **kolekcjami na pracowniku** (`SubTable`) — tworzy się je -i odczytuje czystym API biznesowym, **bez `Context`/`KEDU`/sieci**. - -**Kolekcje na `Soneta.Kadry.Pracownik`:** - -| Kolekcja | Typ | Przeznaczenie | -|---|---|---| -| `Pracownik.BilansyOtwarciaPIT` | `SubTable` | bilans otwarcia PIT (przychody/koszty/składki na start) | -| `Pracownik.WynagrodzeniaERP7` | `SubTable` | wynagrodzenia do ERP-7 / Z-3 | -| `Pracownik.NieobecnosciERP7` | `SubTable` | nieobecności do ERP-7 / Z-3 | -| `Pracownik.DeklaracjePodmiotu` | `SubTable` | deklaracje powiązane z pracownikiem-podmiotem | - -**Typ `Soneta.Place.BilansOtwarciaPIT`** (root `GuidedRow`, tabela `BilansyOtwPIT`) jest -**ABSTRAKCYJNY** — instancjonuje się jedną z konkretnych wersji odpowiadających wartościom enuma -`Soneta.Place.WersjaBilansuOtwarciaPIT` (`PIT11_11`, `PIT11_29`): -`Soneta.Place.BilansOtwarciaPIT_11` (Wersja = `PIT11_11`) lub `Soneta.Place.BilansOtwarciaPIT_29` -(Wersja = `PIT11_29`). Konkretne klasy mają publiczny ctor `(Pracownik pracownik)`; bazowy -`BilansOtwarciaPIT` ma ctor `(Pracownik, WersjaBilansuOtwarciaPIT)`, ale jest abstrakcyjny. -Property `Pracownik` i `Wersja` są **read-only** (ustawiane przez ctor; brak ctora bezparametrowego). -Pola bazodanowe m.in.: `Data: Date`, kwoty przychodów/kosztów/składek w rozbiciu -etat/umowa/macierzyński (`Przychod26ZwolEtat`, `Przychod26ZwolUmowa`, `PrzychodUlgaEtat`, -`PrzychodUlgaUmowa`, `Spoleczne`, `Spoleczne26`, `Zdrowotne9Procent`, `SkladkiCzlonkowskie` itd.) -oraz kolekcja `Elementy: SubTable`. - -**ERP-7** (wcześniej druk ZUS Rp-7) opiera się na `WynagrodzeniaERP7`/`NieobecnosciERP7` pracownika -oraz zaświadczeniach `Soneta.Deklaracje.ZUS.Z3`/`Z3a` (workery `ZUSZ3.Z3Worker`/`Z3aWorker` na -`Nieobecnosc`) — sam druk Z-3/ERP-7 to generowanie dokumentu w module Deklaracje. - -**Snippet (dodanie bilansu otwarcia PIT i odczyt):** - -```csharp -var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; - -using (var t = session.Logout(editMode: true)) -{ - // BilansOtwarciaPIT jest abstrakcyjny — tworzymy konkretną wersję (_29 => PIT11_29, _11 => PIT11_11). - // Pracownik ustawia ctor (property read-only), więc NIE używamy inicjalizatora obiektu na Pracownik. - var bo = session.AddRow(new Soneta.Place.BilansOtwarciaPIT_29(pracownik)); - bo.Data = new Date(2026, 1, 1); - bo.PrzychodUlgaEtat = 12000m; - bo.Spoleczne = 1645.20m; - t.Commit(); -} -session.Save(); - -// Odczyt bilansów otwarcia PIT pracownika (typ kolekcji: SubTable): -foreach (Soneta.Place.BilansOtwarciaPIT bo in pracownik.BilansyOtwarciaPIT) -{ - // bo.Data, bo.PrzychodUlgaEtat, bo.Spoleczne, bo.Wersja -} -``` - -**Pułapki:** -- `BilansOtwarciaPIT` ma kolekcję `Elementy` — niektóre kwoty są wyliczane z elementów; sprawdź na - Demo, czy ustawiasz pola root, czy elementy. -- Bilanse są **danymi wdrożeniowymi** (jednorazowe na start) — nie myl z naliczonymi deklaracjami. -- ERP-7 (Z-3/Z-3a) wymaga modułu Deklaracje i `KEDU`/PUE do eksportu — samo wprowadzenie - `WynagrodzeniaERP7`/`NieobecnosciERP7` jest lokalne, ale wygenerowanie druku — nie. - -## K. Ewidencje pracownicze - -> **Wzorzec wspólny.** Wszystkie ewidencje pracownicze to **kolekcje `SubTable` na rootcie -> `Pracownik`** (nie na `PracHistoria`). Każdy element jest osobnym `GuidedRow` (child pracownika) -> z polem `Pracownik: Soneta.Kadry.Pracownik` ustawianym automatycznie przez konstruktor -> `new Xxx(pracownik)`. Schemat dodania jest jednolity: -> -> ```csharp -> using (var t = session.Logout(editMode: true)) { -> var wpis = session.AddRow(new Xxx(pracownik)); // ctor wiąże wpis z pracownikiem -> // ... ustaw pola ... -> t.Commit(); // Commit() w kodzie biznesowym -> } -> session.Save(); -> ``` -> -> `session.AddRow(new Xxx(pracownik))` i `pracownik.Kolekcja.AddRow(new Xxx(pracownik))` są -> równoważne — wpis trafia do tej samej tabeli i do `SubTable` pracownika. Większość typów wymaga -> wskazania **definicji** (rekord słownikowy, tabela konfiguracyjna) — definicję pobierasz przez -> `WgNazwy[...]` z odpowiedniego modułu, **nie** tworzysz jej w teście operacyjnym. - -| Ewidencja | Kolekcja na `Pracownik` | Typ elementu | Tabela | -|---|---|---|---| -| K1 Badania lekarskie | `BadaniaLekarskie: SubTable` | `Soneta.Kadry.BadanieLekarskie` | `BadaniaLekarskie` | -| K2 Szkolenia BHP | `SzkoleniaBHP: SubTable` | `Soneta.Kadry.SzkolenieBHP` | `SzkoleniaBHP` | -| K3 Wnioski o szkolenia | `WnioskiOSzkolenia: SubTable` | `Soneta.HR.WniosekOSzkolenie` | `WnioskiOSzkol` | -| K3 Ukończone szkolenia | `UkończoneSzkolenia: SubTable` | `Soneta.HR.UkończoneSzkolenie` | `UkonczSzkolenia` | -| K3 Uprawnienia | `Uprawnienia: SubTable` | `Soneta.HR.UprawnieniePracownika` | `UprawnieniaPrac` | -| K4 Nagrody i kary | `NagrodyKary: SubTable` | `Nagroda` / `Kara` (`NagrodaKara` abstr.) | `NagrodyKary` | -| K4 Oświadczenia | `Oświadczenia: SubTable` | `Soneta.Kadry.OświadczeniePracownika` | `OswiadczeniaPrac` | -| K5 Wypadki przy pracy | `Wypadki: SubTable` | `Soneta.Kadry.Wypadek` | `Wypadki` | - ---- - -### K1 — Badania lekarskie - -**Cel:** zarejestrować badanie lekarskie pracownika (wstępne/okresowe/kontrolne) wraz z terminami -ważności i datą następnego badania; ewentualnie wykonać operację seryjną dla grupy osób. - -**Mechanizm:** `BadanieLekarskie` ma publiczny konstruktor `BadanieLekarskie(Pracownik pracownik)`. -Wpis wymaga `Definicja: DefinicjaBadaniaLekarskiego` (słownik, tabela konfiguracyjna `DefBadanLek`, -pobierana przez `WgNazwy[...]`). Jeśli definicja jest **cykliczna** (`Definicja.Cykliczne == true`, -ma `NastepneDefinicja`/`NastepneTermin`), platforma wylicza termin kolejnego badania — -udostępniony jako wyliczane `NastępneTermin`/`NastępneDefinicja`. - -**Pola i typy (rekord `BadanieLekarskie`):** - -| Pole | Typ | Uwaga | -|---|---|---| -| `Definicja` | `Soneta.Kadry.DefinicjaBadaniaLekarskiego` | wymagana; słownik `DefBadanLek` | -| `Data` | `Soneta.Types.Date` | data wykonania badania | -| `Termin` | `Soneta.Types.Date` | termin badania — **read-only** (wyliczany z `Data`+definicji); ustawienie rzuca `ColReadOnlyException` | -| `WazneDo` | `Soneta.Types.Date` | „Ważne do" — koniec ważności (ustawialny) | -| `PracaWOkularach` | `bool` | adnotacja medyczna | -| `KwotaDofinansowania` | `decimal`, `DataDofinansowania: Date` | dofinansowanie badania | -| `Opis` | `Soneta.Business.MemoText` | opis/uwagi | -| `Anulowany` | `bool` | flaga anulowania | -| `Pracownik` | `Soneta.Kadry.Pracownik` | ustawiany przez ctor | -| `NastępneTermin`, `NastępneDefinicja`, `Następne` | (wyliczane) | termin/def./wpis następnego badania | - -**Manager:** `pracownik.Badania: Pracownik.BadaniaLekarskieManager` — pomocnik tylko do odczytu; -`pracownik.Badania.ZNajkrótszymTerminem(definicja = null): BadanieLekarskie` zwraca badanie z -najbliższym terminem wygaśnięcia (do raportów „badania okresowe do wykonania"). - -**Snippet:** - -```csharp -var kadry = session.GetKadry(); -var pracownik = kadry.Pracownicy.WgKodu["006"]; -var definicja = kadry.DefBadanLek.WgNazwy["Wstępne"]; // słownik konfiguracyjny - -using (var t = session.Logout(editMode: true)) -{ - var badanie = session.AddRow(new BadanieLekarskie(pracownik)); // ctor wiąże z pracownikiem - badanie.Definicja = definicja; - badanie.Data = Date.Today; - // UWAGA: badanie.Termin jest read-only (wyliczany) — NIE ustawiaj go ręcznie. - badanie.WazneDo = new Date(Date.Today.Year + 2, Date.Today.Month, Date.Today.Day); - - t.Commit(); -} -session.Save(); -``` - -**Operacja seryjna (grupa pracowników):** w warstwie UI istnieje worker -`DodajBadaniaLekarskieWorker` (warianty `ZListyBadań`, `ZListyPracowników`) z akcją menu -„Operacje seryjne/Dodaj badania lekarskie" — iteruje po wybranych pracownikach i dla każdego robi -`new BadanieLekarskie(pracownik)` + `BadaniaLekarskie.AddRow(...)`. W kodzie biznesowym -seryjność realizujesz tą samą pętlą `foreach (var p in wybrani) { … AddRow … }` w jednej transakcji. - -**Pułapki:** -- `Definicja` jest **wymagana** — bez niej `Save()` rzuci `RowException`. -- `Data`/`WazneDo` to `Soneta.Types.Date`, nie `DateTime`. `Termin` jest **read-only** (wyliczany) — - próba ustawienia rzuca `ColReadOnlyException`. Reguła w weryfikatorach: `WazneDo` nie może być - wcześniejsze niż `Termin`; termin następnego badania musi być **późniejszy** niż termin badania - bieżącego — naruszenie wybucha jako `RowException` przy zapisie. -- `pracownik.Badania` to manager (odczyt), a kolekcją CRUD jest `pracownik.BadaniaLekarskie` - (`SubTable`). Nie myl tych dwóch. - ---- - -### K2 — Szkolenia BHP - -**Cel:** zarejestrować odbyte szkolenie BHP (wstępne/okresowe) z terminem ważności i datą szkolenia -następnego (analogicznie do badań lekarskich). - -**Mechanizm:** konstruktor `SzkolenieBHP(Pracownik pracownik)`; kolekcja `pracownik.SzkoleniaBHP`. -Wymagana `Definicja: DefinicjaSzkoleniaBHP` (słownik konfiguracyjny `DefSzkolenBHP`, `WgNazwy[...]`). -Cykliczność (`Definicja.Cykliczne`) wylicza `NastępneTermin`/`NastępneDefinicja`. - -**Pola i typy (rekord `SzkolenieBHP`):** - -| Pole | Typ | Uwaga | -|---|---|---| -| `Definicja` | `Soneta.Kadry.DefinicjaSzkoleniaBHP` | wymagana; słownik `DefSzkolenBHP` | -| `Data` | `Soneta.Types.Date` | data szkolenia | -| `Termin` | `Soneta.Types.Date` | termin — **read-only** (wyliczany); ustawienie rzuca `ColReadOnlyException` | -| `WażneDo` | `Soneta.Types.Date` | koniec ważności (wyliczane) | -| `Zakres` | `string` | zakres szkolenia | -| `Osoba` | `string` | prowadzący / osoba szkoląca | -| `Opis` | `Soneta.Business.MemoText` | uwagi | -| `Anulowany` | `bool` | flaga anulowania | -| `Pracownik` | `Soneta.Kadry.Pracownik` | ustawiany przez ctor | -| `NastępneTermin`, `NastępneDefinicja`, `Następne` | (wyliczane) | następne szkolenie | - -**Snippet:** - -```csharp -var kadry = session.GetKadry(); -var pracownik = kadry.Pracownicy.WgKodu["007"]; -var definicja = kadry.DefSzkolenBHP.WgNazwy["Wstępne"]; - -using (var t = session.Logout(editMode: true)) -{ - var szkolenie = session.AddRow(new SzkolenieBHP(pracownik)); - szkolenie.Definicja = definicja; - szkolenie.Data = Date.Today; - // UWAGA: szkolenie.Termin jest read-only (wyliczany) — NIE ustawiaj go ręcznie. - szkolenie.Zakres = "Instruktaż ogólny"; - - t.Commit(); -} -session.Save(); -``` - -**Operacja seryjna:** UI udostępnia `DodajSzkolenieBHPWorker` (akcja menu, lista pracowników) — -w kodzie biznesowym pętla `foreach` + `new SzkolenieBHP(p)` + `AddRow` w jednej transakcji. - -**Pułapki:** -- `Definicja` wymagana (jak w K1). -- Uwaga na pisownię: pole nazywa się `WażneDo` (z „ż"), a w `BadanieLekarskie` — `WazneDo` (bez). -- `Termin` jest **read-only** (wyliczany) — ustawienie rzuca `ColReadOnlyException`. -- `Termin` następnego szkolenia musi być późniejszy niż bieżący — inaczej `RowException`. - ---- - -### K3 — Szkolenia i uprawnienia (moduł HR/HR2) - -**Cel:** obsłużyć cykl rozwoju kompetencji: **wniosek o szkolenie** → **ukończone szkolenie** → -**uprawnienie/certyfikat**, wraz z kosztem i budżetem szkoleń. Typy leżą w module `Soneta.HR` -(`session.GetHR()`). - -**K3a — Wniosek o szkolenie** — `WniosekOSzkolenie([Required] Pracownik pracownik)`; kolekcja -`pracownik.WnioskiOSzkolenia`. Pola: - -| Pole | Typ | Uwaga | -|---|---|---| -| `Definicja` | `Soneta.HR.DefinicjaSzkolenia` | rodzaj szkolenia (słownik HR) | -| `Etap` | `Soneta.HR.EtapRealizacjiSzkolenia` | np. „Wniosek zatwierdzony" (`hr.EtapRealizSzkol.WgNazwy[...]`) | -| `Realizacja` | `Soneta.HR.RealizacjaSzkolenia` | konkretna realizacja | -| `Budzet` | `Soneta.HR.BudżetSzkoleń` | budżet, z którego finansowane | -| `Koszt` | `Soneta.Types.Currency` | koszt szkolenia | -| `DataZgloszenia`, `Termin`, `DataAnulowania` | `Soneta.Types.Date` | daty cyklu wniosku | -| `Kierownik` | `Soneta.Kadry.Pracownik` | akceptujący | -| `SkierowanyPrzezZaklad` | `bool` | skierowanie pracodawcy | -| `Ocena` | `string`, `Opis: MemoText` | ocena/uwagi | - -**K3b — Ukończone szkolenie** — dwa ctory: `UkończoneSzkolenie([Required] Pracownik pracownik)` -oraz `UkończoneSzkolenie(WniosekOSzkolenie wniosek)` (przepina pracownika z wniosku). Kolekcja -`pracownik.UkończoneSzkolenia`. Pola: `Nazwa: string`, `Okres: FromTo`, `Ocena: string`, -`Opis: MemoText`, `Wniosek: WniosekOSzkolenie` (powiązanie). - -**K3c — Uprawnienie / certyfikat** — `UprawnieniePracownika([Required] Pracownik pracownik)`; -kolekcja `pracownik.Uprawnienia`. Pola: - -| Pole | Typ | Uwaga | -|---|---|---| -| `Definicja` | `Soneta.HR.DefinicjaUprawnienia` | rodzaj uprawnienia | -| `Numer` | `string` | numer uprawnienia/certyfikatu | -| `DataUzyskania`, `DataUtraty`, `TerminWaznosci` | `Soneta.Types.Date` | daty ważności | -| `Okres` | `Soneta.Types.FromTo` | okres obowiązywania | -| `WydanePrzez` | `string` | organ wydający | -| `Zrodlo` | `Soneta.HR.IŹródłoUzyskaniaUprawnienia` | źródło (np. ukończone szkolenie) | - -**Snippet (wniosek → koszt z budżetu):** - -```csharp -var hr = session.GetHR(); -var pracownik = session.GetKadry().Pracownicy.WgKodu["008"]; - -using (var t = session.Logout(editMode: true)) -{ - var wniosek = session.AddRow(new WniosekOSzkolenie(pracownik)); - wniosek.Definicja = hr.DefinicjeSzkolen.WgNazwy["Kurs zawodowy"]; - wniosek.Etap = hr.EtapRealizSzkol.WgNazwy["Wniosek zatwierdzony"]; - wniosek.DataZgloszenia = Date.Today; - wniosek.Koszt = new Currency(1500m); - - t.Commit(); -} -session.Save(); -``` - -**Pułapki:** -- Typy K3 są w `Soneta.HR` (`session.GetHR()`), nie w `Soneta.Kadry`. -- `Etap`/`Definicja` to wpisy słownikowe HR — pobieraj `WgNazwy[...]`, nie twórz w teście. -- `Koszt`/`Budżet` używają `Soneta.Types.Currency` (waluta), nie `decimal`. - ---- - -### K4 — Nagrody i kary; oświadczenia (PIT-2, RODO, zgody) - -**K4a — Nagrody i kary.** Klasa bazowa `Soneta.Kadry.NagrodaKara` jest **abstrakcyjna** — używaj -konkretnych podtypów: `Soneta.Kadry.Nagroda(Pracownik)` i `Soneta.Kadry.Kara(Pracownik)`. Oba ctory -delegują do `NagrodaKara(pracownik, TypNagrodyKary)` ustawiając `Typ` na `Nagroda`/`Kara`. Kolekcja -`pracownik.NagrodyKary: SubTable`. Pola: - -| Pole | Typ | Uwaga | -|---|---|---| -| `Definicja` | `Soneta.Kadry.DefinicjaNagrodyKary` | słownik `DefNagrodKar`; ma własne pole `Typ` (Nagroda/Kara) — musi zgadzać się z podtypem wpisu, inaczej `set_Definicja` rzuca `ArgumentException`; może nieść `Element`/`Kwota` | -| `Typ` | `Soneta.Kadry.TypNagrodyKary` | `Nagroda`/`Kara` (ustawia ctor podtypu) | -| `Data` | `Soneta.Types.Date` | data nadania | -| `DataAnulowania` | `Soneta.Types.Date` | anulowanie | -| `Rozliczenie` | `Soneta.Kadry.RozliczenieSwiadczenia` (subrow) | `Rozliczenie.Kwota: Currency`, `Rozliczenie.Element: DefinicjaElementu`, `Rozliczenie.Okres: FromTo` — powiązanie z wypłatą | -| `Opis` | `Soneta.Business.MemoText` | treść nagrody/kary | -| `Pracownik` | `Soneta.Kadry.Pracownik` | ustawiany przez ctor | - -**K4b — Oświadczenia (PIT-2, RODO, zgody).** `Soneta.Kadry.OświadczeniePracownika` — trzy ctory: -`OświadczeniePracownika([Required] Pracownik pracownik, [Required] DefinicjaOświadczenia definicja)`, -wariant z `Date dataZłożenia`, oraz `(RowCreator)`. Kolekcja `pracownik.Oświadczenia`. Rodzaj -oświadczenia (PIT-2, zgoda RODO, zgoda na e-doręczenia itp.) określa `Definicja` (słownik -konfiguracyjny `DefOswiadczen`). Pola: - -| Pole | Typ | Uwaga | -|---|---|---| -| `Definicja` | `Soneta.Kadry.DefinicjaOświadczenia` | wymagana w ctorze; słownik `DefOswiadczen` | -| `DataZlozenia` | `Soneta.Types.Date` | data złożenia | -| `DataWycofania` | `Soneta.Types.Date` | data wycofania | -| `Okres` | `Soneta.Types.FromTo` | okres obowiązywania (z `Definicja.OkresWaznosci`/`OkresIlosc`) | -| `Tresc` | `Soneta.Business.MemoText` | treść | -| `TrescOswiadczenia` | `Soneta.Kadry.TreśćOświadczenia` | treść strukturalna | -| `Pracownik` | `Soneta.Kadry.Pracownik` | ustawiany przez ctor | - -**Snippet (nagroda + oświadczenie PIT-2):** - -```csharp -var kadry = session.GetKadry(); -var pracownik = kadry.Pracownicy.WgKodu["009"]; - -using (var t = session.Logout(editMode: true)) -{ - // nagroda — konkretny podtyp, NIE abstrakcyjna NagrodaKara - var nagroda = session.AddRow(new Nagroda(pracownik)); - nagroda.Definicja = kadry.DefNagrodKar.WgNazwy["Nagroda uznaniowa"]; - nagroda.Data = Date.Today; - - // oświadczenie — definicja jest wymagana w konstruktorze - var defPit2 = kadry.DefOswiadczen.WgNazwy["PIT-2"]; - var oswiadczenie = session.AddRow(new OświadczeniePracownika(pracownik, defPit2, Date.Today)); - - t.Commit(); -} -session.Save(); -``` - -**Pułapki:** -- **Nie** rób `new NagrodaKara(...)` — typ abstrakcyjny. Używaj `Nagroda`/`Kara`. -- `Definicja` musi mieć **`Typ` zgodny** z podtypem wpisu (`Nagroda` → def. o `Typ==Nagroda`, `Kara` → - def. o `Typ==Kara`); przypisanie niezgodnej typem definicji rzuca `ArgumentException` w `set_Definicja`. - Filtruj słownik: `DefNagrodKar.Cast().FirstOrDefault(d => d.Typ == TypNagrodyKary.Nagroda)`. -- `OświadczeniePracownika` **nie ma** ctora samego `(Pracownik)` — definicja jest `[Required]` - w konstruktorze; bez niej kod się nie skompiluje. -- `Rozliczenie.*` na nagrodzie/karze to subrow powiązania z wypłatą (`Currency`, `DefinicjaElementu`) - — wypełniane przy rozliczaniu w płacach, nie przy samym wpisie. - ---- - -### K5 — Wypadki przy pracy - -**Cel:** zarejestrować wypadek przy pracy wraz z dokumentacją powypadkową (protokół, decyzja, -okoliczności, skutki) i ewentualnym świadczeniem. - -**Mechanizm:** `Soneta.Kadry.Wypadek(Pracownik pracownik)`; kolekcja `pracownik.Wypadki`. Wpis jest -numerowany (`Numer: Soneta.Core.NumerDokumentu`) i wymaga `Definicja: Soneta.Core.DefinicjaDokumentu` -(definicja dokumentu wypadku). - -**Pola i typy (rekord `Wypadek`):** - -| Pole | Typ | Uwaga | -|---|---|---| -| `Definicja` | `Soneta.Core.DefinicjaDokumentu` | definicja dokumentu (numeracja) | -| `Numer` | `Soneta.Core.NumerDokumentu` (subrow) | `Numer.Pelny`, `Numer.Symbol`, `Numer.Numer` | -| `Data` | `Soneta.Types.Date` | data wypadku | -| `Godzina` | `Soneta.Types.Time` | godzina wypadku | -| `DataZgloszenia` | `Soneta.Types.Date` | data zgłoszenia | -| `Miejsce` | `string` | miejsce wypadku | -| `Rodzaj` | `Soneta.Kadry.RodzajWypadku` | klasyfikacja wypadku | -| `PrzyPracy`, `Ciezki`, `Smiertelny`, `Niezdolnosc` | `bool` | kwalifikacja skutków | -| `Okolicznosci`, `Skutki`, `Odmowa` | `Soneta.Business.MemoText` | dokumentacja opisowa | -| `ProtokolNumer`, `ProtokolData` | `string` / `Date` | protokół powypadkowy | -| `DecyzjaNumer`, `DecyzjaData` | `string` / `Date` | decyzja | -| `PismoNumer`, `PismoData` | `string` / `Date` | pismo | -| `SKW` | `string` | statystyczna karta wypadku | -| `Kwota` | `decimal` | kwota świadczenia | -| `PracHistoria` | `Soneta.Kadry.PracHistoria` | (wyliczane) zapis kadrowy na datę | -| `Pracownik` | `Soneta.Kadry.Pracownik` | ustawiany przez ctor | - -**Snippet:** - -```csharp -var kadry = session.GetKadry(); -var pracownik = kadry.Pracownicy.WgKodu["010"]; - -using (var t = session.Logout(editMode: true)) -{ - var wypadek = session.AddRow(new Wypadek(pracownik)); - wypadek.Data = Date.Today; - wypadek.Godzina = new Time(10, 30); - wypadek.DataZgloszenia = Date.Today; - wypadek.Miejsce = "Hala produkcyjna"; - wypadek.PrzyPracy = true; - wypadek.Okolicznosci = new MemoText("Poślizgnięcie na mokrej posadzce."); - - t.Commit(); -} -session.Save(); -``` - -**Pułapki:** -- `Numer` jest subrowem nadawanym wg `Definicja` (numeracja) — nie ustawiaj `Numer.Pelny` ręcznie, - numer nadaje platforma; gdy `Definicja` ma własną numerację, podpięcie definicji wystarcza. -- `Godzina` to `Soneta.Types.Time`, `Data` to `Soneta.Types.Date` — nie `DateTime`. -- Pola opisowe (`Okolicznosci`, `Skutki`, `Odmowa`) to `MemoText`, nie `string`. - -### K6 — RODO/GIODO: oświadczenia, uprawnienia do przetwarzania, wymiana danych - -**Cel:** ewidencjonować zgody/oświadczenia RODO pracownika, uprawnienia do przetwarzania danych -osobowych oraz fakty wymiany danych (pozyskanie / udostępnienie / powierzenie). Pracownik jest -hostem GIODO — implementuje `IGIODOOświadczenieHost`, `IGIODOUprawnienieHost`, `IGIODOWymianaDanychHost`, -`IGIODOZgodnyHost`. Zapis „teczki" personalnej do pliku jest operacją plikową (poza zakresem testów). - -**Publiczny kontrakt — kolekcje na `Pracownik` (moduł `Soneta.Core`):** - -| Kolekcja | Typ elementu | Zawartość | -|---|---|---| -| `GIODOOświadczenia` | `SubTable` | oświadczenia / zgody RODO | -| `GIODOUprawnienia` | `SubTable` | uprawnienia do przetwarzania danych | -| `GIODOUdostępnienia` | `SubTable` | pozyskanie / udostępnienie / powierzenie danych | -| `PotwierdzeniaGIODO` | `SubTable` | potwierdzenia zgodności; `ZgodnoscGIODOPotwierdzona: bool` (kalkulowane) | - -**`GIODOOświadczenie` (tabela `GIODOOswiadcz`, root) — pola bazodanowe:** - -| Pole | Typ | Uwaga | -|---|---|---| -| `Host` | `IGIODOOświadczenieHost` | składający oświadczenie (= `Pracownik`) | -| `Definicja` | `Soneta.Core.GIODODefinicjaOświadczenia` | **referencja konfiguracyjna** (wymagana przez ctor) | -| `Data` | `Soneta.Types.Date` | data oświadczenia (zapisywalne) | -| `Okres` | `Soneta.Types.FromTo` | okres obowiązywania zgody — **read-only** (wyliczane z definicji) | -| `Rodzaj` | `Soneta.Core.RodzajeOświadczeńGIODO` | `Oświadczenie`, `UdzielenieZgody`, `WycofanieZgody` — **read-only** (wynika z definicji) | -| `Oswiadczenie` | `bool` | flaga oświadczenia | -| `Tresc` | `Soneta.Business.MemoText` | treść | -| `SposobPozyskania` | `string` | — | -| `DataWycofaniaZgody` | `Soneta.Types.Date` | — | -| `WycofanieZgody` | `GIODOOświadczenie` | powiązanie z zapisem wycofania | -| `Bufor` | `bool` | zatwierdzenie | - -Ctor: `new GIODOOświadczenie(IGIODOOświadczenieHost host, GIODODefinicjaOświadczenia definicja)`. - -**`GIODOUprawnienie` (tabela `GIODOUprawnienia`, root) — pola bazodanowe:** - -| Pole | Typ | Uwaga | -|---|---|---| -| `Uprawniony` | `IGIODOUprawnienieHost` | = `Pracownik` | -| `Definicja` | `Soneta.Core.GIODODefinicjaUprawnienia` | **referencja konfiguracyjna** (wymagana przez ctor) | -| `Data`, `Przyznane`, `Odebrane` | `Soneta.Types.Date` | data zapisu / od kiedy przyznane / od kiedy odebrane | -| `Okres` | `Soneta.Types.FromTo` | okres przyznania | -| `Tresc` | `Soneta.Business.MemoText` | — | -| `WycofanieUprawnienia` | `GIODOUprawnienie` | powiązanie z wycofaniem | - -Ctor: `new GIODOUprawnienie(IGIODOUprawnienieHost uprawniony, GIODODefinicjaUprawnienia definicja)`. - -**`GIODOWymianaDanych` (tabela `GIODOWymDanych`, root) — pola bazodanowe:** - -| Pole | Typ | Uwaga | -|---|---|---| -| `Host` | `IGIODOWymianaDanychHost` | = `Pracownik` | -| `Kierunek` | `Soneta.Core.GIODOKierunekWymianyDanych` | `Powierzenie`, `Pozyskanie`, `PowierzenieZbioru`, `PozyskanieZbioru`, `Udostępnienie` | -| `Podmiot` | `Soneta.Core.IKontrahent` | druga strona wymiany | -| `Data` | `Soneta.Types.Date` | data wymiany | -| `Zakres` | `Soneta.Business.MemoText` | zakres danych | -| `SposobPozyskania` | `string` | — | -| `PozyskaneOdOsoby`, `UdostepnioneOsobie`, `NaWniosekOsoby`, `TylkoDostep` | `bool` | flagi | -| `Definicja` | `Soneta.Core.DefinicjaDokumentu` | def. numeracji dokumentu | -| `ZbiorDanych` | `Soneta.Core.GIODO.GIODOZbiorDanych` | zbiór danych | - -`GIODOWymianaDanych` **nie ma publicznego konstruktora** — rekordy tworzą wyłącznie workery poniżej -(zwracają konkretne podtypy `GIODOPozyskanieDanych` / `GIODOUdostępnienieDanych` / `GIODOPowierzenieDanych`). - -**Workery RODO (jedyna droga dodania przez API; klasy zagnieżdżone w `Soneta.Kadry.Pracownicy`):** - -| Worker | Metoda | Zwraca | Parametry (`Pars` / `Params`) | -|---|---|---|---| -| `Pracownicy.DodajOświadczeniaWorker` | `GIODOOświadczenie DodajOświadczenia()` | oświadczenie | `Pars`: `Definicja: GIODODefinicjaOświadczenia`, `Data`, `Oddział`, `SposobPozyskania`, `Zatwierdź: bool` | -| `Pracownicy.DodajUprawnieniaWorker` | `GIODOUprawnienie DodajUprawnienia()` | uprawnienie | `Pars`: `Definicja: GIODODefinicjaUprawnienia`, `Data`, `Przyznane`, `Odebrane`, `Oddział`, `Zatwierdź: bool` | -| `Pracownicy.DodajPozyskanieDanychWorker` | `GIODOPozyskanieDanych DodajPozyskanieDanych()` | wymiana (pozyskanie) | `Pars`: `Podmiot: IKontrahent`, `Data`, `Zakres: string`, `Oddział`, `SposobPozyskania`, `Zatwierdź: bool` | -| `Pracownicy.DodajUdostępnienieDanychWorker` | `GIODOUdostępnienieDanych DodajUdostępnienieDanych()` | wymiana (udostępnienie) | `Pars`: `Podmiot: IKontrahent`, `Data`, `Zakres: string`, `Oddział`, `Zatwierdź: bool` | -| `Pracownicy.DodajPowierzenieDanychWorker` | `GIODOPowierzenieDanych DodajPowierzenieDanych()` | wymiana (powierzenie) | `Pars` (analogicznie) | - -Wszystkie workery RODO mają bezparametrowy ctor oraz property `Hosts: Pracownik[]` (`[Context]`, lista -pracowników, których dotyczy operacja) i `Session`. - -**Zapis teczki personalnej do pliku — `Pracownik.ZapiszTeczkęDoPlikuWorker`** (akcja -„Teczka.../Zapisz teczkę do pliku", metoda `ZapiszTeczkeDoPliku()`, property `Param`) — to -**operacja plikowa** (serializacja dokumentacji do plików XML/katalogu na dysku). **Poza zakresem -testów jednostkowych → `[Ignore]`** (zależność od systemu plików). - -**Snippet (dodanie oświadczenia GIODO workerem):** - -```csharp -var kadry = session.GetKadry(); -var pracownik = kadry.Pracownicy.WgKodu["006"]; - -// Definicja oświadczenia z konfiguracji (musi istnieć w bazie): -var defOswiadczenia = session.ExecuteConfig(s => - s.GetCore().GIODODefinicjeOświadczeń.WgNazwy["Zgoda na przetwarzanie danych"]); - -using (var t = session.Logout(editMode: true)) -{ - var worker = new Pracownik.Pracownicy.DodajOświadczeniaWorker { Hosts = new[] { pracownik } }; - worker.Pars.Definicja = session.Get(defOswiadczenia); - worker.Pars.Data = Date.Today; - worker.Pars.Zatwierdź = true; - GIODOOświadczenie oswiadczenie = worker.DodajOświadczenia(); - t.CommitUI(); -} -session.Save(); - -// Odczyt oświadczeń pracownika: -foreach (GIODOOświadczenie o in pracownik.GIODOOświadczenia) -{ - // o.Definicja, o.Okres, o.Rodzaj, o.Data -} -``` - -**Snippet (dodanie oświadczenia bez workera — bezpośrednim ctorem):** - -```csharp -using (var t = session.Logout(editMode: true)) -{ - var o = session.AddRow(new GIODOOświadczenie(pracownik, session.Get(defOswiadczenia))); - // host i Definicja wynikają z ctora; Rodzaj/Okres są WYLICZANE (read-only) z definicji — nie ustawiaj ich. - o.Data = Date.Today; - o.SposobPozyskania = "Formularz papierowy"; - t.Commit(); -} -session.Save(); -``` - -**Pułapki:** -- `GIODOOświadczenie`/`GIODOUprawnienie` wymagają **referencji do definicji konfiguracyjnej** - (`GIODODefinicjaOświadczenia` / `GIODODefinicjaUprawnienia`) — pobierz istniejący rekord - (`ExecuteConfig`), nie twórz „w locie". Bez definicji w bazie scenariusz wymaga uprzedniej - konfiguracji modułu RODO/GIODO. -- `GIODOWymianaDanych` **nie ma publicznego ctora** — dodawaj wyłącznie workerami - `DodajPozyskanieDanychWorker` / `DodajUdostępnienieDanychWorker` / `DodajPowierzenieDanychWorker`. -- Workery RODO modyfikują dane i są uruchamiane „jak z UI" → transakcja edycyjna + `CommitUI()` + - `Save()`. `Hosts`/`Podmiot` muszą pochodzić z bieżącej sesji (safe-code §2.1). -- Obowiązywanie zgody jest „na dzień" — czytaj `Okres`/`Data`, nie zakładaj bezterminowości. -- Dane wrażliwe (treść oświadczeń, podmioty) — nie loguj nadmiarowo (safe-code §12). -- Workery RODO wymagają praw do obszaru GIODO; w teście biznesowym egzekucji praw nie sprawdzamy - (safe-code §7.2). - -### K7 — Struktura organizacyjna: przypisanie do wydziału/struktury, powiązania - -**Cel:** przypisać pracownika do jednostki organizacyjnej (wydziału) oraz do elementów struktury -organizacyjnej (np. stanowiska w strukturze, relacje przełożony–podwładny). Wydział wynika z warunków -etatu (`Etat.Wydzial`, historyczne — patrz sekcja B), a powiązania ze strukturą trzyma osobna kolekcja. - -**Publiczny kontrakt:** - -| Składnik | Typ | Rodzaj | Uwaga | -|---|---|---|---| -| `Etat.Wydzial` | `Soneta.Kadry.Wydzial` | bazodanowe (na `PracHistoria.Etat`) | jednostka organizacyjna; korzeń: `session.GetKadry().Wydzialy.Firma` (zmiana „od daty" — A14) | -| `PowiązaniaStrOrg` | `SubTable` | kolekcja na `Pracownik` | powiązania ze strukturą organizacyjną | -| `StrukturaOraganizacyjna` | `Pracownik.StrukturaOraganizacyjnaManager` | manager (read-only API) | nawigacja przełożeni/podwładni | -| Pracownik implementuje | `IŹródłoPowiązaniaStrukturyOrganizacyjnej` | interfejs | jest źródłem powiązań | - -**`PowiązanieStrukturyOrganizacyjnej` (tabela `PowiazaniaStrOrg`, child przez `Zrodlo`) — pola:** - -| Pole | Typ | Uwaga | -|---|---|---| -| `Zrodlo` | `IŹródłoPowiązaniaStrukturyOrganizacyjnej` | guided-parent (= `Pracownik`) | -| `Element` | `Soneta.Core.ElementStrukturyOrganizacyjnej` | referencja do **instancji** elementu struktury z tabeli `CoreModule.ElementyStrOrg` (NIE z `DefElStrukturOrg`, która trzyma `DefinicjaElementuStrukturyOrganizacyjnej`); `ElementStrukturyOrganizacyjnej` nie ma publicznego ctora — pobierz istniejący rekord | -| `Okres` | `Soneta.Types.FromTo` | okres obowiązywania powiązania (zapisywalne) | - -Ctor: `new PowiązanieStrukturyOrganizacyjnej(ElementStrukturyOrganizacyjnej element, IŹródłoPowiązaniaStrukturyOrganizacyjnej zrodlo)`. - -**Manager `StrukturaOraganizacyjnaManager` (tylko odczyt nawigacyjny):** - -| Metoda / property | Sygnatura | Zwraca | -|---|---|---| -| `Przełożony(...)` | `Pracownik Przełożony(StrukturaOrganizacyjna, Date, bool, Func<...>)` | bezpośredni przełożony | -| `PrzełożonyWgPodległości(...)` | `Pracownik PrzełożonyWgPodległości(Date, bool, Func<...>)` | przełożony wg podległości | -| `Przełożeni(...)` | `IEnumerable …` | przełożeni | -| `Podwładni(...)` | `IEnumerable Podwładni(FromTo, bool, Func<...>)` | podwładni w okresie | -| `GetDomyślnyPrzełożony(naDzień[, bezpośredni, warunek])` | `Pracownik GetDomyślnyPrzełożony(Date, bool=…, Func=…)` | domyślny przełożony na dzień (property `DomyślnyPrzełożony` jest **przestarzała** — używaj metody) | - -**Workery zmiany powiązań (klasy zagnieżdżone w `Soneta.Kadry.Pracownik`):** - -| Worker | Akcja (menu) | Metoda | Parametry | -|---|---|---|---| -| `Pracownik.DodajPowiązanieStrukturyWorker` | „Struktura organizacyjna/Dodaj lub modyfikuj powiązanie…" | `object DodajPowiązanieStruktury()` | `Params: WybórElementuContext`, `Pracownicy: Pracownik[]` (`[Context]`) | -| `Pracownik.UsuńPowiązanieStrukturyWorker` | „Struktura organizacyjna/Usuń powiązanie…" | `void DodajPowiązanieStruktury()` | `Params: WybórElementuContext`, `Pracownicy: Pracownik[]` | - -**Snippet (dodanie powiązania ze strukturą — bezpośrednim ctorem):** - -```csharp -var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; - -// Instancja elementu struktury (musi istnieć w bazie — tabela ElementyStrOrg, NIE DefElStrukturOrg): -ElementStrukturyOrganizacyjnej element = - session.GetCore().ElementyStrOrg.Cast().FirstOrDefault(); - -using (var t = session.Logout(editMode: true)) -{ - var p = session.AddRow(new PowiązanieStrukturyOrganizacyjnej(element, pracownik)); - p.Okres = new FromTo(Date.Today, Date.MaxValue); - t.Commit(); -} -session.Save(); - -// Odczyt nawigacyjny struktury: -Pracownik przelozony = pracownik.StrukturaOraganizacyjna.GetDomyślnyPrzełożony(Date.Today); -``` - -**Snippet (zmiana wydziału — nowy zapis „od daty", A14):** - -```csharp -var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; -var kadry = session.GetKadry(); - -using (var t = session.Logout(editMode: true)) -{ - var ph = pracownik[Date.Today]; // zapis obowiązujący na dzień (A15) - ph.Etat.Wydzial = kadry.Wydzialy.Firma; // referencja do istniejącego wydziału (korzeń struktury) - t.Commit(); -} -session.Save(); -``` - -**Pułapki:** -- `Etat.Wydzial` jest **danymi historycznymi** (na `PracHistoria.Etat`) i jest **wymagany dla etatu** — - zmieniaj nowym zapisem „od daty" (A14), nie nadpisuj bieżącego (zmieniłoby cały okres wstecz). -- `PowiązanieStrukturyOrganizacyjnej.Element` to **referencja konfiguracyjna** — pobierz istniejący - element struktury; bez zdefiniowanej struktury organizacyjnej scenariusz wymaga konfiguracji. -- `StrukturaOraganizacyjnaManager` jest **tylko do odczytu** — zmiany realizują workery - `DodajPowiązanieStrukturyWorker` / `UsuńPowiązanieStrukturyWorker` lub bezpośredni zapis do - `PowiązaniaStrOrg`. -- Workery struktury modyfikują dane „jak z UI" → transakcja + `CommitUI()` + `Save()`; rekordy z - bieżącej sesji (safe-code §2.1). - -### K8 — Oceny okresowe: arkusze ocen, cele okresowe, karty kompetencji - -**Cel:** prowadzić oceny okresowe pracownika (arkusz oceny z elementami), cele okresowe wraz z ich -realizacją, karty kompetencji i karty opisu stanowiska. Funkcjonalność należy do modułów **HR** -(`session.GetHR()`, `OcenyPracownikow`, `EtapyRekrutacji`) i **HR2** (`session.GetHR2()`, -`CeleOkresowePrac`, `KartyKompPrac`, `KartyOpStanowisk`). Pracownik implementuje `IOceniany`, -`IOceniający`, `IOdpowiedzialnyZaOcenę`, `IŹródłoKartyOpisuStanowiska`. - -**Publiczny kontrakt — kolekcje na `Pracownik`:** - -| Kolekcja | Typ elementu | Zawartość | -|---|---|---| -| `Oceny` | `SubTable` | arkusze ocen okresowych (root) | -| `ElementyOceny` | `SubTable` | pojedyncze elementy/pozycje arkuszy ocen | -| `CeleOkresowe` | `SubTable` | cele okresowe | -| `KartyKompetencji` | `SubTable` | karty kompetencji | -| `KartyOpisuStanowiska` | `SubTable` | karty opisu stanowiska | -| `KartyRealizacjiCelu` | `SubTable` | karty realizacji celów | -| `Oceniani` / `Oceniający` | `SubTable` | role pracownika w ocenie | - -**`OcenaPracownika` (tabela `OcenyPracownikow`, root; impl. `IOcenaPracownika`) — pola:** - -| Pole | Typ | Uwaga | -|---|---|---| -| `Pracownik` | `Soneta.Kadry.Pracownik` | oceniany | -| `Nazwa` | `string` | nazwa arkusza | -| `Data`, `Termin` | `Soneta.Types.Date` | data oceny / termin | -| `Opis` | `Soneta.Business.MemoText` | — | -| `Anulowany` | `bool` | — | -| `ElementyOceny` | `SubTable` | pozycje arkusza | - -Ctor: `new OcenaPracownika(Pracownik pracownik)`. - -**`ElementOcenyPracownika` (tabela `ElementyOcenPrac`, child przez `Ocena`) — pola:** - -| Pole | Typ | Uwaga | -|---|---|---| -| `Ocena` | `IOcenaPracownika` | guided-parent (arkusz oceny lub etap rekrutacji) | -| `Pracownik` | `Soneta.Kadry.Pracownik` | — | -| `Definicja` | `Soneta.HR.DefElementuOcenyPracownika` | **referencja konfiguracyjna** (z tabeli `HRModule.DefElemOcenPrac`); zapisywalna i wymagana do zapisu | -| `Typ` | `Soneta.HR.TypyElementowOceny` | `Historyczny`, `Aktualny` — **read-only** (wynika z definicji) | -| `Data` | `Soneta.Types.Date` | **read-only** (wyliczane) | -| `Wartosc` | `decimal` | wartość liczbowa oceny (zapisywalna) | - -Ctor: `new ElementOcenyPracownika(IOcenaPracownika ocena)`. Dodawaj przez `session.AddRow(new ElementOcenyPracownika(ocena))` (NIE `ocena.ElementyOceny.AddRow(...)` — `SubTable` nie udostępnia `AddRow`). - -**`CelOkresowyPracownika` (tabela `CeleOkresowePrac`, root) — pola:** - -| Pole | Typ | Uwaga | -|---|---|---| -| `Pracownik` | `Soneta.Kadry.Pracownik` | — | -| `Nazwa` | `string` | nazwa celu | -| `Data`, `Termin` | `Soneta.Types.Date` | — | -| `Opis` | `Soneta.Business.MemoText` | — | -| `Definicja` | `Soneta.Oceny.DefinicjaElementuOceny` | **referencja konfiguracyjna** (opcjonalna) | -| `Anulowany` | `bool` | — | -| `Realizacja` | `Soneta.HR2.RealizacjaCelu` | bieżąca realizacja (subrow) | -| `Realizacje` | `SubTable` | realizacje celu | - -> `CelOkresowyPracownika` **nie ma pola `Wartosc`** — postęp/ocena celu jest reprezentowana przez `Realizacja`/`Realizacje` (`Soneta.HR2.RealizacjaCelu`). Pole `Wartosc` (typu decimal) ma natomiast `ElementOcenyPracownika`. - -Ctor: `new CelOkresowyPracownika(Pracownik pracownik)`. - -**`KartaOpisuStanowiskaBase` (tabela `KartyOpStanowisk`, root) — pola:** - -| Pole | Typ | Uwaga | -|---|---|---| -| `Zrodlo` | `IŹródłoKartyOpisuStanowiska` | = `Pracownik` / `DefinicjaStanowiska` / wakat / oferta | -| `Typ` | `Soneta.HR2.TypyKartOpisuStanowiska` | `KartaOpisuStanowiska`, `OgłoszenieOPracę` | -| `Data` | `Soneta.Types.Date` | — | -| `Elementy` | `SubTable` | elementy opisu | -| `Kompetencje` | `SubTable` | kompetencje | - -`KartaOpisuStanowiskaBase` i `KartaKompetencjiPracownika` **nie mają publicznego ctora bezparametrowego**; -`KartaKompetencjiPracownika` ma ctor `(Pracownik pracownik, IŹródłoKartyCharakterystykiPracownika zrodlo)`. -Karty zwykle tworzone są workerami kopiującymi (`KopiujKartęOpisuStanowiskaWorker.KopiujZDefinicjiStanowiska()`, -`KopiujKartęKompetencjiWorker.KopiujZKOS()`/`KopiujZPoprzedniej()`). - -**Workery oceniania (klasy w `Soneta.HR` / `Soneta.HR2`):** - -| Worker | Metoda | Parametry | -|---|---|---| -| `Soneta.HR.OcenaPracownikowWorker` | `Oceń()` | `Pars`, `Idxs: Pracownik[]` (`[Context]`); ctor `(Context)` | -| `Soneta.HR.WzorOcenyPracownika.ZainicjujOcenęWorker` | `Zainicjuj()` | `Ocena: IOcenaPracownika`, `Pars`; ctor `(Session)` | -| `Soneta.HR2.KopiujKartęOpisuStanowiskaWorker` | `KopiujZDefinicjiStanowiska()`, `KopiujZPoprzedniej()` | `Karta: KartaOpisuStanowiskaBase` | -| `Soneta.HR2.KopiujKartęKompetencjiWorker` | `KopiujZKOS()`, `KopiujZPoprzedniej()` | `Karta: KartaKompetencjiPracownika` | - -**Snippet (dodanie celu okresowego — wymaga definicji elementu oceny w bazie HR2):** - -```csharp -var pracownik = session.GetKadry().Pracownicy.WgKodu["006"]; -var hr2 = session.GetHR2(); - -// Definicja elementu oceny z konfiguracji modułu Oceny (musi istnieć): -var defElementu = session.ExecuteConfig(s => - /* pobranie DefinicjaElementuOceny z modułu Oceny */ default); - -using (var t = session.Logout(editMode: true)) -{ - var cel = new CelOkresowyPracownika(pracownik); - hr2.CeleOkresowePrac.AddRow(cel); - cel.Nazwa = "Wdrożenie nowego modułu"; - cel.Data = Date.Today; - cel.Termin = new Date(2026, 12, 31); - cel.Definicja = session.Get(defElementu); - t.Commit(); -} -session.Save(); - -// Odczyt celów okresowych: -foreach (CelOkresowyPracownika c in pracownik.CeleOkresowe) -{ - // c.Nazwa, c.Termin, c.Wartosc.Punktacja -} -``` - -**Snippet (utworzenie arkusza oceny i dodanie elementu):** - -```csharp -using (var t = session.Logout(editMode: true)) -{ - var ocena = new OcenaPracownika(pracownik); - session.GetHR().OcenyPracownikow.AddRow(ocena); - ocena.Nazwa = "Ocena roczna 2026"; - ocena.Data = Date.Today; - - var el = session.AddRow(new ElementOcenyPracownika(ocena)); // ocena jako IOcenaPracownika - el.Definicja = defElementu; // wymagana (z HRModule.DefElemOcenPrac); Typ/Data są wyliczane (read-only) - el.Wartosc = 4m; // Wartosc to decimal - t.Commit(); -} -session.Save(); -``` - -**Pułapki:** -- Cele/elementy ocen wymagają **referencji do definicji konfiguracyjnych** (`DefElementuOcenyPracownika`, - `Soneta.Oceny.DefinicjaElementuOceny`) — bez nich scenariusz wymaga uprzedniej konfiguracji modułu - Oceny/HR/HR2. W bazie Demo te definicje **mogą nie istnieć** — najpierw sprawdź dostępność. -- Karty opisu stanowiska / kompetencji nie mają prostego ctora — twórz je workerami kopiującymi - (`KopiujKartę…Worker`) z definicji stanowiska lub poprzedniej karty. -- `ElementOcenyPracownika.Ocena` to `IOcenaPracownika` — może to być arkusz oceny **lub etap - rekrutacji** (`EtapRekrutacji` także implementuje `IOcenaPracownika`, patrz K9). -- `CelOkresowyPracownika` **nie ma pola `Wartosc`** — postęp/wynik celu reprezentują `Realizacja`/`Realizacje` - (`RealizacjaCelu`). Liczbową wartość ma `ElementOcenyPracownika.Wartosc` (`decimal`). -- `ElementOcenyPracownika`: `Typ`/`Data` są **read-only** (wyliczane z definicji), a do tabeli dodawaj przez - `session.AddRow(...)` — `SubTable` nie ma metody `AddRow`. -- Workery oceniania uruchamiane „jak z UI" → transakcja + `CommitUI()` + `Save()`. - -### K9 — Rekrutacja: wakaty, ogłoszenia, aplikacje, etapy, stan zatrudnienia - -**Cel:** prowadzić proces rekrutacji — wakaty (zapotrzebowanie), oferty/ogłoszenia o pracę, aplikacje -kandydatów oraz etapy rekrutacji z ocenami, aż do zatrudnienia kandydata. Funkcjonalność należy do -modułów **HR2** (`session.GetHR2()`, `RekrutAplikacje`, `RekrutWakaty`) i **HR** -(`session.GetHR()`, `Rekrutacje`, `EtapyRekrutacji`). - -**Publiczny kontrakt — kolekcje na `Pracownik` (kandydat jest reprezentowany rekordem `Pracownik`):** - -| Kolekcja | Typ elementu | Zawartość | -|---|---|---| -| `Aplikacje` | `SubTable` | aplikacje kandydata | -| `Wakaty` | `SubTable` | wakaty | -| `Rekrutacje` / `Kandydatury` | `SubTable` | rekrutacje (kandydatury) | -| `EtapyRekrutacji` | `SubTable` | etapy rekrutacji | - -**`RekrutacjaAplikacja` (tabela `RekrutAplikacje`, root; impl. `IŹródłoRekrutacji`) — pola:** - -| Pole | Typ | Uwaga | -|---|---|---| -| `Pracownik` | `Soneta.Kadry.Pracownik` | kandydat | -| `Stanowisko` | `Soneta.HR.DefinicjaStanowiska` | **referencja konfiguracyjna** stanowiska | -| `Wydzial` | `Soneta.Kadry.Wydzial` | jednostka organizacyjna | -| `Oferta` | `Soneta.HR2.OfertaPracy` | oferta, na podstawie której wpłynęła aplikacja | -| `Stan` | `Soneta.HR2.StanAplikacji` | `Wprowadzona`, `Zakończona`, `Anulowana` | -| `Data` | `Soneta.Types.Date` | data aplikacji | -| `PlanowanaDataZatrudnienia` | `Soneta.Types.Date` | — | - -Ctor: `new RekrutacjaAplikacja(Pracownik pracownik, Soneta.HR.WydziałDefinicjiStanowiska stanowisko)`. -`WydziałDefinicjiStanowiska` jest w module **`Soneta.HR`** (NIE `Soneta.HR2`) i ma ctor -`new WydziałDefinicjiStanowiska(DefinicjaStanowiska definicjaStanowiska)` — definicję pobierz -z `session.GetHR().DefStanowisk`. `RekrutacjaAplikacja.Stanowisko` zwraca tę `DefinicjaStanowiska`. - -**`EtapRekrutacji` (tabela `EtapyRekrutacji`, root; impl. `IOcenaPracownika`) — pola:** - -| Pole | Typ | Uwaga | -|---|---|---| -| `Rekrutacja` | `Soneta.HR.Rekrutacja` | rekrutacja nadrzędna | -| `Definicja` | `Soneta.HR.DefinicjaEtapuRekrutacji` | **referencja konfiguracyjna** | -| `Lp` | `int` | numer etapu | -| `Data`, `Termin` | `Soneta.Types.Date` | — | -| `Odpowiedzialny` | `Soneta.Oceny.IOceniający` | osoba odpowiedzialna | -| `Opis` | `Soneta.Business.MemoText` | — | -| `ElementyOceny` | `SubTable` | oceny etapu (etap jest `IOcenaPracownika`) | - -Ctor: `new EtapRekrutacji(Rekrutacja rekrutacja)`. - -**`Rekrutacja` (tabela; impl. `IOcenaPracownika`) — ctory:** -`new Rekrutacja(Pracownik pracownik)`, `new Rekrutacja(Pracownik pracownik, IŹródłoRekrutacji źródło)`. - -**`RekrutacjaWakat` (tabela `RekrutWakaty`, root) — ctory:** -`new RekrutacjaWakat(WydziałDefinicjiStanowiska stanowisko)`, -`new RekrutacjaWakat(DefinicjaStanowiska definicjaStanowiska, Wydzial wydzial)`. - -**`OfertaPracy` (tabela; ogłoszenie o pracę) — ctory:** -`new OfertaPracy(WydziałDefinicjiStanowiska stanowisko)`, `new OfertaPracy(RekrutacjaWakat wakat)`. - -**Workery rekrutacji (klasy zagnieżdżone):** - -| Worker | Metoda | Parametry | -|---|---|---| -| `Soneta.HR2.RekrutacjaAplikacja.NowaRekrutacjaWorker` | rozpoczęcie rekrutacji z aplikacji | `Aplikacje: RekrutacjaAplikacja[]` | -| `Soneta.HR2.RekrutacjaWakat.NowaRekrutacjaWorker` | rozpoczęcie rekrutacji z wakatu | `Wakat: RekrutacjaWakat`, `Pracownicy: Pracownik[]` | -| `Soneta.HR2.OfertaPracy.NowaRekrutacjaWorker` | rozpoczęcie rekrutacji z oferty | `Oferta: OfertaPracy`, `Pracownicy: Pracownik[]` | -| `Soneta.HR.OcenaKandydatowWorker` | `Oceń()` | `Pars`, `Elementy: Rekrutacja[]`; ctor `(Context)` | -| `Soneta.HR.Rekrutacja.ZatrudnijWorker` | `PracHistoria Zatrudnij()` | `Pars`, `Rekrutacja: Rekrutacja` — tworzy zatrudnienie (zapis historii) | - -**Snippet (dodanie aplikacji kandydata — wymaga def. stanowiska w bazie):** - -```csharp -var hr2 = session.GetHR2(); -var kandydat = session.GetKadry().Pracownicy.WgKodu["006"]; - -// Definicja stanowiska z konfiguracji HR (musi istnieć w session.GetHR().DefStanowisk): -var defStanowiska = session.GetHR().DefStanowisk - .Cast().FirstOrDefault(); -var wydzialDefStanowiska = new Soneta.HR.WydziałDefinicjiStanowiska(defStanowiska); - -using (var t = session.Logout(editMode: true)) -{ - var aplikacja = session.AddRow(new RekrutacjaAplikacja(kandydat, wydzialDefStanowiska)); - aplikacja.Data = Date.Today; - aplikacja.Stan = StanAplikacji.Wprowadzona; - t.Commit(); -} -session.Save(); - -// Odczyt aplikacji kandydata: -foreach (RekrutacjaAplikacja a in kandydat.Aplikacje) -{ - // a.Stanowisko, a.Stan, a.Data, a.Oferta -} -``` - -**Pułapki:** -- Cały proces rekrutacji wymaga **konfiguracji HR/HR2** (`DefinicjaStanowiska`, - `DefinicjaEtapuRekrutacji`, `WydziałDefinicjiStanowiska`). W bazie Demo te definicje **mogą nie - istnieć** — przed scenariuszem sprawdź dostępność, inaczej `Save()` rzuci wyjątek weryfikatora. -- `RekrutacjaAplikacja` przyjmuje w ctorze `WydziałDefinicjiStanowiska`, nie samą `DefinicjaStanowiska` - (wydział definicji powstaje z `new WydziałDefinicjiStanowiska(definicjaStanowiska)`). -- `EtapRekrutacji` i `Rekrutacja` implementują `IOcenaPracownika` — oceny etapów trzyma - `EtapRekrutacji.ElementyOceny` (te same `ElementOcenyPracownika` co w K8). -- `new Rekrutacja(pracownik)` ustawia pole `Pracownik` i dodaje rekord do **roota** `HRModule.Rekrutacje` - (oraz `EtapRekrutacji` do `HRModule.EtapyRekrutacji`). Kolekcje na `Pracownik` (`Rekrutacje`/`Kandydatury`/ - `EtapyRekrutacji`) to `ChildTable` wiązane przez relacje — do weryfikacji w teście pewniejszy jest root - `session.GetHR().Rekrutacje` niż `pracownik.Rekrutacje` (zależnie od relacji może być pusta dla samego `Pracownik`). -- Zatrudnienie kandydata realizuje `Rekrutacja.ZatrudnijWorker.Zatrudnij()` (zwraca `PracHistoria`) — - spina rekrutację z zatrudnieniem (sekcja A). Worker modyfikuje dane → transakcja + `CommitUI()` + `Save()`. -- `Stan` aplikacji (`Wprowadzona`/`Zakończona`/`Anulowana`) steruje cyklem życia — nie usuwaj aplikacji - z historią, oznaczaj `Anulowana`.