diff --git a/.gitignore b/.gitignore
index 0cfbe71..6ed4687 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
**-workspace/**
+obj/
.DS_Store
.idea
diff --git a/Soneta.Skills.Test/CRM/Kontrahenci/AdresKontrahentaTest.cs b/Soneta.Skills.Test/CRM/Kontrahenci/AdresKontrahentaTest.cs
new file mode 100644
index 0000000..c476e4b
--- /dev/null
+++ b/Soneta.Skills.Test/CRM/Kontrahenci/AdresKontrahentaTest.cs
@@ -0,0 +1,64 @@
+using AwesomeAssertions;
+using NUnit.Framework;
+
+namespace Soneta.Skills.Test.CRM.Kontrahenci;
+
+///
+/// W6 — Adres kontrahenta.
+/// Test pokazuje, że Adres to property zwracająca obiekt złożony (nie da się przypisać
+/// całego adresu) — modyfikujemy jego pola. Uwaga na typ KodPocztowy = int
+/// (do formatu „00-000" służy KodPocztowyS).
+///
+[TestFixture]
+public class AdresKontrahentaTest : KontrahentTestBase
+{
+ [Test]
+ [Description("Ustawienie pól adresu głównego (ulica, kod pocztowy, miejscowość) jest zapisywane.")]
+ public void UstawienieAdresuGlownego_JestZapisywane()
+ {
+ var kod = UnikalnyKod();
+ UtworzKontrahenta(kod, "Firma Z Adresem");
+ SaveDispose();
+
+ var k = Crm.Kontrahenci.WgKodu[kod];
+ InTransaction(() =>
+ {
+ var a = k.Adres; // edytujemy pola obiektu adresu
+ 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.Kraj = "Polska";
+ });
+ SaveDispose();
+
+ var a2 = Crm.Kontrahenci.WgKodu[kod].Adres;
+ a2.Ulica.Should().Be("Wadowicka");
+ a2.NrDomu.Should().Be("8A");
+ a2.Miejscowosc.Should().Be("Kraków");
+ a2.KodPocztowy.Should().Be(30415);
+ }
+
+ [Test]
+ [Description("Adres do korespondencji jest odrębnym obiektem od adresu głównego.")]
+ public void AdresDoKorespondencji_JestOdrebnyOdGlownego()
+ {
+ var kod = UnikalnyKod();
+ UtworzKontrahenta(kod, "Firma Z Korespondencja");
+ SaveDispose();
+
+ var k = Crm.Kontrahenci.WgKodu[kod];
+ InTransaction(() =>
+ {
+ k.Adres.Miejscowosc = "Kraków";
+ k.AdresDoKorespondencji.Miejscowosc = "Warszawa";
+ });
+ SaveDispose();
+
+ var zapisany = Crm.Kontrahenci.WgKodu[kod];
+ zapisany.Adres.Miejscowosc.Should().Be("Kraków");
+ zapisany.AdresDoKorespondencji.Miejscowosc.Should().Be("Warszawa");
+ }
+}
diff --git a/Soneta.Skills.Test/CRM/Kontrahenci/DaneKontaktoweTest.cs b/Soneta.Skills.Test/CRM/Kontrahenci/DaneKontaktoweTest.cs
new file mode 100644
index 0000000..a6b3cb6
--- /dev/null
+++ b/Soneta.Skills.Test/CRM/Kontrahenci/DaneKontaktoweTest.cs
@@ -0,0 +1,68 @@
+using System.Linq;
+using AwesomeAssertions;
+using NUnit.Framework;
+using Soneta.Core;
+
+namespace Soneta.Skills.Test.CRM.Kontrahenci;
+
+///
+/// W7 — Dane kontaktowe i adresy WWW.
+/// Testy pokazują dodanie kanału e-mail do kolekcji Kontakty (typ rodzaju pobierany ze
+/// słownika RodzajeKontaktow) oraz dodanie adresu WWW (konstruktor z hostem
+/// new AdresWWW(kontrahent), pole URL nazywa się Adres).
+///
+[TestFixture]
+public class DaneKontaktoweTest : KontrahentTestBase
+{
+ [Test]
+ [Description("Dodanie domyślnego kontaktu e-mail pojawia się w kolekcji Kontakty kontrahenta.")]
+ public void DodanieEmaila_PojawiaSieWKolekcjiKontakty()
+ {
+ var kod = UnikalnyKod();
+ UtworzKontrahenta(kod, "Firma Z Mailem");
+ SaveDispose();
+
+ var email = "kontakt@firma-" + kod + ".pl";
+ var k = Crm.Kontrahenci.WgKodu[kod];
+ var rodzajEmail = Session.GetCore().RodzajeKontaktow[RodzajeKontaktow.AdresEmail];
+
+ InUITransaction(() =>
+ {
+ var dk = Add(new DaneKontaktowe { Host = k });
+ dk.Rodzaj = rodzajEmail;
+ dk.Kontakt = email;
+ dk.Domyslny = true;
+ });
+ SaveDispose();
+
+ var zapisany = Crm.Kontrahenci.WgKodu[kod];
+ zapisany.Kontakty.Cast()
+ .Any(d => d.Kontakt == email)
+ .Should().BeTrue();
+ }
+
+ [Test]
+ [Description("Dodanie adresu WWW (new AdresWWW(host)) pojawia się w kolekcji AdresyWWW.")]
+ public void DodanieAdresuWWW_PojawiaSieWKolekcji()
+ {
+ var kod = UnikalnyKod();
+ UtworzKontrahenta(kod, "Firma Z WWW");
+ SaveDispose();
+
+ var url = "https://www.firma-" + kod + ".pl";
+ var k = Crm.Kontrahenci.WgKodu[kod];
+
+ InUITransaction(() =>
+ {
+ var www = Add(new AdresWWW(k)); // ctor przyjmuje IAdresyWWWHost
+ www.Adres = url; // pole URL nazywa się Adres
+ www.Domyslny = true;
+ });
+ SaveDispose();
+
+ var zapisany = Crm.Kontrahenci.WgKodu[kod];
+ zapisany.AdresyWWW.Cast()
+ .Any(w => w.Adres == url)
+ .Should().BeTrue();
+ }
+}
\ No newline at end of file
diff --git a/Soneta.Skills.Test/CRM/Kontrahenci/KlasyfikacjaIPowiazaniaTest.cs b/Soneta.Skills.Test/CRM/Kontrahenci/KlasyfikacjaIPowiazaniaTest.cs
new file mode 100644
index 0000000..b29e041
--- /dev/null
+++ b/Soneta.Skills.Test/CRM/Kontrahenci/KlasyfikacjaIPowiazaniaTest.cs
@@ -0,0 +1,47 @@
+using System.Linq;
+using AwesomeAssertions;
+using NUnit.Framework;
+using Soneta.CRM;
+
+namespace Soneta.Skills.Test.CRM.Kontrahenci;
+
+///
+/// W13/W14 — Klasyfikacja i powiązania (odczyt kontraktu publicznego).
+/// Testy dokumentują dostęp do kolekcji klasyfikacyjnych (Kategorie, Branze,
+/// Features) oraz powiązań (Opiekunowie, Podrzedni, PodmiotNadrzedny).
+/// Świeżo utworzony, samodzielny kontrahent ma te kolekcje puste i brak podmiotu nadrzędnego —
+/// co czyni asercje deterministycznymi.
+///
+[TestFixture]
+public class KlasyfikacjaIPowiazaniaTest : KontrahentTestBase
+{
+ [Test]
+ [Description("Świeży kontrahent ma dostępne i puste kolekcje klasyfikacyjne; Features != null.")]
+ public void NowyKontrahent_KolekcjeKlasyfikacjiSaPusteAleDostepne()
+ {
+ var kod = UnikalnyKod();
+ UtworzKontrahenta(kod, "Firma Klasyfikacja");
+ SaveDispose();
+
+ var k = Crm.Kontrahenci.WgKodu[kod];
+
+ k.Features.Should().NotBeNull(); // cechy definiowalne — dostęp po nazwie
+ k.Kategorie.Cast().Should().BeEmpty();
+ k.Branze.Cast().Should().BeEmpty();
+ }
+
+ [Test]
+ [Description("Świeży kontrahent nie ma opiekunów, podmiotów podrzędnych ani nadrzędnego.")]
+ public void NowyKontrahent_BrakPowiazan()
+ {
+ var kod = UnikalnyKod();
+ UtworzKontrahenta(kod, "Firma Powiazania");
+ SaveDispose();
+
+ var k = Crm.Kontrahenci.WgKodu[kod];
+
+ k.Opiekunowie.Cast().Should().BeEmpty();
+ k.Podrzedni.Cast().Should().BeEmpty();
+ k.PodmiotNadrzedny.Should().BeNull();
+ }
+}
diff --git a/Soneta.Skills.Test/CRM/Kontrahenci/KontrahentTestBase.cs b/Soneta.Skills.Test/CRM/Kontrahenci/KontrahentTestBase.cs
new file mode 100644
index 0000000..7aa0517
--- /dev/null
+++ b/Soneta.Skills.Test/CRM/Kontrahenci/KontrahentTestBase.cs
@@ -0,0 +1,50 @@
+using System;
+using Soneta.Core;
+using Soneta.CRM;
+using Soneta.Test;
+
+namespace Soneta.Skills.Test.CRM.Kontrahenci;
+
+///
+/// Wspólna baza testów kontrahenta. Dziedziczy z , dzięki czemu:
+///
+/// - udostępnia gotową sesję operacyjną (Session) powiązaną z testową bazą Demo,
+/// - automatycznie wycofuje (rollback) wszystkie zmiany w bazie po zakończeniu testu,
+/// - daje metody pomocnicze InTransaction/SaveDispose do pracy w transakcjach.
+///
+/// Baza dodaje skróty często powtarzane w testach kontrahenta (dostęp do modułu CRM,
+/// generowanie unikalnego kodu, utworzenie minimalnego kontrahenta).
+///
+public abstract class KontrahentTestBase : TestBase
+{
+ /// Moduł CRM bieżącej sesji operacyjnej.
+ protected CRMModule Crm => Session.GetCRM();
+
+ /// Generuje krótki, unikalny kod kontrahenta (na potrzeby testów).
+ protected static string UnikalnyKod() => Guid.NewGuid().ToString("N").Substring(0, 10);
+
+ ///
+ /// Tworzy w bieżącej sesji nowego kontrahenta z minimalnym kompletem danych
+ /// (kod, nazwa, status i rodzaj podmiotu) wewnątrz transakcji edycyjnej.
+ /// Zwrócony obiekt żyje w bieżącej sesji — pozostaje ważny do czasu SaveDispose.
+ ///
+ protected Kontrahent UtworzKontrahenta(
+ string kod,
+ string nazwa = null,
+ StatusPodmiotu status = StatusPodmiotu.PodmiotGospodarczy,
+ RodzajPodmiotu rodzaj = RodzajPodmiotu.Krajowy)
+ {
+ Kontrahent k = null;
+ InTransaction(() =>
+ {
+ // AddRow MUSI poprzedzać ustawianie pól — obiekt najpierw trafia do tabeli.
+ k = new Kontrahent();
+ Session.AddRow(k);
+ k.Kod = kod;
+ k.Nazwa = nazwa ?? kod;
+ k.StatusPodmiotu = status;
+ k.RodzajPodmiotu = rodzaj;
+ });
+ return k;
+ }
+}
diff --git a/Soneta.Skills.Test/CRM/Kontrahenci/ModyfikacjaIStatusyTest.cs b/Soneta.Skills.Test/CRM/Kontrahenci/ModyfikacjaIStatusyTest.cs
new file mode 100644
index 0000000..80c353f
--- /dev/null
+++ b/Soneta.Skills.Test/CRM/Kontrahenci/ModyfikacjaIStatusyTest.cs
@@ -0,0 +1,50 @@
+using AwesomeAssertions;
+using NUnit.Framework;
+using Soneta.CRM;
+
+namespace Soneta.Skills.Test.CRM.Kontrahenci;
+
+///
+/// W4 — Modyfikacja danych i statusów kontrahenta.
+/// Testy pokazują zmianę nazwy oraz ustawienie statusów dostępności/handlowych:
+/// Blokada (ukrycie na listach) i BlokadaSprzedazy (zakaz dokumentów rozchodu).
+///
+[TestFixture]
+public class ModyfikacjaIStatusyTest : KontrahentTestBase
+{
+ [Test]
+ [Description("Zmiana nazwy kontrahenta jest trwale zapisywana.")]
+ public void ZmianaNazwy_JestZapisywana()
+ {
+ var kod = UnikalnyKod();
+ UtworzKontrahenta(kod, "Nazwa Pierwotna");
+ SaveDispose();
+
+ var k = Crm.Kontrahenci.WgKodu[kod];
+ InTransaction(() => k.Nazwa = "Nazwa Zmieniona");
+ SaveDispose();
+
+ Crm.Kontrahenci.WgKodu[kod].Nazwa.Should().Be("Nazwa Zmieniona");
+ }
+
+ [Test]
+ [Description("Ustawienie Blokada i BlokadaSprzedazy jest trwale zapisywane.")]
+ public void UstawienieBlokad_JestZapisywane()
+ {
+ var kod = UnikalnyKod();
+ UtworzKontrahenta(kod, "Do Zablokowania");
+ SaveDispose();
+
+ var k = Crm.Kontrahenci.WgKodu[kod];
+ InTransaction(() =>
+ {
+ k.Blokada = true; // ukrycie na listach
+ k.BlokadaSprzedazy = true; // zakaz wystawiania dokumentów rozchodu
+ });
+ SaveDispose();
+
+ var zapisany = Crm.Kontrahenci.WgKodu[kod];
+ zapisany.Blokada.Should().BeTrue();
+ zapisany.BlokadaSprzedazy.Should().BeTrue();
+ }
+}
diff --git a/Soneta.Skills.Test/CRM/Kontrahenci/OsobyKontaktoweTest.cs b/Soneta.Skills.Test/CRM/Kontrahenci/OsobyKontaktoweTest.cs
new file mode 100644
index 0000000..2cd8845
--- /dev/null
+++ b/Soneta.Skills.Test/CRM/Kontrahenci/OsobyKontaktoweTest.cs
@@ -0,0 +1,44 @@
+using System.Linq;
+using AwesomeAssertions;
+using NUnit.Framework;
+using Soneta.CRM;
+
+namespace Soneta.Skills.Test.CRM.Kontrahenci;
+
+///
+/// W8 — Osoby kontaktowe.
+/// Test pokazuje dodanie osoby kontaktowej i powiązanie jej z kontrahentem przez
+/// KontaktOsoba.Kontrahent — osoba pojawia się wtedy w kolekcji Osoby kontrahenta.
+///
+[TestFixture]
+public class OsobyKontaktoweTest : KontrahentTestBase
+{
+ [Test]
+ [Description("Dodana i powiązana osoba kontaktowa pojawia się w kolekcji Osoby kontrahenta.")]
+ public void DodanieOsoby_PojawiaSieWKolekcjiOsoby()
+ {
+ var kod = UnikalnyKod();
+ UtworzKontrahenta(kod, "Firma Z Osoba");
+ SaveDispose();
+
+ var email = "a.nowak@firma-" + kod + ".pl";
+ var k = Crm.Kontrahenci.WgKodu[kod];
+
+ InTransaction(() =>
+ {
+ var os = new KontaktOsoba();
+ Session.AddRow(os);
+ os.Kontrahent = k; // powiązanie osoby z kontrahentem
+ os.Imie = "Anna";
+ os.Nazwisko = "Nowak";
+ os.Stanowisko = "Kierownik zakupów";
+ os.EMAIL = email;
+ });
+ SaveDispose();
+
+ var zapisany = Crm.Kontrahenci.WgKodu[kod];
+ zapisany.Osoby.Cast()
+ .Any(o => o.Nazwisko == "Nowak" && o.Imie == "Anna")
+ .Should().BeTrue();
+ }
+}
\ No newline at end of file
diff --git a/Soneta.Skills.Test/CRM/Kontrahenci/TworzenieKontrahentaTest.cs b/Soneta.Skills.Test/CRM/Kontrahenci/TworzenieKontrahentaTest.cs
new file mode 100644
index 0000000..4a92fd4
--- /dev/null
+++ b/Soneta.Skills.Test/CRM/Kontrahenci/TworzenieKontrahentaTest.cs
@@ -0,0 +1,71 @@
+using AwesomeAssertions;
+using NUnit.Framework;
+using Soneta.Core;
+using Soneta.CRM;
+
+namespace Soneta.Skills.Test.CRM.Kontrahenci;
+
+///
+/// W3 — Tworzenie kontrahenta.
+/// Testy pokazują utworzenie rekordu z minimalnym kompletem danych w transakcji edycyjnej
+/// oraz trwały zapis (SaveDispose) i ponowny odczyt z nowej sesji. Pokrywają warianty:
+/// podmiot gospodarczy krajowy, podmiot unijny oraz osoba fizyczna (finalny).
+///
+[TestFixture]
+public class TworzenieKontrahentaTest : KontrahentTestBase
+{
+ [Test]
+ [Description("Tworzy krajowy podmiot gospodarczy z NIP i zapisuje go trwale w bazie.")]
+ public void TworzeniePodmiotuKrajowego_ZapisujeRekord()
+ {
+ var kod = UnikalnyKod();
+
+ var k = UtworzKontrahenta(kod, "Krajowa Firma Sp. z o.o.");
+ InTransaction(() =>
+ {
+ k.PodatnikVAT = true;
+ k.NIP = "1234563218"; // ustawienie NIP synchronizuje EuVAT
+ });
+ SaveDispose();
+
+ // Ponowny odczyt z nowej sesji potwierdza trwały zapis.
+ var zapisany = Crm.Kontrahenci.WgKodu[kod];
+ zapisany.Should().NotBeNull();
+ zapisany.Nazwa.Should().Be("Krajowa Firma Sp. z o.o.");
+ zapisany.StatusPodmiotu.Should().Be(StatusPodmiotu.PodmiotGospodarczy);
+ zapisany.RodzajPodmiotu.Should().Be(RodzajPodmiotu.Krajowy);
+ zapisany.PodatnikVAT.Should().BeTrue();
+ }
+
+ [Test]
+ [Description("Tworzy podmiot unijny (RodzajPodmiotu.Unijny).")]
+ public void TworzeniePodmiotuUnijnego_UstawiaRodzajUnijny()
+ {
+ var kod = UnikalnyKod();
+
+ UtworzKontrahenta(kod, "EU Trading GmbH",
+ status: StatusPodmiotu.PodmiotGospodarczy,
+ rodzaj: RodzajPodmiotu.Unijny);
+ SaveDispose();
+
+ var zapisany = Crm.Kontrahenci.WgKodu[kod];
+ zapisany.Should().NotBeNull();
+ zapisany.RodzajPodmiotu.Should().Be(RodzajPodmiotu.Unijny);
+ }
+
+ [Test]
+ [Description("Tworzy osobę fizyczną (StatusPodmiotu.Finalny).")]
+ public void TworzenieOsobyFizycznej_UstawiaStatusFinalny()
+ {
+ var kod = UnikalnyKod();
+
+ UtworzKontrahenta(kod, "Jan Kowalski",
+ status: StatusPodmiotu.Finalny,
+ rodzaj: RodzajPodmiotu.Krajowy);
+ SaveDispose();
+
+ var zapisany = Crm.Kontrahenci.WgKodu[kod];
+ zapisany.Should().NotBeNull();
+ zapisany.StatusPodmiotu.Should().Be(StatusPodmiotu.Finalny);
+ }
+}
\ No newline at end of file
diff --git a/Soneta.Skills.Test/CRM/Kontrahenci/UsuwanieKontrahentaTest.cs b/Soneta.Skills.Test/CRM/Kontrahenci/UsuwanieKontrahentaTest.cs
new file mode 100644
index 0000000..a70421d
--- /dev/null
+++ b/Soneta.Skills.Test/CRM/Kontrahenci/UsuwanieKontrahentaTest.cs
@@ -0,0 +1,48 @@
+using AwesomeAssertions;
+using NUnit.Framework;
+
+namespace Soneta.Skills.Test.CRM.Kontrahenci;
+
+///
+/// W5 — Bezpieczne usuwanie kontrahenta.
+/// Test pokazuje czyste usunięcie świeżo utworzonego rekordu (brak powiązań) oraz alternatywę
+/// „miękkiego" wycofania (Blokada=true), zalecaną gdy istnieją dokumenty/rozrachunki.
+///
+[TestFixture]
+public class UsuwanieKontrahentaTest : KontrahentTestBase
+{
+ [Test]
+ [Description("Usunięcie kontrahenta bez powiązań (DeleteRow) usuwa rekord z bazy.")]
+ public void CzysteUsuniecie_UsuwaRekord()
+ {
+ var kod = UnikalnyKod();
+ UtworzKontrahenta(kod, "Do Usuniecia");
+ SaveDispose();
+
+ var k = Crm.Kontrahenci.WgKodu[kod];
+ k.Should().NotBeNull();
+
+ InTransaction(() => k.Delete());
+ SaveDispose();
+
+ // Po usunięciu indeksator zwraca null.
+ Crm.Kontrahenci.WgKodu[kod].Should().BeNull();
+ }
+
+ [Test]
+ [Description("Miękkie wycofanie: zamiast usuwać, ustawiamy Blokada=true (rekord pozostaje).")]
+ public void MiekkieWycofanie_UstawiaBlokade()
+ {
+ var kod = UnikalnyKod();
+ UtworzKontrahenta(kod, "Do Wycofania");
+ SaveDispose();
+
+ var k = Crm.Kontrahenci.WgKodu[kod];
+ InTransaction(() => k.Blokada = true);
+ SaveDispose();
+
+ var zapisany = Crm.Kontrahenci.WgKodu[kod];
+ zapisany.Should().NotBeNull();
+ zapisany.Blokada.Should().BeTrue();
+ }
+}
diff --git a/Soneta.Skills.Test/CRM/Kontrahenci/WalidacjaNipRegonTest.cs b/Soneta.Skills.Test/CRM/Kontrahenci/WalidacjaNipRegonTest.cs
new file mode 100644
index 0000000..10d456c
--- /dev/null
+++ b/Soneta.Skills.Test/CRM/Kontrahenci/WalidacjaNipRegonTest.cs
@@ -0,0 +1,56 @@
+using AwesomeAssertions;
+using NUnit.Framework;
+using Soneta.Core;
+
+namespace Soneta.Skills.Test.CRM.Kontrahenci;
+
+///
+/// W2 — Walidacja NIP / REGON / EU VAT przed zapisem.
+/// Testy weryfikują publiczne, statyczne walidatory z Soneta.Core
+/// (, , ) oraz normalizację numerów.
+/// Walidatory sprawdzają format i sumę kontrolną — to NIE jest weryfikacja w MF/VIES (patrz W15).
+///
+[TestFixture]
+public class WalidacjaNipRegonTest : KontrahentTestBase
+{
+ [Test]
+ [Description("Nip.Test akceptuje poprawny NIP (10 cyfr i format z myślnikami), odrzuca błędny.")]
+ public void NipTest_RozrozniaPoprawnyIBledny()
+ {
+ // 1234563218 ma poprawną sumę kontrolną.
+ Nip.Test("1234563218").Should().BeTrue();
+ Nip.Test("123-456-32-18").Should().BeTrue();
+
+ // Zmiana ostatniej cyfry psuje sumę kontrolną.
+ Nip.Test("1234563219").Should().BeFalse();
+ Nip.Test("123").Should().BeFalse();
+
+ // Normalizacja: Flat usuwa myślniki, Format dodaje.
+ Nip.Flat("123-456-32-18").Should().Be("1234563218");
+ Nip.Format("1234563218").Should().Be("123-456-32-18");
+ }
+
+ [Test]
+ [Description("Regon.Test akceptuje poprawny REGON 9-znakowy, odrzuca błędny i o złej długości.")]
+ public void RegonTest_RozrozniaPoprawnyIBledny()
+ {
+ // 123456785 ma poprawną sumę kontrolną dla 9-znakowego REGON.
+ Regon.Test("123456785").Should().BeTrue();
+ Regon.Test("123456784").Should().BeFalse();
+ Regon.Test("12345").Should().BeFalse();
+ }
+
+ [Test]
+ [Description("EuVat.Test akceptuje krajowy numer z prefiksem PL nad poprawnym NIP, odrzuca błędny.")]
+ public void EuVatTest_PrefiksPL_DzialaNaPoprawnymNip()
+ {
+ // EuVat.Test wymaga ISessionable (sprawdza listę krajów UE w bazie).
+ EuVat.Test("PL1234563218", Session).Should().BeTrue();
+ EuVat.Test("PL1234563219", Session).Should().BeFalse();
+
+ // Rozbicie numeru na kod kraju + identyfikator.
+ EuVat.Split("PL1234563218", out var kraj, out var numer);
+ kraj.Should().Be("PL");
+ numer.Should().Be("1234563218");
+ }
+}
\ No newline at end of file
diff --git a/Soneta.Skills.Test/CRM/Kontrahenci/WarunkiPlatnosciTest.cs b/Soneta.Skills.Test/CRM/Kontrahenci/WarunkiPlatnosciTest.cs
new file mode 100644
index 0000000..fff003f
--- /dev/null
+++ b/Soneta.Skills.Test/CRM/Kontrahenci/WarunkiPlatnosciTest.cs
@@ -0,0 +1,56 @@
+using AwesomeAssertions;
+using NUnit.Framework;
+using Soneta.CRM;
+using Soneta.Kasa;
+
+namespace Soneta.Skills.Test.CRM.Kontrahenci;
+
+///
+/// W9 — Warunki płatności i limity kredytowe.
+/// Testy pokazują ustawienie sposobu zapłaty (rekord FormaPlatnosci z modułu Kasa),
+/// terminu płatności oraz typu limitu kredytowego. Pola kalkulowane (np.
+/// LimitNieograniczony) są tylko do odczytu i wynikają z ustawień.
+///
+[TestFixture]
+public class WarunkiPlatnosciTest : KontrahentTestBase
+{
+ [Test]
+ [Description("Ustawienie sposobu zapłaty (Przelew) i terminu płatności jest zapisywane.")]
+ public void WarunkiPlatnosci_SposobIZaplatyTermin_SaZapisywane()
+ {
+ var kod = UnikalnyKod();
+ UtworzKontrahenta(kod, "Firma Z Platnosciami");
+ SaveDispose();
+
+ var k = Crm.Kontrahenci.WgKodu[kod];
+ var przelew = Session.GetKasa().FormyPlatnosci[FormaPlatnosci.Przelew];
+
+ InTransaction(() =>
+ {
+ k.SposobZaplaty = przelew;
+ k.Termin = 14; // dni
+ });
+ SaveDispose();
+
+ var zapisany = Crm.Kontrahenci.WgKodu[kod];
+ zapisany.SposobZaplaty.Should().NotBeNull();
+ zapisany.Termin.Should().Be(14);
+ }
+
+ [Test]
+ [Description("Typ limitu kredytowego = Nieograniczony skutkuje kalkulowanym LimitNieograniczony=true.")]
+ public void LimitKredytowy_Nieograniczony_UstawiaFlageKalkulowana()
+ {
+ var kod = UnikalnyKod();
+ UtworzKontrahenta(kod, "Firma Bez Limitu");
+ SaveDispose();
+
+ var k = Crm.Kontrahenci.WgKodu[kod];
+ InTransaction(() => k.TypLimituKredytowego = TypLimituKredytowego.Nieograniczony);
+ SaveDispose();
+
+ var zapisany = Crm.Kontrahenci.WgKodu[kod];
+ zapisany.TypLimituKredytowego.Should().Be(TypLimituKredytowego.Nieograniczony);
+ zapisany.LimitNieograniczony.Should().BeTrue(); // pole kalkulowane (read-only)
+ }
+}
\ No newline at end of file
diff --git a/Soneta.Skills.Test/CRM/Kontrahenci/WyszukiwanieKontrahentaTest.cs b/Soneta.Skills.Test/CRM/Kontrahenci/WyszukiwanieKontrahentaTest.cs
new file mode 100644
index 0000000..1dea4d7
--- /dev/null
+++ b/Soneta.Skills.Test/CRM/Kontrahenci/WyszukiwanieKontrahentaTest.cs
@@ -0,0 +1,70 @@
+using System.Linq;
+using AwesomeAssertions;
+using NUnit.Framework;
+using Soneta.Core;
+using Soneta.CRM;
+
+namespace Soneta.Skills.Test.CRM.Kontrahenci;
+
+///
+/// W1 — Wyszukiwanie i identyfikacja kontrahenta.
+/// Testy pokazują trzy podstawowe sposoby odnajdywania kontrahenta używane w kodzie dodatków:
+/// po kodzie (klucz unikalny), po nazwie (klucz nieunikalny) oraz po NIP (filtr serwerowy
+/// SubTable[condition], zamiast iteracji całej tabeli w pamięci).
+///
+[TestFixture]
+public class WyszukiwanieKontrahentaTest : KontrahentTestBase
+{
+ [Test]
+ [Description("Wyszukanie po kodzie (indeks WgKodu) zwraca dokładnie utworzony rekord.")]
+ public void WyszukiwaniePoKodzie_ZwracaUtworzonyRekord()
+ {
+ var kod = UnikalnyKod();
+ UtworzKontrahenta(kod, "Firma Po Kodzie");
+ SaveDispose();
+
+ // WgKodu to klucz unikalny — indeksator zwraca pojedynczy rekord lub null.
+ var znaleziony = Crm.Kontrahenci.WgKodu[kod];
+
+ znaleziony.Should().NotBeNull();
+ znaleziony.Nazwa.Should().Be("Firma Po Kodzie");
+ }
+
+ [Test]
+ [Description("Wyszukanie po nazwie (indeks WgNazwy, nieunikalny) zwraca zbiór z rekordem.")]
+ public void WyszukiwaniePoNazwie_ZwracaRekordWZbiorze()
+ {
+ var kod = UnikalnyKod();
+ var nazwa = "Wyszukiwarka " + kod;
+ UtworzKontrahenta(kod, nazwa);
+ SaveDispose();
+
+ // WgNazwy jest kluczem nieunikalnym — zwraca zbiór, z którego bierzemy pierwszy.
+ var znaleziony = Crm.Kontrahenci.WgNazwy[nazwa].FirstOrDefault();
+
+ znaleziony.Should().NotBeNull();
+ znaleziony.Kod.Should().Be(kod);
+ }
+
+ [Test]
+ [Description("Wyszukanie po NIP filtrem serwerowym SubTable[condition] zwraca rekord; " +
+ "dedup wykrywa istniejący podmiot.")]
+ public void WyszukiwaniePoNip_FiltrSerwerowy_ZnajdujeISygnalizujeDuplikat()
+ {
+ var kod = UnikalnyKod();
+ var nip = "1234563218"; // poprawny NIP (suma kontrolna)
+ var k = UtworzKontrahenta(kod, "Firma Z NIP");
+ InTransaction(() => k.NIP = nip);
+ SaveDispose();
+
+ // Filtr po stronie serwera (klauzula WHERE w SQL), nie iteracja w pamięci.
+ // Warunek aplikujemy na indeksie tabeli (WgNIP); porównania tekstowe są case-insensitive.
+ var znaleziony = Crm.Kontrahenci.WgNIP[(Kontrahent x) => x.NIP == nip].FirstOrDefault();
+ znaleziony.Should().NotBeNull();
+ znaleziony.Kod.Should().Be(kod);
+
+ // Typowy dedup przed dodaniem nowego kontrahenta:
+ bool juzIstnieje = Crm.Kontrahenci.WgNIP[(Kontrahent x) => x.NIP == nip].Any();
+ juzIstnieje.Should().BeTrue();
+ }
+}
\ No newline at end of file
diff --git a/Soneta.Skills.Test/Soneta.Skills.Test.csproj b/Soneta.Skills.Test/Soneta.Skills.Test.csproj
new file mode 100644
index 0000000..d8642eb
--- /dev/null
+++ b/Soneta.Skills.Test/Soneta.Skills.Test.csproj
@@ -0,0 +1,13 @@
+
+
+ $(SonetaTargetFramework)
+
+
+
+
+
+
+
+
+
+
diff --git a/soneta-programming/SKILL.md b/soneta-programming/SKILL.md
index a42df88..bdab3da 100644
--- a/soneta-programming/SKILL.md
+++ b/soneta-programming/SKILL.md
@@ -40,6 +40,7 @@ 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) |
| **Zasady bezpiecznego kodu biznesowego — checklist do review i refaktoringu** | [references/safe-code.md](references/safe-code.md) |
| Skanowanie pól obiektu biznesowego z DLL (Roslyn MetadataReference) | [references/scan-props.md](references/scan-props.md) |
| Inwentaryzacja modułów i tabel (`*Module` / `*Row` / `*Table`) z DLL | [references/scan-modules.md](references/scan-modules.md) |
diff --git a/soneta-programming/references/domeny/kontrahent.md b/soneta-programming/references/domeny/kontrahent.md
new file mode 100644
index 0000000..b7550e1
--- /dev/null
+++ b/soneta-programming/references/domeny/kontrahent.md
@@ -0,0 +1,957 @@
+# Kontrahent — receptury kodu biznesowego (Soneta / enova365)
+
+Zbiór gotowych wzorców kodu dla obiektu biznesowego **`Soneta.CRM.Kontrahent`** (tabela `Kontrahenci`).
+Dokument jest częścią skilla `soneta-programming`. Celem jest, aby agent pisał **bezbłędny kod
+biznesowy** operujący na kontrahencie — trafiający w realne pola, kolekcje i workery platformy.
+
+> Format **zwarty**: każdy wzorzec opisuje ogólny przypadek + tabelę wariantów, zamiast wielu wąskich
+> pozycji. Fundamenty (sesja, transakcja, blokada optymistyczna, praca z `SubTable`, obsługa błędów)
+> są opisane w [`safe-code.md`](../safe-code.md), [`session-login.md`](../session-login.md) oraz
+> [`worker-extender.md`](../worker-extender.md) — tutaj się do nich odwołujemy, nie powtarzamy ich.
+>
+> **Cały kod w tym dokumencie jest zgodny z C# 10** (target-typed `new`, `var`, wyrażenia `switch`,
+> nazwane parametry `bool`). Snippety operują wyłącznie na **publicznym kontrakcie** platformy — nie
+> ma odwołań do prywatnych klas ani kodu źródłowego aplikacji.
+
+## Fakty o typie (zweryfikowane skanem DLL — `scan-props.csx`)
+
+- **Klasa biznesowa:** `Soneta.CRM.Kontrahent` — `GuidedRow` (root), tabela `Soneta.CRM.Kontrahenci`.
+- **Implementuje:** `IPodmiot`, `IKontrahent`, `IPodmiotKasowy`, `IElementSlownika`, `IAdresHost`,
+ `IKodowany`, `IAdresyWWWHost`, `IDaneKontaktoweHost`, `IEmailElement`, `IRegonHost`,
+ `IGIODOZgodnyHost`, `IGIODOWymianaDanychHost`, `IGIODOOświadczenieHost`.
+- **Pola:** 75 bazodanowych + 142 kalkulowane.
+- **Moduł:** `Soneta.CRM.CRMModule`, dostęp `session.GetCRM()`. Tabela: `crm.Kontrahenci`.
+- **Kluczowe pola bazodanowe (zapisywalne):** `Kod: string`, `Nazwa: string`, `NIP: string`,
+ `EuVAT: string`, `PESEL: string`, `REGON: string`, `KRS: string`,
+ `StatusPodmiotu: Soneta.Core.StatusPodmiotu`, `RodzajPodmiotu: Soneta.Core.RodzajPodmiotu`
+ (= „Rodzaj VAT dla sprzedaży"), `RodzajPodmiotuZakup: Soneta.Core.RodzajPodmiotu`,
+ `PodatnikVAT: bool`, `VATLiczonyOd: Soneta.CRM.VatKontahentaLiczonyOd`,
+ `FormaPrawna: Soneta.CRM.FormaPrawna`, `Waluta: Soneta.Waluty.Waluta`,
+ `SposobZaplaty: Soneta.Kasa.FormaPlatnosci`, `Termin: int`, `TerminPlanowany: int`,
+ `LimitKredytu: Currency`, `TypLimituKredytowego: Soneta.CRM.TypLimituKredytowego`,
+ `KontrolaKwota: Currency`, `KontrolaDni: int`, `TypPrzeterminowania: Soneta.CRM.TypLimituKredytowego`,
+ `Blokada: bool`, `BlokadaSprzedazy: bool`, `Zamiennik: Kontrahent`,
+ `EFaktura: Soneta.Core.EFaktura`, `EFakturaOkres: FromTo`,
+ `NieWindykowac: bool`, `DefinicjaSprawyWindykacyjnej`, `OddzialFirmy`, `Region`, `Rabat: Percent`,
+ `DomyslnySzablonPolOpcjonalnychKSeF`, `KSeFSposobObslugiWysylkiCeny`.
+- **Pola złożone:** `Adres: Soneta.Core.Adres`, `AdresDoKorespondencji: Soneta.Core.Adres`,
+ `Kontakt: Soneta.Core.Kontakt` (`Kontakt.EMAIL`, `Kontakt.TelefonKomorkowy`, `Kontakt.WWW`,
+ `Kontakt.SkrytkaPocztowa`, `Kontakt.Skype`), `OdsKarne: Soneta.Kasa.OdsetkiKarne`.
+- **Pola kalkulowane (tylko do odczytu):** `Nazwa` jest zapisywalna, ale `NazwaFormatowana`,
+ `NazwaPierwszaLinia`, `KodKraju`, `JestIncydentalny`, `IsStandard`, `DomyslnyRachunek`,
+ `Platnik`, `LimitNieograniczony`, `PrzeterminowanieNieograniczone`, `KontrolaAktywna`,
+ `AktualnyStatusVAT`, `AktualnyStatusVATMF`, `AktualnyStatusVATVies` — **nie ustawiaj** ich
+ bezpośrednio.
+- **Kolekcje (`SubTable`):** `Osoby` (`SubTable`), `Kontakty` (`SubTable`),
+ `AdresyWWW` (`SubTable`), `Kategorie` (`SubTable`),
+ `Branze` (`SubTable`), `Opiekunowie` (`SubTable`),
+ `Rachunki` (`SubTable`),
+ `Rozrachunki` (`SubTable`), `Podrzedni` (`SubTable`),
+ `StatusyVAT` (`SubTable`), `KodyKreskowe` (`SubTable`),
+ `GIODOOświadczenia` (`SubTable`), `GIODOUdostępnienia` (`SubTable`),
+ `PotwierdzeniaGIODO` (`SubTable`).
+- **Cechy:** `Features: Soneta.Business.FeatureCollection` (indeksator po nazwie definicji cechy).
+
+## Szablon wzorca
+
+Każdy wzorzec (`Wn`) ma stałą strukturę:
+
+- **Cel** — co robi i kiedy go użyć.
+- **Warianty** — tabela odmian przypadku.
+- **Pola i typy** — realne właściwości/kolekcje i ich typy.
+- **Snippet** — kod C# 10.
+- **Pułapki** — typowe błędy i zasady safe-code.
+
+---
+
+## 1. Wyszukiwanie i identyfikacja
+
+### W1 — Wyszukiwanie kontrahenta
+
+**Cel:** odnaleźć istniejącego kontrahenta po wybranym kluczu, zanim zaczniemy go modyfikować lub
+zanim utworzymy nowy rekord.
+
+**Warianty:**
+
+| Wariant | Klucz | Uwaga |
+|---|---|---|
+| Po kodzie | `Kod` | indeks `WgKodu`, klucz unikalny — zwraca pojedynczy rekord |
+| Po nazwie / fragmencie | `Nazwa` | indeks `WgNazwy` (nieunikalny) lub `SubTable[pattern]` |
+| Po NIP / EU VAT | `NIP`, `EuVAT` | normalizacja: `Nip.Flat` / `EuVat.Flat` przed porównaniem |
+| Po adresie | `Adres.*` | miejscowość, kod pocztowy, ulica |
+| Po PESEL / REGON / KRS | `PESEL`, `REGON`, `KRS` | osoby fizyczne / podmioty |
+| Dedup przed dodaniem | `NIP` | sprawdzenie, czy podmiot już istnieje |
+| Kontrahent incydentalny | `JestIncydentalny` | systemowy rekord (`Kontrahent.INCYDENTALNY`) |
+
+**Pola i typy:** `Kod: string`, `NIP: string`, `EuVAT: string`, `Nazwa: string`,
+`Adres: Soneta.Core.Adres`, `PESEL/REGON/KRS: string`, `JestIncydentalny: bool`.
+
+**Snippet:**
+
+```csharp
+var crm = session.GetCRM();
+
+// 1. Po kodzie — klucz unikalny, zwraca pojedynczy rekord lub null
+Kontrahent poKodzie = crm.Kontrahenci.WgKodu["ABC"];
+
+// 2. Po nazwie — indeks nieunikalny, zwraca zbiór; bierzemy pierwszy
+Kontrahent poNazwie = crm.Kontrahenci.WgNazwy["Firma XYZ"].FirstOrDefault();
+
+// 3. Po NIP — filtr serwerowy; warunek aplikujemy na indeksie. Porównania tekstowe są case-insensitive
+var nip = Nip.Flat("123-456-32-18"); // usuwa myślniki
+Kontrahent poNip = crm.Kontrahenci.WgNIP[(Kontrahent k) => k.NIP == nip].FirstOrDefault();
+
+// 4. Po fragmencie nazwy / mieście — serwerowy LIKE (warunek na indeksie WgNazwy)
+foreach (Kontrahent k in crm.Kontrahenci.WgNazwy[(Kontrahent k) =>
+ k.Nazwa.Contains("bud") && k.Adres.Miejscowosc == "Kraków"])
+{
+ // ...
+}
+
+// 5. Dedup przed dodaniem nowego kontrahenta
+bool juzIstnieje = crm.Kontrahenci.WgNIP[(Kontrahent k) => k.NIP == nip].Any();
+```
+
+**Pułapki:**
+- `WgKodu[...]` zwraca pojedynczy rekord (klucz unikalny) — może być `null`. `WgNazwy[...]` zwraca
+ **zbiór** (klucz nieunikalny), trzeba `.FirstOrDefault()`/iterację.
+- **Nie iteruj całej tabeli** `Kontrahenci` z `if` w pamięci — to tabela kartotekowa (rośnie z
+ biznesem). Filtruj przez warunek aplikowany **na indeksie**, np.
+ `crm.Kontrahenci.WgKodu[(Kontrahent k) => …]` (warunek wykonywany przez SQL). Indeksator samej
+ tabeli (`crm.Kontrahenci[…]`) służy do dostępu po `ID`/kluczu, nie przyjmuje wyrażenia LINQ.
+ Patrz [`rowcondition.md`](../rowcondition.md) i [`safe-code.md`](../safe-code.md) §6.
+- W `RowCondition` (wyrażeniu LINQ) wolno użyć **tylko pól bazodanowych**. `NazwaFormatowana`,
+ `KodKraju`, `Platnik` są kalkulowane → rzucą `LinqConditionException`.
+- Porównania tekstowe w warunku są **case-insensitive** — nie dubluj `ToLower()`.
+- Przed porównaniem NIP/EU VAT normalizuj wejście (`Nip.Flat`, `EuVat.Flat`), bo w bazie bywają
+ formaty z myślnikami i bez.
+
+### W2 — Walidacja NIP / REGON / EU VAT
+
+**Cel:** sprawdzić poprawność NIP/REGON (suma kontrolna) i EU VAT (format/kraj) przed zapisem,
+niezależnie od weryfikacji online (W15).
+
+**Warianty:**
+
+| Wariant | Wejście | Metoda publiczna |
+|---|---|---|
+| NIP krajowy | 10 cyfr lub `DDD-DDD-DD-DD` | `Soneta.Core.Nip.Test(string)` |
+| REGON 9/14 | 9 lub 14 cyfr | `Soneta.Core.Regon.Test(string)` |
+| EU VAT | prefiks kraju + numer | `Soneta.Core.EuVat.Test(string, ISessionable)` |
+| Normalizacja | usunięcie myślników/spacji | `Nip.Flat`, `Nip.Format`, `EuVat.Flat` |
+| Rozbicie EU VAT | kraj + numer | `EuVat.Split(value, out country, out nip)` |
+
+**Pola i typy:** `NIP: string`, `REGON: string`, `EuVAT: string`. Walidatory są **statyczne**;
+`EuVat.Test` wymaga `ISessionable` (sprawdza listę krajów UE w bazie).
+
+**Snippet:**
+
+```csharp
+// Walidatory rzucają NullReferenceException dla null — najpierw odsiej puste wejście.
+if (!nip.IsNullOrEmpty() && Nip.Test(nip)) { /* NIP poprawny */ }
+if (!regon.IsNullOrEmpty() && Regon.Test(regon)) { /* REGON poprawny */ }
+if (!euVat.IsNullOrEmpty() && EuVat.Test(euVat, session)) { /* EU VAT poprawny */ }
+
+// Rozbicie EU VAT "PL1234563218" -> kraj "PL", numer "1234563218"
+EuVat.Split(euVat, out string kodKraju, out string numer);
+
+// Walidacja w event-handlerze zapisu (rzut PRZED Commit/Save):
+if (!kontrahent.NIP.IsNullOrEmpty() && !Nip.Test(kontrahent.NIP))
+ throw new RowException(kontrahent, "Nieprawidłowy NIP".Translate(), nameof(kontrahent.NIP));
+```
+
+**Pułapki:**
+- `Nip.Test`, `Regon.Test`, `EuVat.Test` **rzucają `NullReferenceException` dla `null`** (odwołują się
+ do `.Length`). Zawsze najpierw sprawdź `IsNullOrEmpty`.
+- To walidacja **formatu/sumy kontrolnej**, a nie weryfikacja w MF/VIES — patrz W15.
+- Komunikaty walidacyjne rzucaj jako `RowException(row, "…".Translate(), nameof(Pole))` **przed**
+ `Commit()` (safe-code §5.1). Wyjątek po `Commit()` nie wycofa zmiany z sesji.
+- Ustawienie `NIP`/`EuVAT` na samym `Kontrahent` uruchamia wbudowaną synchronizację (NIP↔EuVAT,
+ auto-zmiana `RodzajPodmiotu`) — własna walidacja jest dodatkiem, nie zastępstwem.
+
+---
+
+## 2. Tworzenie, modyfikacja, usuwanie
+
+### W3 — Tworzenie kontrahenta
+
+**Cel:** utworzyć nowy rekord kontrahenta z poprawnym minimalnym zestawem pól i wartościami domyślnymi.
+
+**Warianty:**
+
+| Wariant | Charakterystyka | Pola krytyczne |
+|---|---|---|
+| Podmiot gospodarczy krajowy | firma w PL | `StatusPodmiotu=PodmiotGospodarczy`, `RodzajPodmiotu=Krajowy`, `NIP` |
+| Unijny / zagraniczny | sprzedaż wewn.-unijna / eksport | `EuVAT`, `RodzajPodmiotu=Unijny/Eksportowy` |
+| Osoba fizyczna / finalny | konsument | `StatusPodmiotu=Finalny`, `PESEL` |
+
+**Pola i typy:** `Kod: string`, `Nazwa: string`, `StatusPodmiotu: Soneta.Core.StatusPodmiotu`
+(`PodmiotGospodarczy=0`, `Finalny=1`), `RodzajPodmiotu: Soneta.Core.RodzajPodmiotu`
+(`Krajowy=0`, `Eksportowy=1`, `EksportowyPodróżny=2`, `Unijny=3`, `UnijnyTrójstronny=4`, `BezVAT=5`),
+`PodatnikVAT: bool`, `FormaPrawna: Soneta.CRM.FormaPrawna`.
+
+**Nadawanie kodu / numeracji:** `Kod` jest polem tekstowym ustawianym jawnie. Może być wymagana jego
+unikalność (zależnie od konfiguracji modułu CRM); w razie kolizji `Save()` zgłosi `RowException` z
+`DuplicateKeyException` w `InnerException`.
+
+**Snippet:**
+
+```csharp
+var crm = session.GetCRM();
+
+using (var t = session.Logout(editMode: true))
+{
+ var k = new Kontrahent();
+ crm.Kontrahenci.AddRow(k); // najpierw dodaj do tabeli, potem ustawiaj pola
+
+ k.Kod = "FIRMA001";
+ k.Nazwa = "Firma XYZ Sp. z o.o.";
+ k.StatusPodmiotu = StatusPodmiotu.PodmiotGospodarczy;
+ k.RodzajPodmiotu = RodzajPodmiotu.Krajowy;
+ k.PodatnikVAT = true;
+ k.NIP = "1234563218"; // ustawienie NIP synchronizuje EuVAT
+
+ t.Commit(); // Commit() w kodzie biznesowym
+}
+session.Save(); // zapis do bazy; tu wykryte konflikty/duplikaty
+```
+
+**Pułapki:**
+- Tworzenie **wyłącznie w transakcji** (`session.Logout(editMode: true)`). `AddRow` przed
+ ustawianiem pól.
+- W workerze/extenderze (uruchamianym z UI) używaj `t.CommitUI()` zamiast `t.Commit()`
+ (safe-code, [`worker-extender.md`](../worker-extender.md)).
+- `Nazwa` jest zapisywalna; `NazwaFormatowana`/`NazwaPierwszaLinia` są kalkulowane — nie ustawiaj.
+- Dla podmiotu unijnego ustaw `EuVAT` (z prefiksem kraju) — platforma sama dostosuje `RodzajPodmiotu`.
+- Brak `Commit()` = automatyczny rollback przy `Dispose()`.
+
+### W4 — Modyfikacja i statusy
+
+**Cel:** zmienić dane istniejącego kontrahenta lub jego status dostępności/handlowy.
+
+**Warianty:**
+
+| Wariant | Pole / operacja |
+|---|---|
+| Edycja danych identyfikacyjnych | `Kod`, `Nazwa`, `NIP`, … (blokada optymistyczna) |
+| Ukrycie na listach | `Blokada: bool` |
+| Blokada sprzedaży | `BlokadaSprzedazy: bool` |
+| Zmiana formy prawnej | `FormaPrawna` (poj. lub masowo: worker `ZmienFormePrawnaKontrahentowWorker`) |
+| Zastąpienie (zamiennik) | `Zamiennik: Kontrahent` (ustawia automatycznie `Blokada=true`) |
+| Kopiowanie kontrahenta | worker `Soneta.CRM.KopiujKontrahentaWorker` (akcja „Kopiuj kontrahenta...") |
+
+**Pola i typy:** `Blokada: bool`, `BlokadaSprzedazy: bool`, `FormaPrawna: Soneta.CRM.FormaPrawna`,
+`Zamiennik: Soneta.CRM.Kontrahent`.
+
+**Snippet:**
+
+```csharp
+var crm = session.GetCRM();
+var k = crm.Kontrahenci.WgKodu["FIRMA001"];
+if (k == null) return;
+
+using (var t = session.Logout(editMode: true))
+{
+ k.Nazwa = "Firma XYZ S.A.";
+ k.BlokadaSprzedazy = true; // zakaz wystawiania dokumentów rozchodu
+ k.Blokada = true; // ukrycie na listach
+ t.Commit();
+}
+session.Save();
+
+// Kopiowanie kontrahenta — programowe użycie workera (bez UI):
+var kopiarka = new KopiujKontrahentaWorker { Kontrahent = k };
+using (var t = session.Logout(editMode: true))
+{
+ Kontrahent nowy = kopiarka.Kopiuj();
+ t.Commit();
+}
+session.Save();
+```
+
+**Pułapki:**
+- **Blokada optymistyczna**: konflikt edycji (ktoś inny zapisał ten rekord) wybucha w `session.Save()`
+ jako `RowConflictException` — obsłuż go (refresh + retry lub eskalacja), nie połykaj (safe-code §4).
+- Nie nadpisuj `Kod` rekordów standardowych (`IsStandard == true`) ani incydentalnego
+ (`JestIncydentalny == true`).
+- `Zamiennik` ma efekt uboczny — ustawienie zamiennika włącza `Blokada=true`. Do rozwiązania
+ „aktualnego" kontrahenta służy `Kontrahent.Coalesce(k)` (zwraca zamiennika albo sam rekord).
+- Worker `KopiujKontrahentaWorker` ma property `[Context] Kontrahent` — przy ręcznym użyciu ustaw ją
+ przed wywołaniem `Kopiuj()`; operacja musi być w transakcji.
+
+### W5 — Bezpieczne usuwanie
+
+**Cel:** usunąć kontrahenta albo świadomie odmówić usunięcia, gdy istnieją powiązania.
+
+**Warianty:**
+
+| Wariant | Sytuacja | Zalecenie |
+|---|---|---|
+| Usunięcie czyste | brak dokumentów/rozrachunków/zadań/zdarzeń | dozwolone (`DeleteRow`) |
+| Usunięcie zablokowane | są dokumenty/rozrachunki/zapisy | zamiast usuwać → `Blokada=true` |
+| Kontrahent systemowy | `IsStandard` / `JestIncydentalny` | nie usuwać |
+
+**Pola i typy:** `DokumentyHandlowe`, `Rozrachunki`, `Zadania`, `Zdarzenia` (kolekcje `SubTable`),
+`IsStandard: bool`, `JestIncydentalny: bool`, `Blokada: bool`.
+
+**Snippet:**
+
+```csharp
+var crm = session.GetCRM();
+var k = crm.Kontrahenci.WgKodu["FIRMA001"];
+if (k == null) return;
+
+if (k.IsStandard || k.JestIncydentalny)
+ throw new BusException("Nie można usunąć kontrahenta systemowego.".Translate());
+
+bool maPowiazania = !k.DokumentyHandlowe.IsEmpty || !k.Rozrachunki.IsEmpty
+ || !k.Zadania.IsEmpty || !k.Zdarzenia.IsEmpty;
+
+using (var t = session.Logout(editMode: true))
+{
+ if (maPowiazania)
+ k.Blokada = true; // miękkie wycofanie zamiast usunięcia
+ else
+ k.Delete(); // twarde usunięcie tylko gdy brak powiązań
+ t.Commit();
+}
+session.Save();
+```
+
+**Pułapki:**
+- Sprawdź powiązania **przed** `DeleteRow()`. Próba usunięcia powiązanego rekordu i tak zostanie
+ odrzucona przez integralność (wyjątek w `Save()`), ale lepiej zdecydować świadomie.
+- Preferuj `Blokada=true` (kontrahent znika z list, dane pozostają) zamiast kasowania, gdy są
+ powiązania historyczne.
+- `IsEmpty`/`Any` na kolekcji `SubTable` to **właściwości** (test serwerowy `exists …`, bez
+ nawiasów) — nie materializuj kolekcji do pamięci (`.ToList().Count`).
+
+---
+
+## 3. Adres, kontakt, osoby
+
+### W6 — Adres
+
+**Cel:** wprowadzić lub zaktualizować adres kontrahenta.
+
+**Warianty:**
+
+| Wariant | Pole |
+|---|---|
+| Adres główny | `Adres: Soneta.Core.Adres` |
+| Adres do korespondencji | `AdresDoKorespondencji: Soneta.Core.Adres` |
+| Telefon / faks na adresie | `Adres.Telefon`, `Adres.Faks` |
+| Dane rozszerzone / nietypowa lokalizacja / GLN | `Adres.NietypowaLokalizacja`, `Adres.GLN` |
+
+**Pola i typy (`Soneta.Core.Adres`):** `Ulica: string`, `NrDomu: string`, `NrLokalu: string`,
+`KodPocztowy: int`, `KodPocztowyS: string` (sformatowany, np. `"31-000"`), `Poczta: string`,
+`Miejscowosc: string`, `Gmina: string`, `Powiat: string`, `Wojewodztwo: Soneta.Core.Wojewodztwa`
+(enum), `Kraj: string`, `KodKraju: string`, `GLN: string`, `Telefon: string`, `Faks: string`,
+`ZagranicznyKodPocztowy: string`.
+
+**Snippet:**
+
+```csharp
+var k = session.GetCRM().Kontrahenci.WgKodu["FIRMA001"];
+
+using (var t = session.Logout(editMode: true))
+{
+ var a = k.Adres; // property zwraca obiekt adresu — edytujemy jego pola
+ a.Ulica = "Wadowicka";
+ a.NrDomu = "8A";
+ a.NrLokalu = "2";
+ a.KodPocztowyS = "30-415"; // string z myślnikiem; pole int KodPocztowy = 30415
+ a.Miejscowosc = "Kraków";
+ a.Poczta = "Kraków";
+ a.Wojewodztwo = Wojewodztwa.małopolskie;
+ a.Kraj = "Polska";
+ a.Telefon = "+48 12 345 67 89";
+
+ // Adres do korespondencji (gdy różny od głównego)
+ k.AdresDoKorespondencji.Ulica = "Skrytka pocztowa 15";
+ t.Commit();
+}
+session.Save();
+```
+
+**Pułapki:**
+- `Adres` to property **kalkulowana zwracająca obiekt złożony** — nie da się przypisać `k.Adres = …`;
+ modyfikuj jego pola.
+- `KodPocztowy` jest typu **`int`** (np. `30415`). Do wartości z myślnikiem używaj `KodPocztowyS`
+ (string), które samo rozkłada/składa kod.
+- `Wojewodztwo` to **enum** `Soneta.Core.Wojewodztwa`, nie string.
+- `KodKraju` adresu bywa kalkulowane z `Kraj` — ustawiaj `Kraj`/`KodKraju` spójnie.
+
+### W7 — Dane kontaktowe i adresy WWW
+
+**Cel:** odczytać i zapisać kanały kontaktu (e-mail, telefon, faks, WWW) z oznaczeniem domyślnego.
+
+**Warianty:**
+
+| Wariant | Kolekcja / pole |
+|---|---|
+| Odczyt domyślnego e-maila/telefonu/WWW | `Kontakt.EMAIL`, `Kontakt.TelefonKomorkowy`, `Kontakt.WWW` |
+| Dodanie kanału kontaktu | `Kontakty: SubTable` (`Rodzaj`, `Kontakt`, `Domyslny`) |
+| Adresy WWW | `AdresyWWW: SubTable` (`Adres`, `Domyslny`) |
+| e-faktura | `EFaktura: Soneta.Core.EFaktura`, `EFakturaOkres: FromTo` |
+
+**Pola i typy:** `Kontakt: Soneta.Core.Kontakt` (zsumowany „domyślny" kontakt — `EMAIL`,
+`TelefonKomorkowy`, `WWW`, `SkrytkaPocztowa`, `Skype`). `DaneKontaktowe`: `Host: IDaneKontaktoweHost`,
+`Rodzaj: RodzajKontaktu`, `Kontakt: string`, `Domyslny: bool`. `AdresWWW`: `Adres: string`,
+`Domyslny: bool`.
+
+**Snippet:**
+
+```csharp
+var k = session.GetCRM().Kontrahenci.WgKodu["FIRMA001"];
+
+// Odczyt domyślnych kanałów (do automatyzacji wysyłek):
+string email = k.Kontakt.EMAIL;
+string tel = k.Kontakt.TelefonKomorkowy;
+string www = k.Kontakt.WWW;
+
+// Dodanie nowego kanału e-mail i oznaczenie go jako domyślny:
+var rodzajEmail = session.GetCore().RodzajeKontaktow[RodzajeKontaktow.AdresEmail];
+using (var t = session.Logout(editMode: true))
+{
+ var dk = new DaneKontaktowe { Host = k }; // Host = kontrahent (IDaneKontaktoweHost)
+ session.AddRow(dk);
+ dk.Rodzaj = rodzajEmail;
+ dk.Kontakt = "kontakt@firma-xyz.pl";
+ dk.Domyslny = true;
+
+ // Dodanie adresu WWW:
+ var strona = new AdresWWW(k); // ctor przyjmuje IAdresyWWWHost
+ session.AddRow(strona);
+ strona.Adres = "https://www.firma-xyz.pl";
+ strona.Domyslny = true;
+ t.Commit();
+}
+session.Save();
+```
+
+**Pułapki:**
+- `DaneKontaktowe.Rodzaj` to rekord słownika `RodzajKontaktu` — pobierz go po stałej Guid przez
+ `session.GetCore().RodzajeKontaktow[RodzajeKontaktow.AdresEmail]` (analogicznie `TelefonKomórkowy`,
+ `TelefonStacjonarny`, `Faks`, `Skype`).
+- Tylko **jeden** kontakt domyślny w obrębie rodzaju — ustawienie `Domyslny=true` na nowym zwykle
+ zdejmuje flagę z poprzedniego.
+- `k.Kontakt.*` to **zagregowany** widok domyślnych kontaktów (do odczytu w automatyzacji). Pełna
+ lista kanałów jest w kolekcji `k.Kontakty`.
+- `AdresWWW` tworzymy konstruktorem z hostem (`new AdresWWW(k)`); pole adresu URL nazywa się `Adres`
+ (nie `Url`).
+
+### W8 — Osoby kontaktowe
+
+**Cel:** zarządzać osobami kontaktowymi przypisanymi do kontrahenta.
+
+**Warianty:**
+
+| Wariant | Operacja |
+|---|---|
+| Odczyt listy | `Osoby: SubTable` (`Imie`, `Nazwisko`, `Stanowisko`, `EMAIL`, `Nieaktualny`) |
+| Dodanie osoby | nowy `KontaktOsoba`, ustaw `Kontrahent` |
+| Edycja osoby | zmiana pól |
+| Oznaczenie nieaktualnej | flaga `Nieaktualny` (zamiast usuwania) |
+| Dołącz / odłącz istniejącą | workery `DolaczOsobeKontrahentaWorker`, `RozlaczKontrahentaWorker` |
+
+**Pola i typy (`KontaktOsoba`):** `Imie: string`, `Nazwisko: string`, `Stanowisko: string`,
+`EMAIL: string`, `Nieaktualny: bool`, `Kontrahent: IKontrahent` (powiązanie).
+
+**Snippet:**
+
+```csharp
+var k = session.GetCRM().Kontrahenci.WgKodu["FIRMA001"];
+
+// Odczyt aktualnych osób:
+foreach (KontaktOsoba os in k.Osoby[(KontaktOsoba o) => !o.Nieaktualny])
+ Console.WriteLine($"{os.Imie} {os.Nazwisko} — {os.Stanowisko}");
+
+// Dodanie osoby kontaktowej:
+using (var t = session.Logout(editMode: true))
+{
+ var os = new KontaktOsoba();
+ session.AddRow(os);
+ os.Kontrahent = k; // powiązanie z kontrahentem
+ os.Imie = "Anna";
+ os.Nazwisko = "Nowak";
+ os.Stanowisko = "Kierownik zakupów";
+ os.EMAIL = "a.nowak@firma-xyz.pl";
+ t.Commit();
+}
+session.Save();
+```
+
+**Pułapki:**
+- Powiązanie osoby z kontrahentem ustawiamy przez `os.Kontrahent = k` (pod spodem powstaje rekord
+ relacji w `OsobyKontaktowe`); osoba pojawia się wtedy w `k.Osoby`.
+- **Nie usuwaj** osób, których dotyczyła historia kontaktu — oznaczaj `Nieaktualny=true`. Uwaga:
+ ustawienie `Nieaktualny` ma efekty uboczne (kaskada na powiązania, integracja z kontem webowym) —
+ rób to tylko w pełnej, zalogowanej sesji aplikacyjnej.
+- Filtruj aktualne/nieaktualne serwerowo: `k.Osoby[(KontaktOsoba o) => !o.Nieaktualny]`.
+
+---
+
+## 4. Warunki handlowe i finanse
+
+### W9 — Warunki płatności i limity kredytowe
+
+**Cel:** ustawić warunki płatności i parametry kontroli kredytu kupieckiego.
+
+**Warianty:**
+
+| Wariant | Pola |
+|---|---|
+| Warunki płatności | `SposobZaplaty: FormaPlatnosci`, `Termin: int`, `TerminPlanowany: int`, `Waluta` |
+| Limit kredytu | `TypLimituKredytowego`, `LimitKredytu: Currency` |
+| Kontrola przeterminowania | `TypPrzeterminowania`, `KontrolaKwota: Currency`, `KontrolaDni: int` |
+| Odczyt stanu (kalkulowane) | `LimitNieograniczony`, `PrzeterminowanieNieograniczone`, `KontrolaAktywna` |
+| e-faktura | `EFaktura: Soneta.Core.EFaktura`, `EFakturaOkres: FromTo` |
+| Odsetki / windykacja | `OdsKarne` (złożone), `NieWindykowac`, `DefinicjaSprawyWindykacyjnej` |
+| Rachunki bankowe | `Rachunki: SubTable`, `DomyslnyRachunek` (kalkulowane) |
+
+**Pola i typy:** `SposobZaplaty: Soneta.Kasa.FormaPlatnosci`, `Termin: int`,
+`LimitKredytu: Soneta.Types.Currency`, `TypLimituKredytowego: Soneta.CRM.TypLimituKredytowego`
+(`Kwota=0`, `Nieograniczony=1`), `KontrolaKwota: Currency`, `KontrolaDni: int`,
+`TypPrzeterminowania: Soneta.CRM.TypLimituKredytowego`.
+
+**Snippet:**
+
+```csharp
+var crm = session.GetCRM();
+var k = crm.Kontrahenci.WgKodu["FIRMA001"];
+
+using (var t = session.Logout(editMode: true))
+{
+ // Warunki płatności:
+ k.SposobZaplaty = session.GetKasa().FormyPlatnosci[FormaPlatnosci.Przelew];
+ k.Termin = 14; // dni
+
+ // Limit kredytu kupieckiego:
+ k.TypLimituKredytowego = TypLimituKredytowego.Kwota;
+ k.LimitKredytu = new Currency(50000m, "PLN"); // kwota + symbol waluty
+
+ // Kontrola przeterminowania:
+ k.TypPrzeterminowania = TypLimituKredytowego.Kwota;
+ k.KontrolaKwota = new Currency(5000m, "PLN");
+ k.KontrolaDni = 7;
+ t.Commit();
+}
+session.Save();
+
+// Odczyt pól kalkulowanych (tylko do odczytu):
+bool bezLimitu = k.LimitNieograniczony;
+RachunekBankowyPodmiotu domyslny = k.DomyslnyRachunek;
+```
+
+**Pułapki:**
+- Kwoty to **`Currency`** (kwota + waluta), nie `decimal`/`double` (safe-code §10). Twórz
+ `new Currency(kwota, waluta)`.
+- `LimitNieograniczony`, `PrzeterminowanieNieograniczone`, `KontrolaAktywna`, `DomyslnyRachunek` są
+ **kalkulowane** — tylko do odczytu.
+- `SposobZaplaty` to rekord `FormaPlatnosci` — pobierz go z `session.GetKasa().FormyPlatnosci[…]`
+ (np. stała `FormaPlatnosci.Przelew`), nie ustawiaj „z palca".
+- Ustawienie `TypLimituKredytowego = Nieograniczony` czyni `LimitKredytu` polem nieaktywnym (w UI
+ read-only) — ustawiaj kwotę tylko dla typu `Kwota`.
+
+### W10 — Konto księgowe / rozrachunkowe
+
+**Cel:** odczytać/ustawić powiązanie kontrahenta z rozliczeniami (kontrahent jako `IPodmiotKasowy`).
+
+**Warianty:**
+
+| Wariant | Mechanizm |
+|---|---|
+| Kontrahent jako podmiot kasowy | rzutowanie/użycie przez interfejs `Soneta.Kasa.IPodmiotKasowy` |
+| Domyślny płatnik | `Platnik: IPodmiotKasowy` (kalkulowane — nadrzędny z relacji lub sam podmiot) |
+| Rachunki podmiotu | `Rachunki: SubTable` |
+
+**Pola i typy:** `Platnik: Soneta.Kasa.IPodmiotKasowy` (kalkulowane), `Rachunki`,
+`DomyslnyRachunek` (kalkulowane). `Kontrahent` implementuje `IPodmiotKasowy`.
+
+**Snippet:**
+
+```csharp
+var k = session.GetCRM().Kontrahenci.WgKodu["FIRMA001"];
+
+// Kontrahent jest podmiotem kasowym — można go podać tam, gdzie wymagany jest IPodmiotKasowy:
+IPodmiotKasowy podmiot = k;
+
+// Domyślny płatnik (gdy kontrahent jest podrzędny, zwraca nadrzędnego z relacji):
+IPodmiotKasowy platnik = k.Platnik;
+```
+
+**Pułapki:**
+- `Platnik` jest **kalkulowany** (zależny od relacji podmiotów, W14) — nie zapisuj go bezpośrednio.
+- Konta księgowe rozrachunkowe należą do modułu księgowego; z poziomu kontrahenta operuj przez
+ interfejs `IPodmiotKasowy` i kolekcje rozrachunków (W11), nie przez prywatne pola księgowe.
+
+### W11 — Rozrachunki i płatności
+
+**Cel:** odczytać należności/zobowiązania i ostatnie płatności kontrahenta.
+
+**Warianty:**
+
+| Wariant | Źródło |
+|---|---|
+| Należności i zobowiązania | `Rozrachunki: SubTable` |
+| Płatności / zapłaty | `Platnosci: SubTable`, `Zaplaty: SubTable` |
+| Dokumenty rozliczeniowe | `DokumentyRozliczeniowe: SubTable` |
+| Przelewy | `Przelewy: SubTable` |
+
+**Pola i typy:** wszystkie powyższe to kolekcje `SubTable` na `Kontrahent`. `RozrachunekIdx` ma
+m.in. pola kwotowe i datę rozrachunku.
+
+**Snippet:**
+
+```csharp
+var k = session.GetCRM().Kontrahenci.WgKodu["FIRMA001"];
+
+// Rozrachunki nierozliczone — filtr serwerowy po kolekcji:
+foreach (RozrachunekIdx r in k.Rozrachunki)
+{
+ // r.* — kwota, waluta, data, kierunek (należność/zobowiązanie)
+}
+
+// Ostatnie zapłaty (zawężaj zakresem czasu — to dane operacyjne!):
+var od = Date.Today.AddMonths(-3);
+foreach (Zaplata z in k.Zaplaty)
+{
+ // ...
+}
+```
+
+**Pułapki:**
+- Rozrachunki to dane **wyliczane/operacyjne** — przy szerszych analizach **zawężaj zakres czasowy**
+ i nie ładuj całej historii (safe-code §6.3).
+- Saldo/przeterminowanie na dany dzień to wynik wyliczeń — czytaj przez dedykowane pola/kolekcje,
+ nie sumuj „ręcznie" całej tabeli.
+- `RozrachunekIdx` / `Platnosc` / `Zaplata` żyją w module `Soneta.Kasa` — wymagana referencja do
+ `Soneta.Kasa`.
+
+---
+
+## 5. Sprzedaż i dokumenty
+
+### W12 — Dokumenty i dane sprzedażowe
+
+**Cel:** odczytać dokumenty handlowe kontrahenta oraz (opcjonalnie) utworzyć dokument.
+
+**Warianty:**
+
+| Wariant | Źródło / worker |
+|---|---|
+| Dokumenty, w których kontrahent jest nabywcą | `DokumentyHandlowe: SubTable` |
+| Dokumenty, w których jest odbiorcą | `DokumentyHandloweOdbiorcy: SubTable` |
+| Dokumenty ewidencji | `DokumentyEwidencji: SubTable` |
+| Utworzenie dokumentu | przez moduł `Handel` (definicja dokumentu + ustawienie `Kontrahent`) |
+
+**Pola i typy:** `DokumentyHandlowe`, `DokumentyHandloweOdbiorcy`, `DokumentyEwidencji` — kolekcje
+`SubTable` na `Kontrahent`.
+
+**Snippet:**
+
+```csharp
+var k = session.GetCRM().Kontrahenci.WgKodu["FIRMA001"];
+
+// Ostatnie dokumenty handlowe kontrahenta jako nabywcy:
+foreach (var d in k.DokumentyHandlowe)
+{
+ // d.* — numer, data, wartości
+}
+```
+
+**Pułapki:**
+- Tworzenie dokumentu handlowego realizuje moduł `Handel` (definicja `DefDokHandlowych`,
+ `new DokumentHandlowy`, ustawienie `Kontrahent`) — to osobny obszar; z poziomu kontrahenta
+ korzystaj z jego kolekcji do odczytu.
+- `DokHandlowe` to tabela **operacyjna guided** — przy iteracji poprzecznej zawężaj zakres czasowy
+ (safe-code §6.3). Kolekcja `k.DokumentyHandlowe` jest już zawężona do jednego kontrahenta.
+
+---
+
+## 6. Klasyfikacja
+
+### W13 — Cechy, kategorie, branże, GUS
+
+**Cel:** uzupełnić cechy definiowalne i klasyfikacje kontrahenta.
+
+**Warianty:**
+
+| Wariant | Kolekcja / mechanizm |
+|---|---|
+| Cecha definiowalna | `Features: FeatureCollection` (odczyt/zapis po nazwie definicji) |
+| Kategorie | `Kategorie: SubTable` (poj. lub worker `KontrahenciPrzypiszKategorieWorker`) |
+| Branże | `Branze: SubTable` |
+| PKD / dane GUS | worker `DaneZGusBirWorker` (online; pobiera też kody PKD) |
+
+**Pola i typy:** `Features: Soneta.Business.FeatureCollection` (indeksator `Features["NazwaCechy"]`
+zwraca/przyjmuje `object`), `Kategorie: SubTable`, `Branze: SubTable`.
+`KategoriaKth` tworzymy konstruktorem `new KategoriaKth(kontrahent, defKategorii)`.
+
+**Snippet:**
+
+```csharp
+var crm = session.GetCRM();
+var k = crm.Kontrahenci.WgKodu["FIRMA001"];
+
+using (var t = session.Logout(editMode: true))
+{
+ // Cecha definiowalna — dostęp po nazwie definicji (cecha musi być wcześniej zdefiniowana):
+ k.Features["Segment"] = "Premium";
+
+ // Przypisanie do kategorii (defKat: DefKategKth z konfiguracji CRM, indeks WgNazwy):
+ var defKat = crm.DefKategoriiKth.WgNazwy["VIP"];
+ if (defKat != null && crm.KategorieKth.WgKontrahent[k, defKat] == null)
+ crm.KategorieKth.AddRow(new KategoriaKth(k, defKat));
+ t.Commit();
+}
+session.Save();
+
+// Odczyt cechy:
+object segment = k.Features["Segment"];
+```
+
+**Pułapki:**
+- Cecha jest dostępna **po nazwie definicji**; odwołanie do niezdefiniowanej cechy rzuca wyjątek —
+ upewnij się, że definicja istnieje (cechy vs pola natywne to dwie różne rzeczy).
+- Przed dodaniem kategorii sprawdź duplikat: `crm.KategorieKth.WgKontrahent[k, defKat]`.
+- Masowe przypisanie kategorii: worker `KontrahenciPrzypiszKategorieWorker` (`[Context] Kontrahent[]`
+ + `Params.Kategoria`).
+- Pobranie kodów PKD odbywa się **online** z GUS-BIR (worker `DaneZGusBirWorker`) — patrz W15.
+
+---
+
+## 7. Powiązania
+
+### W14 — Powiązania i opiekunowie
+
+**Cel:** zarządzać opiekunami i relacjami między kontrahentami.
+
+**Warianty:**
+
+| Wariant | Operacja / worker |
+|---|---|
+| Opiekun (dodanie / główny) | `Opiekunowie: SubTable`, worker `UstawOpiekunaGlownegoWorker` |
+| Sprawdzenie opieki na dzień | metody `JestOpiekunemNaDzis(...)`, `OpiekunowieWOkresie(...)` |
+| Podmiot nadrzędny / podrzędny | workery `NowyPodmiotNadrzednyWorker`, `NowyPodmiotPodrzednyWorker` |
+| Relacje podmiotów | `Podrzedni: SubTable`, `PodmiotNadrzedny: IPodmiot` |
+| Połącz / rozłącz | workery `PolaczKontrahentowWorker`, `RozlaczKontrahentaWorker` |
+
+**Pola i typy (`Opiekun`):** `Kontrahent: Kontrahent`, `Operator: Operator`, `Typ: TypOpiekuna`
+(`Glówny=0`, `Zastępca=1`), `Rola: RolaOpiekun`, `OddzialFirmy`, `DataOd: Date`, `DataDo: Date`,
+`Aktywny: bool`.
+
+**Snippet:**
+
+```csharp
+var crm = session.GetCRM();
+var k = crm.Kontrahenci.WgKodu["FIRMA001"];
+
+using (var t = session.Logout(editMode: true))
+{
+ var op = new Opiekun();
+ crm.Opiekunowie.AddRow(op);
+ op.Kontrahent = k;
+ op.Operator = oper; // Operator pobrany z modułu Business
+ op.Typ = TypOpiekuna.Glówny;
+ op.DataOd = Date.Today;
+ op.DataDo = Date.MaxValue;
+ op.Aktywny = true;
+ t.Commit();
+}
+session.Save();
+
+// Odczyt relacji podmiotów:
+foreach (RelacjaPodmiotu r in k.Podrzedni)
+{
+ // r.Nadrzedny, r.PowiazaniePodmiotu.Rola, r.PowiazaniePodmiotu.RodzajPowiazania
+}
+IPodmiot nadrzedny = k.PodmiotNadrzedny;
+```
+
+**Pułapki:**
+- `Opiekun.Operator` to rekord operatora (dane konfiguracyjne) — w kodzie biznesowym pobieraj go
+ spójnie z bieżącą sesją; nie mieszaj rekordów z różnych sesji (safe-code §2.1, użyj `session.Get(...)`).
+- Do sprawdzania opieki „na dziś"/„w okresie" używaj metod publicznych `JestOpiekunemNaDzis`,
+ `OpiekunowieWOkresie` zamiast ręcznego filtrowania dat.
+- Relacje podmiotów (nadrzędny/podrzędny, płatnik/odbiorca) zakładaj workerami
+ `NowyPodmiotNadrzednyWorker`/`NowyPodmiotPodrzednyWorker` — mają walidatory spójności.
+
+---
+
+## 8. Weryfikacja statusu
+
+### W15 — Weryfikacja VAT (GUS / MF / VIES)
+
+**Cel:** zweryfikować dane i status podatnika w rejestrach zewnętrznych. **Wszystkie operacje są
+online** (wymagają połączenia i bywają limitowane).
+
+**Warianty:**
+
+| Wariant | Worker (jednostkowo / masowo) | Wynik na kontrahencie |
+|---|---|---|
+| Dane z GUS-BIR (też PKD) | `DaneZGusBirWorker` / `DaneZGusBirMultipleWorker` | nazwa, adres, REGON, KRS, PKD |
+| Status MF / biała lista | `DaneZMfWorker`, `KontrahentBialaListaWorker` / `KontrahenciBialaListaWorker` | `AktualnyStatusVATMF` |
+| Status VIES | `DataFromViesWorker` / `KontrahenciDaneZViesWorker` | `AktualnyStatusVATVies` |
+| Historia statusu VAT | kolekcja `StatusyVAT: SubTable` | — |
+
+**Pola i typy (odczyt wyniku):** `AktualnyStatusVAT`, `AktualnyStatusVATMF`, `AktualnyStatusVATVies`
+(typ `Soneta.CRM.StatusNumeruVAT`, kalkulowane), `AktStatusVATData/DataMF/DataVIES: DateTime?`,
+`StatusyVAT: SubTable`.
+
+**Snippet:**
+
+```csharp
+var k = session.GetCRM().Kontrahenci.WgKodu["FIRMA001"];
+
+// Odczyt ostatnio zapisanych statusów (offline — bez sieci):
+StatusNumeruVAT statusMF = k.AktualnyStatusVATMF;
+StatusNumeruVAT statusVies = k.AktualnyStatusVATVies;
+DateTime? dataMF = k.AktStatusVATDataMF;
+
+// Historia statusów:
+foreach (StatusVAT s in k.StatusyVAT) { /* ... */ }
+
+// Weryfikacja online — przez worker (przykład: status MF):
+// var w = new DaneZMfWorker { Kontrahent = k, Context = context };
+// w.DaneZMf(); // WYMAGA SIECI — obuduj obsługą braku połączenia/limitów
+```
+
+**Pułapki:**
+- Operacje GUS/MF/VIES **wymagają sieci** — obuduj je obsługą błędów połączenia i limitów; **nie
+ testuj ich w testach jednostkowych** (zależność od usług zewnętrznych).
+- Status VAT z rejestru to dane „na dzień" — zapisuj datę weryfikacji (`AktStatusVATData*`).
+- W kodzie offline czytaj wyłącznie pola kalkulowane (`AktualnyStatusVAT*`) i historię `StatusyVAT`.
+- Nie loguj nadmiarowo numerów NIP/PESEL (safe-code §12).
+
+---
+
+## 9. RODO/GIODO i KSeF
+
+### W16 — RODO / GIODO
+
+**Cel:** obsłużyć zgody i wymianę danych osobowych kontrahenta.
+
+**Warianty:**
+
+| Wariant | Mechanizm / worker | Kolekcja |
+|---|---|---|
+| Oświadczenia | `KontrahentDodajOswiadczeniaWorker` | `GIODOOświadczenia: SubTable` |
+| Pozyskanie danych | `KontrahentDodajPozyskanieDanychWorker` | `GIODOUdostępnienia` |
+| Udostępnienie danych | `KontrahentDodajUdostepnienieDanychWorker` | `GIODOUdostępnienia` |
+| Powierzenie danych | `KontrahentDodajPowierzenieDanychWorker` | `GIODOUdostępnienia` |
+| Potwierdzenia zgodności | — | `PotwierdzeniaGIODO: SubTable` |
+
+**Pola i typy:** `GIODOOświadczenia: SubTable`,
+`GIODOUdostępnienia: SubTable`, `PotwierdzeniaGIODO: SubTable`,
+`ZgodnoscGIODOPotwierdzona: bool` (kalkulowane).
+
+**Snippet:**
+
+```csharp
+var k = session.GetCRM().Kontrahenci.WgKodu["FIRMA001"];
+
+// Odczyt oświadczeń RODO kontrahenta:
+foreach (GIODOOświadczenie o in k.GIODOOświadczenia)
+{
+ // o.* — definicja oświadczenia, okres obowiązywania, status zgody
+}
+
+// Dodawanie oświadczeń realizują workery RODO (dziedziczą po bazowych z Soneta.Core):
+// new KontrahentDodajOswiadczeniaWorker(...).DodajOświadczenia();
+```
+
+**Pułapki:**
+- Obowiązywanie zgody jest „na dzień" — czytaj okresy z rekordów `GIODOOświadczenie`, nie zakładaj
+ bezterminowości.
+- Dane osobowe (PESEL, e-mail osób) są wrażliwe — nie loguj ich (safe-code §12).
+- Workery RODO mają tryb `ConfirmSave` i wymagają praw do obszaru GIODO.
+
+### W17 — KSeF
+
+**Cel:** ustawić parametry KSeF kontrahenta.
+
+**Warianty:**
+
+| Wariant | Pole |
+|---|---|
+| Szablon pól opcjonalnych | `DomyslnySzablonPolOpcjonalnychKSeF: Soneta.Core.KSeFSzablonPolOpcjonalnych` |
+| Sposób wysyłki ceny | `KSeFSposobObslugiWysylkiCeny: Soneta.Core.SposobObslugiWysylkiCenyDoKSeF` |
+| Powiązanie z e-fakturą | `EFaktura`, `EFakturaOkres` (patrz W7) |
+
+**Pola i typy:** jak w tabeli powyżej (oba pola bazodanowe, zapisywalne).
+
+**Snippet:**
+
+```csharp
+var k = session.GetCRM().Kontrahenci.WgKodu["FIRMA001"];
+
+using (var t = session.Logout(editMode: true))
+{
+ k.KSeFSposobObslugiWysylkiCeny = SposobObslugiWysylkiCenyDoKSeF.CenaPoRabacie;
+ // k.DomyslnySzablonPolOpcjonalnychKSeF = ... // rekord szablonu z konfiguracji
+ t.Commit();
+}
+session.Save();
+```
+
+**Pułapki:**
+- `DomyslnySzablonPolOpcjonalnychKSeF` to referencja do rekordu konfiguracyjnego — pobierz istniejący
+ szablon, nie twórz „w locie".
+- Konfiguracja KSeF współgra z `EFaktura` (W7) — ustawiaj je spójnie.
+
+---
+
+## 10. Operacje masowe
+
+### W18 — Operacje na zbiorze kontrahentów
+
+**Cel:** wykonać operację na wielu kontrahentach efektywnie i bezpiecznie.
+
+**Warianty:**
+
+| Wariant | Mechanizm |
+|---|---|
+| Iteracja z warunkiem | serwerowy LINQ `crm.Kontrahenci[(Kontrahent k) => …]` (patrz [`rowcondition.md`](../rowcondition.md)) |
+| Masowa aktualizacja | jedna transakcja, paczki (patrz [`safe-code.md`](../safe-code.md)) |
+| Masowa zmiana formy prawnej | worker `ZmienFormePrawnaKontrahentowWorker` |
+| Masowe przypisanie kategorii | worker `KontrahenciPrzypiszKategorieWorker` |
+| Masowa weryfikacja VAT/VIES (online) | `KontrahenciBialaListaWorker`, `KontrahenciDaneZMfWorker`, `KontrahenciDaneZViesWorker` |
+| Eksport / import | datapack / business.xml (patrz [`datapack-guidedrow.md`](../datapack-guidedrow.md)) |
+
+**Snippet:**
+
+```csharp
+var crm = session.GetCRM();
+
+// Masowa zmiana: ustaw blokadę sprzedaży dla kontrahentów bez NIP — filtr serwerowy + 1 transakcja
+using (var t = session.Logout(editMode: true))
+{
+ foreach (Kontrahent k in crm.Kontrahenci.WgKodu[(Kontrahent k) =>
+ k.NIP == null && k.StatusPodmiotu == StatusPodmiotu.PodmiotGospodarczy])
+ {
+ k.BlokadaSprzedazy = true;
+ }
+ t.Commit();
+}
+session.Save();
+```
+
+**Pułapki:**
+- **Nie ładuj całej tabeli** do pamięci — filtr serwerowy (`SubTable[condition]`).
+- Duże operacje dziel na **paczki** (krótkie transakcje), by nie blokować innych użytkowników i nie
+ zwiększać ryzyka konfliktu optymistycznego (safe-code §13.1).
+- Workery masowe (`*Worker` na typie `Kontrahenci`) mają property `[Context] Kontrahent[]` —
+ przy użyciu programowym ustaw tablicę zaznaczonych rekordów.
+
+---
+
+## Powiązane dokumenty
+
+- [`safe-code.md`](../safe-code.md) — sesja, transakcje, blokada optymistyczna, zasady bezpiecznego kodu.
+- [`session-login.md`](../session-login.md) — `Session`, `Login`, `Database`.
+- [`worker-extender.md`](../worker-extender.md) — workery, akcje menu Czynności, bindowanie.
+- [`rowcondition.md`](../rowcondition.md) — serwerowy LINQ, `RowCondition`, `SubTable[condition]`.
+- [`datapack-guidedrow.md`](../datapack-guidedrow.md) — eksport/import, `GuidedRow`.
+- [`scan-props.md`](../scan-props.md) / [`scan-workers.md`](../scan-workers.md) — inwentaryzacja pól i workerów.