fix(import): resolve correct id_articol for duplicate CODMATs + gestiune setting

Unified id_articol selection logic in Python (resolve_codmat_ids) and PL/SQL
(resolve_id_articol): filters sters=0 AND inactiv=0, prefers article with
stock in configured gestiune, falls back to MAX(id_articol). Eliminates
mismatch where Python and PL/SQL could pick different id_articol for the
same CODMAT, causing ORA-20000 price-not-found errors.

- Add resolve_codmat_ids helper in validation_service.py (single batch query)
- Refactor validate_skus/validate_prices/ensure_prices to use it
- Add resolve_id_articol function in PL/SQL package body
- Add p_id_gestiune parameter to importa_comanda (spec + body)
- Add /api/settings/gestiuni endpoint and id_gestiune setting
- Add gestiune dropdown in settings UI

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-03-17 12:18:18 +00:00
parent 09a5403f83
commit 7a2408e310
7 changed files with 215 additions and 105 deletions

View File

@@ -33,6 +33,7 @@ class AppSettingsUpdate(BaseModel):
discount_id_pol: str = ""
id_pol: str = ""
id_sectie: str = ""
id_gestiune: str = ""
gomag_api_key: str = ""
gomag_api_shop: str = ""
gomag_order_days_back: str = "7"
@@ -597,6 +598,7 @@ async def get_app_settings():
"discount_id_pol": s.get("discount_id_pol", ""),
"id_pol": s.get("id_pol", ""),
"id_sectie": s.get("id_sectie", ""),
"id_gestiune": s.get("id_gestiune", ""),
"gomag_api_key": s.get("gomag_api_key", "") or config_settings.GOMAG_API_KEY,
"gomag_api_shop": s.get("gomag_api_shop", "") or config_settings.GOMAG_API_SHOP,
"gomag_order_days_back": s.get("gomag_order_days_back", "") or str(config_settings.GOMAG_ORDER_DAYS_BACK),
@@ -616,6 +618,7 @@ async def update_app_settings(config: AppSettingsUpdate):
await sqlite_service.set_app_setting("discount_id_pol", config.discount_id_pol)
await sqlite_service.set_app_setting("id_pol", config.id_pol)
await sqlite_service.set_app_setting("id_sectie", config.id_sectie)
await sqlite_service.set_app_setting("id_gestiune", config.id_gestiune)
await sqlite_service.set_app_setting("gomag_api_key", config.gomag_api_key)
await sqlite_service.set_app_setting("gomag_api_shop", config.gomag_api_shop)
await sqlite_service.set_app_setting("gomag_order_days_back", config.gomag_order_days_back)
@@ -624,6 +627,26 @@ async def update_app_settings(config: AppSettingsUpdate):
return {"success": True}
@router.get("/api/settings/gestiuni")
async def get_gestiuni():
"""Get list of warehouses from Oracle for dropdown."""
def _query():
conn = database.get_oracle_connection()
try:
with conn.cursor() as cur:
cur.execute(
"SELECT id_gestiune, nume_gestiune FROM nom_gestiuni WHERE sters=0 AND inactiv=0 ORDER BY id_gestiune"
)
return [{"id": str(row[0]), "label": f"{row[0]} - {row[1]}"} for row in cur]
finally:
database.pool.release(conn)
try:
return await asyncio.to_thread(_query)
except Exception as e:
logger.error(f"get_gestiuni error: {e}")
return []
@router.get("/api/settings/sectii")
async def get_sectii():
"""Get list of sections from Oracle for dropdown."""

View File

