Files

506 lines
28 KiB
Markdown

# 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`**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).
---