From 0ab83884fc0b4a95ae14c38f433e7180ad68907c Mon Sep 17 00:00:00 2001 From: Claude Agent Date: Wed, 25 Mar 2026 16:30:55 +0000 Subject: [PATCH] feat: add inventory note script for populating stock from imported orders Resolves all SKUs from imported GoMag orders directly against Oracle (ARTICOLE_TERTI + NOM_ARTICOLE), creates id_set=90103 inventory notes (DOCUMENTE + ACT + RUL + STOC) with configurable quantity and 30% markup pricing. Supports dry-run, --apply, and --yes flags. Co-Authored-By: Claude Opus 4.6 (1M context) --- scripts/create_inventory_notes.py | 494 ++++++++++++++++++++++++++++++ 1 file changed, 494 insertions(+) create mode 100644 scripts/create_inventory_notes.py diff --git a/scripts/create_inventory_notes.py b/scripts/create_inventory_notes.py new file mode 100644 index 0000000..053aee5 --- /dev/null +++ b/scripts/create_inventory_notes.py @@ -0,0 +1,494 @@ +#!/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), 4) + + 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()