#!/usr/bin/env python3 """ Create inventory notes (note de inventar) in Oracle to populate stock for articles from imported GoMag orders. Inserts into: DOCUMENTE, ACT, RUL, STOC (id_set=90103 pattern). Usage: python3 scripts/create_inventory_notes.py # dry-run (default) python3 scripts/create_inventory_notes.py --apply # apply with confirmation python3 scripts/create_inventory_notes.py --apply --yes # skip confirmation python3 scripts/create_inventory_notes.py --quantity 5000 --gestiune 1 """ import argparse import sqlite3 import sys from datetime import datetime from pathlib import Path import oracledb # ─── Configuration ─────────────────────────────────────────────────────────── SCRIPT_DIR = Path(__file__).resolve().parent PROJECT_DIR = SCRIPT_DIR.parent API_DIR = PROJECT_DIR / "api" SQLITE_DB = API_DIR / "data" / "import.db" TNS_DIR = str(API_DIR) ORA_USER = "MARIUSM_AUTO" ORA_PASSWORD = "ROMFASTSOFT" ORA_DSN = "ROA_CENTRAL" # Inventory note constants (from existing cod=1140718 pattern) ID_SET = 90103 ID_FDOC = 51 ID_UTIL = 8 ID_SECTIE = 6 ID_SUCURSALA = 167 ID_VALUTA = 3 ID_PARTC = 481 ID_TIP_RULAJ = 6 ADAOS_PERCENT = 0.30 # 30% markup # Gestiune defaults (MARFA PA) DEFAULT_GESTIUNE = 1 GEST_CONT = "371" GEST_ACONT = "816" # ─── Oracle helpers ────────────────────────────────────────────────────────── def get_oracle_conn(): return oracledb.connect( user=ORA_USER, password=ORA_PASSWORD, dsn=ORA_DSN, config_dir=TNS_DIR ) # ─── SQLite: get articles from imported orders ────────────────────────────── def get_all_skus_from_sqlite(): """Get ALL distinct SKUs from imported orders (regardless of mapping_status).""" conn = sqlite3.connect(str(SQLITE_DB)) cur = conn.cursor() cur.execute(""" SELECT DISTINCT oi.sku FROM order_items oi JOIN orders o ON o.order_number = oi.order_number WHERE o.status = 'IMPORTED' """) skus = {row[0] for row in cur.fetchall()} conn.close() return skus # ─── Oracle: resolve SKUs to articles ──────────────────────────────────────── def resolve_articles(ora_conn, all_skus): """Resolve SKUs to {codmat: {id_articol, cont, codmat}} via Oracle. Tries both mapped (ARTICOLE_TERTI) and direct (NOM_ARTICOLE) lookups. """ articles = {} # codmat -> {id_articol, cont, codmat} cur = ora_conn.cursor() sku_list = list(all_skus) # 1. Mapped: SKU -> codmat via articole_terti (priority) placeholders = ",".join(f":m{i}" for i in range(len(sku_list))) binds = {f"m{i}": sku for i, sku in enumerate(sku_list)} cur.execute(f""" SELECT at.codmat, na.id_articol, na.cont FROM articole_terti at JOIN nom_articole na ON na.codmat = at.codmat AND na.sters = 0 AND na.inactiv = 0 WHERE at.sku IN ({placeholders}) AND at.activ = 1 AND at.sters = 0 """, binds) mapped_skus = set() for codmat, id_articol, cont in cur: articles[codmat] = { "id_articol": id_articol, "cont": cont, "codmat": codmat } # Find which SKUs were resolved via mapping cur.execute(f""" SELECT DISTINCT at.sku FROM articole_terti at WHERE at.sku IN ({placeholders}) AND at.activ = 1 AND at.sters = 0 """, binds) mapped_skus = {row[0] for row in cur} # 2. Direct: remaining SKUs where SKU = codmat remaining = all_skus - mapped_skus if remaining: rem_list = list(remaining) placeholders = ",".join(f":s{i}" for i in range(len(rem_list))) binds = {f"s{i}": sku for i, sku in enumerate(rem_list)} cur.execute(f""" SELECT codmat, id_articol, cont FROM nom_articole WHERE codmat IN ({placeholders}) AND sters = 0 AND inactiv = 0 """, binds) for codmat, id_articol, cont in cur: if codmat not in articles: articles[codmat] = { "id_articol": id_articol, "cont": cont, "codmat": codmat } return articles def get_prices(ora_conn, articles): """Get sale prices from CRM_POLITICI_PRET_ART for each article. Returns {id_articol: {pret_vanzare, proc_tvav}} """ if not articles: return {} cur = ora_conn.cursor() id_articols = [a["id_articol"] for a in articles.values()] placeholders = ",".join(f":a{i}" for i in range(len(id_articols))) binds = {f"a{i}": aid for i, aid in enumerate(id_articols)} cur.execute(f""" SELECT pa.id_articol, pa.pret, pa.proc_tvav FROM crm_politici_pret_art pa WHERE pa.id_articol IN ({placeholders}) AND pa.pret > 0 AND ROWNUM <= 1000 """, binds) prices = {} for id_articol, pret, proc_tvav in cur: # Keep first non-zero price found if id_articol not in prices: prices[id_articol] = { "pret_vanzare": float(pret), "proc_tvav": float(proc_tvav) if proc_tvav else 1.19 } return prices def get_current_stock(ora_conn, articles, gestiune, year, month): """Check current stock levels. Returns {id_articol: available_qty}.""" if not articles: return {} cur = ora_conn.cursor() id_articols = [a["id_articol"] for a in articles.values()] placeholders = ",".join(f":a{i}" for i in range(len(id_articols))) binds = {f"a{i}": aid for i, aid in enumerate(id_articols)} binds["gest"] = gestiune binds["an"] = year binds["luna"] = month cur.execute(f""" SELECT id_articol, NVL(cants,0) + NVL(cant,0) - NVL(cante,0) as disponibil FROM stoc WHERE id_articol IN ({placeholders}) AND id_gestiune = :gest AND an = :an AND luna = :luna """, binds) stock = {} for id_articol, disponibil in cur: stock[id_articol] = float(disponibil) return stock # ─── Oracle: create inventory note ────────────────────────────────────────── def create_inventory_note(ora_conn, articles_to_insert, quantity, gestiune, year, month): """Insert DOCUMENTE + ACT + RUL + STOC for inventory note.""" cur = ora_conn.cursor() now = datetime.now() today = now.replace(hour=0, minute=0, second=0, microsecond=0) # Get sequences cur.execute("SELECT SEQ_COD.NEXTVAL FROM dual") cod = cur.fetchone()[0] cur.execute("SELECT SEQ_IDFACT.NEXTVAL FROM dual") id_fact = cur.fetchone()[0] # NNIR pattern: YYYYMM + 4-digit seq cur.execute("SELECT MAX(nnir) FROM act WHERE an = :an AND luna = :luna", {"an": year, "luna": month}) max_nnir = cur.fetchone()[0] or 0 nnir = max_nnir + 1 # NRACT: use a simple incrementing number cur.execute("SELECT MAX(nract) FROM act WHERE an = :an AND luna = :luna AND id_set = :s", {"an": year, "luna": month, "s": ID_SET}) max_nract = cur.fetchone()[0] or 0 nract = max_nract + 1 # 1. INSERT DOCUMENTE cur.execute(""" INSERT INTO documente (id_doc, dataora, id_util, sters, tva_incasare, nract, dataact, id_set, dataireg) VALUES (:id_doc, :dataora, :id_util, 0, 1, :nract, :dataact, :id_set, :dataireg) """, { "id_doc": id_fact, "dataora": now, "id_util": ID_UTIL, "nract": nract, "dataact": today, "id_set": ID_SET, "dataireg": today, }) inserted_count = 0 for art in articles_to_insert: pret = art["pret"] proc_tvav = art["proc_tvav"] suma = -(quantity * pret) # 2. INSERT ACT cur.execute(""" INSERT INTO act (cod, luna, an, dataireg, nract, dataact, scd, ascd, scc, ascc, suma, nnir, id_util, dataora, id_sectie, id_set, id_fact, id_partc, id_sucursala, id_fdoc, id_gestout, id_valuta) VALUES (:cod, :luna, :an, :dataireg, :nract, :dataact, '607', '7', :scc, :ascc, :suma, :nnir, :id_util, :dataora, :id_sectie, :id_set, :id_fact, :id_partc, :id_sucursala, :id_fdoc, :id_gestout, :id_valuta) """, { "cod": cod, "luna": month, "an": year, "dataireg": today, "nract": nract, "dataact": today, "scc": GEST_CONT, "ascc": GEST_ACONT, "suma": suma, "nnir": nnir, "id_util": ID_UTIL, "dataora": now, "id_sectie": ID_SECTIE, "id_set": ID_SET, "id_fact": id_fact, "id_partc": ID_PARTC, "id_sucursala": ID_SUCURSALA, "id_fdoc": ID_FDOC, "id_gestout": gestiune, "id_valuta": ID_VALUTA, }) # 3. INSERT RUL cur.execute(""" INSERT INTO rul (cod, an, luna, nnir, id_articol, id_gestiune, pret, cante, cont, acont, dataact, dataout, id_util, dataora, id_fact, proc_tvav, id_tip_rulaj, id_set, id_sucursala, nract, id_valuta) VALUES (:cod, :an, :luna, :nnir, :id_articol, :id_gestiune, :pret, :cante, :cont, :acont, :dataact, :dataout, :id_util, :dataora, :id_fact, :proc_tvav, :id_tip_rulaj, :id_set, :id_sucursala, :nract, :id_valuta) """, { "cod": cod, "an": year, "luna": month, "nnir": nnir, "id_articol": art["id_articol"], "id_gestiune": gestiune, "pret": pret, "cante": -quantity, "cont": GEST_CONT, "acont": GEST_ACONT, "dataact": today, "dataout": today, "id_util": ID_UTIL, "dataora": now, "id_fact": id_fact, "proc_tvav": proc_tvav, "id_tip_rulaj": ID_TIP_RULAJ, "id_set": ID_SET, "id_sucursala": ID_SUCURSALA, "nract": nract, "id_valuta": ID_VALUTA, }) # 4. MERGE STOC cur.execute(""" MERGE INTO stoc s USING (SELECT :id_articol AS id_articol, :id_gestiune AS id_gestiune, :an AS an, :luna AS luna FROM dual) src ON (s.id_articol = src.id_articol AND s.id_gestiune = src.id_gestiune AND s.an = src.an AND s.luna = src.luna AND s.pret = :pret AND s.cont = :cont AND s.acont = :acont) WHEN MATCHED THEN UPDATE SET s.cante = s.cante + (:cante), s.dataora = :dataora, s.dataout = :dataout WHEN NOT MATCHED THEN INSERT (id_articol, id_gestiune, an, luna, pret, cont, acont, cante, dataora, datain, dataout, proc_tvav, id_sucursala, id_valuta) VALUES (:id_articol, :id_gestiune, :an, :luna, :pret, :cont, :acont, :cante, :dataora, :datain, :dataout, :proc_tvav, :id_sucursala, :id_valuta) """, { "id_articol": art["id_articol"], "id_gestiune": gestiune, "an": year, "luna": month, "pret": pret, "cont": GEST_CONT, "acont": GEST_ACONT, "cante": -quantity, "dataora": now, "datain": today, "dataout": today, "proc_tvav": proc_tvav, "id_sucursala": ID_SUCURSALA, "id_valuta": ID_VALUTA, }) inserted_count += 1 ora_conn.commit() return cod, id_fact, nnir, nract, inserted_count # ─── Main ──────────────────────────────────────────────────────────────────── def main(): parser = argparse.ArgumentParser( description="Create inventory notes for GoMag order articles" ) parser.add_argument("--quantity", type=int, default=10000, help="Quantity per article (default: 10000)") parser.add_argument("--gestiune", type=int, default=DEFAULT_GESTIUNE, help=f"Warehouse ID (default: {DEFAULT_GESTIUNE})") parser.add_argument("--apply", action="store_true", help="Apply changes (default: dry-run)") parser.add_argument("--yes", action="store_true", help="Skip confirmation prompt") args = parser.parse_args() now = datetime.now() year, month = now.year, now.month print(f"=== Create Inventory Notes (id_set={ID_SET}) ===") print(f"Gestiune: {args.gestiune}, Quantity: {args.quantity}") print(f"Period: {year}/{month:02d}") print() # 1. Get SKUs from SQLite if not SQLITE_DB.exists(): print(f"ERROR: SQLite DB not found at {SQLITE_DB}") sys.exit(1) all_skus = get_all_skus_from_sqlite() print(f"SKUs from imported orders: {len(all_skus)} total") if not all_skus: print("No SKUs found. Nothing to do.") return # 2. Connect to Oracle and resolve ALL SKUs (mapped + direct) ora_conn = get_oracle_conn() articles = resolve_articles(ora_conn, all_skus) print(f"Resolved to {len(articles)} unique articles (codmat)") print(f"Unresolved: {len(all_skus) - len(articles)} SKUs (missing from Oracle)") if not articles: print("No articles resolved. Nothing to do.") ora_conn.close() return # 3. Get prices prices = get_prices(ora_conn, articles) # 4. Check current stock stock = get_current_stock(ora_conn, articles, args.gestiune, year, month) # 5. Build list of articles to insert articles_to_insert = [] skipped = [] for codmat, art in sorted(articles.items()): id_articol = art["id_articol"] current = stock.get(id_articol, 0) if current >= args.quantity: skipped.append((codmat, current)) continue price_info = prices.get(id_articol, {}) pret_vanzare = price_info.get("pret_vanzare", 1.30) proc_tvav = price_info.get("proc_tvav", 1.19) pret_achizitie = round(pret_vanzare / (1 + ADAOS_PERCENT), 2) articles_to_insert.append({ "codmat": codmat, "id_articol": id_articol, "pret": pret_achizitie, "pret_vanzare": pret_vanzare, "proc_tvav": proc_tvav, "current_stock": current, }) # 6. Display summary print() if skipped: print(f"Skipped {len(skipped)} articles (already have >= {args.quantity} stock):") for codmat, qty in skipped[:5]: print(f" {codmat}: {qty:.0f}") if len(skipped) > 5: print(f" ... and {len(skipped) - 5} more") print() if not articles_to_insert: print("All articles already have sufficient stock. Nothing to do.") ora_conn.close() return print(f"Articles to create stock for: {len(articles_to_insert)}") print(f"{'CODMAT':<25} {'ID_ARTICOL':>12} {'PRET_ACH':>10} {'PRET_VANZ':>10} {'TVA':>5} {'STOC_ACT':>10}") print("-" * 80) for art in articles_to_insert: tva_pct = round((art["proc_tvav"] - 1) * 100) print(f"{art['codmat']:<25} {art['id_articol']:>12} " f"{art['pret']:>10.2f} {art['pret_vanzare']:>10.2f} " f"{tva_pct:>4}% {art['current_stock']:>10.0f}") print("-" * 80) print(f"Total: {len(articles_to_insert)} articles x {args.quantity} qty each") if not args.apply: print("\n[DRY-RUN] No changes made. Use --apply to execute.") ora_conn.close() return # 7. Confirm and apply if not args.yes: answer = input(f"\nInsert {len(articles_to_insert)} articles with qty={args.quantity}? [y/N] ") if answer.lower() != "y": print("Cancelled.") ora_conn.close() return cod, id_fact, nnir, nract, count = create_inventory_note( ora_conn, articles_to_insert, args.quantity, args.gestiune, year, month ) print(f"\nDone! Created inventory note:") print(f" COD = {cod}") print(f" ID_FACT (documente.id_doc) = {id_fact}") print(f" NNIR = {nnir}") print(f" NRACT = {nract}") print(f" Articles inserted: {count}") print(f"\nVerify:") print(f" SELECT * FROM act WHERE cod = {cod};") print(f" SELECT * FROM rul WHERE cod = {cod};") ora_conn.close() if __name__ == "__main__": main()