SKILL: Uporządkowanie skills domenowych - podział na mniejsze pliki i wspólna numeracja
This commit is contained in:
@@ -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) |
|
||||
|
||||
@@ -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<KontaktOsoba>`), `Kontakty` (`SubTable<DaneKontaktowe>`),
|
||||
`AdresyWWW` (`SubTable<AdresWWW>`), `Kategorie` (`SubTable<KategoriaKth>`),
|
||||
`Branze` (`SubTable<BranzaKth>`), `Opiekunowie` (`SubTable<Opiekun>`),
|
||||
`Rachunki` (`SubTable<Soneta.Kasa.RachunekBankowyPodmiotu>`),
|
||||
`Rozrachunki` (`SubTable<Soneta.Kasa.RozrachunekIdx>`), `Podrzedni` (`SubTable<RelacjaPodmiotu>`),
|
||||
`StatusyVAT` (`SubTable<StatusVAT>`), `KodyKreskowe` (`SubTable<KodKreskowy>`),
|
||||
`GIODOOświadczenia` (`SubTable<GIODOOświadczenie>`), `GIODOUdostępnienia` (`SubTable<GIODOWymianaDanych>`),
|
||||
`PotwierdzeniaGIODO` (`SubTable<GIODOZgodny>`).
|
||||
- **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.
|
||||
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
@@ -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`).
|
||||
|
||||
---
|
||||
|
||||
@@ -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<DaneKontaktowe>` (`Rodzaj`, `Kontakt`, `Domyslny`) |
|
||||
| Adresy WWW | `AdresyWWW: SubTable<AdresWWW>` (`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<KontaktOsoba>` (`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]`.
|
||||
|
||||
---
|
||||
|
||||
@@ -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<RachunekBankowyPodmiotu>`, `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<RachunekBankowyPodmiotu>` |
|
||||
|
||||
**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<Soneta.Kasa.RozrachunekIdx>` |
|
||||
| Płatności / zapłaty | `Platnosci: SubTable<Platnosc>`, `Zaplaty: SubTable<Zaplata>` |
|
||||
| Dokumenty rozliczeniowe | `DokumentyRozliczeniowe: SubTable<DokRozliczBase>` |
|
||||
| Przelewy | `Przelewy: SubTable<PrzelewBase>` |
|
||||
|
||||
**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`.
|
||||
|
||||
---
|
||||
|
||||
@@ -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<DokEwidencji>` |
|
||||
| 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.
|
||||
|
||||
---
|
||||
|
||||
@@ -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<KategoriaKth>` (poj. lub worker `KontrahenciPrzypiszKategorieWorker`) |
|
||||
| Branże | `Branze: SubTable<BranzaKth>` |
|
||||
| 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<KategoriaKth>`, `Branze: SubTable<BranzaKth>`.
|
||||
`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.
|
||||
|
||||
---
|
||||
|
||||
@@ -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<Opiekun>`, worker `UstawOpiekunaGlownegoWorker` |
|
||||
| Sprawdzenie opieki na dzień | metody `JestOpiekunemNaDzis(...)`, `OpiekunowieWOkresie(...)` |
|
||||
| Podmiot nadrzędny / podrzędny | workery `NowyPodmiotNadrzednyWorker`, `NowyPodmiotPodrzednyWorker` |
|
||||
| Relacje podmiotów | `Podrzedni: SubTable<RelacjaPodmiotu>`, `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.
|
||||
|
||||
---
|
||||
|
||||
@@ -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<StatusVAT>` | — |
|
||||
|
||||
**Pola i typy (odczyt wyniku):** `AktualnyStatusVAT`, `AktualnyStatusVATMF`, `AktualnyStatusVATVies`
|
||||
(typ `Soneta.CRM.StatusNumeruVAT`, kalkulowane), `AktStatusVATData/DataMF/DataVIES: DateTime?`,
|
||||
`StatusyVAT: SubTable<StatusVAT>`.
|
||||
|
||||
**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).
|
||||
|
||||
---
|
||||
|
||||
@@ -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<GIODOOświadczenie>` |
|
||||
| Pozyskanie danych | `KontrahentDodajPozyskanieDanychWorker` | `GIODOUdostępnienia` |
|
||||
| Udostępnienie danych | `KontrahentDodajUdostepnienieDanychWorker` | `GIODOUdostępnienia` |
|
||||
| Powierzenie danych | `KontrahentDodajPowierzenieDanychWorker` | `GIODOUdostępnienia` |
|
||||
| Potwierdzenia zgodności | — | `PotwierdzeniaGIODO: SubTable<GIODOZgodny>` |
|
||||
|
||||
**Pola i typy:** `GIODOOświadczenia: SubTable<GIODOOświadczenie>`,
|
||||
`GIODOUdostępnienia: SubTable<GIODOWymianaDanych>`, `PotwierdzeniaGIODO: SubTable<GIODOZgodny>`,
|
||||
`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.
|
||||
|
||||
---
|
||||
|
||||
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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<PozycjaDokHandlowego>` | pozycje dokumentu |
|
||||
| `SumyVAT` | `SubTable<SumaVAT>` | tabelka VAT (netto/VAT/brutto wg stawek) |
|
||||
| `Platnosci` | `SubTable<Soneta.Kasa.Platnosc>` | płatności dokumentu |
|
||||
| `Obroty` | `SubTable` | obroty magazynowe bezpośrednie dokumentu |
|
||||
| `ObrotyWszystkie` | `ListWithView` | obroty łącznie z dokumentami zależnymi |
|
||||
| `Zasoby` | `SubTable` | zasoby magazynowe utworzone przez dokument |
|
||||
| `DokumentyMagazynowe` | `DokumentHandlowy[]` | dokumenty magazynowe powiązane z fakturą |
|
||||
| `DokumentyHandlowe` | `DokumentHandlowy[]` | faktury powiązane z dokumentem magazynowym |
|
||||
| `DokumentKorygowany` | `DokumentHandlowy` | dokument korygowany (kalkulowane — tworzy relacja/UI) |
|
||||
| `DokumentyKorygujące` | `IEnumerable<DokumentHandlowy>` | korekty tego dokumentu |
|
||||
| `DokumentyZaliczkowe` | `DokumentHandlowy[]` | nadrzędne dokumenty zaliczkowe |
|
||||
| `Rezerwacja` | `DokumentHandlowy` | dokument rezerwacji towarów |
|
||||
| `SumaPozycji` | `BruttoNettoPozycji` | wyliczona suma wartości pozycji |
|
||||
| `Bufor` / `Zatwierdzony` / `Anulowany` | `bool` | skróty stanu (kalkulowane z `Stan`) |
|
||||
| `Features` | `Soneta.Business.FeatureCollection` | cechy definiowalne dokumentu |
|
||||
|
||||
### Pozycja dokumentu — `Soneta.Handel.PozycjaDokHandlowego`
|
||||
|
||||
| Pole | Typ | Znaczenie |
|
||||
|---|---|---|
|
||||
| `Towar` | `Soneta.Towary.Towar` | towar pozycji (ustaw pierwszy — inicjuje jednostkę na `Ilosc`/`Cena`) |
|
||||
| `Ilosc` | `Soneta.Towary.Quantity` | ilość; twórz `new Quantity(wartość, poz.Ilosc.Symbol)` |
|
||||
| `Cena` | `Soneta.Types.DoubleCy` | cena (netto/brutto wg `LiczonaOd`); `new DoubleCy(wartość, poz.Cena.Symbol)` |
|
||||
| `Rabat` | `Soneta.Types.Percent` | procent rabatu |
|
||||
| `Features` | `FeatureCollection` | cechy pozycji (m.in. przeniesione z partii/towaru) |
|
||||
|
||||
Konstruktor pozycji wymaga dokumentu: `new PozycjaDokHandlowego(dokument)`.
|
||||
|
||||
## Podstawowe typy i obiekty pomocnicze
|
||||
|
||||
| Typ | Rola |
|
||||
|---|---|
|
||||
| `Soneta.Handel.HandelModule` | moduł Handel: `DokHandlowe`, `DefDokHandlowych` |
|
||||
| `Soneta.Magazyny.MagazynyModule` | magazyny, zasoby, obroty, partie (`GrupaDostaw`) — `session.GetMagazyny()` |
|
||||
| `Soneta.Towary.TowaryModule` | towary, jednostki, ceny — `session.GetTowary()` |
|
||||
| `Soneta.CRM.CRMModule` | kontrahenci — `session.GetCRM()` |
|
||||
| `Soneta.Handel.DefDokHandlowego` | definicja dokumentu (symbol, kierunek, numeracja, flagi) |
|
||||
| `Soneta.Types.Quantity` | ilość z jednostką miary |
|
||||
| `Soneta.Types.DoubleCy` | wartość zmiennoprzecinkowa z walutą (cena) |
|
||||
| `Soneta.Types.Currency` | kwota z walutą (wartości, płatności) |
|
||||
| `Soneta.Types.Percent` | procent (rabat, stawka) |
|
||||
| `Soneta.Types.Date` | data biznesowa |
|
||||
| `Soneta.Handel.StanDokumentuHandlowego` | stan cyklu życia dokumentu |
|
||||
|
||||
## Szablon wzorca
|
||||
|
||||
Każdy wzorzec (`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.
|
||||
@@ -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`.
|
||||
|
||||
---
|
||||
|
||||
@@ -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<SumaVAT>`,
|
||||
`Platnosci: SubTable<Soneta.Kasa.Platnosc>` (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`.
|
||||
|
||||
---
|
||||
|
||||
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
@@ -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<IRelacjeService>(); // rzuca, gdy serwisu brak
|
||||
> // albo: var rel = session.GetService<IRelacjeService>(); // 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<Context, Magazyn>` | definicja relacji ma `WyborPozycji = WybórMagazynu` — wskaż magazyn docelowy |
|
||||
| `WybierzMagazynDocelowyCallback` | `Func<DokumentDocelowy, Magazyn>` | wybór magazynu dla dokumentu docelowego (domyślnie `d.MagazynDo`) |
|
||||
| `WybierzPozycjeCallback` | `Action<DokumentDocelowy>` | definicja ma `WyborPozycji = WybórPozycji` — zaznacz pozycje (domyślnie `PrzeliczPozycje()`) |
|
||||
| `WybierzDostawyCallback` | `Action<DostawaWorker>` | wskazanie partii/dostaw przy rozchodzie (gdy `WskazaniePartii` wymuszone) |
|
||||
| `WybierzDokumentyZaliczkoweCallback` | `Action<DokumentDocelowy>` | faktura z zaliczkami |
|
||||
| `UstawParametryFakturowania` | `Action<DefRelacjiCyklicznaFakturowanieParams>` | fakturowanie cykliczne |
|
||||
|
||||
Domyślnie `WybierzPozycjeCallback` przepisuje wszystkie pozycje (`PrzeliczPozycje()`). Callbacki bez
|
||||
sensownej wartości domyślnej (`WybierzMagazynCallback`, `WybierzDostawyCallback`,
|
||||
`WybierzDokumentyZaliczkoweCallback`) rzucają `NotImplementedException`, dopóki ich nie nadpiszesz.
|
||||
|
||||
---
|
||||
|
||||
### 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<IRelacjeService>();
|
||||
|
||||
// zamowienie jest już zatwierdzone (StanDokumentuHandlowego.Zatwierdzony)
|
||||
DokumentHandlowy[] faktury;
|
||||
using (var t = session.Logout(editMode: true))
|
||||
{
|
||||
faktury = rel.NowyPodrzednyIndywidualny(
|
||||
new[] { zamowienie },
|
||||
"FV"); // bez HandlerSet — gdy relacja nie wymaga rozstrzygnięć
|
||||
t.Commit(); // CommitUI() w workerze/extenderze
|
||||
}
|
||||
session.Save();
|
||||
|
||||
DokumentHandlowy faktura = faktury[0]; // jeden nadrzędny → jeden podrzędny
|
||||
```
|
||||
|
||||
Wariant z wyborem pozycji (przepisz tylko pozycje danego towaru):
|
||||
|
||||
```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<IRelacjeService>();
|
||||
|
||||
// faktury: DokumentHandlowy[] — wszystkie zatwierdzone, zgodne (ten sam kontrahent/magazyn wg konfiguracji)
|
||||
DokumentHandlowy wz;
|
||||
using (var t = session.Logout(editMode: true))
|
||||
{
|
||||
var wynik = rel.NowyPodrzednyZbiorczy(faktury, "WZ");
|
||||
wz = wynik[0]; // jeden zbiorczy dokument magazynowy
|
||||
t.Commit();
|
||||
}
|
||||
session.Save();
|
||||
```
|
||||
|
||||
**Pułapki:**
|
||||
- Dokumenty zbiorcze powstają tylko z dokumentów **zgodnych** (wymóg ten sam kontrahent / magazyn /
|
||||
waluta — zależnie od definicji relacji zbiorczej). Niezgodne wejście → wyjątek lub pominięcie.
|
||||
- Wszystkie nadrzędne muszą być **zatwierdzone**.
|
||||
- Tak jak w 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<IRelacjeService>();
|
||||
|
||||
// wydania: DokumentHandlowy[] — zatwierdzone WZ tego samego kontrahenta
|
||||
DokumentHandlowy fakturaZbiorcza;
|
||||
using (var t = session.Logout(editMode: true))
|
||||
{
|
||||
fakturaZbiorcza = rel.NowyPodrzednyZbiorczy(wydania, "FV")[0];
|
||||
t.Commit();
|
||||
}
|
||||
session.Save();
|
||||
```
|
||||
|
||||
**Pułapki:**
|
||||
- Kierunek relacji (magazynowy → handlowy) musi być skonfigurowany jako `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<IRelacjeService>();
|
||||
|
||||
DokumentHandlowy wz;
|
||||
using (var t = session.Logout(editMode: true))
|
||||
{
|
||||
var wynik = rel.NowyPodrzednyIndywidualny(
|
||||
new[] { faktura }, "WZ",
|
||||
handlers: new HandlerSet
|
||||
{
|
||||
WybierzDostawyCallback = dostawaWorker =>
|
||||
{
|
||||
// dla każdej pozycji wskaż pobierane zasoby/partie
|
||||
foreach (var poz in dostawaWorker.GetListPozycja())
|
||||
{
|
||||
dostawaWorker.Pozycja = poz;
|
||||
foreach (Zasob z in dostawaWorker.Zasoby.Cast<Zasob>())
|
||||
{
|
||||
using var tz = z.Session.Logout(editMode: true);
|
||||
// ... oznacz zasób jako pobrany (Pobrano = true)
|
||||
tz.Commit();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
wz = wynik[0];
|
||||
t.Commit();
|
||||
}
|
||||
session.Save();
|
||||
```
|
||||
|
||||
**Pułapki:**
|
||||
- Gdy definicja `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<IRelacjeService>();
|
||||
|
||||
// Dołącz fakturę do istniejącego zamówienia jako nadrzędnego (relacja "Zamówienie")
|
||||
using (var t = session.Logout(editMode: true))
|
||||
{
|
||||
var powiazane = rel.DolaczNadrzedny(new[] { faktura }, "Zamówienie");
|
||||
t.Commit();
|
||||
}
|
||||
session.Save();
|
||||
```
|
||||
|
||||
**Pułapki:**
|
||||
- `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`).
|
||||
|
||||
---
|
||||
|
||||
@@ -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<PozycjaDokHandlowego>` (sortowane po `Lp`).
|
||||
- **Pozycje danego towaru (historia obrotu):** `towar.Pozycje` — `SubTable<PozycjaDokHandlowego>`
|
||||
(klucz `WgTowar`). Klucze na `PozycjeDokHan`: `WgDaty` (`Data`), `WgKierunek`
|
||||
(`Towar`, `KierunekMagazynu`, `Data`, `Czas`), `WgTowarDokumentu` (`Towar`, `Dokument`).
|
||||
- **Numer dokumentu:** pole `dok.Numer: NumerDokumentu`. Pełny numer do **odczytu** to
|
||||
`dok.Numer.NumerPelny` (kalkulowane). W warunku serwerowym używaj pola bazodanowego `Numer.Pelny`
|
||||
(np. `dok => dok.Numer.Pelny == "FV 1/2026"`).
|
||||
- **Korekty:** `dok.DokumentKorygowany` (dokument korygowany przez tę korektę),
|
||||
`dok.DokumentyKorygujące` (`IEnumerable<DokumentHandlowy>` — łańcuch korekt tego dokumentu),
|
||||
`dok.Korekta: bool` (pole bazodanowe — czy dokument jest korektą). Wszystkie powiązania korekt to
|
||||
pola **kalkulowane** (oprócz `Korekta`).
|
||||
- **Kolekcje na `Kontrahent` (z modułu CRM):** `k.DokumentyHandlowe` i `k.DokumentyHandloweOdbiorcy`
|
||||
to **nietypowane** `SubTable` (CRM nie referuje Handlu). Iteracja działa, ale typowane filtrowanie
|
||||
serwerowe rób od strony Handlu: `hm.DokHandlowe.WgKontrahentaObcy[dok => dok.Kontrahent == k]`.
|
||||
|
||||
---
|
||||
|
||||
### HANDEL-W25 — Odczytanie pozycji dokumentu
|
||||
|
||||
**Cel:** przejść po pozycjach (towar, ilość, cena, rabat, wartość) wczytanego dokumentu — np. do
|
||||
wydruku, eksportu czy przeliczeń własnych.
|
||||
|
||||
**Warianty:**
|
||||
|
||||
| Wariant | Źródło / operacja |
|
||||
|---|---|
|
||||
| Wszystkie pozycje wg Lp | `dok.Pozycje` (`LpSubTable`, sortowane po `Lp`) |
|
||||
| Tylko pozycje danego towaru | `dok.Pozycje[(PozycjaDokHandlowego p) => p.Towar == towar]` |
|
||||
| Pozycje o niezerowej ilości | warunek serwerowy na `p.Ilosc.Value` |
|
||||
| Wartości pozycji | `p.WartoscCy`, `p.Suma` (`BruttoNetto`: `NettoCy`/`VATCy`/`BruttoCy`) |
|
||||
|
||||
**Pola i typy (`PozycjaDokHandlowego`):** `Towar: Towar`, `Ilosc: Quantity`
|
||||
(`.Value`, `.Symbol`), `Cena: DoubleCy`, `Rabat: Percent`, `WartoscCy: Currency`,
|
||||
`Suma: BruttoNetto` (`NettoCy`, `VATCy`, `BruttoCy` — typ `Currency`; `Netto`/`VAT`/`Brutto` — `decimal`),
|
||||
`Lp: int`, `Stawka: StawkaVat`, `Opis: string`.
|
||||
|
||||
**Snippet:**
|
||||
|
||||
```csharp
|
||||
var hm = session.GetHandel();
|
||||
var dok = hm.DokHandlowe[guid]; // dokument wczytany po Guid (HANDEL-W29)
|
||||
if (dok == null) return;
|
||||
|
||||
// Iteracja po pozycjach (LpSubTable jest już posortowana po Lp):
|
||||
foreach (PozycjaDokHandlowego p in dok.Pozycje)
|
||||
{
|
||||
string towar = p.Towar?.Kod;
|
||||
Quantity ilosc = p.Ilosc; // p.Ilosc.Value + p.Ilosc.Symbol (jednostka)
|
||||
DoubleCy cena = p.Cena;
|
||||
Percent rabat = p.Rabat;
|
||||
Currency netto = p.Suma.NettoCy; // wartość netto pozycji w PLN
|
||||
Currency brutto = p.Suma.BruttoCy;
|
||||
Currency wartosc = p.WartoscCy; // wartość pozycji w walucie ceny
|
||||
}
|
||||
|
||||
// Tylko pozycje wybranego towaru — filtr serwerowy na kolekcji:
|
||||
var towar = session.GetTowary().Towary.WgKodu["BIKINI"];
|
||||
foreach (PozycjaDokHandlowego p in dok.Pozycje[(PozycjaDokHandlowego p) => p.Towar == towar])
|
||||
{
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**Pułapki:**
|
||||
- `Ilosc` to `Quantity`, a `Cena`/`WartoscCy` to `DoubleCy`/`Currency` (kwota + waluta), **nie**
|
||||
`decimal`/`double` (safe-code §10). Składowe: `p.Ilosc.Value`, `p.Ilosc.Symbol`.
|
||||
- Do filtrowania pozycji **na jednym dokumencie** możesz iterować `dok.Pozycje` (to mała kolekcja),
|
||||
ale i tak preferuj warunek `dok.Pozycje[p => …]` — wykona się serwerowo.
|
||||
- `p.Suma`/`p.WartoscCy` są przeliczane przez platformę — czytaj je, nie wyliczaj „ręcznie".
|
||||
- `p.Towar` bywa `null` dla pozycji nietowarowych (opis/koszt) — zabezpiecz dostęp (`?.`).
|
||||
|
||||
---
|
||||
|
||||
### HANDEL-W26 — Odczytanie dokumentów dla kontrahenta
|
||||
|
||||
**Cel:** pobrać dokumenty wystawione na danego kontrahenta — jako nabywcę (`Kontrahent`) lub jako
|
||||
odbiorcę (`Odbiorca`).
|
||||
|
||||
**Warianty:**
|
||||
|
||||
| Wariant | Źródło | Typ |
|
||||
|---|---|---|
|
||||
| Kontrahent jako nabywca (kolekcja CRM) | `k.DokumentyHandlowe` | nietypowany `SubTable` |
|
||||
| Odbiorca (kolekcja CRM) | `k.DokumentyHandloweOdbiorcy` | nietypowany `SubTable` |
|
||||
| Filtr typowany od strony Handlu | `hm.DokHandlowe.WgKontrahentaObcy[dok => dok.Kontrahent == k]` | `SubTable<DokumentHandlowy>` |
|
||||
| Zawężenie okresem | dołóż `&& dok.Data >= od` w warunku | — |
|
||||
|
||||
**Pola i typy:** `dok.Kontrahent: Kontrahent`, `dok.Odbiorca: Kontrahent` (oba bazodanowe).
|
||||
`Kontrahent.DokumentyHandlowe` / `DokumentyHandloweOdbiorcy` to kolekcje `SubTable` na kontrahencie
|
||||
(zawężone już do jednego kontrahenta).
|
||||
|
||||
**Snippet:**
|
||||
|
||||
```csharp
|
||||
var hm = session.GetHandel();
|
||||
var k = session.GetCRM().Kontrahenci.WgKodu["Abc"];
|
||||
if (k == null) return;
|
||||
|
||||
// Wariant A — kolekcja na kontrahencie (nietypowana, ale wygodna do prostego przejścia):
|
||||
foreach (DokumentHandlowy dok in k.DokumentyHandlowe)
|
||||
{
|
||||
// dok.Numer.NumerPelny, dok.Data, dok.Suma ...
|
||||
}
|
||||
|
||||
// Wariant B — typowany filtr serwerowy od strony Handlu + zawężenie okresem
|
||||
// (klucz WgKontrahentaObcy nadaje sortowanie wg kontrahenta):
|
||||
var od = Date.Today.AddMonths(-3);
|
||||
foreach (DokumentHandlowy dok in hm.DokHandlowe.WgKontrahentaObcy[
|
||||
(DokumentHandlowy dok) => dok.Kontrahent == k && dok.Data >= od])
|
||||
{
|
||||
// tylko dokumenty kontrahenta z ostatnich 3 miesięcy
|
||||
}
|
||||
|
||||
// Dokumenty, w których kontrahent jest ODBIORCĄ:
|
||||
foreach (DokumentHandlowy dok in hm.DokHandlowe[
|
||||
(DokumentHandlowy dok) => dok.Odbiorca == k])
|
||||
{
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**Pułapki:**
|
||||
- `k.DokumentyHandlowe` jest **nietypowane** (`SubTable`, nie `SubTable<DokumentHandlowy>`) — pętla
|
||||
`foreach (DokumentHandlowy …)` działa, ale do filtrowania wyrażeniem LINQ użyj kolekcji od strony
|
||||
Handlu (`hm.DokHandlowe.WgXxx[…]`), gdzie typ wiersza jest znany kompilatorowi.
|
||||
- `Kontrahent` i `Odbiorca` to **dwa różne pola** — wybierz świadomie (nabywca ≠ odbiorca towaru).
|
||||
- To dane operacyjne — przy szerokich analizach **zawężaj okres** (`dok.Data >= od`), nie ładuj całej
|
||||
historii (safe-code §6.3).
|
||||
- Porównuj po referencji rekordu (`dok.Kontrahent == k`), a nie po `Kod` — referencja generuje
|
||||
szybkie `JOIN` po `ID`.
|
||||
|
||||
---
|
||||
|
||||
### HANDEL-W27 — Ostatnie pozycje dokumentów dla wskazanego towaru
|
||||
|
||||
**Cel:** prześledzić historię obrotu danym towarem — pozycje dokumentów, w których towar wystąpił
|
||||
(np. ostatnie zakupy/sprzedaże, kierunek magazynowy, ceny historyczne).
|
||||
|
||||
**Warianty:**
|
||||
|
||||
| Wariant | Źródło / warunek |
|
||||
|---|---|
|
||||
| Wszystkie pozycje towaru | `towar.Pozycje` (klucz `WgTowar`) |
|
||||
| Tylko rozchody / przychody | filtr na `p.KierunekMagazynu` (`KierunekPartii`) |
|
||||
| Z zakresu dat | `towar.Pozycje[p => p.Data >= od]` |
|
||||
| Tylko z dokumentów zatwierdzonych | warunek przez referencję: `p.Dokument.Stan == StanDokumentuHandlowego.Zatwierdzony` |
|
||||
| Ostatnie N po dacie | sortuj kluczem `WgKierunek`/`WgDaty` i ogranicz w pamięci po zawężeniu |
|
||||
|
||||
**Pola i typy (`PozycjaDokHandlowego`):** `Towar: Towar`, `Dokument: DokumentHandlowy`,
|
||||
`Data: Date`, `Czas: Time`, `KierunekMagazynu: Soneta.Magazyny.KierunekPartii`
|
||||
(`Rozchód=-1`, `Brak=0`, `Przychód=1`), `Cena: DoubleCy`, `Ilosc: Quantity`. Kolekcja
|
||||
`towar.Pozycje: SubTable<PozycjaDokHandlowego>`.
|
||||
|
||||
**Snippet:**
|
||||
|
||||
```csharp
|
||||
var towar = session.GetTowary().Towary.WgKodu["BIKINI"];
|
||||
if (towar == null) return;
|
||||
|
||||
// Pozycje towaru z ostatnich 6 miesięcy — filtr serwerowy na kolekcji towaru:
|
||||
var od = Date.Today.AddMonths(-6);
|
||||
foreach (PozycjaDokHandlowego p in towar.Pozycje[(PozycjaDokHandlowego p) => p.Data >= od])
|
||||
{
|
||||
DokumentHandlowy dok = p.Dokument; // dokument macierzysty pozycji
|
||||
string numer = dok.Numer.NumerPelny;
|
||||
// p.KierunekMagazynu, p.Ilosc, p.Cena, p.Data ...
|
||||
}
|
||||
|
||||
// Tylko rozchody (sprzedaż/wydania) danego towaru z dokumentów zatwierdzonych:
|
||||
foreach (PozycjaDokHandlowego p in towar.Pozycje[(PozycjaDokHandlowego p) =>
|
||||
p.KierunekMagazynu == KierunekPartii.Rozchód
|
||||
&& p.Dokument.Stan == StanDokumentuHandlowego.Zatwierdzony
|
||||
&& p.Data >= od])
|
||||
{
|
||||
// historia rozchodów towaru
|
||||
}
|
||||
```
|
||||
|
||||
**Pułapki:**
|
||||
- Filtruj na `towar.Pozycje[…]` (kolekcja zawężona do jednego towaru), nie iteruj globalnie
|
||||
`PozycjeDokHan` — to jedna z największych tabel operacyjnych (safe-code §6.3).
|
||||
- Warunek przez referencję (`p.Dokument.Stan == …`) jest dozwolony — `Stan` jest polem
|
||||
bazodanowym i wygeneruje `JOIN`. Nie używaj w warunku pól kalkulowanych dokumentu
|
||||
(np. `p.Dokument.Zatwierdzony` rzuci `LinqConditionException`).
|
||||
- „Ostatnie N" realizuj przez sortowanie kluczem (`WgKierunek`/`WgDaty`) **po** zawężeniu okresem;
|
||||
nie pobieraj całości po to, by wziąć kilka rekordów.
|
||||
- `KierunekPartii` żyje w `Soneta.Magazyny` — wymagana referencja do modułu Magazyny.
|
||||
|
||||
---
|
||||
|
||||
### HANDEL-W28 — Wyszukiwanie dokumentów wg okresu, definicji, stanu, serii
|
||||
|
||||
**Cel:** odfiltrować dokumenty po kryteriach nagłówkowych (data, definicja, stan, magazyn, seria)
|
||||
serwerowo, bez obiektów warstwy UI (`View`).
|
||||
|
||||
**Warianty:**
|
||||
|
||||
| Wariant | Warunek (pole bazodanowe) |
|
||||
|---|---|
|
||||
| Okres dat | `dok.Data >= od && dok.Data <= do` |
|
||||
| Konkretna definicja (symbol) | `dok.Definicja == def` (rekord z `DefDokHandlowych.WgSymbolu[...]`) |
|
||||
| Stan dokumentu | `dok.Stan == StanDokumentuHandlowego.Zatwierdzony` |
|
||||
| Magazyn | `dok.Magazyn == mag` |
|
||||
| Seria | `dok.Seria == "A"` |
|
||||
| Wiele kryteriów | koniunkcja `&&` / alternatywa `||` w jednym wyrażeniu |
|
||||
|
||||
**Pola i typy:** `dok.Data: Date`, `dok.Definicja: DefDokHandlowego`,
|
||||
`dok.Stan: StanDokumentuHandlowego`, `dok.Magazyn: Magazyn`, `dok.Seria: string`,
|
||||
`dok.Kategoria: KategoriaHandlowa`. Klucz `WgDaty` daje sortowanie po dacie.
|
||||
|
||||
**Snippet:**
|
||||
|
||||
```csharp
|
||||
var hm = session.GetHandel();
|
||||
|
||||
var def = hm.DefDokHandlowych.WgSymbolu["FV"]; // definicja faktury sprzedaży
|
||||
var mag = session.GetMagazyny().Magazyny.WgSymbol["F"];
|
||||
var od = new Date(2026, 1, 1);
|
||||
var doDt = new Date(2026, 3, 31);
|
||||
|
||||
// Zatwierdzone faktury FV z I kwartału na magazynie F — jeden warunek serwerowy.
|
||||
// Klucz WgDaty nadaje sortowanie po Data, Czas:
|
||||
foreach (DokumentHandlowy dok in hm.DokHandlowe.WgDaty[(DokumentHandlowy dok) =>
|
||||
dok.Definicja == def
|
||||
&& dok.Magazyn == mag
|
||||
&& dok.Stan == StanDokumentuHandlowego.Zatwierdzony
|
||||
&& dok.Data >= od && dok.Data <= doDt])
|
||||
{
|
||||
// dok.Numer.NumerPelny, dok.Suma, dok.Kontrahent ...
|
||||
}
|
||||
|
||||
// Wariant: warunek jako wartość przekazywana dalej (np. do metody):
|
||||
var cond = RowCondition.FromExpression<DokumentHandlowy>(
|
||||
dok => dok.Definicja == def && dok.Seria == "A");
|
||||
foreach (DokumentHandlowy dok in hm.DokHandlowe.WgDaty[cond]) { /* ... */ }
|
||||
```
|
||||
|
||||
**Pułapki:**
|
||||
- **Nie używaj `View`** w kodzie biznesowym (to obiekt UI) — filtruj `SubTable[expression]` lub
|
||||
`RowCondition.FromExpression` ([`rowcondition.md`](../rowcondition.md)).
|
||||
- Porównuj definicję/magazyn po **rekordzie** (`dok.Definicja == def`), nie po stringu symbolu —
|
||||
rekord pobierz raz przez `WgSymbolu[...]`/`WgSymbol[...]` poza pętlą.
|
||||
- Stan porównuj enumem (`dok.Stan == StanDokumentuHandlowego.Zatwierdzony`); skróty `dok.Zatwierdzony`
|
||||
są kalkulowane i **nie wolno** ich użyć w warunku LINQ.
|
||||
- Wybór klucza (`WgDaty`, `WgMagazynuNumer`, `WgKontrahentaObcy`) decyduje tylko o **sortowaniu** —
|
||||
warunek i tak trafia do `WHERE`. Dla dużych zbiorów dobierz klucz pasujący do oczekiwanej kolejności.
|
||||
|
||||
---
|
||||
|
||||
### HANDEL-W29 — Odczyt dokumentu wg numeru lub Guid
|
||||
|
||||
**Cel:** odnaleźć pojedynczy dokument po jego pełnym numerze (`Numer.Pelny`) albo po globalnym
|
||||
identyfikatorze `Guid` (np. zapisanym wcześniej w innym systemie / w teście).
|
||||
|
||||
**Warianty:**
|
||||
|
||||
| Wariant | Mechanizm | Zwraca |
|
||||
|---|---|---|
|
||||
| Po Guid | `hm.DokHandlowe[guid]` (indeksator `GuidedTable`) | `DokumentHandlowy`; **rzuca `RowNotFoundException`**, gdy brak |
|
||||
| Po pełnym numerze | filtr serwerowy `dok => dok.Numer.Pelny == numer` | zbiór (bierz `.FirstOrDefault()`) |
|
||||
| Po numerze w obrębie magazynu | klucz `WgMagazynuNumer` (`Magazyn` + `Numer.Pelny`) | precyzyjniej (numer bywa unikalny per magazyn) |
|
||||
| Po numerze obcym | klucz `WgMagazynuObcy` / pole `dok.Obcy.Numer` | dokument z numerem dostawcy |
|
||||
|
||||
**Pola i typy:** `dok.Numer: NumerDokumentu` (odczyt pełnego numeru: `dok.Numer.NumerPelny`;
|
||||
pole bazodanowe w warunku: `Numer.Pelny`), `dok.Guid: Guid` (z `GuidedRow`),
|
||||
`dok.Obcy.Numer: string` (numer dokumentu obcego).
|
||||
|
||||
**Snippet:**
|
||||
|
||||
```csharp
|
||||
var hm = session.GetHandel();
|
||||
|
||||
// 1. Po Guid — najpewniejszy, jednoznaczny dostęp. UWAGA: indeksator GuidedTable RZUCA
|
||||
// RowNotFoundException dla nieznanego Guid (nie zwraca null) — obuduj try/catch, gdy brak pewności:
|
||||
DokumentHandlowy poGuid;
|
||||
try { poGuid = hm.DokHandlowe[guid]; }
|
||||
catch (Soneta.Business.RowNotFoundException) { poGuid = null; }
|
||||
|
||||
// 2. Po pełnym numerze — warunek serwerowy na polu bazodanowym Numer.Pelny.
|
||||
// Numer może się powtarzać między magazynami, więc bierzemy pierwszy / iterujemy:
|
||||
DokumentHandlowy poNumerze = hm.DokHandlowe.WgMagazynuNumer[
|
||||
(DokumentHandlowy dok) => dok.Numer.Pelny == "FV 1/2026"].FirstOrDefault();
|
||||
|
||||
// 3. Po numerze w obrębie magazynu (precyzyjniej — numeracja zwykle per magazyn):
|
||||
var mag = session.GetMagazyny().Magazyny.WgSymbol["F"];
|
||||
DokumentHandlowy wMagazynie = hm.DokHandlowe.WgMagazynuNumer[(DokumentHandlowy dok) =>
|
||||
dok.Magazyn == mag && dok.Numer.Pelny == "FV 1/2026"].FirstOrDefault();
|
||||
|
||||
if (poGuid != null)
|
||||
{
|
||||
string pelny = poGuid.Numer.NumerPelny; // odczyt pełnego numeru (kalkulowane)
|
||||
}
|
||||
```
|
||||
|
||||
**Pułapki:**
|
||||
- W warunku LINQ używaj pola bazodanowego `Numer.Pelny`; do **odczytu** sformatowanego numeru służy
|
||||
kalkulowane `dok.Numer.NumerPelny` — w wyrażeniu serwerowym rzuciłoby `LinqConditionException`.
|
||||
- Pełny numer **nie jest** globalnie unikalny (numeracja bywa per magazyn/seria/rok) — dlatego filtr
|
||||
zwraca zbiór; bierz `.FirstOrDefault()` albo dołóż `dok.Magazyn == mag`.
|
||||
- Indeksator `hm.DokHandlowe[guid]` to dostęp po `Guid` (z `GuidedTable`) — dla nieznanego `Guid`
|
||||
**rzuca `Soneta.Business.RowNotFoundException`** (NIE zwraca `null`). Gdy brak pewności istnienia,
|
||||
obuduj go `try/catch`. Nie myl z dostępem po `ID` (klucz wewnętrzny tabeli).
|
||||
- Numer obcy (dostawcy) jest w `dok.Obcy.Numer` — to inne pole niż własny `Numer`.
|
||||
|
||||
---
|
||||
|
||||
### HANDEL-W30 — Korekty dokumentu i dokument korygowany
|
||||
|
||||
**Cel:** dla danego dokumentu ustalić jego korekty (dokumenty korygujące) oraz — dla korekty —
|
||||
dokument, który koryguje.
|
||||
|
||||
**Warianty:**
|
||||
|
||||
| Wariant | Pole / kierunek | Typ |
|
||||
|---|---|---|
|
||||
| Dokument korygowany przez tę korektę | `korekta.DokumentKorygowany` | `DokumentHandlowy` (lub `null`) |
|
||||
| Wszystkie korekty danego dokumentu | `dok.DokumentyKorygujące` | `IEnumerable<DokumentHandlowy>` (łańcuch) |
|
||||
| Najbliższa korekta | `dok.DokumentKorygujący` | `DokumentHandlowy` (lub `null`) |
|
||||
| Ostatnia korekta w łańcuchu | `dok.DokumentKorygującyOstatni` | `DokumentHandlowy` |
|
||||
| Czy dokument jest korektą | `dok.Korekta` | `bool` (pole bazodanowe) |
|
||||
| Serwerowy filtr korekt | `hm.DokHandlowe[d => d.Korekta]` | `SubTable<DokumentHandlowy>` |
|
||||
|
||||
**Pola i typy:** `dok.Korekta: bool` (bazodanowe — czy dokument jest korektą),
|
||||
`dok.DokumentKorygowany: DokumentHandlowy`, `dok.DokumentyKorygujące: IEnumerable<DokumentHandlowy>`,
|
||||
`dok.DokumentKorygujący`/`DokumentKorygującyOstatni: DokumentHandlowy`,
|
||||
`dok.DokumentyKorygowane: IEnumerable<DokumentHandlowy>` (cały łańcuch korygowanych) —
|
||||
wszystkie powiązania **kalkulowane** (tylko do odczytu; korekty zakładaj przez `IRelacjeService`).
|
||||
|
||||
**Snippet:**
|
||||
|
||||
```csharp
|
||||
var hm = session.GetHandel();
|
||||
var dok = hm.DokHandlowe[guid];
|
||||
if (dok == null) return;
|
||||
|
||||
// Korekty tego dokumentu (łańcuch korekt — kolejne korekty korekt):
|
||||
foreach (DokumentHandlowy korekta in dok.DokumentyKorygujące)
|
||||
{
|
||||
string nr = korekta.Numer.NumerPelny;
|
||||
DokumentHandlowy korygowany = korekta.DokumentKorygowany; // wskazuje z powrotem na dok
|
||||
}
|
||||
|
||||
// Gdy mamy w ręku korektę — odczyt dokumentu korygowanego:
|
||||
if (dok.Korekta)
|
||||
{
|
||||
DokumentHandlowy zrodlo = dok.DokumentKorygowany; // dokument pierwotny
|
||||
}
|
||||
|
||||
// Serwerowe wyszukanie samych korekt w okresie (pole Korekta jest bazodanowe):
|
||||
var od = Date.Today.AddMonths(-1);
|
||||
foreach (DokumentHandlowy k in hm.DokHandlowe.WgDaty[(DokumentHandlowy d) =>
|
||||
d.Korekta && d.Data >= od])
|
||||
{
|
||||
// d.DokumentKorygowany — dokument, którego dotyczy korekta
|
||||
}
|
||||
```
|
||||
|
||||
**Pułapki:**
|
||||
- `DokumentKorygowany`/`DokumentyKorygujące`/`DokumentKorygujący` 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`.
|
||||
|
||||
---
|
||||
|
||||
@@ -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<Zasob>`) | filtr po `Partia.Dokument == dok` |
|
||||
| Zasoby łącznie z dokumentami zależnymi | `dok.ZasobyWszystkie` (`ListWithView`) | obejmuje powiązane dok. magazynowe |
|
||||
| Iteracja po module | `mag.Zasoby.WgTowar[towar, okres, magazyn]` | gdy nie mamy uchwytu do dokumentu |
|
||||
|
||||
**Pola i typy:** `dok.Zasoby: SubTable` (elementy `Soneta.Magazyny.Zasob`). `Zasob`:
|
||||
`Ilosc: Quantity`, `IloscRezerwowana: Quantity`, `Kierunek: KierunekPartii`,
|
||||
`Magazyn: Magazyn`, `Towar: Towar`, `Okres: OkresMagazynowy`, `Partia: PartiaTowaru` (subrow),
|
||||
`PartiaPierwotna: PartiaTowaru`.
|
||||
|
||||
**Snippet:**
|
||||
|
||||
```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<Obrot>())
|
||||
{
|
||||
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<Obrot>()`. 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<Zasob>`.
|
||||
`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<GrupaDostaw>` |
|
||||
| Wszystkie partie towaru | `mag.GrupyDostaw.WgTowar[towar]` | partie danego towaru |
|
||||
| Po dacie | `mag.GrupyDostaw.WgData[data]` | indeks po `Data` |
|
||||
| Po cesze | `partie[(GrupaDostaw g) => warunek]` na indeksie | cecha musi być zdefiniowana |
|
||||
|
||||
**Pola i typy:** `GrupaDostaw`: `Numer: string` (`public virtual`, czasem nadawany
|
||||
automatycznie), `Towar: Towar`, `Data: Date`, `Blokada: bool`,
|
||||
`Features: FeatureCollection`, `KodKreskowy: string`. Klucz `WgNumer` = (`Numer`, `Towar`).
|
||||
|
||||
**Snippet:**
|
||||
|
||||
```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<Obrot>().FirstOrDefault();
|
||||
PozycjaDokHandlowego pozycjaPrzyjecia = przychod?.Przychod.Dokument?
|
||||
.Pozycje.Cast<PozycjaDokHandlowego>()
|
||||
.FirstOrDefault(p => p.Towar == towar);
|
||||
|
||||
using (var t = session.Logout(editMode: true))
|
||||
{
|
||||
var dok = new DokumentHandlowy();
|
||||
session.AddRow(dok);
|
||||
dok.Definicja = session.GetHandel().DefDokHandlowych.WgSymbolu["WZ"];
|
||||
dok.Magazyn = magazyn;
|
||||
|
||||
var poz = new PozycjaDokHandlowego(dok);
|
||||
session.AddRow(poz);
|
||||
poz.Towar = towar; // USTAW PIERWSZY
|
||||
poz.Ilosc = new Quantity(2, poz.Ilosc.Symbol);
|
||||
poz.Dostawa = pozycjaPrzyjecia; // WSKAZANIE JEDNEJ partii (dostawy)
|
||||
t.Commit(); // CommitUI() w workerze/extenderze
|
||||
}
|
||||
session.Save(); // tu nalicza się obrót/zasób rozchodowy
|
||||
```
|
||||
|
||||
**Pułapki:**
|
||||
- Wskazanie partii działa tylko, gdy definicja dokumentu na to pozwala
|
||||
(`WskazaniePartii != Zabroniony`). 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<Obrot>().FirstOrDefault();
|
||||
PozycjaDokHandlowego dostawa = przychod?.Przychod.Dokument?
|
||||
.Pozycje.Cast<PozycjaDokHandlowego>().FirstOrDefault(p => p.Towar == towar);
|
||||
|
||||
var poz = new PozycjaDokHandlowego(dok);
|
||||
session.AddRow(poz);
|
||||
poz.Towar = towar;
|
||||
poz.Ilosc = new Quantity(ilosc, poz.Ilosc.Symbol);
|
||||
poz.Dostawa = dostawa; // każda pozycja wskazuje INNĄ partię
|
||||
}
|
||||
t.Commit();
|
||||
}
|
||||
session.Save();
|
||||
```
|
||||
|
||||
**Pułapki:**
|
||||
- Każda wskazana partia = **osobna pozycja** rozchodu. Nie da się jedną pozycją wskazać
|
||||
dwóch różnych partii — `poz.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<GrupaDostaw>()
|
||||
.FirstOrDefault(g => Equals(g.Features["NumerSerii"], "LOT-2026-001"));
|
||||
```
|
||||
|
||||
**Pułapki:**
|
||||
- Cecha musi być **wcześniej zdefiniowana** (`FeatureSetDefinition`) i — by przenosiła się
|
||||
na partię — odpowiednio skonfigurowana w module magazynowym. Odwołanie do niezdefiniowanej
|
||||
cechy rzuca wyjątek.
|
||||
- Partia powstaje dopiero **po `Session.Save()`** przyjęcia — przed 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.
|
||||
|
||||
---
|
||||
|
||||
@@ -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<DokumentHandlowy>(guid) w testach
|
||||
|
||||
// --- Odczyt nietypowany (object; null gdy brak wartości) ---
|
||||
object centrum = dok["CentrumKosztow"];
|
||||
if (centrum == null) { /* cecha bez wartości na tym dokumencie */ }
|
||||
|
||||
// --- Odczyt typowany przez Features ---
|
||||
string opis = dok.Features.GetString("OpisDodatkowy");
|
||||
Date dostawa = dok.Features.GetDate("DataDostawy");
|
||||
bool pilne = dok.Features.GetBool("Pilne");
|
||||
|
||||
// pozycja:
|
||||
PozycjaDokHandlowego poz = dok.Pozycje.Cast<PozycjaDokHandlowego>().First();
|
||||
string nrSerii = poz.Features.GetString("NrSerii");
|
||||
|
||||
// --- Zapis cech: wymaga transakcji edycyjnej (jak każda modyfikacja Row) ---
|
||||
using (var t = session.Logout(editMode: true))
|
||||
{
|
||||
dok["OpisDodatkowy"] = "Pilna realizacja"; // String
|
||||
dok["Pilne"] = true; // Bool
|
||||
dok["DataDostawy"] = Date.Today.AddDays(3); // Date
|
||||
poz["NrSerii"] = "S-2026-001"; // String na pozycji
|
||||
t.Commit(); // CommitUI() w workerze/extenderze
|
||||
}
|
||||
session.Save();
|
||||
|
||||
// Istnienie / usunięcie wartości:
|
||||
bool ma = dok.Features.Exists("OpisDodatkowy");
|
||||
using (var t = session.Logout(editMode: true))
|
||||
{
|
||||
dok.Features.Remove("OpisDodatkowy");
|
||||
t.Commit();
|
||||
}
|
||||
session.Save();
|
||||
```
|
||||
|
||||
**Pułapki:**
|
||||
- Cecha musi mieć **wcześniej utworzoną definicję** (`FeatureDefinition`) zarejestrowaną dla
|
||||
właściwej tabeli (`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.
|
||||
|
||||
---
|
||||
|
||||
@@ -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<SumaVAT>`) | po jednej pozycji na stawkę |
|
||||
| Kwoty w walucie systemowej | `suma.Suma` (`BruttoNetto`) | `Netto`/`VAT`/`Brutto` jako `decimal` |
|
||||
| Kwoty w walucie dokumentu | `suma.SumaCy` (`BruttoNettoCy`) | `NettoCy`/`VATCy`/`BruttoCy` jako `Currency` |
|
||||
| Procent / opis stawki | `suma.Stawka`, `suma.DefinicjaStawki` | `StawkaVat.Procent: Percent` |
|
||||
| Sumy z dokumentów nadrzędnych | `dok.NadrzędneSumyVAT` (`IList`) | scalone stawki nadrzędnych |
|
||||
|
||||
**Pola i typy:** `dok.SumyVAT: SubTable<SumaVAT>`. `SumaVAT` udostępnia: `DefinicjaStawki:
|
||||
DefinicjaStawkiVat`, `Stawka: StawkaVat` (`Stawka.Procent: Percent`), `Suma: BruttoNetto`
|
||||
(`Netto`, `VAT`, `Brutto` — `decimal`), `SumaCy: BruttoNettoCy` (`NettoCy`, `VATCy`, `BruttoCy` —
|
||||
`Currency`), `Dokument: DokumentHandlowy`.
|
||||
|
||||
**Snippet:**
|
||||
|
||||
```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<SumaVAT>` — kolekcja serwerowa; iteruj po niej, nie materializuj do listy,
|
||||
jeśli wystarczy przebieg jednorazowy. Tabela VAT jest mała (kilka stawek), więc `.Sum(...)` jest
|
||||
akceptowalne.
|
||||
- Rozróżniaj `Suma` (`BruttoNetto`, `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`.
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
@@ -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<IRelacjeService>();` (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<IRelacjeService>();
|
||||
|
||||
DokumentHandlowy korekta;
|
||||
using (var t = session.Logout(editMode: true)) {
|
||||
korekta = rel.NowaKorekta(new[] { faktura }, "KWN")[0]; // symbol definicji korekty
|
||||
|
||||
// 3. Korekta ilościowa: zmiana ilości na pozycji korekty
|
||||
// (pozycje korekty są wstępnie zainicjowane wartościami oryginału)
|
||||
var poz = korekta.Pozycje.First();
|
||||
poz.Ilosc = new Quantity(8, poz.Ilosc.Symbol); // było 10 -> korygujemy do 8
|
||||
|
||||
// 4. Korekta ceny / rabatu — alternatywnie:
|
||||
// poz.Cena = new DoubleCy(4.5m, poz.Cena.Symbol);
|
||||
// poz.Rabat = new Percent(0.15);
|
||||
|
||||
t.Commit();
|
||||
}
|
||||
session.Save();
|
||||
|
||||
// Odczyt powiązania:
|
||||
DokumentHandlowy oryginal = korekta.DokumentKorygowany;
|
||||
```
|
||||
|
||||
**Pułapki:**
|
||||
- `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<IRelacjeService>();
|
||||
|
||||
DokumentHandlowy korektaPrzyjecia;
|
||||
using (var t = session.Logout(editMode: true)) {
|
||||
// przyjecie = zatwierdzony dokument PZ/PW
|
||||
korektaPrzyjecia = rel.NowaKorekta(new[] { przyjecie })[0];
|
||||
|
||||
var poz = korektaPrzyjecia.Pozycje.First();
|
||||
poz.Ilosc = new Quantity(9, poz.Ilosc.Symbol); // przyjęto 10, korygujemy stan do 9
|
||||
|
||||
t.Commit();
|
||||
}
|
||||
session.Save(); // tu księgują się skorygowane obroty/partie
|
||||
```
|
||||
|
||||
**Pułapki:**
|
||||
- **Dedykowany worker `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<DokumentDocelowy>` — wskazanie zaliczek (tor „na pozycje").
|
||||
- `HandlerSet.WybierzZaliczkiWgStawkiVatCallback: Action<DokumentDocelowy>` — 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<SumaVAT>`; `dok.BruttoCy`.
|
||||
|
||||
**Snippet:**
|
||||
|
||||
```csharp
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Soneta.Handel;
|
||||
using Soneta.Handel.RelacjeDokumentow.Api;
|
||||
|
||||
var rel = session.GetRequiredService<IRelacjeService>();
|
||||
|
||||
// zaliczkowa = zatwierdzona faktura zaliczkowa (FZAL).
|
||||
// Rozliczamy ją dokumentem końcowym FV — callback wskazuje, które zaliczki przenieść:
|
||||
DokumentHandlowy[] koncowy;
|
||||
using (var t = session.Logout(editMode: true)) {
|
||||
koncowy = rel.NowyPodrzednyIndywidualny(
|
||||
new[] { zaliczkowa },
|
||||
"FV",
|
||||
handlers: new HandlerSet {
|
||||
WybierzDokumentyZaliczkoweCallback = WybierzZaliczki
|
||||
});
|
||||
t.Commit();
|
||||
}
|
||||
session.Save();
|
||||
|
||||
// koncowy[0].BruttoCy == 0, jeśli zaliczka pokryła całość
|
||||
|
||||
// Callback: zaznacza wszystkie dokumenty zaliczkowe powiązane z dokumentem docelowym.
|
||||
static void WybierzZaliczki(DokumentDocelowy target) {
|
||||
var w = new DokumentHandlowyRealizacjaZaliczkiWorker { Docelowy = target };
|
||||
foreach (var d in target.DokumentyZaliczkowe.Cast<DokumentHandlowy>()) {
|
||||
w.Dokument = d;
|
||||
w.Wybrany = true; // przenosi zaliczkę na dokument końcowy
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Pułapki:**
|
||||
- Bez dostarczenia odpowiedniego callbacka (`WybierzDokumentyZaliczkoweCallback` / `WybierzZaliczkiWgStawkiVatCallback`) domyślne handlery rzucają `NotImplementedException` — **musisz** wskazać tryb przenoszenia zaliczki zgodny z konfiguracją definicji końcowej (`SposobPrzenoszeniaZaliczki`: `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.
|
||||
|
||||
---
|
||||
|
||||
@@ -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<IRelacjeService>()`
|
||||
(`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<IRelacjeService>();
|
||||
DokumentHandlowy[] zamowienia = /* zaznaczone, zatwierdzone ZO */;
|
||||
using (var t = session.Logout(editMode: true))
|
||||
{
|
||||
DokumentHandlowy[] faktury = rel.NowyPodrzednyIndywidualny(zamowienia, "FV");
|
||||
t.Commit();
|
||||
}
|
||||
session.Save();
|
||||
```
|
||||
|
||||
**Pułapki:**
|
||||
- `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).
|
||||
|
||||
---
|
||||
|
||||
@@ -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<PozycjaDokHandlowego>` (`.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.
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
@@ -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<IReportService>();
|
||||
> ```
|
||||
|
||||
**Metody `IReportService` (publiczne):**
|
||||
|
||||
| Metoda | Zwraca | Zastosowanie |
|
||||
|---|---|---|
|
||||
| `Stream GenerateReport(ReportResult rr)` | strumień (PDF/XLSX/PNG/…) | generowanie wydruku binarnego do strumienia/pliku/e-maila |
|
||||
| `string GenerateReportStr(ReportResult rr)` | string | wydruk tekstowy (`HTML`, `TXT`) |
|
||||
| `void PrintReport(ReportResult rr, bool archive = false, string archivePath = "")` | — | wydruk **na drukarkę** (sprzęt), opcjonalna archiwizacja na dysk |
|
||||
| `Type[] GetParameterTypes(string templateFileName, Context context)` | typy parametrów | sprawdzenie, jakich obiektów parametrów wymaga wzorzec |
|
||||
|
||||
**Pola `ReportResult` (publiczne, najważniejsze):**
|
||||
|
||||
| Pole | Typ | Znaczenie |
|
||||
|---|---|---|
|
||||
| `TemplateFileName` | `string` | nazwa wzorca (np. `"Sprzedaz.repx"`, `"Zakup.repx"`). Ustawienie go włącza tryb automatyczny (bez UI). |
|
||||
| `DataType` | `Type` | typ danych branych z kontekstu: `typeof(DokumentHandlowy)` (jeden), `typeof(DokumentHandlowy[])` (zaznaczone), `typeof(DokHandlowe)` (cały widok). |
|
||||
| `Context` | `Context` | kontekst z rekordem(-ami) i parametrami wydruku (`Context.Set(...)`). |
|
||||
| `OutputFormat` | `ReportFormats` | `PDF`, `XLSX`, `XLS`, `CSV`, `DOCX`, `TXT`, `HTML`, `MHT`, `PNG`. Domyślnie `HTML`. |
|
||||
| `Target` | `ReportTargets` | cel: `File`, `Printer`, `PrinterService`, `Preview`, `Attachment`, `Email`, `ShareDocument`, `OpenApplication`. Domyślnie `File`. |
|
||||
| `AskForParameters` | `bool` | `false` = brak okien z pytaniem o parametry (tryb wsadowy). |
|
||||
| `PrinterName` | `string` | nazwa drukarki dla `Target = Printer`. |
|
||||
| `Encrypt` | `string` | hasło szyfrujące PDF. |
|
||||
| `Sign`, `VisibleSignature` | `bool` | podpis certyfikatem (tylko tryb interaktywny okienkowy). |
|
||||
| `OutputHandler` | `Func<Stream,object>` | własna obsługa gotowego strumienia (tryb wzorca; **nieobsługiwane przez `IReportService`** — patrz 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<IReportService>();
|
||||
|
||||
// 1. Kontekst: pojedynczy dokument + jego elementy + parametry wzorca.
|
||||
var context = new Context(session);
|
||||
context.Set(dok);
|
||||
context.Set(dok.Definicja);
|
||||
context.Set(dok.Kontrahent);
|
||||
context.Set(new DokumentHandlowy[] { dok }); // wymagane przez niektóre wzorce
|
||||
context.Set(new ParametryWydrukuDokumentu(context) { Duplikat = false });
|
||||
|
||||
// 2. Opis wydruku — tryb automatyczny (TemplateFileName) → bez UI.
|
||||
var rr = new ReportResult {
|
||||
TemplateFileName = "Sprzedaz.repx", // "Zakup.repx" dla faktury zakupu
|
||||
DataType = typeof(DokumentHandlowy), // wydruk dla pojedynczego dokumentu
|
||||
Context = context,
|
||||
OutputFormat = ReportFormats.PDF,
|
||||
AskForParameters = false // tryb wsadowy — nie pytaj o parametry
|
||||
};
|
||||
|
||||
// 3. Generowanie do strumienia i zapis do pliku.
|
||||
using (Stream pdf = raporty.GenerateReport(rr))
|
||||
using (var plik = new FileStream(@"C:\Temp\FV.pdf", FileMode.Create, FileAccess.Write))
|
||||
pdf.CopyTo(plik);
|
||||
```
|
||||
|
||||
**Pułapki:**
|
||||
- `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 `"<!DOCTYPE html"`.
|
||||
- **Druk na fizyczną drukarkę** (`PrintReport`, `Target = Printer`) wymaga sprzętu i
|
||||
sterownika — **nie da się tego przetestować jednostkowo**. W testach i integracjach
|
||||
używaj ścieżki `GenerateReport` → strumień/PDF.
|
||||
|
||||
---
|
||||
|
||||
### HANDEL-W63 — Wydruk dokumentu magazynowego (PZ/WZ/MM)
|
||||
|
||||
**Cel:** wydrukować dokument magazynowy (PZ, WZ, MM, RW, PW) — identyczny mechanizm jak
|
||||
dla faktury, różni się tylko wzorcem dobranym do rodzaju dokumentu (wg jego definicji).
|
||||
|
||||
**Warianty:**
|
||||
|
||||
| Wariant | Wzorzec / `DataType` |
|
||||
|---|---|
|
||||
| Przyjęcie / wydanie magazynowe | wzorzec magazynowy (`*.repx`), `DataType = typeof(DokumentHandlowy)` |
|
||||
| Przesunięcie MM | wzorzec MM |
|
||||
| Wydruk wg definicji dokumentu | wzorzec domyślny przypisany do `dok.Definicja` |
|
||||
|
||||
**Pola i typy:** jak w HANDEL-W62 — `IReportService.GenerateReport`, `ReportResult.TemplateFileName`,
|
||||
`DokumentHandlowy.Definicja` (decyduje o domyślnym wzorcu).
|
||||
|
||||
**Snippet:**
|
||||
|
||||
```csharp
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Soneta.Business.UI;
|
||||
using Soneta.Handel;
|
||||
|
||||
// 'wz' — zatwierdzony dokument WZ (rozchód magazynowy).
|
||||
var raporty = session.GetRequiredService<IReportService>();
|
||||
|
||||
var context = new Context(session);
|
||||
context.Set(wz);
|
||||
context.Set(wz.Definicja);
|
||||
context.Set(wz.Magazyn);
|
||||
context.Set(new DokumentHandlowy[] { wz });
|
||||
context.Set(new ParametryWydrukuDokumentu(context) { Duplikat = false });
|
||||
|
||||
var rr = new ReportResult {
|
||||
TemplateFileName = "WydanieZewnetrzne.repx", // wzorzec właściwy dla danego rodzaju dokumentu
|
||||
DataType = typeof(DokumentHandlowy),
|
||||
Context = context,
|
||||
OutputFormat = ReportFormats.PDF,
|
||||
AskForParameters = false
|
||||
};
|
||||
|
||||
using (Stream pdf = raporty.GenerateReport(rr)) {
|
||||
// pdf → plik / e-mail / archiwum
|
||||
}
|
||||
```
|
||||
|
||||
**Pułapki:**
|
||||
- Dokument magazynowy i faktura to ten sam typ `DokumentHandlowy` — różni je **definicja**
|
||||
(`dok.Definicja`) i przypisany wzorzec. 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<IReportService>();
|
||||
|
||||
var dzien = Date.Today;
|
||||
var context = new Context(session);
|
||||
context.Set(new FromTo(dzien, dzien)); // parametr okresu wzorca zestawienia
|
||||
|
||||
var rr = new ReportResult {
|
||||
TemplateFileName = "ZestawienieSprzedazy.repx", // wzorzec rejestru/zestawienia
|
||||
DataType = typeof(Soneta.Handel.DokHandlowe), // wydruk dla zbioru dokumentów z widoku
|
||||
Context = context,
|
||||
OutputFormat = ReportFormats.PDF,
|
||||
AskForParameters = false
|
||||
};
|
||||
|
||||
using (Stream pdf = raporty.GenerateReport(rr)) {
|
||||
// zapis / wysyłka
|
||||
}
|
||||
|
||||
// --- Ścieżka 2: fiskalny raport okresowy (WYMAGA DRUKARKI FISKALNEJ) ---
|
||||
// var fiskal = session.GetRequiredService<Soneta.Fiskal.IFiscalPrinterAPI>();
|
||||
// var pars = new Soneta.Fiskal.RaportOkresowyParams(context) {
|
||||
// RaportZaOkres = new FromTo(new Date(2026, 6, 1), new Date(2026, 6, 30))
|
||||
// };
|
||||
// fiskal.DrukujRaportOkresowy("Posnet Thermal", pars); // druk na sprzęcie
|
||||
```
|
||||
|
||||
**Pułapki:**
|
||||
- Rozróżnij dwie rzeczy o podobnej nazwie: **raport dobowy/okresowy drukarki fiskalnej**
|
||||
(`IFiscalPrinterAPI`, rozliczenie utargu na sprzęcie) vs. **bazodanowe zestawienie/rejestr**
|
||||
za dzień/okres (`IReportService` + wzorzec). Dodatek raportujący zwykle chce ścieżki 2.
|
||||
- `RaportOkresowyParams.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<IReportService>();
|
||||
|
||||
var context = new Context(session);
|
||||
context.Set(zaznaczone); // zbiór rekordów do wydruku
|
||||
|
||||
var rr = new ReportResult {
|
||||
TemplateFileName = "Sprzedaz.repx",
|
||||
DataType = typeof(DokumentHandlowy[]), // wydruk dla ZAZNACZONYCH rekordów
|
||||
Rows = zaznaczone, // jawne wskazanie zbioru (opcjonalne)
|
||||
Context = context,
|
||||
OutputFormat = ReportFormats.PDF,
|
||||
AskForParameters = false
|
||||
};
|
||||
|
||||
using (Stream pdf = raporty.GenerateReport(rr)) {
|
||||
// jeden strumień PDF z wieloma dokumentami
|
||||
}
|
||||
```
|
||||
|
||||
**Pułapki:**
|
||||
- Kluczowa różnica vs 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<Stream, object>` (tylko rezultat operacji UI).
|
||||
|
||||
**Snippet:**
|
||||
|
||||
```csharp
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Soneta.Business.UI;
|
||||
using Soneta.Handel;
|
||||
|
||||
var raporty = session.GetRequiredService<IReportService>();
|
||||
|
||||
var context = new Context(session);
|
||||
context.Set(dok);
|
||||
context.Set(new DokumentHandlowy[] { dok });
|
||||
context.Set(new ParametryWydrukuDokumentu(context) { Duplikat = false });
|
||||
|
||||
var rr = new ReportResult {
|
||||
TemplateFileName = "Sprzedaz.repx",
|
||||
DataType = typeof(DokumentHandlowy),
|
||||
Context = context,
|
||||
OutputFormat = ReportFormats.PDF,
|
||||
Encrypt = "tajne-haslo", // (opcjonalnie) PDF chroniony hasłem
|
||||
AskForParameters = false
|
||||
};
|
||||
|
||||
// 1. Do pamięci — np. bajty do wysyłki e-mailem przez własny mechanizm:
|
||||
byte[] pdfBytes;
|
||||
using (Stream src = raporty.GenerateReport(rr))
|
||||
using (var ms = new MemoryStream()) {
|
||||
src.CopyTo(ms);
|
||||
pdfBytes = ms.ToArray();
|
||||
}
|
||||
// pdfBytes → załącznik wiadomości, REST API, repozytorium dokumentów...
|
||||
|
||||
// 2. Wariant: niech mechanizm sam wyśle e-mail (rezultat operacji w workerze UI):
|
||||
// rr.Target = ReportTargets.Email; // wymaga konfiguracji konta pocztowego i szablonu
|
||||
```
|
||||
|
||||
**Pułapki:**
|
||||
- `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 `"<!DOCTYPE html"`.
|
||||
> - **Nietestowalne jednostkowo (wymaga sprzętu):** druk na fizyczną drukarkę
|
||||
> (`PrintReport`, `Target = Printer`) oraz fiskalny raport dobowy/okresowy drukarki
|
||||
> (`IFiscalPrinterAPI.DrukujRaport`/`DrukujRaportOkresowy`, `Fiskalizuj`). Dla nich
|
||||
> testuj tylko poprawne ustawienie `ReportResult`/`RaportOkresowyParams`, bez faktycznego
|
||||
> druku.
|
||||
|
||||
---
|
||||
|
||||
@@ -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<string>`.
|
||||
- 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).
|
||||
|
||||
---
|
||||
|
||||
@@ -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<Soneta.Kasa.Platnosc>`). Pojedyncza płatność to obiekt `Soneta.Kasa.Platnosc` — w praktyce jedna
|
||||
z dwóch klas konkretnych: `Naleznosc` (kierunek `Przychod`, sprzedaż) lub `Zobowiazanie` (kierunek
|
||||
`Rozchod`, zakup). Wymagana referencja do `Soneta.Kasa`.
|
||||
|
||||
> **Pojęcia.** Kwota płatności (`Kwota: Currency`) jest w walucie dokumentu; `KwotaKsiegi: 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<Platnosc>`) |
|
||||
| Kwota i waluta | `p.Kwota: Currency` (`.Value`, `.Symbol`) |
|
||||
| Sposób zapłaty | `p.SposobZaplaty: Soneta.Kasa.SposobZaplaty` (`.Nazwa`, `.Typ`, `.MPP`) |
|
||||
| Termin płatności | `p.Termin: Date`, `p.TerminDni: int` (dni od daty odniesienia) |
|
||||
| Stan rozliczenia | `p.StanRozliczenia`, `p.Rozliczono: bool`, `p.KwotaRozliczona`, `p.DoRozliczenia` |
|
||||
| Kwota nierozliczona po terminie | `p.DoRozliczenia` + warunek `p.Termin < Date.Today` |
|
||||
| Należność / zobowiązanie | `p.Kierunek`, `p.CzyNaleznosc: bool`, `p.CzyZobowiazanie: bool` |
|
||||
|
||||
**Pola i typy:** `Platnosc.Kwota: Soneta.Types.Currency`, `KwotaKsiegi: Currency` (PLN),
|
||||
`SposobZaplaty: Soneta.Kasa.SposobZaplaty`, `Termin: Soneta.Types.Date`, `TerminDni: int`,
|
||||
`StanRozliczenia: Soneta.Kasa.StanRozliczenia` (`Nierozliczony=0`, `Czesciowo=1`, `Calkowicie=2`,
|
||||
`NiePodlega=3`), `Rozliczono: bool`, `KwotaRozliczona: Currency`, `DoRozliczenia: Currency`,
|
||||
`Kierunek: Soneta.Kasa.KierunekPlatnosci`, `EwidencjaSP: Soneta.Kasa.EwidencjaSP`.
|
||||
|
||||
**Snippet:**
|
||||
|
||||
```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`.
|
||||
|
||||
---
|
||||
|
||||
@@ -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<Soneta.Kadry.PracHistoria>`.
|
||||
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<PracHistoria>` — zapisy historyczne (dane kadrowe + `Etat`).
|
||||
- `Nieobecnosci: FromToSubTable<Soneta.Kalend.Nieobecnosc>` — nieobecności.
|
||||
- `Limity: SubTable<Soneta.Kalend.LimitNieobecnosci>` — limity nieobecności (np. urlop).
|
||||
- `Dodatki: SubTable<Soneta.Kadry.Dodatek>` — stałe elementy wynagrodzenia (dodatki).
|
||||
- `Akordy: SubTable<Soneta.Kadry.Akord>` — akordy.
|
||||
- `Umowy: SubTable<Soneta.Kadry.Umowa>` — umowy cywilnoprawne; `UmowyZewnetrzne: SubTable<UmowaZewnetrzna>`.
|
||||
- `Rachunki: SubTable<Soneta.Kasa.RachunekBankowyPodmiotu>` — rachunki bankowe pracownika.
|
||||
- `DniPracy: DateSubTable<Soneta.Kalend.DzienPracy>` — plan/realizacja czasu pracy (dzień po dniu).
|
||||
- `DniRCP: DateSubTable<Soneta.Kalend.DzienRCP>` — zarejestrowany czas pracy (RCP).
|
||||
- `DniPlanu: DateSubTable` — plan pracy (harmonogram).
|
||||
- `Kalendarze: SubTable<Soneta.Kalend.KalendarzBase>` — 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.
|
||||
|
||||
---
|
||||
|
||||
<!-- SEKCJE FUNKCJONALNOŚCI — uzupełniane przez subagentów dokumentujących.
|
||||
Kolejność wg listy zadań (A–K). Każda pozycja gwiazdkowana (★) ma własny wzorzec. -->
|
||||
|
||||
|
||||
## 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* |
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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<Soneta.Core.PodzielnikKosztow>` → każdy podzielnik ma **historię**
|
||||
`PodzielnikKosztow.Historia: HistorySubTable<HistoriaPodzielnika>` → a zapis historii ma kolekcję
|
||||
`HistoriaPodzielnika.Elementy: SubTable<Soneta.Core.ElementPodzielnika>` (poszczególne udziały).
|
||||
|
||||
> Uwaga: `Pracownik.ElementyPodzielnika: SubTable<ElementPodzielnika>` 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<ElementPodzielnika>`.
|
||||
**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.
|
||||
|
||||
@@ -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<Soneta.Kadry.DodHistoria>`, parametry
|
||||
„od–do" siedzą w zapisach `DodHistoria`). Dodatek jest childem pracownika i pojawia się w
|
||||
`pracownik.Dodatki: SubTable<Soneta.Kadry.Dodatek>`.
|
||||
|
||||
**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<DodHistoria>`, `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<DefinicjaElementu>()
|
||||
.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<Soneta.Kadry.AkordHistoria>`; parametry „od–do" w zapisach
|
||||
`AkordHistoria`, dostęp do bieżącego przez `Akord.Last`). Akord jest childem pracownika:
|
||||
`pracownik.Akordy: SubTable<Soneta.Kadry.Akord>`.
|
||||
|
||||
**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<Soneta.Kalend.DzienAkorduBase>` | 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<Soneta.Kadry.ProgAkordu>`.
|
||||
|
||||
**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<Soneta.Kadry.ZajęcieKomorniczeHistoria>`; limity i kwoty
|
||||
„od–do" w zapisach historii, bieżący przez `Last`). Child pracownika:
|
||||
`pracownik.ZajęciaKomornicze: SubTable<Soneta.Kadry.ZajęcieKomornicze>`. **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<Soneta.Kadry.KorektaZajęciaKomorniczego>` | 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<DefinicjaElementu>()
|
||||
.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<Soneta.Kadry.Pracownik>` |
|
||||
| `Elementy` | `SubTable<Soneta.Przeszeregowania.ElementPrzeszeregowania>` |
|
||||
| `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<Pracownik>().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<Soneta.Kadry.SwiadczSocjalne>`. **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<Soneta.Kadry.FundPozyczkowy>`. Ctor:
|
||||
`new FundPozyczkowy(pracownik, definicja)`.
|
||||
- `Soneta.Kadry.Pozyczka` (tabela `Pozyczki`) — **pożyczka** udzielona w ramach funduszu; kolekcja
|
||||
`fundusz.Pozyczki: SubTable<Soneta.Kadry.Pozyczka>`. Ctor: `new Pozyczka(fundusz)`.
|
||||
- `Soneta.Kadry.RataPozyczki` (tabela `RatyPozyczek`) — **rata** harmonogramu; `pozyczka.Raty:
|
||||
SubTable<Soneta.Kadry.RataPozyczki>`. 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<Soneta.Kadry.RataPozyczki>` | 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;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -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<Soneta.Kalend.Nieobecnosc>`**
|
||||
(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<Soneta.Kalend.LimitNieobecnosci>`**
|
||||
(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<Pracownik>` (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<LimitNieobecnosci>()
|
||||
.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<Pracownik>` /
|
||||
`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<Soneta.Place.PodstawaNieobecnosci>`**
|
||||
(jest też `PodstawyNieobecnościOkresowe: SubTable<PodstawaNieobecnosciOkresowa>`).
|
||||
- **`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<Soneta.Kadry.WniosekUrlopowy>`**
|
||||
(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<Soneta.Kalend.PlanowanaNieobecność>`**;
|
||||
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<DefinicjaNieobecnosci>().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<WniosekUrlopowy>()
|
||||
.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<Soneta.Kadry.LokalizacjaPracyZdalnej>`**
|
||||
(tabela `LokPracZdalnej`).
|
||||
- Wnioski o pracę zdalną: **`pracownik.WnioskiPracyZdalnej: SubTable<Soneta.Kalend.WniosekPracyZdalnej>`**
|
||||
(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.
|
||||
|
||||
@@ -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<Soneta.Kalend.DzienPracy>` — **ewidencja** (realizacja) czasu pracy.
|
||||
> - `pracownik.DniRCP : DateSubTable<Soneta.Kalend.DzienRCP>` — **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<Pracownik[]>()
|
||||
};
|
||||
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<Pracownik[]>(),
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
@@ -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<Soneta.Kalend.DzienRCP>` (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<Soneta.Kalend.DzienPracy>` | indeksator `[Date]` (get); element ctorem |
|
||||
| Dzień ewidencji | `pracownik.DniPracy[data]` | `Soneta.Kalend.DzienPracy` | `null` przy braku |
|
||||
| RCP zweryfikowane (kolekcja) | `pracownik.DniRCP` | `DateSubTable<Soneta.Kalend.DzienRCP>` | 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<Soneta.Kalend.WejscieWyjscie>` | 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<DzienPracy>` / `<DzienRCP>`) — 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<WejscieWyjscie>` | 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<Soneta.Kalend.WejscieWyjscie>` | 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<Soneta.Kalend.StrefaRCP>` | | 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<DzienRCP>`) — 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<Soneta.Kalend.StrefaPracy>` | 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<Soneta.Kalend.RozliczenieCzasuPracy>` | dokumenty rozliczenia czasu (podzielniki) |
|
||||
| Elementy rozliczenia | `pracownik.ElementyRozliczeniaCzasuPracy` | `SubTable<Soneta.Kalend.ElementRozliczeniaCzasuPracy>` | 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).
|
||||
|
||||
@@ -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["<kod>"]` (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<UmowaHistoria>`** (`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<Soneta.Kasa.RachunekBankowyPodmiotu>` 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<Soneta.Place.RozliczenieWynagrodzenia>` — rozliczenia
|
||||
> wynagrodzenia z umowy,
|
||||
> - `Umowa.Elementy: SubTable<Soneta.Place.WypElement>` — 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<Umowa>()
|
||||
.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<WyplataUmowa>().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<Umowa>().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.
|
||||
|
||||
@@ -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<WypElement>`.
|
||||
>
|
||||
> **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<BłądNaliczaniaWynagrodzenia>` | 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<Umowa>().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<Wyplata>` | wszystkie wypłaty pracownika (klucz `WgPracownik`) |
|
||||
| `session.GetPlace().Wyplaty.WgPracownik[pracownik]` | `SubTable<Wyplata>` | równoważnie z modułu |
|
||||
| `session.GetPlace().Wyplaty.WgData[date]` | `SubTable<Wyplata>` | wypłaty z datą `date` |
|
||||
| `listaPlac.Wyplaty` | `SubTable<Wyplata>` | 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<WypElement>` | 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<WypElement>` (`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<Wyplata>().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<WypElement>` (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<DefinicjaElementu>()
|
||||
.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<StornoElementu>` | 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<WypElement>().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`.
|
||||
|
||||
@@ -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<Wyplata>`. 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<Wyplata>` | 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<BłądNaliczaniaWynagrodzenia>`,
|
||||
`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<IReportService>();
|
||||
|
||||
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<IReportService>()`
|
||||
(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<IReportService>();
|
||||
|
||||
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<Soneta.Kasa.PrzelewBase>` | przelewy pracownika |
|
||||
| `Pracownik.DokumentyPreliminarza` | `SubTable<Soneta.Kasa.PreliminarzDokument>` | dokumenty preliminarza |
|
||||
| `Pracownik.DokumentyRozliczeniowe` | `SubTable<Soneta.Kasa.DokRozliczBase>` | dokumenty rozliczeniowe |
|
||||
| `Pracownik.Rozrachunki` | `SubTable<Soneta.Kasa.RozrachunekIdx>` | rozrachunki |
|
||||
| `Pracownik.Rachunki` | `SubTable<Soneta.Kasa.RachunekBankowyPodmiotu>` | 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<Soneta.Kasa.PrzelewBase>` | 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<RozliczenieSP>` oraz `Wyplata.Dokumenty: SubTable<RozliczenieSP>` —
|
||||
rozliczenia,
|
||||
- `Wyplata.Rozrachunki: SubTable<RozrachunekIdx>` — 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`).
|
||||
|
||||
@@ -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<Soneta.Place.BilansOtwarciaPIT>` | bilans otwarcia PIT (przychody/koszty/składki na start) |
|
||||
| `Pracownik.WynagrodzeniaERP7` | `SubTable<Soneta.Kalend.WynagrodzenieERP7>` | wynagrodzenia do ERP-7 / Z-3 |
|
||||
| `Pracownik.NieobecnosciERP7` | `SubTable<Soneta.Kalend.NieobecnoscERP7>` | 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<Soneta.Place.ElementBilansuOtwarciaPIT>`.
|
||||
|
||||
**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<BilansOtwarciaPIT>):
|
||||
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.
|
||||
|
||||
@@ -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<BadanieLekarskie>` | `Soneta.Kadry.BadanieLekarskie` | `BadaniaLekarskie` |
|
||||
| KADRY-K2 Szkolenia BHP | `SzkoleniaBHP: SubTable<SzkolenieBHP>` | `Soneta.Kadry.SzkolenieBHP` | `SzkoleniaBHP` |
|
||||
| KADRY-K3 Wnioski o szkolenia | `WnioskiOSzkolenia: SubTable<WniosekOSzkolenie>` | `Soneta.HR.WniosekOSzkolenie` | `WnioskiOSzkol` |
|
||||
| KADRY-K3 Ukończone szkolenia | `UkończoneSzkolenia: SubTable<UkończoneSzkolenie>` | `Soneta.HR.UkończoneSzkolenie` | `UkonczSzkolenia` |
|
||||
| KADRY-K3 Uprawnienia | `Uprawnienia: SubTable<UprawnieniePracownika>` | `Soneta.HR.UprawnieniePracownika` | `UprawnieniaPrac` |
|
||||
| KADRY-K4 Nagrody i kary | `NagrodyKary: SubTable<NagrodaKara>` | `Nagroda` / `Kara` (`NagrodaKara` abstr.) | `NagrodyKary` |
|
||||
| KADRY-K4 Oświadczenia | `Oświadczenia: SubTable<OświadczeniePracownika>` | `Soneta.Kadry.OświadczeniePracownika` | `OswiadczeniaPrac` |
|
||||
| KADRY-K5 Wypadki przy pracy | `Wypadki: SubTable<Wypadek>` | `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<BadanieLekarskie>`). 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<NagrodaKara>`. 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<DefinicjaNagrodyKary>().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<Soneta.Core.GIODOOświadczenie>` | oświadczenia / zgody RODO |
|
||||
| `GIODOUprawnienia` | `SubTable<Soneta.Core.GIODOUprawnienie>` | uprawnienia do przetwarzania danych |
|
||||
| `GIODOUdostępnienia` | `SubTable<Soneta.Core.GIODOWymianaDanych>` | pozyskanie / udostępnienie / powierzenie danych |
|
||||
| `PotwierdzeniaGIODO` | `SubTable<Soneta.Core.GIODOZgodny>` | 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<Soneta.Core.PowiązanieStrukturyOrganizacyjnej>` | 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<Pracownik> …` | przełożeni |
|
||||
| `Podwładni(...)` | `IEnumerable<Pracownik> 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<ElementStrukturyOrganizacyjnej>().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<Soneta.HR.OcenaPracownika>` | arkusze ocen okresowych (root) |
|
||||
| `ElementyOceny` | `SubTable<Soneta.HR.ElementOcenyPracownika>` | pojedyncze elementy/pozycje arkuszy ocen |
|
||||
| `CeleOkresowe` | `SubTable<Soneta.HR2.CelOkresowyPracownika>` | cele okresowe |
|
||||
| `KartyKompetencji` | `SubTable<Soneta.HR2.KartaKompetencjiPracownika>` | karty kompetencji |
|
||||
| `KartyOpisuStanowiska` | `SubTable<Soneta.HR2.KartaOpisuStanowiskaBase>` | karty opisu stanowiska |
|
||||
| `KartyRealizacjiCelu` | `SubTable<Soneta.HR2.KartaRealizacjiCelu>` | karty realizacji celów |
|
||||
| `Oceniani` / `Oceniający` | `SubTable<Soneta.Oceny.OcenaOceniany/OcenaOceniający>` | 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<ElementOcenyPracownika>` | 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<Soneta.HR2.RealizacjaCelu>` | 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<ElementKartyOpisuStanowiska>` | elementy opisu |
|
||||
| `Kompetencje` | `SubTable<KompetencjaKartyOpisuStanowiska>` | 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<ElementOcenyPracownika>` 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<Soneta.HR2.RekrutacjaAplikacja>` | aplikacje kandydata |
|
||||
| `Wakaty` | `SubTable<Soneta.HR2.RekrutacjaWakat>` | wakaty |
|
||||
| `Rekrutacje` / `Kandydatury` | `SubTable<Soneta.HR.Rekrutacja>` | rekrutacje (kandydatury) |
|
||||
| `EtapyRekrutacji` | `SubTable<Soneta.HR.EtapRekrutacji>` | 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<ElementOcenyPracownika>` | 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<Soneta.HR.DefinicjaStanowiska>().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`.
|
||||
@@ -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<KontaktOsoba>`), `Kontakty` (`SubTable<DaneKontaktowe>`),
|
||||
`AdresyWWW` (`SubTable<AdresWWW>`), `Kategorie` (`SubTable<KategoriaKth>`),
|
||||
`Branze` (`SubTable<BranzaKth>`), `Opiekunowie` (`SubTable<Opiekun>`),
|
||||
`Rachunki` (`SubTable<Soneta.Kasa.RachunekBankowyPodmiotu>`),
|
||||
`Rozrachunki` (`SubTable<Soneta.Kasa.RozrachunekIdx>`), `Podrzedni` (`SubTable<RelacjaPodmiotu>`),
|
||||
`StatusyVAT` (`SubTable<StatusVAT>`), `KodyKreskowe` (`SubTable<KodKreskowy>`),
|
||||
`GIODOOświadczenia` (`SubTable<GIODOOświadczenie>`), `GIODOUdostępnienia` (`SubTable<GIODOWymianaDanych>`),
|
||||
`PotwierdzeniaGIODO` (`SubTable<GIODOZgodny>`).
|
||||
- **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<DaneKontaktowe>` (`Rodzaj`, `Kontakt`, `Domyslny`) |
|
||||
| Adresy WWW | `AdresyWWW: SubTable<AdresWWW>` (`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<KontaktOsoba>` (`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<RachunekBankowyPodmiotu>`, `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<RachunekBankowyPodmiotu>` |
|
||||
|
||||
**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<Soneta.Kasa.RozrachunekIdx>` |
|
||||
| Płatności / zapłaty | `Platnosci: SubTable<Platnosc>`, `Zaplaty: SubTable<Zaplata>` |
|
||||
| Dokumenty rozliczeniowe | `DokumentyRozliczeniowe: SubTable<DokRozliczBase>` |
|
||||
| Przelewy | `Przelewy: SubTable<PrzelewBase>` |
|
||||
|
||||
**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<DokEwidencji>` |
|
||||
| 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<KategoriaKth>` (poj. lub worker `KontrahenciPrzypiszKategorieWorker`) |
|
||||
| Branże | `Branze: SubTable<BranzaKth>` |
|
||||
| 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<KategoriaKth>`, `Branze: SubTable<BranzaKth>`.
|
||||
`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<Opiekun>`, worker `UstawOpiekunaGlownegoWorker` |
|
||||
| Sprawdzenie opieki na dzień | metody `JestOpiekunemNaDzis(...)`, `OpiekunowieWOkresie(...)` |
|
||||
| Podmiot nadrzędny / podrzędny | workery `NowyPodmiotNadrzednyWorker`, `NowyPodmiotPodrzednyWorker` |
|
||||
| Relacje podmiotów | `Podrzedni: SubTable<RelacjaPodmiotu>`, `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<StatusVAT>` | — |
|
||||
|
||||
**Pola i typy (odczyt wyniku):** `AktualnyStatusVAT`, `AktualnyStatusVATMF`, `AktualnyStatusVATVies`
|
||||
(typ `Soneta.CRM.StatusNumeruVAT`, kalkulowane), `AktStatusVATData/DataMF/DataVIES: DateTime?`,
|
||||
`StatusyVAT: SubTable<StatusVAT>`.
|
||||
|
||||
**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<GIODOOświadczenie>` |
|
||||
| Pozyskanie danych | `KontrahentDodajPozyskanieDanychWorker` | `GIODOUdostępnienia` |
|
||||
| Udostępnienie danych | `KontrahentDodajUdostepnienieDanychWorker` | `GIODOUdostępnienia` |
|
||||
| Powierzenie danych | `KontrahentDodajPowierzenieDanychWorker` | `GIODOUdostępnienia` |
|
||||
| Potwierdzenia zgodności | — | `PotwierdzeniaGIODO: SubTable<GIODOZgodny>` |
|
||||
|
||||
**Pola i typy:** `GIODOOświadczenia: SubTable<GIODOOświadczenie>`,
|
||||
`GIODOUdostępnienia: SubTable<GIODOWymianaDanych>`, `PotwierdzeniaGIODO: SubTable<GIODOZgodny>`,
|
||||
`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.
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user