From 01de89b7b5d290bb03ffb1ddfe11cc435cbbbe53 Mon Sep 17 00:00:00 2001 From: Marcin Wojas Date: Fri, 5 Jun 2026 15:48:46 +0200 Subject: [PATCH] kontrahent.md --- .gitignore | 1 + .../CRM/Kontrahenci/AdresKontrahentaTest.cs | 64 ++ .../CRM/Kontrahenci/DaneKontaktoweTest.cs | 68 ++ .../KlasyfikacjaIPowiazaniaTest.cs | 47 + .../CRM/Kontrahenci/KontrahentTestBase.cs | 50 + .../Kontrahenci/ModyfikacjaIStatusyTest.cs | 50 + .../CRM/Kontrahenci/OsobyKontaktoweTest.cs | 44 + .../Kontrahenci/TworzenieKontrahentaTest.cs | 71 ++ .../Kontrahenci/UsuwanieKontrahentaTest.cs | 48 + .../CRM/Kontrahenci/WalidacjaNipRegonTest.cs | 56 + .../CRM/Kontrahenci/WarunkiPlatnosciTest.cs | 56 + .../WyszukiwanieKontrahentaTest.cs | 70 ++ Soneta.Skills.Test/Soneta.Skills.Test.csproj | 13 + soneta-programming/SKILL.md | 1 + .../references/domeny/kontrahent.md | 957 ++++++++++++++++++ 15 files changed, 1596 insertions(+) create mode 100644 Soneta.Skills.Test/CRM/Kontrahenci/AdresKontrahentaTest.cs create mode 100644 Soneta.Skills.Test/CRM/Kontrahenci/DaneKontaktoweTest.cs create mode 100644 Soneta.Skills.Test/CRM/Kontrahenci/KlasyfikacjaIPowiazaniaTest.cs create mode 100644 Soneta.Skills.Test/CRM/Kontrahenci/KontrahentTestBase.cs create mode 100644 Soneta.Skills.Test/CRM/Kontrahenci/ModyfikacjaIStatusyTest.cs create mode 100644 Soneta.Skills.Test/CRM/Kontrahenci/OsobyKontaktoweTest.cs create mode 100644 Soneta.Skills.Test/CRM/Kontrahenci/TworzenieKontrahentaTest.cs create mode 100644 Soneta.Skills.Test/CRM/Kontrahenci/UsuwanieKontrahentaTest.cs create mode 100644 Soneta.Skills.Test/CRM/Kontrahenci/WalidacjaNipRegonTest.cs create mode 100644 Soneta.Skills.Test/CRM/Kontrahenci/WarunkiPlatnosciTest.cs create mode 100644 Soneta.Skills.Test/CRM/Kontrahenci/WyszukiwanieKontrahentaTest.cs create mode 100644 Soneta.Skills.Test/Soneta.Skills.Test.csproj create mode 100644 soneta-programming/references/domeny/kontrahent.md 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.