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) <noreply@anthropic.com>
This commit is contained in:
494
scripts/create_inventory_notes.py
Normal file
494
scripts/create_inventory_notes.py
Normal file
@@ -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()
|
||||
Reference in New Issue
Block a user