SKILL: Uporządkowanie skills domenowych - podział na mniejsze pliki i wspólna numeracja

This commit is contained in:
Marcin Wojas
2026-06-07 08:31:37 +02:00
parent fb2f2695a3
commit 5605ad2915
42 changed files with 13500 additions and 13526 deletions
@@ -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`**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.
---
---