import logging from .. import database logger = logging.getLogger(__name__) def check_orders_in_roa(min_date, conn) -> dict: """Check which orders already exist in Oracle COMENZI by date range. Returns: {comanda_externa: id_comanda} for all existing orders. Much faster than IN-clause batching — single query using date index. """ if conn is None: return {} existing = {} try: with conn.cursor() as cur: cur.execute(""" SELECT comanda_externa, id_comanda FROM COMENZI WHERE data_comanda >= :min_date AND comanda_externa IS NOT NULL AND sters = 0 """, {"min_date": min_date}) for row in cur: existing[str(row[0])] = row[1] except Exception as e: logger.error(f"check_orders_in_roa failed: {e}") logger.info(f"ROA order check (since {min_date}): {len(existing)} existing orders found") return existing 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) - direct: found in NOM_ARTICOLE by codmat (not in ARTICOLE_TERTI) - missing: not found anywhere - direct_id_map: {codmat: id_articol} for direct SKUs (saves a round-trip in validate_prices) """ if not skus: return {"mapped": set(), "direct": set(), "missing": set(), "direct_id_map": {}} mapped = set() sku_list = list(skus) own_conn = conn is None if own_conn: conn = database.get_oracle_connection() try: with conn.cursor() as cur: # Check in batches of 500 for i in range(0, len(sku_list), 500): batch = sku_list[i:i+500] placeholders = ",".join([f":s{j}" for j in range(len(batch))]) params = {f"s{j}": sku for j, sku in enumerate(batch)} # Check ARTICOLE_TERTI cur.execute(f""" SELECT DISTINCT sku FROM ARTICOLE_TERTI WHERE sku IN ({placeholders}) AND activ = 1 AND sters = 0 """, params) for row in cur: mapped.add(row[0]) # 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) missing = skus - mapped - direct logger.info(f"SKU validation: {len(mapped)} mapped, {len(direct)} direct, {len(missing)} missing") return {"mapped": mapped, "direct": direct, "missing": missing, "direct_id_map": direct_id_map} def classify_orders(orders, validation_result): """Classify orders as importable or skipped based on SKU validation. Returns: (importable_orders, skipped_orders) Each skipped entry is a tuple of (order, list_of_missing_skus). """ ok_skus = validation_result["mapped"] | validation_result["direct"] importable = [] skipped = [] for order in orders: order_skus = {item.sku for item in order.items if item.sku} order_missing = order_skus - ok_skus if order_missing: skipped.append((order, list(order_missing))) else: importable.append(order) return importable, skipped def validate_prices(codmats: set[str], id_pol: int, conn=None, direct_id_map: dict=None) -> dict: """Check which CODMATs have a price entry in CRM_POLITICI_PRET_ART for the given policy. If direct_id_map is provided, skips the NOM_ARTICOLE lookup for those CODMATs. Returns: {"has_price": set_of_codmats, "missing_price": set_of_codmats} """ if not codmats: return {"has_price": set(), "missing_price": set()} codmat_to_id = dict(direct_id_map) if direct_id_map else {} ids_with_price = set() own_conn = conn is None if own_conn: conn = database.get_oracle_connection() try: with conn.cursor() as cur: # 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] placeholders = ",".join([f":a{j}" for j in range(len(batch))]) params = {f"a{j}": aid for j, aid in enumerate(batch)} params["id_pol"] = id_pol cur.execute(f""" SELECT DISTINCT pa.ID_ARTICOL FROM CRM_POLITICI_PRET_ART pa WHERE pa.ID_POL = :id_pol AND pa.ID_ARTICOL IN ({placeholders}) """, params) for row in cur: ids_with_price.add(row[0]) finally: if own_conn: database.pool.release(conn) # Map back to CODMATs has_price = {cm for cm, aid in codmat_to_id.items() if aid in ids_with_price} missing_price = codmats - has_price logger.info(f"Price validation (policy {id_pol}): {len(has_price)} have price, {len(missing_price)} missing price") return {"has_price": has_price, "missing_price": missing_price} def ensure_prices(codmats: set[str], id_pol: int, conn=None, direct_id_map: dict=None): """Insert price 0 entries for CODMATs missing from the given price policy. Uses batch executemany instead of individual INSERTs. Relies on TRG_CRM_POLITICI_PRET_ART trigger for ID_POL_ART sequence. """ if not codmats: return own_conn = conn is None if own_conn: conn = database.get_oracle_connection() try: with conn.cursor() as cur: # Get ID_VALUTA for this policy cur.execute(""" SELECT ID_VALUTA FROM CRM_POLITICI_PRETURI WHERE ID_POL = :id_pol """, {"id_pol": id_pol}) row = cur.fetchone() if not row: logger.error(f"Price policy {id_pol} not found in CRM_POLITICI_PRETURI") return id_valuta = row[0] # Build batch params using direct_id_map (already resolved via resolve_codmat_ids) batch_params = [] codmat_id_map = dict(direct_id_map) if direct_id_map else {} for codmat in codmats: id_articol = codmat_id_map.get(codmat) if not id_articol: logger.warning(f"CODMAT {codmat} not found in NOM_ARTICOLE, skipping price insert") continue batch_params.append({ "id_pol": id_pol, "id_articol": id_articol, "id_valuta": id_valuta }) if batch_params: cur.executemany(""" INSERT INTO CRM_POLITICI_PRET_ART (ID_POL, ID_ARTICOL, PRET, ID_VALUTA, ID_UTIL, DATAORA, PROC_TVAV, PRETFTVA, PRETCTVA) VALUES (:id_pol, :id_articol, 0, :id_valuta, -3, SYSDATE, 1.19, 0, 0) """, batch_params) logger.info(f"Batch inserted {len(batch_params)} price entries for policy {id_pol}") conn.commit() finally: if own_conn: database.pool.release(conn) logger.info(f"Ensure prices done: {len(codmats)} CODMATs processed for policy {id_pol}")