Document de referinta end-to-end (replicabil in alt proiect): normalizare CUI (strip RO, typo OCR, checksum), client ANAF (v9/tva, contract tri-valent, cache), gate CUI, mod strict platitor/neplatitor TVA si procedurile Oracle cauta_partener_dupa_cod_fiscal / cauta_sau_creeaza_partener. Link in README. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
380 lines
17 KiB
Markdown
380 lines
17 KiB
Markdown
# Căutarea și selecția clientului după codul fiscal (cu verificare ANAF)
|
||
|
||
> Document de referință pentru replicarea logicii într-un alt proiect.
|
||
> Descrie **unde** se caută, **cum** se normalizează codul fiscal, **ce** se verifică pe ANAF
|
||
> și **procedurile** (Python + Oracle PL/SQL) implicate, end-to-end.
|
||
|
||
Sistemul curent: GoMag (web) → ROA Oracle. Partenerii sunt căutați/creați în tabela Oracle
|
||
`nom_parteneri`, iar codul fiscal este validat și corectat folosind webserviciul ANAF.
|
||
|
||
---
|
||
|
||
## 0. Fișiere sursă (în acest proiect)
|
||
|
||
| Componentă | Fișier | Funcții cheie |
|
||
|---|---|---|
|
||
| Validare/normalizare CUI + apel ANAF | `api/app/services/anaf_service.py` | `strip_ro_prefix`, `validate_cui`, `validate_cui_checksum`, `sanitize_cui`, `check_vat_status_batch`, `_call_anaf_api`, `determine_correct_cod_fiscal`, `normalize_company_name` |
|
||
| Extragere date partener din comandă | `api/app/services/import_service.py` | `determine_partner_data` |
|
||
| Orchestrare sync + gate CUI + override | `api/app/services/sync_service.py` | `evaluate_cui_gate`, secțiunea Step 4/5 (ANAF batch, anaf_strict, overrides) |
|
||
| Căutare/creare partener (Oracle) | `api/database-scripts/05_pack_import_parteneri.pck` | `cauta_partener_dupa_cod_fiscal`, `cauta_sau_creeaza_partener`, `cauta_partener_dupa_denumire` |
|
||
| Teste | `api/tests/test_cui_validation.py`, `test_partner_cui_lookup.py`, `test_partner_anaf_override.py`, `test_sync_cui_gate.py` | — |
|
||
|
||
---
|
||
|
||
## 1. Privire de ansamblu (flux complet)
|
||
|
||
```
|
||
Comandă web (JSON)
|
||
│
|
||
▼
|
||
determine_partner_data() # PJ vs PF, extrage cod_fiscal/denumire/registru
|
||
│
|
||
▼ (doar PJ romanesc cu cod_fiscal)
|
||
sanitize_cui() # scoate RO, corectează typo OCR, validează format+checksum
|
||
│
|
||
▼
|
||
check_vat_status_batch() → ANAF # POST batch la webserviciul ANAF (platitor TVA?)
|
||
│ răspuns: {scpTVA, denumire_anaf, checked_at} (cache 7 zile in SQLite)
|
||
▼
|
||
evaluate_cui_gate() # BLOCHEAZĂ comanda dacă: format invalid / checksum gresit / ANAF notFound
|
||
│ (ANAF down → tolerează, lasă să treacă)
|
||
▼
|
||
determine_correct_cod_fiscal() # aplică prefix RO dacă e plătitor TVA
|
||
anaf_strict = 1 # marchează că avem date ANAF → căutare STRICTĂ în Oracle
|
||
denumire_override = denumire_anaf # la CREARE partener nou folosește numele oficial ANAF
|
||
│
|
||
▼
|
||
PACK_IMPORT_PARTENERI.cauta_sau_creeaza_partener(cod_fiscal, denumire, registru, is_pj, anaf_strict, OUT id_part)
|
||
│
|
||
├─ STEP 1: cauta_partener_dupa_cod_fiscal() # prioritate 1
|
||
├─ STEP 2: cauta_partener_dupa_denumire() # prioritate 2 (sărit în mod strict)
|
||
├─ STEP 2b: permutări nume (doar PF, non-strict)
|
||
└─ STEP 3: creare partener nou (pack_def.adauga_partener)
|
||
```
|
||
|
||
Principiu central: **codul fiscal are prioritate** în identificarea partenerului; numele e doar
|
||
fallback. ANAF servește la (a) a decide forma corectă a CUI (cu/fără `RO`) și (b) a bloca CUI-uri
|
||
inexistente/invalide înainte de a polua baza cu parteneri greșiți.
|
||
|
||
---
|
||
|
||
## 2. Normalizarea codului fiscal (Python)
|
||
|
||
Fișier: `api/app/services/anaf_service.py`.
|
||
|
||
### 2.1 `strip_ro_prefix(cod_fiscal) -> str`
|
||
Aduce CUI-ul la forma „bare” (doar cifre):
|
||
1. `strip().upper()`
|
||
2. elimină prefixul `RO` cu spațiu opțional: `re.sub(r'^RO\s*', '', cleaned)`
|
||
3. corectează confuzii OCR frecvente: `O→0`, `I→1`, `L→1` (via `str.maketrans('OIL', '011')`)
|
||
|
||
```python
|
||
def strip_ro_prefix(cod_fiscal: str) -> str:
|
||
if not cod_fiscal:
|
||
return ""
|
||
cleaned = cod_fiscal.strip().upper()
|
||
cleaned = re.sub(r'^RO\s*', '', cleaned)
|
||
cleaned = cleaned.translate(str.maketrans('OIL', '011'))
|
||
return cleaned
|
||
```
|
||
|
||
În plus, la extragerea din comandă (`import_service.determine_partner_data`) se colapsează
|
||
**tot** spațiul intern: `cod_fiscal = re.sub(r'\s+', '', raw_cf)` → `"RO 34963277"` devine `"RO34963277"`.
|
||
|
||
### 2.2 `validate_cui(bare_cui) -> bool`
|
||
Format: doar cifre, lungime 2–10.
|
||
|
||
### 2.3 `validate_cui_checksum(bare_cui) -> bool` — algoritmul oficial românesc
|
||
Cheia de control: `[7, 5, 3, 2, 1, 7, 5, 3, 2]` (9 ponderi).
|
||
1. ultima cifră = cifra de control; corpul = restul cifrelor
|
||
2. corpul se aliniază la dreapta pe 9 poziții (padding cu 0 la stânga)
|
||
3. `total = Σ(cifra_i * pondere_i)`
|
||
4. `result = (total * 10) % 11`; dacă `result == 10` → `result = 0`
|
||
5. valid dacă `result == cifra_de_control`
|
||
|
||
```python
|
||
_CUI_KEY = [7, 5, 3, 2, 1, 7, 5, 3, 2]
|
||
|
||
def validate_cui_checksum(bare_cui: str) -> bool:
|
||
if not validate_cui(bare_cui):
|
||
return False
|
||
digits = [int(d) for d in bare_cui]
|
||
check_digit = digits[-1]
|
||
body = digits[:-1]
|
||
padded = [0] * (9 - len(body)) + body
|
||
total = sum(d * k for d, k in zip(padded, _CUI_KEY))
|
||
result = (total * 10) % 11
|
||
if result == 10:
|
||
result = 0
|
||
return result == check_digit
|
||
```
|
||
|
||
### 2.4 `sanitize_cui(raw_cf) -> (bare_cui, warning|None)`
|
||
Combină normalizarea + validarea și produce avertisment, fără a arunca excepție:
|
||
- strip RO + typo fix
|
||
- dacă format **și** checksum OK → `(bare, None)`
|
||
- dacă format OK dar checksum greșit → `(bare, "CUI ... nu trece verificarea cifrei de control")`
|
||
- dacă nici format → `(bare, "CUI ... conține caractere invalide...")`
|
||
|
||
### 2.5 `determine_correct_cod_fiscal(bare_cui, is_vat_payer) -> str`
|
||
Decide forma corectă pe baza statusului TVA ANAF:
|
||
- `is_vat_payer is True` → `"RO" + bare`
|
||
- `False` sau `None` → `bare` (conservator)
|
||
|
||
### 2.6 `normalize_company_name(name)` (auxiliar, comparare nume firme)
|
||
Uppercase, scoate diacritice, elimină forme juridice (`SRL/SA/SC/SNC/PFA/II/Întreprindere Individuală`),
|
||
elimină punctuația, colapsează spațiile. Util pentru comparații nume, nu pentru lookup-ul principal.
|
||
|
||
---
|
||
|
||
## 3. Ce se verifică pe ANAF și cum
|
||
|
||
Fișier: `api/app/services/anaf_service.py`.
|
||
|
||
### 3.1 Endpoint
|
||
```
|
||
POST https://webservicesp.anaf.ro/api/PlatitorTvaRest/v9/tva
|
||
Content-Type: application/json
|
||
Body: [{"cui": <int>, "data": "YYYY-MM-DD"}, ...] # batch, max 500 / cerere
|
||
```
|
||
|
||
### 3.2 Câmpuri citite din răspuns
|
||
Pentru fiecare CUI din lista `found`:
|
||
- `inregistrare_scop_Tva.scpTVA` → **plătitor TVA?** (bool)
|
||
- `date_generale.denumire` → **denumire oficială ANAF** (`denumire_anaf`)
|
||
- `date_generale.cui` → CUI (cheia rezultatului)
|
||
|
||
Plus `checked_at` (timestamp local), folosit pentru validitatea cache-ului.
|
||
|
||
CUI-urile din lista `notFound` (ANAF le întoarce ca **întregi simpli**, nu obiecte) primesc
|
||
`{scpTVA: None, denumire_anaf: ""}`.
|
||
|
||
### 3.3 Contractul de date ANAF (foarte important)
|
||
Restul sistemului interpretează rezultatul ANAF strict astfel:
|
||
|
||
| Rezultat | Semnificație | Acțiune |
|
||
|---|---|---|
|
||
| `None` (lipsă din dict / batch eșuat) | ANAF down / eroare tranzitorie | **tolerează** (lasă să treacă) |
|
||
| `{scpTVA: None, denumire_anaf: ""}` | ANAF notFound explicit | **blochează** comanda |
|
||
| `{scpTVA: bool, denumire_anaf: str}` | ANAF found | **trece** |
|
||
|
||
> Dacă schimbi această semantică în clientul ANAF, trebuie actualizat și `evaluate_cui_gate`.
|
||
|
||
### 3.4 Robustețe apel (`_call_anaf_api`)
|
||
- timeout 10s
|
||
- `429` → 1 retry după 10s, apoi renunță (`{}`)
|
||
- `5xx` → 1 retry după 3s, apoi renunță (`{}`)
|
||
- `4xx` → fail-fast, **fără** retry (`{}`)
|
||
- timeout / excepție → 1 retry după 3s
|
||
- „renunță” înseamnă întoarce `{}` ⇒ tratat ca *transient* ⇒ comanda **nu** e blocată
|
||
|
||
### 3.5 Cache & batching (orchestrare în `sync_service`)
|
||
- se adună CUI-urile firmelor **RO** din comenzile importabile (`sanitize_cui` + `validate_cui`)
|
||
- se verifică cache-ul SQLite (`anaf_cache`, valabilitate 7 zile); doar CUI-urile necacheate intră în batch
|
||
- pre-populare: la fiecare sync se reîmprospătează CUI-urile din ultimele 3 luni cu cache expirat
|
||
- rezultatele batch se scriu în cache (`bulk_populate_anaf_cache`)
|
||
|
||
---
|
||
|
||
## 4. „Gate”-ul CUI — când se blochează comanda
|
||
|
||
Fișier: `api/app/services/sync_service.py`, funcția `evaluate_cui_gate`.
|
||
|
||
```python
|
||
def evaluate_cui_gate(is_ro_company, company_code_raw, bare_cui, anaf_data) -> str | None:
|
||
if not is_ro_company or not company_code_raw:
|
||
return None # PF sau firmă non-RO → trece
|
||
if not anaf_service.validate_cui(bare_cui):
|
||
return f"CUI invalid (format): {company_code_raw!r}"
|
||
if not anaf_service.validate_cui_checksum(bare_cui):
|
||
return f"CUI invalid (cifra de control): {bare_cui}"
|
||
if (anaf_data is not None
|
||
and anaf_data.get("scpTVA") is None
|
||
and not (anaf_data.get("denumire_anaf") or "").strip()):
|
||
return f"CUI {company_code_raw!r} ... nu exista in registrul ANAF ..."
|
||
return None # altfel trece
|
||
```
|
||
|
||
Reguli rezumate:
|
||
- **PF** sau **firmă non-RO** → trece necondiționat (fără verificare ANAF/CUI)
|
||
- **PJ RO**: blochează la format invalid, checksum greșit, sau ANAF notFound explicit
|
||
- **ANAF down** (`anaf_data is None`) → trece (toleranță, ca să nu blocăm tot importul când ANAF pică)
|
||
|
||
La blocare se scrie o comandă cu status `ERROR` (nu se creează partener). Există un prag
|
||
de siguranță: după >10 erori, importul se oprește.
|
||
|
||
---
|
||
|
||
## 5. Determinarea modului de căutare și a override-urilor (Python)
|
||
|
||
Fișier: `api/app/services/sync_service.py` (în bucla de import, Step 5).
|
||
|
||
Pentru fiecare comandă PJ RO cu date ANAF valide (`scpTVA is not None`):
|
||
1. **`cod_fiscal_override`** = `determine_correct_cod_fiscal(bare_cui, scpTVA)` — corectează forma `RO`/bare
|
||
2. **`anaf_strict = 1`** — semnal pentru Oracle că avem certitudine ANAF ⇒ căutare strictă
|
||
3. **`denumire_override`** = `denumire_anaf` (uppercase, doar dacă non-gol) — folosit **doar la creare** partener nou
|
||
|
||
Aceste valori se transmit către `import_single_order(...)`, care apoi cheamă procedura Oracle.
|
||
|
||
`anaf_strict` rămâne `None` (fallback non-strict) când: firmă non-RO, fără date ANAF, sau ANAF down.
|
||
|
||
---
|
||
|
||
## 6. Căutarea/selecția în Oracle
|
||
|
||
Fișier: `api/database-scripts/05_pack_import_parteneri.pck`.
|
||
|
||
### 6.1 `cauta_partener_dupa_cod_fiscal(p_cod_fiscal, p_strict_search) RETURN NUMBER`
|
||
|
||
Tabela: `nom_parteneri`, coloana `cod_fiscal`. Întotdeauna filtrează `NVL(sters,0)=0`
|
||
(exclude șterse) și preferă activii (`NVL(inactiv,0) ASC`), apoi `id_part DESC` (cel mai nou).
|
||
|
||
Pași comuni:
|
||
1. `RETURN NULL` dacă `p_cod_fiscal` e NULL sau mai scurt de `C_MIN_COD_FISCAL`
|
||
2. `v_cod_fiscal_curat := UPPER(TRIM(...))`
|
||
3. extrage `v_bare_cui` (fără RO) cu `REGEXP_LIKE(..., '^RO\s*\d')` + `REGEXP_REPLACE`
|
||
4. `v_ro_cui := 'RO' || v_bare_cui`
|
||
|
||
**Mod STRICT (`p_strict_search = 1`)** — diferențiază plătitor vs neplătitor TVA, **fără cross-match**
|
||
(sunt entități fiscale distincte):
|
||
- input cu prefix RO (`^RO\s*\d`, plătitor TVA) → caută **doar** `RO<bare>` și `RO <bare>` (cu spațiu)
|
||
- input bare (neplătitor) → caută **doar** `<bare>`
|
||
|
||
**Mod NON-STRICT (`p_strict_search IS NULL`)** — anti-duplicare, caută toate cele 3 forme
|
||
(`bare`, `RO<bare>`, `RO <bare>`), cu prioritate pe forma exactă a input-ului.
|
||
|
||
```sql
|
||
FUNCTION cauta_partener_dupa_cod_fiscal(p_cod_fiscal IN VARCHAR2,
|
||
p_strict_search IN NUMBER DEFAULT NULL)
|
||
RETURN NUMBER IS
|
||
v_id_part NUMBER; v_cod_fiscal_curat VARCHAR2(50);
|
||
v_bare_cui VARCHAR2(50); v_ro_cui VARCHAR2(52);
|
||
BEGIN
|
||
IF p_cod_fiscal IS NULL OR LENGTH(TRIM(p_cod_fiscal)) < C_MIN_COD_FISCAL THEN
|
||
RETURN NULL;
|
||
END IF;
|
||
v_cod_fiscal_curat := UPPER(TRIM(p_cod_fiscal));
|
||
IF REGEXP_LIKE(v_cod_fiscal_curat, '^RO\s*\d') THEN
|
||
v_bare_cui := TRIM(REGEXP_REPLACE(v_cod_fiscal_curat, '^RO\s*', ''));
|
||
ELSE
|
||
v_bare_cui := v_cod_fiscal_curat;
|
||
END IF;
|
||
v_ro_cui := 'RO' || v_bare_cui;
|
||
BEGIN
|
||
IF p_strict_search = 1 THEN
|
||
IF REGEXP_LIKE(v_cod_fiscal_curat, '^RO\s*\d') THEN
|
||
SELECT id_part INTO v_id_part FROM (
|
||
SELECT id_part FROM nom_parteneri
|
||
WHERE UPPER(TRIM(cod_fiscal)) IN (v_ro_cui, 'RO ' || v_bare_cui)
|
||
AND NVL(sters, 0) = 0
|
||
ORDER BY NVL(inactiv, 0) ASC, id_part DESC
|
||
) WHERE ROWNUM = 1;
|
||
ELSE
|
||
SELECT id_part INTO v_id_part FROM (
|
||
SELECT id_part FROM nom_parteneri
|
||
WHERE UPPER(TRIM(cod_fiscal)) = v_bare_cui
|
||
AND NVL(sters, 0) = 0
|
||
ORDER BY NVL(inactiv, 0) ASC, id_part DESC
|
||
) WHERE ROWNUM = 1;
|
||
END IF;
|
||
ELSE
|
||
SELECT id_part INTO v_id_part FROM (
|
||
SELECT id_part FROM nom_parteneri
|
||
WHERE UPPER(TRIM(cod_fiscal)) IN (v_bare_cui, v_ro_cui, 'RO ' || v_bare_cui)
|
||
AND NVL(sters, 0) = 0
|
||
ORDER BY NVL(inactiv, 0) ASC,
|
||
CASE WHEN UPPER(TRIM(cod_fiscal)) = v_cod_fiscal_curat THEN 0 ELSE 1 END ASC,
|
||
id_part DESC
|
||
) WHERE ROWNUM = 1;
|
||
END IF;
|
||
RETURN v_id_part;
|
||
EXCEPTION WHEN NO_DATA_FOUND THEN RETURN NULL;
|
||
END;
|
||
EXCEPTION WHEN OTHERS THEN
|
||
pINFO('ERROR in cauta_partener_dupa_cod_fiscal: ' || SQLERRM, 'IMPORT_PARTENERI');
|
||
RAISE;
|
||
END;
|
||
```
|
||
|
||
### 6.2 `cauta_sau_creeaza_partener(...)` — orchestrarea selecției/creării
|
||
|
||
Semnătură:
|
||
```sql
|
||
PROCEDURE cauta_sau_creeaza_partener(
|
||
p_cod_fiscal IN VARCHAR2,
|
||
p_denumire IN VARCHAR2,
|
||
p_registru IN VARCHAR2,
|
||
p_is_persoana_juridica IN NUMBER DEFAULT NULL, -- 1=PJ, 0=PF, NULL=auto-detect CNP
|
||
p_strict_search IN NUMBER DEFAULT NULL, -- 1=avem date ANAF
|
||
p_id_partener OUT NUMBER) -- >0 ok, -1 eroare
|
||
```
|
||
|
||
Ordinea priorităților:
|
||
- **STEP 1 — după cod fiscal** (prioritate maximă): `cauta_partener_dupa_cod_fiscal(cod, strict)`.
|
||
Dacă găsește → return.
|
||
- **STEP 2 — după denumire exactă**: `cauta_partener_dupa_denumire` (match uppercase exact).
|
||
**Sărit** când `p_strict_search` e setat (în mod ANAF strict vrem partener nou cu CUI corect,
|
||
nu unul vechi găsit doar după nume).
|
||
- **STEP 2b — permutări nume** (doar PF, doar non-strict): rezolvă inversarea nume/prenume din web.
|
||
2 cuvinte → încearcă `W2 W1`; 3 cuvinte → încearcă celelalte 5 permutări.
|
||
- **STEP 3 — creare partener nou** (`pack_def.adauga_partener`):
|
||
- tip: parametru explicit `p_is_persoana_juridica` are prioritate; altfel auto-detect prin CNP (13 cifre)
|
||
- PF: `separa_nume_prenume`, `tnTip_persoana => 2`
|
||
- PJ: `tnTip_persoana => 1`
|
||
- denumirea se trece prin `strip_diacritics` înainte de stocare
|
||
- return `-1` la eșec
|
||
|
||
### 6.3 `cauta_partener_dupa_denumire(p_denumire)`
|
||
Match exact pe denumire normalizată (`UPPER(TRIM(...))`). Folosit ca fallback secundar și pentru permutările PF.
|
||
|
||
---
|
||
|
||
## 7. PJ vs PF: cum se decide tipul și ce date se folosesc
|
||
|
||
Fișier: `api/app/services/import_service.py`, `determine_partner_data`.
|
||
|
||
- **PJ (persoană juridică)** — când comanda are flag `billing.is_company`:
|
||
- `denumire` = numele firmei (curățat, uppercase); fallback la numele persoanei din billing dacă lipsește
|
||
- `cod_fiscal` = codul firmei cu spațiul colapsat (`re.sub(r'\s+','',...)`)
|
||
- `registru` = registrul comerțului
|
||
- `is_pj = 1`
|
||
- **PF (persoană fizică)** — altfel:
|
||
- `denumire` = numele din **shipping** (fallback billing), cuvinte **sortate alfabetic**
|
||
(`" ".join(sorted(...))`) — stabilizează ordinea pentru match
|
||
- `cod_fiscal = None`, `registru = None`, `is_pj = 0`
|
||
|
||
La creare partener **nou PJ** se preferă numele oficial ANAF (`denumire_override`) în locul numelui
|
||
din web (care poate avea typo-uri). Partenerii **existenți nu sunt modificați**.
|
||
|
||
---
|
||
|
||
## 8. Exemple (input → comportament)
|
||
|
||
| Input cod_fiscal | scpTVA (ANAF) | anaf_strict | Caută în `nom_parteneri` |
|
||
|---|---|---|---|
|
||
| `"RO 34963277"` | True | 1 | `RO34963277`, `RO 34963277` (doar formele RO) |
|
||
| `"34963277"` (plătitor) | True | 1 | corectat → `RO34963277` (override), apoi formele RO |
|
||
| `"34963277"` (neplătitor) | False | 1 | doar `34963277` (bare) |
|
||
| `"34963277"` | — (ANAF down) | NULL | toate 3 formele (anti-dedup) |
|
||
| `"RO123"` strict | — | 1 | nu face match pe `123` bare (entități distincte) |
|
||
| CUI checksum greșit | — | — | **BLOCAT** la gate (status ERROR) |
|
||
| CUI inexistent ANAF | None+gol | — | **BLOCAT** la gate |
|
||
| PF (fără cod) | — | — | căutare după nume (+permutări), apoi creare PF |
|
||
|
||
---
|
||
|
||
## 9. Checklist de replicare în alt proiect
|
||
|
||
1. **Normalizare CUI**: strip `RO` (cu spațiu opțional), fix typo OCR (`O→0,I→1,L→1`), colapsare spații.
|
||
2. **Validare**: format (2–10 cifre) + checksum românesc (cheia `7,5,3,2,1,7,5,3,2`, `(Σ*10)%11`, 10→0).
|
||
3. **Client ANAF**: POST batch (≤500) la `v9/tva`, citește `scpTVA` + `denumire`; respectă contractul
|
||
tri-valent (None=down→tolerează, notFound→blochează, found→trece); retry doar pe 429/5xx/timeout.
|
||
4. **Cache**: stochează rezultatele ANAF (ex. 7 zile) ca să nu reinteroghezi la fiecare rulare.
|
||
5. **Gate**: blochează doar PJ RO la format/checksum/notFound; PF și ANAF-down trec.
|
||
6. **Forma corectă CUI**: aplică `RO` doar dacă `scpTVA = True`.
|
||
7. **Căutare DB**: prioritate cod fiscal → nume exact → (PF) permutări nume → creare.
|
||
În mod strict (date ANAF), separă plătitor (`RO...`) de neplătitor (`bare`) **fără cross-match**;
|
||
în mod non-strict, caută toate formele cu prioritate pe forma exactă.
|
||
8. **Creare partener nou**: nume oficial ANAF pentru PJ; nu atinge partenerii existenți.
|
||
9. **Excluderi**: ignoră înregistrările șterse; preferă active + cea mai nouă la ambiguitate.
|