Compare commits
2 Commits
dcc2c9f308
...
53862b2685
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53862b2685 | ||
|
|
adf5a9d96d |
16
CLAUDE.md
16
CLAUDE.md
@@ -90,6 +90,22 @@ Documentatie completa: [README.md](README.md)
|
||||
- Coloanele `factura_*` pe `orders` (SQLite), populate lazy din Oracle (`vanzari WHERE sters=0`)
|
||||
- Refresh complet: verifica facturi noi + facturi sterse + comenzi sterse din ROA
|
||||
|
||||
## Sync articole VENDING → MARIUSM_AUTO
|
||||
|
||||
```bash
|
||||
# Dry-run (arată diferențele fără să modifice)
|
||||
python3 scripts/sync_vending_to_mariusm.py
|
||||
|
||||
# Aplică cu confirmare
|
||||
python3 scripts/sync_vending_to_mariusm.py --apply
|
||||
|
||||
# Fără confirmare (automatizare)
|
||||
python3 scripts/sync_vending_to_mariusm.py --apply --yes
|
||||
```
|
||||
|
||||
Sincronizează via SSH din VENDING (prod Windows) în MARIUSM_AUTO (dev ROA_CENTRAL):
|
||||
nom_articole (noi by codmat, codmat updatat) + articole_terti (noi, modificate, soft-delete).
|
||||
|
||||
## Deploy Windows
|
||||
|
||||
Vezi [README.md](README.md#deploy-windows)
|
||||
|
||||
@@ -103,7 +103,7 @@ def _derive_customer_info(order):
|
||||
customer = shipping_name or billing_name
|
||||
payment_method = getattr(order, 'payment_name', None) or None
|
||||
delivery_method = getattr(order, 'delivery_name', None) or None
|
||||
return shipping_name, billing_name, customer, payment_method, delivery_method
|
||||
return shipping_name.upper(), billing_name.upper(), customer.upper(), payment_method, delivery_method
|
||||
|
||||
|
||||
async def _fix_stale_error_orders(existing_map: dict, run_id: str):
|
||||
|
||||
433
scripts/sync_vending_to_mariusm.py
Executable file
433
scripts/sync_vending_to_mariusm.py
Executable file
@@ -0,0 +1,433 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Sync nom_articole and articole_terti from VENDING (production Windows)
|
||||
to MARIUSM_AUTO (development ROA_CENTRAL).
|
||||
|
||||
Usage:
|
||||
python3 scripts/sync_vending_to_mariusm.py # dry-run (default)
|
||||
python3 scripts/sync_vending_to_mariusm.py --apply # apply changes
|
||||
python3 scripts/sync_vending_to_mariusm.py --apply --yes # skip confirmation
|
||||
|
||||
How it works:
|
||||
1. SSH to production Windows server, runs Python to extract VENDING data
|
||||
2. Connects locally to MARIUSM_AUTO on ROA_CENTRAL
|
||||
3. Compares and syncs:
|
||||
- nom_articole: new articles (by codmat), codmat updates on existing articles
|
||||
- articole_terti: new, modified, or soft-deleted mappings
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import subprocess
|
||||
import textwrap
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
import oracledb
|
||||
|
||||
# ─── Configuration ───────────────────────────────────────────────────────────
|
||||
|
||||
SSH_HOST = "gomag@79.119.86.134"
|
||||
SSH_PORT = "22122"
|
||||
VENDING_PYTHON = r"C:\gomag-vending\venv\Scripts\python.exe"
|
||||
VENDING_ORACLE_LIB = "C:/app/Server/product/18.0.0/dbhomeXE/bin"
|
||||
VENDING_USER = "VENDING"
|
||||
VENDING_PASSWORD = "ROMFASTSOFT"
|
||||
VENDING_DSN = "ROA"
|
||||
|
||||
MA_USER = "MARIUSM_AUTO"
|
||||
MA_PASSWORD = "ROMFASTSOFT"
|
||||
MA_DSN = "10.0.20.121:1521/ROA"
|
||||
|
||||
# Columns to sync for nom_articole (besides codmat which is the match key)
|
||||
NOM_SYNC_COLS = ["codmat", "denumire", "um", "cont", "codbare"]
|
||||
|
||||
# ─── Data classes ────────────────────────────────────────────────────────────
|
||||
|
||||
@dataclass
|
||||
class SyncReport:
|
||||
nom_new: list = field(default_factory=list)
|
||||
nom_codmat_updated: list = field(default_factory=list)
|
||||
at_new: list = field(default_factory=list)
|
||||
at_updated: list = field(default_factory=list)
|
||||
at_deleted: list = field(default_factory=list)
|
||||
errors: list = field(default_factory=list)
|
||||
|
||||
@property
|
||||
def has_changes(self):
|
||||
return any([self.nom_new, self.nom_codmat_updated,
|
||||
self.at_new, self.at_updated, self.at_deleted])
|
||||
|
||||
def summary(self):
|
||||
lines = ["=== Sync Report ==="]
|
||||
lines.append(f" nom_articole new: {len(self.nom_new)}")
|
||||
lines.append(f" nom_articole codmat updated: {len(self.nom_codmat_updated)}")
|
||||
lines.append(f" articole_terti new: {len(self.at_new)}")
|
||||
lines.append(f" articole_terti updated: {len(self.at_updated)}")
|
||||
lines.append(f" articole_terti deleted: {len(self.at_deleted)}")
|
||||
if self.errors:
|
||||
lines.append(f" ERRORS: {len(self.errors)}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
# ─── Remote extraction ───────────────────────────────────────────────────────
|
||||
|
||||
def ssh_run_python(script: str) -> str:
|
||||
"""Run a Python script on the production Windows server via SSH."""
|
||||
# Inline script as a single command argument
|
||||
cmd = [
|
||||
"ssh", "-p", SSH_PORT,
|
||||
"-o", "ConnectTimeout=10",
|
||||
"-o", "StrictHostKeyChecking=no",
|
||||
SSH_HOST,
|
||||
f"{VENDING_PYTHON} -c \"{script}\""
|
||||
]
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"SSH command failed:\n{result.stderr}")
|
||||
# Filter out PowerShell CLIXML noise
|
||||
lines = [l for l in result.stdout.splitlines()
|
||||
if not l.startswith("#< CLIXML") and not l.startswith("<Obj")]
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def extract_vending_data() -> tuple[list, list]:
|
||||
"""Extract nom_articole and articole_terti from VENDING via SSH."""
|
||||
print("Connecting to VENDING production via SSH...")
|
||||
|
||||
# Extract nom_articole
|
||||
nom_script = textwrap.dedent(f"""\
|
||||
import oracledb,json,sys
|
||||
oracledb.init_oracle_client(lib_dir='{VENDING_ORACLE_LIB}')
|
||||
conn = oracledb.connect(user='{VENDING_USER}',password='{VENDING_PASSWORD}',dsn='{VENDING_DSN}')
|
||||
cur = conn.cursor()
|
||||
cur.execute('SELECT id_articol,codmat,denumire,um,cont,codbare,sters,inactiv FROM nom_articole WHERE codmat IS NOT NULL')
|
||||
rows = [[r[0],r[1],r[2],r[3],r[4],r[5],r[6],r[7]] for r in cur.fetchall()]
|
||||
sys.stdout.write(json.dumps(rows))
|
||||
conn.close()
|
||||
""").replace("\n", ";").replace(";;", ";")
|
||||
|
||||
raw = ssh_run_python(nom_script)
|
||||
json_line = next((l for l in raw.splitlines() if l.startswith("[")), None)
|
||||
if not json_line:
|
||||
raise RuntimeError(f"No JSON in nom_articole output:\n{raw[:500]}")
|
||||
vending_nom = json.loads(json_line)
|
||||
print(f" VENDING nom_articole: {len(vending_nom)} rows with codmat")
|
||||
|
||||
# Extract articole_terti
|
||||
at_script = textwrap.dedent(f"""\
|
||||
import oracledb,json,sys
|
||||
oracledb.init_oracle_client(lib_dir='{VENDING_ORACLE_LIB}')
|
||||
conn = oracledb.connect(user='{VENDING_USER}',password='{VENDING_PASSWORD}',dsn='{VENDING_DSN}')
|
||||
cur = conn.cursor()
|
||||
cur.execute('SELECT sku,codmat,cantitate_roa,activ,sters FROM articole_terti')
|
||||
rows = [[r[0],r[1],float(r[2]) if r[2] else 1,r[3],r[4]] for r in cur.fetchall()]
|
||||
sys.stdout.write(json.dumps(rows))
|
||||
conn.close()
|
||||
""").replace("\n", ";").replace(";;", ";")
|
||||
|
||||
raw = ssh_run_python(at_script)
|
||||
json_line = next((l for l in raw.splitlines() if l.startswith("[")), None)
|
||||
if not json_line:
|
||||
raise RuntimeError(f"No JSON in articole_terti output:\n{raw[:500]}")
|
||||
vending_at = json.loads(json_line)
|
||||
print(f" VENDING articole_terti: {len(vending_at)} rows")
|
||||
|
||||
return vending_nom, vending_at
|
||||
|
||||
|
||||
# ─── Comparison ──────────────────────────────────────────────────────────────
|
||||
|
||||
def compare(vending_nom: list, vending_at: list, ma_conn) -> SyncReport:
|
||||
"""Compare VENDING data with MARIUSM_AUTO and build sync report."""
|
||||
report = SyncReport()
|
||||
cur = ma_conn.cursor()
|
||||
|
||||
# ── nom_articole ──
|
||||
# Get ALL MARIUSM_AUTO articles indexed by codmat and id_articol
|
||||
cur.execute("SELECT id_articol, codmat, denumire, sters, inactiv FROM nom_articole")
|
||||
ma_by_id = {}
|
||||
ma_by_codmat = {}
|
||||
for r in cur.fetchall():
|
||||
ma_by_id[r[0]] = {"codmat": r[1], "denumire": r[2], "sters": r[3], "inactiv": r[4]}
|
||||
if r[1]:
|
||||
ma_by_codmat[r[1]] = r[0] # codmat -> id_articol
|
||||
|
||||
print(f" MARIUSM_AUTO nom_articole: {len(ma_by_id)} total, {len(ma_by_codmat)} with codmat")
|
||||
|
||||
# vending_nom: [id_articol, codmat, denumire, um, cont, codbare, sters, inactiv]
|
||||
for row in vending_nom:
|
||||
v_id, v_codmat, v_den, v_um, v_cont, v_codbare, v_sters, v_inactiv = row
|
||||
if not v_codmat or v_sters or v_inactiv:
|
||||
continue
|
||||
|
||||
if v_codmat not in ma_by_codmat:
|
||||
# New article - codmat doesn't exist anywhere in MARIUSM_AUTO
|
||||
report.nom_new.append({
|
||||
"codmat": v_codmat,
|
||||
"denumire": v_den,
|
||||
"um": v_um,
|
||||
"cont": v_cont,
|
||||
"codbare": v_codbare,
|
||||
"vending_id": v_id,
|
||||
})
|
||||
else:
|
||||
# Article exists by codmat - check if codmat was updated on a
|
||||
# previously-null article (id match from VENDING)
|
||||
# This handles: same id_articol exists in MA but had NULL codmat
|
||||
if v_id in ma_by_id:
|
||||
ma_art = ma_by_id[v_id]
|
||||
if ma_art["codmat"] != v_codmat and ma_art["codmat"] is None:
|
||||
report.nom_codmat_updated.append({
|
||||
"id_articol": v_id,
|
||||
"old_codmat": ma_art["codmat"],
|
||||
"new_codmat": v_codmat,
|
||||
"denumire": v_den,
|
||||
})
|
||||
|
||||
# Also check: MARIUSM_AUTO articles that share id_articol with VENDING
|
||||
# but have different codmat (updated in VENDING)
|
||||
vending_by_id = {r[0]: r for r in vending_nom if not r[6] and not r[7]}
|
||||
for v_id, row in vending_by_id.items():
|
||||
v_codmat = row[1]
|
||||
if v_id in ma_by_id:
|
||||
ma_art = ma_by_id[v_id]
|
||||
if ma_art["codmat"] != v_codmat:
|
||||
# Don't duplicate entries already found above
|
||||
existing = [x for x in report.nom_codmat_updated if x["id_articol"] == v_id]
|
||||
if not existing:
|
||||
report.nom_codmat_updated.append({
|
||||
"id_articol": v_id,
|
||||
"old_codmat": ma_art["codmat"],
|
||||
"new_codmat": v_codmat,
|
||||
"denumire": row[2],
|
||||
})
|
||||
|
||||
# ── articole_terti ──
|
||||
cur.execute("SELECT sku, codmat, cantitate_roa, activ, sters FROM articole_terti")
|
||||
ma_at = {}
|
||||
for r in cur.fetchall():
|
||||
ma_at[(r[0], r[1])] = {"cantitate_roa": float(r[2]) if r[2] else 1, "activ": r[3], "sters": r[4]}
|
||||
|
||||
print(f" MARIUSM_AUTO articole_terti: {len(ma_at)} rows")
|
||||
|
||||
# vending_at: [sku, codmat, cantitate_roa, activ, sters]
|
||||
vending_at_keys = set()
|
||||
for row in vending_at:
|
||||
sku, codmat, qty, activ, sters = row
|
||||
key = (sku, codmat)
|
||||
vending_at_keys.add(key)
|
||||
|
||||
if key not in ma_at:
|
||||
report.at_new.append({
|
||||
"sku": sku, "codmat": codmat,
|
||||
"cantitate_roa": qty, "activ": activ, "sters": sters,
|
||||
})
|
||||
else:
|
||||
existing = ma_at[key]
|
||||
changes = {}
|
||||
if existing["cantitate_roa"] != qty:
|
||||
changes["cantitate_roa"] = (existing["cantitate_roa"], qty)
|
||||
if existing["activ"] != activ:
|
||||
changes["activ"] = (existing["activ"], activ)
|
||||
if existing["sters"] != sters:
|
||||
changes["sters"] = (existing["sters"], sters)
|
||||
if changes:
|
||||
report.at_updated.append({
|
||||
"sku": sku, "codmat": codmat, "changes": changes,
|
||||
"new_qty": qty, "new_activ": activ, "new_sters": sters,
|
||||
})
|
||||
|
||||
# Soft-delete: MA entries not in VENDING (only active ones)
|
||||
for key, data in ma_at.items():
|
||||
if key not in vending_at_keys and data["activ"] == 1 and data["sters"] == 0:
|
||||
report.at_deleted.append({"sku": key[0], "codmat": key[1]})
|
||||
|
||||
return report
|
||||
|
||||
|
||||
# ─── Apply changes ───────────────────────────────────────────────────────────
|
||||
|
||||
def apply_changes(report: SyncReport, ma_conn) -> SyncReport:
|
||||
"""Apply sync changes to MARIUSM_AUTO."""
|
||||
cur = ma_conn.cursor()
|
||||
|
||||
# ── nom_articole: insert new ──
|
||||
for art in report.nom_new:
|
||||
try:
|
||||
cur.execute("""
|
||||
INSERT INTO nom_articole
|
||||
(codmat, denumire, um, cont, codbare,
|
||||
sters, inactiv, dep, id_subgrupa, cant_bax,
|
||||
id_mod, in_stoc, in_crm, dnf)
|
||||
VALUES
|
||||
(:codmat, :denumire, :um, :cont, :codbare,
|
||||
0, 0, 0, 0, 1,
|
||||
0, 1, 0, 0)
|
||||
""", {
|
||||
"codmat": art["codmat"],
|
||||
"denumire": art["denumire"],
|
||||
"um": art["um"],
|
||||
"cont": art["cont"],
|
||||
"codbare": art["codbare"],
|
||||
})
|
||||
except Exception as e:
|
||||
report.errors.append(f"nom_articole INSERT {art['codmat']}: {e}")
|
||||
|
||||
# ── nom_articole: update codmat ──
|
||||
for upd in report.nom_codmat_updated:
|
||||
try:
|
||||
cur.execute("""
|
||||
UPDATE nom_articole SET codmat = :codmat
|
||||
WHERE id_articol = :id_articol
|
||||
""", {"codmat": upd["new_codmat"], "id_articol": upd["id_articol"]})
|
||||
except Exception as e:
|
||||
report.errors.append(f"nom_articole UPDATE {upd['id_articol']}: {e}")
|
||||
|
||||
# ── articole_terti: insert new ──
|
||||
for at in report.at_new:
|
||||
try:
|
||||
cur.execute("""
|
||||
INSERT INTO articole_terti
|
||||
(sku, codmat, cantitate_roa, activ, sters,
|
||||
data_creare, id_util_creare)
|
||||
VALUES
|
||||
(:sku, :codmat, :cantitate_roa, :activ, :sters,
|
||||
SYSDATE, 0)
|
||||
""", at)
|
||||
except Exception as e:
|
||||
report.errors.append(f"articole_terti INSERT {at['sku']}->{at['codmat']}: {e}")
|
||||
|
||||
# ── articole_terti: update modified ──
|
||||
for at in report.at_updated:
|
||||
try:
|
||||
cur.execute("""
|
||||
UPDATE articole_terti
|
||||
SET cantitate_roa = :new_qty,
|
||||
activ = :new_activ,
|
||||
sters = :new_sters,
|
||||
data_modif = SYSDATE,
|
||||
id_util_modif = 0
|
||||
WHERE sku = :sku AND codmat = :codmat
|
||||
""", at)
|
||||
except Exception as e:
|
||||
report.errors.append(f"articole_terti UPDATE {at['sku']}->{at['codmat']}: {e}")
|
||||
|
||||
# ── articole_terti: soft-delete removed ──
|
||||
for at in report.at_deleted:
|
||||
try:
|
||||
cur.execute("""
|
||||
UPDATE articole_terti
|
||||
SET sters = 1, activ = 0,
|
||||
data_modif = SYSDATE, id_util_modif = 0
|
||||
WHERE sku = :sku AND codmat = :codmat
|
||||
""", at)
|
||||
except Exception as e:
|
||||
report.errors.append(f"articole_terti DELETE {at['sku']}->{at['codmat']}: {e}")
|
||||
|
||||
if report.errors:
|
||||
print(f"\n{len(report.errors)} errors occurred, rolling back...")
|
||||
ma_conn.rollback()
|
||||
else:
|
||||
ma_conn.commit()
|
||||
print("\nCOMMIT OK")
|
||||
|
||||
return report
|
||||
|
||||
|
||||
# ─── Display ─────────────────────────────────────────────────────────────────
|
||||
|
||||
def print_details(report: SyncReport):
|
||||
"""Print detailed changes."""
|
||||
if report.nom_new:
|
||||
print(f"\n--- nom_articole NEW ({len(report.nom_new)}) ---")
|
||||
for art in report.nom_new:
|
||||
print(f" codmat={art['codmat']:20s} um={str(art.get('um','')):5s} "
|
||||
f"cont={str(art.get('cont','')):5s} {art['denumire']}")
|
||||
|
||||
if report.nom_codmat_updated:
|
||||
print(f"\n--- nom_articole CODMAT UPDATED ({len(report.nom_codmat_updated)}) ---")
|
||||
for upd in report.nom_codmat_updated:
|
||||
print(f" id={upd['id_articol']} {upd['old_codmat']} -> {upd['new_codmat']} {upd['denumire']}")
|
||||
|
||||
if report.at_new:
|
||||
print(f"\n--- articole_terti NEW ({len(report.at_new)}) ---")
|
||||
for at in report.at_new:
|
||||
print(f" {at['sku']:20s} -> {at['codmat']:20s} qty={at['cantitate_roa']}")
|
||||
|
||||
if report.at_updated:
|
||||
print(f"\n--- articole_terti UPDATED ({len(report.at_updated)}) ---")
|
||||
for at in report.at_updated:
|
||||
for col, (old, new) in at["changes"].items():
|
||||
print(f" {at['sku']:20s} -> {at['codmat']:20s} {col}: {old} -> {new}")
|
||||
|
||||
if report.at_deleted:
|
||||
print(f"\n--- articole_terti SOFT-DELETED ({len(report.at_deleted)}) ---")
|
||||
for at in report.at_deleted:
|
||||
print(f" {at['sku']:20s} -> {at['codmat']:20s}")
|
||||
|
||||
if report.errors:
|
||||
print(f"\n--- ERRORS ({len(report.errors)}) ---")
|
||||
for e in report.errors:
|
||||
print(f" {e}")
|
||||
|
||||
|
||||
# ─── Main ────────────────────────────────────────────────────────────────────
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Sync nom_articole & articole_terti from VENDING to MARIUSM_AUTO")
|
||||
parser.add_argument("--apply", action="store_true",
|
||||
help="Apply changes (default is dry-run)")
|
||||
parser.add_argument("--yes", "-y", action="store_true",
|
||||
help="Skip confirmation prompt")
|
||||
args = parser.parse_args()
|
||||
|
||||
# 1. Extract from VENDING
|
||||
vending_nom, vending_at = extract_vending_data()
|
||||
|
||||
# 2. Connect to MARIUSM_AUTO
|
||||
print("Connecting to MARIUSM_AUTO...")
|
||||
ma_conn = oracledb.connect(user=MA_USER, password=MA_PASSWORD, dsn=MA_DSN)
|
||||
|
||||
# 3. Compare
|
||||
print("Comparing...")
|
||||
report = compare(vending_nom, vending_at, ma_conn)
|
||||
|
||||
# 4. Display
|
||||
print(report.summary())
|
||||
if not report.has_changes:
|
||||
print("\nNothing to sync — already up to date.")
|
||||
ma_conn.close()
|
||||
return
|
||||
|
||||
print_details(report)
|
||||
|
||||
# 5. Apply or dry-run
|
||||
if not args.apply:
|
||||
print("\n[DRY-RUN] No changes applied. Use --apply to execute.")
|
||||
ma_conn.close()
|
||||
return
|
||||
|
||||
if not args.yes:
|
||||
answer = input("\nApply these changes? [y/N] ").strip().lower()
|
||||
if answer != "y":
|
||||
print("Aborted.")
|
||||
ma_conn.close()
|
||||
return
|
||||
|
||||
print("\nApplying changes...")
|
||||
apply_changes(report, ma_conn)
|
||||
|
||||
# 6. Verify
|
||||
cur = ma_conn.cursor()
|
||||
cur.execute("SELECT COUNT(*) FROM nom_articole WHERE sters=0 AND inactiv=0")
|
||||
print(f" nom_articole active: {cur.fetchone()[0]}")
|
||||
cur.execute("SELECT COUNT(*) FROM articole_terti WHERE activ=1 AND sters=0")
|
||||
print(f" articole_terti active: {cur.fetchone()[0]}")
|
||||
|
||||
ma_conn.close()
|
||||
print("Done.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user