kontrahent.md

This commit is contained in:
Marcin Wojas
2026-06-05 15:48:46 +02:00
parent 67fcc9e996
commit 01de89b7b5
15 changed files with 1596 additions and 0 deletions
+1
View File
@@ -1,3 +1,4 @@
**-workspace/**
obj/
.DS_Store
.idea
@@ -0,0 +1,64 @@
using AwesomeAssertions;
using NUnit.Framework;
namespace Soneta.Skills.Test.CRM.Kontrahenci;
/// <summary>
/// W6 — Adres kontrahenta.
/// Test pokazuje, że <c>Adres</c> to property zwracająca obiekt złożony (nie da się przypisać
/// całego adresu) — modyfikujemy jego pola. Uwaga na typ <c>KodPocztowy</c> = <c>int</c>
/// (do formatu „00-000" służy <c>KodPocztowyS</c>).
/// </summary>
[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");
}
}
@@ -0,0 +1,68 @@
using System.Linq;
using AwesomeAssertions;
using NUnit.Framework;
using Soneta.Core;
namespace Soneta.Skills.Test.CRM.Kontrahenci;
/// <summary>
/// W7 — Dane kontaktowe i adresy WWW.
/// Testy pokazują dodanie kanału e-mail do kolekcji <c>Kontakty</c> (typ rodzaju pobierany ze
/// słownika <c>RodzajeKontaktow</c>) oraz dodanie adresu WWW (konstruktor z hostem
/// <c>new AdresWWW(kontrahent)</c>, pole URL nazywa się <c>Adres</c>).
/// </summary>
[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<DaneKontaktowe>()
.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<AdresWWW>()
.Any(w => w.Adres == url)
.Should().BeTrue();
}
}
@@ -0,0 +1,47 @@
using System.Linq;
using AwesomeAssertions;
using NUnit.Framework;
using Soneta.CRM;
namespace Soneta.Skills.Test.CRM.Kontrahenci;
/// <summary>
/// W13/W14 — Klasyfikacja i powiązania (odczyt kontraktu publicznego).
/// Testy dokumentują dostęp do kolekcji klasyfikacyjnych (<c>Kategorie</c>, <c>Branze</c>,
/// <c>Features</c>) oraz powiązań (<c>Opiekunowie</c>, <c>Podrzedni</c>, <c>PodmiotNadrzedny</c>).
/// Świeżo utworzony, samodzielny kontrahent ma te kolekcje puste i brak podmiotu nadrzędnego —
/// co czyni asercje deterministycznymi.
/// </summary>
[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<KategoriaKth>().Should().BeEmpty();
k.Branze.Cast<BranzaKth>().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<Opiekun>().Should().BeEmpty();
k.Podrzedni.Cast<RelacjaPodmiotu>().Should().BeEmpty();
k.PodmiotNadrzedny.Should().BeNull();
}
}
@@ -0,0 +1,50 @@
using System;
using Soneta.Core;
using Soneta.CRM;
using Soneta.Test;
namespace Soneta.Skills.Test.CRM.Kontrahenci;
/// <summary>
/// Wspólna baza testów kontrahenta. Dziedziczy z <see cref="TestBase"/>, dzięki czemu:
/// <list type="bullet">
/// <item>udostępnia gotową sesję operacyjną (<c>Session</c>) powiązaną z testową bazą Demo,</item>
/// <item>automatycznie wycofuje (rollback) wszystkie zmiany w bazie po zakończeniu testu,</item>
/// <item>daje metody pomocnicze <c>InTransaction</c>/<c>SaveDispose</c> do pracy w transakcjach.</item>
/// </list>
/// Baza dodaje skróty często powtarzane w testach kontrahenta (dostęp do modułu CRM,
/// generowanie unikalnego kodu, utworzenie minimalnego kontrahenta).
/// </summary>
public abstract class KontrahentTestBase : TestBase
{
/// <summary>Moduł CRM bieżącej sesji operacyjnej.</summary>
protected CRMModule Crm => Session.GetCRM();
/// <summary>Generuje krótki, unikalny kod kontrahenta (na potrzeby testów).</summary>
protected static string UnikalnyKod() => Guid.NewGuid().ToString("N").Substring(0, 10);
/// <summary>
/// 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 <c>SaveDispose</c>.
/// </summary>
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;
}
}
@@ -0,0 +1,50 @@
using AwesomeAssertions;
using NUnit.Framework;
using Soneta.CRM;
namespace Soneta.Skills.Test.CRM.Kontrahenci;
/// <summary>
/// W4 — Modyfikacja danych i statusów kontrahenta.
/// Testy pokazują zmianę nazwy oraz ustawienie statusów dostępności/handlowych:
/// <c>Blokada</c> (ukrycie na listach) i <c>BlokadaSprzedazy</c> (zakaz dokumentów rozchodu).
/// </summary>
[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();
}
}
@@ -0,0 +1,44 @@
using System.Linq;
using AwesomeAssertions;
using NUnit.Framework;
using Soneta.CRM;
namespace Soneta.Skills.Test.CRM.Kontrahenci;
/// <summary>
/// W8 — Osoby kontaktowe.
/// Test pokazuje dodanie osoby kontaktowej i powiązanie jej z kontrahentem przez
/// <c>KontaktOsoba.Kontrahent</c> — osoba pojawia się wtedy w kolekcji <c>Osoby</c> kontrahenta.
/// </summary>
[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<KontaktOsoba>()
.Any(o => o.Nazwisko == "Nowak" && o.Imie == "Anna")
.Should().BeTrue();
}
}
@@ -0,0 +1,71 @@
using AwesomeAssertions;
using NUnit.Framework;
using Soneta.Core;
using Soneta.CRM;
namespace Soneta.Skills.Test.CRM.Kontrahenci;
/// <summary>
/// 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).
/// </summary>
[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);
}
}
@@ -0,0 +1,48 @@
using AwesomeAssertions;
using NUnit.Framework;
namespace Soneta.Skills.Test.CRM.Kontrahenci;
/// <summary>
/// W5 — Bezpieczne usuwanie kontrahenta.
/// Test pokazuje czyste usunięcie świeżo utworzonego rekordu (brak powiązań) oraz alternatywę
/// „miękkiego" wycofania (<c>Blokada=true</c>), zalecaną gdy istnieją dokumenty/rozrachunki.
/// </summary>
[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();
}
}
@@ -0,0 +1,56 @@
using AwesomeAssertions;
using NUnit.Framework;
using Soneta.Core;
namespace Soneta.Skills.Test.CRM.Kontrahenci;
/// <summary>
/// W2 — Walidacja NIP / REGON / EU VAT przed zapisem.
/// Testy weryfikują publiczne, statyczne walidatory z <c>Soneta.Core</c>
/// (<see cref="Nip"/>, <see cref="Regon"/>, <see cref="EuVat"/>) oraz normalizację numerów.
/// Walidatory sprawdzają format i sumę kontrolną — to NIE jest weryfikacja w MF/VIES (patrz W15).
/// </summary>
[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");
}
}
@@ -0,0 +1,56 @@
using AwesomeAssertions;
using NUnit.Framework;
using Soneta.CRM;
using Soneta.Kasa;
namespace Soneta.Skills.Test.CRM.Kontrahenci;
/// <summary>
/// W9 — Warunki płatności i limity kredytowe.
/// Testy pokazują ustawienie sposobu zapłaty (rekord <c>FormaPlatnosci</c> z modułu Kasa),
/// terminu płatności oraz typu limitu kredytowego. Pola kalkulowane (np.
/// <c>LimitNieograniczony</c>) są tylko do odczytu i wynikają z ustawień.
/// </summary>
[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)
}
}
@@ -0,0 +1,70 @@
using System.Linq;
using AwesomeAssertions;
using NUnit.Framework;
using Soneta.Core;
using Soneta.CRM;
namespace Soneta.Skills.Test.CRM.Kontrahenci;
/// <summary>
/// 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
/// <c>SubTable[condition]</c>, zamiast iteracji całej tabeli w pamięci).
/// </summary>
[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();
}
}
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>$(SonetaTargetFramework)</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Soneta.Business\Soneta.Business.csproj" />
<ProjectReference Include="..\Soneta.Core\Soneta.Core.csproj" />
<ProjectReference Include="..\Soneta.CRM\Soneta.CRM.csproj" />
<ProjectReference Include="..\Soneta.Kasa\Soneta.Kasa.csproj" />
<ProjectReference Include="..\Soneta.Test\Soneta.Test.csproj" />
<ProjectReference Include="..\Soneta.Types\Soneta.Types.csproj" />
</ItemGroup>
</Project>
+1
View File
@@ -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) |
@@ -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<KontaktOsoba>`), `Kontakty` (`SubTable<DaneKontaktowe>`),
`AdresyWWW` (`SubTable<AdresWWW>`), `Kategorie` (`SubTable<KategoriaKth>`),
`Branze` (`SubTable<BranzaKth>`), `Opiekunowie` (`SubTable<Opiekun>`),
`Rachunki` (`SubTable<Soneta.Kasa.RachunekBankowyPodmiotu>`),
`Rozrachunki` (`SubTable<Soneta.Kasa.RozrachunekIdx>`), `Podrzedni` (`SubTable<RelacjaPodmiotu>`),
`StatusyVAT` (`SubTable<StatusVAT>`), `KodyKreskowe` (`SubTable<KodKreskowy>`),
`GIODOOświadczenia` (`SubTable<GIODOOświadczenie>`), `GIODOUdostępnienia` (`SubTable<GIODOWymianaDanych>`),
`PotwierdzeniaGIODO` (`SubTable<GIODOZgodny>`).
- **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<DaneKontaktowe>` (`Rodzaj`, `Kontakt`, `Domyslny`) |
| Adresy WWW | `AdresyWWW: SubTable<AdresWWW>` (`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<KontaktOsoba>` (`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<RachunekBankowyPodmiotu>`, `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`
**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<RachunekBankowyPodmiotu>` |
**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<Soneta.Kasa.RozrachunekIdx>` |
| Płatności / zapłaty | `Platnosci: SubTable<Platnosc>`, `Zaplaty: SubTable<Zaplata>` |
| Dokumenty rozliczeniowe | `DokumentyRozliczeniowe: SubTable<DokRozliczBase>` |
| Przelewy | `Przelewy: SubTable<PrzelewBase>` |
**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<DokEwidencji>` |
| 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<KategoriaKth>` (poj. lub worker `KontrahenciPrzypiszKategorieWorker`) |
| Branże | `Branze: SubTable<BranzaKth>` |
| 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<KategoriaKth>`, `Branze: SubTable<BranzaKth>`.
`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<Opiekun>`, worker `UstawOpiekunaGlownegoWorker` |
| Sprawdzenie opieki na dzień | metody `JestOpiekunemNaDzis(...)`, `OpiekunowieWOkresie(...)` |
| Podmiot nadrzędny / podrzędny | workery `NowyPodmiotNadrzednyWorker`, `NowyPodmiotPodrzednyWorker` |
| Relacje podmiotów | `Podrzedni: SubTable<RelacjaPodmiotu>`, `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<StatusVAT>` | — |
**Pola i typy (odczyt wyniku):** `AktualnyStatusVAT`, `AktualnyStatusVATMF`, `AktualnyStatusVATVies`
(typ `Soneta.CRM.StatusNumeruVAT`, kalkulowane), `AktStatusVATData/DataMF/DataVIES: DateTime?`,
`StatusyVAT: SubTable<StatusVAT>`.
**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<GIODOOświadczenie>` |
| Pozyskanie danych | `KontrahentDodajPozyskanieDanychWorker` | `GIODOUdostępnienia` |
| Udostępnienie danych | `KontrahentDodajUdostepnienieDanychWorker` | `GIODOUdostępnienia` |
| Powierzenie danych | `KontrahentDodajPowierzenieDanychWorker` | `GIODOUdostępnienia` |
| Potwierdzenia zgodności | — | `PotwierdzeniaGIODO: SubTable<GIODOZgodny>` |
**Pola i typy:** `GIODOOświadczenia: SubTable<GIODOOświadczenie>`,
`GIODOUdostępnienia: SubTable<GIODOWymianaDanych>`, `PotwierdzeniaGIODO: SubTable<GIODOZgodny>`,
`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.