555 lines
26 KiB
Markdown
555 lines
26 KiB
Markdown
# 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.
|
|
|
|
---
|
|
|