@@ -108,7 +108,7 @@ def build_articles_json(items, order=None, settings=None) -> str:
return json.dumps(articles)
def import_single_order(order, id_pol: int = None, id_sectie: int = None, app_settings: dict = None) -> dict:
def import_single_order(order, id_pol: int = None, id_sectie: int = None, app_settings: dict = None, id_gestiune: int = None) -> dict:
"""Import a single order into Oracle ROA.
Returns dict with:
@@ -255,6 +255,7 @@ def import_single_order(order, id_pol: int = None, id_sectie: int = None, app_se
addr_fact_id, # p_id_adresa_facturare
id_pol, # p_id_pol
id_sectie, # p_id_sectie
id_gestiune, # p_id_gestiune
id_comanda # v_id_comanda (OUT)
])

View File

@@ -334,9 +334,17 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
# (can happen if previous import committed partially without rollback)
await _fix_stale_error_orders(existing_map, run_id)
# Load app settings early (needed for id_gestiune in SKU validation)
app_settings = await sqlite_service.get_app_settings()
id_pol = id_pol or int(app_settings.get("id_pol") or 0) or settings.ID_POL
id_sectie = id_sectie or int(app_settings.get("id_sectie") or 0) or settings.ID_SECTIE
id_gestiune = int(app_settings.get("id_gestiune") or 0) or None # None = orice gestiune
logger.info(f"Sync params: ID_POL={id_pol}, ID_SECTIE={id_sectie}, ID_GESTIUNE={id_gestiune}")
_log_line(run_id, f"Parametri import: ID_POL={id_pol}, ID_SECTIE={id_sectie}, ID_GESTIUNE={id_gestiune}")
# Step 2b: Validate SKUs (reuse same connection)
all_skus = order_reader.get_all_skus(orders)
validation = await asyncio.to_thread(validation_service.validate_skus, all_skus, conn)
validation = await asyncio.to_thread(validation_service.validate_skus, all_skus, conn, id_gestiune)
importable, skipped = validation_service.classify_orders(orders, validation)
# ── Split importable into truly_importable vs already_in_roa ──
@@ -390,12 +398,6 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
)
# Step 2d: Pre-validate prices for importable articles
# Load app settings (for transport/discount CODMAT config AND id_pol/id_sectie override)
app_settings = await sqlite_service.get_app_settings()
id_pol = id_pol or int(app_settings.get("id_pol") or 0) or settings.ID_POL
id_sectie = id_sectie or int(app_settings.get("id_sectie") or 0) or settings.ID_SECTIE
logger.info(f"Sync params: ID_POL={id_pol}, ID_SECTIE={id_sectie}")
_log_line(run_id, f"Parametri import: ID_POL={id_pol}, ID_SECTIE={id_sectie}")
if id_pol and (truly_importable or already_in_roa):
_update_progress("validation", "Validating prices...", 0, len(truly_importable))
_log_line(run_id, "Validare preturi...")
@@ -505,7 +507,7 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
result = await asyncio.to_thread(
import_service.import_single_order,
order, id_pol=id_pol, id_sectie=id_sectie,
app_settings=app_settings
app_settings=app_settings, id_gestiune=id_gestiune
)
# Build order items data for storage (R9)

View File

@@ -1,5 +1,4 @@
import logging
from datetime import datetime, timedelta
from .. import database
logger = logging.getLogger(__name__)
@@ -29,7 +28,67 @@ def check_orders_in_roa(min_date, conn) -> dict:
return existing
def validate_skus(skus: set[str], conn=None) -> dict:
def resolve_codmat_ids(codmats: set[str], id_gestiune: int = None, conn=None) -> dict[str, int]:
"""Resolve CODMATs to best id_articol: prefers article with stock, then MAX(id_articol).
Filters: sters=0 AND inactiv=0.
Returns: {codmat: id_articol}
"""
if not codmats:
return {}
result = {}
codmat_list = list(codmats)
# Build stoc subquery dynamically for index optimization
if id_gestiune is not None:
stoc_filter = "AND s.id_gestiune = :id_gestiune"
else:
stoc_filter = ""
own_conn = conn is None
if own_conn:
conn = database.get_oracle_connection()
try:
with conn.cursor() as cur:
for i in range(0, len(codmat_list), 500):
batch = codmat_list[i:i+500]
placeholders = ",".join([f":c{j}" for j in range(len(batch))])
params = {f"c{j}": cm for j, cm in enumerate(batch)}
if id_gestiune is not None:
params["id_gestiune"] = id_gestiune
cur.execute(f"""
SELECT codmat, id_articol FROM (
SELECT na.codmat, na.id_articol,
ROW_NUMBER() OVER (
PARTITION BY na.codmat
ORDER BY
CASE WHEN EXISTS (
SELECT 1 FROM stoc s
WHERE s.id_articol = na.id_articol
{stoc_filter}
AND s.an = EXTRACT(YEAR FROM SYSDATE)
AND s.luna = EXTRACT(MONTH FROM SYSDATE)
AND s.cants + s.cant - s.cante > 0
) THEN 0 ELSE 1 END,
na.id_articol DESC
) AS rn
FROM nom_articole na
WHERE na.codmat IN ({placeholders})
AND na.sters = 0 AND na.inactiv = 0
) WHERE rn = 1
""", params)
for row in cur:
result[row[0]] = row[1]
finally:
if own_conn:
database.pool.release(conn)
logger.info(f"resolve_codmat_ids: {len(result)}/{len(codmats)} resolved (gestiune={id_gestiune})")
return result
def validate_skus(skus: set[str], conn=None, id_gestiune: int = None) -> dict:
"""Validate a set of SKUs against Oracle.
Returns: {mapped: set, direct: set, missing: set, direct_id_map: {codmat: id_articol}}
- mapped: found in ARTICOLE_TERTI (active)
@@ -41,8 +100,6 @@ def validate_skus(skus: set[str], conn=None) -> dict:
return {"mapped": set(), "direct": set(), "missing": set(), "direct_id_map": {}}
mapped = set()
direct = set()
direct_id_map = {}
sku_list = list(skus)
own_conn = conn is None
@@ -64,18 +121,14 @@ def validate_skus(skus: set[str], conn=None) -> dict:
for row in cur:
mapped.add(row[0])
# Check NOM_ARTICOLE for remaining — also fetch id_articol
remaining = [s for s in batch if s not in mapped]
if remaining:
placeholders2 = ",".join([f":n{j}" for j in range(len(remaining))])
params2 = {f"n{j}": sku for j, sku in enumerate(remaining)}
cur.execute(f"""
SELECT codmat, id_articol FROM NOM_ARTICOLE
WHERE codmat IN ({placeholders2}) AND sters = 0 AND inactiv = 0
""", params2)
for row in cur:
direct.add(row[0])
direct_id_map[row[0]] = row[1]
# Resolve remaining SKUs via resolve_codmat_ids (consistent id_articol selection)
all_remaining = [s for s in sku_list if s not in mapped]
if all_remaining:
direct_id_map = resolve_codmat_ids(set(all_remaining), id_gestiune, conn)
direct = set(direct_id_map.keys())
else:
direct_id_map = {}
direct = set()
finally:
if own_conn:
database.pool.release(conn)
@@ -113,37 +166,15 @@ def validate_prices(codmats: set[str], id_pol: int, conn=None, direct_id_map: di
if not codmats:
return {"has_price": set(), "missing_price": set()}
codmat_to_id = {}
codmat_to_id = dict(direct_id_map) if direct_id_map else {}
ids_with_price = set()
codmat_list = list(codmats)
# Pre-populate from direct_id_map if available
if direct_id_map:
for cm in codmat_list:
if cm in direct_id_map:
codmat_to_id[cm] = direct_id_map[cm]
own_conn = conn is None
if own_conn:
conn = database.get_oracle_connection()
try:
with conn.cursor() as cur:
# Step 1: Get ID_ARTICOL for CODMATs not already in direct_id_map
remaining = [cm for cm in codmat_list if cm not in codmat_to_id]
if remaining:
for i in range(0, len(remaining), 500):
batch = remaining[i:i+500]
placeholders = ",".join([f":c{j}" for j in range(len(batch))])
params = {f"c{j}": cm for j, cm in enumerate(batch)}
cur.execute(f"""
SELECT id_articol, codmat FROM NOM_ARTICOLE
WHERE codmat IN ({placeholders})
""", params)
for row in cur:
codmat_to_id[row[1]] = row[0]
# Step 2: Check which ID_ARTICOLs have a price in the policy
# Check which ID_ARTICOLs have a price in the policy
id_list = list(codmat_to_id.values())
for i in range(0, len(id_list), 500):
batch = id_list[i:i+500]
@@ -191,28 +222,10 @@ def ensure_prices(codmats: set[str], id_pol: int, conn=None, direct_id_map: dict
return
id_valuta = row[0]
# Build batch params using direct_id_map where available
# Build batch params using direct_id_map (already resolved via resolve_codmat_ids)
batch_params = []
need_lookup = []
codmat_id_map = dict(direct_id_map) if direct_id_map else {}
for codmat in codmats:
if codmat not in codmat_id_map:
need_lookup.append(codmat)
# Batch lookup remaining CODMATs
if need_lookup:
for i in range(0, len(need_lookup), 500):
batch = need_lookup[i:i+500]
placeholders = ",".join([f":c{j}" for j in range(len(batch))])
params = {f"c{j}": cm for j, cm in enumerate(batch)}
cur.execute(f"""
SELECT codmat, id_articol FROM NOM_ARTICOLE
WHERE codmat IN ({placeholders}) AND sters = 0 AND inactiv = 0
""", params)
for r in cur:
codmat_id_map[r[0]] = r[1]
for codmat in codmats:
id_articol = codmat_id_map.get(codmat)
if not id_articol:

View File

@@ -9,12 +9,22 @@ document.addEventListener('DOMContentLoaded', async () => {
async function loadDropdowns() {
try {
const [sectiiRes, politiciRes] = await Promise.all([
const [sectiiRes, politiciRes, gestiuniRes] = await Promise.all([
fetch('/api/settings/sectii'),
fetch('/api/settings/politici')
fetch('/api/settings/politici'),
fetch('/api/settings/gestiuni')
]);
const sectii = await sectiiRes.json();
const politici = await politiciRes.json();
const gestiuni = await gestiuniRes.json();
const gestiuneEl = document.getElementById('settIdGestiune');
if (gestiuneEl) {
gestiuneEl.innerHTML = '<option value="">— orice gestiune —</option>';
gestiuni.forEach(g => {
gestiuneEl.innerHTML += `<option value="${escHtml(g.id)}">${escHtml(g.label)}</option>`;
});
}
const sectieEl = document.getElementById('settIdSectie');
if (sectieEl) {
@@ -65,6 +75,7 @@ async function loadSettings() {
if (el('settDiscountIdPol')) el('settDiscountIdPol').value = data.discount_id_pol || '';
if (el('settIdPol')) el('settIdPol').value = data.id_pol || '';
if (el('settIdSectie')) el('settIdSectie').value = data.id_sectie || '';
if (el('settIdGestiune')) el('settIdGestiune').value = data.id_gestiune || '';
if (el('settGomagApiKey')) el('settGomagApiKey').value = data.gomag_api_key || '';
if (el('settGomagApiShop')) el('settGomagApiShop').value = data.gomag_api_shop || '';
if (el('settGomagDaysBack')) el('settGomagDaysBack').value = data.gomag_order_days_back || '7';
@@ -86,6 +97,7 @@ async function saveSettings() {
discount_id_pol: el('settDiscountIdPol')?.value?.trim() || '',
id_pol: el('settIdPol')?.value?.trim() || '',
id_sectie: el('settIdSectie')?.value?.trim() || '',
id_gestiune: el('settIdGestiune')?.value?.trim() || '',
gomag_api_key: el('settGomagApiKey')?.value?.trim() || '',
gomag_api_shop: el('settGomagApiShop')?.value?.trim() || '',
gomag_order_days_back: el('settGomagDaysBack')?.value?.trim() || '7',

View File

@@ -38,6 +38,12 @@
<div class="card h-100">
<div class="card-header py-2 px-3 fw-semibold">Import ROA</div>
<div class="card-body py-2 px-3">
<div class="mb-2">
<label class="form-label mb-0 small">Gestiune (ID_GESTIUNE)</label>
<select class="form-select form-select-sm" id="settIdGestiune">
<option value="">— orice gestiune —</option>
</select>
</div>
<div class="mb-2">
<label class="form-label mb-0 small">Secție (ID_SECTIE)</label>
<select class="form-select form-select-sm" id="settIdSectie">
@@ -146,5 +152,5 @@
{% endblock %}
{% block scripts %}
<script src="{{ request.scope.get('root_path', '') }}/static/js/settings.js?v=3"></script>
<script src="{{ request.scope.get('root_path', '') }}/static/js/settings.js?v=4"></script>
{% endblock %}

View File

@@ -61,6 +61,7 @@ CREATE OR REPLACE PACKAGE PACK_IMPORT_COMENZI AS
p_id_adresa_facturare IN NUMBER DEFAULT NULL,
p_id_pol IN NUMBER DEFAULT NULL,
p_id_sectie IN NUMBER DEFAULT NULL,
p_id_gestiune IN NUMBER DEFAULT NULL,
v_id_comanda OUT NUMBER);
-- Functii pentru managementul erorilor (pentru orchestrator VFP)
@@ -88,6 +89,56 @@ CREATE OR REPLACE PACKAGE BODY PACK_IMPORT_COMENZI AS
g_last_error := NULL;
END clear_error;
-- ================================================================
-- Functie helper: selecteaza id_articol corect pentru un CODMAT
-- Prioritate: sters=0 AND inactiv=0, preferinta stoc, MAX(id_articol) fallback
-- ================================================================
FUNCTION resolve_id_articol(p_codmat IN VARCHAR2, p_id_gest IN NUMBER) RETURN NUMBER IS
v_result NUMBER;
BEGIN
IF p_id_gest IS NOT NULL THEN
-- Cu gestiune specifica — Oracle poate folosi index pe stoc(id_gestiune, an, luna)
BEGIN
SELECT id_articol INTO v_result FROM (
SELECT na.id_articol
FROM nom_articole na
WHERE na.codmat = p_codmat AND na.sters = 0 AND na.inactiv = 0
ORDER BY
CASE WHEN EXISTS (
SELECT 1 FROM stoc s
WHERE s.id_articol = na.id_articol
AND s.id_gestiune = p_id_gest
AND s.an = EXTRACT(YEAR FROM SYSDATE)
AND s.luna = EXTRACT(MONTH FROM SYSDATE)
AND s.cants + s.cant - s.cante > 0
) THEN 0 ELSE 1 END,
na.id_articol DESC
) WHERE ROWNUM = 1;
EXCEPTION WHEN NO_DATA_FOUND THEN v_result := NULL;
END;
ELSE
-- Fara gestiune — cauta stoc in orice gestiune
BEGIN
SELECT id_articol INTO v_result FROM (
SELECT na.id_articol
FROM nom_articole na
WHERE na.codmat = p_codmat AND na.sters = 0 AND na.inactiv = 0
ORDER BY
CASE WHEN EXISTS (
SELECT 1 FROM stoc s
WHERE s.id_articol = na.id_articol
AND s.an = EXTRACT(YEAR FROM SYSDATE)
AND s.luna = EXTRACT(MONTH FROM SYSDATE)
AND s.cants + s.cant - s.cante > 0
) THEN 0 ELSE 1 END,
na.id_articol DESC
) WHERE ROWNUM = 1;
EXCEPTION WHEN NO_DATA_FOUND THEN v_result := NULL;
END;
END IF;
RETURN v_result;
END resolve_id_articol;
-- ================================================================
-- Procedura principala pentru importul unei comenzi
-- ================================================================
@@ -99,6 +150,7 @@ CREATE OR REPLACE PACKAGE BODY PACK_IMPORT_COMENZI AS
p_id_adresa_facturare IN NUMBER DEFAULT NULL,
p_id_pol IN NUMBER DEFAULT NULL,
p_id_sectie IN NUMBER DEFAULT NULL,
p_id_gestiune IN NUMBER DEFAULT NULL,
v_id_comanda OUT NUMBER) IS
v_data_livrare DATE;
v_sku VARCHAR2(100);
@@ -203,8 +255,7 @@ CREATE OR REPLACE PACKAGE BODY PACK_IMPORT_COMENZI AS
-- Cauta mai intai in ARTICOLE_TERTI (mapari speciale / seturi)
v_found_mapping := FALSE;
FOR rec IN (SELECT at.codmat, at.cantitate_roa, at.procent_pret,
(SELECT MAX(na.id_articol) FROM nom_articole na WHERE na.codmat = at.codmat) AS id_articol
FOR rec IN (SELECT at.codmat, at.cantitate_roa, at.procent_pret
FROM articole_terti at
WHERE at.sku = v_sku
AND at.activ = 1
@@ -212,6 +263,14 @@ CREATE OR REPLACE PACKAGE BODY PACK_IMPORT_COMENZI AS
ORDER BY at.procent_pret DESC) LOOP
v_found_mapping := TRUE;
v_id_articol := resolve_id_articol(rec.codmat, p_id_gestiune);
IF v_id_articol IS NULL THEN
v_articole_eroare := v_articole_eroare + 1;
g_last_error := g_last_error || CHR(10) ||
'Articol activ negasit pentru CODMAT: ' || rec.codmat;
CONTINUE;
END IF;
v_cantitate_roa := rec.cantitate_roa * v_cantitate_web;
v_pret_unitar := CASE WHEN v_pret_web IS NOT NULL
THEN (v_pret_web * rec.procent_pret / 100) / rec.cantitate_roa
@@ -220,7 +279,7 @@ CREATE OR REPLACE PACKAGE BODY PACK_IMPORT_COMENZI AS
BEGIN
PACK_COMENZI.adauga_articol_comanda(V_ID_COMANDA => v_id_comanda,
V_ID_ARTICOL => rec.id_articol,
V_ID_ARTICOL => v_id_articol,
V_ID_POL => NVL(v_id_pol_articol, p_id_pol),
V_CANTITATE => v_cantitate_roa,
V_PRET => v_pret_unitar,
@@ -236,40 +295,34 @@ CREATE OR REPLACE PACKAGE BODY PACK_IMPORT_COMENZI AS
END;
END LOOP;
-- Daca nu s-a gasit mapare, cauta direct in NOM_ARTICOLE
-- Daca nu s-a gasit mapare, cauta direct in NOM_ARTICOLE via resolve_id_articol
IF NOT v_found_mapping THEN
BEGIN
SELECT id_articol, codmat
INTO v_id_articol, v_codmat
FROM nom_articole
WHERE codmat = v_sku
AND id_articol = (SELECT MAX(id_articol) FROM nom_articole WHERE codmat = v_sku);
v_id_articol := resolve_id_articol(v_sku, p_id_gestiune);
IF v_id_articol IS NULL THEN
v_articole_eroare := v_articole_eroare + 1;
g_last_error := g_last_error || CHR(10) ||
'SKU negasit in ARTICOLE_TERTI si NOM_ARTICOLE (activ): ' || v_sku;
ELSE
v_codmat := v_sku;
v_pret_unitar := NVL(v_pret_web, 0);
PACK_COMENZI.adauga_articol_comanda(V_ID_COMANDA => v_id_comanda,
V_ID_ARTICOL => v_id_articol,
V_ID_POL => NVL(v_id_pol_articol, p_id_pol),
V_CANTITATE => v_cantitate_web,
V_PRET => v_pret_unitar,
V_ID_UTIL => c_id_util,
V_ID_SECTIE => p_id_sectie,
V_PTVA => v_vat);
v_articole_procesate := v_articole_procesate + 1;
EXCEPTION
WHEN NO_DATA_FOUND THEN
v_articole_eroare := v_articole_eroare + 1;
g_last_error := g_last_error || CHR(10) ||
'SKU negasit in ARTICOLE_TERTI si NOM_ARTICOLE: ' || v_sku;
WHEN TOO_MANY_ROWS THEN
v_articole_eroare := v_articole_eroare + 1;
g_last_error := g_last_error || CHR(10) ||
'Multiple articole gasite pentru SKU: ' || v_sku;
WHEN OTHERS THEN
v_articole_eroare := v_articole_eroare + 1;
g_last_error := g_last_error || CHR(10) ||
'Eroare adaugare articol ' || v_sku || ' (CODMAT: ' || v_codmat || '): ' || SQLERRM;
END;
BEGIN
PACK_COMENZI.adauga_articol_comanda(V_ID_COMANDA => v_id_comanda,
V_ID_ARTICOL => v_id_articol,
V_ID_POL => NVL(v_id_pol_articol, p_id_pol),
V_CANTITATE => v_cantitate_web,
V_PRET => v_pret_unitar,
V_ID_UTIL => c_id_util,
V_ID_SECTIE => p_id_sectie,
V_PTVA => v_vat);
v_articole_procesate := v_articole_procesate + 1;
EXCEPTION
WHEN OTHERS THEN
v_articole_eroare := v_articole_eroare + 1;
g_last_error := g_last_error || CHR(10) ||
'Eroare adaugare articol ' || v_sku || ' (CODMAT: ' || v_codmat || '): ' || SQLERRM;
END;
END IF;
END IF;
END; -- End BEGIN block pentru articol individual